deep-dive2025-02-20·9 min·183/348

MongoDB GridFS 파일 저장: 대용량 파일 관리 전략

MongoDB GridFS를 활용한 대용량 파일 저장과 관리 방법을 설명합니다.

MongoDB GridFS 파일 저장

Introduction

MongoDB GridFS는 16MB를 초과하는 대용량 파일을 저장하기 위한 명세입니다. 바이너리大型 파일을 청크(chunk)로 분할하여chunks와 files 컬렉션에 저장하며, 메타데이터와 파일 데이터를 효율적으로 관리할 수 있습니다. 이 글에서는 GridFS의 동작 원리와 최적화 방법을 다루겠습니다.

Environment

# MongoDB GridFS 명령어 테스트
mongofiles --host localhost --port 27017 --db mydb

# GridFS 상태 확인
mongosh --eval "db.getSiblingDB('mydb').fs.files.find().count()"
// Node.js MongoDB 클라이언트
const { MongoClient, GridFSBucket } = require('mongodb');
const fs = require('fs');
const path = require('path');

const client = new MongoClient('mongodb://localhost:27017');

Problem

GridFS 사용 시 발생하는 문제들:

// 파일 업로드 성능 문제
async function uploadFile(filePath) {
  const bucket = new GridFSBucket(client.db('mydb'));
  const uploadStream = bucket.openUploadStream(path.basename(filePath));
  const fileStream = fs.createReadStream(filePath);
  
  fileStream.pipe(uploadStream);
  
  // 대용량 파일 업로드 시 느린 성능
  // 1GB 파일 업로드: 45초 소요
}

// 파일 다운로드 성능 문제
async function downloadFile(fileId) {
  const bucket = new GridFSBucket(client.db('mydb'));
  const downloadStream = bucket.openDownloadStream(fileId);
  const writeStream = fs.createWriteStream('downloaded_file');
  
  downloadStream.pipe(writeStream);
  
  // 대용량 파일 다운로드 시 메모리 문제
}
# GridFS 컬렉션 상태 확인
mongosh --eval "
db.getSiblingDB('mydb').fs.files.find().count()
db.getSiblingDB('mydb').fs.chunks.find().count()
"

# 결과:
# fs.files: 10000
# fs.chunks: 150000  // 청크가 15배 많음

Analysis

GridFS 성능 문제를 분석했습니다:

// GridFS 파일 메타데이터 확인
const files = await client.db('mydb').collection('fs.files').find().toArray();
console.log('파일 통계:', {
  총파일수: files.length,
  평균크기: files.reduce((sum, f) => sum + f.length, 0) / files.length,
  최대크기: Math.max(...files.map(f => f.length)),
  청크크기: files[0]?.chunkSize || 255 * 1024  // 기본 255KB
});

// 청크 분포 확인
const chunks = await client.db('mydb').collection('fs.chunks').aggregate([
  { $group: { _id: '$files_id', chunkCount: { $sum: 1 } } },
  { $group: { _id: null, avgChunks: { $avg: '$chunkCount' } } }
]).toArray();

console.log('평균 청크 수:', chunks[0]?.avgChunks);
// GridFS 인덱스 상태 확인
const fileIndexes = await client.db('mydb').collection('fs.files').indexes();
const chunkIndexes = await client.db('mydb').collection('fs.chunks').indexes();

console.log('files 인덱스:', fileIndexes);
console.log('chunks 인덱스:', chunkIndexes);
// chunks 인덱스: { files_id: 1, n: 1 }

Solution

1단계: GridFS 최적화 설정

// 최적화된 GridFS 버킷 생성
const bucket = new GridFSBucket(client.db('mydb'), {
  bucketName: 'uploads',
  chunkSizeBytes: 255 * 1024,  // 255KB 청크 크기
  // 대용량 파일의 경우更大的 청크 크기 사용
  writeConcern: { w: 'majority' },
  readPreference: 'primaryPreferred'
});

