하나의 비즈니스 동작을 캡슐화한 클래스.
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) // 순수한 비용
- }
- class GetUserUseCase(private val repo: UserRepository) {
"모든 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 를 반환하면서 안에서 부수효과를 내면 구독할 때마다 그 효과가 반복된다