ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] Repository 패턴과 아키텍처feat.Riverpod
    Flutter/project 2024. 1. 14. 16:14
    로그인 회원가입 등 구현에 대해 이야기하기 전, 아키텍처에 관해 이야기를 먼저 해야 구현 부분을 매끄럽게 이어갈 수 있을거 같아 글을 쓰게 되었습니다. 아키텍처에 대해 진지하게 생각한 것은 한 백엔드 팀원이 들어오고 난 직후였습니다. 이전의 나는 강의의 내용을 조금씩 수정하며 내가 정확히 어떤 아키텍처를 사용하고 있는지도 모르고 개발을 하고 있었습니다. 그렇게 아키텍처에 대해 학습하고 프로젝트에 적용하는 과정을 거칠 때, 진심으로 개발다운 개발을 하고 있다는 느낌이 들었습니다. 오늘은 길었던 아키텍처 도입 여정에 대해 얘기해보도록 하겠습니다.

     

    * 이 글은 codewithAdrea의 riverpod, repository 관련 아티클을 참고하여 작성했습니다.

     

     

    Flutter에서 상태관리 라이브러리들은 동시에 아키텍처 패턴의 역할도 하기 때문에 처음 접하면 조금 혼란스러울 수도 있습니다. 저 역시도 아키텍처 구조와 상태관리 툴을 별개로 인식하고 공부를 했었다가 riverpod을 도입할 때 조금 난항을 겪기도 했었죠.

     

    오늘 소개할 리버팟 아키텍처에 사용되는 상태관리 패키지인 리버팟에 대해 먼저 알아보겠습니다

    GetX vs Provider vs BLoc

     

    flutter 아키텍처를 검색하면 항상 거론되는 오픈 아키텍처 라이브러리입니다.

    정확히 말하면 GetX와 Provider는 상태관리 라이브러리에 가깝고, BLoc는 아키텍처 라이브러리에 가깝죠.

    이러한 이유로 BloC의 아키텍쳐 + Provider의 상태관리를 조합 해 사용하는 경우도 있습니다.

     

    처음에는 간편하며 비교적 많은 기능을 제공하는 GetX를 쓰는 게 현명해 보일 수 있지만 경량화되고 유연한 만큼 프로젝트가 커질수록 트러블슈팅에 취약해집니다.

    반면 BLoC은 상태관리와 비즈니스 로직을 분리하기 위한 패턴을 사용하기 때문에 테스트 및 유지보수에는 강점이 있지만 cubit 등 생소한 개념을 익혀야 하고 보일러플레이트 코드가 발생한다는 단점이 존재합니다.

     

    Provider는 BLoC과 거의 흡사합니다. 하지만 BLoC보다 코드작성이 간편하며 BuildContext 없이도 객체를 불러올 수 있는 GetX보단 유연하지 못합니다. (저같은 경우는 내 프로젝트가 볼륨이 작진 않다고 판단해서 GetX는 제외하였고 Flutter개발은 혼자 해야 했기 때문에 러닝커브가 높은 BLoC보단 Provider를 사용하기로 결정했었습니다.)

     

     

    🤷🏻‍♂️ "그런데 웬 뜬금없이 Riverpod?"

     

    리버팟 공식문서를 보면 Provider에는 다음과 같은 단점이 있었다고 합니다.

    "UI가 업데이트될 때마다 비동기요청을 다시 실행해야 하는 것은 효율적이지 않기 때문에 로컬에 캐싱해야 하는 방법을 사용해야 한다."

     

    • 로컬에 있는 캐시를 관리하지 않으면 오래된 상태로 남을 수 있다. 이러한 캐시들을 사용할 때마다 오류와 로딩 상태를 처리해야 한다.
    • 영향을 받게 되는 기능들
      • pull to refresh
      • infinite lists / fetch as we scroll
      • search as we type
      • debouncing asynchronous requests
      • cancelling asynchronous requests when no-longer used
      • optimistic UIs
      • offline mode

     

    이런 문제점들은 Provider 패키지에서 개선하기 어려웠기 때문에 완전히 재작생해서 Riverpod이라는 새로운 라이브러리가 탄생하게 되었습니다. 리버팟은 반응형 캐싱 및 데이터바인딩 프레임 워크로 위의 단점을 보완해서 탄생했기 때문에 대게 프로바이더의 상위 호환 라이브러리라고 여겨집니다.

     

    프로바이더의 내부 철학과 개념은 동일하기 때문에 캐싱에 관해 보완된 리버팟을 쓰는 것이 가장 적절해 보였고 해외나 국내에서 인기가 좋아지면서 자연스럽게 써보고 싶다는 생각이 들었습니다. (코드팩토리님 강의에서도 리버팟을 사용한 것도 한몫했습니다😎)

     

    * Riverpod에 대해 학습한 내용은 추후에 포스팅할 예정입니다. 

     

    도대체 Flutter에서 지향하는 아키텍처 패턴이 뭔데?

    아키텍처 패턴: 주어진 문맥 안에서 소프트웨어 아키텍처의 공통적인 발생 문제에 대한 일반적인, 재사용 가능한 해결책을 의미

     

    *출처: 위키백과

     

    GetX와 Provider, BLoC는 아키텍쳐 패턴으로 MVVM을 사용합니다.

    • MVVM
      • Model: 비즈니스 로직과 데이터를 책임지며 독립적으로 존재한다. 외부 dataSource와 교류하거나 데이터를 가공하는 부분이다.
      • View: 사용자 인터페이스를 담당하며 UI를 구성하고 입력 등을 받는 부분이다.
      • ViewModel: view와 model의 중간에 위치하며 비지니스 로직을 포함하고 view에 필요한 데이터를 제공한다. view에 의존적이지 않으며 데이터 바인딩 등을 통해 view와 소통한다.

     

    MVVM 패턴

     

     

    MVVM의 가장 큰 특징은 뷰와 로직(뷰모델)이 분리되어 있고 뷰가 뷰모델에 의존적이란 것입니다.

    BLoC의 목적이 '상태관리와 비즈니스 로직을 효과적으로 분리'하기 위함이기때문에 MVVM 패턴을 사용한 것도 그 이유중에 하나이며,

    안드로이드 공식문서에서도 권장 아키텍처로 MVVM을 소개하기때문에 모바일 환경에서 가장 적합한 패턴이라고 짐작할수있죠.

     

    플러터에서 지향하는 아키텍쳐가 MVVM인지는 모르겠으나 기본적으로 상태관리 패키지를 쓰는 이유가 비지니스 로직과 상태, 그리고 view를 분리하기 위함이라는 것을 인지만 하고있어도 개발 방향을 잡는데 도움이 되었습니다.

     

     

    아키텍처와 디자인 패턴

     

    아키텍처 패턴은 프로젝트의 뼈대를 잡기 위한 것이라면 디자인패턴은 근육을 붙이는 것과 같습니다. 이두와 전완근에 구성되는 근육의 모양과 근질이 다른 것처럼 한 프로젝트에는 특정 상황에 따라 적절한 디자인패턴을 가져다 쓰기 때문에 다수의 디자인패턴이 사용되죠.

    • 대표적인 디자인 패턴
      • 싱글톤 패턴: 전역적으로 하나의 인스턴스만 갖고 있으며, 자원의 공유나 설정 값과 같이 하나의 인스턴스로 관리하는 경우에 사용
      • 옵저버 패턴: 객체 간의 일대다 의존성을 정의해 한 객체의 상태가 변경되면 종속 객체들이 자동으로 알림을 받는 패턴
      • 팩토리 메서드 패턴: 객채 생성을 서브클래스에서 처리하는 패턴으로 상위 클래스에서 인스턴스를 생성하는 메서드를 선언 후, 서브클래스에서 구체적인 생성 방법을 결정한다.

     

    아키텍처 패턴은 디자인 패턴을 포함하는 개념이기때문에 디자인 패턴을 아키텍처로 가져가는 경우도 있습니다.

    하지만 오늘날 인기 있는 소프트웨어 개발 방법 중에 하나인 TDD(Test-Driven Development)에 가깝게 프로젝트를 구성하고싶다면 모든 코드 흐름이 하나의 인스턴스를 전역으로 공유하는 싱글톤으로 구성되게 해선 안됩니다. 만약 테스트 간의 상태가 공유된다면 특정 테스트에서 변경된 상태가 다른 테스트에 영향을 줄 수 있기 때문입니다.

    또한 특정 순서대로 실행되어야 하는 테스트의 경우 싱글톤의 상태에 따라 결과가 달라질 수 있습니다.

    이를 두고 테스트 간의 '의존성이 높다'라고 표현하는데 테스트간의 의존성이 높다면 안타깝지만 TDD와 멀어지게 됩니다.

     

    TDD는 단위 테스트를 강조하므로 인스턴스 생성을 테스트가 가능하도록 분리하거나 의존성 주입(dependency injection)을 활용해  필요시 외부에서 주입받아 사용하는 방법 등이 필요합니다.

     

    플러터에선 의존성 주입에 가장 흔하게 쓰이는 패키지로 get_it이라고 있습니다. 이외에도 porvider, get 등이 있지만 Flutter 업계의 저명한 개발자, Andrea Bizzotto는 Riverpod을 통해 계층 간에 의존성을 주입하며 수평적으로 기능이 확장되는 repository 패턴을 도입한 Riverpod 아키텍처를 만들었습니다. (Rémi가 인정한 공식 아키텍처는 아님) 

    *Rémi: 리버팟 개발자

     

     

     Repository pattern을 품은 Riverpod 아키텍처

     

     

     

    클린아키텍쳐와 안드로이드 아키텍쳐

     

    안드레아의 레포지토리 패턴 다이어그램, 이미지 출처: codewithandrea

     

    안드레아가 주장하는 레포지토리패턴의 다이어그램을 보면 클린 아키텍처와 많이 유사합니다. 실제로 레포지토리의 레이어는 클린아키텍쳐의 레이어와 거의 1대1로 대응되며 안드로이드 앱 아키텍쳐와 비슷한 형태를 띠고 있습니다.

     

     

    • Presentation Layer
      • 흔히 UI레이어라고 말한다. UI는 애플리케이션의 데이터를 표시하고 사용자 상호작용 또는 외부 입력으로 데이터가 변할 때 변경사항을 시각적으로 나타내는 역할을 한다.
      • Widget: 화면에 표시할 데이터를 UI로 표현한다.
      • Controller: 비동기 데이터 변경을 수행하고 위젯의 상태를 관리한다.
      • States: 위젯의 상태이며 데이터 변형에 따라 상태도 변동된다.
    • Application Layer
      • 프레젠테이션 레이어의 컨트롤러와 데이터 레이어의 레포지토리 사이에 존재하며 중재자의 역할을 한다.
      • Service: 비즈니스 로직을 정리한 클래스
      • 프레젠테이션 레이어에는 여러 컨트롤러가 존재하고, 데이터레이어에도 여러 레포지토리가 존재한다.
        만약 service 없이 컨트롤러나 레포지토리에 직접 로직을 작성하면 해당 로직은 특정 레이어에 의존성이 높아져 유지 보수 및 테스트가 어려워지기 때문에 service는 레포지토리 패턴에서 중요한 역할을 한다.
    • Domain Layer
      • 데이터 레이어에서 제공되는 데이터형식을 애플리케이션별 모델 클래스를 정의하는 것들의 집합체이다.
      • Model: 데이터의 연산에 관련된 메서드와 직렬화 로직이 정의되어 있다.(ex.fromJson, toJson)
    • Data Layer
      • Repository: 외부 API나 도메인 서버 API 등 네트워크 통신을 시도하고 에러를 처리하는 코드가 정의된 클래스다.
      • DTO: 통신할 때 전송되는 구조화되지 않은 데이터를 의미한다.(ex.Json)
      • DataSource:   DTO를 반환하는 외부 API와 도메인 서버, 외부에 종속된 패키지들을 의미한다. DTO처럼 코드베이스로 존재하지 않는다.
      • dto와 datasource는 가시적으로 존재하지않기때문에 단순히 repository의 집합체라고 볼수있다.

     

    그럼 실제로 어떤 식으로 아키텍처가 적용되는지 예시와 함께 알아보겠습니다.

     

    홈화면에서 광고 중인 가게를 클릭하면 해당 가게의 상세화면으로 이동하는 기능

     

     

    📌 DataLayer

     

    다음 화면을 자세히 보면 유저에게 보여줄 데이터는 1. 매장 소개 2. 매장 메뉴 2. 장바구니에 담은 아이템 개수임을 알 수 있습니다.

    매장 소개와 매장메뉴 조회 api는 'store repository'에 작성하고, 장바구니 조회 api는 'cart repository'에 작성할 수 있습니다.

     

     

     

    📌 Domain Layer

     

    데이터 레이어에서 patch요청 등을 할 때는 model 클래스에 작성한 toJson으로 Json객체를 만들어 전송하고 get요청을 할때는 response.data를 model 클래스에 작성한 fromJson을 사용해 받습니다.

    도메인 레이어에 model들이 정의되긴 하지만 respone, request에 사용되기 때문에 레포지토리 클래스의 메서드가 실행될 때 객체가 생성되죠.

     

     

     

    📌 Application Layer

     

    가게 상세화면에 필요한 데이터들을 불러오는 레포지토리 인스턴스를 주입받아 서비스를 구성합니다.

     

     

     

    📌 Presentation Layer

     

    불러온 데이터들은 각각의 컨트롤러의 상태로서 관리되며, 컨트롤러와 상태는 1:1 관계지만 상태와 관련된 위젯은 다수가 될 수 있습니다.

     

     

    ✍🏼 장점

     

    안드레아의 리버팟 아키텍쳐는 서버 로직과 철저히 분리하는 것에 초점을 두고있다. 마치 마트에 진열된 api들을 보면서 지금 화면 구성에 필요한 api의 모음만 골라와서 적용하는 것과 같습니다.

    인터페이스와 도메인 로직을 분리하고, 도메인 로직은 데이터 엑세스 로직과 분리되어있어 필요할 때 의존성을 레이어간에 주입받는 형태로 구성되어있다. 이렇게 철저히 분리된 계층덕분에 테스트 주도개발(TDD)의 "테스트 가능한 코드"조건을 만족하며, 도메인 모델은 비지니스 로직에만 집중하고, 데이터베이스와의 통신은 레포지토리가 담당함으로써 도메인 주도 설계(DDD)에 가까워집니다.

     

     

    ✍🏼 단점

     

    레포지토리 패턴은 수평으로 확장되는 특징과 철저한 레이어 및 관심사 분리로 인해 코드가 길어지고 파일이 많아지는 등 전형적인 안티패턴의 형태라고 볼 수있습니다.

     

    또한 의존성 주입 등 학습곡선도 높기때문에 작은 규모의 어플리케이션에서 도입하는 것은 오히려 오버헤드로 여겨질수있다.

     

     


     

    실제 프로젝트를 진행할때 프레젠테이션에서 어떤 서비스를 써야할 지, 한 컨트롤러에서 여러 서비스를 사용해도 되는지, 계층간에 분리는 잘 되고 있는지 등 신경 쓸 게 정말 많았습니다. 나중에 관심사를 넘나드는 복합적인 기능이 요구되는 때가 있었는데 이때 아키텍쳐 설계 측면에서 굉장히 애를 먹었던 기억이 있습니다. 다음에는 오늘 소개한 리버팟 아키텍처를 어떻게 적용하는 지 실제 프로젝트와 함께 알아보도록 하겠습니다.

     

    긴글 읽어주셔서 감사합니다.

     

     

    [Flutter] 실제 Repository 패턴 프로젝트 구조 파헤치기#1 (Data, Domain)

    창업 아이템이 정해지고 나서 Flutter를 학습했기 때문에 프로젝트의 모든 과정이 새로웠다. 특히 아키텍처의 도입은 볼륨이 커질수록 생각만큼 매끄럽게 적용하기 어려웠다. 내가 겪었던 시행착

    nomal-dev.tistory.com

     

    이 글이 꼭 정답은 아닙니다. 잘못된 부분이나 부족한 부분을 알려주시면 학습 후 수정하겠습니다.

     

Designed by Tistory.