[Spring DB 2편] 9. 스프링과 트랜잭션

HJ·2023년 2월 7일
0

Spring DB 2편

목록 보기
9/11
post-custom-banner

김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard


1. 트랜잭션 복습

1-1. 트랜잭션 추상화

  • 각 데이터 접근 기술들은 트랜잭션을 처리하는 코드가 다르기 때문에 기술을 변경하면 코드도 변경해야한다

   PlatFormTransactionManager라는 인터페이스를 통해 트랜잭션 추상화

  • 스프링부트는 어떤 기술을 사용하는지 자동으로 인식해서 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다

1-2. 프록시

  • @Transactional 을 메서드나 클래스에 붙이면 해당 객체는 트랜잭션 AOP 적용 대상이 되고, 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다

  • 주입을 받을 때도 실제 객체 대신에 프록시 객체가 주입된다

  • 스프링부트는 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다

  • 프록시를 적용하면 트랜잭션을 처리하는 객체와 서비스 객체를 분리할 수 있다




2. 트랜잭션 적용 확인

2-1. 프록시 확인

public class TxBasicTest {

    @Autowired BasicService basicService;

    static class BasicService {

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

        public void nonTx() { ... }
    }
}
  • @Transactional 어노테이션이 특정 클래스나 메서드에 하나라도 있으면 있으면 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록

   ➜ 실제 basicService 객체 대신에 프록시인 basicService$$CGLIB 를 스프링 빈에 등록

   ➜ 프록시는 내부에 실제 basicService 를 참조

  • txBasicTest 가 스프링 컨테이너에 @Autowired 로 의존관계 주입을 요청하면

   ➜ 스프링 컨테이너에는 프록시가 스프링 빈으로 등록되어 있기 때문에 프록시를 주입

   ➜ basicService.getClass() 로 확인하면 프록시 클래스의 이름이 출력된다


2-2. 트랜잭션 확인

@Transactional
public void tx() {
    boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("tx active={}", txActive);
}

public void nonTx() {
    boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("tx active={}", txActive);
}
  • basicService.tx() 호출 ➜ 프록시의 tx() 가 호출

   ➜ 프록시는 tx() 메서드가 트랜잭션을 사용할 수 있는지 확인

   @Transactional 이 붙어있으므로 트랜잭션 적용 대상

   ➜ 따라서 트랜잭션을 시작한 다음에 실제 basicService.tx() 를 호출

   ➜ 실제 basicService.tx() 의 호출이 끝나서 프록시로 제어가( 리턴 ) 돌아오면
    프록시는 트랜잭션 로직을 커밋하거나 롤백해서 트랜잭션을 종료


  • basicService.nonTx() 호출 ➜ 프록시의 nonTx() 가 호출

   ➜ 프록시는 nonTx() 메서드가 트랜잭션을 사용할 수 있는지 확인

   ➜ @Transactional 이 없음

   ➜ 트랜잭션을 시작하지 않고, 실제 basicService.nonTx() 를 호출하고 종료


  • isActualTransactionActive() : 현재 Thread에 트랜잭션이 적용되어 있는지 확인



3. 트랜잭션 적용 위치

3-1. 설명

  • @Transactional 의 적용 위치에 따라 우선순위가 다르다

  • 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다

  • 클래스와 메서드에 모두 붙어 있는 경우 메서드에 붙은 것이 높은 우선순위

  • 인터페이스와 구현 클래스에 붙어 있는 경우 구현한 클래스가 높은 우선 순위

  • AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기 때문에 구체 클래스에 사용을 권장


3-2. 코드로 확인

static class LevelService {

    @Transactional(readOnly = false)
    public void write() { ... }

    public void read() { ... }
}
  • 기본적으로 트랜잭션은 읽기와 쓰기 모두 가능한 트랜잭션이 생성

  • @Transactional(readOnly = true) : 읽기 전용 트랜잭션이 생성

  • write() : readOnly = false 가 적용 ( 디폴트 )

  • read() : readOnly = true 가 적용




4. 트랜잭션 AOP 주의사항 - 프록시 내부 호출

