Swift - MVC, MVVM 패턴


MVC와 MVVM 패턴이 무엇인지 알아보자.


MVC, MVVM 등 개발할 때 많이 듣는 얘네들은 디자인 패턴이다. 디자인 패턴은 공통으로 발생하는 문제에 대한 재사용 가능한 해결책으로, 일반적인 문제에 대한 해결책들이 패턴화 되어 있기 때문이 디자인 패턴을 이용하면 더 효율적이고 빠르게 개발을 할 수 있는 것이다. 프레임워크와 라이브러리도 하나의 디자인 패턴으로 볼 수 있다.

참고로 디자인 패턴은 오래전부터 연구되던 소프트웨어 공학 분야이기 때문에 종류가 다양하다.

image

위와 같이 굉장히 다양한 패턴들이 많이 있는데, 이 중에서도 MVC 패턴은 Observer 패턴과 Mediator 패턴을 섞어놓은 패턴이다.

MVC

MVC는 Model + View + Controller를 합친 말로, observer pattern과 mediator pattern을 섞어놓은 것으로 알려져 있다.

  • observer pattern: 하나의 model에 다양한 view를 둬서 상태가 변할 때 그 모델을 구독하는 객체(observer)가 자동으로 갱신 됨
  • mediator pattern: Observer pattern에서 M:N 관계가 많이 생성되면 전체가 복잡해지기 때문에 중간에 중재자(mediator)를 둬서 이벤트가 발생하고 전달하는 것을 단순화 함

그래서 Presentation 부분과 logic이 분리되어 앱의 여러 부분들을 간결하게 볼 수 있다는 이유로 버그 픽스에 유리하다. 핵심은 비즈니스 로직과 presentation layer를 분리해놓은 것이다. 이 패턴은 데스크탑 GUI(graphical user interface)에서 주로 사용된다.

구조

스크린샷 2021-05-09 오후 2 18 14

  • Model : 어플리케이션에서 사용되는 데이터데이터를 처리하는 부분이다. 데이터, 알고리즘, DB 등을 Model이라 하는데, 앱이 무엇을 처리할 것인지를 정의한다.
  • View : 사용자에게 보여지는 UI 부분이다. 사용자에게 보여 줄 정보들을 Controller, Model에서 가져와서 보여주고, 입력을 받을 수 있다.
  • Controller : 사용자의 입력(Action)을 받고 처리하는 부분이다. Model이나 View의 요소들을 유저의 요청에 맞게 어떻게 처리 할 것인지를 정의한다.

위의 내용을 다시 말하자면 아래와 같다.

  • Model은 어떻게 보일지(View)나 어떻게 처리할지(Controller)에 대해 신경쓰지 않아도 된다. 데이터는 그 자체로 존재한다.
  • View는 데이터(Model)나 로직(Controller)를 신경쓰지 않고 이를 출력하는데만 관심 있다.
  • Controller는 무엇을 처리할지나 어떻게 출력할지는 관심이 없다.

그래서 각각 역할이 분리되어 있는데, 이렇게 역할이 확실히 분리될 수록 디자인 패턴이 잘 적용됐다고 할 수 있다.

동작

  1. 사용자가 view와 상호작용한다.
  2. View는 특정 이벤트를 controller에게 알린다.
  3. Controller가 Model을 update한다.
  4. Model이 자기가 바뀐 걸 Model에게 알린다.
  5. View는 모델 데이터를 가져와서 자기 자신을 업데이트 한다.

참고로 View가 업데이트 되는 방법은 아래와 같이 세 가지 정도가 있다.

  • View가 Model을 이용해서 직접 업데이트 하는 방법
  • Model에서 View에게 notify 해서 업데이트 하는 방법
  • View가 Polling으로 주기적으로 Model의 변경을 감지해서 업데이트

특징

Controller는 여러개의 View를 선택할 수 있는 1:n구조이다.

Controller는 View를 선택할 뿐 직접 업데이트 하지 않는다. 여기서 View는 Controller를 모른다.

장점

가장 단순하고, 많이 사용되는 디자인 패턴이다.

단점

View-Model 간 의존성이 높다. 이렇게 의존성이 높아지면 어플리케이션이 커질수록 복잡해지고 유지보수를 어렵게 한다.

MVP

