우테코 8기/Android

Dagger 기본 개념과 @Inject, @Component

Reddish 2026. 8. 1. 17:51
반응형

Dagger를 사용해 의존성을 관리하면 프로젝트가 커져도 복잡도를 낮출 수 있다.

Dagger는 개발자가 직접 작성해야 하는 코드를 자동으로 생성해준다. 코드가 컴파일 시점에 생성되므로 추적이 가능하고, Guice(Java DI 도구) 같은 리플렉션 기반 보다 성능이 뛰어나다


Dagger의 장점

Dagger를 통해 보일러플레이트 코드를 줄일 수 있다.

  • 수동 의존성 주입 섹션에서 직접 구현한 AppContainer 코드(어플리케이션 그래프)를 자동으로 생성해준다.
  • 어플리케이션 그래프에서 사용할 수 있는 클래스의 팩토리를 생성한다. 이를 통해 내부적으로 의존성이 충족된다
  • 범위(scope)를 사용해 의존성을 재사용할지 새로운 인스턴스를 생성할지 결정한다
  • Dagger 서브컴포넌트를 사용해 특정 화면 흐름에 맞는 컨테이너를 만든다. 이를 통해 더 이상 필요없는 객체를 메모리에서 해제해 앱 성능을 높일 수 있다.

 

이는 수동으로 DI를 구현했을 때 더 극명한 차이를 느낄 수 있다. 

https://developer.android.com/training/dependency-injection/manual?hl=ko

 

종속 항목 수동 삽입  |  App architecture  |  Android Developers

Android에서 종속 항목 삽입을 수동으로 실행하는 방법을 설명하며, 기본 직접 인스턴스화부터 컨테이너 및 애플리케이션별 흐름으로 종속 항목을 관리하는 방법까지 반복적인 접근 방식을 보여

developer.android.com

직접 DI를 넣어 구현해보면 위와 같은 구조가 나오게 된다. 

MyApplication이 싱글턴 형태로 존재해(싱글턴이란 것은 아니다.) AppContainer를 호출하고, 해당 AppContainer에서 필요한 Repository들을 만들어 Activity에 만들어진 Repository 들을 주입해준다. 그러면 아래와 같이 AppContainer에 많은 코드가 작성된다. 아래는 장바구니 기능"만" 들어간 앱의 AppContainer 코드이다. 장바구니 기능에서 더 확장되면 코드는 더 장황해질 수 밖에 없다.

class AppContainer(
    context: Context,
) {
    private val database: Database = Room
            .databaseBuilder().build() // RoomDB 생성 코드
    private val networkClient = RetrofitClient
    private val encoder = BasicAuthEncoder

    val productRepository: ProductRepository =
        RetrofitProductRepository(networkClient.productService)
    val cartRepository: CartRepository =
        RetrofitCartRepository(
            encoder = encoder,
            service = networkClient.cartService,
        )
    val recentProductRepository: RecentProductRepository by lazy {
        RoomRecentProductRepository(
            recentProductDao = database.recentProductDao(),
            productRepository = productRepository,
        )
    }
    val orderRepository: OrderRepository =
        RetrofitOrderRepository(
            encoder = encoder,
            service = networkClient.orderService,
        )
    val couponRepository: CouponRepository =
        RetrofitCouponRepository(
            service = networkClient.couponService,
        )
    val notificationRepository: NotificationRepository =
        SharedPreferenceNotificationRepository(context)
    val paymentAlarmScheduler: PaymentAlarmScheduler =
        PaymentAlarmScheduler(context)
}

그리고 개발자가 각 Activity나 ViewModel에 필요한 Repository들을 넣어줘야 하고, 이 또한 비대해지면 어느 Repository가 어느 ViewModel에 들어가는지 확인하기 어려워진다.

그에 대한 대안으로 나온 것이 Dagger이다. Dagger를 통해서 아래와 같이 해결할 수 있다. 

객체들 간에 의존성만 정의해 주고 그래프의 범위, 진입점을 적어주면, MyApplication에서 create를 호출하면 Dagger가 AppComponent를 만들어서 의존성 관계를 알아서 찾고, 객체를 생성해 주입을 받는 객체에게 넘겨주게 된다. 

내부적으로는 클래스의 인스턴스를 제공할 참조 객체 그래프를 생성한다. 그래프에 있는 모든 클래스에 대해, 해당 타입의 인스턴스를 가져오는데 사용하는 팩토리 타입 클래스를 내부적으로 생성한다.

빌드 시점에 Dagger는 코드를 분석해 아래 작업을 수행한다

  • 의존성 그래프를 빌드하고 검증해 다음을 확인한다
    • 모든 객체의 의존성을 충족할 수 있어 런타임 예외가 발생하지 않는다
    • 의존성 순환이 없어 무한 루프에 빠지지 않는다
  • 실제 객체와 그 의존성을 생성하는데 런타임에 사용될 클래스를 생성한다

기본 개념

의존성 그래프(Dependency Graph)

의존성이 있는 관계를 화살표로 나타낸 것이 그래프이다.

조금 커지면 아래와 같다.

Dagger의 본질은 이 그래프를 분석해 생성 순서와 연결 코드를 컴파일 시 생성하는 것이다.

Binding - 이 타입을 어떻게 얻는지

Binding은 “Engine이 필요하면 이렇게 만들면 된다” 라는 규칙이다.

class Engine constructor() {
	fun start() = "Engine Start"
}

class Car constructor(private val engine: Engine) {
	fun drive() = "Car driving with ${engine.start()}"
}

