성공하는 개발자가 가져야할 7 가지 습관

1. 설계
2. 디자인
3. 디버깅
4. 테스트
5. 리팩토링
6. 문제해결
7. 프로젝트 파일 관리

디버깅. 필자는 이 단어가 다소 어려운 이미지를 물씬 풍기고 있다고 생각한다. 마치 큰 벽처럼 프로젝트의 성공을 위해서는 이 디버깅이라는 벽을 뛰어넘어야만 하는 것 같은 생각이 들기도 한다. 하지만, 약간 다르게 생각을 해 보면, 사실은 디버깅이라는 것은 프로그램이 정상적으로 잘 작성되었다면, 아무것도 아닌 일이 될 수 도 있다. 디버깅이라는 것은 애초에 버그가 있어야 한다는 것인데, 이 버그가 없거나 매우 단순하다면 디버깅 작업도 단순할 수 밖에 없는 것이다. 모든 문제는 단순화시켜 놓으면 매우 단순하고 작아보인다. 하지만, 이것이 복합적으로 복잡하고 꼬여 있을 때에는 단순한 것들도 매우 커 보이는데, 사실은 이 문제들이 아주 작은 것들에서 비롯되어있고, 간단한 수정만으로 문제를 해결할 수 있는 경우도 참 많다.
 하지만, 실제로 디버깅 과정은 고난과 역경의 연속이다. 때로는 상상할 수 없을 만큼 일들이 많이 꼬여 있어서, 더 이상 어떻게 손쓸 수 없는 경우도 있다. 필자가 경험한 프로젝트중 가장 디버깅이 어려웠던 프로그램은 오랜 시간동안 수많은 사람들의 손을 거친 프로그램이었는데, 이 프로그램의 일부 코드는 코드의 줄 수를 가지고 프로그램의 가치를 측정했던 오래전에 작성되었는지, 의미없는 코드의 반복까지 포함하고 있었던 프로그램이었다. 이 프로그램은 사실 디버깅을 하는 것 보다 실제로 프로그램을 새로 작성하는 것이 더 좋아보였다. 더군다나 이 프로그램의 디버깅이 더 어려웠던 이유는 이 프로그램은 어떤 기계의 데이터를 정리하고 그래프로 보여주는 프로그램이었는데, 필자의 이 기계에 대한 이해가 매우 부족했기 때문이다. 프로그램의 각 코드들을 이리저리 옮겨갈 수 는 있었지만, 과연 이 값이 두개로 유지될 필요가 있는 것인지에 대한 판단은 그 기계에 대한 이해없이는 어려웠기 때문이다.
 이렇게 어려운 디버깅을 성공적으로 하기 위해서는 어떻게 해야 할까? 디버깅을 하는 일은 앞에서 말했다시피, 디버깅을 하는 프로그램이 작성된 과정이 매우 중요하기 때문에, 그 과정도 어떻게 딱 잘라서 말할 수 없는 복잡함이 있다. 그럼에도 불구하고 디버깅에서 가장 중요한 것은, 동일한 문제를 재현하는 것이다. '재현' 의 중요함은 독자들도 이미 수 많은 책들에서 읽었으리라 생각한다. 문제를 재현한다는 것은 그 문제에 대한 이해를 좀 더 높이는것이라고 볼 수 있다. 필자가 여기서 독자들에게 하고싶은 이야기는 단지 재현을 위한 재현이 아니라, 이 프로그램이 시스템의 어떤 부분들을 거치면서 문제를 만들어내는지를 항상 추적하면서 문제를 재현해야 한다는 것이다. 물론, 이러한 것을 정확하고 쉽게 하기 위해서는 수많은 경험이 필요하다. 여러가지 프로그램을 만들어보면서, 산전수전을 다 겪은 개발자는 어떤 문제가 발생하면, 자신의 경험에서 그 원인과 해결책을 금새 찾아낸다. 하지만, 모든 문제가 이렇게 해결되지는 않는다. 또한, 모든 개발자들이 이러한 능력을 얻을 수 는 없다. 그렇기 때문에 필수적으로 해야 하는 것이 바로 리버스 엔지니어링이다. 갑자기 리버스 엔지니어링에 대한 이야기가 나와서 놀란 독자들도 있을 것이다. 왜 갑자기 리버스 엔지니어링 이야기가 나왔는지 이제 좀 더 자세히 풀어보도록 하자.
  이제 더 글을 읽기 전에 잠깐 디버깅을 했던 그 순간을 떠올려보자. 디버깅을 시작하는 이유는 프로그램에 문제가 있거나, 문제가 있을만한 부분을 확인하기 위함이다. 디버깅 툴을 켜는 순간 보통 '어디에서 문제가 발생했을까?' 라는 의문을 가지고 시작하게 된다. 그와 동시에 머리속에서는 프로그램이 동작하는 로직이 그려질 것이다. 디버깅을 하는 과정은 사실 리버스 엔지니어링을 하는 과정과 매우 유사하다. 단지 한가지 다른점이 있다면, 자신의 프로그램을 디버깅하는 프로그래머에게는 프로그램에 대한 소스와 그에 대한 정보들이 있지만, 리버스 엔지니어에게는 그러한 것이 없는 백지 상태에서 일이 시작된다는 것이다.
  다른 일 보다도 프로그래밍에 대해서는 경험에 대한 것을 더 가치롭게 여긴다. 이론적인 지식도 매우 중요하지만, 실제로 프로그램을 작성하는 프로그래머에게 빠져서는 안될 덕목 중 하나가 바로 경험이기 때문이다. 세상 모든 일이 그렇겠지만, 책에서 정리한 이론에는 실제 세상(Real World)에 있는 많은 것들이 미화되어있거나 빠져있는 경우가 많다. 좀 더 이해하기 쉬운 예를 들자면, 게임을 만드는 게임 프로그래머가 자신이 만든 게임을 가장 잘 하는 것은 아니라는 것을 독자들은 알 고 있을 것이다. 그것은 게임을 하기 위해서는 게임에 대한 법칙(Rule)을 잘 아는 것만이 아니라, 그 법칙들이 서로 상호관계를 맺고 있는 여러가지 사항들에 대해서 잘 알 고 있어야 한다는 것이다. 더욱이 온라인 게임의 경우에는 이러한 논리에 의해서만 설명되는 것이 아닌, 사람과 사람 사이의 상호관계에 대한 내용도 포함되기 때문에, 게임의 법칙(Rule)을 아무리 잘 알 고 있다고 해도, 그 게임의 절대적인 고수가 될 수 없는 것이다. 이와 마찬가지로, 엄청난 이론으로 무장하고 있는 프로그래머라고 할 지라도, 실제로 프로그램을 작성하는데에는 문제가 많을 수 있다.
 독자들이 실무에서 프로그램을 만들고 있고, 자신의 실력에 부족함을 느끼지 않는다면, 앞에서 필자가 말한 이론과 실제의 차이는 그리 큰 문제가 아니며, 관심거리도 아닐 것이다. 하지만, 애석하게도, 필자가 본 많은 프로그래머와 프로그래밍 관련 내용을 전공하는 많은 사람들은 자신이 알고 있는 것 만큼 프로그램을 잘 작성하지 못하는 사람들이 많았다. 무엇이 이러한 괴리를 만들어내는 것일까? 필자는 이 질문에 대한 해답을 기본적인 사고방식에서 찾고자 한다.
