SameOS ~/tools/php-large-file-download.md

PHP로 대용량 파일 안전하게 내려주기

개인 서버에서 백업 아카이브나 동영상 같은 큰 파일을 PHP로 내려줄 때 자주 만나는 문제는 세 가지입니다. 메모리를 다 먹는 출력 버퍼(보낼 내용을 서버가 잠시 모아 두는 임시 공간), 한글 파일명이 깨지는 Content-Disposition 헤더, 그리고 CDN이나 리버스 프록시(방문자와 서버 사이에서 요청을 대신 받아 전달하는 중계 서버) 뒤에서 발생하는 타임아웃입니다. 실제 운영 중인 코드에서 검증된 처리 방법을 정리했습니다.

출력 버퍼 정리와 청크 스트리밍

readfile()이나 file_get_contents()로 큰 파일을 그대로 출력하면 PHP 출력 버퍼와 메모리에 파일 전체가 올라갈 수 있습니다. 전송 전에 열려 있는 출력 버퍼를 모두 닫고, 8KB 단위로 읽어서 바로 내보내면 파일 크기와 무관하게 메모리 사용이 일정해집니다.

한글 파일명이 깨지지 않는 Content-Disposition

Content-Disposition 헤더의 filename 값에 한글을 그대로 넣으면 브라우저나 다운로드 관리자에 따라 깨집니다. 표준 해법은 두 값을 함께 보내는 것입니다. ASCII로 대체한 filename을 구형 클라이언트용으로, RFC 5987 방식의 filename*=UTF-8 값을 최신 브라우저용으로 같이 넣으면 양쪽 모두 안전합니다.

CDN·리버스 프록시 뒤에서의 타임아웃

Cloudflare 같은 CDN은 원본 서버가 일정 시간(무료 요금제 기준 약 100초) 동안 아무 응답도 보내지 않으면 연결을 끊습니다. 파일을 만들거나 변환하는 데 오래 걸리는 작업이라면, 준비가 끝날 때까지 침묵하지 말고 진행 상황을 조금씩 출력해 연결을 살려 두어야 합니다. Nginx를 쓴다면 X-Accel-Buffering: no 헤더로 응답 버퍼링을 꺼야 출력이 실시간으로 전달됩니다.

아래는 이 세 가지를 반영한 전송 함수입니다. 파일 경로 검증(basename 처리, 허용 폴더 확인)은 호출 전에 반드시 별도로 해야 합니다.

function stream_file(string $file): void {
    $name = basename($file);
    $ascii = preg_replace('/[^\x20-\x7e]/', '_', $name);
    $utf8  = rawurlencode($name);

    while (ob_get_level()) {
        ob_end_clean();               // 열려 있는 출력 버퍼 전부 정리
    }
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . $ascii . '"'
         . "; filename*=UTF-8''" . $utf8);
    header('Content-Length: ' . filesize($file));
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('X-Accel-Buffering: no');  // Nginx 응답 버퍼링 끄기

    $h = fopen($file, 'rb');
    while (!feof($h)) {
        echo fread($h, 8192);         // 8KB 청크 스트리밍
        flush();
    }
    fclose($h);
}

운영 팁

전송 완료 후 임시 파일을 지우는 일회성 다운로드라면 스트리밍이 끝난 뒤 unlink()(파일 삭제 함수)를 호출하면 됩니다. 다운로드 중단에 대비해 임시 폴더는 cron(정해 둔 시간마다 명령을 자동 실행해 주는 리눅스 스케줄러)으로 오래된 파일을 주기적으로 정리하는 편이 안전합니다. 재개(Range 요청)가 필요한 경우에는 PHP 대신 Nginx의 X-Accel-Redirect로 정적 전송을 맡기는 쪽이 훨씬 효율적입니다.

전체 스크립트 받기 (GitHub) PHP와 Nginx로 비공개 파일 직접 접근 막기 Nginx 파일 보호 규칙 생성기