4-1. 설명

  • @Transactional 을 사용하면 스프링의 트랜잭션 AOP가 적용되는데 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용

  • 트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체( Target )을 호출해야 프록시에서 먼저 트랜잭션을 적용하고, 이후에 대상 객체를 호출하게 된다

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

  • AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록하기 때문에 일반적으로 대상 객체를 직접 호출하는 일은 발생하지 않는다

  • but> 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하고 @Transactional이 있어도 트랜잭션이 적용되지 않는다


4-2. 코드로 확인

static class CallService{
        
    public void external() {
        printTxInfo();
        internal();
    }

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

    private void printTxInfo() {
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("tx Active={}", txActive);
    }
}
  • @Transactional 이 있기 때문에 프록시 객체가 만들어지고 실체 객체 대신 주입된다

  • internal() 을 호출하면 @Transactional 이 있으므로 프록시가 트랜잭션을 적용하기 때문에 txActive = true 가 출력

  • external() 을 호출하면 txActive = false가 출력되고 내부에서 internal() 을 호출했을 때 txActive = false 가 출력된다

  • 즉, internal() 에서 트랜잭션이 적용되지 않은 것이다


4-3. 원인 분석하기

  1. 클라이언트인 테스트 코드가 callService.external() 을 호출

  2. callService 의 트랜잭션 프록시가 호출

  3. external() 에는 @Transactional 이 없으므로 프록시는 트랜잭션을 적용하지 않는다

  4. 트랜잭션을 적용하지 않고 실제 callServiceexternal() 를 호출

  5. external() 은 내부에서 internal() 메서드를 호출하는데 여기서 문제가 발생


  • 실제 대상 객체 내부에서 호출하기 때문에 this.internal() 이 되어버린다

  • 프록시를 거치지 않고 실제 대상 객체( this )의 메서드를 호출하기 때문에 트랜잭션을 적용할 수 없다

  • 즉, 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없는데 이것이 프록시 방식의 AOP 의 한계


4-4. 문제 해결하기

4-4-1. 코드로 확인

static class CallService{

    private final InternalService internalService;

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

static class InternalService {
        
    @Transactional
    public void internal() {
        printTxInfo();
    }
}
  • 가장 간단한 해결 방법은 internal() 메서드를 별도의 클래스로 분리하는 것이다

  • CallService 에는 트랜잭션 관련 코드가 없으므로 트랜잭션 프록시가 적용되지 않는다

  • InternalService 에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용된다

  • external() 에서는 txActive = false가 출력되지만 internal() 에서는 txActive = true 가 출력된다


4-4-2. 실행 흐름

  1. 클라이언트인 테스트 코드가 callService.external() 을 호출

  2. callService 는 실제 callService 객체 인스턴스이다

  3. callService 는 주입 받은 internalService.internal() 을 호출

  4. internalService 는 트랜잭션 프록시이며 internal() 메서드에 @Transactional 이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다

  5. 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 을 호출




5. public과 트랜잭션

  • 스프링의 트랜잭션 AOP 기능은 public 메서드에만 적용하도록 기본 설정이 되어 있다

  • 그래서 protectedprivate 에는 트랜잭션이 적용되지 않는다

  • 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 적용될 수 있는데 그렇게 되면 트랜잭션을 의도하지 않은 곳까지 적용되어 버린다

  • 트랜잭션은 주로 비지니스 로직의 시작점에서 걸기 때문에 대부분 외부에 열어준 곳
    ( public )을 시작점으로 사용한다

  • 이러한 이유로 public 메서드에서만 트랜잭션을 적용하도록 설정되어 있는 것이다




6. 트랜잭션 AOP 주의사항 - 초기화 시점

6-1. @PostConstruct

@PostConstruct
@Transactional
public void initV1() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init @PostConstruct tx active={}", isActive);
}
  • @PostConstruct@Transactional 을 함께 사용하면 트랜잭션이 적용되지 않는다

  • 초기화 코드가 먼저 호출되고, 트랜잭션 AOP가 적용되기 때문에 false 가 출력된다

  • 즉, 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다


6-2. ApplicationReadyEvent

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
  • 컨테이너와 AOP 등 모든 스프링이 완성된 후에 메서드가 호출되도록 한다

  • ApplicationReadyEvent : 트랜잭션 AOP 를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이 이벤트가 붙은 메서드를 호출해준다




