삽질의 현장/- TypeScript

Angular 를 통해 바라본 의존성 주입 이해

shovelman 2019. 10. 12. 15:29

서비스란,

애플리케이션 전역의 관심사를 구현.

의존성 주입 (Dependency Injection)

의존성의 인스턴스를 생성하는 코드와 사용하는 코드가 컴포넌트 내 같이 존재한다면

컴포넌트와 의존성이 긴밀한 결합(Tight Coupling)을 하고 있다고 할 수 있다.


긴밀한 결합을 느슨한 결합(Loose Coupling)으로 의존 관계를 갖게하려면
의존성 인스턴스를 사용하는 코드는 인스턴스 생성에 관여하지 않고

단지 필요한 인스턴스를 요구하기만 하고 외부 환경에서 요구된 인스턴스를 생성하여 전달하면 된다.

 

그렇게 한다면 의존성 인스턴스를 사용하는 코드는 인스턴스를 생성하는 방법을 알 필요가 없어지고,
의존성이 변경된다 하더라도 인스턴스를 전달하는 외부 환경이 변경된 의존성의 인스턴스를 전달하기만 한다면

코드의 수정없이 변경된 의존성 인스턴스를 사용할 수 있다.


의존 관계에 있는 두 개의 객체가 서로 상호 작용을 하기는 하지만 서로에 대해 잘 알지 못한다는 것을 의미하며,
서로에게 주는 영향을 최소화하여 변경에 유연하게 대처할 수 있는 가능성을 확보할 수 있다.

    // A와 B는 의존 관계다. A가 B에 의존하고 있다.
    class A {
      dependency: B;
      constructor() {
        // 의존성 인스턴스 생성
        this.dependency = new B()
      }

      foo() { this.dependency.bar(); }
    }

    class B {
      bar() { console.log('bar'); }
    }

    const a = new A();
    a.foo();

위 코드의 A는 B에 의존하고 있다. A가 B에 의존하는 의존 관계에 있을 때, B의 기능이 변경되면 A는 영향을 받는다.
즉, A가 B의 메소드를 사용한다면 B의 메소드 형식이 변경되었을 때 A도 수정되어야 한다.
그리고 A의 constructor에서 B를 직접 생성하고 있기 땐에 B의 인스턴스의 생성 방법을 알고 있어야한다.

인스턴스의 생성 방식은 다양하다.
예를 들어, new 키워드를 사용할 수 있고, 애플리케이션 전역에서 단일 인스턴스를 공유하는 싱글턴 패턴의 경우

getInstance()와 같은 함수를 호출할 수 있으며, 팩토리 패턴의 경우 createGrettingService()와 같은 팩토리 함수를 사용할 수 있다.

 

위 코드를 의존성 주입(DI) 패턴을 사용하여 긴밀한 결합에서 느슨한 결합으로 전환해 보자.

    class A {
      constructor(private dependency: B) {}
      foo() {this.dependency.bar(); }
    }

    class B {
      bar() { console.log('bar'); }
    }

    // A의 외부 환경에서 의존성 인스턴스를 주입한다. 이때 의존성 인스턴스의 생성 방법을 알아야한다.
    const a = new A(new B());
    a.foo();

의존성 주입은 의존 관계를 긴밀한 결합에서 느슨한 결합으로 의존 관계를 전환하기 위해
구성요소 간의 의존 관계를 코드 내부가 아닌 외부의 설정 등을 통해 정의하는 디자인 패턴 중의 하나로서
구성 요소 간 결합도를 낮추고 재사용성을 높인다.

 

의존성 주입을 사용하면 컴포넌트가 직접 의존성의 인스턴스를 생성하는 것이 아니라,
컴포넌트는 단지 필요한 의존성을 요구하고 (ex. constructor 파라미터로 필요한 의존성을 선언),
프레임워크 / 라이브러리(type-di)가 제어권(Control)을 갖는 주체로 동작하여 요구된 의존성 인스턴스를 생성하여 전달한다.

이를 제어권의 역전(Inversion of Control, IoC)이라 한다.

 

