Phase 4: Galileo Pricing API 심층 분석

1. API 엔드포인트 개요

1.1 Pricing 호출 시점

Galileo는 독립적인 Pricing API 엔드포인트가 없으며, 다음 시점에 내부적으로 호출됩니다:

호출 시점목적API위치
예약 생성실시간 운임 검증AirPriceRQGalileoBookingService.kt:65-70
발권 준비운임 재계산AirPriceRQGalileoTicketingService.kt:76-80
예약 확정Repricing 검증AirPriceRQGalileoBookingService.kt:160-164
운임 규정 조회FareInfo 생성AirPriceRQGalileoFareRuleService.kt:63-73

2. Pricing API 구조

2.1 AirPriceRQ 엔드포인트

위치: GalileoClient.kt:96-203

SOAP API:

  • 엔드포인트: /AirService
  • 메시지: AirPriceRQ
  • 인증: Basic Authentication

2.2 두 가지 Pricing 메서드

2.2.1 FareItinerary 기반 Pricing

위치: GalileoClient.kt:96-150

fun pricing(
    fareItinerary: FareItinerary,
    airportMap: Map<String, Airport>,
    passengerCountMap: Map<PassengerType, Int>,
): PricingFare

사용 시점:

  • 예약 생성 (Book)
  • 운임 규정 조회 (FareRule)

파라미터:

  • fareItinerary: 검색 결과 FareItinerary
  • airportMap: 공항 정보 (timezone 등)
  • passengerCountMap: 승객 타입별 인원수

2.2.2 Booking 기반 Pricing

위치: GalileoClient.kt:152-203

fun pricing(
    booking: Booking,
    originSchedules: List<Schedule>,
    airportMap: Map<String, Airport>,
): PricingFare

사용 시점:

  • 발권 준비 (Ready)
  • 예약 확정 (Repricing)

파라미터:

  • booking: 기존 예약 정보
  • originSchedules: 원본 스케줄 (변경 감지용)
  • airportMap: 공항 정보

3. Pricing 플로우

3.1 전체 Pricing 플로우

flowchart TD
    A[Pricing 요청] --> B{요청 타입}
    B -->|FareItinerary| C[AirPriceRQ 생성<br/>FareItinerary 기반]
    B -->|Booking| D[AirPriceRQ 생성<br/>Booking 기반]

    C --> E[SOAP 요청<br/>/AirService]
    D --> E

    E --> F{에러 체크}
    F -->|에러| G{are not bookable?}
    G -->|Yes| H[SOLD_OUT 예외]
    G -->|No| I[PRICING_FAILED 예외]

    F -->|정상| J{Price Results<br/>존재?}
    J -->|No| K[PRICING_FAILED<br/>Price Results is empty]
    J -->|Yes| L{Pricing Segments<br/>존재?}
    L -->|No| M[PRICING_FAILED<br/>Pricing Segment is empty]

    L -->|Yes| N[AirPricingSolution<br/>검색]
    N --> O{Solution<br/>발견?}
    O -->|No| P[SOLD_OUT<br/>Not Found AirPricingSolution]
    O -->|Yes| Q[PricingFare 변환<br/>toPricingFare]

    Q --> R[Pricing 완료]

    style F fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style G fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style H fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style I fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style J fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style O fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style P fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style R fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000

3.2 단계별 상세 분석

Step 1: 요청 생성

FareItinerary 기반:

val request = AirPriceRQ.of(
    targetBranch = galileoApiProperties.soap.branchCode,
    fareItinerary = fareItinerary,
    airportMap = airportMap,
    passengerCountMap = passengerCountMap
)

Booking 기반:

val request = AirPriceRQ.of(
    targetBranch = galileoApiProperties.soap.branchCode,
    booking = booking,
    airportMap = airportMap
)

Step 2: SOAP 요청 실행

"${galileoApiProperties.soap.endpoint}/AirService"
    .post(request)
    .authenticate(galileoApiProperties.soap.userName, galileoApiProperties.soap.password)
    .requestBodyConvert(soapRequestBodyConverter())
    .execute<GalileoResponse<AirPriceRS>>(soapBodyDeserializerOf(logger, objectMapper))

Step 3: 에러 체크 및 분류

위치: GalileoClient.kt:115-125

response.checkError { errorMessages ->
    errorMessages.forEach { (code, message) ->
        if (message.contains("are not bookable")) {
            throw StatusInvalidException(ErrorMessage.SOLD_OUT, code, message)
        }
    }
 
    throw InternationalAdapterException(
        ErrorMessage.PRICING_FAILED,
        errorMessages.joinToString { (code, message) -> "$code: $message" }
    )
}

