본문 바로가기
개발/차근차근 개발일지 TIL

TIL 230501_Layered architecture pattern 적용하기1

by 코딩하는짱구 2023. 5. 1.
반응형

✅오늘 학습 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를 이용한 프로젝트에서 각 단계별로 데이터가 어떤 과정을 거쳐 전송되는지에 대해 더 꼼꼼히 파악하며 진행해야겠다. 

반응형