MVP는 Model + View + Presenter를 합친 용어다. Model과 View는 MVC 패턴과 동일하며, Controller대신 Presenter가 존재한다.

스크린샷 2021-05-09 오후 2 24 16

  • Model : MVC와 동일하게 데이터와 데이터를 처리하는 부분이다.
  • View : MVC와 동일하게 사용자에게 보여지는 UI부분이다.
  • Presenter : View에서 요청한 정보로 Model을 가공해 View에 전달해주는 부분이다. View와 Model을 연결해주는 역할을 한다.

동작

  1. Action은 View를 통해 들어간다.
  2. View는 데이터를 Presenter에 요청한다.
  3. Presenter는 Model에게 데이터를 요청한다.
  4. Model은 Presenter에게 요청받은 데이터를 응답한다.
  5. Presenter는 View에게 데이터를 응답한다.
  6. View는 Presenter가 응답한 데이터를 이용해서 화면에 출력한다.

특징

Presenter가 View와 Model을 이어주는 중개자 역할을 한다. Presenter와 View는 1:1 관계다. 위의 MVC 패턴에서 View와 Controller의 관계는 n:1이었다.

장점

View와 Model의 의존성이 없다. MVP 패턴은 MVC 패턴의 단점이었던 View와 Model의 의존성을 해결했다. (Presenter를 통해서만 데이터를 전달받았기 떄문에)

단점

View와 Model 사이의 의존성은 해결되었지만 View-Presenter 사이의 의존성이 높게 된다. 어플리케이션이 복잡해질수록 View-Presenter 의 의존성이 강해진다.

MVVM

MVVM은 Model + View + ViewModel을 합친 용어다. MVVM은 MVC가 있는데 왜 등장한 것일까? 모든 것에 한계가 있듯이, MVC도 시스템의 규모가 커질수록 한계가 드러났다.

  • Model과 View가 M:N 관계로 복잡해진다. 의존성 문제가 생긴다.
  • 모든 로직, 처리가 Controller에게 전가 될 수 있다. 이럴 경우 controller의 역할이 매우 커져서 필요에 따라서는 model 쪽에 이런 로직들을 분산시켜 처리해야 한다.

MVVM의 view model은 value converter로, 모델의 데이터 객체들을 관리하고 보여주기 쉽게 출력해야 한다는 역할을 가지고 있다.

MVC vs MVVM?

이와 관련해서 stackoverflow에서 굉장히 투표를 많이 받은 글이 있어서 가져와봤다.

일단 MVC/MVVM은 선택의 문제가 아니다. 두 패턴은 ASP.Net과 Silverlight/WPF 개발 모두에서 다른 방식으로 나타난다.

ASP.Net에서 MVVM은 데이터와 뷰를 two-way binding을 하기 위해 사용된다. 참고로 two-way binding은

  1. Model의 프로퍼티가 업데이트 되면, UI도 업데이트 된다.
  2. UI 요소가 업데이트 되면, 이 변화가 model에 전달된다.

를 의미한다.

image

그림으로 나타내면 위와 같다.

그리고 MVC는 서버 단의 여러 문제들을 분리하기 위한 다른 방법이다.

Silverlight와 WPF에서 MVVM 패턴은 MVC에 비해 포괄적이고, MVC의 대체재처럼 보일 수 있다. 이 패턴의 viewModel은 단순히 MVC의 controller 를 대체했다는 가정도 있다.

ViewModel이 별도의 Controller의 필요성을 꼭 대체하는 것은 아니다. 즉 Controller 대신에 꼭 vm을 써야 하는 법은 없다는 소리다.

문제는 개별로 테스트가 가능해야 하고, 필요할 때 재사용 되기 위해 viewModel은 어떤 뷰가 출력하는지, 어디에서 데이터가 오는 지 알면 안된다. 보통 controller들은 unit test를 요구하는 로직들을 vm에서 제거한다. 이러면 vm은 굉장히 적은 양의 test를 요구하는 “멍청한” 컨테이너가 된다. 이는 vm을 디자이너와 코더 사이의 연결 고리로 만드는 좋은 경우가 되기 때문에 단순하게 만드는 것이 좋다. 하지만 여전히 컨트롤러를 만들어서 앱의 로직을 다루게 된다.

