본문 바로가기
TYLER SONGBlog
← Blog index
CS9 min read

Proxy: 클라이언트와 서버 사이에서 요청을 중계하는 네트워크 대리자

프록시(Proxy)는 클라이언트와 서버 사이에서 요청과 응답을 대신 전달한다. 정방향 프록시(Forward Proxy)는 클라이언트를, 역방향 프록시(Reverse Proxy)는 서버를 대신한다. 접근 제어, 캐싱, 부하 분산, 전송 계층 보안(TLS) 종료, 관측성 확보에 활용된다.

By 송민성

1. 개념

프록시(Proxy)는 클라이언트의 요청을 받아 대상 서버에 전달하고, 서버의 응답을 다시 클라이언트에 전달하는 중간 시스템이다.

text
클라이언트 → 프록시 → 대상 서버
클라이언트 ← 프록시 ← 대상 서버

프록시는 어느 쪽을 대신하느냐에 따라 구분한다.

정방향 프록시

정방향 프록시(Forward Proxy)는 클라이언트를 대신한다. 대상 서버는 실제 클라이언트가 아니라 프록시를 요청 주체로 인식한다.

text
사내 클라이언트들 → 정방향 프록시 → 인터넷 서버

주요 목적은 다음과 같다.

  • 외부 사이트 접근 제어
  • 클라이언트 인터넷 프로토콜 주소(IP Address) 은닉
  • 악성 사이트 차단
  • 요청 기록과 감사
  • 반복 응답 캐싱

역방향 프록시

역방향 프록시(Reverse Proxy)는 서버를 대신한다. 클라이언트는 실제 애플리케이션 서버의 주소와 개수를 알지 못한 채 프록시에 요청한다.

text
인터넷 클라이언트 → 역방향 프록시 → 애플리케이션 서버들

주요 목적은 다음과 같다.

  • 부하 분산(Load Balancing)
  • 전송 계층 보안 종료(TLS Termination)
  • 캐싱
  • 압축
  • 인증과 요청 제한
  • 내부 서버 구조 은닉

프록시는 요청을 중계하는 역할을 나타내는 넓은 개념이다. 게이트웨이(Gateway)는 서로 다른 네트워크나 프로토콜 사이를 연결한다는 의미가 강하지만, 실제 제품에서는 두 용어가 겹쳐 사용되기도 한다.

2. 왜 사용하는가

클라이언트가 모든 서버에 직접 연결하면 보안 정책, 인증, 로깅, 인증서 관리 같은 기능을 서버마다 구현해야 한다. 프록시는 공통 기능을 하나의 경계에 모은다.

예를 들어 여러 애플리케이션 서버 앞에 역방향 프록시를 두면 다음 작업을 중앙에서 처리할 수 있다.

  • HTTPS 연결을 종료하고 내부 서버에는 HTTP로 전달한다.
  • 요청을 여러 서버에 분산한다.
  • 정적 파일이나 조회 응답을 캐싱한다.
  • 특정 인터넷 프로토콜 주소나 경로의 접근을 차단한다.
  • 요청 수, 지연 시간, 응답 상태 코드를 기록한다.
  • 장애가 발생한 서버를 요청 대상에서 제외한다.

정방향 프록시는 조직 내부의 외부 통신 정책을 통제할 때 사용한다. 모든 외부 요청이 프록시를 거치게 하면 사용자별 접근 정책과 감사 로그를 한곳에서 관리할 수 있다.

3. 동작 원리

HTTP 역방향 프록시의 기본 흐름은 다음과 같다.

text
1. 클라이언트가 프록시에 HTTP 요청 전송
2. 프록시가 요청 대상과 정책 확인
3. 프록시가 업스트림(Upstream) 서버 선택
4. 요청 헤더와 본문을 업스트림 서버에 전달
5. 업스트림 서버가 프록시에 응답
6. 프록시가 응답 헤더와 본문을 클라이언트에 전달

프록시는 서로 독립된 두 연결을 관리한다.

text
클라이언트 ← 연결 A → 프록시 ← 연결 B → 업스트림 서버

클라이언트와 프록시 사이의 연결이 그대로 서버까지 이어지는 것은 아니다. 프록시는 연결 B를 새로 만들거나 연결 풀(Connection Pool)의 기존 연결을 재사용한다.

헤더 전달

역방향 프록시 뒤의 서버는 네트워크 연결만으로 원래 클라이언트 정보를 알 수 없다. 프록시는 보통 다음 헤더를 추가한다.

  • Forwarded: 표준화된 클라이언트 및 프록시 정보
  • X-Forwarded-For: 원래 클라이언트 인터넷 프로토콜 주소
  • X-Forwarded-Proto: 원래 요청의 http 또는 https
  • X-Forwarded-Host: 클라이언트가 요청한 호스트

X-Forwarded-For는 여러 프록시를 거치면서 목록 형태로 누적될 수 있다.

