providers instance 생성자에 주입된 객체 들을 건들면 안되는 이유

Jae Min·2024년 1월 2일
0
post-thumbnail

overview

nestjs 에서 class 를 인스턴스화 시켜서 언제 어디서 사용해도 동일한 값을 이용하기 위해 constructor() 안에 사용하고 싶은 객체를 주입해서 주로 사용한다.

여러 예시가 있지만, 이 글에서는 db 에 접근하는 layer 인 repository 에서 어떻게 주입해서 사용하는지 코드를 통해 확인해보자.

code

// 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 로 주입시켜서 쿼리를 실행하는 구조이다.

problem

하나의 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 를 인자값으로 넘겨줘서 쿼리를 실행시킬 수 있다.

생성자에 주입된 객체를 바꾸려고 하지 말자!

profile
자유로워지고 싶다면 기록하라.

0개의 댓글