백엔드/NestJs

[nestjs] new DataLoader

STUFIT 2023. 7. 2. 22:03
반응형
  1. DataLoader??

DataLoader는 데이터 로딩 및 캐싱을 최적화하기 위한 유틸리티입니다. 주로 GraphQL에서 N+1 문제를 해결하고 중복 로드를 방지하기 위해 사용됩니다. DataLoader는 여러 개의 요청을 배치로 처리하여 데이터베이스나 외부 API와의 라운드트립 횟수를 최소화하고 성능을 향상시킵니다. 따라서 DataLoader는 데이터 로딩과 관련된 문제를 해결하기 위한 효과적인 도구이다.

DataLoader는 주로 데이터베이스에서 값을 가져오는 데 사용됩니다. 일반적으로 데이터베이스에서 쿼리를 실행하여 결과를 반환하고, DataLoader는 이러한 쿼리 결과를 캐싱하고 중복된 요청을 효율적으로 처리합니다. 따라서 DataLoader는 데이터베이스로부터 값을 가져오는 데 주로 사용됩니다.

하지만 DataLoader는 단순히 데이터베이스에만 제한되지 않습니다. DataLoader는 다른 데이터 소스에서 값을 가져오는 데에도 사용될 수 있습니다. 예를 들어, 외부 API 호출을 통해 데이터를 가져올 때에도 DataLoader를 사용하여 성능을 최적화할 수 있습니다. DataLoader는 데이터 로딩과 캐싱에 특화되어 있으므로, 어떤 데이터 소스에서든 효율적인 데이터 로딩을 위해 활용될 수 있습니다.

따라서 DataLoader는 데이터베이스 쿼리를 실행하여 결과를 가져오는 것은 물론, 다른 데이터 소스에서의 데이터 로딩에도 유용하게 사용될 수 있습니다. 주로 데이터베이스와의 상호 작용에 초점을 두고 설계되었지만, 다양한 데이터 소스에서 데이터를 가져올 때 성능 및 캐싱 측면에서 DataLoader를 활용할 수 있습니다.

  1. DataLoader vs QueryBuilder??

: 나는 데이터로더와 쿼리빌더가 같은 기능이라고 생각했기 때문에 해당 부분에 대해 차이점을 chat gpt에 물어봤었고 아래는 그에 대한 답변이다.

💡 DataLoader와 QueryBuilder는 각각 다른 상황에서 사용됩니다. 어떤 것을 사용해야 하는지는 상황과 요구사항에 따라 다릅니다. 이해를 돕기 위해 각각의 사용 사례를 설명하겠습니다.

  1. DataLoader 사용 사례:
    • N+1 문제 해결: GraphQL에서 여러 요청에 대해 데이터를 로드해야 할 때, DataLoader는 중복된 요청을 효율적으로 처리하고 성능을 최적화하는 데 도움을 줍니다. 데이터베이스 쿼리를 최적화하고 중복 로드를 방지하기 위해 사용됩니다.
    • 데이터 소스 캐싱: DataLoader는 요청된 데이터를 캐싱하여 중복된 요청에 대한 데이터 로드를 방지하고 성능을 향상시킵니다. 동일한 요청이 여러 번 발생하는 경우에 유용합니다.
  2. QueryBuilder 사용 사례:
    • 복잡한 쿼리 작성: QueryBuilder는 데이터베이스 쿼리를 동적으로 생성하는 데 사용됩니다. 복잡한 조건, 조인, 정렬 등을 처리할 때 유용합니다. SQL 또는 ORM과 함께 사용되며, 데이터베이스에 대한 직접적인 쿼리 작성을 간소화합니다.

따라서 데이터를 로드해야 할 때 DataLoader를 사용하는 것은 N+1 문제 및 데이터 소스 캐싱과 관련된 상황에서 유용합니다. 반면에 QueryBuilder는 데이터베이스에 대한 복잡한 쿼리 작성이 필요한 경우에 사용됩니다.

