MVC (Model-View-Controller)

애옹이·5일 전
post-thumbnail

MVC 패턴, 한 번에 정리하기

1. MVC 패턴은 무엇인가?

MVC (Model-View-Controller)는 소프트웨어를 세 가지 역할로 나누어 설계하는 아키텍처 패턴(Architectural Pattern)이야.

각각의 의미는 다음과 같아.

  • Model : 데이터와 비즈니스 로직을 담당
  • View : 사용자에게 보여주는 화면을 담당
  • Controller : 사용자의 입력을 받아 적절한 처리를 연결하고 제어

여기서 MVC의 핵심은 단순히 세 가지로 나눈다는 것이 아니야.

서로 다른 책임을 가진 코드들을 분리해서, 하나의 부분이 변경되더라도 다른 부분에 미치는 영향을 줄이려는 것이야.

이것을 관심사의 분리(Separation of Concerns) 라고 해. SOLID 원칙 중 단일 책임 원칙(SRP) 과도 취지가 통하는 부분이지.

💡 MVC는 디자인 패턴 아닌가요?

MVC는 원래 Smalltalk 환경에서 사용자 인터페이스의 데이터, 화면 표현, 입력 처리를 분리하기 위해 등장했어. 이후 웹 애플리케이션을 비롯한 다양한 소프트웨어 구조에 적용되면서 아키텍처 패턴으로도 널리 설명되고 있어.

자료에 따라 MVC를 디자인 패턴이라고 부르기도 해. "반복되는 설계 해결책"을 넓게 묶어 디자인 패턴이라고 부르는 경우도 많기 때문이야.

하지만 오늘날 웹 프레임워크에서는 MVC가 애플리케이션 전체의 구조를 결정하기 때문에, 이 글에서는 아키텍처 패턴으로 설명할게.

2. MVC의 전체 구조

사용자 ── 요청 ──▶ Controller ── 데이터 요청 ──▶ Model
                     │◀──────── 결과 반환 ────────┘
                     │
                     └── 데이터 전달 ──▶ View ── 화면 ──▶ 사용자

예를 들어 사용자가 게시판에서 게시글 상세 페이지를 클릭했다고 해보자.

  1. 사용자 : 게시글 상세 페이지를 요청해.
  2. Controller : URL의 게시글 ID를 확인하고 Model에 필요한 데이터를 요청해.
  3. Model : 게시글을 조회하고 필요한 비즈니스 규칙을 처리한 뒤 결과를 돌려줘.
  4. Controller : 조회 결과를 View에 전달해.
  5. View : 전달받은 게시글 데이터를 화면으로 표현해.

단, MVC의 구현은 프레임워크마다 달라. 고전적인 MVC에서는 View가 Model을 직접 관찰하기도 하고, 웹 MVC에서는 Controller가 Model을 조회한 후 View에 데이터를 전달하기도 해.

따라서 위 그림은 MVC의 유일한 구조가 아니라 이해를 돕기 위한 대표적인 흐름이야.

3. MVC의 구성 요소별 역할

Model — 데이터와 비즈니스 로직

Model은 소프트웨어가 관리하는 데이터와 그 데이터에 적용되는 비즈니스 규칙을 담당해.

예를 들어 게시판의 Model이라면 다음 기능을 생각할 수 있겠지.

  • 게시글 데이터 조회
  • 게시글 등록 및 수정
  • 게시글 삭제 권한 검증 (예: 작성자 본인만 삭제 가능)
  • 게시글 데이터의 유효성 검증

💡 "로그인한 사용자인가?" 를 확인하는 인증은 보통 미들웨어가 담당해. Model이 맡는 것은 "이 사용자가 이 게시글을 삭제해도 되는가?" 같은 비즈니스 규칙이야.

여기서 중요한 것은 Model이 단순히 데이터베이스 자체를 의미하는 것은 아니라는 점이야.

Prisma는 DB에 접근하기 위한 도구이고, Model은 그보다 넓은 개념이야. 실제 프로젝트에서는 서비스(Service)와 저장소(Repository) 계층으로 Model의 책임을 나누기도 해.

View — 사용자에게 보여주는 화면

View는 데이터를 어떤 형태로 사용자에게 보여줄지를 담당해.

예를 들어 게시글 상세 화면이라면 다음을 표현하겠지.

