오늘은 MSA에 대해서 공부를 해보았다 예전 인프런에서 공부를 해보았는데 다시 한번 검토를 하니 좋은 시간이 었고 세부적인 내용도 추가적으로 기록할 예정이다.
본 포스팅은 MSA(Microservice Architecture)의 기본 개념부터 Spring Cloud 생태계, 장애 내성(Resilience4j), 중앙 설정 관리(Config Server & Bus), 그리고 분산 데이터 처리 및 K8s까지의 핵심 흐름을 정리한 글입니다.
하나의 거대한 애플리케이션(Monolithic)을 독립적으로 개발, 배포, 유지보수할 수 있는 여러 개의 작은 서비스 단위로 분리하는 소프트웨어 아키텍처 스타일입니다.
MSA 환경에서는 서비스 인스턴스가 동적으로 생성되고 소멸합니다. 유레카(Eureka)는 각 서비스의 IP와 포트 정보를 동적으로 관리하고 찾아주는 중앙 등록소 역할을 합니다.
@EnableEurekaServer: 유레카 서버 역할 명시@EnableDiscoveryClient: 유레카 클라이언트로 서버에 등록됨을 명시클라이언트와 마이크로서비스 단 사이의 단일 진입점(Single Point of Entry) 역할을 담당합니다.
GlobalFilter를 구현하여 Auth 서버에서 발급한 JWT 토큰의 인증/인가 및 만료 여부를 검증Spring Cloud에서 제공하는 선언적 HTTP 클라이언트로, 인터페이스와 어노테이션 정의만으로 RESTful 웹 서비스를 손쉽게 호출할 수 있습니다.
서버 사이드 로드 밸런서(예: L4/L7 스위치) 대신, 요청을 보내는 클라이언트가 직접 여러 서버 목록 중 하나를 선택하여 부하를 분산하는 방식입니다.
[Client Service] --- (Eureka에서 서버 목록 수집) ---> [Ribbon / LoadBalancer]
│
┌──────────────────┴──────────────────┐
▼ ▼
[Target Server A] [Target Server B]
외부 서비스 장애가 전체 시스템으로 전파되는 것을 막는 차단기 역할입니다.
스프링 클라우드 기본 추상화 레이어 대신, 직관적이고 가벼운 Resilience4j 라이브러리를 주로 사용합니다.
MSA 환경에서는 특정 서비스의 장애가 다른 서비스로 전파되지 않도록 장애 격리(Fault Isolation)가 중요하다.
Spring Cloud 환경에서는 Resilience4j를 사용해 Circuit Breaker, Retry, Rate Limiter, Bulkhead 등의 장애 대응 기능을 구현할 수 있다.
resilience4j:
circuitbreaker:
instances:
myServiceCircuitBreaker:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 5
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
주요 설정은 다음과 같다.
| 설정 | 의미 |
|---|---|
sliding-window-type | 실패율을 계산할 기준. COUNT_BASED 또는 TIME_BASED |
sliding-window-size | 실패율 계산에 사용할 호출 범위 |
minimum-number-of-calls | Circuit Breaker가 판단하기 위한 최소 호출 횟수 |
failure-rate-threshold | 실패율이 해당 비율 이상이면 OPEN |
wait-duration-in-open-state | OPEN 상태를 유지하는 시간 |
permitted-number-of-calls-in-half-open-state | HALF_OPEN에서 허용할 테스트 호출 수 |
automatic-transition-from-open-to-half-open-enabled | OPEN에서 HALF_OPEN으로 자동 전환할지 여부 |
Sliding Window는 Circuit Breaker가 최근 어떤 호출들을 기준으로 장애율을 계산할 것인지를 결정한다.
Sliding Window
│
"최근 어떤 호출을 볼 것인가?"
│
┌──────────┴──────────┐
↓ ↓
실패율 계산 Slow Call 계산
│ │
↓ ↓
failure-rate-threshold slow-call-rate-threshold
│ │
└──────────┬──────────┘
↓
조건 충족 여부
↓
CircuitBreaker OPEN
최근 N번의 호출을 기준으로 판단한다.
예를 들어:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
이라면 최근 10번의 호출을 기준으로 실패율을 계산한다.
최근 N초 동안 발생한 호출을 기준으로 판단한다.
즉,
COUNT_BASED → 최근 몇 번?
TIME_BASED → 최근 몇 초?
라고 이해하면 된다.
Circuit Breaker는 일반적으로 다음 세 가지 상태를 가진다.
CLOSED
│
│ 실패율 임계치 초과
▼
OPEN
│
│ 일정 시간 경과
▼
HALF_OPEN
│
├── 정상 → CLOSED
│
└── 실패 → OPEN
OPEN 상태에서는 외부 서비스에 실제 요청을 보내지 않고 호출을 차단한다.
이때 호출은 CallNotPermittedException으로 실패할 수 있으며, Fallback을 설정했다면 Fallback 로직으로 처리할 수 있다.
Fallback은 외부 서비스 호출이 실패하거나 Circuit Breaker가 요청을 차단했을 때 대체 동작을 수행하는 로직이다.
예를 들어:
@CircuitBreaker(
name = "payment",
fallbackMethod = "fallback"
)
fun requestPayment(): PaymentResponse {
return paymentClient.request()
}
fun fallback(e: Exception): PaymentResponse {
return PaymentResponse.failed()
}
역할을 구분하면 다음과 같다.
Retry
→ 다시 시도
Circuit Breaker
→ 호출을 계속할지 차단할지 판단
Fallback
→ 호출 실패/차단 이후 무엇을 할지 결정
Circuit Breaker 내부에서는 다양한 이벤트가 발생한다.
예를 들어:
등이 있다.
Event Listener는 이러한 이벤트를 받아 로그, 메트릭, 알림 등의 추가 작업을 수행한다.
┌─────────────────────┐
│ CircuitBreaker │
│ │
│ Success │
│ Error │
│ State Transition │
│ Call Not Permitted │
└──────────┬──────────┘
│ Event
▼
┌─────────────────────┐
│ EventListener │
│ │
│ 로그 │
│ 메트릭 │
│ 알림 │
└─────────────────────┘
중요한 점은 Event Listener가 Circuit Breaker의 장애 대응을 수행하는 것이 아니라, Circuit Breaker에서 발생한 이벤트를 관찰하는 역할이라는 것이다.
Resilience4j의 메트릭을 Micrometer 등을 통해 수집하고 Prometheus와 Grafana를 연결하면 Circuit Breaker 상태를 시각화할 수 있다.
Application
│
▼
Resilience4j
│
▼
Micrometer
│
▼
Prometheus
│
▼
Grafana
MSA에서는 여러 서비스가 존재하기 때문에 각 서비스의 설정을 개별적으로 관리하면 설정 변경과 관리가 어려워진다.
이를 해결하기 위해 Spring Cloud Config를 사용할 수 있다.
Spring Cloud Config는 분산 시스템의 설정 파일을 중앙에서 관리할 수 있도록 해준다.
일반적으로 Git과 함께 사용한다.
Git Repository
│
▼
Config Server
│
├──────────┐
▼ ▼
Order Payment
Service Service
Config Server는 설정을 직접 사용하는 것이 아니라 각 마이크로서비스가 필요한 설정을 제공하는 역할을 한다.
/actuator/refresh설정 파일의 값을 변경했다고 해서 이미 실행 중인 Spring Bean의 값이 자동으로 변경되는 것은 아니다.
@RefreshScope를 적용한 Bean은 /actuator/refresh를 호출하여 설정을 다시 반영할 수 있다.
Git
│
│ 설정 변경
▼
Config Server
│
▼
Order Service
│
│ POST /actuator/refresh
▼
설정 재조회
│
▼
RefreshScope Bean 갱신
예를 들어:
@RefreshScope
@Component
class PaymentProperties(
@Value("\${payment.timeout}")
private val timeout: Long
)
설정이 변경된 후:
POST /actuator/refresh
를 호출하면 해당 Refresh Scope Bean을 다시 생성하여 변경된 설정을 반영할 수 있다.
/actuator/refresh자체가 설정을 변경하는 것은 아니다.
해당 서비스가 Config Server에서 최신 설정을 다시 가져오도록 갱신을 트리거하는 역할이다.
서비스가 많아지면 각각의 서비스에 /actuator/refresh를 호출하는 것은 번거롭다.
이때 Spring Cloud Bus를 사용할 수 있다.
Spring Cloud Bus는 RabbitMQ, Kafka 등의 메시지 브로커를 이용하여 설정 변경 이벤트를 여러 서비스에 전파한다.
Config Server
│
│ 설정 변경 이벤트
▼
Message Broker
(RabbitMQ/Kafka)
│
┌────────┼────────┐
▼ ▼ ▼
Order Payment Inventory
│ │ │
Refresh Refresh Refresh
따라서 하나의 서비스에서 Bus Refresh 이벤트를 발생시키면 연결된 여러 서비스에 설정 변경 이벤트를 전달할 수 있다.
POST /actuator/busrefresh
/refresh는 개별 서비스의 설정 갱신에 사용하고,
/busrefresh는 Spring Cloud Bus를 통해 여러 서비스에 갱신 이벤트를 전파하는 방식이다.
MSA에서는 서비스마다 DB가 분리될 수 있다.
예를 들어:
Order Service
│
└── Order DB
Payment Service
│
└── Payment DB
Inventory Service
│
└── Inventory DB
따라서 하나의 트랜잭션으로 여러 서비스의 DB를 묶기 어려워진다.
예를 들어 주문 생성 후 재고 차감이 필요한 경우:
Order 생성
↓
Inventory 차감
↓
Payment 처리
중간 단계에서 장애가 발생하면 서비스 간 데이터 상태가 달라질 수 있다.
Order DB → 주문 생성 완료
Inventory DB → 재고 차감 실패
Payment DB → 결제 처리 안 됨
이처럼 분산 환경에서는 모든 서비스를 하나의 DB 트랜잭션처럼 묶는 것이 어렵다.
대표적으로 다음과 같은 방법을 고려할 수 있다.
핵심은 서비스 간 데이터 일관성을 어떻게 유지할 것인가이다.
Spring Cloud Stream 등을 이용하면 Producer와 Consumer 간 비동기 메시지 기반 통신을 구성할 수 있다.
Order Service
│
│ OrderCreated Event
▼
Message Broker
│
├──────────────┐
▼ ▼
Inventory Notification
Service Service
Producer가 이벤트를 발행하면 Consumer가 해당 이벤트를 비동기적으로 처리한다.
이때 실제 운영 환경에서는 다음 문제를 함께 고려해야 한다.
따라서 단순히 메시지를 발행하는 것보다 장애가 발생했을 때 어떻게 복구할 것인지가 중요하다.
MSA에서는 하나의 사용자 요청이 여러 서비스를 거칠 수 있다.
Client
│
▼
Gateway
│
▼
Order
│
├──→ Payment
│
└──→ Inventory
Order 요청이 느려졌을 때 실제 원인이 Order인지 Payment인지 Inventory인지 확인하기 어려울 수 있다.
이때 Distributed Tracing을 사용한다.
Request
│
▼
Trace ID: abc123
│
├── Gateway
├── Order
├── Payment
└── Inventory
하나의 요청에 Trace ID를 부여하고 각 서비스의 호출 정보를 연결하면 전체 요청 흐름을 추적할 수 있다.
Spring 생태계에서는 Micrometer를 이용하여 메트릭 및 관측 데이터를 다룰 수 있다.
분산 추적 환경에서는 Trace ID, Span 등의 정보를 이용해 서비스 간 요청 흐름을 연결한다.
Zipkin은 분산 시스템의 Trace 데이터를 수집하고 시각화하는 시스템이다.
Gateway
↓
Order
↓
Payment
↓
Inventory
각 호출에 대한 시간을 확인하여 어느 구간에서 지연이 발생했는지 분석할 수 있다.
Kubernetes는 컨테이너화된 애플리케이션을 배포하고 운영하기 위한 컨테이너 오케스트레이션 플랫폼이다.
MSA 환경에서는 여러 서비스와 여러 인스턴스를 운영해야 하기 때문에 Kubernetes를 활용할 수 있다.
대표적인 기능은 다음과 같다.
예를 들어 트래픽이 증가하면:
평상시
Order Pod
├── Pod 1
└── Pod 2
트래픽 증가
Order Pod
├── Pod 1
├── Pod 2
├── Pod 3
├── Pod 4
└── Pod 5
HPA(Horizontal Pod Autoscaler)를 이용하면 CPU 사용량이나 기타 지표에 따라 Pod 개수를 자동으로 조절할 수 있다.
지금까지 살펴본 기술을 하나의 MSA 구조로 연결하면 다음과 같다.
Client
│
▼
┌─────────────────┐
│ API Gateway │
└────────┬────────┘
│
Load Balancer
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
Order Service Payment Service Inventory Service
│ │ │
└──────────┬───────┴──────────┬───────┘
│ │
▼ ▼
Eureka Message Broker
│ │
│ Event Driven
│ │
▼ ▼
Service Discovery Async Processing
┌─────────────────────────┐
│ Spring Cloud Config │
└────────────┬────────────┘
│
▼
Git
Observability
Application
│
├── Resilience4j
├── Micrometer
│
▼
Prometheus
│
▼
Grafana
Distributed Tracing
Services
│
▼
Trace Data
│
▼
Zipkin
| 구분 | 기술 | 역할 |
|---|---|---|
| Discovery | Spring Cloud Eureka | 서비스 인스턴스 등록 및 위치 조회 |
| Gateway | Spring Cloud Gateway | 외부 요청의 진입점 및 라우팅 |
| Communication | FeignClient + LoadBalancer | 서비스 간 REST 호출 및 인스턴스 부하 분산 |
| Resilience | Resilience4j | Circuit Breaker 등 장애 격리 |
| Config | Spring Cloud Config | 중앙 설정 관리 |
| Config Propagation | Spring Cloud Bus | 설정 변경 이벤트 전파 |
| Messaging | Kafka / RabbitMQ | 서비스 간 비동기 메시지 전달 |
| Observability | Micrometer + Prometheus + Grafana | 메트릭 수집 및 모니터링 |
| Tracing | Micrometer Tracing + Zipkin | 분산 요청 흐름 추적 |
| Container | Docker | 애플리케이션 컨테이너화 |
| Orchestration | Kubernetes | 컨테이너 배포 및 운영 자동화 |
MSA에서 중요한 것은 각각의 기술을 따로 외우는 것이 아니라 각 기술이 어떤 문제를 해결하기 위해 존재하는지 연결해서 이해하는 것이다.
서비스가 많아짐
↓
서비스 위치를 어떻게 찾지?
→ Eureka
외부 요청을 어떻게 통제하지?
→ API Gateway
서비스끼리 어떻게 호출하지?
→ Feign + LoadBalancer
상대 서비스가 장애 나면?
→ Resilience4j
설정이 여러 서비스에 흩어지면?
→ Spring Cloud Config
설정 변경을 여러 서비스에 어떻게 전파하지?
→ Spring Cloud Bus
서비스 간 비동기 통신이 필요하면?
→ Kafka / RabbitMQ
요청이 여러 서비스를 거치면 장애 원인을 어떻게 찾지?
→ Distributed Tracing
서비스 인스턴스가 많아지면 어떻게 운영하지?
→ Kubernetes
결국 MSA의 핵심은 "기술을 많이 사용하는 것"이 아니라, 분산 시스템에서 발생하는 문제를 어떤 기술로 해결할 것인지 판단하는 것이다.