시스템 아키텍처 개관

arch-overview pattern-strategy config-infra

이 노트의 목적

air-intl-adapter11개 항공 공급사(GDS/NDC/LCC)의 제각각인 API를 하나의 Triple 내부 표준 API로 변환(adapt) 하는 Kotlin/Spring Boot 마이크로서비스다. 이름 그대로 “어댑터(Adapter)” 역할이 본질이다. 이 문서는 신입이 코드베이스에 처음 들어왔을 때 “어디에 무엇이 있고, 요청이 어떻게 흐르며, 왜 이렇게 설계됐는가”를 한 번에 파악하도록 돕는 출발점이다.


1. 한눈에 보는 정체성

항목내용근거 (파일)
서비스 성격국제선 항공 멀티 공급사 어댑터 마이크로서비스CLAUDE.md, AirIntlAdapterApplication.kt
빌드 도구Gradle (Kotlin DSL)build.gradle.kts
언어/런타임Kotlin 2.0.0, Java 21 (JvmTarget.JVM_21)build.gradle.kts:13-24,101-110
프레임워크Spring Boot 3.3.0 (+ spring-boot-starter-web, -web-services, -aop)build.gradle.kts:14-49
그룹/패키지com.triple.air.intl / com.triple.air.intl.adapterbuild.gradle.kts:20
진입점AirIntlAdapterApplication.kt (서버 포트 8000)AirIntlAdapterApplication.kt, application.yml:5-6
HTTP 노출공급사별 내부 REST API /internals/{SUPPLIER}/... + /health*Controller.kt, HealthController.kt

"중앙 디스패처가 없다"

많은 어댑터 시스템은 POST /search 하나에 supplier 파라미터를 받아 분기하는 중앙 디스패처를 둔다. 이 시스템은 그렇지 않다. 공급사마다 자체 REST 컨트롤러(/internals/AMADEUS/search, /internals/SABRE/search …)가 독립적으로 존재하고, 라우팅은 URL 경로(공급사명)로 끝난다. Triple 예약 시스템(호출자)이 “어느 공급사를 부를지” 이미 알고 그 경로로 직접 호출한다. → 자세한 호출 관계는 caller-callee-map 참고.


2. 기술 스택 (build.gradle.kts 기준)

// build.gradle.kts:14-18
id("org.springframework.boot") version "3.3.0"
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
java.sourceCompatibility = JavaVersion.VERSION_21
카테고리라이브러리 / 버전용도근거
spring-boot-starter-web, -web-servicesREST 컨트롤러 + SOAP(WS) 통신build.gradle.kts:34-36
캐시/세션Redis + Redisson 3.39.0 (lettuce/jedis 제외)검색결과 캐시, 분산락, 세션build.gradle.kts:37-44
HTTP 클라이언트OkHttp 4.9.3모든 공급사 외부 호출의 실제 전송 계층build.gradle.kts:91, support/web/ClientSupport.kt
회복탄력성Resilience4j 2.2.0 (spring-boot3 + all)서킷브레이커/리트라이/벌크헤드build.gradle.kts:78-80
리트라이(보조)spring-retry (@EnableRetry)@Retryable 백오프build.gradle.kts:38, AirIntlAdapterApplication.kt:8
직렬화Jackson (kotlin / xml(dataformat) / no-ctor-deser)JSON + XML/SOAP 양쪽 매핑build.gradle.kts:51-55
SOAP 보안wss4j 1.6.19 / wss4j-ws-security-dom 2.4.1WS-Security 서명/헤더build.gradle.kts:46-47
비동기kotlinx-coroutines-core + -slf4j병렬 호출, MDC 전파build.gradle.kts:58-59
모니터링Datadog APM(dd-trace-api 1.2.0) + opentracing분산 추적, traceIdbuild.gradle.kts:70-76
오류 추적Sentry 6.30.0 (spring-boot-starter-jakarta)예외 캡처/알림build.gradle.kts:61-62
비밀관리AWS Secrets Manager + S3 SDK자격증명 주입, 파일 저장build.gradle.kts:64-65
경보Slack API 1.44.2운영/긴급 채널 알림build.gradle.kts:82-86
압축snappy-java, brotli/dec, (gzip/deflate JDK)Redis 값 압축 + 응답 압축 해제build.gradle.kts:39,92
LCC 암호화libs/TwaySEED.jar (로컬 jar)T’way SEED 인증 암호화build.gradle.kts:57
SFTPjcraft jsch 0.1.55T’way/Jin Air FTP 연동build.gradle.kts:82

