미션 : 저장한 파일을 조회해서 클라이언트가 다운로드하도록 코드 짜기
내츄럴 방법으로 파일 읽기
파일을 읽고 클라이언트에 제공하려면 가장 기본적으로 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
- Java에서 파일, 네트워크 리소스, URL 등의 API를 직접 다루면 각각 리소스를 처리하는 방식이 달라져서 힘들다
- 이처럼, 리소스 유형별로 접근 방식이 다르기 때문에, 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이 일정 부분 관리
- 리소스 관리되는 과정
- ResponseEntity<Resource>를 반환
- Spring MVC는 내부적으로 Resource의 InputStream을 HTTP 응답 스트림(OutputStream)으로 복사한다.
- 이때, 복사가 완료되면 기본적으로 HTTP 응답 스트림이 닫힌다.
- 근데 이게 닫아준다는 것보다는 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 지정도 필요한데, 이건 너무 글이 길어지니 여기서 마무리
만들어본 코드
'백엔드 > 스프링' 카테고리의 다른 글
단위 테스트가 어려운 이유: 과도한 Mocking (0) | 2025.03.29 |
---|---|
복잡한 동적 쿼리 테스트하기 (0) | 2025.03.27 |
Swagger 테스트 시 - application/octet-stream is not supported (0) | 2025.03.05 |
Swagger 오류 : Failed to load API definition (1) | 2025.02.24 |
외부 API 호출하는 방법 (feat. RestClient) (0) | 2025.02.20 |