[MSA] 클라이언트 사이드 로드밸런싱

박원준·2026년 3월 24일

MSA

목록 보기
4/9

1️⃣ 클라이언트 사이드 로드 밸런싱 개요

🔹 로드 밸런싱이란?

로드 밸런싱은 네트워크 트래픽을 여러 서버로 분산시켜 서버의 부하를 줄이고, 시스템의 성능과 가용성을 높이는 기술입니다. 서버 간 트래픽을 고르게 분배하여 특정 서버에 부하가 집중되는 것을 방지합니다. 종류로는 클라이언트 사이드 로드 밸런싱서버 사이드 로드 밸런싱 이 있습니다.


🔹 클라이언트 사이드 로드 밸런싱이란?

클라이언트 사이드 로드 밸런싱이란 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식입니다. 여기서 클라이언트를 프론트엔드(웹 브라우저)로만 착각하는 경우도 많습니다. 하지만 여기서 말하는 클라이언트란 요청을 보내는 주체(클라이언트)를 말합니다. 예를 들어, 한 마이크로서비스가 다른 마이크로서비스로 요청을 보낼 때도 클라이언트 역할을 할 수 있습니다.

로드밸런싱, 클라이언트 사이드 로드밸런싱, 서버 사이드 로드밸런싱 들을 자세히 알고싶다면 다음 포스팅을 참고해주세요.
로드밸런싱 자세히 알아보기


2️⃣ FeignClient

FeignClient는 Spring Cloud에서 제공하는 HTTP 클라이언트로, 선언적으로 RESTful 웹 서비스를 호출할 수 있습니다. Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행합니다.

이때 HTTP 클라이언트란, HTTP 프로토콜을 사용해서 웹 서버와 통신하는 모든 애플리케이션을 말합니다. 쉽게 말하면 브라우저도 HTTP 클라이언트이고 Postman도 HTTP 클라이언트 입니다.

주요 HTTP 클라이언트에는 RestTemplate, WebClient, FeignClient 등이 있습니다.
자세한 내용은 다음 링크에서 확인할 수 있습니다. HTTP 클라이언트

🔹 특징

  • 선언적 HTTP 클라이언트: 인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있습니다.
  • Eureka 연동: Eureka와 통합하여 서비스 인스턴스 목록을 동적으로 조회하고 로드 밸런싱을 수행합니다.
  • 자동 로드 밸런싱: Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행합니다.

3️⃣ Ribbon

  • 넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, 마이크로서비스 아키텍처에서 서비스 인스턴스 간의 부하를 분산합니다.
  • 다양한 로드 밸런싱 알고리즘을 지원하며, Eureka와 같은 서비스 디스커버리와 연동하여 사용합니다.

🔹 특징

  • 서버 리스트 제공자: Eureka 등으로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용
  • 로드 밸런싱 알고리즘: 라운드 로빈, 가중치 기반 등 다양한 로드 밸런싱 알고리즘 지원
  • Failover: 요청 실패 시 다른 인스턴스로 자동 전환

4️⃣ FeignClient와 Ribbon 설정

스프링부트 어플리케이션에서 Feign Client와 리본을 사용하려면, 먼저 build.gradle 파일에 필요한 의존성을 추가해야 합니다.

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

또한, 어플리케이션의 메인 자바 파일에 @EnableFeignClients를 포함시켜야 합니다.

@SpringBootApplication
@EnableFeignClients
public class MyServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyServiceApplication.class, args);
    }
}

그 후에 FeignClient 인터페이스를 작성해야 합니다. 호출 대상 서비스를 나타내는 Feign Client 인스턴스를 인터페이스 형태로 작성하고, 해당 서비스의 host 및 호출할 엔드포인트를 명시해야 합니다. 엔드포인트 내부에는 전달할 값과 응답 데이터 유형 등 필요한 정보를 정의합니다.

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "my-service")
public interface MyServiceClient {

    @GetMapping("/endpoint")
    String getResponse(@RequestParam(name = "param") String param);
}

