[Node.js] Controller, Service, Repository로 계층 분리하기

2024. 12. 22. 15:43·BE
728x90

로그인을 이용한  TODO를 구현하면서 다음과 같은 피드백을 받았습니다.

백엔드 코드 리뷰를 받고, Service와 Controller에 대해 알아보고 계층을 나눠보라는 코드 리뷰가 있어서 이번 기회를 통해 알아보고 계층별로 코드를 분리하는 시간을 가져보겠습니다 !

 

우선 기존의 폴더 구조는 최상단에 app.js가 있고 그 아래 routes 폴더에 login 로직이 들어있는 파일이 있습니다.

리팩토링 전 폴더구조

 

본론에 들어가기에 앞서 Service와 Controller, 나아가서 Repository로 분리하는 이유와 각각 어떤 역할을 하는지에 대해 알아보겠습니다.

Controller는 HTTP 요청과 응답을 처리하고 클라이언트와 상호작용하는 역할을 합니다.

Service는 Controller에 의해 비즈니즈 로직 즉, 핵심 로직을 처리하는 부분으로 Repository를 활용하여 DB와 상호작용합니다. 저의 코드의 경우에는 로그인 로직(인증)이 주 기능이므로 이 부분이 Service에 해당합니다.

Repository는 Service에 의해 호출되어 DB에서 데이터를 처리하여 반환하고 DB의 CRUD 작업을 담당합니다.

위 그림처럼, 로직을 분리하는 이유는 기존 코드를 한 파일에 작성하면 코드를 보는 이가 이해하기 힘들뿐더러 각각의 레이어가 어떤 작업을 하는지 알기 어렵기 때문입니다.

실제로 레이어를 분리하여 코드를 재작성해보니 어떤 역할을 하는지 명확해지는 장점이 있었습니다. 또 프런트엔드에서 다루지 않았던 SQL 쿼리문이 코드와 함께 있는 것이 코드를 읽을 때 불편했는데, Repository로 분리하고 가독성이 좋아졌습니다.

이밖에도 테스트 용이성이 있다고 하여 테스트 작성도 진행해 볼 예정입니다.

 

JWT를 이용한 기존 login 코드입니다.

const express = require("express");
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
const router = express.Router();
const db = require("../database/db");

dotenv.config();

router.post("/login", (req, res, next) => {
  const key = process.env.SECRET_KEY;
  if (!key) {
    return res.status(500).json({
      message: "서버 설정 오류: SECRET_KEY가 누락되었습니다.",
    });
  }

  const { id, password } = req.body;

  if (!id || !password) {
    return res.status(400).json({
      message: "아이디와 비밀번호를 입력하세요",
    });
  }

  const query = "SELECT id, password FROM users WHERE id = ?";
  db.get(query, [id], (err, user) => {
    if (err) {
      return res.status(500).json({
        message: "서버 에러",
      });
    }

    if (!user) {
      return res.status(404).json({
        message: "존재하지 않는 사용자입니다.",
      });
    }

    if (password !== user.password) {
      return res.status(401).json({
        message: "비밀번호가 일치하지 않습니다.",
      });
    }

    const token = jwt.sign(
      {
        type: "JWT",
        id: user.id,
      },
      key,
      {
        expiresIn: "15m",
        issuer: "토큰발급자",
      }
    );

    return res.status(200).json({
      code: 200,
      message: "토큰이 발급되었습니다.",
      token: token,
    });
  });
});

module.exports = router;

서비스 작동에는 문제가 없지만 위 코드는 컨트롤러에서 비즈니스 로직(사용자 검증, 비밀번호 비교, JWT토큰 생성)을 모두 처리하고 있어서 코드가 복잡하고 기능 확장 시에 코드를 모두 수정해야 하는 문제점을 가지고 있습니다.

 

login 코드의 주요 기능을 나열해 보자면 다음과 같습니다.

1️⃣ [ “/login” 주소로 POST 요청 ]

→ POST 요청을 보내고, 클라이언트의 요청을 처리하는 부분으로 Controller에 해당합니다.

2️⃣ [ DB에서 사용자 정보 조회 ]

→ DB와 상호작용하는 부분으로 사용자 정보를 가져와야 하기 때문에 Repository에 해당합니다.

3️⃣ [ 로그인 유저 존재 확인 ]

→ DB에서 조회된 회원가입된 유저(로그인하는 유저)가 존재하는지 확인하는 주요 로직이라 Service에 해당합니다.

4️⃣ [ JWT 토큰 생성 ]

→ 로그인 인증에 성공 시, JWT토큰을 생성해야 하기 때문에 이 로직도 Service에 해당합니다.

5️⃣ [ 클라이언트에게 응답 반환 ]

→ 위 과정을 거쳐 반환된 결과를 클라이언트에게 전달하기 때문에 Controller에 해당합니다.

 

위 과정을 바탕으로 변경한 폴더구조입니다.

 

Controller 레이어에서는 사용자에게 id, pw를 받는 부분이고 비즈니스 로직(서비스)에 loginService.login 메서드로 id와 pw를 전달하는 loginController.js 파일입니다.

