ABOUT ME

Today
Yesterday
Total
  • 의존성주입 ( DI, Dependency Inject ) 에 대해 알아보기
    Android/Programming 2020. 8. 25. 21:17

    의존성에 대해 알아보기


    DI는 'Dependency Inject' 의 약자로 의존성 주입에 대한 약자이다.

    의존성 주입은 클라이언트의 의존성 생성과 클라이언트의 행동을 분리하여 클래스의 관계가 느슨하게 결합되고 의존성 반전 및 단일 책임 원칙을 따를 수 있다.

    이에 의존성 주입은 4가지의 역할이 있는데 다음과 같다

    • 서비스 : 데이터 제공
    • 클라이언트 : 서비스를 사용
    • 인터페이스 : 클라이언트가 서비스를 사용할 수 있도록 도움
    • 인젝터 : 클라이언트에 서비스를 주입하기 위한 책임

    간단하게 아래의 코드를 보도록 하자.

    class EnergyCar(name: String) {
        fun move() {}
    }
    
    class Driver() {
            val car = EnergyCar("가성비 좋은 자동차")
    
        fun drive() = car.move()
    }
    
    fun main() {
        Driver().drive()
    }

    Q. 간단하게 에너지 자동차를 운전하는 운전자다. 만약 에너지 자동차가 아니라 가스 자동차를 운전하려고 하면 어떻게 될까?

    A. 바로 운전자 객체의 에너지 자동차가 아닌 가스 자동차로 변경해주어야 한다.

     

    위의 코드에서는 큰 문제가 하나 있다.

    예를 들어서 테스트를 하기 위해서 에너지 자동차가 아닌 테스트 자동차를 넣으려고 한다면 무려 운전자 객체 자체를 변경해야 하는 상황이 된다.

    이는 테스트 코드를 작성하기 어려울 뿐더러 테스트 하자고 실 코드를 변경하는 셈이 된다.

    하지만 아래와 같이 코드를 수정하면 어떻게 될까?

    abstract class Car(val name: String) { // 자동차
        abstract fun move()
    }
    
    class GasCar(name: String) : Car(name) { // 서비스
        override fun move() {}
    }
    
    class EnergyCar(name: String) : Car(name) { // 서비스
        override fun move() {}
    }
    
    class Driver(val car: Car) { // 클라이언트
        fun drive() = car.move()
    }
    
    fun main() {
        Driver(GasCar("가성비 좋은 자동차")).drive()
    }

    가스 자동차를 타고 싶다면 운전자를 생성할때 가스 자동차 객체로 테스트를 한다고 하면 간단하게 테스트 자동차 객체로 "넣어" 주면 된다.

    위에서 이번 글에서 핵심인 단어 "넣어" 즉, 주입 이라는 소리이다.

     

    첫번째 코드에서는 운전자 라는 객체 안에서 에너지 자동차 객체를 생성하였다.
    하지만 두번째 코드에서는 운전자 라는 객체 밖에서 자동차 객체를 주입받았다.

     

    즉, 객체 안에서 다른 객체를 생성을 하게 되면 강한 의존성이 생겨서 추후에 변경이나 테스트 코드 작성시에 매우 불편하고 고된 작업을 하게 될 것이다.

     

    하지만 외부에서 객체를 주입받는다면 운전자는 Car 라는 추상 객체만 바라보면 된다.
    즉, 어떠한 자동차가와도 move 라는 함수를 호출하니 테스트 할때는 테스트 자동차, 다른 엔진의 자동차를 원하면 해당 자동차의 객체를 구현하고 주입을 해주면 되는 것이다.

     

    이로서 데이터에 구분이 생겼다. 즉, 단일책임의 원칙이 가능해진다는 것이다.
    자동차는 자신의 동작에 대해서만 구현을 하면되고, 운전자는 자동차를 운전만 하면 되는 것이다.

    <Service> Energy Car / <Abstract Service> Car ← <Client> Driver

    의존성 주입하기


    이러한 의존성 주입은 여러 방법으로 가능하다

    * 인터페이스를 통한 주입도 가능하지만 안드로이드 에서는 잘 안쓰니 Pass

    • 생성자를 통한 의존성 주입

    class Driver(val car: EnergyCar) {
        fun drive() = car.move()
    }
    • 세터를 이용한 의존성 주입
    class Driver {
        lateinit var car: EnergyCar
            // lateinit이 싫다면 EnergyCar? = null 도 가능!
    
        fun drive() = car.move()
    }

    실제로 사용하는 DI 라이브러리


    Kotlin → Koin

    Android → Dagger with Hilt

    Java Base Server → Spring? ( 서버 프레임워크 이기도 하면서 DI를 지향하는 라이브러리 )

    장점과 단점


    장점

    • 의존성 주입을 통해 클라이언트는 유연하게 구성 할 수 있다.
      인터페이스만 보고 구현을 하면 서비스가 변경 되더라도 클라이언트는 그대로 유지 할 수 있다.
    • 의존성 주입은 코드 동작을 변경할 필요가 없기 때문에 리펙토링을 할때 레거시 코드에 적용 할 수 있다.
      리펙토링을 시도하려고 할때 인터페이스로 의존성을 주입받고 구성이 가능하여 더 쉽다.
    • 코드 동작을 변경할 필요가 없기 때문에 테스트의 용이성이 뛰어나다.
      단위 테스트 시 스텁 또는 모의 개체를 사용하여 격리된 단위테스트가 훨신 쉽다. Mock
    • 의존성 주입을 하게 되면 두 명의 개발자가 서로를 사용하는 클래스를 독립적으로 개발할 수 있으며, 클래스가 통신 할 인터페이스만 알고 있으면 되기에 협업에 용이하다.

    단점

    • 명백한 기본값을 사용할때는 부담이 될 수 있다.
      단순히 하드코딩으로 해결되는 문제에서 의존성 주입을 하려고 한다면 배보다 배꼽이 더 커질 수 있다.
    • 동작과 생성을 분리하기 때문에 코드 추적을 어렵게 만들 수 있다.
      즉, 개발자는 문제를 파악하기 위해 더 많은 파일을 참조해야 한다.
      데이터를 가져오는 클래스를 하나 만들기 위해서는 인터페이스 하나 클래스 하나가 필요하기 때문

    꼭 써야 할까?


    굳이 사용할 필요가 없지만 프로젝트의 규모가 점점 커진다면 도입하여야 한다고 생각한다.

    위의 2가지의 단점이 존재하긴 하지만 그를 씹어먹는 장점 또한 많기도 하다.

    또한, 제일 중요하게 생각하는 테스트와 리펙토링에 대해서는 한없이 좋은 개발방법이기도 하기에 한번은 써보는 걸 추천한다.

    댓글

Designed by Tistory.