MVC (Model-View-Controller)는 소프트웨어를 세 가지 역할로 나누어 설계하는 아키텍처 패턴(Architectural Pattern)이야.
각각의 의미는 다음과 같아.
여기서 MVC의 핵심은 단순히 세 가지로 나눈다는 것이 아니야.
서로 다른 책임을 가진 코드들을 분리해서, 하나의 부분이 변경되더라도 다른 부분에 미치는 영향을 줄이려는 것이야.
이것을 관심사의 분리(Separation of Concerns) 라고 해. SOLID 원칙 중 단일 책임 원칙(SRP) 과도 취지가 통하는 부분이지.
💡 MVC는 디자인 패턴 아닌가요?
MVC는 원래 Smalltalk 환경에서 사용자 인터페이스의 데이터, 화면 표현, 입력 처리를 분리하기 위해 등장했어. 이후 웹 애플리케이션을 비롯한 다양한 소프트웨어 구조에 적용되면서 아키텍처 패턴으로도 널리 설명되고 있어.
자료에 따라 MVC를 디자인 패턴이라고 부르기도 해. "반복되는 설계 해결책"을 넓게 묶어 디자인 패턴이라고 부르는 경우도 많기 때문이야.
하지만 오늘날 웹 프레임워크에서는 MVC가 애플리케이션 전체의 구조를 결정하기 때문에, 이 글에서는 아키텍처 패턴으로 설명할게.
사용자 ── 요청 ──▶ Controller ── 데이터 요청 ──▶ Model
│◀──────── 결과 반환 ────────┘
│
└── 데이터 전달 ──▶ View ── 화면 ──▶ 사용자
예를 들어 사용자가 게시판에서 게시글 상세 페이지를 클릭했다고 해보자.
단, MVC의 구현은 프레임워크마다 달라. 고전적인 MVC에서는 View가 Model을 직접 관찰하기도 하고, 웹 MVC에서는 Controller가 Model을 조회한 후 View에 데이터를 전달하기도 해.
따라서 위 그림은 MVC의 유일한 구조가 아니라 이해를 돕기 위한 대표적인 흐름이야.
Model은 소프트웨어가 관리하는 데이터와 그 데이터에 적용되는 비즈니스 규칙을 담당해.
예를 들어 게시판의 Model이라면 다음 기능을 생각할 수 있겠지.
💡 "로그인한 사용자인가?" 를 확인하는 인증은 보통 미들웨어가 담당해. Model이 맡는 것은 "이 사용자가 이 게시글을 삭제해도 되는가?" 같은 비즈니스 규칙이야.
여기서 중요한 것은 Model이 단순히 데이터베이스 자체를 의미하는 것은 아니라는 점이야.
Prisma는 DB에 접근하기 위한 도구이고, Model은 그보다 넓은 개념이야. 실제 프로젝트에서는 서비스(Service)와 저장소(Repository) 계층으로 Model의 책임을 나누기도 해.
View는 데이터를 어떤 형태로 사용자에게 보여줄지를 담당해.
예를 들어 게시글 상세 화면이라면 다음을 표현하겠지.
function PostView({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
이 컴포넌트는 전달받은 게시글 데이터를 화면에 표현하는 역할을 하고 있어.
React의 컴포넌트는 View의 역할을 수행할 수 있지만, 모든 React 컴포넌트가 순수한 View인 것은 아니야. 상태 관리, 데이터 조회, 이벤트 처리까지 함께 담당할 수도 있기 때문이지.
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의 역할은 세 가지야.
req.params.id로 요청한 게시글 ID를 확인해.postService.findById(id)로 게시글 데이터를 요청해.즉, Controller는 직접 모든 일을 처리하는 존재라기보다 다른 구성 요소들이 적절한 일을 수행하도록 연결하는 조정자에 가까워.
위 Controller는 화면이 아니라 JSON 데이터를 응답하고 있어. 이건 서버를 어떻게 구성하느냐에 따라 달라지는 부분이야.
| 방식 | Controller의 응답 | View의 위치 |
|---|---|---|
| 전통적인 서버 렌더링 (Express + EJS 등) | res.render("post", { post }) | 서버의 템플릿 파일 |
| API 서버 + React | res.json(post) | 브라우저에서 실행되는 React 컴포넌트 |
전통적인 방식에서는 서버가 템플릿 엔진으로 HTML을 직접 만들어서 보내기 때문에 M, V, C가 모두 서버 안에 있어.
반면 Express를 API 서버로 쓰고 화면은 React로 만드는 구조에서는, 서버는 JSON 데이터만 보내고 실제 화면은 앞에서 본 PostView 같은 React 컴포넌트가 그려. View의 역할이 프론트엔드로 옮겨간 셈이지.
그렇다면 굳이 Model, View, Controller를 나누는 이유가 뭘까?
MVC를 사용하지 않고 게시글의 데이터 조회, 유효성 검증, 화면 렌더링 등을 하나의 함수에 작성했다고 생각해 보자.
처음에는 코드가 짧고 편리할 수 있어.
하지만 프로젝트가 커지면 다음과 같은 문제가 발생할 수 있어.
MVC는 이런 문제를 줄이기 위해 책임을 분리하는 거야.
| 책임이 혼합된 구조 | MVC로 분리한 구조 | |
|---|---|---|
| 형태 | 하나의 거대한 함수 | Model / Controller / View |
| 담당 | DB 조회 + 입력 검증 + 로직 + 화면 렌더링 | 데이터·규칙 / 요청·제어 / 화면·표현 |
| 변경할 때 | 여러 책임에 영향을 주기 쉬움 | 책임을 구분해 변경 영향을 관리 |
MVC 전체는 아니지만, 데이터를 가져오는 책임과 화면을 그리는 책임을 나누면 어떤 점이 좋은지 작은 예시로 살펴보는 코드.
export async function getPost() {
const response = await fetch(
"https://example.com/api/post/123"
);
const data = await response.json();
return data;
}
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 응답
{
post_id: 1,
post_title: "첫 번째 게시글"
created_at: "2026-10-11"
}
// 변경된 API 응답
{
id: 1,
title: "첫 번째 게시글"
createdAt: "2026-10-11"
}
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는 아무런 걱정 없이 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>
);
}
| 장점 | 설명 |
|---|---|
| 유지보수성 향상 | 각 부분의 책임이 분리되어 수정할 코드의 위치를 파악하기 쉬움 |
| 재사용성 향상 | 동일한 Model을 여러 View나 Controller에서 활용 가능 |
| 테스트 용이성 | UI와 비즈니스 로직을 분리하여 독립적으로 테스트하기 쉬움 |
| 협업 효율 향상 | 역할별로 작업 범위를 나누고 병렬 개발하기 쉬움 |
| 확장성 및 유연성 | 화면이나 데이터 처리 방식 변경 시 영향을 줄일 수 있음 |
다만 MVC를 적용하기만 하면 모든 장점이 자동으로 생기는 건 아니야.
예를 들어 Controller에 모든 비즈니스 로직을 넣으면 Controller가 지나치게 비대해질 수 있어. 이를 흔히 Fat Controller 문제라고 해.
결국 MVC에서도 각 구성 요소의 책임을 적절히 유지하는 것이 중요하지.
이제 우리가 앞에서 배웠던 개념과 연결해 보자.
| 개념 | MVC와의 관계 |
|---|---|
| 클린 아키텍처 | 핵심 비즈니스 로직을 DB, 프레임워크, UI 같은 외부 기술에 종속되지 않도록 분리하는 아키텍처 접근 방법. MVC와 마찬가지로 관심사를 분리하지만, 특히 의존성 방향과 비즈니스 로직의 독립성을 강조 |
| SOLID | MVC의 각 요소를 설계할 때 활용할 수 있는 5가지 원칙 |
| 디자인 패턴 | 반복적으로 등장하는 설계 문제를 해결하는 재사용 가능한 설계 방식. MVC는 일반적인 객체 수준의 디자인 패턴보다 범위가 큰 아키텍처 패턴이며, 내부 구현에는 옵저버 같은 디자인 패턴이 활용되기도 함 |
| 개발 방법론 | 애자일, 폭포수 등 어떤 개발 방법론을 사용하든 MVC를 적용할 수 있음 |
여기서 한 가지 흥미로운 점이 있어.
MVC는 1970년대 후반 Smalltalk 환경에서 처음 등장했고, SOLID 원칙은 2000년대 초에 정리됐어. 즉 MVC는 SOLID보다 훨씬 먼저 존재했던 패턴이야.
따라서 MVC가 SOLID를 기반으로 만들어졌다고 말하는 것은 정확하지 않아. 다만 MVC의 책임 분리 방식은 SRP와 같은 SOLID 원칙의 취지와 잘 맞아.
가능해. 하지만 Next.js App Router는 전통적인 MVC 프레임워크가 아니기 때문에 세 구성 요소가 반드시 명확하게 나뉘는 건 아니야.
예를 들어 게시글 상세 페이지를 구현한다고 생각해 보자.
| MVC 역할 | Next.js에서 대응할 수 있는 부분 |
|---|---|
| Model | 도메인 로직, Service, Repository 등 |
| View | JSX를 반환하는 React 컴포넌트 |
| Controller | Route Handler, Server Function 및 요청·입력을 조정하는 로직 |
단, 이 대응은 개념적인 매핑이야. 특히 서버 컴포넌트는 데이터 조회와 UI 렌더링을 함께 담당할 수도 있기 때문에 단순히 MVC의 View로만 정의하기 어려운 경우도 있어.
그리고 앞에서 배웠던 services, repositories, routes처럼 역할을 나누는 구조와 MVC는 서로 배타적인 것이 아니야.
Next.js에서는 서버 컴포넌트 하나가 데이터 조회와 화면 렌더링을 모두 담당할 수 있어. 따라서 MVC를 적용한다는 것은 반드시 Model, View, Controller라는 이름의 파일을 각각 만드는 것이 아니라, 프로젝트의 복잡도에 따라 각 책임을 적절하게 분리하는 것을 의미해.
웹 개발을 배우다 보면 다음 세 가지가 자주 등장해.
| 구분 | 핵심 질문 | 예시 |
|---|---|---|
| 개발 방법론 | 어떤 과정으로 개발할까? | 애자일, 폭포수 |
| 소프트웨어 아키텍처 | 시스템 전체를 어떻게 구성할까? | 계층형, 마이크로서비스 |
| 디자인 패턴 | 반복되는 설계 문제를 어떻게 해결할까? | 팩토리, 전략, 옵저버 |
아키텍처와 디자인 패턴은 경계가 일부 겹치기도 해. 특히 MVC처럼 시스템의 책임을 나누는 방식은 아키텍처 패턴으로 분류하는 것이 일반적이야.
💡 마무리
MVC의 본질은 세 개의 파일이나 폴더를 만드는 것이 아니라, 서로 다른 책임과 역할을 분리해 변경에 유연한 소프트웨어를 설계하는 것에 있어.
관심사의 분리는 설계 원칙이고, MVC는 그 원칙을 실현하기 위한 여러 아키텍처 패턴중에 하나인거지.