
부제: 전부 거부됐는데 202를 돌려주던 API, 그리고 끝내 못 고친 이유
이미지 업로드가 조용히 실패했다.
파일이 전부 거부됐는데 서버는 202를 반환했다.
앞단(nginx·백엔드)은 그 상태코드를 보고 "성공"으로 처리했다.
정작 실패 목록은 응답 본문의 errors[] 안에 얌전히 담겨 있었다.
상태코드만 보는 호출자는 알 길이 없었다.
상태코드가 결과를 속인다
문제를 이렇게 정의했다.
2xx를 반환하지만 실제로는 실패해서, 상태코드만 보는 호출자가 성공으로 오인하는 응답.
핵심은 상태코드가 결과를 정직하게 반영하지 않는다는 것이다.
배치 처리, 비동기 접수, 예외를 삼키는 핸들러 — 이런 자리에서 잘 생긴다.
한 곳에서 났으면 다른 곳에도 있을 거라 봤다.
그래서 라우터를 전수조사하기로 했다.
무엇이 거짓말이고 무엇이 정상인가
여기가 제일 중요했다.
2xx인데 실패가 섞였다고 전부 잘못은 아니다. 먼저 갈라야 했다.
- 전부 실패인데 2xx — 생성된 게 0건인데 성공처럼 보인다. 진짜 거짓말.
- 예외를 잡아 2xx로 — 핸들러가 에러를 삼키고 성공 코드를 준다. 진짜 거짓말.
- 비동기 접수 202 후 백그라운드 실패 — 이건 정상이다. 접수는 진짜로 됐으니까. 다만 이후 조회(폴링)가 "실패했음"을 분명히 보여주는지는 확인해야 한다.
- 스트림 200 시작 후 중간 실패 — 이것도 정상이다. 응답은 이미 시작됐고 에러는 스트림 이벤트로 간다. 클라이언트가 그 이벤트를 처리하는지가 관건.
- 일부만 실패인데 2xx — 논쟁적이다. 부분 성공을 4xx로 만들면 성공한 것까지 실패로 취급돼 유실된다. 그래서 상태코드보다는 "호출자가
errors[]를 반드시 보게 만드는" 가시성 문제로 다뤘다.
표적은 앞의 둘로 좁혔다.
나머지는 정상 패턴이니 "실패가 보이긴 하는가"만 검증했다.
정상 비동기 응답을 실패로 오해해 동기 4xx를 강제하는 건, 고치는 게 아니라 망가뜨리는 것이다.
고치려니 계약이었다
그래서 전부 실패면 4xx를 주면 되겠네 — 라고 하기엔 걸리는 게 있었다.
상태코드를 바꾸는 건 API 계약 변경이다.
백엔드는 이미 2xx를 성공으로 처리하도록 짜여 있다.
우리가 맘대로 4xx로 바꾸면 그쪽이 깨진다. 협의 없이 배포할 수 없다.
그래서 레버를 둘로 나눴다.
- ① 로깅 — 거짓 성공이 나는 순간 서버에 경고를 남긴다. 생성 0건인데 성공으로 나가는 것, 예외를 삼킨 사유. 계약 변경이 아니라 즉시 가능.
- ② 상태코드 정정 — 실제 실패면 4xx. 계약 변경이라 백엔드 협의 후.
가시성은 로깅으로 먼저 덮고, 계약은 협상 테이블로 넘겼다.
못 고치는 동안에도 최소한 우리는 알 수 있게 만든 것이다.
정리
- 업로드가 전부 거부돼도 202 → 상태코드만 보는 앞단은 성공으로 오인, 실패는 응답 본문에 묻힘
- 한 곳에서 났으면 다른 곳도 → 라우터 전수조사
- 2xx+실패가 전부 잘못은 아니다 — 정상 async 202·스트림 200과 "진짜 거짓 성공"을 가르는 분류가 먼저
- 부분 성공을 4xx로 만들면 성공분이 유실 → 상태코드가 아니라 가시성 문제로 처리
- 정정이 계약 변경이라 못 고침 → 로깅(즉시)과 상태코드(협의)를 분리
- 원칙: 상태코드는 결과를 속이면 안 된다. 다만 당장 못 고치면, 최소한 보이게 만든다
숫자 하나가 거짓말을 하면 그 위의 모든 판단이 틀어진다.
그런데 그 거짓말을 고치는 게 남과의 계약이라면, 고치는 것과 드러내는 것을 분리해야 한다.
드러내는 건 오늘 할 수 있고, 고치는 건 협상이 필요하니까.
'개발' 카테고리의 다른 글
| [에이전트] 만료됐다고 지우지 않는다, valid_until을 d_day로 바꾼 이유 (0) | 2026.07.15 |
|---|---|
| [백엔드] BE 개발자 없이 시작 느슨한 JSONB 계약 컨텍스트 (0) | 2026.07.14 |
| [백엔드] 외부 API 선택 이론적 정확도보다 실사용 제약 (0) | 2026.07.13 |
| [open-webui] OpenAI + Anthropic Models (0) | 2024.11.20 |
| [선형대수] 전사함수와 일대일함수: ONTO and ONE-TO-ONE (0) | 2024.03.25 |