Project Detail
Travel Advisor
Travel Advisor — Side Project
Booking Orchestrator가 분산 트랜잭션을 조율하고, Helm, Skaffold 파이프라인과 Observability 스택으로 운영 자동화와 모니터링을 강화했습니다.
- Saga, Outbox orchestration
- Debezium CDC 파이프라인 구축
- Helm, Skaffold CI/CD 자동화
호텔, 항공권, 차량 예약을 하나의 여정으로 연결하는 통합 플랫폼입니다. Saga, Outbox, Debezium CDC와 Observability 스택을 결합해 분산 환경에서도 안정적인 예약 경험을 제공합니다.
STABILITY
분산 트랜잭션 안정성
Saga, Outbox 패턴과 Debezium CDC를 조합해 호텔 → 항공 → 차량 예약 흐름의 일관성을 확보했습니다.
Saga, Outbox, Debezium CDC
AUTOMATION
운영 자동화 파이프라인
Helm, Skaffold 기반 CI/CD 자동화와 Observability 스택으로 배포와 모니터링을 자동화했습니다.
Helm, Skaffold, Observability
JOURNEY
여정 중심 설계
Booking Orchestrator가 단일 여정으로 묶어 사용자가 예약 진행도를 직관적으로 파악하도록 구성했습니다.
Booking Orchestrator UX
Travel Advisor — Github Repository
1 / 8프로젝트 개요
Travel Advisor는 해외 여행의 필수 요소인 항공권, 호텔, 차량
예약을 하나의 플랫폼에서 제공하는 올인원 서비스입니다. 이
프로젝트는 마이크로서비스 아키텍처(MSA), 도메인 주도 설계(DDD),
헥사고날 아키텍처를 결합하여 분산 트랜잭션 관리 및 유연한
확장성을 목표로 진행되었습니다. Saga와 Outbox 패턴, Debezium
기반 CDC 기술을 적용해 데이터 일관성을 유지하며, 사용자가 예약
과정을 간단히 처리할 수 있도록 설계되었습니다.
기술적 도전과 주요 특징
-
MSA 아키텍처 설계: 항공권, 호텔, 차량 도메인을 독립적인 마이크로서비스로 구성하여 확장성과 장애 격리를 극대화
-
Saga 및 Outbox 패턴: Saga 오케스트레이션을 중심으로 각 예약 도메인의 데이터 일관성을 보장하며, Outbox 패턴과 CDC를 활용해 데이터 무결성을 유지
-
쿠버네티스 및 CI/CD: Helm과 Skaffold를 사용해 배포 파이프라인을 자동화하고 클라우드 환경에서 높은 가용성을 보장
-
모니터링 및 로깅: OpenTelemetry, Prometheus, Grafana를 통합해 실시간 모니터링 체계를 구축하고 장애 추적 및 문제 해결을 용이하게 함
-
인증 및 권한 관리: Keycloak을 활용한 OAuth2 인증과 Spring Cloud Gateway를 통한 API Gateway 설계로 보안 강화
주요 책임 및 성과
-
Microservices Architecture(MSA) 기반의 통합 예약 플랫폼 설계 및 구축
-
항공권, 호텔, 차량 예약 등 각 도메인을 독립적인 마이크로서비스로 분리 및 배포
-
Saga, Outbox 패턴과 CDC를 활용한 트랜잭션 관리 및 데이터 일관성 보장. 호텔/항공권/차량 예약 흐름을 구현해 트랜잭션 문제 해결 사례 제시
-
Outbox 테이블, Saga 오케스트레이션으로 멱등성, 데이터 무결성 보장
-
Helm과 Skaffold를 활용한 Kubernetes 리소스 관리 및 배포 자동화
-
Kafka 및 Debezium CDC를 사용한 분산 이벤트 처리 설계. 높은 데이터 처리량과 안정성을 유지
-
Booking Orchestrator를 중심으로 Saga 패턴 구현
-
Outbox 패턴으로 데이터 일관성과 트랜잭션 안정성 보장
-
OpenTelemetry, Spring Actuator, Micrometer, Prometheus, Loki를 활용한 로그 및 메트릭 수집
-
Grafana, Prometheus, Loki, Tempo를 통합하여 실시간 모니터링 및 트레이싱 구성
-
Keycloak을 활용한 OAuth2 인증 서버 구성
-
Spring Cloud Gateway와 통합하여 리소스 서버 및 인증 서버 간의 인증 흐름 구현
-
Hexagonal Architecture를 통해 도메인 중심 설계(DDD) 구현
-
Resilience4j를 활용한 회복력 확보(Circuit Breaker, Bulkhead, Rate Limit 패턴 적용)
담당 역할
이 프로젝트는 개인 프로젝트로, 설계부터 구현까지 모든 과정을
직접 담당하였습니다. 호텔, 항공권, 차량 예약 서비스를 DDD(Domain
Driven Design) 방식을 적용해 구현하였으며, LLT(Long-Lived
Transaction, 분산된 환경에서의 데이터베이스 트랜잭션)를
안정적으로 유지하고 완료하기 위해 Saga 패턴, Outbox 테이블, CDC
기반 이벤트 처리 방식을 도입했습니다.
코드 베이스의 경우 각 마이크로서비스에 Hexagonal
Architecture를 적용하였고, 이를 통해 도메인 로직과
인프라스트럭처 로직을 분리하여 유지보수하기 편리하고, 테스트
가능한 코드를 작성했습니다. (※ 코드 복잡도가 올라갈 수 있다는
점은 충분히 인지하고 있으며, 학습을 위해 Gateway, Config 서버를
제외한 모든 마이크로서비스에 데모로 적용시켜보았습니다.)
Skaffold와 Helm을 통해 로컬 쿠버네티스 클러스터에
배포하는 CI/CD 파이프라인을 자동화하고, 빠르고 반복적인 배포가
가능하도록 개발 워크플로우를 단순화했습니다.
시스템 전반의 상태를 실시간으로 관찰할 수 있는 모니터링
체계를 구축했습니다. 각 미이크로서비스 애플리케이션의 로그와
트레이스는 OpenTelemetry를, 메트릭은 Spring Actuator +
Micrometer + Prometheus 사용해 수집하고, Grafana와 Prometheus,
Loki, Tempo를 통해 이를 시각화했습니다. Observability(logs,
traces, metrics, events)를 지속해서 수집하고 관찰하기 위해 분산
트레이싱(Distributed Tracing)을 도입하였으며, Correlation ID를
게이트웨이로 들어오는 요청부터 분산된 각 애플리케이션까지 전달해
동일한 연관 ID로 연결된 컨텍스트를 제공하고, 이를 통해 요청 처리
과정, 성능 병목 현상 등의 복잡한 분산 시스템의 문제를 진단할 수
있는 통찰력을 제공할 수 있는 시스템을 구축했습니다.
인증과 권한 관리는 KeyCloak을 사용하여 구현했습니다.
사용자와 리소스 서버 간의 인증 흐름을 구성하고, Spring Cloud
Gateway를 통해 외부 API 호출을 통제했습니다. 또한, API
Resilience4j를 활용해 Circuit Breaker, Retry, Bulkhead 패턴을
적용해 악의적인 Bruteforce 공격 혹은 잦은 빈도의 요청을 방어하고
리소스의 격리를 통해 서비스의 안정성과 가용성을 높여 전반적인
서비스 품질을 강화하기 위해 노력했습니다.
배운 점
MSA(Microservice Architecture)를 적용하며 얻은 교훈
Travel Advisor 프로젝트는 단순한 기능 구현을 넘어, MSA를
실제로 적용해보면서 아키텍처 관점에서 결정을 내릴 수 있는 경험과
기반이 생겼습니다. 모놀리식과 마이크로서비스 아키텍처의 차이점을
이해하고, 어느 환경에서는 모놀리식을, 어느 환경에서는 각
도메인을 독립적인 서비스로 분리해야 할 지에 대한 통찰이
생겼습니다.
모놀리식 아키텍처의 경우 간단하고 빠른 속도로 개발할 수 있다는
장점이 있는 반면 다음과 같은 문제가 있을 수 있습니다.
Travel Advisor는 회원, 호텔, 항공권, 차량, 예약 등 각기 다른
비즈니스 도메인을 포함해야 합니다. 초기 설계 단계에서는 이러한
도메인들이 하나의 통합된 서비스로 구성된 모놀리식 아키텍처로
구성될 경우 확장성에 제한이 있을 수 있습니다. 모든 도메인이
하나의 코드베이스에 포함돼 한 도메인의 변경 사항이 다른 도메인에
영향을 주는 경우가 발생할 수 있습니다.
그리고 각 도메인의 로직이 하나의 서비스에 집중되어 있어 코드의
책임 경계가 불분명해 책임의 모호성이 발생할 수 있습니다.
또한, 시스템이 꾸준히 발전하면서 지속해서 새로운 기능을
추가하고, 수정해야 하는데 그럴 때마다 무거운 전체 시스템을 다시
빌드하고 테스트해야 하는 부담이 발생합니다.
이를 해결하기 위해 MSA로 전환하여 각 도메인을 독립적인 서비스로
분리했습니다. 다음과 같은 개선점을 확인할 수 있었습니다.
-
서비스 간 독립성 확보: 호텔, 항공권, 차량, 예약 등 각 도메인을 독립된 마이크로서비스로 분리하면서 각 서비스가 자신의 책임에 집중할 수 있게 되었습니다.
-
장애 격리: 특정 서비스에 장애가 발생하더라도 다른 서비스는 독립적으로 운영될 수 있는 구조가 갖춰졌습니다.
-
확장성: 각 서비스가 별도의 배포 주기를 가질 수 있어 기능 추가 및 변경이 더욱 유연해졌습니다.
DDD(Domain Driven Design)를 도입해 도메인 로직의 명확화
도메인 주도 설계를 적용하면서 가장 인상 깊었던 점은,
복잡한 비즈니스 로직을 단순히 코드로 구현하는 것을 넘어, 이를
명확한 도메인 언어로 정의하고 팀 내 공통된 이해를 형성할 수
있다는 점입니다. 구체적인 사례는 다음과 같습니다.
-
Ubiquitous Language(통합 언어)의 중요성: 호텔, 항공권, 차량이라는 각 도메인별로 사용하는 용어와 개념이 모두 다릅니다. 예를 들어, 항공권 예약에서는 FlightLeg(항공 구간)이나 PNR(예약 코드) 같은 개념이 존재하고, 호텔 예약에서는 RoomType이나 Availability 같은 용어가 중심이었습니다. 이러한 도메인별 용어를 정확히 정의하고, 이를 코드 설계와 문서에 통합함으로써 팀원 간의 의사소통 효율을 크게 향상시킬 수 있습니다.
-
Bounded Context의 도입: 각 도메인의 로직과 데이터를 분리하기 위해, DDD의 Bounded Context 개념을 활용했습니다. 다음과 같습니다.
호텔 도메인의 경우 RoomType, Booking, Cancellation과 같은
개념이 포함된 컨텍스트를,
항공권 도메인의 경우 FlightType, FlightLeg, Passenger, PNR와
같은 개념이 포함된 컨텍스트를,
차량 도메인의 경우 CarType, SeatType, RentalPeriod 등이
포함된 컨텍스트를 정의합니다. 이를 통해 각 도메인의 책임을
명확히 할 수 있고, 도메인 간의 경계를 확실하게 나누어 각
도메인 간의 결합력을 낮추고, 비즈니스 코어 로직의 응집력을
높일 수 있었습니다.
- Aggregate Root 설계: Bounded Context 내 도메인 엔터티들의 일관성을 보장하기 위해 최상위 엔터티를 AR(Aggregate Root)로 정의하고, 이를 통해서만 하위 도메인 엔터티들의 상태를 검증하고 변경할 수 있도록 했습니다.
예를 들어 호텔 도메인에서는 HotelOffer 엔터티가 예약 상태를
검증하고 변경(예약 생성, 취소 등)할 수 있도록 Aggregate
Root로 지정했고, 이를 위해 내부적으로 예약 상태에 해당하는
BookingApproval 하위 도메인의 상태를 검증하고 변경하는
책임을 갖습니다.
Aggregate Root와 Domain Entity 정의는 다음과 같습니다.
1/** 2 * 상속하는 클래스 본인이 Aggregate Root(AR) 임을 알리기 위한 Marker Class 입니다. 3 * 타입으로 사용하므로 내부적으로 메서드나 필드를 갖지 않습니다. 4 * Aggregate Root 는 고유 식별자를 가지므로 DomainEntity 를 상속합니다. 5 * 6 * Aggregate Root의 역할 7 * - 하위 엔터티들의 상태들을 관리하고, 일관된 상태로 유지하기 위해 항상 엄격한 유효성 검사를 하는 책임을 갖습니다. 8 * 9 * @param Aggregate Root 의 고유 식별자 타입 10 */ 11public abstract class AggregateRoot extends DomainEntity { 12 13 /* *** NO-OP: 이곳에 필드나 메서드를 추가하시면 안됩니다. *** */ 14 15} 16 17/** 18 * 모든 Entity는 고유 식별자를 가지며, Entity 간의 비교는 ID를 통해서만 합니다. 즉, ID가 같으면 동일한 Entity로 인지합니다. 19 * 20 * @param Entity의 고유 식별자 타입 21 */ 22public abstract class DomainEntity { 23 24 private ID id; 25 26 public ID getId() { 27 return id; 28 } 29 30 protected void setId(ID id) { 31 this.id = id; 32 } 33 34 @Override 35 public boolean equals(Object o) { 36 if (this == o) return true; 37 if (o == null || getClass() != o.getClass()) return false; 38 DomainEntity that = (DomainEntity) o; 39 return Objects.equals(id, that.id); 40 } 41 42 @Override 43 public int hashCode() { 44 return Objects.hash(id); 45 } 46 47}
DDD에서 정의하는 도메인에 해당하는 요소는 모두 DomainEntity 추상
클래스를 상속받습니다. 그리고 Aggregate Root의 경우
AggregateRoot 추상 클래스를 상속받습니다. AggregateRoot는
DomainEntity를 확장하는 마커 클래스로, 그저 Aggregate Root 임을
알리기 위해 확장한 마커 클래스입니다. Aggregate Root는 하위
Domain Entity 집합을 생성하고, 수정하고, 값을 검증하는 단일
포인트의 관리자 역할을 하며, 이는 DDD의 핵심 개념 중 하나입니다.
MSA, DDD 그리고 Hexagonal Architecture 적용에서 느낀 트레이드오프에 대한 통찰
MSA, DDD 그리고 Hexagonal Architecture 모두 개발자 또는
팀에게 많은 이점을 주기도 하지만, 많은 사람들이 말하듯, 이들은
Silver Bullet이 아니라고 하는 것에 동의합니다. 예를 들어 다음과
같은 문제가 발생할 수 있습니다.
- 작은 규모의 프로젝트에서의 복잡도 증가: Travel Advisor는 MSA 데모 성격의 작은 규모의 프로젝트였기 때문에, MSA와 DDD를 적용하는 과정에서 오히려 구성의 복잡도와 러닝 커브가 높은 점이 단점으로 작용하기도 했습니다. 특히, Hexagonal Architecture를 도입하면서 코드 베이스에 추상화 레이어가 추가되어 개발 속도가 느려지는 경험을 했습니다. 이는 다른 사람들 또한 어느 정도 공감할 수 있는 부분이라고 봅니다.
하지만 이러한 단점들도 장기적인 관점에서 팀이 커지고 동료가
늘어났을 때, 유지보수와 확장성에 도움이 될 것이라는 확신이
있었습니다.
- DDD 적용의 한계: DDD를 적용하며 도메인 로직 중심의 설계를 지향했지만, 모든 상황에서 DDD가 적합한 것은 아니라는 점도 느꼈습니다. 특히, 간단한 CRUD 작업에까지 DDD의 복잡한 패턴을 적용할 경우 오히려 코드가 비대해지고 유지보수가 어려워지는 문제가 있었습니다. 이를 통해, DDD는 비즈니스 복잡성이 높은 부분에 집중적으로 적용해야 한다는 점을 깨달았습니다.
이러한 트레이드오프들에 관해 생각해보면, MSA, DDD, Hexagonal
Architecture는 단순한 유행이 아니라, 상황과 요구에 따라 신중히
선택하고 활용해야 할 강력한 도구라는 점을 깊이 깨달을 수
있었습니다. Travel Advisor 프로젝트는 이러한 아키텍처적 실험을
통해 저의 설계 역량을 한 단계 끌어올리는 동시에, 미약하지만 길고
끊임 없는 도전 속에서 성장의 가능성을 증명한 의미 있는
여정이었습니다.
Kubernetes 리소스 구성 및 Skaffold를 통한 효율적인 배포
파이프라인 구축 과정
Kubernetes와 Skaffold를 활용해 환경에 MSA로 환경하는
구성해보는 경험을 할 수 있었습니다. 이 과정에서 얻은 배움은 단순히
기술적 도구를 사용하는 것을 넘어, 애플리케이션 배포와 관리의
전체적인 흐름을 이해하고, 자동화를 통해 효율성과 신뢰성을 높이는
방법을 체득했습니다.
Kubernetes는 컨테이너화된 애플리케이션의 배포, 관리, 확장을
효율적으로 지원하는 플랫폼으로, MSA 환경에서 그 중요성을 다시금
느낄 수 있었습니다. 이번 프로젝트에서는 다음과 같은 실질적인
배움을 얻었습니다.
-
Kubernetes 리소스 설계 및 관리: Kubernetes의 Deployment, Service, ConfigMap, Secret, 그리고 Ingress와 같은 다양한 리소스를 설계하고 구성하면서, 애플리케이션의 상태를 선언적으로 정의하는 방법을 익혔습니다. 특히 ConfigMap과 Secret을 활용하여 환경별 설정과 민감한 데이터를 안전하게 관리하는 중요성을 배웠습니다.
-
Helm 차트를 통한 템플릿화: 다수의 마이크로서비스를 Kubernetes에 배포하는 과정에서 Helm 차트를 활용하여 매니페스트를 템플릿화하고, 환경에 따라 다른 값을 쉽게 적용할 수 있었습니다. 이를 통해 반복적인 설정 작업을 줄이고, 여러 환경(로컬, QA, 프로덕션) 간의 일관성을 유지할 수 있었습니다.
-
Observability 시스템 구축: Kubernetes Pod와 노드의 상태를 실시간으로 모니터링하기 위해 Prometheus와 Grafana를 사용했습니다. 또한, Loki를 통해 애플리케이션 로그를 수집하고 분석하며, Tempo로 분산 트레이싱을 구현하여 복잡한 요청 흐름을 시각화할 수 있었습니다. 이 과정에서 Observability의 중요성과 구현 방법을 명확히 이해하게 되었습니다.
Kubernetes 구성의 마지막 퍼즐은 Skaffold를 통해 자동화된 배포
파이프라인을 구축한 것이었습니다. Skaffold는 코드 변경 사항을
감지하고, 빌드부터 배포까지의 작업을 간단한 명령 하나로 처리할 수
있게 해주는 도구로, 다음과 같은 배움을 얻을 수 있었습니다.
-
로컬 환경에서의 빠른 피드백 루프: Skaffold의 dev 명령어를 통해 코드를 변경하면 즉시 빌드 및 배포가 이루어지며, 변경 결과를 바로 확인할 수 있습니다. 이를 통해 빠르게 피드백을 받고, 반복적인 개발 작업을 간소화할 수 있습니다.
-
Kubernetes와의 원활한 통합: Skaffold는 Kubernetes 매니페스트와 직접 연동되기 때문에, 로컬 개발 환경에서도 실제 Kubernetes 클러스터와 동일한 환경을 유지할 수 있습니다. 이는 배포 시 환경 차이에 의해 발생할 수 있는 문제를 사전에 방지하는 데 큰 도움이 됩니다.
다음은 skaffold.yaml 파일 구성입니다.
1apiVersion: skaffold/v4beta11 2kind: Config 3metadata: 4 name: traveladvisor 5build: 6 local: 7 push: false 8 artifacts: 9 # 이 이미지 이름에 존재하는 /, ., - 는 _ 로 sanitize 됩니다. 10 # 따라서 이 파일의 deploy.helm.releases 내 setValueTemplates, setValues에서 _ 로 치환해 적어야 생성된 이미지 이름과 제대로 매핑됩니다. 11 # (참조: https://skaffold.dev/docs/renderers/helm/#sanitizing-the-artifact-name-from-invalid-go-template-characters) 12 - image: com.traveladvisor/batchserver # Push 없이 로컬 이미지를 활용합니다. setValueTemplates에서 완전히 일치해야 합니다. 13 # Multi Module 프로젝트의 경우 container 모듈을 빌드 대상으로 지정해야 합니다. 14 # container 모듈에서 서브 모듈들을 불러와 Spring Application 을 완성하기 때문입니다. 15 context: ./batchserver/batch-container 16 jib: 17 args: 18 - -DskipTests 19 - -Djib.useOnlyProjectCache=false 20 - -Djib.alwaysCacheBaseImage=false 21 - -Dspring-boot.run.arguments=--debug 22 fromImage: docker.io/library/eclipse-temurin:17-jre 23 - image: com.traveladvisor/memberserver 24 context: ./memberserver/member-container 25 jib: 26 args: 27 - -DskipTests 28 - -Djib.useOnlyProjectCache=false 29 - -Djib.alwaysCacheBaseImage=false 30 - -Dspring-boot.run.arguments=--debug 31 fromImage: docker.io/library/eclipse-temurin:17-jre 32 33 # ... 생략 ... 34 35deploy: 36 helm: 37 releases: 38 - name: dev-env 39 chartPath: helm/environments/dev-env 40 valuesFiles: 41 - helm/environments/dev-env/values.yaml 42 version: 0.1.0 43 setValueTemplates: 44 batchserver.image.repository: "" 45 batchserver.image.tag: "" 46 memberserver.image.repository: "" 47 memberserver.image.tag: "" 48 49 # ... 생략 .. 50 51 setValues: 52 batchserver.image.pullPolicy: "Never" 53 memberserver.image.pullPolicy: "Never" 54 55# ... 생략 ...
로컬 개발 환경에서 쿠버네티스 클러스터에 빠르게 배포하기 위한
CI/CD 코드이며, Google Cloud 팀에서 제작했습니다. 따라서
인텔리제이에 Google Cloud 플러그인을 설치하면 이 skaffold.yaml
파일을 인식해 간편하게 DEV 환경을 실행하거나, Pod의 상태 모니터링
및 로그 확인도 실시간으로 가능하게 해줍니다.
Observability와 분산 메트릭 추적
이번 프로젝트에서 Grafana, Loki, Tempo, Prometheus,
Micrometer, Spring Actuator와 같은 도구를 활용하여 각
마이크로서비스 즉, 시스템의 Observability를 구축하며 많은 것을
배웠습니다. 특히, 이러한 도구들이 결합되어 어떻게 애플리케이션
상태를 실시간으로 모니터링하고, 문제를 빠르게 추적하며, 시스템의
신뢰성을 높이는지에 대한 통찰을 얻게 되었습니다. 이 과정에서
Observability에 대한 이해가 생겼는데, 다음과 같습니다.
-
Observability: Observability는 단순한 모니터링을 넘어 시스템 내부 상태를 외부에서 관찰 가능하도록 만드는 것을 목표로 합니다. 이를 위해 Logs, Metrics, Traces, 그리고 Events라는 네 가지 핵심 요소를 의미합니다.
-
Logs: Loki를 활용해 애플리케이션 로그를 실시간으로 수집할 수 있습니다. 이 로그에 태그를 추가하거나, 컨텍스트 정보를 포함시켜 문제 발생 시 원인을 더 빠르게 파악할 수 있습니다.
-
Metrics: Prometheus와 Micrometer를 통해 CPU, 메모리, 요청 속도와 같은 주요 성능 메트릭을 수집하고, 이를 통해 성능 병목현상을 조기에 감지하고, 시스템의 성능 상태를 정량적으로 평가할 수 있습니다.
-
Traces: Tempo와 Correlation ID를 사용하여 분산 트랜잭션을 추적하며, 마이크로서비스 간의 호출 흐름을 시각화했습니다. 이는 복잡한 요청 흐름에서 병목 구간과 실패 지점을 빠르게 식별하는 데 도움이 됩니다.
Grafana를 중심으로 한 Observability 통합
Grafana는 여러 도구에서 수집한 데이터를 통합하여 시각화하는
데 핵심적인 역할을 했습니다. Grafana 대시보드에서 애플리케이션의
전체 상태를 실시간으로 관찰할 수 있었고, 경고(Alert)를 설정하여
이상 상황 발생 시 즉각적으로 대응할 수 있는 체계를 구축했습니다.
-
Loki와 Grafana의 통합: Loki로 수집된 로그 데이터를 Grafana에서 시각화하며, 특정 요청의 실패 로그를 빠르게 필터링하고 분석할 수 있습니다.
-
Prometheus와 Grafana의 통합: Prometheus에서 수집된 메트릭 데이터를 활용해 서비스의 응답 시간, 요청 처리량, CPU 사용량과 같은 주요 지표를 추적합니다.
-
Tempo와 Grafana의 통합: Tempo에서 생성된 분산 트레이스를 Grafana에서 시각화하여 복잡한 요청 흐름을 쉽게 파악할 수 있습니다.
Spring Actuator와 Micrometer의 실용성
Spring Actuator와 Micrometer는 애플리케이션 내부 상태를
메트릭으로 노출하는 데 중요한 역할을 합니다.
-
Spring Actuator: 기본적으로 제공되는 엔드포인트(/actuator/health, /actuator/metrics)를 통해 서비스의 상태를 쉽게 확인할 수 있습니다. 또한, 커스텀 Actuator를 추가하여 특정 비즈니스 로직에 대한 상태 정보를 메트릭으로 노출함으로써, 도메인 관련 이슈를 빠르게 파악할 수 있습니다.
-
Micrometer: Prometheus와의 통합을 통해 Micrometer에서 수집된 데이터를 Prometheus로 전송하고, 이를 기반으로 세부적인 성능 분석을 수행할 수 있었습니다. 예를 들어, 특정 메서드의 호출 빈도와 실행 시간을 추적하여 성능 병목을 발견할 수 있습니다.
Correlation ID를 활용한 컨텍스트 관리
Correlation ID는 하나의 요청이 여러 서비스 간을 거칠 때
해당 요청의 흐름을 식별할 수 있도록 도와주는 고유 식별자입니다.
API Gateway에서 생성된 Correlation ID를 모든 마이크로서비스에
전달하고, 이를 로그와 트레이싱 데이터에 포함시켜 특정 요청의
흐름을 명확히 추적할 수 있었습니다. 이 Correlation ID를 통해
서비스 간 의존성이나 성능 병목을 파악하고, 장애가 발생한 경우
문제를 정확히 진단할 수 있습니다.
Spring Cloud Gateway와 Resilience4j를 활용한 트래픽 관리와 안정성 확보
Spring Cloud Gateway를 통해 각 마이크로서비스의 엔트리
포인트 역할을 수행하며, Resilience4j를 활용해 트래픽을 제어하고
서비스의 안정성과 가용성을 강화하는 방법을 익혔습니다. 이 과정에서
얻은 배움은 다음과 같습니다.
-
단일 진입점 구성: 모든 클라이언트 요청은 Spring Cloud Gateway를 통해 마이크로서비스에 전달되도록 구성했습니다. 이를 통해 클라이언트와 마이크로서비스 간의 직접적인 통신을 차단하고, 서비스 간의 연결을 명확히 분리할 수 있습니다.
-
요청 라우팅과 필터링: Spring Cloud Gateway의 라우팅 규칙을 활용하여 클라이언트 요청을 적절한 마이크로서비스로 전달했습니다. 필터 체인을 사용해 요청과 응답을 사전 및 사후 처리하며, 로깅, 인증, 압축, 헤더 수정 등 다양한 작업을 수행했습니다.
-
로깅과 모니터링: Gateway에서 모든 요청과 응답을 기록하고, Correlation ID를 포함하여 각 요청의 흐름을 추적할 수 있도록 구현했습니다. 이를 통해 장애 발생 시 문제를 신속히 진단하고, 성능 병목을 분석할 수 있습니다.
-
보안 관리: Spring Cloud Gateway를 통해 인증과 권한 관리를 중앙 집중화했습니다. Keycloak과의 통합으로 OAuth2 인증을 처리하며, API 호출의 무단 접근을 방지할 수 있습니다.
-
Circuit Breaker: 특정 서비스가 장애 상태에 빠지면 해당 서비스로의 호출을 차단하고, 이를 빠르게 실패 처리(Fail-Fast)하여 전체 시스템의 안정성을 유지했습니다.
-
Rate Limiter: 특정 서비스로의 요청 빈도를 제한하여 과도한 트래픽으로 인한 서비스 장애를 예방했습니다. 이를 통해 DDoS 공격이나 예상치 못한 대규모 트래픽 상황에서도 서비스를 보호할 수 있습니다.
-
Retry: 네트워크 오류나 일시적인 장애가 발생했을 때, 일정 횟수만큼 요청을 재시도하도록 설정했습니다. Retry 로직을 Gateway에 통합함으로써 클라이언트가 장애를 인식하지 않고 서비스를 이용할 수 있도록 합니다.
-
Bulkhead: 서비스 간의 자원 격리를 통해 하나의 서비스가 과부하에 걸려도 다른 서비스에 영향을 미치지 않도록 설정했습니다. 이를 통해 전체 시스템의 가용성을 유지할 수 있습니다.
다음은 Resilience4j를 Spring Cloud Gateway의 RouteLocator에 설정한 코드입니다.
1@Bean 2public RouteLocator routeConfig(RouteLocatorBuilder routeLocatorBuilder) { 3 return routeLocatorBuilder.routes() 4 .route(p -> p 5 .path("/booking/**") 6 .filters(f -> f.rewritePath("/booking/(?.*)", "/${segment}") 7 .circuitBreaker(config -> config.setName("bookingServiceCircuitBreaker") 8 .setFallbackUri("forward:/contact-us")) 9 .requestRateLimiter(config -> config.setRateLimiter(redisRateLimiter()) 10 .setKeyResolver(userKeyResolver()))) 11 .retry(retryConfig -> retryConfig.setRetries(3) 12 .setMethods(HttpMethod.GET) 13 .setBackoff(Duration.ofMillis(100), Duration.ofMillis(1000), 2, true))) 14 .uri("http://bookingserver:9100")); // 이 프로젝트는 K8s Discovery Server를 사용하므로 쿠버네티스 클러스터 내 서비스 호출을 위해 DNS 이름을 사용합니다. 15}
Circuit Breaker를 적용해 다른 서비스로 에러가 전파되지 않도록
하고, Rate Limit을 적용해 잦은 빈도의 요청을 방지하며, Retry를
적용해 서비스 호출 시 실패 응답이 왔을 때 Backoff 전략을 통해 해당
서비스가 회복될 수 있도록 잠시 여유 시간을 갖고 다시 요청을
보내도록 합니다.
Keycloak의 구성과 활용
Keycloak을 활용해 인증 및 권한 관리 체계를 구축하면서
다음과 같은 개념을 익혔습니다.
-
Realm 구성: Keycloak에서 인증 설정의 단위를 나타내는 Realm을 활용하여 프로젝트의 독립적인 인증 도메인을 구성했습니다. 각 Realm은 자체적인 사용자를 관리하고, 독립된 인증 및 권한 정책을 적용할 수 있어 다양한 환경에서 유연하게 활용할 수 있습니다.
-
Client 설정: 각 마이크로서비스를 KeyCloak의 Client로 등록하여 OAuth2.0 기반 인증 흐름을 구성했습니다. 이를 통해 클라이언트가 액세스 토큰을 발급받아 리소스 서버에 안전하게 접근할 수 있습니다.
-
사용자 관리: KeyCloak의 사용자 관리 기능을 활용하여 사용자 생성, 역할(Role) 부여, 그룹(Group) 설정 등을 구성했습니다. 이를 통해 사용자별 권한 정책을 세밀하게 제어할 수 있습니다.
-
OAuth2.0 흐름 이해: Authorization Code Grant, Client Credentials Grant 등의 OAuth2.0 흐름을 적용하면서, 클라이언트 인증, 액세스 토큰 발급, 리프레시 토큰 처리 등 주요 인증 절차를 익혔습니다.
-
Spring Security와의 통합: Spring Security의 OAuth2.0 기능을 활용해 KeyCloak과 통합했습니다. 이를 통해 Gateway 서버(리소스 서버 역할)가 KeyCloak 인증 서버의 키 정보를 활용해 사용자를 검증할 수 있습니다.
-
Role-Based Access Control (RBAC): KeyCloak에서 정의한 역할(Role)을 기반으로 API의 접근 권한을 제어했습니다. 예를 들어, Admin, User와 같은 역할을 설정하고, 각 역할에 따라 접근 가능한 리소스를 제한할 수 있습니다.
PostgreSQL의 강점과 트랜잭션 관리
-
WAL(Write-Ahead Logging) 이해: PostgreSQL의 WAL 로그를 활용하여 데이터 변경 사항을 안정적으로 기록하고, Debezium을 통해 CDC(Change Data Capture)를 구현했습니다. 이 과정에서 WAL 로그의 역할과 트랜잭션 로그 기반 복제의 동작 원리를 이해할 수 있었습니다.
-
트랜잭션 일관성 유지: PostgreSQL의 ACID 특성을 활용하여 로컬 트랜잭션의 안정성을 보장하면서도, 이를 다른 마이크로서비스와 연계하기 위해 Outbox 패턴을 사용했습니다. 이를 통해 데이터의 일관성을 유지하면서도, 분산 환경에서 발생할 수 있는 트랜잭션 충돌을 효과적으로 방지할 수 있었습니다.
Debezium CDC를 활용한 데이터 변경 추적
-
Debezium의 동작 원리: Debezium을 사용하여 PostgreSQL의 데이터 변경 사항을 Kafka 토픽으로 스트리밍하며, CDC를 기반으로 한 실시간 데이터 동기화를 구현했습니다. Debezium이 WAL 로그를 읽어 변경 사항을 추출하고 이를 Kafka로 전송하는 과정을 직접 구현하면서, CDC 기술의 실질적인 동작 방식과 활용 가능성을 배웠습니다.
-
Outbox 패턴과 CDC의 결합: Outbox 테이블을 활용하여 트랜잭션 종료 시 데이터를 Outbox에 기록하고, Debezium이 이를 Kafka로 전달하도록 설계했습니다. 이 방식은 트랜잭션 커밋 시 Outbox 테이블에 데이터가 기록되므로, 데이터의 일관성과 신뢰성을 보장할 수 있습니다. 그리고 Debezium과 Kafka를 통해 이벤트를 비동기로 처리함으로써, 마이크로서비스 간의 강결합을 줄이고 성능을 최적화할 수 있습니다.
booking 서비스를 기준으로 Outbox와 CDC가 어떤 흐름으로 동작하는지
설명드리겠습니다.
1@Transactional 2public CreateBookingResponse createBooking(CreateBookingCommand createBookingCommand, String correlationId) { 3 // 가입된 회원인지 확인합니다. 4 validateMemberIsExists(createBookingCommand.email(), correlationId); 5 // 예약 가능한 호텔인지 확인합니다. 6 validateHotelOfferIsExists(createBookingCommand.hotelOfferId(), correlationId); 7 // 예약 가능한 항공권인지 확인합니다. 8 validateFlightOfferIsExists(createBookingCommand.flightOfferId(), correlationId); 9 // 예약 가능한 차량인지 확인합니다. 10 validateCarOfferIsExists(createBookingCommand.carOfferId(), correlationId); 11 12 // 예약 도메인 코어 서비스를 사용해 예약서를 생성하고 영속화 합니다. 13 BookingCreatedEvent bookingCreatedEvent = bookingDomainService.initializeBooking( 14 bookingMapper.toBooking(createBookingCommand)); 15 Booking savedBooking = saveBooking(bookingCreatedEvent.getBooking()); 16 17 /* 18 * 이벤트 전달: Booking 서비스 ---BookingCreatedEvent---> Hotel 서비스 19 * Saga 시작 단계이므로 Saga Action, Outbox 상태를 STARTED로 지정해 hotel_outbox 테이블에 BookingCreatedEvent 이벤트와 함께 저장합니다. 20 * Saga Action 상태는 BookingStatus에 따라 결정됩니다. 21 */ 22 saveHotelOutbox(bookingCreatedEvent); 23 24 return bookingMapper.toCreateBookingResponse(savedBooking, "호텔/항공권/차량 예약을 완료했습니다. 즐거운 여행되세요!"); 25} 26 27private void saveHotelOutbox(BookingCreatedEvent bookingCreatedEvent) { 28 hotelOutboxHelper.save( 29 bookingMapper.toBookingCreatedEventPayload(bookingCreatedEvent), 30 bookingCreatedEvent.getBooking().getBookingStatus(), 31 bookingSagaActionHelper.toSagaActionStatus(bookingCreatedEvent.getBooking().getBookingStatus()), 32 OutboxStatus.STARTED, 33 UUID.randomUUID() 34 ); 35}
유저가 호텔/항공권/차량 예약을 요청했을 때 booking 서비스에서
예약서를 생성하고 Hotel Outbox에 예약서 생성 완료 이벤트를
저장합니다. 그럼 Debezium Postgres Connector는 Hotel Outbox의
WAL에 CREATE가 발생한 것을 감지하고 이를 JSON으로 변환해 Kafka
토픽에 전달합니다. 예약서 생성 완료에 따른 호텔 예약을 수행해야
하는데, 이는 hotel 서비스의 Kafka Consumer가 담당합니다.
1// Outbox WAL의 관찰 대상은 생성 트랜잭션이므로 CREATE OPERATOR 인 경우에만 처리할 수 있도록 합니다. 2if (message.getBefore() != null && !(DebeziumOperator.CREATE.getValue().equals(message.getOp()))) { 3 return; 4} 5 6log.info("[IN-BOUND] 수신 메시지: {}, 키: {}, 파티션: {}, 오프셋: {}", message, key, partition, offset); 7 8// 카프카 메시지로부터 Avro 메시지와 EventPayload를 추출합니다. 9Value bookingCreatedEventAvroModel = message.getAfter(); 10BookingCreatedEventPayload bookingCreatedEventPayload = kafkaMessageHelper.getEventPayload( 11 bookingCreatedEventAvroModel.getEventPayload(), BookingCreatedEventPayload.class); 12 13try { 14 switch (HotelBookingStatus.valueOf(bookingCreatedEventPayload.getHotelBookingStatus())) { 15 // Booking 상태가 PENDING 인 경우 호텔 예약을 처리합니다. 16 case PENDING -> { 17 bookingCreatedEventListener.processHotelBooking(hotelMessageMapper 18 .toHotelBookingCommand(bookingCreatedEventPayload, bookingCreatedEventAvroModel)); 19 } 20 // Booking 상태가 CANCELLED 인 경우 호텔 예약을 취소 처리합니다. 21 case CANCELLED -> { 22 bookingCreatedEventListener.compensateHotelBooking(hotelMessageMapper 23 .toHotelBookingCommand(bookingCreatedEventPayload, bookingCreatedEventAvroModel)); 24 } 25 default -> log.warn("알 수 없는 호텔 예약 상태입니다. BookingId: {}", bookingCreatedEventPayload.getBookingId()); 26 } 27}
hotel 서비스의 Kafka Consumer가 이를 전달받아 hotel 예약을
수행하도록 합니다. 호텔 예약을 정상적으로 마치면 hotel 서비스 측
RDB Outbox 테이블에 호텔 예약 완료 이벤트를 저장하면 CDC를 통해
Kafka 토픽으로 자동 발행될 것이고, booking 서비스 측의 Kafka
Consumer가 이를 캐치해 예약서를 업데이트 하고 그 다음 flight
예약을 요청하는 방식입니다. 이렇게 booking hotel
booking
flight booking car booking 순서로 LLT(Long
Lived Transaction)를 제어합니다.
Kafka를 활용한 이벤트 기반 아키텍처
-
이벤트 전달 및 재처리: Kafka는 분산 환경에서 데이터 전송의 중추적인 역할을 했습니다. 특히, Kafka의 멱등성 보장 기능과 이벤트 재처리 전략을 통해 메시지 손실이나 중복으로 인한 데이터 불일치를 방지할 수 있었습니다.
-
Saga 오케스트레이션: Kafka와 함께 Saga 패턴을 적용하여 분산 트랜잭션을 제어했습니다. 각 마이크로서비스는 독립적으로 로컬 트랜잭션을 수행하며, 상태 변경 이벤트를 Kafka를 통해 전달받아 처리하도록 해 전체 트랜잭션을 조율하도록 설계했습니다.
다음은 Saga 상태 정의입니다.
1/** 2 * Saga Action 진행 상태 3 */ 4public enum SagaActionStatus { 5 6 /** 7 * Saga Action 시작 8 */ 9 STARTED, 10 11 /** 12 * Saga Action 진행 중 13 */ 14 PROCESSING, 15 16 /** 17 * Saga Action 보상 진행 중 18 */ 19 COMPENSATING, 20 21 /** 22 * Saga Action 성공 23 */ 24 SUCCEEDED, 25 26 /** 27 * Saga Action 보상 완료 28 */ 29 COMPENSATED, 30 31 /** 32 * Saga Action 실패 33 */ 34 FAILED, 35 36}
이 Saga 상태를 활용해 LLT의 현재 상태를 글로벌 하게 관리합니다.
그리고 이 Saga 상태 관리 주체는 Saga Orchestrator인 booking
서비스가 담당합니다. booking 서비스는 중앙에서 hotel, flight, car
예약을 요청하고 예약 상태에 따른 Saga 상태를 관리하는 역할입니다.
Saga + Outbox 패턴을 PostgreSQL, Debezium, Kafka로 구현하면서 느낀 점
-
기술 도구의 조합과 설계: PostgreSQL, Debezium, Kafka를 결합한 아키텍처는 강력했지만, 설계 단계에서 많은 고려가 필요했습니다. 도구 각각에 대한 최소 지식이 필요했으며, 적지 않은 러닝 커브가 발생했습니다. 실무에서 급하게 적용하기보다는 도구의 특성을 이해하고, 적용 가능한지, 이에 따라 얻는 이점은 확실한지 등을 알아볼 수 있는 충분한 시간이 필요해 보입니다.
-
성능과 확장성: 대규모 트래픽을 처리할 수 있고 확장성 있는 시스템을 구축하는 여러 방법 중 하나인 Outbox 패턴
- Kafka 비동기 메시징 + Debezium CDC를 도입했습니다. 하지만, Kafka와 CDC 도입에 따라 커지는 인프라의 복잡성은 쉽지 않은 새로운 도전 과제입니다. 성능 향상을 위한다는 명목만으로 무작정 달려들었다가는 나중에 비대해지고 이곳 저곳에 산재한 비동기 메시지가 사업 확장의 발목을 잡을 요인이 되기 쉬워 보였습니다. 토스뱅크를 예로 설명드리자면, 상당히 복잡한 아키텍처로 구성된 토스뱅크에서는 수백 개의 MSA 서버가 독립된 스키마(DB)를 바라보고 있고, 이 스키마들은 수십 개의 분리된 물리 서버 위에 그룹핑되어 존재한다고 합니다. 그리고 사용하는 DB 역시 Oracle, MySQL, MongoDB 등 다양한 DB를 Polyglot 형태로 사용한다고 합니다. 이것이 바로 Outbox를 적용하고자 할 때 트레이드 오프를 고려해야 하는 경계가 됩니다. 수백 개의 MSA에 일괄적으로 Outbox 패턴을 적용하려면, DB 종류, 물리 서버, 스키마마다 다양한 형태의 아웃박스 테이블들을 만들어줘야 하고, 테이블에서 메시지를 발행하는 애플리케이션도 각각 작성해야 합니다. 토스뱅크는 이러한 복잡도를 피하기 위해 Outbox 테이블을 따로 구성하는 게 아닌, Kafka 브로커에 이벤트를 발송하는 형태로 구축되었다고 합니다. 이는 Outbox 패턴이 만능은 아니라는 것을 여실히 보여주는 실제 사례인 것 같습니다.
Tech Stack
예약 여정과 트래픽을 처리하기 위해 헥사고날 아키텍처 기반 마이크로서비스, 제로 트러스트 보안, Observability 스택을 조합했습니다.
Back-end
Core Platform Services
예약 여정과 트래픽을 처리하는 핵심 마이크로서비스 구성요소
Java 17Spring BootSpring Cloud GatewaySpring BatchSpring Data JPASpring ValidationSpring ActuatorOpenFeignResilience4jSpring Cloud ConfigK8s Discovery Client
Security
Security & Identity
제로 트러스트 원칙을 따르는 인증/인가 파이프라인
Spring SecurityOAuth2Keycloak
Data
Data & Messaging
여정 상태를 일관되게 유지하기 위한 데이터/메시징 레이어
PostgresApache KafkaDebezium CDCElasticsearch
Observability
Observability & SRE
메트릭/로그/트레이스를 통합해 배포 안정성을 확보
OpenTelemetryMicrometerPrometheusGrafanaGrafana LokiGrafana Tempo
Infra
Infrastructure & Delivery
클러스터 기반 배포 자동화를 위한 인프라 도구
K8sDocker ComposeHelmSkaffold