신입이 먼저 외울 4개

OkHttp(나가는 호출) · Redisson/Redis(상태·캐시) · Resilience4j(장애 격리) · Datadog/Sentry(관측). 이 4개가 모든 공급사 모듈을 가로지르는 공통 토대다. 자세히는 configuration-and-infra, resilience-and-events.


3. 패키지 레이아웃 (레이어드 + 멀티공급사)

루트 패키지: com.triple.air.intl.adapter. 최상위는 공통(cross-cutting) 레이어 8개공급사 모듈 묶음 supplier/ 로 나뉜다.

com.triple.air.intl.adapter
├── AirIntlAdapterApplication.kt   # @SpringBootApplication, @EnableRetry, JVM 기본 TZ=UTC
│
├── interfaces/        # (공통) REST 진입. HealthController, 공통 request/response DTO, AirportService 등
├── application/       # (공통) 횡단 서비스: SlackService(경보), CalculateTimezoneService
├── configuration/     # Spring 설정: OkHttp, Redis/Redisson, S3, OpenApi, WebMvc(필터순서), Properties
├── domain/            # 공통 도메인: FareRule, Amenity + repository(Redis 기반)
├── infrastructure/    # 공통 외부연동: city(CityClient), slack(SlackClient)
├── support/           # 유틸/공통: web(ClientSupport·MDCHolder·Result), exception, util, enums, cache, filter, log
│
└── supplier/          # ★ 11개 공급사 모듈. 각 모듈이 같은 5+1 레이어를 반복
    ├── amadeus/        sabre/        galileo/      amadeusndc/
    ├── tway/           jinair/       koreanair/    lufthansa/
    └── singaporeair/   jejuair/      groupair/

3.1 공급사 모듈의 내부 구조 (11개 모두 동일 — “전략 패턴의 손맛”)

supplier/{name}/동일한 6개 하위 패키지를 갖는다 (Glob로 11개 전부 확인됨):

supplier/{name}/
├── interfaces/      # controller/internals/{Name}{Op}Controller.kt + DTO(request/response)
├── application/     # {Name}{Op}Service.kt — 비즈니스 오케스트레이션
├── infrastructure/  # {Name}Client.kt — 실제 외부 API 호출 (OkHttp/SOAP/NDC)
├── domain/          # 공급사 고유 도메인 모델 + repository
├── support/         # 공급사 전용 유틸/enum/model
└── configuration/   # 공급사 전용 Bean/Redis 설정

네이밍 규칙(코드 전반에서 강제됨)

  • 컨트롤러: {Name}{Op}Controller → 예: AmadeusSearchController, SabreBookingController
  • 서비스: {Name}{Op}Service → 예: AmadeusFlightSearchService, AmadeusTicketingService
  • 클라이언트: {Name}Client → 예: TwayClient, KoreanairClient, GalileoClient
  • HTTP 경로: /internals/{SUPPLIER}/{resource} (대문자 공급사명)

이 규칙 덕분에 “어느 파일을 열어야 하는지”가 클래스명만 봐도 예측된다. 새 공급사 추가도 이 골격을 복제하는 식이다.

모듈 크기는 균등하지 않다 (kt 파일 수)

공급사kt 파일 수공급사kt 파일 수
sabre882koreanair213
amadeus873jinair114
galileo525jejuair99
amadeusndc357singaporeair88
tway253lufthansa87
groupair34

GDS(amadeus/sabre/galileo)와 NDC(amadeusndc)는 자동생성된 SOAP/NDC 스키마 DTO가 대부분이라 파일 수가 폭발한다. 신입이 sabre/amadeus를 처음 열면 압도되기 쉽다 → 컨트롤러 → 서비스 → 클라이언트 3개만 따라가면 흐름은 동일하다. groupair(34개)가 골격 학습용으로 가장 적합하다.


4. 공급사 11개 × 통합 방식 매핑

