Design Patterns: MVVM, MVVM-C, MVP, Clean Architecture

2025. 1. 7. 23:55·iOS/Swift

디자인 패턴은 앱의 규모와 요구사항에 따라 선택할 수 있으며, 단순한 앱에서는 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, 왜 비슷해 보일까?

  1. iOS에서는 데이터 바인딩을 별도의 라이브러리 없이 구현하면, MVVM에서도 데이터 전달을 위해 클로저나 프로토콜을 사용할 수 있다. 이런 경우, MVP와 MVVM의 코드 구조가 매우 유사해질 수 있다.
  2. MVVM과 MVP 모두 View와 비즈니스 로직을 분리하려는 목적이 있어, 큰 그림에서 유사한 컴포넌트들이 생긴다.

MVVM과 MVP를 구분하는 방법

  1. MVVM:
    • ViewModel은 View와 독립적이어야 하며, 데이터 바인딩을 통해 간접적으로 통신.
    • Presenter와 달리 View를 전혀 참조하지 않음.
    • 클로저 또는 Reactive Programming(RxSwift, Combine)을 적극 활용.
  2. MVP:
    • Presenter는 View의 참조를 필요로 하며, View를 적극적으로 업데이트.
    • 명령형 스타일로 Presenter가 View에 데이터를 전달.

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
'iOS/Swift' 카테고리의 다른 글
  • iOS 앱 개발자를 위한 RESTful API 실전 적용과 WebSocket
  • Swift) 값 타입과 참조 타입 그리고 클래스와 구조체 (Value type & Reference type, Class & Struct)
  • Swift 앱의 생명주기와 헷갈리는 UI 관련 개념 정리
  • Swift 중요한 용어와 개념
hyunjicraft
hyunjicraft
모든 것을 기록하고 싶었지만 복잡하지 않은 것만 기록하게 된 블로그
    반응형
  • hyunjicraft
    개발망고발
    hyunjicraft
  • 전체
    오늘
    어제
    • 분류 전체보기
      • iOS
        • Swift
        • RxSwift
      • 공부하기
        • React
        • Python
        • 다른 PL
        • Figma
      • 스타트업
      • 글쓰기
        • 회고
  • 블로그 메뉴

    • 태그
  • 인기 글

  • 태그

    알고리즘
    중니어
    마스터 컴포넌트 연결 해제
    피그마 인스턴스
    Communication Patterns
    블렌더 g키
    생활코딩
    react
    ios system architecture
    computer systems
    Python
    맥에서 블렌더
    스타트업개발
    daummap
    RxSwift 이미지 다운로드
    URLSessionDataTask
    블렌더
    문자열 포맷
    RxSwift 비교
    swift
    setState()
    함수방식 컴포넌트
    비동기 프로그래밍
    RxSwift image download
    게임런칭
    스타트업경험
    기술적도전
    swift codable
    mvvm-c
    blender g
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
hyunjicraft
Design Patterns: MVVM, MVVM-C, MVP, Clean Architecture
상단으로

티스토리툴바