text
X-Forwarded-For: client, proxy1, proxy2

이 헤더는 클라이언트가 직접 위조할 수 있다. 애플리케이션은 신뢰할 수 있는 프록시가 추가하거나 검증한 값만 사용해야 한다.

부하 분산

역방향 프록시는 여러 업스트림 서버 중 하나를 선택할 수 있다.

  • 라운드 로빈(Round Robin): 서버를 순서대로 선택
  • 최소 연결(Least Connections): 활성 연결이 가장 적은 서버 선택
  • 해시(Hash): 클라이언트나 세션 키를 기준으로 서버 선택
  • 가중치 기반(Weighted): 처리 용량에 따라 요청 비율 조정

프록시는 상태 확인(Health Check)을 수행해 응답하지 않는 서버를 선택 대상에서 제외할 수 있다.

캐싱

프록시는 요청을 캐시 키(Cache Key)로 변환하고 저장된 응답을 조회한다.

text
요청
  ↓
캐시 조회
  ├─ 적중(Cache Hit) → 저장된 응답 반환
  └─ 실패(Cache Miss) → 서버 요청 → 응답 저장 → 반환

HTTP 캐시는 Cache-Control, Expires, ETag, Vary 같은 헤더를 이용해 저장 가능 여부와 유효 기간을 판단한다.

4. 코드 예제

다음 코드는 Node.js 표준 모듈만 사용한 간단한 HTTP 역방향 프록시다. 요청과 응답 본문을 스트림(Stream)으로 전달한다.

typescript
import { createServer, get, IncomingHttpHeaders, RequestOptions, } from "node:http"; const listenPort = 8080; const upstream = new URL("http://127.0.0.1:3000"); function forwardedHeaders( headers: IncomingHttpHeaders, clientIp: string, ): IncomingHttpHeaders { const previous = headers["x-forwarded-for"]; const chain = Array.isArray(previous) ? previous.join(", ") : previous; return { ...headers, host: upstream.host, "x-forwarded-for": chain ? `${chain}, ${clientIp}` : clientIp, "x-forwarded-proto": "http", "x-forwarded-host": headers.host ?? "", }; } const proxy = createServer((clientRequest, clientResponse) => { const clientIp = clientRequest.socket.remoteAddress ?? "unknown"; const options: RequestOptions = { protocol: upstream.protocol, hostname: upstream.hostname, port: upstream.port, method: clientRequest.method, path: clientRequest.url, headers: forwardedHeaders(clientRequest.headers, clientIp), }; const upstreamRequest = get(options, (upstreamResponse) => { clientResponse.writeHead( upstreamResponse.statusCode ?? 502, upstreamResponse.headers, ); upstreamResponse.pipe(clientResponse); }); upstreamRequest.on("error", (error) => { console.error("업스트림 요청 실패:", error.message); if (!clientResponse.headersSent) { clientResponse.writeHead(502, { "content-type": "text/plain; charset=utf-8", }); } clientResponse.end("Bad Gateway"); }); clientRequest.on("aborted", () => { upstreamRequest.destroy(); }); clientRequest.pipe(upstreamRequest); }); proxy.listen(listenPort, "127.0.0.1", () => { console.log(`프록시 실행: http://127.0.0.1:${listenPort}`); });

테스트용 업스트림 서버는 다음과 같이 작성할 수 있다.

typescript
import { createServer } from "node:http"; createServer((request, response) => { response.writeHead(200, { "content-type": "application/json; charset=utf-8", }); response.end( JSON.stringify( { method: request.method, url: request.url, forwardedFor: request.headers["x-forwarded-for"], forwardedHost: request.headers["x-forwarded-host"], }, null, 2, ), ); }).listen(3000, "127.0.0.1", () => { console.log("업스트림 서버 실행: http://127.0.0.1:3000"); });

두 파일을 각각 upstream.ts, proxy.ts로 저장한 뒤 실행한다.

shell
npx tsx upstream.ts npx tsx proxy.ts curl http://127.0.0.1:8080/users?page=1

예제는 프록시의 중계 흐름을 보여주기 위한 코드다. 운영 환경에서는 요청 제한, 시간 초과, 헤더 검증, 전송 계층 보안, 상태 확인 기능을 갖춘 검증된 프록시를 사용한다.

5. 시간 복잡도 또는 성능 특성

단순 프록시는 요청 본문과 응답 본문을 한 번씩 전달하므로 데이터 크기를 b라고 할 때 전송 처리량 기준 시간 복잡도는 O(b)다. 스트림으로 전달하면 전체 본문을 메모리에 저장하지 않아도 되므로 요청 하나당 본문 버퍼 메모리를 O(b)까지 점유하는 방식을 피할 수 있다.

프록시가 추가되면 네트워크 경로에 중계 구간이 생긴다.

text
클라이언트 ↔ 프록시 ↔ 서버

