MVC 패턴

“사용자 입력을 받는 쪽(Controller), 데이터를 다루는 쪽(Model), 화면에 보여주는 쪽(View)을 셋으로 쪼개 각자 책임을 나눠 갖는 웹/GUI 기본 아키텍처.”

MVC(Model View Controller, 모델-뷰-컨트롤러, 1979년 Trygve Reenskaug가 Smalltalk-80에서 제안한 UI 분리 아키텍처)는 사용자 요청을 처리하고 화면을 그리는 흐름을 세 역할로 나누는 패턴이다. 사무실에 비유하면 민원 창구(Controller), 서류 보관함(Model), 안내판(View)을 각각 다른 직원이 맡는 구조임.

한눈에 보기

항목내용
분류아키텍처 패턴(UI 계층 분리)
구성 요소Model(데이터/도메인), View(표현), Controller(조정)
해결 문제데이터 처리 코드와 화면 렌더링 코드가 뒤섞이는 현상
대표 프레임워크Spring MVC, Django, Ruby on Rails, ASP.NET MVC, Laravel
파생 패턴MVP, MVVM, Flux, Redux
포함 관계대부분의 웹 프레임워크가 기본 제공, API Gateway 뒤 각 서비스 내부 구조로도 자주 사용

한마디 요약: “입력 받는 놈, 계산하는 놈, 그리는 놈 이렇게 셋으로 쪼개라.”


일상 비유: 레스토랑 주방

손님이 주문을 하면 이런 순서로 흘러간다.

  1. 홀 서버(Controller)가 주문을 받음
  2. 주방장(Model)이 레시피와 재료(데이터)를 가지고 요리를 만듦
  3. 플레이팅 담당(View)이 접시에 예쁘게 담아 냄

홀 서버는 요리법을 모르고, 주방장은 테이블 번호를 모르고, 플레이팅 담당은 손님 주문을 직접 받지 않는다. 각자 자기 책임만 안다. MVC도 똑같다.


구조

flowchart LR
    User((사용자))

    subgraph MVC[MVC 애플리케이션]
        direction LR
        Controller[Controller<br/>요청 처리]
        Model[(Model<br/>데이터 + 비즈니스 로직)]
        View[View<br/>화면 렌더링]
    end

    User -- "1. HTTP 요청" --> Controller
    Controller -- "2. 데이터 조회/변경" --> Model
    Model -- "3. 결과 반환" --> Controller
    Controller -- "4. 데이터 전달" --> View
    View -- "5. HTML 응답" --> User

책임 구분:

역할책임모르는 것
Model데이터 구조, 검증 규칙, 비즈니스 로직, DB 접근화면이 어떻게 생겼는지
View데이터를 받아 HTML/JSON으로 렌더링데이터가 어떻게 계산됐는지
Controller요청 파싱, Model 호출, View 선택화면 디자인, DB 세부 쿼리

요청 처리 흐름 단계 분해

sequenceDiagram
    participant U as 브라우저
    participant R as Router
    participant C as PostController
    participant M as Post Model
    participant D as Database
    participant V as Template

    U->>R: GET /posts/42
    R->>C: show(id=42)
    C->>M: Post.find(42)
    M->>D: SELECT * FROM posts WHERE id=42
    D-->>M: 행 1개
    M-->>C: Post 객체
    C->>V: render("post_detail", post)
    V-->>C: HTML 문자열
    C-->>U: 200 OK + HTML

1단계: 브라우저가 URL 입력 → 2단계: Router가 Controller 메서드 매칭 → 3단계: Controller가 Model에 데이터 요청 → 4단계: Model이 DB에서 조회 → 5단계: Controller가 View에 데이터 넘김 → 6단계: View가 HTML 생성 → 7단계: 응답 반환.


나쁜 예 vs 좋은 예 (Python/Flask)

나쁜 예: 한 파일에 다 때려박음

@app.route("/posts/<int:id>")
def show_post(id):
    conn = sqlite3.connect("blog.db")
    row = conn.execute("SELECT * FROM posts WHERE id=?", (id,)).fetchone()
    if not row:
        return "<h1>404 Not Found</h1>", 404
 
    title, body, created = row[1], row[2], row[3]
    html = f"""
    <html>
      <body>
        <h1>{title}</h1>
        <p>작성일: {created}</p>
        <article>{body}</article>
      </body>
    </html>
    """
    return html

문제:

  • DB 쿼리, HTML 생성, 요청 처리가 한 함수에 뒤엉킴
  • 테스트할 때 DB + 렌더링을 동시에 띄워야 함
  • 같은 게시글을 JSON으로도 내려주고 싶으면 코드 복붙

좋은 예: MVC로 분리

# models/post.py (Model)
from dataclasses import dataclass
 
@dataclass
class Post:
    id: int
    title: str
    body: str
    created_at: str
 
    @classmethod
    def find(cls, id: int) -> "Post | None":
        row = db.execute("SELECT * FROM posts WHERE id=?", (id,)).fetchone()
        return cls(*row) if row else None
 
 
# controllers/post_controller.py (Controller)
from flask import render_template, abort
from models.post import Post
 
def show(id):
    post = Post.find(id)
    if not post:
        abort(404)
    return render_template("post_detail.html", post=post)
 
 
# templates/post_detail.html (View)
<html>
  <body>
    <h1>{{ post.title }}</h1>
    <p>작성일: {{ post.created_at }}</p>
    <article>{{ post.body }}</article>
  </body>
</html>
 
 
# app.py
app.add_url_rule("/posts/<int:id>", view_func=post_controller.show)

장점:

  • Model만 따로 단위 테스트 가능(DB mock)
  • View는 디자이너가 HTML/CSS만 만지면 됨
  • JSON 응답이 필요하면 Controller에 show_json 메서드만 추가(jsonify(post))

