RustFS 실습 - 2 단일 업로드 / 멀티파트 업로드

TopOfTheHead·2026년 7월 25일

AWS SDK for S3의 업로드 관련 클래스
。S3 관련 기능을 정의한 클래스
▶ RustFS는 S3 API를 그대로 사용하므로, 호환 가능

단일 업로드

  • PutObjectRequest
    。S3에 실제 업로드를 수행할 파일의 정보를 정의
    ▶ 버킷명 / 파일명( = key ) / Content-Type 등을 정의
PutObjectRequest objectRequest = PutObjectRequest.builder()
        .bucket("my-bucket")
        .key("images/profile.png")
  		.contentType("video/mp4")
		.metadata(Map.of(
            "userId", "123",
            "project", "asset-box"
		))
        .build();
my-bucket
 └── images
      └── profile.png

bucket : 업로드할 S3 버킷을 지정
key : 저장될 객체의 경로 및 이름을 정의하여 저장될 파일명 지정
contentType : 파일의 MIME 타입( image/png, image/jpeg, video/mp4 등 )

  • PutObjectPresignRequest
    。정의한 PutObjectRequest 객체를 기반으로 S3 Presigned URL을 생성하기 위한 요청 객체
    ▶ S3에 파일을 업로드할 수 있는 PresignedURL을 생성하기위한 요청을 포함하는 객체
PutObjectPresignRequest presignRequest =
    PutObjectPresignRequest.builder()
        .signatureDuration(Duration.ofMinutes(10))
        .putObjectRequest(objectRequest)
        .build();

signatureDuration : Presigned URL의 유효시간
▶ 유효시간이 지나면 403 Forbidden 발생

  • PresignedPutObjectRequest
    。S3Presigner에 의해 생성된 PresignedURL과 관련 정보를 담는 결과 객체
    ▶ PresignedPutObjectRequest객체.url().toString();를 통해 PresignedURL을 추출

  • S3Presigner
    。업로드를 위한 S3 Presigned URL을 생성하기 위한 객체
    ▶ AWS 자격증명으로 서명을 수행한 Presigned URL을 생성하는 역할을 수행

    。S3은 기본적으로 인증 사용자만 접근가능하므로, 브라우저에서 바로 업로드 시도 시 AWS Access Key가 없으므로, 403이 도출
    ▶ AWS Access Key를 가진 백엔드 서버가 대신 서명을 생성하여 적용한 URL을 제공하여 업로드를 수행

    。AWS Region / AWS Access Key / AWS Secret Key를 정의하여 S3 Presigner의 Spring Bean을 정의
    ▶ 그러나, 본 프로젝트에서는 RustFS를 사용하므로 , RustFS에 대한 설정을 정의
S3Presigner presigner = S3Presigner.builder()
        .region(Region.AP_NORTHEAST_2)
        .credentialsProvider(credentialsProvider)
        .build();
  • S3Presigner객체.presignPutObject(PutObjectPresignRequest객체)
    。S3에 PresignedURL을 요청하는 메서드
PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
String presignedUrl = presignedRequest.url().toString(); // PresignedURL 추출

▶ 파일 정보를 포함하는 PutObjectPresignRequest 객체를 포함하여 Object Storage에서 PresignedURL을 요청 후 PresignedPutObjectRequest 객체로 반환

  • S3Presigner객체.presignGetObject(GetObjectPresignRequest객체)
    。GetObjectPresignRequest에 서명을 추가해서 RustFS에 전달함으로써 다운로드 URL을 정의하는 PresignedURL을 생성하여 PresignedGetObjectRequest객체를 반환하는 메서드

멀티파트 업로드

  • CreateMultipartUploadRequest
    。S3에서 멀티파트 업로드를 시작하기 위한 요청 객체
    ▶ 해당 파일에 대해 멀티파트 업로드를 시작한다는 정보를 전달하는 객체

    。s3Client객체.createMultipartUpload()를 통해 S3에 전달 시 uploadId를 생성하여 반환
