본문 바로가기
Android

[Jetpack] 앱 아키텍처 가이드

by yonikim 2021. 10. 1.
728x90

 

대부분의 경우 데스크톱 앱에는 데스크톱 또는 프로그램 런처의 단일 진입점이 있으며 하나의 모놀리식 프로세스로 실행된다. 

반면에 Android 앱의 구조는 훨씬 복잡하다. 일반적인 Android 앱에는 Activity, Fragment, Service, Content providers, Broadcasts 를 비롯하여 여러 앱 구성요소가 포함된다. (개발자는 AndroidManifest.xml 파일에서 이러한 앱 구성요소의 대부분을 선언할 것이다.) 

 

이와 같이 Android 앱은 여러 구성요소를 포함하고 있는데, 사용자는 짧은 시간 내에 여러 앱과 상호작용할 때가 많다. 또한 휴대기기는 리소스가 제한되어 있으므로, 운영체제에서는 새로운 앱을 위한 공간을 확보하기 위해 언제든지 일부 앱 프로세스를 종료할 수도 있다. 

 

즉, 앱을 개발할 땐 사용자 중심의 다양한 워크플로우 및 작업에 맞게 조정될 수 있도록 개발해야 하는데, 앱 구성요소에 앱 데이터나 상태를 저장해서는 안되며 앱 구성요소가 서로 종속되면 안된다. 

 

 

일반적인 아키텍처 원칙

1. 관심사 분리

보통 개발하다 보면 Acitivty 또는 Fragment 에 모든 코드를 작성할 때가 있는데, Acitivty 와 Fragment 같이 UI 기반의 클래스에는 UI 및 운영체제 상호작용을 처리하는 로직만 포함해야 한다. Acitivty 와 Fragment 는 Android OS 와 앱 사이의 계약을 나타내도록 이어주는 클래스일 뿐으로, 메모리 부족과 같은 시스템 상태나 사용자 상호작용에 의해 언제든지 제거될 수 있다. 따라서 해당 클래스에 대한 의존성을 최소화하는 것이 좋다. 

 

2. 모델에서 UI 도출하기

Model 은 앱의 데이터 처리를 담당하는 구성요소로, 앱의 View 객체 및 앱 구성요소와 독립되어 있으므로 앱의 수명 주기 및 관련 문제의 영향을 받지 않는다. 따라서 Model 클래스를 기반으로 앱을 개발하면 쉽게 테스트하고 일관성을 유지할 수 있다. 

 

 

권장 앱 아키텍처

각 구성요소가 한 수준 아래의 구성요소에만 종속되는 것을 확인할 수 있다. 예를 들어 Activity 와 Fragment 는 ViewModel 에만 종속되어 있다. Repository 는 여러개의 다른 클래스에 종속되는 유일한 클래스이다.

 

 

사용자 프로필을 표시하는 UI 를 제작한다고 가정해 보자. 

 

사용자 인터페이스 제작

UI는 Fragment(UserProfileFragment) 와 관련 레이아웃 파일(user_profile_layout.xml) 로 구성된다. 

UI를 도출하려면 데이터 Model 에 다음과 같은 요소가 있어야 한다.

  • 사용자 ID: 사용자의 식별자로, Fragment arguments 를 사용하여 Fragment 에 이 정보를 전달하는 것이 좋다. Android OS 에서 프로세스를 제거해도 이 정보는 유지되므로, 앱을 다시 시작해도 해당 ID 를 사용할 수 있다.
  • 사용자 객체: 사용자에 관한 세부정보를 보유한 데이터 클래스이다. 

ViewModel 아키텍처 구성요소에 기반한 UserProfileViewModel 을 사용하여 이 정보를 유지한다. 

(ViewModel 객체는 Fragment 나 Activity 와 같은 특정 UI 구성요소에 관한 데이터를 제공하고, Model 과 통신하기 위한 데이터 처리 비즈니스 로직을 포함한다.)

 

▷ user_profile_layout.xml: 화면의 UI 레이아웃 정의

▷ UserProfileFragment: 데이터를 표시하는 UI 컨트롤러

▷ UserProfileViewModel: UserProfileFragment 에서 볼 수 있도록 데이터를 준비하고, 사용자 상호작용에 반응하는 클래스

 

 

데이터 가져오기

