DevOps

Nginx 완벽 가이드 - 리버스 프록시부터 로드 밸런싱까지

백엔드 개발자 김승원 2026. 4. 15. 11:56

들어가며

"Spring Boot 앱을 직접 80 포트로 열면 안 되나요?" 개발 환경에서는 가능하지만, 운영 환경에서는 절대 권장하지 않습니다. SSL 처리, 정적 파일 서빙, 로드 밸런싱, rate limiting, 그리고 보안. 이 모든 것을 애플리케이션 서버 앞에서 처리해주는 것이 바로 Nginx입니다.

3~7년차 백엔드 개발자라면 Nginx 설정 파일을 한두 번은 만져봤겠지만, 복사해서 붙여넣기만 하다 보니 정확한 동작 원리를 모르는 경우가 많습니다. location 블록의 매칭 우선순위, upstream의 로드 밸런싱 알고리즘, SSL 설정의 보안 모범 사례 등을 제대로 이해하면 서비스의 안정성과 성능을 한 단계 높일 수 있습니다.

이 글에서는 Nginx의 이벤트 기반 아키텍처부터 리버스 프록시, 로드 밸런싱, SSL/TLS, rate limiting, 캐싱, WebSocket 프록시, Docker 활용까지 운영에서 바로 쓸 수 있는 설정을 다룹니다.

1. Nginx 아키텍처: 이벤트 기반 모델

Nginx가 Apache보다 높은 동시 처리 성능을 보이는 이유는 이벤트 기반(Event-Driven) 아키텍처 덕분입니다.

Apache vs Nginx 비교

항목 Apache (prefork) Nginx
처리 모델 프로세스/스레드 기반 이벤트 기반 (비동기)
동시 연결 연결당 1 스레드 단일 스레드로 수천 연결 처리
메모리 사용 연결 수에 비례 증가 거의 일정
정적 파일 보통 매우 빠름
동적 처리 모듈로 직접 처리 (mod_php 등) 프록시로 전달
# Nginx 프로세스 구조
# Master Process: 설정 읽기, Worker 관리
# Worker Process: 실제 요청 처리 (이벤트 루프)

#   ┌─────────────────────────┐
#   │     Master Process      │
#   │  (설정 읽기, Worker 관리) │
#   └──┬──────┬──────┬────────┘
#      │      │      │
#   ┌──▼──┐┌──▼──┐┌──▼──┐
#   │Worker││Worker││Worker│  ← CPU 코어 수만큼
#   │  1  ││  2  ││  3  │
#   └─────┘└─────┘└─────┘
#   각 Worker가 수천 개의 동시 연결을 이벤트 루프로 처리

# nginx.conf 기본 설정
worker_processes auto;           # CPU 코어 수에 맞게 자동 설정
worker_rlimit_nofile 65535;      # Worker당 최대 파일 디스크립터

events {
    worker_connections 4096;     # Worker당 최대 동시 연결
    use epoll;                   # Linux 이벤트 방식 (Linux)
    multi_accept on;             # 한 번에 여러 연결 수락
}

2. server/location 블록 이해

server 블록: 가상 호스트

# /etc/nginx/conf.d/myapp.conf

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

# HTTPS 서버
server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # 공통 설정
    charset utf-8;
    client_max_body_size 10M;

    # 로그
    access_log /var/log/nginx/api.access.log;
    error_log /var/log/nginx/api.error.log;

    # location 블록들...
}

location 블록: URL 매칭 우선순위

# location 매칭 우선순위 (높음 → 낮음)

# 1. 정확한 매칭 (=)
location = /health {
    return 200 'OK';
    add_header Content-Type text/plain;
}

# 2. 우선 접두사 매칭 (^~)
location ^~ /static/ {
    root /var/www;
    expires 30d;
}

# 3. 정규표현식 매칭 (~: 대소문자 구분, ~*: 무시)
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    root /var/www/static;
    expires 7d;
    add_header Cache-Control "public, immutable";
}

# 4. 일반 접두사 매칭 (없음 또는 /)
location /api/ {
    proxy_pass http://backend;
}

location / {
    root /var/www/html;
    try_files $uri $uri/ /index.html;
}

# 매칭 테스트 예시
# GET /health          → = /health (정확한 매칭)
# GET /static/logo.png → ^~ /static/ (우선 접두사)
# GET /img/photo.jpg   → ~* \.(jpg|...)$ (정규표현식)
# GET /api/orders      → /api/ (접두사 매칭)
# GET /about           → / (기본 매칭)

3. 리버스 프록시 설정 (Spring Boot 연동)

# Spring Boot 앱이 localhost:8080에서 실행 중

