Amadeus 운임 계산(Pricing) API 심층 분석

1. API 개요

1.1 주요 기능

  • 운임 계산: PNR 기반 정확한 운임 계산 및 TST 생성
  • TST 관리: Transitional Stored Ticket 생성 및 업데이트
  • Endorsement 처리: 항공사별 특별 조건 및 생년월일 정보 추가
  • Fare Rule 조회: ART API를 통한 운임 규정 조회

1.2 관련 파일 구조

supplier/amadeus/
├── application/
│   ├── AmadeusPricingService.kt      # 운임 계산 서비스
│   └── AmadeusFareRuleService.kt     # 운임 규정 조회 서비스
├── infrastructure/
│   ├── AmadeusClient.kt               # SOAP API 클라이언트
│   └── topas/
│       └── ArtClient.kt               # ART API 클라이언트
└── interfaces/controller/
    └── internals/
        └── AmadeusFareRuleController.kt  # 운임 규정 컨트롤러

2. 핵심 비즈니스 로직

2.1 운임 계산 프로세스

flowchart TD
    A[운임 계산 요청] --> B[Pricing API 호출]
    B --> C{응답 성공?}
    C -->|성공| D[TST 생성]
    C -->|실패| E[오류 처리]

    D --> F{할인 적용 대상?}
    F -->|Yes| G[TST 업데이트]
    F -->|No| H[다음 단계]

    G --> I[Tour Code 저장]
    I --> H

    H --> J{항공사 체크}
    J -->|PR/MU| K[Endorsement 처리]
    J -->|기타| L[완료]

    K --> M[기존 Endorsement 제거]
    M --> N[새 Endorsement 저장]
    N --> L

    style C fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style F fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style J fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000

2.2 운임 계산 서비스 (AmadeusPricingService)

2.2.1 메인 로직

// AmadeusPricingService.kt:16-107
fun pricing(
    validatingCarrier: String,
    passengerAirPrices: List<PassengerAirPrice>,
    onlyFreeBaggageInclude: Boolean,
    corporateId: String?,
    statefulBuilder: StatefulBuilder,
)

주요 단계:

  1. 운임 계산: AmadeusClient.pricing() 호출 (AmadeusPricingService.kt:23-31)
  2. TST 생성: AmadeusClient.tstCreate() 호출 (AmadeusPricingService.kt:33-35)
  3. TST 업데이트: 할인 적용 시 (AmadeusPricingService.kt:37-57)
  4. Endorsement 처리: PR/MU 항공사 특별 처리 (AmadeusPricingService.kt:59-106)

2.2.2 TST 업데이트 조건

// AmadeusPricingService.kt:37-46
val updatableTstList = createdTstList.mapNotNull { createdTst ->
    acceptableFares.find { acceptableFare ->
        acceptableFare.underFare != null &&
        createdTst.passengerReferences.containsAll(acceptableFare.underFare.passengerReferences) &&
        acceptableFare.ticketingFare.tourCode == null && // 이미 할인적용된 운임 제외
        ((acceptableFare.discount ?: 0) > 0) // 할인금액이 마이너스일 때 제외
    }
}

2.3 항공사별 특별 처리

2.3.1 PR/MU 항공사 Endorsement

flowchart LR
    A[항공사 확인] --> B{PR or MU?}
    B -->|Yes| C[PNR 정보 조회]
    B -->|No| G[스킵]

    C --> D[기존 Endorsement 제거]
    D --> E[새 Endorsement 생성]
    E --> F[DOB 정보 추가]

    E --> H{PR 항공?}
    H -->|Yes| I["RFND ONLY TO ISSUE AGT" 추가]
    H -->|No| F
    I --> F

    F --> J[Endorsement 저장]

    style B fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style H fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000

구현 코드:

