Case study

Select 호텔 백엔드

backend·35 commits·2026

Architecture

pinch to zoom · drag to pan

Stack

  • Python 3.12
  • FastAPI
  • SQLAlchemy 2.0 async
  • asyncpg
  • Supabase PostgreSQL
  • httpx
  • cachetools TTLCache

External integrations

  • Sabre Hotel API
  • Supabase PostgreSQL (Supavisor pool)

Highlights

  • 55+ routes, 3-tier architecture
  • Multi-layer TTL cache (120-3600s)
  • Sabre circuit breaker 5/30s

문제정의

Select 호텔 도메인의 데이터는 네 개의 다른 소스에 흩어져 있었다 — Supabase의 정적 메타데이터(호텔·지역·브랜드·혜택), Sabre의 실시간 요금, 분석 이벤트 로그, 추천·후기·통계 콘텐츠. 프론트엔드가 각 소스를 직접 호출하면 세 가지 문제가 누적된다.

문제영향
인증·캐싱·에러 처리 중복프론트 코드가 비대해지고 정책이 갈라짐
외부 API(Sabre) 장애가 그대로 노출사용자가 5xx를 본다
분석 이벤트 기록이 산발적통계 일관성 부재
pinch to zoom · drag to pan

목표는 세 가지로 좁혔다.

  1. 통합 진입점 — 호텔 도메인 전체를 한 API로 노출.
  2. 외부 장애 격리 — Sabre 같은 외부 API 사고가 프론트에 새지 않게.
  3. 다층 캐시 — 빈번 조회를 평탄화해 DB·외부 API 부하 최소화.

구현

Day 1 스캐폴드 (2026-02-12)

24개 테이블 모델링이 선행되어 있어 하루 안에 모든 인프라를 푸시할 수 있었다 — SQLAlchemy 모델 + 호텔·메타·Sabre·분석·미디어 API + 통합 테스트 + Docker + CI/CD까지 한 번에.

항목내용
모델SQLAlchemy 2.0 async · 24 테이블 · Pydantic 스키마 매핑
API호텔 CRUD · 검색 · 지역/브랜드/체인 · 블로그·피처슬롯·추천·후기·통계 · Sabre 프록시 · 분석 이벤트
인프라Docker · Cloud Run · GitHub Actions

3-tier 구조 (Routes → Services → Repositories)

pinch to zoom · drag to pan

각 tier가 명확히 한 일만 하니, 캐시는 Service에만, DB는 Repository에만, 인증은 Router 진입점에만 박혀 있다. 변경 시 영향 반경이 한 tier로 제한된다.

외부 API 격리 — Sabre Circuit Breaker

SabreProxyService5회 실패 / 30초 윈도우 circuit breaker를 적용. Sabre가 흔들리면 5번 안에 회로를 열고, 30초 동안은 외부 호출 없이 빠른 실패를 반환한다. 프론트는 5xx 대신 명시적 "외부 일시 장애" 응답을 받아 처리할 수 있다.

다층 TTL 캐시

데이터 변화 빈도에 따라 캐시 수명을 다르게 설정.

도메인TTL이유
호텔 메타120s운영 중 변경 가능 (가격·상태)
지역·브랜드·혜택1800-3600s거의 정적
Sabre 요금(캐시 없음)실시간성 우선

비동기 분석 이벤트 (BackgroundTasks, 202 Accepted)

분석 이벤트는 응답 경로에서 빼냈다 — Router가 BackgroundTasks에 enqueue하고 즉시 202를 반환, 백그라운드에서 Repository를 통해 기록된다. 사용자 응답 지연 0, 통계 일관성 유지.

Supavisor 호환 (운영 학습)

배포 후 Supavisor connection pooler와 asyncpg의 prepared statement 캐시가 충돌했다. 해결: statement_cache_size=0. 운영에서 잡힌 사고가 코드에 영구 기록된 사례.

출시

시기주요 변경
Day 1 (2026-02-12)24 테이블 · 55+ 엔드포인트 · Sabre 프록시 · 분석 · Docker · CI/CD 한 번에
Week 2 (~02-25)API 완성도 — Promotions/Benefits/Tags CRUD · OpenAPI 메타데이터 · testimonials → reviews 리네이밍 · ruff format
Week 2 사고Supavisor 비호환 · statistics 500 (테이블 참조 교체) · Docker uv.lock 누락
Month 2 (2026-04-03)Sabre rates 새 가격 API 통합
Month 2 (2026-04-20)/regions에 country 필터·by-slug 라우트 추가 (이슈 #1440, #1441 대응) — 도시·국가 식별자 후속

Day 1의 빠른 푸시 이후, 운영 사고와 후속 요청을 점진적으로 흡수한 패턴이다.

결과학습

항목결과
통합 진입점FastAPI 단일 백엔드 · 55+ 엔드포인트
도메인 모델24 테이블 SQLAlchemy 2.0 async
외부 격리Sabre circuit 5회/30초
캐시 계층TTL 120 ~ 3600s 다층
분석 응답 지연0ms (BackgroundTasks 202)
운영 학습Supavisor statement_cache_size=0, statistics fix, Docker uv.lock

기술적·운영적으로 가져간 학습은 네 가지다.

  1. 도메인 모델링이 끝나 있으면 Day 1에 다 푸시할 수 있다. 24 테이블이 명확했기에 SQLAlchemy → Pydantic → CRUD → 라우터까지 일직선으로 갔다. 모델링이 흐릿한 채로 시작하면 같은 양을 일주일 내내 만진다.
  2. 외부 API는 circuit으로 격리한다. Sabre 장애를 프론트가 모르게 하는 게 백엔드의 본질적 책임 — 5xx를 흘리는 백엔드는 격리가 부족한 것.
  3. Pooler 호환성은 운영에서만 학습된다. Supavisor + asyncpg의 statement cache 충돌 같은 사고는 로컬 개발에서 안 잡힌다. 배포 후 사고가 났을 때 영구 기록(코드)에 새기는 것이 가장 견고한 메모.
  4. 분석은 응답 경로 밖으로 빼라. BackgroundTasks로 옮긴 순간 응답 지연이 0이 됐고, 동시에 분석 기록 누락도 사라졌다. 통계 일관성과 응답 속도는 트레이드오프가 아니다.
pinch to zoom · drag to pan

Next

  • 캐시 hit-rate 측정 + 자동 TTL 튜닝 — 도메인별 캐시 적중률을 Langfuse/Prometheus로 측정해 TTL을 데이터 기반으로 조정.
  • Circuit breaker 알림 + 자동 복구 점검 — 회로가 열릴 때 Slack 알림 + 절반 열기(half-open) 상태 점검 로직 추가.
  • 호텔매핑 과제 정합성privia-front-common이 시작점이 된 호텔매핑 과제의 도시·국가 식별자 모델과 정렬 (04-20 변경이 그 방향의 첫 걸음).