Promise.all 을 활용한 API 성능 개선

강 진성·2025년 4월 20일

문제상황


게임 데이터 조회 API 를 개발하면서 성능 이슈를 겪고 있었다. 사용자 정보와 최근 경기 기록을 불러오는 과정이 생각보다 오려 걸려서 화면 로딩이 느려지는 문제가 있었다. 특히 MongoDB 쿼리를 여러 번 순차적으로 실행하면서 시간이 많이 소요되었다.


해결과정


처음 해결해보려고 했던 방법

일단 tanstack query 에서 placeholder data 를 제공해준다.

const {
    data: userDetail = [],
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ["userDetail", player?.name],
    queryFn: () => fetchPlayerDetail(player?._id),
    enabled: !!player?._id,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
    staleTime: 1000 * 60 * 5,
    placeholderData: keepPreviousData,
  });

이처럼 placeholderData 를 쓰면 새 데이터가 로딩되기 전까지 "잠깐 대체용"으로 이전 데이터를 보여주는 역할을 한다. 그래서 로딩 상태를 안 보이게 할 수 있다.

이러면서 UX 를 개선할 수 있지만 단순 눈속임일 뿐이라 생각했다. 실질적으로 데이터 통신을 더 빠르게 할 수 있는 방법이 없나 검색해보다가 Promise 를 써서 병렬 처리를 하면 좀 더 빠르게 데이터 통신을 할 수 있다고 알았다.


기존 방식: 순차적 데이터 로딩

처음에는 아래와 같이 순차적으로 데이터를 불러오는 방식을 사용했다.

  1. 사용자 기본 정보 조회

  2. 가장 많이 플레이한 챔피언 조회

  3. 최근 5경기 데이터 조회

  4. 최근 5경기 승률 데이터 조회

// 유저 기본 정보 조회
const user = await User.findById(id)
  .select("name nickName position eloRating")
  .lean();

// 유저의 가장 많이 플레이한 챔피언
const mostPlayedChampion = await Match.aggregate([
  // 집계 파이프라인...
]);

// 유저의 최근 5경기
const recentMatches = await Match.find({
  "players.userNickname": user.nickName,
})
  .sort({ createdAt: -1 })
  .limit(5);

// 유저의 최근 5경기의 승률
const recentMatchesData = await Match.aggregate([
  // 집계 파이프라인...
]);

이렇게 순차적으로 데이터를 불러오면 한 쿼리가 완료된 후 다음 쿼리가 실행되어 전체 처리 시간이 길어졌다.

성능 측정

순차적 처리 방식의 성능을 측정하기 위해 console.timeconsole.timeEnd 를 사용했다.

console.time("순차적");
// 모든 순차적 쿼리 실행
console.time("순차적");

10번의 테스트 결과, 순차적 처리 방식은 평균적으로 다음과 같은 시간이 소요되었다.

  • 208ms, 180ms, 205ms, 204ms, 210ms, 219ms, 208ms, 204ms, 209ms, 209ms
  • 평균: 205.6ms

개선 방식: Promise.all 을 활용한 병렬 처리

성능 개선을 위해 Promise.all 을 사용하여 여러 쿼리를 병렬로 실행하는 방식으로 변경했다.

// 유저 정보는 먼저 따로 불러와야 함
const user = await User.findById(id)
  .select("name nickName position eloRating")
  .lean();

// 닉네임을 기준으로 다른 쿼리를 병렬 처리
const [mostPlayedChampionAgg, recentMatches, recentMatchesDataAgg] = 
  await Promise.all([
    Match.aggregate([/* 첫 번째 쿼리 */]),
    Match.find({ /* 두 번째 쿼리 */ }),
    Match.aggregate([/* 세 번째 쿼리 */])
  ]);

병렬 처리 방식의 성능을 동일하게 측정했다.

console.time("병렬")
// 병렬 쿼리 실행
console.timeEnd("병렬");

10번의 테스트 결과, 병렬 처리 방식은 다음과 같은 시간이 소요되었다.

  • 127ms, 86ms, 130ms, 127ms, 124ms, 126ms, 124ms, 127ms, 108ms, 126ms
  • 평균: 120.5ms

성능 비교 결과

두 방식의 성능을 비교한 결과:

  • 순차적 처리: 평균 205.6ms
  • 병렬 처리: 평균 120.5ms
  • 개선율: 약 41.4% 향상

개선 원리: Promise.all 의 활용


Promise.all 메서드는 여러 프로미스를 병렬로 실행하고 모든 프로미스가 완료되었을 때 결과를 반환한다. 이를 활용하면 서로 의존성이 없는 여렁 데이터베이스 쿼리를 동시에 실행할 수 있다.

주의할 점은 병렬 처리를 하려면 쿼리 간의 의존성을 파악해야 한다는 것이다. 내 경우에는 유저 기본 정보(특히 닉네임)가 다른 쿼리에 필요했기 때문에 유저 정보만 먼저 조회한 후, 나머지 세 가지 쿼리를 병렬로 처리했다.

// 1. 유저 정보 먼저 조회 (다른 쿼리들이 이 정보에 의존)
const user = await User.findById(id).lean();

// 2. 나머지 쿼리들은 병렬로 처리
const [query1Result, query2Result, query3Result] = await Promise.all([
  // 여러 쿼리 동시 실행
]);

결론


Promise.all을 활용한 병렬 처리는 API 성능을 크게 향상시킬 수 있는 간단하면서도 효과적인 방법이다. 내 프로젝트에서는 약 41%의 성능 향상을 달성했고, 이는 사용자 경험 개선으로 이어졌다.

특히 여러 개의 독립적인 데이터베이스 쿼리가 필요한 경우, 순차적 처리 대신 병렬 처리를 고려해보는 것이 좋다. 다만 쿼리 간의 의존성을 파악하여 필요한 경우 일부 쿼리는 순차적으로 실행해야 한다는 점을 명심해야 한다.

profile
완전완전완전초보초보초보

0개의 댓글