Backend-dev/nodeJS & express

[javascript] 콜 스택과 이벤트 루프 | 동기/비동기 처리 차이?

Hannana. 2024. 5. 25. 19:02
728x90
반응형

 

동기 처리란, 쉽게 말해 순차적인 처리를 의미한다.

만약 응답과 요청이 동기라면, 어떤 일이 발생할까?


A 클라이언트가 요청을 보냈다고 하자. Aa서버는 요청을 받고 응답을 한다.

문제 없다.

 

케이스를 바꿔보자.

A 클라이언트와 B 클라이언트가 요청을 보냈다고 하자. Aa 서버는 요청을 각각 받고 각각 응답을 한다.

여기까진 괜찮을 지도 모른다.

 

 

 

그런데 보통, 클라이언트:서버는 다:1 관계다.

서버는 다중 처리를 해야 하는 경우가 대부분이다.

이렇게 요청을 받을 때는 참고로 콜 스택을 사용하기 때문에 계속 요청이 쌓이게 되는 것이다.

이렇게 되면 나중으로 갈수록 대기 시간이 천문학적으로 길어지게 되고

이러한 불편함은 클라이언트가 감수해야 된다. 성능은 크게 떨어지고 서버의 부하도 심해질 것이다.

 

⇒ 고로 서버를 동기로 만들기는 좀 힘들다는 게 결론이다.

요청, 응답 한번에 처리하게 하지 않고

동시에 돌아가는 것처럼 처리하게 하면 어떨까?! 이게 바로 멀티쓰레드의 근간이다.

 

하지만 자바스크립트는 싱글 쓰레드 기반이다.

멀티 쓰레드가 지원이 안되어 다른 방법으로 비동기 처리를 해줘야 한다.

고로 주로 싱글 쓰레드 기반의 이벤트 루프를 이용해 

서버를 돌리는 것으로 알려져있다.

 

 


Q. 싱글 스레드 기반의 이벤트 루프란?

노드는 js기반으로 멀티 쓰레드 지원이 되지 않는다. 대신 비동기 처리를 위해 콜백 큐를 사용한다. 기본적으로는 콜 스택을 사용해 순서대로 실행하지만, 시간이 많이 걸리는 작업을 만나면 잠시 콜백 큐에 저장해두었다가 콜 스택이 비면 하나씩 꺼내와 처리한다. 이것이 이벤트 루프다. 비동기 작업을 효율적으로 처리하고 코드의 블로킹을 최소화하는 방법!
콜백큐에 저장해두었다가, 콜 스백이 비면 하나씩 꺼내와 처리하는 과정으로
→ 비동기 작업을 효율적으로 처리하고 코드의 블로킹을 최소화하여 성능 개선이 목적이다.

 

 

 

다음은 싱글 스레드 이벤트 루프의 예시다. 

const fs = require('fs');
const http = require('http');

// 파일 읽기 (비동기)
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
  } else {
    console.log('File content:', data);
  }
});

// 네트워크 요청 (비동기)
const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};

const req = http.request(options, (res) => {
  let responseData = '';

  res.on('data', (chunk) => {
    responseData += chunk;
  });

  res.on('end', () => {
    console.log('Response from www.example.com:', responseData);
  });
});

req.on('error', (err) => {
  console.error('Error with the request:', err);
});

req.end();

// 타이머 사용 (비동기)
setTimeout(() => {
  console.log('Timeout executed after 2000ms');
}, 2000);

console.log('This log happens before the asynchronous operations');

 

위의 예시는 노드가 기본적으로 제공하는 내장 함수를 이용한 비동기 처리 예시다.

이 외에도 사용자 정의의 비동기 처리도 가능하다.

 

 

 

1. 콜백을 사용한 비동기 처리

function asyncTask(callback) {
  setTimeout(() => {
    console.log('Async Task Complete');
    callback();
  }, 2000);
}

console.log('Start');
asyncTask(() => {
  console.log('Callback Invoked');
});
console.log('End');


asyncTask 함수는 2초 후에 "Async Task Complete"을 출력하고, 콜백 함수를 호출한다.
console.log('Start')와 console.log('End')는 비동기 작업 전에 즉시 실행된다.

작업 양이 간단하기 때문이다.

이걸 비동기 처리를 안하면 간단한 End를 받는 것도 한~참 걸릴 것이다. ‘갇혀버림(blocking)’ 현상 때문이다.
블락킹이 발생하면 다음 응답할 여유가 사라진다.

