Phase 1: Sabre 검색 API 심층 분석
1. API 엔드포인트 개요
1.1 검색 API
- 엔드포인트:
POST /internals/SABRE/search - API 타입: REST API (BargainFinderMax v5)
- Controller:
SabreSearchController.kt:25-42 - Service:
SabreFlightSearchService.kt:47-129 - Client:
SabreRestClient.kt:98-202
1.2 상세 조회 API
- 엔드포인트:
GET /internals/SABRE/search - Controller:
SabreSearchController.kt:44-46 - Service:
SabreFlightSearchService.kt:145-152
1.3 Revalidate API
- API 타입: REST API
- Service:
SabreFlightSearchService.kt:182-200 - Client:
SabreRestClient.kt:204-265
2. 핵심 비즈니스 로직 플로우
2.1 전체 처리 흐름도
flowchart TD A[요청 수신] --> B[캐시 키 생성] B --> C{Redis 캐시<br/>확인} C -->|캐시 히트| D[캐시된 결과 반환] C -->|캐시 미스| E[REST Token 발급<br/>Redis 캐싱] E --> F[공항 코드 확장] F --> G[Cartesian Product<br/>모든 조합 생성] G --> H[병렬 검색 실행<br/>pmap] H --> I[REST 요청<br/>BargainFinderMaxRQ] I --> J{에러 체크} J -->|무시 가능한 에러| K[빈 리스트 반환] J -->|정상| L[결과 파싱] L --> M[필터링 1:<br/>항공사 매칭] M --> N[필터링 2:<br/>child/infant 운임 체크] N --> O[필터링 3:<br/>NonAir 제외] O --> P[필터링 4:<br/>NonTicketableCarrier 제외] P --> Q[결과 병합<br/>flatten] Q --> R[중복 제거<br/>distinctBy] R --> S[출발/도착 검증] S --> T{캐싱 조건<br/>확인} T -->|useCache=true &<br/>결과 존재| U[Redis 저장] T -->|조건 미충족| V[응답 반환] U --> V[응답 반환] 다크/라이트 모드 호환 색상 style B fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style D fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style F fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style Z fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000 style G fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
3.2.2 NonAir 제외
위치: SabreRestClient.kt:459-465
private fun FareItinerary.isNonAir(): Boolean {
return this.schedules.any { schedule ->
schedule.segments.any {
NonAirEquipment.existValueOf(it.equipmentType)
}
}
}NonAir 장비 타입
- BUS: 버스
- TRN: 기차
- LMO: 리무진
- 기타 비항공 운송 수단
3.2.3 NonTicketableCarrier 제외 (OZ 항공사 특별 처리)
위치: SabreRestClient.kt:471-481
private fun FareItinerary.hasNonTicketableCarrier(): Boolean {
val excludeMarketingCarriers = listOf("CM", "A3", "FI", "S7", "AM", "VN")
val excludeOperatingCarriers = listOf("CM", "A3", "FI", "AM")
// 제한 사유:
// 1. 좌석 조회 시 HK이나 예약 생성 시 UC로 상태코드 변경으로 발권 불가
// 2. VC: OZ, Marketing carrier: S7일 경우 정산 불가 이슈로 발권 불가
return this.validatingCarrier == "OZ" && this.schedules.any { schedule ->
schedule.segments.any {
excludeMarketingCarriers.contains(it.marketingCarrier) ||
excludeOperatingCarriers.contains(it.operatingCarrier)
}
}
}제외 대상
| 항공사 코드 | 항공사명 | 제외 유형 | 제외 이유 |
|---|---|---|---|
| CM | Copa Airlines | Marketing/Operating | 상태 코드 변경 이슈 (HK → UC) |
| A3 | Aegean Airlines | Marketing/Operating | 상태 코드 변경 이슈 |
| FI | Icelandair | Marketing/Operating | 상태 코드 변경 이슈 |
| S7 | S7 Airlines | Marketing | OZ 정산 불가 이슈 |
| AM | Aeromexico | Marketing/Operating | 상태 코드 변경 이슈 |
| VN | Vietnam Airlines | Marketing | 상태 코드 변경 이슈 |
판별 로직
flowchart TD A[FareItinerary] --> B{Validating Carrier<br/>== OZ?} B -->|No| C[Ticketable] B -->|Yes| D{Marketing Carrier<br/>in excludeList?} D -->|Yes| E[NonTicketable<br/>제외] D -->|No| F{Operating Carrier<br/>in excludeList?} F -->|Yes| E F -->|No| C 다크/라이트 모드 호환 색상 style B fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style C fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style D fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style H fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style J fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style K fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style M fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style I fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000 style L fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
9.2 항공사 필터링 코드
위치: SabreFlightSearchService.kt:164-180
private fun List<FareItinerary>.filterIncludedAirline(
airlines: List<String>?,
sotoAirlines: List<String>?,
): List<FareItinerary> {
return if (airlines == null) {
this
} else {
this.filter {
if (it.tripDirectionType == TripDirectionType.INBOUND &&
!CityUtils.isDomesticAirport(it.departure)
) {
// INBOUND이고 국내 공항 출발이 아니면 sotoAirlines 체크
sotoAirlines?.contains(it.validatingCarrier) ?: false
} else {
// 그 외는 airlines 체크
airlines.contains(it.validatingCarrier)
}
}
}
}10. Amadeus vs Sabre 비교
| 항목 | Amadeus | Sabre |
|---|---|---|
| API 타입 | SOAP | REST |
| 인증 방식 | Session Token (Stateful) | OAuth 2.0 (Stateless) |
| 엔드포인트 | FareMasterPricerTravelBoardSearch | BargainFinderMax v5 |
| Token 캐싱 | 없음 | Redis (만료 전까지) |
| 병렬 처리 | cartesianProduct + pmap (SOAP) | cartesianProduct + pmap (REST) |
| 타임아웃 | 기본 타임아웃 | 30초 (searchClient) |
| Circuit Breaker | 있음 (Resilience4j) | 없음 |
| 에러 처리 | 에러 코드 목록 (866, 931, 977, 996, 118, 950) | 에러 메시지 목록 (NO AVAILABILITY, NO FARE, …) |
| NonAir 제외 | 있음 | 있음 (동일) |
| NonTicketableCarrier | MH 항공사 | OZ + excludeCarriers (CM, A3, FI, S7, AM, VN) |
| Revalidate | SOAP | REST |
| Multi-ticket | 필터링 단계에서 처리 | 검색 요청에 포함 |
11. 주요 발견사항 및 특징
11.1 REST API 기반의 장점
- 무상태 (Stateless): Session 관리 불필요
- Token 재사용: Redis 캐싱으로 성능 향상
- 표준 HTTP: 디버깅 및 모니터링 용이
- JSON 응답: 파싱 간편
11.2 항공사 특별 처리
- OZ 항공사: NonTicketableCarrier 체크 (CM, A3, FI, S7, AM, VN)
- 상태 코드 변경 이슈: HK → UC 변경으로 발권 불가
- 정산 불가 이슈: OZ + S7 조합 시 정산 불가
11.3 에러 처리 전략
- 무시 가능한 에러: 빈 리스트 반환 (검색 결과 없음)
- 부분 실패 허용: 일부 성공 시 계속 진행
- Revalidate 실패: null 반환 → 원본 사용
11.4 성능 최적화
- Token 캐싱: 불필요한 OAuth 요청 방지
- 병렬 검색: pmap으로 I/O 대기 최소화
- 검색 결과 캐싱: 동일 요청 즉시 반환
- 비동기 Amenity 저장: 응답 지연 방지
12. 개선 가능 영역
12.1 Circuit Breaker 부재
현황: Amadeus와 달리 Circuit Breaker 패턴 미적용
영향:
- GDS 장애 시 연쇄 실패 가능
- 복구 시간 지연
제안:
@CircuitBreaker(name = "sabreSearch", fallbackMethod = "searchFallback")
fun search(...): List<FareItinerary> { ... }
fun searchFallback(e: Exception, ...): List<FareItinerary> {
logger.error("Sabre search failed, returning empty list", e)
return emptyList()
}12.2 중복 코드 (TODO 주석 존재)
위치: SabreRestClient.kt:230
// TODO(triple-dominic): search 쪽에 중복되는 코드와 합쳐보자제안: search와 revalidate의 응답 파싱 로직 공통화
12.3 에러 메시지 상수화
현황: 에러 메시지 목록이 하드코딩되어 있음
제안:
object SabreErrorMessages {
val IGNORABLE_ERRORS = listOf(
"NO AVAILABILITY",
"NO COMBINABLE FARES FOR CLASS USED",
// ...
)
}13. 참고 자료
13.1 주요 클래스
SabreFlightSearchService.kt: 검색 서비스 (47-209)SabreRestClient.kt: REST 클라이언트 (45-502)BargainFinderMaxRequest.kt: 요청 모델BargainFinderMaxResponse.kt: 응답 모델
13.2 주요 메소드
SabreRestClient.getToken(): OAuth 2.0 Token 발급 (67-96)SabreRestClient.search(): BargainFinderMax 검색 (98-202)SabreRestClient.revalidate(): 운임 재검증 (204-265)SabreFlightSearchService.search(): 검색 통합 로직 (47-129)
13.3 주요 상수
NonVoidableAirline: HY, MF, SU, QHexcludeMarketingCarriers: CM, A3, FI, S7, AM, VNexcludeOperatingCarriers: CM, A3, FI, AMsearchTimeout: 30000msdefaultTimeout: 60000ms