Sabre GDS API 분석 문서
1. 개요
1.1 Sabre GDS 시스템
- 공급사 타입: Global Distribution System (GDS)
- 통신 방식: REST API + SOAP API 혼합 아키텍처
- 인증 방식:
- REST: OAuth 2.0 Client Credentials (Token 기반)
- SOAP: Session Token 기반 인증
- 트랜잭션 관리: Stateful Session Management (SOAP), Stateless (REST)
- 주요 특징: PNR 기반 예약 관리, 하이브리드 API 구조
1.2 아키텍처 특징
Controller Layer → Service Layer → Client Layer → Sabre API
↓ ↓ ↓
Request DTO Business Logic REST/SOAP Client
↓ ↓ ↓
Response DTO Domain Model Session Management
API 타입별 분류
- REST API: 검색(BargainFinderMax), 운임 규칙, 환불 처리, Token 발급
- SOAP API: 예약, 발권, Void, 큐 관리, Session 관리
1.3 인증 메커니즘
1.3.1 REST Token 인증
// SabreRestClient.kt:67-96
fun getToken(): String {
val secret = with(sabreApiProperties) {
val clientIdRaw = "V1:$epr:${pcc.online}:AA".encodeUtf8().base64Url()
val passwordRaw = password.encodeUtf8().base64Url()
"$clientIdRaw:$passwordRaw".encodeUtf8().base64Url()
}
// POST /v2/auth/token (OAuth 2.0 Client Credentials)
// Redis 캐싱: 토큰 만료 전까지 재사용
}1.3.2 SOAP Session Token 관리
// SabreClient.kt:293-340
fun getSessionToken(channel, funnel, targetDate): String {
// SessionCreateRQ - SOAP Session 생성
// 반환: SessionToken (모든 SOAP API 호출 시 필수)
}
fun closeSessionToken(token, channel, funnel, targetDate) {
// SessionCloseRQ - SOAP Session 종료 (리소스 해제)
}2. API 엔드포인트별 상세 분석
2.1 항공편 검색 API (Flight Search)
2.1.1 검색 API
엔드포인트: POST /internals/SABRE/search
API 타입: REST API (BargainFinderMax)
기능 설명
- Sabre GDS를 통한 항공편 검색
- REST API 기반 병렬 검색 처리
- Redis 캐싱 및 Token 재사용
요청 파라미터
data class SearchRequest(
val originDestinationLocationInfos: List<OriginDestinationLocationInfo>,
val cabins: List<CabinType>,
val preferences: List<SearchPreference>,
val airlines: List<String>?,
val sotoAirlines: List<String>?,
val onlyDirect: Boolean,
val onlyFreeBaggageInclude: Boolean,
val advancedOption: AdvancedOption?,
val useCache: Boolean = true,
val useMultiTicket: Boolean = false,
val logging: Boolean = false
)비즈니스 로직 단계
- 캐시 확인: Redis에서 기존 검색 결과 확인
- Token 발급: OAuth 2.0 Client Credentials (Redis 캐싱)
- 공항 확장: Cartesian Product로 모든 조합 생성
- 병렬 검색: pmap으로 병렬 처리
- REST 요청: BargainFinderMaxRQ 호출 (30초 타임아웃)
- 결과 필터링:
- NonAir 제외 (isNonAir)
- NonTicketableCarrier 제외 (hasNonTicketableCarrier)
- 항공사 필터링 (airlines, sotoAirlines)
- 캐싱 저장: Redis에 결과 저장 (TTL 적용)
에러 처리
- Circuit Breaker 없음 (REST API 특성)
- Fallback: 예외 발생 시 빈 리스트 반환 (onFailure)
- Retry: pmap 레벨에서 실패 시 다른 요청 계속 진행
2.1.2 Revalidate API
API 타입: SOAP API
기능 설명
- 운임 재검증 (가격 변동 확인)
- FareRule 조회 전 필수 단계
처리 로직
- Session Token 생성 → Revalidate → Session 종료
2.2 예약 API (Booking)
2.2.1 예약 생성
엔드포인트: POST /internals/SABRE/bookings
API 타입: SOAP API
비즈니스 로직 단계
// SabreBookingService.kt:34-134
val token = sabreClient.getSessionToken()
try {
// 1. markSeat: 좌석 마킹 + PriceQuote 생성
val priceQuoteMap = sabreClient.markSeat(token, fareItinerary, passengers)
// 2. savePassengerInfo: 승객 정보 저장 (SSR, OSI, Contact 포함)
val pnr = sabreClient.savePassengerInfo(
token, validatingCarrier, reservationUser, passengers,
priceQuoteMap, departureDate, accountCode
)
// 3. getBooking: 예약 조회 및 검증
val booking = sabreClient.getBooking(token, pnr)
// 검증 1: 유아 SSR 상태 체크 (KK/HK/NN)
// 검증 2: 스케줄 상태 (confirmed/confirming)
// 검증 3: CarrierTimeLimit null 처리 (3초 대기 후 재조회)
// 검증 4: Cabin null 처리 (재조회)
// 검증 5: 승객 수 불일치 (재조회)
} catch (e: Exception) {
// SOLD_OUT, INFANT_SOLD_OUT 발생 시 비동기 PNR 취소
} finally {
sabreClient.closeSessionToken(token)
}주요 처리 사항
- 좌석 마킹: 재고 확보 (MarkSeat)
- 승객 정보: 이름, 생년월일, 여권, 연락처
- SSR 추가:
- DOCS (여권 정보)
- CTCE (이메일)
- CTCM (전화번호)
- INFT (유아 정보)
- PNR 생성: 6자리 영숫자 코드
- CarrierTimeLimit null 특별 처리:
// SabreBookingService.kt:101-111 when { booking.carrierTimeLimit == null -> { // 3초 대기 후 재조회 delay(3000) retrieveOrNull(booking.pnr!!) ?: booking } }
2.2.2 예약 조회
엔드포인트: GET /internals/SABRE/bookings/{pnr}
처리 로직
- Session Token 생성
- GetBooking 호출 (SOAP)
- 예약 정보 파싱
- Session Token 종료
2.2.3 예약 확인 (Confirm)
엔드포인트: POST /internals/SABRE/bookings/{pnr}/confirm
검증 사항
- 스케줄 상태가 confirmed인지 체크
- 미확인 시 PNR 취소 및 예외 발생
2.2.4 Repricing
엔드포인트: POST /internals/SABRE/bookings/{pnr}/repricing
처리 로직
// SabreBookingService.kt:273-290
if (booking.priceQuoteCreatedAt.toLocalDate() != today()) {
// PriceQuote가 당일이 아니면 재생성
sabreClient.deletePriceQuote(token)
sabreClient.repricing(token, booking.passengers)
sabreClient.endTransaction(token)
// FareBasis 변경 검증 (변경 시 PNR 취소)
validateFareBasisChange(originBooking, newBooking)
}2.2.5 PNR 분할 (Divide)
엔드포인트: POST /internals/SABRE/bookings/{pnr}/divide
처리 로직
- 특정 승객을 새 PNR로 분리
- SplitPnr 명령 사용
- 새 PNR 반환
2.3 발권 API (Ticketing)
2.3.1 발권 준비
엔드포인트: POST /internals/SABRE/ticketing/ready
검증 사항
- Carrier Time Limit 확인 (2분 이상 남아있어야 함)
- Carrier PNR 존재 여부
- 스케줄 상태 (confirmed)
- Repricing 필요 여부 (priceQuoteCreatedAt 당일 체크)
2.3.2 발권 실행
엔드포인트: POST /internals/SABRE/ticketing
API 타입: SOAP API
발권 프로세스
// SabreTicketingService.kt:52-134
val token = sabreClient.getSessionToken()
var payment: Payment? = null
try {
// 1. 예약 조회 및 검증
val passengers = sabreClient.getBooking(token, pnr)
.validateBookingConditionForTicketing()
.passengers
// 2. 결제 승인 (TossPay 또는 KeyInCard)
payment = paymentService.approve(pnr, validatingCarrier, passengerPrices, paymentInfo)
// 3. 발권 처리
val passengerTickets = sabreClient.ticketing(
token, pnr, validatingCarrier, passengers, passengerPrices, payment, paymentInfo,
addEndorsement = "RFND ONLY TO ISSUE AGT".takeIf { validatingCarrier == "PR" },
timeoutCallback = { slackService.sendTicketingTimeout(Supplier.SABRE, pnr) }
)
// 4. Uncommitted 티켓 검증
val uncommittedTickets = passengerTickets.filterNot { it.committed }
if (uncommittedTickets.isNotEmpty()) {
throw StatusInvalidException(ErrorMessage.TICKETING_FAILED, pnr, "uncommitted tickets")
}
// 5. 티켓 정보 조회
return retrieveTicket(token, pnr, passengerTickets, validatingCarrier)
.withPayment(payment)
} catch (e: Exception) {
// 발권 실패 시 5초 대기 후 비동기 취소
if (payment != null) {
cancelAsync(pnr, validatingCarrier, payment, keepPnr, ticketCount)
}
throw e
} finally {
sabreClient.closeSessionToken(token)
}주요 API 호출
AirTicketLLSRQ: 티켓 발행EnhancedAirTicket: 티켓 정보 조회
항공사별 특별 처리
- PR 항공사: “RFND ONLY TO ISSUE AGT” endorsement 추가 (SabreTicketingService.kt:83)
2.4 운임 규정 API (Fare Rules)
엔드포인트: GET /internals/SABRE/fare-rules
API 타입: REST API + SOAP API 조합
기능 설명
- 운임 규정 및 제한 사항 조회
- 환불/변경 수수료 정보
처리 로직
// SabreFareRuleService.kt:24-56
fun findFareRules(key, adult, child, infant): List<FareRule> {
// 1. Redis 캐시 확인
val savedFareRules = fareRuleRepository.findFareRules(fareRuleKey)
if (savedFareRules != null) return savedFareRules
// 2. FareItinerary 조회
val fareItinerary = sabreFlightSearchService.getFareItinerary(key)
// 3. Revalidate (SOAP API)
val revalidateFareItinerary = sabreFlightSearchService.revalidate(
fareItinerary, adult, child, infant
)
// 4. FareBasis 및 Price 비교 검증
compareFarebasis(fareItinerary, revalidateFareItinerary)
comparePrice(fareItinerary, revalidateFareItinerary)
// 5. FareRule 조회 (REST API)
return sabreFareRuleClient.findFareRules(
adult, child, infant, revalidateFareItinerary ?: fareItinerary, airportMap
).also {
// 6. Redis 캐싱
fareRuleRepository.saveFareRules(fareRuleKey, it)
}
}2.5 취소 API (Cancel)
엔드포인트: PUT /internals/SABRE/bookings/{pnr}/cancel
API 타입: SOAP (Void) + REST (Refund) 조합
취소 유형 판별
// SabreCancelService.kt:38-102
fun cancel(pnr, validatingCarrier, payment, autoRefundable, waivers): List<Refund> {
val token = sabreClient.getSessionToken()
try {
val booking = sabreClient.getBooking(token, pnr)
// 티켓 히스토리 없음 → PNR만 취소
if (booking.ticketHistories.isNullOrEmpty()) {
handleEmptyTicketHistories(booking, validatingCarrier, payment, token)
return emptyList()
}
return when {
// Case 1: Void 가능
booking.isVoidable -> {
voidTickets(token, booking, validatingCarrier, payment)
emptyList()
}
// Case 2: Waiver 또는 자동 환불
waiverRefundable || autoRefundable -> {
validateRefundableTickets(booking, waiverRefundable)
// Waiver 저장 (OSI/SSR/REMARK)
if (waiverRefundable) saveWaiverRefunds(waivers, token, booking)
// CancellableTicket 조회
val cancellableTickets = getCancellableTickets(token, pnr)
if (!cancellableTickets.isRefundable) {
throw InternationalAdapterException(ErrorMessage.CANCEL_UNABLE, pnr)
}
// 환불 처리 (REST API)
refundTickets(token, booking, cancellableTickets, waivers)
}
// Case 3: 취소 불가
else -> throw InternationalAdapterException(ErrorMessage.CANCEL_UNABLE, pnr)
}
} finally {
pnrCancelAsync(booking.pnr!!) // 5초 대기 후 PNR 취소
sabreClient.closeSessionToken(token)
}
}isVoidable 판별 로직
// Booking.kt:23-25
val isVoidable: Boolean
get() = NonVoidableAirline.notContains(validatingCarrier)
// NonVoidableAirline.kt:3-8
enum class NonVoidableAirline {
HY, // 우즈베키스탄 항공
MF, // 샤먼 항공
SU, // 아에로플로트
QH, // 밤부 항공
}Void 프로세스
// SabreCancelService.kt:297-332, 430-478
private fun voidTickets(token, booking, validatingCarrier, payment, cancellableTickets) {
val voidableTickets = booking.ticketHistories!!
.filter { it.isVoidable }
.map { it.ticketNumber to it.rph!! }
// 부분 Void 알림
if (booking.passengers.size != voidableTickets.size) {
slackService.sendIncompleteVoid(Supplier.SABRE, pnr, passengerCount, ticketCount)
}
// 순차 Void (3회 재시도)
voidAll(tickets = voidableTickets, token = token, pnr = booking.pnr!!)
// Ignore 처리
sabreClient.ignore(token)
// 비동기 결제 취소
if (validatingCarrier != null && payment != null) {
paymentService.cancelAsync(pnr, validatingCarrier, payment)
}
}
private fun voidRepeat(token, pnr, rph) {
for (count in 1..3) {
try {
void(token, pnr, rph)
break
} catch (e: Exception) {
if (count < 3 && (
e.message?.contains("RETRY") == true ||
e.message?.contains("NO TCN/AT NBR MATCH") == true ||
e.message?.contains("PROCESSING ERROR DETECTED") == true
)) {
sabreClient.ignore(token)
} else {
throw e
}
}
}
}
private fun void(token, pnr, rph) {
sabreClient.openPnr(token, pnr)
sabreClient.void(token, rph)
sabreClient.void(token, rph) // 2번 호출 (Void 확정)
delay(1000)
}Waiver 처리
// SabreCancelService.kt:516-540
private fun saveWaiverRefunds(waivers, token, booking) {
// AUTH_CODE 제외
val filteredWaivers = waivers.filter { it.type != WaiverType.AUTH_CODE }
// OSI/SSR 타입 처리
val specialServiceWaivers = filteredWaivers.filter {
it.type == WaiverType.OSI || it.type == WaiverType.SSR
}
if (specialServiceWaivers.isNotEmpty()) {
sabreClient.saveWaiverRefunds(token, specialServiceWaivers, booking.validatingCarrier)
}
// REMARK 타입 처리
val remarkWaivers = filteredWaivers.filter { it.type == WaiverType.REMARK }
if (remarkWaivers.isNotEmpty()) {
remarkWaivers.forEach { waiver ->
sabreClient.saveRemark(token, waiver.text)
}
}
sabreClient.endTransaction(token)
}PNR 취소
// SabreCancelService.kt:382-428
private fun pnrCancelAsync(pnr: String) {
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000) // Locked PNR 방지
val token = sabreClient.getSessionToken()
try {
pnrCancelRepeat(pnr, token)
} finally {
sabreClient.closeSessionToken(token)
}
}
}
private fun pnrCancelRepeat(pnr, token) {
for (count in 1..2) { // 2회 재시도
try {
pnrCancel(token, pnr)
break
} catch (e: Exception) {
if (e is ApiException && e.errorMessage == ErrorMessage.ALREADY_CANCELED_PNR) {
logger.info(e.message, e)
break
}
if (count == 2) {
slackService.sendCancelFail(Supplier.SABRE, pnr, reason = e.message)
throw e
}
sabreClient.ignore(token)
}
}
}2.6 환불 API (Refund)
엔드포인트: N/A (Cancel API 내부에서 호출)
API 타입: REST API
환불 프로세스
// SabreCancelService.kt:334-380
private fun refundTickets(token, booking, cancellableTicket, waivers): List<Refund> {
val (isAllRefunded, refundedTickets) = try {
// REST API: RefundTickets
sabreRestClient.refundTickets(token, booking, cancellableTicket, waivers)
} catch (e: Exception) {
// Socket Timeout 특별 처리
when (e) {
is SocketTimeoutException, is SocketException, is IOException ->
slackService.sendCancelFailTimeout(Supplier.SABRE, booking.pnr!!)
else -> slackService.sendRefundFail(Supplier.SABRE, booking.pnr!!)
}
throw InternationalAdapterException(ErrorMessage.REFUND_FAILED, e.message!!)
}
// 부분 환불 실패 처리
if (isAllRefunded.not()) {
val targetTickets = booking.passengers.map { it.eTicket!!.ticketNumber }
val failTickets = targetTickets - refundedTickets.map { it.ticketNumber }.toSet()
slackService.sendAllTicketRefundFail(Supplier.SABRE, booking.pnr!!, targetTickets, failTickets)
throw InternationalAdapterException(ErrorMessage.REFUND_FAILED, "All ticket is not refunded")
}
return refundedTickets
}CancellableTicket 조회
// SabreCancelService.kt:502-514
private fun getCancellableTickets(token, pnr): CancellableTicket {
return try {
// REST API: CheckTickets
sabreRestClient.checkCancellableTickets(token, pnr)
} catch (e: Exception) {
slackService.sendCheckFlightTicketsFail(Supplier.SABRE, pnr, e.message)
throw e
}
}환불 검증
// SabreCancelService.kt:489-500
private fun validateRefundableTickets(booking, waiverRefundable) {
// EMD 티켓 존재 시 환불 불가
if (booking.passengers.any { it.emdTicket != null }) {
throw InternationalAdapterException(ErrorMessage.CANCEL_UNABLE, booking.pnr!!, "EMD Ticket Exists")
}
// Waiver 아닐 때 스케줄 상태 확인
if (!waiverRefundable && booking.schedules?.all { it.confirmed } == false) {
throw InternationalAdapterException(
ErrorMessage.CANCEL_UNABLE_BY_SCHEDULE_STATUS,
booking.pnr!!,
booking.schedules.joinToString { it.status }
)
}
}2.7 큐 관리 API (Queue Management)
2.7.1 큐 목록 조회
엔드포인트: GET /internals/SABRE/queues
API 타입: SOAP API
처리 로직
// SabreQueueService.kt:32-83
fun getQueuePnrs(): List<QueuePnrInfo> {
// TARGET_ORIGIN_QUEUE_NUMBERS = [5, 6, 7, 20]
// LEGACY_PCC = [3OGJ, 7CZJ] (제외)
return sabreProperties.channels
.flatMap { channel -> channel.funnels.flatMap { listOf(it.pcc.online, it.pcc.offline) } }
.filter { it !in LEGACY_PCC }
.distinct()
.flatMap { pcc ->
val token = sabreClient.getSessionToken(channel, funnel, targetDate)
try {
// 병렬 처리 (pmap)
TARGET_ORIGIN_QUEUE_NUMBERS.pmap { originQueueNumber ->
val queueCount = sabreClient.getPnrCountInQueue(token, originQueueNumber, pcc)
if (queueCount > 0) {
sabreClient.getPnrsInQueueAction(
token, originQueueNumber, listOfRecordLocator = true, pcc
).map { pnr ->
QueuePnrInfo(pnr, originQueueNumber, pcc)
}
} else {
emptyList()
}
}.getOrEmpty()
} catch (e: Exception) {
slackService.sendQueueFail(supplier = "${Supplier.SABRE.name}($pcc)", e.message)
emptyList()
} finally {
sabreClient.closeSessionToken(token, channel, funnel, targetDate)
}.flatten()
}
}2.7.2 큐 제거
엔드포인트: PUT /internals/SABRE/queues
API 타입: SOAP API
처리 로직
// SabreQueueService.kt:85-138
@Retryable(maxAttempts = 3, backoff = Backoff(delay = 5000))
fun remove(queueNumber, pcc, pnrs): Boolean {
val token = sabreClient.getSessionToken(channel, funnel, targetDate)
val removePnrs = mutableListOf<String>()
return try {
val queueCount = sabreClient.getPnrCountInQueue(token, queueNumber, pcc)
// 큐 작업 모드 첫 진입 시 첫 번째 PNR 응답
var currentPnr = sabreClient.getPnrsInQueueAction(
token, pcc, queueNumber
).firstOrNull()
// 순차 처리
repeat(queueCount) {
// QR: 삭제, I: 건너뛰기
val nextPnr = sabreClient.getPnrsInQueueAction(
token, pcc,
actionType = if (currentPnr in pnrs) QueueAccessActionType.QR else QueueAccessActionType.I
).firstOrNull()
if (currentPnr in pnrs) removePnrs.add(currentPnr!!)
currentPnr = nextPnr
}
true
} catch (e: Exception) {
slackService.sendQueueFail(supplier = "${Supplier.SABRE.name}($pcc)", e.message)
false
} finally {
sabreClient.closeSessionToken(token, channel, funnel, targetDate)
}
}3. 공통 컴포넌트
3.1 Session Token 관리
REST Token vs SOAP Session Token
// REST Token (무상태)
val restToken = sabreRestClient.getToken() // Redis 캐싱
// - OAuth 2.0 Client Credentials
// - 만료 시까지 재사용
// - 모든 REST API에 Authorization Header로 전달
// SOAP Session Token (상태 유지)
val sessionToken = sabreClient.getSessionToken(channel, funnel, targetDate)
try {
// 모든 SOAP API 호출 시 sessionToken 전달
sabreClient.markSeat(sessionToken, ...)
sabreClient.savePassengerInfo(sessionToken, ...)
} finally {
sabreClient.closeSessionToken(sessionToken, channel, funnel, targetDate)
}3.2 Redis 캐싱 전략
캐시 키 구조
SABRE_{type}_{key}_{timestamp}
TTL 설정
- REST Token: 만료 시까지 (sabreAccessToken:{pcc})
- 검색 결과: 설정된 TTL (flightSearchKey, fareItinerary)
- 운임 규정: 설정된 TTL (fareRule)
3.3 재시도 메커니즘
Void 재시도 (3회)
// SabreCancelService.kt:459-478
for (count in 1..3) {
try {
void(token, pnr, rph)
break
} catch (e: Exception) {
if (count < 3 && isRetryableError(e)) {
sabreClient.ignore(token)
} else {
throw e
}
}
}PNR 취소 재시도 (2회)
// SabreCancelService.kt:394-418
for (count in 1..2) {
try {
pnrCancel(token, pnr)
break
} catch (e: Exception) {
if (count == 2) {
slackService.sendCancelFail(pnr)
throw e
}
sabreClient.ignore(token)
}
}큐 제거 재시도 (3회)
// SabreQueueService.kt:85
@Retryable(maxAttempts = 3, backoff = Backoff(delay = 5000))
fun remove(queueNumber, pcc, pnrs): Boolean { ... }3.4 비동기 처리 패턴
5초 대기 후 비동기 처리
// 1. PNR 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000) // Locked PNR 방지
cancelService.onlyPnrCancel(pnr)
}
// 2. 발권 실패 시 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
delay(5000)
cancelService.ticketingFailedCancel(pnr, validatingCarrier, payment, keepPnr, ticketCount)
}
// 3. 결제 취소 비동기
CoroutineScope(Dispatchers.IO).withLaunch {
paymentService.cancelAsync(pnr, validatingCarrier, payment)
}3.5 에러 처리 패턴
에러 타입별 처리
StatusInvalidException: 예약/발권 상태 불일치InternationalAdapterException: 비즈니스 로직 에러SocketTimeoutException: 환불 타임아웃 (Slack 알림)ApiException: API 호출 실패 (특정 에러 메시지 처리)
Slack 알림 케이스
- 발권 타임아웃 (SabreTicketingService.kt:85-89)
- 부분 발권 실패 (SabreTicketingService.kt:184-193)
- Incomplete Void (SabreCancelService.kt:309-314)
- 환불 실패 (SabreCancelService.kt:350-352, 366-371)
- PNR 취소 실패 (SabreCancelService.kt:406-410)
- 큐 조회/제거 실패 (SabreQueueService.kt:72, 128)
4. Sabre 특징
4.1 하이브리드 API 아키텍처
REST API vs SOAP API 분류
| 작업 | API 타입 | 이유 |
|---|---|---|
| 검색 (BargainFinderMax) | REST | 무상태, 병렬 처리 최적화 |
| 운임 규칙 조회 | REST | 무상태, 빠른 응답 |
| 환불 처리 | REST | 무상태, 비동기 처리 가능 |
| Token 발급 | REST | OAuth 2.0 표준 |
| 예약 | SOAP | 상태 유지 필요 (Session) |
| 발권 | SOAP | 트랜잭션 보장 필요 |
| Void | SOAP | 순차 처리 필요 |
| 큐 관리 | SOAP | 상태 유지 필요 |
4.2 항공사별 특별 처리
4.2.1 PR 항공사 (필리핀 항공)
// SabreTicketingService.kt:83
addEndorsement = "RFND ONLY TO ISSUE AGT".takeIf { validatingCarrier == "PR" }4.2.2 NonVoidableAirline (Void 불가능 항공사)
// NonVoidableAirline.kt:3-8
enum class NonVoidableAirline {
HY, // 우즈베키스탄 항공
MF, // 샤먼 항공
SU, // 아에로플로트
QH, // 밤부 항공
}
// Booking.kt:23-25
val isVoidable: Boolean
get() = NonVoidableAirline.notContains(validatingCarrier)4.2.3 OSI 필수 항공사
// Constants.kt:5
const val OSI_CARRIERS = "MF,CZ,MU,NX,TG,ZE"4.2.4 FOID Passport 필수 항공사
// Constants.kt:6
const val FOID_PASSPORT_CARRIERS = "MU,CZ,CA,MF,KC,C6"4.2.5 항공사별 카드 코드 특별 처리
// CardCode.kt:32-53
private val airlineCardBrandsMap = mapOf(
// CORPORATE 코드 사용 항공사 + 카드 브랜드 조합
// 예: OZ + 삼성카드 → CORPORATE
)
fun of(cardNumber, cardBrand, validatingCarrier): CardCode {
return when {
airlineCardBrandsMap[validatingCarrier]?.contains(cardBrand) == true ->
CORPORATE
else -> fromCardBrand(cardBrand)
}
}4.3 PNR 시스템
- 구조: 6자리 영숫자 (예: ABC123)
- 정보 포함: 승객, 일정, 요금, SSR, OSI, PriceQuote
- 상태 코드:
- HK: 확정
- KK: 확인 중
- UN: 대기
- NN: 유아 좌석 (SabreBookingService.kt:71-78)
4.4 PriceQuote 시스템
- 용도: 발권 전 운임 정보 저장
- 생성: markSeat 단계에서 자동 생성
- 유효기간: priceQuoteCreatedAt 기준 당일까지
- 재생성: 당일이 아니면 deletePriceQuote → repricing
4.5 특수 요청 (SSR/OSI)
SSR (Special Service Request)
- DOCS: 여권 정보
- CTCE: 이메일
- CTCM: 전화번호
- INFT: 유아 정보
OSI (Other Service Information)
- OSI_CARRIERS 항공사에 필수
- 추가 정보 전달용
5. 주의사항
5.1 성능 고려사항
- SOAP Session Token 리소스 관리 중요 (반드시 closeSessionToken 호출)
- REST API 병렬 처리 적극 활용 (pmap)
- Redis Token 캐싱으로 불필요한 인증 요청 방지
- 검색 결과 캐싱으로 반복 요청 최소화
5.2 에러 처리
- Void 실패 시 재시도 (3회)
- PNR 취소 실패 시 재시도 (2회)
- 큐 제거 실패 시 재시도 (3회, @Retryable)
- Socket Timeout 특별 처리 (환불 시)
- Locked PNR 방지 (5초 대기)
5.3 보안
- Session Token 안전 관리
- REST Token Redis 캐싱 (만료 관리)
- PCI DSS 준수 (카드 정보)
- 민감 정보 로깅 금지
5.4 비동기 처리 주의
- PNR 취소, 결제 취소는 5초 대기 후 비동기 처리
- 발권 실패 시 취소도 5초 대기 후 비동기 처리
- Locked PNR 방지가 목적
6. Amadeus와의 차이점 비교
| 항목 | Amadeus | Sabre |
|---|---|---|
| API 타입 | SOAP only | REST + SOAP 하이브리드 |
| 인증 방식 | Session 기반 (SOAP) | OAuth 2.0 (REST) + Session (SOAP) |
| 검색 API | SOAP (FareMasterPricer) | REST (BargainFinderMax) |
| 운임 규칙 | SOAP (CommandCryptic) | REST (FareRuleClient) |
| 환불 API | SOAP (DocRefund) | REST (RefundTickets) |
| Session 관리 | Start → InSeries → End | getSessionToken → closeSessionToken |
| Token 캐싱 | 없음 | Redis 캐싱 (REST Token) |
| 병렬 처리 | cartesianProduct (SOAP) | pmap (REST) |
| Circuit Breaker | 있음 (Resilience4j) | 없음 |
| Void 재시도 | 3회 | 3회 |
| PNR 취소 재시도 | 2회 | 2회 |
| 비동기 처리 | 5초 대기 | 5초 대기 |
| 항공사 특별 처리 | AB, KE, CZ, MU, PR | PR, HY, MF, SU, QH, OSI_CARRIERS, FOID_PASSPORT_CARRIERS |
| Void 불가 항공사 | HY, MF, SU, QH | HY, MF, SU, QH |
| Endorsement | MU: 생년월일, PR: RFND ONLY | PR: RFND ONLY TO ISSUE AGT |
| TST/PriceQuote | TST (FarePricePnr) | PriceQuote (markSeat) |
7. 트러블슈팅 가이드
7.1 일반적인 문제
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| SESSION EXPIRED | SOAP Session 타임아웃 | 새 Session 생성 |
| PNR LOCKED | 동시 접근 또는 작업 중 | 5초 대기 후 재시도 |
| TOKEN EXPIRED | REST Token 만료 | 새 Token 발급 (자동) |
| VOID FAILED | Void 불가 항공사 | Refund로 전환 |
| UNCOMMITTED TICKETS | 발권 미완료 | 재조회 또는 에러 발생 |
| CARRIER TIME LIMIT | 발권 기한 초과 | Repricing 또는 재예약 |
7.2 디버깅 팁
- SOAP 요청/응답 로깅 활성화 (supplierLoggingProperties)
- REST API 로깅 활성화 (enableSearchLog)
- Session Token 추적
- PNR 히스토리 확인 (getBooking)
- Redis 캐시 확인 (Token, FareItinerary, FareRule)
- Slack 알림 확인 (에러 발생 시)
8. 참고 자료
8.1 주요 클래스
SabreClient: SOAP 통신 클라이언트SabreRestClient: REST 통신 클라이언트SabreFlightSearchService: 검색 서비스SabreBookingService: 예약 서비스SabreTicketingService: 발권 서비스SabreCancelService: 취소 서비스SabreQueueService: 큐 관리 서비스SabrePaymentService: 결제 서비스SabreFareRuleService: 운임 규칙 서비스
8.2 설정 파일
application.yml: 기본 설정application-{env}.yml: 환경별 설정redisson-{env}.yml: Redis 설정SabreProperties.kt: Sabre 공급사 설정
8.3 상수 파일
NonVoidableAirline.kt: Void 불가능 항공사 (HY, MF, SU, QH)Constants.kt: OSI_CARRIERS, FOID_PASSPORT_CARRIERSCardCode.kt: 항공사별 카드 코드 매핑
8.4 관련 문서
- Sabre Web Services Documentation
- Sabre REST API Guide
- Sabre SOAP API Guide
- OAuth 2.0 Specification