함수형 프로그래밍 - 비동기 제어(3)

jiseong·2022년 7월 9일
0

T I Learned

목록 보기
288/291

이전 포스팅에서는 비동기적인 실행들이 순차적으로 진행되어 아래와 같은 상황에서 매우 비효율적인 코드가 되었었다. 그 뿐만 아니라 데이터베이스에 날리는 쿼리, 이미지 처리등등에서도 지금까지 배웠던 함수들을 활용할 수가 있는데 여러개의 요청을 동시에 날리는 경우에는 효율적이지 못한다고 생각이 든다. 그래서 이번에는 이를 순차적으로 동작하는 코드가 아닌 병렬적으로 코드가 동작될 수 있도록 변경하려고 한다.

const delay1000 = a => new Promise(resolve => {
  setTimeout(() => resolve(a), 1000);
});

go([1,2,3,4,5],
   L.map(a => delay1000(a*a)),
   L.filter(a => a % 2),
   reduce((a, b) => a + b),
   console.log);

순차적으로 진행되어 대략 5초정도의 시간이 걸리는 것을 볼 수 있다.

C.reduce

이제 병렬적으로 동작하는 C.reduce를 구현해보자.

const C = {};

C.reduce = curry((f, acc, iter) => iter ?
   reduce(f, acc, [...iter]) :
   reduce(f, [...acc]));

reduce에서 사용했던 인자들을 그대로 넘겨주는데 조금 다른 점은 iterable의 값들을 즉시 실행([...iter])시켜서 넘겨준다는 차이점이 있다.

그리고 실행결과를 보면 이렇게 함으로써 순차적으로 동작했던 방식이 병렬적으로 동작하게 되어 코드가 조금 더 효율적이게 동작한다.

(5초정도가 걸렸던 결과가 병렬적으로 동작하게 되어 1초만에 결과를 만들어내고 있다.)

그런데 아직 약간의 문제가 남아있다.
다음의 코드를 node 환경에서 실행시켜보면 오류가 발생하는 것을 볼 수 있다.

이는 반환되는 promise에서 .catch로 reject 처리를 하지 않은 경우에 발생하는 에러로 우리의 경우에는 이후에 catch해서 정리할 예정이기 때문에 비동기적으로 해당하는 에러를 catch 해줄 것이라는 것을 명시해줘야 에러가 발생하지 않는다.

catchNoop이라는 함수를 만들고 iterable을 인자로 넘겨주면 해당 함수에서 즉시 실행시켜주고 그 안에서 반복문을 돌면서 promise인 경우에 사전에 catch문을 작성함으로써 오류를 피할 수 있다.

function noop() {}
const catchNoop = ([...arr]) => 
  (arr.forEach(a => a instanceof Promise ? a.catch(noop) : a), arr);

C.reduce = curry((f, acc, iter) => iter ? 
  reduce(f, acc, catchNoop(iter)) :
  reduce(f, catchNoop(acc))
);

이제 정상적으로 동작한다.

C.take

결과값으로 만들어내는 함수인 take함수 역시 유사한 방식으로 작성할 수 있다.

C.take = curry((l, iter) => take(l, catchNoop(iter)));

C.take함수는 병렬적으로 실행되기 때문에 다음과 같은 코드에서 기존 take함수보다 빠르게 실행되는 것을 볼 수 있다.

  go([1,2,3,4,5],
    L.map(a => delay1000(a * a)),
    L.filter(a =>delay1000(a % 2)),
    L.map(a => delay1000(a + 1)),
    take(2),
    console.log)

이번에는 상황에 따라서 적절한 전략을 취할 수 있도록 특정 라인에서만 병렬적으로 평가하고 그 이후엔 그냥 실행하는 C.map, C.filter를 만들어 볼 것이다.

C.map

위에서 만들었던 C.take함수를 조합함으로써 기존의 L.map을 병렬적으로 처리할 수 있다.

C.takeAll = C.take(Infinity);
C.map = curry(pipe(L.map, C.takeAll));

이후 테스트해보면 병렬적으로 동작하는것을 볼 수 있다.

const delay300 = (a, name) => new Promise(resolve => {
  console.log(`${name}: ${a}`);
  setTimeout(() => resolve(a), 300);
});

C.map(a => delay300(a * a, 'CMap'), [1, 2, 3, 4]).then(console.log);

C.filter

C.filter도 동일하게 작성한다.

C.filter = curry(pipe(L.filter, C.takeAll));

이후 동일한 delay함수로 실행해보면 병렬적으로 동작하는것을 볼 수 있다.

C.filter(a => delay300(a % 2, 'CFilter'), [1, 2, 3, 4]).then(console.log);

이제 상황에 맞는 적절한 평가전략을 취할 수 있게 된다.

상황에 맞는 평가전략

사용할 기본 코드 예시

go([1, 2, 3, 4, 5, 6, 7, 8],
   map(a => delay300(a * a, 'map 1')),
   filter(a => delay300(a % 2, 'filter')),
   map(a => delay300(a + 1, 'map 2')),
   reduce((a, b) => a + b),
   console.log)

예시 1)

filter함수까지는 병렬적으로 처리하고 그 이후에는 다시 순차적으로 처리하는 방식으로 초반에 부하를 많이 주고 결과를 빠르게 얻는 예시를 들 수 있다.

go([1, 2, 3, 4, 5, 6, 7, 8],
   L.map(a => delay300(a * a, 'map 1')),
   C.filter(a => delay300(a % 2, 'filter')),
   L.map(a => delay300(a + 1, 'map 2')),
   reduce((a, b) => a + b),
   console.log)

예시 2)

아예 순차적으로 처리하는방식으로 평가를 최소화하는 예시를 들 수 있다.

go([1, 2, 3, 4, 5, 6, 7, 8],
   L.map(a => delay300(a * a, 'map 1')),
   L.filter(a => delay300(a % 2, 'filter')),
   L.map(a => delay300(a + 1, 'map 2')),
   reduce((a, b) => a + b),
   console.log)


Reference

0개의 댓글