[Android] DI(Dependency Injection) 의존성 주입이란?

오늘은 훌륭한 앱 아키텍쳐의 기반을 마련하는데 빼놓을 수 없는 개념인 DI(Dependency Injection)에 대해서 알아볼 것이다.

DI(Dependency Injection)의 개념


DI란 Dependency Injection의 약자로 의존성 주입을 의미한다.

의존성 주입은 외부에서 의존 객체를 생성하여 넘겨주는 것을 의미하고 하나의 객체가 다른 객체의 의존성을 제공하는 기술이다.

비유하자면 ‘의존성’은 서비스로 사용할 수 있는 객체이고 ‘주입’은 의존성(서비스)을 사용하려는 객체로 전달하는 것을 의미한다.

이를 일반적인 객체 생성과 비교한다면 일반적인 객체 생성의 경우 클래스 안에서 사용할 객체를 생성하지만 DI를 적용한 객체 생성은 외부에서 생성된 객체를 주입 받는다.

클래스가 필요한 객체를 얻는 방법

간단한 코드를 통해 예시를 한번 볼텐데 예를 들어 Car 클래스와 Engine 클래스가 있는 경우 Car 클래스는 실행되기 위해 Engine 클래스를 필요로 한다.

이러한 필요한 클래스(Engine)를 의존성이라 하고 흔히 클래스들은 다른 클래스 객체를 필요로 한다.

그렇다면 클래스가 필요한 객체를 얻는 방법 3가지를 한번 봐보자.

1. 클래스가 필요한 종속 항목을 구성

위 예시를 토대로 Car는 자체 Engine 인스턴스를 생성하여 초기화 한다.

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

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

이러한 방법으로 클래스가 필요한 객체를 얻는다면 Car와 Engine이 밀접하게 연결되어 있어 문제가 발생할 수 있다.

그말이 무슨 말이냐면 Car 인스턴스 안에 자신이 생성한 Engine 유형만을 사용하기에 만약 Engine의 유형이 1가지가 아닌 Gas와 Electric 두가지라고 한다면 두가지 유형의 Car 인스턴스를 생성해야 할 것이다.

그렇다면 코드의 재사용이 굉장히 어렵다는 단점을 가지게 된다.

게다가 이처럼 종속항목이 강하게 의존하고 있는 경우 다양한 테스트를 하기가 어려워지는데 Car가 실제 Engine 인스턴스를 사용하기에 다양한 테스트 상황에서 Engine을 수정할 수 없게된다.

2. 다른 곳에서 객체를 가져옴

Context getter 혹은 getSystemService()와 같은 일부 안드로이드 API가 이러한 방식으로 작동한다.

3. 객체를 매개변수로 전달받음

1번의 방법과 다르게 클래스가 구성될 때 종속 항목을 제공하거나 각 종속 항목이 필요한 함수에 전달할 수 있는 앱의 특성을 이용한 방법이다.

그래서 1번 방법처럼 Car 인스턴스 초기화 시 자체 Engine 객체를 구성하는 대신 Engine 객체를 생성자의 매개변수로 받는다.

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

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

위와 같은 방법을 사용할 경우에 앱은 Engine 인스턴스를 생성한 후 Car 인스턴스를 구성하게 된다.

그로인해 다양한 유형의 Engine을 Car에 전달하여 Car의 재사용이 가능해지며 다양한 시나리오를 테스트 할 수 있게된다.

이 3번째 방법이 바로 의존성 주입(DI)이다.



DI의 장점


그렇다면 위같이 의존성 주입(DI)을 사용했을 때 어떠한 장점을 가질 수 있을까?

DI 구현으로 인한 장점을 나열해보자면

코드의 가독성 높여줌

UNIT Test가 쉬워짐

코드의 재활용성 높여줌

객체 간의 의존성(종속성)을 직접 설정하여 줄이거나 없앨 수 있음

객체 간의 결합도를 낮추면서 유연하게 만들 수 있음



안드로이드에서의 DI


안드로이드에서 의존성 주입을 실행하는 두 가지 주요 방법에는 2가지가 있다.

생성자 삽입 : 위 예제와 같이 클래스의 종속 항목을 생성자에 전달하는 방법이다.

필드 삽입 (setter 삽입) : Activity나 Fragment 같은 특정 안드로이드 프레임워크 클래스는 시스템에서 인스턴스화하기 때문에 생성자 삽입이 불가능하다. 따라서 필드 삽입을 사용해 클래스가 생성된 후 종속 항목을 인스턴스화한다.

필드 삽입 예시

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

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



안드로이드 DI 라이브러리


위에서 설명한 예시처럼 클래스의 종속 항목을 직접 생성, 제공, 관리하는 것을 수동 의존성 주입이라고 한다.

위처럼 Car에 종속 항목이 하나만 있었기에 수동으로 의존성을 주입하는 것이 가능했지만 종속 항목과 클래스가 많아지면 수동 의존성 주입을 사용할 때 문제가 발생할 수 있다.

그로 인해 종속 항목을 생성하고 제공하는 프로세스를 자동화해서 문제를 해결하는 라이브러리를 사용하는 것이 좋다.

안드로이드에도 이러한 라이브러리가 있다.

Hilt

Dagger

Dagger는 구글에서 유지 관리하며 자바, 코틀린 및 안드로이드용으로 널리 사용되는 라이브러리이다. 컴파일 타임 정확성, 런타임 성능, 확장성 및 안드로이드 스튜디오 지원 등의 장점이 있다.

Hilt는 Dagger를 기반으로 빌드된 Jetpack의 권장 라이브러리로, 프로젝트의 모든 안드로이드 클래스에 컨테이너를 제공하고 생명주기를 자동으로 관리해 앱에서 DI를 실행하는 표준 방법을 정의한다.

위 라이브러리에 대해서는 나중에 따로 다뤄볼 예정이다.



마무리


사이드 프로젝트를 다 마무리한 시점에서 Dagger Hilt라는 라이브러리를 알게 되었는데 매우 아쉬웠다.

‘조금만 더 빨리 알았더라면 사이드 프로젝트에도 적용시켜서 더 나은 아키텍쳐를 기반으로 개발할 수 있었을텐데’ 하고 말이다.

그래도 이제 앱 개발이 다 끝나서 플레이스토어에 배포 검토 중인만큼 남은 시간 동안에는 앱의 코드를 더욱더 개선시키거나 앱의 기능적인 부분들을 개선할 예정이다.

매일 퇴근 후 남은 시간에 앱 개발을 꾸준히 한 결과로 정확히 1달만에 원했던 구성의 앱 개발을 완료할 수 있었다.

나름대로 뿌듯하기도 하고 앞으로 더 성장해서 좋은 앱을 계속해서 개발하고 싶다.

Categories:

Updated: