Spring Cloud란? — MSA를 Spring으로 구현할 때 필요한 도구상자

최병현·2026년 6월 19일

spring boot

목록 보기
29/34

1. 개념 소개

Monolith와 MSA를 비교하면서 Feign Client, Circuit Breaker 같은 단어들이 나왔는데, 이것들이 전부 Spring Cloud라는 하나의 생태계에 속한 도구들이다.

Spring Cloud는 MSA 환경에서 흔히 마주치는 공통 문제들(서비스 탐색, 설정 관리, 서비스 간 통신, 장애 격리 등)을 Spring 방식으로 쉽게 해결할 수 있도록 모아놓은 프로젝트 모음이다.

Spring Boot가 "하나의 애플리케이션을 빠르게 만드는 도구"라면, Spring Cloud는 "여러 개의 Spring Boot 애플리케이션을 하나의 분산 시스템으로 엮는 도구"라고 보면 된다.

Spring Cloud는 단일 프레임워크가 아니라 여러 하위 프로젝트의 모음이다.

  • Spring Cloud Gateway — API Gateway
  • Spring Cloud Netflix Eureka — 서비스 디스커버리
  • Spring Cloud OpenFeign — 선언적 HTTP 클라이언트
  • Spring Cloud Config — 중앙 설정 관리
  • Spring Cloud CircuitBreaker — 장애 격리
  • Spring Cloud Sleuth / Micrometer Tracing — 분산 트레이싱

2. 왜 필요한가

MSA로 서비스를 쪼개기 시작하면 모놀리식에서는 없던 문제들이 새로 생긴다.

  • 서비스가 여러 개인데, 서로의 IP와 포트를 어떻게 알지? (서비스가 늘었다 줄었다 하는데)
  • 클라이언트가 회원 서비스, 주문 서비스, 결제 서비스 주소를 각각 다 알아야 하나?
  • 설정 파일(application.yml)을 서비스마다 따로 관리하면 너무 번거롭지 않나?
  • 다른 서비스를 호출하는 코드를 매번 RestTemplate으로 직접 짜야 하나?
  • 한 서비스가 응답하지 않으면 호출하는 쪽이 전부 멈춰버리지 않나?
  • 요청이 여러 서비스를 거쳐갈 때, 어디서 느려졌는지 어떻게 추적하지?

이 모든 문제를 직접 처음부터 구현하려면 엄청난 시간이 든다. Spring Cloud는 이런 분산 시스템의 공통 문제들에 대해 이미 검증된 해결책을 라이브러리로 제공한다. @FeignClient 애노테이션 하나로 서비스 간 통신을 처리하고, @EnableEurekaClient 하나로 서비스 등록과 탐색을 해결하는 식이다.


3. 전체 동작 흐름

Spring Cloud의 핵심 구성 요소들이 함께 동작하는 흐름을 "주문 생성" 요청으로 살펴보자.

[서비스 등록 단계 - 애플리케이션 시작 시]

User Service 시작
    ↓
Eureka Server에 자신을 등록 (IP, 포트, 서비스명)
    "user-service: 192.168.0.10:8081"

Order Service 시작
    ↓
Eureka Server에 자신을 등록
    "order-service: 192.168.0.11:8082"

Product Service 시작
    ↓
Eureka Server에 자신을 등록
    "product-service: 192.168.0.12:8083"


[요청 처리 단계 - 클라이언트 요청 시]

[Client]
    |
    | POST /api/orders
    ↓
[Spring Cloud Gateway] (단일 진입점, 포트 8080)
    |
    | ① 라우팅 규칙 확인: /api/orders → order-service로
    | ② Eureka에서 order-service의 현재 위치 조회
    ↓
[Order Service]
    |
    | ③ OpenFeign으로 User Service 호출
    |   UserServiceClient.getUser(userId)
    |   → 내부적으로 Eureka에서 user-service 위치 조회 후 호출
    ↓
[User Service] → 응답

