Dependency Injection (DI, 의존성 주입)

dongbin is free·2023년 2월 9일

CS

목록 보기
2/2

DI Package?

개요

아키텍처 구조를 익히기 위해 여러 레퍼런스를 보며 di 패키지의 존재 이유가 궁금했다.
이를 알아보기 위해 자료를 찾다가 DI IoC DIP라는 용어가 튀어나와 이를 정리해보려고 한다.

Inversion of Control (IoC, 제어의 역전)

코드의 흐름을 제어하는 주체가 바뀌는 것

  • Object를 생성하고, Lifecycle을 관리하고, 메서드를 수행하는 것 등 코드의 흐름을 일반적인 프로그램은 스스로 수행한다.
  • IoC를 적용한다는 것은 이러한 흐름 제어를 또 다른 3자가 수행한다는 것을 의미한다.


Android에서 IoC가 적용된 사례

class MainActivity : AppCompatActivity() {
	override fun onResume() {
    	super.onResume()
    	/* .. */
    }
    override fun onPause() {
    	super.onPause()
        /* .. */
    }
}
  • 생명주기 메서드가 호출되었을 때 동작만을 정의하고, 언제 생명주기 메서드를 호출할 지 신경쓰지 않는다. 액티비티의 메인 흐름권은 내 코드가 아닌 안드로이드 플랫폼이 갖는다.
  • IoC 관점에서 Library와 Framework 차이
    • Library는 내 코드가 Library를 이용한다. (제어권이 내 코드에 있음)
    • Framework는 Framework가 내 코드를 실행한다. (제어권이 Framework에 있음)

Dependency Inversion Principle (DIP, 의존관계 역전 법칙)

  • SOLID 원칙 중 하나로 DIP가 주장하는 바의 핵심은 추상화에 의존하는 것이다.


추상화가 아닌 구체클래스에 의존한 사례

class A {
	val b = B()
    fun text(str: String) = b.call(str)
}
  • B에 변경이 발생하면 이를 의존하는 A Class 역시 변경이 발생한다.
  • 즉 변경에 유연하지 않은 구조이다.


추상화에 의존할 경우

fun main() {
	val main = Main()
    main.inject()
    main.test()
}

interface Test {
	fun test(str: String)
}

class TestImpl: Test {
	override fun test(str: String) {
    	println(str)
    }
}

class Main {
	private lateinit var test: Test
    
    fun inject() {
    	test = TestImpl()
    }
    
    fun test() {
    	test.test("Test")
    }
}
  • Main Class는 Test Interface에 의존하기에 Test의 구현체인 TestImpl에서 변경이 발생해도 영향을 받지 않는다.
  • 다형성을 활용하여 변경에 유연한 구조가 된다.

Dependency Injection (DI, 의존성 주입)

  • 필요로 하는 Object를 스스로 생성하는 것이 아닌 외부로부터 주입받는 기법이다.
  • 생성자를 통한 주입, Setter와 같은 메서드를 통한 주입, Interface로 주입받는 방식이 있다.


Dependency (의존성)

  • 함수에 필요한 클래스 또는 참조 변수나 객체에 의존하는 것
  • 의존대상 B가 변하면 그것이 A에 영향을 미치는 것
  • 햄버거 가게 요리사는 햄버거 레시피에 의존하여 요리한다를 코드로 표현해보자
class BurgerChef {
	private var hamburgerRecipe: HamburgerRecipe
    
    fun cook() {
    	hamburgerRecipe = HamburgerRecipe().getBurgerRecipe()
    }
}

class HamburgerRecipe {
	/* .. */
}
  • BurgerChef Class는 HamburgerRecipe Class에 의존 관계가 생긴다.


의존관계를 interface로 추상화

  • 현재 구현에서 BurgerChef는 hamburgerRecipe만을 의존하는 구조이다.
  • 더 다양한 BurgerRecipe를 의존 받을 수 있도록 구현하려면 interface로 추상화 해야한다.
class BurgerChef {
	private var burgerRecipe: BurgerRecipe
    
    fun cookHamburger() {
    	burgerRecipe = HamburgerRecipe().getBurgerRecipe()
    }
    
    fun cookCheeseBurger() {
    	burgerRecipe = CheeseBurgerRecipe().getBurgerRecipe()
    }
}

interface BurgerRecipe {
	getBurgerRecipe()
}

class HamBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): HamburgerRecipe {
    	return HamburgerRecipe()
    }
}

class CheeseBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): CheeseBurgerRecipe {
    	return CheeseBurgerRecipe()
    }
}


Dependency Injection (주입)
의존관계를 외부에서 결정하는 것이기에 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이 된다. 런타임 시점의 의존관계를 외부에서 주입하여 DI구현을 완성한다.

  • 생성자를 이용
class BurgerChef(burgerRecipe: BurgerRecipe) {
	private var burgerRecipe: BurgerRecipe
    
    this.burgerRecipe = burgerRecipe
    
    fun cook() {
    	burgerRecipe = burgerRecipe.getBurgerRecipe()
    }
}

interface BurgerRecipe {
	getBurgerRecipe()
}

class HamBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): HamburgerRecipe {
    	return HamburgerRecipe()
    }
}

class CheeseBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): CheeseBurgerRecipe {
    	return CheeseBurgerRecipe()
    }
}

class Restaurant {
	private var burgerChef = BurgerChef(HamburgerRecipe())
    
    fun order() {
    	burgerChef.cook()
    }
}
  • Setter 메서드 이용
class BurgerChef() {
	private var burgerRecipe: BurgerRecipe
    
    fun setBurgerRecipe(burgerRecipe: BurgerRecipe) {
    	this.burgerRecipe = burgerRecipe
    }
    
    fun cook() {
    	burgerRecipe = burgerRecipe.getBurgerRecipe()
    }
}

interface BurgerRecipe {
	getBurgerRecipe()
}

class HamBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): HamburgerRecipe {
    	return HamburgerRecipe()
    }
}

class CheeseBurgerRecipe: BurgerRecipe {
	override fun getBurgerRecipe(): CheeseBurgerRecipe {
    	return CheeseBurgerRecipe()
    }
}

class Restaurant {
	private var burgerChef = BurgerChef()
    
    fun order() {
    	burgerChef.setBurgerRecipe(HamburgerRecipe())
        burgerChef.cook()
    }
}
  • 장점
    • 의존성이 줄어든다.
      • 의존한다는 것은 의존대상의 변화에 취약하다는 것을 의미한다.
      • 즉 대상이 변화했을 때, 이에 맞게 수정 과정이 필요하다.
      • DI로 구현 시, 주입받는 대상이 변하더라도 구현 자체를 수정할 일이 없거나 줄어든다.
    • 재사용성이 높은 코드가 된다.
      • 기존에 BurgerChef 내부에서만 사용되었던 BurgerRecipe를 별도로 구분하여 구현하면, 다른 클래스에서도 재사용할 수 있다.
    • 테스트하기 좋은 코드가 된다.
      • BurgerRecipe의 테스트를 BurgerChef 테스트와 분리하여 진행할 수 있다.
    • 가독성이 높아진다.
      • BurgerRecipe의 기능들을 별도로 분리하게 되어 자연스럽게 가독성이 높아진다

Android에서의 의존성 주입

안드로이드에서 사용되는 DI는 koin, dagger, hilt를 이용하여 구현된다. 이는 이후 알아보도록 할 계획이다.

참고1
참고2
참고3
참고4
참고5

profile
배운 것을 적어나가는 그런 공간.. 적다 보면 또 까먹는 그런 사람..

0개의 댓글