Node.js 이벤트루프 제대로 이해하기


Node.js 는 이벤트 기반의 플랫폼입니다. 이 말은 즉슨, 노드에서 일어나는 모든 일은 어떤 이벤트에 대한 반응이라는 말과 같습니다. Node에서 일어나는 모든 처리는 전부 일련의 콜백내용들입니다.

libuv 라는 추상화된 라이브러리가 이벤트루프라는 기능을 제공합니다.

이 이벤트루프는 아마 오해하기 가장 쉬운 개념일 것입니다.

저는 성능 모니터링 제작업체인 Dynatrace에서 일하며, 이곳에서 이벤트루프 모니터링에 대한 작업을 할때, 실제로 우리가 무엇을 측정하고 있는지 제대로 이해하기 위해서 많은 노력을 기울였습니다.

이 글에서는, 이벤트루프가 실제로 동작하는 방식과 어떻게 이를 올바르게 모니터링 하는 방법에 대해 다룰 예정입니다.

이벤트루프에 대한 오해

Libuv는 Node.js에 이벤트루프를 제공하는 라이브러리 입니다. libuv의 코어 개발자였던 Bert Belder가 발표했던 Node 동작방식에 대한 강연에서, 우리가 이벤트루프를 구글에 검색했을때 나오는 이미지들과 그 이미지에서 설명하는 이벤트루프 동작방식들에 대해 이는 대부분이 틀렸으며 실제로 이렇게 동작하지 않는다고 설명합니다.

이제 우리가 할 수 있는 가장 흔한 오해들 부터 설명해보겠습니다.

오해 1: 이벤트루프는 우리가 실제로 작성한 코드와는 별개로 별도의 스레드에서 실행됩니다.

오해

우리가 작성한 자바스크립트를 코드를 실행시켜주는 main 스레드가 존재하고, 이벤트루프를 실행하는 또다른 쓰레드가 존재합니다. 비동기 작업이 실행 될 때마다, main 스레드는 이벤트루프 스레드에게 작업을 넘겨주고, 작업이 완료되면 이벤트루프 스레드가 main 스레드에게 신호를 보내 콜백을 실행합니다.

실제

자바스크립트를 실행시키는 스레드는 단 하나뿐이며, 이 스레드가 바로 이벤트루프가 실행되는 스레드입니다. Node.js 어플리케이션에서 실행되는 사용자의 코드는 전부 콜백이라고 볼 수 있습니다. 이 콜백함수의 실행은, 이벤트루프에 의해 수행됩니다. 이 내용은 조금 뒤에서 깊이 다룰 예정입니다.

오해 2: 모든 비동기 작업은 스레드 풀에서 처리합니다.

오해

파일I/O나 외부와 통신하는 HTTP통신이나 데이터베이스통신같은 비동기 작업들은 항상 libuv가 제공하는 스레드 풀에서 수행됩니다.

실제

Libuv는 기본적으로 비동기작업을 수행하기 위해 4개의 스레드를 스레드 풀에 할당하여 놓습니다. 현대의 OS들은 많은 I/O 작업들을 위해 비동기 Interface들을 제공합니다. 이 예로는 Linux의 AIO가 있습니다.
Libuv는 가능하다면, 직접 OS의 비동기 Interface를 사용하며, 스레드풀에 작업을 넘겨주지 않습니다.
데이터베이스 같은 서드파티시스템에도 동일합니다. DB 드라이버를 이용해 스레드풀을 활용하지 않고, 비동기 인터페이스를 직접 사용합니다.
즉, 다른 방법이 없는 경우에만, libuv는 비동기 작업들을 스레드 풀에 할당합니다.

오해 3: 이벤트 루프는 스택 or 큐 같은 것입니다.

오해

비동기 작업들은 FIFO(선입선출 큐)에 쌓이고, 이벤트루프는 이 큐를 계속 돌면서, 태스크가 완료되었을 때 콜백을 실행합니다.

실제

이벤트루프에 큐 형식의 자료구조가 포함되있는건 맞습니다. 하지만 이벤트루프는 이 큐를 돌면서 실행하지 않고, 스택을 처리합니다. 이벤트루프는 round-robin 방식으로 차례차례 돌면서 처리되는 특정 작업들의 단계들로 이루어져 있습니다.
약간 말이 어려운데요. 밑에서 자세히 다뤄보겠습니다.

