-
Spring의 예외처리(5) - ErrorCode를 직접 정의해보자!Springboot/study 2023. 12. 1. 14:28
✍️ 들어가며
카카오 Open API를 보면 Error Response 스펙에 자체적으로 정의한 code를 사용하는 것을 확인할 수 있습니다.
특히, 기존의 HTTP Code 와는 구분하기 위해 음수로 정의하고 있습니다.
- Kakao API 레퍼런스 : https://developers.kakao.com/docs/latest/ko/rest-api/reference
개발자가 Kakao Open API를 사용하며 발생한 오류를 이해하기 위해서는 문서를 봐야하는 번거로움이 있을 것입니다. 그럼에도 불구하고 카카오는 왜 직접 정의한 Error Code 사용하는 것일까요?
제가 진행했던 프로젝트를 예시로 포스팅을 진행하겠습니다.
🎧 omoinotake - 幾億光年
https://youtu.be/P7bVX6fJfCg?si=Y3g8WMqUfUp2RRID
🤔 왜 직접 ErrorCode를 적용해야 할까?
기존의 에러 응답 코드
{ "status": 404, "error": "NOT_FOUND", // "message" : "회원 정보가 존재하지 않는 경우" "message": "동화 정보가 존재하지 않는 경우" }
초기 개발 과정에서는 회원 정보가 존재하지 않을 때, 동화가 존재하지 않을 때 모두 404(Not Found)를 내려주었습니다.
클라이언트단에서는 어떤 예외인지에 따라 다르게 처리하는 로직이 필요하지만, 같은 Code를 내려주다 보니 자세한 예외 내용은 함께 내려주는 Error Message를 참조해야 했습니다.
클라이언트 단에서 Message를 이용해서 예외 처리하는 것은 클라이언트 단과 서버 단이 불필요하게 결속됨을 의미하고 추후 변경 및 유지보수가 어려울 수 있습니다.
여기서 서비스에서 직접 정의하고 약속한 Code가 필요함을 느꼈습니다.
새로운 에러 응답 코드
{ "status": 404, "error": "NOT_FOUND", "code": "T-001", "message": "동화 정보가 존재하지 않는 경우" }
다음과 같이 동화 정보가 존재하지 않는 경우 사용자 지정 에러 코드를 추가해 응답을 제공합니다.
다만 서비스 내부 Error Code는 HTTP StatusCode와 절대 혼동되어서는 안됩니다. 따라서, "T-001"과 같은 문자열 코드를 추가해 내려주었습니다. (Kakao에서는 이러한 이유로 ErrorCode를 음수로 사용하는 방식을 사용하는 것입니다.)
✍️ Custom ErrorCode 적용기
ErrorResponse
@Getter @Builder public class ErrorResponse { private final int status; private final String error; private final String code; private final String message; public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) { return ResponseEntity .status(errorCode.getStatus()) .body(ErrorResponse.builder() .status(errorCode.getStatus().value()) .error(errorCode.getStatus().name()) .code(errorCode.getCode()) .message(errorCode.getMessage()) .build()); } }
예외가 발생했을 때 내려줄 ErrorResponse을 정의합니다.
ResponseEntity에 ErrorResponse 담아 출력하기 위해 static 메서드를 활용한 toResponseEntity()를 만들었습니다. 사용자정의 에러코드를 위해 만든 ErrorCode를 매개 변수로 받습니다.
두번째 toResponseEntity()는 Exception 까지 받아 처리하는데, 이유는 아래에서 설명하겠습니다.
ErrorCode
@AllArgsConstructor @Getter public enum ErrorCode { // Tale TALE_NOT_FOUND(TaleCode.NOT_FOUND.getCode(), NOT_FOUND, "존재하지 않는 동화"), TALE_NOT_CREATED(TaleCode.NON_CREATED.getCode(), NOT_FOUND, "생성된 동화가 없는 경우"), // Common INVALID_REQUEST_PARAMETER(CommonCode.REQUEST_PARAMETER.getCode(), BAD_REQUEST, "잘못된 요청 형식"), INVALID_JSON_TYPE(CommonCode.JSON_TYPE.getCode(), BAD_REQUEST, "JSON을 파싱할 수 없는 경우"), ... private final String code; private final HttpStatus status; private final String message; }
ErrorResponse로 전달할 ErrorCode를 정의합니다. 각 Attribute는 아래의 역할을 수행합니다.
- code: Payload로 반환할 서비스에서 정의한 에러 코드
- status: Header 및 Payload로 반환할 HTTP Status
- message: 에러 코드 문서화를 위한 설명
추가로 code 에는 exception을 각 도메인에서 쓰이는 예외별로 따로 관리하기 위해 TaleCode, CommonCode 등과 같이 분리하였습니다. 패키지 구조와 코드는 아래와 같습니다.
@Getter @RequiredArgsConstructor public enum TaleCode { NOT_FOUND("T-001"), KEYWORD_COUNT_LIMIT("T-002"), ... ; private final String code; }
RestControllerAdvice
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 비즈니스 예외 처리시 발생 @ExceptionHandler(BusinessException.class) protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { return ErrorResponse.toResponseEntity(e.getErrorCode()); } // 지원하지 않은 HTTP Method 호출 할 경우 발생 @ExceptionHandler(HttpRequestMethodNotSupportedException.class) protected ResponseEntity<ErrorResponse> requestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("handleHttpRequestMethodNotSupportedException", e); return ErrorResponse.toResponseEntity(ErrorCode.INVALID_METHOD_TYPE); } ... // 나머지 에러 여기서 처리 @ExceptionHandler(Exception.class) protected ResponseEntity<ErrorResponse> handleException(Exception e) { log.error("handleEntityNotFoundException", e); return ErrorResponse.toResponseEntity(ErrorCode.SERVICE_UNAVAILABLE); } }
전역 예외처리를 위한 GlobalExecptionHandler를 등록합니다.
여기서 표준 예외처리, 그리고 Spring 내부에 정의된 각 표준 예외마다의 일관된 처리를 위해 각 Exception들을 위에서 정의해놓은 ErrorCode를 이용한 핸들러 메서드들을 생성합니다.
- MethodArgumentTypeMismatchException
- HttpRequestMethodNotSupportedException
- BindException
- ...
맨 마지막에 정의되어 있는 @ExceptionHandler(Exception.class) 어노테이션이 붙은 메서드를 봅시다.
이 메서드는 발생한 예외가 다른 핸들링 메서드를 전부 타지 못했을 때 최종적으로 처리할 수 있게합니다.
클라이언트 단에선 ErrorCode의 SERVICE_UNAVAILABLE에 저장되어 있는 정보로 받을 것이며, 이를 서버 단에 알려주면 서버 단에서는 log.error()으로 로깅된 잡지 못한 예외를 확인할 수 있게 설계하였습니다.
앞선 포스팅에서도 언급하였듯이 해당 핸들러를 통해 예외처리를 하고자 하는 사용자 정의 익셉션이 BusinessException을 상속한다면 일관된 예외처리를 할 수 있을 것입니다.
@Getter public class BusinessException extends RuntimeException { private final ErrorCode errorCode; public BusinessException(ErrorCode errorCode) { this.errorCode = errorCode; } }
이와 같은 설계를 통해 다음과 같은 에러 메세지를 받을 수 있게됐습니다.😀
{ "status": 404, "error": "NOT_FOUND", "code": "T-001", "message": "동화 정보가 존재하지 않는 경우" }
📜 에러 코드 문서화
직접 Notion으로 API 문서를 만들어 관리하였습니다.
🧑💻 마무리
예외 처리 로직이 깔끔해졌습니다.
예외 처리가 필요한 상황에는 BusinessException 또는 그 하위 요소를 상속한 Exception을 정의해서 터트려주면 GlobalExceptionHandler가 Spec에 맞게 처리하여 클라이언트 단에 내려주게 됐습니다.
추후 Spring RestDocs를 적용하여 관리하는 것이 저의 목표입니다..🫡
참고자료
https://jaehoney.tistory.com/240
Spring - REST API에서 직접 정의한 Error code를 사용하는 이유!
네이버나 카카오 Open API를 보면 Error Response 스펙에 서비스가 자체적으로 정의한 code를 사용한다. API 레퍼런스 kakao: https://developers.kakao.com/docs/latest/ko/reference/rest-api-reference#response naver: https://develop
jaehoney.tistory.com
https://jaehoney.tistory.com/275
Spring - Exception 처리 전략 적용기 (+ 에러 코드 문서화!)
요즘에 익셉션에 대한 처리에 관심이 많아져서 관련 포스팅을 썼었다. REST에서 예외를 처리하는 다양한 방법! REST API에서 직접 정의한 Error code를 사용해야 하는 이유! 이번에 공부한 내용들과 추
jaehoney.tistory.com
'Springboot > study' 카테고리의 다른 글
Spring의 예외처리(4) - 표준 예외 vs 사용자 정의 예외 (0) 2023.11.30 Spring의 예외처리(3) - Spring 전역에서 예외 처리해보자 (0) 2023.11.30 Spring의 예외처리(2) - Spring의 예외처리 흐름과 다양한 예외처리 방식 (1) 2023.11.30 Spring의 예외처리(1) - 예외 종류 (0) 2023.11.30