개요
스프링 트랜잭션의 적용 위치, 옵션 AOP 주의 사항을 학습합니다.
@Transactional 동작 확인하기
@Transactional으로 프록시 방식의 AOP를 이용한 트랜잭션 작동은 아래와 같습니다. 트랜잭션 시작 시 트랜잭션 매니저는 데이터 소스에서 커넥션을 만들고 트랜잭션 동기화 매니저에 커넥션을 보관합니다. 트랜잭션 로직을 처리할 때 트랜잭션 동기화 매니저에 보관된 커넥션을 사용하고, 트랜잭션을 모두 종료시키고 커넥션을 반납합니다.
스프링에서 선언적 트랜잭션인 @Transactional를 예시 코드로 설명하겠습니다.
@Slf4j
@SpringBootTest
public class TxBasicTest {
...
@Test
void txTest() {
basicService.tx();
basicService.nonTx();
}
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
- basicService.tx() 호출
@Transactional이 적용되어 있기 때문에, basicService$$CGLIB 프록시는 트랜잭션을 시작하고 basicService.tx()를 호출합니다. 호출이 끝나면 프록시로 제어가 리턴하여 프록시는 트랜잭션을 커밋하거나 롤백해서 트랜잭션을 종료합니다.
- basicService.nonTx() 호출
@Transactional이 적용되어 있지 않기 때문에, basicService$$CGLIB 프록시는 트랜잭션을 시작하지 않고 단순히 basicService.nonTx()를 호출하고 종료합니다.
TransactionSynchronizationManager.isActualTransactionActive()로 선언적 트랜잭션 @Transactional이 해당 메서드에 선언되어 있는지 아닌지 알 수 있습니다.
@Transactional이 클래스 혹은 메서드에 있으면 트랜잭션 AOP는 프록시를 만들어 스프링 컨테이너에 등록합니다. tx() 메서드에 @Transactional이 있으므로 basicService$$CGLIB 프록시를 생성해서 스프링 컨테이너에 등록합니다. basicService에 의존성 주입을 하더라도 basicService가 아닌 basicService$$CGLIB 프록시가 스프링 빈으로 등록됩니다. basicService는 프록시에 의해 참조됩니다.
결과 값은 아래와 같습니다.
#tx() 호출
TransactionInterceptor : Getting transaction for [..BasicService.tx]
y.TxBasicTest$BasicService : call tx
y.TxBasicTest$BasicService : tx active=true
TransactionInterceptor : Completing transaction for
[..BasicService.tx]
#nonTx() 호출
y.TxBasicTest$BasicService : call nonTx
y.TxBasicTest$BasicService : tx active=false
@Transactional을 가지고 있는 tx() 메서드는 tx active=true 입니다.
@Transactional이 없는 nonTx() 메서드는 tx active=false 입니다.
트랜잭션 적용 위치
@Slf4j
@Transactional(readOnly = true)
static class LevelService {
@Transactional(readOnly = false)
public void write() {
log.info("call write");
printTxInfo();
}
public void read() {
log.info("call read");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}", readOnly);
}
...
}
@Transactional 설정이 구체적일수록 적용되는 우선순위가 높습니다.
- LevelService
@Transactional(readOnly = true)가 Service에 적용되어 있으며, 이는 Service 내에 있는 모든 메서드에 동일하게 적용됩니다.
- write()
@Transactional(readOnly = false)가 메서드에 적용되어 있으며, LevelService라는 클래스보다 구체적인 메서드에 적용되어 있으므로 @Transactional(readOnly = false)가 적용됩니다.
- read()
아무런 설정이 없다면, 클래스에 선언되어 있는 @Transactional(readOnly = true)가 적용됩니다.
스프링 트랜잭션 주의사항
@Transactional을 사용할 때 주의사항이 있습니다. @Transactional이 없는 메서드에서 같은 클래스에 있는 @Transactional이 있는 메서드를 호출할 때 @Transactional이 적용되지 않습니다.
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall() {
callService.external();
}
@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);
}
}
- internal() 호출
internal()을 호출하면, @Transactional이 선언되어 있기 때문에 callService 프록시가 트랜잭션을 시작하고 internal()을 호출하며, 호출이 끝나면 트랜잭션을 종료합니다.
- external() 호출
external()을 호출하면, @Transcational이 없기 때문에 callService 프록시는 트랜잭션을 시작하지 않고 external()을 호출합니다. 그리고 internal()을 호출하는데 비록 internal()에 @Transactional이 선언되어 있더라도, 여기서는 internal()이 트랜잭션에 적용되지 않습니다. 왜냐하면 external()이 실행된 이후 internal()은 this.internal() 호출되는데 this는 현재 객체를 가리키므로 callService AOP Proxy가 아닌 target에서 바로 internal()이 실행되기 때문입니다. 이는 callService 프록시를 전혀 거치지 않고 바로 target에 있는 internal()을 실행합니다.
- 문제 해결 방법(새로운 크래스로 메서드 분리)
이 문제를 해결하기 위해서는 external() 내부에서 internal()을 실행하는 것이 아니라 internal()를 별도의 클래스에서 분리하여 호출해야 합니다. 아래와 같이 internalService 클래스에 @Transactional이 있는 internal()를 생성하고 internalService.internal()을 호출하면 정상적으로 트랜잭션이 적용됩니다.
public에만 적용되는 @Transactional
@Transactional은 public 메서드에서만 유효하도록 스프링에서 설정되어 있습니다. 모든 수준의 접근 제어자에 트랜잭션 설정이 유효하다면 의도하지 않은 곳까지 트랜잭션이 과도하게 적용됩니다.
트랜잭션 초기화 조심하기
트랜잭션은 @PostConstruct와 @Transactional을 동시에 사용할 수 없습니다. 왜냐하면, @PostConstruct는 @Transactional로 인해 AOP가 준비되기 이전에 빈이 생성되어 있는 시점에서 바로 작동하기 때문입니다. 따라서 @EventListener(value = ApplicationReadyEvent.class)로 스프링 빈 초기화와 AOP 사용을 위한 모든 준비가 완료된 이후에 실행을 하도록 해야 합니다.
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void init2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
rollbackFor
스프링에서 트랜잭션 기본 정책은 다음과 같습니다. 언체크 예외는 롤백을 하고, 체크 예외는 롤백을 하지 않고 커밋합니다. 특정 예외 발생 시 롤백을 하도록 설정할 수 있습니다.
@Transactional(rollbackFor = Exception.class)
비록 Exception.class가 체크 예외이더라도 위의 설정을 통해 롤백 설정을 할 수 있습니다.
'Spring' 카테고리의 다른 글
스프링 AOP (3) - 실무 주의사항 (0) | 2023.02.02 |
---|---|
스프링 AOP (2) - AOP 구현 및 예제 (0) | 2023.01.12 |
트랜잭션의 역사 (0) | 2022.09.04 |
@Controller @RestContrller (0) | 2022.08.25 |
@Controller, @Service, @Repository 차이 (0) | 2022.08.25 |