안드로이드 아키텍처를 공부해보자!

Hanseul Lee·2022년 9월 28일
1

우리는 왜 아키텍처를 알아야 할까?

우선 아키텍처가 왜 중요한지 생각해보자.

Android 앱에는 Activity, Fragment, Content Provider, Broadcast Receiver 등 다양한 앱 컴포넌트가 있다. 이 컴포넌트들을 활용해서 사용자 경험을 앱에 녹이게 되는데 핸드폰은 리소스가 제한적이다. 그래서 OS에서 새로운 앱을 위한 공간을 확보할 수 있도록 언제든지 앱의 프로세스를 종료할 수 있어야 한다.

그래서 앱 컴포넌트는 개별적이고 비순차적으로 구성되어야 한다. OS나 사용자가 언제든 앱 컴포넌트를 제거해야 하기 때문이다. 이 때문에 앱 데이터나 state가 컴포넌트에 저장되면 안 된다. 앱 컴포넌트끼리 종속되어서는 안 된다는 말이고, 이를 위해서 우리는 앱 아키텍처를 정의해야 한다.

일반적인 아키텍처 원칙

앱 컴포넌트와 데이터와 state를 저장할 수 없다면 앱을 어떻게 설계해야 할까?

💡 앱 확장성과 견고성을 높여서 쉽게 테스트할 수 있도록 해야 한다.

이를 위해서는 앱의 파트와 각 파트에 필요한 기능 간 경계를 정의 해야 한다. 이를 위해서는 다음과 같은 원칙들을 준수해야 한다.

관심사 분리(Separation of concerns)

그동안 ActivityFragment에 모든 코드를 작성하곤 했는데, 이거는 실수와 다름 없다! 이렇게 코딩을 해서는 안 된다. 이 두 가지 컴포넌트는 UI 기반 클래스고, 따라서 UI 및 OS 상호작용을 처리하는 로직만 포함해야 한다.

기억하자. 두 컴포넌트는 OS와 앱을 이어주는 클래스다. OS는 사용자의 행동에 따라, 혹은 메모리 부족과 같은 경우가 벌어질 때 언제든지 클래스를 제거할 수 있다. 그렇기 때문에 ActivityFragment에 모든 코드를 넣게 되면 많은 문제가 일어날 것이다! 사용자에게 만족스러운 사용자 경험을 제공하고, 또 수월하게 앱을 관리하려면 클래스 의존성을 최소화하는 게 좋다.

관심사 분리 시 장점을 세 가지로 정리해보자.

  • 클래스가 가벼워짐
  • 생명주기와 관련된 문제가 해결됨
  • 테스트 기능이 개선됨

데이터 모델에서 UI 도출하기 (Drive UI from data models)

데이터 모델에서 UI를 도출해야 하는데 가급적이면 지속적인 모델이 권장된다. 이를 기반으로 앱 아키텍처를 구축하면 테스트 가능성과 견고성이 높아지기 때문이다.

이게 무슨 말이냐면 데이터 모델은 앱의 데이터다. 데이터는 앱의 UI 요소나 기타 컴포넌트들과 독립되어 있다. UI, 앱 컴포넌트 생명주기와 관련이 없다는 말이다. 하지만 OS가 메모리에서 앱의 프로세스를 삭제하면 데이터 모델도 삭제된다.

그래서 지속적인 데이터 모델이 이상적인 이유는 다음과 같이 정리할 수 있다.

  • OS에서 리소스를 확보하기 위해 앱을 제거해도 데이터가 삭제되지 않는다
  • 네트워크 연결이 불안정하거나 비연결 상태여도 앱이 동작한다

단일 소스 저장소 (Single source of truth)

앱에서 새로운 데이터 유형을 정의할 때는 데이터 유형에 단일 소스 저장소(SSOT)를 할당해야 한다.

SSOT는 데이터의 소유자고, SSOT만 데이터를 수정하거나 변경 가능하다. 이런 특징을 수행하기 위해 SSOT는 불변 유형으로 데이터를 노출하고, 다른 유형이 호출할 수 있는 이벤트를 수신하거나 메서드를 노출해 데이터를 수정한다.

이런 패턴의 장점은 다음과 같이 정리된다.

  • 특정 유형 데이터의 모든 변경사항을 한 곳에서 관리한다. (일원화)
  • 다른 유형이 조작할 수 없도록 데이터를 보호한다.
  • 데이터 변경사항을 쉽게 추적할 수 있다. 그러면 버그를 발견하기 쉽다!

