리팩토링: Avoid return statement with value if you can.

영어로 제목을 쓰려던 것은 아니었으나, “Catch me if you can”을 의식하다보니, 이런 제목이 되었습니다. 사실 영어는 종종 의도를 드러내는데 유리하기도 하구요. 🙂

프로그래밍 언어에서 return문은 아주 중요한 역할을 합니다. 프로그래밍을 수학적 모델로 표현하는데 있어서, 함수라는 개념은 핵심적인 역할을 하고 있으며, 함수의 출력을 받는다는 면에서 매우 중요하지요. 하지만, return문은 독입니다. 왜냐구요? 중요한데 왜 독이냐구요? 이야기를 한번 풀어보도록 하겠습니다.

소프트웨어는 개발 기간이 길어지면, 복잡해지기 마련입니다. 처음 개발할때의 요구사항이 그대로 유지되는 경우는 없다고 보아도 무방하며, 늘어난 요구사항은 처음에 짜둔 아름다운 흐름을 망가뜨리기 시작합니다. 소프트웨어의 구조가 무너집니다. 코드는 점점 스파게티가 되어갈 것이고, 문제라도 생겨서 디버깅이라도 하려고 하면 지옥이 따로 없죠.

이런 코드들을 고통을 감내하며 Sequence Diagram을 그려보면, 왜 복잡한지 드러나는 경우가 많습니다. 클래스별로 호출이 들어갔다가 나오고, 그 결과에 따라서 새로운 분기가 발생하는 사태가 벌어지죠. 그리고, 이 분기의 수가 많아지면, Sequence Diagram은 읽기 힘든 선들의 난잡한 리좀이 되어버립니다. 들뢰즈가 극찬했던 리좀이지만, 각 클래스별의 자유로운 결합과 소통이 아닌 서로를 점점 더 구속하는 꼬인 실이 되어버리죠. 그냥 이정도면 모르겠는데, 반환값이 여러개여야 하는 경우에는 return문을 쓰지 못하고, reference나 pointer를 이용한 억지스러운 반환을 하는 경우까지 발생합니다. 점점 더 복잡해져만 갑니다.

갑작스런 이야기 전환이지만, 전투기에 탑승해서 미사일을 쏜다고 생각해봅시다. 미사일은 날아가고 있고, 그 미사일이 격추 결과를 반환하고, 그 결과에 따라 제어를 해야한다면.. 네, 생각만해도 복잡합니다. 반면에, 그냥 미사일을 쏘고 뭐가 어찌되든 알아서 해결된다고 하면, 편안하죠. fire-and-forget입니다. call-site에선 신경쓰지 않는겁니다. callee쪽에서 알아서 하겠죠 뭐.

함수를 호출하는데 있어서 그 결과값에 신경써야 한다는 것은 상당한 위험성을 내포하고 있습니다. 단순한 수학 연산이나, 문자열 검색같은 피할수 없는 반환값이라면 모르겠지만, 소프트웨어가 구현하는 Business Logic이라면, 그 결과값에 따른 논리구조의 분기는 피할 수 없습니다. 결과값이 존재한다는건, 그 결과값에 따라 무언가 다르게 하려고 했기 때문일테니까요. 그리고, 그 분기는 요구사항이 늘어나면 늘어날수록 두고두고 당신을 괴롭히겠죠.

Sequence Diagram을 생각해봅시다. return문이 없다면, 함수호출을 의미하는 화살표만 있으면 됩니다. 반환값에 따른 분기도 없을테고, 분기가 일어나는 시점은 바로 그 분기조건을 확인하는 그 순간입니다. 더욱 간단해지죠. 그래봐야 Sequence Diagram이 복잡해지는건 매한가지 아니냐? 라고 생각할 수 있겠지만, 원래 복잡한 논리구조를 표현하는데 복잡한건 당연하겠죠. 다만 그 복잡함을 따라가는데 얼마나 더 쉬운가를 이야기하는 겁니다.

