Spring Boot REST API 오류 처리 가이드
게시 됨: 2022-03-11의미 있는 오류 메시지를 제공하면서 API에서 오류를 올바르게 처리하는 것은 API 클라이언트가 문제에 적절하게 대응하는 데 도움이 될 수 있으므로 매우 바람직한 기능입니다. 기본 동작은 이해하기 어렵고 궁극적으로 API 클라이언트에 쓸모 없는 스택 추적을 반환하는 경향이 있습니다. 오류 정보를 필드로 분할하면 API 클라이언트가 이를 구문 분석하고 사용자에게 더 나은 오류 메시지를 제공할 수도 있습니다. 이 기사에서는 Spring Boot로 REST API를 빌드할 때 적절한 오류 처리를 수행하는 방법을 다룰 것입니다.
Spring으로 REST API를 빌드하는 것은 지난 몇 년 동안 Java 개발자를 위한 표준 접근 방식이 되었습니다. Spring Boot를 사용하면 많은 상용구 코드를 제거하고 다양한 구성 요소의 자동 구성을 가능하게 하므로 상당한 도움이 됩니다. 여기에 설명된 지식을 적용하기 전에 해당 기술을 사용한 API 개발의 기본 사항에 익숙하다고 가정합니다. 기본 REST API를 개발하는 방법에 대해 여전히 확신이 없다면 Spring MVC에 대한 이 기사 또는 Spring REST 서비스 구축에 대한 다른 기사부터 시작해야 합니다.
오류 응답을 더 명확하게 만들기
이 기사 전체에서 우리는 새를 나타내는 개체를 검색하기 위해 REST API를 구현하는 애플리케이션의 GitHub에서 호스팅되는 소스 코드를 사용할 것입니다. 이 문서에 설명된 기능과 오류 처리 시나리오의 몇 가지 추가 예가 있습니다. 다음은 해당 애플리케이션에서 구현된 엔드포인트의 요약입니다.
GET /birds/{birdId} | 새에 대한 정보를 가져오고 찾을 수 없으면 예외를 throw합니다. |
GET /birds/noexception/{birdId} | 이 호출은 또한 새가 발견되지 않은 경우 예외를 throw하지 않는다는 점을 제외하고 새에 대한 정보를 얻습니다. | POST /birds | 새를 만듭니다. | </tr>
Spring 프레임워크 MVC 모듈에는 오류 처리에 도움이 되는 몇 가지 훌륭한 기능이 있습니다. 그러나 이러한 기능을 사용하여 예외를 처리하고 API 클라이언트에 의미 있는 응답을 반환하는 것은 개발자의 몫입니다.
정수를 예상해야 하는 "mass" 필드에 "aaa" 문자열이 있는 다음 JSON 객체를 사용하여 /birds
엔드포인트에 HTTP POST를 실행할 때 기본 Spring Boot 응답의 예를 살펴보겠습니다.
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
적절한 오류 처리가 없는 Spring Boot 기본 답변:
{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }
음... 응답 메시지에는 몇 가지 좋은 필드가 있지만 예외가 무엇인지에 너무 집중되어 있습니다. 그건 그렇고, 이것은 Spring Boot의 DefaultErrorAttributes
클래스입니다. timestamp
필드는 타임스탬프가 있는 측정 단위에 대한 정보조차 전달하지 않는 정수입니다. exception
필드는 Java 개발자에게만 흥미롭고 메시지는 API 소비자가 자신과 관련이 없는 모든 구현 세부 정보에서 손실되도록 합니다. . 그리고 오류가 발생한 예외에서 추출할 수 있는 세부 정보가 더 있다면 어떻게 될까요? 따라서 이러한 예외를 적절하게 처리하고 API 클라이언트가 더 쉽게 사용할 수 있도록 더 나은 JSON 표현으로 래핑하는 방법을 알아보겠습니다.
Java 8 날짜 및 시간 클래스를 사용할 것이므로 먼저 Jackson JSR310 변환기에 대한 Maven 종속성을 추가해야 합니다. @JsonFormat
주석을 사용하여 Java 8 날짜 및 시간 클래스를 JSON 표현으로 변환하는 작업을 처리합니다.
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
자, 이제 API 오류를 나타내는 클래스를 정의해 보겠습니다. REST 호출 중에 발생하는 오류에 대한 관련 정보를 보유하기에 충분한 필드가 있는 ApiError
라는 클래스를 생성할 것입니다.
class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
status
속성은 작업 호출 상태를 유지합니다. 클라이언트 오류를 나타내는 4xx부터 서버 오류를 의미하는 5xx까지입니다. 일반적인 시나리오는 예를 들어 클라이언트가 잘못된 이메일 주소와 같이 형식이 잘못된 필드를 보낼 때 BAD_REQUEST를 의미하는 http 코드 400입니다.timestamp
속성은 오류가 발생한 날짜-시간 인스턴스를 보유합니다.message
속성은 오류에 대한 사용자 친화적인 메시지를 보유합니다.debugMessage
속성은 오류를 더 자세히 설명하는 시스템 메시지를 보유합니다.subErrors
속성은 발생한 하위 오류의 배열을 보유합니다. 이것은 단일 호출에서 여러 오류를 나타내는 데 사용됩니다. 여러 필드가 유효성 검사에 실패한 유효성 검사 오류가 그 예입니다.ApiSubError
클래스는 이를 캡슐화하는 데 사용됩니다.
abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }
따라서 ApiValidationError
는 ApiSubError
를 확장하고 REST 호출 중에 발생한 유효성 검사 문제를 표현하는 클래스입니다.
아래에서는 이 기사가 끝날 때까지 얻게 될 내용에 대한 아이디어를 얻기 위해 여기에 설명된 개선 사항을 구현한 후 생성되는 JSON 응답의 몇 가지 예를 볼 수 있습니다.
다음은 엔드포인트 GET /birds/2
를 호출하는 동안 엔터티를 찾을 수 없을 때 반환되는 JSON의 예입니다.
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
다음은 새의 질량에 대해 잘못된 값으로 POST /birds
호출을 실행할 때 반환되는 JSON의 또 다른 예입니다.
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }
스프링 부트 오류 처리
예외를 처리하는 데 사용할 몇 가지 Spring 주석을 살펴보겠습니다.
RestController
는 REST 작업을 처리하는 클래스의 기본 주석입니다.

