Dependency Injection, IoC, DIP, IoC container정리
Laravel 프레임워크를 통해 처음 Dependency Injection
(이하 DI로 명칭 통일) 개념을 접했습니다. 단순히 프레임워크가 자동으로 의존성을 주입한다고 알고 있었는데 최근 DI에 대해 잘 모른다는 생각이 들어 여기에 정리합니다.
DI를 찾아보니 Inversion of Control
(IoC)와 같이 알고 있는 개념도 나오고 Dependency Inversion Principle
(DIP)과 IoC Container
라는 생소한 개념도 접했습니다.
IoC, DIP, DI, IoC container
를 설명하기 앞서 각각이 무엇인지 아래 그림을 통해 확인하겠습니다.
IoC
와 DIP
는 principle
입니다. 특정한 원리에 대한 guideline을 제공하지만 디테일한 구현 방법을 제공하진 않습니다. DI
는 Design Pattern
이고 IoC Container
는 프레임워크입니다. 아래에서 각각에 대해 상세히 보겠습니다.
(추후 예제에 사용할 언어는 TypeScript
입니다.)
Inversion of Control (IoC)
IoC는 객체 간 결합도를 줄이기 위해 제어를 역전시키는 개념입니다. 여기서 제어란 프로그램의 흐름, 객체의 생성, 의존성을 가진 객체의 생성과 binding을 포함합니다.
프로그램의 흐름
이 역전되는 case로 예를 들어 보겠습니다. 아래에 C언어로 이름을 입력받는 코드가 있습니다.
void main()
{
char firstName[10], lastName[10];
scanf("%s", firstName);
scanf("%s", lastName); doSomething()
}
위 코드에서 제어
는 프로그램이 가지고
있습니다. main 함수가 실행되면서 입력과 그 후에 대한 제어를 프로그램이 가지고 있습니다.
위 코드를 event기반의 GUI로 아래와 같이 만들었다고 가정합니다.
이 경우 제어
는 프로그램이 아닌 사용자가 가지고
있습니다. 사용자가 이름을 입력 후 Save를 눌러야만 프로그램이 동작하기 때문에 제어가 역전
되었다고 할 수 있습니다.
다른 예로, 의존성 있는 객체의 생성
의 역전에 대해 알아보겠습니다. 실제 DB에 쿼리를 수행하는 DataAccess
클래스와 그 데이터를 받아 다루는 CustomerBusinessLogic
클래스가 있다고 가정합니다.
코드를 보면 알 수 있듯, CustomerBusinessLogic
생성자에서 직접DataAccess
객체를 생성하기 때문에 CustomerBusinessLogic
가 DataAccess
에 대해 의존성이 있는 것을 알 수 있습니다.
위 코드의 문제는 CustomerBusinessLogic
가 DataAccess
객체를 포함하고 있기 때문에 두 객체간 결합도가 높습니다. 결합도가 높기 때문에 발생할 수 있는 문제들로는,
DataAccess
가 변경될 경우CustomerBusinessLogic
이 변경 될 수 있습니다.- 다른 DB나 Web service로 부터 데이터를 추가적으로 가져올 경우
CustomerBusinessLogic
이 변경 될 수 있습니다. DataAccess
를 new 키워드로 생성하기 때문에 클래스 이름이 바뀌면 전체 소스에서DataAccess
를 찾아 변경해야 합니다.
객체 간 결합도를 느슨하게 하여 위 문제들을 해결하기 위한 방법으로 IoC
와 DIP
를 같이 사용할 수 있습니다. IoC를 구현하기 위한 다양한 디자인 패턴들이 있습니다.
여기서 Factory 패턴을 사용하여 IoC를 구현하도록 하겠습니다.
변경된 코드는 new 키워드
를 사용해 직접 객체를 생성하는 대신 DataAccessFactory
를 통해 객체를 받았습니다. 따라서 DataAccess
객체의 생성이 CustomerBusinessLogic
에서 DataAccessFactory
로 역전 된 것을 알 수 있습니다.
위 코드는 객체간 결합도를 느슨하게 하기 위한 첫 걸음으로 IoC를 구현한 것입니다. 위에서 언급했듯이 객체간 느슨한 결합도를 위해 IoC와 함께 DIP, DI를 같이 사용해야 합니다.
Dependency Inversion Principle(DIP)
위에서 Factory 패턴을 사용하여 IoC를 구현하여 객체간 낮은 결합도를 위한 첫 걸음에 진입했습니다. 이제 DIP에 대해 알아보겠습니다.
DIP는 Robert Martin
(Uncle BoB)에 의해 소개된 SOLID
원칙 중 하나입니다. DIP는 아래 2가지 원칙을 따릅니다.
- 상위 모듈은 하위 모듈에 종속되면 안되고 둘 다
추상화
에 의존해야 합니다. - 추상화는 세부사항에 의존하면 안 되고, 세부사항은 추상화에 의존해야 합니다.
위 원칙을 이해하기 위해 IoC에서 소개한 예제를 다시 보겠습니다.
위 코드는 IoC의 예제를 그대로 가져온 것으로 Factory 패턴을 사용하여 IoC를 구현하였습니다. 문제는 CustomerBusinessLogic
이 구체적인 DataAccess
객체를 사용하고 있기 때문에 아직 결합도가 강하다 할 수 있습니다.
따라서 DIP를 사용해 객체간 결합도를 낮춰보도록 하겠습니다. DIP 원칙
중 첫 번째
는 상위 모듈은 하위 모듈에 종속되면 안되고 둘 다 추상화에 의존해야 합니다.
그렇다면 어떤 모듈이 상위 모듈이고 하위 모듈인지 먼저 판단하겠습니다. 예제에서 CustomerBusinessLogic
이 DataAccess
에 의존성이 있습니다. 이 때 상위 모듈은 CustomerBusinessLogic
, DataAccess
는 하위 모듈이 됩니다.
따라서 CustomerBusinessLogic
은 구체적인 DataAccess
에 의존성을 가지면 안되고 둘 다 추상화에 의존해야 합니다.
프로그래밍 세계에서 추상화란 구체적이지 않은 interface나 abstract-class를 의미합니다. 따라서 두 객체 모두 interface나 abstract-class에 의존해야 합니다.
그럼 DIP를 만족하는 코드로 변경하도록 하겠습니다. CustomerBusinessLogic
은 DataAccess
의 GetCustomerName
메소드를 사용합니다. 따라서 이 메소드를아래와 같이 interface
로 선언합니다.
interface ICustomerDataAccess {
getCustomerName(id: number): string;
}
기존 DataAccess
대신 ICustomerDataAccess
인터페이스를 구현하는 CustomerDataAccess
를 아래와 같이 작성합니다.
class CustomerDataAccess implements ICustomerDataAccess {
public getCustomerName(id: number): string {
return "Dummy Customer Name";
}
}
DataAccessFactory
역시 구체적인 DataAccess
대신 ICustomerDataAccess
를 리턴하도록 아래와 같이 변경합니다.
class DataAccessFactory
{
public static getDataAccessObj(): ICustomerDataAccess
{
return new CustomerDataAccess();
}
}
마지막으로 CustomerBusinessLogic
역시 구체적인 DataAccess
대신 ICustomerDataAccess
를 사용하도록 아래와 같이 변경합니다.
class CustomerBusinessLogic {
custDataAccess: ICustomerDataAccess ;
constructor() {
this.custDataAccess = DataAccessFactory.getDataAccessObj();
}
public getCustomerName() {
const id = 5;
return this.custDataAccess.getCustomerName(id);
}
}
이렇게 만든 코드는 DIP 원칙에 따라 상위 모듈(CustomerBusinessLogic
) 과 하위 모듈(CustomerDataAccess
) 모두 추상화에 의존적이며 또한 추상화(ICustomerDataAccess
)는 세부사항(CustomerDataAccess
)에 의존적이지 않지만 세부사항은 추상화에 의존적입니다.
아래는 DIP를 적용한 전체 코드입니다.
위 코드에서 DIP의 장점은 CustomerBusinessLogic
이 구체적인 DataAccess
대신 interface
를 구현함으로써 CustomerBusinessLogic
,CustomerDataAccess
두 객체의 결합도가 낮다는것 입니다.
아직 위 코드에는 CustomerBusinessLogic
이 ICustomerDataAccess
를 얻기 위해 factory 클래스를 사용하는 단점이 있습니다. 즉, CustomerBusinessLogic
내부에서 ICustomerDataAccess
를 구현하는 다른 객체를 사용하려면 CustomerBusinessLogic
을 변경해야 합니다. 이때 DI를 통해 이를 해결 할 수 있습니다.
Dependency Injection(DI)
DI는 IoC를 구현하기 위한 디자인 패턴입니다. DI는 의존성 있는 객체의 생성을 class 외부에서 수행 후 다양한 방법으로 의존성 있는 객체를 class에게 제공합니다.
DI에선 3가지 유형의 클래스가 존재하는데 각 관계는 아래와 같습니다.
- Client: Service Class에 의존적인 class
- Service: Client Class에 제공되는 class
- Injector: Service Class를 Client에게 주입하는 class
이 관계는 아래와 같이 표현 할 수 있습니다.
알 수 있듯이, Injector class가 Service 객체를 생성하여 Client에게 주입합니다. DI로 의존성을 주입하는 방법에는 크게 3가지 방법이 있습니다.
- Constructor Injection: Injector가 Client의 생성자에 의존성 주입
- Property Injection: Injector가 Client의 public property에 의존성 주입
- Method Injection: Injector가 Client의 method에 의존성 주입
DIP가 적용된 예제의 문제는 CustomerBusinessLogic
내부에서 DataAccessFactory
를 사용하기 때문에 추후 변경, 확장 시 CustomerBusinessLogic
이 변경될 수 있다는 점이였습니다.
이 때 DI를 사용하여 의존성을 생성자, property, method에 주입하여 이 문제를 해결할 수 있습니다. 그럼 코드를 통한 예제를 보겠습니다.
Constructor Injection
CustomerBusinessLogic
생성자에서 ICustomerDataAccess
타입의 parameter를 받고 있습니다. 호출하는 CustomerInjector
에서 CustomerDataAccess
객체를 생성하여 주입하고 있습니다. 이 때 CustomerBusinessLogic
내부에서 CustomerDataAccess
객체를 new 키워드나 factory 패턴으로 생성할 필요가 없기 때문에 결합도가 훨씬 낮아졌습니다.
Property Injection
CustomerBusinessLogic
이 dataAccess property를 가지고 있고 주입하는 CustomerInjector
에서 property에 의존성을 주입하고 있습니다.
Method Injection
CustomerBusinessLogic
이 SetDependency()
메소드를 포함한 IDataAccessDependency
인터페이스를 구현하고 있습니다. 이를 통해 CustomerInjector
에서 CustomerDataAccess
객체를 생성하여 SetDependency()
메소드에 주입 할 수 있습니다.
이렇게 DI를 사용하여 객체의 결합도를 느슨하게 만들 수 있었습니다. 헌데 실무에선 수 많은 객체들의 의존성을 매번 이렇게 해결할 순 없습니다. 이를 위해 IoC Container
가 등장합니다.
IoC Container(= DI Container)
IoC Container
는 자동으로 의존성을 주입하기 위한 프레임워크
입니다. 이 프레임워크는 객체의 생성, 생명 주기, 의존성 있는 객체를 다른 객체에게 주입하는 역할을 합니다.
IoC Container
는 특정 클래스의 객체를 생성하고, 이 객체에 필요한 모든 의존성 있는 객체들을 생성자, property, method를 통해 주입할 수 있습니다. 즉, 우리가 의존성 주입을 수동으로 할 필요가 없다는 것입니다.
정리하며
단순히 Dependency Injection
이라 함은, IoC를 구현하기 위한 Design 패턴으로 제 3의 객체(Injector 역할)가 객체에게 의존성을 주입하는 것입니다.
또한 우리가 흔히 알고 있는 Framework(Spring, Laravel..)
에서 Dependency Injection
이라 함은 최소한 DI + IoC Container
라고 할 수 있습니다. 아마 일반적으로 DI + DIP + IoC Container
일 것이라 생각하지만.. 모든 프레임워크가 DI를 구현하기 위해 항상 DIP 원리가 사용되는지에 대해 확신은 없습니다.