확정 도메인 설계
제품 코드는 collector·processor·api 세 패키지, 두 프로세스로 확정했다. collector와 processor가 배치 프로세스, api가 요청 프로세스다. 모델 학습·비교·선정은 별도 연구 환경에 남는다.
2026-08-17 재검토(1차 모델 적용 결론) 반영 · 객체 구조 v8 기준
전체 흐름 2개 프로세스 · 패키지 3 · PostgreSQL
배치 프로세스가 수집과 발행을, 요청 프로세스가 조회를 맡는다. 두 프로세스는 서로를 호출하지 않고 PostgreSQL을 사이에 두고 만난다. 학습·평가·선정은 외부 ML 환경의 일이고, 서버에는 승인된 계수 번들만 들어온다.
- 공공 API GBIS에서 차량 위치와 노선·정류장 기준을 받는다.
- 수집 collector가 60초마다 호출 결과를 정규화해 쌓는다. 3노선 60초는 하루 4,320회로 한도 10,000의 43%다.
- 저장 PostgreSQL 표 8개가 두 프로세스의 운영 데이터 경계다.
- 발행 processor가 승인된 계수 번들로 확률을 계산해 발행본을 봉인한다. 새 판은 시간대 경계(07·09·17·20시 KST)·모델 승격·노선 판본 변경 때만 만든다.
- 조회 api가 활성 발행본을 읽어 응답한다.
collector 프로세스 A · 수집 · 객체 20
공공 API를 관측 기록으로 바꾸는 경계. 차량이 없는 정상 응답과 실패를 구분하고, 관측값과 호출 장부를 각각 남긴다.
CollectLocationJob이 노선마다 RealtimeObservationSource 구현(GbisLocationSource)을 60초 주기로 호출한다.
60초인 이유는 학습 라벨 조건이 "연속 관측 간격 90초 이하"라서다 — 90초 주기로 잡으면 지연과 지터로 라벨이 자주 탈락한다.
CallLedger는 일일 호출 한도를 예약 방식으로 지킨다. quota를 먼저 예약하고 전송하므로
호출 결과 PollOutcome은 아홉 가지다 — 진행 2 · 정상 종료 2(SUCCESS_ROWS·SUCCESS_EMPTY) · 실패 종료 5.
"예약했는데 안 보냄"과 "보냈는데 결과를 모름"을 실패로 뭉치면 재시도가 한도를 두 번 쓴다.
SUCCESS_EMPTY는 "그 순간 반환할 차량이 없다"는 뜻일 뿐, 운행 종료의 근거가 아니다.
관측 0건 poll도 지우지 않는다 — 간격 90초 판정에서 관측 사이의 연결을 끊는 근거다.
관측값은 VehicleObservation으로 정규화한다. 좌석 -1은 0이 아니라 null과 사유
MINUS_ONE으로 남기고(SeatReading), 혼잡도는 1~4만 유효값으로 본다.
차량 번호판은 필드를 만들지 않고, 번호판을 구조적으로 제거한 payload의 지문(evidence_payload_sha256)만 남긴다.
노선 판본 RouteReferenceVersion은 활성 플래그가 아니라 유효기간(valid_from/valid_to)으로 관리한다.
기간이면 range exclusion 제약이 겹침을 DB에서 막고, 과거 관측에 그때의 판본을 정확히 붙일 수 있다.
과거 학습 사건에 현재 판본을 사후 부착하지 않는다.
| 소유 데이터 | LocationPoll 호출 장부 · VehicleObservation 차량 관측 · RouteReferenceVersion/RouteStop 노선 판본 |
|---|---|
| 구성 | 객체 20 — 인터페이스 RealtimeObservationSource·RouteReferenceSource · enum PollOutcome·FailureCode·SeatUnavailableReason·TrackPhase · 리포지토리 3 |
| 경계 | GBIS 응답을 받는 유일한 창구다. 라벨·예측·평가를 만들지 않는다. |
collector 객체 장부 — 20개
- 실행과 정책
CollectLocationJob전체 노선 수집 실행 ·CallLedger일일 호출 한도 예약 ·AttemptKey호출 시도 식별 ·RefreshRouteReferenceJob노선 판본 갱신- 공공 API 경계
RealtimeObservationSource차량 위치 소스 계약(인터페이스) ·GbisLocationSourceGBIS 차량 위치 호출 ·RouteReferenceSource노선 기준 소스 계약(인터페이스) ·GbisRouteStopSourceGBIS 노선·정류장 호출- 호출 장부와 관측값
LocationPoll한 번의 호출 장부 ·PollOutcome호출 결과 9가지(enum) ·FailureCode실패 분류(enum) ·VehicleObservation차량 한 대의 관측값 ·SeatReading좌석값 해석 ·SeatUnavailableReason좌석 결측 사유(enum) ·TrackPhase정류장 진행 상태(enum)- 노선 판본
RouteReferenceVersion유효기간을 가진 노선 판본 ·RouteStop판본 안의 정류장- 저장 경계
CallLedgerRepository한도 예약 ·LocationPollRepositorypoll 예약과 확정 ·RouteReferenceRepository판본 조회와 대체
processor 프로세스 A · 발행 · 객체 20
승인된 계수 번들로 확률을 만들어 발행본으로 봉인하는 경계. 서버는 학습하지 않는다.
모델 입력은 좌표 셋뿐이다 — 노선, 정류장 순번, KST 시간대(TimeBand: MORNING 07~09 · EVENING 17~20 · OTHER).
현재 차량의 위치·잔여석은 추론 입력이 아니라서 수집이 잠깐 끊겨도 예보 값은 안 변한다.
모델 계약은 FullnessProbabilityModel 인터페이스이고, 구현은
상호작용 로지스틱 회귀를 담는 InteractionLogisticModel이다.
이 모델은 2026-08-17~08-19 의 1차 운영 선택이었다 — 운영 적용을 위한 팀 선택이지 성능 입증이 아니었고, 일일 평가는 잠정인 채 승자가 없었다.
08-19 팀 검토로 그 자리는 08번에 넘어갔고 02번은 비운영 참조가 됐다(아래 문단). 이 문서는 v3 서빙 설계의 기록이라 경계 서술 자체는 그대로 둔다.
CoefficientBundle은 외부 환경에서 승인된 불변 계수 번들을 담아 golden vector로 검증한다.
지원 범위 밖이면 절편만으로 조용히 확률을 내지 않고 발행 전체를 거절한다.
결과는 sealed PredictionResult로 받는다 — Estimated(만석 확률)와
NotEstimated(사유 BOARDING_NOT_ALLOWED·OUT_OF_SCOPE)로 타입부터 다르다.
2026-08-19에 더해진 08번 후보(오고 있는 버스 보기)는 이 경계에 들어오지 않는다.
그 후보는 지금 오고 있는 차량의 잔여석을 입력으로 받아 도착 시 좌석 분포를 내므로,
발행 주기가 시간대 경계인 /board의 계산과 성질이 다르다.
v3가 예보와 실시간 차량을 갈라 둔 근거(갱신 주기가 24배 다르다)가 그대로 적용되어,
차량 예보는 /vehicles 쪽에 선택 필드로 붙는 형태를 검토 중이다
(v4-fe 증분안). /board의 계산·스키마·발행 주기는 그대로 둔다.
학습 단위도 다르다 — 08번은 노선별로 따로 적합하고 계수를 공유하지 않아,
채택되면 CoefficientBundle이 노선마다 하나씩 필요하다.
이전 설계의 정류장·시간대 위험 통계 표 cell_risk는 제거했다.
서버가 통계를 집계하지 않고 승인된 번들 하나만 실행하기 때문이다.
그 자리에 배포 원장 model_deployment가 들어와 DB는 표 8개 그대로다.
배포 ModelDeployment는 STAGED → ACTIVE → RETIRED로 움직이고, 승격은 전용 프로시저 한 번이다 —
기존 ACTIVE가 전달받은 ID인지 CAS로 확인하고, candidate 발행본이 전부 봉인·미공개인지 검사한 뒤 한 transaction으로 갈아탄다.
롤백은 과거 행을 다시 쓰지 않고 새 배포 episode를 만든다.
발행본 ForecastPublication은 봉인 뒤에만 공개되고, 승차 가능한 정류장 하나라도 실패하면 발행 전체를 실패시킨다.
적재할 계수 번들은 아직 없다 — 평가 결과에 최종 계수·특징 사전·golden vector가 없어
외부 환경에서 한 번 더 적합해야 하고, 그 전에는 processor가 뜨지 않는다.
| 소유 데이터 | ModelDeployment 모델 배포 원장 · ForecastPublication/StopPrediction 발행본 |
|---|---|
| 구성 | 객체 20 — 인터페이스 FullnessProbabilityModel·CoefficientSource·sealed PredictionResult · enum TimeBand·NotEstimatedReason · 리포지토리 3 |
| 경계 | collector가 저장한 판본·대상만 읽는다. api를 호출하지 않고 발행본을 PostgreSQL에 쓴다. 학습·평가·선정은 전부 외부 환경이다. |
processor 객체 장부 — 20개
- 발행 실행
ForecastJobcandidate 생성과 시간대 갱신 ·RouteTargetQuery발행 대상 조회 ·ForecastTarget노선 판본·순번·시각 좌표 ·TimeBand시간대 3구분(enum)- 모델 계약
FullnessProbabilityModel만석 확률 모델 계약(인터페이스) ·InteractionLogisticModel상호작용 로지스틱 회귀 구현 ·ApprovedModelRef승인 release 참조 ·CoefficientBundle불변 계수 번들 ·TermKey특징 key 생성 ·CoefficientSource번들 적재 계약(인터페이스)- 배포
ModelDeployment배포 episode ·ModelDeploymentRepository활성 조회와 승격- 판정 결과
PredictionResult판정 결과 계약(sealed 인터페이스) ·Estimated확률이 계산된 결과 ·NotEstimated계산하지 않은 결과 ·FullnessProbability만석 확률 값 ·NotEstimatedReason미산출 사유 2가지(enum)- 발행본
ForecastPublication봉인·발행되는 예보 한 판 ·StopPrediction정류장별 값 ·PublicationRepository봉인과 일괄 공개
api 프로세스 B · 조회 · 객체 14
읽어서 내보내는 경계. processor를 직접 호출하지 않는다. 조회 경로가 둘이고 서로 읽는 표를 공유하지 않는다.
| 경로 | 읽는 표 | 하는 변환 | 바뀌는 때 |
|---|---|---|---|
/board 예보 | forecast_publication·stop_prediction·route_stop | 만석 확률 → 빈자리 확률 | 시간대 경계에만. 하루 4번 |
/vehicles 실시간 | vehicle_observation·location_poll·route_stop | 좌석 값+사유 → EXACT/UNKNOWN | 수집할 때마다. 하루 1,440번 |
둘을 가른 이유가 있다. 한 조회가 발행본과 관측을 같이 읽으면 모델이 실시간을 본다는 오해가 생기고, 둘 중 하나가 죽을 때 같이 죽는다. 예보가 503이어도 차량은 정상 동작한다.
BoardController가 노선 보드 요청을 받고 BoardQueryService가 조회를 조정한다.
BoardQuery는 활성 발행본을 SQL로 읽는다.
DB의 만석 확률 p_full은 서비스 계층 메서드 하나에서 1 - p_full로 뒤집어
seatAvailableProbability로 내보내며 반올림하지 않는다 — 0.996이 1.00이 되면 "반드시 자리가 있다"로 읽힌다.
응답에 차량 참조를 붙이지 않는다. 이 모델은 차량을 고르지 않아서 붙이면 거짓 의미가 된다.
최상위 freshness도 뺐다 — 수집 신선도는 모델 신선도가 아니다.
노선 ID는 넷으로 분리한다: 제품 공개 ID(미도입) · 공급자 ID(204000057) · 표시명(3330) · 모델 노선 키(3330).
v1의 API path는 공급자 원문 ID를 그대로 쓴다 — 의도된 결합이고 공급자 변경은 breaking change다.
표시명과 모델 키는 문자열이 같아 보여도 다른 개념이다.
확정 API 계약은 노선 예보 조회 v1이다.
실시간 차량은 LiveVehicleController가 받고 LiveVehicleQuery가 마지막 수집분을 읽는다.
차량 식별은 GBIS vehId 원문을 그대로 쓴다 — 응답 한정 번호를 쓰면 같은 버스를 이어 붙일 수 없어
목록 key와 이동 추적이 깨진다. 정렬도 서버가 고정한다: direction → currentStopSequence 오름차순.
혼잡도는 저장은 하되 응답에 넣지 않는다 — v1 화면이 잔여석만 쓴다.
| 소유 데이터 | 없음 — 예보는 활성 ForecastPublication을, 차량은 마지막 vehicle_observation을 읽기만 한다 |
|---|---|
| 구성 | 객체 14 — 예보 7(BoardController·BoardQueryService·BoardQuery·BoardRow·StopRow·BoardView·StopView) · 실시간 차량 projection(LiveVehicleController·LiveVehicleQueryService·LiveVehicleQuery·LiveSnapshotRow·VehicleRow·LiveVehicleView·ObservationView·VehicleView) |
| 경계 | 읽기 전용이다. 예측 계산·수집을 하지 않고, 모델이 무엇으로 바뀌어도 이 패키지는 고칠 일이 없다. |
api 객체 장부 — 14개
- 예보 · HTTP 진입
BoardController노선 보드 HTTP 진입점- 예보 · 조회 흐름
BoardQueryService조회 조정과 확률 뒤집기 ·BoardQuery활성 발행본 읽기 SQL- 예보 · 읽기 행과 응답
BoardRow노선 단위 읽기 행 ·StopRow정류장 단위 읽기 행 ·BoardView노선 보드 응답 ·StopView정류장 응답- 실시간 차량 · HTTP 진입
LiveVehicleController실시간 차량 HTTP 진입점- 실시간 차량 · 조회 흐름
LiveVehicleQueryService관측 상태 판정과 신선도 ·LiveVehicleQuery마지막 수집분 읽기 SQL- 실시간 차량 · 읽기 행과 응답
LiveSnapshotRow수집 단위 읽기 행 ·VehicleRow차량 단위 읽기 행 ·LiveVehicleView실시간 차량 응답 ·ObservationView관측 상태 응답 ·VehicleView차량 응답
계약 규모
객체 구조 v8의 정본 수치다. 도식에서 세던 필드·메서드·관계 수는 정본이 아니라서 뺐다.
| 패키지 | 객체 |
|---|---|
| collector | 20 |
| processor | 20 |
| api | 14 |
| 합계 | 54 |
합계 54는 v8 문서가 확정한 객체 수다. v7 대비 api가 7에서 14로 늘었다 — 실시간 차량 조회를 별도 projection으로 갈랐기 때문이다. 다만 v8 클래스 도식은 실시간 차량 클래스를 8개 선언해 표제 수치(+7)와 하나 어긋난다. 어느 것을 별도 객체로 세지 않았는지는 정본 확인이 필요하다. 인터페이스·enum·리포지토리 구분은 각 패키지의 장부에 있다. 구현이 바뀌면 이 표도 다시 센다.