지연 시간에는 다음 비용이 포함된다.

  • 프록시까지의 네트워크 왕복
  • 프록시의 요청 분석과 정책 평가
  • 업스트림 연결 생성 또는 연결 풀 조회
  • 암호화와 복호화
  • 로깅과 압축
  • 큐 대기 시간

캐시 적중 시에는 업스트림 서버 요청을 생략하므로 응답 지연과 서버 부하를 줄일 수 있다. 반대로 압축, 상세 로깅, 애플리케이션 계층 검사는 중앙 처리 장치(CPU)와 입출력 비용을 증가시킨다.

프록시가 모든 요청의 단일 경로가 되면 병목과 단일 장애점(Single Point of Failure)이 될 수 있다. 운영 환경에서는 여러 프록시 인스턴스, 상태 확인, 연결 제한, 시간 초과를 함께 구성한다.

6. 실무 사용 사례

웹 서비스 진입점

엔진엑스(Nginx), HAProxy, Envoy 같은 역방향 프록시를 애플리케이션 서버 앞에 배치한다. HTTPS 처리, 경로 기반 라우팅, 압축, 요청 제한을 중앙에서 수행한다.

text
/api/users → 사용자 서비스
/api/orders → 주문 서비스
/static/*   → 정적 파일 서버

쿠버네티스 인그레스

쿠버네티스(Kubernetes)의 인그레스(Ingress) 구현체나 게이트웨이 응용 프로그래밍 인터페이스(Gateway API) 구현체는 외부 요청을 클러스터 내부 서비스로 전달한다. 호스트 이름과 경로를 기준으로 목적지를 선택한다.

콘텐츠 전송 네트워크

콘텐츠 전송 네트워크(Content Delivery Network, CDN)는 사용자와 원본 서버 사이의 역방향 프록시처럼 동작한다. 사용자와 가까운 엣지(Edge) 서버가 이미지, 스크립트, 동영상 같은 콘텐츠를 캐싱해 전송한다.

기업 네트워크 접근 제어

사내 정방향 프록시는 직원의 외부 웹 요청을 검사한다. 도메인 차단, 악성 파일 검사, 사용자별 접근 정책, 감사 로그를 적용할 수 있다.

서비스 메시

서비스 메시(Service Mesh)의 사이드카 프록시(Sidecar Proxy)는 서비스 간 통신을 중계한다. 상호 전송 계층 보안(Mutual TLS, mTLS), 재시도, 시간 초과, 트래픽 분할, 분산 추적 정보를 애플리케이션 코드 밖에서 처리한다.

7. 주의할 점

  1. 전달 헤더를 무조건 신뢰하지 않는다. 외부 클라이언트가 보낸 X-Forwarded-For를 제거하거나 신뢰 가능한 프록시 체인을 기준으로 검증해야 한다.
  2. 프록시 요청 시간 초과(Timeout)를 설정한다. 느리거나 응답하지 않는 업스트림 연결이 계속 쌓이면 자원이 고갈된다.
  3. 무제한 재시도를 피한다. 처리 결과를 알 수 없는 쓰기 요청을 재시도하면 주문이나 결제가 중복 실행될 수 있다.
  4. 캐시 키에 영향을 주는 헤더와 쿼리 문자열을 확인한다. 사용자별 응답을 잘못 공유하면 개인정보가 노출될 수 있다.
  5. 개방형 프록시(Open Proxy)가 되지 않도록 목적지를 제한한다. 사용자가 임의 주소를 지정할 수 있으면 내부망 접근이나 서버 측 요청 위조(Server-Side Request Forgery, SSRF)에 악용될 수 있다.
  6. 요청 본문과 헤더 크기를 제한한다. 제한이 없으면 과도한 데이터로 메모리와 연결 자원이 소모될 수 있다.
  7. 프록시의 실제 클라이언트 주소 처리 방식을 애플리케이션 설정과 맞춘다. 잘못된 신뢰 설정은 접근 제어와 요청 제한을 우회하게 만든다.
  8. 웹소켓(WebSocket), 서버 전송 이벤트(Server-Sent Events), 장시간 스트리밍은 일반 요청과 연결 수명 및 시간 초과 조건이 다르다.

8. 핵심 정리

  • 프록시는 클라이언트와 서버 사이에서 요청과 응답을 중계한다.
  • 정방향 프록시는 클라이언트를 대신하고, 역방향 프록시는 서버를 대신한다.
  • 역방향 프록시는 부하 분산, TLS 종료, 캐싱, 라우팅, 인증, 관측성에 활용된다.
  • 프록시는 클라이언트 연결과 업스트림 연결을 각각 관리한다.
  • 전달 헤더, 시간 초과, 재시도, 캐시 키, 목적지 제한을 잘못 설정하면 보안 사고나 장애가 발생할 수 있다.
  • 프록시 자체의 가용성과 처리 용량도 전체 서비스 성능에 포함된다.