Proxy vs Reverse Proxy 완전 정리

정세현·2026년 7월 22일

NHN AX Internship Notes

목록 보기
11/16

백엔드 개발자 관점에서 정리한 문서. 개념 → 동작 → 설정 → 트러블슈팅 → 면접 순서.


0. 한 줄 요약

구분누구를 대신하나누가 설치하나상대방이 존재를 아는가
Forward Proxy클라이언트를 대신함클라이언트 쪽(회사, 개인)서버는 클라이언트가 누군지 모름
Reverse Proxy서버를 대신함서버 쪽(서비스 운영자)클라이언트는 뒤에 뭐가 있는지 모름

핵심 구분법: "이 프록시는 누구 편이고, 누가 돈 내고 설치했는가?"
클라이언트 편이면 forward, 서버 편이면 reverse. 끝.

proxy 라는 단어 자체가 "대리인"이라는 뜻이다. 대리인이 누구를 대리하느냐의 차이일 뿐이다.


1. Forward Proxy (정방향 프록시)

1.1 위치

[클라이언트] → [Forward Proxy] → (인터넷) → [서버]
   내부망          경계             ↑
                                서버가 보는 IP = 프록시 IP

1.2 동작

  1. 클라이언트가 "나는 프록시를 쓰겠다"고 명시적으로 설정한다.
    • 브라우저 프록시 설정
    • HTTP_PROXY=http://proxy.company.com:3128 환경변수
    • npm config set proxy, git config --global http.proxy
  2. 클라이언트는 목적지 서버가 아니라 프록시에게 요청을 보낸다.
  3. 프록시가 대신 서버에 요청하고, 응답을 클라이언트에게 전달한다.

중요: forward proxy는 클라이언트가 설정해야 동작한다. 클라이언트가 인지하고 있다는 게 reverse proxy와의 가장 큰 차이다.

1.3 HTTPS는 어떻게 처리하나

HTTP는 프록시가 내용을 다 볼 수 있지만, HTTPS는 암호화되어 있다. 그래서 CONNECT 메서드를 쓴다.

CONNECT api.github.com:443 HTTP/1.1
Host: api.github.com

프록시는 그냥 TCP 터널만 뚫어주고 암호화된 바이트를 그대로 중계한다. 내용은 못 본다.

내용까지 보려면 MITM(중간자) 프록시를 써야 하는데, 이 경우 프록시가 자체 CA 인증서로 가짜 인증서를 발급한다. 그래서 회사 노트북에 "사내 루트 인증서 설치"를 시키는 것이다. (Charles, Fiddler, mitmproxy 원리도 이것)

1.4 왜 쓰나

목적설명
접근 제어사내에서 특정 사이트 차단. 방화벽과 함께 조합
캐싱같은 리소스를 여러 직원이 받을 때 대역폭 절약
익명성서버가 실제 클라이언트 IP를 모름
로깅/감사누가 어디 접속했는지 기록
IP 우회지역 제한 우회, 크롤링 시 IP 로테이션
패키지 미러사내 Nexus/Artifactory로 npm/maven 요청 프록시

1.5 대표 소프트웨어

  • Squid — 전통적인 캐싱 프록시
  • mitmproxy — 디버깅용
  • Charles / Fiddler — 앱 개발 시 API 트래픽 확인
  • Zscaler, Blue Coat — 기업용 보안 프록시

1.6 VPN과의 차이

Forward ProxyVPN
계층L7 (주로 HTTP)L3 (네트워크 전체)
범위설정한 앱/프로토콜만기기의 모든 트래픽
암호화프록시 구간 암호화 안 될 수도터널 전체 암호화

2. Reverse Proxy (역방향 프록시)

2.1 위치

                                    ┌→ [App Server 1 :8080]
[클라이언트] → (인터넷) → [Reverse Proxy] → [App Server 2 :8080]
                          :80/:443    └→ [App Server 3 :8080]
                              ↑
                    클라이언트는 이게 "그 서버"인 줄 안다

