본문 바로가기

Projects/Monkey Penthouse

에러 표준화 과정 2 - Custom Exception, Exception Handler

Custom Exception

Custom Exception에 대해서 구글링을 좀 해보니 사용 여부에 대해서 회의적인 사람들이 많아 보였다.

나의 프로젝트에 Custom Exception을 써야 할지에 대해서 고민하는 데에는 다음 글이 도움이 되었다.

custom exception을 언제 써야 할까?
 

custom exception을 언제 써야 할까?

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

tecoble.techcourse.co.kr

다음 글을 보면서 Custom Exception을 써봐야 겠다고 생각했는데

그 이유들은 다음과 같다.

  • 표준 Exception을 사용할 경우 어디서 나온 Exception인지 디버깅 없이는 알기 힘듦
  • 이를 해결하기 위해 예상하고 잡아야하는 Exception 종류를 ExpectedException으로 정의하고 이를 모두 상속받게 하면 예상되는 Exception에 한해서는 일괄 처리가 가능해짐
  • 해당 상황에 대한 에러 코드를 저장하여 공통적인 상황에 대한 에러 코드를 일관적으로 관리할 수 있음
  • field를 이용하여 에러와 관련된 정보를 저장할 수 있음

 

그래도 계속 Custom Exception이 상속 관계로 기하급수적으로 Exception이 늘어나는 것을 방지하기 위해서 꼭 필요한 것들만 정의하려고 했다. 

 

따라서 요청들에 대해서 가능한 예외사항을 모두 기록하고 거기서 공통적으로 보여지는 예외 케이스에러 관련 정보를 응답으로 보내줘야하는 상황, 그리고 상황을 대표할 표준 Exception이 존재하지 않는 케이스에 대해서만 Custom Exception을 정의했다. 

 

그리고 다음과 같이 Exception 클래스들의 상속 관계를 정의했다. 

 

  • 모든 Custom Exception은 ExpectedException을 상속 받아 statusCode를 관리하도록 했다.
  • AuthFailedException은 인증/인가 관련 에러로서 클라이언트에서 인증을 시도한 유저 정보를 담은 user 객체를 담을 수 있도록 했다.
  • SocialLoginFailedException은 소셜 로그인 실패 에러로 로그인을 시도한 소셜 플랫폼 정보를 loginType에 담을 수 있도록 했다.
  • LifeStyleTestNeededException은 테스트 미완료 유저일 시 존재하는 유저는 맞지만 테스트부터 실행하도록 유도하기 위해 별도의 403 에러 코드를 담았다.
  • DataNotFoundException은 404 에러 코드를 갖고 있으며, 클라이언트에서 요청했지만 DB에는 없는 타겟 정보를 target에 저장할 수 있도록 했다.
  • LocalLoginFailedException이메일이나 비밀번호 둘 중 무엇이 잘못되었는지 메세지로 보고하기 위해 정의해놓았다.

이렇게 정의해놓은 후, 서비스에서 예외상황이 발생할 수 있는 곳에 적절히 Exception을 던졌다.


Exception Handler

Exception Handler의 필요성

  • try/catch 문의 작성으로 코드의 가독성이 떨어짐 → try/catch 문 없이 오류 처리를 할 수 있음
  • 동작과 오류 처리의 로직이 결합되어있다. → 이 로직을 분리시킴으로써 동작만 고려하여 코드를 작성할 수 있다.
  • 같은 형태의 오류에 대해서 매번 같은 코드를 작성해야한다. -> 같은 형태의 오류가 다양한 요청에서 발생해도 이를 하나의 Handler로 처리할 수가 있다.

거기다가 Custom Exception을 상속 관계로 연결하여 정의해놓았으니, 부모 Exception을 처리함으로써 각각 다른 종류의 자식 Exception을 한번에 처리할 수가 있다.

 

일단 이미 정의된 모든 Controller에 대해 전역으로 Exception을 처리하는 Controller를 정의할 때에는 @RequiredArgsConstructor를 붙여주면 된다. 

 

그리고 처리해줄 각각의 오류에 대해서 메소드를 다음과 같이 정의하고 @ExceptionHandler({ [원하는 Exception].class })를 붙이면 모든 Controller에서 발생할 Exception이 catch되어 이 메소드의 파라미터로 전달된다.

    // 예상가능한 Exception 처리
    @ExceptionHandler(ExpectedException.class)
    public ResponseEntity<DefaultRes<?>> handleExpectedException(ExpectedException e) {
        return new ResponseEntity<>(
                DefaultRes.res(
                        e.getStatusCode().value(),
                        e.getMessage()),
                e.getStatusCode()
        );
    }

이렇게 메소드를 여러개 정의하면 정의된 순서대로 catch된 Exception이 처리되기 때문에, 구체적인 Exception을 처리할 메소드부터 처음에 정의하고, 가장 마지막에는 예상 불가능한 모든 Exception에 대하여 500 에러 응답을 보내는 메소드를 정의하면 된다. 

    // 500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<DefaultRes<?>> handleAll(final Exception e) {
        System.out.println("e = " + e);
        return new ResponseEntity<>(
                DefaultRes.res(
                        HttpStatus.INTERNAL_SERVER_ERROR.value(),
                        "서버의 내부적인 오류로 인해 문제가 발생하였습니다."),
                HttpStatus.INTERNAL_SERVER_ERROR
        );
    }