개발/안드로이드

의존성 주입이란?

스몰스테핑 2024. 7. 30. 14:57

이전에도 종속 항목 삽입에 대해 이야기를 하며 개념과 예제를 다루고, Hilt에 대해 다뤄보며 이야기를 해봤지만, 솔직히 혼자 이해하기에는 아직 어려운 관계로 강의를 통해 공부하며 좀 더 자세히 알아보고 프로젝트를 진행해보고자 한다.

 

이전에 다뤘던 의존성 주입, 종속 항목 삽입에 대한 포스팅은 다음과 같다.

https://small-stepping.tistory.com/957

 

종속 항목 삽입

이전글에서 이어지는 내용입니다.https://small-stepping.tistory.com/956 UI 레이어와 데이터 레이어 분리레이어를 분리하는 이유코드를 여러 레이어로 분리하면 앱의 확장성이 높아지며 앱이 더 견고해

small-stepping.tistory.com

https://small-stepping.tistory.com/1096

 

Android에서 Hilt란?

Hilt란? 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Andorid용 종속 항목 삽입 라이브러리이다. 종속 항목 삽입(DI)를 실행하려면 모든 클래스와 종속 항목을 수동으로 구성하

small-stepping.tistory.com

https://small-stepping.tistory.com/1105

 

기존 프로젝트에 Hilt 적용해보기

https://developer.android.com/codelabs/basic-android-kotlin-compose-practice-bus-schedule-app?hl=ko&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-6-pathway-2%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.andr

small-stepping.tistory.com

 

 


 

이번 글은 첫 포스팅이었던 "종속 항목 삽입" 포스팅과 글이 매우 유사할 것이다.

의존성이라는 개념에 대해 다시 다루고, 예제를 다루며, 의존성 주입을 알아보고, Injector에 대해 알아보며, 의존성 주입의 장점을 다시 한번 알아보고자 한다.

 

 

1. 의존성이란?

의존성이란, 어떤 대상이 참조하는 객체(또는 함수)를 말한다.

class Engine

class Car {
    val engine = Engine()
}

 

Car 클래스에서 engine을 생성하고 있다.

그렇기에 Car는 Engine에 의존한다. 또는, 의존적이다라고 할 수 있다.

Car 클래스 입장에서는 Engine은 의존성이 된다.

 

 

2. 의존성 주입이란?

의존성 주입이란, 대상 객체(Client)에 의존성을 제공하는 기술이다.

class Engine

class Car {
    val engine = Engine()
}

 

방금 다뤘던 예제 코드를 다시 보자.

Car는 Engine 인스턴스를 생성하는 책임을 가지고 있다.

그 책임을 제거하면 다음과 같이 생성자에 들어가게 된다.

 

class Car(val engine: Engine) {

}

 

Engine을 외부에서 전달 받는 것이다.

Car는 Engine 생성에 대한 책임이 없다. 이는 IoC(제어의 역전)이라고 할 수 있다.

 

생성자 매개변수에 엔진이 들어가면, Car는 엔진을 생성하는 대신 외부에서 엔진을 생성하게 되고, 생성된 엔진을 매개변수에서 받게된다.

이렇게 된다면 Car는 더이상 엔진 생성에 대한 책임을 갖지 않게 된다.

 

이러한 패턴을 Inversion Of Control이라고 부른다. 한국말로 제어의 역전이라고도 한다.

말 그대로 객체의 생성에 대한 책임을 내부에서 외부로 뒤집으면서 엔진에 대한 제어를 역전시킨다는 것이다.

제어를 역전시킴으로써 엔진이라는 의존성을 외부에서 주입할 수 있게 세팅한다는 것이다.

 

3. 의존성 주입의 장점

의존성 주입이 가져다 주는 장점은 다음과 같다.

fun main(args: Array<String>) {
    val gasolineCar = Car(GasolineEngine())
    val dieselCar = Car(DieselEngine())
}

 

  • Car 소스코드를 변경하지 않는다. (재사용성)
  • 결합도를 느슨하게 만들어준다. (디커플링)

 

위처럼 엔진은 외부에서 생성되어 Car의 생성자 인수로 전달된다.

이렇게 된다면 Car의 코드를 변경하지 않고, 단순히 어떠한 엔진을 상속해서 확장한 여러 타입의 엔진을 할당하는 것이 가능해진다.

Car의 소스코드 변경은 없기에 재사용성도 늘어나며, Car 내부에서 엔진을 초기화하지 않고 외부에서 주입하고 있기 대문에 클래스간의 결합도 또한, 낮췄다고 할 수 있다.

 

class Car (val engine: Engine) {
    // 매우 긴 소스 코드
}

 

