스프링 트랜잭션

HUSII·2023년 5월 10일
0

스프링 DB

목록 보기
4/4

@Transactional의 우선순위
1. 클래스의 메서드 (우선순위가 가장 높다)
2. 클래스의 타입
3. 인터페이스의 메서드
4. 인터페이스의 타입 (우선순위가 가장 낮다)
스프링은 무조건 구체적인 것이 더 높은 우선순위를 가진다

하지만 인터페이스에 @Transactional을 사용하는 것은 권장하지 않는 방법이라고 한다


@Transactional을 사용하면 스프링은 대상 객체 대신 프록시를 스프링 빈으로 등록한다. 따라서 의존관계 주입시에 프록시 객체를 주입한다.
이때 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
이렇게 되면 호출되는 메서드에 @Transactional이 있어도 트랜잭션이 적용되지 않는다.

내부 호출을 피하기 위해 해결하는 방법은 호출하는 내부 메서드를 별도의 클래스로 분리하는 것이다.


@Transactionalpublic 메서드만 호출 가능하다.
public이 아닌 메서드에 @Transactional을 호출하면 예외는 발생하지 않고, 트랜잭션 적용만 무시된다.


@PostConstruct@Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.
왜냐하면 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다. 따라서 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다.

이를 해결할 방안은 ApplicationReadyEvent 이벤트를 사용하는 것이다

@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void init2() {
	log.info("Hello init ApplicationReadyEvent");
}

이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출해준다.
이렇게 되면 해당 메서드는 트랜잭션이 적용된다.


@Transactional의 다양한 옵션

readOnly
트랜잭션은 기본적으로 읽기 쓰기가 모두 가능한 트랜잭션이 생성된다.
readOnly=true 옵션을 사용하면 읽기 전용 트랜잭션이 생성된다. 이 경우 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다. (드라이버나 데이터베이스에 따라 정상 동작하지 않는 경우도 있다.) 그리고 readOnly 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있다.

rollbackFor
@Transactional
언체크 예외인 RuntimeException, Error와 그 하위 예외가 발생하면 롤백한다.
체크 예외인 Exception과 그 하위 예외들은 커밋한다.

@Transactional의 다양한 옵션중 하나인 rollbackFor을 사용하면

@Transactional(rollbackFor = Exception.class)

기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다

스프링이 체크 예외는 커밋하고, 언체크(런타임) 예외는 롤백하는 이유

  • 체크 예외: 비즈니스 의미가 있을 때 사용
  • 언체크 예외: 복구 불가능한 예외
    라고 가정
    꼭 이런 정책을 따를 필요는 없다

체크 예외는, 시스템은 정상 작동했지만, 비즈니스 상황에서 문제가 되기 때문에 발생하는 예외이다. 비즈니스 예외는 반드시 처리해야 하는 경우가 많으므로 체크 예외를 고려할 수 있다.


트랜잭션이 둘 이상 있을 때

  1. 트랜잭션이 각각 수행될때
    각각 수행되므로, 한족이 커밋하든 롤백되든 다른쪽에 영향을 주지않는다.
  2. 트랜잭션을 각각 사용하는 것이 아니라, 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행할때를 트랜잭션 전파라 한다.

트랜잭션 전파(propagation)

처음 시작된 트랜잭션을 외부 트랜잭션이라하고
외부에 트랜잭션이 수행되고 있는 도중에 호출된 트랜잭션을 내부 트랜잭션이라 한다.

스프링의 경우 외부 & 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다. 내부 트랜잭션이 외부 트랜잭션에 참여하게 된다.

여기서 각각의 트랜잭션을 논리 트랜잭션이라 부르고,
논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
물리 트랜잭션은 실제 DB에 적용되는 트랜잭션을 뜻한다.

원칙
1. 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다

내부 트랜잭션이 시작되면 내부는 외부 트랜잭션에 참여하게 된다.
여기서 외부 트랜잭션만 물리 트랜잭션을 시작하고, 커밋한다.
내부에서 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문이다.
-> 모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.

내부 트랜잭션은 커밋되는데, 외부 트랜잭션이 롤백되는 상황
논리 트랜잭션 하나라도 롤백되면 전체 물리 트랜잭션은 롤백된다.

내부 트랜잭션은 롤백되는데, 외부 트랜잭션이 커밋되는 상황
이때는 전체 물리 트랜잭션이 롤백되고, UnexpectedRollbackException런타임 예외를 던진다.
-> 커밋을 시도했지만, 기대하지 않은 롤백이 발생했다는 것을 명확하게 알려준다.

정리

  • 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.
  • 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다.
  • 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다. 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고, UnexpectedRollbackException예외를 던진다.

외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 사용하는 방법
(이렇게 되면 커밋과 롤백도 각각 별도로 이루어지게 된다)

내부 트랜잭션을 시작할때 @Transactional(REQUIRES_NEW) 옵션을 사용하면 된다.

대신 DB 커넥션이 2개 사용된다

profile
공부하다가 생긴 궁금한 것들을 정리하는 공간

0개의 댓글