한 줄로 간단하게 정리하면 데이터를 안전하게 보호하기 위해 다른 클래스에서 수정할 수 없도록 해야 한다는 말이다. 내부에서만 수정이 가능하고, 외부에서는 읽기만 가능하도록! 이렇게 하면 앱 데이터가 외부 클래스로부터 원치 않아도 변경되지 않게 보호할 수 있다.

다음 포스팅에서 예시를 정리해뒀다.

ViewModel을 안전하게 사용하자!

class GameViewModel : ViewModel() {

    private var _currentScrambledWord: String
    val currentScrambledWord: String get() = _currentScrambledWord

}

주로 데이터베이스가 데이터 소스가 되지만 ViewModel이나 UI인 경우도 있다. 위 예시는 VeiwModel.

아래 비디오도 SSOT를 잘 설명했으니 봐보자. 한 줄 정리하면 데이터 소스를 이원화하지 말라는 내용이다.

https://www.youtube.com/watch?v=Ex9IT1bq0PQ

단방향 데이터 흐름 (Unidirectional Data Flow)

단일 소스 저장소 원칙은 단방향 데이터 흐름(UDF) 패턴과 함께 사용된다. UDF에서 state는 한 방향으로만 흐른다. data flow를 수정하는 이벤트는 그 반대 방향으로 흐르게 된다.

Android에서 state나 데이터는 일반적으로 계층 구조의 상위에서 하위로 흐른다. 이벤트는 하위에서 트리거되어 상응하는 데이터 유형의 SSOT에 닿게 된다. 사용자가 버튼을 누른 상황을 예시로 생각해보자. 클릭 이벤트는 UI에서 SSOT로 흐르고, SSOT에서 데이터가 불변 유형으로 수정 혹은 변경된다.

이런 패턴은 다음 장점을 가진다.

  • 데이터 일관성 강화
  • 오류 발생 확률 축소
  • 쉬운 디버깅
  • SSOT 패턴의 모든 이점 제공

LiveData와 DataBinding, ViewModel의 흐름을 생각해보면 이해가 쉬울 거 같다.

앱 구조화

광범위하게 적용되는 아키텍처로 이것을 가이드라인으로 삼고 필요와 요건에 따라 조정하도록 하자.

위에서 살펴본 일반적인 아키텍처 원칙에 따라 최소한 UI 레이어와 Data 레이어가 앱에 포함되어야 한다.

  • 화면에 데이터를 표시하는 UI Layer
  • 앱의 비즈니스 로직을 포함하고 데이터를 노출하는 Data Layer
  • UI, Data 레이어 간 상호작용을 간소화하고 재사용하기 위한 Domain Layer

cf) 화살표는 클래스 간 종속성을 나타낸다. 예를 들어 domain 레이어는 data 레이어에 종속된다.

UI Layer

화면에 데이터를 표시한다. 사용자 상호작용(버튼 클릭)이나 외부 입력(네트워크)으로 데이터가 변할 때마다 변경사항을 반영하도록 업데이트 되어야 한다.

data layer에서 가져온 state를 시각적으로 나타낸다는 말이다! 하지만 일반적으로 data layer에서 가져오는 데이터는 표시해야 하는 정보와 다른 형식이다. 사용자에게 정보를 표시하기 위해 서로 다른 두 데이터 소스를 병합해야 할 수도 있다는 말이다. 그래서 데이터 변경사항을 UI가 표시할 수 있는 형식으로 변환 후 표시하는 파이프라인으로 이해하는 게 훨씬 좋다.

앱에서 사용자에게 표시하는 정보를 UI state라고 한다. 그러니까 사용자가 보는 것이 UI라면 UI state는 사용자가 봐야 한다고 앱이 지정하는 것이란 말이다. UI는 UI state를 시각적으로 나타내기 때문에, UI state가 변경되면 변경사항이 즉시 UI에 반영된다.

UI state는 변경 불가능한 스냅샷이다. 하지만 데이터의 동적 특성에 따라 state는 시간이 지나면서 변경되거나, 아니면 사용자 상호작용이나 기타 이벤트로 변경될 수 있는데 이때는 어떻게 처리해야 할까? 이때 바로 아키텍처 원칙 중 하나인 단방향 데이터 흐름(UDF)를 활용한다.

state가 아래로 향하고, 이벤트는 위로 향하는 패턴이 바로 단방향 흐름 패턴이라고 배웠다. 이 패턴이 앱 아키텍처에 미치는 영향을 정리하면 다음과 같다.

  1. ViewModel이 UI에 사용될 state를 보유하고 노출한다. 이때 UI state는 ViewModel에 의해 변환된 앱 데이터다.
  2. UI가 ViewModel에 사용자 이벤트를 알린다.
  3. ViewModel이 사용자 작업을 처리하고 state를 업데이트한다.
  4. 업데이트된 state가 렌더링할 UI에 다시 제공된다.