#공급사코드 식별자통합 방식GDS 코드주요 오퍼레이션핵심 특이점
1AmadeusAMADEUSGDS / SOAP (TOPAS·ART)1ASearch·Booking·Ticketing·FareRule·Queue·CashReceiptstateful PNR 세션, 가장 큰 모듈, 국내 TOPAS 경유
2SabreSABREGDS / SOAP(+REST)1SSearch·Booking·Ticketing·FareRule·Queue·CashReceipt코루틴 사용(SabrePassengerService), SOAP+REST 혼용
3GalileoGALILEOGDS / SOAP (Universal API)1GSearch·Booking·Ticketing·FareRule·Queue·CashReceiptSessionContext, 스키마 방대, REST/KPS/KRT 보조
4Amadeus NDCAMADEUSNDCNDC (ART)Search·Booking·Ticketing·FareRule·QueueNDC ART 클라이언트, GPS 보조
5T’wayTWAYLCC / REST (XML)Search·Booking·Ticketing·FareRule·Ancillary·AgencyCreditTwaySEED.jar SEED 암호화 인증, FTP
6Jin AirJINAIRLCC / RESTSearch·Booking·Ticketing·FareRule·Ancillary·AgencyCredit부가서비스/대리점 크레딧, FTP, payment/dsr 분리
7Korean AirKOREANAIRNDC (V21.3)Search·Booking·Ticketing·FareRuleNDC 표준, NicePay 결제
8LufthansaLUFTHANSANDC (V17.2)Search·Booking·Ticketing·FareRule·Ancillarycertification 스키마, 재발행
9Singapore AirSINGAPOREAIRNDC (EDIST 18.1)Search·Booking·Ticketing·FareRule·AncillaryEDIST 스키마, 재발행 샘플
10Jeju AirJEJUAIRLCC / RESTSearch·Booking·Ticketing·FareRule취소 코루틴 흔적
11Group AirGROUPAIR그룹/단체 운임Search·Booking·Ticketing·FareRule가장 작은 모듈, 단체 예약, search/booking 엔드포인트 분리

