HTTP multi-part 파일 업로드를 받을 수 있는 서버 애플리케이션을 생성해보겠습니다.
파일 업로드 요청을 받아들일 수 있는 스프링 부트 웹 애플리케이션을 생성합니다. 테스트 파일을 올릴 수 있는 간단한 HTML 인터페이스도 빌드할 것입니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'guides'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
클래스 레벨 어노테이션:
@Controller
, @RestController
: 클래스가 컨트롤러임을 지정합니다. @RestController
는 @Controller
와 @ResponseBody
를 합친 것으로 RESTful 서비스에서 사용됩니다.@Service
: 비즈니스 로직을 담당하는 서비스 클래스임을 지정합니다.@Repository
: 데이터베이스와의 상호 작용을 담당하는 DAO(Data Access Object) 클래스임을 지정합니다.@Configuration
: Spring Bean 구성을 담당하는 클래스임을 지정합니다.메서드 레벨 어노테이션:
@ResponseBody
: 메서드가 반환하는 값을 HTTP 응답 본문으로 사용하도록 지정합니다.@GetMapping
, @PostMapping
, @PutMapping
, @DeleteMapping
: 각각 GET, POST, PUT, DELETE 요청을 처리하는 메서드로 지정합니다.@ExceptionHandler
: 특정 예외를 처리하는 메서드로 사용됩니다.메서드 및 클래스 레벨 어노테이션
이러한 애너테이션들은 메서드 레벨에서는 해당 메서드에 적용되며, 클래스 레벨에서는 클래스 내의 모든 메서드에 적용됩니다. 이를 통해 특정한 기능이나 설정을 일괄적으로 적용할 수 있습니다.
@RequestMapping
: 이미 언급한 것처럼 URL 경로를 매핑하며, 메서드 레벨과 클래스 레벨에서 모두 사용될 수 있습니다.@Transactional
: 데이터베이스 트랜잭션 처리를 위한 애너테이션으로, 메서드 또는 클래스에 적용할 수 있습니다.@Secured
, @PreAuthorize
, @PostAuthorize
: 스프링 시큐리티를 이용하여 메서드 또는 클래스의 접근 권한을 제어하는 애너테이션입니다.@CrossOrigin
: CORS(Cross-Origin Resource Sharing)을 허용하기 위해 사용되며, 메서드 또는 클래스에 적용할 수 있습니다.메서드 매개변수 레벨 어노테이션
@RequestParam
: HTTP 요청에서 쿼리 매개변수나 폼 매개변수를 추출합니다. 이는 메서드의 매개변수로 전달된다. 예를 들어, ?id=1
과 같은 쿼리 매개변수를 처리할 때 사용됩니다.
@PathVariable
: URI 경로의 일부인 경로 변수를 추출합니다. /users/{userId}
와 같은 경로에서 userId
를 추출할 때 사용됩니다.
@RequestBody
: HTTP 요청의 본문을 자바 객체로 변환합니다. 주로 JSON 또는 XML과 같은 형태의 요청 본문을 자바 객체로 변환할 때 사용됩니다.
@RequestHeader
: HTTP 요청의 헤더 값을 추출합니다. 특정 헤더 값을 추출하거나 헤더 값을 메서드의 매개변수로 전달할 때 사용됩니다.
@RequestAttribute
: HTTP 요청의 어트리뷰트 값을 추출합니다. 주로 프레임워크 수준의 요청 어트리뷰트 값을 가져올 때 사용됩니다.
@RequestParamMap
: HTTP 요청의 모든 쿼리 매개변수를 Map으로 받습니다.
Buffer: 임시 데이터 저장 공간으로 입출력 속도 조절이나 데이터 버퍼링에 사용됩니다.
List: 데이터를 순차적으로 저장하고, 삽입, 삭제 등의 연산에 유용한 데이터 구조입니다.
Channel: 입출력을 위한 연결 통로로, 파일이나 네트워크 등 데이터를 읽고 쓰는데 사용됩니다.
Queue: 선입선출(FIFO) 구조로 데이터를 저장하며, 대기열을 다루거나 작업을 조율할 때 유용합니다.
Stream: 연속적인 데이터의 흐름을 다루며, 데이터의 처리 및 전송에 사용됩니다.
구분 | 특징 | 사용 목적 | 예시 |
---|---|---|---|
Buffer | 데이터를 일시적으로 저장하는 영역 | 데이터를 임시로 버퍼링하거나 입출력 속도를 조절하기 위해 사용 | ByteBuffer, CharBuffer 등 |
List | 순차적인 데이터 구조 | 데이터를 순차적으로 저장하고 관리하는 목적에 사용 | ArrayList, LinkedList 등 |
Channel | 입출력을 위한 연결 통로 | 데이터를 읽고 쓰는데 사용되는 입출력 채널 | FileChannel, SocketChannel 등 |
Queue | FIFO(First-In-First-Out) 구조 | 데이터를 먼저 넣은 순서대로 꺼내는 구조 | LinkedList, ArrayDeque 등 |
Stream | 데이터의 연속적인 흐름을 다룸 | 데이터를 연속적으로 처리하거나 전송하는 용도에 사용 | InputStream, OutputStream, FileInputStream 등 |
java.nio
와 java.io
패키지는 Java에서 파일과 입출력을 다루는 데 사용되는 패키지들입니다. 각각의 패키지는 다음과 같은 차이점이 있습니다:
버퍼 지향(I/O) vs 스트림 지향(I/O):
java.io
패키지는 스트림 기반의 입출력을 제공합니다. InputStream
과 OutputStream
을 사용하여 데이터를 읽고 쓸 수 있습니다. 이는 데이터가 연속적으로 흐르는 스트림을 통해 이루어집니다.java.nio
패키지는 버퍼 지향의 입출력을 제공합니다. Buffer
클래스와 Channel
인터페이스를 사용하여 데이터를 버퍼에 읽고 쓰며, 데이터를 읽고 쓸 때 더욱 유연한 작업이 가능합니다.블로킹 vs 논블로킹 I/O:
java.io
는 주로 블로킹 I/O를 사용합니다. I/O 작업이 완료될 때까지 프로그램이 대기하며, 데이터가 도착하기 전까지 다른 작업을 수행할 수 없습니다.java.nio
는 논블로킹 I/O를 지원합니다. 비동기적인 입출력 작업을 통해 다중 채널을 단일 스레드로 관리할 수 있으며, 채널이 데이터를 기다리는 동안에도 다른 작업을 수행할 수 있습니다.스케일러블한 I/O:
java.nio
는 다중 스레드로 작업을 처리하는 경우에 더 효율적입니다. 대용량 데이터 처리나 다중 채널 관리 시에 java.nio
는 더 나은 성능을 보일 수 있습니다.새로운 기능들:
java.nio
는 비동기 파일 채널, 멀티플렉서, 셀렉터 등의 새로운 기능을 제공합니다. 이러한 기능들은 네트워킹과 관련된 작업이나 파일 I/O 작업을 더욱 효과적으로 다룰 수 있게 도와줍니다.일반적으로, java.nio
는 더욱 많은 기능과 유연성을 제공하지만, 사용하기에는 더 복잡할 수 있습니다. 작은 파일이나 간단한 입출력 작업을 처리할 때는 java.io
가 간단하고 편리할 수 있습니다.
Files: 자바 NIO(Non-blocking I/O) 파일 관련 유틸리티를 제공하는 클래스로, 파일의 복사, 이동, 삭제 등 다양한 파일 작업을 지원합니다.
MultipartFile: HTTP 멀티파트 요청에서 업로드된 파일을 나타내는 인터페이스로, 스프링에서 파일 업로드 처리를 위해 사용됩니다. InputStreamSource
를 통해 데이터를 읽을 수 있습니다.
InputStreamSource
를 확장하여 스트림으로부터 데이터를 읽을 수 있도록 합니다.Model: 스프링에서 데이터를 뷰로 전달하기 위한 인터페이스로, 뷰에 전달할 데이터를 담는 역할을 합니다.
MvcUriComponentsBuilder: 스프링 MVC에서 URI(Uniform Resource Identifier)를 생성하는 빌더 클래스로, 컨트롤러 및 메서드에 대한 URI를 동적으로 생성하는데 사용됩니다.
https://www.example.com/about
와 같은 웹 주소가 URI의 한 형태입니다.Model
을 확장하여 모델 데이터를 리다이렉트 시에도 유지할 수 있게 합니다.리다이렉트 요청은 클라이언트의 요청을 다른 위치로 전환하는 데 사용됩니다. 일반적으로, 이전에 요청한 URL에서 새로운 URL로 사용자를 전송하거나, 웹 애플리케이션에서 다른 페이지로 이동할 때 사용됩니다. 이는 사용자가 새로운 위치로 이동하거나, 새로운 리소스를 찾을 수 있도록 돕는 데 사용됩니다. 종종 HTTP 상태 코드 중 하나인 3xx 코드와 함께 사용되며, 서버에서 클라이언트로 리다이렉션 정보를 전송하여 처리합니다.
Flash attribute는 일시적으로 데이터를 저장하고 다음 요청으로 전달하는 메커니즘입니다. 스프링 프레임워크에서는 주로 리다이렉트 후에 데이터를 유지하고 싶을 때 사용됩니다. Flash attribute를 사용하면 다음과 같은 상황에서 데이터를 전달할 수 있습니다:
Flash attribute는 일시적으로 유지되기 때문에 한 번의 요청에 대한 응답 이후 소멸됩니다. 이를 통해 리다이렉트 직후에 데이터를 전달하고 이후에는 필요하지 않을 때 사용됩니다.
일반적으로 Flash attribute는 주로 사용자에게 성공 또는 실패 메시지를 표시하거나 리다이렉트 후에 데이터를 전달할 때 활용됩니다.
Path: 파일 시스템 경로를 나타내는 인터페이스로, 파일 또는 디렉토리의 경로를 추상화합니다. Comparable
로 비교 가능하며, Iterable
로 요소를 순회하고, Watchable
로 파일 시스템의 변경을 감시할 수 있습니다.
Paths: 파일 시스템의 경로를 생성하는 유틸리티 클래스로, 파일 시스템 경로를 다루기 쉽게 만들어 줍니다.
StandardCopyOption: 파일 복사에 대한 옵션을 제공하는 열거형 클래스로, 파일을 복사할 때 동작을 지정하는 옵션들을 정의합니다.
FileSystemUtils: 파일 시스템 관련 유틸리티를 제공하는 추상 클래스로, 파일 및 디렉토리 관련 작업을 수행하는 메서드를 제공합니다.
Files.walk(Path start, int maxDepth, FileVisitOption... options)
start
경로부터 시작하여 maxDepth
깊이까지 탐색하며, FileVisitOption
으로 탐색 옵션을 설정할 수 있습니다.Stream<Path>
형태로 반환하여 각 경로를 처리할 수 있습니다.Path.resolve(Path other)
Path.relativize(Path other)
Path.normalize()
다음 Thymeleaf 템플릿(src/main/resources/templates/uploadForm.html)은 파일을 업로드하는 방법과 업로드된 내용을 보여주는 예를 보여줍니다.
이 템플릿은 세 부분으로 구성됩니다.
<html xmlns:th="https://www.thymeleaf.org">
<body>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
<div>
<form method="POST" enctype="multipart/form-data" action="/">
<table>
<tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
<tr><td></td><td><input type="submit" value="Upload" /></td></tr>
</table>
</form>
</div>
<div>
<ul>
<li th:each="file : ${files}">
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div>
</body>
</html>
서비스 : 기능
(1) 저장소 초기화 : initialize 할 때 필요한 것? 초기값, 기존에 저장된 파일 목록
(2) 파일 열기 : 파일을 검색하고 선택하고 가져오기 위해 필요한 단서가 무엇일까? 경로, 이름, 유형
(3) 파일 저장하기
(4) 저장소의 파일 지우기
기능 별 요소
(1-1) 초기화 : 기존에 저장된 파일 목록
List<?>
(1-2) 초기화 : 리다이렉션 - 에러, 실패
(1-3) 초기화 : 올린 파일 찾기
Resource
형태로 추상화ResponseEntity
(1-4) 초기화 : 리디렉션 : 업로드, 성공
Model
' 객체에 flash-scoped 메시지 관련 속성 더하기ResponseEntity
사용ResponseEntity
(2-1) 파일 열기 : 초기값 경로(루트) 가져오기
Path
Path
의 메서드를 활용하는 것이 편해보임.String
String
으로 표현된 디렉터리를 Path
로 변환(2-2) 파일 열기 : 파일 이름으로 가져오기
Path
String
Path.resolve
(2-3) 파일 열기 : 저장된 파일 전체 가져오기
Path
인 Buffer 또는 StreamStream<Path>
형태로 반환하여 각 경로를 처리하는 Files.walk
, 현재 경로 기준 다른 경로와의 상대 경로 계산하는 Path.relativize
(2-4) 파일 열기 : 파일을 리소스 형태로 변환하기
Resource
Path
를 URI로 변환 후 이것을 다시 URL로 만들기(3-1) 파일 저장하기
void
MultipartFile
Spring Boot MVC 애플리케이션을 시작하려면 먼저 스타터가 필요합니다. 이 샘플에서는 spring-boot-starter-thymeleaf
및 spring-boot-starter-web
이 이미 종속성으로 추가되었습니다. 서블릿 컨테이너로 파일을 업로드하려면 MultipartConfigElement
클래스(web.xml
의 <multipart-config>
)를 등록해야 합니다. Spring Boot 덕분에 모든 것이 자동으로 구성됩니다!
이 애플리케이션을 시작하는 데 필요한 것은 다음 UploadingFilesApplication
클래스(src/main/java/guides/uploadingfiles/UploadingFilesApplication.java)입니다.
package guides.uploadingfiles;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UploadingFilesApplication {
public static void main(String[] args) {
SpringApplication.run(UploadingFilesApplication.class, args);
}
}
Spring MVC 자동 구성(auto-configure)의 일부로 Spring Boot는 MultipartConfigElement
빈을 생성하고 파일 업로드를 준비합니다.
Spring Boot는 주로 서블릿 컨테이너를 내장하여 사용되지만, 필요에 따라 리액티브 컨테이너를 사용할 수도 있고, 외부의 웹 서버나 컨테이너 오케스트레이션 플랫폼과 통합하여 애플리케이션을 실행할 수 있습니다. Spring Boot는 이러한 다양한 환경에서 유연하게 실행될 수 있도록 다양한 설정 옵션을 제공합니다.
리액티브 웹 컨테이너:
웹 서버:
컨테이너 오케스트레이션 플랫폼:
애플리케이션 서버:
FileUploadController
Controller초기 애플리케이션에는 업로드된 파일을 디스크에 저장하고 로드하는 작업을 처리하는 몇 가지 클래스가 이미 포함되어 있습니다. 모두 guides.uploadingfiles.storage
패키지에 있습니다. 이를 새 FileUploadController
에서 사용하게 됩니다. 다음 목록(src/main/java/guides/uploadingfiles/FileUploadController.java)은 파일 업로드 컨트롤러를 보여줍니다.
package guides.uploadingfiles;
import guides.uploadingfiles.storage.StorageFileNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.util.stream.Collectors;
import guides.uploadingfiles.storage.StorageFileNotFoundException;
import guides.uploadingfiles.storage.StorageService;
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/")
public String listUploadFiles(Model model) throws IOException{
model.addAttribute(
"files",
storageService.loadAll().map(
path -> MvcUriComponentsBuilder.fromMethodName(
FileUploadController.class,
"serveFile",
path.getFileName().toString()
).build().toUri().toString()
).collect(Collectors.toList()));
return "uploadForm";
}
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
if (file==null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"")
.body(file);
}
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute(
"message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
}
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}
}
FileUploadController
클래스는 @Controller
로 주석 처리되어 Spring MVC가 이를 선택하고 경로를 찾을 수 있습니다. 각 메서드에는 @GetMapping
또는 @PostMapping
태그가 지정되어 경로와 HTTP 작업을 특정 컨트롤러 작업에 연결합니다.
이 경우:
GET /
: StorageService
에서 현재 업로드된 파일 목록을 조회하고 이를 Thymeleaf 템플릿에 로드합니다. MvcUriComponentsBuilder
를 사용하여 실제 리소스에 대한 링크를 계산합니다.
GET /files/{filename}
: 리소스(존재하는 경우)를 로드하고 Content-Disposition 응답 헤더를 사용하여 다운로드할 브라우저로 보냅니다.
POST /
: 여러 부분으로 구성된 메시지 파일을 처리하고 저장을 위해 StorageService에 제공합니다.
프로덕션 시나리오에서는 파일을 임시 위치, 데이터베이스 또는 NoSQL 저장소(예: Mongo의 GridFS)에 저장할 가능성이 높습니다. 애플리케이션의 파일 시스템에 콘텐츠를 로드하지 않는 것이 가장 좋습니다.
@Controller
컨트롤러는 웹 애플리케이션에서 주로 사용자 인터페이스와 비즈니스 로직 간의 중간 매개체로 작동하며, 클
라이언트의 요청에 따라 적절한 데이터를 검색하고, 가공하여 적절한 뷰에 전달합니다.
웹 애플리케이션에서 컨트롤러는 다양한 엔드포인트에 매핑되어 각각의 요청을 처리합니다. 엔드포인트는 네트워크 통신에서 특정한 서비스, 기능 또는 리소스에 접근하기 위한 경로나 URL을 나타냅니다.
이러한 요청에는 HTTP 메소드(GET, POST, PUT, DELETE 등)와 함께 오는 데이터가 포함됩니다. 컨트롤러는 이러한 요청을 처리하고, 비즈니스 로직이나 서비스를 호출하여 데이터를 가져오거나 변경한 뒤, 적절한 뷰를 렌더링하여 사용자에게 응답을 제공합니다.
@Autowired
: 생성자 주입 방식을 사용하여 StorageService
의 인스턴스를 주입합니다.
@GetMapping("/")
: 루트 경로(/
)에 대한 GET 요청을 처리합니다. Model
을 인자로 받아 파일 목록을 불러와 uploadForm
템플릿을 렌더링합니다.
Model
은 스프링에서 데이터를 뷰에 전달하는 데 사용되는 클래스입니다. uploadForm
은 뷰를 나타내는 문자열이며, 스프링에서는 이것을 템플릿의 이름으로 사용합니다. 이 이름은 렌더링할 실제 템플릿 파일을 찾기 위해 사용됩니다. 일반적으로 uploadForm
은 프론트엔드에서 사용되는 HTML 템플릿 파일의 이름이 될 수 있습니다.model.addAttribute("files", ...)
: 모델에 files
라는 속성을 추가합니다.storageService.loadAll().map(...).collect(Collectors.toList())
: storageService
에서 파일 목록을 가져오는 메서드를 호출하고, 파일 목록의 각 파일에 대해 (...)
에 해당하는 내용을 처리 합니다. 그리고 처리한 파일을 리스트로 수집합니다.MvcUriComponentsBuilder.fromMethodName
은 컨트롤러 클래스와 메서드 이름을 기반으로 URI를 생성하는 데 사용됩니다. 위 코드에서는 FileUploadController
클래스의 serveFile
메서드에 대한 URI를 생성하고 있어요. serveFile
메서드는 /files/{filename:.+}
와 연결되어 있고, path.getFileName().toString()
은 파일 이름을 의미합니다. 따라서 MvcUriComponentsBuilder.fromMethodName
은 파일을 다운로드하기 위한 URI를 생성하는 데 사용됩니다.path.getFileName()
은 Path
의 파일 이름을 나타내는 Path
객체를 반환합니다. 이 객체는 toString()
메서드를 통해 문자열로 변환될 수 있습니다build()
메서드 호출로 UriComponents
객체가 만들어집니다. 이 객체는 URI를 나타내는데, .toUri()
를 호출하면 실제 java.net.URI
객체로 변환됩니다. 그리고 이렇게 만들어진 java.net.URI
객체를 문자열로 변환하기 위해 .toString()
을 사용할 수 있습니다. UriComponents
는 스프링 프레임워크의 URI 관련 유틸리티 클래스입니다. 이 클래스는 URI의 각 부분을 개별적으로 가져오고 조작할 수 있는 메서드들을 제공합니다. 반면 java.net.URI
클래스는 Java 표준 라이브러리에서 제공하는 URI를 다루기 위한 클래스입니다. 두 클래스는 비슷한 목적으로 URI를 다루지만, UriComponents
는 스프링에서 제공하는 확장된 기능과 유틸리티를 제공하는 반면, java.net.URI
는 Java 표준 기능만을 포함하고 있습니다.UriComponents
의 toString()
메서드를 호출하면 URI의 문자열 표현을 얻을 수 있으므로, toUri()
메서드를 따로 사용하지 않아도 됩니다4.@GetMapping("/files/{filename:.+}")
: /files/{filename}
{filename}
: 경로 변수. 여기에는 실제 파일 이름이 올 것입니다.:.+
: .
은 임의의 문자 하나를 의미하고, +
는 하나 이상의 문자가 있는지를 나타냅니다. 따라서 :.+
는 파일 이름에 마침표를 포함한 어떤 문자열이든 허용한다는 의미입니다. /files/
다음에 오는 경로 변수(filename
)에는 확장자를 포함한 모든 파일 이름이 올 수 있도록 정의되었습니다.5.@ResponseBody
: HTTP 응답 본문으로 직접 데이터를 작성하는 데 사용되는 어노테이션입니다.
6.@PathVariable
: Spring MVC에서 URL 경로의 일부를 매개변수로 캡처하기 위해 사용되는 어노테이션입니다. @PathVariable
은 URL 경로에서 특정한 부분을 추출하여 메서드의 매개변수로 전달합니다. 이 경우, "/files/{filename:.+}"
에서 {filename}
에 해당하는 값을 String filename
매개변수로 받아옵니다.
ResponseEntity
: HTTP 응답을 나타내는 Spring 프레임워크의 클래스입니다. ResponseEntity
는 HTTP 응답 코드, 헤더, 본문 등을 포함할 수 있는 강력한 반환 유형을 제공합니다. 이 클래스를 사용하면 응답을 특정 상태 코드와 함께 보낼 수 있고, 헤더 및 본문을 추가적으로 조작할 수 있습니다.
HttpHeaders
: HTTP 헤더를 나타내는 Spring 프레임워크의 클래스입니다. HttpHeaders
를 사용하여 HTTP 응답 또는 요청의 헤더를 설정할 수 있습니다. 위 코드에서는 HttpHeaders.CONTENT_DISPOSITION
을 사용하여 응답의 Content-Disposition 헤더를 설정하여 다운로드 파일의 이름을 지정하고 있습니다.
7.@PostMapping("/")
: 루트 경로(/
)에 대한 POST 요청을 처리합니다. 업로드된 파일을 저장하고, redirectAttributes
를 사용하여 메시지를 추가한 뒤 루트 경로로 리다이렉션합니다.
8.@ExceptionHandler(StorageFileNotFoundException.class)
: StorageFileNotFoundException
예외를 처리하는 메소드로, 해당 예외가 발생하면 404 상태 코드를 반환합니다.
FileSystemStorageService
Service컨트롤러가 스토리지 계층(예: 파일 시스템)과 상호 작용할 수 있도록 StorageService
를 제공해야 합니다. 다음 목록(src/main/java/guides/uploadingfiles/storage/StorageService.java)은 해당 인터페이스를 보여줍니다.
package guides.uploadingfiles.storage;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}
또한 StorageService
를 지원하려면 네 가지 클래스가 필요합니다.
package guides.uploadingfiles.storage;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage")
public class StorageProperties {
/**
* Folder location for storing files
*/
private String location = "upload-dir";
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
package guides.uploadingfiles.storage;
public class StorageException extends RuntimeException {
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
package guides.uploadingfiles.storage;
public class StorageFileNotFoundException extends StorageException {
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
package guides.uploadingfiles.storage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
@Service
public class FileSystemStorageService implements StorageService {
private final Path rootLocation;
@Autowired
public FileSystemStorageService(StorageProperties properties) {
if (properties.getLocation().trim().length() == 0) {
throw new StorageException("File upload location can not be Empty");
}
this.rootLocation = Paths.get(properties.getLocation());
}
@Override
public void store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new StorageException("Failed to storage empty file.");
}
Path destinationFile = this.rootLocation.resolve(
Paths.get(file.getOriginalFilename())
).normalize().toAbsolutePath();
if (!destinationFile.getParent().equals(
this.rootLocation.toAbsolutePath()
)) {
throw new StorageException("Cannot store file outside current directory.");
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(
inputStream,
destinationFile,
StandardCopyOption.REPLACE_EXISTING
);
}
} catch (IOException e) {
throw new StorageException("Failed to store file.", e);
}
}
@Override
public Stream<Path> loadAll () {
try {
return Files.walk(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
.map(this.rootLocation::relativize);
} catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load (String filename) {
return rootLocation.resolve(filename);
}
@Override
public Resource loadAsResource (String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new StorageFileNotFoundException("Could not read file: " + filename);
}
} catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
@Override
public void deleteAll () {
FileSystemUtils.deleteRecursively(rootLocation.toFile());
}
@Override
public void init () {
try {
Files.createDirectories(rootLocation);
} catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
}
public void store(MultipartFile file) {...}
this.rootLocation.resolve(...).normalize().toAbsolutePath
: rootLocation에 새로운 경로(...)
를 연결하여 표준 경로로 정규화 한 후 절대 경로로 변환 합니다.
rootLocation
은 파일이 저장될 기본 디렉터리입니다. 이 저장소는 파일 시스템의 특정 위치를 가리키며, 데이터베이스가 아닌 파일 시스템에 파일을 저장합니다. 사용자가 지정한 위치에 파일을 저장하거나 불러올 수 있게끔 구성되어 있습니다./folder1//folder2///file.txt
와 같이 중복된 슬래시가 있는 경우, 정규화를 하면 /folder1/folder2/file.txt
와 같이 중복된 슬래시를 제거/myFolder/../myFile.txt
와 같은 경로는 상위 디렉토리로 이동하는 ..
을 포함하고 있습니다. normalize를 하게 되면 이런 상위 경로 이동 구문을 해석하여 경로를 더 간결하게 만들어줍니다. 따라서 이 경우 /myFile.txt
로 경로가 정규화toAbsolutePath()
는 좀 더 철저한 절대 경로destinationFile.getParent()
: getParent()
는 현재 경로의 부모 경로를 나타내는 Path
객체를 반환합니다. 이 메서드는 파일 시스템에 직접 접근하지 않으며, 경로 또는 해당 부모가 존재하지 않을 수 있습니다. 또한, 이 메서드는 구현에 따라 ".
", "..
"과 같은 특수 이름을 제거하지 않습니다. 예를 들어 UNIX 시스템에서 /a/b/c
의 부모는 /a/b
이며, x/y/.
의 부모는 x/y
입니다.
try (InputStream inputStream = file.getInputStream()) {...}
: Java 7에서 추가된 기능 중 하나인 'try-with-resources'입니다. 이를 사용하면 try
블록 내에서 자동으로 리소스를 닫을 수 있습니다.
AutoCloseable
인터페이스를 구현하는 리소스를 사용할 때 특히 유용합니다. 이것을 사용하면 코드 블록이 끝나는 시점에 리소스를 자동으로 닫아 메모리 누수를 방지할 수 있습니다. Java에서 파일, 소켓 및 데이터베이스 연결과 같은 외부 리소스를 다룰 때 유용하게 활용됩니다.inputStream
은 getInputStream()
으로 얻은 값을 파일에 복사할 때 사용됩니다public Stream<Path> loadAll () {...}
Files.walk(this.rootLocation, 1)
: 지정된 경로(this.rootLocation
)에서 하위 파일과 디렉토리를 포함하여 최대 1단계까지 탐색합니다..filter(path -> !path.equals(this.rootLocation))
: 시작 경로(this.rootLocation
)를 제외한 모든 경로를 필터링합니다..map(this.rootLocation::relativize)
: 시작 경로(this.rootLocation
)로부터 상대 경로를 가져옵니다./root/folder1
이고 경로 B가 /root/folder2
인 경우, 이 두 경로는 같은 루트인 /root
를 가지고 있으며, 서로 다른 하위 디렉토리인 folder1
과 folder2
를 부모로 가지고 있습니다. 따라서 이런 상황에서 relativize
메서드를 사용하여 상대 경로를 만들 수 있습니다.catch (IOException e) { throw new StorageException("Failed to read stored files", e); }
: 입출력 예외가 발생하면 StorageException
을 던집니다.deleteRecursively(rootLocation.toFile())
deleteRecursively()
는 스프링 프레임워크의 FileSystemUtils
클래스에서 제공되는 메서드이며, 지정된 경로를 재귀적으로 삭제하는데 사용됩니다. 지정된 경로가 디렉터리인 경우, 해당 디렉터리와 그 내용물을 모두 삭제합니다.deleteRecursively(rootLocation.toFile())
는 rootLocation
에 지정된 디렉터리를 모두 삭제합니다.rootLocation
자체는 삭제하지 않습니다. Files.createDirectories(rootLocation)
createDirectories()
메서드는 파일 시스템에 디렉터리를 생성할 때 사용됩니다. 해당 경로에 디렉터리가 없으면 디렉터리를 생성하고, 디렉터리가 이미 존재하는 경우에는 아무런 동작을 하지 않습니다.Files.createDirectories(rootLocation)
은 rootLocation
에 해당하는 디렉터리가 존재하지 않으면 그 디렉터리를 생성합니다. public Resource loadAsResource (String filename) {...}
Path file = load(filename)
: 같은 클래스 내부의 load
메서드를 사용한 것이며 rootLocation.resolve(filename)
과 같다. Resource resource = new UrlResource(file.toUri())
: UrlResource
는 Resource
인터페이스를 구현한 추상 클래스 AbstractResource
를 상속한 AbstractFileResolvingResource
를 상속한 클래스이다. - 업캐스팅을 위한 (Resource)
가 생략되었다. public Path load (String filename) { return rootLocation.resolve(filename);}
load
의 반환 값을 보았을 때, 컨트롤러에서 model에 files
라는 속성명으로 storageService.loadAll()
로 가져온 Stream<Path>
에 map
으로 처리하는 일련의 가공을 단순화 할 수도 있을 것 같다. model.addAttribute( "files", storageService.loadAll().map(path -> path.toUri()).collect(Collectors.toList()));
path -> path.toUri()
를 메서드 참조 Path::toUri
로 교체 가능하다. Path
객체를 가져와 toUri()
메서드를 호출하여 해당 경로를 URI로 변환합니다. 이 두 가지 방법은 기능적으로 동등하다.파일 업로드를 구성할 때 파일 크기에 대한 제한을 설정하는 것이 유용한 경우가 많습니다. 5GB 파일 업로드를 처리한다고 상상해보세요! Spring Boot를 사용하면 일부 속성 설정으로 자동 구성된 MultipartConfigElement
를 조정할 수 있습니다.
기존 속성 설정(src/main/resources/application.properties)에 다음 속성을 추가합니다.
spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB
멀티파트 설정은 다음과 같이 제한됩니다.
spring.servlet.multipart.max-file-size
는 128KB로 설정됩니다. 이는 총 파일 크기가 128KB를 초과할 수 없음을 의미합니다.spring.servlet.multipart.max-request-size
는 128KB로 설정됩니다. 이는 multipart/form-data
에 대한 총 요청 크기가 128KB를 초과할 수 없음을 의미합니다.<form>
태그의 enctype
속성은 폼 데이터(form data)가 서버로 제출될 때 해당 데이터가 인코딩되는 방법을 명시합니다.multipart/form-data
이 속성값은 모든 문자를 인코딩하지 않음을 명시하며 <form>
요소가 파일이나 이미지를 서버로 전송할 때 주로 사용합니다.파일을 업로드할 대상 폴더가 필요하므로 Spring Initializr가 생성한 기본 UploadingFilesApplication
클래스를 강화하고 Boot CommandLineRunner
를 추가하여 시작 시 해당 폴더를 삭제하고 다시 생성해야 합니다. 다음 목록(src/main/java/guides/uploadingfiles/UploadingFilesApplication.java)은 이를 수행하는 방법을 보여줍니다.
package guides.uploadingfiles;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import guides.uploadingfiles.storage.StorageProperties;
import guides.uploadingfiles.storage.StorageService;
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class UploadingFilesApplication {
public static void main(String[] args) {
SpringApplication.run(UploadingFilesApplication.class, args);
}
@Bean
CommandLineRunner init(StorageService storageService) {
return (args) -> {
storageService.deleteAll();
storageService.init();
};
}
}
@ConfigurationProperties
: 외부화된 구성에 대한 애너테이션입니다. 일부 외부 속성(예: .properties
파일에서)을 바인딩하고 유효성을 검사하려면 @Configuration
클래스의 @Bean
메서드 또는 클래스 정의에 이를 추가하세요.
바인딩은 애너테이션이 달린 클래스에서 setter
를 호출하거나 @ConstructorBinding
을 사용하는 경우 생성자 매개 변수에 바인딩하여 수행됩니다.
@Value
와는 반대로 SpEL 표현식은 속성 값이 외부화되므로 평가되지 않습니다.
@ConfigurationProperties("storage")
는 속성 파일(application.properties
)이나 환경 변수에서 storage.*
와 일치하는 속성을 찾습니다. 이 코드에서는 StorageProperties
클래스의 필드와 매핑하려고 시도합니다. 만약 storage.*
와 매치되는 속성이 없다면 그 필드들은 디폴트 값(null 또는 primitive type의 default 값)으로 초기화됩니다.
@EnableConfigurationProperties
는 @ConfigurationProperties
로 설정된 클래스들을 스프링에서 사용할 수 있도록 빈으로 등록합니다. 설정 클래스에 정의된 필드들과 매칭하여, 애플리케이션의 설정 파일에 있는 값을 읽어올 수 있게 됩니다.
@Bean CommandLineRunner init(StorageService storageService)
: init
메서드를 통해 StorageService
를 주입받아 빈을 생성하고 있음. @Bean CommandLineRunner run(@Autowired StorageService storageService)
는 생성자 주입 annotation인 @Autowired
를 통해 StorageService
를 주입받는 run
메서드를 통해 빈을 생성하므로 두 코드가 기능적으로 같음.
1.@SpringBootTest
: Spring Boot 기반 테스트를 실행하는 테스트 클래스에 지정할 수 있는 주석입니다. 일반 Spring TestContext Framework 외에 다음 기능을 제공합니다.
@ContextConfiguration(loader=...)
이 정의되지 않은 경우 SpringBootContextLoader
를 기본 ContextLoader
로 사용합니다.@Configuration
이 사용되지 않고 명시적 클래스가 지정되지 않은 경우 @SpringBootConfiguration
을 자동으로 검색합니다.properties
속성을 사용하여 사용자 정의 Environment
속성을 정의할 수 있습니다.args
속성을 사용하여 애플리케이션 인수를 정의할 수 있습니다.webEnvironment
모드에 대한 지원을 제공합니다.TestRestTemplate
및/또는 WebTestClient
Bean을 등록합니다.2.Mockito 와 BDDMockito
3.@MockBean
Spring ApplicationContext에 Mock을 추가하는 데 사용할 수 있는 어노테이션입니다.
@Configuration
클래스, SpringRunner
로 실행되는(@RunWith
) 테스트 클래스의 필드에 사용할 수 있습니다.Mock의 등록 방식
(1) 일반적인 사용 예시
@RunWith(SpringRunner.class)
public class ExampleTests {
@MockBean
private ExampleService service;
@Autowired
private UserOfService userOfService;
@Test
public void testUserOfService() {
given(this.service.greet()).willReturn("Hello");
String actual = this.userOfService.makeUse();
assertEquals("Was: Hello", actual);
}
@Configuration
@Import(UserOfService.class) // ExampleService로 주입되는 @Component
static class Config {
}
}
테스트의 목적은 UserOfService
를 테스트하고, 이 때 ExampleService
를 Mock으로 대체하여 특정 상황을 시뮬레이션하는 것으로 보입니다.
@RunWith(SpringRunner.class)
: 이 어노테이션은 테스트를 실행할 때 스프링의 테스트 컨텍스트와 함께 JUnit을 실행하도록 지시합니다. SpringRunner
는 JUnit에 통합된 스프링 테스트 지원을 활용하여 테스트를 실행합니다.
@MockBean
: 이 어노테이션은 테스트에서 사용할 Mock 빈을 선언합니다. 여기서는 ExampleService
를 Mock으로 선언하고 있습니다. MockBean
어노테이션을 사용하면 해당 빈이 스프링 애플리케이션 컨텍스트에 Mock으로 등록되고, 필요한 경우 실제 빈 대신 Mock이 사용됩니다.
@Autowired
: 이 어노테이션은 필드 또는 생성자에 사용되어 해당 타입의 빈을 주입받습니다. 여기서는 UserOfService
를 주입받고 있습니다. 스프링은 @Autowired
를 통해 필요한 빈을 찾아서 주입해줍니다.
@Configuration
: 이 어노테이션은 스프링의 자바 기반 설정 클래스를 나타냅니다. 여기서는 Config
클래스가 @Configuration
어노테이션을 가지고 있어서 스프링에게 이 클래스가 애플리케이션의 설정을 포함하고 있다고 알려줍니다.
@Import(UserOfService.class)
: 이 어노테이션은 다른 설정 클래스나 컴포넌트를 현재의 설정 클래스에 추가로 포함하도록 합니다. 여기서는 UserOfService
클래스를 추가로 포함시켜서 해당 클래스에 정의된 빈들을 사용할 수 있도록 합니다.
(2) 동일한 타입의 빈을 여러개 생성한 경우 : 필드 레벨에서 한정자(@Qulaifier
) 메타데이터를 지정해야 합니다.
여러 데이터베이스 연결을 위해 동일한 타비의 빈을 생성한 상황
@Component
@Qualifier("mysql")
public class MySQLDataSource implements DataSource {
// MySQL 데이터베이스 연결에 필요한 설정
}
@Component
@Qualifier("postgresql")
public class PostgreSQLDataSource implements DataSource {
// PostgreSQL 데이터베이스 연결에 필요한 설정
}
위의 코드에서 MySQLDataSource
와 PostgreSQLDataSource
는 모두 DataSource
인터페이스를 구현하고 있습니다. 그러나 이 두 개의 빈은 각각 MySQL
과 PostgreSQL
과 같은 다른 데이터베이스와 연관되어 있습니다.
이제 두 개의 빈을 사용하여 다른 클래스에서 주입할 때 @Qualifier
를 사용할 수 있습니다.
@Service
public class DataService {
private final DataSource mysqlDataSource;
private final DataSource postgresqlDataSource;
@Autowired
public DataService(@Qualifier("mysql") DataSource mysqlDataSource,
@Qualifier("postgresql") DataSource postgresqlDataSource) {
this.mysqlDataSource = mysqlDataSource;
this.postgresqlDataSource = postgresqlDataSource;
}
// ...
}
우리 애플리케이션에서 이 특정 기능을 테스트하는 방법에는 여러 가지가 있습니다. 다음 목록(src/test/java/guides/uploadingfiles/FileUploadTests.java)은 서블릿 컨테이너를 시작할 필요가 없도록 MockMvc
를 사용하는 한 가지 예를 보여줍니다.
이러한 테스트에서는 다양한 모의 객체를 사용하여 컨트롤러 및 StorageService
와의 상호 작용을 설정하지만 MockMultipartFile
을 사용하여 서블릿 컨테이너 자체와의 상호 작용도 설정합니다.
package guides.uploadingfiles;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import guides.uploadingfiles.storage.StorageFileNotFoundException;
import guides.uploadingfiles.storage.StorageService;
@AutoConfigureMockMvc
@SpringBootTest
public class FileUploadTests {
@Autowired
private MockMvc mvc;
@MockBean
private StorageService storageService;
@Test
public void shouldListAllFiles() throws Exception {
given(this.storageService.loadAll())
.willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt")));
this.mvc.perform(get("/")).andExpect(status().isOk())
.andExpect(model().attribute("files",
Matchers.contains("http://localhost/files/first.txt",
"http://localhost/files/second.txt")));
}
@Test
public void shouldSaveUploadedFile() throws Exception {
MockMultipartFile multipartFile = new MockMultipartFile("file", "test.txt",
"text/plain", "Spring Framework".getBytes());
this.mvc.perform(multipart("/").file(multipartFile))
.andExpect(status().isFound())
.andExpect(header().string("Location", "/"));
then(this.storageService).should().store(multipartFile);
}
@SuppressWarnings("unchecked")
@Test
public void should404WhenMissingFile() throws Exception {
given(this.storageService.loadAsResource("test.txt"))
.willThrow(StorageFileNotFoundException.class);
this.mvc.perform(get("/files/test.txt")).andExpect(status().isNotFound());
}
}
@AutoConfigureMockMvc
와 @SpringBootTest
: 이 테스트 클래스는 Spring의 MockMvc를 사용하여 웹 애플리케이션을 테스트하며, @SpringBootTest
를 통해 Spring Boot 애플리케이션의 전체적인 컨텍스트를 로드합니다.
@Autowired private MockMvc mvc
: MockMvc
를 주입받아 사용할 수 있도록 합니다.
@MockBean private StorageService storageService
: StorageService
의 Mock 객체를 주입받습니다. 이는 실제 파일 저장소를 대체하여 테스트에서 사용됩니다.
shouldListAllFiles()
: 특정 경로에서 파일 목록을 불러오는지 테스트하는 메서드입니다. given
을 통해 storageService.loadAll()
이 호출되면 몇 가지 파일을 반환하도록 설정하고, 이를 테스트합니다.
shouldSaveUploadedFile()
: 업로드된 파일을 저장하는지 확인하는 메서드입니다. MockMultipartFile
을 사용하여 가짜 파일을 만들고, 해당 파일을 업로드하고 저장되었는지 확인합니다.
should404WhenMissingFile()
: 파일이 없을 때 404 응답을 반환하는지 테스트합니다. 특정 파일을 요청했을 때 StorageFileNotFoundException
을 던지도록 Mock 객체를 설정하고, 이에 대한 404 응답을 테스트합니다.
통합 테스트의 예는 FileUploadIntegrationTests
클래스(src/test/java/guides/uploadingfiles에 있음)를 참조하세요.
package guides.uploadingfiles;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.ArgumentMatchers.any;
import guides.uploadingfiles.storage.StorageService;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FileUploadIntegrationTests {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private StorageService storageService;
@LocalServerPort
private int port;
@Test
public void shouldUploadFile() throws Exception {
ClassPathResource resource = new ClassPathResource("testupload.txt", getClass());
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("file", resource);
ResponseEntity<String> response = this.restTemplate.postForEntity("/", map,
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(response.getHeaders().getLocation().toString())
.startsWith("http://localhost:" + this.port + "/");
then(storageService).should().store(any(MultipartFile.class));
}
@Test
public void shouldDownloadFile() throws Exception {
ClassPathResource resource = new ClassPathResource("testupload.txt", getClass());
given(this.storageService.loadAsResource("testupload.txt")).willReturn(resource);
ResponseEntity<String> response = this.restTemplate
.getForEntity("/files/{filename}", String.class, "testupload.txt");
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
.isEqualTo("attachment; filename=\"testupload.txt\"");
assertThat(response.getBody()).isEqualTo("Spring Framework");
}
}
파일 업로드를 수신하는 서버 측 부분을 실행합니다. 로깅 출력이 표시됩니다. 서비스는 몇 초 내에 시작되어 실행되어야 합니다.
서버가 실행 중인 상태에서 브라우저를 열고 http://localhost:8080/
을 방문하여 업로드 양식을 확인해야 합니다. (작은) 파일을 선택하고 업로드를 누르세요. 컨트롤러에서 성공 페이지가 표시되어야 합니다. 너무 큰 파일을 선택하면 보기 흉한 오류 페이지가 표시됩니다.
그러면 브라우저 창에 다음과 유사한 줄이 표시됩니다.
“<파일 이름>을(를) 성공적으로 업로드했습니다!”