스프링 DB 2편 - 데이터 접근 활용 기술 강의의 내용을 저의 말로 정리한 것입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
디비 접근 기술마다 트랜잭션 관련 코드가 다르다. 따라서 기술이 바뀌면 서비스 계층의 코드가 바뀔 수도 있고 이를 위해서 스프링은 PlatformTransactionManager로 트랜잭션을 추상화하였다.

스프링 부트는 사용하고 있는 데이터 접근 기술을 자동으로 인지해서 적절한 트랜잭션 매니저를 선택해서 스프링 빈을 등록한다.
@Transactional을 사용하면 프록시 방식의 aop가 적용된다.
만약 @Transactional이 메소드나 클래스에 하나라도 있다면 트랜잭션 aop는 이 클래스를 상속하여 프록시 객체를 만들고 빈으로 등록한다. 프록시는 실제 객체를 참조하고 클라이언트는 프록시 객체를 참조한다.

만약
public class TxEx {
@Transactional
public void tx(){
...
}
public void nonTx(){
...
}
}
이렇게 하나의 메소드에만 어노테이션이 붙었다면
스프링 트랜잭션 aop는 public 메소드에 대해서만 작동한다. 따라서 public 외 다른 메소드에 @Transactional이 붙어있으면 트랜잭션이 적용되지 않는다.
메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스 순으로 우선 순위가 높다.
예를 들어 클래스에도 붙어있고 메소드에도 붙어있다면
메소드에 붙은 것을 따른다.
인터페이스에 어노테이션을 붙이는 것은 권장되지 않는다.
- @Transactional이 붙은 메소드만 실행했을 때 프록시를 통해 트랜잭션이 시작된다. 붙지 않은 메소드는 바로 위임한다.
- 트랜잭션 코드는 프록시에만 있어 프록시에서만 트랜잭션을 시작할 수 있다.
- 위와 같은 이유로 원본 객체에서는 트랜잭션을 시작할 수 없다.
만약 아래와 같이 트랜잭션을 적용하지 메소드 내부에서 트랜잭션 적용 대상 메소드가 호출된다고 할 때
public void external() {
printTxInfo();
internal();
}
@Transactional
public void internal() {
printTxInfo();
}

먼저
따라서 위와 같은 경우 internal을 트랜잭션으로 처리하고 싶었어도 불가능하다.
위와 같은 문제를 해결하려면 내부 클래스를 만들고 internal 메소드를 내부 클래스로 분리해야 한다.
public class OuterClass{
InnerClass innerClass;
public void external(){
innerClass.internal();
}
static class InnerClass{
@Transactional
public void internal(){
...
}
}
}
위와 같이 분리하면

위와 같은 방식으로 내부에서 트랜잭션이 적용된 메소드를 호출할 때 트랜잭션이 적용되지 않는 문제를 해결할 수 있다.
public class Test{
@PostConstruct
@Transactional
public void init(){
...
}
}
위 코드 처럼 초기화 메소드에 @Transactional을 붙여서 트랜잭션 처리를 하려 하면 작동하지 않는다. 왜냐면 초기화 메소드가 먼저 호출되고 트랜잭션 aop가 작동하기 때문이다. 이 경우 트랜잭션을 적용하려면 초기화 메소드도 호출되고 트랜잭션 aop를 포함한 스프링 컨테이너가 완전히 생성된 후 적용해야 한다. @EventListener를 통해서 이를 해결할 수 있다.
@EventListener는 특정 사건이 발생하면 스프링이 사건을 기다리던 모든 메소드를 실행하게 한다.
매개변수로는 기다리고 있는 이벤트를 넣어주면 된다.
public class Test{
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void init(){
...
}
}
ApplicationReadyEvent는 스프링 컨테이너가 완전히 떴다는 사건이다.
트랜잭션 매니저를 지정할 때 사용한다.
트랜잭션을 사용하려면 먼저 트랜잭션 매니저를 주입받아야 한다.
이 옵션의 값으로 빈으로 등록된 트랜잭션 매니저의 이름을 적어주면 된다.
생략하면 기본을 등록된 트랜잭션 매니저를 사용한다.
속성이 value 하나의 경우 value=를 생략하고 바로 빈 이름을 넣을 수도 있다.
스프링의 기본 트랜잭션 정책은 런타임 에러가 발생하면 롤백하고 체크 에러가 발생하면 커밋하는 것이다. 이 옵션을 통해서 발생시 롤백할 예외를 추가 지정할 수 있다.
rollbackFor의 반대로 발생시 커밋할 예외를 추가로 지정하는 것이다.
트랜잭션 격리 수준을 지정한다. 기본값은 데이터베이스에서 설정한 수준을 따르는 DEFAULT이다. 개발자가 직접 지정하는 경우는 드물다.
트랜잭션 수행 시간에 대한 타임아웃을 초단위로 지정한다.
@Transactional을 붙이면 기본적으로는 읽기와 쓰기가 모두 가능한 트랜잭션이 생성된다. 이걸 true로 하면 일기 전용 트랜잭션이 만들어진다. 등록 수정을 할 수 없지만 성능을 최적화할 수 있다.
읽기 전용 트랜잭션 안에 변경 기능이 있다면 예외가 발생한다.
jpa의 경우 읽기 전용 트랜잭션은 커밋 시점에서 플러시를 호출하지 않는다. 변경이 일어나지 않기 때문이다. 같은 이유로 스냅샷도 만들지 않는다. 이를 통해서 최적화를 할 수 있다.

트랜잭션 내부에서 예외가 발생하여 트랜잭션 aop 프록시 객체까지 오면 두가지 선택을 한다.
- 런타임 예외가 왔으면 롤백하고 밖으로 던진다.
- 체크 예외가 왔으면 커밋하고 밖으로 던진다.
스프링에서는 비지니스 적으로 의미가 있는 예외는 체크 예외를 아닌 예외는 런타임 예외를 사용한다.
예를 들어 주문을 하였는데 잔고가 부족해서 예외가 발생하면 이는 반드시 처리해야 하는 비지느스적으로 중요한 문제이므로 체크 예외로 만들어 지나칠 수 없게 한다. 이렇게 비지니스 상 중요한 예외를 체크 예외로 만들어서 체크 예외가 넘어오면 커밋을 한다. 만약 롤백을 한다면 주문 정보가 모두 날아가 조치를 취할 수 없다. 반면 체크 예외로 넘어오면 주문 상태를 대기로 바꾼 후 커밋하는 등의 조치를 취할 수 있다.