본문 바로가기

TIL

[TIL] Kotlin 의존성 주입 ( Dependency Injection ) 개념편

Topic =  의존성 주입의 개념에 대해서 학습해보자.

 

 


 

 

 

 

DI 의존성 주입 ( Dependency Injection ) 

 

 

의존성 주입이란??

 

OOP 원칙 중 DIP 원칙을 지키기 위해 사용되는 방법으로, 하나의 객체가 다른 객체의 의존성을 제공하는 방법을 일컫는다. '의존성'은 사용할 수 있는 객체라고 할 수 있다. 해당 객체를 사용하는 클라이언트가 어떤 사용할 수 있는 객체를 사용할 것인지 지정하는 대신 클라이언트에게 어떤 서비스를 사용할 것인지 알려주는 것이 의존성 주입이다 이때, 의존성(사용 가능한 객체) 을 클라이언트 (사용하는 객체)로 전달하는 것이 주입이라고 이해하면 좋을 것 같다.

 

사용할 수 있는 객체라는 단어에 추가적인 설명을 더하자면 의존성(사용 가능한 객체)은 자동차의 엔진, 바퀴와 같이 클라이언트 상태의 일부이다. Dog객체가 SpeakInEnglish라는 메서드를 사용할 수 없기 떄문.

 

의존성 주입의 핵심적인 의도는 객체의 생성과 사용의 관심을 분리하는 역할을 하는 것이다. 이로인해 자연스럽게 OOP의 DIP 원칙이 지켜지게 되는 것이다. 믹서기는 특정 물건을 갈아버리는 역할만 하면 된다. 이미 믹서기에 특정 물건을 주입하는 사람이 분리를 끝냈기 때문에 믹서기에 어떤 것을 넣는지는 믹서기가 고민할 필요가 없다. 

 

Car클래스와 Engine 클래스의 관계와 같이 특정 클래스가 다른 클래스의 참조를 필요한 상황이 존재한다. 특정 클래스가 다른 클래스의 객체를 주입 받는 방법에는 여러가지 방법이 있다. 아래의 코드 예제를 보면서 추가적으로 알아보면 좋을 것 같다.

 

의존성 역전 원칙 X 코드 예제
class Car(){
    val engine = Engine()

    fun startEngine (){
        engine.operate()
    }
}

class Engine(){
    fun operate(){
        println("엔진 시작")
    }
}

fun main() {
    val car = Car()

    car.startEngine()
}
  • Car 클래스에서 Engine 클래스의 객체를 직접적으로 생성한 후 Engine 클래스의 operate 를 통해 startEngine 메서드를 실행하는 로직이다.
  • 위와 같은 코드를 작성할 경우 추상화 된 것에 의존하지 않고 Engine 클래스의 구체적인 구현에 종속되어 있어 상위 수준의 모듈이 하위 수준의 모듈에 의존하게 되어버린다. 이는 객체 간의 결합도가 높아져 객체 서로가 자신의 주요 기능에만 집중할 수 없게 되어버리고 확장 및 재사용에 어려움이 생겨 유연성과 확장성 마저 떨어지게 된다.
  • 이를 해결하기 위해 DIP 원칙을 지키도록 추상화된 인터페이스를 통해 접근하도록 로직을 만들어 주어야 한다.

 

 

의존성 주입 적용 - [ 생성자 주입 ]
  • main 에서 car 클래스 객체가 생성될 때 component 객체를 주입받아 사용되는 생성자 주입 방식
class Car(private val component: Component){

    fun startEngine (){
        component.operate()
    }
}

fun main() {

    val component = Component()
    val car = Car(component)

    car.startEngine()
    
}

class Component{
    fun operate()
}
  • 주입받을 의존성은 클래스를 인스턴스화할 때 반드시 전달되어야 하므로, 의존성이 필수적인 경우에 적합하다.
  • 코드상에서 명확하게 의존성을 표현하고, 클래스의 불변성을 보장하는데 용이하다.
  • 의존성 주입을 위한 코드가 명시적으로 존재하므로 코드의 가독성이 향상된다.

 

 

 

의존성 주입 적용 - [ 메서드 주입 ]
  • main에서 메서드를 사용하는 시점에서 매개변수를 통해 의존성을 주입하는 방식
class Car {
    private lateinit var engine: Engine

    fun setEngine(engine: Engine) {
        this.engine = engine
    }

    fun startEngine() {
        engine.operate()
    }
}

