Select 호텔 백엔드
Architecture
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를 본다 |
| 분석 이벤트 기록이 산발적 | 통계 일관성 부재 |
목표는 세 가지로 좁혔다.
- 통합 진입점 — 호텔 도메인 전체를 한 API로 노출.
- 외부 장애 격리 — Sabre 같은 외부 API 사고가 프론트에 새지 않게.
- 다층 캐시 — 빈번 조회를 평탄화해 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)
각 tier가 명확히 한 일만 하니, 캐시는 Service에만, DB는 Repository에만, 인증은 Router 진입점에만 박혀 있다. 변경 시 영향 반경이 한 tier로 제한된다.
외부 API 격리 — Sabre Circuit Breaker
SabreProxyService에 5회 실패 / 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 |
기술적·운영적으로 가져간 학습은 네 가지다.
- 도메인 모델링이 끝나 있으면 Day 1에 다 푸시할 수 있다. 24 테이블이 명확했기에 SQLAlchemy → Pydantic → CRUD → 라우터까지 일직선으로 갔다. 모델링이 흐릿한 채로 시작하면 같은 양을 일주일 내내 만진다.
- 외부 API는 circuit으로 격리한다. Sabre 장애를 프론트가 모르게 하는 게 백엔드의 본질적 책임 — 5xx를 흘리는 백엔드는 격리가 부족한 것.
- Pooler 호환성은 운영에서만 학습된다. Supavisor + asyncpg의 statement cache 충돌 같은 사고는 로컬 개발에서 안 잡힌다. 배포 후 사고가 났을 때 영구 기록(코드)에 새기는 것이 가장 견고한 메모.
- 분석은 응답 경로 밖으로 빼라.
BackgroundTasks로 옮긴 순간 응답 지연이 0이 됐고, 동시에 분석 기록 누락도 사라졌다. 통계 일관성과 응답 속도는 트레이드오프가 아니다.
Next
- 캐시 hit-rate 측정 + 자동 TTL 튜닝 — 도메인별 캐시 적중률을 Langfuse/Prometheus로 측정해 TTL을 데이터 기반으로 조정.
- Circuit breaker 알림 + 자동 복구 점검 — 회로가 열릴 때 Slack 알림 + 절반 열기(half-open) 상태 점검 로직 추가.
- 호텔매핑 과제 정합성 —
privia-front-common이 시작점이 된 호텔매핑 과제의 도시·국가 식별자 모델과 정렬 (04-20 변경이 그 방향의 첫 걸음).