나만의 작은 도서관
[TIL][C++] 250526 MMO 서버 개발 25일차: 이미 git에서 추적되는 파일 제거하기, Protobuf 자동 생성 함수에 대해 알아보기 본문
Today I Learn
[TIL][C++] 250526 MMO 서버 개발 25일차: 이미 git에서 추적되는 파일 제거하기, Protobuf 자동 생성 함수에 대해 알아보기
pledge24 2025. 5. 27. 01:02주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
이미 git에서 추적되는 파일 제거하기
- 나는 당연히 .gitignore에 적용한 타입의 파일은 이미 올라간 파일에도 적용될 줄 알았다. 하지만 .gitignore에 추적하지 않을 파일 타입을 추가해도 계속해서 git에 올라가는 것을 발견했고, 찾아보니 추적은 계속된다는 것이었다.
.gitignore의 특징
- .gitignore은 “새로 추가되려는 파일”에만 적용되며, Git이 이미 추적(tracked) 중인 파일은 해당 파일을 추가해도 무시되지 않는다.
- 예를 들어, git으로 추적되는 디렉토리 A가 있고, X.dll 파일이 존재한다고 가정해 보자.
- 이 상황에서 뒤늦게 .gitignore에 .dll 확장자 파일들을 추적하지 않기 위해 .dll을 .gitignore에 추가했다면, 앞으로 추가되는 Y.dll, Z.dll은 추적 대상에 포함되지 않을 것이다
- 하지만 X.dll은 이미 git의 추적 대상이므로, X.dll이 변경되면(수정 및 삭제) 해당 변경 사항은 스테이지에 올라간다.
일부 파일을 추적 대상에서 제거하는 법
- 위와 같은 이유로 중간에 .gitignore가 변경되더라도 제외 대상인 파일들이 계속해서 스테이지에 변경사항이 올라갈 것이다.(매우 짜증 나는 상황)
- 그렇다고 해당 파일 및 폴더들을 로컬에서 깡그리 지우고 다시 생성하기엔 너무 과격하고 부담이 많이 된다.
- 다행히도 로컬 내용을 변경하지 않고 git에 추적되지 않도록 하는 명령어가 있는데, 명령어는 아래와 같다.
git rm --cached 경로
// ex. Build 폴더 아래에 있는 폴더를 추적하지 않음
git rm --cached Build/
- 위 명령어를 사용하면 이미 추적 중인 파일들을 전부 추적 대상에서 제거할 수 있다.
- 정확히는 지정한 경로에 포함된 파일들(또는 파일)을 추적대상에서 제거하는 건데, 이 때문에 해당 경로에 존재하는 폴더 내부의 파일들에는 적용되지 않는다.
- 만약, 경로 내부에 있는 폴더 또한 추적 대상에서 제거하고 싶다면 재귀 옵션인 -r을 아래와 같이 붙이면 된다.
git -r rm --cached 경로
// ex. Build 폴더 및 내부 파일/폴더들을 추적하지 않음
git -r rm --cached Build/
Protobuf 자동 생성 함수에 대해 알아보기
- protobuf의 컴파일러 protoc에 의해 자동으로 생성된 파일들에는 각 메시지의 구조로 만들어진 클래스가 정의되어 있다.(ex. Protocol::S_LOGIN)
- 이 클래스 내부에는 자동으로 생성된 멤버 함수들이 여럿 있는데, 오늘은 이 멤버 함수들 중 일부에 대해 알아본 내용을 정리하고자 한다.
set_xxx() / xxx()
- 가장 기본적인 멤버 함수인 setter/getter가 있다. setter는 함수 앞에 set_이 붙고 그 뒤엔 필드 이름이 붙는다. getter의 경우 get_이 붙진 않고 그저 필드 이름으로 함수 이름을 가진다.
// Protocol.proto file
message S_LOGIN
{
bool success = 1;
repeated PlayerInfo players = 2; // 아이디 발급 전
}
// code
#include "Protocol.pb.h"
Protocol::S_LOGIN pkt;
pkt.set_success(true);
bool sc = pkt.success();
구조체 필드 관련 함수
- 사용자 정의 타입인 구조체 타입의 필드가 메시지 내에 존재할 수 있다. 이 경우, 여러 함수들을 사용할 수 있는데, 각 함수들을 알아보면 아래와 같다.
// Protocol.proto file
message S_ENTER_GAME
{
bool success = 1;
PlayerInfo player = 2;
}
message PlayerInfo
{
uint64 object_id = 1;
float x = 2;
float y = 3;
float z = 4;
float yaw = 5;
}
getter() 함수: xxx()와 mutable_xxx()
Protocol::S_ENTER_GAME pkt;
Protocol::PlayerInfo p1 = pkt.player();
p1.set_x(10); // 안 됨: 읽기 전용임
Protocol::PlayerInfo* p2 = pkt.mutable_player();
p2->set_x(10); // 됨: 쓰기 가능 참조임
- protobuf엔 2가지 getter함수가 있다. 하나는 xxx() 함수로, const& 를 반환하는 읽기 전용 참조이며, 다른 하나는 mutable_xxx() 함수로 포인터를 반환하는 쓰기 가능 참조이다.
setter() 함수: set_allocated_xxx()
- 구조체 형태의 필드인 경우, 일반적인 set_xxx() 형태의 함수는 존재하지 않는다. 대신, set_allocated_xxx() 형태의 함수가 존재한다.
- set_allocated_xxx()의 경우, 포인터 타입을 인자로 받는다.
Protocol::PlayerInfo* p = new Protocol::PlayerInfo();
p->set_object_id(1);
p->set_x(100.f);
p->set_y(100.f);
p->set_z(100.f);
p->set_yaw(45.f);
Protocol::S_ENTER_GAME pkt;
pkt.set_allocated_player(p); // p의 메모리 소유권이 pkt로 이동
- 조심해야 할 점은 set_allocated_xxx()의 경우, 인자로 받는 변수의 메모리 소유권(ownership)을 protobuf 객체로 완전히 이전(move)되기 때문에, 해당 메모리를 직접적으로 해제해 주면 안 된다.
- 만약 인자의 소유권을 뺏기고 않고 싶다면(어딘가에서 계속 들고 있어야 하는 데이터인 경우처럼) release_xxx() 함수를 호출하면 된다.
...
Protocol::S_ENTER_GAME pkt;
pkt.set_allocated_player(p); // p의 메모리 소유권이 pkt로 이동
p = pkt.release_player();// 소유권 돌려줌
- 참고로 release_player를 하는 순간, pkt는 데이터에 접근할 수 없다. (물건을 넘겨주면 물건이 나한테 없는 것과 같다)
복사 함수: CopyFrom()
- CopyFrom() 함수는 “깊은 복사(Deep Copy)” 방식의 복사를 수행하는 함수로, 수행 시 객체의 모든 데이터를 다른 객체로 완전히 복사한다.
Protocol::PlayerInfo p;
p.set_object_id(1);
p.set_x(100.f);
p.set_y(100.f);
p.set_z(100.f);
p.set_yaw(45.f);
Protocol::S_ENTER_GAME pkt;
pkt.mutable_player()->CopyFrom(p);
*(pkt.mutable_player()) = p; // CopyFrom과 동일한 효과
- CopyFrom()은 완전히 똑같은 객체가 하나 더 생긴다는 점에서 용이하지만, 깊은 복사 방식이므로 대용량 데이터나 복잡한 중첩 구조를 가지는 경우 성능 오버헤드가 발생할 수 있으므로 조심해서 사용해야 한다.
구조체 관련 함수 정리
접근 함수
const Type& field_name()
- 읽기 전용 참조 반환
- 필드가 없으면 기본값 반환
Type* mutable_field_name()
- 수정 가능한 포인터 반환
- 필드가 없으면 새로 생성해서 반환
소유권 관리 함수
void set_allocated_field_name(Type* ptr)
- 포인터의 소유권을 protobuf 객체로 이전
- 기존 값은 자동 해제
- nullptr 전달 시 필드 클리어
Type* release_field_name()
- 소유권을 호출자로 이전하며 포인터 반환
- 원본 객체에서는 해당 필드가 제거됨
- 호출자가 메모리 해제 책임
복사 함수
void CopyFrom(const Type& other)
- 깊은 복사 수행
- 모든 중첩 데이터까지 완전히 새로 복사
- 복사 후 두 객체는 완전히 독립적
void MergeFrom(const Type& other)
- 기존 데이터 유지하며 선택적 병합
상태 확인 함수
bool has_field_name()
- 필드가 설정되어 있는지 확인
void clear_field_name()
- 필드를 기본 상태로 클리어
repeated 필드 관련 함수
- repeated는 표준의 벡터와 같은 포지션인데, 벡터가 idx/iterator 이 두 가지 방식으로 데이터에 접근할 수 있었던 것처럼, repeated도 idx/iterator 두 가지 방식으로 접근할 수 있도록 함수를 지원하고 있다.
- 간단하게나마 정리하면 아래와 같다.
// Protocol.proto file
message S_LOGIN
{
bool success = 1;
repeated PlayerInfo players = 2; // 아이디 발급 전
}
// code
Protocol::S_LOGIN pkt;
// add_players(): push_back()같은 효과 + 추가한 위치 반환.
Protocol::PlayerInfo *info = pkt.add_players();
// players_size(): size()와 같은 함수
int32 size = pkt.players_size();
// idx 기반 순회 방식.
for (int i = 0; i < pkt.players_size(); i++)
{
Protocol::PlayerInfo player = pkt.players(i);
int x = player.x();
}
// iterator 기반 순회 방식
for (auto player : pkt.players())
{
int x = player.x();
}