[Spring DB] 트랜잭션 프록시 주의사항

Loopy·2023년 2월 7일
0

스프링

목록 보기
13/16
post-thumbnail

☁️ 트랜잭션 적용 위치

스프링 @Transactional 은 두가지 규칙이 존재한다.

  1. 우선순위 규칙
  2. 클래스에 적용하면 메서드는 자동 적용

우선순위 규칙

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

따라서 만약 @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의 주의사항(1) : 프록시 내부 호출

트랜잭션을 적용하려면 항상 프록시 객체가 먼저 요청을 받아서 처리하고, 그 다음 실제 객체를 호출하는 과정이 있어야 한다.

AOP 를 적용하면 빈 후처리기에 의해서 자동으로 프록시가 스프링 빈에 대신 등록되기 때문에, 프록시를 건너뛰고 대상 객체를 직접 호출하는 일은 발생하지 않는다.

하지만, 대상 객체 내부에서 메서드 호출이 발생하면 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않는 문제가 발생한다.

예제 코드

static class CallService {
   public void external() {
        log.info("call external");
        printTxInfo();
        internal();  // 내부 호출
   }
 
   @Transactional
   public void internal() {
        log.info("call internal");
        printTxInfo();
   }
}
  1. 정상 수행의 경우

  2. 내부 호출 문제 발생한 경우

트랜잭션 관련 코드도 보이지 않고, 트랜잭션 인터셉터 내부에서 찍은 로그의 txActive=False 를 통해 확실히 트랜잭션이 적용되지 않은 것을 볼 수 있다.

이유가 무엇일까? this는 나 자신의 인스턴스 주소를 의미한다. 즉, 실제 대상 객체target내부 메서드를 호출하게 되기 때문에 프록시를 거치지 않아 트랜잭션을 적용할 수 없는 것이다.

☁️ 프록시 내부 호출 문제 해결 방안

내부 호출을 피하기 위해, internal() 메서드를 별도의 클래스로 분리하면 해결할 수 있다. AOP 는 AOP가 적용된든 메서드가 있는 클래스 자체를 상속받거나 구현해서 동작하기 때문이다.

☁️ 프록시 AOP의 주의사항(2) : 초기화 시점

초기화 코드에 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다. 초기화 코드가 먼저 실행되고 이후에 트랜잭션 AOP가 적용되기 때문이다.

스프링 빈 라이프사이클

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존관계 주입
  4. 초기화 콜백사용 : @PostConstruct
  5. 빈 사용
  6. 소멸전 콜백 : @PreDestro

빈 후처리기는 스프링 빈을 생성할 때 프록시를 대신 등록해주며, 해당 과정은 3-5번 사이에서 일어난다. 하지만, 초기화 콜백인 @PostConstruct 도 역시 빈 후처리기를 사용한다.

결과적으로 순서가 꼬일 수 있기 때문에, @PostConstruct에서는 프록시 빈이 정상 주입 될 것으로 기대하면 안되는 것이다.

☁️ 트랜잭션 옵션

value, transactionManager

트랜잭션 매니저를 지정하며, 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용한다.

public class TxService {
      @Transactional("memberTxManager")
      public void member() {...}
  }

rollbackfor

스프링 트랜잭션 AOP는 예외의 종류에 따라 다음과 같이 처리한다.

🔖 기본 정책
언체크 예외(RunTimeException, Error) : 트랜잭션 롤백
체크 예외(Exception) : 트랜잭션 커밋

propagation

트랜잭션 전파에 대한 옵션이다. 다음 장에서 더 자세히 설명한다.

isolation

대부분 기본값인 READ_COMMITED 인 격리 수준을 사용한다.

readOnly

읽기 전용 트랜잭션으로, 읽기에서 다양한 성능 최적화가 발생할 수 있다.
따라서, 등록 및 수정과 삭제를 하면 오류가 난다.

readOnly 옵션은 크게 아래의 3가지 경우에서 적용된다.

  1. 프레임워크
  • JdbcTemplate : 읽기 전용 트랜잭션 안에서 변경이 발생하면 예외를 던진다.
  • JPA : 읽기 전용이므로 커밋 시점에 flush가 발생하지 않으며, 변경 감지를 위한 스냅샷 또한 생성하지 않으므로 내부에서 성능 최적화가 발생한다.
  1. JDBC 드라이버
  • 읽기 전용 트랜잭션에서 변경이 발생하면 예외를 던진다.
  • 읽기 및 쓰기 DB를 구분해서 요청한다. 즉, 조회 요청의 부하를 분산하기 위해 마스터가 아닌 읽기 전용 슬레이브 DB에서 커넥션을 획득해서 사용한다.
  1. 데이터베이스
    읽기만 하면 되므로, 데이터베이스 자체에서 성능 최적화가 발생한다.

☁️ 예외와 트랜잭션 커밋 및 롤백

스프링은 기본적으로 다음의 원칙을 가지고 커밋 및 롤백을 수행한다.

  1. 체크 예외 : 비즈니스 의미가 있을 때 사용
  2. 언체크 예외 : 복구가 불가능한 예외

비즈니스 요구 상황

  1. 정상 케이스: 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료로 처리한다.
  2. 시스템 예외: 네트워크 오류 및 DB 시스템 오류 등 시스템에서 복구가 안되는 예외가 터지면 전체 데이터를 롤백해서 초기화한다.
  3. 비즈니스 예외: 주문 시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기로 처리한다. 그리고 따로 금액을 입금하라고 알린다. 롤백하면 주문 데이터 자체가 사라지기 때문에 안된다.

고객의 잔고가 부족한 것은, 시스템에 문제가 있는게 아니라 비즈니스 상황 자체가 예외가 되기 때문에 비즈니스 예외라 하는 것이다. 따라서 비즈니스 예외는 매우 중요하고 무조건 잡아서 처리해야 함으로 컴파일 단계에서의 체크가 강제되는 체크 예외가 되는 것이다.

rollbackfor 옵션 사용

기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다.
따라서, 체크 예외의 경우 rollbackFor 옵션을 사용해서 비즈니스 상황에 따라서 커밋과 롤백을 선택하면 된다.

@Transactional(rollbackFor = MyException.class)
public void rollbackFor() {
	...
}
profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글