관심쟁이 영호
[Spring Boot] 파일 업로드 본문
오늘은 Spring Boot를 이용하여 파일 업로드에 대해서 공부할 예정이다.
목차
- HTML Form 전송 방식
- File 도메인 생성
- 게시글 쓰기 Post Mapping 수정
- 파일 올리기 Service 작성
- FileHanler 작성
HTML 폼 전송 방식
파일을 서버와 주고받기 위해서는 가장 먼저, HTML Form 전송 방식을 알아야 한다.
두 가지의 전송 방식이 있다.
- application/x-www-form-urlencoded
- multipart/form-data
application/x-www-form-urlencoded
- 해당 방식은 HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다. form 태그에 enctype 옵션을 넣어주지 않으면 브라우저는 헤더에 다음 내용을 추가한다.
Content-Type: application/x-www-form-urlencoded
또한, 폼 내용을 HTTP Body에 A=10&B=13과 같이 "&"를 구분자로 이용하여 전송한다.
※ 파일을 업로드를 하기위해서는 문자가 아닌 바이너리 데이터를 전송해야 한다. 문자로는 파일을 전송하기가 어렵다. 근데 폼을 이용하여 파일을 전송해야 할 경우에는 파일뿐만 아니라 문자도 포함되어 있는 폼일 확률이 높다.
위와 같은 이유때문에 단순히 구분자 "&"를 이용하는 application/x-www-form-urlencoded를 이용하기는 한계가 있다.
multipart/form-data
- 해당 방식은 문자와 바이너리 데이터를 같이 보낼 수 있도록 하는 방식이다. 이것을 이용하기 위해서는 form에 enctype="multipart/form-data"라는 속성을 추가해야 한다.
- 이 방식으로 생성한 HTTP 메시지는 각각 전송 항목이 구분되어있다. 위의 예에서는 A, B, File로 각각 분리되어서 전송된다. 문자는 문자 그대로 전송, 파일은 파일 이름과 Content-Type이 추가되고 바이너리 데이터가 전송된다.
더 정확한 이해를 위해 실제 코드를 살펴보자.
실제 코드
html form
<form id="board" action="/board_write" method="post" th:object="${board}" enctype="multipart/form-data">
<div class="form-group">
<label for="title" class="h4">제목</label>
<input type="text" class="form-control" id="title" name="title" placeholder="제목을 작성해주세요.">
</div>
<hr width = "100%" color = "blue" size = "3"><br>
<input type="file" id="imageFiles" name="imageFiles" multiple="multiple" accept="image/*">
<hr width = "100%" color = "blue" size = "3">
<div class="form-group">
<label for="content" class="h4">내용</label><br>
<textarea class="form-control" id="content" name="content" rows="10"></textarea>
</div>
<hr width = "100%" color = "blue" size = "3"> <br>
<div class="text-center">
<button type="submit" class="btn btn-success" style="color:white;">등록하기</button>
<button type="button" class="btn btn-danger" style="color:white;" onclick="location.href='/board'">목록으로</button>
</div>
</form>
그리고 다음과 같이 내용을 작성했다.
그럼 HTTP 내용을 살펴보자!
※ HTTP 요청 내용을 보기 위해서 application yml에 다음과 같은 로깅 코드를 추가했다.
logging:
level:
org:
apache:
coyote:
http11: debug
그럼 HTTP 내용을 보자!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="title"
this is title!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="image"; filename="ì¬ì§.jpg"
Content-Type: image/jpeg
... ÿØÿà·'j©?AGÙ'ìÿÙ ...
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="content"
this is content!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2--
바이너리 코드가 너무 길어 생략했다.
살펴보면 다음과 같은 구조를 볼 수 있다.
- "---xxx"로 영역 구분
- "Content-Dispositon: form-data; ~~" : 영역의 시작(해당 영역에 대한 정보)
- "---xxx--"로 끝 명시
File 도메인 생성
File 도메인을 새롭게 생성하는 이유는 다음과 같다.
- User 정보에서 올린 파일들을 조회할 수 있기 위해
- 파일 또는 게시글 정보로만 양방향으로 접근할 수 있게 하기 위해
- 사용자가 정해진 여러 개의 파일을 올릴 수 있게 하기 위해
File이라는 객체를 따로 도메인으로 생성해놓으면, 여러 가지 방면에서 좋은 점이 있을 것이라고 판단이 된다.
Board 내에 리스트로 만들어놓는 것보다 유연하다고 판단된다. 그리고 User 정보만으로도 File로 접근할 수 있고(리스트로 하면 Board를 거쳐야 한다.) File만으로도 User 및 Board에 접근할 수 있다.
※ 따로 관리하는 만큼 조금 더 복잡하겠지..
다음은 도메인을 생성한 코드이다.
@Data
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity
@Table(name = "user_file")
public class UserFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "file_id")
private Long id;
@ManyToOne
@JoinColumn(name = "boardId")
private Board board;
@Column(nullable = false)
private String origFileName; // 파일 원본명
@Column(nullable = false)
private String filePath; // 파일 저장 경로
@Column(nullable = false)
private String savedName;
private Long fileSize;
@ManyToOne
@JoinColumn(name = "userId")
private User user;
@CreationTimestamp
private Timestamp createDate;
}
여러 가지의 내용이 있다.
처음에는 관련 자료들을 찾아보다가 이해가 안 되었다. 그래서 다음 그림을 보면서 이해해보자.
실제 서버는 각종 파일들을 어떻게 관리할까?
- 실제 서버가 가지고 있는 DB에는 파일이 저장되어있는 디렉터리 정보만 저장되어 있다.
- 이후에 해당 파일을 접근해야 될 일이 있으면 DB의 디렉터리를 이용하여 접근한다.
이러한 이유로 파일 도메인은
directory, original name 등등 여러 가지의 필드가 존재한다.
그 외 나머지는 @ManyToOne 관계의 객체, 저장 시간, 시퀀스 등등이다.
게시글 쓰기 Post Mapping 수정
이전에 사용한 post에서 파일을 받을 수 있게 수정해주어야 한다.
스프링에서는 파일을 주고받을 때, MultipartFile을 이용한다.
코드를 살펴보자.
@PostMapping("/board_write")
public String BoardWriter(@ModelAttribute Board board, @RequestParam(value="imageFiles", required=false) List<MultipartFile> files, @SessionAttribute(name = SessionConst.LOGIN_USER, required = false)
User user) throws IOException {
boardWriteService.write(board, user);
boardUserFileService.userFilelRepository(fileHandler.UserFileUpload(files, user.getUserId()), board);
return "redirect:/board";
}
@RequestParam을 이용하여 파라미터를 받는 것을 볼 수 있다.
List인 이유는 사용자가 한 번의 요청으로 여러 개의 파일을 보낼 수가 있기 때문이다.
파일 올리기 서비스 작성
코드를 바로 살펴보자!
@Service
@RequiredArgsConstructor
public class BoardUserFileService {
private final UserFileRepository userFileRepository;
public void userFilelRepository(List<UserFile> userFileList, Board board){
if(userFileList.isEmpty()){
return;
}
for(UserFile userfile : userFileList){
userfile.setBoard(board);
userFileRepository.save(userfile);
}
}
};
유저가 파일을 안 올렸을 경우에는 바로 리턴,
파일을 올려주었을 경우에는 File과 해당 Board를 연결해주어야 한다. 그래서 File의 내용에 board객체를 set 해주는 것을 볼 수 있다.
그리고 jpa를 이용하여 저장했다.
FileHandler 작성
실제로 요청으로 오는 파일은 바이너리 값과 이름이다. 그래서 해당 바이너리를 디렉터리, 저장 이름 등을 설정해주어야 한다.
※ 여기서 디렉터리를 새롭게 생성해주거나 저장할 파일 이름(사용자가 입력한 거랑 다름!!) 그리고 MultipartFile -> File 객체로 변경해준다.
다음 코드를 보자
@Component
@RequiredArgsConstructor
@Slf4j
public class FileHandler {
private final BoardUserFileService boardUserFileService;
private final UserRepository userRepository;
public List<UserFile> UserFileUpload(List<MultipartFile> files, String userId) throws IOException {
// 반환할 파일 리스트
List<UserFile> fileList = new ArrayList<>();
User user = userRepository.findByUserId(userId);
// 전달되어 온 파일이 존재할 경우
if(!CollectionUtils.isEmpty(files)) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern("yyyyMMdd");
String current_date = now.format(dateTimeFormatter);
for(MultipartFile multipartFile : files) {
String path = "C:/test"+"/"+userId+"/";
String extension;
String contentType = multipartFile.getContentType();
if(ObjectUtils.isEmpty(contentType)) {
break;
}
extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
String new_file_name = current_date + "_"+System.nanoTime() + "." + extension;
File file = new File(path + new_file_name); //파일 저장 경로 확인, 없으면 만든다.
if (!file.exists()) {
file.mkdirs();
}
UserFile userFile = new UserFile();
userFile.setUser(user);
userFile.setFilePath(path + new_file_name);
userFile.setFileSize(multipartFile.getSize());
userFile.setOrigFileName(multipartFile.getOriginalFilename());
userFile.setSavedName(new_file_name);
fileList.add(userFile);
multipartFile.transferTo(file);
file.setWritable(true);
file.setReadable(true);
}
return fileList;
}
return null;
}
};
- 유저 id로 디렉터리 생성하였다.
- 날짜 + 나노 초를 통해서 파일 이름의 중복 문제를 해결했다.
- file을 저장해주고 File객체로 변경한 다음 List로 리턴해주었다.
List를 리턴해주면 Service에서 board와 연결할 것이다!
'Bank-End > Spring Boot' 카테고리의 다른 글
[#1 Spring Boot 정주행] Spring & Spring Boot가 무엇인가? (0) | 2021.09.09 |
---|---|
[Spring Boot] Model 과 ModelAndView 차이. (1) | 2021.08.04 |
[Spring Boot] 인터셉터 ㅣ intercepter (0) | 2021.08.03 |
[Spring Boot] 쿠키, 세션 (0) | 2021.08.03 |
[Spring Boot + JPA] 로그인 구현하기. (0) | 2021.08.03 |