의존성 주입이란?
의존성 주입(Dependency Injection, DI)은 객체가 직접 필요한 다른 객체를 생성하지 않고, 외부에서 만들어진 객체를 전달받아 사용하는 설계 방식입니다. 여기서 ‘의존성’이란 어떤 클래스가 동작하기 위해 필요로 하는 다른 클래스나 모듈을 의미합니다. 즉 A라는 클래스가 B라는 클래스의 기능을 사용해야 한다면, A는 B에 의존하고 있는 것입니다.
보통 이런 의존성은 클래스 내부에서 직접 new 키워드로 생성하는 경우가 많습니다. 하지만 이렇게 되면 A 클래스는 B 클래스의 구체적인 구현에 강하게 묶이게 되고, B가 바뀌면 A도 함께 수정해야 하는 문제가 생깁니다. DI는 이 생성 책임을 A 클래스 밖으로 빼내어, 외부(주로 프레임워크나 컨테이너)에서 B의 인스턴스를 만들어 A에게 ‘주입’해주는 방식으로 이 문제를 해결합니다.
동작 원리와 예시
비유를 들어보면 이해가 쉽습니다. 자동차를 만들 때 엔진을 자동차 공장 내부에서 직접 만들지 않고, 엔진 전문 제조사에서 완성된 엔진을 받아 조립하는 것과 같습니다. 자동차 공장은 ‘어떤 엔진이 들어올지’만 규격(인터페이스)으로 정해두고, 실제 엔진 생산은 외부에 맡기는 것입니다.
코드로 보면 다음과 같은 차이가 있습니다.
- DI를 사용하지 않는 경우:
class Car { engine = new GasEngine(); }이렇게 작성하면 Car는 항상 GasEngine에만 의존하게 됩니다. - DI를 사용하는 경우:
class Car { constructor(engine) { this.engine = engine; } }이렇게 생성자를 통해 엔진을 외부에서 전달받으면, GasEngine이든 ElectricEngine이든 상황에 맞게 갈아 끼울 수 있습니다.
이때 외부에서 어떤 객체를 생성해서 넘겨줄지 관리하는 역할을 ‘DI 컨테이너’ 또는 ‘IoC(Inversion of Control) 컨테이너’라고 부릅니다. 스프링(Spring)의 ApplicationContext나 앵귤러(Angular)의 Injector가 대표적인 예입니다.
실무에서 왜 사용하는가
DI를 사용하면 크게 세 가지 이점이 있습니다.
- 테스트 용이성: 실제 DB에 연결하는 객체 대신 가짜(Mock) 객체를 주입할 수 있어 단위 테스트가 쉬워집니다.
- 유연한 확장: 인터페이스 기반으로 설계하면 구현체를 교체해도 사용하는 쪽 코드는 수정할 필요가 없습니다.
- 결합도 감소: 클래스 간 강한 결합을 줄여 유지보수와 협업이 수월해집니다.
결국 DI는 ‘누가 객체를 만드느냐’의 책임을 사용하는 쪽에서 외부로 옮기는 설계 원칙입니다. 처음에는 코드가 복잡해 보일 수 있지만, 프로젝트 규모가 커질수록 변경에 유연하게 대응할 수 있는 구조를 만들어주기 때문에 스프링을 비롯한 많은 프레임워크가 DI를 핵심 원리로 채택하고 있습니다.