프로그래밍은 컴퓨터를 통해서 하는 일이다. 많은 사람들이 프로그래밍은 컴퓨터와 의사소통하는 것이라고 생각하고, 컴퓨터와만 잘 의사소통하면 된다고 생각한다. 하지만, 이러한 생각은 프로그래밍을 더 어렵게 만든다. 프로그래머가 컴퓨터와 의사소통을 하는데에 필요한 것은, 어셈블리 언어 혹은 프로그램이 만들어진 언어가 전부이다. 이 언어만 있으면 컴퓨터에게 무언가를 질문할 수 도 있고, 얻어낼 수 도 있다. 하지만, 많은 독자들이 알고 있겠지만, 이것만으로는 프로그램을 작성하는 것이 그리 순탄치만은 않다. 프로그램을 작성하면서 버그를 만났을 때, 버그를 도저히 찾을 수 없었던 경우가 있는지 모르겠다. 필자는 그런 경우가 꽤 많았다. 이러한 경우에, 많은 엔지니어들은 그러한 것을 기존에 존재하는 프로그래머들이 만들어 놓은 버그라고 생각한다. 왜냐하면, 그들이 알고 있는 논리와 기술적인 내용으로는 도저히 이해할 수 없기 때문이다. 하지만, 실제로 그 버그들의 정체는 나 자신의 문제인 경우가 대부분이다.
기존에 정상적으로 동작하고 있던 내용들이 자신의 프로그램으로 인하여 무언가 다른 동작을 하고 이상해 졌다면, 그것은 '나의 버그' 이다. 단순히, Windows 가 제공해주는 기능을 사용했는데 생각대로 동작하지 않는다고 해서 그것이 Windows 의 버그라고 할 수 는 없다. 실제로 Windows 는 그렇게 동작하도록 만들어졌고, 그렇게 동작하지 않는 것이 오히려 버그일 수 도 있는 것이다. 그렇다면, 이렇게 복잡하고 예측할 수 없는 환경에서 프로그램을 작성하기 위해서는 어떻게 해야 할까? 그것이 바로 리버스 엔지니어링을 배워야 하는 이유이다. 리버스 엔지니어링은 단순히 프로그램에 대하여 알아보는 것에서 그치는게 아니라, 프로그램을 만든 사람의 생각과 철학까지 리버싱 할 수 있어야 한다. 즉, 나 자신이 그 프로그램을 만든 사람의 입장이 되어보고, 왜 그렇게 만들었어야 했는지를 이해하여야 한다는 것이다. 그래야만, 그것에 따른 부가 효과(Side Effect)에 대해 이해하고, 그 부과 효과로인한 미묘한 동작들(실제로 많은 프로그래머들이 이해하지 못하고 버그라고 치부하는 것들)에 대한 이해와 올바른 사용을 할 수 있는 것이다. 필자는 리버스 엔지니어링을 하는 엔지니어는 마치 자연과학을 탐구하는 과학자와 같다고 생각한다. 왜냐하면, 자연과학자들은 우리에게 실제로 벌어지는 현상들을 토대로 하여 자연의 근본을 밝혀낸다. 그와 같이 리버스 엔지니어는 프로그램이 동작하는 실질적인 실행 과정을 통하여 프로그램의 근본 논리를 밝혀낸다. 모든 사물에 근본이 있듯이, 프로그램에도 근본적인 존재들이 있다. 프로그래머에게 있어서 가장 단순한 실행 상태인 CPU 의 명령어(Instruction)의 단계까지 내려가게 된다면, 그것을 기반으로 하여, 실질적인 프로그램의 논리를 파악할 수 있으며, 그렇게 되면, 사실상 프로그램의 원 소스 코드를 보고 있는 것과 별반 다를게 없는 형국이된다. 물론 이렇게 까지 자세하게 프로그램을 분석하는데에는 매우 오랜 시간과 노동이 필요하다. 하지만, 그것을 통해서 얻을 수 있는 것들은 결코 작지 않다.
우리가 살고 있는 세상은 엄청나게 작은 원자들이 모여서 분자를 이루고, 그것들이 다시 모여서 좀 더 큰 사물을 이루며, 그것이 다시 세상을, 지구를, 우주를 이루고 있다. 여기서 아주 작은 원자 하나가 바뀌게 된다면 어떻게 될까? 독자들이 알고 있는 것 처럼, 숱과 다이아몬드는 아주 작은 차이로부터 그 차이가 유래한다. 그것과 같이 근본적인 곳에서의 아주 작은 변화는, 실제하는 사물에서는 엄청나게 큰 변화를 초래한다. 이것은 프로그래밍에서도 동일하게 적용된다. 우리가 사용하고 있는 소프트웨어들은 수많은 계층 구조를 가지고 있다. 단순하게 생각했을 때에도, 이미 대부분의 소프트웨어는 Windows 라는 운영체제 아래에서 동작한다. 또한, 그 Windows 는 Kernel 이 존재하며, 이 Kernel 에 접근하기 위한 API 계층이 존재한다. 또, .NET Framework 등을 사용하는 프로그램의 경우에는 API 계층 아래에 .NET Framework 계층까지 존재한다. 프로그래머가 단순하게 생각하여 사용하는 가장 단순한 기능들도 이러한 무수히 많은 계층들을 거쳐서 실행되게 되는 것이다. 그렇기 때문에, 이 계층들에 대한 잘못된 이해는 실질적으로 엄청나게 잘못 만들어진 프로그램을 초래한다.
리버스 엔지니어링은 이러한 사소한 잘못된 이해들을 바로잡을 수 있는 방법이다. 물론, 잘못된 이해를 바로잡는 방법으로 정확한 레퍼런스를 보는것을 대안으로 주장하는 사람들도 있을 것이다. 하지만, 잘못된 편견을 가지고 있는 상태에서 정확한 레퍼런스를 본다고 하여, 그것이 올바르게 받아들여지지는 않는다. 마치, 지구는 둥글다라는 것을 실제로 보지 않고는 아무도 믿지 않는 것과 동일한 이치이다. 많은 프로그래머들이 오랜 시간 동안 프로그램을 작성하면서, 여러가지 시행착오를 겪고, 자신과 관련된 프로그램(e.g. 운영체제)에 대한 이해를 높여간다. 하지만, 정확한 분석 없이는 '그런것 같다' 라는 식의, 추측성 이해와 그로인한 임기응변성 테크닉만 늘어가게 된다. 예를 들면, 동기화 문제를 해결하는 방법은 여러가지가 있다. 하나는 정확한 논리를 사용하여 알맞은 동기화 객체를 사용하도록 하는 방법과, 동기화 문제는 주로 타이밍에 따라서 발생 빈도가 결정되므로, 타이밍을 좀 더 어긋나도록 하는 방법이 있다. 앞에서의 방법은 당연히 올바르고 정상적인 방법이지만, 후자는 임기응변식 방법이다. 이 방법은 마치 잘 해결되는 것 처럼 보이지만, 다른 사람에게 설명할 때에는, '그랬더니 잘 되더라' 라는 식의 두리뭉실한 설명밖에 해줄 수 없는 반쪽짜리 해결책이 된다. 또한, 이것은 해결책이라고 말할 수 도 없다. 왜냐하면, 타이밍이 달라지는 다른 PC, 특히 실행 속도가 차이나는 PC 에서 실행할 경우, 문제가 재발하거나 더 심각해질 수 있기 때문이다.
리버스 엔지니어링은 프로그램을 정확히 이해하는 과정이다. 정확히 이해한다는 것은, 말 그대로 '틀림이 없이' 이해한다는 것이다. 이 말은 참으로 혹독한 말이라서, 사실 정확하게 이해하기 위해서는 아예 그 프로그램의 소스코드를 읽을 수 있어야 한다. 하지만, 우리에게 소스코드는 주어지지 않는다. 그렇다면 어떻게 해야 할까? 프로그램을 정확히 이해하는 첫 걸음은 바로 프로그램을 오래 사용하여 익숙해지는 것이다. 아무리 뛰어난 프로그래머일 지라도, 익숙하지 못한 프로그램까지 잘 이해할 수 는 없다. 프로그램 자체를 한번도 보여주지 않고, '여기에서 A 버튼을 누르면 어떻게 될까요?' 라는 물음에는 절대 대답할 수 없다. 그렇기 때문에, 프로그램을 잘 이해하기 위해서 가장 먼저 해야 할 일은 프로그램에 익숙해지는 것 이다. 그러기 위해서는 먼저 프로그램을 많이 사용해보아야 한다. 특히, 입력에 대한 반응, 즉, 입력과 출력에 대한 부분을 집중해서 보아야 한다. 이러한 행동을 취했을 때, 이 프로그램의 반응이 어떤것인지를 알아야 한다. 필자가 말하는 리버스 엔지니어링은 프로그램의 모든 부분을 샅샅히 파해치는 것이 목적이 아니다. 앞에서 언급한 것과 같이, 훌륭한 프로그래머가 되기 위한 과정일 뿐이다. 즉, 다른 사람의 프로그램을 모두 해부해보는 것이 목적이 아니라, 나의 프로그램을 더 잘 만들기 위함이다. 그러기 위해서는 나의 프로그램이 상호작용하는 프로그램(대부분은 운영체제가 될 것이다)에 대하여 집중할 필요가 있다.
리버스 엔지니어링을 하면서, 버려야할 큰 것이 하나 있다. 바로, 통합 개발 환경(IDE)에 붙어있는 디버거를 사용하는 것이다. 그 이유는, 통합 개발 환경에 붙어있는 디버거는 운영체제의 좀 더 깊은 곳을 디버깅 하기에 적합하지 않기 때문이다. 물론, 적합하다고 하는 독자들도 있을 것이고, 실제로 그 환경이 더 편한 독자들도 있을 것이다. 하지만, 여기서 필자가 말하고자 하는 것은, 통합 개발환경에 붙어있는 디버거를 버리라는 것이 아니라, 통합 개발 환경에 있는 디버거를 사용하면서, 실제 라이브러리 내부의 코드들이나, API 함수 내부의 코드들을 디버깅하지 않으려는 자세를 버려야한다는 것이다. 실질적으로 많은 프로그래머들이 잘못 생각하는 것 중 하나는, API 는 완벽하다는 편견이다. 물론 API 는 대부분의 라이브러리나 함수들에 비해서 완벽하고 오류가 없다. 하지만, 이러한 것이 어떠한 환경에서도 절대로 API 는 문제를 발생시키지 않는다라는 이야기는 아니다. 특히 API 함수들 중, 다른 API 의 조합으로 만들어진 함수의 경우에는 다소 문제가 있는 경우가 더러 있다. 예를 들면,

+ Recent posts