[NestJS] @InjectRepository 데코레이터 알아보기

toto9602·2023년 11월 19일
0

NestJS 공부하기

목록 보기
2/4

NestJs에서, 특정 Entity의 Repository를 의존관계로 주입할 때는 여타 의존관계와 달리 @InjectRepository 데코레이터를 사용합니다!
해당 데코레이터는 많이 사용했지만, 정작 어떻게 다르게 동작하는지는 모르고 사용해 왔기에,
한번 들여다 보려고 합니다!

참고 자료

Nest.js는 실제로 어떻게 의존성을 주입해줄까?
타입스크립트 @데코레이터 개념 & 사용법
Nest JS TypeORM 소스 코드
Nest JS 소스코드

@InjectRepository

소스 코드

import { Inject } from '@nestjs/common';
import { DataSource, DataSourceOptions } from 'typeorm';
import { EntityClassOrSchema } from '../interfaces/entity-class-or-schema.type';
import { DEFAULT_DATA_SOURCE_NAME } from '../typeorm.constants';
import {
  getDataSourceToken,
  getEntityManagerToken,
  getRepositoryToken,
} from './typeorm.utils';

export const InjectRepository = (
  entity: EntityClassOrSchema,
  dataSource: string = DEFAULT_DATA_SOURCE_NAME, // default
): ReturnType<typeof Inject> => Inject(getRepositoryToken(entity, dataSource));

소스 코드는 생각보다 내용이 많지 않네요..!

첫 번째 인자로 entity 클래스를, 두 번째 인자로 dataSource 이름을 받아
Inject 함수의 반환 결과를 반환하는 함수인 것 같습니다.

[ cf. ReturnType< Type > ]

  • 함수 Type 의 반환 타입으로 구성된 타입을 생성!

[ 예제 ]

declare function f1(): { a: number; b: string };

type T4 = ReturnType<typeof f1>;

↓ ↓

type T4 = {
    a: number;
    b: string;
}

@InjectRepository 가 감싸고 있는 실제 로직인 Inject를 살펴 보기 전에, 함수 인자로 들어가고 있는 getRepositoryToken 메서드를 살펴보려 합니다!

getRepositoryToken

[ 소스코드 ]

/**
 * This function generates an injection token for an Entity or Repository
 * @param {EntityClassOrSchema} entity parameter can either be an Entity or Repository
 * @param {string} [dataSource='default'] DataSource name
 * @returns {string} The Entity | Repository injection token
 */
export function getRepositoryToken(
  entity: EntityClassOrSchema,
  dataSource:
    | DataSource
    | DataSourceOptions
    | string = DEFAULT_DATA_SOURCE_NAME,
): Function | string {
  if (entity === null || entity === undefined) {
    throw new CircularDependencyException('@InjectRepository()');
  }
  const dataSourcePrefix = getDataSourcePrefix(dataSource);
  if (
    entity instanceof Function &&
    (entity.prototype instanceof Repository ||
      entity.prototype instanceof AbstractRepository)
  ) {
    if (!dataSourcePrefix) {
      return entity;
    }
    return `${dataSourcePrefix}${getCustomRepositoryToken(entity)}`;
  }

  if (entity instanceof EntitySchema) {
    return `${dataSourcePrefix}${
      entity.options.target ? entity.options.target.name : entity.options.name
    }Repository`;
  }
  return `${dataSourcePrefix}${entity.name}Repository`;
}

우선은 인자로 들어온 entitynull이거나 undefined 인 경우 CircularDependencyException을 throw하는 부분이 눈에 띄었습니다!

[ CircularDependencyException ]

export class CircularDependencyException extends Error {
  constructor(context?: string) {
    const ctx = context ? ` inside ${context}` : ``;
    super(
      `A circular dependency has been detected${ctx}. Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Also, try to eliminate barrel files because they can lead to an unexpected behavior too.`,
    );
  }
}

위의 경우에서는 @InjectRepository 라는 문자열을 받아서, 순환 참조 에러에 맞는 에러 메시지를 넘겨주는 Exception이네요!

이제 해당 에러가 발생하지 않았을 때, 실행되는 getDataSourcePrefix 부분을 보겠습니다.

[ getDataSourcePrefix ]

/**
 * This function returns a DataSource prefix based on the dataSource name
 * @param {DataSource | DataSourceOptions | string} [dataSource='default'] This optional parameter is either
 * a DataSource, or a DataSourceOptions or a string.
 * @returns {string | Function} The DataSource injection token.
 */
export function getDataSourcePrefix(
  dataSource:
    | DataSource
    | DataSourceOptions
    | string = DEFAULT_DATA_SOURCE_NAME,
): string {
  if (dataSource === DEFAULT_DATA_SOURCE_NAME) {
    return '';
  }
  if (typeof dataSource === 'string') {
    return dataSource + '_';
  }
  if (dataSource.name === DEFAULT_DATA_SOURCE_NAME || !dataSource.name) {
    return '';
  }
  return dataSource.name + '_';
}

매개변수로 입력된 dataSource가 String 타입인지, DataSource 또는 DataSourceOptions object인지에 따라 분기 처리가 되어 있지만,

default DataSource일 경우 빈 문자열을,
사용자가 dataSource 이름을 명시한 경우 ${name}_ 형식의 문자열을 반환해주는 함수네요!

