FareRuleProxyController DFS (기능명세서)
1. API 개요 및 목적
1.1 개요
| 항목 | 값 |
|---|---|
| 클래스명 | FareRuleProxyController |
| 패키지 | com.triple.air.intl.search.interfaces.controller.internal.proxy |
| 소스 위치 | air-intl-search/src/main/kotlin/.../interfaces/controller/internal/proxy/FareRuleProxyController.kt:16 |
| Base Path | /internals/proxy/flights/fare-rules |
| 용도 | 내부 프록시 서비스용 운임 규정 조회 API |
1.2 목적
FareRuleProxyController는 **내부 서비스 간 통신(Internal Proxy)**을 위한 운임 규정 조회 API를 제공합니다. 주요 특징:
- 내부 시스템 간 호출을 위한 전용 엔드포인트
- MIX 항공권(분리 발권) 지원을 위한 다중 detailKey 처리
- 동기 방식의 직접 응답 (폴링 불필요)
- 판매 채널 및 퍼널 헤더 필수
2. 필수 헤더 요구사항
컨트롤러 레벨에서 다음 헤더가 필수로 요구됩니다:
| 헤더명 | 상수 | 설명 |
|---|---|---|
x-triple-sales-channel | Constants.TRIPLE_SALES_CHANNEL_HEADER | 판매 채널 식별자 |
x-triple-sales-funnel | Constants.TRIPLE_SALES_FUNNEL_HEADER | 판매 퍼널 식별자 |
소스 위치: FareRuleProxyController.kt:14, Constants.kt:6-7
@RequestMapping(
"/internals/proxy/flights/fare-rules",
headers = [Constants.TRIPLE_SALES_CHANNEL_HEADER, Constants.TRIPLE_SALES_FUNNEL_HEADER]
)헤더가 누락된 경우 Spring Framework에서 404 또는 헤더 관련 오류를 반환합니다.
3. 엔드포인트별 상세 분석
3.1 운임 규정 그룹 조회 (getFareRulesGroup)
기본 정보
| 항목 | 값 |
|---|---|
| 엔드포인트 | GET /internals/proxy/flights/fare-rules/{detailKey1} |
| 대체 엔드포인트 | GET /internals/proxy/flights/fare-rules/{detailKey1}/{detailKey2} |
| 메서드 | getFareRulesGroup |
| 소스 위치 | FareRuleProxyController.kt:20-41 |
Request 스펙
Path Variables
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
detailKey1 | String | O | 첫 번째 항공권 상세 키 |
detailKey2 | String? | X | 두 번째 항공권 상세 키 (MIX 항공권용) |
Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
adult | Int | O | - | 성인 승객 수 |
child | Int | X | 0 | 아동 승객 수 |
infant | Int | X | 0 | 유아 승객 수 |
Response 스펙
HTTP Status: 200 OK
Response Type: List<InternalProxyFareRuleGroupView>
// 소스: FareRulesView.kt:57-72
data class InternalProxyFareRuleGroupView(
val detailKey: String,
val fareRules: List<InternalProxyFareRulesView>,
)
// 소스: FareRulesView.kt:44-55
data class InternalProxyFareRulesView(
val groupTitle: String,
val rules: List<InternalProxyFareRuleItemView>,
)
// 소스: FareRulesView.kt:74-89
data class InternalProxyFareRuleItemView(
val type: FareRuleType?, // REFUND_AND_CHANGE, BAGGAGE, MILEAGE, COMMON
val title: String,
val content: String?,
val order: Int,
)FareRuleType Enum (소스: FareRuleType.kt:3-8)
| 값 | 설명 |
|---|---|
REFUND_AND_CHANGE | 환불 및 변경 규정 |
BAGGAGE | 수하물 규정 |
MILEAGE | 마일리지 규정 |
COMMON | 일반 규정 |
3.2 구조화된 운임 규정 조회 (getStructuredFareRules)
기본 정보
| 항목 | 값 |
|---|---|
| 엔드포인트 | GET /internals/proxy/flights/fare-rules/structured/{detailKey1} |
| 대체 엔드포인트 | GET /internals/proxy/flights/fare-rules/structured/{detailKey1}/{detailKey2} |
| 메서드 | getStructuredFareRules |
| 소스 위치 | FareRuleProxyController.kt:43-56 |
Request 스펙
Path Variables
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
detailKey1 | String | O | 첫 번째 항공권 상세 키 |
detailKey2 | String? | X | 두 번째 항공권 상세 키 (MIX 항공권용) |
참고: 구조화된 운임 규정 조회에서는 승객 수 파라미터가 필요하지 않습니다.
Response 스펙
HTTP Status: 200 OK
Response Type: List<InternalProxyStructuredFareRuleView>
// 소스: FareRulesView.kt:91-103
data class InternalProxyStructuredFareRuleView(
val detailKey: String,
val fareRules: InternalProxyStructuredFareRuleResponseView,
)
// 소스: FareRulesView.kt:105-114
data class InternalProxyStructuredFareRuleResponseView(
val segments: List<InternalProxySegmentFareRulePolicyView>,
)
// 소스: FareRulesView.kt:116-127
data class InternalProxySegmentFareRulePolicyView(
val id: String,
val policy: InternalProxyFareRulePolicyView,
)InternalProxyFareRulePolicyView 구조 (소스: FareRulesView.kt:129-146)
data class InternalProxyFareRulePolicyView(
val cancellation: InternalProxyCancellationPolicyView?,
val change: InternalProxyChangePolicyView?,
val baggage: InternalProxyBaggagePolicyView?,
val mileage: InternalProxyMileagePolicyView?,
val additionalServices: InternalProxyAdditionalServicesPolicyView?,
)세부 정책 구조
| 정책 | 클래스 | 주요 필드 |
|---|---|---|
| 취소 정책 | InternalProxyCancellationPolicyView | available, conditions |
| 변경 정책 | InternalProxyChangePolicyView | available, conditions |
| 수하물 정책 | InternalProxyBaggagePolicyView | type, baggages |
| 마일리지 정책 | InternalProxyMileagePolicyView | available, amount, rate |
| 부가 서비스 | InternalProxyAdditionalServicesPolicyView | seatSelectionAvailable, mealIncluded |
PenaltyCondition 구조 (소스: FareRulesView.kt:174-189)
data class InternalProxyPenaltyConditionView(
val type: String, // 조건 유형
val period: Int, // 기간 (일/시간)
val currency: String, // 통화 코드
val fee: BigDecimal, // 수수료
)4. 비즈니스 로직 흐름
4.1 운임 규정 그룹 조회 플로우
sequenceDiagram participant Client participant Controller as FareRuleProxyController participant Validator as checkSearchablePassengers participant Service as FareRuleService participant FlightSearch as FlightSearchService participant Adapter as AdapterClient participant External as External API Client->>Controller: GET /fare-rules/{detailKey1}/{detailKey2} Controller->>Validator: checkSearchablePassengers(adult, child, infant) alt 유효성 검사 실패 Validator-->>Client: MethodArgumentInvalidException end Controller->>Service: getFareRulesGroups(detailKeys, adult, child, infant) Service->>Service: validateMultiTicketParameter(detailKeys) alt detailKeys.size > 1 Service->>FlightSearch: getFlightItem(detailKey) FlightSearch-->>Service: FlightItem alt !flightItem.isMix Service-->>Client: MethodArgumentInvalidException(INVALID_PARAMETER) end end loop 각 detailKey에 대해 (병렬) Service->>Adapter: getFareRulesGroups(detailKey, adult, child, infant) Adapter->>External: GET /{supplier}/fare-rules External-->>Adapter: FareRuleResponse[] Adapter-->>Service: FareRuleGroup end Service-->>Controller: List<FareRuleGroup> Controller-->>Client: List<InternalProxyFareRuleGroupView>
4.2 구조화된 운임 규정 조회 플로우
sequenceDiagram participant Client participant Controller as FareRuleProxyController participant Service as FareRuleService participant Adapter as AdapterClient participant External as External API Client->>Controller: GET /fare-rules/structured/{detailKey1}/{detailKey2} Controller->>Service: getStructuredFareRules(detailKey1, detailKey2) loop 각 detailKey에 대해 (병렬) Service->>Adapter: getStructuredFareRules(detailKey) Adapter->>External: GET /{supplier}/fare-rules/structured External-->>Adapter: StructuredFareRuleResponse Adapter-->>Service: StructuredFareRuleDetail end Service->>Service: StructuredFareRule.of(detailKey, fareRules) Service-->>Controller: List<StructuredFareRule> Controller-->>Client: List<InternalProxyStructuredFareRuleView>
5. Service 연동 분석
5.1 FareRuleService
소스 위치: FareRuleService.kt
getFareRulesGroups (라인 37-59)
fun getFareRulesGroups(
detailKeys: List<String>,
adult: Int,
child: Int,
infant: Int,
): List<FareRuleGroup> {
validateMultiTicketParameter(detailKeys) // MIX 항공권 검증
return withBlocking(Dispatchers.IO) {
detailKeys.pmap { detailKey -> // 병렬 처리
adapterClient.getFareRulesGroups(...)
}
.getOrThrow()
.sortedBy { fareRuleGroup ->
detailKeys.indexOf(fareRuleGroup.detailKey) // 입력 순서 유지
}
}
}getStructuredFareRules (라인 61-74)
fun getStructuredFareRules(detailKey1: String, detailKey2: String?): List<StructuredFareRule> {
val detailKeys = listOfNotNull(detailKey1, detailKey2)
return withBlocking(Dispatchers.IO) {
detailKeys.pmap { detailKey ->
detailKey to adapterClient.getStructuredFareRules(detailKey)
}.getOrThrow()
.map { (detailKey, structuredFareRules) ->
StructuredFareRule.of(detailKey, structuredFareRules)
}
}
}5.2 AdapterClient 연동
소스 위치: AdapterClient.kt
getFareRulesGroups (라인 156-195)
- 엔드포인트:
{endpoint}/{supplier}/fare-rules - HTTP Method: GET
- 쿼리 파라미터:
key,adult,child,infant - detailKey 분해:
destructDetailKey()함수로 groupKey, key, supplier 추출
getStructuredFareRules (라인 197-220)
- 엔드포인트:
{endpoint}/{supplier}/fare-rules/structured - HTTP Method: GET
- 쿼리 파라미터:
key - 승객 수 불필요: 구조화된 규정은 승객 수와 무관
6. detailKey1/detailKey2 처리 (MIX 항공권 지원)
6.1 MIX 항공권이란?
MIX 항공권은 분리 발권 상품으로, 가는 편과 오는 편이 서로 다른 항공권으로 발권되는 경우입니다.
| 구분 | 일반 왕복 | MIX 항공권 |
|---|---|---|
| 발권 수 | 1장 | 2장 |
| detailKey | 1개 | 2개 |
| 운임 규정 | 1세트 | 2세트 (각 항공권별) |
isMix 플래그 | false | true |
6.2 URL 패턴
# 단일 항공권 (일반)
GET /internals/proxy/flights/fare-rules/{detailKey1}
# MIX 항공권 (분리 발권)
GET /internals/proxy/flights/fare-rules/{detailKey1}/{detailKey2}
6.3 처리 로직 (소스: FareRuleProxyController.kt:29, FareRuleService.kt:115-126)
// Controller에서 detailKeys 리스트 생성
val detailKeys = listOfNotNull(detailKey1, detailKey2)
// Service에서 MIX 항공권 검증
private fun validateMultiTicketParameter(detailKeys: List<String>) {
if (detailKeys.size > 1) {
withBlocking(Dispatchers.IO) {
detailKeys.pmap {
val flightItem = flightSearchService.getFlightItem(it)
if (!flightItem.isMix) {
throw MethodArgumentInvalidException(MessageKey.INVALID_PARAMETER)
}
}.getOrThrow()
}
}
}6.4 검증 규칙
| 조건 | 결과 |
|---|---|
detailKey2 = null | 검증 스킵, 단일 항공권으로 처리 |
detailKeys.size > 1 && 모두 isMix = true | 정상 처리 |
detailKeys.size > 1 && 하나라도 isMix = false | MethodArgumentInvalidException |
7. FareRuleController와의 차이점
7.1 비교 표
| 항목 | FareRuleController | FareRuleProxyController |
|---|---|---|
| 경로 | /fare-rules | /internals/proxy/flights/fare-rules |
| 용도 | 외부 클라이언트용 | 내부 서비스 간 통신 |
| 헤더 요구 | 없음 | x-triple-sales-channel, x-triple-sales-funnel 필수 |
| 응답 방식 | 폴링 기반 (비동기) | 직접 응답 (동기) |
| MIX 항공권 | 미지원 (단일 detailKey) | 지원 (detailKey1/detailKey2) |
| 구조화된 규정 | 미지원 | 지원 (/structured 엔드포인트) |
| 응답 타입 | FareRuleInfoView | InternalProxyFareRuleGroupView |
7.2 FareRuleController 응답 방식 (폴링)
sequenceDiagram participant Client participant Controller as FareRuleController participant Service as FareRuleService participant Cache as FareRuleRepository Client->>Controller: GET /fare-rules?flightDetailKey=... Controller->>Service: init(detailKey, ...) Service->>Cache: save(PENDING) Service-->>Controller: UUID (fareRuleKey) Controller-->>Client: 202 Accepted + {"key": UUID} Note over Service: 백그라운드에서 운임 규정 조회 Client->>Controller: GET /fare-rules/{fareRuleKey} Controller->>Service: getFareRules(fareRuleKey) Service->>Cache: find(fareRuleKey) alt status = PENDING Cache-->>Client: 202 Accepted + {status: PENDING} else status = COMPLETE Cache-->>Client: 200 OK + {status: COMPLETE, fareRules: [...]} else status = ERROR Cache-->>Client: Exception end
7.3 FareRuleProxyController 응답 방식 (직접)
sequenceDiagram participant Client participant Controller as FareRuleProxyController participant Service as FareRuleService participant Adapter as AdapterClient Client->>Controller: GET /internals/proxy/flights/fare-rules/{detailKey} Controller->>Service: getFareRulesGroups(...) Service->>Adapter: 외부 API 호출 (동기) Adapter-->>Service: FareRuleGroup Service-->>Controller: List<FareRuleGroup> Controller-->>Client: 200 OK + List<InternalProxyFareRuleGroupView>
7.4 주요 차이점 요약
-
폴링 vs 직접 응답
FareRuleController: 키 발급 후 폴링 필요FareRuleProxyController: 즉시 결과 반환
-
MIX 항공권 지원
FareRuleController: 단일flightDetailKey만 지원FareRuleProxyController:detailKey1,detailKey2지원
-
유지보수 시간 체크
FareRuleController:checkMaintenanceTime(supplier)호출FareRuleProxyController: 유지보수 시간 체크 없음
8. 에러 처리 및 예외 상황
8.1 승객 수 유효성 검사
소스: SearchValidator.kt:6-30
| 조건 | 예외 | MessageKey |
|---|---|---|
adult < 1 | MethodArgumentInvalidException | INVALID_PASSENGERS |
adult + child + infant > 9 | MethodArgumentInvalidException | INVALID_PASSENGERS_COUNT |
child > adult * 3 | MethodArgumentInvalidException | INVALID_PASSENGERS_CHILD |
adult < infant | MethodArgumentInvalidException | INVALID_PASSENGERS_INFANT |
8.2 MIX 항공권 검증 에러
소스: FareRuleService.kt:115-126
| 조건 | 예외 | MessageKey |
|---|---|---|
2개의 detailKey 중 하나라도 isMix = false | MethodArgumentInvalidException | INVALID_PARAMETER |
8.3 외부 API 에러
소스: AdapterClient.kt:188-194, 213-218
| 에러 코드 | 예외 | MessageKey |
|---|---|---|
SOLD_OUT | InternationalSearchException | SOLD_OUT |
| 기타 | InternationalSearchException | FARE_RULE_FAILED |
8.4 캐시 키 관련 에러
| 조건 | 예외 | MessageKey |
|---|---|---|
| detailKey로 FlightItem 조회 실패 | CacheKeyInvalidException | INVALID_CACHE_KEY |
8.5 예외 클래스 계층 구조
소스: Exceptions.kt
ApiException (abstract)
├── MethodArgumentInvalidException // 파라미터 유효성 검사 실패
├── CacheKeyInvalidException // 캐시 키 무효
├── InternationalSearchException // 일반 검색 오류
└── SystemMaintenanceException // 시스템 점검 중
9. 관련 도메인 모델 분석
9.1 FareRuleGroup
소스: FareRuleGroup.kt:3-6
data class FareRuleGroup(
val detailKey: String,
val fareRules: List<FareRule>,
)9.2 FareRule
소스: FareRule.kt:7-21
data class FareRule(
val groupTitle: String, // "규정 1", "규정 2" 등
val rules: List<FareRuleItem>
) : Serializable9.3 FareRuleItem
소스: FareRuleItem.kt:7-24
data class FareRuleItem(
val type: FareRuleType?,
val title: String,
val content: String?,
val order: Int,
) : Serializable9.4 StructuredFareRule
소스: StructuredFareRule.kt:6-15
data class StructuredFareRule(
val detailKey: String,
val fareRules: StructuredFareRuleDetail,
)9.5 StructuredFareRuleDetail
소스: StructuredFareRule.kt:17-27
data class StructuredFareRuleDetail(
val segments: List<SegmentFareRulePolicy>,
)9.6 FareRulePolicy
소스: StructuredFareRule.kt:43-61
data class FareRulePolicy(
val cancellation: CancellationPolicy?,
val change: ChangePolicy?,
val baggage: BaggagePolicy?,
val mileage: MileagePolicy?,
val additionalServices: AdditionalServicesPolicy?,
)9.7 정책 세부 모델
| 모델 | 필드 | 소스 |
|---|---|---|
CancellationPolicy | available, conditions | StructuredFareRule.kt:63-75 |
ChangePolicy | available, conditions | StructuredFareRule.kt:77-89 |
PenaltyCondition | type, period, currency, fee | StructuredFareRule.kt:91-107 |
BaggagePolicy | type, baggages | StructuredFareRule.kt:109-121 |
MileagePolicy | available, amount, rate | StructuredFareRule.kt:123-137 |
AdditionalServicesPolicy | seatSelectionAvailable, mealIncluded | StructuredFareRule.kt:139-151 |
10. 응답 예시
10.1 운임 규정 그룹 조회 응답 예시
[
{
"detailKey": "uuid-key-1::cache-key-1::SUPPLIER1",
"fareRules": [
{
"groupTitle": "규정 1",
"rules": [
{
"type": "REFUND_AND_CHANGE",
"title": "환불 규정",
"content": "출발 7일 전까지 무료 환불 가능...",
"order": 1
},
{
"type": "BAGGAGE",
"title": "수하물 규정",
"content": "무료 위탁 수하물 23kg 1개...",
"order": 2
}
]
}
]
}
]10.2 구조화된 운임 규정 조회 응답 예시
[
{
"detailKey": "uuid-key-1::cache-key-1::SUPPLIER1",
"fareRules": {
"segments": [
{
"id": "segment-1",
"policy": {
"cancellation": {
"available": true,
"conditions": [
{
"type": "BEFORE_DEPARTURE",
"period": 7,
"currency": "KRW",
"fee": 50000
}
]
},
"change": {
"available": true,
"conditions": [
{
"type": "BEFORE_DEPARTURE",
"period": 3,
"currency": "KRW",
"fee": 30000
}
]
},
"baggage": {
"type": "WEIGHT",
"baggages": [
{
"unit": "KG",
"allowance": 23
}
]
},
"mileage": {
"available": true,
"amount": 1500,
"rate": 0.5
},
"additionalServices": {
"seatSelectionAvailable": true,
"mealIncluded": false
}
}
}
]
}
}
]부록: 소스 파일 위치 요약
| 파일 | 경로 |
|---|---|
| FareRuleProxyController | interfaces/controller/internal/proxy/FareRuleProxyController.kt |
| FareRuleController | interfaces/controller/FareRuleController.kt |
| FareRuleService | application/FareRuleService.kt |
| AdapterClient | infrastructure/adapter/AdapterClient.kt |
| Constants | support/Constants.kt |
| FareRulesView | interfaces/response/FareRulesView.kt |
| FareRule | domain/FareRule.kt |
| FareRuleGroup | domain/FareRuleGroup.kt |
| FareRuleItem | domain/FareRuleItem.kt |
| StructuredFareRule | domain/StructuredFareRule.kt |
| FareRuleType | support/enums/FareRuleType.kt |
| SearchValidator | support/util/SearchValidator.kt |
| Exceptions | support/exception/Exceptions.kt |
| MessageKey | support/MessageKey.kt |
문서 생성일: 2025-12-15 소스 기준: air-intl-search 프로젝트