Android

(Android) 의존성 주입(Dependency Injection) 정의 및 예시 + Dagger-Hilt 활용

돗개진 2024. 9. 2. 21:30

 

의존성 주입 (= DI (Dependency Injection))

-> 클래스와 클래스 간 관계를 맺을 시 내부에서 직접 생성하는 것이 아닌 외부에서 주입을 함으로써 관계를 맺게 만드는 것을 의미함

 

=> 인터페이스화를 통하여 객체 변경에 대한 유연성을 증대, 객체를 내부에서 생성하는 것이 아닌 Container에서 생성하여 (= 제어의 역전) 주입하는 것을 의미

 

정리

  • 인터페이스화를 통한 객체 참조 변경의 유연성 증대
  • 객체 내부에서 생성하는 것이 아닌 DI Container 에서 생성하여 주입 (= 제어의 역전)

 

사용 이유

  • 결합도 감소 : 객체가 직접 다른 객체를 생성할 시 두 객체 간의 결합도가 높아짐. DI를 사용하면 객체 간의 결합도가 낮아져 코드 수정 시 영향 범위가 줄어듦
  • 테스트 용이성 : 의존성이 주입되므로 테스트 시에는 실체 객체 대신 Mock 객체를 주입할 수 있음. 이에 따라 단위 테스트가 쉬워짐
  • 유연성 증가 : 객체 생성 방식이나 구성을 외부에서 결정할 수 있으므로 코드 수정 없이 다양한 환경에 맞게 유연하게 대처 가능

 

관점

  • 한 개의 클래스 관점
  • 다중 클래스 관점

 


 

한 개의 클래스 관점

fun main(args: Array) {
	val car = Car()
    car.start()
}

class Car {
	val engine = OilEngine()
    
    fun start() {
    	engine.startOilEngine()
    }
}

class OilEngine {

	fun startOilEngine() {
    		print("startOilEngine!!")
        }
}

 

 

자동차(Car) 내부에 엔진(Engine)이 포함되게 되어 있는데 자동차는 OilEngine 뿐만 아니라 ElectronicEngine 을 사용하기도 하기 때문에 해당 내부 구조를 변경하게 된다.

 

fun main(args: Array) {
    val car = Car()
    car.start()
}

class Car {
    val engine = ElectronicEngine() // OilEngine -> ElectronicEngine으로 변경함
    
    fun start() {
        engine.startOilEngine() // 이부분 컴파일 에러!
    }
}


class ElectronicEngine {
    
    fun startElectronicEngine() {
        print("startElectronicEngine!!")
    }
}

 

 

ElectronicEngine 클래스를 추가하고 해당 엔진을 구동하기 위한 함수도 추가했다. 하지만 이렇게 되면 Car 클래스의 engine.startOilEngine() 에서 오류가 발생한다. 때문에 해당 부분을 engine.startElectronicEngine() 으로 수정해 줘야 한다.

 

하지만 만약 OilEngine 을 사용하는 클래스가 예시처럼 한 군데가 아니라 100개가 넘는 곳에서 사용하고 있었다면 이를 일일히 바꾸는 것은 매우 번거롭고 비효율적인 일이 될 것이다. 이를 해결하려면 어떻게 해야 할까?

 


 

인터페이스를 사용하여 객체 변경에 대한 유연성을 증대

fun main(args: Array) {
    val car = Car()
    car.start()
}

class Car {
    val engine: Engine = ElectronicEngine()
                   OR
    val engine: Engine = OilEngine()
                   OR
    val engine: Engine = GasolineEngine()
    
    fun start() {
        engine.startEngine()
    }
}

interface Engine {
    fun startEngine()
}

class ElectronicEngine: Engine {
    
    override fun startEngine() {
        print("startElectronicEngine!!")
    }
}

class OilEngine: Engine {
    
    override fun startEngine() {
        print("startOilEngine!!")
    }
}

class GasolineEngine: Engine {
    
    override fun startEngine() {
        print("startGasolineEngine!!")
    }
}

 

위 코드에는 Engine 인터페이스를 정의했다. (인터페이스는 정의만 하고 구현하지 않는다.) 그리고 자식 클래스가 해당 인터페이스를 상속받게 함으로써 Engine 종류 객체 변경에 유연하도록 만들 수 있다.

 

 