Car는 엔진을 제외하고도 여러 복잡한 기계장치, 부품들로 구성되어 있다.

이를 표현하게 된다면 아마 매우 긴 소스 코드가 Car 안에 구성하게 될 것이고, 이를 유지보수하거나 수정하려 든다면 매우 골치 아프게 될 것이다. 그렇다면 나머지 소스코드들도 엔진처럼 외부에서 주입받게 만든다면?

Car의 소스코드를 간결하게 만들 수 있을 것이다.

 

class Car (
    val engine: Engine,
    val wheels: Wheels,
    val wiper: Wiper,
    val battery: Battery
) {
    // 적어진 소스 코드
}

 

엔진 휠 와이퍼 배터리 같이 하나의 기능만을 책임질 수 있도록 캡슐화 시키는 것을 단일 책임 원칙(SRP)이라고 한다.

이런 적은 소스 코드가 있다면, 개발자는 Car 클래스가 가져야할 비즈니스 로직에 더욱 더 집중할 수 있게 된다.

즉, 개발과 유지보수가 더욱 간편해진다는 것이다.

 

거대한 소스 코드를 단일 책임 원칙을 갖는 작은 캡슐들로 나누게 된다면, 다른 부서를 포함한 팀원과 협업시에도 업무를 나누어 진행하는데 도움이 된다.

 

의존성 주입의 또다른 장점은 테스트가 편해진다는 것이다.

 

class CarTest {
    @CarTest
    fun 'Car 성공 케이스 테스트'() {
        val car = Car(FakeEngine())
        // 생략
    }
}

 

예를 들어 엔진을 확장한 클래스인 FakeEngine()이 있다.

이 엔진을 주입함으로써 테스트의 성공과 실패를 제어할 수 있다.

 

class CarTest {
    @CarTest
    fun 'Car 실패 케이스 테스트'() {
        val car = Car(FakeBrokenEngine())
        // 생략
    }
}

 

 

성공 케이스 뿐만 아니라 실패 케이스 또한, 위처럼 확장 클래스를 주입하여 제어할 수 있다.

이를 통해 테스트에 대한 용이성이 얼마나 좋아졌는지 확인이 가능하다.

 

4. Injector란?

Injector란, 의존성을 클라이언트에게 제공하는 역할이다.

의존성을 클라이언트에게 제공하는 것은 Injector라는 개념을 통해 이루어진다.

 

fun main(args: Array<String>) {
    val engine = Engine()
    val car = Car(engine)
}

 

 

예를 들어, 위처럼 Injector가 없는 코드가 존재한다.

메인 함수에서 engine을 생성하고 car에 생성자 인수로 전달한다.

 

class Injector {
    fun getEngine() {
        return Engine()
    }
}

fun main(args: Array<String>) {
    val engine = Injector().getEngine()
    val car = Car(engine)
}

 

 

여기에 Injector를 추가한다.

엔진을 생성하고 반환하는 메서드가 존재하고, 반환된 엔진 객체를 Car에 주입한다.

이제 더이상 메인 함수에서 엔진을 생성하는 코드를 사용하지 않고, Injector를 통해서 엔진을 생성하고 Car 클래스에 제공하고 있다.

 

Injector를 사용하게 되면 필요할 때마다 getEngine을 호출하여 Engine 객체를 얻을 수 있다.

호출할 때마다 매번 다른 Engine 인스턴스를 얻게 될 것이다.

 

만약 getEngine을 호출해서 동일한 Engine 인스턴스를 얻고 싶다면?

 

class Injector {
    val engine = Engine()
}

fun main(args: Array<String>) {
    val injector = Injector()
    val engine1 = injector.engine
    val engine2 = injector.engine
    val car1 = Car(engine1)
    val car2 = Car(engine2)
}

 

 

방법은 다양하나 현 상황에서 제일 보기 좋은 것은 위와 같이 Injector 클래스에서 하나의 엔진을 생성해두고, 여러 곳에서 참조할 수 있게 하는 것이다. engine1, engine2로 다르지만, 동일한 engine 인스턴스를 참조하고 있다. (자원 공유)

 

이렇게 Injector를 통해 자원 공유가 가능하다.

 

또한, Injector의 또 다른 명칭은 다음과 같다.

  1. Container
  2. Assembler
  3. Provider
  4. Factory

 

5. 의존성 주입 장점 요약

  1. 결합도를 낮춘다.
  2. 재사용성이 가능하다.
  3. 보일러플레이트 감소한다. (별 수정 없이 반복적으로 사용되는 코드의 감소)
  4. 테스트가 쉽다.
  5. 의존성 관리가 용이하다. (자원 공유)