회사에서 테스트 주도 개발(원제: Test Driven Development: By Example) 2판을 빌려와서 설 연휴에 보고 있다. TDD에 대해 권위있는 책이다. 처음 보는 것은 아니지만 요즘 TDD가 잘 안되서 기초를 다지기 위해 다시 한번 보고 있다.

그런데 문제 - 사실 나쁜 의미에서의 문제는 아니다 - 는 모든 코드가 Java로 짜여져 있다는 것이다. 그리고 나는 C++에서 적용하고 싶었다. 내가 이 책을 처음 보았을 때는 TDD에 대해 아무 것도 모르던 시절이라 그냥 이런 것이 있나 보다 하고 눈으로 따라가면서 읽은 것이 다였다. 원래는 키보드로 쳐가면서 했어야 했지만 그러지 못했다. 그 이유 중에 하나가 바로 C++에서 사용할 수 있는 테스팅 프레임워크를 잘 몰랐던 것이다.

지금은 GoogleTest를 몇 달 동안 사용해왔기 때문에 잘 따라갈 수 있을 것 같다.


Money 객체를 만들면서 예제를 따라가다보니 언어의 차이 때문에 몇 가지 문제가 발생했다.

첫번째는 비교 구문이다. 
Java에서는 equals()를 사용하기 때문에 operator==()로 대신 구현했다. 그런데 중간에 보면 Dollar와 Franc을 상위 클래스에서 비교하는 구문에서 RTTI를 이용하는 부분이 나온다. Java에서는 getClass()로 런타임에 객체의 타입을 알아낼 수 있다. 그래서 책에서는 통화가 다른 경우 false를 리턴하게 했다. C++에서도 typeid()를 사용할 수 있기는 한데 이상하게 제대로 동작하지 않았다. 그래서 어쩔 수 없이 해당 부분 테스트는 주석 처리해버렸다. 이것 때문에 뒤의 몇 가지 테스트는 제대로 돌려보지 못했다.

두번째는 팩토리 패턴을 구현하는 부분에서부터 발생했다.
Money 클래스에서 자식 객체인 Dollar를 리턴하는데 코드가 아래와 같았다.
Money five = Money::Dollar(5);
그런데 Money 클래스는 abstract class라서 인스턴스화가 되지 않는다는 컴파일 에러가 떠버렸다. 아악... 이 문제 때문에 잠시동안 C++ 언어를 욕하다가 그냥 shared_ptr를 사용하는 것으로 문제를 해결했다.
typedef ::std::tr1::shared_ptr<Money> MoneyPtr;
MoneyPtr five = Money::Dollar(5);
물론 거의 절반에 가까운 테스트 코드와 클래스 코드를 바꾸어야 했다. 가끔 코드를 짜다 보면 이렇게 막다른 골목에 부딪히는 경우가 있다. 나 같은 경우 C++에서는 포인터 사용을 최대한 하지 않으려고 하는 경향이 있는데 그러다 보니 이런 문제가 가끔씩 생긴다 - 즉 포인터를 써야만 하는 상황이 오고야 말았을 때 시그니처가 바뀌면서 코드가 뒤집힌다.

문제는 한 번 스마트 포인터를 사용하기 시작하면 코드 전체가 스마트 포인터를 써야 한다는 것이다. 메소드의 파라미터, 리턴값, 생성자에서 받을 포인터와 객체 내에 보관하는 포인터까지... 결국 모든 클래스의 인스턴스를 스마트 포인터로 처리했다. 

개인적으로 스마트 포인터를 쓰는 것은 나쁘지 않다고 생각하지만, 문제는 코드 전반에 스마트 포인터에 종속성이 걸리는 경우다. 후반부로 갈 수록 거의 돌이키기 힘든 디자인 문제가 되기 때문에 아주 골치가 아프다.

반응형
아래 코드를 보자.

typedef unsigned int UINT32;

std::string text = GetText();
for(UINT32 i = text.size() - 1; i >= 0; --i)
{
    // 텍스트를 뒤에서부터 읽어서 처리
    DoSomething(text[i]);
}
주석처럼 스트링을 뒤에서부터 읽어가면서 뭔가 처리를 하려고 한다. 그런데 프로그램이 뻗어버린다.

이유는 unsigned 때문이다. i가 0까지는 잘 내려오지만, 그 직후에 언더플로우가 발생해버린다. 그러면 약 42억의 수를 가지게 될 테고... 결과는? 쾅~


int를 사용하면 되겠지만, 저기 std::string::size() 라는 녀석은 리턴값이 size_t이다. 즉 unsigned int와 같다. unsigned와 int를 섞게 되면 컴파일러가 경고를 하게 된다. 경고는 당연히 피해야 하는 것이고 따라서 위와 같이 unsigned를 사용했던 것이다.



