이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Proxy, reverse proxy, gateway가 HTTP 요청의 어떤 부분을 바꿀 수 있는가?
  • 4xx/5xx 응답이 앱에서 난 것인지 proxy에서 난 것인지 어떻게 구분하는가?
  • 백엔드 개발자가 proxy 설정을 API 계약으로 다뤄야 하는 이유는 무엇인가?

개요

Proxy는 client와 server 사이의 중계 계층이다. Forward proxy는 client 쪽을 대표하고, reverse proxy는 server 앞단에서 외부 요청을 내부 서비스로 전달한다. 백엔드 개발자에게 중요한 쪽은 대부분 reverse proxy와 gateway다. 이 계층은 HTTP 요청을 그대로 흘려보내지 않고 header, path, body, timeout, protocol, 보안 정책을 바꿀 수 있다.

  • TLS를 종료하거나 backend로 다시 TLS를 맺는다.
  • 외부 path를 내부 service path로 rewrite한다.
  • body size, header size, timeout, buffering을 제한한다.
  • rate limit, auth, WAF, bot protection을 적용한다.
  • access log와 request id를 만들어 관측 경계를 제공한다.

원리

요청은 보통 다음 경로를 지난다.

Client
  -> CDN / WAF
  -> Load Balancer
  -> Reverse Proxy / API Gateway
  -> Application

각 계층은 응답을 직접 만들 수 있다. 앱이 200을 만들 수도 있지만, gateway가 401을 만들고, WAF가 403을 만들고, proxy가 413/502/504를 만들 수 있다. 그래서 백엔드 장애 분석의 첫 질문은 “어느 계층이 응답했는가”다.

응답 주체 구분

증상우선 확인
앱 로그 없음 + 403WAF/gateway auth/ACL
앱 로그 없음 + 413proxy body limit
앱 처리 중 + 499client 또는 proxy가 먼저 연결 종료
앱 로그 없음 + 502upstream connect/protocol/TLS/DNS
앱 처리 길어짐 + 504proxy/LB timeout
redirect loopforwarded proto/host, HTTPS redirect 중복

Server, Via, X-Request-ID, X-Upstream-Status, gateway request id 같은 header도 힌트다. 단, 보안상 header를 숨기는 환경도 있으므로 access log와 함께 봐야 한다.

Reverse Proxy 설정 예시

upstream app_upstream {
    server app-1:8080 max_fails=3 fail_timeout=10s;
    server app-2:8080 max_fails=3 fail_timeout=10s;
    keepalive 64;
}
 
server {
    listen 443 ssl;
    server_name api.example.com;
 
    location /api/ {
        proxy_pass http://app_upstream;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Request-ID $request_id;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 2s;
        proxy_read_timeout 30s;
        client_max_body_size 20m;
    }
}

이 설정은 앱 코드 밖에 있지만 API 계약을 만든다. 앱은 Host, scheme, client IP, body limit, timeout, request id가 어떻게 들어오는지 알아야 한다.

관측 포인트

NGINX access log에 upstream 정보를 남기면 원인 분리가 빨라진다.

log_format upstream_timing
  '$remote_addr $request_id "$request" $status '
  'upstream=$upstream_addr upstream_status=$upstream_status '
  'request_time=$request_time upstream_time=$upstream_response_time';
 
access_log /var/log/nginx/access.log upstream_timing;

앱 로그에도 같은 request id를 남기면 proxy와 app 로그를 연결할 수 있다.

인프라 협업 포인트

  • proxy가 어떤 header를 추가/삭제/덮어쓰는지 앱 팀이 알아야 한다.
  • 앱 팀은 endpoint별 body size, timeout, streaming 여부, upload 여부를 인프라 팀에 제공해야 한다.
  • 인프라 팀은 4xx/5xx가 proxy에서 만들어졌는지 upstream에서 왔는지 구분 가능한 로그 포맷을 제공해야 한다.
  • request id 전파 규칙을 정하지 않으면 장애 때 CDN, gateway, app 로그가 서로 끊긴다.

실전 팁

  • proxy 설정 변경은 코드 배포처럼 리뷰한다. 한 줄의 proxy_pass나 timeout 변경이 API 동작을 바꾼다.
  • health check location과 실제 API location의 설정이 다를 수 있다. health는 통과하지만 실제 API만 실패하는 경우가 흔하다.
  • gzip, buffering, HTTP/2 termination은 response streaming과 gRPC 동작에 영향을 줄 수 있다.
  • 개인 프로젝트에서도 NGINX access/error log와 request id는 켜 두는 편이 좋다.
  • “앱에 로그가 없다”는 사실은 proxy 이전 문제를 강하게 시사하지만, access log 샘플과 함께 말해야 설득력이 있다.

위험 신호!

  • 앱 팀이 실제 적용된 proxy 설정을 볼 수 없다.
  • proxy access log에 upstream status와 response time이 없다.
  • request id가 proxy에서 새로 생성되지만 app까지 전달되지 않는다.
  • proxy 설정 변경에 smoke test가 없다.

확인 질문

확인 질문

  • reverse proxy가 API 계약에 포함되는 이유는 무엇인가?
    • header, path, timeout, body size, protocol, response status를 바꿔 앱이 받는 요청의 실제 형태를 결정하기 때문이다.
  • 앱 로그가 없는 413은 무엇을 의미할 가능성이 큰가?
    • body size 제한이 proxy나 gateway에서 걸려 앱에 요청이 도달하지 않았을 가능성이 크다.
  • proxy와 app 로그를 연결하려면 무엇이 필요한가?
    • 같은 request id를 proxy에서 생성하거나 전달하고, 두 계층의 로그에 모두 남겨야 한다.

참고 문서