Lsiron

HELLO FOLIO(express, mongoose)- 리팩토링 ( 이미지 업로드, 삭제 기능) 본문

개발일지/HELLO FOLIO

HELLO FOLIO(express, mongoose)- 리팩토링 ( 이미지 업로드, 삭제 기능)

Lsiron 2024. 8. 14. 08:00

대표적으로 최근에 포스팅 한, 이미지 업로드, 삭제 기능을 리팩토링 해 보겠다.

 

먼저, 초기 코드에서는 이미지 업로드와 삭제 기능이 myPageRouter.js 파일 안에 직접 구현되어 있었다.

 

이 코드 구조는 작은 프로젝트에서는 문제되지 않을 수 있지만, 프로젝트가 커지면서 유지보수가 어려워질 수 있다.

 

따라서 service와 controller 그리고 router로 나눠보려고 한다.

 

초기 코드의 문제점.

  • 관심사의 분리 부족: 업로드와 삭제 로직이 라우터와 함께 존재하므로, 코드가 복잡해지고 읽기 어려웠다.
  • 중복 코드: 비슷한 로직이 여러 곳에 반복되어 코드 중복이 발생했다.
  • 테스트 어려움: 모든 로직이 한 파일에 모여 있어, 개별적인 테스트가 어렵다.

리팩토링의 주요 목표는 다음과 같다.

  • 코드 구조 개선: 관심사를 분리하여 모듈화된 코드를 작성하고, 각 기능을 독립적인 서비스로 분리한다.
  • 재사용성 증가: 코드 중복을 제거하고, 재사용 가능한 함수를 작성한다.
  • 유지보수성 향상: 코드의 가독성을 높이고, 기능 추가와 수정이 용이하도록 한다.

현재 폴더 구조

web_project/
├── db/
│   ├── model             # 각종 모델 폴더
│   ├── schemas           # 각종 스키마 폴더
│   └── index.js          # db 연결파일
├── middleware/           # 각종 middleware 폴더
├── passport/	          # 로그인 관련 passport 폴더
│   ├── strategies        # 전략 구성
│   └── index.js          # 전략 exports 파일
├── routes/               # 라우트 관련 폴더
│   └── index.js     	  # 라우트 exports 파일
├── services/             # 유저 관련 service 폴더
├── controllers/          # 유저 관련 controller 폴더
├── views/                # EJS 템플릿 폴더
├── public/               # static 파일 폴더
│   ├── css/              # CSS 파일들
│   ├── js/               # JS 파일들
│   └── images/           # 이미지 파일들
├── .env                  # 환경 변수 파일
├── .gitignore            # Git 무시 파일
├── package.json          # 프로젝트 메타데이터 및 종속성 목록
├── app.js                # 애플리케이션 진입점
├── index.js              # 서버 실행 파일
└── README.md             # 프로젝트 설명 파일

현재 설치한 모듈

  "dependencies": {
    "bcrypt": "^5.1.1",
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.4.5",
    "ejs": "^3.1.10",
    "express": "^4.19.2",
    "jsonwebtoken": "^9.0.2",
    "mongodb": "^6.7.0",
    "mongoose": "^8.4.3",
    "nodemailer": "^6.9.13",
    "passport": "^0.7.0",
    "passport-jwt": "^4.0.1",
    "passport-local": "^1.0.0",
    "@aws-sdk/client-s3": "^3.598.0",
    "multer": "^1.4.5-lts.1",
    "multer-s3": "^3.0.1",
  },

app.js

const express = require('express');                     
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport');				

const { userRouter, myPageRouter } = require("./routes");		               

require('./passport')();                                           
const authMiddleware = require('./middleware/authMiddleware');       
app.use(express.json());							
app.use(express.urlencoded({ extended: false }));	
app.use(cookieParser());                               

app.use(passport.initialize());					

app.use('/', userRouter);
app.use('/', resetPwRouter);				
app.use('/my-page', authMiddleware ,myPageRouter); 

module.exports = { app };

.env

# DB 접속 URL
DB_URL="디비주소"
# DB 프로젝트 이름
DB_NAME="디비이름"
# 서버 포트 번호
PORT=3000
# JWT토큰 발급 키
SECRET="secret"
# REFRESH 토큰 발급 키
REFRESH_SECRET="toosecret"
# 비밀번호 찾기 이메일 발송메일
EMAIL_USER=내이메일
# 비밀번호 찾기 이메일 비밀번호
EMAIL_PASS=내비번
# aws에서 발급받은 엑세스 키
S3_KEY="엑세스 키"    
# aws에서 발급받은 시크릿 키
S3_SECRET="시크릿 키"

 

(1) 서비스 계층 추가: userCardService.js

 

먼저, AWS S3와 관련된 이미지 업로드와 삭제 로직을 서비스 계층인 userCardService.js로 옮겼다.

 

이를 통해 S3 관련 로직을 분리하고, 다른 곳에서도 쉽게 재사용할 수 있게 되었다.

const { S3Client, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const multer = require('multer');
const multerS3 = require('multer-s3');
const UserModel = require('../db/model/userModel');

const s3 = new S3Client({
  region: 'ap-northeast-2',
  credentials: {
    accessKeyId: process.env.S3_KEY,
    secretAccessKey: process.env.S3_SECRET,
  },
});

const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'elicewebproject',
    key: function (req, file, cb) {
      cb(null, Date.now().toString());
    }
  })
});

const uploadImage = async (req) => {
  const imageUrl = req.file.location;
  await UserModel.findOneAndUpdate(
    { email: req.user.email },
    { imageUrl: imageUrl },
    { new: true }
  );
};

const deleteImage = async (email) => {
  const user = await UserModel.findOne({ email });
  if (!user.imageUrl) {
    const error = new Error('ImageNotFoundError');
    error.statusCode = 404;
    throw error;
  }
  const key = user.imageUrl.split('/').pop();
  const deleteParams = {
    Bucket: 'elicewebproject',
    Key: key,
  };
  await s3.send(new DeleteObjectCommand(deleteParams));
  await UserModel.findOneAndUpdate(
    { email },
    { imageUrl: '' },
    { new: true }
  );
};

module.exports = {
  upload,
  uploadImage,
  deleteImage,
};
  • 관심사의 분리: S3 관련 로직이 서비스 계층으로 이동하여, 라우터와 로직이 분리되었다.
  • 재사용성 증가: 동일한 기능이 여러 곳에서 필요할 경우, 서비스 메서드를 호출하기만 하면 된다.

(2) 컨트롤러 계층 추가: userCardController.js

 

다음으로, 컨트롤러 계층을 추가하여 HTTP 요청을 처리하는 로직을 분리했다.

 

컨트롤러는 클라이언트의 요청을 받아 적절한 서비스를 호출하고, 그 결과를 반환한다.

const userCardService = require('../services/userCardService');

const uploadImage = async (req, res, next) => {
  try {
    await userCardService.uploadImage(req);
    res.status(200).json({ message: 'Image uploaded successfully' });
  } catch (err) {
      next(err); // 미들웨어로 에러 넘기기
  }
};

const deleteImage = async (req, res, next) => {
  try {
    await userCardService.deleteImage(req.user.email);
    res.status(200).json({ message: 'Image deleted successfully' });
  } catch (err) {
    if (err.statusCode === 404) {
      res.status(404).json({ message: 'Image not found' });
    } else {
      next(err); // 미들웨어로 에러 넘기기
    }
  }
};

module.exports = {
  upload: userCardService.upload,
  uploadImage,
  deleteImage,
};
  • 단일 책임 원칙: 컨트롤러는 요청을 처리하는 데만 집중하고, 비즈니스 로직은 서비스 계층에 위임한다.
  • 유지보수 용이성: 각 기능이 독립적인 모듈로 나뉘어 있어, 수정이 필요할 때 변경 범위가 제한적이다.

(3) 리팩토링된 라우터: myPageRouter.js

 

마지막으로, 라우터에서는 서비스나 로직에 대한 직접적인 참조를 제거하고, 컨트롤러를 호출하도록 수정했다.

const { Router } = require('express');
const router = Router();
const userCardController = require('../controllers/userCardController');
const userCardService = require('../services/userCardService');

router.post('/upload', userCardController.upload.single('profileImage'), userCardController.uploadImage);

router.delete('/delete', userCardController.deleteImage);

module.exports = router;
  • 가독성 향상: 라우터는 경로와 관련된 설정만 포함하여 가독성이 좋아졌다.
  • 확장성: 새로운 기능을 추가할 때 구조를 쉽게 확장할 수 있다.

(4) 에러 처리 미들웨어: app.js

 

서비스나 컨트롤러에서 처리되지 않은 에러를 중앙에서 처리하기 위해 에러 처리 미들웨어를 설정했다.

const express = require('express');                     
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport');				

const { userRouter, myPageRouter } = require("./routes");		               

require('./passport')();                                           
const authMiddleware = require('./middleware/authMiddleware');       
app.use(express.json());							
app.use(express.urlencoded({ extended: false }));	
app.use(cookieParser());                               

app.use(passport.initialize());					

app.use('/', userRouter);
app.use('/', resetPwRouter);				
app.use('/my-page', authMiddleware ,myPageRouter); 

// 에러 처리 미들웨어
app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({
    message: err.message || 'Internal Server Error',
  });
});

module.exports = { app };

 

이번 리팩토링 작업을 통해 코드의 구조가 훨씬 명확해지고, 유지보수와 확장이 용이해졌다.

 

특히, 관심사의 분리와 모듈화를 통해 각 기능이 독립적으로 동작하며, 다른 곳에서도 쉽게 재사용할 수 있게 되었다.