16장 SerialDate 리팩터링
1. 📌 핵심 개념 정리
✅ 요약하기
1. 첫째, 돌려보자
SerialDateTest
또는SerialDateTests
라는 클래스는 단위 테스트 케이스를 몇 개 포함하며, 실행 시 실패하는 케이스는 없지만 모든 경우를 점검하지 않는다는 사실을 알 수 있습니다.- 저자는
SerialDateTest.java
에 있는 모든 테스트 케이스가 통과할 수 있게 만들었습니다. Clover
라는Code Coverage
분석 도구를 이용하여 실행 코드와 실행하지 않는 코드를 확인한 결과,SerialDateTests
는 **50%**의 경우만 테스트함을 확인할 수 있었습니다. 구체적으로는 185개 중 **50%**만 실행하고 있었습니다.MonthCodeToQuarter
메서드는 전혀 호출되지 않았습니다.- 클래스를 철저히 이해하고 리팩터링하려면 훨씬 높은 테스트 커버리지가 필요했습니다.
2. 둘째, 고쳐보자
- 쓸모없는 주석은 제거합니다.
변경 이력, 작성자 등은 Git 등의 소스 코드 관리 도구로 대체할 수 있으므로 삭제합니다. 특히 변경 이력 주석은 1960년대 방식으로, 현재는 불필요합니다. - 법적 정보는 유지합니다.
라이선스 및 저작권 관련 주석은 반드시 보존합니다. - 좋은 주석은 유지합니다.
변수의 역사와 의미를 설명하는 주석은 코드 이해에 도움이 되므로 유지합니다. - Javadoc에는 하나의 언어만 사용합니다.
자바, 영어, HTML 등 여러 언어가 혼용되면 오히려 가독성을 해칩니다. 필요 시<pre>
태그로 통일된 형식을 사용합니다. import java.text.*
,import java.util.*
형태로 범용 패키지를 사용해 코드 라인을 줄입니다.- 기능을 명확히 표현합니다.
클래스와 메서드는 이름만 보고도 역할이 파악되도록 작성합니다. - **단일 책임 원칙(SRP)**을 지킵니다.
하나의 메서드는 하나의 역할만 하도록 분리하고, 복잡한 로직은 적절히 나눕니다. - 플래그 인수는 제거하고 서술형 이름으로 분리합니다.
예:getMonths(boolean fullName)
→getMonthNames()
/getMonthShortCodes()
등으로 분리. - 불필요한 상속은 제거합니다.
예:DayDate
가MonthConstants
를 상속하는 것은 적절하지 않으며, 상수는enum
으로 분리합니다. - 사용하지 않는 직렬화 제거
클래스 직렬화 버전이 달라질 경우 문제가 발생하므로, 직렬화를 피하는 것이 안전합니다. - 변수 선언 시
final
키워드를 제거합니다.
실질적인 코드 안정성 향상보다는 복잡성만 높이므로, 전체적으로 제거합니다. - 임시 변수와 설명을 통해 알고리즘 가독성을 높입니다.
- 변수명은 의미가 명확하고 클래스 컨텍스트에 어울리도록 수정합니다.
- 중복된
if
조건은||
연산자로 합칩니다.
불필요한 중복을 줄이고 가독성을 높입니다. - 정확한 의미를 반영한 이름으로 변경합니다.
예:SerialDate
→DayDate
.
Serial
은 제품 일련번호와 관련된 용어이므로 날짜를 표현하기에 적절하지 않습니다. - 중복 메서드는 통합하여 의미를 명확히 합니다.
예:getMonths
가 두 개 존재할 경우, 역할에 따라 메서드를 구분하거나 하나로 통합합니다. - 변수가 특정 클래스에서만 사용된다면 추상 클래스가 아닌 해당 클래스에 위치시킵니다.
변수와 사용 코드 간 거리를 줄이면 가독성이 높아집니다. - 구현 정보를 추상 클래스에 담지 않습니다.
자식 클래스와의 결합도를 낮추기 위해ABSTRACT FACTORY
패턴을 적용합니다. - 정적 메서드는 추상 메서드로 위임하고,
SINGLETON
,DECORATOR
,FACTORY
패턴과 조합하여 구현합니다.
부모 클래스는 자식 클래스를 몰라도 동작해야 합니다.
필요하다면 이 원칙들을 바탕으로 리팩토링 전후 코드 예시도 도와드릴 수 있어요. 특정 클래스나 메서드가 있다면 알려주세요!
3. 결론
- 보이스카우트 규칙을 따랐습니다. 체크아웃한 코드보다 좀 더 깨끗한 코드를 체크인할 수 있게 되었습니다.
- 테스트 커버리지 증가, 버그 사항 개선 또는 수정, 코드 크기 감소, 코드가 명확해짐, 코드의 가독성 증가.
DayDate
코드 커버리지가 감소했지만, 이는 테스트하는 코드가 줄어서가 아니라 클래스 크기가 작아지는 바람에 테스트하지 않는 코드의 비중이 커졌기 때문입니다.- 다음 사람은 좀 더 코드를 쉽게 이해할 수 있게 되었습니다.
- 다음의 코드 개선은 난이도가 조금 더 낮아질 것입니다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
1. 커버리지 도구
- 어려웠던 부분: Clover 같은 도구가 어떤 점에서 도움이 되고, 어디까지 신뢰할 수 있는가?
- 알게된 점:
- [Clover 와 같은 코드 커버리지 도구의 장점]
- 테스트 사각지대 시각화: 테스트되지 않은 코드가 어디인지 하이라이트 색상 등으로 시각적으로 표시되어, 어떤 분기(if), 루프(for), 예외 처리 블록 등이 실행되지 않았는지를 바로 확인할 수 있어 테스트 보완에 유용합니다.
- 커버리지 수치로 측정 가능: 코드 전체의 문장(Statement), 분기(Branch), 메서드, 클래스 커버리지를 %로 제공합니다.
- 리팩토링하거나 기능 추가 시 테스트 범위 감소 여부 감지 용이.
- 자동화된 리포트 생성: HTML, XML, PDF 등의 리포트를 제공해 CI/CD 파이프라인에 통합하거나 팀원들과 공유하기 좋습니다.
- 테스트 품질 개선의 출발점: 어디에 테스트가 부족한지 알려주는 출발점 역할을 하며, 커버리지를 높이는 과정에서 예외 상황이나 엣지 케이스를 놓치지 않도록 도와줍니다.
- [Clover를 무조건 신뢰하면 안 되는 이유]
- "높은 커버리지 ≠ 좋은 테스트": 커버리지가 100%여도,
assert
문 없이 메서드만 호출해도 수치는 올라갑니다. 로직 검증이 빠져있는 형식적인 테스트는 실제 버그를 걸러내지 못합니다. - 복잡한 로직 커버리지는 간과될 수 있음: 하나의
if
문이 여러 조건을 포함할 경우, 모든 조건 조합을 검사하지 않아도 커버리지가 올라갈 수 있습니다. 분기 커버리지(Branch Coverage)가 없거나 약한 경우 놓치는 부분이 생깁니다. - 리팩토링 과정에서 거짓 경고: 동일한 로직 구조이지만 코드 위치나 표현 방식이 바뀌면 새로운 코드처럼 인식되어 커버리지가 낮아질 수 있습니다.
- "높은 커버리지 ≠ 좋은 테스트": 커버리지가 100%여도,
- Clover는 “어디를 테스트해야 할지”를 알려주는 유용한 도구이지만, “테스트가 충분한지”는 사람이 판단해야 합니다.
- 커버리지는 테스트의 양적 지표이고, 품질은 정성적인 코드 리뷰와 시나리오 설계로 보완되어야 합니다.
- Clover는 테스트 보완의 출발점, 품질 진단의 도구, 리팩토링 후 퇴행 방지의 방패막 역할을 합니다.
- [Clover 와 같은 코드 커버리지 도구의 장점]
2. 커버리지
- 어려웠던 부분: 코드 커버리지와 테스트 커버리지의 차이
- 궁금한 점: 각 커버리지의 의미와 수치가 높아야 좋은 것인가?
- 알게된 점:
- 코드 커버리지: 코드의 어느 부분이 테스트 실행 중 실제로 실행되었는지를 측정합니다.
- 테스트 커버리지: 전체 요구사항 또는 테스트 시나리오 중 얼마나 많은 테스트가 수행되었는지 측정합니다.
- 둘은 함께 쓰여야 좋은 품질을 보장 가능합니다. 코드 커버리지가 높아도, 잘못된 시나리오만 테스트했다면 의미 없습니다. 테스트 커버리지가 높아도, 코드가 실제로 실행되지 않았다면 버그 발견 못합니다.
3. 둘째, 고쳐보자
- 어려웠던 부분: 저자는 테스트 커버리지를 높이기 위해서 주석 처리된 테스트 코드까지 모두 통과하도록 코드를 변경했는데 이를 이해하기가 어려웠습니다.
- 궁금한 점: 주석 처리된 테스트 코드 중에는 좋지 않은 예시도 포함되어 있는데 그렇다면 왜 그런 테스트 코드를 수정하지 않고 그대로 뒀을까?
4. final 키워드
- 어려웠던 부분:
final
키워드를 로버트 시몬스는 코드 전체에 사용하라고 강력히 권장하지만, 책의 저자는 모두 없앴다. - 궁금한 점: 어떤 기준인 것인지 이해가 가지 않았습니다.