Hayden's Archive
클린코드(Clean Code) 9, 10장 - 로버트 C. 마틴 본문
book.naver.com/bookdb/book_detail.nhn?bid=7390287
9장 단위테스트
- TDD 법칙 세 가지
- 첫째, 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다
- 둘째, 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다
- 셋째, 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다
- 위 세 가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶임
- 테스트 코드와 실제 코드가 함께 나오면서 테스트 코드가 실제 코드보다 불과 몇초 전에 나옴
- 깨끗한 테스트 코드 유지하기
- 실제 코드가 진화하면 테스트 코드도 변해야 함
- 새 버전을 출시할 때마다 팀이 테이스 케이스를 유지하고 보수하는 비용도 늘어남
- 하지만 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없고 결함율이 높아짐
- 테스트 코드는 실제 코드 못지 않게 중요하다
- 따라서 실제 코드 못지 않게 깨끗해야 한다
- 테스트는 유연성, 유지보수성, 재사용성을 제공한다
- 단위 테스트는 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목
- 테스트 케이스가 없다면 모든 변경이 잠정적인 버그임
- 테스트 커버리지가 높을수록 변경이 쉬워짐
- 깨끗한 테스트 코드
- 깨끗한 테스트를 만들기 위해 필요한 것 : 가독성
- 가독성은 실제 코드보다 테스트 코드에 더더욱 중요
- 가독성을 높이기 위해 명료성, 단순성, 풍부한 표현력 필요
- 최소의 표현으로 많은 것을 나타내야 함
- BUILD-OPERATE-CHECK 패턴
- BUILD : 테스트 자료를 만든다
- OPERATE : 테스트 자료를 조작한다
- CHECK : 조작한 결과가 올바른지 확인한다
- 테스트 코드는 잡다하고 세세한 코드는 거의 다 없애고, 본론에 도입해 진짜 필요한 자료 유형과 함수만 사용함
- 코드를 읽는 사람은 코드가 수행하는 기능을 재빨리 이해함
- 도메인에 특화된 테스트 언어
- 도메인 특화 언어(DSL, Domain Specific Language)로 테스트 코드를 구현함
- 흔히 쓰는 시스템 조작 API를 사용하는 대신 API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용
- 이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 됨
- 도메인 특화 언어(DSL, Domain Specific Language)로 테스트 코드를 구현함
- 이중 표준
- 테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 다름
- 단순하고 간결하고 표현력이 풍부해야 하지만 실제 코드만큼 효율적일 필요는 없음
- 실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문
- 실제 환경에서는 절대 안 되지만 테스트 환경에서는 전혀 문제없는 방식이 있음
- 코드의 깨끗함과는 철저히 무관한, 메모리나 CPU 효율과 관련 있는 경우
- 테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 다름
- 깨끗한 테스트를 만들기 위해 필요한 것 : 가독성
- 테스트당 assert 하나
- assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠름
- 하나의 테스트에서 하나의 assert 문을 사용하기 위해 테스트를 쪼개는 과정에서 given-when-then이라는 관례 사용
- 테스트 코드를 읽기 쉬워지지만 given-when에서 중복되는 코드가 많아지게 됨
- 해결법 1) TEMPLATE METHOD 패턴을 사용해서 중복 제거(given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 둔다)
- 해결법 2) 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣음
- 하지만 이러한 해결법은 배보다 배꼽이 커서 이럴 경우에는 assert 문을 여럿 사용하는 편이 나을 수도 있음
- 하지만 assert 문의 개수는 최대한 줄이는 게 좋음
- 테스트당 개념 하나
- 테스트 함수는 개념 하나만 테스트한다
- 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피함
- F.I.R.S.T.
- Fast(빠르게)
- 테스트는 빠르게 자주 돌아야 함
- Independent(독립적으로)
- 각 테스트는 서로 의존하면 안 됨 (한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안됨)
- 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 함
- 그래야 원인을 진단하기 쉽고, 결함을 찾기 편함
- Repeatable(반복가능하게)
- 테스트는 어떤 환경에서도 반복 가능해야 함
- 실제 환경, QA 환경, 네트워크가 연결되지 않은 노트북 환경 등
- 테스트는 어떤 환경에서도 반복 가능해야 함
- Self-Validating(자가검증하는)
- 테스트는 부울 값으로 결과를 내야 함(성공 아니면 실패)
- 통과 여부를 알기 위해 로그 파일을 읽게 만들거나 텍스트 파일 두 개를 수작업으로 비교하게 만들어서는 안 됨
- 테스트가 스스로 성공과 실패를 가늠해야 함
- Timely(적시에)
- 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현함
- 그렇지 않으면, 어떤 실제 코드는 테스트하기 너무 어렵다고 판명날 수 있고, 테스트가 불가능하도록 실제 코드를 설계하게 될 수도 있음
- 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현함
- Fast(빠르게)
10장 클래스
- 클래스 체계
- 변수 목록
- 정적static 공개public 상수가 있다면 맨 처음에 나옴
- 다음으로 정적 비공개private 변수가 나옴
- 이어서 비공개 인스턴스 변수가 나옴
- (공개 변수가 필요한 경우는 거의 없음)
- 변수 목록 다음에는 공개 함수가 나오고, 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣음
- 추상화 단계가 순차적으로 내려가면서, 프로그램은 신문 기사처럼 읽힘
- 캡슐화
- 변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫다
- 캡슐화를 풀어주는 결정은 언제나 최후의 수단
- 때로는 같은 패키지 안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면, 그 함수나 변수를 protected로 선언하거나 패키지 전체로 공개하기도 함
- 변수 목록
- 클래스는 작아야 한다!
- 함수는 물리적인 행 수로 크기를 측정했다면, 클래스는 클래스가 맡은 책임을 센다
- 클래스 이름은 해당 클래스 책임을 기술해야 함
- 작명은 클래스 크기를 줄이는 첫 번째 관문
- 클래스 이름이 모호하다면(예: Processor, Manager, Super) 클래스 책임이 너무 많은 것
- 클래스 설명은 만일(if), 그리고(and), 하며(or), 하지만(but)을 사용하지 않고서 25단어 내외로 가능해야 함
- 단일 책임 원칙(SRP, Single Responsibility Principle)
- 클래스나 모듈을 변경할 이유(책임)가 단 하나뿐이어야 한다는 원칙
- 만능 클래스를 단일 책임 클래스 여럿으로 분리해야 함
- 큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다
- 응집도
- 클래스는 인스턴스 변수 수가 작아야 함
- 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 함
- 일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다
- 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다
- 응집도가 높다 = 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다
- '함수를 작게, 매개변수 목록을 짧게' 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아짐
- 새로운 클래스로 쪼개야 한다는 신호
- 응집도를 유지하면 작은 클래스 여럿이 나온다
- 큰 함수를 작은 함수 여럿으로 쪼개는 과정 예시
- 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다
- 변수 네 개를 새 함수에 인수로 옮기지 말고, 대신 네 변수를 클래스 인스턴스 변수로 승격한다
- 그런데 이렇게 하면 몇몇 함수만 사용하는 인스턴스 변수가 점점 늘어나서 클래스가 응집력을 잃는다
- 그러므로 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리한다
- 큰 함수를 작은 함수 여럿으로 쪼개는 과정 예시
- 변경하기 쉬운 클래스
- 새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직함
- 변경으로부터 격리
- 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리
- 상세한 구현에 의존하는 코드는 테스트가 어려움
- 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아짐
- 결합도가 낮다 = 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다
- 결합도를 최소로 줄이면 자연스럽게 DIP(Dependency Inversion Principle)를 따르는 클래스가 나옴
- 본질적으로 DIP는 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