java 의 enum 객체를 활용하여 spring boot의 프로젝트에 global exception handler 적용시키기
public enum ExceptionCode { // 예외 발생시, body에 실어 날려줄 상태, code, message 커스텀
POST_NOT_FOUND(404, "POST_001", "해당되는 id 의 글을 찾을 수 없습니다."),
REPLY_NOT_FOUND(404, "REPLY_001", "해당되는 id의 댓글을 찾을 수 없습니다."),
ALREADY_LIKED(400, "LIKE_001", "이미 '좋아요'를 누른 상태입니다."),
USER_NOT_FOUND(404, "USER_004", "해당 유저를 찾을 수 없습니다."),
TOKEN_NOT_VALID(401, "TOKEN_001", "토큰이 만료되었습니다. 다시 로그인 해주세요."),
USER_CAN_NOT_BE_NULL(400, "USER_005", "사용자는 null이 될 수 없습니다."),
USER_ID_NOT_FOUND(404, "USER_006", "해당되는 id의 사용자를 찾을 수 없습니다."),
NULL_POINT_ERROR(404, "G010", "NullPointerException 발생"),
// @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
NOT_VALID_ERROR(404, "G011", "Validation Exception 발생");
// 1. status = 날려줄 상태코드
// 2. code = 해당 오류가 어느부분과 관련있는지 카테고리화 해주는 코드. 예외 원인 식별하기 편하기에 추가
// 3. message = 발생한 예외에 대한 설명.
private final int status;
private final String code;
private final String message;
ExceptionCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
public int getStatus() {
return status;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
status), 오류 코드(code), 그리고 오류 메시지(message)가 포함되어있다. 이렇게 하면 프로그램 전체에서 일관된 방식으로 예외를 처리할 수 있으며, 특정 예외가 발생했을 때 필요한 모든 정보를 한 곳에서 관리할 수 있다.package com.spring.exception;
public class BusinessException extends RuntimeException{
// runtimeException 발생시 날려줄 메시지들을 enum에 작성해놓고,
// CustomException(현재 클래스) 에서 enum 객체를 생성
private final ExceptionCode exceptionCode;
public BusinessException(ExceptionCode exceptionCode){
this.exceptionCode = exceptionCode;
}
public ExceptionCode getExceptionCode() {
return exceptionCode;
}
}
ExceptionCode 인스턴스를 받아서 초기화하며, 이 값을 기반으로 HTTP 상태 코드와 오류 메시지 등 필요한 정보를 제공한다.이런 식으로 구현하면 다음과 같은 장점이 있다
ExceptionCode enum에 중앙 집중식으로 관리되므로, 나중에 메시지를 변경하거나 새로운 종류의 예외를 추가하는 것이 편리하다.이런 구조는 RESTful API 서버 개발 시 많이 사용되는 패턴 중 하나다. 서버 애플리케이션은 다양한 종류의 오류 상황을 잘 처리해야 하는데, 위와 같은 방법으로 구현하면 이 작업을 체계적이고 효율적으로 할 수 있다는 점에서 유용하다.
package com.spring.exception.globalHandler;
import com.spring.exception.ExceptionCode;
import lombok.*;
import org.springframework.validation.BindingResult;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// globalExceptionHandler(전역예외처리)에서 예외 발생 처리시 보내줄 응답 객체(status, code, message)
public class ErrorResponse {
private int status;
private String code;
private String message;
private List<FieldError> errors; // 상세 에러 메시지
private String reason; // 에러 이유
@Builder
public ErrorResponse(final ExceptionCode code) {
this.status = code.getStatus();
this.code = code.getCode();
this.message = code.getMessage();
this.errors = new ArrayList<>();
}
@Builder
protected ErrorResponse(final ExceptionCode code, final String reason) {
this.message = code.getMessage();
this.status = code.getStatus();
this.code = code.getCode();
this.reason = reason;
}
@Builder
protected ErrorResponse(final ExceptionCode code, final List<FieldError> errors) {
this.status = code.getStatus();
this.code = code.getCode();
this.message = message; // 주어진 메시지를 사용합니다.
this.errors = errors;
}
public static ErrorResponse of(final ExceptionCode code, final BindingResult bindingResult) {
return new ErrorResponse(code, FieldError.of(bindingResult));
}
@Builder
public static ErrorResponse of(final ExceptionCode code) {
return new ErrorResponse(code);
}
@Builder
public static ErrorResponse of(final ExceptionCode code, final String reason){
return new ErrorResponse(code, reason);
}
public int getStatus(){
return status;
}
public void setStatus(int status){
this.status = status;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
/**
* 에러를 e.getBindingResult() 형태로 전달 받는 경우 해당 내용을 상세 내용으로 변경하는 기능을 수행한다.
*/
@Getter
public static class FieldError {
private final String field;
private final String value;
private final String reason;
public static List<FieldError> of(final String field, final String value, final String reason) {
List<FieldError> fieldErrors = new ArrayList<>();
fieldErrors.add(new FieldError(field, value, reason));
return fieldErrors;
}
private static List<FieldError> of(final 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());
}
@Builder
FieldError(String field, String value, String reason) {
this.field = field;
this.value = value;
this.reason = reason;
}
}
}
이 클래스는 클라이언트에게 반환할 오류 응답의 형식을 정의한다.
@Slf4j // Lombok라이브러리의 다양한 logging시스템을 쓸 수 있음(예 : log.debug(), log.info(), log.error())
@RestControllerAdvice // 비동기식 controller의 예외처리 관리시 붙이는 어노테이션
public class GlobalExceptionHandler {
private final HttpStatus HTTP_STATUS_OK = HttpStatus.OK;
//비즈니스 로직의 예외처리(Unchecked Exception 발생시 처리)
@ExceptionHandler(BusinessException.class) // 만들어준 커스텀익셉션 발생시 처리해주는 곳
public ResponseEntity<ErrorResponse> handleCustomException(BusinessException ex) {
log.error("Business Exception Error", ex);
final ErrorResponse errorResponse = ErrorResponse.of(ex.getExceptionCode(), ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));
}
//여기부턴 클라이언트 측의 잘못된 요청에 의한 에러를 처리해줌.
@ExceptionHandler(NullPointerException.class) // nullPointerExceptiono발생시
protected ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e) {
log.error("handleNullPointerException", e);
final ErrorResponse response = ErrorResponse.of(ExceptionCode.NULL_POINT_ERROR, e.getMessage());
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
// @valid 유효성 검증에 실패했을 경우 발생하는 예외 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
log.error("handleMethodArgumentNotValidException", ex);
BindingResult bindingResult = ex.getBindingResult();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
stringBuilder.append(fieldError.getField()).append(":");
stringBuilder.append(fieldError.getDefaultMessage());
stringBuilder.append(", ");
}
final ErrorResponse response = ErrorResponse.of(ExceptionCode.NOT_VALID_ERROR, String.valueOf(stringBuilder));
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
}
이 클래스는 애플리케이션 전체에서 발생하는 예외를 처리하는 역할을 한다.
• @RestControllerAdvice: 이 어노테이션이 붙은 클래스는 모든 컨트롤러가 실행되는 동안 발생할 수 있는 예외를 잡아서 처리한다.
• @ExceptionHandler(CustomException.class): 이 어노테이션은 해당 메소드가 CustomException 타입의 예외를 처리하도록 지정한다.
• handleIdNotFoundException(): 이 메소드는 CustomException이 발생했을 때 호출된다. 인자로 받은 예외 객체로부터 상태 코드, 오류 코드, 오류 메시지를 가져와서 ErrorResponse 객체를 생성하고, 이 객체와 함께 HTTP 응답을 생성하여 반환한다.
따라서 위의 구조로 구현하면 애플리케이션 전체에서 일관된 방식으로 에러 응답을 제공할 수 있다. 또한 모든 컨트롤러나 서비스 로직에서 개별적으로 예외 처리 로직을 작성하지 않아도 되므로 유지보수성도 향상된다.