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
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
인젝터가 모든 의존성을 성공적으로 주입했다.
'삽질의 현장 > - TypeScript' 카테고리의 다른 글
TypeScript로 개발 할 때 유용한 라이브러리 (0) | 2019.10.12 |
---|---|
Angular 를 통해 바라본 의존성 주입 이해 (0) | 2019.10.12 |
TypeScript TypeORM Migration 정리 (0) | 2019.10.12 |
의존성 주입이란 무엇인가? (0) | 2019.10.12 |
@Decorator에 대하여 알아보자 (0) | 2019.10.12 |