ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 DB 데이터접근 기술 - 스프링 트랜잭션 이해
    Spring-Boot/스프링 DB 2편 - 데이터 접근 기술 2022. 9. 23. 17:47

    * 선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리 *

    선언적 트랜잭션 관리 ( Declarative Transaction Management )

    - @Transactional 애노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리라 한다.

    - 선언적 트랜잭션 관리는 과거 XML에 설정하기도 했다

    - 이름 그대로 해당 로직에 트랜잭션을 적용하겠다 라고 어딘가에 선언하기만 하면 트랜잭션이 적용되는 방식이다

     

    프로그래밍 방식의 트랜잭션 관리( programmatic transaction management )

    - 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것을 프로그래밍 방식의 트랜잭션 관리라 한다.

     

    - 프로그래밍 방식의 트랜잭션 관리를 사용하게 되면, 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합

    - 선언적 트랜잭션 관리가 프로그래밍 방식에 비해서 훨씬 간편하고 실용적이기 때문에 실무에서는 대부분 선언적 트랜잭션 관리를 사용한다.

     

    선언적 트랜잭션과 AOP

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

    - 트랜잭션을 처리하기 위한 프록시를 적용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.

     

    * 스프링 컨테이너에 트랜잭션 프록시 등록 *

    - `@Transactional` 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록한다.

     

    * 로그 추가*

    logging.level.org.springframework.transaction.interceptor=TRACE

    - 이 로그를 추가하면 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확하게 로그로 확인 가능

     


    트랜잭션 적용 위치

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

    - 예를 들어 메서드와 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 메서드가 더 높은 우선순위를 가진다

    - 인터페이스와 해당 인터페이스를 구현한 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 클래스가 높은 우선 순위를 가진다.

     

     

    - 스프링의 `@Transactional`은 두 가지 규칙이 있다

    1. 우선순위 규칙

    2. 클래스에 적용하면 메서드는 자동 적용

     

     - 참고로 `readOnly=false`는 기본 옵션이기 때문에 보통 생략한다. 

       `@Transactional == @Transactional(readOnly=false)` 와 같다.

     

    * 인터페이스에 @Transactional 적용 * 

    - 인터페이스에도 `@Transactional`을 적용할 수 있다.

    - 다음 순서로 적용

        ㆍ1. 클래스의 메서드( 우선순위가 가장 높다 )

        ㆍ2. 클래스의 타입

        ㆍ3. 인터페이스의 메서드

        ㆍ4. 인터페이스의 타입 ( 우선순위가 가장 낮다 )

     

    - 그런데 인터페이스에 `@Transactional` 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이다.

    - AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용되지 않는 경우도 있기 때문이다

    - 가급적 구체 클래스에 `@Transactional`을 사용하자


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

    - 트랜잭션AOP는 기본적으로 프록시 방식의 AOP를 사용한다.

    - `@Transactional`을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고 실제 객체를 호출

    - AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.

    - 따라서 스프링은 의존관계 주입 시에 항상 실제 객체 대신에 프록시 객체를 주입힌다.

    - 하지만 *대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생*

    - 이렇게 되면 @Transactional 이 있어도 트랜잭션이 적용되지 않는다.

     

    * 프록시와 내부 호출 *

    * 문제 원인 *

    - 자바 언어에서 메서드 앞에 별도의 참조가 없으면 `this`라는 뜻으로 자기 자신의 인스턴스를 가리킨다

    - 결과적으로 자기 자신의 내부 메서드 호출하면 실제 대상 객체의 인스턴스를 뜻한다.

    - 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 트랜잭션을 적용할 수 없다.

     

    * 프록시 방식의 AOP 한계 *

    - `@Transacitonal` 를 사용하는 트랜잭션 AOP는 프록시를 사용한다. 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.

     

    - 해결 방안

        ㆍ여러가지 해결방안이 있지만 실무에서는 별도의 클래스로 분리하는 방법을 주로 사용

     

    public 메서드만 트랜잭션 적용

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

    - 그래서 `protected`, `private`, `package-visible` 에는 트랜잭션이 적용되지 않는다.

    - 생각해보면 `protected`, `package-visible`도 외부에서 호출이 가능하다. 따라서 앞서 설명한 프록시의 내부 호출과는 무관하고, 스프링이 막아둔 것이다.

     

    - 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있다.

    - 그러면 트랜잭션이 의도하지 않는 곳까지 트랜잭션이 과도하게 적용된다.

    - 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다.

    - 이런 이유로 `public` 메서드에만 트랜잭션을 적용하도록 설정되어 있다.

     

    - 참고로 `public`이 아닌곳에 `@Transactional`이 붙어 있으면 예외가 발생하지는 않고, 트랜잭션 적용만 무시된다.


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

    - 스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.

     

    - 초기화 코드 ( 예: `@PostConstruct` ) 와 `@Transactional`을 함께 사용하면 트랜잭션이 적용되지 않는다.

    - 왜냐하면 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다. 

    - 따라서 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다.

     

    - 가장 확실한 대안은 `ApplicationReadyEvent` 이벤트를 사용하는 것이다.

    @EventListener(ApplicationReadyEvent.class)
    @Transactional
    public void initV2() {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("Hello init ApplicationReadyEvent tx active={}",isActive);
    }

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


    트랜잭션 옵션

    * value, transactionManager *

    - 트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할 지 알아야 한다.

    - 코드로 직접 트랜잭션을 사용할 때 트랜잭션 매니저를 주입 받아서 사용했다.

      @Transactional 에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다.

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

    - 이 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하기 때문에 대부분 생략한다.

      그런데 사용하는 트랜잭션 매니저가 둘 이상이라면 트랜잭션 매니저의 이름을 지정해서 구분

     

    - 참고로 애노테이션에서 속성이 하나인 경우 `value`는 생략 가능

     

    * rollbackFor *

    - 예외 발생시 스프링 트랜잭션의 기본 정책은 다음과 같다.

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

        ㆍ체크 예외인 `Exception`과 그 하위 예외들은 커밋한다.

     

    - 이 옵션을 사용하면 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다.

    @Transactional(rollbackFor = Exception.class)

    - 예를 들어 이렇게 지정하면 체크 예외인 `Exception`이 발생해도 롤백하게 된다. ( 하위 예외들도 대상에 포함 )

     

    - `rollbackForClassName` 도 있는데, `rollbackFor`는 예외 클래스를 직접 지정하고, `rollbackForClassName`는 예외 이름을 문자로 넣으면 된다.

     

    * noRollbackFor *

    - 앞서 설명한 `rollbackFor`와 반대이다.

    - 기본정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정할 수 있다.

    - 예외 이름을 넣을 수 있는 `noRollbackForClassName`도 있다

     

    * isolation *

    - 트랜잭션 격리 수준을 지정할 수 있다.

    - 기본 값은 데이터베이스에서 설정한 트랜잭션 격리 수준을 사용하는 `DEFAULT`이다.

    - 대부분 데이터베이스에서 설정한 기준을 따른다.

       애플리케이션 개발자가 트랜잭션 격리 수준을 직접 지정하는 경우는 드물다.

     

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

        ㆍ`READ_UNCOMMITED` : 커밋되지 않은 읽기

        ㆍ`READ_COMMITTED` : 커밋된 읽기

        ㆍ`REPEATABLE_READ` : 반복 가능한 읽기

        ㆍ`SERIALIZABLE` : 직렬화 가능

     

    * timeout *

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

    - 기본 값은 트랜잭션 시스템의 타임아웃을 사용한다.

    - 운영 환경에 따라 동작하는 경우도 있고 그렇지 않은 경우도 있기 때문에 꼭 확인하고 사용해야 한다.

    - `timeoutString`도 있는데, 숫자 대신 문자 값으로 지정할 수 있다.

     

    * label *

    - 트랜잭션 애노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 떄 사용할 수 있다. 일반적으로 사용하지 않는다

     

    * readOnly *

    - 트랜잭션은 기본적으로 읽기 쓰기가 모두 가능한 트랜잭션이 생성된다

    - `readOnly=true` 옵션을 사용하면 읽기 전용 트랜잭션이 생성된다. 

       이 경우 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다. ( 드라이버나 DB에 따라 정상 작동하지 않는 경우도 있다 )

    - 그리고 `readOnly` 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있다.

     

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

    1. * 프레임 워크 *

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

     

    2. *JDBC드라이버*

    - 참고로 여기서 설명하는 내용들은 DB와 드라이버 버전에 따라서 다르게 동작하기 때문에 사전 확인 필요

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

    - 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청한다. 읽기 전용 트랜잭션인 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용한다

     

    3. *데이터 베이스*

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


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

    - 예외가 발생했는데, 내부에서 예외를 처리하지 못하고, 트랜잭션 범위(`@Transactional가 적용된 AOP`) 밖으로 예외를 던지면 어떻게 될까

     

    - 예외 발생시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.

        ㆍ언체크 예외인 `RuntimeException`, `Error`와 그 하위 예외가 발생하면 트랜잭션을 롤백한다

        ㆍ체크 예외인 `Exception`과 그 하위 예외가 발생하면 트랜잭션을 커밋한다

        ㆍ물론 정상 응답(리턴)하면 트랜잭션을 커밋한다.

     

    - 스프링은 왜 체크 예외는 커밋하고, 언체크(런타임) 예외는 롤백할까?

      스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임(언체크) 예외는 복구 불가능한 에외로 가정

        ㆍ체크 예외 : 비즈니스 의미가 있을 때 사용

        ㆍ언체크 예외 : 복구 불가능한 예외

     

    - 참고로 꼭 이런 정책을 따를 필요는 없다. `rollbackFor` 라는 옵션을 사용해서 체크 예외도 롤백하면 된다

     

     

    댓글

Designed by Tistory.