GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.
Uber's cross-platform mobile architecture framework. - GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.
github.com
RIBs는 Uber에서 만든 Cross Platform 아키텍쳐입니다.
→ 도입시 iOS와 Android가 동일한 아키텍쳐를 사용하여 플랫폼간 협업 및 비즈니스 로직 코드를 교차 검증할 수가 있다는 이점이 생깁니다.
RIBs는 여러 Riblet(리블렛, 하나의 기능단위. 이하 RIB)으로 구성되며 Router, Interactor, Builder의 약자로 RIB의 핵심 요소들을 의미합니다. 그 외에 View, Presenter, Component와 같은 요소도 있습니다.
RIBs의 구성
Builder
해당 RIB의 구성들(Router, Interactor, View, Component)을 생성하는 Factory로 DI를 정의하며, 자녀 RIB의 Builder도 인스턴스화 합니다. 각 Component들은 자신의 코드를 그대로 유지한 채 DI Framework(Swinject, Needle 등)을 Builder에서 알맞게 사용함으로써 Component들의 Mockability를 향상시켜줄 수도 있습니다. Builder는 A라는 RIB의 Interactor와 B라는 RIB의 Router를 생성시켜 C라는 RIB을 만드는 일도 가능하여 재사용성을 극대화시켜줍니다.
Component
부모 RIB의 Builder가 자식 RIB의 Builder에게 Component를 통해 Dependency Injection을 할 수 있게 해줍니다.
Interactor✨
비즈니스 로직을 담당합니다. Rx로 Subscribe를 하거나, State를 관리하거나, 데이터를 다루거나, 필요에 따라 Router에게 자식 RIB을 attach, detach할 것을 요청하는 등의 일을 수행합니다. 앱의 중심 로직입니다.
Router
부모 RIB의 Router는 자식 RIB을 attach, detach하여 RIBs의 논리적 트리를 만들어줍니다. 두 Interactor 간의 추가적 레이어로 Interactor가 비즈니스 로직에 집중할 수 있게 해주며, Mockability도 최대한 높여주어 복잡한 Interactor의 로직도 테스트하기 쉽게 해줍니다.
View(Controller)
UI Layout을 그리고 ViewModel을 통한 업데이트를 하거나 애니메이션 등을 처리합니다. Uber에서는 View를 그저 정보를 보여주기만 하도록 최대한 '바보처럼' 설계되었으니, 단위테스트가 필요한 로직을 넣지말 것을 이야기합니다.
Presenter
View로직이 생기면 Optional하게 필요해질 수 있는 요소로 Interactor와 View 사이에서 model의 변환을 담당합니다. Interactor에서 View로 넘어오는 model은 정보가 너무 많을수도, 부족할수도 있기 때문에 Interactor가 가지고 있는 비즈니스 모델을 ViewModel로 변환해주거나. 반대로 View에서 넘어오는 Action을 Interactor가 해석할 수 있도록 변환을 해줍니다. Presenter를 생략시 Interactor나 View가 그 역할을 해야합니다.
여기서 중요한 것은 대부분의 아키텍쳐들이 View를 필수요소로 가지고 있지만 RIBs에서는 필수요소가 아니라는 점입니다.
VIPER와 같은 다른 아키텍쳐들과 마찬가지로 각각의 Component들에 적절한 역할을 나누어 기능을 구현하고자 만든 것은 같지만,
RIBs만의 이점은 여기에 있습니다.
RIBs에서는 View가 필수요소가 아니기 때문에 Router, Interactor, Builder만으로 구성된 비즈니스 로직만을 위한 RIB노드를 만들 수 있습니다. 이는 RIBs는 View를 중심으로 트리가 만들어지는 것이 아닌 비즈니스 로직을 중심으로 State에 따른 트리가 만들어진다는 의미입니다.
State Tree
아래 예시는 'Multiplatform Architecture RIBs in Swift'영상에서 나온 타다 서비스의 State Tree입니다.
RIBs 아키텍쳐는 아래에서처럼 View에 의해서 State가 변하는 것이 아닌 비즈니스 로직에 의해 State가 변해야는 것을 강조하고 State 트리에 따라 앱의 흐름이 만들어집니다.
(View Tree보다 더 depth가 깊고 디테일합니다)
각각이 하나의 RIB이고 View가 있기도 하고 없기도 합니다.
Root RIB에서 시작하여
로그인 정보 저장 여부에 따라 LoggedOut RIB을 Attach하거나, Main RIB을 Attach하게 됩니다.
Home에서 호출(Request)을 누르면 Request RIB의 판단에 의해서 LocationEditor RIB을 생성해 Attach하고,
LocationEditor RIB에서 사용자가 출발지, 도착지의 위치를 모두 결정하고 나면,
경로를 확인하는 Confirm RIB이 만들어져 Attach되고, LocationEditor RIB이 Detach됩니다.
Confirm RIB 아래로 Coupon RIB, Card RIB이 Attach하게 됩니다.
아직 Request하기 전이기 때문에 상단에 Request RIB이 역할을 하고 있습니다.
사용자가 '호출하기'버튼을 누르면
Confirm RIB이 Detach되고, Refinement(위치조정) RIB이 Attach됩니다.
그리고 Refinement에서 '확인'을 누르면, Request RIB이 Detach되고 Ride RIB이 Attach됩니다.
RIDE RIB에선 View가 없고, 서버에서의 유저 State가 Pending State인지, Matched State인지, DroppedOff State인지를 받아서 각각의 RIB을 Attach, Detach해주는 역할을 합니다.
Scopes
오른쪽은 State가 아닌 View에 따른 tree 형태일 때입니다.
깊이가 얕아지고, MainView하나에서 관리해야하는 서브뷰들이 많아지게 되어 MainView가 복잡해지게 됩니다.
RIBs에서는 비즈니스 로직에 의해 State를 관리하고, 해당 State에 따라 Scope를 가두어서 관리를 할 때 더 쉬운 코드가 될 수 있도록 해줍니다.
왼쪽은 앱을 처음 진입했을 때 만들어진 아무런 State가 없는 상태의 Root RIB입니다.
로그인 정보(AuthToken, UserDTO)가 있다면 그 아래의 Main RIB을 attach하고 해당 정보를 넘겨줄 수 있고,
해당 Main RIB은 항상 AuthToken, UserDTO는 항상 있다고 가정하여(non-optional) 옵셔널 바인딩 등을 체크하지 않고 깔끔하게 코드가 작성해집니다.
// 기존 방식의 코드
class AuthenticateNetworkRequest: NetworkRequest {
private var authToken: String?
public func makeRequest() {
if let authToken = authToken {
addAuthToken(authToken)
}
super.makeRequest()
}
...
}
// Main RIB으로부터 주입되어 항상 존재하는 authToken으로 작성
class AuthenticateNetworkRequest: NetworkRequest {
private var authToken: String
public func makeRequest() {
addAuthToken(authToken)
super.makeRequest()
}
...
}
Ride 상태가 되어 Main RIB에 Ride RIB이 Attach된 상태.
이 상태가 되었다는 것은 Main에서 Ride 관련 객체를 Ride의 자식 RIB Tree에게 전달했다는 의미이고,
Ride Scope에 갖혀있는 모든 자식 RIB들은 Ride 객체가 있다는 가정하에 코드를 작성할 수 있게 됩니다.
// 기존 방식의 코드
public func refresh() {
if let ride = self.ride {
getRideStatus(ride.id)
} else {
getUserStatus(self.user!.id)
}
}
// 항상 존재하는 ride 객체로 작성
public func refresh() {
getRideStatus(ride.id)
}
RIB들의 Communications
Scope에 따라 개발을 하다보면
어떤 Interactor가 다른 RIB에게 이벤트를 보내야할 때는 어떻게 할까요?
위 예시는 Main RIB의 서브 RIB인 Home RIB과 Request RIB 사이에서 커뮤니케이션이 필요하게 된 상황입니다.
저런 식의 코드를 작성하기 보다는 RIBs에서 제안하는 Communication 방식이 있습니다.
크게 Upwards, Downwards Communication이 있습니다.
1. Upwards Communication
Upwards Communication의 방법으로는 Listener interface를 사용하도록 합니다.
자신(Home)의 Listener interface를 선언해놓고, 그 주입받은 Listener를 동작시킴으로써 communication을 합니다.
RIB Tree상 부모 RIB은 자식 RIB을 알지만, 자식 RIB은 부모 RIB을 모릅니다.
자식 RIB이 부모 RIB의 특정 기능을 직접 호출하지 못하게 막아놓음으로써, 부모와 자식간의 coupling을 최대한 없앨 수 있습니다.
2. Downwards Communication
자식 RIB이 여러개가 있을 수 있기 때문에 Downwards Communication은 Listener interface로 구현할 수가 없습니다.
위에서 다뤘던 Scope로 주입받는 것이 중요한데, Downwards로는 주입을 받을 수가 없습니다. Property로 Listener를 선언해서 해당 리스너를 호출할 수도 있지만 이는 RIBs 아키텍쳐가 추구하는 방향이 아닙니다.
그래서 Downwards로는 Rx를 이용해서 전달합니다. 시퀀스로 이벤트를 전달하고 방출된 이벤트를 받는 방식입니다.
(RIBs를 fork해서 만든 ModernRIBs는 Rx대신 Combine을 사용하도록 되어있습니다)
그러면 Main은 Location이벤트를 만들어 계속 방출하고, 자식 RIB(Home, Request) 중 필요하면 이를 Subscribe해서 사용합니다.
RIBs는 이렇게 Scope를 가두고, RIB간의 Communication 방식을 제한하는 방법으로 공동작업 결과를 매우 끌어올려줍니다.
각 RIB은 의존성만 제대로 주입이 되어 있다면, 각각 독립적으로 코드를 작성할 수 있기 때문입니다.
이는 View가 포함되어 있는 모듈화도 할 수 있게 해주어 모듈화를 극대화할 수 있게 환경을 만들어줍니다.