본문 바로가기

토비의 스프링 정리

토비의 스프링 - 6.6 트랜잭션 속성

6.6.1 트랜잭션 속성

  • 트랜잭션 추상화를 할 때 그냥 넘어간 것이 한 가지 있음
  • 트랜잭션 속성을 담당하는 DefaultTransactionDefinition 오브젝트임
  • 트랜잭션 경계는 트랜잭션 매니저에서 트랜잭션을 가져오는 것commit(), rollback() 중 하나를 호출하는 것으로 설정됨
public Object invoke(MethodInvocation invocation) throws Throwable {
    TransactionStatus status =
            this.transactionManager.getTransaction(
                    new DefaultTransactionDefinition());
    try{
        Object ret = invocation.proceed();
        this.transactionManager.commit(status);
        return ret;
    }catch (RuntimeException e){
        this.transactionManager.rollback(status);
        throw e;
    }
}

6.6.2 트랜잭션

  • 트랜잭션은 더 이상 쪼갤 수 없는 최소 단위의 작업임
  • 트랜잭션 경계 안의 작업은 commit()을 만나 성공 하거나, rollback()을 만나 취소가 되야 함
  • DefaultTransactionDefinition가 구현하는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 주는 네 가지 속성을 지님
    1. 트랜잭션 전파(Transaction Propagation)
    2. 격리수준(Isolation Level)
    3. 제한시간(Timeout)
    4. 읽기전용(Read Only)

6.6.3 트랜잭션 전파

  • 트랜잭션 전파란, 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가 결정
  • 각각 독립적인 트랜잭션 경계를 가진 A, B가 존재한다고 가정

  • 그림과 같이 A가 종료되기 전에 B를 호출했을 때, B가 정상적으로 종료되고 (2)에서 예외 발생 시 동작을 결정하는 것이 트랜잭션 전파
    1. A의 트랜잭션이 시작되서 진행중이고, B는 새로운 트랜잭션을 만들지 않고 A의 트랜잭션에 참여하면 (2)에서 예외 발생시 A, B의 작업이 하나로 묶여있기 때문에 모두 취소
    2. A, B가 독립적인 트랜잭션으로 동작을 하면 (2)에서 예외 발생시 B는 커밋되고 A는 롤백됨
  • 즉, 트랜잭션 전파 속성은 이미 진행중인 트랜잭션이 어떻게 영향을 미칠 수 있는가를 정의하는 것임
  • 트랜잭션 전파 속성 세 가지
    1. PROPAGATION_REQUIRED
      • 진행중인 트랜잭션이 없으면새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여함
      • DefaultTransactionDefinition 전파 속성은 PROPAGATION_REQUIRED임
    2. PROPAGATION_REQUIRES_NEW
      • 항상 새로운 트랜잭션을 시작함
      • 모든 트랜잭션은 독립적으로 동작함
      • 독립적인 트랜잭션이 보장돼야 하는 코드에 적용할 수 있음
    3. PROPAGATION_NOT_SUPPORTED
      • 트랜잭션 없이 동작하도록 할 수 있음
      • 트랜잭션 경계설정은 AOP를 이용해 한 번에 많은 메소드를 동시에 적용하는 방법 사용
      • 특정 메소드만 제외하는 포인트컷을 만드는 것은 복잡함
      • 모든 메소드에 트랜잭션 AOP가 적용되게 하고 특정 메소드의 전파 속성만 PROPAGATION_NOT_SUPPORTED를 사용하여 트랜잭션을 제외 시키는 것이 좋음
  • 트랜잭션 매니저가 getTransaction()를 사용하는 트랜잭션 전파 속성이 있기 때문임

6.6.4 격리수준

  • 모든 트랜잭션은 격리수준을 갖고 있어야 함
  • 적절한 격리수준 조정으로 가능한 많은 트랜잭션을 동시에 동작시키면서 문제가 발생하지 않도록 해야 성능이 증가
  • 필요시 트랜잭션 단위로 격리수준을 조정할 수 있음
  • DefaultTransactionDefinition의 격리 수준은 ISOLATION_DEFAULT임
  • 💡 DataSource에 설정되어 있는 디폴트 격리수준을 따른다는 의미임
  • 기본적으로 디폴트를 따르는 것이 좋지만, 특별한 작업을 수행하는 경우 독자적 격리수준을 지정할 필요가 있음

