본문으로 건너뛰기
개인FE+BE 웹 · 오늘 날짜 정각 15개 slot 재조회 · singleton KST · 고정 DTO projection

대신물류 배차 운영 원장

기획·FE·BE·배포·운영 단독

2026-01-01 — present

배차 검색·통계 원장의 수집을 singleton으로 분리하고 ARM64 웹·수집기를 운영 배포했다. 첫 시도 실패 후 기존 수집기를 복구하고, 재배포에서 백업 무결성·실제 변경 행의 API 일치를 확인했다. 운영자가 기록한 단일 배포 관측과 합성 10개 시나리오를 구분해 공개했다.

검증된 결과

오늘 데이터 수집

오늘 날짜 · 월~토 06:00–20:00 · 정각 15개 slot

오전·지연 공개 재수집

이전: 14시 이전 전일 선택

replica 중복

singleton 15개 slot

중복 실행 제거

이전: replica별 15개 slot

브라우저 데이터 경계

차량정보 공개 · 내부 ID·credential URL 제외

고정 public route DTO

이전: 원천·내부 필드 구분 불명확

← 좌우로 스크롤해 시나리오 보기 →

합성 데이터로 재현한 날짜별 배차현황, 노선 검색, 노선 상세의 세 단계 사용 흐름
실제 React UI 구조를 고정 synthetic fixture로 재현한 사용 시나리오이며 실시간 데이터 근거로 사용하지 않습니다.

문제 해결 과정

1

오전에도 원천에 오늘 데이터가 있었지만 14시 이전에는 전일을 선택하는 규칙 때문에 누락됐다

해결 과정

기본 수집일을 오늘 날짜로 고정하고 singleton KST scheduler가 월~토 06:00–20:00의 정각 15개 cron slot에서 재조회하도록 변경했다

결과

오전 데이터와 늦게 공개된 변경분을 정적 날짜·건수 문구 없이 후속 실행에서 반영할 수 있도록 변경

← 좌우로 스크롤해 시나리오 보기 →

오전 누락을 막는 오늘 날짜 재조회
오전 누락을 막는 오늘 날짜 재조회.14시 이전 전일 선택 규칙을 제거하고 월~토 06:00–20:00의 정각 15개 cron slot에서 오늘 날짜를 독립 재조회한다. 실패는 기록하며 즉시 retry/backoff 없이 다음 스케줄이 다시 확인한다.
2

web replica마다 scheduler가 시작되어 동일 수집이 중복 실행됐다

해결 과정

수집 책임을 web process 밖의 singleton KST scheduler 하나로 분리했다

결과

replica별 15개 cron slot 구성에서 singleton의 15개 slot으로 축소(프로세스 시작 시 1회 동기화 제외)

← 좌우로 스크롤해 시나리오 보기 →

replica별 중복 수집을 singleton 책임으로 분리
replica별 중복 수집을 singleton 책임으로 분리.각 web replica가 15개 cron slot을 등록하던 구조에서 수집 책임을 별도 singleton KST scheduler 하나로 이동했다.
3

운영 화면에 필요한 원천 필드는 제공하면서 내부·credential 필드는 차단해야 했다

해결 과정

method/path allowlist, 필수 필드 검증, 고정 출력 projection과 원천 URL contract로 운영 화면 필드만 통과시키고 내부 ID·알 수 없는 필드·credential-bearing tracking URL을 제외했다

결과

고정 출력 projection과 원천 URL allowlist를 redirect·payload·배포 회귀 경계와 함께 검증

← 좌우로 스크롤해 시나리오 보기 →

공개 route 응답과 인증 경로의 분리
공개 route 응답과 인증 경로의 분리.Next.js proxy 전체가 아니라 public route 응답에만 allowlist projection을 적용한다. exact method/path·schema·payload limit·redirect 차단을 거치며 인증 경로는 allowlist된 method/path만 별도로 forward한다.
4

합성 원천에서 정상 노선 1개가 빈 결과가 되고, 건수 1,234·수량 2,345가 1·2로 축소됐다

해결 과정

5행 미만 표를 버리던 조건을 제거하고 정수 파싱 전에 쉼표를 제거했다. 실제 EUC-KR 크롤러와 임시 SQLite를 loopback HTTP로 연결해 확인했다

결과

노선 1개와 1234·2345를 보존하고, 중복·재수집에도 날짜·노선코드별 한 행과 최초 저장 시각을 유지하는 동작을 재현했다

5

별도 writer가 DB 값을 10→20으로 정정해도 같은 reader는 프로세스 내부 1시간 캐시에서 10을 반환했다

해결 과정

다른 검색 경로와 같은 DB 직접 조회로 전환하고 캐시·DI 의존성을 제거했다. 독립된 Prisma 연결 두 개로 지연 추가·당일 및 과거 정정을 재현했다

결과

공개 합성 재현 10개 시나리오에서 같은 reader가 1234→1250 정정을 다음 요청에서 확인했다. HTTP 503과 배치 중간 DB 쓰기 실패 모두 이전 저장 상태를 보존했고, 실패 원인을 제거한 뒤 명시적 재실행으로 변경을 반영했다

