Hanbit the Developer

Android Dependent Use Case 이슈 해결 본문

Mobile/Android

Android Dependent Use Case 이슈 해결

hanbikan 2024. 2. 5. 16:16

문제 상황

앱에서 계정을 생성했을 때 그 계정에 종속되는 TutorialTask 리스트를 초기화해야 한다. AddUserUseCase 클래스에서 InitializeTutorialTasksUseCase를 호출하고자 한다:

class AddUserUseCase @Inject constructor(
    private val userRepository: UserRepository,
    private val appStateRepository: AppStateRepository,
    private val initializeTutorialTasksUseCase: InitializeTutorialTasksUseCase,
) {
    suspend operator fun invoke(user: User) {
        val addedUserId = userRepository.insertUser(user)

        initializeTutorialTasksUseCase(addedUserId) // Here!
    // ...
}

문제는 InitializeTutorialTasksUseCase를 구현할 때 발생한다. core.domain 모듈은 의존성 그래프 상 최하단에 존재하며 순수 Kotlin 모듈인데, TutorialTask를 생성할 때 R.drawable.rocks 와 같은 안드로이드에 의존하는 코드가 필요하다는 점이 문제이다.

TutorialTask(day = 2, name = "...", detailImageId = R.drawable.eight_rocks, userId = userId, isDone = false),

Attemption

Hilt로 의존성 주입을 하면 해결되지 않을까 하고 List<TutorialTask>를 반환하는 provides 함수를 구현을 해보았는데, 간과한 것이 있었다. TutorialTask가 userId에 의존하기 때문에 상황에 맞는 userId 인자가 필요하다는 것이다. 따라서 다른 방법이 필요하다.

우선 고정할 수 있는 것부터 고정해보자.

안드로이드에 의존하기 때문에 TutorialTask 인스턴스 생성 함수를 상위 모듈에 두어야 한다:

// feature.tutorial module
// TutorialTask.kt
fun TutorialTask.Companion.createInitialTutorialTasks(userId: Int): List<TutorialTask> = listOf(
    // ...
    TutorialTask(day = 2, name = "...", detailImageId = R.drawable.eight_rocks, userId = userId, isDone = false),
    // ...
)

다음 스텝은 어떤 ‘마법’이 일어나서 core.domain에서 상위 모듈에 있는 createInitialTutorialTasks 를 호출할 수 있어야 한다. 아래 코드에서 당연히 createInitialTutorialTasks 를 호출할 수 없다:

class InitializeTutorialTasksUseCase @Inject constructor(
    private val tutorialTaskRepository: TutorialTaskRepository,
) {
    suspend operator fun invoke(userId: Int) {
        // val tutorialTasks = TutorialTask.createInitialTutorialTasks(userId)
        tutorialTaskRepository.insertTutorialTasks(tutorialTasks)
    }
}

Solution

위 코드가 상위 안드로이드 모듈에서 실행되어야만 한다는 점에서 떠오른 방법이다. 유스케이스의 인터페이스를 core.domain에 두고 상위 모듈에서 이를 구현하고 Hilt로 구현체를 전달하는 방식이다:

core.domain module

/**
 * 특정 계정에 대한 [TutorialTask]를 초기화하는 유스케이스입니다. [TutorialTask]가 android resource id에 의존하기
 * 때문에 상위 안드로이드 모듈에서 DI로 구현체를 넣어주는 방식을 선택하였습니다.
 */
interface InitializeTutorialTasksUseCase {
    suspend operator fun invoke(userId: Int)
}

feature.tutorial module

class InitializeTutorialTasksUseCaseImpl @Inject constructor(
    private val tutorialTaskRepository: TutorialTaskRepository,
) : InitializeTutorialTasksUseCase {
    override suspend operator fun invoke(userId: Int) {
        val tutorialTasks = TutorialTask.createInitialTutorialTasks(userId)
        tutorialTaskRepository.insertTutorialTasks(tutorialTasks)
    }
}
@Module
@InstallIn(SingletonComponent::class)
interface InitializeTutorialTasksUseCaseModule {
    @Binds
    fun bindsInitializeTutorialTasksUseCase(
        initializeTutorialTasksUseCaseImpl: InitializeTutorialTasksUseCaseImpl,
    ): InitializeTutorialTasksUseCase
}

마치며

사실 엔티티에 안드로이드 리소스 id가 있는 것 자체가 문제가 된다. 도메인, 데이터 영역을 UI로 물들인 데다가 만약 리소스가 변경되면 일일이 고쳐줘야 할 수도 있다.

서버 개발자 없이 혼자 하는 토이 프로젝트여서 불가피 하긴 했지만(서버가 들어갔다면 resource id 대신 imageUrl이 들어갔을 것임) 개선 방안을 생각하면 좋을 듯 하다. 엔티티에서 안드로이드 종속 필드를 제거하고, 관련 내용을 기능 모듈에서 처리한다든지..?