2013. 8. 5. 15:33

data serialize (데이터 직렬화) 공유


dataserializetest.zip

License : GPL V3.0 (본 소스 사용시 GPL 라이선스를 따르기 바랍니다)


하나의 object를 파일로 저정하거나, network로 통신하기 위해서는 object 형태의 구조화된 databyte 단위의 stream으로 변환해야 합니다. 다음의 class를 예를 들어 보겠습니다.

class CData

{

...

private:

...

char m_buf[1024];

int m_arrIndex[1024];

...

}


CData cData;

즉, 예를 들어 cData를 file로 저장하고, 나중에 file로 읽어들이려면 어떻게 해야 할까요? 가장 쉽게 생각할 수 있는 것은, ::WirteFile(..., &cData, sizeof(cData))라고 생각할 수 있지만, 해당 구문은 오류를 발생시킨다는 사실, 시도만 해보면 알 수 있습니다. 아니면, 다음과 같이 구조체를 만들어,...

typdef struct tagST_DATA

{

char buf[1024];

int arrIndex[1024];

} ST_DATA, *LPST_DATA;


ST_DATA stData;

::WriteFile(..., &stData, sizeof(stData));가 가장 좋은 해결방법인 것으로 보여집니다. 허나,

1. 구조체의 member가 변경되는 경우(즉, BOOL bSet[1024]가 추가되는 경우), 호환성의 유지

2. sizeof(ST_DATA) 만큼 파일, 즉, 전체 byte 크기가 커짐

와 같은 단점이 발생할 수 있습니다.

그래서 보통, 이기종간의 data 교환을 위해, xml을 일반적으로 사용하기도 합니다(최근엔 json도 포함).

<?xml version="1.0" encoding="utf-8">

<root>

<buf><![CDATA[...]]></buf>

<Index>0</Index>

<Index>5</Index>

<Index>6</Index>

...

</root>

와 같은 형태로 사용할 수 있을 것입니다. 문제는 xml이라는 무거운 개념이 들어간 것이 걱정이긴 합니다. 물론, 속도도 그렇구요.

따라서, 본 포스트에서는 다음과 같은 데이터 직렬화(data serialize) 함수를 공유하고자 합니다. 대략 사용법은 dataserializetest.cpp를 참고하시기 바랍니다.

CDataSerialize::LPST_STREAM pStream = NULL;

...

pStream = CDataSerialize::Create(0, 0, NULL, 0, FALSE);

...

CDataSerialize::SetValue(pStream, "version", "1", 1, 1);

...

CDataSerialize::SetValue(pStream, "buf", buf, length, sizeof(buf));

...

CDataSerialize::SetValue(pStream, "index", arrIndex, count, sizeof(arrIndex));

...

char* lpszVersion = CDataSerialize::GetValue(pStream, "version", &dwSize, NULL);

...

CDataSerialize::LPST_STREAM pStream2 = NULL;

...

LPVOID stream = NULL;

stream = CDataSerialize::GetStream(pStream, &dwSize);

pStream2 = CDataSerialize::Create(0, 0, stream, dwSize, FALSE);

...

CDataSerialize::Destroy(pStream);

CDataSerialize::Destroy(pStream2);

와 같습니다.

즉, (valuename, value) pair를 set/get 해주고 있습니다. 또한, allocation size를 추가 입력하도록 하여, (a, "1"), (a, "111"), (a, "111111...")과 같이 하나의 값이 계속 update될 때, 그 크기를 미리 설정하여 reallocation 횟수를 낮춰 성능을 올릴 수 있습니다. 물론, 미리 설정한 크기를 벗어나는 경우도 문제없이 저장되도록 하였습니다.


다음과 같은 함수가 지원됩니다.

LPST_STREAM Create(OPTIONAL IN DWORD dwInitialAllocSize, OPTIONAL IN DWORD dwGrowthSize, OPTIONAL IN LPVOID pAttachStream, OPTIONAL IN DWORD dwAttachStreamSize, OPTIONAL IN BOOL bMemoryShare)

리턴값 : STREAM 개체. caller는 내부 값을 들여다볼 필요는 없다. 종료시 Destroy를 호출해야 한다.

dwInitialAllocSize : 초기 stream 크기. 0 가능. 초기에 메모리를 많이 잡고 시작하면, 메모리 단편화가 줄어들 것이다. 이 값은 메모리 할당 값이지 향후 stream 길이에는 상관이 없다.

