이 글은 김영한님의 스프링 DB 2편 강의중 제목과 관련된 부분을 블로그장의 취향대로 요약한 것이며 강의 자료 및 출처는 가장 아래에서 확인할 수 있습니다.
1. 트랜잭션AOP의 프록시 등록 원리
특정 클래스나 메서드에 @Transactional 어노테이션이 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어서 컨테이너에 등록한다.
내부 프록시 객체는 실제 객체를 상속 받고 있는 형태가 된다.
우선순위
DI를 공부할때도 봤었지만 스프링은 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
따라서 @Transactional 또한 우선순위 규칙이 적용된다.
* @Transactional의 규칙
1. 우선순위 규칙
예를 들어 A클래스에 @Transactional(readOnly = true) , A클래스의 A메서드에 @Transactional(readOnly = false)가 붙어있는 상태에서 A메서드를 사용한다면 메서드가 클래스보다 더 구체적이므로 @Transactional(readOnly=false)가 적용된다.
2. 클래스에 적용하면 메서드는 자동 적용
클래스에 만약 @Transactional이 존재한다면 메서드들은 자동으로 @Transactional이 설정된다.
2. 트랜잭션 AOP의 주의 사항 - 프록시 내부 호출
아래와 같은 클래스가 있다.
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(); // 현재 쓰레드에 트랜잭션이 적용되어있는지 boolean값으로 리턴
log.info("tx active={}", txActive);
}
}
@Transactional이 internal()에만 적용되어 있다.
여기까지 알수있는 점은 현재 CallService에 @Transactional이 하나라도 있기때문에 CallService 트랜잭션 프록시가 컨테이너에 등록되어있다는 점이다.
@Test
void internalCall(){
callService.internal();
}
internal()을 실행하게되면 internal()에 @Transactional이 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
따라서 결과적으로 로그는 tx active=true 로 찍히며, 트랜잭션이 적용되어 있음을 확인할 수 있다.
@Test
void externalCall(){
callService.external();
}
external()은 @Transactional이 없어서 트랜잭션 프록시는 트랜잭션을 적용하지 않는다.
중간에 internal()을 만나서 internal()을 호출하게 되고, internal()은 @Transactional이 있기에 트랜잭션이 적용될 것 처럼 예상한다.
그러나 결과적으로 로그는 external() 과 internal() 모두 tx active = false 가 찍히게된다.
프록시 객체의 external()을 호출 후, 트랜잭션 적용이 되지 않아 실제 external()을 호출하는 것 까지(위 그림에서1~3번) 예상한 흐름이다.
그러나 다음으로 실제 external()은 프록시 객체의 internal()을 호출하는 것이 아닌 실제 객체의 internal()을 호출한다.(4번)
internal()엔 @Transactional이 있기때문에 프록시 internal()을 호출해야 할것 같지만 그렇지 않다.
이는, 자바에서 메서드 앞에 별도의 참조가 없으면 this로 자기 자신을 가리키기 때문에, this.internal() 즉, 실제 객체 대상(target)을 호출하기 때문이다. 다시 말해 실제 객체의 external()을 호출한 상태에서, internal()을 호출하면 실제 internal()이 호출된다.
결론적으로 위는 프록시 방식의 AOP한계로, 트랜잭션을 적용하지 않은 상태에서 내부 호출을 하게 되면 내부 호출된 메서드에 트랜잭션을 적용해도 트랜잭션이 적용 되지 않는다.
3. 프록시 내부 호출을 해결하는 법
위 2번에서 문제는 this의 실제 대상 객체 호출이였다. 이를 해결하는 법은 간단하게 생각해서 this를 쓰지 않도록 클래스를 분리해버리면 된다.
따라서 internal()메서드를 InternalService 클래스를 만들어 내부로 옮겨주었다.
@Slf4j
@RequiredArgsConstructor
static class CallService{
private final InternalService internalService;
public void external(){
log.info("call external");
printTxInfo();
internalService.internal();
}
private void printTxInfo(){
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
@Slf4j
static class InternalService{
@Transactional
public void internal(){
log.info("call internal");
printTxInfo();
}
private void printTxInfo(){
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
CallService 클래스 내부에는 @Transactional이 없으므로 트랜잭션 프록시가 적용되지 않는다.
반대로 InternalService 클래스 내부엔 @Transactional이 있으므로 트랜잭션 프록시가 적용된다.
@Test
void externalCallV2(){
callService.external();
}
결과적으로 로그는 external - tx active=false , internal - tx active=true로 정상적으로 internal()은 트랜잭션이 적용된 것을 확인할 수 있다.
2번에선 내부 호출로 인해 트랜잭션을 적용하지 않은 상태에서 프록시 객체를 조회할 수 없었다.
그러나 위는 내부 호출이 없으므로 external()이 실행되고, 프록시 객체의 internal()을 정상 호출하게된다.
4. 프록시 AOP 주의 사항 - 초기화 시점
스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있어 주의가 필요하다.
@Slf4j
static class Hello{
/**
* @Transactional이 적용되지 않는다.
* 왜냐하면 초기화 코드(@PostConstruct)가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다.
*/
@PostConstruct
@Transactional
public void initV1(){
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
/**
* 따라서 확실한 대안으로 ApplicationReadyEvent를 사용한다.
* 이는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음 이벤트가 붙은 메서드를 호출해준다.
*/
@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void init2(){
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
initV1은 초기화 코드가 먼저 호출되고, 트랜잭션 AOP가 적용될수 있어 문제가 될 수 있다.
따라서 initV2와 같이 ApplicationReadyEvent를 사용하면 확실하게 트랜잭션AOP를 적용할 수 있다.
5.트랜잭션 AOP의 예외 처리
만약 @Transactional가 적용된 AOP밖으로 예외가 던져지면 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
언체크 예외(RuntimeException, Error)가 발생하면 트랜잭션을 롤백한다.
체크 예외(Exception)또는 그 하위 예외가 발생하면 트랜잭션을 커밋한다.
물론 정상 응답(리턴)하면 트랜잭션을 커밋한다.
* 출처 자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
'Spring' 카테고리의 다른 글
Spring Advanced 정리 1) 쓰레드 로컬 (0) | 2024.03.14 |
---|---|
Spring DB 정리 4) 트랜잭션 전파 (2) | 2024.03.13 |
Spring DB 정리 2) 트랜잭션 처리 및 예외 처리 (0) | 2024.02.13 |
Spring DB 정리 1) JDBC,커넥션풀과 데이터소스 (0) | 2023.12.28 |
Spring MVC 정리 4) 예외 처리 (0) | 2023.12.18 |