프로토콜 버퍼란 무엇인가(Protocol Buffer) 서버, 클라이언트 예제와 함께 feat. express & Vue

Jinny·2022년 2월 9일
4
post-thumbnail

회사에서 주로 mfc를 다루다가 최근 웹프론트엔드 파트 업무를 맡게 되면서 한꺼번에 html, css, javascript, vue를 접하게 되었다...혼돈...😱

아무튼 과장님이 백엔드에서 프로토버퍼로 데이터를 보낼거니까 어떻게 받아서 쓰는지 알아두라고 하셔서 프로토콜 버퍼에 대해 알아보았다!

아래와 같은 순서로 프로토콜버퍼에 대해 알아보자✨

  1. 프로토콜 버퍼란?
  2. 예제 코드
    2-1. 서버 구현 using express
    2-2. 클라이언트 구현 using vue
    2-3. 동작

1. 프로토콜 버퍼란?

구조화된 데이터를 직렬화하는 방식이다.
직렬화(Serialization)는 데이터를 파일로 저장하거나 네트워크 통신에 사용하기 위한 형식, 즉 바이트 스트림 형태로 변환하는 것이다.

주로 server-client간 데이터를 교환할 때 JSON형태를 많이 사용하는데 JSON 대신 프로토콜 버퍼를 이용하는 이유는 무엇일까? JSON 방식과 비교하여 프로토콜버퍼의 원리를 알아보자.

예를들어 {name : "jinny", age : 27}라는 객체가 있다고하자.

🔨JSON 포맷으로 직렬화

let person = { name : "jinny" age : 27 };

//JS객체 -> JSON 문자열로 변경
let serializedJson = JSON.stringify(person);
//serializedJson = {"name":"jinny","age":27} 데이터 크기는 총 25byte 

🔨프로토콜 버퍼 포맷으로 직렬화

* 프로토 파일 : 프로토콜 버퍼 방식을 사용하기위해 필요한 파일
message Person{
	string name = 1; //필드 넘버(필드 태그..?)
    int32 age = 2;
}

프로토콜 버퍼를 사용한 경우에는 아래 그림과 같이 9바이트를 사용하게 된다. name, age와 같은 속성값을 프로토파일에서 작성한 필드 넘버로 대체하는 것이 핵심이다.
프로토콜 버퍼를 사용한경우
최초 1바이트 중 5bit는 필드 넘버를, 3bit는 필드 타입을 나타낸다. type을 비롯한 더욱 자세한 설명은 링크에서 확인할 수 있다.

🦾프로토콜 버퍼의 장단점

결과적으로 프로토콜 버퍼를 사용하면 직렬화, 역질렬화 속도가 빠르고 직렬화된 파일의 크기를 월등히 줄일 수 있어, 대용량 데이터를 처리할 때 성능이 더 좋다고 한다..!
JSON과 달리 사람이 읽기 어려우며, proto파일이 없으면 데이터 해석이 불가하다는 단점도 있지만 스키마가 존재하여 이에 따라 쉽게 직렬화, 역질렬화가 가능하기 때문에 내부서비스에서 사용하면 효과적이라고 한다..!


2. 예제코드

2-1. 서버 구현 Using express

🌞 백엔드 환경 세팅

1) npm installl -g express-generator

//backend폴더가 생성되면 cd backend 해당 폴더로 이동
2) express --veiw=pug backend(원하는 폴더명)

3) npm install

//node.js에서 protobuf를 사용하기위한 라이브러리 설치
4) npm inatall protobufjs --save 

//서버 실행
5) npm start  

🌞 .proto 파일 작성(test.proto)

package test;
syntax = "proto3"; //ver3 사용을 위해 명시

message Person{
	string name = 1;
	int32 age = 2;
}

🌞 proto.js파일 생성(routes/proto.js)
서버쪽으로 get 요청이 오는 경우 protobuf를 통해 데이터를 인코딩하여 클라이언트쪽으로 보낼 것이다.

const express = require("express");
const router = express.Router();
///import protobuf 
const protobuf = require("protobufjs"); 

router.get("/", function (req, res, next) {
  //위에서 작성한 프로토파일을 로드한다.
  protobuf.load("test.proto", function (err, root) {
    if (err) throw err;

    //메시지 타입을 얻어온다.
    let personMessage = root.lookupType("./test.Person"); //"프로토파일 패키지명.메시지이름"
    
    //예시 데이터(해당 데이터를 전송할 예정)
    let payload = { name: "jinny", age: 27 };

    //필요하다면 payload가 형식에 알맞는지 유효성검사를 진행한다.
    let errMsg = personMessage.verify(payload);
    //payload 유효하다면 null을 반환하고,
    //유효하지 않으면(age:"27"이라고 작성한다면 "age:integer expected"오류를 반환)오류를 반환한다.
	if (errMsg) throw Error(errMsg);
    
    //객체 직렬화(message => uint8Array)
    var buffer = personMessage.encode(payload).finish();
    console.log(buffer);
    //<Buffer 0a 05 6a 69 6e 6e 79 10 1b>

    //클라이언트에 직렬화한 데이터를 전송한다.
    res.send(buffer);
  });
});

