안드로이드 공식 가이드가 권장하는 UI · 도메인 · 데이터 3계층 구조.
무엇을 푸나 — god Activity
한 Activity 안에
- 뷰 조작 + 네트워크 호출 + DB 접근 + 비즈니스 규칙 + 상태 관리
- 2,000줄짜리 클래스가 된다
결과
- 테스트가 불가능하다 (Android 프레임워크 없이는 못 돌린다)
- 화면 회전마다 상태가 사라진다
- 어디를 고쳐야 할지 알 수 없다
세 계층
-
UI 레이어 — 화면에 상태를 그리고 사용자 이벤트를 위로 올린다
- Activity/Fragment/Composable + ViewModel
-
도메인 레이어 — 비즈니스 규칙. 선택적이다
- UseCase — 여러 Repository 를 조합하거나 복잡한 규칙을 담는다
-
데이터 레이어 — 데이터의 단일 출처
- Repository + DataSource(원격·로컬)
의존성 규칙 — 위에서 아래로만
-
UI → 도메인 → 데이터
-
아래는 위를 모른다 (Repository 가 ViewModel 을 참조하지 않는다)
-
건너뛰기는 허용된다 (도메인이 없으면 UI → 데이터)
-
역방향은 절대 금지
"Repository가 Context를 알면" 이미 규칙이 깨진 것이다 — 안드로이드 프레임워크 의존이 아래로 새고 있다는 신호다.
SSOT — 단일 출처 원칙
-
같은 데이터의 출처는 한 곳이어야 한다
-
그 데이터를 바꾸는 방법도 그곳만 제공한다
-
다른 곳은 읽기만 한다
-
오프라인 우선 앱에서는 대개 로컬 DB 가 SSOT 다
- 네트워크는 DB 를 갱신할 뿐, 화면은 항상 DB 를 본다
UDF — 단방향 데이터 흐름
상태는 아래로 흐르고, 이벤트는 위로 올라간다
ViewModel ──상태(StateFlow)──→ UI
ViewModel ←──이벤트(함수 호출)── UI
양방향이면 "누가 이 값을 바꿨나" 를 추적할 수 없다
단방향이면 상태 변경 경로가 하나뿐이라 예측 가능하다
도메인 계층은 선택이다
필요한 경우
- 여러 ViewModel 이 같은 로직을 쓴다
- 여러 Repository 를 조합해야 한다
- 비즈니스 규칙이 복잡하다
필요 없는 경우
- ViewModel 이 Repository 를 그냥 부르면 되는 단순 CRUD
- UseCase 가 Repository 를 한 줄 위임만 하면 순수한 비용이다
면접 함정
- ❌ "3계층은 반드시 지켜야 한다" → 도메인은 선택이다. 공식 가이드도 그렇게 말한다.
- ❌ "MVVM이 곧 3계층" → MVVM은 UI 계층 안의 패턴이다. 계층 분리와 다른 축이다.
계층을 나눈 뒤에도 흔한 실수
- ViewModel 이 Retrofit 인터페이스를 직접 주입받는다
- 데이터 계층을 건너뛴 게 아니라 계층을 없앤 것이다
- Repository 가 LiveData 를 반환한다
- LiveData 는 UI 계층 타입이다. 데이터 계층은 Flow 를 반환한다
- 도메인 모델에 @SerializedName 이 붙어 있다
- 네트워크 스키마가 도메인까지 올라온 것이다
계층이 지켜지는지 어떻게 확인하나
// 멀티 모듈이면 컴파일러가 강제한다
// :core:data 가 :feature:home 을 의존하지 않으므로 역방향 참조가 아예 불가능하다
// 단일 모듈이면 정적 분석으로 막는다 (Konsist · ArchUnit 계열)
@Test fun `데이터 계층은 UI 계층을 모른다`() {
Konsist.scopeFromPackage("..data..")
.classes()
.assertFalse { it.hasImport { i -> i.name.contains(".ui.") } }
}
정리
계층 분리의 목적은 '멋진 구조' 가 아니라
- 변경의 파급을 좁히고
- 테스트 가능한 조각을 만드는 것 이다. 그 둘이 안 되면 계층을 나눈 의미가 없다