출처

마이클 C. 패더스. 레거시 코드 활용 전략. 이우영, 고재한(역). 서울:에이콘출판, 2008.


9장 뚝딱! 테스트 하니스에 클래스 제대로 넣기

우리가 직면하는 공통적인 네 가지 문제이다.

  1. 클래스의 객체는 쉽게 만들어지지 않는다.
  2. 테스트 하니스는 그 안의 클래스와 함께 쉽게 만들어지지 않는다.
  3. 우리가 사용하고자 하는 생성자는 부작용을 일으킨다.
  4. 구조에서 중요한 일이 발생하며, 우리는 이를 알 필요가 있다.

위에서 언급한 문제를 푸는 방법에는 여러 가지가 있다. 의존관계를 깨는 기법, 절충하는 기법, 그리고 특정 상황에 맞도록 응용하는 기법 등에 친숙해지는 길을 제공할 것이다.

테스트 하니스

소프트웨어 컴포넌트의 테스트를 가능하게 하거나 프로그램의 입력을 받아들이거나 빠진 컴포넌트의 기능의 대신하거나 실행 결과와 예상 결과를 비교하기 위하여 동원된 소프트웨어 도구

성가신 매개변수의 경우

숨은 의존관계인 경우

생성 블랍인 경우

성가신 전역 의존관계인 경우

끔찍한 인클루드 의존관계인 경우

헤더 의존관계 (Header Dependency)

양파 매개변수인 경우

별칭 붙은 매개변수인 경우


10장 테스트 하니스에서 실행할 수 없는 메소드

변화시키는데 필요한 메소드의 테스트 루틴을 작성하면서 부딪히는 문제는 다음과 같다.

  • 메소드가 테스트 루틴에 접근할 수 없는 경우 : 프라이빗(private)인 메소드이거나 접근 가능성에 문제가 있는 경우이다.
  • 메소드를 호출하기 힘든 경우 : 메소드를 호출하는 데 필요한 매개변수를 생성하기 어려운 경우이다.
  • 메소드가 좋지 않은 부작용을 유발하는 경우 : (예를 들면 데이터베이스의 수정, 크루즈 미사일의 장착 등과 같은 것들) 그래서 테스트 하니스 안에서의 실행이 불가능한 경우이다.
  • 메소드가 사용하고 있는 몇몇 객체들을 통해서만 테스트할 수 있는 경우

숨은 메소드인 경우

만약 프라이빗 메소드에 대한 테스트가 필요하다면 그것을 퍼블릭으로 선언한다.

  1. 메소드가 단지 하나의 유틸리티인 경우, 클라이언트는 해당 메소드에 관심이 없다.
  2. 클라이언트들이 그 메소드를 사용한다면, 해당 클래스의 다른 메소드들로부터 결과값이 영향을 받는다.

첫 번째 이유는 심각하지 않다. 비록 다른 클래스 내에 해당 메소드를 작성하는 것이 더 나은지 판단해야 하는 번거로움은 있을지언정, 클래스의 인터페이스 내에서 부가적인 퍼블릭 메소드는 허용할 수 있다.
두 번째 이유는 심각한 경우지만 해결책이 있다. 프라이빗 메소드를 새로운 클래스로 이동하는 것이다. 이들 프라이빗 메소드는 새 클래스에서 퍼블릭이 될 수 있고, 기존의 클래스에서 그 메소드를 위한 내부 인스턴스를 생성할 수도 있다. 이렇게 할 경우 그 메소드들은 테스트가 가능해지고, 더 나은 설계가 이뤄지게 된다.
중요한 것은 좋은 설계란 테스트가 가능해야 하고, 테스트가 불가능한 설계는 나쁜 것이라는 점이다.

도움이 되는 언어 특징인 경우

탐지 불가능한 부작용인 경우

명령어/쿼리 분리

