Phase 6: Galileo 운임 규정 API 심층 분석
1. API 엔드포인트 개요
1.1 운임 규정 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/GALILEO/fare-rules | POST | SOAP + KRT | 운임 규정 조회 | GalileoFareRuleController.kt:17-30 |
2. 운임 규정 조회 플로우
2.1 전체 처리 플로우
flowchart TD A[운임 규정 요청] --> B[캐시 키 생성<br/>generateFareRuleKey] B --> C{Redis 캐시<br/>확인} C -->|캐시 히트| D[캐시된 결과 반환] C -->|캐시 미스| E[FareItinerary 조회<br/>getFareItinerary] E --> F[공항 정보 수집<br/>airportMap] F --> G[Pricing 실행<br/>galileoClient.pricing] G --> H[성인 운임만 추출<br/>filter ADULT] H --> I[FareInfo 추출<br/>flatMap fareInfos] I --> J[운임 규정 조회<br/>getFareRules] J --> K[그룹 순서별<br/>groupBy groupSequence] K --> L[병렬 KRT 조회<br/>pmap] L --> M[결과 평탄화<br/>flatten] M --> N[정렬<br/>groupSequence, ordered] N --> O[Redis 캐싱<br/>saveFareRules] O --> P[운임 규정 반환] %% 에러 플로우 G --> ERR{예외 발생} J --> ERR ERR --> Q{SOLD_OUT?} Q -->|Yes| R[검색 캐시 제거<br/>unexposed 저장] Q -->|No| S[일반 에러 처리] R --> T[예외 재발생] S --> T style C fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000 style D fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000 style L fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style P fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000 style ERR fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style T fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
2.2 단계별 상세 분석
Step 1: 캐시 키 생성
위치: GalileoFareRuleService.kt:34-49
fun getFareRules(
detailKey: String,
adult: Int,
child: Int,
infant: Int,
): List<FareRule> {
val fareRuleKey = CacheKeyGenerator.generateFareRuleKey(
detailKey = detailKey,
adult = adult,
child = child,
infant = infant
)
fareRuleRepository.findFareRules(fareRuleKey)
?.takeIf { it.isNotEmpty() }
?.run { return this }
// ...
}캐시 키 구조:
FARE_RULE:{detailKey}:{adult}:{child}:{infant}
캐시 우선 조회:
- 캐시 히트: 즉시 반환
- 캐시 미스: 새로운 조회 수행
Step 2: FareItinerary 조회
위치: GalileoFareRuleService.kt:51
val fareItinerary = fareItineraryRepository.getFareItinerary(detailKey)detailKey:
- 검색 시 생성된 FareItinerary 고유 키
- Redis에 저장된 FareItinerary 조회
Step 3: Pricing 실행
위치: GalileoFareRuleService.kt:54-73
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 }
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 }성인 운임만 사용:
- 운임 규정은 성인 기준
- 소아/유아는 동일 규정 적용
Step 4: Galileo 운임 규정 조회
위치: GalileoFareRuleService.kt:78-86
galileoClient.getFareRules(fareItinerary = fareItinerary, pricingFareInfos = fareInfos)
.groupBy { it.groupSequence }
.entries
.pmap { (_, fareRules) ->
krtClient.getFareRules(
fareItinerary = fareItinerary,
fareRules = fareRules
)
}.getOrThrow()SOAP API: GalileoClient.kt:205-258
- 엔드포인트:
/AirService(AirFareRulesRQ) - 파라미터:
pricingFareInfos(FareInfo 리스트) - 반환:
List<FareRuleModel>
3. Galileo 운임 규정 API
3.1 AirFareRulesRQ
위치: GalileoClient.kt:205-258
fun getFareRules(
fareItinerary: FareItinerary,
pricingFareInfos: List<PricingFareInfo>,
): List<FareRuleModel> {
val galileoApiProperties = galileoProperties.getApiProperties()
val request = AirFareRulesRQ.of(
targetBranch = galileoApiProperties.soap.branchCode,
pricingFareInfos = pricingFareInfos
)
val itinerarySummary = "VC:${fareItinerary.validatingCarrier} ${
fareItinerary.schedules.flatMap { it.segments }
.joinToString { "CLS:${it.bookingClass} ${it.departure}-${it.arrival} ${it.departureAt}" }
}"
return "${galileoApiProperties.soap.endpoint}/AirService"
.post(request)
.authenticate(galileoApiProperties.soap.userName, galileoApiProperties.soap.password)
.header(headerMap)
.requestBodyConvert(soapRequestBodyConverter())
.execute<GalileoResponse<AirFareRulesRS>>(soapBodyDeserializerOf(logger, objectMapper))
.fold(
success = { response ->
response.checkError { errorMessages ->
if (response.body?.fareRules?.shouldSkipFareRuleError() == true) {
return@checkError
}
throw InternationalAdapterException(
ErrorMessage.FETCH_FARE_RULES_FAILED,
itinerarySummary,
errorMessages.joinToString { (code, message) -> "$code: $message" }
).capture()
}
if (response.body!!.fareRules.isNullOrEmpty()) {
throw InternationalAdapterException(ErrorMessage.FETCH_FARE_RULES_FAILED, "Fare Rules is empty")
}
response.body.fareRules.flatMapIndexed { groupSequence, fareRule ->
fareRule.fareRuleInfos
.orEmpty()
.mapIndexed { index, fareRuleInfo ->
fareRuleInfo.toFareRule(groupSequence = groupSequence + 1, ordered = index + 1)
}
}
},
failure = {
throw it.handleSoapFaultException(
ErrorMessage.FETCH_FARE_RULES_FAILED,
itinerarySummary,
"Fare Rules Search Error"
)
}
)
}3.2 특별 에러 처리
위치: GalileoClient.kt:260-267
private fun List<FareRule>.shouldSkipFareRuleError(): Boolean {
return this.takeIf { it.size == 2 }
?.last()
?.fareRuleResultMessages
?.any { it.code == 999 && it.value?.contains("RULE NOT FOUND - VERIFY CARRIER") == true } == true
}특별 처리 로직:
- 조건: 왕복 운임 (2개) + 복편 규정 에러
- 에러 코드: 999
- 에러 메시지: “RULE NOT FOUND - VERIFY CARRIER”
- 처리: 에러 무시 (편도 규정만 사용)
- 이유: 일부 항공사는 복편 규정 미제공, Galileo가 모두 배상
예시:
편도 규정: 정상 조회
복편 규정: "RULE NOT FOUND - VERIFY CARRIER" (999)
→ 편도 규정만 노출 (에러 무시)
4. KRT 운임 규정 변환
4.1 KRT란?
KRT (Korean Rule Translation):
- Galileo 운임 규정을 한국어로 번역하는 서비스
- Triple 자체 번역 API
- 영문 → 한글 자동 변환
4.2 KRT API 호출
위치: GalileoFareRuleService.kt:82-86
.pmap { (_, fareRules) ->
krtClient.getFareRules(
fareItinerary = fareItinerary,
fareRules = fareRules
)
}.getOrThrow()병렬 처리:
pmap: 그룹별 병렬 번역getOrThrow: 모든 실패 시 예외, 일부 성공 시 계속
4.3 KRT 클라이언트
위치: KrtClient.kt
fun getFareRules(
fareItinerary: FareItinerary,
fareRules: List<FareRuleModel>,
): List<FareRule> {
// KRT API 호출
// 영문 → 한글 변환
// FareRuleModel → FareRule 변환
}변환 내용:
- 운임 규정 본문 번역
- 카테고리별 규정 정리
- 포맷팅 및 구조화
4.4 결과 정렬
위치: GalileoFareRuleService.kt:87-88
.flatten()
.sortedWith(compareBy({ it.groupSequence }, { it.ordered }))
.also { fareRuleRepository.saveFareRules(fareRuleKey = fareRuleKey, fareItems = it) }정렬 기준:
groupSequence: 그룹 순서 (구간별)ordered: 그룹 내 순서 (카테고리별)
예시:
그룹 1 (ICN-NRT):
- ordered 1: 예약 규정
- ordered 2: 취소 규정
- ordered 3: 환불 규정
그룹 2 (NRT-ICN):
- ordered 1: 예약 규정
- ordered 2: 취소 규정
- ordered 3: 환불 규정
5. 에러 처리
5.1 SOLD_OUT 처리
위치: GalileoFareRuleService.kt:91-97
} catch (e: Exception) {
if (isUnexposedFareItinerary(e)) {
removeFlightSearchKey(detailKey)
saveUnexposedFareItinerary(fareItinerary)
}
throw e
}
private fun isUnexposedFareItinerary(e: Exception): Boolean {
return (e is StatusInvalidException && e.errorMessage == ErrorMessage.SOLD_OUT)
}SOLD_OUT 처리:
- 검색 캐시 제거 (
removeFlightSearchKey) - Unexposed 저장 (
saveUnexposedFareItinerary) - 예외 재발생
목적: 매진된 FareItinerary 검색 결과 제외
5.2 비동기 작업
위치: GalileoFareRuleService.kt:100-110
private fun saveUnexposedFareItinerary(fareItinerary: FareItinerary) {
CoroutineScope(Dispatchers.IO).withLaunch {
unexposedFareItineraryRepository.save(key = fareItinerary.requestKey, value = fareItinerary.id)
}
}
private fun removeFlightSearchKey(key: String) {
CoroutineScope(Dispatchers.IO).withLaunch {
flightSearchKeyRepository.removeKey(key)
}
}비동기 처리:
- Redis 작업 시 API 응답 지연 방지
- 검색 캐시 정리
- Unexposed 저장
6. FareRule 데이터 구조
6.1 FareRule 모델
data class FareRule(
val groupSequence: Int, // 구간 순서 (1, 2, ...)
val ordered: Int, // 카테고리 순서 (1, 2, ...)
val category: String, // 카테고리 (예약, 취소, 환불 등)
val title: String, // 제목
val content: String, // 내용 (한글)
val originContent: String?, // 원문 (영문)
)6.2 그룹화 전략
위치: GalileoFareRuleService.kt:79-81
.groupBy { it.groupSequence }
.entries
.pmap { (_, fareRules) -> ... }그룹화 목적:
- 구간별 병렬 번역
- KRT API 호출 최소화
- 성능 최적화
예시:
그룹 1: [편도 규정 1, 편도 규정 2, 편도 규정 3]
그룹 2: [복편 규정 1, 복편 규정 2, 복편 규정 3]
→ 2번의 병렬 KRT 호출
7. 성능 최적화
7.1 캐싱 전략
캐시 키:
FARE_RULE:{detailKey}:{adult}:{child}:{infant}
캐싱 이유:
- 운임 규정은 변경 빈도 낮음
- Pricing + Galileo + KRT 총 3단계 API
- 응답 시간 대폭 단축
효과:
캐시 미스: Pricing (3s) + Galileo (2s) + KRT (3s) = 8s
캐시 히트: ~50ms (약 160배 향상)
7.2 병렬 처리
위치: GalileoFareRuleService.kt:77-86
withBlocking(Dispatchers.IO) {
galileoClient.getFareRules(...)
.groupBy { it.groupSequence }
.entries
.pmap { (_, fareRules) ->
krtClient.getFareRules(...)
}.getOrThrow()
}병렬 처리 대상:
- KRT API 호출 (그룹별)
- I/O 병렬화 (Coroutines)
효과:
순차 처리: 그룹 2개 * 3초 = 6초
병렬 처리: max(3초) = 3초 (약 2배 향상)
7.3 성인 운임만 조회
위치: GalileoFareRuleService.kt:74
.filter { it.passengerType == PassengerType.ADULT }이유:
- 운임 규정은 성인 기준
- 소아/유아는 동일 규정
- API 호출 및 번역 횟수 감소
8. Amadeus/Sabre와의 비교
8.1 운임 규정 API 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| API 타입 | SOAP + KRT | SOAP | SOAP |
| 엔드포인트 | AirFareRulesRQ | Fare_GetFareRules | GetFareRulesRQ |
| 번역 서비스 | KRT (자체) | 없음 (영문) | 없음 (영문) |
| 병렬 처리 | 있음 (pmap) | 없음 | 없음 |
| 특별 에러 처리 | 있음 (왕복 복편) | 없음 | 없음 |
| 캐싱 | Redis (detailKey 기반) | Redis | Redis |
| 성인 운임만 | 필터링 | 필터링 | 필터링 |
8.2 에러 처리 비교
| 에러 시나리오 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 운임 규정 없음 | FETCH_FARE_RULES_FAILED | FETCH_FARE_RULES_FAILED | FETCH_FARE_RULES_FAILED |
| 복편 규정 에러 | 에러 무시 (특별 처리) | 에러 발생 | 에러 발생 |
| SOLD_OUT | Unexposed 저장 | Unexposed 저장 | Unexposed 저장 |
| KRT 실패 | 일부 성공 허용 | N/A | N/A |
8.3 독특한 특징
Galileo 고유 특징
-
KRT 통합:
- 자체 한글 번역 서비스
- 영문 → 한글 자동 변환
- 사용자 경험 향상
-
복편 규정 에러 처리:
- 왕복 운임 복편 규정 에러 무시
- Galileo 배상 보증
- 편도 규정만 노출
-
그룹별 병렬 번역:
- 구간별 KRT API 병렬 호출
- 성능 최적화
-
Pricing 통합:
- 운임 규정 조회 전 Pricing 필수
- FareInfo 기반 규정 조회
-
부분 실패 허용:
- KRT 일부 실패 시 성공한 그룹 반환
- 가용성 향상
9. 주요 발견사항
9.1 Galileo 운임 규정 시스템의 특징
- KRT 통합: 한글 자동 번역
- 복편 에러 무시: 특별 처리 로직
- 병렬 KRT: 그룹별 병렬 번역
- Pricing 선행: FareInfo 기반 조회
- 성인 운임 기준: 소아/유아 동일 규정
9.2 개선 가능 영역
1. KRT 타임아웃 설정
현황: 기본 타임아웃
제안: KRT 전용 타임아웃
galileo:
krt:
timeout: 5000 # 5초
retry-count: 12. 복편 에러 로깅
현황: 로그 없음
제안: 특별 처리 로깅
if (response.body?.fareRules?.shouldSkipFareRuleError() == true) {
logger.warn("[SKIP_FARE_RULE_ERROR] Itinerary: $itinerarySummary, " +
"Reason: Return fare rule not found")
return@checkError
}3. KRT 캐싱 분리
현황: 최종 결과만 캐싱
제안: KRT 결과 별도 캐싱
val krtCacheKey = "KRT:${fareRules.hashCode()}"
krtCache.get(krtCacheKey) ?: run {
krtClient.getFareRules(...).also {
krtCache.put(krtCacheKey, it, 1.hours)
}
}10. 참고 자료
10.1 주요 클래스
GalileoFareRuleService.kt: 운임 규정 서비스 (24-115)GalileoClient.kt: SOAP 클라이언트 (205-267)KrtClient.kt: KRT 번역 클라이언트AirFareRulesRQ.kt: 운임 규정 요청 모델AirFareRulesRS.kt: 운임 규정 응답 모델
10.2 주요 메소드
GalileoFareRuleService.getFareRules(): 운임 규정 조회 (34-98)GalileoClient.getFareRules(): SOAP 운임 규정 API (205-258)GalileoClient.shouldSkipFareRuleError(): 복편 에러 체크 (260-267)KrtClient.getFareRules(): KRT 번역 API
10.3 관련 API
- AirPriceRQ: Pricing (FareInfo 생성)
- AirFareRulesRQ: 운임 규정 조회
- KRT API: 한글 번역
이 문서는 Triple Air International Adapter 프로젝트의 Galileo 운임 규정 API 심층 분석 문서입니다.