의존성 주입(Dependency Injection, DI) 은 객체 지향 프로그래밍에서 객체가 자신이 필요로 하는 의존성을 직접 생성하지 않고, 외부에서 제공(주입)받는 설계 패턴이자 원칙입니다(Fowler, 2004). 여기서 의존성이란 한 객체가 다른 객체의 기능이나 데이터를 필요로 할 때 그 객체를 의미합니다. DI는 이러한 의존성을 객체 내부에서 직접 생성하지 않고, 외부에서 전달받음으로써 객체 간의 결합도를 낮추고 코드의 유연성과 재사용성을 향상시킵니다.
DI는 다양한 프로그래밍 언어와 프레임워크에서 널리 사용되며, 객체 지향 프로그래밍의 핵심 원칙 중 하나인 단일 책임 원칙(Single Responsibility Principle) 을 준수하도록 돕습니다. 또한 DI는 제어의 역전(Inversion of Control, IoC) 원칙을 구현하는 한 가지 방법으로, 객체 생성과 의존성 관리를 애플리케이션이 아닌 외부 컨테이너나 프레임워크가 담당하게 합니다.
왜 사용하는가 (Why Use Dependency Injection)
결합도 감소 (Reduced Coupling): 객체가 구체적인 구현체가 아닌 인터페이스나 추상 클래스와 같은 추상화에 의존함으로써 클래스 간의 강한 결합도를 낮춥니다. 이는 코드 변경 시 영향 범위를 최소화하고, 모듈화와 재사용성을 향상시킵니다.
테스트 용이성 (Improved Testability): 의존성을 외부에서 주입받기 때문에, 테스트 시 모의 객체(mock objects)나 스텁(stubs)을 활용하여 단위 테스트를 쉽게 수행할 수 있습니다. 이는 테스트 주도 개발(Test-Driven Development, TDD) 을 실천하는 데 큰 도움이 됩니다.
유연성과 확장성 (Flexibility and Scalability): 런타임 시 의존성을 교체하거나 구성할 수 있어 애플리케이션의 유연성을 높입니다. 새로운 기능 추가나 변경 시 기존 코드를 최소한으로 수정하여 확장성을 제공합니다.
가독성 및 유지보수성 향상 (Improved Readability and Maintainability): 의존성이 명시적으로 드러나기 때문에, 코드의 가독성이 향상되고 유지보수가 쉬워집니다.
의존성을 직접 생성하는 경우
의존성을 주입하지 않는다는 것은 객체 내부에서 필요한 의존성을 직접 생성한다는 의미이며, 이는 강한 결합(Tight Coupling) 을 초래합니다. 이러한 경우 다음과 같은 문제가 발생합니다.
유연성 감소: 의존 대상의 구현체가 변경되면 이를 사용하는 모든 객체를 수정해야 합니다.
테스트의 어려움: 객체 내부에서 의존성을 생성하기 때문에, 단위 테스트 시 모의 객체를 주입하기 어렵습니다.
재사용성 저하: 특정 구현체에 종속되어 다른 컨텍스트에서 재사용하기 어렵습니다.
코드 중복 증가: 동일한 의존성을 여러 곳에서 생성하면 코드 중복이 발생합니다.
아래 코드는 의존성을 직접 생성하는 경우의 예시입니다.
publicclassUserService{privateUserRepository userRepository;publicUserService(){this.userRepository =newUserRepository();// 의존성을 직접 생성}publicvoidperformAction(){
userRepository.save();}}
위 코드에서 UserService는 UserRepository의 구체적인 구현체를 직접 생성하고 있습니다. 만약 UserRepository의 구현을 변경하거나 다른 구현체로 교체하려면 UserService 코드를 수정해야 합니다.
반면에 의존성을 주입받게 되면, UserService는 UserRepository의 구체적인 구현에 대해 알 필요가 없으며, 필요에 따라 다른 구현체를 주입할 수 있습니다.
어떻게 적용하는가 (How to Apply Dependency Injection)
생성자 주입(Constructor Injection): 생성자를 통해 필요한 의존성을 주입받는 방식입니다. 의존성이 반드시 필요한 경우에 사용하며, 객체의 불변성을 유지할 수 있습니다.
초점의 차이 때문이었습니다. 두 패턴 모두 "복잡성을 줄인다"는 공통 목표를 가진다고 생각할 수 있습니다. 그러나 DI와 퍼사드 패턴(Facade Pattern) 은 서로 다른 문제를 해결하는 패턴입니다.
퍼사드 패턴은복잡한 하위 시스템에 대한 간단한 인터페이스를 제공하여 사용자가 시스템의 내부 복잡성을 알 필요 없이 기능을 사용할 수 있게 합니다. DI(의존성 주입) 객체가 자신의 의존성을 외부에서 주입받는 방식을 정의하여 객체 간의 결합도를 낮추고, 유연성과 테스트 용이성을 향상시킵니다.
제가 혼동한 이유는 즉 두 패턴 모두 추상화와 결합도 줄이기에 중점을 두고 있기 때문입니다. 하지만, 퍼사드 패턴은 주로 인터페이스 단순화에, DI는 객체 간 결합도 줄이기와 테스트 용이성에 초점을 맞추고 있습니다.
각 전략의 본질을 알아봅시다. DI는 단순히 객체의 의존성을 주입하는 것이 아니라, 객체 간의 강한 결합을 느슨하게 만듭니다. 이를 통해 코드가 더 쉽게 수정되고, 유닛 테스트가 용이해집니다. 또한 시스템의 유연성을 높여 변경 사항에 유연하게 대응할 수 있게 합니다. 퍼사드의 역할: 퍼사드는 시스템의 내부 동작을 감추고 단순한 API를 제공하여 클라이언트가 복잡한 시스템을 이해할 필요 없이 쉽게 사용할 수 있게 합니다. 퍼사드가 직접적으로 의존성을 처리하는 것이 아니라, 하위 시스템의 복잡성을 감추는 데 중점을 둡니다.
즉 두 패턴의 목적과 적용 범위가 다르다는 점을 파악해야 합니다. 퍼사드는 복잡한 하위 시스템의 상호작용을 간소화하고, DI는 시스템을 더 유연하고 테스트 가능한 구조로 만드는 데 기여합니다.
음향 엔지니어 예시로 설명
퍼사드 패턴은 복잡한 음향 시스템을 관리하는 하나의 단순한 인터페이스로 비유할 수 있습니다. 예를 들어, 콘서트에서 믹싱 엔지니어는 다양한 장비(마이크, 스피커, 이펙터)를 일일이 조작하지 않고, 믹싱 콘솔이라는 하나의 통합된 인터페이스를 통해 전체 시스템을 제어합니다. 이는 퍼사드가 복잡한 시스템을 간단하게 조작할 수 있게 하는 역할과 유사합니다.
반면, DI(의존성 주입) 는 각 장비(의존성)를 외부에서 주입받아 사용하는 개념과 비슷합니다. 믹싱 엔지니어는 필요한 장비를 직접 제작하거나 연결하지 않고, 외부에서 제공되는 장비를 받아 시스템에 연결합니다. 이를 통해 장비를 교체하거나 업그레이드할 때 시스템 전체를 변경할 필요 없이 간단히 새로운 장비를 주입할 수 있습니다.
Characteristics and Considerations
제어의 역전(Inversion of Control, IoC):
프로그램의 제어 흐름을 개발자가 아닌 프레임워크나 컨테이너가 관리하는 원칙입니다.
DI는 IoC의 한 구현체로서, 객체 생성과 의존성 관리를 외부로 위임합니다.
이를 통해 객체는 자신의 핵심 로직에만 집중할 수 있습니다.
의존성 역전 원칙(Dependency Inversion Principle, DIP):
SOLID 원칙 중 하나로, 고수준 모듈이 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존해야 한다는 원칙입니다.
DI를 통해 구현체가 아닌 인터페이스나 추상 클래스에 의존함으로써 이 원칙을 준수할 수 있습니다.
이를 통해 시스템의 유연성과 확장성이 향상됩니다.
단일 책임 원칙(Single Responsibility Principle, SRP):
클래스는 하나의 책임만 가져야 하며, 변경 사유는 하나여야 합니다.
DI를 활용하여 객체의 생성과 로직을 분리함으로써 이 원칙을 지킬 수 있습니다.
객체는 자신의 책임에만 집중하고, 의존성 관리는 외부에 위임합니다.
개방-폐쇄 원칙(Open/Closed Principle, OCP):
소프트웨어 엔티티는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
DI를 통해 새로운 의존성을 주입함으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있습니다.
리스코프 치환 원칙(Liskov Substitution Principle, LSP):
프로그램의 객체는 그 하위 타입의 인스턴스로 대체할 수 있어야 합니다.
DI를 사용하여 인터페이스나 추상 클래스에 의존함으로써 이 원칙을 준수할 수 있습니다.
인터페이스 분리 원칙(Interface Segregation Principle, ISP):
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
DI를 통해 필요한 의존성만 주입받아 사용함으로써 이 원칙을 지킬 수 있습니다.
주의사항:
복잡성 증가: 작은 규모의 프로젝트에서 DI를 과도하게 사용하면 오히려 복잡성이 증가할 수 있습니다. 필요에 따라 적절히 적용해야 합니다.
학습 곡선: DI 컨테이너나 프레임워크의 사용법을 숙지해야 하므로 초기 학습 비용이 발생할 수 있습니다.
성능 고려: DI 컨테이너는 런타임 시 객체를 생성하고 주입하므로, 성능에 영향을 줄 수 있습니다. 그러나 일반적으로 이는 미미한 수준이며, 애플리케이션의 구조적 이점이 더 큽니다.
디버깅의 어려움: 의존성이 자동으로 주입되므로, 디버깅 시 객체 생성과정이 명확하지 않을 수 있습니다.
오버엔지니어링: 필요 이상으로 복잡한 구조를 도입하면 유지보수가 어려워질 수 있습니다.
DI in Various Languages and Frameworks
JavaScript/TypeScript:
InversifyJS, TSyringe 등의 라이브러리를 사용하여 DI를 구현할 수 있습니다.