클라이언트가 적어도 서버가 죽을 수 있다? 파일 업로드 시 Nginx가 Disk I/O를 감수하며 백엔드를 지키는 원리(Request Buffering)를 심층 분석합니다.
UI 서빙을 위한 웹 서버 하드웨어 스펙을 산정할 때, 흔히 하는 착각이 있습니다.
"클라이언트 수가 늘지 않으면 부하도 늘지 않을 것이다."
이 판단의 근거는 보통 다음과 같습니다.
하지만 이는 **'파일 업로드'**라는 변수를 만나면 완전히 빗나간 예상이 됩니다. 웹 서버의 부하는 단순한 데이터 양이 아니라, 연결 유지 시간과 리소스 점유 관점에서 봐야 하기 때문입니다.
결론부터 말하자면, 웹 서버를 경유하는 파일 업로드는 심각한 부하를 줄 수 있습니다. 업로드 작업 자체의 CPU 부하보다는, 느린 클라이언트를 기다리는 동안 서버의 메모리와 스레드가 묶이기 때문입니다.
소수의 클라이언트라면 괜찮지만, 동시 접속자가 조금만 늘어도 스레드 풀(Thread Pool) 고갈로 이어져, 파일 업로드를 하지 않는 일반 사용자의 요청까지 처리하지 못하는 장애(Hang)가 발생합니다.
대부분의 아키텍처에서 앞단에 있는 Nginx는 Request Buffering 전략을 통해 뒷단의 비싼 백엔드(Java/Node.js)를 보호합니다.
브라우저가 파일 전송을 시작합니다. Nginx는 데이터를 받습니다.
업로드가 완전히 완료되면, Nginx는 디스크에 쓴 파일을 다시 읽어서(Read I/O 발생) 뒤쪽 서버(Java)로 보냅니다.
이 과정에서 Nginx는 중계소 역할을 위해 하드디스크를 혹사시킵니다. 정작 가벼운 정적 파일 서빙이 디스크 I/O 병목으로 인해 느려질 수도 있습니다.
그럼에도 불구하고 왜 이런 비효율적인 짓을 할까요?
Nginx는 **"비싼 몸인 Java 스레드"**를 보호하기 위해 궂은일을 자처하는 것입니다. 100MB 파일을 100초 동안 전송하는 느린 클라이언트 상황을 가정해 봅시다.
Nginx가 데이터를 받는 족족 Java로 토스한다고 가정해 봅시다.
결과: 이런 느린 사용자가 200명만 있어도 Java 스레드 200개가 100초 동안 마비됩니다. 서버는 다운됩니다.
이 전략이 유효한 이유는 Nginx와 백엔드 서버가 같은 네트워크(VPC) 혹은 같은 서버 안에 있다는 가정 덕분입니다.
| 구분 | 대역폭 (속도) | 100MB 전송 시 소요 시간 | Java 스레드 점유 시간 |
|---|---|---|---|
| User → Nginx | 10 Mbps | 80초 | (버퍼링 시) 0초 |
| Nginx → Java | 10 Gbps | 0.08초 | 0.08초 (순식간) |
이 덕분에 파일 업로드가 느린 원인이 브라우저의 파일 읽기 속도가 아니라, Nginx가 열심히 버퍼링 중이었기 때문이라는 것을 알 수 있습니다.
우리는 Java의 안녕을 위해 Nginx의 디스크를 희생시킨 것입니다.