본문 바로가기

[Express + Node]

[Express] 레이어드 아키텍쳐 구현하기 (Layered Architecture)

728x90

🚩 소개

안녕하세요! 대학생 개발자 주이어입니다!
오늘은 레이어드 아키텍쳐 구조화 방법을 사용하여 Express 폴더를 정리해보려고 합니다! 

레이어드 아키텍쳐는 실무에서 자주 사용되는 백엔드 구조화 방식이니 한 번씩 읽고 가시길 바랍니다!


📌 레이어드 아키텍쳐란?

레이어드 아키텍쳐는 애플리케이션의 구조를 여러 개의 계층(layer)으로 나누어 각 계층이 하나의 책임만을 가지도록 구성하는 소프트웨어 설계 방식입니다. 

주로 백엔드에서 많이 사용되며, 각 계층은 아래와 같이 나눠집니다.

계층 설명
Controller 클라이언트 요청을 받고, 응답을 반환하는 역할
Service 실제 요청을 처리하는 역할
Model DB를 설계하거나, 직접 통신하는 역할
Route API 경로를 정의하고, 요청에 맞는 Controller에 연결하는 역할

위와 같은 구조로 이루어져 있고, 상황에 맞게 더 많은 계층으로 분리되기도 합니다.

또한 이러한 구조는 Express, NextJS, Spring 등에서 기본으로 권장하거나 템플릿에 포함되어 있는 구조입니다.


🎯 왜 레이어드 아키텍처를 사용할까?

그럼 이러한 레이어드 아키텍쳐를 왜 사용하는 것 일까요?

레이어드 아키텍쳐를 사용하는 이유는 크게 3가지 정도로 분류할 수 있을 것 같습니다.

1. 유지보수와 확장에 유리

  • 기능 하나를 수정할 때, 다른 계층의 코드를 건드릴 일이 거의 없기 때문에 쉽게 유지보수할 수 있습니다.
  • 예시로, DB 구조가 바뀌어도 service나 controller는 그대로 유지할 수 있습니다.(Model에서 수정하기 때문)

2. 테스트 코드 작성이 쉬움

  • Service 로직만 따로 테스트 가능하기 때문에 테스트 코드의 작성이 간결해집니다.
  • DB나 요청을 직접 연결하지 않아도 로직을 검증할 수 있습니다.

3. 협업 능률 상승

  • 역할에 따른 분업이 가능해집니다.
    - 한 사람은 Controller, 한 사람은 Model, 한 사람은 Service 이런식으로 초기에 코드 작성 규칙만 정해둔다면 빠르고 쉽게 협업이 가능해집니다.

기존 코드와 비교 예시

레이어드 아키텍쳐를 사용하는 이유는 사실 코드 비교를 통해 확 체감할 수 있습니다.

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    credentials: true,
  })
);
app.use(exprees.json());

const db = await mysql.createConnection({
  host: "hidden",
  user: "hidden",
  password: "hidden",
  database: "WooStar",
  ssl: {
    ca: fs.readFileSync("./isrgrootx1.pem"),
  },
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
});

export default db;

app.post("/login", async (req, res) => {
  const { userId, password } = req.body;
  const user = await getUserDB(userId);

  if (!user) {
    res.json({ success: false });
  } else {
    try {
      const isMatch = await bcrypt.compare(password, user.password);
      if (isMatch) {
        return res.json({ success: true });
      } else {
        res.json({ success: false });
      }
    } catch (err) {
      console.error(err);
      res.json({ success: false });
    }
  }
});

위 코드는 제가 이전 프로젝트에서 만들었던 백엔드 파일 입니다. (실제로는 훨씬 더 긴 코드입니다.)

보이시는 것 처럼 db 연결, 요청 연결, 요청 처리, 요청 반환 등 모든 코드가 한 파일 내에서 동작하는 것을 알 수 있습니다.

물론 간단한 사이드 프로젝트나 개인 포트폴리오 용도라면 크게 문제가 없을 수 있지만, 프로젝트 규모가 커지거나, 협업을 할 경우에는 유지보수에 큰 어려움을 겪게될 코드입니다.

그 이유는 코드를 보기가 힘들 뿐만 아니라, 하나의 기능이 수정 됐을 경우 코드 전체를 수정해야할 경우가 생길 수 있기 때문입니다.

 

또한 기능을 수정하고 테스트를 할 때, 코드 분리가 되어있지 않기 때문에 정확히 어느 부분에서 오류가 생겼는지 확인하기에도 어려울 것 입니다.

레이어드 아키텍쳐 구조

위 사진은 현재 진행하고 있는 프로젝트에서 백엔드에 레이어드 아키텍쳐를 적용하여 제작한 모습입니다.

보시는 것과 같이, controllers, prisma(ORM도구), routes, services, utils, app.js(메인 코드)로 이루어져 있습니다.

 

간단하게 동작 코드를 살펴보겠습니다.

import express from "express";
import {
  createPlan,
  savePlan,
  deletePlan,
  modifyPlan,
  allPlan,
} from "../controllers/plan.controller.js";

const router = express.Router();

router.post("/create", createPlan);
router.post("/save", savePlan);

router.get("/delete", deletePlan);
router.get("/modify", modifyPlan);
router.get("/all", allPlan);

export default router;

 

먼저 plan.routes.js 코드입니다. 

