[CS] 동기와 비동기 , 이벤트 기반 프로그래밍

swing·2021년 1월 27일
5

[CS]

목록 보기
1/5
post-thumbnail

동기와 비동기에 관해 알아보자.

동기적 , 비동기적 프로그래밍이란 단어를 많이 들어보았다.

하지만 그것들이 실제 어떤 원리로 작동하는지와 , 자바스크립트에서 어떤 식으로 적용되는지 모른다.

그것에 관해 알아보도록 하자.

BlockNon-Block
Sync
Async

???...뭐지 이 표는

Block,Non-Block,Sync,Async의 개념이 크로스오버된 표가 있다.

표에 표시된 개념들을 정리하며, 마지막에 이 표를 채워넣는 것을 해보자.

- Sync VS Async (Synchronous VS ASynchoronous)

단어가 어떻게 이루어져 있는지 살펴보자.

  • Syn + chrono + us : 함께 시간을 맞춰서 실행됨.
  • A + Syn + chrono + us : 함께 시간을 맞춰서 실행되지 않음.

용어를 풀어보니 두 단어가 어떠한 작업이 시간에 의해서 나눠짐을 확인할 수 있다.

Sync : 동기

어떠한 작업이 순차적으로 실행되는 개념이며, 요청을 하면 결과가 반환되는 것을 기다려야 한다.

동기방식 설계는 매우 직관적이지만, 결과 반환까지 대기해야 하는 시간비효율적인 단점이 있다.

Async : 비동기

어떠한 작업이 동시에 일어날 수 있는 개념이며, 요청과 결과 반환이 동시에 일어나지 않는다.

비동기방식 설계는 병렬적으로 작업을 진행하기 때문에 효율적이지만, 설계가 복잡한 단점이 있다.

이렇게 개념을 나눴지만 사실 두 개념은 밀접한 관계에 있는 것 같다.

카페 이벤트를 예로 들어보자.

주문을 받고, 현황판에 띄우고, 커피를 만들고, 손님에게 주는.... 이런 작업들은 비동기적으로 이루어 진다.

작업들이 동시병렬적으로 이루어 질 수 있기 때문에 비동기적이라 볼 수 있다.

하지만 한 손님이 관점에서 보면, 지극히 동기적인 과정이다.

주문을 받고 -> 현황판에 띄워지고 -> 커피가 다 만들어 지면-> 손님에게 전달되는

결국,,,동기와 비동기는 완전히 따로 이뤄지는 개념이 아니라, 서로 섞여서 진행되는 것들이라 볼 수 있다.

비동기 병렬 처리

자바스크립트는 단일 쓰레드, 즉 하나의 작업만 돌아간다고 볼 수 있다.

이로 인해 어떻게 비동기 작업이 가능한지 헷갈릴 수 있다.

자바스크립트 비동기 병렬 처리에 대해서 알아보자.

멀티 쓰레드 VS 단일 쓰레드

멀티 쓰레드

: 한 프로세스를 여러 개의 스레드로 구성하여 같이 처리하는 것

멀티 쓰레드 개념으로 보면 비동기 병렬 처리는 자연스럽다.

단일 쓰레드

: 한 프로세스 당 하나의 스레드로 구성되어 일을 처리하는 것

단일 쓰레드 개념에선, 병렬 처리라는 것 자체가 어색하다.

자바스크립트는 단일 쓰레드 인데 어떻게 비동기 병렬 처리가 가능한 것일까? 한번 알아보도록 하자.

JS 비동기 프로그래밍의 특징

JS는 단일 쓰레드의 단점을 회피하기 위해 비동기 프로그래밍을 사용한다.

Source를 순회하는 쓰레드는 하나이지만 Netword IO나 DB를 조회하는 등의 시간 비용이 큰 로직은

다른 쓰레드로 위임하고 또 다른 로직으로 이동해 작업을 수행한다.

스크린샷 2021-01-26 오후 3 31 39

이렇게 큰 일들을 다른 쓰레드로 위임하는 것이 JS 비동기의 특징이다.

위임시키는 대상은 API라는 곳인데, 브라우저엔 WebAPI , NojeJS에서는 Node API라고 부르는 별개의 쓰레드 영역이다.

