16장. SerialDate 리팩터링

공부하는 감자·2024년 2월 19일
0

클린코드

목록 보기
16/18

이 장의 주제

부록의 코드와 함께 보면서 설명하는 챕터다.

책의 흐름을 따라가며 리팩토링 시 신경써야 할 것 같은 부분들만 일부 정리해봤다.

  • JCommon 라이브러리에서 org.jfree.date 패키지의 SerialDate라는 클래스를 탐험한다.
  • SerialDate는 우수한 코드이지만, 이 장에서는 코드를 낱낱이 까발려 본다.
    • 여기서 수행하는 분석은 전문가 입장에서 수행하는 검토, 그 이상도 그 이하도 아니다.
    • 우리 모두가 편안하게 여기고 남이 내게 해준다면 감사히 반겨야 할 활동이다.

SerialDate 클래스

  • 날짜를 표현하는 자바 클래스
  • 자바에서 제공하는 java.util.Date , java.util.Calendar 등과 같은 클래스의 불편함을 없애고자 구현되었다.
    • 구현한 사람인 데이비트는 javadoc 주석에 이유를 잘 설명해 두었다.

개선

첫째, 돌려보자

테스트 실행

  • SerialDateTests 라는 클래스는 단위 테스트 케이스 몇 개를 포함한다.
    • 돌려보면 실패하는 데스트 케이스는 없지만, 테스트 케이스를 훑어보면 모든 경우를 점검하지 않는다는 사실이 드러난다.
    • 예를 들어, MonthCodeToQuarter 메서드를 전혀 호출(실행)하지 않는다.

테스트 케이스 조사

  • 클로버(Clover)를 이용해 단위 테스트가 실행하는 코드와 실행하지 않는 코드를 조사했다.
  • 클로버에 따르면, SerialDate에서 실행 가능한 문장 185개 중 단위 테스트가 실행하는 문장은 91개에 불과했다.
    • 즉, 대략 50% 정도였다.

테스트 케이스 구현

  • 클래스를 철저히 이해하고 리팩터링하려면 훨씬 높은 테스트 커버리지가 필요하다.
    • 따라서 서적에서는 독자적으로 단위 케이스를 구현했다.
  • 실패한 테스트는 주석으로 임시 처리하고, 리팩토링하면서 모든 테스트 케이스(실패한 테스트도) 통과하게 코드를 손 본다.
    • 경계 조건 오류 등
  • 모든 테스트 케이스를 통과하도록 코드를 수정하고 나면 다음 단계로 넘어간다.

둘째, 고쳐보자

리팩토링

  • 코드를 고칠 때마다 JCommon 단위 테스트와 직접 구현한 단위 테스트를 실행한다.
    • 즉, 변경한 코드는 JCommon 프레임워크에서 사용해도 문제 없다.

[C2] 오래된 주석 개선

  • 처음에 나오는 주석은 너무 오래되어서 간단하게 고치고 개선한다.
  • 1행을 보면 라이선스 정보, 저작권, 작성자, 변경 이력이 나온다.
    • 법적인 정보(라이선스 정보, 저작권)은 보존하고 1960년대에 나온 방식으로 적힌 변경 이력은 없애도록 한다.
    • 이제는 변경 이력 대신 소스 코드 제어 도구를 사용한다.
  • 한 소스 코드에 여러 언어를 사용하여 만들어진 javadoc 주석을 모두 <pre> 태그로 감쌌다.
    • 이 주석은 자바, 영어, Javadoc, HTML을 사용했다.
    • 주석 전부를 <pre> 태그/로 감싸면 소스 코드에 보이는 형식이 Javadoc에 그대로 유지된다.

[J1] Import 문 줄이기

  • import 문은 java.util.*java.util.* 로 줄인다.

