선생님, 개발을 잘하고 싶어요.

[의존성 주입] Dagger2 알아보기 (의존성 주입 ~ Component, Injector) 본문

개발/소프트웨어 개발

[의존성 주입] Dagger2 알아보기 (의존성 주입 ~ Component, Injector)

알고싶은 승민 2020. 2. 22. 21:15

도입

  실제 프로젝트를 진행함에 있어서 의존성 주입(Dpendency Injection, 줄여서 DI) 프레임워크의 사용은 필수 불가결하다. 내가 사이드 프로젝트로 사용하던 Koin 도 매우 좋은 DI 프레임워크 이지만, 회사에서는 Dagger2를 사용해서 구현하고 있었다.

 

  Dagger는 어노테이션을 사용해서 컴파일 타임에 DI 코드를 작성해주기 때문에, Koin보다 진입장벽이 높았다. 어노테이션만 보고 코드 흐름을 이해하기가 어렵기 때문인데, 그래서 오늘은 가장 주요한 개념들 위주로 소개(라고 쓰고 정리)를 하려고 한다.

의존성 주입 (DI)

  고오오대의 내 블로그 글 중에 Koin을 애찬하는 글이 하나 있는데, 그림으로 좀 더 쉽게 이해해보자.

우리의 큰 그림

  의존성 주입이 없다면, Activity에서 원하는 객체(ArticService)를 사용하기 위해, ArticService를 만들어 내는데 필요한 것들을 모두 알고 하나하나 손수 ArticService를 만들어 주어야 한다.

 

여기서 문제점들이 발생하는데, 다음과 같다.

  • HttpClient는 모든 애플리케이션에서 하나면 충분하다. (재생성을 하고 싶지 않다.)
  • ArticService는 모든 애플리케이션에서 하나면 충분하다. (재생성을 하고 싶지 않다.)
  • ArticService는 ArticleActivity에서만 쓰이지 않고, 다양한 곳에서 사용된다. 

우리는 첫 번째, 두 번째 상황에서 싱글톤을 이용한 구현을 할 수 있고, 세 번째 상황에서 전역 변수를 떠올려 어디서든 재활용할 수 있게 만드는 것을 생각할 수 있을 것이다.

 

실제로 작은 규모의 프로젝트에서는 이러한 생성이 타당하겠으나, 규모가 커지면 커질수록 객체 생성의 요구 사항이 늘어나고, 비효율적인 코드 구조가 늘어나며 온갖 버그를 발생시킬 가능성이 있다.

 

구글 공식 문서에서도 작은 녀석은, DI Framework 말고 다른 선택지도 괜찮다고 주장한다.

그래서 우리는 확장 가능한 의존성 주입을 위해 DI 프레임워크를 사용하여 프로젝트 규모가 커졌을 때도 객체 생성으로 고통받는 일을 피하고 생산성을 늘리고 싶다.

 

최강 인싸 DI

그래서 우리는 필요한 객체의 생성에 필요한 모든 지식을 DI 프레임워크에게 전가시킨다. 실제로 ArticleActivity는 객체 생성을 DI 프레임워크에 대고 요청하고 있다.

 

이런 일을 하기 위해서 우리가 알아야 하는 것은

  • 프레임워크가 어떻게 객체를 생성하는 지를 아는가
  • 애플리케이션은 프레임워크가 어떤 객체를 제공하는지 어떻게 아는가
  • 애플리케이션에서 프레임워크에게 객체를 어떻게 요청하는가

이를 위해 우리는 Dagger에서

  • 어떻게 객체를 생성하는지 --> Provides, 생성자 inject, Module
  • 프레임워크가 어떤 객체를 제공하는지 --> Component
  • 객체를 어떻게 요청 --> Inject (Member Injection)

를 알아보자.

Provides, 생성자 Inject

우리가 객체를 생성하는 방법은? 바로 생성자를 호출하는 것이다. DI 프레임워크라고 다르지 않다. 객체를 생성하려면 생성자를 호출해 주어야 한다.

 

