타입스크립트 사용 시 IOC 컨테이너가 인터페이스 기반으로 의존성을 주입할 수 있도록 해보자.
Typescript DI의 한계
타입스크립트의 Type, Interface는 JS로 변환되면 삭제된다
- 런타임에 정보를 줄 수없다.
- 해당 개념이 자바스크립트에 없기 때문
- NestJS / TypeGraphQL 같은 프레임워크의 예제는 구체적인 Class를 주입한다
// TypegraphQL 예제: AddRecipeInput라는 구체적인 클래스를 직접 명시함
// 만약 Java였으면 IADDRecepeInput이 가능
@Resolver()
class RecipeResolver {
// ...
@Mutation()
addRecipe(@Arg("data") newRecipeData: AddRecipeInput, @Ctx() ctx: Context): Recipe {
// sample implementation
const recipe = RecipesUtils.create(newRecipeData, ctx.user);
this.recipesCollection.push(recipe);
return recipe;
}
}
해결 방법 : interface를 string/symbol로 대체한다.
ioc컨테이너가 symbol 키맵을 이용하여 class 정보를 관리하는 것으로 생각된다.
IOC 컨테이너를 사용하려면 한 인터페이스를 구현하는 클래스는 하나여야 하고(2개 이상이면 주입 대상을 알 수 없음),
interface를 사용하는 이유는 다형성 지원 / 사용처를 바꾸지 않고 주입 대상을 바꾸는 것이다.
즉, SOLID원칙과 부합한다.
(참고 : js는 다형성을 factory method로 구현한다.)
1. Symbol(string)에 클래스 정보를 바인딩한다.
인터페이스를 구현하는 injectable한 클래스는 단 하나여야 한다.
DI를 쓰는 이유는 개발자가 코드에서 직접 객체의 라이프 사이클을 관리하는 것을 지양하고자 하기 위함이다.
Interface를 사용하는 이유는 사용하고자 하는 곳의 코드를 수정하지 않고, 구체적인 클래스를 교체하기 위함이다(/w @injectable)
inversify.js 예시
// inversify 라이브러리를 이용한 DI 구현
// 컴파일 타임에도 남아있는 상수에 바인딩한다.
const Types = {
IDough: Symbol.for('IDough'),
IFlour: Symbol.for('IFlour'),
ISalt: Symbol.for('ISalt'),
IWater: Symbol.for('IWater'),
IYeast: Symbol.for('IYeast'),
};
interface IYeast {}
interface IWater {}
interface ISalt {}
interface IFlour {}
@injectable()
class Water implements IWater {}
@injectable()
class Yeast implements IYeast {}
@injectable()
class Salt implements ISalt {}
@injectable()
class Flour implements IFlour {}
// @injectable()
// IFlour 구현체는 하나만 있어야 IOC 컨테이너가 뭔지 알 수 있다.
// 두 개 등록하면 오류발생.
class OldFlour implements IFlour {}
interface IDough {
getFlour(): IFlour;
getYeast(): IYeast;
}
@injectable()
class Dough implements IDough {
private flour;
private water;
private salt;
public constructor(
@inject(Types.IFlour) flour: IFlour,
@inject(Types.IWater) water: IWater,
@inject(Types.ISalt) salt: ISalt
) {
this.flour = flour;
this.water = water;
this.salt = salt;
}
getFlour(): IFlour {
throw new Error('Method not implemented.');
}
getYeast(): IYeast {
throw new Error('Method not implemented.');
}
}
NestJs 예시
/* IAnimal.ts */
export interface IAnimal {
speak(): string;
}
// export const IAnimalToken = "IAnimal";
// or slightly better...
export const IAnimalToken = Symbol("IAnimal");
/* animal.service.ts */
export class Dog implements IAnimal {
speak() {
return "Woof";
}
}
/* animal.module.ts */
@Module({
providers: [{
provide: IAnimalToken,
useClass: Dog,
}]
})
export class AnimalModule{};
/* client.ts */
export class Client {
constructor(@Inject(IAnimalToken) private animal: IAnimal) {}
// Later...
this.animal.speak();
}
아래와 같이 스트링을 하드코딩해도 되긴 된다. (비추)
/* IAnimal.ts */
export interface IAnimal {
speak(): string;
}
/* animal.service.ts */
export class Dog implements IAnimal {
speak() {
return "Woof";
}
}
/* animal.module.ts */
@Module({
providers: [{
provide: "IAnimal",
useClass: Dog,
}]
})
export class AnimalModule{}
/* client.ts */
export class Client {
constructor(@Inject("IAnimal") private animal: IAnimal) {}
// Later...
this.animal.speak();
}
2. Abstract Class를 이용한다.
- Java 인터페이스와의 차이점은 개발자가 `메서드를 구현`하거나 `필드를 초기화`하는 것을 방해하지 않는다는 것임.
- 추상 클래스를 인터페이스처럼 사용하는 한 안전하게 사용할 수 있음
// nestjs
/* IAnimal.ts */
export abstract class IAnimal {
abstract speak(): string;
}
/* animal.service.ts */
// You can implement an abstract class without extending it!
export class Dog implements IAnimal {
speak() {
return "Woof";
}
}
/* animal.module.ts */
@Module({
providers: [{
provide: IAnimal,
useClass: Dog,
}]
})
export class AnimalModule
/* client.ts */
export class Client {
constructor(private animal: IAnimal) {}
// Later...
this.animal.speak();
}
// TypeGraphQL은 이렇게 한다. 클래스 메타정보로 Schema를 만들기 때문.
@InterfaceType()
abstract class IPerson {
@Field(type => ID)
id: string;
@Field()
name: string;
@Field(type => Int)
age: number;
}
@ObjectType({ implements: IPerson })
class Person implements IPerson {
id: string;
name: string;
age: number;
}
실무에서는 1번 방식을 많이 활용하는것 같다.
참고자료
https://dev.to/ef/nestjs-dependency-injection-with-abstract-classes-4g65
Simplifying Dependency Injection and IoC Concepts using TypeScript | by Saad Bin Amjad | Monstar Lab Bangladesh Engineering | Medium
'FrontEnd' 카테고리의 다른 글
Ad hoc polymophism(애드훅 다형성) with typescript (0) | 2021.12.10 |
---|---|
The constrained identity function : Record Type의 키타입 정의 생략. (0) | 2021.12.08 |
브라우저에서 ESModule 사용하기 (0) | 2021.12.05 |
Node.js 메모리 누수 찾기 관련 게시글 모음 (0) | 2021.12.05 |
제어의 역전(IOC : Inversion of Control) in React (0) | 2021.12.04 |