[Flutter] GetX로 보는 MVVM (vs MVC)
오늘은 Flutter에서 많이 사용하는 상태 관리 패키지 중 하나인 GetX를 활용하여, MVVM 패턴과 MVC 패턴을 비교해보려고 한다. 그 전에, 그동안 헷갈리던 MVC 패턴, MVP 패턴, MVVM 패턴을 복습해보자.
MVC vs MVP vs MVVM
우선 두 패턴 모두 M과 V를 가진다. M은 Model, V는 View를 의미한다.
- Model: 데이터의 구조를 정의하고, 데이터를 처리하는 역할을 한다. 예를 들면, 모델 클래스 내부에
fromJson()메소드를 정의하는 것도 데이터 처리 과정의 일부라고 볼 수 있다. - View: 위젯 등을 통해 사용자에게 보여지는 UI를 정의한다.
이 Model과 View 사이의 의존성을 어떻게 처리하느냐에 따라 아키텍처 패턴이 달라진다.
1️⃣ MVC 패턴 (Model - View - Controller)
전통적인 방식이다.
- 사용자 입력은 Controller가 받는다.
- Controller는 입력에 따라 Model을 업데이트한다.
- Model의 변화는 View로 반영된다. 이때 View를 직접 업데이트하는 것은 Controller가 아니라 Model이다.
Model이 View를 업데이트하는 방식에는 여러 가지가 있다. 보통 Model이 자신의 변화를 View에 알리거나, View가 주기적으로 Model을 가져와 스스로 업데이트한다.
MVC의 특징은 다음과 같다.
- 가장 단순한 구조다.
- 뷰와 컨트롤러는 n:1 관계를 가진다.
- Model과 View 사이의 의존성이 높다.
- Controller에서 너무 많은 역할을 하게 돼서 나중에 엄청 복잡해진다.
MVC는 오래된 패턴인 만큼, 플랫폼에 따라 다양하게 해석되고 변형되어 왔다.
변형된 형태에서는, Controller가 View를 직접 업데이트하기도 한다. 하지만 이것은 전통적인 MVC라기보다는, 최근의 MVVM 등의 패턴들과 비교하기 위해서 간략화된 MVC라고 이해하는 것이 좋다.
참고 : Django에서는 MVC가 아닌 MTV(Model - Template - View) 패턴을 사용한다. 이때 Django의 Template이 View 역할을, Django의 View가 Controller 역할을 한다.
2️⃣ MVP 패턴 (Model - View - Presenter)
MVC에서 변형되었다.
Presenter가 Model과 View를 이어준다.
- 사용자 입력을 View에서 받는다.
- View는 해당 입력을 Presenter에게 전달한다.
- Presenter는 Model을 업데이트하고, View도 직접 업데이트한다.
MVP의 특징은 다음과 같다.
- MVC보다 계층이 명확해졌다.
- Model과 View 사이에 의존성이 없다. 가운데에서 Presenter가 다 해주기 때문이다.
- View와 Presenter는 1:1 관계를 갖는다. 그래서 View와 Presenter 간의 의존성은 강하다.
- View의 수만큼 Presenter를 만들어야 하므로 다소 번거롭다.
3️⃣ MVVM 패턴 (Model - View - ViewModel)
MVP의 단점을 보완한 구조다.
ViewModel이 Model과 View를 이어주는데, 상태(state)를 바탕으로 한다는 특징이 있다.
- 사용자 입력을 View에서 받는다.
- View는 해당 입력을 ViewModel에게 전달한다.
- ViewModel이 Model을 업데이트한다. 하지만, View는 업데이트하지 않는다(ViewModel은 View의 존재를 모른다!🚨).
- 대신, View는 ViewModel의 상태를 구독하여, 상태가 바뀌었을 때 자신이 자동으로 갱신된다.
MVVM의 특징은 다음과 같다.
- ViewModel이 View와 Model 사이의 의존성을 분리해준다.
- View와 ViewModel은 n:1 관계를 가진다. View가 ViewModel을 참조하긴 하지만, ViewModel이 View를 모르기 때문에, 계층 간의 결합이 가장 느슨하다.
- 확장성이 뛰어나다.
추가로, Repository를 통해 ViewModel이 데이터를 요청하는 형태의 구조가 많은데, 이는 API나 DB에 따른 추상화 계층을 만들기 위해 사용된다. 대규모 프로젝트에서 권장된다.
GetX 코드를 통해 본 MVC와 MVVM
유튜버 ‘개발하는 남자’ 님의 영상을 보고 학습했는데, 영상의 소스 코드)에서 MVC -> MVVM으로 전환되는 과정을 한눈에 확인할 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// mvvm_getx_controller.dart
import 'package:dev_pattern_sample/src/model/model.dart';
import 'package:get/get.dart';
class MVVMGetxController extends GetxController {
- Rx<Model> model = Model().obs;
+ late Model model;
+ RxInt count = 0.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ model = Model();
+ }
void incrementCounter() {
- model.update((val) {
- val!.incrementCounter();
- });
+ model.incrementCounter();
+ count(model.counter);
}
void decreamentCounter() {
- model.update((val) {
- val!.decrementCounter();
- });
+ model.decrementCounter();
+ count(model.counter);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// mvvm_getx_view.dart
import 'package:dev_pattern_sample/src/mvvm_getx/mvvm_getx_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
class MVVMGetxView extends GetView<MVVMGetxController> {
const MVVMGetxView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MVC 패턴')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(
- () => Text(controller.model.value.counter.toString(),
+ () => Text(controller.count.toString(),
style: TextStyle(fontSize: 150)),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
controller.incrementCounter();
},
child: const Text('+')),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
controller.decreamentCounter();
},
child: const Text('-')),
),
],
)
],
),
),
);
}
}
위 코드를 보면, 이렇게 모델 전체가 아닌 count만 observe하도록 변경되었다.
1
2
3
4
5
6
// 기존
Rx<Model> model = Model().obs;
// 변경 후
late Model model;
RxInt count = 0.obs;
기존에는 Rx<Model>을 관찰(.obs)했지만, 이제는 RxInt count만 관찰한다. View에서 모델 내부를 직접 접근해서 관찰하는 구조에서, ViewModel이 모델의 상태를 대표해주는 구조로 바뀐 것이다.
☀️ View가 자신을 자동으로 갱신한다
1
2
Obx(() => Text(controller.count.toString(),
style: TextStyle(fontSize: 150)))
Obx()로 감싼 위젯은 controller.count가 바뀔 때 자동으로 리렌더링된다. 즉, setState() 없이도 상태 변화가 UI에 반영된다.
관찰(.obs)할 대상이 controller.count밖에 없기 때문에, View는 오직 이 숫자가 바뀌는지만 신경쓰면 된다.
1
2
3
4
void incrementCounter() {
model.incrementCounter();
count(model.counter); // count를 갱신 → Obx가 감지하여 UI 업데이트
}
기존에도 Obx()를 사용했지만, controller.model.value.counter를 직접 읽고 있었기 때문에, View가 Model에 간접적으로 의존하고 있었다. 즉, 완전한 MVVM은 아니었다.
지금의 View는 오직 ViewModel의 count만 바라보므로, 더 명확한 MVVM 구조가 되었다고 볼 수 있다.
☀️ Model과 View 사이의 의존성이 사라진다
1
2
3
4
5
// 기존
Text(controller.model.value.counter.toString())
// 변경
Text(controller.count.toString())
기존에는 View가 Model의 내부 필드인 counter에 직접 접근하고 있었다. 이 구조에서는 Model의 내부가 바뀌면 View 코드도 수정해야 했다. 예를 들어, Model의 counter가 counter1로 이름이 바뀐다면 저 View 코드에서 수정해야 했다.
하지만 지금 구조에서는 ViewModel 코드를 수정하면 된다.
마무리
Spring의 MVC나 Django의 MTV와는 다르게, Flutter는 특정 아키텍처 패턴을 강제하지 않는다고 한다. GetX도 마찬가지다. 그래서 개발자가 어떤 구조로 설계할 건지 스스로 선택해야 한다. 그렇기에 각 패턴의 특징을 정확하게 이해하고, 내가 만들고자 하는 앱에 가장 적합한 구조를 선택할 수 있어야겠다는 생각이 들었다.
이번 비교를 통해 MVVM 구조가 코드 가독성에서도 그렇고, 유지보수 측면에서도 더 낫다는 걸 알게 되었다. 앞으로 프로젝트에서도 GetX를 이용해 MVVM 구조를 적극적으로 적용해봐야겠다.
참고자료
[ 디자인 패턴 - 3부 ] 플러터에서 mvvm 패턴 어떻게 사용될까? - Getx , Provider 로 mvvm 패턴 적용
안드로이드 개발자를 위한 MVC, MVP, MVVM, MVI 아키텍쳐 끝장정리— 1