명령어/쿼리 분리는 버트란트 메이어(Bertrant Meyer)에 의해 처음 기술된 설계 원칙으로, 간단히 말하면 다음과 같다. 하나의 메소드는 하나의 명령어이거나 쿼리여야 한다는 것이고, 두 가지 기능을 모두 가져서는 안 된다는 원리이다. 명령어는 객체의 상태를 수정할 수 있는 메소드이지만 값은 반환하지 않는다. 쿼리는 값을 반환하지만 객체를 변화시키지는 않는다.
이러한 원칙은 왜 중요할까? 여러가지 이유가 있겠지만 가장 중요한 것은 통신 때문이다. 임의의 메소드가 쿼리라면 부작용을 일으키지 않으면서 여러 번 그 메소드를 사용할 수 있을지 없을지를 확인하기 위해 그 메소드의 바디를 살펴볼 수밖에 없다.

리팩토링 도구를 사용함에 있어서 테스트하기 위한 조잡한 이름과 구조를 가진 메소드들을 추출하는 것은 용납된다는 점을 기억하자. 무엇보다 안정성이 최우선이다. 테스트하고 난 후에는 코드를 좀 더 간결하게 정리할 수 있다.


11장 코드 변경 과정에서 꼭 테스트해야 할 메소드

우리는 변경시킬 필요가 있고 이미 존재하는 동작을 규정하기 위한 특성화 테스트(Characterization Tests)를 작성해야 한다. 그럼 어디에 작성해야 할까? 간단하게 말하자면 바꾸고자 하는 각 메소드마다 테스트하는 것이다.
종종 최적의 테스트 장소를 찾기 위해서는 코드가 끼치는 영향을 복합적으로 추론해 봐야 한다.

효과에 대한 추론

이 다이어그램들을 효과 스케치(Effect sketches)라고 부른다. 여기서의 핵심은 영향받을 수 있는 모든 변수와 반환값이 바뀔 수 있는 모든 메소드들이 각자 자기의 버블을 갖게 하는 것이다. 때때로 변수들은 동일 객체 상에 있기도 하고, 또 어떤 경우에는 다른 객체 상에 존재하기도 한다. … 변경하고자 하는 것을 위해 버블을 만들고, 실행할 때 그 값들에 의해 바뀔 수 있는 것들을 포함하는 버블들을 화살표로 가리키도록 선을 긋는다.

제대로 작성되지 못한 프로그램에서는 어떤 결과가 왜 나왔으며 그것이 무엇을 의미하는지 알기 어렵다. 그런 시점에 있고 디버깅에 문제까지 있는 경우에는 해당 문제로부터 소스 코드까지 역으로 추론해 가야만 한다.

전방 추론

효과 스케치를 수행할 때 당신이 검토하는 클래스의 모든 클라이언트를 파악했는지 확실히 하자. 당신의 클래스가 상위클래스나 하위클래스를 가지고 있다면 당신이 고려하지 않은 다른 클라이언트가 있을 것이다.

전방 추론은 레거시 코드에서의 변경이 유발하는 영향을 평가할 때 필요한 추론의 대표적인 종류라 할 수 있다. 테스트할 곳을 찾기 위해서 변경이 어디에서 일어나는지를 알아내야 한다. 즉, 변경 효과가 어떠한지를 알아야 한다. 효과를 탐지할 수 있는 곳을 알게 되었을 때는 테스트 루틴을 작성하는 과정에서 그 여러 곳들 가운데 하나를 선택할 수 있다.

효과 전파

매개변수로 객체를 받는 객체를 하나 가진다면 그 객체는 그것의 상태를 변경하고, 이 변경은 응용프로그램의 나머지 부분에 영향을 미치게 된다.
코드의 어느 부분이 다른 코드에 영향을 주는 은밀한 방법은 전역 또는 정적데이터에 의해 이뤄진다.

코드 내에서의 효과 전파 방법 3가지

  1. 호출자가 사용하는 값을 반환함으로써
  2. 매개변수로 전달되고 나중에 사용될 객체들을 수정함으로써
  3. 나중에 사용될 정적 또는 전역 데이터를 수정함으로써
    어떤 언어는 추가적인 기법을 제공한다. 예를 들면, 관점지향 언어(Aspect-oriented language)인 경우에는 프로그래머가 관점(Aspect)이라 부르는 생성물(Constrcuts)을 작성할 수 있는데, 이것들은 시스템의 다른 곳에 있는 코드 동작에 영향을 미친다.