이벤트루프 작업 이해하기

이벤트루프를 이해하기 위해서는, 각 단계에 어떤 작업들이 수행되는지를 알아야 합니다.
이벤트루프가 어떻게 실행되는 지는 다음과 같습니다.

eventloop

여기서 초록색 박스는 이벤트루프의 각 단계들을 의미합니다.
이 단계들을 하나씩 살펴보겠습니다. 자세한 설명은 Node.js 공식문서에 한국어로 아주 잘 설명되어있으니, 읽어보시면 도움이 될 것입니다.

Timers

setTimeout() 또는 setInterval()에서 스케줄링된 모든 것은 여기서 처리됩니다. 하지만 그 타이머 안에 등록한 콜백함수의 실행은 Polling 단계의 가장 앞부분에서 이루어집니다.

IO Callbacks

대부분의 콜백들이 여기서 처리됩니다. 사용자가 작성한 Node.js의 모든 코드는 기본적으로 콜백입니다.(예: HTTP 요청에 의한 콜백함수는 다시 여러개의 콜백함수를 실행시킵니다.)
Node.js 공식문서에서 설명하는 이벤트루프의 각 단계별 역할에 대한 내용이 이 글의 원본에 적혀있는 내용과 달라 정정합니다.
Timers단계의 다음 단계인 Pending Callbacks 라고 불리는 단계에서는, TCP오류 같은 시스템 작업의 콜백을 반환합니다. 예를들어, TCP 소켓이 연결을 시도하다가 ECONNREFUSED 같은 메시지를 받으면, 이에 대한 오류보고를 위해 이에 대한 콜백함수가 IO Callbacks (pending callbacks) 단계의 큐에 추가됩니다.

IO Polling

이벤트루프가 Poll 단계에 진입하면,
먼저 Timers 단계에 스케줄링 되었던 콜백들 중에, 시간이 지난 타이머의 콜백을 실행합니다.
그 다음, Poll 단계의 큐에 있는 이벤트들을 처리합니다. 대부분의 콜백들은 이 Poll 단계에서 처리됩니다.

Set Immediate (=Check)

setImmediate()를 통해 등록된 모든 콜백을 실행됩니다.

Close

‘close’ 이벤트에 대한 모든 콜백이 실행됩니다.

이벤트루프 모니터링

Node.js 어플리케이션에서 진행되는 모든 작업은 이벤트루프를 통해 실행됩니다. 이 말은 즉슨, 이 이벤트루프에 대한 메트릭을 측정할 수만 있다면, 이 정보들을 이용해 Node.js 어플리케이션에 대한 전반적인 상태나 성능에 대한 중요한 정보들을 알아낼 수 있습니다.
이벤트루프에서 런타임때 메트릭 정보들을 가져올 수 있는 API가 없기때문에, 각 모니터링 툴들이 직접 자기들만의 메트릭을 구축하여 이벤트루프에 대한 모니터링을 제공합니다. 여기서 우리가 이를 어떻게 해결했는지 한번 보겠습니다.

Tick Frequency

시간마다 tick 수입니다.

Tick Duration

1번의 tick에 걸리는 시간을 의미합니다.

실상황에서 Tick 빈도수와 Tick 소요시간 지표

우리가 서로 다른레벨의 부하테스트를 줘봤을 때, 결과는 놀라웠습니다. 예제를 보여드리겠습니다.

다음 시나리오에서는 다른 HTTP 서버로 외부요청을 보내는 express.js 노드 프로그램을 호출합니다.

네가지 시나리오가 있습니다.

  1. Idle
    들어오는 요청이 없습니다.

  2. ab -c 5
    Apache bench를 이용하여 한번에 5개의 요청을 생성합니다.

  3. ab -c 10
    한번에 10개의 요청을 생성합니다.

  4. ab -c 10 (느린 백엔드)
    서버가 요청보내는 외부 HTTP 서버는 요청을 받으면 1초 후에 응답을 반환하도록 하여, 느린 백엔드를 만들었습니다. 이렇게 하면, 노드서버가 HTTP 서버로부터 응답을 기다리면서 흔히 말하는 back pressure가 발생합니다.

eventloop_f

이 결과 차트를 보면, 재밌는 내용을 발견 할 수 있습니다.

이벤트루프 tick 수행시간과 빈도는 동적으로 조정됩니다.

