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,
)주요 단계:
- 운임 계산:
AmadeusClient.pricing()호출 (AmadeusPricingService.kt:23-31) - TST 생성:
AmadeusClient.tstCreate()호출 (AmadeusPricingService.kt:33-35) - TST 업데이트: 할인 적용 시 (AmadeusPricingService.kt:37-57)
- 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 현재 이슈
- 하드코딩된 재시도 횟수:
@Retryable(maxAttempts = 2)설정 가능하게 변경 필요 - PR/MU 항공사 특별 처리: 더 많은 항공사 추가 시 확장성 고려 필요
9.2 개선 방안
- 재시도 정책 외부화: application.yml에서 관리
- 항공사별 처리 전략 패턴: Strategy Pattern 적용 검토
- 캐시 TTL 동적 관리: 항공사별/노선별 차별화
10. 참고사항
10.1 특별 요구사항
- PR 항공사: “RFND ONLY TO ISSUE AGT” 텍스트 필수 포함
- MU 항공사: DOB 정보 필수 (생년월일 형식: ddMMMyy)
- TST 업데이트 제외 조건:
- 이미 할인 적용된 운임 (tourCode != null)
- 할인 금액이 음수인 경우