공부하면서 정리한 글이기 때문에 잘못된 내용이 들어가 있을 수 있습니다! 틀린점/이상한 점들이 있다면 피드백 부탁드립니다! 😊

MVC Design Pattern

기본적으로 MVC 소프트웨어 디자인 패턴은 '화면(사용자 인터페이스)로부터 비즈니스 로직을 분리'하는데 중점을 두고 있다. (이를 '관심사 분리'라고 한다.)

→ 이를 통해 어플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있게 된다.

 

MVC 패턴에서는 어플리케이션의 구성요소를 3가지 형태로 나누고 있다. - Model, View, Controller

MVC에서 모델은 애플리케이션의 정보(데이터)를 나타내며, 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타내고, 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.

Mozilla에서는 각 구성 요소에 대해 아래와 같이 설명하고 있다.

  1. 모델: 데이터와 비즈니스 로직을 관리합니다.
  2. 뷰: 레이아웃과 화면을 처리합니다.
  3. 컨트롤러: 명령을 모델과 뷰 부분으로 라우팅합니다.
    여기서 말하는 명령은 뷰로부터 들어온 사용자 입력이라고 봐도 무방할 것 같다.
출처: MDN

MVC는 위와같은 형태를 띄고 있다.

  • 컨트롤러는 모델에 명령을 보냄으로써 모델의 상태를 변경할 수 있다. 또, 컨트롤러가 관련된 뷰에 명령을 보냄으로써 모델의 표시 방법을 바꿀 수 있다.
    • '컨트롤러 → 뷰'방향의 명령 예시 - 리스트로 보이는 데이터를 특정값 기준으로 정렬하여 보고자 하는 경우(이 명령은 데이터의 변화가 필요치 않다)
  • 모델은 모델의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다. 이와 같은 통보를 통해서 뷰는 최신의 결과를 보여줄 수 있고, 컨트롤러는 모델의 변화에 따른 적용 가능한 명령을 추가·제거·수정할 수 있다. 어떤 MVC 구현에서는 통보 대신 뷰나 컨트롤러가 직접 모델의 상태를 읽어 오기도 한다.
  • 는 사용자가 볼 결과물을 생성하기 위해 모델로부터 정보를 얻어 온다. (표시할 데이터를 모델로부터 받아온다.)

  

그럼 뷰가 어떻게 모델의 데이터를 가져오는가?

이는 전통적으로 Observer패턴을 사용했다.

출처: Apple archive - 'Concepts in Objective-C Programming'

  • 모델은 데이터 변경사항을 알리고 뷰는 이를 관찰하고 있다가 변경사항을 반영한다.

 

근데 이렇게 Observer 패턴을 사용하면 모델이 뷰를 알고 있는 형태가 되는데, 이렇게 하면 안되는거 아니냐고??

→ 아니다. MVC에서는 이것을 허용하고 있었다. 😅
출처: 위키백과 - MVC

  

하지만 애플 문서에서는 위와 같은 형태가 문제점이 있다고 지적하고 있었다.

  • 뷰 객체와 모델 객체는 어플리케이션 내에서 반드시 재사용가능해야 한다.

    • 뷰 객체는 운영체제와 시스템이 지원하는 어플리케이션의 "외관과 느낌"을 나타냅니다. 모양과 행동의 일관성은 필수적이기에 높은 재사용성의 객체가 필요한 것은 당연합니다.

      → 애플이 Human Interface Guidelines에서 말하고 있는 "Consistency" 개념과도 일맥상통.

    • 정의(definition)에 의한 모델 객체는 문제 도메인과 관련된 데이터를 캡슐화하며 해당 데이터에 대한 작업을 수행할 수 있습니다.

      위에서 말하는 객체는 '데이터를 담고 있거나 / 네트워크 통신과 관련된 Entity'정도로 보면 될 것 같다.

  • 디자인 측면에서 모델과 뷰 객체를 서로 분리하는 것은 재사용성을 증진시킨다는 측면에서 매우 좋다.

→ 모델이 뷰를 아는 위와같은 상황에서는 '뷰와 모델이 분리되어있지 않으므로(의존성이 존재하므로) 재사용성이 떨어진다' 맥락 정도로 이해하면 될 것 같다.

 

그래서 Cocoa에서는 컨트롤러와 뷰 객체에 주어지는 역할에 차이점을 두었다. 바로 아래 그림과 같이 말이다.

MVVM 탄생과정, 특징 - 박이얏호 블로그 내용에 따르면 아래와 같은 방식은 Apple이 만들어낸 것이라고 한다.

출처: Apple archive
  • 이전 그림과의 가장 큰 차이점은 ViewModel이 직접적으로 통신하고 있지 않다는 것이다.

  • '대부분의 Cocoa 어플리케이션에서, 모델 객체의 상태변화 알림은 컨트롤러 객체를 통해 뷰 객체에 전달됩니다.' 라고 애플은 말하고 있다.

