Programming/Android

[Android Roadmap] Part4: Design Patterns and Architecture (1/2)

코딩뽀시래기 2023. 2. 21. 18:11
728x90

📌 Part4: Design Patterns and Architecture

 

Design Patterns and Architecture: The Android Developer Roadmap - Part 4

In this post, you’ll learn about design patterns, architecture, and essential solutions for Android and how they have evolved over Android’s long history.

getstream.io


 

1️⃣ Design Patterns

- 반복되고 일반적인 소프트웨어 문제를 해결하기 위한 재사용 가능한 솔루션

- 디자인 패턴은 어떤 문제를 해결하느냐에 따라 Creational 패턴, Behavioral 패턴, Concurrency 패턴으로 분류 가능

 

- Android 개발에서는 Dependency Injection  Observer 패턴과 같은 Android 플랫폼의 일반적인 문제를 해결하고 리소스 수명 주기를 관리하기 위해 자주 사용되는 대표적인 디자인 패턴들이 있음

- Builder 패턴, Factory Method 패턴, Singleton 패턴과 같은 Creational 패턴들은 다양한 Android framework에서 이미 많이 활용되고 있음.

 

🔴 Dependency Injection (의존성 주입)

- 모던 Android 개발에서 가장 인기 있는 패턴 중 하나이며, 클래스의 인스턴스를 생성해야 하는 의무를 외부로 이전하는 패턴

- 객체 생성 의무를 외부로 이전함으로써 클래스는 서로 종속성을 가질 필요가 없음. 따라서 클래스 간에 느슨하게 결합된 종속성을 설계할 수 있음.

 

< 의존성 주입을 통해 얻을 수 있는 이점 >

  • 객체 생성을 위한 보일러 플레이트 코드를 줄일 수 있습니다.
  • 클래스 간의 결합이 느슨하여 단위 테스트를 쉽게 작성할 수 있습니다.
  • 클래스 재사용성을 향상시킵니다.
  • 결과적으로 코드 유지 보수성을 향상시킵니다.

 

1) Hilt

- Dagger 위에서 동작하는 Android 전용 컴파일 타임 의존성 주입 라이브러리

- 표준 Android Dagger 컴포넌트들을 생성하고 Dagger-Android 라이브러리에 비해 필요한 보일러 플레이트 코드의 양을 줄임.

- Google에서 Hilt와 관련하여 ViewModel, Jetpack Compose, Navigation  WorkManager와의 호환성을 제공

- 현재 포스트 작성 시점에서 모던 Android 개발을 위해 적극 권장되는 의존성 주입 라이브러리

 

2) Dagger

- javax.inject annotation (JSR 330)을 기반으로 하는 컴파일 타임 의존성 주입 라이브러리

- Dagger를 사용하여 Android 프로젝트에서 의존성 주입을 구성할 수 있지만 Dagger는 Android 프로젝트만을 위한 라이브러리가 아니기 때문에, 많은 추가 셋업 비용이 필요

- 셋업 비용을 크게 줄이려면 Hilt 또는 Dagger-Android를 사용하는 것을 권장

 

3) Koin

- Kotlin 프로젝트에서 인기 있는 의존성 주입(엄밀히 말하자면 service locator pattern) 라이브러리

- 사용하기 쉽고 셋업 비용이 낮아서 Dagger 및 Hilt의 사용이나 도입이 당장 어려우신분들께 권장

- 런타임에 의존성을 생성하고 제공한다는 부분에서 대규모 프로젝트일 수록 컴파일 타임 기반 라이브러리인 Hilt 및 Dagger 보다 성능이 제한

 

🟠 Observer Pattern

- 관찰자에게 상태 변경을 자동으로 알리는 구독 메커니즘을 활용한 behavioral design 패턴

- Android 개발에서 컴포넌트 간에 느슨하게 결합된 아키텍처를 구축하기 위해 가장 자주 사용되는 패턴 중 하나

- 독립적인 Android 컴포넌트 간의 통신과 같은 Android 플랫폼에서의 제약을 극복하는 데 활용할 수 있음

 

1) LiveData

- 안드로이드 수명 주기를 인식하고 스레드로부터 안전한 데이터 홀더 옵저버 패턴

- LiveData의 관찰자는 Android 수명 주기에 바인딩되어 있으므로, 관찰자를 수동으로 구독 취소할 필요가 없으며 수명 주기가 활성화되지 않은 경우 방출되는 데이터를 구독하지 않음.

- 결과적으로 예측할 수 없고 식별하기 어려운 메모리 누수를 별다른 처리 없이 쉽게 방지할 수 있음

