본문 바로가기
Android

[Android] 의존성 주입에 대해 완벽히 이해하기 (Hilt, DIP, IoC)

by 안솝우화 2022. 8. 24.
반응형

의존성 주입에 대해 헷갈리는 부분과 잘 이해가 안 가는 부분들을 모두 안드로이드에 대입해서 Kotlin으로 쉽게 설명해 보겠습니다

 

의존성 주입? 그게 뭐야?

다른 블로그에서도 많이 소개되어있고 많은 분들이 아시는 의존성 주입의 기본적인 의미는 바로 외부에서 객체를 주입해 준다는 것입니다. 코드로 확인해 보겠습니다.

class MainRepository {
	val dataSource = MainDataSource()
    
    fun getApi() = dataSource.get
}

위 코드는 MainDataSource의 객체를 직접적으로 MainRepository 클래스 안에서 생성합니다.

이렇게 하게 될 경우 MainRepository와 MainDataSource 간의 강한 결합도가 생기게 된다고 말합니다.

이렇게 되면 MainDataSource가 변경이 되면 MainRepository까지 영향을 끼칩니다. 때문에 아래와 같이 코드를 수정해야 합니다

class MainRepository(
	private val dataSource : MainDataSource
) {
	
    fun getApi() = dataSource.get
}

위 코드는 생성자 주입을 사용해 생성자로 MainDataSoucre를 주입해 주는 코드입니다.

일반적으로 이렇게 사용하려면 MainRepository클래스를 사용하는 곳에서 MainDataSource의 객체를 만들어 인자 값으로 넘겨주게 됩니다, 아래처럼 말이죠

fun test(){
	val dataSource = MainDataSource()
    val mainRepository = MainRepository(dataSource)
}

그런데 여기서 우리는 이러한 의문이 듭니다.

아니 주입해 주는 건 알겠는데.. 주입을 해준다고 해도 만약 MainDataSource에서 SubDataSource로 변경되면 그래도 영향이 전파가 되지 않아??

답은 바로 네, 맞습니다! 전파가 됩니다. 그럼 여기서 이런 생각이 들겠죠, 아니 의존성 주입해주면 의존도가 낮아지거나 없어져서 유지보수 측면에서 이점을 얻고 한다던데...

이것에 대한 답은 바로 DIP와 IoC에 있습니다.

 

의존 관계 역전 법칙 (DIP), 많이 들어 봤는데..?

개발하시면서 SOLID라는 말을 많이 들어보셨을 겁니다. 이것은 객체지향의 5가지 원칙인데 DIP는 이것 중에 하나에 속합니다.

의존 관계 역전 법칙은 상위 모듈이 하위 모듈에 의존하면 안 되고 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다는 법칙입니다. 바로 코드로 보겠습니다

class MainRepository {
	val dataSource = MainDataSource()
    
    fun getApi() = dataSource.get
}

class MainDataSource {

	fun get(){
    	...
    }
}

방금 앞에서 설명했던 코드입니다. 여기서 MainRepository클래스가 상위 모듈이고 MainDataSource클래스가 하위 모듈입니다.

이 코드에서는 상위 모듈이 하위 모듈에 의존하고 있기 때문에 앞에 말한 의존 관계 역전 법칙이 지켜지지 않고 있습니다.

여기서 지켜지게 코드를 수정하려면 어떻게 해야 할까요?

class MainRepository {
	val dataSource = MainDataSource()
    
    fun getApi() = dataSource.get
}

interface DataSource {
	fun get()
}

class MainDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

class SubDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

이렇게 수정해 보았습니다. 여기서는 하위 모듈 MainDataSourceImpl는 추상화된 내용에 의존하지만 상위 모듈은 여전히 하위 모듈에 의존하고 있습니다. 다시 한번 수정해 보겠습니다.

class MainRepository(val dataSource : DataSource) {
    
    fun getApi() = dataSource.get
}

interface DataSource {
	fun get()
}

class MainDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

class SubDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

//사용하는 부분
fun test(){
	val dataSource = MainDataSourceImpl()
    val mainRepository = MainRepository(dataSource)
}

이번에는 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존하게 됩니다. 바로 의존성 주입(생성자 주입)을 사용했기 때문입니다.

여기서 만약 MainDataSourceImpl에서 SubDataSourceImpl로 교체를 해야 한다고 생각해 봅시다. 교체는 정말 간단합니다. 

class MainRepository(val dataSource : DataSource) {
    
    fun getApi() = dataSource.get
}

interface DataSource {
	fun get()
}

class MainDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

class SubDataSourceImpl : DataSource {

	override fun get(){
    	...
    }
}