그렇다면 큰 일을 던져준 JS 메인 쓰레드는 쉬는가? 그렇지 않다.

던져준 일을 콜백 함수로 처리하고 다른 일을 진행하며, 이것이 Non-Blocking 의 개념이다.

Blocking은 처리된 일을 기다리고 다음 일을 진행하는 것을 뜻할 것이다.

스크린샷 2021-01-26 오후 3 31 47

이런 식으로 API에게 던진 일이 끝나면, 이벤트 큐에 등록하고 이벤트 루프를 통해 메인쓰레드에 알려주는 시스템이다. 이 시스템을 이벤트 기반 아키텍쳐 라고 부른다.

  1. 처리된 일은 Event Queue 에 들어가 대기한다.
  2. 메인 쓰레드가 일을 마쳐 CallStack에서 실행할 게 없어지면
  3. Event LoopEvent Queue에 있는 일을 하나 꺼내서 CallStack에 집어 넣어 실행한다.

이 과정이 JS에서 비동기를 처리하는 방법이며,

그 처리 방식에는 CallBack이 있고 이를 개선한 Promise와 async & await 이 있다.

JS 비동기 처리 방식 3가지

  • Callback

: 비동기를 호출하는 함수를 호출하면서 , 콜백 함수라는 인자를 넣어 함수의 결과물을 필요로 하는 뒤의 로직을 구성할 수 있게 된다.

간단한 예시를 통해 콜백을 이해해보자.

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) });

일단 가독성이 매우 안좋다.

사실 간단한 동작을 실행하는 함수이다.

  1. 유저에게서 아이디와 패스워드를 입력받는다.
  2. 해당하는 유저이면 onSuccess라는 함수를 통해 id를 전달하고 그렇지 않으면 에러를 띄운다.
  3. getRoles에서는 받은 유저 id가 swing이면 role을 띄우고 아니면 에러를 띄운다.

이런 간단하게 동작하는 함수도 콜백 함수가 여러 개 필요한 것은 사실이다.

이를 개선하기 위해 Promise가 도입되었다.

  • 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가 호출되어 값을 넘긴다.

동작부는 더욱 직관적이게 되었다. 살펴보자면,

  1. userStorage.loginUser에 id와 password를 넘겨준다.
  2. resolve이면, then(userStorage.getRoles를 호출한다.)
  3. resolve이면 then(알람을 띄운다.)
  4. reject이면 catch(에러를 띄운다.)

이렇게 콜백 함수를 promise chaining을 통해 then과 catch라는 키워드로 간단하게 바꿀 수 있다.

하지만, 여기서 async & await 을 사용하면 더욱 획기적으로 간단하게 바꿀 수 있다.

  • async & await

Promise chaining을 계속하다보면, 결국 callback hell처럼 가독성이 떨어진다.

이를 보완하고자, promise를 간결하게 동기적으로 실행되는 것처럼 보이게 한 APIasync & await이다.

먼저 async function을 만들어야 그 안에서 await 사용이 가능하다.

예를 살펴보자.

  • async

기존 방식(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));
  • await

