아주 정확하게 이해하셨습니다, 정우님. 👌
지금 말씀하신 구조는 “서버가 인증·검증만 담당하고, 실제 업로드는 브라우저 → MinIO 직통” 방식입니다.
이건 대규모 파일 업로드 서비스나 SaaS API에서 거의 표준 패턴입니다.
정리해드리면, 정우님이 구상하신 구조는 다음과 같습니다 👇
[브라우저] ⇄ [Go API 서버] ⇄ [MinIO]
| 단계 | 설명 |
|---|---|
| 1️⃣ | 브라우저가 업로드를 요청 (POST /api/presign) |
| 2️⃣ | Go API 서버가 용량·파일명 검증 후 MinIO SDK로 presigned URL 생성 |
| 3️⃣ | presigned URL과 form 필드를 브라우저에 반환 |
| 4️⃣ | 브라우저가 해당 URL로 직접 업로드 (fetch or axios 등 사용) |
| 5️⃣ | 업로드 성공 시 브라우저에서 API 서버로 “업로드 완료 콜백” 전달 |
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var minioClient *minio.Client
func main() {
var err error
minioClient, err = minio.New("localhost:9000", &minio.Options{
Creds: credentials.NewStaticV4("minioadmin", "minioadmin", ""),
Secure: false,
})
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/api/presign", handlePresign)
log.Println("Server started at :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handlePresign(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// 예: 클라이언트가 업로드할 파일명, 예상 크기를 전달
filename := r.URL.Query().Get("filename")
sizeStr := r.URL.Query().Get("size") // bytes 단위
if filename == "" || sizeStr == "" {
http.Error(w, "missing filename or size", http.StatusBadRequest)
return
}
// 용량 제한 (예: 50MB)
// 실제 파일 크기는 브라우저가 미리 File.size 로 전달하도록 함
// 너무 크면 URL 생성 자체를 막음
// (MinIO 정책으로도 content-length-range 를 중첩 제한 가능)
// -----------------------------
// NOTE: 여기선 브라우저측에서도 JS에서 File.size 체크하도록 병행 권장
// -----------------------------
const maxSize = 50 * 1024 * 1024
// ... size parsing 생략 (단순화)
// presigned POST 정책 생성
policy := minio.NewPostPolicy()
policy.SetBucket("uploads")
policy.SetKey("user-uploads/" + filename)
policy.SetExpires(time.Now().UTC().Add(5 * time.Minute))
policy.SetContentLengthRange(0, maxSize)
// presigned URL 및 form 필드 생성
presignedURL, formData, err := minioClient.PresignedPostPolicy(ctx, policy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 브라우저로 전달
json.NewEncoder(w).Encode(map[string]interface{}{
"url": presignedURL.String(),
"form": formData,
})
}
<input type="file" id="fileInput" />
<button onclick="uploadFile()">Upload</button>
<script>
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert("파일을 선택하세요");
// 1️⃣ presigned URL 요청
const res = await fetch(`/api/presign?filename=${file.name}&size=${file.size}`);
const { url, form } = await res.json();
// 2️⃣ FormData 구성
const formData = new FormData();
for (const [k, v] of Object.entries(form)) formData.append(k, v);
formData.append("file", file);
// 3️⃣ MinIO 로 직접 업로드
const uploadRes = await fetch(url, {
method: "POST",
body: formData,
});
if (uploadRes.ok) {
alert("업로드 성공!");
} else {
alert("업로드 실패!");
}
}
</script>
| 항목 | 설명 |
|---|---|
| 🚫 Go 서버는 파일 데이터를 전혀 받지 않음 | 트래픽과 메모리 부하가 크게 줄어듭니다. |
| ⚡ MinIO가 직접 처리 | 업로드 속도 빠르고 서버 리소스 효율적입니다. |
| 🧩 사전 검증 가능 | 파일 크기, 확장자 등 제한을 presigned 발급 전에 확인 가능 |
| 🕒 짧은 유효시간 | 예: 5분 → 이후 자동 만료 |
| 🧱 보안적 분리 | 업로드 전용 권한만 노출되어, 다른 파일 접근 불가 |
| 항목 | 설명 |
|---|---|
| presigned URL 유효시간 | 보통 3~10분 |
| 파일 크기 제한 | 서버 발급 시 SetContentLengthRange() + JS File.size 병행 |
| 서버 역할 | presigned URL 생성 + 검증 (파일 직접 수신 없음) |
| 클라이언트 역할 | presigned URL로 직접 MinIO에 업로드 |
| 1회용 여부 | 필요시 DB로 관리, 기본은 여러 번 업로드 가능 |
이 방식은 CloudFront + S3 기반 SaaS 업로드 구조와 거의 동일하며,
정우님이 직접 구축하는 MinIO 환경에서도 완벽히 같은 아키텍처로 작동합니다.
원하신다면 다음 단계로,
👉 위 구조를 Gin 프레임워크 기반으로 완성한 실행 가능한 예제 프로젝트 구조 (디렉토리 + 코드 전체)로 정리해드릴까요?