Phase 1: 아마데우스 검색 API 심층 분석

1. API 엔드포인트 개요

1.1 검색 API

  • 엔드포인트: POST /internals/AMADEUS/search
  • Controller: AmadeusSearchController.kt:25-53
  • Service: AmadeusFlightSearchService.kt:39-120
  • Client: AmadeusClient.kt:114-195

1.2 상세 조회 API

  • 엔드포인트: GET /internals/AMADEUS/search
  • Controller: AmadeusSearchController.kt:55-70

2. 핵심 비즈니스 로직 플로우

2.1 전체 처리 흐름도

flowchart TD
    A[요청 수신] --> B{Circuit Breaker<br/>체크}
    B -->|정상| C[캐시 키 생성]
    B -->|OPEN| D[빈 리스트 반환<br/>Fallback]
    C --> E{Redis 캐시<br/>확인}
    E -->|캐시 히트| F[캐시된 결과 반환]
    E -->|캐시 미스| G[공항 코드 확장]
    G --> H[Cartesian Product<br/>모든 조합 생성]
    H --> I[병렬 검색 실행<br/>pmap]
    I --> J[SOAP 요청<br/>FareMasterPricerTravelBoardSearch]
    J --> K[결과 병합<br/>flatten]
    K --> L[중복 제거<br/>distinctBy]
    L --> M[필터링<br/>출발/도착 검증]
    M --> N{캐싱 조건<br/>확인}
    N -->|useCache=true &<br/>결과 존재| O[Redis 저장]
    N -->|조건 미충족| P[응답 반환]
    O --> P[응답 반환]

     다크/라이트 모드 호환 색상
    style B fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style D fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style E fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000

조건 판단 코드

위치: AmadeusFlightSearchService.kt:40-44

useMultiTicket = if (request.preferences.all { it.fareFamilies.isNullOrEmpty() }) {
    request.useMultiTicket  // fareFamilies가 없을 때만 multi-ticket 사용
} else {
    false
}

필터링 로직

위치: AmadeusFlightSearchService.kt:83-97

.filter { fareItinerary ->
    originDestinationLocationInfos.all {
        val index = when {
            useMultiTicket && fareItinerary.tripDirectionType != null -> 0
            else -> originDestinationLocationInfos.indexOf(it)
        }
 
        val originDestinationLocationInfo = when (fareItinerary.tripDirectionType) {
            TripDirectionType.OUTBOUND -> originDestinationLocationInfos.first()
            TripDirectionType.INBOUND -> originDestinationLocationInfos.last()
            else -> it
        }
 
        // 출발/도착 공항 일치 확인
        val segments = fareItinerary.schedules[index].segments
        originDestinationLocationInfo.origin.isSameCode(segments.first().departure)
            && originDestinationLocationInfo.destination.isSameCode(segments.last().arrival)
    }
}

4. 성능 최적화 전략

4.1 병렬 처리

위치: AmadeusFlightSearchService.kt:58-79

withBlocking(Dispatchers.IO) {
    AirportUtils.makeOriginDestinations(originDestinationLocationInfos)
        .cartesianProduct()
        .pmap { /* 병렬 실행 */ }
        .onFailure { exceptions, successes ->
            if (successes.isEmpty()) {
                throw InternationalAdapterException(ErrorMessage.SEARCH_FAILED, exceptions.first())
            }
        }
        .getOrEmpty()
}

4.2 캐싱 전략

캐시 저장 조건

위치: AmadeusFlightSearchService.kt:100-108

.also {
    if (useCache && it.isNotEmpty()) {
        flightSearchKeyRepository.set(requestKey = requestKey, key = key)
        fareItineraryRepository.setFareItineraries(
            key = key,
            fareItineraries = it
        )
    }
}

캐시 TTL

  • 검색 결과: 30분 (Redis 설정)
  • 키 구조: AMADEUS_{channel}_{requestHash}_{timestamp}

4.3 중복 제거

위치: AmadeusFlightSearchService.kt:81

.flatten()
.distinctBy { it.id }  // ID 기준 중복 제거

5. 에러 처리 및 복구

에러 처리 플로우

