RECENT POSTS

최근에 작성한 글

01 / 03

전체 글

전체 보기

[TIL] MSA - Eureka, LoadBalancer, FeignClient 이해하기

MSA(Microservice Architecture)

MSA는 하나의 애플리케이션을 여러 개의 독립적인 서비스로 분리하여 개발하는 아키텍처. 각 서비스는 독립적으로 배포 및 확장이 가능하며, 서로 HTTP 통신을 통해 데이터를 주고받는다.

예를 들어 쇼핑몰이라면 다음과 같이 서비스를 분리할 수 있다.

User Service
Order Service
Product Service

각 서비스는 서로 다른 서버에서 실행될 수 있기 때문에 서비스 간 통신이 필요하다.


Service Discovery(Eureka)

서비스가 여러 대의 서버에서 실행된다면 서버의 IP와 Port를 직접 관리하기가 매우 어렵다.

예를 들어 Product Service가 3개의 서버에서 실행되고 있다고 가정하면

Product Service
- 192.168.0.10:8080
- 192.168.0.11:8080
- 192.168.0.12:8080

Order Service는 이 모든 주소를 알고 있어야 Product Service를 호출할 수 있다.

하지만 서버가 추가되거나 종료될 때마다 모든 서비스의 주소를 수정해야 하는 문제가 발생한다.

이를 해결하기 위해 Eureka(Service Discovery) 를 사용한다.

Eureka는 실행 중인 서비스의 IP와 Port를 등록하고 관리하는 서비스 디스커버리 서버이다. 각 서비스는 실행되면서 자신의 정보를 Eureka에 등록하고, 다른 서비스는 Eureka를 통해 필요한 서비스의 위치를 조회한다.

즉,

  • 서비스 등록(Service Registration)
  • 서비스 검색(Service Discovery)
  • 헬스 체크(Health Check)

를 담당한다.


Eureka는 로드밸런서를 하는 것이 아니다.

처음에는 Eureka가 요청을 적절한 서버로 보내주는 로드밸런서 역할을 하는게 아닌가 라고 생각했다.

하지만 실제 역할은 서비스 목록을 관리하는 것이다.

실제 요청 흐름은 다음과 같다.

동작 순서는 다음과 같다.

  1. 사용자가 Gateway로 요청을 보낸다.
  2. Gateway는 Eureka에게 Product Service가 어디에서 실행 중인지 조회한다.
  3. Eureka는 실행 중인 Product Service 인스턴스 목록을 반환한다.
  4. Spring Cloud LoadBalancer가 여러 인스턴스 중 하나를 선택한다.
  5. 선택된 서버로 요청을 전달한다.

즉,

  • Eureka → 서비스 위치를 알려주는 역할
  • LoadBalancer → 어느 서버로 요청을 보낼지 결정하는 역할

이다.


FeignClient는 왜 사용할까?

처음에는

"RestController가 있는데 왜 FeignClient를 사용할까?"

라는 의문이 들었다.

같은 프로젝트라면

Controller → Service → Repository 구조이기 때문에

userService.findById(id);

처럼 Service 메서드를 직접 호출하면 된다.


하지만 MSA에서는

Order Service와 User Service는 서로 다른 Spring Boot 서버이다.

Order Service (8081)

User Service (8082)

서버가 다르기 때문에

userService.findById(id);

처럼 메서드를 직접 호출할 수 없다.

대신 HTTP 요청을 보내야 한다.

FeignClient는 이러한 서비스 간 HTTP 통신을 쉽게 만들어주는 선언형 HTTP 클라이언트이다.

예를 들어

