TIL 2일차

HanEol~·4일 전
post-thumbnail

개요

오늘은 MSA에 대해서 공부를 해보았다 예전 인프런에서 공부를 해보았는데 다시 한번 검토를 하니 좋은 시간이 었고 세부적인 내용도 추가적으로 기록할 예정이다.

[MSA] Spring Cloud 기반 마이크로서비스 아키텍처 핵심 개념 정리

본 포스팅은 MSA(Microservice Architecture)의 기본 개념부터 Spring Cloud 생태계, 장애 내성(Resilience4j), 중앙 설정 관리(Config Server & Bus), 그리고 분산 데이터 처리 및 K8s까지의 핵심 흐름을 정리한 글입니다.


📌 목차

  1. MSA(Microservice Architecture) 개요
  2. 서비스 디스커버리 & API 게이트웨이
  3. 서비스 간 통신 및 로드 밸런싱
  4. 서킷 브레이커 & 장애 대응 (Resilience4j)
  5. 중앙 집중식 설정 관리
  6. 마이크로서비스 간 통신과 분산 데이터 관리
  7. 분산 추적 & 쿠버네티스
  8. 전체 구조 정리

1. MSA(Microservice Architecture) 개요

MSA란?

하나의 거대한 애플리케이션(Monolithic)을 독립적으로 개발, 배포, 유지보수할 수 있는 여러 개의 작은 서비스 단위로 분리하는 소프트웨어 아키텍처 스타일입니다.

  • 전환 이유: 확장성(Scalability), 신뢰성(Fault Isolation), 빠른 개발 및 배포 속도
  • 주요 도전 과제:
    • 도메인마다 서버와 DB가 분리되어 있어 시스템 복잡도 증가
    • 운영 비용 및 네트워크 통신 비용 증가
    • 분산 데이터 관리 및 트랜잭션 처리의 어려움

2. 서비스 디스커버리 & API 게이트웨이

서비스 디스커버리 (Eureka)

MSA 환경에서는 서비스 인스턴스가 동적으로 생성되고 소멸합니다. 유레카(Eureka)는 각 서비스의 IP와 포트 정보를 동적으로 관리하고 찾아주는 중앙 등록소 역할을 합니다.

  • @EnableEurekaServer: 유레카 서버 역할 명시
  • @EnableDiscoveryClient: 유레카 클라이언트로 서버에 등록됨을 명시

API 게이트웨이 (API Gateway)

클라이언트와 마이크로서비스 단 사이의 단일 진입점(Single Point of Entry) 역할을 담당합니다.

  • 주요 역할: 유레카에 등록된 서비스 위치를 기반으로 요청 라우팅
  • 보안 구성: GlobalFilter를 구현하여 Auth 서버에서 발급한 JWT 토큰의 인증/인가 및 만료 여부를 검증

3. 서비스 간 통신 및 로드 밸런싱

FeignClient

Spring Cloud에서 제공하는 선언적 HTTP 클라이언트로, 인터페이스와 어노테이션 정의만으로 RESTful 웹 서비스를 손쉽게 호출할 수 있습니다.

클라이언트 사이드 로드 밸런싱 (Ribbon / Spring Cloud LoadBalancer)

서버 사이드 로드 밸런서(예: L4/L7 스위치) 대신, 요청을 보내는 클라이언트가 직접 여러 서버 목록 중 하나를 선택하여 부하를 분산하는 방식입니다.


[Client Service] --- (Eureka에서 서버 목록 수집) ---> [Ribbon / LoadBalancer]
│
┌──────────────────┴──────────────────┐
▼                                     ▼
[Target Server A]                     [Target Server B]
  • FeignClient 내부에는 로드 밸런서가 통합되어 있어 별도 구현 없이 자동 로드 밸런싱이 수행됩니다.

4. 서킷 브레이커 & 장애 대응 (Resilience4j)

Circuit Breaker

외부 서비스 장애가 전체 시스템으로 전파되는 것을 막는 차단기 역할입니다.
스프링 클라우드 기본 추상화 레이어 대신, 직관적이고 가벼운 Resilience4j 라이브러리를 주로 사용합니다.

Resilience4j

MSA 환경에서는 특정 서비스의 장애가 다른 서비스로 전파되지 않도록 장애 격리(Fault Isolation)가 중요하다.

Spring Cloud 환경에서는 Resilience4j를 사용해 Circuit Breaker, Retry, Rate Limiter, Bulkhead 등의 장애 대응 기능을 구현할 수 있다.

4.1 Circuit Breaker 설정

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-callsCircuit Breaker가 판단하기 위한 최소 호출 횟수
failure-rate-threshold실패율이 해당 비율 이상이면 OPEN
wait-duration-in-open-stateOPEN 상태를 유지하는 시간
permitted-number-of-calls-in-half-open-stateHALF_OPEN에서 허용할 테스트 호출 수
automatic-transition-from-open-to-half-open-enabledOPEN에서 HALF_OPEN으로 자동 전환할지 여부