6.6.5 제한시간

  • DefaultTransactionDefinition는 제한시간이 없음
  • 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED나 PROPAGATION_REQUIRES_NEW와 함께 사용해야 의미가 있음

6.6.6 읽기전용

  • 읽기전용 설정시 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있음

6.6.7 디폴트 외 속성 적용

  • TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 지닌 TransactionAdvice임
  • 트랜잭션 정의를 바꾸고 싶으면 디폴트인 DefaultTransactionDefinition대신 외부에서 정의 된 TransactionDefinition 오브젝트를 DI 받으면 됨
  • TransactionDefinition 타입의 빈을 정의해두면 프로퍼티를 통해 원하는 속성을 지정할 수 있음
  • 하지만, 이 방법으로 트랜잭션 속성을 변경하려면 TransactionAdvice를 사용하는 모든 트랜잭션의 속성이 변경
  • 일부 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 없음

6.6.8 TransactionInterceptor

  • 메소드별로 다른 트랜잭션 정의를 적용하기 위해 어드바이스의 기능을 확장해야 함
  • 메소드 이름 패턴에 따라 트랜잭션 정의가 적용되도록 함
  • 기존의 TransactionAdvise 대신 TransactionInterceptor 사용
  • TransactionInterceptor는 TransactionAdvise와 비슷하지만,트랜잭션 정의를 메소드 이름 패턴을 이용해 다르게 지정할 수 있는 방법 추가로 제공
  • TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있음
  • Properties 타입의 프로퍼티 이름은 transactionAttribute로, 트랜잭션 속성을 정의한 프로퍼티임
  • TransactionAttribute 인터페이스는 TransactionDefinition의 네 가지 기본 항목에 rollbackOn()가 추가로 존재함
  • rollbackOn()은 예외 발생 시 롤백을 할 것인지 결정하는 메소드임
  • TransactionAdvise는 RuntimeException이 발생하는 경우에만 롤백함
  • 때문에, 체크 예외는 트랜잭션이 정상적으로 처리되지 않고 메소드를 빠져나감
  • TransactionInterceptor는 런타임 예외가 발생하면 트랜잭션은 롤백됨
  • 반면에 체크 예외는 스프링의 기본적 원칙에 따라 비즈니스 로직에 따른 의미가 있다고 판단하여 트랜잭션을 커밋함
  • 스프링의 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외 상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 예외는 런타임 예외로 포장해서 던짐
  • 하지만, TransactionInterceptor는 예외처리 기본 원칙을 따르지 않은 경우가 있음
  • TransactionAttribute의 rollbackOn()은 기본 원칙과 다른 예외 처리를 가능하게 함
  • 특정 체크 예외의 경우 롤백을 시키고, 특정 런타임 예외는 커밋하게 할 수 있음
  • TransactionInterceptor는 이런 TransactionAttribute를 Properties라는 일종의 Map 타입 오브젝트로 전달받음
  • 컬렉션을 사용하는 이유는 메소드 패턴에 따라 각기 다른 트랜잭션 속성을 부여하기 위함임

6.6.9 메소드 이름 패턴을 이용한 트랜잭션 속성 지정

  • Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키(Key)값(Value)으로 갖는 컬렉션임
  • 트랜잭션 속성은 밑과 같은 문자열로 정의할 수 있음
/*
PROPAGATION_NAME : 트랜잭션 전파 방식. 필수
ISOLATION_NAME : 격리수준. 생략시 디폴트
readOnly : 읽기전용. 생략가능(디폴트는 읽기전용 아님)
timeout_NNNN : 제한시간. 생략가능
-Exception1 : 체크 예외 중 롤백 대상으로 추가할 것을 넣음
+Exception2 : 런타임 예외 중 롤백하지 않을 예외를 넣음
*/
PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
  • 트랜잭션 전파 항목만 필수이고 나머지 생략 가능
  • 생략시 DefaultTransactionDefinition에 설정된 디폴트 속성이 부여됨
  • “+”, “-”는 기본 원칙을 따르지 않은 예외를 정의할 때 사용
  • 예시
