스프링부트 커스텀 예외처리 메서드 추상화로 중복제거하기

jonghyun.log·2023년 8월 10일
0
post-thumbnail
post-custom-banner

문제 : 예외처리 메서드에서 반복이 너무 많다

@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class UserExceptionHandler {
    
    @ExceptionHandler(AlreadyExistUserException.class)
    public ResponseEntity<ExceptionMessage> handle(AlreadyExistUserException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionMessage.of(e.getStatus(), e.getMessage()));
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionMessage> handle(UserNotFoundException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionMessage.of(e.getStatus(), e.getMessage()));
    }

    @ExceptionHandler(UserIllegalStateException.class)
    public ResponseEntity<ExceptionMessage> handle(UserIllegalStateException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionMessage.of(e.getStatus(), e.getMessage()));
    }

    @ExceptionHandler(UserNotAccessRightException.class)
    public ResponseEntity<ExceptionMessage> handle(UserNotAccessRightException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionMessage.of(e.getStatus(), e.getMessage()));
    }

}

서버를 개발하면서 에러가 발생하면 위처럼 각 커스텀 에러 클래스가 터질 때 마다

@RestControllerAdvice 클래스의 handle() 메서드로 잡는 방식으로 처리를 해주었다.

하지만 위처럼 만드니 반복되는 코드가 너무 많아져 고민이 되었고 어떻게 처리하면 좋을지 생각해보게 되었다.

해결 방법

1. 메서드 추출을 통한 에러처리

@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class UserExceptionHandler {

    @ExceptionHandler(AlreadyExistUserException.class)
    public ResponseEntity<ExceptionMessage> handle(AlreadyExistUserException e) {
        return getMessageResponseEntity(e.getStatus(), e.getMessage());
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionMessage> handle(UserNotFoundException e) {
        return getMessageResponseEntity(e.getStatus(), e.getMessage());
    }

    @ExceptionHandler(UserIllegalStateException.class)
    public ResponseEntity<ExceptionMessage> handle(UserIllegalStateException e) {
        return getMessageResponseEntity(e.getStatus(), e.getMessage());
    }

    @ExceptionHandler(UserNotAccessRightException.class)
    public ResponseEntity<ExceptionMessage> handle(UserNotAccessRightException e) {
        return getMessageResponseEntity(e.getStatus(), e.getMessage());
    }
    
    private ResponseEntity<ExceptionMessage> getMessageResponseEntity(StatusEnum e, String e1) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionMessage.of(e, e1));
    }
}

이런식으로 handle() 메서드 내부의 로직을 메서드 추출을 통해 호출하는 방식으로 바꾸어보았다.

하지만 아직도 여전히 여러개의 handle() 메서드가 존재하고 가독성이 좋지 못하다.

2. CustomException 부모 클래스 생성을 통한 HandlerMethod 추상화

2-1. 기존 코드 구조

우선 현재의 코드 구조는

@Getter
public class AlreadyExistUserException extends RuntimeException {

    private final StatusEnum status;

    private static final String message = "삭제된 유저는 조회 할 수 없습니다.";

    public AlreadyExistUserException(StatusEnum status) {
        super(message);
        this.status = status;
    }
}

이렇게 커스텀 에러를 정의하는 구조를 가지고 있고

@Getter
public enum StatusEnum {
    BAD_REQUEST(400, "BAD_REQUEST"),
    VOTE_TYPE_NOT_MATCH(400,"VOTE_TYPE_NOT_MATCH"),
    VOTE_DRINKS_DUPLICATED(400,"VOTE_DRINK_DUPLICATED"),
    VOTE_NOT_FOUND(200, "VOTE_NOT_FOUND"),
    TOKEN_EXPIRED(401, "TOKEN_EXPIRED"),
    USER_NOT_FOUND(404, "USER_NOT_FOUND"),
    COMMENT_NOT_FOUND(404, "COMMENT_NOT_FOUND"),
    ALREADY_VOTE_RESULT_EXIST(403, "ALREADY_VOTE_RESULT_EXIST"),
    TOKEN_NOT_EXIST(404, "TOKEN_NOT_EXIST"),
    ACCESS_RIGHT_FAILED(412, "ACCESS_RIGHT_FAILED");

    private final int statusCode;
    private final String code;

    private StatusEnum(int statusCode, String code) {
        this.statusCode = statusCode;
        this.code = code;
    }
}

각 커스텀 에러에 맞는 상태코드와 메시지를 정의해서 사용하고 있다.

2-2. 기존 코드에 추상화 적용하기

public abstract class CustomException extends RuntimeException{

    public abstract StatusEnum getStatus();

    public abstract String getMessage();

    public CustomException(String message) {
        super(message);
    }
}

각 커스텀 에러 클래스를 추상화 하는 클래스를 정의했다.

public class AlreadyExistUserException extends CustomException {

    private final StatusEnum status;

    private static final String message = "삭제된 유저는 조회 할 수 없습니다.";

    public AlreadyExistUserException(StatusEnum status) {
        super(message);
        this.status = status;
    }

    @Override
    public StatusEnum getStatus() {
        return status;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

그 이후 각 커스텀 에러 클래스들에서 위의 부모 클래스를 상속받아서 메서드를 오버라이드 해주고

@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class UserExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ExceptionMessage> handle(CustomException e) {
        int statusCode = e.getStatus().getStatusCode();
        return ResponseEntity.status(HttpStatus.valueOf(statusCode))
                .body(ExceptionMessage.of(e.getStatus(), e.getMessage()));
    }
}

기존 ExceptionHandler 에서는 그냥 CustomException 을 잡아주기만 하면 자식 예외들이 모두 잡힌다!!

더 나아가 다른 도메인에서 사용하는 예외들도 CustomException을 상속받고 있기만 한다면

한개의 handle() 메서드로 모두 잡을 수 있게 되었다.

혹은 RunTimeException 말고 다른 기본 자바 내장 예외를 상속해야한다면 그 예외를 상속받은 공통인터페이스를 정의하고 그것을 잡는 handle() 메서드 한개만 정의해서 사용하면 된다.
한마디로 코드의 중복을 엄청나게 줄였다~!! 야호 😄

추가로) HttpStatus.valueOf() 메서드를 통해 각 에러에 맞는 HttpStatus 상태 객체도 가져와서 반환해주었다.

배운 점

부모 클래스를 handler에서 잡기만 하면 자식들 모두가 다형성으로 처리하는것을 개념적으로만 알다가
직접 적용해보니 역시 아는것과 체화는 다르다는것을 느꼈고
추상화를 통해 중복되는 코드의 양을 대폭 줄이는 것에 성공해서 기분이 좋다.
역시 이맛에 개발하지 🥳

post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

잘 봤습니다. 좋은 글 감사합니다.

답글 달기