iOS 앱 개발 환경이 점차 복잡해지면서, 기존의 아키텍처 패턴들이 직면한 한계를 극복하기 위해 코디네이터 패턴이 등장했다. 특히 MVC 패턴의 문제점이 두드러지게 나타났다. MVC패턴에서는 ViewController가 UI로직, 비즈니스 로직, 그리고 화면 전환 로직까지 모두 담당하게 되어 "Massive ViewController" 문제가 발생했다. 이로 인해 코드의 재사용성이 떨어지고, 유지보수가 어려워졌으며, 단위 테스트 작성도 복잡해졌다.
앱의 규모가 커지고 기능이 다양해지면서, 화면 간의 전환 로직도 복잡해졌다. 여러 화면을 가쳐가는 사용자 플로우를 관리하는 것이 점점 더 어려워졌고, 이는 코드의 가독성과 유지보수성을 저하시켰다. 또한, 앱의 각 부분을 독립적인 모듈로 개발하고 쉽게 확장할 수 있는 구조에 대한 요구가 증가했다. 개발자들은 SOLID 원칙, 특히 단일 책임 원칙을 더 잘 준수할 수 있는 방법을 찾고 있었다.
이러한 이유로 Khanlou가 코디네이터 패턴을 제시했다.
❓MVC 패턴의 문제점이 두드러지게 나타났다 ➡️ MVVM이 등장했잖아?
MVVM 패턴은 주로 View와 Model 사이의 결합도를 낮추고, ViewController의 책임을 줄이는 데 초점을 맞추고 있다. 하지만 MVVM만으로는 해결되지 않는 문제가 있다.
1. 화면 전환 로직 : MVVM은 주로 단일 화면 내의 로직을 처리하는 데 집중한다. 하지만 여러 화면 간의 전환 로직, 즉 앱의 전체적인 흐름 관리에 대해서는 명확한 해결책을 제시하지 않는다.
2. 앱 수준의 로직 : MVVM은 개별 화면의 비즈니스 로직을 ViewModel로 분리하지만, 앱 전체의 흐름을 관리하는 로직에 대해서는 다루지 않는다.
3. 모듈 간 의존성 : MVVM만으로는 각 화면(또는 모듈)간의 의존성을 완전히 제거하기 어렵다.
코디네이터 패턴은 MVVM(또는 다른 아키텍처 패턴)과 함께 사용될 수 있으며, 아래와 같은 추가적인 이점을 제공한다.
1. 화면 전환 책임 분리 : 코디네이터가 화면 전환 로직을 담당하여, ViewModel이나 ViewController에서 이 책임을 제거한다.
2. 앱 흐름의 중앙화 : 앱의 전체적인 네비게이션 흐름을 코디네이터에서 관리함으로써, 앱의 구조를 더 명확하게 만든다.
3. 모듈 간 의존성 감소 : 각 화면이 다른 화면에 대해 직접적으로 알 필요가 없어져, 모듈 간 의존성이 줄어든다.
4. 재사용성 향상 : 화면 전환 로직이 분리되어 있어, 쉽게 재사용할 수 있다.
따라서, MVVM과 코디네이터 패턴은 서로 다른 문제를 해결하며, 상호 보완적으로 사용될 수 있다. MVVM이 개별 화면의 로직을 정리한다면, 코디네이터 패턴은 앱 전체의 흐름과 화면 간 전환을 관리한다.
Coordinator Pattern?
코디네이터 패턴은 앱의 화면 전환 및 흐름 제어 로직을 별도의 객체(코디네이터)로 분리하는 아키텍처 패턴이다. 코디네이터 패턴은 화면 간의 의존성을 줄이고 앱의 네비게이션 로직을 중앙화하는 것이 주적이다.
class ParentViewController: UIViewController {
...
private func buttonTapped() {
// 부모 뷰컨트롤러에서 자식 뷰컨트롤러의 객체를 생성했음. (부모 뷰컨이 자식 뷰컨을 알아야한다.)
let childViewController = ChildViewController()
self.navigationController?.pushViewController(childViewController, animated: true)
}
...
}
✅ 기존에는 화면 전환을 ViewController가 담당했다. 때문에 부모 뷰컨은 자식 뷰컨을 알아야했고, 자식 뷰컨에 필요한 객체 또한 모두 부모 뷰컨 내부에서 생성해야 했다. 이는 객체 간 결합도를 증가시킨다. 코디네이터를 도입하면 더 이상 부모 뷰컨은 자식 뷰컨을 알 필요가 없다. 자식 뷰컨의 의존성을 주입하고 객체를 생성해 화면 전환을 하는 역할은 코디네이터가 하기 때문이다.
Coordinator Pattern의 구조
1. Coordinator 프로토콜
import UIKit
protocol Coordinator: AnyObject {
var parentCoordinator: Coordinator? { get set }
var children: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
extension Coordinator {
func childDidFinish(child: Coordinator) {
children.removeAll { $0 == child }
}
}
코디네이터 패턴의 기본 구조를 정의하는 프로토콜이다. 개발자마다 구현 방법이 다르겠지만, 가장 정형화 된 방법이다.
✅ parentCoordinator : 상위 코디네이터에 대한 약한 참조를 저장한다. 앱의 구조와 복잡도에 따라 항상 필요한 것은 아니다. 간단한 앱에서는 생략할 수 있다.
1. 계층 구조 관리 : 복잡한 앱에서는 여러 레벨의 코디네이터가 존재할 수 있다. parentCoordinator를 통해 상위 코디네이터에 접근할 수 있어, 전체 코디네이터 트리를 쉽게 탐색할 수 있다.
2. 메모리 관리 : 자식 코디네이터가 더 이상 필요하지 않을 때, 부모 코디네이터에게 이를 알리고 제거할 수 있다.
3. 상위 흐름으로의 복귀 : 특정 흐름이 완료되었을 때, 자식 코디네이터는 부모 코디네이터에게 이를 알리고 제어권을 넘길 수 있다. 예를 들어, 로그인 흐름이 완료되면 메인 흐름으로 돌아갈 때 사용할 수 있다.
4. 데이터 전달 : 자식 코디네이터에서 부모 코디네이터로 데이터를 전달할 때 사용할 수 있다.
5. 유연한 네비게이션 : 특정 상황에서 현재 흐름을 완전히 종료하고 앱의 다른 부분으로 전환해야 할 때, 부모 코디네이터를 통해 이를 쉽게 구현할 수 있다.
class ChildCoordinator: Coordinator {
weak var parentCoordinator: Coordinator?
func finish() {
parentCoordinator?.childDidFinish(self)
}
}
class ParentCoordinator: Coordinator {
var children: [Coordinator] = []
func childDidFinish(child: Coordinator) {
// 자식 코디네이터 제거 및 정리 작업
children.removeAll { $0 === child }
}
}
이렇게 부모 코디네이터를 사용하면 코디네이터 간의 관계를 명확히 하고, 복잡한 네비게이션 흐름을 더 쉽게 관리할 수 있다.
2. AppCoordinator
import UIKit
final class AppCoordinator: Coordinator {
weak var parentCoordinator: Coordinator?
var children: [Coordinator] = []
var navigationController: UINavigationController
func start() {
print("앱 코디네이터 시작")
connectMainViewControllerFlow()
}
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
deinit {
print("앱 코디네이터 해제")
}
}
extension AppCoordinator {
func connectMainViewControllerFlow() {
let mainCoordinator = MainCoordinator(navigationController: navigationController)
mainCoordinator.parentCoordinator = self
children.append(mainCoordinator)
mainCoordinator.start()
}
}
AppCoordinator는 코디네이터 패턴에서 가장 상위에 위치한 코디네이터로, 일반적으로 앱의 흐름을 관리하는 역할을 한다.
1. 최상위 코디네이터 : 앱의 루트 코디네이터 역할을 하며, 다른 모든 코디네이터의 부모 코디네이터가 된다.
2. 앱 시작점 : 앱이 시작될 때 AppDelegate나 SceneDelegate에서 생성되어 시작된다. 즉, 앱의 초기 화면을 설정하고 시작한다.
3. 주요 흐름 관리 : 로그인/로그아웃, 메인 탭바, 온보딩 등 앱의 주요 흐름을 제어한다.
4. 전역적인 네비게이션 결정 : 앱 전체에 영향을 미치는 네비게이션 결정을 담당한다. 예를 들어, 사용자 세션 만료 시 로그인 화면으로 강제 이동 등이 있다.
5. 의존성 주입 : 앱 전체에서 사용되는 주요 서비스나 매니저들을 하위 코디네이터에 주입할 수 있다.
3. 구체적인 Coordinator 클래스
import UIKit
final class MainCoordinator: Coordinator {
weak var parentCoordinator: Coordinator?
var children: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
print("메인 코디네이터 시작")
goToMainViewController()
}
func goToMainViewController() {
let vc = MainViewController()
navigationController.pushViewController(vc, animated: true)
}
}
위의 예제와 같이 구체적인 코디네이터 클래스 내부에서는 start 메소드와 (이동할 자식이 있다면)다음 자식 화면으로 이동하는 메소드를 구현해준다.
'iOS > Design Pattern' 카테고리의 다른 글
[iOS/Design Pattern] 간단한 예제로 Clean Architecture를 설계해보자. (0) | 2024.08.27 |
---|---|
[iOS/Design Pattern] Clean Architecture를 알아보자. (0) | 2024.08.26 |
[iOS/Design Pattern] MVVM에 대한 나의 고찰 (0) | 2024.05.01 |
[iOS/Design Pattern] MVVM 패턴 이해해보기 (0) | 2024.01.05 |
[iOS/Design Pattern] 예제로 알아보는 MVVM패턴 - 2 (0) | 2023.08.08 |