때는 2007년 봄. 나는 대학교 외에서의 첫 실무 프로젝트를 시작하게 되었다. 20명 남짓 규모의 작은 SI 회사에 취직하게 되었고, 나에게 주어진 임무는 이 회사에서 기존에 만들었던 것과 비슷한, 하지만 다른 고객 회사를 위한 어플리케이션을 작성하는 것이었다.

작은 회사가 원래 다 그런건지 아니면 SI 회사가 다 그런건지는 모르겠지만, 목표만 던져줬을 뿐, 코딩 스타일 가이드나 기본적인 설계와 철학, 이 회사에서 사용하는 라이브러리나 기본적인 프로젝트 시작 방법에 관하여 별 다른 업무 지도를 받지 못하였다. 뭘 어떻게 만들라는건지 갈피를 못 잡다가 마감일이 점점 다가오기 시작하자 나는 조급해졌다. 아무래도 이대로는 힘들다 싶어 선배님의 코드를 열어 살피기 시작했다.

참고로 당시 비주얼 C++ 6.0 을 사용하고 있었다. 비주얼 스튜디오 프로젝트 파일을 더블클릭하는 순간, 나는 어디선가에서 나는 강한 악취를 느꼈다. 그 악취는 바로 눈앞의 소스 코드에서 풍겨져 나오고 있었던 것이다.

당시 내가 보았던 코드는 내 기억에 따르면 아래와 비슷했다:

---------------
char g_szMstNum[3];
...
memset(g_szMstNum, 0, 3);
memcpy(g_szMstNum, "Bar", 3);
---------------

위 코드를 보고 혹시 문제점을 몇 가지 찾아낸 사람 있는가? 아니면 이해가 잘 되고 어디가 문제라는건지 모르겠는가?

내가 생각하는 이 코드의 문제점은 다음과 같다.

1. sz라고 써있지만 null로 끝나는 문자열이 아니다.

일반적으로 C/C++ 에서는 스트링의 마지막을 표시하기 위해 0 또는 null 을 사용한다. 그런데 여기서는 3바이트 캐릭터 배열에 3글자를 집어넣었기 때문에 sprintf(file, "%s", g_szMstNum); 따위의 코드를 넣었다가는 수백 수천 바이트가 파일에 기록될 수도 있고 잘못된 메모리 주소에 접근하여 크래시가 날 수도 있다. 그러나 변수 이름에서 이 사실을 알아낼 수 없으며 오직 정의 부분과 "Bar"를 대입하는 코드를 둘 다 찾아보고 메모리 크기와 스트링 크기가 정확히 일치한다는 것을 눈치채야만 알 수 있다. 요약하자면 완전히 잘못된 정보를 주고 있다.


2. 변수 이름만 봐서는 이 변수가 무엇을 의미하는지, 어디서 사용되는지 쉽게 알 수 없다.

g_szMstNum 변수에서 헝가리안 표기법에 해당하는 g_sz 를 떼어내고 나면 "MstNum"이 남는다. 이게 무슨 의미인지 알 수가 없다. Master 인지 Most 인지 Minimum Spanning Tree 인지 보는 사람마다 다르게 해석할 수 있을 것이다.


3. 변수 이름과 변수 내용에 접점이 없다.

Num으로 미루어 보아 숫자일 것이지만 sz 접두어를 보면 문자열이다.


위는 약간 각색하긴 했지만 실제로 있었던 일이다. 특히 sz 접두어를 붙였지만 문자열이 null 로 끝나지 않도록 만들어 둔 바람에 여기다가 스트링 처리를 했던 모든 코드들에서 문제가 생겼었다. 몇 시간 동안 디버거를 통해 변수가 어떻게 초기화되는지 찾아보고 나서 위와 같은 코드를 찾은 나는 헝가리안 표기법에 대한 깊은 의구심이 들기 시작했고 이에 대한 외국 포럼의 토론 글을 찾아보기 시작했다. 그리고 내린 결론이다:




나는 헝가리안 표기법(Hungrian notation)은 이제 사용하지 않아야 한다고 강력히 주장하는 바이다.

첫 번째 이유는 사람들이 헝가리안 표기법을 잘 못 이해하고 사용할 가능성이 높으며, 이 경우 큰 혼란을 초래할 수 있다는 것이다. 가령 sz가 String terminated by Zero 의 약자인데도 사람들이 잘 모르고 스트링을 대표하는 접두어로 그냥 쓴다는 것이다. 만약 해당 변수가 null로 끝나지 않는다면 나와 같은 상황이 생길 수도 있다. 이 외에도 int 형 변수의 접두어를 누구는 n을 쓰고 누구는 i를 쓰고, bool 변수는 b를 쓰기도 하고 is를 쓰기도 하며 float 변수는 f를 쓰기도 하고 fl을 쓰기도 하며 static 변수 앞에 s를 붙이기도 하지만 스트링 앞에 s를 붙이기도 하는 등 일관성이 없는 문제가 있다. 일관성이 없어지기 쉽다는 것이야말로 이 방법이 잘못되기 쉽다는 반증이기도 하다.

