220111 CustomException 리팩토링

GuruneLee·2022년 1월 11일
1

GIST청원사이트-BE

목록 보기
9/11

관련된 포스트
[Java] Exception 분류 및 처리방법
[Spring] 사용자 정의 예외 활용하기

문제 상황

기존의 예외처리 방식에선 모든 예외 발생 사항을 직접 만든 CustomException 클래스에 담아 보낸다. 모든 예외응답의 http status는 BAD_REQUEST(400)를 담고, 모든 메시지는 개발자가 각 예외 인스턴스마다 직접 쓴다.
이렇다보니 예외 분류가 안돼서 예외별로 처리해주는 것이 까다로워 졌다. 현재는 예외를 받으면 단순히 response body 에 담아서 응답하는 것으로 구현되어 있다. 분류를 위해 Enum 클래스로 ErrorMessage 를 선언하였지만, 이를 잘 사용하지 않고 있다.

해결

CustomException 을 잘 분류해 계층화해서 가독성과 사용성을 높여보자.

예외 분류하기

먼저, 우리 서버에서 뱉는 모든 예외를 모아놓고 보니 메시지만 따로모아서 도메인 별로 나눌 수 있었다.

  • ~ 할 권한이 없습니다.
  • 존재하지 않는 ~ 입니다.
  • 중복되는 ~ 입니다.
    등등

도메인별로 정리해 보니 다음과 같았다.

post 도메인
├ 존재하지 않는 청원입니다.
├ 이미 답변이 존재합니다.
├ 이미 동의하셨습니다.
└ 답변이 존재하지 않습니다.

user 도메인
├ 존재하지 않는 회원입니다.
├ 이메일 인증이 필요합니다.
├ 인증되지 않은 사용자입니다.
├ 권한이 없습니다.
├ 이미 존재하는 회원입니다.
├ 이메일 형식이 올바르지 않습니다.
└ 비밀번호가 맞지 않습니다.

comment 도메인
└ 존재하지 않는 댓글입니다.

verification 도메인
├ 존재하지 않는 인증 코드 입니다.
├ 인증되지 않은 코드입니다.
├ 유효하지 않은 회원인증 정보입니다.
├ 만료된 인증 코드입니다.
└ 이미 인증된 정보입니다.

분류된 예외 구현

ApplicationException

직접 만든 예외. 자바 혹은 다른 라이브러리에서 제공하는 예외와는 맞지 않아서 직접 정의한 예외.
보통 실행중 예외사항을 상정하므로 RuntimeException 을 상속시켰다.

public class ApplicationException extends RuntimeException {
    private final HttpStatus httpStatus;

    public ApplicationException(String message, HttpStatus httpStatus) {
        super(message);
        this.httpStatus = httpStatus;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

기존에 CustomException 으로 던지던 예외를 도메인 별로 분류해서 ApplicationExcetion을 상속하도록 하였다.

//상속 관계
ApplicationException.java
├── CommentException.java
│   └── NoSuchCommentException.java
├── PostException.java
│   ├── DuplicatedAgreementException.java
│   ├── DuplicatedAnswerException.java
│   ├── NoSuchPostException.java
│   └── UnAnsweredPostException.java
├── UserException.java
│   ├── DuplicatedUserException.java
│   ├── InvalidEmailFormException.java
│   ├── NoSuchUserException.java
│   ├── NotConfirmedEmailException.java
│   ├── NotMatchedPasswordException.java
│   ├── UnAuthenticatedException.java
│   └── UnAuthorizedUserException.java
└── VerificationException.java
    ├── DuplicatedVerificationException.java
    ├── ExpiredVerificationCodeException.java
    ├── InvalidVerificationInfoException.java
    ├── NoSuchVerificationCodeException.java
    ├── NoSuchVerificationInfoException.java
    └── NotConfirmedVerificationCodeException.java

WrappedException

checked exception 을 잡아야 할때 이를 WrappeException 에 넣어서 처리한다.
예외처리회피의 일종.

public class WrappedException extends ApplicationException {

    private final Throwable cause;

    public WrappedException(String message, Throwable cause) {
        this(message, cause, HttpStatus.BAD_REQUEST);
    }

    public WrappedException(String message, Throwable cause, HttpStatus httpStatus) {
        super(message, httpStatus);
        this.cause = cause;
    }

    @Override
    public Throwable getCause() {
        return cause;
    }
}

ControllerAdvice에서 처리하기

예외를 전역 @ExceptionHandler 에서 받아서 메시지와 HttpStatus 를 response body 에 json 형식으로 담아서 보낸다. 이 때 'ErrorResponse' 클래스를 사용한다.

// ErrorResponse.java
public class ErrorResponse {
    private String message;

    public ErrorResponse() {
    }

    public ErrorResponse(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}
// ControllerAdvice.java
@RestControllerAdvice
public class ControllerAdvice {

    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity<ErrorResponse> handle(ApplicationException ex) {
        return ResponseEntity.status(ex.getHttpStatus()).body(new ErrorResponse(ex.getMessage()));
    }

    @ExceptionHandler(WrappedException.class)
    public ResponseEntity<ErrorResponse> handle(WrappedException ex) {
        return ResponseEntity.status(ex.getHttpStatus()).body(new ErrorResponse(ex.getCause().getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> validException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return ResponseEntity.badRequest().body(new ErrorResponse(message));
    }
}

마치며

예외를 다룬다는 것에 이렇게 로드가 크게 들지 몰랐다. 그렇지만, 자세히 분류를 하고 나니 예외처리 상황을 더 다양하게 다룰 수 있게 되었고, 테스트를 못했던 경우들이 잘 보였던 것 같다.
다만, 너무 많은 경우의 수를 하나하나 예외로 만들다보니 만들어놓은 예외들의 재사용성이 떨어짐을 느꼈고, 이를 위해 더 세세한 계층화가 필요할 수 있겠다고 생각했다.

profile
Today, I Shoveled AGAIN....

3개의 댓글

comment-user-thumbnail
2022년 1월 12일

고생많으셨어요 체리천사님! 👍👍

1개의 답글
comment-user-thumbnail
2022년 1월 12일

체리피커네요

답글 달기