콜러/콜리 의존성 맵

arch-overview pattern-layered pattern-di

이 노트의 목적

“이 클래스를 고치면 무엇이 깨지는가?”를 답하기 위한 노트다. air-intl-adapter에는 중앙 디스패처가 없으므로, 의존성은 공급사별 컨트롤러 → 서비스 → 클라이언트라는 거의 동일한 모양으로 11번 반복된다. 이 노트는 (1) 그 반복 패턴의 골격, (2) 공급사 내부 서비스끼리의 호출(가장 헷갈리는 부분), (3) 모든 공급사가 공유하는 횡단 컴포넌트를 추적해 변경 영향도(blast radius) 를 산정한다.

선행 학습: 시스템 아키텍처 개관, 요청 흐름. 후속: 공통 오퍼레이션, 공통 지원 컴포넌트, DTO.


1. 큰 그림: 4계층 단방향 의존

모든 호출은 위에서 아래로만 흐른다. 역방향 의존(클라이언트가 서비스를 호출)은 없다.

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
Booking{Name}BookingController/internals/{SUPPLIER}/bookings{Name}BookingService (+ {Name}PassengerService, {Name}CancelService)
Ticketing{Name}TicketingController/internals/{SUPPLIER}/tickets{Name}TicketingService
FareRule{Name}FareRuleController/internals/{SUPPLIER}/fare-rules{Name}FareRuleService + {Name}FlightSearchService
Queue{Name}QueueController/internals/{SUPPLIER}/queues{Name}QueueService
CashReceipt{Name}CashReceiptController/internals/{SUPPLIER}/cash-receipts{Name}CashReceiptService
Ancillary{Name}AncillaryController(LCC/NDC 일부){Name}AncillaryService
AgencyCredit{Name}AgencyCreditController(LCC 일부){Name}AgencyCreditService

골격 검증 (실제 코드)

AmadeusSearchController.kt:21-24SabreSearchController 의 생성자가 글자 그대로 같은 구조다.

@RestController
@RequestMapping("/internals/AMADEUS/search")
class AmadeusSearchController(
    private val flightSearchService: AmadeusFlightSearchService, // 공급사 전용 콜리
    private val flightAmenityService: FlightAmenityService,      // 공유 콜리
)

Sabre도 동일: SabreSearchController(flightSearchService: SabreFlightSearchService, flightAmenityService: FlightAmenityService).

2-1. 컨트롤러가 2~3개 서비스를 주입하는 경우

대부분의 컨트롤러는 서비스 1개만 주입하지만, Booking 컨트롤러는 예외적으로 여러 서비스를 직접 주입한다. 컨트롤러가 직접 cancel/changeApis를 호출하기 때문이다.

컨트롤러주입 서비스근거
AmadeusBookingControllerAmadeusBookingService, AmadeusPassengerService, AmadeusCancelServiceAmadeusBookingController.kt:20-23
SabreBookingControllerSabreBookingService, SabrePassengerService, SabreCancelServiceSabreBookingController.kt:19-21
TwayBookingControllerTwayBookingService, TwayPassengerService, TwayCancelServicegrep 확인
JejuairBookingControllerJejuairFlightSearchService, JejuairBookingService, JejuairCancelServicegrep 확인
AmadeusndcTicketingControllerAmadeusndcBookingService, AmadeusndcTicketingServicegrep 확인

컨트롤러가 PassengerService를 직접 부른다 (서비스 우회 아님, 의도된 구조)

APIS(여권/탑승객 정보) 변경처럼 예약 본문(Booking)과 독립된 작업은 PassengerService로 분리되어 컨트롤러가 직접 호출한다. 예: AmadeusBookingController.changeApis()passengerService.changeApis(...)(AmadeusBookingController.kt:45). SabrePassengerService는 이 안에서 코루틴(runBlocking) 으로 APIS를 병렬 처리한다(SabrePassengerService.kt:19runBlocking). 자세한 비동기 흐름은 코루틴 참조.


3. 대표 공급사 상세 의존성 맵