노드 어플리케이션에 아무런 요청도 없는 상태인 경우, (타이머, 콜백 등) 대기중인 작업이 없는 상태로서, 위에서 설명했던 모든 단계들(timers, I/O callbacks, polling 등)을 미친듯이 계속 도는게 아니라 이벤트루프가 이에 적응하고 polling 단계에서 새로운 콜백이 등록되기를 잠시 기다립니다.

즉, 요청이 없을때에는 이벤트루프의 tick 빈도는 적고 tick의 duration은 긴 상황입니다.

이 상황에서의 지표는 이후에 진행한 느린 백엔드에서 진행했던 지표와 유사합니다.

이 노드 어플리케이션은 5개의 요청을 한번에 요청했던 시나리오에서 최고로 좋은 성능을 냅니다.

결과적으로, tick 빈도나 tick duration을 보고 분석할 때에는 노드 서버의 초당요청 수를 기준삼아 보아야합니다.

이 지표들은 우리에게 의미있는 내용들을 제공해주긴 했지만, 아직까지는 실제로 어느 단계(phase)에서 시간이 소비되었는지 알지 못하기 때문에 두가지 측정항목이 더 필요합니다.

작업 처리 지연시간

작업처리 지연시간은 비동기 작업이 스레드 풀에서 처리 될 때까지 걸리는 시간을 의미합니다.

여기서 지연시간이 길다면, 이는 libuv 스레드 풀의 사용량이 많고 굉장히 바쁘다는 것과 같습니다.

이 지연되는 시간을 측정하기 위해, express.js 노드 어플리케이션에 Sharp 라이브러리를 이용하여 이미지를 가공하거나 처리해주는 API를 추가했습니다. 이미지처리는 부하가 있는 작업이기에, Sharp 라이브러리는 스레드 풀을 사용합니다.

sharp

그래프를 보면, Apache bench를 이용하여 5개의 요청을 동시에 보냈을때 이미지처리가 있는 요청과 그 전에서 진행했던 일반적인 HTTP서버 처리가 있는 요청은 확실하게 구별됩니다.

이벤트루프 지연시간

이벤트루프 지연시간은 setTimeout(X)로 스케줄링 된 작업이 실제로 처리될 때 까지 추가로 소요되는 시간을 의미합니다.

이벤트루프 지연시간이 높다는 것은, 이벤트루프가 콜백을 처리하기 엄청 바쁜 상태라는 것과 같습니다.

이 이벤트루프 지연시간 측정을 위해, 이번에는 굉장히 비효율적인 알고리즘으로 피보나치를 계산하는 API를 추가했습니다.

fibonacci

Apache bench를 이용해 피보나치를 계산하는 API로 부하를 주면 콜백 큐가 바쁘다는 걸 알 수 있습니다.

우리는 이러한 4가지 주요 메트릭(Tick Frequency, Tick duration, 작업처리 지연시간, 이벤트루프 지연시간)으로 Node.js 내부동작을 더 잘 이해할 수 있게 되었고, 꽤 중요한 내용들도 알게되었습니다.

이벤트루프 튜닝

물론, 이 메트릭 정보들만으로 문제를 해결 할 수는 없습니다. 실제 이벤트루프가 고갈되었을때, 어떻게 대응 해야하는지 알아봅시다.

exhausted

모든 CPU 활용

Node.js 어플리케이션은 싱글 스레드로 작동합니다. 멀티코어 환경에서 1개의 Node.js 어플리케이션은 효율적으로 작동하지 않습니다. 낭비되는 CPU가 있기 때문입니다. Cluster Module을 사용하면, CPU 마다 child 프로세스를 쉽게 만들 수 있습니다. 각각의 child 프로세스는 각자 자신만의 이벤트루프가 존재하고 master 프로세스는 모든 자식들에게 요청을 분산시켜 줍니다.

스레드 풀 조정

앞서 언급했듯이, libuv는 스레드 4개로 스레드 풀을 생성합니다. 스레드 풀의 기본 크기는 UV_THREADPOOL_SIZE 환경변수를 설정해서 수정할 수 있습니다. 이 방법은 I/O 작업이 많은 어플리케이션에서 도움이 될 수 있겠지만, 큰 스레드 풀은 메모리나 CPU를 고갈시킬 수 있음을 기억해야 합니다.

작업을 다른서비스에 맡기기

