의존성 주입(DI, Dependency Injection)
소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 기술입니다. 여기서 "의존성"은 서비스로 사용할 수 있는 객체를 의미하며 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다. "주입"은 의존성(서비스)을 사용하려면 객체(클라이언트)로 전달하는 것을 의미합니다. 서비스는 클라이언트의 상태의 일부입니다.
즉, 의존성 주입은 객체간의 의존 관계를 외부에서 주입(Inject) 하는 디자인 패턴입니다. 객체가 직접 의존성을 생성하는 것이 아니라, 외부에서 주입해주므로 결합도를 낮추고 코드의 유지보수성과 테스트 용이성을 높일 수 있습니다.
의존성 주입의 의도
객체가 다른 객체에 의존할 때, 직접 객체를 생성하면 다음과 같은 문제점이 발생합니다.
- 결합도가 높아짐 → 특정 클래스에 강하게 의존하게 되어 변경이 어렵다.
- 테스트가 어려워짐 → 특정 객체를 직접 생성하면, 테스트 시 Mock 객체를 주입하기 어렵다.
- 유연성이 떨어짐 → 객체를 변경하려면 코드를 직접 수정해야 한다.
의존성 주입을 활용하면, 객체를 직접 생성하는 것이 아니라 외부에서 주입하기 때문에 위의 문제를 해결 할 수 가 있습니다. 의존성 주입을 통해 해당 문제들을 해소 하여 프로그램 설계 시 결합도를 낮추고, 의존관계 역전 원칙(DIP)과 단일 책임 원칙(SRP)을 따르도록 도와줍니다. 이는 클라이언트가 의존성을 찾기 위해 시스템에 대해 알 필요 없이, 외부에서 필요한 의존성을 주입받는 방식이기 때문입니다.
의존성 주입과 의존관계 역전 원칙(DIP)
의존성 주입은 SOLID 원칙 중 하나인 의존관계 역전 원칙(Dependency Inversion Principle, DIP)를 구현하는 방법중에 하나 입니다. DIP는 다음을 강조하고 있습니다.
- 상위 모듈은 하위 모듈에 의존해서는 안 된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.
즉, 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안 된다"라는 것을 의미합니다.
의존성 주입 방법
DI는 다양한 방식으로 구현할 수 있습니다. 대표적으로 생성자 주입, 세터(Setter) 주입, 인터페이스 기반 주입 등이 있습니다.
생성자 주입 (Constructor Injection)
객체의 생성자를 통해 의존성을 주입하는 방식입니다.
📌 C++ 예제
class Engine
{
public:
void Start()
{
std::cout << "Engine started" << std::endl;
}
};
class Car
{
private:
Engine* EngineInstance;
public:
Car(Engine* InEngine) : EngineInstance(InEngine) {}
void Drive()
{
EngineInstance->Start();
std::cout << "Car is driving" << std::endl;
}
};
// 사용 예제
Engine MyEngine;
Car MyCar(&MyEngine);
MyCar.Drive();
- ✅ 장점 : 불변성을 보장하며, 객체의 의존성을 명확하게 설정할 수 있습니다.
- ❌ 단점 : 의존성이 많아질 경우 생성자가 복잡해질 수 있습니다.
세터 주입 (Setter Injection)
객체를 생성한 후, Setter 메서드를 통해 의존성을 주입하는 방식입니다.
📌 언리얼 C++ 예제
class Engine
{
public:
void Start()
{
UE_LOG(LogTemp, Log, TEXT("Engine started"));
}
};
class Car
{
private:
Engine* EngineInstance;
public:
void SetEngine(Engine* InEngine) { EngineInstance = InEngine; }
void Drive()
{
if (EngineInstance)
{
EngineInstance->Start();
UE_LOG(LogTemp, Log, TEXT("Car is driving"));
}
}
};
// 사용 예제
Engine MyEngine;
Car MyCar;
MyCar.SetEngine(&MyEngine);
MyCar.Drive();
- ✅ 장점 : 선택적으로 의존성을 주입할 수 있으며, 필요할 때만 주입이 가능합니다.
- ❌ 단점 : 객체가 생성된 후에 반드시 주입이 필요하기 때문에 주입을 잊어버리면 런타임 오류가 발생할 수 있습니다.
인터페이스 기반 주입(Interface Injection)
인터페이스를 통해 객체의 의존성을 주입하는 방식입니다. 이는 다양한 구현체를 사용할 수 있도록 유연성을 제공합니다.
📌 언리얼 C++ 예제
UINTERFACE(MinimalAPI)
class UEngineInterface : public UInterface {
GENERATED_BODY()
};
class IEngineInterface {
GENERATED_BODY()
public:
virtual void Start() = 0;
};
UCLASS()
class APetrolEngine : public AActor, public IEngineInterface {
GENERATED_BODY()
public:
virtual void Start() override {
UE_LOG(LogTemp, Log, TEXT("Petrol Engine started"));
}
};
- ✅ 장점 : 다양한 엔진 타입을 쉽게 교체할 수 있습니다.
- ❌ 단점 : 인터페이스를 별도로 정의해야 하므로 코드가 복잡해질 수 있습니다.
의존성 주입 프레임워크
프레임워크 | 설명 |
Spring Framework | Java 기반의 DI 프레임워크 |
Google Guice | 애너테이션 기반 Java DI 프레임워크 |
Dagger | Android용 DI 프레임워크 |
Unreal Engine | TWeakObjectPtr, TSharedPtr, UInterface 등을 활용 |
정리
의존성 주입(DI)은 코드의 결합도를 낮추고 유지보수성과 테스트 용이성을 높이는 중요한 설계 패턴 입니다.
'Software Engineering > Software Development Principles' 카테고리의 다른 글
Refactoring(리팩토링) (0) | 2025.03.03 |
---|---|
Code Smell (0) | 2025.03.03 |
Compiler (0) | 2025.03.03 |