이번 미션에서 스프링으로 사용자가 방탈출을 예약하는 기능을 구현하였다.
이 과정에서 예약 신청 시 생기는 여러 가지 오류 사항을 핸들링해야했는데, 사용자에게 어떤 에러인지만 보여주면 되지 않을까 하는 생각에 아래와 같이 400 Status Code와 함께 직접 정의한 에러 메시지가 반환되도록 하였다.
그런데 이렇게 메시지 형태로만 응답을 보내도 괜찮을까? 이 응답을 받는 입장에서 에러에 충분히 대응할 수 있는 정보가 주어진걸까? 하는 의문이 들었었다.
그리고 미션 피드백 시간에 이런 질문을 받았다.
에러 응답에는 어떤 값이 포함되어야 하는가?
이 피드백을 통해 서버가 클라이언트에게 에러를 응답하는 이유가 무엇인지, 그리고 어떤 값이 포함되어야 하는 지에 대해 궁금하였다. 이를 공부해보고 미션에 적용한 과정을 포스팅해보려 한다.
🤔 왜 에러 응답이 필요한가?
REST 서비스는 클라이언트에게 자원에 접근하거나 변경할 수 있도록 API를 제공한다. 예를 들어서 어떤 자원을 저장하는 경우 저장된 자원에 접근할 수 있는 Location을 함께 반환하는 것이 일반적이다.
에러 응답도 이와 같다. 클라이언트에게 어떤 자원에 접근하거나 변경하거는 등의 작업에 실패했다면 그 이유가 무엇인지 적절한 상태코드와 응답 객체의 본문을 통해 알려줄 필요가 있다. 이 정보들은 개발자가 해당 에러에 대해 트러블 슈팅할 수 있는 단서를 제공해준다.
🤔 어떤 내용을 포함해야할까?
단서를 제공해주기 위해서 어떤 정보를 포함해주어야 할까?
아래 예시 코드를 보자.
{
"timestamp":"2019-09-16T22:14:45.624+0000",
"status":500,
"error":"Internal Server Error",
"message":"No message available",
"path":"/api/book/1"
}
에러가 발생한 시간과 HTTP Status Code와 함께, 에러 메시지와 에러가 발생한 URL 경로를 반환하고 있다.
정리하면 에러 응답에 꼭 포함해야 하는 정보는 아래와 같다.
1. 어떤 에러인지 파악할 수 있는 고유한 식별자
2. 사용자가 읽을 수 있는 에러 메시지
3. 개발자가 읽을 수 있는 상세한 에러에 대한 정보
조금 더 정형화된 방식으로 에러 응답을 보내주기 위해 REST API는 에러 응답에 대한 표준을 제공한다.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
이 필드들에 대한 설명은 아래와 같다.
type – a URI identifier that categorizes the error
title – a brief, human-readable message about the error
status – the HTTP response code (optional)
detail – a human-readable explanation of the error
instance – a URI that identifies the specific occurrence of the error
만약 에러 응답의 정형화를 원하는 경우 이 응답 형식을 사용할 수 있다.
정리하자면 REST API에서 에러를 핸들링하는 Best Practice는 다음과 같다.
- 구체적인 상태 코드 제공
- 응답 본문에 에러에 대한 추가적인 정보 제공
- 일관된 방식으로의 Exception Handling
👩💻 미션 코드에 적용해보기
이러한 REST API의 표준인 RFC 7087을 스프링에서 적용하기 위한 클래스가 있다.
ProblemDetail이라는 클래스인데, 정형화된 형식을 사용해서 에러 응답 객체를 만들기 위해 미션 코드에 적용해보았다.
public class ProblemDetail {
private static final URI BLANK_TYPE = URI.create("about:blank");
private URI type = BLANK_TYPE;
@Nullable
private String title;
private int status;
@Nullable
private String detail;
@Nullable
private URI instance;
@Nullable
private Map<String, Object> properties;
이걸 사용해서 응답 객체의 필드를 늘려보자.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = IllegalArgumentException.class)
public ResponseEntity<ProblemDetail> handle(final IllegalArgumentException exception) {
return ResponseEntity.badRequest().body(
ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
exception.getMessage()
)
);
}
}
전역적으로 반환되는 IllegalArgumentException 오류를 잡아서 의미를 포함한 응답 객체로 반환하도록 수정하였다. 👍
적용한 과정은 단순했지만, 올바른 에러 응답에 대해 고민해볼 수 있었다.
참고
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html
https://www.baeldung.com/rest-api-error-handling-best-practices
https://datatracker.ietf.org/doc/html/rfc7807
'우아한테크코스 > 레벨2' 카테고리의 다른 글
[Java] 서블릿(Servlet) 이란? Java Application에서 어떻게 사용될까? (0) | 2024.05.11 |
---|---|
[회고] 우아한테크코스 6기 백엔드 레벨2 3주차 회고 (3) | 2024.05.06 |
[회고] 우아한테크코스 6기 백엔드 레벨2 2주차 회고 (5) | 2024.04.28 |
[OOP] POJO (Plain Old Java Objects) 란? (feat. 스프링의 등장 배경) (0) | 2024.04.27 |
[Spring] 직렬화 (Serialization)와 역직렬화 (Deserialization) 의 정의 및 스프링에서 사용하는 방법 (4) | 2024.04.27 |