파생 패턴과의 비교

패턴특징대표 사용처
MVCController가 Model, View 둘 다 조정Spring MVC, Django, Rails
MVP(Model-View-Presenter)View가 수동적, Presenter가 모든 표시 로직 담당Android(과거), WinForms
MVVM(Model-View-ViewModel)ViewModel이 상태 바인딩으로 View와 자동 동기화WPF, Vue.js, SwiftUI
Flux/Redux단방향 데이터 흐름, 상태를 한 곳에 모음React 생태계

MVC는 가족이고, MVP/MVVM/Flux는 MVC에서 갈라진 친척들이다. 공통 뿌리: “UI 로직을 세 조각으로 쪼갠다.”


왜 이렇게 설계하는가

1. 관심사 분리(Separation of Concerns): 데이터 변경이 화면 코드를 깨지 않게, 디자인 변경이 비즈니스 로직을 깨지 않게 만든다. 서로 다른 이유로 바뀌는 코드를 같은 파일에 두지 않겠다는 설계 의도임.

2. 테스트 용이성: Model을 단위 테스트할 때 브라우저, 템플릿 엔진이 필요 없다. Controller는 Model을 mock해서 요청 처리 로직만 검증 가능.

3. 병렬 개발: 백엔드 개발자는 Model/Controller, 프론트엔드/디자이너는 View를 동시에 작업.

4. 재사용: 같은 Model을 웹 View, JSON API, CLI 출력에 모두 재사용할 수 있다.

대안 대비 장점: 페이지 단위로 코드를 쌓는 구조(PHP 초기 스타일)나 Smart UI 패턴(모든 걸 UI 코드에 집어넣는 방식)에 비해 코드가 커져도 유지보수 비용 증가폭이 훨씬 완만하다.


AI 에이전트 개발에서의 MVC

LLM 기반 에이전트 백엔드도 MVC 구조를 그대로 빌려 쓸 수 있음. 다음과 같이 매핑된다.

flowchart LR
    U((사용자))

    subgraph App[에이전트 웹앱]
        CT[ChatController<br/>요청 파싱 + 세션 관리]
        AG[AgentModel<br/>LLM 호출 + 도구 사용 + 메모리]
        VW[ResponseView<br/>JSON/SSE 스트림 렌더링]
    end

    LLM[(LLM API)]
    DB[(대화 DB)]
    TOOL[(Tool 서버)]

    U --> CT
    CT --> AG
    AG --> LLM
    AG --> TOOL
    AG --> DB
    AG --> CT
    CT --> VW
    VW --> U
레이어역할
ModelLLM 호출, 프롬프트 구성, 도구 실행, RAG(Retrieval-Augmented Generation, 검색 증강 생성) 컨텍스트 조립
View채팅 UI, 스트리밍 응답 포맷, JSON 스키마
Controller요청 라우팅, 인증, 세션 ID 관리, 모델 선택

FastAPI + LangChain 조합에서도 패턴을 유지하면 모델 교체(Claude → GPT)UI 교체(웹 → CLI → Slack 봇) 시 Model/View 한쪽만 바꿔치기하면 된다.


트레이드오프

장점단점
역할 분리로 유지보수 쉬움작은 앱에서는 파일 수가 쓸데없이 많아짐
병렬 개발 가능초기 학습 곡선 존재
테스트 전략이 명확해짐Controller가 비대해지는 Fat Controller 안티패턴 주의
프레임워크 지원 풍부Model이 단순 DTO로 전락하면 Anemic Domain Model 안티패턴

Fat Controller(비대한 컨트롤러): 비즈니스 로직이 Controller에 쌓이면 Model이 비어버림. 대응: 서비스 레이어를 추가해 Controller → Service → Model 구조로 확장.


언제 쓰면 좋은가 / 피해야 할 때

좋을 때:

  • 여러 화면과 여러 데이터 소스를 다루는 중대형 웹앱
  • 팀이 여러 명이고 역할 분담이 필요할 때
  • 동일 데이터를 웹 + API + 모바일로 다양하게 내보내야 할 때

피해야 할 때:

  • 한 화면짜리 정적 사이트
  • 마이크로서비스 하나가 순수 작업 큐 처리만 할 때(화면 없음)
  • 이벤트 스트림 처리 같은 비-UI 시스템

직접 확인하기

  1. Django 튜토리얼(https://docs.djangoproject.com/en/stable/intro/tutorial01/) 공식 문서 1~4장 따라하면 MVC(Django는 MVT로 부름: Model-View-Template) 감이 잡힘
  2. 자기 프로젝트에서 한 Controller 파일을 열어봐라. 200줄을 넘어간다면 Service 레이어 분리 신호
  3. grep -r "SELECT" controllers/ 로 Controller에 DB 쿼리가 섞여 있지 않은지 점검

다른 패턴과의 관계 (포함 관계)

  • API Gateway 패턴과 조합: Gateway 뒤 각 서비스 내부가 MVC 구조인 경우가 흔함
  • Microservices의 단일 서비스 내부 구조로 자주 채택
  • Observer 패턴 활용: 원조 Smalltalk MVC에서 Model이 바뀌면 View에 Observer로 통지
  • Strategy 패턴과 결합: Controller가 상황에 따라 다른 렌더링 전략(HTML/JSON/XML) 선택
  • Facade 패턴의 사촌: Controller가 복잡한 Model 호출을 단순화해 View에 노출

요약

“요청 받는 Controller, 데이터 다루는 Model, 화면 그리는 View 셋으로 쪼개기. 서로 다른 이유로 바뀌는 코드가 같이 섞이지 않게 만드는 게 목적.”

관련 문서: