구조 다이어그램
Model <—> Controller | View Controller가 모든 것을 소유: - Model 직접 접근 - View 직접 조작 - 사용자 이벤트 처리
핵심 학습 포인트
  • Apple 기본 패턴 — UIKit의 핵심 구조
  • Controller가 Model과 View를 모두 소유
  • "Massive View Controller" 문제 체감
  • 테스트 작성의 어려움 직접 느끼기
  • 다음 아키텍처 비교의 기준점
내장 디자인 패턴
Singleton Delegate

Singleton — TaskStorage.shared로 전역 저장소 접근.

Delegate — UITableViewDelegate로 이벤트 위임.

 01-MVC/Start/TaskListViewController.swift
import UIKit

// MARK: - MVC Start
// TODO: MVC 패턴으로 Task 목록 화면을 완성하세요.
// 주목: Controller가 얼마나 커지는지 느껴보세요!

class TaskListViewController: UITableViewController {

    private var tasks: [TaskItem] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Tasks (MVC)"
        setupUI()
        loadTasks()
    }

    private func setupUI() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add, target: self, action: #selector(addTapped))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    private func loadTasks() {
        // TODO: TaskStorage.shared에서 tasks를 로드하고 tableView를 리로드하세요
    }

    @objc private func addTapped() {
        // TODO: UIAlertController로 새 Task 추가 UI를 만드세요
    }

    // MARK: - TableView DataSource

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // TODO: tasks.count 반환
        return 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        // TODO: task의 title, priority emoji, 완료 상태를 표시하세요
        return cell
    }

    // MARK: - TableView Delegate

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        // TODO: 완료 상태를 토글하세요
    }

    override func tableView(_ tableView: UITableView,
                            commit editingStyle: UITableViewCell.EditingStyle,
                            forRowAt indexPath: IndexPath) {
        // TODO: 스와이프 삭제를 구현하세요
    }
}
 01-MVC/Finish/TaskStorage.swift
import Foundation

// MARK: - Singleton 패턴 (완성)

final class TaskStorage {
    static let shared = TaskStorage()  // ← Singleton
    private init() {}

    private let key = "mvc_tasks"
    private let defaults = UserDefaults.standard

    func loadAll() -> [TaskItem] {
        guard let data = defaults.data(forKey: key),
              let tasks = try? JSONDecoder().decode([TaskItem].self, from: data)
        else { return TaskItem.samples }
        return tasks
    }

    func saveAll(_ tasks: [TaskItem]) {
        if let data = try? JSONEncoder().encode(tasks) {
            defaults.set(data, forKey: key)
        }
    }

    func add(_ task: TaskItem) {
        var tasks = loadAll(); tasks.insert(task, at: 0); saveAll(tasks)
    }

    func update(_ task: TaskItem) {
        var tasks = loadAll()
        if let idx = tasks.firstIndex(where: { $0.id == task.id }) { tasks[idx] = task }
        saveAll(tasks)
    }

    func delete(id: UUID) {
        var tasks = loadAll(); tasks.removeAll { $0.id == id }; saveAll(tasks)
    }
}
 01-MVC/Finish/TaskListViewController.swift
import UIKit

// MARK: - MVC Finish (완성본)
// 주목: Controller가 Model 접근 + View 설정 + 사용자 인터랙션을 모두 처리

class TaskListViewController: UITableViewController {

    private var tasks: [TaskItem] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Tasks (MVC)"
        setupUI()
        loadTasks()
    }

    private func setupUI() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add, target: self, action: #selector(addTapped))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    private func loadTasks() {
        tasks = TaskStorage.shared.loadAll()
        tableView.reloadData()
    }

    @objc private func addTapped() {
        let alert = UIAlertController(title: "New Task", message: nil, preferredStyle: .alert)
        alert.addTextField { $0.placeholder = "Task title" }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self] _ in
            guard let title = alert.textFields?.first?.text, !title.isEmpty else { return }
            TaskStorage.shared.add(TaskItem(title: title))
            self?.loadTasks()
        })
        present(alert, animated: true)
    }

    override func tableView(_ tv: UITableView, numberOfRowsInSection s: Int) -> Int { tasks.count }

    override func tableView(_ tv: UITableView, cellForRowAt ip: IndexPath) -> UITableViewCell {
        let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
        let task = tasks[ip.row]
        cell.textLabel?.text = "\(task.priority.emoji) \(task.title)"
        cell.textLabel?.textColor = task.isCompleted ? .secondaryLabel : .label
        cell.accessoryType = task.isCompleted ? .checkmark : .none
        return cell
    }

    override func tableView(_ tv: UITableView, didSelectRowAt ip: IndexPath) {
        tv.deselectRow(at: ip, animated: true)
        tasks[ip.row].isCompleted.toggle()
        TaskStorage.shared.update(tasks[ip.row])
        tv.reloadRows(at: [ip], with: .automatic)
    }

    override func tableView(_ tv: UITableView, commit style: UITableViewCell.EditingStyle, forRowAt ip: IndexPath) {
        guard style == .delete else { return }
        let id = tasks[ip.row].id
        tasks.remove(at: ip.row)
        TaskStorage.shared.delete(id: id)
        tv.deleteRows(at: [ip], with: .fade)
    }
}
핵심 개념
Singleton 패턴
TaskStorage.shared는 앱 전체에서 하나의 인스턴스만 존재합니다. private init()으로 외부 생성을 막고, static let shared로 전역 접근점을 제공합니다.
Delegate 패턴
UITableView가 자신의 데이터 소스/이벤트를 Controller에 위임합니다. didSelectRowAt, numberOfRowsInSection 등이 Delegate 메서드입니다.
Massive VC 문제
완성된 Controller를 보면 UI 설정, 데이터 로드, 비즈니스 로직, 네트워크 요청이 한 파일에 모입니다. 앱이 커질수록 유지보수가 급격히 어려워집니다.
테스트 어려움
Controller가 UIKit에 강하게 결합되어 있어 비즈니스 로직만 단독으로 테스트할 수 없습니다. MVP/MVVM이 이 문제를 Protocol로 해결합니다.
MVC vs 다음 아키텍처
항목
MVC
MVP / MVVM
비즈니스 로직 위치
Controller 내부
Presenter / ViewModel 분리
View 독립성
Controller가 직접 조작
Protocol / Binding으로 분리
단독 테스트
UIKit 의존 — 어려움
Mock View로 쉽게 가능