만약 Node.js가 CPU사용이 과도하게 필요한 작업에서 사용된다면, 이 특정 작업에 더 잘맞는 다른 언어를 선택해서 그 쪽으로 처리를 옮겨 작업량을 줄이는 것이 가능한 방법일 수 있습니다.

요약

  • 이벤트루프는 Node.js를 실행시켜주는 것이다.
  • 이벤트루프의 기능을 잘못 알고 있는 경우가 많습니다 - 몇가지 단계들을 차례대로 반복하면서 순서대로 각 단계들의 작업들을 처리해나갑니다.
  • 이벤트루프 자체에서 제공해주는 메트릭이 없으므로, 수집된 메트릭들은 APM 제품들마다 다릅니다.
  • 이벤트루프 메트릭들은 병목현상에 대해 중요한 정보들을 제공해주지만, 이벤트루프와 실행중인 코드에 대한 깊은 이해가 가장 중요합니다.

감사합니다.

공유하기 댓글

Node.js의 Buffer를 제대로 이해해보자


Node.js에서 Buffer, Stream, Binary Data 를 마주치게 될때면, 이것들을 아예 모르는 사람들도 있고, 늘 이 개념에 대해 두려워하는 사람도 있으며, 자기 자신이 이를 정확하게 이해 하고있는지도 종잡을 수 없는 사람도 많습니다.

이 내용들은 nodejs 시니어나 package 모듈 개발자정도는 되어야 다룰 수 있을 것 같은 기분이 듭니다.

실제로, Node.js 자체의 소스코드나 유명라이브러리들의 소스코드를 까보시면, Buffer을 안마주칠 확률보다 마주칠확률이 더 높습니다.

특히 컴퓨터공학과정을 거치지 않은사람이, nodejs로 개발 할때에 저런 단어들을 마주칠때면 더욱 두려움을 느끼곤 합니다.

안타깝게도, 시중에 있는 많은 Node.js 개발서적들은 Node.js의 핵심기능들이나 그 기능구현의 이유에 대해 설명하기도 전에, 바로 Node.js 패키지들을 이용해서 Web Application을 구현하는 방법에 대해 설명합니다.
그리고 어떤사람들은 뻔뻔하게도, Buffer, Stream, Binary Data 같은 것들은 어차피 다룰 일이 없을 것이기 때문에, 알 필요도 없다고 주장합니다.

물론 스스로 그저그런 Node.js 개발자로 살기로 선택한 사람이라면, 이런것들을 마주칠 일은 없을 수도 있습니다.

그게아니라, Node.js에 대해 더 깊이 이해하고 싶거나, 도대체 Buffer 같은 Node.js 의 핵심기능에 대해 궁금하고 더 알고싶은 호기심이 들면, 바로 이것이 제가 글을 쓰는 이유입니다 :)

이 글에선 Node.js의 핵심기능중 일부를 설명하고, 이 글을 읽게되면 Node.js에 대한 이해가 한 차원 더 높아질 것입니다.


Node.js 공식문서에서는 Buffer를 다음과 같이 정의합니다.

바이너리 데이터들의 스트림을 읽거나, 조작하는 매커니즘.

이 Buffer클래스는 Node.js의 일부로 도입되어 TCP 스트림이나 파일시스템같은 작업에서의 octet 스트림과의 상호작용을 가능하기 위해 만들어졌습니다.

* octet Stream은 일반적으로 8bit 형식으로 된 데이터를 의미합니다.

음.. 정의를 읽어보니 약간 말이 복잡합니다.
이 말을 쉽게 풀어쓰면 즉,

Buffer클래스는 바이너리 데이터들의 스트림을 직접 다루기 위해 Node.js API에 추가되었습니다.

라고 볼 수 있습니다.

그래도 말이 좀 간단해 졌죠 ? 하지만 아직까지는 Buffer, Stream, Binary Data .. 이런 단어들이 확 와닿지는 않습니다.
이제부터 처음부터 끝까지 이것들을 살펴봅시다.

Binary Data ? 이게 뭔가요?

우리는 컴퓨터가 이진수로 데이터를 저장하고 표현한다는걸 이미 알고있습니다. 이진수는 단순히 1과 0의 집합입니다. 예를들어, 다음은 서로 다른 이진수 5개이며, 이 이진수들은 서로다른 1과 0의 집합입니다.

10, 01, 001, 1110, 00101011