이렇게 짜다 보면, 어떤 객체의 상태변화에 대해서 반응하는 코드를 구현하기가 어려워집니다. 이때 사용하는 기법이 Callback이고, Callback을 사랑하는 이유죠. Callback은 대부분의 경우, 직관적이지 못하다는 이유로 배척당하곤 하는데, 간단한 소프트웨어의 경우엔 그렇습니다. 하지만, 복잡해지면 복잡해질수록 그 진가를 발휘하곤 하죠. 특히, 복잡해지면서 유사한 상태변화가 늘어날 경우에 더더욱 심해집니다. Callback기법을 적절히 사용하고 있다면, 해당 상태변화가 감지되었을때, 그 감지에 대한 분기를 추가하는 것이 아니라, 미리 지정된 Callback을 호출하기만 하면 되니까요. 이렇게 하면, 소프트웨어의 실행흐름을 Input Layer -> Business Layer -> Reaction Layer로 나누게 됩니다. 그리고, Reaction Layer를 만들때 Callback을 활용하게 되죠.

이는 객체지향 패러다임의 원칙중 하나인 Encapsulation과도 관계가 있습니다. Encapsulation이 필요한 데이터만 노출해서 소프트웨어를 간단하게 만든다면, 이번에 제시하는 이 원칙은 대부분의 분기를 감추는 것으로 소프트웨어를 간단하게 만든다고 볼 수 있지요. 또한, Callstack을 보는 것 만으로 분기가 어떻게 일어났는지 확인할 수 있다는 장점도 있습니다. 재현가능한 버그라면 모르겠지만, 소프트웨어가 비정상종료 되면서 남긴 제한적인 Callstack정보로 디버깅을 하는 경우에는 더더욱 유리해집니다.

이게 뭔소리냐. 하실 수 있겠지만, 제목에 if you can이 붙은건 가급적이면 피하라는 이야기입니다. 반환값을 가진 return문은 결국 호출측에서의 분기를 유발하게 되고, 그 분기는 소프트웨어를 복잡하게 만들 가능성을 내포하고 있으니까요.

 

디버깅: 누가 나의 this를 옮겼을까? #1. 아이디어.

C++ 컴파일러들은 아주아주 훌륭합니다. 최적화를 켜면 이리저리 쿵짝쿵짝 뭔가 대단해보이는걸 해치워서 코드를 빠르게 만들죠. 요런류의 최적화중 중요한 한가지 기법이 특정 변수를 register를 사용하게 만드는 겁니다. 넵. 대단합니다. 멋지죠. DRAM보다는 Cache가 빠르고 Cache보다는 Register가 빠르니까요. 빠른게 최고인겁니다.

하지만, 디버깅 과정에서는 재앙입니다. 저런 최적화를 수행하게 되면.. 변수가 날아갑니다. 특히 this포인터는 무용지물이 됩니다. MSVC의 경우, this포인터는 CX레지스터를 이용하는데.. 최적화를 수행할 경우 CX레지스터를 범용 연산 레지스터로 씁니다. 즉.. CX레지스터를 다른 용도로 쓰게 되고.. this포인터는 이상한 값을 남발하죠. (보통 0인 경우가 많습니다.) 재현이 가능한 버그라면, 최적화를 끄고 재현해서 문제를 해결할 수 있겠지만, Post-mortem디버깅(크래쉬덤프를 이용한 디버깅)이나 타이밍 문제로 버그가 발생하는 동시성 문제라면 최적화를 끌수도 없습니다. 전자는 끄는게 불가능하고, 후자는 경험상 단언컨데 최적화 끄면 문제가 안생길 확률이 은근히 높습니다. OTL

가장 근본적인 방법은 disassemble된 코드를 읽어서 this포인터를 찾는 것이지만.. 사실상 힘들죠. 그 값이 유지되고 있다는 보장도 없구요. 결국은 포기하기 마련입니다. 아니면, 소설을 쓰거나요. 음… 일단 상황을 정리해보면 아래 3가지로 정리가 될겁니다.

  1. 최적화를 끄고 재현이 가능한 상황.
  2. 최적화를 끄면 재현이 안되는 상황.
  3. 재현이 불가능한 상황(Post-mortem)

1번의 경우에는 앞에서 언급한대로 그냥 최적화를 끄고 디버깅을 하면 됩니다. 문제는 2번과 3번인데, 2번은 코드를 살짝 수정해서 문제가 되는 부분의 this pointer를 전역변수로 저장해두면 됩니다. 의외로 간단하지만 쓸만하죠. 마지막, 3번이 골아픕니다.

