Spring Boot 파일 다운로드와 응답 처리 핵심
Spring Boot에서 파일 다운로드와 범용 응답 처리를 예제 중심으로 정리한 기술 설명. Stream 방식과 ResponseEntity 활용, 오류 처리 패턴
목차
개요
웹 애플리케이션에서 파일을 안정적으로 전송하는 것은 기본 요구사항이다. 특히 대용량 파일이나 다양한 클라이언트 환경을 고려하면 단순 전송만으로는 부족하다. 여기서는 Spring Boot에서 파일 다운로드를 구현할 때 자주 쓰는 방법을 정리한다. 주요 키워드는 spring boot 파일 다운로드 예제, spring boot response entity 파일, spring boot stream file download 등이다.
ResponseEntity를 이용한 기본 다운로드
작은 파일이나 메모리 로드가 가능한 경우 ResponseEntity<Resource>를 사용하면 간단하다. Content-Type과 Content-Disposition 헤더를 직접 설정해 브라우저가 파일로 인식하도록 한다.
예제: Resource 반환 방식
@GetMapping("/files/{name}")
public ResponseEntity<org.springframework.core.io.Resource> downloadFile(@PathVariable String name) throws IOException {
Path path = Paths.get("storage").resolve(name);
org.springframework.core.io.Resource resource = new org.springframework.core.io.PathResource(path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(path);
return ResponseEntity.ok()
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"")
.header(org.springframework.http.HttpHeaders.CONTENT_TYPE, contentType != null ? contentType : "application/octet-stream")
.body(resource);
}
이 방식은 간단하지만 파일이 메모리에 완전히 로드되거나 컨테이너가 파일 핸들을 관리해야 하는 상황에서 성능 제약이 있을 수 있다.
대용량 파일: StreamingResponseBody
대용량 파일이나 느린 네트워크 환경에서는 스트리밍 전송이 더 안전하다. StreamingResponseBody를 사용하면 서버가 직접 OutputStream으로 데이터를 스트리밍한다.
예제: 스트리밍 방식
@GetMapping("/stream/{name}")
public ResponseEntity<org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody> streamFile(@PathVariable String name) throws IOException {
Path path = Paths.get("storage").resolve(name);
if (!Files.exists(path)) {
return ResponseEntity.notFound().build();
}
StreamingResponseBody stream = outputStream -> {
try (InputStream in = Files.newInputStream(path)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
};
String contentType = Files.probeContentType(path);
return ResponseEntity.ok()
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"")
.header(org.springframework.http.HttpHeaders.CONTENT_TYPE, contentType != null ? contentType : "application/octet-stream")
.body(stream);
}
StreamingResponseBody는 서버 메모리 사용을 줄이고, 큰 파일 전송에서 타임아웃 조절과 더불어 안정성을 높인다.
범용 응답 포맷 설계
파일 전송 이외 엔드포인트와 일관된 응답 구조를 사용하면 클라이언트 구현이 쉬워진다. 성공/실패 구조를 규정하고, 메타 정보와 에러 코드를 포함시키는 것이 좋다.
예제: ApiResponse 클래스
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
// 생성자, getter, setter 생략
}
파일 다운로드는 파일 바디를 직접 반환하므로 ApiResponse와 함께 메타만 제공하거나, 실패 시 ApiResponse를 반환하도록 설계할 수 있다. 예를 들어 파일이 없을 때는 JSON 기반 ApiResponse로 404 응답을 보내는 식이다.
오류 처리와 예외 매핑
ControllerAdvice를 이용해 공통 예외를 잡고 적절한 HTTP 상태와 메시지를 반환하면 클라이언트가 일관된 방식으로 에러를 처리할 수 있다.
예제: 전역 예외 처리
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(java.nio.file.NoSuchFileException.class)
public ResponseEntity<ApiResponse<Object>> handleNoFile(NoSuchFileException ex) {
ApiResponse<Object> resp = new ApiResponse<>(false, "파일을 찾을 수 없습니다.", null);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(resp);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Object>> handleAll(Exception ex) {
ApiResponse<Object> resp = new ApiResponse<>(false, "서버 오류", null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(resp);
}
}
이렇게 하면 파일 다운로드 엔드포인트는 성공 시 파일 스트림을 반환하고, 실패 시 일관된 JSON 응답을 제공한다.
실무에서의 고려사항
- Content-Disposition 헤더를 정확히 설정해 파일명 깨짐을 방지한다.
- 대용량 파일은 StreamingResponseBody로 처리해 메모리 사용을 최소화한다.
- 권한 체크와 접근 제어를 다운로드 앞단에서 수행한다.
- 파일 경로는 외부 입력에 의해 조작되지 않도록 경로 정규화와 검증을 수행한다.
- 타임아웃, 접속 제한, 전송 중 중단 처리 로직을 고려한다.
맺음말
ResponseEntity 기반 전송과 StreamingResponseBody 기반 스트리밍을 상황에 맞게 조합하면 안정적인 파일 다운로드를 제공할 수 있다. 범용 응답 포맷과 전역 예외 처리를 더하면 클라이언트와 서버 간 계약이 명확해진다. 실무에서는 보안과 성능을 항상 함께 검토하는 것이 중요하다.