Android의 종속 항목 삽입

종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다.

종속 항목 삽입을 구현하면 다음과 같은 이점을 누릴 수 있습니다.

  • 코드 재사용 가능
  • 리팩터링 편의성
  • 테스트 편의성

종속 항목 삽입의 기본사항

Android의 종속 항목 삽입을 구체적으로 다루기 전에 이 페이지에서는 종속 항목 삽입이 작동하는 방식에 관한 일반적인 개요를 제공합니다.

종속 항목 삽입이란?

클래스에는 흔히 다른 클래스 참조가 필요합니다. 예를 들어 Car 클래스는 Engine 클래스 참조가 필요할 수 있습니다. 이처럼 필요한 클래스를 종속 항목이라고 하며, 이 예에서 Car 클래스가 실행되기 위해서는 Engine 클래스의 인스턴스가 있어야 합니다.

클래스가 필요한 객체를 얻는 세 가지 방법은 다음과 같습니다.

  1. 클래스가 필요한 종속 항목을 구성합니다. 위의 예에서 Car는 자체 Engine 인스턴스를 생성하여 초기화합니다.
  2. 다른 곳에서 객체를 가져옵니다. Context getter 및 getSystemService()와 같은 일부 Android API는 이러한 방식으로 작동합니다.
  3. 객체를 매개변수로 제공받습니다. 앱은 클래스가 구성될 때 이러한 종속 항목을 제공하거나 각 종속 항목이 필요한 함수에 전달할 수 있습니다. 위의 예에서 Car 생성자는 Engine을 매개변수로 받습니다.

세 번째 옵션이 종속 항목 삽입입니다! 이 접근 방법을 사용하면 클래스 인스턴스가 자체적으로 종속 항목을 얻는 대신 클래스의 종속 항목을 받아서 제공합니다.

다음 예를 참고하세요. 종속 항목 삽입 없이 코드에서 자체 Engine 종속 항목을 생성하는 Car를 나타내는 것은 다음과 같습니다.

Kotlin

class Car {

    private val engine = Engine()

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

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

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
종속 항목 삽입이 없는 Car 클래스

이 예는 Car 클래스가 자체 Engine을 구성하기 때문에 종속 항목 삽입의 예가 아닙니다. 이는 다음과 같은 이유로 문제가 될 수 있습니다.

  • CarEngine은 밀접하게 연결되어 있습니다. Car 인스턴스는 한 가지 유형의 Engine을 사용하므로 서브클래스 또는 대체 구현을 쉽게 사용할 수 없습니다. Car가 자체 Engine을 구성했다면 GasElectric 유형의 엔진에 동일한 Car를 재사용하는 대신 두 가지 유형의 Car를 생성해야 합니다.

  • Engine의 종속성이 높은 경우 테스트하기가 더욱 어려워집니다. Car는 실제 Engine 인스턴스를 사용하므로 다양한 테스트 사례에서 테스트 더블을 사용하여 Engine을 수정할 수 없습니다.

종속 항목 삽입 시 코드는 어떤 모습일까요? 다음과 같이 Car의 각 인스턴스는 초기화 시 자체 Engine 객체를 구성하는 대신 Engine 객체를 생성자의 매개변수로 받습니다.

Kotlin

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

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

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
종속 항목 삽입을 사용하는 Car 클래스

main 함수에서 Car를 사용합니다. CarEngine에 종속되므로 앱은 Engine 인스턴스를 생성한 후 이를 사용하여 Car 인스턴스를 구성합니다. 이 DI 기반 접근 방법의 이점은 다음과 같습니다.

  • Car의 재사용 가능. Engine의 다양한 구현을 Car에 전달할 수 있습니다. 예를 들어 Car에서 사용할 ElectricEngine이라는 새로운 Engine 서브클래스를 정의할 수 있습니다. DI를 사용한다면 업데이트된 ElectricEngine 서브클래스의 인스턴스를 전달하기만 하면 되며 Car는 추가 변경 없이도 계속 작동합니다.

  • Car의 테스트 편의성. 테스트 더블을 전달하여 다양한 시나리오를 테스트할 수 있습니다. 예를 들어 FakeEngine이라는 Engine의 테스트 더블을 생성하여 다양한 테스트에 맞게 구성할 수 있습니다.

Android에서 종속 항목 삽입을 실행하는 두 가지 주요 방법은 다음과 같습니다.

  • 생성자 삽입. 위에서 설명한 방법입니다. 즉, 클래스의 종속 항목을 생성자에 전달합니다.

  • 필드 삽입(또는 setter 삽입). 활동 및 프래그먼트와 같은 특정 Android 프레임워크 클래스는 시스템에서 인스턴스화하므로 생성자 삽입이 불가능합니다. 필드 삽입을 사용하면 종속 항목은 클래스가 생성된 후 인스턴스화됩니다. 코드는 다음과 같습니다.

Kotlin

class Car {
    lateinit var engine: Engine

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

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

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

종속 항목 자동 삽입

이전 예에서는 라이브러리를 사용하지 않고 다양한 클래스의 종속 항목을 직접 생성, 제공 및 관리했습니다. 이를 종속 항목 직접 삽입 또는 종속 항목 수동 삽입이라고 합니다. Car 예에서는 종속 항목이 하나만 있었지만 종속 항목과 클래스가 많아지면 수동으로 종속 항목을 삽입하는 작업이 더 지루해질 수 있습니다. 또한 수동 종속 항목 삽입에는 다음과 같은 문제도 몇 가지 있습니다.

