fontcolor_theme
Deepkit App

모듈

Deepkit은 고도로 모듈화되어 있어 애플리케이션을 여러 유용한 모듈로 쉽게 분할할 수 있습니다. 각 모듈은 자체 의존성 주입 하위 컨테이너(모든 상위 provider를 상속), 구성(configuration), 명령(commands) 등을 가집니다. 시작하기 장에서 이미 하나의 모듈(루트 모듈)을 만들었습니다. new App은 모듈과 거의 동일한 인자를 받는데, 이는 백그라운드에서 자동으로 루트 모듈을 생성하기 때문입니다.

애플리케이션을 서브 모듈로 분할할 계획이 없거나, 모듈을 패키지로 만들어 다른 사람에게 제공할 계획이 없다면 이 장은 건너뛰어도 됩니다.

모듈은 클래스 모듈 또는 함수형 모듈로 정의할 수 있습니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({
  //new App({})와 동일한 옵션
  providers: [MyService]
}) {
}
import { AppModule } from '@deepkit/app';

export function myModule(options: {} = {}) {
    return (module: AppModule) => {
        module.addProvider(MyService);
    };
}

이 모듈은 애플리케이션이나 다른 모듈에 가져와(import) 사용할 수 있습니다.

import { MyModule, myModule } from './module.ts'

new App({
    imports: [
        new MyModule(), //클래스 모듈 import
        myModule(), //함수형 모듈 import
    ]
}).run();

이제 App과 동일하게 이 모듈에 기능을 추가할 수 있습니다. createModule의 인자는 동일하지만, 모듈 정의에서는 imports를 사용할 수 없습니다. 함수형 라우트의 경우 AppModule의 메서드를 사용하여 자체 옵션에 따라 동적으로 구성할 수 있습니다.

HTTP/RPC/CLI 컨트롤러, 서비스, 구성, 이벤트 리스너 및 다양한 모듈 훅을 추가하여 모듈을 더욱 동적으로 만드세요.

컨트롤러

모듈은 다른 모듈에 의해 처리되는 컨트롤러를 정의할 수 있습니다. 예를 들어 @deepkit/http 패키지의 데코레이터가 있는 컨트롤러를 추가하면, 해당 HttpModule 모듈이 이를 감지하여 라우터에 발견된 라우트를 등록합니다. 하나의 컨트롤러에는 이러한 데코레이터가 여러 개 있을 수 있습니다. 이러한 데코레이터를 제공하는 모듈 작성자가 컨트롤러를 어떻게 처리할지는 그에게 달려 있습니다.

Deepkit에는 이러한 컨트롤러를 처리하는 세 가지 패키지(HTTP, RPC, CLI)가 있습니다. 자세한 내용은 각 장을 참고하세요. 아래는 HTTP 컨트롤러의 예시입니다:

import { createModuleClass } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

class MyHttpController {
  @http.GET('/hello)
  hello() {
    return 'Hello world!';
  }
}

export class MyModule extends createModuleClass({
  controllers: [MyHttpController]
}) {
}


//App에서도 동일하게 가능
new App({
  controllers: [MyHttpController]
}).run();

Provider

애플리케이션의 providers 섹션에 provider를 정의하면 애플리케이션 전반에서 접근할 수 있습니다. 그러나 모듈의 경우 이러한 provider는 해당 모듈의 의존성 주입 하위 컨테이너에 자동으로 캡슐화됩니다. 다른 모듈이나 애플리케이션에서 사용할 수 있도록 하려면 각 provider를 수동으로 export해야 합니다.

provider 동작에 대해 더 알아보려면 의존성 주입 장을 참조하세요.

import { createModuleClass } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

export class HelloWorldService {
  helloWorld() {
    return 'Hello there!';
  }
}

class MyHttpController {
  constructor(private helloService: HelloWorldService) {
  }

