. 주유소 이야기

자동차 도로를 만들고 관리하는 데는 막대한 비용이 드는데 국가에서는 이 비용을 국민의 세금으로 충당한다. 그런데 일반 세금으로 도로를 닦는다면 이는 엄청난 불평등을 초래할 것이다. 강원도 산골짜기에 사는 사람이나 전라도 섬지역에 사는 사람의 경우 평소에 차를 몰고 다니지도 않는데 도로를 설치하는 세금을 부담하라고 한다면 몹시 불만스러울 것이다. 그래서 차를 많이 사용하는 사람에게 도로 설치에 필요한 세금을 부담시키는데 이를 전문용어로 수혜자 부담의 원칙이라고 한다.

그렇다면 누가 차를 많이 몰고 다니는지 어떻게 알 수 있을까? 차를 가진 사람들에게 올해 얼마나 돌아다니셨어요? 하면서 일일이 물어볼 수도 없고 물어본다고 해도 정직하게 대답할지도 의문이다. 자동차 거리계라는 것도 조작이 가능하므로 믿을 수가 없으며 길목마다 지키고 서서 누구 차가 이 도로를 지나 다니는지 일일이 체크할 수도 없다. 세금을 걷어야 도로를 설치하고 관리할 텐데 도대체 누구한테 얼마를 걷어야 할지를 결정하기가 난감한 것이다.

하지만 조금만 생각해보면 기가 막힌 방법이 있다. 차를 운전하는 사람은 반드시 기름이 필요하고 많이 돌아 다닐수록 많은 기름을 소비하기 마련이다. 차를 이고 다니지 않는 바에야 기름없이 어떻게 차가 갈 수 있겠는가? 그래서 국가는 도로 사용에 대한 세금을 기름값에 부담시키며 이는 가장 공평하고 합리적인 방법이다. 도로를 쓰는 사람은 반드시 주유소에 와야만 하는 것이다. , 주유소는 도로 수혜자가 반드시 거쳐야 하는 피할 수 없는 길목이며 국가는 이 길목을 지키고 앉아서 세금을 편하게, 공평하게 걷고 있는 것이다. 물론 가짜 휘발유라는 방법으로 이런 길목을 피해 다니려는 고약한 사람들도 있긴 하지만 대부분은 이 길목을 벗어나지 못한다.

이런 길목은 코드에서도 찾을 수 있다. 어떤 동작을 하려면 반드시 그곳을 지나야만 하는 곳이 있는데 그곳이 바로 코드의 길목이다. 특정 동작을 할 때 같이 수행해야 하는 처리가 있다면 길목만 찾아서 코드를 작성하면 된다. 바로 앞의 Insert, Delete 함수가 길목의 좋은 예이다. 북마크는 문서가 편집될 때마다 조정되어야 하며 문서가 편집되는 길목 중 가장 좋은 곳이 Insert, Delete 함수이다.

왜냐하면 ApiEdit의 모든 편집코드는 Ime3 시절부터 Insert를 통하지 않고서는 단 1바이트도 삽입할 수 없도록 되어 있으며, 문자열을 삭제하려면 반드시 Delete를 호출해야 하기 때문이다. 만약 코드의 여기 저기서 아무런 원칙도 없이 buf를 편한 대로 뜯어 고치고 지우도록 되어 있다면 북마크를 조정하는 것은 아주 어려운 작업이 될 것이다. 문서가 변하는 모든 곳을 찾아 북마크를 관리해야 하는데 이렇게 되면 코드량도 늘어날 뿐만 아니라 이후 북마크 관리 규칙이 변하게 되면 수정하기도 아주 어려워진다.

이런 구조일 때 가장 큰 문제는 불일치가 발생할 수 있다는 점이다. 한 동작을 두 군 이상에서 하고 있는데 각각의 코드가 다르다면 어떻게 되겠는가? 일부러 그렇게 하지는 않겠지만 코드를 수정하다 보면 원치 않게 실수를 하게 된다. 어떤 경우는 잘 되고 어떤 경우는 잘 안되는 골치 아픈 버그가 마구 양산될 것이다. 재현되지 않는 버그가 가장 골치 아픈 버그이다.

