🌍 네트워크

프록시

Proxy

클라이언트와 서버 사이의 중개 서버. 포워드/리버스 프록시. 캐싱, 보안, 로드밸런싱에 활용.

📖 상세 설명

프록시(Proxy)는 "대리인"이라는 뜻으로, 네트워크에서 클라이언트와 서버 사이에 위치하여 요청과 응답을 중계하는 중간 서버입니다. 프록시는 단순히 트래픽을 전달하는 것을 넘어 캐싱, 보안 필터링, 로드밸런싱, 익명화, SSL 종료(SSL Termination) 등 다양한 기능을 수행합니다. 현대 웹 아키텍처에서 프록시는 성능 최적화와 보안 강화의 핵심 구성 요소입니다.

포워드 프록시(Forward Proxy)는 클라이언트 측에 위치하여 클라이언트를 대신해 외부 서버에 요청을 보냅니다. 기업 네트워크에서 인터넷 접근 통제, 콘텐츠 필터링, 사용자 활동 로깅에 사용됩니다. 클라이언트의 실제 IP를 숨겨 익명성을 제공하며, VPN과 유사한 역할을 합니다. 대표적으로 Squid, Privoxy 등이 있습니다.

리버스 프록시(Reverse Proxy)는 서버 측에 위치하여 클라이언트의 요청을 받아 뒤에 있는 실제 서버(Origin Server)로 전달합니다. 클라이언트는 리버스 프록시만 보고 실제 서버의 존재를 알지 못합니다. 로드밸런싱(여러 서버에 트래픽 분산), SSL 종료(프록시에서 HTTPS 처리), 캐싱(자주 요청되는 콘텐츠 저장), 압축, 보안(WAF 통합, DDoS 방어) 등 다양한 기능을 제공합니다. Nginx, HAProxy, Apache Traffic Server, Envoy 등이 대표적입니다.

API Gateway는 리버스 프록시의 특수한 형태로, 마이크로서비스 아키텍처에서 모든 API 요청의 단일 진입점 역할을 합니다. 인증/인가, 속도 제한(Rate Limiting), 요청 변환, API 버전 관리, 모니터링 등 API 특화 기능을 제공합니다. Kong, AWS API Gateway, Traefik 등이 있습니다. CDN(Content Delivery Network)도 글로벌 규모의 리버스 프록시로, 전 세계 엣지 서버에서 콘텐츠를 캐싱하여 사용자에게 가장 가까운 위치에서 응답합니다.

💻 코드 예제

Nginx 리버스 프록시 설정

# /etc/nginx/nginx.conf 또는 /etc/nginx/conf.d/app.conf

# 업스트림 서버 그룹 정의 (로드밸런싱)
upstream backend_servers {
    # 라운드 로빈 (기본)
    server 10.0.1.10:3000 weight=5;   # 가중치 높음
    server 10.0.1.11:3000 weight=3;
    server 10.0.1.12:3000 backup;     # 백업 서버

    # 연결 유지 설정 (Keep-alive)
    keepalive 32;
}

# API 서버 그룹 (최소 연결 방식)
upstream api_servers {
    least_conn;  # 연결 수가 가장 적은 서버로 라우팅
    server 10.0.2.10:8080;
    server 10.0.2.11:8080;
}

# HTTP 서버 블록
server {
    listen 80;
    server_name example.com www.example.com;

    # HTTPS 리다이렉트
    return 301 https://$server_name$request_uri;
}

# HTTPS 서버 블록
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL 인증서
    ssl_certificate /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # SSL 보안 설정
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    # 보안 헤더
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Gzip 압축
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    gzip_min_length 1000;

    # 정적 파일 캐싱
    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # API 요청 프록시
    location /api/ {
        proxy_pass http://api_servers;
        proxy_http_version 1.1;
        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;

        # 버퍼링 설정
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }

    # WebSocket 프록시
    location /ws/ {
        proxy_pass http://backend_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400;  # 24시간
    }

    # 기본 요청 프록시
    location / {
        proxy_pass http://backend_servers;
        proxy_http_version 1.1;
        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_set_header Connection "";

        # 캐싱 설정
        proxy_cache my_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502;
        add_header X-Cache-Status $upstream_cache_status;
    }

    # 헬스체크 엔드포인트
    location /health {
        access_log off;
        return 200 "OK";
        add_header Content-Type text/plain;
    }
}

# 캐시 영역 정의 (http 블록에 추가)
# proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m inactive=60m max_size=1g;

Node.js에서 프록시 서버 구현 (http-proxy)

import http from 'http';
import httpProxy from 'http-proxy';
import { URL } from 'url';

// 프록시 서버 생성
const proxy = httpProxy.createProxyServer({
  changeOrigin: true,       // 호스트 헤더 변경
  ws: true,                 // WebSocket 지원
  xfwd: true,               // X-Forwarded-* 헤더 추가
  proxyTimeout: 30000,      // 30초 타임아웃
  timeout: 30000,
});