각각 이진수에서 1 혹은 0으로 되어있는 자리를 비트(bit)라고 합니다. 이는 Binary digIT의 약자입니다.

컴퓨터가 어떤 데이터를 저장하거나 표현하기 위해서는, 컴퓨터는 해당 데이터를 이진수로 변환해야합니다.
예를들어, 숫자 12를 변환하려면 컴퓨터는 12를 이진수인 1100로 변환해야합니다.

하지만 우리가 다루는 데이터중에는 숫자만 있는것은 아닙니다. 우리가 다루는 데이터중에는 문자열도 있고 이미지, 비디오형식도 있습니다. 컴퓨터는 모든 유형의 데이터를 이진수로 변환하는 법을 알고 있습니다.
문자열을 예시로 들어봅시다.
어떻게 컴퓨터가 문자열 ‘L’을 이진수로 나타낼까요 ?

어떤 문자를 이진수로 나타내기 위해서는 첫번째로 그 문자를 숫자로 변환해야 합니다. 숫자로 변환하면 그 숫자를 이진수로 변환하면 끝나기 때문이죠.
컴퓨터는 문자열 ‘L’에 대해서 먼저 ‘L’을 나타내는 숫자로 변환합니다. 어떻게 변환할까요?
웹브라우저 콘솔을 열어 이 코드를 실행시켜보세요 : 'L'.charCodeAt(0)
숫자 76이 출력되나요? 이 숫자는 문자 ‘L’을 나타내는 숫자 값입니다. 이는 Character Code 혹은 Code Point 라고 불립니다. 그렇다면 컴퓨터는 어떻게 이 문자를 보고 바로 매칭해서 숫자를 알려줄 수 있었을까요? 어떻게 문자 ‘L’을 보고 숫자 76을 알려줄 수 있었을까요?

Character Set (문자 집합)

Character Set은 각각문자를 숫자로 나타낼 수 있도록 정의해놓은 규칙입니다. 위에 보았듯이 ‘L’을 76으로 매칭할 수 있게 각각문자에 해당하는 숫자를 표로 정리해놓은 것이라고 보시면 됩니다. 그렇다고 한가지 Character Set 만 있는것은 아니고 여러가지 Set들이 있습니다. 이중에서 유명한 것은 우리가 자주 들어본 유니코드, 아스키코드 가 있습니다. 자바스크립트는 유니코드와 아주 궁합이 잘맞습니다. 사실은 브라우저에서 ‘L’을 76으로 표현한 것은 유니코드입니다.

이렇게 우리는 어떻게 컴퓨터가 문자를 숫자로 표현하는지 보았습니다. 이제는, 컴퓨터가 숫자 76을 이진표현로 변환할 것입니다. 우리는 그냥 단지 숫자 76을 이진수로 변환하면 될 것이라고 생각합니다. 과연 그럴까요?

Character Encoding (문자인코딩)

문자를 숫자로 나타내는 것에 규칙이 있는 것 처럼, 숫자를 바이너리 데이터로 나타내는 데에도 규칙이 있습니다. 정확히 말하면, 숫자를 몇 bit로 나타낼 것인가를 정하는 것입니다. 이것을 Character Encoding 이라고 부릅니다.
Character Encoding의 정의중 하나가 UTF-8 입니다. UTF-8은 문자가 바이트단위로 인코딩 되어야 합니다. 1바이트는 8개 비트의 집합을 의미합니다. 즉 8개의 1 또는 0을 의미합니다. 그래서 문자를 바이너리로 나타내는데에는 8개의 1과 0으로된 집합이 사용됩니다.

위에 언급한 내용을 다시 이어나가자면, 숫자 12을 이진수로 나타내면 1100 입니다. UTF-8 명세에 따르면 숫자 12는 8bit로 구성되어야 합니다. 8bit로 구성하려면 12의 실제 이진수 표현의 왼쪽에 더 많은 bit를 추가해서 바이트로 만들면 됩니다.
그래서 12는 00001100 으로 저장될 것입니다.
이것이, 컴퓨터가 숫자 혹은 문자를 바이너리 데이터로 저장하는 방식입니다.
이것과 비슷하게, 이미지나 비디오데이터 또한 바이너리 데이터로 저장하는 방식도 따로 정해져 있습니다. 이중에 핵심은, 컴퓨터는 모든 데이터를 바이너리 이진 데이터로 저장한다는 것입니다.