// AmadeusPricingService.kt:59-106
if (validatingCarrier == "PR" || validatingCarrier == "MU") {
    // PNR 정보 조회
    val pnrInfo = statefulBuilder.inSeries {
        amadeusClient.getPnrInfo(statefulBuilder = this)
    }
 
    // PR 항공사 특별 텍스트 추가
    if (validatingCarrier == "PR") {
        val appendText = "RFND ONLY TO ISSUE AGT"
        if (!previousEndorsementsText.contains(appendText)) {
            append(appendText)
        }
    }
 
    // 생년월일 추가
    append(" DOB$birthDate")
}

3. Fare Rule API

3.1 운임 규정 조회 프로세스

flowchart TD
    A[Fare Rule 요청] --> B[캐시 확인]
    B --> C{캐시 존재?}
    C -->|Yes| D[캐시 데이터 반환]
    C -->|No| E[FareItinerary 조회]

    E --> F[ART API 호출]
    F --> G{성공?}
    G -->|Yes| H[결과 캐싱]
    G -->|No| I[@Retryable 재시도]

    H --> J[FareRule 반환]
    I --> K{재시도 횟수 초과?}
    K -->|No| F
    K -->|Yes| L[예외 발생]

    style C fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style G fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style K fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000

3.2 운임 규정 서비스 (AmadeusFareRuleService)

3.2.1 캐싱 전략

// AmadeusFareRuleService.kt:17-34
fun findFareRules(key: String, adult: Int, child: Int, infant: Int): List<FareRule> {
    val fareRuleKey = CacheKeyGenerator.generateFareRuleKey(key, adult, child, infant)
    val savedFareRules = fareRuleRepository.findFareRules(fareRuleKey)
 
    return if (savedFareRules.isNullOrEmpty()) {
        // 캐시 미스: ART API 호출 후 캐싱
        val fareItinerary = fareItineraryRepository.getFareItinerary(key)
        artClient.findFareRules(adult, child, infant, fareItinerary).also {
            fareRuleRepository.saveFareRules(fareRuleKey, it)
        }
    } else {
        savedFareRules  // 캐시 히트
    }
}

3.3 ART Client 구현

3.3.1 API 호출

// ArtClient.kt:29-60
@Retryable(maxAttempts = 2)
fun findFareRules(adult: Int, child: Int, infant: Int, fareItinerary: FareItinerary): List<FareRule> {
    val request = ArtRequest.ofFareItinerary(adult, child, infant, fareItinerary)
 
    return "${amadeusApiProperties.art.endpoint}/api/v1/art/getrule/${amadeusApiProperties.art.agentCode}/${amadeusApiProperties.officeId}"
        .post(request)
        .header(
            mapOf(
                HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
                "x-api-key" to amadeusApiProperties.art.apiKey
            )
        )
        .execute<ArtResponse>()
        .fold(
            onSuccess = { it.toFareRules() },
            onFailure = { throw it }
        )
}

4. API 클라이언트 구현

4.1 Pricing API 호출

// AmadeusClient.kt:569-608
fun pricing(
    validatingCarrier: String,
    passengerAirPrices: List<PassengerAirPrice>,
    onlyFreeBaggageInclude: Boolean,
    corporateId: String?,
    statefulBuilder: StatefulBuilder? = null,
): List<AcceptableFare> {
    val request = FarePricePnrWithBookingClass.of(
        validatingCarrier = validatingCarrier,
        passengerAirPrices = passengerAirPrices,
        onlyFreeBaggageInclude = onlyFreeBaggageInclude,
        corporateId = corporateId
    ).withSession(statefulBuilder?.session)
 
    return amadeusApiProperties.endpoint
        .post(request)
        .header(getHeaderMap(request))
        .requestBodyConvert(soapRequestBodyConverter(amadeusApiProperties))
        .execute<AmadeusResponse<FarePricePnrWithBookingClassReply>>(
            soapBodyDeserializerOf(logger, objectMapper)
        )
        .fold(
            onSuccess = { response ->
                response.responseBody.toAcceptableFares()
            },
            onFailure = { error ->
                throw AmadeusPricingException("Pricing failed", error)
            }
        )
}

