본문 바로가기
Back-End/토비의 스프링

토비의 스프링 | 6장. AOP4 - AOP란 무엇인가?

by Havi 2017. 5. 2.
반응형

이제까지 UserService에 트랜잭션을 적용해 온 과정을 되짚어 보자.

1. 트랜잭션 서비스 추상화

트랜잭션 적용을 비즈니스 로직과 함께 넣었을 때 문제가 발생한다. 트랜잭션을 제외한 부분에서 수정작업이 있더라도 트랜잭션 적용 코드를 수정해야 하는 심각한 문제가 있었다. 그래서 인터페이스와 DI를 통해 무엇을 하는지를 남기고 분리하는 트랜잭션 추상화를 수행하였다. 트랜잭션 적용은 더 이상 비즈니스 로직 코드에는 영향을 주지 않고 독립적으로 변경할 수 있게 되었다.

2. 프록시와 데코레이션 패턴

여전히 비즈니스 로직에는 트랜잭션을 어디에 적용할지 경계설정을 담당하는 코드가 남아 있었다. 이를 제거하기 위해 DI를 이용한 데코레이터 패턴을 적용하였다. 클라이언트가 인터페이스와 DI를 통해 접근하도록 설계하고 데코레이터 패턴을 적용해서, 트랜잭션 부가기능을 자유롭게 적용할 수 있게 되었다. 그래서 클라이언트가 일종의 대리자인 프록시 역할을 하는 트랜잭션 데코레이터를 거쳐서 타깃에 접근할 수 있게 됐다.

3. 다이내믹 프록시와 프록시 팩토리 빈

데코레이션 패턴의 결과 비즈니스 로직에서 트랜잭션 코드는 모두 제거하였지만. 비즈니스 로직 인터페이스에 모두 트랜잭션을 부여받아 프록시 클래스를 만드는 작업은 큰 단점으로 남았다. 모든 코드에 일일이 다 구현해야 했다. 그래서 다이내믹 프록시 기술을 적용하여 부가기능 코드의 중복을 해결하고 메소드 선정 패턴을 이용할 수 있었다. 하지만 프록시를 여러 프로젝트에 적용할 경우 오브젝트 단위로는 중복이 일어났다. 그래서 다이내믹 프록시와 같은 프록시 기술을 추상화한 스프링의 프록시 팩토리 빈을 이용해서 다이내믹 프록시 생성 방법에 DI를 도입했다. 내부적으로 템플릿/콜백 패턴을 활용하는 스프링의 프록시 팩토리 빈 덕분에 부가기능을 담은 어드바이스와 포인트컷은 프록시에서 분리될 수 있었고 여러 프록시에서 공유해서 사용할 수 있었다.

4. 자동 프록시 생성 방법과 포인트컷

트랜잭션 적용 대상마다 일일이 프록시 팩토리 빈을 설정해야 하는 부담이 있었다. 이를 위해 스프링 컨테이너의 빈 생성 후처리 기법을 활용해 컨테이너 초기화 시점에서 자동으로 프록시를 만들어주는 방법을 도입했다. 포인트컷도 완전히 분리하였고 최종적으로 포인트컷 표현식을 써서 좀 더 편리하고 깔끔하게 설정할 수 있었다.

5. 부가기능의 모듈화

지금까지의 DI, 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷은 여기저기 흩어져 있는 트랜잭션 기능을 독립적인 모듈로 만들기 위한 방법이었다. 덕분에 TransactionAdvice라는 이름으로 모듈화하여 코드가 중복되지 않고 변경이 필요한 한 곳만 수정하면 되었다. 결국 핵심은 모듈화였다.

6. AOP: 애스펙트 지향 프로그래밍

