자동차를 만드는데, 엔진과 바퀴, 차체를 모두 강력 접착제로 붙여버렸다고 상상해 보세요. 이제 타이어 하나를 교체하려고 해도, 차체 전체를 부수고 다시 만들어야 합니다. 끔찍하죠? 😱 놀랍게도, 많은 개발자들이 자신도 모르게 코드 세계에서 이런 '강력 접착제'를 사용하고 있습니다.
하나의 모듈(객체)이 다른 모듈을 직접 생성하고 사용하는 강한 결합(Tightly Coupled) 구조가 바로 그것입니다. 이런 코드는 수정이 어렵고, 테스트는 더더욱 힘듭니다. 이 '의존성 지옥'에서 우리를 구원해 줄 핵심 디자인 패턴이 바로 의존성 주입(Dependency Injection, DI) 입니다.
의존성 주입이란? 외부에서 부품을 조립하는 레고(LEGO) 방식
의존성 주입은 간단히 말해, 객체가 필요로 하는 다른 객체(의존성)를 내부에서 직접 만들지 않고, 외부에서 만들어서 넣어주는(주입) 방식입니다.
다시 자동차 비유로 돌아가 볼까요?
- 강한 결합 (직접 생성): 자동차(객체)가 스스로 "나는 '한국타이어'만 쓸 거야!"라고 결정하고, 공장 안에서 직접 한국타이어(의존 객체)를 만들어 장착합니다. 만약 '미쉐린타이어'로 바꾸고 싶다면? 자동차 설계도 자체를 뜯어고쳐야 합니다.
- 의존성 주입 (외부에서 주입): 자동차는 그저 "바퀴가 장착될 4개의 구멍"만 가지고 있습니다. 실제 어떤 타이어를 장착할지는 자동차를 조립하는 외부의 '조립공(DI 컨테이너, 혹은 팩토리)'이 결정합니다. 오늘은 한국타이어를, 내일은 미쉐린타이어를 얼마든지 갈아 끼울 수 있죠. 자동차는 자신에게 장착된 타이어가 어느 회사 제품인지 전혀 신경 쓰지 않습니다. 🔧
이처럼 의존성 주입은 객체 간의 관계를 느슨하게 만들어(느슨한 결합, Loosely Coupled), 코드를 훨씬 유연하고 재사용 가능하게 만듭니다.
코드로 보는 차이 (Before vs. After)
알림을 보내는 Notifier 클래스가 있고, 알림 방식으로는 EmailSender를 사용한다고 해봅시다.
👎 의존성 주입이 없는 코드 (강한 결합)
// 알림을 보내는 방식이 'EmailSender'로 고정되어 있다.
class EmailSender {
send(message) {
console.log(`이메일 발송: ${message}`);
}
}
class Notifier {
constructor() {
// Notifier가 직접 EmailSender를 생성하고 의존한다! (강력 접착제)
this.sender = new EmailSender();
}
sendNotification(message) {
this.sender.send(message);
}
}
const notifier = new Notifier();
notifier.sendNotification("안녕하세요!");
만약 여기서 알림 방식을 이메일이 아닌 SMS로 바꾸려면 Notifier 클래스의 내부 코드를 직접 수정해야만 합니다. 테스트하기도 까다롭죠.
👍 의존성 주입을 사용한 코드 (느슨한 결합)
// 다양한 알림 방식을 인터페이스로 약속한다.
class EmailSender {
send(message) { console.log(`이메일 발송: ${message}`); }
}
class SmsSender {
send(message) { console.log(`SMS 발송: ${message}`); }
}
class Notifier {
// 생성자를 통해 외부에서 'sender' 객체를 주입받는다!
constructor(sender) {
this.sender = sender;
}
sendNotification(message) {
this.sender.send(message);
}
}
// 외부(조립공)에서 어떤 부품을 쓸지 결정해서 넣어준다.
const emailNotifier = new Notifier(new EmailSender());
emailNotifier.sendNotification("안녕하세요!");
const smsNotifier = new Notifier(new SmsSender());
smsNotifier.sendNotification("반갑습니다!");
이제 Notifier는 자신이 사용하는 sender가 이메일 방식인지, SMS 방식인지 전혀 알 필요도, 신경 쓸 필요도 없습니다. 그저 send라는 메서드를 가지고 있다는 사실만 알면 되죠. 덕분에 코드는 훨씬 유연해졌고, 테스트 시에는 가짜 MockSender를 주입하여 쉽게 테스트할 수 있습니다.
마치며
의존성 주입(DI) 은 "객체 생성의 제어권을 외부로 넘긴다"는 제어의 역전(Inversion of Control, IoC) 원리를 구현하는 대표적인 방법입니다. 스프링 프레임워크(Spring Framework)나 앵귤러(Angular) 같은 수많은 현대적인 프레임워크들이 바로 이 DI를 핵심 원리로 삼고 있습니다.
처음에는 개념이 조금 어색할 수 있지만, "내부에서 만들지 말고, 밖에서 넣어준다"는 이 간단한 규칙 하나만으로도 여러분의 코드는 변화에 유연하고, 테스트하기 쉬우며, 재사용성이 높은 '레고 블록'처럼 변하게 될 것입니다. ✨