✅오늘 학습 Keyword
3계층 아키텍쳐
1. Controller : 요청, 응답처리
2. Service: 비즈니스로직이 수행되는 부분, 요구사항을 처리하는 중심 부분이기 떄문에 현업에서는 서비스코드가 비대해진다
3. Repository : DB와 맞닿아 있는 제일 안쪽 부분
Layered Architecture 의 플로우
1. 클라이언트가 요청을 보냄
2. 요청을 controller가 받음
3. controller가 service를 호출
4. service는 repository에 데이터를 요청, 가공 후 controller에게 넘김
6. controller가 service의 res를 클라이언트에게 전달
//각각 다른 차원이라고 생각하지말고, 페이지만 다를 뿐 수행하는 역할이 다르다고 생각해야 이해가 쉬웠다.
✅오늘 겪은 문제 및 해결
1. 기존에 있던 router를 controller, service, repository로 나누는것 부터 쉽지 않았다.
a. routes에 왜 index파일이 추가되는가?
기존에 app.js 각 라우터들을 연결해주었다면 지금은 app.js와 index.route.js를 연결하고, index에서 router들을 관리한다.
라우터에서 요청이 오면->api(app.js)로 들어가->index에서 해당 라우터로 연결->controller 순으로 이해했는데, 이 부분은 실습을 진행하면서 더 확실히 할 예정이다.
b.처음에는 signup routes의 post 부분에 경로를 '/signup'으로 지정했는데 에러가 발생했다.
그렇게 되면 결국 /api/signup/signup이 되기에 처음 요청을 받을때는 '/'기본 값으로 설정해줘야 한다.
//순서대로 실행된다는 것을 인지하자!!
//signup.routes.js
const express = require('express');
const router = express.Router();
const SignupController = require('../controllers/signup.controller');
const signupController = new SignupController();
//api로 들어와서
router.post('/', signupController.createSignup);
module.exports = router;
//app.js
const express = require('express');
const app = express();
const port = 3000;
// const postsRouter = require('./routes/posts.routes');
const router = require('./routes');
app.use(express.json());
app.use('/api', router);
app.listen(port, () => {
console.log(port, '포트로 서버가 열렸어요!');
});
//routes에 index.js가 갖는 의미가 뭔지?
//index.js
const express = require('express');
const router = express.Router();
const postsRouter = require('./posts.routes');
const signupRouter = require('./signup.routes');
router.use('/posts/', postsRouter);
router.use('/signup/', signupRouter);
module.exports = router;
2. 계층구조에서의 에러핸들링
a. 회원가입 api에서 발생할 수 있는 각 에러들을 어떤 계층에서 핸들링 할지가 막막했다.
DB에 직접 관여하는 에러들은 service로 보내고, 단순히 body data형식에 관한 문제는 controller로 보냈다.
//signup.controller.js
const SignupService = require('../services/signup.service');
const myError = require('../utils/error');
// Signup의 컨트롤러(Controller)역할을 하는 클래스
class SignupController {
signupService = new SignupService(); // Post 서비스를 클래스를 컨트롤러 클래스의 멤버 변수로 할당합니다.
createSignup = async (req, res, next) => {
try {
const { nickname, password, confirm } = req.body;
//필요한 데이터가 입력되었는지
if (!nickname || !password || !confirm) {
throw myError(400, '모든 필드는 필수값 입니다.');
}
// 서비스 계층에 구현된 createSignup 로직을 실행합니다.
const createsignUpdata = await this.signupService.createSignup(
nickname,
password,
confirm
);
res.status(201).json({ data: createsignUpdata });
} catch (err) {
res.status(err.statusCode).json({ errormessage: err.message });
}
};
}
module.exports = SignupController;
//signup.service.js
const SignupRepository = require('../repositories/signup.repository');
//실제로 db를 끌어다 쓰기때문에 repository를 호출한다.
const { Users } = require('../models');
const myError = require('../utils/error');
class SignupService {
signupRepository = new SignupRepository();
createSignup = async (nickname, password, confirm) => {
//데이터 조건 검사
// 닉네임 길이 제한
if (nickname.length < 3) {
throw myError(412, '닉네임 형식이 일치하지 않습니다.');
}
// 닉네임 형식
const nicknameRegex = /^[a-zA-Z0-9]+$/;
if (!nicknameRegex.test(nickname)) {
throw myError(412, '닉네임 형식이 일치하지 않습니다.');
}
//사용자가 이미 존재하는지 찾을꺼야
const existingUser = await this.signupRepository.findUser(nickname);
if (existingUser) {
throw myError(412, '이미 존재하는 사용자입니다.');
}
// 닉네임 4자리 이상, 닉네임과 같은 값 포함
if (password.length < 4 || password.includes(nickname)) {
throw myError(412, '비밀번호 형식이 일치하지 않습니다.');
}
// 비번 재확인
if (password !== confirm) {
throw myError(412, '비밀번호가 일치하지 않습니다.');
}
// 저장소(Repository)에게 저장될 데이터를 요청합니다.
const createSignUpdata = await this.signupRepository.createSignup(
nickname,
password
);
// 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
return {
userId: createSignUpdata.userId,
nickname: createSignUpdata.nickname,
password: createSignUpdata.password,
createdAt: createSignUpdata.createdAt,
updatedAt: createSignUpdata.updatedAt,
};
};
}
module.exports = SignupService;
3. 기존에 했던 것 처럼 각 라우터 각 단계에서 error를 직접 입력하여 출력할 수 없게되었다.
그렇다면? try catch throw를 통해 모든 에러를 controller에서 잡아가도록 구현했다.
가장 큰 이유는 service에서 res로 error코드를 반환할 수 없기에 throw로 해야하는데, 그렇게 되면 에러 status를 명시할 수 없고, try catch문을 쓰게되면 controller, service 두 계층에서 try catch 문을 반복하게 되므로 비효율적이다.
즉 service 비즈니스 로직에서 발생할 수 있는 에러들을 모두 controller로 throw하여 controller에서 catch하게 만든 것. 그렇게 하기 위해 전역에서 쓰일 에러출력함수를 아래와같이 따로 만들었다.
아래와 같이 에러를 출력할 형식을 만들어주고 필요한 곳에서 끌어다 쓰는 방식.
//코드랑 에러내용이 들어가야댐
const myError = (statusCode, message) => {
let error = new Error(message);
error.statusCode = statusCode;
return error;
};
module.exports = myError;
4.나는 service에서 db에 접근하여 사용자가 이미 존재하는지 찾아보고, 존재한다면 에러를, 존재하지 않는다면 다음 조건으로 넘어가고싶었고 유저를 찾는 함수를 service에 구현하였는데, 그렇게 하면 계층을 나누는 것의 의미가 퇴색되는 것이므로 함수를 repo에 구현하였다.
이 부분을 이해하기 좀 힘들었는데, 결국 db를 직접적으로 만지는건 모두 repo에서 하겠다는 것이다.
회원가입 api에서 db에 수행할 작업은 저장, 찾아오기 이므로 아래와 같이 코드를 짰다.
즉 코드가 작성된 페이지만 다를 뿐 내가 서비스에서 user를 찾아오게끔 만들고(repo에서 찾아올 user를 저장할 변수만 지정), 함수의 직접 실행은 repo에서 하는 것.
const { Users } = require('../models');
class SignupRepository {
createSignup = async (nickname, password) => {
// ORM인 Sequelize에서 Posts 모델의 create 메소드를 사용해 데이터를 요청합니다.
const createSignupData = await Users.create({
nickname,
password,
});
return createSignupData;
};
findUser = async (nickname) => {
const user = await Users.findOne({ where: { nickname } });
return user;
};
}
module.exports = SignupRepository;
//여기서 진행되는애들은 db에 저장이 되므로 confirm은 필요가 없다.
5. 사용자가 이미 있는지 확인하는 조건을 맨 위에 걸게 되면, 아래 닉네임의 조건에 부합하지 않는 값을 입력해도 그 값을 모든 데이터와 대조할 것이다. 즉 쓸모없는 작업을 한 후에 다른 조건으로 넘어가는 것이라 비효율 적임.
아래와 같이 먼저 닉네임 형식부터 검증한 후에 이미 존재하는 닉네임을 확인하는 것으로 수정.
// 닉네임 길이 제한
if (nickname.length < 3) {
throw myError(412, '닉네임 형식이 일치하지 않습니다.');
}
// 닉네임 형식
const nicknameRegex = /^[a-zA-Z0-9]+$/;
if (!nicknameRegex.test(nickname)) {
throw myError(412, '닉네임 형식이 일치하지 않습니다.');
}
//사용자가 이미 존재하는지 찾을꺼야
const existingUser = await this.signupRepository.findUser(nickname);
if (existingUser) {
throw myError(412, '이미 존재하는 사용자입니다.');
}
// 닉네임 4자리 이상, 닉네임과 같은 값 포함
if (password.length < 4 || password.includes(nickname)) {
throw myError(412, '비밀번호 형식이 일치하지 않습니다.');
}
// 비번 재확인
if (password !== confirm) {
throw myError(412, '비밀번호가 일치하지 않습니다.');
}
// 저장소(Repository)에게 저장될 데이터를 요청합니다.
const createSignUpdata = await this.signupRepository.createSignup(
nickname,
password
);
✅학습하며 느낀 점
3계층 아키텍쳐라는 개념이 생각보다 정~~말 이해하기 어렵다.
3계층 아키텍쳐, sequelize를 이용한 프로젝트에서 각 단계별로 데이터가 어떤 과정을 거쳐 전송되는지에 대해 더 꼼꼼히 파악하며 진행해야겠다.