Search
Duplicate

비동기: 동시성 프로그래밍1

callback과 Promise

callback

함수의 인자값으로 함수를 받아 해당 함수에 인자값을 전달하면서 로직을 수행하는 방법이라 할 수 있습니다.
function add10(a, callback){ setTimeout(()=>callback(a+10), 100); } add10(5, log); // 15
JavaScript
복사
물론 비동기적으로 수행되기 때문에 하단에 즉시 수행되는 로직(ex:log('hi"))라도 있으면 해당 메서드가 먼저 호출될 것입니다.

Promise

callback과 가장 큰 차이점중 하나는 Promise객체를 반환한다는 것입니다.
function add20(a) { return new Promise(resolve => setTimeout(() => resolve(a + 20), 100)); } add20(5) .then(log)//25
JavaScript
복사
return 당시에는 Promise타입 객체를 반환하며 .then 메소드를 통해 내부의 값을 꺼내어 사용합니다.

차이점

1.
callback은 함수를 인자로 받아 수행하는 반면, Promise는 Promise객체를 반환합니다. 로직이 수행되는 주체가 다릅니다.
2.
Code Depth가 callback의 경우 계속해서 증가하는 반면 Promise 의 경우 1 Depth에서 더이상 깊어지지 않습니다.
//callback add10(5, res=>{ add10(res, res=>{//1_depth add10(res, res=>{//2_depth add10(res, res=>{//3_depth log(res); }) }) }) }); //Promise add20(5) .then(add20)//1_depth .then(add20)//1_depth .then(add20)//1_depth .then(add20)//1_depth .then(log);//1_depth
JavaScript
복사

비동기 값으로 만드는 Promise

위에서 간략하게 callback과 Promise의 차이점을 나열했지만 사실 가장 큰 차이점은 Promise는 일급값으로 비동기상황을 다룬다는 점입니다.
Promise는 Promise로 만들어진 인스턴스를 반환하여 대기,성공,실패를 다루는 일급 값으로 이뤄져있는데, 이는 로직을 끝내는 것을 코드나 컨텍스트로만 다루는게 아니라 대기중이라는 값을 생성한다는 점에서 Callback과 가장 큰 차이점입니다.
값으로 대입하는 Promise
var a =add10(5, res=>{ add10(res, res=>{ add10(res, res=>{ add10(res, res=>{ log(res); }) }) }) }); log(a); // undefined var b = add20(5) .then(add20) .then(add20) .then(log); log(b); //Promise {<pending>}
JavaScript
복사
callback의 결과를 담은 a와 다르게 Promise의 반환객체를 담은 b의 출력값을 보면 Promise 객체를 보여주는데, 이는 callback에서는 반환값에 중점을 두는게아닌 코드적인 상황(setTimeout)이나 Context(callback)만 중점으로 둔다는 것입니다.
Promise는 즉시 Promise객체르 반환된다는 특징이 있는데, 이는 callback과는 다르게 Context에 함수의 로직에 이어지는 로직을 then을 통해 추가적으로 이어갈 수 있다는 것입니다. 즉, Promise의 경우 비동기로 이뤄진 상황에 대해서 값으로 다룬다는 것이고 일급값이라는 의미가 되며 다른 곳에서 해당 일급값을 재사용할 수 있다는 의미에서 연속성을 가질 수 있다는 점입니다.

값으로서의 Promise활용

Promise가 비동기 상황을 일급의 상황을 가지고 있다는점을 활용해서 여러가지를 시도할 수 있습니다.
일급 값으로 다룰 수 있다는 의미는 비동기 상황을 값으로 다뤄서 해당 값이 비동기상황인지 상태를 검사할 수 있다는 것입니다.

예제

const go1 = (a, f) => f(a); const add5 = a => a + 5; log(go1(10, add5)); // 15
JavaScript
복사
a라는 인자값을 f에 들어온 인자값 함수에 전달해 로직을 수행하는 go1을 만들었습니다. 이때 go1이라는 함수가 정상적으로 동작하기 위해서는
인자값(a, f)들이 모두 동기적으로 값을 알 수 있는 값이어야 합니다. 즉, Promise객체가 아닐 때만 정상적으로 동작을 한다는 것입니다.
만일 a라는 인자값이 비동기적으로 100ms 후에 나타나는 값이라면 어떻게 동작할까요?
const delay100 = a => new Promise(resolve => setTimeout(()=>resolve(a),100)); log(go1(delay100(10), add5));//[object Promise]5
JavaScript
복사
결과는 정상적이지 못합니다. Promise객체를 일급 값으로써 받는 경우에도 정상적으로 동작하게 하기 위해서는 어떻게 구현을 해야 할까요?
const go1 = (a, f) => a instanceof Promise ? a.then(f): f(a); var r2 = go1(delay100(10), add5); r2.then(log);//15 go1(go1(delay100(10), add5), log);//15 const n2 = delay100(20); go1(go1(n2, add5), log);//25 log(go1(go1(n2, add5), log));//Promise {<pending>}
JavaScript
복사
go1 함수에서 a인자값이 Promise인지 평가해서 상황에 맞는 로직(a.then or f(a))을 수행하도록 합니다.
결과값이 나온 후에 출력을하면 Promise 객체가 출력됩니다. 이는 해당 일급값을 아직 이어서 추가적인 작업을 지속적으로 수행할 수 있다는 의미입니다.

합성 관점에서의 Promise와 모나드

Promise는 비동기상황에서 함수 합성을 안전하게 하기 위한 모나드라고 할 수 있습니다.
자바스크립트는 동적타입언어이자 스크립트 언어이기 때문에 타입에 대해 엄격하지 않기에 모나드라던지 대수구조의 타입이 잘 묻어나지 않는 경향이 있기 때문에 자바스크립트에서는 모나드를 직접적으로 사용하거나 모나드 개념을 이용한 사용자 정의 객체를 만들면서 구현을 하지는 않습니다.
하지만, 함수형 프로그래밍이나 함수합성에서 모나드에 개념을 알고 있으면 좀 더 높은 퀄리티의 코드를 작성할 수 있습니다.

모나드란?

모나드는 일종의 박스이고 박스안에 값이 들어있다고 볼 수 있습니다. (ex: [1])
그리고 이 값을 통해서 함수합성들을 안전하게 수행해나가는 것이라 볼 수 있습니다. 예제를 통해 알아보겠습니다.
우선, 인자값을 받아 1을 더해주는 g라는 함수와 인자값을 제곱해 반환하는 f라는 함수를 만들어봅시다.
const g = a => a+1; const f = a => a*a;
JavaScript
복사
함수를 만들었다면 함수합성을 통해 인자값에 1을 더한 뒤 제곱한 값을 출력해봅니다.
log(f(g(1)));//4
JavaScript
복사
정상적으로 인자값 1에 1을 더해 2로만든뒤 제곱값인 4가 출력됩니다. 그런데 만약 인자값이 없는 상태로 함수합성이 되어 로직이 수행된다면 어떻게 될까요?
log(f(g())); //NaN
JavaScript
복사
정상적으로 값이 호출되지않거나 에러가 발생합니다. 최종적으로 수행되는 log가 비정상적인 동작을 했다는 것입니다. 이는 함수합성에 사용되는 인자값이 유의미한 값이 아니라면 문제가 발생한다는 것인데, 실무에서는 인자값으로 어떤 값이 올 지 모르고 빈값이 올 가능성도 충분합니다.
그 말은, 해당 함수합성(f(g(x)))은 안전하지 않은 함수합성이라 할 수 습니다. 그럼 이처럼 인자값이 어떤 값이 올 지 모르는 불안한 상황에서 함수합성을 어떻게 안전하게 할 지 고려하며 나온 것이 모나드입니다.
모나드는 위에서 말했듯 박스를 가지고있고 그 내부에 실제 효과나 연산에 필요한 재료들을 가지고 있고 이를 통해 함수합성을 합니다.
log([1].map(g).map(f)); // [4]
JavaScript
복사
반환값이 Array 타입인걸 볼 수 있는데 이는 필요한 값은 아닙니다. Array라는 값은 개발자가 값을 다룰 때 사용하는 도구이지, 최종 유효값이라 볼 수는 없습니다. 그렇기 때문에 Array안에 있는 최종 값을 꺼낼 필요가 있습니다.
[1].map(g).map(f).forEach(r=>log(r)); //4
JavaScript
복사
forEach를 통해 map(f)의 결과값인 Array타입의 [4]를 실제 효과(값)를 만들어냅니다. 이는 log(f(g(1))); 와 동일합니다. 그럼 이 방법(모나드를 이용)은
유효하지않거나 없는 값에 대해서 어떤 결과가 나올까요
[].map(g).map(f).forEach(r=>log(r)); // 결과 없음
JavaScript
복사
안에 아무런 값이 없어도 어떤 잘못된 출력이나 에러가 발생하지 않습니다. 실제 최종 결과값을 도출해내는 함수(forEach)에 도착하기전에 안전하게 종료됩니다.
모나드: x라는 인자값을 a라는 함수에게 전달했을때 a라는 함수가 수행 된 이후 그 값이 바로 b라는 함수에 전달되는 것을 함수 합성이라 하는데, 이러한 함수 합성을 안전하게 수행하도록 하는 것을 모나드라 하는데 대표적으로 Promise가 있습니다 ⇒ b(a(x))

Promise는?

Promise.resolve(1).then(g).then(f).then(log); // 4
JavaScript
복사
Promise는 비동기적으로 일어나는 상황에 대해 안전하게 함수 합성을 하기위한 도구입니다.
좀 더 정확히는 잘못된 값보다는 지연되어 나오는 값들(비동기적)에 대해서 안전하게 핸들링 하기 위한 도구라고 할 수 있습니다.

Kleisli Composition 관점에서의 Promise

Promise는 Kleisli Composition을 지원하는 도구라 볼 수 있는데, Kleisli Composition 혹은 Kleisli Arrow라고 불리는
함수 합성 방법은 오류가 있을수 있는 상황에서의 함수함성을 안전하게 하는 하나의 규칙이라 볼 수 있습니다.
수학적인 프로그래밍을 하게 되면 함수를 합성하게 되고, 수학적이란 것은 항상 정확하고 안전한 변수를 통해서 함수합성및 평가와 결과도출까지 된다는 것인데, 실무에서는 외부와의 상황이나 여러 상황으로 인해 함수합성이 원하는대로 되지 않을 가능성이 있습니다.
Kleisli Composition는 들어오는 인자가 잘못되어 함수에서 오류가 나거나 정확한 인자가 들어왔더라도 특정 함수가 의존하는 외부의 상태에 의해 결과를 정확히 전달하기 힘든 상황에서 에러가 나는 것을 해결하기 위한 함수 합성이라 볼 수 있습니다.

예제

유저목록에서 id로 해당 유저를 찾아 이름을 반환하는 로직
const users = [ {id:1, name:'aa'}, {id:2, name:'bb'}, {id:3, name:'cc'}, ]; const getuserById = id => find(u=>u.id === id, users); const f = ({name}) => name; const g = getuserById; const fg = id => f(g(id)); const r = fg(2); log(r); // bb
JavaScript
복사
users에서 인자값으로 받은 id와 동일한 user를 찾는 함수 getuserById
name을 구조분해하여 얻어 반환하는 f
getuserById를 값으로 취급하는 g
f와 g를 합성해 users에서 특정 id의 name을 추출해 반환하는 fg
fg(2);를 호출하면 g(2)가 실행되어 users에서 id가 2인 obj를 꺼내전달하고 ({id:2, name:'bb}) f함수에서는 name을 구조분해하여 추출후 반환합니다.
여기서 찾고자하는 id가 현재 users에 없는 id(ex: 4,5,6..n)이거나 외부 상황에 의해 users의 내용이 변한다면 어떻게 될까요?
const r = fg(5); //Uncaught TypeError: Cannot destructure property 'name' of 'undefined' as it is undefined. users.pop(); users.pop(); const r = fg(2)//Uncaught TypeError: Cannot destructure property 'name' of 'undefined' as it is undefined.
JavaScript
복사
잘못 된 값 혹은 외부의 변화에 따라 상황에따라 에러가 발생할 수 있습니다. 이런 상황에서 문제가 발생하지 않도록 하는 것을 Kleisli Arrow라고 합니다.
const fg = id => Promise.resolve(id).then(g).then(f) fg(2).then(log); // bb
JavaScript
복사
위에서 모나드에 대해 얘기하며 했던 방법으로 then을 통해 함수합성을 시도합니다.
하지만 외부 환경에 의해서 변경이 생기면 여전히 에러가 발생합니다.
const fg = id => Promise.resolve(id).then(g).then(f) users.pop(); users.pop(); fg(2).then(log); //Cannot destructure property 'name' of 'undefined' as it is undefined.
JavaScript
복사
이는 getuserById에서 찾으려는 users의 항목이 외부변화에 의해 없어졌기 때문인데 이를 해결하기 위해 검색결과가 없을 경우에 대한 반환값을 만들어 줍니다.
const getuserById = id => find(u=>u.id === id, users)||Promise.reject("없어요!"); const fg = id => Promise.resolve(id).then(g).then(f).catch(a=>a); users.pop(); users.pop(); fg(2).then(log); //없어요!
JavaScript
복사
getuserById함수에서는 이제 검색결과가 없을 경우 Promise객체를 반환하게되고 fg에서는 해당객체의 reject가 호출되면 catch부분에서 받아서 출력을 함으로써 에러는 발생하지 않게 됩니다.
이런식으로 Promise에서 Kleisli 관점으로 바라볼 수 있습니다.

go, pipe, reduce에서 비동기 제어

비동기를 다루는 Promise객체를 이용해 go, pipe, reduce함수들에서도 비동기를 값으로 다루는 성질을 이용해 프로미스와 같이 비동기상황에 놓여져도 잘 대응하는 함수를 만들 수 있습니다. 또는 Kleisli처럼 중간에 reject가 발생했을 경우에도 대응하는 방법을 적용할 수 있습니다.

예제

go(1, a=>a+10, a=>a+100, a=>a+1000, log );//1111
JavaScript
복사
예를들어 위와같은 인자값에 10,100,1000을 더한 뒤 출력해주는 함수가 있다고 합니다.
근데 여기서 3Line 의 a+100이 Promise타입으로 바뀌면 어떻게 될까요.
go(1, a=>a+10, a=>Promise.resolve(a+100), a=>a+1000, log );//[object Promise]1000
JavaScript
복사
올바르지 않은 값이 출력되는걸 볼 수 있습니다. 이는 go함수내부에서 호출하는 reduce부분에서 발생한 문제 때문입니다.
const go = (...args) => reduce((a, f) => f(a), args); const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value; } for (const a of iter) { acc = f(acc, a); } return acc; });
JavaScript
복사
go를 통해 reduce에 함수가 전해져서 실행이되고있는게 reduce인데 Promise가 인자로 전달되면 반복문 부분에서 f(Promise, a)가 되버려서 다음 루프가 도는 경우 에러가 발생합니다.

해결책 - 1

const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value; } for (const a of iter) { //acc = f(acc, a); acc = acc instanceof Promise ? acc.then(acc=>f(acc,a)): f(acc, a); } return acc; }); // 1111
JavaScript
복사
코드는 동작합니다. 하지만, 아직 고려해야할 부분이 있는데, 함수가 중간에 Promise를 만나게되면 그 뒤로는 계속해서 Promise Chaning이 걸리게 됩니다. 그리고 이런 함수합성이 많으면 불필요한 분기를 타게 되면서 성능저하도 일어나게 됩니다.
그럼 Promise 가 된 다음 실행 함수에서는 또 동기적으로 즉시평가를 하고싶으면 어떻게 해야할까요?

해결책 - 2

const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value; } return function recur(acc) { for (const a of iter) { acc = f(acc, a); if(acc instanceof Promise) return acc.then(recur); } return acc; }(acc); }); go(1, a=>a+10, a=>Promise.resolve(a+100), a=>a+1000, log );//1111
JavaScript
복사
재귀를 이용한 방법을 사용합니다. 유명함수 를 이용해 return 값에 recur함수를 선언해서 내부에서 우선 인자값 함수를 실행 후 해당 값이 Promise이면 acc.then(recure)로 재귀호출을 하면, Promise의 내용의 실제 값이 ㅇ인자로 전달됩니다. 이렇게 되면 go내부의 [1, a⇒a+1`] 이 하나의 callstack, [a⇒a+1000, log ]가 하나의 callstack으로 동작해 성능향상을 바랄 수 있습니다.

보강 - 첫 인자값이 Promise인 경우

go에서 사용되는 첫 번째 인자값이 Promise인 경우에는 아직 에러가 발생합니다.
go(Promise.resolve(1), a=>a+10, a=>Promise.resolve(a+100), a=>a+1000, log );//[object Promise]101001000
JavaScript
복사
위와같이 첫 번째 인자부터 Promise이라면 reduce의 유명함수 즉시실행부분에서 이미 첫 번째 인자를 Promise로 전달하기 때문에 제대로 된 결과가 나오지 않습니다. 이런 경우에는 첫 번째 인자값을 받더라도 즉시 전달하기전 Promise인지 아닌지 판단해서 Promise인경우 풀어서 전달할 필요가 있습니다.
const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a); const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value; } return go1(acc, function recur(acc) { for (const a of iter) { acc = f(acc, a); if(acc instanceof Promise) return acc.then(recur); } return acc; }); }); go(Promise.resolve(1), a=>a+10, a=>Promise.resolve(a+100), a=>a+1000, log );//1111
JavaScript
복사
reduce 함수내에서 유명함수를 그대로 즉시호출하는게 아닌 go1이라는 Promise 대응함수를 만들어 첫 번째 인자값과 실행될 유명함수 recur를 인자값으로 전달하면 첫 인자값이 Promise인지 아닌지에 따라 로직이 수행되기 때문에 첫 번째 인자가 Promise일지라도 정상동작합니다.

보강 - 2

중간의 인자값함수가 Promise.reject('error')라면 ?
go(Promise.resolve(1), a=>a+10, a=>Promise.reject('error'), a=>log('----'), a=>a+1000, log );//Uncaught (in promise) error
JavaScript
복사
로직이 더 진행되지 못하고 에러가 발생합니다. 그렇다면 reject가 발생하여 실패했을때에 대한 catch문을 작성해서 보완해줍니다.
go(Promise.resolve(1), a=>a+10, a=>Promise.reject('error'), a=>log('----'), a=>a+1000, log ).catch(a=>log(a));//error~
JavaScript
복사

결론

이제 함수합성에서 초기값 혹은 중간에서 Promise를 받거나 수행할 경우에도 정상적으로 동작을 할 수 있게 됩니다. 이제 go, pipe, reduce같은 함수에서는 Promise를 만났을때 좀 더 다형적으로 대응이 가능하고 비동기객체(Promise)를 값으로 다루면서 Promise의 기능인 then으로 이루어진 로직수행만 하는게 아닌 원하는 시점에서 원하는 동작을 할 수 있는 고차함수를 만드는 등의 응용이 가능해집니다.

promise.then의 중요한 규칙

Promise에는 아무리 Promise가 중첩되서 사용도더라도 원하는 곳에서 then을 통해 값을 꺼내 쓸 수 있다는 규칙이 있습니다.
Promise.resolve(Promise.resolve(1)).then(log);//1 new Promise(resolve => resolve(new Promise(resolve1 => resolve1(1)))).then(log);//1
Java
복사
그냥 보기에는 then에 들어가는 인자값이 2depth의 Promise객체일 것 같지만 실제로는 아무리 Promise가 중첩되있더라도 그 내부에있는 결과값이 도출되어 전달됩니다.