dwGrowthSize : stream 크기 증가분 단위. stream은 계속 그 크기가 커지기 마련인데, 증가분을 설정한다. 0 가능 (기본값)

pAttachStream : 이전의 stream을 가지고 stream 개체를 생성할 때 사용된다. 사용하지 않을 때 NULL 가능.

dwAttachStreamSize : pAttachStream 사용시, 해당 stream의 길이. 내부적으로 메모리에 할당될 stream의 길이를 계산하는데, 만약 dwInitialAllocSize가 작은 경우, 자동으로 dwInitialAllocSize를 증가하여 생성시켜준다.

bMemoryShare : attach 정보가 있을때 의미있으며, attach할 때 pAttachStream을 copy-on-write하지 않고 그대로 공유한다. share mode로 create된 stream에 대해서는 SetValue가 지원되지 않는다. 만일 다른 곳에서 pAttachStream 내용을 변경한다면, 변경이 반영된다. 일반적으로 network이든 file이든 stream을 가져오기 위해서 내부 buffer를 사용할 것인데, 해당 buffer를 share mode로 create하면 메모리를 절약할 수 있다.


LPVOID GetStream(IN LPST_STREAM pstStream, OUT LPDWORD pdwStreamSize)

stream 개체를 가지고 stream pointer를 구한다. 실패시 NULL을 리턴한다.

pdwStreamSize를 통해 해당 stream의 길이를 알려준다.


BOOL SetValue(IN LPST_STREAM pstStream, IN LPCSTR lpszValueName, IN LPVOID pData, IN DWORD dwDataSize, IN DWORD dwDataAllocSize)

값을 쓰는데 성공하면 TRUE, 실패하면 FALSE를 리턴한다.

만일 이전값이 저장되어 있는 경우, 이전값이 저장된 위치에 overwrite하거나, 이전값을 delete flag를 설정하고 끝에 값을 다시 추가한다. overwrite 판단은 이전 동일값의 allocation size가 요청한 allocation size보다 큰 경우에 해당된다.


pstStream : Create 호출한 리턴값

lpszValueName : value 이름. unicode가 아닌 ansi로 구성된다.

pData : 저장할 data pointer

dwDataSize : 저장할 pData의 크기

dwDataAllocSize : 데이터 할당 크기. 동일값 중복 저장이 예상되는 경우, 가능한 경우 max값을 넣어두면, 메모리 효율이 높아진다. 단, 불필요한 메모리가 많이 발생할 수 있다. 만일, 동일값 중복 저장이 발생하지 않는 경우에는 dwDataSize와 동일 값으로 전달하면 된다.


LPVOID GetValue(IN LPST_STREAM pstStream, IN LPCSTR lpszValueName, OUT LPDWORD pdwDataSize, OPITONAL OUT LPDWORD pdwDataAllocSize)

값을 읽는다. 없는 경우 NULL을 리턴한다. data가 저장된 pointer가 리턴된다.

해당 pointer는 Destroy를 호출하면 휘발되거나, 지속적인 SetValue등의 호출로 인해 dangling reference가 발생할 수 있다. 따라서, 본 함수 호출후, 계속 data를 사용해야 한다면, 리턴값을 따로 다른 메모리에 할당하여 복사한뒤 사용하길 바란다. pdwDataAllocSize는 NULL 가능한데, 해당 값이 저장된 allocation 크기를 전달받는다.


BOOL Clear(IN LPST_STREAM pstStream)

BOOL ClearAndAttach(IN LPST_STREAM pstStream, IN LPVOID pAttachStream, IN DWORD dwAttachStreamSize)

첫번째 함수는 stream을 clear한다. 단, memory allocation은 그대로 둔다. 즉, size flag등을 0으로 변경하고, ZeroMemory를 수행하지, 메모리를 free하지는 않는다. 초기 적당히 큰 size로 allocation한뒤, clear를 호출하고 Set/Get을 수행하게 되면 memory 단편화가 발생하지 않을 것이다.

두번째 함수는 clear를 수행한뒤 attach를 실행한다. attach 할 때 새로운 attach할 stream이 현재의 allocation 크기보다 작은 경우, 기존의 pstStream의 메모를 그대로 사용한다. 만일 큰 경우, 메모리를 다시 할당받아 사용한다. 이것 역시 초기 적당히 큰 size로 allocation한뒤 ClearAttach/Get등을 수행하게 되면 memory 단편화가 발생하지 않을 것이다.


