이미지 로딩 중...
AI Generated
2025. 11. 14. · 4 Views
리버스 프록시 완벽 가이드
실무에서 자주 사용되는 리버스 프록시의 개념부터 Nginx를 활용한 구체적인 설정 방법까지 초급 개발자를 위해 쉽고 상세하게 설명합니다. 보안, 로드밸런싱, SSL 등 실전 활용법을 포함합니다.
목차
- 리버스 프록시 기본 개념
- 로드밸런싱 설정
- SSL/TLS 종료 설정
- 캐싱 설정
- API 라우팅과 URL 재작성
- Rate Limiting 요청 속도 제한
- 보안 헤더 설정
- Health Check와 상태 모니터링
- 정적 파일 서빙 최적화
- WebSocket 프록시 설정
1. 리버스 프록시 기본 개념
시작하며
여러분이 웹 애플리케이션을 배포할 때 이런 고민을 해본 적 있나요? "사용자가 직접 백엔드 서버에 접근하면 보안상 위험하지 않을까?", "여러 개의 서버를 운영하는데 어떻게 트래픽을 분산시키지?" 이런 문제는 실제 개발 현장에서 매우 자주 발생합니다.
특히 서비스가 성장하면서 단일 서버로는 모든 요청을 처리하기 어려워지고, 보안 문제도 점점 더 중요해집니다. 또한 백엔드 서버의 IP 주소를 직접 노출하면 DDoS 공격이나 해킹의 위험에 쉽게 노출될 수 있습니다.
바로 이럴 때 필요한 것이 리버스 프록시입니다. 리버스 프록시는 클라이언트와 서버 사이에서 중개자 역할을 하며, 외부의 모든 요청을 받아서 내부 서버로 전달하고, 서버의 응답을 다시 클라이언트에게 돌려줍니다.
개요
간단히 말해서, 리버스 프록시는 클라이언트 앞이 아닌 서버 앞에 위치하는 프록시 서버입니다. 일반 프록시(포워드 프록시)가 클라이언트를 대신해서 요청을 보내는 것과 달리, 리버스 프록시는 서버를 대신해서 요청을 받습니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 보안을 크게 향상시킬 수 있고, 둘째로 여러 서버에 트래픽을 분산시킬 수 있으며, 셋째로 SSL 인증서 관리를 한 곳에서 할 수 있습니다. 예를 들어, Node.js 애플리케이션 서버 3대를 운영하는 경우, Nginx 리버스 프록시 하나로 모든 트래픽을 받아서 세 서버에 균등하게 분배할 수 있습니다.
기존에는 클라이언트가 직접 애플리케이션 서버의 IP와 포트(예: 192.168.1.100:3000)에 접근해야 했다면, 이제는 리버스 프록시 하나의 주소(예: api.example.com)로 접근하면 프록시가 알아서 적절한 백엔드 서버로 요청을 전달합니다. 리버스 프록시의 핵심 특징은 크게 세 가지입니다.
첫째, 백엔드 서버를 외부로부터 숨겨서 보안을 강화합니다. 둘째, 캐싱 기능을 통해 성능을 향상시킵니다.
셋째, 로드밸런싱으로 서버의 부하를 분산시킵니다. 이러한 특징들이 중요한 이유는 현대의 웹 서비스가 높은 트래픽과 보안 위협에 동시에 대응해야 하기 때문입니다.
코드 예제
# Nginx 기본 리버스 프록시 설정
server {
listen 80;
server_name api.example.com;
location / {
# 백엔드 서버로 요청 전달
proxy_pass http://localhost:3000;
# 클라이언트의 실제 IP 주소 전달
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 호스트 정보 전달
proxy_set_header Host $host;
}
}
설명
이것이 하는 일: 위 설정은 Nginx를 리버스 프록시로 구성하여, api.example.com으로 들어오는 모든 HTTP 요청을 localhost:3000에서 실행 중인 백엔드 애플리케이션으로 전달합니다. 첫 번째로, listen 80과 server_name 지시어는 Nginx가 80번 포트에서 api.example.com 도메인에 대한 요청을 수신하도록 설정합니다.
이렇게 하면 사용자는 80번 포트(기본 HTTP 포트)로 접근하지만, 실제 애플리케이션은 3000번 포트에서 실행될 수 있습니다. 외부에서는 3000번 포트를 알 필요도 없고 접근할 수도 없어 보안이 향상됩니다.
그 다음으로, proxy_pass 지시어가 실행되면서 실제 요청을 백엔드 서버로 전달합니다. proxy_set_header 지시어들은 매우 중요한데, 이들은 원본 요청의 메타데이터를 백엔드 서버에 전달합니다.
X-Real-IP는 클라이언트의 실제 IP 주소를, X-Forwarded-For는 프록시 체인을 통과한 모든 IP를 기록합니다. 마지막으로, Host 헤더를 설정하여 백엔드 서버가 어떤 도메인으로 요청이 들어왔는지 알 수 있게 합니다.
최종적으로 백엔드 서버의 응답이 다시 Nginx를 거쳐 클라이언트에게 전달됩니다. 이 모든 과정은 클라이언트 입장에서는 투명하게 진행되어, 마치 직접 백엔드 서버와 통신하는 것처럼 느껴집니다.
여러분이 이 설정을 사용하면 백엔드 서버를 외부로부터 완전히 숨길 수 있고, 서버 IP나 포트를 변경해도 클라이언트는 영향을 받지 않으며, 나중에 로드밸런싱이나 SSL 같은 고급 기능을 쉽게 추가할 수 있습니다.
실전 팁
💡 proxy_set_header 설정을 반드시 포함하세요. 이 설정 없이는 백엔드 서버가 클라이언트의 실제 IP를 알 수 없어 로깅이나 보안 정책이 제대로 작동하지 않습니다.
💡 개발 환경에서는 proxy_pass http://localhost:3000으로 시작하되, 프로덕션에서는 내부 네트워크의 사설 IP를 사용하는 것이 더 안전합니다.
💡 Nginx 설정을 변경한 후에는 반드시 nginx -t로 문법을 검사하고 nginx -s reload로 재시작하세요. 잘못된 설정은 전체 서비스를 다운시킬 수 있습니다.
💡 로그 파일(/var/log/nginx/access.log, /var/log/nginx/error.log)을 주기적으로 확인하여 프록시가 제대로 작동하는지 모니터링하세요.
2. 로드밸런싱 설정
시작하며
여러분의 서비스에 사용자가 점점 많아지면서 단일 서버로는 모든 요청을 처리하기 어려워진 적 있나요? 특정 시간대에 트래픽이 몰려서 서버가 느려지거나, 심지어 다운되는 상황을 경험해본 적이 있을 것입니다.
이런 문제는 성장하는 모든 서비스가 겪는 자연스러운 과정입니다. 하나의 서버는 물리적인 한계가 있고, CPU와 메모리는 한정되어 있습니다.
트래픽이 증가하면 응답 시간이 길어지고, 최악의 경우 서버가 과부하로 죽어버릴 수 있습니다. 바로 이럴 때 필요한 것이 로드밸런싱입니다.
여러 개의 백엔드 서버를 준비하고, Nginx가 들어오는 요청을 이 서버들에게 고르게 분배함으로써 각 서버의 부담을 줄이고 전체 처리 능력을 늘릴 수 있습니다.
개요
간단히 말해서, 로드밸런싱은 여러 서버에 작업을 균등하게 분배하는 기술입니다. Nginx는 다양한 로드밸런싱 알고리즘을 제공하여 상황에 맞게 트래픽을 분산시킬 수 있습니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 서비스의 가용성을 높일 수 있습니다. 한 서버가 다운되어도 다른 서버가 계속 요청을 처리할 수 있습니다.
둘째로 수평적 확장(Scale Out)이 가능해집니다. 트래픽이 증가하면 서버를 추가하기만 하면 됩니다.
예를 들어, 온라인 쇼핑몰에서 블랙프라이데이 같은 대규모 이벤트 시즌에 일시적으로 서버를 늘렸다가 이벤트가 끝나면 다시 줄일 수 있습니다. 기존에는 서버 성능을 높이기 위해 더 좋은 CPU와 메모리를 장착하는 수직적 확장(Scale Up)에 의존했다면, 이제는 저렴한 여러 대의 서버를 추가하는 것으로 같은 효과를 낼 수 있습니다.
비용도 절감되고 유연성도 높아집니다. 로드밸런싱의 핵심 특징은 세 가지입니다.
첫째, Round Robin, Least Connections, IP Hash 등 다양한 분배 알고리즘을 선택할 수 있습니다. 둘째, Health Check 기능으로 죽은 서버를 자동으로 제외합니다.
셋째, 가중치를 설정하여 서버 성능에 따라 다르게 트래픽을 분배할 수 있습니다. 이러한 특징들이 중요한 이유는 실제 운영 환경에서는 모든 서버가 항상 동일한 성능과 상태를 유지하지 않기 때문입니다.
코드 예제
# Nginx 로드밸런싱 설정
upstream backend_servers {
# 서버별 가중치 설정 (weight)
server localhost:3000 weight=3;
server localhost:3001 weight=2;
server localhost:3002 weight=1;
# 연결이 가장 적은 서버로 우선 분배
least_conn;
# 서버 상태 체크 (max_fails: 최대 실패 횟수, fail_timeout: 타임아웃)
server localhost:3003 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name api.example.com;
location / {
# upstream 그룹으로 요청 전달
proxy_pass http://backend_servers;
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_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
}
설명
이것이 하는 일: 위 설정은 4대의 백엔드 서버를 하나의 upstream 그룹으로 묶고, 들어오는 요청을 연결 수가 가장 적은 서버로 우선 분배하며, 각 서버의 성능에 따라 가중치를 다르게 적용합니다. 첫 번째로, upstream 블록은 backend_servers라는 이름의 서버 그룹을 정의합니다.
여기에 4대의 서버가 등록되어 있고, 각각 다른 포트에서 실행되는 애플리케이션입니다. weight 매개변수는 각 서버가 받을 트래픽의 비율을 결정하는데, 3000번 포트의 서버는 가장 성능이 좋아서 가중치 3을, 3002번 포트의 서버는 성능이 낮아서 가중치 1을 받았습니다.
이렇게 하면 트래픽이 3:2:1 비율로 분배됩니다. 그 다음으로, least_conn 지시어가 로드밸런싱 알고리즘을 결정합니다.
이 알고리즘은 현재 활성 연결이 가장 적은 서버로 새 요청을 보냅니다. 기본 Round Robin 방식보다 더 효율적인데, 요청마다 처리 시간이 다를 수 있기 때문입니다.
예를 들어, 어떤 요청은 0.1초만에 끝나지만 다른 요청은 5초가 걸릴 수 있습니다. least_conn은 이런 상황에서 더 공평하게 부하를 분산시킵니다.
마지막으로, max_fails와 fail_timeout 설정은 자동 장애 조치(failover)를 구현합니다. 3003번 포트의 서버가 30초 내에 3번 연속 실패하면, Nginx는 자동으로 이 서버를 일시적으로 제외하고 다른 서버로만 요청을 보냅니다.
30초 후에 다시 시도해서 서버가 복구되었는지 확인합니다. proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout은 각각 연결, 전송, 읽기 단계의 타임아웃을 설정하여 느린 서버로 인한 전체 시스템 지연을 방지합니다.
여러분이 이 설정을 사용하면 단일 서버 대비 3-4배의 처리 능력을 얻을 수 있고, 한 서버가 다운되어도 서비스가 중단되지 않으며, 서버를 추가하거나 제거할 때 다운타임 없이 유연하게 대응할 수 있습니다.
실전 팁
💡 로드밸런싱 알고리즘은 서비스 특성에 따라 선택하세요. API 서버처럼 요청 처리 시간이 일정하면 기본 Round Robin도 충분하지만, 이미지 처리나 복잡한 계산이 포함되면 least_conn이 더 효과적입니다.
💡 max_fails를 너무 작게 설정하면 일시적인 네트워크 문제로도 서버가 제외될 수 있습니다. 프로덕션에서는 3-5 정도가 적절합니다.
💡 서버를 추가하거나 제거할 때는 nginx -s reload를 사용하세요. 이 명령은 기존 연결을 유지하면서 새 설정을 적용하므로 다운타임이 없습니다.
💡 각 백엔드 서버의 응답 시간과 에러율을 모니터링 도구(Prometheus, Grafana 등)로 추적하여 어떤 서버가 문제인지 빠르게 파악하세요.
💡 세션 기반 애플리케이션의 경우 ip_hash 알고리즘을 사용하거나 Redis 같은 외부 세션 저장소를 사용하여 세션 유지 문제를 해결하세요.
3. SSL/TLS 종료 설정
시작하며
여러분이 웹 서비스를 운영하면서 "HTTPS를 꼭 써야 한다는데, 모든 백엔드 서버에 인증서를 설치해야 하나?"라는 고민을 해본 적 있나요? 여러 대의 서버를 운영하는 경우, 각 서버마다 SSL 인증서를 관리하는 것은 매우 번거롭고 복잡합니다.
이런 문제는 마이크로서비스 아키텍처나 로드밸런싱 환경에서 특히 심각합니다. 인증서가 만료되면 모든 서버에서 갱신해야 하고, 설정이 조금이라도 다르면 보안 문제가 발생할 수 있습니다.
또한 SSL 암호화/복호화는 CPU를 많이 사용하므로 각 애플리케이션 서버에 부담을 줍니다. 바로 이럴 때 필요한 것이 SSL/TLS 종료(SSL Termination)입니다.
리버스 프록시인 Nginx에서 SSL을 처리하고, 백엔드 서버와는 내부 네트워크에서 평문 HTTP로 통신함으로써 관리를 단순화하고 성능도 향상시킬 수 있습니다.
개요
간단히 말해서, SSL/TLS 종료는 클라이언트와의 암호화된 통신을 프록시 서버에서 복호화하고, 프록시와 백엔드 서버 사이는 암호화하지 않는 방식입니다. 모든 SSL 처리를 한 곳에서 담당하는 것입니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 인증서 관리가 매우 간편해집니다. 하나의 Nginx 서버에서만 인증서를 관리하면 되고, Let's Encrypt 같은 자동 갱신 도구도 한 곳에만 설정하면 됩니다.
둘째로 백엔드 서버의 부담이 줄어듭니다. SSL 암호화/복호화는 CPU 집약적인 작업이므로, 이를 전담 프록시 서버에서 처리하면 애플리케이션 서버는 비즈니스 로직에만 집중할 수 있습니다.
예를 들어, 10대의 Node.js 서버를 운영한다면, Nginx 하나에서만 SSL을 처리하고 나머지는 평문 HTTP로 통신하는 것이 훨씬 효율적입니다. 기존에는 각 애플리케이션 서버에서 HTTPS를 직접 처리해야 했고, 인증서 경로나 암호 스위트 설정을 모든 서버에서 일관되게 유지해야 했다면, 이제는 Nginx 하나의 설정 파일만 관리하면 됩니다.
설정 변경도 한 곳에서만 하면 됩니다. SSL/TLS 종료의 핵심 특징은 세 가지입니다.
첫째, 중앙 집중식 인증서 관리로 운영 복잡도를 크게 낮춥니다. 둘째, 현대적인 암호화 프로토콜과 암호 스위트를 한 곳에서 일관되게 적용할 수 있습니다.
셋째, SSL 처리를 전담함으로써 백엔드 서버의 성능을 향상시킵니다. 이러한 특징들이 중요한 이유는 보안과 성능, 운영 편의성을 모두 고려한 실용적인 아키텍처이기 때문입니다.
코드 예제
# Nginx SSL/TLS 종료 설정
server {
# HTTPS 리스닝
listen 443 ssl http2;
server_name api.example.com;
# SSL 인증서 경로
ssl_certificate /etc/nginx/ssl/certificate.crt;
ssl_certificate_key /etc/nginx/ssl/private.key;
# 강력한 SSL 프로토콜만 허용
ssl_protocols TLSv1.2 TLSv1.3;
# 보안 암호 스위트 설정
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# SSL 세션 캐시 (성능 향상)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
# 백엔드는 평문 HTTP로 통신
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 백엔드에 HTTPS로 접속했음을 알림
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP를 HTTPS로 리디렉션
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
설명
이것이 하는 일: 위 설정은 Nginx가 443번 포트에서 HTTPS 요청을 받아 SSL을 복호화하고, 내부 네트워크에서는 평문 HTTP로 백엔드 서버와 통신하며, 80번 포트로 들어오는 HTTP 요청은 자동으로 HTTPS로 리디렉션합니다. 첫 번째로, listen 443 ssl http2 지시어는 Nginx가 443번 포트(HTTPS 기본 포트)에서 SSL을 활성화하고 HTTP/2 프로토콜도 지원하도록 합니다.
HTTP/2는 멀티플렉싱과 헤더 압축으로 성능을 크게 향상시킵니다. ssl_certificate와 ssl_certificate_key는 실제 인증서 파일의 위치를 지정하는데, Let's Encrypt를 사용한다면 certbot이 자동으로 생성한 경로를 입력합니다.
그 다음으로, ssl_protocols TLSv1.2 TLSv1.3 설정이 중요한 보안 역할을 합니다. 구형 프로토콜인 SSLv3, TLSv1.0, TLSv1.1은 알려진 보안 취약점이 있어 명시적으로 제외합니다.
ssl_ciphers는 어떤 암호화 알고리즘을 사용할지 결정하는데, HIGH 등급의 안전한 암호만 허용하고 NULL 암호화나 약한 MD5 해시는 제외합니다. ssl_prefer_server_ciphers on은 서버의 암호 스위트 우선순위를 따르도록 하여 가장 안전한 암호가 선택되게 합니다.
세 번째로, ssl_session_cache와 ssl_session_timeout은 성능 최적화를 위한 설정입니다. SSL 핸드셰이크는 비용이 많이 드는 작업이므로, 세션을 10분간 캐시에 보관하여 같은 클라이언트의 재연결 시 핸드셰이크를 생략합니다.
10MB의 공유 메모리로 약 4만 개의 세션을 캐시할 수 있습니다. 마지막으로, X-Forwarded-Proto 헤더는 백엔드 애플리케이션에 원래 요청이 HTTPS였음을 알려줍니다.
이는 애플리케이션이 리디렉션 URL을 생성하거나 보안 쿠키를 설정할 때 필요합니다. 별도의 server 블록은 80번 포트의 모든 HTTP 요청을 HTTPS로 301 영구 리디렉션하여, 사용자가 실수로 http://로 접속해도 자동으로 보안 연결로 전환됩니다.
여러분이 이 설정을 사용하면 하나의 인증서로 여러 백엔드 서버를 보호할 수 있고, Let's Encrypt 자동 갱신을 한 곳에서만 설정하면 되며, 최신 보안 표준(TLS 1.3, 강력한 암호 스위트)을 쉽게 적용할 수 있습니다.
실전 팁
💡 Let's Encrypt를 사용하면 무료로 SSL 인증서를 받고 자동 갱신도 설정할 수 있습니다. certbot을 설치하고 certbot --nginx 명령으로 간단하게 설정하세요.
💡 내부 네트워크에서도 보안이 중요하다면 백엔드 서버와도 HTTPS로 통신할 수 있습니다. 이 경우 proxy_pass https://backend_servers로 변경하고 자체 서명 인증서를 사용하세요.
💡 SSL Labs(ssllabs.com/ssltest)에서 무료로 SSL 설정을 테스트하여 A+ 등급을 받을 수 있는지 확인하세요. 보안 취약점을 조기에 발견할 수 있습니다.
💡 ssl_session_cache 크기는 트래픽에 따라 조정하세요. 대규모 서비스는 50m~100m까지 늘려서 더 많은 세션을 캐시할 수 있습니다.
💡 HTTP/2를 활성화하면 페이지 로딩 속도가 크게 향상되므로 반드시 http2 옵션을 추가하세요. 단, 이는 HTTPS에서만 동작합니다.
4. 캐싱 설정
시작하며
여러분의 API 서버가 같은 데이터를 반복적으로 조회하는 요청을 계속 받고 있다면, 백엔드 서버와 데이터베이스에 불필요한 부담을 주고 있는 것입니다. "이 데이터는 자주 바뀌지 않는데, 매번 데이터베이스를 조회해야 하나?"라는 생각을 해본 적 있나요?
이런 문제는 특히 정적 콘텐츠나 변경이 드문 API 응답에서 심각합니다. 예를 들어, 상품 목록이나 카테고리 정보는 하루에 몇 번 정도만 업데이트되는데, 수천 명의 사용자가 매초마다 같은 데이터를 요청합니다.
이는 서버 리소스의 낭비이고, 응답 시간도 불필요하게 느려집니다. 바로 이럴 때 필요한 것이 프록시 캐싱입니다.
Nginx가 백엔드 응답을 메모리나 디스크에 저장해두고, 같은 요청이 오면 백엔드를 거치지 않고 바로 캐시된 응답을 돌려줌으로써 속도를 획기적으로 높이고 서버 부하를 줄일 수 있습니다.
개요
간단히 말해서, 프록시 캐싱은 자주 요청되는 응답을 Nginx에 저장해두고 재사용하는 기술입니다. 백엔드 서버에 실제로 요청을 보내지 않고도 응답을 제공할 수 있습니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 응답 속도가 밀리초 단위로 빨라집니다. 데이터베이스 조회나 복잡한 계산 없이 메모리에서 바로 응답하므로 10-100배 빠를 수 있습니다.
둘째로 백엔드 서버와 데이터베이스의 부하가 크게 줄어듭니다. 셋째로 비용이 절감됩니다.
같은 트래픽을 처리하는 데 필요한 서버 수가 줄어들기 때문입니다. 예를 들어, 뉴스 사이트의 메인 페이지 API는 수백만 번 호출되지만 내용은 10분마다 한 번만 바뀝니다.
이런 경우 캐싱으로 서버 부하를 99% 이상 줄일 수 있습니다. 기존에는 모든 요청이 백엔드 서버를 거쳐야 했고, Redis나 Memcached 같은 별도의 캐시 서버를 구축해야 했다면, 이제는 Nginx에 내장된 캐싱 기능만으로도 충분한 성능 향상을 얻을 수 있습니다.
추가 인프라 없이 설정만으로 가능합니다. 프록시 캐싱의 핵심 특징은 세 가지입니다.
첫째, 캐시 키를 유연하게 설정하여 URL, 쿼리 파라미터, 헤더 등을 기준으로 캐싱할 수 있습니다. 둘째, TTL(Time To Live)을 설정하여 캐시의 유효 기간을 제어합니다.
셋째, 캐시 우회 조건을 설정하여 특정 요청은 항상 백엔드로 전달할 수 있습니다. 이러한 특징들이 중요한 이유는 모든 응답을 무조건 캐싱하면 오래된 데이터를 제공할 수 있으므로, 상황에 맞는 세밀한 제어가 필요하기 때문입니다.
코드 예제
# Nginx 캐싱 설정
# 캐시 영역 정의 (nginx.conf 또는 http 블록에)
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
listen 80;
server_name api.example.com;
location /api/products {
proxy_pass http://localhost:3000;
# 캐시 영역 사용
proxy_cache api_cache;
# 200, 301, 302 응답은 10분간 캐싱
proxy_cache_valid 200 301 302 10m;
# 404 응답은 1분간 캐싱
proxy_cache_valid 404 1m;
# 캐시 키 정의 (URL + 쿼리 스트링 기준)
proxy_cache_key "$scheme$request_method$host$request_uri";
# 캐시 상태를 응답 헤더에 추가 (디버깅용)
add_header X-Cache-Status $upstream_cache_status;
# POST 요청은 캐싱하지 않음
proxy_cache_methods GET HEAD;
# 특정 헤더가 있으면 캐시 우회
proxy_cache_bypass $http_cache_control;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
설명
이것이 하는 일: 위 설정은 /api/products 엔드포인트의 응답을 최대 10분간 캐싱하고, 같은 요청이 오면 백엔드를 거치지 않고 캐시에서 바로 응답하며, 캐시 히트 여부를 헤더로 제공합니다. 첫 번째로, proxy_cache_path 지시어는 캐시 저장소를 설정합니다.
/var/cache/nginx/api 디렉토리에 캐시 파일을 저장하고, levels=1:2는 디렉토리 구조를 2단계로 나누어 파일 시스템 성능을 최적화합니다. keys_zone=api_cache:10m은 캐시 키를 저장할 메모리 영역을 10MB로 할당하는데, 이는 약 8만 개의 캐시 키를 저장할 수 있습니다.
max_size=1g는 전체 캐시 크기를 1GB로 제한하고, inactive=60m은 60분 동안 접근되지 않은 캐시는 자동으로 삭제합니다. 그 다음으로, proxy_cache api_cache가 이 location 블록에서 캐싱을 활성화합니다.
proxy_cache_valid는 HTTP 응답 코드별로 다른 캐시 시간을 설정할 수 있는데, 성공 응답(200)과 리디렉션(301, 302)은 10분간, 404 에러는 1분만 캐싱합니다. 404를 짧게 캐싱하는 이유는 새로운 리소스가 추가되었을 때 빠르게 반영하기 위함입니다.
세 번째로, proxy_cache_key는 어떤 기준으로 캐시를 구분할지 정의합니다. 기본적으로 스킴(http/https), HTTP 메서드(GET/POST), 호스트, URI를 조합하여 고유한 캐시 키를 생성합니다.
예를 들어, /api/products?category=electronics와 /api/products?category=books는 다른 캐시 키를 가지므로 별도로 캐싱됩니다. add_header X-Cache-Status는 응답 헤더에 캐시 상태(HIT, MISS, BYPASS 등)를 추가하여, 개발자가 캐싱이 제대로 작동하는지 쉽게 확인할 수 있게 합니다.
마지막으로, proxy_cache_methods GET HEAD는 GET과 HEAD 요청만 캐싱하고 POST, PUT, DELETE 같은 수정 작업은 캐싱하지 않습니다. 이는 데이터 일관성을 위해 매우 중요합니다.
proxy_cache_bypass $http_cache_control은 클라이언트가 Cache-Control: no-cache 헤더를 보내면 캐시를 우회하고 항상 최신 데이터를 가져오도록 합니다. 여러분이 이 설정을 사용하면 자주 조회되는 데이터의 응답 시간이 수백 밀리초에서 수 밀리초로 줄어들고, 백엔드 서버의 CPU와 데이터베이스 부하가 크게 감소하며, 같은 인프라로 훨씬 많은 트래픽을 처리할 수 있습니다.
실전 팁
💡 개발 중에는 add_header X-Cache-Status 헤더로 캐시가 제대로 동작하는지 항상 확인하세요. HIT가 나와야 캐싱이 작동하는 것입니다.
💡 사용자별로 다른 데이터를 제공하는 API는 캐싱하면 안 됩니다. 인증이 필요한 엔드포인트는 proxy_no_cache $http_authorization으로 캐시를 비활성화하세요.
💡 캐시 무효화(cache purging)가 필요하다면 proxy_cache_purge 모듈을 사용하거나, 버전 번호를 URL에 포함시켜 새 버전은 새로운 캐시 키를 가지도록 하세요(예: /api/products?v=2).
💡 캐시 히트율을 모니터링하세요. Nginx 상태 모듈이나 로그 분석으로 히트율이 70% 이상인지 확인하고, 낮다면 TTL이나 캐시 키 설정을 조정하세요.
💡 메모리가 충분하다면 keys_zone 크기를 늘려서 더 많은 캐시 키를 저장하세요. 캐시 키가 메모리에서 제거되면 해당 캐시 데이터도 무효화됩니다.
5. API 라우팅과 URL 재작성
시작하며
여러분이 마이크로서비스 아키텍처를 운영하면서 "사용자는 하나의 도메인으로 접근하는데, 어떻게 내부의 여러 서비스로 요청을 분배하지?"라는 고민을 해본 적 있나요? 사용자 서비스, 주문 서비스, 결제 서비스가 각각 다른 서버에서 실행되는데, 외부에는 단일 API 엔드포인트만 노출하고 싶습니다.
이런 문제는 마이크로서비스가 보편화되면서 거의 모든 팀이 겪는 상황입니다. 각 서비스가 독립적으로 배포되고 관리되지만, 클라이언트 입장에서는 복잡한 내부 구조를 알 필요가 없어야 합니다.
또한 레거시 API를 새로운 버전으로 마이그레이션하면서 기존 URL을 유지해야 하는 경우도 많습니다. 바로 이럴 때 필요한 것이 API 게이트웨이 패턴과 URL 재작성입니다.
Nginx를 사용해 경로 기반으로 요청을 적절한 백엔드 서비스로 라우팅하고, 필요한 경우 URL을 변환하여 내부 서비스의 실제 구조를 숨길 수 있습니다.
개요
간단히 말해서, API 라우팅은 URL 경로를 기준으로 요청을 여러 백엔드 서비스로 분배하는 것이고, URL 재작성은 클라이언트가 사용하는 URL과 백엔드가 받는 URL을 다르게 만드는 기술입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 마이크로서비스의 복잡성을 클라이언트로부터 숨길 수 있습니다.
클라이언트는 api.example.com이라는 단일 엔드포인트만 알면 되고, 내부에 몇 개의 서비스가 있는지 알 필요가 없습니다. 둘째로 버전 관리가 쉬워집니다.
/v1/users를 /v2/users로 마이그레이션하면서 기존 클라이언트도 계속 지원할 수 있습니다. 예를 들어, 모바일 앱을 업데이트하지 않은 사용자도 서비스를 계속 사용할 수 있어야 하는 경우가 많습니다.
기존에는 클라이언트가 각 서비스의 주소를 직접 알아야 했고, CORS 설정도 복잡했으며, 서비스 주소가 바뀌면 클라이언트 코드도 수정해야 했다면, 이제는 Nginx가 중앙에서 모든 라우팅을 처리하므로 백엔드 구조를 자유롭게 변경할 수 있습니다. API 라우팅의 핵심 특징은 세 가지입니다.
첫째, 경로 기반 라우팅으로 /users는 사용자 서비스로, /orders는 주문 서비스로 보낼 수 있습니다. 둘째, 정규표현식을 사용한 복잡한 URL 변환이 가능합니다.
셋째, 요청 헤더나 쿼리 파라미터를 기준으로도 라우팅할 수 있습니다. 이러한 특징들이 중요한 이유는 실제 프로덕션 환경에서는 단순한 1:1 매핑보다 훨씬 복잡한 라우팅 요구사항이 있기 때문입니다.
코드 예제
# Nginx API 라우팅 및 URL 재작성 설정
upstream user_service {
server localhost:3001;
}
upstream order_service {
server localhost:3002;
}
upstream payment_service {
server localhost:3003;
}
server {
listen 80;
server_name api.example.com;
# /api/users -> 사용자 서비스
location /api/users {
# /api 접두사 제거
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://user_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# /api/orders -> 주문 서비스
location /api/orders {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://order_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# /api/payments -> 결제 서비스
location /api/payments {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://payment_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 레거시 API를 새 버전으로 리디렉션
location /v1/users {
return 301 /api/users$request_uri;
}
}
설명
이것이 하는 일: 위 설정은 단일 도메인(api.example.com)으로 들어오는 요청을 경로에 따라 세 개의 독립적인 마이크로서비스로 분배하고, 클라이언트가 사용하는 /api/ 접두사를 제거하여 백엔드 서비스에 전달하며, 레거시 URL은 새 버전으로 리디렉션합니다. 첫 번째로, 세 개의 upstream 블록이 각각의 마이크로서비스를 정의합니다.
user_service는 3001번 포트에서, order_service는 3002번, payment_service는 3003번 포트에서 실행됩니다. 각 서비스는 독립적으로 개발, 배포, 스케일링될 수 있습니다.
나중에 각 upstream에 여러 서버를 추가하여 서비스별로 로드밸런싱을 적용할 수도 있습니다. 그 다음으로, 각 location 블록이 경로 기반 라우팅을 구현합니다.
/api/users로 시작하는 모든 요청은 user_service로, /api/orders는 order_service로 전달됩니다. rewrite 지시어는 매우 중요한데, ^/api/(.*)$는 정규표현식으로 "/api/"로 시작하는 URL을 캡처하고, /$1로 "/api/" 부분을 제거합니다.
break 플래그는 재작성 후 더 이상 다른 rewrite 규칙을 적용하지 않도록 합니다. 예를 들어, 클라이언트가 GET /api/users/123을 요청하면, Nginx는 이를 GET /users/123으로 변환하여 user_service로 전달합니다.
백엔드 서비스는 /api/ 접두사를 처리할 필요가 없으므로, 코드가 더 깔끔해지고 서비스를 독립적으로 테스트하기도 쉽습니다. 마지막으로, 레거시 API 지원을 위한 리디렉션이 포함되어 있습니다.
/v1/users로 요청이 오면 301 영구 리디렉션으로 /api/users로 보냅니다. $request_uri는 원본 URL의 쿼리 스트링까지 포함하므로, /v1/users?page=2는 /api/users?page=2로 정확히 변환됩니다.
301 상태 코드는 검색 엔진과 클라이언트에게 URL이 영구적으로 변경되었음을 알립니다. 여러분이 이 설정을 사용하면 마이크로서비스를 추가하거나 제거할 때 클라이언트 코드를 수정할 필요가 없고, 각 서비스의 배포와 스케일링을 독립적으로 할 수 있으며, API 버전 관리가 매우 유연해집니다.
실전 팁
💡 location 블록의 순서가 중요합니다. 더 구체적인 경로를 먼저 배치하세요. 예를 들어, /api/users/admin은 /api/users보다 앞에 위치해야 합니다.
💡 rewrite의 플래그를 정확히 이해하세요. break는 재작성 후 현재 블록 내에서 처리를 계속하고, last는 재작성 후 location 매칭을 다시 시작합니다. 대부분의 경우 break가 적절합니다.
💡 정규표현식 location(location ~ ^/api/v[0-9]+/)을 사용하면 여러 버전을 동적으로 처리할 수 있습니다. /api/v1/, /api/v2/ 등을 하나의 블록으로 처리 가능합니다.
💡 각 서비스로 전달되는 요청을 로깅하여 어떤 서비스가 얼마나 호출되는지 모니터링하세요. 이는 성능 최적화와 용량 계획에 도움이 됩니다.
💡 CORS 문제를 Nginx에서 중앙 집중식으로 처리하면 각 백엔드 서비스에서 CORS 헤더를 설정할 필요가 없어집니다. add_header 지시어로 필요한 CORS 헤더를 추가하세요.
6. Rate Limiting 요청 속도 제한
시작하며
여러분의 API 서버가 갑자기 특정 사용자로부터 초당 수천 건의 요청을 받아서 다른 정상 사용자들이 서비스를 이용하지 못하는 상황을 경험해본 적 있나요? 이는 악의적인 공격일 수도 있고, 잘못 작성된 클라이언트 코드의 무한 루프일 수도 있습니다.
이런 문제는 공개 API를 운영하는 거의 모든 서비스에서 발생합니다. 한 사용자의 과도한 요청이 서버 리소스를 독점하면 다른 모든 사용자의 경험이 나빠지고, 최악의 경우 서버가 다운될 수 있습니다.
또한 크롤링 봇이나 DDoS 공격으로부터 서비스를 보호해야 하는 필요성도 있습니다. 바로 이럴 때 필요한 것이 Rate Limiting(속도 제한)입니다.
Nginx는 IP 주소나 API 키를 기준으로 일정 시간당 허용되는 요청 수를 제한하여, 공정한 리소스 사용을 보장하고 악의적인 트래픽으로부터 서비스를 보호합니다.
개요
간단히 말해서, Rate Limiting은 특정 클라이언트가 일정 시간 동안 보낼 수 있는 요청 수를 제한하는 보안 및 안정성 기술입니다. 정해진 한계를 초과하면 요청이 거부됩니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 서비스의 안정성을 보장할 수 있습니다. 모든 사용자가 공평하게 서버 리소스를 사용하도록 강제합니다.
둘째로 비용을 절감할 수 있습니다. 과도한 트래픽으로 인한 인프라 확장을 방지하고, 종량제 클라우드에서는 직접적인 비용 절감 효과가 있습니다.
셋째로 보안을 강화합니다. Brute-force 공격(무차별 대입 공격)이나 DDoS 공격을 효과적으로 차단할 수 있습니다.
예를 들어, 로그인 엔드포인트에 초당 3회 제한을 걸면 비밀번호 무차별 대입 공격이 거의 불가능해집니다. 기존에는 애플리케이션 코드에서 직접 Rate Limiting을 구현해야 했고, Redis 같은 외부 저장소를 사용해야 했으며, 모든 서비스에 동일한 로직을 중복으로 작성해야 했다면, 이제는 Nginx 레벨에서 설정만으로 모든 백엔드 서비스를 보호할 수 있습니다.
Rate Limiting의 핵심 특징은 세 가지입니다. 첫째, IP 주소, API 키, 사용자 ID 등 다양한 기준으로 제한할 수 있습니다.
둘째, 버스트(burst) 허용으로 일시적인 트래픽 급증을 수용하면서도 장기적인 평균 속도를 제한합니다. 셋째, 경로별로 다른 제한을 적용할 수 있어 중요한 엔드포인트는 더 엄격하게 보호할 수 있습니다.
이러한 특징들이 중요한 이유는 모든 엔드포인트가 동일한 보호 수준을 필요로 하지 않고, 실제 사용 패턴은 완벽하게 균일하지 않기 때문입니다.
코드 예제
# Nginx Rate Limiting 설정 (nginx.conf 또는 http 블록에)
# IP 주소당 초당 10개 요청으로 제한 (영역 크기 10MB)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 로그인은 더 엄격하게 제한 (초당 3개)
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=3r/s;
server {
listen 80;
server_name api.example.com;
# 일반 API 엔드포인트
location /api/ {
# rate limit 적용 (burst: 최대 20개까지 큐에 대기)
limit_req zone=api_limit burst=20 nodelay;
# 429 상태 코드 대신 커스텀 응답
limit_req_status 429;
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 로그인 엔드포인트 (더 엄격한 제한)
location /api/auth/login {
# 초당 3개, burst 5개 허용
limit_req zone=login_limit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 특정 IP는 제한에서 제외 (내부 모니터링 도구 등)
location /api/health {
# 10.0.0.0/8 내부 네트워크는 제한 없음
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
proxy_pass http://localhost:3000;
}
}
설명
이것이 하는 일: 위 설정은 IP 주소를 기준으로 일반 API는 초당 10개, 로그인 API는 초당 3개로 요청을 제한하고, 일시적인 버스트는 허용하되 장기적으로 과도한 요청은 429 에러로 거부하며, 내부 네트워크는 제한에서 제외합니다. 첫 번째로, limit_req_zone 지시어가 Rate Limiting의 핵심 메커니즘을 정의합니다.
$binary_remote_addr는 클라이언트의 IP 주소를 바이너리 형태로 사용하여 메모리 효율을 높입니다. (문자열 IP 대비 약 1/4 크기) zone=api_limit:10m은 10MB의 공유 메모리 영역을 할당하는데, 이는 약 16만 개의 IP 주소 상태를 추적할 수 있습니다.
rate=10r/s는 각 IP 주소당 초당 10개의 요청만 허용한다는 뜻입니다. 그 다음으로, limit_req 지시어가 실제로 제한을 적용합니다.
burst=20은 매우 중요한 매개변수인데, 이는 일시적으로 초당 10개를 초과하는 요청을 최대 20개까지 큐에 대기시킬 수 있다는 의미입니다. 예를 들어, 사용자가 페이지를 새로고침할 때 여러 개의 API가 동시에 호출되는 경우가 있는데, burst 설정이 없다면 이 중 일부가 거부됩니다.
nodelay 옵션은 버스트 요청도 즉시 처리하되 제한 카운터는 정확히 적용합니다. 이 옵션이 없으면 버스트 요청들이 지연되어 응답 시간이 느려집니다.
세 번째로, 로그인 엔드포인트에는 더 엄격한 제한이 적용됩니다. 초당 3개로 제한하고 버스트도 5개만 허용하여, 무차별 대입 공격을 효과적으로 차단합니다.
공격자가 10,000개의 비밀번호를 시도하려면 약 55분이 걸리므로, 실질적으로 공격이 불가능해집니다. limit_req_status 429는 제한을 초과한 요청에 대해 HTTP 429 (Too Many Requests) 상태 코드를 반환합니다.
마지막으로, 헬스체크 엔드포인트는 allow와 deny 지시어로 IP 기반 접근 제어를 구현합니다. 내부 네트워크(10.0.0.0/8)와 로컬호스트만 접근을 허용하고 나머지는 모두 거부하여, 모니터링 도구가 제한 없이 상태를 체크할 수 있게 합니다.
여러분이 이 설정을 사용하면 DDoS 공격이나 악의적인 크롤러로부터 서비스를 보호할 수 있고, 모든 사용자가 공평하게 서비스를 이용할 수 있으며, 인프라 비용을 예측 가능한 수준으로 유지할 수 있습니다.
실전 팁
💡 Rate limit을 너무 낮게 설정하면 정상 사용자도 영향을 받습니다. 실제 트래픽 패턴을 분석하여 95 백분위수(95th percentile)보다 약간 높게 설정하세요.
💡 $binary_remote_addr 대신 $http_authorization나 $http_x_api_key를 사용하면 API 키 기준으로 제한할 수 있습니다. 이는 여러 사용자가 같은 NAT 뒤에 있을 때 유용합니다.
💡 burst 값은 rate의 1-2배가 적절합니다. 너무 크면 급격한 트래픽 증가를 막지 못하고, 너무 작으면 정상 사용자의 경험이 나빠집니다.
💡 429 에러를 받은 클라이언트에게 Retry-After 헤더로 언제 재시도할 수 있는지 알려주면 사용자 경험이 개선됩니다. add_header Retry-After 60 같이 추가하세요.
💡 CloudFlare나 AWS WAF 같은 서비스를 함께 사용하면 더 고급 DDoS 방어를 구현할 수 있지만, Nginx의 Rate Limiting만으로도 대부분의 악의적 트래픽을 막을 수 있습니다.
7. 보안 헤더 설정
시작하며
여러분의 웹 애플리케이션이 XSS(Cross-Site Scripting) 공격이나 클릭재킹(Clickjacking) 같은 웹 보안 위협에 노출되어 있다면, 사용자 데이터가 위험에 처할 수 있습니다. "코드에는 보안 문제가 없는데, 브라우저 레벨에서 추가 보호를 받을 수 있을까?"라는 생각을 해본 적 있나요?
이런 문제는 특히 사용자가 생성한 콘텐츠를 다루는 서비스에서 심각합니다. 완벽한 입력 검증을 하더라도, 브라우저가 제공하는 추가 보안 계층을 활용하지 않으면 여러 공격에 취약할 수 있습니다.
예를 들어, iframe을 통한 클릭재킹 공격이나, MIME 타입 스니핑을 악용한 공격 등이 있습니다. 바로 이럴 때 필요한 것이 보안 헤더 설정입니다.
Nginx에서 적절한 HTTP 보안 헤더를 추가함으로써 브라우저가 다양한 보안 정책을 시행하도록 하여, 애플리케이션의 보안을 크게 강화할 수 있습니다.
개요
간단히 말해서, 보안 헤더는 브라우저에게 특정 보안 정책을 시행하도록 지시하는 HTTP 응답 헤더들입니다. 이를 통해 다양한 웹 공격으로부터 사용자를 보호합니다.
왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 방어 심층화(Defense in Depth) 전략을 구현할 수 있습니다. 코드 레벨의 보안과 더불어 브라우저 레벨의 보호를 추가하는 것입니다.
둘째로 제로데이 취약점을 완화할 수 있습니다. 아직 발견되지 않은 보안 버그가 있더라도 보안 헤더가 공격을 차단할 수 있습니다.
셋째로 규정 준수(Compliance) 요구사항을 충족합니다. OWASP나 PCI DSS 같은 보안 표준에서 권장하는 헤더들을 쉽게 적용할 수 있습니다.
예를 들어, 금융 서비스나 의료 정보를 다루는 애플리케이션은 엄격한 보안 헤더 설정이 필수입니다. 기존에는 각 애플리케이션 프레임워크마다 다른 방식으로 보안 헤더를 설정해야 했고, 일부 헤더는 미들웨어를 직접 작성해야 했다면, 이제는 Nginx에서 중앙 집중식으로 모든 보안 헤더를 관리할 수 있습니다.
모든 백엔드 서비스에 동일한 보안 정책이 자동으로 적용됩니다. 보안 헤더의 핵심 특징은 세 가지입니다.
첫째, 다양한 공격 벡터를 차단합니다. XSS, 클릭재킹, MIME 스니핑, 정보 유출 등을 방어합니다.
둘째, 설정이 간단하고 성능 오버헤드가 거의 없습니다. 단순히 헤더를 추가하는 것이므로 서버 부하가 증가하지 않습니다.
셋째, 브라우저의 최신 보안 기능을 활용합니다. Content Security Policy 같은 강력한 메커니즘을 쉽게 적용할 수 있습니다.
이러한 특징들이 중요한 이유는 웹 보안은 끊임없이 진화하는 분야이고, 브라우저 벤더들이 제공하는 최신 보호 기능을 적극 활용해야 하기 때문입니다.
코드 예제
# Nginx 보안 헤더 설정
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 설정 (생략)
location / {
proxy_pass http://localhost:3000;
# X-Frame-Options: 클릭재킹 방지
add_header X-Frame-Options "SAMEORIGIN" always;
# X-Content-Type-Options: MIME 스니핑 방지
add_header X-Content-Type-Options "nosniff" always;
# X-XSS-Protection: 브라우저 XSS 필터 활성화
add_header X-XSS-Protection "1; mode=block" always;
# Referrer-Policy: Referrer 정보 제어
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions-Policy: 브라우저 기능 제한
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Content-Security-Policy: 강력한 콘텐츠 보안 정책
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
# Strict-Transport-Security: HTTPS 강제 (HSTS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 서버 정보 숨기기
proxy_hide_header X-Powered-By;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
설명
이것이 하는 일: 위 설정은 8가지 핵심 보안 헤더를 모든 응답에 추가하여 클릭재킹, XSS, MIME 스니핑, 정보 유출 등 다양한 공격을 방어하고, HTTPS 사용을 강제하며, 브라우저가 접근할 수 있는 리소스와 기능을 제한합니다. 첫 번째로, X-Frame-Options: SAMEORIGIN은 클릭재킹 공격을 방어합니다.
이 헤더는 브라우저에게 페이지가 동일 출처의 iframe에서만 표시되도록 지시합니다. 공격자가 여러분의 페이지를 투명한 iframe으로 감싸서 사용자를 속이려는 시도를 차단합니다.
예를 들어, 사용자가 게임을 하고 있다고 생각하지만 실제로는 그 뒤에 숨겨진 송금 버튼을 클릭하게 만드는 공격을 방지합니다. 그 다음으로, X-Content-Type-Options: nosniff는 MIME 스니핑 공격을 방지합니다.
브라우저가 서버가 선언한 Content-Type을 무시하고 파일 내용을 분석하여 타입을 추측하는 것을 막습니다. 이는 공격자가 이미지 파일처럼 위장한 JavaScript 파일을 업로드하여 실행하는 공격을 차단합니다.
X-XSS-Protection은 브라우저의 내장 XSS 필터를 활성화하여 반사형 XSS 공격을 탐지하면 페이지 렌더링을 중단합니다. 세 번째로, Referrer-Policy: strict-origin-when-cross-origin은 다른 사이트로 이동할 때 전달되는 Referrer 정보를 제어합니다.
같은 출처 내에서는 전체 URL을 전달하지만, 다른 사이트로는 출처(도메인)만 전달하여 민감한 정보(예: 쿼리 파라미터의 토큰)가 유출되지 않도록 합니다. Permissions-Policy는 브라우저의 강력한 기능(위치 정보, 마이크, 카메라 등)에 대한 접근을 제한합니다.
이는 악의적인 스크립트가 이러한 기능을 악용하는 것을 방지합니다. 네 번째로, Content-Security-Policy(CSP)는 가장 강력한 보안 헤더입니다.
default-src 'self'는 모든 리소스를 기본적으로 같은 출처에서만 로드하도록 제한합니다. script-src, style-src 등은 각 리소스 타입별로 허용되는 출처를 세밀하게 제어합니다.
'unsafe-inline'은 인라인 스크립트를 허용하는데, 프로덕션에서는 제거하고 nonce나 hash를 사용하는 것이 더 안전합니다. frame-ancestors 'none'은 X-Frame-Options의 더 강력한 버전으로, 어떤 출처의 iframe에도 포함되지 않도록 합니다.
마지막으로, Strict-Transport-Security(HSTS)는 브라우저가 항상 HTTPS로만 사이트에 접속하도록 강제합니다. max-age=31536000은 1년간 이 정책을 기억하라는 의미이고, includeSubDomains는 모든 서브도메인에도 적용하며, preload는 브라우저의 HSTS 사전 로드 목록에 포함되도록 신청할 수 있게 합니다.
proxy_hide_header X-Powered-By는 백엔드 서버가 반환하는 기술 스택 정보를 숨겨 공격자가 타겟팅하기 어렵게 만듭니다. 여러분이 이 설정을 사용하면 Mozilla Observatory나 SecurityHeaders.com에서 A 등급을 받을 수 있고, 다양한 웹 공격으로부터 사용자를 보호할 수 있으며, 보안 감사나 규정 준수 요구사항을 충족할 수 있습니다.
실전 팁
💡 CSP를 처음 적용할 때는 Content-Security-Policy-Report-Only 헤더를 먼저 사용하세요. 이는 정책을 시행하지 않고 위반 사항만 보고하므로, 실제 적용 전에 문제를 파악할 수 있습니다.
💡 always 파라미터를 반드시 추가하세요. 이것이 없으면 에러 응답(4xx, 5xx)에는 보안 헤더가 포함되지 않아 보안 허점이 생깁니다.
💡 SecurityHeaders.com이나 Mozilla Observatory에서 무료로 헤더를 테스트하고 개선 방안을 받을 수 있습니다. 정기적으로 검사하여 새로운 보안 헤더가 추가되었는지 확인하세요.
💡 CSP의 'unsafe-inline'은 가능한 한 제거하고, nonce나 hash 기반 CSP를 사용하세요. 이는 인라인 스크립트 공격을 완벽하게 차단합니다.
💡 서브도메인을 사용한다면 HSTS의 includeSubDomains 옵션을 추가하기 전에 모든 서브도메인이 HTTPS를 지원하는지 확인하세요. 그렇지 않으면 해당 서브도메인이 접근 불가능해집니다.
8. Health Check와 상태 모니터링
시작하며
여러분이 로드밸런싱 환경에서 여러 백엔드 서버를 운영하는데, 한 서버가 조용히 다운되어도 한참 뒤에야 알게 되는 상황을 경험해본 적 있나요? 그동안 일부 사용자는 에러를 받고, Nginx는 계속해서 죽은 서버로 요청을 보내고 있었을 것입니다.
이런 문제는 고가용성(High Availability) 시스템에서 치명적입니다. 서버가 다운되는 것은 피할 수 없지만, 다운된 서버를 빠르게 감지하고 자동으로 제외하지 못한다면 서비스 품질이 크게 저하됩니다.
또한 서버가 응답은 하지만 내부적으로 문제가 있는 경우(예: 데이터베이스 연결 끊김)도 정상 서버처럼 보일 수 있습니다. 바로 이럴 때 필요한 것이 Health Check(헬스 체크)와 상태 모니터링입니다.
Nginx가 주기적으로 백엔드 서버의 상태를 확인하고, 문제가 있는 서버를 자동으로 제외하며, 복구된 서버를 다시 포함시킴으로써 항상 건강한 서버로만 트래픽을 보낼 수 있습니다.
개요
간단히 말해서, Health Check는 백엔드 서버가 정상적으로 작동하는지 주기적으로 확인하는 메커니즘이고, 비정상 서버를 자동으로 격리하여 서비스 품질을 유지하는 기술입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 서비스의 가용성을 크게 높일 수 있습니다.
서버 장애를 수 초 내에 감지하고 자동으로 대응하므로, 사용자는 거의 영향을 받지 않습니다. 둘째로 운영 부담이 줄어듭니다.
새벽에 서버가 다운되어도 자동으로 처리되므로, 엔지니어가 즉시 대응할 필요가 없습니다. 셋째로 배포가 안전해집니다.
새 버전을 배포할 때 해당 서버가 정상 작동하는지 자동으로 확인한 후에야 트래픽을 받기 시작합니다. 예를 들어, 블루-그린 배포나 카나리 배포 전략을 구현할 때 Health Check가 핵심 역할을 합니다.
기존에는 수동으로 서버 상태를 확인하거나, 외부 모니터링 도구에 의존해야 했으며, 문제를 발견해도 수동으로 Nginx 설정을 변경해야 했다면, 이제는 Nginx가 자동으로 모든 것을 처리합니다. Passive Health Check(수동적 헬스 체크)는 실제 요청의 실패를 기반으로 판단하고, Nginx Plus의 Active Health Check(능동적 헬스 체크)는 주기적으로 헬스체크 요청을 보냅니다.
Health Check의 핵심 특징은 세 가지입니다. 첫째, 실패 임계값과 타임아웃을 설정하여 일시적인 오류와 실제 장애를 구분합니다.
둘째, 자동 복구 메커니즘으로 서버가 정상화되면 다시 트래픽을 받기 시작합니다. 셋째, 다양한 체크 방식을 지원합니다.
HTTP 상태 코드, 응답 시간, 응답 내용 등을 기준으로 판단할 수 있습니다. 이러한 특징들이 중요한 이유는 서버 장애는 다양한 형태로 나타나고, 너무 민감하게 반응하면 정상 서버까지 제외할 수 있기 때문입니다.
코드 예제
# Nginx Health Check 설정
upstream backend_servers {
# 서버 목록
server localhost:3000 max_fails=3 fail_timeout=30s;
server localhost:3001 max_fails=3 fail_timeout=30s;
server localhost:3002 max_fails=3 fail_timeout=30s;
# Passive Health Check 설정
# max_fails: 연속 실패 횟수 (3회 실패하면 제외)
# fail_timeout: 제외된 서버를 다시 시도하기까지의 시간 (30초)
# 연결 타임아웃 등 추가 설정
keepalive 32; # Keep-alive 연결 유지
}
server {
listen 80;
server_name api.example.com;
# Health check 전용 엔드포인트
location /health {
access_log off; # 로그에 기록하지 않음 (너무 많은 요청)
proxy_pass http://backend_servers/health;
# Health check 전용 타임아웃 (더 짧게)
proxy_connect_timeout 2s;
proxy_send_timeout 2s;
proxy_read_timeout 2s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Health check 응답 캐싱 (부하 감소)
proxy_cache health_cache;
proxy_cache_valid 200 10s;
}
# 일반 API 요청
location /api/ {
proxy_pass http://backend_servers;
# 일반 요청의 타임아웃
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 에러 시 다음 서버로 자동 전환
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 2; # 최대 2번 재시도
proxy_next_upstream_timeout 5s; # 재시도 전체 타임아웃
}
}
# 캐시 영역 정의 (http 블록에)
proxy_cache_path /var/cache/nginx/health keys_zone=health_cache:1m max_size=10m;
설명
이것이 하는 일: 위 설정은 각 백엔드 서버의 요청 실패를 모니터링하여 30초 내 3회 연속 실패하면 자동으로 해당 서버를 제외하고, 30초 후 자동으로 재시도하며, 요청 실패 시 다른 정상 서버로 자동 전환하고, 별도의 헬스체크 엔드포인트를 제공합니다. 첫 번째로, upstream 블록의 max_fails와 fail_timeout 파라미터가 Passive Health Check의 핵심입니다.
Nginx는 각 서버로 보낸 실제 요청의 결과를 추적합니다. 한 서버가 fail_timeout 기간(30초) 내에 max_fails 횟수(3회)만큼 연속으로 실패하면, Nginx는 자동으로 이 서버를 "unhealthy"로 표시하고 더 이상 요청을 보내지 않습니다.
"실패"의 기준은 연결 실패, 타임아웃, HTTP 5xx 에러 등입니다. 30초가 지나면 Nginx는 다시 한 번 시도해보고, 성공하면 서버를 정상 상태로 복귀시킵니다.
그 다음으로, /health 엔드포인트는 외부 모니터링 도구나 로드밸런서가 사용하는 헬스체크 전용 경로입니다. access_log off로 설정하여 수 초마다 오는 헬스체크 요청이 로그를 가득 채우지 않도록 합니다.
타임아웃을 2초로 짧게 설정하여 빠르게 응답하지 못하는 서버는 비정상으로 판단합니다. proxy_cache로 10초간 응답을 캐싱하여 백엔드의 부담을 줄입니다.
백엔드 애플리케이션은 /health 엔드포인트에서 데이터베이스 연결, 외부 API 연결 등 중요한 의존성을 체크하고 200 OK 또는 503 Service Unavailable을 반환해야 합니다. 세 번째로, proxy_next_upstream 설정이 자동 장애 조치를 구현합니다.
요청이 한 서버에서 실패하면(error, timeout, 5xx 응답), Nginx는 자동으로 다른 서버로 같은 요청을 다시 보냅니다. proxy_next_upstream_tries 2는 최대 2개의 다른 서버를 시도한다는 의미이고, proxy_next_upstream_timeout 5s는 이 전체 재시도 과정이 5초를 넘지 않도록 제한합니다.
이는 사용자가 무한정 기다리지 않도록 보장합니다. keepalive 32는 백엔드 서버와의 연결을 최대 32개까지 재사용하도록 설정하여 TCP 핸드셰이크 오버헤드를 줄입니다.
이는 특히 헬스체크처럼 빈번한 요청에서 성능을 크게 향상시킵니다. 여러분이 이 설정을 사용하면 서버 장애를 수 초 내에 자동으로 감지하고 대응할 수 있으며, 사용자는 거의 에러를 경험하지 않고, 운영팀은 여유를 가지고 문제를 해결할 수 있습니다.
실전 팁
💡 max_fails를 1로 설정하면 너무 민감하여 일시적인 네트워크 문제로도 서버가 제외됩니다. 프로덕션에서는 3-5가 적절합니다.
💡 백엔드 애플리케이션의 /health 엔드포인트는 단순히 200만 반환하지 말고, 실제로 중요한 의존성(DB, Redis, 외부 API 등)을 체크하세요. "응답은 하지만 제대로 작동하지 않는" 상황을 감지할 수 있습니다.
💡 Nginx Plus(상용 버전)를 사용하면 Active Health Check를 사용할 수 있습니다. 주기적으로 백엔드에 헬스체크 요청을 보내므로, 실제 사용자 요청이 없어도 서버 상태를 파악할 수 있습니다.
💡 Prometheus나 Grafana 같은 모니터링 도구와 연동하여 서버의 healthy/unhealthy 상태 변화를 추적하고, 자주 다운되는 서버를 파악하세요.
💡 proxy_next_upstream에 non_idempotent를 추가하지 마세요. POST 요청 같은 멱등성이 없는 요청이 재시도되면 중복 처리(예: 중복 결제)가 발생할 수 있습니다.
9. 정적 파일 서빙 최적화
시작하며
여러분이 React나 Vue로 만든 SPA(Single Page Application)를 배포하면서 "이미지, CSS, JavaScript 파일을 Node.js 서버로 서빙하는 게 맞나? 너무 느린 것 같은데..."라는 생각을 해본 적 있나요?
정적 파일을 애플리케이션 서버로 처리하면 불필요한 리소스 낭비가 발생합니다. 이런 문제는 특히 이미지나 동영상 같은 큰 파일이 많은 서비스에서 심각합니다.
Node.js나 Python 같은 애플리케이션 서버는 동적 콘텐츠 생성에 최적화되어 있고, 정적 파일 서빙은 그들의 강점이 아닙니다. 정적 파일 요청이 동적 API 요청과 같은 리소스를 사용하면 전체 성능이 저하됩니다.
바로 이럴 때 필요한 것이 Nginx를 통한 정적 파일 서빙입니다. Nginx는 정적 파일을 서빙하는 데 최적화되어 있고, gzip 압축, 브라우저 캐싱, 효율적인 파일 읽기 등의 기능으로 속도를 크게 향상시킬 수 있습니다.
개요
간단히 말해서, 정적 파일 서빙은 HTML, CSS, JavaScript, 이미지 같은 변하지 않는 파일을 Nginx가 직접 제공하는 것이고, 애플리케이션 서버는 동적 API만 처리하도록 하는 아키텍처 패턴입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 성능이 극적으로 향상됩니다.
Nginx는 C로 작성되어 정적 파일 서빙에서 Node.js보다 수십 배 빠를 수 있습니다. 둘째로 애플리케이션 서버의 부담이 줄어듭니다.
정적 파일 요청이 전체 트래픽의 70-80%를 차지하는 경우가 많은데, 이를 Nginx가 처리하면 애플리케이션 서버는 실제 비즈니스 로직에만 집중할 수 있습니다. 셋째로 CDN과의 통합이 쉬워집니다.
Nginx에서 정적 파일을 서빙하면 나중에 CloudFront나 CloudFlare 같은 CDN 앞에 배치하기만 하면 됩니다. 예를 들어, 이커머스 사이트에서 상품 이미지 수천 장을 서빙하는 경우, Node.js로 처리하면 서버가 과부하되지만 Nginx는 여유롭게 처리합니다.
기존에는 정적 파일도 애플리케이션 프레임워크의 미들웨어로 처리하거나, 별도의 파일 서버를 구축해야 했다면, 이제는 이미 있는 Nginx 리버스 프록시에서 정적 파일도 함께 처리하면 됩니다. 인프라가 단순해지고 관리가 쉬워집니다.
정적 파일 서빙의 핵심 특징은 세 가지입니다. 첫째, gzip/brotli 압축으로 전송 크기를 크게 줄입니다.
텍스트 파일은 70-90% 압축됩니다. 둘째, 브라우저 캐싱 헤더로 반복 방문 시 파일을 다시 다운로드하지 않습니다.
셋째, sendfile 시스템 콜을 사용하여 커널 레벨에서 파일을 전송하므로 매우 효율적입니다. 이러한 특징들이 중요한 이유는 정적 파일이 전체 페이지 로딩 시간의 대부분을 차지하고, 사용자 경험에 직접적인 영향을 미치기 때문입니다.
코드 예제
# Nginx 정적 파일 서빙 최적화
server {
listen 80;
server_name example.com;
# 정적 파일 루트 디렉토리
root /var/www/html;
index index.html;
# gzip 압축 활성화
gzip on;
gzip_vary on;
gzip_min_length 1024; # 1KB 이상 파일만 압축
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/rss+xml font/truetype font/opentype
application/vnd.ms-fontobject image/svg+xml;
# 정적 파일 (이미지, CSS, JS, 폰트)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
# 브라우저 캐싱 (1년)
expires 1y;
add_header Cache-Control "public, immutable";
# 파일이 없으면 404
try_files $uri =404;
# 액세스 로그 비활성화 (성능 향상)
access_log off;
}
# SPA를 위한 fallback (React, Vue 등)
location / {
try_files $uri $uri/ /index.html;
# HTML은 캐싱하지 않음 (항상 최신 버전)
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# API 요청은 백엔드로 프록시
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 보안: 숨겨진 파일 접근 차단
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
설명
이것이 하는 일: 위 설정은 Nginx가 /var/www/html 디렉토리의 정적 파일을 직접 서빙하고, gzip으로 압축하며, 브라우저 캐싱을 1년으로 설정하고, SPA의 클라이언트 라우팅을 지원하며, API 요청만 백엔드로 전달합니다. 첫 번째로, root /var/www/html 지시어가 정적 파일의 위치를 정의합니다.
예를 들어, 사용자가 /images/logo.png를 요청하면 Nginx는 /var/www/html/images/logo.png 파일을 찾습니다. index index.html은 디렉토리 요청 시 기본 파일을 지정합니다.
/about/로 요청이 오면 /var/www/html/about/index.html을 반환합니다. 그 다음으로, gzip 설정이 전송 크기를 획기적으로 줄입니다.
gzip on으로 압축을 활성화하고, gzip_min_length 1024로 1KB 이상의 파일만 압축합니다. 너무 작은 파일은 압축 오버헤드가 더 클 수 있습니다.
gzip_types는 압축할 MIME 타입을 지정하는데, 이미지나 동영상 같은 이미 압축된 포맷은 제외합니다. 텍스트 기반 파일(HTML, CSS, JS, JSON, SVG)은 70-90% 압축되어 로딩 속도가 크게 향상됩니다.
gzip_vary on은 Vary: Accept-Encoding 헤더를 추가하여 CDN이나 프록시가 압축 여부에 따라 다른 캐시를 유지하도록 합니다. 세 번째로, location ~* \.(jpg|jpeg|png|gif|...)$ 블록이 정적 파일에 대한 브라우저 캐싱을 설정합니다.
~*는 대소문자 구분 없는 정규표현식 매칭입니다. expires 1y는 1년 후에 만료되도록 Expires와 Cache-Control: max-age 헤더를 자동으로 추가합니다.
immutable 속성은 브라우저에게 이 파일은 절대 변경되지 않으니 재검증하지 말라고 알립니다. 이는 webpack이나 Vite가 파일명에 해시를 포함시키는 방식(app.a3f2b1c.js)과 완벽하게 호환됩니다.
파일 내용이 바뀌면 파일명도 바뀌므로 새로운 캐시 항목이 생성됩니다. access_log off로 정적 파일 요청을 로그에 기록하지 않아 디스크 I/O를 줄입니다.
네 번째로, SPA를 위한 try_files $uri $uri/ /index.html 설정이 클라이언트 사이드 라우팅을 지원합니다. 사용자가 /products/123을 요청하면, Nginx는 먼저 파일을 찾고, 없으면 디렉토리를 찾고, 그것도 없으면 /index.html을 반환합니다.
그러면 React Router나 Vue Router가 클라이언트에서 라우팅을 처리합니다. HTML 파일은 expires -1과 no-store, no-cache로 절대 캐싱되지 않도록 하여, 새 배포 시 사용자가 항상 최신 버전을 받습니다.
마지막으로, location ~ /\. 블록이 .git, .env 같은 숨겨진 파일의 접근을 차단하여 민감한 정보 유출을 방지합니다. 여러분이 이 설정을 사용하면 페이지 로딩 속도가 2-3배 빨라지고, 서버 대역폭 사용량이 70% 감소하며, 애플리케이션 서버의 CPU 사용량이 크게 줄어들고, 사용자가 재방문 시 거의 즉시 페이지가 로드됩니다.
실전 팁
💡 webpack이나 Vite를 사용한다면 파일명에 해시를 포함시키는 설정을 활성화하세요. 그러면 정적 파일을 안전하게 1년 캐싱할 수 있습니다.
💡 Brotli 압축을 추가로 활성화하면 gzip보다 15-20% 더 압축됩니다. brotli on; 지시어를 추가하되, Nginx에 brotli 모듈이 설치되어 있어야 합니다.
💡 큰 이미지는 WebP 포맷으로 변환하여 크기를 50-70% 줄이세요. Nginx에서 image_filter 모듈을 사용하거나, 빌드 타임에 변환할 수 있습니다.
💡 CDN을 사용한다면 Cache-Control: public을 반드시 추가하세요. private이면 CDN이 캐싱하지 않습니다.
💡 정적 파일이 많다면 open_file_cache를 설정하여 파일 디스크립터를 캐싱하면 파일 시스템 접근 횟수를 줄여 성능을 더 향상시킬 수 있습니다.
10. WebSocket 프록시 설정
시작하며
여러분이 실시간 채팅, 알림, 라이브 데이터 업데이트 같은 기능을 구현하면서 "WebSocket 연결이 Nginx를 통과할 수 있을까? 일반 HTTP 프록시 설정으로는 안 되는데..."라는 문제를 겪어본 적 있나요?
WebSocket은 일반 HTTP와 다른 프로토콜이므로 특별한 설정이 필요합니다. 이런 문제는 실시간 기능을 제공하는 거의 모든 현대 웹 애플리케이션에서 발생합니다.
WebSocket은 HTTP 연결을 업그레이드하여 지속적인 양방향 통신 채널을 생성하는데, 일반 리버스 프록시 설정은 이를 제대로 처리하지 못합니다. 연결이 끊기거나, 타임아웃되거나, 업그레이드에 실패하는 문제가 생깁니다.
바로 이럴 때 필요한 것이 WebSocket 프록시 설정입니다. Nginx는 HTTP Upgrade 헤더를 감지하고 WebSocket 연결을 올바르게 프록시할 수 있도록 특별한 헤더와 타임아웃 설정을 제공합니다.
개요
간단히 말해서, WebSocket 프록시는 클라이언트와 백엔드 서버 사이의 WebSocket 연결을 Nginx가 중개하되, 연결을 유지하고 양방향 통신을 지원하는 설정입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 첫째로 단일 엔드포인트로 HTTP와 WebSocket을 모두 제공할 수 있습니다.
클라이언트는 같은 도메인으로 일반 API와 WebSocket 모두에 접근할 수 있어 CORS 문제가 없습니다. 둘째로 로드밸런싱과 SSL 종료를 WebSocket에도 적용할 수 있습니다.
여러 WebSocket 서버에 트래픽을 분산시키고, 암호화도 Nginx에서 처리합니다. 셋째로 Rate Limiting이나 인증 같은 보안 정책을 WebSocket 연결에도 적용할 수 있습니다.
예를 들어, 채팅 애플리케이션에서 수천 명의 동시 접속자를 관리하는 경우, Nginx가 WebSocket 연결을 여러 백엔드 서버로 분배하여 확장성을 높일 수 있습니다. 기존에는 WebSocket은 별도의 포트나 도메인으로 제공해야 했고, SSL 인증서도 따로 관리해야 했으며, 로드밸런싱도 복잡했다면, 이제는 Nginx에서 HTTP와 WebSocket을 통합해서 관리할 수 있습니다.
WebSocket 프록시의 핵심 특징은 세 가지입니다. 첫째, HTTP Upgrade 메커니즘을 올바르게 처리하여 연결을 WebSocket으로 전환합니다.
둘째, 긴 타임아웃 설정으로 지속적인 연결을 유지합니다. 일반 HTTP는 몇 초면 충분하지만 WebSocket은 몇 시간 동안 연결될 수 있습니다.
셋째, Keep-Alive 설정으로 연결이 유휴 상태에서도 끊기지 않도록 합니다. 이러한 특징들이 중요한 이유는 WebSocket의 가치는 지속적인 연결에 있고, 연결이 자주 끊기면 실시간 기능의 의미가 없어지기 때문입니다.
코드 예제
# Nginx WebSocket 프록시 설정
upstream websocket_backend {
# WebSocket 서버들
server localhost:3000;
server localhost:3001;
# IP Hash로 같은 클라이언트는 같은 서버로
# (WebSocket은 상태를 유지하므로 중요)
ip_hash;
# Keep-alive 연결 유지
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 설정 (생략)
# WebSocket 엔드포인트
location /ws {
# WebSocket 백엔드로 프록시
proxy_pass http://websocket_backend;
# WebSocket Upgrade 헤더 처리 (필수!)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 일반 프록시 헤더
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;
# WebSocket을 위한 긴 타임아웃 (1시간)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# 버퍼링 비활성화 (실시간 전송)
proxy_buffering off;
# WebSocket 핑/퐁 유지
proxy_connect_timeout 7d;
}
# 일반 HTTP API
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
설명
이것이 하는 일: 위 설정은 /ws 경로의 HTTP 요청을 WebSocket으로 업그레이드하고, 백엔드 서버로 프록시하며, IP 해시로 같은 클라이언트를 같은 서버에 연결하고, 최대 1시간의 긴 타임아웃으로 지속적인 연결을 유지합니다. 첫 번째로, upstream 블록의 ip_hash 지시어가 매우 중요합니다.
WebSocket 애플리케이션은 종종 연결 상태를 서버 메모리에 저장합니다. 예를 들어, 채팅 애플리케이션에서 사용자가 어떤 방에 있는지, 누구와 연결되어 있는지 등의 정보는 서버의 메모리에 있습니다.
만약 같은 클라이언트의 요청이 매번 다른 서버로 간다면 상태가 유지되지 않습니다. ip_hash는 클라이언트의 IP 주소를 해시하여 항상 같은 서버로 라우팅하므로, 연결 상태가 유지됩니다.
더 나은 대안은 Redis 같은 외부 저장소에 상태를 저장하는 것이지만, ip_hash는 간단한 해결책입니다. 그 다음으로, WebSocket Upgrade를 위한 필수 헤더들이 설정됩니다.
proxy_http_version 1.1은 HTTP/1.1을 사용하도록 강제하는데, WebSocket은 HTTP/1.1의 Upgrade 메커니즘을 사용합니다. proxy_set_header Upgrade $http_upgrade는 클라이언트가 보낸 Upgrade: websocket 헤더를 백엔드로 전달하고, proxy_set_header Connection "upgrade"는 연결을 업그레이드하려는 의도를 알립니다.
이 두 헤더가 없으면 WebSocket 핸드셰이크가 실패하고 연결이 일반 HTTP로 유지되거나 에러가 발생합니다. 세 번째로, 타임아웃 설정이 WebSocket의 특성에 맞게 조정됩니다.
proxy_read_timeout 3600s와 proxy_send_timeout 3600s는 읽기/쓰기 타임아웃을 1시간으로 설정합니다. WebSocket 연결은 데이터를 주고받지 않아도 연결을 유지해야 하는데, 일반 HTTP의 기본 타임아웃(60초)으로는 너무 짧습니다.
실제 애플리케이션에서는 클라이언트와 서버가 주기적으로 ping/pong 메시지를 교환하여 연결을 활성 상태로 유지하므로, 이 타임아웃에 도달