3-1. Amadeus (가장 큰 모듈, GDS/SOAP stateful) — 서비스 간 의존이 가장 복잡

flowchart LR
    SearchC["AmadeusSearchController"]
    BookingC["AmadeusBookingController"]
    TicketingC["AmadeusTicketingController"]
    FareRuleC["AmadeusFareRuleController"]
    QueueC["AmadeusQueueController"]
    CashReceiptC["AmadeusCashReceiptController"]

    FlightSearchS["AmadeusFlightSearchService"]
    BookingS["AmadeusBookingService"]
    PassengerS["AmadeusPassengerService"]
    CancelS["AmadeusCancelService"]
    TicketingS["AmadeusTicketingService"]
    PricingS["AmadeusPricingService"]
    RetrieveS["AmadeusRetrieveService"]
    RefundS["AmadeusRefundService"]
    FareRuleS["AmadeusFareRuleService"]
    QueueS["AmadeusQueueService"]
    CashReceiptS["AmadeusCashReceiptService"]
    Timezone["CalculateTimezoneService (공유)"]
    Slack["SlackService (공유)"]

    AmadeusClient["AmadeusClient"]
    GpsClient["GpsClient"]
    ArtClient["ArtClient (TOPAS ART)"]

    SearchC --> FlightSearchS
    FlightSearchS --> AmadeusClient

    BookingC --> BookingS
    BookingC -->|"passengerService"| PassengerS
    BookingC -->|"cancelService"| CancelS
    BookingS --> AmadeusClient
    BookingS --> FlightSearchS
    BookingS --> CancelS
    BookingS --> PricingS
    BookingS --> RetrieveS
    BookingS --> Timezone
    BookingS --> Slack

    PassengerS --> AmadeusClient

    PricingS --> AmadeusClient
    RetrieveS --> AmadeusClient

    CancelS --> AmadeusClient
    CancelS --> GpsClient
    CancelS --> RefundS
    CancelS --> RetrieveS
    CancelS --> Timezone
    CancelS --> Slack
    RefundS --> AmadeusClient

    TicketingC --> TicketingS
    TicketingS --> AmadeusClient
    TicketingS --> GpsClient
    TicketingS --> PricingS
    TicketingS --> CancelS
    TicketingS --> RetrieveS
    TicketingS --> Timezone
    TicketingS --> Slack

    FareRuleC --> FareRuleS
    FareRuleS --> ArtClient

    QueueC --> QueueS
    QueueS --> AmadeusClient
    QueueS --> Slack

    CashReceiptC --> CashReceiptS
    CashReceiptS --> AmadeusClient
    CashReceiptS --> GpsClient
    CashReceiptS --> Slack
  • AmadeusFlightSearchService는 repo 재사용: FareItinerary, FlightSearchKey, Unexposed (BookingService가 FlightSearchService를 재사용 호출).
  • AmadeusRefundService는 AmadeusClient 외에 InitRefundRepo도 사용.
서비스 (콜러)호출하는 다른 서비스 (콜리)호출하는 클라이언트근거
AmadeusBookingServiceFlightSearch, Cancel, Pricing, Retrieve, CalculateTimezone, SlackAmadeusClientAmadeusBookingService.kt 생성자
AmadeusCancelServiceRefund, Retrieve, CalculateTimezone, SlackAmadeusClient, GpsClientAmadeusCancelService.kt 생성자
AmadeusTicketingServicePricing, Cancel, Retrieve, CalculateTimezone, SlackAmadeusClient, GpsClientAmadeusTicketingService.kt 생성자
AmadeusRefundServiceRetrieve, SlackAmadeusClientAmadeusRefundService.kt 생성자
AmadeusRetrieveServiceSlackAmadeusClientAmadeusRetrieveService.kt 생성자
AmadeusFlightSearchService(없음)AmadeusClientAmadeusFlightSearchService.kt:31-37

허브 서비스를 조심하라: AmadeusRetrieveService, AmadeusPricingService, AmadeusCancelService

