Hanbit the Developer

Android Dependent Use Case 이슈 해결 본문

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이 들어갔을 것임) 개선 방안을 생각하면 좋을 듯 하다. 엔티티에서 안드로이드 종속 필드를 제거하고, 관련 내용을 기능 모듈에서 처리한다든지..?

 

(추가: 2024-08-23)

앱을 업데이트 하고 이미지가 안 보이거나 다른 리소스를 보여주는 문제가 발생했습니다. 이전 버전에서 유효하던 리소스 ID가, 앱이 다시 빌드되고 업데이트 한 이후에 유효하지 않게 된 것으로 보입니다. 리소스 ID 대신 리소스의 이름을 기반으로 동적으로 리소스를 가져오는 방법이 있습니다. 하지만 제 경우 이미지 리소스가 사용자에게 자주 보여지는 이미지가 아니기 때문에 리모트 URL을 읽는 방식으로 변경하게 되었습니다.(firebase 사용)

 

참고: https://stackoverflow.com/questions/24837083/does-resource-id-changes-everytime-an-application-starts