nestjs 에서 class 를 인스턴스화 시켜서 언제 어디서 사용해도 동일한 값을 이용하기 위해 constructor()
안에 사용하고 싶은 객체를 주입해서 주로 사용한다.
여러 예시가 있지만, 이 글에서는 db 에 접근하는 layer 인 repository 에서 어떻게 주입해서 사용하는지 코드를 통해 확인해보자.
// user.repository.ts
/**
* @description default db connection : main db
*/
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
constructor(
@InjectEntityManager() private readonly EntityManager: EntityManager,
@InjectEntityManager('bo') private readonly boEntityManager: EntityManager,
) {
super(EntityManager);
}
우리는 읽는 repository 와 쓰는 repository 를 구분해서 사용하는데, 각 repository 에서 상속받는 클래스가 다르다.
read repository 에서는 ReadRepo
를 상속받는데, ReadRepo 는 다음과 같이 생겼다.
export abstract class ReadRepo extends Repo {
constructor(entityManager: EntityManager) {
super(entityManager);
}
protected async queryMany<T extends object>(
query: string,
parameters?: any[],
classConstructor?: ClassConstructor<T>,
): Promise<T[]> {
const queryResult = columnToCamel(await this.query(query, parameters));
if (!classConstructor) {
return queryResult;
}
return Promise.all(
queryResult.map((r) => this.validateReturnType(r, classConstructor)),
);
}
UserFunctions
의 생성자에서
super(EntityManager);
를 통해서 ReadRepo
의 생성자로 entityManager
를 주입시켜 주고,
export class Repo {
constructor(protected entityManager: EntityManager) {}
protected query<T>(query: string, parameters?: any[]): Promise<T> {
return this.entityManager.query(query, parameters);
}
}
주입받는 entityManager
를 다시
super(EntityManager);
를 통해서 상속받는 Repo
로 주입시켜서 쿼리를 실행하는 구조이다.
하나의 repository 에서 하나의 entityManager
만 사용한다면 위 코드는 정말 잘 짜여진 코드라고 생각된다. 모듈화도 잘 시켜놨고, 코드의 재사용성 및 유지보수에도 좋다고 생각이 들지만...
문제는 여러 entityManager
를 사용하는 경우에 발생했다.
// user.repository.ts
/**
* @description default db connection : main db
*/
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
constructor(
@InjectEntityManager() private readonly EntityManager: EntityManager,
@InjectEntityManager('bo') private readonly boEntityManager: EntityManager,
) {
super(EntityManager);
}
async func1(): Promise<any> {
this.entityManager = this.EntityManager
this.queryMany('select ~'); // 1
this.queryMany('select ~'); // 2
this.queryMany('select ~'); // 3
}
async func2(): Promise<any> {
this.entityManager = this.boEntityManager
return this.queryMany('select ~');
}
func1 함수에서는 일반 디비 entityManager 를 사용하고, func2 함수에서는 bo 디비 entityManager 를 사용하고 있었고, 다음과 같이 각 함수에서 생성자에 주입된 entityManager 를 다른 entityManager 로 재할당 해줘서 사용하고 있었다.
그리고 func1 은 실행되는데 5초가 걸린다고 하고 호출 순서는 func1 -> func2 라고 해보자.
그렇게 된다면 일반 디비 entityManager 를 주입해줘서 func1 을 실행하는 도중에 func2 가 실행된다면 주입되는 entityManager 는 bo entityManager 로 재할당이 되어 버려서 원하는 테이블 혹은 sp 를 찾을 수 없다라는 에러
가 발생한다.
func1 에서 사용하는 entityManager 가 function scope 안에서 선언되어서 사용된 객체가 아니기 때문에, func2 가 실행되는 도중에 재할당을 시켜버리면 func1 에서도 bo entityManager 를 사용하게 되는 것이었다.
쿼리 결과에 대한 validator 체크를 해주는 queryMany()
함수를 사용하고자 하면, 주입된 entityManager 를 바꾸지 말고 하나만 사용을 해야 한다.
하나의, 수정되지 않는 entityManager 를 사용하기 위해서는 기존에 사용하고 있던 queryMany()
를 수정하는 것 보다는 새로 만드는 것이 더 효율적이라고 판단했다.
애초에 기존에 사용하고 있는 queryMany()
를 수정하면 해당 함수를 사용하고 있는 모든 부분을 찾아서 수정을 해양하고, 하나의 repository 에서 두개 이상의 entityManager 를 주입받아서 사용하는 경우도 흔치 않기 때문이다.
그래서 새로 만든 함수는 다음과 같다.
protected async queryManyForPickDB<T extends object>(
entityManager: EntityManager, // 인스턴스의 생성자에 종속된 DB 말고 다른 DB 를 사용하고자 하는 함수가 있다면 해당 entityManager 를 주입시킴. ex) getDashboard
query: string,
parameters?: any[],
classConstructor?: ClassConstructor<T>,
): Promise<T[]> {
const queryResult = columnToCamel(
await entityManager.query(query, parameters),
);
if (!classConstructor) {
return queryResult;
}
return Promise.all(
queryResult.map((r: object) =>
this.validateReturnType(r, classConstructor),
),
);
}
특정 원하는 디비에 접근하기 위해서 인자값으로 entityManager 를 받았고, 이것으로 쿼리를 실행하는 함수이다. queryManyForPickDB
호출부를 보면
// user.repository.ts
/**
* @description default db connection : main db
*/
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
constructor(
@InjectEntityManager() private readonly EntityManager: EntityManager,
@InjectEntityManager('bo') private readonly boEntityManager: EntityManager,
) {
super(EntityManager);
}
async func1(): Promise<any> {
return this.queryManyForPickDB(
this.boEntityManager,
'select ~',
);
}
다음과 같이 super(EntityManager);
를 통해 기존 default 로 연결된 디비 말고 다른 디비 entityManager 를 인자값으로 넘겨줘서 쿼리를 실행시킬 수 있다.
생성자에 주입된 객체를 바꾸려고 하지 말자!