Post

Node/Express에서 MVC 제대로 적용하기

컨트롤러 비대화 방지, 동적 라우트 설계, 모델/리포지토리 분리를 다루는 MVC 가이드

Node/Express에서 MVC 제대로 적용하기

결론 요약

  • MVC는 파일 분리 패턴이 아니라 “역할과 의존성 경계”를 강제하는 설계 규칙이다.
  • 컨트롤러 비대화는 기능 추가가 아니라 책임 누적에서 발생한다.
  • 동적 라우트는 URL 기술이 아니라 리소스 식별 규칙이다.
  • 모델은 “DB 접근 코드 묶음”이 아니라 도메인 규칙의 소유자다.
  • 파일 스토리지에서 DB로 옮길 때 구조가 유지되면, 확장은 비용이 아니라 선택이 된다.

MVC 패턴을 Node/Express에서 제대로 적용하는 법

컨트롤러 비대화를 구조적으로 차단하기


1️⃣ MVC 정의: 역할과 의존성 관점

결론

MVC는 누가 무엇을 알아도 되는지를 제한하는 규칙이다.

계층책임(해야 할 일)알면 안 되는 것
Controller요청 해석, 응답 조립DB 구조, 저장 방식
Model데이터 접근, 도메인 규칙HTTP(req/res), 상태 코드
View데이터 표현비즈니스 규칙, DB

핵심은 의존성 방향이다. Controller → Model은 가능하지만, Model → Controller는 구조 붕괴 신호다.


2️⃣ 왜 필요한가: 컨트롤러가 커지는 순간 구조는 이미 깨졌다

컨트롤러는 본질적으로 입출력 어댑터다. 여기에 규칙·검증·저장 로직이 쌓이면 다음 문제가 동시에 발생한다.

  • 테스트 불가능
  • 중복 로직 증가
  • DB 교체 시 전면 수정
  • API/SSR 병행 불가

즉, MVC는 “깔끔함” 문제가 아니라 변경 비용 통제 문제다.


3️⃣ 컨트롤러 비대화의 5가지 신호와 리팩토링 방향

신호 1: 컨트롤러에 if/else가 늘어난다

  • 원인: 도메인 규칙이 컨트롤러에 있음
  • 해결: 규칙을 모델 메서드로 이동

신호 2: DB 쿼리가 컨트롤러에 직접 등장

  • 원인: 데이터 접근과 요청 처리가 결합
  • 해결: 모델/리포지토리로 추출

신호 3: 동일한 로직이 여러 컨트롤러에 복제

  • 원인: 공통 도메인 규칙 부재
  • 해결: 서비스/모델 계층 도입

신호 4: 컨트롤러 테스트가 어렵다

  • 원인: 외부 의존성(DB, 파일)이 직접 연결
  • 해결: 의존성 주입 + 모델 단위 테스트

신호 5: 저장소 변경 시 컨트롤러 수정

  • 원인: 저장 방식이 컨트롤러에 노출
  • 해결: 추상화(Repository Interface)

4️⃣ 동적 라우트와 리소스 설계: URL은 식별자다

결론

동적 라우트(:id)는 기능이 아니라 리소스 식별 규칙이다.

예시: 제품 / 장바구니

1
2
/products/:productId
/cart/:userId

설계 기준

  • URL은 “행동”이 아니라 대상(리소스) 중심
  • 행동은 HTTP method로 표현

    • GET /products/:id
    • POST /cart/:userId/items

컨트롤러는 ID를 해석만 하고, 무엇이 가능한지는 모델이 결정한다.


5️⃣ 모델 계층의 책임: 데이터 + 도메인 규칙

❌ 흔한 오해

“Model = DB CRUD 함수 모음”

✅ 실제 역할

  • 데이터 접근
  • 도메인 규칙(가격 계산, 재고 차감, 상태 전이)
  • 일관성 보장

toy 예시

1
2
// 나쁜 예 (컨트롤러에서 규칙 처리)
if (price < 0) return res.status(400).end();
1
2
// 개선 예 (모델 책임)
Product.create({ price }); // 내부에서 검증

컨트롤러는 요청을 조립하고, 모델은 정합성을 보장한다.


6️⃣ 파일 스토리지 → DB 이전 시 구조를 유지하는 방법

문제 상황

  • 초반: JSON 파일 저장
  • 확장: SQL/Mongo로 이전

실패 패턴

  • 컨트롤러에 fssequelize 코드 직접 교체

해결 패턴: 리포지토리 추상화

1
2
3
4
5
// repository interface
productRepository.save(product);

// 구현체 교체
FileProductRepository  DbProductRepository

컨트롤러/서비스는 저장 방식 변경을 모른다. 이 구조가 있으면 “기술 선택”은 리팩토링이 아니라 설정이 된다.


7️⃣ 나쁜 MVC vs 좋은 MVC

❌ 나쁜 MVC

1
2
3
4
app.post('/product', (req, res) => {
  fs.writeFileSync('products.json', JSON.stringify(req.body));
  res.json({ ok: true });
});
  • 요청/저장/응답 결합
  • 테스트 불가
  • 확장 불가

✅ 좋은 MVC

1
2
3
router.post('/', productController.create);

controller  service  repository
  • 책임 분리
  • 교체 가능
  • 테스트 가능

권장 폴더 구조 예시

1
2
3
4
5
6
7
src/
 ├─ routes/
 ├─ controllers/
 ├─ services/
 ├─ models/
 ├─ repositories/
 └─ views/

핵심은 이름이 아니라 의존성 방향이다.


구조 점검 체크리스트 (15개)

  • 컨트롤러에 DB 코드가 없다
  • 컨트롤러는 req/res만 다룬다
  • 모델은 HTTP를 모른다
  • 도메인 규칙은 모델/서비스에 있다
  • URL은 리소스 중심이다
  • HTTP method로 행동을 표현한다
  • 동적 라우트는 식별자 역할만 한다
  • 중복 로직이 모델로 수렴한다
  • 저장 방식 교체가 컨트롤러 수정으로 이어지지 않는다
  • 파일 → DB 이전 시 인터페이스가 유지된다
  • 테스트가 모델 단위로 가능하다
  • 컨트롤러 함수가 20줄을 넘지 않는다
  • 에러 처리가 중앙화되어 있다
  • 서비스 계층 도입 기준이 명확하다
  • MVC를 “파일 구조”가 아니라 “책임 규칙”으로 설명할 수 있다

This post is licensed under CC BY 4.0 by the author.