class Engine {
    fun operate() {
        println("엔진 시작")
    }
}

fun main() {
    val car = Car()
    val engine = Engine()
    
    car.setEngine(engine)

    car.startEngine()
}

 

 

의존성 주입 적용 - [ 인터페이스 주입 ]
  • 인터페이스 주입 방식은 생성자 주입 방식, 메서드 주입 방식과 함께 사용된다.
class Car(private val component: Component){

    fun startEngine (){
        component.operate()
    }
}

fun main() {

    val component = ComponentImpl()
    val car = Car(component)

    car.startEngine()
    
}

interface Component{
    fun operate()
}

class ComponentImpl():Component{
    override fun operate() {
        println("엔진시작")
    }
}
  • 인터페이스 주입 방식의 특징으로는 주입 받을 클래스가 특정 인터페이스를 구현하도록 하고, 의존성을 주입받을 인터페이스 메서드를 선언하는 방식으로 적용된다.
  • 인터페이스를 거치지 않고 A class객체와 B class 의존성 객체 사이의 의존성을 직접적으로 주입하는 방법에 반해 인터페이스를 통해 의존성에 접근하므로, 구체적인 구현체에 대한 의존성을 분리할 수 있어 클래스와 의존성 사이의 결합도를 낮출 수 있다. 새로운 구현체를 추가하기 위해 인터페이스를 구현하는 클래스만 작성하면 되므로 확장성이 높고 인터페이스를 통한 구현체를 사용하므로 재사용성 및 유연성을 높일 수 있다.

 

 

의존성 주입 적용 - [ 필드 주입 ]
  • 필드 주입은 Car 클래스 내부에 멤버 변수를 직접 주입하여 의존성을 설정하는 방식
class Car {
    lateinit var engine: Engine

    fun startEngine() {
        engine.operate()
    }
}

class Engine {
    fun operate() {
        println("엔진 시작")
    }
}

fun main() {
    val car = Car()
    car.engine = Engine()

    car.startEngine()
}
  • 필드 주입의 특징은 필요한 시점에 의존성을 주입할 수 있으므로 동적인 의존성 주입이 가능하며 간단하게 코드 작성이 가능하여 직관적이지만, 클래스의 내부 구조를 외부로부터 감추지 않으므로, 의존성의 가시성이 높아질 수 있다.

 

  • 그렇다면 어느 경우에 사용하면 좋을까?
    • 의존성이 고정되어 있고 클래스의 구조가 변경되지 않는다는 가정이 있고, 간단한 의존성만 필요한 경우 적합하다
    • 클래스 구조가 변경되며, 의존성이 고정되어 있지 않은 상황에 사용할 경우 클래스 내부에서 어떤 의존성이 사용되는지 명확하게 파악하기가 어렵다는 등의 문제가 발생할 수 있다.

 

 

 

 

Why - ViewModel을 생성할 때마다 ViewModelProviderFactory를 생성해야 하는가?  → Hilt Library

 

Firebase와 상호작용하는 로직을 추상화하는 Repository와 RepositoryImpl을 ViewModel에서 사용할 수 있도록 하고, Fragment에는 ViewModelProviderFactory를 통해 Repository의 의존성을 주입하여 Repository의 의존성을 주입 받아 ViewModel의 메서드를 접근하는 로직을 작성하였는데 각각 여러 Fragment에 여러 Repository가 필요할 경우 일일이 코드를 작성해주어야 하는 번거로움이 생기는데 이를 Hilt 라이브러리를 통해 의존성 주입을 적용할 수 있다.

 

✔️다음 글 에서 의존성 주입 라이브러리에 대해서 다루고 글 작성 후 링크 추가 예정

 

 

 

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. OOP 원칙 의존성 역전 원칙

 

B. Hilt 라이브러리 사용하기

 

B. 캐싱에 대해서도 학습이 필요할 것 같다 -  Paging 등등..

 


 

[오류,에러 등등]

1. 프로젝트 진행 내용의 구조적인 문제 외에 개념학습엔 특별한 오류는 없었다.

 


 

[느낀 점]

1. 개념학습이 중요한 것 같다

 

2. 학습 속도가 느린 느낌이다

 

 

 


[Reference]

 

// DI

https://developer.android.com/topic/architecture/data-layer?hl=ko

https://ko.wikipedia.org/wiki/%EC%9D%98%EC%A1%B4%EC%84%B1_%EC%A3%BC%EC%9E%85