댓글 검색 목록

[javascript] 고급 Promise 패턴 : Promise Memoization

페이지 정보

작성자 운영자 작성일 21-01-15 16:31 조회 704 댓글 0

이 게시물에서는 일반적인 캐싱 구현이 경쟁 조건을 어떻게 노출하는지 보여줍니다. 그런 다음이 문제를 해결하는 방법을 보여주고 (대부분의 최적화와 달리) 프로세스에서 코드를 단순화합니다.


Singleton Promise 패턴을 기반으로 하는 Promise Memoization 패턴을 도입하여 이를 수행합니다.


https://www.jonmellman.com/posts/promise-memoization


사용 사례 : 비동기 결과 캐싱 


다음과 같은 간단한 API 클라이언트를 고려하십시오.


const getUserById = async (userId: string): Promise<User> => {
  const user = await request.get(`https://users-service/${userId}`);
  return user;
};


매우 간단합니다.


하지만 성능이 문제라면 어떻게 해야 합니까? 사용자 서비스가 사용자 세부 정보를 확인하는 속도가 느릴 수 있으며 동일한 사용자 ID 집합으로 이 메서드를 자주 호출 할 수 있습니다.


아마도 캐싱을 추가하고 싶을 것입니다. 어떻게 할 수 있습니까?


나이브 솔루션 


내가 자주 보는 순진한 솔루션은 다음과 같습니다.


const usersCache = new Map<string, User>();

const getUserById = async (userId: string): Promise<User> => {
  if (!usersCache.has(userId)) {
    const user = await request.get(`https://users-service/${userId}`);
    usersCache.set(userId, user);
  }

  return usersCache.get(userId);
};


매우 간단합니다. 사용자 서비스에서 사용자 세부 정보를 확인한 후 결과로 인 메모리 캐시를 채 웁니다.


경쟁 조건 (다시) 


이 구현의 문제점은 무엇입니까? 내 Singleton Promises 게시물과 동일한 경쟁 조건이 적용됩니다.


특히 다음과 같은 경우 중복 네트워크 호출을 수행합니다.

await Promise.all([
  getUserById('user1'),
  getUserById('user1')
]);


문제는 첫 번째 호출이 해결 될 때까지 캐시를 할당하지 않는다는 것입니다. 하지만 결과를 얻기 전에 캐시를 어떻게 채울 수 있습니까?


Singleton Promises to the Rescue 


결과 자체가 아니라 결과에 대한 약속을 캐시하면 어떨까요? 코드는 다음과 같습니다.


const userPromisesCache = new Map<string, Promise<User>>();

const getUserById = (userId: string): Promise<User> => {
  if (!userPromisesCache.has(userId)) {
    const userPromise = request.get(`https://users-service/v1/${userId}`);
    userPromisesCache.set(userId, userPromise);
  }

  return userPromisesCache.get(userId)!;
};


매우 유사하지만 네트워크 요청을 기다리는 대신 약속을 캐시에 넣고 호출자 (결과를 기다리는 사람)에게 반환합니다.


더 이상 await를 호출하지 않기 때문에 더 이상 메서드 async를 선언하지 않습니다. 메서드 서명은 변경되지 않았습니다. 우리는 여전히 promise를 반환하지만 동시에 그렇게 합니다. (혼란스러워 보이면 실험을 실행하는 것이 좋습니다.)


이것은 경쟁 조건을 수정합니다. 타이밍에 관계없이 getUserById ( 'user1')를 여러 번 호출하면 하나의 네트워크 요청 만 실행됩니다. 이는 모든 후속 호출자가 첫 번째 호출과 동일한 단일 약속을 받기 때문입니다.


문제 해결됨! 이제 한 걸음 물러서 봅시다.



Promise Memoization 


다른 각도에서 보면 마지막 캐싱 구현은 문자 그대로 getUserById를 모하는 것입니다! 우리가 이미 본 입력이 주어지면 우리는 저장 한 결과를 반환합니다.


따라서 비동기 메서드를 메모하면 경쟁 조건 없이 캐싱이 가능해졌습니다.


이 통찰력의 장점은 lodash를 포함하여 메모를 간단하게 만드는 많은 라이브러리가 있다는 것입니다.


즉, 마지막 솔루션을 단순화하여 다음을 수행 할 수 있습니다.


import _ from 'lodash';

const getUserById = _.memoize(async (userId: string): Promise<User> => {
  const user = await request.get(`https://users-service/${userId}`);
  return user;
});


캐시가 없는 원래 구현을 가져와 _.memoize 래퍼에 넣었습니다! 매우 간단하고 비 침습적입니다.


(프로덕션 사용 사례의 경우 캐싱 전략을 제어 할 수 있는 메모와 같은 것을 원할 것입니다.)


결론 


이 게시물에서 우리는 비동기 메서드 주위에 메모 화 래퍼를 사용하는 방법을 보았습니다.


이 접근 방식은 결과 캐시를 수동으로 채우는 것보다 간단하지만 일반적인 경쟁 조건을 방지하기 때문에 더 낫다는 것도 알게 되었습니다.



댓글목록 0

등록된 댓글이 없습니다.

웹학교 로고

온라인 코딩학교

코리아뉴스 2001 - , All right reserved.