Sliding Window

Sliding Window는 Circuit Breaker가 최근 어떤 호출들을 기준으로 장애율을 계산할 것인지를 결정한다.

                    Sliding Window
                         │
             "최근 어떤 호출을 볼 것인가?"
                         │
              ┌──────────┴──────────┐
              ↓                     ↓
          실패율 계산             Slow Call 계산
              │                     │
              ↓                     ↓
 failure-rate-threshold    slow-call-rate-threshold
              │                     │
              └──────────┬──────────┘
                         ↓
                  조건 충족 여부
                         ↓
                CircuitBreaker OPEN

COUNT_BASED

최근 N번의 호출을 기준으로 판단한다.

예를 들어:

sliding-window-type: COUNT_BASED
sliding-window-size: 10

이라면 최근 10번의 호출을 기준으로 실패율을 계산한다.

TIME_BASED

최근 N초 동안 발생한 호출을 기준으로 판단한다.

즉,

COUNT_BASED → 최근 몇 번?
TIME_BASED  → 최근 몇 초?

라고 이해하면 된다.

Circuit Breaker 동작

Circuit Breaker는 일반적으로 다음 세 가지 상태를 가진다.

CLOSED
  │
  │ 실패율 임계치 초과
  ▼
OPEN
  │
  │ 일정 시간 경과
  ▼
HALF_OPEN
  │
  ├── 정상 → CLOSED
  │
  └── 실패 → OPEN

OPEN 상태에서는 외부 서비스에 실제 요청을 보내지 않고 호출을 차단한다.

이때 호출은 CallNotPermittedException으로 실패할 수 있으며, Fallback을 설정했다면 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
→ 호출 실패/차단 이후 무엇을 할지 결정

4.2 Event Listener를 이용한 모니터링

Circuit Breaker 내부에서는 다양한 이벤트가 발생한다.

예를 들어:

  • Success
  • Error
  • State Transition
  • Call Not Permitted

등이 있다.

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

5. 중앙 집중식 설정 관리

MSA에서는 여러 서비스가 존재하기 때문에 각 서비스의 설정을 개별적으로 관리하면 설정 변경과 관리가 어려워진다.

이를 해결하기 위해 Spring Cloud Config를 사용할 수 있다.

5.1 Spring Cloud Config

Spring Cloud Config는 분산 시스템의 설정 파일을 중앙에서 관리할 수 있도록 해준다.

일반적으로 Git과 함께 사용한다.

Git Repository
      │
      ▼
Config Server
      │
      ├──────────┐
      ▼          ▼
 Order        Payment
 Service      Service

Config Server는 설정을 직접 사용하는 것이 아니라 각 마이크로서비스가 필요한 설정을 제공하는 역할을 한다.


5.2 설정 갱신

수동 갱신 - /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에서 최신 설정을 다시 가져오도록 갱신을 트리거하는 역할이다.


5.3 Spring Cloud Bus

서비스가 많아지면 각각의 서비스에 /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를 통해 여러 서비스에 갱신 이벤트를 전파하는 방식이다.


6. 마이크로서비스 간 통신과 분산 데이터 관리

MSA에서는 서비스마다 DB가 분리될 수 있다.

예를 들어:

Order Service
     │
     └── Order DB

Payment Service
     │
     └── Payment DB

Inventory Service
     │
     └── Inventory DB

따라서 하나의 트랜잭션으로 여러 서비스의 DB를 묶기 어려워진다.

6.1 데이터 동기화 문제

예를 들어 주문 생성 후 재고 차감이 필요한 경우:

Order 생성
   ↓
Inventory 차감
   ↓
Payment 처리

중간 단계에서 장애가 발생하면 서비스 간 데이터 상태가 달라질 수 있다.

Order DB      → 주문 생성 완료
Inventory DB  → 재고 차감 실패
Payment DB    → 결제 처리 안 됨

이처럼 분산 환경에서는 모든 서비스를 하나의 DB 트랜잭션처럼 묶는 것이 어렵다.

해결 방향

대표적으로 다음과 같은 방법을 고려할 수 있다.

  • 최종적 일관성(Eventual Consistency)
  • 메시지 브로커를 이용한 비동기 이벤트 전달
  • Outbox Pattern
  • Saga Pattern
  • 보상 트랜잭션
  • 재처리 및 DLQ

핵심은 서비스 간 데이터 일관성을 어떻게 유지할 것인가이다.


6.2 이벤트 드리븐 아키텍처

Spring Cloud Stream 등을 이용하면 Producer와 Consumer 간 비동기 메시지 기반 통신을 구성할 수 있다.

Order Service
     │
     │ OrderCreated Event
     ▼
Message Broker
     │
     ├──────────────┐
     ▼              ▼
Inventory        Notification
 Service           Service

Producer가 이벤트를 발행하면 Consumer가 해당 이벤트를 비동기적으로 처리한다.