에러 분류:

  • "are not bookable": 좌석 매진 → SOLD_OUT
  • 기타: Pricing 실패 → PRICING_FAILED

Step 4: 응답 검증

val airPriceRS = response.body!!
if (airPriceRS.priceResults.isNullOrEmpty()) {
    throw InternationalAdapterException(ErrorMessage.PRICING_FAILED, "Price Results is empty")
}
 
val pricingSegments = airPriceRS.pricingSegments
    ?: throw InternationalAdapterException(ErrorMessage.PRICING_FAILED, "Pricing Segment is empty")

검증 항목:

  1. priceResults: 운임 계산 결과
  2. pricingSegments: 구간별 운임 정보

Step 5: AirPricingSolution 검색

FareItinerary 기반:

airPriceRS
    .findPricingSolution(fareItinerary)
    ?.toPricingFare(pricingSegments)
    ?: throw StatusInvalidException(
        ErrorMessage.SOLD_OUT,
        "Not Found AirPricingSolution",
        fareItinerary.schedules.flatMap { it.segments }
            .joinToString { segment -> segment.fareBasisInfos.joinToString { "${it.passengerType}:${it.fareBasis}" } }
    )

Booking 기반:

airPriceRS.findPricingSolution(originSchedules)
    ?.toPricingFare(segments = pricingSegments, passengers = booking.passengers)
    ?: throw StatusInvalidException(
        ErrorMessage.SOLD_OUT,
        "Not Found AirPricingSolution",
        originSchedules.joinToString { segment -> segment.fareBasisInfos.joinToString { "${it.passengerType}:${it.fareBasis}" } }
    )

검색 실패 시: SOLD_OUT 예외 (FareBasis 변경 또는 매진)


4. PricingFare 구조

4.1 PricingFare 데이터 모델

data class PricingFare(
    val passengerFares: List<PassengerFare>,  // 승객별 운임
    val pricingSolution: PricingSolution       // Pricing Solution (발권 시 참조)
)
 
data class PassengerFare(
    val passengerType: PassengerType,
    val fareInfos: List<PricingFareInfo>,     // 구간별 Fare Info
    val totalFare: Fare                        // 총 운임
)
 
data class PricingFareInfo(
    val key: String,                           // FareInfo Key (운임 규정 조회 시 사용)
    val fareBasis: String,                     // FareBasis Code
    val bookingClass: String,                  // Booking Class
    // ...
)

4.2 PricingFare 활용

4.2.1 예약 생성 (Book)

val pricingFare = galileoClient.pricing(
    fareItinerary = fareItinerary,
    airportMap = airportMap,
    passengerCountMap = passengers.groupingBy { it.type }.eachCount()
)
 
providerPnr = galileoClient.book(
    fareItinerary = fareItinerary,
    pricingFare = pricingFare,  // Pricing 결과 전달
    reservationUser = reservationUser,
    passengers = passengers,
)

4.2.2 발권 준비 (Ready)

val pricingFare = galileoClient.pricing(
    booking = this,
    originSchedules = booking.schedules,
    airportMap = airportMap
)
galileoClient.addPriceInfo(booking = this, pricingFare = pricingFare)

4.2.3 운임 규정 조회 (FareRule)

val fareInfos = galileoClient
    .pricing(
        fareItinerary = fareItinerary,
        airportMap = airportMap,
        passengerCountMap = mapOf(
            PassengerType.ADULT to adult,
            PassengerType.CHILD to child,
            PassengerType.INFANT to infant
        )
    )
    .passengerFares
    .filter { it.passengerType == PassengerType.ADULT }
    .flatMap { it.fareInfos }
 
galileoClient.getFareRules(fareItinerary = fareItinerary, pricingFareInfos = fareInfos)

5. 조건부 분기 및 예외 처리

5.1 에러 타입별 처리

5.1.1 SOLD_OUT 예외

발생 조건:

  1. "are not bookable" 에러 메시지
  2. AirPricingSolution 미발견
  3. FareBasis 변경

처리:

  • 검색 캐시 제거
  • Unexposed FareItinerary 저장
  • 비동기 PNR 취소 (예약 생성 시)

5.1.2 PRICING_FAILED 예외

발생 조건:

  1. Price Results 없음
  2. Pricing Segments 없음
  3. 기타 Pricing 에러

처리:

  • 비동기 PNR 취소 (예약 생성 시)
  • 예외 재발생

5.2 재시도 로직

Galileo Pricing API는 재시도 로직이 없습니다.

  • 이유: Pricing 실패는 운임 매진 또는 시스템 에러
  • 대안: 검색부터 다시 수행

6. Amadeus/Sabre와의 비교

6.1 Pricing API 비교

