Curl Up Black Cat


😎 서론 

안녕하세요 오늘은 Spring에서 자주 마주치는 문제 중 하나인 API 잘못된 메시지 요청에 대한 이야기를 해보려고 합니다. 그리고 이를 어떻게 처리했는지 작성해 볼게요.

 

게시판 프로젝트를 만들어 보면서 백엔드에서 클라이언트로부터 API 요청을 받게 되면, 요청의 JSON 포맷에 오류가 있는 경우가 있었습니다. 그 중에서도 알 수 없는 필드를 전달 받았을 때 이 예외가 발생합니다. 이 문제를 어떻게 해결 할 수 있을까 고민을 했고..


🤔 HttpMessageNotReadableException 처리

Spring 에서 제공하는 @ExceptionHandler 를 사용하여 HttpMessageNotReadableException 을 처리하면서, 그 원인이 되는 cause 를 통해 더 구체적인 예외 상황을 파악하는 방법을 알게되었습니다.

@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity handleJsonParseException(HttpMessageNotReadableException ex) {
    final Throwable cause = ex.getCause();
    ...
}

 

이 때, 발견한 것이 UnrecognizedPropertyException 이라는 것입니다. 이 예외는 JSON알 수 없는 필드 때문에 발생하므로, 해당 필드의 이름을 얻어와 클라이언트에게 더 구체적인 오류 메시지를 전달하고자 했습니다.

if (cause instanceof UnrecognizedPropertyException) {
    final UnrecognizedPropertyException unrecognizedPropertyException = (UnrecognizedPropertyException) cause;
    final String fieldName = unrecognizedPropertyException.getPropertyName();
    ...
}

 

그리고 이렇게 얻어온 필드 이름을 ErrorCode.UNKNOWN_FIELD 와 함께 반환하여 클라이언트에게 해당 필드가 문제라는 것을 응답하였습니다. 이렇게 처리하면, 클라이언트는 어떤 필드 때문에 요청이 실패했는지 쉽게 파악할 수 있습니다.😀

 

전체 소스를 보면 아래와 같이 @ExceptionHandler 를 구축해보았습니다.

@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity handleJsonParseException(HttpMessageNotReadableException ex) {
    final Throwable cause = ex.getCause();

    if (cause instanceof UnrecognizedPropertyException) {
        final UnrecognizedPropertyException unrecognizedPropertyException = (UnrecognizedPropertyException) cause;
        final String fieldName = unrecognizedPropertyException.getPropertyName();
        final String errorMessage = String.format(ErrorCode.UNKNOWN_FIELD.getMessage() + " : '%s'", fieldName);
        final ResponseModel responseModel = ResponseModel.failure(ErrorCode.UNKNOWN_FIELD, errorMessage);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
    }

    // 기본 메시지 처리
    final ResponseModel responseModel = ResponseModel.failure(ErrorCode.INVALID_JSON, ex.getMessage());
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
}

 

그 밖에 JSON 문법 오류 / 타입 불일치 / JSON 구조의 불일치 / 데이터 바인딩 오류가 있는 경우는 기본 메시지 처리로 응답하는 INVALID_JSON 으로 응답하도록 구현하였습니다.


🤠 Response 응답 메시지

구현된 핸들러의 응답메시지는 한번 살펴보겠습니다.🙂

 

잘못된 필드 데이터 전달이 되었을 경우..

UnrecognizedPropertyException 예외

 

JSON 문법오류 등 기본 메시지 전달이 되었을 경우..

그 밖의 HttpMessageNotReadableException 예외


🥸 결론

예외 처리는 단순히 오류를  포착하는 것을 넘어 중요한 역할을 하는거 같습니다. 이를 통해 사용자와의 원활한 소통과 협업이 가능하며, 이로 인해 보다 나은 서비스를 제공할 수 있게 됩니다. 게시판 프로젝트를 만들어 보면서 이에 대한 연구와 고민을 지속적으로 해 나갈 계획입니다.🙂

 


😎 서론

안녕하세요! 오늘은 Spring Boot 에서 API 응답과 예외 처리 모델을 구축해봤던 내용을 공유해 보려고 합니다. 코드들도 하나하나 분석해 볼께요. 스타트~!

 


1. ResponseModel - 응답 모델

먼저, 모든 API 응답에 공통적으로 사용될 ResponseModel 을 살펴봅니다.

@Getter
@Setter
@ToString
@AllArgsConstructor(staticName = "of")
public class ResponseModel<T> {
    private boolean success;
    private T data;
    private ErrorModel error;

    public static <T> ResponseModel<T> of(boolean success, T data) {
        return ResponseModel.of(success, data, null);
    }

    public static ResponseModel of(boolean success, ErrorCode code) {
        return ResponseModel.of(success, null, ErrorModel.of(code.name(), code.getMessage(), null));
    }

    public static ResponseModel of(boolean success, ErrorCode code, Exception ex) {
        return ResponseModel.of(success, null, code, ex);
    }

    public static <T> ResponseModel<T> of(boolean success, T data, ErrorCode code, Exception ex) {
        final ErrorModel error = (code != null) ? ErrorModel.of(code, ex) : null;
        return ResponseModel.of(success, data, error);
    }
}

주요 구성 요소:

1. success: 요청 처리의 성공 여부를 나타냅니다.

2. data: 요청에 대한 결과 데이터를 포함합니다.

3. error: 발생한 오류에 대한 정보를 포함하는 ErrorModel 객체 입니다.

 

