C++에서 Protobuf를 활용한 버전 호환성 높은 직렬화 구현

Protobuf는 구글에서 개발한 바이너리 직렬화 포맷으로, 다양한 프로그래밍 언어에서 사용되며, 효율적인 데이터 직렬화와 역직렬화를 지원합니다. 특히 C++ 환경에서 Protobuf를 활용하면 네트워크 통신, 파일 저장, 분산 시스템 등에서 빠르고 가벼운 데이터 변환이 가능합니다.

하지만 소프트웨어가 발전하면서 메시지 형식이 변경될 가능성이 크기 때문에, 새로운 버전에서도 기존 데이터를 정상적으로 처리할 수 있도록 버전 호환성(version compatibility)을 유지하는 것이 중요합니다.

본 기사에서는 C++에서 Protobuf를 활용하여 버전 호환성이 높은 직렬화 방식을 구현하는 방법을 다룹니다. Protobuf 메시지 정의 방법, 필드 변경 시의 주의사항, reservedoneof 필드 활용법, backward 및 forward compatibility 개념 등을 설명하며, 실제 C++ 코드 예제도 함께 제공합니다. 이를 통해 버전 변경에도 안정적으로 동작하는 Protobuf 기반 직렬화를 구현하는 방법을 익힐 수 있습니다.

목차
  1. Protobuf와 직렬화 개요
    1. Protobuf란 무엇인가?
    2. 직렬화의 필요성
    3. Protobuf의 주요 특징
  2. Protobuf 메시지 정의와 기본 직렬화
    1. Protobuf 메시지 정의 방식
    2. Protobuf 코드 생성
    3. Protobuf 메시지 직렬화와 역직렬화
    4. 실행 결과
    5. 기본 직렬화 방식 정리
  3. 필드 변경에 대한 버전 호환성 유지 전략
    1. 필드 추가 시의 호환성 유지
    2. 필드 삭제 시 주의할 점
    3. 예약 필드를 활용한 안전한 필드 삭제
    4. 필드 타입 변경 시 주의점
    5. 정리: Protobuf 버전 호환성을 유지하는 방법
  4. Backward Compatibility와 Forward Compatibility 개념
    1. Backward Compatibility(하위 호환성)
    2. Forward Compatibility(상위 호환성)
    3. Backward Compatibility와 Forward Compatibility 비교
    4. Protobuf 호환성을 유지하는 최선의 방법
  5. 옵션 필드와 예약 필드를 활용한 안정적인 변경
    1. 옵션 필드(`optional`)의 활용
    2. 예약 필드(`reserved`)의 활용
    3. 옵션 필드와 예약 필드를 활용한 안전한 변경 전략
    4. 정리: 안정적인 Protobuf 메시지 변경 방법
  6. Oneof 필드를 활용한 데이터 구조 변경
    1. Oneof 필드란?
    2. Oneof 필드를 활용한 버전 호환성 유지
    3. Oneof 필드를 사용할 때 주의할 점
    4. Oneof 필드를 활용한 데이터 구조 변경 전략
    5. 정리: Oneof 필드를 활용한 안정적인 구조 변경
  7. 버전 관리를 위한 Protobuf 코드 생성과 관리
    1. 1. Protobuf 코드 생성 및 기본 빌드 방법
    2. 2. Protobuf 파일의 버전 관리를 위한 폴더 구조
    3. 3. Protobuf 버전 관리를 위한 자동화 도구 (Bazel, Git)
    4. 4. 프로덕션 환경에서의 Protobuf 버전 관리 전략
    5. 정리: Protobuf 코드 생성과 버전 관리의 핵심
  8. C++에서 Protobuf 직렬화 적용 예제
    1. 1. Protobuf 메시지 정의
    2. 2. C++에서 Protobuf 직렬화 및 역직렬화
    3. 3. 실행 및 결과 확인
    4. 4. 네트워크 전송을 위한 직렬화
    5. 5. 직렬화된 데이터의 바이너리 크기 비교
    6. 정리: C++에서 Protobuf 직렬화 적용
  9. 요약

Protobuf와 직렬화 개요

Protobuf란 무엇인가?


Protobuf(Protocol Buffers)는 Google에서 개발한 데이터 직렬화 프레임워크로, JSON이나 XML보다 더 작고 더 빠르며 더 효율적인 바이너리 직렬화를 제공합니다. 다양한 프로그래밍 언어에서 사용할 수 있으며, 특히 네트워크 통신, 파일 저장, 데이터 교환 등에서 활용됩니다.

직렬화의 필요성


직렬화는 객체 데이터를 네트워크 전송이나 파일 저장을 위해 변환하는 과정입니다. 직렬화된 데이터를 다시 사용할 수 있도록 복원하는 과정을 역직렬화(deserialization)라고 합니다.

직렬화는 다음과 같은 상황에서 필요합니다.

  • 네트워크 통신: 서로 다른 시스템 간에 데이터를 주고받을 때
  • 파일 저장 및 데이터베이스 저장: 데이터를 영속적으로 저장할 때
  • IPC(Inter-Process Communication): 프로세스 간 데이터 공유가 필요할 때

