Dependency Injection, IoC, DIP, IoC container정리

Jeongkuk Seo
sjk5766
Published in
12 min readAug 4, 2020

Laravel 프레임워크를 통해 처음 Dependency Injection(이하 DI로 명칭 통일) 개념을 접했습니다. 단순히 프레임워크가 자동으로 의존성을 주입한다고 알고 있었는데 최근 DI에 대해 잘 모른다는 생각이 들어 여기에 정리합니다.

DI를 찾아보니 Inversion of Control(IoC)와 같이 알고 있는 개념도 나오고 Dependency Inversion Principle(DIP)과 IoC Container라는 생소한 개념도 접했습니다.

IoC, DIP, DI, IoC container를 설명하기 앞서 각각이 무엇인지 아래 그림을 통해 확인하겠습니다.

IoCDIPprinciple 입니다. 특정한 원리에 대한 guideline을 제공하지만 디테일한 구현 방법을 제공하진 않습니다. DIDesign 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 를 찾아 변경해야 합니다.

객체 간 결합도를 느슨하게 하여 위 문제들을 해결하기 위한 방법으로 IoCDIP를 같이 사용할 수 있습니다. 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 원칙첫 번째는 상위 모듈은 하위 모듈에 종속되면 안되고 둘 다 추상화에 의존해야 합니다.

그렇다면 어떤 모듈이 상위 모듈이고 하위 모듈인지 먼저 판단하겠습니다. 예제에서 CustomerBusinessLogicDataAccess에 의존성이 있습니다. 이 때 상위 모듈은 CustomerBusinessLogic , DataAccess는 하위 모듈이 됩니다.

따라서 CustomerBusinessLogic은 구체적인 DataAccess에 의존성을 가지면 안되고 둘 다 추상화에 의존해야 합니다.

프로그래밍 세계에서 추상화란 구체적이지 않은 interface나 abstract-class를 의미합니다. 따라서 두 객체 모두 interface나 abstract-class에 의존해야 합니다.

그럼 DIP를 만족하는 코드로 변경하도록 하겠습니다. CustomerBusinessLogicDataAccessGetCustomerName 메소드를 사용합니다. 따라서 이 메소드를아래와 같이 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 두 객체의 결합도가 낮다는것 입니다.

아직 위 코드에는 CustomerBusinessLogicICustomerDataAccess를 얻기 위해 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

CustomerBusinessLogicSetDependency() 메소드를 포함한 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 원리가 사용되는지에 대해 확신은 없습니다.

레퍼런스

--

--