앱 아키텍처는 고품질 Android 애플리케이션의 기반입니다. 잘 정의된 아키텍처를 사용하면 스마트폰, 태블릿, 폴더블, ChromeOS 기기, 자동차 디스플레이, XR 등 계속 확장되는 Android 기기 생태계에 적응할 수 있는 확장 가능하고 유지관리 가능한 앱을 만들 수 있습니다.
앱 구성
일반적인 Android 앱은 서비스, 콘텐츠 제공자, broadcast receiver와 같은 여러 앱 구성요소로 구성됩니다. 이러한 구성요소는 앱 매니페스트에서 선언합니다.
앱의 사용자 인터페이스도 구성요소입니다. 이전에는 여러 활동을 사용하여 UI를 빌드했습니다. 하지만 최신 앱은 단일 활동 아키텍처를 사용합니다. 단일 Activity는 프래그먼트 또는 Jetpack Compose 대상으로 구현된 화면의 컨테이너 역할을 합니다.
여러 폼 팩터
앱은 휴대전화뿐만 아니라 태블릿, 폴더블, ChromeOS 기기 등 다양한 폼 팩터에서 실행할 수 있습니다. 앱은 세로 모드 또는 가로 모드 방향을 가정할 수 없습니다. 기기 회전이나 폴더블 기기 접기/펼치기와 같은 구성 변경으로 인해 앱이 UI를 리컴포즈해야 하며 이는 앱 데이터와 상태에 영향을 미칩니다.
리소스 제약 조건
휴대기기(대형 화면 기기 포함)는 리소스가 제한되어 있으므로 언제든지 운영체제에서 새로운 앱을 위한 공간을 확보하도록 일부 앱 프로세스를 중지할 수 있습니다.
변수 출시 조건
리소스가 제한된 환경에서는 앱의 구성요소가 개별적으로 비순차적으로 실행될 수 있으며, 운영체제나 사용자가 언제든지 앱 구성요소를 제거할 수 있습니다. 따라서 앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 됩니다. 앱 구성요소는 서로 독립적이어야 합니다.
일반 아키텍처 원칙
애플리케이션 데이터와 상태를 저장하는 데 앱 구성요소를 사용할 수 없다면 앱을 어떻게 설계해야 할까요?
Android 앱은 크기가 커지기 때문에 앱을 확장할 수 있는 아키텍처를 정의하는 것이 중요합니다. 잘 설계된 앱 아키텍처는 앱의 부분과 그 각 부분에 필요한 기능 간의 경계를 정의합니다.
관심사 분리
몇 가지 특정 원칙을 준수하도록 앱 아키텍처를 설계합니다.
가장 중요한 원칙은 관심사 분리입니다. Activity 또는 Fragment에 모든 코드를 작성하는 실수는 흔히 일어납니다.
Activity 또는 Fragment의 기본 역할은 앱의 UI를 호스팅하는 것입니다. Android OS는 수명 주기를 제어하며 화면 회전과 같은 사용자 작업이나 메모리 부족과 같은 시스템 이벤트에 따라 자주 제거하고 다시 만듭니다.
이러한 일시적인 특성으로 인해 애플리케이션 데이터나 상태를 저장하는 데 적합하지 않습니다. Activity 또는 Fragment에 데이터를 저장하면 구성요소가 다시 생성될 때 데이터가 손실됩니다. 데이터 지속성을 보장하고 안정적인 사용자 환경을 제공하려면 이러한 UI 구성요소에 상태를 위임하지 마세요.
적응형 레이아웃
앱은 기기 방향 변경이나 앱 창 크기 변경과 같은 구성 변경을 적절하게 처리해야 합니다. 적응형 표준 레이아웃을 구현하여 다양한 폼 팩터에서 최적의 사용자 환경을 제공하세요.
데이터 모델에서 UI 도출
또 하나의 중요한 원칙은 데이터 모델에서 UI를 도출해야 한다는 것입니다. 가급적 지속적인 모델을 권장합니다. 데이터 모델은 앱의 데이터를 나타내며, 앱의 UI 요소 및 기타 구성요소로부터 독립되어 있습니다. 즉, 이들은 UI 및 앱 구성요소 수명 주기와는 관련이 없습니다. 하지만 OS가 메모리에서 앱의 프로세스를 삭제하면 데이터 모델도 삭제됩니다.
지속 모델이 이상적인 이유는 다음과 같습니다.
Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터가 삭제되지 않습니다.
네트워크 연결이 간헐적이거나 연결되어 있지 않아도 앱이 계속 작동합니다.
데이터 모델 클래스를 기반으로 앱 아키텍처를 구축하여 앱의 견고성과 테스트 가능성을 높이세요.
단일 소스 저장소
앱에서 새로운 데이터 유형을 정의할 때는 데이터 유형에 단일 소스 저장소 (SSOT)를 할당해야 합니다. SSOT는 데이터의 소유자이며, SSOT만 데이터를 수정하거나 변경할 수 있습니다. SSOT는 이를 위해 불변 유형을 사용하여 데이터를 노출하며, 다른 유형이 호출할 수 있는 이벤트를 수신하거나 함수를 노출하여 데이터를 수정합니다.
이 패턴에는 여러 가지 이점이 있습니다.
- 특정 유형 데이터의 모든 변경사항을 한곳으로 일원화합니다.
- 다른 유형이 조작할 수 없도록 데이터를 보호합니다.
- 데이터 변경사항을 더 쉽게 추적할 수 있으므로 버그를 더 쉽게 발견할 수 있습니다.
오프라인 중심 애플리케이션의 애플리케이션 데이터 정보 소스는 주로 데이터베이스입니다. 정보 소스가 ViewModel인 경우도 있습니다.
단방향 데이터 흐름
단일 소스 저장소 원칙은 종종 단방향 데이터 흐름 (UDF) 패턴과 함께 사용됩니다. UDF에서 상태는 일반적으로 상위 구성요소에서 하위 구성요소로 한 방향으로만 흐릅니다. 데이터 흐름을 반대 방향으로 수정하는 이벤트입니다.
Android에서 상태 또는 데이터는 일반적으로 계층 구조의 상위 범위 유형에서 하위 범위 유형으로 흐릅니다. 이벤트는 보통 하위 범위 유형에서 트리거되어 상응하는 데이터 유형의 SSOT에 도달합니다. 예를 들어 애플리케이션 데이터는 보통 데이터 소스에서 UI로 흐릅니다. 버튼 누르기와 같은 사용자 이벤트는 UI에서 SSOT로 흐르며, SSOT에서는 애플리케이션 데이터가 불변 유형으로 수정 및 노출됩니다.
이 패턴은 데이터 일관성을 강화하고, 오류가 발생할 확률을 줄여 주며, 디버그하기 쉽고, SSOT 패턴의 모든 이점을 제공합니다.
권장 앱 아키텍처
일반적인 아키텍처 원칙에 따라 각 애플리케이션에는 최소한 다음 두 가지 레이어가 포함되어야 합니다.
- UI 레이어: 화면에 애플리케이션 데이터를 표시합니다.
- 데이터 레이어: 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출합니다.
UI와 데이터 레이어 간의 상호작용을 간소화하고 재사용하기 위한 도메인 레이어라는 레이어를 추가할 수 있습니다.
최신 앱 아키텍처
최신 Android 앱 아키텍처에서는 다음 기법을 사용합니다.
- 적응형 및 계층형 아키텍처
- 앱의 모든 레이어에서의 단방향 데이터 흐름 (UDF)
- 상태 홀더가 있는 UI 레이어로 UI의 복잡성 관리
- 코루틴 및 흐름
- 종속 항목 삽입 권장사항
자세한 내용은 Android 아키텍처 권장사항을 참고하세요.
UI 레이어
UI 레이어 (또는 프레젠테이션 레이어)의 역할은 화면에 애플리케이션 데이터를 표시하는 것입니다. 사용자 상호작용(예: 버튼 누르기) 또는 외부 입력(예: 네트워크 응답)으로 인해 데이터가 변할 때마다 변경사항을 반영하도록 UI가 업데이트되어야 합니다.
UI 레이어는 두 가지 유형의 구조로 구성됩니다.
- 화면에 데이터를 렌더링하는 UI 요소. 적응형 레이아웃을 지원하려면 Jetpack Compose 함수를 사용하여 이러한 요소를 빌드합니다.
- 데이터를 보유하고 이를 UI에 노출하며 로직을 처리하는 상태 홀더 (예:
ViewModel)
적응형 UI의 경우 ViewModel 객체와 같은 상태 홀더는 다양한 창 크기 클래스에 적응하는 UI 상태를 노출합니다. currentWindowAdaptiveInfo()를 사용하여 이 UI 상태를 파생시킬 수 있습니다. 그러면 NavigationSuiteScaffold와 같은 구성요소가 이 정보를 사용하여 사용 가능한 화면 공간에 따라 다양한 탐색 패턴 (예: NavigationBar, NavigationRail, NavigationDrawer) 간에 자동으로 전환할 수 있습니다.
자세한 내용은 UI 레이어 페이지를 참고하세요.
데이터 영역
앱의 데이터 레이어에는 비즈니스 로직이 포함되어 있습니다. 비즈니스 로직은 앱에 가치를 부여하는 요소로, 앱의 데이터 생성, 저장, 변경 방식을 결정하는 규칙으로 구성됩니다.
데이터 레이어는 각각 0개부터 여러 개의 데이터 소스를 포함할 수 있는 저장소로 구성됩니다. 앱에서 처리하는 다양한 유형의 데이터별로 저장소 클래스를 만들어야 합니다. 예를 들어 영화 관련 데이터에는 MoviesRepository 클래스를 만들거나 결제 관련 데이터에는 PaymentsRepository 클래스를 만들 수 있습니다.
저장소 클래스는 다음을 담당합니다.
- 앱의 나머지 부분에 데이터 노출
- 데이터 변경사항을 한곳으로 일원화
- 여러 데이터 소스 간의 충돌 해결
- 앱의 나머지 부분에서 데이터 소스 추상화
- 비즈니스 로직 포함
각 데이터 소스 클래스는 파일, 네트워크 소스, 로컬 데이터베이스와 같은 하나의 데이터 소스만 사용해야 합니다. 데이터 소스 클래스는 데이터 작업을 위해 애플리케이션과 시스템 간의 가교 역할을 합니다.
자세한 내용은 데이터 레이어 페이지를 참고하세요.
도메인 레이어
도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어입니다.
도메인 레이어는 복잡한 비즈니스 로직이나 여러 뷰 모델에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당합니다. 모든 앱에 이러한 요구사항이 있는 것은 아니므로 도메인 레이어는 선택사항입니다. 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 사용해야 합니다.
도메인 레이어의 클래스는 일반적으로 사용 사례 또는 상호작용자라고 합니다.
각 사용 사례는 하나의 기능을 담당해야 합니다. 예를 들어 여러 뷰 모델이 시간대를 사용하여 화면에 적절한 메시지를 표시하는 경우 앱에는 GetTimeZoneUseCase 클래스가 있을 수 있습니다.
자세한 내용은 도메인 레이어 페이지를 참고하세요.
구성요소 간 종속성 관리
앱의 클래스는 올바른 작동을 위해 다른 클래스에 종속됩니다. 특정 클래스의 종속 항목을 수집하는 데 다음 디자인 패턴 중 하나를 사용할 수 있습니다.
- 종속성 주입 (DI): 종속성 주입을 사용하면 클래스가 자신의 종속성을 구성할 필요 없이 종속성을 정의할 수 있습니다. 런타임 시 다른 클래스가 이 종속 항목을 제공해야 합니다.
- 서비스 로케이터: 서비스 로케이터 패턴은 클래스가 자신의 종속 항목을 구성하는 대신 종속 항목을 가져올 수 있는 레지스트리를 제공합니다.
이 패턴은 코드를 중복하거나 복잡성을 추가하지 않아도 종속 항목을 관리하기 위한 명확한 패턴을 제공하므로 코드를 확장할 수 있습니다. 이 패턴을 사용하면 테스트와 프로덕션 구현 간에 신속하게 전환할 수도 있습니다.
일반 권장사항
프로그래밍은 창조적인 분야이며 Android 앱 제작도 예외가 아닙니다. 문제를 해결하는 방법은 여러 가지가 있습니다. 즉, 여러 활동이나 프래그먼트 간에 데이터를 교환하거나, 원격 데이터를 가져와서 오프라인 모드에서 사용하도록 데이터를 로컬에 보존하거나, 중요한 앱에서 발생하는 다른 일반적인 시나리오를 필요한 수만큼 처리하는 것입니다.
다음 권장사항은 필수는 아니지만, 대부분의 경우 권장사항을 따르면 코드베이스가 더 강력하고, 테스트 가능하며, 유지관리하기 쉬워집니다.
앱 구성요소에 데이터를 저장합니다.
활동, 서비스, broadcast receiver와 같은 앱의 진입점을 데이터 소스로 지정하지 마세요. 진입점은 다른 구성요소와만 조정하여 해당 진입점과 관련된 데이터 하위 집합을 가져와야 합니다. 각 앱 구성요소는 사용자와 기기의 상호작용 및 시스템 용량에 따라 단기간만 지속됩니다.
Android 클래스의 종속 항목을 줄입니다.
앱 구성요소는 Context 또는 Toast 같은 Android 프레임워크 SDK API를 사용하는 유일한 클래스여야 합니다. 앱 구성요소와 별도로 앱의 다른 클래스를 추상화하면 테스트 가능성은 높이고 앱 내의 커플링은 줄일 수 있습니다.
앱의 모듈 간 책임이 명확하게 정의된 경계를 만듭니다.
네트워크에서 데이터를 로드하는 코드를 코드베이스의 여러 클래스나 패키지 전체에 분산하면 안 됩니다. 마찬가지로 데이터 캐시와 데이터 결합 등 여러 개의 관련 없는 책임을 동일한 클래스에 정의하면 안 됩니다. 권장 앱 아키텍처를 따르면 도움이 됩니다.
각 모듈에서 가능하면 적게 노출합니다.
내부 구현 세부정보를 노출하는 바로가기를 만들지 마세요. 단기적으로는 약간의 시간을 벌 수 있지만, 코드베이스가 발전함에 따라 기술적 문제가 여러 번 발생할 수 있습니다.
다른 앱과 차별되도록 앱의 고유한 핵심에 초점을 맞춥니다.
동일한 상용구 코드를 반복하여 작성하느라 시간을 낭비하지 마세요. 대신 앱을 독특하게 만드는 데 시간과 에너지를 집중하세요. 반복적인 상용구는 Jetpack 라이브러리와 기타 권장 라이브러리가 처리하도록 하세요.
표준 레이아웃과 앱 디자인 패턴을 사용합니다.
Jetpack Compose 라이브러리는 적응형 사용자 인터페이스를 빌드하기 위한 강력한 API를 제공합니다. 앱에서 표준 레이아웃을 사용하여 다양한 폼 팩터와 디스플레이 크기에서 사용자 환경을 최적화하세요. 앱 디자인 패턴 갤러리를 검토하여 사용 사례에 가장 적합한 레이아웃을 선택합니다.
구성 변경 시 UI 상태를 유지합니다.
적응형 레이아웃을 설계할 때는 디스플레이 크기 조절, 접기, 방향 변경과 같은 구성 변경 시 UI 상태를 유지하세요. 아키텍처는 사용자의 현재 상태가 유지되어 원활한 환경을 제공하는지 확인해야 합니다.
재사용 가능하고 구성 가능한 UI 구성요소를 설계합니다.
적응형 디자인을 지원하기 위해 재사용 가능하고 구성 가능한 UI 구성요소를 빌드합니다. 이를 통해 상당한 리팩터링 없이 다양한 화면 크기와 자세에 맞게 구성요소를 결합하고 재정렬할 수 있습니다.
앱의 각 부분을 독립적으로 테스트하는 방법을 고려합니다.
네트워크에서 데이터를 가져오기 위해 API를 잘 정의하면 해당 데이터를 로컬 데이터베이스에 보존하는 모듈을 더 쉽게 테스트할 수 있습니다. 그러지 않고 두 함수의 로직을 한 위치에 혼합하거나, 네트워크 코드를 전체 코드베이스에 분산하면 테스트가 불가능하지는 않을지라도 훨씬 더 어려워집니다.
유형은 동시 실행 정책을 담당합니다.
장기 실행 차단 작업을 실행하는 유형은 이 계산을 올바른 스레드로 옮기는 일을 담당합니다. 이 유형은 자신이 실행하는 컴퓨팅 유형이 무엇이고 어느 스레드에서 실행되어야 하는지를 알고 있습니다. 유형은 기본 안전성을 갖춰야 합니다. 즉, 기본 스레드에서 차단 없이 안전하게 호출될 수 있어야 합니다.
가능한 한 관련성이 높은 최신 데이터를 보존합니다.
이렇게 하면 기기가 오프라인 모드일 때도 사용자가 앱의 기능을 이용할 수 있습니다. 모든 사용자가 끊김 없고 속도가 빠른 연결을 사용하지는 않는다는 점에 유의하세요. 끊김 없고 속도가 빠르더라도 혼잡한 곳에서는 수신 상태가 좋지 않을 수 있습니다.
아키텍처의 이점
앱에 좋은 아키텍처를 구현하면 프로젝트팀과 엔지니어링팀에 다음과 같은 여러 이점이 제공됩니다.
- 앱의 전반적인 유지관리성, 품질, 견고성이 개선됩니다.
- 앱을 확장할 수 있습니다. 코드 충돌이 최소화되어 더 많은 인력과 팀이 동일한 코드베이스에 기여할 수 있습니다.
- 온보딩에 도움이 됩니다. 아키텍처는 프로젝트에 일관성을 부여하므로 새로운 팀원이 빠르게 업무를 시작하고 보다 짧은 시간에 효율을 높일 수 있습니다.
- 테스트하기가 더 쉬워집니다. 좋은 아키텍처는 테스트하기가 더 쉬운 간단한 유형을 사용하도록 지원합니다.
- 잘 정의된 프로세스를 사용하여 버그를 체계적으로 조사할 수 있습니다.
아키텍처에 투자하는 것은 사용자에게도 직접적인 영향을 줍니다. 엔지니어링팀의 생산성이 높아짐에 따라 애플리케이션의 안정성이 향상되고 더 많은 기능이 적용됩니다. 하지만 아키텍처에는 초기에 투자하는 시간이 필요합니다. 조직의 다른 구성원에게 이러한 시간 투자의 정당성을 설득하려면 앱에 좋은 아키텍처를 적용한 다른 회사의 성공사례가 정리된 우수사례를 살펴보세요.
샘플
다음 샘플은 좋은 앱 아키텍처를 보여줍니다.