장애가 났을 때 사용자에게 500 Internal Server Error만 보이고 운영자는 긴 로그에서 원인을 찾으며, 고객 지원은 요청 시각을 다시 묻는 흐름은 생각보다 비쌉니다. 반대로 예외 메시지와 SQL, 고객 입력을 그대로 응답에 넣으면 디버깅은 쉬워 보여도 개인정보와 내부 구조를 외부로 흘립니다. 좋은 오류 처리는 “자세히 보여 줄까, 숨길까”의 선택이 아니라 누가 어떤 근거로 다음 행동을 할 수 있는가를 나누는 설계입니다.

이 글은 API 오류 의미와 재시도 계약, 관측 데이터의 분류·마스킹, Trace Sampling 정책, 계정 복구의 증거와 지원자 override를 연결합니다. 핵심은 오류 응답 하나를 만들면서도 사용자 경험, 재시도 안전성, 지원 효율, 보안 경계를 함께 유지하는 것입니다.

이 글에서 얻는 것

  • 클라이언트 응답, 애플리케이션 로그, trace, 지원 도구에 각각 어느 수준의 오류 정보를 둘지 정할 수 있습니다.
  • request_id, trace_id, support_ref를 같은 값으로 뭉개지 않고 조회 목적과 노출 범위에 맞춰 연결할 수 있습니다.
  • timeout·부분 성공·입력 오류를 재시도 가능 여부와 함께 전달해 클라이언트의 무한 재시도를 줄일 수 있습니다.
  • 지원 조회에 최소 권한, 시간 제한, 감사 이력을 붙여 “로그를 보여 주는 운영”을 줄일 수 있습니다.

핵심 개념/이슈

1) 오류에는 세 개의 독자가 있다

같은 실패라도 클라이언트, on-call 운영자, 고객 지원 담당자가 필요한 정보는 다릅니다. 클라이언트는 수정할 입력, 다시 시도해도 되는지, 언제 재시도할지를 알아야 합니다. 운영자는 실패한 dependency, 배포 revision, trace, 영향 범위를 보고 복구해야 합니다. 지원 담당자는 고객이 제시한 참조 번호로 상태를 찾되, 다른 고객의 요청 내용과 내부 예외를 보지 않아야 합니다.

따라서 오류 정보를 한 JSON에 모두 넣지 않습니다. 외부 응답은 안정적인 code, 사람이 읽을 짧은 message, 안전한 support_ref, 필요할 때의 retry_after_seconds까지만 둡니다. 내부 이벤트에는 route, dependency, 배포 버전, 정제된 stack trace와 trace link를 남깁니다. 지원 화면은 support_ref로 상태·발생 시각·다음 안내만 조회하게 하고, 원문 payload와 token은 별도 권한 없이는 열지 않게 합니다.

독자반드시 필요한 정보기본적으로 제외할 정보성공 기준
API client안정적 code, 수정 가능 항목, 재시도 규칙stack trace, SQL, 타 사용자 식별자앱이 추측 없이 다음 행동을 선택
on-callrequest/trace 연결, dependency 상태, 배포·설정 revision원문 credential, 불필요한 PII15분 안에 영향 범위를 좁힘
고객 지원support reference, 사용자·조직 범위의 상태, 안내 문구전체 로그, 내부 호스트명, 다른 tenant 정보본인 확인 뒤 재현 없이 상태 안내

이 분리는 기록을 세 번 복사하자는 뜻이 아닙니다. 한 번 발생한 오류 이벤트를 안전한 식별자로 연결하되, 각 UI와 API가 읽을 필드만 다르게 만드는 것입니다. 로그 저장소가 강한 권한을 가졌다는 사실은 고객 응답이 같은 권한을 가져도 된다는 근거가 아닙니다.

2) 진단 ID는 비밀번호도, 만능 열쇠도 아니다

request_id는 ingress에서 요청 단위를 묶는 관측용 ID입니다. trace_id는 서비스·큐·외부 호출을 넘나드는 trace의 상관관계 키입니다. 둘은 high-cardinality 진단 데이터이며 보통 외부 사용자에게 의미 있는 조치 권한을 주지 않습니다. 반면 support_ref는 사용자가 지원 채널에 전달할 수 있는 공개용 참조 번호입니다. 이 값만으로 상세 데이터를 열어 주면 안 됩니다.

