Nginx 도입 2편: 적용

2025. 11. 9. 23:36·프로젝트/웹 성능 테스트

이 글은 1편의 이론을 바탕으로 프로덕션에서 곧바로 쓸 수 있는 Nginx 설정 템플릿과 설계 의도를 정리합니다.

단순히 “되는 설정”이 아니라, 왜 이렇게 설정하는지를 각 블록마다 설명합니다. 문서 끝에는 점검용 커맨드와 운영 팁을 덧붙였습니다.

 


 

디렉터리 구성

/etc/nginx/
 ├─ nginx.conf               # 글로벌 설정
 ├─ conf.d/
 │   ├─ 00-security.conf     # 공통 보안/압축/캐시/업스트림 헬퍼
 │   ├─ api.conf             # API + SSE 프록시
 │   └─ static.conf          # 정적 파일(선택)
 └─ snippets/
     ├─ ssl.conf             # TLS/HTTP2/HSTS 공통
     └─ proxy-headers.conf   # 공통 프록시 헤더

 

Docker Compose를 쓴다면 로컬에는 ./nginx/ 아래 동일한 구조로 두고 볼륨 마운트로 컨테이너 안 /etc/nginx/에 반영합니다.

 


 

1. nginx.conf — 코어/워커/로그 기본값

user  nginx;
worker_processes auto;    # CPU 코어 수에 맞추어 자동
pid /var/run/nginx.pid;

events {
  worker_connections  4096;  # 동시 접속에 여유
}

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

  # 성능 관련 기본값
  sendfile       on;
  tcp_nopush     on;
  tcp_nodelay    on;
  keepalive_timeout  65;

  # 접근/에러 로그
  log_format  main  '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct=$upstream_connect_time '
                    'urt=$upstream_response_time uaddr=$upstream_addr';
  access_log  /var/log/nginx/access.log  main;
  error_log   /var/log/nginx/error.log   warn;

  # 공통 보안/압축/캐시/업스트림 헬퍼
  include /etc/nginx/conf.d/*.conf;
}

 

왜 이렇게 했는가

  • worker_processes auto 는 코어 수에 맞춰 병렬 처리량을 극대화합니다.
  • log_format 에 upstream 지표(연결/응답 시간)를 포함해 Spring/DB 병목을 바로 파악합니다.
  • keepalive_timeout 65 는 ALB/프록시와의 idle 타임아웃과 충돌을 줄입니다.

 


 

2. security.conf — 공통 보안/압축/캐시

 
# 공통 보안 헤더(필요에 따라 강화)
add_header X-Content-Type-Options  nosniff always;
add_header X-Frame-Options         DENY    always;
add_header Referrer-Policy         no-referrer-when-downgrade always;
add_header X-XSS-Protection        "0"     always;

# HSTS(HTTPS만 운용 시에만 활성화)
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# 압축: 텍스트 자원만
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript application/xml text/xml;

# 파일 업로드 한도(대용량 업로드 시)
client_max_body_size 20m;

# 업스트림 공통 헤더
map $http_x_forwarded_proto $real_proto {
  default $scheme;
  https   https;
}

# 프록시 공통 헤더 스니펫
# (snippets/proxy-headers.conf 로 분리해도 좋습니다)
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   $real_proto;
proxy_http_version 1.1;
proxy_read_timeout  75s;
proxy_send_timeout  75s;

왜 이렇게 했는가

  • 보안 헤더는 애플리케이션 이전 단계에서 최소한의 방어막을 제공합니다.
  • gzip 은 텍스트 응답만 압축하여 CPU/지연의 밸런스를 맞춥니다.
  • client_max_body_size 는 파일 업로드 실패를 예방합니다.
  • X-Forwarded-* 헤더는 애플리케이션에서 정확한 클라이언트 정보를 알게 해 줍니다.

 


 

3. ssl.conf — TLS/HTTP2/HSTS 스니펫

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

# 인증서 경로(실서버 도메인 반영)
ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# HTTP/2 권장
# server {} 블록 listen에 'http2' 추가

왜 이렇게 했는가

  • TLS 1.2/1.3만 허용해 구식 암호화 스위트를 차단합니다.
  • 인증서 경로는 Certbot이 갱신하는 위치로 고정합니다.

 


 

4. HTTP(80): ACME 챌린지 + HTTPS 리디렉션

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

  # Certbot ACME 인증 파일 제공
  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
  }

  # 나머지는 HTTPS로 리디렉션
  location / {
    return 301 https://$host$request_uri;
  }
}

왜 이렇게 했는가

  • 80 포트는 도메인 소유 검증(ACME) 과 HTTPS 리디렉션 전용으로 심플하게 둡니다.

 


 

5. HTTPS(443): API + SSE 프록시(핵심)

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

  include /etc/nginx/snippets/ssl.conf;
  # 공통 보안/압축/프록시 설정
  include /etc/nginx/conf.d/00-security.conf;

  # ---- 일반 API ----
  location / {
    proxy_pass http://127.0.0.1:8080;     # Spring Boot
    # 공통 헤더/타임아웃은 00-security.conf에서 지정
    # 캐시가 필요 없는 API라면 no-store 권장
    add_header Cache-Control "no-store" always;
  }

  # ---- SSE 엔드포인트 ----
  # 예: GET /api/tests/{id}/events
  location ~* ^/api/tests/.*/events$ {
    proxy_pass http://127.0.0.1:8080;
    proxy_buffering off;          # 버퍼링 비활성화 → 즉시 전송
    proxy_cache     off;          # 캐시 금지
    proxy_read_timeout 3600s;     # 장기 연결 유지
    add_header X-Accel-Buffering "no";  # 일부 리버스 프록시에서 버퍼링 방지
    # 필요 시 CORS 노출
    # add_header Access-Control-Allow-Origin *;
  }

  # ---- 헬스 체크/액추에이터(선택) ----
  location /actuator/health {
    proxy_pass http://127.0.0.1:8080;
    add_header Cache-Control "no-store" always;
  }

  # ---- 정적 파일(선택, 프론트 빌드 산출물) ----
  # location /app/ {
  #   alias /var/www/html/;
  #   try_files $uri $uri/ /app/index.html;
  # }
}