기존 JSON, XML과 비교했을 때, Protobuf는 더 작은 용량과 빠른 속도를 자랑하며, 구조화된 데이터를 다루기에 적합합니다.

포맷데이터 크기처리 속도가독성확장성
JSON느림높음높음
XML매우 느림높음높음
Protobuf작음빠름낮음높음

Protobuf의 주요 특징

  • 바이너리 포맷 사용: JSON이나 XML보다 더 작고 빠름
  • 다양한 언어 지원: C++, Python, Java, Go 등 다양한 언어에서 사용 가능
  • 스키마 정의 가능: .proto 파일을 이용하여 구조화된 데이터 정의
  • 자동 코드 생성: Protobuf 컴파일러를 통해 특정 언어에 맞는 코드 자동 생성

이제 Protobuf에서 기본적인 직렬화 방식을 살펴보겠습니다.

Protobuf 메시지 정의와 기본 직렬화

Protobuf 메시지 정의 방식


Protobuf에서 데이터를 다루려면 먼저 메시지(message)를 정의해야 합니다. 메시지는 .proto 파일에서 정의하며, 다음과 같은 형식을 따릅니다.

syntax = "proto3"; // 프로토콜 버전 지정

message Person {
  string name = 1;  // 필드 이름과 타입 지정
  int32 age = 2;    // 각 필드에는 고유한 번호(ID)를 부여
  string email = 3;
}
  • syntax = "proto3"; → Protobuf 3버전을 사용한다는 선언
  • message PersonPerson이라는 메시지 정의
  • 각 필드는 데이터 타입과 함께 고유한 ID 번호를 가짐

Protobuf 코드 생성


.proto 파일을 작성한 후, C++에서 사용할 코드를 생성하려면 protoc(Protobuf Compiler)를 사용합니다.

터미널에서 다음 명령어를 실행하면 .pb.h.pb.cc 파일이 생성됩니다.

protoc --cpp_out=. person.proto
  • --cpp_out=.: 현재 디렉터리에 C++ 코드 생성
  • 결과물: person.pb.h, person.pb.cc

Protobuf 메시지 직렬화와 역직렬화


생성된 C++ 코드를 활용하여 데이터를 직렬화(serialize)하고, 다시 역직렬화(deserialize)하는 과정을 살펴보겠습니다.

#include <iostream>
#include <fstream>
#include "person.pb.h"  // Protobuf에서 생성된 헤더 파일

int main() {
    Person person;
    person.set_name("Alice");
    person.set_age(30);
    person.set_email("alice@example.com");

    // 직렬화하여 파일로 저장
    std::ofstream output("person.bin", std::ios::binary);
    person.SerializeToOstream(&output);
    output.close();

    // 역직렬화하여 데이터 복원
    Person person2;
    std::ifstream input("person.bin", std::ios::binary);
    if (person2.ParseFromIstream(&input)) {
        std::cout << "이름: " << person2.name() << "\n";
        std::cout << "나이: " << person2.age() << "\n";
        std::cout << "이메일: " << person2.email() << "\n";
    } else {
        std::cerr << "역직렬화 실패\n";
    }
    input.close();

    return 0;
}

실행 결과


파일 person.bin에 직렬화된 데이터를 저장한 후, 이를 다시 읽어 역직렬화하면 다음과 같은 결과가 출력됩니다.

이름: Alice  
나이: 30  
이메일: alice@example.com  

기본 직렬화 방식 정리

  1. .proto 파일에서 메시지 구조를 정의
  2. protoc 컴파일러를 사용하여 C++ 코드 생성
  3. C++ 코드에서 메시지를 생성하고, .bin 파일로 직렬화
  4. 파일을 읽어 역직렬화하여 데이터 복원

이제, 메시지 구조 변경 시에도 기존 데이터와 호환성을 유지하는 방법을 살펴보겠습니다.

필드 변경에 대한 버전 호환성 유지 전략

Protobuf의 강력한 장점 중 하나는 기존 데이터와의 호환성 유지가 가능하다는 점입니다. 하지만 메시지 구조를 변경할 때는 몇 가지 규칙을 지켜야 기존 데이터가 깨지지 않고 정상적으로 동작합니다.

필드 추가 시의 호환성 유지


새로운 필드를 추가하는 것은 비교적 안전한 변경 방식입니다. Protobuf에서는 기존 코드가 알지 못하는 필드를 무시하기 때문에, 새로운 필드를 추가해도 기존 데이터를 읽는 코드가 정상적으로 동작합니다.

안전한 추가 예시

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
}

// 새로운 필드 추가 (호환성 유지)
message Person {
  string name = 1;
  int32 age = 2;
  string email = 3; // 새로운 필드 추가
}
  • 기존 데이터를 읽을 때 email 필드는 무시되므로 backward compatibility(이전 버전과의 호환성)이 유지됩니다.
  • 새로운 버전의 코드에서는 email을 사용하여 확장 가능합니다.

필드 삭제 시 주의할 점


