본문 바로가기
IT CHANNEL/Infra

[Django/AWS S3] File Chunk Upload | File Multipart Upload로 대용량 파일 업로드 진행상황 확인하고, 효율적으로 업로드하기

by TitanX 2023. 9. 10.

 

File Chunk Upload란 파일을 분할로 업로드한다는 의미이다.

File Chunk Upload 의 개념을 AWS 에 적용한다면 File Multipart Upload 으로 사용할 수 있다.

비교적 용량이 큰 파일들을 업로드 해야하는 상황이 있다

대용량의 파일을 업로드 할 경우에는 클라이언트에서 업로드가 끝날 때까지 묵묵히 기다리는 수밖에는 없다

즉, 진행과정을 알 수 없었던 부분을 해결하기 위해 파일이 얼만큼 올라갔는지 사용자에게 보여주는 방법이 필요하다고 생각했다

 

 

File Chunk Upload를 왜 해야할까?

 

 

파일을 업로드하면서 단일 요청에 대한 값으로서 ‘그냥 업로드 요청이 잘 들어오고 잘 업로드하면 되는’ 수준으로 생각했는데, 용량이 커지면서 종종 뭔가 더 효율적인 방법이 있을 것 이라고 생각했다

파일을 업로드 하는 상황은 주로 아래와 같다

(큰 범주에서 다음과 같은 상황들에서 업로드가 이뤄진다)

  1. django admin 에서 파일을 업로드(File Field)
  2. API를 통해서 파일을 업로드(Form Data)
  3. (운영환경의 경우 AWS S3 를 쓰다보니 presigned url 을 사용하는게 더 효율적이다)

그러던 중 chunk upload, (S3에서는) multi-part upload 의 개념을 적용했다

chunk는 파일을 나눈 단위로 언급(?) 되는데

 

예를 들어 1GB 의 파일을 업로드할 때 chunk를 100MB 단위로 설정하면 총 10개의 chunk 가 업로드 되는 것 이다

 

File Chunk Upload 업로드 방식

 

 

파일의 관점에서 볼 때 스토리지에 저장하는 로직을 크게 아래 3가지로 정리 할 수 있다

 

 

 

  • File Upload 1 : django 내부의 media 루트에 저장
    • WAS를 스토리지로 쓰는 것은 제일 좋지 않은 방식 같다
  • File Upload 2: frontend -> django 를 통해서 AWS S3에 저장
    • 1번과 비교했을 때 frontend 입장에서 달라지는 것은 없다 하지만 backend는 S3로 미디어가 저장되도록 세팅 해야한다
  • (다음 글에서 설명)File Upload 3: (presigned_url을 이용해)frontend 에서 AWS S3에 저장

 

직관적으로 생각해도 파일을 업로드하는데 (django)서버를 거치지 않고 업로드 하는 것보다,

바로 스토리지로 업로드 하는 것이 더 효율적이다

 

 

각 관점에서 파일을 분할해서 업로드 해보려고 한다

 

 

File Chunk Upload : File Upload 1 → local chunk upload

model example

class FileUpload(BaseModel):
    file = models.FileField(upload_to="uploads/")
    file_name = models.CharField(max_length=255)
    uploaded_percentage = models.FloatField(default=0)
    status = models.CharField(max_length=50, default="uploading")

create example

# serializer (view 단 이어도 비슷하다)
def create(self, validated_data):
    uploaded_file = validated_data.get("file")
    return handle_uploaded_file(uploaded_file)

main funcition

def handle_uploaded_file(uploaded_file):
    chunk_size = 5 * 1024 * 1024 # 5mb 이다
    total_size = uploaded_file.size
    uploaded_size = 0

    unique_filename = uploaded_file.name
    file_upload, created = FileUpload.objects.get_or_create(file_name=unique_filename)

    # with open(file_upload.file.path, "wb") as destination:
    with open(
        f"경로!!!/uploads/{unique_filename}", "wb"
    ) as destination:
        for chunk in uploaded_file.chunks(chunk_size):
            destination.write(chunk)
            uploaded_size += len(chunk)

            # 업로드 진행률 계산 및 데이터베이스 업데이트
            uploaded_percentage = (uploaded_size / total_size) * 100
            file_upload.uploaded_percentage = uploaded_percentage
            file_upload.save()
            print(f"청크:{uploaded_percentage}%")
    file_upload.file = f"uploads/{unique_filename}"
    file_upload.status = "completed"
    file_upload.save()
    return file_upload

 

