저자 : 로에디 그린(Roedy Green)
원문 : Canadian Mind Products
표면으로 올라오는 시간이 오래 걸리는 버그일수록 찾기가 어렵다.
- 로에디 그린(Roedy Green)
위장술, 숨기기, 어떤 것을 마치 다른 것처럼 보이게 하기 등의 기술은 유지보수 할 수 없는 코드에 필수적인 기법이다. 이런 기술 중 대다수는 사람의 눈이나 텍스트 편집기로는 알아채기 힘들다는 약점을 이용한다.
주석으로 위장한 코드와 코드로 위장한 주석
실제로는 주석처리 되었지만 얼핏 보면 주석처리 되지 않은 것처럼 보이게 할 수 있다.
for(j=0; j위 코드에서 다른 색으로 표시하지 않았다면 세 줄의 코드가 주석처리 되었다는 사실을 알 수 있었을까?/* 속도 향상을 위해 total += array[j+3]; * 루프의 코드를 길게 total += array[j+4]; * 펼쳐놓았다. total += array[j+5]; */ total += array[j+6 ]; total += array[j+7 ]; }
네임스페이스
C는 Struct/union와 typedef struct/union의 네임스페이스를 구별한다(그러나 C++에서는 구별하지 않는다). 구조체든 유니언 네임스페이스든 같은 이름을 사용하자. 가능하다면 둘이 서로 호환되게 하자.
typedef struct { char* pTr; size_t lEn; } snafu; struct snafu { unsigned cNt char* pTr; size_t lEn; } A;
매크로 정의를 숨겨라
자질구레한 주석을 이용해 매크로 정의를 숨길 수 있다. 보통 프로그래머라면 지루한 주석을 끝까지 읽지 않으므로 절대 매크로를 찾을 수 없다. 매크로를 만들 때는 다음과 같이 특이한 동작을 써서 평범한 할당문처럼 보이게 만들어야 한다.
#define a=b a=0-b
매우 바쁜 것처럼 보여야 한다
다음과 같이 define 문을 이용해서 함수를 만들고 매개변수는 그냥 주석 처리한다.
#define fastcopy(x,y,z) /*xyz*/ ... fastcopy(array1, array2, size); /* does nothing */
define문을 여러 줄에 걸쳐 기술하면서 변수를 숨겨라
나쁜 예,
#define local_var xy_z"xy_z"를 두 줄로 분리한 좋은 예,
#define local_var xy _z // local_var OK이렇게 하면 xy_z를 검색해도 나오지 않는다. C 전처리 프로그램은 줄의 끝 부분에 나오는 ""를 다음 줄로 이어진다는 의미로 해석한다.
키워드를 위장한 이름
문서화 할 때에는 "file "과 같이 파일명을 표시해야 하고 Charlie.dat" 또는 "Frodo.txt"처럼 파일명을 명백히 표시하지 말아야 한다. 되도록이면 가능한 한 예약어처럼 보이는 이름을 사용하는 것이 좋다. 예를 들어, "bank", "blank", "class", "const ", "constant", "input", "key", "keyword", "kind", "output", "parameter" "parm", "system", "type", "value", "var" and "variable " 등을 매개변수나 변수명으로 사용해야 한다. 실제 예약어를 임의적으로 사용하면 명령어 프로세서나 컴파일러가 처리를 거부할 수 있다. 이를 잘 활용하면 사용자는 우리가 만든 임의의 이름과 예약어를 혼동하게 만들 수 있다. 누군가 딴지를 걸면, 사용자가 각 변수의 이해를 적절히 돕기 위해 사용한 것이라고 발뺌하면 그만이다.
코드에 사용한 이름은 화면 표시 이름과 달라야 한다.
화면에 표시되는 값과 변수명은 전혀 관련이 없도록 해야 한다. 예를 들어, 화면에는 "Postal Code"로 표시되는 변수의 이름을 "zip"과 같이 색다르게 정할 수 있다.
이름을 변경하지 마라
전체적으로 이름을 바꾸는 방법으로 두 섹션 코드를 동기화하는 것보다는 같은 심볼에 여러 TYPEDEF문을 사용하는 것이 바람직하다.
금지된 지역변수를 감추는 방법
전역 변수는 "악"과 같은 존재이므로 전역적으로 사용할 모든 데이터를 저장할 구조체를 정의하고 EverythingYoullEverNeed와 같이 똘똘한 이름을 붙여준다. 모든 함수가 이 구조체에 대한 포인터(포인터명은 handle이라고 함으로써 혼란을 더할 수 있다)를 갖게 할 수 있다. 실제로는 "handle"을 통해 전역변수를 마음껏 사용하면서 다른 이에게는 우리가 전역 변수를 사용하지 않는다는 인상을 줄 수 있다. 전역 변수를 사용하는 모든 코드에서 정적 변수를 선언하는 것도 좋은 방법이다.
동의어로 인스턴스 숨기기
유지보수 프로그래머가 뭔가를 수정하고 그로 인해 발생할 수 있는 부수효과를 확인할 때 일반적으로 프로그램 전체에서 사용된 변수명을 검색할 것이다. 동의어 사용이라는 간단한 방법으로 이러한 유지보수 프로그래머의 시도를 좌절시킬 수 있다.
#define xxx global_var // in file std.h #define xy_z xxx // in file ..othersubstd.h #define local_var xy_z // in file ..codestdinst.h위 정의를 서로 다른 include 파일에 흩어놓아야 한다. 특히 include 파일이 서로 다른 디렉터리에 위치한 경우 효과적이다. 가능한 모든 범위에서 이름을 재사용하는 기법도 있다. 컴파일러는 정확하게 모든 이름을 구별할 수 있겠지만, 단세포적인 텍스트 검색기로는 이름을 구별하기 어려울 것이다. 불행하게도 SCID(Source Code in Database)가 점점 발전하면서 편집기가 컴파일러처럼 범위 규칙을 이해하게 되면 간단한 기법은 더 이상 사용할 수 없게 될 것이다.
길고 비슷한 변수명
변수명이나 클래스명은 되도록이면 길게 만들고 두 개 이상의 이름이 필요할 경우 한 글자만 바꿔놓거나 대소문자만 다르게 한다. 변수명 swimmer와 swimner는 좋은 예다. 대부분의 폰트로는 ilI1|나 oO08를 명확하게 구별하기 어렵다는 점을 악용하자. 예를 들어, parselnt와 parseInt 혹은 D0Calc와 DOCalc를 명확히 구분하기 어렵다. 이 중에서도 l은 얼핏 보기에 1과 구별하기 힘들기 때문에 변수명으로 사용하기 가장 좋은 알파벳 중 하나다. 뿐만 아니라 대부분의 폰트에서 rn은 m처럼 보이는 경우가 많다. 따라서 swimmer와 쉽게 구별하기 어려운 swirnrner도 좋은 변수명이다. HashTable과 Hashtable처럼 한 글자의 대소문자만 살짝 변경해서 변수명을 만드는 것도 좋은 방법이다.
비슷하게 발음되고, 비슷하게 보이는 변수명
xy_z라는 변수명 이외에 xy_Z, xy__z, _xy_z, _xyz, XY_Z, xY_z, Xy_z처럼 다양한 변수명을 사용하지 말라는 법은 없다.
때로는 변수명을 소리나는 대로 혹은 스펠링으로 기억하는 프로그래머를 많이 볼 수 있는데 대소문자나 밑줄로만 구별되는 변수명이 이들을 혼란에 빠뜨릴 것이다.
오버로드 그리고 당황
C++에서 #define을 사용해 라이브러리 함수를 오버로드하자. 얼핏 보면 친숙한 함수를 쓰고 있는 것처럼 보이겠지만 사실은 완전 다른 기능을 하게 할 수 있다.
효율적인 오버로드 연산자 선택하기
C++에서 +,-,*,/ 등과 같은 연산자를 사칙 연산의 의미와 전혀 관련 없는 동작을 하도록 오버라이드 하자. 스트로우스트룹(Stroustroup)도 쉬프트 연산자를 I/O에 사용했는데 우리도 그처럼 창의적이지 못할 이유가 없지 않은가? +를 오버로드 할 때에는 i = i + 5;가 i += 5;와 같은 의미를 갖지 않도록 해야 한다. 최첨단 연산자 혼란 오버로딩 기법을 소개하겠다. 클래스의 ‘!’ 연산자를 오버로드 하면서 뭔가를 뒤집거나 부정하는 동작과는 아무 관련이 없는 동작을 하게 하는 것이 핵심이다. ‘!’ 연산자가 정수를 반환하게 한다. ‘!’를 논리 연산자로 사용하려면 ‘! !’로 표기해야 한다. 그러나 ‘!’ 연산 자체가 로직을 변경시키므로 결국 하나를 더 붙여서 ‘! ! !’를 사용해야 한다. 여기서 말하는 ! 연산자는 불린 값 0이나 1을 반환하는 연산자로 비트단위의 논리 부정 연산자 ~와 혼동하지 말자..
new를 오버로드하라
"new" 연산자를 오버로드하라. 이는 +-/*를 오버로드하는 것보다 훨씬 위험하다. 기존 함수를 뭔가 다른 기능(그러나 오브젝트의 기능에 필수적인 함수이므로 변경하기 쉽지 않다)으로 오버로드한다면 큰 혼란을 야기할 수 있다. 사용자가 동적 인스턴스를 생성할 때에 온전한 인스턴스가 아닌 잘려나간 인스턴스 조각만 얻게 하는 것이 핵심이다. "New"라는 멤버 변수를 추가하므로 대소문자를 이용한 혼란 기법을 가미할 수 있다.
#define
C++의 소스코드 판독을 어렵게 하는데 #define의 활용도는 무궁무진해서 이에 대한 내용만 따로 집필할 수 있을 정도다. 소문자로 된 #define 변수로 원래 변수를 대체할 수 있다. 선처리 함수에는 절대 파라미터를 사용하지 말아야 한다. 전역 #define으로 원하는 모든 기능을 수행하자. 누군가는 #define을 활용해서 실제 컴파일이 이루어질 때까지 CPP를 다섯 번 통과하게 만들었다고 한다. 필자가 들어본 사례 중 가장 창의적인 활용방법이다. 똘똘하게 define과 ifdef를 사용해 각 헤더 파일에서 몇 번이나 해당 구문을 include했느냐에 따라 결과가 달라지게 할 수 있고, 이로써 코드는 혼란의 경지에 이르게 된다.
#ifndef DONE #ifdef TWICE // 세 번째 정의 내용 void g(char* str); #define DONE #else // TWICE #ifdef ONCE // 두 번째 정의 내용 void g(void* str); #define TWICE #else // ONCE // 첫 번째 정의 내용 void g(std::string str); #define ONCE #endif // ONCE #endif // TWICE #endif // DONE이제 얼마나 많이 헤더를 include했느냐에 따라 결과가 달라지므로 g() 함수에 char*를 전달해 호출하면 어떤 재미있는 일이 벌어지는지 구경하는 일만 남았다.
컴파일러 지시어
컴파일러 지시어는 같은 코드를 상황에 따라 다르게 동작하도록 만들어졌다. Boolean 쇼트 서킷 지시어와 long strings 지시어를 반복적으로 줄기차게 껐다 켜기를 반복하자.
다음회 계속
최신 콘텐츠