@Transactional을 통한 선언적 트랜잭션 관리 방식을 사용하면, 기본적으로 프록시 방식의 AOP가 적용
log.info("aop class={}", basicService.getClass());
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
AopUtils.isAopProxy()
: 선언적 트랜잭션 방식에서 스프링 트랜잭션은 AOP를 기반으로 동작하므로 true결과적으로 실제 객체 대신 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록되고 주입을 받을 때도 동일
public static boolean isActualTransactionActive() {
return (actualTransactionActive.get() != null);
}
TransactionSynchronizationManager.isActualTransactionActive()
@Transactional 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 있으면 트랜잭션 AOP는 프록시를 만들어 스프링 컨테이너에 등록
(프록시는 클래스를 대상으로 만들어짐)
실제 객체 대신 프록시를 스프링 빈에 등록
-> 의존관계 주입시 프록시 주입
프록시는 내부에 실제 객체를 참조하며(상속) 다형성 활용 가능
basicService.tx() 호출
1. 프록시의 tx()가 호출되면 프록시는 tx()가 트랜잭션을 사용할 수 있는지 확인
2. 트랜잭션을 시작하고, 실제 basicService.tx()를 호출
3. 실제 basicService.tx()의 호출이 끝나고 프록시로 제어가 돌아오면, 프록시는 트랜잭션 로직을 커밋하거나 롤백하여 종료
basicService.nonTx() 호출
1. 프록시의 nonTx()가 호출되면 프록시는 nonTx()가 트랜잭션을 사용할 수 있는지 확인
2. nonTx()는 @Transactional 이 없으므로 적용 대상이 아님
3. 트랜잭션을 시작하지 않고, 실제 basicService.nonTx() 를 호출하고 종료
로그 추가
application.properties
logging.level.org.springframework.transaction.interceptor=TRACE
ex)
메서드와 클래스에 애노테이션 -> 메서드가 높은 우선순위
인터페이스와 해당 인터페이스를 구현한 클래스에 애노테이션 -> 클래스가 높은 우선순위
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 트랜잭션도 적용되지 않음
but , 대상 객체의 내부에서 메서드 호출이 발생하면
프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생
@Slf4j
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
internal(); // = this.internal();
}
@Transactional
public void internal() {
log.info("call internal");
..
}
}
문제 원인
but, 이러한 내부 호출은 프록시를 거치지 않아 트랜잭션을 적용 불가
외부 호출인 경우만 트랜잭션이 적용된다 !
문제 해결
가장 단순한 해결 방법은
내부 호출을 피하기 위해 internal() 메서드를 별도의 클래스로 분리하는 것
초기화 코드 @PostConstruct 와 @Transactional 을 함께 사용시, 트랜잭션 적용
👉 초기화 코드가 먼저 호출되고, 그 후 트랜잭션 AOP가 적용된다
트랜잭션 안에서 수행해야 한다면 ApplicationReadyEvent 이벤트를 사용
@EventListener(value = ApplicationReadyEvent.class)
: 트랜잭션 AOP를 포함한 스프링 컨테이너가 완전히 생성된 후 실행
@Transactional
public @interface Transactional {
String value() default "";
String transactionManager() default "";
Class<? extends Throwable>[] rollbackFor() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
String[] label() default {};
}
value , transactionManager
ex)
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
@Transactional("orderTxManager")
public void order() {...}
}
rollbackFor
예외 발생시 스프링 트랜잭션 기본 정책
rollbackFor 옵션을 사용하여 추가로 어떤 예외가 발생할 때 롤백할 지 지정
ex) 체크 예외인 Exception 이 발생해도 롤백 (하위 예외 포함)
@Transactional(rollbackFor = Exception.class)
💡 왜 언체크는 롤백, 체크는 커밋하는가?
스프링은 기본적으로 아래처럼 가정
비즈니스 의미가 있는 비즈니스 예외란?
ex) 결제시 잔고 부족으로 실패 -> 주문 데이터를 저장하고 결제 상태를 대기로 처리 -> 고객에게 잔고 부족을 알리고 입금 안내
강사님은 옵션을 사용하기보다 기본 설정대로 사용하는 것을 추천하신다고 함
noRollbackFor
propagation
isolation
DEFAULT : 데이터베이스에서 설정한 격리 수준을 따름
READ_UNCOMMITTED : 커밋되지 않은 읽기
READ_COMMITTED : 커밋된 읽기 (일반적으로 많이 사용)
REPEATABLE_READ : 반복 가능한 읽기
SERIALIZABLE : 직렬화 가능
timeout
label
readOnly
readOnly 옵션은 크게 3곳에서 적용된다