부가기능을 어떻게 모듈화할 것인가를 연구한 사람들은 이 모듈화 작업이 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다. 그래서 새롭게 탄생한 이름이 **애스펙트(aspect)**이다.

  • 애스팩트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.
  • 애스팩트는 부가될 기능을 정의한 코드인 어드바이스와 정의 범위를 지정하는 포인트컷을 함께 갖고 있다.
  • 우리가 사용한 어드바이저는 아주 단순한 형태의 애스팩트라 볼 수 있다.
  • 2차원적인 평면구조가 아닌, 3차원의 다면체 구조로 가져가면서 각각 성격이 다른 부가기능은 다른 면에 존재하도록 만들었다. 이를 애스펙트 지향 프로그래밍(AOP)라 한다.

AOP 적용기술

스프링의 AOP는 우리가 사용했던 기법들을 활용한 프록시 방식의 AOP와 AspectJ를 사용하여 프록시를 쓰지 않는 AOP 기술이 있다. 그렇다면 어떻게 프록시 없이 부가기능을 타깃 오브젝트에 적용해 줄 수 있을까? AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용한다. 컴파일된 타깃의 클래스 파일 자체를 JVM에 로딩되는 시점을 가로채서 바이트코드로 조작하는 복잡한 방법을 사용한다. 즉, 트랜잭션 코드가 비즈니스 로직과 함께 있었을 때처럼 만들어버린다. 왜 프록시를 사용하지 않고 이러한 방법을 사용할까?

  • 1)바이트코드를 조작해 타깃 오브젝트를 직접 수정해버리면 DI 컨테이너의 도움을 받아 프록시를 생성하지 않고 AOP를 적용할 수 있기 때문이다.
  • 2)프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하기 때문이다.
    • 프록시를 사용하면 대상이 메소드로 제한되지만 바이트코드를 직접 조작하면 오브젝트의 생성, 필드 값의 조회와 조작, 스태틱 초기화 등의 다양한 부가기능을 수행할 수 있다.
    • 타깃 오브젝트가 생성되는 순간에도 작업이 가능하다.

AspectJ같은 고급 기술을 사용하기 위해 별도의 바이트코드 컴파일러, 클래스 로더를 사용해야 하는 번거로움이 필요하다. 따라서 대부분은 프록시 방식의 스프링 AOP로도 충분하다.

AOP 용어

  • 타깃: 부가기능을 부여할 대상, 경우에 따라 프록시 오브젝트일 수도 있다.
  • 어드바이스: 타깃에게 제공할 부가기능을 담은 모듈
    • MethodInterceptor처럼 메소드 호출 과정에 전반적으로 참여하는 것도 있고 예외가 발생했을 때만 동작하는 어드바이스도 있다.
  • 조인 포인트: 어드바이스가 적용될 수 있는 위치. 프록시 AOP에서는 메소드의 실행 단계분이다.
  • 포인트컷: 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다.
    • 스프링 AOP의 조인 포인트는 메소드의 실행이므로 포인트컷은 메소드 선정기능을 같는다.
    • 그래서 포인트컷 표현식은 메소드의 실행의 의미인 execution으로 시작한다.
    • 메소드 선정이란 클래스를 선정하고 그 안의 메소드를 선정하는 과정이다.
  • 프록시: 클라이언트와 타깃 사이의 부가기능을 제공하는 오브젝트이다.
    • DI를 통해 타깃 대신 클라이언트에게 주입되며, 클라이언트의 메소드 호출을 대신 받아서 타깃에 위임해 준다. 그 과정에서 부가기능을 수행한다.
  • 어드바이저: 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트이다. AOP의 가장 기본이 되는 모듈.
  • 애스펙트: 애스팩트는 AOP의 기본 모듈이다.
    • 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들며 보통 싱글톤 형태의 오브젝트이다.
    • 모듈 정의와 오브젝트와 같은 실체의 구분이 없으며 모두 애스펙트라 불린다.
    • 스프링의 어드바이저는 아주 단순한 애스펙트라 볼 수 있다.

AOP 네임스페이스

