@Transactional 옵션으로 (rollbackFor = Exception.class)를 왜 쓸까?

☝️
여기서 설명하는 @Transactionalorg.springframework.transaction.annotation.Transactional이다. javax, jakarta 패키지에 기본 포함된 @Transactional의 경우 동일한 기능을 하기는 하나, 일부 옵션의 이름이 상이할 수 있으니 유의 바란다. 참고로 javax, jakarta에서 rollbackFor와 동일한 옵션은 rollbackOn이다.

그런 글들 자주 봤다

@Transactional 관련 글을 보면 예제로 @Transactional(rollbackFor=Exception.class)를 적어두거나, 선임이 해당 내용을 적어 넣으라고 했다는 내용을 자주 볼 수 있다. 대부분의 어노테이션들은 가장 많은 상황을 포함할 수 있도록 옵션들에 대한 기본 값을 설정 해놓는 경우가 많다. 따라서 일반적인 어노테이션들은 그냥 선언만 해도 대부분의 상황에서 정상적인 동작이 가능하다. 개발자라는 족속들이 사람들이 어떤 사람인지 생각해 보자, 일단 효율부터 추구하고 보는 사람들이 대부분이다. 그런데도 굳이 옵션을 추가로 적는다는 것은 그만한 이유가 있을 것이다. 이제부터는 그 이유를 알아보려고 한다.


트랜잭션이 모든 상황에서 롤백 시키지는 않는다

@Transactional은 아주 마술 같은 어노테이션이다. 그냥 메소드나 클래스 위에 선언만 해두면, 서비스 코드에서 오류가 발생하더라도 알아서 해당 진행 상황들을 롤백해주니까 말이다. 덕분에 코드 여기저기에 덕지덕지 트랜잭션 관련 객체를 선언하고 코드를 작성할 필요도 없다. 하지만 트랜잭션이 모든 예외 상황에서 롤백 처리를 해주는 것은 아니다. 이는 Java 예외 처리를 공부했다면 알 수도 있는 사실일 텐데, 트랜잭션 동작은 Unchecked Exception, Error가 발생했을 때만 롤백 처리되고, Checked Exception이 발생했을 때는 오히려 커밋을 해버린다. 대체 왜 이런 차이가 있는 걸까? 우선 위에서 나온 Checked/Unchecked ExceptionError의 차이에 대해 알아보자.

Checked Exception

  • 예외 처리가 필수다.
  • 개발자가 예측하기 쉽다.
  • 코드 수준의 문제 보다는, 주로 외부적인 요인(파일/경로 없음, 입출력 도중 연결 끊김 등)에 의해 발생한다.

Unchecked Exception

  • 예외 처리를 강제하지 않는다.
  • 개발자가 예측하기 어렵다.
  • 주로 코드 수준에서 발생하는 문제다. 일반적으로 개발자의 부주의 함에 의해 발생하는 경우가 많다.

Error

  • 예외 처리를 강제하지 않는다. 애초에 코드 수준에서 흐름을 복구할 수 있는 방법이 전무하다.
  • 개발자가 예측하기 어렵다.
  • 외부적인 요인에 의해 발생한다.

여기서 트랜잭션이 예외 상황 발생 시, 롤백 처리해주는 Unchecked ExceptionError에 대한 공통점을 찾아보자. 우선 이 둘은 예외 처리를 강제하지 않고 개발자가 예측하기 어렵다는 공통점이 있다. 반면 롤백 처리되지 않는 Checked Exception의 경우에는 예외 처리를 강제하고 예측하기 쉽다는 차이가 있다. 여기서 유추해 볼 수 있는 것은 개발자들이 Java의 트랜잭션을 설계할 때, 예외 처리를 강제하지 않고 개발자가 예측하기 어려운 오류/예외 상황에 대해서는 기계적으로 롤백 처리를 하는 것이 더 효율적이라 판단했을 것으로 볼 수 있다. 이는 충분히 합리적이어 보인다.

우선 Unchecked ExceptionError는 개발자가 예측하기 어렵거나 뭘 해볼 수 없기 때문에, 이를 명시적으로 예외 처리 하지 않아도 괜찮도록 설계된 것이다. 이럴 경우 당연히 개발자가 흐름을 복구하기 위한 오류/예외 처리를 대부분 하지 않았을 것이고, 또 오류/예외 처리의 필요성을 인지하지 못했을 가능성이 높다.(컴파일 단계에서 확인이 불가능 하니까!) 그렇다고 지금까지 진행했던 사항을 그대로 방치하는 것도 안된다. 그랬다가는 ACID 원칙에 어긋날 가능성이 높다. 따라서 개발자가 아무것도 못하는 상황이라면 트랜잭션이 강제로 롤백 처리를 하도록 하는 게 현명한 선택일 것이다.

