삽질의 현장/- TypeScript

TypeScript로 DI 구현해보기

shovelman 2019. 10. 12. 15:19

TypeScript 공부 중 DI에 대한 좋은 자료가 있어 번역하며 정리해본다.
참고: Dependency Injection in TypeScript

성숙한 프레임워크는 일종의 의존성 주입(DI)를 구현할 수 있다.
DI는 한 개체가 다른 개체의 종족성을 제공하는 기술이다.

    class Foo {}

    class Bar {
      foo: Foo;
      constructor() {
        this.foo = new Foo();
      }
    }

    class Foobar {
      foo: Foo;
      bar: Bar;

      constructor() {
        this.foo = new Foo();
        this.bar = new Bar();
      }
    }

이것은 클래스 간에 직접적이고 교환 불가능한 종속성을 갖게 된다.
코드가 복잡해지고 구성 요소의 재사용성이 어려워져 테스트도 어려울 것이다.

DI는 종속성을 주입한다.

    class Foo {}
    class Bar {
      constructor(foo: Foo){}
    }
    class Foobar {
      constructor(foo: Foo, bar: Bar) {}
    }

Foobar 인스턴스를 얻으려면 다음과 같이 구성해야한다.

    const foobar = new Foobar(new Foo(), new Bar(new Foo()));

객체 생성을 담당하는 Injector를 사용하면더 간단하게 만들 수 있다.

    const foobar = Injector.resolve<Foobar>(Foobar); // 주입된 모든 의존성을 가진 Foobar의 인스턴스를 반환

Dependency injection in TypeScript

필요한 모든 의존성을 주입하여 인스턴스를 해결할 수 있는 자체 인젝터 클래스를 구현해보자.
이를 위해 서비스를 정의하는 @Service 데코레이터와 인스턴스를 해결할 실제 인젝터를 구현하자.

Reflection and decorators

reflection-metadata 패키지를 사용하여 런타임에 리플랙션 기능을 사용할 수 있다.
이 패키지를 사용하면 클래스 구현 방법에 대한 정보를 얻을 수 있다.

    const Service = () : ClassDecorator => {
      return target => {
        console.log(Reflect.getMetadata('design:paramtypes', target));
      };
    };

    class Bar {}

    @Service()
    class Foo {
      constructor(bar: Bar, baz: string) {}
    }

결과 값

    [ [Function: Bar], [Function: String] ]

The type of target

클래스는 사실 함수이다. 슈가 코딩 되어있을 뿐.

    class Foo {
      constructor() {}
      bar() {}
    }

변환시

    var Foo = /** @class */ (function() {
      function Foo() {}
      Foo.prototype.bar = function() {}
    }());

실제 인스턴스를 다룰 때 어떤 타입인지 확인할 수 있는 유형이 필요하다.

    interface Type<T> {
      new(...args: any[]): T;
    }

Type는 객체가 어떤 인스턴스인지 즉, new로 호출할 때 무엇을 얻을 수 있는지 알려준다.

    const Service = (): ClassDecorator => {
      return target => {};
    }

객체의 타입을 알기 위해 아래와 같이 표현할 수 있다.

    export type GenericClassDecorator<T> = (target: T) => void;

Interfaces are gone after compilation

인터페이스는 자바스크립트의 것이 아니다. 따라서 타입스크립트에서는 인터페이스를 DI 할 수 없어야 한다.

    interface LoggerInterface {
      write(message: string);
    }

    class Server {
      constructor(logger: LoggerInterface) {
        this.logger.write("Service called");
      }
    }

인터페이스가 런타임에 없어지기 때문에 Injector가 여기에 무엇을 주입해야하는지 알 수 있는 방법이 없다.
인터페이스 대신 실제 클래스를 항상 입력해야하는 것을 의미한다.
물론, 해결 방법이 있다. 인터페이스 비스무리한 형식으로 ...

    interface LoggerInterface {
      kind: 'logger';
    }

    class FileLogger implements LoggerInterface {
      kind: 'logger';
    }

Circular dependencies causes trouble

    @Service()
    class Bar {
      constructor(foo: Foo) {}
    }

    @Service()
    class Foo {
      constructor(bar: Bar) {}
    }

레퍼런스 에러가 발생될 것이다. (ReferenceError: Foo is not defined)
타입스크립트에서 Bar를 얻는 시점에서 Foo를 모르기 때문이다.

Implementing our very own Injector

이제 기본적인 Injector class를 구현해보자.
'The @Service decorator'

    const Service = (): GenericClassDecorator<Type<object>> => {
      return (target: Type<Object>) => {} // target으로 무언가를 할 것.
    }

'The Injector'
인젝터는 인스턴스의 요청을 분석할 수 있다. 인스턴스를 저장하는 것과 같은 추가 기능(공유 인스턴스)이 있을 수 있지만, 우선 단순하게 구현.

    export const Injector = new class { // Injector implementation };

클래스 형식으로 구현하지 않은 이유는 인젝터는 싱글턴으로 구현할 것이기 때문이다.
동일한 인젝터가 아니라면, 매번 새로운 인젝터의 인스턴스를 가질 수 있고, 등록한 서비스가 없을 수 있다.

    export const Injector = new class {
      resolve<T>(target: Type<any>): T {
        let token = Reflect.getMetadata('design:paramtypes', target) || [],
       injections = tokens.map(token => Injector.resolve<any>(token));

       return new target(...injections);
      }
    }

구현한 인젝터는 이제 인스턴스를 해결할 수 있게 되었다.

이제 최초 구현한 코드를 적용해보자.

    @Service()
    class Foo {
      doFooStuff() {
        console.log('foo');
      }
    }

    @Service()
    class Bar {
      constructor(public foo: Foo) {
      }

      doBarStuff() {
        console.log('bar');
      }
    }

    @Service()
    class Foobar {
      constructor(public foo: Foo, public bar: Bar) {
      }
    }

    const foobar = Injector.resolve<Foobar>(Foobar);
    foobar.bar.doBarStuff();
    foobar.foo.doFooStuff();
    foobar.bar.foo.doFooStuff();

결과값

    bar
    foo
    foo

인젝터가 모든 의존성을 성공적으로 주입했다.