[Android] DI 파헤치기

ByWindow·2022년 5월 22일
0

Android

목록 보기
2/14
post-thumbnail

@HiltAndroidApp???


sunflower 클론코딩 중 MainApplication.kt 파일에서 @HiltAndroidApp Annotation을 발견했는데, 이것이 Hilt와 연관이 있다는 것을 알게 되었다.
이번 포스팅에서 이 Hilt라는 것이 무엇인지 알아보려고 했으나... 그 전에 DI부터 공부해야 Hilt를 이해할 수 있을 것 같아 먼저 DI에 대해 알아보려고 한다.

Dependency Injection


Dependency?

  • 의존성을 사전 그대로 해석하면, 다른 것에 의지하여 존재하는 성질을 의미한다.
  • android에서의 의존성
    • 한 클래스의 객체가 제대로 작동하기 위해 다른 클래스의 객체가 필요할 때 발생한다
    • 객체지향언어에서는 두 클래스 간의 관계라고도 말할 수 있는데, 안드로이드에서 클래스 간의 연결은 보통 변수로 연결되니까 의존성이란 어떤 클래스의 멤버변수라고 설명할 수 있다.

Dependency의 위험성

class Garden {
	var plant: Plant
    
    init {
    	plant = Plant()
    }
    
    fun storeGarden() {
    	System.out.println("Planting it in the gardne!")
    }
}

위의 코드를 보면, Garden 클래스는 init 블럭에서 의존성을 가지는 Plant를 인스턴스화한다. 즉, Garden은 이 function의 객체에 의존적이다.

그렇다면, 왜 이러한 의존성이 위험한걸까?

우리가 init 블럭에서 클래스의 dependencies들을 인스턴스화 할 때, 해당 클래스가 그것의 dependencies와 tightly coupled되어 있게 된다. 위의 상황에서는 GardenPlant와 tightly coupled된 상태이다. 그래서 이런 경우에 우리가 새로운 Garden 인스턴스를 만들기 위해서는 Plant객체 또한 새로 만들어야하는 것이 불가피하다. 하나의 모듈이 바뀌면 의존하는 다른 모듈도 수정해야하고, 유연하게 사용할 수 없기 때문에 재사용성이 떨어진다.
객체의 initializer block에서 dependencies를 인스턴스화 하는 것을 지양하는 또다른 이유 중 하나는 unit test때문이다. 의존성이 있으면 테스트하고자 하는 해당 클래스만 테스트하는 것이 어렵다. unit test는 다른 모듈로부터 독립적으로 이루어지는 것이 목적인데 의존성이 있다면 이를 지킬 수 없기 때문이다.

의존성 주입하기?

위의 위험성을 통해, 클래스 안에서 새롭게 만드는 것보다 외부 소스로부터 의존성을 건네 주는 것이 중요하게 되었고, 이것이 Dependency Injection이다.
Android는 context의 영향을 많이 받는다. 즉, Activity, Fragment 내에서 선언되고 사용되는 Instance들은 Activity, Fragment의 영향을 받는다. 인스턴스 생성 시 내부 환경의 영향을 받는다면, 같은 인스턴스라도 다른 환경에서 다르게 동작할 수 있다. 하지만 항상 같은 환경에서만 Instance를 생성하고, Activity나 Fragment에서는 생성된 Instance를 받아서 사용만 한다면 내부 환경과 상관없이 같은 동작을 하며, 범용적으로 재사용 할 수 있다.
이런 개념을 Dependency Injection이라고 한다.

DI의 이점

  • 위에서 소개한 dependency의 위험성을 없애기 위해 사용한다.
  • 위의 사진과 같이 Component는 Activity A와 B에게 필요한 Instance를 선언합니다. 그리고 각 Activity가 요청한 Instance를 Injection합니다. 따라서 Instance들은 Activity 의존성이 느슨해지고 어떤 Activity가 되더라도 같은 instance를 주입 받는다
  • 코드의 가독성을 높여준다
  • Unit test가 쉬워진다
  • 코드의 재활용성을 높인다
  • 객체 간의 의존성을 직접 설정하여 줄일 수 있다
  • 객체 간의 결합도를 낮추면서 유연하게 만들 수 있다