4.2 TST 생성/업데이트

// AmadeusClient.kt:610-636
fun tstCreate(acceptableFares: List<AcceptableFare>, statefulBuilder: StatefulBuilder? = null): List<TstCreate> {
    val request = TicketCreateTstFromPricing.of(acceptableFares).withSession(statefulBuilder?.session)
    // API 호출 및 응답 처리
}
 
// AmadeusClient.kt:638-655
fun tstUpdate(tstUpdate: TstUpdate, statefulBuilder: StatefulBuilder? = null) {
    val request = TicketUpdateTST.of(tstUpdate).withSession(statefulBuilder?.session)
    // API 호출 및 응답 처리
}

5. 오류 처리

5.1 재시도 메커니즘

  • ART API: @Retryable(maxAttempts = 2) (ArtClient.kt:29)
  • 실패 시: AmadeusPricingException 발생

5.2 예외 처리 전략

flowchart TD
    A[API 호출] --> B{성공?}
    B -->|실패| C[재시도]
    C --> D{재시도 횟수 초과?}
    D -->|No| A
    D -->|Yes| E[예외 발생]

    E --> F[AmadeusPricingException]
    F --> G[오류 로깅]
    G --> H[상위 레이어 전파]

    style B fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style D fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000

6. 성능 최적화

6.1 캐싱 전략

  • Fare Rule 캐싱: Redis 기반 캐싱 (AmadeusFareRuleService.kt:24-33)
  • 캐시 키 생성: CacheKeyGenerator.generateFareRuleKey()
  • TTL: Repository 레벨에서 관리

6.2 Stateful Session 활용

  • 세션 재사용: 모든 pricing 작업은 단일 세션 내 처리
  • 트랜잭션 보장: StatefulBuilder를 통한 일관성 유지

7. 주요 도메인 모델

7.1 핵심 모델

  • PassengerAirPrice: 승객별 항공 운임 정보
  • AcceptableFare: 수락 가능한 운임 결과
  • TstCreate/TstUpdate: TST 생성/업데이트 정보
  • FareRule: 운임 규정 정보
  • FareItinerary: 운임 여정 정보

7.2 데이터 흐름

graph LR
    A[PassengerAirPrice] --> B[Pricing API]
    B --> C[AcceptableFare]
    C --> D[TstCreate]
    D --> E[TstUpdate]

    F[FareItinerary] --> G[ART API]
    G --> H[FareRule]

    style B fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style G fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000

8. 모니터링 및 로깅

8.1 주요 모니터링 포인트

  • Pricing API 응답 시간
  • TST 생성 성공률
  • ART API 재시도 횟수
  • 캐시 히트율

8.2 로깅 전략

  • 모든 API 호출 요청/응답 로깅
  • 예외 상황 상세 로깅
  • 항공사별 특별 처리 로깅

9. 개선 권장사항

9.1 현재 이슈

  1. 하드코딩된 재시도 횟수: @Retryable(maxAttempts = 2) 설정 가능하게 변경 필요
  2. PR/MU 항공사 특별 처리: 더 많은 항공사 추가 시 확장성 고려 필요

9.2 개선 방안

  1. 재시도 정책 외부화: application.yml에서 관리
  2. 항공사별 처리 전략 패턴: Strategy Pattern 적용 검토
  3. 캐시 TTL 동적 관리: 항공사별/노선별 차별화

10. 참고사항

10.1 특별 요구사항

  • PR 항공사: “RFND ONLY TO ISSUE AGT” 텍스트 필수 포함
  • MU 항공사: DOB 정보 필수 (생년월일 형식: ddMMMyy)
  • TST 업데이트 제외 조건:
    • 이미 할인 적용된 운임 (tourCode != null)
    • 할인 금액이 음수인 경우

10.2 관련 문서