Galileo GDS API 분석 문서
1. 개요
1.1 Galileo GDS 시스템
- 공급사 타입: Global Distribution System (GDS)
- 통신 방식: REST API (검색) + SOAP API (예약/발권/취소) 하이브리드 아키텍처
- 인증 방식:
- REST: OAuth 2.0 Password Grant (Token 기반)
- SOAP: Basic Authentication (Username/Password)
- 트랜잭션 관리: Stateless (REST), PNR 기반 예약 관리 (SOAP)
- 주요 특징: 하이브리드 API 구조, 발권 4명 청킹 제약, FareBasis 변경 자동 검증
1.2 아키텍처 특징
Controller Layer → Service Layer → Client Layer → Galileo API
↓ ↓ ↓
Request DTO Business Logic REST/SOAP Client
↓ ↓ ↓
Response DTO Domain Model Authentication
(OAuth 2.0 / Basic Auth)
API 타입별 분류
- REST API: 항공편 검색 (CatalogProductOfferings), Token 발급
- SOAP API: 예약 (AirCreateReservation), 발권 (AirTicketing), Pricing (AirPrice), Void (AirVoidDocument), 운임 규정 (AirFareRules), PNR 관리 (UniversalRecord*), 큐 관리 (GdsQueue*)
- 보조 API: KPS (결제), KRT (운임 규정 번역)
1.3 인증 메커니즘
1.3.1 REST Token 인증 (OAuth 2.0)
// GalileoRestClient.kt:48-74
fun getToken(): String {
// 1. Redis 캐시 확인
findGalileoAccessTokenInRedis(pcc) ?: run {
// 2. OAuth 2.0 Token 발급 (grant_type=password)
val response = "${galileoApiProperties.rest.endpoint}/oauth2/v1/token"
.post("grant_type=password&username=$username&password=$password")
.execute<AuthTokenRS>()
// 3. Redis 캐싱 (만료 10분 전까지)
saveGalileoAccessTokenInRedis(
pcc = pcc,
accessToken = response.accessToken,
expiresIn = response.expiresIn - 600 // -10분
)
}
}Token 캐싱 구조
- 캐시 키:
GALILEO_ACCESS_TOKEN::{pcc} - TTL: Token 만료 10분 전까지
- 갱신: 자동 (만료 시 재발급)
1.3.2 SOAP Basic Authentication
// GalileoClient.kt
private fun <T> post(endpoint: String, request: Any): GalileoResponse<T> {
return "${galileoApiProperties.soap.endpoint}$endpoint"
.post(objectMapper.writeValueAsBytes(request).inputStream())
.basicAuth(galileoApiProperties.soap.userName, galileoApiProperties.soap.password)
.header("Content-Type", "text/xml; charset=UTF-8")
.execute()
}Channel/Funnel 기반 라우팅
- PCC (Pseudo City Code)별로 다른 credential 사용
- Offline PCC 지원 (큐 관리용)
2. API 엔드포인트별 상세 분석
2.1 항공편 검색 API (Flight Search)
2.1.1 검색 API
엔드포인트: POST /internals/GALILEO/search
API 타입: REST API (CatalogProductOfferings v11)
기능 설명
- Galileo GDS를 통한 항공편 검색
- REST API 기반 병렬 검색 처리
- Redis 캐싱 (Gzip 압축) 및 Token 재사용
요청 파라미터
data class SearchRequest(
val originDestinationLocationInfos: List<OriginDestinationLocationInfo>,
val cabins: List<CabinType>,
val preferences: List<SearchPreference>,
val airlines: List<String>?,
val onlyDirect: Boolean,
val onlyFreeBaggageInclude: Boolean,
val useCache: Boolean = true,
val logging: Boolean = false
)비즈니스 로직 단계
- 캐시 확인: Redis에서 기존 검색 결과 확인 (GalileoFlightSearchService.kt:45-47)
- Token 발급: OAuth 2.0 Password Grant (Redis 캐싱)
- 공항 확장: 도시 코드를 공항 리스트로 확장
- Cartesian Product: 모든 공항 조합 생성
- 병렬 검색:
pmap으로 병렬 처리 (Coroutines) - REST 요청: CatalogProductOfferings 호출 (30초 타임아웃)
- 결과 필터링:
- 국내 공항 경유 제외 (withoutDomesticAirportInViaRoute)
- NonTicketableCarrier 제외 (MH 항공사 특정 조건)
- 중복 제거 (distinctBy id)
- 미노출 FareItinerary 제외
- 항공사 필터링
- 캐싱 저장: Redis에 결과 저장 (Gzip 압축, TTL 적용)
에러 처리
- Circuit Breaker 적용 (resilience4j)
- slidingWindowSize: 180초
- failureRateThreshold: 35%
- waitDurationInOpenState: 120초
- Fallback: 부분 실패 시 성공한 결과만 반환
- Retry:
pmap레벨에서 실패 시 다른 요청 계속 진행
2.2 예약 API (Booking)
2.2.1 예약 생성
엔드포인트: POST /internals/GALILEO/bookings
API 타입: SOAP API
비즈니스 로직 단계
// GalileoBookingService.kt:49-106
1. FareItinerary 조회
↓
2. 공항 정보 조회 (CityClient) → TimeZone 설정
↓
3. Pricing API 호출 (가격 재확인)
- GalileoClient.pricing()
- SOLD_OUT 검증: "are not bookable" 메시지
↓
4. Book API 호출 (PNR 생성)
- GalileoClient.book()
- Request: AirCreateReservationRQ (4268 라인!)
- ProviderPNR 획득
↓
5. Retrieve API 호출 (예약 확인)
- GalileoClient.getBooking()
- carrierTimeLimit null 시 3초 대기 후 재조회
↓
6. Schedule 순서 복원 (fareItinerary 기준)
↓
7. 성공 시:
- FareItinerary 저장 (PNR 기준)
- 검색 캐시 삭제
실패 시:
- PNR 취소 (비동기)
- 미노출 FareItinerary 마킹 (SOLD_OUT인 경우)주요 처리 사항
- TimeZone 조회: CityClient를 통해 공항별 TimeZone 동적 조회 (Amadeus/Sabre는 고정 매핑)
- 승객 정보:
- 이름, 생년월일, 성별, 국적
- 여권 정보 (DOCS)
- 연락처 (SSR: CTCE 이메일, CTCM 전화번호)
- 유아 정보 (INFT)
- 항공사별 SSR/OSI 포맷 매핑
- PNR 생성: ProviderPNR (6자리 영숫자 코드)
- CarrierTimeLimit null 특별 처리:
// GalileoBookingService.kt:80-88 if (booking.carrierTimeLimit == null) { withBlocking { delay(3000) // 3초 대기 galileoClient.getBooking(providerPnr = providerPnr, findTimeZoneId = findTimeZoneId) } }
2.2.2 예약 조회
엔드포인트: GET /internals/GALILEO/bookings/{pnr}
처리 로직
- UniversalRecordRetrieve 호출 (SOAP)
- 예약 정보 파싱 (승객, 일정, PriceQuote, SSR, OSI)
- 예약 정보 반환
2.2.3 Repricing
엔드포인트: POST /internals/GALILEO/bookings/{pnr}/repricing
처리 로직
// GalileoBookingService.kt:132-172
fun repricing(pnr, passengers): Booking {
val booking = galileoClient.getBooking(pnr)
// Guaranteed Pricing 체크
if (!booking.isGuaranteedPrice) {
// 1. 기존 AirPricing 삭제
galileoClient.deleteElements(pnr, ElementType.AIR_PRICING_INFO)
// 2. 재 Pricing
val newBooking = pricing(booking.pnr!!, passengers)
// 3. FareBasis 변경 검증
validateFareBasisChange(booking, newBooking)
}
return booking
}
private fun validateFareBasisChange(originBooking: Booking, newBooking: Booking) {
// FareBasis가 변경되면 PNR 취소 + SOLD_OUT 예외
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"
)
}
}
}Guaranteed Pricing
- Galileo만의 고유한 개념
isGuaranteedPrice: Pricing 시점에서 가격이 보장되는지 여부- 보장되지 않으면 발권 전에 반드시 Repricing 필요
- Amadeus/Sabre는 이 개념 없음
2.2.4 PNR 분할 (Divide)
엔드포인트: POST /internals/GALILEO/bookings/{pnr}/divide
처리 로직
- 특정 승객을 새 PNR로 분리
- UniversalRecordModify의 Split 기능 사용
- 제약사항:
- 유아/소아 포함 시 불가
- 모든 승객 요청 시 불가
2.3 발권 API (Ticketing)
2.3.1 발권 준비
엔드포인트: POST /internals/GALILEO/ticketing/ready
검증 사항
- Carrier Time Limit 확인 (2분 이상 남아있어야 함)
- Carrier PNR 존재 여부
- 스케줄 상태 (Confirming 또는 Confirmed)
- 티켓 미발권 상태
- Guaranteed Pricing 체크:
- 보장 안 된 경우 Repricing 수행
- FareBasis 변경 검증
2.3.2 발권 실행
엔드포인트: POST /internals/GALILEO/ticketing
API 타입: SOAP API
발권 프로세스
// GalileoTicketingService.kt:91-190
val booking = galileoClient.getBooking(pnr)
var payment: Payment? = null
try {
// 1. 예약 상태 검증
booking.validateBookingConditionForTicketing()
// 2. KPS 결제 승인
payment = kpsPaymentClient.approve(
pnr = pnr,
validatingCarrier = validatingCarrier,
prices = passengerPrices,
paymentInfo = paymentInfo
)
// 3. Payment Remark 추가
galileoClient.addPaymentInfoRemark(pnr, payment)
// 4. Commission 처리
val commission = convertCommission(
pricingCommission = booking.passengers?.first()?.fare?.commission,
overCommission = overCommission
)
if (commission != null) {
galileoClient.saveCommission(pnr, validatingCarrier, commission)
}
// 5. 발권 (4명씩 청킹)
val ticketingResults = passengerPrices
.groupBy { "${it.type}${it.cashPrice}" }
.flatMap { (_, groupedPassengerPrices) ->
groupedPassengerPrices.chunked(4).flatMap { chunkedPassengerPrice ->
galileoClient.ticketing(
pnr = pnr,
validatingCarrier = validatingCarrier,
passengerPrices = chunkedPassengerPrice,
payment = payment,
commission = commission,
timeoutCallback = {
slackService.sendTicketingTimeout(Supplier.GALILEO, pnr)
}
)
}
}
// 6. 티켓 문서 조회
val tickets = galileoClient.getTicketDocuments(pnr)
return booking.copy(tickets = tickets).withPayment(payment)
} catch (e: Exception) {
// 발권 실패 시 5초 대기 후 비동기 취소
if (payment != null) {
cancelAsync(pnr, validatingCarrier, payment, keepPnr, ticketCount)
}
throw e
}주요 API 호출
AirTicketingRQ: 티켓 발행 (4명씩 청킹)UniversalRecordRetrieve: 티켓 문서 조회
발권 4명 청킹 제약
// GalileoTicketingService.kt:140
groupedPassengerPrices.chunked(4).forEach { chunkedPassengerPrice ->
// 발권
}- 이유: Galileo API 제약 (1회 요청당 4명까지)
- Amadeus/Sabre는 이러한 제약 없음
Commission 로직
// GalileoTicketingService.kt:192-208
private fun convertCommission(
pricingCommission: Commission?,
overCommission: Commission?,
): Commission? {
return when {
// Pricing Commission이 있으면 Override가 더 큰 경우만 사용
pricingCommission != null ->
overCommission?.takeIf { pricingCommission.value < it.value }
// Pricing Commission이 없으면 Override 사용
overCommission != null -> overCommission
// 둘 다 없으면 GROSS 0.0
else -> Commission(type = CommissionType.GROSS, value = 0.0, tourCode = null)
}
}2.4 운임 규정 API (Fare Rules)
엔드포인트: GET /internals/GALILEO/fare-rules
API 타입: SOAP API + KRT (한국어 번역)
기능 설명
- 운임 규정 및 제한 사항 조회
- 환불/변경 수수료 정보
- KRT 서비스를 통한 한국어 번역
처리 로직
// GalileoFareRuleService.kt:34-98
fun getFareRules(key, adult, child, infant): List<FareRule> {
// 1. Redis 캐시 확인
val savedFareRules = fareRuleRepository.findFareRules(fareRuleKey)
if (savedFareRules != null) return savedFareRules
// 2. FareItinerary 조회
val fareItinerary = galileoFlightSearchService.getFareItinerary(key)
// 3. 공항 정보 조회 (TimeZone)
val airportMap = getAirportMap(fareItinerary)
// 4. Pricing API 호출 (성인만)
val pricedFareItinerary = galileoClient.pricing(
fareItinerary = fareItinerary,
passengerInfo = PassengerInfo(adult = adult, child = 0, infant = 0)
).first().apply {
// 성인 Fare만 필터링
passengers!!.filter { it.type == PassengerType.ADULT }
}
// 5. Fare Rules 조회 (SOAP)
val fareRules = galileoClient.getFareRules(
fareItinerary = pricedFareItinerary,
airportMap = airportMap
)
// 6. 그룹별 병렬 번역 (KRT)
return fareRules.groupBy { it.groupSequence }
.entries
.pmap { (_, fareRules) ->
krtClient.getFareRules(
validatingCarrier = pricedFareItinerary.validatingCarrier,
fareRules = fareRules
)
}.flatten()
.sortedWith(compareBy({ it.groupSequence }, { it.ordered }))
.also {
// 7. Redis 캐싱
fareRuleRepository.saveFareRules(fareRuleKey, it)
}
}KRT (Korean Translation) 서비스
- 엔드포인트:
/FareRuleTransKor.aspx - 역할: Galileo Fare Rule을 한국어로 번역
- 처리: 그룹별 병렬 번역 (pmap)
- 필터링:
isVisible=true만 반환
2.5 취소 API (Cancel)
엔드포인트: PUT /internals/GALILEO/bookings/{pnr}/cancel
API 타입: SOAP (Void + PNR Cancel)
취소 유형 판별
// GalileoCancelService.kt:29-60
fun cancel(pnr, validatingCarrier, payment): List<Refund> {
val booking = galileoClient.getBooking(pnr)
// 티켓 없음 → PNR 취소만
if (booking.tickets.isNullOrEmpty()) {
handleEmptyTicketHistory(booking, validatingCarrier, payment)
return emptyList()
}
// Void 가능 여부 확인
return if (isVoidable(booking)) {
// Void 가능 → Void + Payment 취소 + PNR 취소
voidAll(booking, validatingCarrier, payment)
emptyList()
} else {
// Void 불가 → 취소 불가 예외
throw InternationalAdapterException(ErrorMessage.CANCEL_UNABLE, pnr)
}
}isVoidable 판별 로직
// GalileoCancelService.kt:234-261
private fun isVoidable(booking: Booking): Boolean {
// 1. NonVoidableAirline 체크
if (NonVoidableAirline.contains(booking.validatingCarrier)) {
return false
}
// 2. EMD 티켓 체크
if (booking.hasEmdTicket) {
return false
}
// 3. 티켓 없음
if (booking.tickets.isNullOrEmpty()) {
return false
}
// 4. 티켓 문서 조회
return galileoClient.getTicketDocuments(booking.pnr!!, booking.tickets)
.let {
// 티켓 수 불일치
if (booking.tickets!!.size != it.size) return false
// 티켓 상태 확인
it.isVoidable()
}
}
// Ticket.kt
fun List<Ticket>.isVoidable(): Boolean {
// Check-in 상태면 즉시 false
if (this.any { it.status == TicketStatus.CHECKIN }) {
throw StatusInvalidException(ErrorMessage.CANCEL_UNABLE_BY_ALREADY_CHECK_IN)
}
// 발권일이 오늘이고, 모든 티켓이 ISSUE 또는 AIRPORT_CONTROL 상태
return this.all { it.issuedAt.toLocalDate().isEqual(today()) } &&
this.all { it.status == TicketStatus.ISSUE || it.status == TicketStatus.AIRPORT_CONTROL }
}Void 프로세스
// GalileoCancelService.kt:108-151
private fun voidAll(booking: Booking, validatingCarrier: String, payment: Payment?) {
val ticketNumbers = booking.tickets!!
.map { it.ticketNumber }
.matchingTicketsFrom(booking.tickets!!) // Conjunction Ticket 필터링
// Void 재시도 (최대 3회)
val voidedTicketNumbers = mutableListOf<String>()
for (count in 1..3) {
val aliveTicketNumbers = ticketNumbers - voidedTicketNumbers
try {
withBlocking {
delay(1000)
galileoClient.void(
pnr = booking.pnr!!,
ticketNumbers = aliveTicketNumbers
).apply { voidedTicketNumbers.addAll(this) }
}
break
} catch (e: Exception) {
if (count >= 3) {
slackService.sendVoidFail(
supplier = Supplier.GALILEO,
pnr = booking.pnr!!,
targetTicketNumbers = ticketNumbers,
voidedTicketNumbers = voidedTicketNumbers,
reason = e.message
)
throw e
}
}
}
// Payment 취소 (비동기)
if (validatingCarrier != null && payment != null) {
paymentCancelAsync(booking.pnr!!, validatingCarrier, payment)
}
// PNR 취소 (비동기)
pnrCancelAsync(booking.pnr!!)
}Conjunction Ticket 필터링
// GalileoCancelService.kt:153-164
private fun List<String>.matchingTicketsFrom(tickets: List<PnrTicket>): List<String> {
// Conjunction Ticket이 없으면 전체 반환
if (tickets.all { it.conjunctionTicketNumbers == null }) {
return this
}
// Conjunction Ticket이 있으면 메인 티켓만 필터링
val validTicketNumbers = tickets
.filter { it.type == TicketType.TICKET }
.map { it.ticketNumber }
.toSet()
return this.filter { it in validTicketNumbers }
}PNR 취소
// GalileoCancelService.kt:78-102
private fun pnrCancel(pnr: String) {
for (count in 1..2) { // 최대 2회 재시도
try {
galileoClient.cancelPnr(pnr)
break
} catch (e: Exception) {
// 이미 취소된 PNR인 경우 정상 종료
if (e is ApiException && e.errorMessage == ErrorMessage.ALREADY_CANCELED_PNR) {
logger.info("[GALILEO] PNR already canceled: $pnr", e)
break
}
// 최종 실패 시 Slack 알림
if (count >= 2) {
slackService.sendCancelFail(
supplier = Supplier.GALILEO,
pnr = pnr,
reason = e.message
)
throw e
}
}
}
}2.6 큐 관리 API (Queue Management)
2.6.1 큐 목록 조회
엔드포인트: GET /internals/GALILEO/queues
API 타입: SOAP API
처리 로직
// GalileoQueueService.kt:31-56
fun getQueuePnrs(): List<QueuePnrInfo> {
// TARGET_QUEUE_NUMBERS = [21, 22, 23]
// 1. PCC별 큐 속성 맵 생성 (Offline PCC 포함)
val queuePropertyMap = getQueuePropertyMap()
return queuePropertyMap.values.flatMap { queueProperty ->
// 2. 모든 큐의 PNR 수 조회 (병렬)
TARGET_QUEUE_NUMBERS.pmap { queueNumber ->
val queueCount = galileoClient.getQueueCount(
queueProperty = queueProperty,
queueNumber = queueNumber
)
// 3. PNR이 있는 큐에 대해 PNR 목록 조회
if (queueCount > 0) {
val pnrs = galileoClient.getQueuePnrs(
queueProperty = queueProperty,
queueNumber = queueNumber
)
pnrs.map { pnr ->
QueuePnrInfo(
pnr = pnr,
queueNumber = queueNumber,
pcc = queueProperty.pcc
)
}
} else {
emptyList()
}
}.getOrEmpty()
}.flatten()
}Offline PCC 지원
// GalileoQueueService.kt:111-121
private fun getQueuePropertyMap(): MutableMap<String, QueueRequestProperty> {
return mutableMapOf<String, QueueRequestProperty>().apply {
// Online PCC 추가
galileoApiProperties.forEach { ... }
// Offline PCC 추가 (큐 관리 전용)
galileoApiProperties.first().soap.let {
set(
it.offline.pcc, QueueRequestProperty(
pcc = it.offline.pcc,
endpoint = it.endpoint,
userName = it.offline.userName,
password = it.offline.password,
branchCode = it.offline.branchCode,
)
)
}
}
}2.6.2 큐 제거
엔드포인트: PUT /internals/GALILEO/queues
API 타입: SOAP API
처리 로직
// GalileoQueueService.kt:59-93
fun remove(queuePnrInfos: List<QueuePnrInfo>): List<QueuePnrInfo> {
// 1. PCC별로 그룹화
val failedQueues = queuePnrInfos.groupBy { it.pcc }
.flatMap { (pcc, queuePnrInfos) ->
// 2. 10개씩 청킹 후 병렬 제거
queuePnrInfos.chunked(10).flatMap { chunkedTargetQueues ->
chunkedTargetQueues.pmap {
try {
galileoClient.removePnrsInQueue(
queueProperty = queuePropertyMap[pcc]!!,
queueNumber = it.queueNumber,
pnrs = listOf(it.pnr)
)
null // 성공
} catch (e: Exception) {
it // 실패한 큐 반환
}
}.filterNotNull()
}
}
// 3. 전체 실패 시 Slack 알림
if (failedQueues.size == queuePnrInfos.size) {
slackService.sendQueueFail(
supplier = Supplier.GALILEO.name,
reason = "All queues failed to remove"
)
}
return failedQueues
}재시도 메커니즘
// GalileoClient.kt:880
@Retryable(maxAttempts = 3, backoff = Backoff(delay = 5000))
fun removePnrsInQueue(...)- 최대 3회 재시도
- 5초 간격
3. 공통 컴포넌트
3.1 인증 및 보안
REST Token 관리
- OAuth 2.0 Password Grant
- Redis 캐싱 (만료 10분 전까지)
- 자동 갱신
SOAP Basic Auth
- PCC별로 다른 credential
- Channel/Funnel 기반 라우팅
- Offline PCC 지원
3.2 캐싱 전략
캐시 키 구조
GALILEO_{type}_{key}_{timestamp}
TTL 설정
- REST Token: 만료 10분 전까지 (
GALILEO_ACCESS_TOKEN::{pcc}) - 검색 결과: 설정된 TTL (flightSearchKey, fareItinerary)
- 운임 규정: 설정된 TTL (fareRule)
Redis 압축
// RedisConfiguration.kt:24-27
this.valueSerializer = GzipRedisSerializer<FareItinerary>(
Jackson2JsonRedisSerializer(objectMapper, FareItinerary::class.java)
)- FareItinerary: Gzip 압축 (직렬화 크기가 큼)
- Amadeus: Snappy 압축 사용
3.3 재시도 및 복구
재시도 정책
- PNR 취소: 2회 재시도 (수동 루프)
- Void: 3회 재시도 (수동 루프, 1초 간격)
- 큐 제거: 3회 재시도 (@Retryable, 5초 간격)
- CarrierTimeLimit 조회: 1회 재시도 (3초 대기)
Circuit Breaker
# application.yml
resilience4j.circuitbreaker:
instances:
galileoSearch:
slidingWindowSize: 180
failureRateThreshold: 35
waitDurationInOpenState: 120s- 검색 API에만 적용
- 180초 윈도우에서 실패율 35% 초과 시 Open
- Open 상태 2분 유지
3.4 비동기 처리 패턴
5초 대기 후 비동기 처리
// 1. PNR 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000) // Locked PNR 방지
pnrCancel(pnr)
}
// 2. 발권 실패 시 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000)
cancelAsync(pnr, validatingCarrier, payment, keepPnr, ticketCount)
}
// 3. Payment 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
paymentCancelAsync(pnr, validatingCarrier, payment)
}3.5 에러 처리 패턴
에러 타입별 처리
StatusInvalidException: 예약/발권 상태 불일치InternationalAdapterException: 비즈니스 로직 에러ApiException: API 호출 실패 (특정 에러 메시지 처리)
Slack 알림 케이스
- 발권 타임아웃 (GalileoTicketingService.kt:149-155)
- Void 실패 (GalileoCancelService.kt:179-188)
- Payment 취소 실패 (GalileoCancelService.kt:218-224)
- PNR 취소 실패 (GalileoCancelService.kt:93-99)
- 큐 작업 실패 (GalileoQueueService.kt:90)
4. Galileo 특징
4.1 하이브리드 API 아키텍처
REST API vs SOAP API 분류
| 작업 | API 타입 | 이유 |
|---|---|---|
| 검색 (CatalogProductOfferings) | REST | 무상태, 병렬 처리 최적화, 빠른 응답 |
| Token 발급 | REST | OAuth 2.0 표준 |
| 예약 (AirCreateReservation) | SOAP | 복잡한 요청 구조, 트랜잭션 보장 |
| 발권 (AirTicketing) | SOAP | 순차 처리 필요 |
| Pricing (AirPrice) | SOAP | 예약 프로세스 일부 |
| Void (AirVoidDocument) | SOAP | 순차 처리 필요 |
| 운임 규정 (AirFareRules) | SOAP | 상태 유지 필요 |
| PNR 관리 (UniversalRecord*) | SOAP | 트랜잭션 보장 필요 |
| 큐 관리 (GdsQueue*) | SOAP | 상태 유지 필요 |
4.2 항공사별 특별 처리
4.2.1 NonVoidableAirline (Void 불가능 항공사)
// NonVoidableAirline.kt
enum class NonVoidableAirline {
HY, // Uzbekistan Airways
MF, // Xiamen Airlines
SU, // Aeroflot
QH, // Bamboo Airways
}4.2.2 SSR Email Suffix
// PnrUtils.kt:17-23
fun ssrEmailSuffix(validatingCarrier: String): String {
return when (validatingCarrier) {
"AA", "BR", "CI", "DL", "HA", "LO", "SV", "AM" -> "/EN"
"NH" -> "/KO"
else -> ""
}
}4.2.3 SSR Mobile Suffix
// PnrUtils.kt:25-32
fun ssrMobileSuffix(validatingCarrier: String): String {
return when (validatingCarrier) {
"AA", "BR", "CI", "LO", "SV" -> "/EN"
"NH" -> "/KO"
"QR", "CA" -> "/KR"
else -> ""
}
}4.2.4 OSI Email Prefix
// PnrUtils.kt:68-74
fun osiEmailPrefix(validatingCarrier: String): String? {
return when (validatingCarrier) {
"MU", "UO", "VJ", "ZE", "ET", "7C", "SC", "MF", "HA", "TG" -> "CTCE "
"CZ" -> "EMAIL "
else -> null
}
}4.2.5 OSI Mobile Prefix
// PnrUtils.kt:76-88
fun osiMobilePrefix(validatingCarrier: String): String? {
return when (validatingCarrier) {
"MU" -> "CTCM0082"
"UO" -> "CTCM SEL 82"
"VJ" -> "CTCM 82-"
"7C" -> "CTCM SEL"
"ZE" -> "CTCM "
"ET", "MF" -> "CTCM 82"
"NX" -> "82-"
"CZ", "SC" -> "CTCM82"
else -> null
}
}4.2.6 FOID Passport 필수 항공사
// Constants.kt:4
const val FOID_PASSPORT_CARRIERS = "MU,CZ,CA,MF,KC"- 중국 항공사들: 여권 정보를 FOID로 추가 전송
4.2.7 NonTicketableCarrier (MH 항공사)
// GalileoRestClient.kt:163-169
private fun FareItinerary.hasNonTicketableCarrier(): Boolean {
return this.validatingCarrier == "MH" && this.schedules.any { schedule ->
schedule.segments.any {
it.marketingCarrier != this.validatingCarrier
}
}
}- MH (Malaysia Airlines): 타 항공사 마케팅 편이 포함된 경우 발권 불가
4.3 Galileo만의 고유 개념
4.3.1 Guaranteed Pricing
// Booking.kt
val isGuaranteedPrice: Boolean
get() = this.passengers?.all {
it.fare?.airPricingModifiers?.any {
it.prohibitedRuleCategories?.guaranteedFareType == "Guaranteed"
} ?: false
} ?: false- 개념: Pricing 시점에서 가격이 보장되는지 여부
- 사용: 발권 전 Repricing 필요 여부 판단
- Amadeus/Sabre: 이 개념 없음
4.3.2 TimeZone 동적 조회
- CityClient를 통해 공항별 TimeZone 동적 조회
- Amadeus/Sabre: 고정 매핑 사용
4.3.3 발권 4명 청킹
- Galileo API 제약: 1회 요청당 4명까지
- Amadeus/Sabre: 이러한 제약 없음
4.3.4 CarrierTimeLimit 재조회
- 예약 직후 null인 경우 3초 대기 후 재조회
- Amadeus/Sabre: 이러한 처리 없음
4.3.5 FareBasis 변경 자동 검증
- Repricing/Ticketing 시 FareBasis 변경 시 자동으로 SOLD_OUT 처리
- Amadeus: 이러한 검증 없음 (가격 변경만 체크)
5. 주의사항
5.1 성능 고려사항
- REST Token 캐싱으로 불필요한 인증 요청 방지
- 검색 결과 Gzip 압축으로 Redis 메모리 절약
- 병렬 처리 (pmap) 적극 활용
- 발권 4명 청킹으로 API 제약 준수
5.2 에러 처리
- Void 실패 시 재시도 (3회, 1초 간격)
- PNR 취소 실패 시 재시도 (2회)
- 큐 제거 실패 시 재시도 (3회, @Retryable)
- Locked PNR 방지 (5초 대기)
- Circuit Breaker (검색 API)
5.3 보안
- REST Token Redis 캐싱 (만료 관리)
- SOAP Basic Auth (PCC별 credential)
- PCI DSS 준수 (카드 정보는 KPS 처리)
- 민감 정보 로깅 금지
5.4 비동기 처리 주의
- PNR 취소, Payment 취소는 5초 대기 후 비동기 처리
- 발권 실패 시 취소도 5초 대기 후 비동기 처리
- Locked PNR 방지가 목적
5.5 발권 제약
- 4명씩 청킹 발권 (Galileo API 제약)
- Type+Price로 그룹화하여 발권
- 타임아웃 시 Slack 알림
5.6 Guaranteed Pricing
- 발권 전 반드시 체크
- 보장 안 된 경우 Repricing 필수
- FareBasis 변경 시 PNR 취소 + SOLD_OUT
6. Amadeus/Sabre와의 차이점 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| API 타입 | REST (검색) + SOAP (예약/발권) | SOAP only | REST (검색) + SOAP (예약/발권) |
| 인증 방식 | OAuth 2.0 (REST) + Basic Auth (SOAP) | Session 기반 (Security Token) | OAuth 2.0 (REST) + Session (SOAP) |
| 검색 API | REST (CatalogProductOfferings) | SOAP (FareMasterPricer) | REST (BargainFinderMax) |
| 운임 규정 | SOAP (AirFareRules) + KRT 번역 | SOAP (CommandCryptic) | REST (FareRuleClient) |
| Token 캐싱 | Redis 캐싱 (만료 10분 전) | 없음 | Redis 캐싱 (만료 전) |
| 병렬 처리 | pmap (REST) | cartesianProduct + pmap (SOAP) | pmap (REST) |
| Circuit Breaker | 있음 (검색만) | 있음 (Resilience4j) | 없음 |
| 발권 제약 | 4명씩 청킹 | 없음 | 없음 |
| Void 조건 | 발권일=오늘 + 상태 체크 | 발권일=오늘 + 항공사별 제약 | 발권일=오늘 |
| Void 재시도 | 3회 (1초 간격) | 3회 | 3회 |
| PNR 취소 재시도 | 2회 | 2회 | 2회 |
| Guaranteed Pricing | 있음 (고유 개념) | 없음 | 없음 |
| FareBasis 검증 | Repricing 시 자동 검증 | 없음 | 없음 |
| TimeZone 처리 | 동적 조회 (CityClient) | 고정 매핑 | 고정 매핑 |
| CarrierTimeLimit | 재조회 메커니즘 (3초) | 특별 처리 (3초) | 특별 처리 (3초) |
| 큐 관리 | 21/22/23번 큐 자동 조회 | 수동 처리 | 5/6/7/20번 큐 자동 조회 |
| Offline PCC | 지원 (큐 관리용) | 없음 | LEGACY_PCC 제외 |
| 결제 시스템 | KPS | TopAs | SabrePaymentClient |
| 운임 규정 번역 | KRT (한국어) | ART (한국어) | 없음 |
| Conjunction Ticket | 자동 필터링 | 수동 처리 | 수동 처리 |
| NonVoidableAirline | HY, MF, SU, QH | HY, MF, SU, QH | HY, MF, SU, QH |
| Redis 압축 | Gzip | Snappy | 없음 |
7. 트러블슈팅 가이드
7.1 일반적인 문제
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| TOKEN EXPIRED | REST Token 만료 | 새 Token 발급 (자동) |
| SOLD_OUT (Pricing) | “are not bookable” 메시지 | FareItinerary 미노출 마킹, PNR 취소 |
| CarrierTimeLimit null | 예약 직후 미반환 | 3초 대기 후 재조회 |
| FareBasis 변경 | Repricing 시 FareBasis 변경 | PNR 취소 + SOLD_OUT 예외 |
| VOID FAILED | Void 불가 조건 | 조건 확인 (NonVoidableAirline, 발권일, 상태) |
| ALREADY_CANCELED_PNR | 이미 취소된 PNR | 재시도 중단 (정상 처리) |
| CHECKIN 상태 | 체크인 완료 | CANCEL_UNABLE_BY_ALREADY_CHECK_IN 예외 |
| 발권 타임아웃 | 네트워크/시스템 지연 | Slack 알림, 비동기 취소 |
| 4명 이상 발권 | 청킹 미처리 | 4명씩 청킹 확인 |
7.2 디버깅 팁
- REST API 로깅 활성화 (logging 파라미터)
- SOAP 요청/응답 로깅 활성화 (supplierLoggingProperties)
- Redis 캐시 확인 (Token, FareItinerary, FareRule)
- Slack 알림 확인 (에러 발생 시)
- Guaranteed Pricing 플래그 확인
7.3 성능 이슈
- Token 캐싱 확인 (Redis)
- 검색 결과 캐싱 확인
- Gzip 압축 확인
- 병렬 처리 (pmap) 활용 확인
8. 참고 자료
8.1 주요 클래스
GalileoRestClient: REST 통신 클라이언트 (검색, Token)GalileoClient: SOAP 통신 클라이언트 (917 라인, 핵심)GalileoFlightSearchService: 검색 서비스GalileoBookingService: 예약 서비스GalileoTicketingService: 발권 서비스GalileoCancelService: 취소 서비스GalileoQueueService: 큐 관리 서비스GalileoFareRuleService: 운임 규칙 서비스GalileoPassengerService: 승객 정보 관리 서비스KpsPaymentClient: KPS 결제 클라이언트KrtClient: 운임 규정 번역 클라이언트
8.2 설정 파일
application.yml: 기본 설정application-{env}.yml: 환경별 설정redisson-{env}.yml: Redis 설정GalileoProperties.kt: Galileo 공급사 설정
8.3 상수 파일
NonVoidableAirline.kt: Void 불가능 항공사 (HY, MF, SU, QH)Constants.kt: FOID_PASSPORT_CARRIERSPnrUtils.kt: SSR/OSI 포맷 매핑
8.4 관련 문서
- Galileo Web Services Documentation
- Galileo REST API Guide (CatalogProductOfferings)
- Galileo SOAP API Guide (AirService, UniversalRecordService, GdsQueueService)
- OAuth 2.0 Specification
9. 변경 이력
| 버전 | 날짜 | 변경 내용 | 작성자 |
|---|---|---|---|
| 1.0 | 2025-09-30 | 최초 작성 | Claude Code |
이 문서는 Triple Air International Adapter 프로젝트의 Galileo GDS API 분석 문서입니다.