[2024.10.25 TIL] FastAPI 고용량 파일 업로드 시 청크 사용하기

My_Code·2024년 10월 25일
0

FastAPI

목록 보기
1/1

💻 TIL(Today I Learned)

📌 Today I Done

✏️ 청크(Chucks)란?

  • 청크는 대용량 파일을 작고 관리하기 쉬운 조각으로 나눈 것입니다.

  • 일반적으로 1MB에서 5MB 사이의 크기로 나누지만, 네트워크 상태와 서버 설정에 따라 조절할 수 있습니다.

  • 각 청크는 고유 식별자를 가지며, 이를 통해 서버는 청크의 순서와 완전성을 확인합니다.


✏️ 왜 청크 단위로 쪼개서 업로드하는가?

  • 게이트웨이 제한 우회:

    • 대부분의 서버와 게이트웨이는 10MB 이상의 데이터 전송을 제한합니다.

    • 청크 업로드를 사용하면 이 제한을 우회하여 수백 MB 또는 GB 단위의 대용량 파일도 전송할 수 있습니다.

  • 네트워크 중단에 대한 복원력:

    • 업로드 중 연결이 끊어져도 이미 전송된 청크는 서버에 유지됩니다.

    • 연결이 복구되면 마지막으로 성공한 청크부터 업로드를 재개할 수 있습니다.

  • 리소스 최적화:

    • 서버와 클라이언트의 메모리 사용을 효율적으로 관리할 수 있습니다.
  • 병렬 처리 가능:

    • 여러 청크를 동시에 업로드하여 전체 속도를 향상시킬 수 있습니다.

✏️ 구현 과정

  • 프론트엔드:

    • 파일을 업로드 받습니다.

    • 해당 파일을 지정된 크기(예: 5MB)의 청크로 나눕니다.

    • 각 청크에 대해 FormData 객체를 생성하고 청크 데이터와 메타데이터를 추가합니다.

    • 반복적으로 axios POST 요청을 보내 청크들을 하나씩 전달합니다.

    • 이 때, 청크의 총 갯수와 현재 전송하고 있는 청크의 인덱스 번호를 전달합니다.

  • 백엔드:

    • 해당 엔드포인트로 클라이언트가 전달한 청크 데이터와 메타데이터를 받습니다.

    • 받은 청크를 임시 저장소(디렉토리, 전역 변수 등)에 저장합니다.

    • 청크를 받아서 S3에 임시로 저장합니다.

    • 청크 데이터가 모두 모이게 되면 하나의 파일 데이터로 바꿔서 해당 파일에 대한 URL를 반환합니다.


✏️ 클라이언트 예시 코드

async function uploadFile(file) {
    const chunkSize = 5 * 1024 * 1024; // 5MB
    const totalChunks = Math.ceil(file.size / chunkSize);

    for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
        const start = (chunkNumber - 1) * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const chunk = file.slice(start, end);

        const formData = new FormData();
        formData.append('file', chunk, file.name);
        formData.append('filename', file.name);
        formData.append('chunk_number', chunkNumber);
        formData.append('total_chunks', totalChunks);

        try {
            const response = await axios.post('/upload-chunk/', formData, {
                headers: { 'Content-Type': 'multipart/form-data' }
            });
            console.log(response.data.message);
            
            if (chunkNumber === totalChunks) {
                console.log('파일 업로드 완료. URL:', response.data.file_url);
                // 여기서 file_url을 사용하여 추가 작업을 수행할 수 있습니다.
            }
        } catch (error) {
            console.error('업로드 실패:', error);
            break; // 에러 발생 시 업로드 중단
        }
    }
}

✏️ boto3를 이용한 S3 청크 업로드 구현

from fastapi import FastAPI, File, UploadFile, Form
from fastapi.responses import JSONResponse
import boto3
from botocore.exceptions import ClientError

app = FastAPI()

s3_client = boto3.client('s3',
    aws_access_key_id='YOUR_ACCESS_KEY',
    aws_secret_access_key='YOUR_SECRET_KEY',
    region_name='YOUR_REGION'
)

BUCKET_NAME = 'YOUR_BUCKET_NAME'
S3_URL_PREFIX = f"https://{BUCKET_NAME}.s3.amazonaws.com/"

multipart_uploads = {}

@app.post("/upload-chunk/")
async def upload_chunk(
    file: UploadFile = File(...),
    filename: str = Form(...),
    chunk_number: int = Form(...),
    total_chunks: int = Form(...)
):
    try:
        if filename not in multipart_uploads:
            response = s3_client.create_multipart_upload(Bucket=BUCKET_NAME, Key=filename)
            multipart_uploads[filename] = {
                "UploadId": response["UploadId"],
                "Parts": []
            }

        upload_id = multipart_uploads[filename]["UploadId"]

        response = s3_client.upload_part(
            Bucket=BUCKET_NAME,
            Key=filename,
            PartNumber=chunk_number,
            UploadId=upload_id,
            Body=file.file
        )

        multipart_uploads[filename]["Parts"].append({
            "PartNumber": chunk_number,
            "ETag": response["ETag"]
        })

        if chunk_number == total_chunks:
            complete_response = s3_client.complete_multipart_upload(
                Bucket=BUCKET_NAME,
                Key=filename,
                UploadId=upload_id,
                MultipartUpload={"Parts": sorted(multipart_uploads[filename]["Parts"], key=lambda x: x["PartNumber"])}
            )
            del multipart_uploads[filename]
            
            # S3 URL 생성
            file_url = f"{S3_URL_PREFIX}{filename}"
            
            return JSONResponse(content={
                "message": "파일 업로드 완료",
                "file_url": file_url
            }, status_code=200)
        
        return JSONResponse(content={"message": f"청크 {chunk_number}/{total_chunks} 업로드 성공"}, status_code=200)

    except ClientError as e:
        return JSONResponse(content={"message": f"업로드 실패: {str(e)}"}, status_code=500)

✏️ 참고자료



📌 Tomorrow's Goal

✏️ FastAPI 공식문서 더 살펴보기

  • 의존성 주입(Dependency Injection) 심화 학습

  • FastAPI의 보안 기능 (인증, 권한 관리) 탐구



📌 Today's Goal I Done

✔️ 파일 데이터를 청크단위로 쪼개서 업로드

  • 대용량 파일 처리의 복잡성을 직접 경험하며 효율적인 업로드 방식의 중요성을 깨달았습니다.

  • 청크 단위 업로드를 통해 네트워크 불안정성에 대응하는 방법을 학습했습니다. 이는 실제 서비스에서 매우 중요한 부분이라고 느꼈습니다.

  • S3와 FastAPI를 연동하는 과정에서 클라우드 서비스와 웹 프레임워크의 통합에 대한 이해도가 높아졌습니다.

  • 단일 엔드포인트에서 여러 기능을 처리하는 방식을 구현하며, API 설계의 유연성에 대해 고민해볼 수 있었습니다.


profile
조금씩 정리하자!!!

0개의 댓글