CreateMultipartUploadRequest request =
    CreateMultipartUploadRequest.builder()
        .bucket("my-bucket")
        .key("videos/movie.mp4")
        .contentType("video/mp4")
        .build();
my-bucket
 └── videos
      └── movie.mp4
  • CompletedPart
    。멀티파트 업로드를 수행하는 경우 각 Part의 업로드 결과 ( = ETag )을 저장하는 객체
    ▶ 파트 번호와 ETag을 포함

    。클라이언트에서 파트 별 Presigned URL에 각 파트를 업로드하면 S3은 응답 헤더에 ETag을 전달
CompletedPart part1 =
    CompletedPart.builder()
        .partNumber(1) // 
        .eTag("ab123")
        .build();
  • CompletedMultipartUpload
    。모든 CompletedPart를 하나로 묶는 Part 목록을 담당하는 객체
CompletedMultipartUpload upload =
    CompletedMultipartUpload.builder()
        .parts(
            part1,
            part2,
            part3
        )
        .build();
  • CompleteMultipartUploadRequest
    。멀티파트 업로드를 수행 중인 uploadId에 대해 실제로 완료했다고 RustFS에 요청하는 객체
CompleteMultipartUploadRequest request =
    CompleteMultipartUploadRequest.builder()
        .bucket(bucket) // 버킷명
        .key(path) // 경로를 포함한 파일명
        .uploadId(uploadId) // uploadId
        .multipartUpload(upload) // CompletedMultipartUpload
        .build();

▶ 버킷명 / 파일명 / uploadId / CompletedMultipartUpload를 포함

。해당 객체를 S3Client.completeMultipartUpload(completeRequest)의 인자로 포함하여 RustFS로 전달

  • S3Client
    。Spring 서버에서 S3 API를 사용하여 파일의 업로드 / 다운로드 / 삭제하는 기능을 수행하는 객체
    • S3Client객체.createMultipartUpload(CreateMultipartUploadRequest객체)
      。S3에 멀티파트 업로드를 시작하도록 요청하는 메서드
      ▶ S3은 업로드를 식별하는 UploadId 등이 포함된 CreateMultipartUploadResponse 객체를 반환

    • S3Client객체.completeMultipartUpload(CompleteMultipartUploadRequest객체)
      。S3에서 멀티파트 업로드를 통해 업로드 된 Part들을 단일 파일로 병합하도록 RustFS에게 요청하는 API
      ▶ 호출 시점에서 Part1 , Part2 , ...를 파일.mp4로 병합

단일 / 멀티파트 업로드 구현

  • RustFS 도커 이미지 실행
    RUSTFS

  • S3에 대한 의존성 정의
    。AWS SDK S3에 대한 의존성 정의
implementation 'software.amazon.awssdk:s3:2.29.26'
  • application.yml / .env 정의
spring:
  application:
    name: springboot-sns-sample
  config:
    import: optional:file:.env[.properties]
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
rustfs:
  endpoint: ${RUSTFS_ENDPOINT}
  access-key: ${RUSTFS_ACCESS_KEY}
  secret-key: ${RUSTFS_SECRET_KEY}
  bucket: ${RUSTFS_BUCKET_NAME}
  region: ${RUSTFS_REGION}
  presigned-url-expiration-seconds: ${PRESIGNEDURL_EXPIRATION_TIME_SEC}
#PostgreSQL
DB_URL=jdbc:postgresql://localhost:5432/snsdb
DB_USERNAME=snsuser
DB_PASSWORD=snspassword
# RUSTFS
RUSTFS_ENDPOINT=http://localhost:9000
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=1234567
RUSTFS_BUCKET_NAME=media
RUSTFS_REGION=ap-northeast-2
PRESIGNEDURL_EXPIRATION_TIME_SEC=3600

