Case study

투어비스 항공 수수료

frontend·699 commits·2025–2026

Architecture

pinch to zoom · drag to pan

Stack

  • Next.js 15
  • React 19
  • TypeScript
  • TanStack Query
  • Zustand
  • OpenAI

External integrations

  • Stella Tourvis API
  • OpenAI GPT-4o
  • Supabase 로깅
  • Vercel

Highlights

  • orderId 첫 글자로 공급사 분류 (DOM/LCC/GDS/NDC)
  • Parser 16종 · LLM 폴백 2-way 라우팅
  • 공통 FeeCalculator Strategy로 결과 수렴

문제정의

투어비스 상담사는 고객 전화를 받으면서 항공권 취소·변경 수수료를 즉석에서 안내해야 한다. 21개 항공사 × 4개 공급사(DOM·LCC·GDS·NDC)의 84가지 조합 — 각 항공사별로 운임 등급(BASIC/STANDARD/...)·통화·NO-SHOW 규정이 다르다. 사람이 매번 정확히 외울 수 없다.

초기 시스템은 GPT-4o 단일 프롬프트로 21개 항공사 규정을 처리했다. 운영하며 세 가지 근본적 한계가 드러났다.

한계수치영향
속도건당 평균 53.5초1분 가까운 대기 — 통화 흐름에서 사용 불가
비용69M 토큰 누적일평균 454건 × 매 호출 = 비용 증가 곡선
비결정성같은 OrderId 두 번 조회 시 금액이 달라지는 케이스상담사 신뢰도 손상
pinch to zoom · drag to pan

핵심 재정의: "AI를 어떻게 최적화할까"가 아니라 "엔진 자체를 교체할까" 였다. 항공사 규정의 본질이 if/else 분기의 집합이라면, 확률적 AI 추론보다 결정론적 파싱이 구조적으로 더 적합하다.

구현

Phase 1 → 2 → 3 진화

7개월 동안 시스템은 세 단계로 진화했다. 각 단계는 직전 단계의 누적된 운영 데이터가 강제한 전환이었다.

pinch to zoom · drag to pan

Phase 1 → 2 (7d6e9d0f): 단일 프롬프트에 에어아시아(AK)의 백분율 환불 규정을 추가했더니, 다른 LCC 항공사의 정액 수수료 계산에 사이드 이펙트가 생겼다 — 항공사별 프롬프트 25개로 격리.

Phase 2 → 3 (ff50c154): CoT + Few-shot으로 정확도는 잡혔지만 속도는 그대로. 프롬프트 최적화는 느린 엔진을 튜닝하는 것이지 엔진을 교체하는 것이 아니다 — 엔진 자체를 결정론적 파서로 교체.

Strategy + Builder + Template Method

세 가지 디자인 패턴을 조합해 공급사 4개 × 항공사 21개 조합을 관리한다.

  • StrategyFeeCalculationStrategy 인터페이스로 공급사별(DOM/LCC/NDC/GDS) 전략 분리. AK(에어아시아)·BX(에어부산)는 LCC 내부 특수 규정 때문에 별도 전략으로 등록 우선순위 보유.
  • Builder — Fluent Interface로 컨텍스트 단계 조립: FeeCalculatorBuilder.create().withData(...).withSupplier("LCC").withAirline("TW").build().calculate()
  • Template MethodBaseFeeStrategy가 흐름(준비 → 그룹화 → 통합 → 정렬 → 집계)을 고정하고 각 전략이 consolidateFees() 등을 오버라이드. 파서 측도 BaseParser 상속 (DOM 2·LCC 8·GDS 5).

확장 포인트: 새 항공사 추가 시 수정 파일 3개(파서·프롬프트·JSONL)뿐. 기존 Strategy/Calculator 코드는 안 건드린다 (OCP).

AI를 보조 역할로 재배치

AI를 제거하지 않고 세 가지 역할로 재배치한 것이 핵심이었다.

역할동작트리거
폴백 엔진Phase 2의 25개 프롬프트가 작동파서가 처리 못 하는 5% 케이스 (미지원 항공사, 비정형 규정)
검증 게이트27개 검증 프롬프트로 파서 결과 교차 검증파서 정확도 지속 모니터링
데이터 생성기비정형 규정을 JSONL로 구조화신규 항공사 추가 시 파서 입력 생성

