티스토리 뷰

Application/C/C++

아, C++이었지

알 수 없는 사용자 2007. 1. 1. 11:40

아, C++이었지



 


hey의 그림


| |

얼마전의 일입니다. 직장 동료 하나가 엑셀 로더를 작성하고 있었습니다. 엑셀 로더는 익스포터와 한 쌍으로 동작하는데, 기획자가 익숙하게 사용하는 엑셀로 데이터를 작성하면 일단 XML로 내보낸 다음 실제 프로그램은 이 XML을 로더를 통해 읽어들이는 형태입니다. 엑셀에는 VBS와 노트 등 우리가 관심없는 내용이 덕지덕지 붙어있으니 가볍게 사용하기에는 XML로 내보내 사용하는 것이 편하죠. 특히나 XML은 리비전 관리와 비교가 쉬운 장점도 있습니다.


새로운 엑셀 파일을 익스포터에 끌어다 놓으면 자동으로 내보낼 컬럼이 지정되고, 필요없는 컬럼을 제거하거나 컬럼의 자료형을 지정해 주어야 합니다. 기존에는 문자열, 정수, 실수, 불린 자료형을 지원하고 있었습니다. 나는 그에게 새로운 타입으로 enum을 지원하게 해달라고 했습니다. 기획자가 문자열로 enum 상수를 입력하면 엑셀 로더에서 문자열과 기 입력된 Enumeration 정보를 참조해 숫자로 변환해 주는 변수형입니다. 기획자는 글자로 프로그래머는 상수로 쓸 수 있으니 실수도 줄고 편하겠죠. 대신 로딩하기 전에 Enumeration 객체를 로더에 넘겨주어야 하는데, 인터페이스를 다음처럼 디자인 했습니다.
class Enumeration
{
public:
bool addEnum( int, const char* ); ///< Enum 등록
const char* convertEnumToString( int ); ///< Enum을 문자열로 변환
int convertStringToEnum( const char* ); ///< 문자열을 Enum으로 변환
};

그리고 enum 상수가 추가되거나 변경될 때 문자열을 일일이 등록할 필요가 없도록 하기 위해 매크로 함수를 적절히 사용하는 유명한 트릭도 함께 사용하기로 결정했습니다.

EnumOptionType.h


AUTO_ENUMERATION( eOPTION_ZERO )
AUTO_ENUMERATION( eOPTION_ONE )
AUTO_ENUMERATION( eOPTION_TWO )
AUTO_ENUMERATION( eOPTION_THREE )

위 모양대로 헤더 파일을 등록시켜 놓으면,
#define AUTO_ENUMERATION(o) o,
enum EOPTION
{
#include "EnumOptionType.h"
};
#undef AUTO_ENUMERATION

Enum 등록과,
#define AUTO_ENUMERATION(o) #o,
const char* enumerations={
#include "EnumOptionType.h"
NULL
};
#undef AUTO_ENUMERATION

문자열 정의를 한 번에 하게 됩니다.

그런데 기획자가 작성한 문자열이 코드상에는 없는 enum이라면 로딩 시점에 가서야 이걸 알 수 있습니다. XML로 내보내는 단계에서 검증하는 방법은 없을까요? 좋아, 그럼, "내보낼 때 svn 웹 뷰어에서 최신 소스를 받아와서 한 차례 검증해 주세요".

문제가 발생했습니다. 동료의 표정이 좋지 않습니다. 무슨 일일까요? 그냥 제가 알려준 주소에서 내용을 받아와서 정규표현식으로 거른 다음 문자열 비교만 하면 될텐데. 내보낼 때야 시간이 좀 걸려도 누가 뭐라 하지 않을텐데 말이에요.

아, 그렇죠, 우리 프로젝트 전체는 C++로 짜여진 코드 덩어리입니다. 파이썬 류의 스크립트 구현만 떠올리고 단순하게 생각했던 거에요! 조엘 29에서 지나가면서 본 기억이 있거든요. 루비로 작성된, 소스 파일을 가져오는 코드입니다.
require 'net/http'
Net::HTTP.start('hailydaily.net', 80) do |http| print( http.get('/sources/EnumOptionType.h').body) end

