서론
Swift를 사용해서 iOS 개발을 하는 사람이라면 누구나 디자인패턴에 대해서 얘기를 들어보는 것은 물론 직접 사용하고 고민해 봤을 것입니다.
개발자이면서 동시에 여러 회사를 준비하는 입장에서 예상질문이나 JD에 항상 등장하는 것이 MVVM 경험에 대한 것이고,
그래서 오늘은 실제로 현업, JD에서 가장 자주 나오는 디자인패턴에 대해서 알아보려 합니다.
실제로 디자인 패턴을 선택할 때는
- 코드의 유지보수성
- 확장성
- 재사용성
을 향상하는데 중요한 역할을 하는 것을 인지하고 각각의 패턴에 대한 이해와 컨셉에 대해서 이해하는 것이 중요한 것 같습니다.
각각 패턴에 대해서 알아보고 간단하게, Count Example을 작성해 보겠습니다.
본론
먼저, 가장 많이 등장하는 MVVM 입니다.
MVVM ( Model -View- ViewModel )
MVVM 아래 세 가지로 이루어져 있습니다.
- VIew : 유저의 인터페이스를 받고 Model을 보여주는 화면
- VIewModel : 모델과 뷰의 브릿지역할을 해주는 역할. 뷰에 데이터를 바인딩하기 위해서 비즈니스 로직을 처리합니다.
- Model : 데이터 상태변경을 뷰모델에 알립니다.
사용이유
- MVVM을 사용하는 가장 큰 이유
- 표면적으로는 데이터 바인딩을 통한 뷰와 비즈니스 로직의 분리
- 이를 통한 뷰와 모델 간의 의존성을 분리할 수 있습니다.
- 개발방법론적으로는 테스트의 용이성과 유지보수성이 향상되기 때문입니다.
다른 디자인패턴과의 가장 큰 차이점
- 데이터 바인딩을 통한 뷰와 뷰모델 간의 동기화
MVVM의 핵심은 데이터 바인딩입니다.
- 테스트의 용이성
뷰모델은 UI로직을 캡슐화하고, 뷰와 독립적으로 테스트할 수 있습니다. 뷰와 상관없이 뷰모델의 코드만 수정해서 테스트를 가능하게 합니다.
MVC와의 차이점 : 뷰와 모델 간의 직접적 의존성이 없음
MVI와의 차이점 : 상태관리보다 데이터 바인딩에 중점을 둠
MVP와의 차이점 : MVVM 보다 뷰와의 의존성이 낮음
정도로 요약할 수 있고,
장점은 역시 데이터의 바인딩에 따른 뷰의 업데이트에 강점을 가진다는 것이고
반대로 단점은,
View에 표시되는 데이터가 많고 다양할수록 ViewModel이 Massive 해 질 수 있다는 것입니다.
이를 위한 해결책은 [여기](새로운 글 링크)에 조금 더 설명해 두겠습니다.
선택이유
- 비즈니스 로직과 뷰를 분리하기 위해
- RxSwift를 사용하여 데이터바인딩을 하기에 적합한 형태
- UI와 비즈니스 로직의 명확한 분리
MVVM은 뷰와 비즈니스 로직을 명확하게 분리하여, 각각의 유지보수와 확장성을 향상시킵니다.
- 데이터 바인딩을 통한 자동 업데이트
데이터 바인딩은 뷰모델의 상태 변화가 자동으로 뷰에 반영되도록 하여, 코드의 양을 줄입니다. 실제로 매번 비즈니스 로직이 일어날때마다 뷰를 수정하는 코드를 작성할 필요없이 바인딩 한번이면 viewModel의 어떤 로직에서든 해당 바인딩 값을 수정하면 뷰에 반영이 됩니다.
- 테스트와 유지보수의 용이성
뷰모델의 독립성과 데이터 바인딩의 자동화는 테스트와 유지보수를 용이하게 합니다.
Example ( MVVM )
- 실제로 현업에서는 RxSwift, RxCocoa를 사용이 거의 필수이기 때문에 이를 활용해서 예제를 작성해 보겠습니다.
// Model
struct CounterModel {
var count: Int = 0
}
// ViewModel
class CounterViewModel {
private var model = CounterModel()
// 데이터 바인딩을 위한 클로저
var countDidChange: ((Int) -> Void)?
func incrementCount() {
model.count += 1
countDidChange?(model.count)
}
}
// View ( ViewContrller )
class CounterViewController: UIViewController {
var viewModel = CounterViewModel()
var countLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// ViewModel과 뷰를 바인딩
viewModel.countDidChange = { [weak self] count in
self?.countLabel.text = "Count: \(count)"
}
}
@IBAction func incrementButtonTapped(_ sender: Any) {
viewModel.incrementCount()
}
}
- View에서. map은 고차함수 map은 아니고 Observable입니다. viewModel.count에서 방출한 요소를 저장하고 새로운 Observable로 변환하는 과정입니다.
고차함수 map과 비슷하지만 기본적으로 Obsevable이므로 착오 없으시길 바랍니다.
MVP ( Model - View - Presenter)
MVP 패턴은 다음과 같이 이루어져 있습니다.
- Presenter : 뷰와 모델 사이의 중개자 역할. UI를 업데이트하고 사용자 작업을 처리
- Model : 비즈니스 로직을 나타내고 데이터를 나타냄
- View : 유저의 인터페이스를 프레젠터에 전달하거나 프레젠터에서 받은 데이터로 UI를 업데이트한다.
가장 큰 특징
- 프레젠터가 뷰의 상태를 직접 관리합니다.
- 뷰는 프레젠터의 지시에 따라 UI를 업데이트한다.
사용 이유
- 뷰와 비즈니스 로직의 분리
- 테스트 용이성 : 프레젠터가 뷰의 구체적인 구현과는 분리되어 테스트에 용이하다.
가장 큰 차이점
뷰와 모델 사이의 중재자 역할을 하는 프레젠터를 통한 과정은 아래와 같습니다.
- 뷰의 이벤트 -> 프레젠터
- 프레젠터 -> 모델에 데이터 요청
- 모델 -> 프레젠터에 응답
- 프레젠터 -> 뷰로 데이터 및 변경 명령 전달
이 처럼 프레젠터는 뷰에 직접적으로 명령을 전달해서 관리합니다.
MVC와 비슷한 구조인데, MVC에서는 Controller가 View를 선택할 수 있어 1:N관계인 것에 비해
MVP의 프레젠터는 하나의 뷰에 대응되는 프레젠터를 가져 1:1의 관계를 가집니다.
참조링크
다른 패턴과의 차이는
- MVVM과의 차이: MVP 패턴에서는 Presenter가 있다는 점이 가장 큰 차이점입니다.
이를 활용해서 뷰와 비즈니스 로직을 분리하는 것은 MVVM과 같지만
프레젠터가 뷰의 상태를 직접 관리한다는 점에서 다릅니다. - MVI와의 차이 : 인텐트 기반의 상태관리보다는 명령-응답 방식에 중점을 둡니다.
장점은
뷰의 재사용성과 테스트가 용이하다는 것입니다.
여기서 1:1로 관계를 가진다면서 재사용성과 테스트가 용이?라는 의문이 들었는데요,
일단은 프레젠터는 구체적인 뷰의 구현과는 분리가 되어있어 테스트가 용이하다는 뜻이고
기능이 커지고 뷰가 많아질수록 관리해야 하는 프레젠터도 늘어나기 때문에
개발하면서 이를 잘 나누는 것이 중요하다고 합니다.
따라서 단점은,
MVVM보다는 뷰와 프레젠터의 결합이 강하다는 것
마찬가지로 프레젠터의 로직이 거대해진다면 유지보수가 어렵다는 점입니다.
Example ( MVP )
// Model
struct CounterModel {
var count: Int = 0
}
// View
protocol CounterView: AnyObject {
func displayCount(_ count: Int)
}
// Presenter
class CounterPresenter {
weak var view: CounterView?
private var model = CounterModel()
func incrementCount() {
model.count += 1
view?.displayCount(model.count)
}
}
// View Implementation
class CounterViewController: CounterView {
var presenter: CounterPresenter!
func displayCount(_ count: Int) {
// 업데이트 된 카운트를 화면에 표시
}
@IBAction func incrementButtonTapped(_ sender: Any) {
presenter.incrementCount()
}
}
- 개인적으로 Preseter를 사용하는 방식이 VIewModel과 유사한 것 같아서 차이점이나 반드시 MVP를 사용하는 이유에 대해서는 크게 와닿지는 않습니다.
- Protocol을 사용해서 뷰를 관리하고 이 Protocol이 프레젠터를 보고 있다는 점에서 결합도가 MVVM 보다는 강합니다.
MVI (Model-View-Intent)
MVI에서 가장 이해가 되지 않는 것은 Intet였는데요, GPT 형님은 사용자의 의도를 나타낸다고 추상적으로 설명해서...
조금 더 공부해 보니 결국 UseCase를 분리해서 나타낸다는 말 같았습니다.
사실 대부분 MVVM을 사용하고 MVI에 대한 경험을 원했던 회사는 1-2군데 확인했었던 것 같지만, 각각 디자인패턴에 대해 이해하고 있는 것은 의미가 있다고 생각합니다.
- Model: 앱의 데이터와 상태를 관리합니다. 인텐트의 작업을 처리하고 상태를 업데이트.
- View: 사용자 인터페이스 담당, 모델의 상태를 표시하여 상태를 변경하여 반영
- Intent: 사용자의 액션, 의도를 모델에 전달
MVI의 기본 콘셉트
- 사용자의 인텐트를 중심으로 한 상태 관리
- **상태와 인텐트를 명확하게 구분
- 상태 변화에 따른 뷰의 자동 업데이트
사용 이유
- 상태 관리의 명확성
- 예측 가능한 상태 변화
- 반응형 UI.
차이점
- MVVM과의 차이점: 상태 관리에 더 큰 중점을 둡니다.
- MVP와의 차이점: 사용자의 인텐트를 상태 변화로 직접 변환합니다.
장점
상태 변화의 예측 가능성과 반응형 UI
단점
- 복잡한 상태 관리 로직
- 학습 곡선과 구현의 복잡성
- VIewModel과 같은 비즈니스 로직을 처리하기 위한 객체가 반드시 필요하며 이는 코드의 복잡성을 높일 수 있다고 생각합니다.
Example ( MVI )
// Model
struct CounterState {
var count: Int = 0
}
// Intent
enum CounterIntent {
case increment
}
// ViewModel
class CounterViewModel {
private var state = CounterState()
var updateView: ((CounterState) -> Void)?
func processIntent(_ intent: CounterIntent) {
switch intent {
case .increment:
state.count += 1
updateView?(state)
}
}
}
// View
class CounterViewController {
var viewModel = CounterViewModel()
init() {
viewModel.updateView = { [weak self] state in
// 업데이트 된 상태를 화면에 표시
}
}
@IBAction func incrementButtonTapped(_ sender: Any) {
viewModel.processIntent(.increment)
}
}
- 언제고 JAVA인가, Flutter를 공부하면서 UseCase별로 클래스를 생성해서 아키택처를 잡았던 기억이 있는데 비슷한 방식 같습니다.
- Intent를 통해서 뷰에서 일어날 수 있는 이벤트와 의도를 관리할 수 있다는 점에서 보다 명확할 수 있고 관리하기 편해 보입니다. 그러나 위에서도 언급했듯 너무 많은 이벤트는 도리어 독이 될 수 있다고 생각합니다.
결론
현업에서 가장 많이 언급되는 UIKit기반의 iOS 디자인패턴이었습니다.
저부터 늘 들어는 봤는데 명확히 알지 못했던 것을 정리해서 조금은 후련합니다.
대부분 MVVM을 따르기도 하고 사실 RxSwift를 사용하기 위해서 MVVM을 사용하는 경우도 왕왕 있다고 알고 있습니다. 그럼에도 우리는 그렇게 답할 수는 없기 때문에...!! 각각 디자인패턴을 선택하는 이유와 그렇지 않은 이유에 대해서 숙지하고 결정하는 개발자가 됩시다!
PS.
SwiftUI는 그 자체로 ViewModel을 역할을 하고 있기 때문에 MVVM을 사용하는 것이 점점 지양되는 분위기입니다. 이를 위해서 TCA라는 패턴이 도입되어 발전 중입니다.
이에 대해서는 별도로 다시 다뤄보겠습니다.
'Swift' 카테고리의 다른 글
[Swift]Configuration, flag을 이용한 #if 전처리문 제어 (1) | 2024.11.04 |
---|---|
[Swift/iOS] tagView, ChipView를 만들어보자! (0) | 2024.05.15 |
[Swift] 고차함수 Flatmap, CompatMap, map 의 사용 (0) | 2023.11.21 |
[Swift] ClosedRange (1) | 2023.10.23 |
[Swift]iOS 17 업데이트로 인한 Apple의 URL Parsing 변경 이슈 (1) | 2023.10.06 |