안드로이드는 유독 react, flutter와는 아키텍처구조가 참 특이하다.
안드로이드 특성상 웹이랑은 코드 작성 방식도 다르고, 통신을 다루는 방법, DI, class와 같은 것들이 너무나 다르다.
가장 큰 차이를 만들어내는 이유는 싱글쓰레드와 멀티쓰레드로 인한 구조적 차이 때문인것 같다.
궁금했던 점들을 적어본다.
repository 패턴의 network vs local_db의 추상화
안드로이드에서는 repository로 외부를 인터페이스로 막아버리고 내부에서 remoteDatasource, localDatasoure를 두고 또다시 remote에서는 retrofit, local에서는 room을 쓰는 경우가 많다.
클래스가 과도하게 많다고 느껴졌다.
내가 생각하는 해당 구조의 단점은 다음과 같았다.
✅ Repository 패턴이 안티패턴이 될 수도 있는 이유
1. 데이터 소스의 동작 방식이 명확하지 않음
• Repository가 내부적으로 네트워크를 호출할지, 로컬 DB를 사용할지 숨기고 있으면, 클라이언트 코드에서 예상치 못한 동작이 발생할 수 있어.
• 예를 들어, getUser()를 호출했을 때 네트워크 요청이 발생할지, 로컬에서 가져올지 개발자가 예측하기 어려움.
• 만약 내부 정책이 변경되면, Repository를 사용하는 모든 코드가 영향을 받을 수도 있음.
2. 캐싱 및 데이터 흐름을 이해하기 어려움
• Repository가 내부적으로 네트워크 → 로컬DB의 흐름을 관리하면, 이를 사용하는 UseCase나 ViewModel에서 데이터 흐름을 추적하기 어려워.
• 예를 들어, 네트워크 요청 후 DB에 캐싱하고, 다음 요청에서 DB를 먼저 조회하는 로직이 Repository 내부에 숨어 있다면 외부에서는 데이터가 어디서 오는지 알기 어려워.
3. 테스트가 어려워짐
• Repository 내부에 네트워크와 DB 로직이 동시에 존재하면, 이를 모킹(mocking)하기 어렵고, 단위 테스트가 복잡해질 수 있어.
• 네트워크를 모킹할지, DB를 모킹할지 명확하지 않아서 테스트의 목적이 불분명해짐.
---
✅ 그러면 Repository 패턴을 어떻게 개선하면 좋을까?
1. Repository는 데이터 소스를 직접 노출하는 형태로 변경
기존 Repository 패턴처럼 모든 걸 감추는 게 아니라, 네트워크와 로컬 DB를 명확히 구분하면 더 이해하기 쉬움.
예시) Repository가 내부 구현을 감추지 않고 노출하는 경우
interface UserApiClient {
suspend fun findUser(id: String): User?
}
interface UserRepository {
suspend fun findUser(id: String): User?
suspend fun saveUser(user: User)
}
2. UseCase에서 데이터 흐름을 관리하도록 변경
• 기존 Repository에서 데이터 소스를 숨기는 게 아니라, UseCase에서 네트워크와 로컬 DB를 조합하는 역할을 하면 더 명확함.
class GetUserUseCase(
private val remote: UserApiClient,
private val local: UserRepository,
) {
suspend fun execute(userId: String): User {
val localUser = local.findUser(userId)
return if (localUser != null) {
localUser
} else {
val remoteUser = remote.findUser(userId)
local.saveUser(remoteUser) // 캐싱
remoteUser
}
}
}
• 이렇게 하면 데이터 소스의 흐름이 UseCase에서 명확하게 보이고, 클라이언트 코드에서 예측 가능해짐.
• Repository가 불필요한 추상화를 제공하는 대신, UseCase에서 로직을 명확하게 정의.
안드로이드의 ViewModel은 왜 상태관리의 도구가 아닐까?
안드로이드에서는 ViewModel이 전역 상태 관리 도구로 사용되지 않으며, 대신 데이터 계층(Data Layer)에서 캐싱을 담당한다.
반면, 플러터의 Riverpod은 전역 상태 관리 라이브러리로서 상태 관리뿐만 아니라 데이터 캐싱까지 포함한다.
React의 TanStack Query 또한 데이터 캐싱을 담당하는 도구로 활용된다.
✅ 1. 안드로이드의 ViewModel은 왜 전역 상태 관리 도구가 아닐까?
📌 ViewModel은 “화면 단위”로 관리됨
• 안드로이드의 ViewModel은 Activity 또는 Fragment 생명주기에 맞춰 생성 및 관리된다.
• 즉, 특정 화면(View)에 한정된 상태 관리를 수행할 뿐, 앱 전체의 전역 상태를 관리하지는 않음.
📌 안드로이드의 전역 상태는 Repository나 Singleton을 활용
• ViewModel은 UI 상태를 유지하는 용도이며,
• 전역 상태 관리(예: 사용자 로그인 정보, 캐싱된 API 데이터 등)는 데이터 계층(Data Layer)에서 관리함.
📌 안드로이드는 “레이어드 아키텍처”를 따름
• 데이터와 UI 상태를 분리하기 위해 Repository에서 데이터 처리 + ViewModel에서 UI 상태 관리하는 방식이 권장됨.
• ViewModel이 데이터를 직접 캐싱하면 UI 로직과 데이터 로직이 섞이기 때문에 유지보수가 어려움.
• 따라서 데이터를 캐싱하려면 Repository(데이터 계층)에서 관리하는 것이 일반적.
즉, 안드로이드의 ViewModel은 오로지 UI와 관련된 로직만을 담는다.
또한, ViewModel은 ViewModelStoreOwner에 의해 생성과 소멸이 이루어지므로 화면의 생명주기에 맞춰 관리된다.
따라서 캐싱을 ViewModel에서 처리하면 화면이 종료될 때 데이터가 함께 소멸될 수 있다.
대신, 데이터의 지속성을 보장하기 위해 Repository 또는 Data Layer에서 캐싱을 담당하는 것이 일반적이다.
✅ 2. Riverpod은 왜 전역 상태 관리까지 담당할까?
📌 Riverpod은 애초에 “전역 상태 관리 라이브러리”
• Riverpod은 앱 전체에서 어디서든 접근 가능한 상태(state)를 관리하는 것을 목표로 설계됨.
• 즉, 화면 단위 관리가 아니라 애플리케이션 전체에서 공유할 수 있는 상태 관리를 제공함.
📌 Provider 패턴으로 “UI 상태 + 비즈니스 로직 + 캐싱”까지 포함
• Provider를 통해 데이터를 관리하면서 동시에 상태도 관리할 수 있음.
• Riverpod의 StateProvider, FutureProvider, StateNotifierProvider 등 다양한 방식으로 UI 상태와 데이터 상태를 함께 관리할 수 있음.
📌 Flutter에서는 ViewModel 개념 없이 Provider가 역할을 대체
• 플러터는 Activity/Fragment 개념이 없고, UI 자체가 위젯 트리 기반이라 ViewModel 같은 개념이 필요하지 않음.
• 대신 Riverpod의 Provider들이 각각의 ViewModel 역할을 수행하면서 전역 상태 관리까지 가능
안드로이드의 ViewModel 특성상 자연스럽게 뷰모델끼리의 참조가 불가능해지는데,
의존성이 명확해지고, 복잡한 순환 참조를 방지할 수 있다는 점에서 오히려 강점이 되는것 같다.
'Android' 카테고리의 다른 글
[Android] Ktor Client에서 Jwt 인증 로직 구현하기 (0) | 2025.05.05 |
---|---|
[Android] Jetpack Compose에서 Props drilling을 피하는 패턴 (1) | 2025.04.11 |
[Android] 선언형과 리액티브 프로그래밍을 왜 사용하는가 (0) | 2025.04.07 |
[Android] Navigation 정리 (0) | 2025.01.30 |