Dagger2 기초: 의존에 대하여

5 분 소요

개체의 의존성

우리는 개체 지향 프로그래밍을 할 때 필연적으로 개체에 의존을 하게 된다. 예를 들면 아래와 같은 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
class CentralProcessingUnit {
  private val memory: Memory = DramMemory(GigaBytes(4))

  fun process(instruction: Instruction) : Result {
    val data = memory.fetchData()
    return execute(instruction, data)
  }
}

fun main() {
  val cpu = CentralProcessingUnit()
  println(cpu.process(Instructions.Add(1, 1)))
}

CentralProcessingUnit(이하 CPU) 라는 개체는 Memory 에 의존해서 명령어를 수행한다. 물론 개체 자체만으로 무언가할 수는 있겠지만, 보통은 이렇게 여러 개체들의 협력으로 우리 프로그램은 수행된다.

갑자기 컴퓨터를 업그레이드하고 싶어졌다. 메모리를 좀 더 올려보도록 하자.

의존성 주입

위 코드로는 도저히 메모리를 교체할 수가 없다. 하고 싶다면 CPU 를 분해해서 메모리를 교체해아하는데, 불가능에 가까울 것 같다. 이런 CPU는 굉장히 확장성이 떨어진다. 메모리를 올리고 싶어도 올리기 매우 어렵고 까다롭기 때문이다.

그렇다면 이제 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CentralProcessingUnit(
  private val memory: Memory
) {
  fun process(instruction: Instruction) : Result {
    val data = memory.fetchData()
    return execute(instruction, data)
  }
}

fun main() {
  val memory = DramMemory(GigaBytes(8))
  val cpu = CentralProcessingUnit(memory)
  println(cpu.process(Instructions.Add(1, 1)))
}

CPU가 메모리 개체를 받아들이게 함으로서, 메모리를 확장할 수 있게되었다!

CPU는 자신이 하는 일에 영향을 받지 않으면서, 좀 더 크거나 혹은 더 빠른 메모리를 사용할 수 있다.

위 코드는 동일한 개체간 의존성이라도 어떻게 구현하느냐에 따라서 확장성있게 만들 수 있느냐를 보여주는 대표적인 예이다.

그리고 후자를 외부에서 의존성을 받는다 라고 하여 의존성 주입 이라고 표현한다.

위키피디아의 Dependency injection 정의도 아래에 첨부했다.

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies.

참고로 첫 번째 코드처럼 의존이 개체의 라이프사이클과 동일(생성과 함께 의존도 생성되고, 파괴될 때 함께 파괴된다)한 것을 구성(Composition) 이라고 한다.

그렇다고해서 구성이 나쁜 것은 아니다.

구성

캡슐화를 위해 의존 개체를 개체 안으로 얼마든지 숨길 수 있다. 무조건적으로 의존성 주입을 사용하는게 아니라, 확장성이 필요한 의존을 선택적으로 주입받는게 중요하다.

위 예를 확장해서 얘기하자면, CPU, Memory 등 부품은 서로 의존하면서 확장성있게 구현했다고 하자. 하지만 이를 사용하는 본체는 사용자가 알 필요가 없을 수도 있다. 필요하다면 본체를 본해해서 조립하면 된다. 코드로 표현해보자면 대략 이런 모양일 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Computer {
  private val cpu: Cpu = IntelCpu()
  private val memory: Memory = DramMemory()
  private val gpu: Gpu = NvidiaGpu()
  private val storage: Stroage = M2Ssd()

  fun run() {
    storage.initilize()
    memory.initilize()
    gpu.initilize()
    cpu.initilize()
  }
}

fun main() {
  Computer().run()
}

이 코드를 보고 의존성 주입을 사용하지 않았다고 할 수도 있겠지만, 의도에 따라 구성을 사용했을 수도 있다는 점에 유의하자.

Dependency Injection Framework

위 예제 코드처럼 개체를 확장성있게 만들었다고 하더라도, 의존 변경시 번거로운건 여전하다. 개체 밖에선 여전히 어떤 개체를 의존으로 줄지 결정하고 생성해야 하기 때문이다.

코드가 복잡해지면 관리도 잘 안되고 실수를 할 여지가 많아진다. 그럼 확장성 있게 만들 수 있다는걸 알더라도 다소 복잡해진(혹은 귀찮)다는 이유로 의존을 내부로 숨기게 된다.

인간은 도구를 사용하는 동물이라고 했던가. 다양한 프레임워크(혹은 라이브러리)가 의존성 주입을 도와주고 있고, 다음 포스팅부터 그 중 안드로이드 환경에서 많이 쓰고 있는 Dagger2 에 대해서 알아볼까 한다.

덧붙이자면 안드로이드 환경에서 Dagger2 를 더 편하게 사용할 수 있는 Hilt도 있는데, 이에 대해선 나아-중에 알아보도록 하자.

참조

댓글남기기