필드를 제거할 때는 무작정 삭제하면 안 됩니다. 이전 데이터를 읽을 때 삭제된 필드의 데이터가 유실될 가능성이 있기 때문입니다.

잘못된 삭제 방식 (호환성 깨짐)

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}

// 새로운 버전 (email 필드 삭제, 문제 발생 가능)
message Person {
  string name = 1;
  int32 age = 2;
}
  • email 필드가 사라졌으므로, 이전 버전의 데이터를 읽을 때 email 필드가 유실될 수 있음.
  • 해결 방법: 필드를 삭제하는 대신 예약(reserved) 처리를 사용해야 합니다.

예약 필드를 활용한 안전한 필드 삭제


Protobuf에서는 reserved 키워드를 사용하여 삭제된 필드 번호를 재사용하지 않도록 설정할 수 있습니다.

안전한 삭제 방법

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

  // 필드 3번을 삭제하고 예약 처리
  reserved 3;
}
  • reserved 3;를 선언하면 미래 버전에서도 3번 필드를 다시 사용하지 않도록 방지할 수 있습니다.
  • 만약 3번 필드를 다시 사용하면 컴파일 오류가 발생하여 실수를 방지할 수 있습니다.

필드 타입 변경 시 주의점


Protobuf에서 필드 타입을 변경할 때는 데이터가 호환되지 않을 수 있으므로 신중해야 합니다.

잘못된 타입 변경 (호환성 깨짐)

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
}

// 새로운 버전 (age의 타입 변경 → 문제 발생 가능)
message Person {
  string name = 1;
  string age = 2; // int32 → string 변경 (호환성 문제 발생)
}
  • 이전 데이터에서 ageint32로 저장되었지만, 새로운 버전에서는 string으로 해석해야 함.
  • 결과: 역직렬화 오류 발생 가능

안전한 타입 변경 방법

  • 기존 필드를 유지하고 새로운 필드를 추가하는 방식이 안전함.
message Person {
  string name = 1;
  int32 age = 2;
  string age_text = 3; // 새로운 필드 추가 (안전)
}
  • 새로운 코드에서는 age_text 필드를 사용하고, 이전 데이터는 age 필드를 계속 유지할 수 있음.

정리: Protobuf 버전 호환성을 유지하는 방법

변경 사항가능 여부해결 방법
새로운 필드 추가✅ 가능필드 번호를 새로 할당하면 안전
필드 삭제⚠️ 주의 필요reserved를 사용하여 번호 재사용 방지
필드 타입 변경❌ 위험새로운 필드를 추가하는 방식이 안전
필드 번호 변경❌ 위험변경하지 말고 새로운 필드를 추가

이제 backward compatibility(이전 버전과의 호환성)forward compatibility(미래 버전과의 호환성) 개념을 더 자세히 살펴보겠습니다.

Backward Compatibility와 Forward Compatibility 개념

Protobuf에서 버전 호환성(version compatibility)을 유지하려면 Backward Compatibility(하위 호환성)Forward Compatibility(상위 호환성) 개념을 이해하는 것이 중요합니다.

  • Backward Compatibility(하위 호환성):
    새로운 버전의 프로그램이 이전 버전에서 생성된 데이터를 정상적으로 읽을 수 있어야 함
  • Forward Compatibility(상위 호환성):
    이전 버전의 프로그램이 새로운 버전에서 생성된 데이터를 일부라도 정상적으로 읽을 수 있어야 함

Backward Compatibility(하위 호환성)


이전 버전에서 생성된 데이터를 새로운 버전에서도 정상적으로 사용할 수 있도록 보장하는 것이 하위 호환성입니다.

하위 호환성을 유지하는 안전한 변경

  • 새로운 필드 추가 → 가능
  • 필드 삭제 대신 reserved 사용 → 가능
  • 기존 필드의 타입 변경 → 불가능

예제:

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
}

// 새로운 버전 (하위 호환성 유지)
message Person {
  string name = 1;
  int32 age = 2;
  string email = 3; // 새로운 필드 추가 (안전)
}

이전 버전의 코드에서 새로운 데이터(email 필드)가 포함된 메시지를 읽으면 email 필드를 무시하고 기존 데이터(name, age)만 처리하기 때문에 하위 호환성이 유지됩니다.

하위 호환성이 깨지는 변경

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
}

// 새로운 버전 (age의 타입 변경 → 하위 호환성 깨짐)
message Person {
  string name = 1;
  string age = 2; // int32 → string 변경 (호환성 문제 발생)
}

이전 버전에서 int32로 저장된 데이터를 새로운 버전에서 string으로 읽으려 하면 역직렬화 오류가 발생할 수 있습니다. 따라서 필드 타입 변경은 피해야 합니다.

Forward Compatibility(상위 호환성)


새로운 버전에서 생성된 데이터를 이전 버전의 프로그램이 일부라도 정상적으로 읽을 수 있도록 보장하는 것이 상위 호환성입니다.

상위 호환성을 유지하는 안전한 변경

  • 기존 필드를 유지하면서 새로운 필드 추가
  • 이전 버전에서 알 수 없는 필드는 무시됨

예제:

