본문 바로가기

카테고리 없음

UnExpectedRollbackException과 예외를 잡아내지 못한 거짓 음성 테스트

 

 

 

센트리에 생전 처음 보는 에러가 나타났다.

이게 대체 무엇이냐?

 

이게 뭔지는 이미 다른 훌륭한 개발자 분들이 아주 상세하게 정리해주셨다.

온라인 쓰레기를 생산하지 않기 위해 자세한 설명은 생략한다. 아래 링크 보셈.

 

https://techblog.woowahan.com/2606/

 

응? 이게 왜 롤백되는거지? | 우아한형제들 기술블로그

{{item.name}} 이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다.

techblog.woowahan.com

 

 

요약하자면 아래와 같다.

1. try catch로 감싼다고 롤백 안되는 거 아니다.

2. 예외가 자식 트랜잭션의 경계를 넘었다면 해당 트랜잭션의 rollback status에 true가 마킹되며 최종적으로 부모 트랜잭션이 commit()을 실행할 때 예외가 발생한다. (님아 이거 롤백해야 하는데 왜 커밋하려고 함?)

 

 

근데 제가 설마 TC하나 없이 예외가 전파 안될 거라고 단정 짓고 코드를 작성했을까봐요??????????

회사 코드를 올릴 수는 없으니 간단한 예시 코드를 작성해왔다.

 

 

마트 서비스도 다를 거 없음.

 

ExampleService는 AService, BService의 process()를 각각 호출한다.

각 서비스 중 하나가 실패하더라도 나머지는 성공하기를 원하는 상황이다.

 

그래서 예쁘게 try catch로 감싸고 로그만 남기기로 했다.

 

이제 테스트 코드를 작성하고 실행해보자.

 

성공~!

 

ㅋㅋ

이래서 배포 전에 문제를 미리 파악하지 못했다.

 

무엇이 문제일까요?

 

 

 

1. 테스트 메서드(혹은 클래스에) @Transactional을 적용하면 테스트 코드 내부에서 실행하는 메서드는 테스트 메서드의 트랜잭션을 위임받게 된다.

2. 테스트 트랜잭션은 최종적으로 commit()이 아닌 rollback()을 실행하고 데이터를 원상복구 시킨다.

3. UnexpectedRollbackException은 rollback하기로 예정된 트랜잭션이 commit() 메서드를 호출할 때 발생하는 예외이므로 감지 불가능.

 

그러면 어떻게 이 예외를 테스트코드에서 발생시킬 수 있을까?

테스트할 메서드의 Transaction의 propagation을 Requires_NEW로 수정해보자.

 

 

 

Transaction silently rolled back because it has been marked as rollback-only org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only at app//org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752) at app//org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)

 

바로 UnexpectedRollbacException이 발생한다.

전파 단계를 REQUIRES_NEW로 설정하면 테스트 트랜잭션과 테스트 대상의 트랜잭션이 별개로 동작하므로 commit() 시점에 예외를 던지게 되는 것이다.

 

하지만 테스트를 위해 굳이 어플리케이션 로직의 설정을 건드릴 필요는 없다.

TestTransaction 클래스의 정적 메서드를 이용해 테스트 트랜잭션을 강제 커밋시킨 뒤 테스트할 메서드를 실행하는 방법으로도 똑같은 결과를 얻을 수 있다.

 

터진다.

 

 

 

하.. 이런 그지 같은 거짓 음성.

스프링 부트 테스트에 @Transactional을 사용하는 것이 테스트 신뢰도를 떨어트릴 수 있다는 걸 알고는 있었지만 직접 겪어보니 당황스럽다.

물론 @Transactional의 문제보다는 스프링에 대해 잘 이해하지 못하고 있었던 내 잘못이 크다.

 

아무튼 문제가 생겼으니 해결을 해야한다.

해결책은 네 가지가 있다.

 

1. @Transactional에 noRollbackFor 옵션 지정하기

 

주의 : 부모 트랜잭션의 옵션에 noRollbackFor를 걸어봤자 아무 의미 없다.

자식 트랜잭션에서 예외 발생 시 rollback status를 true로 박아버리는 거라서 자식 트랜잭션이 rollback status를 건드리지 않도록 설정해줘야 한다.

 

 

2. @Transactional의 propagation을 Requirest_new로 설정하기

 

마찬가지로 당연히 자식 트랜잭션에 설정해줘야 한다.

트랜잭션 전파 단계에서 Requires_new는 이전에 진행중이던 트랜잭션과 별개로 동작하게 되므로 기존 트랜잭션에 영향을 주지 않게 된다.

 

 

3. CheckedException 던지기

 

근데 굳이 이런 짓을 해야할까?

저는 안할래요

 

 

4. 실패, 성공 여부를 반환값으로 전달하기

 

코틀린에서는 sealed 클래스를 이용해서 우아하게 처리할 수 있긴 한데..

내 로직에서 굳이 이 정도의 섬세한 조정은 필요하지 않았다.

 

 

 

 

 

 

나는 1번이나 4번이 마음에 든다.

 

1번의 경우 -> a 예외 발생 시에는 전체 롤백, b 예외 발생 시에는 롤백하지 않는 등 섬세한 조정 가능. but, 커스텀 예외를 만들어서 처리해야 안전할 듯.

4번의 경우 -> 어차피 외부에서 반환값을 받아서 처리하는 경우에는 괜찮은 것 같다.