스프링의 프록시 방식 AOP를 적용하려면 최소한 네 가지 빈을 등록해야 한다.

  • 자동 프록시 생성기: DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록한다.
    • 빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성하는 기능을 담당한다.
  • 어드바이스: 부가기능을 구현한 클래스를 빈으로 등록한다.
  • 포인트컷: 스프링의 AspectJExpressionPointcut을 빈으로 등록하고 expression 프로퍼티에 포인트컷 표현식을 넣어주면 된다.
  • 어드바이저: 스프링의 DefaultPointcutAdvisor 클래스를 빈으로 등록해서 사용한다.
    • 어드바이스와 포인트컷을 프로퍼티로 참조하는 것 외의 기능은 없다.
    • 자동 프록시 생성기에 의해 자동 검색된다.
<aop:config>
	<aop:pointcut id="transactionPointcut" expression="execution(* *..*ServiceImpl.upgrade*(..))" />
	<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut" />
</aop:config>

<!--어드바이저와 결합된 형태(더 간편하게 사용)-->
<aop:config>
	<aop:advisor advice-ref="transactionAdvice" pointcut-ref="execution(* *..*ServiceImpl.upgrade*(..))" />
</aop:config>

트랜잭션 속성

트랜잭션을 설명하면서 제대로 설명을 못했던 부분이 있었다. 트랜잭션을 불러와서 동작을 수행하고 commit(), rollback() 중 하나를 수행하게끔 되어 있다. 트랜잭션 매니저에게 전달하는 DefaultTransactionDefinition는 무슨 용도일까?

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;
    }
}

트랜잭션 정의

DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다.

1)트랜잭션 전파(transaction propagation)

트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. 그림에서 A의 트랜잭션을 그대로 B까지 가져간다면 B의 동작이 수행되고 (2)에서 예외가 발생하면 A, B의 디비작업이 둘 다 rollback될 것이다. 반대로 따로따로 가져간다면 (2)에서 예외가 수행되도 영향을 주지 않을 것이다. 이러한 방식을 트랜잭션 전파라 부르며 속성은 다음과 같다.

  • PROPAGATION_REQUIRED
    • 가장 많이 사용하며 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다.
    • A, B가 모두 PROPAGATION_REQUIRED로 선언되어 있다면 A, B, A->B, B->A 모두 가능하다.
    • DefaultTransactionDefinition의 트랜잭션 전파 속성이 PROPAGATION_REQUIRED이다.
  • RPOPAGATION_REQUIRES_NEW
    • 항상 새로운 트랜잭션을 시작한다. 즉, 이전에 시작된 트랜잭션에 상관없이 항상 새로운 트랜잭션을 만들어 시작한다.
  • PROPAGATION_NOT_SUPPORTED
    • 트랜잭션 없이 동작하도록 만들 수도 있다. 진행 중인 트랜잭션이 있어도 무시한다.
    • 트랜잭션을 무시하는 속성을 둔 이유는 여러 메소드 동작시 특정 메소드만 트랜잭션 적용을 제외시키기 위해서 이다.
    • 물론 포인트컷을 잘 만들어서 제외시킬 수도 있지만 그러면 포인트컷이 상당히 복잡해질 수 있다.

2)격리수준(isolation level)

모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다. 여러 트랜잭션을 순차적으로 실행시킬 수도 있지만 이러면 성능이 떨어진다. 따라서 적절한 격리수준을 조정해서 가능한 여러 트랜잭션을 동시에 진행하면서 문제가 발생하지 않는 제어가 필요하다.

  • 격리수준은 기본적으로 DB에 설정되어 있지만 DataSource 등에서 재설정할 수 있다.
  • DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT이다. 이는 DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻이다.

3)제한시간(timeout)

DefaultTransactionDefinition의 기본 설정은 제한시간이 없다. 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED나 RPOPAGATION_REQUIRES_NEW와 함께 사용해야만 의미가 있다.

4)읽기전용(read only)

읽기전용으로 설정하면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 액세스 기술에 따라서 성능이 향상될 수 있다.

