Dagger 기본사항

Android 앱에서 수동 종속 항목 삽입 또는 서비스 로케이터는 프로젝트의 크기에 따라 문제가 될 수 있습니다. Dagger를 사용하여 종속 항목을 관리함으로써 프로젝트가 확장될 때 프로젝트의 복잡성을 제한할 수 있습니다.

Dagger는 개발자가 직접 작성했을 코드를 모방하는 코드를 자동으로 생성합니다. 코드가 컴파일 시간에 생성되므로 Guice 같은 다른 리플렉션 기반 솔루션보다 성능이 더 뛰어나며 추적 가능합니다.

Dagger 사용의 이점

Dagger를 사용하면 다음이 가능하므로 지루하고 오류가 발생하기 쉬운 상용구 코드를 작성하지 않아도 됩니다.

  • 수동 DI 섹션에서 수동으로 구현한 AppContainer 코드(애플리케이션 그래프)를 생성합니다.

  • 애플리케이션 그래프에서 사용할 수 있는 클래스의 팩토리를 만듭니다. 이는 종속 항목이 내부적으로 충족되는 방식입니다.

  • 범위 사용을 통해 종속 항목을 재사용할지 아니면 새 인스턴스를 만들지 결정합니다.

  • Dagger 부분 구성요소를 사용하여 이전 섹션의 로그인 흐름에서와 같이 특정 흐름의 컨테이너를 만듭니다. 이렇게 하면 더 이상 필요하지 않은 객체를 메모리에서 해제함으로써 앱 성능이 향상됩니다.

개발자가 클래스의 종속 항목을 선언하고 주석을 사용하여 종속 항목을 충족하는 방법을 지정하기만 하면 Dagger는 빌드 시간에 자동으로 이러한 모든 일을 완료합니다. Dagger는 개발자가 수동으로 작성한 것과 유사한 코드를 생성합니다. 내부적으로 Dagger는 클래스의 인스턴스를 제공하는 방법을 찾기 위해 참조할 수 있는 객체의 그래프를 만듭니다. 그래프의 모든 클래스와 관련하여 Dagger는 필요한 유형의 인스턴스를 가져오는 데 내부적으로 사용하는 팩토리 유형 클래스를 생성합니다.

빌드 시간에 Dagger는 코드를 검토하고 다음을 실행합니다.

  • 종속 항목 그래프를 빌드하고 그 유효성을 검사하여 다음을 확인합니다.

    • 모든 객체의 종속 항목이 충족될 수 있으므로 런타임 예외가 없습니다.
    • 종속 항목 주기가 없으므로 무한 루프가 없습니다.
  • 런타임 시 실제 객체 및 종속 항목을 만드는 데 사용되는 클래스를 생성합니다.

Dagger의 간단한 사용 사례: 팩토리 생성

Dagger를 사용하여 작업하는 방법을 보여주기 위해 다음 다이어그램에 나와 있는 UserRepository 클래스의 간단한 팩토리를 만들어 보겠습니다.

다음과 같이 UserRepository를 정의합니다.

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

다음과 같이 Dagger가 UserRepository를 생성하는 방법을 알 수 있도록 @Inject 주석을 UserRepository 생성자에 추가합니다.

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

위의 코드 스니펫에서는 Dagger에 다음을 알려줍니다.

  1. @Inject 주석이 달린 생성자를 사용하여 UserRepository 인스턴스를 만드는 방법을 알려줍니다.

  2. 종속 항목은 UserLocalDataSourceUserRemoteDataSource임을 알려줍니다.

이제 Dagger는 UserRepository의 인스턴스를 만드는 방법을 알고 있지만 종속 항목을 만드는 방법은 모릅니다. 다음과 같이 다른 클래스에도 주석을 지정하면 Dagger는 그 클래스를 만드는 방법을 알게 됩니다.

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() { }
}

public class UserRemoteDataSource {
    @Inject
    public UserRemoteDataSource() { }
}

Dagger 구성요소

Dagger는 종속 항목이 필요할 때 이러한 종속 항목을 가져올 위치를 찾는 데 사용할 수 있는 프로젝트의 종속 항목 그래프를 만들 수 있습니다. Dagger가 이렇게 하도록 하려면 인터페이스를 만들고 @Component로 주석을 지정해야 합니다. Dagger는 수동 종속 항목 삽입 시와 마찬가지로 컨테이너를 만듭니다.

