0. 들어가며 🏃🏻♂️
스프링으로 개발을 하다 보면 예외에 대한 코드를 작성하는 경우가 많이 있습니다. 예외처리를 위해 커스텀 예외 클래스를 종종 만들곤 했는데 대부분의 경우 RuntimeException을 상속받아 클래스를 작성했습니다. 이러한 방법에 불편함이 없었기 때문에 별생각 없이 쓰고 있다가 문득 나는 왜 개발할 때 Checked Exception이 아닌 Unchecked Exception 즉, Runtime Exception을 주로 사용했지라는 물음에 답을 할 수 없음을 깨달았습니다. 이에 해당 내용을 공부해 보았고, 공부한 내용을 정리해보려 합니다.
1. Java의 Exception 간단한 복습 📖
본격적으로 글을 쓰기 전에 먼저 Java에서의 Exception에 대해 간단히 내용정리를 해보겠습니다.
Java에서 예외와 관련된 Class들은 아래 그림과 같은 계층 구조를 가지고 있습니다.
Error는 이번 글에서 중요한 내용이 아니므로 넘기도록 하고.. 살펴볼 곳은 바로 Exception(Checked)와 RuntimeException(Unchecked) 부분입니다.
먼저 Checked Exception을 살펴보도록 하겠습니다. Checked Exception은 Exception의 서브 클래스 중 RuntimeException을 상속받지 않은 예외를 말하는데 Checked Exception를 발생시키면 이를 반드시 예외처리 코드를 통해 처리해주어야 합니다. 예외처리 코드라 함은 try-catch문을 통해 예외를 잡아 처리하거나 throws를 통해 예외를 밖으로 던져 책임을 넘겨버리는 작업을 말합니다. 이렇게 예외 처리를 하지 않으면 컴파일 에러가 발생하죠.
그에 반해 Unchecked Exception은 RuntimeException을 상속받은 예외입니다. Unchecked Exception은 Checked Exception과 다르게 예외처리를 하지 않아도 컴파일 에러가 나지 않는 Exception입니다. 물론 예외처리하고 싶다면 해줘도 상관없습니다.
위와 같이 Checked Exception과 Unchecked Exception은 예외처리를 필수로 해야하는가 안 해도 되는가라는 큰 차이가 있습니다. 그렇다면 이러한 특성 차이로 인해 어떤 장단점이 생기는지를 알아보고, 실제 개발할 때는 어떤 예외를 주로 사용해야하는지에 대한 원칙을 정할 필요가 있습니다.
2. Checked Exception을 쓰는건 어떤가❓
Checked Exception은 예외처리를 하지 않는다면 컴파일 에러가 난다는 특징 때문에 개발자가 발생할 가능성이 있는 예외를 실수로 누락할 여지를 사전에 차단해 줍니다. 늘 그렇듯 컴파일 에러는 문제가 발생하기 전 미리 발견할 수 있는 좋은 에러고 항상 좋은 안전장치가 됩니다. 이 말만 두고 본다면 "실수를 막아준다니 좋은데..?" 라는 생각이 들 수 있겠죠. 하지만 Checked Excption에는 크게 두 가지 단점이 존재합니다.
첫 번째로 복구 불가능한 예외가 발생하는 경우 난감해지는 상황이 생긴다는 것입니다. Checked Exception은 개발자에게 예외처리를 강제하는데 이는 해당 Exception이 발생했을 시 이를 적절히 복구해달라! 라는 의도를 담고 있습니다. 그런데.. 우리가 자주 접하는 서버 환경에서는 복구 가능성이 있는 예외가 그리 많지 않습니다. 한 예로 Checked Exception 중 하나인 SQLException이 발생하는 상황만 살펴보더라도 SQL 문법에러, DB 자체의 문제 가능성 등 복구가 불가능한 상황들에서 발생하는 경우가 많습니다. 이런 경우에는 개발자가 예외를 잡아서 무언가를 조치할 방법이 없습니다. 이때 미봉책으로 try-catch 문을 사용해 예외를 대충 잡아먹어 버리면 문제가 더 심각해질 수 있습니다. 그렇다고 예외를 잡지 말고 throws로 예외처리 책임을 전가해 버리자니 메서드 caller 쪽에서도 이를 처리할 방법이 없기는 마찬가지인 상황일 테니 무책임한 throws가 되어버릴 수밖에 없습니다. 즉, throws의 의도에 맞지 않게 사용하는 꼴이 되어버리죠. 또한 무책임한 throws들 때문에 caller 메서드들에도 throws 문 여러 개가 덕지덕지 붙게 되어 코드가 복잡해질 수 있습니다. 실제로 이러한 이유 때문에 Checked Exception들의 공통 조상인 Exception을 사용해 throws Exception 식으로 코드를 뭉개버리고 넘어가는 나쁜 코드들도 많이 생겨났다고 합니다. 아래와 같이 말이죠.
두 번째로는 예외 클래스에 대한 의존 관계가 생긴다는 문제가 있습니다. 앞서 살펴봤듯 복구가 불가능한 예외들은 계속해서 위에 있는 계층에 예외를 던집니다. 위에 있는 계층이라고해서 복구 불가능한 예외를 처리할 수 있는 것은 아니니 계속해서 윗 계층에 던질 것이고 그렇게 되면 모든 계층에서 해당 예외에 의존하게 되는 상황이 되어버립니다. 아래 코드와 같이 말이죠!
위 코드를 보시면 Repository에서 발생한 Exception이 Service의 메서드 -> Controller의 메서드까지 올라가게 되고 이 때문에 메서드들이 전부 SQLException에 의존하게 되는 문제가 생깁니다. 불필요한 의존관계가 생겨버리는 것이죠. 이러한 문제는 가령 Repository에서 사용하는 DB 접근 기술을 변경하고 싶을 때 쉽게 갈아 끼우기 힘든 코드를 만듭니다. 예를 들어 지금 사용하고 있는 것과 다른 DB 접근 기술을 사용할 때는 기존의 SQLException이 아닌 JpaException이 발생할 수도 있는 것이죠. 이렇게 되면 DI를 통한 장점을 누리지 상황으로 이어지게 됩니다.
3. Runtime(Unchecked) Exception은 어떤가❗️
위에서 Checked Exception의 단점에 대해 알아보았는데 그렇다면 Runtime Exception은 어떨까요?
우선 Runtime Exception 역시 단점은 존재합니다. Checked Exception과 다르게 예외처리를 강제하지 않기 때문에 개발자가 처리해야 하는 예외를 실수로 빼먹을 수 있죠. 하지만 이러한 단점에 비해 Checked Exception의 단점들이 좀 더 문제이기 때문에 최근 Java와 Spring의 코드들에서는 Runtime Exception을 더 선호하는 것으로 보입니다. (실제로 내부 코드들을 좀 뒤져보니 Checked Exception을 try-catch문으로 잡아 Unchecked Exception으로 전환하는 코드들을 많이 볼 수 있었습니다.)
Checked Exception에서 살펴본 두 가지 단점 모두 예외처리 코드를 작성하다 보니 (특히 throws 문으로 인해) 생기는 일인데 Runtime Exception의 경우 예외처리 코드가 필수적이지 않다 보니 Checked Exception에서 살펴본 단점을 자연스럽게 없앨 수 있습니다.
Runtime Exception을 사용하게 되면 복구 불가능한 예외를 모든 계층에서 다루어줄 필요가 없게 됩니다. 즉, 예외가 발생한 계층 이외에 상위 계층에서는 해당 예외에 대해 신경 쓸 필요가 없어지고 이는 불필요한 throws문과 예외 의존관계를 없애는 효과를 줍니다. 이렇게 Runtime Exception을 사용해 처리해 버리고 Spring MVC 같은 경우 ControllerAdvice와 같은 기능을 이용하여 발생한 예외에 대해 공통 처리하는 로직만 작성해주면 이전보다 깔끔하게 문제가 해결됩니다. 이러한 방식은 서버 환경에서 더욱 효과적입니다. 많은 사용자들이 동시에 요청을 보내고 각 요청이 독립적인 작업으로 취급되는 서버 환경에서는 하나의 요청 처리 중 예외가 발생 시 그 작업을 중단시키면 됩니다. 이때 사용자에게는 적절한 예외 문구를 내려주고, 적절한 로그를 남긴 뒤 개발자에게 해당 내용을 슬랙 혹은 메일 등의 수단을 사용해 통보하는 등의 수단을 마련해 놓으면 되죠.
이러한 이유로 지금까지 개발할 때 RuntimeException을 상속받은 커스텀 예외를 만들고, 그를 활용해 예외 처리를 하는 것이 딱히 문제가 있다고 느껴지지 않고 자연스러웠던 것 같습니다.
5. 결론 😎
이번에 공부한 내용을 정리하고 제가 나름대로 내린 결론은 다음과 같습니다.
서버 환경에서는 복구 불가능한 예외 상황들이 많이 생기며, 많은 요청들이 들어오는 상황에서 예외가 발생한 요청을 정상흐름으로 돌리도록 해야 할 의무성도 많이 떨어지는 것 같습니다. 그저 사용자에게는 적절한 에러메시지를 주고 개발자를 위해서는 로깅과 문제 발생 알림 처리 정도를 하면 괜찮은 것이죠.
또한 복구 불가능한 예외 상황을 Checked Exception으로 잡아봤자 불필요한 예외처리 코드만 생기게 될 가능성이 높습니다. 불필요한 예외처리 코드들은 try-catch문, throws문의 사용 의도를 제대로 살려서 사용하기 어렵고 개발자로 하여금 예외를 대충 처리하게끔 유도하는 결과를 낳게 됩니다. 이 역시 문제이지만 예외에 의존관계가 생겨버린다는 문제로 인해 DI의 장점을 제대로 살린 코드를 작성하기도 어려워집니다.
따라서 개발할 때는 RuntimeException을 사용하고, ControllerAdvice에서 발생한 예외를 적절히 처리해주는 것을 기본적인 원칙으로 생각하는 게 장점이 더 많은 것 같고, 꼭 정상흐름으로 바꾸어 처리하고 싶은 예외의 경우에만 한정하여 Checked Exception을 사용하는 것이 좋아 보입니다.
'Spring' 카테고리의 다른 글
[Spring] 동시성 이슈 해결하는 방법 찍먹하기 (1) | 2023.07.22 |
---|---|
[Spring] 의존성 주입과 의존성 주입 방법에 대하여. (0) | 2023.04.30 |
[Spring] Spring MVC는 어떻게 요청에 응답할까? (Dispatcher Servlet을 중심으로) (0) | 2023.03.16 |
[Spring & Dev] 웹소켓에 대하여. (1) | 2023.01.14 |
[Spring] @SpringBootApplication 에 대하여. (0) | 2022.04.12 |