“유지보수하기 어렵게 코딩하는 방법”은 신입 개발자일 때 만난 책입니다. 부제는 “평생 개발자로 먹고 살 수 있다. “입니다. 아주 마음에 드는 제목이죠.
태아 작명법의 새로운 용도
태아 작명법 서적을 구입하자. 그러면 변수명을 뭐로 지어야 할지에 대한 고민을 덜 수 있을 것이다. Fred는 멋진 이름이며 입력하기도 쉽다. 입력이 쉬운 변수명을 원한다면 asdf를 사용해 보기 바란다.
“…이게 무슨 내용이지?”
이 책에서 다루는 첫 번째 예시를 보고 든 생각이었습니다. 가벼운 내용에 별생각 없이 재미있게 웃으며 저자의 다양한 생각을 즐겼습니다. 그리고 몇 년이 지난 뒤, 다시 이 책을 만났을 때는 재미보다 공감의 감정이 더 크게 다가왔습니다.
아래에서 기억에 남는 몇 가지 내용들을 이야기하려 합니다. 이 내용들은 따라 하면 안 됩니다. “오 그런가 보다.” 하고 설득당하시면 안 됩니다.
코드 한 줄을 작성할 때도 고민 또 고민해야 합니다. 진심으로 여기에 있는 규칙들을 따른다면 본인 외에는 누구도 코드를 유지보수 할 수 없게 됩니다. 즉, 평생직장을 얻을 수 있습니다. (혹은 본인도 코드를 유지보수 할 수 없게 됩니다.)
일반 규칙
모든 내용은 “유지보수 담당자의 생각하는 방식을 이해하자”에서 시작합니다. 유지보수를 담당하는 개발자를 좌절시키려면 그가 생각하는 방식을 우선 이해해야 하기 때문이죠.
유지보수 담당 개발자는 대체로 거대한 프로그램을 넘겨받습니다. 전체 코드를 읽기는 힘들기 때문에 해당 프로그램의 이해도가 프로그램을 작성한 우리보다는 낮은 편 일 겁니다. 아마도 그는 가능한 한 빨리 수정할 곳을 찾아 코드를 수정한 다음, 해당 수정으로 인한 Side Effect가 없는지 확인하고 작업을 마무리하려 할 것입니다.
유지보수 개발자는 망원경을 통해 우리 코드 내부를 살펴보는 것과 같은 상황에 부닥쳐 있습니다. 그가 우리 코드의 일부분들을 보면서 이해할 때 전체적인 그림을 그려낼 수 없게 하는 것이 핵심입니다. 그가 찾고 있는 코드를 가능한 찾기 어렵게 만들어야 하며, 더욱 중요한 사실은 그가 안심하고 코드를 무시할 수 있도록 가능한 한 서툴게 코드를 작성해야 한다는 점입니다.
개발자는 규약(Convention)을 통해 안심하는 습성이 있습니다. 간혹 조금이라도 규약 위반을 발견하면 돋보기를 들고, 코드 전체 라인을 샅샅이 조사할 가능성이 큽니다. 언어의 기능을 적절하게 오용해야 유지보수가 어려운 코드를 만들 수 있습니다.
다시 한번 이야기 하겠습니다.
“이렇게 하지 마세요.”
이름 짓기
변수와 함수의 이름을 짓는 방법은 유지보수하기 힘든 코드를 작성할 때 상당이 중요한 기술입니다. 이를 통해 유지보수 개발자의 정신을 혼미하게 만드는 방법들을 알아보겠습니다.
창의적 오타, 다른 언어의 이름 활용
뭔가를 설명하는 변수명이나 함수명을 사용해야 하는 상황이라면 오타라는 무기를 선택해 봅시다. 몇몇 함수명과 변수명에 오타를 내고 다른 곳에서는 오타를 사용하지 않는다면 grep 나 IDE 검색 기능을 효과적으로 무력화할 수 있습니다.
다른 언어의 이름을 활용하면 유지보수 개발자가 의미를 해독하면서 다양한 문화를 경험할 수 있게 만들 수 있죠.
void SetWnidowFlag(); // Window 오타
void SetColour(); // 색상(color)의 영국식 단어
추상화
it, everything, data, handle, stuff, do, routine, perform, 숫자 값 등과 같은 추상적인 단어를 사용해 보는 건 어떨까요? 이름을 봐도 동작을 전혀 유추할 수 없을 겁니다.
void PerformDataFunction();
void DoRoutineX3();
bool CanHandleStuff();
줄임말
줄임말로 코드를 간결하게 만들어 보세요. 무엇을 줄인 건지는 촉(?)으로 알아맞히는 거죠! 같은 단어를 줄임말과 줄임말 아닌 단어를 반복적으로 사용한다면 검색을 더욱 어렵게 만들 수 있습니다.
void PDF(); // PerformDataFunction() 의 앞 글자로 약어를 만듦
void printTGIF(); // 음식점이 아닌 Thank Got it's Friday 불금이다 의 뜻
String GetLastEducation(); // 여기서는 Education을 사용했으나 아래에서는
void SetLastEDC(String lastEDC); // Education 줄임말 EDC를 사용함
유의어 사전
되도록 많은 단어가 같은 동작을 가리도록 하는 것도 지루함을 달랠 좋은 방법의 하나입니다. 실제 의미상 차이가 없는 단어라 할지라도 알 듯 말 듯 하게 모호한 느낌을 제공해 봅시다.
// 아래 함수들의 차이점이 뭘까요?
bool DisplayWindow();
bool ShowWindow();
위장술
어떤 것을 마치 다른 것처럼 보이게 하는 기술은 유지보수 할 수 없는 코드의 필수적인 기법입니다. 이런 기술 중 대다수는 사람의 눈이나 텍스트 편집기로는 알아채기 힘들다는 약점을 이용합니다.
이름을 변경하지 않기
전체적으로 이름을 바꾸는 방법으로 두 세션 코드를 동기화하는 것보다는 같은 심볼에 여러 typedef 문을 사용하는 것이 바람직합니다.
세션 A
status
세션 B
condition
세션 A + B
typedef status condition;
typedef condition status;
이렇게 하면 세션 A와 세션 B 코드를 거의 수정하지 않고 동기화한 뒤 추가 코드도 쉽게 작성할 수 있습니다.
길고 비슷한 변수 명
변수명이나 클래스명 두 개 이상의 이름이 필요한 경우 한 글자만 바꿔놓거나 대소문자를 다르게 해보는 것도 좋습니다.
파일 A
Class HashTable {
...
}
파일 B
Class Hashtable {
...
}
HashTable 자료구조를 사용할 때, 파일 A에서는 HashTable 이름의 클래스를 만들고 다른 파일 B에서는 Hashtable 클래스 명을 사용해 봅시다. 그리고 A와 B를 다양하게 사용하여 나중에 코드를 봤을 때, 무엇을 사용하는 게 맞는지 혼란스럽게 만들 수 있습니다.
동의어로 인스턴스 숨기기
유지보수 개발자가 뭔가를 수정하고 그로 인해 발생할 수 있는 Side Effect를 확인할 때 일반적으로 프로그램 전체에서 사용된 변수명을 검색할 것입니다. 동의어 사용이라는 간단한 방법으로 이러한 유지보수 개발자의 시도를 좌절시킬 수 있습니다.
#define xxx global_var // in file std.h
#define xy_z xxx // in file ..\other\substd.h
#define local_var xy_z // in file ..\codestd\inst.h
위 정의들을 서로 다른 헤더 파일에 흩어놓아야 합니다. 특히 헤더 파일들이 서로 다른 폴더에 있는 경우 더 효과적입니다.
문서화
컴퓨터는 주석과 문서화 부분은 무시합니다. 따라서 온 힘을 기울여 주석과 문서화를 활용한다면 불쌍한 유지보수 개발자에게 좌절시킬 수 있습니다.
주석에 거짓말을 추가하기
적극적으로 거짓말을 할 필요는 없습니다. 그냥 자연스럽게 주석을 업데이트하지 않아 내용이 맞지 않는 것처럼 보이게 하면 됩니다.
사실을 문서화하기, 이유는 빼고 “어떻게”에 대해서만 문서화하기
코드에 /* Add 1 to i */ 와 같은 사실을 담은 양념을 추가해 봅시다.
/* Add 1 to i */
i = i + 1;
...
// 자식 컨트롤을 시리얼라이즈 한다.
if(childControl.Serializable()) {
childControl.Serialization();
}
프로그램이 무엇을 하는지에 대한 세부 사항과 프로그램이 무엇을 하지 않는지 문서화를 해봅시다.
버그가 생기면 수정을 담당하는 개발자는 어떻게 수정해야 하는지 알 수 없게 됩니다.
여기서 중요한 점은 패키지나 함수의 전체 목적과 같이 어려운 부분은 절대 문서화하지 않는다는 사실입니다.
“명백하게” 문서화하지 않기
예를 들어, 항공기 예약 시스템을 구현하고 있는데 다른 항공편을 추가하려면 25군데를 수정해야 한다고 가정해 봅시다. 물론 어디를 수정해야 할지를 문서화하면 안 됩니다. 나중에 누군가 우리 코드를 수정하려면 전체 라인을 완벽하게 이해해야만 원하는 수정을 할 수 있을 것입니다.
문서화 템플릿의 적절한 활용
함수 문서화 프로토타입을 이용해 자동으로 코드에 문서화 템플릿을 제공할 수 있습니다. 이때 다른 함수(혹은 함수나 클래스)에서 복사해서 사용하고 절대 필드에는 문서화 템플릿을 사용하지 말아야 합니다. 어쩔 수 없이 필드에 문서화 프로토타입을 사용하게 된다면 모든 함수에서 같은 파라미터 이름을 사용하도록 하는 방법이 좋습니다.
측정 단위
인치, 피트, 미터와 같은 측정 단위를 변수, 입력, 출력, 매개변수에 문서화는 절대 하지 않아야 합니다. 마찬가지로 변환 상수의 측정 단위값이 어떻게 전달되는지 등도 문서화하지 않습니다. 좀 더 사악한 방법을 원한다면 새로운 유닛 단위를 만들어서 사용하는 방법도 좋은 방법입니다. 누군가 이러한 방식에 이의를 제기한다면 “소수점 연산보다 정수 연산을 잘할 수 있도록 작업한 내용입니다. “ 와 같은 동문서답을 이용해 보세요.
문제점
코드의 문제점을 문서화하지 않습니다. 클래스에 버그가 있을 수 있다는 사실을 발견했으면 혼자만의 비밀로 간직해야 합니다. 코드를 어떻게 재조직하거나 재작성해야 할지 아이디어가 떠올랐을지라도 문서로 남겨 놓지 말아야 합니다. 정 뭔가 남기고 싶다면 “수정해야 함”과 같이 익명의 주석을 활용해 봅시다. 특히 이 주석이 어느 부분을 가리키는지 명확하지 않을수록 좋습니다.
프로그램 디자인
유지보수가 쉬운 코드 작성의 핵심 요소는 응용프로그램의 각 요소를 한곳에 정의하는 것이죠. 바꿔서 생각해 보면 유지보수할 수 없는 코드를 작성하는 쉬운 방법은 가능한 한 여러 장소에 최대한 다양하게 기록하는 것입니다.
검증을 멀리하기
입력 데이터에 대한 어떤 종류의 불일치 검사나 정확성 검사를 수행하지 않습니다. 우리는 회사 장비를 신뢰하고 있으며 모든 프로젝트 파트너와 다른 개발자들을 존중하는 완벽한 팀워크를 보여줄 수 있습니다. 입력 데이터가 이상하거나 문제가 있는 듯 보이더라도 항상 합리적인 값을 반환하기 위해 노력해야 합니다.
복사하고 수정하기
효율성이란 명목으로 Copy & Paste를 남발해 봅시다. 이 방식은 작은 재사용 가능한 모듈 여럿을 사용하는 것보다 실행속도가 빠르다는 장점이 있습니다. 특히 이 방식은 우리가 작성한 코드 라인 수를 업무 진행 척도로 여기는 곳에서 일할 때 더욱 유용합니다. 누군가 로직에 이의를 제기한다면 이미 모듈에서 사용하고 있는 코드라고 반박합시다.
혼합과 매치
접근자 Accessor(Getter/Setter) 함수와 public 변수를 함께 사용해 볼까요? 이 방식을 사용하면 접근자 함수를 호출하지 않고도 객체 변수를 변경할 수 있습니다. 이 방법은 변수값 변경 추적을 위해 로깅 기능을 추가한 유지보수 개발자를 좌절시킬 수 있는 장점을 제공합니다.
감싸고 감싸고 감싸고 감싸기
우리가 구현하지 않은 코드를 우리 함수에 사용해야 할 때는 다른 더러운 코드에 우리 코드가 오염되지 않도록 적어도 한 번 이상 래퍼 레이어를 사용해야 합니다. 이러한 방법을 이용하여 혼잡성을 증가시킬 수 있습니다.
어쩌면 다른 부분을 작성한 개발자도 언젠가 모든 메소드의 이름을 자기 마음대로 바꾸어 버리는 경우를 대비한다는 타당성도 있습니다. 래퍼 레이어 생성 시 소스 코드 혼잡성을 더 극대화하려면 각 단계에서 함수 이름을 변경하고 유의어 사전에서 동의어를 사용하는 방법도 있습니다. #define으로 이들 함수를 연결하는 것도 좋은 방법입니다.
이와 같은 방법을 이용해 마치 뭔가가 일어나고 있다는 환상을 심어줄 수 있습니다. 그러면 나중에 이름을 변경하면서 프로젝트 용어의 일관성을 깨뜨릴 가능성도 커집니다.
자식 클래스에 양보하는 미덕
클래스가 멤버 변수 또는 함수 10개를 가지고 있다고 가정해 봅시다. 베이스 클래스는 하나의 프로퍼티를 갖고 있고, 베이스 클래스를 상속하는 클래스에서 한 개씩만 속성을 추가하는 방식으로 9단계를 상속받도록 클래스 계층을 구성할 수 있습니다.
디자인 패턴인 데코레이터 패턴을 사용했다는 논리도 내세울 수 있습니다.
잡동사니 수집
사용하지 않고 오래된 함수나 변수라 할지라도 모두 코드에 모아둡니다. 20년 전에 딱 한 번 사용한 코드라도 언제 어떻게 사용할지 누가 알겠습니까?
함수와 변수 주석을 수수께끼처럼 남겨둔다면 금상첨화입니다.
코드 혼잡화
간결하게 만드는 코드로도 혼란을 야기할 수 있습니다.
암시적 변환을 악용하기
소수점 변수를 배열 인덱스로, 문자를 루프 카운터로, 문자열 함수를 숫자에 사용합시다. 우리가 만든 코드를 유지보수 할 개발자는 암시적 데이터형 변환 전체 내용을 읽어야만 하는 기회를 준 것에 대해 우리에게 감사할 것입니다.
매크로 전처리기
매크로 전처리기는 코드 난독화를 할 좋은 기회를 제공합니다. 핵심은 매크로를 여러 단계에 거쳐 확장하고 여러 헤더파일을 확인해야 뜻을 파악할 수 있도록 만드는 것입니다. 실행할 코드를 매크로에 넣은 다음 소스 코드 파일에서 인클루드하여 사용한다면 코드가 변경될 때마다 다시 컴파일해야 할 코드의 양을 극대화할 수 있습니다.
중괄호 {} 피하기
문법적으로 어쩔 수 없는 경우가 아니라면 절대로 if/else 블록을 감싸는 {} 를 사용하지 마세요. 들여쓰기 없이 if/else와 블록을 여러 단계로 중첩한다면 유지보수 능력이 뛰어난 개발자라도 가볍게 해치울 수 있습니다.
테스트
프로그램에 버그를 남겨둠으로써 유지보수 프로그래머에게도 재미있는 일거리를 제공해야 합니다. 잘 만든 버그라면 어디서 어떻게 발생했는지에 관한 단서를 남기지 마세요. 버그를 남겨두는 가장 게으른 방법으로는 우리 코드를 절대 테스트하지 않는 방법도 있습니다.
절대 테스트하지 않기
에러, 기기 크래시, OS 결함을 처리하는 코드는 절대 테스트하지 않습니다. OS가 반환하는 코드도 검사하지 않습니다. OS가 반환하는 코드는 실행에 아무 도움이 되지 않으며 우리 테스트 시간만 오래 걸리게 합니다.
왜 컴퓨터를 신뢰하지 않는 걸까요? 사용자가 프로그램의 문제에 대해 불평한다면 사용자가 잘 알 수 없는 OS 나 하드웨어 탓으로 떠넘기는 것도 좋습니다.
세상이 무너져도 성능 테스트를 하지 않기
프로그램이 좀 느리다면? 고객에게 더 빠른 컴퓨터를 사라고 말해봅시다. 성능 테스트를 수행하면 문제가 발생하는 지점을 찾을 겁니다. 아마 문제를 해결하려면 알고리즘을 변경해야 할 것이고 제품 전체를 완전히 다시 설계해야 하는 경우도 생길 수 있습니다. 이런 일을 누가 하고 싶어 할까요?
디버그 모드에서만 동작하는 코드
TESTING 을 1로 정의했다면 아래와 같이 TESTING 이 1인 경우에만 수행되는 별도의 코드 블록을 가질 수 있습니다.
#define TESTING 1
#if TESTING==1
x = rt_val;
#endif
누군가 TESTING 을 0으로 재설정하면 위의 코드는 동작하지 않아요. 조금만 창의력을 발휘하면 이 기법으로 로직을 망가뜨리고 컴파일러 기능 장애를 초래할 수 있습니다.
잡다한 기법
디버거 차단
디버거는 보통 라인 단위로 동작합니다. 코드 한줄 한줄 길게 만들어서 디버거를 이용해 코드를 이해하려는 사람을 좌절시킬 수 있습니다. 특히 브레이크 포인트를 잡기 어렵게 if와 then을 한 줄에 모두 사용하면 더 효과적입니다. 그러면 분기 문이 어느 문장을 수행하는 것인지 구별하기 어려워집니다.
버그 수정과 업그레이드를 혼합
“버그만 수정한” 버전을 릴리즈 하지 마세요. 버그를 수정했으면 데이터베이스 형식도 바꾸고, 복잡한 사용자 인터페이스도 변경하고, 관리자 인터페이스도 다시 만들어 봅시다. 이렇게 하면 사용자들은 버그에 익숙해지려 하고 결국 버그를 기능이라 부르기 시작할 겁니다.
컴파일러 종속 코드
컴파일러나 인터프리터 버그를 발견했으면 이 버그를 이용해 우리 코드가 제대로 동작하게 만듭시다. 이제 우리 프로그램을 사용하는 모든 이는 다른 컴파일러를 사용할 수 없게 됩니다.
마치며
대부분 내용은 실제로 경험해 봤었던 내용들입니다. 지금 글을 작성하면서 기억에 남는 버그들을 몇 가지 이야기해 보려 합니다.
// 기존 정상 동작 코드
int columnWidth = 0;
...
columnWidth *= factor.Width;
그 후 특정 시점 이후 코드 안정성 향상을 위해 컴파일러 경고가 발생하는 코드들을 정리하는 작업을 진행했었습니다.
factor.Width는 double 형으로 int 형에 대입할 때 데이터가 손실된다는 컴파일러 경고 C4244 가 발생했고, 명시적 형 변환을 위해 static_cast<int> 코드를 추가해 줬었습니다.
// 수정한 코드
int columnWidth = 0;
...
columnWidth *= static_cast<int>(factor.Width);
문제를 찾으셨나요?
columnWidth *= factor.Width;
는
columnWidth = columnWidth * factor.Width;
이 코드 동작이고,
columnWidth *= static_cast<int>(factor.Width);
이 코드는
columnWidth = columnWidth * static_cast<int>(factor.Width);
이렇게 표현이 됩니다.
그래서 factor.Width 값이 0~1 사이의 값이면 0으로 형 변환되어 columnWidth 가 항상 0 이 나오는 버그가 발생했었습니다. 암시적 변환을 악용하기가 자연스럽게 사용됐었던 거죠.
다른 기억에 남는 버그는 Product 용 빌드 서버에서 빌드된 프로그램에서는 재현이 되고 개발 환경에서는 재현이 안 되는 이슈였습니다. 버그를 확인해야 하는데 개발 환경에서 재현이 되지 않아 난감한 상황이었어요.
그래서 브레이크 포인트를 프로그램 처음 시작부터 버그 발생하는 동작까지 하나하나 찾아가면서 확인하여 아래 같은 코드를 찾았습니다.
public void SetAAA(bool isAAAToSet) {
#if !PRODUCTION
// 동작 1
productCertification = true;
#else
// 동작 2
#endif //PRODUCTION
}
제품인증 설정에 따른 동작이 필요했었는데 특정 값을 사용하여 상태를 관리하여 디버그 모드에서만 동작하는 코드가 되었던 거였습니다.
이처럼 “유지보수하기 어렵게 코딩하는 방법”은 농담 같은 내용이지만 의도하지 않아도 자연스럽게 코드에 녹아들어 있는 경우들이 있습니다.
재미있게 읽으셨나요? 혹시 설득되지는 않으셨죠? 내용이 흥미로우셨다면 책을 읽어보시는 것도 추천해 드립니다. 그리고 마지막으로 한 번만 더 말하겠습니다.