Spring MVC 단일/다중 파일업로드/다운로드

devdo·2022년 11월 3일
0

SpringBoot

목록 보기
33/39
post-thumbnail

기본 지식

첨부파일을 하기 위한 form 태그 전송 방식

multipart/form-data 전송 방식

  • multipart/form-data는 여러 형태의 자료(문자 / 바이너리)를 전송하기 위한 전송형태다.
    여러 형태를 전송하기 때문에 multipart라는 이름을 가진다.
  • multipart/form-data를 사용하기 위해서는 Form 태그에 enctype="multipart/form-data"를 입력해줘야한다.

예시

<form id="form" action="/board/register" method="post" enctype="multipart/form-data">
    <input type="file" name='uploadFile' multiple>  // multiple: 다중 파일
</form>

multipart/form-data가 생성한 HTTP 메세지들

보면 기본적으로 쓰는 application/x-www-urlencoded와 다르다는 것을 알 수 있다. 먼저 Content-type에 boundary라는 값이 보인다. 데이터는 이렇게 boundary로 나누어져서 전송되는 것을 알 수 있다.

전송되는 데이터들은 Boundary로 나누어지고, 이것을 Part라고 한다. 각 Part는 각 Part에 맞는 Header 정보와 Body값을 가진채로 서버로 전송된다.
파일을 업로드하기 위해서는 Part 단위로 전송되는 이 값을 처리해주어야 한다.

스프링이 알아서 변환해준다. MultiPartResolver

multipart/form-data = true인 경우, 스프링은 Dispatcher Servlet에서 MultiPartResolver를 실행한다. MultiPartResolver는 멀티파트 요청(file까지 보내는 요청)인 경우, 서블릿 컨테이너가 전달하는 일반전인 HttpServletRequest인 RequestFacade를 MultiPartHttpservletRequest 형태로 변환해서 넘겨준다!

파일업로드 방식 2가지!

  • 서블릿 기술 : HttpServletRequest에서 getPart로 받기
  • 스프링 기술 : MultiPartFile로 받기

🍀 참고)

Spring은 @RequestBody가 아닌 경우, 자동적으로 기본형 자료에는 @RequestParam이, 나머지 자료에는 @ModelAttribute가 적용된다.


UUID

업로드 된 파일을 저장할 때 주의할 사항이 있다. 서버는 하나지만, 클라이언트는 여러 군데에서 값을 올려준다. 서로 다른 클라이언트가 2.jpg라는 값을 각각 올려주면 별다른 조치를 하지 않으면 값은 소실될 수 밖에 없다. 이것을 해결하기 위해서 UUID를 이용해 파일을 저장하는 것을 추천한다.

서버에 저장될 파일 이름은 UUID로 만들어준다. 그리고 그 UUID로 만든 파일명이 업로드 파일명을 맵핑해준다. 위처럼 객체 형태로 파일명을 바인딩해서, 바인딩 된 파일명만 DB에 넣어주는 방식도 있다. 그리고 파일이 필요할 때는, 해당 객체를 DB에서 찾아와서 여기서 이름을 참조해서 값을 내려주는 것이다


설정

build.gradle

# file upload/download
implementation 'commons-io:commons-io:2.6'
implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'

application.yml

spring:
  servlet:
    multipart:
      enabled: 'true'        # multipart/form-data를 사용할 수 있는지를 정한다.
      max-request-size: 10MB # 한번에 올릴 수 있는 전체 최대 파일 용량 
      max-file-size: 1MB     # 파일 하나 당 올릴 수 있는 최대 파일 용량

파일업로드 코드 구현

upload 파일 경로를 설정하는 방식에는 크게 두가지

1) 하드코딩

아래 코드처럼 아예 코드로 못박아 두는 방식

private final static String UPLOAD_DIRECTORY = "upload";

2) @Value 어노테이션으로 설정하는 방식

아래 사진처럼 application.yml에 임의의 이름으로 파일경로를 적어둔다.

import org.springframework.beans.factory.annotation.Value;

application.yml

file:
  upload:
    path: C:\\upload

그리고 스프링 Component 안에 @Value 어노테이션을 써서 불러올 수 있게 할 수 있다. (Lombok의 @Value가 X)

    @Value("${file.upload.path}")
    private String uploadPath;

