Spring Cloud LoadBalancer 상세 가이드 및 소스 코드 분석

Spring Cloud LoadBalancer는 Spring Cloud 공식에서 제공하는 클라이언트 측 로드 밸런서로, spring-cloud-commons 라이브러리에 포함되어 있습니다. Netflix Ribbon이 더 이상 업데이트되지 않음에 따라 그 대체재로 개발되었습니다. 이 글에서는 Spring Cloud LoadBalancer의 개요, 사용법, 다양한 통합 방식, 그리고 핵심 소스 코드를 분석합니다.

개요

로드 밸런서는 크게 서버 측과 클라이언트 측으로 나뉩니다. 서버 측 로드 밸런서는 F5, LVS, Nginx와 같이 게이트웨이 계층에서 동작합니다. 반면, 클라이언트 측 로드 밸런서인 Spring Cloud LoadBalancer는 애플리케이션 내부에서 서비스 목록을 관리하고, 라운드 로빈, 랜덤, 카나리 배포 등의 전략으로 요청을 분산합니다.

Spring Cloud는 반응형 프로그래밍을 지원하는 ReactiveLoadBalancer 인터페이스를 도입했으며, 기본 구현체로 RoundRobinLoadBalancerRandomLoadBalancer를 제공합니다. 실제 인스턴스 선택은 ServiceInstanceListSupplier를 통해 이루어지며, 서비스 디스커버리 클라이언트를 통해 레지스트리에서 가져온 정보를 사용합니다.

다음 설정으로 Spring Cloud LoadBalancer를 비활성화할 수 있습니다.

spring:
  cloud:
    loadbalancer:
      enabled: false

시작하기

spring-cloud-starter-loadbalancer 의존성을 추가하면 Spring Boot Caching 및 Evictor 기능도 함께 사용할 수 있습니다.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

Spring은 다양한 HTTP 클라이언트를 지원하며, 그중 RestTemplate은 가장 널리 사용됩니다. @LoadBalanced 애노테이션을 추가하면 RestTemplate이 로드 밸런싱 기능을 사용할 수 있습니다. 기본 로드 밸런서는 RoundRobinLoadBalancer입니다.

@Configuration
public class RestTemplateConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

컨트롤러에서 RestTemplate을 주입받아 서비스 이름(예: ecom-storage-service)으로 요청을 보내면, LoadBalancer가 자동으로 인스턴스를 선택합니다.

@RestController
public class OrderController {
    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/deductRest/{commodityCode}/{count}")
    public String deductRest(@PathVariable("commodityCode") String commodityCode, 
                             @PathVariable("count") int count) {
        String url = "http://ecom-storage-service/deduct/" + commodityCode + "/" + count;
        return restTemplate.getForObject(url, String.class);
    }
}

3개의 인스턴스를 실행하고 6번 요청을 보내면, 기본 라운드 로빈 방식으로 트래픽이 분산되는 것을 확인할 수 있습니다.

로드 밸런싱 알고리즘 변경

알고리즘을 변경하려면 ReactorLoadBalancer 인터페이스를 구현한 빈을 정의해야 합니다. 예를 들어, 랜덤 로드 밸런서를 사용하려면 다음과 같이 설정합니다.

