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

Repository·UseCase — 데이터 단일 출처와 비즈니스 캡슐화

데이터 계층의 얼굴이 Repository, 도메인 계층의 일꾼이 UseCase다. Repository는 데이터가 어디서 오는지(API·DB·캐시)를 감추고 단일 출처가 되며, UseCase는 하나의 비즈니스 동작을 캡슐화해 재사용·테스트를 쉽게 한다. 이 둘이 ViewModel과 데이터 소스 사이를 잇는다. (출처: Android — Data layer / Domain layer.)

Repository — 데이터의 단일 출처(SSOT)

Repository는 여러 데이터 소스(원격 API·로컬 DB·인메모리 캐시)를 조율상위 계층엔 하나의 일관된 인터페이스만 보여준다. ViewModel은 데이터가 캐시에서 왔는지 네트워크에서 왔는지 모른다 — 그저 Repository에 요청한다.

다이어그램 로딩 중…
interface MemberRepository {
    fun observeMembers(): Flow<List<Member>>   // DB를 구독 — 변경 시 자동 방출
    suspend fun refresh()                       // 네트워크에서 받아 DB 갱신
}

class MemberRepositoryImpl(
    private val api: MemberApi,
    private val dao: MemberDao,
) : MemberRepository {
    override fun observeMembers() = dao.observeAll().map { it.toDomain() }
    override suspend fun refresh() {
        val remote = api.getMembers()            // 네트워크
        dao.upsertAll(remote.toEntities())       // 로컬 DB 갱신 → observe가 자동 방출
    }
}

오프라인 우선 — DB를 진실의 원천으로

현대 안드로이드의 권장은 오프라인 우선(offline-first)로컬 DB(Room)를 SSOT로 두고, UI는 DB를 구독하며, 네트워크는 DB를 갱신할 뿐이다. 그래서 네트워크가 끊겨도 앱이 동작하고, 네트워크 갱신이 DB를 통해 UI에 반영된다(단방향). Flow로 DB를 구독하면 데이터 변경이 화면까지 자동으로 흐른다.

UseCase — 하나의 비즈니스 동작

UseCase(Interactor)는 하나의 비즈니스 규칙·동작을 캡슐화한다 — 여러 Repository를 조합하거나 복잡한 규칙을 담아, ViewModel을 얇게 유지하고 로직을 재사용·테스트하게 한다. 코틀린에선 *operator fun invoke*로 함수처럼 호출하는 관용구를 쓴다.

class GetVisibleMembersUseCase(
    private val repo: MemberRepository,
    private val settings: SettingsRepository,
) {
    operator fun invoke(): Flow<List<Member>> =
        combine(repo.observeMembers(), settings.showHidden()) { members, showHidden ->
            if (showHidden) members else members.filter { it.visible }
        }
}

// ViewModel에서 — 함수처럼 호출
val members = getVisibleMembers()        // invoke 덕에 () 로 호출

언제 UseCase를 두나여러 Repository를 합치거나, 같은 로직을 여러 ViewModel이 쓰거나, 규칙이 복잡할 때. 단순 위임뿐이면 생략(ViewModel이 Repo 직접 호출)해도 된다 — 불필요한 패스스루 UseCase는 보일러플레이트다.

결과 표현 — Result로 성공/실패

데이터 호출은 실패할 수 있으니 Result나 sealed 타입으로 성공/실패를 명시해 ViewModel이 처리하게 한다.

suspend fun refresh(): Result<Unit> = runCatching {
    dao.upsertAll(api.getMembers().toEntities())
}   // ViewModel: .onSuccess{} .onFailure{}

정리 — 감추고(Repository) 캡슐화한다(UseCase)

  • Repository: 데이터 소스를 감추는 단일 출처(SSOT). 오프라인 우선으로 DB를 진실의 원천에 두고 Flow로 구독. 네트워크는 DB를 갱신.
  • UseCase: 하나의 비즈니스 동작을 캡슐화(여러 Repo 조합·복잡 규칙). invoke로 함수처럼. 단순 위임이면 생략.

이 둘이 ViewModel을 얇게, 데이터 흐름을 단방향으로 만든다.

한 줄 요약 — Repository=데이터 소스(API·Room) 감추는 단일 출처, 오프라인 우선(DB를 SSOT·Flow 구독·네트워크는 DB 갱신). UseCase=하나의 비즈니스 동작(여러 Repo 조합·operator invoke), 단순 위임이면 생략. 실패는 Result로.

(출처: Android — Data layer / Offline-first / Domain layer / Now in Android.)

Clean Architecture — domain·data·presentation의존성 주입 — Hilt·Koin