필자에게 프로그래밍이 무엇이냐고 묻는다면 프로그래밍이란 "버그와의 끝없는 싸움"이라고 대답하고 싶습니다. 필자가 처음 프로그래밍을 접해본 것은 중학교 3학년 때인 1984년이었습니다. 친구 집에 놀러갔다가 접한 SPC-1000에서 베이직으로 간단하게 계산기를 만들어본 것이 처음이었습니다. 그 컴퓨터란 물건이 얼마나 부럽던지 반년 동안 아버지를 졸라서 고등학교 1학년 때 애플 II 컴퓨터를 샀고 몇몇 컴퓨터 잡지를 사서 소스를 아무 생각없이 입력한 것 말고는 입시 준비(?)에 시달리느라 제대로 프로그래밍을 해본 적은 없었습니다. 그러다가 대학에 들어와서 포트란, 파스칼, C등을 배우면서 좀더 본격적인 프로그래밍을 시작하게 되었습니다. 그 시절을 돌이켜보면 참 어떻게 그렇게 무식하게 (?) 프로그래밍을 할 수 있었는지 대단하다는 생각이 간혹 들기도 합니다.
그 시절을 잠깐 회상하자면 일단 무턱대고 컴퓨터 앞에 앉아서 프로그래밍을 시작하지만 넘치는 의욕에 비해 실력이 따르질 않아서 쉬운 숙제에도 쩔쩔매며 간신히 코드를 만들었습니다. 컴파일하면 에러가 잔뜩 !!. 간신히 에러를 잡고 나서 실행하면 프로그램이 죽던지 엉뚱한 결과가 나오고... 그러면 이제부터 디버깅이 시작됩니다. 일단 의심이 가는 곳을 대충 고친 다음에 먼저 돌려보고 안 되면 다시 의심가는 곳을 고치고... 코드가 누더기가 된 다음에야 간신히 코드는 돌아가고 원하는 결과가 나오기는 하는데 왜 동작하는지 본인도 알 수 없고...
아마 대부분의 프로그래머들이 비슷한 과정을 거쳤거나 앞으로 거치리라 생각됩니다. 100줄 이상의 프로그램이라면 프로그램에 버그가 없을 수는 없습니다 (아니 적어도 컴파일 에러라도 있습니다). 그렇기 때문에 좋은 프로그래머가 되려면 버그가 적은 프로그램을 작성할 수 있거나 버그를 빨리 발견할 줄 알아야 합니다. 이 글에서는 여러분들이 이러한 목표를 달성하는데 도움이 될 수 있도록 먼저 좋은 코딩 습관에 대해 살펴보겠습니다. 그리고나서 다음으로 실례를 중심으로 자주 접하게 되는 버그 상황과 디버깅 팁에 대해 알아보도록 하겠습니다. 참고로 여기서는 비주얼 C++을 바탕으로 설명을 진행하겠습니다.
1. 바람직한 코딩 습관
버그 없는 프로그램을 작성하는 가장 좋은 방법은 바람직한 코딩 습관을 몸에 배게 하는 것입니다. 저도 지금 생각해보면 처음 프로그래밍을 배울 때 누군가 옆에서 이러한 것에 대해 이야기해주는 사람이 있었다면 고생을 좀 덜하지 않았을까 하는 생각을 간혹 하곤 합니다. 물론 자신이 직접 깨닫는 것만큼 확실히 배우는 것은 아니고 그 당시 그런 조언을 이해할만한 수준이 아니었기 때문에 그런 것들을 들었다 해도 얼마나 습득할 수 있었을지 의문이긴 합니다만. 참 이런 이야기를 하다보니 제가 이제는 무슨 완벽한 프로그래머가 된 것처럼 되어버렸는데 저도 이런 코딩 습관을 확립하기 위해 지금도 노력하고 있는 중입니다.
자 그럼 제가 생각하는 바람직한 코딩 습관에 대해 하나씩 알아보도록 하겠습니다. 이제 읽어보면 알겠지만 여기에 무슨 눈이 번쩍 뜨이는 그런 이야기들이 있는 것이 아닙니다. 항상 그렇지만 모든 성공은 작은 습관과 집중력으로부터 비롯합니다.
1> 사용하는 프로그래밍 언어를 제대로 이해하자.
이 것은 초보 프로그래머에게는 아주 중요한 사항입니다. 프로그래밍을 처음 배우는 사람이라면 그 개념을 제대로 이해하지 못하는 경우가 허다합니다. 특히 C 언어를 배우는 사람이라면 포인터의 개념을 제대로 이해하지 못하는 경우가 많고 C++/자바 등의 언어를 배우면 클래스의 상속(Inheritance)이나 가상 함수(Virtual function)와 같은 개념을 이해하지 못하는 경우가 허다합니다. 이런 것을 이해하지 못하고 일단 프로그래밍에 뛰어들게 되면 당연히 많은 시간을 소비하기 마련이고 결과물을 만들어내지 못하는 경우도 많습니다. 아무리 시간이 없어도 한걸음 한걸음 배워나가는 것이 결국에는 시간을 단축하기 마련입니다.
프로그래밍 서적을 보고 공부할 때도 모니터 앞에 앉아서 지금 보고 있는 부분에 대해 실습을 해보며 공부하는 것이 최선의 방법입니다. 그렇게 하면 보다 더 이해하기가 쉽고 개발환경의 사용법에 대해서도 배워볼 수 있기 때문입니다. 그렇기 때문에 초보 프로그래머라면 프로그래밍 언어 관련 서적을 고를 때 예제 코드가 많이 있는지 또한 따라하기식의 구성이 잘 되어있는지를 살펴보는 것이 중요합니다. 즉 프로그래밍 언어는 머리로 이해하는 것도 중요하지만 손으로도 익혀야 한다는 것입니다.
2> 프로그래밍 중에는 프로그래밍에만 집중하자.
이 것은 초보 프로그래머 뿐만 아니라 이 시대의 모든 프로그래머에게 해당되는 이야기라고 할 수 있는데 한마디로 자신의 일에 대한 집중력을 키우라는 이야기입니다. 요즘 웬만한 컴퓨터는 인터넷과 연결되어 있습니다. 인터넷 매체와 메일, 메신저 등이 워낙 발달해 있어서 한 시간이라도 중단 없이 일한다는 것이 쉬운 일이 아닙니다. 이런 상황은 사실 프로그래머 뿐만 아니라 컴퓨터를 사용하는 모든 사람들에게 해당된다고 할 수 있을 것 같습니다. 다음과 같은 비슷한 경험이 있는지 한번 생각해봅시다.
코딩이 좀 막히면 머리를 식히기 위해 인터넷의 바다에서 뛰어들고 코딩이 잘 되어도 기분이 좋아서 인터넷의 바다에 뛰어들고... 또 요즘 메일 클라이언트는 모두 알림 기능이 있어서 메일이 도착하면 이를 알려주게 되어있습니다. 메신저는 어떠한가요 ? 툭하면 친구로부터 채팅 요청이 들어오고... 수다 떠느라 시간가는 줄 모르고...
의식적으로 노력을 해서 이러한 중단이 없도록 해야 합니다. 인터넷의 바다에 뛰어드는 시간대를 정해두는 것이 제일 좋은 것 같습니다. 점심 시간 직후라든가 하는 식으로 말입니다. 메일의 경우에는 새 메일이 도착했다는 알림이 와도 바쁜 경우라면 일단 그 일을 끝낸 후에 보는 식으로 한번 바꿔보기 바랍니다. 메일의 경우 같이 일하는 사람들간 의사소통 수단이기도 하기 때문에 알림 기능 자체를 꺼버리는 것은 좀 무리가 있는 듯합니다. 메신저 같은 경우는 집중이 필요한 시간에는 아예 로그아웃해버리는 것입니다. 아니면 약간 치사(?)하지만 상태를 "자리 비움" 혹은 "다른 용무 중" 같은 걸로 해두세요.
약간 이야기가 옆길로 새는 것 같긴 하지만 집중력에 관해 얼마 전에 제가 읽은 글이 있어서 참고로 여러분들께 소개해 드리고자 합니다. ("고도원의 아침편지"란 사이트에서 읽은 글입니다.)
밥을 먹을 때에는 밥먹는 일에 집중하고
청소할 때에는 온전히 청소하는 행위만 있어야 합니다.
그렇게 생각하고 말하고 행동하는 것을
달리 말하면, 집중력 또는 통일성이라고 합니다.
이 집중하는 태도와 노력을 통해
우리는 스스로 정화되기도 하고
안정되기도 하며
또 문제의 본질을 통찰하는
힘을 얻기도 합니다.
- 도법스님의 <<내가 본 부처>> 중에서
프로그래밍을 할 때는 프로그래밍에만 집중합시다 !! 인터넷으로 인한 이러한 중단이외에도 회사 생활을 막 시작한 초보 프로그래머의 경우에는 다음과 같은 것을 조심해야 합니다. 긴 업무 시간으로 인한 집중력 상실입니다. 이건 사실 개인의 힘만으로 해결할 수 있는 문제는 아닙니다만 아주 한국적인 근무 환경 하에서는 늦게 퇴근하는 것이 하나의 미덕입니다. 따라서 오랜 시간을 회사에서 보내려니 업무의 강도가 느슨해지기 마련입니다. 하루 근무시간을 8시간으로 생각하지 않고 10시간에서 12시간으로 생각하니까 할 일이 있어도 "저녁에 하지 뭐!" 이런 생각을 갖게 되고 커피마시고 담배피우며 잡담하느라 보내는 시간이 더 많아지게 되는 것입니다. 사실 이는 매니저 역할을 맡고 있는 사람이 해결해야할 문제입니다만 이런 상황으로 인해 집중력을 잃는 일이 발생할 수도 있다는 점을 알아두고 자신이 이런 증상을 보이거든 회사를 옮기던가(?) 자신을 채찍질하여 다시 집중력을 되찾기 바랍니다.
3> 주석을 많이 달자
주석을 많이 다는 것도 굉장히 중요합니다. 아주 복잡하고 거창하게 코드 흐름도를 그리라는 이야기가 아닙니다. 소스 코드 중간 중간에 설명을 자세히 달아놓으라는 이야기입니다. 그것만으로도 다른 사람이나 코드의 원저자 자신이 나중에 소스를 볼 때 큰 도움을 얻을 수 있습니다. 제 아무리 자기가 작성한 코드라고 해도 복잡하고 사연 많은 코드의 세부적인 내용은 몇 개월만 지나도 잊어버리기 십상이기 때문입니다.
귀찮아서 주석을 안 다는 사람들도 많습니다. 심지어 주석이 코드의 미관을 해친다는 이색적인 주장(?)을 펼치며 주석달기를 거부하는 사람도 본 적 있었습니다. 오 마이 갓 !! 주석을 다는 일은 습관이 되면 그리 어려운 일이 아닙니다. 또 주석을 달면서 작성하는 소스의 구성이나 흐름이 보다 더 명확해질 수도 있습니다. 예를 들어 C/C++이나 자바로 코딩을 하는 중이라면 먼저 //부터 입력하고 밑에서 할 일을 간단히 적고 시작하면 자신이 해야할 일이 좀더 명확해지고 나중에 주석을 보고 코드를 기억하거나 이해하기도 쉽기 때문입니다.
소스 파일을 하나 새로 만들면 그 앞에 다음과 같은 식으로 주석을 달기 바랍니다.
// -------------------------------------------------------
// 파일이름 : NewModule.cpp
// 설명 : 전체 프로그램에서의 이 소스 파일의 역할을 기술합니다.
// 노트 : 기억할만한 점이 있으면 기록합니다.
// 히스토리 : 생성 - 한기용, 2002.03.31
// Abc 함수추가 - 두기용, 2002.04.01
// -------------------------------------------------------
함수의 경우에는 함수마다 앞에 다음과 같은 함수 주석을 답니다.
// -------------------------------------------------------
// 함수이름 : Abc
// 설명 : 이 함수의 역할에 대해 기록합니다.
// 노트 : 기억할만한 점이 있으면 기록합니다.
// 인자 : 인자를 하나씩 설명합니다. 인자의 값을 누가 채워주는지도 명시합니다.
// [IN] int nCount
// [IN] char *pString
// [OUT] int *pResultCount
// 리턴값 : 리턴값의 타입과 의미에 대해 설명합니다.
// -------------------------------------------------------
번거롭게 느껴질지 모르지만 한번 몸에 배면 아주 좋은 특히 같이 일하는 사람들이 좋아하는 습관이란 점을 분명히 기억해두기 바랍니다. 문서화를 잘 하는 사람들은 어디를 가든 사랑(?)받습니다.
4> 새로 작성한 코드는 항상 디버거로 따라가 보자
처음 작성했거나 잘 동작하던 코드를 수정한 경우라면 컴파일이 제대로 된 후에 한번 디버거로 흐름을 따라가 보면서 생각하는 대로 동작하는지 살펴보는 것이 아주아주 좋습니다. 좀 번거롭게 생각되어서 이런 이야기를 그냥 듣고 넘길 수도 있는데 이를 습관으로 만들면 여러모로 오히려 편리합니다.
그냥 일단 돌려보고 제대로 동작하지 않으면 디버거로 따라가 보는 전략을 택할 수도 있는데 그것보다는 처음 한번은 일단 디버거로 따라가 보는 것이 여러모로 좋습니다. 디버거로 따라가 보면 결과에 나타나지 않는 에러들도 찾아낼 가능성이 있고 다른 아이디어가 나올 가능성도 더 높기 때문입니다.
>
5> 테스트 코드를 만들자
훌륭한 프로그래머라면 누구나 버그없는 빠르고 깔끔한 프로그램을 만드는 사람이라고 생각할 것입니다. 하지만 앞서 제가 언급한 것처럼 복잡한 프로그램을 작성하다보면 버그 없는 프로그램을 작성하는 것은 거의 불가능한 일입니다. 그렇기 때문에 제가 생각하는 좋은 프로그래머의 요건은 버그를 어떻게 빨리 발견해서 없애느냐에 달려있다고 생각합니다. 참고문헌 3을 보면 "Debugging the Development Process"라는 책이 언급되어 있습니다. 이 책은 제가 아주 감명(?)깊게 읽은 책으로 제대로 된 개발을 하는 방법론에 대해 다루고 있습니다. 시간이 된다면 한번 꼭 읽어보기를 권합니다. 이 책에서는 버그를 발견하고 이를 없앨 때마다 항상 다음 두 가지를 생각해보기를 권하고 있습니다.
어떻게 했으면 이 버그를 내가 미연에 방지할 수 있었을까 ?
어떻게 했으면 이 버그를 내가 아주 쉽게 찾아낼 수 있었을까 ?
이를 좀더 확장해서 생각하면 코드를 작성할 때부터 이걸 어떻게 테스트를 할 것인지 염두에 항상 두고 있어야 한다는 말입니다. 그래서 저 같은 경우는 프로그래머의 수준을 측정할 때 한 가지 척도로 그 사람이 테스트 프로그램을 작성하는지를 일단 봅니다. 자신의 코드를 테스트할 프로그램을 별도로 작성할 정도의 사람이면 일단 기본기가 아주 잘 되어 있는 사람이라고 볼 수 있기 때문입니다.
코드에 따라서는 테스트하는 것이 아주 힘든 경우도 있을 수 있습니다. 하지만 그걸 핑계로 테스트를 건너뛰지는 말기 바랍니다. 테스트할 방법을 계속 생각해보면 결국 어떻게든 그 방법이 떠오르게 되어 있습니다. 테스트하기 힘든 상황에도 그 방법을 생각해낼 정도의 사람이라면 아주 긍정적인 사람으로 인식될 것입니다.
결론적으로 무슨 코드를 만들던지 테스트할 방법을 생각하기 바랍니다. 프로그램 수행의 결과로 파일을 만들어내는 프로그램이라면 그 파일의 내용이 맞는지 체크하는 프로그램을 따로 만들어 볼 수도 있을 것이고 뭔가를 수행해서 화면에 출력하는 간단한 프로그램이라면 그 부분을 별도의 함수로 만들어서 다양한 입력을 주어서 올바른 출력이 나오는지 확인해볼 수 있을 것입니다.
또 실행이 오래 걸리는 프로그램이라면 중간 중간마다 실행 상태를 파일등에 기록해두도록 하는 것도 아주 좋습니다. 이런 용도로 사용되는 파일을 흔히 로그 (log) 파일이라고 합니다. 이때 그 시각도 같이 기록해두면 도움이 많이 될 것입니다.
6> 생각하는 프로그래머가 되자
무슨 코드를 작성하던지 어떻게 할 것인지 먼저 생각하는 습관을 갖기 바랍니다. 그리고 프로그램이 정상적으로 동작하고 원하는 결과를 낸다면 이에 만족하지 말고 더 빠르고 간단하게 처리하도록 개선할 방법이 없는지 자꾸 생각해봐야 합니다. 그 당시에야 일단 실력이 모자라고 개선하는데 시간이 걸리니까 답답하게 여겨질지 모르지만 결국에는 프로그래밍 실력의 발전에 가속이 붙을 것입니다.
사용자 인터페이스 프로그램을 만들고 있는 중이라면 사용자 입장에서 더 편리하게 사용할 수 있는 방법을 자꾸 생각해봐야 합니다. 물론 이 과정은 끝이 없는 과정이 될 수도 있기 때문에 뭔가 데드라인이 있는 일을 작업 중이라면 적정선에서 타협을 봐야 합니다. 이러한 타협은 회사의 운명이 걸린 상용 프로그램을 만들 때는 아주 중요합니다. 자꾸 개선하다가 오히려 에러를 더 낼 수도 있도 그로 인해 결과적으로 주어진 시간 내에 작업을 못 끝낼 수도 있기 때문입니다.
요약하자면 무슨 프로그래밍을 하건 간에 먼저 생각하는 습관을 들이라는 것입니다. 그리고 결과로 만들어지는 코드의 질을 높이기 위해 노력하라는 것입니다. 이런 자세가 습관이 된다면 자신이 만들어낸 코드에 대한 안정성과 높은 성능을 보장해줄 것입니다. 이는 프로 프로그래머가 가져야 할 의식이며 이것이 바탕이 된다면 다른 사람들도 모두 여러분을 믿을만한 사람이라고 높게 평가해줄 것임에 틀림없습니다. 사실 필자도 프로그래밍을 배울 때 이렇게 하지 못했습니다. 프로그래밍을 오래 하다 보니까 그런 자세를 처음부터 갖는 것이 중요하다는 생각이 든 것입니다.
2. 예로 살펴보는 버그와 디버깅 팁
이제부터 실질적으로 코드를 통해 자주 접하게 되는 버그의 유형에 대해 예를 들어 알아보기로 하겠습니다.
1> 부호 숫자 타입과 무부호 숫자 타입
C/C++에는 부호 숫자 타입과 무부호 숫자 타입이 다음과 같이 존재합니다.
타입 크기 |
부호 숫자 타입 크기 |
무부호 숫자 타입 크기 |
8비트 |
char (-128 ~ 127) |
unsigned char (0 ~ 255) |
16비트 |
short (-32,768 ~ 32,767) |
unsigned short (0 ~ 65,535) |
32비트 |
int (-2,147,483,648 ~ 2,147,483,647) |
unsigned int (0 ~ 4,294,967,295) |
32비트 |
long (-2,147,483,648 ~ 2,147,483,647) |
unsigned long (0 ~ 4,294,967,295) |
비주얼 C++에서 사실 int와 long은 모두 32비트라는 점에 유의하기 바랍니다. 초보 프로그래머들이 많이 실수하는 분야 중의 하나가 바로 부적절한 정수 타입을 사용하는 것입니다.
먼저 사용되는 데이터 값의 범위를 확인하고 그에 맞는 변수를 선택해야 합니다. 위의 표를 보고 적절한 변수를 선택하면 됩니다. 위의 표에는 없지만 사실 __int64라고 해서 64비트 짜리 정수 타입도 있습니다. 사실 대부분의 경우 int로 충분합니다.
만일 사용되는 데이터의 값이 음수가 될 수 없는 것이 분명하면 무부호 정수 타입을 사용하기 바랍니다. 예를 들어 int 대신에 unsigned int를 사용하란 이야기입니다.
그런데 무부호 정수 타입을 사용할 경우에는 비교 연산시에 아주 주의해야 합니다. 예를 들어 다음과 같은 코드를 보면
unsigned int dwNumber1 = 100, dwNumber2 = 200;
if (dwNumber1 - dwNumber2 > 0)
AfxMessageBox("dwNumber1 is larger");
else
AfxMessageBox("dwNumber2 is larger");
당연히 if 연산이 거짓이 되어 두 번째 메시지 박스가 출력될 것 같지만 그렇지 않습니다. 이 if 연산은 참이 됩니다. 무부호 정수간의 연산 결과는 다시 무부호 정수가 됩니다. 무부호 정수에는 말그대로 음수가 없습니다. 따라서 이 연산은 항상 참이 됩니다. 위의 코드는 다음과 같은 식으로 변경해야 합니다.
if (dwNumber1 > dwNumber2)
AfxMessageBox("dwNumber1 is larger");
else
AfxMessageBox("dwNumber2 is larger");
즉 무부호 정수끼리 뺄셈을 하는 경우에는 아주 조심해야 한다는 것입니다.
2> 포인터 사용하기
C/C++에 처음 입문한 프로그래머들이 가장 어려움을 느끼는 영역 중의 하나가 바로 포인터라는 개념을 익히는 것입니다. 포인터는 메모리 주소를 가리키는 변수입니다. 여기에 메모리를 할당해서 유효한 메모리 주소를 갖게 하기도 하고 다른 변수의 주소를 가리키게 하기도 합니다. 즉 포인터 변수는 선언한 후에 바로 사용하면 안 되고 무엇인가가 유효한 메모리 영역을 가리키게 만들어야만 한다는 것입니다. 전산을 4년 전공하고 회사에 막 들어온 사람들 중 상당수가 이를 제대로 이해하지 못한 상태임을 여러 번 보았습니다. 그 중의 한 예는 다음과 같습니다.
void DoSomething(char *lpstrSource)
{
char *lpstrString;
strcpy(lpstrString, lpstrSource);
...
}
위의 코드를 보면 pString이란 포인터 변수를 선언한 후에 그걸 그대로 strcpy라는 함수에 사용하고 있습니다. 이 상태에서 pString이란 변수는 메모리의 아무 영역이나 가리키고 있습니다. 여기에다가 인자로 넘어온 pSource가 가리키는 문자열을 복사하려고 하면 당연히 프로그램이 뻗어버립니다. 이런 코드가 나오는 이유는 포인터라는 것의 개념을 이해하지 못했기 때문입니다. 그런 상태에서 strcpy 함수의 원형을 보고 그대로 변수를 선언하고 인자로 지정한 것입니다. 올바른 코드는 다음과 같습니다. 포인터에 메모리를 할당한 다음에 이 것이 제대로 할당된 경우에만 복사를 시도하는 식으로 변경해야 합니다.
void DoSomething(char *lpstrSource)
{
char *lpstrString;
lpstrString = (char *)malloc(strlen(lpstrSource)+1);
if (lpstrString)
strcpy(lpstrString, lpstrSource);
...
}
다시 정리하자면 포인터란 메모리 영역을 가리키는 변수이기 때문에 무엇인가 유효한 영역을 가리키도록 초기화해야만 사용할 수 있습니다.
참고 1. 메모리 할당
컴퓨터 세계의 함수 중에는 쌍으로 사용되는 것이 무지하게 많습니다. 대표적인 것이 바로 메모리 할당을 할 때 사용되는 malloc 함수와 더 이상 사용할 일이 없을 때 이를 운영체제에 반환해주는 free 함수입니다. C++에는 new와 delete가 있지요. 이것들의 쌍이 제대로 맞지 않으면 메모리가 조금씩 조금씩 부족해지다가 결국에는 바닥나게 되어 있습니다.
그런데 화장실 들어갈 때와 나갈 때 마음이 다르다고 처음엔 메모리가 필요하니까 할당해서 사용하지만 사후처리를 잊기 십상입니다. 뭐 간단한 프로그램에서야 메모리할당과 반환이 그다지 복잡하지 않지만 정말로 복잡한 프로그램에서는 이게 쉽지 않습니다. 서버 프로그램의 경우 아주 작은 양의 메모리가 반환되지 않는 경우에는 프로그램이 한두달 돌아야 그게 밝혀지는 경우도 있습니다.
그래서 자바(Java)와 닷넷(.NET)에서는 이런 메모리 반환의 책임을 프로그래머에게 넘기지 않고 시스템 레벨에서 처리합니다. 즉 사용되지 않는 메모리가 있으면 알아서 반환시켜버리는 것입니다. 이를 가비지 컬렉션(Garbage Collection)이라고 부릅니다. 프로그래머 입장에서 두손 들고 환영할 일이지요. 하지만 이 가비지 컬렉션을 해주는 프로세스가 주기적으로 동작해야 하고 메모리가 바로 반환되는 것이 아니라 시간이 걸리기 때문에 프로그램의 실행 속도가 전체적으로 좀 느려지게 됩니다.
3> 함수의 리턴값 체크
초보 프로그래머이건 노련한 프로그래머이건 간에 또 많이 하는 실수가 바로 함수의 리턴값을 체크하지 않고 그대로 코드를 작성하는 경우입니다. 리턴 값을 체크하려면 if 문이 들어가야 하고 이게 좀 귀찮은 일입니다. 또한 이게 많아지면 코드를 한눈에 보기도 힘들어집니다. 그래서 많은 프로그래머들이 별일 있겠냐 하는 마음에 함수 리턴값 체크를 하지 않습니다. 예를 들어 파일 I/O를 한다고 하면 다음의 코드처럼 리턴 값을 체크하지 않는 경우가 허다합니다.
// strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.
CFile file;
char strHeader[256];
file.Open(strFilePath, CFile::modeRead);
file.Read(strHeader, 255);
이런 코드는 strFilePath가 가리키는 파일이 없거나 파일은 있지만 파일의 크기가 부족한 경우에 에러를 내게 됩니다. 사용된 함수의 리턴 값을 모두 체크하도록 코드를 수정하면 다음과 같습니다.
// strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.
CFile file;
char strHeader[256];
bool bError = FALSE;
// CFile::Open 함수는 오픈이 성공하면 TRUE를 리턴한다.
if (file.Open(strFilePath, CFile::modeRead))
{
// CFile::Read 함수는 읽어들인 바이트수를 리턴한다.
if (file.Read(strHeader, 255) != 255)
bError = TRUE;
}
else
bError = TRUE;
if (bError) // 에러가 있으면
{
CString strError;
strError.Format("에러가 발생했습니다 - %d", GetLastError());
AfxMessageBox(strError);
}
수정된 코드를 수정전의 코드와 비교하면 아마도 조금 더 코드의 흐름을 이해하기 힘들다는 느낌이 들 것입니다. 하지만 대신에 훨씬 더 코드가 안정적으로 동작하게 됩니다. 참고로 수정된 코드를 보면 에러가 발생했을 때 GetLastError 함수를 부르는데 이 함수는 윈도우 운영체제가 제공해주는 함수로 방금 발생한 에러에 해당하는 에러코드를 리턴해주는 역할을 수행합니다. 이 값에 해당하는 설명 문자열을 보고 싶다면 비주얼 C++의 도구(Tools) 메뉴의 오류 조회(Error Lookup) 명령을 실행한 다음에 거기에 에러코드를 입력해보면 됩니다. 다음은 "오류 조회" 다이얼로그의 실행 화면입니다.
< 그림 1. "오류 조회(Error Lookup)" 다이얼로그의 실행 화면 >
가끔 테스트 프로그램을 작성하거나 시간이 없을 경우 방금 코드처럼 리턴 값을 일일이 체크하는 것을 생략하는 사람들을 많이 봤습니다. 코드를 작성하는 당시에는 나중에 제대로 검사하게 고쳐야지 하지만 그럴 만큼 부지런한 사람은 드뭅니다. 수정할 기회가 언제 올지 알 수 없기 때문에 무슨 프로그램을 짜던지 항상 최선을 다하는 것이 좋습니다. 즉 되도록이면 "나중에 고치지"라는 생각은 접어 두시기 바랍니다.
참고 2. try/catch
함수의 리턴값 체크가 번거롭다면 그의 대안으로 사용할 수 있는 것이 바로 C++의 try/catch 문법입니다. 비주얼 C++에서는 MFC 클래스에 대해서만 적용가능하다는 단점을 갖고 있긴 하지만 이를 이용하면 if 문을 이용해 함수의 리턴 값을 검사할 필요가 없기 때문에 코딩도 간단해지고 코드를 읽기도 좀더 쉬워집니다. "3. 함수의 리턴값 체크"에서 본 코드를 try/catch를 이용하도록 변경해보면 다음과 같습니다.
// strFilePath가 읽어들이고자 하는 파일의 경로를 가리킨다고 하자.
CFile file;
char strHeader[256];
try
{
file.Open(strFilePath, CFile::modeRead);
file.Read(strHeader, 255);
}
catch(CException e)
{
e.ReportError();
e.Delete();
}
try로 둘러싸여진 블록 내에서 에러가 발생하면 실행이 바로 catch 블록으로 넘어갑니다. 단 모든 에러가 다 try 블록에 의해 감지되는 것은 아닙니다. 에러가 발생한 경우 그것을 throw 키워드를 이용해 리턴하는 함수들에서만 에러가 감지됩니다. MFC 클래스들은 대부분 에러가 발생하면 CException이란 클래스로부터 계승된 각자의 에러 클래스의 개체를 throw하도록 되어있습니다. CFile 클래스의 경우에는 CFileException이란 클래스가 에러 클래스에 해당하며 CException으로 계승된 클래스입니다. 또 클래스는 아니지만 new로 메모리를 할당하는 경우에도 메모리가 부족하면 예외를 발생시킵니다. 즉 ,new를 이용해서 메모리를 할당하는 경우에는 try/catch를 이용한다면 굳이 메모리 할당이 제대로 되었는지 일일이 검사할 필요가 없다는 이야기입니다.
사실 try/catch/throw는 그 자체만으로 상당한 지면을 통해 설명해야 하기 때문에 여기서는 이런 것이 있다는 것을 알리는 정도로 끝을 맺겠습니다. 참고로 비주얼 베이직이나 자바, C# 등의 대부분의 현대적인 프로그래밍 언어들은 이런 방식의 에러처리를 지원합니다.
>
4> 컴파일러 경고 메시지에 신경쓰자
대개의 프로그래머들은 컴파일러가 내주는 에러 메시지에만 신경을 쓰고 경고(Warning) 메시지에는 둔감합니다. 그런데 이 경고 메시지를 눈여겨 보면 간혹 모르고 지나쳤을 버그를 잡는 경우가 있습니다. 조사에 의하면 버그를 발견하는데 드는 시간이 90%이고 이를 수정하는데 걸리는 시간은 10%에 불과하다고 합니다. 경고 메시지를 눈여겨 보면 적은 노력으로 버그를 발견할 수 있는 셈입니다. 다양한 경고 메시지 중에서 네 가지를 살펴보도록 하겠습니다.
비초기화 변수 사용 경고
그 중의 대표적인 것이 바로 변수를 선언은 했지만 그 변수를 초기화하지 않고 사용하는 경우입니다. 예를 들면 다음과 같은 경우가 있습니다.
{
int nNumber1, nNumber2;
nNumber1 = 100;
nNumber1 += nNumber2;
nNumber2에는 어떤 값이 들어가 있을지 아무도 모릅니다. nNumber2에는 임의의 값이 들어가 있을 수 있고 이는 nNumber1의 값에도 영향을 주게 됩니다. 위의 코드를 컴파일하면 다음과 같은 컴파일러 경고 메시지가 나타납니다.
warning C4700: local variable 'nNumber2' used without having been initialized
컴파일러의 경고 메시지도 유의해서 보는 사람이라면 쉽게 nNumber2가 초기화되지 않고 사용된 사실을 알아차릴 수 있습니다. 사실 이런 종류의 에러는 컴파일러의 경고 메시지가 아니면 찾기가 그리 쉽지 않을 수도 있습니다. 그 이유는 위와 같은 결과가 프로그램의 실행 결과에 큰 영향을 끼치지 않을 수도 있기 때문입니다. 즉 모르고 넘어갈 수도 있다는 것입니다.
함수 내에 값을 리턴하지 않는 플로우 존재
함수의 코드가 길어지고 그 안의 다양한 부분에서 값을 리턴해야 한다면 실수로 return문을 빼먹을 수가 있습니다. 이 경우에도 앞서 초기화 안 된 변수를 쓸 때와 마찬가지로 그 당시 스택에 있던 아무 값이나 리턴이 되고 또한 마찬가지로 프로그램이 실행되는데 별 지장이 없을 수 있습니다. 그래서 이 역시 좀 찾기 어려운 에러가 될 수 있습니다. 예를 들어 다음과 함수를 보겠습니다.
int ReturnCheckFunction(int i)
{
switch(iValue)
{
case 1: return 0;
case 0: return 1;
}
}
위의 함수를 보면 인자 iValue로 1,0이외의 값이 들어오는 경우 return 문이 존재하지 않습니다. 이 경우 스택에 있던 아무 값이나 리턴됩니다. 위의 코드를 컴파일하면 다음과 같은 경고 메시지가 발생합니다.
warning C4715: 'ReturnCheckFunction' : not all control paths return a value
논리 판단문안에서의 치환문 사용
가끔들 많이 하는 실수 중의 하나는 if문이나 while 문과 같은 논리 판단문 안에서 == 대신에 실수로 =를 사용하는 것입니다. 이 에러도 경우에 따라서는 아주 찾기 힘든데 컴파일러의 경고 메시지를 눈여겨 보면 아주 쉽게 찾을 수 있습니다. 예를 들어 다음과 같은 코드가 있다고 합시다.
int iRet; iRet = CallSomeFunction(); if (iRet = 0) { ... }
위의 코드에서 if문을 보면 iRet의 값과 0을 비교한다는 것이 잘못 되어서 iRet에 0을 대입하고 있습니다. 이렇게 되면 이 if 문은 항상 거짓이 됩니다. 이 코드를 컴파일하면 다음과 같은 에러가 발생합니다. WARNING 4706: assignment withing conditional expression 반대의 경우로 =를 써서 변수에 어떤 값을 대입하는 경우에 잘못해서 등호 연산자인 ==를 사용하는 경우도 있습니다.
i == 0;
이 것도 컴파일이 됩니다. 단 컴파일러가 다음과 같은 경고 메시지를 내줍니다.
WARNING 4553: '==' : operator has no effect; did you intend '='?
항상 컴파일러가 내주는 경고 메시지에 꼭 신경쓰기 바랍니다. 몇십초 동안 잠깐 살펴보는 것으로 여러분이 몇 시간 혹은 며칠동안 고생하는 것을 방지해줄 수 있습니다.
5> BOOLEAN 타입
비주얼 C++에는 두 종류의 Boolean 타입이 존재합니다. 하나는 BOOL이고 다른 하나는 bool입니다. 이 두 타입은 모두 다른 타입으로부터 typedef를 이용해 재정의된 것인데 다음과 같이 정의되어 있습니다.
typedef int BOOL;
typedef byte bool;
이 두 가지 중에서 bool 타입을 항상 사용하기를 권합니다. 이 타입의 장점은 다음과 같습니다.
크기가 작습니다. 위에서 볼 수 있듯이 BOOL 타입은 크기가 4바이트이고 bool 타입은 크기가 1바이트입니다. 즉, 커다란 배열을 사용할 일이 있으면 bool이 절대적으로 메모리의 낭비를 줄일 수 있습니다.
안전합니다. BOOL은 사실 TRUE, FALSE이외에도 다른 값이 대입 가능합니다. 예를 들어 다음과 같은 코드를 보고 실행 결과를 예측해보기 바랍니다.
BOOL bRet = 100;
if (bRet == TRUE)
AfxMessageBox("bRet가 참입니다.");
else
AfxMessageBox("bRet가 거짓입니다.");
BOOL 타입은 원래 TRUE와 FALSE 둘 중의 한 값을 갖도록 되어 있지만 불행히도 위와 같이 bRet에 100을 대입하는 코드는 전혀 문제를 일으키지 않습니다. 그리고 FALSE는 0으로 정의되어 있고 TRUE는 -1로 정의되어 있습니다. 따라서 위의 if문에서 bRet의 값(여기서는 100이 됩니다)과 TRUE는 서로 다른 값이 되어버리기 때문에 두 번째 메시지 박스가 실행됩니다. 즉 BOOL 타입의 문제는 TRUE, FALSE 이외의 다른 값이 들어갈 수 있는 가능성이 있기 때문에 if 문을 어떻게 사용하느냐에 따라서 전혀 다른 실행 결과를 낳는다는 것입니다. 만일 위의 코드에서도 if (bRet)와 같이 사용했다면 if 문이 참이 될 것입니다.
반면에 bool 타입(혹은 BOOLEAN)은 true와 false라는 단 두 가지의 값만을 가질 수 있습니다. 만일 0이외의 값을 이 타입의 변수에 대입하여도 이 값은 true라는 값으로 바뀌어서 저장됩니다. 다음과 같은 코드를 보기 바랍니다.
bool bRet = 100;
if (bRet == true)
AfxMessageBox("bRet가 참입니다.");
else
AfxMessageBox("bRet가 거짓입니다.");
위의 if 문은 참이 됩니다. 앞서 설명한 것처럼 bool 타입에서는 0이외의 값은 true로 바뀌어 저장되기 때문입니다. 항상 bool 타입을 사용하기 바랍니다.
6> 함수 인자 유효성 체크
이것은 버그를 방지하기 위한 일종의 방법인데 자신이 만드는 함수의 선두 부분에서 주어진 인자가 맞는 범위에 있는지 항상 먼저 체크하는 것입니다. 이 방법은 특히 다른 사람과 사용할 함수를 만들거나 다른 사람이 만든 데이터를 사용하는 함수를 만들 때 아주 효율적인 방법입니다. 즉 자기가 예상하고 있는 데이터들이 들어오는지 사용에 앞서 한번 검사하는 것입니다.
방금 이야기한 것처럼 이 방법은 공동작업에 아주 쓸모가 많습니다. 사람마다 나름대로의 독특한 이해방식을 갖고 있기 때문에 한참을 이야기해서 함수 인자 및 데이터의 형식에 대해 토론을 하고 결론을 내려도 실제로 구현을 해놓고 보면 서로의 이해가 다른 경우가 많습니다. 이로인한 혼란을 막기 위한 방법 중의 하나가 바로 이 방법입니다.
예를 들어 어떤 소팅함수가 있는데 소팅 대상이 되는 것이 0부터 2048 사이의 정수라고 합시다. 그러면 그 함수의 선두 부분에 다음과 같은 검사 코드를 넣어두는 것입니다.
BOOL Sort(int *pnNumbers, int nCount)
{
// 검사 코드를 넣어둡니다.
if (nCount <= 0) // 소팅할 대상이 0보다 작으면 그냥 리턴합니다.
return FALSE;
for(int iIndex = 0;iIndex < nCount;iIndex++)
{
if (pnNumbers[iIndex] < 0||pnNumbers[iIndex] > 2048)
{
printf("%d 번째의 값이 이상합니다. - %d\n", iIndex, pnNumbers[iIndex]);
return FALSE;
}
}
// 실제 소팅 코드가 나온다.
....
}
위와 같은 식으로 코딩을 하면 많은 버그를 잡을 수 있습니다. 그런데 위의 코드를 보면서 속도가 느려지지 않을까 하는 걱정을 하는 사람도 있을 것입니다. 그런 걱정이 든다면 위의 코드를 디버그 모드에서 동작하도록 하면 됩니다. 원래 디버그 모드에서 개발을 하고 디버깅이 끝나고 나면 릴리스 모드로 컴파일해서 사용하는 것이 정석이니까요. 그렇게 할 수 없는 경우도 가끔 있습니다. 디버그 모드에서는 잘 동작하는 프로그램이 릴리스 모드에서는 잘 동작하지 않는 경우가 간혹 있습니다. 다시 본론으로 돌아와서 디버그 모드에서 위의 체크 코드를 활성화시키고 싶다면 #ifdef 조건부 컴파일 지시자와 _DEBUG 상수를 이용하면 됩니다. 이 상수는 디버그 모드로 컴파일되는 경우에 비주얼 C++ 컴파일러에 의해 정의가 됩니다.
bool Sort(int *pnNumbers, int nCount)
{
// 검사 코드를 넣어둡니다.
if (nCount <= 0) // 소팅할 대상이 0보다 작으면 그냥 리턴합니다.
return FALSE;
#ifdef _DEBUG
for(int iIndex = 0;iIndex < nCount;iIndex++)
{
if (pnNumbers[iIndex] < 0||pnNumbers[iIndex] > 2048)
{
printf("%d 번째의 값이 이상합니다. - %d\n", iIndex, pnNumbers[iIndex]);
return FALSE;
}
}
#endif
// 실제 소팅 코드가 나온다.
....
}
#ifdef 다음에 오는 상수가 #define문으로 정의된 것이면 여기서부터 #else 혹은 #endif까지의 코드는 포함되고 결과적으로 컴파일됩니다. 즉 디버그 모드인 경우에만 위의 검사 코드가 포함되는 것입니다. 릴리스 모드에서는 _DEBUG라는 상수가 정의되지 않기 때문에 위의 코드는 포함되지 않습니다. 참고로 #ifndef라는 것도 있습니다. 그 다음에 오는 상수가 정의되어 있지 않으면 참이 되는 것입니다.
다시 한번 말하지만 디버그 모드는 말그대로 개발시에 디버깅을 위한 컴파일 모드입니다. 이 모드에서는 컴파일러가 코드 최적화를 시도하지 않습니다. 그렇기 때문에 덩치도 크고 속도도 릴리스 모드에 비해 20-40% 가량 늦습니다. 따라서 상품화되거나 외부에 서비스를 하는 프로그램이라면 릴리스 모드로 컴파일되는 것이 필수적입니다. 참고로 비주얼 C++ 닷넷에서는 그림 2처럼 빌드(Build) 메뉴의 구성 관리자(Configuration Manager) 명령을 선택하면 이를 선택할 수 있습니다. 비주얼 C++ 6.0에서는 이 컴파일 모드를 결정하는 것은 Build 메뉴의 Set Active Configuration 명령에서 가능합니다.
그림 2. 디버그 모드와 릴리스 모드의 선택
7> 통일된 함수 실행 성공 리턴 값과 시작 인덱스 값
리턴 값이 있는 함수의 경우 성공을 나타내기 위해 사용하는 값을 통일하기 바랍니다. 예를 들어 어떤 함수에서는 성공을 나타내기 위해 0을 사용하고 다른 함수에서는 1을 사용하고 또 다른 함수에서는 -1을 사용한다고 하면 자신이 만든 함수라 해도 항상 코드를 다시 살펴보아야 하는 문제가 발생하며 이로 인해 버그가 발생할 수도 있습니다. 저는 항상 성공을 나타내는 리턴코드로 0을 사용합니다. 성공을 나타내는 함수의 리턴 값을 통일해서 사용하기 바랍니다.
시작 인덱스 값도 마찬가지입니다. 예를 들어 열명의 사람에 대한 정보가 있는데 이름이 주어졌을 때 그 사람에 해당하는 번호를 리턴하는 함수를 만든다고 합시다. 자 그럼 이 번호를 0부터 셀 것인지 아니면 1부터 셀 것인지 혼란이 오는 경우가 있습니다. 저는 이런 경우 항상 0부터 세며 꼭 주석이나 문서에 0부터 센다는 것을 명시합니다. C/C++의 배열 인덱스가 0부터 시작하기 때문에 C/C++에서는 무엇인가 숫자 정보의 시작을 0부터 하는 것이 자연스러운 경우가 많습니다. 자신만의 인덱스 시작값 시작 규칙을 만들어 두기 바랍니다. 만일 여러 사람이 같이 일을 한다면 다같이 통일하면 더욱 좋겠지요.
참고 3. 변수 표기법
프로그래머의 개인 취향에 따라 변수 이름을 만드는 방법은 참 다양합니다. 아무 생각없이 하는 사람도 많긴 합니다만. 윈도우 운영체제에서는 찰스 시모니(Charles Simony)란 사람이 만든 헝가리안 표기법이란 것을 사용합니다. 이런 이름이 붙은 이유는 이 사람이 헝가리 출신이었기 때문이었고 윈도우에서 이 사람의 표기법을 사용하게 된 것은 이 사람이 마이크로소프트의 초창기 멤버로 많은 일을 했기 때문입니다. 이 표기법은 변수 이름 자체에 그 변수의 타입에 대한 정보를 같이 주는 것이지요. 예를 들면 다음과 같습니다.
int nCount; // 정수 변수 앞에는 i나 n을 붙입니다.
DWORD dwCount; // DWORD 타입 앞에는 dw를 붙입니다.
bool bReturn; // Boolean 타입 앞에는 b를 붙입니다.
char *lpstrMessage; // 문자에 대한 포인터앞에는 lpstr 혹은 pstr을 붙입니다.
여기에다가 윈도우에서는 클래스 멤버 변수의 경우, 그 이름 앞에 m_를 붙이는 것이 일반적입니다. 저 같은 경우는 전역 변수의 경우 그 이름 앞에 g_를 붙입니다. 이렇게 하면 좋은 점은 변수를 보는 순간이 이게 어디 정의되었고 타입이 무엇인지를 알 수 있게 된다는 것입니다. 예를 들어 다음과 같은 코드를 보기 바랍니다.
m_nCount = nIndex +g_nMinimum;
위의 코드를 보면 m_nCount는 이 코드가 실행되고 있는 클래스내의 멤버 변수이고 nIndex는 로컬 변수 (흔히 말하는 auto 변수)이고 g_nMinimum은 전역 변수라는 것을 알 수 있습니다. 거기다가 모든 변수가 정수 타입이라는 것도 알 수 있습니다.
결론
지금까지 버그를 방지하거나 버그를 잡기 위한 여러 가지 방법들에 대해 알아보았습니다. 사실 제일 중요한 것은 빠르고 간결하며 정확한 코드를 만들겠다는 마음 자세가 아닌가 싶습니다. 이러한 자세가 바탕이 된다면 위의 방법들이 습관으로 만드는 것이 그리 어렵지 않을 것입니다. 사실 방법 하나하나는 대단한 것이 없습니다. 하지만 이것들이 합쳐지면 시너지 효과가 대단합니다. 이것들을 습관으로 만드는 것이 바로 버그를 없애는 프로그래밍의 시작이며 프로그래밍을 처음 배우는 단계에서 이를 습관으로 만들 수 있다면 여러분은 아주 뛰어난 프로그래머가 될 수 있을 것입니다.
참고문헌
- Steve Maguire, Writing Solid Code : Microsoft's Techniques for Developing Bug-Free C Programs, Microsoft Press, 1993
- Brian W. Kernighan and Rob Pike, The Practice of Programming, Addison-Wesley, 1999
- Steve Maguire, Debugging the Development Process : Practical Strategies for Staying Focused, Hitting Ship Dates, and Building Solid Teams, Microsoft Press, 1994
- Steve C McConnell, Code Complete : A Practical Handbook of Software Construction, Microsoft Press, 1993
'IT-개발,DB' 카테고리의 다른 글
[IT/개발] 개발자 Coder < Programmer < Engineer < Architect < Consultant 로 성장 (0) | 2010.10.13 |
---|---|
[IT/개발] [MSDN] 코딩기술 - 주석(comment) (0) | 2010.10.13 |
[IT/일반] 문서비교 winmerge 유틸 (0) | 2010.10.07 |
[VC++] Building Browser Helper Objects with Visual Studio 2005 (0) | 2010.10.01 |
[ASP.NET] UTF-8 방식일 경우 GET 방식으로 한글데이터 넘기는 방법 ( UrlEncode 매서드 사용 ) (0) | 2010.10.01 |
댓글