보통 하나의 작업을 수행하는 동안 프로그램이 다른 작업을 수행하지 못하도록 블로킹된다고 표현한다.
고로 논 블락킹 되어야 한다. 

 

 

<응용>

function fetchData(callback) {
  setTimeout(() => {
    const data = { message: 'Hello, World!' };
    callback(null, data);
  }, 2000);
}

fetchData((err, data) => {
  if (err) {
    console.error('Error fetching data:', err);
  } else {
    console.log('Data received:', data);
  }
});

 

fetchData 함수는 2초 후에 데이터를 반환하고, 콜백 함수를 호출한다.
콜백 함수는 데이터를 받아서 출력한다.

단, 콜백 헬 문제가 발생할 수 있다.

 

※ 콜백 헬이란?

=> 콜백 함수는 함수의 인자로 함수 리터럴을 지정하는 형태이므로 중첩되어 실행 순서가 엉킬 가능성이 있다. 이것을 콜백 지옥, 콜백 헬이라 부른다.

 

 

 


2. 프라미스를 사용한 비동기 처리

function asyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Async Task Complete');
      resolve();
    }, 2000);
  });
}

console.log('Start');
asyncTask().then(() => {
  console.log('Promise Resolved');
});
console.log('End');


asyncTask 함수는 2초 후에 "Async Task Complete"을 출력하고, 프라미스를 해결(resolve)한다.
asyncTask().then은 프라미스가 해결된 후 "Promise Resolved"를 출력한다.
console.log('Start')와 console.log('End')는 비동기 작업 전에 즉시 실행된다.

 

 

 

<응용>

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { message: 'Hello, World!' };
      resolve(data);
    }, 2000);
  });
}

fetchData()
  .then((data) => {
    console.log('Data received:', data);
  })
  .catch((err) => {
    console.error('Error fetching data:', err);
  });


fetchData 함수는 2초 후에 데이터를 반환하는 프라미스를 생성.
fetchData().then을 사용하여 데이터를 받아서 출력한다. (프라미스 체이닝)

 

※ 프라미스 체이닝 : 이거 다음 이거하고(then) → 순서성 확립

 

 

 

 

3. async/await를 사용한 비동기 처리

function asyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Async Task Complete');
      resolve();
    }, 2000);
  });
}

async function runAsyncTasks() {
  console.log('Start');
  await asyncTask();
  console.log('Async/Await Finished');
  console.log('End');
}

runAsyncTasks();


asyncTask 함수는 앞의 프라미스 예제와 동일하다.
runAsyncTasks 함수는 async 함수로, 내부에서 await를 사용하여 asyncTask가 완료될 때까지 기다림
console.log('Start')는 비동기 작업 전에 실행되고,

asyncTask가 완료된 후 "Async/Await Finished"와 console.log('End')가 실행된다.



 

<응용>

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { message: 'Hello, World!' };
      resolve(data);
    }, 2000);
  });
}

async function getData() {
  try {
    const data = await fetchData();
    console.log('Data received:', data);
  } catch (err) {
    console.error('Error fetching data:', err);
  }
}

getData();


fetchData 함수는 앞의 프라미스 응용 예제와 동일함
getData 함수는 async 함수로, 내부에서 await를 사용하여 fetchData의 결과를 기다리고, 데이터를 받아서 출력한다.

-async = 비동기 
-어떤 걸 가지고 기다리는 지 = await
스프링은 블락킹 문제가 없다. 알아서 해주기 때문이다. 그래도 원리는 알아두자..

 

 

 


 

 

 

 

멀티 쓰레드가 지원되지 않는다고 해서 방법이 없는 것은 아니다.

이렇게나 방법이 많기에 자바 스크립트를 이용해도 충분히 동시 처리가 가능한 서버로 쓸 만할 것이다.

 

이상 오늘은 노드js의 특징 중 비동기 실행과 관련 된 방법에 대해 알아보았다.

각 방법을 선택하는 기준은 코드의 가독성 및 유지보수성에 따라 달라지므로

상황에 따라 선택하면 될 듯하다.

 

내부의 구조도 완전히 달라지고 그에 따라 코드의 표현법이 달라지는 것이 재밌었다.

실행 결과가 달라지는 것을 보고 코드 작성법에 따라 동작의 구조가 달라진다는 것이 확 와닿았다.

효율성을 위해서 더 좋은 코드를 작성하는 것에 익숙해지자.

그리고 여러 방법을 자주 익혀두면 좋을 것 같다.

 

 

반응형