일반적인 DB 조회 작업에서는 QueryBuilder를 사용하여 쿼리를 작성하고 데이터를 가져올 수 있습니다. DataLoader는 N+1 문제와 캐싱을 다루는 데 특화되어 있으므로, 여러 요청에서 동일한 데이터를 중복 로드하는 경우나 성능 최적화가 필요한 경우에 활용됩니다.

따라서 DB 조회 작업에서 DataLoader를 사용할 필요는 없지만, N+1 문제가 발생하거나 중복 로드를 방지하고자 할 때 DataLoader를 고려할 수 있습니다. QueryBuilder는 복잡한 쿼리 작성 및 데이터베이스 조작에 더 적합한 도구입니다

 

  1. N+1 문제가 뭔데??

<aside> 💡 N+1 문제는 데이터베이스 조회에서 성능 문제가 발생하는 상황을 나타내는 용어입니다. 이해를 돕기 위해 다음 예시를 사용하여 설명하겠습니다.

가정하에 사용자와 그들의 게시물을 저장하는 데이터베이스가 있다고 가정해봅시다. 먼저 사용자(User)와 게시물(Post) 간의 관계는 다음과 같습니다:

  • 하나의 사용자(User)는 여러 개의 게시물(Post)을 작성할 수 있습니다.
  • 각 게시물(Post)은 하나의 사용자(User)에 속해야 합니다.

이제 사용자 목록과 각 사용자의 게시물을 조회하는 작업을 생각해봅시다. N+1 문제가 발생하는 상황은 다음과 같습니다:

  1. 첫 번째 단계: 사용자 목록 조회
    • 데이터베이스에서 모든 사용자(User)를 가져옵니다. (1번 데이터베이스 쿼리)
    • 결과로 사용자 목록이 반환됩니다.
  2. 두 번째 단계: 각 사용자별 게시물 조회
    • 사용자 목록을 순회하면서 각 사용자(User)에 대해 해당 사용자의 게시물(Post)을 가져옵니다. (N번 데이터베이스 쿼리)
    • 결과로 각 사용자별 게시물 목록이 반환됩니다.

위의 예시에서 N은 사용자의 수를 나타냅니다. 이 경우 데이터베이스 조회는 1번의 사용자 목록 조회 쿼리와 N번의 각 사용자별 게시물 조회 쿼리로 구성됩니다.

이렇게 N+1 번의 데이터베이스 쿼리가 발생하는 상황에서는 성능 문제가 발생할 수 있습니다. 각각의 추가 쿼리는 네트워크 오버헤드와 데이터베이스 부하를 초래하므로 처리 시간이 증가하고 응답 속도가 저하될 수 있습니다.

N+1 문제를 해결하기 위한 방법 중 하나는 DataLoader를 사용하는 것입니다. DataLoader는 중복된 요청을 배치로 처리하여 데이터베이스 쿼리 수를 최소화하고 성능을 향상시킵니다. 사용자 목록 조회 시에 모든 사용자에 대한 게시물을 한 번의 요청으로 가져오도록 DataLoader를 구성하면 N+1 문제를 효과적으로 해결할 수 있습니다.

따라서 N+1 문제는 데이터베이스 조회에서 성능 문제가 발생하는 상황을 나타내며, DataLoader와 같은 도구를 사용하여 중복된 쿼리를 최소화하고 성능을 최적화할 수 있습니다.

</aside>

  1. 예제

<aside> 💡

다음과 같은 데이터베이스 구조를 갖는 예제를 사용하겠습니다:

사용자(User) 테이블:

id name

1 John
2 Emily
3 Michael

게시물(Post) 테이블:

id title user_id

1 First Post 1
2 Second Post 1
3 Third Post 2
4 Fourth Post 3
5 Fifth Post 3
  1. N+1 문제:

다음은 사용자 목록과 각 사용자의 게시물을 조회하는 코드입니다. QueryBuilder를 사용하여 데이터베이스 조회를 수행합니다.

