저번 글에서는 Micro-Features Architecture가 무엇인지, 그리고 왜 Flyleaf 프로젝트에 이 구조를 적용하게 되었는지 이야기했습니다.
이번 글에서는 Flyleaf 프로젝트에 실제 적용한 Micro-Features Architecture 구조를 이야기해보려고 합니다.
- Flyleaf 프로젝트는 어떤 모듈 구조를 가지고 있는지
- 각 모듈은 어떤 역할을 하는지
- 의존성 방향을 어떤 기준으로 설계했는지
를 중심으로 실제 프로젝트 기준으로 이야기해보려고 합니다.
Flyleaf 프로젝트 모듈 구조
Flyleaf 프로젝트는 Feature 중심의 Micro-Features Architecture를 기반으로 모듈을 구성했습니다. 각 기능은 독립적인 Feature 단위로 분리되어 있으며, Feature 간 직접 의존하지 않고 Interface 모듈을 통해 의존성을 분리하는 구조를 사용하고 있습니다.
현재 Flyleaf 프로젝트의 구조는 아래와 같습니다.
Projects
├ App
├ Core
└ DesignSystem
Features
├ Home
│ ├ Feature
│ ├ Interface
│ ├ Tests
│ ├ Testing
│ └ Example
│
├ Login
│ ├ ...
├ Wishlist
│ ├ ...
├ Journey
│ ├ ...
├ History
│ ├ ...
├ Search
│ ├ ...
└
Projects와 Features 디렉토리를 기준으로 구조를 나누었습니다. 즉, 개념적으로 보면 AppLayer와 FeatureLayer가 분리되어 있습니다. Projects 디렉토리에는 앱 전반에서 사용하는 공통 모듈이 위치하고, Features 디렉토리에는 실제 앱 기능을 담당하는 Feature 모듈들이 존재합니다.
App
App 모듈은 앱의 진입점 역할을 하는 모듈입니다.
이 모듈에서는 실제 기능 구현을 담당하지 않고
- 앱의 Entry Point
- Coordinator를 통한 화면 흐름 관리
- Feature Builder를 통한 화면 생성
을 담당합니다.
중요한 뽀인트는 App 레이어는 Feature 내부 구현을 직접 알지 않는다는 것입니다.
대신 Feature의 Interface에만 의존하도록 설계했습니다.
예를 들어 로그인 화면을 생설할 때도 아래와 같이 Interface를 통해 생성합니다.
let loginVC = loginBuilder.build
이렇게 하면 App 레이어는 LoginFeature 내부 구현(LoginViewController, LoginViewModel 등)을 알 필요가 없습니다.
즉, Feature의 생성 책임은 Feature 내부로 숨기고, App 레이어는 Interface를 통해 화면을 요청하는 역할만 담당하게 됩니다.
Coordinator
Flyleaf에서는 화면 전환 로직을 Coordinator 패턴을 통해 관리하고 있습니다. 일반적으로 VC 내부에서 화면 전환을 처리하게 되면
- VC간 화면 전환 로직까지 담당하게 되어 역할이 커짐
- 여러 화면 간 의존성이 복잡해짐
- 네비게이션 흐음을 한 곳에서 파악하기 어려움
이러한 문제를 해결하기 위해 Coordinator를 통해 화면 흐름을 분리했습니다.
예를 들어 앱 시작 시 아래와 같은 흐름을 Coordinator가 담당합니다.

