사이드 프로젝트랑 실무의 가장 큰 차이 중 하나는 데이터의 규모다. 포트폴리오를 위해 일부러 대용량 데이터를 만들어보는 사람도 있지만, 내가 함께한 팀은 그렇게까지 치밀하지는 못했다. 우리가 다루던 데이터는 늘 ‘적당한 크기’였고, 페이지네이션도 그냥 습관처럼 넣었을 뿐이다. 그래서 큰 응답이 지연된다거나, 대용량 요청이 시스템에 어떤 영향을 주는지까지는 깊게 생각하지 못했다.
하지만 회사에 들어와 실제 프로젝트를 보니, HTTP 요청 횟수 제한과 사이즈 제한이 명확하게 설정되어 있었다. 그리고 우리 팀은 점점 커지는 데이터 전송량을 감당하기 위해 데이터 전송 방식을 근본적으로 개선해야 하는 과제를 안고 있었다. 그때 처음으로 이런 생각이 들었다. “HTTP 요청 사이즈는 진짜로 무제한일까?” 아마 나처럼 이 고민을 제대로 해본 적이 없는 사람도 있을 것이다. 이번 글에서는 HTTP의 POST 사이즈에 대한 이야기를 해보려 한다.

사실 HTTP 스펙 자체에는 POST, PATCH, PUT 요청의 크기에 대한 제한이 없다. 즉, 이론적으로는 무제한이다. 그러나 실제 서비스 환경에서는 다르다. 현실의 시스템은 안전을 위해 요청 크기(limit) 와 요청 횟수(rate limit)에 제한을 둔다.
일반적으로 클라이언트의 요청은 다양한 레이어를 거쳐서 메인 서버에 전달된다. 예를 들어, 프록시 서버 → 로드밸런서 → 웹 서버 → 메인 서버 순으로 전송된다. 그리고 메인 서버에 지나치게 부하가 가는 것을 막기 위해 앞 단계에서 요청량을 조절한다.
각 단계의 제한값은 수동으로 설정이 가능하지만, 별도의 설정을 하지 않는 경우 기본값은 다음과 같다.