효과를 찾을 떄 사용하는 휴리스틱

  1. 변경할 메소드를 식별해 낸다.
  2. 해당 메소드가 반환값을 가진다면 해당 메소드의 호출자를 살펴본다.
  3. 해당 메소드가 어떤 값을 변경시키는지 알아본다. 변경시킨다면 그 값들을 사용하는 메소드들과 그 값들을 사용하는 메소드를 쓰는 메소드들을 살펴본다.
  4. 또한 이 인스턴스 변수와 메소드를 사용할지도 모르는 상위클래스, 그리고 하위클래스를 살펴보는 것을 잊지 않는다.
  5. 해당 메소드에 대한 매개변수를 살펴본다. 해당 메소드가 반환하는 매개변수나 객체가 당신이 변경시키기를 원하는 코드에서 사용되는지 알아본다.
  6. 당신이 식별해 낸 메소드들 안에 있는 전역 변수들과 정적 데이터 중 수정된 것이 있는지 찾아본다.

효과 추론을 위한 도구

프로그래밍 언어에 대한 지식 - 매 언어마다 효과 전파를 막는 방화벽(Firewall)이 존재한다. 그 방화벽이 무엇인지를 안다면 또한 그것을 굳이 지나쳐 갈 필요가 없다는 사실도 알게 된다.

효과 분석을 통한 학습

최상의 코드에는 ‘번뜩이는 생각(Gotchas)’이 그다지 많이 들어 있지 않다. 코드 베이스에 구체화되어 있는 몇몇 ‘규칙’들은 가능한 효과를 검토하는 과정에서 편집증(Paranoid)을 가지지 않도록 해준다.
나쁜 코드인 경우에는 사람들이 규칙이 무엇인지 알 수 없게 되거나 규칙들이 예외(Exceptions)로 인해 엉망이 된다.

효과 스케치 단순화

중복 부분 가운데 매우 작은 일부분을 제거할 때는 종종 겨로가적으로 작은 종점(Endpoint) 집합을 갖는 효과 스케치를 가지게 된다. 이것은 때에 따라서 더 쉬운 테스트 결정으로 해석되기도 한다.

결론

테스트 루틴을 어디에 작성할지 알 필요가 있을 떄는 변경시킬 것들에 의해 어떤 것들이 영향을 받을지 아는 것이 중요하다. 아울러 효과에 대해 추론해야 한다. 우리는 이 같은 종류의 추론을 적은 스케치를 가지고 약식으로 하든지 아니면 더 엄정하게 진행할 수 있다. 어쨋든 시행하면 도움은 된다. 스케치를 적게 하는 대략적인 방법으로 가능하지만 그것을 실행할 필요가 있다. 특히 엉킨 코드라면 이 방법은 제자리 테스트 과정에서 우리가 의지할 수 있는 유일한 기술들 가운데 하나이다.


12장 클래스 의존관계, 반드시 없애야 할까?

단 한 번에 여러 개의 변화를 위한 테스트 루틴을 작성하기 위해 종종 ‘한 단계 후퇴(One level back)’라는 기법을 이용한다. 즉, 여러 개의 프라이빗 메소드들을 여러 번 변경시키기 위해 하나의 퍼블릭 메소드를 만들어 테스트 루틴을 작성하기도 하고, 여러 개의 객체를 가진 협력 객체를 위한 인터페이스에 테스트 루틴을 작성할 수도 있다. 이런 기법을 이용하면 변화에 대한 테스트를 할 수 있는 반면에, 해당 영역에서 리팩토링을 위한 부분들을 우리 스스로 담당해야 하는 부담이 생긴다.

개요

  • 차단지점의 개념
  • 그러한 지점들을 어떻게 찾는지
  • 코드 내에서 찾을 수 있는 최상의 차단 지점인 조임 지점에 대해
  • 이것들을 어떻게 찾아내는지
  • 변경시킬 부분에 대한 테스트 루틴을 작성할 때 이런 방법이 얼마나 도움되는지

차단 지점 기법

