본문으로 바로가기
2222

안녕하세요, 백엔드 개발자입니다.

지난 1편에서는 이미지 업로드 시 서버 부하를 줄이기 위해, 스트리밍 파이프라인을 구성하고 역할을 분리하는 전략에 대해 알아보았습니다.

이번 2편에서는 그 전략을 실제로 구현하는 방법에 대해 자세히 다뤄보겠습니다. NestJS 환경에서 multer와 multer-s3-transform 라이브러리를 사용하여, 이벤트 루프를 막지 않는 스트리밍 파이프라인을 구축해 보겠습니다.

1. 스트리밍 파이프라인의 원리

multer-s3-transform 라이브러리의 핵심은 여러 스트림을 파이프처럼 연결하는 것입니다.

HTTP Request Stream → Sharp Transform Stream → S3 Upload Stream

데이터는 큰 덩어리가 아닌 작은 조각(chunk)으로 위 파이프라인을 따라 흐릅니다. 서버 메모리에는 전체 파일이 아닌, 현재 처리 중인 작은 조각만 머무르기 때문에 이벤트 루프가 블로킹되지 않습니다.

2. 파일 처리 로직 구현 (NestJS Interceptor)

NestJS의 Interceptor를 사용하여 이 스트리밍 파이프라인을 설정합니다.

// s3-image-upload.interceptor.ts (예시 파일)

import { FileInterceptor } from '@nestjs/platform-express';
import * as multerS3 from 'multer-s3-transform';
import * as AWS from 'aws-sdk';
import * as sharp from 'sharp';
import { v4 as uuidv4 } from 'uuid';

const s3 = new AWS.S3({ /* ... AWS 자격증명 설정 ... */ });

const multerOptions = {
  storage: multerS3({
    s3: s3,
    bucket: process.env.AWS_S3_BUCKET_NAME,
    contentType: multerS3.AUTO_CONTENT_TYPE,
    shouldTransform: true,
    // transforms 배열로 여러 개의 파이프라인을 동시에 실행할 수 있습니다.
    transforms: [
      {
        id: 'original',
        key: (req, file, cb) => cb(null, `original/${uuidv4()}.${file.originalname.split('.').pop()}`),
        // transform 함수는 sharp()의 Transform Stream을 반환합니다.
        transform: (req, file, cb) => cb(null, sharp()), // 원본은 변환 없이 그대로 스트리밍
      },
      {
        id: 'thumbnail',
        key: (req, file, cb) => cb(null, `w500/${uuidv4()}.${file.originalname.split('.').pop()}`),
        // 너비 500px로 리사이징하는 Transform Stream을 파이프라인에 추가합니다.
        transform: (req, file, cb) => cb(null, sharp().resize({ width: 500 })),
      },
    ],
    acl: 'public-read',
  }),
};

export const S3ImageUploadInterceptor = FileInterceptor('file', multerOptions);

transform 함수는 sharp() 객체(Transform Stream)를 반환하여, 들어오는 데이터 스트림을 어떻게 변환할지 정의합니다. multer-s3-transform은 이 스트림들을 S3 업로드 스트림에 연결해주는 역할을 합니다.

3. 컨트롤러에 적용하기

만들어진 인터셉터를 @UseInterceptors 데코레이터를 사용하여 컨트롤러에 적용합니다.

// resource.controller.ts

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { ResourceService } from '../services/resource.service';
import { S3ImageUploadInterceptor } from './s3-image-upload.interceptor';

@Controller('resources')
export class ResourceController {
  constructor(private readonly resourceService: ResourceService) {}

  @Post('/upload/image')
  @UseInterceptors(S3ImageUploadInterceptor)
  async uploadImageFile(@UploadedFile() file: any) {
    // 인터셉터가 스트리밍 처리를 모두 완료하면,
    // 'file' 객체에 그 결과 메타데이터가 담겨 컨트롤러로 전달됩니다.
    const originalFile = file.transforms.find(t => t.id === 'original');

    const resource = await this.resourceService.create({
      resource_type: 'image',
      resource_key: originalFile.key,
      resource_url: originalFile.location,
      filesize: originalFile.size,
      // ... 기타 메타데이터 ...
    });

    return this.resourceService.mapData(resource);
  }
}

마치며

이번 2편에서는 multer와 관련 라이브러리들을 활용하여, 이벤트 루프를 막지 않는 스트리밍 파이프라인을 실제로 구현하는 방법을 알아보았습니다.

 

업로드된 원본과 썸네일의 URL을 어떻게 더 유연하게 관리할 수 있을까요? 다음 마지막 3편에서는 DB 스키마를 단순하게 유지하면서 다양한 썸네일 사이즈에 대응할 수 있는 URL 관리 패턴에 대해 알아보겠습니다.

긴 글 읽어주셔서 감사합니다.