티스토리 뷰

이전 글 : [토비의 스프링] 6. AOP (1)

지금까지 해왔던 작업은 비즈니스 로직에 반복적으로 등장하는 트랜잭션 코드를 투명하게 분리해내는 것이었다. 투명하다는 것은 투명한 유리를 사이에 둔 것처럼, 부가기능을 적용한 후에도 기존 설계와 코드에는 영향을 주지 않고 메소드가 호출되는 과정에 다이내믹하게 참여해서 부가적인 기능을 제공해주도록 만들었다는 것이다.


자동 프록시 생성

자동 프록시 생성 빈 후처리기

투명한 부가기능을 적용하는 과정에서 발견됐던 거의 대부분의 문제는 제거했다. 타깃 코드는 깔끔하고, 부가기능은 한 번만 만들어 모든 타깃과 메소드에 재사용이 가능하고, 타깃의 적용 메소드를 선정하는 방식도 독립적으로 작성할 수 있도록 분리되어 있다.

하지만 아직 한 가지 해결할 과제가 남아 있다. 프록시 팩토리 빈 방식의 접근 방법의 한계라고 생각했던 두 가지 문제가 있었다. 그중에서 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 스프링 ProxyFactoryBean의 어드바이스를 통해 해결됐다. 남은 것은 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해주는 부분이다. 새로운 타깃이 등장할 때마다 비슷한 설정을 매번 복사해주어야 한다. 이런 류의 중복은 어떻게 할까?

스프링은 OCP의 가장 중요한 요소인 유연한 확장이라는 개념을 스프링 컨테이너 자신에게도 다양한 방법으로 적용하고 있다. 스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공하고 있다.

그중에서 관심을 가질 만한 확장 포인트는 바로 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기다. 빈 후처리기는 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다. 그중 DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기다. 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 바로 이것이 자동 프록시 생성 빈 후처리기다. DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다. 프록시 적용 대상이면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결해준다. 컨테이너는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용한다.


포인트컷

확장된 포인트컷

한 가지 이상한 점은 지금까지 포인트컷은 부가기능을 적용할 메소드를 판별하는 역할을 수행한다고 했는데, 위에서는 어떤 빈에 프록시를 적용할지를 선택한다고 했던 점이다. 사실 포인트컷은 클래스 필터와 메소드 매처 두 가지를 돌려주는 메소드를 갖고 있다.

public interface Pointcut {
    ClassFilter getClassFilter(); // 프록시를 적용할 클래스인지 확인해준다.
    MethodMatcher getMethodMatcher(); // 어드바이스를 적용할 메소드인지 확인해준다.
}

지금까지는 포인트컷의 메소드 매처만을 사용해 메소드를 선별하는 작업만 해왔던 것이었다. 선정 기능을 모두 사용한다면 먼저 프록시를 적용할 클래스인지 판단하고 나서, 적용 대상 클래스일 경우 어드바이스를 적용할 메소드인지 확인하는 식으로 동작한다.

포인트컷 표현식

좀 더 편리한 포인트컷 작성 방법을 알아보자.

포인트컷 표현식

포인트컷 표현식은 일종의 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법이다. 포인트컷 표현식을 지원하는 포인트컷을 사용하려면 AspectJExpressionPointcut 클래스를 사용하면 된다.

앞서 만들었던 포인트컷은 클래스와 메소드의 이름의 패턴을 독립적으로 비교하도록 만들어져 있다. 하지만 AspectJExpressionPointcut은 클래스와 메소드의 선정 알고리즘을 포인트컷 표현식을 이용해 한 번에 지정할 수 있게 해준다. 사실 스프링이 사용하는 포인트컷 표현식은 AspectJ라는 유명한 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것이다. 그래서 이를 AspectJ 포인트컷 표현식이라고도 한다.

AspectJ 포인트컷 표현식은 포인트컷 지시자를 이용해 작성한다. 포인트컷 지시자 중에서 가장 대표적으로 사용되는 것은 execution()이다. 문법은 다음과 같다.

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...) [throws 예외 패턴])

문법만 보면 복잡해 보이지만, 사실 메소드의 풀 시그니처를 문자열로 비교한다고 생각하면 쉽다. 다음과 같이 말이다.