//loginController.js

const loginService = require("../services/loginService");

const login = async (req, res) => {
  const { id, password } = req.body;

  if (!id || !password) {
    return res.status(400).json({
      message: "아이디와 비밀번호를 입력하세요",
    });
  }

  try {
    const token = await loginService.login(id, password);
    return res.status(200).json({
      message: "토큰이 정상적으로 발급되었습니다.",
      token: token,
    });
  } catch (err) {
    console.log(err);
    return res.status(err.status || 500).json({
      message: "서버 에러",
    });
  }
};

module.exports = { login };

 

 

Service 레이어에서는 사용자를 조회하고 비밀번호를 검증합니다. (if (user.password!== password) ~) 또한 서비스에서 반환된 토큰 또는 에러를 컨트롤러가 받아서 이를 클라이언트에게 응답하도록 합니다.

//loginService.js
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
const loginRepository = require("../repositories/loginRepository");

dotenv.config();

const login = async (id, password) => {
  const key = process.env.SECRET_KEY;

  if (!key) {
    throw {
      status: 500,
      message: "SECRET_KEY가 누락되었습니다.",
    };
  }

  const user = await loginRepository.findUserById(id);

  if (!user) {
    return res.status(404).json({
      message: "존재하지 않는 사용자입니다.",
    });
  }

  if (user.password !== password) {
    throw {
      status: 401,
      message: "비밀번호가 일치하지 않습니다.",
    };
  }

  const token = jwt.sign(
    {
      type: "JWT",
      id: user.id,
    },
    key,
    {
      expiresIn: "15m",
      issuer: "토큰발급자",
    }
  );

  return token;
};

module.exports = {
  login,
};

 

 

Repository 레이어에서는 사용자 id를 기반으로 DB에서 사용자 정보를 가져오는 findUserById함수를 새로 만들었습니다. 또한 쿼리문을 통해 id와 pw를 조회합니다.

SQL 쿼리로 사용자의 id, pw를 조회한 뒤 dbGet함수로 쿼리를 실행하고 결과를 반환합니다. 이후에 DB조회 중 에러가 나면 정의된 에러메시지를 전달합니다.

//loginRepository.js
const db = require("../database/db");
const { promisify } = require("util");

const dbGet = promisify(db.get).bind(db);

const findUserById = async (id) => {
  try {
    const query = "SELECT id, password FROM users WHERE id = ?";
    const user = await dbGet(query, [id]);
    return user;
  } catch (err) {
    throw {
      message: "DB조회중 오류 발생",
    };
  }
};

module.exports = { findUserById };

 

 

 

리팩터링 된 코드를 일련의 과정으로 정리해 보았습니다.

 

 

  1. “/login”으로 POST요청을 클라이언트에게 받으면
  2. Controller는 id, pw를 받아서 Service에게 전달합니다.
  3. Service는 findUserById 함수를 호출합니다.
  4. Repository는 Service에게 전달받은 id값으로 DB에서 사용자 정보를 가져옵니다.
  5. DB는 SQL쿼리문으로 id와 pw를 조회합니다.
  6. 만약 인증된 사용자의 경우, 유저 데이터를 반환하고 JWT토큰을 함께 발행합니다.
  7. Repository, Service에게 유저 데이터를 전달하고
  8. Controller는 클라이언트에게 결과를 반환합니다.

 

 

긴 글 읽어주셔서 감사합니다. 이해가 잘 안 되거나 수정사항이 있다면 언제든지 말씀해 주세요 🙂

728x90

'BE' 카테고리의 다른 글

좋은 피자 위대한 피자 (2) (Type ORM 적용하여 entity 설정하기)  (0) 2025.01.16
좋은 피자 위대한 피자 (1) (프로젝트 소개, ERD 정리)  (0) 2025.01.06
[프로그래머스 데브코스] 풀스택 5기 지원 및 합격 후기  (2) 2024.10.13
4장 http 모듈로 서버 만들기  (1) 2023.11.03
3장 노드 기능 알아보기  (4) 2023.11.03
'BE' 카테고리의 다른 글
  • 좋은 피자 위대한 피자 (2) (Type ORM 적용하여 entity 설정하기)
  • 좋은 피자 위대한 피자 (1) (프로젝트 소개, ERD 정리)
  • [프로그래머스 데브코스] 풀스택 5기 지원 및 합격 후기
  • 4장 http 모듈로 서버 만들기
Yura 🌼
Yura 🌼
만나서 반갑습니다. 어려운 내용을 쉽게 설명하고 싶은 프론트엔드 개발자 김유라입니다.
  • Yura 🌼
    쉽게 설명할 수 없으면, 아는 것이 아니다 ✍️
    Yura 🌼
    • Yura (72)
      • FE (6)
      • BE (14)
      • 코테 (52)
  • 최근 글

  • 링크

    • Github
    • LinkedIn
  • 전체
    오늘
    어제
  • hELLO· Designed By정상우.v4.10.3
Yura 🌼
[Node.js] Controller, Service, Repository로 계층 분리하기
상단으로

티스토리툴바