(Working Effectively with Legacy Code) 003. Sensing and Separation

Sensing and Separation

소프트웨어가 현재 이상적인 환경이라면, 새로운 작업을 시작하는 데 딱히 신경쓸 필요가 없을 것이다.

하지만 실무에서 이상적인 환경을 달성하기란 매우 어렵기때문에 일반적으로 대부분의 경우에는 신경써야하는 상황일 것이다.

단일 클래스로 작성된 프로그램이 아닌이상 클래스들 간의 의존 관계가 존재할 것이고, 테스트 루틴으로 특정한 객체들만 검증하는 것 또한 쉽지않다.

앞선 포스팅에서 이 의존 관계를 제거하면서 단위 테스트를 작성하는 것을 설명하였지만, 단위 테스트 작성만이 의존 관계를 제거하는 이유는 아니다.

일반적으로 테스트 루틴을 배치할 때 의존 관계를 제거하는 이유는 아래 두 가지이다.

1. Sensing(감지)
코드 내에서 계산된 값에 접근할 수 없을 때, 이를 감지하기 위해 의존 관계를 제거해야 한다.

2.Separation(분리)
코드를 테스트 하네스 내에 넣어서 실행할 수 없을 때, 코드를 분리하기 위해 의존 관계를 제거해야 한다.

예제를 통해 한 번 이해해보자.

네트워크 관리 애플리케이션이 있고, NetworkBridge라는 클래스가 있다고 가정한다.

1
2
3
4
5
6
7
8
9
10
class NetworkBridge {
constructor(endPoints: Array<EndPoint>) {
// ...
}

fun formRouting(sourceID: String, destId: String) {
// ...
}
// ...
}

클래스에 대한 동작을 정의해보자.

NetworkBridge 클래스는 EndPoint 배열을 인자로 받아, 로컬 하드웨어를 사용해 네트워크 설정을 관리하며,

사용자는 NetworkBridge 클래스의 메서드를 사용하여 시작점에서 종점까지의 트래픽 경로를 제어한다.

NetworkBridge 클래스는 제어 작업을 수행하기 위해 EndPoint 클래스의 설정을 변경하고,

EndPoint 클래스의 객체들은 소켓을 열고 네트워크를 통해 특정 장치와 통신할 수 있다.

이때 NetworkBridge 클래스의 테스트 루틴을 작성하려면 어떻게 해야할까?

클래스가 하드웨어를 직접 제어하기때문에 고민이 생길 수 밖에 없다.

테스트를 위해 계속해서 하드웨어를 제어하는 것은 어려운 일이기도 때문이다.

즉 내부를 볼 수 없는 블랙박스 상태의 클래스를 대상으로 테스트를 하는 경우에 대해서 알아야 한다.

블랙박스 상태의 클래스는 메소드를 호출했을 때의 영향을 감지할 수도, 애플리케이션의 나머지 영역과 분리해서 실행할 수도 없기 때문이다.

이를 위해 협업 클래스를 위장하는 기법이 존재한다.

Faking Collaborators

상술한 블랙박스 상태의 클래스를 대상으로 테스트하기 위한 방법으로, 영향을 받는 부분을 별도의 코드로 대체하여 변경 대상만을 작성할 수 있다.

이러한 별도의 코드를 가짜 객체 혹은 위장 객체(Fake Object) 라고 부른다.

Fake Objects

위장 객체란 어떤 클래스를 테스트할 때, 그 클래스의 협업 클래스를모방하는 객체를 말한다.

예를 들어 어떤 POS 시스템이 있고, 해당 시스템에 Sale 클래스가 있다고 가정해보자.

Sale 클래스는 scan() 메서드를 가지고 있으며, scan() 메서드를 호출하면 Sale 객체는 해당 품목의 이름과 가격을 POS기의 디스플레이 화면에 출력한다.

이때 POS 기기와 디스플레이 화면은 하드웨어이기 때문에 화면에 미치는 영향을 테스트 하기 위해 위장 객체를 사용할 수 있다.

먼저 객체 ArtR56Display 클래스를 추가해보자.

ArtR56Display 클래스는 현재 사용중인 디스플레이 장치와 통신하는 데 필요한 모든 코드를 포함하고 있다.

화면에 표시될 텍스트를 ArtR56Display 클래스에 전달하고나면 Sale 클래스의 동작은 완료된다.

