TheArtandScienceofSmalltalk:Chapter 15
- 제 15 장 스몰토크 코드 디버깅하기
스몰토크 코드 디버깅하기
앞에서 제시한 디자인 접근법과 코딩 지침을 따른다 하더라도 처음부터 버그로부터 완전히 자유로운 스몰토크를 작성할 가능성은 거의 없다. 운 좋게도 스몰토크 개발 환경의 상호작용적 특성은 버그를 발견하고 수정하는 작업에 적합하다.
이론상 주요 툴-통지자, 인스펙터, 디버거-은 상대적으로 사용하기가 쉽다. 하지만 숙련된 프로그래머들이 사용하는 '비법과 요령'이 몇 가지 있다. 이는 디버깅 과정의 속도를 상당히 증가시켜 혼란을 줄여준다. 본장에서는 스몰토크 디버깅을 논한 후 버그를 감지하고 피하는 방법에 대한 조언과 함께 스몰토크 코드 내 공통적으로 나타나는 버그의 리스트를 제시하고자 한다.
버그의 종류
스몰토크에서는 기본적으로 세 가지 종류의 버그가 있다고 말할 수 있겠다. 첫 번째 종류는 코드가 구문적으로 틀린 경우에 수반된다. 컴파일러는 (accept를 실행할 때마다 호출됨) 시스템에 구문적으로 올바르지 않은 코드를 추가하지 못하도록 한다. 스몰토크의 '과학'을 충분히 이해하여 본문에서 다루지 못했던 문제도 고칠 수 있기를 바란다.
두 번째 버그 종류는 스몰토크가 자신의 코드를 실행하면 실행 도중에 문제가 발견되는 경우에 발생한다. 다시 말해 시스템이 실행하지 못하거나 실행하지 않을 무언가를 발견한다는 말이다. 일반적으로 그 다음에 알림 창이 뜬다. 통지자는 문제가 무엇인지 알려주고 (가능할 경우) 계속할 것인지, 디버깅 창을 열 것인지, 아니면 코드 실행을 취소할 것인지 선택권을 제공한다. 이는 우리가 살펴볼 버그들 중 하나다.
세 번째는 당신의 코드는 올바르나 디자인에 무언가 잘못된 구석이 있을 경우 발생한다. 프로그램은 올바로 실행되지만 당신이 원하는 작업을 실행하지 못한다. 이는 당신이 기존 클래스의 특정 기능이 작동하는 방법을 이해하지 못했거나, 클래스들 중 하나의 구현부 혹은 디자인이 올바르지 않아서 발생할 수 있다. 이러한 버그 종류를 발견하기 위해 자신의 코드 실행과 시스템의 코드를 추적하는 방법 또한 살펴볼 것이다.
현실에서는 물론 네 번째 버그 종류도 있을 수 있다-전달된 클래스 라이브러리에서 사전에 제공되는 버그. 드물긴 하지만 실제로 존재하며, 특히 신규 배포(release)일 경우 더 그러하다. 본장에서 제시한 기법들은 이러한 유형의 버그를 추적할 뿐만 아니라 심지어 (당신에게 그런 재주가 있다면) 수정까지도 도와줄 것이다.
일반 디버깅 원칙
디버깅에 관한 한 스몰토크는 다른 프로그래밍 언어들과 많은 공통점을 가진다. 동일한 원칙이 적용되긴 하지만 숙련된 프로그래머들조차 때로는 원칙을 잊어버리므로 이러한 '규칙'을 먼저 상기시켜보도록 하겠다.
첫 번째 일반 원칙은 오류 메시지를 읽는 것이다. 어떤 컴퓨터든 문제의 기반이 되는 근원이 아니라 직접적이고 표면적인 원인만 알려주지만, 오류 메시지가 알려주는 내용은 살펴볼 만한 가치가 있다. '아, 오류다!'라고 생각되면 곧바로 코드에서 문제를 찾고 싶은 마음이 생길 것이다. 하지만 기억하라. 시스템은 당신에게 무언가 말하고 있다. 최소한 무어라 말하는지 들어나 봐라.
오류 메시지를 읽었다면 속도를 늦춰라. 잠시 멈추고 문제가 무엇인지 생각해보라. 디버깅은 처음에 수정하도록 충분한 시간을 들일 가치가 있는 사항들 중 하나에 해당한다. 주의하지 않으면 문제에 가까워지지 못하고 이것저것 다른 것들을 시도하는 자신의 모습을 발견할 것이다. 특히 스몰토크로 소스 코드의 검사와 단순한 수정을 허용하는 시스템의 경우에 그렇다.
다음 일반 원칙은 어떤 것도 가정하지 말라는 것이다. 특정 시점에 변수가 특정 값을 갖고 있다고 확신하거나, 코드의 특정 부분을 통한 제어 흐름을 확신한다고 해서 실제로 그렇다는 의미는 아니다. 의심이 가는 부분을 확실히 확인하라. 다시 말하지만, 스몰토크 환경은 이 과정을 수월하게 만든다.
이러한 종류의 검사를 강요하는 한 가지 방법으로, 다른 사람에게서 도움을 받는 방법이 있다. 냉소적이고 설득하기 힘든 사람일수록 더 좋지만, 'card-board cut-out' 조차 버그를 발견하게끔 유도하는 추론 과정으로 나아가는 데 도움이 될 수 있다. 자신의 시스템을 다른 사람에게 설명하는 행위나 이러한 행위의 다이어그램을 그리는 행위는 매우 유용하게 작용할 수 있다.
다음으로, 제공된 디버깅 툴의 사용 외에도 자신만의 디버깅 코드를 빌드할 수 있음을 기억하라. 파일로 로그하거나 트랜스크립트 창에 인쇄하는 것이 도움이 된다면 충분한 시간을 갖고 그렇게 하라. 심지어 좀 더 복잡한 '모니터'를 생성하여 원한다면 '트리거'할 수도 있다. 일부 언어에서는 고역이지만 스몰토크에서는 꽤 수월하게 완료할 수 있다.
스몰토크에서 수월한 또 다른 작업은 stubs의 구성이다. 올바른 이름과 매개변수를 가진 메서드를 당신이 결국 작성하게 될 메서드로서 정의하되 최종 구현부를 포함하지 않을 수 있다. 'canned' 고정 값을 리턴하거나 halt로 놓고 (얼마 안 되어) 디버거를 이용해 메서드가 리턴했으면 하는 값을 수동으로 채울 수 있다.
마지막으로, 시스템의 작은 조각들을 빌드, 검사, 디버깅하고자 시도해야 한다. 하지만 프로그램에 부딪칠 경우 자신의 코드를 더 작은 의존적 조각으로 나누고 개별적으로 검사하는 것이 도움이 된다. 그래도 작동이 안 되면 구체적인 test case를 빌드해보라. 다시 말하지만 작동하지 않는 이유를 알기 위해서라도 이러한 작업에 시간과 수고를 들일 가치가 있다.
통지자와 디버거 사용하기
자신의 코드에서 버그에 대해 알게 될 첫 번째는 아마 팝업되는 알림창일 것이다. 스몰토크 통지자는 시스템이 특정 메시지 표현식을 어떻게 처리하는지 모른다거나 하는 '예외적' 상황에서 발생하는 예외에 대한 응답으로서 팝업된다. 다음 페이지에 실린 다이어그램에 몇 가지 통지자가 표시되어 있다.
방금 살펴본 일반 원칙에 따라, 가장 먼저 해야 할 일은 멈추고 통지자를 살펴보는 일이다-읽지 않고 무작정 닫지 마라. 스몰토크 통지자는 꽤 유용한 정보를 많이 제공한다. 예외의 이름은 분명하게 알 수 있다 ('메시지를 이해할 수 없음', '0으로 나눔', 등). 예외가 발생 시 정확히 어떤 일이 발생하는지를 보여주는 스택 back-trace 도 받게 된다.
Back-trace에서 각 행은 오류가 발생한 곳의 하단부터 시작해 상향으로 전송된 메시지를 표시한다. 모든 행에서 당신은 메시지를 수신한 객체의 클래스명을 확인하고, 메서드를 실제로 구현한 그 슈퍼클래스명을 확인한 후 (괄호로), 메서드 선택자 자체를 (>>다음) 표시한다. 다시 말해, 각 행은 다음과 같은 모양을 한다:
Receiver-Class (ImplementorClass) >> message Select or.
스택 back-trace의 최상위에서 스몰토크는 예외를 발생시킨다 (그렇다, 오류 메시지를 생성한 코드를 실제로 볼 수 있다!). 즉, 거의 대부분의 경우 두 번째 행 밑으로 관심을 둘 것이다. 여기에 오류의 직접적 원인이 위치한다. Back-trace는 오류가 발생한 메서드명을 알려주는데, do it, print it, 또는 inspect 도중에 오류가 발생했다면 unboundMethod를 알려준다.
두 번째 행 밑에는 스택 상에서 오류를 야기한 모든 메시지가 존재한다. 콜 스택(call-stack)을 보고 있음을 기억하라. 즉, 예외 이전에 전송되었으나 이미 리턴된 메시지들은 (버그의 궁극적인 원인이라 하더라도) 당신에게 보이지 않을 것이란 의미다.
또 기억해야 할 한 가지는 스몰토크에선 '시스템' 코드와 '당신의' 코드를 구별하지 않는다는 사실이다. 그들은 시스템에 관한 한 하나이자 동일하게 취급된다. 즉, 스택 back-trace는 클래스 라이브러리로부터의 코드와 당신의 코드가 쉽게 섞인 채 구성될 수 있다는 말이다. 따라서 당신에게 표시되는 코드를 모두 알아채지 못하더라도 염려하지 마라. 다음 페이지에 실린 다이어그램은 이러한 현상의 예를 몇 가지 표시한다.
그림에서 보듯이 약간의 시스템 코드부터 당신의 코드가 실행되기 시작하면서 당신의 코드에 예외가 발생하는 스택이 표시될 것이다. 이는 특히 작업공간에서 do it을 실행한 결과로 발생한 예외의 경우 사실이다.
또 다른 경우는 당신의 코드가 클래스 라이브러리 내 어떤 메서드를 호출하면 시스템 코드에 결국 예외가 발생하기 전에 클래스 라이브러리 내 잠재적으로 엄청난 수의 다른 메서드를 호출하게 되는 상황이다. 이는 당신이 시스템 버그를 발견했다는 의미가 아니다! 당신이 코드에서 무언가를 실행하여 문제를 야기하였고, 시스템이 마침내 자체 코드에서 금지된 일을 수행하고 나서야 문제가 인식되었음을 의미한다 (예: Collection에 표시되지 않는 요소로 접근).
오류 메시지와 스택을 살펴본 후에 할 일은 디버거를 열어야 하는지 결정하는 일이다. 당신이 가진 스몰토크 버전에 따라 팝업 메뉴를 통할 것인지, 푸시 버튼을 통할 것인지 결정권이 주어진다. 문제의 원인을 알고 있다고 생각한다면 그것도 좋다. 통지자를 닫고 가서 수정하라. 오류를 발생시킨 코드를 자세히 살펴보길 원한다면 변수를 검사하고 코드를 수정할 기회를 가진 다음 디버거를 열어라.
디버거를 열기로 결정했다면 크게 만들어라. 무엇이 잘못되었는지 살펴보려는 것이기 때문에 작은 창으로 코드를 힘들게 쳐다볼 필요가 없다 (스몰토크가 창을 여는 기본 값은 때때로 터무니없이 작다. 스몰토크의 과학에서 디버거의 기본적인 사용은 다루었고, 매뉴얼에도 포함되어 있다. 중요한 것은 주어진 기능을 완전히 사용하도록 확실히 하는 것이다.)
시스템이 코드를 통하는 경로를 살펴보는 스택을 위아래로 살펴볼 수 있다. 스택이 충분치 않다면 스택 페인(stack pane)의 operate 메뉴를 통해 더 요청할 수 있다. 통지자에서와 같이 자신의 코드와 시스템의 코드가 혼합되어 있을지도 모른다. 스택의 최상위에서 시스템이 보인다면 스크롤다운하여 어디서 코드가 시작되는지 확인할만한 가치가 있다. 자신의 코드와 시스템 코드 간 인터페이스는 당신의 코드가 오류를 발생시키지 않았음을 보여주는 마지막 기회다. 여기서 주로 버그가 존재한다. 하지만 당신의 코드가 훨씬 이전에 오류를 발생시켜 (콜 스택 내부 또는 외부에) 한참 후에 결국 예외가 발생할 때까지 나쁜 객체(bad object)로 본인의 객체가 전달되었을 가능성도 있음을 기억하라. 실제로 버그가 있는 메서드가 이미 리턴했다면 콜 스택에서는 볼 수 없을 것이다. 이런 경우 당신의 코드를 인터럽트하여 버그를 수동으로 찾는 작업을 따라야 할 것이다. 이를 수행하는 방법을 간략하게 살펴보자.
메서드에 대한 매개변수와 임시 변수뿐 아니라 인스턴스 변수의 상태를 살펴보기 위해 디버거의 하단에서 두 개의 내재된(embedded) 인스펙터를 이용할 수 있다. 인스펙터에서 코드를 실행할 수도 있음을 기억하라. 객체가 메시지에 어떻게 응답하는지 시험하고 싶다면 인스펙터 중 하나가 그것을 검사하고 표현식을 입력하여 선택한 후 평가하도록 하라.
문제의 근원을 볼 수 있다고 생각된다면 디버거의 코드 패인을 브라우저처럼 사용하면 된다. 코드를 편집하여 수락할 수 있다. 단, 메서드에 대해 다른 브라우저가 열려 있어서 다시 그 브라우저를 사용하기 위해 되돌아간다면 코드에 곧바로 오류가 옮겨갈 수 있으니 주의하라! 이러한 일이 발생하는 이유는 브라우저가 표시하는 코드가 다른 곳에서 변경된다 하더라도 브라우저는 스스로를 자동으로 업데이트하지 않기 때문이다.
코드 인터럽트하기
스몰토크는 <control>-c 를 눌러 스몰토크를 인터럽트 할 수 있다. 그러면 어떤 일이 발생하든 그것을 중단시키고, '사용자 인터럽트' 예외를 발생시켜 통지자를 연다. 거기서 언제나처럼 디버거를 열 수 있다. 시스템이 '유휴상태'일 때에도 가능하며, 스몰토크의 일반 프로세싱 루프를 인터럽트할 것이다. 이 작업이 흥미로워서 디버거를 열고 시스템이 어떻게 작동하는지 살펴볼 수도 있겠다.
시스템이 <control>-c 에 응답하여 중단되는 상황이 간혹 발생한다. 그 이유는 주로 이미지가 어떤 방식으로든 손상되었기 때문이다. 그런 경우 <shift>-<control>-c 를 눌러보라. 그러면 비상 평가자(emergency evaluator) 라는, 시스템의 최소량만을 이용하는 일종의 '최후 수단'을 열 것이다. 여기서 단일 스몰토크 표현식을 입력하여 평가할 수 있는 기회를 얻게 된다 (<return>이 아니라 <escape>로 종료). 선택은 자신의 상황에 따라 좌우되겠지만 몇 가지 유용한 선택을 들어보겠다:
Processor acti-veProcess terminate. (To try to recover.)
ObjectMemory quit. (To give up and kill Smalltalk.)
무엇을 입력해야 할지 모르겠다면 (적어도 어떤 환경에서는) 다른 스몰토크 이미지를 시작해 살펴보고 결정할 수 있음을 기억하라. 하지 말아야 할 한 가지는, 손상된 이미지를 저장하는 일이다. 손상된 이미지를 저장한 후 이미지를 재시작하면 정확히 똑같이 손상된 상태가 될 것이며, 앞에서 한 작업까지 덮어 씌었을 것이다!
중단점 삽입하기
때로는 예상치 못한 예외의 결과로 팝업된 디버거에서 문제의 실제 근원을 볼 수가 없다. 그 이유는 호출되어 이미 리턴된 메서드에 실제 문제의 근원이 존재하기 때문이다. 이러한 경우, 자신의 코드에 중단점(breakpoint)을 설정하여 오류를 포착하기 위해 잇따른 실행 경로를 따를 수 있다.
스몰토크에서는 디버거를 통해 중단점을 설정하지 않는다. 대신 당신이 원하는 때와 장소에 발생하도록 특별한 유형의 예외를 야기하는 코드 조각을 프로그램으로 삽입한다. 그리고 결과가 되는 통지자에 대한 응답으로 디버거를 열 수 있다.
예외를 발생시키려면 halt 메시지를 전송하면 된다. 이 메시지는 object 에서 구현되므로, 시스템 내 모든 단일 객체는 메시지를 이해하고 동일한 일-예외 발생-을 수행한다. 중단점을 설정하기 위해 일반적으로 하는 일은 코드의 실행을 중단시키고 싶을 때마다 self halt 표현식을 넣는 것이다. 문자열을 매개변수로 취하여 오류 메시지에 포함시키는 half; 메시지를 이용할 수도 있겠다. 이는 코드로 삽입한 다수의 halt 중에 어떤 것이 실제로 도달했는지를 알아내는 데 도움이 된다!
halt가 발생하면, 계속 진행하도록 결정내릴 수 있지만 디버거를 열기로 결정을 내릴 가능성이 크다. 그러면 변수의 값을 보고 메시지를 전송할 수 있다. 이후 step과 send 버튼을 이용해 다음 메시지를 전송하거나, 다음 메시지를 전송하여 고유의 구현부까지 따라 내려갈 수 있다. 단일 단계 이동(single stepping)이 지겹다면 operate 메뉴에서 proceed를 선택할 수 있다.
원한다면 시스템 코드에 self halt를 삽입하는 것이 가능하다. 단, 예외를 발생시키거나, 창을 열거나, 혹은 halt를 다시 꺼내는 것이 가능하도록 만드는 데 필요한 코드 조각에 halt를 넣을 경우 통지자(notifier)가 끝없이 발생할 것이므로 주의하길 바란다! 이러한 경우 앞서 설명한 비상 평가자를 사용할 가능성이 크다.
때로는 특정 조건이 true가 되기 전에는 halt를 원하지 않을지도 모른다. 그러한 경우 조건을 테스트하여 true일 경우에 halt할 수 있다. 코드를 특정 횟수만큼 살펴본 후에 halt를 원할 수도 있다. Proceed를 계속해서 사용해야 하는 수고를 덜어주기 위해 전역 변수를 만들어 계속 증가시키다가 올바른 값이 되면 halt한다. 마지막으로, 코드를 처음으로 살펴볼 때 halt하길 원하는 경우도 있다 (아마 위에서 설명한 통지자의 흐름을 피하기 위해). 이때는 가령 DontHalt라 불리는 전역 변수를 생성하고, false로 초기화하여, halt를 고려하기 이전에 전역 변수가 false인지 확인한 후 실제로 halt를 실행하기 직전에 true로 설정하라.
Halt의 실제 발생 여부를 제어하는 또 다른 방법으로, conditionallyHalt와 같은 메서드를 (Object 클래스로 추가하라) 작성하는 방법이 있다. 해당 메서드에서 특정 조건을 검사하고 (예: 시프트 키를 누른 상태) 상태가 true일 때에만 halt를 실행하라. 그러면 halt 대신 conditionallyHalt를 사용 시 시프트 키가 눌러져 있는지에 따라 코드의 halt 여부를 제어할 수 있다. 시프트 키의 누름 상태를 결정하는 표현식은 다음과 같다:
ScheduledControllers activeController
sensor shiftDown.
실행 추적하기
디버거를 이용한 단일 단계 이동을 통해 코드가 취한 경로를 따라갈 수 있다. 하지만 변수 값이 무엇인지 혹은 특정 메서드가 호출되었는지 여부만 알아보는 데 사용하기엔 지루할 수가 있다. 이러한 경우를 대비해 스몰토크는 디버깅을 위한 재래식 프로그램에 삽입할 수 있는 print 문과 동등한 것을 제공한다. 아래와 같은 표현식을 이용해 트랜스크립트 창에 어떤 문자열이든 인쇄할 수 있다:
Transcript show: 'Here is a string'; cr.
위의 표현식은 'Here is a string'을 인쇄한 다음 캐리지 리턴(carriage return)이 따라온다. 전역 변수 Transcript는 트랜스크립트 창에 대한 모델인 (MVC적 의미에서) TextCollector 클래스의 인스턴스를 보유한다. 트랜스크립트로는 문자열만 전송할 수 있음을 주목하라. printString 메시지를 먼저 전송함으로써 다른 객체들도 인쇄할 수 있다. 예를 들어:
Transcript show: 'anArray is ', anArray
printString; or.
무언가 발생했는지 여부만 알고 싶다면 어떤 일이 발생할 때 시스템이 울리도록 만들 수도 있다. 단순히 아래 표현식을 필요한 곳마다 삽입하면 된다. 하지만 실제로 사용하기 전에 자신이 사용 중인 특정 플랫폼에서 작동하는지 확인해볼 필요가 있다. 그 때 사용할 표현식은 다음과 같다:
Screen default ringBell.
실행을 추적하는 또 다른 방식으로는, 파일로 이벤트를 로깅하거나 '추적 레코드'의 OrderedCollection을 구축하여 코드가 실행되는 동안 중요한 변수의 값을 포착하는 방법이 있다. 이러한 방식에는 당신의 프로그램이 행한 일의 순서를 확인하기 위해 컬렉션을 쉽게 검사할 수 있다는 이점이 있다.
객체 찾기
종종 당신의 코드가 클래스의 인스턴스를 하나 또는 그 이상 생성하였다는 사실은 알지만 인스턴스들이 어디로 갔는지 모르는 경우가 있을 것이다. 이때 유용한 메시지가 몇 가지 있는데, 기억하는 것이 좋겠다:
MyClass allInstances.
MyObject allOwners.
allInstances 메시지는 어떤 클래스로든 전송 가능하다. 해당 메시지는 전체 시스템을 돌아다니며 검색하여 클래스의 모든 인스턴스로 구성된 컬렉션을 리턴한다. 잃어버렸던 클래스의 단일 인스턴스를 생성하였거나 (특히 그것이 runaway Process이거나 그것을 종료하고 싶을 때), 클래스의 인스턴스들 중 어떠한 이유로 인해 쓰레기로 수집되지 않을 인스턴스가 수백 개가 있다고 생각되는 경우에 유용하겠다.
allOwners 메서드는 어떤 객체로든 전송 가능한데, 이 또한 수신자에 대한 참조를 전체 시스템에서 검색한다. 이는 (인스턴스 변수 또는 종속자에) 수신하는 객체에 대한 참조를 가진 다른 객체들의 컬렉션으로 응답할 것이다. 다시 말하지만, 해당 메서드는 일부 객체들이 쓰레기로 수집되지 않는 원인을 찾는 데 유용할 수 있다.
위의 두 메서드 모두 매우 자원 집약적이어서 평균 사이즈의 이미지를 완료하는 데 몇 초간의 시간이 소요됨을 주목하라. 따라서 해당 메서드는 디버깅 시 매우 유용하게 남아 있음에도 불구하고 이를 사용하는 애플리케이션을 디자인 시에는 의당 매우 주의를 기울여야 한다.
의존성 디버깅하기
앞장에서는 객체들 간 의존성 관계가 일반 관계보다 디버깅하기가 좀 더 까다롭다는 사실을 관찰했다. 그 이유로는 두 가지를 들 수 있다-의존성이 저장되는 방식과 의존성이 사용되는 방식 때문이다.
먼저, object에서 구현되는 바와 같이 (하지만 Model에서 재구현되는 방식과는 달리) 의존성 메커니즘은 의존적 객체를 보유하기 위해 인스턴스 변수를 사용하지 않는다. 대신 의존성을 가진 객체들로 가득 찬 dictionary를 보유하는 클래스 변수를 사용한다. 즉, Object의 서브클래스의 종속자들은 Model의 서브클래스의 종속자에 비해 인스펙터에서 살펴보기 쉽지 않다. 인스펙터로 객체를 바라보면서 객체의 종속자들을 보고자 하는데 종속자 인스턴스 변수가 하나도 없는 경우 인스펙터의 우측에 self dependents 표현식을 입력하여 검사하라. 그러면 객체의 종속자들로 이루어진 컬렉션에 대한 인스펙터가 제공될 것이다.
두 번째 문제는 변경 메시지가 종속자를 가진 객체로 전송될 때마다 의존성은 (업데이트) 메시지가 다른 객체들에게 전송되도록 야기한다는 점이다 (확신하지 못할 경우 제 8장을 다시 참조). 이러한 업데이트 메시지들은 '업데이트' 메서드가 행하는 일에 따라 임시적으로 다른 코드를 다량 실행시킬 수 있다. 하지만 디버거에서 단일 단계 실행 시, 변경 메시지의 실행을 위해 step 만 이용할 경우 이러한 업데이트 메시지를 놓쳐 실행되는 코드를 놓치게 될 것이다. 전체 의존성 메커니즘이 촉발(fire)되어 '업데이트' 메서드의 실행을 야기하고, 이는 결국 당신이 보지 못하는 다른 코드를 호출할 것이다. 이러한 메시지가 전달되는 것을 보기 위해서는 디버거에서 단일 단계 실행을 하는 동안 변경 메시지를 실행할 때 send를 사용해야 한다. 그리고 마침내 당신의 업데이트 메서드가 실행되는 모습을 볼 때까지 실행되는 시스템 코드를 작업하려면 계속해서 send를 사용해야 한다.
스몰토크 프로그램에서 공통적인 버그
수많은 종류의 버그들이 스몰토크 프로그램으로 들어간다. 하지만 그 중 다수는 반복해서 나타나는 경향이 있다. 일부 버그는 포착하기가 쉽지만, 일부는 서서히 퍼진다. 이번 장의 마지막 절은 대부분 스몰토크 프로그래머들이 한번쯤은 (때로는 한번 이상) 빠지는 함정을 몇 가지 설명한다. 이러한 버그를 읽는다고 해서 버그를 피할 수 있는 것은 아니지만 당신이 버그를 발견하는 날이 오면 재빠르기 인식하고 수정하도록 도와줄 것이다! 특히 까다로운 버그 때문에 곤혹스럽다면 본장의 마지막 절을 참고하면 되겠다.
doesNotUnderstand: 메시지
이것은 통지자가 팝업될 때 볼 수 있는 가장 흔한 오류 메시지일 것이다. 해당 오류가 발생하는 원인에는 두 가지가 있다: 올바른 메시지를 잘못된 객체로 전송하였거나 올바른 객체로 잘못된 메시지를 전송한 것이다. 통지자가 알려주는 메시지를 주의 깊게 읽으면 당신이 처한 상황을 알려줄 것이다. 메시지명 앞에는 #가 붙는데, 시스템이 그것을 기호(symbol)로 취급하기 때문이다. 당신의 (또는 시스템의) 코드에서 당신이 어디에 위치하는지 알 수 있겠는가? 만일 그렇다면, 어떤 것이 옳다고 인식되는가-전송된 메시지인가, 혹은 메시지를 수신한 객체인가?
올바른 메시지가 잘못된 객체로 전송되는 가장 흔한 상황은 메시지를 nil로 전송하려 할 때다. 이러한 경우, UndefinedObject가 (nil이 유일한 인스턴스인 클래스) 메시지를 이해하지 못한다고 말할 것이다. 이는 보통 무언가 정의, 초기화, 또는 할당되지 않았을 때 발생한다. 또한 이전 메시지가 (cascade에서 혹은 이전 표현식에서) 올바른 값이 아닌 nil을 리턴했을 때도 발생 가능하다. 이를 추적하려면 스택 back-trace를 사용해야 한다.
올바른 객체로 잘못된 메시지가 전송되는 경우는 더 복잡하다. 이론상, 컴파일러는 다른 클래스에 의해 구현되지 않은 메시지는 어떤 것도 수용할 수 없도록 한다. 즉, 전송된 메시지는 어떤 클래스의 인스턴스에 의해 이해되어야 한다. 아마도 당신의 오타로 인해 입력하려던 메시지가 다른 적합한(legal) 메시지로 변환되었을 경우가 크다. 보통 표현식의 괄호치기(bracketing)가 잘못된 경우에 발생한다. 이는 복합 메시지 표현식에서 쉽게 발생하며, 포착하기가 까다롭다. 따라서 이해할 수 없는 doesNotUnderstand: 오류를 받으면 괄호를 어떻게 배열했는지를 확인하라.
사본의 문제
스몰토크에서 객체는 '참조에 의해 전달'된다. 이는 시스템이 매개변수로서 전달되는 객체의 복사본이나 변수에 할당되는 객체의 복사본을 만들지 않음을 의미한다. 대부분은 괜찮지만 심지어 숙련된 프로그래머들조차 복사본이 생성되고 있지 않을 때에도 마치 그렇다고 가정하여 코드를 구성하는 경우가 종종 있다.
당신의 코드가 이상하게 스스로를 수정하는 것처럼 보일 경우, 당신은 클래스의 구분된 두 개의 인스턴스로 생각하는 것이 사실 동일한 인스턴스일지 모른다는 생각을 해본다. 그 여부는 두 개의 객체를 찾아서 (필요 시 인스펙터를 이용해 디버거로 찾은 다음 전역 변수로 할당함으로써) 둘 중 하나에게 나머지 객체를 매개변수로 한 == 메시지를 전송하여 확인할 수 있겠다. 예를 들어, MyRecord와 MyOtherRecord가 동일한 인스턴스인지 확인하려면 다음을 사용하라:
MyRecord == MyOtherRecord
표현식이 true로 평가되면 두 객체는 실제로 하나의 객체인 것이다. false로 평가되면 '='라 할지라도 둘은 구분된 객체인 것이다 (==와 =의 차이를 확실히 모른다면 제 6장, 스콜토크 클래스 라이브러리의 내용을 참고하라). 이와 같은 검사를 이용해 동일한 객체로의 다중 참조가 실제로 동일한 객체로의 참조인지 확인할 수 있다. 실제 객체로 작업하고 있다고 생각하는데 객체의 복사본을 조작하는 경우도 흔한 버그의 원인이 된다.
기존 컬렉션으로부터 새 컬렉션을 빌드 시 select:, reject: 또는 이와 유사한 메시지를 이용할 때에도 또 다른 문제가 발생할 수 있다. 컬렉션 객체는 새것이지만 그 내용은 기존 컬렉션에 있는 동일한 객체(의 하위집합)가 될 것이다. 이는 다른 컬렉션 내 객체들이 '마법과 같이' 변경하는지 확인하기 위한 용도로 하나의 컬렉션 내 객체를 수정할 때 놀라움과 상당한 좌절감을 불러일으킨다. 둘은 동일한 객체들이다!
동등관계(=) 대신 상등관계(a) 사용하기
한 객체가 다른 객체와 동일한 객체인지 검사하고 싶다면 동등관계 검사(equivalence test), '=='를 사용해야 한다. 상등관계의 '='를 이용하면 틀린 양의 결과(false positive result)를 제공할 것이다. 반대로 '='를 원하는데 '=='를 사용할 경우 틀린 음의 결과(false negative)를 제공할 것이다.
반복 도중에 컬렉션 수정하기
때로는 컬렉션을 반복하는 도중에 그 내용을 수정하고 싶을 때가 있다. 예를 들어, 99보다 큰 크기의 컬렉션 요소를 모두 제거하고 싶다고 가정하자. 이러한 경우, 아래와 같은 표현식을 쉽게 작성할 수 있겠다:
MyCollection do: E;i 1 (i size > 99) ifTrue:
EMyCollection remove: i]3.
위의 표현식은 작동하지 않을 것이다. 컬렉션을 반복하는 동안 크기를 줄이기 때문에 반복자는 요소를 계속해서 누락시킬 것이다. 무슨 일이 일어나는지 알아내기 전까지는 매우 당혹스러울 수 있다.
이러한 경우 해야 할 올바른 일은 select: 열거 메서드를 이용해 당신이 원하는 요소만을 포함하는 새 컬렉션을 빌드한 후 오래된 컬렉션을 새 컬렉션으로 대체하는 것이다. select: 또는 그 형제(cousin) 중 하나가 사실상 적합하지 않다면 당신이 새 컬렉션을 생성하여, 기존 컬렉션을 반복하는 동안 새 컬렉션을 채워야 한다.
시스템이 기존 컬렉션으로의 참조를 많이 갖고 있어서 새 컬렉션으로의 참조를 추적하여 변경할 수가 없을 경우, ValueHolder를 이용해 (제 10장 참조) 컬렉션을 보유할 것을 고려하라. 그러면 많은 객체들이 (오래 살아 온) ValueHolder에 대한 참조를 유지하면서도 그 값은 (컬렉션 객체) 계속 교체된다.
컬렉션을 반복하면서 컬렉션 내 객체의 '내부(internals)'를 수정하는 것은 전적으로 안전하다는 사실을 주목하라. 예를 들어, 아래의 코드는 꽤 안전하고 일반적이다. MyCollection 컬렉션 내 모든 객체의 크기를 확인하고, 99보다 크기가 큰 객체를 99로 설정한다:
MyCollection do: [:i | (i size > 99) ifTrue:
[i size: 99].
반환 연산자(^) 제거하기
self 이외의 값을 리턴하도록 되어 있는 메서드에서 리턴 연산자(^)를 빠뜨리기란 매우 쉽다. 올바르지 않은 객체로 올바른 메시지를 전송하는 것으로 보인다면, 콜 스택(call-stack)에서 초기에 올바른 값을 리턴 중인지 확인하라.
이러한 오류는 새로 생성된 인스턴스의 리턴 실패 시 리턴된 클래스를 야기하는 (클래스 메서드 내 self) 인스턴스 생성 메서드의 경우 특히 끔찍하다. 이때 당신이 '새' 객체로 첫 번째 메시지를 전송하려 하면 doesNotUnderstand: 예외를 야기하게 될 것은 거의 확실하다. 불행히도 이러한 예외는 당신의 새로운 객체가 '클래스'라는 단어를 미묘하게 포함하는 것만 제외하면 그것이 이해해야 하는 메시지를 이해하지 못하는 것처럼 보이게 만든다. 다시 말해, 아래에서 첫 번째 예외를 얻지만 두 번째 예외를 보고 있다고 생각한다:
MyClass class (object) >> doesNotUnderstand:
MyClass (object) >> doesNotUnderstand:
새로 생성된 객체가 일부 메시지를 이해하지 못하는데 그 이유를 알 수 없다면 다음을 확인하라-인스턴스 대신 클래스를 사용하고 있을지 모른다!
블록에 '^' 사용하기
당신이 원하는 것을 모르는 바는 아니지만, 블록으로부터 그것을 실행하는 메서드로 꺼내기 위해 리턴 연산자(^)를 사용할 수 없다. 예를 들어:
MyBlock := [:colour | (colour > 3)
ifTrue: [^#red]
ifFalse: [^#yellow]].
블록을 실행하는 메서드로 블록을 꺼내고 싶다면, 자신의 블록을 구조화하면 그 끝에서 꺼낼 수가 있다. 블록의 '리턴' 값은 그것이 포함한 마지막 표현식의 값이라는 사실을 기억하라. 따라서 그 값을 제어하고 싶다면, 임시 변수로 할당하여 표현식 내 변수의 이름으로 블록을 끝마쳐라. 예를 들면 다음과 같다:
MyNewBlock := [:colour | |returnVal| (colour > 3)
ifTrue: [returnVal := #red]
ifFalse: [returnVal := #yellow].
returnVal].
블록에 ^를 사용하면 블록으로부터 리턴할 뿐만 아니라 전체 메서드로부터 리턴할 것이다. 당신이 원한다면 이 방법도 괜찮다. 하지만 블록에서만 꺼낼 의도였다면 원하는 효과를 얻지 못할 것이다. 이에 그치지 않고 블록을 실행 중인 메서드가 아니라 블록을 생성한 메서드로부터 리턴이 발생한다. 그 메서드가 이미 리턴되었다면 'Context cannot return' 예외가 발생한다. 다시 말해, MyBlock은 그것을 정의한 메서드에서 실행될 경우엔 적합하지만 (^의 경우 해당 메서드로부터 리턴을 야기할 것이다) 그 메서드로부터 호출되지 않은 다른 메서드에서 실행 시 부적합하게 된다 (예외를 받을 것이다).
블록 정의의 재실행 실패
이것은 최종적으로 코드를 실행할 때보다는 개발 시 더 문제가 되지만, 그럼에도 불구하고 매우 화나게 만든다. 이 함정은 특히 PluggableAdaptor와 같은 클래스를 사용할 때 빠지기 쉽다. 버그는 블록의 사용에 적용되고, 그보단 덜하지만 한 곳에서 생성된 다른 객체들이 다른 곳에서 사용될 때 적용된다 (메뉴와 같이).
당신의 코드가 블록을 만들어 변수에 보관하다가 다른 곳에서 실행하는 경우, 블록을 만드는 코드를 변경하면 (블록 정의를 변경하면) 블록을 변화시키기 위한 코드를 재실행해야 함을 기억해야 한다.
메서드를 수정하되 변경내용을 위한 새 인스턴스를 눈에 보이게 생성할 필요가 없다는 사실에 매우 익숙해질 수 있다. 이는 블록에선 사실이 아니다. 블록의 정의부를 수정한다고 해서 시스템이 그 정의부로 생성된 모든 블록을 찾아 새로운 정의부로 변경한다는 의미는 아니다! 당신은 블록을 정의하는 메서드를 수정하고, 기존에 있는 인스턴스 내에서 블록을 실행하며, 왜 변경내용이 효과가 없는지 궁금해 하는 과정을 반복하고 있음을 발견할 것이다. 그 이유는 기존 블록이 변경되지 않았기 때문이다!
부울 값으로 올바르지 않은 제어 메시지 전송하기
실수로 Boolean 값에 전송하여 BlockClosure가 제공하는 제어 구조를 몇 가지 사용해보기란 매우 쉽다. 이는 제어 구조가 클래스 계층구조 일부가 아니라 언어의 일부가 되길 예상하는 것과 관련된다. 아래 예제에서는 첫 번째 표현식이 오류를 발생할 것이다. 두 번째 표현식은 올바른 구현부다.
(MyCount > 10) whileFalse: [MyCount := MyCount +1].
[MyCount > 10] whileFalse: [MyCount := MyCount +1].
요약
스몰토크 코드의 디버깅은 다른 언어에서의 코드 디버깅과 그렇게 다르지 않음을 확인했을 것이다. 당신의 스몰토크 코드에서 실제로 무슨 일이 일어나는지에 대해서는 완전한 시야를 확보했지만, 당신의 코드, 시스템 코드, 심지어 디버거조차 모두 동일한 세계에서 살고 있음을 기억할 필요가 있다. 당신이 디버깅을 할 때보다 스몰토크 시스템과 당신 사이를 분명하게 구분하는 곳은 없다.
당신에게 제공된 기능들은 기본적일지는 모르지만 매우 강력하게 결합이 가능하다. 약간의 인내심과 창의적인 사고를 겸비한다면 자신만의 디버깅 코드를 생성하여 코드가 어떻게 행동하는지에 대해 알고 싶은 모든 것을 가상적으로 알려줄 것이다. 그럼에도 불구하고 다른 언어에서 디버깅하는 방법을 다룬 오래된 지침들 역시 스몰토크에 적용 가능하다. 이러한 지침에는 오류 메시지 읽기, 속도 늦추기, 추론하지 않기, 자신만의 디버깅 코드 작성하기, 한 번에 시스템의 작은 조각만 개발하기와 디버깅하기가 포함된다.
지금까지는 많은 숙련된 스몰토커들이 사용하는 기법의 범위를 살펴보았다. 스몰토크 코드의 디버깅에 더 숙련될수록 자신만의 기법을 개발하고 공통된 버그의 징후를 초기에 인식하기 시작할 것이다. 그 중 일부는 이번 장에서 살펴보았는데, 이 내용을 자신만의 경험과 결합한다면 갈수록 버그가 적은 스몰토크 코드를 작성하도록 도와줄 수 있을 것이다.