본문 바로가기
Node.js

24/10/26 - Node.js 플러스(2 - 1): 직렬화와 역직렬화, protobufjs 라이브러리

by Jini_Lamp 2024. 10. 26.

이번 프로젝트에서 사용할 라이브러리는 다음과 같다.

npm install dotenv lodash long mysql2 protobufjs uuid
npm install -D nodemon prettier

 

여기서 protobufjs 라이브러리는 구글의 Protocol Buffers를 사용하여 데이터 직렬화 및 역직렬화를 지원한다.

그렇다면 직렬화는 무엇이고, 왜 사용할까?

 

 

 

사용 이유

어떤 개발 언어를 사용하든, 기본적으로 데이터들의 메모리 구조는 다음과 같이 크게 두가지로 나뉜다.

  • 값 형식 데이터(Value Type)
    int, float, char 등과 같은 데이터들은 스택(Stack) 영역에 저장된다.
    이 값들은 메모리에 직접 저장되며, 값을 바로 접근할 수 있기 때문에 네트워크나 파일에 쉽게 저장하거나 전송할 수 있다.
  • 참조 형식 데이터(Reference Type)
    객체(Object)나 포인터와 같은 타입이며, 힙(Heap) 영역에 메모리가 할당된다.
    이들은 데이터 자체가 아닌 힙 메모리 주소(참조값)을 가지고 있기 때문에, 통신이나 파일 저장에 직접 사용할 수 없다.

 

즉, 참조 형식 데이터는 데이터 자체가 아닌 메모리 주소만 참조한다.

따라서 네트워크 전송이나 파일 저장 시, 메모리 주소를 직접 전송하면 문제가 발생할 수 있다. 간단한 예시를 살펴보면 다음과 같다.

참조 형식 데이터에서는 불가능한 이유

예를 들어, 포인터 변수 Class A를 선언하고 객체를 만들어 그 주소 값이 0x00045523이라고 할 때, 해당 값을 파일에 포함하여 저장을 했다.
그후 프로그램을 종료하고 다시 실행해서 주소 값 0x00045523을 가져오더라도 기존 A 객체의 데이터를 가져올 수 없다. 왜냐하면 프로그램이 종료되면 기존에 할당되었던 메모리(0x00045523)는 해제되고 없어지기 때문이다.

 

따라서 값 형식 데이터는 통신에 사용할 수 있지만, 참조 형식 데이터실제 데이터 값이 아닌 힙에 할당되어 있는 메모리 번지 주소를 가지고 있기 때문에 통신에 사용할 수 없다.

 

네트워크 또한 마찬가지다. 각 PC마다 사용하고 있는 공간 주소는 다르다.

하여, 직렬화를 통해 각 주소 값이 가지는 데이터를 모두 모아 값 형식 데이터로 변환해준다. 이렇게 하면 참조 형식 데이터를 포함한 모든 데이터를 하나의 연속적인 값 형식 데이터(일반적으로 바이트 형태)로 만들 수 있으며, 파일에 저장하거나 네트워크로 전송해도 데이터의 일관성과 무결성을 유지할 수 있다.

 

그렇다면 이젠 직렬화와 역직렬화에 대해 알아보자.

 

 

 

직렬화(Serialization)와 역직렬화(Deserialization)

직렬화와 역직렬화는 데이터 저장과 전송에서 핵심적인 개념으로, 특히 네트워크나 파일 저장을 다룰 때 유용하게 사용된다.

  • 직렬화
    데이터나 객체를 연속적인 바이트 형태로 변환하는 과정이다.
    직렬화가 필요한 이유는 보통 메모리에 존재하는 객체를 파일로 저장하거나 네트워크를 통해 전송할 때 데이터를 바이트 형태로 변환해야 하기 때문이다.
    이를 통해 객체나 데이터를 저장할 수 있는 바이트 형식으로 변환하여 파일에 기록할 수 있고, 변환된 값을 네트워크로 전송하여 다른 시스템에서 활용할 수 있다.
    직렬화 방식은 다음과 같은 것들이 있다.
    • JSON: 객체를 텍스트 형태의 JSO으로 직렬화. 사람에게 읽기 쉬운 포맷으로 웹에서 많이 사용된다.(읽기 쉬운 데이터 전송에서 사용)
    • XML: 데이터 구조를 XML 형태로 직렬화. 주로 문서 교환에서 활용된다.
    • 이진 포맷: 고성능을 필요로 하는 경우 바이트로 직접 직렬화하여 용량을 줄이고 속도를 높인다.(고성능이 필요한 상황에서 사용)
  • 역직렬화
    직렬화된 데이터를 원래 객체나 데이터 구조로 복원하는 과정이다.
    직렬화된 데이터를 네트워크를 통해 받거나 파일에서 불러올 때 역직렬화를 통해 원래의 데이터 형식으로 변환하여 사용한다. 예를 들어 서버에서 받은 JSON 형식의 데이터를 객체로 변환하여 활용하거나, 직렬화된 객체를 파일에서 불러와 원래의 객체로 복원하여 사용한다.

 

