[Android] Dependency injection(DI)에 대해 알아보자

유민국·2023년 12월 1일

DI 학습하기

목록 보기
1/1

Android의 DI

DI는 프로그래밍에서 널리 사용되는 기법으로, Android 개발에 적합하다.
DI의 원칙을 따르면 앱 아키텍처를 위한 토대를 마련할 수 있다.
다음과 같은 이점이 있다

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

📖 DI란?

생성자 또는 메서드 등을 통해 외부로부터 생성된 객체를 전달받는 것

  • Dagger, Koin, Hilt

📌 특징

  • 클래스간 결합도를 느슨하게 함
  • 인터페이스 기반으로 설계되며, 코드를 유연하게 한다.
  • Stub 또는 Mock 객체를 사용하여 단위테스트를 하기가 더욱 쉬워진다.

📌 왜 사용할까?
1. 유연성과 재사용성

  • 객체 간 결합을 느슨하게 하여 코드의 유연성과 재사용성을 향상시킨다.
  • 코드를 보다 유연하게 만들어 변경이나 확장이 용이해진다.

2. 테스트 용이성

  • 클래스는 종속 항목을 관리하지 않으므로 테스트 시 다양한 구현을 전달하여 다양한 모든 사례를 테스트할 수 있다.
  • 모의(Mock) 객체를 사용하여 테스트하기 용이해진다.

3. 유지보수성

  • 코드가 의존성을 명시적으로 나타내기 때문에 어떤 객체가 어떤 의존성을 가지고 있는지 쉽게 파악할 수 있다.
  • 변경이 필요한 경우 특정 부분만 수정하면 되므로 코드의 일부를 변경해도 전체 시스템에 미치는 영향을 최소화할 수 있다.

4. 코드의 가독성과 이해도

  • 의존성이 명시적으로 주입되기 때문에 코드를 읽는 사람들이 해당 객체의 의존성을 쉽게 이해할 수 있다.

5. 의존성 교체 및 관리 용이성

  • DI를 통해 의존성을 주입하면 런타임에 의존성을 교체하는 것이 용이하다
  • 이는 모듈이나 라이브러리를 업그레이드하거나 변경할 때 특히 유용

6. 객체 생성과 생명 주기의 관리

  • DI 프레임워크는 객체의 생성과 생명 주기를 관리하는데 도움을 준다
  • 객체의 생성 시점, 소멸 시점 등을 더 효과적으로 제어할 수 있다.

7. 가독성 및 추상화 증대

  • DI를 사용하면 코드의 가독성이 증가하며, 의존성이나 객체 간의 관계가 추상화되어 코드의 복잡성이 감소한다.

다음은 종속성 주입이 없는 코드 예시이다.

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

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

위 코드는 CarEngine이 강하게 결합되어 있다. Car 객체는 특정 유형의 Engine을 사용하기 때문에 만약, GasEletric 유형의 Engine이 있다고 하면, Car를 재사용할 수 없게된다.

이러한 이유로 다음 두 가지 주요 방법을 사용하여 DI를 실행할 수 있다.

1️⃣ 생성자 삽입

의존성을 클래스의 생성자에 전달하는 것을 의미한다.

📌 장점

  • 의존성이 불변(Immutable): 한 번 생성된 인스턴스는 변경되지 않으므로 의존성이 불변
  • 의존성이 확실하게 주입됨: 객체 생성 시점에 모든 의존성이 주입되므로, 객체가 사용될 때 필요한 모든 의존성이 확실하다
  • 테스트 용이성: 의존성을 주입할 때, 테스트 중에 쉽게 대체할 수 있다
class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

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

2️⃣ 필드 삽입(or Setter 삽입)

필드 삽입은 의존성을 클래스의 필드에 직접 주입하는 방법이다

  • 주로 DI 프레임워크에서 사용
  • 클래스의 생성자나 메서드를 통하지 않고 필드에 직접 주입

📌 Activity 및 Fragment와 같은 특정 클래스는 시스템에서 인스턴화하므로 생성자 삽입이 불가능하다. 이런 경우 필드삽입을 사용하면 종속 항목은 클래스가 생성된 후 인스턴스화 된다.

📌 장점

  • 간결성: 코드가 간결해지고 생성자의 길이가 줄어든다.
  • DI 프레임워크 사용 용이: 일부 DI 프레임워크에서 자동으로 주입할 수 있어 설정이 간편

📌 단점

  • 의존성 추적이 어려움: 필드 삽입을 사용하면 코드에서 어떤 의존성이 주입되었는지 명시적으로 확인하기 어렵다
class Car {
    lateinit var engine: Engine

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

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

📌 코드를 더 명확하게 만들고 테스트 용이성을 높이기 때문에 대부분의 경우 생성자 삽입이 권장하며 필드 삽입은 주로 DI 프레임워크를 사용할 때나 특별한 경우에 사용한다.

Service Locator

서비스 로케이터(Service Locator)는 객체의 위치를 찾아주는 디자인 패턴 중 하나이다. - 객체 간의 의존성을 관리하고 해결하는 데 사용

  • 특히 DI 패턴이나 프레임워크가 없는 환경에서 의존성을 주입하는 데 활용

📌 장점

  • 런타임에 의존성 교체 가능: 의존성을 런타임에 교체할 수 있어 유연성을 제공
  • 서비스 객체의 레지스트리: 서비스 객체들의 레지스트리를 제공하여 중앙 집중적으로 관리할 수 있다.

📌 단점

  • 숨겨진 의존성: 서비스 로케이터를 사용하면 코드에서 의존성이 명시적으로 드러나지 않아서 유지보수와 이해가 어려울 수 있다
  • 테스트 어려움: 의존성을 명시적으로 주입하는 것이 테스트 용이성을 높인다.

📌 대부분의 경우, DI 패턴이나 DI 프레임워크를 사용하는 것이 더 권장된다.

profile
안녕하세요 😊

0개의 댓글