두 번째 이유는 변수의 '타입'을 변수 이름에 기재해야 하는 이유가 없다는 것이다. 요즘은 IDE가 발달해서 변수의 타입 정도는 아주 빨리 알아낼 수 있다. 그리고 C와 달리 C++은 클래스가 도입되어 사용자가 원하는 타입을 거의 무한대로 만들 수 있는데 접두어를 일일히 고안해낼 수도 없는 일이다. PlayerState 클래스 변수의 접두어는 무엇을 할 것인가? ps? iAge를 생각해보자. 나이는 당연히 숫자인데 int 형이라는 정보를 굳이 줄 필요가 있는가?

세 번째 이유는 변수의 가독성이 떨어지고 변수명이 쓸데없이 길어진다는 것이다. 우리는 대부분의 시간에 코드를 쓰기보다는 "읽는다". 변수의 이름은 변수가 어떤 역할을 하는지에 충실해야지 타입을 나타내는데 낭비되어서는 안된다고 생각한다. m_iAge 와 Age 중 어떤게 보기 편할까? 별 차이가 없다고 생각할 지 모르겠지만, 사람은 문자를 읽을 때 마음속으로 모든 글자를 소리내어 발음하려는 경향이 있다. m_iAge 는 "엠, 아이 에이지"로 읽지만 Age 는 그냥 "에이지"로 읽게 된다. 불필요한 문자들의 추가는 가독성을 현저히 떨어뜨리며 코드를 이해하는데 방해가 된다.


네 번째 이유는 변수의 타입이 변경될 경우 변수명을 모두 교체해야 한다는 것이다. 간혹 겪는 일인데, 정수로 계산하던 값이 부동소수점이 되어야 할 일이 생겨서 타입을 바꾸는 경우가 있다. 예를 들어, 캐릭터의 체력은 화면에 정수로 표시되므로 int 타입을 줘서 iHealth 로 만들었다가 나중에 초당 0.5씩의 피해량, 또는 50% 감쇠된 피해량 등을 계산해야되서 float 로 바꾸는 경우가 그것이다.

다섯번째 이유는 타입을 알아야 하는 것 자체가 문제라는 것이다. 예를 들어, 총알을 쐈을 때 적에게 주는 피해량을 50% 증가시키는 버프를 만든다고 생각해보자. 아마 여기서 enemyHealth -= damage * damageBuffRate; 정도의 코드면 이해하는 데 문제가 없다. 그러나 이게 m_iEnemyHealth -= iDamage * fDamageBuffRate; 이면 눈이 피로해지고 잘 읽어지지 않는다. 물론 타입을 알아야 할 순간이 오기는 한다. 그런데 왜 그걸 선언부가 아니고 변수명에 주렁주렁 달고 다니는가?


여섯번째 이유는 기본 타입(Primitive type) 강박증이 생기기 딱 좋은 습관이라는 것이다. 위 g_szMstNum 을 보면 알겠지만 C++ 에 기본적으로 딸려오는 STL 클래스인 std::string 으로 대체해도 충분하며 오히려 char 배열 변수를 사용하는 이유가 무엇인지 물어보고 싶어질 정도이다. 헝가리안 표기법에서는 항상 변수를 만들 때 타입 이름을 무엇을 넣을까 생각하는 과정에서 이미 잘 알려진 타입(i, b, sz, f 등)을 쓰려는 경향이 있다. 따라서 적절한 클래스를 만들어 넣어야겠다는 생각을 하기 어렵고 C native 타입들을 사용하게 되기 마련이다. 기본 타입 강박증은 코드 라인 수가 상당히 길어지고 헤더가 비대해지며 응집도가 낮은 코드를 만드는 경향이 있다. 또 제네릭한 코드와 객체 지향 코드와는 상당히 맞지 않는다.


일곱번째 이유는 변수 이름을 대충 짓는 습관이 생기기 좋다는 것이다. 단지 타입을 변수 이름에 집어넣는 것으로 iPlayer, szPlayer 같은 변수 이름이 얼마나 많이 보이는지 생각해보자. playerIndex, playerName 으로 바꾸면 얼마나 보고 이해하기 좋은가?


