안드로이드 클린 아키텍처 정리
클린 아키텍처란?
최근에 클린 아키텍처를 안드로이드 프로젝트에 적용해보고 있는데, 처음에는 계층이 왜 이렇게 복잡한지 이해가 안 됐다. 하지만 막상 구조를 나눠서 코드를 작성해보니 “UI와 로직, 데이터가 뒤엉키지 않는다”는 게 이렇게 큰 장점일 줄은 몰랐다.
기본적으로 계층은 세 가지로 나뉜다:
- presentation: UI, ViewModel
- domain: UseCase, Entity, Repository Interface
- data: API/DB, Repository 구현체
모듈 구조는 이렇게 나눴다:
/app ← presentation
/domain ← domain
/data ← data
의존성 흐름은 단방향
app → domain
↘
data → domain
이 방향성이 무너지면 프로젝트가 산으로 간다…!!!!!!!
Gradle 설정 예시
settings.gradle.kts
include(":app", ":domain", ":data")
app/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.compose.ui:ui:1.6.0")
}
domain/build.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm")
}
data/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("androidx.room:room-runtime:2.6.1")
}
라이브러리 위치 정리
라이브러리 | 위치 |
---|---|
UI (Compose 등) | app |
비즈니스 로직 | domain |
Retrofit, Room 등 | data |
계층별 예시 (User 정보 불러오기)
“UseCase는 왜 굳이 따로 둬야 하지?” 싶었는데 테스트나 재사용성을 생각하면 결국 분리하는 게 낫더라.
UserRepository
: domain에서 인터페이스UserRepositoryImpl
: data에서 API/DB 로직GetUserUseCase
: domain에서 로직 처리UserViewModel
: presentation에서 상태 처리
Fallback 이란?
API가 터졌을 때 Room에서 데이터를 보여주는 전략. 백그라운드 안전장치 같은 개념이다.
try {
val user = api.getUser()
} catch (e: Exception) {
val fallback = dao.getUser()
}
Test Mock 이란?
실제 서버 없이 로직이 잘 도는지 확인하고 싶을 때 가짜 구현(Fake)을 만들어서 테스트하는 방식.
class FakeUserRepository : UserRepository {
override suspend fun getUser(userId: String): User = User("1", "테스트용")
}
마치며
지금은 이 구조에 적응했지만, 처음엔 “이게 진짜 실무에 맞나?” 싶었다. 그런데 앱이 점점 커지고 기능이 늘어나면서 이 구조 없이는 협업도 테스트도 어렵다는 걸 실감하게 됐다. 그래서 요즘은 개인 프로젝트든 팀 프로젝트든 습관처럼 클린 아키텍처를 도입하고 있다.