public int springbook.learningtest.spring.pointcut.Target.minus(int,int) throws java.lang.RuntimeException

대괄호로 표현한 부분인 접근제한자, 이름패턴에 붙는 타입패턴, 예외 패턴은 생략 가능하고 리턴 타입의 타입패턴, 이름패턴, 파라미터 타입은 필수항목이다. 모든 항목에 대해 적용하고 싶으면 *을 사용한다. 옵션 항목을 생략하면 다음과 같이 간단하게도 사용할 수 있다.

execution(int minus(int,int)) // int 타입의 리턴 값, minus라는 메소드 이름, 두 개의 int 파라미터를 가진 모든 메소드를 선정하는 포인트컷 표현식
execution(* minus(int,int)) // 리턴 타입은 상관없이
execution(* minus(..)) // 리턴 타입과 파라미터의 종류, 개수에 상관없이
execution(* *(..)) // 모든 메소드 허용

AspectJ 포인트컷 표현식은 execution() 뿐만 아니라 여러 가지 표현식 스타일을 갖고 있다. 예를 들어 빈의 이름으로 비교하는 bean()도 있고, 다음과 같이 사용하면 @Transactional이라는 애노테이션이 적용된 부분을 가지고 올 수도 있다.

@annotation(org.springframework.transaction.annotation.Transactional)

표현식을 사용할 때 주의할 점은 클래스 이름에 적용되는 패턴이 클래스 이름 패턴이 아니라 타입 패턴이라는 것이다. 타입 패턴이 적용된다는 것은 어떤 클래스가 상속 받고 있는 부모 클래스 타입, 혹은 구현한 인터페이스 타입까지도 적용될 수 있다는 것을 의미한다. 쉽게 말해 자식 클래스 혹은 구현 클래스가 부모 타입, 인터페이스 타입을 표현한 표현식에 해당된다는 뜻이다.


AOP란 무엇인가?

트랜잭션 적용 과정

지금까지 UserService에 트랜잭션을 적용해온 과정을 정리해보자.

트랜잭션 경계설정 코드를 비즈니스 로직을 담은 코드에 넣으면서 맞닥뜨린 첫 번째 문제는 특정 트랜잭션 기술에 종속되는 코드가 돼버린다는 것이었다. 그래서 트랜잭션 적용이라는 추상적인 작업 내용은 유지한 채로 구체적인 구현 방법을 자유롭게 바꿀 수 있도록 서비스 추상화 기법을 적용했다.

추상화를 통해 트랜잭션을 어떻게 다룰 것인가는 코드에서 제거했지만, 여전히 비즈니스 코드에는 트랜잭션의 잔재가 남아 있었다. 트랜잭션을 어디에 적용할 것인가는 여전히 코드에 노출시켜야 했고, 심지어 대부분의 비즈니스 로직을 담은 메소드에 필요한 기능이었다. 그래서 도입한 것이 바로 DI를 이용해 데코레이터 패턴을 적용하는 방법이었다. 데코레이터 패턴을 적용해서, 비즈니스 로직을 담은 클래스에 코드에는 전혀 영향을 주지 않으면서 트랜잭션 부가기능을 자유롭게 부여했다.

프록시를 이용해서 비즈니스 로직 코드에서 트랜잭션 코드는 모두 제거할 수 있었지만, 비즈니스 로직 인터페이스의 모든 메소드마다 트랜잭션 기능을 부여하는 코드를 넣어 프록시 클래스를 만드는 작업이 오히려 큰 짐이 됐다. 그래서 프록시 클래스 없이도 프록시 오브젝트를 런타임 시에 만들어주는 JDK 다이내믹 프록시 기술을 적용했다.

트랜잭션 적용 대상이 되는 빈마다 일일이 프록시 팩토리 빈을 설정해줘야 한다는 부담이 남아 있었다. 이를 해결하기 위해 스프링 컨테이너의 빈 생성 후처리 기법을 활용해 컨테이너 초기화 시점에서 자동으로 프록시를 만들어주는 방법을 도입했다. 프록시를 적용할 대상을 일일이 지정하지 않고 패턴을 이용해 자동으로 선정할 수 있도록, 클래스를 선정하는 기능을 담은 확장된 포인트컷을 사용했다.

