////
Search
Duplicate
🛖

Chapter 4. 봉합 모델

테스트하기 편리한 프로그램을 완성하는 유일한 방법은 개발 과정에서 테스트 루틴 작성을 병행하거나 테스트를 지원하는 설계를 위해 많은 시간을 투자하는 것뿐이다.
저자의 생각에 전자의 경우, 희망적인 측면이 많은 반면, 후자의 경우, 그간의 사례를 빌어 그다지 성공적인 것 같지 않다.
저자의 경우, 코드를 테스트하는 시도를 통해 다른 관점에서 코드를 바라보기 시작할 수 있었다. 이처럼 코드를 다른 시각으로 바라보는 것은 새롭고 익숙하지 않은 프로그래밍 언어로 작업할 때, 많은 도움을 준다.

봉합

단위 테스트를 위해 개별 클래스를 추출하려면 대부분의 경우, 수많은 의존 관계를 먼저 덜어낼 필요가 있다.
흥미로운 것은 아무리 좋은 설계에 기반하고 있다하더라도 꽤 많은 작업이 수반된다는 점이다.
테스트를 하기 위해 제품 코드로부터 클래스를 추출하는 작업을 하다 보면 좋은 설계란 무엇인지에 대한 기존의 생각이 달라지게 된다. 또한 소프트웨어를 기존과 전혀 다른 측면에서 바라보게 될 것이다.
프로그램을 커다란 종이 위에 적힌 문자들의 나열이라고 생각하지 않게 되기 때문이다.
그럼 프로그램을 어떤 관점에서 바라봐야 할까?
봉합 지점이란 코드를 직접 편집하지 않고도 프로그램의 동작을 변경할 수 있는 위치를 말한다. 아래 예제를 봐보자.
public class MessageSender { public void send(String type) { ... if (type.equals("오류") { ExceptionThrower.throwError(); } ... } } public class ExceptionThrower { public void throwError() { throw new Exception(); } }
Java
복사
MessageSender를 테스트해야 할때, send 메소드가 ExceptionThrower라는 객체에 의존성을 가지고 있다. 하지만 우리는 이 테스트의 범위를 해당 클래스만으로 제한하고 싶을 때 어떻게 할 수 있을까?
봉합 기법은 MessageSenderthrowError 메소드를 추가하고 테스트 시에, throwError 메소드를 재정의한 테스트 객체를 사용하는 것이다.
public class MessageSender { public void send(String type) { ... if (type.equals("오류") { throwError(); } } public void throwError() { throw new ExceptionThrower.throwError(); } } public class TestMessageSender extends MessageSender { @Override public void throwError() { throw new ExceptionThrower.throwError(); } }
Java
복사
이렇게하면 해당 객체가 발생시키는 사이드 이펙트를 고려하지 않고 테스트를 해볼 수 있다.
전역 객체일 경우, 그 영향이 더 클 수 있다.
이와 같은 봉합을 저자는 객체 봉합이라 부르며, 이 경우 호출하는 코드를 변경하지 않고 호출되는 메소드만 변경할 수 있다. 객체 봉합은 객체지향 언어에서 사용해볼 수 있다.
그렇다면 봉합을 왜 써야하는 걸까? 무슨 의미를 가지는 것일까?
수많은 의존 관계를 봉합 지점에서 다른 것으로 대체할 수 있다면 의존 관계를 배제하여 테스트 범위를 한정적으로 제한시킬 수 있기 때문이다.
테스트 시, 의존 관계가 늘어나면 늘어날수록 실패 지점을 찾기가 어려워지고 디버깅도 어렵다.
이러한 작업을 통해 데체적으로 충분한 테스트를 준비할 수 있고 좀 더 적극적인 테스트를 수행할 수 있다.

봉합의 종류

사용할 수 있는 봉합의 종류는 프로그래밍 언어에 따라 달라지는데, 봉합의 종류를 완벽히 이해하는 가장 좋은 방법은 컴파일되는 단계를 일일이 따라가는 것이다.
단계마다 새로운 종류의 봉합을 발견할 수 있을 것이다.

전처리기 봉합

C, C++와 같이 매크로 전처리기를 사용하는 언어에 한정되는 이야기라 불필요하다 판단하여 건너뛰었습니다.

링크 봉합

많은 프로그래밍 언어에서는 컴파일만으로 빌드가 완료되지 않는다. 컴파일러는 코드의 중간 표현을 생성하며, 이 중간 표현은 다른 파일에 들어있는 코드를 호출한다.
이때 링커는 중간 표현들을 조합한다. 링커가 호출 주소를 변환해주기 때문에 완전한 실행 프로그램을 만들 수 있게 된다.
자바의 경우, 이러한 작업을 컴파일러가 내부적으로 수행한다.
언어별로 주소 변환을 수행하는 방법은 다르지만, 일반적으로 링크 봉합 기법을 사용해 프로그램의 일부를 대체할 수 있다.
이는 임포트하고 있는 대상 클래스나 함수에 대해 별도의 라이브러리를 작성한 후, 테스트용으로 빌드 스크립트를 변경하여 원래의 라이브러리에 링크시키는 것이다.
이는 수고를 필요로 하지만 코드 내의 여러 위치에 존재하는 서드파티 라이브러리를 호출하는 경우 많은 도움이 된다.
⇒ 뭔가 쓸 것 같지는 않지만 알아두면 좋을 수 있는 방법일 것 같다. 고대의 프로그래머들은 이런식으로 의존성을 제한했구나 싶은?

객체 봉합

객체 봉합은 객체 지향 언어에서 사용할 수 있는 봉합 기법들 중 가장 유용하다.
cell.recalculate();
Java
복사
이 코드를 보면 recalculate라는 메소드가 존재하고 호출을 통해 이 메소드가 호출되어 실행될 것임을 짐작할 수 있다. 그런데 문제는 동일한 이름의 메소드가 중복으로 존재할 수 있다는 점이다.
abstract class Cell { ... } class ValueCell extends Cell { ... } class FormulaCell extends Cell { ... }
Java
복사
이 상황에서 cell.recalculate()를 호출한다면 어느 메소드가 호출될 까?
cell 변수가 어느 객체를 가리키는지 모르면 어느 함수가 호출될지 알 수 없다. 이처럼 다른 코드의 변경 없이 호출되는 recalculate 메소드를 변경할 수 있다면 해당 위치를 봉합 지점이라고 부를 수 있다.
다만 다음 같은 상황은 봉합 지점이 아님을 인지하자.
public void etc() { Cell cell = new Cell(); cell.recalculate(); }
Java
복사
Cell 변수의 클래스는 객체가 생성될 때 결정되므로 메소드를 수정하지 않는 한 cell 클래스를 변경할 수 없다.
이때 활성화 지점이 없기 때문에 봉합 지점이 존재하지 않는다.
다음과 같은 상황은 봉합 지점이다.
public void etc(Cell cell) { cell.recalculate(); }
Java
복사
이는 어떤 cell이든 Cell을 구현하는 클래스가 제공되어 대체될 수 있기 때문이다.
여기서 활성화 지점은 etc의 인수 리스트다. 어떤 종류의 객체를 전달할지 결정하고 테스트에 필요하다면 메소드의 동작을 변경할 수 있기 때문이다.
다음은 좀 까다롭다. 다음은 봉합 지점이 존재하는가?
public class Etc extends Etb { public void et(Cell cell) { ... recalculate(); ... } private static void recalcualte(Cell cell) { ... } }
Java
복사
et 내의 recalculate 호출은 봉합에 해당한다. et 메소드를 변경하지 않고도 호출 동작을 변경할 수 있기 때문이다.
다음과 같이 봉합해볼 수 있다.
public class Etc extends Etb { public void et(Cell cell) { ... recalculate(); ... } protected void recalcualte(Cell cell) { ... } } public class TestEtc extends Etc{ @Override protected void recalcualte(Cell cell) { ... } }
Java
복사
테스트를 수행할 때, 적절한 종류의 봉합을 찾는 것은 중요하다. 일반적으로 객체 봉합은 객체 지향 언어에서 가장 적합한 방법이다.
전처리기 봉합이나 링크 봉합도 편리할 때가 있지만 객체 봉합만큼 분명하지는 않다. 게다가 이들에 의존하는 테스트 루틴은 관리하기도 어렵다.
이들은 의존 관계가 매우 복잡하거나 다른 대안이 없을 경우에 사용하는 것이 바람직하다.
봉합이라는 관점에서 코드를 바라보는 것이 익숙해지면 테스트 방법이나 테스트 친화적인 코드 구조를 고안하는 방법을 더 쉽게 찾을 수 있게 된다.