본문 바로가기
3_ 매콤한 컴퓨터세상

개발자가 꼭 알아야 할 효율적인 프로그래밍

by 준환이형님_ 2010. 12. 16.

사로자바님의 권유로 마소홈피에 출근 도장을 찍게 되었지요..; 좋은 글이 있어서 포스팅 해 봅니다. 


IT 벤처 붐이 일었던 시절이 벌써 10년여 전이다. 끝없는 성공으로 이어질 것 같았던 벤처붐의 열기가 식은 후, 프로그래머라는 직업에는 ‘3D 직종’이라는 이름표가 붙었다. 필자는 이러한 현상이 벤처 붐이 일어나기 전에는 ‘프로그래머’만의 일이었던 프로그래밍이, 단순히 노동력을 투자하면 좋은 프로그램이 빠르고 많이 생산될 것이라는 잘못된 발상에서 유래된 것이라고 생각한다. 이러한 현실 속에서 누군가는 세상을 탓하며 이 분야를 떠났고, 또 다른 누군가는 남아서 새로운 성공을 꿈꾸고 있다. 지금 이 글을 읽고 있는 여러분들은 분명 후자일 것이리라. 그럼 또 다른 성공을 위해 필요한 것은 무엇일까?


권용휘 rodream@gmail.com | http://rodream.net에서 악성코드 제거기 울타리와 컴퓨터 최적화 프로그램인 클릭 투 트윅을 배포하고 있다. 2008년부터 Visual C++ 분야에서 Microsoft MVP로 활동하고 있으며 데브피아 Visual C++ 분야의 시삽을 맡고 있다.

3D 프로그래머의 역사
벤처 붐이 일어난 지도 어언 10년이 지났다. 그동안 프로그래머라는 직업은 많은 사람들에게 기피 직종 중의 하나가 되었다. 하지만 이러한 프로그래머가 처음부터 3D 직종으로 사람들에게 인식된 것은 아니었다. 벤처 붐이 한창이던 그 시절에는, 프로그래머라고 하면 ‘젊고 촉망되는 사업가’ 또는 ‘창의적인 발명가’의 이미지가 강했다. 그러나 이러한 이미지가 부정적으로 바뀌는 데에는 프로그래밍에 대한 업무적 이해가 없는 기업과 정부가 적지 않은 역할(?)을 했다. 프로그래밍이라는 일을 단순히 사람을 많이 쓰고, 야근을 해서 시간만 많이 늘린다면, 좋은 프로그램이 빠르게 개발될 것이라는 생각을 가진 기업들이 늘어나면서, 프로그래머를 창의적인 일을 하는 사람이 아니라 단순하고 반복적인 노동을 하는 인력으로 생각하게끔 만들었다. 또한, 정부 정책들도 핵심 기술을 진흥하고 발전시켜 나가는 것이 아니라, 단순히 규모만 큰 IT 업계를 만드는 것에 일조했다. 각 업무 특성에 맞는 인력 평가 시스템이 아니라 실제 업무에 연관성이 낮은 자격증이나 근무 시간만으로 인력을 평가하는 비논리적인 시스템으로 프로그래머라는 직종이 올바르게 대한민국에 정착하지 못하게 만든 것이다.
지금은 예전보다는 잘못된 인식을 가진 회사들이 줄어들었다. 이는 인식이 바뀐 이유도 있겠지만, 더 큰 이유는 프로그래머들이 자진해서 그러한 회사를 떠났기 때문이다. 지금으로부터 딱 1년 전, 2009년 여름을 가장 뜨겁게 장식했던 모 기업의 제품 발표회에서 그 회사 관계자가 그 제품을 만드는 과정에서 자사 직원들의 무수한 야근이 있었고 심지어 어떤 직원들은 이러한 야근으로 인해 이혼까지 했다는 것을 자랑스럽게 발표한 적이 있다. 이는 매우 부끄러운 사실이지만, 이를 부끄럽게 여기지 않는 우리 개발 환경의 현주소는 더 심각하게 느껴졌다. 이렇게 아직까지도 프로그래머에 대한 인식은 바닥을 치고 올라올 줄 모른다. 이러한 악조건 속에서 필자는 이 글을 읽고 있는 여러분들이야말로, 이러한 3D 프로그래머의 역사에 종지부를 찍을 수 있는 프로그래머가 되어야 한다고 생각한다. 여기서 필자가 적은 ‘되어야’ 한다는 것의 의미는 두 가지다. 먼저, 이렇게 단순 잡부(?)로 취급받지 않을 만한 실력을 갖춰야 한다. 다음으로는 그러한 실력을 가지고 자기 혼자만 잘 살 것이 아니라, 모든 프로그래머들이 더 좋은 환경에서 프로그램을 만들 수 있도록, 자신의 지식을 나눠주고 함께 발전해 나가는 프로그래머가 되어야 한다고 생각한다.