// 새로운 버전
message Person {
  string name = 1;
  int32 age = 2;
  string email = 3; // 새로운 필드 추가
}

이 메시지를 이전 버전의 코드에서 읽으면 nameage 필드만 처리하고 email 필드는 무시됩니다. 즉, 기존 기능은 정상적으로 동작하면서 새로운 필드는 활용하지 않게 됩니다.

상위 호환성이 깨지는 변경

// 기존 버전
message Person {
  string name = 1;
  int32 age = 2;
}

// 새로운 버전 (필드 번호 재사용 → 문제 발생 가능)
message Person {
  string name = 1;
  bool is_admin = 2; // 기존 필드(age)의 번호를 재사용 (호환성 문제)
}

이전 버전에서는 age 필드가 int32였지만, 새로운 버전에서는 같은 필드 번호(2번)를 bool로 사용했습니다.

  • 결과: 이전 버전의 프로그램에서 새로운 데이터를 읽을 때 잘못된 값이 해석될 가능성이 있음.
  • 해결 방법: 새로운 번호를 사용하고, 기존 필드는 reserved로 설정

Backward Compatibility와 Forward Compatibility 비교

변경 사항Backward Compatibility (하위 호환)Forward Compatibility (상위 호환)
새로운 필드 추가✅ 유지 가능✅ 유지 가능
필드 삭제⚠️ reserved 사용 시 가능⚠️ reserved 사용 시 가능
필드 타입 변경❌ 깨짐❌ 깨짐
필드 번호 변경❌ 깨짐❌ 깨짐

Protobuf 호환성을 유지하는 최선의 방법

  1. 새로운 필드는 추가 가능하지만, 기존 필드를 변경하지 않는다.
  2. 삭제할 필드는 반드시 reserved로 설정한다.
  3. 필드 번호를 절대 재사용하지 않는다.
  4. 타입을 변경하지 않고, 새로운 필드를 추가하는 방식으로 개선한다.

이제 Protobuf에서 옵션 필드와 예약 필드(reserved)를 활용하여 안정적으로 변경하는 방법을 살펴보겠습니다.

옵션 필드와 예약 필드를 활용한 안정적인 변경

Protobuf에서 메시지 필드를 변경할 때 옵션 필드(optional)와 예약 필드(reserved)를 활용하면 버전 호환성을 유지하면서도 안정적으로 데이터 구조를 수정할 수 있습니다.

옵션 필드(`optional`)의 활용

Protobuf 3에서는 모든 필드가 기본적으로 옵션(optional) 필드로 간주됩니다. 즉, 값이 설정되지 않은 필드는 자동으로 기본값(default value)을 가집니다. 하지만, optional 키워드를 명시적으로 사용하면 필드의 존재 여부를 체크할 수 있는 기능이 추가됩니다.

optional 필드를 활용한 변경

syntax = "proto3";

message Person {
  string name = 1;
  optional string email = 2; // optional 키워드 추가
}
  • email 필드가 없을 수도 있으며, 존재 여부를 체크할 수 있음.
  • 새로운 버전에서 email 필드를 추가했을 때, 기존 데이터가 손실되지 않음.
  • 기존 클라이언트는 email 필드를 무시하고, 새로운 클라이언트는 email 필드를 활용 가능.

has_FIELDNAME()을 활용한 존재 여부 체크

C++에서는 has_FIELDNAME() 함수를 사용하여 필드가 존재하는지 확인할 수 있습니다.

Person person;
if (person.has_email()) {  // email 필드가 존재하는지 확인
    std::cout << "이메일: " << person.email() << std::endl;
} else {
    std::cout << "이메일 정보 없음" << std::endl;
}

예약 필드(`reserved`)의 활용

Protobuf에서는 필드를 제거할 때 단순히 삭제하면 안 되고, 반드시 reserved를 사용해야 합니다.

  • 예약된 필드 번호는 다른 필드에 재사용할 수 없음.
  • 실수로 삭제된 필드 번호를 재사용하면 데이터 손상 위험이 있음.

❌ 잘못된 삭제 방식 (호환성 깨짐)

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3; // 삭제할 필드
}

// email 필드를 삭제 (reserved 처리 안 함 → 위험)
message Person {
  string name = 1;
  int32 age = 2;
}
  • 필드 email(번호 3번)이 사라지면서 이전 데이터를 읽을 때 데이터가 손실될 위험이 있음.
  • 만약 3번 필드를 다른 타입의 필드로 재사용하면 역직렬화 시 오류 발생 가능.

✅ 안전한 삭제 방법 (reserved 활용)

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

  // 필드 3번을 삭제하고 예약 처리
  reserved 3;
}
  • 필드 번호 3reserved 처리하면, 나중에 같은 번호를 재사용하지 못하도록 보호.
  • 만약 message Person3번 필드를 새로 추가하려 하면 컴파일 오류 발생.

✅ 여러 개의 필드를 한 번에 예약 가능

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

  reserved 3, 5, 7 to 9; // 여러 개의 필드 번호를 예약
  reserved "email", "address"; // 필드 이름도 예약 가능
}
  • 필드 번호 범위를 지정하여 예약 가능 (7 to 9)
  • 필드 이름을 예약하여 실수로 같은 필드를 다시 추가하지 못하도록 보호