Post-mortem 디버깅이란 프로세스가 비정상종료될때 남기는 정보를 이용해 버그를 잡아내는 방법을 의미합니다. 보통 Win32플랫폼에서는 이를 위해 MS에서 제공하는 DBGHelp라이브러리를 이용해 미니덤프파일을 남기게 됩니다. 물론, Dr.Watson을 이용해도 되지요.

미니덤프를 이용해 프로세스가 비정상종료될때의 위치를 파악한다고 해도, 앞에서 이야기한 것처럼 최적화된 바이너리라면, 문제를 찾아내기 어려울 경우가 발생합니다. 이때 사용가능한 기법이 메모리검색입니다.

네? 메모리 검색이라구요??

네. 메모리 검색입니다. 우리에게 필요한 것은 잃어버린 this 포인터이고, 결국은 메모리안에 존재합니다. 그러므로, 어떻게든 찾아내면 되는 것이죠! 이제 어떻게 찾아내는지가 관건일텐데.. 바람직한 C++프로그래머라면, 아마도 소멸자를 가상함수로 선언해두었을 가능성이 높습니다. 사실 이거 하나면 충분하죠. 무슨 이야기냐구요?

C++은 다형성을 구현하기 위해서, 가상함수테이블을 사용합니다. 그리고, 각 인스턴스들은 가상함수테이블에 대한 포인터를 들고 있습니다. 즉, 인스턴스의 메모리 레이아웃에 포인터변수가 선언되어 있는 것이지요. (_vftable!) 잃어버린 this 포인터가 가리키는 메모리, 즉 해당 클래스의 인스턴스가 차지하고 있는 공간에는 분명히 가상함수테이블에 대한 주소가 적혀있을겁니다. 슬슬 느낌이 옵니다. ㅎㅎㅎ

자. 이제 _vftable의 주소와 이 주소를 갖고있는 메모리를 찾으면 됩니다. 🙂

To be continued.

디버깅: 마음가짐.

전 사실, 대학에서 공학을 전공했습니다. 그것도, 산업공학이란 시스템을 다루는 공학을 전공했지요. 대학시절이 제게 남긴 가장 큰 가르침은 시스템에 대한 정의와 공학적 문제 해결 방법입니다. 시스템에 대한 정의는 언젠가 설계와 관련한 글을 쓸때 써먹게 될 것이고, 오늘 이야기할 디버깅은 공학적 문제 해결 방법을 써먹게 되겠네요.

일을 하면서 느끼는 것은 디버깅을 어렵게 느끼는 사람들이 많다는 점입니다. 어렵게 느끼는 사람들 혹은 어렵게 느끼는 상황을 곰곰히 생각해보면, 어려운 이유는 단 하나입니다. 막막하다는 것이지요. 보통, 막막함에 당황하고, 당황하니 더 막막한 악순환의 고리로 빠져드는 경우가 많습니다. 일단은 침착해야 합니다.

차분하게 공학적 문제 해결 방법을 따라서 생각해보는게 좋습니다.

  1. 문제를 탐색한다.
  2. 문제를 정의한다.
  3. 자료를 수집한다.
  4. 자료를 분석한다.
  5. 대안을 생성한다.
  6. 대안을 평가한다.
  7. 대안을 선정한다.
  8. 대안을 적용한다.
  9. 1번으로 돌아간다.

네. 사실 ‘당연한거 아냐?!’ 라고 생각하기 쉬운 당연하고 자명한 이야기입니다만, 당황하게 되면 의외로 잊기 쉬운 기본적인 것들입니다. 타석에 들어선 야구선수가 공을 끝까지 보고 스윙을 해야하듯이 디버깅도 항상 차분하게 공학적인 자세로 접근해야 하는거죠. 아무리 시스템이 복잡하다해도, 아무리 답이 안보이는 것 같은 문제라 할지라도, 차분하게 접근하면 대부분의 버그들은 찾아낼 수 있습니다. 차분한 마음가짐이 있다면, 비로소 여러 테크닉들을 적용할 수 있게 되니까요. 어려운 버그는 없습니다. 다만 복잡해서 막막한 버그일 뿐이지요. 모든 버그는 그냥 버그일 뿐입니다. 🙂

Zero-configuration: automatic lookup