@Component 인터페이스 내에서 필요한 클래스의 인스턴스(즉, UserRepository)를 반환하는 함수를 정의할 수 있습니다. @Component는 노출하는 유형을 충족하는 데 필요한 모든 종속 항목이 있는 컨테이너를 생성하도록 Dagger에 지시합니다. 이를 Dagger 구성요소라고 합니다. 이 구성요소에는 Dagger가 제공하는 방법을 알고 있는 객체와 객체의 각 종속 항목으로 구성된 그래프가 포함되어 있습니다.

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

프로젝트 빌드 시 Dagger는 자동으로 ApplicationGraph 인터페이스의 구현, 즉 DaggerApplicationGraph를 생성합니다. Dagger는 주석 프로세서를 사용하여 하나의 진입점(UserRepository 인스턴스 가져오기)으로 세 클래스(UserRepository, UserLocalDatasource, UserRemoteDataSource) 간의 관계로 구성된 종속 항목 그래프를 만듭니다. 다음과 같이 사용할 수 있습니다.

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

Dagger는 요청될 때마다 UserRepository의 새 인스턴스를 만듭니다.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

때로 컨테이너에 종속 항목의 고유한 인스턴스가 있어야 합니다. 이는 다음과 같이 여러 가지 이유 때문일 수 있습니다.

  1. 동일한 LoginUserData를 사용하는 로그인 흐름의 여러 ViewModel 객체와 같이 동일한 인스턴스를 공유하기 위해 이 유형을 종속 항목으로 보유하는 다른 유형을 원합니다.

  2. 객체는 생성하는 데 비용이 많이 듭니다. 따라서 인스턴스를 종속 항목으로 선언할 때마다 새 인스턴스를 생성하지는 않으려고 합니다(예: JSON 파서).

이 예에서는 UserRepository를 요청할 때마다 항상 동일한 인스턴스를 얻도록 그래프에서 사용 가능한 고유한 UserRepository 인스턴스를 가지려고 할 수 있습니다. 이는 더 복잡한 애플리케이션 그래프가 있는 실제 애플리케이션에서 UserRepository에 따라 여러 ViewModel 객체를 가질 수 있고 UserRepository를 제공해야 할 때마다 UserLocalDataSourceUserRemoteDataSource의 새 인스턴스를 생성하지는 않으려고 하므로 이 예에서 유용합니다.

수동 종속 항목 삽입에서는 동일한 UserRepository 인스턴스를 ViewModel 클래스의 생성자에 전달함으로써 이 작업을 완료합니다. 그러나 Dagger에서는 코드를 수동으로 작성하지 않으므로 동일한 인스턴스를 사용하려는 것을 Dagger에 알려야 합니다. 이를 위해서는 범위 주석을 사용하면 됩니다.

Dagger로 범위 지정

범위 주석을 사용하여 객체의 전체 기간을 구성요소의 전체 기간으로 제한할 수 있습니다. 즉, 유형을 제공해야 할 때마다 종속 항목의 동일한 인스턴스를 사용합니다.

ApplicationGraph의 저장소를 요청할 때 UserRepository의 고유한 인스턴스를 가지려면 @Component 인터페이스 및 UserRepository에 동일한 범위 주석을 사용합니다. 다음과 같이 Dagger에서 사용하는 javax.inject 패키지와 함께 이미 제공된 @Singleton 주석을 사용할 수 있습니다.

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

또는 맞춤 범위 주석을 만들어 사용할 수 있습니다. 다음과 같이 범위 주석을 만들 수 있습니다.

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

그런 다음, 앞서와 같이 사용할 수 있습니다.

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

두 경우 모두 @Component 인터페이스에 주석을 지정하는 데 사용된 것과 동일한 범위가 객체에 제공됩니다. 따라서 applicationGraph.repository()를 호출할 때마다 동일한 UserRepository 인스턴스를 얻습니다.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

결론

더 복잡한 시나리오에서 Dagger를 사용하기 전에 Dagger의 이점과 작동 방식의 기본사항을 알고 있어야 합니다.

다음 페이지에서는 Android 애플리케이션에 Dagger를 추가하는 방법을 알아봅니다.