여덟전째 이유는 헝가리안 표기법을 썼던 MS 조차도 이제는 쓰지 않는다는 것이다. 헝가리안 표기법을 쓰는 사람 중 일부는 예전 Windows API 스타일을 보고 이 똑똑한 사람들이 만든 법칙에는 분명 무언가 중요한 이유가 있을 것이라는 것을 느끼고 따라한다. 그러나 MS가 만든 현대적인 언어인 C#을 다뤄본 사람은 알겠지만, 헝가리안 표기법은 MS 내에서 더 이상 사용되지 않으며 실제로는 사용이 금지되었다. 

https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/general-naming-conventions

위 링크는 MS의 프레임워크 디자인 가이드 중 명명법(Naming) 항목이다. 여기에 가면 다음과 같은 문장이 있다:

DO NOT use Hungarian notation. (헝가리안 표기법을 사용하지 말 것)

이 정도면 말 다 했다고 생각한다.




정리하면,

  • 좋은 코드의 핵심은 코드를 알아보기 쉽게 하고 가독성을 높이는 것에 있다. 그러나, 헝가리안 표기법은 가독성에 큰 도움을 주지 못하며 오히려 해친다. 
  • 읽는 사람에게 잘못된 정보를 전달하여 버그를 만들 수 있다. 
  • 코드를 작성하는 사람에게는 비 객체 지향적인 코드를 작성하게 만든다. 또 변수 이름에 타입을 넣음으로써 어느 정도 정보를 전달했다고 믿게 만들고 변수명을 대충 짓게 만든다.
  • 타입 검사는 컴파일러에게 맡길 것. Visual Studio 2010부터는 컴파일 없이도 타입 체크가 되어 빨간 색으로 밑줄을 그어주니까 훨씬 편하다.
  • 만약 타입을 반드시 알아야 한다면 클래스 등으로 캡슐화한다거나 해서 추상화 수준을 올려보는 것이 좋다고 생각한다. 


---

2010년에 원고를 썼는데 7년이 지난 이제서야 퇴고하고 올린다.

반응형

책 세 권을 샀다.


패턴을 활용한 리팩터링, 클린 코드, More Effective C++이다. 



일단 C++을 다루는 프로그래머라면 Effective C++ 3판, More Effective C++, 마틴 파울러의 리팩토링 책은 거의 필수로 가지고 있어야 한다고 생각했기 때문에 이걸 사려고 했는데 Effective C++이랑 리팩토링은 현재 품절 상태였다.


품절이 되니까 조금 겁이 나는게, 이러다 절판되서 영영 구할 수 없으면 어쩌나 하는 생각이 들었다. 제발 나와줘 ㅠㅠ



게임 프로그래밍을 하면서 많이 듣는 말이 있는데, 라이브 게임은 소스 코드가 더러울 수 밖에 없다는 말이다. 하지만 난 그 말에 동의하지 않는다. 더러울 수 밖에 없다는 말에는 자포자기와 자기 합리화가 엿보이기 때문이다.


라이브 게임의 코드는 24시간 언제나 변한다. 마감도 항상 빠듯하고, 버그가 발생하면 타격도 크다. 따라서 코드를 수정하는 데 보수적이 되는 경향이 있는 것 같다.



처음엔 클래스가 작았는데, 사소한 기능을 추가하거나 변경하다 보니 새로운 클래스를 만들지 않고 조건문 등을 추가해서 기존의 클래스 내에 기능을 추가하는 경우가 많다. 메소드 하나 짜리 일을 클래스로 빼려면 심리적으로 부담이 되기 마련이다.


하지만 이게 누적이 되다 보니, 클래스가 신적인 존재 - Super Class 또는 God Class - 가 되는 경우가 많다. 물론 코드는 잘 동작한다. 단지 이해하기 힘들고 변경하기 힘들게 될 뿐.



나는 코드가 더러우면 결국엔 프로젝트가 망한다는 믿음을 가지고 있다. 지금 작업 중인 코드를 계속 리팩터링해나가고 있다. 수 년간 쌓여온 코드는 단지 한 가지 기능을 추가하거나 수정할 때 수십개의 파일 또는 클래스를 수정해야 할 정도이다. 


가혹한 환경과 조건에서 프로그래밍을 했던 선임들의 마음은 충분히 이해하지만, 그렇다고 해서 지금 그들의 코드를 그대로 놔두는 것 또한 같은 프로그래머로서의 도리가 아닌 것 같다.



클린 코드(Clean Code) 책 뒤편을 보면 이런 말이 있다.


나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직이 기어간다.


반응형