  • 대규모 앱의 경우 모든 종속 항목을 가져와 올바르게 연결하려면 대량의 상용구 코드가 필요할 수 있습니다. 다중 레이어 아키텍처에서는 최상위 레이어의 객체를 생성하려면 그 아래에 있는 레이어의 모든 종속 항목을 제공해야 합니다. 구체적인 예로, 실제 자동차를 만들려면 엔진, 변속기, 섀시 및 기타 부품이 필요할 겁니다. 그리고 엔진에는 실린더와 점화 플러그도 필요합니다.

  • 종속 항목을 전달하기 전에 구성할 수 없을 때(예를 들어 지연 초기화를 사용하거나 객체 범위를 앱의 흐름으로 지정할 때)는 메모리에서 종속 항목의 전체 기간을 관리하는 맞춤 컨테이너(또는 종속 항목 그래프)를 작성하고 유지해야 합니다.

종속 항목을 생성하고 제공하는 프로세스를 자동화하여 이 문제를 해결하는 라이브러리가 있습니다. 이는 두 가지 카테고리로 분류됩니다.

  • 런타임 시 종속 항목을 연결하는 리플렉션 기반 솔루션

  • 컴파일 시간에 종속 항목을 연결하는 코드를 생성하는 정적 솔루션

Dagger는 Google에서 유지 관리하며 자바, Kotlin 및 Android용으로 널리 사용되는 종속 항목 삽입 라이브러리입니다. Dagger는 종속 항목 그래프를 자동으로 생성하고 관리하여 앱에서의 DI 사용을 용이하게 합니다. 또한 Guice 같은 리플렉션 기반 솔루션의 여러 개발 및 성능 문제를 해결하는 완전 정적 및 컴파일 시간 종속 항목을 제공합니다.

종속 항목 삽입의 대안

종속 항목 삽입의 대안은 서비스 로케이터를 사용하는 것입니다. 서비스 로케이터 디자인 패턴도 구체적인 종속 항목으로부터 클래스가 잘 분리되도록 해줍니다. 종속 항목을 생성하고 저장한 후 필요에 따라 이러한 종속 항목을 제공하는 서비스 로케이터라는 클래스를 생성합니다.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

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

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

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

서비스 로케이터 패턴은 요소가 소비되는 방식에서 종속 항목 삽입과 다릅니다. 서비스 로케이터 패턴을 통해 클래스는 삽입할 객체를 제어하고 요청합니다. 종속 항목 삽입을 통해 앱은 필요한 객체를 제어하고 사전에 삽입합니다.

종속 항목 삽입과 비교 시:

  • 서비스 로케이터에 필요한 종속 항목 컬렉션은 코드를 테스트하기가 더 어렵습니다. 이는 모든 테스트가 동일한 전역 서비스 로케이터와 상호작용해야 하기 때문입니다.

  • 종속 항목은 API 노출 영역이 아닌 클래스 구현에서 인코딩됩니다. 따라서 클래스가 외부에서 필요한 것이 무엇인지 알기가 더 어렵습니다. 결과적으로 Car 또는 서비스 로케이터에서 사용 가능한 종속 항목을 변경하면 참조가 실패하여 런타임 오류 또는 테스트 실패가 발생할 수 있습니다.

  • 전체 앱의 전체 기간이 아닌 다른 기간으로 범위를 지정하려는 경우 객체의 전체 기간을 관리하기가 더 어렵습니다.

Android 앱에서 Hilt 사용

Hilt는 Android에서 종속 항목을 삽입하기 위한 Jetpack의 권장 라이브러리입니다. Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 실행하는 표준 방법을 정의합니다.

Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리인 Dagger를 기반으로 빌드되었습니다.

Hilt에 관한 자세한 내용은 Hilt를 사용한 종속 항목 삽입을 참고하세요.

결론

종속 항목 삽입은 앱에 다음과 같은 이점을 제공합니다.

  • 클래스 재사용 가능 및 종속 항목 분리: 종속 항목 구현을 쉽게 교체할 수 있습니다. 컨트롤 반전으로 인해 코드 재사용이 개선되었으며 클래스가 더 이상 종속 항목 생성 방법을 제어하지 않지만 대신 모든 구성에서 작동합니다.

  • 리팩터링 편의성: 종속 항목은 API 노출 영역의 검증 가능한 요소가 되므로 구현 세부정보로 숨겨지지 않고 객체 생성 시간 또는 컴파일 시간에 확인할 수 있습니다.

  • 테스트 편의성: 클래스는 종속 항목을 관리하지 않으므로 테스트 시 다양한 구현을 전달하여 다양한 모든 사례를 테스트할 수 있습니다.

종속 항목 삽입의 이점을 완전히 이해하려면 종속 항목 수동 삽입에 나와 있는 것처럼 앱에서 수동으로 시도해야 합니다.

추가 리소스

종속 항목 삽입에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.

샘플