의료택시 2.0 · AI 배차 최적화

🚕 배차 최적화, 하나하나 쉽게

"택시 여러 대로 손님 여러 명을 어떻게 나눠 태울까?"
이 문서 하나로 우리가 만든 AI가 정확히 무슨 일을 하는지 이해하도록 씁니다. 수학 몰라도 됩니다.

1 먼저, 무슨 문제를 푸는 걸까?

의료택시는 이런 상황입니다. 택시 몇 대가 있고, 어르신 여러 명이 각자 다른 마을에서 다른 병원으로 가야 합니다. 시간도 제각각(9시 예약, 10시 예약…).

여기서 "누가 누구를 태우고, 어떤 순서로 도느냐"를 정하는 게 배차입니다. 아무렇게나 정하면 택시가 빈 채로 이리저리 왔다갔다 하면서 기름·시간을 낭비하죠.

🎯 목표: "빈 차로 달리는 거리(공차)"를 최대한 줄이는 배차를 찾는 것.

2 '공차'와 '실차'부터 이해하기

택시 주행은 딱 두 종류로 나뉩니다.

차고(택시) 어르신 집 병원 공차(빈차) 실차(손님 탑승)

택시는 차고에서 어르신 집까지 빈 차(공차)로 갔다가, 집→병원은 손님을 태우고(실차) 갑니다. 우리가 줄일 수 있는 건 저 주황 점선(공차)뿐입니다.

3 아주 작은 예제를 손으로 풀어보자

택시 1대, 어르신 2명이 병원에 갔다 와야 합니다. A는 문막, B는 지정에 삽니다. 진료가 끝나면 집에 돌아가야 하죠.

방법 1 — 아무 생각 없이 (한 명씩 왕복)
차고→A집→병원(A내림)→A집으로 빈차 복귀→다시 병원으로 빈차… 이렇게 하면 빈차 이동이 엄청 많습니다.
방법 2 — 똑똑하게 (이어붙이기)
A를 병원에 내려주는 그 순간, 마침 진료가 끝난 B가 병원에 있다면?
→ 그 택시가 빈 채로 돌아가지 않고 곧바로 B를 태워 지정(B집)으로 갑니다.
"A를 데려다준 길""B를 데려오는 길"과 이어져 빈차 구간이 사라집니다.
이 '이어붙이기'가 바로 회송 체이닝입니다. "누군가의 등원 종료 = 다른 이의 귀가 시작"
병원 A집(문막) B집(지정) ① A 등원(실차) ② B 귀가(실차) ★ 병원에서 A 내리고 → 곧바로 B 태움 빈차 복귀 없음 = 공차 0

🔁 회송 체이닝, 더 풀어서

회송(回送) = 손님을 내려준 택시가 빈 채로 돌아가는 것. 이 빈 주행이 공차의 가장 큰 원인입니다. 체이닝(chaining) = 그 빈 주행을 다른 운행과 사슬처럼 이어붙이는 것.

한 문장: "A를 병원에 데려다준 택시가, 빈 차로 돌아가는 대신 그 병원에서 진료 끝난 B를 태워 B의 집으로 간다."

왜 좋은가? 원래라면 두 번의 빈 주행이 생깁니다 — ①A 내려주고 병원→빈차 복귀, ②B 태우러 다시 빈차로 병원行. 체이닝은 이 둘을 하나로 합쳐 없애버립니다.

체이닝 없음체이닝 있음
A 하차 후병원→A동네 빈차 복귀그 자리서 대기
B 태우기B동네→병원 빈차 이동바로 B 승차
공차2구간 낭비0 (사슬로 연결)

중요 — 이건 '합승'이 아닙니다. A가 내린 뒤에 B를 태웁니다. 차 안에 동시에 두 팀이 타지 않아요(정원=1). 어르신 입장에서 낯선 사람과 합승하는 불편이 전혀 없습니다. 단지 택시의 빈 시간·빈 거리를 이어붙이는 겁니다.

즉 회송 체이닝 = "누군가의 등원이 끝나는 병원·시각""누군가의 귀가가 시작되는 병원·시각" 을 맞춰서, 한 택시가 쉬지 않고 연달아 태우게 만드는 것. 이 '맞추기'를 OR-Tools가 하루 전체 예약을 보고 계산합니다.

현실 조건도 반영합니다: ① 시각이 맞아야 함(A 하차 ≈ B 진료 종료) ② 같은/가까운 병원이어야 함 ③ 진료가 예정보다 늦어지면 그 사슬은 포기하고 다른 택시로 재배차(그래서 '진료완료' 신호로 확정). 못 맞추면 그냥 일반 배차 — 손해는 없습니다.

4 그런데 왜 '어려운' 문제일까?

손님이 2명이면 눈으로도 풀립니다. 그런데 하루에 수십 명, 택시 여러 대가 되면?

"누구를 어느 택시에, 어떤 순서로" 배정하는 경우의 수가 폭발합니다.

손님 수가능한 방문 순서의 수
3명6가지
5명120가지
10명362만 가지
15명1조 3천억 가지