Zero Configuration: Simple is best는 어렵다.에서 언급한 바와 같이, 현재 제작중인 소프트웨어의 모듈/시스템수는 엄청나게 증가해버렸다. 서로 연동해야하는 모듈이 많으니 이리저리 입력해야하는 네트워크 정보(각 노드의 IP와 Port)가 매우 많다. (제어UI에는 8개정도지만, 세밀하게 조정에 들어가면 셀수 없이 많다.) 이런 상황에서 각 노드들을 효율적으로 관리하려면 어찌해야 할까?

각 노드마다 설정을 전부 해야한다고 생각하면, 그 복잡도는 노드가 추가될 때마다 선형적으로 증가하며, 각 노드에서 필요한 기능-설정이 추가될 때 마다 기하급수적으로 증가한다. 언제까지 이런 설정을 반복하고 있을 수는 없다. 쉽게 생각할 수 있는 해결책은 중앙집중관리노드를 도입해서 실제로 기능을 수행하는 각 노드들이 중앙 노드를 바라보고 있는 Star형 토폴로지를 구현하는 것이다. 이러면, 문제가 해결될 것 같지만 크게 2가지 관점에서 결정적인 단점을 가지고 있다.

  1. 전체 시스템의 복잡도는 전혀 줄어들지 않으며, 다만 중앙 노드로 압축될 뿐이다.
  2. 중앙 노드에 장애가 발생할 경우, 전체 시스템이 동작하지 않는다.

이런 단점을 최소화하는 모델이라면, 전화번호부 모델이 있다. Java Jini나 Web Service에서 활용하는 Naming 서비스 혹은 Directory서비스가 대표적인 사례인데, 필요한 노드(서비스)의 위치를 관리하는 Naming 서비스를 도입하여, 각 노드간 통신을 위해 필요한 위치정보를 URL 혹은 그와 유사한 형태로 저장하여 서로 Lookup을 하게 하는 것이다. Respository나 Directory 서비스의 복잡도는 상대적으로 낮은 편이다. 권한이나 인증같은 부분을 제외하고 생각하면, 등록과 조회만 있으면 된다. TCP/IP네트워크에 익숙하다면, DNS를 생각하면 된다.

실제 데이터를 주고 받기 위한 통신 채널은 각 노드사이에서 형성되므로 Naming 서비스는 단지 ‘전화번호부’의 역할만을 수행한다. 각 노드들 역시 Naming 서비스의 위치만 설정해주면 되므로, 설정은 더더욱 간편해진다.

중앙 노드로 압축되는 복잡도의 문제는 어느정도 해소가 가능해졌지만, 중앙 노드의 장애에 대한 문제점은 아직 남아있다. Naming 서비스의 이중화로 해결할 수 있는 문제일 수도 있겠지만, Naming 서비스의 조회결과를 로컬에 캐쉬하는 방법도 있다. 노드들이 급격하게 위치를 변경하는 시스템이라면 독이 될 수 있지만, 위치가 그렇게 급격하게 변하지 않는 다소 정적인 시스템이라면 충분히 위력을 발휘할 것으로 생각된다.

멋져 보이지만, 마지막 결정타가 하나 남아있다. 이건 ‘Zero-Configuration’ 이 아니다. 여전히 사용자는 Naming 서비스의 위치를 ‘설정’해주어야 한다. TCP/IP에는 Broadcasting이라는 멋진 방법이 존재한다. 각 노드들에 Naming 서비스를 설정하지 않아도 Broadcasting으로 Naming 서비스를 찾아낼 수 있다. 물론, 수동 설정도 가능해야 하겠지만 말이다. 🙂

XML, XPath 그리고 소프트웨어 테스팅

보통 TDD(Test-driven development)는 잘 짜여진 테스팅 케이스를 이용해 소프트웨어의 수행 결과를 점검하는 형태로 이루어진다. 문제는, 이 방식의 접근은 처음부터 TDD를 하지 않으면 적용하기 힘들다는 거다!

게다가 최근 TDD가 없이는 개발이 불가능한 상황에 봉착했다. 입력 데이터의 종류는 수없이 많고, 이 데이터의 조합에 따라 서로 Side-effect를 발생시키는 골머리아픈 상황이다. (소프트웨어 설계가 잘못된건 아니다. 애시당초 풀어야할 문제가 이렇게 꼬여있다.) 설상가상으로 데이터의 종류가 무엇인지 알지 못하는 상황에서 종류를 guessing하고 조합해서 문제를 풀어야 할 정도니 말 다했다. 그리고, 출력되는 결과물도 굉장히 다양하다. (그 입력만큼이나 복잡하다)

