nest 에서 n+1 문제

Featured image

N+1문제

N+1문제는 데이터베이스 사용시 성능이 저하되는 문제로 orm 사용할때 발생 한번의 쿼리로 여러 데이터를 조회할때 추가적으로 n개의 쿼리를 발생시키는 것을 말한다 이로인해 로우 쿼리와 달리 불필요한 다중쿼리를 발생시켜 성능이 저하된다

카테고리 조회 후 서브 카테고리를 별도의 쿼리로 조회하게 된다 -> n+1문제가 발생

//TODO:  카테고리 전체를 조회 해당 카테고리 하나와와 해당 카테고리에 딸린 서브 카테고리만 조회된다
  async findOneCategory(categoriesId: number) {
    const findOne = await this.categoriesRepostiory.findOne({
      where: { categoriesId, deleteAt: null },
    });
    if (!findOne) {
      throw new HttpException('존재하지 않는 카테고리 입니다', HttpStatus.NOT_FOUND)
    }
    return {
      categoryId: findOne.categoriesId,
      categoryName: findOne.categoriesName,
      subCategories: findOne.subCategories.map((subCategory) => ({
        subCategoryId: subCategory.subcategoriesId,
        subCategoryName: subCategory.subCategoryName,
      })),
    };
  }

원인은 orm이 데이터를 가져올때 연관된 데이터를 따로 조회하는 방식을 사용해 데이터를 별도로 쿼리하고 객체를 처음 로드할때 연관돤 데이터를 미리 가져오지 않고 요청이 들어왔을때 쿼리를 실행하는 레이지로딩이 이루어진다

relation을 설정하지 않아도 select로 결정하지 않으면 전부 한꺼번에 가져오는게 아닌가?

이는 orm의 동작방식에 대한 흔한 오해중 하나로 orm은 관계된 엔티티를 자동으로 가져오지 않는다(lazy loading)

관계된 데이터에 접근할때만 추가적으로 쿼리가 실행된다 -> n+1 이 발생!

해결방법

  1. Eager Loading (즉시 로딩)

Eager Loading은 연관된 데이터를 미리 한 번의 조인 쿼리로 가져오는 방식 여러변 나눠져서 가제오게 하는 것이 아니라 하나의 쿼리로 한 꺼번에 가져오는 방식

relations: [‘subCategories’], // Eager Loading을 통해 서브 카테고리도 함께 조회 이렇게 사용

구현이 쉽고 필요한 데이터를 한번의 쿼리로 가져올 수 있다 하지만 항상 모든 관계를 로드하기 때문에 불필요한 데이터를 가져올 수 있다

  1. Join Queries (QueryBuilder 사용)

쿼리 빌더를 사용해 좀 더 세밀한 쿼리제어가 가능하다


  async findOneCategory(categoriesId: number) {
    const findOne = await this.categoryRepository
      .createQueryBuilder('category')
      .leftJoinAndSelect('category.subCategories', 'subCategory')
      .where('category.categoriesId = :categoriesId', { categoriesId })
      .andWhere('category.deleteAt IS NULL')
      .getOne();

    if (!findOne) {
      throw new HttpException('존재하지 않는 카테고리 입니다', HttpStatus.NOT_FOUND);
    }

    return {
      categoryId: findOne.categoriesId,
      categoryName: findOne.categoriesName,
      subCategories: findOne.subCategories.map((subCategory) => ({
        subCategoryId: subCategory.subcategoriesId,
        subCategoryName: subCategory.subCategoryName,
      })),
    };
  }

쿼리 빌더를 사용하면 복잡한 쿼리를 작성가능해 자세한 쿼리 작성이 가능하지만 코드가 복잡해지고 로우쿼리에 복잡해지기 때문에 orm의 이점이 줄어들 수 있다

  1. 엔티티 관계 설정에서 Eager Loading 사용
@OneToMany(() => SubCategory, subCategory => subCategory.category, { eager: true })
  subCategories: SubCategory[];

엔티티를 조회할 떄마다 자동으로 관련 엔티티를 로드한다 하지만 모든 쿼리에서 항상 관련 엔티티를 로드하기 떄문에 성능 저하가 발생하고 필요하지 않을때에도 데이터를 로드한다는 단점이 있다