@FeignClient(name = "USER-SERVICE")
public interface UserClient {

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

처럼 인터페이스만 작성하면

User user = userClient.getUser(id);

처럼 일반 메서드를 호출하는 것과 동일한 방식으로 다른 서버의 API를 사용할 수 있다.


RestController와 FeignClient의 차이

RestControllerFeignClient

요청을 받는 API 다른 서비스의 API를 호출
클라이언트 또는 다른 서비스의 요청 처리 다른 서버로 HTTP 요청 전송
서버의 진입점 HTTP 클라이언트

즉,

한 서비스의 RestController를 다른 서비스의 FeignClient가 호출하는 구조라고 이해하면 된다.


Circuit Breaker란?

MSA에서는 하나의 서비스가 다른 서비스를 호출하는 경우가 많다.

예를 들어 Order Service가 Product Service를 호출한다고 가정해보자.

Order Service
      │
      ▼
Product Service

하지만 Product Service에 장애가 발생하면 Order Service는 계속 응답을 기다리게 된다.

이러한 요청이 계속 쌓이면 결국 Order Service까지 영향을 받아 전체 시스템의 성능이 저하될 수 있다.

이를 방지하기 위해 사용하는 것이 Circuit Breaker이다.

 

Circuit Breaker는 서비스 간 호출 실패를 감지하여 장애가 다른 서비스로 전파되지 않도록 막아주는 패턴이다. 장애가 지속될 경우 더 이상 해당 서비스를 호출하지 않고 빠르게 실패(Fail Fast)하거나 미리 정의한 대체 로직(Fallback)을 실행하여 시스템의 안정성을 유지한다.

Circuit Breaker의 동작 과정

Circuit Breaker는 세 가지 상태를 가진다.

  • Closed : 정상 상태로 모든 요청을 서비스에 전달한다.
  • Open : 실패율이 일정 수준을 넘으면 더 이상 해당 서비스를 호출하지 않고 즉시 실패 또는 Fallback을 수행한다.
  • Half-Open : 일정 시간이 지난 후 일부 요청만 허용하여 서비스가 정상적으로 복구되었는지 확인한다. 성공하면 Closed, 실패하면 다시 Open 상태가 된다.

예를 들어 Product Service 장애가 발생했을 때

@CircuitBreaker(name = "productService", fallbackMethod = "fallbackMethod")

와 같이 설정하면 Product Service 호출에 실패하더라도

fallbackMethod()

가 실행되어 사용자에게 기본 응답을 제공할 수 있다.

즉, Circuit Breaker의 목적은 서비스 장애가 전체 시스템 장애로 이어지는 것을 막는 것이다.


Circuit Breaker 이벤트와 Event Listener

Circuit Breaker는 내부 상태가 변경되거나 장애가 발생할 때 다양한 이벤트를 발생시킨다. 이러한 이벤트를 활용하면 Circuit Breaker의 동작을 로그로 확인하거나 모니터링 시스템과 연동할 수 있다.

@PostConstruct
public void registerEventListener() {
    circuitBreakerRegistry.circuitBreaker("productService")
            .getEventPublisher()
            .onStateTransition(event ->
                    log.info("State Transition: {}", event))
            .onFailureRateExceeded(event ->
                    log.info("Failure Rate Exceeded: {}", event))
            .onCallNotPermitted(event ->
                    log.info("Call Not Permitted: {}", event))
            .onError(event ->
                    log.info("Error: {}", event));
}
 

와 같이 Event Listener를 등록하면 Circuit Breaker에서 발생하는 이벤트를 실시간으로 확인할 수 있다.

각 이벤트의 의미는 다음과 같다.

  • onStateTransition : Circuit Breaker의 상태가 Closed → Open, Open → Half-Open 등으로 변경될 때 발생한다.
  • onFailureRateExceeded : 설정한 실패율을 초과했을 때 발생한다.
  • onCallNotPermitted : Circuit Breaker가 Open 상태라 호출이 차단되었을 때 발생한다.
  • onError : 서비스 호출 중 예외가 발생했을 때 발생한다.

Event Listener는 Circuit Breaker를 생성하는 기능이 아니라, Circuit Breaker에서 발생하는 이벤트를 감지하여 로그를 남기거나 모니터링하기 위한 기능. 실제 운영 환경에서는 이러한 이벤트를 Prometheus, Grafana 등의 모니터링 도구와 연동하여 서비스 장애를 빠르게 감지하는 데 활용.


API Gateway란?

MSA에서는 여러 개의 서비스가 존재하기 때문에 사용자가 각각의 서비스를 직접 호출하면 관리가 매우 복잡해진다.

예를 들어

User
 ├── Order Service
 ├── Product Service
 └── User Service

처럼 각각의 서비스에 직접 접근한다면

