공부하면서 정리한 글이기 때문에 잘못된 내용이 들어가 있을 수 있습니다! 틀린점/이상한 점들이 있다면 피드백 부탁드립니다! 😊
기본적으로 MVC 소프트웨어 디자인 패턴은 '화면(사용자 인터페이스)로부터 비즈니스 로직을 분리'하는데 중점을 두고 있다. (이를 '관심사 분리'라고 한다.)
→ 이를 통해 어플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있게 된다.
MVC에서 모델은 애플리케이션의 정보(데이터)를 나타내며, 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타내고, 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다.
Mozilla에서는 각 구성 요소에 대해 아래와 같이 설명하고 있다.
MVC는 위와같은 형태를 띄고 있다.
이는 전통적으로 Observer패턴을 사용했다.
출처: Apple archive - 'Concepts in Objective-C Programming'
근데 이렇게 Observer 패턴을 사용하면 모델이 뷰를 알고 있는 형태가 되는데, 이렇게 하면 안되는거 아니냐고??
→ 아니다. MVC에서는 이것을 허용하고 있었다. 😅
출처: 위키백과 - MVC
뷰 객체와 모델 객체는 어플리케이션 내에서 반드시 재사용가능해야 한다.
뷰 객체는 운영체제와 시스템이 지원하는 어플리케이션의 "외관과 느낌"을 나타냅니다. 모양과 행동의 일관성은 필수적이기에 높은 재사용성의 객체가 필요한 것은 당연합니다.
→ 애플이 Human Interface Guidelines에서 말하고 있는 "Consistency" 개념과도 일맥상통.
정의(definition)에 의한 모델 객체는 문제 도메인과 관련된 데이터를 캡슐화하며 해당 데이터에 대한 작업을 수행할 수 있습니다.
위에서 말하는 객체는 '데이터를 담고 있거나 / 네트워크 통신과 관련된 Entity'정도로 보면 될 것 같다.
디자인 측면에서 모델과 뷰 객체를 서로 분리하는 것은 재사용성을 증진시킨다는 측면에서 매우 좋다.
→ 모델이 뷰를 아는 위와같은 상황에서는 '뷰와 모델이 분리되어있지 않으므로(의존성이 존재하므로) 재사용성이 떨어진다' 맥락 정도로 이해하면 될 것 같다.
→ MVVM 탄생과정, 특징 - 박이얏호 블로그 내용에 따르면 아래와 같은 방식은 Apple이 만들어낸 것이라고 한다.
출처: Apple archive이전 그림과의 가장 큰 차이점은 View
와 Model
이 직접적으로 통신하고 있지 않다는 것이다.
'대부분의 Cocoa 어플리케이션에서, 모델 객체의 상태변화 알림은 컨트롤러 객체를 통해 뷰 객체에 전달됩니다.' 라고 애플은 말하고 있다.
검색해보니 일반적으로 MVC에 대해 사람들은 '뷰와 모델은 다른 컴포넌트들을 아예 몰라야 하며 컨트롤러는 뷰와 모델에 대해 알고 있어야 한다'라고 말하고 있었다. 그러면 아래와 같은 두가지 궁금증이 생길 수 있다.
- "Apple의 MVC에서는 사용자 상호작용 전달을 위해 '
View
가Controller
를 알고있다'고도 볼 수 있는 것 아닌가?"- "
Model
에서의 데이터 변화를 어떻게Controller
가 알아차리도록 하지?"→ 이에 대해서는 아래쪽의 나올 수 있는 질문들을 참고해주길 바란다.
모델 객체는 응용프로그램의 데이터를 캡슐화하며 해당 데이터들을 조작하고 처리하는 로직과 연산을 정의한다.
모델 객체는 다른 모델 객체와 일대일 / 일대다 관계를 가질 수 있다. 때때로 응용프로그램의 모델 레이어는 하나 이상의 객체 그래프로 이루어져 있다.
응용프로그램의 영구적 상태에 속하는 대부분의 데이터들은 모델 객체에 존재해야 한다.(그것이 파일로 저장되어있든 데이터베이스로 저장되어있든)
모델 객체는 뷰 객체와 명시적으로 연결되어있어서는 안된다.
Communication
뷰 객체는 응용프로그램에서 사용자들이 볼 수 있는 부분의 객체를 의미한다.
뷰 객체는 뷰를 어떻게 그릴지와 사용자의 행동에 어떻게 응답해야 할지 알고있다.
뷰 객체의 주요 목적은 모델 객체의 데이터를 보여주고 이 데이터들을 편집할 수 있도록 하는 것이다.
→ 뷰 객체는 일반적으로 모델 객체와 분리되어있다.
Communication
컨트롤러 객체는 하나 이상의 뷰 객체와 하나 이상의 모델 객체간의 중개자 역할을 한다.
컨트롤러 객체는 뷰 객체가 모델 객체의 변화를 알게되는 통로이자 모델 객체가 뷰 객체의 변화를 알게되는 통로이다.
또한 컨트롤러 객체는 응용프로그램의 설정 및 조정 작업을 수행하며 다른 객체들의 수명주기(Life Cycle)를 관리하는 역할을 한다.
→ 여기서 말하는 '다른 객체'는 View
와 Model
일 것이다. 결국 Controller
가 View
의 설정과 Model
의 조작을 담당한다는 의미.
Communication
정말 위와 같이 역할이 명확히 구분된다면 좋겠지만 실상은 그렇지 못하다. 왜 그런지는 아래 그림과 함께 살펴보도록 하자.
Apple의 MVC는 View와 Controller가 너무 밀접하다.
Apple의 MVC에서는 ViewController
라는 이름에서도 볼 수 있듯이 View와 Controller가 굉장히 밀접하게 연결되어있다. ViewController는 Controller의 역할뿐만 아니라 View의 life cycle에도 관여하고 있는 것이 현실이다. 이때문에 Model은 분리하여 테스트를 할 수 있어도 View와 Controller는 서로 강하게 연결되어있어 테스트가 어렵다.
뷰, 모델에 맞지 않는 모든 비즈니스 로직들은 Controller에 들어가게 된다.
모델이나 뷰에 넣기 애매한 코드들은 모두 Controller에 들어가게 되는데 이렇다보니 Controller가 비대해질 수 있다.
이를테면 서버에서 받아온 데이터를 가공(포매팅)해서 뷰에 넘겨주는 로직이나 사용자로부터 들어온 interaction을 처리하여 모델/뷰에 넘기는 로직 등, 화면에 보이는 것과 데이터 이외에는 모두 ViewController가 처리하게 된다.
Clint Jang은 MVC의 한계를 아래와 같이 설명해주셨다.
MVC에서 View는 Controller에 연결되어 화면을 구성하는 단위요소이므로 다수의 View들을 가질 수 있습니다. 그리고 Model은 Controller를 통해서 View와 연결되어지지만, 이렇게 Controller를 통해서 하나의 View에 연결될 수 있는 Model도 여러개가 될 수 있습니다.
→ 뷰와 모델이 서로 의존성을 띄게 됩니다.
즉, 화면에 복잡한 화면과 데이터의 구성 필요한 구성이라면, Controller에 다수의 Model과 View가 복잡하게 연결되어 있는 상황이 생길 수 있습니다.
후략...
View
와Model
이 많아지는 상황에서 발생할 수 있는 문제는 2번 맥락에서도 이해해볼 수 있다.
Controller
는View
에서 들어오는 사용자 입력을 처리하고,Model
의 데이터 업데이트를 알아차린 뒤 이를View
에 넘기는 작업들도 수행한다고 했었다. 당연히Controller
에 연결된View
와Model
들이 많아지면 해당 작업들에 대한 코드들이 늘어나는 것이 당연하고Controller
가 커지는 것은 어찌보면 피할 수 없다.
💡 위와 같은 이유들때문에 MVC를 Massive View Controller라고 말하는 사람도 있다. 하지만 MVC가 항상 그런 것은 아니다. MVC도 충분히 클린하게 코드를 짤 수 있다고 한다.
"MVC에서 View는 다른 컴포넌트를 알면 안되는데, Apple의 MVC에서는 이벤트 처리를 위해 View가 Controller를 아는 형태로 구현되지 않나요??"
→ 날카로운 질문이다. 하지만 Apple은 이 문제를 Delegate 디자인 패턴
이라고 하는 아주 우아한(?) 방식으로 해결하였다. Protocol
을 선언하고View
는 해당 Protocol
만을 아는 형태로 만든 뒤, Controller
로 처리할 책임을 위임하였다. 즉, 의존성 역전 원칙(DIP)을 이용하여 View
와 Controller
간 의존성을 떨어뜨렸다. (View
는 Controller
를 직접적으로 알지 못하고 오직 Protocol
만을 알고 있다)
"Controller 또한 View를 알고 있는 형태로 만들어지는데, View도 Controller를 알고 있다면 reference cycle이 생기지 않나요?"
→ 결론부터 말하자면 발생하지 않는다. UIKit
내에 있는 여러 View
요소들이 Controller
를 참조하는 방식은weak reference
이다. (단, 직접 delegate pattern을 구현하여 적용하는 경우에는 cycle이 생기지 않도록 우리가 만들어줘야 한다)
"Delegate Pattern이 아닌 Target-Action으로 View가 Controller를 알도록 하는 것은요? reference cycle 문제가 없나요?"
→ UIControl - addTarget(_:action:for:) 문서에 이 질문에 대한 해답이 나와있다.
"Model도 다른 컴포넌트를 몰라야 한다고 하는데, 그럼 Controller는 Model의 변화를 어떻게 감지하나요??"
→ 여기에는 KVO
, NotificationCenter
같은 Observer Pattern
이나, Delegate Pattern
등 여러가지 방법이 있겠지만 나는 클로저를 이용한 completionHandler
를 주로 쓴다.(callback) 특별히 뭔가를 별도로 구현해 줄 필요가 없다는 점에서 굉장히 편리하다. 👍 (물론 capture에 따른 reference cycle은 별도로 고려해주어야 한다!)
위에서 말한 MVC의 이러한 문제들 때문에 다른 패턴을 쓰는 것만이 유일한 해결책인 것처럼 보일 수 있다.
하지만 나는 그렇게 생각하지 않는다. 다른 패턴을 쓴다고 하더라도 비슷한 문제는 발생할 수 있다.
비대해지는 객체를 적절히 분리하는 것이 해결법이라고 생각한다.
MVC 패턴을 적용했을 때 ViewController
가 너무 비대해진다면 해당 ViewController
를 단일책임원칙(SRP)에 따라 여러개의 ViewController
로 나누어보자!
코든의 2021년 6월 6일 TIL
도움이 많이 되었습니다! 잘보고 갑니다👍