안드로이드 아키텍처 학습 노트 목차

앱 아키텍처 개요 — 관심사 분리·권장 계층

작은 앱은 Activity 하나에 다 욱여넣어도 돈다. 그런데 화면·네트워크·DB·상태가 한곳에 뭉치면 — god Activity가 되어 테스트도 못 하고, 화면 회전에 상태가 날아가고, 변경이 무섭다. 아키텍처관심사를 계층으로 분리해 이 문제를 푼다. 구글은 *공식 "앱 아키텍처 가이드"*로 UI·도메인·데이터 3계층을 권장한다 — 이 장은 그 큰 그림이다. (출처: Android — Guide to app architecture.)

왜 계층으로 나누나 — god Activity의 문제

아키텍처가 없으면 모든 게 화면 클래스 한 곳에 쌓인다. 아래는 흔히 보는 god Activity다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(s: Bundle?) {
        // 네트워크 + 파싱 + DB + UI 갱신이 한 메서드에 뒤엉킴
        thread {
            val json = URL("https://api...").readText()   // 네트워크(메인 아니지만 직접)
            val list = parse(json)                          // 파싱(비즈니스 로직)
            db.insert(list)                                 // DB
            runOnUiThread { adapter.submit(list) }          // UI
        }
    }
}

이 코드의 문제는 세 가지가 한꺼번에 온다.

  • 비대함 — Activity가 UI·네트워크·파싱·DB를 다 책임져 수백 줄로 불어난다.
  • 테스트 불가 — 로직이 Activity(안드로이드 프레임워크)에 묶여 있어 JVM 단위 테스트가 안 된다(에뮬레이터 없이는 검증 불가).
  • 생명주기에 휘둘림 — 화면을 회전하면 Activity가 재생성되며 진행 중이던 작업·받아둔 데이터가 날아간다.

해법은 관심사 분리(separation of concerns)각 부분이 한 가지 일만 하도록 계층으로 쪼개는 것이다.

권장 3계층 — UI·도메인·데이터

다이어그램 로딩 중…
  • UI 계층화면에 상태를 그리고, 사용자 이벤트를 받는다. 화면(Compose/View) + *상태 홀더(ViewModel)*로 구성된다. ViewModel이 화면 상태를 보유하므로 회전으로 화면이 재생성돼도 상태가 살아남는다 — god Activity의 세 번째 문제를 푼다.
  • 도메인 계층(선택)복잡한 비즈니스 규칙이나 여러 Repository를 조합하는 로직을 UseCase로 캡슐화한다. 순수 코틀린이라 프레임워크 없이 단위 테스트가 쉽다. 단순한 앱은 생략하고 ViewModel이 Repository를 바로 쓴다([04]에서 자세히).
  • 데이터 계층Repository가 *데이터의 단일 출처(SSOT)*가 되어 원격(API)·로컬(DB) 소스를 조율한다. UI는 데이터가 캐시에서 왔는지 네트워크에서 왔는지 모른다 — 그저 Repository에 요청한다.

같은 기능을 계층으로 나누면 이렇게 된다.

// 데이터: 소스를 감추는 단일 출처
class MemberRepository(private val api: Api, private val dao: Dao) {
    fun observe(): Flow<List<Member>> = dao.observeAll()          // DB 구독
    suspend fun refresh() = dao.upsert(api.fetch())               // 네트워크→DB
}
// UI: 상태만 보유 (화면을 모름 → 테스트 가능)
class ListViewModel(repo: MemberRepository) : ViewModel() {
    val state = repo.observe().map { UiState.Success(it) }
        .stateIn(viewModelScope, WhileSubscribed(5000), UiState.Loading)
}

이제 네트워크·DB·로직은 Repository에서 JVM 단위 테스트가 되고, Activity는 상태를 그리기만 한다.

의존성 규칙 — 위에서 아래로만

핵심 규칙 — 의존성은 한 방향이다. UI → 도메인 → 데이터로만 의존하고, 반대는 없다. 데이터 계층은 UI를 모르고, 도메인은 어떤 화면이 쓰는지 모른다. 이 단방향 의존성이 주는 실질적 이득은 — 하위 계층을 독립적으로 테스트·교체할 수 있다는 것이다. 예를 들어 데이터 소스를 REST에서 GraphQL로, 또는 Room에서 다른 DB로 바꿔도 Repository 인터페이스만 같으면 UI·도메인 코드는 한 줄도 안 바뀐다.

단일 출처 원칙(SSOT)과 단방향 데이터 흐름(UDF)

두 원칙이 권장 아키텍처를 떠받친다.

  • 단일 출처(SSOT)어떤 데이터의 정답은 한 곳에만 있다(보통 Repository나 DB). 같은 데이터를 여러 곳에서 제각각 들고 있으면 불일치가 난다(화면 A는 갱신됐는데 화면 B는 옛 값). SSOT를 정해 두면 모두가 그 한 곳을 구독하므로 항상 일관된다.
  • 단방향 데이터 흐름(UDF)상태는 위(ViewModel)에서 아래(화면)로 흐르고, 이벤트는 아래에서 위로 올라간다.
다이어그램 로딩 중…

이 단방향성이 "상태가 어디서 바뀌는지"를 ViewModel 한 곳으로 모아 디버깅·테스트를 쉽게 한다([02 MVVM·UDF]에서 자세히).

Now in Android — 참조 구현

구글의 Now in Android(NiA) 샘플이 이 아키텍처의 공식 모범 답안이다 — 멀티 모듈·오프라인 우선·Compose·Hilt·StateFlow로 짜여 있어, 막연한 가이드가 아니라 실무 구조의 살아있는 레퍼런스다. 새 프로젝트의 패키지 구조·DI·테스트 전략을 정할 때 NiA를 베껴 시작하면 큰 실수를 피한다.

정리 — 책임을 나누면 변화에 강하다

아키텍처의 목적은 멋이 아니라 변화에 견디는 것이다 — UI·도메인·데이터로 관심사를 나누고, 의존성을 위에서 아래로만 흐르게 하며, **데이터는 단일 출처(SSOT)·상태는 단방향(UDF)**으로 둔다. 그러면 god Activity의 세 문제(비대·테스트 불가·생명주기 취약)가 모두 풀리고, 각 계층을 따로 테스트·교체할 수 있다. 다음 장들에서 MVVM·Clean·Repository·DI·멀티모듈로 이 그림을 채운다.

한 줄 요약 — god Activity(비대·테스트 불가·생명주기 취약)를 UI(화면+ViewModel)·도메인(UseCase, 선택)·데이터(Repository+소스) 3계층으로 분리. 의존성은 위→아래 단방향(하위 교체 자유), 데이터는 단일 출처(SSOT), 상태는 단방향 흐름(UDF). 참조 구현은 Now in Android.

(출처: Android — Guide to app architecture / Architecture recommendations / Now in Android(GitHub).)

MVVM·단방향 데이터 흐름(UDF)