Skip to content
Underbleu
GithubLinkedin

3-4. 병렬적으로 동시성 다루기 - mapC, 배경 및 개요

Functional programming1 min read

프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 정리한 내용입니다.

# 자바스크립트 비동기가 아름다운 이유?

1. 브라우저를 안 죽게한다

  • 서버에 오래걸리는 작업을 요청했을 때 알아서 비동기적으로 처리하기 때문에, 브라우저 기본 기능(스크롤, 클릭..)을 문제 없이 사용할 수 있다
  • iphone 앱개발일 경우 데이터를 가져오는 thread를 따로 열어서 병렬적으로 작업해야, 기본 기능에 문제가 안생긴다

2. 서버의 요청처리 방식

multi-singleThread

기존의 서버 (Multi Thread)

  • 요청을 동시에 처리하기 위해 하나의 요청에, 하나의 thread를 할당 (비쌈)
  • 요청을 처리하는 동안, 요청 처리를 하는 부분을 제외한 CPU의 나머지 부분은 놀고있음
  • JAVA서버 하나가, 500개 정도의 쓰레드풀을 열 수 있음 (2007년 쯤에)
  • 요청이 500개 이상왔을 때, 요청을 쌓다가 병목이 되어 서버1이 죽고, 요청이 서버2에 몰려 죽고.. 연쇄적으로 서버가 다운됨

NodeJS (Single Thread)

  • 모바일 웹세계가 시작되면서 서버트래픽이 급증하여 기존의 서버시스템으로 감당이 힘들어짐
  • 하나의 Thread를 분할해서 병렬적으로 일을 처리
  • CPU가 노는 부분 없이 다 일을하게 하기 때문에, "500명 -> 2만명" 처리가 가능해짐
  • JAVA 서버보다 느릴지라도, 유저가 많은 서비스에서 동시성을 잘 지원 (게임, 채팅, 페이스북 댓글. UX에서 아주 중요한 부분)
  • 프로그래머가 구간별로 동시성을 다룰 수 있게 동기/비동기 코드를 설계 관건
    • 비동기가 일어나면 콜스택이 비워져 디버깅이 어려워지기 때문 (callback이 콜스택에서 WebAPI로 이동한다)
    • 반면에 JAVA는 모든 I/O를 콜스택에 쌓기때문에, 디버깅이 쉽다

NodeJS는 단순히 자바스크립트로 서버 프로그래밍을 하기위한게 아니라, 많은 트래픽을 동시에 처리 하기 위한 서버시스템이다

3. 함수형프로그래밍의 동시성

CPU는 더 이상 물리적으로 빨라질 수 없기때문에, 거대한 트래픽처리를 위해 언제 평가해도 상관없는 순수함수들로 코드를 설계하여 CPU를 최대한 안놀게하려는 프로그래밍의 방향성이다


C의 의미: concurrency
지금까지는 순서대로 돌아가는 함수를 제어했다면, 이제부터는 병렬적으로 일처리를 하는방법을 배울거다 (어려움)

# mapC: 비동기작업을 병렬적으로 처리하는 map

  • map은 보조함수 리턴값이 프로미스이면, 그 값을 까서 재귀로 전달해주는 순차적 처리 방식으로 시간을 많이 소요했지만
  • mapC는 비동기작업을 병렬적으로 처리하기 때문에, 기존의 map보다 훨씬 빠르다

ex) 서버에서 이미지 요청시, mapC로 여러개를 로드 해놓고 한 번에 부드럽게 그릴 수 있도록 해준다

1. Promise.all을 사용하는 방법 -> Plain Object 지원 안됨

1function time(a) {
2 return new Promise(resolve => {
3 setTimeout(() => {
4 resolve(a)
5 }, 1000)
6 })
7}
8
9const mapC = curry((f, coll) => {
10 const res = []
11 for (const a of coll) {
12 res.push(f(a))
13 }
14 return Promise.all(res)
15})
16
17go([1, 2, 3],
18 map(time),
19 log) // 3초
20
21
22go([10, 20, 30],
23 mapC(time),
24 log) // 1초

2. Promise들을 객체 안에 받아놓고 나중에 까주기 -> 배열/객체 모두 지원

  1. 출발선상에서 모든 인자들을 프로미스로 만들어 세워놓는다
    • 프로미스 아닌척하기 위해 리턴값을 { } 객체괄호 한 번 싸주면,
    • map의 reduce가 acc를 객체로 인지하기 때문에 (속았음 ㅋㅋ), 프로미스 값을 풀어서 recur(acc) 전달하기위한 시간소요를 하지 않아도 된다
  2. 값이 다 모이면, 프로미스를 까주는 2번째 map함수를 실행시킨다
1const mapC = curry((f, coll) =>
2 go(coll,
3 map(a => ({ val: f(a) })),
4 //-> 1. { a: { val: Promise(1) }, b: { val: Promise(2) } }
5 map(b => b.val)))
6 //-> 2. { a: 1, b: 2 }
7
8go({ a: 1, b: 2 },
9 mapC(time),
10 log) // 1초

3. 성능튜닝을 위한 limit

만약 서버에 요청해 가져올 데이터(coll)가 너무 크면, 오히려 성능이 안좋아지거나 서버가 죽을 수 있기때문에 limit을 걸어줘야 한다. 다음강에서 다룰 예정 !