You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
현재 서버가 사용자 대면 에러 문구(detail)를 직접 정해 내려준다. ApiResponseBody 에 code 필드가 없고 detail(문구)이 유일한 계약이다.
문구를 서버가 소유하면 카피 변경마다 서버 배포가 필요하고, 클라가 화면·상황별로 다른 문구를 쓸 유연성이 없다.
문구 선택권을 클라로 넘긴다 — 서버는 code + HttpStatus 만 내리고, 사용자 대면 문구는 클라가 code 로 매핑한다. (내부적으로는 code + category 2-튜플, status 는 category 가 소유)
결정 (2026-07-14 확정 — 기존 "선결 결정 3개" 해소)
code 형식·관리 방식 — 숫자 코드(USER-001 형식). 전역 단일 enum 이 아니라 도메인별 *ErrorCode enum + 공통 ErrorCode 인터페이스로 관리한다(도메인 경계 유지 · 머지 충돌 최소화 · 접두사가 이미 네임스페이스라 번호 충돌 없음). 각 예외는 자기 ErrorCode 하나만 참조.
status·category 구조 — ErrorCode = code + category2-튜플. HttpStatus 를 ErrorCategory 로 이관(category→status 1:1 성립)하여 예외/ErrorCode 가 status 를 직접 들지 않는다. → 도메인 예외에서 HttpStatus 분리 검토 (HttpMappable → 핸들러 매핑) #181 "도메인 예외에서 HttpStatus 분리"를 이 작업이 함께 해소한다. 부수 교정: OAuthException.misconfigured 502→500(미설정은 상류가 아니라 우리 서버 잘못 → 500 이 정확. RETRYABLE·SERVER_ERROR fallback 문구가 동일해 사용자 체감 0).
detail 이관 방식 — 점진(expand → migrate → contract). code+detail 병행 → 도메인별 detail=null → 전역 detail 제거. 클라가 code 매핑을 갖추기 전 detail 이 사라지는 창이 없다.
USER-006 3분리 — 확정. invalidNickname(reason) → blank / tooLong / reserved 3코드. (실측: reason 은 임의 입력·LLM 원문이 아니라 User.validateNickname 이 넘기는 3개 고정 문구뿐)
성공 응답은 code 를 넣지 않는다 (에러 전용) — 성공(2xx)은 HTTP status + data 로 표현하고 code 는 null. code 는 에러 방출 경로에만 부여한다. 공개 사례 조사(아래 "근거" 섹션) 결과 표준·글로벌 대형 API 가 거의 예외 없이 이 방식이라 근거 기반으로 채택. uniform envelope 는 유지된다(성공 = code:null).
단계 (서브프로젝트)
SP0 — 전수조사 리프레시 + 코드 배정표(numbering 원장) 확정.
SP1 (인프라 · 전역 · 비파괴) — ErrorCode 인터페이스 · ErrorCategory 에 httpStatus 이관(+misconfigured 500) · ApiResponseBody 에 code: String? 추가 · 에러 방출 경로가 code emit · code 유니크/번호안정 메타테스트. 이 단계에선 detail 그대로, code 는 대부분 아직 null.
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 는 에러 전용".
표준은 에러 전용: 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) 유지하되, 이 사실을 기록해 둔다.
예외 17클래스 · 97팩토리. (일주일 전 19클래스/약109 → GeminiApiException·PageFetchException 이 PR embedded 파싱 경로 삭제, 원격 extractor 가 유일 경로 (이관 8단계) #700 로 제거됨. product 파싱이 원격 extractor 로 이관되며 두 예외가 소멸, 502 경계는 ProductExtractorException 2팩토리로 수렴)
동적 message 는 USER-006 단 1개(그마저 3개 고정 케이스라 3코드로 정적화 가능).
왜
detail)를 직접 정해 내려준다.ApiResponseBody에code필드가 없고detail(문구)이 유일한 계약이다.code+category2-튜플, status 는 category 가 소유)결정 (2026-07-14 확정 — 기존 "선결 결정 3개" 해소)
USER-001형식). 전역 단일 enum 이 아니라 도메인별*ErrorCodeenum + 공통ErrorCode인터페이스로 관리한다(도메인 경계 유지 · 머지 충돌 최소화 · 접두사가 이미 네임스페이스라 번호 충돌 없음). 각 예외는 자기ErrorCode하나만 참조.ErrorCode=code+category2-튜플. HttpStatus 를ErrorCategory로 이관(category→status 1:1 성립)하여 예외/ErrorCode 가 status 를 직접 들지 않는다. → 도메인 예외에서 HttpStatus 분리 검토 (HttpMappable → 핸들러 매핑) #181 "도메인 예외에서 HttpStatus 분리"를 이 작업이 함께 해소한다. 부수 교정:OAuthException.misconfigured502→500(미설정은 상류가 아니라 우리 서버 잘못 → 500 이 정확. RETRYABLE·SERVER_ERROR fallback 문구가 동일해 사용자 체감 0).invalidNickname(reason)→blank/tooLong/reserved3코드. (실측:reason은 임의 입력·LLM 원문이 아니라User.validateNickname이 넘기는 3개 고정 문구뿐)data로 표현하고code는null.code는 에러 방출 경로에만 부여한다. 공개 사례 조사(아래 "근거" 섹션) 결과 표준·글로벌 대형 API 가 거의 예외 없이 이 방식이라 근거 기반으로 채택. uniform envelope 는 유지된다(성공 =code:null).단계 (서브프로젝트)
ErrorCode인터페이스 ·ErrorCategory에 httpStatus 이관(+misconfigured500) ·ApiResponseBody에code: String?추가 · 에러 방출 경로가 code emit · code 유니크/번호안정 메타테스트. 이 단계에선 detail 그대로, code 는 대부분 아직 null.*ErrorCodeenum + 팩토리 code 배정 → 보강(USER-006 3분리 · fcm 예외 신설 검토 · OpenAPI*Api/*ApiExamples전수 갱신 · 메시지 톤 점검 ·NotificationException.invalidCursor문구 통일) → detail=null + 통합테스트$.detail→$.code.detail컨벤션 재작성.SP1 은 전역 계약을 바꾼다:
ApiResponseBody.code신설 ·ErrorCategory가 httpStatus 소유(예외에서 HttpStatus 제거) ·ErrorCode인터페이스 도입. 이후 모든 도메인이 자기 예외에 code 를 배정해야 한다(각 소유자 작업). detail 제거(SP3)는 전 도메인 code 전환 완료 후 일괄 진행하므로, product · wish · item · image 계열 소유자는 각자 SP2 에 해당하는 code 배정을 수행해야 한다.범위 (에러 방출 5개 경로 전부 code)
ApiResponseSecurityErrorHandlers) · generic(handleIllegalArgument/handleUnexpected)COMMON-UNAUTHORIZED등) 정의OperationExamples.add가 code 추출)·테스트 갱신, 클라 공유 카탈로그(code→message) export근거 — "성공 응답에 code 를 넣는가" 공개 사례 조사 (2026-07-14)
결정 5(성공은 에러 전용 code)의 근거. 실제 접근한 문서·직접 인용으로 검증.
결론: 표준·글로벌 대형 API 는 거의 예외 없이 "성공은 HTTP status(2xx)로만, body
code는 에러 전용".code), GitHub(errors[].code), Microsoft REST Guidelines(성공은 code 불요 명시), Google AIP-193(bodycode=HTTP status), Zalando(RFC 9457 Problem), Twilio, JSON:API(data/errors 배타), 토스페이먼츠·토스플레이스·네이버·카카오.ProblemDetail(에러 전용).{"ok":true/false}— 모든 응답을 HTTP 200으로 주는 구조라 body로 판정) · gRPC google.rpc.Code(OK=0— RPC라 HTTP status 없음) · 한국 Spring "공통 응답 래퍼" 관행(널리 쓰이나 비표준).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)
GeminiApiException·PageFetchException이 PR embedded 파싱 경로 삭제, 원격 extractor 가 유일 경로 (이관 8단계) #700 로 제거됨. product 파싱이 원격 extractor 로 이관되며 두 예외가 소멸, 502 경계는ProductExtractorException2팩토리로 수렴)