저자 : 로에디 그린(Roedy Green)
원문 : Canadian Mind Products
끈기를 가지고 혼잡성, 수퍼 복잡성, 장황함을 피하라.
읽을 수 없는 C
인터넷의 읽을 수 없는 C 경연대회에 참석하고 스승님의 책상다리 근처 앉아 경청하라.
방법을 찾거나 APL 권위자를 찾으라
이 세계에서는 코드는 간결하고 동작을 괴이할수록 더 많은 존경이 따라올 것이다.
나는 수십 개를 이용할 것이다
아무 문제없이 두 개 혹은 세 개를 이용할 수 있는 상황에서 문제를 처리하는 한 개의 변수만을 사용하지 말라.
알려지지 않은 자
같은 작업을 수행하더라도 가장 알려지지 않은 방식을 사용하라. 예를 들어, 배열을 이용해 정수를 문자열로 변경하는 대신 다음과 같은 코드를 이용할 수 있다.
char *p; switch (n) { case 1: p = "one"; if (0) case 2: p = "two"; if (0) case 3: p = "three"; printf("%s", p); break; }
일관성에 대한 고집은 아량이 없는 말썽쟁이 요정이나 하는 짓이다
" ", 32, 0x20, 040와 같이 하나의 문자 상수를 다양한 방법으로 표현할 수 있다. C나 자바에서는 10과 010을 동일시 하지 않는다는 사실을 자유롭게 활용하길 바란다.
형변환
모든 데이터는 void *로 넘겨서 적절한 구조체로 형변환해서 사용한다. 구조체로 형변환하지 않고 바이트 오프셋을 이용한 데이터 접근하는 것도 색다른 재미가 있다.
중첩된 Switch
중첩된 switch 구문(switch문 내의 switch)은 사람을 가장 혼란스럽게 하는 중첩 가운데 하나다.
암시적 변환을 악용하라
프로그래밍 언어에서 제공하는 모든 미묘한 암시적 변환 규칙을 기억한 다음 그들을 최대한 활용하자. 절대 picture(코볼이나 PL/I에서) 변수를 사용하지 말고 일반적인 변환 루틴(C의 sprintf와 같은)도 사용하지 말자. 소수점 변수를 배열 인덱스로, 문자를 루프 카운터로, 문자열 함수를 숫자에 사용해보자. 잘 정의한 함수 덕분에 우리 코드는 한결 단순해진다. 우리가 만든 코드를 유지보수 할 프로그래머는 암시적 데이터 형 변환 전체 장을 읽어야만 하는 기회를 제공해 준데 대해 우리에게 감사할 것이다. 물론 대부분의 프로그래머가 이전에도 암시적 데이터 형 변환을 살펴봤을 테지만, 아마도 다시 공부해야 할 필요성을 절실히 느꼈을 것이다.
정수(int> 그대로
ComboBox를 사용하면서 switch 구문이 필요할 때에는 이름을 붙인 상수를 사용하는 것보다는 정수를 그대로 사용하는 것이 바람직하다.
세미콜론!
문법적으로 허용된 곳이라면 어디에든 세미콜론을 사용하라. 아래 예를 살펴보자.
if(a); else; { int d; d = c; } ;
8진법을 활용하라
아래 예에서 볼 수 있듯이 10진수 리스트에 8진수를 몰래 추가하는 것이 핵심이다.
array = new int []
{
111,
120,
013,
121,
};
간접적으로 변환하라
자바는 변환이 필요할 때마다 코드를 혼잡하게 만들 기회를 제공한다. 간단한 예로 double을 String으로 변환해야 한다면 Double.toString(d)보다는 우회적인 방법을 사용해서 new Double(d).toString()와 같이 하는 것이 좋다. 물론 예제에서 설명한 것보다 더 우회적인 방법을 사용해도 된다! 변환 대필자가 제안하는 변환 기법은 되도록 피해야 한다. 변환하면서 생긴 임시 오브젝트로 힢(heap) 메모리를 낭비하므로 추가 점수를 획득할 수 있음을 잊지 말자.
중첩
가능한 한 깊이 중첩하라. 훌륭한 독자는 한 라인에 사용한 10개의 ()를 이해할 수 있고, 하나의 메소드에 사용한 20개의 {}도 이해할 수 있다. C++의 경우 전처리기 중첩을 이용할 수 있다. 전처리기 중첩은 코드의 중첩 구조와는 독립적이므로 보다 강력한 옵션이다. 인쇄된 코드에서 블록의 시작과 끝이 같은 페이지에 나오지 않게 할 수 있다면 추가 점수를 획득할 수 있다. 가능하다면 중첩된 if 문을 중첩된 [A?B…] 구문으로 바꾸는 것이 바람직하다. 게다가 한 줄 이상의 코드로 확장할 수 있다면 금상첨화다.
숫자 기호
100개의 요소를 갖는 배열이 있다면 최대한 100이라는 기호를 프로그램에서 가능한 많이 사용해야 한다. 절대 static final을 붙인 상수로 100을 대체하거나 myArray.length와 같은 방법을 사용하지 말아야 한다. 상수를 이해하고 대체하기 어렵게 하려면 100/2보다는 50, 100-1보다는 99와 같이 표기하는 것이 좋다. a > 100 보다는 a == 101을 사용하고 a >= 100보다는 a > 99을 사용하므로 100이라는 숫자가 사용되지 않은 것처럼 위장할 수 있다.
헤더에 x개, 바디에 y개, 푸터에 z개 라인을 사용한 페이지 크기가 있다고 가정해보자. 이 때 난독화를 각각에 적용할 수 있으며 부분합 혹은 전체합에도 적용할 수 있다.
이 유서 깊은 기법은 특히 100개의 요소를 갖는 연관성이 없는 두 배열을 갖는 프로그램에 효과적이다. 유지보수 프로그래머가 둘 중 하나의 배열 크기를 변경하려면 프로그램에서 사용된 모든 100이라는 기호를 해독해서 어떤 배열에 해당하는 것인지 이해해야 한다. 이러한 과정에서 그가 적어도 한 번 이상 실수를 저지를 것이라고 장담할 수 있다. 운이 좋다면 그 에러가 몇 년이 지난 후에야 발견될 수도 있을 것이다.
더 사악한 변형 기법도 있다. 매우 가끔 "우발적으로" 100을 사용하므로 유지보수 프로그래머를 안심시킬 수 있다. 가장 사악한 방법은 100이나 이름을 붙인 상수를 쓰지 않고 산발적으로 우연히 100 값을 갖는 상수를 사용하는 것이다. 당연히 배열의 크기를 연상시키는 어떠한 이름도 사용하지 말아야 한다.
C의 기이한 배열 접근법
C 컴파일러는 myArray[i]를 *(myArray + i)로 변환하는데 이는 *(i + myArray)과 같고 이는 i[myArray]과 같다. 전문가들은 이와 같은 사실을 어떻게 활용해야 할지 알고 있다. 인덱스를 생성하는 함수를 이용하므로 이 사실을 숨길 수 있다.
int myfunc(int q, int p) { return p%q; } ... myfunc(6291, 8)[Array];불행히도 이 기법은 네이티브 C 클래스에서만 사용할 수 있으며 자바에서는 사용할 수 없다.
긴 줄
가능하면 많은 내용을 한 줄에 담으려 노력하라. 이 기법은 임시 변수를 줄이고, 줄 바꿈 문자나 공백 문자를 제거함으로써 소스 파일 크기를 줄이는 효과를 제공한다. 좋은 프로그래머는 몇몇 편집기의 한 줄에 들어갈 수 있는 문자 크기의 한계인 255 문자까지 활용하기도 한다. 글자 크기를 6으로 맞추었을 때 글자를 읽지 못한다면 스크롤해서 읽도록 길게 코딩하는 것이 정석이다.
예외
거의 알려지지 않은 코딩 비밀을 공유하려고 한다. 예외의 이면에는 고통이 숨겨져 있다. 제대로 구현한 코드는 실패할 일이 없으므로 예외 자체가 필요 없게 된다. 따라서 예외에 시간낭비 할 필요가 없다. 예외를 상속하는 것은 자신의 코드에 문제가 있음을 인정하는 무능력자가 하는 짓이다. 일반적으로는 전체 응용 프로그램을 감싸는 하나의 try/catch를 사용(main에서)하고 문제가 생겼을 때 System.exit()를 호출함으로써 프로그램을 단순하게 만들 수 있다. 예외를 발생시킬 수 있는지 여부와 관계없이 모든 메소드 헤더에 표준 throws 구분을 추가하자.
언제 예외를 사용해야 하는가?
예외가 발생하지 않는 상황에서만 예외를 사용하라. 주기적으로 루프를 종료하면서 ArrayIndexOutOfBoundsException를 발생시킬 수 있다. 그리고 아무 일도 없는 것처럼 예외가 발생한 메소드에서 표준 결과값을 넘긴다.
스레드에 떠넘기라
제목 그대로 행동하라.
변호사 코드
뉴스 그룹에서 까다로운 코드가 어떻게 수행돼야 하는지를 두고 논쟁을 하는 경우가 있다(예를 들어, a=a++;, f(a++,a++);). 이와 같이 논쟁이 벌어지는 코드를 우리 프로그램에 자유롭게 사용하자. 아래와 같은 선/후 감쇠 코드를 어떻게 처리해야 하는지는 언어 스펙에서 정의하지 않는다.
*++b ? (*++b + *(b-1)) 0따라서 컴파일러마다 각자 임의로 위 수식을 평가한다. 비슷한 방법으로 모든 공백을 없애서 C와 자바의 복잡한 토큰화 규칙을 이용할 수도 있다.
중간 반환문
Goto를 사용하지 않고, 중간에 반환하지 않으며, label을 사용하지 않는다는 규칙을 엄격히 지켜야 한다. 그러나 우리가 최소한 5 단계 이상으로 중첩하는 if/else문을 만들 수 있다면 이 규칙을 꼭 지키지 않아도 된다.
Avoid {}
문법적으로 어쩔 수 없는 경우가 아니라면 절대로 if/else 블록을 감싸는 괄호 { }를 사용하지 말자. 들여쓰기 없이 if/else와 블록을 여러 단계로 중첩한다면 전문 유지보수 프로그래머라도 가볍게 해치울 수 있다. 펄(Perl)을 사용하면 이 기법의 효과가 극대화된다. 일반 코드 뒤에 if문을 양념으로 추가하는 것도 좋은 방법이다.
지옥에서 온 탭
탭을 과소평가하지 마라. 탭이 얼마의 공간을 나타내야 하는지에 대한 표준이 없는 회사에서는 공백 대신에 탭을 사용하므로 큰 혼란을 야기할 수 있다. 스트링 문자에 탭을 추가하거나 공백을 탭으로 변환해주는 툴을 돌리는 것도 좋은 방법이다.
마법 같은 행렬 위치
특정 행렬 값을 플래그로 사용해보자. 변환 행렬에서 동종 좌표 시스템을 이용한 [3][0]에 위치한 요소 등은 좋은 예이다.
마법 같은 행렬 슬롯의 개선
주어진 형식의 여러 변수가 필요할 때에는 변수를 배열로 정의하고 숫자로 각 변수를 접근할 수 있다. 이 때 숫자 규칙은 자신만이 아는 것으로 정하고 문서화하지 않는다. 또한 고생스럽게 #define 상수로 인덱스를 정의할 필요도 없다. 전역 변수 widget[15]는 취소 버튼이라는 사실은 누구나 당연히 알고 있어야 한다. 이는 어셈블러 코드에서 절대적 숫자 주소를 접근할 때 최근 이용하는 방식이다.
아름다움을 멀리하라
절대 자동 코드 정렬 기능으로 코드를 정렬하지 말자. PVCS/CVS(버전 관리를 이력을 추적하는)에서 가짜 정보를 만드는 이와 같은 도구를 사용하지 않도록 회사 현장에서 로비를 하자. 또는 모든 프로그래머는 자신이 만드는 모듈에 영원히 신성불가침한 자신만의 들여쓰기 방식을 가져야 한다고 주장하자. 다른 프로그래머에게는 이러한 방식을 따르더라도 각자가 만든 모듈에서만 이와 같은 특유의 규칙이 나타나므로 별로 신경 쓸 일이 아니라고 설득하자. 아름다움을 멀리하는 방법은 간단하다. 아름다움을 멀리하므로 수동 정렬로 인한 수백만 번의 키 입력을 아낄 수 있고, 정렬이 엉망인 코드를 오역하는 바람에 며칠을 낭비하는 일도 사라진다. 공통 저장소에 코드를 저장할 때 뿐 아니라 편집할 때에도 모두가 같은 정렬 형식을 사용해야 한다고 주장하자. RWAR과 상사를 먼저 설득하자. 아마도 평화를 위해 자동 정렬 기능은 사용 금지될 것이다. 자동 정렬이 금지되었으면 루프나 if문의 바디가 실제보다 길어 보이거나 짧아 보이게 정렬할 수 있다. 혹은 else 문을 다른 if와 연관되는 것처럼 보이게 할 수도 있다. 물론 모든 일은 우발적으로 일어나는 것일 뿐이다.
if(a) if(b) x=y; else x=z;
매크로 전처리기
매크로 전처리기는 코드 난독화를 할 좋은 기회를 제공한다. 핵심은 매크로 확장을 여러 단계에 거쳐 확장하고 여러 *.hpp 파일을 이용해야 뜻을 파악할 수 있게 만드는 것이다. 실행할 코드를 매크로에 넣은 다음 모든 *.cpp 파일에서 이 매크로를 가져다 사용하므로(매크로를 쓰지 않더라도 이렇게 하는 것이 좋다) 코드가 변경될 때마다 다시 컴파일 해야 할 코드 양을 극대화 할 수 있다.
일관성 부족을 악용하라
자바에서 배열 선언은 정말 어지럽다. 예전 C 형식으로 할 수 있고, String x[]와 같은 방식도 가능하며(배열 표시 위치가 앞-뒤로 혼합된 형태), String[] x(배열 표시가 앞쪽에 오는 형태)와 같이 선언할 수 있다. 사람들에게 혼란을 주려면 다음과 같이 여러 표기법을 혼합한다.
byte[ ] rowvector, colvector , matrix[ ];위 코드는 다음과 같다.
byte[ ] rowvector; byte[ ] colvector; byte[ ][] matrix;
에러 복구 코드를 숨겨라
중첩을 특정 함수 호출의 에러를 복구하는 함수를 가능한 한 멀리 배치할 수 있다. 아래 간단한 예제를 좀 더 수정하면 10단계나 12단계 중첩으로 확장할 수 있다.
if ( function_A() == OK ) { if ( function_B() == OK ) { /* 일반 종료의 경우에 처리 코드 */ } else { /* Function_B에 대한 에러 복구 코드 */ } } else { /* Function_A에 대한 에러 복구 코드 */ }
가짜(pseudo) C
#define 의 본래 목적은 다른 프로그래밍 언어에 익숙한 사람이 C로 쉽게 변환할 수 있도록 하는 것이다. 따라서 #define begin { " 또는 " #define end }과 같은 선언문을 이용하면 아주 신선한 코드를 쉽게 만들 수 있다.
혼란을 안겨주는 import
패키지에서 우리가 어떤 메소드를 사용하는 것인지 유지보수 프로그래머에게 직접 알려주지 말고 추측하게 할 수 있다. 즉 아래와 같이 명시하지 말고,
import MyPackage.Read; import MyPackage.Write;이렇게 사용한다.
import Mypackage. *;절대 클래스나 메소드에 패키지명을 포함한 전체 이름을 사용하지 않는다. 유지보수 프로그래머가 우리 코드를 보면서 각각이 어떤 패키지/클래스에 속한 것인지 추측하게 만들자. 물론 경우에 따라 전체 패키지 이름을 사용하거나 import 구문 사용방식을 바꾸어 주면 더욱 효과적이다.
변기 배관
어떤 경우에도 함수나 프로시저가 한 화면에 모두 보이도록 하지 말아야 한다. 간단한 루틴에는 아래 설명하는 기법을 사용하자.
일반적으로 코드의 논리적인 블록을 구분하는데 빈 줄을 사용한다. 우리 코드의 모든 라인은 각각이 논리적인 블록이다. 따라서 모든 줄에 빈 줄을 추가한다.
주석을 코드 끝에 추가하지 말고 윗부분에 추가하라. 어쩔 수 없는 강요에 의해 코드의 뒷부분에 추가해야만 하는 상황이라면 전체 파일에서 가장 긴 줄을 찾아 10개의 공백을 넣고 모든 end-of-line 주석을 왼쪽 정렬한다.
프로시저의 윗부분의 주석에 사용할 템플릿은 적어도 15줄 이상이어야 하고 빈 줄도 자유롭게 추가한다. 아래는 간단한 예제 템플릿이다.
/*
/* 프로시저 이름:
/*
/* 원래 프로시저 이름:
/*
/* 저자:
/*
/* 생성일:
/*
/* 수정일:
/*
/* 수정한 사람:
/*
/* 원래 파일명:
/*
/* 목적:
/*
/* 의도:
/*
/* 직함:
/*
/* 사용한 클래스:
/*
/* 상수:
/*
/* 지역변수:
/*
/* 파라미터:
/*
/* 만든 날짜:
/*
/* 목적:
*/
문서에 여러 번 중복 정보를 넣는 기법을 사용하면 해당 정보를 최신으로 유지하기 어려워 진다. 이를 순진하게 믿는 유지보수 프로그래머는 금방 혼란에 빠질 것이다.
다음회 계속 이전 글 : 탁월한 리더 "딥 스마트"가 되려면...
최신 콘텐츠