iOS 개발 - MVC 패턴과 UIKit의 ViewController

Heechan
HcleeDev
Published in
13 min readJul 18, 2021
Photo by Sigmund on Unsplash

SwiftUI를 사용하는 나로서는 MVC 패턴보다는 MVVM 패턴에 익숙한 편인데, UIKit 쪽을 같이 사용하려하다보면 ViewController라는게 꽤 자주 눈에 밟혔다. 찾아보니 원래 MVC를 많이 채택했던 것으로 보여서, 조금이나마 공부해보는게 좋을 것 같아, 이번주는 MVC에 대해 알아보고 간단히 정리해보았다.

Model, View, Controller

MVC 패턴은 Model-View-Controller 패턴의 줄임말으로, 이름 그대로 세 가지 계층으로 각 코드의 책임과 역할을 나눈다. 계층은 각각 Model, View, Controller로 나뉜다.

Model

Model은 데이터와 관련된 내용을 담고 있다. 그리고 데이터를 관리하는 로직도 포함하고 있다. 네트워크를 통해 받아온 DTO 구조체와 네트워크에 접근하는 로직, 파일로 따로 저장해야하는 Persistance한 데이터를 로드한다든가, 아니면 필요한 구조체를 만드는 경우 해당 내용들은 모두 Model에 포함된다.

Model은 UI와 직접적으로 연결되지 않는다. 당연히 불가능한 것은 아니지만 MVC 패턴을 제대로 활용하기 위해서는 Model은 받아온 데이터를 그에 맞춰 저장할 형태를 만드는 것이 중요하지 UI에서 어떻게 보일지는 신경쓰지 않는 것이 좋다. 예를 들면, Person이라는 구조체에 생일을 저장하는 것은 괜찮지만, 여기서 이 생일 날짜 문자열을 어떤 식으로 파싱해서 어떤 식으로 화면에 띄워줄지는 고민하지 않아도 된다.

Model을 아예 이렇게 분리했기에 테스트할 때 Mock을 만들기 쉬운 점도 있다. 현실적으로 서버 쪽이 만들어지지 않아 데이터를 실제로 받을 수는 없지만 View랑 Controller는 만들어서 테스트를 해봐야 할 때가 있다. 이럴 때 데이터와 관련된 부분을 앱 로직으로부터 아예 분리해 Model에 넣어두었으므로, Model을 Mock으로 하나 만들어서 간단히 테스트해볼 수 있다.

Model에는 대부분 이런 코드들이 포함된다.

  • 데이터로 사용하는 구조체: 위에서 예시로 든 Person 구조체 같은거라고 생각할 수 있을 것 같다. struct Person {let name, birthDate} 이런 느낌.
  • 네트워크 로직: 네트워크 요청을 하고, 그 결과를 받아오는 기본적인 기능을 담은 네트워크 로직이 포함된다.
  • Persistance 로직: 메모리에 저장되는 데이터를 로드 및 세이브하는 로직이 포함된다.
  • 데이터 파싱 로직: 네트워크로 받아오든 내부 파일에서 받아오든, JSON 같은 데이터가 왔을 때 이를 파싱하는 로직도 포함한다.
  • Manager 객체(shared 객체): 구조체를 만들어두고 필요한 경우에는 어디서든 접근해 사용할 수 있도록 따로 Manager를 만드는 경우도 Model에 포함된다.
  • Util, Extension, Constant: 앱으로 따지면 우리가 정의하는 Color나, String의 추가적인 기능, 혹은 특정 사이즈에 대한 정보를 util, extension, constant로 많이 만들게 되는데, 이런 경우도 Model에 포함된다.

View

View는 흔히 말하는 UI다. 유저들에게 데이터를 보여주고, 어떻게 보여줄지 화면을 구성하는 코드들이 포함되어있다. 또한 View 계층은 직접 유저와 상호작용을 해야 하는 곳이다보니, 상호작용을 Controller 계층으로 전달하는 역할도 한다.

기본적으로 View는 Controller로부터 정리된 데이터를 받아서 화면에 보여주는 역할을 하나, 무조건 데이터를 저장해서 안되는 것은 아니다. 간단한 정보들은 View단에서도 정의해서 사용할 수 있다. 상태와 관련된 변수를 View에서 정의해 사용하는 경우도 많다.

View를 작성할 때는 재사용성이 강조된다. 화면에 들어가는 여러 요소들을 어떻게 잘 나누어서 앱 전반에서 재활용할 수 있도록 할지가 중요하다. 예를 들면, 앱 전반에서 일관적으로 사용되는 디자인의 버튼이 있다고 치자. 이때 필요할 때마다 해당 디자인을 다시 코드로 작성하는 것이 아니라, 재사용할 수 있도록 MyButton 이런 덩어리를 따로 만들어서 관리하면 보다 코드가 깔끔해질 것이다.

