메뉴 등록 시 사진 등록이 필요하게 되었다.
apache의 commons-io 설치
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
마지막 업데이트가 2년 이상 되긴 했는데 문제 없이 작동한다.
Controller
@PostMapping(value = "/menu/upload")
public Map<String, Object> upload(@RequestParam("file") MultipartFile multipartFile) {
File targetFile = new File("src/main/resources/static/imgs/" + multipartFile.getOriginalFilename());
try {
InputStream fileStream = multipartFile.getInputStream();
FileUtils.copyInputStreamToFile(fileStream, targetFile);
} catch (IOException e) {
FileUtils.deleteQuietly(targetFile);
e.printStackTrace();
}
Map<String, Object> m = new HashMap<>();
m.put("errorCode", 10);
return m;
}
자바는 File 클래스를 통해서 파일과 디렉터리를 다룬다. 그래서 File 인스턴스는 파일 일 수도 있고 디렉터리 일 수도 있다. File 인스턴스를 생성했다고 해서 파일이나 디렉터리가 바로 생성되는 것은 아니고, 출력스트림을 생성하거나 createNewFile()을 호출해야 한다.
자바에서의 `InputStream`은 abstract class로, BLOB(Binary Large OBject)과 CLOB(Character Large OBject) 모두 다룰 수 있다.
InputStream
This abstract class is the superclass of all classes representing an input stream of bytes.
Applications that need to define a subclass of InputStream must always provide a method that returns the next byte of input.
파일을 말그대로 흘러가면서 읽기 위한, c언어에서의 fp같은 느낌인 것 같다. 이 추상클래스를 구현하는 서브클래스들은 그 다음 바이트를 뱉는 메소드를 꼭 포함해야 한다고 한다. 내생각엔 read()
메소드가 그 예인 것 같다.
MultipartFile
A representation of an uploaded file received in a multipart request.
The file contents are either stored in memory or temporarily on disk. In either case, the user is responsible for copying file contents to a session-level or persistent store as and if desired. The temporary storage will be cleared at the end of request processing.
MultipartFile은 Handler의 매개변수 중 하나로, 사용자가 업로드한 파일을 핸들러에서 쉽게 다룰 수 있도록 도와준다. 이 매개변수를 사용하기 위해 MultipartResolver Bean이 등록되어 있어야 하는데, SpringMVC와는 달리 부트에서는 AutoConfigure 해준다고 한다. 이 파일은 당연하겠지만 메모리나 디스크에 잠시 저장되었다가 요청 프로세스가 끝날때 같이 사라진다. 위의 코드에서 이 MultipartFile을 이용해 파일스트림을 받아왔는데, multipartFile.getInputStream()
의 .getClass()
를 호출해보면 java.io.FileInputStream
라는 것을 알 수 있다.
MultipartFile을 받아와서 그 FileInputStream을 얻고, commons-io의 라이브러리인 FileUtils의 FileUtils.copyInputStreamToFile(fileStream, targetFile);
를 통해 빈 targetFile에 이 스트림을 복사해주었다.
try,catch문을 보면 실패할 시 이 FileUtils.deleteQuietly(targetFile);
를 호출한다.
FileUtils.deleteQuietly(File)
Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
The difference between File.delete() and this method are:
A directory to be deleted does not have to be empty.
No exceptions are thrown when a file or directory cannot be deleted.
그게 폴더라 주의를 필요하든 말든간에 무조건 조용히 지워주는 메소드이다. 아마 모종의 이유로 IO 에러가 났을 때 쓰다말았다거나 등등 roll back 하기 위해 써주면 좋을 것 같다.
이제 이 targetFile은 실제 InputStream을 가지게 되면서 파일을 생성한다.
클라이언트(React) 코드
import React, { Component } from 'react';
import axios, { post } from 'axios';
const URL = 'http://localhost:8080/auth/login';
class Login extends Component {
constructor(props) {
super(props);
this.state = {
file: null
}
}
fileUpload(file) {
const url = 'http://localhost:8080/menu/upload';
const formData = new FormData();
formData.append('file', file)
const config = {
headers: {
'content-type': 'multipart/form-data'
}
}
return post(url, formData, config)
}
upload = (e) => {
e.preventDefault();
this.fileUpload(this.state.file).then((response) => {
console.log(response.data);
})
}
fileChange = (e) => {
this.setState({ file: e.target.files[0] })
}
render() {
return (
< div >
<h1>파일 업로드</h1>
<form onSubmit={this.upload}>
<h1>File Upload</h1>
<input type="file" onChange={this.fileChange} name="file" />
<button type="submit">Upload</button>
</form>
</div>
)
}
}
export default Login;
다른 방법도 있겠지만 그냥 이 서버의, 이 프로젝트의 resource 폴더에 정적으로 저장하고 또 접근하려 한다.
따로 찾아보고 구현한건 아니라서 틀린 방법일 수도 있지만, 그냥 메뉴 업로드 요청을 받아서 메뉴 객체의 새로운 인스턴스를 만드는 순간 랜덤한 uuid를 생성해 이 uuid로 저장했다. 그리고 MySQL에 이 uuid를 저장해 접근할 수 있게 했다.
ip:port/menuimgs/~로 접근했을 때 바로 클래스패스의 menuimgs폴더로 접근하기위해 config 파일의 `addResourceHandler`에 추가해준다.
Config 파일
package com.lastpang.server.Config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/menuimgs/**")
.addResourceLocations("classpath:/menuimgs/");
}
}
Controller
@PostMapping(value = "/menu/upload")
public Map<String, Object> upload(@RequestParam("storename") String storename, @RequestParam("file") MultipartFile multipartFile,
@RequestParam("menuname") String menuname, @RequestParam(value = "price", required = false) Integer price,
@RequestParam(value="desc", required = false) String desc, @RequestParam(value = "options", required = false) String options) {
UUID uid = UUID.randomUUID();
File targetFile = new File("src/main/resources/menuimgs/"+uid.toString());
Menu menu = new Menu();
try {
System.out.println( multipartFile.getInputStream().getClass());
InputStream fileStream = multipartFile.getInputStream();
FileUtils.copyInputStreamToFile(fileStream, targetFile);
String filename = multipartFile.getOriginalFilename();
menu.setMenuImgUuid(uid.toString());
menu.setPrice(price);
menu.setDescription(desc);
menu.setStore(storeRepository.findStoreByStoreName(storename));
menu.setOptions(options);
menu.setMenuName(menuname);
menuRepository.save(menu);
} catch (IOException e) {
FileUtils.deleteQuietly(targetFile);
e.printStackTrace();
}
Map<String, Object> m = new HashMap<>();
m.put("errorCode", 10);
return m;
}
윽 더럽다.. 어떻게 잘하면 Menu 객체에 바로 캐스팅하면서 한번에 처리할 수도 있겠지만 Multipart 때문에 바로 처리할 수는 없다. 예쁘게 파싱해서 쓰거나 할수 있겠지만 일단 급해서 더럽게 구현만..
이제 http://ip:port/menuimgs/0e6de525-f54b-441a-a29d-7c43e1bd0e38 이런 주소로 정적 리소스인 메뉴 이미지에 접근할 수 있다. EC2에 올려서 쓸거라 이 링크 자체를 db에 저장해도 되겠지만 혹시나 ip나 port가 바뀔 수도 있으니 그런건 클라이언트에서 만들어도 무방할 것 같다.사실 uuid를 랜덤 말고 시간+메뉴라던지 더 유니크하게 만든 뒤 해싱하는게 나을 것 같긴 하지만..ㅎㅎ.
다른데서는 어떻게하는지 궁금한데 아마존 S3를 썼을때는 timestamp스럽게 생긴 156785736686 이런 숫자들을 봤던것도 같다. 나중에 다른 좋은 예시를 찾아봐야겠다
참고자료
https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html?is-external=true
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html
https://devbox.tistory.com/entry/Java-File-클래스 [장인개발자를 꿈꾸는 :: 기록하는 공간]
https://galid1.tistory.com/564