본문 바로가기

Network/Network Programming

[C++/Network] 객체 직렬화

 

직렬화(serialization)란 어떤 객체가 랜덤 액세스(random access) 가능한 현태로 메모리상에 존재할 때, 이를 일련의 여러 비트로 변환해 길게 나열하는 것입니다. 외부의 시스템이 이용할 수 있는 포맷으로 변환하는 작업이라고 할 수 있습니다.

 


 

직렬화

 

직렬화가 필요한 이유는, 아래와 같은 플레이어 상태에 대한 클래스 PStatus가 있다고 해보겠습니다. 이를 직렬화 없이 다른 호스트로 전송을 한다면 다음과 같은 방법으로 전송을 할 것입니다.

class PStatus {
public:
    PStatus() {
        hp = 10;
        mp = 10;
    }
private:
    int hp;
    int mp;
};

void send_obj(SOCKET s, const PStatus* ps) {
    send(s, reinterpret_cast<const char*>(ps), sizeof(*ps), 0);
}

 

이를 다른 호스트가 받으면, 수신측의 메모리 공간에서 객체가 송신자의 메모리 공간과 동일한 위치를 참조하고 있을 것이라고 보장할 수 없습니다. 그래서 위의 객체의 내부 데이터들이 엉뚱한 값으로 복제될 가능성이 높습니다. 간단한 객체라면 동일할 수 있지만, 조금씩 복잡해지기 시작하면 이상한 값들이 저장되면서 심각한 현상이 야기될 수 있습니다.

 

따라서, 포인터가 가리키거나 레퍼런스가 참조하는 데이터 전체를 동일하게 복제 전달해주거나, 해당 데이터와 동일하게 취급되는 내용을 수신자 프로세스에서 찾아 연결을 해줘야합니다.

 

이에 대한 해결방법들로는

1. 각 필드를 각각 직렬화 해서 전송

2. 필드마다 패킷을 하나씩 보내기

3. 객체 하나를 전송할 때 그 객체와 관련된 데이터를 하나의 버퍼에 모아두었다가 버퍼를 통째로 전송

 

이렇게 3가지가 있습니다.

 

1, 2 방법은 패킷마다 불필요한 헤더를 무수히 전송하기 때문에 대역폭 낭비가 발생하며 연결에 부하를 줄 수 있습니다. 그래서 보통 3번 방법을 선호하며, 이 방법은 '스트림'을 사용해 쉽게 처리할 수 있습니다.

 

스트림이란, 순서가 있는 데이터 원소의 집합을 캡슐화해 사용자가 그 데이터를 읽거나 쓸 수 있게 해주는 자료구조입니다. 스트림에 대한 자세한 정보는 여기서 다루지는 않겠습니다.

 

스트림을 이용하면 원시 자료형과 POD(plain old data)로만 구성된 자료형을 직렬화 할 수는 있지만, 포인터나 컨테이너 등의 간접 참조 데이터는 처리하지 못합니다. 이러한 데이터를 처리하기 위한 기법인 '임베딩(또는 인 라이닝)'을 알아보겠습니다.

 


 

임베딩(인 라이닝)

 

임베딩이란 독립적인 데이터를 다른 데이터 중간에 끼워 넣는 것을 말합니다. vector 컨테이너를 예시로 직렬화하는 함수를 만들어 본다면 아래와 같이 만들 수 있습니다.

template<typename T>
void Write(const std::vector<T>& inVector) {
    size_t elementCount = inVector.size();
    Write(elementCount);
    for (const T& element : inVector) {
    	Write(element);
    }
}
 
template<typename T>
void Read(std::vector<T>& outVector) {
    size_t elementCount;
    Read(elementCount);
    outVector.resize(elementCount);
    for (T & element : outVector) {
        Read(element);
    }
}

 

Write 함수에서 vector의 길이를 먼저 기록하는데, 이는 아래의 Read 함수에서 vector의 길이만큼 적절하게 미리 할당해둘 수 있기 때문입니다. 위 방법으로 원시 자료 형태로 데이터를 하나씩 주고받을 수 있습니다.

 

그런데, 데이터가 다른 객체와 공유되거나 포인터로 참조된다면 임베딩 방법이 아닌 다른 방법을 사용해야합니다.

 


 

링킹

 

데이터가 하나 이상의 포인터로 여러 곳에서 참조되는 경우에 사용할 수 있는 직렬화방식입니다. 여러번 참조가 일어날 것 같은 객체에 고유 식별자나 ID를 부여해 두었다가 이들 객체를 직렬화할 때 오로지 식별자 값만 직렬화하는 방법입니다.

 

먼저 수신자가 모든 객체 데이터를 복원한 뒤, 수정 루틴을 돌려서 각 식별자에 대응되는 참조 객체를 찾아 적절한 멤버 변수에 끼워넣습니다. 자세한 방법에 대해서는 다음에 알아볼 객체 리플리케이션에서 다루겠습니다.