동기적 , 비동기적 프로그래밍이란 단어를 많이 들어보았다.
하지만 그것들이 실제 어떤 원리로 작동하는지와 , 자바스크립트에서 어떤 식으로 적용되는지 모른다.
그것에 관해 알아보도록 하자.
Block | Non-Block | |
---|---|---|
Sync | ||
Async |
???...뭐지 이 표는
Block,Non-Block,Sync,Async의 개념이 크로스오버된 표가 있다.
표에 표시된 개념들을 정리하며, 마지막에 이 표를 채워넣는 것을 해보자.
단어가 어떻게 이루어져 있는지 살펴보자.
용어를 풀어보니 두 단어가 어떠한 작업이 시간에 의해서 나눠짐을 확인할 수 있다.
Sync : 동기
어떠한 작업이 순차적으로 실행되는 개념이며, 요청을 하면 결과가 반환되는 것을 기다려야 한다.
동기방식 설계는 매우 직관적이지만, 결과 반환까지 대기해야 하는 시간비효율적인 단점이 있다.
Async : 비동기
어떠한 작업이 동시에 일어날 수 있는 개념이며, 요청과 결과 반환이 동시에 일어나지 않는다.
비동기방식 설계는 병렬적으로 작업을 진행하기 때문에 효율적이지만, 설계가 복잡한 단점이 있다.
이렇게 개념을 나눴지만 사실 두 개념은 밀접한 관계에 있는 것 같다.
카페 이벤트를 예로 들어보자.
주문을 받고, 현황판에 띄우고, 커피를 만들고, 손님에게 주는.... 이런 작업들은 비동기적으로 이루어 진다.
작업들이 동시병렬적으로 이루어 질 수 있기 때문에 비동기적이라 볼 수 있다.
하지만 한 손님이 관점에서 보면, 지극히 동기적인 과정이다.
주문을 받고 -> 현황판에 띄워지고 -> 커피가 다 만들어 지면-> 손님에게 전달되는
결국,,,동기와 비동기는 완전히 따로 이뤄지는 개념이 아니라, 서로 섞여서 진행되는 것들이라 볼 수 있다.
자바스크립트는 단일 쓰레드, 즉 하나의 작업만 돌아간다고 볼 수 있다.
이로 인해 어떻게 비동기 작업이 가능한지 헷갈릴 수 있다.
자바스크립트 비동기 병렬 처리에 대해서 알아보자.
멀티 쓰레드
: 한 프로세스를 여러 개의 스레드로 구성하여 같이 처리하는 것
멀티 쓰레드 개념으로 보면 비동기 병렬 처리는 자연스럽다.
단일 쓰레드
: 한 프로세스 당 하나의 스레드로 구성되어 일을 처리하는 것
단일 쓰레드 개념에선, 병렬 처리라는 것 자체가 어색하다.
자바스크립트는 단일 쓰레드 인데 어떻게 비동기 병렬 처리가 가능한 것일까? 한번 알아보도록 하자.
JS는 단일 쓰레드의 단점을 회피하기 위해 비동기 프로그래밍을 사용한다.
Source를 순회하는 쓰레드는 하나이지만 Netword IO나 DB를 조회하는 등의 시간 비용이 큰 로직은
다른 쓰레드로 위임하고 또 다른 로직으로 이동해 작업을 수행한다.
이렇게 큰 일들을 다른 쓰레드로 위임하는 것이 JS 비동기의 특징이다.
위임시키는 대상은 API라는 곳인데, 브라우저엔 WebAPI , NojeJS에서는 Node API라고 부르는 별개의 쓰레드 영역이다.
그렇다면 큰 일을 던져준 JS 메인 쓰레드는 쉬는가? 그렇지 않다.
던져준 일을 콜백 함수로 처리하고 다른 일을 진행하며, 이것이 Non-Blocking
의 개념이다.
Blocking
은 처리된 일을 기다리고 다음 일을 진행하는 것을 뜻할 것이다.
이런 식으로 API에게 던진 일이 끝나면, 이벤트 큐
에 등록하고 이벤트 루프
를 통해 메인쓰레드에 알려주는 시스템이다. 이 시스템을 이벤트 기반 아키텍쳐
라고 부른다.
Event Queue
에 들어가 대기한다.Event Loop
는 Event Queue
에 있는 일을 하나 꺼내서 CallStack에 집어 넣어 실행한다.이 과정이 JS에서 비동기를 처리하는 방법이며,
그 처리 방식에는 CallBack이 있고 이를 개선한 Promise와 async & await 이 있다.
: 비동기를 호출하는 함수를 호출하면서 , 콜백 함수
라는 인자를 넣어 함수의 결과물을 필요로 하는 뒤의 로직을 구성할 수 있게 된다.
간단한 예시를 통해 콜백을 이해해보자.
setTimeout(()=>console.log('Hello World'),1000)
여기서 콘솔에 Hello World를 찍는 함수가 바로 콜백 함수 이다.
뒤의 로직은 1000ms(1초)뒤에 콜백 함수를 실행하는 아주 단순한 비동기 함수이다.
자 이제 callback 함수의 지옥스러운 예제를 살펴보자.
//class선언
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (
(id === 'swing' && password === 'hello') ||
(id === 'coder' && password === 'frontend')
) { onSuccess(id) } //id를 전달한다.
else { onError(new Error('Not Found')) };
}, 2000);
}
getRoles(user, onSuccess, onError) {
setTimeout(() => {
if (user === 'swing') {
onSuccess({ name: 'swing', role: 'admin' });
}
else { onError(new Error('No Access')) };
}, 1000);
}
}
///실제 동작 함수 ///
const userStorage = new UserStorage();
const id = prompt('enter your ID');
const password = prompt('enter your Password');
userStorage.loginUser(id, password, (user) => {
userStorage.getRoles(user, (userWithRole) => {
alert(`Hello ${userWithRole.name} , you have ${userWithRole.role} role`);
}, (error) => { console.log(error) })
}, (error) => { console.log(error) });
일단 가독성이 매우 안좋다.
사실 간단한 동작을 실행하는 함수이다.
이런 간단하게 동작하는 함수도 콜백 함수가 여러 개 필요한 것은 사실이다.
이를 개선하기 위해 Promise가 도입되었다.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if ((id === 'ellie' && password === 'dream') ||
(id === 'coder' && password === 'academy'))
{ resolve(id) }
else { reject(new Error('Not Found')) };
}, 2000);
})
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === 'ellie') { resolve({ name: 'ellie', role: 'admin' }) }
else { reject(new Error('No Access')) }
}, 1000);
})
}
}
const userStorage = new UserStorage();
const id = prompt('enter your ID');
const password = prompt('enter your Password');
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then(user => alert(`Hello ${user.name}! Your role is ${user.role}.`))
.catch(console.log)
훨씬 간단 명료해졌다.
기존 onSuccess , onError는 각각 resolve와 reject로 대체되었다.
then은 resolve가 호출되어 값을 넘기고, catch는 reject가 호출되어 값을 넘긴다.
동작부는 더욱 직관적이게 되었다. 살펴보자면,
이렇게 콜백 함수를 promise chaining을 통해 then과 catch라는 키워드로 간단하게 바꿀 수 있다.
하지만, 여기서 async & await 을 사용하면 더욱 획기적으로 간단하게 바꿀 수 있다.
Promise chaining을 계속하다보면, 결국 callback hell처럼 가독성이 떨어진다.
이를 보완하고자, promise를 간결하게 동기적으로 실행되는 것처럼 보이게 한 API가 async & await이다.
먼저 async function을 만들어야 그 안에서 await 사용이 가능하다.
예를 살펴보자.
기존 방식(Promise)
function fetchUser() {
return new Promise((resolve, reject) => {
resolve(`ellie`);
});
}
const user = fetchUser();
user.then(user => console.log(user));
async사용 (코드 블록이 자동으로 promise로 변환되는 것이다!)
async function fetchUser() {
return `ellie`;
}
const user = fetchUser();
user.then(data => console.log(data));
기존 방식(Promise)
function delay(ms) {
return new Promise (resolve => setTimeout(resolve, ms));
}
function getApple() {
return delay(1000)
.then(() => `🍎`);
}
function getBanana() {
return delay(1000)
.then(() => `🍌`);
}
function pickFruits() {
return getApple()
.then(apple => {
return getBanana().then(banana => `${apple} + ${banana}`);
});
}
pickFruits().then(result => console.log(result));
Async & await 사용
function delay(ms) {
return new Promise (resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(1000);
return `🍎`;
}
async function getBanana() {
await delay(1000);
return `🍌`;
}
// 방법 1: 무식한 코드
async function pickFruits() {
const applePromise = getApple();
const bananaPromise = getBanana();
const apple = await applePromise;
const banana = await bananaPromise;
return `${apple} + ${banana}`;
}
pickFruits().then(result => console.log(result));
// 방법 2: Promise APIs 사용
function pickAllFruits() {
return Promise.all([getApple(), getBanana()]).then(fruits => {
return fruits.join(` + `);
});
// return Promise.all([getApple(), getBanana()]);
}
pickAllFruits().then(console.log);
// 번외: 비동기 처리중 먼저 리턴하는 녀석만 출력
function pickOnlyOne() {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log);
: 동기와 비동기는 서로 상반된 개념이지만 , 프로그래밍 적으로 둘은 아주 밀접하게 사용된다.
Javascript는 단일스레드여서 비동기 작업의 개념이 적용되지 않겠다고 생각 할 수 있다.
하지만 JS는 이를 개선하고자 WebAPI, NodeJS API 등 API에 일을 위임시켜, 별개의 스레드에서 작업을 진행시킨다.
비동기 작업을 할때 , Callback함수와 Promise , async & await 개념을 알아야 한다.
각각은 동기적으로 실행되는 것처럼 보이는, 사실상 비동기 작업을 위해 쓰이는 키워드 들이다.
<참고> 얄팍한 코딩사전 - 비동기프로그래밍
프로그램이 비동기로 동작한다 ?
=> 멀티쓰레드,멀티 프로세스로 멀티태스킹이 구현되는 것이다.
자바스크립트가 비동기 작업을 ?
=> JS는 웹브라우저나 노드 환경의 JS엔진에서 실행되고, 이 엔진안에는 JS용 메인쓰레드가 있다.
이 쓰레드를 메인 선로라 생각하면, 타이머나 서버요청, DB조회등 시간이 걸리는 작업들은 비동기 작업 스레드에 올라간다. 이 비동기 작업이 처리되면 태스크 큐라는 곳에 콜백 함수가 올라가는데, 이벤트 루프라는 물레방아가 돌며 콜백 함수를 다시 메인 선로에 올린다.
이런 식으로 비동기 작업이 수행된다고 이해할 수 있다.
이러면 함수가 뒤죽박죽 섞여서 복잡한데?
=> 그래서 ES6에서 Promise가 도입되었다. 비동기 작업을 하는 함수가 프로미스 객체를 반환하게 한다. 그 프로미스 생성자에 첫번째 인자에는 수행될 비동기 작업, 두번째 인자에는 그 결과물을 콜백 함수에 전달하는 함수가 들어간다. 이것들을 then으로 chaining해서 순차적으로 처리되게끔 보이게 한다.
이것도 체이닝하다보면 복잡해 질텐데?
=> 그래서 ES7에는 async 와 await이 추가되었다.
비동기 작업을 할 함수앞에 async키워드를 붙이면 await을 사용할 수 있다.
await이 선언된 작업에서는 그 작업이 완료되기 전까지 다른 작업으로 넘어가지 않는다.
비동기 작업을 동기적으로 처리되게끔 보이게 하는 장치인 것이다!
Block | Non-Block | |
---|---|---|
Sync | ||
Async |
드디어 이 표를 채워 넣을 시간이다.
위의 그림을 보고 각 용어의 개념을 다시 정리해보자.
: 호출된 함수
가 자신이 할 일을 모두 마칠 때까지 제어권을 계속 가지고서 호출한 함수
에게 바로 돌려주지 않는 것
호출된 함수
가 자신이 할 일을 채 마치지 않았더라도 바로 제어권을 건네주어(return) 호출한 함수
가 다른 일을 진행할 수 있도록 해주는 것
이렇게 보니 , sync와 blocking / async와 Non-blocking이 서로 매우 밀접한 관계인 것처럼 보인다.
하지만 4개의 개념은 섞일 수 있고 결국 저렇게 표로 나누는 것은 의미가 없다는 결론이다.
실상의 예를 통해 이해를 더욱 쉽게 하며, 개념 정리를 해보자.
나 : 대표님, 개발자 좀 더 뽑아주세요..
대표님 : 오케이, 잠깐만 거기 계세요!
나 : …?!!
대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)
나 : (과정 지켜봄.. 궁금함.. 어차피 내 일 하러는 못 가고 계속 서 있음)
나 : 대표님, 개발자 좀 더 뽑아주세요..
대표님 : 오케이, 잠깐만 거기 계세요!
나 : …?!!
대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)
나 : (안 궁금함.. 지나가는 말로 여쭈었는데 붙잡혀버림.. 딴 생각.. 못 가고 계속 서 있음)
나 : 대표님, 개발자 좀 더 뽑아주세요..
대표님 : 알겠습니다. 가서 볼 일 보세요.
나 : 넵!
대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)
나 : 채용하셨나요?
대표님 : 아직요.
나 : 채용하셨나요?
대표님 : 아직요.
나 : 채용하셨나요?
대표님 : 아직요~!!!!!!
나 : 대표님, 개발자 좀 더 뽑아주세요..
대표님 : 알겠습니다. 가서 볼 일 보세요.
나 : 넵!
대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)
나 : (열일중..)
대표님 : 한 분 모시기로 했습니다~!
나 : 😍
맨날 헷갈렸는데 이제 좀 알겠네요 ! 감사합니다.