즉, Coordinator는 어떤 화면을 보여줄지 결정하는 역할을 담당하고, 각 Feature는 자신의 화면을 생성하는 역할만 수행합니다.
Core
Core 모듈은 앱 전체에서 공통적으로 사용되는 도메인 모델과 서비스를 관리하는 모듈입니다.
- UserModel
- AuthServicing
같은 요소들이 Core 모듈에 위치합니다.
Feature에 종속되지 않는 공통 데이터 모델과 서비스 로직을 Core에 배치하여 여러 Feature에서 재사용할 수 있도록 설계했습니다.
DesignSystem
DesignSystem 모듈은 앱에서 전역적으로 사용하는 UI 컴포넌트와 Base UI를 관리하는 모듈입니다.
- 공통 색상, 폰트, 아이콘 에셋, 이미지 등
- UI 컴포넌트
같은 요소들이 DesignSystem 모듈에 위치합니다. 각 Feature에서는 디자인 시스템을 통해 UI를 구성합니다.
Feature 모듈의 내부 구조
Flyleaf에서는 각 기능을 Feature 단위의 독립적인 모듈로 구성했습니다. 각 Feature는 아래와 같은 내부 구조를 가지고 있습니다.
Home
├ Feature
├ Interface
├ Tests
├ Testing
└ Example
Feature
Feature 모듈은 해당 기능의 실제 구현 코드가 위치합니다.
예를 들어 HomeFeature에서는
- HomeVC
- HomeViewModel
- Builder
- Feature 내부 로직
같은 코드들이 이 영역에 포함됩니다.
즉, UI, ViewModel, Feature 로직 등 실제 기능 구현이 모두 이 영역에 존재합니다.
이 영역은 Feature의 내부 구현 영역이기 때문에 외부 모듈에서는 직접 접근하지 않도록 설계했습니다.
Interface
Interface는 Feature 외부에서 접근할 수 있는 API를 정의하는 모듈입니다. 외부 모듈은 Feature의 내부 구현이 아닌 Interface에만 의존하게 됩니다.
예를 들어 AppCoordinator에서는 아래와 같이 Interface 타입만 사용합니다.
private let homeBuilder: HomeBuildable
이를 통해 Feature 내부 구현이 변경되더라도 외부 모듈에는 영향을 최소화할 수 있습니다.
Tests
Tests는 Feature의 단위 테스트를 작성하는 모듈입니다.
- ViewModel 테스트
- 비즈니스 로직 테스트
- 계산 로직 테스트
등 단위 테스트가 작성됩니다.
Testing
Testing은 테스트에 필요한 Mock 객체나 테스트 유틸리티를 모아두는 모듈입니다.
예를 들어 아래와 같은 코드들이 이 모듈에 존재합니다.
- MockAuthService
- 테스트 Helper
Example
Example 모듈은 Feature를 독립적으로 실행해볼 수 있는 예제 앱입니다.
UI 작업을 할 때는 특정 Feature 화면만 빠르게 확인하고 싶은 경우가 많습니다. 기존 구조에서는 앱 전체를 실행해야 했지만, Example 모듈을 사용하면 Feature 단위로 빌드할 수 있습니다.
Example App
└ HomeFeature 실행
이 구조를 사용하면 UI 개발 속도를 높일 수 있고, 예제 앱 배포를 통해 디자이너와 협업 효율도 높일 수 있는 장점이 있습니다.
+ 개인적으로 모듈화를 적용하면서 가장 체감이 컸던 부분이 바로 Example 앱이었습니다.
Interface 모듈을 분리한 이유
Micro-Features Architecture에서 가장 특징적인 구조 중 하나는 Feature 내부 구현과 외부에서 사용하는 API를 분리하는 것이라고 생각합니다.
Feature 간 직접 의존의 문제
import SearchFeature
예를 들어 HomeFeature에서 SearchFeature의 기능을 사용해야 하는 상황이 있다고 가정해봅시다. 만약 Interface 모듈이 없다면 HomeFeature에서는 위와 같이 SearchFeature를 직접 import 해야합니다.
그리고 검색 화면을 사용하기 위해 SearchFeature 내부 구현을 직접 생성하게 됩니다.
let searchVC = SearchViewController(...)
이렇게 되면 HomeFeature는 단순히 검색 기능을 사용하고 싶은 것뿐인데, 결과적으로 SearchFeature의 내부 구현까지 알게 됩니다.
만약 SearchFeature 내부 구조가 변경된다면, 예를 들어 SearchVC의 생성 방식이 바뀌거나, SearchViewModel의 의존성이 변경되면, HomeFeature 역시 함께 수정해야 할 가능성이 생깁니다.

즉, 단순히 기능을 사용하는 관계임에도 불구하고 Feature 간 결합도가 높아지게 됩니다.
또한 이런 방식이 여러 Feature에 반복되면 아래와 같은 구조가 만들어집니다.
HomeFeature -> SearchFeature
WishlistFeature -> SearchFeature
JourneyFeature -> SearchFeature
이렇게 Feature 간 직접 의존이 늘어나게 되면, 프로젝트가 커질수록 의존성 구조가 복잡해지고 관리하기 어려워질 수 있습니다.
Interface를 통한 의존성 분리
이 문제를 해결하는 가장 좋은 방법은 Feature 외부에서 접근할 수 있는 API를 Interface로 구현하는 것입니다.
즉 Feature 구조를 아래와 같이 나누었습니다.
Home
├ Feature // 실제 구현
└ Interface // 외부에 공개되는 API
이 구조에서는 Feature 내부 구현을 직접 노출하지 않고 Interface를 통해서만 접근할 수 있습니다.
예를 들어 LoginFeature에서는 다음과 같은 Interface를 제공합니다.
public protocol LoginBuildable {
func build(onLoginSuccess: @escaping () -> Void) -> UIViewController
}
그리고 실제 구현은 Feature 내부에서 이루어집니다.
public final class LoginBuilder: LoginBuildable {
public func build(onLoginSuccess: @escaping () -> Void) -> UIViewController {
...
}
}
외부에서는 Feature 내부 구현을 알 필요 없이 Interface를 통해 기능을 사용할 수 있습니다.
import LoginInterface
let loginVC = loginBuilder.build
이렇게 Interface를 통해 의존성을 관리하게 된다면 의존성 구조는 아래와 같이 변경됩니다.

