NestJS + AWS S3 파일 업로드 API 설계 문서

여리·2025년 7월 18일

✨ 목적 및 개요

NestJS 애플리케이션에서 AWS S3를 이용해 파일을 업로드하는 기능을 개발한다. 이를 통해 대용량 파일 저장, 안정성, 확장성, 비용 효율성 등의 이점을 활용하며 서비스 품질을 높이는 것이 목적이다.


🌟 사용 시나리오

  • 프로필 이미지 업로드
  • 게시물에 첨부 이미지 업로드
  • 상품 정보에 이미지 등록

🔐 보안 및 유효성 체크

  • 허용된 MIME 타입 검사
  • 업로드 파일 최대 크기 제한
  • 서버단에서 직접 S3에 업로드 (private bucket)
  • 응답 시 파일 URL, 파일명 등 정보 제공

⚖️ 디렉터리 구조 예시

src/
├── file-upload/
│   ├── file-upload.controller.ts
│   ├── file-upload.service.ts
│   └── file-upload.module.ts

📁 .env 설정 예시

AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
AWS_S3_BUCKET=your-bucket-name

📊 전체 코드 흐름

1. file-upload.module.ts

import { Module } from '@nestjs/common';
import { FileUploadController } from './file-upload.controller';
import { FileUploadService } from './file-upload.service';

@Module({
  controllers: [FileUploadController],
  providers: [FileUploadService],
})
export class FileUploadModule {}

2. file-upload.controller.ts

import {
  Controller,
  Post,
  UploadedFile,
  UploadedFiles,
  UseInterceptors,
  Body,
  BadRequestException,
} from '@nestjs/common';
import { FileUploadService } from './file-upload.service';
import {
  FileInterceptor,
  FileFieldsInterceptor,
} from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger';

@ApiTags('File Upload')
@Controller('upload')
export class FileUploadController {
  constructor(private readonly fileUploadService: FileUploadService) {}

  @Post('single')
  @ApiOperation({ summary: '단일 파일 업로드' })
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('file'))
  async uploadSingle(
    @UploadedFile() file: Express.Multer.File,
    @Body() body: any,
  ) {
    if (!file) throw new BadRequestException('파일이 존재하지 않습니다');
    return await this.fileUploadService.uploadSingle(file, body);
  }

  @Post('multiple')
  @ApiOperation({ summary: '다중 파일 업로드 (최대 5개)' })
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(
    FileFieldsInterceptor(
      [{ name: 'files', maxCount: 5 }],
      { limits: { fileSize: 20 * 1024 * 1024 } },
    ),
  )
  async uploadMultiple(
    @UploadedFiles() files: { files: Express.Multer.File[] },
    @Body() body: any,
  ) {
    if (!files?.files?.length) throw new BadRequestException('파일이 없습니다');
    return await this.fileUploadService.uploadMultiple(files.files, body);
  }
}

3. file-upload.service.ts

import {
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { extname } from 'path';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class FileUploadService {
  private s3: S3Client;
  private bucket: string;

  constructor(private readonly configService: ConfigService) {
    this.s3 = new S3Client({
      region: this.configService.get('AWS_REGION'),
      credentials: {
        accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
        secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
      },
    });
    this.bucket = this.configService.get('AWS_S3_BUCKET');
  }

  async uploadSingle(file: Express.Multer.File, body: any) {
    const key = this.generateFileKey(file.originalname, body?.subdir);

    await this.uploadToS3(file.buffer, file.mimetype, key);

    return {
      url: this.getFileUrl(key),
      name: file.originalname,
      key,
    };
  }

  async uploadMultiple(files: Express.Multer.File[], body: any) {
    const results = await Promise.all(
      files.map((file, index) => this.uploadSingle(file, { ...body, index })),
    );
    return results;
  }

  private generateFileKey(filename: string, subdir = 'default'): string {
    const ext = extname(filename);
    const uniqueName = `${uuidv4()}${ext}`;
    return `${subdir}/${uniqueName}`;
  }

  private async uploadToS3(
    buffer: Buffer,
    contentType: string,
    key: string,
  ) {
    try {
      await this.s3.send(
        new PutObjectCommand({
          Bucket: this.bucket,
          Key: key,
          Body: buffer,
          ContentType: contentType,
        }),
      );
    } catch (err) {
      console.error(err);
      throw new InternalServerErrorException('S3 업로드 실패');
    }
  }

  private getFileUrl(key: string): string {
    return `https://${this.bucket}.s3.${this.configService.get('AWS_REGION')}.amazonaws.com/${key}`;
  }
}

🌐 Swagger 문서 예시 (단일)

  • POST /upload/single

  • 요청 본문: multipart/form-data

    • file: [파일]
    • subdir: [저장할 경로]
  • 응답:

{
  "url": "https://your-bucket.s3.ap-northeast-2.amazonaws.com/path/to/file.jpg",
  "name": "file.jpg",
  "key": "path/to/file.jpg"
}

🚀 추가 고려 사항

  • AWS S3 권한 최소화 정책을 적용할 것 (읽기/쓰기 분리 가능)
  • 파일 접근 제한이 필요한 경우 Pre-signed URL을 사용하도록 개선 가능
  • 향후 Lambda 연계하여 썸네일 생성, 이미지 최적화 등 확장 고려

profile
beckend developer

0개의 댓글