포스팅을 시작하겠습니다.
아래는 우리가 작성한 TransactionAdvice의 invoke 메서드 코드다.
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
Object ret = invocation.proceed();
transactionManager.commit(status);
return ret;
}
catch(RuntimeException e){
transactionManager.rollback(status);
throw e;
}
}
우리가 지난 시간에 DefaultTransactionDefinition은 살펴보지 않고 넘어갔다.
이번에는 트랜잭션을 가져올 때 파라미터로 트랜잭션 매니저에게 전달하는 DefaultTransactionDefinition의 용도를 알아보자.
6.6 트랜잭션 속성
트랜잭션이라고 모두 같은 방식으로 동작하지는 않는다.
트랜잭션의 동작방식을 제어할 수 있는 몇 가지 조건이 있다.
DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는
트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다.
트랜잭션 전파
트랜잭션 전파(transaction propagation)란 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다.
대표적으로 다음과 같은 트랜잭션 전파 속성을 줄 수 있다.
- PROPAGATION_REQUIRED: 가장 많이 사용되는 전파 속성이다. 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. 진행 중인 트랜잭션에 참여하는 경우는 트랜잭션 경계의 끝에서 트랜잭션을 커밋시키지도 않는다.
최초로 트랜잭션을 시작한 경계까지 정상적으로 진행돼야 비로소 커밋될 수 있다. - PROPAGATION_REQUIRES_NEW: 항상 새로운 트랜잭션을 시작한다. 즉 앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작하게 한다.
- PROPAGATION_NOT_SUPPORTED: 이 속성을 사용하면 트랜잭션 없이 동작하도록 만들 수 있다. 진행 중인 트랜잭션이 있어도 무시한다. 보통 모든 메서드에 AOP를 동시에 걸고 AOP 적용 대상이 되지 않게 만들기 위해 사용한다.
격리 수준
모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다.
적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 하는 제어가 필요하다.
격리수준은 기본적으로 DB에 설정되어 있지만 JDBC 드라이버나 DataSource 등에서 재설정할 수 있고, 필요하다면 트랜잭션 단위로 격리 수준을 조정할 수 있다.
기본적으로는 DB나 DataSource에 설정된 디폴트 격리수준을 따르는 편이 좋지만, 특별한 작업을 수행하는 메서드의 경우에는 독자적인 격리수준을 지정할 필요가 있다.
제한 시간
트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다.
제한 시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED나 PROPAGATION_REQUIRES_NEW와 함께 사용해야만 의미가 있다.
읽기 전용
읽기 전용(read only)으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다.
또한 데이터 액세스 기술에 따라서 성능이 향상될 수도 있다.
트랜잭션 인터셉터와 트랜잭션 속성
스프링에서는 편리하게 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 만들어진 TransactionInterceptor가 이미 존재한다.
TransactionInterceptor는 우리가 만든 TransactionAdvice와 크게 다르지 않다.
다만 트랜잭션 정의를 메서드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공해줄 뿐이다.
TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있다.
Properties 타입인 두 번째 프로퍼티 이름은 TransactionAttributes로 트랜잭션 속성을 정의한 프로퍼티다.
트랜잭션 속성은 TransactionDefinition의 네 가지 기본 항목에 rollbackOn()이라는 메서드를 하나 더 갖고 있는 TransactionAttribute 인터페이스로 구현된다. 이 TransactionAttribute를 이용하면 트랜잭션 부가기능의 동작 방식을 모두 제어할 수 있다.
스프링이 제공하는 TransactionInterceptor에는 기본적으로 두 가지 종류의 예외 처리 방식이 있다.
런타임 예외가 발생하면 트랜잭션은 롤백된다.
반면에 타깃 메서드가 런타임 예외가 아닌 체크 예외를 던지는 경우에는 이것을 예외 상황으로 해석하지 않고 일종의 비즈니스 로직에 따른, 의미가 있는 리턴 방식의 한 가지로 인식해서 트랜잭션을 커밋해버린다.
TransactionAttribute의 rollbackon()이라는 속성으로 기본 원칙과 다른 예외처리가 가능하게 해준다.
이를 활용해 특정 체크 예외의 경우는 트랜잭션을 롤백시키고, 특정 런타임 예외에 대해서는 트랜잭션을 커밋시킬 수도 있다.
포인트컷과 트랜잭션 속성의 적용 전략
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
- 공통된 메서드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스 속성을 정의한다.
- 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메서드를 호출할 때는 적용되지 않는다. 주의하자.
6.7 애노테이션 트랜잭션 속성과 포인트컷
세밀한 트랜잭션 속성의 제어가 필요한 경우를 위해 스프링이 제공하는 다른 트랜잭션 적용 방식이 있다.
설정 파일에서 패턴으로 분류 가능한 그룹을 만들어서 일괄적으로 속성을 부여하는 대신에 직접 타깃에 트랜잭션 속성 정보를 가진 애노테이션을 지정하는 방법이다.
트랜잭션 애노테이션
@Transactional은 애노테이션의 타깃은 메서드와 타입이다. 따라서 메서드, 클래스, 인터페이스에 적용할 수 있다.
@Transactional 애노테이션을 트랜잭션 속성정보로 사용하도록 지정하면 스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다. 이때 사용되는 포인트컷은 TransactionAttributeSourcePointcut이다.
TransactionInterceptor는 메서드 이름 패턴을 통해 부여되는 일괄적인 트랜잭션 속성 정보 대신
@Transactional 애노테이션의 엘리먼트에서 트랜잭션 속성을 가져오는 AnnotationTransactionAttributeSource를 사용한다.
@Transactional은 메서드 마다 다르게 설정할 수 있으므로 매우 유연한 트랜잭션 속성 설정이 가능해진다.
6.8 트랜잭션 지원 테스트
AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션이라고 한다.
반대로 TransactionTemplate나 개별 데이터 기술의 API를 사용해 직접 코드 안에서 사용하는 방법은 프로그램에 의한 트랜잭션이라고 한다. 특별한 경우가 아니라면 선언적 방식의 트랜잭션을 사용하는 것이 바람직하다.
트랜잭션 동기화와 테스트
트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.,
PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 통해 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다.
또한 트랜잭션 동기화 기술이 있었기에 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유할 수 있었다.
트랜잭션 동기화 기술은 트랜잭션 전파를 위해서도 중요한 역할을 한다.
진행 중인 트랜잭션이 있는지 확인하고,
트랜잭션 전파 속성에 따라서 이에 참여할 수 있도록 만들어주는 것도 트랜잭션 동기화 기술 덕분이다.
트랜잭션 매니저를 이용해 트랜잭션을 미리 시작하게 만드는 테스트를 실습해보았다.
테스트 코드에서 트랜잭션을 모두 동기화할 수 있었다. 이건 트랜잭션 전파 속성이 PROPAGATION_REQUIRED 기 때문이다.
롤백 테스트
롤백 테스트는 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트를 말한다.
롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다.
사실 테스트에서 트랜잭션을 제어할 수 있기 때문에 얻을 수 있는 가장 큰 이점은 바로 이 롤백 테스트다.
테스트를 위한 트랜잭션 애노테이션
@Transactional
- 테스트에도 @Transactional을 적용할 수 있다.
- 이를 이용하면 테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶어줄 수 있다.
- 테스트 메서드나 클래스에 사용하는 @Transactional은 기본적으로 디폴트 속성이 동일하지만 차이점이 임ㅆ다.
- 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백된다는 것이다.
@Rollback
만약 트랜잭션을 커밋시켜서 테스트에서 진행한 작업을 반영하고 싶다면 테스트 메서드에 @Rollback(fals)라는 애노테이션을 추가하면 된다.
@TransactionConfiguration
테스트 클래스의 모든 메서드에 트랜잭션을 적용하면서 모든 트랜잭션이 롤백되지 않고 커밋되게 하려면 어떻게 해야 할까?
무식하게 모든 메서드에 @Rollback(false)를 적용할 수도 있다.
하지만 그보다는 클래스 레벨에 부여할 수 있는 @TransactionConfiguration 애노테이션을 사용하면 편리하다.
@TransactionConfiguration(defaultRollback=false)로 설정하면 된다.
NotTransactionl과 Propagation.NEVER
@NotTransactionl을 테스트 메서드에 부여하면 클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행한다. 또한 NEVER 전파 속성으로 지정해주면 마찬가지로 트랜잭션이 시작되지 않는다.
효과적인 DB 테스트
DB가 사용되는 통합 테스트는 가능한 한 롤백 테스트로 만드는 게 좋다.
테스트가 기본적으로 롤백 테스트로 되어 있다면 테스트 사이에 서로 영향을 주지 않으므로 독립적이고 자동화된 테스트로 만들기가 매우 편하다. 테스트는 어떤 경우에도 의존하면 안 된다.
참고 자료
토비의 스프링 3.1 Vol. 1 (6장 AOP)
댓글