"balance 0.51 — 양호해 보였던 결과"가 함정이었다.
강남 1권역에서 괜찮아 보이던 V1을 6개 권역에 동일하게 적용하니, 권역 모양에 따라 결과 품질이 최대 4배까지 떨어졌다.
이 글은 그걸 어떻게 측정으로 드러냈고, 그 측정이 V2 작업(일반화와 3단계 검증)의 출발점이 됐는지에 대한 이야기다.

요약
- V1 은 강남 1 권역 측정 결과 (balance 0.51, span 1.89km) 만으로 채택했다.
- 6 권역 × 5 회 = 30 시나리오로 다시 측정하니 권역 모양에 따라 결과가 갈렸다.
- 길쭉 권역(강남·수원)은 공간 outlier, 작은 조밀 권역(부천)은 건수 불균형, 중간 둥근 권역(관악)은 우연히 잘 동작.
- "한 권역에서 잘 동작" ≠ "알고리즘이 일반화됨". 이 차이를 인지한 게 V2 작업의 출발점.
- 합성 데이터로 V2 채택 후 production 측정에서 V1 사례가 그대로 재현된 경험까지. 회귀 검증이 단계적이어야 하는 이유.
1. V1 채택 시점의 결과는 분명히 좋아 보였다
V1 알고리즘을 처음 측정한 권역은 강남이었다. 결과는 다음과 같았다.
| 지표 | 값 |
|---|---|
| balance score | 0.51 |
| max span | 1.89km |
| compact 비율 (span < 3km) | 82% |
| scattered 비율 (span ≥ 5km) | 8% |
그때는 이게 양호한 결과로 보였다. 1.89km 면 한 기사가 평균 약 2km 안에서 움직인다는 의미고, balance 0.51 이면 절반 이상 균등하다고 해석할 수 있었다. scattered 4명은 운영 조정으로 흡수 가능한 수준이라고 판단했다. 이 결과로 V1 을 채택했고 시스템을 그 위에 쌓아 올렸다.
2. 그런데 한 권역만 봤다는 사실은 의외로 큰 함정이었다
V2 작업을 시작하면서 가장 먼저 한 건 "다른 권역도 잡아보자" 였다. 6 권역(강남·관악·수원·부천·종로·미추홀)을 각 5 회씩 돌려서 baseline 을 잡았다.
결과 패턴이 명확했다.
| 권역 | 특성 | balance | max span | 평가 |
|---|---|---|---|---|
| se-ga (관악) | 중간 둥근 | 0.37 | 2.99km | 양호 |
| ic-mch (미추홀) | 중간 | 0.23 | 2.98km | 보통 |
| se-gn (강남) | 길쭉 고밀도 | 0.26 | 6.97km | 공간 outlier |
| gg-sw (수원) | 큰 외곽 | 0.22 | 5.96km | 공간 outlier |
| se-jn (종로) | 작음, 178건 | 0.21 | 3.93km | 보통 |
| gg-bc (부천) | 작고 조밀 | 0.14 | 3.70km | 건수 불균형 |
여기서 두 가지가 동시에 드러났다.
첫 번째 — 강남이 앞서 봤던 1.89km 가 아니다. 같은 강남인데 6.97km 가 나왔다. 측정 방식의 차이일까? 살펴보니 그렇지 않았다. 처음 측정은 합성 데이터였고, 이번은 production 데이터였다. 데이터 분포의 차이가 결과를 4 배 이상 벌어졌다.
두 번째 — 권역 모양에 따라 결과 품질이 갈린다.
- 길쭉한 권역(강남·수원) → 한 기사가 권역 끝에서 끝까지 6~7km 를 도는 공간 outlier 발생
- 작고 조밀한 권역(부천) → 권역이 좁아 공간 outlier 는 없지만 건수가 한쪽으로 몰림
- 중간 둥근 권역(관악) → 우연히 잘 동작
이게 무엇을 의미할까 고민했고, 알고리즘 자체가 권역 모양에 robust 하지 않다는 뜻이라는 것을 깨달았다. 즉, 강남에서 잘 됐던 건 강남이 운 좋게 알고리즘과 맞았던 거였다.
3. "측정"과 "코드 정독"이 같이 있어야 진단이 된다
이 패턴을 본 다음에 V1 코드를 정독했다. 측정 결과만으로는 "왜 권역 모양에 따라 결과가 다르지?" 라는 질문에 답할 수 없었다. 코드 안에 답이 있었다.
원인은 크게 두 가지였다
- 알고리즘 안의 두 결정적 결함이었고
- 운영 측면에서는 1500 회의 noop 으로 작동하고 있었다. 어떤 결함이었는지는 별도 글에서 자세히 다루겠습니다.
여기서 배운 게 있다. 측정은 증상을 보여주고, 코드 정독은 원인을 보여준다. 둘 중 하나만 있어도 진단이 안 된다. 코드만 봤을 때는 결함이 있다는 직관이 떠오르지 않았고 (1500 회 반복 코드는 정상적으로 보였다), 측정만 봤을 때는 권역마다 결과가 다르다는 사실은 알지만 왜인지 몰랐다.
4. V2 작업의 출발점 — 채택 기준을 코드 작성 전에 합의
- V1 의 함정을 봤기 때문에, V2 는 다르게 시작하기로 했다.
- V1: "더 좋은 결과가 나오면 채택" → 강남 한 권역만 봤음.
- V2: "코드 작성 전에 채택 기준을 합의" → 6 권역 모두 동시 만족 시에만 채택.
| 메트릭 | V1 baseline | V2 채택 기준 |
|---|---|---|
| max span — se-gn (강남) | 6.97km | ≤ 4.9km (30% 감소) |
| max span — gg-bc (부천) | 3.70km | ≤ 3.0km |
| balance — gg-bc (최악) | 0.14 | ≥ 0.30 |
| balance — se-gn | 0.26 | ≥ 0.40 |
| balance — se-ga (회귀 방지) | 0.37 | ≥ 0.36 |
기준선이 코드 변경의 방향을 결정한다. 측정 → 진단 → 설계 → 측정 사이클이 V2 작업 전체를 관통하는 구조가 됐다.
5. 그런데 합성 측정은 또 한 번의 함정이 됐다
V2 알고리즘 설계 후 합성 데이터로 V1 vs V2 를 비교했다 (6 권역 × 5 회 = 30 시나리오).
결과는 V2 의 승.
- balance 12승 / 2패 / 16 동률,
- span 7-7 동률.
- "회귀 없음"
으로 V2 채택을 결정했다.
그런데 production 데이터(2,870건/45명)에서 측정해보니 V1 의 사례가 그대로 재현됐다. 강남 6.97km, 그리고 "한 기사가 강남역 전체를 도는 span 5.89km 의 기사" 문제도 그대로였다. 합성 데이터(90건/3명)에서는 단일 점수 함수 통합으로도 충분히 동작했지만, production scale (큰 H3 셀이 다수 발생, 다수 드라이버가 밀집하게 위치) 에서는 다른 차원의 문제가 있었다.
여기서 또 하나의 명제가 도출할 수 있었다.
합성 측정에서 회귀 없음 ≠ production 안전성.
이후 검증은 항상 3 단계로 한다.
- 합성 데이터 — 90건/3명 단위. 알고리즘 정합성 확인.
- 소규모 production — 1 권역 production data. 도메인 데이터 품질 영향 확인.
- 전 권역 production — 6+ 권역 production data. 권역 모양 일반화 확인.
6. V1 의 함정이 V2 의 자산이 됐다
돌이켜보면 V1 의 한계를 빠르게 잡아낸 게 V2 작업의 가장 큰 자산이었다. V1 이 처음부터 6 권역에서 잘 됐다면 V2 작업의 동기 자체가 약했을 거고, 합성 측정의 함정도 그 다음 사이클에서야 발견했을 거다.
배운 점을 정리해 본다.
- 한 권역 측정으로 채택은 위험하다. 알고리즘이 권역 모양과 무관하게 일반화됨을 보장하지 않는다.
- 측정과 코드 정독은 한 쌍이다. 측정만 보면 증상은 알지만 원인은 모르고, 코드만 보면 직관이 안 온다.
- 합성 데이터는 production 안전성을 보장하지 않는다. scale 이 달라지면 알고리즘의 다른 면이 노출된다.
- 채택 기준은 코드 작성 전에 합의해 둔다. 그래야 결과가 안 나왔을 때 "재설계 복귀" 라는 옵션이 살아있다.
다음 글은 이번 프로젝트에서 가장 신경 써서 설계한 영역인 실시간 추적 알림에 대해 쓸 예정이다. 음식 배달 식의 위치 노출이 새벽 배송에서는 왜 안 맞는지, 어떻게 우회 설계했는지에 대한 이야기다.
'Project > 배송 권역 시스템' 카테고리의 다른 글
| [배송 권역 시스템 4편] 새벽 배송 알림 설계 — 기사 위치를 직접 노출하지 않는 이유 (0) | 2026.04.28 |
|---|---|
| [배송 권역 시스템 2편] 수도권 새벽 배송 자동 배차 시스템 – 구현을 하면서 깨달은 것들 (0) | 2026.04.11 |
| [배송 권역 시스템 1편] 배송 권역 시각화 시스템을 설계하며 고민한 것들 (0) | 2026.04.05 |