  @http.GET('/hello)
  hello() {
    return this.helloService.helloWorld();
  }
}

export class MyModule extends createModuleClass({
  controllers: [MyHttpController],
  providers: [HelloWorldService],
}) {
}

export function myModule(options: {} = {}) {
  return (module: AppModule) => {
    module.addController(MyHttpController);
    module.addProvider(HelloWorldService);
  };
}

//App에서도 동일하게 가능
new App({
  controllers: [MyHttpController],
  providers: [HelloWorldService],
}).run();

사용자가 이 모듈을 import하더라도, MyModule의 하위 의존성 주입 컨테이너에 캡슐화되어 있기 때문에 HelloWorldService에 접근할 수 없습니다.

내보내기

import하는 쪽의 모듈에서 provider를 사용할 수 있도록 하려면 exports에 provider의 토큰을 포함하면 됩니다. 이는 본질적으로 provider를 한 단계 위로, 즉 상위 모듈(가져오는 쪽)의 의존성 주입 컨테이너로 이동시킵니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({
  exports: [HelloWorldService],
}) {
}

export function myModule(options: {} = {}) {
  return (module: AppModule) => {
    module.addExport(HelloWorldService);
  };
}

FactoryProvider, UseClassProvider 등 다른 provider를 사용하는 경우에도, exports에는 클래스 타입만 사용해야 합니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({
  controllers: [MyHttpController]
  providers: [
    { provide: HelloWorldService, useValue: new HelloWorldService }
  ],
  exports: [HelloWorldService],
}) {
}

이제 해당 모듈을 import하고, 애플리케이션 코드에서 export된 서비스를 사용할 수 있습니다.

import { App } from '@deepkit/app';
import { cli, Command } from '@deepkit/app';
import { HelloWorldService, MyModule } from './my-module';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected helloWorld: HelloWorldService) {
    }

    async execute() {
        this.helloWorld.helloWorld();
    }
}

new App({
    controllers: [TestCommand],
    imports: [
        new MyModule(),
    ]
}).run();

자세한 내용은 의존성 주입 장을 참조하세요.

구성 스키마

모듈은 타입 안전한 구성 옵션을 가질 수 있습니다. 이 옵션 값은 클래스 참조 또는 Partial<Config, 'url'> 같은 타입 함수만으로 해당 모듈의 서비스에 부분적으로 또는 완전히 주입할 수 있습니다. 구성 스키마를 정의하려면 프로퍼티가 있는 클래스를 작성하세요.

export class Config {
    title!: string; //필수이며 제공되어야 함
    host?: string; //옵션

    debug: boolean = false; //기본값도 지원됩니다
}
import { createModuleClass } from '@deepkit/app';
import { Config } from './module.config.ts';

export class MyModule extends createModuleClass({
  config: Config
}) {
}

export function myModule(options: Partial<Config> = {}) {
  return (module: AppModule) => {
    module.setConfigDefinition(Config).configure(options);
  };
}

구성 옵션 값은 모듈의 생성자, .configure() 메서드, 또는 구성 로더(예: 환경 변수 로더)를 통해 제공할 수 있습니다.

import { MyModule } from './module.ts';

new App({
   imports: [
       new MyModule({title: 'Hello World'}),
       myModule({title: 'Hello World'}),
   ],
}).run();

import된 모듈의 구성 옵션을 동적으로 변경하려면 process 모듈 훅을 사용할 수 있습니다. 이는 구성 옵션을 전달하거나, 현재 모듈의 구성 또는 다른 모듈 인스턴스 정보에 따라 import된 모듈을 설정하기에 좋은 위치입니다.

import { MyModule } from './module.ts';

