← Posts

DevOps · Nginx · Proxy

리버스 프록시와 Nginx

앱 앞에 하나를 세우면 라우팅·TLS 종료·정적 서빙·버퍼링이 앱 밖으로 나간다. 로드 밸런서와 어떻게 다른지까지.

목차
  1. 앱은 8080인데 사용자는 443으로 온다
  2. 앞에 세운 하나가 대신 받는다
  3. 역할 하나: 요청을 어디로 보낼지 정한다
  4. 역할 둘: 암호를 여기서 벗긴다
  5. 역할 셋: 정적 파일은 앱까지 안 간다
  6. 역할 넷: 느린 클라이언트를 앱 대신 상대한다
  7. 로드 밸런서와 무엇이 다른가
  8. 미들웨어와 무엇이 다른가
  9. 설정은 어떻게 생겼나
  10. 실무에서 앞단을 둘 때 정하는 것
  11. 정리

앞 글까지 앱을 컨테이너로 만들어 돌보는 데까지 왔다. 이제 그 앱과 사용자 사이를 본다.

앱은 8080인데 사용자는 443으로 온다

로컬에서 앱을 띄우면 http://localhost:8080이다. 그런데 사용자는 https://shop.example.com을 친다. 포트도 다르고 프로토콜도 다르다.

그러면 앱을 443에서 HTTPS로 띄우면 되지 않나. 해보면 금방 막힌다.

  • 443 같은 낮은 포트는 대개 관리자 권한이 필요하다. 앱 프로세스에 그 권한을 주고 싶지 않다.
  • 인증서를 앱이 직접 들고 있어야 한다. 갱신할 때마다 앱을 재시작해야 한다.
  • 앱이 여러 개면 443은 하나뿐이라 나머지는 못 쓴다.
  • 앞 글에서 본 것처럼 앱 컨테이너는 죽고 다시 뜨면서 주소가 바뀐다. 사용자에게 알려준 주소가 그때마다 바뀔 수는 없다.

문제는 하나로 모인다. 사용자가 아는 입구는 하나여야 하는데, 그 뒤의 앱은 여럿이고 계속 바뀐다.

그래서 앞에 하나를 세운다.

앞에 세운 하나가 대신 받는다

리버스 프록시는 사용자의 요청을 대신 받아서 뒤의 서버로 넘기고, 응답을 받아 사용자에게 돌려주는 중개자다.

diagramdiagram

사용자는 리버스 프록시하고만 이야기한다. 뒤에 서버가 몇 대인지, 어떤 포트를 쓰는지, 방금 하나가 죽고 새로 떴는지 알지 못하고 알 필요도 없다.

이름의 “리버스”는 방향을 말한다. 일반 프록시(포워드 프록시)는 클라이언트 편에 서서 클라이언트를 대신해 밖으로 나간다. 회사에서 직원들의 인터넷 접속이 한 게이트웨이를 거치는 게 그것이다. 리버스 프록시는 반대로 서버 편에 서서 서버들을 대신해 밖의 요청을 받는다. 감추는 대상이 클라이언트냐 서버냐가 다르다.

이 자리에 가장 많이 쓰이는 소프트웨어가 Nginx다. Apache, HAProxy, Traefik, Envoy 같은 것들도 같은 자리에 선다.

그러면 이 자리에 세우고 나면 무엇을 맡길 수 있나. 네 가지가 대표적이다.

역할 하나: 요청을 어디로 보낼지 정한다

가장 기본은 라우팅이다. 들어온 요청을 보고 어느 뒤쪽으로 넘길지 고른다.

기준은 보통 둘이다.

  • 호스트 이름 - shop.example.com은 쇼핑몰 앱으로, admin.example.com은 관리자 앱으로.
  • 경로 - /api/로 시작하면 API 서버로, 나머지는 프론트엔드로.
diagramdiagram

이 한 가지만으로도 꽤 많은 게 풀린다. 사용자에게는 도메인 하나만 알려주면 되고, 뒤에서는 서비스를 나눠 둘 수 있다. 브라우저 입장에서 전부 같은 출처라서 CORS 문제도 안 생긴다.

역할 둘: 암호를 여기서 벗긴다

HTTPS 요청은 암호화되어 온다. 이걸 푸는 일을 앱이 아니라 앞단이 맡는 걸 **TLS 종료(TLS termination)**라고 한다.

diagramdiagram

앞단이 맡으면 좋은 이유는 분명하다.

  • 인증서가 한 곳에 있다. 앱이 열 개여도 인증서 갱신은 한 군데서 끝난다.
  • 앱이 암호화를 몰라도 된다. 앱은 평범한 HTTP 서버로 있으면 되고, 로컬 개발과 운영의 차이가 줄어든다.
  • 암복호화 부하가 앱 밖에 있다.

대신 반드시 같이 아는 게 있다. 벗긴 뒤부터 앱까지는 평문이다. HTTPS 글에서 “끝나는 지점이 어디인지 안다”고 했던 그 지점이 바로 여기다. “우리 서비스는 HTTPS니까 다 암호화된다”는 말은 이 지점까지만 참이고, 그 뒤 내부망까지 감쌀지는 따로 정할 문제다.

