API 계약 v4 → v4-1

실코드 2cc1b32를 전 항목 대조해 확정한 개정이다. 변경 10건과 검토 후 유지 13건을 함께 남긴다. 미결 3건은 계약에 섞지 않았다.

적용 기준. 팀 레포 origin/dev의 Client DTO와 계약 테스트가 정본이다. 공개 API는 /routes·/board·/vehicles 세 경로만 두고, 데모 서버의 추가 필드·경로는 계약에 넣지 않는다.

한눈에깨지는 변경은 오류 계약 두 건이다

retryable 제거와 오류 코드 5→9종 확장이 클라이언트 타입에 영향을 준다. staleAt은 호환되는 필드 추가이고, 나머지는 잘못된 설명의 정정이거나 저장 계층 변경이다. OpenAPI 판본은 2.1.0이다.

확정 Client API →

클라이언트 계약 변경

1오류 응답 — 네 필드에서 세 필드로

무엇이 — retryable을 예시·required·속성 정의에서 모두 뺐다.

v4
{
  "code": "ROUTE_NOT_FOUND",
  "message": "등록되지 않은 노선입니다.",
  "requestId": "req-…",
  "retryable": false
}
v4-1
{
  "code": "ROUTE_NOT_FOUND",
  "message": "등록되지 않은 노선입니다.",
  "requestId": "req-…"
}

왜. 재시도 여부는 상태 코드와 Retry-After가 맡는다. 같은 뜻을 body에 한 번 더 싣지 않는다. 근거. PR #26 cb7a945 · ErrorResponse.java 3컴포넌트 record.

2오류 코드 — 5종에서 9종으로

무엇이 — 요청 형식·없는 경로·틀린 메서드·미처리 오류를 같은 오류 응답 모양으로 맞췄다.

v4-1 신설HTTP구분
INVALID_REQUEST400경로 파라미터 밖의 요청 형식 오류
ENDPOINT_NOT_FOUND404등록되지 않은 노선이 아니라 존재하지 않는 API 경로
METHOD_NOT_ALLOWED405경로는 있으나 메서드가 다름
INTERNAL_ERROR500예상하지 못한 서버 오류

왜. 404·405·500이 계약 밖 모양으로 새지 않게 한다. 근거. PR #26 cb7a945 · ErrorCode.java 9종. 모든 4xx·5xx는 no-store다.

3보드 응답에 staleAt을 추가한다

무엇이 — Board 최상위에 required staleAt을 더한다. 값은 observedAt + 5분이며 /vehicles의 observation.staleAt과 같은 뜻이다.

v4
{
  "observedAt": "13:00:00",
  "model": { … }
}
v4-1
{
  "observedAt": "13:00:00",
  "staleAt": "13:05:00",
  "model": { … }
}

왜. 화면을 켜 둔 뒤 언제 낡음 표시로 바꿀지 서버 정책을 하드코딩하지 않게 한다. 서버는 이 선을 이미 넘긴 보드를 주지 않고 503을 낸다. 근거. SAL-113 · PR #46 · 2822dcf.

4차량 목록이 비었을 때 — 0대와 읽기 실패를 가른다

무엇이 — observation.state의 enum은 그대로 두고 UNKNOWN 조건을 한 갈래 보강했다.

상류가 0대라고 답함
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는 현재 순번이 아니라 지나온 정류장 수다.

v4 산문
sequence - horizonStops
  = 현재 순번

이 값으로 turnSequence와
회차 전후를 판정
v4-1 산문
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
Client API · 유지
{ "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
V7collection_strategy_versionPR #40 628f74d
V8채점 상태 5값·정산 불변식·PENDING 인덱스PR #41 2cc1b32
V9day_count 날짜 균등가중PR #41 2cc1b32

왜. 수집기·예보기·정산·통계 경로가 실제로 이어졌다. Client 응답 모양은 바뀌지 않는다.

검토 후 유지 — 13건

아무 일도 하지 않은 목록이 아니다. 실코드와 게시 계약을 다시 맞춰 보고 그대로 두기로 한 항목이다.

항목확인 결과
엔드포인트3경로·GET 전용을 유지한다
/routes 응답5필드, 원문 route ID, DB 생성 순서 정렬을 유지한다
/routes 캐시public, max-age=300을 유지한다
LiveVehicles·Observationrequired 4·3필드와 state 3값을 유지한다
Vehiclerequired 7필드, phase 3값, 원문 currentStopSequence를 유지한다
SeatInfoEXACT | UNKNOWN, unknown 사유 미노출을 유지한다
차량·보드 캐시응답 시각 기준 15·20·600초를 유지한다
StopStaterequired 6필드, 경유 지점 빈 배열을 유지한다
ApproachingVehiclerequired 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