The Modular Architecture (TMA)
TMA는 Apple OS 애플리케이션을 구조화 하는 아키텍처 접근 방식이고, 확장성을 가지며, 빌드와 테스트 주기를 최적화 하고, 팀 내에 좋은 개발 방식을 보장합니다. 이것의 핵심은 독립적인 기능을 만들고 명확하고 간결한 API를 통해 서로 연결하여 애플리케이션을 만드는 것입니다.
이 가이드라인은 아키텍처의 원칙을 소개하며, 다른 계층의 애플리케이션 기능을 식별하고 연결하는데 도움을 줍니다. 이 아키텍처를 사용하기로 결정한다면, 도움이 되는 팁, 툴, 그리고 충고도 소개합니다.
µFEATURES\
이 아키텍처는 이전에 µFeatures로 알려진 아키텍처 입니다. 우리는 이 아키텍처의 목적과 원칙이 더 잘 반영되도록 The Modular Architecture (TMA) 로 이름을 변경했습니다.
주요 원칙
개발자는 메인 앱과 독립적으로 기능을 빠르게 빌드, 테스트, 그리고 실행 할 수 있어야 하고, Xcode의 UI 프리뷰, 코드 자동 완성, 그리고 디버깅 기능이 잘 동작해야 합니다.
모듈이란 무엇인가
모듈은 애플리케이션 기능이며, 다음의 다섯가지 타겟 (여기서 타겟은 Xcode 타겟을 의미) 의 조합입니다:
- Source: 기능의 소스 코드 (Swift, Objective-C, C++, JavaScript...) 와 리소스 (이미지, 폰트, 스토리보드, xib) 를 포함합니다.
- Interface: 기능의 공개 인터페이스와 모델을 포함하는 보조 타겟입니다.
- Tests: 기능의 단위 테스트와 통합 테스트를 포함합니다.
- Testing: 테스트와 예제 앱에서 사용될 수 있는 테스트 데이터를 제공합니다. 또한, 나중에 볼 수 있듯이 다른 기능에서 사용할 수 있는 모듈 클래스와 프로토콜에 대한 모의 객체 (Mock) 를 제공합니다.
- Example: 개발자가 특정 조건 (다른 언어, 다른 화면 크기, 다른 설정) 에서 기능을 확인하는데 사용할 수 있는 예제 앱을 포함합니다.
타겟에 네이밍 규칙을 따를 것을 권장하며, 이는 Tuist의 DSL 덕분에 프로젝트에 강제로 적용할 수 있습니다.
Target | Dependencies | Content |
---|---|---|
Feature | FeatureInterface | 소스 코드와 리소스 |
FeatureInterface | - | 공개 인터페이스와 모델 |
FeatureTests | Feature , FeatureTesting | 단위 테스트와 통합 테스트 |
FeatureTesting | FeatureInterface | 테스트 데이터와 모의 객체 |
FeatureExample | FeatureTesting , Feature | 예제 앱 |
UI 프리뷰\
Feature
는 UI 프리뷰를 사용하기 위해 Development Asset으로 FeatureTesting
을 사용할 수 있습니다.
테스트 타겟 대신 컴파일러 지시문\
또한, Debug
로 컴파일할 때 Feature
나 FeatureInterface
에 테스트 데이터와 모의 객체를 포함하기 위해 컴파일러 지시문을 사용할 수 있습니다. 그래프를 단순화할 수 있지만, 결국 앱을 실행하는데 필요하지 않은 코드를 컴파일하게 될 수 있습니다.
왜 모듈인가
명확하고 간결한 API
모든 앱 소스 코드가 같은 타겟에 있으면 코드에서 암시적 의존성이 쉽게 생기고 결국 잘 알려진 스파게티 코드가 될 수 있습니다. 모든 것이 강하게 결합되어 있고, 상태는 예측하기 힘들어지고, 새로운 변경 사항을 도입하기 힘들어 집니다. 독립적인 타겟에 기능을 정의할 때 기능 구현의 일환으로 공개 API를 설계해야 합니다. 우리는 무엇을 공개할지, 기능이 어떻게 사용되어야 할지, 무엇을 비공개로 남겨야 할지 결정해야 합니다. 우리는 기능 클라이언트가 기능을 어떻게 사용할지에 대해 더 많은 제어를 할 수 있고, 안전한 API 설계로 좋은 개발 방식을 강제할 수 있습니다.
작은 모듈
분할 정복 (Divide and conquer). 작은 모듈로 작업하면 더 집중할 수 있고 기능을 독립적으로 테스트하고 확인할 수 있습니다. 게다가 개발 주기는 훨씬 더 빨라지는데, 이는 기능을 동작시키기 위해 필요한 컴포넌트만 컴파일하는 선택적 컴파일 덕분입니다. 앱 전체의 컴파일은 작업의 마지막에만 필요하며 이때 앱에 기능을 통합해야 합니다.
재사용성
코드를 앱과 확장과 같은 다른 결과물에 재사용하는 것은 프레임워크나 라이브러리를 사용하도록 권장합니다. 모듈을 구축하면 코드 재사용이 매우 간단해 집니다. 기존 모듈을 결합하고 (필요할 때) 플랫폼 별 UI 계층을 추가하여 iMessage 확장, Today 확장, 또는 watchOS 애플리케이션을 만들 수 있습니다.
의존성
모듈이 다른 모듈에 의존할 경우, 해당 모듈은 의존할 모듈의 인터페이스 타겟에 대한 의존성을 선언합니다. 이러면 두 가지 장점이 있습니다. 하나의 모듈 구현이 다른 모듈 구현과 결합되는 것을 방지하고, 기능의 구현만 컴파일하고 직접적인 의존성과 전이적 의존성에 대한 인터페이스만 컴파일하면 되므로 클린 빌드 속도가 빨라집니다. 이 접근 방식은 SwiftRock의 인터페이스 모듈을 사용하여 iOS 빌드 시간 단축에서 영감을 얻었습니다.
인터페이스에 의존하는 것은 앱이 실행 시간에 구현의 그래프를 구성하고, 필요한 모듈에 해당 구현을 의존성 주입해야 합니다. TMA는 이를 어떻게 구현할지 강제하지 않지만, 빌드 시간에 불필요한 간접 참조를 추가하거나 이 목적을 위해 설계되지 않은 플랫폼 API를 사용하지 않는 의존성 주입 솔루션이나 패턴을 권장합니다.
결과물 타입
모듈을 구축할 때, 타겟에 대한 라이브러리와 프레임워크 및 정적과 동적 링킹 중 선택할 수 있습니다. Tuist가 없다면 의존성 그래프를 수동으로 구성해야 하므로 이 결정을 내리는데 더 복잡합니다. 하지만 Tuist 프로젝트 덕분에 이건 아무런 문제가 되지 않습니다.
타겟의 라이브러리나 프레임워크 특성과 번들 접근 로직을 분리하기 위해 개발 중에는 번들 접근자를 사용하여 동적 라이브러리나 동적 프레임워크를 사용하길 권장합니다. 이것은 빠른 컴파일 시간과 SwiftUI 프리뷰가 잘 동작하기 위해 중요한 포인트입니다. 그리고 릴리즈 빌드에서 앱이 더 빠르게 실행되기 위해 정적 라이브러리나 정적 프레임워크를 사용하는 것이 좋습니다. <0>동적 구성</0>을 활용하여 생성 시점에 결과물 타입을 변경할 수 있습니다:
# You'll have to read the value of the variable from the manifest {#youll-have-to-read-the-value-of-the-variable-from-the-manifest}
# and use it to change the linking type {#and-use-it-to-change-the-linking-type}
TUIST_PRODUCT_TYPE=static-library tuist generate
// You can place this in your manifest files or helpers
// and use the returned value when instantiating targets.
func productType() -> Product {
if case let .string(productType) = Environment.productType {
return productType == "static-library" ? .staticLibrary : .framework
} else {
return .framework
}
}
MERGEABLE LIBRARIES\
Apple은 mergeable libraries를 도입하여 정적 라이브러리와 동적 라이브러리 간 변환의 번거로움을 줄이려고 했습니다. 하지만 이것은 빌드 시 항상 동일한 결과를 가져오지 않고 최적화가 어려워 지기 때문에 권장하지 않습니다.
코드
TMA는 모듈의 코드 아키텍처와 패턴에 대해 강요하지 않습니다. 하지만, 경험을 토대로 몇가지 팁을 공유하려고 합니다:
- 컴파일러를 활용하는 것은 훌륭합니다. 그러나 컴파일러를 과도하게 사용하면 비생산적이며 프리뷰와 같은 Xcode의 기능이 원할하게 동작하지 않을 수 있습니다. 우리는 컴파일러를 사용하여 좋은 코드작성 관행과 조기에 오류를 찾도록 권장하지만, 코드를 읽기 어렵고 유지 보수 하기 어렵도록 사용하는 것은 권장하지 않습니다.
- Swift Macros는 신중하게 사용해야 합니다. 이것은 매우 강력하지만 코드를 읽기 어렵고 유지 보수 하기 어렵게 만들 수도 있습니다.
- 플랫폼과 언어를 받아들이고, 추상화 하지 말아야 합니다. 복잡한 추상화 계층을 만들면 오히려 비효율적일 수 있습니다. 플랫폼과 언어는 추가적인 추상화 계층없이도 훌륭한 앱을 만들기에 충분합니다. 좋은 프로그래밍과 좋은 설계 패턴을 참조하여 기능을 구축합니다.