[javascript] promise.all()로 소요 시간 7배 줄이기

김지엽·2023년 11월 1일
1
post-thumbnail
post-custom-banner

1. 개요

최근에 개인프로젝트에서 구현한 기능 코드를 부트캠프 튜터님께 코드 리뷰를 부탁드렸고 피드백을 받았다. 피드백의 내용 중 하나는 promise all을 사용해서 코드의 효율성을 높이는 것이였다.

이번에 수정할 코드는 다음과 같다.

async createRecommendGenre(webtoonId: string): Promise<string[]> {
    const genreCounter: { [genre: string]: number } = {};

    // 7번의 추천으로 각 키워드 마다 빈도 수 세기
    for (let i=0; i<7; i++) {
        const genreText = await this.createRecommendGenreText(webtoonId);
        const genres = genreText.split(" ");

        genres.forEach((genre) => {
            if (genre in genreCounter) genreCounter[genre] += 1;
            else genreCounter[genre] = 1;
        });
    }

    ...
}

2. Promise all

먼저 코드를 리팩토링 하기전에 promise all에 대해 다시 학습했다.

- 개념

비동기 처리(promise)를 병렬로 동시에 작동하도록 하는 자바스크립트 문법이다. 예제를 통해 살펴보자.

await test();
await test(); 
await test();

위와 같이 test라는 프로미즈를 반환하는 비동기 함수가 있고, 그 test의 처리가 끝나야 다음 작업을 처리할 수 있는 경우 await를 사용해야 한다. 하지만, await test()는 3개이고 3개를 따로따로 기다려줄 필요는 없다. 따라서 다음과 같이 수정한다.

await Promise.all([
    test(),
    test(),
    test()
]);

위의 promise all을 도입한 코드는 test()의 처리가 끝나기까지 기다리지만 test() 3개를 동시에 처리한다. 이전의 코드와 결과는 같지만 성능은 3배나 빨라진다.

- 언제 사용할까?

개념만 보면 promise all은 무조건 좋은 것 같지만 제약이 있다. 병렬로 작업을 처리하는 것은 순서가 상관없을 때의 경우이다.

이전 포스팅에서 자바스크립트의 forEach와 for of를 비교할때도 비슷한 주제가 나왔다.

따라서 각각의 비동기 처리가 다른 것과 관련이 없는 또는 순서상으로 상관없는 작업을 진행할때 promise all을 사용하는 것이 적합하다.

3. 리팩토링

직접적인 성능과 관련된 수정이므로 promise all을 도입하는 것이 유의미한 결과가 있는지를 확인하기 위해 성능도 같이 테스트를 한다.

- 리팩토링 이전

async createRecommendGenre(webtoonId: string): Promise<string[]> {
    const genreCounter: { [genre: string]: number } = {};

     const before = new Date().getTime();

      // 7번의 추천으로 각 키워드 마다 빈도 수 세기
      for (let i=0; i<7; i++) {
          const genreText = await this.createRecommendGenreText(webtoonId);
          const genres = genreText.split(" ");

          genres.forEach((genre) => {
              if (genre in genreCounter) genreCounter[genre] += 1;
              else genreCounter[genre] = 1;
          });
      }

      const now = new Date().getTime();
      console.log(`결과: ${JSON.stringify(genreCounter)}\n소요 시간: ${now - before}`);

    ...
}

- 리팩토링 이후

async createRecommendGenre(webtoonId: string): Promise<string[]> {
    const genreCounter: { [genre: string]: number } = {};

     const before = new Date().getTime();

      // 7번의 추천으로 각 키워드 마다 빈도 수 세기
      const genreText = await Promise.all<string>([
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId),
          this.createRecommendGenreText(webtoonId)
      ]);

      const genres = genreText.join(" ").split(" ");
      genres.forEach((genre) => {
          if (genre in genreCounter) {
              genreCounter[genre] += 1;
          } else {
              genreCounter[genre] = 1;
          }
      });

      const now = new Date().getTime();
      console.log(`결과: ${JSON.stringify(genreCounter)}\n소요 시간: ${now - before}`);

    ...
}

- 결과 비교

리팩토링 이전

리팩토링 이후

결과: 10.4초 -> 1.6초

물론, api요청으로 인한 소요 시간은 매번 다르기 때문에 100% 정확하게 측정은 힘들지만 약 6~7배 단축시켰다.

문제점

- 문제 발생

const genreText = await Promise.all<string>([
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId),
    this.createRecommendGenreText(webtoonId)
]);

위와 같은 코드는 this.createRecommendGenreText(webtoonId)를 단순히 7번 적어준 것이였는데, 이렇게 하면 가독성수정에도 안좋게 보이기 때문에 다음과 같이 수정해주었다.

const genreText = await Promise.all<string>(
    new Array(7).fill(this.createRecommendGenreText(webtoonId))
);

하지만 예상치 못한 결과가 발생했다.

this.createRecommendGenreText(webtoonId)는 매번 다른 결과를 반환하는 것이 보통인데 7번 전부 동일한 값을 반환해서 카운팅이 모두 7씩 된것이다.

- 이유

위와 같은 결과가 발생한 이유는 Array(7).fill(...)에서 fill이 기능하기 이전에 내부에 있는 this.createRecommendGenreText(webtoonId)가 먼저 호출되었기 떄문이다. 따라서 전부 동일한 값이 발생한다.

- 해결

const promiseArray = new Array(7).fill(() => this.createRecommendGenreText(webtoonId)).map((f) => f());
const genreText = await Promise.all<string>(promiseArray);

해결 방법은 다음과 같다.

  1. fill이 기능하기 이전에 내부 함수가 먼저 호출되지 않도록 함수형으로 넣어준다.
  2. map을 통해 각 함수를 실행시켜 프로미즈 배열을 반환시킨다.

참고

Promise All

profile
욕심 많은 개발자
post-custom-banner

0개의 댓글