Phase 3: Galileo 발권 API 심층 분석
1. API 엔드포인트 개요
1.1 발권 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/GALILEO/ticketing/ready | POST | SOAP | 발권 준비 (Repricing) | GalileoTicketingController.kt:18-23 |
/internals/GALILEO/ticketing | POST | SOAP + KPS | 실제 발권 실행 | GalileoTicketingController.kt:25-46 |
2. 발권 준비 (Ready)
2.1 전체 발권 준비 플로우
flowchart TD A[발권 준비 요청] --> B[PNR 조회<br/>getBooking] B --> C[발권 가능 상태 검증<br/>validateBookingConditionForTicketing] C --> D{모든 PricingInfo<br/>Guaranteed?} D -->|Yes| E[승객 정보 null 처리<br/>withPassengers] D -->|No| F[공항 정보 수집<br/>airportMap] F --> G[기존 PricingInfo<br/>삭제] G --> H[예약 재조회<br/>API version 갱신] H --> I[Pricing 재실행<br/>galileoClient.pricing] I --> J[PriceInfo 추가<br/>addPriceInfo] J --> K[예약 재조회] K --> L[FareBasis 변경 검증<br/>validateFareBasisChange] E --> M[Ready 완료] L --> M 에러 플로우 L --> ERR{예외 발생} ERR --> V[비동기 취소<br/>cancelAsync] V --> W[5초 대기] W --> X[Void 실행<br/>voidRepeat] X --> Y[결제 취소<br/>paymentCancelAsync] Y --> Z{keepPnr?} Z -->|No| AA[PNR 취소<br/>pnrCancelAsync] Z -->|Yes| AB[PNR 유지] AA --> AC[예외 재발생] AB --> AC style C fill:#B3D9FF,stroke:#333,stroke-width:2px,color:#000 style D fill:#FFE5B4,stroke:#333,stroke-width:2px,color:#000 style G fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style M fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style P fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style U fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000 style ERR fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style V fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
3.2 단계별 상세 분석
Step 1: 예약 조회 및 검증
위치: GalileoTicketingService.kt:91-107
fun issue(
pnr: String,
validatingCarrier: String,
passengerPrices: List<PassengerPrice>,
cardInfo: PaymentInfo.KeyInCard,
keepPnr: Boolean,
): Booking {
val payment = try {
galileoClient.getBooking(providerPnr = pnr, findTimeZoneId = findTimeZoneId).also {
it.validateBookingConditionForTicketing()
}
} catch (e: Exception) {
if (keepPnr.not()) {
cancelService.pnrCancelAsync(pnr)
}
throw e
}.run { ... }
// ...
}keepPnr 파라미터:
- 목적: 발권 실패 시 PNR 유지 여부 결정
- true: 발권 재시도 가능 (PNR 유지)
- false: PNR 취소 (기본값)
Step 2: KPS 결제 승인
위치: GalileoTicketingService.kt:108-117
kpsClient.approve(
cardInfo = cardInfo,
pnr = pnr,
validatingCarrier = validatingCarrier,
reservationCode = this.supplierIdentificationKey!!,
amount = passengerPrices.sumOf { it.cardPrice }.toString()
).also {
galileoClient.addPaymentInfoRemark(booking = this, payment = it)
}KPS (Korean Payment Service):
- API: KpsBspCardAuthRQ
- 파라미터:
cardInfo: 카드 정보 (카드번호, 유효기간, CVC 등)pnr: Provider PNRvalidatingCarrier: 발권 항공사reservationCode: Universal Record PNRamount: 총 결제 금액 (cardPrice 합산)
- 반환: Payment 객체 (승인번호, 거래일시 등)
Payment Remark 추가:
- API: UniversalRecordModifyRQ
- 목적: PNR에 결제 정보 저장
- 내용: 승인번호, 결제 금액, 카드 정보 등
Step 3: Commission 처리
위치: GalileoTicketingService.kt:121-136
// remark 추가 후 api version 갱신을 위해 retrieve
val booking = galileoClient.getBooking(providerPnr = pnr)
booking.bookingReference?.pricingInfoReferences
?.mapNotNull { pricingInfoReference ->
val passenger = booking.passengers!!.find { it.type == pricingInfoReference.type }
val passengerPrice = passengerPrices.find {
it.type == passenger?.type && it.identificationKey == passenger.identificationKey
}
convertCommission(
pricingCommission = passenger?.commission,
overCommission = passengerPrice?.commission
)?.let {
pricingInfoReference.type to it
}
}?.run {
galileoClient.saveCommission(booking = booking, commissions = this)
}Commission 로직:
private fun convertCommission(
pricingCommission: Commission?,
overCommission: Commission?,
): Commission? {
return when {
pricingCommission != null -> overCommission?.takeIf { pricingCommission.value < it.value }
overCommission != null -> overCommission
else -> Commission(
type = CommissionType.GROSS,
value = 0.0,
tourCode = null
)
}
}Commission 우선순위:
pricingCommission < overCommission: overCommission 사용pricingCommission == null: overCommission 사용overCommission == null: 0.0 (GROSS) 사용pricingCommission >= overCommission: null (변경 없음)
Step 4: 승객별 발권 처리
위치: GalileoTicketingService.kt:138-158
passengerPrices.groupBy { "${it.type}${it.cashPrice}" }
.forEach { (_, groupedPassengerPrices) ->
groupedPassengerPrices.chunked(4).forEach { chunkedPassengerPrice ->
val pricingReference =
booking.bookingReference?.pricingInfoReferences?.first { it.type == chunkedPassengerPrice.first().type }
galileoClient.ticketing(
reservationPnr = booking.subPnr!!,
passengerPrices = chunkedPassengerPrice,
cardInfo = cardInfo,
payment = payment,
pricingReference = pricingReference!!,
timeoutCallback = {
slackService.sendTicketingTimeout(
supplier = Supplier.GALILEO,
pnr = pnr,
subPnr = booking.subPnr
)
}
)
}
}발권 전략:
-
그룹화:
승객 타입 + 현금 가격으로 그룹화- 이유: 동일 운임을 한 번에 처리
- 예: “ADULT10000”, “CHILD8000”
-
Chunked 처리: 4명씩 분할
- 이유: Galileo API 제한 (한 번에 최대 4명 발권)
- 예: [P1, P2, P3, P4], [P5, P6]
-
타임아웃 콜백: Slack 알림
- 발권 타임아웃 시 자동 알림
- 운영팀 즉시 대응 가능
SOAP API: GalileoClient.kt:368-413
- 엔드포인트:
/AirService(AirTicketingRQ) - 파라미터:
reservationPnr: Sub PNR (Reservation PNR)passengerPrices: 승객별 가격 정보cardInfo: 카드 정보payment: 결제 승인 정보pricingReference: Pricing 참조 (발권 대상)
Step 5: 티켓 조회 및 검증
위치: GalileoTicketingService.kt:160-180
val issuedBooking = galileoClient.getBooking(providerPnr = pnr, findTimeZoneId = findTimeZoneId)
.also {
if (it.tickets.isNullOrEmpty()) throw InternationalAdapterException(
ErrorMessage.TICKETING_FAILED,
pnr
).capture()
}
val passengerTicketMap = galileoClient.getTicketDocuments(
providerPnr = booking.pnr,
universalRecordPnr = booking.supplierIdentificationKey!!,
reservationPnr = booking.subPnr!!,
passengerPrices = passengerPrices
).groupBy { it.identificationKey }
return issuedBooking
.withPassengers(
passengers = issuedBooking.passengers!!.map {
it.withPassengerTickets(ticketDocuments = passengerTicketMap[it.identificationKey])
}
)
.withPayment(payment = payment)티켓 조회 플로우:
- 발권 완료 예약 조회:
getBooking - 티켓 존재 검증:
tickets.isNullOrEmpty()체크 - 티켓 문서 조회:
getTicketDocuments(AirRetrieveDocumentRQ) - 승객별 매핑:
identificationKey로 그룹화 - 최종 결합: 예약 + 티켓 + 결제 정보
4. 에러 처리 및 복구
4.1 발권 실패 시 비동기 취소
위치: GalileoTicketingService.kt:181-190
try {
// 발권 로직
} catch (e: Exception) {
cancelAsync(
pnr = pnr,
validatingCarrier = validatingCarrier,
payment = payment,
keepPnr = keepPnr
)
throw e
}4.2 취소 로직 상세
위치: GalileoTicketingService.kt:231-260
private fun cancelAsync(
pnr: String,
validatingCarrier: String,
payment: Payment,
keepPnr: Boolean,
) {
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000)
val booking = galileoClient.getBooking(providerPnr = pnr)
booking.tickets?.run {
cancelService.voidRepeat(
providerPnr = pnr,
reservationPnr = booking.subPnr!!,
ticketNumbers = this.map { it.ticketNumber }.distinct()
)
}
cancelService.paymentCancelAsync(
pnr = pnr,
validatingCarrier = validatingCarrier,
reservationCode = booking.supplierIdentificationKey!!,
payment = payment
)
if (keepPnr.not()) {
cancelService.pnrCancelAsync(pnr)
}
}
}취소 순서:
- 5초 대기: GDS 시스템 안정화
- 예약 조회: 최신 상태 확인
- 티켓 Void: 발권된 티켓 취소 (
voidRepeat) - 결제 취소: KPS 결제 취소 (
paymentCancelAsync) - PNR 취소: keepPnr=false인 경우만 (
pnrCancelAsync)
4.3 Void 재시도 로직
위치: GalileoCancelService.kt:166-201
fun voidRepeat(
providerPnr: String,
reservationPnr: String,
ticketNumbers: List<String>,
) {
val voidedTicketNumbers = mutableSetOf<String>()
for (count in 1..3) {
val aliveTicketNumbers = ticketNumbers - voidedTicketNumbers
try {
withBlocking {
delay(1000)
galileoClient.void(
providerPnr = providerPnr,
reservationPnr = reservationPnr,
ticketNumbers = aliveTicketNumbers
).apply {
voidedTicketNumbers.addAll(this)
}
}
break
} catch (e: Exception) {
logger.info(e.message, e)
if (count >= 3) {
slackService.sendVoidFail(
supplier = Supplier.GALILEO,
pnr = providerPnr,
targetTickets = ticketNumbers,
failTickets = aliveTicketNumbers,
reason = e.message
)
throw e
}
}
}
}재시도 전략:
- 최대 3회 시도
- 1초 대기 (각 시도 간)
- 부분 성공 허용 (Void된 티켓 제외하고 재시도)
- 실패 시: Slack 알림 + 예외 발생
5. 항공사별 특별 처리
5.1 Commission 처리
위치: GalileoTicketingService.kt:192-208
Commission 타입:
enum class CommissionType {
GROSS, // 총액 기준
NET // 순액 기준
}Commission 계산 로직:
- Pricing Commission: Pricing API에서 반환된 Commission
- Over Commission: 요청에서 지정한 추가 Commission
- 선택 기준: 둘 중 큰 값 사용
활용 예:
- 프로모션 커미션 적용
- 특별 할인 처리
- 대리점 수수료 조정
5.2 KPS Payment 통합
위치: GalileoTicketingService.kt:108-117
KPS 연동 특징:
- BSP 카드 결제: 항공사 공인 결제 시스템
- Real-time 승인: 즉시 승인/거절
- 취소 지원: 결제 취소 API 제공
- 다국적 카드: 해외 발급 카드 지원
에러 처리:
kpsClient.approve(...).also {
galileoClient.addPaymentInfoRemark(booking = this, payment = it)
}- KPS 승인 후 즉시 PNR에 Remark 추가
- 승인 성공 증적 보존
- 취소 시 승인번호 활용
6. 성능 최적화
6.1 승객 그룹화 전략
위치: GalileoTicketingService.kt:138-158
그룹화 키: "${type}${cashPrice}"
- 목적: 동일 운임 승객 묶음 처리
- 효과: API 호출 횟수 감소
예시:
승객 목록:
- P1: ADULT, 10000원
- P2: ADULT, 10000원
- P3: ADULT, 10000원
- P4: CHILD, 8000원
- P5: ADULT, 10000원
그룹화 결과:
- "ADULT10000": [P1, P2, P3, P5] → chunk → [[P1,P2,P3,P5]]
- "CHILD8000": [P4] → chunk → [[P4]]
API 호출: 2회 (5명을 2번에 발권)
6.2 Chunked 처리
이유: Galileo API 제한 (한 번에 최대 4명)
장점:
- 큰 그룹 자동 분할
- 에러 격리 (일부 실패 시 전체 롤백 방지)
- 병렬 처리 가능성
6.3 비동기 작업
비동기 처리 대상:
- Payment Remark 추가 (로깅 성격)
- 발권 실패 취소 (5초 지연)
- PNR 취소 (최종 정리)
성능 향상:
- API 응답 시간 단축
- 사용자 대기 시간 감소
7. Amadeus/Sabre와의 비교
7.1 발권 플로우 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| Ready API | 있음 (Repricing) | 없음 | 있음 (Repricing) |
| Session 관리 | Stateless | Stateful | Stateful |
| 결제 시스템 | KPS | KPS | Sabre Payment (TossPay/KeyInCard) |
| 발권 API | AirTicketingRQ | DocIssuance_IssueTicket | AirTicketRQ |
| Chunked 처리 | 4명 | 없음 (9명) | 없음 |
| Commission | API 수준 지원 | N/A | API 수준 지원 |
| Payment Remark | 추가 필요 | 추가 필요 | 추가 필요 |
| 티켓 조회 | AirRetrieveDocumentRQ | Ticket_DisplayTST | TravelItineraryReadRQ |
7.2 에러 처리 비교
| 에러 시나리오 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 발권 실패 | Void + Payment 취소 + PNR 취소 | Payment 취소 + PNR 취소 | Void + Payment 취소 + PNR 취소 |
| 타임아웃 | Slack 알림 + 재시도 | 즉시 실패 | Slack 알림 |
| 부분 발권 | Void 재시도 (3회) | N/A | Void 재시도 |
| 비동기 취소 | 5초 지연 | 즉시 | 즉시 |
7.3 독특한 특징
Galileo 고유 특징
-
4명 Chunked 발권:
- API 제한으로 인한 자동 분할
- 대규모 그룹 예약 지원
-
Commission API:
- Pricing 수준의 Commission 설정
- 프로모션/대리점 수수료 유연 처리
-
Payment Remark:
- 결제 정보를 PNR에 명시적 저장
- 운영 추적성 향상
-
Void 부분 성공:
- Void 실패 시 성공한 티켓 제외하고 재시도
- 안정성 향상
-
Element 기반 수정:
- PricingInfo를 Element로 관리
- 삭제/추가 시 버전 동기화 필요
8. 주요 발견사항
8.1 Galileo 발권 시스템의 특징
- KPS 통합: 한국 항공사 표준 결제 시스템
- Element 기반 관리: PricingInfo, Remark 등 독립 관리
- API Version 동기화: 수정 후 재조회 필수
- Chunked 발권: 4명 제한으로 자동 분할
- Commission 지원: API 수준의 수수료 설정
8.2 개선 가능 영역
1. Chunked 크기 설정화
현황: 하드코딩된 4명
groupedPassengerPrices.chunked(4)제안: 설정 파일로 관리
galileo:
ticketing:
chunk-size: 4
max-concurrent-chunks: 22. Commission 로깅
현황: Commission 변경 로그 없음
제안:
convertCommission(...)?.let {
logger.info("[COMMISSION_OVERRIDE] PNR: $pnr, " +
"PassengerType: ${pricingInfoReference.type}, " +
"Original: ${passenger?.commission}, " +
"Override: $it")
pricingInfoReference.type to it
}3. 타임아웃 재시도 전략
현황: 타임아웃 시 즉시 실패
제안: 자동 재시도 (1회)
timeoutCallback = {
if (retryCount < 1) {
logger.warn("Ticketing timeout, retrying...")
retryTicketing(...)
} else {
slackService.sendTicketingTimeout(...)
}
}9. 참고 자료
9.1 주요 클래스
GalileoTicketingService.kt: 발권 서비스 (26-261)GalileoClient.kt: SOAP 클라이언트 (368-413)KpsPaymentClient.kt: KPS 결제 클라이언트GalileoCancelService.kt: 취소 서비스 (166-201)AirTicketingRQ.kt: 발권 요청 모델AirTicketingRS.kt: 발권 응답 모델
9.2 주요 메소드
GalileoTicketingService.ready(): 발권 준비 (45-89)GalileoTicketingService.issue(): 발권 실행 (91-190)GalileoTicketingService.validateFareBasisChange(): FareBasis 검증 (210-229)GalileoTicketingService.convertCommission(): Commission 변환 (192-208)GalileoTicketingService.cancelAsync(): 비동기 취소 (231-260)GalileoClient.ticketing(): SOAP 발권 API (368-413)GalileoClient.saveCommission(): Commission 저장 (763-780)
9.3 관련 API
- AirPriceRQ: 운임 재계산 (Ready 단계)
- UniversalRecordModifyRQ: PricingInfo 삭제/추가, Payment Remark, Commission
- AirTicketingRQ: 발권 실행
- AirRetrieveDocumentRQ: 티켓 문서 조회
- AirVoidDocumentRQ: 티켓 취소 (Void)
- KpsBspCardAuthRQ: KPS 결제 승인
- KpsBspCardAuthXRQ: KPS 결제 취소
이 문서는 Triple Air International Adapter 프로젝트의 Galileo 발권 API 심층 분석 문서입니다.