일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 다익스트라
- 우선순위큐
- 음수가 포함된 최단경로
- compiler
- 기술면접
- top-down
- disjoint set
- 엔테크서비스
- 플로이드 와샬
- Android Studio
- Controller
- bottom-up
- spring boot
- scikit-learn
- 동적계획법
- 코딩테스트
- onclick
- kmeans
- dto
- clean code
- 벨만 포드 알고리즘
- BufferedReader
- Python
- union-find
- 유니온 파인드
- Java
- 거쳐가는 정점
- Django
- 직무면접
- 최단경로
- Today
- Total
춤추는 개발자
[Spring] 트랜잭션으로 알아보는 스프링 AOP 본문
✅ 트랜잭션 정의
트랜잭션의 기본 개념은 더 이상 쪼갤 수 없는 최소 단위의 작업이다. 앞서 작성했던 TransactionAdvice의 코드를 살펴보면 트랜잭션을 가져올 때 파라미터로 DefaultTransactionDefinition이 있다.
DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다.
[ 트랜잭션 전파 ]
트랜잭션 전파란 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때, 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다.
만약, A 트랜잭션이 진행 중인 상태에서 B 트랜잭션이 호출된다면 어떻게 동작할까? B의 코드가 새로운 트랜잭션을 만들지 않고, A 트랜잭션에 포함되어 진행될 수 있다. 또는 A 트랜잭션과 무관하게 독립적인 트랜잭션으로 만들 수 있다.
이렇게 B와 같이 독자적인 트랜잭션 경계를 가진 코드에 이미 진행 중인 트랜잭션이 어떻게 영향을 미칠 수 있는가를 정의한 것이 트랜잭션 전파 속성이다.
대표적으로 다음과 같은 트랜잭션 전파 속성을 줄 수 있다.
- PROPAGATION_REQUIRED
진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. 이는 다양한 방식으로 결합해서 하나의 트랜잭션으로 구성하기 쉽다. A, B, A->B, B->A와 같은 4가지의 트랜잭션이 모두 가능하다. - PROPAGATION_REQUIRES_NEW
항상 새로운 트랜잭션을 시작한다. - PROPAGATION_NOT_SUPPORTED
이 속성은 트랜잭션 없이 동작하도록 만든다. 메서드 중 특별한 메서드만 트랜잭션 적용에서 제외할 때 사용한다. 보통 모든 메서드에 트랜잭션 AOP가 적용되게 하고, 특정 메서드의 트랜잭션 전파 속성만 PROPAGATION_NOT_SUPPORTED로 설정한다.
[ 격리 수준 ]
모든 DB 트랜잭션은 격리 수준을 갖고 있어야 한다. 기본적으로 DB에 설정되어 있지만, JDBC 드라이버나 DataSource 등에서 재설정할 수 있고, 트랜잭션 단위로 격리수준을 조정할 수 있다.
DefaultTransactionDefinition에 설정된 격리 수준은 ISOLATION_DEFAULT이다. 이는 DataSource에 설정된 디폴트 격리수준을 그대로 따른다는 것이다.
[ 제한 시간 ]
참고로 DefaultTransactionDefinition의 기본 설정은 제한 시간이 없다. 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED 또는 PROPAGATION_REQUIRES_NEW와 함께 사용한다.
[ 읽기전용 ]
트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 액세스 기술에 따라서 성능이 향상될 수도 있다. 트랜잭션 정의를 수정하려면 DefaultTransactionDefinition을 사용하는 대신 외부에서 정의된 TransactionDefinition 오브젝트를 DI받아서 사용하도록 만들면 된다.
✅ 트랜잭션 인터셉터와 트랜잭션 속성
메서드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다. 즉, 메서드 이름 패턴에 따라 다른 트랜잭션 정의가 적용되도록 만드는 것이다.
[ TransactionInterceptor ]
- 스프링에서는 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 만들어진 TransactionInterceptor가 존재한다. TransactionInterceptor 어드바이스는 메서드 이름 패턴을 이용해서 트랜잭션 정의를 다르게 지정할 수 있는 방법을 제공한다.
- TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있다. 그 중 Properties 타입인 프로퍼티 이름은 transactionAttributes로 트랜잭션 속성을 정의한 프로퍼티다.
- 트랜잭션 속성은 TransactionDefinition의 4가지 기본 항목에 rollbackOn()이라는 메서드를 더 갖고있는 TransactionAttribute 인터페이스로 정의된다. rollbackOn() 메서드는 어떤 예외가 발생하면 롤백을 할 것인가를 결정한다. 이를 이용하면 트랜잭션 부가기능의 동작 방식을 모두 제어할 수 있다.
- TransactionInterceptor의 기본적인 예외 처리를 따르지 않는 경우에 대응하기 위해 TransactionAttribute는 rollbackOn()이라는 속성을 둬서 기본 원칙과 다른 예외 처리가 가능하게 해준다.
- TransactionInterceptor는 이런 TransactionAttribute를 Properties라는 일종의 맵 타입 오브젝트로 전달받는다. 컬렉션을 사용하는 이유는 메서드 패턴에 따라서 각기 다른 트랜잭션 속성을 부여하기 위해서다.
[ 메서드 이름 패턴을 이용한 트랜잭션 속성 지정 ]
Properties 타입의 transactionAttributes 프로퍼티는 메서드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렌셕이다. 트랜잭션 속성을 다음과 같은 문자열로 정의할 수 있다.
PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
이 중에서 트랜잭션 전파 항목만 필수이며, 모든 항목이 구분가능하기 때문에 순서는 상관없다. '+'와 '-'로 시작하는 예외는 특별한 예외를 정의해주는 것이다. '+'의 경우 런타임 예외지만 커밋시킬 예외들을 넣는다. '-'는 체크 예외 중에서 롤백시킬 예외를 넣는다.
이러한 형식에 맞춰 메서드 이름 패턴과 문자열로 된 트랜잭션 속성을 이용해서 정의한 TransactionInterceptor 타입 빈의 예는 아래와 같다. 참고로 readOnly나 timeout은 트랜잭션이 처음 시작될 때만 적용된다. 이렇게 메서드 이름 패턴을 사용하는 트랜잭션 속성을 활용하면 하나의 트랜잭션 어드바이스를 정의하는 것만으로도 다양한 트랜잭션 설정이 가능해진다.
<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>
[ tx 네임스페이스를 이용한 설정 방법 ]
TransactionInterceptor 타입의 어드바이스 빈과 TransactionAttribute 타입의 속성 정보도 tx 스키마의 전용 태그를 이용해 정의할 수 있다.
<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>
[ 포인트컷과 트랜잭션 속성의 적용 전략 ]
aop와 tx 스키마의 전용 태그를 사용한다면 어플리케이션의 어드바이저, 어드바이스, 포인트컷 기본 설정 방법은 바뀌지 않을 것이다. expression 애트리뷰트에 넣는 포인트컷 표현식과 <tx:attributes>로 정의하는 트랜잭션 속성만 결정하면 된다.
1. 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
- 트랜잭션을 적용할 타깃 클래스의 메서드는 모두 트랜잭션 적용 후보가 되는 것이 바람직하다.
- add() 메서드도 다른 트랜잭션에 참여할 가능성이 높기 때문에 트랜잭션 적용 대상이어야 한다.
- 조회의 경우, 읽기전용으로 트랜잭션 속성을 설정해두면 그만큼 성능의 향상을 가져올 수 있다. 또는 복잡한 조회의 경우 제한시간을 지정하거나 격리 수준에 따라 조회도 반드시 트랜잭션 안에서 진행해야할 필요가 발생하기도 한다.
- 따라서 트랜잭션용 포인트컷 표현식에는 메서드나 파라미터, 예외에 대한 패턴을 정의하지 않는게 바람직하다.
- 가능하면 클래스보다 인터페이스 타입을 기준으로 타입 패턴을 적용하는 것이 좋다.
- 메서드의 execution() 방식의 포인트컷 표현식 대신, 스프링의 빈 이름을 이용하는 bean() 표현식을 사용하는 방법도 고려해볼 만하다.
2. 공통된 메서드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.
- 기준의 되는 몇 가지 트랜잭션 속성을 정의하고, 그에 따라 적절한 메서드 명명 규칙을 만들어 두면 하나의 어드바이스만으로 어플리케이션의 모든 서비스 빈에 트랜잭션 속성을 지정할 수 있다.
- 가끔 예외적인 경우는 트랜잭션 어드바이스와 포인트컷을 새롭게 추가할 필요가 있다.
- 트랜잭션 적용 대상 클래스의 메서드는 일정한 명명 규칙을 따르게 해야 한다.
<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="transactionAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:attributes>
<tx:advice id="batchTxAdvice">
<tx:attributes>...</tx:attributes>
</tx:attributes>
3. 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메서드를 호출할 때는 적용되지 않는다.
- 프록시 방식의 AOP에서 프록시를 통한 부가기능의 적용은 클라이언트로부터 호출이 일어날 때만 가능하다.
- 타깃 오브젝트가 자기 자신의 메서드를 호출할 때는 프록시를 통한 부가기능의 적용이 일어나지 않는다.
- 위 그림에서 보면 [1], [3]의 요청은 트랜잭션 프록시를 지나 타깃 메서드를 호출한다. 이는 트랜잭션 경계설정 부가기능이 부여될 것이다. 하지만 [2]의 요청은 프록시를 거치지 않기 때문에 트랜잭션 속성이 부여되지 않는다.
- 이렇게 같은 타깃 오브젝트 안에서 메서드 호출이 일어나는 경우에는 프록시 AOP를 통해 부여해준 부가기능이 적용되지 않는다는 점을 주의해야 한다.
- 하지만, AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP기술을 적용한다면, [2]의 경우에도 트랜잭션 속성이 부여될 수 있다.
[ 트랜잭션 속성 적용 ]
위에 설명한 트랜잭션 전략을 UserService에 적용해본다. 몇 가지 원칙과 전략에 따라 작업할 것이다.
1. 트랜잭션 경계설정의 일원화
- 일반적으로 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 바람직하다. 비즈니스 로직을 담고 있는 서비스 계층 오브젝트의 메서드가 트랜잭션 경계를 부여하기에 가장 적절한 대상이다.
- 서비스 계층을 트랜잭션이 시작되고 종료되는 경계로 정했다면, 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단한다.
- DAO가 제공하는 주요 기능을 서비스 계층에 위임 메서드로 만들어 둔다. 그러면 부가 로직을 적용하거나 트랜잭션 속성도 제어할 수 있다.
public interface UserService {
void add(User user);
User get(String id);
List<User> getAll();
void deleteAll();
void update(User user);
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
UserDao userDao;
...
@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 user) { userDao.update(user);}
}
- UserService의 메서드가 늘어났기 때문에 이를 상속하여 만들어진 UserServiceImple에도 메서드 구현 코드를 넣는다.
2. 서비스 빈에 적용되는 포인트컷 표현식 등록
- 기존에 upgradeLevels()에만 트랜잭션이 적용되게 했던 표현식을 모든 비즈니스 로직의 서비스 빈에 적용되도록 수정한다.
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
</aop:config>
3. 트랜잭션 속성을 가진 트랜잭션 어드바이스 등록
- 메서드 패턴과 트랜잭션 속성을 TransactionInterceptor를 이용하도록 변경한다.
- 가장 보편적인 방법인 get으로 시작하는 메서드는 읽기전용 속성을 두고 나미저는 디폴트 트랜잭션 속성을 따르도록 설정한다.
- 트랜잭션 어드바이스와 속성도 tx 네임스페이스와 스키마를 추가하여 정리한다.
<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>
<tx:advice id="transactionAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:attributes>
✅ 어노테이션 트랜잭셩 속성과 포인트 컷
지금까지 포인트컷 표현식과 트랜잭션 속성을 이용해 트랜잭션을 일괄적으로 적용하는 방식은 복잡한 트랜잭션 속성이 요구되지 않는 한 대부분의 상황에 잘 들어맞는다. 그러나 가끔 클래스나 메서드에 따라 제각각 속성이 다른, 세밀하게 튜닝된 트랜잭션 속성을 적용해야 하는 경우도 있다.
이런 상황에 대비해서 직접 타깃에 트랜잭션 속성정보를 가진 어노테이션을 지정하는 방법이 있다.
[ 트랜잭션 어노테이션 ]
1. @Transactional
아래 코드는 @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을 트랜잭션 속성정보로 사용하도록 지정하면 스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다.
- 이때 사용되는 포인트컷은 TransactionAttributeSourcePointcut이다. @Transactional이 부여된 빈 오브젝트를 모두 찾아서 포인트 컷의 선정 결과로 돌려준다.
2. 트랜잭션 속성을 이용하는 포인트컷
- @Transactional을 사용했을 때 어드바이저의 동작방식이다. @Transactional는 메서드마다 다르게 설정할 수 있으므로 매우 유연한 트랜잭션 속성 설정이 가능해진다.
- 동시에 포인트컷도 @Transactional을 통한 트랜잭션 속성정보를 참조하도록 만든다.
- 이러한 방식은 포인트컷과 트랜잭션 속성을 어노테이션 하나로 지정할 수 있다.
- 트랜잭션 부가기능 적용 단위는 메서드다. 따라서 메서드마다 @Transactional을 부여하고 속성을 지정할 수 있다. 그러면 유연한 속성제어가 가능하지만 중복된 코드가 발생할 수 있다.
3. 대체 정책
[1]
public interface Service {
[2]
void method1();
[3]
void method2();
}
[4]
public class Servicelmpl implements Service {
[5]
public void method1() (
[6]
public void method2() {
}
이러한 상황에 대처하고자 @Transactional을 적용할 때 4단계의 대체 정책을 이용하게 해준다.
- 메서드의 속성을 확인할 때, 타깃 메서드, 선언 메서드, 선언 타입의 순서에 따라 @Transactional이 적용됐는지 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법이다.
- 위와 같은 코드에서는 @Transactional이 부여될 수 있는 위치가 6개다. 타깃 클래스가 ServiceImpl이라면 @Transactional의 위치를 확인하는 순서는 [5], [6] -> [4] -> [2], [3] -> [1] 이 된다.
- @Transactional을 사용하면 대체 정책을 잘 활용해서 어노테이션 자체를 최소한으로 사용하며 개발할 수 있다.
- 기본적으로 @Transactional 적용 대상은 클라이언트가 사용하는 인터페이스가 정의한 메서드이므로 @Transactional도 타깃 클래스보다 인터페이스에 두는게 바람직하다.
- 하지만, 스프링 AOP가 아닌 방식으로 트랜잭션을 적용하면 인터페이스에 정의한 @Transactional은 무시되기 때문에 안전하게 타깃 클래스에 @Transactional을 두는 방법을 권장한다.
4. 트랜잭션 어노테이션 사용을 위한 설정
<tx: annotation-driven />
위 태그 하나로 트랜잭션 어노테이션을 이용하는데 필요한 어드바이저, 어드바이즈, 포인트컷, 어노테이션을 이용하는 트랜잭션 속성정보가 등록된다.
[ 트랜잭션 어노테이션 적용하기 ]
먼저, tx 스키마의 <tx:attributes> 태그를 이용해 설정했던 트랜잭션 속성을 그대로 어노테이션으로 바꾼다. 참고로 많이 사용되는 한가지를 타입 레벨에 공통 속성으로 지정해주고, 나머지 속성은 개별 메서드에 적용한다.
<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();
}
✅ 트랜잭션 지원 테스트
[ 선언적 트랜잭션과 트랜잭션 전파 속성 ]
- 선언적 트랜잭션: AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법
- 프로그램에 의한 트랜잭션: 반대로 TransactionTemplate나 개별 데이터 기술의 트랜잭션 API를 사용하여 직접 코드 안에서 사용하는 방법
[ 트랜잭션 동기화와 테스트 ]
- 트랜잭션의 자유로운 전파와 유연한 개발이 가능할 수 있었던 기술적인 배경에는 2가지가 있다.
- 첫번째는 프록시를 이용한 트랜잭션 부가기능을 간단하게 어플리케이션 전반에 적용할 수 있게 해주는 AOP, 두번째는 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶어서 추상 레벨에서 관리하게 해주는 트랜잭션 추상화다.
1. 트랜잭션 매니저와 트랜잭션 동기화
- 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.
- 트랜잭션 매니저는 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다. 또한 트랜잭션 동기화 기술이 있었기에 이미 시작된 트랜잭션 정보를 보관했다가 DAO에서 공유할 수 있었다.
- 트랜잭션 동기화 기술은 트랜잭션 전파를 위해서도 중요한 역할을 한다. 트랜잭션 전파 속성에 따라 다양한 트랜잭션 동기화 구조를 만들 수 있기 때문이다.
- 물론, 트랜잭션 매니저를 이용해 트랜잭션에 참여하거나 제어하는 방법이 있는데 바로 테스트다.
트랜잭션 매니저를 아래와 같이 빈으로 선언하여 간단한 테스트 코드를 작성해보자.
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/>
처음 트랜잭션을 가져오는 코드 2줄이 없었다면 트랜잭션은 총 3개가 만들어졌을 것이다. 하지만, 메서드를 호출하기 전에 미리 트랜잭션을 가져오면서 UserService의 메서드들이 하나의 트랜잭션에 참여하게 되었다.
@Test
public void transactionSync() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
transactionManager.commit(status);
}
[ 롤백 테스트 ]
롤백 테스트는 테스트 내의 모든 DB작업을 하나의 트랜잭션 안에서 동작하게 하고, 테스트가 끝나면 무조건 롤백해버리는 테스트를 말한다.
롤백 테스트는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 시작하기 전 상태로 만들어준다.
심지어 여러 개발자가 하나의 테스트용 DB를 사용할 수 있게 해준다. 적절한 격리수준만 보장한다면 동시에 여러 개의 테스트가 가능하다.
@Test
public void transactionSync() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
} finally {
transactionManager.rollback(status);
}
}
[ 테스트를 위한 트랜잭션 어노테이션 ]
스프링의 컨텍스트 테스트 프레임워크는 어노테이션을 이용하여 테스트를 편리하게 만들 수 있는 여러 가지 기능을 추가하게 해준다. @ContextConfiguration을 클래스에 부여하면 테스트를 실행하기 전에 스프링 컨테이너를 초기화하고, @Autowired가 붙은 필드를 통해 테스트에 필요한 빈에 자유롭게 접근할 수 있다.
1. @Transactional
- 테스트 클래스 또는 메서드에 @Transactional를 부여하면 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메서드에 트랜잭션 경계가 자동으로 설정된다.
- 아래와 같이 테스트 코드를 @Transactional을 이용하면 간단하게 만들 수 있다. 게다가, 테스트 메서드의 트랜잭션에 참여해서 하나의 트랜잭션으로 실행된다.
@Test
@Transactional
public void transactionSync() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
2. @Rollback
테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백된다. 즉, @Transactional은 기본적으로 트랜잭션을 강제 롤백시키도록 설정되어 있다.
📢 참고사항
JUnit 테스트는 보통 클래스 단위로 실행된다. 따라서 테스트 클래스 내의 모든 테스트가 함께 실행된다. 이클립스에서는 간단히 특정 테스트 메서드만 실행되게 할 수도 있다. JUnit 뷰에 나타난 테스트 목록에서 하나를 클릭하고 테스트를 실행하거나, 테스트 코드에서 특정 메서드의 이름을 클릭하고 테스트를 실행하면 선택한 테스트 메서드 하나만 실행된다.
- 만약 반대로 테스트에서 진행한 작업을 DB에 반영하고 싶을 때는 @Rollback을 사용한다. 해당 어노테이션은 롤백 여부를 지정하는 값을 갖고 있다.
- 기본 값 true지만, 롤백을 원하지 않는 경우에는 @Rollback(false)라고 입력하면 된다.
3. @TransactionConfiguration
클레스 레벨에 부여할 수 있는 @TransactionConfiguration는 모든 메서드에 트랜잭션을 적용하면서 모든 트랜잭션이 롤백되지 않고 커밋되게 한다. 디폴트 롤백 속성을 false로 하면 해당 클래스에 포함된 모든 메서드에 트랜잭션이 커밋된다.(defaultRollback = false)
4. NotTransactional과 Propagation.NEVER
- 만약 클래스 레벨에 @Transactional을 부여한 상황에서 트랜잭션이 필요없는 메서드의 경우, NotTransactional을 부여한다. 다만, 해당 방법은 스프링 3.0에서 제거 대상이 됐다.
- 대신 @Transactional의 전파 속성을 사용하는 방법이 있다. 다음과 같이 트랜잭션 전파 속성을 지정하면 트랜잭션이 시작되지 않는다.
@Transactional(propagatio=Propagation.NEVER)
🙋♂️ 느낀점
트랜잭션을 기반으로 AOP를 다루면서 확장성을 갖춘 부가기능 구현 방법을 배울 수 있었다. 게다가 일련의 작업들을 하나의 트랜잭션으로 구성하여 예외처리하는 방식이, 특정 절차에 따라 수행하는 일련의 비즈니스 업무처럼 느껴졌다.
트랜잭션의 원자성에 따라 작업을 관리하고 전파속성, 격리수준을 부여하며 작업을 일관되게 수행했다. 이러한 방식이 개발자의 실수나 의도치 않은 작업들을 막아주는 동시에 발생할 수 있는 에러들을 사전에 방지한다는 것을 알 수 있었다.
트랜잭션에 다양한 속성 값들을 직접 부여하다가 어노테이션으로 한번에 관리하며 간결한 코드를 만들었다. 이는 '어떻게하면 편하게 코딩할 수 있을까' 에 대한 결과처럼 보였다. 개발을 편리하게 만들어주는 도구들이 계속 나오듯이 개발 언어도 여러 문법들이 추가되며 발전되기 때문에 최신 기술이나 내가 사용하는 언어의 이슈, 업데이트 사항들을 파악해야 한다고 느꼈다.
이번 포스팅을 마지막으로 AOP에 대한 학습을 마친다. 향후, AspectJ의 사용법을 배우고 실제로 코딩하며 익혀봐야 겠다.
'Developer's_til > 스프링 프레임워크' 카테고리의 다른 글
[inflearn] 스프링 MVC 1편 review (0) | 2023.04.09 |
---|---|
[inflearn] 스프링 핵심 원리 - 기본편 review (0) | 2023.03.04 |
[Spring] 다양한 관점에서 개발하는 스프링의 AOP - 3 (0) | 2021.08.23 |
[Spring] 다양한 관점에서 개발하는 스프링의 AOP - 2 (0) | 2021.08.23 |
[Spring] 다양한 관점에서 개발하는 스프링의 AOP - 1 (0) | 2021.08.23 |