const users = await userRepository.createQueryBuilder('user')
  .getMany();

for (const user of users) {
  const posts = await postRepository.createQueryBuilder('post')
    .where('post.user_id = :userId', { userId: user.id })
    .getMany();

  user.posts = posts;
}

console.log(users);

위의 코드에서 사용자 목록 조회를 위해 userRepository에서 모든 사용자를 가져옵니다. 그런 다음 각 사용자별로 postRepository를 통해 해당 사용자의 게시물을 가져옵니다. 이렇게 되면 사용자 수에 따라 추가적인 쿼리가 발생하게 되어 N+1 문제가 발생합니다.

  1. DataLoader를 사용한 해결:

이번에는 DataLoader를 사용하여 N+1 문제를 해결하는 코드를 살펴보겠습니다. DataLoader를 설정하고, 데이터 로드를 위한 함수를 정의합니다.

const userLoader = new DataLoader(async (userIds) => {
  const users = await userRepository.createQueryBuilder('user')
    .whereInIds(userIds)
    .getMany();

  return userIds.map(userId =>
    users.find(user => user.id === userId)
  );
});

const users = await userRepository.createQueryBuilder('user')
  .getMany();

for (const user of users) {
  const posts = await postLoader.load(user.id);

  user.posts = posts;
}

console.log(users);

위의 코드에서는 userLoader라는 DataLoader 인스턴스를 생성하고, userIds를 기반으로 사용자 데이터를 로드하는 함수를 정의합니다. 이후 사용자 목록 조회를 위해 일반적인 QueryBuilder를 사용하여 모든 사용자를 가져옵니다. 그리고 각 사용자의 게시물을 postLoader를 통해 로드합니다.

postLoader는 user.id를 기반으로 해당 사용자의 게시물을 한 번의 요청으로 가져오도록 DataLoader를 설정합니다.

DataLoader는 중복된 요청을 배치로 처리하여 성능을 향상시킵니다. 여러 개의 요청이 동일한 데이터를 요구할 때, DataLoader는 중복된 요청을 감지하고 단일 요청으로 변환하여 데이터베이스나 다른 데이터 소스에서 한 번에 처리합니다. 이를 통해 데이터베이스의 부하를 줄이고 효율적인 데이터 로딩을 구현할 수 있습니다.

예를 들어, 위의 예시에서 **postLoader**는 사용자의 게시물을 로드하기 위해 DataLoader를 설정한 것입니다. 여러 사용자가 각각의 게시물을 요청하는 경우, DataLoader는 중복된 사용자에 대한 요청을 감지하고 단일 요청으로 변환하여 게시물을 한 번에 가져옵니다. 이는 데이터베이스에 대한 쿼리 수를 줄이고 데이터베이스 연산을 최소화하여 성능을 향상시킵니다.

**postLoader.load(user.id)**를 호출하면 DataLoader는 이미 로드된 게시물에 대한 캐시를 확인하고 존재하는 경우 해당 결과를 반환합니다. 만약 캐시에 없는 경우에는 일괄적으로 요청된 게시물을 한 번의 쿼리로 가져와서 캐시에 저장한 후 반환합니다. 이렇게 함으로써 중복된 쿼리를 피하고 효율적으로 데이터를 로드할 수 있습니다.

DataLoader는 내부적으로 요청된 키들을 수집하여 배치로 묶은 후, 정의된 데이터 로딩 함수를 호출합니다. 함수 내에서는 요청된 키에 해당하는 데이터를 로드하고 캐시에 저장합니다. 캐시에 이미 로드된 데이터가 있는 경우에는 캐시에서 데이터를 반환하여 중복된 요청을 효율적으로 처리합니다.

따라서 DataLoader는 중복된 요청을 배치 처리하고 데이터 소스의 부하를 최소화하여 성능을 향상시키는 역할을 합니다. 이를 활용하여 N+1 문제를 해결하고 데이터 로딩을 최적화할 수 있습니다.

 

반응형