항목GalileoAmadeusSabre
API 타입SOAPSOAPSOAP
엔드포인트AirPriceRQFare_PricePNRWithBookingClassOtaAirPriceRQ
독립 API없음 (내부 호출)없음 (내부 호출)있음 (Repricing)
세션 관리StatelessStatefulStateful
FareBasis 검증AirPricingSolution 매칭FareBasis 비교FareBasis 비교
에러 분류”are not bookable""NO FARE FOR CLASS USED”Schedule confirmed

6.2 Pricing 호출 시점 비교

시점GalileoAmadeusSabre
예약 생성O (필수)O (필수)O (필수, PriceQuote 생성)
발권 준비O (Guaranteed 체크)XO (당일 여부 체크)
예약 확정O (Repricing)XX
운임 규정O (FareInfo 생성)OO

6.3 독특한 특징

Galileo 고유 특징

  1. 두 가지 Pricing 메서드:

    • FareItinerary 기반 (예약 생성)
    • Booking 기반 (발권 준비)
  2. Guaranteed 체크:

    • 모든 PricingInfo가 Guaranteed면 Repricing 생략
    • 성능 최적화
  3. AirPricingSolution 매칭:

    • FareBasis Code로 Solution 검색
    • 매칭 실패 시 SOLD_OUT
  4. PricingSegments 분리:

    • 구간별 운임 정보 독립 관리
    • Map으로 변환하여 빠른 조회
  5. Element 기반 저장:

    • PricingInfo를 Element로 PNR에 저장
    • 삭제/추가 시 버전 동기화 필요

7. 성능 최적화

7.1 Guaranteed 기반 최적화

위치: GalileoTicketingService.kt:57-58

if (booking.bookingReference?.pricingInfoReferences?.all { it.isGuaranteed } == true) {
    booking.withPassengers(null)
}

효과:

  • Repricing 생략
  • API 호출 감소
  • 응답 시간 단축

7.2 공항 정보 캐싱

val airportMap = fareItinerary.schedules
    .asSequence()
    .flatMap { it.segments }
    .flatMap { it.legs }
    .flatMap { listOf(it.departure, it.arrival) }
    .distinct()
    .map { cityClient.getAirportByIata(it) }
    .associateBy { it.iataCode }

최적화:

  • distinct()로 중복 제거
  • Map으로 O(1) 조회
  • 한 번만 조회 후 재사용

8. 주요 발견사항

8.1 Galileo Pricing 시스템의 특징

  1. 독립 API 없음: 다른 API에 내장
  2. 두 가지 호출 방식: FareItinerary vs Booking
  3. Guaranteed 지원: 운임 보장 체크
  4. AirPricingSolution: FareBasis 기반 매칭
  5. Element 기반 저장: PNR에 PricingInfo 저장

8.2 개선 가능 영역

1. Pricing 캐싱

현황: 매번 Pricing API 호출

제안: 단기 캐싱 (1분)

val cacheKey = "PRICING:${fareItinerary.id}:${passengerCountMap}"
pricingCache.get(cacheKey) ?: run {
    galileoClient.pricing(...).also {
        pricingCache.put(cacheKey, it, 60.seconds)
    }
}

2. Pricing 재시도

현황: 재시도 없음

제안: 일시적 에러 재시도 (1회)

@Retryable(
    value = [SocketTimeoutException::class],
    maxAttempts = 2,
    backoff = Backoff(delay = 1000)
)
fun pricing(...): PricingFare

3. Pricing 로깅 강화

현황: 최소 로깅

제안: 상세 로깅

logger.info("[PRICING] FareItinerary: ${fareItinerary.id}, " +
            "Passengers: $passengerCountMap, " +
            "Solution: ${pricingFare.pricingSolution.key}, " +
            "TotalFare: ${pricingFare.passengerFares.sumOf { it.totalFare.total }}")

9. 참고 자료

9.1 주요 클래스

  • GalileoClient.kt: Pricing API (96-203)
  • AirPriceRQ.kt: Pricing 요청 모델
  • AirPriceRS.kt: Pricing 응답 모델
  • PricingFare.kt: Pricing 결과 모델
  • AirPricingSolution.kt: Pricing Solution 모델

9.2 주요 메소드

  • GalileoClient.pricing(fareItinerary): FareItinerary 기반 Pricing (96-150)
  • GalileoClient.pricing(booking): Booking 기반 Pricing (152-203)
  • AirPriceRS.findPricingSolution(): AirPricingSolution 검색
  • AirPricingSolution.toPricingFare(): PricingFare 변환

9.3 관련 API

  • AirPriceRQ: 운임 계산
  • UniversalRecordModifyRQ: PricingInfo 삭제/추가

이 문서는 Triple Air International Adapter 프로젝트의 Galileo Pricing API 심층 분석 문서입니다.