안녕하세요, 백엔드 개발자입니다.
지난 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 관리 패턴에 대해 알아보겠습니다.
긴 글 읽어주셔서 감사합니다.
'Backend' 카테고리의 다른 글
| 나만의 데코레이터 만들기: 실전 기능 구현 (0) | 2025.08.29 |
|---|---|
| 나만의 데코레이터 만들기: NestJS에서 반복적인 API 설정 줄이기 (0) | 2025.08.27 |
| 서버 부하를 줄이는 이미지 업로드 전략 (3편): 유연한 썸네일 URL 관리 패턴 (0) | 2025.08.24 |
| 서버 부하를 줄이는 이미지 업로드 전략 (1편): 역할 분리 (0) | 2025.08.19 |
| 전화번호 OTP 인증, 안전하게 구현하기 (0) | 2025.08.16 |
