Documents
Home>Documents>Dev>Backend

MinIO upload: 상황별로 어떤 방식을 골라야 하는가

10 min readSep 15, 2026Sep 15, 2026

MinIO에 파일을 올리는 코드 자체는 어렵지 않다. SDK 세 줄, CLI 한 줄이면 동작한다. 문제는 방식 선택이다. 브라우저에서 파일을 받아 서버가 중계하는 구조를 그냥 쓰다 트래픽이 두 배가 되거나, 2GB 파일을 올리다 네트워크가 끊기거나, 멀티파트 업로드를 쓰면서 정리 정책을 빠뜨려 디스크가 조용히 줄어드는 일이 생긴다.

업로드 방식 선택을 결정하는 변수는 세 가지다: 파일 크기, 클라이언트가 서버를 경유해야 하는지 여부, 네트워크 안정성. 이 축에 따라 단순 PUT, 멀티파트, Presigned URL 중 무엇을 골라야 하는지 정리한다.

세 가지 경로 요약

  • SDK (fput_object / put_object): 서버 사이드에서 MinIO에 직접 연결해 올린다. 파일 크기를 보고 단순 PUT과 멀티파트를 자동 선택한다.
  • Presigned URL: 서버가 서명된 URL을 발급하고, 브라우저·모바일 클라이언트가 그 URL로 MinIO에 직접 PUT한다. 서버 트래픽을 완전히 우회한다.
  • mc CLI: 배치 업로드와 폴더 동기화용. CI/CD 파이프라인에서 주로 쓴다.

세 경로 모두 내부적으로 S3 호환 API를 쓰므로 AWS SDK로 MinIO에 연결해도 동작한다.

단순 PUT vs 멀티파트: 5MiB 기준

MinIO Python SDK에서 fput_objectput_object를 호출하면, SDK가 파일 크기를 보고 단순 PUT과 멀티파트를 자동으로 선택한다. 기본 임계값은 5MiB(= 5 × 1,048,576 바이트)다.

fput_object는 로컬 파일 경로를 받고, put_object는 파일 객체(스트림)와 length를 함께 받는다.

from minio import Minio

client = Minio(
    "minio.example.com:9000",
    access_key="YOUR-ACCESS-KEY",
    secret_key="YOUR-SECRET-KEY",
    secure=True,
)

# 파일 경로로 업로드 — SDK가 멀티파트 여부 자동 결정
result = client.fput_object(
    "my-bucket",
    "data/report.csv",
    "/local/path/report.csv",
    content_type="text/csv",
)
print(result.object_name, result.etag)

스트림 소스(메모리 버퍼, HTTP 응답 바디 등)를 바로 올릴 때는 put_object를 써야 한다.

import io

data = b"hello, minio"
client.put_object(
    "my-bucket",
    "greet.txt",
    io.BytesIO(data),
    length=len(data),
    content_type="text/plain",
)

멀티파트로 올라갈 때 파트 크기는 part_size 파라미터로 조정할 수 있다. 기본값 0이면 SDK가 파일 크기를 보고 최적 크기를 계산한다. 불안정한 네트워크 환경에서는 파트를 작게 잡는 게 유리하다 — 전체를 다시 올리는 게 아니라 실패한 파트만 재전송하기 때문이다.

result = client.fput_object(
    "my-bucket",
    "large-video.mp4",
    "/local/large-video.mp4",
    part_size=10 * 1024 * 1024,  # 10MiB
)

자주 빠뜨리는 부분이 content_type이다. 누락하면 MinIO는 application/octet-stream으로 저장하고, 다운로드 시 브라우저가 이미지나 PDF를 제대로 처리하지 못한다. 이미지·문서 계열 파일은 반드시 명시한다.

SSL 검증을 비활성화하는 방법(urllib3 인스턴스를 http_client 파라미터로 전달)은 내부망 테스트 환경에서만 허용한다. 프로덕션에서 인증서 없이 운영하려면 자체 서명 인증서를 클라이언트 신뢰 저장소에 등록하는 쪽이 맞다.

멀티파트의 숨겨진 비용은 업로드 중단 시 올라간 파트가 스토리지에 잔류한다는 점이다. 이 문제는 마지막 섹션에서 다룬다.

Presigned URL: 클라이언트에게 권한을 위임하는 설계

서버를 경유해 파일을 올리면 트래픽이 두 배다. 클라이언트 → 서버 → MinIO. 100MB 파일이면 서버 인바운드·아웃바운드 대역폭이 모두 100MB씩 소모된다. Presigned URL은 이 경로를 클라이언트 → MinIO로 단축한다.

서버가 서명된 URL을 생성해 클라이언트에 전달하고, 클라이언트가 그 URL로 HTTP PUT 요청을 직접 보낸다. MinIO는 서명을 검증하고 저장한다. 파일 내용이 서버를 지나지 않는다.

from datetime import timedelta

# 서버 쪽: 1시간 유효한 presigned URL 발급
url = client.presigned_put_object(
    "my-bucket",
    "uploads/user-avatar.png",
    expires=timedelta(hours=1),
)
# 이 URL을 클라이언트에 전달