이 3개는 Booking/Ticketing/Cancel/Refund 여러 곳에서 공유 호출된다(위 표의 “콜리” 열에 반복 등장). 즉 Amadeus 모듈 안의 횡단 컴포넌트다. AmadeusRetrieveService.retrieve()의 시그니처/PNR 파싱 로직을 바꾸면 예약·발권·취소·환불이 한꺼번에 영향받는다. 변경 전 콜러를 전부 grep하라:

grep -rn "retrieveService\.\|RetrieveService(" supplier/amadeus

3-2. Sabre (GDS/SOAP, 코루틴 사용처) — REST/SOAP 클라이언트 분리

Sabre는 클라이언트가 4개로 쪼개져 있다: SabreClient(SOAP), SabreRestClient, SabreFareRuleClient(REST), SabrePaymentClient(SOAP).

서비스 (콜러)호출 서비스 (콜리)호출 클라이언트근거
SabreFlightSearchService공유 FlightAmenityServiceSabreClient, SabreRestClientSabreFlightSearchService.kt 생성자
SabreBookingServiceSabreFlightSearchService, SabreCancelServiceSabreClientSabreBookingService.kt 생성자
SabreCancelServiceSabrePaymentService, CalculateTimezone, SlackSabreClient, SabreRestClientSabreCancelService.kt 생성자
SabreTicketingServiceSabrePaymentService, SabreCancelService, SlackSabreClientSabreTicketingService.kt 생성자
SabreFareRuleServiceSabreFlightSearchService, 공유 AirportServiceSabreFareRuleClient, SabreClientSabreFareRuleService.kt 생성자
SabrePaymentServiceSlackSabrePaymentClientSabrePaymentService.kt 생성자
SabreCashReceiptServiceSlackSabreClient, SabrePaymentClientSabreCashReceiptService.kt 생성자
SabrePassengerService(없음, runBlocking 코루틴)SabreClientSabrePassengerService.kt:9-10,19

Sabre만 유일하게 FlightAmenityService를 서비스 레이어에서 호출

다른 공급사는 FlightAmenityService컨트롤러(Search) 에서 주입하는데, Sabre는 SabreFlightSearchService 생성자에 직접 넣었다(SabreFlightSearchService.kt 생성자). 동작은 같지만 위치가 다르므로, “어메니티 저장 시점을 바꿔달라”는 요청이 오면 Sabre는 서비스 안, 나머지는 컨트롤러를 봐야 한다.

3-3. Tway (LCC/REST) — 가장 단순, 단일 클라이언트 위주

LCC는 stateless REST라 의존이 얕다. 거의 모든 서비스가 TwayClient 하나만 본다.

서비스 (콜러)호출 서비스 (콜리)호출 클라이언트근거
TwayFlightSearchServiceTwayRouteServiceTwayClientTwayFlightSearchService.kt 생성자
TwayBookingServiceTwayFlightSearchService, TwayPricingServiceTwayClientTwayBookingService.kt 생성자
TwayTicketingServiceTwayFlightSearchService, TwayBookingService, SlackTwayClientTwayTicketingService.kt 생성자
TwayAncillaryServiceTwayFlightSearchServiceTwayClient, TwayAncillaryClientTwayAncillaryService.kt 생성자
TwayCancel/Passenger/Pricing/AgencyCreditService(없음)TwayClient각 생성자
TwayRouteService(없음)TwayClient (+TwayRouteRepository)TwayRouteService.kt 생성자

GDS vs LCC 의존 깊이 차이를 외워두면 디버깅이 빨라진다

  • GDS(Amadeus/Sabre/Galileo): 서비스끼리 4~6단계로 얽힘 + 클라이언트 여러 개(SOAP/REST/Payment 분리). 세션·발권·취소가 서로 호출.
  • LCC(Tway/Jinair/Jejuair): 서비스 의존 12단계 + 클라이언트 12개. “스택트레이스가 짧다”. 장애 분석 시 GDS는 “어느 내부 서비스에서 죽었나”를, LCC는 “외부 API 응답이 뭐였나”를 먼저 본다.

4. 모든 공급사가 공유하는 횡단(cross-cutting) 컴포넌트

