본문 바로가기
백엔드/스프링

Spring에서 파일 다운로드 코드 구현하기 (다양한 리소스)

by ARlegro 2025. 3. 10.

미션 : 저장한 파일을 조회해서 클라이언트가 다운로드하도록 코드 짜기

 

 

내츄럴 방법으로 파일 읽기

파일을 읽고 클라이언트에 제공하려면 가장 기본적으로 Java에서 제공하는 API인 InputStream을 사용할 수 있다.

Inputstream inputStream = Files.newInputStream(path)
inputstream.read~~~~

 

InputStream을 통해 파일을 읽고, 이 데이터를 HTTP 응답으로 내려주는 방식을 기본적으로 써야한다고 생각할 수 있다

 

❌ 한계

하지만 InputStream을 직접 다룰 때는 몇 가지 단점이 있다

  • 리소스 관리가 까다롭다
    • 직접 열고 닫아야 해서 close() or try-with-resources 같은 방식으로 관리해줘야 한다
    • 안 그러면 메모리 누수가 발생
  • Spring에서 일관된 방식으로 다루기 어렵다.
    • Spring에서는 파일뿐만 아니라 URL, 클래스 패스 등 다양한 리소스를 다루는데,
    • InputStream을 직접 사용하면 일관된 방식으로 처리하기 어렵다
  • 한 번만 읽을 수 있다
    • InputStream은 한 번 읽으면 다시 읽을 수 없음
    • 만약, 다운로드 도중 오류가 나서 재시도를 해야할 경우 문제가 생긴다.
    • 물론, 계속해서 배울 Resource의 구현체인 InputStreamResource도 이 단점을 가지지만 다른 구현체들은 이러한 단점을 해소할 수 있다(FileSystemResource, ByteArrayResource)

이러한 문제를 해결하기 위해, Spring에서 Resource 인터페이스를 제공

(참고로, 각 구현체가 모든 문제를 해결해주지는 않고, Resource구현체마다 다르다)

 


Resource 인터페이스란???

  • 파일, 클래스패스, URL, InputStream 등의 리소스들을 일관된 방식으로 다룰 수 있도록 제공하는 추상화된 타입
    • Java에서 파일, 네트워크 리소스, URL 등의 API를 직접 다루면 각각 리소스를 처리하는 방식이 달라져서 힘들다
      • 파일 ⇒ FileInputStream
      • 클래스패스 리소스 ⇒ ClassLoader.getResourceAsStream()
      • URL ⇒ URL.openStream
  • 이처럼, 리소스 유형별로 접근 방식이 다르기 때문에, Spring은 이를 일관된 방식으로 처리할 수 있도록 Resource 인터페이스를 제공
    • 예를 들어, getInputStream() 을 통해 데이터를 읽을 수 있도록 통일된 API
  • 리소스 관리도 일정 부분 해결
    • HTTP 응답으로 보낼 때 일정 부분 관리(관리라기보단 더 이상 못 쓰는 것)

✅사용되는 경우

  • ResponseEntity<Resource> → 파일 다운로드 API에서 사용.
    • ResponseEntity 응답 본문에는 직렬화 가능한 객체를 담아야 하는데, Resoucre타입은 가능하다
    • 반면, InputStream은 직렬화가 불가능
  • 그 외, Spring batych, @Value </aside>
  • 뒤에서 자세히 나옴 

 


 

InputStreamResource 배우기

개념

  • Spring프레임워크의 core.io 패키지에서 제공하는 클래스
  • InputStream을(다른 것도 가능) Spring에서 다룰 수 있는 Resource 객체로 변환해주는 역할?
    • InputStream이 Spring의 Resource 인터페이스를 따를 수 있도록해서 쉽게 활용될 수 있도록 해주는 래퍼 클래스 같은 것
  • 주요 메서드
    getInputStream() InputStream 반환
    exists() 리소스 존재 여부 확인
    isOpen() 스트림이 열려 있는지 확인
    getDescription() 리소스 설명 문자열 반환

 

왜 InputStremaResource로 감싸는가?

  • 기본적으로 InputStream은 Java의 표준 스트림이다(spring과 직접적인 연동 X)
  • But Spring에서는 Resource 인터페이스를 사용하여 파일, URL, 클래스패스 등의 다양한 리소스를 일관된 방식으로 다루려는 설계 철학을 가짐
    • InputStream을 Spring의 Resource 객체로 감싸서 Spring이 요구하는 표준 방식으로 다룰 수 있음.

