본문 바로가기
서버 인프라 실무

Nginx 리버스 프록시 + Spring Boot 실전 운영: CORS, 타임아웃, 업로드 제한, 보안 헤더 한 번에 정리

by yamoojin83 2025. 10. 24.

Nginx 리버스 프록시 + Spring Boot 실전 운영: CORS, 타임아웃, 업로드 제한, 보안 헤더 한 번에 정리

실서비스에서 Nginx(프록시)Spring Boot(백엔드)를 조합할 때, 자잘한 설정 불일치가 499/504, CORS, 업로드 실패 같은 장애로 이어지기 쉽습니다. 이 글은 Ubuntu 24.04 기준으로 프록시-백엔드 타임아웃 정합, CORS, 업로드 제한, 보안 헤더를 한 번에 잡는 운영 템플릿과 점검 루틴을 제공합니다.


1) 베이스 아키텍처

  • Nginx: 80/443 수신, 정적 파일 서빙, 백엔드로 리버스 프록시
  • Spring Boot: 내장 톰캣(기본 8080), REST API 제공
  • 권장: HTTP → HTTPS 301, 대표 호스트 통일(비WWW 또는 WWW), HSTS는 검증 후 적용
# Nginx 버전/상태
nginx -v
sudo nginx -t

2) 공통 서버 블록(HTTPS 기준 템플릿)

Nginx에서 프록시 기본기를 먼저 맞춥니다. (인증서는 Certbot 자동설정 또는 수동 지정)

# /etc/nginx/sites-available/app.example.com
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name app.example.com;

  # (필수) 인증서 경로
  ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

  # (권장) 보안 헤더 - 아래 6장에서 상세 설명
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;

  # 업로드 용량 제한(백엔드와 정합)
  client_max_body_size 20m;

  # 공통 프록시 파이프라인
  location / {
    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_http_version 1.1;
    proxy_set_header   Connection "";

    # 타임아웃(프록시 기준값) - 4장에서 정합 설명
    proxy_read_timeout  65s;
    proxy_send_timeout  30s;
    proxy_connect_timeout 5s;
  }
}

HTTP → HTTPS 리다이렉트:

server {
  listen 80;
  listen [::]:80;
  server_name app.example.com;
  return 301 https://app.example.com$request_uri;
}

3) CORS: 프록시가 아닌 백엔드에서 통제(권장)

CORS는 응답 헤더의 약속입니다. 정석은 Spring Security/Handler에서 출처·메서드·헤더를 선언하고, 프록시는 건드리지 않는 것입니다.

3-1) Spring Boot(Java) 전역 CORS 설정

// @Configuration
public class CorsConfig {
  @Bean
  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://www.example.com", "https://app.example.com")
            .allowedMethods("GET","POST","PUT","PATCH","DELETE")
            .allowedHeaders("*")
            .exposedHeaders("Location","Link")
            .allowCredentials(true)
            .maxAge(3600);
      }
    };
  }
}

3-2) Spring Security 6(필터 체인)에서 CORS 활성

// @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  http
    .cors(Customizer.withDefaults())
    .csrf(csrf -> csrf.disable()); // API는 보통 토큰 기반. 폼 로그인은 상황별 조정
  return http.build();
}

주의: allowedOrigins("*") + allowCredentials(true)는 스펙 위반입니다. 자격 증명(쿠키/Authorization) 사용 시 반드시 정확한 출처만 화이트리스트로 지정하세요.

3-3) 프리플라이트(OPTIONS) 확인

# 브라우저 콘솔이 번거로우면 curl로 확인
curl -i -X OPTIONS https://app.example.com/api/hello \
  -H "Origin: https://www.example.com" \
  -H "Access-Control-Request-Method: POST"

4) 타임아웃 정합: Nginx ↔ Spring Boot

타임아웃 불일치가 499(Client Closed)/504(Gateway Timeout)로 이어집니다. 프록시(앞단) ≤ 백엔드(뒷단) 순으로 여유를 둡니다.

구성권장설정 위치
Nginx proxy_read_timeout65sserver/location
Nginx proxy_send_timeout30sserver/location
Spring server.tomcat.connection-timeout70s 이상application.yml
Spring servlet.async.request-timeout(비동기)90sapplication.yml
# application.yml 예시
server:
  tomcat:
    connection-timeout: 70s
spring:
  mvc:
    async:
      request-timeout: 90s

테스트: 의도적으로 60초 걸리는 API를 호출해 499/504 없이 응답이 오는지 점검하세요.


5) 업로드 제한: 프록시와 백엔드 값 맞추기

Nginx의 client_max_body_size와 Spring의 spring.servlet.multipart.max-file-size/max-request-size를 일치시켜야 합니다.

