컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 17

포워드 / 리버스 프록시

인프라리버스프록시프록시

웹사이트에 접속할 때 클라이언트가 항상 서버와 직접 연결되는 것은 아닙니다. 중간에 요청을 대신 받아 다른 쪽으로 전달하고 응답을 돌려주는 서버가 끼어 있는 경우가 많고, 이 중계 서버를 프록시(proxy)라고 합니다. 프록시는 요청과 응답을 중간에서 보고, 걸러 내고, 저장하고, 바꿀 수 있는 위치에 있어서 보안, 성능, 관리 면에서 여러 가지 일을 맡습니다. 프록시는 어느 편을 대신하느냐에 따라 크게 두 가지로 나뉩니다.

포워드 프록시(forward proxy)는 클라이언트 쪽에 놓여 클라이언트를 대신해 외부 서버에 요청을 보냅니다. 회사 사내망에서 인터넷으로 나가는 요청을 한곳으로 모아 통제하는 구성이 대표적입니다. 리버스 프록시(reverse proxy)는 서버 쪽에 놓여 서버를 대신해 클라이언트의 요청을 받습니다. 클라이언트에게는 프록시가 곧 서버로 보이고, 요청을 실제로 처리하는 백엔드 서버는 그 뒤에 숨어 있습니다. 이 토픽에서는 포워드 프록시의 중계와 캐시, 리버스 프록시의 분산과 TLS 종료, 두 프록시의 차이를 단계별로 봅니다.

핵심 개념

프록시

요청과 응답을 중간에서 대신 전달하는 서버입니다. 양쪽 모두 프록시와 연결을 맺고, 프록시가 반대편과 따로 연결을 맺어 메시지를 이어 줍니다.

핵심 개념

포워드 프록시

클라이언트의 대리인입니다. 보통 클라이언트가 프록시를 지정해 요청을 보내면, 프록시가 외부 서버에 대신 요청합니다. 접속 제어, 캐시, 사용 기록에 쓰입니다.

핵심 개념

리버스 프록시

서버의 대리인입니다. 클라이언트는 프록시 주소로만 접속하고, 프록시가 내부의 백엔드 서버로 요청을 전달합니다. nginx와 HAProxy가 대표적인 리버스 프록시 소프트웨어입니다.

핵심 개념

TLS 종료

리버스 프록시가 HTTPS 연결을 대신 끝맺고 요청을 복호화하는 방식입니다. 인증서를 프록시에만 두면 되어 관리가 쉽고, 백엔드는 암호화 부담을 덜 수 있습니다. 프록시와 백엔드 사이가 평문이 되므로 사설망 안에서 쓰는 것이 전제입니다.

핵심 개념

캐싱

프록시가 응답을 저장해 두었다가, 같은 요청이 오면 저장본이 아직 유효할 때 원래 서버에 묻지 않고 저장본으로 답합니다. 두 종류의 프록시 모두 응답 속도를 높이고 서버 부하를 줄이는 데 씁니다.

핵심 개념

원본 주소 전달 헤더

프록시를 거치면 백엔드에는 프록시의 주소만 보입니다. 원래 클라이언트 주소는 X-Forwarded-For나 표준 Forwarded 헤더에 담아 전달하는 것이 일반적입니다.

누구의 대리인인지가 구분 기준입니다 — 같은 Squid도 사내 클라이언트 앞에서는 포워드, 웹 서버 앞에서는 리버스(accelerator 모드) 프록시로 쓸 수 있습니다. 소프트웨어가 아니라 놓인 위치와 역할로 나뉩니다.
포워드는 밖을 향한 요청을 모읍니다 — 모든 외부 요청이 프록시를 지나므로 접속 규칙, 캐시, 기록을 한곳에서 관리할 수 있습니다. 외부 서버에는 프록시의 주소가 보입니다.
리버스는 서버 앞의 관문입니다 — 백엔드를 늘리거나 바꿔도 클라이언트가 쓰는 주소는 그대로이고, 부하 분산, TLS 종료, 캐시, 서버 보호를 프록시에서 맡습니다.
원본 주소는 헤더로 이어 줍니다 — 프록시가 요청을 새로 만들어 보내기 때문에 백엔드는 원래 클라이언트를 알 수 없습니다. 헤더로 전달하되, 신뢰할 수 있는 프록시가 넣은 값만 믿어야 합니다.
이 시각화에서 확인할 것 — "포워드 프록시" 탭에서 프록시가 요청을 대신 보내고 캐시와 차단을 처리하는 과정을, "리버스 프록시" 탭에서 TLS 종료와 분산을, "비교" 탭에서 두 프록시의 차이를 STEP으로 확인하세요.

