Reverse Proxy: 서버 앞에서 요청을 중계하고 트래픽을 제어하는 역방향 프록시
역방향 프록시(Reverse Proxy)는 클라이언트 요청을 대신 받아 내부 서버로 전달하고 응답을 반환한다. 부하 분산(Load Balancing), 전송 계층 보안 종료(TLS Termination), 캐싱, 인증, 요청 제한 같은 공통 기능을 애플리케이션 서버 앞에서 처리한다. 클라이언트는 실제 서버의 주소와 구성을 알 필요 없이 역방향 프록시의 주소
By 송민성
1. 개념
역방향 프록시(Reverse Proxy)는 하나 이상의 서버 앞에서 클라이언트 요청을 받아 적절한 내부 서버로 전달하는 중간 시스템이다.
클라이언트 → 역방향 프록시 → 애플리케이션 서버
클라이언트 ← 역방향 프록시 ← 애플리케이션 서버클라이언트는 역방향 프록시를 실제 서버로 인식한다. 내부 애플리케이션 서버의 주소, 개수, 네트워크 구조는 외부에 직접 노출되지 않는다.
정방향 프록시(Forward Proxy)와는 대신하는 대상이 다르다.
| 구분 | 대신하는 대상 | 주된 목적 |
|---|---|---|
| 정방향 프록시(Forward Proxy) | 클라이언트 | 외부 접근 통제, 클라이언트 주소 은닉 |
| 역방향 프록시(Reverse Proxy) | 서버 | 부하 분산, 서버 보호, 공통 기능 처리 |
대표적인 역방향 프록시 구현에는 엔진엑스(NGINX), HAProxy, 아파치 HTTP 서버(Apache HTTP Server), 트래픽(Traefik), 클라우드 로드 밸런서가 있다.
2. 왜 사용하는가
여러 애플리케이션 서버가 각각 HTTPS, 인증, 로깅, 압축, 요청 제한을 처리하면 설정과 운영 비용이 증가한다. 역방향 프록시는 이런 공통 기능을 서버 앞의 단일 경계로 모은다.
내부 서버 은닉
외부에는 역방향 프록시 주소만 공개한다. 애플리케이션 서버는 사설 네트워크에 배치하고 프록시에서 들어오는 요청만 허용할 수 있다.
부하 분산
요청을 여러 업스트림 서버(Upstream Server)에 분산한다.
┌→ 서버 A
클라이언트 → 프록시 ├→ 서버 B
└→ 서버 C서버 한 대의 처리 용량을 넘는 트래픽을 여러 서버로 나눌 수 있다. 상태 검사(Health Check)에 실패한 서버를 요청 대상에서 제외할 수도 있다.
전송 계층 보안 종료
전송 계층 보안 종료(TLS Termination)는 프록시가 HTTPS 연결과 인증서를 관리하고 복호화된 요청을 내부 서버로 전달하는 구조다.
클라이언트 ── HTTPS ──> 프록시 ── HTTP 또는 HTTPS ──> 내부 서버인증서 갱신과 암호화 설정을 한곳에서 관리할 수 있다. 내부 구간에 민감한 데이터가 흐르거나 신뢰할 수 없는 네트워크를 통과한다면 내부 통신에도 HTTPS를 사용해야 한다.
캐싱과 압축
변경이 적은 응답을 프록시에 저장하면 동일한 요청을 애플리케이션이 반복 처리하지 않아도 된다. 응답 압축도 프록시에서 공통으로 적용할 수 있다.
접근 제어
인터넷 프로토콜 주소(IP Address), 요청 경로, 인증 정보, 요청 빈도를 기준으로 요청을 허용하거나 차단한다. 웹 애플리케이션 방화벽(Web Application Firewall)과 결합해 악성 요청을 검사하기도 한다.
3. 동작 원리
HTTP 역방향 프록시의 기본 흐름은 다음과 같다.
1. 클라이언트가 프록시에 요청
2. 프록시가 요청 정책 검사
3. 프록시가 업스트림 서버 선택
4. 프록시가 업스트림 서버에 새 요청 전송
5. 업스트림 서버가 프록시에 응답
6. 프록시가 클라이언트에 응답 전달프록시는 서로 독립된 두 연결을 관리한다.
클라이언트 ← 연결 A → 프록시 ← 연결 B → 업스트림 서버연결 B는 요청마다 새로 만들거나 연결 풀(Connection Pool)의 기존 연결을 재사용한다. 연결 재사용은 TCP 연결과 TLS 협상 비용을 줄인다.
업스트림 서버 선택
대표적인 부하 분산 알고리즘은 다음과 같다.
- 라운드 로빈(Round Robin): 서버를 순서대로 선택
- 최소 연결(Least Connections): 활성 연결이 가장 적은 서버 선택
- 해시(Hash): 클라이언트 주소나 쿠키 값을 해시해 서버 선택
- 가중 라운드 로빈(Weighted Round Robin): 서버별 처리 용량에 따라 요청 비율 조정
로그인 세션을 특정 서버 메모리에만 저장하면 같은 사용자를 동일한 서버로 보내는 세션 고정(Session Affinity)이 필요할 수 있다. 공유 세션 저장소나 무상태(Stateless) 구조를 사용하면 서버 선택 제약이 줄어든다.
요청 정보 전달
애플리케이션 서버에서 직접 확인되는 원격 주소는 클라이언트가 아니라 프록시 주소다. 원래 요청 정보는 다음 헤더로 전달한다.
Forwarded: 프록시 정보를 전달하는 표준 헤더X-Forwarded-For: 원래 클라이언트 인터넷 프로토콜 주소X-Forwarded-Proto: 원래 요청의http또는httpsX-Forwarded-Host: 클라이언트가 요청한 호스트Host: 업스트림 서버가 처리할 호스트
여러 프록시를 통과하면 X-Forwarded-For 값이 목록으로 누적된다.
X-Forwarded-For: 203.0.113.20, 10.0.0.5클라이언트도 이 헤더를 직접 전송할 수 있다. 애플리케이션은 신뢰할 수 있는 프록시가 검증하거나 추가한 값만 사용해야 한다.
스트리밍과 버퍼링
프록시는 요청과 응답을 두 방식으로 전달할 수 있다.
- 스트리밍(Streaming): 데이터가 도착하는 즉시 다음 연결로 전달
- 버퍼링(Buffering): 일정량 또는 전체 데이터를 저장한 뒤 전달
스트리밍은 첫 바이트 전달 시간을 줄이고 큰 응답에 필요한 메모리를 제한한다. 버퍼링은 느린 클라이언트로부터 업스트림 서버를 보호하고 재시도나 캐싱을 쉽게 만들지만 메모리 또는 디스크 사용량이 증가한다.
4. 코드 예제
다음 예제는 Node.js 표준 모듈로 업스트림 서버와 역방향 프록시를 함께 실행한다.
server.ts를 작성한다.
import http, {
IncomingHttpHeaders,
IncomingMessage,
ServerResponse,
} from "node:http";
const upstreamPort = 4000;
const proxyPort = 3000;
const upstream = http.createServer(
(request: IncomingMessage, response: ServerResponse) => {
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(
JSON.stringify({
server: "upstream",
method: request.method,
path: request.url,
forwardedFor: request.headers["x-forwarded-for"],
forwardedProto: request.headers["x-forwarded-proto"],
}),
);
},
);
upstream.listen(upstreamPort, "127.0.0.1", () => {
console.log(`업스트림 서버: http://127.0.0.1:${upstreamPort}`);
});
function createForwardHeaders(request: IncomingMessage): IncomingHttpHeaders {
const remoteAddress = request.socket.remoteAddress ?? "";
const previous = request.headers["x-forwarded-for"];
const forwardedFor = previous
? `${previous}, ${remoteAddress}`
: remoteAddress;
return {
...request.headers,
host: `127.0.0.1:${upstreamPort}`,
"x-forwarded-for": forwardedFor,
"x-forwarded-proto": request.socket.encrypted ? "https" : "http",
};
}
const proxy = http.createServer(
(clientRequest: IncomingMessage, clientResponse: ServerResponse) => {
const upstreamRequest = http.request(
{
hostname: "127.0.0.1",
port: upstreamPort,
method: clientRequest.method,
path: clientRequest.url,
headers: createForwardHeaders(clientRequest),
timeout: 5_000,
},
(upstreamResponse: IncomingMessage) => {
clientResponse.writeHead(
upstreamResponse.statusCode ?? 502,
upstreamResponse.headers,
);
upstreamResponse.pipe(clientResponse);
},
);
upstreamRequest.on("timeout", () => {
upstreamRequest.destroy(new Error("업스트림 요청 시간 초과"));
});
upstreamRequest.on("error", (error: Error) => {
if (!clientResponse.headersSent) {
clientResponse.writeHead(502, {
"content-type": "application/json; charset=utf-8",
});
}
clientResponse.end(
JSON.stringify({
error: "잘못된 게이트웨이",
message: error.message,
}),
);
});
clientRequest.pipe(upstreamRequest);
},
);
proxy.listen(proxyPort, "127.0.0.1", () => {
console.log(`역방향 프록시: http://127.0.0.1:${proxyPort}`);
});다음 명령으로 실행한다.
npm install --save-dev typescript tsx @types/node
npx tsx server.ts다른 터미널에서 프록시로 요청한다.
curl http://127.0.0.1:3000/users응답 예시는 다음과 같다.
{
"server": "upstream",
"method": "GET",
"path": "/users",
"forwardedFor": "::ffff:127.0.0.1",
"forwardedProto": "http"
}요청 본문과 응답 본문은 파이프(Pipe)로 연결되므로 전체 데이터를 메모리에 저장하지 않고 스트리밍한다. 실제 서비스에서는 홉 단위 헤더(Hop-by-Hop Header) 제거, 요청 크기 제한, 취소 전파, 재시도 정책, 보안 검증을 추가해야 한다.
엔진엑스에서는 같은 구조를 다음처럼 설정할 수 있다.
upstream application_servers {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://application_servers;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
}
}application_servers에 등록된 두 서버로 요청이 분산된다. proxy_connect_timeout은 연결 수립 제한 시간이고, proxy_read_timeout은 업스트림 응답을 기다리는 제한 시간이다.
5. 시간 복잡도 또는 성능 특성
역방향 프록시는 알고리즘보다 네트워크 입출력, 연결 관리, 암호화, 버퍼링 비용의 영향을 크게 받는다.
헤더 전체 크기를 H, 요청과 응답 본문 크기를 B라고 하면 요청 한 건을 검사하고 전달하는 기본 작업량은 O(H + B)다. 스트리밍 방식은 전체 본문을 복사해 보관하지 않으므로 본문 크기와 무관한 고정 크기 버퍼를 사용할 수 있다. 전체 버퍼링 방식은 요청당 최악 O(B)의 메모리나 디스크 공간을 사용할 수 있다.
라운드 로빈 방식의 서버 선택은 인덱스를 갱신하므로 요청당 O(1)이다. 최소 연결 방식도 우선순위 큐를 사용하면 서버가 n대일 때 선택과 갱신에 O(log n)이 걸릴 수 있다. 모든 서버의 연결 수를 매번 순회하는 단순 구현은 O(n)이다.
프록시를 추가하면 네트워크 경유와 처리 단계가 하나 늘어난다. 반면 다음 최적화로 전체 응답 시간을 줄일 수 있다.
- 연결 풀을 통한 TCP 및 TLS 연결 재사용
- 캐시 적중 시 업스트림 요청 제거
- 압축을 통한 전송량 감소
- 여러 서버로 요청 분산
- 정적 콘텐츠 직접 응답
처리량은 중앙 처리 장치(CPU), 메모리, 네트워크 대역폭, 동시 연결 수, TLS 연산량의 제한을 받는다. 프록시 한 계층의 용량을 초과하면 모든 서비스의 공통 병목이 된다.
6. 실무 사용 사례
웹 서비스 진입점
하나의 도메인으로 받은 요청을 경로에 따라 다른 서비스로 전달한다.
/api/users/* → 사용자 서비스
/api/orders/* → 주문 서비스
/assets/* → 정적 파일 서버무중단 배포
새 버전 서버를 준비한 뒤 프록시의 업스트림 대상을 변경한다. 블루-그린 배포(Blue-Green Deployment)에서는 기존 환경과 새 환경 사이의 트래픽을 전환한다. 카나리 배포(Canary Deployment)에서는 일부 요청만 새 버전으로 보낸다.
쿠버네티스 인그레스
쿠버네티스 인그레스(Ingress)나 게이트웨이 API(Gateway API)는 클러스터 외부 요청을 서비스로 라우팅한다. 실제 요청 전달은 인그레스 컨트롤러(Ingress Controller)나 게이트웨이 구현이 담당한다.
캐시 계층
이미지, 자바스크립트 파일, 조회 응답을 프록시에 캐싱한다. 캐시 적중 시 애플리케이션과 데이터베이스 부하가 줄어든다.
인증 경계
프록시가 토큰이나 세션을 검사한 뒤 인증된 요청만 내부 서비스로 전달한다. 서비스마다 같은 인증 로직을 반복하지 않아도 되지만, 서비스 내부의 권한 검사까지 모두 생략해서는 안 된다.
7. 주의할 점
X-Forwarded-For같은 전달 헤더를 무조건 신뢰하면 클라이언트가 인터넷 프로토콜 주소를 위조할 수 있다. 외부에서 받은 값을 제거하거나 신뢰 프록시 목록을 기준으로 해석해야 한다.- 업스트림 주소를 사용자 입력으로 자유롭게 선택하게 만들면 서버 측 요청 위조(SSRF)나 개방형 프록시(Open Proxy)로 악용될 수 있다. 전달 대상을 허용 목록으로 제한해야 한다.
- 연결, 읽기, 쓰기 제한 시간을 설정하지 않으면 느리거나 응답하지 않는 업스트림이 연결 자원을 계속 점유한다.
- 실패한 요청을 재시도할 때 멱등성(Idempotency)을 확인해야 한다. 처리 여부가 불명확한
POST요청을 자동 재시도하면 결제나 주문이 중복 실행될 수 있다. - 프록시가 단일 장애점(Single Point of Failure)이 되지 않도록 여러 인스턴스, 상태 검사, 장애 조치(Failover)를 구성해야 한다.
추가로 웹소켓(WebSocket), 서버 전송 이벤트(Server-Sent Events), 장기 실행 요청은 일반 HTTP 요청보다 긴 연결 시간을 사용한다. 프록시의 프로토콜 전환 헤더, 버퍼링, 유휴 제한 시간 설정을 별도로 확인해야 한다.
8. 핵심 정리
- 역방향 프록시는 서버 앞에서 요청을 받아 내부 업스트림 서버로 전달한다.
- 부하 분산, TLS 종료, 캐싱, 압축, 인증, 로깅을 공통 계층에서 처리한다.
- 클라이언트 연결과 업스트림 연결은 서로 독립적이며 연결 풀로 재사용할 수 있다.
- 전달 헤더는 신뢰할 수 있는 프록시가 관리한 값만 사용해야 한다.
- 제한 시간, 재시도, 상태 검사, 이중화 설정이 안정성과 장애 범위를 결정한다.