이번 시간에는 VIPER와 관련된 우리팀의 이야기를 들려주고 경험을 공유하고 싶다. 특히 우리가 특정 상황에서 어떻게 VIPER적으로 다루는지, 우리들의 추천은 어떤지 이야기 할 것이다. 또한 당신의 경험을 코멘트 해주길 바란다(원문 링크에 들어가서 코멘트 해주시면 됩니다).
이 글의 목표는 VIPER의 규칙에 대해 이야기하거나 VIPER의 모든 컴포넌트들을 설명하려는게 아니다. 이러한 이야기는 이미 더 좋은 글들이 많다.
이 글에서는 우리 팀이 프로젝트를 하면서 무얼 배웠고, 이 아키텍처를 사용하는 동안 어떤 것들이 큰 도전이었는지 알려 주고 싶다. 우리는 시작할 때 많은 궁금증들이 있었다. 작년에 큰 프로젝트를 시작하였고 이야기는 여기서부터 시작되었다. (컨퍼런스 관련 앱인데 의제나 참석자의 목록, 발표자의 목록, 뉴스의 목록 등으로 구성된 앱이다)
VIPER를 타고 달려나갈 준비가 되었는가?
VIPER Car
왜 우리는 VIPER를 골랐을까?
Note : 만약 새 프로젝트에 적용시키길 적절한 아키텍처를 찾고있다면
이 사이트를 한 번 들어가보아라. 우리의 경우 최종적으로 VIPER를 선택했다.
- VIPER 아키텍처는 프로젝트 초기에 요구사항이 잘 정의되어 있다면 적합하다고 할 수 있다. 운좋게 우리가 그러했다. 만약 당신의 화면정의나 비즈니스 로직이 프로덕트 오너에 의해 바뀌기가 쉽다면, VIPER는 별로 좋지 않은 솔루션일 수 있다. 하나의 작은 변화에도 당신의 모든 모듈(View, Presenter, Interactor...)을 손봐야 하는 수가 있다. 이러한 대규모의 재설계는 엄청난 시간낭비이고 차라리 새로운 VIPER 모듈을 만드는게 나을지도 모른다.
- 우리 프로젝트는 상당히 규모가 컸다. 하나의 모듈을 세팅함으로써, 파일들을 생성하고 수많은 반복적인 코드를 만들어낸다. 모든 VIPER 컴포넌트로부터 데이터를 주고 받는일이 잦은데, 한 View에서 API 관리자로 데이터를 넘겨주고, 다시 데이터를 View에 돌려준다. 데이터를 자꾸 옮겨 다녀야하기 때문에 이게 왜 작은 프로젝트에는 적합하지 못한지 보여주는 예이다.
- (몇몇 예외를 제외한) VIPER는 각 요소마다 기능 정의가 아주 명확하다. 덕분에 파일의 코드 양을 줄여주고, 하나의 기능 컴포넌트에 따라 옳바르게 모듈이 나눠져있을 것이다. 추가적으로 VIPER 프로젝트는 모든 개발자에게 비슷한 관습을 만들어주기 때문에 구조가 잘 잡힌다. 새로운 개발자가 팀에 합류한다해도 빠르게 그 VIPER에 적응하게 될것이고, 새로운 개발자가 원래 프로젝트의 구조를 바꾸기는 쉽지 않을 것이다.
- 3명의 개발자가 있는 팀에서 일한다면, 모두가 하나의 모듈을 개발할 수 있다. 쉽게 쪼게어 개발할 수 있다는 뜻이다.
- 우리 프로젝트는 시작단계부터 화면정의와 기능정의가 잘 되있었기 때문에 VIPER 모듈로 만드는 것이 어렵지 않았다.
- VIPER 컴포넌트의 기본은 한 모듈에 있는 모든것들이 굉장히 잘 나눠져있다. 따라서 유닛 테스트 하기 좋은 조건이다. 이 글을 보면 VIPER에서 TDD 이야기를 들을 수 있다.
- 마지막으로 우리팀은 새로운 아키텍처를 시도해보고 싶었다!!!
Massive
시작은 MVC로 했지만, 결국 Massive(덩어리의)VC로 끝나버렸다.
프로젝트 구조, 폴더, VIPER 모듈들
혹시 모든 VIPER 모듈의 컴포넌트들을 외우고 있는가? 우리는 이 웹사이트를 기반으로 컴포넌트를 정의했고, 여기서는 Services라 부르는 컴포넌트를 사용했다. 당신은 Services라는 섹션에서 더 많은 정보를 얻을 수 있을 것이다.
VIPER 다이어그램
프로젝트 파일에 어떻게 이것들을 적용시킬까? 모든 컴포넌트는 각 폴더와 클래스로 적용될 수 있다.
다음 질문으로 VIPER 모델로서 어떤것이 제격일까? 기본적으로 가장 쉬운 접근법은 한 화면 단위로 VIPER 모델을 만드는 것이다. 아래 예를 보자.
- Login Screen (로그인 화면) -> Login Module
- Participant List (참여자 목록) -> Participant List Module
흠 모든 것을 수작업으로 하나하나 만들어야 할까? 다행히도 자동생성기를 사용하면 된다.
VIPER 모듈 자동생성기(Generator)
당신의 앱을 정말 VIPER 아키텍처 기반으로 만들고 싶다면 손으로 하나한하 처넣을 생각은 안해도 된다. 그것은 재앙이다! 새 모듈을 자동으로 생성해주는 프로세스가 필요하다. 먼저 VIPER 자동생성기 프로그램을 아래 링크에서 다운받을 수 있다.
우리 프로젝트에서는 첫번째 VIPER gen을 선택하여 우리 목적에 맞게 커스텀하여 사용했다. 예를들어, Interactor와 Presenter를 위한 테스트 파일을 추가했다. 커스텀된 vipergen툴은 아래 링크에서 사용할 수 있다.
이 커스텀 과정에서 루비로 약간이 수정이 필요했고, 단지 자동으로 모든 유닛테스트 파일에 Swift 모듈 이름을 추가해주는 기능이다.
아마 당신은 당신만의 템플릿이 필요할 것이다. 그러려먼 이미 있는 기본 템플릿에서 폴더를 복사하고 수정해가면서 간단하게 만들 수 있을 것이다.
이 저장소에 들어가면 새 템플릿을 어떻게 추가하는지 배울 수 있다. github를 쓸 수 있는 사람이라면 쉽게 커스텀이 가능할 것이다.
우리는 사실 다른 솔루션을 많이 사용해보진 않았으나, Generamba는 CLI를 셋업하기 좋게 제공되는 툴처럼 보인다. 여러분은 각자 상황에 맞게 가능한 많은 솔루션을 체크해보고 사용하는걸 추천한다.
VIPER 모듈들끼리 서로 정보 보내기
시작할때부터 우리는 VIPER 모듈 사이에 데이터를 어떻게 다루는지 생각해보았는데, 명확한 답이 보이지 않았다. 아래 토픽들은 프로젝트 시작할 때 읽으면 굉장히 도움이 될 것이다.
최종적으로 우리는 "한 모듈"에서 "다른 한 모듈에있는 Presenter"에 정보를 보내기로 결정했다. (이것이 최대한 VIPER 컴포넌트를 망가뜨리지 않고 사용할 수 있는 가장 좋은 방법이라 생각했다.)
Passing Data
코드에선 어떤걸 의미할까?
SpeakerDetails 모듈은 SessionList wireframe에서 불려진 클래스 메소드를 기반으로 초기화된다. 그러므로 SpeakerDetails의 Presenter 메소드인 위 메소드는 유저가 어떤 세션을 선택했는지 알고 있어야 한다.
VIPER, Entity와 Core Data
우선 우리는 Core Data Stack을 만들기로 결정했다. 왜 이 작업은 외부 라이브러리를 사용하지 않았냐고 물어본다면, 우리는 Persistence Store의 컨트롤을 자유자재로 하고 싶었기 때문이다.
우리의 Core Data Stack은 Object Context가 두가지가 있다. 하나는 메인 쓰레드용이고 하나는 백그라운드 쓰레드용이다. 둘 다 같은 Persistent Store Coordinator에 연결되어, 각 context는 서로 독립적으로 동작한다. 저장이 완료되었다는 신호(did-save notification)와 함께 변경사항들이 맞바꿔진다.
CoreData Schema
Note : 이 아이디어는 Advanced Core Data라는 책으로부터 얻었는데, Core Data Stack을 많은 선택지와 함께 어떻게 세팅할 수 있는지 알려준다. 우리는 강력히 이 책을 추천한다.
그러나 Entity는 어떨까?
Entity들은 CoreData의 NSManagedObject 인스턴스가 아닌 VIPER 컴포넌트에 의해 주고 받아진다. Managed Object는 local manager들 에서만 접근이 가능하다. 이것들은 Entity로 바꾸거나 Interactor로 보낸다.
CoreData Convert To Entity
왜 NSManagedObject들을 보내지 않을까? 그 이유는 우리 앱은 데이터 모델과 데이터 레이어가 분리되있기 때문이다. 이 방법에서는 우리는 CoreData를 data store layer로 따로 빼어 놓았다.
어떤 데이터 타입으로 Entity를 표현할까? 우리는 모두 구조체(Struct)로 만들었다.
위 구조체는 알기쉽고, 변경불가능하며 쓰레드-세이브(Thread-Safe)하여 완벽하다 😃
의존성 주입(Dependency Injection)
VIPER 아키텍처를 사용하는 것은 의존성 주입(dependency injection)을 적용해볼 수 있는 기회를 제공한다. 예를들어 이 local manager 예제를 살펴보자.
VIPER 아키텍처를 사용할 때 모든 요소마다 DI를 사용하는 것은 좋은 습관이다. 우리는 유닛테스트라는 섹션에서 몇 예제와 함께 테스트할때 어떻게 이런 접근이 우리에게 도움이 되는지 보여줄 것이다.
실제 Wireframe이 무엇인가?
우리의 경우 wireframe은 두가지 기능을 가진다.
- 각 VIPER 컴포넌트들의 인스턴스들을 초기화하고 그들을 연결해준다.
- 두번째 기능은 다른 모듈에 navigate와 present 해준다.
이게 다다.
유닛 테스트
우리는 이전에 유닛테스트에 경험이 많이 없었다. 이 프로젝트에서 유닛테스트의 첫발을 내딛었는데, Interactor와 Presenter를 테스트하는 것 부터 시작했다. 그 이유는 Interactor에서 메인 비즈니스 로직을 가지고 있고 Presenter는 데이터를 보여주기 직전에 준비하는 작업을 하기 때문이다. 이 컴포넌트들은 다른 것들에 비해 좀 더 중요해 보였으나 오로지 우리 주관적인 의견임을 기억해주기 바란다.
유닛 테스트에서 사용된 라이브러리들이다.
VIPER에서 한 모듈의 모든 요소들은 각 기능별 유닛 테스트하기 적합하게 만들어져 있으며 이것들은 엄격히 분리되어 있다.
Unit Test Mock
위에서 볼 수 있듯이 컴포넌트들을 분리함으로써 우리는 Interactor의 테스트에만 집중할 수 있다. Interactor와 연결된 다른 요소는 단지 테스트를 위해 임의로 만든 목(mock)이다.
Services
Services가 무엇일까? 이것은 엄격한 VIPER 컴포넌트로부터 약간 분리되어있으며, 독립적인 컴포넌트라 할 수 있다. Service는 여기에서 언급되었으며, 이것이 반드시 필요한 것은 아니나 굉장히 유용한데 특히 높은 결합력을 만들어준다. 한 Service는 여러 모듈에서 사용될 수 있다.
여기 그 기능의 예시이다.
- 캘린더를 다루는 것 (Calendar Service)
- 연락처를 다루는 것 (AddressBook Service)
- 키체인 관리 (Keychain Service)
- 사람들 서비스 (Person Service) - 사진과 같이 유저의 데이터 다운로딩을 관리한다. 이 경우 Service는 네트워크 요청을 위한 apiClient 인스턴스가 필요하다.
Service 동작
백엔드로부터 변화(갱신)들을 듣는(listening)것은 어떻게 다뤄지는가?
우리 앱에서는 자동으로 갱신되는 두개의 목록이 있다. 우리의 Synchronized Service 는 백그라운드에서 JSON responses를 다운받고 CoreData에 넣는 무거운 작업을 한다. 이 작업은 매 1분마다 하게된다.
notification을 보내기 위해 파라미터를 받는 save 메소드를 가진다.
갱신 플로우에서 다음 컴포넌트는 interactor 이다.
dataUpdated(notification:NSNotification)는 interactor로부터 override 되었다. 당신이 예상한대로 표준 VIPER 플로우가 되었다. interactor는 notification을 받은 후에 데이터를 위해 local data manager에게 그것을 물어본다. 그러면 새로운 데이터르 미리 프로세스하기 위해 presenter에 보낸다. 그리고 presenter는 view에 보내어 화면에 표시한다. 됐다!
VIPER 모듈 컴포넌트들 사이에 API들은 각 프로토콜 파일에 정의되므로 항상 양방향 소통이 가능하다. 끝나는 시점에 작업을 하기 위해 클로저(closure)를 사용하는 것은 interactor에서 presenter로 데이터를 넣는 중에 블락될 수 있다.
오히려 옛날의 delegate 패턴이 우리 업데이트 메커니즘에 더 잘 동작한다. presenter가 interactor에 데이터를 얻기 위해 물어볼 뿐만 아니라, 모든 업데이트 프로세스를 초기화 할 수 있다.
보기에 깔끔하고 딴딴해보이지 않는가?
그래서 언제 VIPER를 사용하고, 언제 사용하지 말아야할까?
항상 그렇듯 대답은 "상황에 따라 다르다”.
아래 다이어그램이 이 중요한 질문에 답변이 될 수 있길 바란다.
ViperOrNot
마지막 요약
VIPER 사용을 시작하는 것은 크나큰 도전일 수 있다. 특히 이 아키텍처를 처음 적용시키는 경우는 더더욱 그렇다. 우리는 여러분이 이 글을 읽고 많은 의문들이 사라졌기를 바란다.
우리의 경우 git flow로 pull request와 함께 연습해보았다. 모든 개발자들이 저장소에 동료가 어떤 것을 push 했는지 조심히 관찰할 수 있다면, 이것은 굉장히 유용했다. 만약 대부분의 사람들이 서로 이야기나 관찰 없이 그들만의 VIPER 버전을 만들어버리면 이것은 재앙이다. 그 즉시 바로 모여 브레인스토밍하고 모두가 사용할 수 있는 새로운 솔루션을 다같이 찾아봐야한다.
VIPER는 앱을 어떻게 만들지 개괄적인 결정을 한다. 우리는 당신에게 오픈 마인드를 가지고, 각 컴포넌트를 커스터마이징하고 최적화하는 것을 멈추지 마라고 조언하고싶다.
VIPER는 지속적인 개선이 필요하며, 우리는 새 프로젝트가 이전 처음 프로젝트보다 더 나은 경험을 할 수 있을거라 기대한다.
특히 프로젝트 첫 시작부분인 당신이 프로젝트 구조를 세팅하는 시점에서 아키텍처를 바꾸지 않는다면, 한 걸음 걸음이 악몽일 것이다. 아키텍처에서 하나의 실수가 더 많은 실수를 유발하게 할 수 있다. 이것이 수 많은 힘겨운 일거리가 만들어지는 이유이다.
마지막으로 VIPER는 유닛테스트에 경험이 없어도 이것을 구현하기 쉽도록 해준다. VIPER에게 감사하다.
흥미로운 자료들
iOS 아키텍처 관련 번역글