export class MainModule extends createModuleClass({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

export function myModule(options: Partial<Config> = {}) {
    return (module: AppModule) => {
        module.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    };
}

애플리케이션 레벨에서는 약간 다르게 동작합니다:

new App({
    imports: [new MyModule({title: 'Hello World'}],
})
    .setup((module, config) => {
        module.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    })
    .run();

루트 애플리케이션 모듈이 일반 모듈에서 생성된 경우, 일반 모듈과 유사하게 동작합니다.

class AppModule extends createModuleClass({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

App.fromModule(new AppModule()).run();

모듈 이름

모든 구성 옵션은 환경 변수를 통해서도 변경할 수 있습니다. 이는 모듈에 이름이 할당된 경우에만 동작합니다. 모듈 이름은 createModule을 통해 정의할 수 있으며, 인스턴스 생성 시점에 동적으로 변경할 수도 있습니다. 후자의 패턴은 동일한 모듈을 두 번 import하고 각각을 구분하고자 할 때 유용합니다.

export class MyModule extends createModuleClass({
  name: 'my'
}) {
}

export function myModule(options: Partial<Config> = {}) {
    return (module: AppModule) => {
        module.name = 'my';
    };
}
import { MyModule } from './module';

new App({
    imports: [
        new MyModule(), //'my'가 기본 이름
        new MyModule().rename('my2'), //'my2'가 새로운 이름
    ]
}).run();

환경 변수 또는 .env 파일에서 구성 옵션을 로드하는 방법에 대한 자세한 내용은 구성 장을 참고하세요.

Imports

모듈은 다른 모듈을 import하여 기능을 확장할 수 있습니다. App에서는 모듈 정의 객체의 imports: []를 통해 다른 모듈을 import할 수 있습니다:

new App({
    imports: [new Module]
}).run();

일반 모듈에서는 객체 정의에 인스턴스를 넣으면 전역이 되어버리므로(보통 원치 않는 동작) 이 방식이 불가능합니다. 대신, 모듈 자체의 imports 프로퍼티를 통해 모듈 내에서 인스턴스를 생성하여, 모듈의 각 새 인스턴스마다 import된 모듈의 인스턴스가 생성되도록 할 수 있습니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({}) {
  imports = [new OtherModule()];
}

export function myModule() {
  return (module: AppModule) => {
    module.addImport(new OtherModule());
  };
}

process 훅을 사용하여 구성에 따라 모듈을 동적으로 import할 수도 있습니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({}) {
  process() {
    if (this.config.xEnabled) {
      this.addImport(new OtherModule({ option: 'value' });
    }
  }
}

export function myModule(option: { xEnabled?: boolean } = {}) {
  return (module: AppModule) => {
    if (option.xEnabled) {
      module.addImport(new OtherModule());
    }
  };
}

훅(Hooks)

서비스 컨테이너는 루트/애플리케이션 모듈부터 시작하여 import된 순서대로 모든 모듈을 로드합니다.

이 과정에서 서비스 컨테이너는 등록된 모든 구성 로더를 실행하고, setupConfig 콜백을 호출하며, 각 모듈의 구성 객체를 검증합니다.

서비스 컨테이너 로딩의 전체 과정은 다음과 같습니다:

  1. 각 모듈 T에 대해(루트부터 시작)
    1. 구성 로더 ConfigLoader.load(T) 실행.
    2. T.setupConfig() 호출.
    3. T의 구성 검증. 유효하지 않으면 중단.
    4. T.process() 호출. 여기서 모듈은 유효한 구성 옵션에 따라 자신을 수정할 수 있습니다. 새로운 import, provider 등을 추가합니다.
    5. T의 각 import된 모듈에 대해 1. 반복.
  2. 등록된 모든 모듈 검색.
  3. 발견된 각 모듈 T를 처리.
    1. T의 미들웨어 등록.
    2. 이벤트 디스패처에 T의 리스너 등록.
    3. 2.에서 발견된 모든 모듈에 대해 Module.processController(T, controller) 호출.
    4. 2.에서 발견된 모든 모듈에 대해 Module.processProvider(T, token, provider) 호출.
    5. T의 각 import된 모듈에 대해 3. 반복.
  4. 모든 모듈에 대해 T.postProcess() 실행.
  5. 모든 모듈에서 bootstrap 클래스 인스턴스화.
  6. 의존성 주입 컨테이너가 이제 빌드됨.

훅을 사용하려면 모듈 클래스에서 process, processProvider, postProcess 메서드를 등록하면 됩니다.

import { createModuleClass, AppModule } from '@deepkit/app';
import { isClass } from '@deepkit/core';
import { ProviderWithScope, Token } from '@deepkit/injector';

export class MyModule extends createModuleClass({}) {
  imports = [new FrameworkModule()];

  //먼저 실행됨
  process() {
    //this.config에는 완전히 검증된 구성 객체가 들어 있습니다.
    if (this.config.environment === 'development') {
      this.getImportedModuleByClass(FrameworkModule).configure({ debug: true });
    }
    this.addModule(new AnotherModule);
    this.addProvider(Service);

    //추가 설정 메서드를 호출합니다.
    //이 경우 의존성 주입 컨테이너가 Service를 인스턴스화할 때
    //'method1'을 주어진 인자로 호출합니다.
    this.configureProvider<Service>(v => v.method1(this.config.value));
  }

  //모든 모듈에서 발견된 각 컨트롤러에 대해 실행됨
  processController(module: AppModule<any>, controller: ClassType) {
    //예를 들어 HttpModule은 각 컨트롤러에 대해 @http 데코레이터가 사용되었는지 확인하고,
    //그렇다면 모든 라우트 정보를 추출하여 라우터에 넣습니다.
  }

  //모든 모듈에서 발견된 각 provider에 대해 실행됨
  processProvider(module: AppModule<any>, token: Token, provider: ProviderWithScope) {
    //예를 들어 FrameworkModule은 deepkit/orm Database를 상속하는 제공된 토큰을 찾아
    //마이그레이션 CLI 명령과 Framework Debugger에서 사용할 수 있도록
    //자동으로 DatabaseRegistry에 등록합니다.
  }

  //모든 모듈이 처리된 후 실행됨.
  //process/processProvider에서 처리한 정보를 기반으로
  //module.configureProvider를 통해 provider를 설정할 수 있는 마지막 기회입니다.
  postProcess() {

  }
}

상태를 가진 모듈

각 모듈은 new Module로 명시적으로 인스턴스화되기 때문에 상태를 가질 수 있습니다. 이 상태는 의존성 주입 컨테이너에 주입되어 서비스에서 사용할 수 있습니다.

예시로 HttpModule의 사용 사례를 생각해봅시다. 애플리케이션 전체에 등록된 각 컨트롤러에 특정 @http 데코레이터가 있는지 확인하고, 있다면 컨트롤러를 레지스트리에 넣습니다. 이 레지스트리는 Router에 주입되고, Router가 인스턴스화될 때 해당 컨트롤러의 모든 라우트 정보를 추출하여 등록합니다.

class Registry {
    protected controllers: { module: AppModule<any>, classType: ClassType }[] = [];
        
    register(module: AppModule<any>, controller: ClassType) {
        this.controllers.push({ module, classType: controller });
    }
        
    get(classType: ClassType) {
        const controller = this.controllers.find(v => v.classType === classType);
        if (!controller) throw new Error('Controller unknown');
        return controller;
    }
}
        
class Router {
    constructor(
        protected injectorContext: InjectorContext,
        protected registry: Registry
    ) {
    }
        
    getController(classType: ClassType) {
        //주어진 컨트롤러 classType에 대한 classType과 모듈을 찾습니다
        const controller = this.registry.get(classType);
        
        //여기서 컨트롤러가 인스턴스화됩니다. 이미 인스턴스화되어 있으면
        //이전 인스턴스를 반환합니다(해당 provider가 transient: true가 아닌 경우)
        return injector.get(controller.classType, controller.module);
    }
}
        
class HttpModule extends createModuleClass({
    providers: [Router],
    exports: [Router],
}) {
    protected registry = new Registry;
        
    process() {
        this.addProvider({ provide: Registry, useValue: this.registry });
    }
        
    processController(module: AppModule<any>, controller: ClassType) {
        //컨트롤러 소비자는 컨트롤러를 모듈의 providers에 넣어야 합니다
        if (!module.isProvided(controller)) module.addProvider(controller);
        this.registry.register(module, controller);
    }
}
        
class MyController {}
        
const app = new App({
    controllers: [MyController],
    imports: [new HttpModule()]
});
        
const myController = app.get(Router).getController(MyController);

루트용

root 프로퍼티를 사용하면 모듈의 의존성 주입 컨테이너를 루트 애플리케이션의 컨테이너로 이동시킬 수 있습니다. 이를 통해 모듈의 모든 서비스가 자동으로 루트 애플리케이션에서도 사용할 수 있게 됩니다. 이는 기본적으로 각 provider(컨트롤러, 이벤트 리스너, provider)를 루트 컨테이너로 이동시키는 것입니다. 의존성 충돌이 발생할 수 있으므로, 정말 전역만 포함하는 모듈에만 사용해야 합니다. 가능하면 각 provider를 수동으로 export하는 것을 권장합니다.

여러 모듈에서 사용할 수 있는 라이브러리를 구축하는 경우, root 사용을 피해야 합니다. 다른 라이브러리의 provider 토큰과 충돌할 수 있기 때문입니다. 예를 들어, 이 라이브러리 모듈이 서비스를 정의하는 foo 모듈을 import하고, 일부 서비스를 필요에 맞게 재구성했는데, 사용자의 애플리케이션도 동일한 foo 모듈을 import하는 경우, 사용자는 당신이 재구성한 서비스를 받게 됩니다. 더 단순한 사용 사례에서는 문제가 되지 않을 수도 있습니다.

import { createModuleClass } from '@deepkit/app';

export class MyModule extends createModuleClass({}) {
  root = true;
}

서드파티 모듈의 root 프로퍼티를 forRoot()를 사용하여 변경할 수도 있습니다.

new App({
    imports: [new ThirdPartyModule().forRoot()],
}).run();

Injector Context

InjectorContext는 의존성 주입 컨테이너입니다. 이를 사용하여 자신의 모듈이나 다른 모듈에서 서비스의 요청/인스턴스화를 수행할 수 있습니다. 예를 들어 processControllers에서 컨트롤러를 저장해 두었다가, 이를 올바르게 인스턴스화하고자 할 때 필요합니다.

English中文 (Chinese)한국어 (Korean)日本語 (Japanese)Deutsch (German)