- LiveData-ktx 라이브러리는 유용한 연산자를 제공하고 Data Binding  Room 호환성을 지원

- 최신 Android 개발에서는 코루틴이 광범위하게 채택되는 양상을 보이고 있기 때문에 LiveData보다 Kotlin의 Flow를 선호하고 있음

 

2) Kotlin Flows

- Kotlin의 언어 수준에서 지원되는 비동기 및 non-blocking 솔루션

- Asynchronous Flow 코루틴과 함께 동작하며 sequences와 유사한 cold streams 비동기 솔루션

- Flows는 Transform 연산자, Flattening 연산자  flowOn 연산자와 같은 유용한 연산자를 제공하여 다양한 비동기 연산을 수행할 수 있도록 함.

- Android에서 StateFlow  SharedFlow를 활용하여 state를 holding하고 관찰 가능한 flow를 구현하고 여러 구독자에게 값을 내보낼 수 있음

 

3) RxKotlin(RxJava)

- 옵저버 패턴, iterator 패턴 및 함수형 프로그래밍 등을 한꺼번에 제공하는 ReactiveX에서 유래

- 관찰 가능한 시퀀스를 사용하여 비동기 및 이벤트 기반 프로그램을 구성할 수 있는 많은 유용한 연산자를 제공

- 저수준 스레딩, 동기화 및 스레드 안전성과 같이 골치아픈 동시성 문제를 쉽게 해결 가능

- RxAndroid와 같은 Android를 위한 유용한 솔루션이 많이 있음.

- RxKotlin에는 많은 연산자가 포함되어 있어 새로 접하신분들께는 상당히 복잡할 수 있음.

 

🟡 Repository Pattern

- 데이터의 추상화를 제공하여 도메인과 데이터를 중재하는 소프트웨어 접근 방식인 Domain-driven design에서 비롯됨

- 리젠테이션 레이어는 로컬 데이터베이스에서 데이터를 쿼리하고 네트워크에서 원격 데이터를 가져오는 것과 같은 비즈니스 로직에 근접한 인터페이스와 함께 간단한 추상화를 사용

- 실제 구현 클래스는 무거운 작업을 수행하고 도메인 관련 작업을 실행

- 리파지토리는 도메인과 관련된 실행 가능한 함수 집합체를 개념적으로 캡슐화하고, 다른 계층에 보다 객체 지향적인 측면을 제공

 

 

- 모던 Android 개발에서 데이터 계층은 다른 계층에 공개 인터페이스로 노출되고 단일 정보 출처 원칙(single source-of-truth)을 따르는 리파지토리들로 구성됨.

- 다른 레이어들은 Kotlin의 Flow 또는 LiveData와 같은 스트림으로 도메인 데이터를 관찰할 수 있으며 신뢰할 수 있는 소스(source of truth)를 보장받을 수 있음

 


2️⃣ Architecture

- 아키텍처는 프로젝트의 전체 코드 복잡성과 유지 보수 비용을 결정하기 때문에 모든 확장 가능한 소프트웨어 개발에 있어서 필수적으로 고려되어야 하는 개념

- 프로젝트의 함수 수가 증가함에 따라 코드 라인과 코드 응집력도 그에 따라 증가

- 애플리케이션 아키텍처는 프로젝트의 복잡성, 확장성 및 견고성에 광범위하게 영향을 미치며 테스트를 더 쉽게 만듦

- 각 계층 간의 경계를 정의하여 책임을 명확하게 정의하고 전담 역할로 모듈화하여 각 책임을 분리 가능

 

🔴 MVVM

1) View

- 사용자가 화면에서 보는 사용자 인터페이스 구성을 담당

- View는 TextView, Button 또는 Jetpack Compose UI와 같은 UI 컴포넌트를 포함하는 Android 컴포넌트들로 구성

- UI 컴포넌트는 ViewModel에 대한 사용자 이벤트를 트리거하고 ViewModel에서 데이터 또는 UI 상태를 관찰하여 UI 화면을 구성

- 이상적으로 View에는 리스너와 같은 화면 및 사용자 상호 작용을 나타내는 UI 로직만 포함되고 비즈니스 로직은 포함되지 않음.

 

2) ViewModel

- View에 대한 종속성이 없는 독립적인 구성 요소

- Model의 비즈니스 데이터 또는 UI 상태를 보유하여 UI 컴포넌트로 전파

- 일반적으로 ViewModel과 Model 사이에는 여러(일대다) 관계가 있으며 ViewModel은 데이터 변경 사항을 도메인 데이터 또는 UI 상태로 View에 알림.