  • 인증을 모든 서비스에서 구현해야 하고
  • 로깅도 각각 구현해야 하며
  • 서비스 주소도 모두 알아야 한다.

이를 해결하기 위해 API Gateway를 사용한다.

API Gateway는 클라이언트의 모든 요청을 가장 먼저 받는 단일 진입점(Single Entry Point) 이다. 요청을 적절한 서비스로 라우팅하며 인증, 로깅, 요청 필터링, 로드 밸런싱 등의 공통 기능을 수행한다.

API Gateway의 요청 흐름

이번 강의를 들으면서 가장 헷갈렸던 부분은 Eureka와 LoadBalancer의 역할이었다.

 

실제 동작은 다음과 같다.

사용자
    │
    ▼
API Gateway
    │
    ├── Pre Filter
    │     (JWT 인증, 권한 검사, 로깅 등)
    │
    ▼
Spring Cloud LoadBalancer
    │
    ▼
Eureka Server
(서비스 목록 조회)
    │
    ▼
Spring Cloud LoadBalancer
(인스턴스 선택)
    │
    ▼
Product Service
(Eureka Client)
    │
    ▼
API Gateway
    │
    └── Post Filter
          (응답 로깅, 헤더 추가 등)
    │
    ▼
사용자

동작 순서는 다음과 같다.

  1. 사용자가 API Gateway로 요청을 보낸다.
  2. Gateway의 Filter에서 JWT 인증, 로그인 여부 등을 확인한다.
  3. Gateway는 Eureka Server에 필요한 서비스의 위치를 조회한다.
  4. Eureka는 실행 중인 서비스 인스턴스 목록을 반환한다.
  5. Gateway 내부의 Spring Cloud LoadBalancer가 여러 인스턴스 중 하나를 선택한다.
  6. 선택된 서비스(Eureka Client)로 요청을 전달한다.

즉,

  • Eureka → 서비스 위치를 관리한다.
  • Spring Cloud LoadBalancer → 어느 인스턴스로 요청을 보낼지 결정한다.
  • API Gateway → 모든 요청을 받아 공통 기능을 수행하고 적절한 서비스로 전달한다.

실제 Gateway에서는

uri: lb://product-service

처럼 lb://를 사용하여 Eureka와 연동된 서비스를 조회하고, Spring Cloud LoadBalancer를 통해 적절한 인스턴스로 요청을 전달한다.


이번 시간에는 Spring Cloud에서 각 구성 요소가 어떤 역할을 하는지 전체적인 흐름을 이해할 수 있었다.

  • MSA에서는 서비스를 여러 개로 분리하여 독립적으로 개발하고 배포한다.
  • Eureka는 서비스의 위치(IP, Port)를 등록하고 관리하는 Service Discovery 서버이다.
  • Spring Cloud LoadBalancer는 Eureka에서 받은 서비스 목록 중 실제 요청을 보낼 인스턴스를 선택한다.
  • FeignClient는 다른 마이크로서비스의 API를 일반 메서드를 호출하는 것처럼 사용할 수 있게 해주는 HTTP 클라이언트이다.
  • Circuit Breaker는 서비스 장애가 다른 서비스로 전파되는 것을 방지하고 Fallback을 통해 시스템의 안정성을 유지한다.
  • API Gateway는 모든 요청의 진입점으로 인증, 로깅, 필터링, 라우팅 등의 공통 기능을 수행한 뒤 적절한 서비스로 요청을 전달한다.

참고: 기존 자료에서는 FeignClient가 Ribbon과 함께 동작한다고 설명되어 있지만, 최신 Spring Boot/Spring Cloud 환경에서는 Ribbon 대신 Spring Cloud LoadBalancer를 사용하는 것이 일반적이다.