Skip to content

object storage에 이미지 업로드하기

cdj2073 edited this page Dec 14, 2023 · 1 revision

지난 주에 object storage 없이 일단 서버에 multer를 이용해서 이미지와 동영상을 업로드할 수 있도록 구현했었다. 이를 object storage에 업로드하도록 수정하게 됐다.

multer

  • Node 환경에서 파일 업로드를 위해 사용되는 미들웨어
  • multipart/form-data 형태의 데이터를 처리한다.
  • Nest에서는 File upload와 관련된 기본적인 내장 모듈 제공
  • 따로 지정하지 않을 경우 파일은 MemoryStorage에 저장되며 Buffer 객체로 확인 가능

최초 코드

지난주에 multerOption을 사용해 storagefile limit 등을 지정해주었기에 object storage에 업로드할 때에는 multerOptionstorageS3storage로 변경해주면 될 거라고 생각했다.

import multerS3 from 'multer-s3';
import { S3Client } from '@aws-sdk/client-s3';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { extname } from 'path';
import { BadRequestException } from '@nestjs/common';
import 'dotenv/config';

const imageMimeType = ['image/jpg', 'image/jpeg', 'image/png'];
const videoMimeType = ['video/mp4'];
const MAX_FILE_SIZE = 30 * 1024 * 1024;

export const s3 = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY ?? '',
    secretAccessKey: process.env.AWS_SECRET_KEY ?? '',
  },
  endpoint: process.env.S3_ENDPOINT,
});
export const s3_bucket = process.env.S3_BUCKET ?? 'meetmeet';

export const multerOptions = (
  folder: string,
  containVideo: boolean,
): MulterOptions => {
  return {
    storage: multerS3({
      s3,
      bucket: s3_bucket,
      contentType: multerS3.AUTO_CONTENT_TYPE,
      key(req, file, callback) {
        callback(null, `${folder}/${Date.now()}${extname(file.originalname)}`);
      },
      acl: 'public-read',
    }),
    fileFilter(req, file, callback) {
      const types = containVideo
        ? [...imageMimeType, ...videoMimeType]
        : imageMimeType;
      const mimeType = types.find((type) => file.mimetype === type);

      if (!mimeType) {
        callback(
          new BadRequestException(`${types.join(', ')}만 저장할 수 있습니다.`),
          false,
        );
      }
      return callback(null, true);
    },
    limits: { fileSize: MAX_FILE_SIZE },
  };
};
@UseGuards(JwtAuthGuard)
@UseInterceptors(FilesInterceptor('contents', 10, multerOptions))
@Post()
@ApiOperation({
  summary: '피드 생성 API',
  description: '일정 멤버만 피드를 생성할 수 있습니다.',
})
@ApiBearerAuth()
@ApiConsumes('multipart/form-data')
createFeed(
  @GetUser() user: User,
  @UploadedFiles() contents: Array<Express.Multer.File>,
  @Body() createFeedDto: CreateFeedDto,
) {
  return this.feedService.createFeed(user, contents, createFeedDto);
}

그렇게 위와 같이 코드를 작성하고 나니 object storage에 잘 업로드 되는 것을 확인할 수 있었다.

image

문제 🚨

그러나 여기서 발생했던 문제는 요청이 실패했을 경우에도 object storage에 파일이 업로드된다는 것이었다.

Multer는 요청과 컨트롤러 사이에서 파일을 업로드하고 @UploadFile() 데코레이터로 처리된 파일을 매개변수로 넘겨준다. 따라서 컨트롤러에서 에러가 발생해 요청이 실패하는 경우에도 Multer에서 처리한 파일은 object storage에 업로드되는 것이었다.

⇒ Multer에서 memory storage에 업로드해두었다가 비즈니스 로직 이후에 object storage에 업로드하도록 변경

변경된 코드

async createContent(file: Express.Multer.File, dir: string) {
  file.path = this.generateFilePath(dir, file.originalname);
  const content = this.createEntity(file);

  await this.objectStorage.upload(file).catch(() => {
    throw new ObjectStorageUploadException();
  });
  return await this.contentRepository.save(content);
}
@Injectable()
export class ObjectStorage {
  private readonly s3: S3;
  private readonly s3Bucket: string;
  private readonly locationPrefix: string;

  constructor(configService: ConfigService) {
    this.s3 = new S3({
      region: configService.get<string>('AWS_REGION'),
      credentials: {
        accessKeyId: configService.get('AWS_ACCESS_KEY'),
        secretAccessKey: configService.get('AWS_SECRET_KEY'),
      },
      endpoint: configService.get('S3_ENDPOINT'),
    });

    this.s3Bucket = configService.get('S3_BUCKET', 'meetmeet');
  }

  async upload(file: Express.Multer.File) {
    await this.s3
      .upload({
        Bucket: this.s3Bucket,
        Key: file.path,
        ACL: 'public-read',
        Body: file.buffer,
      })
      .promise();
  }

  async delete(filePath: string) {
    await this.s3
      .deleteObject({ Bucket: this.s3Bucket, Key: filePath })
      .promise();
  }

  async deleteBulk(files: string[]) {
    await this.s3
      .deleteObjects({
        Bucket: this.s3Bucket,
        Delete: {
          Objects: files.map((file) => {
            return { Key: file };
          }),
        },
      })
      .promise();
  }
}

AWS sdk를 이용해 object storage에 업로드하기 위한 클래스를 따로 만들어주고, 비즈니스 로직 이후에 마지막으로 content 테이블에 이미지에 대한 정보를 저장하기 전 object storage에 업로드하도록 변경했다.

결과적으로 요청이 성공적으로 이루어진 경우에만 object storage에 파일이 업로드되고 해당 정보가 content 테이블에 저장되는 것을 확인할 수 있었다.

⚽️협업 룰

코딩 컨벤션

📔회고

팀 회고

개인 회고

K004 김근범

K016 박찬민

K032 이해림

J153 차세찬

J156 최다정

👨‍🏫멘토링 회의록

💻개발일지

Android

K004 김근범

K016 박찬민

K032 이해림

J153 차세찬

J156 최다정

💡트러블슈팅

Android

K004 김근범

K016 박찬민

K032 이해림

J153 차세찬

J156 최다정

📋회의록

스크럼 회의

스프린트 회의

밋밋 회의

공통

BackEnd

Android

기획

Clone this wiki locally