Nginx 리버스 프록시에서 SSE가 버퍼링될 때 고치는 설정 세 줄

Nginx 리버스 프록시에서

이 글에서는 Nginx 리버스 프록시에서 SSE(Server-Sent Events) 응답이 브라우저까지 실시간으로 오지 않고 중간에 뭉쳐서 도착하는 문제를 어떤 설정으로 풀 수 있는지 확인하실 수 있습니다. 결론만 먼저 말씀드리면, 해당 location 블록에 proxy_buffering off, proxy_cache off, chunked_transfer_encoding off 이 세 줄을 넣는 것으로 대부분의 지연이 해결됩니다. 다만 SSE가 버퍼링될 때의 원인이 이 세 줄만으로 설명되지 않는 경우도 있어서, 아래에서 타임아웃·gzip·CDN 같은 예외 상황도 함께 짚어드리겠습니다.

Nginx 리버스 프록시에서 SSE 응답이 버퍼링되는 이유

Nginx는 proxy_buffering 값이 기본적으로 on으로 설정되어 있습니다. 이 값이 켜져 있으면 Nginx는 백엔드(WAS)가 보낸 응답을 곧바로 클라이언트에 넘기지 않고, proxy_buffer_sizeproxy_buffers로 정해진 메모리 버퍼에 어느 정도 채운 다음에야 클라이언트로 흘려보냅니다.

일반적인 HTML·JSON 응답에서는 이 방식이 오히려 유리합니다. 느린 클라이언트가 응답을 천천히 받아가더라도 백엔드 워커는 버퍼에 데이터를 넘긴 즉시 다음 요청을 처리할 수 있기 때문입니다. 문제는 SSE처럼 백엔드가 연결을 계속 열어두고 이벤트를 몇 초 간격으로 조금씩 흘려보내는 응답입니다. 이런 응답은 버퍼가 다 채워질 때까지 Nginx 안에 갇혀 있다가, 한참 뒤 한꺼번에 클라이언트로 넘어가는 현상이 나타납니다.

버퍼링을 끄는 설정 세 줄부터 적용해보세요

SSE를 서빙하는 location 블록에만 아래 세 줄을 추가합니다. 서버 전체가 아니라 해당 엔드포인트에만 좁게 적용하는 게 중요한데, 이유는 뒤에서 설명하겠습니다.

location /events/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;

    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;
}

Nginx 서버 터미널 설정 코드 화면

각 줄의 역할은 다음과 같습니다.

  • proxy_buffering off — Nginx가 백엔드 응답을 버퍼에 모으지 않고, 받는 즉시 클라이언트로 전달합니다. SSE가 버퍼링될 때 가장 먼저 효과를 보는 지점입니다.
  • proxy_cache offproxy_cache_path로 캐시 존을 설정해둔 서버라면, SSE 같은 무한 스트림 응답이 캐시 로직에 걸려 완료를 기다리는 상황을 막아줍니다.
  • chunked_transfer_encoding off — 백엔드가 Content-Length를 주지 않는 스트리밍 응답에서 Nginx가 자체적으로 청크 인코딩을 다시 씌우는 동작을 끕니다. 프레임워크에 따라 이 줄 없이도 문제없는 경우가 있어, 앞의 두 줄로 해결되지 않을 때 추가로 시도해볼 항목으로 보시면 됩니다.

설정을 바꾼 뒤에는 nginx -t로 문법을 확인하고 nginx -s reload로 반영합니다. 별도 재시작 없이 무중단으로 적용됩니다.

연결이 끊기는 건 버퍼링과 다른 문제입니다

세 줄을 넣은 뒤 이벤트는 실시간으로 오는데, 일정 시간이 지나면 연결 자체가 끊어지는 경우가 있습니다. 이건 버퍼링이 아니라 proxy_read_timeout 문제입니다. 기본값이 60초라서, 백엔드가 60초 넘게 아무 바이트도 보내지 않으면 Nginx는 백엔드가 죽었다고 판단하고 연결을 닫아버립니다.