프록시의 배경

HTTP 명세(RFC 9110)는 프록시를 클라이언트가 보통 설정으로 골라 쓰는 메시지 중계자로, 게이트웨이(리버스 프록시)를 바깥쪽에서는 원래 서버처럼 행동하는 중계자로 구분합니다. RFC 9110은 프록시를 조직의 HTTP 요청을 한곳으로 모아 보안과 공유 캐시를 맡기는 데 쓴다고, 게이트웨이를 서버 성능 개선과 여러 서버로의 분산에 쓴다고 설명합니다. 많은 웹 서비스가 앞단에 리버스 프록시나 로드 밸런서를 두고, CDN도 넓게 보면 전 세계에 분산된 리버스 프록시입니다.

숫자로 확인하기

포워드 프록시 시각화 — 사내 클라이언트 10.0.0.5가 보낸 요청 3개 가운데 외부 서버에 도달한 것은 1개입니다. 나머지는 캐시 적중 1개와 차단 도메인 1개를 프록시가 처리했습니다.
서버가 보는 주소 — 포워드 프록시를 거친 요청의 출발지는 클라이언트의 10.0.0.5가 아니라 프록시의 외부 주소 203.0.113.5입니다.
리버스 프록시 시각화 — 요청 3개 가운데 백엔드에 도달한 것은 2개입니다. 라운드 로빈으로 백엔드 A와 B에 1개씩 가고, 정적 파일 요청 1개는 프록시 캐시가 처리합니다.
기본 포트 — Squid 설정 파일의 기본 예시 포트는 3128(http_port 3128)이고, 리버스 프록시는 보통 HTTPS의 443 포트에서 요청을 받아 백엔드로 넘깁니다.

자주 하는 오해

프록시는 익명성을 보장한다 — 프록시가 원래 주소를 헤더로 알리지 않는다면 서버에는 프록시 주소가 보이지만, 프록시 운영자는 요청의 목적지를 볼 수 있고, 평문 HTTP라면 내용도 볼 수 있습니다(HTTPS는 터널 안의 내용을 읽지 못합니다). 완전한 익명이 되는 것은 아닙니다.
리버스 프록시는 로드 밸런서와 같다 — L7 로드 밸런서는 리버스 프록시가 요청 분산을 맡는 형태이고, L4 로드 밸런서는 연결 단위로 분배하는 방식도 있습니다. 리버스 프록시는 TLS 종료, 캐시, 헤더 처리 같은 일도 함께 하며, 같은 소프트웨어가 두 역할을 겸하는 경우가 많습니다.
X-Forwarded-For는 항상 믿을 수 있다 — 클라이언트가 이 헤더를 직접 넣어 보낼 수 있습니다. 신뢰하는 프록시가 덧붙인 값만 쓰고, 그 앞의 값은 위조될 수 있다고 봐야 합니다.
프록시를 쓰면 항상 빨라진다 — 캐시에 적중하면 빨라지지만, 중간에 한 단계가 더 끼므로 캐시를 쓸 수 없는 요청은 지연이 조금 늘어납니다. 프록시 자체가 병목이 되지 않게 구성해야 합니다.

면접에서 이렇게 물어요

면접 질문

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

