Flyleaf는 Coordinator를 통해 화면 전환을 처리하고 있고, 모든 화면 전환을 AppCoordinator에서 관리하는 구조를 선택했습니다. 이 선택은 나름 자연스러운(?) 저의 사고였는데요.. 앱의 진입점이기도 하고, 전체 흐름을 한 곳에서 관리하는 것이 직관적이라고 판단했기 때문입니다.
하지만 개발을 진행하면서 가장 많은 고민이 있었는데요..! 바로 AppCoordinator의 역할이 점점 비대해지고 있다는 점이었습니다.
모든 흐름이 AppCoordinator에 몰리기 시작했다
초기에는 구조가 단순했습니다.
로그인 -> 메인 화면
메인 -> 특정 화면 이동
정도의 흐름만 있었기 때문에 AppCoordinator 하나로 충분했습니다. 하지만 기능이 추가되면서 상황이 조금 달라졌는데요.
- 위시리스트 등록 플로우
- 체크인 플로우
- 히스토리 등록 및 상세 조회 플로우
- 여행 생성 플로우
- 검색 플로우
와 같은 모든 흐름들이 결국 AppCoordinator로 모이게 되었고, 결과적으로 AppCoordinator는 단순한 진입점 Coordinator가 아니라, 모든 피쳐의 네비게이션을 직접 관리하는 거대한 객체가 되어버렸습니다.
코드도 점점 비대해졌다
이 구조에서 AppCoordinator 내부에 메소드가 계속 늘어난 이유는, 단순히 화면이 많아졌기 때문만은 아니었습니다.
제 구조에서는 각 피쳐가 자신의 화면을 직접 push 하거나 다른 피쳐를 직접 생성하지 않도록 설계했습니다. 대신 VC는 사용자 액션을 클로저를 통해 외부로 전달하고, 실제 네비게이션은 Coordinator가 담당하는 구조를 사용하고 있었습니다.
예를 들어, 어떤 화면에서
- 뒤로가기 버튼을 누르거나
- 검색 버튼을 누르거나
- 티켓 생성을 완료하거나
등과 같은 이벤트가 발생하면, 뷰컨은 이를 onTapBack과 같은 클로저로 외부에 전달합니다. 그리고 이 이벤트를 받아 실제로 어떤 화면을 띄울지 결정하는 역할을 AppCoordinator가 맡고 있었습니다.
문제는, 이 구조가 모든 피쳐에서 동일하게 반복되면서 결과적으로 AppCoordinator에 모든 흐름 제어 로직이 누적되기 시작되었다는 점이었습니다.
예를 들면,
- 위시리시트 등록 화면에서 도서 검색을 누르면 검색 화면을 띄워야 하고
- 검색 결과를 다시 위시리스트 등록 화면으로 전달해야 하고
- 이후 공항 선택 화면으로 이어지고
- 마지막에는 티켓 생성 화면으로 이동해야 했습니다.
이 모든 흐름을 앱코디네이터가 직접 알고 있어야 했기 때문에, 자연스럽게 아래와 같은 메소드들이 계속 추가될 수밖에 없었습니다.
func showRegisterWishlist() { ... }
func showWishTicket(...) { ... }
func showCheckInWishTicket(...) { ... }
func showRegisterHistory() { ... }
func showDetailHistory(...) { ... }
func showRegisterJourney() { ... }
func showJourneyTicket(...) { ... }
func showBookSearch() { ... }
func showDepartureAirportSearch() { ... }
func showArrivalAirportSearch() { ... }
즉, 이 메소드들은 단순히 화면 수가 많아서 생긴 것이 아니라, ViewController에서는 이벤트만 전달하고, 실제 흐름은 모두 AppCoordinator가 결정한다는 현재 구조에서 자연스럽게 AppCoordinator 안에 모이게 된 코드들이었습니다. 처음에는 이 방식이 명확해 보였지만, 기능과 화면이 늘어날수록 AppCoordinator는 앱 전체 흐름뿐 아니라 각 피쳐 내부의 세부 플로우까지 모두 알고 있어야 하는 구조가 되었고, 그 결과 코드도 점점 비대해지게 되었습니다.
각 피쳐가 자신의 흐름을 관리하는 Coordinator를 갖자
정말 이 흐름들을 AppCoordinator가 모두 알고 있어야 할까? 라는 근본적인 질문을 던졌습니다.
조금 더 생각해보면, 위시리스트 등록, 여정 생성, 히스토리 조회와 같은 플로우들은 각각 하나의 독립된 기능 흐름이라고 볼 수 있습니다.
그렇다면 이 흐름들은 AppCoordinator가 아니라 각 피쳐 내부에서 스스로 관리하는 것이 더 자연스럽지 않을까라는 생각이 들었습니다.
결국 문제의 본질은,
- AppCoordinator가 앱 전체 흐름 + Feature 내부 흐름 모두 담당하고 있다.
- 그로 인해 책임이 과도하게 집중되고 있다.
- 기능이 추가될수록 구조가 계속 무거워질 수 밖에 없다.
그래서 이 문제를 해결하기 위해 흐름을 기준으로 책임을 다시 나누기로 했습니다.
기존에는 AppCoordinator 하나가 모든 화면 전환을 관리하는 구조였다면, 리팩토링 후에는 각 피쳐가 자신의 흐름을 관리하는 Coordinator를 가지는 구조로 개선하기로 했습니다. 이렇게 분리하게 되면 각 피쳐의 흐름은 해당 코디네이터 안에서만 관리되고 AppCoordinator는 다시 앱 전체 흐름을 조립하는 역할을 하게 됩니다.
결과적으로 책임의 경계를 명확하게 나누고, 구조를 확장해 유리한 형태로 개선할 수 있게 됩니다.
1. Coordinator를 어디에 둘 것인가
Feature 단위로 코디네이터를 분리하기로 결정한 이후, 다음으로 고민했던 지점은 이 Feature별 Coordiantor를 어디에 위치시킬 것인가였습니다.
제가 생각한 선택지는 크게 두 가지였습니다.
1. App 모듈에 모든 Coordinator를 모아두는 방식
2. 각 Feature 모듈 내부에 Coordiantor를 두는 방식
먼저 Feature 모듈 내부에 Coordinator를 두는 구조를 고려했습니다. 이 방식은 Feature 내부 흐름을 완전히 캡슐화할 수 있고, 모듈 단위 책임이 명확하게 나뉘는 장점이 있습니다. 하지만 이 구조를 적용하려면 Feature 모듈이 네비게이션 컨트롤러를 직접 다루거나, 상위 흐름과의 연결을 위한 인터페이스 설계가 필요했습니다. 또한 Feature 간 흐름 연결이 많아질수록 모듈 간 의존 관계를 더 신경 써야 하는 부담이 있었습니다.
반면 App 모듈에 두는 구조는 navigaiton 흐름을 한 곳에서 관리할 수 있고, 기존 구조를 크게 변경하지 않으면서도 Feature 단위로 책임을 나누는 것이 가능했습니다.
즉, Feature의 VC는 그대로 두면서, 흐름 제어만 Coordinator로 분리하는 점진적인 리팩토링이 가능했습니다. 기존 구조를 크게 깨지 않고 적용할 수 있고, 네비게이션 흐름을 App 모듈 한 곳에서 관리가 가능하다는 점에서 결국 App 모듈에 모든 코디네이터를 모아두는 방식을 선택했습니다.
2. Feature Coordinator 도입
구조를 결정한 이후, 각 Feature에 Coordinator를 도입하기 시작했습니다.
예를 들어 Wishlist 피쳐를 기준으로 보면,
기존에는 AppCoordinator가 아래와 같은 흐름을 직접 처리하고 있었습니다.
- 위시리스트 등록 화면 진입
- 도서 검색 화면 이동
- 공항 선택 화면 이동
- 티켓 생성 화면 이동
이제 이 흐름을 WishlistCoordinator로 이동시켰습니다.
final class WishlistCoordinator: Coordinator {
weak var parentCoordinator: Coordinator?
var childCoordinators: [Coordinator] = []
let navigationController: UINavigationController
func start() {
showRegisterWishlist()
}
private func showRegisterWishlist() {
// 등록 화면 진입
}
private func showWishTicket(...) {
// 티켓 생성 화면
}
private func startBookSearch(...) {
// 검색 플로우
}
}
기존에는 아래와 같이 AppCoordinator가 직접 화면을 생성하고 push 했다면,
func showRegisterWishlist() {
let vc = registerWishlistBuilder.build(...)
navigationController.pushViewController(vc, animated: true)
}
이제는 Feature Coordinator를 생성하고 시작만 하도록 변경되었습니다.
func startWishlistFlow() {
let coordinator = WishlistCoordinator(
navigationController: navigationController,
...
)
coordinator.parentCoordinator = self
childCoordinators.append(coordinator)
coordinator.start()
}
이 리팩토링을 통해 AppCoordinator의 코드량이 줄어들고, 피쳐 단위의 흐름이 명확하게 분리되었습니다.
아직 끝나지 않았다?
하지만 또 하나의 문제가 남아있었는데요.
Feature Coordinator로 분리했음에도 불구하고, 여전히 ViewController와 Coordinator 사이의 이벤트 전달 구조가 복잡하다는 점이었습니다.
특히 여러 개의 클로저 기반 콜백은 여전히 코드 가독성과 확장성에 부담이 되고 있었습니다.
ViewController에서
- 뒤로가기 버튼을 눌렀을 때
- 도서 검색을 눌렀을 때
- 출발 공항을 선택하려고 할 때
- 도착 공항을 선택하려고 할 때
- 티켓 생성 버튼을 눌렀을 때
이런 액션들을 각각 별도의 클로저로 외부에 전달하는 구조였습니다. 문제는, 이 방식이 이벤트 수가 적을 때는 괜찮지만 기능이 조금만 복잡해져도 금방 부담스러워진다는 점이었습니다.
예를 들어 RegisterWishlistViewController는 아래와 같이 여러 개의 클로저를 가지고 있었습니다.
public var onTapBack: (() -> Void)?
public var onTapRegisterBookSearch: ((@escaping (BookInfo) -> Void) -> Void)?
public var onTapSelectDepartureButton: ((@escaping (AirportInfo) -> Void) -> Void)?
public var onTapSelectDestinationButton: ((@escaping (AirportInfo) -> Void) -> Void)?
public var onTapCreateTicket: ((BookInfo, AirportInfo, AirportInfo, String) -> Void)?
이벤트 하나하나를 모두 개별 클로저로 분리하다 보니, ViewController의 프로퍼티도 점점 많아지고 있었습니다.
이 구조는 Builder 시그니처도 함께 복잡하게 만들었습니다.
public protocol RegisterWishlistBuildable {
func build(
onTapBack: (() -> Void)?,
onTapRegisterBookSearch: ((@escaping (BookInfo) -> Void) -> Void)?,
onTapSelectDepartureButton: ((@escaping (AirportInfo) -> Void) -> Void)?,
onTapSelectDestinationButton: ((@escaping (AirportInfo) -> Void) -> Void)?,
onTapCreateTicket: ((BookInfo, AirportInfo, AirportInfo, String) -> Void)?
) -> UIViewController
}
즉, ViewController에서 이벤트가 늘어날수록 Builder의 시그니처도 함께 비대해지는 구조였습니다.
Coordinator에서 Builder를 호출하는 코드 역시 자연스럽게 복잡해질 수밖에 없었습니다.
let registerWishlistVC = registerWishlistBuilder.build(
onTapBack: { [weak self] in
self?.finishFlow()
},
onTapRegisterBookSearch: { [weak self] onSelected in
self?.startBookSearch(onSelected: onSelected)
},
onTapSelectDepartureButton: { [weak self] onSelected in
self?.startDepartureAirportSearch(onSelected: onSelected)
},
onTapSelectDestinationButton: { [weak self] onSelected in
self?.startArrivalAirportSearch(onSelected: onSelected)
},
onTapCreateTicket: { [weak self] book, departure, destination, reason in
self?.showWishTicket(
book: book,
departure: departure,
destination: destination,
reason: reason
)
}
)
처음에는 괜찮아 보였지만, 이런 패턴이 피쳐마다 반복되면서 점점 이벤트 전달 구조 자체가 확장에 불리하다는 느낌이 강하게 들었습니다.
그래서 Route를 도입했다.
이 문제를 해결하기 위해, 여러 개로 흩어져 있던 클로저를 하나의 Route enum으로 통합하기로 했습니다.
예를 들어 RegisterWishlist의 경우 이벤트를 아래와 같이 하나의 enum으로 표현했습니다.
public enum RegisterWishlistRoute {
case back
case bookSearch((BookInfo) -> Void)
case departureSearch((AirportInfo) -> Void)
case destinationSearch((AirportInfo) -> Void)
case createTicket(BookInfo, AirportInfo, AirportInfo, String)
}
그리고 빌더의 시그니처도 아래와 같이 단순해졌습니다.
public protocol RegisterWishlistBuildable {
func build(
onRoute: ((RegisterWishlistRoute) -> Void)?
) -> UIViewController
}
이제 ViewController는 여러 개의 클로저를 따로 가지고 있을 필요 없이, 하나의 onRoute만 외부로 전달하면 됩니다.
public var onRoute: ((RegisterWishlistRoute) -> Void)?
이렇게 리팩토링하고 나니 구조가 훨씬 명확해졌습니다.
1. VC는 어떤 이벤트가 발생했는지만 전달
2. Coordinator는 그 이벤트를 받아 실제 흐름을 결정
3. Builder는 길고 복잡한 시그니처 대신 단일 Route만 주입
확장에도 유리하다.
Route를 도입하면서 크게 느낀 장점 중 하나는 기능 추가 시 변경 범위가 훨씬 줄어든다는 점이었습니다.
기존 클로저 기반 구조에서는 새로운 이벤트가 하나 추가될 때마다 아래와 같은 작업이 필요했습니다.
예를 들어 "임시 저장(Draft 저장)" 기능을 추가한다고 가정해봅시다.
1. ViewController에 새로운 클로저 추가
public var onTapSaveDraft: (() -> Void)?
2. Builder 시그니처 수정
func build(
onTapBack: (() -> Void)?,
...
onTapSaveDraft: (() -> Void)? // 추가
) -> UIViewController
3. Builder 구현부 수정
public final class RegisterWishlistBuilder: RegisterWishlistBuildable {
public init() {}
public func build(
onTapBack: (() -> Void)?,
onTapRegisterBookSearch: ((@escaping (BookInfo) -> Void) -> Void)?,
onTapSelectDepartureButton: ((@escaping (AirportInfo) -> Void) -> Void)?,
onTapSelectDestinationButton: ((@escaping (AirportInfo) -> Void) -> Void)?,
onTapCreateTicket: ((BookInfo, AirportInfo, AirportInfo, String) -> Void)?,
onTapSaveDraft: (() -> Void)?
) -> UIViewController {
let viewModel = RegisterWishlistViewModel()
let viewController = RegisterWishlistViewController(viewModel: viewModel)
viewController.onTapBack = onTapBack
viewController.onTapRegisterBookSearch = onTapRegisterBookSearch
viewController.onTapSelectDepartureButton = onTapSelectDepartureButton
viewController.onTapSelectDestinationButton = onTapSelectDestinationButton
viewController.onTapCreateTicket = onTapCreateTicket
viewController.onTapSaveDraft = onTapSaveDraft
return viewController
}
}
4. Coordinator에서 Builder 호출부 수정
onTapSaveDraft: { [weak self] in
self?.saveDraft()
}
즉, 하나의 이벤트 추가를 위해 여러 레이어를 전부 수정해야 하는 구조였습니다.
하지만 Route 구조에서는 이 변경이 훨씬 단순해집니다.
1. Route enum에 케이스만 추가
enum RegisterWishlistRoute {
case back
case bookSearch((BookInfo) -> Void)
case departureSearch((AirportInfo) -> Void)
case destinationSearch((AirportInfo) -> Void)
case createTicket(BookInfo, AirportInfo, AirportInfo, String)
case saveDraft // 추가
}
2. ViewController에서 이벤트 발생 시 route 호출
onRoute?(.saveDraft)
3. Coordinator에서 switch case 하나만 추가
switch route {
case .saveDraft:
self.saveDraft()
...
}
여기서 중요한 포인트는, 더이상 ViewController에 클로저를 추가하지 않아도 되고, 빌더 시그니처는 전혀 변경되지 않는다는 점입니다. 즉, Route enum + Coordinator의 분기만 수정하면 기능 추가가 완료 됩니다.
여기서 끝이 아니다 (찐막) - Coordinator의 생명주기 관리
Feature Coordinator를 분리하고, Route 기반으로 이벤트 전달 구조까지 정리하고 나니 전체 구조는 많이 깔끔해졌습니다..만! 하나의 문제가 남아 있었습니다. 바로 Coordinator의 생명주기를 언제, 어떻게 정리할 것인가에 대한 문제였습니다.
초기에는 뒤로가기 버튼을 눌렀을 때, 직접 childDidFinish()를 호출해서 Coordinator를 정리하는 방식을 사용했습니다. 하지만 이 방식이 항상 안전하지 않을 수 있다는 점을 코디네이터와 라우트 관련된 레퍼런스를 재조사 하면서 알게 되었습니다.
예를 들면,
func finishFlow() {
navigationController.popViewController(animated: true)
parentCoordinator?.childDidFinish(self)
}
직접 흐름을 종료하면서, 해당 코디네이터를 상위 코디네이터의 childCoordinators 배열에서 제거하는 방식이었습니다.
하지만 이 방식에는 한 가지 문제가 있었습니다. iOS의 화면 전환은 항상 코드로만 발생하지 않습니다. 네비게이션 바의 뒤로가기 버튼, 백 스와이프 처럼 사용자 제스처로도 화면이 pop될 수 있기 때문입니다. 이 경우에는 ViewController가 직접 finishFlow()를 호출하지 않기 때문에, 화면은 사라졌지만 Coordinator는 여전히 childCoordinators에 남아 있을 수 있습니다.
즉, 화면은 pop되었지만, Coordinator가 정리되지 않는 상황이 생길 수 있었고, 이는 결국 Coordinator의 생명주기와 실제 네비게이션 스택 상태가 어긋나는 문제로 이어질 수 있었습니다.
UINavigationControllerDelegate를 사용했습니다
iOS Swift Coordinator pattern and back button of Navigation Controller
I am using pattern MVVM+Coordinator. Every my controllers are created by coordinators. But what is the correct way to stop my coordinators when tapping on back button of Navigation Controller? class
stackoverflow.com
Coordinator & MVVM - Clean Navigation and Back Button in Swift
After introducing how to implement Coordinator pattern with an MVVM structure, it feels natural for me to go further and cover some of the blank spots of Coordinator and how to fix along the way.
benoitpasquier.com
iOS에서 Coordinator 패턴을 사용할 때 언급되는 문제 중 하나는 네비게이션 뒤로가기 시 Coordiantor를 어떻게 정리할 것인가 입니다.
위에서 이야기 했지만, 백버튼이나 스와이프로 화면이 pop되는 경우, Coordinator는 이를 직접 감지할 수 없기 때문에 생명주기 관리가 어긋날 수 있습니다.
이 문제에 대한 대표적인 해결 방법으로 여러 레퍼런스에서 공통적으로 제시하는 방법이 UINavigationControllerDelegate를 활용하여 실제 pop이 발생한 시점을 감시하는 방법입니다.
핵심은 아래와 같습니다.
func navigationController(
_ navigationController: UINavigationController,
didShow viewController: UIViewController,
animated: Bool
) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
removeFinishedCoordinator(
from: childCoordinators,
poppedViewController: fromViewController
)
}
이 로직은 다음과 같은 방식으로 동작합니다.
1. 화면 전환이 완려된 뒤 didShow가 호출된다.
2. 전환 직전의 fromViewController를 확인한다.
3. 만약 그 VC가 네비게이션 스택에 더 이상 존재하지 않는다면, 실제로 pop 된 것으로 판단한다.
4. 그 VC를 root로 가지는 Coordinator를 찾아 제거한다.
이 방식을 사용하기 위해 각 Coordinator는 자신이 시작한 화면을 rootViewController로 보관하도록 했습니다.
예를 들어,
final class WishlistCoordinator: Coordinator {
var rootViewController: UIViewController?
func start() {
let registerWishlistVC = registerWishlistBuilder.build(...)
rootViewController = registerWishlistVC
navigationController.pushViewController(registerWishlistVC, animated: true)
}
}
이렇게 하면 navigation stack에서 어떤 ViewController가 사라졌는지 기준으로 어떤 Coordinator를 정리해야 하는지 판단할 수 있습니다.
아직 뭔가 아쉽다.. (DI Container?)
Feature Coordinator로 흐름을 분리하고, Route 기반으로 이벤트 전달 구조까지 정리하면서 전체 구조는 이전보다 훨씬 명확해졌습니다.
하지만 리팩토링을 진행하고 나서도 한 가지 아쉬움은 계속 남아 있습니다.
바로 AppCoordinator가 여전히 너무 많은 의존성을 알고 있다는 점이었습니다.
private let authService: AuthServicing
private let homeBuilder: HomeBuildable
private let loginBuilder: LoginBuildable
private let searchBuilder: SearchBuildable
private let wishlistBuilder: WishlistBuildable
private let checkInWishTicketBuilder: CheckInWishTicketBuildable
private let registerWishlistBuilder: RegisterWishlistBuildable
private let wishTicketBuilder: WishTicketBuildable
private let journeyTicketBuilder: JourneyTicketBuildable
private let registerHistoryBuilder: RegisterHistoryBuildable
private let registerJourneyBuilder: RegisterJourneyBuildable
private let jourenyBuilder: JourneyBuildable
private let historyBuilder: HistoryBuildable
private let detailHistoryBuilder: DetailHistoryBuildable
Feature Coordinator로 책임을 나누었음에도 불구하고, AppCoordinator는 여전히 각 Feature에서 필요한 빌더와 서비스를 모두 알고 있어야 했습니다. 즉, 흐름의 책임은 분리되었지만 생성의 책임은 여전히 AppCoordinator를 지나가고 있는 상태였습니다.
객체를 "쓰는 쪽"이 아니라 "전달하는 쪽"이 너무 많다.
현재 구조를 보면, 실제로 어떤 빌더를 사용하는 주체는 가장 아래쪽의 Feature Coordinator인 경우가 많습니다.
예를 들어 SearchBuilder를 실제로 사용하는 것은 SearchCoordinator인데, 생성과 전달의 흐름은 대략 아래와 같습니다.
SceneDelegate -> AppCoordinator -> WishlistCoordinator -> SearchCoordinator
생성 전달 전달 사용
즉, 실제로 사용하는 것은 SearchCoordinator인데, 그 전에 SceneDelegate, AppCoordinator, WishlistCoordinator는 그 객체를 그냥 들고 있다가 전달만 하는 역할을 하고 있는 셈입니다. 이 구조는 동작에는 문제가 없지만, 조금 더 구조적으로 생각해보면 아쉬움이 많이 남아있습니다.
- 실제로 쓰지 않는 객체를 상위 계층이 알고 있어야 하고
- 생성 책임과 사용 책임이 분리되지 않았으며
- 의존성이 아래로 내려갈 수록 "전달만 하는 코드"가 늘어날 수 있기 때문입니다.
DI Container?
이 지점에서 자연스럽게 떠오른 것이 DI Container였습니다. DI Container를 도입하는 핵심 이유는 객체를 "어떻게 생성하는가"와 "어떻게 사용하는가"를 분리하는 데 있다고 생각합니다.
지금 구조에서는
- SceneDelegate가 객체를 생성하고
- AppCoordinator가 이를 전달하고
- 하위 Coordinator가 또 전달하고
- 최종적으로 가장 아래 Coordinator가 실제로 사용합니다.
즉, 객체를 사용하는 쪽이 아니라 중간 계층이 생성 방식과 전달 구조를 모두 알고 있어야 하는 상태입니다.
만약 DI Contaienr를 도입한다면, 이 생성 책임을 하나의 조립 지점으로 모을 수 있습니다.
예를 들면 개념적으로는 아래와 같은 흐름이 될 수있습니다.
DIContainer AppCoordinator -> WishlistCoordinator -> SearchCoordinator
생성 resolve / 사용
이렇게 되면 상위 코디네이터는 모든 빌더를 직접 들고 있을 필요 없이, 정말 필요한 시점에만 필요한 의존성을 꺼내서 사용할 수 있게 됩니다.
즉, 생성 책임은 Container가 맡고, Coordinator는 흐름 제어에 더 집중할 수 있는 구조가 됩니다.
그렇다면 지금 바로 도입해야 할까?
네.. 고민 중입니다. DI Container는 분명 이 문제를 해결하는 좋은 방법이 될 수 있지만, 그 자체로도 또 하나의 구조적 복잡성을 추가하는 선택이기 때문입니다.
현재 프로젝트 규모를 기준으로 생각해보면
- 피쳐의 수가 아주 많은 편은 아님.
- 의존성 그래프가 감당 불가능할 정도로 복잡하지 않음.
- 아직은 수동으로 주입하는 방식으로도 추적가능하고 충분함.
그래서 지금은 "반드시 지금 당장 도입해야 한다"기보다는, 도입 시점을 고민해볼 만한 단계에 가까운 것 같다고 느끼고 있습니다.
- AppCoordinator가 여전히 너무 많은 빌더를 알고 있다.
- 생성 책임과 사용 책임이 완전히 분리되지는 않았다.
- DI Container는 이 문제를 해결할 수 있는 후보가 될 수 있다.
- 하지만 현재 규모에서는 조금 과한 선택이지 않을까?
'Flyleaf - 독서를 여행처럼 > 리팩토링' 카테고리의 다른 글
| [리팩토링] 02. MKTileOverlay에 캐싱을 적용해봅시다. (0) | 2026.03.29 |
|---|