Phase 2에서 만든 25개 프롬프트는 폐기되지 않고 위 세 역할로 재활용됐다.

도메인 모델링

  • Discriminated UnionCancellationFee = DaysBeforeFee | RelativeTimingFee. 항공사 규정이 "N일 전"과 "출발 N시간 전"을 혼용하는 현실을 두 축으로 인정. (47d10475, 3번의 revert 끝에 정착)
  • MultiCurrencyTotal{ fees: CurrencyAmount[], isSingleCurrency } 스키마로 외화 수수료 관통. 29개 파일 변경. (31f84c5e)
  • 7개 커스텀 에러 + isRetryable — 재시도 가능 여부를 타입 레벨에서 결정. ParsingFailedError만 폴백 트리거. (509390a1)
  • MU 구간 불일치selectMostRestrictiveFee()로 보수적 추정. 외부 데이터 불일치는 수정이 아니라 대응 전략을 설계하는 문제. (e0ec44c0)

출시

운영 중인 시스템(일평균 454건)을 한 번에 전환할 수는 없었다. 공급사별 단계적 마이그레이션으로 트래픽 비중이 높은 순서대로 전환하면서, 각 단계마다 검증 대시보드로 95% 일치율 게이트를 통과해야 다음으로 넘어가게 했다.

pinch to zoom · drag to pan

핵심 안전장치는 자동 폴백이다. 95% 미만이면 자동으로 AI 폴백으로 우회 — 파서 버그가 나도 서비스는 중단되지 않는다. 검증 대시보드(/parser/validation)에서 운영 OrderId를 대량 입력하면 p-limit으로 3건씩 병렬 처리하면서 genByAPI vs genByParser 일치율을 실시간 집계한다.

운영 실적 (57일)

  • 총 처리: 25,851건 · 일평균 454건 · 최대 720건
  • 국내선(B): 2,473건 (9.6%)
  • 해외선(C): 23,378건 (90.4%)
  • 343개 테스트 케이스 (22 파일) 지속 검증

결과학습

항목Before (Phase 1)After (Phase 3)개선
처리 속도53.5초 / 건12.3초 / 건↓ 77%
토큰 비용69M19M↓ 72%
테스트 케이스145개343개↑ 136%
결정성같은 OrderId, 다른 금액같은 OrderId, 같은 금액상담사 신뢰 회복

기술적·전략적으로 가져간 학습은 다섯 가지다.

  1. 구조화된 규제 데이터는 결정론적 파싱이 더 적합하다. AI의 강점은 비정형 데이터 해석이지 정형 규칙의 반복 적용이 아니다.
  2. AI는 제거가 아니라 재배치. 주 엔진에서 빼고 폴백·검증·데이터 수집으로 옮기니 오히려 AI의 가치가 분명해졌다 — 파서가 못 푸는 5%에서 결정적 역할.
  3. 도메인 모델은 현실을 단순화하지 말고 인정한다. "시간 기준 단일 축으로 통일"이라는 깔끔한 설계 3번 revert 끝에 "두 축을 Union 타입으로 인정"이 정답이었다.
  4. 에러도 도메인 모델이다. instanceof + isRetryable 플래그 조합으로 폴백 분기를 컴파일 타임에 결정. 에러 메시지 문자열 비교는 오타에 취약하고 리팩토링에 깨진다.
  5. 마이그레이션은 리스크 관리. 완벽한 전환보다 안전한 전환. 단계적 + 자동 폴백으로 "실패해도 서비스는 유지"되는 구조가 운영의 본질.
pinch to zoom · drag to pan

Next

  • 검증 대시보드 자동화 — CI에 343개 테스트 + genByParser vs genByAPI 일치율 게이트를 묶어 회귀 차단.
  • 신규 항공사 추가 파이프라인 — AI 데이터 생성기가 만든 JSONL을 자동으로 파서 입력에 합류시키는 셀프-서브 흐름. 신규 항공사 온보딩이 코드 PR 한 번으로 끝나는 것이 목표.
  • 다중 통화 환율 통합 — KRW 단일 화면 vs 외화 분리 표시의 UX 결정을 운영 데이터로 검증.