1. 서론: 왜 WAS를 외부에 직접 노출하면 안 되는가?

웹 서비스를 처음 배포할 때 흔히 저지르는 실수 중 하나는 Spring Boot(내장 Tomcat)나 Node.js 같은 백엔드 WAS 애플리케이션을 공인 IP의 80/443 포트에 직접 바인딩하여 외부에 노출하는 것입니다.

WAS는 복잡한 비즈니스 로직과 데이터베이스 트랜잭션을 처리하는 데 특화된 소프트웨어입니다. 이를 인터넷 최전선에 그대로 노출하면 다음과 같은 심각한 문제가 발생합니다:

  • 느린 클라이언트 공격(Slowloris 등)에 취약: 대역폭이 좁거나 악의적인 클라이언트가 TCP 연결을 질질 끌 경우 WAS의 귀중한 작업 스레드(Worker Thread)가 묶여 전체 서비스가 마비됩니다.
  • SSL 핸드셰이크의 연산 낭비: 무거운 비대칭 암호화 연산을 JVM이 직접 수행하느라 CPU 자원을 낭비합니다.
  • 정적 리소스 서빙 비효율: 이미지, CSS, JS 같은 정적 정적 파일을 WAS를 거쳐 제공하면 불필요한 서블릿 오버헤드가 발생합니다.
  • 무중단 배포 불가: 서버를 재기동할 때마다 클라이언트에게 즉각 연결 실패 에러가 노출됩니다.

이러한 문제를 완벽하게 해결하는 웹 인프라의 표준 관문이 바로 **Nginx 리버스 프록시(Reverse Proxy)와 로드 밸런서(Load Balancer)**입니다.


2. Nginx 리버스 프록시 아키텍처

클라이언트와 백엔드 WAS 사이에 Nginx가 위치하여 보안, 캐싱, SSL 오프로딩, 그리고 부하 분산을 총괄하는 전체 아키텍처입니다.

flowchart TB Client["외부 클라이언트 (Web / Mobile)"] -- "HTTPS (Port 443)" --> Nginx
subgraph Nginx_DMZ ["Nginx Reverse Proxy Gateway"]
    Nginx["Nginx Master & Worker Processes"]
    SSL["1. SSL Termination (OpenSSL 고속 복호화)"]
    Buffer["2. Request/Response Buffering (Slowloris 방어)"]
    Cache["3. Static Asset Caching (이미지, JS 즉각 서빙)"]
    LB["4. Load Balancer (로드 밸런싱 알고리즘)"]
    
    Nginx --- SSL
    Nginx --- Buffer
    Nginx --- Cache
    Nginx --- LB
end

subgraph Private_VPC ["내부 고속 사설망 (Private VPC)"]
    WAS1["WAS 1 (Spring Boot :8080)"]
    WAS2["WAS 2 (Spring Boot :8080)"]
    WAS3["WAS 3 (Backup Server)"]
end

LB -- "HTTP/1.1 Keep-Alive" --> WAS1
LB -- "HTTP/1.1 Keep-Alive" --> WAS2
LB -. "장애 시 작동" .-> WAS3

3. 핵심 리버스 프록시 설정 (proxy_pass & 헤더 포워딩)

리버스 프록시를 설정할 때 가장 핵심적인 작업은 클라이언트의 원천 정보(IP, 프로토콜, 호스트명)가 백엔드 WAS까지 그대로 온전히 전달되도록 프록시 헤더를 매핑하는 것입니다.

1) location 블록 필수 헤더 설정

location / {
    proxy_pass http://backend_cluster;

    # 1. 원본 요청 호스트 보존
    proxy_set_header Host $host;

    # 2. 클라이언트 실제 공인 IP 전달
    proxy_set_header X-Real-IP $remote_addr;

    # 3. 프록시 경유 IP 체인 누적 보존
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 4. 원래 접속한 프로토콜(http or https) 전달
    proxy_set_header X-Forwarded-Proto $scheme;

    # 5. 백엔드 WAS와의 Keep-Alive 활성화 (중요!)
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

[!IMPORTANT] 왜 proxy_http_version 1.1;과 proxy_set_header Connection "";이 필수인가? Nginx의 기본 백엔드 프록시 통신은 HTTP/1.0을 따릅니다. 즉, 매 요청마다 WAS와 TCP 커넥션을 맺고 끊기를 반복하여 소켓 고갈(TIME_WAIT 폭증)을 초래합니다. HTTP/1.1을 명시하고 Connection 헤더를 비워주어야 백엔드와 영구 연결(Keep-Alive) 풀을 유지하여 레이턴시를 획기적으로 낮출 수 있습니다.


4. 로드 밸런싱 알고리즘과 upstream 블록 설계

upstream 지시어를 사용하여 복수의 백엔드 인스턴스를 하나의 논리적 그룹으로 묶고, 목적에 맞는 트래픽 분산 정책을 수립합니다.

1) 대표적인 분산 전략 4가지

