Spring Exception Handler & Rest Controller Advice

2023. 4. 3. 07:35·📘 Backend/Spring

📘Exception Handler

예외 발생 가능성이 있는 상황

  • 클라이언트 요청 데이터에 대한 유효성 검증에서 발생하는 예외
  • 서비스 계층의 비즈니스 로직에서 던져지는 의도된 예외
  • 런타임 예외

UserController에 Exception Handler 적용

  • RequestBody에 유효하지않은 데이터 요청이 오면, 유효성 검증에 실패
  • 유효성 검증에 실패시 내부적으로 던져진 예외인 MethoddArgumentNotValidException을 handleException() 이 받음
  • 그 후, MethodArgumentNotValidException 개체에서 getBindingResult(), getFieldErrors() 를 통해,
    발생한 에러 정보를 리스트로 확인 가능
  • 객체에서 얻은 정보인 filedErrors를 ResponseEntity를 통해 ResponseBody로 전달

UserController에 ExceptionHandler 적용

img


HTTP Request를 보내 ExceptionHandler 적용 확인

img

[
    {
        "codes": [
            "Email.userPostDto.email",
            "Email.email",
            "Email.java.lang.String",
            "Email"
        ],
        "arguments": [
            {
                "codes": [
                    "userPostDto.email",
                    "email"
                ],
                "arguments": null,
                "defaultMessage": "email",
                "code": "email"
            },
            [],
            {
                "arguments": null,
                "defaultMessage": ".*",
                "codes": [
                    ".*"
                ]
            }
        ],
        "defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
        "objectName": "userPostDto",
        "field": "email",
        "rejectedValue": "asdad23@",
        "bindingFailure": false,
        "code": "Email"
    }
]

하지만, 반환값에는 알수없는 정보를 전부 포함한 내용은 제외하고 싶다.
ErrorResponse 클래스를 따로 만들어서 원하는 출력정보만 나오게 해보자.

  • 유효성 검증 실패 시, 실패한 필드에대한 Error정보만 담아서 응답으로 전송하기 위한 클래스
  • 위의 Exception 정보를 리턴받은 값을 보면 배열로 되어있다.
    유효성 검증에 실패하는 멤버 변수가 하나 이상이 될 수 있기 때문.
  • 그렇기 때문에, 유효성 검증에 실패한 에러 정보를 담기위해 List객체를 이용하며, 이 한개의 필드 에러 정보는
    Error_Detail 이라는 별도의 static class를 Error클래스의 멤버 클래스로 정의
    Error는 에러 정보만 담는 클래스이기 때문에 필드의 에러 정보를 담는 Error_Detail클래스 역시,
    에러라는 공통 관심사를 가지고 있으므로 Error의 멤버로 표현하는것이 적절하다.

공통 관심사를 처리할 Error 클래스 작성

img


UserController 내부 ExceptonHandler 수정 -> Error 객체를 응답으로 전송

img


원하는 에러 정보만 표시

img


Exception Handler의 단점

@ExceptionHandler Annotation 사용 시 문제점

  • 각각의 컨트롤러에서 @ExceptionHandler를 사용해야 하므로 코드중복 발생
  • 컨트롤러에서 Validation에 대한 예외뿐 아니라 여러가지 예외 처리를 해야하므로 핸들러 메소드의 증가
  • 해결법은? RestControllerAdvice를 사용한 예외 처리 공통화

📘RestControllerAdvice를 사용한 예외 처리 공통화

어떤 클래스에 @RestControllerAdvice를 추가하면 여러개의 컨트롤러에서
@ExceptionHanndler, @InitBinder, @ModelAttribute가 추가된 메소드를 공유해서 사용 가능


ApplyExceptionAdviceAllControllers 클래스 생성하여 공통 처리

img


Throw Exception

img


예외의 구분

  • 어플리케이션의 발생예외는 크게 체크예외, 언체크예외로 구분
  • 체크예외 : 발생한 예외를 잡고(catch) 체크 후, 예외 복구&회피 등 구체적인 처리 필요
    • ex: ClassNotFoundException
  • 언체크예외 : 예외를 잡고(catch) 따로 처리가 필요하지 않음
    • ex: NullPointerException, ArrayIndexOutOfBoundsException
  • 휴먼에러로 인해 발생하는 오류는 전부 RuntimeException을 상속한 예외들임
  • 자바는 많은 RuntimeException을 지원하지만 직접 예외를 만들어야 하는 경우도 있음

의도적으로 예외 throw & catch 하기

  • Custom Exception(enum) 생성
  • 특정 계층에서 사용할 클래스 생성 후 생성자에 CustomException을 파라미터로 사용
  • ExceptionAdvice에 @ExceptionHandler를 사용하는 메소드 추가 작성
  • 예외가 발생할만한 서비스에 throw new Custom_Exception(Custom_enum.Method) 작성하여 throw해주기
  • 던진 예외를 예외처리를 담당하는 클래스에 던진 예외를 잡기위한 @ExceptionHandler 메소드 생성

📘GlobalExceptionAdvice구현


GlobalExceptionAdvice 클래스

처리된 에러를 Global로 한곳에 받아 적절한 응답으로 전달