5️⃣ 로드 밸런싱 알고리즘

  • 라운드 로빈: 각 서버에 순차적으로 요청을 분배하는 방식입니다. 간단하고 공평하게 트래픽을 분산합니다.

  • 가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고, 가중치에 비례하여 요청을 분배하는 방식입니다. 서버의 성능이나 네트워크 상태에 따라 가중치를 조절합니다.

  • 최소 연결: 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식입니다.

  • 응답 시간 기반: 서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식입니다.

알고리즘에 대한 자세한 정보는 로드밸런싱 자세히 알아보기 에서 확인해주세요.

실습에서는 라운드 로빈을 사용하며, Feign Client는 유레카와 연동해 동적으로 서비스 인스턴스를 조회하고 Load Balancing을 수행해보겠습니다.


6️⃣ FeignClient 연동

🔹 Eureka 설정

EurekaFeignClient를 함께 사용하면 동적으로 서비스 인스턴스를 조회하여 로드 밸런싱을 수행합니다. 아래는 application.yml 파일 설정 예시입니다.

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

🔹 Ribbon 설정

리본 활성화를 위해 어플리케이션 야물이나 프로퍼티 파일에서 eureka enabled: true로 설정해야 합니다. 아래는 application.yml 파일 설정 예시입니다.

my-service:
  ribbon:
    eureka:
      enabled: true

🔹 FeignClient와 Ribbon 동작 원리

  1. 서비스 이름은 Annotation에 등록하여 (@FeignClient(name = "my-service")) 유레카에 등록된 서비스로 참조합니다. name은 각 서비스의 application.yml이나 properties파일에 있는 name입니다.

  2. 그리고 서비스 인스턴스를 조회합니다. Eureka 서버에서 my-service라는 이름으로 등록된 서비스 인스턴스 목록을 조회합니다. 그러면 같은 서비스가 여러 개의 Application으로 떠 있다면 그 중에 하나를 선택해야합니다. 어떤 것을 선택해야 할까요?

  3. 바로 로드 밸런싱을 기준으로 선택하게 됩니다. 로드밸런싱을 통해 조회된 서비스 인스턴스 목록 중 하나를 선택하여 요청을 보냅니다.

  4. 그 다음 요청 분배를 합니다. 여러 서비스 인스턴스가 있을 경우, Round Robin 또는 다른 설정된 로드 밸런싱 알고리즘을 사용하여 요청을 분배합니다.

이렇게 말로 설명하면 잘 이해가 되지 않습니다. 시나리오로 생각해보겠습니다.

💡
Order 서비스는 Product 서비스를 호출하여 상품 정보를 가져옵니다. 이 과정에서 어떻게 동작하는지 설명하겠습니다.

  • Order 서비스 인스턴스: 1개
  • Product 서비스 인스턴스: 3개
  1. Order 서비스 실행: Order 서비스가 실행되면 Eureka 서버에서 Product 서비스 인스턴스 목록을 가져옵니다.
  2. Product 서비스 호출: Order 서비스에서 Product 서비스의 정보를 가져오기 위해 FeignClient를 사용하여 호출합니다.
  3. Ribbon을 통한 로드 밸런싱: FeignClient는 Ribbon을 통해 3개의 Product 인스턴스 중 하나를 선택하여 호출합니다. 이 과정에서 Round Robin 알고리즘을 사용하여 요청을 순차적으로 분배합니다.
  4. 응답 처리: 선택된 Product 인스턴스에서 응답을 받아 Order 서비스에 반환하고, 최종적으로 클라이언트에 응답을 전달합니다.

🔹 FeignClient 인터페이스 작성 예시

FeignClient 인터페이스를 작성하여 서비스를 호출하게 됩니다.
만약 A서비스에서 B서비스로 호출할 때는 어떤 정보를 A서비스에 기록해야 할까요?
바로 B를 기록해야합니다. 아래의 예시코드는 B서비스, 우리가 호출할 대상의 서비스를 지정한겁니다.

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "my-service")
public interface MyServiceClient {

	// http://my-service/endpoint?param=...
    @GetMapping("/endpoint")
    String getResponse(@RequestParam(name = "param") String param);
}

🔹 서비스 사용 예제