프로게이머도 아닌데 엄청난 키보드+마우스 컨트롤을 자랑하며 테스팅을 할 수는 없는 노릇이고 해서, 간단한 테스팅 프로그램을 도입해서 문제를 해결하기로 생각하였다. 입력데이터를 잘 준비해두면 생성된 결과물을 잘 검사하기만 하면 되는 일 아니겠는가?

이 정도만 되면, 문제를 해결하기는 쉬울 것이지만 검사해야 할 항목들이 자주 변한다는 사실을 깨달았다. 입력데이터별로 차이도 존재하고, 데이터 종류에 따라, 또 그 조합에 따라 항목은 계속 바뀌어야 한다. 이쯤되면, 간단한 스크립트 언어를 만들어서 검사항목을 쉽게 변경할 수 있게 하면 되리라는 생각이 든다. 어머나. 배보다 배꼽이 커져버렸다. 🙂

그래서, XML과 XPath를 도입하기로 했다. 수행결과를 XML로 남기고, 남겨진 XML파일이 가져야할 조건, 즉 정상적인 생성물의 조건을 XPath 표현식으로 작성하는 것이다. TDD의 용어로 말하면, XPath 표현식은 Test case가 된다.

시험적으로 전체 시스템의 일부에만 적용해 보았는데 아주 마음에 든다. 물론, 서로 의존관계에 있는 XPath 표현식을 계층적으로 처리해주는 작업을 해야하긴 하지만, 그건 천천히 해도 될 것 같다. 🙂

ps. 관련된 코드와 예제를 공개하고 싶지만, 작업하다보니 대외비 코드가 너무 많이 들어갔네요. T_T

Zero-configuration: Simple is Best는 어렵다.

Zero-configuration이란 말을 처음 접했던 건, 한국에서 널리 사용되는 자막 파일인 SMI에 대한 지원을 Mac OS X용 통합 코덱(?)인 Perian에 넣을 수 없다는 애플포럼의 한 쓰레드 때문이었다. 요지는, SMI를 지원하기 위해서 자막의 인코딩 셋업같은 별도의 세팅이 필요하기 마련인데, 이게 Zero-configuration에 위배되기 때문에 SMI에 대한 지원을 넣을 수 없다는 내용이었다. (물론 이유가 이것만 있는 것은 아니었다.)

Zero-configuration은 말 그대로 설치 이후 설정이 필요없는 소프트웨어의 특성을 의미한다. 더 보기 “Zero-configuration: Simple is Best는 어렵다.”

XMLCC: XML생성을 위한 C++ 라이브러리

어쩌다 이름이 XMLCC가 되었는지는 모릅니다. 그냥 손가락에서 저렇게 나왔어요. 신의 뜻이라 생각하고 넘어갑니다. 씨익-

사건의 발단은 회사에서 XML을 생성해서 서버에 전송해야 하는 일이 생겨서 std::stringstream을 이용해 XML을 생성하다 보니 코드가 너무 지저분해지는 겁니다. 후.. 이런저런 고민을 한 끝에 좀 깔끔하게 짤 수 있는 프로그램을 작성해보기로 했지요. 🙂

일단 만들 XML부터 봅시다.


<author>Crow, Lim</author>

저런 문서를 하나 만들기 위해, std::stringstream을 사용하게 되면 다음과 유사한 코드가 등장할겁니다.

std::stringstream ss;

void make_xml(std::stringstream&amp; ss, std::string const&amp; title, std::string const&amp; author, ...(생략)...)
{
ss << "<author>";
ss << author;
ss << "</author>";
}

보기만 해도 끔찍하군요. 엄청난 escaping문자들.. 보기만해도 어지럽습니다. 만약 다음과 같이 작성할수 있다면 행복할텐데요.

void make_xml(std::stringstream& ss, std::string const& title, std::string const& author, ...(생략)...)
{
// 들여쓰기는 보기 편하라고 한 것 입니다.
element_start(ss, "book");
    attribute(ss, "title", title);

element_start(ss, "author");
    text(ss, author);
element_end(ss, "author");

element_start(ss, "date");
    attribute(ss, "year", year);
    attribute(ss, "month", month);
    attribute(ss, "day", day);
element_end(ss, "date");

element_end(ss, "book");
}

