멀티 바인딩은 여러 개의 객체를 하나의 컬렉션으로 주입받을 수 있게 해주는 Dagger 2의 고급 기능이다. 이 기법은 플러그인 아키텍처나 확장 가능한 시스템을 구현할 때 특히 유용하다. 주의 할점은 많은 수의 객체를 바인딩할 경우 초기화 시간이 늘어날 수 있다.

멀티 바인딩 종류

Set 멀티바인딩

set 멀티 바인딩은 @Intoset 어노테이션을 사용하여 제공된 객체를 set에 추가한다.

보통 여러 전략 패턴 구현체를 동시에 사용해야할 때 사용한다.

예를들어 많은 안드로이드 앱은 여러 데이터 소스(로컬 데이터베이스, 네트워크 API, 캐시 등)를 사용한다. 이러한 다중 데이터 소스를 효과적으로 관리하고 사용하는 예제를 통해 멀티 바인딩의 실제 활용을 알아보자

interface DataSource {
    suspend fun getUserData(userId: String): UserData
    suspend fun saveUserData(userData: UserData)
}
class LocalDataSource @Inject constructor(
    private val database: AppDatabase
) : DataSource {
    override suspend fun getUserData(userId: String): UserData {
        return database.userDao().getUser(userId)
    }

    override suspend fun saveUserData(userData: UserData) {
        database.userDao().insertUser(userData)
    }
}

class RemoteDataSource @Inject constructor(
    private val apiService: ApiService
) : DataSource {
    override suspend fun getUserData(userId: String): UserData {
        return apiService.getUser(userId)
    }

    override suspend fun saveUserData(userData: UserData) {
        apiService.updateUser(userData)
    }
}

class CacheDataSource @Inject constructor(
    private val cache: UserCache
) : DataSource {
    override suspend fun getUserData(userId: String): UserData {
        return cache.getUser(userId)
    }

    override suspend fun saveUserData(userData: UserData) {
        cache.putUser(userData)
    }
}
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {

		@Singleton
    @Binds
    @IntoSet
    abstract fun bindLocalDataSource(dataSource: LocalDataSource): DataSource

		@Singleton
    @Binds
    @IntoSet
    abstract fun bindRemoteDataSource(dataSource: RemoteDataSource): DataSource

		@Singleton
    @Binds
    @IntoSet
    abstract fun bindCacheDataSource(dataSource: CacheDataSource): DataSource
}
class UserRepository @Inject constructor(
    private val dataSources: Set<DataSource>
) {
    suspend fun getUserData(userId: String): UserData {
        for (dataSource in dataSources) {
            try {
                return dataSource.getUserData(userId)
            } catch (e: Exception) {
                // 로그 기록 또는 에러 처리
            }
        }
        throw NoDataFoundException("User data not found in any data source")
    }

    suspend fun saveUserData(userData: UserData) {
        dataSources.forEach { dataSource ->
            try {
                dataSource.saveUserData(userData)
            } catch (e: Exception) {
                // 로그 기록 또는 에러 처리
            }
        }
    }
}
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _userData = MutableLiveData<UserData>()
    val userData: LiveData<UserData> = _userData

    fun loadUserData(userId: String) {
        viewModelScope.launch {
            try {
                _userData.value = userRepository.getUserData(userId)
            } catch (e: Exception) {
                // 에러 처리
            }
        }
    }

    fun saveUserData(userData: UserData) {
        viewModelScope.launch {
            try {
                userRepository.saveUserData(userData)
            } catch (e: Exception) {
                // 에러 처리
            }
        }
    }
}

이렇게 처리하면 새로운 데이터 소스를 추가할때 모듈에 바인딩만 하고 Set을 통해서 동일한 데이터 소스를 동일하게 다루며 테스트도 쉽게 mock 객체를 쉽게 주입할 수 있다.

Map 멀티바인딩

Map 멀티 바인딩은 @IntoMap을 통해 제공된 객체를 Map에 추가한다. 그리고 @stringKey는 Map의 키를 지정한다.