MVP 패턴
개요
상품 검색 화면을 만든다고 하자. 검색 버튼의 클릭 핸들러가 입력 칸의 글자를 읽고, 두 글자 미만이면 오류 라벨을 채우고, 아니면 상품 목록을 검색해 결과 목록 상자를 채운다. 동작은 하지만 "한 글자 검색어는 검색하지 않는다"는 규칙을 확인하려면 폼을 띄우고 버튼을 눌러 봐야 한다. 규칙과 컨트롤 조작이 한 메서드에 섞여 있어서, 폼 없이 규칙만 실행할 방법이 없다.
MVP(Model-View-Presenter)는 이 코드를 세 역할로 나눈다. 모델(Model)은 화면을 모르는 업무 기능으로, 이 예제에서는 상품을 검색하는 ICatalog가 모델이다. 뷰(View)는 화면 자체로, 사용자 입력을 받아 프레젠터에 넘기고 화면을 그린다. 프레젠터(Presenter)는 둘 사이에서 입력을 검사하고 모델을 호출한 뒤, 뷰에 무엇을 보여 줄지 정한다. 뷰가 모델을 직접 관찰해 스스로 갱신하는지, 프레젠터가 보여 줄 값을 모두 넘겨주는지는 변형마다 다르다. 이 문서의 예제는 뒤에서 설명할 Passive View처럼 프레젠터가 모두 넘겨준다.
프레젠터는 구체적인 폼 클래스가 아니라 ISearchView 같은 뷰 인터페이스만 안다. 뷰 인터페이스는 프레젠터가 화면에 요구하는 동작(검색어 읽기, 결과 보여 주기, 오류 보여 주기)을 모은 약속이다. 이 약속이 있으면 실제 폼 대신 호출을 기록하기만 하는 가짜 뷰를 끼워 프레젠터를 시험할 수 있고, 이 문서의 예제도 그래서 콘솔 뷰와 가짜 뷰를 함께 쓴다.
MVC 패턴도 모델과 화면을 나누지만 입력을 다루는 단위가 다르다. Smalltalk의 MVC는 체크 상자나 입력 칸 같은 GUI 객체 하나를 모델, 뷰, 컨트롤러로 표현하고, 모델이 변경을 알리면 뷰가 다시 그린다.1 Fowler는 MVP에서 위젯이 뷰와 컨트롤러로 나뉘지 않고, 프레젠터가 보통 위젯이 아니라 폼 단위로 놓이며, 사용자 이벤트에 어떻게 반응할지는 프레젠터가 정한다고 정리했다.2
구조
이 그림은 예제 전체의 역할을 한데 모은 것이다. InMemoryCatalog와 ConsoleSearchView는 첫 예제에, FakeSearchView는 범위 넓힌 예제에 나오며, 그 예제의 테스트용 카탈로그 ScriptedCatalog는 생략했다. SearchPresenter는 ISearchView와 ICatalog만 참조하므로, 콘솔 뷰와 가짜 뷰 어느 쪽과도 그대로 동작한다.
뷰는 클릭을 SearchRequested 이벤트로 넘길 뿐 검색어를 검사하지 않는다. 검사, 모델 호출, 보여 줄 내용의 결정은 모두 프레젠터 안에서 일어난다.
역사
Taligent의 Mike Potel은 1996년 글에서 MVP를 Smalltalk MVC를 일반화한 C++·Java용 프로그래밍 모델로 소개했다. 당시 Taligent는 IBM의 완전 자회사였다. 그는 프로그램 설계를 데이터 관리 쪽 질문 셋(모델, 선택(selection), 명령(command))과 사용자 인터페이스 쪽 질문 셋(뷰, 인터랙터(interactor), 프레젠터)으로 나눴다. 프레젠터는 고전 Smalltalk 컨트롤러를 애플리케이션 수준으로 끌어올린 것으로, 사용자가 일으킨 이벤트를 해석해 모델을 조작하는 명령으로 연결한다.3
Dolphin Smalltalk를 만든 Andy Bower와 Blair McGlashan은 ESUG 2000 튜토리얼 논문 Twisting the Triad에서 MVP를 택하기까지의 과정을 적었다. 1995년 여름 UI 프레임워크 작업을 시작해 위젯 기반 프레임워크를 먼저 만들었고, 그해 말부터 약 두 달 동안 MVC로 교체하는 작업을 거의 마쳤다. 그러나 두 가지가 맞지 않았다. MVC의 애플리케이션 모델은 뷰와 간접적으로만 연결되었고, Windows 기본 컨트롤은 이미 컨트롤러 기능 대부분을 갖고 있었다. 동료가 Taligent의 MVP를 알려 주어 관련 논문을 찾아 읽은 뒤, 프레젠터가 뷰와 직접 연결되는 구조를 채택했다. 단순함을 위해 Taligent 설계의 인터랙터는 기본 프레임워크에 넣지 않았다.4
2002년 Michael Feathers의 The Humble Dialog Box는 테스트 쪽에서 이 분리를 더 밀고 나갔다. 대화 상자 클래스보다 로직을 담은 클래스(Feathers는 smart object라고 불렀다)를 먼저 만들고, 그 클래스는 순수 가상 함수만 있는 뷰 인터페이스로 화면과 대화한다. 테스트에서는 이 인터페이스를 구현한 모의(mock) 뷰를 넣고, 대화 상자 클래스에는 단순한 위임과 값 설정만 남긴다. Feathers는 로직 객체가 뷰에 데이터를 밀어 넣는 이 방향이 대부분의 UI 프레임워크의 경향과 정반대라고 적었다.5 Fowler는 이 글이 원래 MVP보다 훨씬 깊은 방식으로 프레젠터를 쓰고, 그 대화 상자가 프레젠터가 조작하는 Passive View를 이룬다고 정리했다.6
Martin Fowler는 2004년 Model View Presenter라는 패턴 초안을 공개했다가, 2006년 7월 이를 Supervising Controller와 Passive View 두 패턴으로 나누고 원래 항목을 은퇴시켰다.7 같은 달 공개한 GUI Architectures에서는 MVP가 IBM에서 처음 나타났고 1990년대 Taligent에서 더 널리 알려졌으며, Potel의 논문이 가장 흔히 인용된다고 적었다.2
.NET에서는 2010년 MSDN Magazine에 Dino Esposito가 Windows Forms와 ASP.NET Web Forms에서 뷰 인터페이스와 프레젠터 클래스를 직접 작성하는 방식을 소개했다. 그는 뷰의 세부를 인터페이스로 추상화한 점을 MVP의 주요한 혁신으로 들었고, 프레젠터가 그 인터페이스만 알기 때문에 재사용과 테스트가 쉬워진다고 설명했다.8
연혁
연도 | 사건 | 출처 |
|---|---|---|
1970년대 후반 | Xerox PARC에서 Smalltalk의 MVC가 개발되었다 | |
1995 | Dolphin Smalltalk 팀이 위젯 기반 UI 프레임워크를 만든 뒤 그해 말 MVC로 교체하기 시작(약 두 달 소요) | |
1996 | Mike Potel, "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java" | |
2000 | Andy Bower, Blair McGlashan, "Twisting the Triad" (ESUG 2000 튜토리얼 논문) | |
2002 | Michael Feathers, "The Humble Dialog Box" | |
2004 | Martin Fowler가 Model View Presenter 패턴 초안을 공개(7월 19일) | |
2006 | Fowler가 MVP 항목을 Supervising Controller와 Passive View로 나누고 은퇴시킴(7월 11일), GUI Architectures 공개(7월 18일) | |
2010 | Dino Esposito가 MSDN Magazine에서 Web Forms·Windows Forms용 MVP 구현을 소개 |
- 연도
1970년대 후반
- 사건
Xerox PARC에서 Smalltalk의 MVC가 개발되었다
- 연도
1995
- 사건
Dolphin Smalltalk 팀이 위젯 기반 UI 프레임워크를 만든 뒤 그해 말 MVC로 교체하기 시작(약 두 달 소요)
- 연도
1996
- 사건
Mike Potel, "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java"
- 연도
2000
- 사건
Andy Bower, Blair McGlashan, "Twisting the Triad" (ESUG 2000 튜토리얼 논문)
- 연도
2002
- 사건
Michael Feathers, "The Humble Dialog Box"
- 연도
2004
- 사건
Martin Fowler가 Model View Presenter 패턴 초안을 공개(7월 19일)
- 연도
2006
- 사건
Fowler가 MVP 항목을 Supervising Controller와 Passive View로 나누고 은퇴시킴(7월 11일), GUI Architectures 공개(7월 18일)
- 연도
2010
- 사건
Dino Esposito가 MSDN Magazine에서 Web Forms·Windows Forms용 MVP 구현을 소개
Passive View와 Supervising Controller
Fowler가 나눈 두 변형은 뷰에 얼마나 많은 일을 남기느냐로 갈린다. Supervising Controller에서 사용자 입력은 모두 프레젠터(Fowler의 용어로는 컨트롤러)가 받아 처리한다. 뷰는 데이터 바인딩(화면 컨트롤의 값과 모델 데이터 중 한쪽이 바뀌면 프레임워크가 다른 쪽에 자동으로 반영하는 기능) 같은 선언적 방식으로 모델과의 단순한 동기화를 맡고, 바인딩으로 표현하기 어려운 화면 로직이 있을 때만 프레젠터가 뷰를 직접 조작한다.9
Passive View에서는 뷰가 아무것도 판단하지 않고, 위젯에 넣을 값까지 프레젠터가 정해서 밀어 넣는다. 뷰가 모델에서 스스로 갱신하지 않으므로 화면 로직은 모두 프레젠터에 있고, 뷰와 모델 사이에는 어느 방향으로도 의존이 없다. Fowler는 이 방식의 주된 동기를 테스트로 들었다. 뷰를 테스트 더블(test double, 테스트에서 실제 객체 대신 넣는 대역 객체)로 바꿔 프레젠터를 검사할 수 있기 때문이다.10 Supervising Controller 항목은 Passive View가 코드를 더 쓰게 만드는 대신 화면 동작 전체를 테스트할 수 있게 한다고 비교한다.9
이 문서의 예제는 Passive View 쪽이다. 뷰는 ShowResults와 ShowError로 받은 값을 그대로 출력하고, 오류 문구와 빈 결과 처리도 프레젠터가 정한다. 다만 Passive View는 처음부터 MVP의 모습은 아니었다. Potel과 Dolphin의 원래 MVP에서는 뷰가 옵서버로 모델을 관찰해 스스로 다시 그렸고,11 Fowler는 Passive View가 원래 MVP 설명에는 없었고 테스트 가능성을 찾는 과정에서 생겼다고 적었다.2
왜 사용하는가?
프레젠터는 뷰 인터페이스만 알기 때문에, 가짜 뷰를 넣으면 "짧은 검색어면 모델을 부르지 않는다" 같은 화면 규칙을 UI 없이 평범한 단위 테스트로 확인할 수 있다. 같은 프레젠터를 다른 UI 기술로 만든 뷰에 붙이는 것도 가능한데, Esposito는 이를 Windows Forms, Web Forms, WPF 사이의 재사용으로 설명했다.
대가로 화면마다 뷰 인터페이스, 뷰 구현, 프레젠터가 생겨 코드가 늘어난다. Esposito도 MVP가 애플리케이션 코드의 복잡도를 높이며, 그 비용은 작은 프로그램보다 큰 애플리케이션에서 감당하기 쉽다고 적었다. 입력 칸 몇 개에 규칙이 거의 없는 화면이라면 이벤트 핸들러에 바로 쓰는 편이 읽기 쉽다.
가짜 뷰로 하는 테스트는 프레젠터만 검증하므로, 실제 위젯에 값이 제대로 들어가는지나 화면 배치가 깨지지 않는지는 여전히 UI 테스트나 직접 확인으로 봐야 한다. 그래서 뷰 구현은 위임과 값 설정만 하도록 얇게 둔다. WPF처럼 양방향 데이터 바인딩을 중심으로 만든 프레임워크에서는 뷰가 바인딩할 상태를 노출하는 MVVM 패턴이 도구 지원을 받기 쉽다.12
예제
콘솔 뷰, 프레젠터, 메모리 카탈로그로 검색을 두 번 실행한다. 뷰는 입력을 이벤트로 넘기고 받은 내용을 출력만 한다.
using System;
using System.Collections.Generic;
using System.Linq;
var view = new ConsoleSearchView();
var presenter = new SearchPresenter(view, new InMemoryCatalog());
view.Type("ke");
view.ClickSearch();
view.Type("k");
view.ClickSearch();
abstract record SearchResult;
sealed record Found(IReadOnlyList<string> Items) : SearchResult;
sealed record Failed(string Reason) : SearchResult;
// 모델: 화면을 모르는 검색 기능이다.
interface ICatalog { SearchResult Search(string query); }
sealed class InMemoryCatalog : ICatalog
{
private readonly string[] products = ["keyboard", "mouse", "monitor", "keycap"];
public SearchResult Search(string query) =>
new Found(products.Where(p => p.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList());
}
// 뷰 인터페이스: 프레젠터가 화면에 대해 아는 것은 이것뿐이다.
interface ISearchView
{
string Query { get; }
event Action? SearchRequested;
void ShowResults(IReadOnlyList<string> items);
void ShowError(string message);
}
// 프레젠터: 입력을 검사하고 모델을 호출한 뒤, 뷰에 보여 줄 내용을 정한다.
sealed class SearchPresenter
{
private readonly ISearchView view;
private readonly ICatalog catalog;
public SearchPresenter(ISearchView view, ICatalog catalog)
{
this.view = view;
this.catalog = catalog;
view.SearchRequested += OnSearchRequested;
}
private void OnSearchRequested()
{
var query = view.Query.Trim();
if (query.Length < 2)
{
view.ShowError("Enter at least 2 characters.");
return;
}
switch (catalog.Search(query))
{
case Found found: view.ShowResults(found.Items); break;
case Failed failed: view.ShowError("Search failed: " + failed.Reason); break;
default: view.ShowError("Unexpected search result."); break;
}
}
}
// 콘솔 뷰: 입력을 받아 이벤트로 넘기고, 받은 내용을 그대로 출력만 한다.
sealed class ConsoleSearchView : ISearchView
{
public string Query { get; private set; } = "";
public event Action? SearchRequested;
public void Type(string text)
{
Query = text;
Console.WriteLine($"> search \"{text}\"");
}
public void ClickSearch() => SearchRequested?.Invoke();
public void ShowResults(IReadOnlyList<string> items) => Console.WriteLine("results: [" + string.Join(", ", items) + "]");
public void ShowError(string message) => Console.WriteLine("error: " + message);
}> search "ke"
results: [keyboard, keycap]
> search "k"
error: Enter at least 2 characters.한 글자 검색어를 걸러 낸 곳은 뷰가 아니라 프레젠터의 OnSearchRequested다. 이 최소 예제는 오류를 보여 줄 때 앞서 보여 준 결과 목록을 그대로 두며, 이 정책은 범위 넓힌 예제에서 정한다.
범위 넓힌 예제
이번에는 빈 검색 결과와 검색 실패를 화면에 어떻게 보여 줄지도 정한다. 검색 결과가 비면 안내 문구를 보여 주고, 모델이 실패하면 오류를 보여 준다. 오류를 보여 주는 경로(짧은 검색어, 결과 없음, 모델 실패)는 모두 ShowFailure를 거치며, 여기서 이전 결과 목록부터 비운다. 콘솔 뷰 대신 FakeSearchView를 넣어 단위 테스트처럼 프레젠터를 직접 몰아 본다. 가짜 뷰는 화면을 그리지 않고 받은 호출을 기록하며, ScriptedCatalog는 정해 둔 결과를 순서대로 돌려주고 받은 검색어를 남긴다.
using System;
using System.Collections.Generic;
using System.Linq;
var passed = 0;
var failed = 0;
// 1) 입력이 짧으면 모델을 부르지 않고, 이전 결과를 비운 뒤 검증 오류를 보여 준다.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog();
_ = new SearchPresenter(view, catalog);
view.Query = "k";
view.ClickSearch();
Check("short query", view, catalog,
["ShowResults()", "ShowError(Enter at least 2 characters.)"], []);
}
// 2) 모델이 실패하면 이전 결과를 비우고 오류를 보여 준다.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog(new Found(["keyboard"]), new Failed("catalog timeout"));
_ = new SearchPresenter(view, catalog);
view.Query = "key";
view.ClickSearch();
view.ClickSearch();
Check("failure clears old results", view, catalog,
["ShowResults(keyboard)", "ShowResults()", "ShowError(Search failed: catalog timeout)"],
["key", "key"]);
}
// 3) 앞뒤 공백은 프레젠터가 잘라서 모델에 넘긴다.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog(new Found([]));
_ = new SearchPresenter(view, catalog);
view.Query = " zz ";
view.ClickSearch();
Check("trim and no match", view, catalog,
["ShowResults()", "ShowError(No products match \"zz\".)"], ["zz"]);
}
Console.WriteLine($"{passed} passed, {failed} failed");
void Check(string name, FakeSearchView view, ScriptedCatalog catalog, string[] expectedCalls, string[] expectedQueries)
{
var ok = view.Calls.SequenceEqual(expectedCalls) && catalog.Queries.SequenceEqual(expectedQueries);
if (ok) passed++; else failed++;
Console.WriteLine($"{(ok ? "PASS" : "FAIL")} {name}");
Console.WriteLine(" view : " + string.Join(" | ", view.Calls));
Console.WriteLine(" catalog: " + (catalog.Queries.Count == 0 ? "(not called)" : string.Join(", ", catalog.Queries)));
}
abstract record SearchResult;
sealed record Found(IReadOnlyList<string> Items) : SearchResult;
sealed record Failed(string Reason) : SearchResult;
interface ICatalog { SearchResult Search(string query); }
// 가짜 모델: 정해 둔 결과를 순서대로 돌려주고, 받은 검색어를 기록한다.
sealed class ScriptedCatalog(params SearchResult[] script) : ICatalog
{
private int next;
public List<string> Queries { get; } = [];
public SearchResult Search(string query)
{
Queries.Add(query);
return script[next++];
}
}
interface ISearchView
{
string Query { get; }
event Action? SearchRequested;
void ShowResults(IReadOnlyList<string> items);
void ShowError(string message);
}
sealed class SearchPresenter
{
private readonly ISearchView view;
private readonly ICatalog catalog;
public SearchPresenter(ISearchView view, ICatalog catalog)
{
this.view = view;
this.catalog = catalog;
view.SearchRequested += OnSearchRequested;
}
private void OnSearchRequested()
{
var query = view.Query.Trim();
if (query.Length < 2)
{
ShowFailure("Enter at least 2 characters.");
return;
}
switch (catalog.Search(query))
{
case Found { Items.Count: 0 }:
ShowFailure($"No products match \"{query}\".");
break;
case Found found:
view.ShowResults(found.Items);
break;
case Failed failure:
ShowFailure("Search failed: " + failure.Reason);
break;
default:
ShowFailure("Unexpected search result.");
break;
}
}
// 오류와 함께 남은 이전 목록이 이번 검색의 결과처럼 보이지 않도록 먼저 비운다.
private void ShowFailure(string message)
{
view.ShowResults([]);
view.ShowError(message);
}
}
// 가짜 뷰: 화면을 그리지 않고, 프레젠터가 보낸 호출을 문자열로 남긴다.
sealed class FakeSearchView : ISearchView
{
public string Query { get; set; } = "";
public event Action? SearchRequested;
public List<string> Calls { get; } = [];
public void ClickSearch() => SearchRequested?.Invoke();
public void ShowResults(IReadOnlyList<string> items) => Calls.Add($"ShowResults({string.Join(", ", items)})");
public void ShowError(string message) => Calls.Add($"ShowError({message})");
}PASS short query
view : ShowResults() | ShowError(Enter at least 2 characters.)
catalog: (not called)
PASS failure clears old results
view : ShowResults(keyboard) | ShowResults() | ShowError(Search failed: catalog timeout)
catalog: key, key
PASS trim and no match
view : ShowResults() | ShowError(No products match "zz".)
catalog: zz
3 passed, 0 failed이 검사는 UI 프레임워크 없이 돌지만 프레젠터와 뷰 인터페이스 사이의 대화만 확인하며, 실제 폼이 ShowResults를 받아 목록을 제대로 그리는지는 이 검사의 범위 밖이다. 예제의 호출은 모두 동기적이지만 실제 검색은 보통 비동기로 실행된다. 빠르게 두 번 검색하면 먼저 보낸 요청의 응답이 나중에 도착해 새 결과를 덮어쓸 수 있다. 그런 화면에서는 프레젠터가 마지막 요청의 응답만 반영하도록 요청 번호를 비교하거나 이전 요청을 취소해야 하고, 응답을 UI 스레드에서 뷰에 반영하는 처리도 따로 필요하다. ScriptedCatalog는 준비한 결과보다 많이 호출되면 예외를 던지는 테스트용 대역이다.
TypeScript 예제
같은 구조를 TypeScript로 옮긴다. 이벤트 대신 onSearch로 콜백을 등록하고, 모델의 결과는 kind 필드로 구분하는 유니언 타입으로 표현한다. 두 번째 뷰에는 항상 실패하는 카탈로그를 붙였다.
type SearchResult =
| { kind: "found"; items: readonly string[] }
| { kind: "failed"; reason: string };
// 모델: 화면을 모르는 검색 기능이다.
interface Catalog { search(query: string): SearchResult; }
class InMemoryCatalog implements Catalog {
private readonly products = ["keyboard", "mouse", "monitor", "keycap"];
search(query: string): SearchResult {
const q = query.toLowerCase();
return { kind: "found", items: this.products.filter((p) => p.includes(q)) };
}
}
class DownCatalog implements Catalog {
search(_query: string): SearchResult {
return { kind: "failed", reason: "catalog timeout" };
}
}
// 뷰 인터페이스: 프레젠터가 화면에 대해 아는 것은 이것뿐이다.
interface SearchView {
readonly query: string;
onSearch(handler: () => void): void;
showResults(items: readonly string[]): void;
showError(message: string): void;
}
// 프레젠터: 입력을 검사하고 모델을 호출한 뒤, 뷰에 보여 줄 내용을 정한다.
class SearchPresenter {
constructor(private readonly view: SearchView, private readonly catalog: Catalog) {
view.onSearch(() => this.search());
}
private search(): void {
const query = this.view.query.trim();
if (query.length < 2) {
this.showFailure("Enter at least 2 characters.");
return;
}
const result = this.catalog.search(query);
if (result.kind === "failed") {
this.showFailure("Search failed: " + result.reason);
return;
}
if (result.items.length === 0) {
this.showFailure(`No products match "${query}".`);
return;
}
this.view.showResults(result.items);
}
// 오류와 함께 남은 이전 목록이 이번 검색의 결과처럼 보이지 않도록 먼저 비운다.
private showFailure(message: string): void {
this.view.showResults([]);
this.view.showError(message);
}
}
// 콘솔 뷰: 입력을 받아 이벤트로 넘기고, 받은 내용을 그대로 출력만 한다.
class ConsoleSearchView implements SearchView {
query = "";
private handler: () => void = () => {};
onSearch(handler: () => void): void { this.handler = handler; }
type(text: string): void {
this.query = text;
console.log(`> search "${text}"`);
}
clickSearch(): void { this.handler(); }
showResults(items: readonly string[]): void { console.log("results: [" + items.join(", ") + "]"); }
showError(message: string): void { console.log("error: " + message); }
}
const view = new ConsoleSearchView();
new SearchPresenter(view, new InMemoryCatalog());
view.type("k");
view.clickSearch();
view.type(" ke ");
view.clickSearch();
view.type("zz");
view.clickSearch();
const offline = new ConsoleSearchView();
new SearchPresenter(offline, new DownCatalog());
offline.type("ke");
offline.clickSearch();> search "k"
results: []
error: Enter at least 2 characters.
> search " ke "
results: [keyboard, keycap]
> search "zz"
results: []
error: No products match "zz".
> search "ke"
results: []
error: Search failed: catalog timeout정상 카탈로그든 실패하는 카탈로그든 보여 줄 내용은 같은 SearchPresenter가 정하고, 뷰 클래스는 어느 경우에도 스스로 판단하지 않고 받은 내용을 출력한다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.
각주
- Mike Potel. "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java". Taligent, Inc. (1996). PDF. Smalltalk Programming Model 절. ↩
- Martin Fowler. "GUI Architectures". martinfowler.com (2006년 7월 18일). 원문. Model-View-Presenter (MVP) 절. ↩
- Mike Potel. "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java". Taligent, Inc. (1996). PDF. Three User Interface Questions 절. ↩
- Andy Bower, Blair McGlashan. "Twisting the Triad: The evolution of the Dolphin Smalltalk MVP application framework". ESUG 2000 Tutorial Paper (2000). Internet Archive 사본. Model-View-Controller, Twisting the triad 절과 각주 5. ↩
- Michael Feathers. "The Humble Dialog Box". Object Mentor (2002). Internet Archive 사본. ↩
- Martin Fowler. "GUI Architectures". martinfowler.com (2006년 7월 18일). 원문. Humble View 절. ↩
- Martin Fowler. "Retirement note for Model View Presenter Pattern". martinfowler.com (2006년 7월 11일). 원문. Significant Revisions 항목. ↩
- Dino Esposito. "Cutting Edge: Better Web Forms with the MVP Pattern". MSDN Magazine 25(9) (2010년 9월). Microsoft Learn 보관본. MVP at a Glance 절. ↩
- Martin Fowler. "Supervising Controller". martinfowler.com (2006년 6월 19일). 원문. ↩
- Martin Fowler. "Passive View". martinfowler.com (2006년 7월 18일). 원문. ↩
- Andy Bower, Blair McGlashan. "Twisting the Triad: The evolution of the Dolphin Smalltalk MVP application framework". ESUG 2000 Tutorial Paper (2000). Internet Archive 사본. The View 절. ↩
- Dino Esposito. "Cutting Edge: Better Web Forms with the MVP Pattern". MSDN Magazine 25(9) (2010년 9월). Microsoft Learn 보관본. MVP at a Glance, MVP and MVVM, Postback 절. ↩