[N1, N2] 클래스 이름 변경

  • 클래스 이름이 SerialDate 인 이유는 ‘일련변호(serial number)’를 사용해 클래스를 구현했기 때문이다.
    • 단서는 SERIAL_LOWER_BOUNDSERIAL_UPPER_BOUND 라는 상수에 있다.
    • 1899년 12월 30일을 기준으로 경과한 날짜 수를 사용한다.
  • 서적에서는 두 가지 이유를 들어 클래스 명을 변경했다.
    1. [N1] ‘일련번호’보다 상대 오프셋(relative offset)이 더 정확하다.
      • 일련번호라는 용어는 날짜보다 제품 식별 번호로 더 적합하다.
    2. [N2] SerialDate 라는 이름은 구현을 암시하는데 실상은 추상 클래스다.
      • 구현은 숨기는 편이 좋다.
      • 추상화 수준이 올바르지 못하다.
  • 변경한 클래스 이름은 DayDate 이다.
    • 자바 라이브러리에는 Date라는 클래스가 너무 많아서 그냥 Date라는 이름은 사용하지 못했다.
    • 실제 SerialDate는 시간이 아니라 날짜를 다루므로 Day라는 이름을 고려했으나, Day 또한 너무 많이 쓰이므로 사용하지 못했다.

[G12] enum을 모두 독자적인 소스 파일로 이동

  • DayDate 클래스는 MonthConstants 라는 월을 정의하는 static final 상수 모음 클래스를 상속받고 있다.
    • 상수 클래스를 사용하면 MonthConstants.January 와 같은 표현을 사용할 필요가 없어진다.
    • 옛날 자바 프로그래머들이 많이 쓰던 기교로, 바람직하다고 보기는 어렵다.
  • 따라서, MonthConstants 는 enum으로 정의한다.

[G4] 직렬화 ID를 자동 제어하도록 수정

  • serialVersionUID 변수는 직렬화(serializer)를 제어한다.
    • 이 변수 값을 변경하면 이전 소프트웨어 버전에서 직렬화한 DayDate를 더 이상 인식하지 못한다.
    • 이 변수를 선언하지 않으면 컴파일러가 자동으로 생성한다.
    • 사소한 변경을 가했는데 이전 클래스를 복원하지 못하는 사태를 피하기 위해 직접 제어하는 편이 낫다고 주장하는 이들도 있다.
    • 그러나 책의 저자는 자동 제어를 사용함으로써 발생하는 오류는 원인과 결과가 분명함으로 자동 제어를 사용하도록 수정했다.
    • 요약하자면 특정 버전에서 직렬화한 클래스를 다른 버전에서 복원하지 않는 편이 안전하다.

[G6] 정적 변수와 정적 메서드를 새 클래스로 이동

  • DayDate 클래스가 표현할 수 있는 최초 날짜와 최후 날짜를 의미하는 두 변수가 있다.
    • DayDate 클래스에서 파생한 SpreadsheetDate 클래스를 살펴보면 이해가 간다.
public static final int EARLIEST_DATE_ORDINAL = 2;   // 1/1/1900
public static final int LASTEST_DATE_ORDINAL = 2958; // 12/31/9999
  • 이 사안은 SpreadsheetDate 의 구현과 관련되지, DayDate 와 아무런 상관이 없다.
    • 따라서 DayDate 클래스에서 SpreadsheetDate 클래스로 이동한다.

[G7] 추상 클래스 생성

  • DayDate 는 어디까지나 추상 클래스로, 구체적인 구현 정보를 포함할 필요가 없다.
    • 최소 년도와 최대 년도를 지정하는 변수 두 개도 SpreadsheetDate 클래스로 분리하도록 한다.
    • 그런데, 다른 클래스(RelativeDayOfWeekRule )도 두 변수를 사용한다.
    • 추상 클래스 사용자가 구현 정보를 알아야 한다는 딜레마가 생긴다.
  • DayDate 자체를 훼손하지 않으면서 구현 정보를 전달할 방법이 필요하다.
    • 일반적으로 파생 클래스의 인스턴스로부터 구현 정보를 가져오는데, 코드 상 DayDate 인스턴스를 생성하는 createInstance 메서드는 SpreadsheetDate 인스턴스를 생성하고 있다.
  • 일반적으로 기반 클래스는 파생 클래스를 몰라야 바람직하다.
    • Abstract Factory 패턴을 적용해 DayDateFactory 추상 클래스를 만들었다.
    • DayDateFactoryDayDate 인스턴스를 생성하며 구현 관련 질문에도 답한다.
    • 추상 메서드로 위임하는 정적 메서드는 SINGLETON , DECORATOR , ABSTRACT FACTORY 패턴 조합을 사용한다.

