TIL

Java로 구현하는 스트리밍 파일 다운로드

빈코 2025. 4. 23. 13:36

Java Streaming

개요

안녕하세요, 빈코입니다. 오늘은 Java에서 대용량 파일을 다운로드할 때 메모리 부하를 줄이기 위해 사용하는 '스트리밍 다운로드' 방식에 대해 이야기해보려고 합니다. 스트리밍은 데이터를 한 번에 모두 불러오는 것이 아니라, 조각조각 나눠서 전송하는 방식인데요.
특히 큰 파일을 다운로드하거나, 여러 사용자가 동시에 파일을 요청해 메모리 사용량이 급증할 수 있는 상황에서 자주 활용됩니다.

그럼 이제 실제 코드를 보면서 좀 더 자세히 설명드릴게요😃

 

Front📙

<a class="download_test">
   <span id="download_test">다운로드 테스트</span>
</a>

 

첫 번째로, download_test라는 버튼을 UI에 정의하였습니다. 사용자들은 해당 버튼을 통해 다운로드를 진행하게 됩니다.

 

document.getElementById('download_test').addEventListener('click', async () => {
   if (isDownloading) return; // 이미 다운로드 중이면 무시
   isDownloading = true;

   const button = document.getElementById('download_test');
   button.innerText = '다운로드 중..';
   try {
      const response = await fetch('/download/file?name=testfile}'); // 서버 파일 다운로드 API URL
      if (!response.ok) {
         throw new Error('파일 다운로드에 실패했습니다.');
      }

      // 스트리밍 데이터를 Blob으로 변환
      const contentType = response.headers.get('Content-Type');
      const blob = await response.blob();

      // Blob을 링크로 변환하여 다운로드 트리거
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;

      // 파일 이름 설정 (Content-Disposition 헤더에서 추출 가능)
      const disposition = response.headers.get('Content-Disposition');
      let filename = 'testfile';
      if (disposition && disposition.includes('filename=')) {
         filename = disposition.split('filename=')[1].replace(/"/g", '');
      }

      a.download = filename;
      a.click();

      // 메모리 해제
      URL.revokeObjectURL(url);
   } catch (error) {
      alert('다운로드 중 오류 발생');
   } finally {
      isDownloading = false;
      button.innerText = '다운로드 테스트';;
   }
});

 

JS 코드는 주석을 통해 설명을 드렸지만, 가장 핵심적인 포인트는 다운로드가 비동기 방식으로 진행된다는 점입니다.
fetchBlob을 함께 사용해, 서버에서 스트리밍된 파일을 Blob 객체로 변환한 뒤, 자동으로 다운로드 링크를 클릭하는 방식으로 구현했습니다.

 

fetch는 브라우저가 서버에 비동기적으로 파일을 요청하고, 서버는 그 파일 데이터를 스트리밍 방식으로 조각조각 전송하게 됩니다.
JS에서는 이 스트리밍된 데이터를 받아서, 브라우저 메모리 상에서 Blob 객체로 변환하는 과정을 거치죠.

중요한 점은, fetch로 받아온 응답은 스트림 형태이기 때문에 바로 파일로 저장할 수 없고, 반드시 Blob 객체로의 변환 작업이 필요하다는 것입니다.

 

추가적으로 파일의 용량이 상당히 클 경우 스트리밍 방식으로 쪼개서 실시간으로 진행하여도 시간이 걸릴 수 있기 때문에, 다운로드 클릭 시 버튼의 문구를 변경하고, 다운로드가 완료되었을 때 원래 문구로 돌려놓는 작업도 함께 진행하였습니다.

 

Controller📘

@RequestMapping(value = "/download/file", method = RequestMethod.GET)
public StreamingResponseBody fileDownloadTest(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) throws Exception {
  
   ListenableFuture<StreamingResponseBody> result =downloadService.fileDownGet(paramMap);

   return result.get(5, TimeUnit.MINUTES);
};

 

Controller는 클라이언트 요청 수신 및 서비스를 호출하는 역할을 합니다. 비동기 응답(ListenableFuture)을 StreamingResponseBody로 반환하여 큰 파일도 효율적으로 처리할 수 있도록 구현했습니다.

 

Service📒

@Override
public ListenableFuture<StreamingResponseBody> fileDownGet(Map<String, Object> paramMap) throws Exception {
   boolean success = false;
   if (paramMap.containsKey("name")) {
      String fileName = paramMap.get("name").toString();
      String filePath = null;
      JSONParser parser = new JSONParser();
      JSONArray array = (JSONArray) parser.parse(downCfg);
      
      for (int i=0; i<array.size(); i++) {
         JSONObject obj = (JSONObject) array.get(i);
          if(obj.get("name").equals(fileName)||(fileName.indexOf("{")>-1&&fileName.indexOf("}")>-1)){
             filePath = (String) obj.get("path");
             break;
          }
      }

      if (filePath!=null) {
         InputStream inputStream = Files.newInputStream(Paths.get(filePath));

         StreamingResponseBody streamingResponseBody = outputStream -> {
            FileCopyUtils.copy(inputStream, outputStream);
         };
         return new AsyncResult<>(streamingResponseBody);
      }
   }
   return null;
}

 

StreamingResponseBody는 Spring에서 스트리밍 응답을 보내기 위한 객체입니다. 흐름적인 부분은 요청 파라미터에서 fileName을 추출하고 설정을 파싱하여 해당 파일 경로를 조회하는 과정을 거칩니다.

 

해당 파일이 있을 경우에 InputStream으로 파일을 읽고 StreamingResponseBody로 스트리밍 처리를 진행한 후에 AsyncResult로 비동기로 반환하게 됩니다.

 

마치며

지금까지 Java에서 스트리밍 방식을 활용해 파일을 다운로드하는 방법에 대해 알아보았습니다.

 

특히 설치 파일처럼 여러 사용자가 동시에 다운로드를 요청하는 경우, 파일 용량도 크고 시스템에 부하가 걸릴 수 있기 때문에,
이러한 스트리밍 방식을 적용해 안정적으로 동작하도록 구현해두는 것이 좋습니다🙇‍♂️

반응형