TIL

[SpringBoot] 에러 응답 형식 통일

coding-orange 2024. 1. 2. 13:56
728x90

 

요구사항

 

지난번에는 ConstraintViolationException이 발생했을 때 응답을 커스터마이징 했다. 하지만 Exception은 하나의 애플리케이션에서 여러 개가 발생할 수 있다. 이 때 마다 응답 형식이 다르면 일관성이 없어보일 수 있지 않을까? 라는 생각을 했다.

 

그래서 이번에는 Exception에 대한 응답 형식을 만들고, 해당 형식에 맞게 응답을 반환하는 것을 구현하고자 한다.

 

 


 

 

구현

 

먼저, global 패키지의 exception 패키지에 enum 타입인 ErrorCode를 작성한다. ErrorCode에는 세 개의 속성이 있는데, code는 오류 코드를 식별하는 문자열로, message는 응답에 포함되는 오류 메시지, status는 HTTP 응답에서 사용될 상태 코드이다.

package kr.go.data.global.exception;

import lombok.Getter;

@Getter
public enum ErrorCode {

    ID_DUPLICATION("ID_001", "이미 존재하는 아이디입니다.", 400),
    ID_NOT_FOUND("ID_002", "존재하지 않는 아이디입니다.", 404),
    EMAIL_DUPLICATION("EMAIL_001", "이미 존재하는 이메일입니다.", 400),
    EMAIL_NOT_FOUND("EMAIL_002", "존재하지 않는 이메일입니다.", 404),
    INPUT_VALUE_INVALID("INPUT_001", "입력값이 올바르지 않습니다.", 400);

    private final String code;
    private final String message;
    private final int status;

    ErrorCode(String code, String message, int status) {
        this.code = code;
        this.message = message;
        this.status = status;
    }
}

 

그리고 ErrorResponse 클래스를 작성한다. 이 클래스는 응답 형식을 정의하고 있다.

package kr.go.data.global.exception;

import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
    private String message;
    private String code;
    private int status;
    private List<FieldError> errors;

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    public static class FieldError {
        private String field;
        private String value;
        private String reason;
    }
}

 

위와 같이 작성하면, 아래와 같은 형식으로 반환하게 된다.

{
    "message": "",
    "code": "",
    "status": "",
    "errors": [
        {
            "field": "",
            "value": "",
            "reason" : ""
        }
    ]

}

 

이제 Exception을 처리하는 부분인 GlobalExceptionHandler를 수정하면 된다.

package kr.go.data.global.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import java.util.List;
import kr.go.data.global.exception.ErrorResponse.FieldError;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
        List<ErrorResponse.FieldError> fieldErrors = ex.getConstraintViolations().stream()
                .map(violation -> new ErrorResponse.FieldError(violation.getPropertyPath().toString(), violation.getInvalidValue().toString(), violation.getMessage()))
                .collect(Collectors.toList());

        ErrorResponse errorResponse = new ErrorResponse(
                ErrorCode.INPUT_VALUE_INVALID.getMessage(),
                ErrorCode.INPUT_VALUE_INVALID.getCode(),
                ErrorCode.INPUT_VALUE_INVALID.getStatus(),
                fieldErrors
        );

        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));
    }
}

 

 

 

 


 

 

해결 완료

 

이후 응답을 보면 다음과 같이 나온다.

{
    "message": "입력값이 올바르지 않습니다.",
    "code": "INPUT_001",
    "status": 400,
    "errors": [
        {
            "field": "checkIsAvailableId.id",
            "value": " ",
            "reason": "아이디는 비어있을 수 없습니다."
        }
    ]
}

 

Postman으로 요청한 결과이다.

728x90