Dependency Injection Pattern
객체가 필요로 하는 다른 객체(의존성)를 직접 생성하지 않고, 외부(컨테이너, 프레임워크 등)에서 생성하여 전달받는 디자인 패턴.

"직접 하지 말고, 시키세요": 의존성 주입으로 탈출하는 결합의 감옥
우리는 흔히 무언가를 '직접' 하는 것이 가장 확실하고 안전하다고 믿곤 합니다. 요리사가 자신의 칼을 직접 갈고, 목수가 자신의 망치를 고집하는 것처럼 말이죠. 프로그래밍의 세계에서도 마찬가지였습니다. 클래스가 필요한 객체를 스스로 생성하고 관리하는 것은 오랫동안 '책임감 있는 설계'로 여겨졌습니다.
하지만 어느 날, 우리는 깨닫게 됩니다. 모든 것을 스스로 하려던 그 책임감이 오히려 거대한 '결합의 감옥'이 되어 우리를 가두고 있었다는 사실을요.
상태(State) 패턴으로 객체의 내면을 다스리고, 옵저버(Observer) 패턴으로 타인과의 소통 창구를 열어준 여러분에게, 이제는 그 관계의 '주도권'을 완전히 뒤바꿀 기술을 소개합니다. 바로 의존성 주입(Dependency Injection, DI) 입니다.
1. "내가 만들게"에서 "가져다줘"로: 발상의 전환
상상해 보세요. 여러분이 아주 근사한 레스토랑을 운영하고 있습니다. 그런데 스테이크를 굽기 위해 요리사가 직접 소를 키우고 도축까지 해야 한다면 어떨까요? 아마 스테이크 한 접시를 내놓기도 전에 레스토랑은 문을 닫아야 할 겁니다. 하지만 실제 레스토랑은 다릅니다. 고기는 정육점에서, 채소는 농장에서 '공급'받습니다. 요리사는 그저 주어진 재료로 요리에만 집중하면 됩니다.
C++ 코드로 돌아와 볼까요? 우리가 흔히 쓰는 new 키워드는 아주 강력하지만, 동시에 아주 위험한 양날의 검입니다. 클래스 내부에서 다른 객체를 직접 생성하는 순간, 두 객체는 운명공동체가 되어버리거든요. 자원을 마구 잡아먹는 무거운 객체가 코드 곳곳에 단단히 박혀(Hard-coded) 있다고 생각해보세요. 코드를 수정하거나 테스트하는 일은 마치 거대한 콘크리트 건물을 망치 하나로 부수는 것처럼 고통스러운 작업이 될 거예요.
2. '결합의 저주'를 푸는 열쇠: C++ 실전 코드
의존성 주입은 이 문제를 아주 우아하게 해결합니다. 객체가 스스로 필요한 것을 만들지 않고, 외부에서 '주입'받도록 구조를 바꾸는 것이죠. 특히 모던 C++의 std::unique_ptr은 소유권을 명확히 하면서도 자원 누수를 방지해주는 가장 든든한 조력자입니다.

#include <iostream>
#include <memory>
#include <string>
/**
* 1. 추상화의 단계 (Interface)
* 요리사가 필요로 하는 '식재료(Ingredient)'의 역할을 정의합니다.
*/
class Ingredient {
public:
virtual ~Ingredient() = default;
virtual std::string getName() const = 0;
};
/**
* 2. 구체적인 구현체들 (Concrete Classes)
*/
class WagyuBeef : public Ingredient {
public:
std::string getName() const override { return "최상급 와규 소고기"; }
};
class OrganicTomato : public Ingredient {
public:
std::string getName() const override { return "유기농 토마토"; }
};
/**
* 3. 의존성 주입이 일어나는 핵심 클래스 (The Chef)
* Chef는 스스로 재료를 생성하지 않고 생성자를 통해 '주입'받습니다.
*/
class Chef {
private:
std::unique_ptr<Ingredient> ingredient;
public:
// 생성자 주입 (Constructor Injection)
// std::move를 사용하여 외부 자원의 소유권을 안전하게 이전받습니다.
Chef(std::unique_ptr<Ingredient> ing) : ingredient(std::move(ing)) {
if (!ingredient) {
std::cerr << "경고: 요리사에게 재료가 전달되지 않았습니다!" << std::endl;
}
}
void cook() {
if (ingredient) {
std::cout << "요리사가 " << ingredient->getName() << "(으)로 명품 요리를 시작합니다." << std::endl;
}
}
};
/**
* 4. 제어의 역전 (Main / IoC 지점)
*/
int main() {
// 시나리오 A: 와규 스테이크 주입
auto premiumBeef = std::make_unique<WagyuBeef>();
Chef gordon(std::move(premiumBeef));
gordon.cook();
// 시나리오 B: 토마토 샐러드 주입 (Chef 클래스 수정 불필요)
auto tomato = std::make_unique<OrganicTomato>();
Chef jamie(std::move(tomato));
jamie.cook();
return 0;
}
3. 소유권의 이전과 유연함의 획득
위 코드에서 가장 눈여겨보아야 할 부분은 Chef 클래스의 내부입니다. Chef는 자신이 WagyuBeef를 쓰는지 OrganicTomato를 쓰는지 전혀 모릅니다. 그저 Ingredient라는 가면을 쓴 무언가가 들어오기만을 기다릴 뿐이죠.
std::move의 미학: 생성자에서 std::move(ing)를 사용하는 순간, 재료에 대한 주도권은 외부에서 요리사 내부로 완전히 넘어옵니다. 이는 객체의 생명주기를 명확하게 관리할 수 있게 해주는 모던 C++의 장점입니다.
테스트의 용이성: 만약 실제 와규가 너무 비싸서 테스트하기 힘들다면, 우리는 아주 가벼운 MockIngredient를 만들어 주입하기만 하면 됩니다. 무거운 자원을 가짜 객체로 대체하는 이 기술은 대규모 시스템 개발에서 생존을 위한 필수 전략이 되곤 합니다.
4. 유연함이 주는 자유, 그리고 책임
의존성 주입을 받아들인다는 것은, 클래스가 누리던 '창조의 권력'을 포기하고 외부 시스템에 그 권한을 양도하는 것을 의미합니다. 이건 마치 산업혁명 시기, 가내수공업자가 공장의 부품 생산 방식에 적응하며 느꼈던 당혹감과 비슷할지도 몰라요. 내 손을 떠난 통제권이 불안할 수도 있겠죠.
하지만 이 '위대한 디커플링(The Great Decoupling)'을 통해 여러분은 비로소 자유로워집니다. 데이터베이스가 바뀌든, 실제 네트워크 대신 가짜 객체를 끼워 넣든, 우리의 핵심 로직은 흔들리지 않고 고고하게 유지될 수 있으니까요.
결국 중요한 건 '관계의 거리'
State와 Observer를 지나 Injection의 문턱에 서 있는 지금, 여러분의 코드는 이전보다 훨씬 유연하고 숨쉬기 편해졌을 거예요. 하지만 잊지 마세요. 모든 기술이 그렇듯 DI 역시 만능 열쇠는 아닙니다. 과도한 추상화는 때로 코드의 흐름을 파악하기 어렵게 만드는 '안개'가 되기도 하거든요.
과연 여러분은 어디까지 유연해져야 할까요? 그리고 우리가 만든 이 느슨한 연결고리들이 미래의 변화를 온전히 감담해낼 수 있을까요? 정답은 아마 여러분이 마주할 다음 프로젝트의 성격에 따라 달라질 거예요.
이제 이 강력한 도구를 여러분이 손에 쥐고 고민해 볼 차례입니다.