단일 파일 업로드

    @PostMapping("/single")
    public String singleFileUpload(@RequestParam("singleFile") MultipartFile singleFile, HttpServletRequest request) {

        log.info("singleFile: {}", singleFile);
        String path = request.getSession().getServletContext().getRealPath("resources");
        log.info("path: {}", path);     // C:\WebStudy\Study\ebrainSoft\board_mybatis\src\main\webapp\resources
        String root = path + File.separator + UPLOAD_DIRECTORY;

        File file = new File(root);

        if (!file.exists()) file.mkdir();

        String originalFilename = singleFile.getOriginalFilename();
        String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
        String uuidFileName = UUID.randomUUID().toString() + ext;

        File changeFile = new File(root + File.separator + uuidFileName);

        try {
            singleFile.transferTo(changeFile);
            log.info("파일업로드 성공");
        } catch (IOException e) {
            log.error("파일업로드 실패");
            e.printStackTrace();
        }
        return "result";
    }

다중 파일 업로드

1)번 형태

    @PostMapping("/multi")
    public String multiFileUpload(
            @RequestParam("multiFile") List<MultipartFile> multipartFiles,
            HttpServletRequest request
    ) {
        log.info("multipartFiles: {}", multipartFiles);

        String path = request.getSession().getServletContext().getRealPath("resources");
        log.info("path: {}", path);     // C:\WebStudy\Study\ebrainSoft\board_mybatis\src\main\webapp\resources
        String root = path + File.separator + UPLOAD_DIRECTORY;

        File fileCheck = new File(root);

        if(!fileCheck.exists()) fileCheck.mkdirs();

        List<Map<String, String>> fileList = new ArrayList<>();

        for (int i = 0; i < multipartFiles.size(); i++) {
            String originFile = multipartFiles.get(i).getOriginalFilename();
            String ext = originFile.substring(originFile.lastIndexOf("."));
            String changeFile = UUID.randomUUID().toString() + ext;

            Map<String, String> map = new HashMap<>();
            map.put("originFile", originFile);
            map.put("changeFile", changeFile);

            fileList.add(map);
        }

        try {
            for(int i = 0; i < multipartFiles.size(); i++) {
                File uploadFile = new File(root + File.separator + fileList.get(i).get("changeFile"));
                multipartFiles.get(i).transferTo(uploadFile);
            }
            log.info("다중 파일 업로드 성공");
            } catch (IOException e) {
                log.info("다중 파일 업로드 실패");
                e.printStackTrace();
                for(int i = 0; i < multipartFiles.size(); i++) {
                    new File(root + "\\" + fileList.get(i).get("changeFile")).delete();
                }
            }
        return "result";
    }

2)번 형태

