NestJs에서, 특정 Entity의 Repository를 의존관계로 주입할 때는 여타 의존관계와 달리 @InjectRepository 데코레이터를 사용합니다!
해당 데코레이터는 많이 사용했지만, 정작 어떻게 다르게 동작하는지는 모르고 사용해 왔기에,
한번 들여다 보려고 합니다!
Nest.js는 실제로 어떻게 의존성을 주입해줄까?
타입스크립트 @데코레이터 개념 & 사용법
Nest JS TypeORM 소스 코드
Nest JS 소스코드
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 함수의 반환 결과를 반환하는 함수인 것 같습니다.
Type 의 반환 타입으로 구성된 타입을 생성!declare function f1(): { a: number; b: string };
type T4 = ReturnType<typeof f1>;
↓ ↓
type T4 = {
a: number;
b: string;
}
@InjectRepository 가 감싸고 있는 실제 로직인 Inject를 살펴 보기 전에, 함수 인자로 들어가고 있는 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`;
}
우선은 인자로 들어온 entity 가 null이거나 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를 볼 차례네요!
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,
);
};
}
Inject는 target, key, index를 매개변수로 받는, 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의 의존관계 주입을 다룬 이 글에서 참조하였습니다!
보다 자세한 내용을 위해 참고하시면 좋을 것 같습니다.
@InjectRepository는 getRepositoryToken 메서드에서 반환되는@Inject의 인자로 담아 실행@Inject는 해당 토큰을 메타데이터로 추가하여, 생성자 파라미터를 Dependency Injection의 target으로 등록한다.
깊게 공부하고 정리하신 내용에 도움 받고 갑니다!
다른 글도 보다가 궁금해진 건데 혹시 이직하신 회사가 판교에 있는 교육 플랫폼 회사 맞나요?