“이 클래스를 고치면 무엇이 깨지는가?”를 답하기 위한 노트다. air-intl-adapter에는 중앙 디스패처가 없으므로, 의존성은 공급사별 컨트롤러 → 서비스 → 클라이언트라는 거의 동일한 모양으로 11번 반복된다. 이 노트는 (1) 그 반복 패턴의 골격, (2) 공급사 내부 서비스끼리의 호출(가장 헷갈리는 부분), (3) 모든 공급사가 공유하는 횡단 컴포넌트를 추적해 변경 영향도(blast radius) 를 산정한다.
flowchart TD
Triple["Triple 예약 시스템<br/>유일한 호출자 (내부 API 소비자)"]
IF["interfaces (컨트롤러 + DTO)<br/>NameOpController : @RestController, @CircuitBreaker<br/>DTO(SearchRequest 등)를 도메인 모델(Passenger 등)로 변환"]
APP["application (서비스) 비즈니스 오케스트레이션<br/>NameFlightSearchService, NameBookingService, ...<br/>서비스끼리 호출(예: Booking에서 Cancel에서 Pricing)<br/>공유 서비스 호출(SlackService, CalculateTimezoneService)<br/>공유 리포지토리 호출(Redis)"]
INFRA["infrastructure (클라이언트)<br/>NameClient : ClientSupport<br/>외부 GDS/NDC/LCC HTTP/SOAP"]
REPO["domain.repository (Redis)<br/>FlightSearchKeyRepository ...<br/>공급사 공유 캐시"]
Triple -->|"HTTP POST/GET /internals/SUPPLIER/op"| IF
IF -->|"생성자 주입 (@Service)"| APP
APP --> INFRA
APP --> REPO
DI 와이어링은 100% 생성자 주입
모든 컴포넌트가 Kotlin 주생성자 파라미터로 의존성을 받는다(@Autowired 필드 주입 없음). Spring은 @Service / @Component / @RestController 스캔으로 빈을 만들고 타입으로 매칭한다. 따라서 “누가 무엇을 호출하는가”는 곧 “어떤 클래스의 생성자에 무엇이 들어있는가” 와 같다 — 이 노트의 표는 전부 생성자 시그니처에서 추출했다.
2. 반복되는 골격 패턴 (11개 공급사 공통)
오퍼레이션별 컨트롤러는 정확히 동일한 모양을 11번 복제한다. 한 번 익히면 나머지 10개는 이름만 바뀐다.
오퍼레이션
컨트롤러
매핑 경로
주입받는 서비스(콜리)
Search
{Name}SearchController
/internals/{SUPPLIER}/search
{Name}FlightSearchService + 공유 FlightAmenityService
AmadeusSearchController.kt:21-24 와 SabreSearchController 의 생성자가 글자 그대로 같은 구조다.
@RestController@RequestMapping("/internals/AMADEUS/search")class AmadeusSearchController( private val flightSearchService: AmadeusFlightSearchService, // 공급사 전용 콜리 private val flightAmenityService: FlightAmenityService, // 공유 콜리)
컨트롤러가 PassengerService를 직접 부른다 (서비스 우회 아님, 의도된 구조)
APIS(여권/탑승객 정보) 변경처럼 예약 본문(Booking)과 독립된 작업은 PassengerService로 분리되어 컨트롤러가 직접 호출한다. 예: AmadeusBookingController.changeApis() → passengerService.changeApis(...)(AmadeusBookingController.kt:45). SabrePassengerService는 이 안에서 코루틴(runBlocking) 으로 APIS를 병렬 처리한다(SabrePassengerService.kt:19의 runBlocking). 자세한 비동기 흐름은 코루틴 참조.
3. 대표 공급사 상세 의존성 맵
3-1. Amadeus (가장 큰 모듈, GDS/SOAP stateful) — 서비스 간 의존이 가장 복잡
허브 서비스를 조심하라: AmadeusRetrieveService, AmadeusPricingService, AmadeusCancelService
이 3개는 Booking/Ticketing/Cancel/Refund 여러 곳에서 공유 호출된다(위 표의 “콜리” 열에 반복 등장). 즉 Amadeus 모듈 안의 횡단 컴포넌트다. AmadeusRetrieveService.retrieve()의 시그니처/PNR 파싱 로직을 바꾸면 예약·발권·취소·환불이 한꺼번에 영향받는다. 변경 전 콜러를 전부 grep하라:
다른 공급사는 FlightAmenityService를 컨트롤러(Search) 에서 주입하는데, Sabre는 SabreFlightSearchService 생성자에 직접 넣었다(SabreFlightSearchService.kt 생성자). 동작은 같지만 위치가 다르므로, “어메니티 저장 시점을 바꿔달라”는 요청이 오면 Sabre는 서비스 안, 나머지는 컨트롤러를 봐야 한다.
3-3. Tway (LCC/REST) — 가장 단순, 단일 클라이언트 위주
LCC는 stateless REST라 의존이 얕다. 거의 모든 서비스가 TwayClient 하나만 본다.
domain/repository/* 는 @Component로 등록되어 모든 공급사 서비스가 직접 주입한다. (Spring Data 인터페이스가 아니라 RedisTemplate를 감싼 일반 컴포넌트다.)
리포지토리
주입 횟수(대략)
누가 쓰나
근거
FlightSearchKeyRepository
~25
거의 모든 Search/Booking 서비스 (requestKey→fareKey 캐시)
domain/repository/FlightSearchKeyRepository.kt
UnexposedFareItineraryRepository
~18
Search/Booking 서비스 (노출 안 된 운임 임시 저장)
동 패키지
FareRuleRepository
~11
각 공급사 FareRule 서비스
동 패키지
FlightAmenityRepository
1 (간접)
FlightAmenityService만 직접 사용
동 패키지
캐시 키 포맷 변경 = 전 공급사 동시 장애
CacheKeyGenerator (컨트롤러에서 generateSearchRequestKey 호출, 예: AmadeusSearchController.kt:34)나 CacheSet의 cacheName/ttl을 바꾸면 검색→예약 사이의 키 매칭이 전 공급사에서 동시에 깨진다. 검색 시 저장한 키를 예약 시 못 찾으면 “운임 만료”로 처리되어 예약이 막힌다. support-common에서 키 생성 규칙 확인.
4-2. 모든 클라이언트의 공통 부모: ClientSupport
11개 공급사의 23개 클라이언트가 모두support/web/ClientSupport.kt(추상 클래스)를 상속한다. 이것이 유일한 공유 베이스클래스다(컨트롤러·서비스에는 공유 베이스/인터페이스 없음 — 전부 독립 클래스).
flowchart TD
Base["ClientSupport (abstract)<br/>OkHttpClient(searchClient/defaultClient) 생성, 타임아웃 정책<br/>String.get / post / put / delete 확장 to OkHttpRequestBuilder<br/>execute RES : Result RES, OkHttpError (성공/실패 래핑)<br/>handleSoapFaultException (SOAP 공통 처리)"]
G1["AmadeusClient, ArtClient, GpsClient"]
G2["SabreClient, SabreRestClient, SabreFareRuleClient, SabrePaymentClient"]
G3["GalileoClient, GalileoRestClient, KpsPaymentClient, KrtClient"]
G4["TwayClient, TwayAncillaryClient, JinairClient, JejuairClient"]
G5["KoreanairClient, KoreanairPaymentClient, LufthansaClient, SingaporeairClient"]
G6["AmadeusndcClient, NdcArtClient, GpsClient(ndc), GroupairClient"]
G7["CityClient (공유 인프라)"]
G1 -->|"상속"| Base
G2 -->|"상속"| Base
G3 -->|"상속"| Base
G4 -->|"상속"| Base
G5 -->|"상속"| Base
G6 -->|"상속"| Base
G7 -->|"상속"| Base
ClientSupport 변경의 blast radius = 전체
execute()의 응답 파싱(objectMapper.readValue), OkHttpError.isTimeout 판정, 타임아웃 기본값(searchTimeout=30000, defaultTimeout=60000), 또는 LoggingAndCompressionInterceptor 부착 로직을 바꾸면 23개 클라이언트 전부가 영향받는다. 특히 isTimeout은 Resilience4j 리트라이/서킷브레이커 동작과 직결된다(resilience-and-events). 여기를 만질 때는 GDS(SOAP)·NDC(XML)·LCC(JSON) 응답을 모두 회귀 테스트해야 한다.
5. “이 변경이 무엇을 깨뜨리는가” — 영향도 체크리스트
변경 대상별 파급 범위를 빠르게 판단하는 표. 위로 갈수록 위험.
변경 대상
blast radius
깨지는 것
변경 전 필수 grep
ClientSupport (execute/timeout/isTimeout)
전 공급사 23 클라이언트
모든 외부 호출의 파싱·타임아웃·서킷브레이커
: ClientSupport(
공유 Redis 리포지토리 키/TTL/CacheSet
전 공급사
검색→예약 키 매칭, 운임 만료
Repository( 주입처 + CacheKeyGenerator
SlackService 메서드 시그니처
10개 공급사 32 클래스
운영 경보(컴파일 에러로 즉시 발견 가능)
slackService: SlackService
CalculateTimezoneService.calculateToUTC
Amadeus·Galileo·Sabre의 취소/발권
TTL·취소위약 시각 → 금액
CalculateTimezoneService
공유 DTO (SearchRequest, BookingRequest 등)
전 공급사 컨트롤러
모든 진입점 역직렬화
interfaces/request 사용처
공급사 내부 허브 서비스 (예: Amadeus RetrieveService/PricingService)
해당 공급사 전체 오퍼레이션
예약·발권·취소·환불 연쇄
RetrieveService|PricingService (모듈 내)
특정 {Name}Client
해당 공급사만
그 공급사 외부 호출
해당 모듈 내
단일 컨트롤러/오퍼레이션 서비스
그 오퍼레이션만
가장 안전
해당 파일
변경 영향도 판단 3단계 (실무 루틴)
위치 파악: 바꾸려는 클래스가 support/ · application/(루트) · domain/repository/ 에 있으면 = 횡단 공유 → 전 공급사 의심.
시그니처 변경이면 컴파일러가 잡아준다: Kotlin 생성자 주입 덕에 누락 콜러는 빌드 에러로 드러난다. 위험한 건 시그니처는 그대로인데 동작(의미)만 바뀌는 변경(예: 타임존 계산 로직, 캐시 TTL) — 이건 컴파일러가 못 잡으니 회귀 테스트로 막아야 한다. landmines 참조.
6. 빠른 참조: 콜러→콜리 추적 명령어
# 1) 특정 서비스를 주입(=호출)하는 모든 콜러 찾기grep -rn "AmadeusRetrieveService" --include=*.kt supplier/amadeus# 2) 특정 컨트롤러가 어떤 서비스를 쓰는지 (생성자 확인)sed -n '/^class AmadeusBookingController/,/) {/p' \ .../supplier/amadeus/interfaces/controller/internals/AmadeusBookingController.kt# 3) 공유 컴포넌트(SlackService)를 쓰는 전 공급사 나열grep -rl "slackService: SlackService" --include=*.kt supplier# 4) ClientSupport 상속 클라이언트 전수grep -rn ": ClientSupport(" --include=*.kt
7. 연습 문제
Q1. AmadeusRetrieveService.retrieve() 의 반환 타입을 바꾸면 어떤 클래스들이 컴파일 에러가 나는가? 어떻게 한 번에 찾는가?
정답 보기
AmadeusRetrieveService를 생성자에 주입한 콜러 전부: AmadeusBookingService, AmadeusCancelService, AmadeusTicketingService, AmadeusRefundService (모두 위 3-1 표의 “콜리” 열에 등장). 찾는 법: grep -rn "retrieveService\." supplier/amadeus. Kotlin 생성자 주입이라 시그니처를 바꾸면 빌드가 즉시 실패해 누락 콜러가 노출된다. 단, Amadeus 모듈 안에만 있으므로 다른 공급사는 영향 없음.
Q2. 신입이 "검색은 되는데 예약 시 항상 '운임 만료'가 난다"는 버그를 받았다. 콜러/콜리 관점에서 가장 먼저 의심할 공유 컴포넌트는?
정답 보기
CacheKeyGenerator(검색 컨트롤러에서 generateSearchRequestKey 호출)와 FlightSearchKeyRepository. 검색이 requestKey→fareKey를 Redis에 저장(addKey)하고 예약이 findKey로 되찾는데, 키 포맷이 어긋나거나 TTL이 짧으면 예약 단계에서 못 찾아 “운임 만료”가 된다. 이건 전 공급사 공유이므로 한 공급사만의 문제가 아닐 가능성을 함께 본다. → support-common, request-flow.
Q3. 왜 컨트롤러·서비스에는 공유 베이스클래스가 없는데 클라이언트에만 ClientSupport가 있을까?
정답 보기
컨트롤러/서비스의 “공통점”은 구조(레이어 위치)일 뿐 실제 로직은 공급사마다 완전히 다르다(상속할 공통 동작이 없음 → 상속하면 오히려 결합도만 높아짐). 반면 클라이언트의 공통점은 구체적 인프라 동작이다: OkHttp 호출, 타임아웃, 응답 파싱, 에러 래핑, 로깅 인터셉터 — 이건 11개 공급사가 똑같이 필요하다. “공통 동작이 실재할 때만 상속”이라는 좋은 설계 신호다. 자세히는 support-common, error-handling.