//사용하는 부분
fun test(){
	val dataSource = SubDataSourceImpl() // 바뀐 부분
    val mainRepository = MainRepository(dataSource)
}

단지 인자로 넘겨주는 값을 SubDataSourceImpl로 바꿔주기만 하면 됩니다.

왜냐하면 MainRepository클래스의 생성자 자료형이 DataSource 인터페이스고 이 인터페이스를 MainDataSourceImpl, SubDataSourceImpl 모두 상속받았기 때문이죠.

이렇게 해주게 되면 이제 의존도가 낮아지고 변화에 유연하게 대체를 할 수 있습니다. 

이제 어느 정도 궁금증이 해소되었습니다. 하지만 우리가 실제 hilt나 의존성 주입 라이브러리를 사용하면 코드에서 저렇게 SubDataSourceImpl의 객체를 직접적으로 만들어서 사용하지 않습니다.

이것은 IoC와 연관이 있습니다

 

IoT는 들어봤는데 IoC는 뭐죠??

IoC는 한국말로 제어의 역전이라는 말입니다. 제어의 역전?? 그게 뭐예요 라는 질문이 생기실 겁니다.

위의 코드 중 fun test 부분을 다시 보며 설명해보겠습니다

fun test(){
	val dataSource = SubDataSourceImpl() // 바뀐 부분
    val mainRepository = MainRepository(dataSource)
}

여기서 test부분이 클라이언트 쪽이라고 생각해 보겠습니다. 지금 코드에서는 클라이언트가 SubDataSourceImpl도 알고 MainRepository에 SubDataSourceImpl를 넣어주는 일까지 하고 있습니다. 

이것을 우리는 클라이언트 쪽에서 모든 것을 제어하고 있는 구조라고 말할 수 있습니다.

그런데 만약 지금은 사용하는 부분이 한 군데이지만 MainRepository를 사용하는 부분이 100곳이라고 가정해 보겠습니다.

그런데 SubDataSourceImpl에서 다시 MainDataSourceImpl로 바꿔야 한다면 어떻게 해야 할까요??

다시 개발자가 손으로 일일이 바꿔주는 건 객체 주입하는 100곳을 바꿔줘야 할까요? 아닙니다, 이것은 굉장히 비효율적이기 때문에 이러한 제어를 컨테이너에서 해주게 하면 되면 컨테이너만 바꿔주면 나머지 부분도 바뀌게 되는데 이게 바로 제어의 역전입니다. 

 

Hilt를 사용해 의존성 주입을 해보자!

우선 Module을 만들어 보겠습니다. DataSource에서 testApi부분과 Retrofit 등 NetworkModule은 이미 만들어져 있다고 가정하고 하겠습니다

@Module
@InstallIn(SingletonComponent::class)
class DataSourceModule {

    @Provides
    fun provideMainDataSource(
 		testApi : TestApi // 예로 넣은 것이기 때문에 무시하셔도 됩니다
    ) : DataSource {
        return MainDataSourceImpl(
            testApi // 예로 넣은 것이기 때문에 무시하셔도 됩니다
        )
    }

    @Provides
    fun provideSubDataSource(
        testApi : TestApi // 예로 넣은 것이기 때문에 무시하셔도 됩니다
    ) : DataSource {
        return SubDataSourceImpl(
            testApi // 예로 넣은 것이기 때문에 무시하셔도 됩니다
        )
    }
}

이렇게 DataSource에 대한 모듈을 만들어주었으니 이제 Repository에 대한 모듈을 만들어 주겠습니다.

(실제 안드로이드에서 개발할 때는 클린 아키텍처를 지향하기 때문에 Repository와 UseCase까지 존재해야 하며 Repository도 인터페이스로 구현해야 하지만 예시기 때문에 그냥 진행하겠습니다)

@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {

    @Provides
    fun provideMainRepository(
        mainDataSource : MainDataSource
    ): MainRepository {
        return MainRepository(
            mainDataSource
        )
    }
}

이렇게 다 만들어 주었으면 실제 코드에서 사용해 봅니다 (보통 View단에 바로 적용하기보다는 ViewModel에 주입받기 때문에 ViewModel에서 적용하겠습니다)

@HiltViewModel
class MainViewModel @Inject constructor(
    private val mainRepository : MainRepository
) : ViewModel() {

	fun test(){
    	mainRepository.getApi()
    }
}

이런 식으로 직접적으로 MainRepository 객체를 생성하지 않아도 hilt가 제어의 역전을 해주기 때문에 간편하게 사용할 수 있게 됩니다.

이제 얼추 어느 정도 의존성 주입이 필요한 이유와 왜 어떻게 의존성을 주입하는지에 대해 알게 되셨을 거라 생각됩니다!

반응형