Phase 6: Galileo 운임 규정 API 심층 분석

1. API 엔드포인트 개요

1.1 운임 규정 관련 엔드포인트

엔드포인트메서드API 타입기능위치
/internals/GALILEO/fare-rulesPOSTSOAP + 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) }

정렬 기준:

  1. groupSequence: 그룹 순서 (구간별)
  2. 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 처리:

  1. 검색 캐시 제거 (removeFlightSearchKey)
  2. Unexposed 저장 (saveUnexposedFareItinerary)
  3. 예외 재발생

목적: 매진된 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 비교

항목GalileoAmadeusSabre
API 타입SOAP + KRTSOAPSOAP
엔드포인트AirFareRulesRQFare_GetFareRulesGetFareRulesRQ
번역 서비스KRT (자체)없음 (영문)없음 (영문)
병렬 처리있음 (pmap)없음없음
특별 에러 처리있음 (왕복 복편)없음없음
캐싱Redis (detailKey 기반)RedisRedis
성인 운임만필터링필터링필터링

8.2 에러 처리 비교

에러 시나리오GalileoAmadeusSabre
운임 규정 없음FETCH_FARE_RULES_FAILEDFETCH_FARE_RULES_FAILEDFETCH_FARE_RULES_FAILED
복편 규정 에러에러 무시 (특별 처리)에러 발생에러 발생
SOLD_OUTUnexposed 저장Unexposed 저장Unexposed 저장
KRT 실패일부 성공 허용N/AN/A

8.3 독특한 특징

Galileo 고유 특징

  1. KRT 통합:

    • 자체 한글 번역 서비스
    • 영문 → 한글 자동 변환
    • 사용자 경험 향상
  2. 복편 규정 에러 처리:

    • 왕복 운임 복편 규정 에러 무시
    • Galileo 배상 보증
    • 편도 규정만 노출
  3. 그룹별 병렬 번역:

    • 구간별 KRT API 병렬 호출
    • 성능 최적화
  4. Pricing 통합:

    • 운임 규정 조회 전 Pricing 필수
    • FareInfo 기반 규정 조회
  5. 부분 실패 허용:

    • KRT 일부 실패 시 성공한 그룹 반환
    • 가용성 향상

9. 주요 발견사항

9.1 Galileo 운임 규정 시스템의 특징

  1. KRT 통합: 한글 자동 번역
  2. 복편 에러 무시: 특별 처리 로직
  3. 병렬 KRT: 그룹별 병렬 번역
  4. Pricing 선행: FareInfo 기반 조회
  5. 성인 운임 기준: 소아/유아 동일 규정

9.2 개선 가능 영역

1. KRT 타임아웃 설정

현황: 기본 타임아웃

제안: KRT 전용 타임아웃

galileo:
  krt:
    timeout: 5000  # 5초
    retry-count: 1

2. 복편 에러 로깅

현황: 로그 없음

제안: 특별 처리 로깅

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 심층 분석 문서입니다.