카테고리 없음

ExpressJS multer-s3 builder pattern

realtrynna 2023. 4. 24. 20:30

서버는 클라이언트로부터 다양한 형식의 파일을 업로드 받을 수 있다.

PNG 형식의 이미지 파일은 사용자의 프로필 이미지가 될 수 있으며, MP3 형식의 음성 파일은 사용자의 목소리를 녹음한 자기소개가 될 수 있다.

 

다음 코드는 이미지와 비디오 각자 다른 형식의 파일을 업로드하는 로직이다.

이미지는 images/..., 5MB 비디오는 video/..., 20MB 두 형식 모두 저장될 경로와 저장 사이즈를 다르게 처리해야 한다.

추후 허용할 업로드 형식이 추가된다면 파일이 늘어날 수 있다. 파일 개수가 많아지다 보면 유지 보수적인 측면에서 비효율적이고 의도치 않는 실수로 이어질 수 있다.

// image.multer.js
const s3 = new S3Client({...});

const storage = multerS3({
    s3,
    bucket: "Your bucket",
    key: (req, file, done) => {
        done(null, `images/${file.originalname}`);
    }
});

const fileFilter = (req, file, done) => {
    const extension = file.mimetype;
    const allowExtension = ["image/jpeg", "image/jpg", "image/png", "image/gif"];

    if (allowExtension.includes(extension)) done(null, true);
    else done(new Error(`허용된 형식이 아닙니다. ${file.mimeType}`), false);
}

const limits = {
    fileSize: 5 * 1_024 * 1_024,
}

export default multer({ fileFilter, storage, limits }).single("file");
// video.multer.js
const s3 = new S3Client({...});

const storage = multerS3({
    s3,
    bucket: "Your bucket",
    key: (req, file, done) => {
        done(null, `video/${file.originalname}`);
    }
});

const fileFilter = (req, file, done) => {
    const extension = file.mimetype;
    const allowExtension = ["video/quicktime", "video/mp4", "video/x-ms-wmv"];

    if (allowExtension.includes(extension)) done(null, true);
    else done(new Error(`허용된 형식이 아닙니다. ${file.mimeType}`), false);
}

const limits = {
    fileSize: 20 * 1_024 * 1_024,
}

export default multer({ fileFilter, storage, limits }).single("file");

 

 

다음은 위 로직을 빌더 패턴으로 구성한 코드이다.

파일 형식별로 동적으로 변해야 하는 요소(형식, 경로, 사이즈)들을 메서드를 통해 동적으로 설정한다. 객체와 객체의 구조를 분리하고 복잡한 객체를 단계별로 생성할 수 있다. fileFilter 익명 함수는 파일 형식을 매개 변수로 받아 형식에 대한 검증(Validation)을 하며 클래스 생성 시 순차적으로 허용 타입(setAllow...)과 저장 경로(setPath)를 설정할 수 있다.

// multer.builder.js
export const imageMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
export const videoMimeTypes = ["video/quicktime", "video/mp4", "video/x-ms-wmv", "video/webm"];

export const fileFilter = (kind) => (req, file, done) => {
    const type = kind === "image" ? imageMimeTypes : videoMimeTypes;
    const prepareMimeType = type.find(mimeType => mimeType === file.mimetype);

    if (!prepareMimeType) done(new Error(`허용된 형식이 아닙니다. ${type}`), false);

    done(null, true);
}

export class MulterBuilder {
    #s3;
    #allowMimeTypes = [];
    #path = "";
    #fileFilter;
    #fileSize;

    constructor() {
        this.#s3 = new S3Client({
            region: "Your region",
            credentials: {
                accessKeyId: "Your access key",
                secretAccessKey: "Your secret access key",
            }
        });
    }

    setAllowImageMimeTypes() {
        this.#allowMimeTypes.push(...imageMimeTypes);
        this.#fileSize = 5 * 1_024 * 1_024;
        return this;
    }

    setAllowVideoMimeTypes() {
        this.#allowMimeTypes.push(...videoMimeTypes);
        this.#fileSize = 50 * 1_024 * 1_024;
        return this;
    }

    setPath(path) {
        this.#path = path;
        return this;
    }

    build() {
        return multer({
            fileFilter: this.#fileFilter,
            storage: multerS3({
                s3: this.#s3,
                bucket: "Your bucket",
                contentType: multerS3.AUTO_CONTENT_TYPE,
                key: (req, file, done) => {
                    const splitFileName = file.originalname.split('.');
                    const extension = splitFileName.at(splitFileName.length - 1);
                    const realName = Buffer.from(file.originalname, "latin1").toString("utf8");

                    return done(null, encodeURI(`${this.#path}/${splitFileName[0]}.${extension}`));
                }
            }),
            limits: {
                fileSize: this.#fileSize
            }
        })
    }
}
// router
import { UploadController } from "../controllers/upload.controller.js";
import { MulterBuilder } from "../builder/multer-builder.js";

const uploadRouter = Router();

const imageUpload = new MulterBuilder().setAllowImageMimeTypes().setPath("images").build().single("file");
const videoUpload = new MulterBuilder().setAllowVideoMimeTypes().setPath("vidoes").build().single("file");

uploadRouter.post("/images", imageUpload, uploadController.imageUpload);
uploadRouter.post("/videos", videoUpload, uploadController.videoUpload);

export { uploadRouter }

 

 

평소 디자인 패턴은 면접 자리에서나 언급되는 이론적인 내용인 줄 알았다.

이번에 패턴을 직접 사용해 봄으로써 디자인 패턴이 왜 소프트웨어에 자주 발생하는 문제들에 대한 일반적인 해결책이라고 불리는지에 대해 알게 되었다. 이런 양질의 코드들이 모여 확장성과 고가용성을 갖춘 고 수준의 프로그램이 되는 게 아닐까.

 

 

참고 자료

https://github.com/expressjs/multer/blob/master/doc/README-ko.md

https://kscodebase.tistory.com/m/620