Dependency injection is a simple design pattern that decouples a component from its dependency.
Dependency Injection, 즉 DI 란 외부의 두 객체 간의 관계에 대한 디자인 패턴으로, 원문에서 볼 수 있듯 간단히 말해 의존성과 관련해 결합도를 낮추는 방식이다. 두 객체에 대해서 결합도 (Coupling) 에 대해 낮추어 느슨한 결합을 가지게 하여 의존 관계가 고정되지 않도록 하고, 런타임에 동적으로 주입하는 형태라고 할 수 있다.
여느 디자인 패턴이 그렇듯 사실 설명만으로는 이해하기가 힘들기 때문에 예시 코드 스니펫을 보면서 설명해보고자 한다.
간단히, Player 클래스에 대해 InputReader를 통해 버튼 입력을 감지하여 Jump하도록 하는 상황의 예시를 들어보자.
// InputReader 클래스 : 버튼 입력 감지 메서드
public class InputReader()
{
public void GetButtonInput()
{
// logic
}
}
// Player 클래스
public class Player
{
// 속성으로 InputReader 클래스를 가짐
private InputReader inputReader;
public Player()
{
inputReader = new InputReader();
}
public void Jump()
{
inputReader.GetButtonInput();
}
}
이 경우를 살펴보면,
Player 클래스는 InputReader 클래스에 대해 의존성을 가진다.
즉, 강한 결합 Tight Coupling 상태로, 변경 사항에 취약해진다.
using System;
// 1. 인터페이스 정의 (추상화)
public interface IReader
{
void GetButtonInput();
}
// 구체적인 구현체
public class InputReader : IReader
{
public void GetButtonInput()
{
// 입력 처리 로직
}
}
public class Player
{
// 인터페이스에 의존
private IReader inputReader;
// 생성자를 통한 의존성 주입
public Player(IReader inputReader)
{
this.inputReader = inputReader;
}
public void Jump()
{
inputReader.GetButtonInput();
}
}
public class PlayerFactory
{
public void SpawnPlayer()
{
IReader inputReader = new InputReader();
Player player = new Player(inputReader);
}
}
이 경우, InputReader 클래스는 IReader 인터페이스를 통해 구현된다.
기존의 Player 클래스에서 Jump 메서드는 Player 객체가 직접 생성한 InputReader에 의존적이었던 반면, 인터페이스에 의존하게 된다.
PlayerFactory 클래스는 여기서 의존성 주입을 위한 DI 컨테이너 역할을 한다.
DI Container
- DI Container 는 런타임에서 필요한 오브젝트를 생성하고 의존성이 있는 두 객체 연결을 위해 주입하는 역할을 한다.
- 위의 예시에서 PlayerFactory는 InputReader 객체를 만들고, 이 객체를 Player에 주입시킨다.
이러한 개념을 제어의 역전(Inversion of Control, IoC) 라고 부른다.
제어의 역전(Inversion of Control, IoC) 은 소프트웨어 디자인 원칙 중 하나로, 객체의 생성, 생명 주기 관리, 의존성 관리 등의 제어 흐름을 개발자가 직접 수행하는 대신 프레임워크나 컨테이너와 같은 외부 주체에게 위임하는 것을 의미한다.