계속 진행하던 팀 프로젝트에서 DB 값들을 좀 둘러보다가 일정에 들어 있는 국가 대표 이미지 링크를 열어봤다. 근데 내가 이미지 url 만료 기간이 있는지 모르고 DB에 값을 넣었었다... 우리 서비스의 프로필 사진을 azure storage에 저장하는데, 국가 대표 이미지도 MultipartFile로 변환해서 storage에 저장하기로 했다.
프로필 사진을 저장하는데 필요했던 ImageService의 코드를 사용하기로 했다. 이미지 파일은 MultipartFile로 입력 받기 때문에 기존 upload 메서드를 사용하고자 Image url을 MultipartFile로 변환하기로 했다.
먼저 MultipartFile로 변환 될 수 있는 형태가 File, byte[]가 있어서 두 가지 변환 과정을 생각했다.
image url
→ File
→ MultipartFile
image url
→ byte[]
→ MultipartFile
결과적으로는.. 실패한 방법이다. image url → File 변환은 가능하지만, File → MultipartFile 과정에서 사용되는 CommonsMultipartFile
가 2023년에 deprecated 됐다고 한다.
Upgrading to Spring Framework 6.x안돼서 한참 찾았는데
CommonsMultipartResolver
가 drop되면서 그와 관련된 CommonsMultipartFile
도 같이 drop됐다. StandardServletMultipartResolver
사용을 권장 한다는데.. 찾아봐도 File → MultipartFile 변환은 CommonsMultipartFile
을 사용하는 방법들이 많고, 최근에 변경된 사항이라 그런지 File → MultipartFile 변환 하는 자료를 찾기가 너무 어려워서 일단 보류했다.
image url을 byte 배열로 변환하기 위해선 URL의 입력 스트림을 열어 byte 배열로 이미지 데이터를 읽어오고, 출력 스트림에 이를 jpg 확장자로 쓰는 과정을 거친다.
InputStream
Java의 byte 기반 입력 소스를 추상화한 클래스로, 파일, 메모리 버퍼 등 다양한 입력으로부터 바이트 데이터를 읽어올 때 사용된다.
ByteArrayOutputStream
메모리 버퍼에 byte 배열을 저장하는 클래스이다. toByteArray()
메서드를 호출해 버퍼에 저장된 byte 배열을 반환할 수 있다.
BufferedImage
메모리에 로드 된 이미지 데이터를 나타내는 클래스이다.
ImageIO
이미지 파일을 읽고 쓰는 작업을 수행하는 클래스이다. read()
메서드로 InputStream, 파일 등으로부터 이미지 데이터를 읽어올 수 있고, write()
메서드로는 BufferedImage, OutputStream으로 출력할 수 있다.
URL url = new URL(imageUrl);
// image url의 input stream, byte 배열로 저장할 output stream 열기
try(InputStream inputStream = url.openStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// ImageIO.read()로 image url의 이미지 데이터 읽어오기
BufferedImage urlImage = ImageIO.read(inputStream);
// 메모리에 로드 된 이미지 데이터를 output stream에 jpg 확장자로 저장
ImageIO.write(urlImage, "jpg", bos);
// byte 배열로 변환
byte[] byteArray = bos.toByteArray();
}
InputStream과 OutputStream은 메모리를 사용하므로 다 사용하면 close가 필요하다. 따라서 InputStream
, ByteArrayOutputStream
은 try-with-resources문의 resource로 열어, 동작이 종료되면 자동으로 close 되도록 한다.
byte 배열을 MultipartFile
객체로 변환하기 위해서는 MultipartFile
인터페이스를 구현한 CustomMultipartFile
클래스가 필요하다.
CustomMultipartFile
클래스를 생성해 변환하고자 하는 데이터의 byte 배열을 input 필드로 추가한다. String filename로 이미지 파일 이름도 추가하고 구현을 위한 메서드들을 오버라이딩 한다.
public class CustomMultipartFile implements MultipartFile {
private byte[] input;
private String filename;
public CustomMultipartFile(byte[] input,String filename) {
this.input = input;
this.filename = filename;
}
@Override
public String getName() {
return null;
}
@Override
public String getOriginalFilename() {
return filename;
}
@Override
public String getContentType() {
return null;
}
@Override
public boolean isEmpty() {
return input == null || input.length == 0;
}
@Override
public long getSize() {
return input.length;
}
@Override
public byte[] getBytes() throws IOException {
return input;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(input);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
try(FileOutputStream fos = new FileOutputStream(dest)){
fos.write(input);
}
}
}
transferTo(File dest)
는 파일의 내용을 지정된 dest 파일에 쓰는 역할이다. byte 배열인 input을 dest 파일에 쓰는 작업을 한다.
이미지 데이터의 byte 배열과 저장할 이미지의 이름을 넘겨 CustomMultipartFile
객체를 생성하면 MultipartFile
변환 끝!
MultipartFile multipartFile = new CustomMultipartFile(byteArray, imageUrl);
private String convertUrlToMultipartFile(String imageUrl) throws IOException {
URL url = new URL(imageUrl);
try(InputStream inputStream = url.openStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// 1) image url -> byte[]
BufferedImage urlImage = ImageIO.read(inputStream);
ImageIO.write(urlImage, "jpg", bos);
byte[] byteArray = bos.toByteArray();
// 2) byte[] -> MultipartFile
MultipartFile multipartFile = new CustomMultipartFile(byteArray, imageUrl);
return imageService.uploadImageAndGetName(multipartFile); // image를 storage에 저장하는 메서드 호출
}
}
참고
[JAVA] 이미지 url을 byte array로 변환
Convert byte[] to MultipartFile in Java | Baeldung