파일을 업로드하려면 바이너리 데이터를 전송해야 하는데 폼을 전송할 때는 파일만 전송하는 것이 아니라 문자와 바이너리 데이터를 동시에 전송하는 경우가 많다.

 

HTTP는 multipart/form-data 전송 방식으로 다른 종류의 여러 파일와 폼의 내용을 함께 전송할 수 있는데 서블릿 컨테이너는 멀티파트 데이터를 request 객체에 각각 나누어 담아 전송하고 request.getParts()로 받을 수 있다.

 

업로드 사이즈는 다음과 같이 제한할 수 있다.

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

// 옵션을 끄면 서블릿 컨테이너는 멀티파트 처리를 하지 않고 request.getParts()는 비어서 전송된다.
// Default true
spring.servlet.multipart.enabled=false

스프링의 DispatcherServlet에서 MultipartResolver가 실행이 되고 서블릿 컨테이너가 전달하는 HttpSerlvetRequest를 자식 인터페이스인 MultipartHttpServletRequest로 변환해서 반환해서 멀티파트와 관련된 추가 기능을 할 수 있게 한다.

 

서블릿이 제공하는 Parts는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공하지만 HttpServletRequest를 사용해야 하고 스프링이 제공하는 MultipartFile 인터페이스가 훨씬 편리하기 때문에 잘 사용하지 않는다.

 


 

스프링 MultipartFile

 

@RequestParam MultipartFile file, @ModelAttribute MultipartFile file 처럼 컨트롤러 파라미터에 입력만 해주면 사용할 수 있다.

  • getOriginalFilename() : 업로드 파일 명
  • transferTo() : 파일 저장

 

1. 업로드 파일 정보를 보관할 클래스

 

@Data
public class UploadFile {
    // 클라이언트에서 업로드한 파일명
    private String uploadFileName;
    // 서버에서 관리할 파일명 (중복되지 않을 이름으로 보관해야한다.)
    private String storeFileName;
    
    public UploadFile(String uploadFileName, String storeFileName) {
        this.uploadFileName = uploadFileName;
        this.storeFileName = storeFileName;
    }
}

 

2. 파일 저장 등을 처리하는 클래스

 

@Component
public class FileStore {
    @Value("${file.dir}")
    private String fileDir;
    
    public String getFullPath(String filename) {
        return fileDir + filename;
    }
    
    public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException {
        List<UploadFile> storeFileResult = new ArrayList<>();
        
        for (MultipartFile multipartFile : multipartFiles) {
            if (!multipartFile.isEmpty()) {
                storeFileResult.add(storeFile(multipartFile));
            }
        }
        return storeFileResult;
     }
    
    public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
         if (multipartFile.isEmpty()) {
             return null;
         }
         
         String originalFilename = multipartFile.getOriginalFilename();
         String storeFileName = createStoreFileName(originalFilename);
         multipartFile.transferTo(new File(getFullPath(storeFileName)));
         
         return new UploadFile(originalFilename, storeFileName);
    }
       
    private String createStoreFileName(String originalFilename) {
       String ext = extractExt(originalFilename);
       String uuid = UUID.randomUUID().toString();
       
       return uuid + "." + ext;
    }
       
    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(".");
        
        return originalFilename.substring(pos + 1);
    }
}

 

설정 값 분리의 필요성 (@Value)

더보기

DB 연결 정보나 외부 API 주소, 파일 경로 같은 설정 값들을 클래스에 입력하는 대신 properties, yml 등으로 분리해서 관리하면 환경에 따라 유연하게 값을 설정해줄 수 있고 불필요한 컴파일, 직접 클래스 코드를 하나 하나 수정하는 등의 문제를 해결할 수 있다.

 

@Value

 

1. 프로퍼티 치환법

 