이 일을 파이썬으로 하면 어떻게 될까요? 제 사정을 듣고 나이누옹이 알려주었는데, 코드 조각을 잃어버리는 바람에 새로 적었습니다.
import urllib, re
def getHeader(url):
return urllib.urlopen(url).read()

def getEnums(string):
pattern='AUTO_ENUMERATION( *([a-z].[0-9a-z_]+) *)'
return re.compile(pattern, re.IGNORECASE).findall(string)

헤더 파일을 가져오는 것 한 줄, 내친김에 정규식 처리에 두 줄이 소요되었습니다. 억지로 줄인 모양새도 아니라 보기도 좋습니다. 물론 예외 처리를 하면 더 길어지겠지만요. 루비의 정규식 처리는 아직 안 봐서 잘 모르겠습니다. 하는 김에 PHP로도 작성해 볼까요?
<?
function getHeader($url)
{

$fp=fopen($url, "r");
return fread($fp, 1024);
}

function getEnums($contents)
{
$enums=array();
preg_match_all("/AUTO_ENUMERATION( *([a-z].[0-9a-z_]+) *)/i", $contents, $out);
for ($i=0; $i < count($out[0]); $i++) {
array_push($enums, $out[1][$i]);
}
return $enums;
}

?>

좀 길지만 나쁘지 않습니다.

자, 두려운데요. C++에서 이 작업을 하려면 어떻게 해야합니까? libcurl이라는 라이브러리가 있습니다. 예제를 참조해 작성해 보겠습니다. MS VS 2003에서 프로젝트를 만들고 다음의 코드를 작성해 넣었습니다.
#include "stdafx.h"
#include <curl/curl.h>

struct HttpResult {
HttpResult(size_t recvKb=0)
{
if (recvKb == 0) {
recvKb = 1024 * 1;
}
size = 0;
maxSize = 1024 * recvKb;
contents = new char[maxSize];
memset(contents, 0, maxSize);
}

~HttpResult()
{
delete contents;
}

char* contents;
size_t size;
size_t maxSize;
};

size_t writeMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data)
{
HttpResult* result = (HttpResult*)data;
size_t totalSize = size * nmemb;

size_t resultSize = result->size;
if (resultSize + size < result->maxSize) {
memcpy(result->contents + resultSize, ptr, totalSize);
result->size += totalSize;
return totalSize;
}
return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
CURL* curl;
CURLcode res;

HttpResult result;

curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, "http://hailydaily.net/sources/EnumOptionType.h");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);

res = curl_easy_perform(curl);

curl_easy_cleanup(curl);
}

printf("%sn", result.contents);

getchar();
return 0;
}

휴, 됐습니다. 그나마 미리 잡아둔 버퍼 크기를 넘어가면 받질 못하는군요. C++이니만큼 std::string을 사용하도록 바꿔보겠습니다.
#include "stdafx.h"
#include <curl/curl.h>
#include <string>

using std::string;

size_t writeMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data)
{
string* result = (string*)data;
result->insert(result->size(), (char*)ptr);
return size * nmemb;
}

int _tmain(int argc, _TCHAR* argv[])
{
CURL* handle = curl_easy_init();
if (handle) {
string result;

curl_easy_setopt(handle, CURLOPT_URL, "http://hailydaily.net/sources/EnumOptionType.h");
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &result);
curl_easy_perform(handle);
curl_easy_cleanup(handle);

printf("%sn", result.c_str());
}

getchar();
return 0;
}

이제 좀 단순해졌습니다. 다른 스크립트 언어의 코드처럼 단순하고 아름답진 않지만 그렇다고 엉망으로 복잡하지도 않습니다. 이걸로 저 자신도 가지고 있었던 분명한 두려움은 지나친 것이라는 걸 알게 되었습니다. 필요한 문자열 처리를 추가해서 동료에게 알려주어야겠어요.