ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] GlobalExceptionHandler
    Backend/개발 2021. 8. 18. 23:30
    반응형

    TIL 40일차 개인프로젝트 

    globalExceptionhandler를 만들어보려고 한다.

     

    Controller를 작성할 때 예외상황을 고려하며 처리해야 하는 작업이 늘어남에 따라, 스프링 MVC에서는 @ExceptionHandler와 @ControllerAdvice를 이용해 처리하고, ResponseEntity를 이용해 예외 메세지를 구성한다.

     

    @ExceptionHandler

    @controller, @RestController가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능을 한다.

     

    Controller 내부에서 호출한 Service에서 예외가 발생하더라도 잡아낸다.

     

    @ControllerAdvice

    AOP를 이용하여 공통적인 예외사항에 별도로 @ControllerAdvice를 이용해서 분리한다.

     

    @ExceptionHandler가 하나의 클래스에 대한 것이라면, @ControllerAdvice는 모든 @Controller, 즉 전역에서 발생할 수 있는 예외를 잡아주는 어노테이션이다.

     

    여기서 @RestControllerAdvice는 @ControllerAdvice와 같은 역할을 수행하며 @ResponseBody를 통해 객체를 리턴할 수도 있다.

     

    @ControllerAdvice는 해당 객체가 스프링 컨트롤러에서 발생하는 예외를 처리하는 존재임을 명시하는 용도로 사용하고, @ExceptionHandler는 해당 메서드가 들어가는 예외 타입을 처리하는 것을의미한다.


    @ExceptionHandler 어노테이션 속성으로 Excepction 클래스 타입을 지정할 수 있다.

     

    Custom Exception :: 사용자 정의 예외

    표준 예외를 적극적으로 사용하자

    커스텀 예외의 이름만 봐도 어떤 예외인지 알아볼 수 있다. 하지만 표준 예외를 사용하고 errorMessage 로 오류 상황을 나타내는 정도로도 충분히 표현이 가능하다면, 굳이 커스텀 예외를 사용하지 않는것이 좋다.

     

    반대로 CustomException을 받는 곳에서 처리하기 쉽기 위해 추가로 에러에 관한 정보를 넣어주거나, 여러 타입의 Exception이 발생할 수 있는 코드에서 한가자로 Exception 타입으로 묶어 처리할때는 custome exception을 사용할만 하다.

     

    표준 예외를 사용하면 가독성이 높아진다.

    • NullPointerException : null을 허용하지 않는 메서드에 null을 건냈을 때
    • IndexOutBoundsException : 범위 밖의 index에 접근할 때
    • IllegalArgumentException : 허용하지 않는 값이 인수로 건네졌을 때
    • IllegalStateException : 객체가 메서드를 수행하기에 적절하지 않은 상태일 때
    • UnsupportedOperationException : 요청받은 작업을 지원하지 않는 경우 일 때



    // errors.mayoException
    package com.project.auction.lol.errors;
    
    import org.springframework.http.HttpStatus;
    
    public class MayoException extends RuntimeException{
    
        private HttpStatus httpStatus;
    
        public MayoException(HttpStatus httpStatus,String message){
            super(message);
            this.httpStatus = httpStatus;
        }
    
        public HttpStatus getHttpStatus(){
            return httpStatus;
        }
    }
    

    RuntimeException에 이미 message를 포함하는 생성자가 있어서 super(message)를 해주고, httpStatus는 따로 생성해주었다.

    // errors.GlobalExceptionHandler
    package com.project.auction.lol.errors;
    
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MayoException.class)
        public ResponseEntity<String> handleException(MayoException e){
            return ResponseEntity
                    .status(e.getHttpStatus().value())
                    .body(e.getMessage());
        }
    }
    

    일단 ControllerAdvice와 ExceptionHandler를 사용하는 방법 정도만 알아봤다.



    [21.08.27] ErrorCode, ErrorResponse를 사용한 에러 핸들링

    스크린샷 2021-08-27 오후 9.22.14

    네이버 Developers의 API 오류코드 형식을 살펴보면 다음과 같이 errorMessage와 errorCode를 나타내준다 (HTTP 상태코드 별도)

    단순히 백엔드에서 에러가 생겼을 때 raw한 값을 보내주는것 보다, 에러 메세지의 형식을 갖춰 응답하는것이 좋고 또한 errorCode는 나같이 토이프로젝트에서는 별로 의미 없을 수 있지만, 기본적인 서비스관련 오류, 인증 오류, 다른 라이브러리 사용 오류 등을 나타내어 하나의 문서화 할 때 백엔드쪽에서든, 클라이언트 쪽에서든 찾아보기 쉬울 것이라고 느껴졌다.

     

    그래서 나도 에러코드도 만들고, 조금 더 깔끔한 에러 메세지로 전역 핸들링을 해보기로 했다!

     

    에러 코드

    @Getter
    @RequiredArgsConstructor
    public enum ErrorCode {
    
        //Common error
        DUPLICATE_SUMMONER_NAME(HttpStatus.CONFLICT,"C001", "DUPLICATE_SUMMONER_NAME"),
        POSITION_NOT_FOUND(HttpStatus.NOT_FOUND,"C002","POSITION_NOT_FOUND"),
        EXIST_TEAM(HttpStatus.CONFLICT,"C003","EXIST_TEAM");
    
        private final HttpStatus status;
        private final String code;
        private final String message;
    }
    

    에러코드는 Enum type으로 만들었다. 이렇게 만들면 컨트롤러나 서비스 단에서

    throw new MayoException(ErrorCode.EXIST_TEAM, "이미 팀이 생성되었습니다.");

    이런식으로 에러를 던질 수 있는데, 자동완성으로 찾기도 쉽고 어떤 에러인지 이름으로 확인할 수 있는 것 같다.

     

    또한 TCP School에 따르면 불규칙한 상수값을 설정할 때 그 값을 저장할 인스턴스 변수와 생성자를 만들어줘야한다고 나와있다. Enum은 class형식을 띄지만 어쨌든 상수이므로 외부에서 생성하거나 변경할 수 없다고 이해했다.

     

    그리고 이후에 errorcode의 값을 가지고 ResponeEntity를 만들어 return하기 위해 @Getter를 사용했다.

    ErrorResponse

    @Getter
    @ToString
    @NoArgsConstructor
    public class ErrorResponse {
        private HttpStatus status;
        private String code;
        private String message;
    
        @Builder
        public ErrorResponse(HttpStatus status, String code, String message) {
            this.status = status;
            this.code = code;
            this.message = message;
        }
    }
    

    다음은 응답값을 ResponseEntity<ErrorResponse> 모양으로 나타내기 위해서 생성한 응답 클래스이다. ErrorCode와 거의 같아 굳이 만들어야 하나 싶기도 했지만 각각의 역할이 있기도 하고, 메서드 return type ResponseEntity<>에 확실한 타입을 지정해주어야 좋다는 글을 봐서,, Object나 String에 Errocode.getMessage() 와 같이 만들고 싶지 않았다 !!

     

    이렇게 구구절절 적는 이유는 다른 블로그에 보면 너무나 당연한듯이 만드는데 왜 만드는지 이해하고 넘어가려고 해도 와닿지 않아서 내맘대로 이유를 만들어봤다 ㅠㅠ

    CustomException

    public class MayoException extends RuntimeException{
    
        private final ErrorCode code;
    
        public MayoException(ErrorCode code, String message){
            super(code.getMessage() + ": "+message);
            this.code = code ;
        }
        public MayoException(ErrorCode code){
            super(code.getMessage());
            this.code = code;
        }
        @Override
        public String getMessage() {
            return super.getMessage();
        }
    
        public ErrorCode getCode(){
            return this.code;
        }
    }
    

    앞서 작성한 CustomException을 errorcode를 사용해서 변경해주었다.

     

    code만 명시해서 exception을 날릴 때와, 추가적으로 덧붙일 말이 있을 때를 대비해 message도 포함한 생성자를 만들었다. 그리고 super()부분에서 변경하더라도 getMessage()를 Override하지않으면 나타나지 않으니 유의하자.

    GlobalExceptionHandler

    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = MayoException.class)
        public ResponseEntity<ErrorResponse> handleMayoException( MayoException e){
            ErrorResponse errorResponse = ErrorResponse.builder()
                    .code(e.getCode().getCode())
                    .message(e.getMessage())
                    .status(e.getCode().getStatus())
                    .build();
            log.error(errorResponse.toString() );
            return ResponseEntity.status(e.getCode().getStatus())
                    .body(errorResponse);
        }
    }
    

    CustomExcetpion에 대한 ExceptionHandler를 변경해주었다.

    return 타입을 ResponseEntity<ErrorResponse>로 했고, .message(e.getMessage())부분에는 추가적으로 설정한 message도 나오게 하려고 code가 아닌 Exception값에서 message를 받아왔다.

     

    스크린샷 2021-08-27 오후 10.00.18스크린샷 2021-08-27 오후 10.00.55

    controller나 service에서 throw를 던지면 따로 try catch로 받지 않아도 이렇게 오류 메세지가 잘 나온다.



    RuntimeException

    앞으로 계속 추가적인 에러핸들링을 할 생각인데, @RequestParam으로 명시한 값을 잘 받아오지 못할 때라던가,, 뭔가 생각지 못한 RuntimeException이 생기면

    스크린샷 2021-08-27 오후 10.03.19

    400,500 이런식으로 그냥 보이는데 최대한 내가 할수있는 부분에 대해서는 핸들링 하고 싶어서

        @ExceptionHandler(value = RuntimeException.class)
        public ResponseEntity<ErrorResponse> handleRuntimeException( RuntimeException e){
    
            /*
            format of e.getCause().getMessage()의 형식이
            detailMessage : com.project.auction.lol.who.throws.exception;
            다음과 같아 프로젝트 내부 구조를 보여주지 않기 위해 parsing하였다.
             */
            String detailMessage = e.getCause().getMessage().split(":")[0];
    
            ErrorResponse errorResponse = ErrorResponse.builder()
                    .message(detailMessage)
                    .status(HttpStatus.BAD_REQUEST)
                    .build();
            log.error(e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(errorResponse);
        }

    RuntimeException에 대해서도 일단 잡아줬다. 나중에는 더 세부적인 Exception을 핸들링해주면 좋을 것 같다.

     

    그리고 e.getMessage()를 하면 프로젝트 내부 구조가 다 보이길래 split으로 detailMessage만 보여지게했다.

     

    스크린샷 2021-08-27 오후 10.07.05스크린샷 2021-08-27 오후 10.07.23

    나한텐 이런식으로 로그가 남는다.



    errorcode부분을 잘 이해 못해서 미뤘는데 일단 대략적인 틀은 잡힌것 같다 !! 야호

    출처 / 정리하면서 한번씩 읽어본 블로그

    Spring Boot - Rest Api 예외 처리

    SpringController(Exception) 처리

    @ControllerAdvice를 이용한 예외 처리

    @ControllerAdvice, @ExceptionHandler를 이용한 예외처리 분리, 통합하기(Spring에서 예외 관리하는 방법, 실무에서는 어떻게?)

    명쾌한 Custom Exception in Java

    tecoble- custom exception을 언제 써야 할까?

    Spring Guide - Exception 전략 -> 다시 확인

    댓글

Designed by Tistory.