서버가 여러 대인데, 어떤 서버에 요청을 보내야 가장 효율적일까요?

개념 정의

로드밸런싱(Load Balancing) 은 들어오는 요청을 여러 서버에 분배하는 기술입니다. 핵심은 "어떤 알고리즘으로 서버를 선택하느냐"입니다.

왜 필요한가

  • **고가용성 **: 한 서버가 죽어도 나머지가 처리
  • ** 확장성 **: 서버를 추가하면 처리량 증가
  • ** 성능 **: 부하를 균등하게 분배하여 응답 시간 최소화

Round Robin

가장 단순합니다. 서버 목록을 순서대로 돌면서 요청을 분배합니다.

JAVA
public class RoundRobinBalancer {
    private final List<String> servers;
    private final AtomicInteger index = new AtomicInteger(0);

    public RoundRobinBalancer(List<String> servers) {
        this.servers = servers;
    }

    public String nextServer() {
        int idx = index.getAndIncrement() % servers.size();
        return servers.get(idx);
    }
}
  • ** 장점 **: 구현이 매우 간단, 균등 분배
  • ** 단점 **: 서버 성능 차이를 고려하지 않음, 요청 처리 시간 차이 무시

Weighted Round Robin

서버에 ** 가중치 **를 부여하여, 성능이 좋은 서버에 더 많은 요청을 보냅니다.

JAVA
public class WeightedRoundRobin {
    private final List<String> servers;
    private final int[] weights;
    private int currentIndex = 0;
    private int currentWeight = 0;
    private final int maxWeight;
    private final int gcdWeight;

    public WeightedRoundRobin(List<String> servers, int[] weights) {
        this.servers = servers;
        this.weights = weights;
        this.maxWeight = Arrays.stream(weights).max().orElse(1);
        this.gcdWeight = gcd(weights);
    }

    public synchronized String nextServer() {
        while (true) {
            currentIndex = (currentIndex + 1) % servers.size();
            if (currentIndex == 0) {
                currentWeight -= gcdWeight;
                if (currentWeight <= 0) {
                    currentWeight = maxWeight;
                }
            }
            if (weights[currentIndex] >= currentWeight) {
                return servers.get(currentIndex);
            }
        }
    }

    private int gcd(int[] arr) {
        int result = arr[0];
        for (int i = 1; i < arr.length; i++) {
            result = gcd(result, arr[i]);
        }
        return result;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}

// 사용: 서버A(weight=5), 서버B(weight=3), 서버C(weight=2)
// → A에 50%, B에 30%, C에 20%의 요청

Least Connections

** 현재 연결 수가 가장 적은 서버 **에 요청을 보냅니다.

JAVA
public class LeastConnectionsBalancer {
    private final Map<String, AtomicInteger> connections = new ConcurrentHashMap<>();

    public LeastConnectionsBalancer(List<String> servers) {
        servers.forEach(s -> connections.put(s, new AtomicInteger(0)));
    }

    public String nextServer() {
        return connections.entrySet().stream()
            .min(Comparator.comparingInt(e -> e.getValue().get()))
            .map(Map.Entry::getKey)
            .orElseThrow();
    }

    public void onRequestStart(String server) {
        connections.get(server).incrementAndGet();
    }

    public void onRequestEnd(String server) {
        connections.get(server).decrementAndGet();
    }
}
  • ** 장점 **: 느린 요청이 많은 서버에 추가 요청을 보내지 않음
  • ** 단점 **: 연결 수 추적 오버헤드, 새 서버 추가 시 몰림 현상

IP Hash

클라이언트 IP를 해싱하여 항상 같은 서버로 라우팅합니다.

JAVA
public class IPHashBalancer {
    private final List<String> servers;

    public IPHashBalancer(List<String> servers) {
        this.servers = servers;
    }

    public String getServer(String clientIP) {
        int hash = clientIP.hashCode();
        int index = Math.abs(hash) % servers.size();
        return servers.get(index);
    }
}
  • ** 장점 **: 세션 친화성(같은 클라이언트 → 같은 서버)
  • ** 단점 **: 서버 추가/제거 시 매핑 변경 (일관된 해싱으로 개선 가능)

Least Response Time

** 응답 시간이 가장 짧은 서버 **에 요청을 보냅니다.

JAVA
public class LeastResponseTimeBalancer {
    private final Map<String, Double> avgResponseTime = new ConcurrentHashMap<>();
    private final double alpha = 0.3; // 지수 이동 평균 가중치

