-
[Spring Boot] GlobalExceptionHandlerBackend/개발 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를 사용한 에러 핸들링
네이버 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를 받아왔다.controller나 service에서 throw를 던지면 따로 try catch로 받지 않아도 이렇게 오류 메세지가 잘 나온다.
RuntimeException
앞으로 계속 추가적인 에러핸들링을 할 생각인데, @RequestParam으로 명시한 값을 잘 받아오지 못할 때라던가,, 뭔가 생각지 못한 RuntimeException이 생기면
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만 보여지게했다.나한텐 이런식으로 로그가 남는다.
errorcode부분을 잘 이해 못해서 미뤘는데 일단 대략적인 틀은 잡힌것 같다 !! 야호
출처 / 정리하면서 한번씩 읽어본 블로그
SpringController(Exception) 처리
@ControllerAdvice, @ExceptionHandler를 이용한 예외처리 분리, 통합하기(Spring에서 예외 관리하는 방법, 실무에서는 어떻게?)
tecoble- custom exception을 언제 써야 할까?
Spring Guide - Exception 전략 -> 다시 확인
'Backend > 개발' 카테고리의 다른 글
[Spring Boot] 슬라이스테스트, Validation 유효성 검사 (1) 2021.08.28 [Spring Boot] SVG와 PNG , 준우승 은별 표시 (0) 2021.08.19 [Spring Boot] 예외처리, 테스트코드에 관한 고민 (0) 2021.08.13 [Spring Boot] 우승멤버 등록, controller 테스트 코드 작성하기 1 (0) 2021.08.12 [Spring Boot] 우승 멤버 별표표시 , @ElementCollection (2) 2021.08.05