본문 바로가기

FrontEnd

타입스크립트 의존성 주입, 제어의 역전 with IOC 컨테이너

반응형

타입스크립트 사용 시 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

반응형