이하 코드에서는, 여기서 반환된 prefix와, entity의 prototype에 따라
문자열, 혹은 Repository 자체를 반환하는 부분인 듯한데, 살펴 보겠습니다!

 if (
    entity instanceof Function &&
    (entity.prototype instanceof Repository ||
      entity.prototype instanceof AbstractRepository)
  ) { // TypeORM Repository 인스턴스라면 
    if (!dataSourcePrefix) { // default DataSource인 경우
      return entity; // 인스턴스를 반환
    } // 그렇지 않다면 `${prefix}${repository.name}` 반환합니다. 
    return `${dataSourcePrefix}${getCustomRepositoryToken(entity)}`;
  }

  if (entity instanceof EntitySchema) { // EntitySchema 인스턴스라면
    // target 속성으로 문자열을 생성하여 반환합니다. 
    return `${dataSourcePrefix}${
      entity.options.target ? entity.options.target.name : entity.options.name
    }Repository`;
  }
// Repository도, EntitySchema도 아니라면, 아래 문자열을 반환합니다. 
  return `${dataSourcePrefix}${entity.name}Repository`;
}

[ cf. getCustomRepositoryToken ]

/**
 * This function generates an injection token for an Entity or Repository
 * @param {Function} This parameter can either be an Entity or Repository
 * @returns {string} The Repository injection token
 */
export function getCustomRepositoryToken(repository: Function): string {
  if (repository === null || repository === undefined) {
    throw new CircularDependencyException('@InjectRepository()');
  }
  // 순환 참조 에러가 없다면,name 속성을 반환합니다. 
  return repository.name;
}

정리

→ 매개변수인 entity 의 prototype에 따른 분기가 있지만,
getRepositoryToken은 TypeORM Repository 인스턴스 혹은 dataSource 및 Entity 이름 등을 포함한 단일 문자열을 반환하는 것으로 보입니다!

그렇다면 이제 실제 로직이 포함되어 있을 것 같은 Inject를 볼 차례네요!

@Inject

소스 코드

export function Inject<T = any>(token?: T) {
  return (target: object, key: string | symbol | undefined, index?: number) => {
    const type = token || Reflect.getMetadata('design:type', target, key);
	// 
    if (!isUndefined(index)) {
      let dependencies =
        Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || []; // SELF_DECLARED_DEPS_METADATA = `self:paramtypes`

      dependencies = [...dependencies, { index, param: type }];
      Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, dependencies, target);
      // SELF_DECLARED_DEPS_METADATA = `self:paramtypes`
      return;
    }
    // PROPERTY_DEPS_METADATA = `self:properties_metadata`
    let properties =
      Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];

    properties = [...properties, { key, type }];
    Reflect.defineMetadata(
      PROPERTY_DEPS_METADATA,
      properties,
      target.constructor,
    );
  };
}

Injecttarget, key, index를 매개변수로 받는, Parameter Decorator로 보입니다.

[ cf. Parameter Decorator ]

  • target : static 프로퍼티가 속한 클래스의 생성자 함수 또는 인스턴스 프로퍼티가 속한 클래스의 prototype 객체
  • key : 매개변수가 들어있는 메서드의 이름
  • index : 매개변수의 순서 인덱스

[ cf. SELF_DECLARED_DEPS_METADATA ]

  • SELF_DECLARED_DEPS_METADATA 는, reflect-metadata 라이브러리에서 제공하는 design:paramtypes 메타데이터를 Nest에서 유사하게 만든 것이라고 합니다.
  • design:paramtypes 메타데이터는 생성자 함수의 매개변수 타입들을 가져오는 데 사용하는데,
    어떠한 형태로든 데코레이터가 붙어있어야 가져올 수 있다고 합니다.

소스 코드 다시보기

export function Inject<T = any>(token?: T) {
  return (target: object, key: string | symbol | undefined, index?: number) => {
    // @Inject는 명시적으로 가져올 프로바이더를 지정하므로, 조회한 타입이 아니라 데코레이터의 토큰을 사용
    const type = token || Reflect.getMetadata('design:type', target, key);
	
    if (!isUndefined(index)) {
      // index가 undefined가 아니다 => @Inject를 생성자의 매개변수에 사용했다!
      let dependencies =
        Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || []; 
      // 현재까지 SELF_DECLARED_DEPS_METADATA 메타데이터에 저장된 값 조회

      dependencies = [...dependencies, { index, param: type }];
      // 메타데이터에 현재 토큰을 추가
      Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, dependencies, target);
      // 추가 후 다시 배열을 메타데이터에 저장
      return;
    }
    // index가 undefined라는 건, @Inject를 프로퍼티(필드)에 사용했다는 것. 
    let properties =
      Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];

    properties = [...properties, { key, type }];
    Reflect.defineMetadata(
      PROPERTY_DEPS_METADATA,
      properties,
      target.constructor,
    );
  };
}

해당 부분의 코드 해석은 Nest JS의 의존관계 주입을 다룬 이 글에서 참조하였습니다!
보다 자세한 내용을 위해 참고하시면 좋을 것 같습니다.

정리

  • @InjectRepositorygetRepositoryToken 메서드에서 반환되는
    Repository, 혹은 단일 String을 @Inject의 인자로 담아 실행
  • @Inject는 해당 토큰을 메타데이터로 추가하여, 생성자 파라미터를 Dependency Injection의 target으로 등록한다.
profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글