지금까지 여느 글과 마찬가지로, 이번 글도 내가 개발하면서 새롭게 깨달은 부분들을 정리해두려는 기록이다. 이번 주제는 예외 처리이다.
많은 글들이 성공적인 응답을 어떻게 구성할지에 대해서는 상세히 설명하지만, 실패했을 때 어떤 기준으로 예외를 처리하고 응답을 구성해야 하는지에 대해서는 명확히 다루지 않는다. 나 역시 예외를 던질 때마다 새로운 예외를 꼭 정의해야 하는지, 매번 잡아서 처리해야 하는지 애매한 지점이 많았다.
이참에 예외 처리에 대한 기준을 다시 세워야겠다는 생각이 들었고, 그 계기로 다시 꺼낸 책이 토비의 스프링 3.1이다. 두 번쯤 정독했던 책이지만, 이번엔 ‘예외’라는 주제로 다시 읽어보니 이전과는 전혀 다르게 다가왔다.
읽기 전에 스스로 세운 전제는 이렇다.
웹 애플리케이션이든 일반 애플리케이션이든, 자바의 예외는 동일하게 동작한다. 다만 요즘 웹 환경에서는 클라이언트와 서버가 분리되어 있기 때문에, 예외 상황을 HTTP 응답으로 표현해야 하고, 이 과정에서 자연스럽게 메시지 포장이 필요해진다. 하지만 그렇다고 해서 자바의 예외 처리 방식 자체가 달라지는 것은 아니다.
토비의 스프링에서는 예외 처리 방식으로 크게 세 가지를 소개한다. 예외 복구, 예외 회피, 그리고 예외 전환. 이 중 웹 환경에 적합한 방식은 단연 예외 전환이다.
예외 전환은 발생한 예외를 그대로 전달하지 않고, 의미를 분명하게 하고 처리하기 쉽게 바꿔 던지는 방식이다. 예외 전환의 목적은 두 가지다.
사실 우리가 @ExceptionHandler, @ResponseStatus 등을 통해 예외를 JSON으로 변환해 응답하는 방식 자체가 예외 전환에 해당한다.
그동안 나도 그런 방식으로 예외를 처리해왔는데, 왜 매번 정의한 예외를 잡아야 하는지에 대해선 명확한 기준이 없었다.
하지만 돌이켜보니, 예외 전환의 두 가지 목적 "의미를 분명히 하고, 처리하기 쉽게 포장하는 것" 이 성립하지 않는다면, 굳이 잡을 이유도 없다는 걸 깨달았다. 반대로, 기존에 잡은 예외를 재사용하더라도 이 두 목적을 충분히 충족할 수 있다면, 역시 새 예외를 정의하고 잡을 이유도 없다. 이게 내게 꽤 큰 인사이트였다.
예외를 새로 정의하는 이유에 대해서도 책에서 힌트를 얻었다.
던진다는 고전적인 기준이다.
다만 Spring MVC의 특성상 @ExceptionHandler가 checked/unchecked 여부를 따질 수 없기 때문에, 복구 가능성을 구분하기 위한 커스텀 예외를 도입하는 것이 지금까지는 별다른 고민 없이 당연하게 해왔던 방식이었다.
결국 복구 가능하기에 클라이언트가 의미를 파악하고, 적절히 대응할 수 있는 예외는 커스텀 예외로 정의하는 것이 맞다.
그리고 @ExceptionHandler에서 이를 잡아, 응답으로 전환해주는 과정.
이게 바로 예외 전환이다.
그렇다면 복구 불가능한 예외는 어떻게 해야 할까? 이건 선택의 문제라고 본다. 내부에서 로깅만 하고 응답에는 드러내지 않을 수도 있고, 어차피 복구는 불가능하지만 클라이언트에게 오류 메시지를 알려야 하는 경우라면 역시 전환해서 응답해줘야 한다. 디버깅을 위해서든, 클라이언트 피드백을 위해서든, 커스텀 예외가 도움이 되는 지점이다.
정리하자면, 클라이언트가 복구 가능한 예외는 커스텀 예외로 정의해서 명시적으로 응답을 설계하고, 복구 불가능한 예외는 로깅만 하든 응답으로 노출하든 상황에 맞게 선택하면 된다.
사실 지금까지 해왔던 방식에서 크게 달라진 건 없지만, 이제는 그 방식의 이유와 기준을 알고 있다는 점에서 다르다. 앞으로도 예외를 던질 때마다 그 의미와 방향성을 놓치지 않고 싶다.
한 가지 번외로, IllegalArgumentException, IllegalStateException 같은 자바의 표준 예외는 어떻게 다뤄야 할지도 고민해봤다. 이 예외들은 자바 표준 라이브러리뿐 아니라 스프링, 하이버네이트 등 다양한 프레임워크에서도 널리 쓰이기 때문에, 우리가 직접 던진 건지 아니면 내부에서 발생한 건지 명확히 알기 어렵다. 때문에 예외 전환의 관점에서 봤을 때, 의미를 분명히 하거나 처리하기 쉽도록 만들 필요가 없는 예외일 수도 있다.
이런 이유로 나는 의미 있는 예외 전환이 필요하다면, 즉 우리가 던진 예외임을 명확히 하고 싶다면, 커스텀 예외를 만들어서 사용하는 걸 추천한다. 그렇게 하면 @ExceptionHandler에서도 일관되게 예외를 처리할 수 있고, 클라이언트에게도 명확한 응답을 줄 수 있다.