Spring Boot 7. 파일 업로드

 Spring Boot 7. 파일 업로드





1. 설정


내장된 Tomcat을 사용

application.properties 파일에 아래 코드 추가


# 파일 업로드를 위해 코드 추가
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=파일을 업로드할 경로
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB

enabled : 파일 업로드 가능 여부

location : 업로드된 파일의 입시 저장 경로

max-request-size : 한 번에 최대 업로드 가능한 용량

max-file-size : 파일 하나의 최대 크기



2. 파일 업로드 컨트롤러 준비


업로드된 파일의 처리는 컨트롤를 사용

Spring에서는 MultipartFile 타입을 제공해서 추가적인 처리 없이 바로 사용 가능

파일 업로드와 관련된 모든 작업은 Ajax 방식으로 처리

업로드 결과는 JSON 형태로 제공


controller 패키지 안에 UploadController 클래스 생성

Post 매핑으로 메소드를 생성하고 매개변수로 MultipartFile 타입의 배열을 받음 (여러 파일 업로드 가능)

@RestController
@Log4j2
public class UploadController {

@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles) {

for (MultipartFile uploadFile : uploadFiles) {
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(
                     originalName.lastIndexOf("//")+1);

log.info("File Name : " + fileName);
}
}
}

RestController : 별도의 화면 없이 데이터만 전송하는 컨트롤러

getOriginalFilename : MultipartFile 타입의 오리지날 이름 반환. 브라우저의 종류에 따라 전체 경로이거나 파일이름일 수 있음 -> 전체 경로일 경우를 대비해 파일 이름만 추출하는 작업 필요

substring : 해당하는 인덱스 부터 문자열을 추출해서 반환

lastIndexOf : 문자열에서 전달받은 내용이 마지막으로 나오는 인덱스 반환

=> 전체 경로가 포함된 이름에서 파일 이름만 추출 




3. 테스트 화면


테스트용 화면을 위한 컨트롤러 정의

controller 패키지 안에 UploadTestController 클래스 생성


GET 맵핑으로 html 파일을 화면에 표시

@Controller
public class UploadTestController {

@GetMapping("/uploadEx")
public void uploadEx() {}

}


templates 폴더에 uploadEx.html 파일 생성


jQuery를 사용하기 위한 코드를 <head> 태그 안에 추가

<script src="https://code.jquery.com/jquery-3.7.0.js"></script>


파일을 업로드하기 위한 <input> 태그와 <button> 태그 추가

<input> 태그의 타입을 file로 지정 -> 파일을 불러오는 요소가 화면에 표시됨

<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>


파일을 전송하기 위한 javascript 코드 추가

<script>

$(".uploadBtn").click(function () {
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;

for (var i = 0; i < files.length; i++) {
console.log(files[i]);
formData.append("uploadFiles", files[i]);
}

});

</script>

FormData 객체를 생성하고 파일을 FormData 객체에 추가

jQuery를 이용해서 file 타입인 요소를 불러오고 .files 메소드를 이용해서 파일들을 불러옴

for 문으로 요소에 하나씩 접근하며 FormData 객체의 append() 메소드를 이용해서 추가 (set메소드를 이용하면 덮어쓰기가 됨)


-> 실행해보면 업로드한 파일의 정보가 콘솔창에 출력됨










- Ajax 업로드 처리

Ajax를 이용해 파일 업로드 처리

$.ajax() 를 이용해서 몇가지 속성을 지정

위 코드의 for문 아래에 이어서 아래 코드 작성


// 파일 업로드 처리
$.ajax({
url: "/uploadAjax",
processData: false,
contentType: false,
data: formData,
type: "POST",
dataType: "json",
success: function (result) {
console.log(result);
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(textStatus);
}
});

url : 전송할 url 지정. UploadController에서 POST 맵핑한 주소로 작성

multipart/form-data 타입을 사용하기 위해 contentType을 false로 지정

type : POST로 지정해서 POST 방식으로 요청

success & error : 요청에 성공했을 때와 실패했을 때 수행할 작업 지정



실행해서 파일을 업로드하면 아직 컨트롤러에 업로드하는 부분을 작성하지 않았기 때문에 업로드가 되지는 않지만 로그에 파일의 이름이 출력됨








4. 컨트롤러에서 파일 업로드 처리


MultipartFile 타입에서 제공하는 transferTo() 메소드를 이용해서 파일을 저장


파일을 저장할 경로를 설정 파일을 통해 저장하고 사용할 수 있도록 application.properties 파일에 별도의 설정값을 추가하고 컨트롤러에서 해당 설정 값을 이용


application.properties

