26/08/07 IL - (1)-1 MultipartFile을 활용한 파일 업로드 구조와 FileStore 설계

Let's take a break·2026년 8월 18일

MultipartFile을 활용한 파일 업로드 구조와 FileStore 설계

이번 프로젝트에서는 기존 영화 CRUD 프로젝트를 확장하여 이미지 파일 업로드 기능을 구현하였다. 단순히 영화 제목과 가격 같은 텍스트 데이터만 처리하는 것이 아니라, 사용자가 여러 장의 이미지를 선택하여 업로드하고 이를 서버(Local) 또는 S3와 같은 외부 저장소에 저장하는 구조를 학습하였다.

특히 MultipartFile을 이용한 파일 처리, UploadFile을 활용한 파일 정보 관리, FileStore 인터페이스를 이용한 저장소 추상화, StorageProperty를 통한 설정 관리 등을 구현하면서 객체지향적인 파일 저장 구조를 이해할 수 있었다.


MultipartFile이란?

기존 프로젝트에서는 사용자가 입력한 값이 모두 문자열이나 숫자였다.

제목

가격

하지만 이번 프로젝트에서는 사용자가 파일도 함께 업로드한다.

제목

이미지1

이미지2

이미지3

Spring에서는 업로드된 파일을 MultipartFile 객체로 변환하여 전달한다.

즉,

브라우저

↓

파일 선택

↓

HTTP Multipart Request

↓

MultipartFile 객체 생성

↓

Controller

과정으로 동작한다.


MovieDTO를 이용한 입력 데이터 관리

사용자가 입력한 데이터는 MovieDTO에서 관리하였다.

public record MovieDTO(

        @NotBlank String title,

        @NotEmpty List<MultipartFile> images

) {
}

@NotBlank String title

영화 제목은 반드시 입력되어야 한다.

  • null 불가
  • 빈 문자열 불가
  • 공백만 입력 불가

Validation을 통해 잘못된 입력을 사전에 방지한다.

List<MultipartFile> images

파일 하나가 아니라

포스터

스틸컷1

스틸컷2

처럼 여러 장을 업로드할 수 있으므로

MultipartFile image;

가 아니라

List<MultipartFile> images;

를 사용하였다.


MultipartFile 안에는 무엇이 들어 있을까?

MultipartFile은 단순히 파일만 저장하는 객체가 아니다.

다음과 같은 정보를 포함한다.

메서드의미
getOriginalFilename()원본 파일 이름
getContentType()MIME 타입
getSize()파일 크기
getInputStream()파일 내용
transferTo()실제 파일 저장

UploadFile DTO

파일을 저장한 뒤에는 MultipartFile 자체를 계속 사용할 필요가 없다.

public record UploadFile(

        String originalName,

        String storedName

) {

    public String url() {

        return "/upload/%s".formatted(storedName);

    }

}

파일을 저장하면 두 가지 이름이 존재한다.

예를 들어 사용자가

cat.jpg

를 업로드하였다.

실제로 서버에는

2fa39fd8-8ab2.jpg

처럼 저장된다.

따라서

originalNamestoredName
cat.jpg2fa39fd8-8ab2.jpg

를 모두 저장해야 한다.


originalName을 저장하는 이유

사용자는

cat.jpg

를 보고 싶다.

다운로드 화면에서도

cat.jpg

라고 표시되어야 한다.


storedName을 저장하는 이유

서버에서는 파일명이 중복되면 안 된다.

예를 들어

두 사용자가 모두

cat.jpg

를 업로드하면 기존 파일이 덮어써질 수 있다.

그래서 UUID를 이용하여

cat.jpg

↓

7fd81e2d.jpg

처럼 저장한다.

이를 통해 파일명 충돌을 방지할 수 있다.


URL 생성 메서드

UploadFile에는

public String url() {
    return "/upload/%s".formatted(storedName);
}

도 존재한다.

이 메서드는 저장된 파일명을 이용하여 브라우저에서 접근 가능한 URL을 생성한다.

예를 들어 storedName이

abc123.jpg

이면

/upload/abc123.jpg

를 반환한다.

화면에서는 이 URL을 <img> 태그의 src 속성으로 사용하여 이미지를 출력한다.


FileStore 인터페이스

public interface FileStore {

    UploadFile storeFile(MultipartFile image);

}

MovieService는 파일을 어떻게 저장하는지 알 필요가 없다.

UploadFile upload = fileStore.storeFile(image);

만 호출한다.

실제로 저장하는 방법은

FileStore

LocalFileStore 또는 S3FileStore가 담당한다.

즉, MovieService는 인터페이스에만 의존한다.


LocalFileStore 구현

LocalFileStore는 프로젝트 내부 폴더에 파일을 저장한다.

@Component
public class LocalFileStore implements FileStore {

    private final Path uploadPath =
            Path.of("src/main/resources/static/upload");

}

업로드된 파일은

src

↓

main

↓

resources

↓

static

↓

upload

폴더에 저장된다.

브라우저는

/upload/파일명

으로 접근할 수 있다.


파일 저장 과정

LocalFileStore 내부에서는

다음 순서로 동작한다.

MultipartFile

↓

비어있는지 검사

↓

확장자 검사

↓

UUID 생성

↓

파일 저장

↓

UploadFile 반환

즉, MovieService는 파일이 어디에 저장되는지 전혀 알지 못한다.


StorageProperty를 이용한 설정 관리

S3 정보를 관리하기 위해 StorageProperty를 사용하였다.

@ConfigurationProperties(prefix = "app.storage")

@Validated

public record StorageProperty(

        String bucket,

        String endpoint,

        String region,

        String access_key,

        String secret_key

) {
}

기존에는

@Value(...)

를 여러 번 사용해야 했다.

하지만 ConfigurationProperties를 사용하면

application.yml

↓

app.storage

↓

StorageProperty

로 자동 매핑된다.

따라서 S3 관련 설정을 하나의 객체로 관리할 수 있다.


FileStore를 인터페이스로 만든 이유

현재는

LocalFileStore

를 사용할 수도 있고

S3FileStore

를 사용할 수도 있다.

MovieService는

private final FileStore fileStore;

만 알고 있다.

따라서

나중에

AWS S3

↓

Azure Blob

↓

Google Cloud Storage

등으로 변경하더라도 MovieService는 전혀 수정하지 않아도 된다.

이것이 다형성과 DIP(의존성 역전 원칙) 의 대표적인 예시이다.


학습한 내용

학습 내용세부 내용
MultipartFile업로드된 파일을 Java 객체로 처리하는 방법
MovieDTO여러 장의 이미지를 List<MultipartFile>로 관리하는 방법
UploadFile원본 파일명과 저장 파일명을 분리하여 관리하는 이유
UUID파일명 중복을 방지하기 위한 고유 식별자 생성
FileStore저장 방식을 추상화한 인터페이스
LocalFileStore프로젝트 내부에 파일을 저장하는 구현체
StorageProperty@ConfigurationProperties를 활용한 설정 관리
DIP인터페이스에 의존하여 저장소 변경에 유연하게 대응하는 구조

0개의 댓글