왜 이렇게 했는가

  • SSE는 ‘지연 없이’ 흘려야 하므로 proxy_buffering off + X-Accel-Buffering: no 를 같이 둡니다.
  • 장시간 스트림을 고려해 proxy_read_timeout 을 충분히 크게 둡니다(예: 30~60분).
  • API는 Cache-Control: no-store 로 브라우저/중간 캐시를 차단합니다(민감 응답).

 


 

6. 개발용(HTTP only): Docker Compose에 맞는 최소 예시

 
server {
  listen 80;
  server_name localhost;

  # 일반 API 프록시
  location / {
    proxy_pass http://app:8080;    # docker-compose 서비스명
    include /etc/nginx/conf.d/00-security.conf;
    add_header Cache-Control "no-store" always;
  }

  # SSE 전용: 즉시 전송 + 장기 연결
  location ~* ^/api/tests/.*/events$ {
    proxy_pass http://app:8080;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    add_header X-Accel-Buffering "no";
  }
}

왜 이렇게 했는가

  • 로컬에서는 HTTPS(443) 없이 빠르게 개발/디버깅,
    운영만 443 + 인증서로 보안/성능을 확보합니다.

 


 

7.  설계 의도 정리

  1. TLS 종료를 Nginx에서
    앱은 평문 HTTP로 빠르게 비즈니스 로직에 집중합니다. 인증서/암복호화 관리는 게이트웨이 한 곳에 모읍니다.
  2. 공통 보안/압축/프록시 설정의 분리
    00-security.conf 같은 공통 스니펫 분리는 설정 누락·불일치 사고를 줄입니다.
  3. SSE 버퍼링 완전 차단
    proxy_buffering off 와 X-Accel-Buffering: no 를 함께 넣어 즉시 전송을 보장합니다. 중간 버퍼링으로 TTFB가 지연되면 UX가 급격히 나빠집니다.
  4. 장기 연결 타임아웃 상향
    proxy_read_timeout 을 넉넉히 잡아 ALB/Nginx idle과 충돌을 줄입니다. 동시에 서버 측 주기 핑(예: 20초) 을 보내면 더 안정적입니다.
  5. 로그에 upstream 타이밍 포함
    애플리케이션/DB 지연과 네트워크 지연을 구분할 수 있어 장애 분석 시간이 단축됩니다.
  6. Cache-Control 최소화 전략
    민감 데이터(API)는 no-store 로 캐싱을 막고, 정적 파일(해시 빌드 산출물)은 immutable 로 강하게 캐싱하는 “양극화 전략”이 운영 비용을 줄입니다.
  7. 업스트림 keepalive
    백엔드 서버 수가 늘수록 커넥션 생성 비용이 커집니다. keepalive는 이를 상쇄해 지연/CPU를 절감합니다.
  8. 구성 파일 역할 분리
    운영에서 변경이 잦은 것(도메인, 인증서 경로, 라우팅)과 드문 것(코어/압축 정책)을 분리하면 릴리즈 리스크가 줄고 코드 리뷰 난이도가 낮아집니다.

 