@PostMapping("/uploadAjaxAction")
    public ResponseEntity<List<AttachVO>> uploadAjaxPost(MultipartFile[] uploadFile) {

        List<AttachVO> attachList = new ArrayList<>();

        for (MultipartFile multipartFile : uploadFile) {

            String uploadFileName = multipartFile.getOriginalFilename();
            long size = multipartFile.getSize();
            log.info("-------------------------------------");
            log.info("Upload File Name: " + uploadFileName);
            log.info("Upload File Size: " + size);

            AttachVO attachVO = new AttachVO();

            uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
            log.info("only file name: " + uploadFileName);
            attachVO.setFileName(uploadFileName);

            UUID uuid = UUID.randomUUID();

            uploadFileName = uuid.toString() + "_" + uploadFileName;

            try {
                File saveFile = new File(uploadPath, uploadFileName);
                multipartFile.transferTo(saveFile);

                attachVO.setUuid(uuid.toString());
                attachVO.setUploadPath(uploadPath);

                if (checkImageType(saveFile)) {
                    attachVO.setImage(true);
                }

                attachList.add(attachVO);
            } catch (Exception e) {
                log.error(e.getMessage());
            } // end catch
        } // end for

        return new ResponseEntity<>(attachList, HttpStatus.OK);
    }

    // 이미지파일인지 유무확인
    private boolean checkImageType(File file) {

        try {
            String contentType = Files.probeContentType(file.toPath());
            log.info("checkImageType: {}", contentType);

            return contentType.startsWith("image");

        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

파일다운로드 코드 구현

파일 저장하기 기술

  • 서블릿 기술 : part.getWrite()로 저장
  • 스프링 기술 : MultiPartfile.transferTo()로 저장

다른 형태의 기술이기 때문에 당연히 업로드 된 파일을 저장하는 것도 다르다. 서블릿 기술은 경로를 잡고 part를 getWrite()를 해주면 된다. MultiPartfile은 transferTo()로 저장을 할 수 있게 된다.


파일 다운로드 Header 정보 꼭 필요!

파일을 실제로 PC에 다운받으려면 응답에 특정 Header를 포함시켜야한다. 만약, PC에서도 파일을 직접 다운 받을 수 있게 하고 싶다면

Content-Disposition : attachment; filename="파일명" 

형식의 헤더를 반드시 응답에 포함시켜서 내려줘야한다.

여기서 지정된 파일명은 실제 PC에 다운로드 받아질 때의 파일명이 된다.
웹이 이미지를 렌더링하고 싶다면, 위 헤더를 포함하지 않고 응답을 내려주기만 하면 된다.

ContentType(MMIE 타입)을 지정해주어야 한다.

"application/octer-stream"  // 이미지외에는 바이너리파일로 만들어준다.

파일 다운로드 (기본)

    @RequestMapping("download")
	public void fileDownload(HttpServletRequest req, HttpServletResponse res) {
		String filename = req.getParameter("fileName");
		String readFilename = "";
		System.out.println("filename: "+filename);

		try {
			String browser = req.getHeader("User-Agent"); // User-Agent 정보 꼭 필요!
				// 파일 인코딩
			if(browser.contains("MSIE") || browser.contains("Trident") || browser.contains("Chrome")) {
				filename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
				
			} else {
					filename = new String(filename.getBytes("UTF-8"), "ISO-8859-1");
			}
		} catch (UnsupportedEncodingException e) {
			
			e.printStackTrace();
			}
			readFilename = "I:\\upload\\" + filename;
			System.out.println("readFilename: "+ readFilename);
			File file = new File(readFilename);
			if(!file.exists()) {
				return ;
		}
		
		// 파일명 지정
		res.setContentType("application/octer-stream");
		res.setHeader("Content-Transfer-Encoding", "binary;");
		res.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
		
		try {
			OutputStream os = res.getOutputStream();
			FileInputStream fis = new FileInputStream(readFilename);
			
			int ncount = 0;
			byte[] bytes = new byte[512];
			
			while((ncount = fis.read(bytes)) != -1) {
				os.write(bytes, 0, ncount);
			}
			fis.close();
			os.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

Resource 객체 사용

    @GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName, HttpServletRequest request) throws UnsupportedEncodingException {

        Resource resource = new FileSystemResource(URLDecoder.decode(fileName, "UTF-8"));

        if (!resource.exists()) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        String resourceName = resource.getFilename();
        // remove UUID
        String resourceOriginalName = resourceName.substring(resourceName.indexOf("_") + 1);

        HttpHeaders headers = new HttpHeaders();
        try {

            boolean checkIE = (userAgent.indexOf("MSIE") > -1 || userAgent.indexOf("Trident") > -1);
            String downloadName = null;

            if (checkIE) {
//                downloadName = URLEncoder.encode(resourceOriginalName, "UTF8").replaceAll("\\+", " ");
                downloadName = resourceOriginalName.replaceAll("\\+", " ");
            } else {
                downloadName = new String(resourceOriginalName.getBytes("UTF-8"), "ISO-8859-1");
            }
            headers.add("Content-Disposition", "attachment; filename=" + downloadName);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return new ResponseEntity<>(resource, headers, HttpStatus.OK);
    }

파일 삭제

    @PostMapping("/deleteFile")
    public ResponseEntity<String> deleteFile(String fileName, String type) {

        log.info("deleteFile: " + fileName);
        File file;
        try {
            file = new File(URLDecoder.decode(fileName, "UTF-8"));
            file.delete();
            // type image일시 썸네일 이미지까지 삭제
            if (type.equals("image")) {
                String largeFileName = file.getAbsolutePath().replace("s_", "");
                log.info("largeFileName: " + largeFileName);
                file = new File(largeFileName);
                file.delete();
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<>("deleted", HttpStatus.OK);
    }


참고

profile
배운 것을 기록합니다.

0개의 댓글