(Working Effectively with Legacy Code) 005. Tools

Tools

레거시 코드를 다루기 위해 어떤 도구들에 대해서 알아보자.

Automated Refactoring Tools (리팩토링 자동화 도구)

리팩토링을 개발자하는 직접 하는 것도 좋지만, 개발자 대신에 리팩토링을 수행하는 도구가 있다.

1992년 랄즈 존스의 박사 과정 학생이었던 Bill Opdyke(빌 옵다이크) 가 리팩토링에 관한 학위 논문을 작성하기 위해 C++ 리팩토링 도구를 개발하였다.

참고 William F. Opdyke’s REFACTORING OBJECT-ORIENTED FRAMEWORKS (1992)

이 도구는 상용화되진 않았지만 다른 언어의 리팩토링 도구에 많은 영향을 주었다.

특히 일리노이 대학의 John Brant(존 브랜트) 와 **Don Roberts(돈 로버츠)**가 개발한 스몰토크 리팩토링 브라우저에 영향을 끼쳤으며, 이 브라우저는 많은 종류의 리팩토링을 지원하며 오랫동안 자동화 리팩토링 기술의 가장 발전된 형태로 인정받았다.

많은 수의 자바 리팩토링 도구들도 현재 존재하고 있으며, 대부분 IDE에 포함되어있다.

C나 Java 뿐만 아니라 Delphi, C# 언어 등의 리팩토링 도구의 개발도 현재 진행중이다.

여기서 리팩토링의 정의에 대해 다시 한 번 상기해보자.

Martin Fowler(마틴 파울러) 가 직접 정의한 리팩토링은 소프트웨어의 기존 동작을 변경하지 않으면서, 이해 및 변경이 용이하도록 소프트웨어의 내부 구조를 변경하는 작업 이다.

참고 리팩토링의 정의는 마틴 파울러의 Refactoring : Improving the Design of Existing Code (1999) 에서 사용되었다.

요점은 코드 변경이 리팩토링으로 인정받으려면, 기존 동작이 변경되지않았음을 검증해야 한다는 것이며 이를 리팩토링 도구들을 활용하여 수행한다.

때문에 리팩토링 도구는 신중하게 선택해야 하며, 해당 도구의 개발자가 어떻게 안정성에 대해 언급하고 있는지도 검토해야 한다.

Tests and Automated Refactoring(테스트와 리팩토링 자동화)

리팩토링 도구가 있으면 리팩토링을 수행한 코드에 대해 테스트 루틴을 작성할 필요가 없다고 착각할 수 있다.

이 말이 완전히 틀린 말은 아니지만(테스트 루틴이 필요없는 경우가 있을 수 있으므로), 도구로 수행한 리팩토링이 기존 동작에 영향을 주지 않을 것이라고 장담할 수는 없다.

아래 예제를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A {
private int alpha = 0;
private int getValue() {
alpha++;
return 12;
}

public void doSomething() {
int v = getValue();
int total = 0;
for (int n = 0; n < 10; n++) {
total += v;
}
}
}

위의 코드를 보면 doSomething() 메서드에서 선언된 변수 v를 제거할 수 있다는 것을 알 수 있다.

도구를 이용해 아래와 같이 리팩토링 하였다고 가정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A {
private int alpha = 0;
private int getValue() {
alpha++;
return 12;
}

public void doSomething() {
int total = 0;
for (int n = 0; n < 10; n++) {
total += getValue();
}
}
}

변수 v는 제거되었지만, alpha 변수가 한 번이 아닌 10번의 증가를 수행하게 된다.

즉, 동작이 명백하게 바뀌게 된 것이다.

이 예제처럼 리팩토링 도구에 대한 맹신을 버리고, 테스트 루틴을 통해 검증 절차를 수행하는 것이 권장된다.

Mock Objects (모조 객체)

거듭 언급하듯, 레거시 코드를 다룰 때 가장 큰 문제점은 의존 관계이며 이를 제거하는 것은 매우 어려운 작업이다.

다른 코드를 제거한 상태로 특정 코드를 제대로 테스트하려면 제거한 다른 코드 대신 올바른 값을 제공하는 또 다른 코드가 필요해지며,

이를 객체 지향에서는 Mock Object(모조 객체) 라고 부른다.

참고 모조 객체 라이브러리 관련 블로그