아쉽게도 위와 같은 코드는 작동하지 못합니다. XML의 특성상 attribute는 element 시작태그 내에서 정의가 되어야 하기 때문이지요. 물론, 이를 해결하기 위해 element_start의 인수로 attribute의 list를 넘겨주는 방법도 있습니다만, 이 방법은 해당하는 리스트를 생성해야한다는 단점이 있습니다. 의미상 코드에서 정적으로 정의가 가능한 XML을 굳이 리스트를 생성하면서 만들 필요는 없지요.

이를 위해 element_start()의 리턴을 객체로 주는 방법을 사용했습니다. 🙂 그 객체에서 시작태그를 생성하고 attribute도 처리하는 거지요.

void make_xml(std::stringstream& ss, std::string const& title, std::string const& author, ...(생략)...)
{
    element_start(ss, "book")
        .attribute("title", title);

    element_start(ss, "author");
    text(ss, author);
    element_end(ss, "author");

    element_start(ss, "date")
        .attribute("year", year)
        .attribute("month", month)
        .attribute("day", day);
    element_end(ss, "date");

    element_end(ss, "book");
}

element_start()의 리턴객체에서 attribute를 호출합니다. 자세히 보시면 뒤에 세미콜론(;)이 없는 라인들이 있는데 사실 한 expression이란 겁니다. 일종의 호출 체인이랄까요?

xmlcc.h에 있는 주요 부분을 보면 다음과 같습니다.

	struct element_start_tag
	{
		std::ostream& out_;

		element_start_tag(std::ostream& out, std::string const& name)
			: out_(out)
		{
			out_ << "<" << name;
		};

		element_start_tag(element_start_tag const& tag)
			: out_(tag.out_)
		{
		};

		element_start_tag& attribute(bool use, std::string const& name, std::string const& value)
		{
			if(use == true)
			{
				return attribute(name, value);
			}

			return *this;
		}

		element_start_tag& attribute(std::string const& name, std::string const& value)
		{
			out_ << " ";
			text(out_, name); 
			out_ << "=\"";
			text(out_, value); 
			out_ << "\"";
			return *this;
		}

		template<class ValueT, class FilterFuncT>
			element_start_tag& attribute(std::string const& name, ValueT const& value, FilterFuncT const& f)
		{
			out_ << " ";
			text(out_, name); 
			out_ << "=\"";
			text(out_, f(value)); 
			out_ << "\"";

			return *this;
		}

		template<class Func>
			element_start_tag& attribute_with_generator(std::string const& name, Func const& f)
		{
			out_ << " " << name << "=" << "\"";
			f(out_);
			out_ << "\"";
			return *this;
		}

		element_start_tag& operator()(std::string const& name, std::string const& value)
		{
			return attribute(name, value);
		}

		element_start_tag& operator()(bool use, std::string const& name, std::string const& value)
		{
			return attribute(use, name, value);
		}

		void close_now()
		{
			out_ << "/";
		}

		~element_start_tag()
		{
			out_ << ">";
		};
	};


	inline element_start_tag element_start(std::ostream& out, std::string const& name)
	{
		return element_start_tag(out, name);
	}

보시면, 생성자에서는 시작태그의 요소이름까지만 출력을 하고, 소멸자에서 태그를 닫습니다. attribute를 사용하면 stream에 attribute를 출력해주지요. 또한, close_now()를 호출하면 “/”를 출력시켜줌으로써 스스로 닫는 element를 생성해줍니다. 그리고, element_start()는 이 element_start_tag를 생성해주는 도우미 역할을 하지요.

attribute메소드는 operator()를 이용해도 사용할 수 있습니다. 이 경우 코드가 훨씬 간단해지지요.

void make_xml(std::stringstream& ss, std::string const& title, std::string const& author, ...(생략)...)
{
element_start(ss, "book")
    ("title", title);

    element_start(ss, "author");
        text(ss, author);
    element_end(ss, "author");

    element_start(ss, "date")
        ("year", year)
        ("month", month)
        ("day", day);
    element_end(ss, "date");

element_end(ss, "book");
}

사실 XML생성에 있어서 다른 XML의 구성요소들은 크게 문제가 되지 않는데, 가장 머리 아픈 부분은 element의 시작태그와 그 안에 내포된 attribute인지라 이런 잔머리를 굴려봤습니다. 🙂 덕분에 소스코드는 escape문자 없이 깔끔해졌군요. 🙂 잇힝-

