첨부파일을 하기 위한 form 태그 전송 방식
multipart/form-data
전송 방식
문자 / 바이너리
)를 전송하기 위한 전송형태다.multipart
라는 이름을 가진다. enctype="multipart/form-data"
를 입력해줘야한다. 예시
<form id="form" action="/board/register" method="post" enctype="multipart/form-data">
<input type="file" name='uploadFile' multiple> // multiple: 다중 파일
</form>
보면 기본적으로 쓰는 application/x-www-urlencoded
와 다르다는 것을 알 수 있다. 먼저 Content-type에 boundary
라는 값이 보인다. 데이터는 이렇게 boundary로 나누어져서 전송되는 것을 알 수 있다.
전송되는 데이터들은 Boundary로 나누어지고, 이것을 Part
라고 한다. 각 Part는 각 Part에 맞는 Header 정보와 Body값을 가진채로 서버로 전송된다.
파일을 업로드하기 위해서는 Part
단위로 전송되는 이 값을 처리해주어야 한다.
multipart/form-data = true인 경우, 스프링은 Dispatcher Servlet
에서 MultiPartResolver
를 실행한다. MultiPartResolver는 멀티파트 요청(file까지 보내는 요청)인 경우, 서블릿 컨테이너가 전달하는 일반전인 HttpServletRequest인 RequestFacade를 MultiPartHttpservletRequest
형태로 변환해서 넘겨준다!
HttpServletRequest
에서 getPart
로 받기MultiPartFile
로 받기Spring은 @RequestBody가 아닌 경우, 자동적으로 기본형 자료에는 @RequestParam
이, 나머지 자료에는 @ModelAttribute
가 적용된다.
업로드 된 파일을 저장할 때 주의할 사항이 있다. 서버는 하나지만, 클라이언트는 여러 군데에서 값을 올려준다. 서로 다른 클라이언트가 2.jpg라는 값을 각각 올려주면 별다른 조치를 하지 않으면 값은 소실될 수 밖에 없다. 이것을 해결하기 위해서 UUID를 이용해 파일을 저장하는 것을 추천한다.
서버에 저장될 파일 이름은 UUID로 만들어준다.
그리고 그 UUID로 만든 파일명이 업로드 파일명을 맵핑해준다.
위처럼 객체 형태로 파일명을 바인딩해서, 바인딩 된 파일명만 DB에 넣어주는 방식도 있다. 그리고 파일이 필요할 때는, 해당 객체를 DB에서 찾아와서 여기서 이름을 참조해서 값을 내려주는 것이다
# file upload/download
implementation 'commons-io:commons-io:2.6'
implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
spring:
servlet:
multipart:
enabled: 'true' # multipart/form-data를 사용할 수 있는지를 정한다.
max-request-size: 10MB # 한번에 올릴 수 있는 전체 최대 파일 용량
max-file-size: 1MB # 파일 하나 당 올릴 수 있는 최대 파일 용량
upload 파일 경로
를 설정하는 방식에는 크게 두가지
아래 코드처럼 아예 코드로 못박아 두는 방식
private final static String UPLOAD_DIRECTORY = "upload";
@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()
로 저장을 할 수 있게 된다.
파일을 실제로 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();
}
}
@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);
}