Unit-Testing Harnesses (단위 테스트 하네스)

테스트는 매우 중요하고 어려운 문제임에도, 기존의 애플리케이션에 대한 특별한 처리 없이 GUI나 웹 인터페이스에서 곧바로 테스트하려는 경우가 많다.

이러한 외부 테스트가 불가능한 것은 아니지만, 실제로는 훨씬 많은 추가 작업이 필요하다.

더욱이 사용자 인터페이스는 테스트 루틴을 작성하기에 좋은 위치가 아니기도 하다.

이는 UI 자체가 매우 자주 바뀌는 성질의 것이기도 하고, 테스트 대상 기능으로부터 어느정도의 거리가 존재하기 때문이기도 하다.

xUnit Test Framework

xUnit Test Framework는 무료로 쓸 수 있는 단위 테스트용 프레임워크이다.

Kent Back(켄트 백) 이 스몰토크로 작성한 버전을 추후 Erich Gamma 와 함께 자바로 이식하였다.

주요 특징들은 아래와 같다.

  • 프로그래머는 현재 사용 중인 개발 언어로 테스트 루틴을 작성할 수 있다.
  • 모든 테스트는 독립적으로 실행된다.
  • 테스트들을 그룹 단위로 묶어서 필요할 때마다 실행 또는 재실행할 수 있다.

xUnit은 대부분의 주요 언어와 일부 비주류 언어에 이식되어 있다.

이 xUnit 설계의 혁신적인 점은 단순함과 집중성이며, 이로 인해 큰 어려움 없이 단위 테스트를 작성할 수 있다.

물론 좀 더 대규모의 테스트에도 사용될 수 있다.

jUnit

자바의 경우 xUnit의 하네스로서 jUnit이 주로 사용된다.

jUnit에서는 TestCase 라는 클래스를 상속받아 아래와 같이 테스트 루틴을 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
import junit.framework.*;

public class FormulaTest extends TestCase {
public void testEmpty() {
assertEquals(0, new Formula("").value());
}

public void testDigit() {
assertEquals(1, new Formula("1").value());
}
}

테스트를 정의하려면 테스트 클래스에 대해 테스트 케이스마다 void testXXX()와 같은 형태의 시그니처를 갖는 메서드를 작성하면 된다.

테스트 메서드는 코드 및 assert*() 메서드를 사용하는 assertion(확중문) 포함할 수 있다.

jUnit으로 테스트를 실행하면 TestRunner가 테스트 클래스를 읽고 Reflection을 사용하여 테스트 메서드를 모두 찾아낸다.

그 다음 각 테스트 메서드마다 별개의 객체를 생성하여 테스트 메서드간의 영향 받을 여지를 없애버린다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EmployeeTest extends TestCase { 

private Employee employee;

protected void setUp() {
employee = new Employee("Fred", 0, 10);
TDate cardDate = new TDate(10, 10, 2000);
employee.addTimeCard(new TimeCard(cardDate,40));
}

public void testOvertime() {
TDate newCardDate = new TDate(11, 10, 2000);
employee.addTimeCard(new TimeCard(newCardDate, 50));
assertTrue(employee.hasOvertimeFor(newCardDate));
}

public void testNormalPay() {
assertEquals(400, employee.getPay());
}
}

EmployeeTest 클래스에서는 setUp() 이라는 특별한 메서드가 있다.

setUp() 메서드는 TestCase 클래스에 정의되어있고, 테스트 메서드가 실행되기 전에 각 테스트 객체에서 실행된다.

해당 메서드를 사용하여 각 테스트 객체가 테스트를 실행하기전 사용될 객체 혹은 값들에 대해 초기화를 진행할 수 있다.

테스트가 완료된 후 특별한 처리가 필요하다면 tearDown() 메서드를 재정의하면 된다.

그렇다면 TestCase 클래스에는 setUp() 메서드와 tearDown() 메서드가 필요한 것일까?

별도의 생성자가 아닌 메서드에서 객체를 초기화하는 이유는 TestRunner가 각 테스트하려는 클래스의 메서드마다 객체를 생성하기 때문이다.

생성하려는 객체의 수가 많더라도 필요한 것만 생성해서 쓴다면 큰 문제가 되지 않으므로, 메모리를 절약하는 것이다.

CppUnitLite