해결 방법은 두 가지입니다. 하나는 해당 location에 proxy_read_timeout 3600s;처럼 넉넉한 값을 주는 것이고, 다른 하나는 백엔드에서 주기적으로 하트비트를 보내는 것입니다. SSE 스펙은 콜론(:)으로 시작하는 줄을 주석으로 정의하고 있어서, 예를 들어 15~30초마다 : keep-alive\n\n 같은 줄을 흘려보내면 클라이언트는 무시하지만 Nginx 입장에서는 계속 데이터가 오는 것으로 인식해 타임아웃에 걸리지 않습니다. 실무에서는 타임아웃을 늘리는 방식보다 하트비트 쪽이 더 안정적인데, 타임아웃 값을 무작정 키우면 죽은 연결을 오래 붙잡고 있는 부작용이 생기기 때문입니다.

gzip·CDN·로드밸런서 같은 중간 계층의 예외

세 줄을 넣었는데도 지연이 남아 있다면 Nginx 앞뒤로 다른 계층이 끼어 있는지부터 확인해야 합니다. 아래 표는 흔히 놓치는 지점을 정리한 것입니다.

서버 네트워크 데이터 스트리밍 흐름 개념도

계층 버퍼링이 생기는 이유 조치
Nginx gzip 모듈 압축 처리를 위해 데이터를 일부 모아두었다가 내보낼 수 있음 해당 location에 gzip off; 적용
클라우드 로드밸런서(ALB 등), CDN Nginx와 별개로 자체 버퍼링·타임아웃 정책을 가짐 해당 서비스의 스트리밍/버퍼링 옵션을 별도로 확인
백엔드 프레임워크·WAS 동기식 워커가 응답을 전부 모은 뒤에야 전송하는 구조일 수 있음 스트리밍 응답을 지원하는 워커·핸들러 방식으로 전환

특히 Nginx 앞에 또 다른 리버스 프록시나 CDN이 있는 구성이라면, Nginx의 proxy_buffering off는 Nginx 계층에서만 효과가 있을 뿐 그 앞단의 버퍼링까지 없애주지는 않습니다. 이 경우는 이 글에서 다루는 세 줄짜리 설정만으로는 해결되지 않는 대표적인 사례입니다.

proxy_buffering을 껐는데도 지연이 남아있다면?

이럴 때는 문제가 Nginx 구간인지, 백엔드 구간인지부터 나눠서 확인하는 게 빠릅니다. curl -N 옵션은 curl 자체의 출력 버퍼링을 끄기 때문에, 브라우저 개발자 도구보다 원인을 구분하기에 유용합니다.

# 1) 백엔드에 직접 요청 — 여기서도 늦게 오면 백엔드 문제
curl -N http://localhost:8000/events/

# 2) Nginx를 거쳐 요청 — 1번은 빠른데 2번이 느리면 Nginx 설정 문제
curl -N http://your-domain.com/events/

1번, 2번 모두 실시간으로 오는데 실제 브라우저에서만 지연이 느껴진다면, 그건 Nginx가 아니라 브라우저나 프런트엔드 코드에서 이벤트를 모아 렌더링하는 로직을 의심해볼 차례입니다. 참고로 proxy_buffering off를 http나 server 블록처럼 넓은 범위에 걸어두면, 느린 클라이언트가 백엔드 워커를 응답이 끝날 때까지 계속 붙잡고 있게 되어 동시 접속이 많은 환경에서는 오히려 백엔드 부하가 늘어날 수 있습니다. 그래서 SSE를 서빙하는 location 블록에만 좁게 적용하시는 걸 권해드립니다.

지금 적용해볼 수 있는 확인 순서

정리하면, SSE 엔드포인트 location에 proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; 세 줄을 넣고 reload하는 것이 첫 조치입니다. 그다음 연결이 도중에 끊긴다면 proxy_read_timeout을 늘리거나 백엔드에서 하트비트를 보내도록 하고, 그래도 지연이 남으면 gzip과 앞단 CDN·로드밸런서의 버퍼링 옵션을 순서대로 점검해보시면 됩니다. 지금 서버의 Nginx 설정 파일을 열어 해당 location 블록을 찾은 뒤, 위 세 줄부터 먼저 넣고 curl -N으로 결과를 확인해보시길 권해드립니다.

Celery 워커가 죽었는데 모니터링에 안 걸릴 때, 헬스체크 이렇게 설계하세요

Leave a Comment