RFC 7807: Problem Details for HTTP APIs#
RFC 7807은 HTTP API에서 오류 상황을 일관되고 기계가 처리하기 쉬운 방식으로 전달하기 위한 표준이다.
이 규격은 “Problem Details for HTTP APIs"라는 제목으로 2016년 3월에 공식 발표되었으며, HTTP API에서 발생한 오류 상황을 구조화된 JSON 또는 XML 형식으로 표현하는 방식을 정의한다.
발표: 2016년 3월
저자: Mark Nottingham 외
상태: Proposed Standard (표준화 단계의 공식 규격)
RFC 7807의 배경과 목적#
HTTP API는 다양한 클라이언트와 통신하며, 여러 오류 상황에 직면한다. 전통적으로 각 API는 자체적인 오류 응답 형식을 정의했는데, 이로 인해 클라이언트 개발자는 API마다 다른 오류 처리 로직을 구현해야 했다.
기존의 HTTP 오류 응답은 다음과 같은 한계가 있었다:
- 상태 코드만 제공 (예: 404, 500 등)
- 메시지는 단순한 텍스트로 되어 있고, 기계가 이해하기 어려움
- 클라이언트가 오류 원인 및 처리 방법을 알기 어려움
RFC 7807은 오류 응답을 표준화된 구조로 표현함으로써, API 소비자(클라이언트)가 오류를 정확히 파악하고 자동으로 처리할 수 있도록 도와준다.
이 규격의 주요 목적은:
- HTTP API에서 오류 상황을 표현하기 위한 일관된 형식 제공
- 사람과 기계 모두가 쉽게 이해할 수 있는 오류 정보 제공
- 다양한 미디어 타입을 통한 유연한 오류 정보 전달 지원
- 기존 HTTP 상태 코드와의 호환성 유지
Problem Details 형식의 기본 구조#
RFC 7807은 JSON 또는 XML 형식으로 오류 정보를 전달할 수 있는 구조를 정의한다.
필드 | 필수 | 설명 |
---|
type | 선택 | 문제 유형의 식별자 (URL 형식 권장) |
title | 선택 | 문제의 짧고 간결한 요약 (사람이 읽기 쉽게) |
status | 선택 | HTTP 상태 코드 (예: 404, 400) |
detail | 선택 | 문제의 구체적인 설명 |
instance | 선택 | 문제가 발생한 리소스의 URI (예: /users/123 ) |
JSON 형식의 기본 예시:
1
2
3
4
5
6
7
| {
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "계정 잔액이 2300원으로, 요청된 3000원의 출금을 처리할 수 없습니다.",
"instance": "/account/12345/transactions/54321"
}
|
1
2
3
4
5
6
7
8
9
10
| HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://example.com/probs/user-not-found",
"title": "사용자를 찾을 수 없습니다",
"status": 404,
"detail": "ID가 123인 사용자가 존재하지 않습니다.",
"instance": "/users/123"
}
|
3. 미디어 타입#
RFC 7807은 두 가지 공식 미디어 타입을 정의한다:
- application/problem+json: JSON 형식의 Problem Details
- application/problem+xml: XML 형식의 Problem Details
이러한 미디어 타입을 사용하면 클라이언트는 오류 응답을 식별하고 적절히 처리할 수 있다.
HTTP 응답에서는 Content-Type 헤더를 통해 이를 지정한다.
1
2
3
4
5
6
7
8
9
10
11
| HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: ko
{
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "계정 잔액이 2300원으로, 요청된 3000원의 출금을 처리할 수 없습니다.",
"instance": "/account/12345/transactions/54321"
}
|
확장성: 문제 유형 정의#
RFC 7807의 핵심 강점 중 하나는 확장성이다.
API 설계자는 자신만의 문제 유형을 정의할 수 있으며, 각 유형에 대해 추가 속성을 포함할 수 있다.
유형 URI 설계#
문제 유형은 URI 형태로 표현되며, 이는 다음과 같은 방식으로 설계될 수 있다:
문서 URI: 문제 유형에 대한 문서를 제공하는 웹 페이지 URI
1
| "type": "https://api.example.com/errors/insufficient-funds"
|
URN 체계: 문서를 직접 가리키지 않는 식별자
1
| "type": "urn:example:api:problems:insufficient-funds"
|
상대 URI: 기본 문제 유형 네임스페이스에 상대적인 참조
1
| "type": "/problems/insufficient-funds"
|
추가 속성#
문제 유형에 따라 특정 정보를 제공하기 위한 추가 속성을 정의할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
| {
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "계정 잔액이 2300원으로, 요청된 3000원의 출금을 처리할 수 없습니다.",
"instance": "/account/12345/transactions/54321",
"balance": 2300,
"accounts": [
"/account/12345",
"/account/67890"
],
"minimum_withdrawal": 1000
}
|
이 예시에서 balance
, accounts
, minimum_withdrawal
은 이 특정 문제 유형을 위한 추가 속성이다.
다중 오류 처리#
RFC 7807은 단일 오류에 초점을 맞추고 있지만, 실제 API에서는 여러 오류가 동시에 발생하는 경우가 많다(예: 폼 유효성 검사). 이를 위한 공식적인 방법은 명시되지 않았지만, 커뮤니티에서는 다음과 같은 접근 방식을 채택하고 있다:
배열 기반 접근법#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| {
"type": "https://example.com/probs/validation-error",
"title": "유효성 검사 오류",
"status": 422,
"detail": "여러 필드에서 유효성 검사 오류가 발생했습니다.",
"instance": "/users",
"errors": [
{
"field": "email",
"message": "유효한 이메일 주소여야 합니다.",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "비밀번호는 최소 8자 이상이어야 합니다.",
"code": "TOO_SHORT"
}
]
}
|
필드 기반 접근법#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| {
"type": "https://example.com/probs/validation-error",
"title": "유효성 검사 오류",
"status": 422,
"detail": "여러 필드에서 유효성 검사 오류가 발생했습니다.",
"instance": "/users",
"errors": {
"email": {
"message": "유효한 이메일 주소여야 합니다.",
"code": "INVALID_FORMAT"
},
"password": {
"message": "비밀번호는 최소 8자 이상이어야 합니다.",
"code": "TOO_SHORT"
}
}
}
|
사용 방식 요약#
Content-Type 설정
- 오류 응답일 경우,
Content-Type
을 다음과 같이 설정:application/problem+json
- 또는
application/problem+xml
(XML 지원 가능)
HTTP 상태 코드는 여전히 그대로 사용
- 구조화된 메시지를 추가할 뿐, 기존의 4xx/5xx 상태 코드는 그대로 유지
확장 필드 가능
- 추가적인 정보를 담기 위해 커스텀 필드도 포함 가능
1
2
3
4
5
6
7
8
| {
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "현재 계정 잔액으로는 요청을 처리할 수 없습니다.",
"balance": 30,
"account": "/accounts/12345"
}
|
RFC 7807 구현 예시#
다양한 프로그래밍 언어 및 프레임워크에서 RFC 7807을 구현하는 방법
Spring Boot (Java)#
Spring Framework는 RFC 7807을 구현하기 위한 ProblemDetail
클래스를 제공한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ProblemDetail> handleInsufficientFunds(
InsufficientFundsException ex, WebRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.FORBIDDEN, ex.getMessage());
problem.setType(URI.create("https://example.com/probs/out-of-credit"));
problem.setTitle("잔액 부족");
problem.setProperty("balance", ex.getBalance());
problem.setProperty("required", ex.getRequired());
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}
}
|
Express (Node.js)#
Express에서는 미들웨어를 사용하여 문제 상세 정보를 생성할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| class ApiError extends Error {
constructor(type, title, status, detail, instance, extensions = {}) {
super(detail);
this.type = type;
this.title = title;
this.status = status;
this.detail = detail;
this.instance = instance;
this.extensions = extensions;
}
}
// 오류 처리 미들웨어
app.use((err, req, res, next) => {
if (err instanceof ApiError) {
const problem = {
type: err.type,
title: err.title,
status: err.status,
detail: err.detail,
instance: err.instance,
...err.extensions
};
return res
.status(err.status)
.set('Content-Type', 'application/problem+json')
.json(problem);
}
// 일반 오류 처리
res
.status(500)
.set('Content-Type', 'application/problem+json')
.json({
type: 'https://example.com/probs/internal-error',
title: '내부 서버 오류',
status: 500,
detail: '서버에서 예기치 않은 오류가 발생했습니다.',
instance: req.originalUrl
});
});
|
Django REST Framework (Python)#
Django REST Framework에서는 예외 핸들러를 통해 문제 상세 정보를 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| from rest_framework.views import exception_handler
from rest_framework.exceptions import APIException
from rest_framework.response import Response
def problem_exception_handler(exc, context):
# 기본 처리를 먼저 시도
response = exception_handler(exc, context)
# 응답이 없으면 처리할 수 없는 예외
if response is None:
return None
if isinstance(exc, APIException):
problem = {
"type": f"https://example.com/probs/{exc.__class__.__name__.lower()}",
"title": str(exc.default_detail),
"status": response.status_code,
"detail": str(exc),
"instance": context['request'].path
}
# APIException의 상세 정보가 있으면 추가
if hasattr(exc, 'detail') and isinstance(exc.detail, dict):
for key, value in exc.detail.items():
if key not in problem:
problem[key] = value
response.data = problem
response['Content-Type'] = 'application/problem+json'
return response
|
보안 고려사항#
RFC 7807을 구현할 때 몇 가지 보안 측면을 고려해야 한다:
민감한 정보 노출
오류 메시지에 민감한 정보가 포함되지 않도록 주의해야 한다.
예를 들어:
- 내부 서버 경로나 파일 이름
- 데이터베이스 쿼리나 오류 메시지
- 내부 IP 주소나 호스트 이름
- 사용자 개인 정보
스택 추적 정보
디버깅 정보(예: 스택 추적)는 프로덕션 환경에서 노출되지 않아야 한다. 이러한 정보는 로그에 기록하고, 클라이언트에게는 일반적인 오류 메시지를 제공해야 한다.
오류 메시지 최소화
오류 메시지는 필요한 정보만 포함해야 한다. 너무 상세한 오류 메시지는 공격자에게 유용한 정보를 제공할 수 있다.
RFC 7807과 HATEOAS 통합#
REST 성숙도 모델의 높은 수준에서는 HATEOAS(Hypertext As The Engine Of Application State)를 채택한다. RFC 7807을 HATEOAS와 통합하는 방법은 다음과 같다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| {
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "계정 잔액이 2300원으로, 요청된 3000원의 출금을 처리할 수 없습니다.",
"instance": "/account/12345/transactions/54321",
"balance": 2300,
"required": 3000,
"_links": {
"help": {
"href": "https://example.com/help/insufficient-funds"
},
"about": {
"href": "/account/12345/balance"
},
"deposit": {
"href": "/account/12345/deposit",
"method": "POST"
}
}
}
|
이 예시에서 _links
필드는 문제 해결을 위한 관련 작업에 대한 링크를 제공한다.
국제화(i18n) 지원#
RFC 7807은 다국어 지원을 위한 메커니즘을 직접 정의하지 않지만, HTTP의 기존 메커니즘을 활용할 수 있다:
Content-Language 헤더 사용#
1
2
3
4
5
6
7
8
9
| HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: ko
{
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
...
}
|
다국어 메시지 키 추가#
1
2
3
4
5
6
7
8
9
| {
"type": "https://example.com/probs/out-of-credit",
"title": "잔액 부족",
"status": 403,
"detail": "계정 잔액이 2300원으로, 요청된 3000원의 출금을 처리할 수 없습니다.",
"instance": "/account/12345/transactions/54321",
"title_key": "error.insufficient_funds.title",
"detail_key": "error.insufficient_funds.detail"
}
|
이 방식에서 title_key
와 detail_key
는 클라이언트 측 번역을 위한 키를 제공한다.
RFC 7807의 장단점#
- 표준화: 일관된 오류 형식으로 클라이언트 개발 간소화
- 자기 설명적: 오류 유형과 상세 정보를 명확히 전달
- 확장성: 특정 문제 유형에 맞는 추가 속성 정의 가능
- 미디어 타입 지원: JSON 및 XML 형식 지원
- HTTP와의 통합: 기존 HTTP 상태 코드 및 헤더와 자연스럽게 통합
- 다중 오류 처리 부재: 여러 오류를 처리하기 위한 표준 방식이 명확하지 않음
- 복잡성 증가: 간단한 API에서는 과도한 복잡성을 추가할 수 있음
- 구현 오버헤드: 기존 시스템에 통합하려면 추가 작업 필요
- 국제화 메커니즘 부재: 다국어 오류 메시지에 대한 표준 접근 방식 부재
- 채택 수준: 모든 API가 이 표준을 따르지는 않음
실제 적용 사례 및 모범 사례#
환경 | 적용 예시 |
---|
RESTful API | 인증 오류, 유효성 검사 실패, 비즈니스 로직 오류 등 |
gRPC-Gateway | HTTP 레이어에서 오류 메시지를 구조화할 때 사용 |
마이크로서비스 | API 간 표준화된 오류 전파 |
Swagger/OpenAPI | application/problem+json 을 에러 응답 스펙으로 정의 가능 |
GitHub API#
GitHub API는 RFC 7807과 유사한 오류 형식을 사용한다:
1
2
3
4
5
6
7
8
9
10
11
| {
"message": "Validation Failed",
"documentation_url": "https://docs.github.com/rest/reference/users#update-a-user",
"errors": [
{
"resource": "User",
"field": "email",
"code": "invalid"
}
]
}
|
이 형식은 정확히 RFC 7807을 따르지는 않지만, documentation_url
이 type
필드와 유사한 역할을 한다.
모범 사례#
RFC 7807을 효과적으로 구현하기 위한 모범 사례:
- 유의미한 유형 URI 사용: 단순히 식별자가 아닌, 문제에 대한 문서를 제공하는 URI 사용
- 명확한 제목과 상세 정보: 간결하면서도 구체적인 오류 메시지 제공
- 일관된 형식 유지: 모든 API 엔드포인트에서 동일한 오류 형식 사용
- 필요한 추가 정보 포함: 문제 해결에 도움이 되는 관련 정보 제공
- 민감한 정보 제외: 보안 취약점을 야기할 수 있는 내부 정보 노출 방지
- 오류 코드 체계 수립: 일관된 오류 코드 체계를 통해 오류 유형 쉽게 식별
- 문서화: 가능한 모든 오류 유형과 추가 속성에 대한 명확한 문서 제공
용어 정리#
참고 및 출처#