좀 더 삽질해서 DSL(Domain Specific Langauge)형태로 만들어 보려고 했는데, 시간도 없고 이정도면 쓸만한거 같아서 여기서 멈췄습니다. 🙂

xmlcc.h 다운로드

XML에 대한… 고찰?!!

XML을 처음 접하고 사용하기 시작한게 2000년 경이었을테니, 얼추 7년정도 사용한듯 하다. 2000년에는 XML문서버젼관리 시스템 프로젝트를 했었고, 그 뒤에는 PlayAction이란 사이트 겸 CMS를 만드는데도 사용했었고, 회사다니면서는 각종 설정파일, 유틸리티, 메타정보등을 생성/관리하는데 사용해왔다. 그리고, C++로 xml pulling parser방식의 인터페이스를 가진 라이브러리도 고민했었다. (libxml을 이용한 DOM라이브러리로 전환하긴 했지만..) 이렇듯 프로그래머로서의 삶에 깊숙하게 침투하신 이 분을 어떻게 요리할까라는 생각이 재개된건, 졸업프로젝트로 고려중인 Simulation Engine제작 때문인게 크긴 하다.

각설하고, XML을 분석하는 관점은 크게 Well-formed Document와 Valid Document라고 볼 수 있다. Well-formed는 XML의 기본 문법에 맞는지의 여부이고, Vaild는 문서정의 (DTD나 XML Schema)에 맞는지의 여부다. 이 두가지 관점을 항상 같이 고민하다보니 문제가 어려워졌던 것 같다. 오랜만에 다가온 Eureka!!는 실로 반갑다. 자 그럼 그 Eureka!!가 무엇이냐 하면.. (사실 별거 아니다)

XML을 가장 간단하게 분석하는 방법은 <와 >로 묶인 태그라는 점이다. XML의 가장 기본적인 요소인 Element외에도 DocType(<!DOCTYPE >)이나 Processing Instruction(<? ?>), Comment(<!– –>), CDATA(<![CDATA[ ]]>)등도 결국은 <와 >로 묶인 태그였다. 이렇게 관점을 바꿔놓고 보니 몇가지 재미있는 속성들이 발견되었다.

  1. <![ 로 시작되는 태그들은 ]]>로 닫힌다.
  2. <! 로 시작되는 태그들은 >로 닫힌다.
  3. <? 로 시작되는 태그들은 ?>로 닫힌다.
  4. <로 시작되는 태그들은 >로 닫힌다.

이런걸 왜 이제야 생각했는지 모르겠다. -_-

XML의 전체 스펙을 생각하고 파서를 작성하려고 들면, 일이 복잡해진다. 하지만, 위의 4가지 속성만 고려하고 작성한다면, 규모가 작아지고 작성이 간단해질 것 같다. – 기존에 사용하던 state_machine클래스를 사용하면 더욱더 간단히 🙂 –

또한, 각 태그들을 Event로 간주한다면, 트리기반의 DOM이나 콜백기반의 SAX와는 달리, XML문서를 Event의 리스트로 관리할 수 있게 된다. SAX의 단점이라면, 역시 수정이 불가능하다는 것이고, DOM의 문제라면 메모리를 많이 차지한다는 점일텐데, 이 두 문제점 사이에서 적절히 타협할 수 있는 안이 되지 않을까 라는 생각이 들었다. 그리고, Event의 리스트로 관리하게 되면 복잡하긴 하겠지만, 트리형태의 Access도 불가능한 것은 아닐거라 생각한다.

남은 일은, 구현하는 일이지만… 회사일과 졸업논문, 각종 시험과 과제가 발목을 잡고 있는 현실이 안타까울 따름….. 응??

C++ Native AOP의 가능성.

Aspected Oriented Programming(AOP)을 가장 잘 지원하고 있는 언어를 꼽으라면, 숨도 쉬지않고 Java를 선택할 수 있겠다. Java의 경우에는 주로, VM코드를 직접 수정하는 방식으로 구현하는 모양인데, C++에서 기계어를 고치고 있을 수는 없는 것 아닌가.. 상상만 해도 우울하다.
더 보기 “C++ Native AOP의 가능성.”

