앞서 Spring은 기본적으로 멀티스레드 방식, Thread Pool에서 하나의 요청에 하나의 쓰레드를 배정하는 방식으로 작동한다. 이와 다르게 Javascript 런타임 중 하나인 NodeJs는 싱글스레드 방식으로 작동한다. 따라서 둘의 특징과 차이점을 공부해 본 내용을 정리해보았다.
JavaScript 동작원리
JavaScript를 사용하면서 흔하게 접하는 것이 콜백(Callback)함수 개념이다. 주로 비동기 작업(Ajax, Timer)에 콜백함수를 연결하여 해당 콜백함수를 통해 비동기 작업의 결과를 통지받는 식으로 작성된다. 이처럼 Javascript에서는 비동기 작업마다 콜백함수를 대응하여 생각하게 된다.
JavaScript의 특징은 크게 두가지이다. 싱글스레드와 비동기 논블로킹 I/O 이다. 이 둘은 상충하는 개념으로 생각되지만 Javascript는 이 두가지 특징을 모두 지니고 있다.
간략하게 Javascript의 구조를 그림으로 알아보면,
좌측은 Javascript 엔진의 동작을, 우측은 Javascript 런타임(해당 그림에서는 Web 브라우저)이 지원하는 API를 나타내고 있다.
Javascript는 단일 스레드에서 작동하므로 하나의 Heap 메모리와 하나의 Call Stack을 갖게 된다.
이때 Web API는 웹 브라우저에서 제공하는 API로, 비동기 처리와 관련된 작업은 내부적으로 비동기 처리등을 메인스레드가 아닌 자신의 스레드에서 수행하고, 수행한 결과에 대한 콜백함수를 이벤트 루프를 통해 전달해준다.
이벤트 루프(Event Loop)
이벤트 루프는 비동기 I/O 처리와 관련된 요청들을 관리하는 역할을 한다. 비동기 작업 요청을 받으면 해당 비동기 작업을 Web API에 위임하고 해당 비동기 작업들을 처리한다. 이후에 완료된 작업에 대한 콜백함수를 다시 Calback Queue(Event Queue, Task Queue 등 다양하게 부르는 것 같다..) 에 등록하고 이벤트 루프는 Call Stack이 비어있을때 완료된 Callback 함수들을 Call Stack에 전달해준다. 물론 실제로는 단일한 Callback Queue가 존재하는 것이 아닌, 루프 자체가 여러개의 Phase로 나뉘어져 각 Phase에 Queue가 존재한다.
위와 같은 작업들을 루프를 돌며 처리를 하기 때문에 이벤트 루프라고 불린다.
즉, 자바스크립트 엔진 자체는 싱글 스레드로 작동하지만, 런타임 내부에서는 이벤트 루프를 통해 비동기 처리를 위해 멀티 스레드처럼 작동한다는 것이다.
이때 오해하면 안되는 것이, 이벤트 루프라는 작업을 하는 쓰레드가 따로 존재하는게 아니라, 자바스크립트는 Single Thread이므로 메인 쓰레드에서 작동한다.
NodeJS 동작원리
NodeJS는 Web API 대신, libuv 라이브러리를 사용하여 비동기 작업을 처리한다. libuv는 Thread pool의 worker thread 또는 OS 커널 스레드를 통해 비동기 처리들을 해당 Thread Pool에 있는 worker thread를 통해 처리한다.
이때 눈여겨 보아야 할 부분은 libuv 자체에서 비동기 처리들을 자신들의 worker thread에서 처리할 수 있는 작업인지, OS 커널 영역에서 처리하는 작업인지 구분하여 비동기 작업을 처리한다는 것이다.
이때 이벤트 루프의 동작을 정리하면
- 요청이 들어오면 해당 요청이 Blocking I/O인지 아닌지 판별.
- 커널의 비동기 I/O (윈도우의 IOCP, 리눅스의 AIO)의 지원을 받을 수 있는 Non-Blocking I/O 요청이면 커널의 interface로 해당 요청을 처리 한 후 Event Queue에 callback을 등록.
- Blocking I/O라면 (예를 들면 File / Network 작업들) libuv 내의 별도의 Thread Pool에서 Worker Thread를 선택하여 작업을 위임. Worker Thread는 작업을 완료한 후 Event Queue로 callback을 등록.
- Event Loop는 주기적으로 call stack이 비어있는지 체크하고 Event Queue에 실행 대기중인 callback이 있다면 callback들을 call stack으로 이동시켜 Main Thread에 의해 실행될 수 있게 만들어줌.
이처럼 NodeJS는 싱글스레드이지만, 런타임에서 제공하는 API, 라이브러리 덕분에 시간이 오래걸리는 작업을 비동기로 처리할 수 있다는 특징이 있다.
Spring vs NodeJS == 멀티스레드 vs 싱글스레드?
간단하게 생각하면 Spring(멀티스레드) vs NodeJS(싱글스레드) 라고 생각할 수 있다. 하지만 실제로 NodeJS 는 싱글스레드로'만' 작동한다고 보기는 어렵다. 따라서 NodeJS 의 특징은 단순 싱글스레드의 특징과 같다고만 생각할 수는 없다.
기본적으로 멀티스레드 vs 싱글스레드 라고 한다면
멀티스레드
장점
- 작업을 병렬적으로 처리하여 CPU 자원을 효율적으로 사용할 수 있다.
단점
- 공유 자원에 대해 Race condtion이 발생하게 된다.(이게 일단 매우 hell이다..)
- 스레드 context switching에 대한 오버헤드가 발생한다.
싱글스레드
장점
- 프로그래밍의 난이도가 비교적 쉽고(공유자원에 대한 문제가 없기 때문) 스레드 관리에 필요한 CPU 메모리를 사용하지 않기 때문에 메모리를 적게 사용한다.
단점
- 연산이 오래걸리는 작업에 대해 그 작업이 완료되어야 다른 작업을 수행할 수 있음.
하지만, 이렇게 항목별로만 작성하여 생각한다면 위험하다. 실제 서비스는 하나의 작업으로 이루어지지 않고, 여러 작업들이 복합적으로, 유기적으로 연결되어 일어나게 된다. 따라서 멀티스레드를 이용하더라도 어떤 방식으로 멀티스레드를 이용하느냐에 따라, 싱글스레드를 이용하더라도 어떤 방식으로 싱글스레드를 이용하냐에 따라 특성이 달라지기 때문이다.
예를들어, 서버의 작업을 요청 확인 > 요청 처리 > 응답 이렇게 크게 3가지로 나뉜다고 생각해보자.
만약, 각 요청확인 + 처리 + 응답을 멀티스레드로 처리하지만 하나의 스레드가 맡아 처리한다고 생각해보자. 이때 만약 각 스레드에서 공유자원에 대해 접근을 하는 작업이 빈번하게 일어난다면, lock을 통해 접근하게 되는 경우 멀티스레드 중에 실제로는 대부분의 스레드가 lock이 걸린 상태로 멈춰있게 되므로 멀티스레드의 병렬 작업이라는 장점을 얻지 못하고 오히려 CPU 자원, 메모리만 과도하게 사용하는 문제가 발생할 수도 있다.
반대로, 요청확인은 멀티스레드로 처리하고 해당 요청들을 Queue에 저장하고, 요청 처리는 싱글스레드로(NodeJs와 비슷하게)하게 된다면, Lock에 대한 걱정없이 프로그래밍할 수 있고 멀티스레드에 비해 더 적은 스레드로 작동하므로 CPU 자원을 낭비하지 않게 된다.
이처럼 멀티스레드라도 어떤 방식으로 스레드를 분배할 것인지? 싱글스레드라면 어떤 식으로 비동기 입출력을 처리할것인지?와 같은 정책등에 따라서 여러 구조의 서버 구조가 나올 수 있고 각각의 장단점이 존재하게 된다.
이처럼 단순히 싱글스레드와 멀티스레드 구조만으로 Spring과 NodeJs를 비교하기 보다는 실제로 제작할 서비스의 특징에 맞게 올바른 선택지를 고르는 것이 더 좋은 접근법일 것이다..
결론
NodeJS는 싱글스레드로 요청을 처리하지만, 요청 처리에서 시간이 오래 걸리는 작업들을 결국 이벤트 루프를 이용한 비동기 I/O를 통해 멀티스레드처럼 블로킹되지 않고 다른 작업을 할 수 있다. 즉, 싱글스레드로 작동하기때문에 가볍다는 싱글스레드의 장점과 멀티스레드의 장점을 차용하여 만들어진 구조이다.
물론, 기본적으로 NodeJS는 결국 응답을 싱글스레드로 한번에 하나의 요청만 처리하기 때문에 앞 요청이 처리 될때까지 뒤 요청은 처리하지 못하게 된다. 따라서 대규모 트래픽을 nodejs 서버 하나로 처리하기에는 문제가 발생할 여지가 있을 것으로 예상된다. 따라서 NodeJS를 간단한 Rest API 서버를 구축할 때 많이 사용하는 이유를 이해할 수 있는 부분이다. 또 내장 엔진(V8)등이 C로 작성되어 속도 면에서도 우수하다는 장점도 있으므로 여러모로 장점이 많다...
'서버' 카테고리의 다른 글
[C++] 메모리 풀(Memory Pool) 구현 (1) | 2024.09.24 |
---|---|
[서버구조분석] Spring 프레임워크 기본구조 분석 (0) | 2024.03.26 |
락 프리(Lock-Free) 알고리즘 (4) | 2024.03.13 |
소켓 통신 프로그래밍(2) (1) | 2024.03.06 |
소켓 통신 프로그래밍 정리(1) (1) | 2024.03.05 |