Post

Spring Transaction 1

스프링 트랜잭션 추상화

데이터 접근 기술들은 트랜잭션을 처리하는 코드 자체가 다르다. 아래는 JPA와 JDBC의 트랜잭션 코드 예시이다.

1
2
3
4
5
6
7
8
9
10
11
// JPA 트랜잭션
EntityTransaction txn = entityManager.getTransaction();
txn.begin();
...
txn.commit();

// JDBC 트랜잭션
Connection con = ...
con.setAutoCommit(false);
...
con.commit();

데이터 접근 기술을 바뀐다면 트랜잭션 관련 코드를 개발자가 모두 수정해야 한다. 스프링은 이런 문제를 해결하기 위해 PlatformTransactionManager 인터페이스를 만들어 트랜잭션 코드를 추상화했다.
해당 인터페이스의 구현체는 스프링 부트가 데이터 접근 기술을 자동으로 인식해서 적절한 구현체를 빈으로 등록시켜준다. 따라서 개발자는 PlatformTransactionManager 인터페이스만 잘 알고있으면 된다.

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

스프링 트랜잭션 사용 방식

선언적 트랜잭션 관리(Declarative Transaction Management)

  • @Transactional 애노테이션을 이용해 AOP를 적용시킨다.
  • 대부분 이 방식을 사용 (아래 모든 내용은 선언적 트랜잭션을 전제로 설명한다.)
  • 스프링이 자동으로 비즈니스 로직 앞, 뒤에 트랜잭션 관련 코드를 넣은 프록시 클래스를 빈으로 등록한다
  • AOP 관련해서는 이후에 다른 게시물로 알아볼 예정이다.

프로그래밍 방식의 트랜잭션 관리(Programmatic Transaction Management)

  • 트랜잭션 매니저나 트랜잭션 템플릿 등을 사용해서 직접 관련 코드를 작성하는 방식
  • 애플리케이션 코드(비즈니스 로직)이 트랜잭션 코드와 강하게 결합되기 때문에 주로 사용되지 않는다.
1
2
3
4
5
6
7
8
9
10
// TransactionTemplate을 사용하는 방법도 존재한다. (맨 아래 cf 참고)
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // 트랜잭션 시작
try {
    //비즈니스 로직
    bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백
    throw new IllegalStateException(e);
}

트랜잭션 적용 우선순위

스프링은 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다. 트랜잭션뿐만 아니라 대부분의 우선순위 처리를 동일한 원칙으로 다룬다.
트랜잭션의 경우 아래와 같은 우선순위를 가진다.

  1. 클래스의 메서드 (우선순위가 가장 높다.)
  2. 클래스의 타입
  3. 인터페이스의 메서드
  4. 인터페이스의 타입 (우선순위가 가장 낮다.) readOnly와 같은 옵션값은 항상 높은 우선순위의 값을 따르게 된다. 또한 인터페이스에도 @Transactional을 사용할 순 있지만 권장하는 방식은 아니라고 한다.


트랜잭션 AOP 주의 사항 - 프록시 내부 호출(중요)

@Transactional를 사용하는 트랜잭션 AOP는 프록시를 사용한다. 여기서 생기는 한계점이 있는데, 바로 메서드 내부 호출은 프록시 코드가 적용되지 않는다는 점이다. 아래 코드를 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
    @Autowired
    CallService callService;

    @Test
    void externalCall() {
        callService.external();
    }

    @TestConfiguration
    static class InternalCallV1Config {
        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    @Slf4j
    static class CallService {
        public void external() {
            log.info("call external");
            printTxInfo();
            internal();
        }

        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive =
                    TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active={}", txActive);
        }
    }
}
/*
CallService   : call external
CallService   : tx active=false
CallService   : call internal
CallService   : tx active=false
*/

internal() 메서드에 트랜잭션을 걸었음에도 실제로는 걸리지 않았다. external()에서 내부 호출을 사용하고 있기 때문이다. 아래 그림을 참고하자.

이런 문제를 해결하기 위한 가장 간단한 방법은 internal() 메서드를 별도의 클래스로 분리하는 것이다.

참고로 스프링 AOP는 public 메서드에만 트랜잭션이 적용된다. 스프링 부트 3.0부터는 protected, default 메서드에서도 트랜잭션이 적용된다. 골자는 private 메서드에는 AOP가 적용되지 않는다는 것이다.(트랜잭션 적용이 무시된다.)

두번째 참고로는, @PostContruct가 붙은 메서드가 트랜잭션 AOP보다 빨리 호출된다. 따라서 해당 메서드에는 트랜잭션을 적용할 수 없다.
해결 방법으로는 @PostContruct 대신 @EventListener(value = ApplicationReadyEvent.class)을 사용하면 된다. 해당 애노테이션은 모든 스프링 컨테이너가 완전히 생성된 이후에 호출되게 한다.

세번째 참고로는, readOnly 옵션이다. 데이터 읽기만 존재하는 경우 readOnly=true옵션을 통해 성능 최적화가 발생한다는 점은 알고있는 사실이다.
궁금한 점은 @Transactional을 아예 붙이지 않는 것과의 성능비교이다. 영한님 피셜로는 readOnly가 DB 내부 최적화를 수행해서 성능상 이점이 있을수도 있고, 오히려 네트워크 통신이 1번 더 일어나서 성능상 불리한 경우도 있다고 한다. 따라서 DB마다 다르므로 성능 테스트 후에 결정해야 한다고 한다.

예외와 트랜잭션 커밋, 롤백

스프링 트랜잭션 AOP는 예외 종류에 따라 트랜잭션을 커밋하거나 롤백한다.

  1. 언체크 예외(RuntimeException, Error) : 트랜잭션 롤백
  2. 체크 예외(Exception) : 트랜잭션 커밋
  3. 정상 응답 : 트랜잭션 커밋

스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 언체크 예외는 복구 불가능한 예외로 가정한다.
이때 애노테이션 옵션으로 rollbackFor을 사용하면 체크 예외더라도 트랜잭션 롤백을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 런타임 예외 발생: 롤백
@Transactional
public void runtimeException() {
    throw new RuntimeException();
}

//체크 예외 발생: 커밋
@Transactional
public void checkedException() throws Exception {
    throw new Exception();
}


//체크 예외 rollbackFor 지정: 롤백
@Transactional(rollbackFor = Exception.class)
public void rollbackFor() throws Exception {
    throw new Exception();
}

CF. TransactionTemplate

스프링에서는 전략 패턴 중 특별한 케이스인 템플릿 콜백 패턴을 많이 사용한다. 트랜잭션 처리를 편리하게 도와주는 TransactionTemplate 도 전략패턴으로 구현되어 있다.

1
2
3
4
5
6
7
8
PlatformTransactionManager transactionManager = ...;

TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); // 템플릿
transactionTemplate.execute(status -> { // 콜백
            orderRepository.save(order);
            return order;
        }
);

코드 내부에서는 인자로 주어진 콜백 메서드를 실행하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TransactionTemplate.java
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    ...
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {
        result = action.doInTransaction(status);
    }
    catch (RuntimeException | Error ex) {
        // Transactional code threw application exception -> rollback
        rollbackOnException(status, ex);
        throw ex;
    }
    catch (Throwable ex) {
        // Transactional code threw unexpected exception -> rollback
        rollbackOnException(status, ex);
        throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
    }
    this.transactionManager.commit(status);
    return result;
}

Reference

김영한 스프링 DB 2편 - 데이터 접근 활용 기술
토비의 스프링 6 - 이해와 원리

This post is licensed under CC BY 4.0 by the author.