250x250
Notice
Recent Posts
Recent Comments
관리 메뉴

탁월함은 어떻게 나오는가?

[Spring Boot] 이미지 업로드, 조회, 삭제 기능 작업 테스트 본문

[Snow-ball]server/스프링(Spring)

[Spring Boot] 이미지 업로드, 조회, 삭제 기능 작업 테스트

Snow-ball 2024. 2. 19. 21:05
반응형

스프링 부트의 파일 업로드와 관련된 설정은

1) 별도의 파일 업로드 라이브러리(commons- fileupload 등)를 이용하는 경우와

2) Servlet 3버전부터 추가된 자체적인 파일 업로드 라이브러리를 이용하는 방식으로 구분 할 수 있다.

 

별도의 파일 업로드 라이브러리는 WAS의 버전이 낮은 경우나 WAS가 아닌 환경에서 스프링 부트 프로젝트를 실행한다면 별도의 라이브러리를 사용하는 것이 좋다. 나의 경우에는 스프링 부트 자체에서 지원해주는 이미지 업로드 기능을 사용해보았다.

 

 

 


 

 

 

파일 업로드 위한 설정

1
2
3
4
5
6
7
8
9
10
server.port = 8080
 
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=/Users/ijeonghyeon/Desktop/junghyun/board-spring-boot/upload
#spring.servlet.multipart.location=C:\\upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB
 
#movie.upload.path = C:\\upload
movie.upload.path =/Users/ijeonghyeon/Desktop/junghyun/board-spring-boot/upload
cs

 

* spring.servlet.multipart.enabled: 파일 업로드 기능 여부를 선택

* spring.servlet.multipart.location: 업로드된 파일의 임시 저장 경로

   > window와 mac 의 경우에는 경로 설정이 다르다. 윈도우의 경우에는 주석처럼 처리하면 된다.

* spring.servlet.multipart.max-request-size: 한 번에 최대 업로드 기능 용량

* spirng.servlet.multipart.max-file-size: 파일 하나의 최대 크기

 

 

 


 

 

 

업로드 파일 코드

 

UploadResultDTO.java

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
31
32
33
34
35
36
37
38
package com.jh.move_review.dto;
 
import lombok.AllArgsConstructor;
import lombok.Data;
 
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
 
@Data
@AllArgsConstructor
public class UploadResultDTO implements Serializable {
 
    private String fileName;
    private String uuid;
    private String folderPath;
 
