Files
llm-wiki/wiki/concepts/api-error-envelope-design.md

152 lines
12 KiB
Markdown

---
title: API Error Envelope 설계 (custom vs ProblemDetail vs rpc.Status)
source_type: llm-generated
status: draft
confidence: medium
tags: [api-design, error-handling, http]
related_projects: [ca-skeleton]
last_reviewed: 2026-05-22
---
# API Error Envelope 설계 (custom vs ProblemDetail vs rpc.Status)
> Layer: `wiki/concepts/` — 일반 개념. 특정 프로젝트의 결정/구현 사실은 `wiki/projects/`에서 다룬다.
## Summary
API error envelope은 실패 응답의 구조 계약이다. 표준 후보는 RFC 7807 ProblemDetail, Google `rpc.Status`, JSON:API errors, GraphQL errors가 있고, 그 외 대형 서비스의 custom envelope (Stripe / GitHub / 토스페이먼츠 등)이 사실상 진영별 컨벤션으로 자리잡았다. 설계 결정의 핵심 축은 (a) 성공/실패 응답의 대칭 여부, (b) `code` · `category` · `retryable` 같은 운영 메타데이터의 1급 필드 승격 여부, (c) 표준 lock-in과 client SDK 호환성의 trade-off다.
## Standard (공식 정의)
### RFC 7807 ProblemDetail (실패 전용 평면)
IETF 표준. `application/problem+json` media type. 필드: `type` (URI), `title`, `status`, `detail`, `instance`. 모든 필드 optional이고 확장은 top-level에 임의 필드 추가로 한다. RFC 9457로 obsolete되었지만 의미상 호환이며, Spring 6+는 `ProblemDetail` 클래스로 기본 지원한다. 성공 응답에는 적용되지 않고 실패 전용 평면 shape이다.
### Google `rpc.Status` (gRPC, typed details)
Google AIP-193. `code` (정수, `google.rpc.Code` enum), `message`, `details: Any[]`. `details``google.protobuf.Any`로 packing되며 표준 detail 타입(`ErrorInfo`, `LocalizedMessage`, `Help`, `RetryInfo`, `QuotaFailure`, `BadRequest`)을 포함한다. `RetryInfo`로 retryable + delay까지 표준화되어 있다. REST/gRPC 양쪽에 동일 모델로 매핑된다.
### JSON:API errors (배열)
JSON:API v1.1 spec. top-level에 `errors: []` array 필수. 각 error 객체는 `id`, `links`, `status`, `code`, `title`, `detail`, `source.pointer` (JSON Pointer), `meta` 중 하나 이상을 가진다. `source.pointer`로 form 필드 단위 오류를 가리킨다.
### GraphQL errors (HTTP 200 + errors field)
GraphQL Specification (October 2021) §7.1.2. 응답은 `data``errors`를 모두 가질 수 있고, error 객체는 `message` (required), `locations`, `path`, `extensions`를 가진다. transport는 보통 HTTP 200이고 4xx/5xx는 transport-level 실패에만 사용한다.
### 진영별 custom envelope (표준 아님)
- **Stripe**: `{ error.{ type, code, decline_code, message, param, doc_url, ... } }`. `type` enum이 사실상 category 역할.
- **GitHub**: `{ message, documentation_url, errors[].{ resource, field, code } }`. validation 항목별 풀이가 명시적.
- **토스페이먼츠**: `{ code, message }`. 가장 얇은 envelope. retryable/category는 `code` semantic으로 추론.
이 세 사례는 어떤 IETF/W3C 표준도 따르지 않으며, 각 회사 SDK가 envelope을 흡수하는 전제로 동작한다.
## 한계 / 주의점
### Custom envelope
- 외부 표준이 존재하지 않으므로 client SDK를 직접 작성하거나 envelope 처리 규칙을 client에게 명시적으로 전달해야 한다.
- 성공/실패 대칭, `retryable` 1급 같은 운영 친화 결정을 자유롭게 둘 수 있지만 그 비용은 "표준 client 라이브러리 0개"다.
### RFC 7807 ProblemDetail
- 실패 전용 평면 shape이므로 "성공도 envelope으로 감싸 `success: true/false`로 분기하고 싶다"는 요구와 구조적으로 충돌한다.
- `code` 필드가 표준에 없다 — `type` URI가 식별자다. 짧은 머신리더블 코드를 원하면 확장 필드를 강제해야 하고, 결국 "표준 위에 사실상 custom 레이어"가 된다.
- Spring 6+는 기본 활성이므로, custom envelope을 채택한다는 것은 의식적으로 표준 인프라를 비활성화하는 선택이다.
- `application/problem+json`을 content-negotiation으로 처리하는 client는 흔하지 않다 — 실질 호환성 이득은 명목 수준에 가깝다.
### Google `rpc.Status`
- 본질적으로 gRPC/protobuf 생태계 결합이다. HTTP REST 전용 서비스에 강제하면 `Any` 디코딩 부담이 client에 mismatch로 전가된다.
- 표준 detail 타입 카탈로그를 알아야 효용이 발휘되어 학습 곡선이 높다.
- 가벼운 CRUD API에는 과한 표현력이다.
### JSON:API errors
- `errors[]` array와 `source.pointer`는 항목 단위 오류 표현에 강하지만, `category`/`retryable`이 1급 필드가 아니라 `meta`로 빠진다.
- 부분 채택 시 표준성이 사라진다. 완전 채택 시 success response 리소스 객체 구조, sparse fieldsets 등 spec 전체에 lock-in된다.
### GraphQL errors
- HTTP 200 + `errors` field가 transport 규약이라 CDN / proxy / observability 도구의 4xx/5xx 기반 알람·캐시·라우팅과 부조화한다.
- partial success가 1급 개념이라 REST envelope과 패러다임 자체가 다르다 — REST 컨텍스트에서 직접 비교해 "GraphQL이 옳다/그르다"라고 말할 수 없다.
### 흔한 오해
- "Stripe / GitHub / 토스페이먼츠가 그렇게 하니까 industry standard다" — 표준이 아니라 진영별 컨벤션이다. SDK 없이 직접 다루는 client는 거의 없다는 전제 위에서 동작한다.
- "ProblemDetail은 잘못된 설계다" — 실패 전용 use case (예: 외부 노출 API, RFC 9457 client 생태계 활용)에서는 유효한 선택이다.
## Project Application
- [[wiki/projects/ca-tmpl/api-error-envelope-design]] — ca-tmpl 의사결정 기록 (`verified` — envelope record/handler 코드 구현 + `./gradlew check` 로컬 통과). 실제 구현 범위·검증 수준은 project 문서 참조.
- [[raw/project-notes/ca-skeleton-operational-contract]] — §3 Structured API Response Contract / §5 Exception Ownership Contract / §6 Operational Error Category / §29 Topic 4 (custom envelope 결정 라인업)
- [[raw/branch-notes/feature-operational-error-observability-foundation]] — envelope schema SSOT
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — validation error → `error.details` 매핑
- [[raw/branch-notes/feature-business-rule-validation-contract]] — business invariant → category 매핑
위 branch-note들은 success / error 대칭, `error.code` · `error.category` · `error.retryable` · `error.details` 분리, `meta.requestId` / `meta.traceId` / `meta.correlationId` 1급 노출, raw exception / SQL / token / body의 응답 leak 금지를 계약으로 둔다.
## Claim-backed Knowledge
> 이 개념 문서의 핵심 설명은 raw source claim 으로 뒷받침되어야 한다.
> 공식 문서 claim, 회사 사례 claim, 내 프로젝트 decision 을 분리한다.
| Knowledge Point | Supporting Claims | Confidence | Notes |
|---|---|---|---|
| RFC 7807 ProblemDetail은 `application/problem+json` 기반 실패 전용 평면 shape이며 `type` URI가 식별자다 (`code` 필드 없음) | [[raw/official-docs/problem-detail-rfc-7807]], [[raw/official-docs/spring-problem-detail]] | `high` | 공식 표준 (IETF / Spring) — success/error 대칭·머신리더블 `code` 요구와 구조적으로 충돌 |
| Google `rpc.Status``RetryInfo` 등 typed detail로 retryable + delay까지 표준화 (REST/gRPC 공통 모델) | [[raw/official-docs/google-api-error-format]] | `high` | 공식 vendor 문서(AIP-193) — 단 protobuf/`Any` 결합이라 HTTP REST 전용에는 과한 표현력 |
| JSON:API는 `errors[]` + `source.pointer`(JSON Pointer)로 항목 단위 오류를 가리키지만 `category`/`retryable`이 1급 필드가 아니다 | [[raw/official-docs/json-api-errors-spec]] | `high` | 공식 표준 — 부분 채택 시 표준성 상실, 완전 채택 시 spec 전체 lock-in |
| Stripe/GitHub/토스페이먼츠 envelope은 IETF/W3C 표준이 아니라 진영별 컨벤션이며 각 사 SDK가 envelope을 흡수하는 전제로 동작한다 | [[raw/company-tech-blogs/stripe-error-format]], [[raw/company-tech-blogs/github-api-error-format]], [[raw/company-tech-blogs/toss-payments-error-format]] | `medium` | company-case-study — 공식 best practice로 일반화 금지. SDK 부재 client는 거의 없다는 전제 |
## 내가 설명할 수 있어야 하는 것
- API error envelope의 후보 표준(RFC 7807 / Google `rpc.Status` / JSON:API / GraphQL errors)의 공식 정의와 각자의 식별자 표현 방식은?
- 어떤 문제를 해결하는가 — client가 실패를 어떻게 분기·재시도·관측 가능하게 만드는 구조 계약인가?
- 어떤 상황에서는 custom envelope을 쓰면 안 되는가(표준 client 생태계 활용이 우선인 외부 노출 API 등)?
- 공식 표준이 말하지 않는 부분(success/error 대칭, `retryable`·`category` 1급화)은 무엇이고 그 비용("표준 client 라이브러리 0개")은 무엇인가?
- Stripe/GitHub/토스 사례를 industry standard처럼 일반화하면 안 되는 지점은?
- 내 프로젝트에서는 어떤 branch decision(custom envelope 채택 + ProblemDetail 거부)으로 연결됐는가?
- 이 개념을 코드/운영에서 검증하려면 무엇을 확인해야 하는가(envelope 직렬화, leak 금지, ProblemDetail 비활성 build-time 강제 등)?
## Interview Questions
- 왜 RFC 7807 ProblemDetail을 채택하지 않았는지? 표준을 우회한 비용은 무엇이고, 그 대신 무엇을 얻는지?
- `retryable`을 1급 필드로 둔 이유는? client는 `retryable: true`를 받았을 때 어떻게 다르게 동작해야 하는지?
- validation error를 `error.details`에 담을 때 GitHub `errors[].{resource, field, code}` 또는 JSON:API `source.pointer`와 비교하면 어떤 형식을 택했고, 왜 그렇게 택했는지?
- `error.code``error.category`를 분리한 이유는? client 분기는 어느 쪽으로 하라고 가이드하는지?
- 응답에 절대 leak하면 안 되는 항목은? exception class name, stack trace, SQL, token, raw body, upstream raw error body 각각이 왜 금지인지 설명할 수 있는지?
## Do Not Overclaim
- "내 envelope이 표준이다" / "ca-tmpl envelope이 IETF 표준 envelope이다"라고 말하면 안 된다. 어떤 표준도 success/error 대칭 + `retryable` 1급 + `category` 1급을 동시에 강제하지 않는다 — 자체 결정일 뿐이다.
- "ProblemDetail은 잘못된 설계다"라고 단정하면 안 된다. 실패 전용 평면이라는 그 자체가 결함이 아니며, 외부 표준 client 호환을 우선하는 use case에서는 합리적이다.
- "Stripe / GitHub / 토스가 다 custom이니까 표준은 의미 없다"라고 말하면 안 된다. 그들은 SDK가 envelope을 흡수하는 전제 위에 동작하며, 표준 미준수가 정당화되는 것이 아니라 trade-off가 다른 것뿐이다.
- Google `rpc.Status``RetryInfo.retry_delay`보다 `retryable: boolean`이 우월하다고 주장하면 안 된다 — 후자는 단순하지만 actionable한 delay 정보를 잃는다.
## Sources
### 공식 표준
- [[raw/official-docs/problem-detail-rfc-7807]] — RFC 7807 (Problem Details for HTTP APIs)
- [[raw/official-docs/spring-problem-detail]] — Spring Framework `ProblemDetail` (RFC 9457 기본 지원)
- [[raw/official-docs/google-api-error-format]] — Google AIP-193, `google.rpc.Status`
- [[raw/official-docs/json-api-errors-spec]] — JSON:API v1.1 Errors
- [[raw/official-docs/graphql-errors-spec]] — GraphQL Specification (October 2021) Errors
### 진영별 사례 (표준 아님)
- [[raw/company-tech-blogs/stripe-error-format]] — Stripe custom envelope
- [[raw/company-tech-blogs/github-api-error-format]] — GitHub REST API error format
- [[raw/company-tech-blogs/toss-payments-error-format]] — 토스페이먼츠 `{code, message}`
### Canonical (프로젝트 결정 사실)
- [[raw/project-notes/ca-skeleton-operational-contract]] §3 / §5 / §6 / §29 Topic 4
## Cluster / 묶음
<!-- GENERATED: derived-blogs:start -->
- [[wiki/blog/ca-tmpl-api-error-envelope-design-2026-07-02]]
<!-- GENERATED: derived-blogs:end -->