본문 바로가기

Language & Framework/개발잡담

사용자 정의 예외에 대한 고민

* 이 카테고리에 올라오는 모든 글은 정론이 아닌 개인의 의견입니다. *

 

 

 

수많은 UnchekedException

 

 

두어달 전, 쓸데없는 Custom Exception을 양산하지 말라는 취지의 글들을 처음으로 접했다.

 

https://stackify.com/java-custom-exceptions/

 

Implement Custom Exceptions in Java: Why, When and How

Learn 4 best practices for custom exceptions in Java. Understand when and how to use them in your application.

stackify.com

 

https://tecoble.techcourse.co.kr/post/2020-08-17-custom-exception/

 

custom exception을 언제 써야 할까?

우아한테크코스의 두 크루인 오렌지와 우가 싸우고 있다. 왜 싸우고 있는지 알아보러 가볼까? 오렌지 : 아니 굳이 사용자 정의 예외 안 써도 됩니다!! 우 : 아닙니다!! 써야 합니다!!! 사용자 정의

tecoble.techcourse.co.kr

 

 

 

게다가 EffectiveJava의 Item 72는 "표준 예외를 사용하라"이다.

 

 

...
자바 라이브러리는 대부분 API에서 쓰기에 충분한 수의 예외를 제공한다.
표준 예외를 재사용하면 얻는 게 많다. 그중 최고는 여러분의 API가 다른 사람이 익히고 사용하기 쉬워진다는 것이다.
많은 프로그래머에게 이미 익숙해진 규약을 그대로 따르기 때문이다. 여러분의 API를 사용한 프로그램도 낯선 예외를 사용하지 않게 되어 읽기 쉽게 된다는 장점도 크다.
마지막으로, 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.
...

- EffectiveJava Item.72

 

 

내가 이런 정보들을 처음 접한 당시, 나는 갓 사용자 정의 예외를 접해서 마구잡이로 만들어보던 참이였고, 어느새 산처럼 쌓여있는 커스텀 예외들을 보며 현자 타임에 잠긴 상태였기 때문에 이런 글들이 더욱 와닿았다.

 

실제로 우리가 잘 사용하지 않을 뿐이지, 이미 자바 표준 예외에 엄청나게 다양한 것들이 정의되어 있다.

특히 IllegalArgumentsException이나 IllegalStateException, NoSuchElementException, MissingResourceException 같은 것들은 보기만 해도 다방면으로 써먹을 수 있어서, 당장에라도 우리의 사용자 지정 예외를 다 지워버리고 자바 표준 예외를 사용하고 싶은 마음이 들게 한다.

 

이런 정보들을 한동안 나를 굉장히 괴롭게 했다.

무엇이 최선인가?

만약 내가 자바 표준 예외를 사용하자고 한들, 팀원들이 동의할까?

팀원들이 올바른 표준 예외를 사용할 수 있을까?

그리고 애초에 나부터도 통일성있게 예외를 적용할 자신이 있는가?

 

내가 고민 끝에 내린 결론은 다음과 같다.

 

 

 

 

(적어도 내가 가정할 수 있는 상황에서는) 서비스 로직에서 발생하는 예외는 사용자 정의 예외가 최선이다.

 

이유는 다음과 같다.

 

첫 번째, 표준 예외는 분명히 대부분의 상황에 적용할 수 있을만큼 다양하게 준비되어 있다.

그러나 비즈니스 로직에서 사용할만한 예외는 내가 위에서 언급한 (IllegalArgumentsException, IllegalStateException, NoSuchElementException, MissingResourceException) 정도가 끝이다.

네 가지만으로 다양한 비즈니스 로직에서의 예외를 표현하기 어렵다.

 

두 번째, 사실 첫 번째에서 말한 것이 어렵지만 불가능한 것은 아니다. 위의 예외들은 굉장히 다양한 경우를 포괄적으로 나타내고 있기 때문이다.

그러나 이 부분이 문제가 된다. 비즈니스 로직에서 발생하는 대다수의 문제가 결국 IllegalArgumentsException이라는 것이다.

클라이언트가 어떤 요청 API를 보냈는데 조건이 일치하지 않는 경우 나타내는 예외가 IllegalArgumentsException이고, 모두 IllegalArgumentsException을 사용하게 된다면 변별력이 떨어지고 오히려 처음 보는 예외 클래스를 사용하는 것보다 더 코드를 이해하기 힘들어질 것이다.

물론 메세지를 읽고 이해할 수 있으나, 어떤 특징을 가진 예외명보다 IllegalArgumentsException이 파악하는 것에 조금이라도 더 시간이 걸릴만하다는 것은 누구라도 인정할 것이고, 이 메세지 또한 상수로 관리하지 않으면 개발자마다, 혹은 심지어 스스로도 비슷한 상황에 매번 다른 메세지를 작성하게 될 우려가 있다.

상황에 맞게 더 직관적으로 준비된 예외 클래스가 코드를 더 읽기 쉽게 만들어준다.

 

세 번째, 서비스 계층에서 예외를 던지는 것에는 일반적인 프로그래밍 상황과 달리 특수한 목적이 있다.

서비스 계층에서 발생하는 예외의 대다수는 클라이언트의 잘못된 요청에 대한 안내의 목적이 가장 주된 것이다.

즉, 얼마나 자세한 정보를 포함하는가보다 간단한 설명과 Status Code 정도를 표현해서 클라이언트에서 그것을 적절하게 처리하도록 돕는 것이 목표라는 것인데, IllealArgumentsException을 어떻게든 살려보겠다고 애쓸 경우 이 간단한 처리가 굉장히 복잡해진다.

