REST API 에러 핸들링 best practice - 에러를 어떻게 내려 주어야 할까
이 글은 baeldung의 'Best Practice For Rest API Error Handling'를 번역하고 실습해본 내용입니다.
Best Practice For Rest API Error Handling
소개
REST는 stateless(상태가 없는) 아키텍쳐이며 클라이언트가 서버의 자원에 접근하고 조작할 수 있습니다. 일반적으로, REST는 HTTP를 이용하여 클라이언트가 자원을 얻거나 상태를 바꾸는 API를 제공합니다. 이 튜토리얼에서는 REST API 에러에 대해 best practice를 배워봅니다.
HTTP 상태 코드
클라이언트가 서버에 HTTP 요청을 보내면, 그리고 서버가 그 요청을 잘 받았다면, 서버는 반드시 클라이언트에게 그 요청이 성공적으로 처리됐는지 아닌지 알려주어야 합니다. HTTP는 다음과 같은 5가지의 상태코드 카테고리를 제공해서 그것을 해결합니다.
100-level (정보): 서버가 요청을 알아 들었습니다.
200-level(성공): 서버가 요청을 예상한대로 완료했습니다.
300-level(리다이렉션): 요청을 완료하려면 클라이언트의 추가작업이 필요합니다.
400-level(클라이언트 에러): 클라이언트가 유효하지않은 요청을 했습니다.
500-level(서버 에러): 서버가 서버 에러 때문에 유효한 요청을 수행하지 못했습니다.
이러한 응답 코드들을 기반으로 클라이언트는 특정 요청에 대한 결과를 추정할 수 있습니다.
에러 핸들링
에러를 처리하는 가장 간단한 방법은 적절한 응답 코드를 내려주는 것입니다.
자주 나오는 응답 코드들은 다음과 같습니다.
400 Bad Request - 클라이언트가 유효하지 않은 요청을 보낸 경우 (request body나 파라미터를 빼먹은 경우같이)
401 Unauthorized - 해당 서버에 클라이언트 인증이 실패한 경우
403 Forbidden - 클라이언트가 인증은 됐지만 요청한 자원에 대한 권한은 없는 경우 (예를 들어 로그인된 사용자가 관리자 페이지에 접근하는 경우 겠죠?)
404 Not Found - 요청한 자원이 존재하지 않는 경우
412 Precondition Failed - Request Header 필드 중 한 개 이상의 값이 잘못 된 경우
500 Internal Server Error - 서버에서 발생된 일반적인 에러
503 Service Unavailable - 요청된 서비스가 이용가능하지 않는 경우
일반적으로 우리는 500 대 에러를 클라이언트에 보여주면 안됩니다. 500 에러는 요청을 처리하는데 서버에서 예상하지 못한 예외발생 같은 경우에 대한 신호이기 때문입니다. 그러므로, 서버 내부 에러는 클라이언트의 비지니스가 아닙니다.
대신에, 우리는 부지런히 내부에러를 처리하거나 캐치해서 400 대 에러를 내려주어야 합니다. 예를들어, 요청된 자원이 존재하지 않는다면 500 에러가 아닌 404 에러를 주어야 합니다.
기본적이지만 이러한 코드를 통해 클라이언트는 발생한 오류의 광범위한 특성을 이애할 수 있습니다. 예를들어, 우리는 403 에러가 발생했다면 자원에 대한 권한이 없다는 것을 알 수 있겠죠.
그래도 많은 경우에 이러한 응답에 대한 추가 설명도 필요합니다.
Default Spring Error Response
이러한 경우들은 너무 보편적이어서 스프링이 기본 오류처리 메커니즘으로 체계화했습니다.
예제 코드로 살펴볼까요?
Book을 관리하는 아주 간단한 api입니다. 전체코드를 확인하려면 밑의 링크에서 봐주세요.
https://github.com/JunHoPark93/blog-code/tree/master/error-handling-best-practice
책 아이디 1번에 해당하는 책이 없으면 컨트롤러에서 BookNotFoundException을 던집니다.
@RestController
@RequestMapping("/api/book")
public class BookController {
@Autowired
private BookRepository repository;
@GetMapping("/{id}")
public Book findById(@PathVariable long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
}
이 엔드포인트로 GET요청을 보내면 response body에 다음과 같은 에러메세지가 나옵니다.
localhost:8080/api/book/1
{
"timestamp": "2019-11-18T08:02:21.347+0000",
"status": 500,
"error": "Internal Server Error",
"message": "No message available",
"path": "/api/book/1"
}
잘 보면 이 기본 (Default) 에러 핸들러는 에러가 발생한 시간대(timestamp), HTTP 상태코드, 타이틀(error), 메세지(디폴트로는 빈 메세지), 에러가 발생한 URL 경로가 포함되어 있습니다.
이 필드들은 클라이언트나 개발자에게 트러블슈팅을 하도록 도와줍니다. 또한 표준 오류처리 메커니즘을 구성하는 몇 가지 필드를 구성합니다.
추가로, 스프링이 자동으로 BookNotFoundException이 발생했을 때 500 HTTP 상태코드를 내렸죠. 비록 일부 API는 단순하게 하기 위해 모든 오류에 대해 500 혹은 400 상태 코드를 반환하지만 가능한 구체적인 오류코드를 사용하는 것이 가장 좋습니다.
더 상세한 Responses
스프링의 예제에서 본 것처럼 때로는 상태코드가 에러에 대한 상세내용을 보여주기엔 부족합니다. 필요하다면 우리는 추가정보를 응답 body에 추가해줄 수 있습니다.
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleException(BookNotFoundException ex) {
ApiErrorResponse response =
new ApiErrorResponse("error-0001", "No book is found with ID : " + ex.getId());
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
ApiErrorResponse를 간단히 정의하여 상태코드와 함께 ResponseEntity로 내려줍니다.
상세한 응답을 제공하려면 다음이 필요합니다.
Error - 에러에 대한 고유한 식별자
Message - 사람이 읽을 수 있는 간단한 메세지
Detail - 에러에 대한 더 긴 설명
예를 들어 클라이언트가 틀린 인증 정보를 보냈다면 우리는 403 응답을 이 body와 함께 보낼 수 있습니다.
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct"
}
에러 필드는 응답코드와 같으면 안되고 애플리케이션에서 고유한 코드여야 합니다. 일반적으로 에러 필드에 대한 컨벤션은 없지만 고유한 코드를 내려준다는 것만 기억하세요.
일반적으로 이 필드는 알파벳, 숫자 그리고 연결문자(대쉬나 언더스코어)를 포함합니다. 예를들어 0001, auth-0001가 그 표준 예시입니다.
메세지 필드의 내용 비율은 주로 유저 인터페이스에 잘 보이는 정도 입니다. 그러므로 국제화를 지원한다면 번역도 고려해야 합니다.
디테일 필드는 개발자에게 필요하고 엔드 유저에는 필요하지 않기 때문에 번역은 필요하지 않습니다.
추가로 우리는 help 필드와 같은 URL을 제공해서 클라이언트가 다른 정보를 얻기 위해 가이드를 줄 수 있습니다.
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
}
때때로, 우리는 요청에 대한 에러를 한 개 이상 주고 싶을 때가 있습니다. 이런 경우 에러를 리스트로 내려 줍니다.
{
"errors": [
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
},
...
]
}
그리고 단일 에러가 발생하면 우리는 한 개의 요소를 가진 리스트를 내려 줍니다. 주의할 것은 간단한 애플리케이션에서 여러 에러를 내리는 설계는 너무 복잡하다는 것만 기억하세요. 많은 경우에 첫 번째 에러나 가장 중요한 에러 하나면 충분합니다.
Response Bodies의 표준화
대부분의 REST API들에서 비슷한 컨벤션을 따릅니다. 차이가 있다면 라이브러리나 프레임워크에서 균일하게 처리하기가 어렵게 되겠죠.
REST API 에러 핸들링을 표준화하기위한 노력으로, IETF는 일반화된 에러 핸들링 스키마를 만드는 RFC 7807을 고안했습니다. https://tools.ietf.org/html/rfc7807
이 스키마는 5개의 파트로 이루어져 있습니다.
type - 에러를 분류하기 위한 URI 식별자
title - 사람이 읽을 수 있는 간단한 에러에 대한 메세지
status - HTTP 응답 코드 (optional)
detail - 사람이 읽을 수 있는 에러에 대한 설명
intance - 에러가 발생한 URI
{
"type": "/errors/incorrect-user-pass",
"title": "Incorrect username or password.",
"status": 403,
"detail": "Authentication failed due to incorrect username or password.",
"instance": "/login/log/abc123"
}
먼저 번 것들과 비교해보면 키 이름이 조금씩 다르네요. 무엇을 선택 하든 일관성 있게 내려 주면 될 것 같습니다.
예시
위의 예시들은 REST API들에 대해 보편적인 방식입니다. 필드에 대한 이름은 사이트마다 다르지만 일반적인 패턴은 거의 비슷합니다.
유명 서비스들은 어떤 정책을 가지고 있을까요? 트위터와 페이스북을 살펴볼까요?
트위터에 authentication 없이 GET 요청을 하나 찔러 보겠습니다.
https://api.twitter.com/1.1/statuses/update.json?include_entities=true
트위터 API는 위와 같이 400 에러를 바디와 함께 내려 줍니다. (400대 에러지만 자체 코드로 정의한 모습입니다. 자세한 내용은 트위터 문서에 가면 나와있습니다) 또한 잘보면 에러를 리스트로 내려 주네요. 지금은 싱글 에러니까 하나지만 대괄호로 묶여있는 것을 보면 여러 에러를 한꺼번에 내려주는 형식을 따르는 것을 알 수 있습니다. 그리고 트위터의 경우 상세 메세지는 없네요.
https://graph.facebook.com/oauth/access_token?client_id=foo&client_secret=bar&grant_type=baz
트위터 처럼 페이스북도 자체 코드를 내려줍니다 (400대 에러이지만). message도 있네요. 페이스북은 에러를 카테고리화한 type 필드를 가지고 있고 trace ID 도 가지고 있습니다. fbtrace_id는 페이스북 문서내용에 따르면 Internal Support Identifier의 역할을 한다고 합니다. 버그 리포팅을 할 때 trace ID가 디버깅을 위한 로그데이터를 찾는데 도움을 준다고 하네요.
페이스북 Error Response 상세 내용
https://developers.facebook.com/docs/graph-api/using-graph-api/error-handling
결론
이 글에서는 우리가 REST API의 에러핸들링에 대한 best practice를 알아보았습니다.
- 특정 상태코드를 내려주기
- 응답 바디에 추가적인 정보를 담아주기
- 통일된 방식으로 에러 처리를 하기
애플리케이션 마다 에러핸들링을 하는 상세한 방법은 다를 수 있지만 이 일반적인 원칙들은 거의 모든 REST API에 적용되고 가능한 한 고수해야 합니다.
참고: https://www.baeldung.com/rest-api-error-handling-best-practices