WEB/Spring

스프링 DB 2편 1) 스프링 트랜잭션 이해

Tony Lim 2022. 6. 20. 13:27
728x90
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);
    }
}

어떤 스레드가 tx,nontx 메소드를 호출했을시 해당 스레드에 연관된 ThreadLocal 을 통해서 트랜잭션이 active한가를 알려주게된다.

 

스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가지게 된다.

@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();
    }
}

클래스단위 보다 보다 구체적인 write 메소드의 경우는 readOnly=false가 적용이 되고 read 메소드는 readOnly=true가 적용이 된다.

 

우선순위

메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스 타입

하지만 인터페이스에 @Transactional을 쓰는건 권장하지 않는 방법이다. Dynamic JDK proxy로 생성되는 경우면 인터페이스를 상속하지만 CGLIB의 경우에는 클래스 자체를 상속하기 때문에 적용이 안될 수 도 있다.

하지만 Spring 5.0 이후에는 CGLIB로 프록시를 생성해도 인터페이스에 적용된 @Transactional 도 잘되지만 미래에 새로운 프록시 생성방법이 나올 수 있으므로 구체클래스에 작성해주자

 


 

트랜잭션 AOP 주의 사항 - 프록시 내부 호출

트랜잭션을 사용하기위해서는 항상 프록시를 먼저 거치고 진짜 target 로직이 호출해야 한다.

하지만 external 내부에서 @Transactional로 감싸져있는 internal을 호출하게 되면 external는 pointcut에 매핑이 안되기에 프록시를 거치지 않고 target 객체에서 바로 호출이된다.

따라서 AOP 에서 advice의 기능을 적용받지 못하고 순수 메소드만 호출이 되게 된다.

 

이런일을 방지하기 위한 가장 간단한 방법은 internal 메소드가 항상 프록시를 통해 호출 될 수 있게 별도의 클래스로 분리하는 방식이 제일 많이 사용된다.

참고로 public 에만 @Transaction 이 메소드에 걸리게 된다. 

 


초기화 시점에 AOP Transaction 의 적용

@PostConstruct
@Transactional
public void initV1() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init @PostConstruct tx active={}", isActive);
}

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}

2022-06-20 11:50:27.704  INFO 2956 --- [    Test worker] hello.springtx.apply.InitTxTest$Hello    : class Name : hello.springtx.apply.InitTxTest$Hello
2022-06-20 11:50:27.704  INFO 2956 --- [    Test worker] hello.springtx.apply.InitTxTest$Hello    : Hello init @PostConstruct tx active=false

해당 클래스를 빈으로 만들고 @PostConstruct가 걸린 메소드를 호출하게 된다. 이떄 로그를 보면 알 수 있듯이 프록시가 적용이 안된 상태로 target 클래스를 먼저 호출하게 된다.

@PostConstruct는 해당 현재 클래스만 bean으로 준비가 완료가 되었다는것이지 나머지들도 준비가 완료되었는지 알길이 없다. 따라서 프록시빈들이 준비가 되었는지 알 수 없어서 뜻대로 동작을 하지 않는 것이다.

아래와 같이 ApplicationReadEvent(전체 앱이 준비가 완료) 를 듣고 잇다가 호출하면 제대로 프록시를 거쳐서 호출하게 된다.

 


 

@Transactional 옵션들

value, transactionManager 를 지정할 수 있다.

 

rollbackFor 

uncheckedExceptoin 인 RuntimeExceptoin , Error 및 그하위 예외가 발생하면 롤백한다.

checkedException 의 과 하위 예외들은 커밋한다.

@Transactional(rollbackFor = Exception.class)

이렇게 기본설정말고 명시적으로 설정하게 되면 이 경우에는 Exception 포함 하위 예외들도 다 롤백이 된다.

 

isolation

대부분 데이터베이스에서 설정한 기준을 따르게 된다. 앱단에서 트랜잭션 격리 수준을 직접 지정하는 경우는 드물다.

DEFAULT, READ_UNCOMMITED , READ_COMMITED, REPEATABLE_READ , SERIALIZABLE 

 


 

예외

런타임 예외 -> 롤백

체크 예외 -> 커밋

체크 예외 rollbackFor로 지정함 -> 해당 예외 발생시 롤백

 

체크예외 = 비즈니스 의미가 있을 때 사용

언체크 에외 = 복구가 불가능한 예외

스프링은 위와 같이 기본적으로 가정하고 있다.

 

비즈니스 예외

주문 시스템에서 사용자의 계좌에 돈이 부족해서 NotEnoughMoneyException이 발생한 경우를 가정하자

이 예외는 시스템 문제가 아니다. 시스템은 문제없이 동작한것이고 비즈니스 상황에서 예외가 발생한 것이다.

이 경우 롤백을 하지 않게 하고 별도의 입금계좌를 알려주는 방식으로 예외를 처리할 수 있다.

 

 

 

728x90