이번 포스팅에서는 프로젝트 구조설계에서 가장 중요한 계층화, 특히 Express에 맞추어 한번 정리해보고자 한다.
각각의 컴포넌트는 웹, 로직, 데이터 접근코드(DAL) 등을 위한 계층을 분리해야한다. 이것은 예상되는 버그를 잡고, 개발자 입자에서 깨끗하게 역할이 분담되며 모의 객체(mock) 를 만들어 단위 테스트 과정에서 굉장히 유용하다.
router.get('/:banner_id', jwt.graphql.isLoggedIn, async (req, res) => {
try {
///////////// 1. client에서 넘어오는 데이터 처리
const { banner_id } = req.params;
///////////// 2. DB에서 원하는 데이터 조회
var transaction = await sequelize.transaction();
const banners = await Banner.findOne({
where: {
id: banner_id
},
include: [
{
model: Banner_sub_img,
order: [
['order', 'ASC']
]
}
],
transaction
});
await transaction.commit();
///////////// 3. client에 response 전송
res.status(200).json(response.success(resMessage.READ_SUCCESS, banners));
} catch (err) {
error_handling.normal(err, res, transaction);
}
});
위와 같이 Get 메소드의 처리를 Express 의 router와 콜백함수 형태를 섞어버리는 것은 최악이다. 일반적인 패턴임에도 불구하고, API 서버 개발자들은 Express req, res 를 비즈니스 로직과 뒤섞어 버리는 경우가 많다.
이것은 어플리케이션 내부 기능들에 서로 의존성을 만들고 Express 를 통해서만 접근이 가능하게하는 단점이 있다. 테스트도 postman을 이용하는 것 외에는 불가능하며(직접 작성하기 까다롭다), 수정시에도 귀찮다는 단점이 있다. Cron 작업의 경우에서 그렇다.
3 layer architecture는 비즈니스 로직을 분리하는 것을 목적으로 하며 Controller, Service Layer, Data Access Layer 라는 세개의 층으로 나뉜다.
Controller : client와 통신에서 필요한 req, res를 처리
Service : 비즈니스 로직 처리
Data Access Laer : JPA, ORM, MYBATIS.. 데이터베이스 통신 처리
위 3가지 단계를 나눔으로서의 가장 큰 장점은 확장성이다. 레이어 별로 분리하면 언제든지 필요에 따라 독립적으로 크기를 조정하거나 수정할 수 있다는 장점이 있다.
route.post('/',
validators.userSignup, // this middleware take care of validation
async (req, res, next) => {
// ... The actual responsability of the route layer.
const userDTO = req.body;
// ... Call to service layer.
const { user, company } = await UserService.Signup(userDTO);
// ... Return a response to client.
return res.json({ user, company });
});
참고로 이전 포스팅에서 언급한 Express 와 로직의 분리를 위해 req.body를 userDTO로 변경했다.
서비스 계층은 나머지 애플리케이션에서 모든 비즈니스 로직을 캡슐화하고 추상화한다.
해야할 것
비즈니스 로직 포함
데이터 액세스 계층을 활용해서 데이터베이스와 상호 작용
Controller 계층에 전달할 데이터를 리턴
하지 말아야 것
req 직접 활용
클라이언트에 대한 res
데이터베이스 접근
module.exports = {
readAll: () => {
return new Promise(async (resolve, reject) => {
// ... User DAL methods
const city = await City.findAll({});
if(city.length == 0) {
resolve({
json: utils.successFalse(sc.NO_CONTENT, rm.CITY_EMPTY)
});
return;
}
if (!city) {
resolve({
json: utils.successFalse(sc.INTERNAL_SERVER_ERROR, rm.CITY_READ_ALL_FAIL)
});
return;
}
resolve({
json: utils.successTrue(sc.SUCCESS, rm.CITY_READ_ALL_SUCCESS, city)
});
});
}
}