Skip to content

에러 응답을 message 기반에서 code 기반으로 전환 #728

Description

@sevineleven

  • 현재 서버가 사용자 대면 에러 문구(detail)를 직접 정해 내려준다. ApiResponseBodycode 필드가 없고 detail(문구)이 유일한 계약이다.
  • 문구를 서버가 소유하면 카피 변경마다 서버 배포가 필요하고, 클라가 화면·상황별로 다른 문구를 쓸 유연성이 없다.
  • 문구 선택권을 클라로 넘긴다 — 서버는 code + HttpStatus 만 내리고, 사용자 대면 문구는 클라가 code 로 매핑한다. (내부적으로는 code + category 2-튜플, status 는 category 가 소유)

결정 (2026-07-14 확정 — 기존 "선결 결정 3개" 해소)

  1. code 형식·관리 방식 — 숫자 코드(USER-001 형식). 전역 단일 enum 이 아니라 도메인별 *ErrorCode enum + 공통 ErrorCode 인터페이스로 관리한다(도메인 경계 유지 · 머지 충돌 최소화 · 접두사가 이미 네임스페이스라 번호 충돌 없음). 각 예외는 자기 ErrorCode 하나만 참조.
  2. status·category 구조ErrorCode = code + category 2-튜플. HttpStatus 를 ErrorCategory 로 이관(category→status 1:1 성립)하여 예외/ErrorCode 가 status 를 직접 들지 않는다. → 도메인 예외에서 HttpStatus 분리 검토 (HttpMappable → 핸들러 매핑) #181 "도메인 예외에서 HttpStatus 분리"를 이 작업이 함께 해소한다. 부수 교정: OAuthException.misconfigured 502→500(미설정은 상류가 아니라 우리 서버 잘못 → 500 이 정확. RETRYABLE·SERVER_ERROR fallback 문구가 동일해 사용자 체감 0).
  3. detail 이관 방식 — 점진(expand → migrate → contract). code+detail 병행 → 도메인별 detail=null → 전역 detail 제거. 클라가 code 매핑을 갖추기 전 detail 이 사라지는 창이 없다.
  4. USER-006 3분리 — 확정. invalidNickname(reason)blank / tooLong / reserved 3코드. (실측: reason 은 임의 입력·LLM 원문이 아니라 User.validateNickname 이 넘기는 3개 고정 문구뿐)
  5. 성공 응답은 code 를 넣지 않는다 (에러 전용) — 성공(2xx)은 HTTP status + data 로 표현하고 codenull. code 는 에러 방출 경로에만 부여한다. 공개 사례 조사(아래 "근거" 섹션) 결과 표준·글로벌 대형 API 가 거의 예외 없이 이 방식이라 근거 기반으로 채택. uniform envelope 는 유지된다(성공 = code:null).