- 최신 Android 개발에서 Google은 개발자가 비즈니스 데이터를 쉽게 유지하고 화면 전환이 발생하는 동안 상태를 유지하는 데 도움이 되는 ViewModel 라이브러리를 사용할 것을 권장

 

3) Model

- 일반적으로 비즈니스 로직, 복잡한 계산 작업 및 유효성 검사 로직을 포함하는 앱의 도메인/데이터 모델을 캡슐화

- 모델 클래스는 일반적으로 실행 가능한 도메인 함수의 집합체 같은 데이터 접근을 캡슐화하는 리파지토리의 remote service 및 로컬 데이터베이스와 함께 사용

- 리파지토리는 앱 전반에서 활용되는 데이터에 대한 여러 데이터 소스 및 불변성을 제공하고 신뢰할 수 있는 단일 소스(single source of truth)를 보장

 

🟠 MVI

- MVI(Model-View-Intent)는 Jetpack Compose의 선언적 프로그래밍 도입으로인해 모던 Android 개발에서 인기 있는 아키텍처 중 하나

- 다른 계층에 불변 상태를 제공하고 사용자 작업의 결과를 나타내며 UI 화면을 구성하는 상태의 단방향 및 불변성을 제공하는 단일 진실 원칙(single source of truth)에 중점

- MVI는 상태 관리 메커니즘은 MVP 또는 MVVM과 같은 다른 패턴과 함께 구현할 수 있음.

- MVI 아키텍처는 아키텍처 설계에 따라 Presenter 또는 ViewModel 개념을 가져올 수 있음.

 

1) Intent

- 사용자 작업(버튼 클릭 이벤트와 같은 UI 이벤트)을 처리하는 인터페이스 및 기능에 대한 정의

- 함수는 UI 이벤트를 Model의 인터페이스로 변환하고 결과를 조작할 수 있도록 Model에 전달

- Model 함수를 수행하려는 '의도 (Intent)'가 있다고 말할 수 있음.

 

2) Model

- MVI의 Model 정의는 MVP 및 MVVM과 완전히 다릅니다. MVI에서 Model은 Intent의 출력을 가져와 View에서 렌더링할 수 있는 UI state로 조작하는 기능적 메커니즘

- state는 변경할 수 없으며, 단일 소스(single source of truth) 및 단방향 데이터 흐름 (unidirectional data flow)을 따르는 비즈니스 로직에서 가져옴

 

3) View

- MVI에서 View는 MVP 및 MVVM과 동일한 역할을 하며 화면과 리스너와 같은 사용자 상호 작용을 나타내며 비즈니스 로직을 포함하지 않음

- 다른 패턴과의 구현에서 가장 큰 차이점 중 하나는 MVI가 단방향 데이터 흐름을 보장하므로 View는 Model에서 오는 UI 상태에 따라 UI 컴포넌트를 렌더링한다는 것임.

 

🟡 Clean Architecture

- Dagger와 같은 종속성 주입 솔루션과 다중 모듈 프로젝트 환경이 도입된 이후 모던 Android 개발에서 널리 사용되고 있음

- 이 이론은 MVP, MVVM 및 MVI와 같은 다른 아키텍처와 함께 사용할 수 있음

- 모듈 분리, 재사용성 증가, 확장성 향상, 단위 테스트 사례 작성 용이성과 같은 이점을 제공

- 복잡한 비즈니스 로직이 필요하지 않은 소규모~중규모 프로젝트를 (혹은 프로젝트 성격에 따라 도입이 반드시 필요하지 않은 경우) 진행하는 경우 이 아키텍처가 오버 엔지니어링이 수 있으므로 아키텍처가 프로젝트에 이점을 제공하는지 반드시 사전 조사해야 함.

 

< 소프트웨어 설계 원칙 >

  • Single Responsibility (단일 책임 원칙)
    • 클래스나 모듈과 같은 각 소프트웨어 구성 요소는 변경해야 할 이유가 하나만 있어야 함.
    • 즉, 동일한 구성 요소, 클래스 또는 모듈에서 관련 없는 기능을 설계하면 코드의 목적을 이해하기 어렵고 목적이 명확하지 않게 됨

 

  • Open/Closed (개방-폐쇄의 원칙)
    • 지점을 중단하거나 용도를 수정하지 않고(수정을 위해 폐쇄) 구성 요소의 기능을 확장할 수 있어야 함. (확장을 위해 개방).

 

  • Liskov Substitution (리스코프 치환 원칙)
    • 확장 클래스는 상위 클래스를 대체할 수 있어야 함.
    • 즉, 상위 클래스에는 확장할 간결한 목적을 제공하는 최소한의 인터페이스가 있어야 하며 하위 클래스는 상위 클래스의 모든 추상화를 구현해야 함.

 

  • Interface Segregation (인터페이스 분리 원칙)
    • 이름으로 추정할 수 있듯이 원자 중심 원칙이며 단일 책임 및 Liskov 대체 원칙과 연결될 수 있음.
    • 비대한 함수 작성을 방지하고 Liskov 대체 원칙을 위반하지 않으려면 거대한 인터페이스보다 작은 인터페이스를 많이 만드는 것이 좋음.

 

  • Dependency Inversion (의존관계 역전 원칙)
    • 클래스와 모듈은 단방향 및 선형 종속성을 달성하기 위해 구체화가 아닌 추상화에 종속되어야 함.
    • 또한 이는 컴포넌트의 순결성을 보장하므로 각 컴포넌트는 본인만의 역할을 담당함.
    • 이 개념은 의존성 주입 디자인 패턴과는 다름.

 

 