여기에 "택시 여러 대", "시간 약속(9시 예약은 9시까지)", "왕복" 조건까지 붙으면 — 사람이 손으로 최선을 찾는 건 불가능합니다. 이걸 학문적으로 차량경로문제(VRP), 그중 병원 왕복형은 DARP라고 부릅니다.

🤔 "그럼 이 어려운 걸 우리가 다 프로그래밍해야 하나요?" → 아니요! 다음 단계가 핵심입니다.

5 핵심: 어려운 계산은 'OR-Tools'가 대신 해준다

구글이 만든 무료 최적화 엔진 OR-Tools가 있습니다. 이 폭발하는 경우의 수를 똑똑하게 탐색해서 좋은 답을 빠르게 찾아주는 도구예요. (전 세계 물류회사가 씁니다.)

우리가 할 일은 "문제를 설명해주는 것"뿐입니다. 마치 내비게이션에 목적지만 찍으면 경로는 내비가 계산하듯이요.

우리가 OR-Tools에게 알려주는 4가지 (=모델링)
알려줄 것의미
① 지점들각 어르신 집·병원의 위치(좌표)
② 시간 약속"이 어르신은 9시 예약" → 그 전에 도착
③ 정원 = 1택시 1대에 1팀만 (억지 합승 금지)
④ 비용 = 거리"총 이동거리가 짧을수록 좋다"고 알려줌

이 4가지만 넣으면 OR-Tools가 "몇 번 택시가, 누구를, 몇 시에, 어떤 순서로"를 스스로 계산해서 돌려줍니다. 빈차(공차)가 최소가 되도록요.

💡 정리: 어려운 알고리즘 = OR-Tools 담당. 우리 = 문제를 좌표·시간·규칙으로 번역해서 넣기만.

6 "총거리 최소 = 공차 최소"인 이유

OR-Tools에게 "총 이동거리를 줄여라"라고만 시켰는데, 왜 공차가 줄어들까요?

총거리 = 실차 + 공차 입니다. 그런데 실차는 못 줄여요(손님은 무조건 집→병원 가야 하니까 고정). 그래서 총거리를 줄이라고 하면, OR-Tools가 건드릴 수 있는 건 공차뿐 → 자연스럽게 빈차 이동을 최소화하는 답(=회송 체이닝을 많이 쓰는 답)을 찾아냅니다.

7 혹시 OR-Tools가 못 풀면? (안전장치)

만약 예약이 너무 많거나 시간이 촉박해서 OR-Tools가 제한시간 안에 최선을 못 찾으면 — 답이 아예 안 나오면 큰일이죠. 그래서 그리디(greedy)라는 간단한 백업 방법을 같이 넣었습니다.

그리디 = "각 예약마다, 지금 가장 가까운 택시에 그냥 붙인다." 최선은 아니어도 항상 답이 나오고 빠릅니다.

OR-Tools(최선) → 안 되면 그리디(항상 됨) 2단 구조라 서비스가 멈추지 않습니다.

8 "우리 규모에선 사실 안 어렵다"

배차 최적화가 어렵다는 건 차량 수천~수만 대 얘기입니다(예: 전국 택배). 우리 의료택시는?

규모가 작아서 오히려 깔끔하게 최적해가 나오는 이상적인 케이스입니다.

9 이건 진짜 'AI'인가?

솔직하게 — 딥러닝·자율주행·챗봇은 아닙니다. 우리가 쓰는 AI는 두 가지이고, 둘 다 정통 인공지능 기술입니다.

이 둘은 맞물려 돕니다: ②가 "이 어르신 10시 40분쯤 진료 끝남"을 정확히 맞힐수록, ①이 그 시각에 맞춰 회송 택시를 더 잘 이어붙여 공차가 줄어듭니다.

심사위원이 "이게 왜 AI냐" 물으면: "차량경로 최적화(DARP)는 인공지능·운영과학의 조합최적화 분야이고 구글 OR-Tools로 구현했으며, 귀가시각 예측은 데이터로 학습하는 머신러닝(scikit-learn)입니다." — 최적화 + 학습, 둘 다 있으니 과장 없이 'AI 활용'이 확실합니다.

10 한 장 요약

① 문제: 택시로 어르신들 병원 통원 — 빈차(공차)를 줄이고 싶다
② 방법: 회송 체이닝(내려준 택시가 곧바로 다른 귀가 손님을 태움)
③ 계산: 경우의 수가 폭발 → 어려운 부분은 구글 OR-Tools가 담당
④ 우리 몫: 좌표·시간·정원·거리로 문제를 번역해서 넣기 + 그리디 백업
⑤ 규모가 작아 1초 내 최적해 / 이미 코드로 구현됨(optimizer.py)

📄 실제 구현 파일: ai_dispatch/app/services/optimizer.py (OR-Tools DARP + 그리디 폴백, DB 없이 순수함수라 단독 테스트 가능). 웹 데모(원주_AI배차_웹데모.html)는 이 원리를 브라우저에서 가볍게 재현한 버전입니다.