@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        final ErrorResponse response = ErrorResponse.of(e.getBindingResult());

        return response;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleConstraintViolationException(
            ConstraintViolationException e) {
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }

    @ExceptionHandler
    public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
        final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());

        return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode()
                .getStatus()));
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public ErrorResponse handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e) {

        final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);

        return response;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleHttpMessageNotReadableException(
            HttpMessageNotReadableException e) {

        final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
                "Required request body is missing");

        return response;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMissingServletRequestParameterException(
            MissingServletRequestParameterException e) {

        final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
                e.getMessage());

        return response;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleException(Exception e) {
        log.error("# handle Exception", e);
        /* 
         * TODO 애플리케이션의 에러는 에러 로그를 로그에 기록하고, 관리자에게 이메일이나 카카오 톡,
         * 슬랙 등으로 알려주는 로직이 있는게 좋음.
         */

        final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);

        return response;
    }
}

BusinessLogicException 클래스

적절한 Custom 상태코드인 ExcetionCode 반환

public class BusinessLogicException extends RuntimeException {

    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}

ExceptionCode 클래스

Custom 상태코드

public enum ExceptionCode {

    MEMBER_NOT_FOUND(404, "Member not found"),
    MEMBER_EXISTS(409, "Member exists"),
    COFFEE_NOT_FOUND(404, "Coffee not found"),
    COFFEE_CODE_EXISTS(409, "Coffee Code exists"),
    ORDER_NOT_FOUND(404, "Order not found"),
    CANNOT_CHANGE_ORDER(403, "Order can not change"),
    NOT_IMPLEMENTATION(501, "Not Implementation"),
    INVALID_MEMBER_STATUS(400, "Invalid member status");

    @Getter
    private int status;

    @Getter
    private String message;

    ExceptionCode(int status, String message) {
        this.status = status;
        this.message = message;
    }
}

ErrorResponse 클래스

직접적인 에러 처리 클래스, 용도벌 of 메소드 사용으로 특정 Exception별 에러 처리

@Getter
public class ErrorResponse {
    private int status;
    private String message;
    private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;

    private ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }

    private ErrorResponse(final List<FieldError> fieldErrors,
                          final List<ConstraintViolationError> violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }

    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

    public static ErrorResponse of(ExceptionCode exceptionCode) {
        return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
    }

    public static ErrorResponse of(HttpStatus httpStatus) {
        return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
    }

    public static ErrorResponse of(HttpStatus httpStatus, String message) {
        return new ErrorResponse(httpStatus.value(), message);
    }

    @Getter
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        private FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();

            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

    @Getter
    public static class ConstraintViolationError {
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        private ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<ConstraintViolationError> of(Set<ConstraintViolation<?>> constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -> new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()))
                    .collect(Collectors.toList());
        }
    }
}
저작자표시 (새창열림)

'📘 Backend > Spring' 카테고리의 다른 글

JPA & Hibernate 개념  (0) 2023.04.03
Aggregate & JDBC  (0) 2023.04.03
Service Layer  (0) 2023.04.03
정규표현식 (Regex)  (0) 2023.04.02
Data Transfer Object (DTO)  (0) 2023.04.02
'📘 Backend/Spring' 카테고리의 다른 글
  • JPA & Hibernate 개념
  • Aggregate & JDBC
  • Service Layer
  • 정규표현식 (Regex)
신건우
신건우
조용한 개발자
  • 신건우
    우주먼지
    신건우
  • 전체
    오늘
    어제
    • 분류 전체보기 (422)
      • 📘 Frontend (71)
        • Markup (1)
        • Style Sheet (2)
        • Dart (8)
        • Javascript (12)
        • TypeScript (1)
        • Vue (36)
        • React (2)
        • Flutter (9)
      • 📘 Backend (143)
        • Java (34)
        • Concurrency (19)
        • Reflection (1)
        • Kotlin (29)
        • Python (1)
        • Spring (42)
        • Spring Cloud (5)
        • Message Broker (5)
        • Streaming (2)
        • 기능 개발 (5)
      • 💻 Server (6)
        • Linux (6)
      • ❌ Error Handling (11)
      • 📦 Database (62)
        • SQL (31)
        • NoSQL (2)
        • JPQL (9)
        • QueryDSL (12)
        • Basic (4)
        • Firebase (4)
      • ⚙️ Ops (57)
        • CS (6)
        • AWS (9)
        • Docker (8)
        • Kubernetes (13)
        • MSA (1)
        • CI & CD (20)
      • 📚 Data Architect (48)
        • Data Structure (10)
        • Algorithm (8)
        • Programmers (17)
        • BaekJoon (5)
        • CodeUp (4)
        • Design Pattern (4)
        • AI (0)
      • ⚒️ Management & Tool (8)
        • Git (7)
        • IntelliJ (1)
      • 📄 Document (10)
        • Project 설계 (6)
        • Server Migration (3)
      • 📄 책읽기 (2)
        • 시작하세요! 도커 & 쿠버네티스 (2)
      • 🎮 Game (4)
        • Stardew Vally (1)
        • Path of Exile (3)
  • 블로그 메뉴

    • 링크

      • Github
    • 공지사항

    • 인기 글

    • 태그

      React #Markdown
      GStreamer #Pipeline
      Lock #Thread #Concurrency
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    신건우
    Spring Exception Handler & Rest Controller Advice
    상단으로

    티스토리툴바