6

writer lock 때문에 수집이 거절돼도 기존 데이터가 보존되고 같은 작업을 재실행할 수 있는지 확인해야 했다

해결 과정

100·1,000행 부하와 150ms 목표 lock 경합에 거절 실험을 추가했다. 별도 fixture 클라이언트에만 SQLite 대기 한도 1초를 설정하고 실제 UPDATE lock을 유지했다. 독립 reader로 상태를 검사한 뒤 lock을 해제하고 같은 응답으로 명시적 재실행 1회를 수행했다. 새 임시 DB에서 전체 실험을 두 번 실행했다

결과

Node 24·macOS arm64 합성 관측: 짧은 경합 6회는 해제 후 저장에 성공했다. 별도 거절 6회 모두 P1008을 확인했고 DB 전체 행·ID·시각이 보존됐다. 해제 후 재실행 6회도 수정·추가와 기존 ID·시각 보존을 통과했다. 운영 처리량·SLA나 장애 복구 보장은 아니다

7

ARM64 운영 배포 첫 시도가 OperationalError로 실패해 기존 수집기와 route를 복구했다. 원래 SQLite 세부 오류 코드는 기록되지 않았다

해결 과정

합성 교차 런타임 재현을 근거로 실시간 SQL 관측·백업을 writer와 같은 Linux VM으로 옮기고 호스트는 완료된 백업 사본만 열도록 수정했다. 원래 장애의 정확한 원인이 입증된 것은 아니다. 읽기 전용 원천 백업의 무결성·격리 조회를 확인한 뒤 재배포했다

결과

운영자가 기록한 단일 배포에서 native ARM64 웹·수집기, health gate와 실제 변경 행의 API 일치를 확인했다. DB 변경 감지→API 일치 115.4ms(관측 poll 간격 1.287초), 1초 이상 간격의 순차 데이터 조회 10회 중앙값 27.178ms·범위 23.487–34.199ms. 모든 gate 통과 후 이전 slot을 중지·보존했으며 DB 복원은 없었다

시스템 아키텍처와 운영 경계

수집은 오늘 날짜를 각 정각에 독립 재조회해 crawler와 SQLite로 전달합니다. 조회는 Next.js proxy의 public route DTO branch에서 공개 필드만 투영한 뒤 Traefik과 Express active slot을 거칩니다. 인증 경로는 별도로 전달하고, 비활성 slot은 health check 통과 뒤 전환합니다.

← 좌우로 스크롤해 수집·조회·배포 흐름 전체 보기 →

오늘 날짜 독립 재조회 수집 흐름, Next.js proxy의 public route DTO branch 조회 흐름, health-gated slot switch와 manual rollback 경계

운영 한계

운영 증거는 운영자가 기록한 단일 배포의 제한된 smoke 관측이며 독립 검증이 아니다. 공개 소스·CI 및 합성 10개 시나리오와 구분하며 최신성 SLA·가동률을 증명하지 않는다.
합성 실험은 단일 macOS arm64 호스트의 작은 표본이며 실행별·크기별 n=3의 p95는 최댓값이다. 거절 실험의 1초 대기 한도는 fixture 전용으로 운영 설정과 다르다. BEGIN IMMEDIATE에서 쓰기 전 거절될 수 있어 중간 쓰기 rollback 증거는 기존 trigger 시나리오와 구분한다.
upsert-only라 후속 원천에서 빠진 행은 남고 createdAt은 최초 저장 시각이다. 실패는 다음 정각에 다시 확인하며 즉시 retry/backoff는 없다.
노선코드 검색은 요청마다 SQLite를 조회한다. 10회 순차 조회는 용량·처리량·p95·전후 개선율의 근거가 아니며 수평 쓰기 확장에는 한계가 있다.
115.4ms는 DB 변경 감지→API 일치 구간이다. 관측 poll 간격은 1.287초이며 DB commit 시각은 관측하지 않아 원천 변경·commit→API 지연으로 해석할 수 없다.
비활성 slot health check 후 전환하며 문제 발생 시 이전 slot을 복구한다. 첫 OperationalError의 정확한 SQLite 세부 코드는 미기록이며 합성 재현은 같은 VM에서 관측·백업하는 수정의 근거이지 원래 원인의 확증이 아니다.

Claim → 공개 근거

각 주장에 대응하는 공개 근거를 연결합니다. 소스·테스트와 운영 관측 기록의 검증 범위는 구분합니다.

배포 소스 revision — 실행 기록과 별개

어느 소스를 배포 근거로 고정했는가? (소스 자체는 운영 실행 증거가 아님)

수집 품질·운영 의존성 CI

전체 회귀·합성 수집·운영 의존성 감사·최종 이미지 실행을 통과했는가?

기술 스택

TypeScriptExpressNext.jsReactPrismaSQLiteCheerioTSyringeTanStack QueryDockerTraefikTailwind CSSVitest