커스터마이징하게 트랜잭션 동작방식을 바꾸고 싶다면 DefaultTransactionDefinition 대신 따로 정의한 TransactionDefinition 오브젝트를 DI 받아서 사용하도록 만들면 된다.

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

메소드별로 트랜잭션을 다르게 적용하려면 어드바이스의 기능을 확장하여 메소드 이름 패턴에 따라 다른 트랜잭션 정의가 적용되도록 만든다.

TransactionInterceptor

이미 스프링에는 편리하게 트랜잭션 경계설정 어드바이스로 사용하도록 만들어진 TransactionInterceptor가 존재한다. 기존까지 사용해온 TransactionAdvice는 어드바이스의 동작원리를 알아보려고 만들었던 것이므로 그만 사용하고 이제 TransactionInterceptor를 사용해보자. TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있다. 트랜잭션 매니저 프로퍼티는 잘 알고 있지만 Properties 타입은 두 번째 프로퍼티 이름은 transactionAttributes로, 트랜잭션 속성을 정의한 프로퍼티다. TransactionDefinition은 네 가지 기본 항목에 rollbackOn()이라는 메소드를 하나 더 갖고 있는 TransactionAttribute 인터페이스로 정의된다.

  • TransactionAdvice는 RuntimeException이 발생하는 경우에만 트랜잭션을 롤백시킨다.
  • 런타임 예외 이외의 모든 예외에서 트랜잭션 롤백을 시켜야 할까?
    • 그래서는 안 된다. 비즈니스 로직상의 예외 경우를 나타내기 위해 타깃 오브젝트가 체크 예외를 던지는 경우에는 DB 트랜잭션은 커밋시켜야 하기 때문이다.
    • 스프링의 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 순수한 예외의 경우는 런타임 예외로 포장돼서 전달한다.
  • 그런데 TransactionInterceptor의 이러한 예외처리 따르지 않고 TrasactionAttribute는 rollbackOn()이라는 속성을 둬서 기본 원칙과 다른 예외처리가 가능하도록 해준다.

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

Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬랙션이다. 트랜잭션 속성은 다음과 같이 문자열로 정의한다.

  • PROPAGATION_NAME - 트랜잭션 전파 방식, 필수항목이다. PROPAGATION_으로 시작
  • ISOLATION_NAME - 격리수준, ISOLATION_으로 시작한다. 생략 가능하고 생략하면 디폴트 격리 수준으로 지정된다.
  • readOnly - 일기전용 항목. 디폴트는 읽기전용이 아니다. 생략 가능
  • timeout_NNNN - 제한시간, timout_으로 시작하고 초 단위 시간을 뒤에 붙인다. 생략 가능
  • -Exception1 - 체크 예외 중에서 롤백 대상으로 추가할 것을 넣는다. 한 개 이상을 등록 가능
  • +Exception2 - 런타일 예외지만 롤백시키지 않을 예외들을 넣는다. 한 개 이상 등록 가능

전파 항목만 빼고 모두 생락하면 모두 DefaultTransactionDefinition에 설정된 디폴트 속성이 부여 된다. 모든 항목이 구분되어 순서도 상관 없다.

    • 또는 -로 시작하는 건 기본 원칙을 따르지 않는 예외를 정의해주는 것이다.
  • 모든 런타임 예외는 롤백돼야 하지만 +XXXRuntimeException이라고 해주면 런타일 예외라도 커밋하게 만들 수 있다.
  • 체크 예외는 모두 커밋하는 것이 기본 처리 방식이지만 -를 붙여서 넣어주면 트랜잭션은 롤백 대상이 된다.
  • 이렇게 하면 대부분은 디폴트를 사용해도 충분하므로 편리해 진다.