중요

TLS를 앞단에서 벗기면 앱이 보는 요청은 전부 앞단에서 온 것처럼 보인다. 클라이언트의 실제 IP도, 원래 프로토콜이 HTTPS였다는 사실도 앱은 모른다. 그래서 앞단이 X-Forwarded-For·X-Forwarded-Proto 같은 헤더에 그 정보를 실어 넘긴다. 앱이 “접속 IP”로 뭔가를 판단한다면 이 헤더를 읽어야 하고, 이 헤더를 신뢰할 수 있는 건 앞단을 우리가 통제할 때뿐이다.

역할 셋: 정적 파일은 앱까지 안 간다

이미지·CSS·자바스크립트 같은 파일은 그냥 디스크에서 읽어 보내면 되는 것들이다. 여기에 앱 프로세스가 관여할 이유가 없다.

이런 요청까지 앱이 받으면 앱의 처리 자원이 파일 전송에 쓰인다. 반면 Nginx 같은 웹 서버는 정적 파일을 내보내는 데 특화되어 있다.

그래서 흔한 배치는 이렇다. 앞단이 정적 파일을 직접 응답하고, 동적 요청만 앱으로 넘긴다.

응답에 캐시 헤더를 붙이는 것도 대개 여기서 한다. 브라우저가 다음번엔 아예 안 물어보게 만들면, 그 요청은 앞단까지도 안 온다.

역할 넷: 느린 클라이언트를 앱 대신 상대한다

이건 눈에 잘 안 띄는데 실제로 중요한 역할이다.

앱 서버는 요청 하나를 처리하는 동안 자원(스레드나 워커)을 하나 붙잡는다. 그런데 지하철에서 접속한 사용자는 요청 본문을 아주 천천히 보내고, 응답도 천천히 받는다.

앞단이 없으면 그 느린 클라이언트 하나가 앱의 자원을 그 시간 내내 붙잡는다. 앱이 할 일은 이미 끝났는데 데이터가 다 나갈 때까지 놓지 못한다.

diagramdiagram

앞단이 요청을 다 모아서 넘기고, 응답을 다 받아 대신 내보낸다. 이걸 버퍼링이라고 한다. 앱은 빠른 상대(프록시)하고만 이야기하니 자원을 짧게 쓰고 놓는다.

연결을 유지하고 여러 요청을 태우는 이야기는 연결 글에서 다뤘는데, 실무에서 그 연결을 실제로 받아주는 자리가 대개 이 앞단이다. 바깥으로는 수많은 사용자 연결을 유지하면서, 안쪽으로는 앱과 소수의 연결만 재사용한다.

로드 밸런서와 무엇이 다른가

여기서 가장 많이 헷갈리는 걸 짚는다. 위 그림에서 리버스 프록시가 뒤의 앱 여러 대로 요청을 나눠 보냈다. 그러면 이건 로드 밸런서 아닌가.

겹친다. 그리고 실무에서 한 물건이 둘 다 하는 경우가 많다. 그래서 “무엇이 다른 물건인가”보다 **“무엇을 하는 역할인가”**로 보는 게 정확하다.

로드 밸런싱리버스 프록시
답하는 질문여러 대 중 어디로 보낼까요청을 대신 받아 무엇을 해줄까
주된 관심분배 · 서버 상태 확인 · 가용성라우팅 · TLS 종료 · 정적 서빙 · 버퍼링
대상같은 역할의 서버 여러 대하나든 여럿이든 상관없다
없으면한 대에 몰린다앱이 위 네 가지를 직접 해야 한다

핵심은 서버가 한 대여도 리버스 프록시는 쓸모가 있다는 것이다. 나눌 대상이 없어도 TLS를 벗기고 정적 파일을 주고 느린 클라이언트를 상대해준다. 반대로 로드 밸런싱은 나눌 대상이 여럿일 때만 의미가 있다.

그리고 층도 다를 수 있다.

diagramdiagram

앞단 자체도 한 대면 그게 단일 장애점이 되니, 앞단을 여러 대 두고 그 앞에 다시 분배 장치를 놓는 구성이 나온다. 부하를 어떻게 나누고 어떤 알고리즘으로 고르는지는 규모를 키우는 쪽의 이야기라 여기서는 여기까지만 본다. 이 글에서 가져갈 건 분배는 앞단이 하는 여러 일 중 하나라는 것이다.

참고

API 게이트웨이라고 부르는 것도 이 자리에 선다. 리버스 프록시가 하는 일에 인증 확인·호출 제한·API 단위 라우팅 같은 걸 얹어 하나로 묶은 형태라고 보면 된다. 이름이 여럿인 이유는 물건이 다른 게 아니라 무엇에 무게를 두느냐가 달라서다.

미들웨어와 무엇이 다른가

앞단이 하는 일 목록을 보면 어딘가 익숙하다. 요청이 오면 가로채서 공통 처리를 하고 넘긴다. 미들웨어와 모양이 같다.

