동기와 비동기

gamjaJeon·2023년 5월 28일
0

1. 동기와 비동기

동기

서버에 요청을 보냈을 때 응답이 돌아와야 다음 동작을 수행 하는 처리방식

비동기

요청을 보냈을 대 응답 상태와 상관없이 다음 동작을 수행 하는 처리방식

그럼 왜 javascript는 비동기인걸까?

자바스크립트의 메인스레드인 이벤트 루프가 한 개의 콜 스텍으로 이루어진 Single Thread 이기 때문이다.
지금까지의 내용을 종합적으로 미루어 보았을 때 이것은
하나의 스레드 = 하나의 콜 스텍 = 한번에 하나의 작업을 의미한다.
그렇다면 여기서 스레드(thread)란 무엇일까?
그 것을 이해하기 위해 먼저 프로세스에 대해 알아보자.

프로세스 란?

실행중에 있는 프로그램 즉 메모리상에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)이다.
스케줄링(여러 프로세스가 번갈아가며 사용하는 자원을 어떤 시점에 어떤 프로세스에게 자원을 할당할 지 결정하는 것)되는 작업(task)과 같은 의미로 사용된다.
프로세스는 최소 하나 이상의 스레드를 가지고 있고 실제로 프로세스 안에서는 스레드 단위로 스케줄링이 이루어진다.
그리고 이 프로세스엔 context가 존재한다.

프로세스의 문맥(context)란?

프로세스가 현재 어떤 상태에서 수행되고 있는지 정확히 규명하기 위해 필요한 정보를 말하는 것.

현대의 운영체제는 여러 프로세스가 함께 수행되는 시분할 시스템 환경이다.

시분할 시스템 환경
아주 짧은 시간을 두고 빠르게 여러 개의 프로그램을 전환하면서 실행 하는 것

시분할 시스템 환경에서는 타이머 인터럽트에 의해 짧은 시간동안 CPU를 점유하고 다른 프로세스에게 넘겨주고 다시 차례가 되면 CPU를 점유하여 명령을 수행한다.

타이머 인터럽트
마이크로프로세서의 내부 시계를 이용하여 일정 시간일 때 발생되는 인터럽트를 말한다.
쉽게 말해 cpu가 일을 하고 있는데 거기에 태클을 거는 행위이다.

인터럽트
A라는 작업을 수행하던 도중 A를 중단(or 보류) 시키고 B작업을 수행하게 하는 것

때문에 다시 명령을 수행하기 위해서 이전에 어디까지 명령을 수행했는지 정확한 수행 시점과 상태를 재현할 수 있는 정보가 필요했고이 정보를
프로세스 문맥(process context)이라 한다.

이 프로세스의 문맥은 크게 3가지로 나뉜다.

1. 하드웨어 문맥

CPU 수행 상태를 나타내는 것으로 PC(Program Counter)와 각종 레지스터에 저장하고 있는 값들을 말한다.

2. 프로세스의 메모리 영역

Code,Data,Stack,Heap으로 구성된 프로세스만의 독자적인 주소 공간이다.

  • Code 영역
    실행할 프로그램의 코드나 명령어들이 기계어 형태로 저장된 영역이다.
    CPU는 코드영역에 저장된 명령어들을 하나씩 처리한다.
  • Data 영역
    코드에서 선언한 전역 변수와 정적 변수가 저장되는 영역이다. 프로그램이 실해되면서 할당되고 종료되면서 소멸한다.
  • Stack 영역
    함수 안에서 선언된 지역변수,매개변수,리터값등이 저장된다. 함수 호출시 기록되고 종료되면서 제거된다.
  • Heap 영역
    관리가 가능한 데이터 이외의 다른 형태의 데이터를 관리하기 위한 자유공간이다.

3. 커널상의 문맥

프로세스 관리를 위한 자료 구조인 PCB(Process Control Block)Kernel stack(커널내의 주소)을 말한다.

드디어 스레드(thread)란 무엇일까?

하나의 프로세스 내에서 더 작은 단위들로 각각 독립적으로 실행되나, 그 각각은 제어가 가능한 흐름이고 실행컨텍스트 또는 경량급 프로세스라고도 한다.
그리고 각 스레드는 같은 프로세스에 속한 다른 스레드들과 함께 코드,데이터 섹션, 열린 파일 등을 공유하게 된다.
ex) 같은 사무실에서 일하는 일꾼들이 서로 업무를 위한 정보를 공유하는 것 처럼 말이다.
즉 단일 스레드 환경은 하나의 응용 프로그램이 단 하나의 실행의 흐름을 만들 수 있다.
그렇기 때문에 한 번에 한가지 일 만 수행 가능하며 이 경우 아까 프로세스 부분에서 설명한 인터럽트 등의 비동기적 처리가 어려워지게 된다.
ex) 내가 사무실에서 혼자 일을 하게 된다면 지금 하는 업무가 끝나기 전까지 다른 업무를 시작할 수 없듯이.
이러한 이유 때문에 자바스크립트는 비동기적 처리를 위한 방법을 고안하게 된다.

