안드로이드 아키텍처 용어 사전
도메인Interactor

UseCase

하나의 비즈니스 동작을 캡슐화한 클래스. 여러 Repository를 조합하거나 규칙이 복잡할 때 쓴다.

하나의 비즈니스 동작을 캡슐화한 클래스.

class GetUserFeedUseCase @Inject constructor(
    private val userRepo: UserRepository,
    private val postRepo: PostRepository
) {
    // operator invoke 로 함수처럼 부른다
    suspend operator fun invoke(userId: Long): Feed {
        val user = userRepo.getUser(userId)
        val posts = postRepo.getPosts(user.followingIds)
        return Feed(user, posts.filter { !it.isBlocked })   // 조합 + 규칙
    }
}

// 호출부
val feed = getUserFeed(userId)

언제 만드나

만들 이유가 있다

  • 여러 Repository 를 조합한다
  • 여러 ViewModel 이 같은 로직을 쓴다
  • 비즈니스 규칙이 복잡해 테스트를 따로 하고 싶다

만들지 말아야 한다

  • Repository 를 한 줄 위임만 한다
    • class GetUserUseCase(private val repo: UserRepository) {
      • suspend operator fun invoke(id: Long) = repo.getUser(id) // 순수한 비용
    • }

"모든 Repository 호출마다 UseCase를 만든다" 가 가장 흔한 과설계다. 파일만 두 배가 되고 얻는 것이 없다.

규칙

  • 하나의 UseCase 는 하나의 동작만 (public 메서드 하나)
  • 이름이 동사로 시작한다 (GetUser · RefreshFeed · SubmitOrder)
  • 상태를 갖지 않는다 (호출마다 독립적)
  • 안드로이드 프레임워크를 모른다 (순수 코틀린)
  • Dispatcher 를 안에서 정하지 않고 주입받는다 (테스트 가능하게)

결과를 어떻게 표현하나

sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(val exception: Throwable) : Result<Nothing>
    data object Loading : Result<Nothing>
}
// 예외를 던지는 대신 타입으로 표현하면 처리를 강제할 수 있다
when (val result = getUserFeed(userId)) {
    is Result.Success -> render(result.data)
    is Result.Error -> showError(result.exception)
    Result.Loading -> showSpinner()
}
예외 vs Result
  예외    코틀린에 검사 예외가 없어 처리를 강제하지 못한다
          예상 가능한 실패(네트워크 없음)를 예외로 다루면 흐름이 흩어진다
  Result  처리를 컴파일러가 강제한다 (sealed + when)
          대신 코드가 조금 늘어난다

예상 가능한 실패는 Result, 프로그래밍 오류는 예외 — 로 나누는 것이 관용구다

면접 함정

  • "UseCase는 필수 계층" → 공식 가이드도 선택이라고 명시한다.
  • "UseCase가 ViewModel을 대체한다" → ViewModel은 UI 상태를 보유하고, UseCase는 비즈니스 동작이다.

Dispatcher를 주입받는 이유

class ParseLogUseCase @Inject constructor(
    @IoDispatcher private val dispatcher: CoroutineDispatcher
) {
    suspend operator fun invoke(raw: String): List<LogEntry> =
        withContext(dispatcher) { heavyParse(raw) }
}

안에서 Dispatchers.IO 를 직접 쓰면

  • 테스트에서 실제 스레드 풀이 돈다 → 느리고 비결정적이다

주입받으면 테스트에서 StandardTestDispatcher 를 넣어

  • 가상 시간으로 즉시 끝낸다

흐름을 반환하는 UseCase

class ObserveFeedUseCase @Inject constructor(
    private val userRepo: UserRepository,
    private val postRepo: PostRepository
) {
    operator fun invoke(userId: Long): Flow<Feed> =
        combine(userRepo.observe(userId), postRepo.observeAll()) { user, posts ->
            Feed(user, posts.filter { it.authorId in user.followingIds })
        }
}
  • suspend 반환 — 한 번 가져온다 (버튼 눌러 제출)
  • Flow 반환 — 계속 관찰한다 (목록 화면)

둘을 섞지 않는다 — Flow 를 반환하면서 안에서 부수효과를 내면 구독할 때마다 그 효과가 반복된다

함께 보면 좋은 용어

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