Spring & Spring boot

[Spring Boot] 예외 처리 - 3. CustomException 사용하기

jh4dev 2024. 8. 16. 13:13
<목차>

1. Custom Exception
2. 생성 방법
3. 테스트

 

[Custom Exception]

Exception 을 커스텀하여 사용하는 것에 대해 다양한 의견이 있다.

대부분의 상황에 대해 표준 예외로 처리할 수 있는 부분이긴 하지만, 실제 프로젝트를 경험하며 커스텀 Exception 객체를 사용하지 않은 적은 없었다.

경험 상 가장 큰 이유는, 에러 메시지를 처리하고 관리하는 방법에 대해, 고객사의 의견이 제각각이기 때문이다.

DB 관리, 상수 관리, properties 파일 관리 등 각 요구사항에 맞추다보면 Exception 객체를 커스텀하는 것은 거의 필수였다.

또한, 개발 단계에서 발생하는 예외들을 개발자가 직접 관리하고 처리하기 수월해지기 때문에 실무에서 사용하는 것이 좋다고 본다.

 

CustomException 객체의 생성 방법은 개발자에 따라 다양하다.

위에서 얘기했다시피, 경험상 에러 메시지를 처리하는 내용이 주를 이루었기 때문에, 메시지 관리하는 측면에서 사용하는 방법에 대해 적어본다.

 

Exception 을 커스텀하기에 앞서 근본적으로 이해하고 있어야 하는 부분이 있다.

 

Exception 상속 구조

 

Exception 객체와, 그 부모인 Throwable 의 구조를 이해하고 있어야 커스텀 Exception 객체를 다루기 쉽다.

먼저 Exception 객체를 살펴보자. (주석은 제거하였다.)

public class Exception extends Throwable {
    static final long serialVersionUID = -3387516993124229948L;

    public Exception() {
        super();
    }

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

    public Exception(String message, Throwable cause) {
        super(message, cause);
    }

    public Exception(Throwable cause) {
        super(cause);
    }

  
    protected Exception(String message, Throwable cause,
                        boolean enableSuppression,
                        boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
  • Throwable 클래스를 상속받고 있다.
  • 8번 라인 ~ 10번 라인 / 12번 라인 ~ 14번 라인
    생성자 메서드에서 String 타입의 메시지 문자열을 받고 있으며, 이 생성자는 부모인 Throwable 클래스의 생성자를 호출하고 있다.

그렇다면, Throwable 을 살펴보자.

Exception 에서 호출하는 Throwable 의 생성자 메서드들이다.

public class Throwable implements Serializable {

    private transient Object backtrace;

    private String detailMessage;

    ... 생략 ...

    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }

    ... 생략 ...

    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }

    ... 생략 ...

    public String getMessage() {
        return detailMessage;
    }

    public String getLocalizedMessage() {
        return getMessage();
    }

    ... 생략 ...
}
  • Exception 으로부터 전달받은 message 변수의 값을 Throwable 의 detailMessage 변수로 전달받고 있다.
  • 따라서, 커스텀할 Exception 객체도, 이 Throwable 의 detailMessage 를 Set 하는 것을 목적으로 가진다.

[생성 방법]

클라이언트에게 에러 메시지를 전달하는 것을 목적으로, 커스텀 Exception 객체를 아래와 같이 구성해보겠다.

 

  • 에러 타입 (HttpStatus 의 reasonPhrase)
  • 에러 코드 (HttpStatus 의 코드 value)
  • 에러 메시지 - 도메인 정보 포함

1. 도메인 Enum 생성

<Constants>

    public class Constants {

        public enum ExceptionClass {
            PRODUCT("Product");

            private String exceptionClass;

            ExceptionClass(String exceptionClass) {
                this.exceptionClass = exceptionClass;
            }

            public String getExceptionClass() {
                return exceptionClass;
            }

            @Override
            public String toString() {
                return "[" + getExceptionClass() + "Exception] ";
            }
        }
    }

2. CustomException 생성

<CustomException>

    public class CustomException extends Exception {

        private Constants.ExceptionClass exceptionClass;
        private HttpStatus httpStatus;

        public CustomException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus, String message) {
            super(exceptionClass.toString() + message);
            this.exceptionClass = exceptionClass;
            this.httpStatus = httpStatus;
        }

        public Constants.ExceptionClass getExceptionClass() {
            return exceptionClass;
        }

        public int getHttpStatusCode() {
            return httpStatus.value();
        }

        public String getHttpStatusType() {
            return httpStatus.getReasonPhrase();
        }

        public HttpStatus getHttpStatus() {
            return httpStatus;
        }
    }

3. CustomException 을 처리하는 Exception Handler 메서드 생성

<CustomExceptionHandler> customExceptionHandler()

    @ExceptionHandler(value = CustomException.class)
    public ResponseEntity<Map<String, String>> customExceptionHandler(CustomException e, HttpServletRequest request){

        log.error("Advice 내 customExceptionHandler() 호출, {}, {}", request.getRequestURI(), e.getMessage());

        Map<String, String> responseMap = new HashMap<>();
        responseMap.put("error_type", e.getHttpStatusType());
        responseMap.put("code", String.valueOf(e.getHttpStatusCode()));
        responseMap.put("message", e.getMessage());

        return ResponseEntity.status(e.getHttpStatus()).body(responseMap);
    }

[테스트]

CustomException 이 발생하는 컨트롤러를 생성하여 테스트를 진행해보자.

    @GetMapping("/custom")
    public void customExceptionController() throws CustomException {
            throw new CustomException(Constants.ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST, "Custom Exception Controller Test");
    }

{
  "code": "400",
  "error_type": "Bad Request",
  "message": "[ProductException] Custom Exception Controller Test"
}

 

실무에서 REST API 를 제공하는 상황이라면, HTTP STATUS 는 200 으로 고정하여 리턴하기도 한다.