옵션 필드와 예약 필드를 활용한 안전한 변경 전략

변경 사항해결 방법예제
새로운 필드 추가optional 키워드 사용optional string email = 3;
필드 삭제reserved 사용reserved 3;
필드 타입 변경새로운 필드 추가 후 기존 필드 유지string new_age = 4;
필드 번호 변경❌ 불가능 (필드 번호 변경 금지)X

정리: 안정적인 Protobuf 메시지 변경 방법

  1. 새로운 필드는 optional을 사용하여 추가한다.
  2. 필드를 삭제할 때는 반드시 reserved로 예약한다.
  3. 기존 필드 타입을 변경하지 않고, 새로운 필드를 추가한다.
  4. 필드 번호를 변경하지 않는다.

이제 Oneof 필드를 활용한 데이터 구조 변경 방법을 살펴보겠습니다.

Oneof 필드를 활용한 데이터 구조 변경

Protobuf에서 데이터 구조를 변경할 때 기존 필드를 유지하면서도 새로운 필드를 추가해야 하는 경우가 많습니다.
이때 oneof 필드를 활용하면 하나의 필드만 유지하면서도 유연하게 데이터 구조를 변경할 수 있어 메시지 크기를 최적화하고, 호환성을 유지할 수 있습니다.

Oneof 필드란?

  • oneof을 사용하면 여러 개의 필드 중 하나만 활성화됨.
  • 여러 필드 중 하나만 값을 가질 수 있으며, 다른 필드는 자동으로 초기화됨.
  • 불필요한 필드 값을 제거하여 메모리 사용을 줄이고, 데이터 크기를 최적화할 수 있음.

oneof 필드를 활용한 기본 예제

message ContactInfo {
  oneof contact {
    string email = 1;
    string phone = 2;
  }
}
  • emailphone하나의 필드만 값을 가질 수 있음.
  • email이 설정되면 phone은 자동으로 초기화됨.

✅ C++ 코드에서 oneof 필드 사용하기

#include <iostream>
#include "contact.pb.h"

int main() {
    ContactInfo contact;
    contact.set_email("alice@example.com");

    if (contact.has_email()) {
        std::cout << "이메일: " << contact.email() << std::endl;
    } else if (contact.has_phone()) {
        std::cout << "전화번호: " << contact.phone() << std::endl;
    }

    // 전화번호 설정하면 기존 email 값은 자동 초기화됨
    contact.set_phone("123-456-7890");

    if (contact.has_email()) {
        std::cout << "이메일: " << contact.email() << std::endl;
    } else if (contact.has_phone()) {
        std::cout << "전화번호: " << contact.phone() << std::endl;
    }

    return 0;
}

✅ 실행 결과

이메일: alice@example.com
전화번호: 123-456-7890
  • 처음에는 email 필드가 설정됨.
  • 이후 phone을 설정하면 email 값이 자동으로 초기화됨.

Oneof 필드를 활용한 버전 호환성 유지

✅ 기존 필드에서 새로운 필드로 안전하게 전환

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

  oneof contact_info {
    string email = 3;
    string phone = 4;
  }
}
  • 기존 버전에서 email만 사용했더라도, 새로운 버전에서는 phone을 추가할 수 있음.
  • 이전 데이터를 읽을 때도 email을 유지하면서 새로운 버전에서는 phone을 활용 가능.

❌ 잘못된 필드 변경 (호환성 깨짐)

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

  string email = 3;
  string phone = 4; // 새로운 필드 추가 (oneof 미사용 → 데이터 충돌 가능)
}
  • 기존 버전이 email을 사용하고 있었는데, 새로운 버전에서 phone을 추가하면 두 필드가 충돌할 가능성이 있음.
  • oneof을 사용하면 필드 충돌 없이 하나의 값만 유지할 수 있음.

Oneof 필드를 사용할 때 주의할 점

  • 하나의 필드만 활성화됨 → 다른 필드는 자동으로 초기화됨.
  • 이전 데이터를 읽을 때 oneof 필드가 설정되지 않을 수도 있음 → 기본값을 고려해야 함.
  • Oneof 필드는 has_FIELDNAME()을 사용하여 필드가 설정되었는지 확인 가능.

Oneof 필드를 활용한 데이터 구조 변경 전략

변경 사항해결 방법예제
새로운 필드 추가oneof을 사용하여 기존 필드와 병렬로 유지oneof { email, phone }
기존 필드 삭제reserved 처리 후 oneof 추가reserved 3; oneof contact_info { phone = 4; }
타입 변경oneof을 사용하여 새 필드로 전환oneof { old_format, new_format }

정리: Oneof 필드를 활용한 안정적인 구조 변경

  1. 기존 필드를 삭제하지 말고, oneof을 사용하여 새로운 필드 추가
  2. 한 번에 하나의 필드만 활성화되므로 데이터 충돌 방지 가능
  3. C++에서 has_FIELDNAME()을 사용하여 필드 존재 여부를 확인
  4. 메모리 사용을 줄이고, 데이터 크기를 최적화하는 효과