자바스크립트의 비동기 런타임 과정

V8 : 크롬 기반 자바스크립트의 엔진이다.
하지만 이 V8소스 안에서는 setTimeout,DOM,HTTP 요청을 관리하는 코드들을 찾을 수는 없다.
보통 자바스크립트에서의 비동기 코딩을 말하라면 바로 떠오르는 요소들인데 자바스크립트 엔진 안에 이 요소들이 없다니 어떻게 된 일일까?

그림에서 보이는 것과 같이 자바스크립트 런타임 환경에서는 Call Stack 뿐만이 아닌 Web API ,Task Queue,Event Loop등이 서로의 역할을 분담하여 작업을 진행하게 되는 것이다.

V8 엔진: 자바스크립트 코드를 평가하여 객체 등의 동적 데이터가 할당되는 memory heap, context의 실행을 담당하는 하나의 call stack(= execution context stack)을 가진다.
Call Stack : 자바스크립트에서 수행해야 할 함수들을 순차적으로 스택에 담아 처리
Web API : 웹 브라우저에서 제공하는 API로 AJAX나 Timeout등의 비동기 작업을 실행
Task Queue : Callback Queue라고도 하며 Web API에서 넘겨받은 Callback 함수를 저장
Event Loop : Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김 pending 상태인 자바스크립트 task와 microtask를 execution context stack에 push하여 실행시킨다. 다음 loop iteration 이전에 렌더링이나 페인팅이 필요하면 먼저 수행한다.

원래 자바스크립트는 위에서 언급했다시피 하나의 콜 스택을 가지고 있기 때문에 한번의 하나의 작업을 할 수 있는데 이것을 콜 스택을 통해 알아보자

console.log("첫 번째");
setTimeout(() => console.log("두 번째"),1000);
console.log("세 번째");

원래의 하나의 콜스텍만 가지고 있는 자바스크립트의 동기적인 흐름으로 따지자면

//첫 번째
1초 대기 후
//세 번째
//두 번째

의 순서대로 출력하는 것이 맞는 순서일 것이다.

하지만 위에 언급했다 시피 setTimeout은 V8에서 지원하는 API가 아닌 런타임 중 Web API가 지원하는 기능이다 따라서

  1. console.log("첫 번째")가 Call Stack에 쌓여 바로 실행 되어 제거
  2. 차례대로 setTimeout이 Call Stack에 쌓여 바로 실행 이 때는 바로 제거가 되는 것이 아닌 Web API로 넘어가 이곳에서 제공하는 Timeout이 timer를 생성해 해당 시간이 완료 될때까지 실행하고 있는다.
  3. Web API 가 Timeout을 넘겨받아 실행하고 있는 동안 Call Stack은 빈 상태이기 때문에 바로 다음 번 줄인 console.log("세 번째")가 실행되어 제거된다.
  4. 이 과정이 진행되는 동안 Web API는 실행중이었던 Timeout에 설정해 둔 1000ms 도달 시점에 Task Queue로 Call Back을 전달한다.
  5. 이 때 Event Loop가 Call Stack에 스택이 없는 것을 확인하면, setTimeout의 콜백함수를 Call Stack으로 호출시킨다.
  6. 드디어 Call Stack에 불러온 콜백함수가 실행되고 제거된다.

이 과정을 거친 후의 출력되어지는 순서는 아래와 같다.
//첫 번째
//두 번째
//세 번째

Callback - Promise - Async/Await

Callback이란?

콜백함수는 다른 함수에 매개변수로 넘겨준 함수를 말한다. 함수를 명시적으로 호출하는 방식이 아니라 특정 이벤트가 발생했을 때 시스템에 의해 호출된다.
call(부르다)back(다시)
callback = 특정 상황에 도달하면 다시 불러줘

setTimeout(() => console.log("두 번째"),1000);

이 방식은 비동기 함수를 다른 함수에 호출하는 방법이 아닌 함수 내부에 직접적으로 정의하여 호출 한 방식이다.

  • 사용자 데이터를 백엔드에서 받아오는 클래스를 만든다고 가정하여
    //Callback Hell example