검색해보니 일반적으로 MVC에 대해 사람들은 '뷰와 모델은 다른 컴포넌트들을 아예 몰라야 하며 컨트롤러는 뷰와 모델에 대해 알고 있어야 한다'라고 말하고 있었다. 그러면 아래와 같은 두가지 궁금증이 생길 수 있다.

  1. "Apple의 MVC에서는 사용자 상호작용 전달을 위해 'ViewController를 알고있다'고도 볼 수 있는 것 아닌가?"
  2. "Model에서의 데이터 변화를 어떻게 Controller가 알아차리도록 하지?"

→ 이에 대해서는 아래쪽의 나올 수 있는 질문들을 참고해주길 바란다.

  

위 그림에서의 각 객체 역할과 책임, 커뮤니케이션 방법에 대해 애플 문서에서는 아래와 같이 설명하고 있다.

Model

  • 모델 객체는 응용프로그램의 데이터를 캡슐화하며 해당 데이터들을 조작하고 처리하는 로직과 연산을 정의한다.

  • 모델 객체는 다른 모델 객체와 일대일 / 일대다 관계를 가질 수 있다. 때때로 응용프로그램의 모델 레이어는 하나 이상의 객체 그래프로 이루어져 있다.

  • 응용프로그램의 영구적 상태에 속하는 대부분의 데이터들은 모델 객체에 존재해야 한다.(그것이 파일로 저장되어있든 데이터베이스로 저장되어있든)

  • 모델 객체는 뷰 객체와 명시적으로 연결되어있어서는 안된다.

  • Communication

    • 뷰 레이어에서 데이터를 생성하거나 변경시키는 사용자 행동은 컨트롤러를 통해 모델 객체의 생성 또는 업데이트를 일으키게 된다.
    • 모델 객체가 변경되면 그것은 컨트롤러 객체에 알려지고 컨트롤러 객체는 적절한 뷰 객체를 업데이트한다.

View

  • 뷰 객체는 응용프로그램에서 사용자들이 볼 수 있는 부분의 객체를 의미한다.

  • 뷰 객체는 뷰를 어떻게 그릴지와 사용자의 행동에 어떻게 응답해야 할지 알고있다.

  • 뷰 객체의 주요 목적은 모델 객체의 데이터를 보여주고 이 데이터들을 편집할 수 있도록 하는 것이다.

    → 뷰 객체는 일반적으로 모델 객체와 분리되어있다.

  • Communication

    • 뷰 객체는 모델 객체의 변화를 컨트롤러 객체를 통해 알게 된다.
    • 사용자에 의해 시작된 변화들은 - 이를테면 텍스트 필드에 텍스트를 넣는다던가 - 컨트롤러 객체를 통해 모델객체에 전달된다.

Controller

  • 컨트롤러 객체는 하나 이상의 뷰 객체와 하나 이상의 모델 객체간의 중개자 역할을 한다.

  • 컨트롤러 객체는 뷰 객체가 모델 객체의 변화를 알게되는 통로이자 모델 객체가 뷰 객체의 변화를 알게되는 통로이다.

  • 또한 컨트롤러 객체는 응용프로그램의 설정 및 조정 작업을 수행하며 다른 객체들의 수명주기(Life Cycle)를 관리하는 역할을 한다.

    → 여기서 말하는 '다른 객체'는 ViewModel일 것이다. 결국 ControllerView의 설정과 Model의 조작을 담당한다는 의미.

  • Communication

    • 컨트롤러 객체는 뷰 객체에서의 사용자 행동을 해석하여 데이터의 생성이나 변경에 대한 것을 모델 레이어에 알린다.
    • 모델 객체에서 변경이 발생하면 컨트롤러 객체는 새로운 모델 데이터를 뷰 객체에게 알린다.(뷰 객체가 새로운 데이터를 보여줄 수 있도록)

      
     
     


MVC의 한계

정말 위와 같이 역할이 명확히 구분된다면 좋겠지만 실상은 그렇지 못하다. 왜 그런지는 아래 그림과 함께 살펴보도록 하자.
 

출처: https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52
  1. Apple의 MVC는 View와 Controller가 너무 밀접하다.

    Apple의 MVC에서는 ViewController라는 이름에서도 볼 수 있듯이 View와 Controller가 굉장히 밀접하게 연결되어있다. ViewController는 Controller의 역할뿐만 아니라 View의 life cycle에도 관여하고 있는 것이 현실이다. 이때문에 Model은 분리하여 테스트를 할 수 있어도 View와 Controller는 서로 강하게 연결되어있어 테스트가 어렵다.

  1. 뷰, 모델에 맞지 않는 모든 비즈니스 로직들은 Controller에 들어가게 된다.

    모델이나 뷰에 넣기 애매한 코드들은 모두 Controller에 들어가게 되는데 이렇다보니 Controller가 비대해질 수 있다.

    이를테면 서버에서 받아온 데이터를 가공(포매팅)해서 뷰에 넘겨주는 로직이나 사용자로부터 들어온 interaction을 처리하여 모델/뷰에 넘기는 로직 등, 화면에 보이는 것과 데이터 이외에는 모두 ViewController가 처리하게 된다.

  • Clint Jang은 MVC의 한계를 아래와 같이 설명해주셨다.

    • MVC에서 View는 Controller에 연결되어 화면을 구성하는 단위요소이므로 다수의 View들을 가질 수 있습니다. 그리고 Model은 Controller를 통해서 View와 연결되어지지만, 이렇게 Controller를 통해서 하나의 View에 연결될 수 있는 Model도 여러개가 될 수 있습니다.

      → 뷰와 모델이 서로 의존성을 띄게 됩니다.

    • 즉, 화면에 복잡한 화면과 데이터의 구성 필요한 구성이라면, Controller에 다수의 Model과 View가 복잡하게 연결되어 있는 상황이 생길 수 있습니다.

      후략...

    ViewModel이 많아지는 상황에서 발생할 수 있는 문제는 2번 맥락에서도 이해해볼 수 있다.

    ControllerView에서 들어오는 사용자 입력을 처리하고, Model의 데이터 업데이트를 알아차린 뒤 이를 View에 넘기는 작업들도 수행한다고 했었다. 당연히 Controller에 연결된 ViewModel들이 많아지면 해당 작업들에 대한 코드들이 늘어나는 것이 당연하고 Controller가 커지는 것은 어찌보면 피할 수 없다.

 