▶ PresignedUrl의 유효시간 : 1시간 지정

  • RustFS에서 AWS S3을 사용하도록 마이그레이션하는 방법
    。단순하게 application.yml의 설정을 RustFS에서 AWS S3으로 전환

    rustfs:
      endpoint: AWS S3 주소
      access-key: AWS S3 Access Key
      secret-key: AWS S3 Secret Key


  • 9001 포트에 진입하여 RustFS 웹콘솔 진입 및 버킷 생성
    。.env파일에서 지정한 버킷명에 해당하는 버킷을 생성


    ▶ 해당 버킷으로 파일의 업로드가 수행됨.

  • RustFS 관련 Properties 클래스 정의
    。사전에 진입점 클래스에 @ConfigurationPropertiesScan이 선언되어야한다.

    。application.yml 파일에서 설정된 버킷명( rustfs: bucket: ${RUSTFS_BUCKET_NAME} )의 버킷으로 업로드 수행
    ▶ 사전에 해당 버킷이 정의되어있어야한다.
@ConfigurationProperties(prefix = "rustfs")
public record RustFsProperties(
        String endpoint,
        String accessKey,
        String secretKey,
        String bucket,
        String region,
        Long presignedUrlExpirationSeconds
) {
}
  • S3의 Configuration Class 정의
    。RustFS는 S3 API를 그대로 사용하므로, 호환 가능하며, RustFS와 관련된 설정을 수행
@Configuration
@RequiredArgsConstructor
public class S3Config {
    private final RustFsProperties rustFsProperties;
    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .endpointOverride(URI.create(rustFsProperties.endpoint()))
                .region(Region.of(rustFsProperties.region()))
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(
                                rustFsProperties.accessKey(),
                                rustFsProperties.secretKey()
                        )
                ))
                .forcePathStyle(true)
                .build();
    }
    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.builder()
                .endpointOverride(URI.create(rustFsProperties.endpoint()))
                .region(Region.of(rustFsProperties.region()))
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(
                                rustFsProperties.accessKey(),
                                rustFsProperties.secretKey()
                        )
                ))
                .serviceConfiguration(S3Configuration.builder()
                        .pathStyleAccessEnabled(true)
                        .build())
                .build();
    }
}

DTO 정의

  • 단일 업로드 시 Init POST API로 요청할 DTO
public record MediaInitRequest(
        MediaType mediaType,
        Long fileSize
) {
}
  • Presigned URL을 포함할 DTO
public record PresignedUrl(
        Media media,
        String presignedUrl,
        String uploadId,
        List<PresignedUrlPart> presignedUrlParts
) {
    public static PresignedUrl forSingleUpload(
            Media media, 
            String presignedUrl
    ) {
        return new PresignedUrl(media, presignedUrl, null, null);
    }
    public static PresignedUrl forMultipartUpload(
            Media media, 
            String uploadId, 
            List<PresignedUrlPart> presignedUrlParts
    ) {
        return new PresignedUrl(media, null, uploadId, presignedUrlParts);
    }
}
  • 멀티파트 업로드를 수행 시 각 파트 별 PresignedUrl을 포함할 DTO
public record PresignedUrlPart(
        int partNumber,
        String presignedUrl
) {
}
  • 파일 업로드 후 응답값을 포함하는 DTO
public record MediaInitResponse(
        Long id,
        MediaType mediaType,
        String path,
        MediaStatus status,
        Long userId,
        String presignedUrl,
        String uploadId,
        List<PresignedUrlPart> presignedUrlParts,
        LocalDateTime createdAt,
        LocalDateTime modifiedAt
) {
    public static MediaInitResponse from(PresignedUrl presignedUrl) {
        Media media = presignedUrl.media();
        return new MediaInitResponse(
                media.getId(),
                media.getMediaType(),
                media.getPath(),
                media.getStatus(),
                media.getUserId(),
                presignedUrl.presignedUrl(),
                presignedUrl.uploadId(),
                presignedUrl.presignedUrlParts(),
                media.getCreatedAt(),
                media.getModifiedAt()
        );
    }
}