전략 지시어 특징 및 권장 유스케이스
라운드로빈 (Round Robin) (기본값) 모든 서버에 순차적으로 요청 배분. 서버 스펙이 동일할 때 적합
가중치 (Weighted) weight=N 스펙이 높은 서버에 더 많은 트래픽 할당 (예: 16코어 서버에 weight=3)
최소 연결 (Least Connections) least_conn; 현재 활성 TCP 연결 수가 가장 적은 서버로 전달. 응답 시간이 들쭉날쭉한 API에 최적
IP 해시 (IP Hash) ip_hash; 클라이언트 IP를 해싱하여 동일 사용자를 동일 서버로 고정. 세션 동기화 미구축 시 사용

2) upstream 상세 설정 템플릿 (상태 체크 및 페일오버)

upstream backend_cluster {
    # 처리 부하가 가장 적은 서버로 우선 전달
    least_conn;

    # 32개의 영구 Keep-Alive 소켓 유지
    keepalive 32;

    # 가중치 2, 10초 내 3회 실패 시 30초간 비활성화
    server 10.0.1.10:8080 weight=2 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 weight=1 max_fails=3 fail_timeout=30s;

    # 정기 점검 또는 긴급 백업용 대기 서버
    server 10.0.1.12:8080 backup;
}
  • max_fails=3 fail_timeout=30s: 30초 동안 3번 연속 연결 실패나 타임아웃이 발생하면 해당 서버를 비정상(Unhealthy)으로 판단하고 트래픽 인입을 30초간 차단합니다.
  • backup: 등록된 모든 메인 서버가 다운되었을 때만 비상으로 투입되는 예비 인스턴스입니다.

5. 버퍼링 및 타임아웃 튜닝 (Slow Client 방어)

Nginx는 백엔드 WAS로부터 내려오는 응답 바이트를 메모리 버퍼에 고속으로 수신한 뒤 백엔드 연결을 즉시 해제하고, 느린 모바일 네트워크 클라이언트에게는 Nginx가 천천히 데이터를 밀어줍니다.

# 프록시 버퍼 튜닝
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 16 16k;
proxy_busy_buffers_size 32k;

# 타임아웃 설정 (밀리초/초 단위)
proxy_connect_timeout 5s;  # 백엔드 TCP 연결 수립 대기 시간
proxy_read_timeout 60s;    # 백엔드로부터 데이터 수신 대기 시간
proxy_send_timeout 60s;    # 백엔드로 데이터 전송 대기 시간

6. 프로덕션 통합 nginx.conf 완성 예제

SSL 종단, Gzip 압축, 접근 제어, 로드 밸런싱이 모두 통합된 실무용 가상 호스트 설정입니다.

# HTTP ➔ HTTPS 강제 리다이렉트
server {
    listen 80;
    server_name api.myservice.com;
    return 301 https://$host$request_uri;
}

# HTTPS 메인 리버스 프록시 서버
server {
    listen 443 ssl http2;
    server_name api.myservice.com;

    # SSL 인증서 경로
    ssl_certificate /etc/letsencrypt/live/api.myservice.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.myservice.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # 정적 리소스 직접 서빙 (WAS 부하 제거)
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
        root /var/www/static;
        expires 7d;
        add_header Cache-Control "public, no-transform";
    }

    # 백엔드 API 클러스터 프록시
    location / {
        proxy_pass http://backend_cluster;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # 502/504 발생 시 즉시 다른 서버로 페일오버 재시도
        proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
    }
}

7. 트러블슈팅 및 장애 검증 가이드

  1. 502 Bad Gateway 에러:
    • Nginx가 백엔드 WAS 포트에 접근하지 못할 때 발생합니다. curl -I http://10.0.1.10:8080으로 WAS가 실제로 구동 중인지, 사설망 방화벽(Security Group)이 열려 있는지 확인하세요.
  2. 504 Gateway Timeout 에러:
    • 백엔드 애플리케이션의 특정 쿼리나 API가 proxy_read_timeout(기본 60초)을 초과하여 응답하지 않을 때 발생합니다. WAS의 슬로우 쿼리를 점검하거나 타임아웃 값을 조정해야 합니다.
  3. 설정 문법 검증 및 무중단 적용:
    # 문법 이상 유무 검사
    sudo nginx -t
    
    # 무중단 서비스 리로드 (클라이언트 연결 유지)
    sudo systemctl reload nginx
    

8. 결론

Nginx 리버스 프록시와 로드 밸런서는 현대 웹 인프라에서 보안과 확장성을 지탱하는 가장 견고한 방패이자 분배기입니다.

단일 인스턴스를 운영하더라도 앞단에 Nginx를 배치하여 SSL 오프로딩과 느린 클라이언트 방어를 적용하고, 서비스가 성장할 때 upstream 블록에 WAS 노드를 추가하는 것만으로 손쉬운 수평 확장(Scale-out)을 달성하시기 바랍니다.