본문 바로가기

테크 이야기

node.js와 닷넷은 어떻게 다른가?

오늘 아침에 훈스닷넷에 올라온 질문글에 대한 답변으로 node.js와 닷넷의 웹 서버 동작 구조에 대한 썰을 풀어보고자 한다.

이전 글 "완전히 새로운 닷넷 개발 환경이 온다"에서 런던에서 게임을 개발할 때 IIS기반의 ASP.NET 웹 페이지를 개발하다가 Node.js 로 옮겨 같 사례를 언급한적이 있었다. 당시 그 팀은 SignalR 기술을 이용해서 플래쉬 게임의 데이터를 닷넷으로 구현된 서버에 전달했다. 서버의 메모리 보다는 CPU 효율이 낮다보니 여러 가지 테스트를 거쳐 node.js가 당시 시나리오에서 더 좋은 효율을 가져와 교체하게 되는 결정을 하게 되었다.

이번 글에서는 Node는 어떻게 동작되고 닷넷의  웹 서버는 요청을 어떻게 처리하는지 살펴보도록 하겠다.

먼저 성능에 대한 이야기를 하기 전에 웹서버의 로드(Load)에 대한 정의가 필요하다. 웹서버의 성능은 하나의 요청을 얼만큼 빠르게 응답하느냐로 정의되지 않는다. 1초에 500개의 요청이 갔을 때 그 서버가 얼마나 빠르게 처리할 수 있느냐에 따라서 성능이 정의되고 걸리는 시간을 우리는 로드로 칭할 수 있다.

DB나 파일에 대한 읽고 쓸 필요없이 처리해야 된다면 당연히 IIS가 더 빠를 것이다. IIS는 커널 모드의 캐싱을 이용하고 있기 때문에 바로 커널 모드에서 정적 페이지를 반환해 버리게 된다. 하지만 우리는 동적인 페이지들에 대해서 고려해보고 비교해볼 필요가 있는 것이다.

노드와 같은 경우 기존에 웹서버들이 걸리게 되는 로드들을 어떻게 하면 더 효율적으로 처리할 수 있을까를 착안해서 개발한 플랫폼이다. 즉, 로드가 없다면 닷넷이든 노드든 서로 성능에 대해서는 크게 민감하지 않을 것이다. 닷넷이 더 빠를수도 있고 노드가 더 빠를수도 있다.


ASP.NET의 동작구조

그렇다면 기존의 ASP.NET이 어떤 이유로 로드가 생기고 노드는 어떻게 다른지 살펴보도록 하자. 먼저 요청을 처리하는 웹 서버의 기술 자체가 전혀 다르다고 할 수 있다. ASP.NET은 하나의 request가 있을때 마다 스레드를 생성하고 그 작업은 스레드 풀을 통해서 관리된다. 그리고 스레드 한계 갯수가 넘어가게 되면 그 요청을 큐에 쌓게 된다. 이런 모델만 두고 봤을때는 멀티쓰레드로 처리되기 때문에 큰 문제가 없다.

하지만 스레드에서 공통 리소스에 대한 Lock을 걸게 되면서 로드는 시작된다. 즉, I/O에 따른 요청들이 발생했을 때인데 주로 DB를 호출한다거나 파일을 디스크에 쓴다거나와 같은 요청들이 I/O접근을 요구하는 요청들이다. 이때 각각의 스레드들은 서로 그 리소스에 접근하겠다고 시도를 하게 된다. 버스 문은 하나인데 10명의 사람들이 서로 들어가려고 애쓰는 있다고 생각해보면 이해가 빠르다. 문제는 이 10명은 어떻게든 버스를 타게 되면되는데 이 혼잡으로 인해서 버스를 탈 필요도 없는 사람들이 지나가는데 불편을 초래하는 케이스다. 이 때문에 스레드 풀은 오히려 서버 리소스만 차지하게 될 뿐이지 로드를 빠르게 없애는데 큰 도움이 되지 못한다.

이런 문제를 MS도 알고 있기 때문에 여러가지 방안들을 소개하기도 했다. I/O Completion Ports 라는 기술이 바로 그것이다. 하지만 아쉽게도 여러 닷넷 기술들이 I/O Completion Ports를 지원하지 않는다는 것이다. 엔티티 프레임워크가 대표적인 예이다. 아무튼 이것을 적용하는 것이 굉장히 어렵고 불편한 작업이기 때문에 더이상 닷넷을 한다고 하기 어렵다.