단계 (서브프로젝트)

  • SP0 — 전수조사 리프레시 + 코드 배정표(numbering 원장) 확정.
  • SP1 (인프라 · 전역 · 비파괴)ErrorCode 인터페이스 · ErrorCategory 에 httpStatus 이관(+misconfigured 500) · ApiResponseBodycode: String? 추가 · 에러 방출 경로가 code emit · code 유니크/번호안정 메타테스트. 이 단계에선 detail 그대로, code 는 대부분 아직 null.
  • SP2 (도메인별 마이그레이션 + 보강)@sevineleven 소유 도메인(user · auth · tournament · announcement · notification · fcm) 각각: *ErrorCode enum + 팩토리 code 배정 → 보강(USER-006 3분리 · fcm 예외 신설 검토 · OpenAPI *Api/*ApiExamples 전수 갱신 · 메시지 톤 점검 · NotificationException.invalidCursor 문구 통일) → detail=null + 통합테스트 $.detail$.code.
  • SP3 (contract · 팀 전체 후행) — 전 도메인(타 팀 소유 포함)이 code 로 전환된 뒤 detail 필드 제거 + CLAUDE.md detail 컨벤션 재작성.

⚠️ 전역 변경 — 타 도메인 소유자 주의

SP1 은 전역 계약을 바꾼다: ApiResponseBody.code 신설 · ErrorCategory 가 httpStatus 소유(예외에서 HttpStatus 제거) · ErrorCode 인터페이스 도입. 이후 모든 도메인이 자기 예외에 code 를 배정해야 한다(각 소유자 작업). detail 제거(SP3)는 전 도메인 code 전환 완료 후 일괄 진행하므로, product · wish · item · image 계열 소유자는 각자 SP2 에 해당하는 code 배정을 수행해야 한다.

범위 (에러 방출 5개 경로 전부 code)

  • 도메인 예외 17클래스 · 약 97팩토리(HttpMappable) · Bean Validation 400 · Spring 표준 MVC · Security 401/403(ApiResponseSecurityErrorHandlers) · generic(handleIllegalArgument/handleUnexpected)
  • 문구 없는 경로용 공통 코드(COMMON-UNAUTHORIZED 등) 정의
  • examples/OpenAPI(OperationExamples.add 가 code 추출)·테스트 갱신, 클라 공유 카탈로그(code→message) export

근거 — "성공 응답에 code 를 넣는가" 공개 사례 조사 (2026-07-14)

결정 5(성공은 에러 전용 code)의 근거. 실제 접근한 문서·직접 인용으로 검증.

결론: 표준·글로벌 대형 API 는 거의 예외 없이 "성공은 HTTP status(2xx)로만, body code 는 에러 전용".

  • 성공에 code 없음(에러 전용): Stripe(error object에만 nullable code), GitHub(errors[].code), Microsoft REST Guidelines(성공은 code 불요 명시), Google AIP-193(body code=HTTP status), Zalando(RFC 9457 Problem), Twilio, JSON:API(data/errors 배타), 토스페이먼츠·토스플레이스·네이버·카카오.
  • 표준은 에러 전용: RFC 9457 Problem Details(이름부터 problem, 성공 미규정) · RFC 9110(성공은 status line 2xx) · Spring ProblemDetail(에러 전용).
  • 성공에도 code 넣는 사례(소수·특수): Slack({"ok":true/false} — 모든 응답을 HTTP 200으로 주는 구조라 body로 판정) · gRPC google.rpc.Code(OK=0 — RPC라 HTTP status 없음) · 한국 Spring "공통 응답 래퍼" 관행(널리 쓰이나 비표준).
  • 부수(에러 코드 형식): 문자열 슬러그(Stripe card_declined·GitHub·Microsoft·토스페이먼츠 ALREADY_PROCESSED_PAYMENT)가 글로벌 주류, 숫자는 카카오(음수)·네이버·Twilio. 도메인 prefix 를 명시적으로 쓴 공식 사례는 드묾. → 본 작업은 숫자 prefix(USER-001) 유지하되, 이 사실을 기록해 둔다.

출처: docs.stripe.com/api/errors · docs.github.com REST troubleshooting · google.aip.dev/193 · opensource.zalando.com/restful-api-guidelines · rfc-editor.org/rfc/rfc9457 · rfc-editor.org/rfc/rfc9110 · docs.spring.io ProblemDetail · docs.tosspayments.com/reference/error-codes · naver.github.io/naver-openapi-guide/errorcode.html · grpc.io/docs/guides/status-codes · github.com/googleapis/googleapis google/rpc/code.proto

전수조사 (실측, 2026-07-14)

  • 예외 17클래스 · 97팩토리. (일주일 전 19클래스/약109 → GeminiApiException·PageFetchException 이 PR embedded 파싱 경로 삭제, 원격 extractor 가 유일 경로 (이관 8단계) #700 로 제거됨. product 파싱이 원격 extractor 로 이관되며 두 예외가 소멸, 502 경계는 ProductExtractorException 2팩토리로 수렴)
  • 동적 message 는 USER-006 단 1개(그마저 3개 고정 케이스라 3코드로 정적화 가능).
  • @sevineleven 소유: user · auth(Auth·OAuth·Apple) · tournament · announcement(Announcement·AnnouncementImage) · notification · fcm(현재 커스텀 예외 0개).

Metadata

Metadata

Assignees

Labels

epic큰 기능 단위 이니셔티브

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions