"나는 저 거대한 심연에서 악마를 불러낼 수도 있지."
"그쯤이야, 나도 할 수 있고 누구든 할 수 있지 않소.
한데, 당신이 부르면 악마가 오기는 하는 거요?"- 셰익스피어 (Shakespeare), "헨리 4세" 1부
세심한 기능 정의, 주의 깊은 명세, 겉치레 기능이나 기술적인 공상을 배제하도록 훈련하는 일 모두 시스템 버그의 수를 줄여준다.
> 한 마디로 개념적 일관성
의 획득이 중요하다.
코드가 한 줄이라도 만들어지기 훨씬 전부터, 명세는 외부 테스팅 그룹으로 넘겨져서 완전성과 명확성을 검토 받아야 한다.
시스템 구축을 설계, 구현, 제품화로 나눈 것은 하향식 설계의 개념이 적용된 것이며 더 나아가서 설계, 구현, 제품화의 각 과정은 하향식으로 이루어질 때 가장 좋은 결과를 가져온다.
작업 정의를 세분화함에 따라 해볍을 담은 알고리즘도 점차 세분화되며, 그 과정에서 데이터 표현법 또한 세분화될 수 있다. 이런 과정에서 해법이나 데이터 내의 모듈
들이 드러나게 되는데, 각 모듈은 다른 작업과 무관하게 각각 세분화해 나갈 수 있다. (Divide and Conquer... 그것은 40년이 지나도 변하지 않는 만고불변의 진리)
좋은 하향식 설계는 여러 면에서 버그를 회피하도록 해준다:
나는 (책의 저자; 프레더릭 브룩스) 하향식 설계가 프로그래밍에 있어서 근래 10년을 통틀어 가장 중요하고도 새로운 형식화 방법이라고 확신한다.
프로그램에서 버그를 줄이려는 또 다른 일련의 아이디어는 주로 데이크스트라(Dijkstra; 아마 에츠허르 데이크스트라 선생님을 말하는 듯하다. 사실 컴퓨터 계에서 Dijikstra 는 이분 밖에 없긴 하다.) 로부터 비롯되며, 이것은 뵘(Bohm) 과 야코피니(Jacopini) 에 의한 이론적 구조에 근거를 둔다.
원래 다 옮길 생각이 없었는데 저자의 주장이 너무나도 공감이 되서 한번 그대로 옮겨 보겠다.
이 접근법(여기에서 말하는 이 접근법은 데이크스트라 선생님이 작성하신 Go To Statement Considered Harmful 을 말하는 것이라 생각한다) 은 기본적으로 프로그램의 제어 구조를 DO WHILE 같은 반복문으로만 구성하고, 조건부로 수행될 문장들은 꺽쇠로 둘러싸서 IF ... THEN ... ELSE 로 구분하도록 설계하는 것이다. 뵘과 야코피니는 이런 구조가 이론적으로 충분함을 보인다. 데이크스트라는 다른 대안, 즉 GOTO 에 의한 무조건 분기가 논리적 오류에 취약한 구조를 만든다고 주장한다.
이런 기본 개념은 확실히 타당하다. (필자 역시 동의하는 바이다). 그 위로 다양한 비평이 가해졌는데, 여러 경우를 처리하기 위한 다방향 분기(소위 CASE 문), 장애 상황에서 탈출하기 (GO TO ABNORMAL END) 같은 추가적인 제어 구조는 상당히 편리하다. 매우 교조적(敎條的) 인 어떤 이들은 GO TO 를 전혀 쓰지 말자고 주장하는데, 이것은 다소 지나친 듯하다.
정말 GOTO 를 무슨 죄악으로 여기는 경우가 더러 있으나 이는 정말 프로그래밍의 식견이 짧은 사람이라고 밖에 생각하기 어렵다. (물론, 에츠허르 데이크스트라 선생님이 그렇다는 말은 아니다. 그 역시 Turing Award Prize Winner 이며 인류를 대표하는 컴퓨터 과학자이므로 필자가 그를 판단할 자격도 주제도 안됨을 미리 밝힌다.)
이러한 무조건 분기(unconditional branch)가 정말 유용한 순간은 분명히 존재한다. 위에서 소개한 CASE
와 GO TO ABNORMAL END
의 Exception Handling
이 바로 그러한 대표적인 경우이다. 따라서 필자는 GOTO
의 해악을 인지하면서 아주 국소적이며 특정한 경우에 한하여 GOTO
의 기능을 십분 활용하는 프로그래밍을 지고의 선
으로 바라본다.
중요한 것, 즉 버그 없는 프로그램을 만드는 데 필수적인 것은, 시스템의 제어 구조를 개별적인 분기문이 아니라 그야말로 제어 구조로 생각해야 한다는 점이다. 이와 같은 사고방식이 앞으로 한 발 나아가는 큰 걸음이 된다.
정말 너무 적고 싶은 말도 많고 하고 싶은 말도 많은데 Linus Torvalds 의 TED 영상을 올리고 여기에서 줄이겠다. 그가 하고 싶었던 말 또한 이와 동일하다.
v르고르매을 디버깅하는 절차는 지난 20년에 걸쳐 커다란 주기를 지나왔고, 어떤 면에서는 출발했던 곳으로 다시 돌아와 있다. 그 주기 안에는 네 단계가 있었는데 이는 이하와 같다.
초창기 컴퓨터들은 입출력 장치가 상대적으로 빈약했고 입력과 출력 간의 시간 간격도 길었다. 따라서 디버깅 작업은 할당받은 장비 세션 내에 가능한 한 많은 시도를 하도록 설계 되었다.
프로그래머는 디버깅을 할 때 실행을 어디서 중단
하고, 메모리의 어느 곳을 조사
할 것이며, 거기서 무엇을 찾아야
하고, 만약 찾지 못한다면 어떻게 할 것인지
같은 절차를 주의 깊게 설계했다. 이는 프로그래밍하는 데 드는 시간은 디버깅할 프로그램을 짜는 시간의 절반에 이르기도 했다.
실행 중단 지점이 계획적으로 설정된 테스트 구획으로 프로그램을 분할해 두지도 않고 대담하게
시작
버튼을 누르는 것은, 말하자면 용서받지 못할 죄를 짓는 일이였다.
온-머신 디버깅은 매우 효과적이어서 두 시간 세션이면 열두 번 (지금은 5억번을 돌릴 것 같은데 ㅎㅎ...) 은 돌릴 수 있었다. 하지만 컴퓨터는 몹시 부족했고 비용도 많이 들었기에, 컴퓨터 시간을 낭비한다는 것은 생각만으로도 끔찍한 일이었다.
그러다가 고속 프린터가 온라인으로 연결되면서 방법도 바뀌었다. 프로그램은 검사가 실패할 때까지 수행되었고, 그 다음에는 메모리 전체가 덤프되었다. (흔히 말하는 core dump; core 는 자기 코어 메모리를 의미하며 과거의 메모리를 의미함. 지금은 RAM dump 가 맞을 수 있겠다.)
이후 각 메모리 주소의 내용을 해명하기 위한 고된 서류 작업이 시작되었고 이 작업은 온-머신 디버깅 때와 그다지 다를 바 없었지만, 테스트 수행 전의 계획 단계가 아니라 테스트 후의 해독 단계 라는 차이가 있었다.
이러한 절차는 전반적으로 컴퓨터 사용 시간을 최소화해서 가능한 한 많은 프로그래머가 이용하도록 하는 데 중점 을 두고 설계됐다.
메모리 덤프 기법이 개발되던 때의 장비들은 2000 ~ 4000 워드, 즉 8KB 에서 16KB 정도의 메모리를 가지고 있었다. 그러나 메모리 용량이 극적으로 증대되면서 전체 메모리를 덤프한다는 것은 비현실적인 일이 되었다.
그렇게 해서 선택적 덤프, 선택적 추적, 프로그램 내 스냅샷 삽입 같은 기법이 개발되었다. OS/360 의 TESTRAN 은 이 방식의 종착지라 할 수 있는데, 어셈블이나 컴파일을 다시 하지 않고도 프로그램에 스냅샷을 넣을 수 있게 해 주는 도구였다.
최초로 접했던 온-머신 디버깅과 오늘날의 대화식 디버깅 사이에서 사용자가 느끼는 주된 차이점은, 관리 프로그램 및 언어 해석기 덕분에 이용 가능해진 여러 가지 도구일 것이다. 개발자는 고급 언어로 프로그램을 짜고 디버깅할 수 있게 되었으며, 효율적인 편집 도구는 프로그램 변경과 스냅샷 작업을 쉽게 해준다.
즉각적인 회송이라는 온-머신 디버깅의 장점으로 다시 돌아오기는 했지만, 디버깅 세션 전의 사전 준비까지도 다시 필요하게 된 것은 아니다. 어떤 의미에서는 이런 준비 작업이 당시만큼 필요하지 않게 되었는데, 앉아서 생각할 동안 컴퓨터 시간이 낭비되지는 않기 때문이다.
그렇긴 해도, 골드(Gold)의 흥미로운 실험 결과는 대화식 디버깅의 각 세션에서 첫 번째 인터랙션의 진척률이 그 이후에 비해 세 배에 달했음을 보여준다. 이것은 사전 계획의 미비로 인해 대화식 디버깅의 잠재력이 발휘되지 못하고 있음을 강력히 시사 한다.
좋은 터미널 시스템을 제대로 쓰기 위해서는, 두 시간의 터미널 세션마다 두 시간의 서류 작업이 필요한 것 같다. 그 시간의 절반 동안에는 마지막 세션의 뒷 정리를 하는데, 디버깅 로그를 갱신하고, 프로그램 리스팅의 최신판을 노트에 철해두고, 이상한 현상을 해석하는 등의 작업을 하게 된다.
솔직히 필자는 여기에서 하는 얘기가 정확하게 와닿지가 않아서 뭔가 정리를 하려고 했는데도 중구난방이 되어 버렸다. 간단하게 추려서 정리하면 다음과 같다고 생각한다:
온-머신 디버깅
은 미리 디버깅할 위치를 따로 뽑아서 그 부분에 대해서만 국지적으로 디버깅을 수행한다.시스템 테스트의 어려움을 감안한다면 다음 두 가지는 수긍할 수밖에 없다.
통상적인 관례까지는 아니지만 상식적으로 생각할 때 시스템 디버깅은 각 부분이 제대로 동작하게 된 다음에 시작하는 것이 타당하지만 실제 관례는 두 가지 면에서 이런 상식을 벗어난다.
여기에는 구성 요소 버그 외에도 시스템(즉 인터페이스) 버그가 추가로 더 있을 거라는 인식이 깔려 있다. 각 부분을 일찍 통합할수록 시스템 버그도 일찌감치 드러난다는 것이다. 약간 덜 세련된 것으로, 각 부분을 서로 테스트에 이용함으로써 테스트용 비계(飛階; scaffolding) 만드는 일을 상당히 줄일 수 있다는 견해도 있다.
하지만 현장의 경험은 그것만이 전부가 아님을 말해준다. 디버깅되어 깨끗한 상태의 구성 요소를 사용함으로써 시스템 테스트에서 얻는 시간적 이득은, 비계를 만들고 구성 요소를 철지히 테스트하는 데 소요된 시간보다도 훨씬 크다.
여기서는 어떤 구성 요소의 결함이 모두 발견되었을 때
시스템 테스트에 들어갈 준비가 된 것으로 본다. 물론 이것은 결함이 모두 수정될 때
보다는 훨씬 전이다. 이 이론에 의하면, 발견된 버그에 의한 효과가 어떤 것인지는 파익된 상태이므로, 시스템 테스팅에 들어가서는 그런 효과를 무시(?)하고 새로운 현상에 집중할 수 있다는 것이다.
그러나 이 모든 이야기는 단지 늦어진 일정을 합리화하고자 만들어낸 희망 사항일 뿐이다. 알려진 버그가 어떤 효과를 일으키는지 모두 알 도리는 없다.
여기서 비계라고 함은, 디버깅을 목적으로 만들어졌지만 최종 제품에 포함될 일은 전혀 없는 프로그램과 데이터를 두루 일컫는 말이다.
비계에는 다음 종류가 있다:
제품이라 할 수 있는 문서
에 대한 엄격한 통제와 깊은 존중이 절실히 필요하다. 이 기법에서 중요한 요소는, 모든 변경 사항을 일지에 기록해 두는 것, 그리고 충분한 검토와 테스트와 문서화를 거친 수정 사항과 응급조치 간의 차이점을 소스코드 상에 두드러지도록 구분해두는 것이다.
이 교훈 역시 너무 뻔해 보이지만, 낙관주의와 게으름이 우리로 하여금 이것을 어기도록 유혹한다. 교훈대로 하자면 더미 종류를 비롯한 이런저린 비계가 필요하고, 그러려면 작업을 해야 한다. 그리고 어쨌거나, 그 모든 작업은 아마도 괜한 일이 되지 않을까? 버그가 없을지도 모르는 일 아닌가?
그렇지 않다! 유혹을 물리쳐라! 그것이 체계화된 시스템 테스트가 목표로 하는 모든 것이다.
유의할 것은, 완전한 테스트 케이스를 확보해 두고 새로운 요소가 추가될 때마다 시스템의 해당 부분을 테스트해야 한다는 점이다. 다른 요소가 잘 동작했더라도 새 요소가 추가된 후에는 회귀 테스트를 통해 다시 검사되어야 한다.
리먼(Lehman)과 벨러디(Belady)는 변경 사항의 묶음이 아주 크고 띄엄 띄엄하거나, 아니면 아주 작고 빈번해야 한다는 증거리르 제시하고 있다. 그들의 모델에 따르면 후자의 전략이 더 불안정하며 내 경험(저자)도 그렇다. 나는 그런 전략을 택함으로써 위험을 감수할 생각이 전혀 없다.
[책] 맨먼스 미신: 소프트웨어 공학에 관한 에세이 (프레더릭 브룩스 지음, 강중빈 옮김)
[영상] https://www.youtube.com/watch?v=o8NPllzkFhE