💡 위와 같은 이유들때문에 MVC를 Massive View Controller라고 말하는 사람도 있다. 하지만 MVC가 항상 그런 것은 아니다. MVC도 충분히 클린하게 코드를 짤 수 있다고 한다.

  

나올 수 있는 질문들

  1. "MVC에서 View는 다른 컴포넌트를 알면 안되는데, Apple의 MVC에서는 이벤트 처리를 위해 View가 Controller를 아는 형태로 구현되지 않나요??"

    → 날카로운 질문이다. 하지만 Apple은 이 문제를 Delegate 디자인 패턴이라고 하는 아주 우아한(?) 방식으로 해결하였다. Protocol을 선언하고View는 해당 Protocol만을 아는 형태로 만든 뒤, Controller로 처리할 책임을 위임하였다. 즉, 의존성 역전 원칙(DIP)을 이용하여 ViewController간 의존성을 떨어뜨렸다. (ViewController를 직접적으로 알지 못하고 오직 Protocol만을 알고 있다)

  2. "Controller 또한 View를 알고 있는 형태로 만들어지는데, View도 Controller를 알고 있다면 reference cycle이 생기지 않나요?"

    → 결론부터 말하자면 발생하지 않는다. UIKit내에 있는 여러 View 요소들이 Controller를 참조하는 방식은weak reference이다. (단, 직접 delegate pattern을 구현하여 적용하는 경우에는 cycle이 생기지 않도록 우리가 만들어줘야 한다)

  3. "Delegate Pattern이 아닌 Target-Action으로 View가 Controller를 알도록 하는 것은요? reference cycle 문제가 없나요?"

    UIControl - addTarget(_:action:for:) 문서에 이 질문에 대한 해답이 나와있다.

  4. "Model도 다른 컴포넌트를 몰라야 한다고 하는데, 그럼 Controller는 Model의 변화를 어떻게 감지하나요??"

    → 여기에는 KVO, NotificationCenter 같은 Observer Pattern이나, Delegate Pattern등 여러가지 방법이 있겠지만 나는 클로저를 이용한 completionHandler를 주로 쓴다.(callback) 특별히 뭔가를 별도로 구현해 줄 필요가 없다는 점에서 굉장히 편리하다. 👍 (물론 capture에 따른 reference cycle은 별도로 고려해주어야 한다!)

  

그러면 어떻게 하라고?!?

위에서 말한 MVC의 이러한 문제들 때문에 다른 패턴을 쓰는 것만이 유일한 해결책인 것처럼 보일 수 있다.
하지만 나는 그렇게 생각하지 않는다. 다른 패턴을 쓴다고 하더라도 비슷한 문제는 발생할 수 있다.

  • 이를테면 MVVM을 쓴다고 하더라도 ViewModel이 비대해질 수 있다.
  • VIPER패턴이 무엇인지는 아직 잘 모르지만 해당 패턴을 쓴다고 하더라도 Interactor가 비대해지는 문제가 생길 수 있다고 한다.

내가 생각하는 해결책?

비대해지는 객체를 적절히 분리하는 것이 해결법이라고 생각한다.

MVC 패턴을 적용했을 때 ViewController가 너무 비대해진다면 해당 ViewController단일책임원칙(SRP)에 따라 여러개의 ViewController로 나누어보자!

  

References

profile
iOS 공부중인 Coden

6개의 댓글

comment-user-thumbnail
2021년 12월 15일

도움이 많이 되었습니다! 잘보고 갑니다👍

1개의 답글
comment-user-thumbnail
2021년 12월 15일

이 글을 읽고나니 MVC에 대해서 붕붕 떠있던 개념들이 정리가 확 되네요! 감사히 잘 읽었습니다😊👍
MVVM에 대한 글도 기대해도 될까요?😎

1개의 답글
comment-user-thumbnail
2021년 12월 17일

와! 글 정말 잘 쓰셨네요! 멋집니다!

1개의 답글