[Spring Boot] Checked Exception에 Rollback 적용하기
들어가며
프로젝트 진행 중 MySQLTransactionRollbackException, SQLException이 발생하는 경우가 있었다. 해당 프로젝트는 AOP를 이용하여 트랜잭션을 적용한 상태였다. 그런데 해당 예외가 발생했을 때 정상적으로 Rollback이 발생하지 않는 상황이 발생했다. 이러한 문제를 조사하고 해결하며 참고한 자료를 정리해보았다.
Rollback 이란?
Rollback이란 트랜잭션의 원자성이 깨질 때, 즉 하나의 트랜잭션 처리가 비정상적으로 종료되었을 때의 상태를 뜻한다.
Rollback이 이뤄진다면 트랜잭션을 다시 실행하거나 부분적으로 변경된 결과를 취소할 수 있다.
자세한 내용은 해당 글을 참고하면 된다.
링크 : https://jangjjolkit.tistory.com/4
Checked Exception
Checked Exception은 Compile Exception이라고도 하며, Exception을 바로 상속받는다. Compile Exception이라는 이름에서 알수 있듯이, 컴파일 시점에 예외를 catch하는지 정적으로 확인한다. 만일 컴파일 시점에 예외에 대한 처리(try/catch)를 하지 않는다면 컴파일 에러가 발생한다.
또한, 트랜잭션 Rollback이 안된다는 속성도 있다.
자세한 내용은 해당 글을 참고하면 된다.
참고 : https://jangjjolkit.tistory.com/2
Spring Boot Transaction 적용
트랜잭션을 적용하는 여러 방법이 있다.
1. @Transactional 어노테이션 사용
@Transactional
public void someMethod() {
// ...
}
2. AOP Transaction 적용
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;
@Aspect
@Configuration
@EnableAspectJAutoProxy
public class TransactionConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionConfig.class);
private static final int TX_METHOD_TIMEOUT = 3;
private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.test..service.impl.*ServiceImpl.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
// private DataSourceTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
TransactionInterceptor txAdvice = new TransactionInterceptor();
Properties txAttributes = new Properties();
List<RollbackRuleAttribute> rollbackRules = new ArrayList<RollbackRuleAttribute>();
/** If need to add additionall exceptio, add here **/
DefaultTransactionAttribute readOnlyAttribute = new DefaultTransactionAttribute(
TransactionDefinition.PROPAGATION_REQUIRED);
readOnlyAttribute.setReadOnly(true);
readOnlyAttribute.setTimeout(TX_METHOD_TIMEOUT);
RuleBasedTransactionAttribute writeAttribute = new RuleBasedTransactionAttribute(
TransactionDefinition.PROPAGATION_REQUIRED, rollbackRules);
writeAttribute.setTimeout(TX_METHOD_TIMEOUT);
String readOnlyTransactionAttributesDefinition = readOnlyAttribute.toString();
String writeTransactionAttributesDefinition = writeAttribute.toString();
// read-only
txAttributes.setProperty("get*", readOnlyTransactionAttributesDefinition);
// write rollback-rule
txAttributes.setProperty("*", writeTransactionAttributesDefinition);
txAdvice.setTransactionAttributes(txAttributes);
txAdvice.setTransactionManager(transactionManager);
return txAdvice;
}
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// pointcut.setExpression("(execution(* *..*.service..*.*(..)) ||
// execution(* *..*.services..*.*(..)))");
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
필자는 2번째 방법을 사용했다. 그리고 위에서 말한 Rollback이 발생하지 않는 문제가 발생했다.
Checked Exception에서 Rollback이 안되는 이유
앞서 말했듯 Checked Exception은 Rollback이 되지 않는 특징이 있다. 이는 스프링이 EJB 관습을 따르기 때문이다.
메소드 API를 설계할 때 해당 메소드 호출이 어떤 예외를 발생 시키는지 또한 API 규악의 중요한 내용이고, 때로는 그런 예외에 대한 처리를 호출자에게 강제할 수 있어야 한다는 뜻인 것 같다. 컴파일 시 체크할 수 있는 예외에 대해서는 알아서 하라는 말이다.
어노테이션을 사용하든 AOP에 설정을 하든 스프링은 Unchecked Exception(RuntimeException)과 Error 발생 시 롤백 적용을 기본 정책으로 한다.
- DefaultTransactionAttribute#rollbackOn(Throwable ex)
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
스프링에서 제공하는 트랜잭션 설정 클래스 DefaultTransactionAttribute 의 메소드 rollbackOn를 보면 RuntimeException과 Error인 경우에 true를 반환하는 것을 볼 수 있다.
- @Transaction#rollbackFor()
어노테이션 @Transaction 의 rollbackFor 메소드 설명을 보면
- 기본적으로 트랜잭션은 {@link RuntimeException} 및 {@link Error}에서 롤백
- 확인된 예외(비즈니스 예외)에서는 롤백되지 않음
- 자세한 사항은 DefaultTransactionAttribute#rollbackOn 참고
즉, 어노테이션을 사용하는 경우 아무것도 설정하지 않으면 아래와 같이 인삭한다.
@Transactional(rollbackFor = {RuntimeException.class, Error.class})
public void someMethod() {
// ...
}
Checked Exception에서 Rollback 발생시키기
1. @Transactional 어노테이션에 rollbackFor 옵션 이용하기
@Transactional(rollbackFor = Exception.class)
public void someMethod() {
// ...
}
모든 예외에 대해서 롤백을 처리한다.
2. RollbackRuleAttribute 를 이용해 롤백 규칙을 추가하기 (Spring Boot Transaction 적용 방법 2번과 비교)
List<RollbackRuleAttribute> rollbackRules = new ArrayList<RollbackRuleAttribute>();
rollbackRules.add(new RollbackRuleAttribute(Exception.class));
RuleBasedTransactionAttribute writeAttribute = new RuleBasedTransactionAttribute(
TransactionDefinition.PROPAGATION_REQUIRED, rollbackRules);
String writeTransactionAttributesDefinition = writeAttribute.toString();
txAttributes.setProperty("add*", writeTransactionAttributesDefinition);
txAdvice.setTransactionAttributes(txAttributes);
txAdvice.setTransactionManager(transactionManager);
rollbackRules.add(new RollbackRuleAttribute(Exception.class)); 를 추가하여 마찬가지로 모든 예외에 대해서 롤백을 처리한다.
코드를 수정하고 나니 정상적으로 모든 예외상황에서 rollback이 되었다.
정리하며
개발자가 컴파일 시 예측/처리 가능한 예외의 처리를 개발자에게 강제하기 위해 Checked Exception은 스프링에서 제공하는 트랜잭션에서의 롤백의 기본 정책에 포함되지 않는다. 때문에, 직접 규칙을 추가해야 한다.