upstream spring_backend {
    server 127.0.0.1:8080;
    keepalive 32;  # 백엔드와의 keepalive 연결 유지
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # SSL 설정 (생략)

    location / {
        proxy_pass http://spring_backend;

        # 필수 헤더 전달
        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_set_header X-Forwarded-Port $server_port;

        # 타임아웃 설정
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # 버퍼 설정
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;

        # keepalive 연결 사용
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    # 헬스체크 엔드포인트 (Actuator)
    location /actuator/health {
        proxy_pass http://spring_backend;
        proxy_set_header Host $host;
        access_log off;  # 헬스체크 로그 비활성화
    }
}

Spring Boot 설정 (프록시 인식)

# application.yml
server:
  port: 8080
  forward-headers-strategy: framework  # X-Forwarded-* 헤더 신뢰
  tomcat:
    remoteip:
      remote-ip-header: X-Real-IP
      protocol-header: X-Forwarded-Proto

4. upstream으로 로드 밸런싱

# 로드 밸런싱 알고리즘

# 1. Round Robin (기본값)
upstream backend_rr {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

# 2. Weighted Round Robin (가중치)
upstream backend_weighted {
    server 10.0.0.1:8080 weight=5;  # 5배 더 많은 트래픽
    server 10.0.0.2:8080 weight=3;
    server 10.0.0.3:8080 weight=1;  # 성능이 낮은 서버
}

# 3. Least Connections (최소 연결)
upstream backend_least {
    least_conn;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

# 4. IP Hash (세션 고정)
upstream backend_iphash {
    ip_hash;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

# 5. 실무 권장 설정
upstream backend {
    least_conn;

    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:8080 backup;  # 백업 서버 (다른 서버 모두 다운 시)

    keepalive 64;  # keepalive 연결 풀
}

# max_fails=3: 3번 실패하면 서버를 비활성화
# fail_timeout=30s: 30초 후 다시 시도
# backup: 다른 서버가 모두 다운될 때만 사용

5. SSL/TLS 설정 (Let's Encrypt + Certbot)

Certbot으로 인증서 발급

# Certbot 설치 (Ubuntu)
sudo apt update
sudo apt install certbot python3-certbot-nginx

# 인증서 발급 (Nginx 플러그인)
sudo certbot --nginx -d api.example.com -d www.example.com

# 인증서 자동 갱신 확인
sudo certbot renew --dry-run

# 자동 갱신 cron 등록
# /etc/cron.d/certbot
0 0,12 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"

SSL 보안 최적화

# /etc/nginx/conf.d/ssl.conf (공통 SSL 설정)

# TLS 버전 (1.2 이상만 허용)
ssl_protocols TLSv1.2 TLSv1.3;

# 암호화 스위트
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# SSL 세션 캐시
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

# 보안 헤더
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

6. Rate Limiting

# Rate Limiting 설정
# http 블록에 정의
http {
    # 요청 제한 존 정의
    # $binary_remote_addr: IP 기반 제한
    # zone=api_limit:10m: 10MB 공유 메모리 (약 16만 IP 주소)
    # rate=10r/s: 초당 10 요청
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # 로그인 엔드포인트는 더 엄격하게
    limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

    # 429 응답 커스텀
    limit_req_status 429;

    server {
        # API 엔드포인트
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            # burst=20: 최대 20개 요청 버스트 허용
            # nodelay: 버스트 요청도 즉시 처리 (대기하지 않음)
            proxy_pass http://backend;
        }

        # 로그인 엔드포인트
        location /api/auth/login {
            limit_req zone=login_limit burst=5 nodelay;
            proxy_pass http://backend;
        }

        # 특정 IP 제외 (모니터링 서버 등)
        location /api/internal/ {
            allow 10.0.0.0/8;
            deny all;
            proxy_pass http://backend;
        }
    }
}

7. Gzip 압축과 캐싱

Gzip 압축

http {
    # Gzip 압축 활성화
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;          # 압축 레벨 (1-9, 6이 적절)
    gzip_min_length 1000;       # 1KB 이상만 압축
    gzip_types
        text/plain
        text/css
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        image/svg+xml;

    # Gzip 미리 압축된 파일 사용
    gzip_static on;  # .gz 파일이 있으면 직접 서빙
}

프록시 캐싱

http {
    # 캐시 저장소 정의
    proxy_cache_path /var/cache/nginx
        levels=1:2
        keys_zone=api_cache:10m   # 10MB 키 저장소
        max_size=1g               # 최대 1GB 캐시
        inactive=60m              # 60분 미사용 시 삭제
        use_temp_path=off;

    server {
        # 캐싱 적용
        location /api/products {
            proxy_cache api_cache;
            proxy_cache_valid 200 10m;       # 200 응답: 10분 캐시
            proxy_cache_valid 404 1m;        # 404 응답: 1분 캐시
            proxy_cache_use_stale error timeout updating;
            proxy_cache_lock on;             # 동일 요청 중복 방지

            # 캐시 상태 헤더 추가 (디버깅용)
            add_header X-Cache-Status $upstream_cache_status;
            # HIT, MISS, EXPIRED, BYPASS 등

            proxy_pass http://backend;
        }

        # 캐시 우회 (관리자 요청)
        location /api/admin/ {
            proxy_cache_bypass $http_authorization;
            proxy_no_cache $http_authorization;
            proxy_pass http://backend;
        }
    }
}

8. 정적 파일 서빙

server {
    listen 443 ssl http2;
    server_name www.example.com;

    # 정적 파일 서빙 (SPA)
    root /var/www/html;

    location / {
        try_files $uri $uri/ /index.html;
        # 1. 요청 URI의 파일 찾기
        # 2. 디렉토리 찾기
        # 3. 없으면 index.html (SPA 라우팅)
    }

    # 정적 자산 캐싱
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # 미디어 파일
    location ~* \.(mp4|webm|ogg)$ {
        root /var/www/media;
        sendfile on;
        sendfile_max_chunk 1m;
        tcp_nopush on;
    }

    # API 프록시
    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

9. WebSocket 프록시

# WebSocket 연결을 위한 Nginx 설정

# WebSocket용 맵 설정
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # 일반 API
    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket 엔드포인트
    location /ws/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # WebSocket 타임아웃 (기본 60s는 너무 짧음)
        proxy_read_timeout 3600s;   # 1시간
        proxy_send_timeout 3600s;

        # 버퍼링 비활성화 (실시간 통신)
        proxy_buffering off;
    }

    # Spring Boot STOMP WebSocket
    location /ws-stomp {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}

10. Docker에서 Nginx 사용

docker-compose.yml

version: '3.8'

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./static:/var/www/static:ro
    depends_on:
      - app1
      - app2
    restart: unless-stopped
    networks:
      - app-network

  app1:
    image: myapp:latest
    expose:
      - "8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    restart: unless-stopped
    networks:
      - app-network

  app2:
    image: myapp:latest
    expose:
      - "8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    restart: unless-stopped
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h; done'"

networks:
  app-network:
    driver: bridge

Docker 환경 Nginx 설정

# nginx/conf.d/default.conf
# Docker에서는 서비스명으로 접근

upstream backend {
    least_conn;
    server app1:8080 max_fails=3 fail_timeout=30s;
    server app2:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    # Certbot 인증 챌린지
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    location / {
        proxy_pass http://backend;
        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 "";
    }
}

Nginx 설정 테스트와 리로드

# 설정 문법 테스트
docker exec nginx nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Zero-downtime 리로드
docker exec nginx nginx -s reload
# Worker 프로세스가 현재 요청 처리 후 새 설정으로 교체

# 로그 확인
docker logs -f nginx

# 실시간 접속 수 확인
location /nginx_status {
    stub_status on;
    allow 10.0.0.0/8;   # 내부 네트워크만 허용
    deny all;
}
# Active connections: 291
# server accepts handled requests
#  16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106

마치며

Nginx는 단순한 웹 서버를 넘어서, 현대 백엔드 아키텍처에서 리버스 프록시, 로드 밸런서, SSL 터미네이터, 캐시 서버 등 다양한 역할을 수행합니다. 이 글의 핵심 내용을 정리합니다.

  • 이벤트 기반 아키텍처: 적은 리소스로 수만 동시 연결을 처리합니다. worker_processes는 CPU 코어 수에 맞추고, worker_connections는 예상 동시 접속의 2배 이상으로 설정합니다.
  • location 매칭 우선순위를 이해: 정확한 매칭(=) > 우선 접두사(^~) > 정규표현식(~, ~*) > 접두사 매칭 순입니다. 의도하지 않은 매칭이 발생하지 않도록 우선순위를 파악합니다.
  • 리버스 프록시는 헤더 전달이 핵심: X-Real-IP, X-Forwarded-For, X-Forwarded-Proto를 반드시 전달하고, Spring Boot에서 forward-headers-strategy를 설정합니다.
  • 로드 밸런싱은 least_conn + health check: 대부분의 경우 least_conn이 균등하게 분산합니다. max_fails와 fail_timeout으로 장애 서버를 자동 제외합니다.
  • SSL은 TLS 1.2+ 필수: Let's Encrypt + Certbot으로 무료 인증서를 발급하고, 자동 갱신을 설정합니다. HSTS 헤더를 추가합니다.
  • Docker 환경에서 서비스명으로 연결: docker-compose의 서비스명이 DNS 역할을 합니다. nginx -t로 설정을 검증하고, nginx -s reload로 무중단 반영합니다.

Nginx 설정은 처음에는 복잡해 보이지만, 패턴을 이해하면 매우 직관적입니다. 이 글의 설정을 기반으로 서비스에 맞게 커스터마이징하면, 안정적이고 성능이 좋은 서비스 인프라를 구축할 수 있습니다.