Phase 2: Galileo 예약 API 심층 분석
1. API 엔드포인트 개요
1.1 예약 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/GALILEO/bookings | POST | SOAP | 예약 생성 | GalileoBookingController.kt:28-38 |
/internals/GALILEO/bookings/{pnr} | GET | SOAP | 예약 조회 | GalileoBookingController.kt:90-94 |
/internals/GALILEO/bookings/{pnr} | PUT | SOAP | APIS 정보 변경 | GalileoBookingController.kt:40-51 |
/internals/GALILEO/bookings/{pnr}/check-pnr | GET | SOAP | PNR 존재 확인 | GalileoBookingController.kt:96-101 |
/internals/GALILEO/bookings/{pnr}/confirm | GET | SOAP | 예약 확정 | GalileoBookingController.kt:103-107 |
/internals/GALILEO/bookings/{pnr}/repricing | GET | SOAP | 재운임 계산 | GalileoBookingController.kt:109-119 |
/internals/GALILEO/bookings/{pnr}/divide | POST | SOAP | 예약 분리 | GalileoBookingController.kt:121-130 |
/internals/GALILEO/bookings/{pnr}/cancel | PUT | SOAP | 예약 취소 | GalileoBookingController.kt:53-63 |
/internals/GALILEO/bookings/{pnr}/expected-cancel | GET | SOAP | 취소 가능 여부 조회 | GalileoBookingController.kt:72-81 |
/internals/GALILEO/bookings/{pnr}/cancelable | GET | SOAP | 취소 타입 조회 | 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)
}처리 이유:
- API Version 동기화: Galileo는 매 수정마다 version 증가
- 최신 운임 적용: 실시간 운임 재계산
- 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-Ticket | EMD Ticket |
|---|---|---|
| 용도 | 항공권 (Flight) | 부가서비스 (Baggage, Meal, Seat 등) |
| 발행 | 항상 발행 | 선택적 발행 |
| API | getTicketDocuments | getEmdTicketDocuments |
| 식별 | 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 PNRproviderPnr: Provider PNRpassengerReferences: 분리할 승객의 identification key 리스트
- 반환: 신규 PNR
7. 에러 처리 전략
7.1 에러 타입별 처리 매트릭스
| 에러 타입 | 발생 시점 | 처리 방법 | PNR 취소 | 캐시 제거 |
|---|---|---|---|---|
SOLD_OUT | Pricing | StatusInvalidException | 비동기 | Yes |
BOOKING_FAILED | Book | InternationalAdapterException | 비동기 | No |
PRICING_FAILED | Pricing | InternationalAdapterException | 비동기 | No |
DIVIDE_FAILED | Divide | InternationalAdapterException | No | No |
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 예약 플로우 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| Session 관리 | Stateless (Basic Auth) | Stateful (Session Token) | Stateful (Session Token) |
| Pricing 단계 | 필수 (예약 전) | 필수 (예약 전) | 필수 (PriceQuote 생성) |
| 예약 API | AirCreateReservationRQ | PNR_AddMultiElements | EnhancedAirBookRQ + PassengerDetailsRQ |
| PNR 구조 | Universal Record + Provider PNR | PNR | Provider PNR |
| CarrierTimeLimit | 재조회 로직 있음 | 없음 | 없음 |
| Schedule Sequence | FareItinerary에서 복사 | 순서 보장 | 순서 보장 |
| Divide | ProviderReservationDivideRQ | N/A | N/A |
8.2 에러 처리 비교
| 에러 시나리오 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 좌석 매진 | ”are not bookable" | "NO FARE FOR CLASS USED” | Schedule confirmed 체크 |
| 유아 SSR | N/A | Passenger 상태 체크 | isInfantSoldOut 체크 |
| 비동기 취소 | 5초 대기 | 즉시 취소 | 즉시 취소 |
| FareBasis 변경 | 검증 후 SOLD_OUT | 검증 후 SOLD_OUT | 검증 후 SOLD_OUT |
8.3 독특한 특징
Galileo 고유 특징
-
Universal Record 구조:
- Universal Record (최상위)
- Provider Reservation (항공사별)
- Provider PNR
- Sub PNR (Reservation PNR)
- Provider Reservation (항공사별)
- Universal Record (최상위)
-
CarrierTimeLimit 재조회:
- 예약 직후 Time Limit이 생성되지 않을 수 있음
- 3초 대기 후 재조회 로직
-
Schedule Sequence 복사:
- Galileo는 스케줄 순서를 보장하지 않음
- FareItinerary의 순서를 Booking에 복사
-
Element 기반 관리:
- PricingInfo, Remark 등을 Element로 관리
- 삭제/추가 시 UniversalRecordModifyRQ 사용
-
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 캐싱 전략
- FareItinerary 조회: Redis 캐시 (검색 시 저장)
- FareItinerary PNR 매핑: Redis 캐시 (예약 시 저장)
- Unexposed FareItinerary: Redis Set (매진 시 저장)
10. 주요 발견사항
10.1 Galileo 예약 시스템의 특징
- Stateless 아키텍처: Session Token 없이 Basic Auth만 사용
- Universal Record 구조: 계층적 PNR 관리
- Element 기반 수정: 각 요소를 독립적으로 관리
- 지연된 Time Limit: 예약 직후 즉시 생성되지 않음
- 비보장 순서: Schedule 순서를 FareItinerary에서 복사 필요
10.2 개선 가능 영역
1. CarrierTimeLimit 재조회 로직 개선
현황: 하드코딩된 3초 대기
delay(3000)제안: 설정 파일로 관리
galileo:
booking:
carrier-time-limit-retry-delay: 3000
carrier-time-limit-retry-count: 22. 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 심층 분석 문서입니다.