# 파일을 저장할 경로
com.example.upload.path=/Users/lte/Desktop/DEV/SpringBoot/Board/upload


UploadController

// 설정값 불러오기
@Value("${com.example.upload.path}")
private String uploadPath;

* @Value는 lombok의 Value가 아니라 org.springframework.bean.factory.annotation의 Value




* 파일을 저장할 때 고려해야 할 사항

- 동일한 이름의 파일

첨부 파일의 이름이 같으면 기존의 파일이 사라지고 새로운 파일로 변경됨

고유한 이름을 생성해서 파일 이름으로 사용해야 함

UUID 를 이용해서 고유한 값을 만들어서 사용

랜덤으로 UUID를 생성해서 'UUID값_파일이름' 형식으로 파일 저장



- 동일한 폴더에 너무 많은 파일

파일들을 동일한 넣으면 너무 많은 파일이 쌓일 수 있음 -> 성능 저하, 운영체제에 따라 하나의 폴더에 넣을 수 있는 파일의 수에 대한 제한이 있음

파일이 저장되는 시점의 년/월/일 폴더를 따로 생성해서 날짜별로 폴더 구분



- 파일 확장자 체크

첨부파일을 이용한 쉘(shell) 스크립트 파일을 업로드해서 공격하는 기법도 있기 때문에 특정 파일만 업로드할 수 있도록 처리

MultipartFile 타입의 getContentType() 메소드를 이용해서 처리



- 컨트롤러에서 처리


오늘 날짜에 해당하는 폴더를 생성하고 경로를 반환하는 메소드 구현

UploadController 클래스 안에 메소드로 구현

// 오늘 날짜 폴더 생성하고 경로 반환하는 메소드
private String makeFolder() {
String str = LocalDate.now().
format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));

String folderPath = str.replace("/", File.separator);

// 폴더 생성
File uploadPathFolder = new File(uploadPath, folderPath);
if (uploadPathFolder.exists() == false) {
uploadPathFolder.mkdirs(); // 오늘날짜 폴더가 없으면 생성
}

return folderPath;
}

format(DateTiemFormatter.ofPattern()) : 해당 날짜를 지정한 포멧의 문자열로 반환

replace() : 문자열에서 특정 요소를 변경

File.separator : 시스템 경로 구분자

File : 파일이나 디렉토리를 나타내는 자료형

exists() : 해당 경로가 존제하는지 확인하는 메소드

mkdirs() : 해당 경로의 폴더를 생성하는 메소드

=> 오늘 날짜를 문자열로 변환한 후 경로로 변경해서 존재하지 않으면 폴더를 생성하고 경로를 반환하는 메소드



uploadFile 메소드 안에 이어서 파일 업로드 처리


MultipartFile 배열의 요소를 순환하는 for문 안에 해당 파일이 이미지 파일인지 확인하는 코드 추가

// 이미지 파일인지 확인
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this file is not image type");
return;
}



위에서 선언한 makeFolder 메소드를 호출해서 오늘 날짜에 해당하는 폴더가 없다면 생성하고 경로를 반환 받음

/ 날짜 폴더 생성
String folderPath = makeFolder();


UUID를 이용해서 고유한 UUID를 생성하고 이를 이용해 파일 이름과 저장 경로 설정

// 고유한  UUID 생성
String uuid = UUID.randomUUID().toString();

// 파일이름과 저장할 경로
// (업로드 경로/오늘날짜 경로/UUID_파일 이름)
String saveName = uploadPath + File.separator + folderPath +
                         File.separator + uuid + "_" + fileName;

(업로드 경로는 application.properties에서 지정한 값을 받아와서 사용)


Paths.get() 메소드를 이용해서 결로를 받아화서 실제 파일 시스템 경로에 매핑하는 Path 객체 생성

MultipartFile 의 transferTo 메소드에 Path 객체를 전달해서 해당 경로에 파일 저장

// 저장 경로
Path savePath = Paths.get(saveName);

try {
// 파일 저장
uploadFile.transferTo(savePath);
}
catch (IOException e) {
e.printStackTrace();
}



실행하고 파일을 업로드하면 지정한 경로에 오늘 날짜 폴더가 생성되고 그 안에 업로드한 파일들이 UUID값이 추가된 이름으로 저장됨










5. 업로드 결과 반환


업로드 결과는 JSON 으로 전송됨

브라우저에서는 파일의 원래 이름, UUID 값, 저장경로가 필요


dto 패키지 안에 UploadResultDTO 클래스를 생성해서 필요한 정보를 담아서 전송

JSON 형식으로 전송하기 위해 클래스가 Serializable 인터페이스를 구현하도록 함