function PostView({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

이 컴포넌트는 전달받은 게시글 데이터를 화면에 표현하는 역할을 하고 있어.

React의 컴포넌트는 View의 역할을 수행할 수 있지만, 모든 React 컴포넌트가 순수한 View인 것은 아니야. 상태 관리, 데이터 조회, 이벤트 처리까지 함께 담당할 수도 있기 때문이지.

Controller — 입력을 받아 흐름을 제어

Controller는 요청이나 사용자 입력을 해석하고, 적절한 Model의 동작을 호출한 뒤 응답을 결정하는 역할이야.

Express를 예로 들어볼게.

app.get("/posts/:id", async (req, res) => {
  const id = Number(req.params.id);

  const post = await postService.findById(id);

  if (!post) {
    return res.status(404).json({ message: "게시글 없음" });
  }

  res.json(post);
});

위 코드에서 Controller의 역할은 세 가지야.

  1. req.params.id로 요청한 게시글 ID를 확인해.
  2. postService.findById(id)로 게시글 데이터를 요청해.
  3. 조회 결과에 따라 오류를 반환하거나 데이터를 응답해.

즉, Controller는 직접 모든 일을 처리하는 존재라기보다 다른 구성 요소들이 적절한 일을 수행하도록 연결하는 조정자에 가까워.

잠깐, 그러면 View는 어디에 있을까?

위 Controller는 화면이 아니라 JSON 데이터를 응답하고 있어. 이건 서버를 어떻게 구성하느냐에 따라 달라지는 부분이야.

방식Controller의 응답View의 위치
전통적인 서버 렌더링 (Express + EJS 등)res.render("post", { post })서버의 템플릿 파일
API 서버 + Reactres.json(post)브라우저에서 실행되는 React 컴포넌트

전통적인 방식에서는 서버가 템플릿 엔진으로 HTML을 직접 만들어서 보내기 때문에 M, V, C가 모두 서버 안에 있어.

반면 Express를 API 서버로 쓰고 화면은 React로 만드는 구조에서는, 서버는 JSON 데이터만 보내고 실제 화면은 앞에서 본 PostView 같은 React 컴포넌트가 그려. View의 역할이 프론트엔드로 옮겨간 셈이지.

4. MVC 패턴을 사용하는 이유

그렇다면 굳이 Model, View, Controller를 나누는 이유가 뭘까?

MVC를 사용하지 않고 게시글의 데이터 조회, 유효성 검증, 화면 렌더링 등을 하나의 함수에 작성했다고 생각해 보자.

처음에는 코드가 짧고 편리할 수 있어.

하지만 프로젝트가 커지면 다음과 같은 문제가 발생할 수 있어.

  • 화면 디자인을 변경하려는데 데이터 조회 코드까지 건드려야 해.
  • 데이터베이스 처리 방식을 변경했더니 화면에 오류가 생겨.
  • 게시글 작성 기능을 테스트하려는데 실제 화면까지 실행해야 해.
  • 여러 개발자가 하나의 커다란 파일을 동시에 수정하면서 충돌이 발생해.

MVC는 이런 문제를 줄이기 위해 책임을 분리하는 거야.

책임이 혼합된 구조MVC로 분리한 구조
형태하나의 거대한 함수Model / Controller / View
담당DB 조회 + 입력 검증 + 로직 + 화면 렌더링데이터·규칙 / 요청·제어 / 화면·표현
변경할 때여러 책임에 영향을 주기 쉬움책임을 구분해 변경 영향을 관리

< 코드로 보기 >

MVC 전체는 아니지만, 데이터를 가져오는 책임과 화면을 그리는 책임을 나누면 어떤 점이 좋은지 작은 예시로 살펴보는 코드.


기존에 작성한 posts.js

export async function getPost() {
  const response = await fetch(
    "https://example.com/api/post/123"
  );

  const data = await response.json();

  return data;
}

기존 page.js 의 데이터 사용 방식

import { getPost } from "@/lib/api/posts";

export default async function PostsPage() {
  const post = await getPost();

  return (
    <div>
      <h1>{post.post_title}</h1>
      <p>{post.created_at}</p>
    </div>
  );
}

API 응답 방식이 바뀌는 경우

// 기존 API 응답
{
  post_id: 1,
  post_title: "첫 번째 게시글"
  created_at: "2026-10-11"
}

// 변경된 API 응답
{
  id: 1,
  title: "첫 번째 게시글"
  createdAt: "2026-10-11"
}

posts.js 를 이렇게 리턴한다면?

page.js와 posts.js 사이에 다음과 같은 약속이 있다고 생각하면 돼.
"getPost() 함수는 언제나 id, title, createdAt이라는 속성을 가진 게시글 객체를 반환한다."

// lib/api/posts.js

export async function getPost() {
  const response = await fetch("/api/posts/1");
  const data = await response.json();

  return {
    id: data.id,
    title: data.title,
    createdAt: data.createdAt,
  };
}

바꿀 필요가 없는, page.js

page.js는 아무런 걱정 없이 API 호출 함수만 가져다가, 자신의 역할을 수행하면 되는 구조 탄생.

import { getPost } from "@/lib/api/posts";

export default async function PostsPage() {
  const post = await getPost();

  return (
    <div>
      <h1>{post.title}</h1>
      <span>{post.id}</span>
      <p>{post.createdAt}</p>
    </div>
  );
}

5. MVC 패턴의 장점

장점설명
유지보수성 향상각 부분의 책임이 분리되어 수정할 코드의 위치를 파악하기 쉬움
재사용성 향상동일한 Model을 여러 View나 Controller에서 활용 가능
테스트 용이성UI와 비즈니스 로직을 분리하여 독립적으로 테스트하기 쉬움
협업 효율 향상역할별로 작업 범위를 나누고 병렬 개발하기 쉬움
확장성 및 유연성화면이나 데이터 처리 방식 변경 시 영향을 줄일 수 있음

다만 MVC를 적용하기만 하면 모든 장점이 자동으로 생기는 건 아니야.

예를 들어 Controller에 모든 비즈니스 로직을 넣으면 Controller가 지나치게 비대해질 수 있어. 이를 흔히 Fat Controller 문제라고 해.

결국 MVC에서도 각 구성 요소의 책임을 적절히 유지하는 것이 중요하지.

6. 함께 알아두면 좋은 개념들과 MVC의 관계

이제 우리가 앞에서 배웠던 개념과 연결해 보자.

개념MVC와의 관계
클린 아키텍처핵심 비즈니스 로직을 DB, 프레임워크, UI 같은 외부 기술에 종속되지 않도록 분리하는 아키텍처 접근 방법. MVC와 마찬가지로 관심사를 분리하지만, 특히 의존성 방향과 비즈니스 로직의 독립성을 강조
SOLIDMVC의 각 요소를 설계할 때 활용할 수 있는 5가지 원칙
디자인 패턴반복적으로 등장하는 설계 문제를 해결하는 재사용 가능한 설계 방식. MVC는 일반적인 객체 수준의 디자인 패턴보다 범위가 큰 아키텍처 패턴이며, 내부 구현에는 옵저버 같은 디자인 패턴이 활용되기도 함
개발 방법론애자일, 폭포수 등 어떤 개발 방법론을 사용하든 MVC를 적용할 수 있음

여기서 한 가지 흥미로운 점이 있어.

MVC는 1970년대 후반 Smalltalk 환경에서 처음 등장했고, SOLID 원칙은 2000년대 초에 정리됐어. 즉 MVC는 SOLID보다 훨씬 먼저 존재했던 패턴이야.

따라서 MVC가 SOLID를 기반으로 만들어졌다고 말하는 것은 정확하지 않아. 다만 MVC의 책임 분리 방식은 SRP와 같은 SOLID 원칙의 취지와 잘 맞아.

7. Next.js에서도 MVC 패턴을 사용할 수 있을까?

가능해. 하지만 Next.js App Router는 전통적인 MVC 프레임워크가 아니기 때문에 세 구성 요소가 반드시 명확하게 나뉘는 건 아니야.

예를 들어 게시글 상세 페이지를 구현한다고 생각해 보자.

MVC 역할Next.js에서 대응할 수 있는 부분
Model도메인 로직, Service, Repository 등
ViewJSX를 반환하는 React 컴포넌트
ControllerRoute Handler, Server Function 및 요청·입력을 조정하는 로직

단, 이 대응은 개념적인 매핑이야. 특히 서버 컴포넌트는 데이터 조회와 UI 렌더링을 함께 담당할 수도 있기 때문에 단순히 MVC의 View로만 정의하기 어려운 경우도 있어.

그리고 앞에서 배웠던 services, repositories, routes처럼 역할을 나누는 구조와 MVC는 서로 배타적인 것이 아니야.

Next.js에서는 서버 컴포넌트 하나가 데이터 조회와 화면 렌더링을 모두 담당할 수 있어. 따라서 MVC를 적용한다는 것은 반드시 Model, View, Controller라는 이름의 파일을 각각 만드는 것이 아니라, 프로젝트의 복잡도에 따라 각 책임을 적절하게 분리하는 것을 의미해.

8. 한 가지 개념 더 구분해 보기

웹 개발을 배우다 보면 다음 세 가지가 자주 등장해.

구분핵심 질문예시
개발 방법론어떤 과정으로 개발할까?애자일, 폭포수
소프트웨어 아키텍처시스템 전체를 어떻게 구성할까?계층형, 마이크로서비스
디자인 패턴반복되는 설계 문제를 어떻게 해결할까?팩토리, 전략, 옵저버

아키텍처와 디자인 패턴은 경계가 일부 겹치기도 해. 특히 MVC처럼 시스템의 책임을 나누는 방식은 아키텍처 패턴으로 분류하는 것이 일반적이야.

💡 마무리

MVC의 본질은 세 개의 파일이나 폴더를 만드는 것이 아니라, 서로 다른 책임과 역할을 분리해 변경에 유연한 소프트웨어를 설계하는 것에 있어.

관심사의 분리는 설계 원칙이고, MVC는 그 원칙을 실현하기 위한 여러 아키텍처 패턴중에 하나인거지.

profile
풀스택 웹 개발자가 목표인 애옹이

0개의 댓글