Phase 2: Galileo 예약 API 심층 분석

1. API 엔드포인트 개요

1.1 예약 관련 엔드포인트

엔드포인트메서드API 타입기능위치
/internals/GALILEO/bookingsPOSTSOAP예약 생성GalileoBookingController.kt:28-38
/internals/GALILEO/bookings/{pnr}GETSOAP예약 조회GalileoBookingController.kt:90-94
/internals/GALILEO/bookings/{pnr}PUTSOAPAPIS 정보 변경GalileoBookingController.kt:40-51
/internals/GALILEO/bookings/{pnr}/check-pnrGETSOAPPNR 존재 확인GalileoBookingController.kt:96-101
/internals/GALILEO/bookings/{pnr}/confirmGETSOAP예약 확정GalileoBookingController.kt:103-107
/internals/GALILEO/bookings/{pnr}/repricingGETSOAP재운임 계산GalileoBookingController.kt:109-119
/internals/GALILEO/bookings/{pnr}/dividePOSTSOAP예약 분리GalileoBookingController.kt:121-130
/internals/GALILEO/bookings/{pnr}/cancelPUTSOAP예약 취소GalileoBookingController.kt:53-63
/internals/GALILEO/bookings/{pnr}/expected-cancelGETSOAP취소 가능 여부 조회GalileoBookingController.kt:72-81
/internals/GALILEO/bookings/{pnr}/cancelableGETSOAP취소 타입 조회GalileoBookingController.kt:83-88

2. 예약 생성 (Create Booking)

2.1 전체 예약 플로우

flowchart TD
    A[예약 요청 수신] --> B[FareItinerary<br/>조회]
    B --> C[공항 정보 수집<br/>airportMap 생성]

    C --> D[Step 1: Pricing<br/>galileoClient.pricing]
    D --> E[Step 2: Book<br/>galileoClient.book]
    E --> F[Step 3: GetBooking<br/>providerPnr로 조회]

    F --> G{CarrierTimeLimit<br/>null?}
    G -->|Yes| H[3초 대기]
    H --> I[재조회<br/>getBooking]
    G -->|No| J[정상 처리]
    I --> J

    J --> K[Schedule Sequence<br/>복사]
    K --> L[FareItinerary<br/>PNR로 저장]
    L --> M[검색 캐시<br/>제거]
    M --> N[예약 완료<br/>Booking 반환]

     에러 플로우
    L --> ERR{FareBasis<br/>변경?}
    ERR -->|Yes| N[비동기 취소]
    ERR -->|No| M
    N --> O[SOLD_OUT 예외]

    style D fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style E fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
    style G fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style L fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style ERR fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style O fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000

Step 1: Guaranteed 체크

return if (booking.bookingReference?.pricingInfoReferences?.all { it.isGuaranteed } == true) {
    booking
} else {
    // Repricing 로직
}

Guaranteed:

  • 의미: 운임이 보장됨 (변경 없음)
  • 조건: 모든 PricingInfo가 Guaranteed 상태
  • 결과: 재계산 생략 (성능 최적화)

Step 2: PricingInfo 삭제 및 재생성

위치: GalileoBookingService.kt:153-166

galileoClient.deleteElements(
    booking = booking,
    elementType = ElementType.AIR_PRICING,
    deleteKeys = booking.bookingReference!!.pricingInfoReferences!!.map { it.key }
)
 
// API version 갱신을 위해 재조회
galileoClient.getBooking(pnr).run {
    val pricingFare = galileoClient.pricing(
        booking = this,
        originSchedules = booking.schedules,
        airportMap = airportMap,
    )
    galileoClient.addPriceInfo(booking = this, pricingFare = pricingFare)
}

처리 이유:

  1. API Version 동기화: Galileo는 매 수정마다 version 증가
  2. 최신 운임 적용: 실시간 운임 재계산
  3. Element 기반 관리: Galileo는 각 요소를 독립적으로 관리

Step 3: FareBasis 변경 검증

위치: GalileoBookingService.kt:265-284

