안녕하세요, 오늘은 제가 최근에 작업한 프로젝트를 바탕으로 zstd 라이브러리를 이용한 압축 방법을 예제와 함께 공유하려고 합니다.
요즘 제가 주식 틱체결 데이터를 수집 중인데요, 체결틱, 호가틱 등 하루치 데이터가 무려 종목당 수십MB까지 커지다 보니 이걸 그냥 저장하면 디스크 공간을 만만치 않게 차지하더라고요.
그런데 압축을 해보니까 용량이 단 10% 수준으로 줄어드는 겁니다.
이렇게 용량을 줄이면 데이터 관리 비용을 상당히 절약할 수 있겠죠?
하지만 매번 윈도우 탐색기에서 압축을 하는 게 여간 번거로운 일이 아닙니다.
그래서 "데이터 저장 단계에서 바로 압축하여 저장하면 어떨까?"라는 생각을 하게 되었고,
그 결과 선택한 라이브러리가 바로 zstd였습니다.
막상 zstd를 쓰려고 보니, 유명한 라이브러리임에도 불구하고 정작 실제 MFC 프로젝트에서 구체적으로 적용한 사례나 친절한 예제코드는 거의 없더라고요.
대부분의 포스팅이 공식 홈페이지 설명이나 벤치마크만 가져와서 있어 보이기만 하는 글들이 많았습니다.
결국 저는 직접 소스코드를 Git에서 다운로드하여 빌드한 뒤 라이브러리를 추가해, 압축과 해제를 코드로 구현했습니다.
오늘은 그 핵심 내용을 쉽게 정리해서 공유해 드리려고 합니다.
1. zstd란 무엇인가? 왜 zstd를 선택했나?
zstd (Zstandard)는 페이스북에서 개발한 오픈소스 압축 라이브러리로, 뛰어난 속도와 압축률의 균형을 갖춘 것으로 유명합니다. 사실 파일 압축 포맷은 Zip, Tar.gz, 7z, LZ4 등 다양합니다. 그럼에도 제가 zstd를 선택한 이유는 다음과 같습니다.
- 압축 해제 속도가 뛰어나다: 데이터를 압축할 때 걸리는 시간보다, 나중에 데이터를 사용할 때 압축을 해제하는 속도가 훨씬 중요합니다. zstd는 압축 해제 속도가 매우 빨라 효율적입니다.
- 스트리밍 압축 지원: 스트리밍 압축 방식을 지원하여 메모리 사용량을 줄이고 임시 데이터 저장 공간도 효율적으로 사용할 수 있습니다.
- 압축률과 속도를 자유롭게 조정 가능: 압축 속도와 압축률 사이에서 1~22 단계까지 자유롭게 선택할 수 있어 원하는 상황에 맞게 최적의 성능을 구현할 수 있습니다.
특히 제가 사용하는 환경, 저장된 데이터 기반으로 시뮬레이션과 백테스팅을 돌리는 환경에서는 압축 해제 속도가 중요했기 때문에, zstd가 가장 적합한 선택이었습니다.
2. zstd 소스코드 빌드 및 MFC 프로젝트 적용법
우선, zstd 소스코드를 Git에서 다운받아 빌드한 뒤, 헤더파일과 생성된 라이브러리파일을 MFC 프로젝트에 추가해 사용합니다. Debug 버전과 Release 버전 각각 빌드하여 프로젝트에 추가합니다.
2.1. zstd 소스코드 다운로드
다음의 주소에서 소스코드를 다운받습니다.
https://github.com/facebook/zstd.git
GitHub - facebook/zstd: Zstandard - Fast real-time compression algorithm
Zstandard - Fast real-time compression algorithm. Contribute to facebook/zstd development by creating an account on GitHub.
github.com
PC에 git 클라이언트가 설치되어 있으면 다음 명령어로 다운로드 가능합니다.
"git clone https://github.com/facebook/zstd.git"
2.2. zstd 소스코드 빌드
MFC 프로젝트에 적용하기 위해서는 visual studio 용 빌드 스크립트를 실행하면 됩니다.
"zstd\build\VS_scripts"경로에 있습니다. 빌드 스크립트 버전이 VS2010부터 VS2017까지만 있는데 저는 VS2022에서 사용할 것이므로 "build.generic.cmd" 스크립트에 다음과 같은 파라미터를 전달하여 빌드합니다.
파라미터는 "<VS버전> <아키텍처> <빌드모드> <컴파일러버전>"입니다. 이 스크립트가 실행되면 시스템에 설치된 VS 컴파일러의 경로를 찾아내서 빌드합니다.
// x64, Debug for VS2022
// zstd\build\VS_scripts\bin/Debug/x64/ 경로에 생성
> build.generic.cmd VS2022 x64 Debug V143
// x64, Release for VS2022
// zstd\build\VS_scripts\bin/Release/x64/ 경로에 생성
> build.generic.cmd VS2022 x64 Release V143
// Win32, Debug for VS2022
> build.generic.cmd VS2022 Win32 Debug V143
※ CRT 옵션(런타임 라이브러리 옵션) 유의사항
프로젝트의 CRT 옵션(프로젝트 속성>C/C++>Code Generation>Runtime Library)에 따라 LNK4098 경고가 발생할 수 있습니다. 그 이유는 프로젝트의 라이브러리 설정과 zstd 라이브러리가 빌드될 때 사용된 CRT 설정이 서로 다를 수 있기 때문입니다. zstd의 Debug 모드 RuntimeLibrary 속성값은 /MDd에 해당하는 "MultiThreadedDebugDLL"이고 Release 모드의 RuntimeLibrary 속성값은 /MTd에 해당하는 "MultiThreaded"입니다. 저는 Visual Studio가 설치되어 있지 않은 환경에서도 제 프로그램 실행파일이 동작하도록 Debug/Release 모두 "/MTd 및 /MT"로 설정해두었기 때문에 Debug 모드에서 LNK4098 경고 발생을 피하려면 zstd의 "libzstd.vcxproj"의 RuntimeLibrary 속성값을 "MultiThreadedDebugDLL"에서 "MultiThreadedDebug"로 변경합니다. Visual Studio Native Command Prompt 에서 "dumpbin"을 했을 때 "/DEFAULTLIB:MSVCRTD" 값이 아닌 "/DEFAULTLIB:LIBCMTD"라고 나오면 MT 모드로 빌드가 된 것입니다.
2.3. MFC 프로젝트 적용
다음과 같이 라이브러리 파일과 헤더파일 3개(zstd.h, zstd_errors.h, zdict.h)를 프로젝트 소스코드에 포함시킵니다.
그리고 프로젝트 속성창을 띄워서 링커 옵션을 수정합니다.
링커 옵션에 새로운 zstd 라이브러리 파일 경로를 추가합니다. Debug/Release 모두 경로가 같으므로 "Configuration"은 "All Configuration"으로 두고 "Platform"은 "x64" 확인합니다.
Linker>General>Additional Library Directories 항목에 새로운 zstd 경로를 추가합니다.
이제 링킹 단계에서 사용할 라이브러리 파일명을 적어줘야 하는데 Debug/Release 에 사용할 파일이 다르므로 이에 유의합니다.
프로젝트 설정이 완료되었으면 라이브러리를 사용할 준비가 되었는지 테스트가 필요합니다.
zstd API를 사용하는 헬로월드 수준의 코드를 작성 후 프로젝트를 빌드/실행하여 오류가 발생하는지 확인합니다.
#include "prebuilt/zstd/include/zstd.h"
#include "prebuilt/zstd/include/zstd_errors.h"
#include "prebuilt/zstd/include/zdict.h"
BOOL CNoyecubeApp::InitInstance() {
ZSTD_CCtx* cctx = ZSTD_createCCtx();
if (cctx == NULL) {
printf("ZSTD_CCtx 생성 실패!\n");
return 1;
}
printf("ZSTD_CCtx 생성 성공!\n");
printf("ZSTD Library Version: %d.%d.%d\n",
ZSTD_VERSION_MAJOR, ZSTD_VERSION_MINOR, ZSTD_VERSION_RELEASE);
ZSTD_freeCCtx(cctx);
return TRUE;
}
3. zstd Compression/Decompression - HowTo
zstd는 Simple API와 Streaming API를 지원합니다. Simple API는 주어진 입력 버퍼 전체를 하나의 완전한 압축 프레임으로 처리합니다. 이 프레임에는 압축 헤더와 블록 관리 정보가 포함되므로, 만약 100MB의 데이터를 10MB씩 잘라서 압축하면, 각각의 10MB 블록이 독립적인 압축 프레임이 되어 각 프레임마다 헤더가 기록됩니다. 따라서 전체 100MB 데이터를 한 번에 압축한다면, 하나의 압축 프레임(헤더가 단 한번 기록됨)이 생성되고, 10MB씩 독립적으로 압축하면 10개의 별도의 프레임이 생성되므로 10개의 헤더가 하나의 파일에 기록됩니다.
메모리 여유가 있다면 100MB 전체 데이터를 한 번에 Simple API로 압축하는 것이 헤더 오버헤드를 줄이고 압축 효율을 높일 수 있습니다. 만약 메모리 제한이나 실시간 데이터 수집 등의 이유로 10MB 단위로 데이터를 모아서 압축해야 한다면, Simple API를 여러번 호출하게 되는데 이 경우 각 10MB 청크마다 독립적 압축 프레임이 생성되고 헤더가 중복 기록됩니다.
Streaming API는 데이터를 점진적으로 입력받아 하나의 압축 프레임으로 처리할 수 있도록 설계되어 있습니다. 10MB 단위로 데이터를 모으더라도, Streaming API를 사용하면 내부적으로 계속 데이터를 누적하면서 단 하나의 압축 프레임(헤더가 한번만 작성됨)으로 출력할 수 있습니다.
압축코드를 작성하기전, plain text(CSV)로 저장하던 기존 코드 먼저 간략하게 살펴보겠습니다. 주식 체결데이터를 저장하는 부분을 예로 들겠습니다. 제 프로그램의 구조는 수신부에서 텍스트기반의 체결 데이터를 모은뒤 정수형, 실수형 타입의 바이너리 형태로 메모리에 레코드 단위로 저장했다가 파일에 CSV 또는 Parquet 포맷으로 저장합니다.
파일에 저장할 때에는 zstd의 단순압축 API를 사용하고, 향후 임시 버퍼로 사용하는 메모리 공간의 절약을 위해 스트리밍 API를 적용할 예정입니다.
/* 주식체결 샘플
* vProcessMsgRealData:70][22548] RealType(0) sRealKey(012450) FidCnt(25) 체결시간 (100045) 현재가 (-277500) 전일대비 (-1500)
등락율 (-0.54) 최우선매도호가(-277500) 최우선매수호가 (-277000) 체결량 (+3) 누적체결량(307074) 누적거래대금(84927) 시가( 279000) 고가(+281000) 저가(-270000)
전일대비기호 (5) 전일거래량대비(-422004) 거래대금증감 (-120913983500)
전일거래량대비(비율)(-42.12) 거래회전율 (0.67) 거래비용(584) 체결강도(96.10) 시가총액(억)(126488) 장구분(2) KO접근도(0) 상한가발생시간(000000) 하한가발생시간 (000000)
전일동시간거래량비율 (10218)
*/
((CKiwoomRealMsgNode*)pNode)->m_ullSeqNo = ++(p->_m_ullSeqNo);
((CKiwoomRealMsgNode*)pNode)->m_sRealKey = sRealKey;
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(0, _T("100045"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(1, _T("-277500"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(2, _T("-1500"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(3, _T("-0.54"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(4, _T("-277500"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(5, _T("-277000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(6, _T("+3"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(7, _T("307074"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(8, _T("84927"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(9, _T(" 279000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(10, _T("+281000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(11, _T("-270000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(12, _T("5"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(13, _T("-422004"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(14, _T("-120913983500"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(15, _T("-42.12"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(16, _T("0.67"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(17, _T("584"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(18, _T("96.10"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(19, _T("126488"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(20, _T("2"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(21, _T("0"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(22, _T("000000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(23, _T("000000"));
((CKiwoomRealMsgNode*)pNode)->m_arCString.SetAt(24, _T("10218"));
ptheDlgKiwoomMsg->vPushMsgPump(pNode);
////////////////////// 이하 파일 저장 부분 /////////////////////
CFile* pcfp = nullptr;
CArchive* pcarpCsv = nullptr;
/* Open file for CSV */
if (TRUE == pWp->boFlushToCSV) {
cstrFileFullPath.Format(_T("%s\\%s.csv"), szFinalPath, cstrFileName);
DPRINTF("[INFO] Fullpath (%s)\n", (LPCTSTR)cstrFileFullPath);
pcfp = new CFile();
boRet = pcfp->Open(cstrFileFullPath,
CFile::modeWrite | CFile::modeCreate | CFile::shareDenyNone); // delete old file if exist with same file name
if (FALSE == boRet) {
DPRINTF("[ERR]: Fail to create file(%s)\n", (LPCTSTR)cstrFileFullPath);
delete pcfp;
pcfp = nullptr;
}
/* Check CFile Pointer */
if (pcfp) {
pcarpCsv = new CArchive(pcfp, CArchive::store, 4096 * 10);
if (!pcarpCsv) {
if (nullptr != pcfp) { pcfp->Flush(); pcfp->Abort(); delete pcfp; pcfp = nullptr; }
}
}
else {
pcarpCsv = nullptr;
}
}
///// 생략 //////
if (cstrCsvBuf.GetLength() > 0) {
cstrCsvBuf.SetAt(cstrCsvBuf.GetLength() - 1, _T('\0'));
if (pcarpCsv) { // FIXME: optimization needed?
pcarpCsv->WriteString(cstrCsvBuf);
}
}
///// 생략 /////
/* Flush buffer & Close Archive & File */
if (nullptr != pcarpCsv) { pcarpCsv->Flush(); pcarpCsv->Abort(); delete pcarpCsv; pcarpCsv = nullptr; }
if (nullptr != pcfp) { pcfp->Flush(); pcfp->Abort(); delete pcfp; pcfp = nullptr; }
기존 코드에서는 CFile과 CArchive를 활용하여 체결데이터를 레코드 단위로 파일에 저장하였습니다.
압축효율을 높이려면 압축대상이 되는 단위 버퍼에 중복되는 내용이 많아야 하는데 버퍼 사이즈가 작으면 압축효율이 떨어집니다. 따라서 기존과 같이 레코드 단위로 압축을 진행하게 되면 CPU 자원소모는 늘어나고 압축률은 떨어지게 되므로 파일 출력 내용을 메모리에 버퍼링을 해뒀다가 일정 수준에 도달했을 때 압축을 진행하는 것이 좋습니다.
3.1. Simple Compression/Decompression
Simple 방식은 너무 간단하여 필요한분 댓글 남겨주시면 적겠습니다.
3.2. Streaming Compression/Decompression
주식전종목 2500여개의 체결데이터, 호가데이터 등의 압축작업은 수천번 수행되어야 하므로 ZSTD_CCtx 객체와 input, output 버퍼는 스레드 단위별로 재활용하는것이 좋습니다. 그래서 다음과 같은 구조체를 정의하고 각 스레드(IOCP Worker)별로 객체를 생성하고 초기화 하였습니다. ZSTD_CStreamInSize()와 ZSTD_CStreamOutSize()로 ZSTD가 추천하는 버퍼 사이즈를 얻을 수 있으며 확인 결과 약 128KB 정도의 크기였습니다. 버퍼크기는 성능관점에서 거거익선이기 때문에 저는 이 기본사이즈 10배수의 버퍼를 할당하였습니다. ZSTD 예제코드에는 모든 API 호출시 에러가 발생하면 프로그램을 종료시키는데 실제 개발하는 수집 프로그램에서는 그렇게 하면 안되기 때문에 초기화 실패시에만 프로그램을 종료하고 나머지 경우에는 경고 출력 및 로그에 사유를 남기고 계속 동작하도록 하였습니다.
그리고 ZSTD_CCtx 객체를 재사용하기 위해서는 ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only); 을 새로운 세션 시작전에 호출해줘야 합니다. 세션만 리셋하기 때문에 옵션으로 설정한 압축레벨과 압축파일에 체크섬값 포함 여부는 삭제되지 않습니다.
typedef struct _COMP_KEY_FLUSH_REALDATA_CACHE_TO_FILE_S {
DWORD dwIocpTrBytes;
void* const buffIn;
void* const buffOut;
size_t const buffInSize;
size_t const buffOutSize;
size_t buffInUsedSizeBytes;
ZSTD_CCtx* cctx;
_COMP_KEY_FLUSH_REALDATA_CACHE_TO_FILE_S() :buffIn(nullptr), buffOut(nullptr), buffInSize(0), buffOutSize(0) {
dwIocpTrBytes = 0;
buffInUsedSizeBytes = 0;
cctx = nullptr;
}
void zstdResetSession(void) {
buffInUsedSizeBytes = 0;
if (cctx) {
ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only);
}
}
} TST_LPARAM_COMP_KEY_FLUSH_REALDATA_CACHE_TO_FILE, * PTST_LPARAM_COMP_KEY_FLUSH_REALDATA_CACHE_TO_FILE;
/////// 스레드 초기화 코드 부분(스레드 루프 진입 직전) ///////
size_t r;
KIWOOM::TST_LPARAM_COMP_KEY_FLUSH_REALDATA_CACHE_TO_FILE stLp;
SetEvent((HANDLE)(pParams->lpOpaque_3)); // send parameter copy completed signal to thread creator
/* [START] Create the input and output buffers and zstd context for CSV Compression */
(size_t)(stLp.buffInSize) = ZSTD_CStreamInSize() * 10; /* can always read one full block */
(size_t)(stLp.buffOutSize) = ZSTD_CStreamOutSize() * 10; /* can always read one full block */
(void*)(stLp.buffIn) = malloc(stLp.buffInSize);
if (nullptr == stLp.buffIn) {
DPRINTF("[ERROR] malloc fail. Program Exit.\n");
ExitProcess(EXIT_FAILURE);
}
(void*)(stLp.buffOut) = malloc(stLp.buffOutSize);
if (nullptr == stLp.buffOut) {
DPRINTF("[ERROR] malloc fail. Exit Program.\n");
ExitProcess(EXIT_FAILURE);
}
(ZSTD_CCtx*)stLp.cctx = ZSTD_createCCtx();
if (nullptr == stLp.cctx) {
DPRINTF("[ERROR] ZSTD_createCCtx fail. Exit Program.\n");
ExitProcess(EXIT_FAILURE);
}
r = ZSTD_CCtx_setParameter(stLp.cctx, ZSTD_c_compressionLevel, ZSTD_CLEVEL_DEFAULT); /* Set compression parameters according to pre-defined cLevel table. */
if (ZSTD_isError(r)) {
DPRINTF("[WARN] ZSTD_CCtx_setParameter fail ZSTD_c_compressionLevel.\n");
}
r = ZSTD_CCtx_setParameter(stLp.cctx, ZSTD_c_checksumFlag, TRUE); /* A 32-bits checksum of content is written at end of frame (default:FALSE) */
if (ZSTD_isError(r)) {
DPRINTF("[WARN] ZSTD_CCtx_setParameter fail ZSTD_c_checksumFlag.\n");
}
DPRINTF("[INFO] ZSTD buffInSize(%zd) buffOutSize(%zd) for IOCP thread(%d)\n", stLp.buffInSize, stLp.buffOutSize, GetCurrentThreadId());
/* [END] Create the input and output buffers and zstd context for CSV Compression */
프로그램상에 CSV 포맷의 레코드를 압축 할 것인지 말것인지를 결정할 수 있도록 했는데
압축모드일 경우에는 파일명의 확장자를 ".zst"로 바꾸고 CFile 오픈시 CFile::typeBinary 모드를 추가하였습니다.
if (TRUE == pWp->boCSV_Compress) { /* CFile Open with typeBinary, no CArchive */
cstrFileFullPath.Format(_T("%s\\%s.csv.zst"), szFinalPath, cstrFileName);
DPRINTF("[INFO] Fullpath for compressed CSV (%s)\n", (LPCTSTR)cstrFileFullPath);
pcfp = new CFile();
boRet = pcfp->Open(cstrFileFullPath,
CFile::modeWrite | CFile::modeCreate | CFile::shareDenyNone | CFile::typeBinary); // delete old file if exist with same file name
}
else { /* pain text mode. CFile Open , connect with CArchive */
cstrFileFullPath.Format(_T("%s\\%s.csv"), szFinalPath, cstrFileName);
DPRINTF("[INFO] Fullpath (%s)\n", (LPCTSTR)cstrFileFullPath);
pcfp = new CFile();
boRet = pcfp->Open(cstrFileFullPath,
CFile::modeWrite | CFile::modeCreate | CFile::shareDenyNone); // delete old file if exist with same file name
}
CFile Open이 성공했으면 버퍼링 저장이 가능하도록 CArchive 객체에 연결합니다. 이 때 모드는 CArchive::store이며 버퍼사이즈는 기본 4KB가 아닌 ZSTD의 output 버퍼 사이즈 + 4KB로 하였습니다. CString cstrCsvBuf에 레코드 단위의 데이터가 담겨 있습니다. 압축모드가 아닐 경우에는 문자열 맨 끝의 ',' 문자를 '\0'로 교체 후 문자열 기록을 하고, 압축모드일 경우에는 문자열 맨 끝의 ',' 직전까지의 사이즈를 계산하여 버퍼에 메모리 복사를 수행합니다. 다음의 코드는 CSV 파일의 첫 행을 기록하는 코드입니다. pLp->zstdResetSession();이 호출됨에 유의해주세요.
앞서 말한 ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only);가 호출됩니다.
/* Check CFile Pointer, Open CArchive*/
if (pcfp) {
pcarpCsv = new CArchive(pcfp, CArchive::store, (int)(pLp->buffOutSize + 4096));
if (!pcarpCsv) {
if (nullptr != pcfp) { pcfp->Flush(); pcfp->Abort(); delete pcfp; pcfp = nullptr; }
}
else { // everything successfully opened for CSV
iLen = cstrCsvBuf.GetLength();
if (TRUE == pWp->boCSV_Compress) {
pLp->zstdResetSession(); // re-use zstd resource
if (iLen > 0) {
sizeToWrite = (iLen - 1) * sizeof(TCHAR); // (iLen - 1) : remove last tail char ','
pLp->buffInUsedSizeBytes = sizeToWrite; // init buffInUsedSizeBytes with sizeToWrite
CopyMemory(pLp->buffIn, (LPCTSTR)cstrCsvBuf, sizeToWrite);
}
}
else {
if (iLen > 0) {
cstrCsvBuf.SetAt(iLen - 1, _T('\0'));
pcarpCsv->WriteString(cstrCsvBuf);
}
}
}
}
스트리밍 압축모드에서는 input 버퍼에 더 이상 레코드 단위의 데이터를 복사할 수 없을 때 실제 압축을 수행 후 파일에 기록합니다. 다음 코드에서 buffInUsedSizeBytes가 실제 input 버퍼의 사용량을 관리하는 변수이며, 압축 수행 후 파일에 기록이 완료되면 이 값을 0으로 초기화 해줘야 input 버퍼를 처음부터 기록할 수 있습니다.
압축을 진행하기전 ZSTD_inBuffer와 ZSTD_outBuffer를 초기화합니다. 각 객체의 pos 값은 0으로 설정하며 inBuffer의 사이즈는 inBuffer에 전체 사이즈가 아닌 유효 데이터가 위치한 offset, outBuffer의 사이즈는 outBuffer의 전체 사이즈를 입력합니다. 그리고나서 ZSTD_compressStream2를 호출하여 inBuffer의 내용을 outBuffer로 압축저장합니다. 사양서에 따라 오류가 발생할 수 있기 때문에 리턴값은 ZSTD_isError()로 확인해주고 에러가 아닐 경우 실제 파일 버퍼에 기록합니다. 스트리밍 압축이기 때문에(끝이 어디인지 모르기 때문에) ZSTD_e_continue 모드로 호출합니다. inBuffer를 다 읽지 못했으면 이 과정을 계속 반복해야 하고 pos 값이 size에 도달했을 때 압축 루프를 탈출합니다.
ZSTD_inBuffer stZstdInObj;
ZSTD_outBuffer stZstdOutObj;
iLen = cstrCsvBuf.GetLength();
if (iLen > 0) {
if (TRUE == pWp->boCSV_Compress) {
if (iLen < (pLp->buffInSize - pLp->buffInUsedSizeBytes)) {
sizeToWrite = (iLen - 1) * sizeof(TCHAR); // (iLen - 1) : remove last tail char ','
CopyMemory((char*)pLp->buffIn + pLp->buffInUsedSizeBytes, (LPCTSTR)cstrCsvBuf, sizeToWrite);
pLp->buffInUsedSizeBytes += sizeToWrite; //
}
else { // need to flush
stZstdInObj.src = pLp->buffIn; stZstdInObj.size = pLp->buffInUsedSizeBytes; stZstdInObj.pos = 0;
do {
stZstdOutObj.dst = pLp->buffOut; stZstdOutObj.size = pLp->buffOutSize; stZstdOutObj.pos = 0;
sizeRemaining = ZSTD_compressStream2(pLp->cctx, &stZstdOutObj, &stZstdInObj, ZSTD_e_continue);
if (ZSTD_isError(sizeRemaining)) {
DPRINTF("[ERROR] ZSTD_compressStream2 (%zd)(%s).\n", sizeRemaining, ZSTD_getErrorName(sizeRemaining));
/* TODO: Report this at Logfile */
break; // escape loop anyway
}
else {
pcarpCsv->Write(pLp->buffOut, (UINT)(stZstdOutObj.pos));
}
if (stZstdInObj.pos == stZstdInObj.size) {
break;
}
} while (TRUE);
pLp->buffInUsedSizeBytes = 0; // input buffer flushed. reset used offset.
}
}
else {
cstrCsvBuf.SetAt(iLen - 1, _T('\0')); // mark end of string by '\0'
pcarpCsv->WriteString(cstrCsvBuf);
}
}
마지막으로 압축파일을 닫는 과정입니다. 나머지 데이터가 있건 없건 ZSTD_e_end 플래그로 ZSTD_compressStream2를 호출하면 압축파일의 끝이 정상적으로 마무리됩니다.
/* [START] Flush the remaining data of stZstdInObj in ZSTD_e_end mode. */
if (TRUE == pWp->boCSV_Compress) {
stZstdInObj.src = pLp->buffIn; stZstdInObj.size = pLp->buffInUsedSizeBytes; stZstdInObj.pos = 0;
do {
stZstdOutObj.dst = pLp->buffOut; stZstdOutObj.size = pLp->buffOutSize; stZstdOutObj.pos = 0;
sizeRemaining = ZSTD_compressStream2(pLp->cctx, &stZstdOutObj, &stZstdInObj, ZSTD_e_end);
if (ZSTD_isError(sizeRemaining)) {
DPRINTF("[ERROR] ZSTD_compressStream2 (%zd)(%s).\n", sizeRemaining, ZSTD_getErrorName(sizeRemaining));
/* TODO: Report this at Logfile */
break; // escape loop anyway
}
else {
pcarpCsv->Write(pLp->buffOut, (UINT)(stZstdOutObj.pos));
}
if (0 == sizeRemaining) {
break;
}
} while (TRUE);
pLp->buffInUsedSizeBytes = 0; // input buffer flushed. reset used offset.
}
/* [END] Flush the remaining data of stZstdInObj in ZSTD_e_end mode. */
/* Flush buffer & Close Archive & File */
if (nullptr != pcarpCsv) { pcarpCsv->Flush(); pcarpCsv->Abort(); delete pcarpCsv; pcarpCsv = nullptr; }
if (nullptr != pcfp) { pcfp->Flush(); pcfp->Abort(); delete pcfp; pcfp = nullptr; }
4. 결론: 압축은 비용을 절약하는 가장 현명한 방법
압축정도를 결정하는 압축레벨 22단계 중 3단계 압축 진행시 다음과 같은 압축 결과를 얻었습니다. 원본파일의 17% 수준 용량입니다.
이렇게 압축 라이브러리를 활용하면 메모리 및 디스크 공간을 90%까지 절약할 수 있으며, 이는 결국 데이터 관리 비용 절감과 직결됩니다. 여러분도 zstd와 같은 압축 라이브러리를 활용하여 효율적인 데이터 관리를 시작해 보시는 건 어떨까요?
이번 포스팅은 이론적인 소개가 아니라 직접 프로젝트에 적용해본 실제 사례를 공유했다는 점에서 더욱 유익한 정보가 되었기를 바랍니다. 앞으로도 실질적인 개발 노하우를 공유하겠습니다. 감사합니다.
'프로그래밍 > C | C++' 카테고리의 다른 글
[MFC] Parquet 파일 생성(Apache Arrow 기반) (0) | 2025.04.15 |
---|---|
블록암호 알고리즘 모듈(LEA) 적용 및 테스트 (0) | 2024.05.10 |
[MFC] SQLite 연동 및 사용법 (0) | 2024.05.08 |
[MFC] Worker Thread 동적 관리기능 구현 (0) | 2024.04.25 |
[MFC] CMap class 사용법 예제 (0) | 2023.12.30 |