[Order Service]
    |
    | ④ Circuit Breaker로 감싼 Product Service 호출
    |   만약 Product Service가 응답 없으면 → fallback 메서드 실행
    ↓
[Product Service] → 응답 (또는 장애 시 fallback)

[Order Service]
    |
    | ⑤ 주문 생성 완료, 응답 반환
    ↓
[Spring Cloud Gateway]
    ↓
[Client]
    Response

핵심은 각 서비스가 서로의 정확한 위치(IP, 포트)를 몰라도, Eureka를 통해 동적으로 찾아서 통신할 수 있다는 것이다. 서비스가 재시작되어 IP가 바뀌어도 Eureka에 다시 등록되면 자동으로 갱신된다.


4. 핵심 구성 요소

4-1. Spring Cloud Gateway

모든 클라이언트 요청을 받는 단일 진입점(API Gateway)이다. 요청 경로에 따라 적절한 서비스로 라우팅하고, 인증, 로깅, Rate Limiting 같은 공통 기능을 여기서 처리할 수 있다. 클라이언트는 개별 서비스 주소를 몰라도 Gateway 하나만 알면 된다.

4-2. Eureka — 서비스 디스커버리

각 서비스가 자신의 위치 정보를 등록하고, 다른 서비스의 위치를 조회할 수 있게 하는 레지스트리다.

  • Eureka Server — 서비스들의 위치 정보를 관리하는 중앙 저장소
  • Eureka Client — 각 마이크로서비스에 포함되어, 시작 시 자신을 Server에 등록하고 주기적으로 생존 신호(Heartbeat)를 보냄

4-3. OpenFeign — 선언적 HTTP 클라이언트

다른 서비스를 호출할 때, RestTemplate이나 WebClient로 직접 URL을 조립하고 호출하는 대신 인터페이스만 정의하면 구현체를 자동으로 만들어주는 라이브러리다. Eureka와 연동하면 서비스 이름만으로 호출할 수 있다.

4-4. Spring Cloud Config — 중앙 설정 관리

서비스가 많아지면 application.yml 설정도 서비스마다 따로 관리해야 한다. Config Server는 모든 서비스의 설정을 한 곳(Git 저장소 등)에서 중앙 관리하게 해준다. 설정이 바뀌면 서비스 재배포 없이 갱신할 수도 있다(Spring Cloud Bus와 결합 시).

4-5. Resilience4j — Circuit Breaker

한 서비스의 장애가 연쇄적으로 다른 서비스까지 퍼지는 것을 막는 역할이다. 일정 횟수 이상 호출이 실패하면 Circuit을 "열어서" 더 이상 호출을 시도하지 않고 즉시 fallback으로 응답한다. 일정 시간 후 다시 정상 여부를 확인한다.

4-6. Micrometer Tracing (구 Spring Cloud Sleuth)

하나의 요청이 여러 서비스를 거칠 때, 각 서비스 호출에 고유 추적 ID를 붙여서 전체 흐름을 하나로 연결해서 추적할 수 있게 해준다. Zipkin 같은 도구와 연동해서 시각화한다.


5. Spring Boot에서 어떻게 연결되는가

Spring Cloud는 Spring Boot 프로젝트에 의존성을 추가하는 방식으로 사용한다. 버전 관리는 Spring Cloud BOM(Bill of Materials)으로 Spring Boot 버전과 호환되는 버전을 맞춘다.

