스프링 트랜잭션

ksh98·2024년 3월 29일

스프링 DB

목록 보기
7/8

스프링 DB 2편 - 데이터 접근 활용 기술 강의의 내용을 저의 말로 정리한 것입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2

트랜잭션 추상화

디비 접근 기술마다 트랜잭션 관련 코드가 다르다. 따라서 기술이 바뀌면 서비스 계층의 코드가 바뀔 수도 있고 이를 위해서 스프링은 PlatformTransactionManager로 트랜잭션을 추상화하였다.

스프링 부트는 사용하고 있는 데이터 접근 기술을 자동으로 인지해서 적절한 트랜잭션 매니저를 선택해서 스프링 빈을 등록한다.

선언적 트랜잭션과 AOP

@Transactional을 사용하면 프록시 방식의 aop가 적용된다.
만약 @Transactional이 메소드나 클래스에 하나라도 있다면 트랜잭션 aop는 이 클래스를 상속하여 프록시 객체를 만들고 빈으로 등록한다. 프록시는 실제 객체를 참조하고 클라이언트는 프록시 객체를 참조한다.

만약

public class TxEx {

	@Transactional
	public void tx(){
    	...
    }
    
    public void nonTx(){
    	...
    }
}

이렇게 하나의 메소드에만 어노테이션이 붙었다면

  1. 클라이언트가 프록시의 메소드를 호출하고
  2. 호출한 메소드 @Transactional이 붙었는지 확인한다.
    2-1. 만약 있다면
    a. 트랜잭션 적용 대상이므로 트랜잭션을 시작하고
    b. 실제 객체의 메소드를 호출하고
    c. 프록시로 제어가 돌아오면 트랜잭션을 커밋 또는 롤백한다.
    2-2. 만약 없다면 트랜잭션을 시작안 하고 실제 객체의 메소드를 호출한다.

스프링 트랜잭션 aop는 public 메소드에 대해서만 작동한다. 따라서 public 외 다른 메소드에 @Transactional이 붙어있으면 트랜잭션이 적용되지 않는다.

@Transactional 위치에 따른 우선 순위

메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스 순으로 우선 순위가 높다.

예를 들어 클래스에도 붙어있고 메소드에도 붙어있다면
메소드에 붙은 것을 따른다.

인터페이스에 어노테이션을 붙이는 것은 권장되지 않는다.

프록시 내부에서 호출이 일어난 경우

  1. @Transactional이 붙은 메소드만 실행했을 때 프록시를 통해 트랜잭션이 시작된다. 붙지 않은 메소드는 바로 위임한다.
  2. 트랜잭션 코드는 프록시에만 있어 프록시에서만 트랜잭션을 시작할 수 있다.
  3. 위와 같은 이유로 원본 객체에서는 트랜잭션을 시작할 수 없다.

만약 아래와 같이 트랜잭션을 적용하지 메소드 내부에서 트랜잭션 적용 대상 메소드가 호출된다고 할 때

public void external() {
	printTxInfo();
	internal();
}

@Transactional
public void internal() {
	printTxInfo();
}

먼저

  1. 클라이언트는 프록시 객체의 external을 호출하고
  2. 프록시는 exteranl에 @Transactional이 붙지 않았으니 바로 위임 한다.
  3. 실제 객체는 external을 수행하고 내부에 있는 internal을 호출한다.
  4. 실제 객체의 internal은 object.internal() 처럼 참조되지 않는다. 따라서 자기 자신의 internal을 수행하고 실제 객체에는 트랜잭션 코드가 없으니 internal을 하나의 트랜잭션으로 수행하지 못하고 그냥 호출한다.

따라서 위와 같은 경우 internal을 트랜잭션으로 처리하고 싶었어도 불가능하다.

별도의 클래스로 분리

위와 같은 문제를 해결하려면 내부 클래스를 만들고 internal 메소드를 내부 클래스로 분리해야 한다.

public class OuterClass{
	InnerClass innerClass;
    
    public void external(){
    	innerClass.internal();
    }
    
    static class InnerClass{
    	
        @Transactional
    	public void internal(){
        	...
        }
    }
}        

위와 같이 분리하면

  1. OuterClass는 @Transactional이 붙어 있지 않으므로 프록시가 만들어지지 않는다.
  2. 따라서 클라이언트는 실제 OuterClass의 external을 호출한다.
  3. external을 실행하면서 OterClass가 internal을 호출한다.
  4. InnerClass의 메소드인 internal에 @Transactional이 붙어있으므로 사실은 InnerClass의 프록시 객체의 internal을 호출한 것이다.
  5. 프록시 객체에는 트랜잭션 코드가 있으므로 트랜잭션을 시작하고 실제 객체의 internal을 호출한다.

