디자인 패턴은 앱의 규모와 요구사항에 따라 선택할 수 있으며, 단순한 앱에서는 MVP나 MVVM, 복잡한 네비게이션이 포함된 앱에서는 MVVM-C를 선호할 수 있다. 유지 보수를 위해 가독성이 좋고 의존성을 줄이고자 사용하는 것이기 때문에 누구나 알기 쉽게 작성하는 데 중점을 두어야 하고, 디자인 패턴을 위해 코드가 복잡해지는 것은 바람직하지 않다.
참고로 Apple에서 권장하는 디자인패턴은 Swift는 MVC, SwiftUI는 MVVM이라고 한다.
1. MVP (Model-View-Presenter)
Model, View, Presenter로 구성된 디자인 패턴으로, View와 Model을 분리하여 코드의 유지보수성을 높이는 구조이다.
- Model: 애플리케이션의 비즈니스 로직과 데이터 관리 부분을 담당한다. 데이터베이스와의 상호작용 또는 외부 API 호출 등을 처리한다.
- View: 사용자 인터페이스(UI)와 관련된 부분이다. 사용자가 볼 수 있는 화면 요소들을 나타내며, 사용자와의 상호작용을 처리한다.
- Presenter: View와 Model을 연결하는 역할을 한다. Presenter는 View에 데이터를 전달하고, 사용자의 액션을 Model에 전달하여 결과를 처리한 후 다시 View에 반영한다.
장점:
- View와 Model의 의존성이 줄어든다. 즉, UI와 비즈니스 로직을 명확하게 분리할 수 있다.
- Presenter는 UI와 독립적이기 때문에 유닛 테스트가 용이하다.
단점:
- 코드가 다소 복잡해질 수 있으며, Presenter가 너무 많은 책임을 지게 되어 관리하기 어려울 수 있다.
Counter.swift
/// Counter는 간단한 변수 하나만을 가지고 있지만 일반적으로
/// MVP의 Model은 데이터베이스와의 상호작용 또는 외부 API 호출을 맡는다.
struct Counter {
var count: Int = 0
}
CounterView.swift
/// View update Protocol
protocol CounterView: AnyObject {
func updateCounterLabel(with text: String)
}
CounterPresenterMVP.swift
import UIKit
/// View와 Model간 중개 역할을 하는 Presenter.
class CounterPresenterMVP {
private var model: Counter
weak var view: CounterView?
init(model: Counter, view: CounterView) {
self.model = model
self.view = view
}
func incrementCounter() {
model.count += 1
updateView()
}
func getCurrentCounterValue() {
updateView()
}
private func updateView() {
view?.updateCounterLabel(with: "Count: \(model.count)")
}
}
CounterViewControllerMVP.swift
import UIKit
class CounterViewControllerMVP: UIViewController {
...
private var presenter: CounterPresenterMVP?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
// ViewController는 Model을 알지 못한다.
presenter = CounterPresenterMVP(model: Counter(), view: self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter?.getCurrentCounterValue()
}
...
}
extension CounterViewControllerMVP : CounterView {
func updateCounterLabel(with text: String) {
label.text = text
}
}
2. MVVM (Model-View-ViewModel)
MVVM은 Model, View, ViewModel을 사용하여 UI와 비즈니스 로직을 분리하는 아키텍처이다. 특히, 데이터 바인딩을 활용하여 View와 ViewModel 간의 연결을 자동화할 수 있다.
- Model: 데이터와 비즈니스 로직을 담당한다. API 호출, 데이터베이스 연동 등과 같은 작업을 처리한다.
- View: 사용자 인터페이스(UI)로, 사용자가 보고 상호작용하는 화면이다.
- ViewModel: View와 Model을 연결하는 중간 역할을 한다. ViewModel은 View에 필요한 데이터를 Model에서 가져와 가공하여 View에 바인딩할 수 있게 전달한다. View는 ViewModel에 직접적으로 의존하지 않고, 바인딩을 통해 ViewModel을 참조한다.
장점:
- View와 Model 간의 의존성이 완전히 분리된다.
- 데이터 바인딩을 통해 UI 업데이트가 자동으로 이루어지기 때문에 코드가 깔끔해진다.
- 테스트가 용이하고, 코드가 재사용 가능성이 높다.
단점:
- 데이터 바인딩 구현이 초기에는 복잡할 수 있으며, 모든 플랫폼에서 동일하게 동작하지 않을 수 있다.
- ViewModel이 커지면 관리가 어려워질 수 있다.
CounterModel.swift
struct CounterModel {
var count: Int = 0
}
CounterViewModel.swift
class CounterViewModel {
private var model = CounterModel()
var countText: String {
"Count: \(model.count)"
}
var countUpdated: (() -> Void)?
func increment() {
model.count += 1
countUpdated?()
}
}
CounterViewControllerMVVM.swift
class CounterViewControllerMVVM: UIViewController {
private let viewModel = CounterViewModel()
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
bindViewModel()
label.text = viewModel.countText
}
private func bindViewModel() {
viewModel.countUpdated = { [weak self] in
DispatchQueue.main.async {
self?.label.text = self?.viewModel.countText
}
}
}
...
}
MVP & MVVM, 왜 비슷해 보일까?
|
MVVM과 MVP를 구분하는 방법
|
3. MVVM-C (Model-View-ViewModel-Coordinator)
MVVM-C는 MVVM 패턴을 확장한 아키텍처로, Coordinator를 추가하여 네비게이션(화면 전환) 로직을 관리한다.
- Model: 비즈니스 로직과 데이터를 처리한다.
- View: 사용자와의 상호작용을 처리하는 UI를 담당한다.
- ViewModel: UI와 비즈니스 로직 간의 데이터 전송을 담당한다. View에서 필요한 데이터를 Model에서 가져와 가공하여 전달한다.
- Coordinator: 화면 간의 네비게이션 로직을 처리한다. Coordinator는 ViewController들 간의 전환을 담당하며, ViewModel에 의존하지 않고 독립적으로 동작한다. 각 Coordinator는 하나의 화면 흐름을 책임진다.
장점:
- 네비게이션 로직을 Coordinator로 분리하여 코드의 유지보수성이 향상된다.
- ViewController의 책임이 줄어들고, UI 로직만 처리하게 된다.
- 화면 전환과 같은 복잡한 UI 흐름을 명확하게 관리할 수 있다.
단점:
- Coordinator 패턴이 복잡해질 수 있으며, 많은 화면이 포함된 앱에서는 관리가 어려워질 수 있다.
- 추가적인 구조가 필요하므로 초기 구성시에는 더 복잡할 수 있다.
4. Clean Architecture
Clean Architecture는 Robert C. Martin(Uncle Bob)이 제시한 소프트웨어 아키텍처로, 코드의 의존성을 명확하게 구분하고 테스트 가능성, 유지보수성, 확장성을 극대화하려는 구조이다.
Clean Architecture는 4개의 계층으로 나누어진다. 각 계층은 안쪽에서 바깥쪽으로 구분하며 바깥 계층은 안쪽 계층을 알고 있지만, 반대의 경우에는 알지 못한다.
- Entities: 비즈니스 로직과 규칙을 정의한 계층이다. 앱의 핵심적인 도메인 객체들이 이곳에 위치한다.
- Use Cases: 앱 내 기능을 정의하는 계층이다. 각 기능은 Use Case로 모델링되며, 실제 애플리케이션의 동작을 구현한다.
- Interface Adapters: 외부 시스템(예: 데이터베이스, API 등)과 애플리케이션 간의 데이터를 변환하는 계층이다. View와의 연결을 담당하며, 외부와의 인터페이스를 처리한다.
- Frameworks and Drivers: 외부 라이브러리나 프레임워크, 그리고 사용자 인터페이스를 포함한 외부 시스템과의 연결을 담당하는 계층이다.
장점:
- 각 계층이 독립적이고, 의존성이 안쪽에서 바깥쪽으로만 흐르므로 코드의 확장성이 뛰어나고 유닛테스트에 용이하다.
- 비즈니스 로직과 UI, 외부 시스템이 분리되어 있어서 테스트가 용이하고 유지보수성이 높다.
- 의존성 역전 원칙(DIP)을 지키므로, 외부 기술의 변화가 애플리케이션의 핵심 로직에 영향을 미치지 않는다.
단점:
- 초기 설계가 복잡할 수 있으며, 작은 프로젝트에서는 과도한 아키텍처로 느껴질 수 있다.
- 구현하는 데 시간이 더 걸릴 수 있으며, 계층 간의 상호작용을 관리하는 코드가 많아질 수 있다.
CounterEntity.swift
struct CounterEntity {
var count: Int = 0
}
CounterUseCase.swift (Interactor)
/// 비즈니스 로직
protocol CounterUseCaseProtocol {
func increment() -> Int
}
class CounterUseCase: CounterUseCaseProtocol {
private var entity = CounterEntity()
func increment() -> Int {
entity.count += 1
return entity.count
}
}
CounterPresenter.swift
protocol CounterPresenterProtocol {
var countText: String { get }
func incrementCount()
}
/// UseCase를 유저 친화적인 형태로 바꾸어주는 클래스.
class CounterPresenter: CounterPresenterProtocol {
private let useCase: CounterUseCaseProtocol
private weak var view: CounterViewProtocol?
private var count: Int = 0 {
didSet {
view?.updateCountText("Count: \(count)")
}
}
init(useCase: CounterUseCaseProtocol, view: CounterViewProtocol) {
self.useCase = useCase
self.view = view
}
var countText: String {
"Count: \(count)"
}
func incrementCount() {
count = useCase.increment()
}
}
블로그에 정리하려고 만든 아주 간단한 프로젝트 예제라, MVC나 MVP 정도가 적합한 것 같다. 모든 내용을 담지는 못하고 있어서 아쉽지만 아키텍쳐를 사용하는 이유는 어디까지나 유지보수를 위해서라는 점을 명심해야 한다.
'iOS > Swift' 카테고리의 다른 글
iOS 앱 개발자를 위한 RESTful API 실전 적용과 WebSocket (0) | 2025.01.06 |
---|---|
Swift) 값 타입과 참조 타입 그리고 클래스와 구조체 (Value type & Reference type, Class & Struct) (1) | 2021.01.05 |
Swift 앱의 생명주기와 헷갈리는 UI 관련 개념 정리 (2) | 2020.12.14 |
Swift 중요한 용어와 개념 (2) | 2020.12.14 |
Swift 클로저와 일급 함수 (2) | 2019.09.09 |