    public String getImageURL() {
        try {
            return URLEncoder.encode(folderPath + "/" + uuid + "_" + fileName, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
 
        return "";
    }
 
    public String getThumbnailURL() {
        try {
            return URLEncoder.encode(folderPath + "/s_" + uuid + "_" + fileName, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
 
        return "";
    }
}
 
cs

 

 

UploadController.java

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package com.jh.move_review.controller;
 
import com.jh.move_review.dto.UploadResultDTO;
import lombok.extern.log4j.Log4j2;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
 
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
 
@RestController
@Log4j2
@RequestMapping("/movie/")
@CrossOrigin("*")
public class UploadController {
 
    @Value("${movie.upload.path}")
    private String uploadPath;
 
    @Transactional
    @DeleteMapping("/removeFile")
    public ResponseEntity<Boolean> removeFile(String fileName) {
 
        String srcFileName = null;
 
        try {
 
            srcFileName = URLDecoder.decode(fileName, "UTF-8");
            File file = new File(uploadPath + File.separator + srcFileName);
            boolean result = file.delete();
            if (!result) return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
 
            File thumbnail = new File(file.getParent(), "s_" + file.getName());
 
            result = thumbnail.delete();
 
            return new ResponseEntity<>(result, HttpStatus.OK);
 
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
        }
 
    }
 
    @GetMapping("/display")
    public ResponseEntity<byte[]> getFile(String fileName) {
 
        ResponseEntity<byte[]> result = null;
 
        try {
            String srcFileName = URLDecoder.decode(fileName, "UTF-8");
            File file = new File(uploadPath + File.separator + srcFileName);
 
            HttpHeaders header = new HttpHeaders();
 
            // MIME 타입 처리
            header.add("Content-Type", Files.probeContentType(file.toPath()));
 
            // 파일 데이터 처리
            result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
        } catch (Exception e) {
            log.error(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
 
        return result;
    }
 
    @PostMapping("/upload")
    public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) {
 
        List<UploadResultDTO> resultDTOList = new ArrayList<>();
 
        for (MultipartFile uploadFile: uploadFiles) {
 
            // 이미지 파일만 업로드 가능
             if (uploadFile.getContentType().startsWith("image"== false) {
                 log.warn("this file is not image type");
                 return new ResponseEntity<>(HttpStatus.FORBIDDEN);
             }
 
            // 실제 파일 이름 IE나 Edge는 전체 경로가 들어오므로
            String originalName = uploadFile.getOriginalFilename();
            String fileName = originalName.substring(originalName.lastIndexOf("\\"+ 1);
 
            // 날짜 폴더 생성
            String folderPath = makeFolder();
 
            // UUID
            String uuid = UUID.randomUUID().toString();
 
            // 저장할 파일 이름 중간에 "_"를 이용해서 구분
            String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
            Path savePath = Paths.get(saveName);
 
            try {
                uploadFile.transferTo(savePath); // 실제 이미지 저장
 
                // 섬네일 생성
                String thumbnailSaveName = uploadPath + File.separator + folderPath + File.separator + "s_" + uuid + "_" + fileName;
                // 섬네일 파일 이름은 중간에 s_로 시작하도록
                File thumbnailFile = new File(thumbnailSaveName);
                // 섬네일 생성
                Thumbnailator.createThumbnail(savePath.toFile(), thumbnailFile, 100100);
 
                resultDTOList.add(new UploadResultDTO(fileName, uuid, folderPath));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 
        return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
    }
 
    private String makeFolder() {
 
        String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
 
        String folderPath = str.replace("/", File.separator);
 
        // make folder
        File uploadFolder = new File(uploadPath, folderPath);
 
        if (uploadFolder.exists() == false) {
            uploadFolder.mkdirs();
        }
 
        return folderPath;
    }
 
}
 
cs

 

 

 

 


 

 

 

 

테스트 사이클 설명

업로드 기능을 만들었으니 테스트를 진행해볼 예정이다. 테스트는 포스트맨(postman)으로 진행해볼 예정이다.

 

테스트 시작하기 전에 사이클에 대해서 간단하게 설명하고 시작해보자.

 

첫째. 업로드(/upload)를 사용하면 기본적으로 배열값을 반환하게 만들었다. 그렇기 때문에 1개이상일 경우에 배열안에 하나의 객체의 상태로 응답이 오게 만들어져 있다.

 

아래는 하나의 사진만 업로드 한경우의 기본 틀이다.

1
2
3
4
5
6
7
8
9
[
    {
        "fileName": "",
        "uuid": "",
        "folderPath": "",
        "imageURL": "",
        "thumbnailURL": ""
    }
]
cs

 

* fileName: 사진을 업로드할때의 실제 파일 이름이다.

* uuid: 동일한 사진을 여러번 올릴 수 있기 때문에 올릴때마다 겹치지 않기 위해서 uuid를 사진 url에 입력해주었고, 어떤 uuid를 사용했는지를 보여주기 위한 용도이다.

* folderPath: 사진을 저장할 때 한 폴더안에 저장해놓으면 나중에 관리하기 어려울테니 년도/월/달 로 구분되게 저장하게 만들었고, 그에 대한 경로이다.

* imageURL: 실제 크기의 사진의 URL이다.

* thumbnailURL: 썸네일 용도로 사용하기위해 사이즈를 줄인 사진 URL이다.

 

둘째. 디스플레이(/display)를 사용하면 해당 경로에 있는 이미지를 불러오는 기능이다. imageURL을 사용하면 원본의 이미지를 불러오고, thumbnailURL을 사용하면 썸네일 이미지를 불러온다.

 

셋째. 리무브파일(/removeFile)은 이미지 삭제를 위한 용도이다. 이미지는 원본의 imageURL을 넣으면 원본과 썸네일을 같이 지우도록 처리하게 만들었다.

 

 

 


 

 

 

테스트

첫째. 테스트를 진행하기 전인 오늘(2024/02/19)의 폴더 안이다. 현재는 진행한게 없기 때문에 폴더만 존재한다.(테스트를 위해 미리 만들어 놨지만, 실제로는 19일의 폴더가 없는게 맞다. 존재하지 않는다면 자동적으로 만들게 설정해놓았다)

 

 

 

둘째. 포스트맨에서 /upload와 함께 POST로 데이터를 전송하면 아래와 같이 응답이 도착하는걸 확인 할 수 있다.

이미지이기 때문에 Body > form-data를 사용해주었다. 응답이 오고 나서 폴더를 확인해보면 이미지 2개씩(원본, 썸네일)추가가 되기 때문에 총 4개가 추가 된걸 확인 할 수 있다. 

 

그리고 폴더안에서 확인해보면 사진의 크기에 따라 크기(용량)이 달라지는걸 확인할 수 있다. 그렇기 때문에 추후에 AWS든 로컬이든 이미지를 저장할 때 원본으로 모든걸 저장할 필요가 없다면 그에 따라 적절히 저장하면 좋다.

 

 

 

 

셋째. /display와 함께 GET을 요청하면 이제는 이미지를 조회할 수 있어졌다. 

 

3-1) 원본파일 조회 imageURL 사용

 

3-2) 썸네일 파일 조회 thumbnailURL 사용

 

 

 

넷째. /removeFile와 DELETE를 사용하면 사진을 지울 수 있다. DELETE의 경우도 POST와 같이 Body를 사용한다. 하지만, DELETE을 사용해서 파일을 삭제할 때는 x-www-form-urlencoded를 사용해서 보내주면 된다. 

 

삭제를 실행하니 정상적으로 삭제됨을 응닶값 true 로 확인할 수 있다. 그리고나서 저장되는 폴더로 들어가보면 2개를 삭제되어서 2개만 남아있는걸 확인 할 수 있다.

 

 

 

 

 

 

 

 

 

 

https://github.com/akdl911215/movie_board

 

GitHub - akdl911215/movie_board

Contribute to akdl911215/movie_board development by creating an account on GitHub.

github.com

 

반응형
Comments