조만간 진행할 프로젝트에서 적용할 앱 구조에 대해서 고민을 하면서
- Tuist를 기반으로 모듈화
- RIBs를 사용해서 프로토콜 지향 프로그래밍을 사용하고
- Clean Architecture를 도입하는
방법을 생각했습니다.
간단하게 공부를 하고 난 이후에 직접 만들어보며 익혀야겠다는 생각으로 바로 샘플앱을 만들기 시작했습니다.
샘플앱 기본 코드는 RIBs를 공부하기 위해 노수진님의 강의(https://github.com/nsoojin/MiniSuperApp-fastcampus)를 들으며 공부했던 코드를 사용했습니다.
이번 글에서는 설계에 대한 내용을 중심으로 이야기하겠습니다.
이전에 사용하던 모듈화 방법의 불편했던 점
포괄적인 모듈을 나누는 기준
이전의 프로젝트에서는 Tuist를 하나의 프로젝트로 모듈화를 했습니다.
하나의 프로젝트 내부에 여러개의 타겟을 추가하여 구성했고, 그렇게 구성하다보니 각 타겟의 역할별로 묶는 계층이 한 눈에 들어오기 힘든 문제점이 생겼습니다.
또한 앱의 중심이 되는 코드들(Presenter Layer, Domain Layer, Data Layer)은 하나의 Target으로 관리를 하였고 네트워크, 로깅, 캐싱과 같은 독립적인 역할을 하는 부분들만 각각의 타겟으로 분리해서 사용했습니다.
결과적으로 앱의 중심이 되는 코드들은 하나의 타겟 내부에서 여러 역할을 하는 코드들이 하나의 타겟 안에 들어가게 되었습니다.
복잡하고 한 눈에 파악하기 어려운 Target들
여러 Target들을 생성하고 각 Target들의 설정을 해주기 위해서 팩토리를 생성하고 그 안에서 Target들을 보기 쉽게 생성하려고 노력했지만 하나의 project.swift에서 여러 Target들의 Dependency, 설정값을 관리하는 것은 가독성을 쉽게 떨어뜨렸습니다.
그리고 Target을 추가했을 때 기존의 Target들과 폴더링을 동일하게 맞춰주는 작업 자체가 힘든 문제가 있었습니다.
모듈화 구조
샘플앱을 만들면서 그려본 설계도입니다.
App
앱의 시작점입니다.
최상단 리블렛을 가지고 있고, 필요한 모듈의 Implement를 알고 있는 유일한 아이입니다.
Core
이름이 이상할 수 있습니다,.. Util같은 이름이 더 맞을 수도 있습니다.
앱의 다양한 곳에서 사용되는 코드를 Target으로 나누고, 해당 Target들을 전부 가지고 있는 프로젝트입니다.
코드 조각들을 가지고 있는 경우가 많아 Implement와 Interface를 따로 나누지 않고 하나로 Target을 만들어주었습니다.
Features
앱에 메인 기능들을 나눈 폴더들을 가지고 있는 폴더입니다.
내부에는 기능별로 이름이 정의된 폴더가 존재하고 (Finance, Transport, Home 등), 해당 폴더 내부는 Domain, Data, Presentation(UserInterface)으로 나누어져 있습니다.
Domain, Data, Presentation은 각각 프로젝트로 이루어져 있습니다.
하나의 프로젝트는 Interface와 Implement라는 이름의 두개의 Target을 포함하고 있습니다.
각 모듈이 서로의 구현부를 알면서 서로 강하게 결합되어 있다면 모듈화를 하는 의미가 없다고 생각하였고, 이를 해결하기 위해서 Interface와 Implement를 각각 Target으로 나누었습니다.
A라는 모듈에서 B라는 모듈을 알지 않고 B 모듈의 Interface만 알게 함으로써 B모듈이 어떻게 구성되고, 동작하는지 모른 채로 사용할 수 있도록 할 수 있었습니다. 즉, 의존성을 주입해주는 최상단 부분에서만 구현부(Implement)를 알고 있고 사용하는 부분에서는 구현부를 모른 채 Interface Target만을 사용해서 필요한 로직등을 구현할 수 있습니다.
Domain
Domain의 경우 Implement, Interface, Test 총 3개의 Target으로 이루어져있습니다.
Implement의 경우 위에서 설명드린 것과 같이 최상단 모듈만 알고 있기 때문에 Static Library로, Interface의 경우 다양한 모듈에서 알고있을 수 있기 때문에 Dynamic Framework로 설정하였습니다. Mach-O type 결정에 대한 이야기는 아래에서 진행하도록 하겠습니다.
Data
Data의 경우도 Domain과 동일한 구조를 갖도록 하였습니다.
Presentation
Presentation의 경우는 Implement, Interface, DemoApp 총 3개의 Target으로 이루어져 있습니다.
Domain과 Data와 달리 DemoApp을 만들어주어서 해당 모듈의 UI, 로직을 빠르게 확인할 수 있도록 하였습니다.
기본적으로 Target별로 Scheme이 생성이 되기 때문에 Presentation의 구현을 확인할 때 해당 DemoApp을 Scheme으로 설정하고 실행시켜서 현재 내가 관심있는 모듈만 빠르게 확인할 수 있습니다.
ResourceKit
앱의 UI가 Feature별로 모듈화가 되어있기 때문에 각각의 Presentation 모듈들이 각각의 Asset을 가지고 있어야 했습니다.
하나의 모듈에서만 사용되는 Asset의 경우는 따로 관리해도 괜찮지만, 여러 Presentation 모듈에서 사용되는 Asset의 경우는
- Asset을 사용하기 위해서 다른 모듈에 대한 dependency를 추가하거나,
- Asset을 중복으로 선언해야 했습니다.
이러한 방법들은 올바르지 않은 방법이라 생각했고 Asset과 폰트등 UI와 관련된 Resource를 관리하는 프로젝트를 분리하였습니다.
이를 통해 앱 전체에서 사용되는 UI Resource를 한 곳에서만 선언해서 사용할 수 있고, 불필요한 dependency를 줄일 수 있었습니다.
또한 Tuist의 기능 중 하나인 리소스 관리 기능을 통해서 String으로 Asset을 접근하지 않아서 혹시 모를 휴먼 에러를 방지하고 사용성을 개선할 수 있었습니다.
Static vs Dynamic
모듈화를 진행하면서 각 모듈의 Mach-O Type을 어떻게 결정할지에 대해서 고민을 했었습니다.
Static의 경우 바이너리 파일에 코드가 직접적으로 힙에 저장이 되고, Dynamic의 경우 레퍼런스만 힙에 저장이 되는 차이점이 있어서 각 상황에 따라서 어떤 것을 선택해야 할 지를 결정해야 했습니다.
Static과 Dynamic의 차이점은 아래 링크에 자세하게 나와있으니 참고해보시기 바랍니다.(https://github.com/lu15gv/iOS-libraries-cheat-sheet)
현재 모듈의 설계에서는 앱의 최상단에서 모든 dependency를 주입을 해주기 때문에 각 모듈의 구현부는 최상단 모듈만 알게 하면 되었습니다.
이 경우에는 Static Library가 중복이 생기지 않기 때문에 구현부의 경우는 Static Library로 만들어주었습니다.
하지만 Interface의 경우는 여러 모듈에서 사용되기 때문에 바이너리 파일에 중복으로 들어갈 수 있습니다. 따라서 Dynamic Framework로 설정해주었습니다.
RIBs + Clean Architecture
POP를 지향하는 RIBs는 Interface와 Implement로 대부분의 코드를 나눈 상황에서 좋은 선택지였습니다.
기존의 코드에서 모듈화를 위해서 Interface를 새로 만드는 것이 아닌 기존의 RIBs 코드에서 Listener, Buildable, (Dependency)만 Interface로 옮기면 됐습니다.
- Builable을 통해서 하위 riblet을 생성해주는 로직
- 하위 riblet에 대한 listener를 가지고 delegate를 받아내는 로직.
이 두개의 로직만 알고 있으면 다른 내부 동작 로직의 경우는 다른 모듈이 알 필요가 없기 때문이죠.
Dependency의 경우는 Compositional Root가 존재하고, 앱 상단에서 모든 의존성을 주입해주는 게 아니라면 Interface로 옮겨야 하기 때문에 괄호로 표시하였습니다.
클린 아키텍쳐에 대한 사용은 이전에 작성했던 포스트를 참고해주시기 바랍니다.(https://ios-chansoo.tistory.com/5)
Domain Layer 와 Interactor
RIBs에는 Viewless RIBlet들이 존재하고 해당 RIBlet들의 로직을 사용하면 Clean Architecture의 개념을 도입하지 않아도 되지 않냐는 질문이 있었습니다.
물론 RIBs에서 지원하는 정도로만 사용을 해도 무방하지만 Interactor의 역할에 대해서 조금 더 집중을 해보았습니다.
뷰에서 PresentableListener를 통해서 action을 전달해주면 해당 action에 맞는 로직을 실행합니다.
이 로직에서 router의 함수를 호출할 수도 있고, 비즈니스 로직을 수행할 수도 있고, 뷰에 새로운 상태나 값을 전달해줄 수도 있습니다.
Interactor가 하는 일이 많아지면 Interactor 또한 비대해질 수 있기 때문에 비즈니스 로직을 UseCase로 분리하여 비즈니스 로직에 대한 결과만 받아서 뷰에 값을 전달하거나 router의 함수를 호출할 수 있도록 하여 Interactor의 이름에 걸맞게 상호작용을 도와주는 역할에 집중할 수 있도록 했습니다.
또한 해당 Interactor에서 어떠한 로직이 있어야 하는지를 UseCase를 통해서 한 눈에 파악할 수 있는 효과도 있었습니다.
자세한 코드를 보고 싶으시다면 아래의 링크를 참고해주시면 감사하겠습니다.
https://github.com/chansooo/ModularizedSuperApp
'🍎 iOS > ♻️ 모듈화' 카테고리의 다른 글
Modular Architecture 실전편 (0) | 2023.04.30 |
---|---|
Tuist 도입기 (0) | 2022.11.19 |