티스토리 뷰

728x90
반응형

유저는 브라우저 상에서 렌더링 된 HTML을 통해 파일 업로드를 요청하고, 업로드된 데이터는 HTTP의 Multi-Part로 백엔드 서버로 데이터가 전송됩니다. HTTP의 Multi-Part 요청을 처리하는 애플리케이션이 Springboot로 구현되어 있다면, HTTP의 MultiPart로 전송받은 데이터를 처리하는 로직을 구현해야 합니다. 이번 포스팅에서는 SpringBoot에서 Multi-Part로 전송된 데이터를 처리하는 내용에 대해서 알아보겠습니다.

Springboot Web 용 프로젝트 다운로드

https://start.spring.io/ 에서 Dependencies항목에서 Spring Web과 Thymeleaf(타임리프)를 선택합니다. 나머지 프로젝트 설정은 로컬 PC의 개발환경에 맞추어 설정하시면 됩니다.

Spring Web 프로젝트 다운로드

IntelliJ로 다운로드한 프로젝트를 열어 build.gradle파일을 확인하면, dependecie에 타임리프와 스프링 웹 디펜던시가 추가된 것을 확인할 수 있습니다.

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'
}

만약 프로젝트 다운로드 및 소스코드 작성 과정을 건너뛰고 싶으시면, 아래 URL(https://github.com/spring-guides/gs-uploading-files)에서 프로젝트를 다운로드하실 수 있습니다.

파일 업로드 테스트용 HTML 화면 구현

SpringBoot에서는 템플릿 엔진을 타임리프(Thymeleaf) 사용을 권장하고 있습니다. 템플릿 엔진에 대해서 잠깐 설명드리면, HTML과 같은 정적 문서에서 동적으로 변하는 변수를 이용해 HTML 문서에 추가하여, 서버 측에서 HTML 파일을 동적으로 변환하여 렌더링 해주는 템플릿 엔진입니다. 즉, HTML 태그 안에 ${변수명}가 있으면, 템플릿 엔진인 타임리프가 ${변수명}과 Springboot에서 정의한 객체와 매핑하여 HTML 문서를 동적으로 생성한다고 이해하시면 됩니다. 타임리프를 활용하여 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>


HTML 태그 안에 th:으로 시작하는 문법이 템플릿 엔진인 타임리프 문법입니다. 작성된 HTML은 업로드할 파일을 입력받는 버튼과 이를 업로드하는 전송하는 버튼을 나타냅니다. SpringBoot 코드 작성 없이, 작성된 HTML 문서만 브라우저로 출력하면, 아래와 같은 화면이 출력됩니다. HTML의 태그 중 th:if="{message}는 정의된 내용이 없으므로, 출력되지 않은 것을 확인할 수 있습니다. 즉, HTML에서 타임리프 코드는 무시된 것입니다.

파일 업로드 처리를 위한 Springboot 코드 작성하기

브라우저로부터 업로드할 파일을 요청받으면, 백엔드 서버에서는 HTTP Method 처리 함수, 데이터 로드, 데이터 저장, 파일 입출력 예외처리 로직을 구현해야 합니다. HTTP Request 요청을 처리할 Controller를 작성하고, 그 이후에 파일 저장 및 서버에 저장된 데이터를 로드하는 로직을 Service로 구현해보겠습니다. 먼저 FileUploadController를 com/example/demo/FileUploadController.java에 작성합니다.

package com.example.demo;

import java.io.IOException;
import java.util.stream.Collectors;

import com.example.demo.storage.StorageFileNotFoundException;
import com.example.demo.storage.StorageService;
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.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;


@Controller
public class FileUploadController {

    private final StorageService storageService;

    @Autowired
    public FileUploadController(StorageService storageService) {
        this.storageService = storageService;
    }

    @GetMapping("/")
    public String listUploadedFiles(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);
        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();
    }
}