차단 지점이란 프로그램 내에서 특정 변화가 영향을 끼치는 곳을 검사하기 위한 한 지점을 의미한다.
가장 먼저 변경이 필요한 부분을 확인하고 그 변경 지점으로부터 외부로 영향을 미치는 단계들을 추적하면 된다. 이 때 발견한 모든 영향들이 바로 하나의 차단 지점이 된다. 하지만 그것이 최상의 차단 지점은 아닐 것이다. 전체 과정을 통해서 최상의 차단 지점을 찾아야 한다.

상위 단계 차단 지점

대부분의 경우, 변화를 위한 최상의 차단 지점은 우리가 변화시키려는 클래스 내의 퍼블릭 메소드 중의 하나이다.

어떤 클래스도 테스트 루틴을 가지지 않았다면 각 클래스들에 대해 개별적으로 테스트 루틴을 작성하고 우리가 필요로 하는 변화들을 만들 수 있다. 이러한 접근방법도 가능하지만 상위 단계 차단 지점을 찾으려고 노력하는 데에서부터 더 효과적인 작업이 이뤄질 수 있다.
두 가지 장점이 있다. 하나는 의존관계를 줄일 수 있다는 점이고, 다른 하나는 바이스(vice) 내에 큰 덩어리들을 포함할 수 있다는 점이다.

모든 효과들을 검출할 가능성이 있는 장소를 지정하는 기법을 조임 지점(Pinch Point)라고 부른다. 조임 지점은 광범위한 변화들을 다루는 테스트 루틴을 작성할 수 있는 효과적인 스케치들 내의 아주 작은 일부분이다. 설계할 때 하나의 조임 지점을 찾을 수 있다면 나머지 일들은 매우 쉬워진다.

조임 지점

조임 지점이란 효과 스케치 내의 좁은 지역을 의미하며 한 쌍의 메소드들에 대응하는 테스트들이 있는 장소이다. 이 곳을 통해 다수의 메소드 들 내의 변화들을 검출할 수 있다.

조임 지점을 찾는 방법

  • 변화 지점을 다시 찾아간다.
  • 한번에 하나 혹은 두 개의 변경을 위한 조임 지점들을 찾는 것을 고려한다.
  • 최대한 개별적인 변화를 위한 테스트 루틴을 작성한다.

조임 지점을 이용한 설계 판단

조임 지점에서의 테스트 루틴 작성은 프로그램에 있어서 조금은 공격적인 일들을 시작하기 위한 이상적인 방법이다. 일단 여러 개의 클래스들을 다듬는 노력을 기울이고, 이어서 테스트 하니스 상에 그들을 함께 생성시킬 수 있는 지점으로 모을 수 있다. 특성화 테스트 루틴들을 작성한 후에는 아무런 해를 받지 않은 채 변경시킬 수 있게된다.

조임 지점 함정

단위 테스트를 작성할 때 여러 문제에 봉착할 수 있다. 이를 해결하기 위해서, 단위테스트를 천천히 미니 통합테스트로 확장해 나가는 방법이 있다.
때때로 응용프로그램을 조각들로 분리하고 테스트를 위해 다시 조립하는 것은 힘들다. 그러한 테스트 상황이라면, 우리가 다루고 있는 각 클래스들의 좀 더 작은 단위테스트를 작성할 수 있다. 궁극적으로는 조임 지점에서의 테스트가 사라져야 한다.


13장 변경에 필요한 테스트는 뭐가 있을까?

버그를 찾는 테스트는 수동 테스트(manual tests)가 되며, 전략적인 측면에서 보면 잘못 인도된 노력(Misdirected effort)이다.

특성화 테스트

거의 모든 레거시 시스템에서는 시스템이 무엇을 해야 할지(What is supposed to do)보다는 어떤 일을 하는지(What the system does)가 더 중요하다. 버그 찾기도 중요하지만, 현재 우리의 목표는 제자리 테스트를 수행해 변경사항들이 좀 더 결정적이게 만드는 데 있다.

