웹 서비스 앞에는 항상 관문이 필요하다. 수만 개의 동시 접속을 처리하고, 뒷단 서버들에 트래픽을 나눠주고, SSL을 종료하고, 정적 파일을 직접 서빙하는 역할이다. Nginx는 이 모든 것을 단 하나의 프로세스 모델로 처리한다.

비유: 대형 백화점 입구의 안내 데스크와 같다. 방문객(요청)이 오면 어느 층(서버)으로 갈지 안내하고, 신분증 확인(SSL/인증), 인원 제한(Rate Limiting), 안내 책자 직접 배포(정적 파일)까지 처리한다. 실제 각 층 매장은 자신의 업무에만 집중하면 된다.


Nginx 아키텍처: Master/Worker 모델

Nginx가 수만 개의 동시 접속을 처리할 수 있는 이유는 아키텍처에 있다.

graph TD subgraph "Nginx 프로세스 모델" M[Master Process\nPID 1\n설정 로드/관리] W1[Worker Process 1\nEvent Loop] W2[Worker Process 2\nEvent Loop] W3[Worker Process 3\nEvent Loop] W4[Worker Process 4\nEvent Loop] M -->|fork| W1 M -->|fork| W2 M -->|fork| W3 M -->|fork| W4 end C1[Client 1] --> W1 C2[Client 2] --> W1 C3[Client 3] --> W2 C4[Client ...10000] --> W3
Apache (기존 방식):
  요청 1개 = 스레드/프로세스 1개
  10,000 동시 접속 = 10,000 스레드 → 메모리 고갈 (C10K Problem)

Nginx (이벤트 기반):
  Worker 프로세스 수 = CPU 코어 수 (보통 4~8개)
  각 Worker: 비동기 이벤트 루프로 수천 개 연결 처리
  10,000 동시 접속 → Worker 4개로 처리 가능

Master Process 역할

  • 설정 파일(nginx.conf) 읽기 및 유효성 검사
  • Worker 프로세스 생성/관리/재시작
  • 무중단 재로드: nginx -s reload → 새 Worker 생성 후 구 Worker 종료
  • 소켓 바인딩 (1024 이하 포트는 root 필요)

Worker Process 역할

  • 실제 클라이언트 요청 처리
  • 비동기 I/O (epoll on Linux, kqueue on macOS)
  • 단일 스레드 + 이벤트 루프 (Node.js와 유사)

nginx.conf 구조