이런 문제를 빠르게 풀기 위해서였을지 몰라도 C#언어가 빠르게 발전했고 async 모델을 발표했다. 즉, 비동기 I/O를 쉽게 개발자들에게 지원하겠다는 의견이다. 기존에는 이것을 전적으로 개발자들에게 맞기는 것이지 일관된 표준을 지정한 것은 아니다. 물론, 지금도 그렇다. 스레드 대신 쉽게 쓸수 있는 문법을 제공해 주었을 뿐일지 모른다. 성능을 개선하고자 하는 전문가는 직접 비동기 방식을 구현해서 개발을 진행하는 것이 가능하다. 하지만 이것은 사람들의 자유의지와 코드를 적절히 잘 사용하는 사람에게만 좋은 방안일 수 있기 때문에 node가 성능이 더 좋은 것처럼 비추어 질 수 있다. 

단순, 블락킹 논블락킹 모델을 떠나서 노드는 싱글 리스너를 사용하는 반면 닷넷은 멀티플 리스너를 이용한다. 이 차이 때문에 싱글스레드 동작 방식과 멀티 스레드 동작방식의 차이가 생성되므로 CPU 효율이 차이가 나기 마련이다.




Node.js는 어떻게 다른가?

Node.js의 경우 닷넷과 같이 개발자가 이해하고 있고 또 원한다면 I/O에 있어서 비동기로 처리하게 설계하지 않았다. 전부다 I/O 비동기 처리 정책을 만든 것이다. 이 결정 때문에 노드 개발자들은 하나의 노드 인스턴스 별로 하나의 싱글 스레드만 생성하였고 스레드 스위칭을 최소화하게 된다. 그리고 하나의 스레드가 큐에 쌓여진 작업을 순차적으로 처리하는 방식을 사용하게 된 것이다. 

이런 처리 방식때문에 노드는 기존에 스레드가 스위칭되면서 발생하는 CPU의 효율을 최소화하는 것이 가능했다. 왜냐하면 노드는 전부다 비동기 I/O라는 정책이 있었기 때문이다. (물론, 추가적인 add-on 을 통해서 이 모듈도 원하는 방식대로 처리할 수 있다.) 

그렇다면 어떻게 하나의 스레드가 멀티플 요청을 처리하는 것이 가능할까? 먼저 노드는 하나의 리스너 스레드만 존재한다. 그렇다고 스레드 풀을 안쓰는 것은 아니다. 많은 사람들이 노드는 싱글스레드로 동작한다는 이야기 때문에 스레드가 1개로 오해하기도 한다. 단, 하나의 메인스레드가 요청을 받고 각각의 non-blocking 모델을 반환한 이벤트를 처리한다는 부분에서는 분명 싱글스레드가 맞는 것이다. 

즉, 동작원리를 정리하자면 I/O작업과 같이 오랜 시간을 필요로 하는 작업은 기본적으로 스레드 풀로 보내서 작업을 진행하고 작업끝난 녀석들의 이벤트를 다시 싱글 스레드가 받아서 그 이벤트를 실행하는 구조이다. 아래 그림이 노드의 동작을 잘나타 내주고 있다.







닷넷과 노드.js의 차이 정리


- 두 가지 모델에 대한 차이들을 다시 정리하자면 node는 1개 스레드의 리스너를 가지고 있고 닷넷은 N개의 스레드를 이용한다. 만약 스레드에 조금더 효율을 가져가려면 node쪽이 효율이 좋다고 이야기 할 수 있다. 닷넷이든 노드든 서버 리소스에 제한을 받는 것은 마찬가지 이다. 


- 노드의 경우 I/O기반의 작업은 기본적으로 non-blocking 스레드 방식으로 동작한다. 닷넷은 개발자가 async 패턴을 도입해서 구현해야 한다. 여기서 닷넷 개발자의 역량에 따라서 더 좋은 모델이 될수도 있고 안좋은 모델이 될 수도 있다.

- 노드의 경우 이벤트 기반의 작업 패턴을 이용하고 닷넷 또한 이런 작업을 람다식과 async로 구현할 수 있다. 

- 노드와 닷넷 I/O 기반의 작업에서 높은 퍼포먼스를 낼 수 있는 구조이지만 기본적으로 IIS 파이프라인 때문에 노드 만큼의 성능을 내기가 어렵다. IIS는 세션 상태 정보와 인증 등 다양한 모듈 들의 통합 때문에 서버 자체가 노드만큼 가볍지 못하다. 이러한 것들이 많은 사람들이 아파치와 IIS를 떠나 DJango나 Rails와 같은 웹서버를 이용하는 이유이기도 하다.


여기까지가 노드와 현재 닷넷 버전의 차이점으로 정리하였다. 지금 새로운 버전 ASP.NET5의 리눅스 버전에서는 이런 모델을 어떻게 반영하고 개선할지에 대한 기대가 개인적으로 크다. 적어도 IIS 파이프라인보다 심플한 구조의 웹 서버가 탄생하지 않을까 기대해 본다.