// 파일 업로드 최적화
async function optimizedUpload(filePath, metadata = {}) {
  const bucket = new GridFSBucket(client.db('mydb'), {
    bucketName: 'uploads',
    chunkSizeBytes: 1024 * 1024  // 1MB 청크 (대용량 파일용)
  });
  
  const fileName = path.basename(filePath);
  const fileStream = fs.createReadStream(filePath);
  
  const uploadStream = bucket.openUploadStream(fileName, {
    metadata: {
      ...metadata,
      uploadedAt: new Date(),
      originalSize: fs.statSync(filePath).size
    }
  });
  
  return new Promise((resolve, reject) => {
    fileStream.pipe(uploadStream);
    
    uploadStream.on('error', reject);
    
    uploadStream.on('finish', () => {
      resolve({
        fileId: uploadStream.id,
        fileName: fileName,
        chunkSize: uploadStream.chunkSize
      });
    });
  });
}

2단계: 청크 관리 최적화

// 청크 기반 다운로드
async function chunkedDownload(fileId, outputStream) {
  const bucket = new GridFSBucket(client.db('mydb'), {
    bucketName: 'uploads'
  });
  
  const downloadStream = bucket.openDownloadStream(fileId);
  
  return new Promise((resolve, reject) => {
    downloadStream.pipe(outputStream);
    
    downloadStream.on('error', reject);
    
    downloadStream.on('end', () => {
      resolve({
        fileId: fileId,
        bytesDownloaded: outputStream.bytesWritten
      });
    });
  });
}

// 병렬 청크 다운로드
async function parallelDownload(fileId, outputPath) {
  const db = client.db('mydb');
  const chunks = await db.collection('uploads.chunks')
    .find({ files_id: fileId })
    .sort({ n: 1 })
    .toArray();
  
  const writeStream = fs.createWriteStream(outputPath);
  
  for (const chunk of chunks) {
    writeStream.write(chunk.data.buffer);
  }
  
  writeStream.end();
  
  return new Promise((resolve) => {
    writeStream.on('finish', () => {
      resolve({
        fileId: fileId,
        totalChunks: chunks.length,
        totalSize: chunks.reduce((sum, c) => sum + c.data.length, 0)
      });
    });
  });
}

3단계: 메타데이터 관리

// 메타데이터 기반 파일 검색
async function searchFiles(query, options = {}) {
  const db = client.db('mydb');
  const filesCollection = db.collection('uploads.files');
  
  const searchQuery = {
    $or: [
      { filename: { $regex: query, $options: 'i' } },
      { 'metadata.tags': { $in: [query] } },
      { 'metadata.description': { $regex: query, $options: 'i' } }
    ]
  };
  
  if (options.contentType) {
    searchQuery['metadata.contentType'] = options.contentType;
  }
  
  if (options.minSize) {
    searchQuery.length = { $gte: options.minSize };
  }
  
  const files = await filesCollection
    .find(searchQuery)
    .sort({ uploadDate: -1 })
    .limit(options.limit || 100)
    .toArray();
  
  return files;
}

// 파일 메타데이터 업데이트
async function updateFileMetadata(fileId, metadata) {
  const db = client.db('mydb');
  
  await db.collection('uploads.files').updateOne(
    { _id: fileId },
    { $set: { metadata: metadata } }
  );
  
  return { success: true, fileId: fileId };
}

Lessons Learned

  1. 청크 크기 선택: 파일 크기에 따라 적절한 청크 크기를 선택해야 합니다 (255KB~1MB)
  2. 인덱스 관리: fs.filesfs.chunks 컬렉션에 적절한 인덱스를 생성해야 합니다
  3. 메타데이터 활용: 파일 검색과 관리를 위해 메타데이터를 효과적으로 활용해야 합니다
  4. 스트리밍 처리: 대용량 파일은 반드시 스트리밍 방식으로 처리하여 메모리 문제를 방지해야 합니다
  5. 백업 전략: GridFS 데이터의 백업은 mongodump보다 mongorestore와 함께 파일 시스템 백업을 권장합니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.