서비스를 사용하는 구성요소는 더 이상 의존성 인스턴스의 생성에 대해 관여하지 않아도 된다.
프레임워크 / 라이브러리(type-di)가 서비스의 인스턴스를 생성하여 주입해주게 된다.
인스턴스를 어떻게 생성하는지는 프레임워크 / 라이브러리(type-di)가 알지 못하므로 이 정보는 알려줘야한다.
다시 말해 주입될 의존성 인스턴스의 생성 정보를 프레임워크 / 라이브러리(type-di)에 알려 주입을 지시하여야 한다.
이러한 인스턴스 생성 정보를 설정하여 의존성 인스턴스의 주입을 지시하는 것을 프로바이더(provider)라고 부른다.

 

프레임워크 / 라이브러리(type-di)는 주입할 의존 관계 객체의 생성 방법 정보대로 동작하여

의존 관계 객체의 인스턴스를 생성하고 주입한다.


컴포넌트는 의존 관계 객체의 생성 방법을 알 필요가 없고 인젝터가 생성하여

생성자의 인자로 주입한 인스턴스를 사용하기만 하면 된다.

 

주입을 요청할 때는 constructor의 파라미터에 주입된 인스턴스를 담을 변수의 이름과 주입 대상의 타입을 명시한다.

    constructor(private greetingService: GreetingService) {}

프레임워크 / 라이브러리(type-di)는 주입 요청된 인스턴스의 타입 GreetingService와 일치하는 프로바이더의 토큰을 검색한다.
검색이 성공하면 프로바이더의 useClass 프로퍼티에 지정된 클래스를 사용하여 인스턴스를 생성한다.
그리고 이 인스턴스를 greetingService 프로퍼티에 할당하여 주입한다.
constructor 파라미터에 접근 제한자를 선언하였으므로 greetingService는 컴포넌트 내 this에 의해 참조 가능한 클래스 프로퍼티이다.

인젝터(Injector)

의존성 주입 요청에 의해 주입되어야 할 인스턴스가 있다면 프레임워크 / 라이브러리(type-di)는 이 인스턴스의 주입을 인젝터에 요청한다.

인젝터는 컴포넌트와 모듈 레벨로 존재하며 의존성 주입 요청에 의해

프로바이더를 검색하고 인스턴스를 생성하여 의존성 인스턴스를 주입한다.

 

의존성 요청이 있을 때마다 매번 의존성 인스턴스를 생성하는 것은 아니다.
인젝터는 인스턴스의 풀(pool)인 컨테이너를 관리하고 있다.
인젝터는 의존성 주입 요청을 받으면 프로바이더를 참조하여 요청된 인스턴스가 컨테이너에 존재하는지 검색한다.
기존에 생성된 인스턴스는 프로바이더의 토큰을 키로 컨테이너에 저장되어 있다.
인스턴스를 검색할 때에는 토큰을 키로 인스턴스를 검색한다.

 

만약 주입 요청을 받은 인스턴스가 컨테이너에 존재하면 새롭게 인스턴스를 생성하는 것이 아니라
컨테이너에 이미 존재하는 인스턴스를 주입하고,

요청된 인스턴스가 컨테이너에 존재하지 않으면 프로바이더의 useClass 프로퍼티를 참조하여

인스턴스를 생성하고 토큰을 키로 컨테이너에 추가한 후, 이 인스턴스를 constructor에 주입한다.

 

서비스는 인젝터의 주입 범위 내에서 언제나 싱글턴이다. 그러나 컴포넌트의 인젝터는 독립적으로 동작한다.
프로바이더는 사용 방법에 따라 3가지 종류로 구분할 수 있다.

클래스 프로바이더

클래스 프로바이더는 가장 일반적인 프로바이더로서 클래스의 인스턴스를 의존성 주입하기 위한 설정을 등록한다.
providers 프로퍼티는 제공할 인스턴스의 클래스 리스트로 구성된 배열을 값으로 갖는다.

값 프로바이더

문자열이나 객체 리터럴과 같은 값을 의존성 주입하기 위한 설정을 등록한다.

팩토리 프로바이더

의존성 인스턴스를 생성할 때 어떤 로직을 거쳐야 한다면 팩토리 함수를 사용한다.
예를 들어 생성할 인스턴스를 조건에 따라 결정해야 하는 경우, 팩토리 함수를 사용한다.

참고: PoiemaWeb Angular Service