반면 Checked Exception과 같이 개발자가 예측이 쉬우며 예외 처리가 필수적인 예외 상황에는 기계적인 롤백 보다는, 각각의 예외 별로 개발자가 예외 상황에 대한 흐름을 복구하도록 하는 것이 더 나은 선택이 될 수 있다. 이는 @Transactional의 주석을 보면 확실하다. @TransactionalrollbackFor() 메소드 주석을 살펴보면 다음과 같은 내용을 찾아볼 수 있다.

By default, a transaction will be rolled back on RuntimeException and Error but not on checked exceptions (business exceptions). See org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable) for a detailed explanation.

"트랜잭션은 RuntimeException과 Error가 발생했을 경우에만 동작하며, checked exceptions (비즈니스 예외)에는 동작하지 않습니다. 자세한 내용은 org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)의 설명을 살펴보십시오."

이 주석을 따라 다시 DefaultTransactionAttribute 클래스의 rollbackOn() 메소드 주석을 살펴보자.

The default behavior is as with EJB: rollback on unchecked exception (RuntimeException), assuming an unexpected outcome outside any business rules. Additionally, we also attempt to rollback on Error which is clearly an unexpected outcome as well. By contrast, a checked exception is considered a business exception and therefore a regular expected outcome of the transactional business method, i.e. a kind of alternative return value which still allows for regular completion of resource operations.

This is largely consistent with TransactionTemplate's default behavior, except that TransactionTemplate also rolls back on undeclared checked exceptions (a corner case). For declarative transactions, we expect checked exceptions to be intentionally declared as business exceptions, leading to a commit by default.

너무 긴 내용이기 때문에, 중요한 부분만 번역해 보도록 하겠다.

unchecked exception (RuntimeException)에 의한 롤백은 비즈니스 규칙을 벗어난 예상치 못한 결과를 가정합니다. 또한 명백히 예상치 못한 결과인 Error 상황에 대해서도 롤백을 시도합니다. 이와 대조적으로 checked exception는 비즈니스 예외로 간주 되므로예상되는 결과, 즉 리소스 작업의 정기적인 완료를 허용하는 일종의 대체 반환 값입니다. 선언적 트랜잭션의 경우 checked exception이 의도적으로 비즈니스 예외로 선언되어 기본적으로 커밋으로 이어질 것으로 예상됩니다.

즉, Unchecked ExceptionError는 비즈니스 로직 과정에서 발생할 수 있는 예상치 못한 결과 이므로 롤백 처리를 한다. 반면 Checked Exception예상 가능한 결과이며 비즈니스 로직의 일부로 보기 때문에 커밋까지 이어지는 것이다.


그래서 (rollbackFor = Exception.class)는 왜 쓰는데?

거두절미 하고 결론부터 얘기하자면, 위의 옵션은 Checked Exception이 발생하는 상황에도 롤백을 시도하도록 강제하는 것이다. 즉, 프로그램 실행중 발생할 수 있는 모든 오류/예외 상황 구별없이 모두 롤백처리 하도록 강제하는 것이다. 하지만 이것은 좋은 방법은 아니다. 애시당초 트랜잭션을 설계할 때의 의도를 무시하고 전혀 다르게 동작 시키도록 강제하는 것이기 때문이다.

하지만 그렇다고 해서 반드시 rollbackFor = Exception.class 사용을 지양할 필요는 없다. 애시당초 트랜잭션 사용 시, 이 둘에 대한 구분을 확실하게 하려고 했다면 rollbackFor 옵션을 만들지 않으면 그만일 뿐인 문제다. 이 옵션이 별도로 존재한다는 것은,  개발자들이 이 옵션을 사용해야만 할 예외 상황이 있음을 알고 있다는 뜻이다. 따라서 해당 옵션을 사용하기 위해서는, 이 옵션을 사용할 만한 타당한 근거(상위 메소드로 던져서 비즈니스 로직에서 처리할 수 없거나, 어차피 롤백 처리 말고는 할 것이 없다거나 등...)가 있는지 명확히하고 사용하는 것이 좋을 것이다.


정리하며

사실 개인 프로젝트를 진행하면서, 지금까지 rollbackFor 옵션에 대해서는 잘 모르고 있었다. 애시당초 쓸 일도 없었기 때문이다. 그러다 현재 회사에서 진행중인 프로젝트를 소스코드에서 발견한 해당 옵션을 보고 궁금증이 들어서 여러모로 조사하다 나와 비슷한 궁금증을 가진 사람이 많아 보여서, 이 기회에 글로 정리해서 남겨보았다.


참고한 문헌 및 글

  1. [Spring] @Transactional(rollbackFor={exceptionClass}) | da-nyee