근거: support/enums/Supplier.kt(enum 11개), configuration/Properties.kt(공급사별 @ConfigurationProperties), application.yml:11-20(공급사 yml import), 각 supplier/*/interfaces/controller/internals/*Controller.kt(경로 확인).

통합 방식은 크게 3+1 갈래

각 공급사 개요는: amadeus-overview · sabre-overview · galileo-overview · amadeusndc-overview · tway-overview · jinair-overview · koreanair-overview · lufthansa-overview · singaporeair-overview · jejuair-overview · groupair-overview.


5. 레이어 경계 다이어그램

flowchart TD
    Caller["Triple 예약 시스템 외부 호출자<br/>HTTP POST 또는 GET<br/>헤더 x-triple-sales-channel, x-triple-sales-funnel, PNR 등"]
    Caller -->|"/internals/SUPPLIER/resource"| Filters

    subgraph Filters["서블릿 필터 체인 WebMvcConfig 순서"]
        F1["Order 1 ContentCachingWrapperFilter<br/>요청 및 응답 본문 캐싱"]
        F2["Order 2 MDCFilter<br/>헤더에서 MDC로 channel, funnel, pnr, traceId"]
        F3["Order 3 AdapterLoggingFilter<br/>구조화 로깅"]
        F1 --> F2 --> F3
    end

    Filters --> Interfaces

    subgraph Interfaces["INTERFACES supplier 내부 interfaces/controller/internals"]
        I["Name+Op Controller<br/>@RestController @RequestMapping /internals<br/>@CircuitBreaker name은 supplierSearch, fallbackMethod 지정 Resilience4j<br/>요청 DTO 검증, CacheKeyGenerator, 서비스 호출, View 변환"]
    end

    Interfaces --> Application

    subgraph Application["APPLICATION supplier 내부 application"]
        A["Name+Op Service<br/>비즈니스 오케스트레이션 여러 Client 조합<br/>캐시 read 및 write, 병렬화 coroutine pmap<br/>예외를 공통 ErrorMessage로 매핑"]
    end

    Application --> Infrastructure

    subgraph Infrastructure["INFRASTRUCTURE supplier 내부 infrastructure"]
        N["Name Client extends ClientSupport<br/>실제 외부 호출 OkHttp, SOAP NDC REST<br/>LoggingAndCompressionInterceptor 요청 응답 로그 + gzip br deflate 해제"]
    end

    Infrastructure --> Ext["외부 공급사 API<br/>GDS NDC LCC"]
    Infrastructure --> Redis["Redis Redisson<br/>캐시, 세션, 분산락"]
    Infrastructure --> Obs["공통 서비스 및 관측<br/>SlackService 경보, Datadog, Sentry"]

    Support["support 횡단 토대<br/>web, exception, util, enums, cache<br/>application/SlackService<br/>모든 레이어가 공유하며 역방향 의존 없음"]
    Support -.-> Interfaces
    Support -.-> Application
    Support -.-> Infrastructure
  • 외부 호출자(Triple 예약 시스템)는 헤더 x-triple-sales-channel, x-triple-sales-funnel, PNR 등을 실어 /internals/{SUPPLIER}/{resource} 경로로 HTTP POST/GET 호출
  • 서블릿 필터 체인(WebMvcConfig 순서): [@Order 1] ContentCachingWrapperFilter(요청/응답 본문 캐싱) → [@Order 2] MDCFilter(헤더→MDC: channel/funnel/pnr/traceId) → [@Order 3] AdapterLoggingFilter(구조화 로깅)
  • INTERFACES: {Name}{Op}Controller@RestController @RequestMapping(/internals/..), @CircuitBreaker(name="{n}Search", fallbackMethod=...)(Resilience4j) 부착, 요청 DTO 검증 → CacheKeyGenerator → 서비스 호출 → View 변환
  • APPLICATION: {Name}{Op}Service가 비즈니스 오케스트레이션(여러 Client 조합), 캐시 read/write, 병렬화(coroutine pmap), 예외→공통 ErrorMessage 매핑
  • INFRASTRUCTURE: {Name}Client : ClientSupport가 실제 외부 호출(OkHttp, SOAP/NDC/REST), LoggingAndCompressionInterceptor가 요청/응답 로그 + gzip/br/deflate 해제
  • 하단 3대 종착지: 외부 공급사 API(GDS/NDC/LCC) · Redis(Redisson, 캐시·세션·분산락) · 공통 서비스/관측(SlackService 경보·Datadog·Sentry)
  • support/(web·exception·util·enums·cache)와 application/SlackService는 모든 레이어가 공유하는 횡단 토대(역방향 의존 없음)

5.1 레이어별 책임 요약

레이어패키지책임대표 클래스 (file:line)
Interfacesinterfaces, supplier/*/interfacesHTTP 수신·DTO 검증·View 변환·서킷브레이커 부착AmadeusSearchController.kt:18-26
Applicationapplication, supplier/*/application오케스트레이션·캐시·병렬화·에러 매핑AmadeusFlightSearchService.kt, SabrePassengerService.kt:8
Infrastructureinfrastructure, supplier/*/infrastructure외부 API 전송(OkHttp/SOAP/NDC)TwayClient.kt, support/web/ClientSupport.kt:25
Domaindomain, supplier/*/domain도메인 모델·Redis repositorydomain/FareRule.kt, domain/repository/*
Support(횡단)support/*web·exception·util·enums·cache·filter·logMDCHolder.kt, ClientSupport.kt, ErrorMessage.kt

의존 방향 규칙

interfaces → application → infrastructure → (외부)한 방향으로 흐른다. support는 모든 레이어가 의존하는 공통 기반(역방향 의존 없음). 신입이 코드를 읽을 때는 항상 컨트롤러에서 시작해 이 방향을 따라 내려가면 길을 잃지 않는다. 실제 요청 흐름은 request-flow에서 단계별로 추적한다.


6. 진입점과 부트스트랩에서 알아야 할 디테일

// AirIntlAdapterApplication.kt
@EnableRetry
@SpringBootApplication
class AirIntlAdapterApplication {
    @PostConstruct
    fun init() = TimeZone.setDefault(TimeZone.getTimeZone("UTC"))   // ★ JVM 기본 시간대 UTC 고정
}

함정 1 — JVM 전역 시간대를 UTC로 강제한다

@PostConstruct에서 TimeZone.setDefault(UTC)를 호출한다. 국제선은 출발/도착 공항이 서로 다른 시간대이므로, 코드 전반의 LocalDateTime/LocalDate암묵적으로 UTC 기준이라고 가정하고 읽어야 한다. 로컬에서 now()를 KST로 기대하면 디버깅이 어긋난다. 시간대 변환은 별도 서비스 application/CalculateTimezoneService.kt가 담당.

부트스트랩 설정 핵심:

  • 프로파일: local / dev / qa / staging / prod / worker (configuration/Profiles.kt). worker는 배치/큐 처리용으로 항상 다른 프로파일과 함께 활성화된다(SlackService.kt:18-21에서 worker 제외 후 첫 프로파일을 환경명으로 사용).
  • 공급사 설정 분리 import: application.yml:9-20에서 classpath:supplier/{name}.yml 10개를 import (groupair 포함, NDC인 amadeusndc는 amadeus.yml에 포함). 비밀값은 prod에서 optional:aws-secretsmanager:prod/air-intl-adapter로 주입(application-prod.yml:6-7).
  • 가상 스레드 ON: spring.threads.virtual.enabled: true (application.yml:23-25) — Java 21 가상 스레드로 블로킹 외부 호출을 다수 처리.
  • 에러 자동설정 제외: ErrorMvcAutoConfiguration 제외(application.yml:22) → 오류 응답은 전적으로 RestExceptionHandler(@ControllerAdvice)가 책임. → error-handling.

7. “이벤트/상태 전파”는 메시지큐가 아니다

이 시스템에 Kafka/RabbitMQ 같은 메시지큐는 없다

분산 상태 전파는 다음 3가지로 구현된다:

  1. Resilience4j 서킷브레이커 상태전이@CircuitBreaker(name="amadeusSearch")가 실패율 35% 초과 시 OPEN으로 전이, searchFallback이 빈 결과 반환 + Datadog 스팬에 supplier.circuit-breaker=OPEN 태그(AmadeusSearchController.kt:24,72-80). 설정은 application.yml:37-70(slidingWindow 180s/TIME_BASED, failureRate 35%, open 120s).
  2. 예외 전파 + Sentry/Datadog — 모든 예외는 findRootCause()log()sentryLog()로 캡처되고 MDC 컨텍스트(traceId 등)가 태그로 부착됨(RestExceptionHandler.kt:77-105). 코루틴 예외는 AdapterCoroutineExceptionHandler가 동일 핸들러로 위임.
  3. Slack 경보 — 발권 실패/취소 실패/QUOTA 부족/타임아웃 등 운영 개입이 필요한 사건application/SlackService.kt가 긴급/운영 채널로 전송. 메시지에 채널·공급사·PNR·TraceId가 포함된다.

자세히는 resilience-and-events, 비동기 처리는 async-coroutines.


8. 신입이 알아야 할 “큰 그림” 5가지

멘탈 모델

  1. 나는 변환기다. Triple이 “이 공급사로 검색해줘”라고 표준 요청을 보내면, 나는 그것을 공급사 고유 포맷(SOAP/NDC/REST)으로 바꿔 호출하고, 응답을 다시 Triple 표준 View로 바꿔 돌려준다. 비즈니스 “결정”(어느 공급사를 쓸지)은 호출자가 한다.
  2. 모든 공급사는 같은 골격을 반복한다. Controller → Service → Client. 하나를 이해하면 11개를 이해한 셈. 단, 외부 프로토콜만 다르다.
  3. 상태는 Redis에 산다. 검색결과·세션·분산락. 어댑터 자신은 가능한 한 stateless를 지향하지만 GDS 세션(Amadeus PNR)처럼 stateful한 부분이 함정이다.
  4. 장애는 격리한다. 한 공급사가 느려지거나 죽어도 서킷브레이커가 끊고 빈 결과를 주어 전체 검색이 멈추지 않게 한다.
  5. 운영자가 본다. 자동 처리가 실패하면 Slack으로 사람에게 넘긴다(수동 취소/환불/VOID). 코드의 slackService.sendXxxFail(...) 호출은 “여기서 자동화가 끝나고 사람이 개입한다”는 신호다.

학습 순서 추천

  1. [지금 문서] system-architecture
  2. request-flow — 요청 한 건이 필터 → 컨트롤러 → 서비스 → 클라이언트를 어떻게 통과하는가
  3. caller-callee-map — 누가 이 어댑터를 부르고, 이 어댑터는 누구를 부르는가
  4. common-operations — Search/Booking/Ticketing/… 6대 오퍼레이션의 공통 의미
  5. groupair-overview — 가장 작은 모듈로 골격 체득 → 이후 원하는 공급사로 확장

전체 온보딩 동선은 onboarding-map, 빠른 참조는 quick-reference, 함정 모음은 landmines에서.


9. 핵심 진입 파일 빠른 참조

무엇을 보려면파일
앱 부트스트랩/TZAirIntlAdapterApplication.kt
의존성/스택build.gradle.kts
전역 설정·서킷브레이커·공급사 importsrc/main/resources/application.yml
공급사별 자격증명 구조configuration/Properties.kt
필터 순서·ObjectMapperconfiguration/WebMvcConfiguration.kt
외부 호출 공통 추상support/web/ClientSupport.kt
요청 컨텍스트(MDC)support/web/MDCHolder.kt
전역 예외 처리support/exception/RestExceptionHandler.kt
운영 경보application/SlackService.kt
비동기 헬퍼support/util/CoroutineExtensions.kt
컨트롤러 예시supplier/amadeus/interfaces/controller/internals/AmadeusSearchController.kt

기존 심층 분석 문서(공급사 외부 API 관점)

amadeus-gds · sabre-gds · galileo-gds · singaporeair-ndc