특성화 테스트는 코드 일부에 대한 실제 동작을 기록한다. 특성화 테스트를 작성할 때 예상치 못한 것들을 발견했다면 특성화 테스트는 그것들을 명확히 하는 데 도움을 줄 것이다. 그것은 버그일 수도 있다. 이것은 우리의 테스트 슈트(test suite)에 테스트를 포함하지 않았음을 의미하는 것은 아니다. 대신에 그것을 의심스러운 것으로 표시하고 고쳤을 때 무슨 효과가 생길지는 알아봐야 한다.

특성화 테스트를 작성하는 알고리즘

  1. 테스트 하니스(Harness) 내의 코드 일부분을 사용한다.
  2. 당신이 알고 있는 실패할 어쎄션(Assertion)을 하나 작성한다.
  3. 실패한다면 그로 인해 동작이 무엇인지를 알게 해준다.
  4. 테스트를 변경해 코드가 생산해 내는 동작을 예상할 수 있게 한다.
  5. 위 과정을 반복한다.

메소드 사용 규칙

레거시 시스템에 있는 하나의 메소드를 사용하기 전에 그 메소드를 위한 테스트 루틴이 있는지를 검토하라. 테스트 루틴이 없다면 테스트 루틴을 작성하라. 이런 일을 계속한다면 테스트 루틴을 통신수단으로 사용할 수 있게 된다. 이러한 테스트 루틴을 보면 해당 메소드로부터 가능한 일과 가능하지 않은 일이 무엇인지를 감지할 수 있다. 자체적으로 테스트 할 수 있도록 클래스를 만드는 행위는 코드의 질을 향상시키게 될 것이다. 사람들은 어떤 것이 동작하고 또한 어떻게 동작하는지를 알게 될 것이다. 따라서 그들은 코드를 변경시키거나 버그를 바로 잡을 수 있으며 나아가 한발짞 전진해 나갈 수 있다.

클래스 특성화

테스트 루틴에 도움을 주는 휴리스틱(Huristic)

  1. 로직이 엉켜 있는 부분을 찾는다.
  2. 클래스나 메소드의 책임을 하나둘씩 알아가면, 그때 그때 작업을 잠시 멈추고 실수할 수 있는 것들에 대한 목록을 만든다. 아울러 그것들을 발동(trigger)시키는 테스트 루틴을 공식화할 수 있는지 알아본다.
  3. 테스트 과정에서 입력으로 제공하는 것에 대해 생각해 본다. 입력이 극한 값들이라면 어떤 일이 발생할까?
  4. 클래스의 수명이 유지되는 동안 항상 참이어야 하는 조건이 있는가? 불변 값을 검증하기 위해 테스트 루틴을 작성해 보자.

목표 테스트

코드의 어떤 부분을 이해하기 위해 테스트 루틴을 적용한 후에는 변경시키고자 하는 것들을 찾아보고 테스트 루틴이 변경하고자 하는 것들을 제대로 덮고 있는지를 알아봐야 한다.

분기를 위한 테스트

분기(branch)를 위한 테스트 루틴을 작성할 때는 해당 분기를 실행하는 방법 외에 테스트가 통과되도록 하는 다른 방법이 없는지 자문해 보자. 이것에 대한 확신이 없다면 감지변수 도입이나 디버거를 사용해 테스트가 잘 되었는지를 확인한다.

리팩토링할 때 고려해야 할 것

첫 번째는 ‘리팩토링한 후에도 동작은 존재하는가’는 것이고, 두 번째는 ‘동작이 올바로 연결되었는지’ 여부이다.

특성화 테스트 작성을 위한 휴리스틱

  1. 변경하고자 하는 부분을 위한 테스트 루틴을 작성한다. 코드의 동작을 이해하는 데 필요하다고 느끼는 만큼의 사례를 작성한다.
  2. 이어서 변경하고자 하는 특정한 것들을 살펴보고 그들을 위한 테스트 루틴 작성을 시도한다.
  3. 기능을 추출하거나 이동시키려 한다면 사례별로 동작들의 존재 여부와 연결을 검증할 수 있는 테스트 루틴을 작성한다. 이동시키고자 하는 코드를 수행하는지와 그 코드가 적절히 연결되었는지를 검증한 후에 변환을 수행한다.