Character Encoding 에 대해 더 자세히 알고싶으시다면, Character Encoding에 대해 자세히 알아보기를 한번 보시는걸 추천합니다.

우리는 이제 바이너리 데이터가 무엇인지를 알았습니다. 그렇다면 바이너리 데이터의 Stream은 무엇일까요 ?

Stream

Node.js 에서의 스트림은 간단하게 한 지점에서 다른 지점으로 이동하는 일련의 데이터를 의미합니다. 전체적인 의미로는, 만약 우리가 어떤 방대한 양의 데이터를 처리해야 할때, 모든 데이터가 전부다 사용가능 할때까지 기다리지 않아도 된다는 것입니다.

기본적으로 큰 데이터는 청크단위로 세분화되어 전송됩니다. 이말은, 처음 설명했던 Buffer의 정의에 따르면, 파일시스템에서 바이너리 데이터들이 이동한다는걸 의미합니다. 예를들어, file1.txt의 텍스트를 file2.txt로 옮기는 걸 의미합니다.

하지만, Streaming 하는동안에 buffer라는 것이 어떻게 바이너리 데이터를 다룰 수 있게 도와준다는 것일까요? 정확히 buffer는 무엇일까요?

Buffer

우리는 데이터들의 스트림이란 일련의 데이터들이 한지점에서 다른 지점으로 이동하는 것이라는 걸 배웠습니다. 하지만, 데이터들이 정확하게 어떻게 이동한다는 것일까요?
일반적으로 데이터의 이동은 그 데이터를 가지고 작업을 하거나, 그 데이터를 읽거나, 무언가를 하기 위해 일어납니다. 하지만 한 작업이 특정시간동안 데이터를 받을 수 있는 데이터의 최소량과 최대량이 존재합니다. 그래서 만약에 한 작업이 데이터를 처리하는 시간보다 데이터가 도착하는 게 더 빠르다면, 초과된 데이터는 어디에선가 처리되기를 기다리고 있어야 합니다. 데이터를 처리하는 시간보다 훨씬빠르게 계속해서 새로운 데이터가 도착하면 어딘가에는 도착한 데이터들이 미친듯이 쌓일것이기 때문이죠.

반면에, 한 작업이 데이터를 처리하는 시간이 데이터가 도착하는 시간보다 더 빠르다면, 먼저 도착한 데이터는 처리되기 전에 어느정도의 데이터량이 쌓일때까지 기다려야 합니다.

바로 그 기다리는 영역이 buffer 입니다! 컴퓨터에서 일반적으로 RAM이라고 불리는 영역에서 streaming 중에 데이터가 일시적으로 모이고, 기다리며 결국에는 데이터가 처리되기위해 내보내어 집니다.

Streaming과 Buffer의 과정을 버스정류장에 빗대어 설명할 수 있습니다. 어떤 버스정류장에서는, 어느정도 이상의 승객이 모이지 않거나, 출발시간 전일때에는 출발하지 않습니다. 그리고, 승객들은 버스정류장에 도착하는 속도도 도착하는 시간도 각각 다릅니다. 승객 자기자신도 버스도 그 어느 누구도 버스정류장에 승객이 도착하는걸 마음대로 제어할 수 없습니다. (승객이 버스정류장에 도착하는 것도 결국에는 승객 스스로 제어하는게 아니라 환경적 요인에 달려있다는 말입니다.)
어쨌든 일찍 도착한 승객은 버스가 출발하기 전까지는 버스정류장에서 기다려야 합니다. 반대로 승객이 도착했을때, 버스가 이미 문을닫고 출발하기 직전상태 이거나 버스가 이미 출발한 상황일때에는 다음 버스를 기다려야 합니다.

어떤 경우든지, 승객이 기다리는 위치가 있습니다. 바로 버스정류장입니다. 바로 그게 Node.js에서의 Buffer입니다! Node.js는 데이터가 도착하는 시간이나 전송되는 속도를 제어할 수는 없습니다. Node.js가 결정할 수 있는건 언제 데이터를 내보내느냐 입니다. 버스를 언제 출발시킬 수 있는 제어권이 있는 것과 동일합니다. 아직 데이터를 내보낼 때가 아니면, Node.js는 데이터들을 일종의 대기영역인 RAM에 작은 영역인 buffer에 데이터를 넣어놓습니다.

