#1 이전 글
본 게시글을 이해하려면 먼저, 의존성 주입에 대해 쓴 이전 게시글을 읽어야 한다. 또, 이전 게시글의 코드를 안드로이드 프로젝트로 Migration해서 본 게시글의 코드를 작성했다.
#2 dagger
프로젝트에서 Dependent - Dependency 관계가 점점 많아지면 언젠간 프로그래머가 감당하기 힘든 수준에 이를 것이다.
이 때 필요한 Dependency의 암시적 주입, 의존성 코드들의 직관화, 모듈화, 유지보수성 증대를 위한 라이브러리가 바로 dagger다. dagger1에서 2로 넘어갈 때 많은 변화가 있었기에 본 게시글의 제목에 Dagger2로 사용할 버전을 명시했다. 이제, dagger 사용의 기초에 대해 서술해보겠다.
#3 이전 게시글 안드로이드 프로젝트로 Migration
#3-1 개요
먼저, dagger를 적용할 코드가 필요하다. 이전 게시글 #2-1 부분의 코드를 안드로이드 프로젝트로 옮겼다. 그 과정에서 각 클래스를 하나하나 파일로 만들었고,
// package com.example.dagger2basics
import android.util.Log
class Car(
private val engineInstance: Engine,
private val airbagInstance: Airbag,
private val batteryInstance: Battery
) {
fun startCar() {
engineInstance.startEngine()
airbagInstance.startAirbag()
batteryInstance.startBattery()
Log.i("interfacer_han", "${this::class.simpleName} is ready")
}
}
위와 같이, println() 대신 Log.i()로 메시지를 출력하게 수정했다.
#3-2 MainActivity.kt
// package com.example.dagger2basics
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val crankshaft = Crankshaft()
val cylinder = Cylinder()
val piston = Piston(crankshaft, cylinder)
val engine = Engine(pistonInstance)
val airbag = Airbag()
val battery = Battery()
val car = Car(engine, airbag, battery)
car.startCar()
}
}
MainActivity에는, 원래 코드의 main()에서 정의했던 동작을 집어넣는다.
이제, 이 프로젝트가 dagger를 이용해 Dependency(의존성)을 주입(Inject)받도록 수정해본다.
#4 dagger 사용하기
#4-1 프로젝트 수준 build.gradle 수정
plugins {
...
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
}
ksp 플러그인 추가
#4-2 모듈 수준 build.gradle 수정
plugins {
...
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.kapt")
}
android {
...
}
dependencies {
...
// Dagger2
val daggerVersion = "2.51.1"
implementation("com.google.dagger:dagger:$daggerVersion")
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
}
ksp 및 kapt 플러그인 추가 및 Dagger2 라이브러리 다운로드
#4-3 @Inject의 이해
dagger는 어노테이션(@)을 기반으로 작동한다. 그 중 @Inject는 dagger의 기본적이고 핵심적인 역할을 하는 어노테이션이다. Inject(주입)의 사전적 정의 그대로, (생성자를) 주입한다는 의미의 어노테이션이다.
이전 게시글의 #2-1에 의하면, 어떤 클래스의 생성자에 정의된 또 다른 클래스는 Dependency다. 마침, dagger 또한 '생성자 주입' 방식에 기반하여 생성자 주입을 수행한다. 따라서 dagger는 프로그래머가 생성자에 붙인 @Inject를 보고 Dependency임을 판단하도록 설계되었다. 아래 코드를 보자.
class Car @Inject constructor(
private val engine: Engine,
private val airbag: Airbag,
private val battery: Battery
) {
fun startCar() {
engine.startEngine()
airbag.startAirbag()
battery.startBattery()
Log.i("interfacer_han", "${this::class.simpleName} is ready")
}
}
Car의 생성자에 @Inject가 붙었다. 이제, Engine, Airbag, Battery 클래스는 Dagger 라이브러리에 의해 암시적으로 객체가 생성되는 명단에 등록된다. 이 말은, #3-2의 코드에서 engine, airbag, battery의 객체 생성 및 car = Car(engine, airbag, battery)가 암시적으로 수행된다는 얘기다.
class Crankshaft @Inject constructor() { // 얼핏 보기엔, 이 @Inject는 아무런 의미가 없어보인다.
fun startCrankshaft() {
Log.i("interfacer_han", "${this::class.simpleName} is ready")
}
}
헷갈리지 말아야 하는 것은, @Inject가 붙는 대상은 Class가 아니라 생성자라는 사실이다. 위 코드에서 Crankshaft 클래스는 Dependent이지, Denpendency가 아니다. 하지만 그렇다고 생성자 인수를 요구하지 않는 클래스, 즉 의존성 그래프(이전 게시글의 #1-3 참조)에서 말단 부분을 담당하고 있는 클래스에 @Inject를 붙이지 않아야 하는 건 아니다. 왜냐하면, 비어있는 생성자에 @Inject를 붙임으로서 해당 클래스는 Dendency가 없다는 걸 (dagger 입장에서) 명시적으로 파악할 수 있기 때문이다.
#4-4 @Inject 붙이기
의존성 그래프에 나와 있는 모든 클래스에 @Inject를 붙인다. 이제, dagger의 'Denpendency 암시적 전달 명단'에는 Engine, Airbag, Battery, Piston, Crakshift, Cylinder의 6개 클래스가 추가된다.
#4-5 @Component
// package com.example.dagger2basics
import dagger.Component
@Component
interface CarComponent {
fun getCar(): Car
}
@Component 어노테이션이 붙은 인터페이스는 @Inject가 붙은 생성자나 @Provide가 붙은 메소드를 스캔하고, 의존성을 주입하는 역할을 수행한다. @Component에 추상 함수를 추가하고 컴파일을 하면, dagger에 의해 Dagger + 인터페이스 이름으로 명명된 클래스가 생성된다 (여기선 DaggerCarCompenent). 그리고 인터페이스에 정의했던 추상 함수들도 구현되는데, 추상 함수들이 지녔던 반환 타입(여기서는 Car)의 객체를 만들기 위한 의존성 주입 코드가 구현 함수에 들어간다.
#4-6 의존성 그래프
@Component 클래스는 위와 같은 의존성 그래프를 토대로 의존성 주입을 자동으로 수행할 것이다. @Inject 또는 @Provide로 위에서 말했던 명단을 구성해두면 dagger는, 더 정확히는 @Component 클래스는 위와 같은 의존성 그래프를 알아서 그려낸다.
#4-7 컴파일 하기
MainActivity를 수정하기 전 먼저 @Component 클래스를 만들기 위한 컴파일을 진행한다. [Build] - [Clean Project] 후 [ Build ] - [Rebuild Project]를 차례대로 클릭한다.
#4-8 MainActivity에서 수동적 의존성 주입 코드 제거
...
class MainActivity : AppCompatActivity() {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
setContentView(R.layout.activity_main)
/*
val crankshaft = Crankshaft()
val cylinder = Cylinder()
val piston = Piston(crankshaft, cylinder)
val engine = Engine(piston)
val airbag = Airbag()
val battery = Battery()
val car = Car(engine, airbag, battery)
*/
val car = DaggerCarComponent.create().getCar()
car.startCar()
}
}
기존에 썼던 수동으로 의존성을 주입하는 코드를 제거한다. Component 클래스를 생성하고 #4-5에서 만들어두었던 추상 함수의 구현 함수인 getCar()를 호출하면, Compoenent 클래스가 그려낸 의존성 그래프에 기반하여 의존성이 연쇄적으로 주입되고 최종적으로 Car 객체가 만들어진다.
#5 작동 확인 (로그 메시지)
Crankshaft is ready
Cylinder is ready
Piston is ready
Engine is ready
Airbag is ready
Battery is ready
Car is ready
#6 요약
생성자에 Dependency를 명시하는 순간부터, Dagger와 같이 편리한 자동화 라이브러리는 언젠간 나올 운명이었다.
#7 완성된 앱
#8 이어지는 글
#8-1 @Provides 사용하기
클래스의 생성자에 @Inject를 붙일 수 없는 경우의 의존성 주입을 구현해본다.
#8-2 인터페이스 구현체를 의존성 주입하기
Interface를 Dependency로 주입해본다.
#8-3 Activity에 의존성 주입하기
Activity에 Field Injection을 수행해본다.
#8-4 Component 인스턴스 싱글톤으로 관리하기
Application 클래스를 통해 Component 인스턴스를 싱글톤으로 관리한다.
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Dagger2 - 매개변수 동적 할당 (0) | 2024.06.24 |
---|---|
[Android] Dagger2 - @Provides (0) | 2024.06.24 |
[Android] WorkManager - 병렬 Chaining (0) | 2024.06.17 |
[Android] WorkManager - 작업 연쇄하기(Chaining) (0) | 2024.06.17 |
[Android] WorkManager - 제약 조건 (Constraints) (0) | 2024.06.17 |