위 코드에서의 생성 규칙은 아래와 같다.

  1. Engine이 필요하면 Engine()을 호출한다
  2. Car이 필요하면 Engine을 구해서 Car(engine)으로 생성한다.

Component

위와 같은 생성 규칙을 하나의 그래프로 묶고, 객체를 요청하거나 주입할 수 있도록 Dagger에게 알려주는 객체이다.

Component의 메서드는 그래프의 입구(Entry Point)가 된다.

  • fun car(): Car → Car를 만들어 달라는 요청
  • fun inject(activity: MainActivity) → 이미 존재하는 Activity에 주입해 달라는 요청

주입 대상(Inject Target)

  • 일반적으로 Dagger가 직접 만들 수 있는 클래스는 생성자 주입이 자연스럽다
  • Activity, Fragment , Service 와 같은 Android 컴포넌트는 Android 시스템이 생성해 Dagger가 생성자를 호출할 수 없다
    • 이 경우엔 이미 만들어진 객체 필드에 넣는 멤버 주입을 쓴다

Dagger 핵심 어노테이션

@Inject

필드, 생성자, 메서드에 붙여 Component로 부터 의존성 객체를 주입 요청하는 어노테이션이다.

  • 의존성 주입을 요청하면 연결된 Component가 모듈로부터 객체를 생성해 넘겨준다.
  • Component는 @Inject 어노테이션을 의존성 주입할 멤버변수와 생성자에 달아줘 DI 대상을 확인할 수 있다.
  • 객체 생성이 클래스에서 이뤄지는게 아니라 Component가 생성해줘 보일러플레이트를 작성할 필요가 없다.

의존성 정의하는 방법은 아래와 같다.

class Engine @Inject constructor() {
	fun start() = "Engine Start"
}

class Car @Inject constructor(private val engine: Engine) {
	fun drive() = "Car driving with ${engine.start()}"
}

Dagger는 DI의 생성자 주입 방식에 기반해 의존성 주입을 수행한다. 따라서 Dagger는 프로그래머가 생성자에 붙인 @Inject 어노테이션을 보고 Dependency 임을 판단하도록 설계되어있다.

Engine에는 얼핏보면 생성자에 아무것도 없어 Dagger에 아무 영향을 주지 않는 것처럼 보일수도 있다. 하지만, @Inject 어노테이션을 붙여 Dagger에게 아무런 의존성이 없다고 알려준다.

또한, @Inject가 없는 클래스는 Dagger가 그냥 만들 수 없다.

의존성 그래프에 나와 있는 모든 클래스에 @Inject 를 붙인다. Dagger의 “Dependency 암시적 전달 명단” 에는 Engine과 Car 클래스가 추가되게 된다.


@Component

해당 어노테이션이 붙은 인터페이스는 @Inject가 붙은 생성자나 @Provide가 붙은 메서드를 스캔하고, 의존성을 주입하는 역할을 수행한다.

@Component
interface CarComponent {
    // provision 메서드 유형
    // Car를 만들어서 반환해달라는 요청
    fun makeCar() : Car

    // Member-Injection 메서드 유형
    // 인자로 받은 MainActivity(이미 존재함) 에 필요한 값을 주입해줘
    fun inject(mainActivity : MainActivity)
}

@Component에 추상 함수를 추가하고 컴파일 하면 dagger에 의해 (Dagger+인터페이스 이름)으로 명명된 클래스가 생성된다. (여기선 DaggerCarComponent일 것이다) 해당 코드가 의존성을 요청받고 전달하는 Dagger의 주된 역할을 수행한다.

“개념적”으론 아래와 같은 코드가 자동 생성된다고 보면 된다.

class DaggerCarComponent: CarComponent {
	override fun car(): Car {
		return Car(Engine())
	}
}
  • 규칙
    • 인터페이스나 추상클래스에만 사용 가능하다
    • 해당 어노테이션이 달린 인터페이스나 추상클래스엔 반드시 1개 이상의 추상 메서드가 있어야 한다.
  • 해당 추상 메서드를 Component Method라고 부르고 아래 두 가지로 나뉜다.
    • Provision Method
      • 객체의 타입을 return 값으로 갖는다
      • 생성된 Component 클래스에서 Provision 메서드를 통해 객체를 얻을 수 있다.
    • Member-Injection Method
      • 의존성을 주입 시킬 객체를 메서드의 파라미터로 넘기는 방식.
      • Android가 MainActivity를 생성하면 그 인스턴스를 Dagger에게 넘기고, @Inject가 붙은 필드에 값을 채워준다.
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var car: Car

    override fun onCreate(savedInstanceState: Bundle?) {
        (application as MyApplication)
            .carComponent
            .inject(this)

        println(car.drive())
    }
}

“개념적”으론 아래와 같은 코드가 자동 생성된다고 보면 된다.

class DaggerCarComponent: CarComponent {
	override fun car(): Car {
		return Car(Engine())
	}
}

인터페이스에 정의했던 추상 함수들도 구현되는데, 추상 함수들이 지녔던 반환 타입(Car)의 객체를 만들기 위한 의존성 주입 코드가 구현 함수에 들어간다. 대략적으로 아래와 같다.

fun inject(mainActivity: MainActivity) {
		mainActivity.car = Car(Engine())
}

이는 Activity 가 Android 시스템에서 생성되는 객체이기 때문에, 생성자 주입 대신 이미 생성된 Activity의 멤버에 주입하는 것이다.

반응형