Nginx
웹 서비스 앞에는 항상 관문이 필요하다. 수만 개의 동시 접속을 처리하고, 뒷단 서버들에 트래픽을 나눠주고, 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: 뒷단 서버 응답 시간