<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_REQURED이면서 읽기전용이고 시간제한 30초이다.
    • 그런데 읽기전용이 아닌 트랜잭션이 get으로 들어온다면? 다행히 트랜잭션 속성이 readOnly나 timeout 등은 트랜잭션이 처음 시작될 때가 아니라면 적용되지 않는다.
    • get으로 시작하는 메소드에서 트랜잭션을 시작하는 경우 읽기전용에 제한시간이 적용되지만 그 외의 경우에는 진행 중인 트랜잭션의 속성을 따르게 되어 있다.
  • 두 번째는 upgrade로 시작하는 메소드는 항상 독립적인 트랜잭션으로 동작하도록 트랜잭션 전파 항목을 PROPAGATION_REQUIRES_NEW로 설정했다.
    • 다른 동시 작업에 영향받지 않도록 격리수준을 최고 수준인 ISOLATION_SERIALIZABLE로 설정했다.
  • 세 번째는 *만 사용해서 위의 두 가지 조건에 해당되지 않는 나머지 모든 메소드에 해당된다.

프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다

타깃 오브젝트가 자기 자신의 메소드를 호출할 때는 프록시를 통한 부가기능의 적용이 일어나지 않는다. 아래 그림의 1,3은 부가기능이 적용될 것이지만 2는 아니다.

  • 해결 방법 첫 번째는 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤에 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제하는 방법이다.
    • 하지만 비즈니스 로직만 남겨두도록 했던 의도와는 다르게 이는 바람직 하지않다.
  • 두 번째는 AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 기술을 적용하는 것이다.
    • 지금까지의 대부분의 설정을 그대로 둔 채 간단한 옵션을 바꿈으로써 AspectJ 방식으로 트랜잭션 AOP가 적용되게 할 수 있다.

트랜잭션 속성 적용

이제 트랜잭션 속성을 UserService에 적용해보자.

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

  • 아키텍처를 단순하게 가져가면 서비스 계층과 DAO를 통합할 수도 있지만 이후의 확장성과 안정성을 위해 비즈니스 로직을 독자적으로 두고 테스트하려면 서비스 계층을 만들어 사용한다.
  • 아래 UserService는 단순한 CRUD 메소드지만 add() 메소드처럼 단순 위임 이상의 로직을 가질 수 있다.
public interface UserService {
    void add(User user);
    User get(String id);
    List<User> getAll();
    void deleteAll();
    void update(User user);
    
    void upgradeLevels();
}

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

aop 태그를 이용해 포인트컷, 어드바이저 등의 설정을 단순한 빈 이름 패턴을 이용해 설정한다.

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

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

get은 읽기 전용으로 하고 나머지는 디폴트 트랜잭션 속성을 따르게 해준다. 이후 트랜잭션이 설정한데로 옳바르게 동작하는지 테스트를 해 준다.

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

애노테이션 트랜잭션 속성과 포인트컷

이제까지 일괄적으로 적용하는 방식을 살펴봤다. 하지만 경우에 따라 세밀하게 트랜잭션을 적용해 주어야 할 때가 있다. 이러한 상황에서 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법이 있다.

트랜잭션 애노테이션

@Transactional

...

@Target({ElementType.METHOD, ElementType.TYPE}) //사용할 대상을 지정. 메소드와 타입(클래스, 인터페이스)처럼 한 개 이상의 대상 지정 가능.
@Retention(RetentionPolicy.RUNTIME) //애노테이션 정보가 언제까지 유지되는지 지정. 이러한 설정은 런타임 때도 애노테이션 정보를 리플렉션을 통해 얻을 수 있다.
@Inherited //상속을 통해서도 애노테이션 정보를 얻을 수 있다.
@Documented
public @interface Transactional {
    //트랜잭션 속성의 모든 항목을 엘리먼트로 지정할 수 있다. 디폴트 값이 설정되어 있으므로 모두 생략 가능.
    String value() default "";
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    boolean readOnly() default false;
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}
  • @Transactional 애노테이션의 타깃은 메소드와 타입이다. 따라서 메소드, 클래스, 인터페이스에 사용할 수 있다.
  • @Transactional 애노테이션이 사용되는데 쓰이는 포인트컷은 TransactionAttributeSourcePointcut이다.
  • TransactionAttributeSourcePointcut은 스스로 표현식과 같은 선정기준은 없지만 애노테이션이 부여된 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과를 돌려준다.(포인트컷의 자동등록)