1. Engine 인터페이스 설계

2. 하위에 자식 클래스 설계

3. main 에서 부모 타입의 변수로 자식 클래스 참조

 

이렇게 interface 를 통하여 객체 변경에 대한 유연성을 증대할 수 있다. Engine 객체가 Oil 이든 Electronic 이든 Gasoline 이든 문제 없다.

 


 

다중 클래스 관점

위처럼 Car 클래스가 단 하나라는 가정에선 문제 없겠지만 세상엔 차의 종류가 매우 다양하다. 그리고 Car 클래스 내부에는 Engine 객체를 직접 포함하고 있기 때문에 Engine 객체가 바뀔 때마다 또 일일히 코드를 수정해야 하는 불편함이 생길 수 있다.

 

class SonataCar {
    val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
    
    fun start() {
        engine.startEngine()
    }
}

class AvanteCar {
    val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
    
    fun start() {
        engine.startEngine()
    }
}

class JenesissCar {
    val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
    
    fun start() {
        engine.startEngine()
    }
}

class PalisadeCar {
    val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
    
    fun start() {
        engine.startEngine()
    }
}

 

 

직접 참조하고 있는 OilEngine 을 ElectronicEngine 으로 교체하기 위해서 어떤 방법을 사용해야 할까?

 

 

바로 해당 방법을 사용하는 것이다. 이 방법은 외부 Container 에서 객체를 생성하게 함으로써 (제어의 역전) 각 Car 클래스 내부로 주입하는 것이다. 그리고 해당 방법이 바로 DI의 사용이라고 할 수 있다!

 

! Engine 클래스 생성에 대한 권한은 Car 클래스가 아니라 Container 클래스가 담당하게 됨 !

 

 

class SonataCar {

    @Inject
    lateinit var engine: Engine
    
    fun start() {
        engine.startEngine()
    }
}

class AvanteCar {

    @Inject
    lateinit var engine: Engine
    
    fun start() {
        engine.startEngine()
    }
}

class JenesissCar {

    @Inject
    lateinit var engine: Engine
    
    fun start() {
        engine.startEngine()
    }
}

class PalisadeCar {

    @Inject
    lateinit var engine: Engine
    
    fun start() {
        engine.startEngine()
    }
}

@Module
@InstallIn(ApplicationComponent::class)
object EngineModule {

  @Provides
  fun provideElectronicEngine(): Engine {
      return ElctronicEngine()
  }
}

 

위 코드를 보면 새로운 컨테이너를 정의해 주었는데 이는 EngineModule 이다. 해당 모듈을 생성해 줌으로써 Engine 객체들을 각각의 Car 클래스 내부에서 관리하지 않아도 된다. 오로지 새로 정의한 컨테이너에서 이를 관리해 주면 된다. (이로써 의존성 주입(DI) 구현)

 

Engine 객체를 관리하는 포인트가 각각의 Car 클래스인 여러 곳들이었다면 이젠 한 곳의 포인트인 EngineModule 에서만 객체를 관리하면 된다.

 

만약 OilEngine 객체를 다시 주입시키고 싶다면 EngineModule만 수정하면 된다!

 


Dagger-Hilt 주요 어노테이션 정리

  • @Provides : Dagger-Hilt 모듈에서 의존성을 제공하는 메서드에 사용되며 이 메서드를 통해 객체를 생성하고 필요할 때 주입한다.(특정 타입의 객체를 어떻게 생성하고 제공할지 정의하는 메서드에 붙임)
  • @Inject : 클래스나 필드에 의존성을 주입하기 위해 사용되며 해당 어노테이션이 붙은 생성자나 필드에 필요한 의존성을 자동으로 주입한다. (*필드 = 클래스 내에서 정의된 변수, ex: lateinit var engine 등)
  • @Module : Dagger-Hilt 에서 모듈을 정의하는 데 사용되며 이 모듈은 @Provides 메서드를 포함하여 의존성을 제공하는 역할을 한다.
  • @InstallIn : 모듈의 생명 주기와 범위를 정의하기 위해 사용된다. 예를 들어 ApplicationComponent, ActivityComponent, FragmentComponent 등에 모듈을 설치할 수 있다.

 

참고

https://velog.io/@squart300kg/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%99%9C-%EC%93%B0%EB%8A%94%EA%B0%80