콘텐츠로 이동

ADR-0054: publication-api 설명 조회 응답을 공통 응답 포맷으로 통일 — 설명 없음 204 폐지

  • 상태: 승인됨
  • 날짜: 2026-08-24
  • 관련: ALPHA-1008 · ADR-0042(sync Pull 204 폐지 — 범위 한정 조항의 별도 재검토) · ALPHA-498(성공 본문 비포장 — 이 ADR로 대체) · publication-api.md · ADR-0053(위젯 직접 호출 — "바뀌지 않는 것"의 응답 형상·204 수렴 문면은 이 ADR이 부분 대체)

맥락

publication-api 설명 조회(GET /api/v1/explanations/{etf_ticker})는 성공이 두 형상이다: 설명 있음 = 200 + 맨몸 ExplanationResponse(공통 응답 포맷 미적용, ALPHA-498), 설명 없음 = 204 무바디. 컨트롤러는 이 분기 때문에 ResponseEntity 로 상태를 수동 제어한다 — ADR-0042 가 sync Pull 에서 걷어낸 것과 같은 이형 성공 경로이고, 그 ADR 은 이 표면을 "폴링 계약이 아니라 성격이 다르다"며 범위 밖(필요 시 별도 재검토)으로 남겼다. publication-api 리팩토링 착수를 계기로 그 재검토를 연다.

재검토 결과, 당시 "성격이 다르다"의 실질을 이루던 조건들이 현재는 성립하지 않는다:

  • 전환 비용이 지금이 최저다. 계약 문서는 증권사 연동 담당자에게 전달되는 대외 원본이고 증권사 자체 UI 의 직접 호출도 열려 있으나(docs/context.md), 현시점 실배포 소비자는 데모 위젯 하나뿐이다(실 납품·실 연동 전 — 204 해석은 demo/mts-ai-tab/js/broker-api.js 단일 지점). 대면 트래픽이 없는 지금이 계약 변경 비용이 가장 쌀 때라는 ADR-0042 의 판단이 그대로 적용된다 — 실 연동이 생긴 뒤에는 이 전환이 증권사 측 재작업을 요구한다. "공개 API 라 204 가 유리하다"의 전제(통제 밖 다수 소비자·CDN 캐시 실익)도 현시점엔 측정된 적 없는 이론값이다.
  • fail-loud 논거는 여기에도 적용된다. 바디 없는 204 는 소비자가 검증할 수 있는 것이 없다 — 오설정 프록시·라우트가 흘린 204 를 위젯은 "설명 없음(정상)"으로 그린다. "성공은 항상 isSuccess:true 포맷"이면 빈 응답도 자기 형상을 증명해야 하므로 형상 위반이 폴백 화면으로 표면화된다(AGENTS Rule 12). sync 처럼 파이프라인이 영구 정지하는 최악 모드는 아니지만, 고객 화면이 조용히 틀리는 모드다.
  • 소비자 분기 수는 동일하다. "없음"의 표현이 상태 코드(204/200)든 result 유무든 위젯의 분기는 하나다(ADR-0042 와 동일 논거).

204 를 폐지하면 "없음"을 본문으로 표현해야 하므로 성공 200 본문의 비포장 결정(ALPHA-498)이 함께 재검토된다 — 맨몸 DTO 에는 "없음"의 자리가 없다. 비포장의 원래 근거는 "외부 소비자에게 내부 관례를 강요하지 않는다"였으나, 위 소비자 조건에서 이 배려의 수혜자는 없다.

결정

  1. 설명 조회의 성공 응답을 공통 응답 포맷(ApiResponse<ExplanationResponse>)으로 통일한다. 설명 있음 = 200 + result 에 게시분, 설명 없음 = 200 + result 생략(정상 — 모든 ETF 가 매일 설명을 갖지 않는다는 시맨틱 불변). 204 는 폐지한다. 에러 경로는 현행 그대로다(이미 공통 포맷).
  2. ALPHA-498 의 "성공 본문 비포장"을 이 ADR 로 대체한다. 이후 publication-api 의 모든 응답(성공·에러)은 공통 응답 포맷 한 형상이다.
  3. 컨트롤러는 플랫폼 표준으로 돌아간다. ResponseEntity 수동 상태 제어를 제거하고 ApiResponse<T> 직접 반환(항상 200)으로 통일한다 — 이 리팩토링의 발단이었던 이형이 소멸한다.
  4. 400(형식 오류)·404(미지 종목)는 유지한다. "없음"은 정상 상태라 200 이 맞고, 404 로 합치면 "모르는 종목"(SERV4040)과 시맨틱이 충돌한다.