- 원의 중심은 다른 레이어에 대한 종속성이 없는 가장 순수한 범위

- 각 계층은 내부 원 종속성이 있는 외부 계층에서 사용할 추상화를 노출해야 함.

 

  • Entity
    • 애플리케이션의 비즈니스 규칙 및 개체 집합을 캡슐화
    • 이 계층은 또한 최고 수준의 규칙을 따르고 쉽게 사용할 수 있도록 다른 계층에 추상화를 노출
    • Google의 공식 아키텍처 가이드에서는 엔티티 레이어를 데이터 레이어로 취급

 

  • Use Cases
    • Use Cases는 엔티티 계층에서 기능을 트리거하는 사용자 작업과 같은 애플리케이션 비즈니스 규칙의 정의가 포함
    • 이 계층은 엔터티 계층에만 종속되며 응용 프로그램별 비즈니스 로직를 실행하기 위해 외부 계층에 추상화를 노출함.

 

  • Presenters (Interface Adapters)
    • 이 계층은 Use Cases 계층에서 애플리케이션 비즈니스 규칙의 정의인 노출된 모든 인터페이스를 수행하고 UI 계층과 통신
    • MVVM에서 ViewModel은 여기에 속함.

 

  • UI (프레임워크 및 드라이버)
    • UI 레이어는 Android 화면에서 사용할 Android 위젯과 같은 모든 UI 컴포넌트와 Activity, Fragment를 포함하여 UI가 렌더링되는 방식을 나타냄.

 


 

3️⃣ Asynchronous and Concurrency (비동기와 동시성)

- Android에서 시스템은 애플리케이션의 모든 UI 관련 작업을 처리하는 기본 스레드(소위 UI 스레드)를 생성

- 기본 스레드는 UI 요소 렌더링, 적절한 사용자 인터페이스로 이벤트 전달, Android UI toolkit의 컴포넌트 간의 모든 상호 작용을 담당

- 네트워크 요청 및 데이터베이스 쿼리와 같은 I/O 또는 비용이 많이 드는 계산 작업을 수행하려는 경우 기본 스레드 (메인 스레드)가 아닌 다른 스레드(소위 작업자 스레드)에서 처리해야 함.

- 기본 스레드가 화면 렌더링 및 사용자와의 상호 작용만을 담당할 수 있음.

 

 

🔴 RxJava/RxKotlin

- 백그라운드 스레드에서 비즈니스 로직을 실행하고 UI 스레드에서 결과를 받아보는 것과 같은 멀티 스레딩 문제를 쉽게 제어할 수 있음

- Schedulers라는 스레드 풀을 제공하며 스레드 풀에 있는 io, computation, single 스레드와 같은 다양한 종류의 스레드를 사용하거나, 새로운 사용자 정의 스레드를 생성할 수 있음

- 작동해야 하는 스레드와 결과를 소비해야 하는 스레드를 정의하는 SubscribeOn  ObserveOn 연산자로 스케줄러를 지정하여 스레딩 동작을 조작할 수 있음

 

🟠 Coroutines

- 언어 수준에서 코드를 비동기적으로 실행하는 훌륭한 동시성 솔루션

- 스레드와 달리 코루틴은 순전히 사용자 수준 언어의 추상화이므로, OS 리소스에 직접적으로 연결되지 않으며 각 코루틴 개체는 JVM 힙 메모리에 할당됨.

- 코루틴은 사용자 측에서 제어할 수 있고 훨씬 더 가벼운 리소스를 사용하며 스레드보다 컨텍스트 스위칭 (context switching) 비용이 저렴

- 코루틴은 가볍기 때문에, 단일 스레드에서 많은 코루틴을 실행할 수 있고 범위를 기반으로 작동하기 때문에 메모리 누수가 적음.

 

728x90