[NestJS] 기초중에 기초 Provider 란?

김법우·2021년 7월 25일
0

Nest.js

목록 보기
2/10
post-thumbnail

<1> Definition

Provider 는 Nest의 기본 구성 개념이다. 많은 Nest 기본 클래스가 Provider 로 취급 될 수 있다.
내가 이해한 주요 아이디어는 모듈간 의존성의 주입, @Injectable() 클래스 라는 점이다.
(객체간의 다양한 관계를 생성하고 이런 관계는 외부로 부터 받거나 외부로 주입한다.)


<2> Service Class

Provider 로 취급 될 수 있는 기본적인 클래스이다. 일반적으로는 데이터를 저장하고 검색하는 역할을 하는 Provider 로 설계된다.
[Interface + controller] <- Provider: Service = Search, Save, Update, Delete

Provider 는 단순히 클래스이다. 데이터: 필드(인터페이스로 정의)와 기능: 메소드를 보유한다.
즉, 데이터와 기능을 다른 클래스에게 전달하는 클래스이다!

예시로 User 라는 Service Class 를 정의하고 Controller 와 interface 로 테스트를 해보았다.

/auth/interface/auth.interface.ts

export interface Auth {
    name : string;
    id : string;
    age : number;
}

/auth/auth.controller.ts

import { Controller, Get, HttpCode, Post ,Body} from '@nestjs/common';
import { AuthService } from './auth.service';
import { Auth } from './interface/auth.interface';

@Controller('auth')
export class AuthController {
    constructor(private authService: AuthService){
        // class instance 인 authService 를
		// AuthService Provider , 클래스를 이용해 생성한다. 
		// 즉 위의 문법은 private 생성자를 이용해 선언과 초기화를 동시에 하며,
		// class constructor 를 이용해 Provider 를 주입하는 문법이다.
    }

    @Get()
    @HttpCode(200)
    async findAll(): Promise<Auth[]> {
        return this.authService.findAll();
    }

    @Post()
    @HttpCode(201)
    async create(@Body() auth: Auth): Promise<void> {
        return this.authService.create(auth);
    }
}

/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { Auth } from './interface/auth.interface';

/* Provider 임을 명시 */
@Injectable()
export class AuthService {
    private readonly authList: Auth[] = [];
    
    create(auth: Auth):void {
        console.log('[CREATE User]');
        this.authList.push(auth);
    }

    findAll(): Auth[] {
        console.log('[FIND ALL User]');
        return this.authList;
    }
}

result

// Get - localhost:3000/auth 
[
	{"name":"user1","id":"11","age":"21"},
	{"name":"user2","id":"22","age":"12"},
	{"name":"user3","id":"33","age":"31"}
]

<3> Basic Implementation

NestJS 의 Provider 를 공부하다보니 IOC, DI 등의 용어가 자주 나오는데 해당 개념에 대해 따로 정리를 해보았다.

기초 개념 - IOC 와 DI

IOC 와 DI 는 밀접한 개념적 상관관계가 존재한다.
IOC(Inversion Of Control) 제어 역전이란 메소드나 객체의 호출작업이 개발자에 의해 수행되지 않고 외부에서 결정되는 것을 의미한다. 기존 프로그래밍 언어에서 객체나 인스턴스를 new 와 같은 생성 함수로 메모리를 할당받는 작업은 개발자가 직접 수행해야 했다.
하지만 IOC 개념을 도입한 NestJS 에서는 적절한 설정을 통해 인스턴스의 생명주기를 NestJS 에게 위임하도록 하고 있다.

DI(Dependency Injection) 의존성 주입이란, 의존적인 객체를 직접 생성하지 않고 외부에서 결정한 뒤 연결하는 것을 의미한다. DI 를 내포한 코딩을 통해 객체 지향성을 보장하며 모듈간의 결합도를 낮추고 IOC 의 구현에 적합한 플랫폼 부품을 작성 할 수 있게 된다.

정리하면 DI 에 의해 주입된 의존성에 의거해 NestJS 는 IOC를 지원하고 수행한다.
의존성 주입을 위해 의존을 명시하고 의존관계에 묶인 함수, 클래스, API 등을 묶어 처리 할 수 있으므로 쉬운 서비스 추상화가 가능해진다고 나는 이해하였다.

이런 개념을 바탕으로 Provider 의 프로세싱을 따라가보았다.

Provider 의 실행 과정

위에서 작성했던 테스트 코드의 흐름을 그대로 따라가보았다.

  1. @Injectable( ) 데코레이터를 발견시 해당 클래스를 Nest IOC 컨테이너가 관리 가능한
    클래스로 declare 한다.
  2. Controller 에서 해당 Provider 토큰에 대한 의존성을 생성자 주입과 함께 생성한다.
  3. app.module.ts 에서 Provider 토큰을 해당 Provider 의 클래스와 연결한다.
// 축약형
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

// 실제 컴파일
@Module({
  controllers: [CatsController],
  providers: [
	  {
	    provide: CatsService,
	    useClass: CatsService,
	  },
	];
})

위와 같은 종속성 프로세싱은 부트 스트랩 과정에서 발생한다.