// build.gradle
ext {
    set('springCloudVersion', "2024.0.0")
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

각 마이크로서비스는 독립적인 Spring Boot 애플리케이션이고, 필요한 Spring Cloud 모듈만 골라서 추가한다.

[Eureka Server 프로젝트] — 별도의 독립 애플리케이션
    spring-cloud-starter-netflix-eureka-server

[각 마이크로서비스 - User, Order, Product Service]
    spring-cloud-starter-netflix-eureka-client (서비스 등록용)
    spring-cloud-starter-openfeign (서비스 간 호출용)

[Gateway 프로젝트] — 별도의 독립 애플리케이션
    spring-cloud-starter-gateway

6. 간단한 예제 코드

6-1. Eureka Server 구축

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
# application.yml (Eureka Server)
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false  # 자기 자신은 등록하지 않음
    fetch-registry: false

6-2. 마이크로서비스를 Eureka Client로 등록

@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
# application.yml (Order Service)
spring:
  application:
    name: order-service  # 이 이름으로 Eureka에 등록됨

server:
  port: 8082

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

6-3. OpenFeign으로 서비스 간 통신

@SpringBootApplication
@EnableFeignClients // Feign Client 활성화
public class OrderServiceApplication { ... }

// Eureka에 등록된 "user-service"라는 이름으로 자동 탐색해서 호출
@FeignClient(name = "user-service")
public interface UserServiceClient {

    @GetMapping("/users/{id}")
    UserResponse getUser(@PathVariable("id") Long id);
}

@Service
@RequiredArgsConstructor
public class OrderService {

    private final UserServiceClient userServiceClient;

    public Order createOrder(OrderRequest request) {
        // IP, 포트를 몰라도 서비스 이름만으로 호출 가능
        UserResponse user = userServiceClient.getUser(request.userId());
        // ...
    }
}

6-4. Circuit Breaker로 장애 격리

@Service
@RequiredArgsConstructor
public class OrderService {

    private final ProductServiceClient productServiceClient;

    @CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
    public ProductResponse getProduct(Long productId) {
        return productServiceClient.getProduct(productId);
    }

    public ProductResponse getProductFallback(Long productId, Throwable t) {
        // Product Service 장애 시 기본값 반환 — 전체 흐름이 멈추지 않음
        return ProductResponse.unavailable(productId);
    }
}
# application.yml — Resilience4j 설정
resilience4j:
  circuitbreaker:
    instances:
      productService:
        sliding-window-size: 10        # 최근 10번의 호출을 기준으로 판단
        failure-rate-threshold: 50     # 50% 이상 실패하면 Circuit Open
        wait-duration-in-open-state: 5s # 5초 후 재시도

6-5. Spring Cloud Gateway 라우팅 설정

# application.yml (Gateway)
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service       # lb:// = Eureka 기반 로드밸런싱
          predicates:
            - Path=/api/orders/**

        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/products/**

lb:// 접두사는 Eureka에 등록된 서비스 이름을 기반으로 로드밸런싱하며 라우팅한다는 의미다. 서비스 인스턴스가 여러 개라도 Gateway가 자동으로 분산시켜 전달한다.


7. 자주 헷갈리는 부분

Spring Cloud는 MSA를 자동으로 만들어주는 게 아니다

Spring Cloud를 추가한다고 해서 자동으로 MSA가 되는 게 아니다. 서비스를 어떻게 나눌지, 도메인 경계를 어떻게 설계할지는 여전히 개발자의 몫이다. Spring Cloud는 "이미 나눈 서비스들을 어떻게 연결하고 운영할 것인가"에 대한 도구일 뿐이다.

Spring Cloud Gateway vs Nginx

둘 다 리버스 프록시 역할을 하지만 성격이 다르다.

  • Nginx — 범용 웹서버/리버스 프록시. 정적 파일 서빙, SSL 종료에 강함. 설정이 텍스트 파일 기반
  • Spring Cloud Gateway — Spring 생태계와 통합된 Gateway. Eureka와 직접 연동되고, Java/YAML로 라우팅 로직을 유연하게 작성 가능

실무에서는 Nginx(또는 AWS ALB)가 가장 바깥의 진입점 역할을 하고, 그 안쪽에서 Spring Cloud Gateway가 마이크로서비스 라우팅을 담당하는 식으로 함께 쓰는 경우도 많다.

Eureka는 더 이상 Netflix에서 활발히 개발되지 않는다

Eureka는 Netflix OSS 프로젝트로 시작했지만, 현재는 유지보수 모드에 가깝다. Kubernetes 환경에서는 Kubernetes 자체의 서비스 디스커버리(Service, DNS)를 사용하는 경우가 많아져서, Eureka 대신 Consul이나 Kubernetes Native 방식을 선택하는 추세도 있다. 새 프로젝트라면 배포 환경(Kubernetes 여부)을 먼저 고려하는 게 좋다.


8. 실무에서 중요한 포인트

Spring Cloud Config와 Git의 조합

설정 파일을 Git 저장소에 두고 Config Server가 이를 읽어서 각 서비스에 제공하면, 설정 변경 이력을 Git 커밋으로 추적할 수 있고, 환경별(dev, staging, prod) 설정 분리도 브랜치나 디렉토리로 깔끔하게 관리할 수 있다.

Circuit Breaker의 fallback은 신중하게 설계

fallback이 단순히 빈 데이터를 반환하면 클라이언트 입장에서 혼란스러울 수 있다. "이 데이터는 일시적으로 사용 불가능하다"는 걸 명확히 알 수 있는 응답 구조를 설계해야 한다. 무조건 200 OK로 빈 값을 주기보다, 상태 코드나 플래그로 구분하는 게 좋다.

분산 트레이싱은 처음부터 넣어두는 게 좋다

서비스가 몇 개 안 될 때는 트레이싱 없이도 디버깅이 가능하지만, 서비스가 늘어날수록 트레이싱 없이는 문제를 찾기가 거의 불가능해진다. Micrometer Tracing + Zipkin(또는 Jaeger) 조합을 MSA 초기 단계부터 구축해두는 게 나중에 도입하는 것보다 훨씬 수월하다.

Spring Cloud 버전 호환성 확인

Spring Boot와 Spring Cloud는 버전 호환 매트릭스가 존재한다. Spring Boot 버전을 올릴 때 Spring Cloud 버전도 호환되는지 반드시 확인해야 한다. 호환되지 않는 조합은 애플리케이션 구동 자체가 실패하는 경우가 많다.


9. 정리

  • Spring Cloud는 MSA 환경의 공통 문제(서비스 탐색, 통신, 설정 관리, 장애 격리, 추적)를 해결하는 도구 모음이다
  • Eureka로 서비스가 서로의 위치를 동적으로 찾고, OpenFeign으로 서비스 이름만으로 간편하게 호출한다
  • Spring Cloud Gateway가 클라이언트의 단일 진입점 역할을 하며 Eureka와 연동해서 로드밸런싱까지 처리한다
  • Circuit Breaker(Resilience4j)로 한 서비스의 장애가 전체로 퍼지는 것을 막는다
  • Spring Cloud는 MSA를 자동으로 만들어주지 않는다 — 서비스 분리 설계는 여전히 개발자의 몫이다
  • Eureka는 유지보수 모드에 가까워서, Kubernetes 환경에서는 Native 서비스 디스커버리를 고려할 수 있다
  • 분산 트레이싱은 MSA 도입 초기부터 함께 구축해야 디버깅이 가능한 수준으로 운영할 수 있다

10. 느낀 점

MSA의 개념만 공부했을 때는 "서비스를 나누면 되는 거구나" 정도로 생각했는데, Spring Cloud의 구성 요소들을 하나씩 보면서 "나눈 다음에 해결해야 할 문제가 이렇게나 많구나"를 느꼈다. 서비스 위치 찾기, 설정 관리, 장애 전파 방지, 요청 추적까지 — 전부 모놀리식에서는 고민할 필요가 없던 것들이다.

Spring Cloud는 결국 "분산 시스템을 운영하는 비용"을 라이브러리 형태로 줄여주는 도구라는 걸 느꼈다. 이 도구들이 없었다면 MSA를 직접 구현하는 게 훨씬 더 힘들었을 것 같다. 다만 도구가 문제를 자동으로 없애주는 건 아니고, 여전히 각 개념(Circuit Breaker는 언제, Saga는 어떻게)을 제대로 이해하고 적용해야 한다는 걸 다시 느꼈다.

profile
Develop

0개의 댓글