그래서 코드 하나를 작성할 때도 이 코드가 들어가야 할 가장 적당한 길목이 어디인가를 찾는 것이 중요하며 길목을 잘 찾아야 코드가 단순해지고 튼튼해진다. 이런 예는 앞에서 이미 많이 봐 왔다. PrevX 값을 갱신할 가장 좋은 곳은 어디인가? 바로 SetCaret이었다. 캐럿이 이동하지 않고서는 PrevX가 바뀌어야 할 필요가 없으며 SetCaret 외의 다른 곳에서 PrevX 값을 건드려야 할 이유가 없다. 이 위치를 잘못 찾으며 코드가 꼬이기 시작한다. 포맷팅영역을 재계산하기에 가장 좋은 곳은 OnSize이고 GetLine을 호출하기에 가장 좋은 곳은 UpdateLineInfo 함수이다. 더블 버퍼링 비트맵을 파괴할 곳은 OnSize이고 다시 만들어야 할 곳은 OnPaint이다.

사실 여러분들은 길목을 찾는데 이미 아주 익숙해 있다. 운영체제가 제공하는 메시지란 것도 비유를 하자면 모두 중요한 길목들인데 그리고 싶을 때는 WM_PAINT, 초기화 할 때는 WM_CREATE를 사용하도록 되어 있고 키입력을 처리할 때는 WM_KEYDOWN을 통해야만 한다. 운영체제가 이런 메시지를 보내 주는 이유가 바로 길목을 활용할 정확한 시점을 제공하기 위해서이며 개발자는 정해진 약속대로 코드를 작성한다.

길목을 잘 찾는 것보다 더 중요한 것은 스스로 길목을 만들어 나가는 것이다. 어떤 중요한 동작에 대해서는 반드시 지나가야 하는 길목을 만들어 놓고 모든 코드가 이 길목을 통해서만 그 동작을 할 수 있도록 구조를 만든다. 대표적인 예가 Insert이다. ApiEdit는 여기 저기서 문서를 바꾸고 있지만 직접 buf에 무엇인가를 삽입하는 곳은 Insert뿐인데 그 이유는 이 함수를 문서변경의 길목으로 활용하기 위해서이다. 앞으로도 문자열 삽입시에 반드시 해야 할 일이 있다면 모두 이 함수안에 작성될 것이다. SetCaret, Invalidate 등도 의도적으로 만들어진 길목이다.

길목을 설치하고 사용하는 것은 약간의 비효율을 동반한다. 바로 써 넣을 수 있는 코드를 반드시 특정 함수를 경유하도록 코드를 작성해야 하기 때문이다. OnSetFocus에서는 캐럿을 보이게만 만들면 되므로 많은 계산을 하는 SetCaret을 호출하지 않고 CreateCaret를 바로 호출해도 상관없다. 하지만 일관성을 위해 약간의 효율을 희생한 것이다. 속도가 조금 느려지더라도 논리적인 구조를 위해서는 길목을 잘 설계하고 의도적으로 길목을 통해 다니는 것이 좋다.

특히 코드량이 늘어나면 늘어날수록 길목의 중요성은 더욱 증가한다. 처음 프로젝트를 시작할 때는 이런 저런 좋은 아이디어들이 마구 떠오르기 마련이고 이 코드들이 하나 둘씩 구현되어 코드량이 신나게 늘어난다. 하지만 처음 구조가 잘못 잡히면 어느 순간에 더 이상 코드가 늘어날 수 없는 한계에 부닥치게 된다. 기능 하나를 넣으면 이미 넣은 기능 두 개가 안되고 그 두 개를 고치다 보면 잘 되던 기능 네 개가 말썽을 일으키는 악순환이 연속되는 것이다. 이런 구조의 한계를 경험하게 되는 시기는 대체로 소스가 만 줄(프로젝트의 성격에 따라 차이가 있다)을 넘어갈 때이다. 그 전에는 대충대충 코드가 엮어지지만 만 줄을 넘으면 그때부터는 대충이 통하지 않는다.

여기서 길목이라는 비유를 사용했는데 길목은 변수나 객체일 수도 있지만 구체적으로 말하면 함수이다. 앞으로 계속 실습을 진행하면서 또 이후에도 잘 만들어진 예제를 분석할 일이 있다면 코드의 내용만 보지 말고 왜 하필 저기다 작성할까 유심히 보기 바란다. 특히 MFC STL같은 고급 라이브러리를 사용할 때 코드 작성 위치를 정확하게 선정하는 기술이 필요하다.