스프링 @Transactional
은 두가지 규칙이 존재한다.
스프링에서 우선순위는, 항상 더 자세하고 높은 것이 우선순위를 가지게 된다.
따라서 만약 @Transactional
이 다음과 같이 클래스와 메서드 모두에 붙어있는데 옵션이 다르다면, 더 구체적인 메서드의 옵션이 우선순위를 가진다. 마찬가지로 인터페이스와 구현한 클래스도 클래스가 더 높은 우선순위를 가진다.
🔖 참고 사항
인터페이스에@Transactional
사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이므로 가급적 구체 클래스에 사용하자.AOP
를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면AOP
가 적용이 되지 않는 경우도 있기 때문이다. 구체 클래스 기반 프록시를 사용하는CGLIB
방식이 그 예인데, 스프링 5.0부터는 제대로 동작하긴 하지만 여전히 사용하지 않는 것이 좋다.
@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("txActive = {}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly = {}", readOnly);
}
}
메서드에 있는 @Transactional(readOnly = false)
옵션을 사용한 트랜잭션이 적용된다.
해당 메서드에 만약 @Transcational
옵션이 없다면, 더 상위인 클래스로 올라가 어노테이션을 확인하고 해당 옵션을 그대로 적용한다.
트랜잭션을 적용하려면 항상 프록시 객체가 먼저 요청을 받아서 처리하고, 그 다음 실제 객체를 호출하는 과정이 있어야 한다.
AOP
를 적용하면 빈 후처리기에 의해서 자동으로 프록시가 스프링 빈에 대신 등록되기 때문에, 프록시를 건너뛰고 대상 객체를 직접 호출하는 일은 발생하지 않는다.
하지만, 대상 객체 내부에서 메서드 호출이 발생하면 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않는 문제가 발생한다.
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
internal(); // 내부 호출
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
}
정상 수행의 경우
내부 호출 문제 발생한 경우
트랜잭션 관련 코드도 보이지 않고, 트랜잭션 인터셉터 내부에서 찍은 로그의 txActive=False
를 통해 확실히 트랜잭션이 적용되지 않은 것을 볼 수 있다.
이유가 무엇일까? this
는 나 자신의 인스턴스 주소를 의미한다. 즉, 실제 대상 객체인 target
의 내부 메서드를 호출하게 되기 때문에 프록시를 거치지 않아 트랜잭션을 적용할 수 없는 것이다.
내부 호출을 피하기 위해, internal()
메서드를 별도의 클래스로 분리하면 해결할 수 있다. AOP
는 AOP가 적용된든 메서드가 있는 클래스 자체를 상속받거나 구현해서 동작하기 때문이다.
초기화 코드에 @Transactional
을 함께 사용하면 트랜잭션이 적용되지 않는다. 초기화 코드가 먼저 실행되고 이후에 트랜잭션 AOP가 적용되기 때문이다.
@PostConstruct
@PreDestro
빈 후처리기는 스프링 빈을 생성할 때 프록시를 대신 등록해주며, 해당 과정은 3-5
번 사이에서 일어난다. 하지만, 초기화 콜백인 @PostConstruct
도 역시 빈 후처리기를 사용한다.
결과적으로 순서가 꼬일 수 있기 때문에, @PostConstruct
에서는 프록시 빈이 정상 주입 될 것으로 기대하면 안되는 것이다.
트랜잭션 매니저를 지정하며, 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용한다.
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
}
스프링 트랜잭션 AOP는 예외의 종류에 따라 다음과 같이 처리한다.
🔖 기본 정책
언체크 예외(RunTimeException, Error)
: 트랜잭션 롤백
체크 예외(Exception)
: 트랜잭션 커밋
트랜잭션 전파에 대한 옵션이다. 다음 장에서 더 자세히 설명한다.
대부분 기본값인 READ_COMMITED
인 격리 수준을 사용한다.
읽기 전용 트랜잭션으로, 읽기에서 다양한 성능 최적화가 발생할 수 있다.
따라서, 등록 및 수정과 삭제를 하면 오류가 난다.
readOnly
옵션은 크게 아래의 3가지 경우에서 적용된다.
JdbcTemplate
: 읽기 전용 트랜잭션 안에서 변경이 발생하면 예외를 던진다.JPA
: 읽기 전용이므로 커밋 시점에 flush
가 발생하지 않으며, 변경 감지를 위한 스냅샷 또한 생성하지 않으므로 내부에서 성능 최적화가 발생한다.DB
를 구분해서 요청한다. 즉, 조회 요청의 부하를 분산하기 위해 마스터가 아닌 읽기 전용 슬레이브 DB
에서 커넥션을 획득해서 사용한다.스프링은 기본적으로 다음의 원칙을 가지고 커밋 및 롤백을 수행한다.
- 체크 예외 : 비즈니스 의미가 있을 때 사용
- 언체크 예외 : 복구가 불가능한 예외
정상 케이스
: 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료로 처리한다.시스템 예외
: 네트워크 오류 및 DB 시스템 오류 등 시스템에서 복구가 안되는 예외가 터지면 전체 데이터를 롤백해서 초기화한다. 비즈니스
예외: 주문 시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기로 처리한다. 그리고 따로 금액을 입금하라고 알린다. 롤백하면 주문 데이터 자체가 사라지기 때문에 안된다.고객의 잔고가 부족한 것은, 시스템에 문제가 있는게 아니라 비즈니스 상황 자체가 예외가 되기 때문에 비즈니스 예외라 하는 것이다. 따라서 비즈니스 예외는 매우 중요하고 무조건 잡아서 처리해야 함으로 컴파일 단계에서의 체크가 강제되는 체크 예외가 되는 것이다.
기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다.
따라서, 체크 예외의 경우 rollbackFor
옵션을 사용해서 비즈니스 상황에 따라서 커밋과 롤백을 선택하면 된다.
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() {
...
}