실력 있는 프로그래머
앞서 언급한 것과 같이, 실력 있는 프로그래머가 되려면 어떻게 해야 할까? 대답은 매우 간단하다. 실력을 쌓기 위해서는 자기 계발에 충실하면 된다. 하지만 역시 앞서 언급한 대로, 프로그래머에게는 자기 계발에 들이는 시간이 사치라고 느끼는 사람들이 많다. 구글의 전 직원들에게 주어진다는 한 시간의 자기 계발 시간이 각종 언론에 대서특필 된 것을 보면 알 수 있다. 하지만, 결코 하루 한 시간의 자기 계발 시간은 마음씨 좋은 회사가 베풀어야만 생기는 것은 아니다. 구글에서 이러한 제도를 시행하는 것 자체도, 구글의 경영진 입장에서는 좋은 프로그래머를 사용하고 그러한 좋은 인력을 지속적으로 좋은 상태로 유지하기 위해서는 하루 한 시간의 자기 계발 시간이 필수적이라는 생각에서 비롯됐다. 또한 이러한 자기 계발 시간으로 인해 줄어드는 근무 시간이 실제로 업무에 큰 영향을 미치기보다는, 오히려 앞서 말한 자기 계발로 인한 직원들의 능력 향상이 이러한 물리적인 업무 시간의 감소보다 더 큰 이점을 가져온다는 결론에 이르렀을 것이다.

필자가 생각하는 실력 있는 프로그래머는 이러한 자기 계발 시간을 스스로 만들어 낼 수 있어야 한다. 또한 이렇게 만들어 낸 자기 계발 시간에 충실해 더 실력 있는 프로그래머가 될 수 있어야 할 것이다. 물론 자기 계발 시간을 확보하기 위해 일을 하지 말라고 하는 것은 아니다. 이번 8월호 커버스토리의 제목과 같이, 필자가 생각하는 가장 좋은 방법은 효율적으로 일하는 것이다. 대한민국의 수많은 프로그래머들 중에서 스스로 효율적으로 일하고 있다고 생각하는 이는 얼마나 될까? 아직 정확한 통계자료는 나오지 않았지만, 아마도 그리 많은 숫자는 아닐 것이라고 생각한다. 부족한 개발 일정, 시시각각 바뀌는 요구사항, 무능한 프로젝트 관리자의 관리 부실, 기록도 남아 있지 않은 오래된 프로그램에 대한 고객 클레임, 그리고 프로젝트 진행 중에 들어오는 다른 프로젝트의 유지보수 요청. 앞에서 열거한 이러한 것들이 현실의 프로그래머가 처한 환경이다. 이런 것들이 없는 회사라면 하루에 1시간 정도가 아니라, 2~3시간 이상도 자기 계발에 투자할 수 있다. 또한 그런 것들은 회사의 입장에서도 또 다른 추가 수익을 불러오는 요소가 아니라 단순히 유지하기 위한 수단일 뿐이므로 아예 발생하지 않는 것이 이롭다. 예를 들면, 항상 문제가 생겨서 그것을 해결하고 있는 회사보다는 애초에 문제가 생길 것도 없이 좋은 프로그램을 만들어서 직원들이 자기 계발에 시간을 투자하는 회사가 더욱 좋은 회사이며 고객들도 그렇게 느낄 것이다. 그렇다면 이러한 여러 가지 문제들을 해결하기 위해서는 어떻게 해야 할까? 이제부터 소개할 내용이 바로 이러한 문제들을 효율적으로 해결하기 위한 방법들이다.
 
