이번에는 RIBs/wiki 보면서 RIBs에 대해 배워보겠습니다.
RIBs 개념 정리
- RIBs는 크로스 플랫폼 아키텍처 프레임워크
- 프레임워크는 정해진 틀에 코드를 넣으면 시스템이 약속된 기능을 작동시켜주는건데, 이 개념에 맞는지는 좀 더 살펴봐야겠습니다.
- RIB을 작성하는 템플릿이 있지만 프로그래머가 직접 관계를 지정해줘야하는 면에서 라이브러리라고 볼 수 있는건 아닐까? 하는 생각은 들었습니다.
우버를 위해 이 프레임워크를 디자인 했을때, 다음 원칙을 고수함:
- 크로스 플랫폼 협력을 독려함
- iOS와 Android 앱에서 대부분의 복잡한 부분은 비슷함
- RIB는 iOS와 Android에 비슷한 개발 패턴을 제공함
- 의문점
- 아키텍처가 통일된다고 해서 서로의 코드를 공유하는 일이 있을까? 하는 의문이 들기는 합니다.
- 아키텍처가 달라서 문제가 되는 부분이 있을지 의문도 들었습니다.
- 모바일 팀 매니저 입장에서는 관리하기 편하겠다는 생각도 듭니다.
- 여러 플랫폼에서 동일한 아키텍처를 적용하는 시도를 아직 못해봤는데, 일단 한번 경험해보고 싶다는 생각이 들었습니다.
- 전역 상태와 결정을 최소화
- 전역 상태를 변경하시는 것은 예측하지 못한 행동을 불러일으킬 수 있음
- 계층화된 RIB들에 상태값을 캡슐화 하도록 함
- 테스트 및 격리
- 클래스는 유닛 테스트 하기 쉬워야 함
- 클랫스는 독립적으로 추론 가능해야 함
- 독립된 RIB는 고유한 책임을 가짐
- 부모 RIB 로직은 하위 RIB 로직으로 부터 대부분 분리됨
- 대부분? 예외로 안되는 경우도 있나보다
- 개발자 생산성을 위한 도구 제공
- 코드 생성
- 정적 분석
- 런타임 통합
- 이건 뭐지?
- 개방 폐쇄 원칙
- 개발자가 기존 코드 수정 없이 새로운 기능을 추가할 수 있어야 한다
- 부모 RIB에 의존성이 필요한 child RIB을 코드 변경 없이
attach
또는build
할 수 있다
- 비즈니스 로직을 중심으로 구조화
- 비즈니스 로직이 UI 구조를 반영할 필요는 없음
- 애니메이션 & View 퍼포먼스를 높이기 위해서 View 구조는 RIB 구조보다 얕은 계층 구조로 만들 수 있음
- 또는 하나의 RIB이 다른 여러 UI를 컨트롤 할 수 도 있음
- 의문점
- View에 비즈니스 로직을 가두는 구조가 아닌건 흥미로움
- 그러나 이런 케이스가 얼마나 있을까 하는 의구심도 생김
- 구체적인 예를 볼 수 있으면 좋겠습니다.
- 명시적 계약(Contracts)
- 요구사항들은 컴파일 타임 계약으로 명시되어야 함
- 클래스와 순서에 대한 의존성이 만족되지 않으면 클래스는 컴파일 되지 않아야 함
- 순서에 대한 의존성은 무엇일까? Ordering dependencies
- 순서 의존성(ordering dependency)을 표현하기 위해
ReactiveX
를 사용함- 이 부분 무슨말인지 하나도 모르겠음… 나중에 다시 읽어볼 것
We use ReactiveX to represent ordering dependencies, type safe dependency injection (DI) systems to represent class dependencies and many DI scopes to encourage the creation of data invariants.
- 이 부분 무슨말인지 하나도 모르겠음… 나중에 다시 읽어볼 것
RIB 구성 요소
VIPER 아키텍처를 사용하고 있었다면 RIB의 클래스들이 익숙할 것임
Interactor
- 비즈니스 로직이 담겨짐
- Rx subscription을 수행하는 곳
- 이건 뭔지 잘 모르겠음
- 상태 변경을 결정함
- 어떤 데이터를 어디에 저장할지 결정함
- 어떤 child RIB이 attach 되어야할지 결정함
Router
- Router는 Interactor의 Output을 듣고 자식 RIB을
attach
또는detach
하는 것으로 변환 시켜줌 - 아래 3가지 이유로 존재함
- Router는 Humble Object로 기능한다. Child interactor를 mock 하거나 신경쓰지 않고서, 복잡한 Interactor 로직을 테스트하기 편하게 하기 위한 역할.
Humble Object는 무엇일까?
- [https://martinfowler.com/bliki/HumbleObject.html](https://martinfowler.com/bliki/HumbleObject.html) - 테스트하기 어려운 객체를 만남 -> 로직을 테스트 가능한 코드로 이동시킴, 원래 객체는 `humble` 해짐 - ![humble object](/assets/2021/04/humble-object.png) - Router는 부모와 자식 Interactor간에 추가적인 추상 레이어를 만듦.
- 인터렉터 간 synchronous communication을 어렵게 만든다
- 직접 결합 대신 reactive communication을 채택하도록 장려함
- 무슨말인지 모르겠다… 실습 해봐야 체감할 수 있을 듯
- Router는 단순하고 반복적인 라우팅 로직을 포함함
- 그렇지 않았으면 Interactor에 구현되어야 하는 부분
- boilerplate code를 facotring out 하면 Interactor를 작게 유지하고 핵심 비즈니스 로직에 집중할 수 있음
- Router는 Humble Object로 기능한다. Child interactor를 mock 하거나 신경쓰지 않고서, 복잡한 Interactor 로직을 테스트하기 편하게 하기 위한 역할.
Builder
Builder은 RIB의 구성 클래스들을 생성하는 역할. 하위 RIB들의 builder도 생성해줌.
- 클래스 생성 로직을 분리하는 것
- iOS에서의 mockability를 향상 시킴
- 나머지 RIB 코드를 DI 구현의 세부사항과 무관하게 만듦
- DI 시스템을 인식해야하는 RIB의 유일한 부분
- 다른 DI 매커니즘을 통해서 기존의 RIB 코드를 재사용 가능하다
Presenter
- Presenter는 stateless 클래스.
- Business model을 View model로 변형시켜줌 (반대로도 가능)
- 반대로도 가능한게 좀 신선한데… 그런일이 있을까? 나중에 확인해봐야겠다
- 뷰모델 변환 테스트를 가능하게 함
- 하지만 이러한 변환은 너무 사소한 것이라서 Presenter 클래스가 필요없는 경우가 많음
- Presenter가 생략되면 view model로 변환하는 작업은 View(Controller) 또는 Interactor의 책임이 됩니다
View(Controller)
- View들은 UI를 빌드하고 업데이트 함
- UI 컴포넌트들을 생성하고 레이아웃, 유저 인터렉션 핸들링, 데이터로 UI 컴포넌트 채우기, 애니메이션을 포함함
- 가능한 기능이 없어야 함 (Views are designed to be as “dumb” as possible.)
- 정보를 보여주기만 해야 함
- 일반적으로, 유닛 테스트가 필요한 코드는 여기에 포함되지 않습니다
Component
- RIB dependencies를 관리하기 위해 사용됨
- RIB을 구성하는 다른 unit들을 생성해서 Builder들을 도와줌
- Components는 외부 의존성들에 접근할 수 있도록 해줌
- RIB 자체에서 생성된 종속성을 소유함
- 다른 RIB으로 부터 해당 종속성에 대한 접근을 제어함
- 보통 부모 RIB의 Component는 child RIB의 Builder로 주입된다. 자식 RIB에게 부모 RIB의 종속성에 접근할 수 있도록 하기 위해서
State Management
- 어플리케이션 상태는 현재 RIB 트리에 attach된 RIB으로 표시됨
- 예를들어, 차량 공유 앱에서 여러 생태를 거치면서, 앱은 다음과 같이 RIBs를 attach하고 detach 한다
- RIBs는 그들의 scope 안에서만 상태를 결정한다.
- 예를들어,
LoggedIn
RIB은Request
와OnTrip
같은 전환에만 상태 결정을 한다.
일부 State는 RIB 추가/제거를 통해 보관하지 않음
- 예를들어, 유저 프로필의 세팅을 변경할 때, 어떤 RIB도 attach 되거나 detach 되지 않음
- 일반적으로 불변 모델의 스트림 내부에 이 상태를 저장함. 정보가 변경될 때 값을 재방출함(re-emit).
- 예를들어, 사용자 이름은
LoggedIn
scope 안의ProfileDataStream
에 저장될 수 있음 - 네트워크 Response만 이 스트림에 대한 write 권한을 가짐
- 이러한 스트림들에 대한 read 접근에 인터페이스를 DI 그래프로 전달함
RIB state를 위해 single source of truth를 강제하지 않음
- React 같은 프레임워크와는 대조적
- 각 RIB의 컨텍스트 내에서 단반향 데이터 플로우 패턴을 선택하거나
- 효율적인 애니메이션을 위해 비즈니스 상태와 view 상태가 일시적으로 다른것을 허용해줄 수 있음
Communication Between RIBs
- Interactor가 비즈니스 로직을 결정할 때, 다른 RIB에 알려줘야 할 때도 있다.(event, completion, send data…)
- RIB 프레임워크는 RIB 간에 데이터를 전달하는 single way를 포함하지 않음
- 그럼에도 불구하고, 몇 가지 일반적인 패턴을 가능하게 만들어짐
- 만약 커뮤니케이션이 parent RIB의 Interactor로 올라가면, 커뮤니케이션은 listener 인터페이스를 통해 이루어진다.
- parent가 child보다 오래 존재할 수 있기 때문
- 이것도 이해 못했음
- parent RIB 또는 그것의 DI 그래프 안의 object는 listener 인터페이스를 구현한다.
- 그리고 listener 인터페이스를 그것의 DI 그래프에 위치 시킨다.
- children RIB이 호출할 수 있도록!
- parent가 children의 Rx stream을 직접 subscribe 하지 않고, 데이터를 위로 전달하는이 이 패턴을 통해 얻는 이점
- memroy leak 방지
- 어떤 children이 attach 되었는지 모르고 작성, 테스트, 유지할 수 있음
- child RIB을 attach/detach 할 때 생각해야하는 양을 줄여줌
- child RIB을 이러한 방식으로 attach 하면, Rx stream을 unregistered/re-registered 할 필요가 없음
다음 공부할 거리
- 튜토리얼 따라하기: https://github.com/uber/RIBs/wiki/iOS-Tutorial-1
- 의문이 생긴 부분들 튜토리얼 후에 다시 생각해보기