계속 진행하던 팀 프로젝트에서 DB 값들을 좀 둘러보다가 일정에 들어 있는 국가 대표 이미지 링크를 열어봤다. 근데 이미지 URL이 만료되어서 아래와 같이 접근이 불가해서 깨지는 현상이 발생했다.
우리 서비스의 프로필 사진을 azure storage에 저장하는데,
이 국가 대표 이미지도 MultipartFile로 변환해서 storage에 저장하는 것이 해결책일 것이라고 생각했다.
국가 대표 이미지가 외부 API(발급된 signed URL)에 의존하고 있었고,
일정 시간이 지나면 URL이 만료되면서 “이미지 깨짐”이 실제로 발생했다.
프로필/대표 이미지처럼 화면 곳곳에서 사용하는 리소스가 깨지면, 서비스 신뢰도와 사용 경험(UX)에 직접적인 악영향이 있을 것이다.
URL 만료 정책, 접근 권한 등 외부 시스템에 종속되면 서비스 코드와 상관없는 요인(만료 시간 변경, 키 회전 등)으로 장애가 발생할 수 있다.
이미지 파일을 우리 쪽 Azure Storage에 저장해두면, URL 만료/권한 정책을 우리가 통제할 수 있으므로 훨씬 안정적일 것이라고 판단했다.
프로필 사진을 저장하는데 필요했던 ImageService의 코드를 사용하기로 했다. 이미지 파일은 MultipartFile로 입력 받기 때문에 기존 upload 메서드를 사용하고자 Image url을 MultipartFile로 변환하기로 했다.
먼저 MultipartFile로 변환 될 수 있는 형태가 File, byte[]가 있어서 두 가지 변환 과정을 생각했다.
image url → File → MultipartFileimage 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