클라이언트는 https://foliosage.cloud 에 접속했다고 생각한다. 실제로는 Caddy가 받아서 Docker 내부의 backend:8080으로 넘긴 것이지만, 클라이언트는 그걸 알 수 없고 알 필요도 없다.

2.2 왜 쓰나 (이게 진짜 중요)

(1) TLS Termination

HTTPS 복호화를 프록시가 담당한다. 애플리케이션은 평문 HTTP만 처리하면 된다.

클라이언트 --HTTPS--> [Caddy] --HTTP--> Spring Boot :8080

장점: 인증서 관리를 한 곳에서만. Spring Boot에 keystore 설정할 필요 없음. 인증서 갱신도 프록시만 재시작.

(2) 로드 밸런싱

여러 인스턴스에 요청 분배. 알고리즘: round-robin, least connections, ip-hash(세션 고정), weighted.

(3) 포트 은닉 / 단일 진입점

80/443만 열고 나머지는 다 닫는다. 애플리케이션 포트(8080, 5432 등)는 외부에 노출 안 됨.

(4) 경로/도메인 기반 라우팅

/api/*     → Spring Boot 백엔드
/          → Next.js 프론트엔드
admin.x.com → 관리자 서비스

MSA에서 필수.

(5) 정적 파일 서빙

이미지, JS, CSS를 애플리케이션까지 안 보내고 프록시가 바로 응답. 훨씬 빠르고 애플리케이션 부하 감소.

(6) 캐싱 / 압축

gzip, brotli 압축. 응답 캐싱.

(7) 보안

  • Rate limiting (DDoS 완화)
  • WAF (SQL injection, XSS 필터링)
  • 요청 크기 제한, 슬로우 로리스 방어
  • 실제 서버 구조를 외부에서 알 수 없음

(8) 무중단 배포

Blue-Green, Canary 배포 시 프록시에서 트래픽을 전환한다.

2.3 대표 소프트웨어

이름특징
Nginx사실상 표준. 설정 세밀, 성능 좋음, 자료 많음
Caddy자동 HTTPS(Let's Encrypt). 설정이 극단적으로 짧음
TraefikDocker/K8s 라벨 기반 자동 서비스 디스커버리
HAProxy로드밸런싱 특화, L4/L7 모두
EnvoyService Mesh(Istio)의 데이터 플레인
AWS ALB관리형. 오토스케일링과 통합
CloudflareCDN + 리버스 프록시 + WAF

3. 요청 흐름 상세 비교

Forward Proxy

1. 브라우저: "프록시 설정이 있네" → proxy.corp.com:3128 으로 연결
2. 브라우저 → 프록시: GET http://example.com/page HTTP/1.1   ← 절대 URL!
3. 프록시: 정책 검사(차단 목록?) → 캐시 확인 → 없으면 요청
4. 프록시 → example.com: GET /page HTTP/1.1
5. example.com이 보는 소스 IP = 프록시 IP
6. 응답 역순으로 전달

Reverse Proxy

1. 브라우저: DNS 조회 foliosage.cloud → 43.201.x.x (프록시 IP)
2. 브라우저 → 프록시: GET /api/users HTTP/1.1   ← 상대 경로
                      Host: foliosage.cloud
3. 프록시: TLS 복호화 → 라우팅 규칙 매칭 → 헤더 추가
4. 프록시 → backend:8080: GET /api/users HTTP/1.1
                          Host: foliosage.cloud
                          X-Forwarded-For: 121.x.x.x   ← 원래 클라이언트 IP
                          X-Forwarded-Proto: https
5. 백엔드 응답 → 프록시 → (압축/캐싱) → 브라우저

4. 실전 설정

4.1 Caddy (가장 간단)

foliosage.cloud {
    # 인증서 자동 발급/갱신. 이 한 줄로 HTTPS 완료
    reverse_proxy backend:8080
}

api.foliosage.cloud {
    reverse_proxy backend:8080 {
        health_uri /actuator/health
        health_interval 10s
    }
}

# 경로 기반 분기
example.com {
    handle /api/* {
        reverse_proxy backend:8080
    }
    handle {
        reverse_proxy frontend:3000
    }
}

Caddy는 X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host자동으로 붙여준다. Nginx처럼 일일이 안 써도 된다.

4.2 Nginx (세밀한 제어)

upstream backend {
    least_conn;                    # 연결 수 적은 쪽으로
    server app1:8080 max_fails=3 fail_timeout=30s;
    server app2:8080;
    keepalive 32;                  # 커넥션 재사용
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;   # HTTPS 강제
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 20M;      # 업로드 크기 제한

    location / {
        proxy_pass http://backend;

        # ★ 이 4줄이 핵심. 안 쓰면 백엔드가 진실을 모른다
        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 5s;
        proxy_read_timeout    60s;
    }

    # 정적 파일은 프록시가 직접 서빙
    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # WebSocket
    location /ws {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }

    # ★ SSE (스트리밍) — 버퍼링 끄지 않으면 응답이 안 흘러나온다
    location /api/chat/stream {
        proxy_pass http://backend;
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 3600s;
        chunked_transfer_encoding on;
    }
}

4.3 Docker Compose 조합

services:
  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    depends_on:
      - backend

  backend:
    build: .
    expose:
      - "8080"          # ports가 아니라 expose! 외부 노출 안 됨
    environment:
      SPRING_PROFILES_ACTIVE: prod

  db:
    image: postgres:16
    expose:
      - "5432"          # 외부에서 절대 접근 불가

volumes:
  caddy_data:

ports: "8080:8080" 로 열어두면 리버스 프록시를 둔 의미가 절반 사라진다. 공격자가 http://43.201.x.x:8080 으로 프록시를 우회할 수 있기 때문이다. expose를 쓰거나 127.0.0.1:8080:8080 으로 바인딩하자.


5. Forwarded 헤더 — 리버스 프록시의 최대 함정

리버스 프록시를 두는 순간, 백엔드는 원래 요청에 대한 진실을 잃는다.

백엔드가 착각하는 것실제해결 헤더
클라이언트 IP = 172.18.0.2 (프록시)121.x.x.xX-Forwarded-For
프로토콜 = httphttpsX-Forwarded-Proto
호스트 = backend:8080foliosage.cloudX-Forwarded-Host / Host
포트 = 8080443X-Forwarded-Port

5.1 X-Forwarded-For 체인

프록시가 여러 개면 콤마로 누적된다.

X-Forwarded-For: 121.140.1.5, 70.41.3.18, 150.172.238.178
                 ↑ 원래 클라이언트   ↑ 중간 프록시들

보안 주의: 클라이언트가 직접 X-Forwarded-For: 1.2.3.4 를 위조해서 보낼 수 있다. 그래서
1. 최외곽 프록시는 들어온 XFF를 덮어써야 한다 (X-Real-IP $remote_addr)
2. 신뢰하는 프록시 IP 목록(trusted proxies)을 설정하고, 그 뒤에서 오는 값만 믿는다

5.2 RFC 7239 표준 헤더

Forwarded: for=121.140.1.5; proto=https; host=example.com

표준이지만 현실에서는 X-Forwarded-* 가 압도적으로 많이 쓰인다.

5.3 Spring Boot 설정

server:
  forward-headers-strategy: NATIVE   # 또는 FRAMEWORK
동작언제
NATIVETomcat의 RemoteIpValve가 처리임베디드 톰캣 사용 시 (일반적)
FRAMEWORKSpring의 ForwardedHeaderFilter 사용서블릿 컨테이너 무관하게
NONE (기본)무시프록시 없을 때

설정 안 하면 이런 일이 생긴다:

request.getRemoteAddr();   // 172.18.0.1 (도커 게이트웨이) ← 로그가 전부 같은 IP
request.getScheme();       // "http" ← Spring Security가 https로 무한 리다이렉트
ServletUriComponentsBuilder.fromCurrentRequest();  // http://backend:8080/... ← 잘못된 링크

신뢰 프록시 제한:

server:
  tomcat:
    remoteip:
      remote-ip-header: X-Forwarded-For
      protocol-header: X-Forwarded-Proto
      internal-proxies: "172\\.1[6-9]\\.\\d{1,3}\\.\\d{1,3}"

6. Reverse Proxy vs Load Balancer vs API Gateway

셋 다 "앞단에 서서 뒤로 넘긴다"는 점은 같다. 관심사의 범위가 다르다.

     범위 좁음 ────────────────────────────────→ 범위 넓음
     L4 LB      Reverse Proxy(L7)     API Gateway
Load BalancerReverse ProxyAPI Gateway
계층L4(TCP) 또는 L7L7(HTTP)L7 + 애플리케이션
주 목적트래픽 분산서버 대리·보호API 관리
기능헬스체크, 분산+ TLS, 캐싱, 라우팅+ 인증/인가, 레이트리밋, 요청 변환, 응답 집계, API 버저닝, 사용량 과금
예시AWS NLB, HAProxyNginx, CaddySpring Cloud Gateway, Kong, AWS API Gateway

정리하면:

  • 모든 로드밸런서가 리버스 프록시는 아니다 (L4 LB는 HTTP를 이해 못함)
  • 모든 리버스 프록시는 로드밸런싱을 할 수 있다
  • API Gateway는 리버스 프록시 + 애플리케이션 관심사

MSA에서는 보통 이렇게 겹쳐서 쓴다:

[Client] → [ALB] → [Spring Cloud Gateway] → [각 마이크로서비스]
           L7 LB       API Gateway

7. 자주 겪는 트러블슈팅

① 무한 리다이렉트 루프

증상: ERR_TOO_MANY_REDIRECTS
원인: 프록시는 HTTPS로 받았는데 백엔드는 HTTP로 보여서, Spring Security가 "HTTPS로 가라"고 리다이렉트 → 무한 반복
해결: X-Forwarded-Proto 전달 + server.forward-headers-strategy=NATIVE

② 로그의 클라이언트 IP가 전부 172.x.x.x

원인: 도커 네트워크 게이트웨이 IP를 보고 있음
해결: XFF 헤더 전달 + forward-headers 설정

③ SSE / 스트리밍 응답이 안 나오고 한 번에 몰아서 옴

원인: 프록시가 응답을 버퍼링해서 모았다가 한 번에 보냄
해결:

proxy_buffering off;

또는 애플리케이션에서 응답 헤더 추가:

response.setHeader("X-Accel-Buffering", "no");

Spring AI로 LLM 응답을 SSE 스트리밍할 때 로컬에서는 되는데 배포하면 한 번에 나오는 문제, 대부분 이것이다.

④ WebSocket이 연결 안 됨 (400 Bad Request)

원인: Upgrade, Connection 헤더 미전달
해결: proxy_http_version 1.1 + Upgrade 헤더 설정

⑤ 504 Gateway Timeout

원인: 프록시의 proxy_read_timeout(기본 60초) < 애플리케이션 처리 시간
해결: 타임아웃을 프록시 > 애플리케이션 순으로 여유 있게 정렬. 근본적으로는 긴 작업을 비동기로 전환

⑥ 파일 업로드 시 413 Payload Too Large

해결: client_max_body_size 50M; (Nginx 기본 1MB) + Spring의 spring.servlet.multipart.max-file-size도 함께

⑦ CORS 에러가 계속 남

주의: 리버스 프록시로 프론트/백엔드를 같은 도메인에 묶으면 CORS 자체가 사라진다. 프록시를 두고도 CORS 설정을 이중으로 하면 헤더가 중복되어 오히려 에러가 난다.

⑧ 502 Bad Gateway

원인: 백엔드가 안 떠 있거나, 프록시가 백엔드 호스트명을 못 찾음
체크: docker compose ps, 컨테이너 이름과 프록시 설정의 upstream 이름 일치 여부, 같은 도커 네트워크에 있는지


8. 면접 대비 예상 질문

Q. 포워드 프록시와 리버스 프록시의 차이는?

프록시가 누구를 대리하느냐가 다릅니다. 포워드 프록시는 클라이언트를 대리해서 클라이언트가 명시적으로 설정하고, 서버는 실제 클라이언트를 알 수 없습니다. 리버스 프록시는 서버를 대리해서 서비스 운영자가 배치하고, 클라이언트는 뒤에 어떤 서버가 있는지 알 수 없습니다.

Q. 리버스 프록시를 왜 두나요?

TLS 종료로 인증서 관리를 일원화하고, 애플리케이션 포트를 외부에 노출하지 않아 공격 표면을 줄입니다. 또 경로 기반 라우팅과 로드밸런싱으로 무중단 배포와 수평 확장이 가능해집니다.

Q. 리버스 프록시를 두면 애플리케이션에서 뭐가 달라지나요?

원 요청 정보를 잃습니다. getRemoteAddr()가 프록시 IP를 반환하고, HTTPS로 들어와도 스킴이 http로 보입니다. X-Forwarded-* 헤더를 프록시가 붙이고 애플리케이션이 이를 신뢰하도록 설정해야 합니다. Spring Boot에서는 server.forward-headers-strategy로 처리합니다.

Q. X-Forwarded-For를 그냥 믿어도 되나요?

안 됩니다. 클라이언트가 위조할 수 있습니다. 최외곽 프록시가 값을 덮어쓰고, 신뢰 가능한 내부 프록시 IP 대역만 화이트리스트로 두고 그 뒤 값만 사용해야 합니다.

Q. 로드밸런서와 리버스 프록시의 차이는?

리버스 프록시가 상위 개념입니다. 로드밸런싱은 리버스 프록시의 기능 중 하나이고, L4 로드밸런서는 HTTP를 해석하지 않으므로 리버스 프록시라 부르기 어렵습니다.

Q. API Gateway는요?

리버스 프록시 기능에 인증/인가, 레이트 리밋, 요청·응답 변환, API 버저닝 같은 애플리케이션 관심사를 더한 것입니다. MSA에서 각 서비스가 공통 관심사를 중복 구현하지 않도록 앞단으로 뽑아냅니다.


9. 직접 해보기 체크리스트

  • mitmproxy 설치해서 포워드 프록시로 로컬 앱 트래픽 들여다보기
  • Docker Compose로 Nginx + Spring Boot 2대 띄우고 로드밸런싱 확인 (응답에 인스턴스 ID 찍기)
  • proxy_set_header 4줄을 지웠다 넣었다 하며 request.getRemoteAddr() 변화 관찰
  • Caddy로 자동 HTTPS 붙이고 curl -v 로 인증서 확인
  • proxy_buffering on/off 바꿔가며 SSE 응답 흐름 비교
  • 백엔드 컨테이너를 하나 죽여보고 헬스체크·페일오버 동작 확인
  • expose vs ports 차이를 nmap이나 외부에서 curl로 검증

10. 한 장 요약

                    ┌──────────────────────────────┐
                    │  프록시 = 대리인             │
                    └──────────────────────────────┘
                                 │
              ┌──────────────────┴──────────────────┐
              ▼                                      ▼
      FORWARD PROXY                          REVERSE PROXY
      클라이언트의 대리인                     서버의 대리인
              │                                      │
   클라이언트가 설정함                    운영자가 배치함
   서버는 클라이언트를 모름                클라이언트는 서버를 모름
              │                                      │
   용도:                                  용도:
   · 사내 접근 제어                        · TLS 종료
   · 캐싱, 대역폭 절약                     · 로드밸런싱
   · 익명성, IP 우회                       · 경로 라우팅
   · 트래픽 감사                           · 정적 파일 서빙
                                          · 보안(WAF, 레이트리밋)
   Squid, mitmproxy                        Nginx, Caddy, ALB
profile
I'm the best

0개의 댓글