인수와 변수 선언에서 final 키워드 제거

  • 로버트 시몬스는 “코드 전체에 final을 사용하라”고 강력히 권장한다.
  • 하지만 서적의 저자는 final 키워드는 final 상수 등 몇 군데를 제하면 별다른 가치가 없으면 코드만 복잡하게 만든다며 제거했다.

플래그 인수 제거

  • 일반적으로 메서드 인수로 플래그는 바람직하지 못하다.
    • 한 메서드가 다른 메서드를 호출하며 플래그를 넘기는 경우
  • 띠라서 메서드 이름을 변경하고, 단순화하고, 다른 코드로 옮겨서 플래그 인수를 제거한다.

오해의 소지 제거

  • 오해의 소지가 있는 메서드의 이름을 원래 의도를 잘 반영하는 이름으로 변경한다.

리팩터링 도구 사용

  • IDE에서 제공하는 리팩터링 도구를 사용해 메서드를 다른 코드(enum)로 옮긴 후 이름을 변경한다.
    • 그런 다음, 정적 메서드를 인스턴스 메서드로 고쳤다.
  • 테스트를 돌려 모든 테스트를 통과하는 것을 확인한다.
  • 메서드를 삭제하여 테스트가 실패하는 것을 확인한다.
  • 테스트 케이스에서 enum(코드 옮긴 곳)을 사용하도록 변경한 후 테스트를 돌려 통과하는지 확인한다.

사용하지 않는 코드와 테스트 제거

  • 테스트 케이스에서 유일하게 사용하는 메서드는 제거한다.
    • 테스트 케이스도 함께 삭제한다.

지금까지 한 작업 정리

  1. [C2] 처음에 나오는 주석은 너무 오래되었다. 그래서 간단하게 고치고 개선했다.
  2. [G12] enum을 모두 독자적인 소스 파일로 옮겼다.
  3. [G6] 정적 변수(dateFormatSymbols)와 정적 메서드(getMonthNames, isLeapYear, lastDayOfMonth)를 DateUtil이라는 새 클래스로 옮겼다.
  4. [G24] 일부 추상 메서드를 DayDate 클래스로 끌어올렸다.
  5. [N1] Month.make를 Month.fromInt로 변경했다. 다른 enum도 똑같이 변경하고, 모든 enum에 toInt() 접근자를 생성하고 index 필드를 private으로 정의했다.
  6. [G5] plusYears와 plusMonths에 있던 중복을 새 메서드를 생성하여 없앴다. 그래서 세 메서드가 모두 좀 더 명확해졌다.
  7. [G25] 팔방미인으로 사용하던 숫자 1을 없앴다.
  8. SpreadsheetDate 코드를 살펴보고 알고리즘을 조금 손봤다.
  • DayDate 코드 커버리지는 84.9%로 감소했다.
    • 테스트 코드가 줄어서가 아니라, 클래스 크기가 작아지는 바람에 테스트하지 않는 코드의 비중이 커졌기 때문이다.
    • 테스트 케이스는 DayDate 53개 실행 문 중 45개를 테스트한다.
    • 테스트하지 않은 코드는 너무 사소해 테스트할 필요도 없다.

결과

  • 테스트 커버리지가 증가했다.
  • 버그 몇 개를 고쳤다.
  • 코드 크기가 줄었다.
  • 코드가 명확해졌다.
  • 다음 사람은 우리보다 코드를 좀 더 쉽게 이해하고, 우리보다 좀 더 쉽게 개선할 수 있다.
profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글