iOS MVC의 View에는 주로 아래의 코드들이 포함된다.

  • 주로 UIView를 상속해 만들어진 subclass
  • Core Animation
  • Core Graphics

Controller

Controller는 앱의 핵심 로직을 담고 있는 계층이다. Controller는 View, Model에 연결되어 그 중간의 역할을 하고 있다. MVVM 패턴에서 ViewModel이 하고 있는 역할과 비슷하다고 볼 수 있다.

View에서 보여주기 위한 데이터를 이 Controller가 보내주면서 View를 refresh하고, 그 데이터를 Model으로부터 가져오는 기능을 한다. 물론 View로부터 유저와의 상호작용에 대한 정보를 받고, 그 정보를 바탕으로 해당된 로직을 실행하고 Model의 정보를 업데이트하는 기능도 있다.

Controller는 해당 View마다 하나씩 붙어서 그 View에 맞는 로직을 포함하고 있기 때문에, 재사용성은 View보다 훨씬 떨어진다. 재사용성이 적다보니 코드가 길어지는 경우가 많다.

Controller와 UIKit UIViewController?

바로 위에서 말했듯, Controller는 Model과 View에 모두 연결되어있기 때문에, 그 역할에 따라 또 두 가지로 나눌 수 있다.

첫 번째는 Model Controller고, 이 경우엔 Controller가 Model을 가지고 있다. 이 Model Controller는 Model의 데이터를 관리하고, View에 데이터를 전달하는 역할을 한다.

두 번째는 View Controller고 이 경우에는 Controller가 View를 가지고 있다. View를 관리하고 유저와의 상호작용도 관리하며, Model의 데이터를 업데이트하는 역할을 한다.

내가 원래 궁금했던 UIKit의 UIViewController와 Controller 계층은 어떤 관계가 있는지 더 알아보고자 했다.

UIViewController가 Controller와 비슷한 역할을 하기는 한다. UIViewController는 그가 하나의 Root View를 관리하며, 그 아래 있는 많은 subView들까지 관리한다.

  • Model로부터 받아온 데이터의 변화에 따라 View를 업데이트
  • 유저와의 상호작용에 반응
  • View의 Layout을 관리하고 Resize함
  • 다른 객체와 상호작용

이 4가지가 UIViewController가 주로 하는 역할으로, Controller 계층이 맡아야 하는 역할을 대다수 수행하고 있다.

하지만 UIViewController가 약간 다른 느낌으로 다가왔던 이유는, 원래 iOS 앱을 개발하는 방식과도 연관이 있을 것 같다. 지금 SwiftUI는 모든 부분을 코드를 작성하는 것으로 View를 구성하고, ViewModel을 만들지만, UIKit을 사용해 앱을 만들 때는 주로 StoryBoard를 이용했다.

위 사진이 스토리보드고, 그 중 한 화면을 Scene이라고 한다. 우리는 각 Scene마다 ViewController를 하나씩 연결해주어야 하고, 그 ViewController에서 해당 Scene에 있는 하위 View들까지 관리해주는 방식으로 앱을 구성한다.

그렇다보니 Controller와 View 계층이 분리되었다고 느끼기 보단, Controller 안에 View를 집어넣고 하위 View로서 포함하는 느낌이 들어서 위화감이 있었다.

하지만 이는 ViewController가 View를 ‘소유’하고 있어서 그렇게 느껴지는 것 같다. ViewController에서는 주로 @IBOutlet weak var tableView: UITableView이런 식으로 View를 ‘소유’하고 있고, 관련 설정도 조금씩 하곤 한다. 이는 글 말미의 예시에서도 확인할 수 있다.

각 View 컴포넌트가 디자인이 어떻게 생겼는지, 어떤 정보를 담고 있고 어떻게 보여주고 있는지는 Controller가 아니라 View 계층에서 따로 정의해주고있다. @IBOutlet weak var tableView: UITableView 이런 코드를 ViewController가 가지고 있다 하더라도, UITableView라는 View가 어떻게 생긴지는 UITableView의 정의에 작성되어있을 것이다. 따라서 ViewController는 View 계층이 해야 하는 일을 침범하지 않고, Controller 계층이 해야 하는 일을 잘 수행하고 있다고 볼 수 있다.

View Controllers | Apple Developer Documentation 이 곳에 들어가면 UIKit에서 지원하는 다양한 ViewController의 종류를 확인할 수 있다.

iOS에서의 MVC 디자인 패턴

iOS 앱을 개발할 때 MVC 패턴을 적용하면, 아무래도 계층 간의 소통은 어떻게 이뤄지는 것인지가 궁금하다.