DI 방법

manual dependency injection

  • Constructor Injection
    클래스의 종속 항목을 생성자에 전달하는 방식이다. 간단한 예를 들어보자.

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

    위의 예시에서 Engine이라는 객체는 Car 클래스 안에서 초기화된다.
    이것이 DI를 적용하지 않은 대표적인 예시이다. 이렇게 코드를 작성했다면 CarEngine은 강하게 커플링 되어 있고, 만약 Car 클래스를 사용하는 무수히 많은 클래스 중에 어떤 것에서 기본 Engine 유형이 아니라 다른 Engine을 사용하고자 한다면, 그것들을 일일이 수정해줘야 하는 비효율적인 과정이 필요해진다.
    이를 해결하는 방법 중 하나는 Car의 각 인스턴스는 초기화 시 자체 Engine 객체를 구성하는 대신 Engine 객체를 생성자의 매개변수로 받도록 하는 것이다.

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

    위의 코드에서는 main 함수에서 Car을 사용하고 CarEngine에 대해 dependency를 가지므로, 앱은 Engine 인스턴스를 생성한 후 이를 사용하여 Car 인스턴스를 구성한다. 이렇게 코드를 구성하면, Engine의 유형이 달라져도 Car 자체를 수정할 필요없이 재사용하여 다양한 Engine에 대해 Car를 재사용 할 수 있다. 예를 들면, ElectricEngine이라는 Engine 서브클래스를 정의하여 이 서브클래스의 인스턴스를 전달만 하면 되는 것이다.

  • Field Injection
    Activity, Fragment와 같은 특정 Android 프레임워크 클래스는 시스템에서 인스턴스화하므로 생성자 삽입이 불가능하다. Field Injection을 사용하면 dependencies는 클래스가 생성된 후 인스턴스화된다.

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

    이렇게 Engine의 서브클래스를 Car 클래스의 parameter로 넘져주지 않고, 밖에서 해당 멤버변수에 직접 값을 넣는 형태이다.

automated dependency injection

실제 서비스에는 위의 예시보다 더 많은 클래스와 의존성들이 존재하는데, 기존의 manual DI 방식은 좀 더 복잡하고 까다로워지고 몇 가지 문제점을 야기할 수 있다.

  • 커다란 규모의 앱에서, 모든 종속성을 가져와 올바르게 연결하기 위해 수많은 boilerplate code 가 필요할 수 있다. 특히, 다중 계층 아키텍처에서는 최상위 계층에 대한 객체를 만들기 위해 그 아래 계층의 모든 종속성을 제공해야 한다. 실제 Car에는 Engine뿐만 아니라 다양한 부품이 존재하고, Engine을 만들 때도 그 유형에 따라 생산법이 달라질 것이다. 이를 코드로 구현한다면, 매번 필요한 객체를 초기화하여 넣어주는 과정이 불필요하게 길어질 수 있다.
  • 의존성을 전달하기 전에는 의존성을 구성할 수 없는 경우가 생길 수 있다. 예를 들어 lazy initializations 또는 앱의 흐름 내에서 객체를 scoping 하는 경우를 들 수 있다. 이러한 경우, 메모리에서 의존성의 lifetime 을 관리할 수 있게 하는 커스텀 컨테이너를 작성하여 유지해야 한다.

이러한 문제를 해결하기 위해 DI 프레임워크나 라이브러리가 존재하고, 대표적인 것이 Dagger2, Koin, Hilt이다. 이에 대해서는 추후에 포스팅으로 자세히 다루겠다.

IOC


  • Inversion of Control, 제어의 역전, 역제어라고도 한다
  • 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인패턴
  • 코드의 흐름을 제어하는 주체가 바뀌는 것!
  • DI가 의존성이 필요한 클래스나 객체를 외부에서 생성해 주입해 주는 것이므로, IOC를 통해 이루어진다라고 할 수 있다.
profile
step by step...my devlog

0개의 댓글