private fun validateFareBasisChange(originBooking: Booking, newBooking: Booking) {
    newBooking.passengers?.forEach { newPassenger ->
        val originFareBasis = originBooking.passengers
            ?.first { it.type == newPassenger.type }?.fare?.fareComponents
            ?.sortedBy { it.fareBasis }
            ?.joinToString(",")
        val newFareBasis = newPassenger.fare?.fareComponents
            ?.sortedBy { it.fareBasis }
            ?.joinToString(",")
 
        if (originFareBasis != newFareBasis) {
            cancelService.pnrCancelAsync(originBooking.pnr)
 
            throw StatusInvalidException(
                ErrorMessage.SOLD_OUT,
                "${newPassenger.type.name} fareBasis is changed ($originFareBasis->$newFareBasis)",
            ).capture()
        }
    }
}

검증 로직:

  • 승객 타입별 FareBasis 비교
  • 변경 감지 시 SOLD_OUT 처리
  • 비동기 PNR 취소

목적:

  • 운임 클래스 변경 방지
  • 가격 상승 방지
  • 고객 보호

5. 예약 조회 (Retrieve)

5.1 전체 조회 플로우

위치: GalileoBookingService.kt:174-212

fun retrieve(pnr: String): Booking {
    val booking = galileoClient.getBooking(providerPnr = pnr, findTimeZoneId = findTimeZoneId)
 
    if (booking.tickets != null) {
        // E-Ticket 조회
        val eTickets = galileoClient.getTicketDocuments(
            providerPnr = pnr,
            universalRecordPnr = booking.supplierIdentificationKey!!,
            reservationPnr = booking.subPnr!!,
        )
 
        // EMD 티켓 조회
        val emdTickets = galileoClient.getEmdTicketDocuments(providerPnr = pnr, passengers = booking.passengers!!)
            .map {
                galileoClient.getEmdTicketDocument(
                    providerPnr = pnr,
                    identificationKey = it.identificationKey,
                    ticketNumber = it.ticketNumber
                )
            }
 
        // EMD 티켓 추가 (hasEmdTicket = false인 경우만)
        if (emdTickets.isNotEmpty() && booking.hasEmdTicket.not()) {
            booking.withTickets(
                tickets = emdTickets.flatMap { ticket ->
                    ticket.tickets.map { it.toPnrTicket(identificationKey = ticket.identificationKey) }
                }
            )
        }
 
        // 승객별 티켓 매핑
        val tickets = eTickets + emdTickets
        booking.passengers?.forEach { passenger ->
            passenger.withPassengerTickets(
                ticketDocuments = tickets.filter {
                    it.identificationKey == passenger.identificationKey && it.passengerType == passenger.type
                }
            )
        }
    }
 
    return booking
}

5.2 티켓 조회 구조

E-Ticket vs EMD Ticket

항목E-TicketEMD Ticket
용도항공권 (Flight)부가서비스 (Baggage, Meal, Seat 등)
발행항상 발행선택적 발행
APIgetTicketDocumentsgetEmdTicketDocuments
식별ticketNumber (13자리)ticketNumber (13자리) + EMD prefix

EMD 티켓 처리 로직

if (emdTickets.isNotEmpty() && booking.hasEmdTicket.not()) {
    booking.withTickets(
        tickets = emdTickets.flatMap { ticket ->
            ticket.tickets.map { it.toPnrTicket(identificationKey = ticket.identificationKey) }
        }
    )
}

조건:

  • EMD 티켓 존재 (emdTickets.isNotEmpty())
  • Booking에 EMD 플래그 없음 (booking.hasEmdTicket.not())

이유: 일부 Booking 응답에 EMD 정보가 누락될 수 있음


6. 예약 분리 (Divide)

6.1 Divide 플로우

위치: GalileoBookingService.kt:214-241

flowchart TD
    A[Divide 요청] --> B[예약 조회<br/>getBooking]
    B --> C{유소아<br/>존재?}
    C -->|Yes| D[DIVIDE_FAILED<br/>예외 발생]
    C -->|No| E{전체 승객<br/>요청?}
    E -->|Yes| F[DIVIDE_FAILED<br/>예외 발생]
    E -->|No| G[ProviderReservationDivide<br/>API 호출]
    G --> H[신규 PNR 반환]
    H --> I[Retrieve<br/>신규 예약 조회]
    I --> J[Divide 완료]

    style C fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style D fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style E fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style F fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style J fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000

6.2 Divide 제약사항

검증 1: 유소아 존재 체크

위치: GalileoBookingService.kt:216-222