위와 같은 방식으로 내부에서 트랜잭션이 적용된 메소드를 호출할 때 트랜잭션이 적용되지 않는 문제를 해결할 수 있다.

초기화 시점에 aop 적용

public class Test{

	@PostConstruct
    @Transactional
    public void init(){
    	...
    }
}

위 코드 처럼 초기화 메소드에 @Transactional을 붙여서 트랜잭션 처리를 하려 하면 작동하지 않는다. 왜냐면 초기화 메소드가 먼저 호출되고 트랜잭션 aop가 작동하기 때문이다. 이 경우 트랜잭션을 적용하려면 초기화 메소드도 호출되고 트랜잭션 aop를 포함한 스프링 컨테이너가 완전히 생성된 후 적용해야 한다. @EventListener를 통해서 이를 해결할 수 있다.

@EventListener는 특정 사건이 발생하면 스프링이 사건을 기다리던 모든 메소드를 실행하게 한다.
매개변수로는 기다리고 있는 이벤트를 넣어주면 된다.

public class Test{

	@EventListener(ApplicationReadyEvent.class)
    @Transactional
    public void init(){
    	...
    }
}

ApplicationReadyEvent는 스프링 컨테이너가 완전히 떴다는 사건이다.

@Transactional의 옵션

value, trasactionManager

트랜잭션 매니저를 지정할 때 사용한다.
트랜잭션을 사용하려면 먼저 트랜잭션 매니저를 주입받아야 한다.
이 옵션의 값으로 빈으로 등록된 트랜잭션 매니저의 이름을 적어주면 된다.
생략하면 기본을 등록된 트랜잭션 매니저를 사용한다.
속성이 value 하나의 경우 value=를 생략하고 바로 빈 이름을 넣을 수도 있다.

rollbackFor

스프링의 기본 트랜잭션 정책은 런타임 에러가 발생하면 롤백하고 체크 에러가 발생하면 커밋하는 것이다. 이 옵션을 통해서 발생시 롤백할 예외를 추가 지정할 수 있다.

noRollbackFor

rollbackFor의 반대로 발생시 커밋할 예외를 추가로 지정하는 것이다.

isolation

트랜잭션 격리 수준을 지정한다. 기본값은 데이터베이스에서 설정한 수준을 따르는 DEFAULT이다. 개발자가 직접 지정하는 경우는 드물다.

timeout

트랜잭션 수행 시간에 대한 타임아웃을 초단위로 지정한다.

readOnly

@Transactional을 붙이면 기본적으로는 읽기와 쓰기가 모두 가능한 트랜잭션이 생성된다. 이걸 true로 하면 일기 전용 트랜잭션이 만들어진다. 등록 수정을 할 수 없지만 성능을 최적화할 수 있다.

읽기 전용 트랜잭션 안에 변경 기능이 있다면 예외가 발생한다.
jpa의 경우 읽기 전용 트랜잭션은 커밋 시점에서 플러시를 호출하지 않는다. 변경이 일어나지 않기 때문이다. 같은 이유로 스냅샷도 만들지 않는다. 이를 통해서 최적화를 할 수 있다.

예외와 트랜잭션

트랜잭션 내부에서 예외가 발생하여 트랜잭션 aop 프록시 객체까지 오면 두가지 선택을 한다.

  • 런타임 예외가 왔으면 롤백하고 밖으로 던진다.
  • 체크 예외가 왔으면 커밋하고 밖으로 던진다.

스프링에서는 비지니스 적으로 의미가 있는 예외는 체크 예외를 아닌 예외는 런타임 예외를 사용한다.

예를 들어 주문을 하였는데 잔고가 부족해서 예외가 발생하면 이는 반드시 처리해야 하는 비지느스적으로 중요한 문제이므로 체크 예외로 만들어 지나칠 수 없게 한다. 이렇게 비지니스 상 중요한 예외를 체크 예외로 만들어서 체크 예외가 넘어오면 커밋을 한다. 만약 롤백을 한다면 주문 정보가 모두 날아가 조치를 취할 수 없다. 반면 체크 예외로 넘어오면 주문 상태를 대기로 바꾼 후 커밋하는 등의 조치를 취할 수 있다.

profile

0개의 댓글