module.exports = router;

🌞 app.js 수정

//아래 코드를 app.js파일에 추가한다.
//프론트쪽에서 api/proto로 요청이 오는 경우 routes/proto.js 파일로 라우팅
const protoRouter = require("./routes/proto");
pp.use("/api/proto", protoRouter);

2-2. 클라이언트 구현 Using vue

🌞 프론트엔드 환경 세팅

//예제에서는 vue2를 이용했다.
1) vue create frontend(폴더명)

//폴더 이동
2) cd frontend

//protobuf 사용을 위한 라이브러리 설치
3) npm i protobufjs

//서버와 http통신을 위해 axios 설치
4) npm i axios

//실행
5) npm run serve

🌞 main.js 파일 수정 (axios import)

//모든 컴포넌트에서 this.$http.get 과 같은 방식으로 http통신가능하도록 main.js파일에서 아래와 같이 추가하였다.
import Vue from "vue";
import App from "./App.vue";
import axios from "axios";

Vue.config.productionTip = false;
Vue.prototype.$http = axios;

new Vue({
  render: (h) => h(App),
}).$mount("#app");

🌞 .proto파일 작성
백엔드와 동일한 프로토 파일을 작성하면된다.
예제에서는 src/proto/test.proto 해당 경로에 작성하였다.

🌞 js파일 생성
프론트엔드 프로젝트에서 node와 같이 .proto파일을 바로 import할 수 없고, .proto파일을 .js파일로 변환하여 import해야한다.

package.json 파일에서 아래 부분을 추가해준 후
npm run proto 명령어를 실행하면 
src/proto/proto.js 파일이 생성된다.

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
  	//proto 파일이 변경된다면 npm run proto 명령어를 통해 js 파일을 갱신할 수 있다. (경로 확인 필요)
    "proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js  src/proto/*.proto"
  },

🌞 vue.config.js 파일 생성(frontend/vue.config.js)

//파일 추가 후 npm run build를 해주면 프론트에서 /api/~~~ api를 붙여서 요청할 경우 백엔드로 연결된다.

module.exports = {
  devServer: {
    proxy: {
      "/api": {
      //위에서 생성한 express 서버의 포트이다.(3000)
        target: "http://localhost:3000/api", 
        changeOrigin: true, //
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },
};

🌞 App.vue 파일 작성

vue cli로 vue 프로젝트를 생성한 경우 자동으로 생성되는 컴포넌트는 삭제하였다.

<template>
  <div id="app">
    <button type="button" @click="getData">test</button>
    {{ person }}
  </div>
</template>

<script>
//위에서 생성한 .js파일을 import해준다!
import ProtoRoot from "../src/proto/proto";

export default {
  name: "App",
  components: {},
  data() {
    return {
      person: {},
    };
  },
  methods: {
    getData() {
      this.$http.get("/api/proto",{
      //responseType 지정
      responseType: "arraybuffer"
      }).then((res) => {
        //메시지 타입을 얻어온다.
        let personMessage = ProtoRoot.lookupType("test.Person");
        //서버에 데이터를 요청하면 아까 서버에서 보낸 프로토버퍼가 올 것이다.
        //decdoe : uint8Array -> message
        let data = personMessage.decode(Buffer.from(res.data));
        console.log(data);
        this.person = data;
      });
    },
  },
};
</script>

<style></style>

2-3. 동작

위에서 작성한 코드를 실행시켜보자!

vs code termianl

//express 서버 실행 해두고,
backend> npm start

//프론트도 실행
frontend>npm run build
frontend>npm run serve

🌞 localhost:8080 접속
실행 후 vue-cli에서 기본으로 설정된 8080 포트로 접속해보자! 아래와 같은 화면이 보이고 data : person은 비어있는 상태이다.

🌞 test 버튼 클릭 & decode 결과 확인
서버 터미널에서는 아래와 같이 요청이 온 것을 확인할 수 있다.

서버측에서 프로토버퍼로 인코딩하여 보낸 데이터를 수신하여 decode하여 아래와 같이 person 데이터에 값이 들어간 것을 확인할 수 있다.

이상 간단한 예제를 통해 프로토콜 버퍼를 사용하는 법을 알아보았다. axios로 연습했는데 대용량 데이터를 계속 받아야해서 웹소켓으로 구현하게 되었지만🙄

틀린 부분이 있다면 알려주시면 감사하겠습니다(❁´◡`❁)

공부하면서 참고한 사이트들

profile
조금씩 매일 성장해나가고 싶은 병아리 개발자입니다:)

1개의 댓글

comment-user-thumbnail
2023년 5월 3일

예시까지 이해가 정말 잘되었어요. 감사합니다. :)

답글 달기