[Typescript] DI using tsyringe

Falcon·2023년 6월 6일
1

typescript

목록 보기
4/6
post-thumbnail

개요

tsyringe 로 typescript DI 를 활용해보자.

Annotations

Injectable

Q. 클래스'에' 생성자 주입 허용 vs 이 클래스 '를' 생성자주입 허용?
=> 클래스'에'

구체 클래스는 DI 사용 클래스에만 injectable()

dependency-a.ts

import {DependencyB} from './dependency-b'
import {injectable} from 'tsyringe'

@injectable()
export class DependencyA {
  constructor(private readonly _dependencyB: DependencyB) {
  }    
    
  printB(msg): void {
    this._dependencyB.print('Hi i\'m b :' + + msg)
  }
}

dependency-b.ts

export class DependencyB {
  print(str: string): void {
    console.log(str)
  }
    
}

main.ts

import {container} from 'tsyringe'
import {DependencyA} from './dependency-a'

// DI성공.
const dependencyA = container.resolve(DependencyA)
dependencyA.printB('zzz')

Interface 는 반드시 container.register() 등록 필수

dependency-a.ts

@injectable()
export class DependencyA {
  constructor(@inject('IDependencyB')private readonly _dependencyB: IDependencyB) {
  }    
    
  printB(msg): void {
    this._dependencyB.print('Hi i\'m b :' + + msg)
  }
}

dependency-b.ts

export interface IDependencyB {
    print(str: string): void
}

export class DependencyB implements IDependencyB {
  print(str: string): void {
    console.log(str)
  }
}

main.ts

import 'reflect-metadata'
import {container} from 'tsyringe'
import {DependencyA} from './dependency-a'
import {DependencyB} from './dependency-b'

container.register('IDependencyB', {useClass: DependencyB})

const dependencyA = container.resolve(DependencyA)
dependencyA.printB('zzz')

singleton()

singleton() annotation is shorthand of injectable() and container.registerSingleton() .
인스턴스가 오로지 하나만 생성된다.
싱글톤 패턴은 자칫 잘못하면 global state 를가질 수 있으므로 다른 객체와 공유될 필요가 있는 동작에 적용해서는 안된다. ex) 도메인 엔티티 클래스

일반 injectable() 이 붙은 클래스는 DI가 일어날 때마다 새 인스턴스가 생성된다. (상태가 격리된다.)

그럼 언제쓰나?
Service, Repository 같은 클래스에 적격이다. (Spring Bean 의 싱글톤을 연상해보라)

default-app.repository.ts

interface IAppRepository {}

export class DefaultAppRepository implements IAppRepository {
  
}

default-app.service.ts

import {injectable} from 'tsyringe'

interface IAppService {}
// 생성자 주입 (DI) 사용 => container 에 interface 등록 필요
@injectable
export class DefaultAppService implements IAppService {
  constructor(private appRepository: IAppRepository) {}
}

index.ts

import 'reflect-metadata'
import {container} from 'tsyringe'

// AppRepository interface 의 구현체 'DefaultAppRepository' 가 runtime 에 주입됨.
container.register('AppRepository', {
  useClass: DefaultAppRepository
})

// DI 완료된 객체 사용.
const appService = container.resolve(DefaultAppService)

Q & A

Q1. 왜 container.register('String') 을 써야하나요?
A1. 타입스크립트의 interface 는 자바스크립트로 트랜스파일 된 후에는 존재하지 않는다. 따라서 컨테이너에 직접 '문자열 토큰'을 등록해야한다.

이를 지원하는 모듈이 reflect-metadata 다.
그럼, 컨테이너는 key:value 즉, 토큰값: 구체 클래스를 쥐고있게 된다.

이처럼, tsyringe 같은 DI 라이브러리 사용시 등록(register) 및 사용(resolve) 코드가 필요하다.

Spring 에서는 이딴거 필요없이 어노테이션이면 컴포넌트 스캔에 자동 주입에 IoC 다해줬는데...

Q2. DI 할 때마다 새로운 인스턴스가 생성되나?
A2. 그렇다. injectable() 은 그렇고 singleton() 은 그렇지 않다.

export interface IDependencyB {
    print(str: string): void
}
export class DependencyB implements IDependencyB {
  constructor() {
    console.log('dependency B created')
  }
  print(str: string): void {
    console.log(str)
  }

}
container.register('IDependencyB', {useClass: DependencyB})
// DI 발생시마다 DependencyB 객체가 생성된다.
// dependency B created!
const dependencyA = container.resolve(DependencyA)
// dependency B created!
const dependencyA2 = container.resolve(DependencyA)

DI가 아니라 단순 container.resolve() 해오는 과정에선 생성되지 않는다.

Q3. injectable() 하지 않은 클래스도 container 에서 resolve() 할 수 있나?
A3. 구체 클래스는 그렇다. 인터페이스는 반드시 token 값을 지정해서 inject()container.register() 가 필요하다.

export class DependencyA {
  constructor() {
    console.log('created: ', this.constructor.name)
  }
}
import 'reflect-metadata'
import {container} from 'tsyringe'
import {DependencyA} from './dependency-a'

// created: DependencyA
container.resolve(DependencyA)
container.resolve(DependencyA)
container.resolve(DependencyA)

Q4. 어노테이션도 없고, container.register() 도 안했는데 어떻게 컨테이너에서 resolve 가능한 것인가?
A4. 리플렉션 기법 때문이다. tsyringe 에서 내부적으로 리플렉션 기법을 사용한다.
별도의 어노테이션이 없어도 resolve() 하는 순간 해당 클래스에 리플렉션으로 생성자와 멤버 정보를 끌어와 객체를 생성한다.

container 사용시 항상 reflect-metadata 모듈을 import 해오는데 이 모듈이 바로 리플렉션 기능을 지원한다. 리플렉션이 클래스의 생성자와 인자 목록을 자동으로 채워준다.

Q5. 리플렉션의 목적이 정확히 뭐야?
A5. 데코레이팅을 통해 클래스, 멤버 변수, 멤버 메소드 등에 메타데이터를 추가하는 것으로
DI, Runtime Type Assertions, Annotation 에 쓰인다.
자세한 스팩은 여기

C#, Java 같은 언어는 언어 자체에서 지원한다.
타입스크립트에서 이를 지원하기 위해 별도 'refelct-metadata' 모듈을 개발한 것이다.

profile
I'm still hungry

0개의 댓글