← 전체 글로 돌아가기

서버 운영

Nginx 업로드 413을 제한값과 타임아웃으로 나눠 고친 과정

파일 업로드가 간헐적으로 실패할 때 요청 크기와 응답 대기 시간을 구분해 수정한 트러블슈팅 기록입니다.

실패가 항상 같은 413은 아니었다

이미지 업로드 기능을 붙인 뒤 8MB 파일에서만 실패한다는 제보를 받았습니다. 처음에는 애플리케이션의 파일 검증만 살폈는데, 애플리케이션 로그에 요청 자체가 남지 않았습니다. Nginx가 앞에서 거절하고 있다는 신호였습니다.

반대로 4MB 파일은 업로드되지만 모바일 네트워크에서 60초쯤 멈추는 경우도 있었습니다. 두 증상을 하나의 “업로드 오류”로 묶은 것이 첫 번째 실수였습니다.

크기 제한을 먼저 맞췄다

Nginx의 client_max_body_size와 애플리케이션의 제한값을 서로 다르게 두면 어느 계층에서 막혔는지 헷갈립니다. 서비스 정책을 10MB로 정하고 프록시와 앱에 같은 기준을 적용했습니다.

server {
    client_max_body_size 10m;
    location /api/upload {
        proxy_pass http://app:3000;
        proxy_request_buffering off;
        proxy_read_timeout 120s;
    }
}

설정 변경 후에는 문법 검사와 reload를 분리했습니다.

sudo nginx -t
sudo systemctl reload nginx
curl -i -X POST -H 'Content-Type: application/octet-stream' --data-binary @sample-11mb.bin https://example.com/api/upload

11MB 테스트 파일이 413을 받고, 9MB 파일은 앱까지 도달하는지 로그로 확인했습니다. 제한 초과를 413으로 처리하는 것은 정상 동작이므로 무조건 서버 장애로 보지 않기로 했습니다.

느린 업로드는 별도 기준으로 잡았다

크기가 허용된 파일이 오래 걸리는 현상은 client_max_body_size와 관계가 없습니다. 업스트림 응답을 기다리는 proxy_read_timeout과 앱의 처리 시간, 그리고 클라이언트 재시도 정책을 따로 봐야 했습니다. 업로드 중 재시도하면 같은 파일이 중복 저장될 수 있어 요청 ID를 함께 기록했습니다.

앱에서는 MIME 타입만 믿지 않고 확장자와 실제 파일 시그니처도 확인하도록 보완했습니다. 외부 저장소로 넘기기 전 임시 파일을 정리하는 cron도 추가했습니다.

운영 체크리스트

  • 프록시와 애플리케이션의 최대 크기가 같은가?
  • 413과 timeout을 상태 코드와 로그로 구분했는가?
  • 큰 파일, 빈 파일, 손상 파일을 각각 시험했는가?
  • 재시도 때 중복 저장을 막을 키가 있는가?
  • 변경 후 nginx -t와 실제 업로드를 모두 실행했는가?

이번 수정의 핵심은 제한값을 키우는 것이 아니라, 크기 문제와 시간 문제를 다른 사건으로 분리한 데 있었습니다.