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_timeout | 65s | server/location |
| Nginx proxy_send_timeout | 30s | server/location |
| Spring server.tomcat.connection-timeout | 70s 이상 | application.yml |
| Spring servlet.async.request-timeout(비동기) | 90s | application.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 테스트를 꼭 돌리세요.
'서버 인프라 실무' 카테고리의 다른 글
| SSH 포트 포워딩·리버스 터널 완전 정복: 개발/운영 실전 보안 가이드(OpenSSH) (0) | 2025.10.26 |
|---|---|
| SSH 보안 단단하게: Fail2ban + UFW로 무차별 대입 차단(필터/액션/알림/점검 루틴) (0) | 2025.10.25 |
| 로그 보존 자동화: logrotate + systemd-journald로 용량·보존·압축 완전 정복(Ubuntu 24.04) (0) | 2025.10.24 |
| 백업은 존재할 때만 복구된다: rsync/cron으로 무중단 증분 백업(로컬·원격·보안·검증 루틴) (0) | 2025.10.23 |
| 시스템 모니터링 첫걸음: top/htop/journalctl/ss로 원인 추적(근거 있는 장애 대응 루틴) (0) | 2025.10.23 |