AOP, Aspect Oriented Programming

트랜잭션 적용 코드는 기존에 써왔던 코드 분리, 관심사의 분리, 인터페이스 도입, DI 등의 방법으로는 간단하게 분리할 수 없었다. 왜냐하면 트랜잭션 경계설정 기능은 다른 모듈의 코드에 부가적으로 부여되는 기능이라는 특징이 있기 때문이다. 그래서 트랜잭션 코드는 한데 모을 수 없고, 애플리케이션 전반에 여기저기 흩어져 있다. 따라서 트랜잭션 경계설정 기능을 독립된 모듈로 만들려면 지금까지 적용해온 다이내믹 프록시나 빈 후처리 기술 같은 복잡한 기술이 필요하다.

트랜잭션 같은 부가기능은 핵심기능과 같은 방식으로는 모듈화하기가 매우 힘들다. 이름 그대로 부가기능이기 때문에 스스로는 독립적인 방식으로 존재해서는 적용되기 어렵기 때문이다. 트랜잭션 부가기능이란 트랜잭션 기능을 추가해줄 다른 대상, 즉 타깃이 존재해야만 의미가 있다. 따라서 각 기능을 부가할 대상인 각 타깃의 코드 안에 침투하거나 긴밀하게 연결되어 있지 않으면 안 된다.

결국 지금까지 해온 모든 작업은 핵심기능에 부여되는 부가기능을 효과적으로 모듈화하는 방법을 찾는 것이었고, 어드바이스와 포인트컷을 결합한 어드바이저가 단순하지만 이런 특성을 가진 원시적인 형태로 만들어지게 됐다.

전통적인 객체지향 기술의 설계 방법으로는 독립적인 모듈화가 불가능한 부가기능을 어떻게 모듈화할 것인가를 연구해온 사람들은, 이 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다. 그래서 이런 부가기능 모듈을 객체지향에서 사용하는 오브젝트와는 다르게 특별한 이름으로 부르기 시작했는데, 그것이 바로 애스펙트(aspect)다. 애스펙트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.

애스펙트는 부가될 기능을 정의한 코드인 어드바이스와, 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있다. 애스펙트는 그 단어의 의미대로 애플리케이션을 구성하는 한 가지 측면이라고 생각할 수 있다. 핵심기능 코드 사이에 침투한 부가기능을 독립적인 모듈인 애스펙트로 구분해내면서, 핵심기능은 순수하게 그 기능을 담은 코드로만 존재하고 독립적으로 살펴볼 수 있도록 구분된 면에 존재하게 된 것이다.

이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법애스펙트 지향 프로그래밍(Aspect Oriented Programming) 또는 약자로 AOP라고 부른다. AOP는 OOP를 돕는 보조적인 기술이지 OOP를 완전히 대체하는 새로운 개념은 아니다. AOP는 결국 애플리케이션을 다양한 측면에서 독립적으로 모델링하고, 설계하고, 개발할 수 있도록 만들어주는 것이다. 애플리케이션을 특정한 관점을 기준으로 바라볼 수 있게 해준다는 의미에서 AOP를 관점 지향 프로그래밍이라고도 한다.

AOP 적용 기술

스프링 AOP의 핵심은 프록시를 이용했다는 것이다. 프록시로 만들어서 DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여해서 부가기능을 제공해주도록 만들었다. 그리고 프록시 방식을 사용했기 때문에 메소드 호출 과정에 참여해서 부가기능을 제공해준다. 따라서 스프링 AOP의 부가기능을 담은 어드바이스가 적용되는 대상은 오브젝트의 메소드다.

독립적으로 개발한 부가기능 모듈을 다양한 타깃에 다이내믹하게 적용해주기 위해 가장 중요한 역할을 맡고 있는 게 바로 프록시다. 그래서 스프링 AOP는 프록시 방식의 AOP라고 할 수 있다.

프록시 방식이 아닌 AOP도 물론 있다. AOP 기술의 원조이자, 가장 강력한 AOP 프레임워크로 불리는 AspectJ는 프록시를 사용하지 않는 대표적인 AOP 기술이다. AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용한다. 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용하는 것이다. 왜 그런 복잡한 방법을 사용할까?