1. Spring의 Resource 인터페이스와의 호환성

  • Resource 타입을 요구하는 Spring의 다양한 기능과 쉽게 통합할 수 있다.
  • 이미 존재하는 InputStream을 Resource로 감싸서 Spring과 호환되도록
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile() {
    Resource resource = new InputStreamResource(inputStream); <<<<< 요거 

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=data.txt") 
									// 다운로드 강제
            .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream") 
									// 바이너리 파일로 인식
            .body(resource);
}

2. HTTP Response Streaming과의 호환

  • InputStreamResource는 HTTP 응답으로 데이터를 스트리밍하는데 적합
  • 파일 크기가 크더라도 메모리에 한꺼번에 로드하지 않고 스트리밍 전송이 가능
    • 파일을 한 번에 메모리에 올리지 않고 조각(chunk) 단위로 읽어서 전송.
    • 메모리 사용량을 줄일 수 있다.
      • OutOfMemoryError 가능성 줄임
    • 클라이언트가 다운로드를 중단하면, InputStream의 남은 데이터를 읽지 않음.
  • 그니까 데이터를 보낼떄 천천히 읽으면서 줄줄줄 ~ 보내기

 

3. 리소스 관리 용이

  • InputStreamResource사용 시 Spring이 기본적으로 리소스 관리를 해주는 것은 아니지만, ResponseEntity<Resource>같이 HTTP 응답으로 보낼 때, Spring이 일정 부분 관리
  • 리소스 관리되는 과정
    1. ResponseEntity<Resource>를 반환
    2. Spring MVC는 내부적으로 Resource의 InputStream을 HTTP 응답 스트림(OutputStream)으로 복사한다.
    3. 이때, 복사가 완료되면 기본적으로 HTTP 응답 스트림이 닫힌다.
    4. 근데 이게 닫아준다는 것보다는 InputStream을 더 이상 사용할 수 없는 상태가 되는 것에 가깝다 (InputStreamResource는 재사용 불가)
      • But 코드에서 InputStreamResource 내부의 InputStream을 따로 사용하고 있었다면 여전히 수동으로 닫아야 한다.

4. 네트워크 리소스와의 호환

  • 파일뿐만 아니라 네트워크에서 가져온 데이터도 처리 가능하다.
    ex. HTTP API에서 받은 데이터를 InputStreamResource로 감싸서 클라이언트에게 전달할 수 있다.
  • 네트워크에서 받은 데이터를 바로 응답으로 보내야 할 때 유용
InputStream inputStream = new URL("<https://example.com/file.txt>").openStream();
Resource resource = new InputStreamResource(inputStream);

 

특장단점 정리 (InputStreamResource 구현체)

특장점

  • InputStream을 직접 다룰 수 있다
    • 이로 인해, 파일뿐만 아니라 다양한 리소스(네트워크, 메모리, 압축파일 등)에 가능하다
  • 일관된 방식으로 다양한 리소스 지원 가능
  • 파일 경로가 없어도 된다
    • 따라서, 클라우드 스토리지(AWS S3, Google Cloud Storage)에서 데이터를 가져와야 할 경우 InputStreamResource가 필요 !!!!!
    • 반면, FileSystemResource는 로컬 파일 시스템에 저장된 파일만 처리 가능
  • 동적으로 생성한 데이터를 응답할 때 유용

단점

  • 한 번 읽으면 다시 못 읽음
    • 다운로드 중간에 실패하면 다시 읽을 수 없다
    • 반면, FileSystemResource는 필요할 때마다 InputStream 생성 가능하므로 여러번 가능
  • 자동 닫힘 X : getInputSteam을 통해 InputStream을 직접 사용하면 직접 관리해야 한다

적합한 경우

1. 네트워크 스트림을 다운로드할 때

2. ResponseEntity<Resource> 형태로 API에서 파일 스트리밍할 때

3. InputStream을 Resource 타입으로 변환해야 할 때

  • InputStream → 자바
  • Resource → 스프링
  • 즉, 스프링과 호환성을 위해서 Resource로 변환해야 하는 경우가 생김

4. 메모리를 효율적으로 관리하며 파일을 다룰 때

 

 

다운로드 코드를 만들때는 Header 지정도 필요한데, 이건 너무 글이 길어지니 여기서 마무리 

 

 

만들어본 코드