개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Express 프로젝트 구조 — MVC와 Layered 패턴

2025년 2월 7일
Express프로젝트 구조MVCLayeredFeature 분리

Express는 구조를 강제하지 않는다. 파일 하나에 모든 로직을 몰아넣어도 동작하지만, 프로젝트가 커질수록 그 자유가 독이 된다. MVC와 Layered Architecture는 이 문제를 해결하는 가장 보편적인 두 가지 접근법이다.

MVC 구조

MVC(Model-View-Controller) 는 역할을 세 레이어로 나눈다. Express에서 View는 보통 JSON 응답이 대신하므로, 실질적으로는 Controller와 Model 분리가 핵심이다.

src/
├── controllers/
│   ├── userController.ts
│   └── postController.ts
├── models/
│   ├── user.ts
│   └── post.ts
├── routes/
│   ├── userRoutes.ts
│   └── postRoutes.ts
└── app.ts

Controller: HTTP 요청을 받아 Model을 호출하고 응답을 반환하는 역할 / Model: 데이터 구조와 DB 접근 로직을 담당

// controllers/userController.ts
import { Request, Response } from "express";
import { findUserById } from "../models/user";

export async function getUser(req: Request, res: Response) {
  const user = await findUserById(req.params.id);
  if (!user) return res.status(404).json({ message: "Not found" });
  res.json(user);
}

MVC는 단순하고 직관적이지만, Model 레이어에 비즈니스 로직과 DB 쿼리가 섞이기 쉽다.

Layered 구조 (Controller / Service / Repository)

Layered Architecture는 MVC를 더 세밀하게 분리한다. 비즈니스 로직을 Service, DB 접근을 Repository로 독립시킨다.

src/
├── controllers/   ← HTTP 요청/응답만 처리
├── services/      ← 비즈니스 로직
├── repositories/  ← DB 쿼리
├── models/        ← 타입/스키마 정의
├── routes/
└── app.ts

Repository: DB 접근을 추상화하는 레이어. ORM 교체나 테스트 목킹이 쉬워진다.

각 레이어는 바로 아래 레이어만 호출한다:

Request → Controller → Service → Repository → DB
// repositories/userRepository.ts
export async function findById(id: string) {
  return db.query("SELECT * FROM users WHERE id = $1", [id]);
}

// services/userService.ts
import { findById } from "../repositories/userRepository";

export async function getUser(id: string) {
  const user = await findById(id);
  if (!user) throw new Error("User not found");
  return user;
}

// controllers/userController.ts
import { getUser } from "../services/userService";

export async function getUserHandler(req: Request, res: Response) {
  try {
    const user = await getUser(req.params.id);
    res.json(user);
  } catch (e) {
    res.status(404).json({ message: (e as Error).message });
  }
}

MVC vs Layered 비교

항목MVCLayered
복잡도낮음중간
비즈니스 로직 위치ModelService
DB 교체 난이도높음낮음 (Repository만 수정)
테스트 용이성보통높음
소규모 프로젝트적합과할 수 있음

Feature 기반 분리

규모가 더 커지면 레이어 기준 폴더 대신 도메인(Feature) 기준으로 묶는 방식이 효과적이다.

src/
├── users/
│   ├── userController.ts
│   ├── userService.ts
│   ├── userRepository.ts
│   └── userRoutes.ts
├── posts/
│   ├── postController.ts
│   ├── postService.ts
│   └── postRoutes.ts
└── app.ts

Feature 분리: 관련 파일을 도메인 단위로 모아두어 응집도를 높이는 방식. 팀 단위 병렬 개발에 유리하다.

레이어 기준 구조는 같은 기능의 파일이 여러 폴더에 흩어지지만, Feature 기준은 한 폴더 안에서 완결된다. 프로젝트 초기에는 Layered 구조로 시작하고, 도메인이 명확히 분리될 시점에 Feature 분리로 전환하는 전략이 현실적이다.

← 이전 글Express Request Lifecycle — 요청이 처리되는 전체 흐름
다음 글 →Express Middleware — 실무에서 자주 사용하는 라이브러리