ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 마이크로피처 아키텍처란? (µFeatures Architecture)
    iOS Dev 2023. 7. 15. 02:08
    반응형

    시작하기에 앞서, 이 글은 tuist의 µFeatures Architecture를 요약, 번역, 정리한 글임을 알림. iOS에 편향되어있음.

    µFeatures Architecture | Tuist Documentation

    based on MicroServices

    주의할 점

    이 아키텍처는 silver-bullet이 아님. 모든 프로젝트에 적용하려 하지 말고 적절한 곳에 잘 사용하자.

    마이크로피처 아키텍처란?

    마이크로피처 아키텍처는 확장성을 높이고 빌드 및 테스트 주기를 최적화한다.

    명확하고 간결한 API를 사용하여 상호 연결된 독립적인 기능 구축.

    마이크로 피처는 다음 5개의 타겟의 조합으로 이루어져 있다.

    • Source : Feature 소스 코드와 이에 대한 resource (image, font, storyboard, xib 등)을 포함.
    • Interface : public interface와 Feature의 모델을 포함.
    • Tests : Feature에 대해 unit test와 통합 test를 포함.
    • Testing : 테스트에 사용할 수 있는 테스트 데이터와 Example 앱에서 사용할 수 있는 데이터를 제공함. 다른 기능에서 사용할 수 있는 마이크로피처 클래스와 프로토콜에 대한 Mock을 제공.
    • Example : 특정 상황에서 (다양한 언어, 스크린 사이즈, 세팅 등) 개발자들이 Feature를 실행시켜볼 수 있는 타겟.

     

    보통 다음 의존성을 가진다. :

    • FeatureInterface는 해당 Feature의 public interface와 Model을 갖고있다.
    • Feature는 FeatureInterface의 구현을 제공한다.
    • FeatureTesting은 FeatureInterface에 포함된 Model과 Interface에 대한 테스트 데이터와 Mock을 포함하고 있다.
    • FeatureTests는 Feature에 대한 테스트 대상과 테스트 클래스에서 사용할 수 있는 테스트 데이터가 포함되어 있음.
    • FeatureExample : FeatureTesting에 정의되어 있는 테스트 데이터를 갖고 오고, Feature에 구현한 클래스를 인스턴스화 함.

    Types of µFeatures

    Foundation

    Foundation µFeatures는 다른 MicroFeature를 구축하기 위해 결합되는 wrapper, extension 등과 같은 기본 툴을 포함한다.

    예를 들면,

    • µUI: 사용자 인터페이스 레이아웃을 구축하는 데 사용되는 사용자 정의 뷰, UIKit 확장, 글꼴 및 색상을 제공
    • µTesting: XCTest 확장 및 사용자 정의 어서션을 제공하여 테스트를 용이하게 함
    • µCore: 앱의 기반이라고 볼 수 있으며, 분석 리포터, 로거, API 클라이언트 또는 저장소 클래스와 같은 도구를 제공.

    실제로 foundation µFeatures는 XCTest, Foundation 또는 UIKit과 같은 플랫폼 프레임워크의 인터페이스(구조체, 클래스, 열거형) 및 확장을 노출함.

    주의 할 점 : Foundation µFeatures는 전역적으로 사용 가능한 변수들을 갖지 않는다. 이러한 foundation 종속성의 생명주기를 제어하는 것은 앱의 역할이며, 의존성 주입을 사용하여 다른 µFeature에 전달해야 한다.

    Product

    Product µFeatures는 사용자가 실제로 체감하고 상호 작용할 수 있는 기능을 포함함. 이러한 기능은 foundation µFeatures를 결합하여 구축됨.

    예를 들면,

    • µSearch: 플랫폼에서 콘텐츠를 검색할 수 있는 제품 검색 기능을 포함.
    • µPayments: 결제 흐름을 처리하는 비즈니스 로직과 사용자를 프리미엄 요금제로 업그레이드하기 위한 업셀 화면을 포함.
    • µHome: 가장 최신의 플랫폼 콘텐츠가 표시되는 product 홈 화면을 포함.

    팁 ) Product Domain (영역) : Product µFeature는 일반적으로 제품의 기능을 나타냄.

    실제로, Product µFeature는 View와 Service를 노출함. 다음 section에서는 app target이 이 View와 Service를 활용해서 앱을 빌드하는지 설명함.

    Dependencies between µFeatures

    µFeature가 다른 µFeature에 종속되어 있는 경우, 해당 µFeature는 Interface 대상에 대한 종속성을 선언함. 이렇게 하면 다음 두가지 이점이 있음.

    • 하나의 µFeature 구현이 다른 µFeature의 구현과 결합되는 것을 방지함.
    • 빌드를 가속화 할 수 있음.

    ⇒ Feature별로 Interface를 따로 두고, A Feature에서 B Feature를 사용하고 싶다면 B Interface에 종속성을 두고, DI를 통해 주입받는다.

    (그림 출처 : https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets)

    Hooking µFeatures

    µFeature는 인스턴스를 노출시키지 않는다. 앱에만 인스턴스를 생성하고 사용하는 데 책임이 있다. µFeature의 인스턴스화와 연결 방법은 µFeature의 유형에 따라 다르다.

    Services

    App에는 보통 앱 라이프사이클에 상태가 종속된 서비스나 유틸을 갖고 있다. 이 인스턴스들은 전역적으로 관리되고, 다른 feature들이 전부 접근 가능하다.

    // Services.swift in the main application
    import uCore
    import uPlayback
    
    class Services {
        static let playback = PlaybackService() // From uPlayback
        static let client = Client(baseUrl: "https://api.tuist.io") // From uCore
        static let analytics = Analytics(firebaseKey: "xxx") // From uCore
    }

    위의 예시에서 Services 클래스의 모든 프로퍼티들이 전부 static으로 선언되어있다. 이 중 일부는 app의 라이프사이클에 대해 알아야 할 수도 있다. Notification Center를 통해 관리할 수도 있지만 문제가 발생할 수도 있다. 따라서 이럴 때 AppDelegate를 통해 명시적으로 라이프사이클 이벤트에 대한 알림을 전달한다.

    // AppDelegate.swift
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        func applicationDidBecomeActive(_ application: UIApplication) {
            Services.playback.restoreState()
        }
    
    }

    Views/ViewControllers

    Product µFeatures는 View와 ViewController를 노출할 수 있다. 일반적으로 상태에 따라 뷰가 바뀌게끔 캡슐화하고, 유저 액션에 따라 상태를 업데이트 한다.

    // Home.swift in uHome
    import UIKit
    import uCore
    
    public class Home {
        let client: Client
        public init(client: Client) {
            self.client = client
        }
        public func makeViewController(delegate: HomeDelegate) -> UIViewController {
            return HomeViewController(client: client, delegate: delegate)
        }
    }

    위 예시는 Home µFeature가 어떻게 생겼는지 보여준다. 초기화 할 때 의존성을 주입시켜주며, ViewController를 만들 때 HomeViewController가 아닌 UIViewController를 리턴한다. 이 과정을 통해 앱은 구현 세부 정보로부터 추상화된다.

    Delegating navigation

    위에서 ViewController를 인스턴스화 할 때 delegate를 전달하는 것을 볼 수 있었다. 이 delegate는 다른 µFeature로의 탐색을 트리거하는 동작에 응답한다. 이 것은 앱에서 정의하는 것이고, Coordinator 패턴을 사용하면 잘 작동하게 만들 수 있다.

    Dependencies

    µFeature 기반 개발을 시작하다보면, 모든 feature의 의존성을 app이 주입해주어야 한다는 것을 깨닫는다. 하지만 이렇게 주입하다보면 파라미터의 개수가 매우 많아진다는 것을 깨닫는다. 그 대신 프로토콜을 활용해서 µFeature의 종속성을 나타내고 쉽게 전달할 수 있다.

    public protocol BaseDependencies {
        func makeClient() -> Client
        func makeLogger() -> Logger
    }

    위의 예시를 보면 BaseDependencies 프로토콜은 앱 전체에서 사용할 의존성들을 정의해두었다.

    class AppDependencies: BaseDependencies {
        func makeClient() -> Client {
            return Services.client
        }
        func makeLogger() -> Logger {
            return Services.logger
        }
    }

    위 예시에서는 AppDependencies에 전에 정의해두었던 BaseDependencies를 채택한 후 세부 사항을 구현했다.

    public protocol SearchDependencies: BaseDependencies {
        func makeAnalytics() -> Analytics
    } 

    추가적인 의존성을 가지려면 새로운 프로토콜을 생성해야 한다. 위 예시에선 BaseDependencies에 새로운 의존성을 추가하고 SearchDependencies로 명명했다.

    public final class SearchBuilder {
    
        private let dependenciesSolver: SearchDependencies
    
        public init(dependenciesSolver: SearchDependencies) {
            self.dependenciesSolver = dependenciesSolver
        }
    
        public func makeViewController() -> UIViewController {
            let client = dependenciesSolver.makeClient()
            let logger = dependenciesSolver.makeLogger()
            let analytics = dependenciesSolver.makeAnalytics()
            return SearchViewController(client: client, logger: logger, analytics: analytics)
        }
    }
    
    // From the app
    let searchBuilder = SearchBuilder(dependenciesSolver: AppDependencies())

    위 예제는 builder를 통해 의존성 주입하는 과정을 보여준다.

    (+ 추가 : 코드가 잘 돌아가게 하기 위해서는 AppDependencies가 SearchDependencies 프로토콜을 채택하게 하면 된다.)

    Choosing the target product

    모듈화된 앱을 아키텍처링 할 때 타겟을 프레임워크로 할건지, 라이브러리로 할건지, 또는 이것들을 static으로 할건지 dynamic으로 할 건지에 대한 의문이 든다. tuist 적용 전에는 많은 것을 생각했어야 했다.

    • 타겟이 리소스를 포함하는가?
    • 정적 타겟에 의존하고 있을 때 중복된 심볼 문제가 있는가?
    • 시작할 때 연결되어야 할 다이나믹 타겟의 개수가 앱 시작까지 시간에 영향을 주는가?

    Tuist를 도입하면 위의 의사 결정 프로세스가 매우 단축된다.

    Tuist에서는 라이브러리에서 리소스를 정의할 수 있기 때문에 웬만하면 static libraries (정적 라이브러리)를 채택하는 것을 추천한다. Dynamic Framework (동적 프레임워크)가 더 적합한 상황 (예를 들면 동적 업데이트, 독립 배포, 동적 라이브러리 로딩 등)이 아니면 정적 라이브러리 타겟을 생성하자.

    반응형

    댓글

Designed by Tistory.