MediaRepository 정의

public interface MediaRepository extends JpaRepository<Media, Long> {
    Optional<Media> findByIdAndDeletedAtIsNull(Long id);
    default Media findByIdAndDeletedAtIsNullOrThrow(Long id){
        return findByIdAndDeletedAtIsNull(id)
                .orElseThrow(() -> new IllegalArgumentException("Media not found: " + id));
    }
    List<Media> findByUserIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long userId);
    @Query(value = """
            SELECT * 
                FROM media 
                WHERE attributes -> 'parts' @> :partNumberJson\\:\\:jsonb 
                AND deleted_at IS NULL 
                ORDER BY created_at DESC
            """, nativeQuery = true)
    List<Media> findByPartNumber(@Param("partNumberJson") String partNumberJson);
}

INIT POST API 정의

。Presigned URL 요청

Presigned URL 요청 - MediaService 정의

@Service
@RequiredArgsConstructor
public class MediaService {
    private static final long MULTIPART_THRESHOLD = 8 * 1024 * 1024; // 8MB = 8,388,608
	//
    private final MediaRepository mediaRepository;
    private final MultipartService multipartService;
    private final S3Presigner s3Presigner;
    private final RustFsProperties rustFsProperties;
	// ...
}
  • initMedia()
    。파일크기에 따라서 단일 / 멀티파트 업로드를 결정하여 업로드를 시작하고, 업로드를 수행하는 PresignedURL을 반환하는 메서드
    ▶ RustFS의 Bucket에 저장할 Path를 정의
    ex ) RustFS 상 users/1/posts/UUID.jpg로 저장
    private static final long MULTIPART_THRESHOLD = 8 * 1024 * 1024; // 8MB = 8,388,608
    // 파일 업로드를 수행하는 메서드
    public PresignedUrl initMedia(
            MediaType mediaType,
            Long fileSize,
            User user,
            String subPath
    ){
        // 해당 MediaType의 확장자를 반환
        // MediaType : IMAGE인 경우 filename : UUID.jpg
        String filename = UUID.randomUUID() + mediaType.fileExtension();
        // Object Storage 상 미디어파일 저장경로 생성
        // ex ) RustFS 상 users/1/posts/UUID.jpg로 저장
        String path = "users/%s/%s/%s".formatted(
                user.getId(),
                subPath,
                filename
        );
        // 파일크기에 따라서 단일업로드 / 멀티파트 업로드 결정
        if (fileSize != null
        && fileSize > MULTIPART_THRESHOLD)
            return initMultipartUpload(user, path, mediaType, fileSize);
        return initSingleUpload(user,path,mediaType,fileSize);
    }
	// ...
  • initSingleUpload()
    。단일 업로드를 수행하기위해 RustFS에 Presigned Url을 요청하는 메서드
    ▶ initMedia()으로부터 8MB 이하인 파일에 대해 RustFS의 버킷 정보 및 initMedia()에서 정의한 Path 정보와 파일 MediaType 정보를 활용하여 Presigned URL을 요청

    。application.yml 파일에서 설정된 버킷명( rustfs: bucket: ${RUSTFS_BUCKET_NAME} )의 버킷으로 업로드 수행
    ▶ 사전에 해당 버킷이 정의되어있어야한다

    。PresignedUrl을 수신한 경우 INIT 상태의 Media 객체 생성 및 Entity로서 저장