위 코드에서는 STL 자료구조가 사용된 관계로, 그냥 reverse_iterator를 사용하면 해결된다. 하지만 index를 알아야 하는 경우가 있다면... 어떻게 해야 할까?

내 생각엔 int i = static_cast<int>(text.size() - 1) 처럼 사용하는게 낫지 않나 싶다. 가독성이 좀 떨어지는 단점이 있지만 딱히 방법이 없다.

이럴 땐 Java처럼 unsigned를 안쓰는 정책을 사용하거나, C#처럼 웬만하면 기본 라이브러리는 int형을 최대한 사용하고 signed와 unsigned가 같이 쓰이는 경우 컴파일 에러(심지어 케스팅을 해도)를 발생시켜주는 언어가 약간 부럽다.


반응형
좀 전에 아래와 같은 코드를 작성하고 있었다.[각주:1]

using ::std::string;
using ::std::deque;

bool Slump::Parse(const string& string, deque< string >& stringsOut)
{
    return true;
}


그런데 컴파일 에러가 발생했다. deque의 _Ty 템플릿 인수가 잘못되었다고 한다. 대체 뭐가 문제인지 1분동안 고민했다. 그리고 알아냈다.

0번 파라미터 이름이 string이라서, 1번 파라미터를 만들 때 deque의 템플릿 파라미터로 넣은 string이 타입이 아니라 변수로 인식된 것으로 보인다. 0번 파라미터 이름을 str 등으로 바꾸거나, 1번 파라미터를 deque<std::string> 으로 변경하자 문제가 사라졌다.

그러니까 0번 파라미터의 변수가 1번 파라미터에서 사용되어버렸다는 소린데... 컴파일러가 무슨 생각으로 이렇게 동작하는지 모르겠다. 파라미터 간 데이터 전달이라도 가능한 문법이라도 있는걸까?

한가지 더 테스트해보았다.

using ::std::string;

void Foo()
{
    string string;
    string stringAnother;
}
여기서도 첫번째 string string은 잘 컴파일되지만, 두번째 string stringAnother는 컴파일 에러가 발생한다.



보통 아직 의미가 확정되지 않은 - 예를 들면 파싱할 때 입력되는 스트링 - 스트링 변수는 그냥 이름을 string으로 짓는데, 저런 경우에 혼란을 발생시킬 문제가 있으니 다른 것으로 바꿔야겠다.

그런데 뭘로 하지... text? str? s?

코드는 최대한 '소리내어 읽을 수 있는' 형태로 작성하고 싶기 때문에 살짝 고민이 된다.

그냥 타입은 std::string으로 써버릴까?
그러자니 가독성이 저해된다. 그냥 text로 쓰는게 그나마 제일 나을 것 같다.

  1. SyntaxHighlighter에서 꺾쇠 기호를 사용하면 태그로 인식되기 때문에 string 좌우로 꺾쇠에 공백을 넣었다. 원래 코드를 작성할 때는 꺾쇠에 공백을 넣지 않는다. [본문으로]
반응형
사실 이미 인지하고 있었던 문제였긴 한데, 최근에 몇 번 또 당해버렸다.

GoogleTest를 사용하면서 라이브러리를 미리 빌드해놓고 쓰는데 디버그 모드로 빌드한 것과 릴리즈 모드로 빌드한 것 두 가지로 만들고, 디버그 모드로 빌드한 것은 lib 파일 이름 뒤에 'd'를 붙여서 사용하고 있다.

현재 작성중인 코드를 디버그 모드로 빌드할 때는 GoogleTest 라이브러리도 디버그 모드로 빌드한 것을 써야 하고, 릴리즈 모드로 빌드할 때는 라이브러리 역시 릴리즈 모드로 빌드한 것을 써야 한다.

만약 이렇게 하지 않으면 컴파일은 잘 되는데 런타임에 프로그램이 에러를 뱉고 죽어버린다.
그 외에도 std::map을 넘겨받았는데 내용물이 완전 엉망이 되어서 받은 적도 있다.




그런데 CRT를 맞춰봐도 역시 동일하게 프로그램이 죽어버린다. 그러니까 GoogleTest의 라이브러리와 내 작업중인 프로젝트의 프로젝트 속성-C/C++-코드 생성-런타임 라이브러리를 "다중 스레드 DLL(/MD)"로 통일했는데도 계속 발생한다는 말이다.

그래서 이번엔 CRT를 맞춰준 상태에서 Preprocessor의 _DEBUG를 NDEBUG로 바꿔봤는데, 잘 실행이 된다.



여러 가지 조합을 테스트해본 결과 얻어낸 결론:
  1. 런타임 라이브러리를 맞춰줘야 한다. (이건 구글링 결과와 같다)
  2. 그리고 Preprocessor 에서 _DEBUG를 모두 넣어주던가 모두 빼줘야 한다.


