
해당 포스트는 https://www.youtube.com/watch?v=yTSq6hJFmUg&t=193s 의 내용을 기반으로, 추가적인 기술적 해결 방안을 함께 정리한 글입니다.
마이크로서비스 아키텍처(MSA)는 넷플릭스, 아마존, 쿠팡 같은 대규모 서비스의 성공 사례와 함께 많은 개발 조직이 관심을 갖는 아키텍처 패턴이다. 독립적인 배포, 기술 스택의 유연성, 팀 단위 자율성 등의 장점이 존재한다.

하지만 잘못 도입하면 모놀리식보다 훨씬 복잡한 분산 모놀리스(Distributed Monolith)라는 결과를 만들어낼 수 있다. MSA를 도입하기 전에 아래와 같은 신호가 있는지 먼저 점검해봐야 한다.
MSA 도입을 고려해야 하는 시점:
이런 문제가 없다면, 모놀리식 아키텍처에서 시작해 점진적으로 전환하는 것이 훨씬 현명하다. 그럼 MSA를 도입한다고 결정했을 때, 어떤 문제들이 기다리고 있고 각각 어떻게 해결할 수 있는지 하나씩 살펴보자.
마이크로서비스는 서비스의 경계를 어떻게 나누는지가 중요하다. 너무 잘게 나누면 수백, 수천 개의 서비스가 생겨 관리 오버헤드가 폭발적으로 증가하고, 서비스 간 API 호출이 늘어나 네트워크 지연이 생긴다. 반대로 너무 크게 나누면 모놀리식과 다를 바 없는 구조가 된다.
모놀리식에서는 로그 파일로 디버깅이 가능했다. 하지만 MSA에서는 하나의 사용자 요청이 여러 서비스를 거치기 때문에, 어디서 문제가 발생했는지 추적하는 것 자체가 어려워진다. 서비스 간 의존성이 복잡해지면 예기치 못한 장애 지점을 사전에 인지하기도 힘들다.
주요 도구 조합:
| 영역 | 도구 | 역할 |
|---|---|---|
| 분산 추적 | Zipkin, Jaeger | 요청 흐름 추적, 서비스 간 호출 관계 시각화 |
| 로그 수집 | ELK Stack (Elasticsearch + Logstash + Kibana) | 중앙 집중식 로그 수집 및 검색 |
| 메트릭 모니터링 | Prometheus + Grafana | CPU, 메모리, 응답 시간 등 실시간 메트릭 수집 및 대시보드 |
| 알림 | Alertmanager, PagerDuty | 임계치 초과 시 즉시 알림 |
각 서비스에서 발생하는 로그에 Trace ID를 포함시키면, 문제가 발생했을 때 해당 ID로 전체 호출 체인을 한눈에 볼 수 있다.
수많은 서비스가 여러 서버에 분산되어 있을 때, IP 주소를 하드코딩하는 방식은 서비스가 스케일 아웃되거나 재배포될 때마다 깨지게 된다.
서비스 디스커버리는 크게 세 가지 방식으로 접근할 수 있다.
1) Client-Side Discovery: Spring Cloud, Netflix, Eureka
[서비스 A] → [Eureka Server에서 서비스 B 위치 조회] → [서비스 B 직접 호출]
각 서비스가 Eureka Server에 자신을 등록하고, 호출하는 측에서 레지스트리를 조회하여 대상 서비스의 위치를 찾는다.
2) Server-Side Discovery: API Gateway + Load Balancer
[서비스 A] → [API Gateway / Load Balancer] → [서비스 B]
Spring Cloud Gateway나 Kong 같은 API Gateway가 라우팅, 로드밸런싱, 인증을 한 곳에서 처리한다.
3) Service Mesh: Istio / Linkerd
[서비스 A + Sidecar Proxy] ↔ [서비스 B + Sidecar Proxy]
각 서비스 옆에 Sidecar Proxy를 배치하여 서비스 간 통신을 인프라 레벨에서 관리한다.
규모가 작다면 Eureka + API Gateway 조합으로 시작하고, 규모가 커지면 Service Mesh로 전환하는 것이 좋다.
서비스 간 통신이 늘어나면, 아무 서비스나 다른 서비스의 API를 호출하거나 메시지 큐에 임의의 메시지를 넣는 등의 악용이 가능해진다. 외부 사용자에 대한 인증뿐 아니라, 서비스 간 내부 통신에 대한 인증 및 인가도 반드시 필요하다.
[클라이언트] → [API Gateway: JWT 검증] → [서비스 A: JWT에서 권한 확인] → [서비스 B: JWT 전파]
동작 흐름:
1. 클라이언트가 로그인하면 API Gateway(또는 Auth 서비스)에서 JWT 토큰을 발급한다.
2. 이후 모든 요청은 JWT를 헤더에 포함하여 전달한다.
3. 각 서비스는 JWT의 서명을 검증하고, 토큰에 포함된 권한(scope/role)을 확인한다.
4. 서비스 간 내부 호출 시에도 JWT를 전파하거나, 별도의 서비스 간 인증(mTLS)을 적용한다.
서비스 간 통신 보안 강화:
각 서비스는 데이터베이스 접속 정보, API 키, 캐시 설정, 큐 연결 정보 등 다양한 설정값과 secrets를 관리해야 한다. 서비스마다 자체적으로 구성을 저장하면 코드베이스에 secrets가 노출되거나, 설정 변경 시 모든 서비스를 재배포해야 하는 문제가 발생한다.
| 도구 | 특징 |
|---|---|
| Spring Cloud Config | Git 저장소 기반, Spring 생태계와 자연스러운 통합 |
| HashiCorp Vault | Secrets 관리에 특화, 동적 자격증명 발급, 자동 만료 |
| AWS Parameter Store / Secrets Manager | AWS 환경에서 IAM 기반 접근 제어와 자동 로테이션 |
핵심 원칙:
마이크로서비스 아키텍처는 구성 요소가 많아 장애 발생 확률이 높다. 하나의 서비스 장애가 연쇄적으로 전파되어 전체 시스템이 다운되는 Cascading Failure가 MSA에서 가장 위험한 상황이다.
정상 상태 [Closed] → 실패 임계치 초과 → 차단 상태 [Open] → 일정 시간 후 → 반개방 [Half-Open] → 성공 시 → [Closed]
Resilience4j를 활용한 구현:
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() ->
paymentClient.requestPayment(request)
);
}
public CompletableFuture<PaymentResponse> paymentFallback(PaymentRequest request, Throwable t) {
// 결제 서비스 장애 시 대체 로직
return CompletableFuture.completedFuture(
PaymentResponse.pending("결제 서비스 일시 장애, 재시도 예정")
);
}
추가적인 Fault Tolerance 전략:
MSA에서 각 서비스가 자체 데이터베이스를 소유하면, 모놀리식에서 당연하게 사용하던 단일 트랜잭션이 불가능해진다. 예를 들어, "주문 생성 → 결제 처리 → 재고 차감"이 하나의 트랜잭션으로 묶이지 않기 때문에 중간에 실패하면 데이터 불일치가 발생한다.
Choreography 방식 (이벤트 기반):
[주문 서비스] --주문 생성 이벤트--> [결제 서비스] --결제 완료 이벤트--> [재고 서비스]
↓ 결제 실패 시
결제 실패 이벤트--> [주문 서비스: 주문 취소 보상 트랜잭션]
Orchestration 방식:
[Saga Orchestrator] → 1. 주문 생성 요청 → [주문 서비스]
→ 2. 결제 요청 → [결제 서비스]
→ 3. 재고 차감 요청 → [재고 서비스]
→ 실패 시 역순으로 보상 트랜잭션 실행
마이크로서비스 환경에서 테스트는 단일 서비스 테스트만으로는 충분하지 않다. 서비스가 연관된 기능을 통합 테스트하려면 해당 서비스들과 각각의 인프라를 모두 띄워야 하며, 상당한 비용과 시간이 소요된다.
* E2E 테스트 ← 최소한으로 (비용 높음)
* 통합 테스트 ← 핵심 시나리오 위주
* 계약 테스트 ← 서비스 간 API 호환성 검증
* 컴포넌트 테스트 ← 단일 서비스 + 외부 의존성 Mock
* 단위 테스트 ← 가장 많이 (비용 낮음)
계약 테스트(Contract Test): Spring Cloud Contract나 Pact 같은 도구를 사용하면, 서비스 간 API 스펙이 변경될 때 의존하는 서비스가 깨지는 것을 사전에 감지할 수 있다.
Testcontainers를 활용하면 Docker 기반으로 테스트에 필요한 데이터베이스, Redis, Kafka 등을 코드에서 직접 띄우고 관리할 수 있어 통합 테스트의 부담을 줄일 수 있다.
MSA에서의 의존성은 세 가지 차원에서 복잡성을 만든다.
서비스가 수십 개로 늘어나면 각각의 CI/CD 파이프라인을 관리해야 하고, 서비스 간 API 호환성을 유지하면서 독립 배포해야 하는 복잡성이 생긴다.
배포 방식 선택:
API Versioning을 통해 서비스 간 호환성을 관리한다. 새 API 버전을 배포하더라도 기존 버전을 일정 기간 유지하면 의존하는 서비스들이 순차적으로 마이그레이션할 수 있다.
MSA는 특정 문제를 해결하기 위한 아키텍처 도구이지, 그 자체가 목표가 되어서는 안 된다.
| 과제 | 핵심 해결 도구 |
|---|---|
| 서비스 관리 | DDD Bounded Context, Database per Service |
| 모니터링/로깅 | Zipkin/Jaeger, ELK Stack, Prometheus + Grafana |
| 서비스 디스커버리 | Eureka, Spring Cloud Gateway, Istio |
| 인증/인가 | JWT + API Gateway, mTLS, OAuth 2.0 |
| 구성 관리 | Spring Cloud Config, Vault, AWS Secrets Manager |
| 장애 허용 | Circuit Breaker (Resilience4j), Bulkhead, 비동기 통신 |
| 데이터 일관성 | Saga 패턴, Eventual Consistency, Outbox 패턴 |
| 테스트 | Contract Test, Testcontainers |
| 의존성 관리 | Semantic Versioning, API Versioning, 이벤트 기반 통신 |
| 배포 | Blue/Green, Canary, Argo Rollouts |