ExceptionHandler
는 핸들러(컨트롤러 작업) 실행 중에 throw되는 예외를 처리하는 메커니즘을 제공하는 Spring 주석입니다. 이 주석은 컨트롤러 클래스의 메서드에서 사용되는 경우 이 컨트롤러 내에서만 발생하는 예외를 처리하기 위한 진입점 역할을 합니다. 전체적으로 가장 일반적인 방법은 @ControllerAdvice
클래스의 메서드에서 @ExceptionHandler
를 사용하여 예외 처리가 전역적으로 또는 컨트롤러의 하위 집합에 적용되도록 하는 것입니다.
ControllerAdvice
는 Spring 3.2에 도입된 주석으로 이름에서 알 수 있듯이 여러 컨트롤러에 대한 "Advice"입니다. 단일 ExceptionHandler
를 여러 컨트롤러에 적용할 수 있도록 하는 데 사용됩니다. 이런 식으로 우리는 한 곳에서 이러한 예외를 처리하는 방법을 정의할 수 있으며 이 ControllerAdvice
가 다루는 클래스에서 예외가 throw될 때 이 핸들러가 호출됩니다. 영향을 받는 컨트롤러의 하위 집합은 @ControllerAdvice
에서 다음 선택기를 사용하여 정의할 수 있습니다. annotations()
, basePackageClasses()
및 basePackages()
. 선택기가 제공되지 않으면 ControllerAdvice
가 모든 컨트롤러에 전역적으로 적용됩니다.
따라서 @ExceptionHandler
및 @ControllerAdvice
를 사용하여 예외를 처리하고 기본 Spring Boot 오류 처리 메커니즘보다 더 나은 구성으로 ApiError
객체에 이를 래핑하기 위한 중심점을 정의할 수 있습니다.
예외 처리
다음 단계는 예외를 처리할 클래스를 만드는 것입니다. 간단하게 하기 위해 RestExceptionHandler
라고 부르며 Spring Boot의 ResponseEntityExceptionHandler
에서 확장해야 합니다. ResponseEntityExceptionHandler
는 이미 Spring MVC 예외에 대한 기본적인 처리를 제공하므로 확장할 것이므로 기존 예외를 개선하면서 새 예외에 대한 핸들러를 추가할 것입니다.
ResponseEntityExceptionHandler에서 처리되는 예외 재정의
ResponseEntityExceptionHandler
의 소스 코드를 handleHttpMessageNotReadable()
또는 handleHttpMessageNotWritable()
) 과 같은 handle******()
이라는 메서드를 많이 볼 수 있습니다. 먼저 HttpMessageNotReadableException
예외를 처리하기 위해 handleHttpMessageNotReadable()
을 확장하는 방법을 살펴보겠습니다. RestExceptionHandler
클래스에서 handleHttpMessageNotReadable()
메서드를 재정의하기만 하면 됩니다.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }
HttpMessageNotReadableException
이 발생하는 경우 오류 메시지는 "Malformed JSON request"가 되고 오류는 ApiError
개체 내부에 캡슐화된다고 선언했습니다. 아래에서 이 새로운 메서드가 재정의된 REST 호출의 응답을 볼 수 있습니다.
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }
사용자 정의 예외 처리
이제 Spring Boot의 ResponseEntityExceptionHandler
내부에 아직 선언되지 않은 예외를 처리하는 메서드를 만드는 방법을 살펴보겠습니다.
데이터베이스 호출을 처리하는 Spring 애플리케이션의 일반적인 시나리오는 저장소 클래스를 사용하여 ID로 레코드를 찾는 호출을 갖는 것입니다. 그러나 CrudRepository.findOne()
메서드를 살펴보면 객체가 발견되지 않으면 null
을 반환한다는 것을 알 수 있습니다. 즉, 서비스가 이 메서드를 호출하고 컨트롤러에 직접 반환하는 경우 리소스를 찾을 수 없더라도 HTTP 코드 200(OK)을 얻게 됩니다. 사실, 적절한 접근 방식은 HTTP/1.1 사양에 지정된 HTTP 코드 404(NOT FOUND)를 반환하는 것입니다.
이 경우를 처리하기 위해 EntityNotFoundException
이라는 사용자 정의 예외를 생성합니다. 이것은 사용자 정의 생성 예외이며 javax.persistence.EntityNotFoundException
과 다릅니다. 객체 생성을 용이하게 하는 일부 생성자를 제공하고 javax.persistence
예외를 다르게 처리하도록 선택할 수 있기 때문입니다.
즉, RestExceptionHandler
클래스에서 새로 생성된 EntityNotFoundException
에 대한 ExceptionHandler
를 생성해 보겠습니다. 그렇게 하려면 handleEntityNotFound()
라는 메서드를 만들고 @ExceptionHandler
로 주석을 달고 클래스 개체 EntityNotFoundException.class
를 전달합니다. 이것은 EntityNotFoundException
이 발생할 때마다 Spring이 이를 처리하기 위해 이 메소드를 호출해야 함을 Spring에 알립니다. @ExceptionHandler
를 사용하여 메서드에 주석을 추가할 때 여기에 설명된 대로 WebRequest
, Locale
및 기타와 같은 광범위한 자동 주입 매개변수를 허용합니다. 우리는 예외 EntityNotFoundException
자체를 이 handleEntityNotFound
메소드에 대한 매개변수로 제공할 것입니다.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }
엄청난! handleEntityNotFound()
메소드에서 HTTP 상태 코드를 NOT_FOUND
로 설정하고 새로운 예외 메시지를 사용합니다. GET /birds/2
엔드포인트에 대한 응답은 다음과 같습니다.
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
결론
예외 처리를 제어하는 것이 중요하므로 해당 예외를 ApiError
객체에 적절하게 매핑하고 API 클라이언트가 무슨 일이 일어났는지 알 수 있도록 하는 중요한 정보를 제공할 수 있습니다. 여기에서 다음 단계는 응용 프로그램 코드 내에서 throw되는 예외에 대해 더 많은 처리기 메서드(@ExceptionHandler가 있는 메서드)를 만드는 것입니다. GitHub 코드에는 MethodArgumentTypeMismatchException
, ConstraintViolationException
및 기타와 같은 다른 일반적인 예외에 대한 더 많은 예가 있습니다.
다음은 이 문서의 구성에 도움이 된 몇 가지 추가 리소스입니다.
Baeldung - Spring을 사용한 REST에 대한 오류 처리
Spring 블로그 - Spring MVC의 예외 처리