데이터가 어디서 오는지를 감추고 단일 출처를 제공하는 계층.
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 계층까지 새어 나온 것이다