파일을 다루기 때문에 업로드할 파일이나 다운로드할 파일의 관련 설정을 먼저 해야한다(업로드 경로, 업로드 크기 설정, 다운로드 파일 크기 등...) 프로젝트 안에 application.properties나 application.yml이 있을 것이다. 관련 설정 포함 시켜주자.

 

1. 설정

spring.servlet.multipart.enabled=true
spring.servlet.multipart.file-size-threshold=2KB
spring.servlet.multipart.max-file-size=200MB
spring.servlet.multipart.max-request-size=215MB
upload-path=/User/upload

이때 주의할 점은 업로드 경로다. 절대주소에서 최상위 디렉토리(Root)만 빠진 주소다. 최상위 디렉토리를 프로젝트 폴더가 C에 있나 D에 있나로 결정된다. 그래서 프로젝트 폴더가 C/D 드라이브 등 잘 파악하고 하자. 

 

2. Controller - 파일 업로드 , 파일 멀티 업로드, 파일 다운로드 3개로 구성된다.

가끔 다운로드가 안되는 경우가 있다. 코드에 문제는 아니다. 다만 설정을 잘못했을 것이다...

RequestMapping 주소가 있기 때문에 파일 업로드시 다운로드 Uri에 RequestMapping이 추가 됐는지 확인하자.

package com.example.filedemo.controller;

import com.example.filedemo.domain.FileUpload;
import com.example.filedemo.service.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/file")

public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileStorageService fileStorageService;

    @Autowired
    private FileService fileService;
    
    @PostMapping("/uploadfile")
    public File uploadFile(@RequestParam("file") MultipartFile file) {
        if(file.isEmpty()){
            //파일 업로드가 안됐을 시 처리
        }
        String fileName = fileStorageService.storeFile(file);
        //확장자만 추출하는 형태 ex) exe , png, jpg ...
        String fileExt = fileName.replaceAll("^.*\\.(.*)$", "$1");
        
        String fileOriginalName = StringUtils.cleanPath(file.getOriginalFilename());

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/file/downloadfile/")
                .path(fileName)
                .toUriString();

        return new File(fileName, fileOriginalName, fileExt, fileDownloadUri);
    }
    
	@PostMapping("/uploadmultiplefiles")
    public List<File> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        return Arrays.asList(files)
                .stream()
                .map(file -> uploadFile(file))
                .collect(Collectors.toList());
    }
    
	@GetMapping("/downloadfile/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        // Load file as Resource
        Resource resource = fileStorageService.loadFileAsResource(fileName);

        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info("Could not determine file type.");
        }

        // Fallback to the default content type if type could not be determined
        if(contentType == null) {
            contentType = "application/octet-stream";
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }
}

 

3. 파일 업로드에 대한 Response 해줄 클래스다. DTO 쯤으로 생각한다. 별 내용없고 파일 이름, 다운로드 경로, 사이즈 등등...을 저장하는 클래스다. 아마 경우에 따라 필요하지 않아서 생성하지 않을수도 있지만 이번에는 생성하는 방식으로 개발했다.

package com.example.filedemo.domain;

@Getter
@Setter
@AllArgsConstructor
@ToString
public class FileUpload {
    private String fileName;
    private String fileOriginalName;
    private String fileDownloadUri;
    private String fileExt;
    private long size;
    
    public FileUpload(String fileName, String fileOriginalName, String fileExt, String fileDownloadUri) {
        this.fileName = fileName;
        this.fileOriginalName = fileOriginalName;
        this.fileExt = fileExt;
        this.fileDownloadUri = fileDownloadUri;
    }
}

4. 서비스

 

이제 직접적으로 파일 업로드/다운로드의 서비스 로직을 만들 차례다. 

만약 맨 처음 파일 경로가 안 읽어질수도 있다. 주의하자.

파일 업로드 시 파일 이름 등을 바꾸고 싶다면 storeFile()을 보면 된다.

보통 파일이름을 오리지널 그대로 올리는 경우가 있는데 그렇게 해선 안된다. 이름이 겹치면 올라가지 않을뿐더라 나중에 보안에 문제가 생길 수 있다. 원래 파일이름을 통신을 통해 보내서 DB에 저장하고 파일이름 설계에 의한 이름 + 타임스태프로 해주는 경우가 많으니 참고바란다. 우선 난 타임스탬프 + _파일이름으로 해놓을 예정이다. 물론 Response에서 오리지널 이름과 변환 이름 둘 다 반환한다.

package com.example.filedemo.service;

import com.example.filedemo.property.config.Properties;
import com.example.filedemo.exception.FileStorageException;
import com.example.filedemo.exception.MyFileNotFoundException;
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.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;

    @Autowired
    public FileStorageService(Properties Properties) {
        this.fileStorageLocation = Paths.get(Properties.getUploadPath())
                .toAbsolutePath().normalize();

        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
        }
    }

    public String storeFile(MultipartFile file) {
        SimpleDateFormat sdf = new SimpleDateFormat ("yyyyMMddhhmmss_");
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        String timeStamp = sdf.format(timestamp);
        
        String fileName = timeStamp + StringUtils.cleanPath(file.getOriginalFilename());

        try {
            // Check if the file's name contains invalid characters
            if(fileName.contains("..")) {
                throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
            }

            // Copy file to the target location (Replacing existing file with the same name)
            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            return fileName;
        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
        }
    }

    public Resource loadFileAsResource(String fileName) {
        try {
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            if(resource.exists()) {
                return resource;
            } else {
                throw new MyFileNotFoundException("File not found " + fileName);
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException("File not found " + fileName, ex);
        }
    }
}

 

5. 예외 클래스

 

파일을 업로드나 다운로드할 때 예외가 발생을 경우를 처리해 줄 예외클래스를 만들어준다.

package com.example.filedemo.exception;

public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }

    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
package com.example.filedemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
    public MyFileNotFoundException(String message) {
        super(message);
    }

    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

6. 테스트

 

백엔드 개발이 완료 됐으니 API를 Postman으로 테스트를 해보자.

지금 같은 경우 Controller에서 RequestMapping을 해주었다. 그렇기 때문에 주소가 /localhost:8080/api/file/uploadfile이 된다.

'IT > Spring' 카테고리의 다른 글

[Spring Boot] Spring Boot 입문 - 프로젝트 세팅  (0) 2021.06.22
[Spring] Spring Framework 이클립스 세팅  (0) 2021.06.03

+ Recent posts