우리가 따르는 기본적인 MVCVM 가이드는 아래와 같다.

  • 뷰는 데이터의 특정 형태를 출력한다. 데이터가 어디에서 오는지는 관심없다.
  • 뷰모델은 데이터와 커맨드의 특정 형태를 가지고 있고, 데이터나 코드가 어디에서 오는 지, 어떻게 출력되는지 모른다.
  • 모델은 실제 데이터를 가지고 있다.
  • 컨트롤러는 이벤트를 기다리고, 발생시킨다. 컨트롤러는 데이터를 다루는 로직을 제공한다. 또한 VM에 command code를 제공해서 VM이 재사용 가능하게끔 한다.

MVVM이 MVC와 비교했을 때 C가 빠졌다고 해서 컨트롤러가 없어진 건 아니다. 컨트롤러는 그대로 있다. 오직 하나만이 추가됐는데, 바로 “상태”라는 것이다. 데이터는 클라이언트에 캐시되고, 이 데이터를 다룰 수 있게 되었다. 그리고 이제 이 클라이언트 쪽에 캐시된 데이터가 vm이라고 불리는 것이다.

  • MVC = model, controller, view = 단방향 커뮤니케이션 = poor interactivity
  • MVVM = model, controller, cache, view = 양방향 커뮤니케이션 = rich interacitivit

구조

스크린샷 2021-05-09 오후 2 32 16

  • View Model : View를 표현하기 위해 만든 View를 위한 model이다. View를 나타내주기 위한 model이자 View를 나타내기 위한 데이터 처리를 하는 부분이다.

동작

  1. Action은 View를 통해 들어온다.
  2. View에 Action이 들어오면, Command 패턴으로 View Model에 Action이 전달된다.
  3. View Model은 Model에게 데이터를 요청한다.
  4. Model은 View Model에게 요청받은 데이터를 응답한다.
  5. View Model은 응답 받은 데이터를 가공해 저장한다.
  6. View는 View Model과 Data Binding해 화면을 나타낸다.

특징

MVVM 패턴은 Command 패턴과 Data Binding 패턴을 사용해 구현되었다.

Command 패턴과 Data Binding을 이용해 View와 View Model간의 의존성을 없앴다.

View Model과 View는 1:n 관계다.

장점

View - Model간의 의존성이 없다. 또한 Command / Data Binding을 사용해 View-View Model 의 의존성도 없앴다. 각 부분은 독립적이라 모듈화해서 개발할 수 있다.

단점

View Model의 설계가 쉽지 않다.

이제 MVVM이 대략적으로 무엇인지는 알았다. 그렇다면 이 MVVM을 왜 사용해야 하는가?

  1. 가장 핵심적인 장점은 바로 viewmodel의 분리다. 이거는 model에 변경을 가할 때도, view에는 영향이 가지 않는다는 소리가 된다.
  2. 두번 째로, modelview에서 필요한 모든 데이터를 보유하고 있는 동안, model이 지원하지 않는 방법으로 해당 데이터를 추출하고 싶을 수 있다. 예를 들어, model이 date 프로퍼티를 가진다고 해보자. 모델에서는 DateTime이라는 객체를 가지고 있을 수 있지만 view는 완전히 다른 방법으로 이것을 출력하고 싶다. viewmodel 없이는 view를 지원하거나 자칫 ‘model’을 당황하게 만들 수 있는 해당 프로퍼티를 수정하는 작업을 위해 프로퍼티 위해 이 프로퍼티를 model에 중복되게 저장해야 한다.
  3. 또한 viewmodelview가 처리할 수 있는 더 유연한 인터페이스를 활성화하기 위해 여러 클래스/라이브러리에 퍼져 있는 모델들을 종합할 수 있다.

이를 위해, viewviewmodel간의 양방향 데이터 바인딩을 통해 이를 해결할 수 있다.

추가로 Stack Overflow에 이에 대한 좋은 글이 있어서 이를 번역해봤다.