첫째는 바이트코드를 조작해서 타깃 오브젝트를 직접 수정해버리면 스프링과 같은 DI 컨테이너의 도움을 받아서 자동 프록시 생성 방식을 사용하지 않아도 AOP를 적용할 수 있기 때문이다. 둘째는 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하기 때문이다. 프록시 방식의 AOP는 부가기능 부여 대상이 메소드로 한정된다. 하지만 바이트코드를 직접 조작해서 AOP를 적용하면 오브젝트의 생성, 필드 값의 조회와 조작, 스태틱 초기화 등의 다양한 작업에 부가기능을 부여해줄 수 있다.

물론 대부분의 부가기능은 프록시 방식을 사용해 메소드의 호출 시점에 부여하는 것으로 충분하다. 간혹 특별한 요구사항이 생겨서 스프링 AOP의 기능을 넘어서는 기능이 필요하다면, 그때는 AspectJ를 동시에 적용해줄 수도 있다.

AOP의 용어

AOP에서 많이 사용하는 몇 가지 용어를 살펴보자.

타깃

타깃은 부가기능을 부여할 대상이다.

어드바이스

어드바이스는 타깃에게 제공할 부가기능을 담은 모듈이다. 어드바이스는 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의할 수도 있다. 또 메소드 호출 과정에 전반적으로 참여하는 것도 있고, 예외가 발생했을 때만 동작하는 어드바이스처럼 메소드 호출 과정의 일부에서만 동작하는 어드바이스도 있다.

조인 포인트

조인 포인트란 어드바이스가 적용될 수 있는 위치를 말한다. 스프링의 프록시 AOP에서 조인 포인트는 메소드의 실행 단계뿐이다. 타깃 오브젝트가 구현한 인터페이스의 모든 메소드는 조인 포인트가 된다.

포인트컷

포인트컷이란 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다. 스프링 AOP의 조인 포인트는 메소드의 실행이므로 스프링의 포인트컷은 메소드를 선정하는 기능을 갖고 있다.

프록시

프록시는 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트다. DI를 통해 타깃 대신 클라이언트에게 주입되며, 클라이언트의 메소드 호출을 대신 받아서 타깃에 위임해주면서, 그 과정에서 부가기능을 부여한다.

어드바이저

어드바이저는 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트다. 어드바이저는 어떤 부가기능(어드바이스)을 어디에(포인트컷) 전달할 것인가를 알고 있는 AOP의 가장 기본이 되는 모듈이다. 어드바이저는 스프링 AOP에서만 사용되는 특별한 용어이고, 일반적인 AOP에서는 사용되지 않는다.

애스펙트

OOP의 클래스와 마찬가지로 애스펙트는 AOP의 기본 모듈이다. 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재한다. 따라서 클래스와 같은 모듈 정의와 오브젝트와 같은 인스턴스의 구분이 특별히 없다. 두 가지 모두 애스펙트라고 불린다.


트랜잭션 속성

트랜잭션 전파

스프링의 트랜잭션 추상화를 설명하면서 그냥 넘어간 부분이 있다. 트랜잭션 매니저에서 트랜잭션을 가져올 때 사용한 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의 용도가 무엇인지 알아보자. 트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아니다. DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다.

트랜잭션 전파

트랜잭션 전파란 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. 트랜잭션 A가 진행되는 도중 트랜잭션 B가 시작되었을 때 어떻게 해줄 것인가에 대한 설정이다. 대표적으로 다음과 같은 트랜잭션 전파 속성을 줄 수 있다.

- PROPAGATION_REQUIRED : 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. 가장 많이 사용하고, DefaultTransactionDefinition도 이 속성을 사용한다.
- PROPAGATION_REQUIRES_NEW : 항상 새로운 트랜잭션을 시작한다.
- PROPAGATION_NOT_SUPPORTED : 트랜잭션 없이 동작하도록 만든다. 하나의 트랜잭션 내에 여러 메소드 중 특별한 메소드만 트랜잭션 적용에서 제외하려고 할 때 유용하게 쓸 수 있다.

트랜잭션 매니저를 통해 트랜잭션을 시작하려고 할 때 getTransaction()이라는 메소드를 사용하는 이유는 바로 이 트랜잭션 전파 속성이 있기 때문이다. 트랜잭션 전파 속성과 현재 진행 중인 트랜잭션이 존재하는지 여부에 따라서 새로운 트랜잭션을 시작할 수도 있고, 이미 진행 중인 트랜잭션에 참여하기만 할 수도 있다.

격리 수준

모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다. 서버환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있다. 가능하다면 모든 트랜잭션이 순차적으로 진행돼서 다른 트랜잭션의 작업에 독립적인 것이 좋겠지만, 그러자면 성능이 크게 떨어질 수밖에 없다. 따라서 적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 하는 제어가 필요하다. DefaultTransactionDefinition에 설정된 격리수준은 DataSource에 설정되어 있는 격리수준을 따른다는 뜻의 ISOLATION_DEFAULT다.

제한시간

트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다. DefaultTransactionDefinition의 기본 설정은 제한시간이 없는 것이다.

읽기전용

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

트랜잭션 정의를 수정하려면 어떻게 해야 할까? TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 가진 TransactionAdvice다. DefaultTransactionDefinition을 사용하는 대신 외부에서 정의된 TransactionDefinition 오브젝트를 DI 받아서 사용하도록 만들면 된다. 하지만 이 방법으로 트랜잭션 속성을 변경하면 TransactionAdvice를 사용하는 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 잇다. 원하는 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 있는 방법은 없을까?

트랜잭션 인터셉터

메소드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다. 스프링에는 편리하게 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 만들어진 TransactionInterceptor가 존재한다. TransactionInterceptor 어드바이스의 동작방식은 기존의 TransactionAdvice와 크게 다르지 않고, 다만 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공해줄 뿐이다.

TransactionInterceptor의 두 번째 프로퍼티는 Properties 타입의 transactionAttributes인데, 트랜잭션 속성을 정의한 프로퍼티다. 트랜잭션 속성은 TransactionDefinition의 네 가지 기본 항목에 rollbackOn()이라는 메소드를 하나 더 갖고 있는 TransactionAttribute 인터페이스로 정의된다. rollbackOn()은 어떤 예외가 발생하면 롤백을 할 것인가를 결정하는 메소드다. 이 TransactionAttribute를 이용하면 트랜잭션 부가기능의 동작 방식을 모두 제어할 수 있다.

트랜잭션 애노테이션

포인트컷 표현식과 트랜잭션 속성을 이용해 트랜잭션을 일괄적으로 적용하는 방식은 대부분의 상황에 잘 들어맞는다. 그런데 가끔은 클래스나 메소드에 따라 각각 다른 트랜잭션 속성을 적용해야 하는 경우도 있다. 그런 경우를 위해 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법이 있다. @Transactional 애노테이션의 정의는 다음과 같다.

package org.springframework.transaction.annotation;

@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을 적용할 때 4단계의 대체(fallback) 정책을 이용하게 해준다. 메소드의 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스)의 순서에 따라서 @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법이다.

다음과 같이 사용할 수 있다.

@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();
}

테스트를 위한 트랜잭션 애노테이션

테스트에도 @Transactional을 적용할 수 있다. 테스트 클래스 또는 메소드에 @Transactional 애노테이션을 부여해주면 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다. 하지만 중요한 차이점이 있는데, 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백된다는 것이다. 테스트에 적용된 @Transactional은 기본적으로 트랜잭션을 강제 롤백시키도록 설정되어 있다. @Transactional을 지정해주면 롤백 테스트가 되는 것이다.

그런데 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고 싶기는 하지만 강제 롤백을 원하지 않을 수도 있다. 이럴 때는 @Rollback이라는 애노테이션을 적용하면 된다. @Rollback은 롤백 여부를 지정하는 값을 갖고 있다. @Rollback의 기본 값은 true다. 따라서 트랜잭션은 적용되지만 롤백을 원치 않는다면 @Rollback(false)라고 해줘야 한다.

@Rollback은 메소드 레벨에서만 적용되기 때문에 모든 메소드에 해당 설정을 주려면 @TransactionConfiguration(defaultRollback=false)를 사용해야 한다. 이 애노테이션은 클래스 레벨에 부여할 수 있다.


참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday