[Clean Code] 15장 JUnit 들여다보기

ASk·2021년 12월 30일
1

Clean Code

목록 보기
15/17
post-thumbnail

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);
  }
}

접두어 제거

  • 변수의 범위를 나타내는 접두어 제거
// Before
private int fContextLength;

// After
private int contextLength;

조건문 캡슐화

  • 의도를 명확하게 표현하기 위해 조건문을 캡슐화한다.
// Before
if (expected == null || actual == null || areStringsEqual()) {
  return Assert.format(message, expected, actual);
}

// After
if (shouldNotCompact()) {
  return Assert.format(message, expected, actual);
}

private boolean shouldNotCompact() {
  return expected == null || actual == null || areStringsEqual();
}

변수명을 명확하게 변경

  • 함수에서 멤버 변수와 이름이 똑같은 변수를 사용하는 이유가 무엇일까?
  • 다른 의미라면 명확하게 붙여야 한다.
// Before
String expected = compactString(fExpected);
String actual = compactString(fActual);

// After
String compactExpected = compactString(expected);
String compactActual = compactString(actual);

부정문을 긍정문으로 변경

  • 부정문은 긍정문보다 이해하기 약간 더 어렵다.
  • 첫문장 if를 긍정으로 만들어 조건문을 반전한다.
// Before
if (shouldNotCompact()) {
  return Assert.format(message, expected, actual);
} else {
  // 처리 로직
}

private boolean shouldNotCompact() {
  return expected == null || actual == null || areStringsEqual();
}

// After
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);
}

일관적인 함수 사용방식 적용

  • 새 함수에서 마지막 두줄은 변수를 반환하지만 첫째 줄과 둘째 줄은 반환값이 없다.
  • 함수 사용방식이 일관적이지 못하다.
// Before
private void compactExpectedAndActual() {
  findCommonPrefix();
  findCommonSuffix();
  compactExpected = compactString(expected);
  compactActual = compactString(actual);
}

// After
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();
  // 처리 로직
}

리팩토링 정리

  • 모듈은 일련의 분석 함수와 일련의 조합 함수로 나뉜다.
  • 전체 함수는 위상적으로 정렬 했으므로 각 함수가 사용된 직후에 정의된다.
  • 분석 함수가 먼저 나오고 조합 함수가 그 뒤를 이어서 나온다.
  • 리팩토링 과정에서 초반에 내렸던 결정을 번복한 부분도 있다.
  • 코드를 리팩토링 하다 보면 원래 했던 변경을 되돌리는 경우가 흔하다.
  • 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.

결론

  • 우리는 보이스카우트 규칙도 지켰다.
  • 모듈은 처음보다 조금 더 깨끗해졌다. 원래 깨끗하지 못했다는 말은 아니다.
  • 하지만 세상에 개선이 불필요한 모듈은 없다.
  • 코드를 처음보다 조금 더 깨끗하게 만드는 책임은 우리 모두에게 있다.
profile
Backend Developer

0개의 댓글