흔한 문제와 빠른 체크

  • SSE가 중간에 끊긴다 → proxy_buffering off, proxy_read_timeout 확인. 브라우저/프록시 idle도 확인.
  • 클라이언트 IP가 모두 127.0.0.1로 찍힌다 → X-Real-IP, X-Forwarded-For 헤더 전달과 애플리케이션의 추출 로직 확인.
  • 대용량 업로드가 실패한다 → client_max_body_size 조정.
  • 인증서 만료 → certbot renew cron/systemd timer 설정, 만료 알림 모니터링.

 

운영 체크리스트

  • 80 → ACME + HTTPS 리디렉션만
  • 443 → ssl, http2 + 보안 헤더 + 압축
  • API는 no-store, 정적 자원은 immutable
  • SSE는 proxy_buffering off + X-Accel-Buffering: no + 큰 proxy_read_timeout
  • upstream keepalive, 실패 감지 값 조정
  • 액세스 로그에 upstream 타이밍 포함
  • nginx -t → nginx -s reload 플로우 준수
  • 인증서 자동 갱신 및 만료 모니터링

 


 

마무리

이 구성은 Spring Boot + Nginx 게이트웨이에서 HTTPS, 리버스 프록시, SSE 스트리밍을 안정적으로 운영하기 위한 현실적인 기본 템플릿입니다. 핵심은 “즉시 전송이 필요한 경로(SSE) 와 그 외 경로(API/정적) 를 명확히 분리하고, 공통 정책은 스니펫으로 일관되게 적용”하는 것입니다.
이대로 시작한 뒤, 서비스의 트래픽 패턴과 모니터링 지표에 맞춰 timeouts/버퍼/캐시/로그를 점진 튜닝하면 됩니다.

'프로젝트 > 웹 성능 테스트' 카테고리의 다른 글

롱폴링 비동기 완료  (0) 2025.11.27
비동기 통신 방식 비교와 A/B 테스트 설계  (0) 2025.11.09
Nginx 도입 1편: 왜 Nginx  (0) 2025.11.06
Server Sent Events(SSE): 구현  (0) 2025.11.02
Server Sent Events(SSE): 설계  (0) 2025.11.02
'프로젝트/웹 성능 테스트' 카테고리의 다른 글
  • 롱폴링 비동기 완료
  • 비동기 통신 방식 비교와 A/B 테스트 설계
  • Nginx 도입 1편: 왜 Nginx
  • Server Sent Events(SSE): 구현
yoon4360
yoon4360
자바 백엔드 개발자 지망생입니다
  • yoon4360
    yoon4360님의 블로그
    yoon4360
  • 전체
    오늘
    어제
    • 분류 전체보기 (137)
      • 스프링 (17)
      • 프로젝트 (48)
        • 악취 포집기 앱 (4)
        • 기업 일정 관리 웹 (10)
        • 기술 면접 복습 플랫폼 (18)
        • 웹 성능 테스트 (16)
      • CS (9)
      • 자바 (14)
      • 독서 (1)
      • SQL (1)
      • SSAFY (14)
      • 알고리즘 (15)
      • 기술면접 (8)
      • 데이터베이스 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
yoon4360
Nginx 도입 2편: 적용
상단으로

티스토리툴바