SqueakByExample:6.5
디버거(Debugger)
스퀵의 디버거는 틀림없이 스퀵 도구 모음중에서 가장 강력한 도구입니다. 디버가는 디버깅에만 사용되는 것이 아니라, 새로운 코드를 작성하는 작업에도 사용됩니다. 디버거는 버그를 입력함으로서 시작할 수 있습니다.
시스템 브라우저를 사용하여, 클래스 String에 다음 메서드를 추가하십시오:
메서드 6.1 버그가 있는 메서드
suffix
"assumes that I'm a file name, and answers my suffix, the part after the last dot"
| dot dotPosition |
dot := FileDirectory dot.
dotPosition := (self size to: 1 by: -1) detect: [ :i | (self at: i) = dot ].
↑ self copyFrom: dotPosition to: self size
물론 작업한 메서드는 당연히 작동할 거라 예상하고, SUnit test 를 작성하는 대신에, Workspace 에서 'readme.txt' suffix 를 입력한 다음 print it (p) 을 진행합니다. 이야! 대단하네요? 기대했던 결과인 'txt'를 얻는 대신에, 그림 6.24에서 보이는 것처럼, PreDebugWindow 를 볼 수 있습니다.
PreDebugwindow에는 어떤 오류가 발생했는지에 대해 알려주는 타이틀바(title-bar)가 있으며, 오류를 발생시켰던 메시지의 stack trace 상태를 출력합니다. trace의 출력은 아래쪽부터 시작해서 위쪽으로보면 됩니다. UndefinedObject>>DoIt 은 실행되는 컴파일된 코드를 나타내며, 사용자가 Workspace 에서 'readme.txt' suffix 를 이용해서 스퀵에게 print it을 요청한 시점에서 실행됩니다. UndefinedObject>>DoIt 에서 확인할 수 있는 'readme.txt' suffix 라는 코드는 당연히, ByteString 객체('readme.txt')에게 메시지 suffix 를 전송한다는 의미입니다. 이런 과정들을 거쳐 String 클래스에서 ByteString 으로 상속된 suffix메서드가 실행이 됩니다; ByteString(String)>>suffix에 관련된 모든 정보는 stack trace 의 다음줄부터 부호화(encoding) 됩니다. stack이 진행되면서, suffix 매서드는 그 안에서 자신에게 detect:... 메시지 를 보내고 결국 detect:ifNone 에 의해 errorNotFound 가 전송됨을 확인할 수 있습니다.
점(dot)을 발견하지 못한게 문제가 됐습니다. 자세한 이유를 알아보기위해서 디버거가 필요하기때문에 Debug 버튼을 클릭하도록 합니다.
당신이 stack trace 의 정보들중 어떤 라인을 클릭해도, 디버거가 열립니다. 디버거는 문제가 생긴 해당 메서드에 이미 포커스된 상태로 열리게 됩니다.
그림 6.25에 디버거가 보입니다; 이 디버거는 처음에 매우 어렵게 보이지만, 사용하기는 꽤 쉽습니다. 타이틀 바와 상단 패널은 PreDebugWindow 에서 보았던 것들과 매우 흡사합니다. 오류가 일어난 실행부분이 당신의 이미지에 그상태 그대로 존재한다는것은 중요한 사실입니다. 대기상태기는 하지만요. stack trace (추적) 의 각 라인은 실행 stack 의 프레임을 나타내는데 그 프레임이란것은 실행을 연속적으로 수행하기 위해 필요한 모든 정보를 포함하고 있습니다. 실행 stack 의 프레임은 인스턴스 변수들, 실행 메서드들의 모든 임시 변수들 및 연산에 관련된 모든 객체를 포함합니다.
그림 6.25 를 보면, 상단 패널에서 detect:ifNone: 메서드를 선택했습니다. 선택한 메서드의 내용은 중앙의 패널에 표시됩니다; 파란색으로 강조된 value 메세지는 현재 보이는 메서드에서 value 메세지를 보냈으며 답변을 기다리고 있음을 나타냅니다.
디버거의 아래에 있는 4개의 패널은 실제로 두 개의 mini inspector(Workspace 패널은 빠진상태)입니다. 왼쪽의 Inspector 는 현재 객체에 관련된 내용을 보여주며, 이 경우 현재 객체는 가운데 패널의 내용중 self 가 가리키는 객체를 의미합니다. 상단의 목록에서 다른 stack 프레임을 선택하면, self의 아이덴티티는 변경될 수 있으며, self-inspector의 내용도 바뀔 수 있습니다. 좌하단 패널에서 self를 클릭하면 이미 예상했던대로 self의 값은 인터벌 (10 to:1 by -1) 임을 알 수 있습니다. 디버거의 mini inspectors 에서는 Workspace 패널이 필요없는데, 왜냐하면 모든 변수들의 범위는 위쪽의 메서드 패널의 내용에 한정되기 때문입니다; 가운데의 메서드 패널에서 입력을 하거나 또는 표현식들을 선택하고 그것들을 실행해보는 작업을 마음대로 할 수 있기 때문이죠. 언제나, 메뉴 또는 CMD-i를 사용하면 변경사항을 cancel (l)(취소)할 수 있습니다.
우측 하단의 Inspector 는 현재 시점의 임시 변수를 보여줍니다. 그림 6.25에서, exceptionBlock 의 매개변수로 value가 보내졌습니다.
이 파라미터의 현재 값을 보기 위해, 우측하단의 context Inspector 에서 exceptionBlock 을 클릭 합니다. 클릭하면 exceptionBlock 의 값이 [self errorNotFound: ...] 임을 알려줄겁니다. 변수의 값으로 해당되는 오류메세지가 보이는건 당연한 일입니다.
그건 그렇고, 디버그창 아래의 mini inspector 에 보이는 변수들중 한가지에 대해 full inspector 또는 오브젝트 탐색기를 열기 원하는 경우에는, 변수의 이름을 더블클릭(inspect)을 하거나 또는 변수의 이름을 선택하고 노랑 버튼 메뉴에서 inspect (i) 또는 explore (I) 를 선택하면 됩니다. 이렇게 디버깅중 변수를 inpect 또는 explore 하는 작업은 다른 코드를 실행하는 동안 어떻게 변수가 변하는지 보고싶을때 유용합니다.
위쪽의 stack trace창에서 suffix 부분을 클릭한후 메서드가 표시되는 패널을 보면, 문자열 'readme.txt' 에 대해 메서드의 끝에서 두 번째 라인이 점(dot)을 찾아낼거라 생각했습니다만, 메서드가 동작되는 일련의 작업은 마지막 라인까지 진행되지 못했음을 알 수 있습니다. 스퀵은 디버깅중 역실행을 허용하지 않고, 메서드를 처음부터 재시작하게 하며, 사실 이런방법은 객체를 변형시키는게 아니라 새로운 객체를 만드는 코드에서 매우 잘 동작합니다.
Restart를 클릭하면, 가운데 패널에서 현재 stack trace를 통해 선택한 메서드의 실행이 다시 진행되며 과정이 첫 번째 표현(statement)으로 돌아가는걸 확인할 수 있습니다. 파란색으로 강조된 부분을 통해 다음 메시지가 do:에 전송될것이라는걸 보게됩니다 (그림 6.26을 보십시오.)
Into 와 Over를 통해서 두가지 서로다른 실행방법을 진행할 수 있습니다. 만약 Over 버튼을 클릭하면, 스퀵은 오류가 없을 경우, 첫 번째 단계에서 현재 선택된 메시지 전송(이번 경우엔 do:)을 실행합니다. 이후 Over 를 다시 누르면 현재 메서드에 내에서 다음차례의 메시지 전송을 진행하는 위치로 진행되며, 그 대상은 value: 가 됩니다. value 는 디버깅을 시작한 지점이기때문에 더이상 도움이 되지 않을겁니다. 이제 해야 할 일은 do: 메서드가 사용자가 찾으라고 지정한 문자를 찾지 못하는 이유를 알아내는 것입니다.
Over를 클릭하고 그림 6.26에 보이는 상황으로 돌아가기 위해 Restart를 클릭하십시오.
Into를 클릭하면, 스퀵은 강조된 메시지 전송부분 으로 이동합니다. 이번의 경우는 Collection>>do:. 가 되겠군요.
이렇게 메서드로 이동을 하는것도, 많은 도움이 되지는 않습니다: 사실 Collection>>do가 망가지지 않았다는 것에 대해 어느 정도 자신감이 있으니까요. 버그는 메서드에서 스퀵에게 요청한 작업 내에 있을 확률이 높습니다. Through는 다음같은 경우에 사용하기 유용한 버튼입니다: 이제 do: 메서드 자체의 자세한 내용 을 무시하고 인자블록들의 실행에 집중해봅시다.
그림 6.26의 환경에서 Through를 클릭합니다. 이후 우측 하단의 컨텍스트 창에 each가 생기면 늘 하던 방식대로 each를 선택하십시오. do: 메서드가 실행되는 10에서 부터 시작되며 진행되는 각각의 카운트를 볼 수 있습니다.
each가 7이 될 때, 우리는 ifTrue:block이 실행될 것을 기대할 수 있지만, 그것은 실행되지 않습니다. 무엇이 틀렸는지 보기 위해, 그림 6.27 처럼, each의 값이 7일때 value의 Into 실행으로 이동합니다.
Into를 클릭한 이후, 그림 6.28처럼 메서드 내에서 강조된 위치를 확인할 수 있습니다. 이 상황은 마치 suffix 메서드로 돌아간 것처럼 보이게되는데, 왜냐하면 현재, detect: 에 대한 인수로서 제공된 suffix인 블록을 실행하고 있기 때문입니다. 만약 우측하단의 context inspector 에서 i를 선택한다면, 선택한 i의 현재 값을 보실 수 있으며, 당신이 만약 지금까지 잘 따라오셨다면, 그 값은 반드시 7이 되어야 합니다. 그 다음, 좌측 하단의 self 변수가 있는 인스펙토에서 self의 해당 구성요소를 선택하면 됩니다. 그림 6.28에서, 문자열의 구성요소 7이 문자 46임을 볼 수 있으며, 이것은 실제로 dot문자 입니다. 만약 context inspector 에서 dot를 선택한다면, 변수 dot의 값이 '.' 임을 보게됩니다. 그리고 지금 왜 이것들이 같지 않은지를 확인했습니다: dot은 String인에 비해서 'readme.txt'의 일곱번째 문자는 Character 이기 때문입니다.
이제 버그를 확인했고, 수정해야 할 내용은 명확합니다: 'readme.txt' 에 대한 검색을 시작하기 전에 dot를 character 로 변환시켜야만 합니다.
디버거에서 {{{1}}} 라고 올바르게 할당하는 내용으로 코드를 변경한후, accept를 진행해서 변경사항을 적용시킵니다.
현재 detect: 내부의 블록 내부에서 코드를 실행하고 있기 때문에 여러 개의 stack 프레임은 변경사항을 적용하기위해 포기해야만 합니다. 스퀵은 수정작업이 사용자가 원하는 것인지를 물어보며(그림 6.29를 보십시오) 만약 yes를 클릭한다고 가정하면, 새로운 메서드를 저장(그리고 컴파일)할 것입니다.
Restart를 클릭한후 Proceed 버튼을 눌러 진행합니다: 디버거 창이 사라지며, 프로그램식 'readme.txt' suffix 의 처리가 완료된후, 원하던 결과물인 '.txt'를 'readme.txt' suffiix 를 실행한 원래의 Workspace 에 출력합니다.
이 답변은 정확한 것일까요? 안타깝게도 확신 있게 말할 수는 없습니다. Suffix(접미사)는 .txt 가 되어야 할까요? 또는 txt가 되어야 할까요? suffix에 있는 메서드 주석은 정확한것이라고 할 수는 없습니다. 이런문제를 어느정도 피하는 방법은 답변을 정의하는 SUnit test를 작성하는 것입니다.
메서드 6.2: suffix 메서드를 위한 간단한 테스트
testSuffixFound
self assert: 'readme.txt' suffix = 'txt'
SUnit을 사용하는 방법은 Workspace 에서 동일한 테스트를 실행하는 것보다 좀더 많은 노력이 필요합니다만, SUnit을 사용하면 실행이 가능한 문서로서 테스트를 저장하고, 다른 사람들이 그 테스트를 실행하기 쉽게 만들어줍니다. 게다가, 만약 StringTest 클래스에 메서드 6.2 를 추가하고, Test Runner에서 SUnit의 테스트세트를 실행한다면, Workspace 에서 입력할때보다 디버깅 오류를 더 빠르게 불러낼 수 있습니다. Sunit은 실패한 검증사항the failing assertion에 대해 디버거를 엽니다만, 해야할일은 아까처럼 복잡하지 않으며 단지 디버거의 stack을 보고 stack 프레임을 한단계 낮추고, 낮춘 프레임의 내용에서 테스트를 Restart 하고, suffix 메서드로 Into 하면, 그림 6.30에서 볼수있듯이, 오류를 수정할 수 있습니다. 이렇게 오류를 수정한 후, 그 다음으로 해야 할 작업은 단지 Sunit Test Runner에 있는 Run Failures 버튼을 클릭하고, 지금 진행중인 테스트를 확인(confirm)하는 것입니다.
여기에 위의 메서드보다 더 발전된 테스트 내용이 있습니다:
메서드 6.3 suffix 메서드를 위한 더 나은 테스트
testSuffixFound
self assert: 'readme.txt' suffix = 'txt'.
self assert: 'read.me.txt' suffix = 'txt'
왜 이 테스트가 더 좋은걸까요? 이 테스트는 대상 문자열에 하나 이상의 dot(점)이 있는 경우, 테스트메서드를 읽는[1] 사람에에게 메서드가 무엇을 해야 할지를 알려주기 때문입니다.
디버거에 오류와 주장 실패(assertion failures)를 잡아내는 작업 뿐만 아니라, 디버거에 들어가는 몇 가지 다른 방법들이 있습니다. 사용자가 무한 루프에 들어가는 코드를 실행하면, 그 코드를 인터럽트하고, 'CMD-.(dot)을 (dot은 마침표 또는 구두점이라고 부르죠)[2]입력하여 연산진행중에 디버거를 열 수 있습니다. 또한 디버거를 열기위해 의심되는 코드에 self halt 를 삽입하는 방법도 있습니다. 예를들어, suffix 메서드를 편집한 내용을 보도록 하죠.
메서드 6.4 suffix 메서드에 halt 삽입하기
suffix
"assumes that I'm a file name, and answers my suffix, the part after the last dot"
| dot dotPosition |
dot := FileDirectory dot asCharacter.
dotPosition := (self size to: 1 by: --1) detect: [ :i | (self at: i) = dot ].
self halt.
↑ self copyFrom: dotPosition to: self size
우리가 이 메서드를 실행할 때, self halt의 실행은 우리가 진행할 수 있는 장소에 pre-debugger를 불러오거나 또는 디버거로 들어가 변수들을 보고, 연산을 한 단계씩 진행하며(step), 코드를 수정합니다.
지금까지의 모든 내용은 디버거에는 해당됩니다만, suffix 메서드에 대해서 전부 설명이 된건 아닙니다. suffix 메서드를 실행했을때 문자열에서 아무런 dot(점)이 없는경우에 첫 버그가 발생된다는걸 알아야 하거든요. 이렇게 버그가 일어나는 원하는건 아니기때문에, 이번 예제에서는 dot(점)이 문자열에 없는경우, 어떤 일이 일어나야 할지를 지정하기 위한 두 번째 테스트 메서드를 추가하도록 하겠습니다.
메서드 6.5: 메서드를 위한 두 번째 테스트: 타켓은 suffix를 갖고 있지 않습니다.
testSuffixNotFound
self assert: 'readme' suffix = ''
메서드 6.5를 클래스 StingTest에 있는 테스트 세트에 추가하고, 그 테스트를 통해 오류를 발생하는 것을 지켜봅니다. SUnit에서 오류가 있는 테스트를 선택해서 디버거로 들어간후, 코드를 수정하여, 테스트를 통과되게끔 합니다. 오류를 수정하는 가장 쉽고 정확한 방법은, suffix 메서드의 detect: 대신, detect:ifNone:을 이용해서 detect:ifNone: 두번째 인수를 단순히 문자열의 크기를 반환하는걸로 처리하는 겁니다.[3]
SUnit에 대해서는 7장에서 좀 더 배우도록 하겠습니다.