이 구조에서는 HomeFeature 모듈이 LoginFeature의 구현에 직접 의존하지 않고 LoginInterface라는 Interface(Protocol)에만 의존하게 됩니다.
뭔가 익숙하지 않나요? 맞습니다! DIP..! 의존성 역전입니다! 덕분에 Feature 내부 구현이 변경되도라도 외부 모듈에 미치는 영향을 최소화할 수 있습니다.
Interface 분리의 장점
1. Feature 내부 구현 캡슐화
Feature 내부 구조가 외부에 노출되지 않습니다.
즉, VC, ViewModel, 내부 의존성 등과 같은 구현 세부사항을 외부에서 알 필요가 없습니다.
2. 결합도 감소
외부 모듈은 Feature가 아니라 Interface에만 의존합니다.
덕분에 Feature 내부 구조가 변경되더라도, 외부 모듈에 미치는 영향이 줄어듭니다.
3. 모듈 간 의존성 관리
Feature 간 직접 의존이 발생하지 않도록 의존성 경계를 명확하게 만들 수 있습니다.
의존성 방향 설계
Micro-Features Architecture를 적용하면서 가장 중요하게 생각했던 부분은 단순히 모듈을 나누는 것이 아니라 의존성 방향을 명확하게 설계하는 것이었습니다.
모듈화는 물리적으로 폴더를 나누는 것만으로는 충분하지 않습니다. 각 모듈이 서로를 어떤 방향으로 의존하는지까지 함께 설계되어야 결합도를 낮추고 구조를 안정적으로 유지할 수 있습니다.
Flyleaf에서는 아래와 같은 기준으로 의존성 방향을 설계했습니다.
전체 의존성 방향

Flyleaf의 의존성 방향은 크게 보면 위와 같습니다.
1. Feature간 기능 사용 필요시 Interface에만 의존
2. Feature는 자신의 Interface를 구현
3. 공통 모델과 서비스는 Core에 의존
4. UI 컴포넌트는 DesignSystem에 의존하는 구조입니다.
이 구조를 통해 상위 레이어가 하위 레이어의 구현이 아니라, 추상화(Interface)에 의존하도록 만들었습니다.
App은 Feature 구현이 아닌 Interface에 의존한다.
Flyleaf에서 App 모듈의 역할은
- 앱의 진입점 관리
- 화면 흐름 제어
- Feature 조립
입니다.
이 과정에서 AppCoordinator는 Feature를 직접 생성하지 않고 각 Feature가 제공하는 Builder Interface를 통해 화면을 생성합니다.
Feature는 서로 직접 의존하지 않는다.

Flyleaf에서 Feature 간 직접 의존은 가능한 피하려고 했습니다.
예를 들어 HomeFeature에서 SearchFeature의 기능이 필요하다고 해서 아래와 같이 직접 import(참조)하는 구조는 지양했습니다.
import SearchFeature
이렇게 되면 HomeFeature가 SearchFeature의 내부 구현에 직접 의존하게 되고, 결과적으로 Feature 간 결합도가 높아지게 됩니다.
대신 Feature가 다른 Feature의 기능이 필요할 때는 그 Feature의 Interface에만 의존하도록 구조를 설계했습니다.
구현 의존은 Composition Root에서만 허용된다
Flyleaf는 SceneDelegate를 Composition Root로 두고 있습니다.
이 지점에서는 실제 구현체를 생성하고 조립해야 하기 때문에 Feature 구현 모듈을 import 하는 것이 허용됩니다.
'Flyleaf - 독서를 여행처럼 > 개발일지' 카테고리의 다른 글
| [개발일지] 06. 단위 테스팅을 해봅시다! (feat. AI) (0) | 2026.03.20 |
|---|---|
| [개발일지] 05. 홈 독서 지도를 어떻게 구현했을까요? (MapKit 커스텀 + 항공 경로 시각화) (0) | 2026.03.16 |
| [개발일지] 04. Composition Root (feat. 어디까지 몰라야하는가?) (0) | 2026.03.11 |
| [개발일지] 02. Micro-Features Architecture가 무엇이고, Flyleaf에 왜 적용했을까요? (0) | 2026.03.10 |
| [개발일지] 01. 독서를 여행처럼! (2) | 2026.03.06 |