<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>

/*
get으로 시작하는 메소드는 PROPAGATION_REQUIRED, 읽기전용, 제한시간 30초
upgrade로 시작하는 메소드는 PROPAGATION_REQUIRES_NEW, 완벽한 고립
나머지 메소드는 PROPAGATION_REQUIRED
*/
  • 문자열로 표현한 이유는 트랜잭션 속성을 메소드 패턴에 따라 일일이 중첩된 태그와 프로퍼티로 설정하게 만들면 번거롭기 때문임
  • 메소드가 하나 이상의 패턴과 일치하면 가장 정확히 일치하는 것에 적용됨

6.6.10 tx 네임스페이스를 이용한 방법

  • TransactionInterceptor 타입의 어드바이스 빈과 TransactionAttribute 타입의 속성 정보도 tx 스키마의 전용 태그를 이용해 정의할 수 있음
  • <bean> 태그에 비해 작성이 편해 tx 스키마를 사용해 어드바이스를 등록하도록 권장함
  • 예시
<beans 
    ...
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="...
                            http://www.springframework.org/schema/tx
                            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <!--transaction-manager가 transactionManager일 경우 생략 가능-->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30"/>
            <tx:method name="upgrade*" propagation="REQUIRES_NEW" isolation="SERIALIZABLE"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
</beans>

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

  1. 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용함
    • 일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는것이 바람직 함
    • 타깃 클래스의 각 메소드는 필요하는 트랜잭션 TransactionAttribute 속성 값이 다를 수 있음
    • 때문에, 트랜잭션용 포인트컷의 경우 비즈니스로직을 담은 클래스에 메소드 단위까지 세밀하게 포인트컷을 정의하면 안됨
    • 트랜잭션 경계로 삼을 클래스들이 설정됐다면, 패키지를 통채로 선택하거나 클래스 이름에서 일정 패턴을 포인트컷 표현식으로 만들면 됨
    • 관례적으로 비즈니스 로직 서비스를 담당하는 클래스 이름은 Service, ServiceImpl로 끝나는 경우가 많음
    • 이의 경우 “execution(**.. Service*.*(..))”로 포인트컷 표현식을 만들면 됨
    • 가능하면 클래스 보다 인터페이스 타입을 기준으로 타입 패턴을 적용하는 것이 좋음
    💡 A 인터페이스를 구현한 B’클래스는 A인터페이스를 상속받기 때문에 A 타입이라 할 수 있음. 때문에 인터페이스 타입 기준으로 패턴을 적용하면 상속받은 B 클래스도 포함됨
    • 메소드의 시그니처를 이용한 execution() 방식의 포인트컷 표현식 대신 스프링 빈 이름을 이용하는 bean() 표현식을 사용해도 됨
    • 빈 이름을 기준으로 선정하기 때문에 클래스나 인터페이스 이름에 일정한 규칙을 만들기 어려운 경우 유용함
    • 빈의 아이디가 Service로 끝나는 경우 bean(*Service)라 하면 됨
  2. 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의함
    • 몇 가지 트랜잭션 속성을 정의하고 그에 따라 적정한 메소드 명명 규칙을 만들어 사용하는 것이 유용함
    • 예외적인 경우에는 트랜잭션 어드바이스와 포인트컷을 새롭게 추가할 필요성이 있음
    • 디폴트 트랜잭션 속성 부여 예시
<tx:advice id="transactionAdvice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
  • 메소드 이름 패턴을 접두어에 붙여 공통된 규칙을 갖게 해야 함
  • 두 가지 트랜잭션 속성 패턴을 사용한 예
<aop:config>
    <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
    <aop:advisor advice-ref="batchTxAdvice" pointcut="execution(a.b.*BatchJob.*.(..))"/>