private PresignedUrl initSingleUpload(
            User user,
            String path,
            MediaType mediaType,
            Long fileSize
    ) {
        // RustFS의 버킷정보 및 initMedia()에서 정의한 Path 정보와 MediaType 정보를 전달
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(rustFsProperties.bucket()) // RustFS에 저장할 버킷명
                .key(path) // 경로 및 이름을 정의한 저장될 파일명
                .contentType(mediaType.contentType()) // 파일의 MIME 타입
                .build();
        // RustFS에 전달할 PresignedURL에 대한 요청을 생성하기 위해 PutObjectRequest 객체 생성
        // PresignedUrl의 유효기간에 관한 설정도 추가
        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofSeconds(rustFsProperties.presignedUrlExpirationSeconds()))
                .putObjectRequest(putObjectRequest)
                .build();
        // S3Presigner 객체를 통해 RustFS에 요청을 전달하여 PresignedUrl를 수신
        PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
        String presignedUrl = presignedRequest.url().toString();
        // INIT 상태의 Media 객체 생성
        Media media = mediaRepository.save(
                new Media(
                        mediaType,
                        path,
                        user.getId(),
                        fileSize
                )
        );
        // PresinedUrl을 Client에게 반환
        return PresignedUrl.forSingleUpload(media, presignedUrl);
    }

bucket : 업로드할 S3 버킷을 지정
key : 저장될 객체의 경로 및 이름을 정의하여 저장될 파일명 지정
contentType : 파일의 MIME 타입( image/png, image/jpeg, video/mp4 등 )

  • initMultiPartUpload()
    。멀티파트 업로드를 수행하기위해 RustFS에 Presigned Url을 요청하는 메서드
    ▶ 임계값 이상의 파일에 대해 파트별로 업로드를 수행하므로 복수의 Presigned URL을 요청
private PresignedUrl initMultipartUpload(User user, String path, MediaType mediaType, long fileSize) {
        // 멀티파트 업로드를 수행하기 위해 RustFS에 요청을 전달하여 복수의 PresignedUrl를 수신
        MultipartUploadInfo uploadInfo = multipartService.initMultipartUpload(
                path,
                mediaType.contentType(),
                fileSize
        );
        // INIT 상태의 Media 객체 생성
        Media media = mediaRepository.save(
                new Media(
                        mediaType,
                        path,
                        user.getId(),
                        fileSize,
                        uploadInfo.uploadId()
                )
        );
        return PresignedUrl.forMultipartUpload(media, uploadInfo.uploadId(), uploadInfo.presignedUrlParts());
    }

Presigned URL 요청 - MultipartService
。멀티파트 업로드에 대한 기능을 정의

@Service
@RequiredArgsConstructor
public class MultipartService {
    private static final long PART_SIZE = 8 * 1024 * 1024; // 8MB
    private final S3Client s3Client;
    private final S3Presigner s3Presigner;
    private final RustFsProperties rustFsProperties;
	// ...
}
  • initMultipartUpload()
    。멀티파트 업로드를 수행하기위해 RustFS에 복수의 Presigned URL을 요청하는 메서드

    。업로드 시작 전 createMultipartUpload()를 호출하여 RustFS에 멀티파트 업로드를 시작한다고 알림 및 uploadId를 수신

    。ceil(파일크기 / 임계값)을 계산하여 생성할 각 파트 별 Presigned URL 수를 정의
    ex ) 35MB 파일의 경우 ceil(35 / 8) = 5개
    ▶ 정의한 수 만큼 각 파트 별 PresignedURL을 생성 후 반환
	public MultipartUploadInfo initMultipartUpload(String path, String contentType, long fileSize) {
        // 멀티파트 업로드를 위한 요청객체 생성
        CreateMultipartUploadRequest createRequest = CreateMultipartUploadRequest.builder()
                .bucket(rustFsProperties.bucket()) // RustFS에 저장할 버킷명
                .key(path) // 경로 및 이름을 정의한 저장될 파일명
                .contentType(contentType) // 파일의 MIME 타입
                .build();
        // S3에 멀티파트 업로드를 시작하도록 요청 전달 및 uploadId 수신
        CreateMultipartUploadResponse createResponse = s3Client.createMultipartUpload(createRequest);
        String uploadId = createResponse.uploadId();
        // 생성할 PresignedUrl의 수를 계산
        // 35MB 파일의 경우 ceil(35 / 8) = 5개
        int numberOfParts = (int) Math.ceil((double) fileSize / PART_SIZE);
        // 정의한 수 만큼 각 파트별 PresignedURL 생성 및 해당 리스트에 저장
        List<PresignedUrlPart> presignedUrlParts = new ArrayList<>();
        for (int partNumber = 1; partNumber <= numberOfParts; partNumber++) {
            UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
                    .bucket(rustFsProperties.bucket())
                    .key(path)
                    .uploadId(uploadId)
                    .partNumber(partNumber)
                    .build();
            //
            UploadPartPresignRequest presignRequest = UploadPartPresignRequest.builder()
                    .signatureDuration(Duration.ofSeconds(rustFsProperties.presignedUrlExpirationSeconds()))
                    .uploadPartRequest(uploadPartRequest)
                    .build();
            //
            PresignedUploadPartRequest presignedRequest = s3Presigner.presignUploadPart(presignRequest);
            String presignedUrl = presignedRequest.url().toString();
            //
            presignedUrlParts.add(new PresignedUrlPart(partNumber, presignedUrl));
        }
        return new MultipartUploadInfo(uploadId, presignedUrlParts);
    }

