동기적 (Synchronous)
- 직렬적
- 요청을 보낸 후, 응답을 받아야 다음 동작이 이루어지는 방식이다.
- 나머지 동작은 대기한다.
비동기적 (Asynchronous)
- 병렬적
- 요청을 보낸 후 응답 여부에 상관 없이, 다음 동작을 하는 방식이다.
- 콜백 함수 : 비동기 요청시 응답 후 처리할 콜백함수가 처리된다.
비동기 처리 문법
setTimeout
setTimeout(function[, delay, arg1, arg2, ...]);
- 첫 번째 파라미터에 넣은 함수를, 두 번째 파라미터에 넣은 시간이 흐른 후 호출해준다.
- setTimeout을 사용하면, 우리가 정한 작업이 백그라운드에서 수행되기 때문에 기존의 코드 흐름을 막지 않고, 동시에 다른 작업들을 진행할 수 있다.
- function : 타이머가 완료된 뒤 실행할 함수다.
- delay : 주어진 함수 또는 코드를 실행하기 전에 기다릴 밀리초 단위 시간이다.
- arg : function에 전달할 추가 매개변수다.
- return : timeoutID가 반환되는데, 양의 정수로 생성한 타이머를 식별할 때 사용한다. 이 값을 clearTimeout()에 전달하면, 타이머를 취소할 수 있다.
setInterval()
setInterval(func)
setInterval(func, delay)
setInterval(func, delay, arg0)
setInterval(func, delay, arg0, arg1)
setInterval(func, delay, arg0, arg1, /* … ,*/ argN)
- func : delay 마다 실행되는 function
- delay : 타이머가 지정된 함수 도는 코드 실행 사이에 지연해야 하는 밀리초 단위의 시간이다.
- arg : 타이머가 만료되면, func에서 지정한 함수로 전달되는 추가 매개변수
- return : intervalID가 반환되는데, 타이머를 식별하는 0이 아닌 숫자다. 이 값을 clearInterval()에 전달해 interval을 취소할 수 있다.
콜백함수
- 함수 타입의 값을 파라미터로 넘겨줘서, 파라미터로 받은 함수를 특정 작업이 끝나고 호출해주는 것을 의미한다.
- 위의 비동기 작업이 끝난 후, 어떠한 작업이 처리하고 싶다면, 콜백 함수를 파라미터로 전달해주면 된다.
- ex
function work(callback) {
setTimeout(() => {
const start = Date.now();
for (let i = 0; i < 1000000000; i++) {}
const end = Date.now();
console.log(end - start + 'ms');
callback();
}, 0);
}
console.log('작업 시작!');
work(() => {
console.log('작업이 끝났어요!')
});
console.log('다음 작업');
// 작업 시작!
// 다음 작업
// work 결과
// 작업이 끝났어요!
https://learnjs.vlpt.us/async/
Promise
let promise = new Promise((resolve, reject) => {
...
reject(new Error());
...
});
promise
.then(
...
)
.catch(
console.log(error);
);
- resolve : Promise를 성공하면 호출한다.
- reject : Promise를 실패하면 호출한다.
- 작업이 끝나고 다른 작업을 할 때는 .then(..)을 붙여서 사용하면 된다.
- then 내부에 넣은 함수에서, 또 Promise를 리턴하게 된다면, 연달아 사용할 수 있고 코드의 깊이가 깊어지 콜백 지옥을 막을 수 있다.
- 하지만 에러가 어디서 발생했는지 알아내기 어렵고, 조건에 따라 분기를 나누고, 특정 값을 공유하며 작업을 처리하기도 까다롭다.
async / await
- Promise를 더욱 쉽게 사용할 수 있게 해준다.
- async : 함수를 선언할 때 함수의 앞부분에 키워드를 붙여준다.
- await : Promise의 앞부분에 키워드를 붙여준다.
- 이렇게 하면 해당 프로미스가 끝날때까지 기다렸다가 다음 작업을 수행할 수 있다.
function hello() {
return new Promise( ... );
}
function wowError() {
await hello();
const error = new Error();
throw error;
}
async function hi() {
...
try {
wowError();
} catch (e) {
...
}
...
}
hi();
- 함수에서 async를 사용하면, 해당 함수는 결과값으로 Promise를 반환한다.
- 에러를 발생시킬 때는 throw를 사용하고, try / catch 문을 사용해 에러를 잡아낸다.
Promise
Promise.all()
- 함수를 연달아 사용하면, 동기적으로 수행하는 것과 같은 시간이 걸리는데,
Promise.all()을 사용하면, 동시에 작업을 시작해 비동기적으로 수행된다. - Promise.all()을 사용할 때, 등록한 프로미스 중 하나라도 실패하면, 모든게 실패한 것으로 간주한다.
functino hello() {
return new Promise();
}
const bye = async () => {
};
const hi = async () => {
}
async wow() {
const results = await Promise.all([bye(), hi()]);
/** const [bye, hi] = await Promise.all([
bye(),
hi()
]); */
}
Promise.race()
- 여러개의 프로미스를 실행했을 때, 가장 빨리 끝난 것 하나만의 결과 값을 가져온다.
병렬처리와 스레드
- 멀티 스레드는 프로세스가 동시에 여러 작업을 수행하는 스레드다.
- 응답성, 자원 공유, 경제성, 확장성의 장점이 있다.
동시성 (Concurrency)
- 싱글 코어에서 멀티 스레드를 동작 시키기 위한 방식이다.
- 싱글 프로세서가 여러 스레드를 번갈아 수행해, 빠르게 진행 시 여러 스레드가 동시에 실행되는 것처럼 보인다.
병렬성 (Parallelism)
- 멀티 코어에서 멀티 스레드를 동작시키는 방식이다.
- 멀티 코어가 각 스레드를 동시에 수행한다.
스레드 풀
- 스레드를 요청할 때마다 새로운 스레드를 생성, 수행, 삭제를 반복하면 성능이 저하된다.
- 미리 스레드 풀에 여러개의 스레드를 만들어 두고, 요청이 오면 스레드를 풀에서 할당해주는 방법이다.
- 단, 너무 많이 만들 경우 노는 스레드가 발생하고, 메모리 낭비가 생긴다.
node.js 스레드
- node.js 서버는 프로세스 안에 단일 스레드에서 실행된다.
- node.js는 이벤트 기반으로, 애플리케이션의 대부분의 일이 이벤트 루프에서 처리된다.
- 이벤트 루프가 처리하기 무거운 작업을 이벤트 루프에서 처리하면, 스레드가 block되어 시간이 많이 소요될 수 있다.
- block을 방지하기 위해 node.js의 프로세스에는 스레드 풀이 있다.
- 이벤트 루프는 무거운 작업들을 스레드풀이 처리하게끔해 일을 분담한다. -> 자동으로 처리된다.
객체지향 설계와 데이터 흐름
앨런 케이(객체지향 용어 만들고 대중화 시킨 인물)가 생각하는 OOP의 본질은
- messaging (메시징) : 다른 객체의 데이터나 프로시져가 필요할 때는 메시지 <-> 요청 한다. 메시지를 받는 객체는 스스로 처리 방법을 선택한다.
- hiding of state-process (캡슐화) : 관련 있는 데이터와 프로시저를 찾아 묶고, 다른 객체가 내부를 건드리지 못하게 한다.
- extreme late-binding (동적 바인딩) : 메시지를 받는 객체는 그때 그때 달라질 수 있다.
장점
- 변경 가능한 공유 데이터가 최소로 줄어든다.
- How (구현) 부분을 쉽게 바꿀 수 있다.
- 메시지를 실제로 처리하는 객체를 쉽게 바꿀 수 있다.
이벤트 루프 (Event Loop)
JS는 Single Thread 기반 언어인데, 동시에 여러 작업을 처리하는 것을 볼 수 있다.
=> 이러한 동시성을 지원하는 것은 이벤트 루프가 비동기적인 일처리를 가능하게 하기 때문이다.
비동기 요청은 JS가 아닌, JS 엔진을 구동하는 환경, 브라우저나 Node.js가 담당한다.
브라우저 환경
- 비동기 호출을 위한 setTimeout이나 XMLHttpRequest와 같은 함수들은 Web API 영역에 따로 정의되어 있다.
- 이벤트 루프와 태스크 큐와 같은 장치들도, JS 엔진 밖에 구현되어 있다.
Node.js 환경
- 브라우저와 거의 유사하지만, 비동기 I/O를 지원하기 위해 libuv 라이브러리를 사용해, libuv가 이벤트 루프를 제공한다.
- JS 엔진은 비동기 작업 처리를 위해, Node.js의 API를 호출하고, 이 때 넘겨진 콜백은 libuv 이벤트 루프를 통해 스케줄되고, 실행된다.
- libuv
- c++로 작성된, Node.js의 비동기 I/O 라이브러리
- 운영체제의 커널을 추상화한 Wrapping 라이브러리로, 커널이 지원하는 비동기 API를 알고있다.
- 요청한 작업을 커널이 지원하지 않는다면, 워커 스레드가 담긴 스레드 풀을 사용한다.
- 기본적으로 4개의 스레드를 가지는 스레드 풀을 생성한다.
JS의 single thread 기반의 언어라는 말은, JS 엔진이 single call stack을 사용한다는 관점에선 사실이다.
실제 구동되는 환경에선, 여러 개의 스레드가 사용되고, 이러한 구동 환경이 JS 환경과 상호 연동하기 위해 사용하는 장치가 이벤트 루프인 것이다.
Task Queue
- 동기적인 Task들은 Call Stack에 의해 순차적으로 처리되지만, setTimeout과 같은 비동기 이벤트들은 Call Stack에 들어오더라도, Web API와 Task Queue(Callback Queue)를 거친 뒤, Call Stack이 비었을 때만 이벤트 루프에 의해 Call Stack으로 옮겨저 다시 처리된다.
이벤트 큐
Event
애플리케이션 내에서 발생한 응답 가능한 사건이다.
- 이름대로 어떤 사건이다. ex) 클릭, 스크롤, 입력 등
- 2 종류가 있다.
- 시스템 이벤트 : libuv 라이브러리가 적용된 C++ 코어에서 처리한다. (인터넷 연결, 파일 열고 닫기 등)
- JS 코어에서 처리되는 상위 단계의 이벤트 : Node.js의 Event Emitter에서 관리
- 관련 메소드
Event Handler
이런 이벤트가 발생했을 때, 그에 맞는 반응을 위해 일반적으로 함수에 연결되는데, 이 함수를 이벤트 핸들러(Event Hnadler) 라고 한다.
Event Emitter
특정 이벤트에 listener 함수를 달아서, 이벤트가 발생했을 때 이를 catch할 수 있도록 만들어진 API다.
- 일반적으로 이벤트 리스너가 원래 등록된 이벤트 핸들러보다 나중에 호출되기 때문에, 비동기처럼 보인다.
- Event Emitter의 인스턴스 자체 내에서 이벤트와 연결된 모든 이벤트와 리스너를 추적한다.
- 즉, Event Loop의 큐를 사용하는 것이 아니라, 단순히 이벤트 리스너 함수들이 배열로 저장된 이벤트 객체다.
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
myEmitter.on('myevent', () => console.log('myevent'));
myEmitter.emit('myevent');
console.log('hello');
// myevent
// hello
- EventEmitter는 동기적으로 모든 이벤트 핸들러를 호출한다.
Events 메소드
emitter.addListener(event, listener) //on() 메소드와 같습니다. 이벤트를 생성하는 메소드입니다.
emitter.on(event, listener) //addListener()과 동일합니다. 이벤트를 생성하는 메소드입니다.
emitter.once(event, listener) //이벤트를 한 번만 연결한 후 제거합니다.
emitter.removelistener(event, listener) // 특정 이벤트의 특정 이벤트 핸들러를 제거합니다. 이 메소드를 이용해 리스너를 삭제하면 리스너 배열의 인덱스가 갱신되니 주의해야 합니다.
emitter.removeAllListeners([event]) // 모든 이벤트 핸들러를 제거합니다.
emitter.setMaxListeners(n) // n으로 한 이벤트에 최대허용 개수를 지정합니다. node.js는 기본값으로 한 이벤트에 10개의 이벤트 핸들러를 작성할 수 있는데, 11개 이상을 사용하고 싶다면 n값을 넘겨주면 됩니다. n값으로 0을 넘겨주면 연결 개수 제한이 사라집니다.
emitter.emit(eventName[, ...args]) // 이벤트를 발생시킵니다.
Node.js 이벤트 루프 구조
이벤트 루프는 Node.js가 여러 비동기 작업을 관리하기 위한 구현체다.
비동기 작업들을 모아서 관리하고, 순서대로 실행할 수 있게 해주는 도구로, 아래와 같이 구성되어 있다.
- Timer : setTimer
- Pending Callbacks : 이전 루프 순회하며 등록된 I/O 콜백 실행하는 단계
- Idle, Prepare : Node.js 내부관리를 위한 단계로 사용자 애플리케이션 레벨에선 고려X
- Poll : 파일 시스템 I/O, 네트워크 I/O 마치고 실행될 콜백들을 처리하는 단계
- Check : setImmediate()에 의해 스케줄링된 콜백들을 처리하는 특수 단계
- Close Callbacks : 이벤트 루프 마지막 단계로, 소켓 연결 종료와 같은 close 이벤트와 관련된 콜백들 처리하는 단계
위 순서로 Event Loop 순환한다.
각 항목은 Phase 페이즈를 의미하고, 다음 페이즈로 넘어가는 것을 틱(Tick)이라고 한다.
- 각 페이즈는 자신만의 큐를 하나씩 가지고 있다. -> Event Queue
- 각 큐에는 이벤트 루프가 실행해야 하는 작업들이 순서대로 담긴다.
- 만약 큐에 있는 작업을 다 실행하거나, 시스템의 실행 한도에 다다르면, Node.js는 다음 페이즈로 넘어간다.
참고자료
- 동기, 비동기1
- JS 동기, 비동기 처리
- Promise
- 객체지향 프로그래밍의 본질 -> 내용이 너무 좋다.
'boostcamp' 카테고리의 다른 글
Day16 학습 정리 (0) | 2023.07.31 |
---|---|
Day13 학습정리 (0) | 2023.07.26 |
Day09 학습 정리 (0) | 2023.07.20 |
Day08 학습 정리 (0) | 2023.07.19 |
Day07 학습정리 (0) | 2023.07.18 |