2017년 4월 8일 토요일

javax.persistence.RollbackException: Transaction marked as rollbackOnly

Spring에서 위와 같은 RollbackException이 발생하는 경우에 대해서 알아보도록 하자.
먼저 아래와 같은 두 개의 서비스가 있다.

ExceptionService의 throwException() 메소드는 NullPointerException을 발생시키는 메소드이다.
UserService의 addUser 메소드는 ExceptionService.throwException()을 호출하고 user를 add하는 메소드이다.
다만, exception을 처리하기 위해 try-catch 문으로 해당 exception을 처리한 상태이다.

아래와 같은 상황에서 userService.addUser()를 호출하면 어떻게 될까?

- UserServiceImpl.java

@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ExceptionService exceptionService;

    @Override
    public User addUser(String name) {
        try {
            exceptionService.ThrowException();
        } catch (Exception e) {
            log.info("Exception occured");
        }
        return userRepository.save(new User(name));
    }
}

- ExceptionServiceImpl.java

@Transactional
@Service
public class ExceptionServiceImpl implements ExceptionService {

    @Override
    public void ThrowException() throws Exception {
        throw new NullPointerException();
    }
}


아래와 같이 장황한 RollbackException이 발생하게 된다.
왜 이런 상황이 발생되는 지 순서대로 알아보자.

- Output

javax.persistence.RollbackException: Transaction marked as rollbackOnly
 at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:58) ~[hibernate-entitymanager-5.0.9.Final.jar:5.0.9.Final]
 at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:517) ~[spring-orm-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:483) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:290) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
 at org.blog.test.user.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$dcb2eb86.addUser() ~[main/:na]
 at org.blog.test.user.controller.UserController.addUser(UserController.java:18) ~[main/:na]

우리는 두 서비스 모두 @transactional 이 선언되어 있다는 것에 주목할 필요가 있다.

실제 transaction의 흐름을 살펴보자~!!

1. userService.addUser() 가 호출되면서, transaction A가 시작됨
2. 해당 메소드 내의 exceptionService.throwException() 메소드가 호출됨
3. throwException 메소드 역시 @transactional이 선언되어 있기 때문에, 현재 transaction을 확인
4. @transactional의 기본값은 Propagation.REQUIRED 이기 때문에, 기존에 실행된 transaction A를 그대로 사용

- public static final Propagation REQUIRED
Support a current transaction, create a new one if none exists. Analogous to EJB transaction attribute of the same name.
This is the default setting of a transaction annotation.

5. NullPointerException이 발생하면서, 해당 transaction A를 rollbackOnly로 설정 후, userService.addUser()로 돌아감
6. userRepository.save(new User(name)); 가 실행됨.
7. userService.addUser()에서는 해당 NullPointerException이 try-catch 문으로 처리가 되었기 때문에 transaction을 commit 시도.
8. transaction이 rollbackOnly로 설정되었기 때문에 commit 실패하면서, RollbackException 발생

이에 대한 해결 방법은 여러가지가 있다.

1. ExceptionService 를 async로 실행
2. ExceptionService 의 @transactional 제거
3. ExceptionService 의 transaction 의 propagation 변경

그 중 3번에 대해 알아보면,
아래와 같이 transaction의 propagation을 REQUIRES_NEW로 바꾸면 새로운 transaction을 시작하기 때문에,
위와 같은 에러가 발생하지는 않는다.
하지만, 이 방법이 전체 method의 flow에 적합한지 검토 후, 적용하는 것이 좋을 것 같다.

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Service
public class ExceptionServiceImpl implements ExceptionService {

- Output

2017-04-08 20:13:42.193  INFO 13552 --- [nio-8080-exec-8] o.b.t.user.service.impl.UserServiceImpl  : Exception occured
2017-04-08 20:13:42.198  INFO 13552 --- [nio-8080-exec-8] jdbc.sqltiming                           : insert into user (name) values ('femeo') 
 {executed in 2 msec}

댓글 없음 :

댓글 쓰기