Repository 모듈은 데이터 작업을 처리한다. 깔끔한 API 를 제공하므로 나머지 앱에서 데이터를 간편하게 가져올 수 있으며, 데이터가 업데이트될 때 어디에서 데이터를 어디서 가져올지부터 어떤 API 를 호출할지 알고 있다. 즉 Model, Webservice, Cache 등 다양한 데이터 소스 간 중재자의 역할을 한다. 

 

UserRepository 클래스에서 사용자의 데이터를 가져오려면 Webservice 인스턴스가 필요하다. 인스턴스는 간단히 만들 수 있지만, 그렇게 하려면 Webservice 클래스의 종속 항목을 알아야 한다. 그런데 Webservice 가 필요한 클래스가 UserRepository 하나만 있는게 아니라면? 클래스 별로 새로운 Webservice 를 만든다면 앱의 리소스 소모량이 커질 것이다. 

이 문제는 다음과 같은 디자인 패턴을 사용하여 해결할 수 있다.

  • Dependency Injection(DI)
    • 컴포넌트 간 의존 관계를 소스코드 내부가 아닌 외부 설정 파일들을 통해 정외되게 하는 디자인 패턴 중 하나
    • 객체를 직접 생성하지 않고 외부에서 주입한 객체를 사용하는 방식
    • 인스턴스 간 디커플링을 만들어줌 -> 유닛테스트 용이성 증대
    • ex) Hilt, Dagger
  • Service Locator
    • 중앙 등록자 Service Locator를 통해 요청이 들어왔을 때, 특정 인스턴스 반환
    • apk 크기, 빌드 속도, 메서드 수 등 복잡한 제약이 있는 경우 사용하기 편함
    • ex) Koin (경량화된 DI 라고 소개하지만, 내부 동작은 Service Locator 로 봐도 무방)

 

  Dependency Injection(DI) Service Locator
종속성 일부 핵심 클래스에 종속성을 주입 모든 클래스가 서비스 로케이터에 종속
호출방법 처음 한번만 호출 (명시적인 호출 없음) 인젝터를 직접 호출 (명시적인 호출)
의존 관계 의존 관계 파악이 쉬움 의존 관계 파악이 어려움

 

 

+

데이터 캐시

UserRepository 구현은 Webservice 객체 호출을 추출하지만 하나의 데이터 소스에만 의존하기 때문에 유연성이 떨어진다. 

UserRepository 구현에서 발생하는 중요한 문제는 백엔드에서 데이터를 가져온 후 어디에도 보관하지 않는다는 점이다. 따라서 사용자가 UserProfileFragment 를 떠났다가 다시 돌아왔을 때 데이터가 변경되지 않았어도 앱에서 데이터를 다시 가져와야 한다. 

이 디자인은 1. 귀중한 모바일 데이터를 소비한다. 2. 새 쿼리가 완료될 때까지 사용자가 기다려야 한다. 이와 같은 이유로 최적의 방법이 아니다.

따라서 아래와 같이 메모리에 UserRepository 객체를 캐시하는 것이 좋다. 

class UserRepository @Inject constructor(
   private val webservice: Webservice,
   private val userCache: UserCache
) {
   suspend fun getUser(userId: String): User {
       val cached: User = userCache.get(userId)
       if (cached != null) {
           return cached
       }

       val freshUser = webservice.getUser(userId)
       userCache.put(userId, freshUser)
       return freshUser
   }
}

 

데이터 지속

지금까지의 방식을 사용하면 사용자가 기기를 회전하거나 앱에서 나갔다가 즉시 돌아오는 경우 저장소가 메모리 내 캐시에서 데이터를 가져오기 때문에 기존 UI 가 즉시 표시된다. 

 

그런데 사용자가 앱에서 나갔다가 몇시간 뒤 Android OS 에서 프로세스를 종료한 후에 다시 돌아오면 어떻게 될까? 네트워크에서 데이터를 다시 가져와야 한다. 

이 경우엔 Room 과 같은 지속성 라이브러리가 필요하다. Room 은 개체 매핑 라이브러리로, 최소한의 상용구 코드로 로컬 데이터 지속성을 제공한다. Room 을 이용하여 라이브러리의 추상화와 쿼리 유효성 검사 기능을 활용할 수 있다. 

 

 

(출처: https://developer.android.com/jetpack/guide#build-ui)

728x90