요약

  • 모든 패턴의 사용은 상황에 따라 다르며, 장점은 항상 얼마나 복잡성이 줄어들었는지에 따라 판단할 수 있다.
  • MVVM은 GUI 어플리케이션에서 클래스들 간 책임을 어떻게 분산 시키는지 가이드를 해준다.
  • ViewModel 은 Model의 데이터를 View에 적합한 형태로 가공한다.
  • ‘하찮은’ 프로젝트에서 MVVM은 불필요하고, View를 사용하는 것으로 충분하다.
  • 간단한 몇몇 프로젝트에서 ViewModel/Model의 분리는 불필요하고, 오로지 Model과 View를 사용하는 것이 좋다.
  • Model과 ViewModel은 처음부터 있을 필요는 없고 필요할 때 도입되어도 된다.

언제 패턴을 사용하고 언제 사용하지 말지

간단한 어플리케이션에 디자인패턴을 적용하는 것은 과도한 일일 수 있다. 하나의 버튼이 있고 이를 누르면 “Hello World”를 띄우는 GUI 앱을 개발한다고 가정해보자. 이 경우네서는 MVC, MVP, MVVM과 같은 디자인 패턴은 오히려 복잡성을 더한다.

보통, 단순히 적용이 가능하다고 디자인 패턴을 적용하는 것은 항상 좋지 않은 결정이다. 디자인 패턴은 전체의 복잡성을 주림으로써 복잡도를 줄이거나, 별로 익숙하지 않은 복잡한 상황을 익순한 복잡한 상황으로 바꿔서 복잡도를 낮춰준다. 만약 앞의 이 2가지 방법 중 어느 것으로도 복잡도를 줄여주지 않는다면 디자인 패턴을 사용하지 말라.

익숙하고 익숙하지 않은 복잡도를 설명하기 위해, 아래의 두 문자열을 보도록 하자.

  • “D.€Ré%dfà?c”
  • “CorrectHorseBatteryStaple”

아래의 문자열이 첫 번째 문자열보다 두 배의 길이지만, 우리에게 더 익숙하기 떄문에 더 읽기 쉽고, 쓰기 쉽고, 첫 번째 문자열보다 기억하기 쉽다.

이런 문제는 읽는 사람에 따라 익숙함의 정도가 다르다는 것을 고려할 때 다른 차원의 문제를 갖게 된다. 어떤 사람들은 위의 두 경우보다 “3.14159265358979323846264338327950”를 더 읽기 쉽다고 느낄 수 있지만, 그렇지 않은 사람도 있을 것이다. 그래서 만약 MVVM을 사용하고 싶으면, 사용하고 있는 특정 언와 픞레임 워크에서 가장 많이 쓰이는 형태를 미러링하는(표현하는)것을 사용하도록 해라.

MVVM

MVVM은 GUI 어플리케이션에서 각 클래스의 크기가 작고 잘 정의되어 있으면서 이런 클래스들의 수를 적게 유지한 상태에서 클래스들 간의 책임을 분산하는 것을 가이드 해준다.

‘적절한’ MVVM은 최소한 “어딘가”에서 데이터를 가져와서 처리하는 복잡한 어플리케이션임을 가정하는 상황에서 출발한다. 데이터베이스, 파일, 웹 서비스 등에서 데이터를 가져올 수 있다.

예시

ViewModel의 클래스 두 개가 있다. Model은 처음에 csv 파일을 읽고 앱이 종료되면 유저가 변경한 사항을 저장한다. ViewModel에서 데이터를 가져오고 데이터를 수정할 수 있게 해주는 윈도우 class다. csv는 아래와 같은 내용일 수 있다.

ID, Name, Price
1, Stick, 5$
2, Big Box, 10$
3, Wheel, 20$
4, Bottle, 3$

그런데 새로운 요구사항이 들어왔다 : 가격을 유로로 표시하는 것이다.

이제 앱에 변화를 줘야 한다. 데이터는 이미 USD로 가격을 표시하는 “가격” 열이 있다. 우리는 USD에 이어서 미리 정의된 환율로 유로로 가격을 표시하는 새로운 열을 추가해야 한다. csv 파일의 형태는 우리가 통제할 수 없는 다른 앱에서도 이 파일을 이용하기 때문에 변경되면 안된다.

이 문제를 해결하는 방법 중 하나는 단순히 Model 클래스에 새로운 열을 추가하는 것이다. 이것은 Model이 csv의 모든 데이터를 저장하고 있고 우리는 새로운 유로 가격에 대한 열이 csv에 포함되는 것을 원하지 않기 때문에 좋은 해결책이 아니다. 그래서 Model을 변경하는 것은 별로 좋은 방법이 아니고, 모델이 어떤 역할을 하는지 설명해지기 어렵기 때문에 code smell 에 해당한다.