티켓팅이나 수강 신청을 해본 사람들은 트래픽이 과도하게 몰리는 것이 위험하다는 것을 직관적으로 이해할 수 있다. 그렇다면 요청 크기는 왜 제한을 걸어야 할까? 그건 요청 본문이 커질수록 서버는 더 많은 리소스를 소모하고, 결과적으로 서비스 안정성과 보안이 취약해지기 때문이다.
대부분의 서버나 프레임워크는 요청 본문을 메모리에 올려서 처리한다. 메모리는 데이터에 즉시 접근해 처리하기 위한 공간으로, 처리 속도가 빠른 만큼 디스크(SSD, HDD)보다 훨씬 비싸다. 그래서 무한정 크게 사용할 수가 없기 때문에, 효율적으로 관리해야 하는 공간이다.
만약 100MB짜리 JSON을 동시에 여러 클라이언트가 업로드한다면, 서버 메모리는 순식간에 포화되고 Out of Memory(OOM) 에러가 발생할 수도 있다. 앞 단계에서 크기 제한을 두지 않아서 큰 요청이 메인 서버까지 전달되면, body-parser나 express.json()은 요청 본문을 전부 메모리에 읽은 뒤 파싱하게 되고, 큰 Body로 인해 하나의 프로세스가 메모리를 많이, 오래 점유하게 된다. 이처럼 “메모리 폭탄”을 막기 위해 앞단에서 크기를 제한하는 구조가 필요하다.
큰 요청은 전송과 파싱에 시간이 오래 걸린다. 그 과정에서 타임아웃이 발생하거나, 다른 요청들이 대기 상태에 빠질 수 있다. 요청 크기가 크면 전송 시간과 파싱 시간이 길어지고, 전체 처리 시간이 길어진다. 처리 시간이 길어질수록 응답이 지연되고, 타임아웃으로 인한 실패 확률이 높아진다. 클라이언트의 요청을 처리하는 데 관여하는 서비스들은 클라이언트가 지나치게 오래 기다리는 상황을 방지하고자 타임아웃이 걸려있는 것이 일반적이다.
만약 타임아웃이 걸리지 않아서 취소되지 않더라도, 응답이 지나치게 지연되면 다음과 같은 문제들이 발생할 수 있다.
즉, 큰 요청 하나 때문에 전체 서비스의 응답성이 저하될 수 있다.
대용량 요청은 네트워크 장애에 훨씬 더 취약하다. POST나 PUT 요청의 경우, 서버는 요청 본문(Body)을 끝까지 완전히 받아야만 유효한 요청으로 인식한다. 만약 전송 중 일부가 끊기면 HTTP 프로토콜 레벨에서 불완전한 요청(incomplete body)으로 간주되어 폐기된다. 따라서 전송 도중 네트워크가 끊기면 이미 업로드된 데이터는 버리고, 처음부터 다시 전송해야 한다.
이때 클라이언트가 자동 재시도를 여러 번 수행하면, 짧은 시간 안에 과도한 재전송 요청(rate spike)이 발생하여 시스템 부하를 가중시킬 수 있다. 참고로 이런 상황을 방지하기 위해 청크 업로드(Chunked Upload) 방식을 사용하기도 한다. 파일을 여러 조각으로 나누어 전송하고, 실패한 조각만 재전송함으로써 네트워크 오류에 대한 복원력을 확보하는 방식이다.
대용량 요청은 서비스 거부 공격(DoS, Denial of Service)의 수단이 될 수 있다. 공격자가 의도적으로 거대한 요청을 보내면, 서버는 파싱에 모든 리소스를 소비하게 된다.
이런 공격은 보통 두 가지 형태로 나타난다.
두 가지 경우 모두 요청 크기 제한과 타임아웃 설정이 제대로 되어 있지 않다면, 서버가 정상적인 요청을 처리하지 못하고 결국 마비될 수 있다.
우리 팀이 관리하는 서비스 중에는 고객들에게 제공하는 컨텐츠를 업로드하는 백오피스 툴이 있다. 운영자 편의를 위해 CSV를 이용한 대용량 업로드 기능이 구현되어 있고, 각 업로드 요청은 리비전 번호를 부여받는다. 즉, 최신 데이터는 가장 최근 리비전 번호를 가진 데이터를 의미한다.
이 툴은 다음과 같은 방식으로 사용되고 있었다.
이렇듯 데이터 관리 방식 자체가 누적형 구조이기 때문에, 시간이 지날수록 요청 사이즈가 커지는 게 당연했다. PayloadTooLargeError 에러를 해결하는 과정에서, 우리는 일단은 요청을 파싱하는 body-parser의 limit을 약간 올려두고, 장기적으로는 대용량 데이터 전송 구조를 개선하기로 했다.
그 과정에서 검토했던 아이디어는 다음과 같다.
기존 데이터와 새 데이터를 비교하여 변경된 부분(diff) 만 서버로 전송한다. 사용자는 전체 CSV 대신 일부 데이터만 업로드할 수 있으며, 데이터가 기존에 존재하면 덮어쓰고, 없으면 새로 추가하여 최종적으로 새로운 리비전 데이터로 취급한다.
서버에서 명확한 사이즈 제한을 두고, 클라이언트가 제한을 초과하면 경고를 반환하도록 한다. 앞의 아이디어와 마찬가지로, 전체 데이터가 아닌 부분 데이터만 업로드하는 방식이다.
큰 파일을 한 번에 업로드하지 않고, 여러 회차로 나누어 전송한다. 이 방식은 요청 크기를 줄일 뿐 아니라, 중간에 실패하더라도 해당 청크만 재전송할 수 있는 장점이 있다. 다만 요청 횟수가 많아져 네트워크 오버헤드가 늘고, 정합성 관리가 복잡해지는 단점이 있다.
데이터 객체에 ‘기간(period)’ 속성을 추가해 current(활성) 데이터와 past(비활성) 데이터를 분리 저장하는 방식이다. 하지만 이 방식은 근본적인 해결이 되기는 어려웠다. 만약 비활성 데이터 수정 요청이 발생하면 여전히 용량이 커진다는 문제가 있고, 활성 데이터 또한 이론상 크기가 얼마든지 커질 수 있기 때문이다.
이 아이디어를 조금 보완해서, 데이터를 분리해서 저장할 것이 아니라, 필터를 통해 분리해서 요청하는 방식으로 변경했다. 또한 페이지네이션(pagination) 도 함께 적용해 응답 크기를 줄이고 로딩 속도 개선 효과도 기대해 보기로 했다.
이 툴을 개발할 때, 처음부터 큰 규모의 데이터를 고려해서 설계했다면 좋았을 것이다. 사용자에게 제공할 컨텐츠가 많아질 거라는 것은 충분히 예상 가능한 일이기 때문이다. 이처럼 데이터의 확장을 미리 고려하여 설계하는 것은 매우 중요하다.
만약 지금 개발자가 되려고 공부 중이고, CRUD 기능을 구현하고 있다면 대용량 업로드 기능도 한 번쯤 생각해 보면 좋다. 나의 경우, 공부할 때는 단일 데이터에 대한 생성 및 수정만 생각했고, 실무를 시작한 이후에 생각의 범위를 키우느라 시간이 꽤 걸렸다. 생각의 범위는 다루는 데이터의 크기만큼 커진다. 어릴 땐 ‘10’이 가장 큰 수였던 것처럼, 개발도 경험과 함께 스케일이 확장된다.
데이터는 얼마든지 커질 수 있다. 사용자에게 편리한 대용량 전송 환경을 제공하는 것도 중요하지만, 동시에 서비스 안정성과 효율성을 함께 고려해야 한다. 기술이 발전하면서 우리가 다루는 데이터 단위는 점점 커지고 있지만, 적어도 아직까지는 무한하지 않다. 그러므로 항상 효율적으로 다루는 법을 고민해야 한다.
나 또한 이번 개선 과정을 거치며, 요청 크기 문제는 단순히 업로드 로직의 문제가 아니라, 데이터 구조 설계와 시스템 이해의 문제라는 걸 깨달았다. 요청을 가볍게 만드는 가장 확실한 방법은 전송을 최적화하는 것보다, 데이터 자체를 효율적으로 다루는 것이다. 이는 반드시 하나의 기술로만 해결해야 하는 것도 아니다. 결국 좋은 시스템은 데이터를 잘 다루는 시스템이고, 좋은 개발자는 그걸 예상할 수 있어야 한다. 그렇다면 지금의 나는 “데이터를 얼마나 깊이 이해하고 있을까?” 그 질문에서 시작해 보면 좋을 것이다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.