안전한 흐름은 support_ref → error_event_id → request_id/trace_id의 단방향 조회입니다. 지원 도구는 로그인한 담당자의 조직·역할·목적을 먼저 확인하고, 필요한 경우에만 내부 이벤트를 조회합니다. URL에 trace ID를 넣거나 support reference를 데이터베이스 primary key처럼 재사용하면 브라우저 기록, 채팅, analytics에 진단 식별자가 퍼지기 쉽습니다. 외부 참조는 충분한 난수성을 갖되 권한 증표가 아닌 검색 보조 키로 취급하세요.

동일 오류가 급증할 때는 ID를 많이 발급하는 것보다 grouping이 중요합니다. error_code + route_template + dependency + release_revision으로 fingerprint를 만들고, 5분 창에 같은 fingerprint가 50건을 넘으면 incident 후보로 묶습니다. 단, email·주문 번호·실제 URL path처럼 개인 데이터를 fingerprint 재료로 넣지 않습니다. 진단 키의 유용성은 식별자의 길이가 아니라 안전하게 집계하고 다시 찾을 수 있는지에 달려 있습니다.

3) 오류 code는 상태 코드의 별칭이 아니라 다음 행동의 계약이다

HTTP 400, 409, 429, 503만으로는 충분하지 않습니다. 409는 이미 완료된 멱등 요청일 수도 있고, 수정하려던 revision이 오래된 충돌일 수도 있습니다. 503도 read 요청이면 제한된 backoff가 가능하지만, 결제·메일·권한 변경처럼 외부 효과가 있는 write라면 먼저 결과를 확인해야 합니다. 멱등 키의 보존·재사용·재처리 경계처럼 timeout 뒤에는 실패가 아니라 uncertain 상태가 남을 수 있습니다.

외부 응답에는 최소한 code, retryable, retry_after_seconds, support_ref를 명시합니다. 입력 검증 오류에는 필드 이름과 검증 rule ID까지만 주고 입력값 자체를 되비추지 않습니다. rate limit에는 계정 존재 여부를 추론하게 하는 세부 quota를 공개하지 말고, 인증된 principal에 한해 안전한 재시도 시각을 제공합니다. write timeout에는 retryable: false와 조회 방법을 주어 자동 재전송을 막습니다.

{
  "code": "PAYMENT_STATUS_UNKNOWN",
  "message": "처리 결과를 확인하고 있습니다. 같은 요청을 다시 보내지 마세요.",
  "retryable": false,
  "support_ref": "sup_01JQ7A...",
  "next_action": "poll_operation"
}

message는 사용자 안내이고, code와 next_action은 프로그램 계약입니다. 운영자가 문구를 고쳤다고 클라이언트 재시도 규칙이 바뀌면 안 되므로, 문자열 파싱으로 분기하는 SDK도 피해야 합니다.

실무 적용

1) 오류 envelope와 내부 사건 기록을 분리해 도입한다

처음부터 전 API를 바꾸지 말고 최근 30일 기준으로 5xx 비율이 높거나, 지원 문의가 월 10건 이상이거나, 외부 side effect가 있는 endpoint 3~5개를 고릅니다. 각 endpoint에서 아래 두 산출물을 분리합니다.

# 외부 API 응답
code: ORDER_REVISION_CONFLICT
retryable: false
support_ref: sup_01JQ7A...

# 내부 error event
event_id: err_01JQ7B...
request_id: req_01JQ79...
trace_id: 9fe0...
tenant_hash: sha256:...
error_fingerprint: ORDER_REVISION_CONFLICT|PATCH:/orders/{id}|v2026.10.31
redaction_version: 4

tenant_hash는 aggregation·격리 확인용이며 원래 tenant ID를 대체하는 보안 기능은 아닙니다. 내부 데이터도 조회 권한과 보존 기간이 필요합니다. request body 전체를 “나중에 도움이 될지 모른다”는 이유로 error event에 넣는 대신, allowlist한 field_name, 길이, 타입, hash, upstream status처럼 진단에 필요한 최소 사실만 남깁니다.

2) 지원 조회는 읽기 전용 기본값에서 시작한다

지원 도구의 첫 화면은 support_ref, 발생 시각, 고객이 인증된 조직 범위, 상태, 안전한 안내 문구만 보여 줍니다. 원본 로그 링크, PII가 포함된 속성, 재처리 버튼은 다른 권한과 사유 입력을 요구합니다. 조회가 필요할 때도 지원 담당자가 trace 탐색 도구를 자유롭게 검색하게 하기보다, 해당 support reference로부터 허용된 event만 열도록 만드세요.

시작 기준으로는 일반 지원 조회 세션을 15분, 민감정보가 포함된 확장 조회를 5분으로 제한하고, 확장 조회에는 case ID와 사유를 남기는 편이 안전합니다. 하루에 20회 이상 확장 조회가 필요한 오류라면 지원 교육 문제가 아니라 외부 오류 문구·상태 API·운영 대시보드가 부족하다는 신호입니다. 조회 성공 여부뿐 아니라 support_lookup_denied_total, elevated_lookup_total, redaction_match_total을 운영 지표로 둡니다.

3) 품질 지표를 “로그가 많다”가 아니라 복구 시간으로 잡는다

오류 ID를 넣고도 on-call이 매번 로그를 수동 검색한다면 설계가 끝난 것이 아닙니다. 다음처럼 작고 검증 가능한 기준을 세우세요.

신호초기 목표기준 초과 시 행동
5xx 중 support_ref 연결 비율99.5% 이상middleware·async consumer 누락 점검
오류 후 trace 연결 성공률99% 이상propagation·sampling 규칙 검토
P1/P2의 원인 fingerprint 도출 시간15분 이하release·dependency tag 추가
응답·로그의 redaction 탐지0건배포 중단, 샘플 격리·정책 보완
동일 support ref의 반복 문의주간 감소 추세안내 문구·상태 조회 UX 개선

trace를 무조건 100% 보존하는 것도 답은 아닙니다. 4xx 정상 입력 오류는 1~5% sample로 시작하고, 5xx·권한 거부 급증·결제처럼 고위험 흐름은 tail sampling으로 보존합니다. 다만 support reference가 찍힌 오류를 sample에서 버리면 지원 연결이 끊길 수 있으므로, 참조 발급 이벤트와 최소 진단 메타데이터는 별도 저비용 ledger에 남깁니다.

트레이드오프/주의점

상세 오류는 개발 속도를 높이지만 공격자에게 endpoint·라이브러리·데이터 존재 여부를 알려 줄 수 있습니다. 반대로 모든 오류를 “잠시 후 다시 시도하세요”로 통일하면 고객은 반복 제출하고 운영자는 원인을 재현하느라 시간을 씁니다. 해결책은 모호함을 늘리는 것이 아니라, 외부에는 안전한 code와 다음 행동을, 내부에는 접근 통제가 된 진단 근거를 주는 것입니다.

식별자를 마스킹했다고 안전하다고 단정해서도 안 됩니다. 드문 order 상태, 정확한 시각, 특정 route의 조합은 재식별 단서가 될 수 있습니다. redaction은 정규식 한 번이 아니라 필드 분류, allowlist, 저장 전 검사, 접근 제어, 보존 만료, 샘플 감사가 묶인 통제입니다. 특히 support reference를 URL parameter로 넣는다면 referrer policy와 외부 analytics 전송을 함께 검토해야 합니다.

체크리스트 또는 연습

  • 외부 오류, 내부 event, 지원 화면의 필드 allowlist가 각각 문서화돼 있다.
  • support_ref만으로 고객 데이터·재처리 기능에 접근할 수 없고, 조직 범위와 역할을 다시 확인한다.
  • 오류 code별로 retryable 여부와 uncertain일 때의 상태 조회 경로가 있다.
  • request·trace·support reference는 목적별로 분리돼 있고, URL·analytics로 무심코 전송되지 않는다.
  • 5xx와 외부 side effect 오류는 안전한 fingerprint·배포 revision·dependency 상태로 묶인다.
  • 로그·trace·지원 도구에서 PII와 secret redaction을 정기 표본으로 검증한다.
  • 지원의 확장 조회와 재처리에는 사유·시간 제한·감사 이력이 남는다.

연습으로 결제, 파일 업로드, 프로필 수정 중 하나를 고르세요. validation_failed, rate_limited, write_timeout_after_send, permission_denied 네 경우에 대해 외부 응답, 내부 event, 지원 화면에 표시할 필드를 세 칸 표로 적어 보세요. 같은 정보를 세 번 복사하지 않고도 각 독자가 다음 행동을 선택할 수 있다면 오류 처리가 운영 경계로 작동하기 시작한 것입니다.

관련 글