들어가며
아키텍쳐 패턴 중에 하나인 MVC 패턴에 대해서 공부를 해보려 합니다. 전에 아키텍쳐 패턴에 대한 정리를 진행한 적이 있는데, 해당 내용들과 관련지어서 MVC 패턴에 대해 깊게 공부해보겠습니다.
아키텍쳐 패턴의 특징에 따라서 ⓵ 기본적인 윤곽 제시, ⓶ 서브시스템들과 그 역할 정의, ⓷ 서브시스템 사이의 관계와 여러 규칙, 지침 순으로 글을 정리해보도록 하겠습니다.
MVC?
전에 아키텍쳐 패턴에 대해서 정리할 때, 아키텍쳐 패턴을 아키텍쳐를 설계하는데 발생하는 문제들을 해결하기 위해서 만들어 놓은 전형적인 해결 방식이라고 정의내렸습니다.
그렇다면, MVC는 어떤 문제를 해결하기 위한 패턴일까요?
MVC가 탄생하게 된 배경
Trygve Reenskaug created MVC while working on Smalltalk-79 as a visiting scientist at the Xerox Palo Alto Research Center(PARC) in the late 1970s. He wanted a pattern that could be used to structure any program where users interact with a large, convoluted data set. His design initially had four parts: Model, View, Thing, and Editor. After discussing it with the other Smalltalk developers, he and the rest of the group settled on Model, View, and Controller instead
그는 사용자가 크고 복잡한 데이터 셋과 상호 작용하는 프로그램을 구성하는 데 사용할 수 있는 패턴을 원했다.
처음 MVC 패턴이 만들어지게 된 이유는 크고 복잡한 데이터 셋과 해당 데이터와 상호 작용하고 있는 프로그램을 잘 구성하기 위해서 만들어졌다고 볼 수 있겠네요.
위키 하단 𝑅𝑒𝑓𝑒𝑟𝑒𝑛𝑐𝑒𝑠 부분에 Model-View-Controller History를 자세히 알 수 있는 사이트가 있더라구요. 사이트 링크와 간단한 요약은 toggle 로만 남겨두겠습니다. 궁금하신 분들은 펼쳐서 요약한 내용을 보셔도 되고 사이트에 직접 들어가서 읽어보셔도 좋습니다.
𝑴𝒐𝒅𝒆𝒍-𝑽𝒊𝒆𝒘-𝑪𝒐𝒏𝒕𝒓𝒐𝒍𝒍𝒆𝒓 𝑯𝒊𝒔𝒕𝒐𝒓𝒚
MVC의 기본 틀
MVC가 탄생한 배경은 크고 복잡한 데이터 셋과 상호 작용하는 프로그램을 구성하기 위함입니다. 그렇다면 MVC의 기본적인 틀은 어떻게 될까요? 어떤 모습으로 위의 문제점을 해결했을까요?
위키 Model-View-Controller 페이지에 나오는 MVC interaction 다이어그램입니다. 다이어그램에서 보여지는 플로우를 정리해보자면,
⓵ 사용자가 Controller를 사용해서 ⓶ Model를 다루고 변경된 Model 데이터가 ⓷ 뷰 위에 업데이트가 되면 ⓸ 사용자가 모니터나 휴대폰 화면을 통해서 볼 수 있게 됩니다.
다이어그램에 따르면:
•
Controller는 Model를 조작하지만 Model은 Controller를 조작할 수 없습니다.
•
Model은 View의 모양을 변경할 수 있지만 View는 Model에 있는 데이터를 변경할 순 없습니다.
•
View에서 사용자에 의한 Action이 생긴다면 해당 Action이 Controller로 전달됩니다.
다이어그램에 따른 플로우와 기본적인 윤곽이 어떤식으로 구성이 되어 있는지 확인했습니다. 그렇다면 각 요소들이 맡고 있는 역할은 어떻게 될까요?
Model-View-Controller
MVC를 사용한 이유에서도 알 수 있듯이, MVC의 주요 요소는 Model 입니다. ‘복잡한 데이터 셋과 상호작용하는 프로그램을 어떻게 하면 잘 만들 수 있을까?’ 라는 관점에서 패턴을 만들었기 때문에 Model이 주요 요소라고 볼 수 있을 거 같습니다.
주요 요소인 Model은 MVC에서 어떤 역할을 맡고 있을까요?
Model
The central component of the pattern. It is the application's dynamic data structure, independent of the user interface. It directly manages the data, logic and rules of the application.
It is the application's dynamic data structure, independent of the user interface.
위키에서는 Model를 사용자 인터페이스와 독립적인 애플리케이션의 동적 데이터 구조라고 소개합니다.
따라서, Model은 사용자 인터페이스와 독립적이기 때문에 View, Controller에서 일어나는 일을 알지 못합니다.
It directly manages the data, logic and rules of the application.
Model은 애플리케이션의 데이터, 로직 그리고 규칙을 직접 관리한다고 되어있습니다.
즉, 데이터와 관련된 로직들과 처리되는 데이터가 정의되어 있는 부분입니다. Model에 들어있는 데이터들은 Model 내부에 있는 로직에 의해서 변경될 것이기 때문에 뷰에 의해서 Model 데이터 값이 변경되지 않습니다. 그리고 뷰에서 독립적이기 때문에 언제든 재사용이 가능해야 합니다.
View
Any representation of information such as a chart, diagram or table. Multiple views of the same information are possible, such as a bar chart for management and a tabular view for accountants. Smalltalk-80 views communicate with both a model and a controller, whereas with WebObjects, a view talks only to a controller, which then talks to a model.
Any representation of information such as a chart, diagram or table.
위키에서 View는 chart, diagram, table과 같은 정보의 표현이라고 설명합니다.
View는 Model에서 보낸 데이터 정보를 GUI Component를 사용해서 나타냅니다. 하지만 GUI Component가 직접 Model에 데이터 정보를 요청하거나 변경하는건 안 될 겁니다.
설명에 보면 이런 부분이 있습니다.
Smalltalk-80 views communicate with both a model and a controller, whereas with WebObjects, a view talks only to a controller, which then talks to a model.
대충 번역을 해보자면, Smalltalk-80은 View과 Model, Controller 모두와 통신을 하는 반면에 WebObjects는 View가 Controller랑만 통신을 하고 Controller가 Model과 통신을 한다는 그런 내용입니다.
내용에서 알 수 있듯, View가 Model, Controller 모두와 통신을 하는지 Controller랑만 통신하는지는 해당 패턴을 각자 어떻게 설계했는지에 따라서 다르게 나타나는 거 같습니다. iOS에 경우에도 iOS에 맞춰서 View가 통신을 하는 방식이 위키에서 설명하는 방식과 다르게 나타납니다. 그 부분은 뒤에서 마저 얘기하는걸로 합시다.
Controller
Accepts input and converts it to commands for the model or view. A Smalltalk-80 controller handles user input events, such as button presses or mouse movement. At any given time, each controller has one associated view and model, although one model object may hear from many different controllers. Only one controller, the "active" controller, receives user input at any given time; a global window manager object is responsible for setting the current active controller. If user input prompts a change in a model, the controller will signal the model to change, but the model is then responsible for telling its views to update. In WebObjects, the views handle user input, and the controller mediates between the views and the models. There may be only one controller per application, or one controller per window. Much of the application-specific logic is found in the controller.
Accepts input and converts it to commands for the model or view.
위키에서 Controller를 input를 받고 이를 Model 혹은 View의 명령으로 변환해준다고 설명되어 있습니다.
Controller는 위에서도 설명되어 있듯이 Model, View로 input를 전달하는 일을 합니다. 어떤 input이 들어오면 해당 input를 가지고 Model에 맞게 변환해서 전달해줍니다. 해당 input를 받은 Model이 View를 업데이트하겠죠?
View와 동일하게 Controller도 어디서 어떻게 사용하는지에 따라서 사용 방식이 다르게 나타납니다. 위의 설명에서 보면,
At any given time, each controller has one associated view and model, although one model object may hear from many different controllers. Only one controller, the "active" controller, receives user input at any given time
Smalltalk에서는 각 Controller가 View와 Model를 하나씩 가지고 Model은 여러 Controller들과 통신이 가능합니다. 그리고 사용자의 input를 받는 Controller는 오직 하나여야 합니다.
In WebObjects, the views handle user input, and the controller mediates between the views and the models. There may be only one controller per application, or one controller per window. Much of the application-specific logic is found in the controller.
WebObject에서는 View가 사용자 input를 다루고 Controller는 View와 Model 사이를 중재하는 역할을 합니다. 그리고 애플리케이션의 specific한 로직은 Controller가 가지고 있습니다.
View와 동일하게 Controller도 각각의 설계에 맞춰서 변형이 되는 거 같습니다. 그렇다면 iOS에서 적용되는 MVC 패턴은 어떠한 모습을 가지고 있을까요?
iOS와 전통적인 MVC
iOS에서 MVC 패턴이라고 한다면 제 머릿속에 이런 식으로 정리가 됩니다.
•
Model - Struct, Class 형태로 만들어진 데이터
struct Model {
let id: String
let name: String
}
Swift
복사
•
View - UI Component들, 앞에 UI가 붙는 것들(UIView, UIButton, UILabel…) 혹은 Storyboard
class View: UIView {
...
}
Swift
복사
•
Controller - UIViewController
class viewController: UIViewController {
...
}
Swift
복사
하지만 이 요소들이 어떻게 사용하는지가 제 머릿속에 모호하게 존재했습니다. Controller를 통해서만 View와 Model이 통신을 할 수 있고, Model은 Controller, View에 뭐가 있는지 몰라야 하고, View도 동일하게 Model에 무엇이 있는지 몰라야 하고…
그렇게 정리를 하다보니 위키를 통해서 위에 정리한 MVC에 대한 정리가 iOS와는 맞지 않다는 생각이 들었습니다. Controller가 Model를 조작하면 Model이 View에 데이터를 주게 되는데, 이렇게 된다면 iOS에서 Controller를 통해서만 통신한다는 부분에 맞지 않는 방식이 됩니다. 이게 제가 잘못 알고 있는건지, 아니면 iOS가 조금 다른 방식을 사용하고 있는건지 더 자세히 알아볼 필요가 있게 되었습니다.
일단 위에서의 MVC 방식으로 iOS에서 설계한다면 어떻게 될까요? 해당 규칙들을 사용해서 구현이 가능한 걸까요? 애플 문서에서는 전통적인 MVC 방식으로 구현하는 것이 ‘quite possible’ 하다고 설명합니다. 그렇다면 전통적인 MVC 방식을 iOS 개발에 적용해봅시다.
전통적인 MVC
전통적인 MVC 방식을 iOS 개발에 적용한다고 해봅시다. 플로우는 위에서 얘기한대로 이런식으로 흘러가게 될 겁니다.
1.
사용자가 View를 조작해서 이벤트를 발생시키고 발생한 이벤트를 Controller가 받게 됩니다.
2.
Controller는 해당 이벤트를 해석해서 Model에 상태 변경을 요청하거나 View에 동작 또는 모양 변경을 요청하게 됩니다.
3.
Model은 상태가 변경되면 observer로 등록된 모든 object(주로 View)에게 알림을 보냅니다. 알림을 받은 대상이 View라면 외관을 업데이트하게 됩니다.
현재 우리가 Storyboard를 사용해서 코드를 짜고 있었던 상황이라고 가정해봅시다.
사용자가 화면에 있는 버튼을 눌렀을 때 Controller는 화면에 발생한 이벤트를 받습니다. 바로 @IBAction으로 View에서 이벤트가 발생하면 해당 함수를 호출할 수 있게 해둔거죠. 만약 해당 Action이 View를 업데이트하는 것이었다면 View의 appearance가 변경되었을 거고, Model의 상태를 변경하는 거라면 Model 오브젝트로 가서 Model 오브젝트 내부에 있는 속성 값을 변경할 겁니다. 아래 있는 그림처럼 말이죠.
Model의 속성값이 변경된 상황일 때, 위의 순서로 진행을 해보자면 Observer로 등록된 View에게 알림을 보내게 됩니다. 그리고 알림을 받은 View는 화면을 업데이트하게 됩니다. 하지만 Model에서 Observe하고 있던 View로 어떻게 알림을 날릴까요?
Storyboard로 만든 View는 Controller가 Model의 알림을 대신 전달해주지 않으면 알림을 받을 수 있는 방법이 없습니다. 즉, Controller가 중간 매개체로 동작하지 않으면 View는 Model이 보낸 알림을 받을 수 없습니다.
만약 View를 코드만을 사용해서 UIView 형태로 만들었다고 한다면 Model의 값을 전달받기 위해서 Model 객체를 알고 있어야 한다거나, NotificationCenter를 통해서 Post된 값을 전달받아서 사용하는 방식으로 구현할 수 있을 겁니다.
⓵ Model 객체를 View가 알고 있다는 건 서로를 몰라야 한다는 규칙을 위배합니다. 또한 해당 View가 Model 객체에 종속될 수 있습니다.
⓶ NotificationCenter를 사용해서 모델의 값이 변경될 때마다 Post해서 사용한다면 모든 Model과 View가 NotificationCenter를 통해서 Observe, Post하게 되는 상태가 될 겁니다. 해당 방식은 Observer가 많아지게 되면 Model에 보낸 Notification를 어디서 받는지 추적하기가 어려워집니다.
⓷ Model과 View는 가장 재사용 가능한 요소여야 하는데 위의 예시는 View가 Model를 직접 관찰하고 상태 변경 알림을 받게 되기 때문에 재사용이 용이하지 않습니다.
위의 예시가 익숙한 방식인가요? 제가 적용해봤던 MVC와는 조금 다른 느낌입니다. 왜 일까요?
전통적인 MVC는 우리가 익숙하게 봤던 MVC가 아니기 때문입니다. 우리는 전통적인 MVC방식을 Apple만의 MVC로 변형해서 사용해왔습니다. 애플의 MVC문서를 보면 Cocoa version of MVC라는 이름으로 해당 패턴을 설명해줍니다.
Cocoa MVC
Cocoa MVC는 전통적인 MVC와 어떠한 차이가 있는걸까요? 애플 문서에 보면 이렇게 설명이 되어 있습니다.
However, the traditional notion of MVC assigns a set of basic patterns different from those that Cocoa assigns. The difference primarily lies in the roles given to the controller and view objects of an application.
The difference primarily lies in the roles given to the controller and view objects of an application.
차이점은 주로 애플리케이션의 Controller 및 View Object에 부여된 역할에 있습니다.
Controller 및 View에 부여된 역할이 전통적인 MVC와 Cocoa MVC가 차이가 있다는 말이네요. 어떻게 차이가 있는 먼저 그림으로 살펴봅시다.
𝑪𝒐𝒄𝒐𝒂 𝑴𝑽𝑪?
위에 있는 Traditional version of MVC와의 차이가 뭔지 그림으로 확인하셨나요?
바로 Controller를 거쳐서만 View와 Model이 소통할 수 있다는 겁니다. 이전에는 Model이 바로 View로 Notify했다면 현재는 Controller로 Notify를 보내고 Controller가 이를 받아서 View를 업데이트 해준다는 거죠.
그리고 이전보다 요소들이 분리된 느낌이 듭니다. Model와 View는 가장 재사용이 가능해야 하는 요소입니다. 현재 Controller를 통해서만 View와 Model이 소통을 하고 있기 때문에 전통적인 MVC보다는 분리가 잘되고 이는 곧 재사용성 향상으로 이어질 겁니다.
하지만, 실제로 분리가 잘 이루어 지나요?
먼저, View와 Controller의 관계를 보겠습니다.
Storyboard를 사용해서 View를 구현한다고 해도 @IBOutlet를 사용해서 Controller와 View가 한 데 묶이는 모습을 볼 수 있습니다. Controller 내부에 View 코드를 직접 짜는 경우에는 레이아웃 및 View의 Configuration 까지 Controller에서 해줘야 하기 때문에 View와 Controller의 분리는 더욱이 어려워질겁니다.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Swift
복사
ViewController 내부에 UI Component가 @IBOutlet를 사용해서 연결됨
Model과 Controller의 관계도 한 번 봐보면,
위에서 Controller가 Model과 View 사이를 중재하는 역할을 한다고 써뒀었는데요. 각 요소들은 서로를 알 필요가 없기 때문에 Model에서 View로 전달하는 로직들이 Controller 내부에 쌓이게 됩니다. Controller 내부에 쌓이는 로직들이 재사용이 불가능한 로직들로 늘어가는거죠.
현실적인 Cocoa MVC는 이런 모습일 겁니다.
애플 문서에서는 Controller가 1개 이상의 View 객체에 대한 Strategy를 구현하고 View는 비주얼적인 측면만 유지할 수 있도록 도와준다고 되어 있습니다. 덧붙여서 View는 인터페이스 동작의 모든 결정을 Controller에게 위임한다고 되어 있습니다.
UIKit를 예로 들자면 TableView라는 Compound View가 Controller에게 TableViewDelegate(TableView의 동작 API)를 위임하는거죠.
위에 있는 Cocoa MVC는 Controller가 인터페이스(View) 관리 로직, 인터페이스 동작 로직, Model과 View를 이어주는 로직, 그리고 최악의 상황에서는 Model 데이터를 가져와서 서버 연결을 진행하는 로직까지 가지고 있을 겁니다. 말만 들어도 Controller가 무겁다고 느껴지지 않나요?
그 와중에 Controller가 View Life Cycle를 가지기 때문에 해당 부분에 View가 뒤엉키게 됩니다. 겉잡을 수 없이 Controller의 크기가 커지는거죠.
Cocoa MVC는
•
View와 Controller를 분리하기 어렵고
•
View와 Controller를 분리하기 어렵기 때문에 둘 사이에 상호작용이 생겨서 테스트하기 어려워집니다. 그나마 분리가 되어있는 Model만 테스트할 수 있고
•
다른 패턴들에 비해서 쉽게 접근할 수 있는 패턴이기 때문에 개발 속도면에서는 최고의 아키텍쳐입니다.
𝑽𝒊𝒆𝒘 𝑪𝒐𝒏𝒕𝒓𝒐𝒍𝒍𝒆𝒓의 존재부터가 잘못된걸까?
마치며
MVC가 쉽게 접근할 수 있고 각자의 역할만 잘 분리해서 사용한다면 좋은 패턴이라는 생각이 듭니다. 하지만 Cocoa MVC를 사용해야하는 이상 비대한 View Controller를 막는건 쉽지 않습니다. 그렇기 때문에 MVC에서 파생된 다양한 패턴들이 나왔다고 생각이 드네요.
Cocoa MVC를 사용해야 한다면 최대한 재사용 가능한 객체를 많이 만들고 View, Model 객체의 경우에는 재사용이 용이하도록 만들어주면 좋습니다. 또한 클래스 코드에 종속성을 낮춰서 재사용을 할 수 있도록 해줘야 합니다.
MVC 예제 코드는 이후에 작성 후 링크를 연결해둘게요.
[23.03.03 (금)] View / ViewController 역할 분리 관련 코드입니다.
[23.03.16 (목)] 실제 프로젝트에서 M-V-C 아키텍쳐로 짜보려고 노력했습니다.
어떤 생각으로 코드를 작성했는지 PR에 자세하게 작성해뒀습니다.
혹시 잘못된 부분이 있다면 지적해주세요!!