표준안을 지키잣!

이전까지 쓰던 STLPort는 5.0.1이었습니다. 뭐 큰 문제도 없고 해서 그냥그냥 쓰고 있었는데, 5.1.0이 나온걸 보고 업그레이드를 결심. 다운로드후 컴파일하고 프로젝트의 세팅을 전부 변경한 뒤에, 빌드를 시작했지요. (시켜놓고 회식갔다가 오늘 아침에 봤습니다.)

빌드에 오류가 났습니다. 흐음. 5.0.1에서는 오류가 없었는데, 5.1.0에서는 오류가 발생하는군요. 이럴땐, STLPort가 문제일 가능성이 크다고 판단, 오류로그를 뒤지기 시작했습니다.

stlport/stl/_algo.c(130) 에서 오류가 발생하는군요. 에러내역은 다음과 같습니다.

error C2512: ‘SCT::stdex::view_iterator‘ : 사용할 수 있는 적절한 기본 생성자가 없습니다.

SCT::stdex::view_iterator는 제가 만들어서 쓰고있는 커스텀 반복자인데요. 이게 ParentView에 있는 데이터를 순차적으로 접근하기 위한 녀석이라, 기본 생성자를 만들어 두고 있지 않습니다. 그래서, 에러가 난듯 하다고 판단. STLPort의 소스코드를 뒤져보기 시작했습니다.

STLPort 5.0.1의 소스코드

template <class _ForwardIter1, class _ForwardIter2>
_ForwardIter1 search(_ForwardIter1 __first1, _ForwardIter1 __last1,
                     _ForwardIter2 __first2, _ForwardIter2 __last2) {
(.... 생략 ....)
  // 다른 부분!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  _ForwardIter1 __current = __first1;

  while (__first1 != __last1) {
    __first1 = find(__first1, __last1, *__first2);
    if (__first1 == __last1)
      return __last1;

    _ForwardIter2 __p = __p1;
    __current = __first1;
    if (++__current == __last1)
      return __last1;
(.... 생략 ....)
}

STLPort 5.1.0의 소스코드

template <class _ForwardIter1, class _ForwardIter2>
_ForwardIter1 search(_ForwardIter1 __first1, _ForwardIter1 __last1,
                     _ForwardIter2 __first2, _ForwardIter2 __last2) {
(.... 생략 ....)
  // 다른 부분!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  _ForwardIter1 __current;// = __first1;

  while (__first1 != __last1) {
    __first1 = find(__first1, __last1, *__first2);
    if (__first1 == __last1)
      return __last1;

    _ForwardIter2 __p = __p1;
    __current = __first1;
    if (++__current == __last1)
      return __last1;
(.... 생략 ....)
}

STLPort 5.1.0

거의 같고 딱 1부분이 다른거 같습니다. 5.0.1은 __current에 __first1을 대입 하고있고, 5.1.0은 대입 하고있지 않습니다. (그것도 주석처리 되어있지요.)

이유는 28번 라인에서 언제나 __current는 __first1으로 업데이트가 되기때문에, 굳이 처음 생성시 __current에 __first1을 대입시켜줄 필요가 없지요. 이런 이유로 불필요한 작동을 줄이고자 주석처리를 해둔 것이라고 생각됩니다.

view_iterator에는 기본 생성자에 만들어줘야하는지, STLPort 코드를 수정해야할지를 고민하다가, C++표준안(정확히는 97년에 나온 드래프트)을 뒤적거려보니 Forward Iterator는 예측 불가능한 값을 갖는 Iterator를 생성하는 기본 생성자를 갖고 있어야 한답니다. view_iterator는 Random Access Iterator이고, Forward Iterator의 특성을 물려받으므로, 기본 생성자가 있어야 하는군요. 그래서 추가해줬습니다. (그리고, 배를 쨌습니다. 이상한 값을 갖는게 맞다잖아요. 무슨 결과가 나오든 전 모름 @_@)

당황해서 시간을 좀 낭비했는데… 역시 표준안을 숙지하고 미리미리 지켜주는게 인생 편하게 코딩하는 방법인가 봅니다. 에헤.

ps. view_iterator는 별 코드에 다 집어넣고 요상한 알고리즘과도 빙빙 돌려제끼는 반복자인데… 이번이 처음이었습니다.. 이럴줄은.. ;ㅁ;