이렇게 코드를 이전하고나면 Sale 클래스는 ArtR56Display 클래스뿐만 아니라 또 다른 위장 객체인 FakeDisplay와 통신도 수행하더라도 동작의 변경이 없을 것이다.

이 위장 객체를 통해 Sale 클래스가 무슨 동작을 하는지 확인하기 위한 테스트 루틴을 작성할 수 있게 된다.

좀 더 구체화 해보도록 하자.

1
2
3
public interface Display {
fun showLine(line: String)
}

ArtR56Display 클래스와 FakeDisplay 클래스 모두 위의 Display 인터페이스를 구현하면,

Sale 클래스는 생성자를 통해 Display 객체를 전달받아서 처리할 수 있다.

아래 코드로 확인해보자.

1
2
3
4
5
6
7
8
9
10
class Sale(private val disaply: Display) {

fun scan(barcode: String) {
// ...
val itemLine: String = "${item.name} ${item.price.asDisplayText()}"
display.showLine(itemLine)
// ...
}

}

위의 코드처럼 Sale 클래스는 어떤 Display 객체가 주어지든지 동일한 동작을 수행하고 있음을 알 수 있다.

ArtR56Display 객체를 인자로 전달했다면 실제 POS 기기에 값이 표시될 것이고,

FakeDisplay 객체를 전달했다면 실제 화면에 값이 표시되진 않지만 무슨 값이 표시되어야 하는지는 알 수 있다.

UML로 다시 한 번 확인해보자.

위와 같이 위장 객체를 도입하였을 때, 아래와 같은 테스트 루틴을 구축할 수 있게 된다.

1
2
3
4
5
6
7
8
9
class SaleTest : TestCase() {

fun testDisplayAnItem() {
val display = FakeDisplay()
val sale = Sale(display)
sale.scan("1")
assertEquals("Milk $3.99", display.getLastLine())
}
}

이때 FakeDisplay 클래스는 아래와 같다.

1
2
3
4
5
6
7
8
9
class FakeDisplay : Display {
var lastline = ""

fun showLine(line: String) {
lastline = line
}

fun getLastLine() = lastLine
}

showLine() 메서드는 한 줄의 텍스를 받아서 lastLine 변수에 대입한다.

이후 getLastLine() 메서드가 호출될 때마다 lastLine 변수를 반환한다.

The Two Sides of a Fake Object

위장 객체는 양면성, 두 가지 측면을 가진다.

아래 UML을 보자.

FakeDisplay 클래스는 Display 인터페이스를 구현하기 때문에 showLine() 메서드의 구현이 강제된다.

이 메서드가 Sale 클래스가 접근할 수 있는 유일한 메서드이며, getLastLine() 메서드는 테스트용 메서드이다.

Fakes Distilled

여태까지 간단한 예제로 위장 객체의 핵심을 살펴보았다.

위장 객체는 다양한 방식으로 구현할 수 있지만, 객체지향 프로그래밍 언어에서는 간단한 클래스를 구현하는 것이 일반적이다.

객체지향 프로그래밍 언어가 아닐 경우엔 클래스가 아닌 함수를 대체함으로서 위장 코드를 구현할 수 있다.

이 대체 함수에 대해서는 추후에 다시 다루도록 하겠다.

Mock Objects

실제로 테스트 코드를 작성해본 개발자라면 Mock, Mocking이란 단어를 흔하게 접하고 사용했을 것이다.

위장 객체를 많이 사용해야하는 상황이 돌아온다면 위장 객체에서 한층 더 발전한 모조 객체(Mock Object) 의 사용을 검토하는 것이 좋다.

모조 객체란 내부적으로 검증을 수행하는 위장 객체를 말한다.

아래 예제 코드는 모조 객체로 작성한 테스트 루틴의 예시이다.

1
2
3
4
5
6
7
8
9
10
11
class SaleTest : TestCase() {

fun testDisplayAnItem() {
val display = MockDisplay()
display.setExpectation("showLine", "Milk $3.99")

val sale = Sale(display)
sale.scan("1")
display.verify()
}
}

모조 객체의 장점은 예상되는 메서드 호출을 모조 객체에 미리 알려주고, 실제로 해당 메서드가 호출되었는 지 검증할 수 있다.

위의 코드에서는 showLine() 메서드가 “Milk $3.99”라는 값과 함께 호출될 것이라고 미리 지정하며,

이후 verify() 메서드를 호출하여 검증하도록 지시하는 것이다.

이후 이 검증 성공에 따라 테스트의 성공과 실패가 분기된다.