거의 모든 웹 서비스 앞에 무엇이 위치하는가
환영합니다
브라우저에 example.com을 입력하면, 실제로 애플리케이션을 실행하는 머신에 거의 도달하지 않습니다. 대신 요청을 실제 실행 머신으로 전달(forward)하는 머신에 도달합니다. 이 전달을 담당하는 머신에는 이름이 있습니다: 리버스 프록시(reverse proxy)입니다.
이 레슨에서는 리버스 프록시가 무엇을 하는지, 왜 거의 모든 공개 웹 서비스가 그 뒤에 숨어 있는지, 그리고 엣지 레이어가 동시에 처리하는 세 가지 역할이 무엇인지에 대해 배웁니다.
수업이 끝날 때쯤에는 다음을 이해하게 될 것입니다:
- 클라이언트, 프록시, 오리지널 서버의 차이점
- 오리지널 서버 앞에 프록시를 두면 왜 보호, 확장, 그리고 아무도 모르게 구성 요소를 교체할 수 있는지
- 리버스 프록시가 동시에 처리하는 세 가지 역할: 오리지널 서버 숨기기, TLS 종료, 복제본 간 부하 분산
- 요청이 브라우저에서 프록시를 거쳐 업스트림으로 이동하고 다시 돌아오는 과정, 홉(hop) 단위로 살펴보기
수업이 끝날 때쯤에는 프록시 배치, 엣지에서의 관심사 분리, 그리고 무상태(stateless) 프록시 계층은 저렴하게 복제할 수 있는 반면 단일 오리지널 서버는 그렇지 않은 이유에 대해 자신감 있게 추론할 수 있게 됩니다.
포워드 프록시와 리버스 프록시
프록시의 두 방향
두 종류의 프록시 모두 두 당사자 사이에 위치하여 트래픽을 중계합니다. 차이는 어떤 당사자를 대표하는지에 있습니다.
포워드 프록시(Forward proxy): 클라이언트 그룹 앞에 위치합니다. 클라이언트는 프록시의 존재를 알고 있으며, 외부 서버는 클라이언트 주소가 아닌 프록시 주소를 확인합니다. 기업용 아웃바운드(egress) 프록시, 콘텐츠 필터, SOCKS 프록시가 이 패턴에 해당합니다.
리버스 프록시(Reverse proxy): 서버 그룹 앞에 위치합니다. 외부 세계(클라이언트)는 프록시 주소와 통신하며, 실제 서버가 뒤에 숨겨져 있다는 사실을 클라이언트는 전혀 모릅니다. 거의 모든 공개 웹 서비스가 이를 사용합니다.
기억법: 포워드 프록시는 서버로부터 클라이언트를 숨기고, 리버스 프록시는 클라이언트로부터 서버를 숨깁니다.
방향이 왜 중요한가? 역할, 장애 모드, 보안 경계가 모두 다릅니다. 포워드 프록시는 사용자가 누구와 통신하는지(연락 대상)에 신경 쓰고, 리버스 프록시는 누가 자신의 서버와 통신하는지(연락자)에 신경 씁니다.
두 종류의 프록시를 동시에 통해 트래픽을 보내는 클라이언트는 다음과 같은 경로를 따릅니다: client -> forward_proxy -> internet -> reverse_proxy -> origin.
왜 이렇게 많은 기능이 엣지에 위치하는가
엣지 레이어가 그 가치를 증명하는 이유
리버스 프록시는 동시에 세 가지 역할을 수행합니다. 이 중 하나만으로도 해당 레이어의 존재가 정당화되며, 동일한 주소에서 세 가지 역할을 모두 수행한다는 사실은 왜 거의 모든 프로덕션 웹 아키텍처가 프론트 엔드에서 동일한 형태를 띠는지 설명해 줍니다.
역할 1: 오리지널 서버를 숨긴다. 프록시는 공개 IP로 응답합니다. 백엔드는 인터넷에서 접근할 수 없는 비공개 IP에 위치합니다. 공격자가 오리지널 서버를 타격하려면 먼저 프록시를 해킹해야 합니다.
2번 역할: TLS 종료. 프록시는 example.com의 인증서를 보유하며 들어오는 HTTPS 요청을 복호화합니다. 백엔드는 신뢰할 수 있는 네트워크 세그먼트를 통해 프록시와 평문 HTTP(또는 더 단순한 내부 TLS)로 통신합니다. 인증서 회전, 갱신, 암호화 정책은 모두 한 곳에서 관리됩니다.
3번 역할: 부하 분산. 프록시는 각 요청을 어느 백엔드가 처리할지 선택합니다. 하나의 프록시 뒤에 있는 백엔드들은 풀(pool)을 형성하며, 프록시는 라운드 로빈, 최소 연결 수, 헤더 해시와 같은 전략을 사용하여 요청마다 하나를 선택합니다. 용량을 늘리는 것은 모든 클라이언트에게 새로운 주소를 알려주는 것이 아니라, 풀에 백엔드를 추가하는 것을 의미합니다.
각 역할은 작은 프로그램입니다. 이 세 가지가 함께 '파이썬 앱 앞의 Caddy'처럼 단순해 보이는 계층이 파이썬 앱 자체보다 더 큰 설계적 무게를 지니는 이유를 설명합니다.
세 가지 역할 모두를 위한 설계
귀하의 팀은 단일 VM에서 포트 8000으로 경청하는 단일 파이썬 프로세스로 작은 API를 실행하고 있습니다. 트래픽이 증가하여 단일 VM이 감당할 수 없게 되었으며, 보안 검토에서 해당 VM이 공개 IP를 가지고 있고 자체 TLS 인증서를 실행하고 있다는 점이 지적되었습니다.
앞에 리버스 프록시를 배치하기로 결정했습니다. 레이아웃을 스케치해 보세요: DNS는 어디를 가리키고, 인증서는 어디에 위치하며, 부하는 어떻게 백엔드에 도달하고, 원래 VM에 어떤 변화가 생기는지 설명하세요.
아무도 모르게 구성 요소를 교체하기
간접성(Indirection)은 자유를 산다
오래된 컴퓨터 과학 격언이 하나 있습니다. 모든 문제는 간접화(indirection) 계층을 하나 추가함으로써 해결할 수 있다는 것입니다(간접화 계층이 너무 많다는 문제는 제외하고요). 리버스 프록시는 분산 시스템에서 가장 유용한 간접화 기법 중 하나입니다.
이로 인해 얻는 것:
- 교체 가능한 백엔드. 애플리케이션을 Python에서 Go로 옮기거나, 하나의 데이터센터에서 다른 데이터센터로 이전하거나, 제로 다운타임으로 새 버전을 배포해야 하는 경우? 이 모든 작업은 안정적인 공개 주소 뒤에서 이루어집니다. 사용자에게는 아무것도 변하지 않습니다.
- 독립적인 확장성. 프록시 계층은 TLS 계층에서 대역폭 및 CPU 기준으로 확장됩니다. 백엔드 계층은 애플리케이션 워크로드 기준으로 확장됩니다. 서로 다른 머신에 존재하기 때문에 각 계층은 독립된 축으로 성장합니다.
- 장애 격리. 백엔드에서의 잘못된 배포가 공개 주소를 다운시키지 않습니다. 프록시는 계속 가동되며, 수정 사항을 배포하거나 롤백하면 백엔드가 복구될 때 세계가 다시 연결됩니다.
- 단일 위치의 횡단 관심사. 레이트 리미팅, 지리적 차단, 요청 로깅, 헤더 재작성, 캐싱, 응답 압축: 이 모든 것이 프록시에서 처리됩니다. 백엔드 코드는 애플리케이션 로직에 집중할 수 있습니다.
프록시 없이 이 모든 문제는 애플리케이션 프로세스 내부에 존재해야 합니다. 프록시가 있으면 이 문제들은 한 팀이 소유하는 단일 계층에 위치하게 됩니다.
비용: 운영해야 할 계층이 하나 더 생기는 것입니다. 성숙한 팀들은 프록시 계층 자체가 무상태(stateless)로 실행되고 수평적으로 확장되므로 이 비용을 수용합니다. 프록시 하나를 두 개로 교체하는 데는 조율이 필요하지 않습니다.
프록시를 통한 블루/그린 배포
귀하의 팀은 리버스 프록시 뒤의 세 대의 백엔드 VM(블루 풀)에서 API 버전 1을 실행하고 있습니다. 문제가 발생하면 30초 이내에 롤백할 수 있도록 버전 2를 배포하고자 합니다.
버전 2를 실행하는 세 대의 새로운 백엔드 VM(그린 풀)을 블루 풀과 나란히 시작하지만, 아직 트래픽을 그쪽으로 라우팅하지는 않습니다.
브라우저에서 백엔드로, 그리고 다시 돌아오기까지
한 요청을 끝까지 추적하기
백엔드 풀 앞의 리버스 프록시를 통해 https://api.example.com/users/42에 대한 단일 HTTPS GET 요청을 추적해 보세요.
1홉: DNS 해석. 브라우저가 리졸버에게 api.example.com의 주소를 요청합니다. 리졸버는 프록시의 공개 IP(예: 203.0.113.10)를 반환합니다. 브라우저는 203.0.113.10:443으로 TCP 연결을 시작합니다.
2홉: TLS 핸드셰이크. 프록시는 api.example.com에 대한 인증서를 제시합니다. 브라우저는 인증서를 검증하고, 양측은 세션 키에 합의하며, 암호화 채널이 열립니다.
3홉: TLS 내부의 HTTP 요청. 브라우저는 GET /users/42 HTTP/1.1\nHost: api.example.com\n...를 전송합니다. 프록시는 요청 바이트를 복호화합니다.
4단계: 백엔드 선택. 프록시는 api.example.com에 대한 업스트림 풀을 참조하여 로드 밸런싱 전략에 따라 하나의 백엔드(예: 10.0.0.21:8000)를 선택합니다.
5단계: 업스트림 요청. 프록시는 10.0.0.21:8000으로 평문(plain) HTTP 연결을 열거나(또는 재사용하여) 요청을 전달합니다. 프록시는 이 과정에서 헤더를 재작성할 수 있습니다: X-Forwarded-For: <client-ip>를 추가하고, Host: 헤더를 올바르게 설정하며, Connection과 같은 홉-바이-홉(hop-by-hop) 헤더를 제거합니다.
6단계: 백엔드 처리. 백엔드 애플리케이션은 요청을 읽고, 데이터베이스를 조회하며, JSON 응답을 구성합니다.
7단계: 업스트림 응답. 백엔드는 응답을 평문(plain) HTTP로 프록시에게 반환합니다.
8단계: 엣지 응답. 프록시는 응답을 재작성하거나 압축할 수 있으며, 이를 TLS 세션을 통해 다시 암호화하여 브라우저로 전송합니다.
9단계: 연결 수명 주기. TLS 세션은 일반적으로 다음 요청을 위해 열려 있는 상태로 유지됩니다(HTTP/2는 하나의 연결 위에서 여러 요청을 다중화합니다). 프록시에서 백엔드로의 연결은 재사용을 위해 풀(pool)링되는 경우가 많습니다.
모든 공개 웹 서비스는 이 구조의 어떤 변형이든 따릅니다. 각 단계(hop)를 이해하면 지연 시간이 어디서 누적되는지, 로깅이 어디에 속하는지, 그리고 장애가 어디에 숨어 있을 수 있는지를 추론할 수 있습니다.
시간은 어디로 사라졌는가?
사용자가 API가 느리다고 불평합니다. 측정한 결과 요청이 끝에서 끝까지 850ms 걸리는 것을 확인했습니다. 백엔드의 서버 로그는 애플리케이션이 요청을 처리하는 데 40ms만 걸렸다고 표시합니다. 프록시 로그는 프록시가 자신의 쪽에서 일( TLS 핸드셰이크 + 라우팅 + 응답 작성)에 50ms를 사용했다고 표시합니다.
새로운 서비스를 위한 최소한의 엣지 설계하기
종합
포워딩 프록시와 리버스 프록시의 차이, 리버스 프록시가 동시에 처리하는 세 가지 역할, 오리지널 서버를 숨기는 것이 변경 사항이 필요할 때마다 왜 이점을 가져다주는지, 그리고 요청이 엣지를 통해 홉(hop) 단위로 어떻게 흐르는지 배웠습니다.
이제 이를 적용해 보겠습니다.
소규모 팀이 notes.example.com이라는 이름의 새로운 서비스를 출시할 계획입니다. 사용자는 개인 노트를 읽고 쓸 것입니다. 팀은 출시 시 두 대의 백엔드 VM을 실행할 것이며, 향후 1년 내에 10대로 확장할 것으로 예상합니다. 그들은 사용자에게 HTTPS를 제공하고, 새 버전을 점진적으로 배포하며, 백엔드 IP가 공개되지 않기를 원합니다.
이 과정의 다음 단계
이 과정의 다음 단계
이 레슨은 엣지 레이어의 형태를 확립했습니다. 이 과정의 나머지 4개 레슨은 이를 기반으로 구축됩니다:
- 무상태 수평 확장(Stateless Horizontal Scaling): 프록시 티어(그리고 그 뒤의 백엔드)가 왜 저렴하게 복제되는지, 그리고 서지(surge) 상황에서 레플리카 수를 계산하는 수학적 원리.
- 인그레스/에그레스 분리(Ingress / Egress Separation): 수신 및 발신 트래픽을 모두 처리하는 단일 프록시 박스가 왜 놀라운 방식으로 실패하는지, 그리고 레이어를 어떻게 분리하는지.
- 실패 모드 및 영향 범위(Blast Radius): 단일 구성 변경이 장애로 이어지는 방식과 재발을 방지하는 무과실(blameless) 액션 아이템 작성법.
- 관측 가능성 및 용량: 사용자가 문제를 인식하기 전에 시스템의 이상을 감지하기 위해 엣지(edge)에서 측정해야 할 사항.
각 레슨은 독립적으로 구성됩니다. 이를 모두 결합하면 웹 스케일 플릿(web-scale fleet)에 대한 작동 가능한 정신 모델(mental model)을 갖추게 됩니다.
동반 레슨: geometry_of_proxies_and_origins는 이 레슨의 모든 내용을 방향 그래프(directed graph)로 재구성하고, 그래프 이론이 요청 경로에 대해 알려주는 내용을 탐구합니다.
수고하셨습니다. 다음으로 나아가세요.