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에 클랩한번 눌러주세요!


감사합니다.

댓글