if (booking.passengers?.any { it.type != PassengerType.ADULT } == true) {
    throw InternationalAdapterException(
        ErrorMessage.DIVIDE_FAILED,
        ResponseMessage("exists child or infant")
    ).capture()
}

이유: 유소아는 반드시 성인과 동행해야 함

검증 2: 전체 승객 요청 체크

위치: GalileoBookingService.kt:224-230

if (booking.passengers?.size == requestPassengers.size) {
    throw InternationalAdapterException(
        ErrorMessage.DIVIDE_FAILED,
        ResponseMessage("requested all passengers")
    ).capture()
}

이유: 분리 후 원본 예약에 최소 1명 이상 남아야 함

6.3 Divide 실행

위치: GalileoBookingService.kt:233-240

val requestedIdentificationKeys = requestPassengers.map { it.identificationKey }
val newPnr = galileoClient.divide(
    universalPnr = booking.supplierIdentificationKey!!,
    providerPnr = booking.pnr,
    passengerReferences = requestedIdentificationKeys
)
 
return retrieve(newPnr)

SOAP API: ProviderReservationDivideRQ

  • 파라미터:
    • universalPnr: Universal Record PNR
    • providerPnr: Provider PNR
    • passengerReferences: 분리할 승객의 identification key 리스트
  • 반환: 신규 PNR

7. 에러 처리 전략

7.1 에러 타입별 처리 매트릭스

에러 타입발생 시점처리 방법PNR 취소캐시 제거
SOLD_OUTPricingStatusInvalidException비동기Yes
BOOKING_FAILEDBookInternationalAdapterException비동기No
PRICING_FAILEDPricingInternationalAdapterException비동기No
DIVIDE_FAILEDDivideInternationalAdapterExceptionNoNo
INFANT_SOLD_OUT (Sabre 전용)N/A---

7.2 SOAP Fault 처리

위치: GalileoClient.kt:293-314

response.fold(
    success = { response ->
        response.checkError { errorMessages ->
            throw InternationalAdapterException(
                ErrorMessage.BOOKING_FAILED,
                errorMessages.joinToString { (code, message) -> "$code: $message" }
            )
        }
        // 성공 처리
    },
    failure = {
        throw it.handleSoapFaultException(ErrorMessage.BOOKING_FAILED)
    }
)

SOAP Fault 처리:

  • GalileoResponse에 soapFault 필드 포함
  • handleSoapFaultException으로 공통 처리
  • 에러 메시지에 SOAP Fault 내용 포함

8. Amadeus/Sabre와의 비교

8.1 예약 플로우 비교

항목GalileoAmadeusSabre
Session 관리Stateless (Basic Auth)Stateful (Session Token)Stateful (Session Token)
Pricing 단계필수 (예약 전)필수 (예약 전)필수 (PriceQuote 생성)
예약 APIAirCreateReservationRQPNR_AddMultiElementsEnhancedAirBookRQ + PassengerDetailsRQ
PNR 구조Universal Record + Provider PNRPNRProvider PNR
CarrierTimeLimit재조회 로직 있음없음없음
Schedule SequenceFareItinerary에서 복사순서 보장순서 보장
DivideProviderReservationDivideRQN/AN/A

8.2 에러 처리 비교

에러 시나리오GalileoAmadeusSabre
좌석 매진”are not bookable""NO FARE FOR CLASS USED”Schedule confirmed 체크
유아 SSRN/APassenger 상태 체크isInfantSoldOut 체크
비동기 취소5초 대기즉시 취소즉시 취소
FareBasis 변경검증 후 SOLD_OUT검증 후 SOLD_OUT검증 후 SOLD_OUT

8.3 독특한 특징

Galileo 고유 특징

  1. Universal Record 구조:

    • Universal Record (최상위)
      • Provider Reservation (항공사별)
        • Provider PNR
        • Sub PNR (Reservation PNR)
  2. CarrierTimeLimit 재조회:

    • 예약 직후 Time Limit이 생성되지 않을 수 있음
    • 3초 대기 후 재조회 로직
  3. Schedule Sequence 복사:

    • Galileo는 스케줄 순서를 보장하지 않음
    • FareItinerary의 순서를 Booking에 복사
  4. Element 기반 관리:

    • PricingInfo, Remark 등을 Element로 관리
    • 삭제/추가 시 UniversalRecordModifyRQ 사용
  5. EMD 티켓 누락 대응:

    • Booking 응답에 EMD 정보가 누락될 수 있음
    • hasEmdTicket 플래그로 추가 조회 수행

