Core domain
다섯 객체. 각자 한 문장만 책임집니다.
그림 세 장이 설계의 전부다. 누가 무엇을 아는지(책임과 관계), 질문 하나가 어떻게 답이 되는지(흐름), 이 그림을 진짜 API에 통과시킨 결과(실측 대조). 카드를 누르면 상세가 열리고, 🎯 판정 카드는 확대 페이지로 이어진다.
그림 1 — 책임과 관계
누가 무엇을 아는가
그림 2 — 흐름
질문 하나가 답이 되기까지
실측 값(8/13 저녁, 3330 농수산물시장) 그대로다.
다음 버스 탈 수 있어?"
가져온다 차량 20대 · 만석 5대
버스는?" v411 · 잔여 0석
미리 붙여둔 라벨을 읽는다 FULL
탈 확률 46.6%" 근거·시각과 함께 기록
핵심은 두 가지다 — 예측이 아니라 방금 지나간 버스의 관측으로 답하고, 답은 항상 근거(어느 관측, 어느 모델)와 함께 남아서 나중에 그대로 재현할 수 있다.
그리지 않는 화살표
한 번쯤 긋고 싶어지는 관계 네 개
버스.위치위치는 버스의 속성이 아니라 그 순간의 사실. 필드로 넣으면 과거가 사라져 백테스트가 불가능해진다.
노선.차량목록"지금 이 노선을 달리는 차"는 관측이 말해준다. 배속 명단은 실측에서 불완전했다(16대 중 3대 누락).
정류장.노선목록"여기 어떤 노선이 서나"는 검색(질의)이지 필드가 아니다. 필드로 두면 노선 개편 때마다 전 정류장을 고쳐야 한다.
관측.차량객체관측은 차량의 이름(ID)만 안다. 객체를 물면 나중에 정원이 바뀔 때 과거 관측의 의미까지 따라 변한다.
전체 필드표 펼쳐 보기 (스키마의 원형)
| 객체 | 필드 | 관계 | 근거 |
|---|---|---|---|
| Route 🗺️ | id · displayName | — | "3330". 외부 ID는 어댑터 매핑표 뒤에 |
plan | — | 요일 4패턴 첫막차·배차 — 「몇 분」 환산 상수 | |
stops (경로) | 품는다 | 순번(1-based) + 정류장 ID의 순열 + 회차 순번(turnSeq). 상·하행이 한 축(3330은 1..85, 회차 43) — 방면은 「순번 < 회차」→상행으로 유도(실측 197,240행 전건 성립) | |
| Stop 🚏 | id · name · at(좌표) | — | 정류장은 안 움직이니 좌표가 속성. 노선 역참조 금지 |
| Vehicle 🚌 | id · capacity | — | 정원 3형태(등재 45·46·69·71 / 관측하한 41+ / 미상). 위치·잔여석 없음 |
| RouteSnapshot 📸 | route(ID) | 이름만 | 마스터 갱신이 과거 관측을 오염시키지 않도록 |
observedAt | — | 시각은 사진 한 장 단위(GBIS가 차량별 시각을 주지 않음) | |
observations[] | 품는다 | 차량ID(이름만) + 마지막 통과 정류장 + 잔여석(모름≠0). 0대도 사실 | |
| Assessment 🎯 | query · judgedAt | 이름만 | 노선ID + 정류장 순번 + 주입된 판정 시각 |
확률·몇대·몇분 | 품는다 | 미추정은 0이 아니라 사유 있는 "안 함" | |
lineage | 품는다 + 이름만 | 모델 버전(품음) + 근거 관측 범위(ID) — 재현·비교의 좌표 |
그림 3 — 실측 대조
이 그림을 진짜 API에 통과시켰다
위치 응답 201건, 마스터 262,984행, 20일 관측 102,197건 전수 대조. 다섯 카드와 화살표(경계)는 전건 통과했다 — 고칠 것은 경계가 아니라 그 위의 규칙 여섯 개였다.
폴링 1번 = 사진 1장·시각 하나 — 응답도 배치 시각 하나만 주니 도메인과 같은 모양이다. 규칙 밖 상태코드 0건, 스냅샷 안 차량 중복 0건.
순번 1..N 완전연속이 3,139개 전 노선에서 성립. 중복 컬럼(정류장 이름·좌표 사본)은 원본과 불일치 0이라 버려도 안전.
모양은 완벽한 사진으로 번역되지만(축이 같음을 실측) 내용은 접근 중 2대뿐 — 핵심 입력인 "막 지나간 버스"가 27초 만에 응답에서 사라지는 것을 실측했다. 넣는 순간 "노선 전량" 약속이 조용히 깨진다.
실물이 지지한 결정 다섯
방면은 순번에서 유도「순번 < 회차」→상행이 197,240행 전건 성립. 원본의 방면 컬럼은 검증 후 버려도 된다.
사진 한 장에 시각 하나GBIS도 차량별 시각을 주지 않는다. 도메인이 응답 구조를 정확히 모사한다.
차량 ID 하나로 두 원천이 닫힘GBIS vehId ≡ 데이터드림 AUTOMB_ID, 103대 전량 동일 — 매핑표 자체가 불필요.
정원 3형태 (등재·관측하한·미상)신차 2대가 실제로 미상→관측하한으로 승격됐고, 관측하한 ≤ 등재가 41쌍 전건 성립.
상·하행 한 축회차점을 지나도 만석이 안 풀린다(통과 243쌍, 좌석 증감 중앙값 0). 축을 끊을 이유가 없다.
ID 참조 · append-only마스터에서 32대가 사라지고 정류장 이름이 75.8% 겹쳐도 — ID만 참조한 덕에 과거 사실이 안 흔들린다.
고칠 것 여섯 — 전부 카드가 아니라 규칙·필드
셋 다 같은 결론: 애그리거트 경계는 그대로 두고, 필드 추가·타입 교체로 해소된다.
문서 3곳의 선택식이 서로 달랐다(하나는 "가장 멀리 지나간 차"를 고르는 오기). argmin{마지막 통과 ≥ 내 순번} + 동률 타이브레이커로 확정 — 동률이 질의의 약 1%에서 확률을 94.7%↔46.6%로 뒤집는다.
30분 관측창이면 21%의 창에서 같은 차량이 두 바퀴째로 다시 온다. 창 안에서 바퀴(TripSegment)를 나눠 차량마다 최신 바퀴만 직전버스 후보로.
"방면은 순번에서 유도"가 회차 순번 없이는 계산 불가였다. Route 경로에 turnSeq 한 칸 추가(회차 = 첫 하행 정류장).
실측 20일의 92.7%가 수집 공백. 공백 기록(CollectionGap)이 없으면 재생기가 갭을 정상 구간으로 착각해 없는 통과를 만들어낸다.
노선 개편·정류장 폐지 때 순번의 의미가 버전에 묶인다. Route에 발효일 버전, 사진에 그 버전 각인, Stop은 삭제 대신 은퇴 표시. 시간표 빈칸은 0이 아니라 빈칸(마스터 결손 40%).
통과 표식은 정수 0이 아닌 별도 타입(순번과 실수로 비교 못 하게), 판정 근거는 저장됨|즉석 두 형태(무저장 프로토타입이 거짓 ID를 안 만들게), 사진에 수신 시각 복원(소스 시계 멈춤 감지).
번역층(어댑터)이 흡수하는 마찰 열 가지
| 마찰 (전부 실측) | 번역층의 처리 |
|---|---|
| 차량이 1대면 배열이 아니라 객체로 온다 — 하필 첫차·막차 시간대 | "객체면 1원소 배열로 승격"을 무조건 선행 |
| resultCode 4 = 데이터 없음 | 실패가 아니라 0대 사진(성공)으로 접수 |
| stateCd 0/1/2 (교차로/도착/출발) | 통과 표식·정차 여부로 정규화 — 201건 전건 통과 |
| 응답의 stationId와 순번 축이 어긋나면 조용한 오배치 | 순번↔정류장ID 대조 검증 후 stationId 폐기 |
crowded — 2층 표식(lowPlate=2 ⟺ 0, 44/44)과 동치 | 폐기. 좌석형에선 잔여석의 파생값이라 추가 정보 없음 |
plateNo · taglessCd · routeTypeCd | 어댑터 잔류, 코어 미전달 — routeTypeCd는 수집 분기 결정자 |
| 도착 API locationNo = staOrder − stationSeq (같은 축, 2/2 일치) | 변환이 가능해서 더 위험 — 타입 시그니처로 코어 진입 차단 |
| 순번 축에 미정차 지점 7개(3330) — "몇 정거장 전"이 부풀려짐 | 표시 단계에서 정차 정류장만 세는 변환 제공 |
| route_info의 종점은 축의 끝이 아니라 회차점(안양역, seq 43) | 축 구성은 routestation 마스터로만. 기·종점은 표시용 |
| 단발 위치 스파이크 — 한 관측만 물리적으로 불가능한 순번(20일 35건), 유령 직전버스를 만듦 | 원본 사진은 그대로, 증거 조립 때 그 1건만 후보 제외 + 제외 수 기록 |