무엇인가
하나의 기능을 셋으로 가른다.
- 모델 — 데이터와 규칙. 화면을 모른다.
- 뷰 — 모델을 보여 준다. 데이터를 바꾸지 않는다.
- 컨트롤러 — 입력을 받아 모델을 바꾼다.
핵심은 모델이 뷰를 모른다는 것이다. 모델이 화면을 직접 그리기 시작하면 같은 데이터를 다른 방식으로 보여 줄 수 없게 된다.
구조
graph LR U([사용자]) -->|입력| C[컨트롤러] C -->|상태 변경| M[모델] M -->|변경 알림| V[뷰] V -->|표시| U M -.->|조회| V
C++로 보기
모델이 뷰를 직접 부르지 않도록 관찰자 콜백으로만 알린다. 그래서 뷰를 둘 붙여도 모델은 그대로다.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
// ── 모델: 데이터와 규칙만. 화면을 모른다 ──
class Counter {
int value_ = 0;
std::vector<std::function<void(int)>> observers_;
public:
void onChanged(std::function<void(int)> fn) { observers_.push_back(std::move(fn)); }
int value() const { return value_; }
void add(int delta) {
int next = value_ + delta;
if (next < 0) return; // 규칙: 음수가 안 된다
value_ = next;
for (const auto& fn : observers_) fn(value_);
}
};
// ── 뷰: 보여 주기만. 모델을 바꾸지 않는다 ──
class NumberView {
public:
void render(int v) const { std::cout << "[숫자] " << v << '\n'; }
};
class BarView {
public:
void render(int v) const { std::cout << "[막대] " << std::string(v, '#') << '\n'; }
};
// ── 컨트롤러: 입력을 모델의 동작으로 옮긴다 ──
class CounterController {
Counter& model_;
public:
explicit CounterController(Counter& m) : model_(m) {}
void handle(char key) {
switch (key) {
case '+': model_.add(1); break;
case '-': model_.add(-1); break;
default: std::cout << "(무시: " << key << ")\n";
}
}
};
int main() {
Counter model;
NumberView numbers;
BarView bars;
// 뷰가 모델을 구독한다. 모델은 누가 듣는지 모른다.
model.onChanged([&](int v) { numbers.render(v); });
model.onChanged([&](int v) { bars.render(v); });
CounterController controller{model};
for (char key : {'+', '+', '+', '-', 'x', '-', '-', '-'}) controller.handle(key);
std::cout << "최종: " << model.value() << '\n'; // 규칙 덕에 음수로 안 내려간다
}뷰가 둘인데 모델 코드에 뷰라는 단어가 없다. 이것이 분리가 됐다는 신호다.
반대로 모델 안에 std::cout이 있으면 이미 뷰와 붙은 것이다.
언제 쓰나
- 같은 데이터를 여러 방식으로 보여 줘야 할 때.
- 화면이 자주 바뀌고 규칙은 그대로일 때(또는 그 반대).
- 업무 규칙을 화면 없이 테스트하고 싶을 때. 위 예제의
Counter는 단독으로 테스트된다.
대가
- 작은 기능에는 과하다. 파일 셋이 생기고 오가는 코드가 는다.
- 컨트롤러가 비대해지기 쉽다. 갈 곳 없는 코드가 여기 쌓인다.
- 뷰와 모델의 경계가 실제로는 흐릿하다. “표시용 서식”이 어느 쪽인지 늘 논쟁거리다.
- 파생형이 많다(MVP, MVVM). 이름만 같고 책임 배분이 달라 팀 안에서 말이 엇갈린다.
실제로 만나는 곳
거의 모든 GUI 툴킷과 웹 프레임워크. Qt의 모델/뷰, 웹의 라우터-컨트롤러 구조, 그리고 MVVM으로 변형된 형태가 오늘날 프런트엔드의 기본형이다.