토비의 스프링을 보고 정리한 자료입니다.
문제가 있을 시, lshn1007@hanyang.ac.kr 로 메일주시면 삭제하겠습니다.
트랜잭션 경계 설정과 같은 로직과 비즈니스 로직과 같이 성격다른 로직을 분리하면 얻을 수 있는 장점은 아래와 같다.
Test double은 Mocking 또는 Test stub 으로 구성되어 있다. (이전 포스팅 참조)
단위 테스트와 통합 테스트 중 어떤 방법을 쓸지 결정할 때는 아래의 가이드라인을 검토해볼 수 있다.
단위 테스트를 만들기 위해서는 스텁이나 목 오브젝트의 사용이 필수적이다. 의존관계가 없는 단순한 클래스나 세부 로직을 검증하기 위해 메소드 단위로 테스트할 때가 아니라면, 대부분 의존 오브젝트를 필요로 하는 코드를 테스트하게 되기 때문이다.
이 때, 목 오브젝트를 만드는 일은 큰 짐이 되는데 번거로운 목 오브젝트를 편리하게 작성핟조록 도와주는 목 오브젝트 지원 프레임워크가 있다. (Mockito 프레임워크)
Mockito 와 같은 목 프레임워크의 특징은 목 클래스를 일일이 준비해둘 필요가 없다. 간단한 메소드 호출만으로 다이나믹하게 특정 인터페이스를 구현한 테스트용 목 오브젝트를 만들 수 있다.
인터페이스를 구현할 때 사용하지 않을 메소드라면 UnsupportedOperationException을 던지도록 만드는 편이 좋다. 그냥 빈 채로 두거나 null을 리턴하게 해도 문제는 없으나, 실수로 사용될 위험이 있으므로 UnsupportedOperationException을 던지게 해서 지원하지 않는 기능이라는 예외가 발생하도록 만들어라.
클라이언트가 Target(비즈니스로직) 을 실행하기 전에, 우리가 원하는 특정 데코레이터나 프록시를 실행하기 위해 Proxy 오브젝트를 활용할 수 있다.
프록시(Proxy)
라고 부른다.타깃(target)
또는 실체(real subject)
라고 부른다.프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 점과 프록시가 타깃을 제어할 수 있는 위치에 있다는 점이다.
프록시의 사용 목적은 아래와 같이 분류할 수 있다.
(1) 클라이언트가 타깃에 접근하는 방법을 제어하기 위함
(2) 타깃에 부가적인 기능을 부여해주기 위함
프록시의 기능은 다음 두 가지 기능으로 구성된다.
(1) 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
(2) 지정된 요청에 대해서 부가 기능을 수행한다.
(1) 클라이언트에서 타깃 오브젝트에 대한 레퍼런스가 미리 필요한 경우 타깃이 아닌 프록시를 먼저 넘겨줘서 클라이언트에서 선개발이 진행할 수 있도록 하고자 할 때
(2) 원격 오브젝트를 이용하는 경우, 원격 오브젝트에 대한 프록시를 만들어두고 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것처럼 하고자 할 때
(3) 특별한 상황에서 타깃에 대한 접근 권한을 제어하고자 할 때
두 패턴은 구조적으로 유사하나, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다. (생성을 지연하는 프록시의 경우 구체적인 생성 방법을 알아야 하기 때문에 타깃 클래스에 대한 직접적인 정보를 알아야 한다.) 물론 프록시 패턴이라고 하더라도 인터페이스를 통해 위임하도록 만들 수 있다.
프록시는 기존 코드에 영향을 주지 않으면서 타깃의 기능을 확장하거나 접근 방법을 제어할 수 있는 유용한 방법이다.
하지만 프록시는 일반적으로 만들기가 번거롭다는 단점이 있다.
(1) 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거롭다.
(2)부가 기능을 수행하는 코드가 중복될 가능성이 많다.
이런 문제를 해결하기 위해 JDK에서는 DynamicProxy라는 기능을 제공한다.
다이내믹 프록시는 리플렉션 기능을 이용해 프록시를 만들어주는데 Reflection 은 자바의 코드 자체를 추상화해서 접근하도록 만든 것으로 일일이 프록시 클래스를 정의하지 않고도 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성할 수 있다.
(1) 런타임 시 클라이언트가 프록시 생성을 요청
(2) 프록시 팩토리에 의해 프록시 객체를 생성
(3) 클라이언트가 프록시를 통해 메소드를 호출
(4) 다이나믹 프록시 오브젝트는 클라이언트의 요청을 리플렉션으로 변환해서 InvocationHandler
구현체의 invoke()
메소드로 전달
(5) InvocationHandler
구현체는 로직을 수행하고, 타깃에 위임
(6) 결과를 리턴
결론적으로 InvocationHandler가 하나의 프록시 또는 데코레이션 프록시 라고 보면 된다.
굳이 이렇게 하는 이유는 언제나 그랬듯이 중복을 최소화 하기 위함임.
InvocationTargetException
으로 한 번 포장되서 전달된다. 따라서 일단 InvocationTargetException
으로 받은 후 getTargetException()
메소드로 중첩되어 있는 예외를 가져와야 한다.이전 테스트 포스팅에서 언급한 예외를 상위레벨로 던질때 치환할 수 있다는 것을 기억하자
@Setter
public class TransactionHandler implements InvocationHandler {
private Object target; // 부가 기능을 제공할 타깃 오브젝트. 어떤 타입의 오브젝트에도 적용할 수 있다.
private PlatformTransactionManager transactionManager; // 트랜잭션 기능을 제공할 때 필요한 트랜잭션 매니저
private String pattern; // 트랜잭션을 적용할 메소드 이름 패턴
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = method.invoke(target, args);
this.transactionManager.commit(status);
return ret;
} catch (InvocationTargetException e) {
this.transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
참고로 위 예제에서 this.pattern 은 transaction과 관련한 메소드 네이밍으로 선언해야 의도한 대로 동작한다.
Dynamic proxy 객체는 Proxy 클래스의 newProxyInstance() static factory 메소드를 이용해 생성하면 된다.
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 사용되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더(=다이내믹 프록시가 정의되는 클래스 로더 지정)
new Class[] { Hello.class }, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가 기능과 위임 코드를 담은 InvocationHandler
package org.springframework.beans.factory;
...
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception; // 빈 오브젝트를 생성해서 돌려준다.
@Nullable
Class<?> getObjectType(); // 생성되는 오브젝트의 타입을 알려준다.
default boolean isSingleton() { // getObject()가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지 알려준다.
return true;
}
}
ProxyFactoryBean
을 제공해준다. 생성된 프록시는 스프링의 빈으로 등록되어야 한다.FactoryBean
의 구현체와는 다르게 ProxyFactoryBean
은 순수하게 프록시를 생성하는 작업만을 담당하고, 프록시를 통해 제공해줄 부가 기능은 별도의 빈에 둘 수 있다.ProxyFactoryBean
이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor
인터페이스를 구현해서 만든다. MethodInterceptor
는 InvocationHandler
와 비슷하지만 한 가지 다른 점이 있다. InvocationHandler
의 invoke()
메소드는 타깃 오브젝트에 대한 정보를 제공하지 않는다. 따라서 타깃은 InvocationHandler
를 구현한 클래스가 직접 알고 있어야 한다. 반면에 MethodInterceptor
의 invoke()
메소드는 ProxyFactoryBean
으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다. 그 차이 덕분에 MethodInterceptor
는 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. 따라서 MethodInterceptor
오브젝트는 타깃이 다른 여러 프록시에서 함께 사용할 수 있고, 싱글톤 빈으로 등록할 수 있다.MethodInterceptor
로는 메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation
오브젝트가 전달된다. MethodInvocation
은 타깃 오브젝트의 메소드를 실행할 수 있는 기능(proceed()) 이 있기 때문에 MethodInterceptor
는 부가 기능을 제공하는데만 집중할 수 있다.어드바이스
라고 부른다.포인트컷
이라고 부른다.어드바이저
= 포인트컷(메소드 선정 알고리즘)
+ 어드바이스(부가기능)
ProxyFactoryBean
에 어드바이스 와 포인트컷 을 함께 등록할 때는 Advisor
타입으로 묶어서 addAdvisor() 메소드를 호출해야 한다.ProxyFactoryBean
에는 여러 개의 어드바이스와 포인트컷이 추가될 수 있는데 포인트컷과 어드바이스를 따로 등록하면 어떤 포인트컷(메소드 선정)에 대해 어떤 어드바이스(부가기능)를 적용할지 알 수 없기 때문이다.DefaultAdvisorAutoProxyCreator
: 어드바이저를 이용한 자동 프록시 생성기(모든 빈에 대해 프록시 자동 적용 대상을 선별하고, 적용 대상의 경우 어드바이스를 연결해준다.)DefaultAdvisorAutoProxyCreator
가 빈 후처리기로 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다.DefaultAdvisorAutoProxyCreator
는 빈으로 등록된 모든 어드바이저 내의 포인트 컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다.getClassFilter()
→ getMethodMatcher()
순으로 Proxy, Advisor 적용 대상 클래스를 판별한다.package org.springframework.aop;
public interface Pointcut {
ClassFilter getClassFilter(); // 프록시를 적용할 대상 클래스인지 확인
MethodMatcher getMethodMatcher(); // 어드바이스를 적용할 메소드인지 확인
}
NameMatchMethodPointcut
은 getClassFilter()
에서 모든 클래스를 통과시키기 때문에 아래와 같이 재정의해서 사용할 수 있다.NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz.getSimpleName().startsWith("HelloT"); // 'HelloT'로 시작하는 클래스에만 프록시를 적용한다.
}
};
}
};
AspectJExpressionPointcut
은 정규표현식과 유사한 형태의 AspectJ Pointcut expression을 이용해 한번에 지정할 수 있게 해준다.execution()
이다.// AspectJ 포인트컷 표현식 형식
execution([접근제한자 패턴] 반환형_타입패턴 [패키지를 포함한 클래스의 타입패턴.]메소드이름패턴 (파라미터 타입패턴 | "..", ...) [throws 예외패턴]
(1) 접근제한자 패턴(생략가능)
public
(2) 반환형 타입 패턴(필수)
int
(3) 패키지를 포함한 클래스의 타입 패턴(생략가능)
springbook.learningtest.spring.pointcut.Target
(4) 메소드 이름 패턴(필수)
minus()
(5) 메소드 파라미터 타입 패턴(필수)
(int, int)
(6) 예외 이름에 대한 타입 패턴(생략가능)
throws java.lang.RuntimeException
@Test
void methodSignaturePointcut() throws NoSuchMethodException {
// method full signature
System.out.println(Target.class.getMethod("minus", int.class, int.class));
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public int com.corgi.example.learningtest" +
".target.Target.minus(int,int) throws java.lang.RuntimeException)");
/**
* Target.minus()
* ClassFilter와 MethodMatcher를 각각 가져와 비교한다.
*/
assertEquals(true, pointcut.getClassFilter().matches(Target.class)
&& pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null));
/**
* Target.plus()
*/
assertEquals(false, pointcut.getClassFilter().matches(Target.class)
&& pointcut.getMethodMatcher().matches(Target.class.getMethod("plus", int.class, int.class), null));
}
execution(* *..TargetInterface.*(..))
execution()
외에도 스프링에서 사용될 떄 빈의 이름으로 비교하는 bean() 이 있다.bean(*Service)
라고 쓰면 아이디가 Service로 끝나는 모든 빈을 선택한다.@Transactional
어노테이션이 적용된 메소드를 선정하게 해준다.@annotation(org.springframework.transacion.annotation.Transactional)
Aspect
란 그 자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소로, 핵심 기능에 부가 기능을 부여해주는 특별한 모듈을 말한다.Aspect
는 부가 기능을 정의한 코드인 Advice 와 Advice 를 어디에 적용할지를 결정하는 Pointcut 을 함께 갖고 있다. 지금까지 사용한 Advisor 는 아주 단순한 형태의 Aspect 라고 볼 수 있다.스프링 AOP는 독립적으로 개발한 부가 기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이나믹하게 적용해주기 위해 프록시를 이용하므로 스프링 AOP는 프록시 방식의 AOP라고 할 수 있다.
프록시 방식의 AOP 말고도 다른 방식의 AOP가 존재하는데, 대표적으로 AspectJ는 프록시를 사용하지 않는 AOP 기술이다. AspectJ는 스프링처럼 다이나믹 프록시 방식을 사용하지 않는다.
AspectJ는 프록시처럼 간접적인 방식이 아니라 타깃 오브젝트를 뜯어고쳐서 부가 기능을 직접 넣어주는 방법을 사용한다. 여기서 부가 기능을 넣는다고 타깃 오브젝트의 소스코드를 수정할 수는 없으니, 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용한다.
즉, 예제에서 UserService 클래스에 비즈니스 로직과 함께 있었을 때처럼 만들어버리는 것이다.
프록시 방법이 아닌 컴파일된 클래스 파일의 수정이나 바이트 코드 조작과 같은 방법을 사용하는 이유는 두 가지를 생각해볼 수 있다.
(1) 바이트코드를 조작하면 DI 컨테이너의 도움을 받아 자동 프록시 생성 방식을 사용하지 않아도 AOP 적용 가능(즉, 스프링과 같은 컨테이너가 사용되지 않는 환경에서도 AOP 적용 가능)
(2) 프록시 방식보다 훨씬 강력하고 유연한 AOP 가능(프록시를 이용하면 AOP 적용 대상은 클라이언트가 호출하는 메소드로 제한된다. 하지만, 프록시를 이용하지 않고 직접적인 조작을 통해 AOP를 적용하면 private 메소드의 호출, 스태틱 메소드 호출이나 초기화, 심지어 필드 입출력 등에도 부가 기능 적용이 가능하다.)
물론 대부분의 부가기능은 프록시 방식을 사용해 메소드의 호출 시점에 부여하는 것만으로도 충분하다. 게다가 AspectJ 같은 고급 AOP 기술은 바이트코드 조작을 위해 JVM의 실행 옵션을 변경하거나, 별도의 바이트코드 컴파일러를 사용하거나, 특별한 클래스 로더를 사용하게 하는 등의 번거로운 작업이 필요하다. 따라서 일반적인 AOP 적용에는 프록시를 사용하는 스프링 AOP로도 충분하다.
타깃
- 부가기능을 부여할 대상이다. 핵심 기능을 담은 클래스일 수도 있지만 경우에 따라서 다른 부가 기능을 제공하는 프록시 오브젝트일 수도 있다.
어드바이스
- 타깃에게 제공할 부가기능을 담은 모듈이다. 어드바이스는 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의할 수도 있다.
- 어드바이스는 여러 종류가 있는데 메소드 호출 과정에 전반적으로 참여하는 것도 있지만, 예외가 발생했을 때만 동작하는 어드바이스처럼 메소드 호출 과정의 일부에서만 동작하는 어드바이스도 있다
조인 포인트
- 어드바이스가 적용될 수 있는 위치를 말한다. 스프링의 프록시 AOP에서 조인 포인트는 메소드의 실행 단계뿐이다. 타깃 오브젝트가 구현한 인터페이스의 모든 메소드는 조인 포인트가 된다.
포인트컷
- 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다.
- 스프링 AOP의 조인 포인트는 메소드의 실행이므로 스프링의 포인트컷은 클래스와 메소드를 선정하는 기능을 갖고 있다.
프록시
- 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트다.
- DI를 통해 타깃 대신 클라이언트에 주입되며, 클라이언트의 메소드 호출을 대신 받아 타깃에 위임해주면서 그 과정에서 부가 기능을 부여해준다.
어드바이저
- 포인트컷(어드바이스 적용 대상을 선정하는 알고리즘)과 어드바이스(부가 기능)를 하나씩 갖고 있는 오브젝트다.
- 스프링은 자동 프록시 생성기가 어드바이저를 AOP 작업의 정보로 활용한다.
- 스프링 AOP에서만 사용되는 특별한 용어로, 일반적인 AOP에서는 사용되지 않는다.
Aspect
- 어드바이저와 마찬가지로 핵심 기능에 부가 기능을 부여해주는 오브젝트, 독립적인 모듈을 말한다. 일반적인 AOP에서 사용하는 용어로, 어드바이저를 포함하는 포괄적인 개념을 의미한다. 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 오브젝트로 존재한다.
- 스프링의 어드바이저는 아주 단순한 애스팩트라고 볼 수 있다.
스프링의 프록시 방식 AOP를 적용하려면 최소 4가지 빈을 등록해야 한다.
자동 프록시 생성기
- 스프링의
DefaultAdvisorAutoProxyCreator
클래스를 빈으로 등록한다.- 다른 빈을 DI 하지도 않고, 자신도 DI 되지 않으며 독립적으로 존재한다.
- 애플리케이션 컨텍스트가 빈 오브젝트를 생성하는 과정에서 빈 후처리기로 참여한다.
- 빈으로 등록된 어드바이저를 이용해 프록시를 자동으로 생성하는 기능을 담당한다.
어드바이스
- 부가 기능을 구현한 클래스
포인트컷
AspectJExpressionPointcut
을 빈으로 등록하고, expression 프로퍼티에 포인트컷 표현식을 넣어준다.
어드바이저
- 스프링의
DefaultPointcutAdvisor
클래스를 빈으로 등록해서 사용한다.- 어드바이스와 포인트컷을 참조하는 것 외에는 기능이 없다.
- 자동 프록시 생성기에 의해 자동 검색되어 사용된다.
DefaultTransactionDefinition
이 구현하고 있는 TransactionDefinition
인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다. 네 가지 속성을 이용하면 트랜잭션의 동작 방식을 제어할 수 있다. 트랜잭션 정의를 수정하고 싶다면 TransactionDefinition
타입의 빈을 원하는 속성을 지정해 정의해두고, 트랜잭션을 적용할 빈에 DI 받아 사용하면 된다.
PROPAGATION_REQUIRED
- 가장 많이 사용되는 속성으로 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있다면 이에 참여한다.
- DefaultTransactionDefinition 의 트랜잭션 전파 속성은 이 속성으로 지정되어 있다.
PROPAGATION_REQUIRES_NEW
- 항상 새로운 트랜젝션 새로 시작
PROPAGATION_NOT_SUPPORTED
- 트랜잭션 없이 동작하도록 만든다. 진행 중인 트랜잭션이 있어도 무시한다.
- 트랜잭션의 경계 설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용하는데 그 중에서 특별한 메소드만 트랜잭션 적용에서 제외하기 위해 사용한다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
/**
* @Transactional 대체 정책의 예
* [1] ~ [4] 순으로 @Transactional 애노테이션을 확인한다.
*/
[4]
public interface Service {
[3]
void method1();
}
[2]
public class ServiceImpl implements Service {
[1]
public void method1() { }
}
/**
* 예제 - @Transactional 애노테이션을 이용한 속성 부여
*/
@Transactional
public interface UserService {
// @Transactional이 없으므로 대체 정책에 따라 타입 레벨에 부여된 디폴트 속성이 적용된다.
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();
}
@Configuration
public class DatabaseConfig {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
...
@Autowired
private PlatformTransactionManager transactionManager;
...
/**
* 트랜잭션 동기화 테스트(앞서 시작한 트랜잭션에 해당 메소드가 참여하여 돌아가는지 확인)
*/
@Test
void transactionSync() {
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
txDefinition.setReadOnly(true);
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
userDao.deleteAll();
}
/**
* 트랜잭션 롤백 테스트
*/
@Test
void transactionRollback() {
userDao.deleteAll();
assertEquals(0, userDao.getCount());
// 트랜잭션 정의는 기본 값을 사용한다.
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
// 트랜잭션 매니저에게 트랜잭션을 요청한다. 기존에 시작된 트랜잭션이 없으니 새로운 트랜잭션을 시작시키고 트랜잭션 정보를 돌려준다.
// 동시에 만들어진 트랜잭션을 다른 곳에서도 사용할 수 있도록 동기화한다.
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
userService.add(users.get(0));
userService.add(users.get(1));
assertEquals(2, userDao.getCount());
// 강제로 롤백한다 트랜잭션 시작 전 상태로 돌아가야 한다.
transactionManager.rollback(txStatus);
// add()의 작업이 취소되고 트랜잭션 시작 이전의 상태임을 확인할 수 있다.
assertEquals(0, userDao.getCount());
}
@Test
void transactionSync() {
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
// 테스트 코드 상의 모든 작업을 하나의 트랜잭션으로 통합한다.
userDao.deleteAll();
userDao.add(users.get(0));
userDao.add(users.get(1));
} finally {
// 테스트 결과가 어떻든 상관없이 테스트가 끝나면 무조건 롤백한다.
// 테스트 중에 발생했던 DB의 변경 사항은 모두 이전 상태로 복구한다.
transactionManager.rollback(txStatus);
}
}
@Transactional
- @Transactional 애노테이션을 테스트 클래스와 메소드에도 적용할 수 있다. (앞서 사용했던 트랜잭션 매니저를 사용한 코드들을 애노테이션으로 대체할 수 있음)
- 테스트 클래스, 메소드 레벨에 @Transactional 애노테이션을 사용하면 테스트 실행 전에 새로운 트랜잭션 하나를 만들어주고, 예외가 발생하지 않고 테스트가 종료되면, 트랜잭션을 커밋해준다.
@Rollback
- 테스트에 적용된 @Transactional 은 기본적으로 트랜잭션을 강제 롤백시키도록 설정되어 있다.
- 테스트 코드 종료 이후에 강제 롤백을 원하지 않는다면 @Rollback 을 사용하면 된다. 이 애노테이션은 롤백 여부를 지정하는 값을 가지고 있는데, default가 true이므로 강제 롤백을 원하지 않는다면 @Rollback(false) 와 같이 사용하면 된다.
- 테스트 메소드 레벨에만 적용 가능하다.
@TransactionCOnfiguration
- @Rollback 은 테스트 메소드 레벨에만 적용 가능하므로 테스트 클래스 전체에 걸쳐 모든 트랜잭션이 롤백되지 않고 커밋되게 하려면 클래스 레벨에 적용할 수 있는 @TransactionConfiguration 을 사용하면 된다.
@NotTransactional
&& Propagation.NEVER
- @NotTransactional 을 테스트 메소드 레벨에 부여하면 클래스 레벨의 @Transactional 설정을 무시하고, 트랜잭션을 시작하지 않은 채로 테스트를 진행한다. (Spring 3.0부터 deprecated 설정되었으므로 사용하지 말 것)
- 이 대신에 @Transactional 의 트랜잭션 전파 속성 중 Propagation.NEVER 를 사용하면 @NotTransactional 과 마찬가지로 트랜잭션이 시작되지 않는다.