Controller는 Model과 View에 어렵지 않게 접근할 수 있다. Controller에 속한 subView에 직접 접근해 설정을 변경할 수도 있고, 생성된 Model 객체를 받아왔다면 그 객체에 직접 접근해 데이터를 활용할 수도 있다.

하지만 Model에서 Controller로 가거나, View에서 Controller로 갈 때가 문제인데, iOS에서는 이를 KVO와 Delegation으로 해결한다.

Model에서 Controller로 뭔가 전달해야 하는 경우는 데이터가 변화했다는 사실을 알려주는 정도일 것이다. 애초에 Model 쪽에 있는 코드는 데이터를 제공하는 코드지 위에 있는 Controller가 어떻게 만들어져있는지는 아예 모르는 구조이기 때문에, 직접 접근은 불가능하다. 이럴 때 Controller는 Model에 있는 프로퍼티에 Key-Value Observing을 이용해 그 프로퍼티의 변화를 감지할 수 있고, 그에 따라 새로운 데이터를 받아올 수 있다. 혹은 Notification을 이용해 뭔가 변화했다는 신호를 보내두면, 나중에 Controller가 그 알림을 보고 다시 Model에 접근해 새 데이터를 받아올 수 있다.

View에서 Controller로 전달해야 하는 경우는 어떨까. 이 경우는 대부분 유저와의 상호작용에 대해 전달하는 경우일텐데, 주로 Delegation 방식을 이용해 Controller에 관련 코드를 작성한다. Delegate는 위임한다는 뜻을 가지고 있는데, View에 어떤 이벤트가 발생하면 그 이벤트의 처리는 delegate로 연결해둔 Controller에게 위임한다고 생각할 수 있다. ViewController 안에서 imageView.delegate = self 이런 식으로 포함하고 있는 View의 delegate를 ViewController로 설정할 수 있다. Data Source도 비슷한 역할을 한다.

Model이나 View나, 둘 다 생각해보면 결국 View가 Controller에, Model이 Controller에 직접 접근할 수 없으므로 결국 Controller가 그 역할을 도맡아 하고 있다. 말그대로 Controller가 Model과 View가 해야할 일을 ‘위임’받아 하고 있는 느낌이다.

아주 간단한 MVC 예시를 보자.

일단 처음으로, Model에 해당하는 코드다.

struct Person {
let name: String
let age: Int
}

그리고 그 다음은, Controller가 있는 코드다. ViewController가 Controller 역할을 하면서, View에 대해 어떻게 처리할지에 대한 코드도 일부 담고 있다.

class PersonListController: UIViewController {
@IBOutlet weak var tableView: UITableView
let persons = [Person(name: "Heechan", age: 23), Person(name: "Lee", age: 23)]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
}
extension PersonListController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection: Int) -> Int {
persons.count
}
func tableView(_ tableView: UITabelView, cellForRowAt: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
cell.textLabel?.text = persons[cellForRawAt.row].name
return cell
}
}

위 코드를 보면, delegate는 사용하지 않고 DataSource를 사용한 예시를 보여주고 있다. UITableViewDataSource에서 필수로 설정해주어야 하는 두 개 함수를 만들어서, View가 그려질 때 Controller가 가지고 있는 데이터를 View로 전달해주는 모습이다.

여기서 View는 직접 정의하지 않고, UIKit에서 제공하는 UITableViewCell을 그대로 사용했다. View는 재사용하는 것이 강조된다고 했으니, 잘 만들어진 Cell을 그대로 재사용했다. 위 Controller 코드에서 UITableViewCell에 데이터를 넣어주고, 그 cell을 반환하는 식으로 했기에 특별한 View 관련 코드는 없다.

그럼 View의 레이아웃 같은 부분은 어떻게 코딩하나 싶은데, 대부분 Storyboard에 개발자가 직접 필요한 요소(버튼, 라벨 등)를 드래그해서 화면에 배치해서 만드는 경우가 많다. 이런 View 내부의 위치, layout 등의 자세한 정보는 storyboard, nib 파일이 가지고 있다. 위에서 봤듯 Controller도 View 관련 설정을 하긴 하지만, View 계층의 중심은 Storyboard라고 생각할 수 있다.

결론

처음은 ViewController에 대해 궁금했던 것이었는데, MVC도 알게되고 UIKit 기능을 이용할 때도 도움이 될 것 같다. 그리고 UIViewController도 생각보다 알아볼 내용이 많은 것 같아서… 이리저리 앞으로도 쓸 내용이 있을 수도 있겠다 싶다.

조만간 MVVM도 한번 다루어보고 MVVM과 MVC를 비교하는 글도 작성해보아야겠다.

참고한 글

Model-View-Controller (apple.com)

UIViewController | Apple Developer Documentation

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science