같은 발상이 맞다. 다만 어느 층에 있느냐가 다르다.

리버스 프록시미들웨어
사는 곳프로세스 밖프로세스 안
아는 것HTTP 요청의 겉모습 (호스트·경로·헤더)앱의 사용자·권한·도메인 객체
바꾸려면앱 재배포 없이 설정만코드를 고치고 배포
잘하는 일TLS·정적 파일·경로별 분배·속도 제한인증·인가·트랜잭션·도메인 규칙

가르는 기준은 미들웨어 글에서 Filter와 Interceptor를 갈랐던 것과 같다. 앱의 사정을 몰라도 되는 일은 바깥으로, 앱을 알아야 하는 일은 안으로.

예를 들어 “IP당 초당 요청 수 제한”은 앱을 몰라도 되니 앞단이 하면 좋다. “이 사용자가 이 주문을 볼 권한이 있나”는 앱만 아는 일이라 안에서 해야 한다.

설정은 어떻게 생겼나

Nginx 설정은 문법이 많지만, 지금까지의 이야기가 어떻게 글자로 나타나는지만 보면 이 정도다.

nginx
server {
    listen 443 ssl;                          # 사용자는 여기로 온다
    server_name shop.example.com;
    ssl_certificate     /etc/ssl/shop.crt;   # TLS는 여기서 벗긴다
    ssl_certificate_key /etc/ssl/shop.key;

    location /static/ {
        root /var/www;                       # 정적 파일은 앱까지 안 간다
    }

    location / {
        proxy_pass http://app:8080;          # 나머지는 앱으로 넘긴다
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

네 역할이 그대로 보인다. listen 443 ssl과 인증서 두 줄이 TLS 종료, location이 라우팅, root가 정적 서빙, proxy_pass가 뒤로 넘기기다. 마지막 두 줄이 아까 말한 “앱에게 원래 정보를 알려주는” 헤더다.

이 파일의 문법을 외울 필요는 없다. 앞단에 무엇을 맡기고 싶은지가 정해지면 문법은 찾아보면 되고, 반대는 안 된다.

실무에서 앞단을 둘 때 정하는 것

앞단 자체가 단일 장애점이 되지 않게 한다. 모든 요청이 여기를 지나니, 여기가 죽으면 뒤가 다 멀쩡해도 서비스는 죽는다. 그래서 앞단도 여러 대를 두거나, 관리형 로드 밸런서 뒤에 놓는다.

타임아웃을 앱과 맞춘다. 앞단이 60초에 끊는데 앱이 90초짜리 작업을 한다면, 사용자는 게이트웨이 오류를 보고 앱은 혼자 계속 일한다. 이 어긋남이 “가끔 504가 뜬다”의 흔한 원인이다. (뒤가 아예 응답을 못 주면 502, 응답이 제때 안 오면 504다.)

앱 주소를 어떻게 찾을지 정한다. 앞 글에서 봤듯이 컨테이너는 죽고 다시 뜨면서 주소가 바뀐다. 앞단 설정에 IP를 박아두면 그때마다 깨진다. 그래서 오케스트레이터가 주는 서비스 이름을 쓰거나, 앞단이 목록을 자동으로 갱신하게 한다.

어떤 응답을 캐시할지 정한다. 앞단은 응답을 저장해뒀다가 같은 요청에 재사용할 수 있다. 잘 쓰면 앱 부하가 크게 줄지만, 사용자마다 달라야 하는 응답을 캐시하면 남의 화면이 보이는 사고가 난다. 캐시 키에 무엇을 넣을지는 신중하게 정한다.

로그를 여기서 본다. 모든 요청이 지나는 자리라 접근 로그·응답 시간·상태 코드가 한 곳에 모인다. 앱을 안 고치고도 전체 트래픽을 볼 수 있는 자리다.

정리

리버스 프록시란요청을 대신 받아 뒤로 넘기고 응답을 돌려주는 중개자
방향포워드 프록시는 클라이언트 편, 리버스는 서버 편
라우팅호스트·경로를 보고 어디로 넘길지 고른다
TLS 종료인증서를 한 곳에 모으고 앱은 평문 HTTP로 둔다. 벗긴 뒤는 평문이라는 걸 잊지 않는다
정적 서빙파일 응답에 앱 자원을 쓰지 않는다
버퍼링느린 클라이언트를 대신 상대해 앱 자원을 빨리 놓아준다
로드 밸런서와겹치지만 답하는 질문이 다르다. 서버 한 대여도 리버스 프록시는 쓸모 있다
미들웨어와같은 발상, 다른 층. 앱을 몰라도 되는 일은 밖으로

앞단을 세운다는 건 결국 앱이 자기 일만 하게 만드는 것이다. 미들웨어가 핸들러에게 해준 일을, 프록시가 앱 전체에게 해준다.

이제 만들고, 담고, 돌보고, 앞에서 받아주는 데까지 왔다. 남은 건 이 모든 걸 누가 실행하느냐다. 지금까지는 전부 사람이 손으로 했다. 다음 글에서 그 손을 뗀다.