Backend/개발

[Spring Boot] GlobalExceptionHandler

지수쓰 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 전략 -> 다시 확인