# Nginx
client_max_body_size 20m;
# Spring (application.yml)
spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 22MB   # 파일 외 필드/경계 포함 여유

프록시가 더 작으면 413(Request Entity Too Large), 백엔드가 더 작으면 500/에러 핸들러 진입. 둘 모두 로그 위치를 파악해두세요.


6) 보안 헤더 세트(스니펫로 재사용)

기본적인 클릭재킹/스니핑/리퍼러 최소화를 적용합니다. CSP는 서비스 특성에 맞춰 별도 설계 권장.

# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# (선택) HSTS - HTTPS 검증 후에만
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# server 블록에서 include
include /etc/nginx/snippets/security-headers.conf;

7) 정적 파일/캐시/압축

정적 자원은 프록시에서 캐시·압축으로 이득을 봅니다.

location /assets/ {
  root /var/www/app;          # 또는 별도 정적 컨테이너
  expires 7d;
  add_header Cache-Control "public, max-age=604800";
}

# gzip (HTTP/1.1)
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;

HTTP/2면 헤더 압축이 적용되지만 콘텐츠 gzip은 여전히 유효합니다.


8) 499/504/502 진단 루틴

  • 499 (Client Closed Request): 클라이언트가 먼저 연결 종료. 프론트 JS 타임아웃/네트워크, 혹은 너무 긴 서버 처리.
  • 504 (Gateway Timeout): 프록시가 백엔드 응답을 못 받음. proxy_read_timeout < 실제 처리시간.
  • 502 (Bad Gateway): 백엔드 다운/리스닝 안 함/프록시 대상 주소 오류.
# Nginx 오류 로그
sudo tail -f /var/log/nginx/error.log

# 백엔드 로그(journald 사용 시)
journalctl -u myapp.service -f

# 열려 있는 포트/프로세스
ss -lntup | grep -E ':80|:443|:8080'

9) 예시: API 경로별 미세 조정

서버 이벤트 스트림(SSE)이나 대용량 업로드 엔드포인트는 개별 location에서 별도 타임아웃/버퍼를 줍니다.

# SSE는 긴 연결 유지
location /api/stream {
  proxy_pass http://127.0.0.1:8080;
  proxy_read_timeout  1h;
  proxy_send_timeout  1h;
  proxy_buffering off;      # 스트리밍 중간 버퍼 끔
}

# 대용량 업로드(최대 200MB 예시)
location /api/upload {
  client_max_body_size 200m;
  proxy_pass http://127.0.0.1:8080;
}

10) Spring Boot 측 필수 설정 요약

# application.yml (요약)
server:
  tomcat:
    max-http-form-post-size: 25MB  # 폼 기반 업로드 시
    connection-timeout: 70s
spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 22MB
  mvc:
    async:
      request-timeout: 90s

에러 응답 포맷(문제 상세, RFC7807 스타일)을统一하면 프록시/클라이언트에서 진단이 쉬워집니다.


11) 배포 전 점검 체크리스트

  • Nginx nginx -t 통과, systemctl reload nginx 적용
  • HTTP→HTTPS 301, 대표 호스트 통일
  • CORS: Spring Security에서 출처·메서드·헤더 정확히 지정, 프리플라이트 200
  • 타임아웃: Nginx ≤ Spring 정합, 장시간 API 테스트
  • 업로드: client_max_body_size = max-file-size 정합, 413/500 미발생
  • 보안 헤더 활성, (선택) HSTS 검증 후 적용

12) 운영 팁 & FAQ

Q1. CORS를 Nginx에서 처리해도 되나요?
A. 정적 파일만 제공한다면 가능하지만, 인증/쿠키/동적 도메인 등 복잡성이 커지면 백엔드에서 일관되게 처리하는 것이 안전합니다.

Q2. 499가 간헐적으로 많아요.
A. 프론트 타임아웃, 모바일 네트워크, 긴 처리시간이 주원인입니다. 서버 측은 타임아웃을 과하게 늘리기보다 작업 분할/큐잉/비동기 전환을 우선 검토하세요.

Q3. 업로드가 가끔 실패합니다.
A. Nginx와 Spring의 제한값 불일치, 프록시 버퍼/타임아웃이 원인인 경우가 많습니다. 전송 크기/시간을 모두 점검하세요.


13) 마무리

위 템플릿을 적용하면 CORS·타임아웃·업로드·보안 헤더에서 흔한 장애를 초기에 차단할 수 있습니다. 환경에 맞게 수치만 조정해도 대부분의 문제를 예방할 수 있으니, 배포 전 프리플라이트/업로드/장시간 API 테스트를 꼭 돌리세요.