FlightSearchProxyController DFS (기능명세서)
1. API 개요 및 목적
1.1 개요
| 항목 | 값 |
|---|---|
| 컨트롤러 | FlightSearchProxyController |
| 패키지 | com.triple.air.intl.search.interfaces.controller.internal.proxy |
| 소스 위치 | air-intl-search/src/main/kotlin/.../interfaces/controller/internal/proxy/FlightSearchProxyController.kt |
| Base Path | /internals/proxy/flights |
| 용도 | 내부 서비스 간 프록시용 국제선 항공편 검색 API |
1.2 목적
- 내부 서비스 전용:
/internals/경로에서 알 수 있듯이 내부 마이크로서비스 간 통신용 API - 프록시 기능: 외부 GDS/공급사로부터 항공편을 검색하고 결과를 전달
- 다양한 여정 지원: 편도(One-Way), 왕복(Round-Trip), 다구간(Multi-City) 검색 지원
- MultiTicket(Split) 검색 지원: 왕복 여정을 가는편/오는편 별도로 검색하여 최적 조합 제공
1.3 의존성
| 의존 서비스 | 역할 | 소스 위치 |
|---|---|---|
StandardFlightSearchUseCase | 표준 항공편 검색 처리 | application/usecase/StandardFlightSearchUseCase.kt:21 |
SplitFlightSearchUseCase | MultiTicket(분할) 항공편 검색 처리 | application/usecase/SplitFlightSearchUseCase.kt:19 |
LocationService | IATA 코드로 공항/도시 정보 조회 | application/LocationService.kt:10 |
2. 필수 헤더 요구사항
2.1 필수 헤더
컨트롤러 레벨에서 필수 헤더가 정의되어 있습니다.
// FlightSearchProxyController.kt:25-28
@RequestMapping(
"/internals/proxy/flights",
headers = [Constants.TRIPLE_SALES_CHANNEL_HEADER, Constants.TRIPLE_SALES_FUNNEL_HEADER]
)| 헤더명 | 상수값 | 필수 여부 | 설명 |
|---|---|---|---|
x-triple-sales-channel | Constants.TRIPLE_SALES_CHANNEL_HEADER | 필수 | 판매 채널 식별자 |
x-triple-sales-funnel | Constants.TRIPLE_SALES_FUNNEL_HEADER | 필수 | 판매 퍼널 식별자 |
소스:
support/Constants.kt:6-7
2.2 선택적 헤더
| 헤더명 | 상수값 | 사용 엔드포인트 | 설명 |
|---|---|---|---|
cached-search-key | Constants.CACHED_SEARCH_KEY | 왕복 검색 (MultiTicket) | 캐시된 검색 결과 키 |
소스:
support/Constants.kt:10
3. 엔드포인트별 상세 분석
3.1 편도 검색 (One-Way)
| 항목 | 값 |
|---|---|
| HTTP Method | GET |
| URL Pattern | /internals/proxy/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate} |
| 메서드명 | searchOneWayTrip() |
| 소스 위치 | FlightSearchProxyController.kt:34-66 |
Path Variables
| 파라미터 | 타입 | 설명 | 예시 |
|---|---|---|---|
originType | LocationType | 출발지 유형 (CITY/AIRPORT) | c 또는 a |
origin | String | 출발지 IATA 코드 | SEL, ICN |
destinationType | LocationType | 도착지 유형 | c 또는 a |
destination | String | 도착지 IATA 코드 | NRT, TYO |
outboundDate | LocalDate | 출발일 (ISO 형식) | 2024-03-15 |
Request Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
cabins | Set<CabinType> | O | - | 좌석 등급 |
adult | Int | O | - | 성인 승객 수 |
child | Int | X | 0 | 소아 승객 수 |
infant | Int | X | 0 | 유아 승객 수 |
freeBaggageOnly | Boolean | X | false | 무료 수하물 포함 항공편만 |
useRecommendation | Boolean | X | true | 추천 점수 적용 여부 |
useCache | Boolean | X | true | 캐시 사용 여부 |
flightGroupCriteria | FlightGroupCriteria | X | SCHEDULE | 그룹핑 기준 |
호출 예시
GET /internals/proxy/flights/search/c:SEL-a:NRT/2024-03-15?cabins=ECONOMY&adult=2&child=1
3.2 왕복 검색 (Round-Trip)
| 항목 | 값 |
|---|---|
| HTTP Method | GET |
| URL Pattern | /internals/proxy/flights/search/{originType}:{origin}-{destinationType}:{destination}/{outboundDate}/{inboundDate} |
| 메서드명 | searchRoundTrip() |
| 소스 위치 | FlightSearchProxyController.kt:68-136 |
Path Variables
편도와 동일 + 아래 추가:
| 파라미터 | 타입 | 설명 |
|---|---|---|
inboundDate | LocalDate | 귀국일 (ISO 형식) |
Request Parameters
편도 파라미터 + 아래 추가:
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
useMultiTicket | Boolean | X | false | Split 검색 모드 활성화 |
searchTripDirectionType | SearchTripDirectionType | X | null | 검색 방향 (OUTBOUND/INBOUND) |
detailKey | String | X | null | 앵커 항공편 상세키 (INBOUND 검색 시 필수) |
promotionPrincipleId | Long | X | null | 프로모션 정책 ID |
Request Headers
| 헤더 | 설명 |
|---|---|
cached-search-key | MultiTicket 모드에서 캐시된 검색 키 |
비즈니스 로직 분기
// FlightSearchProxyController.kt:92-136
return if (useMultiTicket) {
searchSplitFlights(...) // Split 검색 UseCase 호출
} else {
searchFlights(...) // 표준 검색 UseCase 호출
}3.3 다구간 검색 (Multi-City) - 2구간
| 항목 | 값 |
|---|---|
| HTTP Method | GET |
| URL Pattern | /internals/proxy/flights/search/{originType1}:{origin1}-{destinationType1}:{destination1}/{date1}/{originType2}:{origin2}-{destinationType2}:{destination2}/{date2} |
| 메서드명 | searchMultiCityTrip() (2구간) |
| 소스 위치 | FlightSearchProxyController.kt:138-185 |
3.4 다구간 검색 (Multi-City) - 3구간
| 항목 | 값 |
|---|---|
| HTTP Method | GET |
| URL Pattern | /internals/proxy/flights/search/.../date1/.../date2/.../date3 |
| 메서드명 | searchMultiCityTrip() (3구간) |
| 소스 위치 | FlightSearchProxyController.kt:187-246 |
3.5 다구간 검색 (Multi-City) - 4구간
| 항목 | 값 |
|---|---|
| HTTP Method | GET |
| URL Pattern | /internals/proxy/flights/search/.../date1/.../date2/.../date3/.../date4 |
| 메서드명 | searchMultiCityTrip() (4구간) |
| 소스 위치 | FlightSearchProxyController.kt:248-319 |
참고: 다구간 검색은 최대 4구간까지 지원
4. Request/Response 구조
4.1 공통 Request 모델
LocationType (Enum)
// support/enums/LocationType.kt:3-6
enum class LocationType(val shorter: String) {
CITY(shorter = "c"), // 도시 코드
AIRPORT(shorter = "a"); // 공항 코드
}CabinType (Enum)
// support/enums/CabinType.kt:3-10
enum class CabinType(val value: String) {
ECONOMY(value = "Y"), // 이코노미
PREMIUM_ECONOMY(value = "W"), // 프리미엄 이코노미
BUSINESS(value = "C"), // 비즈니스
FIRST(value = "F") // 일등석
}FlightGroupCriteria (Enum)
// support/enums/FlightGroupCriteria.kt:6-11
enum class FlightGroupCriteria {
NONE, // 그룹핑 없음
SCHEDULE, // 스케줄 기준 (기본값)
SCHEDULE_WITH_CLASS, // 스케줄 + 예약 클래스
SCHEDULE_WITH_FREE_BAGGAGE, // 스케줄 + 무료 수하물
}SearchTripDirectionType (Enum)
// support/enums/SearchTripDirectionType.kt:3-6
enum class SearchTripDirectionType {
OUTBOUND, // 가는편 검색
INBOUND, // 오는편 검색
}4.2 Response 구조
InternalProxyFlightItemView
소스:
interfaces/response/ProxyFlightSearchView.kt:10-48
data class InternalProxyFlightItemView(
val listKey: UUID, // 검색 결과 캐시 키
val score: BigDecimal?, // 추천 점수
val tags: List<TagType>?, // 태그 목록
val validatingCarrier: String, // 발권 항공사
val schedules: List<InternalProxyScheduleView>, // 여정 정보
val fares: List<InternalProxyFlightFareView>, // 운임 정보
val tripDirectionType: TripDirectionType?, // 여정 방향 (MIX용)
val tripType: TripType, // 여정 유형
)InternalProxyScheduleView
소스:
interfaces/response/ProxyFlightSearchView.kt:50-87
data class InternalProxyScheduleView(
val sequence: Int, // 여정 순서
val flightTime: String, // 총 비행시간
val departure: String, // 출발 공항
val arrival: String, // 도착 공항
val departureAt: LocalDateTime, // 출발 시간
val arrivalAt: LocalDateTime, // 도착 시간
val stopPoints: List<String>, // 경유지
val addDay: Int, // 추가일
val stop: Int, // 경유 횟수
val avail: Int, // 잔여 좌석
val freeBaggage: InternalProxyFreeBaggageView?, // 무료 수하물
val segments: List<InternalProxySegmentView>, // 세그먼트
)InternalProxyFlightFareView
소스:
interfaces/response/ProxyFlightSearchView.kt:143-180
data class InternalProxyFlightFareView(
val key: String, // 상세 키
val id: String, // 운임 ID
val funnel: Funnel, // 판매 퍼널
val avail: Int, // 잔여 좌석
val passengerFares: List<InternalProxyFlightPassengerFareView>, // 승객별 운임
val adultAirPrice: Long, // 성인 항공료
val adultTaxPrice: Long, // 성인 세금
val adultTotalPrice: Long, // 성인 총액
val promotionPrincipleId: Long?, // 프로모션 정책 ID
val cardPromotionName: String?, // 카드 프로모션명
val representative: Boolean?, // 대표 카드 여부
val tags: List<String>, // 태그
val lowestAdultTotalPrice: Long?, // 최저 성인 총액 (MultiTicket용)
val lowestTotalPrice: Long?, // 최저 총액 (MultiTicket용)
val identityType: IdentityType, // 신분 유형
)5. 비즈니스 로직 흐름
5.1 표준 검색 흐름 (searchFlights)
flowchart TD A[Controller 진입] --> B[SearchInfo 생성] B --> C[StandardFlightSearchUseCase.searchFlights] C --> D[SearchInfo.validate 검증] D --> E[FlightSearchService.searchFlights] E --> F[AdapterClient 호출<br/>GDS/공급사 검색] F --> G[FareItinerary 필터링] G --> H[FlightItem 생성] H --> I[FlightGroupCriteria별 그룹핑] I --> J[최적 운임 선택<br/>pickOptimalFlight] J --> K{useRecommendation?} K -->|Yes| L[추천 점수 적용] K -->|No| M[InternalProxyFlightItemView 변환] L --> M M --> N[ResponseEntity 반환]
상세 코드 흐름
// FlightSearchProxyController.kt:321-357
private fun searchFlights(...): ResponseEntity<List<InternalProxyFlightItemView>> {
// 1. SearchInfo 생성
val searchInfo = SearchInfo(
adult = adult,
child = child,
infant = infant,
freeBaggageOnly = freeBaggageOnly,
cabins = cabins,
originDestinationLocationInfos = originDestinationLocationInfo.toList(),
useCache = useCache,
)
// 2. UseCase 호출
val flightItems = standardFlightSearchUseCase.searchFlights(
searchInfo = searchInfo,
onlyDirect = false,
useRecommendation = useRecommendation,
flightGroupCriteria = flightGroupCriteria,
)
// 3. View 변환 및 반환
return ResponseEntity.ok(
flightItems.map {
InternalProxyFlightItemView.of(
flightItem = it,
tripType = searchInfo.getTripType(),
)
}
)
}5.2 Split 검색 흐름 (searchSplitFlights)
flowchart TD A[Controller 진입<br/>useMultiTicket=true] --> B[SearchInfo 생성<br/>MultiTicket 포함] B --> C[listKey 생성/사용] C --> D[SplitFlightSearchUseCase.searchFlights] D --> E{cachedListKey 존재?} E -->|Yes| F[캐시에서 FlightItems 조회] E -->|No| G[FlightSearchService 검색] F --> H[splitMultiTicketSearchFlights] G --> H H --> I{searchTripDirectionType?} I -->|INBOUND| J[앵커 기반 오는편 필터링] I -->|OUTBOUND| K[가는편만 반환] I -->|null| L[전체 반환<br/>가는편+오는편] J --> M[그룹핑 및 최적 운임 선택] K --> M L --> M M --> N[cached-search-key 헤더 설정] N --> O[ResponseEntity 반환]
상세 코드 흐름
// FlightSearchProxyController.kt:359-408
private fun searchSplitFlights(...): ResponseEntity<List<InternalProxyFlightItemView>> {
val searchInfo = SearchInfo(
...
multiTicket = MultiTicket(
searchTripDirectionType = searchTripDirectionType,
detailKey = detailKey,
promotionPrincipleId = promotionPrincipleId,
cachedListKey = cachedListKey,
),
...
)
val listKey = cachedListKey ?: CacheKeyGenerator.getFlightSearchCacheKey()
val flightItems = splitFlightSearchUseCase.searchFlights(
listKey = listKey,
searchInfo = searchInfo,
flightGroupCriteria = flightGroupCriteria,
)
return ResponseEntity(
flightItems.map { ... },
HttpHeaders().apply { set(Constants.CACHED_SEARCH_KEY, listKey.toString()) },
HttpStatus.OK,
)
}6. useMultiTicket 옵션 분석 (Split 검색)
6.1 개요
MultiTicket(Split) 검색은 왕복 항공권을 가는편과 오는편을 별도로 검색하여 사용자에게 더 많은 조합 선택지를 제공하는 기능입니다.
6.2 검색 단계
sequenceDiagram participant Client participant Controller participant SplitUseCase participant FlightService participant Cache Note over Client,Cache: 1단계: 전체/가는편 검색 Client->>Controller: searchRoundTrip(useMultiTicket=true) Controller->>SplitUseCase: searchFlights(searchTripDirectionType=null/OUTBOUND) SplitUseCase->>FlightService: searchFlights(useMultiTicket=true) FlightService-->>SplitUseCase: MIX + RoundTrip FlightItems SplitUseCase->>Cache: save(listKey, flightItems) SplitUseCase-->>Controller: 가는편 목록 (lowestTotalPrice 포함) Controller-->>Client: Response + cached-search-key 헤더 Note over Client,Cache: 2단계: 오는편 검색 (가는편 선택 후) Client->>Controller: searchRoundTrip(useMultiTicket=true, searchTripDirectionType=INBOUND, detailKey=선택한가는편키) Controller->>SplitUseCase: searchFlights(searchTripDirectionType=INBOUND) SplitUseCase->>Cache: getFlightItems(cachedListKey) Cache-->>SplitUseCase: 캐시된 FlightItems SplitUseCase->>SplitUseCase: filterAnchorMatchedFlightItems SplitUseCase-->>Controller: 오는편 목록 Controller-->>Client: Response
6.3 MIX vs RoundTrip 항공편
// SplitFlightSearchUseCase.kt:58-114
fun splitMultiTicketSearchFlights(...): List<FlightItem> {
// MIX 항공편과 왕복 항공편 분리
val (mixFlightItems, roundTripFlightItems) = flightItems.partition { it.isMix }
// MIX 항공편을 가는편/오는편으로 분리
val (mixOutboundFlightItems, mixInboundFlightItems) = mixFlightItems
.map { flightItem ->
// MIX 항공권은 성인 기본 PTC만 허용
flightItem.copy(fares = flightItem.fares.filter { it.identityType.isDefault })
}
.partition { it.tripDirectionType == TripDirectionType.OUTBOUND }
return when (searchTripDirectionType) {
SearchTripDirectionType.INBOUND -> {
// 앵커(선택된 가는편) 기반 오는편 필터링
...
}
else -> {
// 전체/가는편 검색
...
}
}
}6.4 최저가 계산 (가는편용)
// SplitFlightSearchUseCase.kt:123-147
private fun calculateLowestFare(
outboundFlightItems: List<FlightItem>,
inboundFlightItems: List<FlightItem>,
) {
// 오는편의 카드프로모션별 최저가 계산
val inboundLowestFareMap = inboundFlightItems
.flatMap { it.fares }
.groupBy { it.cardPromotionId }
.mapValues { (_, fares) -> fares.minBy { it.adultFare.total } }
// 가는편 운임에 예상 최저 총액 설정
outboundFlightItems.forEach { outboundFlightItem ->
outboundFlightItem.fares.forEach { outboundFare ->
val inboundLowestFares = inboundLowestFareMap.getOrDefault(
outboundFare.cardPromotionId,
inboundLowestFareMap.getValue(null)
)
outboundFare.lowestAdultTotalPrice =
outboundFare.adultFare.total + inboundLowestFares.adultFare.total
outboundFare.lowestTotalPrice =
outboundFare.totalPrice + inboundLowestFares.totalPrice
}
}
}6.5 연결 시간 검증
// domain/FlightItem.kt:801-806
fun isValidConnectionTime(
outbound: FlightItem,
inbound: FlightItem,
): Boolean {
// 가는편 도착시간과 오는편 출발시간 차이가 3시간 이상이어야 함
return outbound.inboundArrivalAt differ inbound.outboundDepartureAt >= Duration.ofHours(Constants.FLIGHT_INTERVAL_HOURS)
}소스:
support/Constants.kt:14-FLIGHT_INTERVAL_HOURS = 3L
7. 에러 처리 및 예외 상황
7.1 SearchInfo 유효성 검사
소스:
support/model/SearchInfo.kt:46-98
fun validate(bookableDateRange: LongRange) {
checkBookableDate(bookableDateRange) // 예약 가능 날짜
checkSearchableDates() // 날짜 순서
checkSearchablePassengers(adult, child, infant) // 승객 수
checkSearchableItinerary() // 여정 유효성
checkMultiTicketSearch() // MultiTicket 검증
}7.2 주요 예외 케이스
| 예외 상황 | MessageKey | 설명 | 소스 위치 |
|---|---|---|---|
| 잘못된 캐시 키 | INVALID_CACHE_KEY | 존재하지 않는 캐시 키 | MessageKey.kt:5 |
| 잘못된 파라미터 | INVALID_PARAMETER | 유효하지 않은 요청 파라미터 | MessageKey.kt:6 |
| 승객 수 오류 | INVALID_PASSENGERS | 승객 수 규칙 위반 | MessageKey.kt:7 |
| 날짜 오류 | INVALID_DATES | 출발일 > 귀국일 | MessageKey.kt:13 |
| 예약 불가 날짜 | INVALID_BOOKABLE_DATE | 예약 가능 범위 벗어남 | MessageKey.kt:14 |
| 잘못된 여정 | INVALID_ITINERARY | 출발지=도착지, 국내선 등 | MessageKey.kt:15 |
| 제한 국가 | INVALID_ITINERARY_RESTRICTED_COUNTRY | 검색 제한 국가 (SY, IR, UA, CU) | MessageKey.kt:16 |
| 검색 실패 | SEARCH_FAILED | 공급사 검색 실패 | MessageKey.kt:22 |
7.3 MultiTicket 검색 검증
// support/model/SearchInfo.kt:86-98
private fun checkMultiTicketSearch() {
multiTicket?.let {
// INBOUND 검색 시 detailKey와 cachedListKey 필수
if (it.searchTripDirectionType == SearchTripDirectionType.INBOUND
&& (it.detailKey == null || it.cachedListKey == null)) {
throw MethodArgumentInvalidException(MessageKey.INVALID_PARAMETER)
}
// OUTBOUND/전체 검색 시 detailKey 불허
if (it.detailKey != null && it.searchTripDirectionType != SearchTripDirectionType.INBOUND) {
throw MethodArgumentInvalidException(MessageKey.INVALID_PARAMETER)
}
}
}7.4 제한 국가
// support/model/OriginDestinationLocationInfo.kt:45-47
fun isRestrictedCountry(): Boolean {
return setOf("SY", "IR", "UA", "CU").contains(countryCode)
}| 국가 코드 | 국가명 | 제한 사유 |
|---|---|---|
| SY | 시리아 | 제재 국가 |
| IR | 이란 | 제재 국가 |
| UA | 우크라이나 | 분쟁 지역 |
| CU | 쿠바 | 제재 국가 |
8. 관련 도메인 모델 분석
8.1 SearchInfo
소스:
support/model/SearchInfo.kt:15-106
data class SearchInfo(
val adult: Int, // 성인 수
val child: Int, // 소아 수
val infant: Int, // 유아 수
val freeBaggageOnly: Boolean, // 무료 수하물 필터
val cabins: Set<CabinType>, // 좌석 등급
val originDestinationLocationInfos: List<OriginDestinationLocationInfo>, // 여정 정보
val multiTicket: MultiTicket? = null, // MultiTicket 설정
val useCache: Boolean, // 캐시 사용
) {
val key: String // 검색 조건 식별 키
fun validate(bookableDateRange: LongRange) // 유효성 검사
fun getTripType(): TripType // 여정 유형 반환
}8.2 MultiTicket
소스:
support/model/SearchInfo.kt:108-113
data class MultiTicket(
val searchTripDirectionType: SearchTripDirectionType?, // 검색 방향
val detailKey: String?, // 앵커 항공편 키
val promotionPrincipleId: Long?, // 프로모션 정책
val cachedListKey: UUID?, // 캐시 키
)8.3 OriginDestinationLocationInfo
소스:
support/model/OriginDestinationLocationInfo.kt:11-30
data class OriginDestinationLocationInfo(
val origin: LocationInfo, // 출발지 정보
val destination: LocationInfo, // 도착지 정보
val date: LocalDate, // 출발일
) {
fun isDomesticRoute(): Boolean // 국내선 여부
fun isRestrictedRoute(): Boolean // 제한 국가 여부
fun isSameOriginDestination(): Boolean // 출발지=도착지 여부
}8.4 LocationInfo (Sealed Class)
소스:
support/model/OriginDestinationLocationInfo.kt:32-93
sealed class LocationInfo(val type: LocationType) {
abstract val iata: String // IATA 코드
abstract val airportIatas: Set<String> // 공항 IATA 목록
abstract val cityIata: String // 도시 IATA 코드
abstract val countryCode: String // 국가 코드
abstract val zoneId: ZoneId // 타임존
data class City(...) : LocationInfo(LocationType.CITY)
data class Airport(...) : LocationInfo(LocationType.AIRPORT)
}8.5 FlightItem
소스:
domain/FlightItem.kt:21-329
data class FlightItem(
val listKey: UUID, // 검색 리스트 키
val supplier: String, // 공급사
val scheduleKey: String, // 스케줄 키
val validatingCarrier: String, // 발권 항공사
val schedules: List<Schedule>, // 여정 목록
val fares: List<FlightFare>, // 운임 목록
var score: BigDecimal?, // 추천 점수
var tags: List<RecommendationTag>?, // 추천 태그
val tripDirectionType: TripDirectionType?, // 여정 방향 (MIX용)
val prepayment: Boolean?, // 선결제 여부
val groupKey: String = "", // 그룹 키
) {
val isMix: Boolean // MIX 항공편 여부
val lowestFare: FlightFare // 최저가 운임
val isDirect: Boolean // 직항 여부
val maxStop: Int // 최대 경유 횟수
}8.6 TripType
소스:
support/enums/TripType.kt:3-18
enum class TripType {
ONE_WAY, // 편도
ROUND_TRIP, // 왕복
MULTI_CITY; // 다구간
}8.7 TripDirectionType
소스:
support/enums/TripDirectionType.kt:3-6
enum class TripDirectionType {
OUTBOUND, // 가는편 (MIX 항공편용)
INBOUND, // 오는편 (MIX 항공편용)
}9. 처리 흐름 다이어그램
9.1 전체 시스템 흐름
flowchart TB subgraph Controller["FlightSearchProxyController"] A1[searchOneWayTrip] A2[searchRoundTrip] A3[searchMultiCityTrip] end subgraph UseCase["UseCase Layer"] B1[StandardFlightSearchUseCase] B2[SplitFlightSearchUseCase] end subgraph Service["Service Layer"] C1[FlightSearchService] C2[LocationService] C3[AirConsoleService] end subgraph Infrastructure["Infrastructure"] D1[AdapterClient<br/>GDS/공급사] D2[PricingClient<br/>가격정책] D3[FlightItemRepository<br/>캐시] end A1 --> B1 A2 -->|useMultiTicket=false| B1 A2 -->|useMultiTicket=true| B2 A3 --> B1 B1 --> C1 B2 --> C1 Controller --> C2 C1 --> C3 C1 --> D1 C1 --> D2 C1 --> D3 B2 --> D3
9.2 Response 헤더 처리
| 검색 유형 | cached-search-key 헤더 |
|---|---|
| 표준 검색 (편도/왕복/다구간) | 없음 |
| Split 검색 (useMultiTicket=true) | 항상 포함 |
// FlightSearchProxyController.kt:396-406
return ResponseEntity(
flightItems.map { ... },
HttpHeaders().apply {
set(Constants.CACHED_SEARCH_KEY, listKey.toString())
},
HttpStatus.OK,
)10. 참고 사항
10.1 캐시 키 생성
// support/cache/CacheKeyGenerator.kt:5-8
object CacheKeyGenerator {
fun getFlightSearchCacheKey(): UUID {
return UUID.randomUUID()
}
}10.2 관련 문서
- StandardFlightSearchUseCase - 표준 검색 UseCase 상세
- SplitFlightSearchUseCase - Split 검색 UseCase 상세
- FlightSearchService - 검색 서비스 상세
- FlightItem - 항공편 도메인 모델
변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 1.0 | 2025-12-15 | Claude | 최초 작성 |