이때 실제 운영 환경에서는 다음 문제를 함께 고려해야 한다.

  • 메시지 중복
  • 메시지 유실
  • Consumer 장애
  • 재처리
  • 순서 보장
  • DLQ(Dead Letter Queue)
  • Idempotency

따라서 단순히 메시지를 발행하는 것보다 장애가 발생했을 때 어떻게 복구할 것인지가 중요하다.


7. 분산 추적 & 쿠버네티스

7.1 Distributed Tracing

MSA에서는 하나의 사용자 요청이 여러 서비스를 거칠 수 있다.

Client
  │
  ▼
Gateway
  │
  ▼
Order
  │
  ├──→ Payment
  │
  └──→ Inventory

Order 요청이 느려졌을 때 실제 원인이 Order인지 Payment인지 Inventory인지 확인하기 어려울 수 있다.

이때 Distributed Tracing을 사용한다.

Request
  │
  ▼
Trace ID: abc123
  │
  ├── Gateway
  ├── Order
  ├── Payment
  └── Inventory

하나의 요청에 Trace ID를 부여하고 각 서비스의 호출 정보를 연결하면 전체 요청 흐름을 추적할 수 있다.

Micrometer

Spring 생태계에서는 Micrometer를 이용하여 메트릭 및 관측 데이터를 다룰 수 있다.

분산 추적 환경에서는 Trace ID, Span 등의 정보를 이용해 서비스 간 요청 흐름을 연결한다.

Zipkin

Zipkin은 분산 시스템의 Trace 데이터를 수집하고 시각화하는 시스템이다.

Gateway
   ↓
Order
   ↓
Payment
   ↓
Inventory

각 호출에 대한 시간을 확인하여 어느 구간에서 지연이 발생했는지 분석할 수 있다.


7.2 Kubernetes

Kubernetes는 컨테이너화된 애플리케이션을 배포하고 운영하기 위한 컨테이너 오케스트레이션 플랫폼이다.

MSA 환경에서는 여러 서비스와 여러 인스턴스를 운영해야 하기 때문에 Kubernetes를 활용할 수 있다.

대표적인 기능은 다음과 같다.

  • 컨테이너 배포
  • 서비스 디스커버리
  • 로드 밸런싱
  • 자동 스케일링
  • Self-healing
  • Rolling Update
  • 장애가 발생한 Pod 재시작

예를 들어 트래픽이 증가하면:

평상시

Order Pod
   ├── Pod 1
   └── Pod 2


트래픽 증가

Order Pod
   ├── Pod 1
   ├── Pod 2
   ├── Pod 3
   ├── Pod 4
   └── Pod 5

HPA(Horizontal Pod Autoscaler)를 이용하면 CPU 사용량이나 기타 지표에 따라 Pod 개수를 자동으로 조절할 수 있다.


8. 전체 구조 정리

지금까지 살펴본 기술을 하나의 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

기술 요약

구분기술역할
DiscoverySpring Cloud Eureka서비스 인스턴스 등록 및 위치 조회
GatewaySpring Cloud Gateway외부 요청의 진입점 및 라우팅
CommunicationFeignClient + LoadBalancer서비스 간 REST 호출 및 인스턴스 부하 분산
ResilienceResilience4jCircuit Breaker 등 장애 격리
ConfigSpring Cloud Config중앙 설정 관리
Config PropagationSpring Cloud Bus설정 변경 이벤트 전파
MessagingKafka / RabbitMQ서비스 간 비동기 메시지 전달
ObservabilityMicrometer + Prometheus + Grafana메트릭 수집 및 모니터링
TracingMicrometer Tracing + Zipkin분산 요청 흐름 추적
ContainerDocker애플리케이션 컨테이너화
OrchestrationKubernetes컨테이너 배포 및 운영 자동화

마무리

MSA에서 중요한 것은 각각의 기술을 따로 외우는 것이 아니라 각 기술이 어떤 문제를 해결하기 위해 존재하는지 연결해서 이해하는 것이다.

서비스가 많아짐
    ↓
서비스 위치를 어떻게 찾지?
    → Eureka

외부 요청을 어떻게 통제하지?
    → API Gateway

서비스끼리 어떻게 호출하지?
    → Feign + LoadBalancer

상대 서비스가 장애 나면?
    → Resilience4j

설정이 여러 서비스에 흩어지면?
    → Spring Cloud Config

설정 변경을 여러 서비스에 어떻게 전파하지?
    → Spring Cloud Bus

서비스 간 비동기 통신이 필요하면?
    → Kafka / RabbitMQ

요청이 여러 서비스를 거치면 장애 원인을 어떻게 찾지?
    → Distributed Tracing

서비스 인스턴스가 많아지면 어떻게 운영하지?
    → Kubernetes

결국 MSA의 핵심은 "기술을 많이 사용하는 것"이 아니라, 분산 시스템에서 발생하는 문제를 어떤 기술로 해결할 것인지 판단하는 것이다.

profile
기록을 통해 앞으로 나아가자!

0개의 댓글