
포워드 프록시 vs 리버스 프록시: 대리인의 위치
내가 숨으면 포워드(VPN), 서버가 숨으면 리버스(Nginx). 누가 대리인을 고용했는가?

내가 숨으면 포워드(VPN), 서버가 숨으면 리버스(Nginx). 누가 대리인을 고용했는가?
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

개발 초기, 경험 많은 개발자가 물었습니다:
"너 VPN 써봤지? 그거 프록시야."
며칠 후:
"우리 서버 앞에 Nginx 붙였어. 그것도 프록시야."
저: "???둘 다 프록시인데 왜 이름이 다르죠?"
시니어는 웃으면서 "한 번 찾아봐"라고만 했습니다. 구글링을 해봤지만 도식만 잔뜩 나오고, 영어로 된 설명은 "client-side vs server-side"라는 말만 반복했습니다. 제대로 와닿지 않았습니다.
그때 제가 가진 의문은 하나였습니다: "둘 다 중간에 낀 거면 본질적으로 같은 거 아냐?"
프로젝트에서 인프라를 구축할 때 이 개념이 계속 튀어나왔습니다:
모든 상황에서 "프록시"라는 단어가 나왔지만, 어떤 때는 Forward Proxy를 쓰고, 어떤 때는 Reverse Proxy를 썼습니다. 둘의 차이를 명확히 이해하지 못하니까 설계할 때마다 헷갈렸고, 특히 디버깅할 때 "이 IP는 어디서 온 거지?" 같은 질문에 답을 못 했습니다.
결국 실수를 했습니다. 회사 내부 API를 만들면서 Reverse Proxy 뒤에 있는 백엔드 서버에서 클라이언트 IP를 req.connection.remoteAddress로 찍었는데, 전부 Nginx IP만 찍혔습니다. 시니어가 "X-Forwarded-For 헤더 봐야지"라고 알려줬고, 그제야 "아, Reverse Proxy가 요청을 대신 보내니까 원본 IP가 헤더에 담기는구나"를 이해했습니다.
무엇보다 혼란스러웠던 건, 둘 다 "대리인" 역할을 하는데 왜 이름이 다르냐는 것이었습니다. 나중에 이해했지만, 핵심은 "누구를 위한 대리인이냐"였습니다.
시니어의 비유가 모든 걸 바꿔놨습니다:
"아, 누구 편에 서있느냐의 차이구나!""법정에 섰다고 생각해봐.
Forward Proxy (변호사가 내 옆에): 나: '판사님, 제가 억울합니다!' 변호사: '(끼어들며) 제 의뢰인이 말하고자 하는 바는...' → 나를 대신해서 말해줌 (내 신원 보호)
Reverse Proxy (변호사가 상대편 옆에): 나: '피고인, 나와!' 변호사: '(끼어들며) 제 의뢰인은 직접 대답하지 않겠습니다. 제가 답변드리죠.' → 상대방(서버)을 대신해서 막아줌 (서버 신원 보호)"
이 순간 모든 게 정리됐습니다. Forward Proxy는 클라이언트가 고용한 대리인이고, Reverse Proxy는 서버가 고용한 대리인입니다. 같은 "대리인"이지만 고용주가 다르니까, 보호하는 대상도, 목적도, 설정 방법도 전부 달랐던 겁니다.
[사용자] ← 여기 → [Forward Proxy] → [인터넷] → [서버]
Forward Proxy는 사용자 바로 옆에 붙어있습니다. 마치 비서처럼요.
여기서 핵심은, 서버는 Forward Proxy가 있는지 모릅니다. 서버 입장에서는 그냥 "프록시 IP에서 요청이 왔네"로 끝입니다. 실제로는 프록시 뒤에 직원 100명이 있든, 학생 1000명이 있든 서버는 알 수 없습니다.
제가 이해한 핵심 키워드는 "클라이언트가 주도권을 쥔다"입니다. 클라이언트가 프록시를 설정하고, 클라이언트가 "이 사이트는 프록시 거쳐서 가줘" 또는 "이 사이트는 직접 갈게"를 결정합니다.
프로젝트에서 이런 경험이 있었습니다:
직원 PC → 회사 Forward Proxy 서버 → 인터넷
시나리오:
저희 프로젝트에서는 업무 시간에 SNS 접속이 차단됐는데, 처음엔 "어떻게 차단하지?"가 궁금했습니다. 나중에 알고 보니 Forward Proxy에서 도메인 블랙리스트를 관리하고 있었습니다.
제가 한국에서 미국 넷플릭스를 보려고 할 때:
내 PC (한국) → VPN 서버 (미국) → Netflix (미국)
과정:
여기서 재미있었던 건, VPN이 "암호화 도구"라고만 알고 있었는데 사실은 Forward Proxy의 일종이었다는 점입니다. VPN은 단순히 암호화만 하는 게 아니라, 내 트래픽을 대신 보내주는 프록시 역할도 합니다.
# /etc/squid/squid.conf
# 포트 설정
http_port 3128
# ACL 정의
acl localnet src 192.168.1.0/24
acl blocked_sites dstdomain .facebook.com .youtube.com .instagram.com
acl work_hours time MTWHF 09:00-18:00
# 규칙 적용
http_access deny blocked_sites work_hours
http_access allow localnet
http_access deny all
# 로그 설정
access_log /var/log/squid/access.log squid
이 설정의 의미:
제가 실제로 Squid를 설정했을 때, 로그를 보니까 하루에 수백 건의 차단된 요청이 찍히더군요. 직원들이 페이스북 접속을 시도했다가 막힌 겁니다. 이게 Forward Proxy의 핵심 기능입니다: 아웃바운드 트래픽 제어.
부수적으로, 제가 웹 크롤링 프로젝트를 했을 때도 Forward Proxy가 유용했습니다:
# Python requests with proxy rotation
import requests
proxies = [
'http://proxy1.com:8080',
'http://proxy2.com:8080',
'http://proxy3.com:8080',
]
for i, url in enumerate(target_urls):
proxy = proxies[i % len(proxies)]
response = requests.get(url, proxies={'http': proxy, 'https': proxy})
같은 IP에서 계속 크롤링하면 차단당하니까, Forward Proxy를 여러 개 두고 IP를 바꿔가면서 요청했습니다. 이것도 Forward Proxy의 활용 사례입니다.
[사용자] → [인터넷] → [Reverse Proxy] ← 여기 → [백엔드 서버들]
Reverse Proxy는 서버 바로 앞에 붙어있습니다. 마치 경호원처럼요.
여기서 핵심은, 클라이언트는 Reverse Proxy가 있는지 모릅니다. 클라이언트 입장에서는 "api.company.com에 요청 보냈더니 응답 왔네"로 끝입니다. 뒤에 서버가 1대인지 100대인지, Nginx가 있는지 HAProxy가 있는지 전혀 모릅니다.
제가 이해한 핵심 키워드는 "서버가 주도권을 쥔다"입니다. 서버 관리자가 프록시를 설정하고, 클라이언트는 그냥 평범하게 요청만 보냅니다.
제가 회사 API 서버를 구축했을 때:
사용자 → Nginx (Reverse Proxy)
├→ API Server 1 (Node.js)
├→ API Server 2 (Node.js)
└→ API Server 3 (Node.js)
동작 과정:
처음 이 아키텍처를 구축했을 때 놀라웠던 점은, 서버를 추가하거나 제거해도 클라이언트 코드는 전혀 바꿀 필요가 없었다는 겁니다. Nginx 설정만 바꾸면 됐습니다. 이게 Reverse Proxy의 핵심 장점입니다.
HTTPS 처리를 Nginx에서 한 번에:
사용자 (HTTPS) → Nginx (HTTPS 처리)
↓ (HTTP)
백엔드 서버 (HTTP만 처리)
이점:
제가 실제로 이걸 적용했을 때, Node.js 서버의 CPU 사용률이 약 15% 줄었습니다. SSL handshake가 CPU를 꽤 먹거든요. 이걸 Nginx에게 떠넘기니까 백엔드는 비즈니스 로직만 처리하면 됐습니다.
# /etc/nginx/nginx.conf
upstream backend {
least_conn; # 연결 수가 가장 적은 서버로 분산
server 192.168.1.101:3000 weight=3; # Node 1 (가중치 3)
server 192.168.1.102:3000 weight=2; # Node 2 (가중치 2)
server 192.168.1.103:3000 weight=1; # Node 3 (가중치 1)
server 192.168.1.104:3000 backup; # Node 4 (백업 서버)
}
server {
listen 443 ssl http2;
server_name api.company.com;
# SSL 설정
ssl_certificate /etc/ssl/certs/company.crt;
ssl_certificate_key /etc/ssl/private/company.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://backend;
# 헤더 설정 (중요!)
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 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
이 설정에서 주목할 점:
X-Real-IP, X-Forwarded-For 등으로 원본 클라이언트 정보를 백엔드에 전달합니다.Reverse Proxy의 또 다른 활용: 마이크로서비스 라우팅
server {
listen 80;
server_name api.company.com;
# 사용자 서비스
location /api/users {
proxy_pass http://user-service:3001;
}
# 결제 서비스
location /api/payments {
proxy_pass http://payment-service:3002;
}
# 알림 서비스
location /api/notifications {
proxy_pass http://notification-service:3003;
}
}
클라이언트는 api.company.com만 알면 되고, Nginx가 내부적으로 적절한 마이크로서비스로 라우팅합니다. 이게 제가 받아들였던 핵심 개념입니다: Reverse Proxy는 서버 앞의 교통정리 담당자.
제가 만든 실제 아키텍처:
직원 PC → [회사 Forward Proxy] → 인터넷 → [AWS]
↓
[Nginx Reverse Proxy]
↓
[API Server 1, 2, 3]
시나리오 (요청 흐름):
이 구조의 장점:
처음 이 구조를 설계했을 때는 복잡해 보였지만, 실제로 운영해보니 각자의 역할이 명확히 분리되어서 오히려 관리가 쉬웠습니다.
| 항목 | Forward Proxy | Reverse Proxy |
|---|---|---|
| 위치 | 클라이언트 옆 | 서버 옆 |
| 숨기는 대상 | 클라이언트 IP | 서버 IP/구조 |
| 누가 설정 | 사용자/회사 | 서버 관리자 |
| 목적 | 접근 제어, 익명성, 우회 | 로드밸런싱, 보안, 캐싱 |
| 예시 | Squid, VPN, Tor, 사내망 | Nginx, HAProxy, CloudFlare, AWS ALB |
| 서버가 보는 IP | 프록시 IP | 실제 클라이언트 IP (헤더 통해) |
| 클라이언트가 보는 IP | 실제 서버 IP | 프록시 IP |
| 설정 위치 | 클라이언트 브라우저/OS | 서버 인프라 |
| 트래픽 방향 | 아웃바운드 제어 | 인바운드 제어 |
제가 정리해본 핵심: Forward는 나가는 문지기, Reverse는 들어오는 문지기.
서버 로그를 보면:
[2025-05-12 14:30:15] Access from 203.255.1.100 (회사 프록시 IP)
[2025-05-12 14:30:16] Access from 203.255.1.100 (회사 프록시 IP)
[2025-05-12 14:30:17] Access from 203.255.1.100 (회사 프록시 IP)
실제로는 직원 A (192.168.1.50), 직원 B (192.168.1.51), 직원 C (192.168.1.52)가 각자 접속했는데, 서버는 전부 같은 IP에서 온 것처럼 보입니다.
이게 Forward Proxy의 목적입니다: 클라이언트 신원 보호.
Nginx 설정:
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;
백엔드 로그:
Connection IP: 192.168.1.100 (Nginx IP)
X-Real-IP: 125.50.60.70 (실제 클라이언트 IP)
X-Forwarded-For: 125.50.60.70, 192.168.1.100
백엔드 코드 (Node.js):
// 실제 클라이언트 IP 가져오기
const getClientIP = (req) => {
return req.headers['x-real-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0] ||
req.connection.remoteAddress;
};
app.get('/api/users', (req, res) => {
const clientIP = getClientIP(req);
console.log(`Request from: ${clientIP}`); // 125.50.60.70
// IP 기반 Rate Limiting
const requestCount = ipRateLimiter.get(clientIP) || 0;
if (requestCount > 100) {
return res.status(429).json({ error: 'Too many requests' });
}
// ...비즈니스 로직
});
제가 처음 실수했던 부분이 바로 여기입니다. req.connection.remoteAddress만 찍으면 Nginx IP만 나와서, 모든 클라이언트가 동일한 IP에서 온 것처럼 보였습니다. Rate Limiting을 IP 기반으로 했더니 한 사용자가 요청을 많이 보내면 모든 사용자가 차단되는 문제가 생겼죠.
교훈: Reverse Proxy 뒤에서는 반드시 X-Forwarded-For 헤더를 체크해야 합니다.
더 복잡한 케이스:
Client → CloudFlare → AWS ALB → Nginx → Backend
이 경우 X-Forwarded-For:
X-Forwarded-For: 125.50.60.70, 104.16.1.2, 10.0.1.5, 192.168.1.100
(실제 Client) (CloudFlare) (ALB) (Nginx)
첫 번째 IP가 진짜 클라이언트 IP입니다. 이런 프록시 체인을 이해하고 나니까, 로그 분석이 훨씬 쉬워졌습니다.
공격자 (초당 10,000 요청) → Nginx (Reverse Proxy)
↓ (Rate Limiting 적용)
(초당 100개만 통과)
↓
백엔드 서버 (살아남음!)
Nginx 설정:
# Rate Limiting Zone 정의
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
server {
# 일반 페이지: 초당 10개
location / {
limit_req zone=one burst=20 nodelay;
proxy_pass http://backend;
}
# API: 초당 100개
location /api/ {
limit_req zone=api burst=200 nodelay;
proxy_pass http://backend;
}
}
파라미터 설명:
제가 실제로 DDoS 공격을 받았을 때, 이 설정 덕분에 백엔드 서버는 무사했습니다. Nginx가 초당 수천 개의 요청을 받았지만, 백엔드로는 초당 100개만 전달됐습니다.
server {
location / {
# SQL Injection 차단
if ($args ~* "union.*select|insert.*into|drop.*table") {
return 403 "Blocked: SQL Injection detected";
}
# XSS 차단
if ($args ~* "<script|javascript:|onerror=") {
return 403 "Blocked: XSS attempt detected";
}
# Path Traversal 차단
if ($uri ~* "\.\./") {
return 403 "Blocked: Path traversal detected";
}
proxy_pass http://backend;
}
}
이 설정으로 일주일에 평균 50건의 공격 시도를 막았습니다. 백엔드는 이런 악의적 요청을 아예 받지 않으니까 안전했습니다.
# 관리자 페이지는 사내 IP만 허용
location /admin {
allow 203.255.1.0/24; # 회사 IP 대역
allow 125.50.60.70; # 재택근무 IP
deny all;
proxy_pass http://backend;
}
이렇게 하면 관리자 페이지는 아예 인터넷에서 접근이 불가능합니다. 백엔드 코드에서 권한 체크 로직을 짜기 전에, 인프라 레벨에서 먼저 차단하는 겁니다.
# 캐시 경로 설정
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=my_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
location / {
proxy_cache my_cache;
# 캐싱 규칙
proxy_cache_valid 200 10m; # 200 응답: 10분
proxy_cache_valid 301 302 1h; # 리다이렉트: 1시간
proxy_cache_valid 404 1m; # 404: 1분
# 캐시 키
proxy_cache_key "$scheme$request_method$host$request_uri";
# 헤더 추가 (디버깅용)
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
# 특정 경로는 캐싱 제외
location /api/realtime {
proxy_cache off;
proxy_pass http://backend;
}
}
효과 (제 경험):
특히 정적 콘텐츠 (이미지, CSS, JS)를 Nginx에서 캐싱하니까 백엔드는 거의 쉬는 수준이었습니다.
콘텐츠 업데이트 시 캐시 삭제:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge my_cache "$scheme$request_method$host$1";
}
사용법:
# 캐시 삭제
curl -X PURGE http://localhost/purge/api/posts/123
이렇게 하면 콘텐츠를 업데이트한 후 즉시 캐시를 갱신할 수 있습니다.
사용자 → [ALB (Reverse Proxy)]
├→ Target Group 1: EC2 Instance 1, 2, 3
├→ Target Group 2: Lambda Function
└→ Target Group 3: ECS Container
AWS에서 ALB를 설정했을 때:
# Terraform 설정 예시
resource "aws_lb" "main" {
name = "company-alb"
load_balancer_type = "application"
subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.backend.arn
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
condition {
path_pattern {
values = ["/api/*"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
자동 기능:
제가 온프레미스 Nginx에서 AWS ALB로 마이그레이션했을 때, 관리 포인트가 확 줄었습니다. 특히 SSL 인증서를 수동으로 갱신하던 걸 ACM이 자동으로 해주니까 편했습니다.
사용자 → CloudFlare (전 세계 200+ 데이터센터)
↓ (캐싱, DDoS 방어, WAF)
내 작은 서버 (한국)
CloudFlare는 Reverse Proxy의 끝판왕입니다:
기능:
비용: 월 $0 (무료 플랜)
제가 CloudFlare를 붙이고 나서:
컨테이너 환경:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: company-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
rules:
- host: api.company.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 3000
- path: /payments
pathType: Prefix
backend:
service:
name: payment-service
port:
number: 3000
Kubernetes Ingress도 사실상 Reverse Proxy입니다. Nginx Ingress Controller를 쓰면 내부적으로 Nginx가 동작합니다.
처음엔 "왜 Forward/Reverse야?"가 궁금했습니다.
Forward (앞으로):
클라이언트가 앞으로 나가기 전에 대신 가줌. 마치 심부름꾼처럼, "내가 직접 갈게" 대신 "너가 대신 가줘".
Reverse (역방향):
서버로 들어오는 방향에서 대신 받아줌. 마치 경호원처럼, "나한테 직접 오지 마, 쟤한테 말해".
결국 이거였습니다: 프록시의 방향이 달랐던 겁니다.
처음 이 개념을 공부할 때는 추상적이고 어려웠지만, 지금은 Nginx 설정 파일만 봐도 "아, 이건 Reverse Proxy로 쓰는구나" 바로 이해됩니다. 실제로 써보니까 와닿았던 거죠.
핵심만 정리하면:
둘 다 "대리인"이지만, 누구를 위한 대리인이냐가 전부였습니다.