Presigned URL 요청 - MediaController 정의

@RestController
@RequiredArgsConstructor
public class MediaController {
    private final MediaService mediaService;
  	// ...
}
  • PresignedURL 생성 컨트롤러
    。단일 / 멀티파트 업로드를 위한 Presigned URL을 반환
	@PostMapping("/api/v1/media/init")
    public MediaInitResponse initMedia(
            @RequestBody MediaInitRequest request
    ) {
        PresignedUrl result = mediaService.initMedia(
                request.mediaType(),
                request.fileSize(),
                User.create(
                        "kf16",
                        "wjd747"
                ).setId(1L),
                "posts"
        );
        return MediaInitResponse.from(result);
    }




▶ 단일 / 멀티파트 업로드 여부에 따라서 PresignedURL이 포함되며, 응답값이 서로 다름을 확인 가능

UPLOADED POST API 정의

。클라이언트에서 업로드 완료를 알리는 API를 정의

DTO 정의

  • MediaUploadedRequest
    。멀티파트 업로드 완료를 위해 클라이언트에서 해당 Media 객체 ID와 함께 업로드 완료된 Parts( = partNumber, ETag )들을 전달하는 DTO
public record MediaUploadedRequest(
        Long mediaId,
        List<MultipartUploaded> parts
) {
}
  • MultipartUploaded
    。멀티파트 업로드 시 각 파트의 PresignedURL과 ETAG을 포함하는 DTO
public record MultipartUploaded(
        int partNumber,
        String eTag
) {
}
  • MediaResponse
    。업로드 완료 된 미디어 파일의 메타데이터를 포함하는 DTO
public record MediaResponse(
        Long id,
        MediaType mediaType,
        String path,
        MediaStatus status,
        Long userId,
        Long fileSize,
        Map<String, Object> attributes,
        LocalDateTime createdAt,
        LocalDateTime modifiedAt
) {
    public static MediaResponse from(Media media) {
        return new MediaResponse(
                media.getId(),
                media.getMediaType(),
                media.getPath(),
                media.getStatus(),
                media.getUserId(),
                media.getFileSize(),
                media.getAttributes(),
                media.getCreatedAt(),
                media.getModifiedAt()
        );
    }
}

업로드 완료 요청 - MediaSerivce 정의

  • mediaUploaded()
    。INIT 상태의 미디어 객체를 UPLOADED 상태로 전환하여 업로드 완료를 수행하는 메서드

    。멀티파트 업로드인 경우 completeMultipartUpload()를 호출하여 RustFS에 업로드 완료를 알림
    ▶ RustFS에서 이를 수신 시 RustFS에서 백엔드로부터 API 수신 시 업로드된 파일을 병합 시작