사회적인 해결 방법
프로그래머 또한 사회의 한 구성원이기 때문에 가장 근본적이고 큰 문제는 사람과 사람 사이의 갈등일 수밖에 없다. 특히 관리자와 실무자의 갈등이 이러한 나쁜 작업 환경을 만드는 가장 큰 원인이라고 할 수 있는데, 대부분의 회사들이 관리자만 해당 프로젝트의 내용을 알고, 실무자는 그러한 내용을 알리지 않게 하는 제도를 가지고 있다. 이러한 정보의 비대칭성은 결과적으로 관리자가 생각하는 최종 소프트웨어와 실무자가 만들어낸 최종 소프트웨어와의 차이를 더 늘리게 된다. 결과적으로, 프로그램을 다시 원래의 요구사항에 맞추기 위해서 불필요한 수정 작업이 늘어나게 된다. 한때 유명했던 만화인 ‘프로젝트가 실제로 진행되는 과정’(How Projects Really Works, http://www.projectcartoon.com/cartoon/2)에 보면, 실제로 고객이 원했던 내용과 최초의 고객이 설명했던 내용 자체도 큰 차이가 있는 것을 알 수 있다. 이러한 차이점들은 관리자와 실무자 간의 의사소통이 제대로 되지 않는다면 기하급수적으로 커지게 된다. 결국, 이렇게 의사소통을 제대로 하지 못한 상황에서 수많은 시간 동안 고생해 만든 제품은 고객도, 관리자도, 실무자도 만족하지 못한 결과물로 나올 뿐이다. 물론 어떠한 관리자나 고객도 만족시킬 수 있는 이상적인 프로그램을 만들 수도 있겠지만, 그러기에는 실질적으로는 시간이 부족하다. 앞서 언급한 내용들은 일반적인 솔루션 소프트웨어를 만드는 경우를 중심으로 이야기한 것이다. 하지만 이보다 더 악조건인 환경에서 근무하는 SI(System Integration) 업계의 이야기를 빼놓을 수는 없다. 사실 SI라고 하는 것이 무조건 야근하고 죽을지 살지 모르게 일을 해야 하는 직종은 아니다. 다만 대한민국에서 특히 SI 업계가 더 힘든 이유는 그 이름에서도 알 수 있듯이, 시스템 통합을 해야 하기 때문이다. 의사소통을 통한 효율적인 문제 해결이라는 것에 대해 다소 보수적인 한국사회가 SI 업계를 더 힘들게 만드는 것이다. 필자가 SI 프로젝트를 진행하는 과정에서 가장 크게 문제라고 느낀 것은 대부분의 SI 프로그래머들이 현재 자신이 하고 있는 프로젝트의 위치와 중요성을 파악하지 못하고 있다는 것이다. 그렇기 때문에 어떤 일을 끝내도 항상 남은 일이 있는 SI 업계에서 더 이상 버티지 못하고 나오게 되는 것이다. SI 프로젝트를 진행하고 있는 프로젝트의 실무자는 해당 프로젝트의 전체 그림과 회사가 하고 있는 여러 가지 프로젝트들에 대한 전체적인 그림을 그리는 것이 중요하다. 어떤 것이 중요하고 어떤 것이 중요하지 않은지를 파악해 회사의 비전과 생각에 맞는 일의 자체 스케줄링이 필요하다는 것이다. 혹자는 이를 대한민국의 SI 업계가 조건이 극악한 탓에 해야 하는 편법이라고 생각할지 모르지만, 이는 회사를 경영하는 이의 입장에서는 항상 직원들이 가지길 원하는 좋은 조건이며, 어느 사회에서건 실력 있는 자로 인정받고 살아남기 위해서는 반드시 해야 하는 일 중 하나다. 간혹 이러한 사람들을 자신만 편하자고 하는 기회주의자나 이기주의자라고 생각하는 사람들도 있지만, 사실은 이렇게 강약 있게 목표에 맞춰서 일하는 사람이야말로 어디에서나 필요한 고급 인재다. 앞서 프로그래머 스스로가 바꿔야 하는 여러 가지 것들을 알아보았지만 회사 자체에 대한 것들은 알아보지 않았다. 그 이유는 현실적으로 회사를 바꾸는 것보다 스스로 더 나은 회사를 찾아가는 것이 좋기 때문이다. 어느 그룹이건 문제가 있는 그룹은 항상 존재하는 법이기 때문에 모든 상황을 좋게 해결하는 것보다는, 다소 수동적인 자세로 보일 수 있지만 ‘절이 싫으면 중이 떠나는’ 방법이 더 현실적으로 현명한 방법이다. 그렇다면 중요한 것 중 하나인 ‘언제 절을 떠나면 되는가’라는 문제가 남는다. 이는 간략하게 이야기하면, 본인 스스로 효율적인 프로그래밍을 한다는 가정 하에 본인에게 도움이 되지 않는 환경이라고 생각되는 회사라면 떠나야 한다. 이러한 자신의 뚜렷한 기준 없이, 회사가 힘들다고 이직만을 고려하는 사람들도 있다. 물론 힘들면 회사를 옮기고 좀 더 좋은 길을 찾아가는 것은 개인이 추구해야 할 일이고, 그것에 대해 어떠한 나쁜 말도 할 수 없겠지만, 필자가 여기서 꼭 이야기하고 싶은 것은 과연 그렇게 힘든 회사를 만든 것이 바로 나 자신이 아니었을까라는 생각과 반성은 반드시 필요하다는 것이다. 또한 이직한다고 해서 모든 것이 해결되는 것은 아니다. 모든 환경이 완벽하게 좋은 회사는 찾기 힘들기 때문에, 어느 정도 문제가 있는 것들을 스스로 제어할 수 있는 사람이 되어야 원활하게 업무를 처리할 수 있다.

기술적인 해결 방법
지금까지 기술적이지 않은 측면에서의 문제 해결 방법들을 얘기했다. 하지만 프로그래머라는 직업의 특성상, 이러한 것들만 가지고 모든 문제를 해결하는 것은 사실상 어렵다. 앞서 언급한 것처럼 기술적으로도 우수한 프로그래머가 되어야 스케줄링과 같은 것들을 제대로 할 수 있기 때문이다. 회사에서 기술이 필요한 여러 가지 분야가 있겠지만, 필자가 가장 강조하고 싶은 부분은 바로 기술적으로 일을 많이 만들지 않는 프로그래머가 되는 것이다. 생각보다 많은 프로그래머들이 단순히 일을 많이 하게 되면 모든 것이 해결될 것으로 생각한다. 하지만 단순히 일을 많이 하는 것은 오히려 문제만 크게 만들 수 있다. 가장 무서운 사람이 바로 ‘무식하고 부지런한’ 사람이기 때문이다. 많은 프로그래머들이 생각하고 동의하기도 하는 ‘일단 지금 돌아가면 끝이다’라는 생각은 항상 시간이 부족한 프로그래머들에게 어쩔 수 없는 현실적인 선택처럼 보이지만, 실상은 오히려 그러한 것들 때문에 프로그래머들이 아주 오래된 프로그램의 버그를 고치게 되고, 예상치 못한 고객 클레임을 처리하면서 정작 중요한 프로젝트를 진행하지 못하는 결과를 초래한다. 또한 특정 운영체제에서만 잘 돌아가면 된다는 생각도 큰 문제다. 프로그램이 완성되는 시기나 완성도에 따라서 초기의 플랫폼 지원 계획은 바뀔 수 있고, 그러한 것이 빈번하기 때문에 특정 운영체제에서만 동작하는 프로젝트라고 할지라도 반드시 다른 운영체제를 염두에 둬야 한다. 또한 MSDN에 있는 내용을 충실히 지켜서 프로그램을 만든다면 특별히 많은 작업 없이 대부분의 운영체제에서 잘 동작하는 프로그램을 만들 수 있으므로, 특별한 기능이 없는 데에도 불구하고 특정 운영체제에서만 잘 동작하는 프로그램이라면 사실 그 프로그램은 잘 동작하는 것으로 알고 있는 운영체제에서도 불안정하게 동작하고 있을지도 모른다. 마지막으로 가장 중요한 내용이 남아 있다. 바로 Sleep과 MessageBox, OutputDebugString과 같은 로그 관련 함수들이다. 간혹 매우 해결하기 어려운 난제들을 접할 때 어떤 프로그래머들은 Sleep과 MessageBox, OutputDebugString을 코드에 추가했더니 문제가 사라졌다고 말하곤 한다. 하지만 원칙적으로 보면 이러한 것들이 문제를 해결해 주는 경우는 드물다. 특히 OutputDebugString과 같은 로그를 남기는 함수들은 로그를 쓰는 작업을 해야 하기 때문에 더 많은 문제를 생성해 낼 뿐이다.

효율적인 프로그래밍을 위한 지식 - API함수
API(Application Programming Interface)는 효율적인 프로그래밍을 하기 위한 가장 기본적인 지식이다. 모든 애플리케이션은 API를 사용해 시스템과 통신하기 때문에 API의 중요성은 굳이 강조하지 않아도 될 만큼 많은 사람들이 알고 있다. 하지만 그렇다고 해서 그런 모든 사람들이 API를 심각하게 생각하고 있는 것은 아니다. 가장 큰 예로 WinExec 함수를 들 수 있다. 이름에서 알 수 있듯이 이 함수는 다른 프로그램을 실행시키는 함수이다. 대부분의 프로그래머가 여기까지만 알고 있을 뿐 이 함수가 정확하게 어떻게 동작하는지는 잘 모르는 경우가 많다. 특히 이 함수가 언제 정확히 반환하는지에 대해서는 다소 의견이 분분하다. 어떤 사람들은 이 함수가 프로그램의 실행을 요청하자마자 반환한다고 생각한다. 또 다른 사람들은 이 함수가 프로그램을 실행시킨 후, Focus를 받은 직후에 반환한다고 생각한다. 실제로 이 함수를 실행시켜 보면 프로그램 실행이 된 직후에도 반환되지 않는데, 이를 보면 마치 이 함수가 Focus를 받은 직후에 반환하는 것처럼 생각될 수도 있다. 하지만 실제로 이 함수가 반환되는 시점은 다음과 같이 MSDN에 정의되어 있다.

[WinExec 함수가 반환되는 시점]
The WinExec function returns when the started process calls the GetMessage function or a time-out limit is reached. To avoid waiting for the time out delay, call the GetMessage function as soon as possible in any process started by a call to WinExec.

즉, GetMessage 함수가 호출되거나 정해진 시간 이상이 지난 경우에 반환된다는 설명이다. WinExec 함수를 사용자 인터페이스(User Interface)가 없는 프로그램을 실행하는 데 사용하게 되면, 함수가 반환되지 않는 문제가 발생할 수 있다. 사용자 인터페이스가 없는 프로그램은 GetMessage를 사용할 필요가 없기 때문이다. 하지만 이렇게 WinExec가 GetMessage 함수와 관련되어 있다는 사실을 모른다면 이러한 문제를 해결하는 데 많은 시간이 걸릴 수 있다.

또 하나의 좋은 예는 FindWindow 함수다. 이 함수는 주로 다른 프로그램의 HWND를 얻거나 다른 프로그램의 유무를 확인하고 싶은 경우에 많이 사용된다. 이 함수는 매우 많은 프로그래머들이 사용하는 함수 중 하나이며 사용법도 매우 간단하다. 하지만 이 함수가 잠정적으로 문제를 일으킬 수 있다는 사실은 잘 알려져 있지 않다. FindWindow의 문제점은 사실 그 함수 자체가 아니라 FindWindow 내부에서 사용하는 GetWindowText 함수다. 이 함수는 단순히 지정된 창 또는 컨트롤의 Text를 가져오는 작업을 하지만, 실제 내용은 다소 복잡하다. MSDN의 해당 함수에 대한 설명에는 다음과 같은 내용이 있다.

[MSDN의 FindWindow 함수에 대한 설명 중 일부]
If the lpWindowName parameter is not NULL, FindWindow calls the GetWindowText function to retrieve the window name for comparison. For a description of a potential problem that can arise, see the Remarks for GetWindowText

즉, FindWindow 함수는 GetWindowText 함수를 호출하기 때문에 GetWindowText에 있는 내부적인 문제가 함께 발생할 수 있다. MSDN의 내용 중에서는 이렇게 특정 Remarks를 언급하는 내용은 많지 않으므로, 이러한 내용은 반드시 주의 깊게 살펴봐야 한다.

[GetWindowText의 Remarks 부분]
If the target window is owned by the current process, GetWindowText causes a WM_GETTEXT message to be sent to the specified window or control. If the target window is owned by another process and has a caption, GetWindowText retrieves the window caption text. If the window does not have a caption, the return value is a null string. This behavior is by design. It allows applications to call GetWindowText without becoming unresponsive if the process that owns the target window is not responding. However, if the target window is not responding and it belongs to the calling application, GetWindowText will cause the calling application to become unresponsive.

위의 내용을 다시 정리하면 다음과 같다.

[GetWindowText의 Remarks 부분에 대한 정리]
(1) 찾고자 하는 창 또는 컨트롤이 코드가 실행되는 프로세스와 같은 프로세스에 존재한다면, GetWindowText 함수는 WM_GETTEXT를 해당 창 또는 컨트롤에 보내 값을 얻어온다.
(2) 찾고자 하는 창이나 컨트롤이 코드가 실행되는 프로세스에 없다면, 메시지를 보내지 않고 창 또는 컨트롤의 이름이 저장되어 있는 데이터에서 직접 값을 얻어온다.
(3) 창 또는 컨트롤이 이름을 가지지 않는 경우에는 빈 문자열을 반환하며, 이는 의도된 결과다. 즉, 버그가 아니다.
(4) (2)의 내용은 GetWindowText를 호출한 프로세스가 다른 프로세스가 WM_GETTEXT에 대해 응답하지 않음으로 인해 발생할 수 있는 응답 없음 상태를 초래하지 않게 하기 위함이다. 하지만 창 또는 컨트롤이 코드가 실행되는 프로세스에 존재하며 해당 컨트롤이 반응하지 않는다면 해당 프로그램은 응답 없음 상태가 될 수 있다.

하지만 위의 내용을 가지고도 언제 이것이 문제가 되는지를 명확하게 파악하기는 쉽지 않다. 실제로 이것이 문제가 되는 경우는 (4)번의 내용과 다른 프로세스에 코드를 주입하는 DLL Injection과 같은 기능을 사용하는 상황이다. 윈도우98에서 모든 시스템 프로세스에 DLL Injection을 사용하는 경우, 해당 DLL 안에서 FindWindow를 호출하게 되면 (4)의 시나리오가 발생할 수 있다. 여기서 공교롭게도 Kernel 프로세스가 가지고 있는 어떤 한 창이 메시지에 대해 응답하지 않는 경우가 있다. 결국, 이러한 문제들은 윈도우98 시스템 자체가 정지하게 만드는 큰 문제를 발생시키게 된다. 이런 문제를 해결하기 위해서는 GetWindowText 대신 직접 SendMessageTimeout과 같은 함수를 사용한 FindWindow와 관련된 함수를 만들어야 한다.

효율적인 프로그래밍을 위한 지식: 언어 자체에 대한 이해
프로그램을 작성하는 최종 단계는 프로그래밍 언어로 자신의 생각을 표현하는 것이므로, 언어를 이해하는 것은 매우 중요하다. 설계나 비전 공유가 잘 된 팀이라고 할지라도 실제 구현 자체에서 문제가 생기면 결과물이 나올 수 없기 때문이다. 그렇기 때문에 언어 자체에 대한 이해, 특히 잘 놓치기 쉬운 작은 것들까지 세심한 주의를 기울이는 것이 중요하다. 작은 내용들은 놓치기도 쉽지만, 이것들이 발생시키는 문제들을 찾아내기가 어렵기 때문에 찾아서 고치는 과정도 쉽지 않다.

이러한 놓치기 쉬운 실수들 중 가장 잘 알려진 것은 ++ 연산자다. 일반적인 수학식에서는 존재하지 않는 ++ 연산자는 변수의 앞뒤에 올 수 있다. 예를 들어, value라는 변수에서 ++value를 하는 경우에 해당 식이 계산에 쓰이기 전에 value의 값을 1 증가시킨 후 연산이 진행된다. 문제가 되는 경우는 이를 하나의 수식 안에서 여러 번 사용하는 것이다. 즉, result = (++value*5) + (value++*3)과 같은 형태의 식이 문제를 일으킬 수 있는 대표적인 경우다. 이러한 수식이 문제를 일으킬 수 있는 것은 위와 같은 수식이 명확하게 C 언어 표준에 의해 정의되지 않았고, 이러한 코드들의 동작은 컴파일러마다 다를 수 있기 때문이다. 실제로 이러한 코드들에 대한 C 언어 표준의 정의를 이해하기 위해서는 변수의 값이 변경되는 시점을 지정하는 C 언어 정의 내에서의 Sequence point에 대한 정의를 이해할 필요가 있다. Sequence point는 어떤 종속되지 않은 연산 문장의 끝 부분, 지정된 몇 개의 연산자 부분인 ||, &&, ?, :, comma 연산자(,) 등,  그리고 함수의 호출 이전 부분에 정의되어 있다. 다시 말해, 위에서 언급된 3개의 경우(문장의 끝, 지정된 몇 개의 연산자, 함수 호출 이전)에 변수의 값이 변경되어야 한다고 지정하고 있다. 또한 C 언어에서는 이러한 Sequence point에 관련된 정의가 더 존재하는데, 이는 하나의 Sequence point 안에서는 값의 변경이 최대한 한 번까지만 이뤄질 수 있다는 것이다. 다음은 그 정의에 대한 원문이다.

[Sequence point에 대한 정의]
Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored.

그러므로 이렇게 ++ 연산자가 2회 이상 사용된 부분은 표준에서의 정의에 포함되지 않는 내용이며, 실제로 <리스트 1>과 <리스트 2>는 ‘result = (value++ * 2) + (++value * 5)’를 서로 다른 2개의 컴파일러에서 컴파일한 결과를 비교한 것이다.

<리스트 1> result = (value++*2)+(++value*5);에 대한 어셈블리 코드 (컴파일러 1)
MOV EAX,DWORD PTR [ebp-8] ; value++
ADD EAX,1
MOV DWORD PTR [EBP-8],EAX
MOV ECX,DWORD PTR [EBP-8] ; value*5
IMUL ECX,ECX,5
MOV EDX,DWORD PTR [ebp-8] ; ecx(=value*5) + edx(=value)*2
LEA EAX,[ECX+EDX*2]
MOV DWORD PTR [EBP-4],EAX ; result = eax(=위의 계산)
MOV ECX,DWORD PTR [EBP-8] ; value++
ADD ECX,1
MOV DWORD PTR [EBP-8],ECX

<리스트 2> result = (value++*2)+(++value*5);에 대한 어셈블리 코드 (컴파일러 2)
MOV EAX,DWORD PTR SS:[EBP-8] 
LEA ECX,DWORD PTR DS:[EAX+EAX] ; ecx = value * 2
LEA EAX,DWORD PTR SS:[EBP-8]
INC DWORD PTR DS:[EAX]           ; ++value
MOV EDX,DWORD PTR SS:[EBP-8]
MOV EAX,EDX
SHL EAX,2                           ; eax = value * 4
ADD EAX,EDX                        ; eax = eax + value = value * 5
LEA EAX,DWORD PTR DS:[ECX+EAX] ; eax = (value*2)+(++value*5)
MOV DWORD PTR SS:[EBP-4],EAX
LEA EAX,DWORD PTR SS:[EBP-8]
INC DWORD PTR DS:[EAX]           ; value++
MOV EAX,DWORD PTR SS:[EBP-4]
MOV DWORD PTR SS:[ESP+4],EAX

 
이처럼 서로 다른 컴파일러에 대한 계산 결과는 <리스트 1>의 경우에는 (2*2)+(2*5)로 계산되어 14가 나오게 되고, <리스트 2>의 경우에는 (1*2)+(2*5)가 되어 12가 된다. <리스트 1>과 <리스트 2>의 차이의 기본적인 원인은 C 언어에서 이렇게 2회 이상의 단항 연산자에 대한 정확한 정의가 존재하지 않아서 이에 대한 내용을 각 컴파일러마다 다르게 적용할 수 있기 때문이다. 하지만 여기서 더 큰 문제는 같은 컴파일러라고 할지라도 항상 같은 형태로 계산하는 것은 아니라는 점이다. 앞에서 언급한 <리스트 1>의 컴파일러에서도 항상 이렇게 ++ 연산을 수식이 진행되기 전에 2회 모두 실행한 후 연산을 진행하지는 않는다. 일반적으로 위에서 언급한 <리스트 1>과 같은 형태는 특정한 경우라고 할 수 있는데, 이는 lea를 사용해 덧셈과 곱셈을 계산하는 것이 더 빠르기 때문에, 해당 컴파일러가 최적화를 진행한 결과이기 때문이다.
앞서 언급한 내용은, 언어를 이해하지 못해 만들 수 있는 실수와 관련된 부분이었다. 이와는 약간 다르지만, 언어를 정확하게 이해하지 못해 코드를 올바르지 않게 작성하는 예도 있다. 가장 일반적인 오해의 내용은 코드의 길이와 성능이 비례한다고 생각하는 것이다. 이러한 생각을 하게 하는 것은 바로 C나 C++ 언어의 구문들을 컴퓨터가 1줄씩 실행할 것이라고 생각하기 때문이다. 물론 비주얼 베이직과 같은 인터프리터 언어들은 실행 시간에 코드를 직접 분석해 실행하기 때문에, 프로그래머가 코드를 짧게 쓰는 것이 실행 시간을 단축시킬 수 있다. 하지만 대부분의 경우, 심지어는 비주얼 베이직의 경우에도 실제로는 코드의 길이가 실행 시간에 영향을 주는 중요한 원인이 되지 않는다. 특히, 비주얼 베이직이 아닌 많은 언어들은 문장의 끝을 세미콜론(;)으로 나타내게 되는데, 이것은 프로그래머들의 인식과는 달리 컴퓨터는 세미콜론으로 분리된 코드를 한 줄의 코드로 인식하는 차이가 있음을 이해해야 한다. 예를 들어, <리스트 3>과 <리스트 4>의 코드는 프로그래머의 눈에는 다소 다르게 보일 수 있지만 실행하는 컴퓨터의 입장에서는 사실상 동일한 코드이다.

<리스트 3> 변수를 초기화하는 간단한 코드nData = 10;
nIndex = 0;
nTemp = 20;

<리스트 4> <리스트 3>의 코드를 한 줄로 이어 붙인 코드
nData = 10; nIndex = 0; nTemp = 20;

그렇다면 좀 더 복잡한 예를 들어 보자. <리스트 3>과 <리스트 4>의 내용은 쉽다고 생각할 수도 있지만, <리스트 5>과 <리스트 6>의 내용은 생각보다 많은 프로그래머들이 다르다고 혼동하기 쉬운 내용들이다.

<리스트 5> 조건 판단을 위한 단항 연산자nData = nCheck ? 1 : 0;

<리스트 6> 조건 판단을 위한 if 구문
if (nCheck) {
    nData = 1;
} else {
    nData = 0;
}

<리스트 5>와 <리스트 6>은 디버그 모드에서 컴파일한 경우와 릴리즈 모드에서 컴파일해 최적화가 적용된 경우의 소스 코드가 다르다. 디버그 모드로 컴파일한 경우에는 <리스트 5>의 형태가 더 효율적이고, <리스트 6>의 경우에는 실제로는 크게 필요가 없는 분기 구문에 대한 어셈블리 코드가 포함되어 있지만, 릴리즈 모드로 컴파일한 경우에는 <리스트 7>과 같이 2개의 코드가 모두 동일한 어셈블리 코드를 만들어 낸다.

<리스트 7> 릴리즈 모드로 컴파일한 경우의 어셈블리 코드
.
.
00401009  |. 33C0           XOR EAX,EAX
0040100B  |. 85F6           TEST ESI,ESI
0040100D  |. 0F95C0         SETNE AL
.
.
0040101E  |. 33C0           XOR EAX,EAX
00401020  |. 85F6           TEST ESI,ESI
00401022  |. 0F95C0         SETNE AL


.
.

<리스트 7>에서 0x00401009에서 0x0040100D까지의 어셈블리 코드가 단항연산자를 사용한 코드의 부분이며, 0x0040101E에서 0x00401022까지의 어셈블리 코드가 if 구문을 사용한 코드다.

어셈블리 언어와 같이 기계어에 가까운 언어일수록 실제로 코드 길이는 성능에 지대한 영향을 미친다. 어셈블리 언어의 경우에 대해 첨언하자면, 기계어 코드에는 하나의 코드가 실행되는 시간이 있으며 최근의 CPU들은 이러한 명령어들을 병렬로 처리하는 특징이 있으므로 성능을 최대한 최적화한다면 이러한 명령어들의 실행 속도를 생각해 적절히 배열하는 수준까지도 할 수 있다. 이러한 정도의 최적화는 대부분의 프로젝트에는 적용되기 힘들며, 아주 일부분의 성능이 매우 중요한 프로젝트에만 적용된다. 그러나 대부분의 프로그래머들은 이러한 어셈블리 수준의 코드 길이보다 C 언어와 같은 고급 언어의 형태를 가지고 있는 언어들의 코드 길이에 더 민감하다. 그렇지만 C 언어만 해도 성능과 코드 길이는 다소 많이 동떨어져 있다. 대부분의 고급 언어들은 실제 실행 파일이 되기 위해 중간에 코드를 변환하게 된다. 그러므로 프로그래머가 어떻게 코드를 작성하는지보다는 컴파일러가 코드를 어떻게 바꿔주는지가 사실상 성능에 더 큰 영향을 미친다고 볼 수 있다. 앞서 언급한 ‘?’ 연산자와 if 구문이 대표적인 예라고 할 수 있다. 하지만 은근히 ‘?’ 연산자를 더 즐겨 사용하는 프로그래머들이 많다. 이와 같이 짧은 코드를 선택하게 되는 경우에는 성능은 그대로인 경우가 많지만, 가독성은 반드시 떨어진다. 이는 컴파일러와 상관없이 프로그래머가 그렇게 코드를 기술하는 것 자체가 가독성이 떨어지기 때문이다. 즉, 대부분의 짧은 코드들은 성능과 가독성을 교환해야 한다. 하지만 대부분의 경우 앞서 적은 내용과 같이, 실제로 교환되는 게 아니라, 한쪽은 아예 의미 없는 행위가 되는 경우가 많다. 특히, 고급 언어를 사용하는 프로그래머들은 애초에 이러한 것들을 염두에 두는 것이 좋다.

효율적인 프로그래밍
비록 대한민국에서 프로그래머로서의 생활이 아직까지는 모든 사람들에게 좋게 받아들여지고 있지는 않은 게 현실이다. 그러나 필자는 이러한 현실을 그대로 놓아 둘 것인지, 아니면 더 좋게 바꿀 수 있을 것인지에 대한 선택과 능력도 프로그래머 자신에게 있다고 생각한다. 이 글에 이어질 파트에서는 효율적인 프로그래밍을 위한 좀 더 자세한 내용들을 다룰 것으로 기대되며, 다소 어렵거나 원론적인 내용이더라도 실제 실무에 큰 보탬이 될 것으로 생각한다.

 

참고자료
1. Visual C++ 파워풀 개발 테크닉,- 권용휘, 김용현, 신영진 - 웰북