기본 만료 시간은 7일이고 이게 상한선이기도 하다. SigV4 서명 스펙이 604,800초(7일)를 최대로 제한하며, MinIO도 동일하게 따른다. 업로드 용도로는 URL 수명을 짧게 유지하는 게 맞다 — 클라이언트가 URL을 받자마자 올릴 거라면 1시간도 충분히 길다.

클라이언트에서 PUT은 이렇게 보낸다.

// 브라우저에서 presigned URL로 직접 업로드
const file = document.querySelector('input[type=file]').files[0];
await fetch(presignedUrl, {
  method: 'PUT',
  body: file,
  headers: { 'Content-Type': file.type },
});

한 가지 제약이 있다. 단일 Presigned URL로 올릴 수 있는 파일 크기는 S3 단일 PUT 제한인 5GiB가 상한이다. 그 이상이면 멀티파트 presigned URL을 써야 하는데, 파트마다 URL을 발급하고 각 파트를 올린 뒤 Complete 요청을 별도로 보내는 구현이 필요하다. 복잡도가 상당히 올라가므로, 현실적으로는 5GiB 미만 파일에 단순 presigned PUT을 유지하고 그 이상은 서버 사이드 SDK로 처리하는 쪽이 낫다.

Python SDK 코드 작성 시 흔한 실수

fput_objectput_object를 혼동해서 쓰는 경우가 많다. 차이는 단순하다: 로컬 파일이 있으면 fput_object, 스트림을 다루면 put_object. 실제로 put_object에 스트림을 넘길 때 length-1로 설정하면 MinIO가 청크 전송 인코딩을 쓰는데, 일부 프록시 환경에서 오류가 생긴다. 정확한 길이를 알 수 있으면 항상 명시하는 게 안전하다.

업로드 진행률을 보고 싶으면 progress 파라미터에 콜백을 넘긴다. SDK가 기본 제공하는 urllib3.request.RequestMethods의 progress hook을 직접 쓰거나, tqdm 기반 래퍼를 만들어 쓰는 방법이 있다.

from minio.progress import Progress

result = client.fput_object(
    "my-bucket",
    "large-file.zip",
    "/local/large-file.zip",
    progress=Progress(),
)

Progress() 클래스는 minio-py에 내장되어 있으며 터미널에 진행률 바를 출력한다.

mc CLI: 배치와 자동화

mc CLI는 폴더 동기화와 파이프라인 자동화에 적합하다. alias를 먼저 등록한다.

mc alias set myminio http://minio.example.com:9000 ACCESS-KEY SECRET-KEY

# 단일 파일 업로드
mc cp ./report.csv myminio/my-bucket/data/report.csv

# 폴더 동기화 — 소스에 없는 파일은 버킷에서 제거
mc mirror --overwrite --remove ./local-dir/ myminio/my-bucket/target/

--remove 옵션은 소스 디렉터리에 없는 객체를 버킷에서 삭제한다. 로컬 디렉터리가 비어 있으면 버킷 전체가 삭제될 수 있으므로 프로덕션에서는 명시적인 소스 경로 확인이 필요하다.

CI/CD 파이프라인에서 자격증명 주입은 MC_HOST_{alias} 환경변수 하나로 처리한다.

# GitHub Actions 예시
- name: Upload artifacts
  env:
    MC_HOST_myminio: http://${{ secrets.MINIO_USER }}:${{ secrets.MINIO_PASSWORD }}@minio.example.com:9000
  run: mc cp --recursive ./dist/ myminio/releases/${{ github.sha }}/

alias 등록과 인증을 한 줄로 처리하고, 자격증명이 로그에 직접 노출되지 않는다.

미완료 멀티파트 파트 정리

멀티파트 업로드가 중단되면 이미 전송된 파트들이 버킷에 잔류한다. 완성된 객체로 조립되지 않아 사용자에게 보이지 않지만 스토리지는 차지한다. 업로드 트래픽이 많은 서비스에서 이를 방치하면 디스크가 이유 없이 줄어드는 현상이 생긴다.

현재 버킷의 미완료 파트 조회:

mc find myminio/my-bucket --incomplete

자동 정리는 Lifecycle 정책으로 설정한다. AbortIncompleteMultipartUpload 규칙을 적용하면 지정한 일수 이후 미완료 파트를 MinIO가 자동으로 삭제한다.

<LifecycleConfiguration>
  <Rule>
    <ID>abort-incomplete-multipart</ID>
    <Status>Enabled</Status>
    <AbortIncompleteMultipartUpload>
      <DaysAfterInitiation>7</DaysAfterInitiation>
    </AbortIncompleteMultipartUpload>
  </Rule>
</LifecycleConfiguration>

이 정책은 버킷 생성 시점에 같이 넣어두는 게 맞다. 멀티파트 업로드를 쓴다는 결정과 정리 정책 설정은 세트다 — 나중에 추가하려 할 때는 이미 파트가 쌓여 있다.

Tags
MinIOPythonAPI오브젝트 스토리지S3파일 업로드