9. 성능 최적화

9.1 비동기 처리

위치: GalileoBookingService.kt:243-259

private fun saveUnexposedFareItinerary(fareItinerary: FareItinerary) {
    CoroutineScope(Dispatchers.IO).withLaunch {
        unexposedFareItineraryRepository.save(key = fareItinerary.requestKey, value = fareItinerary.id)
    }
}
 
private fun saveFareItineraryByPnr(pnr: String, fareItinerary: FareItinerary) {
    CoroutineScope(Dispatchers.IO).withLaunch {
        fareItineraryRepository.saveFareItineraryByPnr(key = pnr, value = fareItinerary)
    }
}
 
private fun removeFlightSearchKey(key: String) {
    CoroutineScope(Dispatchers.IO).withLaunch {
        flightSearchKeyRepository.removeKey(key)
    }
}

비동기 처리 대상:

  • Unexposed FareItinerary 저장
  • FareItinerary PNR 매핑 저장
  • 검색 캐시 제거
  • PNR 취소 (5초 지연)

성능 향상:

  • Redis 작업 시 API 응답 지연 방지
  • I/O 병렬 처리

9.2 캐싱 전략

  1. FareItinerary 조회: Redis 캐시 (검색 시 저장)
  2. FareItinerary PNR 매핑: Redis 캐시 (예약 시 저장)
  3. Unexposed FareItinerary: Redis Set (매진 시 저장)

10. 주요 발견사항

10.1 Galileo 예약 시스템의 특징

  1. Stateless 아키텍처: Session Token 없이 Basic Auth만 사용
  2. Universal Record 구조: 계층적 PNR 관리
  3. Element 기반 수정: 각 요소를 독립적으로 관리
  4. 지연된 Time Limit: 예약 직후 즉시 생성되지 않음
  5. 비보장 순서: Schedule 순서를 FareItinerary에서 복사 필요

10.2 개선 가능 영역

1. CarrierTimeLimit 재조회 로직 개선

현황: 하드코딩된 3초 대기

delay(3000)

제안: 설정 파일로 관리

galileo:
  booking:
    carrier-time-limit-retry-delay: 3000
    carrier-time-limit-retry-count: 2

2. FareBasis 변경 감지 로깅

현황: 로그 없음

제안:

if (originFareBasis != newFareBasis) {
    logger.warn("[FARE_BASIS_CHANGED] PNR: ${originBooking.pnr}, " +
                "PassengerType: ${newPassenger.type}, " +
                "Origin: $originFareBasis, New: $newFareBasis")
    cancelService.pnrCancelAsync(originBooking.pnr)
    throw StatusInvalidException(...)
}

3. Divide 제약사항 문서화

현황: 코드 내 검증만 존재

제안: API 명세서에 명시

  • 유소아 포함 불가
  • 전체 승객 요청 불가
  • 최소 1명 이상 남아야 함

11. 참고 자료

11.1 주요 클래스

  • GalileoBookingService.kt: 예약 서비스 (27-285)
  • GalileoClient.kt: SOAP 클라이언트 (64-815)
  • GalileoCancelService.kt: 취소 서비스 (21-262)
  • AirCreateReservationRQ.kt: 예약 요청 모델
  • UniversalRecordRetrieveRS.kt: 예약 응답 모델

11.2 주요 메소드

  • GalileoBookingService.book(): 예약 생성 (49-106)
  • GalileoBookingService.confirm(): 예약 확정 (112-130)
  • GalileoBookingService.repricing(): 재운임 계산 (132-172)
  • GalileoBookingService.retrieve(): 예약 조회 (174-212)
  • GalileoBookingService.divide(): 예약 분리 (214-241)
  • GalileoClient.book(): SOAP 예약 API (270-315)
  • GalileoClient.pricing(): SOAP Pricing API (96-203)

11.3 관련 API

  • AirPriceRQ: 운임 계산
  • AirCreateReservationRQ: 예약 생성
  • UniversalRecordRetrieveRQ: 예약 조회
  • UniversalRecordModifyRQ: 예약 수정 (PricingInfo 추가/삭제)
  • ProviderReservationDivideRQ: 예약 분리
  • UniversalRecordCancelRQ: 예약 취소

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