로드 밸런싱

이진일·2026년 4월 14일
post-thumbnail

로드 밸런싱이란?

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

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

  • 클라이언트 사이드 로드 밸런싱은 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식
  • 클라이언트는 서버의 목록을 가지고 있으며 이를 바탕으로 로드 밸런싱을 수행

Feignclient란?

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

FeignClient의 주요 특징

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

Ribbon이란?

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

Ribbon의 주요 특징

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

로드 밸런싱 알고리즘

라운드 로빈

  • 각 서버에 순차적으로 요청을 분배하는 방식
  • 간단하고 공평하게 트래픽을 분산

가중치 기반 로드 밸런싱

  • 각 서버에 가중치를 부여하고 가중치에 비례하여 요청을 분배하는 방식
  • 서버의 성능이나 네트워크 상태에 따라 가중치를 조절

최소 연결

  • 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식

응답 시간 기반

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

로드 밸런싱 실습해보기

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

Eureka 서버

먼저 Eureka 서버를 준비해둔다.

  • Eureka 서버 설정 예시
spring.application.name=server

server.port=19090

# 유레카 서버에 자신을 등록할지 여부를 설정합니다.
# true로 설정하면 유레카 클라이언트가 유레카 서버에 자신을 등록합니다.
# 유레카 서버에서는 일반적으로 false로 설정하여, 서버가 자기 자신을 등록하지 않도록 합니다.
eureka.client.register-with-eureka=false

# 유레카 서버로부터 레지스트리를 가져올지 여부를 설정합니다.
# true로 설정하면 유레카 클라이언트가 유레카 서버로부터 다른 서비스 인스턴스 목록을 가져옵니다.
# 유레카 서버에서는 일반적으로 false로 설정하여, 레지스트리를 가져오지 않도록 합니다.
eureka.client.fetch-registry=false

# 유레카 서버 인스턴스의 호스트 이름을 설정합니다.
# 유레카 서버가 자신의 호스트 이름을 다른 서비스에 알릴 때 사용합니다.
eureka.instance.hostname=localhost

# 유레카 클라이언트가 유레카 서버와 통신하기 위해 사용할 기본 서비스 URL을 설정합니다.
# 클라이언트 애플리케이션이 유레카 서버에 연결하고 등록하거나 레지스트리를 가져올 때 사용할 URL을 지정합니다.
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/

Product Instance

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);
	}

}
  • @EnableFeignClients 에너테이션을 추가하여 @FeignClient가 선언된 인터페이스가 자동으로 Spring Bean으로 등록되도록 한다.

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 ;
    }
    
}
  • /product/{id} 경로로 GET메서드 요청이 들어오면 아래의 문자열을 반환한다.
    "Product {id} info!!!! From port: {serverPort}"

application.yml

spring:
  application:
    name: product-service
server:
  port: 19092
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

IntelliJ에서 실행 > 구성 편집에서 ProductApplication을 복사하여 19093, 19094 포트로 두개 더 생성한다.

Order Instance

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);
	}

}
  • OrderApplication에서도 동일하게 @EnableFeignClients 애너테이션을 추가한다.

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);
    }
    
}
  • Order Application으로 /order/{orderId} 경로로 GET 메서드 요청이 들어오면 orderService의 getOrder() 메서드를 호출하여 문자열을 반환한다.

ProductClient.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);
}
  • getProduct() 메서드를 호출하면 Eureka 서버의 서비스 디스커버리에서 name이 product-service인 서비스를 찾아 /product/{id} 엔드포인트로 GET 요청을 보낸다.

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...";
        }
    }
  • OrderController에서 /order/{orderId}로 GET 메서드 요청이 들어오면 orderService의 getOrder() 메서드를 호출한다.

  • getOrder() 메서드에서는 orderId가 1이 아니라면 "Not exist order..." 문자열을 반환한다.

  • orderId가 1이면 productId를 2로 설정하고 getProductInfo(productId)를 호출한다. 반환받은 문자열을 productInfo에 담고 "Your order is {orderId} and {productInfo}"를 반환한다.

  • getProductInfo() 메서드는 주입받은 productClient를 사용하여 getProduct(productId)를 호출한다. 그럼 서비스 디스커버리에서 product-service를 찾아 /product/{productId}로 GET 요청을 보낸다.

application.yml

spring:
  application:
    name: order-service
server:
  port: 19091
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

실행 결과

Eureka 서버 -> order -> product(3개) 순서로 모두 실행한다.
http://localhost:19090으로 접속하면 order-service와 product-service 3개가 떠있는 것을 확인할 수 있다.

이제 http://localhost:19091/order/1로 접속하면 아래의 문자열이 나온다.
Your order is 1 and Product 2 info!!!!! From port : 19092
다시 http://localhost:19091/order/1로 접속하면 Your order is 1 and Product 2 info!!!!! From port : 19093으로 나온다. 접속할 때마다 port가 19092 -> 19093 -> 19094 번 순서로 돌아가면서 나오는 것을 확인할 수 있다.
이렇게 라운드 로빈 방식으로 로드 밸런싱이 되는 것을 볼 수 있다.

로드 밸런싱이 자동으로 수행되는 이유

위의 실습에서 우리는 별도의 로드 밸런싱 설정을 하지 않았음에도 ProductClient가 여러 인스턴스에 요청을 보낼 수 있었던 이유는 Spring Cloud의 자동 설정 덕분이다.

자동 설정 메카니즘

  • spring-cloud-starter-netflix-eureka-client가 클래스패스에 존재하는지 체크한다.
  • Spring Boot가 LoadBalancerClient를 자동으로 빈으로 등록한다.
  • Feign을 통해 요청이 날갈 때 이 로드 밸런서가 개입하여 여러 인스턴스 중 하나를 선택하여 실제 주소로 변환해준다.

다른 로드 밸런싱 방식을 사용하려면?

다른 로드 밸런싱 방식을 사용하려면 어떻게 해야 하는지 찾아보다가 Spring Cloud 2020.0 이후 버전부터는 Ribbon 대신 Spring Cloud LoadBalancer를 표준으로 사용한다고 한다.

그래서 Spring Cloud LoadBalancer에서 다른 방식을 사용하려면 로드 밸런싱 알고리즘 설정 클래스를 만들어서 FeignClient에 설정을 연결해주어야 한다고 한다.

0개의 댓글