class UserStorage {
    loginUser(id, password, onSuccess, onError){
        setTimeout(() => {
            if (
                (id === 'ellie' && password === 'dream') ||
                (id === 'coder' && password === 'academy')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
            }
        }, 2000);
    }

    getRoles(user, onSuccess, onError) {
        setTimeout(() => {
            if (user === 'ellie' && password === 'dream') {
                onSuccess({ name:'ellie', 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, 
    // 콜백 1
    user => {
        userStorage.getRoles(
            user,
            //콜백 2
            userWithRole => {
                alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
                // 여기에 콜백 3
                    // 여기에 콜백 4
                        // 여기에 콜백 5 . . . . .
            },
            error => {
                console.log(error);
            }
        );
    }, 
    error => {
        console.log(error);
    }
);

Callback Hell이란?

Callback 안에 콜백이 계속 들어가는 현상...

  1. 가독성이 떨어진다.
  2. 콜백 체인이 길어질수록 디버깅하고 문제점을 찾는데 어려움을 겪는다.
  3. 위와 같은 이유로 유지보수 또한 어려워지게 된다.

그래서 자바스크립트는 이와 같은 단점을 보완해 더 깔끔한 비동기 처리가 가능하도록 ES6 Promise 객체, ES2017에서 async/await 키워드를 제공하기 시작했다.

Promise란?

직역 하자면 약속 이라는 뜻으로
자바스크립트에서 비동기 처리를 위해 ES6부터 도입된 ECMAScript 표준 빌트인 객체다.
Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는데 이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.

정해진(약속되어진)기능이 정상적으로 수행되고 난 후에는 성공의 메시지(resolve)와 함께 결과값이 전달된다.
그러나 수행중 예상치 못한 오류가 발생 할 시에는 에러(reject)를 전달해 준다.

const promise = new Promise((resolve,reject) => {
	console.log('doing something...');
    setTimeout(() => {
    	resolve('ellie');
        reject(new Error('no network')
    }, 2000);
});

promise
	.then(value => {
    	console.log(value);
        //ellie
    })
    .catch(error => {
    	console.log(error);
        //error: no network
    })
    //finally는 성공 실패 상관없이 무조건 수행
    .finally(() => {
    	console.log('finally');
    });

promise는 비동기 처리 상태와 처리 결과를 관리하는 객체로서 비동기 처리가 어떻게 진행되고 있는지 다음과 같은 상태 정보를 가진다.

또한 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 you password');

userStorage.loginUser(id, password) 
.then(userStorage.getRoles) 
// .then(user => userStorage.getRoles(user)); 인자가 똑같으니 생략 가능하다.
.then(user => alert(`hello ${user.name}, you have a ${user.role} role`)) 
.catch(console.log);
Promise.resolve().then(() => console.log('first promise'));
setTimeout(() => {
  console.log('first setTimeout');
  Promise.resolve().then(() => console.log('first inner promise'));
}, 0);

sleep(5000) // 5초 지연

setTimeout(() => console.log('second setTimeout'), 0);
Promise.resolve().then(() => console.log('second promise'));

async/await이란?

프로미스도 콜백만큼은 아니지만 채이닝이 길어질수록 코드가 난잡해보일 수도있다. 때문에 async/await을 이용해 프로미스를 좀 더 간결하고 간편하고 동기적으로 실행되는 것처럼 보이게 만들어준다.
이 async/await은 새로운 것이 아닌 기존에 있는 promise에 새로운 API를 제공하여 동기식으로 코드를 작성하는 것 처럼 간편하게 만들어줄 수 있다.

async function getApple() {
	await delay (1000);
	return '🍎';
}
async function getBanana() {
	await delay (1000) ;
	return '🍌;
}

async function pickFruits() {
	const applePromise = getApple() ;
	const bananaPromise = getBanana () ;
	const apple = await applePromise;
	const banana = await bananaPromise;
	return `${apple} + ${banana}`;
}
    
pickFruits().then(console.log);//🍎 + 🍌

function pickAllFruits() {
	return Promise.all(IgetApple(), getBanana()]). then (fruits =>
		fruits.join(' + ')
    );
}
pickAllFruits (). then (console. log) ;

function pickOnlyOne () {
	return Promise. race( [getApple(), getBanana()]);
}
pickOnlyOne().then(console.log) ;
profile
프론트엔드를 꿈꾸는 강원도 감자

0개의 댓글