legacy/Spring

[Spring] @Transactional과 UnexpectedRollbackException

heemang.dev 2023. 9. 6. 13:34

 

트랜잭션 전파를 공부하면 내부 트랜잭션과 외부 트랜잭션을 공부하게 되는데, 이때 내부 트랜잭션에서 롤백이 호출되었을 때 외부 트랜잭션에서 이를 처리하는 2가지 경우가 존재한다.

 

외부 트랜잭션

트랜잭션이 시작된 곳을 외부 트랜잭션이라고 한다. 

외부 트랜잭션이 최종적으로 커밋 또는 롤백을 처리한다.

내부 트랜잭션

외부 트랜잭션에 참여하는 트랜잭션을 내부 트랜잭션이라고 한다.

내부 트랜잭션에서 발생한 커밋 또는 롤백은 실제로 호출되지 않는다. 커넥션은 단 한 번만 커밋 또는 롤백이 가능하기 때문에, 내부 트랜잭션에서 커밋 또는 롤백을 호출하면 외부 트랜잭션까지 트랜잭션이 사용되지 않고 중간에 종료되는 문제가 발생한다. 


예외 1 - RuntimeException

MemberService에서 MemberRepository와 LogRepository를 호출한다.

Service와 Repository 둘 다 @Transactional을 사용하기 때문에 AOP Proxy가 사용된다.

 

외부 트랜잭션에 참여하는 내부 트랜잭션이 존재할 때, 내부 트랜잭션 하나라도 롤백이 호출된다면 최종적으로 DB에 접근하는 외부 트랜잭션 또한 롤백을 호출하게 된다. 

아래의 경우 MemberService에서 트랜잭션이 시작되었고, MemberRepository와 LogRepository는 MemberService에서 시작된 트랜잭션에 참여하게 된다.(Propagation.REQUIRED) MemberRepository는 정상적으로 처리되어 커밋이 호출되었고 LogRepository는 중간에 문제가 생겨 롤백을 호출하는 상황이다. 내부 트랜잭션에서 롤백을 호출하면 사용 중인 Connection에 롤백을 호출하는 것이 아닌 rollbackOnly 표시를 하게 된다. 이 표시는 마지막에 외부 트랜잭션이 확인하여 커밋 또는 롤백을 처리하는 데 사용된다.

 

내부 트랜잭션(LogRepository)에서 발생한 예외가 외부 트랜잭션으로 전달되었을 때, 이 예외를 처리하는 로직이 존재하지 않는다면 자동으로 롤백을 호출한다. 이 경우에는 트랜잭션 동기화 매니저에 보관된 커넥션에 rollbackOnly가 표시되어 있는지 확인하지 않고 바로 롤백 처리를 한다. 따라서 클라이언트가 요청한 내용을 처리하지 않고 RuntimeException 예외를 반환한다.

 

예외 2 - UnexpectedRollbackException

예외 1과 달리 내부 트랜잭션(LogRepository)에서 발생한 예외를 외부 트랜잭션에서 try-catch로 예외 처리를 한 상황이다. 위에서 말했듯이 내부 트랜잭션에서 롤백이 호출되면 사용 중인 커넥션에 rollbackOnly를 표시한다고 했다. 

 

외부 트랜잭션에서 예외를 처리하였기 때문에 커밋을 요청하지만 최종적으로는 롤백 처리가 된다. 외부 트랜잭션의 AOP Proxy가 사용중인 커넥션을 확인하였을 때 rollbackOnly가 표시되었기 때문에 내부 트랜잭션에서 롤백을 호출하였음을 알 수 있다. 따라서 외부 트랜잭션은 커밋 요청을 무시하고 롤백 처리를 함과 동시에 클라이언트에 UnexpectedRollbackException 예외를 반환한다.


결론

예외 1은 내부 트랜잭션이 RuntimeException을 외부 트랜잭션에 던져 롤백 처리를 하고 RuntimeException 예외를 발생시킨다.

예외 2는 내부 트랜잭션이 RuntimeException을 외부 트랜잭션에 던졌으나 이를 예외 처리를 하였다. 그러나 커넥션에 rollbackOnly 표시가 있기 때문에 롤백 처리를 하고 UnexpectedRollbackException 예외를 발생시킨다.

 

예외 1은 rollbackOnly를 확인하지 않고 바로 RuntimeException 예외를 던졌고, 예외 2는 외부 트랜잭션은 커밋 요청을 하였으나 rollbackOnly로 인해 내부 트랜잭션이 롤백을 호출하였기 때문에 UnexpectedRollbackException 예외를 던진다.