이번 프로젝트에서 내가 담당했던 업장 CRUD를 전에 TIL에서 설명하였던 3계층 아키텍쳐 형식으로 분할하고 작동까지 성공하였다. 자세한 내용을 지금부터 서술하겠다.
일단 CRUD에 대한 기본적인 API명세서는 이렇게 된다. owner 즉 사장님으로 로그인하여야 업장에 접근할 권한이 부여되고 로그인한 상태에서 업장 등록과, 조회, 수정과 삭제가 가능하다.
파일은
업장과 관련된 api파일은 총 4가지로 구성되어 있으며
src/repositories/restaurant.repository.js 파일에서 디비로 접근하여 crud를 구현하는 로직을 담당한다.
src/services/restaurant.service.js 파일로 비지니스 로직을 담당한다.
src/controllers/restaurant.controller.js 파일에서 요청과 응답 로직을 구현하게 된다.
마지막으로
src/routers/restaurant.router.js 파일에서 기본 경로들과 사용하는 미들웨어 등을 담당하게 된다.
기존에는 라우터 파일 하나로 모든 crud를 구성하였는데, 이게 작은 프로젝트에서는 api자체가 많지 않을 뿐더러, 관리와 수정에 큰 문제가 없기 때문에 굳이, 3계층 아키텍쳐를 사용하지 않아도 되지만, 이제 우리는 팀프로젝트 하나와 최종 프로젝트 총 2가지의 프로젝트를 앞두고 있기 때문에 실무에서도 충분히 활용할 수 있고 코딩을 어떻게 해야 유지보수가 간편한지에 대하여 경험을 쌓아둘 필요가 있기 때문에 이렇게 해당 아키텍쳐를 이용하는 연습을 계속 하여야한다.
import express from 'express';
import restaurantsController from '../controllers/restaurants.controller.js';
import { createRestaurantValidator } from '../middlewares/validators/create-restaurant-validator.middleware.js';
import { updateRestaurantValidator } from '../middlewares/validators/update-restaurant-validator.middleware.js';
import { requireAccessToken } from '../middlewares/authorization.middleware.js';
const restaurantRouter = express.Router();
restaurantRouter.post(
'/owners/me/restaurants',
requireAccessToken,
createRestaurantValidator,
restaurantsController.postRestaurant,
);
restaurantRouter.get(
'/owners/me/restaurants',
requireAccessToken,
restaurantsController.getOwnerRestaurant,
);
restaurantRouter.patch(
'/owners/me/restaurants',
requireAccessToken,
updateRestaurantValidator,
restaurantsController.updateRestaurant,
);
restaurantRouter.delete(
'/owners/me/restaurants',
requireAccessToken,
restaurantsController.deleteRestaurant,
);
export default restaurantRouter;
이건 라우터 파일이다. 라우터에서는 api를 호출할때 어떤 url에서 어떠한 방식으로 호출할지 정해주는 파일이다. 해당 경로에 들어오면 일단 인증미들웨어를 거쳐서 본인에게 접근 권한이 있는지 먼저 판단한다. 후에 입력되는 값이 있다면 joi라이브러리를 활용하여 유효성검사를 진행하게 되고 여기까지 통과가 완료되면 본격적으로 컨트롤러 파일에 들어가서 로직을 진행하게 된다.
import restaurantsService from '../services/restaurants.service.js';
class RestaurantController {
#service;
constructor(service) {
this.#service = service;
}
postRestaurant = async (req, res) => {
const ownerId = parseInt(req.user.ownerId);
const { address, phoneNumber, restaurantName, restaurantType, totalPoint } =
req.body;
try {
const data = await this.#service.postRestaurant({
ownerId,
address,
phoneNumber,
restaurantName,
restaurantType,
totalPoint,
});
return res
.status(201)
.json({ message: '업장 등록에 성공하였습니다!', data: data });
} catch (err) {
if (err.message === '사장님을 찾을 수 없습니다.') {
return res.status(404).json({ message: err.message });
}
if (err.message === '업장은 한개만 등록 가능합니다.') {
return res.status(403).json({ message: err.message });
}
next(err);
}
};
getOwnerRestaurant = async (req, res) => {
const ownerId = parseInt(req.user.ownerId);
try {
const data = await this.#service.getOwnerRestaurant({ ownerId });
return res
.status(200)
.json({ message: '업장을 조회하였습니다', data: data });
} catch (err) {
if (err.message === '업장을 찾을 수 없습니다.') {
return res.status(404).json({ message: err.message });
}
next(err);
}
};
updateRestaurant = async (req, res) => {
const ownerId = parseInt(req.user.ownerId);
const { address, phoneNumber, restaurantName, restaurantType } = req.body;
try {
const getRestaurant = await this.#service.getOwnerRestaurant({ ownerId });
const { restaurantId } = getRestaurant;
const data = await this.#service.updateRestaurant({
ownerId,
restaurantId,
address,
phoneNumber,
restaurantName,
restaurantType,
});
return res
.status(201)
.json({ message: '업장이 수정되었습니다.', data: data });
} catch (err) {
if (err.message === '업장을 찾을 수 없습니다.') {
return res.status(404).json({ message: err.message });
}
if (err.message === '업장 등록자가 아닙니다') {
return res.status(404).json({ message: err.message });
}
next(err);
}
};
deleteRestaurant = async (req, res) => {
const ownerId = parseInt(req.user.ownerId);
try {
const getRestaurant = await this.#service.getOwnerRestaurant({ ownerId });
const { restaurantId } = getRestaurant;
const data = await this.#service.deleteRestaurant({
ownerId,
restaurantId,
});
return res
.status(201)
.json({ message: '업장이 삭제 되었습니다.', data: data });
} catch (err) {
if (err.message === '업장을 찾을 수 없습니다.') {
return res.status(404).json({ message: err.message });
}
if (err.message === '업장 등록자가 아닙니다') {
return res.status(404).json({ message: err.message });
}
next(err);
}
};
}
export default new RestaurantController(restaurantsService);
컨트롤러 파일이다 해당 파일에서는 요청과 응답을 조절한다. post부분을 살펴보면 인증로직을 거쳐 req.user.ownerId에 접근하여 사장님의 id값을 가져온다. 그리고 바디값으로 업장과 관련된 부분을 입력을 받고 해당 데이터들을 service.js 파일에 보내게 된다.
import restaurantsRepository from '../repositories/restaurants.repository.js';
class restaurantsService {
#repository;
constructor(repository) {
this.#repository = repository;
}
postRestaurant = async (data) => {
const owner = await this.#repository.findOwner(data);
const ownerRestaurant = await this.#repository.findOwnerRestaurant(data);
if (!owner) {
throw new Error('사장님을 찾을 수 없습니다.');
}
if (ownerRestaurant) {
throw new Error('업장은 한개만 등록 가능합니다.');
}
return await this.#repository.postRestaurant(data);
};
getOwnerRestaurant = async (data) => {
const ownerRestaurant = await this.#repository.findOwnerRestaurant(data);
if (!ownerRestaurant) {
throw new Error('업장을 찾을 수 없습니다.');
}
return ownerRestaurant;
};
updateRestaurant = async (data) => {
const findRestaurant = await this.#repository.findRestaurant(data);
if (!findRestaurant) {
throw new Error('업장을 찾을 수 없습니다.');
}
if (parseInt(data.ownerId) !== findRestaurant.ownerId) {
throw new Error('업장 등록자가 아닙니다');
}
return await this.#repository.updateRestaurant(data);
};
deleteRestaurant = async (data) => {
const findRestaurant = await this.#repository.findRestaurant(data);
if (!findRestaurant) {
throw new Error('업장을 찾을 수 없습니다.');
}
if (parseInt(data.ownerId) !== findRestaurant.ownerId) {
throw new Error('업장 등록자가 아닙니다');
}
return await this.#repository.deleteRestaurant(data);
};
}
export default new restaurantsService(restaurantsRepository);
이곳은 service.js 파일이다 이 곳에서는 중요한 비지니스로직을 전담한다. 아까 위에서 post 업장등록 라우터를 사용하였을때 데이터를 받고 해당데이터를 이곳에서도 사용한다 postRestaurant 매서드 부분을 확인하면 해당 데이터들을 사용하여 repository파일로 보내고 그곳에서 얻은 값을 활용하여 만약 그 값이 다르다면 error로 던지는 등 의 로직을 수행한다.
import { prisma } from '../utils/prisma/index.js';
class RestaurantRepository {
#orm;
constructor(orm) {
this.#orm = orm;
}
// 사장님 정보 찾기
findOwner = async (data) => {
return await this.#orm.owner.findUnique({
where: { ownerId: parseInt(data.ownerId) },
});
};
// 레스토랑 찾기!
findRestaurant = async (data) => {
return await this.#orm.restaurant.findUnique({
where: { restaurantId: parseInt(data.restaurantId) },
});
};
// 사장님 업장 찾기
findOwnerRestaurant = async (data) => {
return await this.#orm.restaurant.findFirst({
where: { ownerId: parseInt(data.ownerId) },
});
};
// 업장 등록하기
postRestaurant = async (data) => {
return await this.#orm.restaurant.create({
data: {
ownerId: data.ownerId,
address: data.address,
phoneNumber: data.phoneNumber,
restaurantName: data.restaurantName,
restaurantType: data.restaurantType,
totalPoint: data.totalPoint,
},
});
};
// 레스토랑 수정(업데이트)
updateRestaurant = async (data) => {
return await this.#orm.restaurant.update({
where: { restaurantId: parseInt(data.restaurantId) },
data: {
address: data.address,
phoneNumber: data.phoneNumber,
restaurantName: data.restaurantName,
restaurantType: data.restaurantType,
},
});
};
// 레스토랑 삭제
deleteRestaurant = async (data) => {
return await this.#orm.restaurant.delete({
where: { restaurantId: parseInt(data.restaurantId) },
});
};
}
export default new RestaurantRepository(prisma);
마지막 repository파일이다. 해당 파일에서는 직접 db에 접근하는 로직을 수행한다. 아까 위의 서비스파일에서 업장 등록 부분에서 사용하는 레포지토리 매서드는 총 두개로 첫번째로 해당 사장님이 실제로 데이터베이스에 존재 되어있는지 확인하는 로직과 그렇다면 그 사장님이 업장을 가지고 있는지 확인하고 만약 가지고있는 업장이 없다면 새로운 업장을 만들게 되는 로직이다.
이런식으로 3계층 아키텍쳐를 활용하여 업장등록 crud를 구성해보았다. 3계층으로 나누는게 처음에는 이해가 잘 안되고 접근하기가 어려웠는데 한두개를 진행하면 눈이 트이고 세네개를 만들어보면 생각보다 어렵지 않다는 것을 느낄 수 있다.
이제 다음 팀 프로젝트와 최종 프로젝트에서도 이러한 아키텍쳐 패턴을 구상하여 다양한 아키텍처 패턴 중에 우리가 사용할 수 있는 패턴과 효율적인 패턴을 찾아 적용하는 방식으로 진행하면 더욱 깔끔한 코딩이 탄생될 것 같다.
'내일배움캠프 TIL' 카테고리의 다른 글
HTTP, HTTPS, SSL, TLS 등 개념 본캠프 TIL 01/15 (0) | 2025.01.15 |
---|---|
CPU의 기본이해 본캠프 TIL 01/13 (0) | 2025.01.13 |
Joi라이브러리 사용법과 사용하는 이유 본캠프 TIL 01/09 (0) | 2025.01.09 |
OSI 7계층에서 네트워크 계층 본캠프 TIL 01/08 (0) | 2025.01.08 |
Layered Architecture Pattern 이란? 본캠프 TIL 01/07 (0) | 2025.01.07 |