하지만 실제 생성자를 호출하는 시점은 DI 프레임워크가 결정한다. 결국 우리는 프레임워크에게 그 생성자를 호출할 코드를 알려주어야 한다.

 

그러한 역할을 하는 것이 Provides 어노테이션과, 생성자 Inject 어노테이션이다. 하나하나 코드 예제를 보고 논의를 진행해 보자.

 

class NetworkModule {

    // Provides 어노테이션 사용, HttpClient 제공, Context 요청
    @Provides
    fun provideOkHttpClient(context: Context): HttpClient {
        return HttpClient.create(context)
    }
}

// 생성자 Inject 사용, ArticService 제공, OkHttpClient 요청
class ArticService @Inject constructor(private val client: OkHttpClient)

HttpClient의 생성자를 호출하고 반환하는 함수를 @Provides라는 어노테이션을 달아 두었고,

우리가 만든 ArticService의 생성자에는 @Inject 어노테이션을 달아 두었다.

 

provideOkHttpClient를 통해 우리는 Dagger에게

  • HttpClient의 객체를 제공한다.
  • Context의 객체를 요청한다.

ArticService 생성자를 통해 우리는 Dagger에게

  • ArticService의 객체를 제공한다.
  • OkHttpClient의 객체를 요청한다.

결국 실제로 객체를 제공하는 책임은, Provides, 생성자 Inject를 통해 이루어 짐을 기억해주면 된다.


 

둘 다 객체를 제공한다면 Provides와 생성자 Inject는 어느 경우에 사용할까?

 

생성자 Inject는 우리가 생성자를 직접 커스텀할 수 있는 객체일 때 용이하다.

  • 우리가 직접 만든 클래스

Provides는 우리가 생성자를 직접 호출하지 못하고, 서드 파티 라이브러리가 만들어 주는 객체에 용이하다.

  • 생성 함수를 제공하는 라이브러리들
  • Activity처럼 별도의 생성 로직을 타는 녀석들

생성자 Inject를 할 수 있으면 생성자 Inject를 해주고, 할 수 없다면, Provides를 해주자.

 

Module

Provides는 하나의 메서드에 대해서 작동한다. 이러한 메서드들을 묶어서 Module이란 것으로 관리할 수 있다. 안 그런다면, 도대체 어떻게 Provides 메서드를 호출할 것인가?

 

실제 provideOkHttpClient를 실행하기 위해선, 해당 메소드를 가지고 있는 객체(Module)를 Dagger가 가지고 있어야 함은 자명하다.

 

이를 Dagger에게 알려주기 위해서 Module 어노테이션을 달아주자.

@Module
class NetworkModule {

    // Provides 어노테이션 사용, Context 의존성 요청
    @Provides
    fun provideOkHttpClient(context: Context): HttpClient {
        return HttpClient.create(context)
    }
}

// 생성자 Inject 사용, OkHttpClient 의존성 요청
class ArticService @Inject constructor(private val client: OkHttpClient)

 

휴, 이제 우리는 Dagger에게 다음 로직을 알려준 것이다.

  • ArticService를 요청하면 ArticService(OkHttpClient) 생성자를 호출하면 된다.
  • OkHttpClient가 필요하면 networkModule.provideOkHttpClient(Context) 함수를 호출하면 된다.

이제 몇 가지 의문이 더 남았다.

  • 프로그래머는 Dagger에 어떻게 접근하는가?
  • 클래스에서 필요한 객체를 어떻게 받아서 사용하는가?
  • Dagger는 Module 객체를 어떻게 만드는가? (Provide 함수를 호출하기 위한 객체는 어떻게 만드는가?)
  • OkHttpClient를 만들기 위해 필요한 context는 어디서 받아오는가?

Component

첫 의문부터 해결해보자. 

 

프로그래머는 Dagger에 어떻게 접근하는가? 바로 Component를 통해서 한다.

// 이 컴포넌트가 관리할 Module을 정의해서, 컴포넌트가 제공하는 객체들을 명시할 수 있다.
@Component(modules = [
    NetworkModule::class
])
interface AppComponent

 