대안

  1. 현행 유지 — 설명 없음 204. HTTP 시맨틱으로 정확하고 이행 비용이 0 이다. 그러나 이형 성공 경로와 fail-silent 화면 모드를 남기고, 이 표면의 소비자 조건에서 204 의 실익이 없다(맥락 절). ADR-0042 가 남긴 재검토를 "유지"로 닫는 안인데, 유지의 근거가 재검토에서 소거됐다.
  2. 404 로 표현. "조회 대상 부재"의 REST 관행이나, 미지 종목 404 와 한 코드에 두 시맨틱이 실려 위젯이 body 의 도메인 코드를 파싱해야 구분된다 — 상태 코드의 정보량이 오히려 준다.
  3. 비포장 유지 + 200 빈 바디(null). envelope 없이 204 만 없앤다. null 리터럴 본문은 형상 검증이 불가능해 fail-loud 논거를 잃고, 비포장 유지의 근거(외부 소비자 배려)는 이미 소거됐다.

결과

  • 마이그레이션은 확장-수축 3단계다 (ADR-0042 M1→M2→M3 패턴 재사용, 구현은 별도 티켓으로 분리):
  • M1 — 위젯 관용(확장): broker-api.js 가 세 형상을 병행 수용한다 — ① 현행 204(=NO_DATA), ② 현행 맨몸 ExplanationResponse 200(=OK — M2 전의 서버는 여전히 이 형상이므로 envelope 를 전 200 에 요구하면 정상 게시분이 전부 폴백이 된다), ③ 신규 공통 포맷 200. ③은 성공 envelope 형상 전체를 검증한 뒤 해석한다: ApiResponse 성공 형상(isSuccess === true·code·message 존재와 타입)을 확인한 응답만 result 있음 = OK·생략 = NO_DATA 로 해석하고, 어느 형상에도 맞지 않는 응답({}·isSuccess:false 200·필드 결손 등)은 전부 FALLBACK. result 유무만 보면 기형 응답이 정상 NO_DATA 로 그려져 이 ADR 이 없애려는 fail-silent 를 재생산한다. 데모 박스 재배포.
  • M2 — 서버 전환: ExplanationController 의 204 제거·envelope 적용, openapi.yaml·publication-api.md(204 조항·비포장 조항)·모듈 runbook(publication-api/README.md· DEVELOPMENT.md 의 204·negative cache·검증 명령 서술)·부하 실험 하네스 전체(tests/loadtest 트리를 204·루트 explanation_as_of 기준으로 전수 스캔 — k6 스크립트·grafana 대시보드 쿼리· summarize.py·publication-baseline.js, 문서 포함) 동반 갱신 — 낡은 판정을 남기면 실험이 성공으로 끝나며 전파·차단 측정만 조용히 오염된다. 데모 박스 재배포.
  • M3 — 위젯 정리(수축): broker-api.js 의 구형 두 분기(204·맨몸 200)를 제거 — 이후 204·비 envelope 200 수신은 폴백 화면으로 표면화. 데모 박스 재배포.
  • 순서 위반(M1 전에 M2) 시 위젯이 정상 게시분을 폴백 화면으로 그린다.
  • 실 연동은 갱신된 계약이 기준이다. 이 전환은 실 납품 전에 완결한다 — 이후 증권사 연동(자체 UI 포함)은 M2 에서 갱신된 publication-api.md·openapi 를 기준으로 시작하므로, 과도기 이중 형상이 대외로 노출되지 않는다.
  • 캐싱 검토 축은 불변이다. 계약 미확정 목록 3(ETag 등 캐싱 지시)은 이 변경과 독립 — 차단·정정 반영 지연 상한 논의는 ExplanationStore TTL 쪽(ALPHA-777)에 있다.
  • 데모 시나리오 문서가 따라간다. demo/mts-ai-tab/README.md 의 204 시나리오 표(305720 등)·docker-compose.yml 주석의 "204 시나리오" 표기는 M2 에서 "result 생략" 표기로 갱신한다.