@Data
@AllArgsConstructor
public class UploadResultDTO implements Serializable { // 인터페이스 구현
    // 전송할 데이터
private String fileName;
private String uuid;
private String folderPath;

}


클래스 내부에 이미지의 url을 반환해주는 getImageURL 메소드 구현

public String getImageURL() {
try {
return URLEncoder.encode(folderPath+"/"+uuid+"_"+fileName,"UTF-8");
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}

URLEncoder.encoder() 메소드를 이용해서 문자열 url을 인코딩한 후에 반환



기존에 컨트롤러의 uploadFile 메소드를 ResponseEntity(HTTP 응답에 본문 부분) 객체를 반환하도록 메소드 수정

@PostMapping("/uploadAjax")
// 반환 타입을 void에서 ResponseEntity로 변경
public ResponseEntity<List<UploadResultDTO>> 
                                uploadFile(MultipartFile[] uploadFiles) {

// 반환할 dto 들을 담을 List 객체 생성
List<UploadResultDTO> resiltDTOList = new ArrayList<>();

for (MultipartFile uploadFile : uploadFiles) {

// 이미지 파일인지 확인
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this file is not image type");
// 이미지 파일이 아니경우 상태코드를 403(리소스 접근권한 없음)으로 해서 반환
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}


                ...  

try {
// 파일 저장
uploadFile.transferTo(savePath);
// UploadResultDTO 객체를 생성해서 List에 추가
resiltDTOList.add(new UploadResultDTO(fileName,uuid,folderPath));
}
catch (IOException e) {
e.printStackTrace();
}
}
// 업로드한 파일들에 대한 dto 리스트를 담아서 반환
return new ResponseEntity<>(resiltDTOList, HttpStatus.OK);
}


=> 파일을 업로드하면 JSON 배열 형태로 결과를 전달받음




6. 업로드 이미지 화면에 출력


브라우저에서 링크를 통해 <img> 태그 추가

서버에서 해당 URL이 호출되는 경우 이미지 파일 데이터를 브라우저로 전송

컨트롤러에 '/display?fileName=' 형식의 URL이 전달되면 호출되는 메소드 작성

파일을 byte타입의 배열로 변환해서 ResponseEntity 객체에 담아서 반환


@GetMapping("/display")
public ResponseEntity<byte[]> getFile(String fileName) {
// byte타입의 배열로 결과를 담을 ResponseEntity 객제 생성
ResponseEntity<byte[]> result = null;

try {
// 전달받은 URL을 URLDecoder.decode를 이용해서 문자열로 디코딩
String srcFileName = URLDecoder.decode(fileName, "UTF-8");
log.info("File Name : " + srcFileName);

// 파일의 전체 주소 생성
File file = new File(uploadPath + File.separator + srcFileName);
log.info(("File : " + file));

// 메타 데이터를 담기 위해 사용
HttpHeaders header = new HttpHeaders();

// MIME 타입 처리
// header의 Content-Type을 해당 파일의 MIME 타입으로 설정
// Files.probeContentType()을 이용해서 해당 파일의 MIME 타입을 반환 받음
header.add("Content-Type", Files.probeContentType(file.toPath()));

// 파일 데이터 처리
// FileCopyUtils.copyToByteArray 를 이용해서 파일을 Byte타입의 배열로 변환
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file),
                                   header, HttpStatus.OK);
}
catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}

return result;
}



- uploadEx.html 파일 수정


이미지를 추가할 <div> 태그 추가

<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>

<div class="uploadResult">
<!-- 이미지가 추가될 곳 -->
</div>


<script> 태그 안에 <div> 태그에 <img> 태그를 추가하는 함수 구현

function showUploadedImages(arr) {
console.log(arr);

var divArea = $(".uploadResult");

for (var i = 0; i < arr.length; i++) {
divArea.append("<img src='/display?fileName=" +
                                      arr[i].imageURL + "'>");
}
}


ajax로 전송하는 부분에 성공후 위에서 정의한 함수를 호출

// 파일 업로드 처리
$.ajax({
url: "/uploadAjax",
processData: false,
contentType: false,
data: formData,
type: "POST",
dataType: "json",
success: function (result) {
console.log(result);
showUploadedImages(result); // 추가
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(textStatus);
}
});
});

=> 업로드에 성공하면 반환받은 dto 리스트를 순환하면서 파일의 url을 이용해서 <img> 태그를 추가



















댓글

이 블로그의 인기 게시물

Spring Boot 6. 계시판 프로젝트

Spring Boot 2. Maria 데이터베이스 & Spring Data JPA

Django 10. DB 연동