Post

스프링 클라우드 기반 MSA 구성 - Load Balancer

스프링 클라우드 기반 MSA 구성 - Load Balancer

1. 개요


서비스가 스케일 아웃되면 동일한 서비스의 인스턴스가 여러 개 뜬다. place-service가 3개 떠 있다면, recommendation-service는 어떤 인스턴스로 요청을 보내야 할까?

이 역할을 Load Balancer(로드 밸런서)가 담당한다. 여러 인스턴스에 요청을 고르게 분산시켜 특정 인스턴스에 부하가 집중되지 않도록 한다.

📌 Server-Side LB vs Client-Side LB

구분Server-Side LBClient-Side LB
방식중앙 LB 장비/서버가 분배 결정클라이언트(서비스) 자신이 분배 결정
예시AWS ALB, Nginx, HAProxySpring Cloud LoadBalancer, Ribbon
장점클라이언트가 신경 안 써도 됨별도 LB 서버 불필요, 레이턴시 낮음
단점LB 서버가 단일 장애점, 네트워크 홉 추가클라이언트가 서비스 목록을 알아야 함

Spring Cloud는 Client-Side LB를 사용한다. Eureka에서 조회한 인스턴스 목록을 클라이언트가 직접 들고 있다가, 요청 시 내부에서 분배 결정을 내린다.

📌 Ribbon → Spring Cloud LoadBalancer

과거 Spring Cloud는 Netflix Ribbon을 기본 로드 밸런서로 사용했다. 하지만 Netflix가 Ribbon을 공식 deprecated 처리했고, Spring Cloud도 2020년 이후 Ribbon을 제거하고 Spring Cloud LoadBalancer를 공식 대체재로 채택했다.

출처: DZone - Spring Cloud LoadBalancer vs Netflix Ribbon

구분Netflix RibbonSpring Cloud LoadBalancer
유지보수 주체Netflix (deprecated)Spring 팀 (현재 활발히 개발 중)
Spring Boot 3 호환
Reactive 지원제한적✅ (WebFlux 네이티브)
기본 알고리즘다양 (RoundRobin, WeightedResponseTime 등)RoundRobin, Random
Caffeine 캐시 통합

2. 핵심 개념


📌 동작 원리

1
2
3
4
5
6
7
8
9
10
recommendation-service가 place-service 호출 시

1. FeignClient (name="place-service")
     ↓ 인스턴스 목록 요청
2. Spring Cloud LoadBalancer
     ↓ 로컬 캐시에서 place-service 인스턴스 목록 조회
     → [172.17.0.5:8082, 172.17.0.6:8082, 172.17.0.7:8082]
     ↓ RoundRobin으로 인스턴스 선택
     → 172.17.0.6:8082
3. 실제 HTTP 요청: http://172.17.0.6:8082/places/123

📌 ServiceInstanceListSupplier

Spring Cloud LoadBalancer의 핵심 인터페이스다. 어떤 인스턴스 목록을 LB에 제공할지 결정한다.

공식 문서에서 설명하는 주요 구현체:

구현체설명
DiscoveryClientServiceInstanceListSupplierEureka 등 DiscoveryClient에서 인스턴스 목록 조회
CachingServiceInstanceListSupplier인스턴스 목록을 로컬에 캐싱 (Caffeine 사용)
HealthCheckServiceInstanceListSupplier헬스체크를 통과한 인스턴스만 사용
SameInstancePreferenceServiceInstanceListSupplier이전에 성공한 인스턴스를 우선 선택

📌 기본 로드 밸런싱 알고리즘

Spring Cloud LoadBalancer가 제공하는 기본 알고리즘은 두 가지다.

  • RoundRobinLoadBalancer (기본): 인스턴스를 순서대로 돌아가며 선택
  • RandomLoadBalancer: 인스턴스를 무작위로 선택

3. 설정


📌 build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * spring-cloud-starter-loadbalancer는 Spring Cloud LoadBalancer와
 * Caffeine 캐시를 포함한 통합 스타터다.
 *
 * FeignClient와 함께 사용하려면 openfeign도 같이 추가한다.
 * Caffeine은 인스턴스 목록 캐시 성능을 향상시킨다.
 */
dependencies {
    // ...기존 코드

    implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

    // LoadBalancer 캐시 성능 향상 (없어도 동작하지만, 있으면 Caffeine 캐시 자동 사용)
    implementation 'com.github.ben-manes.caffeine:caffeine'
}