</aop:config>
    
<tx:advice id="batchTxAdvice">
    ...
</tx:advice>
    
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    ...
</tx:advice>
  • 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때 적용되지 않음
  • 전략이라기 보다는 주의사항
  • 프록시의 부가기능 적용은 클라이언트로부터 호출이 일어날 때만 가능함
  • 즉, 타킷 메소드 내부에서의 호출은 부가기능 적용안됨
  • PROPAGATION_REQUIRES_NEW를 사용하더라도 1, 3번에만 적용이 되지, 2번은 적용 안됨

6.6.12 트랜잭션 경계설정의 일원화

  • 부가기능을 여러 계층에서 중구난방으로 적용하는 것은 옳지 않음
  • 일반적으로 특정 계층의 경계와 트랜잭션 경계와 일치시키는 것이 바람직 함
  • 비즈니스 로직을 담고 있는 서비스 계층 경계와 트랜잭션 경계와 일치하는 것이 적절함
  • 서비스 계층을 트랜잭션이 시작되고 종료되는 경계로 설정 됐으면, 테스트와 같이 특별한 이유가 아니고서는 다른 계층이나 모듈에서 직접적으로 DAO에 접근하는 것을 차단해야 함
  • 트랜잭션을 통해 모든 DAO 접근 작업이 완료되거나 취소 되어야하기 때문에, 다른 계층이 DAO에 접근하기 위해서는 서비스 계층을 거쳐서 DAO에 접근해야 함
  • 예를 들어, 데이터를 추가하는 insert 작업일 경우 직접적으로 UserDao에 접근하지 않고, UserService의 add()함수를 거쳐야 함
  • UserService는 UserDao에 작업을 위임함
  • 이를 위해, UserDao 메소드 중에서 이미 서비스 계층에 부가적 로직을 담은 add()함수를 제외한 나머지를 UserService에 추가해야 함
public interface UserService {
    //이미 서비스 계층에 부가적 로직을 담아서 추가한 메서드
    void add(User user);

    //새로 추가한 메서드
    User get(String id);
    List<User> getAll();
    void deleteAll();
    void update(User user1);

    //UserService에서 만든 메소드
    void upgradeLevels();
}

public class UserServiceImpl implements UserService {
    @Override
    public User get(String id) {
        return userDao.get(id);
    }

    @Override
    public List<User> getAll() {
        return userDao.getAll();
    }

    @Override
    public void deleteAll() {
        userDao.deleteAll();
    }

    @Override
    public void update(User user1) {
        userDao.update(user1);
    }
		...
}

6.6.13 서비스 빈에 적용되는 포인트컷 표현식 등록

  • 기존 포인트컷 표현식을 모든 비즈니스 로직의 서비스 빈에 등록되도록 수정
<aop:config>
    <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
</aop:config>

6.6.14 트랜잭션 속성을 가진 트랜잭션 어드바이스 등록

  • tx 스키마 사용
<!--transaction-manager가 transactionManager일 경우 생략 가능-->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--propagation이 "REQUIRED"일 경우 생략 가능-->
        <tx:method name="get*" read-only="true"/>
        <tx:method name="*" />
    </tx:attributes>
</tx:advice>

6.6.15 트랜잭션 속성 학습 테스트

  • tx:attributes의 “get”으로 시작하는 메소드가 읽기전용으로 셋팅됨
  • 이에 대한 학습 테스트 생성
  • 예외 상황을 만들기 위해 롤백 테스트를 위한 TestUserService 이용
  • 예외는 TransientDataAccessResourceException임
public class UserServiceImpl implements UserService {
    public static class TestUserService extends UserServiceImpl {
        ...
        public List<User> getAll(){
            for (User user:super.getAll()){
                super.update(user); //읽기전용 속성으로 인해 예외 발생
            }
            return null;
        }
    }
}

public class UserServiceTest {
    ...
    @Test(expected = TransientDataAccessResourceException.class)
    public void readOnlyTransactionAttribute(){
        testUserService.getAll();
    }
}