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으로 등록한다.
깊게 공부하고 정리하신 내용에 도움 받고 갑니다!
다른 글도 보다가 궁금해진 건데 혹시 이직하신 회사가 판교에 있는 교육 플랫폼 회사 맞나요?