// 에러 핸들링
proxy.on('error', (err, req, res) => {
  console.error('Proxy error:', err);
  if (res.writeHead) {
    res.writeHead(502, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Bad Gateway', message: err.message }));
  }
});

// 프록시 요청 로깅
proxy.on('proxyReq', (proxyReq, req, res, options) => {
  console.log(`[PROXY] ${req.method} ${req.url} -> ${options.target}`);

  // 커스텀 헤더 추가
  proxyReq.setHeader('X-Proxy-Timestamp', Date.now().toString());
});

// 프록시 응답 수정
proxy.on('proxyRes', (proxyRes, req, res) => {
  // 응답 헤더 추가
  res.setHeader('X-Proxied-By', 'my-proxy-server');

  console.log(`[RESPONSE] ${req.url} - ${proxyRes.statusCode}`);
});

// 라우팅 규칙 정의
interface RouteConfig {
  prefix: string;
  target: string;
  rewrite?: (path: string) => string;
}

const routes: RouteConfig[] = [
  {
    prefix: '/api/users',
    target: 'http://user-service:3001',
    rewrite: (path) => path.replace('/api/users', '/users'),
  },
  {
    prefix: '/api/orders',
    target: 'http://order-service:3002',
    rewrite: (path) => path.replace('/api/orders', '/orders'),
  },
  {
    prefix: '/api/products',
    target: 'http://product-service:3003',
  },
  {
    prefix: '/',
    target: 'http://frontend:3000',
  },
];

// 경로에 맞는 라우트 찾기
function findRoute(path: string): RouteConfig | undefined {
  return routes.find(route => path.startsWith(route.prefix));
}

// HTTP 서버 생성
const server = http.createServer((req, res) => {
  const route = findRoute(req.url || '/');

  if (!route) {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Route not found' }));
    return;
  }

  // 경로 재작성
  if (route.rewrite && req.url) {
    req.url = route.rewrite(req.url);
  }

  // 프록시 실행
  proxy.web(req, res, { target: route.target });
});

// WebSocket 업그레이드 처리
server.on('upgrade', (req, socket, head) => {
  const route = findRoute(req.url || '/');

  if (route) {
    proxy.ws(req, socket, head, { target: route.target });
  } else {
    socket.destroy();
  }
});

const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Proxy server running on port ${PORT}`);
});

Python에서 requests로 프록시 사용

import requests
from requests.auth import HTTPProxyAuth

# 1. 기본 HTTP/HTTPS 프록시 설정
proxies = {
    'http': 'http://proxy.example.com:8080',
    'https': 'http://proxy.example.com:8080',
}

response = requests.get(
    'https://api.example.com/data',
    proxies=proxies,
    timeout=30
)
print(response.json())

# 2. 인증이 필요한 프록시
proxies_with_auth = {
    'http': 'http://username:password@proxy.example.com:8080',
    'https': 'http://username:password@proxy.example.com:8080',
}

# 또는 HTTPProxyAuth 사용
auth = HTTPProxyAuth('username', 'password')
response = requests.get(
    'https://api.example.com/data',
    proxies={'http': 'http://proxy.example.com:8080'},
    auth=auth
)

# 3. SOCKS 프록시 (pip install requests[socks] 필요)
socks_proxies = {
    'http': 'socks5://localhost:1080',
    'https': 'socks5://localhost:1080',
}

response = requests.get(
    'https://api.example.com/data',
    proxies=socks_proxies
)

# 4. Session을 사용한 프록시 설정 (연결 재사용)
session = requests.Session()
session.proxies.update({
    'http': 'http://proxy.example.com:8080',
    'https': 'http://proxy.example.com:8080',
})

# 여러 요청에 동일한 프록시 사용
response1 = session.get('https://api.example.com/users')
response2 = session.get('https://api.example.com/orders')

# 5. 환경 변수로 프록시 설정
# export HTTP_PROXY=http://proxy.example.com:8080
# export HTTPS_PROXY=http://proxy.example.com:8080
# export NO_PROXY=localhost,127.0.0.1,.internal.com

import os
os.environ['HTTP_PROXY'] = 'http://proxy.example.com:8080'
os.environ['HTTPS_PROXY'] = 'http://proxy.example.com:8080'

# requests는 자동으로 환경 변수 사용
response = requests.get('https://api.example.com/data')

# 6. 특정 요청에서 프록시 비활성화
response = requests.get(
    'https://internal.example.com/data',
    proxies={'http': None, 'https': None}  # 프록시 무시
)

🗣️ 실무에서 이렇게 말해요

  • "앞단에 Nginx 리버스 프록시 두고 SSL 처리하면 백엔드는 HTTP로만 통신해도 돼요"
  • "로드밸런서를 L7 프록시로 구성해서 /api는 API 서버로, /static은 CDN으로 라우팅하고 있어요"
  • "사내망에서 외부 API 호출할 때 포워드 프록시 거쳐야 해요. 보안팀에서 outbound 트래픽 모니터링해요"
  • "API Gateway 앞에 CloudFlare 프록시 붙여서 DDoS 방어하고 있어요. Rate Limiting도 거기서 처리하고요"
  • "포워드 프록시는 클라이언트 측에서 외부 서버로의 요청을 대신 보내고, 리버스 프록시는 서버 측에서 클라이언트 요청을 받아 내부 서버로 전달합니다."
  • "리버스 프록시의 주요 용도는 로드밸런싱, SSL 종료, 캐싱, 압축, 보안(WAF, IP 숨김) 등입니다."
  • "SSL Termination은 프록시에서 HTTPS를 해독하고 백엔드와는 HTTP로 통신하는 방식입니다. 백엔드 서버의 암호화 부담을 줄여줍니다."
  • "Nginx의 X-Forwarded-For 헤더를 통해 프록시를 거치더라도 원래 클라이언트 IP를 백엔드에서 확인할 수 있습니다."
  • "proxy_set_header Host 설정 없으면 백엔드에서 원래 호스트 정보 못 받아요. 가상 호스트 라우팅 안 될 수 있어요"
  • "WebSocket 프록시할 때 Upgrade 헤더 전달하고 Connection: upgrade 설정해야 해요. 안 하면 101 Switching 실패해요"
  • "proxy_read_timeout 기본값이 60초인데, 오래 걸리는 API는 이거 늘려줘야 504 Gateway Timeout 안 나요"
  • "upstream 블록에 keepalive 설정해서 백엔드 연결 재사용하면 성능 많이 올라요"

⚠️ 주의사항

🔗 관련 용어

📚 더 배우기