이제 버전 관리를 위한 Protobuf 코드 생성과 관리 방법을 살펴보겠습니다.

버전 관리를 위한 Protobuf 코드 생성과 관리

Protobuf를 사용하여 버전 호환성을 유지하면서도 효율적으로 코드 관리를 수행하는 방법을 이해하는 것은 중요합니다.
여기서는 Protobuf 컴파일러(protoc)를 활용한 코드 생성, 버전 관리를 위한 폴더 구조, 자동화된 빌드 시스템(CMake, Bazel) 활용 방법을 설명합니다.


1. Protobuf 코드 생성 및 기본 빌드 방법

Protobuf 파일(.proto)을 작성한 후, 이를 C++ 코드로 변환하려면 protoc(Protobuf Compiler)를 사용해야 합니다.

✅ Protobuf 코드 생성 예제

protoc --cpp_out=. person.proto
  • --cpp_out=. → C++ 코드로 변환하여 현재 디렉터리에 생성
  • 실행 후 person.pb.hperson.pb.cc 파일이 생성됨

✅ CMake를 사용한 Protobuf 코드 자동 빌드

프로젝트에서 CMake를 사용하여 Protobuf 파일을 자동 변환하고 빌드할 수 있습니다.

📌 CMakeLists.txt 설정 예제

cmake_minimum_required(VERSION 3.10)
project(ProtobufExample)

find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})

add_executable(main main.cpp person.pb.cc)
target_link_libraries(main ${Protobuf_LIBRARIES})
  • find_package(Protobuf REQUIRED) → Protobuf 라이브러리를 자동으로 찾음
  • .proto 파일 변경 시 CMake 빌드 실행하면 자동 변환됨

2. Protobuf 파일의 버전 관리를 위한 폴더 구조

버전 관리에서 중요한 점은 기존 버전을 유지하면서 새로운 버전을 추가하는 것입니다.
이를 위해 다음과 같은 폴더 구조를 유지하는 것이 좋습니다.

📌 버전별 폴더 구조 예제

proto/
 ├── v1/
 │   ├── person_v1.proto
 │   ├── address_v1.proto
 │   └── ...
 ├── v2/
 │   ├── person_v2.proto
 │   ├── address_v2.proto
 │   └── ...
  • 각 버전을 별도 폴더(v1, v2)에 보관하여 기존 버전과의 충돌 방지
  • 새로운 버전이 추가될 때 기존 버전(v1)을 삭제하지 않고 유지

📌 버전별 메시지 예제

// v1/person_v1.proto
syntax = "proto3";
package v1;
message Person {
  string name = 1;
  int32 age = 2;
}
// v2/person_v2.proto (새로운 필드 추가)
syntax = "proto3";
package v2;
message Person {
  string name = 1;
  int32 age = 2;
  string email = 3; // 새로운 필드 추가
}
  • v1.Personv2.Person을 분리하여 이전 버전과의 충돌 방지
  • 새로운 기능을 추가하더라도 기존 서비스에서는 v1.Person을 계속 사용할 수 있음

3. Protobuf 버전 관리를 위한 자동화 도구 (Bazel, Git)

✅ Bazel을 활용한 Protobuf 빌드 자동화

Bazel은 Protobuf 파일을 자동으로 빌드하고, 버전 관리를 쉽게 할 수 있도록 도와줍니다.

📌 Bazel 빌드 설정 (BUILD 파일 예제)

load("@rules_proto//proto:defs.bzl", "proto_library", "cc_proto_library")

proto_library(
    name = "person_proto",
    srcs = ["person.proto"],
)

cc_proto_library(
    name = "person_cc_proto",
    deps = [":person_proto"],
)
  • proto_library.proto 파일을 라이브러리로 빌드
  • cc_proto_library → C++용 .pb.h, .pb.cc를 자동 생성

✅ Git을 활용한 Protobuf 버전 관리

Git에서는 태그와 브랜치를 활용하여 각 버전의 Protobuf 파일을 관리하는 것이 중요합니다.

📌 Git 태그를 활용한 버전 관리 예제

git tag v1.0   # v1 버전 태그 생성
git tag v2.0   # v2 버전 태그 생성
git push origin --tags  # 태그를 원격 저장소에 푸시
  • v1.0, v2.0 태그를 사용하여 각 버전의 Protobuf 파일을 유지
  • 필요할 경우 과거 버전(git checkout v1.0)으로 되돌아갈 수 있음

4. 프로덕션 환경에서의 Protobuf 버전 관리 전략

관리 항목적용 방법
필드 추가기존 구조 유지 후 새로운 필드 추가
필드 삭제reserved를 사용하여 재사용 방지
버전별 유지보수폴더(v1/, v2/)를 사용하여 버전별 관리
자동화 빌드CMake, Bazel을 활용하여 .proto 파일 자동 빌드
Git 버전 태그git tag를 사용하여 각 버전 관리

