토비의 스프링 6장 AOP를 읽고 배우고 학습한 것들을 정리합니다. 특별히 Tansaction과 관련도니 부분을 따로 정리합니다. 트랜잭션을 다루는 개념들을 읽다보니 섬세하게 데이터를 다루는 일에 대한 호기심이 생겨납니다. 처음있는 일입니다. 지금까지는 섬세하게 외부 API 서버를 다루는 일을 했고 대게 하드웨어와 연관된 일이 많았는데 이제 순수하게 데이터를 다루는 일도 점차 해보고 싶습니다.
아래 트랜잭션 경계설정 코드에서 DefaultTransactionDefinition를 사용해 트랜잭선 속성을 설정할 수수 있다. 트랜잭션 설정에는 아래의 4가지 속성이 있다.
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// DefaultTransactionDefinition : 디폴트 트랜잭션 속성 사용 (트랜잭션 전파, 격리수준, read-only, 타임아웃)
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에 설정된 격리수준은 ISOLATION_DEFAULT이다. 이는 DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻이다.
트랜잭션을 수행하는 제한시간을 설정할 수 있다. DefaultTransactionDefinition의 기본 설정은 제한시간이 없다는 것이다. 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED 또는 PROPAGATION_REQUIRED_NEW에서만 의미가 있다. 트랜잭션이 처음 시작되는 경우가아니라면 적용되지 않는다.
읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 읽기 전용 트랜잭션에서 데이터 조작 시도가 있으면 예외가 발생하게 된다. 트랜잭션이 처음 시작되는 경우가아니라면 적용되지 않는다.
메서드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다. 메서드 이름 패턴에 따라 다른 트랜잭션 정의가 적용되도록 만드는 것이다.
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// DefaultTransactionDefinition : 디폴트 트랜잭션 속성 사용 (트랜잭션 전파, 격리수준, read-only, 타임아웃)
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = invocation.proceed();
transactionManager.commit(status);
return ret;
} catch (RuntimeException e) { // 롤백을 수행할 예외를 설정
transactionManager.rollback(status);
throw e;
}
}
PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
<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_REQUIRED_NEW, ISOLATION_SERIALIZABLE</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
TransactionInterceptor 타입의 어드바이스 빈과 TransactionAttribute 타입의 속성 정보도 tx 스키마의 전용 태그를 이용해 정의할 수 있다.
<beans ...
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
...
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
>
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
</aop:config>
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true" />
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
트랜잭션 부가기능을 적용할 후보 메서드를 선정하는 작업은 포인트컷에 의해 진행된다. 그리고 어드바이스의 트랜잭션 전파 속성에 따라서 메서드별로 트랜잭션의 적용 방식이 결정된다. aop와 tx 스키마의 전용 태그를 사용한다면 애플리케이션의 어드바이저, 어드바이스, 포인트컷 기본 설정 방법은 바뀌지 않을 것이다. 이제 expression 애트리뷰트에 넣는 포인트컷 표현식과 <tx:attributes>로 정의하는 트랜잭션 속성만 결정하면된다. 포인트컷 표현식과 트랜잭션 속성을 정의할 때 따르면 좋은 몇 가지 전략을 생각해 보자.
트랜잭션 속성과 그에 따른 트랜잭션 전략을 UserService에 적용해보자. 지금까지 살펴봤던 몇 가지 원칙과 전략에 따라 작업을 진행할 것이다.
UserService에 추가된 메서드
public interface UserService {
// 신규 추가 메서드들
void upgradeLevels();
User get(String id);
List<User> getAll();
void deleteAll();
// 기존 인터페이스
void add(User user);
void update(User user);
}
추가 메서드 구현
@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); }
서비스 빈에 적용되는 포인트 컷 표현식
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
</aop:config>
트랜잭션 속성을 가진 트랜잭션 어드바이스 등록
<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 스키마의 태그를 이용한 트랜잭션 어드바이스 등록
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true" />
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
읽기 전용 메서드에 쓰기 작업을 추가한 테스트용 클래스
static class TestUserServiceImpl extends UserServiceImpl {
@Override
public List<User> getAll() {
for (User user : super.getAll()) {
update(user);
}
return List.of();
}
}
읽기전용 속성 테스트
@Test(expected = UncategorizedSQLException.class)
public void readOnlyTransactionAttribute() {
users.forEach(user -> userDao.add(user));
testUserService.getAll();
}
설정파일에서 패턴으로 분류 가능한 그룹을 만들어서 일괄적으로 속성을 부여하는 대신에 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법이다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
<tx:annotation-driven/>
@Transactional
public interface UserService {
void upgradeLevels();
void add(User user);
@Transactional(readOnly = true)
User get(String id);
@Transactional(readOnly = true)
List<User> getAll();
void deleteAll();
void update(User user);
}
AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있는 방법을 선언적 트랜잭션(declarative transaction)이라고 한다. 반대로 코드 안에서 트랜잭션 API를 사용해 직접 트랜잭션 기능을 부여하는 방법을 프로그램에 의한 트랙잭션(programmatic transaction)이라고 한다.
@Test(expected = UncategorizedSQLException.class)
public void transactionSync() {
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
transactionDefinition.setReadOnly(true);
TransactionStatus txStatus = transactionManager.getTransaction(transactionDefinition);
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
transactionManager.commit(txStatus);
}
@Test
public void transactionRollback() {
userDao.deleteAll();
Assertions.assertEquals(0, userDao.getCount());
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(transactionDefinition);
userService.add(users.get(0));
userService.add(users.get(1));
Assertions.assertEquals(2, userDao.getCount());
transactionManager.rollback(txStatus);
Assertions.assertEquals(0, userDao.getCount());
}
@Transactional 어노테이션을 테스트 클래스에도 적용할 수 있다.
@Transactional
@Test
public void transactionSync() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
@Test
@Transactional
@Rollback(false)
public void transactionRollback() {
userDao.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}