Overview
-
같은 PC 안에서 메인 서버와 서브 서버를 함께 실행하고 있었기 때문에, 처음에는 localhost 프록시 구간이 큰 부담이 없을 것이라고 생각했다.
-
하지만 실제로는 브라우저와 메인 서버 사이에도 HTTPS, 메인 서버와 서브 서버 사이에도 HTTPS hop 이 하나 더 있었고, API 호출 수가 많은 페이지에서는 체감 속도 저하가 분명하게 느껴졌다.
-
이번 정리는 내부 hop 을 HTTP 로 낮추지 않고 HTTPS 를 유지한 상태에서, Node.js https.Agent 와 Keep-Alive 로 업스트림 연결 재사용을 적용해 프록시 비용을 줄인 과정을 기록한 내용이다.
hop: 요청이 목적지까지 가는 중간 단계 하나를 뜻한다.
https.Agent: Node.js 에서 HTTPS 연결을 관리하고 재사용하는 객체이다.
Keep-Alive: 한 번 만든 연결을 바로 끊지 않고 재사용해서 연결 생성 비용을 줄이는 방식이다.
문제 상황
현재 환경은 아래와 같았다.
-
브라우저 진입점은 https://publicip:9443 또는 https://localhost:9443
-
메인 서버는 9000, 9443 포트를 사용
-
서브 서버는 7000, 7443 포트를 사용
-
두 서버는 항상 같은 PC 에 배포됨
-
브라우저는 /api, /subapi 로 API 요청
-
/subapi 진입 시 메인 서버에서 내부적으로 서브 서버로 프록시
초기 구조에서는 아래 흐름으로 동작했다.
-
브라우저가 https://...:9443/subapi/* 호출
-
9443 서버가 내부적으로 https://127.0.0.1:7443/api/* 로 프록시
-
응답을 다시 브라우저로 전달
로컬 PC 에서 통신하는 것이라서 빠를 것 같았지만, 내부적으로는 HTTPS 프록시 hop 이 한 번 더 있었기 때문에 TLS 핸드셰이크와 연결 생성 비용이 반복될 수 있는 구조였다.
특히 새로고침 직후 여러 API 를 연속 호출하거나, 탭 전환 시 요청이 짧은 시간 안에 몰리는 화면에서 속도 저하가 더 체감됐다.
속도 저하 원인
핵심 원인은 아래와 같았다.
-
브라우저와 9443 사이에 HTTPS
-
9443 와 7443 사이에도 HTTPS
-
/subapi 요청마다 내부 업스트림 연결을 새로 만드는 비용이 반복될 가능성
-
일부 화면에서는 여러 API 를 짧은 시간 안에 연속 호출
즉, 문제는 "같은 PC 의 localhost 이므로 프록시 비용이 거의 없을 것"이라는 기대와 달리, 매 요청마다 TLS 핸드셰이크가 동작한다는 점이었다.
Keep-Alive 를 쓰지 않으면 요청마다 TCP 연결과 TLS 세션 협상이 반복될 수 있고, 이 비용은 한 번의 호출에서는 작아 보여도 연속 호출이 많아지면 누적 체감이 커질 수 있다.
로컬 통신인데 HTTP 로 전환하지 않은 이유
가장 먼저 떠올린 대안은 내부 서버만 HTTP 로 낮추는 방법이다. 같은 PC 안에서만 통신하므로 성능상 유리해 보였다.
이 구조를 단순화하면 아래처럼 볼 수 있다.
-
브라우저 -> 9443: HTTPS
-
9443 -> 7000: HTTP
하지만, 이번 환경에서는 내부 hop 도 TLS 를 유지해야 한다는 보안 조건이 있었기 때문에, 이 방향은 맞지 않았다.
특히 이전에 정리했던 것처럼 본문 암호화가 있다고 해서 전송 계층 보안이 대체되는 것은 아니다.
즉, 내부 hop 을 HTTP + AES 로 두는 방향보다, 내부 hop 도 HTTPS 를 유지한 채 연결 재사용으로 비용을 줄이는 쪽이 더 일관된 선택이었다.
선택한 방향은 아래와 같았다.
-
브라우저 HTTPS 유지
-
내부 서브 서버 업스트림도 HTTPS 유지
-
대신 Keep-Alive 로 업스트림 연결을 재사용
-
요청마다 TLS 연결을 새로 만들지 않도록 프록시 계층 최적화
해결 방법
적용한 방법이다.
-
내부 업스트림 연결을 계속 HTTPS 로 유지
-
http-proxy 에 https.Agent 를 연결
-
keepAlive: true 로 업스트림 연결을 재사용
-
요청마다 TLS 연결을 새로 맺지 않도록 프록시를 최적화
이 방식의 장점은 아래와 같았다.
-
보안 구조를 크게 바꾸지 않음
-
브라우저와 내부 업스트림 모두 HTTPS 유지 가능
-
같은 PC 환경에서 localhost TLS 재연결 비용 감소
-
기존 프록시 구조를 유지한 채 적용 가능
적용 코드 예시
실제 적용은 https.Agent 를 프록시의 업스트림 agent 로 연결하는 방식이었다.
import * as https from "https";
import { createProxyServer } from "http-proxy";
const proxyHttpsAgent = new https.Agent({
keepAlive: true, // 연결을 재사용하게 해서 요청마다 소켓을 새로 만들지 않도록 돕는다.
keepAliveMsecs: 30_000, // 유휴 연결을 얼마나 유지할지에 영향을 준다.
maxSockets: 50, // 동시에 유지할 연결 수를 제어한다.
maxFreeSockets: 10, // 동시에 유지할 연결 수를 제어한다.
rejectUnauthorized: false, // 개발 환경에 맞게 조정
});
const subProxy = createProxyServer({
target: `https://127.0.0.1:${SUB_HTTPS_PORT}`,
changeOrigin: false,
ws: true,
xfwd: true,
agent: proxyHttpsAgent,
secure: false, // 개발 환경에 맞게 조정
});핵심은 agent: proxyHttpsAgent 이다. 이 설정으로 프록시가 업스트림 HTTPS 연결을 재사용할 수 있게 된다.
속도 비교 결과
브러우저에 요청 시간을 콘솔로 출력했다.
- 브라우저 콘솔에서 전체 API 요청 시간
- 서버 로그에서 /subapi 프록시 왕복 시간
[API TIMING] [subserver] GET https://localhost:9443/subapi/users total=82.4ms
[HTTPS 9443][SUB API] GET /subapi/users -> 200 64ms- 수정 전
- 수정 후
결론
같은 PC 안에서 두 서버가 실행되더라도, 내부 HTTPS 프록시 hop 은 성능 병목이 될 수 있다.
하지만 내부 hop 을 HTTP 로 내리지 않고, HTTPS 유지 + Keep-Alive 조합만으로 TLS 비용을 줄이고 구조 안정성을 유지할 수 있었다.
보안 요구사항 때문에 내부 HTTP 전환이 어려워, 내부 HTTPS 는 유지하고 업스트림 연결 재사용을 적용했다.
구조를 크게 바꾸지 않으면서도 가장 부담이 적은 최적화였다.
느낀 점
-
처음에는 localhost 이므로 속도 저하에 큰 영향이 없을 줄 알았는데, 실제로는 api 호출마다 내부에서 TLS 핸드셰이크가 동작하고 있었고, 이 동작은 체감될 정도로 속도 저하에 영향을 줬다.
-
보안 요구사항 때문에 구조를 쉽게 바꾸지 못하는 상황에서는, HTTP 로 단순 전환하기보다 현재 구조 안에서 연결 재사용 같은 현실적인 최적화를 먼저 찾는 접근도 있다는 걸 배웠다.
-
앞으로 프록시 성능 문제를 보면 업스트림 프로토콜, 연결 재사용 여부, 화면의 요청 패턴을 같이 봐야겠다고 정리한다.