여기가 blast radius가 가장 큰 영역이다. 한 클래스가 여러 공급사에 동시에 박혀 있으므로, 변경 시 전 공급사 회귀 테스트가 필요하다. 상세 동작은 공통 지원 컴포넌트 참조.

공유 컴포넌트패키지콜러 (주입처)영향 범위
SlackServiceapplication/SlackService.kt10개 공급사의 Ticketing/Cancel/Queue/Refund/CashReceipt 등 32개 클래스(groupair만 미사용)경보 누락/오발송 → 운영 가시성 사고
CalculateTimezoneServiceapplication/CalculateTimezoneService.ktAmadeus(Booking/Cancel/Ticketing), Galileo(Cancel), Sabre(Cancel)TTL(발권시한)·취소위약 시각 계산 오류 → 결제/환불 금액 오류
FlightAmenityServiceinterfaces/application/FlightAmenityService.kt10개 Search 컨트롤러 + Sabre는 서비스검색 응답의 어메니티 정보
AirportServiceinterfaces/application/AirportService.ktSabreFareRuleService (현재 1곳)공항코드→공항 메타 매핑
CityClientinfrastructure/city/CityClient.ktCalculateTimezoneService, AirportService타임존/공항 조회 외부 의존
SlackClientinfrastructure/slack/SlackClient.ktSlackService 전용슬랙 전송 채널

4-1. 공유 Redis 리포지토리 (도메인 캐시 계층)

domain/repository/*@Component로 등록되어 모든 공급사 서비스가 직접 주입한다. (Spring Data 인터페이스가 아니라 RedisTemplate를 감싼 일반 컴포넌트다.)

리포지토리주입 횟수(대략)누가 쓰나근거
FlightSearchKeyRepository~25거의 모든 Search/Booking 서비스 (requestKey→fareKey 캐시)domain/repository/FlightSearchKeyRepository.kt
UnexposedFareItineraryRepository~18Search/Booking 서비스 (노출 안 된 운임 임시 저장)동 패키지
FareRuleRepository~11각 공급사 FareRule 서비스동 패키지
FlightAmenityRepository1 (간접)FlightAmenityService만 직접 사용동 패키지

캐시 키 포맷 변경 = 전 공급사 동시 장애

CacheKeyGenerator (컨트롤러에서 generateSearchRequestKey 호출, 예: AmadeusSearchController.kt:34)나 CacheSetcacheName/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.calculateToUTCAmadeus·Galileo·Sabre의 취소/발권TTL·취소위약 시각 → 금액CalculateTimezoneService
공유 DTO (SearchRequest, BookingRequest 등)전 공급사 컨트롤러모든 진입점 역직렬화interfaces/request 사용처
공급사 내부 허브 서비스 (예: Amadeus RetrieveService/PricingService)해당 공급사 전체 오퍼레이션예약·발권·취소·환불 연쇄RetrieveService|PricingService (모듈 내)
특정 {Name}Client해당 공급사만그 공급사 외부 호출해당 모듈 내
단일 컨트롤러/오퍼레이션 서비스그 오퍼레이션만가장 안전해당 파일

변경 영향도 판단 3단계 (실무 루틴)

  1. 위치 파악: 바꾸려는 클래스가 support/ · application/(루트) · domain/repository/ 에 있으면 = 횡단 공유 → 전 공급사 의심.
  2. 콜러 grep: 클래스명/메서드명으로 grep -rn. 생성자 주입이라 콜러는 “그 타입을 생성자에 가진 클래스”다.
  3. 시그니처 변경이면 컴파일러가 잡아준다: 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() 의 반환 타입을 바꾸면 어떤 클래스들이 컴파일 에러가 나는가? 어떻게 한 번에 찾는가?

Q2. 신입이 "검색은 되는데 예약 시 항상 '운임 만료'가 난다"는 버그를 받았다. 콜러/콜리 관점에서 가장 먼저 의심할 공유 컴포넌트는?

Q3. 왜 컨트롤러·서비스에는 공유 베이스클래스가 없는데 클라이언트에만 ClientSupport가 있을까?


관련 노트