MinIO 기반 파일 업로드와 WebP 변환 적용 경험

MinIO 기반 파일 업로드와 WebP 변환 적용 경험

1. 적용 배경과 선택 이유

PROMs 프로젝트에서 환자/의료진이 업로드하는 이미지와 문진 이미지들을 안정적으로 저장할 필요가 있었습니다. 초기에는 단순 파일 저장만 생각했지만, 실제로는 파일 검증, 버킷 분리, 조회 URL 관리, 용량 최적화까지 함께 고려해야 했습니다. 이 과정에서 Object Storage로 MinIO를 사용하고, 이미지 업로드 시 저장 용량을 줄이기 위해 WebP로 변환하여 저장하는 방식을 적용하였습니다.

MinIO를 선택한 이유는 S3 API와 호환되어 Java 환경에서 연동이 비교적 단순했고, 파일을 Bucket 단위로 관리할 수 있어 이미지 성격에 따라 저장소를 분리하기 쉬웠기 때문입니다. 실제 프로젝트에서는 파일 원본은 MinIO에 저장하고, DB에는 fileId, fileNo, bucket, objectKey와 같은 메타데이터만 저장하는 구조로 구현하였습니다. 이를 통해 실제 파일 저장 책임과 비즈니스 데이터를 분리하고, 조회 시에는 DB의 메타데이터를 기반으로 MinIO 파일에 접근하도록 구성하였습니다.

MinIO 용어 설명 PROMs에서의 의미
Bucket 파일을 저장하는 최상위 저장 공간 survey-template, clinical, notice, tenant, doc
Object 실제 저장되는 파일 단위 변환된 .webp 이미지 파일
Object Key Bucket 내부에서 파일을 식별하는 경로 문자열 uploads/uuid.webp
Object Path 코드에서 사용하는 Object Key 값 objectPath
Endpoint MinIO 서버 주소 내부 Object Storage 서버 주소

2. 실제로 사용한 라이브러리와 역할

라이브러리/클래스 사용 위치 역할
implementation 'io.minio:minio:8.5.10' 업로드
(DB 메타데이터 저장 실패 시 MinIO 객체 삭제 보상 처리)
/ 조회
MinioClient를 사용해 putObject, removeObject 등의
저장소 연동 처리
Spring MultipartFile 파일 입력 컨트롤러/서비스에서 업로드 파일 수신,
크기 및 타입 검증의 시작점
implementation 'org.sejda.imageio:webp-imageio:0.1.6' 이미지 변환 BufferedImage 로딩, 실제 이미지 여부 확인,
WebP 포맷으로 인코딩
WebP SPI 등록 클래스 애플리케이션 초기화 기본 ImageIO에서 WebP를 인식하지 못해
Reader/Writer SPI를 수동 등록
ProcessedImage (커스텀) 업로드 커스텀 객체 변환된 바이트, contentType, size를 하나의 객체로 묶어
후속 업로드 코드와 분리

3. 구현 시 실제로 필요했던 코드 포인트

  • MinioClient Bean 생성 및 버킷별 설정 분리
  • MultipartFile 유효성 검사: null, empty, size, MIME type, 확장자 위조 여부 확인
  • ImageIO.read(file.getInputStream())로 실제 이미지 여부 검사
  • BufferedImage를 WebP 바이트로 변환 후 contentType=image/webp 로 정리
  • DB에는 메타데이터 저장, MinIO에는 실제 파일 저장
  • DB 저장 실패 시 MinIO 업로드 파일을 삭제하는 보상 처리

가. WebP 인식을 위한 초기화 예시

@Configuration
public class ImageConfig {
@PostConstruct
public void registerWebpWriter() {
        IIORegistry registry = IIORegistry.getDefaultInstance();
        registry.registerServiceProvider(new WebPImageWriterSpi());
}
}

나. 업로드 전 변환 처리 예시

BufferedImage image = ImageIO.read(file.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "webp", baos);
byte[] bytes = baos.toByteArray();

return new ProcessedImage(
    bytes,
    file.getOriginalFilename(),
    "image/webp",
    bytes.length
);

4. 구현하면서 겪었던 문제와 정리한 기준

- 가장 먼저 부딪힌 부분은 Java 기본 ImageIO만으로는 WebP가 바로 처리되지 않는다는 점이었습니다. 단순히 ImageIO.write(image, "webp", ...)만 호출해서는 동작하지 않았고, WebP Reader/Writer SPI를 등록하는 초기화 클래스가 필요했습니다. 그래서 boot 모듈 config 파일 내부에 ImageConfig 클래스를 추가하였습니다.

- 병원 로고로 자주 사용될 것으로 예상되는 SVG 파일은 일반 이미지와 다르게 WebP 변환 과정에서 파일이 깨져 업로드에 실패하는 문제를 만났습니다. SVG 파일은 image.svg+xml인 벡터 이미지여서 따로 분기 처리를 하여 WebP로 변환하지 않고 원본 그대로 저장하는 방식으로 구현하였습니다.

- 운영 관점에서는 MinIO 업로드 성공 후 DB 저장이 실패하면 파일만 남는 문제가 생길 수 있었습니다. 이를 막기 위해 업로드 이후 예외 발생 시 해당 objectKey(실제 MinIO 파일 저장 경로)를 다시 삭제하는 보상 처리 코드를 두었습니다. 이 부분이 실제 프로젝트에서는 단순 업로드 코드보다 더 중요하다고 느꼈습니다.

Wade