chunk를 5mb단위로 끊는다

즉 파일을 5mb단위로 나눠서 업로드 하겠다는 것이다

front end 입장에서 API polling으로 file_upload의 percentage를 받아서 쓴다면 업로드 진행상황을 추적가능해, needs는 충족이 된다

 

현재 로직의 단점

  • local app 에 저장이 된다
  • chunk 만큼 트랜잭션이 발생한다는 것.. 서비스가 커진다면 최악인 것이고, 서비스가 커지지 않아도 효율적인 것과 거리가 멀다
그래도 장점이라고 한다면 얼만큼 올라가는지에 대해서는 확실히 알 수 있다

 

 

File Chunk Upload : 2 → S3 multipart-upload(To real bucket in function)

 

 

 

Front로 파일이 업로드 될 때 S3에서 제공하는 multipart upload를 이용해 업로드 하는 방식이다

 

import math

import boto3
from rest_framework import serializers

from app.file_upload.models import FileUpload

class FileUploadSerializer(serializers.ModelSerializer):
    class Meta:
        model = FileUpload
        fields = [
            "id",
            "file",
            "file_name",
            "uploaded_percentage",
            "status",
        ]

    def validate(self, attrs):
        attrs = super().validate(attrs)
        return attrs

    def create(self, validated_data):

        file_obj = validated_data.get("file")
        bucket_name = "taktoon-dev-bucket"
        print("file_obj", file_obj)
        print("file_obj", type(file_obj))
        # S3 클라이언트 생성
        s3 = boto3.client("s3")
   
        try:
            # Multipart 업로드 시작
            mp = s3.create_multipart_upload(Bucket=bucket_name, Key=file_obj.name)
            upload_id = mp["UploadId"]

            part_info = {"Parts": []}

            chunk_size = 5 * 1024 * 1024  # 5MB
            file_size = file_obj.size
            chunk_count = int(math.ceil(file_size / float(chunk_size)))

            for i in range(chunk_count):
                part_num = i + 1

                # 파일의 각 청크를 읽고 업로드
                chunk = file_obj.read(chunk_size)
                part = s3.upload_part(
                    Bucket=bucket_name, Key=file_obj.name, UploadId=upload_id, PartNumber=part_num, Body=chunk
                )
                part_info["Parts"].append({"PartNumber": part_num, "ETag": part["ETag"]})

                # 진행상황 출력
                print(f"Part {part_num}/{chunk_count} uploaded, {(part_num / chunk_count) * 100}% completed")

            # Multipart 업로드 완료
            s3.complete_multipart_upload(
                Bucket=bucket_name, Key=file_obj.name, UploadId=upload_id, MultipartUpload=part_info
            )
        except Exception as e:
            # 에러 발생 시 Multipart 업로드 중단
            s3.abort_multipart_upload(Bucket=bucket_name, Key=file_obj.name, UploadId=upload_id)
            print(e)

        instance = super().create(validated_data)
        return instance

    def update(self, instance, validated_data):
        instance = super().update(instance, validated_data)
        return instance



S3멀티파트 업로드 개념

 

 

 

Multipart 업로드는 S3에서 제공하는 대용량 파일을 여러 부분으로 나누어 업로드하는 프로세스이다

  • 이 청크들은 일시적으로 S3 버킷에 저장되며, 아직 하나의 파일로 합쳐지지 않은 상태이다
  • 만약 업로드의 미완료 처리에 대하여
    • 미완료 청크들은 일시적으로 해당 S3 버킷에 저장된다 (일반적으로 사용자나 애플리케이션에서 접근할 수 없다)
    • multipart 업로드가 완료되지 않을 경우, 청크들은 일정 시간 후(보통 24시간 이내)에 S3에 의해 자동 삭제된다
    • 만약 abort_multipart_upload를 사용하여 미완료 업로드를 중단하고 미완료 청크를 명시적으로 삭제할 수 있다
  • 업로드 완료 (complete_multipart_upload)를 왜 호출할까?
    • 모든 청크가 업로드되면, complete_multipart_upload 호출이 되는데, S3에게 모든 청크를 하나의 파일로 결합하도록 지시하는 역할을 한다
    • 결합이 완료된 파일은 버킷에서 일반적으로 접근할 수 있는 객체로 완성이 된다

 

 

 

레퍼런스