-
[iOS-2] Dependency Container로 의존성 관리하기Project/TalkTalk 2025. 3. 31. 23:01
의존성 주입이란?
의존성 주입(Dependency Injection)은 한 객체가 다른 객체에 의존할 때, 외부에서 해당 의존성을 제공하는 디자인 패턴이며 다음과 같은 이점을 가질 수 있습니다.
- 결합도 감소: 객체가 자신의 의존성을 직접 생성하지 않아 결합도가 낮아집니다.
- 테스트 용이성: 실제 구현체 대신 테스트용 모의 객체를 주입할 수 있습니다.
- 유연성: 런타임에 의존성을 변경할 수 있어 유연한 설계가 가능합니다.
- 코드 재사용성: 의존성이 분리되어 컴포넌트의 재사용이 용이해집니다.
예를 들어 네트워크 통신이 필요한 뷰모델이 있다고 할 때, 이 뷰모델은 네트워크 작업을 하는 객체를 내부에서 직접 생성하지 않고 뷰모델 객체가 생성되는 시점에 외부에서 주입받습니다. 좀더 자세히 설명해 보면 NetworkService라는 프로토콜을 만들고 데이터를 가져오는 메서드를 정의합니다. 그리고 뷰 모델은 이 프로토콜 타입의 프로퍼티로 가지며 이를 초기화 시점에 주입받습니다. 따라서 테스트를 할때는 NetworkService를 구현한 목 객체를 주입하여 사용할 수 있습니다.
// 직접 의존성 생성 class NetworkService { ... } class LoginViewModel { private let network = NetWorkService() ... }
// 외부에서 객체 생성 시점에 의존성 주입 protocol NetworkServiceProtocol { ... } class NetworkService : NetworkServiceProtocol { ... } class LoginViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkerviceProtocol){ self.networkService = networkService } ... } let ns = NetworkService() let vm = LoginViewModel(networkService: ns)
위와 같이 의존성 주입을 활용하면 객체 생성 시점에 의존성을 외부에서 주입하는 형태를 가지며 이는 기존에 객체가 직접 내부에서 의존성을 설정하는 방향과 반대가 되기 때문에 이를 의존성 역전(Dependency Inversion)이라 합니다.
TalkTalk 앱의 의존성 주입 구조
TalkTalk 앱의 의존성 주입 구조는 크게 다음과 같은 컴포넌트로 구성됩니다.
- DependencyContainer: 의존성 등록 및 해결을 담당하는 중앙 관리자
- DependencyRegistrar: 의존성 등록 로직을 모듈화하는 클래스
- 계층화된 의존성 구조: 서비스, 저장소, 리포지토리, 뷰모델 등의 계층
- ViewControllerFactory: 뷰 컨트롤러 생성과 의존성 주입을 담당
이제 각 컴포넌트를 자세히 살펴보겠습니다.
1. DependencyContainer 설계
의존성 컨테이너는 앱 전체의 의존성을 중앙에서 관리하는 핵심 클래스입니다.
final class DependencyContainer { // 싱글턴 인스턴스 저장소 private var singletons: [String: Any] = [:] // 팩토리 등록 저장소 private var factories: [String: Any] = [:] // 타입 등록 (싱글턴) func register<T>(_ type: T.Type, instance: T) { let key = String(describing: type) singletons[key] = instance } // 타입 등록 (팩토리) func register<T>(_ type: T.Type, factory: @escaping () -> T) { let key = String(describing: type) factories[key] = factory } // 매개변수가 있는 타입 등록 func register<T, U>(_ type: T.Type, factory: @escaping (U) -> T) { let key = String(describing: type) + "-with-arg" factories[key] = factory } // 의존성 해결 (싱글턴 우선) func resolve<T>(_ type: T.Type) -> T? { let key = String(describing: type) // 싱글턴이 있으면 반환 if let singleton = singletons[key] as? T { return singleton } // 팩토리로 생성 if let factory = factories[key] as? () -> T { return factory() } return nil } // 매개변수가 있는 타입 해결 func resolve<T, U>(_ type: T.Type, argument: U) -> T? { let key = String(describing: type) + "-with-arg" return (factories[key] as? (U) -> T)?(argument) } }
이 컨테이너는 크게 두 가지 저장소를 가집니다.
- singletons: 앱 전체에서 공유되는 싱글턴 인스턴스를 저장
- factories: 필요할 때마다 새 인스턴스를 생성하는 팩토리 함수(클로저)를 저장
또한 두 가지 주요 기능을 제공합니다.
- register: 의존성 등록 (싱글턴, 팩토리, 매개변수가 있는 팩토리)
- resolve: 등록된 의존성 해결 (싱글톤의 경우 이미 생서된 인스턴스, 이외에는 등록된 클로저를 통해 새로운 인스턴스 만들어서 반환)
2. 계층화된 의존성 구조
TalkTalk 앱은 다음과 같은 계층으로 분리되어 있습니다:
2.1 네트워크 서비스 계층
protocol NetworkService { // 메서드 선언만 } class DefaultNetworkService: NetworkService { // 구현 없음 } protocol WebSocketService { // 메서드 선언만 } class DefaultWebSocketService: WebSocketService { private let networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
네트워크 서비스 계층은 HTTP 통신과 WebSocket 통신을 담당합니다. 주목할 점은 DefaultWebSocketService가 NetworkService에 의존하고 있으며, 이 의존성은 생성자를 통해 주입받고 있습니다.
2.2 저장소 계층 (Store)
actor UserStore { // 프로퍼티 및 메서드 선언만 } actor MessageStore { // 프로퍼티 및 메서드 선언만 } actor ChatStore { // 프로퍼티 및 메서드 선언만 }
저장소 계층은 Swift의 Actor 모델을 사용하여 동시성 안전성을 보장합니다. 이 계층은 앱 내에서 데이터를 일시적으로 저장하고 상태를 관리합니다.
2.3 리포지토리 계층
protocol UserRepository { // 메서드 선언만 } class DefaultUserRepository: UserRepository { private let networkService: NetworkService private let userStore: UserStore init(networkService: NetworkService, userStore: UserStore) { self.networkService = networkService self.userStore = userStore } } protocol ChatRepository { // 메서드 선언만 } class DefaultChatRepository: ChatRepository { private let networkService: NetworkService private let chatStore: ChatStore init(networkService: NetworkService, chatStore: ChatStore) { self.networkService = networkService self.chatStore = chatStore } } protocol MessageRepository { // 메서드 선언만 } class DefaultMessageRepository: MessageRepository { private let networkService: NetworkService private let webSocketService: WebSocketService private let messageStore: MessageStore private let userStore: UserStore init(networkService: NetworkService, webSocketService: WebSocketService, messageStore: MessageStore, userStore: UserStore) { self.networkService = networkService self.webSocketService = webSocketService self.messageStore = messageStore self.userStore = userStore } }
리포지토리 계층은 데이터 접근 로직을 캡슐화하며, 네트워크 서비스와 저장소에 의존합니다. 여기서도 모든 의존성은 생성자 주입 방식을 사용합니다.
2.4 뷰모델 계층
class LoginViewModel { private let userRepository: UserRepository init(userRepository: UserRepository) { self.userRepository = userRepository } } class ChatListViewModel { private let chatRepository: ChatRepository init(chatRepository: ChatRepository) { self.chatRepository = chatRepository } } class ChatDetailViewModel { private let chatRepository: ChatRepository private let messageRepository: MessageRepository init(chatId: String, chatRepository: ChatRepository, messageRepository: MessageRepository) { self.chatRepository = chatRepository self.messageRepository = messageRepository } }
뷰모델 계층은 UI 로직과 상태 관리를 담당하며, 리포지토리에 의존합니다. 이 계층 역시 생성자 주입 방식을 통해 의존성을 받습니다.
3. ViewControllerFactory
ViewControllerFactory는 뷰 컨트롤러 생성과 필요한 뷰모델 주입을 담당합니다.
protocol ViewControllerFactory { func makeLoginViewController() -> LoginViewController func makeRegisterViewController() -> RegisterViewController func makeChatListViewController() -> ChatListViewController func makeChatDetailViewController(chatId: String) -> ChatDetailViewController // 나머지 메서드들... } class DefaultViewControllerFactory: ViewControllerFactory { private let container: DependencyContainer init(container: DependencyContainer) { self.container = container } func makeLoginViewController() -> LoginViewController { let viewModel = container.resolve(LoginViewModel.self)! let vc = LoginViewController() // viewModel 설정 코드는 생략 return vc } // 나머지 메서드들... }
이 팩토리는 DependencyContainer를 통해 필요한 뷰모델을 해결하고, 이를 뷰 컨트롤러에 주입합니다.
4. 의존성 등록
의존성 등록을 관리하기 위해 별도의 DependencyRegistrar 클래스를 사용합니다.
class DependencyRegistrar { private let container: DependencyContainer init(container: DependencyContainer) { self.container = container } func registerDependencies() { registerServices() registerStores() registerRepositories() registerViewModels() registerFactories() } private func registerServices() { // NetworkService container.register(NetworkService.self, instance: DefaultNetworkService()) // WebSocketService container.register(WebSocketService.self, instance: DefaultWebSocketService(networkService: container.resolve(NetworkService.self)!)) } private func registerStores() { // Stores (액터) container.register(UserStore.self, instance: UserStore()) container.register(ChatStore.self, instance: ChatStore()) container.register(MessageStore.self, instance: MessageStore()) } private func registerRepositories() { // Repositories container.register(UserRepository.self, instance: DefaultUserRepository(networkService: container.resolve(NetworkService.self)!, userStore: container.resolve(UserStore.self)!)) container.register(ChatRepository.self, instance: DefaultChatRepository(networkService: container.resolve(NetworkService.self)!, chatStore: container.resolve(ChatStore.self)!)) container.register(MessageRepository.self, instance: DefaultMessageRepository(networkService: container.resolve(NetworkService.self)!, webSocketService: container.resolve(WebSocketService.self)!, messageStore: container.resolve(MessageStore.self)!, userStore: container.resolve(UserStore.self)!)) } private func registerViewModels() { // ViewModels container.register(LoginViewModel.self) { LoginViewModel(userRepository: self.container.resolve(UserRepository.self)!) } container.register(RegisterViewModel.self) { RegisterViewModel(userRepository: self.container.resolve(UserRepository.self)!) } container.register(ChatListViewModel.self) { ChatListViewModel(chatRepository: self.container.resolve(ChatRepository.self)!) } container.register(ChatDetailViewModel.self) { (chatId: String) in ChatDetailViewModel( chatId: chatId, chatRepository: self.container.resolve(ChatRepository.self)!, messageRepository: self.container.resolve(MessageRepository.self)! ) } container.register(ProfileViewModel.self) { ProfileViewModel(userRepository: self.container.resolve(UserRepository.self)!) } container.register(EditProfileViewModel.self) { EditProfileViewModel(userRepository: self.container.resolve(UserRepository.self)!) } } private func registerFactories() { // ViewControllerFactory container.register(ViewControllerFactory.self, instance: DefaultViewControllerFactory(container: self.container)) } }
이 클래스는 등록 로직을 기능별로 분리하여 관리합니다.
5. SceneDelegate에서의 초기화
앱 시작 시 의존성 설정은 SceneDelegate에서 이루어집니다.
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? private var appCoordinator: AppCoordinator? private let container = DependencyContainer() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } // 의존성 등록 setupDependencies() // 윈도우 생성 let window = UIWindow(windowScene: windowScene) self.window = window // 뷰 컨트롤러 팩토리 해결 let viewControllerFactory = container.resolve(ViewControllerFactory.self)! // 앱 코디네이터 생성 및 시작 appCoordinator = AppCoordinator(window: window, viewControllerFactory: viewControllerFactory) appCoordinator?.start() } private func setupDependencies() { let registrar = DependencyRegistrar(container: container) registrar.registerDependencies() } }
6. Coordinator는 ViewControllerFactory를 통해 화면 생성
각 Coordinator의 start( ) 메서드에서 viewControllerFactory를 사용해 현재 화면에 대응하는 Controller를 생성합니다. 이때 viewControllerFactory는 내부에서 DependencyContainer를 통해 Controller 생성에 필요한 의존성을 해결합니다.
class ChatCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController private let viewControllerFactory: ViewControllerFactory init(navigationController: UINavigationController, viewControllerFactory: ViewControllerFactory) { self.navigationController = navigationController self.viewControllerFactory = viewControllerFactory } func start() { let chatListVC = viewControllerFactory.makeChatListViewController() chatListVC.coordinator = self navigationController.setViewControllers([chatListVC], animated: false) } ...
'Project > TalkTalk' 카테고리의 다른 글
[iOS-1] Coordinator 패턴을 사용한 화면 전환 관리 (0) 2025.03.28