원문 : http://www.threeriversinstitute.org/blog/?p=348
Kent Beck의 글들은 언제나 저에게 새로운 느낌과 아이디어를 전달해주지만, 그중에서도 꼭 한번 번역해보고 싶었던 글을 올립니다. 문장들은 참 이해하기 쉽고 단순한데, 역시 번역이 원래 그런것인지 한국말로 옮기기가 난해한 것들이 많네요. 제가 이해한대로 임의로 의역한 부분이 상당합니다. 미흡한점이 많으니 지적 부탁드립니다(와서 읽는 사람이 있다면...).
=======================================
나는 디버거를 설계 도구로 활용한다. 나의 프로그래밍 파트너가 이같은 사실에 가끔 놀라곤 하기에 이 기술(을 설명하는 글)을 써야겠다고 생각했다. 설계를 위해 디버거를 사용하는 것이 효과적인 이유는 애당초 내가 생각했던 것 이상이었다. 디버거는 변화의 여지가 있는 부분들에 대한 범위를 줄여줌으로서 설계를 더욱 효율적으로 만들어준다.
문제(The Problem) 나와 내 파트너는 책임 할당(responsibility allocation)에 대한 문제로 고민하고 있었다. 객체 A는 B를 가리키고, B는 다시 C를 가리키고 있다. 우리는 B의 일부 로직과 데이터가 실상 B에 속한것이 아님을 확인하고 지난주에 그것들을 C로 옮겼다. 그런데 이는 문제를 해결하기보다 오히려 더 많은 문제를 만들어냈다. 우리는 진정으로 A의 책임(할당 - A가 무엇을 해야 하는가 -)이 필요해졌다.
우리는 수많은 테스트가 실패하는 예상치 못한 상황에서 책임을 이동시키기 시작했다. 내 파트너는 곧바로 A, B, C 세 객체의 코드를 살펴보고 에러를 찾아내기를 원했다. 그 대신 나는 코드에 중단점(breakpoint)을 걸어 예외(exception)이 발생하는 시점의 스택을 살펴볼 것을 제안했고, 그리하여 우리는 문제를 빠르게 발견, 수정할 수 있었다.
흠...(Hmmm...) 나는 디버거에서 에러를 찾고 그것들을 살펴보는 등의 일을 과거에도 수차례 했었으나 이러한 방법이 어째서 그토록 잘 먹히는지(why it worked so well) 전혀 이해하지 못했다. 다음은 이에 대해 내가 생각한 것들이다. 디버거로부터 제공되는 기본적인 정보는 프로그램의 현재 상태와 프로그램 카운터(Program Counter)이다. 프로그램 카운터는 단지 현재 루틴에서의 위치 뿐만 아니라 호출된 모든 루틴(스택 프레임, 혹은 콜스택)의 위치를 말한다
(*).
위 그림에는 그것이 내포하는 수많은 정보가 담겨있다. 각각의 프레임은 이미 실행된, 프로그램 카운터 이전의 히스토리를 보여준다.
만약 우리가 이미 실행된 문장(statement)들에 대해 완전히 추적(trace)한다면, 우리는 시스템 내의 문장들을 이미 실행된 것과 아직 실행되지 않은 것으로 분할 할 수 있을것이다.
효율(Efficiency)
테스트가 실패할 때마다 우리는 이미 실행된 문장들중의 어딘가를 수정하려고 한다. 이러한 분할은 설계 도구로서의 디버거가 가지는 효율 첫 단계의 시작이다. 전체 시스템, 혹은 시스템의 특수한 부분집합을 보는 것 보다, 디버거는 정확히 어디를 고쳐야 하는가에 대한 중요한 단서를 제공한다. 위 그림의 빨간 영역중의 일부가 바뀌어아먄 할 것이다.
이러한 분할은 아마도 갑작스런 예외들 속에서(with unexpected exceptions) 테스트를 수정하는 것이 어째서 실패하는 단정문(assertion) 속에서 테스트를 수정하는 것보다 쉽게 느껴지는지를 설명할 것이다. 일반적으로 단정문(assertion)이 실패 했을 때, 코드의 많은 부분이 살펴보아야 할 문제의 한 부분으로 남겨진 채로 모든 테스트가 진행될 것이다. 만약 테스트가 진행되는 중간 예외가 발생한다면 이미 실행된 문장들의 집합은 작을것이고, 때론 아주 작은 부분이 될 수도 있다.
(아마 이러한 분할은 빠른 디버깅에서의 unit test의 가치를 설명하는데 도움을 줄 것 같다. 만약 테스트가 전체 시스템의 일부분만을 실행한다면, 결함을 그 일부분으로부터 격리시키는 것이 전체 시스템으로부터 격리시키는 것보다 훨씬 쉽기 때문이다.)
설계 도구로서 디버거를 사용하는 것이 가지는 효율의 두 번째는 대부분의 시간 소요가 변화가 필요한 스택의 어떤 루틴에서 발생한다는 관찰로부터 온 것이다. (안타깝게도) 나는 이에대한 어떤 데이타(통계자료)를 가진것이 없고, 더하여 나의 모호한 관찰("오, 또 스택에서 어떤 루틴을 수정하고 있어")로부터 온 것이다. 만약 어떤 루틴이 바뀌어아 할 가능성이 그 루틴이 스택 위에 있느냐 아니냐에 강하게 영향을 받는다면
(*), 문제의 코드를 찾아내는
(*) 합리적인 전략은 단지 스택따라 올라가는 것이다. 나는 일반적으로 이것을 디버깅 초반에, 일반적으로 내 파트너보다 훨씬 먼저 진행한다.
디버거에서 나타나는 에러들에 대해 대응하는 몇가지 기교들을 알고있다면 효율을 더 증대시킬 수 있다. 대부분의 문제는 로직을 스택의 위 아래로 이동시킴으로 해결된다. 따라서 디버거 안에서 설계를 선택하는 것은 고쳐져야 하는 부분을 줄이고, 변화의 가능성, 또한 스택에서 변화가 필요한 위치와 그 변화 자체의 결합도를 줄임으로서 이득을 준다.
결론(Conclusion) 내가 여기서 설명한 과정들은 언뜻 그저 "디버깅"으로 보일 수 있다. 그러나 위 내용들의 목표가 프로그램 내 요소들간의 관계 개선을 증진시키는데 있기 때문에 이것은 설계의 한 과정이다. 어떤 수정이 객체간의 경계를 연결시킨다면, 비록 그것들이 같은 스택 위에 존재하는 객체들이라 해도 당신은 설계에 관한 어떤 결정을 해야한다. 매우 드문 경우이지만 만약 문제가 스택 위에 있지 않다면
(*) 요구되는 설계의 변화량은 상당할 것이다. 특히, 어떤 만족되지 않은 임의의 의존관계를 가지고 있다면("아, 이 코드는 이거보다 전에 호출했어야 했는데...") 당신은 호출되지 않은 객체(previously-uninvoked objects)를 포함하여 코드의 제어 흐름(flow of control)을 근본적으로 수정해야 할 것이다. 당장의 목표는 코드의 결점을 제거하는 것일 지라도, 이차적인 목표는 설계를 강화시키는 것이기 때문이다.
... ing...
* 역주: 실제 프로그램 카운터는 그저 현재 위치(정확히는 다음 실행할 instruction의 위치)만을 저장한다. Kent는 디버거가 제공하는 고수준의 정보에 대해 프로그램 카운터로 통칭한 것 같다.
* 모든 프로그램 코드는 결국 런타임에 어떤 스택 프레임 위에 존재하게 될 것이다. 이에 대한 Kent의 의도를 이해하지 못하겠다.
* 원문은 finding offending code이다. Kent도 코드에 문제가 있으면 화나고 짜증나긴 한가보다.
* 문제가 하나의 스택 프레임이 아니라 여러 스택 프레임 사이에 걸쳐있는 겅우를 말하는 것으로 보인다.