방금 전에 사용한 FeignClient interface를 사용해보겠습니다. 즉, FeignClient를 사용하여 다른 서비스 호출을 수행하겠습니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @Autowired
    private MyServiceClient myServiceClient;

    @GetMapping("/call-service")
    public String callService(@RequestParam String param) {
        return myServiceClient.getResponse(param);
    }
}

7️⃣ 실습

💡
유레카 서버 하나에 주문 인스턴스 1개와 같은 기능의 포트만 다른 상품 인스턴스 3개를 연결합니다.
상품을 요청(http://localhost:19091/order/1) 하면 응답하는 인스턴스의 포트를 받아서 노출합니다.
이를 통해 라운드로빈으로 로드밸런싱이 되는것을 확인합니다.

🔹 Eureka Server

[MSA]서비스 디스커버리(Eureka)에서 진행했던 유레카 서버를 그대로 복사해서 실행합니다.


🔹 Product instance

💡
Product는 요청이 오면 아래의 문자열을 반환합니다.
”Product {productId} info!!!! From port : ${serverPort}”

  1. start.spring.io 에 접속하여 프로젝트를 생성합니다 (디펜던시는 이미지 참고)

  2. ProductApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class ProductApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProductApplication.class, args);
	}

}
  1. ProductController.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
    private String serverPort;

    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable String id) {
        return "Product " + id + " info!!!!! From port : " + serverPort ;
    }
}
  1. resources/application.yml
spring:
  application:
    name: product-service
server:
  port: 19092
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
  1. 19092,19093,19094 포트에 같은 애플리케이션 실행하기
    1) 인텔리제이의 상단 메뉴에서 실행 > 구성 편집으로 들어갑니다.

    2) ProductApplication 의 이름을 ProdcutApplication:19092로 변경합니다.

    3) 우측 상단의 복사버튼을 클릭하여 ProductApplication을 두개 더 생성하고 위와 같이 19093,19094로 이름을 변경합니다.
    4) 옵션수정을 클릭하여 VM옵션 추가를 클릭합니다.

    5) 이미지와 같이 -Dserver.port=19093 을 입력합니다. 19094에도 같은 작업을 해줍니다.

🔹 Order instance

💡
Order는 요청이 오면 product를 호출하여 상품의 정보를 가져옵니다. 그리고 아래의 문자열을 반환합니다.
”Your order {orderId} product info => ${프로덕트에서 받은 문자열}”

주문은 주문아이디가 1인 주문만 있다고 가정하겠습니다.
1 주문은 112번 상품을 호출한다고 가정합니다.

  1. start.spring.io 에 접속하여 프로젝트를 생성합니다 (디펜던시는 이미지 참고)

  2. OrderApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderApplication {

	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}

}
  1. OrderController.java
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;


@RestController
@RequiredArgsConstructor
public class OrderController {
    
    private final OrderService orderService;

    @GetMapping("/order/{orderId}")
    public String getOrder(@PathVariable String orderId) {
        return orderService.getOrder(orderId);
    }
}
  1. ProdcutClient.java (인터페이스로 생성)
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/product/{id}")
    String getProduct(@PathVariable("id") String id);
}
  1. OrderService.java
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final ProductClient productClient;

    public String getProductInfo(String productId) {
        return productClient.getProduct(productId);
    }

    public String getOrder(String orderId) {
        if(orderId.equals("1") ){
            String productId = "2";
            String productInfo = getProductInfo(productId);
            return "Your order is " + orderId + " and " + productInfo;

        }
        return "Not exist order...";
        }
    }
}
  1. application.yml
spring:
  application:
    name: order-service
server:
  port: 19091
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

🔹 Run

  1. 유레카 서버 → order → product(3개 모두) 순으로 실행합니다.

  2. http://localhost:19090/ 으로 접속하면 인스턴스를 확인할 수 있습니다.
    PRODUCT-SERVICE에 포트가 19092,19093,19094 3개가 떠있는것을 확인합니다.

  3. http://localhost:19091/order/1 에 접속할때마다 텍스트의 포트가 변경되는것을 볼 수 있습니다. 이를통해 요청마다 라운드로빈으로 동작함을 확인할 수 있습니다.

0개의 댓글