Lsiron
HELLO FOLIO(express, mongoose)- 리팩토링 ( 이미지 업로드, 삭제 기능) 본문
대표적으로 최근에 포스팅 한, 이미지 업로드, 삭제 기능을 리팩토링 해 보겠다.
먼저, 초기 코드에서는 이미지 업로드와 삭제 기능이 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 };
이번 리팩토링 작업을 통해 코드의 구조가 훨씬 명확해지고, 유지보수와 확장이 용이해졌다.
특히, 관심사의 분리와 모듈화를 통해 각 기능이 독립적으로 동작하며, 다른 곳에서도 쉽게 재사용할 수 있게 되었다.