개요
JUnit은 자바 프레임워크 중에서 가장 유명하다.
일반적인 프레임 워크는 개념이 단순하며 정의는 치밀하고 구현은 우아하다.
하지만 실제 코드는 어떨지 이번 장에서 평가해 보자.
참고로 글쓴이는 전체 코드를 보여주지 않고 핵심만 요약해서 알려주겠다.
JUnit 프레임워크
JUnit은 저자가 많다. 하지만 시작은 켄트 벡과 에릭 감마 두 사람이다.
우리가 살펴볼 모듈은 문자열 비교 오류를 파악할 때 유용한 코드다.
코드는 잘 분리되었고, 표현력이 적절하며, 구조가 단순하다.
저자들이 모듈을 아주 좋은 상태로 남겨두었지만 보이스카우트 규칙에 따라 개선해 보자.
변수의 범위를 나타내는 접두어 제거한다.
// 개선 전
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()) {
...
}
private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}
// 개선 후
if (canBeCompacted()) {
...
}
private boolean canBeCompacted() {
return expected != null && actual != null && areStringsEqual();
}
그외
- 함수명을 명확하게 한다
- 함수는 한 가지 일만 하게 하자
- 함수 사용 방식을 일관적으로 하자
- 적절한 이름 사용하기
- 경계 조건을 캡슐화하기
최종 코드
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 contextLength;
private String expected;
private String actual;
private int prefixLength;
private int suffixLength;
public ComparisonCompactor(int contextLength, String expected, String actual) {
this.contextLength = contextLength;
this.expected = expected;
this.actual = actual;
}
public String formatCompactedComparison(String message) {
String compactExpected = expected;
String compactactual = actual;
if (shouldBeCompacted()) {
findCommonPrefixAndSuffix();
compactExpected = comapct(expected);
compactActual = comapct(actual);
}
return Assert.format(message, compactExpected, compactActual);
}
private boolean shouldBeCompacted() {
return !shouldNotBeCompacted();
}
private boolean shouldNotBeCompacted() {
return expected == null && actual == null && expected.equals(actual);
}
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
suffixLength = 0;
for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
break;
}
}
}
private boolean suffixOverlapsPrefix(int suffixLength) {
return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;
}
private void findCommonPrefix() {
int prefixIndex = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixLength < end; prefixLength++) {
if (expected.charAt(prefixLength) != actual.charAt(prefixLength)) {
break;
}
}
}
private String compact(String s) {
return new StringBuilder()
.append(startingEllipsis())
.append(startingContext())
.append(DELTA_START)
.append(delta(s))
.append(DELTA_END)
.append(endingContext())
.append(endingEllipsis())
.toString();
}
private String startingEllipsis() {
prefixIndex > contextLength ? ELLIPSIS : ""
}
private String startingContext() {
int contextStart = Math.max(0, prefixLength = contextLength);
int contextEnd = prefixLength;
return expected.substring(contextStart, contextEnd);
}
private String delta(String s) {
int deltaStart = prefixLength;
int deltaend = s.length() = suffixLength;
return s.substring(deltaStart, deltaEnd);
}
private String endingContext() {
int contextStart = expected.length() = suffixLength;
int contextEnd = Math.min(contextStart + contextLength, expected.length());
return expected.substring(contextStart, contextEnd);
}
private String endingEllipsis() {
return (suffixLength > contextLength ? ELLIPSIS : "");
}
}
코드가 상당히 깔끔해졌다.
모듈은 일련의 분석 함수와 조합 함수로 나뉜다.
전체 함수는 위상적으로 정렬했으므로 각 함수가 사용된 직후에 정의된다.
분석 함수가 먼저 나오고 조합 함수가 그 뒤를 이어서 나온다.
코드를 주의 깊게 살펴보면 초반에 내렸던 결정을 일부 번복했다는 사실을 알 수 있다.
코드를 리팩터링 하다 보면 흔히 원래 했던 변경을 되돌리는 경우가 흔하다.
리팩터링은 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.
결론
보이스카우트 규칙을 지키며 모듈은 처음보다 조금 더 깨끗해졌다.
원래 깨끗하지 못했다는 말이 아니라 저자들은 우수한 모듈을 만들었다.
하지만 세상에 불필요한 모듈은 없다.
코드를 처음보다 조금 더 깨끗하게 만드는 책임은 우리 모두에게 있다.
글쓴이의 생각
책을 보면서 느낀 거는 리팩토링이 단순한 노력으로 뚝딱 되는 게 아니라는 사실이다.
요즘은 AI에 코드 넣고 리팩토링 해달라 하면 한 번에 해주는데
저자가 고치고 번복하고 하는 모습을 보면 많은 노력이 들어가는 작업이라는 사실을 느꼈다.
제일 와닿았던 말은 보이스카우트 규칙이다.
이 뒷장에서도 자주 나오는 말이지만 보이스카우트를 하면 썼던 것보다 더 깨끗이 하고 돌아가야 한다.
코드도 처음보다 더 깨끗하게 되도록 노력해야 한다.
'클린코드(CleanCode) 독후감' 카테고리의 다른 글
[클린코드(CleanCode)] 17장 냄새와 휴리스틱 (2) | 2025.01.31 |
---|---|
[클린코드(CleanCode)] 16장 SerialDate 리팩터링 (6) | 2025.01.30 |
[클린코드(CleanCode)] 14장 점진적인 개선 (2) | 2025.01.30 |
[클린코드(CleanCode)] 13장 동시성 (6) | 2025.01.30 |
[클린코드(CleanCode)] 12장 창발성 (4) | 2025.01.29 |