Phase 4: Galileo Pricing API 심층 분석
1. API 엔드포인트 개요
1.1 Pricing 호출 시점
Galileo는 독립적인 Pricing API 엔드포인트가 없으며, 다음 시점에 내부적으로 호출됩니다:
| 호출 시점 | 목적 | API | 위치 |
|---|---|---|---|
| 예약 생성 | 실시간 운임 검증 | AirPriceRQ | GalileoBookingService.kt:65-70 |
| 발권 준비 | 운임 재계산 | AirPriceRQ | GalileoTicketingService.kt:76-80 |
| 예약 확정 | Repricing 검증 | AirPriceRQ | GalileoBookingService.kt:160-164 |
| 운임 규정 조회 | FareInfo 생성 | AirPriceRQ | GalileoFareRuleService.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: 검색 결과 FareItineraryairportMap: 공항 정보 (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")검증 항목:
priceResults: 운임 계산 결과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 예외
발생 조건:
"are not bookable"에러 메시지AirPricingSolution미발견FareBasis변경
처리:
- 검색 캐시 제거
- Unexposed FareItinerary 저장
- 비동기 PNR 취소 (예약 생성 시)
5.1.2 PRICING_FAILED 예외
발생 조건:
Price Results없음Pricing Segments없음- 기타 Pricing 에러
처리:
- 비동기 PNR 취소 (예약 생성 시)
- 예외 재발생
5.2 재시도 로직
Galileo Pricing API는 재시도 로직이 없습니다.
- 이유: Pricing 실패는 운임 매진 또는 시스템 에러
- 대안: 검색부터 다시 수행
6. Amadeus/Sabre와의 비교
6.1 Pricing API 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| API 타입 | SOAP | SOAP | SOAP |
| 엔드포인트 | AirPriceRQ | Fare_PricePNRWithBookingClass | OtaAirPriceRQ |
| 독립 API | 없음 (내부 호출) | 없음 (내부 호출) | 있음 (Repricing) |
| 세션 관리 | Stateless | Stateful | Stateful |
| FareBasis 검증 | AirPricingSolution 매칭 | FareBasis 비교 | FareBasis 비교 |
| 에러 분류 | ”are not bookable" | "NO FARE FOR CLASS USED” | Schedule confirmed |
6.2 Pricing 호출 시점 비교
| 시점 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 예약 생성 | O (필수) | O (필수) | O (필수, PriceQuote 생성) |
| 발권 준비 | O (Guaranteed 체크) | X | O (당일 여부 체크) |
| 예약 확정 | O (Repricing) | X | X |
| 운임 규정 | O (FareInfo 생성) | O | O |
6.3 독특한 특징
Galileo 고유 특징
-
두 가지 Pricing 메서드:
- FareItinerary 기반 (예약 생성)
- Booking 기반 (발권 준비)
-
Guaranteed 체크:
- 모든 PricingInfo가 Guaranteed면 Repricing 생략
- 성능 최적화
-
AirPricingSolution 매칭:
- FareBasis Code로 Solution 검색
- 매칭 실패 시 SOLD_OUT
-
PricingSegments 분리:
- 구간별 운임 정보 독립 관리
- Map으로 변환하여 빠른 조회
-
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 시스템의 특징
- 독립 API 없음: 다른 API에 내장
- 두 가지 호출 방식: FareItinerary vs Booking
- Guaranteed 지원: 운임 보장 체크
- AirPricingSolution: FareBasis 기반 매칭
- 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(...): PricingFare3. 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 심층 분석 문서입니다.