iOS

[Tuist] Interface Module을 사용하여 domain을 여러 Features로 분리하기

Duno 2023. 3. 14. 02:25

현재 진행 중인 프로젝트에서 Feature가 단일화된 상태에서 Scene Flow를 기준으로 하여 여러 Features로 분리하는 작업 중에 있습니다.

graph

아래의 아이디어를 기준으로 구현했습니다.

  1. 앱 구현 특성 상 서로 다른 Feature의 명세가 필요할 때가 있습니다. 이때 구현 모듈이 서로를 직접 참조하게 되면 순환 참조가 생길 수도 있고, 모듈 간의 결합도도 강해집니다. 따라서 구현 모듈 간에 직접 의존하지 않게 만들기 위해 Interface 모듈을 도입했습니다.
  2. Interface 모듈 내부에는 ViewController를 추상화 할 수 있는 Protocol을 정의하여, 다른 Feature에서 참조하여 viewController를 생성할 수 있도록 하고자 했습니다.
  3. 추상화된 인터페이스와 의존성의 조립은 App Module에서 이루어집니다.

 

BaseFeatureDependency

ViewController를 가져올 수 있는 protocol을 작성합니다.

import UIKit

public protocol ViewRepresentable {
    var viewController: UIViewController { get }
}

public extension ViewRepresentable where Self: UIViewController {
    var viewController: UIViewController {
        return self
    }
}

 
Main Feature

메인 화면입니다. Main 화면에서는 Auth 화면으로 전환이 가능합니다.
 
Interface
ViewRepresentable을 채택하는 MainViewRepresentable을 정의합니다.
MainView가 가져야 하는 다른 속성도 함께 정의 가능합니다. 현재는 ViewController의 추상화와 생성에만 집중했습니다.
makeMainView에 값 전달이 필요한 경우 parameter를 사용 가능합니다.

import BaseFeatureDependency

public protocol MainViewRepresentable: ViewRepresentable { }

public protocol MainViewBuildable {
    func makeMainView() -> MainViewRepresentable
}

 
Target
MainViewRepresentable을 채택한 구현체를 만들어주고, AuthViewBuildable의 의존성으로 authView 생성하도록 합니다.

import UIKit

import MainFeatureInterface
import AuthFeatureInterface

public final
class MainViewController: UIViewController, MainViewRepresentable {
    public var factory: AuthViewBuildable!
}

extension MainViewController {
    func presentAuthView() {
        let authView = factory.makeAuthView().viewController
        self.present(authView, animated: true)
    }
}

AuthFeature

Interface

import BaseFeatureDependency

public protocol AuthViewRepresentable: ViewRepresentable { }

public protocol AuthViewBuildable {
    func makeAuthView() -> AuthViewRepresentable
}

Target

import UIKit

import AuthFeatureInterface

public final
class AuthViewControlelr: UIViewController, AuthViewRepresentable { }

App

DIContainer
의존성을 관리할 Container를 만들어 줍니다.

import AuthFeatureInterface
import AuthFeature
import MainFeatureInterface
import MainFeature

typealias AppBuildable = AuthViewBuildable & MainViewBuildable

final class DIContainer {

}

extension DIContainer: AppBuildable {
    func makeAuthView() -> AuthViewRepresentable {
        let authViewController = AuthViewControlelr()
        authViewController.view.backgroundColor = .blue
        return authViewController
    }

    func makeMainView() -> MainViewRepresentable {
        let mainViewController = MainViewController()
        mainViewController.factory = self
        mainViewController.view.backgroundColor = .gray
        return mainViewController
    }
}

SceneDelegate
SceneDelegate에서 DIContainer를 사용하여 rootView를 생성해 줍니다.

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    var container = DIContainer()

    func scene(_ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }

        window = UIWindow(frame: scene.coordinateSpace.bounds)
        window?.windowScene = scene
        let rootVC = container.makeMainView().viewController
        window?.rootViewController = UINavigationController(rootViewController: rootVC)
        window?.makeKeyAndVisible()
    }

    func sceneDidDisconnect(_ scene: UIScene) {}

    func sceneDidBecomeActive(_ scene: UIScene) {}

    func sceneWillResignActive(_ scene: UIScene) {}

    func sceneWillEnterForeground(_ scene: UIScene) {}

    func sceneDidEnterBackground(_ scene: UIScene) {}
}

결과

- Features 간에는 서로의 세부 사항을 알지 못하면서도, 추상화된 계층에 의존하여 응집도가 높은 모듈을 구현했습니다.
- Features 별로 독립적인 개발이 가능해졌습니다.
- 의존성 주입의 책임을 최상위 모듈에게 넘겨, testable한 코드 작성이 가능해집니다.