# 전역 설정
user nginx;
worker_processes auto;          # CPU 코어 수에 맞게 자동 설정
worker_rlimit_nofile 65535;     # Worker당 최대 파일 디스크립터 수
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    # Worker당 최대 동시 연결 수
    worker_connections 1024;
    # epoll 사용 (Linux 최적화)
    use epoll;
    # 한번에 여러 연결 수락
    multi_accept on;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # 로그 형식 정의
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    # sendfile: 커널 레벨 파일 전송 (정적 파일 고속화)
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Keep-Alive 설정
    keepalive_timeout 65;
    keepalive_requests 100;

    # Gzip 압축
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    gzip_min_length 1024;
    gzip_comp_level 6;

    # 가상 호스트 설정 포함
    include /etc/nginx/conf.d/*.conf;
}

리버스 프록시

클라이언트는 Nginx와 통신하고, Nginx가 뒷단 서버에 요청을 전달한다. 뒷단 서버의 IP/포트는 외부에 노출되지 않는다.

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

    location /api/ {
        # 뒷단 서버로 요청 전달
        proxy_pass http://127.0.0.1:8080;

        # 원래 클라이언트 정보를 헤더로 전달
        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_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

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

업스트림 헤더 처리

Spring Boot에서 클라이언트 IP를 올바르게 얻으려면:

// application.yml
server:
  forward-headers-strategy: native  # 또는 framework

// X-Forwarded-For 헤더에서 실제 IP 추출
String clientIp = request.getHeader("X-Real-IP");
// 또는
String clientIp = request.getRemoteAddr();  // forward-headers-strategy 설정 시 자동

로드밸런싱

여러 뒷단 서버에 트래픽을 분산한다.

upstream order_service {
    # 알고리즘 선택 (기본: round-robin)
    # least_conn;      # 연결 수가 가장 적은 서버 우선
    # ip_hash;         # 클라이언트 IP 기반 고정 (세션 유지)
    # random two;      # 2개 무작위 선택 후 least_conn 적용

    server 10.0.0.1:8080 weight=3;   # 가중치 (트래픽 3배 할당)
    server 10.0.0.2:8080 weight=2;
    server 10.0.0.3:8080 weight=1;

    # 헬스체크
    server 10.0.0.4:8080 backup;     # 모든 서버 다운 시 사용
    server 10.0.0.5:8080 down;       # 수동으로 제외

    # 연결 유지
    keepalive 32;   # upstream 서버와 keep-alive 연결 수
}

server {
    listen 80;

    location /orders {
        proxy_pass http://order_service;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # keepalive를 위해 Connection 헤더 제거
    }
}
graph LR C[클라이언트] --> N[Nginx\nLoad Balancer] N -->|weight=3 33%| S1[Server 1\n:8080] N -->|weight=2 22%| S2[Server 2\n:8080] N -->|weight=1 11%| S3[Server 3\n:8080] N -.->|backup| S4[Server 4\n:8080]

헬스체크 (Nginx Plus / OSS 모듈)

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;

    # OSS: 수동 헬스체크
    # max_fails: 이 횟수만큼 실패 시 일시 제외
    # fail_timeout: 제외 기간 (이후 다시 시도)
    server 10.0.0.3:8080 max_fails=3 fail_timeout=30s;
}

정적 파일 서빙

Nginx는 정적 파일을 직접 서빙하는 데 극도로 최적화돼 있다. 동적 요청만 WAS로 전달하면 WAS 부하를 크게 줄일 수 있다.

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

    # 정적 파일 루트
    root /var/www/html;

    # 정적 파일 직접 서빙
    location /static/ {
        # sendfile + tcp_nopush로 Zero-Copy 전송
        sendfile on;

        # 브라우저 캐시 제어
        expires 30d;
        add_header Cache-Control "public, immutable";

        # gzip 사전 압축 파일 사용
        gzip_static on;
    }

    # SPA (Single Page Application) 설정
    location / {
        try_files $uri $uri/ /index.html;
        # → 파일 없으면 index.html 반환 (React Router 등 지원)
    }

    # API는 뒷단으로
    location /api/ {
        proxy_pass http://backend;
    }

    # 이미지 최적화
    location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;  # 이미지 요청 로그 제외
    }
}

SSL/TLS 설정

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

    # 인증서 파일
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

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

    # 강력한 암호화 스위트
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;  # TLS 1.3에서는 off 권장

    # SSL 세션 캐시 (핸드셰이크 비용 절감)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS (HTTP Strict Transport Security)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # OCSP Stapling (인증서 유효성 검사 속도 향상)
    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_pass http://backend;
    }
}

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

Let’s Encrypt 자동화

# Certbot으로 인증서 발급 및 자동 갱신
certbot --nginx -d api.example.com -d www.example.com

# 자동 갱신 (cron)
0 12 * * * /usr/bin/certbot renew --quiet

Rate Limiting

DoS 공격 방어, API 쿼터 적용에 사용한다.

http {
    # Rate Limit Zone 정의
    # $binary_remote_addr: 클라이언트 IP (binary 형식, 메모리 효율)
    # 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:1m rate=1r/s;

    server {
        location /api/ {
            # burst: 순간 버스트 허용 (대기열)
            # nodelay: burst 요청도 즉시 처리 (지연 없이)
            limit_req zone=api_limit burst=20 nodelay;
            limit_req_status 429;  # Too Many Requests

            proxy_pass http://backend;
        }

        location /auth/login {
            limit_req zone=login_limit burst=5;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}
동작 예시 (rate=10r/s, burst=20):
  1초에 30개 요청 도착
  → 10개: 즉시 처리
  → 20개: burst 큐에 대기
  → 나머지: 429 반환

  다음 1초에 5개 요청:
  → 큐에서 10개 소화 + 5개 즉시 처리

캐싱

http {
    # 캐시 저장 경로 및 설정
    proxy_cache_path /var/cache/nginx
        levels=1:2
        keys_zone=api_cache:10m    # 캐시 키 메타데이터 메모리
        max_size=1g                # 최대 캐시 크기
        inactive=60m               # 60분간 미사용 시 삭제
        use_temp_path=off;

    server {
        location /api/products {
            proxy_cache api_cache;
            proxy_cache_key "$scheme$host$request_uri";
            proxy_cache_valid 200 304 10m;    # 200/304 응답 10분 캐시
            proxy_cache_valid 404 1m;          # 404는 1분
            proxy_cache_bypass $http_pragma;   # Pragma: no-cache 시 캐시 우회
            proxy_no_cache $http_authorization;  # 인증 요청은 캐시 안함

            # 캐시 상태 헤더 추가 (디버깅)
            add_header X-Cache-Status $upstream_cache_status;

            proxy_pass http://backend;
        }

        # 캐시 퍼지 엔드포인트 (내부망만 허용)
        location /purge/ {
            allow 10.0.0.0/8;
            deny all;
            proxy_cache_purge api_cache $scheme$host$1;
        }
    }
}

보안 설정

http {
    # 서버 정보 숨기기
    server_tokens off;

    # 보안 헤더
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # 버퍼 크기 제한 (버퍼 오버플로 방어)
    client_body_buffer_size 1k;
    client_header_buffer_size 1k;
    client_max_body_size 10m;        # 파일 업로드 한도
    large_client_header_buffers 4 8k;

    # 타임아웃 (Slowloris 공격 방어)
    client_body_timeout 10s;
    client_header_timeout 10s;
    send_timeout 10s;

    server {
        # IP 차단
        deny 192.168.1.100;
        allow all;

        # 관리자 경로는 내부 IP만
        location /admin/ {
            allow 10.0.0.0/8;
            deny all;
            proxy_pass http://backend;
        }

        # 숨겨진 파일 접근 차단
        location ~ /\. {
            deny all;
        }
    }
}

무중단 재로드

# 설정 파일 문법 검사
nginx -t

# 설정 재로드 (무중단: 기존 연결 유지하며 새 설정 적용)
nginx -s reload
# 또는
systemctl reload nginx

# 완전 재시작 (연결 끊김)
nginx -s stop && nginx
reload 동작 원리:
1. Master가 새 설정 파일 읽기
2. 새 Worker 프로세스 생성 (새 설정 적용)
3. 기존 Worker에게 "새 연결 받지 말라" 신호
4. 기존 Worker: 현재 처리 중인 요청 완료 후 종료
5. 결과: 다운타임 0, 기존 요청 정상 완료

극한 시나리오

시나리오 1: 대규모 트래픽 (C10K 이상)

# 고트래픽 튜닝
worker_processes auto;
worker_rlimit_nofile 200000;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # TCP 최적화
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Keep-Alive 최적화
    keepalive_timeout 30s;
    keepalive_requests 1000;

    # 업스트림 Keep-Alive
    upstream backend {
        server 10.0.0.1:8080;
        keepalive 100;    # 업스트림 연결 풀 크기
    }
}
최대 동시 연결 = worker_processes × worker_connections
= 8 × 4096 = 32,768개

시나리오 2: 뒷단 서버 장애 시 페일오버

upstream backend {
    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 backup;  # 1, 2 모두 장애 시 활성화
}

server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout http_500 http_502 http_503;
        proxy_next_upstream_tries 3;    # 최대 3개 서버 시도
        proxy_next_upstream_timeout 10s;
    }
}

시나리오 3: WebSocket 프록시

server {
    location /ws/ {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;

        # WebSocket 업그레이드 헤더 전달
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # WebSocket은 장시간 연결 유지
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

로그 분석

# 상위 IP별 요청 수
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

# 상태 코드별 집계
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# 느린 요청 찾기 (응답 시간 포함한 로그 형식 필요)
awk '$NF > 1.0' /var/log/nginx/access.log | head -20

# 실시간 모니터링
tail -f /var/log/nginx/access.log | awk '{print $7}' | head -100
# 응답 시간 포함 로그 형식
log_format detailed '$remote_addr - [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '$request_time $upstream_response_time '
                    '"$http_user_agent"';
# $request_time: 전체 처리 시간
# $upstream_response_time: 뒷단 서버 응답 시간