트랜잭션 관련 포스팅
- [Spring] 트랜잭션 추상화 정리
- [Spring] 트랜잭션 동기화 정리
- [Spring] 트랜잭션 AOP 동작 흐름
- [Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation)
- [Spring] 스프링 트랜잭션 전파와 롤백
- [Spring] 트랜잭션 전파 옵션
트랜잭션 프록시 동작 방식
- @Transactional 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어서 실제 객체(위 그림에서는 BasicService) 대신 컨테이너에 빈으로 등록한다
- 따라서 클라이언트가 스프링 컨테이너에 의존관계 주입을 요청하면 실제 객체 대신 프록시가 주입된다
- 프록시는 실제 객체를 상속해서 만들어지기 때문에 다형성을 활용할 수 있으며, 내부의 참조 변수(target)를 이용하여 실제 객체를 참조할 수 있다
- 위와 같은 상황에서 클라이언트는 트랜잭션을 적용하는 프록시이며, 해당 프록시는 트랙잭션을 적용하는 기능을 수행한다
- 클라이언트는 트랜잭션이 적용된 메서드인 경우 트랜잭션을 시작한 다음 메서드를 호출한다
- 트랜잭션이 적용된 메서드가 아닌 경우엔 트랜잭션을 시작하지 않고 메서드를 호출한다
트랜잭션 AOP 사용 시 주의사항1 - 프록시 내부 호출(self-invocation)
- [Spring] 트랜잭션 AOP 동작 흐름에서 나왔던 것 같이 @Transactional을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고 실제 객체를 호출해준다
- 따라서 트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체(Target)를 호출해야 한다
- 그러나 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하고, @Transactional이 있어도 트랜잭션이 적용되지 않는다
- 실무에서 자주 만나는 문제이므로 특히 주의해야 한다!
예시 코드
@Slf4j
class CallService {
public void external() {
log.info("call external");
printTxInfo(); // this.printTxInfo();
internal(); // this.internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
- 트랜잭션이 적용되지 않은 external() 메서드 내부에서 internal() 메서드를 호출한다
- 자바에선 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리키게 된다
- 따라서 external() 메서드 내에서 호출한 internal()은 자기 자신의 내부 메서드를 호출하는 this.internal()이 되고, 여기서 this는 실제 대상 객체(target)의 인스턴스를 뜻한다
- 결과적으로 target에 있는 internal()을 직접 호출하게 된 것이다
- 이러한 내부 호출은 프록시를 거치지 않기 때문에 트랜잭션을 적용할 수 없다
해결방법
@Slf4j
@RequiredArgsConstructor
class CallService {
private final InternalService internalService;
public void external() {
log.info("call external");
printTxInfo(); // this.printTxInfo();
internalService.internal(); // this.internal();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}", readOnly);
}
}
class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
- 별도의 클래스(위의 InternalService)로 분리하는 방법을 이용하여 문제를 해결할 수 있다
- 또는 ObjectProvider를 활용한 지연 조회를 통해 해결할 수도 있다
- 이와 관련된 자세한 내용은 여기를 참고하자
- 여기서 반대로 트랜잭션이 걸린 메서드(@Transactional)가 트랜잭션이 걸리지 않은 메서드를 내부 호출하는 상황을 생각해볼 수 있다
- 이 경우엔 트랜잭션이 걸리지 않은 메서드에도 트랜잭션이 적용되는데, 이는 부모 트랜잭션(외부 트랜잭션)에 참여하기 때문아다
- 자세한 설명은 https://stackoverflow.com/questions/6222600/transactional-method-calling-another-method-without-transactional-anotation를 참조하자
트랜잭션 AOP 사용 시 주의사항2 - public 메서드만 트랜잭션 적용
@Transactional
public class Hello {
public method1(); // 여기에만 트랜잭션이 적용됨
method2():
protected method3();
private method4();
}
- 스프링 트랜잭션 AOP 기능은 public 메서드에서만 트랜잭션을 적용하도록 기본 설정이 되어있어서 protected, private, package-visible(default) 메서드에는 트랜잭션이 적용되지 않는다
- 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다
- 위의 코드처럼 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있는데, 이는 의도하지 않은 곳까지 트랜잭션이 과도하게 적용된다는 문제가 있다
- 이런 이유로 public 메서드에서만 트랜잭션을 적용하도록 설정되어 있다
- public이 아닌 메서드에 @Transactional을 붙이면 트랜잭션이 걸리지 않을 뿐 예외는 발생하지 않는다
트랜잭션 AOP 사용 시 주의사항3 - 초기화 시점
@Slf4j
class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
// Hello init @PostConstruct tx active=false 출력
}
}
- 스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않는다
- 이는 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다. 따라서 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다
해결방법
@Slf4j
class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
// Hello init @PostConstruct tx active=false 출력
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
// Hello init ApplicationReadyEvent tx active=true 출력
}
}
- 이 문제는 메서드에 이벤트 리스너를 등록하여 해결할 수 있다
- 스프링 컨테이너가 완전히 올라왔을 때(ApplicationReadyEvent) 해당 메서드가 호출되므로 트랜잭션 AOP가 적용된다
참고
'Spring > Spring' 카테고리의 다른 글
[Spring] 트랜잭션 전파 옵션 (0) | 2022.09.07 |
---|---|
[Spring] 스프링 트랜잭션 전파와 롤백 (0) | 2022.09.07 |
[Spring] 테스트 코드를 작성할 때 @Transactional을 주의하라 (4) | 2022.08.17 |
[Spring] @Transactional과 메모리 DB를 활용한 데이터 접근 계층 테스트 (0) | 2022.08.13 |
[Spring] 트랜잭션 동기화 정리 (0) | 2022.08.13 |