이 장의 주제
부록의 코드와 함께 보면서 설명하는 챕터다.
책의 흐름을 따라가며 리팩토링 시 신경써야 할 것 같은 부분들만 일부 정리해봤다.
- JCommon 라이브러리에서
org.jfree.date
패키지의 SerialDate라는 클래스를 탐험한다.
- SerialDate는 우수한 코드이지만, 이 장에서는 코드를 낱낱이 까발려 본다.
- 여기서 수행하는 분석은 전문가 입장에서 수행하는 검토, 그 이상도 그 이하도 아니다.
- 우리 모두가 편안하게 여기고 남이 내게 해준다면 감사히 반겨야 할 활동이다.
SerialDate 클래스
- 날짜를 표현하는 자바 클래스
- 자바에서 제공하는
java.util.Date
, java.util.Calendar
등과 같은 클래스의 불편함을 없애고자 구현되었다.
- 구현한 사람인 데이비트는 javadoc 주석에 이유를 잘 설명해 두었다.
개선
첫째, 돌려보자
테스트 실행
SerialDateTests
라는 클래스는 단위 테스트 케이스 몇 개를 포함한다.
- 돌려보면 실패하는 데스트 케이스는 없지만, 테스트 케이스를 훑어보면 모든 경우를 점검하지 않는다는 사실이 드러난다.
- 예를 들어,
MonthCodeToQuarter
메서드를 전혀 호출(실행)하지 않는다.
테스트 케이스 조사
- 클로버(Clover)를 이용해 단위 테스트가 실행하는 코드와 실행하지 않는 코드를 조사했다.
- 클로버에 따르면, SerialDate에서 실행 가능한 문장 185개 중 단위 테스트가 실행하는 문장은 91개에 불과했다.
테스트 케이스 구현
- 클래스를 철저히 이해하고 리팩터링하려면 훨씬 높은 테스트 커버리지가 필요하다.
- 따라서 서적에서는 독자적으로 단위 케이스를 구현했다.
- 실패한 테스트는 주석으로 임시 처리하고, 리팩토링하면서 모든 테스트 케이스(실패한 테스트도) 통과하게 코드를 손 본다.
- 모든 테스트 케이스를 통과하도록 코드를 수정하고 나면 다음 단계로 넘어간다.
둘째, 고쳐보자
리팩토링
- 코드를 고칠 때마다 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_BOUND
와 SERIAL_UPPER_BOUND
라는 상수에 있다.
- 1899년 12월 30일을 기준으로 경과한 날짜 수를 사용한다.
- 서적에서는 두 가지 이유를 들어 클래스 명을 변경했다.
- [N1] ‘일련번호’보다 상대 오프셋(relative offset)이 더 정확하다.
- 일련번호라는 용어는 날짜보다 제품 식별 번호로 더 적합하다.
- [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;
public static final int LASTEST_DATE_ORDINAL = 2958;
- 이 사안은
SpreadsheetDate
의 구현과 관련되지, DayDate
와 아무런 상관이 없다.
- 따라서
DayDate
클래스에서 SpreadsheetDate
클래스로 이동한다.
[G7] 추상 클래스 생성
DayDate
는 어디까지나 추상 클래스로, 구체적인 구현 정보를 포함할 필요가 없다.
- 최소 년도와 최대 년도를 지정하는 변수 두 개도
SpreadsheetDate
클래스로 분리하도록 한다.
- 그런데, 다른 클래스(
RelativeDayOfWeekRule
)도 두 변수를 사용한다.
- 추상 클래스 사용자가 구현 정보를 알아야 한다는 딜레마가 생긴다.
DayDate
자체를 훼손하지 않으면서 구현 정보를 전달할 방법이 필요하다.
- 일반적으로 파생 클래스의 인스턴스로부터 구현 정보를 가져오는데, 코드 상
DayDate
인스턴스를 생성하는 createInstance
메서드는 SpreadsheetDate
인스턴스를 생성하고 있다.
- 일반적으로 기반 클래스는 파생 클래스를 몰라야 바람직하다.
- Abstract Factory 패턴을 적용해
DayDateFactory
추상 클래스를 만들었다.
DayDateFactory
는 DayDate
인스턴스를 생성하며 구현 관련 질문에도 답한다.
- 추상 메서드로 위임하는 정적 메서드는
SINGLETON
, DECORATOR
, ABSTRACT FACTORY
패턴 조합을 사용한다.
인수와 변수 선언에서 final 키워드 제거
- 로버트 시몬스는 “코드 전체에 final을 사용하라”고 강력히 권장한다.
- 하지만 서적의 저자는 final 키워드는 final 상수 등 몇 군데를 제하면 별다른 가치가 없으면 코드만 복잡하게 만든다며 제거했다.
플래그 인수 제거
- 일반적으로 메서드 인수로 플래그는 바람직하지 못하다.
- 한 메서드가 다른 메서드를 호출하며 플래그를 넘기는 경우
- 띠라서 메서드 이름을 변경하고, 단순화하고, 다른 코드로 옮겨서 플래그 인수를 제거한다.
오해의 소지 제거
- 오해의 소지가 있는 메서드의 이름을 원래 의도를 잘 반영하는 이름으로 변경한다.
리팩터링 도구 사용
- IDE에서 제공하는 리팩터링 도구를 사용해 메서드를 다른 코드(enum)로 옮긴 후 이름을 변경한다.
- 그런 다음, 정적 메서드를 인스턴스 메서드로 고쳤다.
- 테스트를 돌려 모든 테스트를 통과하는 것을 확인한다.
- 메서드를 삭제하여 테스트가 실패하는 것을 확인한다.
- 테스트 케이스에서 enum(코드 옮긴 곳)을 사용하도록 변경한 후 테스트를 돌려 통과하는지 확인한다.
사용하지 않는 코드와 테스트 제거
- 테스트 케이스에서 유일하게 사용하는 메서드는 제거한다.
지금까지 한 작업 정리
- [C2] 처음에 나오는 주석은 너무 오래되었다. 그래서 간단하게 고치고 개선했다.
- [G12] enum을 모두 독자적인 소스 파일로 옮겼다.
- [G6] 정적 변수(dateFormatSymbols)와 정적 메서드(getMonthNames, isLeapYear, lastDayOfMonth)를 DateUtil이라는 새 클래스로 옮겼다.
- [G24] 일부 추상 메서드를 DayDate 클래스로 끌어올렸다.
- [N1] Month.make를 Month.fromInt로 변경했다. 다른 enum도 똑같이 변경하고, 모든 enum에 toInt() 접근자를 생성하고 index 필드를 private으로 정의했다.
- [G5] plusYears와 plusMonths에 있던 중복을 새 메서드를 생성하여 없앴다. 그래서 세 메서드가 모두 좀 더 명확해졌다.
- [G25] 팔방미인으로 사용하던 숫자 1을 없앴다.
- SpreadsheetDate 코드를 살펴보고 알고리즘을 조금 손봤다.
- DayDate 코드 커버리지는 84.9%로 감소했다.
- 테스트 코드가 줄어서가 아니라, 클래스 크기가 작아지는 바람에 테스트하지 않는 코드의 비중이 커졌기 때문이다.
- 테스트 케이스는 DayDate 53개 실행 문 중 45개를 테스트한다.
- 테스트하지 않은 코드는 너무 사소해 테스트할 필요도 없다.
결과
- 테스트 커버리지가 증가했다.
- 버그 몇 개를 고쳤다.
- 코드 크기가 줄었다.
- 코드가 명확해졌다.
- 다음 사람은 우리보다 코드를 좀 더 쉽게 이해하고, 우리보다 좀 더 쉽게 개선할 수 있다.