properties에 작성된 키 값을 ${}안에 넣어주면 Spring이 PropertyPlaceHolderConfigurer를 통해 초기화 작업 중에 해당 값을 실제 값으로 치환한다. PropertyPlaceHolderConfigurer는 팩토리 후처리기로써 빈 설정 메타정보가 모두 준비됐을 때 빈 메타정보 자체를 조작하는 역할을 한다. 하지만 대체할 위치를 치환자로 지정해두고 후처리기가 치환해주기를 기다리는 수동적인 방법으로 빈에서 직접 값을 꺼내는 SpEL 방법이 권장 된다.

 

2. SpEL (Spring Expression Language)

 

SpEL은 기본적으로 #{} 안에 표현식을 넣도록 되어있는데, user.name이라는 표현식은 이름이 user인 빈의 name 프로퍼티를 의미한다. SpEL은 다른 빈의 프로퍼티에 접근가능할 뿐만 아니라 메소드 호출도 가능하고 다양한 연산도 지원하며 클래스 정보에도 접근할 수 있다. 심지어 생성자를 호출해서 객체를 생성할 수도 있다. 

 

 

https://mangkyu.tistory.com/167

 

  • UploadFile : 클라이언트에서 업로드한 파일명과 서버에 저장할 파일명을 가지고 있는 클래스
  • List<UploadFile> storeFiles : multipartFile 을 하나씩 storeFile()을 통해 서버에 저장하고 List에 UploadFile을 add
  • UploadFile storeFile : UUID 랜덤값을 생성하고 확장자를 추출해서 서버 경로에 파일을 저장하고 UploadFile을 반환

 

3. 폼 전송 객체

 

@Data
public class ItemForm {
    private Long itemId;
    private String itemName;
    private List<MultipartFile> imageFiles;
    private MultipartFile attachFile;
}

 

 

4. 컨트롤러

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemRepository itemRepository;
    private final FileStore fileStore;
  
    @GetMapping("/items/new")
    public String newItem(@ModelAttribute ItemForm form) {
        return "item-form";
    }
      
    @PostMapping("/items/new")
    public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes
redirectAttributes) throws IOException {
        UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
        List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

        //데이터베이스에 저장
        Item item = new Item();
        ...
        itemRepository.save(item);
        
        redirectAttributes.addAttribute("itemId", item.getId());
        return "redirect:/items/{itemId}";
    }

    @GetMapping("/items/{id}")
    public String items(@PathVariable Long id, Model model) {
        Item item = itemRepository.findById(id);
        model.addAttribute("item", item);
        return "item-view";
    }
    
    @ResponseBody
    @GetMapping("/images/{filename}")
    public Resource downloadImage(@PathVariable String filename) throws
MalformedURLException {
        return new UrlResource("file:" + fileStore.getFullPath(filename));
}

    @GetMapping("/attach/{itemId}")
    public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId)
throws MalformedURLException {
        Item item = itemRepository.findById(itemId);
        String storeFileName = item.getAttachFile().getStoreFileName();
        String uploadFileName = item.getAttachFile().getUploadFileName();
        UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));

        String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
        String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";

        return ResponseEntity.ok()
                             .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                             .body(resource);
     }
}

 

  • Resource downloadImage : <img> 태그로 이미지를 조회할 때, UrlResource로 이미지 파일을 읽어서 이미지 바이너리를 반환
  • ResponseEntity<Resource> dounloadAttach : 파일을 다운로드 할 때 클라이언트가 업로드했던 파일명으로 생성

 

 

5. View

 

<form th:action method="post" enctype="multipart/form-data">
    <ul>
        <li>상품명 <input type="text" name="itemName"></li>
        <li>첨부파일<input type="file" name="attachFile" ></li>
        <li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
    </ul>
    <input type="submit"/>
</form>
<div class="container">
    <div class="py-5 text-center">
    <h2>상품 조회</h2> </div>
    상품명: <span th:text="${item.itemName}">상품명</span><br/>
    첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|"
th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
    <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/$
{imageFile.getStoreFileName()}|" width="300" height="300"/>

</div>

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

+ Recent posts