포워드 프록시는 클라이언트를 대신해 외부 서버에 요청을 보내고, 리버스 프록시는 서버를 대신해 클라이언트의 요청을 받습니다. 앞은 보통 클라이언트가 프록시를 지정하고(관리자가 가로채는 투명 프록시도 있습니다), 뒤는 클라이언트가 존재를 몰라도 됩니다.

면접 질문

리버스 프록시를 쓰면 무엇이 좋나요?

부하 분산, TLS 종료, 정적 응답 캐시, 내부 서버 구조의 은닉 같은 이점이 있습니다. 서버를 추가하거나 교체해도 클라이언트는 같은 주소로 접속합니다.

면접 질문

프록시 뒤의 서버는 클라이언트 IP를 어떻게 알죠?

프록시가 요청을 새로 보내므로 서버에는 프록시의 주소가 출발지로 보입니다. 그래서 프록시가 X-Forwarded-For나 Forwarded 헤더에 원래 주소를 담아 전달하고, 서버는 신뢰하는 프록시가 넣은 값만 사용합니다.

면접 질문

프록시와 VPN은 어떻게 다른가요?

프록시는 보통 특정 응용(HTTP 등)의 요청을 대신 전달하고, IPsec 같은 VPN은 IP 계층에서 트래픽을 암호화된 터널로 보냅니다(SSL VPN처럼 다른 방식도 있습니다). 프록시는 보통 그 자체로 구간을 암호화하지는 않습니다.

실제 시스템에서는

웹 서비스 앞단 — nginx, HAProxy, Envoy 같은 소프트웨어를 리버스 프록시로 두고 TLS 종료, 라우팅, 분산을 맡깁니다.
기업 사내망 — Squid 같은 포워드 프록시로 외부 접속을 통제하고 기록하며, 허용된 사이트만 열어 두기도 합니다.
쿠버네티스 인그레스 — 클러스터 밖의 HTTP와 HTTPS 요청을 호스트 이름과 경로 규칙에 따라 안쪽 서비스로 보내는 규칙(API 객체)이며, 실제 전달은 nginx 같은 인그레스 컨트롤러가 합니다.
HTTPS와 프록시 — 포워드 프록시로 HTTPS 사이트에 접속할 때는 CONNECT 메서드로 프록시가 서버까지 터널을 열어 주고, 프록시는 TLS를 가로채지 않는 한 그 안의 암호화된 내용을 읽지 못합니다(조직이 인증서를 심어 복호화하는 구성은 예외입니다).

함께 보면 좋은 토픽

로드 밸런서 — 리버스 프록시가 맡는 요청 분산의 방식을 봅니다.
HTTPS / TLS — 프록시가 끝맺는 TLS 연결의 원리를 봅니다.
CDN — 전 세계에 분산된 캐시 서버를 리버스 프록시의 확장으로 봅니다.
방화벽 패킷 필터링 — 프록시와 함께 접근을 통제하는 장치를 봅니다.

참고 자료

Squid — 포워드 프록시 설정과 사용

# squid.conf: 사내망(10.0.0.0/8)만 프록시를 쓰게 한다
http_port 3128

acl localnet src 10.0.0.0/8
acl blocked dstdomain .blocked.example

http_access deny blocked         # 차단 목록을 먼저 검사
http_access allow localnet
http_access deny all             # 나머지는 모두 거부

# 클라이언트에서 프록시를 거쳐 요청하기
curl -x http://10.0.0.1:3128 http://example.com/ -I

Squid는 대표적인 포워드 프록시 소프트웨어로, http_port로 프록시가 요청을 받을 포트를 정합니다. http_access 규칙은 위에서부터 차례로 검사해 처음 일치한 규칙이 적용되므로, 차단 목록(blocked)을 허용 규칙보다 먼저 두어야 차단이 먹힙니다. 마지막 줄의 deny all은 앞의 규칙에 일치하지 않은 요청을 모두 거부하는 규칙이며, 이 줄이 없으면 Squid는 마지막 규칙의 반대 동작을 기본으로 삼아 의도와 다르게 동작할 수 있습니다. 시각화의 포워드 프록시 탭에서 차단 도메인 요청이 프록시에서 거절되는 것과 같은 동작입니다. curl의 -x 옵션은 요청을 지정한 프록시로 보내며, 이때 요청 줄에는 전체 URL이 담겨 프록시가 어느 서버로 전달할지 알 수 있습니다.

