안드로이드 아키텍처 용어 사전
데이터SSOT · DataSource

Repository

데이터 출처를 감추고 단일 출처를 제공하는 계층. 오프라인 우선에서는 DB가 진실의 원천이다.

데이터가 어디서 오는지를 감추고 단일 출처를 제공하는 계층.

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,        // 원격
    private val dao: UserDao         // 로컬
) : UserRepository {

    override fun observeUsers(): Flow<List<User>> = dao.observeAll()   // 항상 DB 를 본다

    override suspend fun refresh() {
        val remote = api.getUsers()
        dao.insertAll(remote.map { it.toEntity() })   // 네트워크는 DB 를 갱신할 뿐
    }
}

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

화면 ──관찰──→ DB ←──갱신── 네트워크

· 화면은 네트워크를 직접 보지 않는다
· 오프라인에서도 마지막 데이터가 보인다
· 네트워크가 갱신하면 Flow 를 통해 화면이 자동으로 바뀐다
· 로딩 스피너 대신 '옛 데이터 + 갱신 중 표시' 가 가능해진다

이 구조가 안드로이드 공식 가이드의 권장 형태다. 화면이 두 출처를 신경 쓸 필요가 없어진다.

캐시 전략

// NetworkBoundResource 패턴 — 로컬을 먼저 주고 네트워크로 갱신
fun observeUsers(): Flow<Result<List<User>>> = flow {
    emit(Result.Loading)
    val cached = dao.getAll()
    if (cached.isNotEmpty()) emit(Result.Success(cached.toDomain()))
    try {
        val fresh = api.getUsers()
        dao.insertAll(fresh.toEntity())
    } catch (e: IOException) {
        if (cached.isEmpty()) emit(Result.Error(e))   // 캐시가 있으면 실패를 숨긴다
    }
}

Repository가 하지 말아야 할 것

  • Context · View · 안드로이드 프레임워크 참조
    • 테스트가 불가능해지고 의존성 규칙이 깨진다
  • UI 포맷팅 (날짜 문자열 만들기 등)
    • UI 계층의 일이다
  • 비즈니스 규칙
    • 도메인의 일이다. Repository 는 '가져오고 저장한다'

인터페이스를 왜 두나

interface UserRepository { ... }        // 도메인이 소유
class UserRepositoryImpl : UserRepository   // 데이터가 구현

· 테스트에서 가짜 구현으로 교체한다
· 의존성 역전이 성립한다

단, 구현이 하나뿐이고 테스트에서 목킹 라이브러리를 쓴다면
인터페이스가 순수한 간접층일 수 있다 — 팀의 선택이다

면접 함정

  • "Repository는 네트워크 호출 래퍼" → 여러 출처를 조율하고 단일 출처를 제공하는 것이 본질이다.
  • "오프라인 우선은 오프라인 앱만" → 온라인 앱에서도 체감 속도와 안정성이 크게 좋아진다.

여러 화면이 같은 데이터를 볼 때

// ❌ 각 ViewModel 이 따로 네트워크를 부르면 같은 요청이 중복된다
// ✅ Repository 가 Flow 를 공유한다
private val users = dao.observeAll()
    .shareIn(scope, SharingStarted.WhileSubscribed(5_000), replay = 1)

동시 갱신 요청을 어떻게 막나

private val refreshMutex = Mutex()

override suspend fun refresh() = refreshMutex.withLock {
    if (System.currentTimeMillis() - lastSync < 30_000) return@withLock   // 스로틀
    val remote = api.getUsers()
    dao.replaceAll(remote.map { it.toEntity() })
    lastSync = System.currentTimeMillis()
}

안 막으면

  • 화면 세 개가 동시에 열리며 같은 API 를 세 번 친다
  • 응답 순서가 뒤바뀌어 옛 데이터가 나중에 DB 를 덮는다

에러를 어디서 다루나

  • Repository — IOException · HttpException 을 도메인 에러로 번역한다

    • (NetworkUnavailable · Unauthorized · NotFound)
    • 상위 계층이 Retrofit 을 몰라도 된다
  • ViewModel — 도메인 에러를 사용자 메시지로 바꾼다

  • UI — 그린다

번역을 안 하면 ViewModel 이 HttpException.code() == 401 을 검사하게 된다 = Retrofit 이 UI 계층까지 새어 나온 것이다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Repository·UseCase — 단일 출처·비즈니스 캡슐화