Component는 프로그래머가 Dagger와 통신하는 연결다리이다. 지금 섹션에서는 단순하게

Module들을 관리하고, 연결시켜주는 다리 역할의 인터페이스라고 생각하고 넘어가 보자. (Inject와 Generated Code 연결하기 섹션에서 좀 더 자세히 알아보자. 첫 의문의 해결은 결국 맨 마지막으로...)

Inject

두 번째 의문, 클래스에서 필요한 객체를 어떻게 받아서 사용하는가?

 

이미 우리는 생성자 Inject에서 해답을 봤다. 생성자 Inject의 매개변수로 있는 객체는 Dagger에게 주입받는다.

하지만, 생성자를 커스텀할 수 없는 녀석이라면? (Like Activity, 액티비티는 운영체제에서 생성자를 호출한다.)

 

그럴 때는 MemberInjection을 사용할 수 있다. 구현하는 방법은 간단한데, 다음 코드를 보자.

class ArticleActivity() : Activity() {

    // member 위에 Inject 어노테이션으로 Dagger에게 ArticService 객체를 요청한다.
    @Inject
    lateinit var articService: ArticService
}

우리는 클래스에 필요한 객체를 Dagger가 주입해 주기 원한다면...

  • 생성자 Injection을 통해 주입받자. (물론 이 경우, 해당 객체를 Dagger에 의해 주입받아야 한다.)
  • MemberInjection을 통해 주입 받자.

이제 완벽한가?

아니다. 위 코드는 실제로 의존성을 주입받지 못한다. 지금 문제점은 Dagger가 ArticleActivity을 모른다는 점에 있다. ArticleActivity의 존재를 Dagger에게 알려줘야 한다. 그래야 Dagger가 객체를 넣어준다.

 

어떻게 Dagger와 프로그래머 코드를 연결할까? 다음 섹션에서 그 내용을 확인해보자. 바로 Generated Code와 연결하는 방법이다.

 

Generated Code와 연결하기

Dagger는 의존성 주입(객체 생성-제공)을 컴파일 타임에 제공하는 DI 프레임워크이다. 빌드 과정에서 Dagger가 의존성 주입을 위해 코드를 생성한다는 의미인데,

이 코드의 세부내용을 알아야 할 필요는 없지만 그 코드와 상호작용 하는 방법은 알아야 한다. 우리가 라이브러리 사용법 (인터페이스)를 알고 사용하는 것처럼, 다음 두 가지를 알아보자.

  • Component 객체 만들기
  • Dagger 의존성 트리에 객체를 태우기(?!)

우리는 이것을 하면 다음과 같은 풀리지 않던 의문을 해결할 수 있을 것이다.

  • Dagger는 Module 객체를 어떻게 만드는가? (Provide 함수를 호출하기 위한 객체는 어떻게 만드는가?)
  • OkHttpClient를 만들기 위해 필요한 context는 어디서 받아오는가?

NetworkModule은 기본 생성자를 제공한다. (매개변수가 없는 생성자) 따라서 Dagger는 해당 모듈을 자동으로 완성한다. 따라서 별도의 매개변수를 받는 Module을 하나 더 고안해보자. 바로 Context를 제공하는 ApplicationModule이다.

 

@Module
class ApplicationModule(private val context: Context) {
    
    @Provides
    fun provideContext(): Context {
        return context
    }
}

// ApplicationModule을 Component가 관리하도록 추가하자.
@Component(modules = [
    ApplicationModule::class,
    NetworkModule::class
])
interface AppComponent

이렇게 하고 빌드를 해보자.

누누이 말하다시피 Dagger는 빌드 시점에 코드를 생성하고, 실제로 프로그래머가 생성된 코드를 사용해서 Dagger와 연결해야 하기 때문에, 빌드하지 않는다면 아래의 코드는 빨간 줄이 그어 저 있을 것이다.

 

