시작 계기
스터디를 목적으로 프로젝트를 진행하다 보면, 기능 구현에 더 많은 시간을 쏟는 바람에 자신도 모르게 가독성을 떨어지게 코드를 짤 때가 많다.
기능별로 분담을 해서 진행하는 소수 인원으로 구성된 프로젝트를 진행하거나, 개인 프로젝트를 진행하는 경우에 더욱 그렇다고 생각하는 것이
일단 나 자신만 해도 그랬기 때문이다.
이 프로젝트를 시작하기 전, 아키텍처에 관한 개념과 구성 방식을 접할 기회가 있었고, 이 방식이 그동안 해왔던 방식보다
보기에 더 깔끔하다고 생각했기 때문에 적용을 해보기로 결심했다.
아키텍처란 무엇인가?
단어 그대로의 의미는 건축학이라는 의미지만 여기서는 시스템, 소프트웨어 또는 컴퓨터 시스템의 구조 또는 설계의 의미를 가진다.
웹 / 앱의 서비스 구조, 구성요소, 데이터 흐름 및 처리 방식을 구조화하여 시스템의 유연성, 확장성, 유지 보수성을 향상 시키는데
도움을 줄 수 있는 것으로 알려져 있으며, 개인적으로는 서비스 내용을 보다 명확하게 이해하는데 큰 도움이 된다고 느꼈다.
예를 들어, 아래와 같이 회원 가입 / 로그인 서비스를 구현했다고 가정하자.
const express = require("express");
const usersRouter = require('./routes/users.js');
const sellersRouter = require('./routes/sellers.js');
const buyersRouter = require('./routes/buyers.js');
const app = express();
const port = 3000;
app.use(express.json());
app.use('/api', [usersRouter, sellersRouter, buyersRouter]);
app.listen(port, () => {
console.log(`Server listen at ${port}`);
})
// routes/users.js
const express = require("express");
const { Users } = require("../models");
const jwt = require("jsonwebtoken");
const router = express.Router();
// 회원가입
router.post('/join', async(req, res) => {
try {
const { userName, userType, password } = req.body;
const existUser = await Users.findOne({
where: { userName }
})
// 유저가 존재하는 경우
if (existUser) {
return res.status(401).json({ errorMessage: "존재하는 ID 입니다." });
}
const newUser = await Users.create({ userName, userType, password });
return res.status(200).json({ message: "회원가입이 완료되었습니다. " });
} catch (error) {
return res.status(400).json({ errorMessage: "회원 가입에 실패했습니다.", detail: error });
}
});
router.post("/login", async(req, res) => {
try {
const { userName, userType, password } = req.body;
const loginUser = await Users.findOne({
where: { userName, userType }
});
// 회원 정보가 없을 때, (id 없음 or 가입 유형 다름) + 비밀번호 불일치
if (!loginUser || password !== loginUser.password) {
return res.status(412).json({ errorMessage: "로그인 정보를 확인해주세요." });
}
const token = jwt.sign({ userId: loginUser.userId }, "scerect-key");
return res.status(200).json({ token: `Bearer ${token}`, userName: loginUser.userName, userType: loginUser.userType });
} catch (error) {
return res.status(400).json({ errorMessage: "로그인에 실패했습니다." });
}
})
module.exports = router;
위 코드에서는, api/join, api/login 의 url 에 접근했을 때 request body의 값을 받아와서 검증 및 DB 접근 등 서비스 구현에 필요한
모든 처리를 한 곳에서 진행한다는 특징이 있다.
위의 경우는 코드 양이 많지 않아 이해하기 쉬울 수 있으나, 여러 가지의 로직이 추가된다면, 어디까지가 유효성 관련 코드이고,
또 어디까지가 DB 관련 코드인지 한 눈에 보기 어려워진다. 또, 에러가 발생하는 경우에, 어느 부분에서 발생했는지 직관적으로 알기가 어렵다.
이런 문제를 해결하기 위해 서비스 로직을 세부적으로 쪼개는 아키텍처가 도입되었는데, 이를 구성하는 패턴은 다양하게 존재하지만
이 글에서는 학습한 내용인 3계층 아키텍처를 적용해 볼 예정이다.
3계층 아키텍처는 서비스 구성요소를 크게 3가지로 분리하여 관리한다는 의미로 이름이 붙었는데, 3가지 요소는 아래와 같이 구분한다.
- 요청이 들어왔을 때, 처리할 로직을 정하는 Controller
- Controller에 의해 비즈니스 로직을 처리하는 Services
- DB에 접근하는 Repositories
각 계층의 예를 들어보면 (회원 가입)
// routes/users.js
const express = require("express");
const router = express.Router();
const UsersController = require("../controllers/UsersController");
const usersController = new UsersController();
router.get('/join', usersController.userJoin);
module.exports = router;
routes 파일에서는 users와 관련된 로직을 처리하기 위해, 컨트롤러 폴더에서 관련 컨트롤러를 호출한 후
요청을받은 url에 따라 어떤 컨트롤러로 처리할지를 명시하고 있다.
다음은 Controller 에서는 request 에서 필요한 데이터를 추출한 뒤 하위 단계인 service 폴더 내 users.js 로 요청을 보낸다.
// controllers/users.js
const UsersService = require("../services/usersService");
class UsersController {
usersServeice = new UsersServeice();
userJoin = async(req, res, next) => {
const { userName, userType, password } = req.body;
const join = await this.UsersServeice.userJoin(userName, userType, password);
res.status(200).json({ data: "sucess" });
};
controller/users.js 파일 내에서는 service 폴더 내 users.js파일을 호출하고, userJoin 로직이 완료된 처리 결과를 반환한다.
마찬가지로, service 폴더 내 users.js 파일에서는 DB에 접근하는 Repositories 폴더 내 users.js 파일을 호출하고
회원 가입에 필요한 정보들 (추출한 데이터)을 전달한다.
// services/users.js
const UsersRepository = require("../repositories/usersRepositories");
class UsersService {
usersRepository = new UsersRepository();
userJoin = async(userName, userType, password) => {
const join = await this.UsersRepository.userJoin(userName, userType, password);
return join;
};
module.exports = UsersService;
마지막으로 Repositories/users.js 파일에서는 db에 접근해서 신청한 userName의 사용자가 있는지 등을 확인한 후
회원 가입 로직을 끝내고 메세지를 반환한다.
// /users.js
const { Users } = require("../models");
const jwt = require("jsonwebtoken");
// 회원가입
class UsersRepository {
userJoin = async(userName, userType, password) => {
try {
const existUser = await Users.findOne({
where: { userName }
})
// 유저가 존재하는 경우
if (existUser) {
return res.status(401).json({ errorMessage: "존재하는 ID 입니다." });
}
const newUser = await Users.create({ userName, userType, password });
return res.status(200).json({ message: "회원가입이 완료되었습니다. " });
} catch (error) {
return res.status(400).json({ errorMessage: "회원 가입에 실패했습니다.", detail: error });
}
});
위 예시의 경우, 모든 로직이 DB에서 확인하는 것으로 구성되어있기 때문에, Services/user.js 파일이 불필요해 보이지만,
DB 접근과는 무관한 로직, 분기 Or 전처리 등이 필요할 때 추가적인 코드를 추가할 때 사용한다.
이렇게 서비스를 분리하여 관리함으로써 각 서비스 파트를 파일로 구별하여 관리할 수 있어 다수의 사람들과 협업 및 서비스 관리가
용이하다는 장점이 있다.
'Backend > Backend 프로젝트' 카테고리의 다른 글
[클론 코딩] 스카이 스캐너(Skyscanner) 클론 코딩 - 후기 (0) | 2023.06.12 |
---|---|
[클론 코딩] 스카이 스캐너(Skyscanner) 클론 코딩 - 서비스 구조화2 (1) | 2023.06.07 |
[클론 코딩] 스카이 스캐너(Skyscanner) 클론 코딩 - 시작하기 (0) | 2023.05.30 |
[미니 프로젝트] 쇼핑몰 플랫폼 만들기 - 그 이후 (0) | 2023.05.25 |
[미니 프로젝트] 쇼핑몰 플랫폼 만들기 - 구매자용 API (0) | 2023.05.25 |