9. 단위 테스트
TDD(Test Driven Development) 세가지 법칙
- 단위 테스트를 작성한 후에 실제 코드를 작성한다.
- 실행이 실패하는 정도로만 작성한다.
- 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
테스트 코드의 중요성
- 테스트 코드가 복잡하게 되면 실제 코드보다 테스트 케이스를 추가하는 시간이 오래 걸리고, 관리하기가 어려워진다.
- 결국 결함율이 높아진다. 결함율이 높아지면 개발자는 변경을 주저한다.
테스트는 유연성, 유지보수성, 재사용성을 제공한다.
FIRST
- Fast
- 테스트는 빨라야 한다.
- 자주 돌려서 초반에 결함을 빨리 찾아야 한다.
- Independent
- Repeatable
- 어떤 환경에서도 반복 가능해야 한다.
- 환경때문에 테스트 코드를 못돌리는 경우는 없어야 한다.
- Self-Validating
- 성공 아니면 실패를 반환한다.
- 통과 여부를 확인하기 위해 로그 파일을 읽어서는 안된다.
- Timely
- 테스트 코드를 작성한 이후에 실제 코드를 작성한다.
- 반대로 작성하게 되면, 실제 코드가 테스트하기 어려워질 수 있다.
10. 클래스
클래스 체계
- 변수
- 함수
- 각각의 변수와 함수는 공개에서 비공개 순으로 나열한다.
캡슐화
- 가능한 숨겨야 한다.
- 그렇지만 반드시 숨겨야 한다는 법칙도 없다.
- 최대한 숨겨보고, 안될 때에는 캡슐화를 푼다.
클래스 크기
- 클래스는 무조건 작아야 한다.
- 클래스가 맡은 책임을 센다.
- 책임이 많다면? 클래스를 나눠야 한다.
단일 책임 원칙(Single Responsibility Principle)
응집도
- 응집도가 높다는 말은,
- 클래스에 속한 변수와 메서드가 서로 의존하며 논리적인 단위로 묶인다는 뜻이다.
- 응집도가 높을 수록 좋다.
클래스 분리
- 클래스를 작성한 후, 반드시 분리한다.
- 극도로 단순하게 쪼개고 쪼갠다.
구체적인 클래스, 추상 클래스
- 구체적인 클래스: 상세한 구현(코드)를 포함한다.
- 추상 클래스: 개념만 포함한다.
11. 시스템
복잡성
복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.
도시
- 도시는 혼자서 관리하지 못한다.
- 각 분야마다 관리하는 팀이 있다.
- 적절한 추상화와 모듈화가 필요하다.
시스템의 제작과 사용을 분리해라.
- 제작과 사용은 다르다.
- 시스템의 제작: 애플리케이션 객체를 제작하고 의존성을 서로 연결하는 준비 과정
- 시스템의 사용: 런타임 로직
Lazy
- lazy하게 선언하는 것은, 작게나마 단일 책임 원칙을 깬다.
- 테스트도 문제다. 테스트를 하기 전에 테스트 전용 객체를 할당해야만 하기 때문이다.
의존성 주입
- 새로운 객체는 넘겨받은 책임만 지므로, 단일 책임 원칙을 지키게 된다.
- 클래스를 완전히 수동적으로 설계한다.
- 필요한 객체를 만든 후에 클래스 내부의 setter를 통해 주입한다.
확장
- 초기의 작은 클래스에 이것 저것 붙으면서 점점 커졌다.
- 성장에는 고통이 따르고, 안되는 부분도 생긴다.
- 처음에 잘못한게 아니고, 당연한 것이다.
- 반복적이고 점진적인 애자일 방법으로, 계속 수정해간면 된다.
결론
- 시스템은 깨끗해야 한다.
- 실제로 돌아가는 가장 단순한 수단을 사용해야 한다.
12. 창발성
단순한 설계 4가지 법칙
- 모든 테스트를 실행한다.
- 중복을 없앤다.
- 프로그래머 의도를 표현한다.
- 클래스와 메서드 수를 최소로 줄인다.
1. 모든 테스트를 실행한다.
- 테스트를 철저히 거친 시스템은 검증이 가능한 시스템이다.
- 설계 품질은 당연하게 올라간다.
- 크기가 작고 책임이 하나인 클래스가 나온다.
- 결합도가 높으면 테스트 케이스를 작성하기 어려우므로 의존성 주입, 인터페이스 등과 같은 방법으로 결합도를 낮춘다.
2~4. 리팩터링
- 테스트 케이스를 작성한 후에, 코드와 클래스를 정리한다.
- 코드를 작성하다가, 설계를 보며 확인한다.
- 테스트 케이스가 있으므로, 기존 시스템이 깨질지 걱정하지 않아도 된다.
중복을 없애라
- 중복 == 추가 작업, 추가 위험, 불필요한 복잡도
- 시스템의 복잡도를 낮춘다.
- 소규모 재사용을 하게 되며, 이후 대규모 재사용이 가능하게 한다.
표현하라
- 자신이 이해하는 코드를 짜기는 쉽다.
- 하지만 남이 이해할 코드를 짜기는 쉽지 않다.
- 그러기 위해서는, 우선 의도를 분명히 해야 한다.
- 명백하게 짜야 한다.
- 네이밍을 고민한다.
- 함수와 클래스의 크기를 줄인다.
- 표준 명칭을 사용한다.
- 단위 테스트 케이스를 꼼꼼하게 작성한다.
13. 동시성
동시성
- 동시성은 결합(coupling)을 없애는 전략이다.
- 무엇과 언제를 분리하는 전략이다.
동시성 방어 원칙
- 단일 책임 원칙
- 따름 정리
- 자료 범위를 제한한다.
- 코드 내 임계영역을 보호한다.
- 임계영역의 수를 줄이는 것도 중요하다.
- 자료 사본
- 공유 자료를 줄이기 위해 애초부터 공유를 하지 않는 방법도 있다.
- 복사해 사용한다.
- 스레드는 가능한 독립적으로 구현한다.
- 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
14. 점진적인 개선
깨끗한 코드 짜기
- 일단 돌아만 가는, 지저분한 코드를 작성한다.
- 이후 깨끗하게 정리한다.
- 네이밍, 인수의 수, 분기의 수 등을 체크하며 개선한다.
- 개선한 후 모든 테스트를 통과하는지 살핀다.
나쁜 코드
- 나쁜 코드를 깨끗한 코드로 개선하기는 어렵다.
- 오래된 의존성을 찾아내 깨려면 상당한 시간과 인내심이 필요하다.
- 점점 무게가 늘어나 팀의 발목을 잡는다.
깨끗한 코드
- 반면 처음부터 깨끗한 코드를 작성하고 유지하는 것은 상대적으로 쉽다.
15. JUnit 들여다보기
JUnit
- iOS에서 UnitTest와 비슷한 개념이다.
- 작은 모듈단위로 테스트한다.
- 모듈에 필요한 기능이 좀 더 드러난다.
보이스카우트 규칙
- 처음 왔을 때보다 더 깨끗하게 해놓고 떠나야 한다.
- 코드를 추가하거나 수정한 경우, 이전 코드보다 더 깨끗하게 해놓는다.
모듈화 팁
- 한 메서드가 여러 기능을 수행하고 있지는 않은지 체크한다.
- 조건문이 너무 길다면, 캡슐화한다.
- 조건문을 메서드로 뽑아내 적절한 이름을 붙인다.
- 인수를 고려해 네이밍을 다듬는다
16. SerialDate 리팩터링
개요
첫째, 돌려보자
- SerialDateTests라는 클래스에서 모든 경우를 테스트하지 않았다.
- 클래스를 철저히 이해하고 리팩토링하기 위해 단위 테스트를 구현한다.
둘째, 고쳐보자
Serial 네이밍
- Serial이라는 네이밍을 고민한다.
- Serial은 구현할 때 일련번호(serial number)를 사용하기에 나온 단어이다.
- SerialDate가 아닌, Date가 더 바람직하지만, Date는 너무 많이 쓰인다.
- OrdinalDate나 DayDate로 변경했다.
변수 선언
- 접근 제어자를 설정한다.
- 가장 제한적인 접근제어자를 사용할 수 있다면 사용해 선언한다.
함수
추상화
17. 냄새와 휴리스틱
코드의 가장 나쁜 냄새
- 부적절한 주석
- 쓸모 없는 주석
- 가독성만 떨어지게 만든다.
- 쓸모 없어지면 바로 삭제한다.
- 애초에 쓸모 없어질 주석은 추가하지 않는다.
- 중복된 주석
- 성의 없는 주석
- 주석 처리된 코드
- 주석 처리된 코드는 바로 삭제한다.
- 어차피 이전 버전에서 해당 코드를 기억하고 있다.
- 여러 단계로 빌드한다?
- 빌드는 한 단계로 끝나야 한다.
- 빌드를 위해 여기저기 뒤적일 필요가 없어야 한다.
- 과정이 많게 되면, 작성 중에 지속적인 빌드가 귀찮게 된다.
- 여러 단계로 테스트한다?
- 모든 단위 테스트는 한 명령으로 돌려야 한다.
- 작성 중에 지속적으로 테스트할 수 있도록 한다.
- 너무 많은 인수
- 함수 인수는 작을 수록 좋고, 없으면 더 좋다.
- 인수 개수가 넷 이상은 무조건 피한다.
- 출력 인수
- 플래그 인수
- Bool 타입 인수는 함수가 여러 기능을 수행한다는 명백한 근거다.
- 플래그 인수는 피하고, 함수를 여러 개로 쪼갠다.
- 죽은 함수
- 한 소스 파일에 여러 언어를 사용한다?
- 혼란스럽고 조잡하다.
- 소스 파일에서 언어 수와 범위를 최대한 줄인다.
- 당연한 동작을 구현하지 않는다?
- 최소 놀람의 법칙에 의거해, 함수느 클래스는 다른 프로그래머가 당연하게 여길 만한 동작이나 기능을 제공해야 한다.
- 경계를 올바로 처리하지 않는다.
- 모둔 경계 조건을 테스트하는 테스트 코드를 작성한다.
- 중복
- 중복을 피한다.
- 중복을 발견하면 없애려고 노력한다.
- 이름과 기능이 일치하는 함수
-
date에 5일을 더하는 건지, 5개월을 더하는 건지 알 수가 없다.
let newDate = date.add(5)
-
date에 5일을 더하는 거라면 다음과 같이 수정할 수 있다.
let newDate = date.daysLater(5)
- 경계 조건을 캡슐화하라