개요
HTTP Status Code만으로는 서비스 로직의 원인을 전달하기 부족함 400대와 500대는 네트워크 관점의 대분류 신호에 가깝고 실제로 필요한 것은 비즈니스 맥락의 구체적 사유임 결론은 단순함 HTTP Status는 대분류 신호로 두고 실제 의미와 추가 컨텍스트는 Response Body에 싣는 구조가 현실적 해법임
핵심 원칙
- HTTP Status는 큰 범주 신호등 역할
- 2xx 성공
- 4xx 클라이언트 오류
- 5xx 서버 오류
- 비즈니스 의미는 Response Body의 커스텀 에러 구조로 표현
- 클라이언트가 코드 기반으로 분기하고 UI를 결정할 수 있어야 함
- 운영 관측을 위해 traceId 제공
업계에서 검증된 기본 구조
아래 형태가 가장 보편적으로 쓰이는 패턴임
// HTTP 400
{
"success": false,
"code": "USER_ALREADY_EXISTS",
"message": "이미 가입된 이메일입니다.",
"traceId": "a1b2c3d4",
"timestamp": "2024-05-21T10:00:00Z"
}이점
- status는 대분류, code는 비즈니스 소분류
- 사람이 읽기 쉬운 문자열 코드가 숫자보다 디버깅 친화적임
- traceId로 로그 상관관계 추적 가능
Validation 에러는 배열로 구조화
여러 필드가 동시에 실패할 수 있으므로 필드 단위로 묶어 제공
// HTTP 400
{
"code": "INVALID_INPUT",
"message": "입력값이 올바르지 않음",
"errors": [
{ "field": "email", "value": "abc@", "reason": "이메일 형식 아님" },
{ "field": "age", "value": "-5", "reason": "0보다 커야 함" }
]
}프론트엔드가 각 필드에 에러를 매핑하기 쉬움
RFC 7807 요약
Problem Details for HTTP APIs 표준 스펙으로 에러 응답을 정형화하는 방법 제공 Spring Boot 3 계열은 ProblemDetail을 기본 제공하며 적용이 용이함 필수는 아니나 상호운용성과 문서화를 원활하게 해줌
// HTTP 403
{
"type": "https://api.example.com/errors/not-enough-credit",
"title": "Not Enough Credit",
"status": 403,
"detail": "Current balance is 30, item costs 50",
"instance": "/account/12345/tx/abc"
}실무에서는 필드를 축약하거나 내부 코드 체계와 혼합 사용하는 경우가 많음
빅테크 사례 관찰
- Google Cloud API는 status와 message, 세부 정보 배열로 구조화
- Meta Graph API는 type, code, subcode, trace id를 포함해 디버깅 친화성 확보
- 국내 주요 서비스들도 대체로 자체 에러 코드와 message 조합 사용
베스트 프랙티스 정리
- HTTP Status는 대분류로 통일
- 400 잘못된 요청
- 401 인증 필요
- 403 권한 없음
- 404 리소스 없음
- 409 상태 충돌
- 422 의미적 검증 실패
- 500 서버 오류
- Response Body 필드 권장
{
"success": false,
"code": "COUPON_ALREADY_USED",
"message": "이미 사용한 쿠폰임",
"details": [],
"traceId": "xyz123",
"timestamp": "2025-01-01T00:00:00Z"
}- 서버에 ErrorCode Enum 정의
export enum ErrorCode {
COMMON_INVALID_PARAMETER = "COMMON_INVALID_PARAMETER",
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
COUPON_EXPIRED = "COUPON_EXPIRED",
PERMISSION_DENIED = "PERMISSION_DENIED",
}효과
- 프론트 분기 단순화
- 디버깅과 핫픽스 속도 향상
- 조직 전반 에러 표현 일관성 유지
i18n 처리 전략
두 가지 중 하나로 결정하고 일관성 유지
- 서버가 Accept-Language 기반으로 message 현지화
- 클라이언트가 code를 로컬 메시지로 매핑해 렌더링
서버 현지화는 중앙집중 제어 용이, 클라이언트 매핑은 번역 배포 독립성 높음 팀의 배포 파이프라인과 책임 분리에 맞춰 선택
보안과 운영 관점 주의사항
- 내부 스택 트레이스, DB 원문 오류 메시지 노출 금지
- 클라이언트에는 안전한 수준의 요약 메시지 제공
- traceId로만 서버 로그와 상관관계 연결하도록 설계
- 실제 예외 상세는 서버 로그와 관측 시스템에서만 확인
마무리
HTTP Status만으로 비즈니스 맥락의 에러를 충분히 설명하기 어려움 대분류는 Status, 실제 의미는 Body의 코드와 메시지, 추가 컨텍스트는 details 또는 errors, 운영 추적은 traceId로 분리 설계 권장 이 패턴은 REST, GraphQL, gRPC 등 전송 방식과 무관하게 적용 가능하며 규모가 커질수록 가치가 커짐
참고자료
- https://www.rfc-editor.org/rfc/rfc7807
- https://cloud.google.com/apis/design/errors
- https://developers.facebook.com/docs/graph-api/using-graph-api/error-handling/
- https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/responseentity.html#page-title
- https://datatracker.ietf.org/doc/html/rfc9110#name-status-codes