[댕댕워크] Promise.all로 API 성능 개선하기

acho·2024년 7월 9일
2

Promise란

프로미스는 비동기로 실행되는 작업의 완료 상태와 리턴값, 완료시 실행될 콜백 함수를 저장하는 객체입니다.
비동기 작업이 완료되기 전에 미리 생성할 수 있어 비동기 함수가 동기 함수처럼 값을 리턴할 수 있게 해줍니다.
프로미스의 상태(State)는 다음의 세 가지 값을 가질 수 있습니다 :

상태설명
pending(대기)이행하지도, 거부하지도 않은 초기 상태
fulfilled(이행)연산이 성공적으로 완료됨
rejected연산이 실패함

상태와 비슷하지만 다른 개념인 Fate도 있습니다 :

Fate설명
resolved이행하거나 거부한다고 해도 아무런 영향을 미치지 않는 상태. 즉 프로미스가 이미 fullfilled / rejected 상태이거나 다른 프로미스에 'locked in' 되어있음
unresolved상태가 변화할 여지가 남아있음 i.e. 이행하거나 거부하려는 시도가 프로미스에 영향을 미칠 수 있음

프로미스의 생성

const promise = new Promise((resolve, reject) => {
	if(/* 비동기 처리 성공 */) {
		resolve('result');
	} else { /* 비동기 처리 실패 */
		reject('failure reason');
	}
});
  • 프로미스 생성자는 프로미스가 해결됐을 때 실행할 콜백함수인 resolve, 실패했을 때 실행할 콜백함수인 reject를 인자로 받습니다. resolve, reject는 자바스크립트에서 제공하는 함수로 사용자가 정의할 필요는 없습니다.
  • resolve / reject 함수의 인자로 실제 실행하고 싶은 로직을 넣으면 됩니다.

async / await

프로미스를 리턴하는 함수를 여러 개를 호출해야 하는데, 각 함수의 호출 순서가 중요하다면 - 즉 a 함수가 b 함수에 의존한다면, b 함수의 처리 이후에 a 함수가 처리되어야 할 수 있습니다. 이처럼 비동기 작업이지만 동기적으로 처리되어야 하는 상황이 빈번히 발생합니다.
이럴 때 async / await를 사용하면 프로미스를 동기 처리처럼 사용할 수 있습니다. 프로미스를 리턴하는 함수 앞에 await 키워드를 붙이면 됩니다.

await this.callDatabase();

await 키워드는 받은 프로미스가 완료될 떄까지 기다렸다가 resolve한 처리 결과를 반환합니다.
await 키워드는 async 키워드가 붙은 함수에서만 사용할 수 있습니다.

Promise.all

Promise.all은 반대로 프로미스를 병렬로 실행하고 싶을 때 사용합니다. 사실 비동기 작업은 그대로 두어도 병렬로 실행되지만, Promise.all을 사용하면 여러 프로미스를 하나로 묶어 모든 프로미스가 완료될 때까지 기다린 뒤 그 리턴값을 사용할 수 있습니다.
Promise.all은 프로미스의 정적 메소드로 프로미스의 배열(이터러블)을 인자로 받아 하나씩 순회합니다. 만약 하나라도 실패하면 실행을 중단하고 에러를 리턴합니다. 모두 성공시 각 프로미스의 리턴값을 담은 하나의 프로미스를 리턴합니다.
이와 비슷하게 Promise.allsettled 라는 메소드도 있습니다. Promise.all과의 차이점은 Promise.all은 중간의 하나의 프로미스라도 실패하면 바로 중단하고 에러를 리턴하는 반면 Promise.allsettled는 중간에 reject 되는 프로미스가 있더라도 프로미스 이터러블을 끝까지 순회한 뒤에 에러를 리턴한다는 것입니다.

코드에 적용해보기

이전 코드에서는 쿼리를 모두 async/await를 사용해 날리고 있었습니다. 즉 각각의 쿼리가 순차적으로 실행되었습니다.

    async getJournalDetail(journalId: number): Promise<JournalDetail> {
        const start = Date.now(); //시작 시간 카운트
        const journalDogIds = await this.journalsDogsService.getDogIdsByJournalId(journalId, start);

        const journalInfoRaw = await this.getJournalInfoForDetail(journalId, start);
        const photoUrlsRaw = await this.journalPhotosService.getPhotoUrlsByJournalId(journalId, start);
        const dogInfoRaw = await this.getDogsInfoForDetail(journalDogIds, start);
        const excrements = await this.excrementsService.getExcrementsCount(journalId, journalDogIds, start);

...
        return new JournalDetail(journalInfo, dogInfo, excrementsInfo);
    }
  • 쿼리를 실행하는 모든 메소드를 await로 순차 실행하고 있습니다.
    async getJournalInfoForDetail(journalId: number, start: number): Promise<Partial<Journals>> {
        const result = this.journalsRepository.findOne({
            where: { id: journalId },
            select: JournalInfoForDetail.getKeysForJournalTable(),
        });
        console.log('2 :', Date.now() - start);
        return result;
    }
  • 각 함수가 종료될 때 첫 함수 호출로부터의 소요 시간을 로그에 찍어보겠습니다.

결과

1 : 17
2 : 18
3 : 30
4 : 46
5 : 84
  • 순차적으로 실행되어 아래로 갈수록 차곡차곡 누적되는 걸 볼 수 있습니다.

   async getJournalDetail(journalId: number): Promise<JournalDetail> {
        const start = Date.now();
        const journalDogIds: number[] = await this.journalsDogsService.getDogIdsByJournalId(journalId, start);

        const [journalInfoRaw, photoUrlsRaw, dogInfoRaw, excrements]: [
            Partial<Journals>,
            Partial<JournalPhotos[]>,
            Partial<Dogs[]>,
            any[],
        ] = await Promise.all([
            this.getJournalInfoForDetail(journalId, start),
            this.journalPhotosService.getPhotoUrlsByJournalId(journalId, start),
            this.getDogsInfoForDetail(journalDogIds, start),
            this.excrementsService.getExcrementsCount(journalId, journalDogIds, start),
        ]);

...

        return new JournalDetail(journalInfo, dogInfo, excrementsInfo);
    }
  • 결과 값이 다른 쿼리의 인자로 쓰이는 쿼리 하나만 await으로 처리하고, 의존성을 갖지 않는 나머지 함수들은 Promise.all로 병렬 처리하였습니다.
  • 중간에 하나라도 에러가 나면 뒤의 쿼리를 실행할 필요가 없어 Promise.allsetteled가 아닌 Promise.all을 사용했습니다.

결과

1 : 7
2 : 8
3 : 14
5 : 15
4 : 16
  • 소요 시간이 현저히 줄어들었습니다. 병렬로 처리되어 누적되지 않고 각각 실행되어 종료되었기 때문입니다.

이를 통해 API 성능을 30ms -> 21ms로 30% 개선할 수 있었습니다.

reference:
mdn 프로미스 문서
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
자바스크립트 딥 다이브

0개의 댓글

관련 채용 정보