종속성(dependency)이란 한 객체가 다른 객체에 의존할 때 그 의존 대상이 되는 객체를 말합니다. 종속성 주입(Dependency Injection, DI)은 객체가 필요로 하는 객체를 스스로 생성하지 않고 외부에서 공급받는 기법으로, '제어 역전(Inversion of Control)'이라고도 부릅니다. 의존 객체를 목(mock)으로 쉽게 대체할 수 있어 테스트를 한결 수월하게 만들어 주는 유용한 기술입니다.
예를 들어 클래스 A가 클래스 B의 메서드를 호출하고, B가 다시 클래스 C의 메서드를 호출한다면 A는 B에, B는 C에 의존하는 관계가 성립합니다. 종속성 주입을 활용하면 각 클래스가 B와 C의 인스턴스를 직접 생성하는 대신, C의 인스턴스를 B에 전달하고 B의 인스턴스를 A에 전달하는 구조로 코드를 작성할 수 있습니다.
아래 예제에서 Runner 클래스는 Logger 클래스에 의존하며, 생성자 안에서 Logger 인스턴스를 직접 생성합니다. 이 코드에는 몇 가지 문제점이 있습니다.
Logger 클래스가 Runner와 강하게 결합되어 있어, Runner 코드를 수정하지 않고는 다른 구현체로 교체할 수 없습니다.
Logger 자체에 의존 객체가 있다면, Runner는 Logger를 인스턴스화하기 전에 그 의존성까지 모두 설정해야 합니다.
테스트가 어려워집니다. Logger가 네트워크나 파일 시스템에 접근하는 것처럼 리소스 소모가 큰 클래스라면 테스트 속도가 느려지고, 쉽게 대체할 수도 없습니다.
using System;
class Program{
static void Main(string[] args){
var runner = new Runner();
runner.Run();
}
}
class Runner{
private Logger _logger;
public Runner(){
_logger = new Logger();
}
public void Run(){
// Do some work
_logger.Log("Message to be logged");
}
}
class Logger{
public void Log(string message){
Console.WriteLine(message);
}
}
종속성 주입을 적용하면 Runner의 생성자가 구체적인 객체 대신 ILogger 인터페이스를 받도록 수정하고, Logger 클래스가 ILogger를 구현하도록 변경합니다. 이렇게 하면 Logger 인스턴스를 Runner의 생성자에 외부에서 전달할 수 있습니다. 이 방식의 가장 큰 장점은 테스트 시 ILogger를 구현하는 TestLogger 같은 클래스를 만들어 Runner 생성자에 넘겨줌으로써, 실제 Logger 없이도 가볍고 빠르게 테스트를 진행할 수 있다는 점입니다.
예제
using System;
class Program{
static void Main(string[] args){
var logger = new Logger();
var runner = new Runner(logger);
runner.Run();
}
}
class Runner{
private ILogger _logger;
public Runner(ILogger logger){
_logger = logger;
}
public void Run(){
// Do some work
_logger.Log("Message to be logged");
}
}
interface ILogger{
void Log(string message);
}
class Logger : ILogger{
public void Log(string message){
Console.WriteLine(message);
}
}
실행 결과
Message to be logged