iOS

iOS) RIBs Architecture

snowe 2022. 1. 12. 22:53
https://github.com/uber/RIBs/wiki를 바탕으로 공부한 RIBs에 대해 정리를 하는 글입니다.

RIBs란?

RIBs는 Uber의 크로스 플랫폼 아키텍쳐 프레임워크입니다. RIBs는 다음과 같은 원칙들을 준수합니다.

 

Encourage Cross-Platform Collaboration

하나의 서비스를 기준으로 볼 때 Android, iOS 모두 개발적인 측면에서 복잡한 부분들은 대체로 비슷합니다. 이 점을 활용하여 RIBs는 Android와 iOS 모두 비슷한 개발 패턴들을 제공합니다. 이는 iOS, Android 팀 모두 하나의 단일 아키텍쳐를 서로 공유할 수 있음을 의미하는데 이를 통해 서로의 비즈니스 로직을 점검해줄 수 있다는 장점이 생깁니다(뭔가 아키텍쳐의 틀을 딱 정해두는 것은 장점이자 단점이 될 수 있을 것 같아요).

 

Minimize Global States and Decisions

전역에 걸친 변경들(Global state changes)은 개발자로 하여금 예기치 못한 행동을 유발할 수 있고, 그런 변경들이 가져올 수 있는 영향에 대해 개발자들이 완벽히 파악하기란 쉽지 않습니다. RIBs는 잘 독립된 각각의 RIB들의 상태를 deep hierarchy 내에서 캡슐화하여 그러한 이슈를 피할 수 있습니다.

 

Testability and Isolation

Class들은 독립적으로 생각되어야 하고 Unit Test가 쉬워야 합니다. 각각의 RIB 클래스들은 서로 다른 책임들을 갖습니다(Routing, Business Logic, View Logic, 다른 RIB 클래스의 생성 등). 더불어, 부모 RIB 로직은 자식 RIB로직과 분리되어 있습니다. 이것이 RIB 클래스들이 테스트가 쉽고 독립적으로 생각될 수 있는 이유입니다.

 

Tooling for Developer Productivity

Adopting non-trivial architectural patterns does not scale beyond small applications without robust tooling. 이건 뭔 느낌의 말인지는 알겠는데 한국어로 표현을 못하겠네요..허허

여기서 하고싶은 말은 다른 아키텍쳐를 사용해도 강력한 tooling 기능이 있어야 되는데 RIBs는 그걸 자체적으로 제공해서 좋다는 의미인 것 같아요. 코드 생성, 정적 분석 및 runtime integrations에 대한 IDE 툴링을 제공한다고 합니다.

 

Open-Closed Principle

개발자는 언제든지 현재 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 합니다.(새로운 기능을 추가하기 위해 기존 코드를 계속 건드려야하면 안좋긴 하겠죠) 이것은 RIBs를 사용하면 가능합니다. 예를들어, 부모 RIB을 거의 변경하지 않고 부모의 종속성이 필요한 복잡한 자식 RIB을 attach하거나 build할 수 있습니다.

 

Structured around Business Logic

앱의 비즈니스 로직 구조는 UI의 구조를 엄격하게 따를 필요는 없습니다. 예를 들어, 뷰의 성능과 애니메이션을 용이하게 하기 위해서, 뷰 계층 구조는 RIB의 구조보다 더 얕을 수 있습니다. 아니면 단일 기능 RIB이 각각 다른 위치에 나타나는 세개의 뷰들을 컨트롤 할 수도 있습니다.(...?)(Or, a single feature RIB may control the appearance of three views that appear at different places in the UI.)

 

Explicit Contracts

요구사항들은 compile-time safe contracts으로 선언되어야 합니다. 클래스 종속성과 순서(ordering) 종속성이 충족되지 않으면 클래스는 컴파일되지 않아야 합니다. 우리는 ReactiveX를 사용하여 순서 종속성을 나타내고, 유형 안전 종속성 주입(DI) 시스템을 사용하여 클래스 종속성을 나타내고, 많은 DI 범위를 사용하여 데이터 불변성 생성을 권장합니다.

 

음 몇개는 아직 읽어도 모르겠는 것들이 있는데.. 일단 계속 하다보면 이해가 될거라 믿습니다..!

 

 


 

RIB의 구조

아까 위에서 Testability and Isolation 부분에서 RIB의 클래스들은 각각 다른 책임을 갖는다고 했어요. 여기서는 그것과 관련된 내용을 다룹니다.

 

VIPER라는 아키텍쳐에서 발전된 형태가 RIBs이기 때문에 VIPER를 기존에 사용해보신 분들은 좀 더 이해가 빠르실 것 같아요. 저는 안써봤기 때문에 찬찬히 살펴보면

 

RIBs는 보통 아래와 같은 구성요소로 이루어져 있어요. 모든 구성 요소들은 자체 클래스로 구현됩니다. 그래서 각각 자신만의 책임을 갖는 셈이 되는거죠.

 

Interactor

  • Interactor는 비즈니스 로직이 포함됩니다.
  • Rx로 subscription을 수행하는 곳도 이 곳이고
  • 상태 변결에 대한 결정,
  • 무슨 데이터를 어디에 저장할 것인지,
  • 어떤 다른 RIBs가 자식으로서 attached되어야 하는지 등을 결정하게 됩니다.

Interactor에 의해 수행되는 모든 동작들은 반드시 자신의 life cycle 내에서 동작해야합니다.

RIBs에는 Interactor가 활성화 된 경우에만 비즈니스 로직이 실행되도록 tooling이 내장되어 있습니다. 그 결과, Interactor가 비활성화되는 시나리오는 방지되지만 subscriptions이 계속 발생하여 비즈니스 로직이나 UI상태에 원치 않는 업데이트가 발생합니다. 그래서 꼭 life cycle 내에서 동작할 수 있도록 해주어야합니다.

 

Router

Router는 Interactor가 주는걸 듣고(받고?) 그걸 자식 RIB에 attaching, detaching 행위로 변환합니다. Router는 다음과 같은 세가지 간단한 이유로 존재합니다.

 

1. Router는 Humble Objects의 역할을 하여, 하위 Interactor를 속이거나 그 존재에 대해 신경 쓸 필요없이 복잡한 Interactor 로직을 쉽게 테스트 할 수 있습니다

 

* Humble Object: 테스트 가능한 객체를 감싸는 wrapper로,  테스트하기 어려운 객체의 로직을 비용 효율적인 방식으로 가져오는 방법.

 

2. Router는 부모 interactor와 자식 interactor간에 추가 추상화 계층을 만듭니다. 이로 인해 interactor간의 동기 통신이 조금 더 어려워지고, RIBs간의 직접 연결 대신 reactive communication 채택이 권장됩니다. 

 

3. Router는 interactor에 의해 수행될 수 있는 단순하고 반복적인 라우팅 로직을 포함하고있습니다. 이 상용구 코드(boilerplate code)를 제외하면(Router로 뺌으로써) Interactor를 작게 유지하고 RIB이 제공하는 핵심 비즈니스 로직에 더 집중 할 수 있습니다. 

 

Builder

Builder의 책임은 모든 RIB의 구성 클래스와 각 RIB의 자식에 대한 빌더를 인스턴스화하는 것입니다.

 

Builder에서 클래스 생성 로직을 분리하면 iOS에서 모의 가능성(mockability)에 대한 지원이 추가괴고 나머지 RIB 코드는 DI구현의 세부 사항에 영향을 미치지 않습니다. Builder는 프로젝트에 사용된 DI 시스템을 인식해야 하는 RIB의 유일한 부분입니다. 다른 빌더를 구현하면 다른 DI 메커니즘을 사용하여 프로젝트에서 나머지 RIB 코드를 재사용할 수 있습니다.

 

Presenter

Presenter는 Business Model을 View Model로 혹은 그 반대로 변환하는 stateless 클래스입니다. View Model 변환 테스트를 용이하게 하는데에 사용될 수 있습니다. 하지만 종종 이러한 변환이 너무 사소하면 이 클래스를 만들지 않습니다. 그러한 경우에는 view model를 변환하는 것은 View(Controller)나 Interactor의 책임이 됩니다.

 

View(Controller)

View는 UI를 빌드하고 업데이트합니다.

  • UI컴포넌트들의 레이아웃을 잡고 인스턴스화 하는것,
  • user interaction을 처리하는 것,
  • UI 컴포넌트들을 받은 데이터로 채우는 것,
  • 애니메이션 등이 View가 하는 일에 포함됩니다.

View는 가능한 한 "벙어리"이도록 디자인되어 있습니다(가능한 한 뭔가 주도적으로 뭔가를 하기보다 시키는 것만 하라는 소리인듯). 그들은 그저 정보를 보여줍니다. 그래서 일반적으로, View는 unit test가 필요한 코드를 포함하지 않습니다.

 

Component

Component는 RIB 종속성을 관리하기 위해 사용됩니다. 애네는 RIB을 구성하는 다른 유닛들을 인스턴스화 하여 Builder를 돕습니다. 

  • Component는 RIB를 구축하는 데 필요한 외부 종속성에 대한 액세스를 제공하고,
  • RIB 자체에 의해 생성된 종속성을 소유하고 다른 RIB에서 이에 대한 액세스를 제어합니다.
  • 상위 RIB의 구성요소는 일반적으로 상위 RIB의 종속성에 대한 하위 액세스를 제공하기 위해 하위 RIB의 빌더에 주입됩니다.

 


 

