-
iOS 엔지니어링 '잘' 하기 (feat. 클린 아키텍쳐)iOS Dev 2023. 3. 25. 22:11반응형
시작하기에 앞서, 본 내용은 23.03.26에 진행한 숭실대학교 중앙동아리 유어슈 모바일 엔지니어링 데이 Ace '김종찬' 연사님의 발표 내용 기반입니다.
모바일 엔지니어를 괴롭히는 요소들과 그에 대한 대책은 무엇이 있을까?
1. 서비스의 방향과 스펙을 계속 흔드는 Product팀
=> 기능의 재사용성을 높여주는 DI / 클린 아키텍쳐 / SOLID원칙 / 빠른 개발을 돕는 선언형UI2. 서비스의 경험과 UI 스펙을 요상하게 주는 Design 팀
=> 정해진 UI 스펙을 지속적으로 재활용하고, 커뮤니케이션 가능하게 해주는 디자인 시스템3. 모바일 클라이언트에서 활용해야 하는 Response를 불편하게 전달하는 Backend 팀
=> 백엔드 레이어와 그 곳에서 전달해주는 데이터를 레이어 별로 변환하여 사용하는 레이어드 아키텍쳐, DTO / Entity 재사용4. 매 해 애플 주도로 변경되는 사항 및 새로운 폼펙터, 디바이스, 플랫폼
=> 클린 아키텍쳐의 Presentation Layer에 해당하는 플랫폼과 우리 서비스에 종속적인 Domain / Data Layer를 잘 분리해서 관리.5. 개선 및 문제를 해결해도 쉽사리 우리 앱을 업데이트 해주지 않으며, 여전히 저사양 기기를 사용해주시는 우리 유저분들
=> Server Driven UI, Sub Threading, 비동기 처리6. 문제를 해결하기 위해 학습하고 트렌드를 분석해야 하는데, 그렇지 않은 개발자들
=> 약속된 아키텍쳐의 제한을 넘어 단일 책임 원칙을 깨는 코드를 만드는 것을 방지하는 모듈화 설계, 의도하지 않은 장애를 방지하는 Unit Test 작성.아키텍쳐 패턴이란? (MVC, MVP, MVVM + MVI)
https://beomy.tistory.com/43 참고.
맨 처음 모바일이 처음 나왔을 때 기존에 웹이나 서버에서 사용하던 MVC 패턴을 그대로 적용해서 만들었다.
MVC는 뷰와 뷰 컨트롤러가 한 몸이기 때문에 내용이 너무 비대해지고, 책임 분리가 안되어서 리펙토링이 힘들었다.
그래서 나온 것이 MVP. 뷰와 프레젠터로 나누어서 책임을 분리. Unit Test도 하기 훨씬 수월해졌다. 대신 뷰와 프레젠터가 1:1이 되는 바람에 프레젠터를 재사용할 수 없는 문제가 생기고, 마찬가지로 프레젠터가 너무 비대해졌다.
그래서 MVVM이 각광을 받게 된다. 옵져버 패턴을 통해 설계하면 뷰 모델이 뷰를 몰라도 된다. 책임도 분리되고, 뷰와 뷰 모델이 n:1 이 되어서 재사용도 용이해졌다. 요즘엔 거의 대부분 MVVM을 선택하고 있다. MVI 도 쓰이고 있다고 한다.선언형 UI (SwiftUI)
순수하게 뷰를 진짜 뷰만 띄워주는 용도로 사용하기 위해 만들어졌다. 따라서 선언형 UI에서의 뷰는 Stateless를 지향해야 한다.(TextField같은 것들은 Stateful으로 해주는게 맞지만, 다른 것들은 stateless를 지향)
Redrawing이 발생하는 조건과 대처방법, 최적화 방법을 생각하면서 코딩을 해야한다.
클린 아키텍쳐란?
클린 아키텍쳐는 본래 모바일을 대상으로 설계 된 것이 아니다. 따라서 지금부터 나올 내용들은 보편적인 클린 아키텍쳐를 소개한다.
레이어 간 의존관계 짙은 파란색으로 되어있는 부분은 사용자와 맞닿아있거나 DB와 맞닿아있다. 즉, 데이터의 흐름에서 양쪽 끝을 담당한다.
그 안쪽에 그냥 파란색으로 되어있는 부분은 뷰를 컨트롤하거나 DB에 쏠 데이터를 준비한다.
회색으로 되어있는 부분은 use case가 있다. 데이터를 어떻게 사용할지를 결정한다.
가장 안쪽에 옅은 파란색으로 되어있는 부분은 데이터가 어떻게 생겼는지를 결정한다.모든 영역에 대해 밖에 있는 영역은 바로 안쪽 영역을 알고 있다. (의존성을 갖고 있다.) 하지만 안쪽에 있는 영역은 바깥쪽 영역을 알지 못한다.
데이터의 흐름 iOS의 관점에서, 맨 위에 짙은 파란색은 iOS 프레임워크를 의미하고, 파란색은 선언형 UI 또는 뷰 모델을 의미한다. 이 둘을 합쳐서 Presentation Layer이라고 한다.
안쪽에 회색 영역은 use case이며, 오직 swift로만 쓰여져 있는 서비스 요소들이다. 여기에 UIKit 이런걸 import하면 안된다. 어떤 플랫폼에도 종속되면 안된다. 그래서 이를 잘 적용하면 플랫폼 별(안드로이드, iOS 등)로 usecase를 하나만 정의해두고 그걸 언어에 맞게 convert해서 쓸 수 있게 된다. use case는 안쪽에 Entity, 즉 데이터 모델을 알고 있다.
가장 안쪽 옅은 파란색은 Entity라고 쓰여져 있다. DTO, VO라고 봐도 여기서는 무방하다.
회색 유즈케이스와 옅은 파란색 Entity는 Domain 계층이라고 한다.
맨 아래 Repository와 데이터 소스(Moya, Alamofire 등)은 데이터 계층이라고 한다.중간에 Tranlater 오타. Translater가 맞음. 조금 더 와닿게 레이어를 나눈 모습이다. 중간에 Repository는 Domain 계층과 Data 계층 사이에 낑겨있다. Repository는 Domain Layer에서 Protocol로 만들어두고, Data Layer에서 그것에 대한 구현(Impl)을 한다.
중간에 Translater의 역할은 모델과 소통하면서 DTO와 VO간 변환 작업을 한다. UseCase가 서버에서 데이터를 받아와서 DTO -> VO를 원하면 Translater를 거쳐 VO를 받아온다. 반대도 마찬가지다.
간단한 컨셉의 모바일 아키텍쳐 (Just Clean Architecture)
클린 아키텍쳐를 적용시키려면 모듈화를 잘 해야한다. 레이어에 따라 어떤 모듈이 위치해야 하는지 대략적으로 나와있다.
먼저 맨 아래 Presentation Layer에 들어갈 모듈은 : App 모듈과 각각의 Feature 모듈. ViewController, ViewModel이 여기에 위치한다.
그 다음 Domain Layer에 들어갈 모듈은 : UseCase와 UseCaseImpl(구현체), protocol로 구성되어있는 Repository으로 구성된 Domain 모듈이다.
그 다음 Data Layer에 들어갈 모듈은 RepositoryImpl(구현체)와 DataSource(네트워크, DB 연결등을 담당)으로 구성된 Data 모듈이다.모바일 클린 아키텍쳐에 기반한 모듈 구조
각각의 의존성을 나타낸 그림이다.
App 모듈은 각각의 Feature모듈, Domain, Data 모듈을 알고 있어야 한다.
각각의 Feature 모듈은 Domain 모듈만 알고 있으면 된다.
Data 모듈은 Domain 모듈을, 정확히는 Domain 모듈의 Repository Protocol들만 알고 있으면 된다.
Domain 모듈은 아무것도 알면 안된다. 플랫폼 등에 종속되면 안된다.근데 여기서 잘 생각해보면, 분명 UseCaseImpl은 Repository Protocol을 채택하고 있는 Repository를 가지게 된다. 해당 Repository를 통해 UseCase가 그 메소드를 실행시켜야 한다. 메소드를 실행시키기 위해선 Data 모듈에 있는 RepositoryImpl을 꼭 알아야 실행시킬 수 있다. 이걸 어케 알지..? 이걸 DI가 한다!
DI란?
DI 는 외부에서 의존성을 주입시켜주는 기법이다. 영어로도 Dependency Injection. 의존성 주입이라고 한다.
https://developer.android.com/training/dependency-injection?hl=ko
Android의 종속 항목 삽입 | Android 개발자 | Android Developers
Android의 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니
developer.android.com
위 링크는 구글이 직접 알려주는 DI의 정확한 정의다.
쉽게 말하면 외부에서 객체를 인자로 받아서 내부 변수에 넣어주는 것이다.DI의 핵심 개념인 DIP는 SOLID원칙(객체지향설계) 중 D에 해당한다. 의존관계 역전 원칙이라고 하는데, 간단하게 말해서 클래스 내부에서는 전부 protocol을 기반으로 실행하고, 해당 protocol을 채택하는 구현체는 밖에서 주입하라는 소리다.
그리고 이 DIP를 그대로 따르는 패턴은 IoC이다.
DIP : https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)DI를 잘 쓰면 소프트웨어 디자인 패턴 중 제어 반전(IoC) 패턴을 잘 쓰는 것이 된다.
제어 반전(IoC) : https://ko.wikipedia.org/wiki/%EC%A0%9C%EC%96%B4_%EB%B0%98%EC%A0%84이 밖에도 DI는 SOLID원칙 중 SRP, OCP, LSP와도 연관이 깊다. 즉 매우매우 중요한 요소임을 알 수 있다.
모바일 클린 아키텍쳐에 기반한 모듈 구조
그렇다면 DI는 모듈 어디에 위치해야 할까? 당연하겠지만 App모듈 내부에 위치해야 한다.
의존 관계는 어떻게 설정할까? 먼저 DI가 해결해야 할 상황은 구현체(클래스)가 Protocol을 갖고 있을 때이다.
현재 Protocol은 총 3개이다. Repository, UseCase, ViewModel. 이 Protocol들을 갖고 있는 클래스들한테 DI가 해당 Protocol을 채택한 객체를 만들어서 주입 시켜 줄 것이다.
하나씩 살펴보자.1. RepositoryImpl와 DataSource 설정
RepositoryImpl은 DI에서 만들어야 하기 때문에 DataSource도 DI에서 만들어서 넣어준다.2. UseCaseImpl과 Repository 설정
DI에서 UseCaseImpl을 만들 때 Repository를 채택한 구현체를 넣어준다.3. ViewModel과 UseCase 설정
ViewModel에 UseCase의 구현체를 넣어준다.4. View와 ViewModel 설정
각각의 Feature 모듈 안에서 View에 ViewModel을 채택한 구현체를 넣어준다.여기까지 봤을 때 다시 거꾸로 올라가보자.
View에 ViewModel의 구현체가 필요하다. 그럼 ViewModel의 구현체는 DI에서 만들겠네?
ViewModel에 UseCase의 구현체가 필요하다. 그럼 UseCase의 구현체는 DI에서 만들겠네?
UseCase의 구현체는 Repository의 구현체가 필요하다. 그럼 Repository의 구현체는 DI에서 만들겠네?
이래서 Repository의 구현체를 DI에서 만드는 것이며, DataSource도 DI에서 만들어서 넣어주어야 한다.
뷰도 DI에서 만들어서 리턴해주면 좋을 것 같다.각 레이어에서 의존할 콘텐츠 강제하기
아무리 이걸 알고 있다고 해도 머리 비우고 코딩하면 Repository에서 UIImage를 만들어서 보내고.. 이럴 것 같다. 어떻게 하면 의존할 콘텐츠를 강제할 수 있을까?
먼저, Domain 모듈에는 Swift와 Rx 요소들만 들어가야 한다. import UIKit하는 순간 노트북을 덮고 자게 해야 한다.
Data 모듈에는 Domain 모듈의 요소들과 네트워크 라이브러리들(Moya, Alamofire)만 들어가야 한다.
각 Feature 모듈은 Domain 모듈만 import하면 된다.그럼 문제. Model들, 그러니까 DTO나 VO같은 친구들은 어디에 있으면 될까?
정답은 Domain 모듈이다. 그래서 앞으로 백엔드 개발자한테 DTO 코드를 받아서 Swift로 바꿔달라고 하면 된다. 어렵다면 ChatGPT의 도움을 요청하자. (스네이크 표기법이면.. 아...)새로운 Yourssu Mobile 아키텍쳐는?
기존에는 다음과 같았다.
앞으로는 어떻게 바뀌면 좋을까?
이렇게 가면 좋을 것 같다고 한다. 물론 각각의 Controller는 다른 Common들을 포함해도 된다. Article Controller에서 파싱 Util이 필요하다고 하면 Utility Common의 내용을 가져오면 된다. 단, 화살표가 역전되면 절대 안된다. 무조건 DAG(Directed Acyclic Gragh) 모양이 되게 유지하면 된다.
내 생각엔 각각의 Feature에 모듈 2개씩 만드는게 좋을 것 같다. 예를 들어 Article Feature는 ArticleController 모듈과 ArticleCommon 모듈로 나뉘어져 있게끔 만들면 좋을 것 같다. ArticleController 모듈에는 import ArticleCommon 이런 식으로 import 되게.
ArticleController 모듈에는 Coordinator로 시작하는 ViewController가 들어간다. ViewController는 View와 ViewModel이 같이 들어가게 되며, View가 ViewModel을 잘 알 수 있게끔 징검다리 역할을 한다.
ArticleCommon 모듈에는 ViewModel과 테스트, 여러 부가적인 메소드와 정보들이 위치한다.Unit Test
MVVM과 클린 아키텍쳐를 적용했다면, Unit Test가 가능하다. 뷰 모델을 테스트한다고 생각하자.
https://github.com/kudoleh/iOS-Clean-Architecture-MVVM/tree/master/ExampleMVVMTests/Presentation/MoviesScene실제 서버와 직접 통신하지 않고, 즉 백엔드에서 돌려주는 response를 실제로 쓰지 않고, Mock 데이터를 스스로 만들어서 잘 실행되는지 본다. 테스트는 기본적으로 given → when → then 단계로 진행한다.
마치며
처음으로 디자인패턴에 대한 고민을 많이 해본 것 같다. 단편적으로 알고 있던 내용이 조립된 느낌이다. 까먹기 전에 리펙토링을 성공적으로 끝내고 나의 것으로 만들고 싶다. 숭실대학교 중앙동아리 유어슈 iOS팀 화이팅!
반응형'iOS Dev' 카테고리의 다른 글
Apple Developer Academy @ POSTECH 3기 합격 후기 (1) 2023.07.20 마이크로피처 아키텍처란? (µFeatures Architecture) (1) 2023.07.15 [Tuist] Tuist를 활용하여 SwiftUI 클린 아키텍쳐를 적용한 모듈로 나눈 후 Github에 업로드 하기 2탄! (0) 2023.05.07 [Tuist] Tuist를 활용하여 SwiftUI 클린 아키텍쳐를 적용한 모듈로 나눈 후 Github에 업로드 하기 1탄 (0) 2023.04.05 UITableViewCell에 Button을 올렸는데 터치 자체가 안될 때 (0) 2022.08.08