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.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
)

비즈니스 로직 단계

  1. 캐시 확인: Redis에서 기존 검색 결과 확인 (GalileoFlightSearchService.kt:45-47)
  2. Token 발급: OAuth 2.0 Password Grant (Redis 캐싱)
  3. 공항 확장: 도시 코드를 공항 리스트로 확장
  4. Cartesian Product: 모든 공항 조합 생성
  5. 병렬 검색: pmap으로 병렬 처리 (Coroutines)
  6. REST 요청: CatalogProductOfferings 호출 (30초 타임아웃)
  7. 결과 필터링:
    • 국내 공항 경유 제외 (withoutDomesticAirportInViaRoute)
    • NonTicketableCarrier 제외 (MH 항공사 특정 조건)
    • 중복 제거 (distinctBy id)
    • 미노출 FareItinerary 제외
    • 항공사 필터링
  8. 캐싱 저장: 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}

처리 로직

  1. UniversalRecordRetrieve 호출 (SOAP)
  2. 예약 정보 파싱 (승객, 일정, PriceQuote, SSR, OSI)
  3. 예약 정보 반환

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 발급RESTOAuth 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와의 차이점 비교

항목GalileoAmadeusSabre
API 타입REST (검색) + SOAP (예약/발권)SOAP onlyREST (검색) + SOAP (예약/발권)
인증 방식OAuth 2.0 (REST) + Basic Auth (SOAP)Session 기반 (Security Token)OAuth 2.0 (REST) + Session (SOAP)
검색 APIREST (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 제외
결제 시스템KPSTopAsSabrePaymentClient
운임 규정 번역KRT (한국어)ART (한국어)없음
Conjunction Ticket자동 필터링수동 처리수동 처리
NonVoidableAirlineHY, MF, SU, QHHY, MF, SU, QHHY, MF, SU, QH
Redis 압축GzipSnappy없음

7. 트러블슈팅 가이드

7.1 일반적인 문제

문제원인해결 방법
TOKEN EXPIREDREST Token 만료새 Token 발급 (자동)
SOLD_OUT (Pricing)“are not bookable” 메시지FareItinerary 미노출 마킹, PNR 취소
CarrierTimeLimit null예약 직후 미반환3초 대기 후 재조회
FareBasis 변경Repricing 시 FareBasis 변경PNR 취소 + SOLD_OUT 예외
VOID FAILEDVoid 불가 조건조건 확인 (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_CARRIERS
  • PnrUtils.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.02025-09-30최초 작성Claude Code

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