public class CustomLoadBalancerConfiguration {
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

그리고 @LoadBalancerClient 애노테이션을 사용해 특정 서비스에 이 설정을 적용합니다.

@Configuration
@LoadBalancerClient(value = "ecom-storage-service", configuration = CustomLoadBalancerConfiguration.class)
public class RestTemplateConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

이제 요청이 랜덤 방식으로 분산됩니다.

통합 방식

Spring Cloud LoadBalancer는 세 가지 방식으로 통합할 수 있습니다.

1. RestTemplate (위 예제)

2. WebClient (Spring WebFlux)

WebClient는 비동기, 논블로킹 HTTP 클라이언트입니다. spring-boot-starter-webflux 의존성을 추가한 후, WebClient.Builder 빈에 @LoadBalanced 애노테이션을 추가합니다.

@Configuration
public class WebClientConfig {
    @LoadBalanced
    @Bean
    WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

컨트롤러에서 이 빌더를 사용합니다.

@Autowired
private WebClient webClient;

@RequestMapping("/deductWebClient/{commodityCode}/{count}")
public Mono<String> deductWebClient(@PathVariable("commodityCode") String commodityCode, 
                                    @PathVariable("count") int count) {
    String url = "http://ecom-storage-service/deduct/" + commodityCode + "/" + count;
    return webClient.get().uri(url).retrieve().bodyToMono(String.class);
}

3. ReactorLoadBalancerExchangeFilterFunction

spring-cloud-loadbalancerspring-webflux가 클래스패스에 있으면 ReactorLoadBalancerExchangeFilterFunction이 자동 설정됩니다. 이를 필터로 사용할 수 있습니다.

@Autowired
private ReactorLoadBalancerExchangeFilterFunction lbFunction;

@RequestMapping("/deductWebFluxReactor/{commodityCode}/{count}")
public Mono<String> deductWebFluxReactor(@PathVariable("commodityCode") String commodityCode, 
                                         @PathVariable("count") int count) {
    return WebClient.builder()
            .baseUrl("http://ecom-storage-service")
            .filter(lbFunction)
            .build()
            .get()
            .uri("/deduct/" + commodityCode + "/" + count)
            .retrieve()
            .bodyToMono(String.class);
}

원리 및 소스 코드 분석

RestTemplate의 동작 방식

RestTemplateInterceptingHttpAccessor를 상속하며, ClientHttpRequestInterceptor 인터페이스를 구현한 인터셉터를 등록할 수 있습니다. Spring Cloud LoadBalancer는 LoadBalancerInterceptor를 자동으로 등록하여 요청 전에 로드 밸런싱을 수행합니다.

LoadBalancerAutoConfiguration

spring-cloud-commons는 자동 설정 클래스 LoadBalancerAutoConfiguration을 제공합니다. 조건이 충족되면 LoadBalancerInterceptor 빈이 생성되어 모든 @LoadBalanced RestTemplate 빈에 주입됩니다.

LoadBalancerInterceptor

LoadBalancerInterceptorClientHttpRequestInterceptor를 구현하며, intercept() 메서드에서 LoadBalancerClient를 호출하여 적절한 서비스 인스턴스를 선택합니다.

BlockingLoadBalancerClient

LoadBalancerClient의 기본 구현체입니다. choose() 메서드를 호출하여 LoadBalancerClientFactory에서 로드 밸런서를 가져오고, ReactiveLoadBalancer.choose()를 통해 인스턴스를 선택합니다.

public class BlockingLoadBalancerClient implements LoadBalancerClient {
    private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;

    @Override
    public <T> ServiceInstance choose(String serviceId, Request<T> request) {
        ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
        if (loadBalancer == null) {
            return null;
        }
        Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
        return loadBalancerResponse != null ? loadBalancerResponse.getServer() : null;
    }
}

LoadBalancerClientFactory

이 팩토리는 클라이언트 이름을 기반으로 Spring ApplicationContext를 생성하고, 필요한 빈(예: ReactorServiceInstanceLoadBalancer)을 추출합니다. 기본 로드 밸런서는 LoadBalancerClientConfiguration에서 RoundRobinLoadBalancer로 설정됩니다.

ReactiveLoadBalancer

인터페이스이며, 주요 구현체로 RoundRobinLoadBalancer, RandomLoadBalancer, NacosLoadBalancer 등이 있습니다.

ReactorLoadBalancerClientAutoConfiguration

WebClient를 위한 자동 설정 클래스로, ExchangeFilterFunction을 구현한 ReactorLoadBalancerExchangeFilterFunction 빈을 생성합니다.

사용자 정의 로드 밸런서

ReactorServiceInstanceLoadBalancer 인터페이스를 구현하여 자체 로드 밸런서를 만들 수 있습니다. 예를 들어, RandomLoadBalancer를 참고하여 다음과 같이 작성할 수 있습니다.

public class CustomRandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;

    public CustomRandomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
        this.supplierProvider = supplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
        return supplier.get(request).next()
                .map(this::selectRandomInstance);
    }

    private Response<ServiceInstance> selectRandomInstance(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        int index = ThreadLocalRandom.current().nextInt(instances.size());
        return new DefaultResponse(instances.get(index));
    }
}

그리고 설정 클래스에서 이를 빈으로 등록합니다.

public class CustomLoadBalancerConfiguration {
    @Bean
    public ReactorServiceInstanceLoadBalancer customLoadBalancer(
            ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
        return new CustomRandomLoadBalancer(supplierProvider);
    }
}

이제 이 로드 밸런서가 적용된 서비스를 호출하면 콘솔에서 선택 로그를 확인할 수 있습니다.

태그: Spring Cloud LoadBalancer ReactiveLoadBalancer RoundRobinLoadBalancer RandomLoadBalancer

5월 21일 15:43에 게시됨