0. 서문
디자인 패턴을 공부할 때 가장 먼저 들었던 키워드가 MVC였습니다. Model-View-Controller로 책임을 나눠 구현하는 방법으로, 가장 널리 사용되는 패턴이기도 합니다. 많이 듣기도 했고 많이 적용도 해봐서였을까요(자만 했었던 것 같습니다), 면접 질문에서 잘 대답했다고 생각했었는데 집으로 돌아오는 길에 확인해보니 잘못된 부분이 분명 있었습니다. 다시 한번 초심으로 돌아간다 생각하고 MVC를 정리해보려 합니다.
1. 전통적인 MVC
우선 전통적인 MVC가 무엇인지 알아야 합니다. 모든 디자인 패턴이 그렇듯이 각 부분은 저마다의 '관심사'가 존재합니다. 이로써 서로의 명확한 관심사를 분리하고, 더 나은 업무의 분리와 향상된 관리를 제공하는 것이 디자인 패턴의 주 목적입니다. 전통적인 MVC의 세 가지 부분은 다음과 같이 설명할 수 있습니다.
- Model 모델: 데이터와 비즈니스 로직을 관리합니다.
- View 뷰: 레이아웃과 화면을 처리합니다.
- Controller 컨트롤러: Model과 View로 명령을 전달합니다.
Model 모델
데이터와 비즈니스 로직을 관리합니다.
- 앱이 포함해야할 데이터가 무엇인지 정의합니다.
- 데이터의 상태가 변경되면, Model을 일반적으로 View에 알리며 가끔 컨트롤러에게 알리기도 합니다.
Model을 일반적으로 View에 알린다는 표현이 생소하게 다가오는 것 같습니다. 아직 본 포스팅에서 Cocoa MVC에 대해 다루지 않았지만, Cocoa에서는 일반적으로 ViewController를 통해서만 View를 업데이트할 수 있기 때문입니다. (Model이 View 혹은 ViewController에 대해 몰라야함) 우선 이 부분을 기억하고 다음 부분도 살펴보겠습니다.
View 뷰
레이아웃과 화면을 처리합니다.
- 앱의 데이터를 보여주는 방식을 정의합니다.
- 표시할 데이터를 모델로부터 받습니다.
역시 모델로부터 데이터를 받는다고 이야기합니다. 그럼 Controller의 역할은 무엇일까요?
Controller
Model과 View로 명령을 전달합니다.
- 앱의 사용자로부터의 입력에 대한 응답으로 Model 또는 View를 업데이트하는 로직을 포함합니다.
- 입력(이벤트)이 Controller로 > Controller는 Model을 업데이트 > 데이터를 View로 전송.
결국 Controller의 역할은 특정 이벤트가 발생했을 때, 혹은 데이터를 View에 맞게 특정 형태로 가공하고 싶을 때 중개하는 역할로 해석할 수 있을 것입니다. 하지만 이렇게만 해석했을 때, View와 Model이 서로를 알고 있어야 업데이트가 가능한 것이 아닐까? 생각되기도 합니다. 모호한 부분이 가시지 않아 GPT의 도움을 받아봤습니다.
MVC의 중요한 원칙 중 하나는 Model과 View 간의 직접적인 의존성을 피하는 것이라고 합니다. 즉, 전통적인 MVC에서 Model이 View를 업데이트 한다는 것은 간접적인 연결(Observer 패턴을 통한 바인딩)로의 업데이트를 의미한다는 것을 알 수 있습니다.
즉, View로부터 출발한 이벤트가 Model을 업데이트하려면 Controller를 거치게 되지만, Model의 변경이 일어났을 때는 Controller를 거치지 않고 간접적으로 View를 업데이트하는 것으로 해석할 수 있습니다.
2. 애플의 Cocoa MVC
Cocoa MVC는 이름부터 MVC로 부터 뿌리를 내려왔습니다. 하지만 애플은 전통적인 MVC의 문제점에 부딪혔고, 그 구체적인 이유는 공식문서에서 찾을 수 있었습니다.
However, there is a theoretical problem with this design. View objects and model objects should be the most reusable objects in an application. View objects represent the "look and feel" of an operating system and the applications that system supports; consistency in appearance and behavior is essential, and that requires highly reusable objects. Model objects by definition encapsulate the data associated with a problem domain and perform operations on that data. Design-wise, it's best to keep model and view objects separate from each other, because that enhances their reusability.
하지만 이 디자인에는 이론적 문제가 있습니다. View 객체와 Model 객체는 애플리케이션에서 가장 재사용 가능한 객체여야 합니다. 뷰 객체는 운영 체제와 시스템이 지원하는 애플리케이션의 "외형과 느낌"을 나타냅니다. 모양과 동작의 일관성이 필수적이며, 이를 위해서는 재사용성이 높은 객체가 필요합니다. 모델 객체는 정의상 문제 도메인과 관련된 데이터를 캡슐화하고 해당 데이터에 대한 작업을 수행합니다. 디자인 측면에서 모델 객체와 뷰 객체를 서로 분리하는 것이 가장 좋습니다. 그렇게 하면 재사용성이 향상되기 때문입니다.
즉, View와 Model 객체는 애플리케이션에서 가장 재사용 가능한 객체여야하기 때문에 View와 Model을 서로 분리하는 것이 Cocoa MVC가 전통적인 MVC와 가장 큰 차이점이라고 정의할 수 있습니다. 전통적인 MVC에서는 Model이 View를 업데이트하기 위한 데이터 바인딩 로직이 포함되어 있었기 때문에, 완전한 분리라고 보기 어렵기 때문입니다. 아래 도식화 이미지를 통해 완전한 분리의 의미를 이해할 수 있습니다.
전통적인 MVC와 가장 다른 점으로는 역시 View와 Model의 업데이트가 모두 Controller를 통해서만 이루어진다는 것을 확인할 수 있습니다. 간접적으로도 연결된 것이 아닌, 완전한 분리를 달성할 수 있는 것입니다.
3. 코드로 구현해보는 MVC
전통적인 MVC과 Cocoa MVC를 비교해보기 위해 간단한 예제를 만들어보려 합니다. '라벨 업데이트 시키기' 버튼을 탭하면, 라벨이 새로운 String으로 업데이트 되는 간단한 로직입니다.
전통적인 MVC
Model
데이터를 표현하는 구조체 Dog와 비즈니스 로직을 관리하는 DogModel로 구성되었습니다. 또한 Model은 새로운 업데이트 내용을 발신(publish)합니다. 이 과정에서는 Publisher와 Observer를 직접 구현하는 대신 애플이 제공하는 NotificationCenter를 활용하고 있습니다.
struct Dog {
var name: String
var age: Int
}
final class DogModel {
/// NotificationCenter를 이용해 이벤트를 발신(Publish)합니다.
func createNewDog() {
NotificationCenter.default.post(
name: .createNewDog,
object: Dog(name: "민톨", age: 4)
)
}
}
extension Notification.Name {
static let createNewDog = Notification.Name("createNewDog")
}
View
View는 옵저버(NotificationCenter)로 부터 받은 내용을 반영해 화면을 업데이트합니다. 간접적으로 Model을 알고 있다고 이야기할 수 있습니다.(이 때문에 재사용성이 떨어진다고 애플은 표현하기도 했습니다.)
final class View: UIView {
let textLabel = UILabel()
let button = UIButton()
/// 라벨을 업데이트 시키는 함수입니다.
@objc func updateTextLabel(notification: Notification) {
guard let dog = notification.object as? Dog else { return }
textLabel.text = "\(dog.name)의 나이는 \(dog.age)입니다."
}
init() {
...
/// View 생성과 동시에 옵저버(Notification)를 구독합니다.
/// 이로써 이벤트를 받음과 동시에 View 업데이트가 가능해집니다.
NotificationCenter.default.addObserver(
self,
selector: #selector(updateTextLabel),
name: .createNewDog,
object: nil
)
...
}
}
Controller
Controller는 Model과 View 사이의 이벤트를 연결합니다. Controller는 Model의 함수를 호출만 하고, View를 업데이트하고 있지는 않습니다. (물론 직접적으로 View를 업데이트 하는 로직도 포함될 수 있습니다!)
final class Controller: UIViewController {
private let customView = View()
private let model = DogModel()
override func loadView() {
view = customView
}
override func viewDidLoad() {
super.viewDidLoad()
customView.button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
/// View로 부터 받은 이벤트를 Controller가 다시 Model을 업데이트합니다.
@objc func buttonTapped() {
model.createNewDog()
}
}
Cocoa MVC
Model
데이터를 표현하는 구조체 Dog와 비즈니스 로직을 관리하는 DogModel로 구성되었습니다. 데이터를 함수를 통해 반환하기만 할 뿐이고, 어느 곳에도 의존하지 않기 때문에 완전히 독립되었다고 이야기할 수 있습니다.
struct Dog {
var name: String
var age: Int
}
struct DogModel {
/// Dog 객체 반환
func createNewDog() -> Dog {
Dog(name: "민톨", age: 4)
}
}
View
화면을 표현하는 디자인 요소로 구성되었습니다. 역시 완전히 독립되었습니다.
final class View: UIView {
let textLabel = UILabel()
let button = UIButton()
/// View는 화면을 그리는데만 초점을 둡니다.
/// 이로써 View는 완전히 독립적입니다.
init() {
super.init(frame: .zero)
backgroundColor = .white
[textLabel, button].forEach {
addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
...
}
}
Controller
Model과 View를 중개하는 역할을 수행합니다. Controller로 들어오는 button 액션을 Model을 통해 데이터를 받고 View를 업데이트합니다.
final class Controller: UIViewController {
private let customView = View()
private let model = DogModel()
override func loadView() {
view = customView
}
override func viewDidLoad() {
super.viewDidLoad()
customView.button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
/// Model로부터 데이터를 받아와 직접적으로 View를 업데이트합니다.
/// Controller가 Model과 View 사이의 이벤트를 직접적으로 관리합니다.
@objc func buttonTapped() {
let newDog = model.createNewDog()
customView.textLabel.text = "\(newDog.name)의 나이는 \(newDog.age)입니다."
}
}
4. 정리
두 가지 패턴을 비교하며 알게 된 점은 지금까지 Cocoa MVC 방식을 전통적인 MVC 방식이라고 착각하고 있다는 것이었습니다. Model과 View의 분리가 당연하다고 생각했고 처음을 Cocoa MVC로 배웠기 때문에 놓쳤었던 부분이었던 것 같습니다. 하지만 이를 뜯어보며 Cocoa MVC가 나온 배경이 재사용성을 높이기 위한 과정임을 배울 수 있었습니다.(또 생각보다 심오한 것들이 MVC안에 숨어 있다는 것도 알게 됨,,,)
기본은 다지고 다져도 부족함을 느끼고 또 성장을 위한 최고의 밑거름임을 다시 한번 느끼게 됩니다.
'iOS' 카테고리의 다른 글
캐플 리팩토링 두 번째 이야기 - 프로젝트 세팅하기 (0) | 2025.01.30 |
---|---|
구조체로 싱글톤 만들기? (0) | 2025.01.30 |
캐플 리팩토링 첫 번째 이야기 - 방향성 설정하기 (0) | 2025.01.22 |
비동기 작업의 단위, Task 알아보기 (0) | 2025.01.22 |
인생 첫 번째 iOS 개발 면접 후기 (1) | 2025.01.14 |