AppComponent는 빌드하면, AppComponent Interface의 구현체를 만들어주는 DaggerAppComponent라는 녀석이 생기고, 

class MyApplication : Application() {

    lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()
        // NetworkModule 은 기본 생성자를 제공하므로 굳이 명시적으로 적어주지 않아도 좋다.
        appComponent = DaggerAppComponent.builder()
            .applicationModule(ApplicationModule(this))
            .networkModule(NetworkModule())
            .build()
    }
}

 

실제로 Module 객체 생성은 프로그래머가 해서 Dagger에게 넘겨준다. 이러면 이제 프로그래머는 appComponent 객체를 사용해서 Dagger에게 의존성 주입을 요청할 수 있다.

 

그러면 실제로 Activity에서는 어떻게 사용하는가?

class ArticleActivity: Activity() {

    @Inject
    lateinit var articService: ArticService

    override fun onCreate(savedInstanceState: Bundle?) {
        // 이 객체를 의존성 트리에 태운다(!)
        (application as MyApplication).appComponent.inject(this)
        // 여기부터 articService 사용이 가능하다.

        super.onCreate(savedInstanceState)
    }
}

@Component(modules = [
    ApplicationModule::class,
    NetworkModule::class
])
interface AppComponent {
    // 실제로 주입을 요청할 객체를 받는다. (해당 activity의 의존성을 dagger가 주입시켜 준다.)
    fun inject(activity: ArticleActivity)
}

 

위의 의문에 대한 답은 다음과 같다.

  • 프로그래머는 Dagger에 어떻게 접근하는가 --> Component를 통해서 Dagger가 생성된 코드에 접근한다.
  • Dagger는 Module 객체를 어떻게 만드는가? (Provide 함수를 호출하기 위한 객체는 어떻게 만드는가?) --> 프로그래머가 직접 생성
  • OkHttpClient를 만들기 위해 필요한 context는 어디서 받아오는가? --> 다른 모듈에서 받아오기

마치며

DI 프레임워크가 한번 잘 구성되면, 사실 DI의 구조는 머릿속에서 지우고 작업을 할 수 있다. (실제로 기능을 개발할 때, 의존성 주입을 어떻게 하는가로 골머리 썩은 기억은 없다. @Inject 하면 모두 한방에 해결)

 

하지만 내가 새로운 의존성을 추가하고 싶은 경우에는, Dagger의 동작 방식의 이해 없이는 본인이 원하는 대로 주입받기 힘들다.

Provides, Inject, Component 이 세 가지 요소의 사용법과 상관관계, 그리고 의존성 트리에 대한 이해만 있다면 Dagger를 사용해 그 어떤 의존성 주입이라도 할 수 있을 것이다. 더 나아가는 개념들에 대해서도 기반이 되는 지식이므로 내가 참조한 링크를 보고 추가적인 공부를 하길 추천한다. (특히 아래의 Codelabs는 꼭 꼭 꼭 해보길 추천한다.)

 

추가적으로, Subcomponent, Scope 등 Dagger 관련 주제는 많이 있으니 다들 한번 찾아보면 좋을 것 같다. (내가 포스팅을 쓸 수 있을까...) 

 

참고 링크

왜 다른 Di가 아니라 Dagger2 인가? 구글 슬라이드 (https://docs.google.com/presentation/d/1fby5VeGU9CN8zjw4lAb2QPPsKRxx6mSwCe9q7ECNSJQ/pub?start=false&loop=false&delayms=3000&slide=id.p)

 

Dagger2 디자인 철학 구글 문서

(https://docs.google.com/document/d/1fwg-NsMKYtYxeEWe82rISIHjNrtdqonfiHgp8-PQ7m8/edit)

 

Dagger 공식 문서

(https://dagger.dev/)

 

Jake Wharton 형님의 Slide

(https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014)

 

Android Developer Dagger 공식 문서

(https://developer.android.com/training/dependency-injection/dagger-android?hl=ko)

 

초초 강추 Codelabs Dagger

(https://codelabs.developers.google.com/codelabs/android-dagger/#0)

Comments