정리: Protobuf 코드 생성과 버전 관리의 핵심

  1. protoc을 사용하여 .proto 파일을 C++ 코드로 변환
  2. CMake 또는 Bazel을 활용하여 빌드를 자동화
  3. 버전별 폴더 구조(v1/, v2/)를 사용하여 호환성 유지
  4. Git 태그를 사용하여 과거 버전으로 되돌릴 수 있도록 관리

이제 C++에서 Protobuf 직렬화를 실제로 적용하는 예제 코드를 살펴보겠습니다.

C++에서 Protobuf 직렬화 적용 예제

이제 C++에서 Protobuf 직렬화(Serialization) 및 역직렬화(Deserialization)를 실제로 구현하는 방법을 살펴보겠습니다.
이 예제에서는 Protobuf 메시지를 파일에 저장하고, 다시 읽어오는 과정을 다룹니다.


1. Protobuf 메시지 정의

먼저, person.proto 파일을 작성합니다.

syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
  optional string email = 3;  // optional 필드 추가
}
  • syntax = "proto3"; → Protobuf 3 버전 사용 선언
  • message PersonPerson 메시지 정의
  • optional string emailoptional을 사용하여 필드 존재 여부 확인 가능

📌 C++ 코드 생성을 위해 터미널에서 실행

protoc --cpp_out=. person.proto
  • 실행 후 person.pb.hperson.pb.cc 파일이 생성됨.

2. C++에서 Protobuf 직렬화 및 역직렬화

📌 person.pb.h, person.pb.cc를 포함한 C++ 코드 (main.cpp)

#include <iostream>
#include <fstream>
#include "person.pb.h"  // Protobuf에서 생성된 헤더 파일

// Protobuf 데이터를 파일에 저장하는 함수
void SaveToFile(const Person& person, const std::string& filename) {
    std::ofstream output(filename, std::ios::binary);
    if (!person.SerializeToOstream(&output)) {
        std::cerr << "직렬화 실패!" << std::endl;
    }
    output.close();
}

// Protobuf 데이터를 파일에서 불러오는 함수
void LoadFromFile(Person& person, const std::string& filename) {
    std::ifstream input(filename, std::ios::binary);
    if (!person.ParseFromIstream(&input)) {
        std::cerr << "역직렬화 실패!" << std::endl;
    }
    input.close();
}

int main() {
    // 1. Protobuf 객체 생성 및 값 설정
    Person person;
    person.set_name("Alice");
    person.set_age(30);
    person.set_email("alice@example.com");

    // 2. Protobuf 데이터를 파일로 저장
    std::string filename = "person_data.bin";
    SaveToFile(person, filename);

    // 3. 파일에서 데이터를 다시 불러오기
    Person loaded_person;
    LoadFromFile(loaded_person, filename);

    // 4. 역직렬화된 데이터 출력
    std::cout << "이름: " << loaded_person.name() << std::endl;
    std::cout << "나이: " << loaded_person.age() << std::endl;

    if (loaded_person.has_email()) {  // optional 필드 체크
        std::cout << "이메일: " << loaded_person.email() << std::endl;
    } else {
        std::cout << "이메일 정보 없음" << std::endl;
    }

    return 0;
}

3. 실행 및 결과 확인

📌 빌드 및 실행 방법

g++ main.cpp person.pb.cc -o protobuf_example -lprotobuf
./protobuf_example

📌 실행 결과

이름: Alice  
나이: 30  
이메일: alice@example.com  

4. 네트워크 전송을 위한 직렬화

Protobuf는 네트워크 통신 시에도 사용 가능합니다.
아래 예제는 Protobuf 메시지를 std::string으로 변환하여 소켓 전송에 활용하는 방법입니다.

Protobuf 메시지를 std::string으로 변환

// 직렬화된 데이터를 string 형태로 변환
std::string SerializeToString(const Person& person) {
    std::string output;
    if (!person.SerializeToString(&output)) {
        std::cerr << "직렬화 실패!" << std::endl;
    }
    return output;
}

// 문자열 데이터를 Protobuf 메시지로 변환
void ParseFromString(Person& person, const std::string& data) {
    if (!person.ParseFromString(data)) {
        std::cerr << "역직렬화 실패!" << std::endl;
    }
}
  • SerializeToString()std::string 형식으로 직렬화하여 네트워크 전송 가능
  • ParseFromString() → 네트워크에서 받은 데이터를 역직렬화

5. 직렬화된 데이터의 바이너리 크기 비교

Protobuf는 JSON이나 XML보다 훨씬 작은 크기의 데이터를 생성합니다.
아래 코드에서는 Protobuf vs JSON 데이터 크기 비교를 수행합니다.

JSON과 Protobuf 크기 비교

#include <google/protobuf/util/json_util.h>

void CompareSerializationSize(const Person& person) {
    std::string proto_data;
    person.SerializeToString(&proto_data);
    std::cout << "Protobuf 데이터 크기: " << proto_data.size() << " bytes" << std::endl;

    std::string json_data;
    google::protobuf::util::MessageToJsonString(person, &json_data);
    std::cout << "JSON 데이터 크기: " << json_data.size() << " bytes" << std::endl;
}