우리는 View를 변경할 수도 있다. 하지만 우리의 어플리케이션이


코드로 MVVM 구현하기

먼저 Model은 Class, Struct 등으로 정의된다.

코드를 작성하기 앞서 먼저 playground 에 아래와 같이 두 줄의 Import 문을 추가해준다.

import PlaygroundSupport
import UIKit

Model 구현하기

public class Pet {
  public enum Cuteness {
    case cute
    case veryCute
    case veryVeryCute
  }
  
  public let name: String
  public let birthday: Date
  public let cuteness: Cuteness
  public let image: UIImage
  
  public init(name: String, birthday: Date, cuteness: Cuteness, image: UIImage) {
    self.name = name
    self.birthday = birthday
    self.cuteness = cuteness
    self.image = image
  }
}

위와 같이 Pet 모델을 정의했다. Pet은 이름, 생일, 귀여움, 이미지를 프로퍼티로 갖고 있고, 이 값들을 생성자로 받아 초기화된다.

Pet 모델은 View에 독자적으로 출력될 수 없다. Model은 ViewModel을 거쳐 View에 출력된다. ViewModel을 통해 View에 출력될 값으로 변환되어 사용된다.

ViewModel 구성하기

ViewModel은 Pet Model을 View에 출력할 수 있는 값으로 변환해준다.

public class PetViewModel {
  // 1. pet 객체 생성, 나이 연산 위한 Calendar도 생성
  private let pet: Pet
  private let calendar: Calendar
  
  public init(pet: Pet) {
    self.pet = pet
    calendar = Calendar(identifier: .gregorian)
  }
  
  // 2. name : pet 이름을 반환한다.
  public var name: String {
    return pet.name
  }
  // 3. image : pet 이미지를 반환한다.
  public var image: UIImage {
    return pet.image
  }
  // 4. ageText : pet 나이를 연산해 반환한다.
  public var ageText: String {
    let today = calendar.startOfDay(for: Date())
    let birthday = calendar.startOfDay(for: pet.birthday)
    let components = calendar.dateComponents([.year], from: birthday, to: today)
    let age = components.year!
    return "\(age) years old"
  }
  
  // 5. cuteText: 해당 펫의 귀염도에 따라 얼마나 귀여운지 text를 반환한다.
  public var cuteText: String {
    switch pet.cuteness {
      case .cute:
        return "Cute"
      case .veryCute:
        return "Very Cute"
      case .veryVeryCute:
        return "Very Very Cute"
    }
  }
}

위 코드는 Model, pet이 거치는 viewModel, PetViewModel을 정의하고 있다.

View 구성하기

public class PetVIew: UIView {
  public let imageVIew: UIImageView
  public let nameLabel: UILabel
  public let ageLabel: UILabel
  public let cuteLabel: UILabel
  
  public override init(frame: CGRect) {
    var childFrame = CGRect(x : 0, y: 16, width: frame.width, height: frame.height / 2)
    imageVIew = UIImageView(frame: childFrame)
    imageVIew.contentMode = .scaleAspectFit
    
    childFrame.origin.y += childFrame.height + 16
    childFrame.size.height = 30
    
    nameLabel = UILabel(frame: childFrame)
    nameLabel.textAlignment = .center
    nameLabel.textAlignment = .center
    
    childFrame.origin.y += childFrame.height
    ageLabel = UILabel(frame: childFrame)
    ageLabel.textAlignment = .center
    
    childFrame.origin.y += childFrame.height
    cuteLabel = UILabel(frame: childFrame)
    cuteLabel.textAlignment = .center
    
    super.init(frame: frame)
    
    backgroundColor = .white
    addSubview(imageVIew)
    addSubview(nameLabel)
    addSubview(ageLabel)
    addSubview(cuteLabel)
    
  }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

출처

  • https://beomy.tistory.com/43
  • https://0urtrees.tistory.com/112
  • https://stackoverflow.com/questions/1644453/why-mvvm-and-what-are-its-core-benefits/1644955#1644955
  • https://stackoverflow.com/questions/2653096/why-use-mvvm
  • https://samslow.github.io/development/2020/06/16/Design_pattern-MVC/