nginx — 리버스 프록시와 로드 밸런싱

upstream backend {
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

server {
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/ssl/example.crt;
    ssl_certificate_key /etc/ssl/example.key;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

upstream 블록은 요청을 나눠 보낼 백엔드 서버 묶음이고, 별도 설정이 없으면 nginx는 요청을 서버에 번갈아 분배하는 라운드 로빈을 씁니다. server 블록은 443 포트에서 인증서로 HTTPS 연결을 끝맺고(TLS 종료), proxy_pass로 요청을 백엔드에 HTTP로 넘깁니다. 프록시를 거치면 백엔드는 모든 요청이 프록시에서 온 것으로 보게 됩니다. 그래서 proxy_set_header로 원래의 Host와, 클라이언트 주소를 담는 X-Forwarded-For, 원래의 접속 방식(http 또는 https)을 담는 X-Forwarded-Proto 헤더를 넘겨 줍니다. X-Forwarded-For는 표준 헤더가 아니라 널리 쓰이는 관례이므로, 백엔드는 신뢰할 수 있는 프록시가 넣은 값만 믿어야 합니다.

Python — 라운드 로빈 분배와 원본 주소 전달

from itertools import cycle

BACKENDS = cycle(["10.0.1.11:8080", "10.0.1.12:8080"])

def route(client_ip, headers):
    backend = next(BACKENDS)                   # 서버를 번갈아 고른다
    out = dict(headers)
    out["X-Forwarded-For"] = client_ip          # 원래 클라이언트 주소를 전달
    return backend, out

for i in range(3):
    print(route("198.51.100.7", {"Host": "www.example.com"}))
# 10.0.1.11, 10.0.1.12, 10.0.1.11 순서로 선택

itertools.cycle은 목록을 끝없이 되풀이하는 반복자여서, next를 부를 때마다 백엔드 A, B, A 순서로 고르는 라운드 로빈을 한 줄로 흉내 냅니다. 시각화의 리버스 프록시 탭에서 요청 1이 A로, 요청 2가 B로 가는 것과 같은 동작입니다. route 함수는 선택한 백엔드와 함께 원래 클라이언트 주소를 X-Forwarded-For 헤더에 담아 돌려줍니다. 실제 프록시에는 이미 들어 있는 X-Forwarded-For에 주소를 덧붙이는 처리가 필요합니다. 실제 프록시는 서버의 상태 검사, 연결 재사용, 실패 시 재시도, 가중치를 함께 다루므로 이 코드는 분배 원리만 보이는 최소 예시입니다.

Q1

포워드 프록시(Forward Proxy)의 역할은?

클라이언트의 대리인입니다.
포워드 프록시는 클라이언트를 대신해 외부 서버에 요청을 보냅니다. 클라이언트의 IP를 숨기거나, 접속 제어와 캐싱을 하는 데 쓰입니다.

Q2

리버스 프록시(Reverse Proxy)의 역할은?

클라이언트는 실제 서버를 직접 보지 못합니다.
리버스 프록시는 서버 측에 위치해 클라이언트 요청을 받아 내부의 실제 서버로 전달하고 응답을 돌려줍니다. 클라이언트는 프록시만 보기 때문에 내부 서버 구조가 숨겨집니다.

Q3

리버스 프록시가 제공할 수 있는 기능의 예로 알맞은 것은?

서버 앞단에서 대신 처리해 주면 좋은 일들입니다.
리버스 프록시는 여러 서버로 요청을 분산(로드 밸런싱)하고, HTTPS 암호화/복호화를 대신 처리(TLS 종료)하며, 자주 요청되는 응답을 캐싱해 서버 부하를 줄입니다.