📌 출력 예제

Protobuf 데이터 크기: 20 bytes  
JSON 데이터 크기: 45 bytes  
  • Protobuf는 JSON보다 데이터 크기가 50% 이상 작음
  • 바이너리 포맷을 사용하여 네트워크 대역폭 절약 가능

정리: C++에서 Protobuf 직렬화 적용


Protobuf 메시지를 정의하고 C++ 코드(.pb.h, .pb.cc)를 생성
Protobuf 데이터를 파일에 저장 및 불러오기 (SerializeToOstream(), ParseFromIstream())
Protobuf 메시지를 네트워크 전송을 위해 std::string으로 변환 가능
JSON 대비 Protobuf가 훨씬 작은 크기의 바이너리 데이터를 생성

이제 마지막으로 전체 내용을 정리하는 요약을 살펴보겠습니다.

요약


본 기사에서는 C++에서 Protobuf를 활용하여 버전 호환성을 유지하면서 직렬화 및 역직렬화하는 방법에 대해 자세히 설명했습니다.

  • Protobuf 메시지 정의 및 코드 생성: .proto 파일을 작성한 후 protoc 컴파일러를 사용하여 C++ 코드로 변환하는 방법을 다루었습니다.
  • 직렬화 및 역직렬화: Protobuf 메시지를 파일에 저장하고 불러오는 방법을 C++ 코드 예제와 함께 설명했습니다.
  • 버전 관리: Protobuf 파일을 버전별로 관리하는 방법과 이를 통해 기존 버전의 호환성을 유지하는 방법을 소개했습니다.
  • 자동화 도구 활용: CMake와 Bazel을 사용하여 Protobuf 파일의 빌드를 자동화하고, Git을 활용해 버전 관리 태그를 적용하는 방법을 다뤘습니다.
  • 네트워크 통신: 직렬화된 데이터를 std::string으로 변환하여 네트워크 전송에 활용하는 방법을 설명했습니다.
  • 효율성: Protobuf는 JSON이나 XML에 비해 더 작은 데이터 크기를 생성하여 대역폭 절약저장 공간 최적화를 가능하게 합니다.

이 기사를 통해 C++에서 Protobuf를 사용하여 효율적이고 호환성 높은 직렬화를 구현할 수 있는 방법을 배웠습니다.

목차
  1. Protobuf와 직렬화 개요
    1. Protobuf란 무엇인가?
    2. 직렬화의 필요성
    3. Protobuf의 주요 특징
  2. Protobuf 메시지 정의와 기본 직렬화
    1. Protobuf 메시지 정의 방식
    2. Protobuf 코드 생성
    3. Protobuf 메시지 직렬화와 역직렬화
    4. 실행 결과
    5. 기본 직렬화 방식 정리
  3. 필드 변경에 대한 버전 호환성 유지 전략
    1. 필드 추가 시의 호환성 유지
    2. 필드 삭제 시 주의할 점
    3. 예약 필드를 활용한 안전한 필드 삭제
    4. 필드 타입 변경 시 주의점
    5. 정리: Protobuf 버전 호환성을 유지하는 방법
  4. Backward Compatibility와 Forward Compatibility 개념
    1. Backward Compatibility(하위 호환성)
    2. Forward Compatibility(상위 호환성)
    3. Backward Compatibility와 Forward Compatibility 비교
    4. Protobuf 호환성을 유지하는 최선의 방법
  5. 옵션 필드와 예약 필드를 활용한 안정적인 변경
    1. 옵션 필드(`optional`)의 활용
    2. 예약 필드(`reserved`)의 활용
    3. 옵션 필드와 예약 필드를 활용한 안전한 변경 전략
    4. 정리: 안정적인 Protobuf 메시지 변경 방법
  6. Oneof 필드를 활용한 데이터 구조 변경
    1. Oneof 필드란?
    2. Oneof 필드를 활용한 버전 호환성 유지
    3. Oneof 필드를 사용할 때 주의할 점
    4. Oneof 필드를 활용한 데이터 구조 변경 전략
    5. 정리: Oneof 필드를 활용한 안정적인 구조 변경
  7. 버전 관리를 위한 Protobuf 코드 생성과 관리
    1. 1. Protobuf 코드 생성 및 기본 빌드 방법
    2. 2. Protobuf 파일의 버전 관리를 위한 폴더 구조
    3. 3. Protobuf 버전 관리를 위한 자동화 도구 (Bazel, Git)
    4. 4. 프로덕션 환경에서의 Protobuf 버전 관리 전략
    5. 정리: Protobuf 코드 생성과 버전 관리의 핵심
  8. C++에서 Protobuf 직렬화 적용 예제
    1. 1. Protobuf 메시지 정의
    2. 2. C++에서 Protobuf 직렬화 및 역직렬화
    3. 3. 실행 및 결과 확인
    4. 4. 네트워크 전송을 위한 직렬화
    5. 5. 직렬화된 데이터의 바이너리 크기 비교
    6. 정리: C++에서 Protobuf 직렬화 적용
  9. 요약