Unique 제약조건 위반 예외인 DataIntegrityViolationException가 발생하는 문제가 있었는데 GlobalExceptionHandler에서 해당 예외를 처리하고 있음에도 불구하고 예외가 잡히지 않는 문제가 발생했었다.
그래서 처음에는 GlobalExceptionHandler가 처리하지 못하는 범위에서 예외가 발생했나 싶어서 GlobalExceptionHandler의 처리 범위를 찾아보았다.
GlobalExceptionHandler는 스프링 MVC의 DispatcherServlet 내부에서 동작하는 HandlerExceptionResolver의 구현체(구체적으로는 ExceptionHandlerExceptionResolver)에 의해 구동된다. 즉, 웹 요청이 DispatcherServlet에 진입한 이후부터 응답이 나가기 전까지의 범위 내에서 발생하는 예외만 처리할 수 있다.
| 구분 | 처리 가능 | 처리 불가 |
|---|---|---|
| 웹 레이어 | Controller 내부 에러 / Interceptor (preHandle, postHandle) / Argument Resolver · Message Converter 바인딩 에러 | Filter 계층 에러 (Spring Security 인증·인가 등) / 서블릿 컨테이너 자체 에러 (404 등) |
| 비즈니스 레이어 | Service · Repository에서 throw되어 Controller까지 전파된 런타임 예외 | 컨트롤러 응답 반환 후 WAS 레이어로 전파된 이후 단계의 에러 |
결과는 GlobalExceptionHandler의 처리 범위때문에 잡히지 않았던 것이 아니였다.
@Transactional이 서비스 계층 메서드에 적용되어 있다면, 트랜잭션 커밋과 영속성 컨텍스트의 flush는 서비스 메서드가 종료되고 제어권이 컨트롤러로 돌아가기 직전, 즉 프록시 레이어(서비스 호출부)에서 일어난다. 따라서 발생 시점 자체는 컨트롤러 메서드 호출 흐름 내부이므로 물리적인 범위로만 보면 GlobalExceptionHandler가 인지할 수 있는 영역이다.
그럼에도 불구하고 @ExceptionHandler(DataIntegrityViolationException.class)가 이를 잡지 못하는 진짜 원인은 범위의 문제가 아니라 예외가 발생하는 시점과 그에 따른 스프링의 예외 변환(Exception Translation) 매커니즘 때문이었다.
결론적으로 DataIntegrityViolationException 예외 유실 문제는 GlobalExceptionHandler의 처리 범위때문이 아닌 예외 발생 시점에 의한 문제였다.
트랜잭션 커밋은 서비스 메서드가 종료된 후의 AOP 프록시의 커밋 시점이기 때문에 TransactionSystemException으로 발생한 것이었다.
다만 이 문제에 대한 해결법으로서 flush()를 명시적으로 호출하고 DataIntegrityViolationException로 catch 할 지, GlobalExceptionHandler에서 TransactionSystemException를 catch 할 지는 선택의 문제인 것 같다.