ResponseModel 에는 여러 static 생성 메서드가 포함되어 있습니다. 이 메서드들을 통해 성공 또는 오류 상황에 따라 쉽게 응답 객체를 구성할 수 있습니다.

 

2. ErrorModel - 에러 모델

ErrorModel 은 오류에 대한 정보를 표준화하여 전달하기 위한 모델입니다.

 

package com.board.backend.model;

import com.board.backend.common.ErrorCode;
import com.board.backend.common.utils.ObjectUtil;
import com.board.backend.common.utils.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@AllArgsConstructor(staticName = "of")
public class ErrorModel {
    private String code;
    private String message;
    private ErrorData data;

    @Getter
    @Setter
    @ToString
    @AllArgsConstructor(staticName = "of")
    public static class ErrorData {
        private String exceptionMessage;
        private String stackTrace;
    }

    public static ErrorModel of(ErrorCode code, Exception ex) {
        final String exceptionMessage = ObjectUtil.nonEmpty(ex) ? ex.getMessage() : null;
        final String stackTrace = ObjectUtil.nonEmpty(ex) ? getStackTraceAsString(ex) : null;
        final ErrorData data = (StringUtil.nonEmpty(exceptionMessage) || StringUtil.nonEmpty(stackTrace)) ? ErrorData.of(exceptionMessage, stackTrace) : null;
        return ErrorModel.of(code.name(), code.getMessage(), data);
    }

    private static String getStackTraceAsString(Exception ex) {
        final StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : ex.getStackTrace()) {
            sb.append(element.toString());
            sb.append("\n");
        }
        return sb.toString();
    }
}

주요 구성 요소:

1. code: 오류 코드를 나타냅니다.

2. message: 오류 메시지를 나타냅니다.

3. data: 오류 발생 시의 추가 정보를 나타내는 ErrorData 객체입니다.

 

ErrorData 클래스는 발생한 예외의 메시지와 스택 트레이스 정보를 포함합니다. 이를 통해 오류의 원인을 더욱 자세히 파악할 수 있습니다.(스택 트레이스란? 프로그램에서 예외가 발생했을 때 해당 예외의 발생 경로를 추적하는 정보입니다. 이는 메서드 호출 순서와 라인 번호를 포함하여 오류가 발생한 위치와 그 원인을 확인할 수 있게 도와줍니다.)

 

3. ErrorCode - 에러 코드

@Getter
@AllArgsConstructor
public enum ErrorCode {
    SERVER_ERROR("서버 에러"),
    ARGUMENT_TYPE_MISMATCH("잘못된 파라미터타입이 전달되었습니다."),
    BOARD_NOT_FOUND("글이 존재하지 않습니다");
    ...
    private String message;
}

각 오류 유형에 대한 메시지를 함께 제공하여 사용자에게 의미 있는 오류 메시지를 전달 합니다. enum 타입으로 코드와 메시지를 함께 관리하였습니다.

 

4. ExceptionAdvice - 전역 예외 처리기

Spring Boot 에서 예외 처리의 중요성을 강조하며, 이를 위해 @ControllerAdvice@ExceptionHandler 를 통해 효과적인 전역 예외 처리 방식을 제공합니다.

@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler(value = {Exception.class})
    public ResponseEntity handleException(Exception ex, WebRequest request) {
        final ResponseModel responseModel = ResponseModel.of(false, ErrorCode.SERVER_ERROR, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseModel);
    }

    @ExceptionHandler(value = {MethodArgumentTypeMismatchException.class})
    public ResponseEntity handleTypeException(Exception ex) {
        final ResponseModel<Object> responseModel = ResponseModel.of(false, ErrorCode.ARGUMENT_TYPE_MISMATCH,  ex);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
    }
    ...
}

 

1) @ControllerAdvice 의 역할

@ControllerAdvice 는 Spring 에서 모든 컨트롤러에서 발생할 수 있는 예외들을 한곳에서 관리하고 처리하기 위해 도입된 어노테이션입니다. 이를 통해 코드의 중복을 줄이고, 일관된 예외 처리 로직을 구성할 수 도와줍니다. 

 

2) @ExceptionHandler 

@ExceptionHandler 는 특정 예외를 처리할 메서드를 지정하기 위한 Spring MVC 의 어노테이션입니다. 그 자체로도 유용하지만, @ControllerAdvice와 결합하여 사용할 경우, 애플리케이션 전체의 예외를 중앙에서 효율적으로 관리할 수 있게 됩니다.

 

예시:

// 단일 지정
@ExceptionHandler({CustomException.class})

// 다중 지정
@ExceptionHandler({CustomException1.class, CustomException2.class, CustomException3.class})

이렇게 설정하면, 위에서 지정된 예외들이 발생했을 때 동일한 방식으로 처리됩니다.

 

3) 확장성

@ControllerAdvice 의 장점중 하나는 그 확장성입니다. 새로운 예외를 추가하거나 예외 처리 방식을 수정해야 할 때, @ExceptionHandler 를 적절한 메서드에 추가하거나 수정함으로써 중앙에서 예외 처리 로직을 쉽게 관리하고 확장할 수 있습니다.


🤠 결론

Spring Boot 환경에서 나름대로 생각해서 구현했던 응답과 예외 처리 방법에 대해 알아보았습니다. 일관된 응답과 정확한 예외 처리는 API의 신뢰성을 높이는 핵심 요소로, 이를 통해 사용자와의 신뢰 관계를 구축하고, 개발팀 간의 협업 효율을 높일 수 있습니다. 🙂

+ Recent posts