MVP 패턴은 Model-View-Presenter로 이루어져 있다. 지금까지 접해본 아키텍처는 모두 MV* 형태였기 때문에, 이제는 Model과 View가 어떤 책임을 찾는 객체인지 모르면 바보다.

MVP 아키텍처 다이어그램
그냥 보면 View와 Model을 중재하는 객체가 존재한다는 점에서 MVVM 아키텍처와 다를 게 없어보인다. (UIKit 프로젝트에서) MVP 패턴에서도 MVVM처럼 Controller가 View에 속하여 사용자의 상호작용에 따른 이벤트를 처리한다. 둘 다 UI 코드와 비즈니스 로직을 분리하는 아키텍처로, 그냥 봤을 때 차이라고는 ViewModel의 존재가 Presenter로 대체된 걸로 밖에 안보이는데, 이 두 아키텍처의 차이는 어디서 발생할까?
우선 ViewModel은 View를 모르지만(참조하지 않지만) Presenter는 View를 Protocol로 추상화하여 간접 의존한다(이 때, 약한 참조 필요). MVVM에서는 바인딩을 통해 ViewModel의 상태 변경이 View에 자동 반영되지만 MVP에서는 Presenter가 View에 접근하여 수동으로 업데이트를 요청한다.
코드 생김새를 직접 보고 차이를 체감해보자.
MVVM에서의 데이터-UI 바인딩 코드class ExampleViewModel: ObservableObject {
@Published var examples: [Example] = []
private let service: ExampleServiceProtocol
private var cancellables = Set<AnyCancellable>()
init(service: ExampleServiceProtocol) {
self.service = service
}
func loadExamples() {
service.fetchExamples()
.sink { [weak self] examples in
self?.examples = examples
}
.store(in: &cancellables)
}
}
class ExampleViewController: UIViewController, UITableViewDataSource {
private let viewModel = ExampleViewModel(service: ExampleService())
private var cancellables = Set<AnyCancellable>()
private let tableView = UITableView()
private var examples: [Example] = []
override func viewDidLoad() {
super.viewDidLoad()
bind() // 바인딩 메소드를 스스로 호출
}
private func bind() {
viewModel.$examples
.receive(on: DispatchQueue.main)
.sink { [weak self] examples in
self?.examples = examples
self?.tableView.reloadData()
}
.store(in: &cancellables)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = examples[indexPath.row].text
return cell
}
}
MVP의 UI 갱신 코드protocol ExampleProtocol: AnyObject {
func showExamples(_ examples: [Example])
}
class ExamplePresenter: NSObject {
private weak var viewController: ExampleProtocol? // presenter가 controller 의존
private let service: ExampleServiceProtocol
init(viewController: ExampleProtocol, service: ExampleServiceProtocol) {
self.viewController = viewController
self.service = service
}
func loadExamples() {
service.fetchExamples { [weak self] examples in
self?.viewController?.showExamples(examples) // view에 접근하여 UI 업데이트 메소드 호출
}
}
}
class ExampleViewController: UIViewController, ExampleProtocol, UITableViewDataSource {
private lazy var presenter = ExamplePresenter(viewController: self, service: ExampleService())
private var examples: [Example] = []
private let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
presenter.loadExamples() // presenter를 통해 UI 업데이트에 필요한 동작 호출
}
// UI update 메소드
func showExamples(_ examples: [Example]) {
self.examples = examples
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = examples[indexPath.row].text
return cell
}
}
viewDidLoad에서 호출하는 코드를 주목해보면, MVVM에서는 바인딩 메소드를 컨트롤러가 스스로 구현하고 호출하지만 MVP에서는 컨트롤러가 UI 업데이트 메소드를 구현은 하지만 호출은 Presenter를 통해 데이터 업데이트와 함께 이루어진다.
사실 MVP 아키텍처를 공부해보기로 마음먹고 처음 이 부분을 실습했을 때 viewController-presenter-viewController로 이어지는 호출 흐름이 다소 복잡하고 비효율적으로 느껴졌다. (솔직히 지금도 그 생각은 크게 바뀌지 않았다. 그래서 MVP보다 MVVM이 인기가 많은 게 아닐까?)
하지만 이렇게 되는 이유는 Presenter의 역할이 View의 이벤트를 받고, Model에서 데이터를 가져와 가공 후 다시 View에 전달하는 것이기 때문이다. 반면 ViewModel의 역할은 View가 필요한 데이터와 상태를 가공해서 제공하는 데에 있다. ViewModel은 이벤트를 받지 않고, View가 ViewModel을 관찰하며 UI를 업데이트한다.
MVVM 아키텍처는 SwiftUI와 궁합이 좋다고 알려져있지만 UIKit과도 나쁘지 않게 작용한다. 하지만 MVP 아키텍처는 UIKit에만 최적화되어 있다. 데이터 변화에 따른 UI 갱신을 Presenter가 명시적으로 호출하기 때문에 명령형 UI 업데이트 패턴에 적합하고 선언적인 SwiftUI의 철학과는 맞지 않는 것이다.
💡 정리
- UIKit + 이벤트 중심 → MVP가 구조 명확
- SwiftUI + 상태 중심 → MVVM이 자연스러움
- UIKit에서도 RxSwift나 Combine 같은 리액티브 툴을 쓰면 MVVM 구현이 쉬움
- MVP는 UI 갱신 흐름을 명시적으로 제어해야 하고, MVVM은 상태 변화를 데이터 바인딩으로 자동 처리함