public Media mediaUploaded(
            Long mediaId, 
            List<MultipartUploaded> parts, 
            User user
    ) {
        Media media = mediaRepository.findByIdAndDeletedAtIsNullOrThrow(mediaId);
        // Media가 본인 소유가 아니면 다운로드 차단
        if (!media.getUserId().equals(user.getId())) {
            throw new IllegalArgumentException("You are not authorized to update this media");
        }
        // Media가 INIT 상태 인 경우에만 허용
        if (media.getStatus() != MediaStatus.INIT) {
            throw new IllegalArgumentException("Media is not in INIT status");
        }
        // 멀티파트 업로드인 경우 S3에 작업완료를 지시 ( 단일 업로드 인 경우 생략 )
        if (media.getUploadId() != null && !CollectionUtils.isEmpty(parts)) {
            // completeMultipartUpload()를 호출하여 S3에 업로드 완료를 알림
            // S3에서 백엔드로부터 API 수신 시 업로드된 파일을 병합 시작
            multipartService.completeMultipartUpload(media.getPath(), media.getUploadId(), parts);
            Map<String, Object> attributes = new HashMap<>();
            attributes.put("parts", parts);
            media.updateAttributes(attributes);
        }
        // Media 객체 상태를 UPLOADED로 전환
        media.updateStatus(MediaStatus.UPLOADED);
        // Media 객체 저장
        return mediaRepository.save(media);
    }

업로드 완료 요청 - MultipartService
。클라이언트에서 멀티파트 업로드가 완료된 경우 RustFS에게 전달

  • completeMultipartUpload()
    。클라이언트에서 PresignedURL을 통해 모든 파트가 업로드 완료 시 RustFS에게 업로드된 파트를 하나의 파일로 병합하도록 요청하는 메서드
// 클라이언트에서 멀티파트 업로드가 완료된 경우 RustFS에
    public void completeMultipartUpload(String path, String uploadId, List<MultipartUploaded> parts) {
        // 각 파트의 파트번호와 ETag을 포함하는 CompletedPart를 생성
        List<CompletedPart> s3Parts = parts.stream()
                .map(part -> CompletedPart.builder()
                        .partNumber(part.partNumber())
                        .eTag(part.eTag())
                        .build())
                .toList();
        // CompletedPart를 하나로 묶는 CompletedMultipartUpload를 생성
        CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                .parts(s3Parts)
                .build();
        // 멀티파트 업로드 완료를 RustFS에게 알리는 CompleteMultipartUploadRequest 객체를 생성
        CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder()
                .bucket(rustFsProperties.bucket())
                .key(path)
                .uploadId(uploadId)
                .multipartUpload(completedMultipartUpload)
                .build();
        // 멀티파트 업로드 완료를 전달하는 API
        // 이후 RustFS는 업로드된 Part들을 하나의 파일로 병합
        s3Client.completeMultipartUpload(completeRequest);
    }
CompletedMultipartUpload
        │
        ▼
CompleteMultipartUploadRequest
        │
        ▼
s3Client.completeMultipartUpload(request)
        │
        ▼
RustFS가 모든 Part를 하나의 파일로 병합

업로드 완료 요청 - MediaController 정의

  • 업로드 완료 컨트롤러
    。멀티파트 업로드가 완료된 시점으로부터 클라이언트에게 해당 Media 객체 ID와 함께 업로드 완료된 Parts( = partNumber, ETag )들을 전달받아 업로드 완료 처리 수행
    @PostMapping("/api/v1/media/uploaded")
    public MediaResponse mediaUploaded(
            @RequestBody MediaUploadedRequest request
    ) {
        Media media = mediaService.mediaUploaded(
                request.mediaId(),
                request.parts(),
                User.create(
                        "kf16",
                        "wjd747"
                ).setId(1L)
        );
        return MediaResponse.from(media);
    }

업로드 테스트
。프론트엔드를 구축 후 업로드를 수행하는 경우 다음처럼 결과가 도출

profile
공부기록 블로그

0개의 댓글