Load Balancer의 7가지 용도: 자세한 설명과 예시
1. 개요
Load Balancer(LB)를 떠올리면 많은 사람들이 단순하게 생각합니다: “여러 서버에 요청을 분산해준다”고요. 실제로 LB는 그 이상의 일을 합니다 — SSL 부하 감소, 사용자 세션 유지, DDoS 방어, 백엔드 상태 모니터링 등.
이 글에서는 Load Balancer의 7가지 주요 용도를 살펴봅시다. 각 용도마다 구체적인 설정이나 코드 예시(nginx, Spring Boot, Kubernetes)가 있어서 실제 시스템에서 어떻게 작동하는지 이해할 수 있습니다. 다 읽고 나면 LB에서 어떤 기능을 언제 활성화해야 하는지, 그리고 흔한 설정 실수를 어떻게 피할 수 있는지 알게 될 거예요.
2. 부하 분산(Traffic Distribution)
이것이 가장 기본적인 기능입니다: LB는 클라이언트의 요청을 받아 여러 백엔드 서버 중 하나로 전달합니다. 이때 사용하는 알고리즘은 round robin, least connections, weighted round robin 등 여러 가지가 있습니다.
upstream order_service {
server 10.0.1.11:8080;
server 10.0.1.12:8080;
server 10.0.1.13:8080;
}
server {
listen 80;
location /orders/ {
proxy_pass http://order_service;
}
}
upstream은 OrderService의 3개 인스턴스를 선언합니다. 기본적으로 nginx는 round robin을 사용합니다 — 요청 1은 .11로, 요청 2는 .12로, 요청 3은 .13으로, 그리고 다시 .11로 갑니다. 만약 어떤 인스턴스가 더 무거운 요청을 처리한다면, least_conn;으로 전환해서 nginx가 열려있는 연결이 가장 적은 서버를 우선적으로 선택하게 할 수 있습니다.
주의: Round robin은 서버가 건강한지 약한지 신경 쓰지 않습니다. 인스턴스가 다른 성능을 가지고 있다면(예를 들어 한 머신이 더 강력한 구성),
weight를 사용하세요:server 10.0.1.11:8080 weight=3; server 10.0.1.12:8080 weight=1;
3. SSL Termination
각 백엔드가 TLS를 직접 해독하는 대신(CPU 낭비), 이 작업을 LB에 맡기세요. LB는 인증서를 보유하고 HTTPS를 순수 HTTP로 해독한 후 내부 네트워크로 전달합니다.
server {
listen 443 ssl;
server_name api.nonladev.com;
ssl_certificate /etc/nginx/certs/nonladev.crt;
ssl_certificate_key /etc/nginx/certs/nonladev.key;
location / {
proxy_pass http://order_service; # 순수 HTTP, TLS 불필요
proxy_set_header X-Forwarded-Proto https;
}
}
이렇게 하면 Spring Boot로 작성된 OrderService는 server.ssl.*를 설정할 필요가 없습니다 — 포트 8080에서 일반 HTTP로만 실행됩니다. X-Forwarded-Proto 헤더는 백엔드가 원래 요청이 HTTPS였음을 알도록 도와서 리다이렉트 URL을 올바르게 생성할 수 있게 합니다.
주의: LB와 백엔드 사이의 내부 네트워크를 신뢰할 수 없다면(예: 여러 서브넷, 다른 클라우드를 거쳐서), termination만으로는 부족합니다 — “SSL bridging”을 사용하세요: LB가 해독한 후 내부 인증서로 다시 암호화한 후 전달합니다.
4. 세션 유지(Session Persistence)
백엔드가 메모리에 세션을 저장할 때(로컬 세션), 같은 사용자의 요청은 항상 같은 인스턴스로 가야 합니다. 그렇지 않으면 사용자가 갑자기 “로그아웃”될 수 있습니다.
upstream order_service {
ip_hash; # 같은 클라이언트 IP → 같은 서버
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
ip_hash는 클라이언트 IP를 해싱하여 upstream의 서버를 고정적으로 선택합니다. 다른 방법은 쿠키 기반의 sticky session입니다 — LB가 스스로 쿠키를 붙여 인스턴스를 표시하며, 클라이언트가 공유 NAT 뒤에 있어도 작동합니다.
더 강력한 해결책은 sticky session을 버리고 Spring Session + Redis를 사용해 세션을 외부화하는 것입니다:
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
}
spring.data.redis.host=redis.internal
spring.data.redis.port=6379
이 설정을 사용하면 세션이 각 인스턴스의 메모리 대신 Redis에 저장됩니다. 어느 서버로 요청이 가든 세션을 읽을 수 있습니다 — LB는 일반 round robin을 사용할 수 있으며 ip_hash가 더 이상 필요하지 않습니다.
주의: 많은 사용자가 같은 NAT을 공유하면(예: 회사 사무실)
ip_hash는 불균형한 부하 분산을 초래할 수 있습니다 — 그들의 모든 트래픽이 1개 서버로 몰립니다. 시스템이 커지면 세션을 외부화하는 것을 우선하세요.
5. 고가용성(High Availability)
LB는 지속적으로 백엔드를 확인합니다. 인스턴스가 다운되면 LB는 자동으로 그것을 서비스 목록에서 제거하고 트래픽을 살아있는 인스턴스로 보냅니다 — 클라이언트는 아무것도 모릅니다.
upstream order_service {
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}
max_fails=3은 3번의 연속 요청 실패 후 nginx가 그 서버를 30초 동안 “down” 상태로 표시합니다(fail_timeout), 새 요청을 보내지 않습니다. 30초 후 nginx는 다시 시도합니다 — 서버가 복구되었다면 다시 순환에 들어갑니다.
[14:02:01] 10.0.1.12 timeout -> fail count 1
[14:02:05] 10.0.1.12 timeout -> fail count 2
[14:02:09] 10.0.1.12 timeout -> fail count 3 -> marked DOWN for 30s
[14:02:09] traffic rerouted to 10.0.1.11
[14:02:39] 10.0.1.12 retried -> healthy -> back in pool
이 메커니즘 덕분에 하나의 인스턴스 충돌이 전체 시스템을 다운시키지 않습니다 — 이것이 네트워크 계층의 고가용성의 기초입니다.
6. 수평 확장(Scalability)
LB는 수평 확장이 작동하기 위한 필수 조건입니다: 새 인스턴스를 추가하면 LB가 자동으로 인식하고 부하를 분산합니다. 백엔드 코드를 수정할 필요가 없습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 2
template:
spec:
containers:
- name: order-service
image: nonladev/order-service:1.4.0
ports:
- containerPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
여기서 Kubernetes Service(내부 LB 역할)는 order-service pod들 사이의 트래픽을 자동으로 균형을 유지합니다. HorizontalPodAutoscaler는 CPU를 모니터링합니다 — 70%를 초과하면 Kubernetes는 추가 pod를 생성합니다(최대 10개). LB는 즉시 새 pod를 서비스 목록에 추가하며, redeploy나 restart가 필요하지 않습니다.
주의: Scaling은 백엔드가 stateless인 경우에만 효과적입니다(또는 섹션 4처럼 세션이 외부화된 경우). 그렇지 않으면 새로 추가된 pod는 사용자의 기존 세션을 얻지 못합니다.
7. DDoS 완화(DDoS Mitigation)
LB는 에지 계층에 있으므로 비정상 트래픽을 백엔드에 도달하기 전에 차단하는 데 이상적입니다 — rate limiting이나 트래픽을 더 넓게 분산시켜서요.
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
server {
location /orders/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass http://order_service;
}
}
limit_req_zone은 각 IP당 최대 초당 20개 요청으로 제한합니다. burst=10은 최대 10개 요청을 단기간에 초과하도록 허용하며, nodelay는 버스트를 즉시 처리하고 지연시키지 않습니다. 이 한계를 초과하면 nginx는 503 Service Temporarily Unavailable을 반환하고 백엔드로 요청을 보내지 않습니다.
애플리케이션 계층에서 rate limiting을 원한다면(LB를 보완하는 것이지 대체하지 않음), Spring Boot에서 Bucket4j를 사용하세요:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>
Bucket bucket = Bucket.builder()
.addLimit(Bandwidth.simple(20, Duration.ofSeconds(1)))
.build();
@GetMapping("/orders")
public ResponseEntity<?> getOrders() {
if (!bucket.tryConsume(1)) {
return ResponseEntity.status(429).body("Too many requests");
}
return ResponseEntity.ok(orderService.findAll());
}
주의: IP 기반 rate limiting은 스푸핑된 IP나 분산 봇넷으로 우회될 수 있습니다. 대규모 DDoS 공격의 경우 LB를 앞단의 CDN/WAF와 결합해야 합니다. LB는 두 번째 방어선일 뿐입니다.
8. 백엔드 상태 모니터링(Health Monitoring)
섹션 5(요청 실패에 반응)와 달리, health monitoring은 LB가 능동적으로 엔드포인트를 주기적으로 호출하여 서버가 살아있는지 확인하는 것입니다 — active health check라고 합니다.
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<String> health() {
boolean dbUp = checkDatabaseConnection();
if (!dbUp) {
return ResponseEntity.status(503).body("DB_DOWN");
}
return ResponseEntity.ok("OK");
}
private boolean checkDatabaseConnection() {
// connection pool을 확인하고 true/false를 반환
return true;
}
}
Spring Boot Actuator를 사용하면 손으로 작성할 필요 없이 유사한 엔드포인트가 이미 있습니다:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=always
LB(또는 Kubernetes)는 몇 초마다 /actuator/health를 호출합니다:
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
periodSeconds: 5
failureThreshold: 3
DB가 실패할 때 엔드포인트가 올바르게 응답하는지 확인하는 테스트를 작성하세요:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HealthControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldReturn503WhenDatabaseDown() {
// test 프로필이나 mock bean을 통해 DB 다운을 시뮬레이션
ResponseEntity<String> response =
restTemplate.getForEntity("/health", String.class);
assertThat(response.getStatusCode().value()).isIn(200, 503);
}
}
이 테스트는 /health 엔드포인트가 항상 유효한 상태(200 또는 503)를 반환하고 예외를 던지지 않음을 보장합니다 — health check 자체가 실패하면 LB가 서버가 다운되었다고 오해하고 건강한 인스턴스를 잘못 제거할 수 있기 때문입니다.
주의: Health check 엔드포인트는 가볍고 빨라야 합니다(몇 백 ms 이내). 많은 테이블을 쿼리하거나 다른 서비스를 호출하면 health check 자체가 타임아웃의 원인이 되고 LB가 잘못 제거할 수 있습니다.
9. 결론
Load Balancer는 단순한 “부하 분산기”가 아닙니다 — SSL 오프로딩, 세션 유지, 장애 감지, 수평 확장 지원, DDoS 방어의 첫 번째 방어선 역할도 합니다. 시스템을 설계할 때 위의 7가지 용도 중 어떤 것이 필요한지 명확히 파악하고, LB 설정에서 올바른 기능을 선택해야 합니다 — nginx, HAProxy, AWS ALB, Kubernetes Service는 모두 대부분의 사용 사례를 지원하며, 선언 방식만 다릅니다.
마지막 조언: 모든 책임을 하나의 LB에 몰아주지 마세요. Active health check, rate limiting, 세션 외부화를 결합하면 시스템이 단순히 round robin을 켜두는 것보다 훨씬 잘 확장되고 장애에 강해집니다.