기존 방식(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 개념을 알아야 한다.

각각은 동기적으로 실행되는 것처럼 보이는, 사실상 비동기 작업을 위해 쓰이는 키워드 들이다.

<참고> 얄팍한 코딩사전 - 비동기프로그래밍

의식의 흐름대로 두번째 정리

  1. 프로그램이 비동기로 동작한다 ?

    => 멀티쓰레드,멀티 프로세스로 멀티태스킹이 구현되는 것이다.

  2. 자바스크립트가 비동기 작업을 ?

    => JS는 웹브라우저나 노드 환경의 JS엔진에서 실행되고, 이 엔진안에는 JS용 메인쓰레드가 있다.

    이 쓰레드를 메인 선로라 생각하면, 타이머나 서버요청, DB조회등 시간이 걸리는 작업들은 비동기 작업 스레드에 올라간다. 이 비동기 작업이 처리되면 태스크 큐라는 곳에 콜백 함수가 올라가는데, 이벤트 루프라는 물레방아가 돌며 콜백 함수를 다시 메인 선로에 올린다.

    이런 식으로 비동기 작업이 수행된다고 이해할 수 있다.

  3. 이러면 함수가 뒤죽박죽 섞여서 복잡한데?

    => 그래서 ES6에서 Promise가 도입되었다. 비동기 작업을 하는 함수가 프로미스 객체를 반환하게 한다. 그 프로미스 생성자에 첫번째 인자에는 수행될 비동기 작업, 두번째 인자에는 그 결과물을 콜백 함수에 전달하는 함수가 들어간다. 이것들을 then으로 chaining해서 순차적으로 처리되게끔 보이게 한다.

  4. 이것도 체이닝하다보면 복잡해 질텐데?

    => 그래서 ES7에는 async 와 await이 추가되었다.

    비동기 작업을 할 함수앞에 async키워드를 붙이면 await을 사용할 수 있다.

    await이 선언된 작업에서는 그 작업이 완료되기 전까지 다른 작업으로 넘어가지 않는다.

    비동기 작업을 동기적으로 처리되게끔 보이게 하는 장치인 것이다!

Blocking I/O , Non-Blocking I/0 ? 그리고 sync & async

BlockNon-Block
Sync
Async

드디어 이 표를 채워 넣을 시간이다.

스크린샷 2021-01-27 오후 3 53 26

위의 그림을 보고 각 용어의 개념을 다시 정리해보자.

Blocking

: 호출된 함수가 자신이 할 일을 모두 마칠 때까지 제어권을 계속 가지고서 호출한 함수에게 바로 돌려주지 않는 것

Non-Blocking

호출된 함수가 자신이 할 일을 채 마치지 않았더라도 바로 제어권을 건네주어(return) 호출한 함수가 다른 일을 진행할 수 있도록 해주는 것

이렇게 보니 , sync와 blocking / async와 Non-blocking이 서로 매우 밀접한 관계인 것처럼 보인다.

하지만 4개의 개념은 섞일 수 있고 결국 저렇게 표로 나누는 것은 의미가 없다는 결론이다.

실상의 예를 통해 이해를 더욱 쉽게 하며, 개념 정리를 해보자.

본격 Case Study : 대표님, 개발자 좀 더 뽑아주세요..

Blocking & Synchronous

나 : 대표님, 개발자 좀 더 뽑아주세요..

대표님 : 오케이, 잠깐만 거기 계세요!

나 : …?!!

대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)

나 : (과정 지켜봄.. 궁금함.. 어차피 내 일 하러는 못 가고 계속 서 있음)

Blocking & Asynchronous

나 : 대표님, 개발자 좀 더 뽑아주세요..

대표님 : 오케이, 잠깐만 거기 계세요!

나 : …?!!

대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)

나 : (안 궁금함.. 지나가는 말로 여쭈었는데 붙잡혀버림.. 딴 생각.. 못 가고 계속 서 있음)

Non-blocking & Synchronous

나 : 대표님, 개발자 좀 더 뽑아주세요..

대표님 : 알겠습니다. 가서 볼 일 보세요.

나 : 넵!

대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)

나 : 채용하셨나요?

대표님 : 아직요.

나 : 채용하셨나요?

대표님 : 아직요.

나 : 채용하셨나요?

대표님 : 아직요~!!!!!!

Non-blocking & Asynchronous

나 : 대표님, 개발자 좀 더 뽑아주세요..

대표님 : 알겠습니다. 가서 볼 일 보세요.

나 : 넵!

대표님 : (채용 공고 등록.. 지원자 연락.. 면접 진행.. 연봉 협상..)

나 : (열일중..)

대표님 : 한 분 모시기로 했습니다~!

나 : 😍


Node.js를 알아보며 이벤트 기반 비동기 프로그래밍을 알아보자.

참조

한눈에 끝내는 Node.js

nodejs의 내부 동작 원리

스레드 풀 이란?

이벤트 루프는 무엇입니까?

nodejs이벤트처리

nodejs이벤트

profile
if(기록📝) 성장🌱

1개의 댓글

comment-user-thumbnail
2023년 10월 1일

맨날 헷갈렸는데 이제 좀 알겠네요 ! 감사합니다.

답글 달기