728x90

기존에 PlatformTransactionManager를 다음과 같이 사용했었다.

TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

이렇게 트랜잭션을 적용했었는데, 좀 더 자세히 알아보자.

 

트랜잭션 정의

우선 DefaultTransactionDefinition이 구현하는 TransactionDefinition 인터페이스를 살펴보자.

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() {
        return 0;
    }

    default int getIsolationLevel() {
        return -1;
    }

    default int getTimeout() {
        return -1;
    }

    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

여기서 4개의 메서드가 좀 중요해보인다.

 

  • getPropagationBehavior

트랜잭션 전파는 트랜잭션 경계에서 만약 이미 진행 중인 트랜잭션이 있을 때, 어떻게 동작할 것인지를 결정하는 방법을 말한다.

트랜잭션 안에 부분적으로 트랜잭션이 포함되는 경우가 많다.

2가지 상황이 바로 떠오른다.

B가 A의 트랜잭션에 속해 B에서 예외가 발생한다면, A와 B의 작업이 롤백되는 경우

B가 별도의 트랜잭션을 시작해, B가 종료되면 독자적으로 커밋되고 그 이후의 A 예외에 대해서는 A만 롤백되고 B는 롤백되지 않는 경우

 

propagation behavior를 통해 이런 방법을 제어할 수 있다.

대표적으로 다음과 같은 트랜잭션 전파 속성이 있다.

  • REQUIRED

가장 많이 사용되는 전파 속성이다.

기존에 트랜잭션이 존재하면 참여하고, 없으면 새로 생성한다.

  • SUPPORTS

기존에 트랜잭션이 존재하면 참여하고, 없으면 트랜잭션 없이 수행한다.

  • MANDATORY

반드시 기존 트랜잭션이 존재해야 하며, 존재하지 않으면 예외가 발생한다.

  • REQUIRES_NEW

항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션은 잠시 보류한다.

  • NOT_SUPPORTED

트랜잭션이 존재하면 일시 중단하고, 트랜잭션 없이 수행한다.

  • NEVER

기존 트랜잭션이 존재하면 예외가 발생한다.

  • NESTED

기존 트랜잭션이 존재하면 중첩 트랜잭션을 생성하고, 존재하지 않으면 새로운 트랜잭션을 생성한다.

 

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

 

이렇게 다양한 트랜잭션 전파 속성이 존재한다.

그렇기에 getTransaction()을 사용하면 무조건 트랜잭션이 시작되는 것은 아니다.

 

  • getIsolationLevel

트랜잭션의 격리 수준을 나타낸다.

격리수준을 높이면 동시성 문제가 줄지만, 병렬작업이 느려진다.

자세한 내용은 다른글을 참고

  • getTimeout

트랜잭션의 제한시간이다.

DefaultTransactionDefinition의 기본 설정은 제한시간이 없으며, 트랜잭션을 직접 시작하는 REQUIRED, REQUIRES_NEW등의 옵션만 의미가 있다.

  • isReadOnly

readOnly로 설정하면 트랜잭션 내의 데이터 조작을 막아준다.

또한 격리 레벨에 따라 성능이 향상되기도 한다.

 

트랜잭션 인터셉터와 트랜잭션 속성

이전에 TransactionHandler에서처럼 메서드의 이름을 이용해 트랜잭션 적용 여부를 판단했던 방식으로 메세드마다 다른 트랜잭션 속성을 부여할 수 있다.

스프링의 TransactionInterceptor 어드바이스를 사용하면 된다.

 

TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 속성을 가지고 있다.

여기서 Propertoes의 이름은 transactionAttributes로, 트랜잭션 속성을 정의한 프로퍼티다.

트랜잭션 속성은 TransactionDefinition의 4가지 기본 항목과 어떤 예외에 대하여 롤백 할 것인지 결정하는 rollbackOn() 메서드를 가지고 있다.

롤백할 예외를 지정하는 이유는 특정 런타임 예외에 대해서는 커밋을 해야하는 경우가 있기 때문이다.

public interface TransactionAttribute extends TransactionDefinition {
    @Nullable
    String getQualifier();

    Collection<String> getLabels();

    boolean rollbackOn(Throwable ex);
}

 

이런 transactionAttributes 속성은 다음과 같이 정의할 수 있다.

PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception

timeout_NNNN에서 N으로 원하는 시간을 초 단위로 설정한다.

