테스트와 클래스
- 4.1 TDD 법칙 세 가지
- 4.2 깨끗한 테스트 코드 유지하기
- 4.3 깨끗한 테스트 코드
- 4.4 도메인에 특화된 테스트 언어
- 4.5 이중 표준
- 4.6 테스트 당 assert 하나
- 4.7 F.I.R.S.T.
- 4.8 클래스 체계
- 4.9 클래스는 작아야 한다
- 4.10 단일 책임 원칙(SRP)
- 4.11 응집도
- 4.12 변경하기 쉬운 클래스
- 4.13 변경으로부터 격리
테스트가 있어야 코드를 고칠 수 있고, 클래스가 작아야 코드를 이해할 수 있어. 테스트는 변경의 안전망이고, 클래스 설계는 변경의 방향을 결정하지.
TDD(테스트 주도 개발)의 세 가지 법칙이 있어. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않고, 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성하고, 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성해. 이 세 법칙을 따르면 개발과 테스트가 30초 주기로 묶이거든. 근데 이렇게 하면 테스트 코드가 방대해져. 방대한 테스트 코드가 엉망이면? 관리가 안 돼.
저자가 겪은 실제 사례가 있어. 어떤 팀이 "테스트 코드니까 품질은 상관없다"는 태도로 테스트를 짰거든. 변수 이름 대충 짓고, 테스트 함수도 길고, 중복도 많고. 처음에는 테스트가 있으니까 안심했는데, 실제 코드가 진화하면서 테스트 코드도 따라서 수정해야 했어. 테스트 코드가 엉망이니까 수정 비용이 갈수록 늘어나고, 결국 팀은 테스트를 전부 폐기했지. 테스트가 없으니 코드 변경이 무서워지고, 버그가 쌓이고, 코드가 썩기 시작했어. 교훈은 명확해 — "테스트 코드는 실제 코드 못지않게 깨끗하게 짜야 한다". 테스트가 있으면 실제 코드를 유연하게 만들 수 있거든. 변경이 두렵지 않고, 아키텍처를 개선할 수 있고, 설계를 고칠 수 있어.
깨끗한 테스트 코드에서 가장 중요한 건 "가독성"이야. 테스트 코드는 "BUILD-OPERATE-CHECK" 패턴을 따르면 좋아. 테스트 자료를 만들고(Build), 조작하고(Operate), 결과가 올바른지 확인해(Check). 잡다한 세부사항은 빼고, 테스트의 본질만 드러내야 해. 테스트를 작성할 때 "도메인에 특화된 언어(DSL)"를 만들어 쓰면 좋은데, 시스템 조작 API를 직접 쓰는 게 아니라 테스트 전용 함수와 유틸리티를 구현해서 테스트 코드를 짜기 편하고 읽기 쉽게 만드는 거야.
테스트 코드에도 깨끗한 코드의 원칙이 적용되지만, 실제 코드와 같은 기준은 아니야. 실제 환경에서는 메모리나 CPU 효율이 중요하지만, 테스트 환경은 자원이 제한적이지 않거든. 가독성이 우선이야. 저자가 "이중 표준"이라고 부르는 건 이거야 — 실제 환경에서는 절대 안 되지만, 테스트 환경에서는 괜찮은 방식이 있다는 것.
"테스트 함수마다 assert 문이 하나만 있어야 한다"는 규칙이 있는데, 더 나은 규칙은 "테스트 함수마다 개념 하나만 테스트하라"야. 여러 개념을 한 테스트 함수에 몰아넣으면 안 돼. 개념 하나에 assert가 여러 개 필요하면 괜찮아. 하지만 한 테스트에 개념이 여러 개면 쪼개야 해.
깨끗한 테스트의 다섯 가지 규칙, **F.I.R.S.T.**도 기억해. Fast(빠르게) — 느리면 자주 안 돌리고, 자주 안 돌리면 문제를 일찍 못 잡아. Independent(독립적으로) — 한 테스트가 다음 테스트 환경을 준비하면 안 돼. Repeatable(반복 가능하게) — 어떤 환경에서든 반복 가능해야 해. Self-Validating(자가 검증하는) — 결과는 성공 아니면 실패야. 로그 파일을 수작업으로 비교해야 한다면 그건 자가 검증이 아니야. Timely(적시에) — 테스트는 실제 코드를 구현하기 직전에 작성해. 실제 코드를 먼저 짜면, 테스트하기 어렵게 설계될 수 있거든.
테스트가 안전망을 제공하듯, 잘 설계된 클래스는 변경의 방향을 잡아줘. 자바 관례에 따르면 클래스 내부 순서는 정적 공개 상수, 정적 비공개 변수, 비공개 인스턴스 변수, 공개 함수, 비공개 함수(자신을 호출하는 공개 함수 직후에 배치) 순이야. 추상화 단계가 순차적으로 내려가도록 짜는 거지. 캡슐화 — 변수와 유틸리티 함수는 가능한 숨겨. 테스트를 위해 protected로 풀거나 패키지 전체 공개로 하는 건 최후의 수단이야.
함수는 물리적 행 수로 "작다"를 측정했잖아. 클래스는 "책임의 수"로 측정해. 클래스 이름이 간결하지 않다면 책임이 너무 많다는 신호야. 클래스 설명에 "if", "and", "or", "but"을 사용하지 않고 25단어 내외로 가능해야 해. SuperDashboard라는 클래스가 마지막으로 포커스를 가진 컴포넌트에 접근하는 메서드 그리고 버전과 빌드 번호를 추적하는 메커니즘을 제공한다면 — "그리고"가 나오니까 책임이 두 개야. 메서드가 적어도 책임이 많을 수 있어.
"클래스는 변경할 이유가 하나여야 한다" — 이게 **SRP(단일 책임 원칙)**의 정의야. SuperDashboard가 버전 정보도 관리하고 UI 컴포넌트도 관리하면, 버전 정보가 바뀔 때도 수정해야 하고 UI가 바뀔 때도 수정해야 하거든. 버전 정보를 Version이라는 별도 클래스로 뽑으면 각각 독립적으로 변경할 수 있어. SRP는 이해하기 쉬운 개념인데, 가장 많이 무시당하는 원칙이기도 해. "돌아가는 소프트웨어"에 초점을 맞추면 "깨끗하고 체계적인 소프트웨어"라는 다음 관심사로 넘어가지 않거든. 프로그램이 돌아가면 "끝났다"고 생각하고 다음 기능으로 넘어가버리는 거야. 그리고 작은 클래스 여러 개보다 큰 클래스 하나가 더 이해하기 쉽다는 착각도 있어. 큰 서랍 하나에 다 넣는 것 vs 작은 서랍 여러 개에 분류하는 것. 후자가 뭔가 찾을 때 훨씬 빨라.
클래스는 인스턴스 변수 수가 적어야 해. 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 하고, 모든 인스턴스 변수를 메서드마다 사용하면 "응집도가 높다"고 해. 함수를 작게 만들고 매개변수 목록을 짧게 유지하다 보면, 인스턴스 변수가 늘어나는 경우가 있어. 이때 몇몇 메서드만 사용하는 인스턴스 변수가 생기면 — 그건 "클래스를 쪼개야 한다는 신호"야. 큰 함수를 작은 함수 여러 개로 쪼개다 보면, 자연스럽게 클래스도 쪼개지거든. 응집도가 높은 클래스가 탄생하는 거야.
대다수 시스템은 지속적으로 변경돼. 저자가 SQL 문자열을 만드는 Sql 클래스를 예로 들어. select, insert, update 등을 지원하는데, update 문을 수정하려면 Sql 클래스 전체를 건드려야 해. 해결은 Sql을 추상 클래스로 만들고, SelectSql, InsertSql, UpdateSql 등 각각의 파생 클래스를 만드는 거야. 이러면 OCP(개방-폐쇄 원칙) — 확장에 열려 있고 수정에 닫혀 있는 구조가 돼.
요구사항은 변하고, 코드도 변해. 구체적인 클래스에 의존하면 구현이 바뀔 때 위험하거든. 그래서 "인터페이스와 추상 클래스"를 사용해 구현의 영향을 격리해. Portfolio 클래스가 외부 TokyoStockExchange API를 직접 호출해서 주가를 얻는다면, 테스트가 어려워. StockExchange라는 인터페이스를 만들고, 테스트에서는 고정된 주가를 반환하는 가짜 구현을 넣으면 테스트가 안정적이야. 이게 "DIP(의존성 역전 원칙)"야. 구체적인 것이 아니라 추상적인 것에 의존해. 시스템의 결합도가 낮아지면 유연성과 재사용성이 높아지고, 테스트도 쉬워져.
정리
4장 읽고 기억할 거 세 가지:
- 테스트 코드는 실제 코드만큼 중요하다. 테스트가 지저분하면 유지보수가 안 되고, 결국 버려지고, 실제 코드도 썩는다. FIRST 원칙을 지켜라
- 클래스는 책임이 하나여야 한다(SRP). 변경할 이유가 두 가지 이상이면 쪼개라. 응집도가 낮아지면 클래스를 나눌 신호다
- 구체적인 것이 아니라 추상에 의존하라(DIP). 인터페이스에 의존하면 변경에 유연하고 테스트도 쉬워진다. OCP와 함께 변경하기 쉬운 구조를 만든다