<4> Custom Provider

NestJS 의 Provider 가 어렵고 흥미롭게 느껴지는 부분이 바로 Custom Provider 부분이었다.
원하는 목적과 최적화 성능에 맞춰 개발자가 Custom Provider 를 정의하고 사용 할 수 있는데 예시 코드를 작성하며 이해해 보았다.

1. useValue

모형 객체에 실제 구현을 대체하는데 용이하다.

providers: [
    {
      provide: CatsService,
      useValue: "대체할 제공자",
    },
  ],

혹은, 문자열 값 토큰을 외부 파일에서 가져온 기존 개체와 연결하는 기능을 제공 할 수도 있다.

import { connection } from './connection'; // 외부 파일의 개채

@Module({
  providers: [
    {
      provide: 'CONNECTION', // 문자열 토큰
      useValue: connection, // 해당 토큰과 연결할 외부 파일 개채
    },
  ],
})
export class AppModule {}

위의 패턴을 사용해 문자열 형태의 토큰으로 개체를 연결하고 싶다면, 종속성을 클래스 이름으로 선언해야 한다!

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

2. useClass

동적으로 해결해야 할 필요가 있는 클래스를 결정할때 용이하다.
(ex 추상 or 기본 클래스를 환경에 따라 다르게 제공하고자 하는 상황 등)

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development' // 환경에 따라 서비스를 정해 연결
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

3. useFactory

동적인 공급자 생성이 가능하다. 실제 공급자는 정의한 Factory Function 의 반환값으로 제공된다. Factory 의 작업에 다른 제공자를 주입하는 것도 가능하다.
(하지만 이 부분은 주의할점이 존재, 하단 메커니즘에 표시)

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider], // 팩토리에 optionProvider를 주입하기 위해 필요
};

@Module({
  providers: [connectionFactory],
})
export class AppModule {}

동적인 공급자 생성 에서 '동적'이라는 의미는 Factory Function 에서 처리된 결과로 반환받은 Provider Class 를 주입받는다는 뜻으로 사용하였다.

useFactory 는 개발 할 때 자주 사용했는데, 위의 예시코드처럼 DataBase Config Service 를 DataBase Service 에 항상 주입해놓지 않고 필요시에 저렇게 Factory 를 통해 주입 할 수 있다는 점에서 메리트를 느꼈기 때문이다.

이렇게 사용하면 Production/dev 환경 혹은 다양한 Database 를 번갈아 사용해야 할 때 등등 손쉽게 Service 적용이 가능하도록 완벽히 config 와 database service 가 분리 될 수 있어 유용하다!

주의 사항! 공급자 주입 useFactory 메커니즘
1. Factory 함수는 Provider Injection이 가능하다. (Provider 를 인수로 받을 수 있다)
2. 반드시 inject: [OptionsProvider], 구문을 통해 인수로 전달할 공급자 배열을 허용 해야 한다.
Nest 는 inject 목록의 인스턴스를 적힌 순서대로 Factory 함수의 인자로 전달한다.


4. useExisting

기존의 Provider 에 대한 별칭 설정이 가능하다. 동일한 Provider 에 대해 접근하는 방법이 2가지로 생기는것!
(ex 클래스 기반 Provider 와 문자열 토큰 기반 Provider 를 별칭을 통해 분리)

@Injectable() // 클래스 기반 Provider
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = { // 문자열 토큰 기반 Provider , 별칭을 생성
  provide: 'AliasedLoggerService',
  useExisting: LoggerService, // 종속성 설정
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

5. Custom Provider Export

다른 Provider 와 동일하게 사용자 지정 RPovider 또한 선언된 모듈 내부로 범위가 지정된다.
다른 모듈에 표시하기 위해서는 Export 를 통해 내보내기를 수행해야한다.
문자열 토큰을 내보낼수도 있으며, 전체 Provider 개체를 내보낼수 있따.

export with Token
const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'], // 지정한 토큰만을 export
})
export class AppModule {}
export with the full provider object
const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory], // 전체 Provider 객체를 export
})
export class AppModule {}

<5> 이외 Provider 관련 기능들!

1. Optional Provider

정의한 Provider 가 선택적인 경우 typescript 의 optional (?) 과 같이 전달되지 않은 경우 기본값을 사용하도록 할 수 있다.

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> { 
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
  // @Optional() 을 사용해 선택적으로 공급자가 주입 될 수 있음을 명시한다. 
}

2. Property-Based Injection

속성 기반 주입, 이전에 활용한 주입은 클래스 생성자 기반 주입인데 최상위 클래스가 여러 Provider 에게 주입받는 경우-의존하는 경우 Super 를 통한 주입이 불편할 경우 사용한다!

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS') // 단순히 @Inject(--) 데코레이터를 활용해 속성 수준에서의 주입
  private readonly httpClient: T;
}
profile
개발을 사랑하는 개발자. 끝없이 꼬리를 물며 답하고 찾는 과정에서 공부하는 개발자 입니다. 잘못된 내용 혹은 더해주시고 싶은 이야기가 있다면 부디 가르침을 주세요!

0개의 댓글