들어온 요청 경로에 맞게 controller에 전달하는 부분입니다.

import { savePlanJson } from "../services/planServices/savePlanJson.service.js";

export const savePlan = async (req, res) => {
  const { id, sub, title, content, flag_link } = req.body;

  console.log(id, sub, title, flag_link);

  try {
    const newId = await savePlanJson(id, sub, title, content, flag_link);
    console.log(newId);
    res.json({ newId: newId });
  } catch (err) {
    res.status(500).json({ error: "서버에러" });
  }
};

다음은 plan.controller.js 코드입니다.

들어온 요청을 service에 보내 처리하며, 값을 반환하는 부분입니다.

import { PrismaClient } from "@prisma/client";
import { findPlanDB } from "./findPlanDB.service.js";
const prisma = new PrismaClient();

export async function savePlanJson(id, sub, title, content, flag_link) {
  const isPlan = await findPlanDB(id);

  if (!isPlan) {
    try {
      const _savePlan = await prisma.Schedule.create({
        data: {
          sub: sub,
          title: title,
          content: content,
          flag: flag_link,
        },
      });
      return _savePlan.id;
    } catch (err) {
      console.error("DB 저장 오류 :", err);
      return -1;
    }
  } else {
    try {
      const _updateSchedule = await prisma.Schedule.update({
        where: { id: id },
        data: {
          title: title,
          content: content,
          flag: flag_link,
        },
      });
      return id;
    } catch (err) {
      console.error("DB 업데이트 오류 :", err);
      return id;
    }
  }
}

다음은 savePlanJson.service.js 코드입니다.

요청을 실질적으로 처리하는 부분이며, Prisma를 사용하여 DB와 통신하는 부분입니다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id Int @id @default(autoincrement())
  sub String @unique
  email String
  schedules Schedule[]
}

model Schedule {
  id Int @id @default(autoincrement())
  sub String 
  title String
  content Json
  flag String
  createdAt DateTime @default(now())
  user User @relation(fields: [sub], references: [sub])
}

마지막으로 schema.prisma 코드 및 쿼리 입니다.

prsima ORM 도구를 사용하여 DB Model을 정의하고 생성하는 부분입니다.

 

참고로 utils 폴더에는 API 연결을 정의하거나, 자주 사용되는 기능을 저장할 때 사용했습니다.

 

이런식으로 코드를 분리해서 작성할 경우, 한 파일에 작성되는 코드의 양이 확 줄어들고, 해당 파일을 수정할 일이 생기더라도 해당 파일만 수정하면 되기 때문에 쉽게 유지보수할 수 있게 됩니다.

또한 위에서 언급했듯이 협업을 하는 경우 역할 분담이 쉽고, 한 사람이 한 파일의 코드를 온전히 작성하기 때문에 협업 능률도 상승하게 됩니다.


😊  마무리

지금까지 레이어드 아키텍쳐를 기반으로 백엔드 파일을 어떻게 구조화할 수 있을지에 대해서 정리해보았습니다.

아마 예시 코드 부분을 통해 많은 분들이 구조화 방식 필요성에 대해서 많이 느끼셨을 거라고 생각합니다.

실제로 이러한 구조화 방식은 스타트업 뿐만 아니라 대부분의 개발 조직에서 많이 사용되는 실무 스킬 중 하나이기 때문에, 이론만이라도 이해하는 것이 중요하다고 생각합니다.


🧱 추가 설명 (DDD)

레이어드 아키텍쳐보다 복잡도를 더 세밀하게 관리할 수 있는 DDD 구조화 방식도 존재합니다.
DDD는 Domain-Driven-Design의 약자로, 도메인을 중심으로 구조화 하는 방식이며, 훨씬 더 규모가 크고 복잡한 서비스에 주로 적용되는 기술입니다.

ㄷㄷㄷ: Domain Driven Design과 적용 사례공유 / if(kakao)2022

실제로 2022년에 카카오 엔터테인먼트에서 DDD 구조를 적용한 사례에 대해서 발표하기도 했습니다.

 

대부분의 개인 프로젝트에 DDD까지 도입하는 경우는 거의 없지만, 이러한 구조를 이해하고 있는 것만으로 실무에서 굉장히 큰 강점이 될 것이라고 생각합니다.

 

추가 부분까지 읽어주셔서 감사드리고, 다음에는 더 좋은 글로 찾아오도록 하겠습니다!

 

💬 함께 공부하고 싶은 내용이 있다면 댓글이나 피드백으로 알려주세요!

[Express] GET 요청으로 유저 검색 기능 구현하는 깔끔한 방법(w. React)

 

[Express] GET 요청으로 유저 검색 기능 구현하는 깔끔한 방법(w. React)

[React] 실시간 검색 - debouncing 기능 구현하기 [React] 실시간 검색 - debouncing 기능 구현하기[React, Express] 서버 구현 및 데이터베이스 연결하기(REST API) [React, Express] 서버 구현 및 데이터베이스 연결하

blog.juyear.dev

이전 글 읽으러 가기!

 

https://discord.gg/8Hh8WgM4zp

 

KYT CODING COMMUNITY Discord 서버에 가입하세요!

Discord에서 KYT CODING COMMUNITY 커뮤니티를 확인하세요. 23명과 어울리며 무료 음성 및 텍스트 채팅을 즐기세요.

discord.com

KYT CODING COMMUNITY 가입하기!

 

728x90