스프링 DB 2편 - 데이터 접근 핵심 원리 : 스프링 트랜잭션

jkky98·2024년 9월 2일
0

Spring

목록 보기
40/77

@Transaction 적용 확인

@Transaction은 편리한 트랜잭션 적용을 가능케 하지만 트랜잭션 관련 코드가 눈에 보이지 않는다.

@Test
    void proxyCheck() {
        log.info("aop class={}", basicService.getClass());
        assertThat(AopUtils.isAopProxy(basicService)).isTrue();
    }

basicService에는 @Transaction이 존재한다. 애노테이션이 메서드레벨 혹은 클래스레벨에 적용되었을 때 스프링은 basicService의 대한 주입을 실제 객체가 아닌 트랜잭션 AOP가 적용된 프록시 객체를 주입해준다.

이 프록시 객체는 CGLib으로 만들어진 BasicService의 자식 객체로 적용된다. 여기서 기억해야할 점은 스프링 컨테이너에 들어있는 BasicService 객체는 프록시 객체라는 것이다.

트랜잭션 적용 위치 우선순위

스프링에서 우선순위는(자바 언어의 메커니즘도 대부분 그러하다) 더 구체적인 것이 높은 우선순위를 가진다. 만약 메서드와 클래스 레벨에 애노테이션을 붙였고 둘 중 하나만 결정되어야 한다면 메서드에 붙은 애노테이션이 우선순위를 가진다.

@Slf4j
    @Transactional(readOnly = true)
    static class LevelService {

        @Transactional(readOnly = false)
        public void write() {
            log.info("call write");
            printTxInfo();
        }

위 설명을 이해하면 write의 트랜잭션은 readOnly = false로 작동한다는 것을 알 수 있다.

인터페이스에도 @Transactional이 사용가능하다. 클래스의 메서드, 클래스, 인터페이스의 메서드, 인터페이스 순으로 우선순위가 결정되는데, 사실 인터페이스에서 @Transactional을 사용하는 것은 공식 메뉴얼에서 권장하지 않는 방법이다.

프록시 내부 호출

중요한 내용이다. 트랜잭션의 내부 호출문제를 이해하기 위해서는 트랜잭션 애노테이션이 적용된 코드와 프록시 코드 두 가지를 잘 구분해야 한다. 프록시 내부 호출의 문제는 프록시 객체를 사용하는 것이 아니라 실제 객체를 사용하기 때문에 발생한다

static class CallService {

        public void external() {
            log.info("call external");
            printTxInfo();
            internal();
        }

        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active={}", txActive);
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            log.info("tx readOnly={}", readOnly);
        }
    }

이 코드는 internal() 메서드에만 트랜잭션이 적용되었다. 그런데 같은 객체에 소속된 다른 메서드인 external()에서 internal()을 호출하는 것이다.

설계 시점에 우리는 @Transactional을 붙인다. 그리고 사용 이전 스프링으로부터 애노테이션을 통해 트랜잭션이 적용된 프록시 객체를 받는다. 하지만 트랜잭션이 붙지 않은 프록시 객체의 external()의 호출에 대해서 프록시 객체는 이것을 실행하지 않고 실제 객체로 넘긴다. 실제 객체에서는 당연히 정상 작동한다 하지만 실제 객체에서의 internal()은 트랜잭션이 적용되지 않은 메서드이다.

이 경우 internal()this.internal()로 동작한다. this는 곧 현재 자신의 객체를 가리킨다. 즉 새로 생성된 프록시와는 전혀 무관해지는 것이다.

즉 트랜잭션이 적용되지 않은 메서드에서 다른 객체가 아닌 자신의 객체의 트랜잭션 메서드를 호출할 경우 트랜잭션이 적용되지 않으므로 주의해야 한다.

해결 방안

이를 해결하기 위해서는 보통 클래스를 분리해야 한다. 트랜잭션 적용이 필요한 로직과 그렇지 않은 로직을 분리해서 적용할 필요가 있다.

초기화 시점 트랜잭션 적용 문제

Spring의 프록시는 빈이 생성되고 의존성이 주입된 후 생성된다. 실제 객체는 스프링이 뜨는 시점에 빈에 구축된다.(생성자, 초기화 로직 작동) 그 후 스프링은 AOP를 인식하여 실제 객체에 대한 프록시를 생성한다. 프록시는 실제 객체를 감싸게 되며(다형성 - 상속관계) 메서드 호출을 가로챈다. 프록시는 실제 객체의 자식 타입으로 생성되므로 Override를 통해 AOP의 내용이 붙은 실제 객체의 메서드 기능을 구현한다.

트랜잭션 옵션

컨테이너에 트랜잭션 매니저가 둘 이상일 경우(기본적으로는 적절한 트랜잭션 매니저를 스프링에서 등록해둔다.)

public class TxService {
     @Transactional("memberTxManager")
     public void member() {...}
     @Transactional("orderTxManager")
     public void order() {...}
 }

위의 코드처럼 트랜잭션 매니저 빈 객체의 이름으로 구분할 수 있으며, value="..."를 통해서도 구분할 수 있다.

rollbackFor

예외 발생시 스프링 트랜잭션은 언체크 예외에 대해서는 롤백하며 체크 예외에 대해서는 커밋한다. rollbackFor 옵션으로 하여금 특정한 체크 예외를 지정하여 해당 예외 이하의 예외에 대해서도 롤백할 수 있다.(체크예외는 기본적으로 커밋이지만 롤백이 가능하다는 것.) 반대의 개념인 noRollbackFor도 존재한다.

isolation

트랜잭션 격리 수준을 지정할 수 있다. 기본값은 DEFAULT로 데이터베이스에서 설정한 격리 수준을 따르는 것이다.

DEFAULT : 데이터베이스에서 설정한 격리 수준을 따른다.
READ_UNCOMMITTED : 커밋되지 않은 읽기
READ_COMMITTED : 커밋된 읽기
REPEATABLE_READ : 반복 가능한 읽기
SERIALIZABLE : 직렬화 가능

timeout

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

readOnly

기본적으로 readOnly=false의 옵션이 적용된다. true일 경우 등록, 수정, 삭제가 안되며 조회만 가능하다. 이를 적절히 사용하는 것에 있어 성능 최적화가 발생할 수 있다.

profile
자바집사의 거북이 수련법

0개의 댓글