스프링 파일 다운로드

스프링 프레임워크 기반의 웹 애플리케이션에서 파일 다운로드 예제를 찾아보면 대부분은 서블릿 스택의 HttpServletResponse의 OutputStream 에 파일의 내용을 쓰는 방식으로 설명하는 경우가 많다. 그러나, 스프링 프레임워크에서는 바이트 처리에 대한 추상화가 되어있기 때문에 더 쉽고 간결한 파일 다운로드 예제 코드를 작성할 수 있다. 이리저리 찾아보며 활용할 수 있는 클래스들을 통해 아래와 같이 코드를 작성해보았다.

FileController.java
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.nio.charset.StandardCharsets; @RestController public class FileController { @GetMapping("/files/sample.csv") public ResponseEntity<byte[]> download() throws IOException { Resource resource = new ClassPathResource("sample/file.csv"); byte[] bytes = resource.getContentAsByteArray(); // NOTE: Use FileCopyUtils.copyToByteArray ContentDisposition contentDisposition = ContentDisposition.attachment() .filename("한글파일명.csv", StandardCharsets.UTF_8) .build(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes); } }
src/main/resources/templates/index.html
<!DOCTYPE html> <html> <body> <a href="/files/sample.csv">sample.csv</a> </body> </html>

Resource 와 ResponseEntity

ResponseEntity<Resource>도 가능하지만 더 명확한 표현을 위해서 byte[]를 명시하였다. ResponseEntity를 쓴 이유는 Content-Disposition 헤더를 통해 파일명을 지정하고 일반적인 바이너리 응답을 의미하도록 application/octet-stream 을 설정하기 위해서이다. HttpServletResponse를 핸들러 함수에 파라미터로 받아서 사용할 수도 있지만 굳이 필요하지 않음을 보여준다.

FileCopyUtils.copyToByteArray

스프링 프레임워크 6 부터는 오래전부터 제공하던 FileCopyUtils.copyToByteArray 를 사용하여 Resource를 바이트 배열로 바꾸는 함수를 제공한다. 만약, 스프링 프레임워크 5 이하의 버전이라면 Resource의 InputStream을 가져와서 FileCopyUtils.copyToByteArray를 직접 이용하면 된다.

ContentDisposition

일부 예제에서는 Content-Disposition 헤더를 지정하기 위해서 문자열을 입력하는 것을 볼 수 있다. 잘못된 것은 아니지만 사람이 입력하는데 실수를 할 수 있기 때문에 스프링 프레임워크에 포함된 ContentDisposition 클래스를 이용해서 실수를 방지할 수 있다. 심지어 한글로 된 파일명을 지정하기 위해서는 URLEncoder를 사용해야하는데 ContentDisposition 클래스 내부적으로 RFC 6266 와 RFC 2047 에 따라 URL 인코딩을 수행하므로 이에 대한 과정도 생략할 수 있다.


어떤가요? 여러분이 작성한 코드보다 간결해졌나요? Downloading a file from spring controllers 에서 더 많은 예제 코드를 확인할 수 있습니다.


대용량 파일 다운로드

일반적인 파일 다운로드는 위와 같이 바이트 배열을 응답하여 처리할 수 있지만 용량이 큰 파일을 다운로드해야하는 경우라면 애플리케이션 메모리에 부담이 있을 수 있다. 이 경우 StreamingResponseBody를 활용하여 아래와 같이 파일을 스트리밍할 수 있다.

@RestController
public class FileController {
    @GetMapping("/files/sample.mp4")
    public ResponseEntity<StreamingResponseBody> download() {
        Resource resource = new ClassPathResource("sample/sample.mp4");
        StreamingResponseBody responseBody = output ->
                StreamUtils.copy(resource.getInputStream(), new BufferedOutputStream(output)); // NOTE: 8192 bytes.
        ContentDisposition contentDisposition = ContentDisposition.attachment()
                .filename("동영상.mp4", StandardCharsets.UTF_8)
                .build();
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(responseBody);
    }
}

동영상 스트리밍

간단한 동영상 스트리밍을 제공하고 싶은 경우 ResourceRegion 을 사용하여 동영상 플레이어에서 전체 파일을 다운로드 받지 않아도 원하는 위치부터 다운로드 받을 수 있도록 처리할 수 있다. 하지만, 대부분의 스트리밍 사이트의 경우 동영상 파일을 잘게 쪼개해두고 CDN으로 처리하는 것 같다.

@RestController
public class FileController {
    @GetMapping("/sample.mp4")
    public ResponseEntity<List<ResourceRegion>> streamingVideo(@RequestHeader HttpHeaders headers) throws MalformedURLException {
        Resource resource = new UrlResource("https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_30mb.mp4");
        List<ResourceRegion> resourceRegions = HttpRange.toResourceRegions(headers.getRange(), resource);
        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                .contentType(MediaType.parseMediaType("video/mp4"))
                .body(resourceRegions);
    }

    @GetMapping("/files/sample.stream")
    public ResponseEntity<Resource> streamingVideo() throws MalformedURLException {
        Resource resource = new UrlResource("https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_30mb.mp4");
        return ResponseEntity.ok().body(resource);
    }
}

동영상 스트리밍에 대해서는 AbstractMessageConverterMethodProcessor에 구현되어 있어서 InputStreamResource가 아니라면 HttpRange를 직접 사용하지 않아도 알아서 처리된다고 한다.