[Node.js] GridFS를 사용하여 MongoDB에 파일을 저장해보자

승우·2025년 2월 3일
post-thumbnail

GridFS의 작동 방식

GridFS는 파일 청크 및 설명 정보가 포함된 Mongo DB 컬렉션의 그룹인 버킷에 파일을 구성한다.

  • chunks 컬렉션: 바이너리 파일 청크를 저장
  • files 컬렉션: 파일의 메타데이터를 저장

즉, GridFS로 파일을 저장할 때 드라이버는 파일을 작은 조각으로 분할하며, 각 조각은 chunks 컬렉션에서 별도의 문서로 표시된다.

또한 files 컬렉션에 고유한 파일ID, 파일명 및 기타 파일 메타데이터가 포함된 문서를 만들고, 메모리 또는 스트림에서 파일을 업로드할 수 있다.

아래 다이어그램을 참고하면 이해하기 훨씬 수월하다.

우선 나는 mongodb를 사용하여 현재 서비스를 배포하기도 했고, 캡스톤 프로젝트라 데이터를 많이 저장할 필요도 없어서 AWS의 S3가 아닌 GridFS를 선택했다.

우선 이번 포스트에서는 업무란에 문서를 업로드하는 API에 대해 설명할 것이다.

작성할 API의 로직은 다음과 같다.

  1. 요청 파라미터로 받은 업무의 id로 업무의 존재유무 판별
  2. 요청한 user가 업무 담당자이거나 생성자인 경우를 판단
  3. req.file의 리소스가 잘 들어왔는지 판단
  4. 데이터베이스에 저장 후 파일의 id 반환

우선, 로직은 생각했으니 GridFS를 사용하기 위한 초기 세팅부터 진행해보자.

GridFS 코드 작성

config/database.js

const mongoose = require('mongoose');
const dotenv = require('dotenv');
const { GridFSBucket } = require('mongodb');

...

dotenv.config();

const dbURI = process.env.MONGO_URI;

// MongoDB 연결 함수
const connectDB = async () => {
  try {
    ...

    const bucket = new GridFSBucket(conn.connection.db, {
      bucketName: 'documents',
    });

    console.log('✅ GridFSBucket initialized!');
    return bucket;
  } catch (err) {
    console.error('MongoDB connection error:', err.message);
    process.exit(1); // 연결 실패 시 프로세스 종료
  }
};

...

module.exports = { connectDB, mongoose };

서버 실행 시, 데이터베이스를 초기화하고 bucket을 만드는 함수이다.

bucket의 이름은 documents로 지정해주었다.

conn.connection.db는 MongoDB의 native driver에 접근할 수 있도록 해준다.

그리고 마지막에 connetcDB와 함께 mongoose를 함께 export하는 것을 볼 수 있는데, 이는 connectDB()가 실행된 후의 mongoose 인스턴스를 사용하여 DB연결을 보장하게끔 해주기 위함이다.

다음으로 multer의 미들웨어에 대한 코드이다.

middlewares/multer.js

const multer = require('multer');

// 메모리 스토리지 사용 (GridFS 업로드용)
const storage = multer.memoryStorage();
const upload = multer({ storage });

// 단일 파일 업로드 설정 (필드명: 'file')
module.exports = {
  uploadSingle: upload.single('file'),
};

우선 multer는 Node.js에서 파일 업로드를 쉽게 처리할 수 있도록 해주는 패키지로, 요놈을 사용하면 multipart/form-data 요청을 파싱할 수 있다.

memoryStorage()를 사용하여 서버의 디스크가 아닌 메모리(RAM)에 저장하는 방식을 사용하였다.

이렇게 하면 req.file.buffer 형태로 파일 데이터가 저장되어 파일을 DB에 직접 업로드하기 편리하다.

그 후 multer를 사용하여 파일 업로드를 처리하는 미들웨어를 생성해주고 module을 export해주면 라우터에서 이 미들웨어에 접근할 수 있게 된다.

이제 서비스 레이어에 접근해보자!

services/docs-service.js

const { mongoose } = require('../../config/database');
const { GridFSBucket } = require('mongodb');
const taskUtil = require('../utils/task-util');
const Task = require('../models/Task');

let bucket;

mongoose.connection.once('open', () => {
  bucket = new GridFSBucket(mongoose.connection.db, {
    bucketName: 'documents',
  });
  console.log('GridFSBucket initialized in docsService');
});

exports.postDocument = async (file, taskId, userId) => {
  try {
    const isExistingTask = await taskUtil.isExistingResource(Task, taskId);
    if (!isExistingTask) {
      throw new Error('No task found');
    }

    const isAccessible = await taskUtil.scopeChecker(userId, isExistingTask);
    if (!isAccessible) {
      throw new Error('You don’t have any privilege to upload file');
    }

    return new Promise((resolve, reject) => {
      try {
        if (!bucket) {
          return reject(new Error('GridFSBucket is not initialized'));
        }

        const uploadStream = bucket.openUploadStream(file.originalname, {
          metadata: { taskId },
        });

        uploadStream.end(file.buffer);

        uploadStream.on('finish', () => {
          resolve({
            success: true,
            message: 'Document uploaded successfully',
            fileId: uploadStream.id,
          });
        });

        uploadStream.on('error', (err) => {
          console.error('GridFS Upload Error:', err.message);
          return reject(new Error('Error during file upload: ' + err.message));
        });
      } catch (error) {
        console.error('Error in uploadDocument function:', error.message);
        return reject(
          new Error('Unexpected error in uploadDocument: ' + error.message)
        );
      }
    });
  } catch (err) {
    console.error('Error in postDocument:', err.message);
    throw new Error('Error occurred during uploading file(s): ' + err.message);
  }
};

taskUtil에서 불러온 함수들은 편의를 위해 구현해놓은 함수들인데 이 포스팅에서 중요한 것은 GridFS에 관한 로직이므로 요놈 위주로 얘기해보겠다.

우선, GridFSBucket을 초기화하여 MongoDB에 파일을 저장할 수 있도록 설정한 후, once('open', callback)을 사용하여 MongoDB가 연결된 후에만 실행하도록 해준다.

이제 postDocument 함수에 대해 살펴보자.

bucket이 초기화되지 않은 경우에 대해 에러 처리를 해주었고, openUploadStream(file.originalname)을 사용하여 GridFS에 파일을 저장했다.

이때 metadata는 어떤 Task에 속한 파일인지를 저장하기 위해 넣어주었고, uploadStream.end(file.buffer)를 이용해 파일 데이터를 업로드 스트림에 보내주었다.

이제 postman으로 POST 요청을 보내보면

멋지게 데이터가 들어간 것을 볼 수 있다.

이제 compass로 들어간 데이터들을 확인해보자!

우선 버킷명을 document로 설정해주었기 때문에 chunksfiles 컬렉션이 저렇게 생성된 것이다.

document.files

document.chunks

위에서 언급했던 다이어그램과 같이 보면 이제 완벽히 어떤 구조로 데이터가 저장되는지 알 수 있을 것이다.

이제,, 문서 삭제, 다운로드, 조회 api 만들러 떠나야지,,ㅜㅜ

profile
이것저것 해볼래요

0개의 댓글