일상생활에서 버퍼작동을 볼 수 있는 예로는 온라인 영상을 시청할 때 입니다. 유튜브를 보는 순간을 상상해보세요. 우리의 인터넷 연결상태가 매우 좋을때에는 영상 스트리밍이 끝날때까지 버퍼를 채우고 데이터가 처리될 수 있게 빠르게 내보내고, 다시 버퍼를 채우고 빠르게 내보내고를 반복합니다.

그러나 인터넷 연결상태가 좋지 못할때에는 첫번째 데이터셋을 처리하고 나서, 영상플레이어는 로딩 아이콘을 띠우면서 ‘buffering’이라는 텍스트를 보여줄 것입니다. 이것은 데이터가 더 모이고 도착할때 까지 기다린다는 의미입니다. 만약에 버퍼가 채워지고 데이터가 처리되면, 영상이 다시 보여지게 될 것입니다. 영상을 보여주는 동안에도, 계속해서 다음 데이터가 도착하고, 버퍼에 채워질 것입니다.

이것이 바로 버퍼입니다.

아까 보았듯이 버퍼에 대한 설명에서, 데이터가 버퍼에 있는동안 우리가 streaming되는 바이너리 데이터들을 조작하고 다룰 수 있다고 했습니다. 우리가 이런 raw한 바이너리 데이터들을 이용해서 어떤 종류의 작업을 할 수 있을까요? Node.js에서 구현한 Buffer 문서에는 우리가 다룰 수 있는 작업 내용들을 리스트로 정리해서 보여줍니다. 그 중에 몇가지를 살펴봅시다.

버퍼다루기

우리는 직접 Buffer를 만들 수도 있습니다. Node.js는 Streaming 하는동안에 자동으로 Buffer를 만드는데요. 이것말고도 우리가 직접 Buffer를 만들고 직접 다룰 수 있습니다. 흥미롭죠? 한번 저희가 직접 만들어봅시다!

버퍼를 만드는데는 여러가지 방법이 있습니다. 한번 보시죠.

1
2
3
4
5
6
7
8
// size가 10인 빈 buffer를 만듭니다.  
// 이 버퍼는 오직 10 byte만 담을 수 있습니다.

const buf1 = Buffer.alloc(10);

// buffer 에 데이터를 담아 만듭니다.

const buf2 = Buffer.from("hello buffer");

버퍼를 만들었으면, 이제 마음대로 조작할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 버퍼구조를 조사합니다.

buf1.toJSON();
// { type: 'Buffer', data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }
// 빈 버퍼입니다

buf2.toJSON();
// { type: 'Buffer',
data: [
104, 101, 108, 108, 111, 32, 98, 117, 102, 102, 101, 114
]
}

// toJSON 메서드는 데이터를 Unicode Code Points 로 표현합니다.

// 버퍼의 크기를 조사합니다.

buf1.length // 10

buf2.length // 12. 파라미터로 넣어주었던 content에 따라 자동으로 크기가 할당됩니다.

// 버퍼에 쓰기
buf1.write("Buffer really rocks!");


// 버퍼를 Decoding 합니다.

buf1.toString(); // 'Buffer rea'

// buf1 은 10 byte 밖에 담을 수 없기 때문에, 나머지 문자들은 할당할 수 없습니다.

우리는 버퍼로 많은 것들을 할 수 있습니다. 이들은 nodejs 공식문서에서 전부 확인할 수 있습니다.

마지막으로, 과제를 하나 남기고 가겠습니다. Node.js 핵심 라이브러리들중 하나인 zlib.js의 소스코드를 분석하여, 어떻게 버퍼를 활용하여 바이너리 데이터 스트림을 다루었는지 확인해보세요. 우리는 버퍼를 배웠기때문에 이제 두려울게 없습니다. 이건 사실 gzip을 구현한 것입니다. 코드분석을 하면서, 새로 알게된 것들을 글로도 한번 적어보시고, 댓글로도 같이한번 의견을 나눠보았으면 좋겠습니다 :)

이 글이 Node.js 의 Buffer를 이해하는데 도움이 되었으면 좋겠습니다.

혹시, 이 글이 도움이 되었고, 남들에게도 이 글을 알려드리고 싶으시다면 원본 Medium article에 클랩한번 눌러주세요!



감사합니다.

공유하기 댓글