[Spring] 스프링 트랜잭션 AOP

hi·2022년 12월 30일
0

@Transactional을 통한 선언적 트랜잭션 관리 방식을 사용하면, 기본적으로 프록시 방식의 AOP가 적용

트랜잭션 적용 확인

log.info("aop class={}", basicService.getClass());

assertThat(AopUtils.isAopProxy(basicService)).isTrue();
  • 클래스 이름 출력시, 뒤에 CGLIB... 라는 프록시 클래스 이름이 출력
  • AopUtils.isAopProxy() : 선언적 트랜잭션 방식에서 스프링 트랜잭션은 AOP를 기반으로 동작하므로 true

결과적으로 실제 객체 대신 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록되고 주입을 받을 때도 동일


public static boolean isActualTransactionActive() {
		return (actualTransactionActive.get() != null);
}

TransactionSynchronizationManager.isActualTransactionActive()

  • 현재 쓰레드에 트랜잭션이 적용되어 있는지 확인
  • 결과가 true 이면 트랜잭션이 적용되어 있는 것

  • @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)
메서드와 클래스에 애노테이션 -> 메서드가 높은 우선순위
인터페이스와 해당 인터페이스를 구현한 클래스에 애노테이션 -> 클래스가 높은 우선순위

  • 클래스에 @Transactional를 적용하면 메서드는 자동 적용
  • 인터페이스에도 적용 가능하나, 공식 메뉴얼에서는 권장하지 않는다.
    AOP를 적용하는 방식에 따라, 적용이 되지 않는 경우도 있기 때문

주의) 내부 호출

  • @Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 객체를 호출
  • 따라서 트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체(Target)을 호출해야 함

만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 트랜잭션도 적용되지 않음

  • 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");
        ..
    }
}

문제 원인

  • 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다
  • 따라서 external() 메서드의 마지막 코드는 this.internal() 이며
    실제 대상 객체 (target)의 인스턴스를 뜻한다

but, 이러한 내부 호출은 프록시를 거치지 않아 트랜잭션을 적용 불가
외부 호출인 경우만 트랜잭션이 적용된다 !

문제 해결

가장 단순한 해결 방법은
내부 호출을 피하기 위해 internal() 메서드를 별도의 클래스로 분리하는 것


public 메서드

  • public 메서드에만 트랜잭션이 적용
  • public이 아닌 곳에 @Transactional이 붙어 있어도 예외가 발생하지는 않음

초기화 시점 (ApplicationReadyEvent)

  • 초기화 코드 @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() {...}
}
  • 애노테이션 속성이 하나인 경우 value 생략 가능

rollbackFor

예외 발생시 스프링 트랜잭션 기본 정책

  • 언체크 예외인 RuntimeException , Error 와 그 하위 예외가 발생하면 롤백
  • 체크 예외인 Exception 과 그 하위 예외들은 커밋

rollbackFor 옵션을 사용하여 추가로 어떤 예외가 발생할 때 롤백할 지 지정

ex) 체크 예외인 Exception 이 발생해도 롤백 (하위 예외 포함)

@Transactional(rollbackFor = Exception.class)

💡 왜 언체크는 롤백, 체크는 커밋하는가?

스프링은 기본적으로 아래처럼 가정

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

비즈니스 의미가 있는 비즈니스 예외란?

ex) 결제시 잔고 부족으로 실패 -> 주문 데이터를 저장하고 결제 상태를 대기로 처리 -> 고객에게 잔고 부족을 알리고 입금 안내

  • 마치 예외를 리턴 값처럼 사용
  • 상황에 따라 커밋과 롤백을 선택

강사님은 옵션을 사용하기보다 기본 설정대로 사용하는 것을 추천하신다고 함


noRollbackFor

  • rollbackFor 와 반대
  • 기본 정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정

🔎 rollbackForClassName , noRollbackForClassName

  • 예외 이름을 문자로 지정

propagation

  • 트랜잭션 전파

isolation

  • 트랜잭션 격리 수준 지정
  • 기본 값은 데이터베이스에서 설정한 트랜잭션 격리 수준을 사용하는 DEFAULT
  • 대부분 데이터베이스에서 설정한 기준을 따름

DEFAULT : 데이터베이스에서 설정한 격리 수준을 따름
READ_UNCOMMITTED : 커밋되지 않은 읽기
READ_COMMITTED : 커밋된 읽기 (일반적으로 많이 사용)
REPEATABLE_READ : 반복 가능한 읽기
SERIALIZABLE : 직렬화 가능

timeout

  • 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정
  • 운영 환경에 따라 동작하지 않는 경우도 있기에 확인 후 사용

🔎 timeoutString

  • 숫자 대신 문자 값으로 지정

label

  • 트랜잭션 애노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용
  • 잘 사용하지 않음

readOnly

  • 읽기 전용 트랜잭션이 생성
  • 등록, 수정, 삭제가 안되고 읽기 기능만 작동
    (드라이버나 DB에 따라 정상 동작하지 않는 경우도 있음)
  • 읽기에서 다양한 성능 최적화가 발생 가능

readOnly 옵션은 크게 3곳에서 적용된다

프레임워크

  • JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던짐
  • JPA(하이버네이트)는 읽기 전용 트랜잭션의 경우 커밋 시점에 플러시를 호출하지 않음
    변경 감지를 위한 스냅샷 객체도 생성 X

JDBC 드라이버

  • DB와 드라이버 버전에 따라서 다르게 동작
  • 읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던짐
  • 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청
  • 읽기 전용 트랜잭션의 경우 읽기 (슬레이브) 데이터베이스의 커넥션을 획득해서 사용

데이터베이스

  • DB에 따라 읽기 전용 트랜잭션의 경우 읽기만 하면 되므로, 내부에서 성능 최적화가 발생

0개의 댓글