C++과 자바의 차이점 중 하나로 리플렉션 기능이 부족하다는 것을 들 수 있다.

C++에서는 실행할 때 접근해야하는 메서드를 등록하는 코드를 개발자가 직접 작성해야하는 번거로움이 존재한다.

이 번거로움을 가진 CppUnit 을 포팅한 것이 CppUnitLite 이다.

1
2
3
4
5
6
7
8
9
#include "testharness.h" 
#include "employee.h"
#include <memory>
using namespace std;

TEST(testNormalPay,Employee) {
auto_ptr<Employee> employee(new Employee("Fred", 0, 10));
LONGS_EQUALS(400, employee -> getPay());
}

CppUnitLite에서는 다양한 매크로로 이 문제점을 해결하게 해준다.

위의 코드는 LONGS_EQUALS 라는 매크로를 사용하여 두 개의 long 타입이 같은 값을 가지는 지 검증한다.

즉 자바의 assertEquals()와 같은 방식으로 동작하는 것이다.

NUnit

NUnit은 .NET 언어를 위한 테스트 프레임워크이다.

C#이나 VB.NET 코드 등 .NET 플랫폼 상에서 구동되는 언어를 위한 테스트 루틴을 작성할수 있다.

대붑눈 jUnitㅇ과 유사하지만, 결정적인 차이로 NUnit은 테스트 메서드와 테스트 클래스를 설정할 때 속성값을 사용한다.

아래는 VB.NET으로 작성된 NUnit 테스트 루틴의 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Imports NUnit.Framework

<TestFixture()> Public Class LogOnTest
Inherits Assertion

<Test()> Public Sub TestRunValid()
Dim display As New MockDisplay()
Dim reader As New MockATMReader()
Dim logon As New LogOn(display, reader)
logon.Run()
AssertEquals("Please Enter Card", display.LastDisplayedText)
AssertEquals("MainMenu",logon.GetNextTransaction().GetType.Name)
End Sub
End Class

etc

상술했듯 xUnit은 다양한 언어와 플랫폼으로 이식되었다.

Ron Jeffries(론 제프리스) 가 운영하는 아래 사이트에서 거의 모든 xUnit 버전을 참조하고 다운로드받을 수 있다.

참고 RonJeffries.com

General Test Harnesses (일반적인 테스트 하네스)

위의 xUnit Test Framework는 본래 단위 테스트용으로 설계되었다.

물론 한 번에 여러 클래스를 테스트할 수는 있지만, 이러한 작업에 좀 더 특화된 도구를 사용하는 것이 좋다.

FIT : Framework for Integrated Test

FIT 은 간결하고 정밀한 테스트 프레임워크로 Ward Cunningham(워드 커닝험) 이 개발하였다.

FIT을 사용하기 위해선 먼저 시스템과 관련된 문서를 작성해야 한다.

해당 문서에 입력 값과 출력 값을 기술한 테이블이 포함되어있고, 이를 HTML 형식으로 저장하면 FIT은 해당 문서를 테스트 루틴으로 실행해준다.

인풋이 HTML인만큼 아웃풋도 HTML 형식으로 출력해준다.

결과로 나온 HTML 파일을 열어보면 인풋에 기술한 테이블에 대해 테스트 성공과 실패 여부를 초록색과 빨간색으로 표기해준다.

FIT의 강점 중 하나는 소프트웨어를 개발하는 사람과 해당 소프트웨어의 요구사항을 명시한 사람간의 의사소통을 용이하게 해준다는 점이다.

요구 사항을 명시한 사람들은 직접 문서를 작성하고 실질적인 테스트 루틴을 해당 문서에 삽입할 수 있기때문이다.

참고 Cunningham & Cunningham, Inc.

Fitnesse

Fitnesse는 위키 상에 구축된 FIT를 가리키며 Robert Martin(로버트 마틴)Micah Martin(미카 마틴) 에 의해 대부분 개발되었다.

Fitnesse는 FIT 테스트를 정의하는 계층적인 웹 페이지를 지원하며, 테스트용 테이블을이 포함된 페이지들을 개별적으로 혹은 한 번에 실행할 수 있다.

또한, 다양한 옵션들을 통해 팀 단위의 협업 작업을 좀 더 쉽게 수행할 수 있게 해준다.

참고 FitNesse front page