2번의 경우 심증이 가는게 있는데, 바로 STL이다. STL의 경우 디버그 모드로 빌드하면 디버그 정보를 담고 경계 검사 등 이것저것 해주는게 많아지는 것으로 알고 있다.

만약 클래스나 구조체가 릴리즈와 디버그 모드일 때 담고 있는 데이터가 달라지게 될 경우, 라이브러리는 이미 코드가 고정되어있는 상태기 때문에 넘겨받은 자료구조 내의 실제로는 존재하지 않는 데이터에 접근한다거나 경계가 달라진다거나 해서 스택이 깨질 수 있다. 이건 실험을 통해 가능하다는 것을 확인했다.

이게 무슨말이냐면, 예를 들어 A라는 라이브러리를 디버그 모드로 빌드했다고 가정하면 이 녀석은 STL 헤더를 디버그용으로 인클루드해서 빌드를 해놓게 된다. 그런데 릴리즈 모드로 B라는 실행 파일이 이 라이브러리를 사용하게 되면 STL 헤더를 릴리즈용으로 인클루드한다. 그러면 쾅~ 서로 자료구조가 같다고 가정하고 링크했지만 실제 자료구조는 다르기 때문에 발생하게 된다. 소켓 통신을 하는데 보내는 사람과 받는 사람이 생각하는 프로토콜 구조가 다른 것과 비슷하다고 할 수 있다.


일단 골치아프지 않으려면 라이브러리도 디버그용과 릴리즈용을 구분해서 쓰는게 좋겠다.



반응형

TDD(Test Driven Development)를 시작하고 있다. 


C++에서의 TDD의 힘겨움

그런데 유명한 TDD 책들을 보니 죄다 Java 언어로 되어 있다.
그 유명한 녹색 막대도 Eclipse IDE에 프로그래스바 플러그인이 JUnit과 함께 녹아들어가 있어서 가능했던 것이었다.

C++ 진영에서는 테스팅 프레임워크가 좀 많다. 그렇지만 IDE 애드온을 제공하는 곳은 없기 때문에 아무래도 까만 콘솔 화면밖에 볼 수 없는 것 같았다.

TDD는 안그래도 시작하기가 힘든데, 이렇게 프레임워크가 다양해서 막막했다. 대체 뭘 골라야 잘 골랐다는 소리를 들을까? Java의 JUnit과 같이 표준스러운게 있다면 선택의 여지도 좁지만 반대로 시작하기는 훨씬 쉬웠을 것이다.

노엘의 홈페이지
CppUnit
CppUnitLite
CxxUnit
UnitTest++
GoogleTest
등등...

너무 녹색 막대가 보고 싶어서(나도 'Green Bar' 보고 싶다고 ㅠㅠ) 처음에는 vutpp + CppUnitLite를 사용했다. 처음에는 녹색 막대를 본다는 사실에 많이 들떠있었지만, 버그도 좀 있고 사용하기가 생각보다 그닥 편하지가 않아서 다른 것을 찾아보게 되었다.

찾아보다 느낀 사실은 Windows + VisualStudio + C++ 조합의 개발자가 적지 않은데도 제대로 된 오픈소스 VS IDE Addon이 없다는 사실이다. TDD 책에 보면 빨간 막대 -> 녹색 막대 -> 리팩토링 이라고 말하는데 그건 자바 사정이고 C++에서 녹색 막대 보기는 물건너간것 같다.


VisualAssert의 발견

라고 생각하던 와중에 VisualAssert라는 VS Addon형 테스팅 프레임워크를 발견했다. 이거 아주 쓸만하다. 초보자라면 CppUnitLite 만큼 강추한다.

이것도 좀 쓰다 보니 문제가 있었는데, 테스트 도중 하나라도 실패하면 전체 테스트가 멈춘다는 것이다.


GoogleTest의 발견

그래서 결국 GoogleTest로 왔다.

역시 구글은 다르다는 느낌을 소스만 보고도 느꼈다. 비록 콘솔창으로 결과가 출력되지만 텍스트에 색을 입혀서 성공과 실패를 금방 알 수 있게 해준다. 녹색 막대를 못 보는 사람들을 세심한 배려(?)가 아닐 수 없다.

C++에는 리플렉션이 없어서 테스팅 프레임워크마다 하나씩 아쉬운 점이 보였는데, GoogleTest에서는 그런 점을 상당 부분 개선하려고 한 부분이 보인다.

예를 들면 특정 문자열로 시작하는 테스트만 실행시킬 수 있는 등 필터 기능과, 에러가 발생했을 때 디버거를 붙일 수 있는 기능 등 실제로 필요하다고 생각되는 기능은 다 있는 것 같다. 그리고 Windows도 차별 없이 잘 지원해주고 있다.

현재 모든 프로젝트는 GoogleTest를 사용해서 진행하고 있다.

반응형

+ Recent posts