📌 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
  cloud:
    loadbalancer:
      # Ribbon이 classpath에 있다면 비활성화 (Spring Boot 2.x 마이그레이션 시)
      ribbon:
        enabled: false

      # 인스턴스 목록 로컬 캐싱 설정
      cache:
        enabled: true
        ttl: 30s         # 캐시 유효 시간
        capacity: 256    # 최대 캐시 항목 수

      # 헬스체크 통과한 인스턴스만 LB 목록에 포함
      health-check:
        initial-delay: 0
        interval: 25s

📌 FeignClient와 연동

FeignClient는 name에 명시한 서비스명을 Spring Cloud LoadBalancer에 위임해서 실제 인스턴스 주소를 해석한다. IP를 직접 쓰지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @FeignClient의 name은 Eureka에 등록된 서비스명과 일치해야 한다.
 * Spring Cloud LoadBalancer가 자동으로 Eureka 레지스트리를 조회해
 * 실제 인스턴스 IP:Port로 변환한다.
 *
 * FeignClient 활성화를 위해 메인 클래스에 @EnableFeignClients가 필요하다.
 */
@FeignClient(name = "place-service")
public interface PlaceServiceClient {

    @GetMapping("/places/{placeId}")
    PlaceResponse getPlace(@PathVariable Long placeId);

    @GetMapping("/places/batch")
    List<PlaceResponse> getPlaces(@RequestParam List<Long> ids);
}
1
2
3
4
5
6
// 메인 클래스에 추가
@SpringBootApplication
@EnableFeignClients  // FeignClient 스캔 활성화
public class RecommendationServiceApplication {
    ...
}

📌 @LoadBalanced RestTemplate

FeignClient 대신 RestTemplate를 사용한다면 @LoadBalanced 어노테이션을 붙이면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * @LoadBalanced 어노테이션을 붙인 RestTemplate은
 * http://서비스명/경로 형식의 URL을 실제 IP:Port로 자동 변환한다.
 * Ribbon이 deprecated됐으므로 Spring Cloud LoadBalancer가 대신 처리한다.
 */
@Configuration
public class RestTemplateConfig {

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

// 사용 예시
@Service
@RequiredArgsConstructor
public class SomeService {

    private final RestTemplate restTemplate;

    public PlaceResponse getPlace(Long placeId) {
        // "place-service"는 IP가 아닌 Eureka 서비스명
        return restTemplate.getForObject(
            "http://place-service/places/" + placeId,
            PlaceResponse.class
        );
    }
}

4. 커스텀 로드 밸런싱 알고리즘


기본 RoundRobin 외에 커스텀 알고리즘이 필요한 경우 ReactorServiceInstanceLoadBalancer를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * place-service에 대해 Random 로드 밸런싱을 적용하는 예시.
 * 서비스별로 다른 알고리즘을 적용하고 싶을 때 사용한다.
 *
 * @LoadBalancerClient는 특정 서비스에만 설정을 적용한다.
 * @LoadBalancerClients는 여러 서비스에 동시에 설정을 적용한다.
 */
@Configuration
@LoadBalancerClient(name = "place-service", configuration = PlaceLBConfig.class)
public class PlaceLBConfig {

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

5. 트러블슈팅


📌 “No instances available for place-service”

LoadBalancer가 Eureka에서 인스턴스 목록을 가져왔지만 사용 가능한 인스턴스가 없을 때 발생한다.

원인 및 해결:

  1. Eureka 대시보드(http://localhost:8761)에서 해당 서비스가 UP 상태인지 확인
  2. Eureka Client 캐시가 갱신되지 않았을 수 있음 → registry-fetch-interval-seconds를 줄임
  3. 헬스체크가 실패해서 인스턴스가 제외됐을 수 있음 → /actuator/health 직접 확인

📌 Ribbon과 Spring Cloud LoadBalancer 충돌

Spring Boot 2.x 프로젝트를 3.x로 마이그레이션할 때 발생할 수 있다.

1
2
3
4
5
spring:
  cloud:
    loadbalancer:
      ribbon:
        enabled: false   # Ribbon 완전 비활성화

6. 정리


  • Spring Cloud LoadBalancer는 Netflix Ribbon의 공식 대체재로, Spring Boot 3.x 환경에서 사용해야 한다.
  • lb://서비스명 또는 @FeignClient(name="서비스명") 형식으로 Eureka + LB 연동이 자동으로 이뤄진다.
  • 기본 알고리즘은 RoundRobin이며, 서비스별로 커스텀 알고리즘을 적용할 수 있다.
  • Caffeine 캐시를 함께 쓰면 인스턴스 목록 조회 성능이 향상된다.
  • @LoadBalanced RestTemplate 또는 FeignClient 중 하나를 선택해서 사용하면 된다.

참고 자료

This post is licensed under CC BY 4.0 by the author.