NodeJS의 Event Loop는 반드시 이해하고 넘어가야할 핵심 개념이다.
Event Loop 개념
이벤트 루프가 실행되는 'SINGLE THREAD'에서 'NODE JS PROCESS'를 실행 시키고 있다. 이벤트 루프는 호출 함수 안에서 응용 프로그램 코드(콜백 함수)가 실행되는 곳이다. 기본적으로 상위 레벨 코드가 아닌 모든 코드는 이벤트 루프에서 실행된다. 일부분은 "THREAD POOL"로 보내질 수 있지만, 대체적으로 이벤트 루프가 모든 것을 처리한다.
NodeJS는 콜백 함수를 중심으로 만들어졌다. 이는 어떤 작업이 완료되면 호출되는 함수이다. Node는 'Event-driven' 아키텍처를 사용하기 때문이다. 이는 서버에서 "HTTP Request"를 받거나, "Timer expired", "Finished file reading" 등이 끝나자마사 Event를 방출한다는 특성을 가진다. Event Loop가 이러한 이벤트를 캐치하여 그 이벤트와 관련된 콜백 함수를 호출한다.
다시 말해, Event Loop는 중요한 일들이 발생할 때마다 이벤트를 수신하고 콜백 함수를 호출한다. 이벤트 루프가 오케스트레이션을 한다는 건 이벤트를 수신하고 콜백함수를 호출해 더 무거운 작업은 "Thread Pool"에 업로드한다는 뜻이다.
Event Loop의 단계 및 순서
NodeJS를 실행하면 이벤트 루프가 즉시 실행된다. 이벤트 루프에는 여러 단계가 존재하고, 각 단계에는 콜백 큐가 있다. 이벤트 루프가 수신하는 이벤트에서 오는 콜백 큐이다. 어떤 곳에선 콜백 큐나 이벤트 큐가 하나뿐인 것을 알 수 있는데 이벤트 루프의 여러 단계의 각 단계마다 고유한 콜백 큐가 존재한다.
이벤트 큐의 4단계를 알아보자.
1단계는 만료된 타이머에 대한 콜백(Expired timer callbacks)을 다룬다. 예를 들면, setTimeout이 있다. 방금 만료된 Timers로부터 콜백 함수가 생성되면, 해당 함수가 가장 먼저 이벤트 루프로 처리된다. 만약, Timer가 다른 단계(2단계, 3단계 등)를 처리하는 동안 나중에 만료 된다면, 해당 Timer는 이벤트 루프가 1단계로 돌아왔을 때 처리된다. 4단계 모두 같은 방식으로 동작한다. 각 큐에 있는 콜백은 큐에 아무것도 남지 않을 때까지 하나씩 처리된다. 모두 처리된 경우에만 이벤트 루프는 다음 단계로 진입하게 된다.
2단계, I/O polling과 execution of I/O callback 단계이다. I/O는 Input/output을 의미한다. Polling은 기본적으로 새로운 I/O 이벤트를 찾아 처리하고 콜백 큐에 넣는 것을 의미한다. Node 어플리케이션의 Context에서 I/O는 주로 네트워킹이나 파일 엑세스를 의미한다. 이 단계에서 코드의 99%가 실행된다.
3단계, setImmediate 콜백이다. SetImmediate는 특별한 타이머로 I/O 폴링과 실행 단계 직후에 콜백을 처리하고 싶을 때 사용한다. 고급 유스케이스에서 중요할 수 있다.
4단계, Close 콜백이다. 중요한 단계는 아니지만 완결성을 위해 여기에서 모든 닫힌(Close) 이벤트가 처리된다. 예를 들어, 웹 서버나 WebSocet이 종료되는 경우이다.
4단계 이외에도 2개의 Queues가 더 존재한다. nextTick() 큐와 Promise로 동작하는 Microtasks 큐이다. 이 둘중 처리될 콜백이 있다면, 이벤트 루프의 전체 단계가 끝나는게 아닌 현재 단계가 끝난 직후에 실행된다. 예를 들어, setTimeout 콜백이 실행되는 동안 Promise가 API 호출하여 데이터를 찾고 반환한다고 가정해보자. 이 경우 Promise 콜백은 타이머가 완료된 직후에 실행된다. 이는 다음 nextTick() 큐에도 적용된다. nextTick()은 현재 이벤트 루프 단계 직후에 특정 콜백을 꼭 실행해야할 때 사용할 수 있는 기능이다.
setImmediate와 비슷한데 차이점은 I/O 콜백 단계 후에만 실행된다는 것이다. 비슷한 점은 둘다 고급 유스 케이스라는 점이다. 현재 강의에서는 필요하지 않을 수 있는 개념이다. 이벤트 루프의 One Tick, 한 사이클을 정리했다. Tick은 기본적으로 이 루프의 한 사이클이다. 이제 이벤트 루프가 다음 틱까지 계속되어야 할지 프로그램이 종료되어야할지 결정해야 한다.
이때 Node는 단순히 Background에 타이머나 I/O 작업이 실행 중인지를 확인한다. 아무것도 없다면 응용 프로그램에서 나가게 된다. 타이머나 I/O 작업이 보류 중이라면 이벤트 루프를 계속 실행해 바로 다음 틱으로 가게된다. 더 자세한 내용은 알고 싶으면 Node 공식 문서를 찾아보면 된다.
Node의 이벤트 루프는 정확히 이해해야 한다. 그래야 잘못된 코드를 디버그할 수 있기 때문이다.
이벤트 루프 요약 : Node VS Others
가장 중요하게 이해야할 부분이 있다. 이벤트 루프가 노드에서 비동기 프로그래밍을 가능하게 한다는 것이다. 이는 다른 플랫폼과 완전히 다른 것이다. 발생하는 모든 이벤트를 처리하고, 스레드 풀에 무거운 작업을 OffLoad함으로써 오케스트레이션을 수행한다. Node에서는 모든게 하나의 스레드에서 작동한다. 그래서 동시에 많은 사용자가 같은 스레드에 액세스 할 수 있다. 덕분에 노드는 가볍고 확장성이 높아졌다. 하지만 동시에 스레드를 차단할 위험이 존재한다. 느리게하거나 멈추게할 위험이 있다.
아파치 PHP의 경우 새 사용자를 위해 새로운 스레드가 생성된다. 리소스 집약적인 형태이다. 하지만 차단 당할 위험은 없다. 해당 아키텍쳐로 초보자가 PHP를 좀 더 쉽게 쓸 수 있지만, 단점이 존재하게 된다.
이벤트 루프를 Block되지 않게 하는게 중요하다. 이에 대한 몇가지 지침이 있다.
첫번째, Sync 콜백 함수에서 fs, crypto 그리고 zlib 모듈을 사용하면 안된다. 이전 프로젝트에서 동기화 버전을 사용했다. 하지만 콜백 밖의 상위 레벨 코드였다. 이벤트 루프가 시작되기 전에 코드가 실행되기 때문에 동기화 버전을 사용하는 것은 문제되지 않는다. 이건 아주 명백하게 이벤트 루프에서 복잡한 계산을 수행하지 않는다.
두번째, 큰 개체에서 JSON을 조심해야한다. 구문 분석이나 문자열화에 시간이 많이 걸릴 수 있기 때문이다.
세번째, 너무 복잡한 정규식은 사용하면 안된다.
차단 문제에는 몇가지 해결책이 있다. 수동으로 스레드풀에 오프로드하거나 Child Process가 있다.
'NodeJS' 카테고리의 다른 글
[NodeJS] V8과 Libuv 그리고 C++ (0) | 2024.06.22 |
---|