Error Handling#
RESTful API의.오류 처리는 개발자 경험과 시스템 안정성에 중요한 영향을 미치는 핵심 요소이다.
오류 상황을 어떻게 다루고 전달하는지에 따라 API의 품질이 크게 달라질 수 있다.
HTTP 상태 코드의 올바른 활용#
HTTP 상태 코드는 클라이언트에게 요청 처리 결과를 전달하는 표준화된 방법이다.
오류 상황에서 적절한 상태 코드를 반환하는 것이 중요하다.
주요 오류 상태 코드 카테고리#
- 4xx (클라이언트 오류): 클라이언트 측의 문제로 인한 오류
- 400 Bad Request: 요청 구문이 잘못되었거나 유효하지 않은 요청
- 401 Unauthorized: 인증되지 않은 요청
- 403 Forbidden: 권한이 없는 요청
- 404 Not Found: 요청한 리소스를 찾을 수 없음
- 405 Method Not Allowed: 허용되지 않은 HTTP 메서드
- 409 Conflict: 리소스 상태와 충돌
- 415 Unsupported Media Type: 지원하지 않는 미디어 타입
- 422 Unprocessable Entity: 유효성 검사 실패
- 5xx (서버 오류): 서버 측의 문제로 인한 오류
- 500 Internal Server Error: 서버 내부 오류
- 502 Bad Gateway: 게이트웨이 오류
- 503 Service Unavailable: 서비스 일시적 사용 불가
- 504 Gateway Timeout: 게이트웨이 타임아웃
상태 코드를 선택할 때는 상황에 가장 정확하게 맞는 코드를 사용해야 한다. 예를 들어, 사용자 인증이 필요한 경우 403(Forbidden)이 아닌 401(Unauthorized)을 사용하는 것이 적절하다.
오류 응답 본문의 구조화#
상태 코드만으로는 오류에 대한 충분한 정보를 제공할 수 없다. 따라서 일관되고 유용한 오류 응답 본문을 구성하는 것이 중요하다.
일반적인 오류 응답 구조#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| {
"status": 400,
"code": "INVALID_PARAMETER",
"message": "입력한 매개변수가 유효하지 않습니다.",
"details": [
{
"field": "email",
"message": "유효한 이메일 형식이 아닙니다."
},
{
"field": "password",
"message": "비밀번호는 최소 8자 이상이어야 합니다."
}
],
"timestamp": "2023-03-22T13:45:30Z",
"requestId": "a1b2c3d4-e5f6-g7h8-i9j0"
}
|
주요 오류 응답 필드#
- status: HTTP 상태 코드
- code: 애플리케이션 내부에서 사용하는 오류 코드
- message: 사용자 친화적인 오류 메시지
- details: 오류에 대한 상세 정보
- timestamp: 오류 발생 시간
- requestId: 문제 추적을 위한 요청 식별자
이러한 구조는 개발자와 사용자 모두에게 유용한 정보를 제공한다. 특히 details
필드는 유효성 검사 오류와 같은 여러 문제가 있을 때 유용하다.
오류 코드 체계 수립#
애플리케이션별 오류 코드를 체계적으로 관리하면 문제 해결이 용이해진다.
오류 코드 설계 방식#
- 카테고리 기반:
AUTH_001
, VALIDATION_002
등 - 리소스 기반:
USER_NOT_FOUND
, PAYMENT_FAILED
등 - 계층 구조:
api.auth.invalid_token
, api.user.not_found
등
일관된 패턴을 사용하고 문서화하는 것이 중요하다.
오류 코드는 개발자가 문제를 빠르게 식별하고 디버깅할 수 있도록 도와준다.
국제화(i18n) 지원#
글로벌 서비스의 경우, 오류 메시지를 다양한 언어로 제공하는 것이 중요하다.
국제화 지원 방법#
클라이언트 주도 방식: 오류 코드만 전송하고 클라이언트에서 번역
1
2
3
4
5
| {
"status": 400,
"code": "INVALID_EMAIL",
"timestamp": "2023-03-22T13:45:30Z"
}
|
서버 주도 방식: 클라이언트의 언어 설정에 따라 서버에서 번역
1
2
3
4
5
6
| {
"status": 400,
"code": "INVALID_EMAIL",
"message": "유효한 이메일 주소를 입력해주세요.",
"timestamp": "2023-03-22T13:45:30Z"
}
|
하이브리드 방식: 기본 메시지와 함께 번역 키 제공
1
2
3
4
5
6
7
| {
"status": 400,
"code": "INVALID_EMAIL",
"message": "유효한 이메일 주소를 입력해주세요.",
"messageKey": "validation.email.invalid",
"timestamp": "2023-03-22T13:45:30Z"
}
|
API의 사용 맥락과 클라이언트 유형에 따라 적절한 방식을 선택한다.
5. 보안 고려사항#
오류 메시지는 보안에 영향을 줄 수 있으므로, 적절한 정보 노출 수준을 결정해야 한다.
오류 정보 노출 원칙#
- 공개 API: 세부적인 오류 메시지를 제공하되, 민감한 정보는 제외
- 내부 API: 디버깅을 위한 더 상세한 정보 제공 가능
- 프로덕션 환경: 스택 트레이스와 같은 내부 정보 노출 제한
- 개발 환경: 디버깅을 위한 세부 정보 제공
예를 들어, 데이터베이스 연결 오류의 경우 공개 API에서는 “서비스 일시적 오류"와 같은 일반적인 메시지를 반환하고, 내부적으로 로깅하는 것이 안전하다.
유효성 검사 오류 처리#
API 요청에 대한 유효성 검사는 흔한 오류 원인이므로, 이에 대한 특별한 처리가 필요하다.
효과적인 유효성 검사 오류 전달 방법#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| {
"status": 422,
"code": "VALIDATION_ERROR",
"message": "입력 데이터 유효성 검사에 실패했습니다.",
"details": [
{
"field": "username",
"value": "a",
"rule": "minLength",
"message": "사용자 이름은 최소 3자 이상이어야 합니다."
},
{
"field": "email",
"value": "invalid-email",
"rule": "format",
"message": "유효한 이메일 형식이 아닙니다."
}
]
}
|
이러한 구조적 응답은 클라이언트가 각 필드별 오류를 처리하기 쉽게 한다. 특히 폼 검증에 유용하다.
비즈니스 로직 오류 처리#
비즈니스 규칙 위반과 같은 애플리케이션별 오류도 명확하게 전달해야 한다.
비즈니스 오류 응답 예시#
1
2
3
4
5
6
7
8
9
10
| {
"status": 400,
"code": "INSUFFICIENT_FUNDS",
"message": "잔액이 부족합니다.",
"details": {
"availableBalance": 5000,
"requiredAmount": 10000,
"currency": "KRW"
}
}
|
비즈니스 오류의 경우, 문제 해결을 위한 구체적인 정보를 details
필드에 포함시키는 것이 유용하다.
멱등성과 재시도 처리#
네트워크 문제로 인한 재시도 시나리오를 고려한 오류 처리가 필요하다.
재시도 관련 오류 응답 예시#
1
2
3
4
5
6
7
8
9
10
| {
"status": 409,
"code": "DUPLICATE_REQUEST",
"message": "동일한 요청이 이미 처리되었습니다.",
"details": {
"requestId": "a1b2c3d4",
"completedAt": "2023-03-22T13:40:25Z",
"originalTransactionId": "txn_12345"
}
}
|
멱등성을 보장하는 API는 중복 요청을 감지하고, 이미 성공적으로 처리된 작업에 대한 정보를 제공해야 한다.
오류 로깅과 모니터링#
오류 처리는 클라이언트 응답뿐만 아니라 서버 측 로깅과 모니터링도 중요하다.
효과적인 오류 로깅 전략#
- 구조화된 로깅: JSON 형식의 로그 사용
- 상관관계 ID: 요청 전체 경로를 추적할 수 있는 ID 포함
- 컨텍스트 정보: 사용자, 요청 매개변수, 환경 정보 등 포함
- 심각도 수준: 오류의 중요도를 분류 (INFO, WARNING, ERROR, FATAL 등)
- 알림 설정: 중요 오류에 대한 즉시 알림 구성
로그와 클라이언트 응답 사이의 일관성을 유지하는 것이 디버깅에 도움이 된다.
문서화와 개발자 지원#
마지막으로, 오류 처리 방식을 명확하게 문서화하여 API 소비자가 예상할 수 있게 해야 한다.
오류 문서화 요소#
- 가능한 모든 오류 코드 목록
- 각 오류에 대한 설명 및 예시
- 문제 해결 가이드
- 오류 처리 모범 사례
OpenAPI(Swagger)와 같은 도구를 사용하여 오류 응답을 공식 API 문서의 일부로 포함시키는 것이 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # OpenAPI 예시
responses:
400:
description: 잘못된 요청
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
validation_error:
summary: 유효성 검사 오류
value:
status: 400
code: "VALIDATION_ERROR"
message: "입력 데이터 유효성 검사에 실패했습니다."
details: [...]
|
실제 구현 예시 (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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| // 오류 클래스 정의
class ApiError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
this.requestId = `req_${Math.random().toString(36).substring(2, 12)}`;
}
}
// 특정 오류 유형 정의
class ValidationError extends ApiError {
constructor(details) {
super(422, 'VALIDATION_ERROR', '입력 데이터 유효성 검사에 실패했습니다.', details);
}
}
// Express 미들웨어로 오류 처리
function errorHandler(err, req, res, next) {
console.error(err);
// API 오류 인스턴스인 경우
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
status: err.statusCode,
code: err.code,
message: err.message,
details: err.details,
timestamp: err.timestamp,
requestId: err.requestId
});
}
// 기타 처리되지 않은 오류
return res.status(500).json({
status: 500,
code: 'INTERNAL_ERROR',
message: '서버 내부 오류가 발생했습니다.',
timestamp: new Date().toISOString(),
requestId: `req_${Math.random().toString(36).substring(2, 12)}`
});
}
// 라우트 핸들러 예시
app.post('/users', async (req, res, next) => {
try {
// 유효성 검사
const errors = validateUser(req.body);
if (errors.length > 0) {
throw new ValidationError(errors);
}
// 비즈니스 로직
const user = await createUser(req.body);
// 성공 응답
res.status(201).json(user);
} catch (err) {
next(err); // 오류 핸들러로 전달
}
});
// 미들웨어 등록
app.use(errorHandler);
|
이 예시는 구조화된 오류 처리 방식을 보여주며, 다양한 오류 유형에 대해 일관된 응답을 생성한다.
용어 정리#
참고 및 출처#
RFC 9457 RFC 9457은 그 후속 버전으로, HTTP API의 오류 응답을 구조화된 형식으로 전달하기 위한 표준이다.
이 문서는 RFC 7807을 대체하며, 이전 버전에서의 경험과 피드백을 반영하여 몇 가지 중요한 개선사항을 도입했다.
RFC 9457의 주요 개선사항 RFC 9457은 RFC 7807을 기반으로 다음과 같은 주요 개선점을 포함하고 있다.
문제 유형(type) 필드의 명확화
기존: type 필드는 문제의 유형을 식별하는 URI로 사용되었지만, 그 사용 방식이 모호할 수 있다. 개선: type 필드의 사용을 명확히 정의하고, 공용 레지스트리를 통해 표준화된 문제 유형을 관리하도록 권장하고 있다. 여러 문제의 표현 지원
...
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마다 다른 오류 처리 로직을 구현해야 했다.
...