@GetMapping("/") 어노테이션이 달려있는 listUploadedFiles() 메서드는 앞서 작성한 uploadForm.html을 반환하는 코드입니다. 이때, files 속성에 대한 값을 추가하여 HTML에 작성된 files변수의 값을 지정해줍니다. @GetMapping("/files/{filename:+}")가 작성된 serveFiles() 메서드는 서버의 업로드 경로에 저장되어 있는 파일목록을 반환해주는 코드입니다. listUploadedFiles()에서 호출하고 있습니다. @PostMapping("/")은 파일을 업로드 요청을 처리하는 메서드입니다.

위 컨트롤러 클래스 이외에 추가로 작성해야 할 소스코드가 많으므로, 아래 git repostory에서 소스코드를 다운로드 또는 참조 부탁드립니다. 

https://github.com/spring-guides/gs-uploading-files.git

위 리포지토리의 complete/src/main/java/com/example/uploadingfiles 경로에 진입하면, 스토리지 관련 클래스들이 있습니다. 파일 업로드용 클래스에 대한 설명은 아래와 참조 바랍니다.

  • FileUploadController : 브라우저로부터 업로드 화면용 HTML 응답, 업로드 처리를 수행하는 Controller
  • FileSystemStorageService : 업로드 디렉터리 자원 조회, 파일 저장, 삭제를 수행하는 Service
  • StorageException : Storage 오퍼레이션 수행 시 발생하는 Exception을 처리하는 클래스
  • StorageExceptionNotFoundException : 업로드 경로를 찾을 수 없을 때 발생하는 예외를 처리하는 클래스
  • StorageProperties : 파일이 업로드될 경로
  • StorageService : 업로드 디렉터리 파일 입출력 연산을 나타내는 Interface

StorageProperties 클래스 위에 명시된 @ConfigurationProperties는 프로퍼티 설정 값을 Java 코드로 작성한 것을 말합니다. 즉, storage.location = "upload-dir"로 작성한 것과 동일한 의미입니다. FileUploadController 클래스에 StorageService 객체로 파일 IO를 수행하고 있는데 StorageService에는 FileSystemStorageService가 주입되어 동작합니다.

반응형

파일 업로드 기능 테스트하기

Main클래스 상단에 @EnableConfigurationProperties를 추가하고, SpringBoot 애플리케이션을 실행해줍니다.

package com.example.demo;

import com.example.demo.storage.StorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class DemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
   }

}

Springboot가 정상적으로 실행된 후, localhost로 접속합니다. port가 이미 사용 중일 경우, port를 아래와 같이 변경해줍니다. 그리고 파일 업로드 제약이 있는데, 이를 변경하고 싶으면 아래와 같이 속성 값을 변경하여 업로드 파일 크기를 변경할 수 있습니다.

server.port = 7080
spring.servlet.multipart.max-file-size=512MB
spring.servlet.multipart.max-request-size=512MB

다음으로 업로드 파일을 저장할 디렉터리를 생성해줍니다. upload-dir은 앞서 StorageProperties에 지정한 경로입니다.

SpringBoot가 정상적으로 실행되고 나면, localhost:7080에 접속합니다. 파일 선택 버튼을 클릭하여, 업로드할 파일을 지정해봅니다. 업로드를 성공하면, 브라우저에서 업로드가 성공하였다는 문구가 출력되고, 업로드 완료된 파일의 최종경로도 브라우저에 출력됩니다.

Multipart를 이용한 파일 업로드 성공 화면

실제 업로드된 파일이 저장되었는지 확인하기 위해선, 이전에 지정한 디렉터리에 업로드 파일이 저장되었는지 확인해봅니다. 업로드 디렉터리에 파일이 추가되어 있다면, 업로드 기능 구현은 완료된 것입니다.

이상으로, SpringBoot에서 Multi-Part를 이용한 업로드 기능 구현에 대해 알아보았습니다.

728x90
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함