ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 의존성 주입
    Android/아키텍처를 알아야 앱 개발이 보인다 2023. 3. 18. 18:42

    https://developer.android.com/training/dependency-injection?hl=ko 

     

    Android의 종속 항목 삽입  |  Android 개발자  |  Android Developers

    Android의 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니

    developer.android.com

     

    • 의존성 Dependency  
      • A가 B를 의존한다 -> B에 변화가 생기면 A에 영향을 끼친다.
      • ex) 요리는 레시피에 의존한다 -> 레시피에 변화가 생기면 요리에 영향을 끼친다.
    • 주입 Injection   : 생성자나 메서드를 통해 외부로부터 생성된 객체를 받는것.
    • 의존성 주입 : 의존 관계에 있는 클래스의 객체를 외부로 부터 생성하여 주입받는것. Car와 engine으로 예시를 들어보자.

    아래는 클래스 간 의존성 주입의 예시이다.

    // 1. 클래스 자체에 종속항목을 구성한다.
    
    class Car {
    
        private val engine = Engine()
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.start()
    }
    
    
    
    // 2. 다른 곳에서 부터 객체를 가져온다.  , 생성자 이용
    
    class Car(private val engine: Engine) {
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val engine = Engine()
        val car = Car(engine)
        car.start()
    }
    
    // 3. 객체를 매개변수로 받아온다.  , 메소드 이용
    
    class Car {
        lateinit var engine: Engine
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.engine = Engine()
        car.start()
    }

     

    1. 클래스 자체에 종속 항목 구성
      • Car 클래스 자체가 Engine을 구성한다.  Car 클래스 내부에서 Engine 클래스의 인스턴스를 생성하고, 자체적으로 관리하는 방식
      • Car와 Engine이 밀접하게 연결되어 있어 서브클래스 및 구현을 쉽게 할수 없다. -> 예를 들어서 전기 엔진 혹은 가솔린 엔진의 차를 구현하려면 새로 두가지 유형의 Car를 생성해야 한다.
      •  Car 클래스와 Engine 클래스 간의 결합도가 높아지기 때문에, 유지보수나 확장성이 낮아질 수 있다.
    2. 객체 가져오기 - 생성자 주입
      • Car 클래스 생성자에서 Engine 인스턴스를 외부에서 받아오는 방식으로, 의존성을 외부로부터 주입받는 것
      • main 함수에서 Car를 사용한다. Car는 Engine에 종속 되므로 앱은 Engine 인스턴스를 생성후 이를 사용해 Car 인스턴스를 구성한다.
      • 이로 인해서 Car를 재사용할 수 있고, Engine의 다양한 구현을 Car에 전달 할 수있다.
      • Car 클래스 내부에서 Engine 클래스의 인스턴스를 생성하지 않으므로, 결합도가 낮아지고, 유지보수나 확장성이 높아질 수 있다.
    3. 매개변수로 객체 받아오기 - 메소드 주입
      • Car 클래스에서 Engine 인스턴스를 선언하지 않고, start() 메서드에서 Engine 인스턴스를 매개변수로 받아오는 방식
      • 2번과 마찬가지로 외부에서 Engine 인스턴스를 받아와 사용하므로, 결합도가 낮아지고 유지보수나 확장성이 높아질 수 있다.
      • Engine 인스턴스가 빈번하게 변경되거나 업데이트되는 경우, 매번 start() 메서드를 호출할 때마다 Engine 인스턴스를 매개변수로 넘겨주어야 한다.

    주로 2,3의 방법을 주로 사용한다.

     

    • 변경의 전이 :  객체 간의 의존성을 느슨하게 결합시키는 원칙 
      • 상위 수준 모듈은 하위 수준 모듈의 구현에 의존하지 않고, 추상화에 의존해야 한다는 원칙 (상위는 interface나 abstract class로, 하위는 class로 구현)
      • Computer의 CPU나 Car의 Engine이 변경되게 되면 (예로, 이름을 CPU_1,Engine_1로 변경하면) 이는 의존 관계에 있는 Computer와  Car에 영향을 끼친다. 이를 해결하기 위해서 CPU나 Engine을 interface로 변경 할 수있다.
    interface Engine {
        fun start()
    }
    
    class GasolineEngine : Engine {
        override fun start() {
            println("Gasoline engine starts.")
        }
    }
    
    class ElectricEngine : Engine {
        override fun start() {
            println("Electric engine starts.")
        }
    }
    
    class Car(private val engine: Engine) {
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array<String>) {
        val engine = GasolineEngine()
        val car = Car(engine)
        car.start()
    }

     Car 클래스가 Engine 인터페이스에만 의존하도록 하여, GasolineEngine 클래스나 ElectricEngine 클래스의 구현 방식이 변경되더라도 Car 클래스는 수정하지 않아도 된다.

     

    • 제어의 역전 : 객체가 자신이 사용할 객체를 직접 생성하거나 관리하지 않고, 외부에서 객체를 생성하고 관리하는 것
      • 외부로 부터 객체를 생성하여 이를 생성자 또는 매개변수로 제공받는다.
      • 제어의 역전을 적용하기 전에는 자체 클래스 내부에서 객체를 생성하고 관리 했으나, 적용 후에는 객체의 생성 및 관리를 외부에서 담당하게 된다. 
      • 이를 통해서 결합도가 약해지고, 클래스는 객체의 변경사항이 있어도 내부 나 매개 변수를 변경하지 않아도 된다.
    interface Service {
        fun doSomething()
    }
    
    class ServiceImpl : Service {
        override fun doSomething() {
            println("Doing something...")
        }
    }
    
    class Client(private val service: Service) {
        fun execute() {
            service.doSomething()
        }
    }
    
    fun main() {
        val service = ServiceImpl()
        val client = Client(service)
        client.execute()
    }
    • Client 클래스가 Service 인터페이스에 의존한다 , Client 클래스의 생성자에서 Service 인터페이스를 받아서 멤버 변수로 저장한다. 
      • 의존하는 객체를 외부에서 주입받게 되면, Client 클래스 내부에서 Service 구현체를 직접 생성하거나 참조하는 것이 아니라, 외부에서 주입받은 객체를 사용하여 메서드를 호출  -> 의존성 주입
      • Client 클래스는 Service 인터페이스에만 의존하고 있으며, 구체적인 구현체(ServiceImpl)에는 의존하지 않습니다. 이렇게 의존성을 느슨하게 결합시키는 것이 제어의 역전(Inversion of Control)
      • Client 클래스는 Service 인터페이스를 사용하므로, Service 인터페이스를 구현하는 다른 클래스를 주입하면 Client 클래스의 동작이 바뀌게 된다.
    • 의존성 주입의 장단점
      • 장점
        • 인터페이스를 기반으로 작성되어 코드가 유연해진다.
        • 주입하는 코드만 변경하면 되므로, 유지보수가 편해진다.
        • 의존성 주입 객체를 stub이나 mock등의 객체로 사용할수 있어 단위테스트가 쉬워진다.
        • 클래스간 결합도가 느슨해 진다.
        • 인터페이스 기반으로 작성되어 여러 개발자가 이를 통해 독립적으로 클래스를 개발할수 있다. -> 클래스간의 의존하는 인터페이스만 알면 된다.
      • 단점
        • 번거롭다.
        • 동작과 구성을 분리해서 코드 추적이 힘들다.
        • dagger2 사용시 시간이 더 걸린다.

    댓글

Designed by Tistory.