    public String nextServer() {
        return avgResponseTime.entrySet().stream()
            .min(Comparator.comparingDouble(Map.Entry::getValue))
            .map(Map.Entry::getKey)
            .orElseThrow();
    }

    public void recordResponseTime(String server, double responseTime) {
        avgResponseTime.compute(server, (k, avg) ->
            avg == null ? responseTime : alpha * responseTime + (1 - alpha) * avg
        );
    }
}

알고리즘 비교

알고리즘균등 분배성능 고려세션 친화성복잡도
Round RobinOXX낮음
Weighted RRO정적X낮음
Least ConnectionsO동적X중간
IP HashXXO낮음
Least Response TimeO동적X높음
Random확률적XX낮음

백엔드/실무 연결

Nginx

NGINX
# Round Robin (기본값)
upstream backend {
    server backend1.example.com;
    server backend2.example.com;
}

# Weighted
upstream backend {
    server backend1.example.com weight=5;
    server backend2.example.com weight=3;
}

# Least Connections
upstream backend {
    least_conn;
    server backend1.example.com;
    server backend2.example.com;
}

# IP Hash
upstream backend {
    ip_hash;
    server backend1.example.com;
    server backend2.example.com;
}

Spring Cloud LoadBalancer

JAVA
@Configuration
public class LoadBalancerConfig {

    // Round Robin (기본)
    @Bean
    public ReactorLoadBalancer<ServiceInstance> roundRobinLoadBalancer(
            ServiceInstanceListSupplier supplier) {
        return new RoundRobinLoadBalancer(supplier, "my-service");
    }

    // Random
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            ServiceInstanceListSupplier supplier) {
        return new RandomLoadBalancer(supplier, "my-service");
    }
}

AWS ALB / NLB

  • ALB (Application Load Balancer): Round Robin + 느린 시작(slow start) 모드
  • NLB (Network Load Balancer): 플로우 해시 (IP + 포트 조합)

L4 vs L7 로드밸런싱

구분L4 (Transport)L7 (Application)
기준IP, 포트URL, 헤더, 쿠키
속도빠름느림
기능단순 분배콘텐츠 기반 라우팅
예시AWS NLBAWS ALB, Nginx

헬스 체크

어떤 알고리즘을 쓰든, 죽은 서버에 요청을 보내면 안 됩니다.

JAVA
// 주기적 헬스 체크
@Scheduled(fixedRate = 5000)
public void healthCheck() {
    for (String server : servers) {
        try {
            HttpResponse response = httpClient.send(
                HttpRequest.newBuilder()
                    .uri(URI.create(server + "/health"))
                    .timeout(Duration.ofSeconds(2))
                    .build(),
                HttpResponse.BodyHandlers.ofString()
            );
            if (response.statusCode() == 200) {
                markHealthy(server);
            } else {
                markUnhealthy(server);
            }
        } catch (Exception e) {
            markUnhealthy(server);
        }
    }
}

주의할 점

Round Robin에서 서버 성능 차이를 무시하는 실수

서버 사양이 다른데 단순 Round Robin을 적용하면, 느린 서버에 요청이 쌓입니다. Weighted Round Robin이나 Least Connections를 사용해야 합니다.

세션 친화성이 필요한데 Round Robin을 사용하는 실수

세션 기반 인증을 사용하면서 Round Robin을 적용하면, 같은 사용자의 요청이 다른 서버로 가서 세션을 찾을 수 없습니다. IP Hash나 쿠키 기반 고정(sticky session)이 필요합니다.


정리

  • Round Robin: 가장 단순하고 균등한 분배, 서버 성능 차이가 없을 때 적합합니다
  • Weighted Round Robin: 서버 성능에 따라 가중치를 부여합니다
  • Least Connections: 현재 부하가 적은 서버를 선택, 요청 처리 시간이 다양할 때 유리합니다
  • IP Hash: 세션 친화성이 필요할 때 사용합니다
  • Nginx, Spring Cloud, AWS ALB/NLB 등 모든 로드밸런서가 이 알고리즘들을 조합하여 사용합니다
  • 헬스 체크와 결합하여 장애 서버를 자동으로 제외하는 것이 필수입니다
댓글 로딩 중...