RIB의 상태 관리

앱의 상태는 현재 RIB 트리에 연결된 RIB에 의해 대부분 관리되고 표현됩니다. 예를 들어, 사용자가 아래와 같은 트리를 갖는 차량 공유 앱을 사용한다고 했을 때 사용자가 타는 flow에 따라 앱은 RIB을 attach하고 detach합니다.

RIB은 그들의 범위 내에서만 상태 결정을 합니다. 예를 들어, LoggedIn RIB은 Requset 와 OnTrip 과 같은 상태간 전환에 대해서만 상태 결정을 합니다. 그 말은 즉슨, 우리가 OnTrip 화면에 들어간 뒤 어떻게 행동할 것인지에 대한 결정은 LoggedIn RIB에서는 전혀 하지 않는다는 것을 의미합니다.

 

RIB을 추가/삭제 함으로써 모든 상태를 저장할 수 있는 것은 아닙니다. 예를 들어, 사용자의 프로필 성정이 변경 될 때, 어떤 RIB도 attach 혹은 detach 되지 않습니다. 일반적으로 이 상태는 세부사항이 변경될 때 값을 다시 방출하는 불변 모델 스트림(streams of immutable models)에 저장됩니다. 예를 들어, 사용자의 이름은 LoggedIn 범위 내에 있는 ProfileDataStream 에 저장 될 수 있습니다. 오직 네트워크의 응답만이 이 Stream에 쓰기 권한으로 접근할 수 있습니다. 우리는 DI 그래프 아래로 이러한 스트림에 대한 읽기 액세스를 제공하는 인터페이스를 전달합니다.

 

RIB은 RIB 상태에 대해 단일 정보 소스를 강제하고 있지 않습니다. 이것은 React와 같은 독단적인 프레임워크가 이미 기본적으로 제공하는 것과 대조됩니다. 각 RIB의 컨텍스트 내에서 단방향 데이터 흐름을 촉진하는 패턴을 채택하도록 선택하거나 효율적인 플랫폼 애니메이션 프레임워크를 활용하기 위해 비즈니스 상태와 보기 상태가 일시적으로 분기되도록 할 수 있습니다.

 


 

RIB 간의 상호작용

Interactor가 비즈니스 로직 결정을 할 때, 또 다른 RIB에 completion과 같은 이벤트에 대해 알려주고, 데이터를 보내야 할 수도 있습니다. RIBs프레임워크에는 RIB 간에 데이터를 전달하는 단일 방법이 포함되어있지 않습니다. 그럼에도 불구하고, 몇몇의 일반적인 패턴들을 용이하게 하기 위해 만들어졌습니다.

 

일반적으로, 만약 커뮤니케이션이 하위 RIB으로 하향 전달되는 경우, 우리는 이 정보를 Rx Stream으로 전달합니다. 혹은 데이터가 하위 RIB의 build() 에 파라미터로서 포함될 수 있습니다. 이러한 경우, 이 파라미터는 하위 RIB의 life time동안 변하지 않습니다. 

 

 

 

만약 커뮤니케이션이 반대로 RIB 트리에서 상위 RIB의 Interactor로 전달된다면(상향통신), 하위 RIB보다(상위 RIB이) 더 오래 남아있을 수 있기 때문에 listener 인터페이스를 통해 커뮤케이션이 진행됩니다. 

 

상위 RIB 또는 DI 그래프의 일부 객체는 listener 인터페이스를 구현하고 DI 그래프에 배치하여 하위 RIB이 호출할 수 있도록 합니다.

 

부모(상위)가 자녀(하위)의 Rx 스트림을 직접 구독하도록 하는 대신 이 패턴을 사용하여 데이터를 위쪽으로 전달하면 몇 가지 이점이 있습니다. 메모리 누수를 방지하고 부모가 어떤 자식이 연결되어 있는지 모른 채 작성, 테스트 및 유지 관리할 수 있으며 자식 RIB를 연결/분리하는 데 필요한 의식의 양을 줄입니다. 이런 식으로 자식 RIB를 연결할 때 Rx 스트림이나 수신기를 등록 취소/재등록할 필요가 없습니다.

 

 

여기까지 인데..음..

감이 잡히는 것도 있고 아닌 것도 있지만.. 일단 튜토리얼도 4단계로 있으니까 계속 진행해보면서 이해해보겠습니다 :)