Test 실행

dataserializetest.exe를 실행하면 다음과 같습니다.

[stream] world, and you?

[stream2] world, and you?

-----------------------

Addr = 0x6e2e98

[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 01 06 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 00 00 00 00 00  ......world.....

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00                                      ....

Addr = 0x6e2e98

[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 01 09 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 21 21 21 00 00  ......world!!!..

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00                                      ....

Addr = 0x6e2e98

[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 00 09 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 21 21 21 00 00  ......world!!!..

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00 77 3D 04 00 00 05 68 65 6C 6C 6F 00  ....w=....hello.

[00050] 77 01 0C 00 00 00 14 00 00 00 77 6F 72 6C 64 2C  w.........world,

[00060] 20 67 6F 6F 64 21 00 00 00 00 00 00 00 00         good!........


stream과 stream2는, attach를 통해 동일 값이 잘 전달되었는지를 테스트한 것입니다.

그리고 그 이후의 메모리 덤프는, stream의 변화를 보여주고 있습니다.

SetValue("hello", "world", 6, 10)

SetValue("abcd", "1234567890", 11, 15)

호출


[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 01 06 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 00 00 00 00 00  ......world.....

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00                                      ....


SetValue("hello", "world!!", 9, 10)

호출


[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 01 09 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 21 21 21 00 00  ......world!!!..

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00                                      ....


SetValue("hello", "world, good!", 12, 20)

호출


[00000] 77 3D 04 00 00 05 68 65 6C 6C 6F 00 77 00 09 00  w=....hello.w...

[00010] 00 00 0A 00 00 00 77 6F 72 6C 64 21 21 21 00 00  ......world!!!..

[00020] 77 54 02 00 00 04 61 62 63 64 00 77 01 0B 00 00  wT....abcd.w....

[00030] 00 0F 00 00 00 31 32 33 34 35 36 37 38 39 30 00  .....1234567890.

[00040] 00 00 00 00 77 3D 04 00 00 05 68 65 6C 6C 6F 00  ....w=....hello.

[00050] 77 01 0C 00 00 00 14 00 00 00 77 6F 72 6C 64 2C  w.........world,

[00060] 20 67 6F 6F 64 21 00 00 00 00 00 00 00 00         good!........

즉, 두번째 SetValue("hello", ...)에서는 기존 값에서 overwrite가 발생하였습니다. 그래서 stream의 크기가 변동되지 않았는데, 세번째 SetValue("hello", ...)에서는 다시 "hello"가 끝에 저장되었습니다. 그래서 총 두개의 "hello"가 존재하고 있습니다. 다만, 앞선 hello에는 01 -> 00으로 변경된 field가 보일 겁니다. 해당 flag로 인해 첫번째 hello는 사용되지 않도록 예외 처리된 것입니다.


마지막으로 아래와 같이 share mode를 테스트합니다.

Stream4 shares the memory of stream3.

Stream4 has not set 'hello' value directly,

  but has ('hello', 'world, good!'), because the memory share with stream3.

Stream3 modified 'hello' value.

Stream4 has ('hello', 'world, modified!'), because stream3 has modified it.

일단, Stream4는 Stream3의 stream memory를 share mode로 생성하는데, stream4는 명시적으로 SetValue한 적이 없지만, stream에서 이미 진행하였기 때문에 'hello' 값이 GetValue됨이 확인됩니다. 그다음 Stream3에서 'hello' 값을 수정하였는데, 이 값이 stream4에서도 그대로 적용됨이 확인됩니다.

여기에서 주의할 것은, stream3의 'hello'는 allocation size가 20으로 잡아놨기 때문에 memory re-allocation이 발생하지 않았다는 점입니다. 즉, 'world, modified!'는 20byte 이내의 크기입니다. 만일, stream3에서 'hello'에 21byte 크기의 값을 SetValue하였다면, stream3의 내부 메모리는 re-allocation(realloc)이 발생하게 되고, 메모리 pointer가 변경하게 된다면, Stream4에서 공유하는 메모리는 damaged된, 즉, dangling-reference가 발생할 수 있습니다. 따라서, 되도록 share mode인 경우, 해당 메모리에 공유하는 객체들은 되도록 read-only로 접근하는 것이 좀더 안전할 것입니다.