SW마에스트로 하면서 진행했던 5인 프로젝트.

프로그래머 4명(대학교 학부생)에 기획 1명으로 진행했다.

2D 비행 슈팅 게임을 만들자! 라는 나의 염원이 이루어진 첫 작품.

C++, DirectX, FMOD, Lua를 사용했다. 기간은 총 한 달. 회의하고 방황한 시간 빼고 본격적으로 코딩한 기간만 따지면 약 2주 정도 된다.

애초에 직접 리소스를 만들 생각이 없었기 때문에 이곳 저곳에서 줏어다 썼다. 특히 Strikers 1945 리소스가 많다. BGM은 배틀필드3.





프로젝트 페이지:
http://dev.naver.com/projects/caffeinea 


반응형

C++에서 C#처럼 enum을 쓰는 방법이 있다!

namespace 관련해서 자료를 조사하다가 나온건데...
(참고: C++ namespace(네임스페이스) 코딩 스타일)
 
 

Unreal쪽 자료를 찾아보다가 여길 보니 enum을 C#스타일로 쓰기 위해 namespace를 사용한다는 내용이 나온다. 나도 예전에 생각해봤던건데 진짜로 쓰는 데가 있었구나!

http://udn.epicgames.com/Three/CodingStandard.html


기존의 C++ enum은 다음과 같은 문제가 있었다.
enum Color
{
    Red,
    Green,
    Blue,
... } void Foo(Color color) { switch(color) { case Red: ... break; case Green: ... break; case Blue: ... break; } }

enum이 전역 공간을 돌아다닐 수 있기 때문에 이름 충돌이 일어날 가능성도 높고 - 예를 들면 어디선가 const string Red = "Red"; 같은거 선언하면 모호해진다 - enum보다는 마치 #define된 상수처럼 보이며, 같은 enum소속인지도 알아보기 힘들고, 같은 enum내에 다른 멤버들이 어떤 것이 있는지 알아보기 힘들며, 어디서 사용되는지 유추할 길이 없는 등의 단점이 있다.

약간의 꼼수로는 Color_Red, Color_Green과 같은 이름을 사용하는 방법이 있긴 하다. 나도 지금까지 이렇게 해왔지만 이제 바꿀 예정!


Scoped enum을 사용하는 방법은 다음과 같다.
 
namespace Color
{
    enum Type
    {
        Red,
        Green,
        Blue,
... } } void Foo(Color::Type color) { switch(color) { case Color::Red: ... break; case Color::Green: ... break; case Color::Blue: ... break; } }
이제 각 enum형 상수들이 소속을 가지게 되었다! Visual Studio같은 IDE를 사용한다면 F12등을 누른다거나 Ctrl+Space를 눌러서 Color의 다른 멤버 목록까지 볼 수 있다.

 
다른 방법 - 밑줄 쓰기

다만 변수를 만들 때 ::Type 이라는 것을 붙여 줘야 하는데 - Color::Type 처럼 - 이것도 싫다면 Color_Red, Color_Green 등올 하는 것이 나을 것으로 보인다.

또 밑줄을 사용할 경우, 단독으로 숫자를 사용할 수 있는 장점이 있다. 예를 들면 Keys_1, Keys_2 처럼 사용하는 것도 가능한데, 만약 namespace를 사용한다면 Keys::1, Keys::2 와 같은 방법은 1과 2처럼 숫자로 시작하는 식별자는 컴파일되지 않는다는 면에서 상대적으로 장점으로 볼 수 있다. 뭐 나같은 경우 Keys::D1, Keys::D2로 쓰고 있기는 하다.


 
반응형

중첩된 namespace는 어떻게 써야 하나?

오늘 코딩을 하다가 namespace를 중첩해서 만들어야 할 일이 생겼다. 내가 C#에 영향을 많이 받은지라, 각 namespace의 계층 구조를 만들어서 써보자는 생각을 했다.

그런데 문제가 생겼다. 이거 들여쓰기를 어떻게 해야 할 지 고민된다.

몇 가지 방법을 생각해냈다.

1번

namespace Foo { namespace Bar {
    class ImYourFather
    {
        ...
    }
} }
2번

namespace Foo
{
    namespace Bar
    {
        class INeedACupOfCoffee
        {
            ...
        }
    }
}

3번

namespace Foo
{
namespace Bar
{
class WatchWhereYouShooting
{
    ...
}
}
}



일단 namespace를 제시하는 방법과, namespace와 class의 들여쓰기 방법이 차이가 난다. 따라서 내가 여기에 제시한 것 말고도 여러 변형이 나올 수 있다만, 일단 저 세 개를 후보로 정했다.