직렬화와 역직렬화의 흐름을 정리하면 다음과 같다.

겍체 → 직렬화 → 바이트 배열 → (네트워크 전송 or 파일 저장) → 바이트 배열 → 역직렬화 → 객체

 

 

하지만 JS에서는 바이트 배열이 아닌 JSON 형태의 문자열로 변환된다.

왜냐하면 JS에서 직렬화에 사용되는 JSON.stringify()는 텍스트 기반 JSON 포멧으로 직렬화하기 때문에, 데이터는 바이트 배열이 아닌 텍스트 문자열로 변환된다.

 

즉, JS에서는 바이트 배열 단계 없이 JSON 문자열을 바로 파일에 저장하거나 네트워크로 전송한다.

이 경우 바이트 배열 변환이 필요한 이진 데이터보다 직렬화 과정이 단순해지고, JSON 문자열 자체가 사람에게도 읽기 쉬운 포멧이라 많이 사용된다.

바이트 배열 변환이 필요할 때는 Buffer와 같은 추가 작업을 한다.

해당 과정은 다음과 같으며, 코드는 아래와 같다.

객체 → 직렬화 → (네트워크 전송 or 파일 저장) → 역직렬화 → 객체
// 직렬화 예시
const user = { name: "Alice", age: 25 }; // 객체 생성
const jsonString = JSON.stringify(user); // JSON 문자열로 직렬화

console.log("Serialized JSON String:", jsonString); 
// 출력: Serialized JSON String: {"name":"Alice","age":25}

// 역직렬화 예시
const parsedUser = JSON.parse(jsonString); // JSON 문자열을 객체로 역직렬화

console.log("Deserialized Object:", parsedUser); 
// 출력: Deserialized Object: { name: "Alice", age: 25 }

 

 

 

그렇다면 이번엔 protobufjs 라이브러리를 이용해 직렬화 과정을 테스트 해볼 것이다.

해당 라이브러리에서는 기존 JS처럼 JSON 기반 직렬화 방식이 아니라, 이진 데이터로 변환하는 방식이다. 이는 일반적인 직렬화 과정(겍체 → 직렬화 → 바이트 배열)과 동일하다. 즉, 바이트 배열을 만들어 네트워크 전송이나 저장을 더욱 효율적으로 수행할 수 있도록 하기 때문에, 고성능을 요구하는 환경에 적합하다.

  • person.proto
syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}

 

  • test.js
import protobuf from 'protobufjs';

protobuf.load("person.proto").then(root => {
  // 'person.proto' 파일을 로드합니다.

  const Person = root.lookupType("Person");
  // 'Person' 메시지 타입을 'root' 객체에서 찾습니다. 이는 person.proto 파일에서 정의한 메시지 타입입니다.

  const message = Person.create({ name: "John Doe", id: 123, email: "johndoe@example.com" });
  // 'Person' 메시지 타입을 사용하여 새로운 메시지 객체를 생성합니다. 여기서는 name, id, email 필드를 설정합니다.

  const buffer = Person.encode(message).finish();
  // 생성된 메시지 객체를 바이너리 형식으로 인코딩합니다. 'finish' 메서드는 최종 인코딩된 버퍼를 반환합니다.

  const decodedMessage = Person.decode(buffer);
  // 인코딩된 버퍼를 다시 메시지 객체로 디코딩합니다.

  console.log("Original message:", message);
  // 원래 생성된 메시지 객체를 콘솔에 출력합니다.

  console.log("Encoded buffer:", buffer);
  // 인코딩된 바이너리 버퍼를 콘솔에 출력합니다.

  console.log("Decoded message:", decodedMessage);
  // 디코딩된 메시지 객체를 콘솔에 출력합니다.

})

결과