Dependency injection (의존성 주입)
DI는 dependency injection의 줄임말입니다. 프로그래밍을 하면서 모두 들어봤을 단어이고, Spring을 사용한다면 알든 모르든 모두가 사용하고 있는 개념입니다.
DI를 번역하면 의존성 주입이라는 뜻이고, 개념을 말하자면 객체 지향 프로그래밍에서 객체 간의 의존성을 관리하는 설계 패턴 중 하나로, 객체가 자신의 의존성을 직접 생성하는 대신 외부에서 주입받는 방식입니다. 이 패턴을 사용하면 코드의 유연성과 재사용성을 높일 수 있으며, 테스트가 용이해집니다.
Dependency injection의 주요 개념
의존성 주입은 의존성과 주입의 두 가지 개념으로 나눠서 설명할 수 있습니다.
의존성 (dependency) 정의
의존성은 한 객체가 다른 객체에 의존하고 있는 상황을 말합니다. 예를 들어, 클래스 A가 클래스 B의 메서드를 호출해야 하는 경우, 클래스 A는 클래스 B에 의존한다고 할 수 있습니다. 이때 클래스 A가 B의 메서드를 호출하려고 직접 B를 생성하는 방식으로 의존성을 해결하면 클래스 A는 클래스 B에 단단히 결합되어 변경에 취약해집니다.
주입 (injection)
주입은 말 그대로 클래스가 필요한 의존성을 외부에서 주입받는 방식입니다. 즉, 클래스가 스스로 의존성을 생성하지 않고, 다른 클래스 또는 컨테이너가 그 역할을 대신합니다. 이를 통해 클래스는 자신이 필요한 객체에 대해 구체적인 정보 없이도 기능을 수행할 수 있게 됩니다.
Dependency injection의 유형
의존성을 주입하는 방법에는 두 가지 유형이 있습니다.
생성자 주입 (constructor injection)
생성자를 통해 객체의 의존성을 전달하는 방식입니다.
가장 원시적이면서도 일반적인 방법으로, 의존성이 필요하지 않은 객체의 생성을 방지할 수 있습니다.
trait Engine
class ElectronicEngine extends Engine
class DieselEngine extends Engine
class Car(val engine: Engine) {
def drive(): Unit = ???
}
val myEngine = new ElectronicEngine()
val myCar = new Car(myEngine) // 외부에서 필요한 의존성을 주입해줍니다
car.drive()
세터 주입 (setter injection)
세터 메서드를 사용하여 의존성을 주입하는 방식입니다. 생성 이후에도 의존성을 설정하거나 변경할 수 있어 유연합니다.
단, 세터를 이용해 의존성을 변경할경우 매우 큰 프로그램을 짜고 있다면 코드의 어느 지점부터 의존성이 변경되었는지 추적하기가 매우 어렵습니다.
메모리가 매우 부족한 머신이 아니라면 현대의 컴퓨터에서는 단지 필요한 부분만 변경해서 객체를 새로 생성하는 방법을 사용해봐도 좋습니다. 이는 함수형 프로그래밍에서 중요하게 여기는 불변성과 연관됩니다.
trait Engine
class ElectronicEngine extends Engine // 전기 엔진
class DieselEngine extends Engine // 디젤 엔진
// kotlin, scala에서는 var로만 선언해도 충분합니다
class Car(var engine: Engine) {
// 자바에서는 setter를 직접 만들어야 합니다
// public void setEngine(Engine engine) {
// this.engine = engine;
// }
def drive(): Unit = ???
}
val myEngine = new ElectronicEngine()
val myCar = new Car(myEngine)
val secondEngine = new DieselEngine()
// 세터를 이용해 변경합니다
myCar.engine = secondEngine // myCar.setEngine(secondEngine)
Dependency injection의 장/단점
Dependency Injection도 만능 도구는 아니고 장/단점이 존재합니다.
Dependency injection의 장점
- 유연한 코드 설계: 객체 간의 결합도가 낮아지기 때문에 서로 다른 객체를 쉽게 교체할 수 있습니다.
- 테스트 용이성: 테스트 시 실제 객체 대신 Mock 객체를 주입할 수 있어 테스트가 간편해집니다.
- 유지보수성 향상: 의존성을 쉽게 교체할 수 있어 새로운 기능 추가 및 수정 시 코드 변경이 최소화됩니다.
Dependency injection의 단점
- 복잡성 증가: 의존성 주입을 위해 추가적인 코드와 설정이 필요할 수 있으며, DI 컨테이너와 같은 도구를 사용할 경우 학습 곡선이 존재할 수 있습니다.
- 추적의 어려움: 객체 간의 의존 관계를 명시적으로 코드에서 보기가 어려워져, 추적이 힘들어질 수 있습니다.
Dependency injection을 위한 라이브러리/프레임워크
본인이 직접 만들고 사용하기에는 어려움이 있어 이미 구현된 라이브러리를 통해 의존성 주입을 해결할 수 있습니다.
- Spring framework: 핵심 개념에도 DI가 포함되어있어 기본적으로 DI 패턴을 이용하도록 강제합니다
- Google guice: Spring framework를 사용하기가 부담되거나, 다른 라이브러리 혹은 프레임워크를 사용중일경우 훌륭한 대안이 될 수 있습니다.
- 이 외에도 ScalaDi, MacWire 등 다양한 라이브러리들이 존재합니다
정리
의존성 주입(Dependency injection)은 객체 간의 의존성을 효과적으로 관리하고 코드의 유연성과 재사용성을 높이는 중요한 설계 패턴입니다. 특히, 유지보수와 테스트의 용이성을 고려할 때 매우 유용한 패턴이며, 대규모 프로젝트에서 복잡한 의존성을 관리하는 데 큰 도움이 됩니다.
앞으로 이를 Scala에서 구현하는 방법에 대해 알아보겠습니다.