1번은 namespace를 옆으로 나열한 것이다. packed 방식이라고 하더라. 들여쓰기는 한 번만 된다.
2번은 namespace마다 들여쓰기를 한 것이다.
3번은 namespace와 class 모두 들여쓰기를 하지 않은 것이다.

예상되는 장점과 단점을 생각해 보자면...

1번
장점
namespace의 계층 구조를 쉽게 파악할 수 있다. 들여쓰기는 한 번만 된다. Visual Studio의 자동 들여쓰기에 의존해도 되므로 편하다. C++은 C#과 달리 namespace를 namespace Foo::Bar {...} 로 선언할 수 없는 것을 가장 유사하게 극복하는 방법이다.

단점
내가 따르는 일반적인 C++ Coding Style을 위반한다. 보통 중괄호는 별개의 라인에 작성해야 한다.




2번
장점
Visual Studio가 알아서 들여쓰기를 해주므로 편하다.

단점
namespace 계층 구조가 깊어질 수록 들여쓰기가 많아진다.




3번
장점
namespace 때문에 들여쓰기가 중첩되는 현상이 없다. 즉, 수평 공간을 낭비하지 않는다.

단점
Visual Studio의 자동 들여쓰기 기능이 있기 때문에 수동으로 왼쪽에 정렬해줘야 하는 불편함이 따른다. 별 것 아니지만 귀찮다.
왠지 namespace만 들여쓰기를 하지 않는게 거슬린다.
계층 구조를 파악하기가 조금 힘들수도 있다.




아... 정말 고민된다!

다른 프로젝트들은 어떻게 하고 있을까?

내가 좋아하는 C#

C#쪽은 대체로 2번 규칙을 따르는 것으로 보인다. 근데 C#은 namespace 지정을 namespace Foo.Bar {...} 이렇게 할 수 있어서 나름 들여쓰기가 절약이 된다.


Google

Google의 경우 3번 규칙과 비슷하다. 다만 중괄호가 같은 라인에 있다.
http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces


Ogre3D

게임 쪽은 어떨까... Ogre3D를 봅시다.
Ogre3D의 경우 2번 규칙과 비슷하다. 중괄호가 namespace와 같은 라인에 있는 경우가 일반적으로 보인다. 별개의 라인에 중괄호가 위치하는 경우도 있기는 하지만 같은 라인에 위치하는 것이 보다 일반적으로 보인다. 좀 더 살펴본 결과 3번 스타일도 사용하는 것으로 확인되었다. 오픈 스스기 때문에 여러 사람이 작업해서 그런건지 스타일도 여러개다.

Ogre3D의 namespace는 계층이 최대 2단계까지만 중첩되기 때문에 어차피 들여쓰기 문제는 없는 것으로 보인다.


WebKit, Mozilla

외국쪽 사정도 비슷한듯... 들여쓰기에 대한 논쟁이 좀 있다.
http://bugreports.qt.nokia.com/browse/QTCREATORBUG-567

살펴보니 WebKit도 3번 규칙을 따른다. http://www.webkit.org/coding/coding-style.html
Mozilla도 3번 규칙을 따른다. https://developer.mozilla.org/En/Mozilla_Coding_Style_Guide


 VS2010에서 namespace indention 안하는 방법
http://stackoverflow.com/questions/3727862/is-there-any-way-to-make-visual-studio-stop-indenting-namespaces


Unreal Engine

사족: C++에서 C#처럼 enum 쓰기
Unreal쪽 자료를 찾아보다가 여길 보니 enum을 C#스타일로 쓰기 위해 namespace를 사용한다는 내용이 나온다. 나도 예전에 생각해봤던건데 진짜로 쓰는 데가 있었구나!

http://udn.epicgames.com/Three/CodingStandard.html


결론

이것 때문에 몇 시간을 다른 사람들은 어떻게 하고 있는지 찾는데 보냈다. 코딩은 언제 하나 ㅠㅠ

대개 3번 유형을 선택하고 있는 것으로 보이고 간혹 2번, 그리고 드물게 1번 스타일을 사용하는 것으로 보인다.

일단은 1번 스타일로 하려고 한다. 중첩된 namespace를 사용하기에는 가장 편리하고 적합한 것으로 보인다. 그리고 namespace 선언으로 발생하는 탭 하나 정도는 괜찮다. 어차피 C#도 보니까 그러더만... 너무 들여쓰기 많아져서 스트레스 받기도 싫고, 들여쓰기 집어넣느라 스트레스 받기 싫다.




반응형
회사에서 테스트 주도 개발(원제: 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