API 계약 v4 → v4-1
실코드 2cc1b32를 전 항목 대조해 확정한 개정이다.
변경 10건과 검토 후 유지 13건을 함께 남긴다. 미결 3건은 계약에 섞지 않았다.
적용 기준. 팀 레포 origin/dev의 Client DTO와 계약 테스트가 정본이다.
공개 API는 /routes·/board·/vehicles 세 경로만 두고,
데모 서버의 추가 필드·경로는 계약에 넣지 않는다.
한눈에깨지는 변경은 오류 계약 두 건이다
retryable 제거와 오류 코드 5→9종 확장이 클라이언트 타입에 영향을 준다.
staleAt은 호환되는 필드 추가이고, 나머지는 잘못된 설명의 정정이거나 저장 계층 변경이다.
OpenAPI 판본은 2.1.0이다.
클라이언트 계약 변경
1오류 응답 — 네 필드에서 세 필드로
무엇이 — retryable을 예시·required·속성 정의에서 모두 뺐다.
{
"code": "ROUTE_NOT_FOUND",
"message": "등록되지 않은 노선입니다.",
"requestId": "req-…",
"retryable": false
}{
"code": "ROUTE_NOT_FOUND",
"message": "등록되지 않은 노선입니다.",
"requestId": "req-…"
}왜. 재시도 여부는 상태 코드와 Retry-After가 맡는다.
같은 뜻을 body에 한 번 더 싣지 않는다. 근거. PR #26 cb7a945 · ErrorResponse.java 3컴포넌트 record.
2오류 코드 — 5종에서 9종으로
무엇이 — 요청 형식·없는 경로·틀린 메서드·미처리 오류를 같은 오류 응답 모양으로 맞췄다.
| v4-1 신설 | HTTP | 구분 |
|---|---|---|
INVALID_REQUEST | 400 | 경로 파라미터 밖의 요청 형식 오류 |
ENDPOINT_NOT_FOUND | 404 | 등록되지 않은 노선이 아니라 존재하지 않는 API 경로 |
METHOD_NOT_ALLOWED | 405 | 경로는 있으나 메서드가 다름 |
INTERNAL_ERROR | 500 | 예상하지 못한 서버 오류 |
왜. 404·405·500이 계약 밖 모양으로 새지 않게 한다.
근거. PR #26 cb7a945 · ErrorCode.java 9종. 모든 4xx·5xx는 no-store다.
3보드 응답에 staleAt을 추가한다
무엇이 — Board 최상위에 required staleAt을 더한다.
값은 observedAt + 5분이며 /vehicles의 observation.staleAt과 같은 뜻이다.
{
"observedAt": "13:00:00",
"model": { … }
}{
"observedAt": "13:00:00",
"staleAt": "13:05:00",
"model": { … }
}왜. 화면을 켜 둔 뒤 언제 낡음 표시로 바꿀지 서버 정책을 하드코딩하지 않게 한다.
서버는 이 선을 이미 넘긴 보드를 주지 않고 503을 낸다. 근거. SAL-113 · PR #46 · 2822dcf.
4차량 목록이 비었을 때 — 0대와 읽기 실패를 가른다
무엇이 — observation.state의 enum은 그대로 두고 UNKNOWN 조건을 한 갈래 보강했다.
outcome = SUCCESS_EMPTY
vehicles = []
→ NO_VEHICLES_OBSERVED
운행 종료를 뜻하지 않는다outcome = SUCCESS_ROWS parsed vehicles = 0 → UNKNOWN 서버 WARN을 남긴다
왜. 공급자 필드 형식이 바뀐 사고를 “차량 없음”으로 숨기지 않는다.
근거. PR #42 f541fad · LiveVehicleQueryService.java.
5horizonStops — 값은 그대로, 축 설명을 바로잡는다
무엇이 — 스키마 1~12는 유지한다. 다만
sequence - horizonStops는 현재 순번이 아니라 지나온 정류장 수다.
sequence - horizonStops
= 현재 순번
이 값으로 turnSequence와
회차 전후를 판정sequence - horizonStops
= 지나온 정류장 수
현재 위치·회차 판정은
/vehicles.currentStopSequence왜. 도착 중인 차량은 통과 순번이 원문 currentStopSequence보다 1 작다.
옛 설명은 회차 경계에서 방향을 뒤집었다. 근거. PR #31 51d3bc7 ·
V4__observation.sql의 passed_stop_order. 산문 9곳을 한 묶음으로 정정했다.
6OpenAPI 판본 — 2.0.0에서 2.1.0으로
무엇이 — v4 세대 안의 계약 개정으로 minor를 올렸다.
왜. 오류 응답·코드 enum과 Board.staleAt이 바뀌었다. 사내 FE 수정과 한 묶음으로 진행한다.
근거. v3 1.0.0 → v4 2.0.0의 기존 표기 규칙과 v4-1 확정 계약.
저장 계층 변경 — 응답 모양은 그대로
7좌석 미상 — 값과 사유를 저장에서 가른다
무엇이 — 저장은 아는 좌석과 모르는 이유를 XOR로 묶는다. 공개 응답은 기존 두 갈래를 유지한다.
Known
remaining_seats = 12
reason = null
Unknown
remaining_seats = null
reason = REPORTED_UNKNOWN
| NOT_REPORTED{ "kind": "EXACT",
"remaining": 12 }
{ "kind": "UNKNOWN" }
사유는 공개하지 않는다왜. -1 센티널을 집계마다 다시 해석하지 않는다.
근거. PR #42 f541fad · V4 migration의 XOR CHECK.
8관측 시각 — 배치 수신 시각 하나를 권위로
무엇이 — 중복이던 vehicle_observation.observed_at을 지웠다.
응답 observedAt은 observation_batch.response_received_at에서 온다.
왜. 같은 수집 묶음의 차량 행은 관측 시각이 모두 같다.
근거. PR #42 f541fad · V4__observation.sql. Client 필드와 뜻은 유지다.
9통과 순번 — passed_stop_order를 저장한다
무엇이 — 원문 stop_order와 모델용 통과 순번을 나눴다.
기점 도착의 0은 유효하며 running_state는 0·1·2만 받는다.
왜. processor는 collector 패키지를 볼 수 없어 컬럼이 유일한 전달 통로다.
근거. PR #31 51d3bc7 · V4 migration. 변경 4의 축 근거다.
10운영 스키마 — V5부터 V9까지
무엇이 — 궤적 조회, 호출 예약, 수집 전략 판본, 예보 정산, 날짜 균등가중을 차례로 올렸다.
| 판 | 핵심 | 근거 |
|---|---|---|
| V5 | 차량 궤적 인덱스 2종 | PR #43 307cacf |
| V6 | 호출 장부·한도·outcome 9값 | PR #44 7c366b7 |
| V7 | collection_strategy_version | PR #40 628f74d |
| V8 | 채점 상태 5값·정산 불변식·PENDING 인덱스 | PR #41 2cc1b32 |
| V9 | day_count 날짜 균등가중 | PR #41 2cc1b32 |
왜. 수집기·예보기·정산·통계 경로가 실제로 이어졌다. Client 응답 모양은 바뀌지 않는다.
검토 후 유지 — 13건
아무 일도 하지 않은 목록이 아니다. 실코드와 게시 계약을 다시 맞춰 보고 그대로 두기로 한 항목이다.
| 항목 | 확인 결과 |
|---|---|
| 엔드포인트 | 3경로·GET 전용을 유지한다 |
/routes 응답 | 5필드, 원문 route ID, DB 생성 순서 정렬을 유지한다 |
/routes 캐시 | public, max-age=300을 유지한다 |
LiveVehicles·Observation | required 4·3필드와 state 3값을 유지한다 |
Vehicle | required 7필드, phase 3값, 원문 currentStopSequence를 유지한다 |
SeatInfo | EXACT | UNKNOWN, unknown 사유 미노출을 유지한다 |
| 차량·보드 캐시 | 응답 시각 기준 15·20·600초를 유지한다 |
StopState | required 6필드, 경유 지점 빈 배열을 유지한다 |
ApproachingVehicle | required 3필드, expectedSeats는 null이 아니라 키 생략을 유지한다 |
horizonStops 스키마 | 정수 1~12를 유지한다. 바뀐 것은 설명뿐이다 |
| 빈자리 확률 | 1 − p_full, 서버 반올림 금지를 유지한다 |
| 보드 신선도 | observedAt + 5분 뒤 503을 유지한다 |
| 혼잡도 | Client 계약 표면에 싣지 않는다 |
보류 — 이 페이지에서 정하지 않은 3건
| 항목 | 현재 간극 | 처리 |
|---|---|---|
RouteStatus 판정 축 | 계약은 노선 판본별 계수 묶음, 구현은 전역 ACTIVE 존재만 본다 | 수정하지 않는다 |
ACTIVE 부재의 MODEL_OUT_OF_SCOPE | 코드 이름은 판본 미지원인데 모델 없음에도 같은 코드를 쓴다 | 수정하지 않는다 |
| 429 산문 | 산문은 429+Retry-After: 60을 약속하지만 9종 enum·responses에는 429가 없다 | 사용자 결정 전까지 원문을 유지한다 |
경계. 보류 3건은 v4-1 확정 계약에 새 결론을 넣지 않았다. 이 페이지는 차이를 드러낼 뿐 대신 정하지 않는다.
기준 코드 2822dcf · OpenAPI 2.1.0 · 2026-08-31