state에 변경사항을 일으키는 모든 이벤트에 위 작업이 반복된다. 조금 더 자세하게는 아래 그래프를 참고하자.

이렇게 UDF를 사용하면 다음 장점이 있다.

  • 데이터 일관성 → UI용 데이터 소스가 하나
  • 테스트 가능성 → state 소스가 분리되므로 UI와 별개로 테스트 가능
  • 유지 관리성 → 변경사항이 일어나면 관련된 소스 모두 영향을 받음

Data Layer

비즈니스 로직이 포함되어 있다. 비즈니스 로직은 앱 데이터 생성, 저장, 변경 방식을 결정하는 규칙으로 구성된다. 비즈니스 로직이랑 앱 데이터 생성, 저장, 변경 방식을 결정하는 규칙을 말한다.

data layer는 데이터 소스를 포함할 수 있는 repository로 구성된다. 앱에서 처리하는 다양한 유형의 데이터별로 저장소 클래스를 만들어야 한다. 사용자 관련 데이터는 UserRepository 클래스를 만들어야 한다는 말이다.

그리고 repository가 다른 repository에 종속되어야 하는 경우도 있다. 관련 데이터가 여러 데이터 소스의 집계거나 책임이 다른 repository에 캡슐화되어야 하는 여러 경우가 있기 때문이다. 예시로 사용자 인증 데이터를 처리하는 repository를 도식화 해보면 다음과 같다.

repository는 다음과 같은 일을 한다.

  • 앱에 데이터 노출
  • 데이터 변경사항을 한 곳에서 집중관리
  • 여러 데이터 소스 간 충돌 해결
  • 데이터 소스 추상화
  • 비즈니스 로직 포함

각 데이터 소스 클래스는 파일, 네트워크 소스, 로컬 데이터베이스와 같은 하나의 데이터 소스만 사용한다. 그러니까 데이터 소스 클래스는 데이터 작업을 위해서 앱과 시스템 사이의 가교 역할을 하게 된다.

data layer에서 알아둬야 하는 주의점을 몇 가지 알아보자.

다른 layer에서 데이터 소스에 직접 액세스해서는 안 된다. 데이터 영역의 진입점은 항상 repository여야 한다. repository 클래스가 진입점이 되어야 아키텍처의 다양한 layer를 독립적으로 확장할 수 있기 때문이다.

data layer에서 노출된 데이터는 변경할 수 없다. 외부에서 데이터에 접근해도 읽기만 가능하지 변경은 못하게 해야 한다는 거다. 그래야 값을 일관된 상태로 만들고, 그럼으로써 여러 스레드에서 안전하게 처리될 수 있다.

Domain Layer

복잡한 비즈니스 로직, 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다. 모든 앱에서 위 기능을 필수로 요구되지는 않기 때문에 이 layer는 선택사항이다. 그러니까 복잡성을 처리하거나 재사용성을 선호하는 경우에 사용된다.

그래서 다음과 같은 장점이 있다.

  • 코드 중복 방지
  • 가독성 개선
  • 앱 테스트 가능성 상승
  • 책임 분할로 대형 클래스 회피

장점에서도 알 수 있듯 가장 중요한 건 클래스를 간단하고 가볍게 유지하는 것인데, 때문에 UseCase는 기능 하나씩만 담당하고 변경 가능한 데이터를 포함해서는 안 된다. 변경되는 데이터는 UI layer나 data layer에서 처리해야 한다.

그리고 UseCase는 이름이 지정되는 규칙이 있다.

💡 *현재 시제의 동사* + *명사/대상(선택사항)* + *UseCase*

FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase

UseCase는 UI layer의 ViewModel과 data layer의 repository 사이에 위치한다. 즉 일반적으로 Repository 클래스에 종속되고, 자바의 경우에는 콜백, 코틀린의 경우에는 코루틴을 써서 Repository와 동일한 방법으로 UI 클래스와 통신한다.

일반적인 권장사항

  • 앱 컴포넌트에 데이터를 저장하면 안 된다
  • 클래스의 의존성을 줄이자
  • 앱의 모듈 간 책임을 잘 정리하자
  • 각 모듈을 가능한 적게 노출하자
  • 고유한 핵심 기능에 초점을 맞추자
  • 각 파트를 독립적으로 테스트하는 방법을 고려하자
  • 동시 실행 정책을 갖추자
  • 가능한 관련성이 높은 최신 데이터를 유지하자

공식문서: Guide to app architecture

0개의 댓글