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)
        }
    }
}

제외 대상

항공사 코드항공사명제외 유형제외 이유
CMCopa AirlinesMarketing/Operating상태 코드 변경 이슈 (HK → UC)
A3Aegean AirlinesMarketing/Operating상태 코드 변경 이슈
FIIcelandairMarketing/Operating상태 코드 변경 이슈
S7S7 AirlinesMarketingOZ 정산 불가 이슈
AMAeromexicoMarketing/Operating상태 코드 변경 이슈
VNVietnam AirlinesMarketing상태 코드 변경 이슈

판별 로직

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 비교

항목AmadeusSabre
API 타입SOAPREST
인증 방식Session Token (Stateful)OAuth 2.0 (Stateless)
엔드포인트FareMasterPricerTravelBoardSearchBargainFinderMax 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 제외있음있음 (동일)
NonTicketableCarrierMH 항공사OZ + excludeCarriers (CM, A3, FI, S7, AM, VN)
RevalidateSOAPREST
Multi-ticket필터링 단계에서 처리검색 요청에 포함

11. 주요 발견사항 및 특징

11.1 REST API 기반의 장점

  1. 무상태 (Stateless): Session 관리 불필요
  2. Token 재사용: Redis 캐싱으로 성능 향상
  3. 표준 HTTP: 디버깅 및 모니터링 용이
  4. JSON 응답: 파싱 간편

11.2 항공사 특별 처리

  1. OZ 항공사: NonTicketableCarrier 체크 (CM, A3, FI, S7, AM, VN)
  2. 상태 코드 변경 이슈: HK → UC 변경으로 발권 불가
  3. 정산 불가 이슈: OZ + S7 조합 시 정산 불가

11.3 에러 처리 전략

  1. 무시 가능한 에러: 빈 리스트 반환 (검색 결과 없음)
  2. 부분 실패 허용: 일부 성공 시 계속 진행
  3. Revalidate 실패: null 반환 → 원본 사용

11.4 성능 최적화

  1. Token 캐싱: 불필요한 OAuth 요청 방지
  2. 병렬 검색: pmap으로 I/O 대기 최소화
  3. 검색 결과 캐싱: 동일 요청 즉시 반환
  4. 비동기 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, QH
  • excludeMarketingCarriers: CM, A3, FI, S7, AM, VN
  • excludeOperatingCarriers: CM, A3, FI, AM
  • searchTimeout: 30000ms
  • defaultTimeout: 60000ms