-Exception1은 체크 예외 중 롤백 대상으로 추가할 것을 넣는다.

+Exception2는 런타임 예외지만 롤백시키지 않을 예외들을 넣는다.

 

예시로 작성해보면 다음과 같다.

    <bean id="transactionAdvice" class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED, readOnly, timeout_30</prop>
                <prop key="upgrade*">PROPAGATION_REQUIRES_NEW, ISOLATION_SERIALIZABLE</prop>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

 

하지만 이것도 aop처럼 독립적으로 트랜잭션을 xml에 정의할 수 있다.

지금처럼 bean과 같이 작성하면 코드도 길어지고 보기 어려우니 추출해보도록 하자.

    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30" isolation="READ_COMMITTED"/>
        </tx:attributes>
    </tx:advice>

 

포인트컷과 트랜잭션 속성의 적용 전략

대부분의 add() 메서드에도 트랜잭션을 적용해야한다.

add() 개별적으로 동작하면 저장하는 부분에 대해서는 원자성을 가지기에 굳이 적용할 필요가 없을 수도 있지만, 대부분의 add() 메서드는 다른 트랜잭션에 참여하기 때문이다.

또한, 단순한 조회의 경우에도 다른 트랜잭션에 참여하기 때문에 readOnly=true 정도의 속성은 설정해주는 것이 좋다.

 

이러한 이유로 트랜잭션이 적용되는 메서드는 네이밍에 일정한 규칙을 가지는 것이 좋다.

만약 읽는 메서드라면 get, find와 같은 방법으로 시작하도록 말이다.

 

그리고 주의할 점으로 AOP는 본인의 메서드를 호출할 때에는 프록시가 적용되지 않는다.

1번 경우처럼 클라이언트가 메서드를 호출하는 경우에는 프록시를 타고 타깃 오브젝트의 메서드가 호출되지만, 타깃오브젝트가 직접 본인의 메서드를 호출하면 프록시를 거치는 것이 아니라 본인의 메서드를 바로 호출하기 때문이다.

이런 방법은 AspectJ와 같이 직접 바이트코드르 수정하는 방법으로 해결 할 수 있다고 하지만, 어려운 방법이기에 나중에 알아보도록 하자.

 

트랜잭션 속성 적용

이제 만들었던 트랜잭션 전략을 UserService에 적용해보자.

우선 현재 서비스 레이어에서 Dao를 사용하며 트랜잭션을 적용하고 있다.

그렇기에 다른 레이어에서 Dao를 직접 사용하는 것은 트랜잭션이 적용되지 못하기에 막아야 한다.

 

우선 UserService에 트랜잭션을 적용해보기 위해, 다음과 같은 메서드들을 추가한다.

public interface UserService {

    void add(User user);

    User get(String id);

    List<User> getAll();

    void deleteAll();

    void update(User user);

    void upgradeLevels();
}

 

이제 이 UserService의 각각의 메서드에 트랜잭션을 적용해보자.

 

우선 aop가 적용되도록 한다.

xml에서 aop 태그 부분을 수정해보자.

    <aop:config>
        <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
    </aop:config>

이렇게 작성해서 Service로 만들어지는 모든 빈에 advice가 적용되도록 했다.

 

그리고 transaction advice를 통해 조회하는 메서드에는 readOnly만 적용하고 나머지 메서드에는 트랜잭션을 설정하도록 한다.

    <tx:advice id="transactionAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

 

이렇게 트랜잭션이 모두 적용되었다고 생각하고, 테스트를 진행해보자.

 

테스트 할 메서드는 get이다.

현재 get 메서드에는 readOnly 속성을 부여하고 있는데, 정말 업데이트가 되지 않을까 한 번 확인해보자.

    @Override
    public List<User> getAll() {
        super.add(
                User.builder()
                        .id(UUID.randomUUID().toString())
                        .build()
        );
        return super.getAll();
    }

이렇게 테스트 클래스를 만들어서 getAll() 과정에서 새로운 데이터를 삽입하려고 시도해보았다.

    @Test
    public void readOnlyTest(){
        userService.getAll();
    }

그리고 getAll()을 바로 호출해보니

 

이런 식으로 트랜잭션 내에서 데이터의 변경은 허용되지 않는다고 나온다.

readOnly 트랜잭션이 적용된 것을 알 수 있다.

 

 

+ Recent posts