예를 들어, 유저가 입력한 아이디 패스워드가 일치하지 않는 경우를 생각해보자. 이 경우 IllegalArgumentsException이 적합하다는 것은 반론의 여지가 없어보인다. 그리고 REST API 관점에서 이는 409 Conflict에 해당한다.

두 번째로, 단순히 유저가 입력한 값이 서버에서 원하는 형태가 아닌 경우를 가정해보자. 실제로는 대부분 스프링에서 정의한 @Vaild 어노테이션을 적용하여 MethodArgumentNotValidException를 던지겠지만, 만약 개발자가 이를 직접 처리하고 자바 표준 예외를 적용한다면 이 또한 IllegalArgumentsException이 적당해 보인다. 그리고 Rest API 관점에서 이는 400 Bad Request에 해당한다.

여기서 문제가 발생하는 것이다. 서비스 계층에서 발생하는 예외는 대다수가 IllegalArgumentsException인데, 클라이언트에게는 상황마다 다른 Status code와 메세지를 반환해주어야 한다.

물론, 상수를 이용해서 충분히 분기 처리할 수 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) {
        if (e.getMessage().equals(ErrorCode.INVALID_USER)) {
            ErrorResponseEntity.toResponseEntity(e.getMessage());
        }
        if (e.getMessage().equals(ErrorCode.EMAIL_EXISTS)) {
            
        }
        if (e.getMessage().equals(ErrorCode.USER_NOT_FOUND)) {
            
        }
        
        ...
    }
cs

 

 

이렇게 말이다.

불공평한 선상에 놓고 비교하지 않으려고 최대한 간단한 형태로 만들었다.

이제 커스텀 예외를 보자.

 

 

1
2
3
4
5
6
7
8
@Getter
public class BusinessLogicException extends RuntimeException {
    private ErrorCode errorCode;
 
    public BusinessLogicException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }
}
cs

 

 

1
2
3
4
5
6
7
@ControllerAdvice
public class ExceptionController {
    @ExceptionHandler(BusinessLogicException.class)
    public ResponseEntity<ErrorResponseEntity> handleBusinessLogicException(BusinessLogicException e) {
        return ErrorResponseEntity.toResponseEntity(e.getErrorCode());
    }
}
cs

 

끝이다.

BusinessLogicException은 간단한 비즈니스 로직 예외에 범용적으로 사용라기 위해 만든 에외로써, 오직 상수인 ErrorCode만을 인자로 가진다.

ErrorCode는 내부적으로 status code와 message를 가지고 있어서 생성 시점에서 해당 예외의 반환 형태는 모두 정해지고 별다른 처리 없이 DTO를 통해 변환하여 클라이언트에게 반환해주면 끝난다.

 

물론 이렇게 대충 처리하면 되겠냐고 거품 물고 싶은 사람도 분명히 있겠지만, 어디까지나 예시라는 사실을 기억해줬으면 한다..

리얼 월드에서는 이렇게까지 대충 처리할 수는 없을 것이고, 인터페이스나 추상클래스를 활용해 BusinessLogicException을 추상화한 뒤 필요에 따라 클래스를 구현해서 사용하는 수고로움이 추가적으로 필요할 것이다.

그럼에도, 그 수고로움으로 얻을 수 있는 유지보수의 편리성이 무시할 수 없는 수준이라는 것을 생각해보자.

 

네번째, 직접 예외를 재정의함으로써 StackTrace를 제거할 수 있다.

StackTrace를 막으면 무엇이 좋은가?

 

https://shipilev.net/blog/2014/exceptional-performance/

 

The Exceptional Performance of Lil' Exception

The post you’re reading is loosely based on two Russian blog posts (1) (2) I wrote a few years ago. Since that time, the JMH microbenchmarking framework has been released, making it possible to render the previous discussion more rigorous and quantitativ

shipilev.net

StackTrace가 무조건 나쁜 것은 아니지만 어디서부터 시작된 것인지가 중요하다.

StackTrace의 depth가 깊을 수록 예외를 던지는 비용은 기하급수적으로 비싸진다.

물론 StackTrace는 우리의 디버깅에 도움을 주는 아주 중요한 존재지만, 3번에서 이야기 했듯이 서비스 계층의 예외는 우리가 디버깅해서 원인을 찾는 것이 주 목적이 아니라 클라이언트에게 정보를 전달하는 것이 주 목적이다.

따라서 비즈니스 계층에서 범용적으로 사용되는 예외들의 경우 stacktrace를 막는 편이 조금이나마 성능 향상에 도움이 될 것이다.

또한, 이렇게 용량을 대폭 줄여놓은 예외의 경우 정말 자주쓰일만한 것들은 캐싱해놓음으로써 예외를 던지는 비용까지 감소시킬 수 있다.

위 글을 읽어보면 알겠지만, 예외를 던지는 비용도 비싸기 때문에 절감할 수 있으면 좋다.

특히나 checked Exception을 catch해서 처리해야하는 경우에도 해당 exception을 적절한 방식으로 처리하면서 stack trace를 끊어내면 도움이 될 것이다.

 

 

 

그러면 비즈니스 로직이 아닌 다른 예외들은 어떻게 해야할까?

이쪽은 사실 깊게 생각해본 적이 없다.. 내가 그런 부분들의 예외까지 커스텀 예외를 만들어가며 처리해야할 만큼 큰 규모의 개발 경험이 없기 때문에.. ㅎ

그냥 집에서 코 긁으며 상상하기로는, 해당 경우에는 자바 표준 예외로 될 것 같기도 하지만.. 서비스가 커지면..?

모르겠다.

 

일단 적어도 오늘의 결론은 "비즈니스 로직 예외는 사용자 정의 예외를 사용하는 것이 좋다."이다.

 

지적해주시면 너무 좋습니다.

끝.