7. 트랜잭션 옵션

7-1. 트랜잭션 매니저 지정

public @interface Transactional {

	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";
}
  • 트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할 지 알아야 한다

  • 코드로 직접 트랜잭션을 사용할 때 트랜잭션 매니저를 주입 받아서 사용한 것처럼 @Transactional에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다

  • 사용할 트랜잭션 매니저를 지정할 때는 value, transactionManager 둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어준다

  • 이 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하는데 트랜잭션 매니저가 둘 이상이면 아래처럼 트랜잭션 매니저의 이름을 지정해서 구분해야한다

    • @Transactional(value = "memberTxManager")

7-2. 롤백 옵션

public @interface Transactional {

	Class<? extends Throwable>[] rollbackFor() default {};

	String[] rollbackForClassName() default {};

	Class<? extends Throwable>[] noRollbackFor() default {};

	String[] noRollbackForClassName() default {};
}
  • rollbackFor : 기본 예외 처리 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정

  • rollbackForClassName 도 동일한 기능을 하는데 이 옵션은 예외 이름을 문자로 넣는다

  • noRollbackFor : rollbackFor처럼 예외 클래스를 직접 지정하지만 기능은 반대이다

  • ex> @Transactional(rollbackFor=Exception.class)


7-3. isolation

public @interface Transactional {

	Isolation isolation() default Isolation.DEFAULT;
}
  • 트랜잭션의 격리 수준을 지정하며 기본 값은 DB에서 설정한 트랜잭션 격리 수준을 사용하는 DEFALUT

  • DEFAULT : 데이터베이스에서 설정한 격리 수준을 따른다

  • READ_UNCOMMITTED : 커밋되지 않은 읽기

  • READ_COMMITTED : 커밋된 읽기

  • REPEATABLE_READ : 반복 가능한 읽기

  • SERIALIZABLE : 직렬화 가능


7-4. timeout, label

public @interface Transactional {

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	String timeoutString() default "";

    String[] label() default {};
}
  • 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정하며 기본 값은 트랜잭션 시스템의 타임아웃을 사용

  • timeoutString : 숫자 대신 문자 값으로 지정할 수 있다

  • label : 트랜잭션 어노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용할 수 있다


7-5. readOnly

public @interface Transactional {

	boolean readOnly() default false;
}
  • readOnly가 true이면 등록, 수정, 삭제가 불가능한 읽기 전용 트랜잭션이 생성된다

  • 또한 이 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있는데 보통 프레임워크, JDBC 드라이버, DB 3곳에서 적용된다

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

  • 프레임워크

    • JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던진다

    • JPA는 읽기 전용 트랜잭션의 경우 커밋 시점에 플러시를 호출하지 않는다

    • 읽기 전용이니 변경에 사용되는 플러시를 호출할 필요가 없다

    • 변경이 필요 없으니 변경 감지를 위한 스냅샷 객체도 생성하지 않는다

  • JDBC 드라이버

    • 읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던진다

    • 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청한다

    • 읽기 전용 트랜잭션의 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용

    • 위 내용들은 DB와 드라이버 버전에 따라 다르게 동작한다




8. 예외와 트랜잭션 커밋, 롤백

  • 체크 예외인 Exception 과 그 하위 예외가 발생하면 커밋한다

  • 언체크 예외인 RuntimeException, Error 와 그 하위 예외가 발생하면 롤백한다

  • 정상 응답( 리턴 )하면 트랜잭션을 커밋

  • 체크 예외는 반드시 처리해야하는 예외인 경우, 비지니스 의미가 있을 때 사용

  • rollbackFor 옵션으로 커밋과 롤백을 선택할 수 있다




9. 참고

  • 메모리 DB를 통해 테스트를 수행하면 테이블 자동 생성 옵션이 활성화 된다

  • JPA는 엔티티 정보를 참고해서 테이블을 자동으로 생성해준다

  • 테이블 자동 생성은 spring.jpa.hibernate.ddl-auto 옵션으로 조정할 수 있다

    • none : 테이블을 생성하지 않는다

    • create : 애플리케이션 시작 시점에 테이블을 생성한다

post-custom-banner

0개의 댓글