Intro
- JUnit은 자바 프레임워크 중에서 가장 유명하다.
- 일반적인 프레임워크가 그렇듯 개념은 단순하며 정의는 정밀하고 구현은 우아하다.
- JUnit에 대해 알아보자.
JUnit 프레임워크
- JUnit은 저자가 많다. 하지만 시작은 켄트 벡과 에릭 감마 두 사람이다.
- 우리가 살펴볼 모률은 문자열 비교 오류를 파악할 때 유용한 코드다.
- 코드는 잘 분리되었고, 표현력이 적절하며, 구조가 단순하다.
- 저자들이 모듈을 아주 좋은 상태로 남겨두었지만 보이스카우트 규칙에 따라 개선해보자.
package junit.framework;
public class ComparisonCompactor {
private static final String ELLIPSIS = "...";
private static final String DELTA_END = "]";
private static final String DELTA_START = "[";
private int fContextLength;
private String fExpected;
private String fActual;
private int fPrefix;
private int fSuffix;
public ComparisonCompactor(int contextLength, String expected, String actual) {
fContextLength = contextLength;
fExpected = expected;
fActual = actual;
}
public String compact(String message) {
if (fExpected == null || fActual == null || areStringsEqual()) {
return Assert.format(message, fExpected, fActual);
}
findCommonPrefix();
findCommonSuffix();
String expected = compactString(fExpected);
String actual = compactString(fActual);
return Assert.format(message, expected, actual);
}
private String compactString(String source) {
String result = DELTA_START + source.substring(fPrefix, source.length() - fSuffix + 1) + DELTA_END;
if (fPrefix > 0) {
result = computeCommonPrefix() + result;
}
if (fSuffix > 0) {
result = result + computeCommonSuffix();
}
return result;
}
private void findCommonPrefix() {
fPrefix = 0;
int end = Math.min(fExpected.length(), fActual.length());
for (; fPrefix < end; fPrefix++) {
if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix)) {
break;
}
}
}
private void findCommonSuffix() {
int expectedSuffix = fExpected.length() - 1;
int actualSuffix = fActual.length() - 1;
for (; actualSuffix >= fPrefix && expectedSuffix >= fPrefix; actualSuffix--, expectedSuffix--) {
if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix)) {
break;
}
}
fSuffix = fExpected.length() - expectedSuffix;
}
private String computeCommonPrefix() {
return (fPrefix > fContextLength ? ELLIPSIS : "") + fExpected.substring(Math.max(0, fPrefix - fContextLength), fPrefix);
}
private String computeCommonSuffix() {
int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength, fExpected.length());
return fExpected.substring(fExpected.length() - fSuffix + 1, end) + (fExpected.length() - fSuffix + 1 < fExpected.length() - fContextLength ? ELLIPSIS : "");
}
private boolean areStringsEqual() {
return fExpected.equals(fActual);
}
}
접두어 제거
private int fContextLength;
private int contextLength;
조건문 캡슐화
- 의도를 명확하게 표현하기 위해 조건문을 캡슐화한다.
if (expected == null || actual == null || areStringsEqual()) {
return Assert.format(message, expected, actual);
}
if (shouldNotCompact()) {
return Assert.format(message, expected, actual);
}
private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}
변수명을 명확하게 변경
- 함수에서 멤버 변수와 이름이 똑같은 변수를 사용하는 이유가 무엇일까?
- 다른 의미라면 명확하게 붙여야 한다.
String expected = compactString(fExpected);
String actual = compactString(fActual);
String compactExpected = compactString(expected);
String compactActual = compactString(actual);
부정문을 긍정문으로 변경
- 부정문은 긍정문보다 이해하기 약간 더 어렵다.
- 첫문장 if를 긍정으로 만들어 조건문을 반전한다.
if (shouldNotCompact()) {
return Assert.format(message, expected, actual);
} else {
}
private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}
if (canBeCompacted()) {
} else {
return Assert.format(message, expected, actual);
}
private boolean canBeCompacted() {
return expected != null && actual != null && areStringsEqual();
}
적절한 함수이름 적용
- compact 함수는 canBeCompacted 가 false 면 압축하지 않는다.
- 그러므로 이름을 compact 로 할 경우 오류 점검이라는 부가 단계가 숨겨진다.
- 또한 해당함수는 단순히 압축 문자열 반환이 아닌 형식이 갖춰진 문자열을 반환한다.
- 따라서 formatCompactedComparison 이라는 이름이 적절하다.
함수 분리
- if문 안에서는 예상 문자열과 실제 문자열을 진짜로 압축한다.
- 이 부분을 빼내 compactExpectedAndActual 이라는 메서드로 만든다.
- compactExpectedAndActual은 압축만 수행한다
private String compactExpected;
private String compactActual;
public String formatCompactedComparison(String message) {
if (canBeCompacted()) {
compactExpectedAndActual();
return Assert.format(message, compactExpected, compactActual);
} else {
return Assert.format(message, expected, actual);
}
}
private void compactExpectedAndActual() {
findCommonPrefix();
findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
일관적인 함수 사용방식 적용
- 새 함수에서 마지막 두줄은 변수를 반환하지만 첫째 줄과 둘째 줄은 반환값이 없다.
- 함수 사용방식이 일관적이지 못하다.
private void compactExpectedAndActual() {
findCommonPrefix();
findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private void compactExpectedAndActual() {
prefixlndex = findCommonPrefix();
suffixlndex = findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
숨겨진 시간적인 결합(hidden temporal coupling)
- findCommonSuffix 에 숨겨진 시간적 결합이 존재한다.
- findCommonSuffix 는 findCommonPrefix가 prefixIndex를 계산한다는 사실에 의존한다.
- 함수 호출 순서가 바뀔경우 오류를 찾아내기 힘들다.
- 따라서 시간 결합을 외부에 노출하고자 인수로 넘기도록 변경한다.
private void compactExpectedAndActual(} {
prefixIndex = findCommonPrefix(};
suffixIndex = findCommonSuffix(prefixlndex);
compactExpected = compactString(expected};
compactActual = compactString(actual};
}
- 하지만 이방식은 호출순서는 명확하지만 prefixlndex 가 필요한 이유는 설명하지 못한다.
- 의도가 분명히 드러나지 않으므로 다른 프로그래머가 되돌려 놓을지도 모른다.
- 따라서 findCommonPrefixAndSuffix 로 합친 이후에 findCommonPrefix 를 먼저 호출하도록 변경한다.
private void compatExpectedAndActual() {
findCommonPrefixAndSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
}
리팩토링 정리
- 모듈은 일련의 분석 함수와 일련의 조합 함수로 나뉜다.
- 전체 함수는 위상적으로 정렬 했으므로 각 함수가 사용된 직후에 정의된다.
- 분석 함수가 먼저 나오고 조합 함수가 그 뒤를 이어서 나온다.
- 리팩토링 과정에서 초반에 내렸던 결정을 번복한 부분도 있다.
- 코드를 리팩토링 하다 보면 원래 했던 변경을 되돌리는 경우가 흔하다.
- 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.
결론
- 우리는 보이스카우트 규칙도 지켰다.
- 모듈은 처음보다 조금 더 깨끗해졌다. 원래 깨끗하지 못했다는 말은 아니다.
- 하지만 세상에 개선이 불필요한 모듈은 없다.
- 코드를 처음보다 조금 더 깨끗하게 만드는 책임은 우리 모두에게 있다.