flowchart TD
    A[검색 요청 실행] --> B{Circuit Breaker<br/>상태 확인}
    B -->|CLOSED| C[정상 실행]
    B -->|OPEN| D[즉시 Fallback<br/>빈 리스트 반환]
    B -->|HALF_OPEN| E[테스트 실행]

    C --> F{실행 결과}
    F -->|성공| G[결과 반환]
    F -->|실패| H{실패율 계산<br/>35% 초과?}
    H -->|예| I[Circuit Breaker<br/>OPEN 전환]
    H -->|아니오| J[실패 카운트 증가]

    E --> K{테스트 결과}
    K -->|성공| L[CLOSED 전환]
    K -->|실패| M[OPEN 유지]

    I --> N[120초 대기 후<br/>HALF_OPEN 전환]

    C --> O{병렬 요청 실패 시}
    O -->|모두 실패| P[예외 발생]
    O -->|부분 실패| Q[성공한 결과만<br/>사용]

    %% 다크/라이트 모드 호환 색상
    style D fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style I fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style P fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
    style Q fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000

5.1 Circuit Breaker 설정

위치: application.yml

resilience4j.circuitbreaker:
  instances:
    amadeusSearch:
      slidingWindowSize: 180        # 180개 요청 기준
      failureRateThreshold: 35      # 실패율 35% 초과 시 OPEN
      waitDurationInOpenState: 120s # OPEN 상태 120초 유지
      slowCallRateThreshold: 90     # Slow call 비율 90%
      slowCallDurationThreshold: 15s # 15초 이상 시 slow call

5.2 Fallback 메커니즘

위치: AmadeusSearchController.kt:73-80

private fun searchFallback(exception: CallNotPermittedException): ResponseEntity<List<FareItineraryView>> {
    val span = GlobalTracer.get().activeSpan()
    if (span != null) {
        span.setTag("supplier.circuit-breaker", "OPEN")
        (span as? MutableSpan)?.localRootSpan?.setTag("supplier.circuit-breaker", "OPEN")
    }
    return ResponseEntity.ok(emptyList())  // 빈 결과 반환
}

5.3 부분 실패 처리

위치: AmadeusFlightSearchService.kt:75-79

.onFailure { exceptions, successes ->
    if (successes.isEmpty()) {
        // 모든 요청 실패 시만 예외 발생
        throw InternationalAdapterException(ErrorMessage.SEARCH_FAILED, exceptions.first())
    }
    // 부분 성공 시 성공한 결과만 사용
}

6. 타임아웃 설정

6.1 검색 타임아웃

위치: AmadeusClient.kt:95

searchTimeout = 25000  // 25초

6.2 기본 타임아웃

defaultTimeout = 60000  // 60초

7. 필터링 조건

7.1 직항 필터

위치: AmadeusFlightSearchService.kt (파라미터)

onlyDirect: Boolean  // true 시 직항만 검색

7.2 무료 수하물 포함 필터

onlyFreeBaggageInclude: Boolean  // true 시 무료 수하물 포함 항공편만

7.3 항공사 필터

airlines: List<String>?      // 특정 항공사만 검색
sotoAirlines: List<String>?  // 제외할 항공사

8. 로깅 및 모니터링

8.1 검색 로깅

위치: AmadeusClient.kt:113

private val enableSearchLog = supplierLoggingProperties.search.contains(Supplier.AMADEUS)

8.2 MDC 컨텍스트

MDC_SUPPLIER_KEY -> "AMADEUS"
MDC_FARE_KEY -> fareKey
MDC_SCHEDULE_KEY -> scheduleKey
MDC_VALIDATING_CARRIER_KEY -> validatingCarrier

9. 주요 예외 사항 및 주의점

9.1 FareFamilies 존재 시 Multi-ticket 비활성화

  • 이유: FareFamilies 옵션과 Multi-ticket은 상호 배타적
  • 위치: AmadeusFlightSearchService.kt:40-44

9.2 부분 실패 허용

  • 정책: 일부 요청 성공 시 성공한 결과만 반환
  • 전체 실패 시만 예외 발생

9.3 특정 에러 코드 무시

  • 866, 931, 977, 996, 118, 950: 운임 관련 경미한 오류로 무시

10. 트러블슈팅 가이드

문제 1: Circuit Breaker OPEN

증상: 검색 결과가 항상 빈 배열 해결:

  1. Circuit Breaker 상태 확인
  2. 실패율 확인 (35% 미만인지)
  3. 120초 대기 후 자동 복구

문제 2: 캐시 미스 발생

증상: 동일 검색이 매번 느림 해결:

  1. Redis 연결 상태 확인
  2. TTL 설정 확인
  3. 캐시 키 생성 로직 확인

문제 3: MH 항공사 예약 불가

증상: MH 항공사 포함 시 오류 원인: Non-ticketable carrier 체크 해결: 마케팅 항공사 일치 여부 확인


다음 Phase

Phase 2: 예약 API 심층 분석으로 진행