트랜잭션 속성을 이용하는 포인트컷

TransactionInterceptor는 메소드 이름 패턴을 통해 부여되는 일괄적인 트랜잭션 속성정보 대신 @Transactional 애노테이션의 엘리먼트에서 트랜잭션 속성을 가져오는 AnnotationTransactionAttributeSource를 사용한다. 이를 통해 @Transactional은 메소드마다 다르게 설정하는 유연함을 제공한다. 동시에 포인트컷도 @Transactional을 통한 트랜잭션 속성정보를 참조하도록 만든다.

메소드마다 애노테이션을 부여로 인한 코드 중복 대체 정책

스프링은 @Transactional에 대한 4단계의 대체정책을 제공한다. 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스)의 순서에 차례로 확인한다. 다음의 예를 보자. @Transactional이 부여될 위치는 총 6개이다.

[1]
public interface Service {
    [2]
    void method1();
    [3]
    void method2();
}
[4]
public class ServiceImpl implements Service {
    [5]
    public void method1() {
    }
    [6]
    public void method2() {
    }
}
  • 타깃 오브젝트의 메소드부터 시작해서 @Transactional 애노테이션이 존재하는지 확인힌다. 따라서 [5], [6]이 첫 번째 후보이다.
  • 여기서 발견되지 못하면 다음은 [4]에 존재하는지 확인한다.
  • 구현 메소드가 가장 우선이고 그 다음이 구현 클래스로 대체 정책이 넘어간다.
  • 그 다음은 인터페이스의 메소드 [2], [3]을 확인한다.
  • 마지막 순서는 인터페이스의 클래스 [1]을 확인한다.
  • 인터페이스를 사용하는 프록시 방식의 AOP가 아닌 방식으로 트랜잭션을 적용하면 인터페이스에 정의한 @Transactional은 무시되기에 타깃 클래스에 애노테이션을 두는 것이 바람직하다.

트랜잭션 애노테이션 사용을 위한 설정

<tx: annotaion-driven />

트랜잭션 애노테이션 적용

다음의 xml 설정을 애노테이션을 이용한 설정으로 바꿔보자.

<tx:attributes>
	<tx:method name="get*" read-only="true" />
	<tx:method name="*" />
</tx:attributes>
@Transactional
public interface UserService {
    void add(User user);
    void deleteAll();
    void update(User user);
    void upgradeLevels();
    
    @Transactional(readOnly=true)
    User get(String id);
    
    @Transactional(readOnly=true)
    List<User> getAll();
}
  • 애노테이션의 'get*'을 적용하려면 각각의 메소드마다 개별적으로 붙여줄 수 밖에 없다.
  • 나머지는 class에 걸려있는 디폴트 설정이 들어갈 것이다.

기타 트랜잭션 애노테이션

@Rollback

테스트 클래스에서 rollback은 기본 적용이 true이다. 만약 테스트를 끝내고 변경된 사항을 그대로 적용하고 싶다면 @Rollback(false)라고 해줘야 한다.

@TransactionConfiguration

@Rollback 애노테이션은 메소드 레벨에만 적용 가능하다. @TransactionConfiguration(defaultRollback=false)를 클래스 부분에 선언해 주면 메소드에 일일이 선언할 필요없이 한번에 가능하다.

@NotTransactional / Propagation.NEVER

@NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행한다. 이는 3.0에서 deprecated 되었기 때문에 사용하기에는 별루이다. 따라서 @Transactional(propagation=Propagation.NEVER)을 지정해주면 동일한 효과를 가질 수 있다.

반응형

댓글