NextStep '단위테스트'와 'TDD'에 대하여 - 숫자 야구 게임에 이어,
['TDD'와 '리팩토링'에 대하여 - 자동차 경주]를 하며 학습한 내용이다.
📌 목표
- 매일 미션 진행하기
- 한 번에 모두 구현하기보다, 매일 일정한 시간을 투자한다.
- 가진 것을 비우기
- 구체적인 요구사항을 회피하지 않고, 적용하기 전과 후의 코드를 분석한다.
- 내가 가진 것을 비울 때, 가장 많은 것을 배울 수 있다.
- 정답을 찾기 위해 집착하지 않는다.
- 미션을 진행하는데 정답은 없다.
- 정답을 찾으려는 노력이 오히려 학습을 방해한다.
- 즉, 현재 상황에서 최선의 답을 끊임없이 찾으려고 노력한다.
✔️ 학습 목표 - 자동차 경주
- TDD 기반으로 프로그래밍하는 경험
- 메소드 분리 + 클래스를 분리하는 리팩토링 경험
- 점진적으로 리팩토링하는 경험
✔️ 객체지향 연습 원칙
- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다. (어렵다...)
- 규칙 2: else 예약어를 쓰지 않는다. (어렵다2...)
- 규칙 3: 모든 원시값과 문자열을 포장한다.
- 규칙 4: 줄여쓰지 않는다(축약 금지).
- 규칙 5: 일급 콜렉션을 쓴다.
✔️프로그래밍 요구사항 Hint!
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 Google Java Style Guide을 원칙으로 한다.
- 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if 문이 있으면, 들여쓰기는 2이다.
- 💡 힌트: indent depth를 줄이는 좋은 방법은, 함수(또는 메소드)를 분리하면 된다.
- else 예약어를 쓰지 않는다.
- 💡 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.(쏠쏠하게 써먹고 있음ㅎ)
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데, switch/case도 허용하지 않는다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- 모든 원시 값과 문자열을 포장한다.
- 일급 컬렉션을 쓴다.
✔️ Clean Code 가이드 1 - 의미있는 이름
(길어서 따로 뺄까 하다가 걍 여기에 같이 넣음)
1. 의도를 분명히 밝혀라
다음은 잘못된 사용 예다.
int d; // 경과 시간(단위: 날짜 수)
- 이름 d는 아무 의미도 드러나지 않는다.
- 따로 주석이 필요하다면, 의도를 분명히 드러내지 못했다는 소리다.
이걸 어떻게 바꿀까?
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModifecation;
int fileAgeInDays;
- 의도가 드러나는 이름을 사용하면, 코드 이해와 변경이 쉬워진다.
2. 그릇된 정보를 피하라
- 프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 그릇된 단서는 코드 의미를 흐린다.
- 서로 흡사한 이름을 사용하지 않도록 주의한다.
- 유사한 개념은 유사한 표기법을 사용한다.
예를 들어, 여러 계정을 그룹으로 묶을 때,
실제 List가 아니라면, accountList라 명명하지 않는다.
프로그래머에게 List라는 단어는 특수한 의미이다.
계정을 담는 컨테이너가 실제 List가 아니라면, 그릇된 정보를 제공하는 셈이다.
(사실, 실제 컨테이너가 List인 경우라도, 컨테이너 유형을 이름이 넣지 않는 편이 바람직하다.)
그러므로 accountGroup, brunchOfAccounts, 아니면 단순히 Accounts라 명명한다.
3. 의미 있게 구분하라
- 컴파일러나 인터프리터만 통과하려는 생각으로 코드를 구현하는 프로그래머는 스스로 문제를 일으킨다.
- 연속적인 숫자를 덧붙인 이름(a1, a2, ..., aN) 덧붙이거나 불용어(Info, Data, a, an, the)를 추가하는 방식은 적절하지 못하다. 이름이 달라야 한다면 의미도 달라져야 한다.
- Info와 Data는 개념을 구분하지 않은 채 이름만 달리한 경우다.
4. 인터페이스 클래스와 구현 클래스
- 인터페이스 이름은 접두어를 붙이지 않는 편이 낫다고 생각한다.
- IShapeFactory(인터페이스), ShapeFactory(구현 클래스) 구조로 이름을 짓는 것은 좋은 선택은 아니다.
- 오히려 인터페이스를 ShpaeFactory로 이름을 짓고, 구현 클래스의 의도를 드러낼 수 있는 이름을 짓는 것을 추천한다.
5. 클래스 이름
- 클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
- Customer, WikiPage, Account, AddressParser 등이 좋은 예다.
- Manager, Processor, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않는다.
6. 메소드 이름
- 메소드 이름은 동사나 동사구가 적합하다.
- postPayment, deletePage, save 등이 좋은 예다.
- 접근자, 변경자, 조건자는 자바 빈 표준에 따라 값 앞에 get, set, is를 붙인다.
- 생성자를 중복해 정의할 때는 정적 팩터리 메소드를 사용한다. 메소드를 인수를 설명하는 이름을 사용한다.
※ 정적 팩토리 메서드 예제
더보기
1. 생성자 new 방식
public class Car {
private final String name;
private final int oil;
public Car(String name, int oil) {
this.name = name;
this.oil = oil;
}
public Car(String name){
this.name = name;
this.oil = 0;
}
}
public static void main(String[] args) {
Car fullOilCar = new Car("fullOilCar", 10);
Car noOilCar = new Car("noOilCar");
}
2. 정적 팩토리 메서드 방식
public class Car {
private final String name;
private final int oil;
public static Car createCar(String name, int oil) {
return new Car(name, oil);
}
public static Car createNoOilCar(String name) {
return new Car(name, 0);
}
private Car(String name, int oil) {
this.name = name;
this.oil = oil;
}
}
public static void main(String[] args) {
Car fullOilCar = createCar("car1", 10);
Car noOilCar = createNoOilCar("car2");
}
7. 개념 하나에 단어 하나를 사용하라
- 추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.
- 일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.
✔️ Clean Code 가이드 2 - 경계
✔️ 들어가기 전, Test Code란?
- 이 그림에서 확인할 수 있듯이 프로덕션 코드(Production Code) 프로그램 구현을 담당하는 부분으로, 사용자가 실제로 사용하는 소스코드를 의미한다.
- 테스트 코드(Test Code)는 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드이다.
✔️ TDD란?
- TDD = TFD(Test First Development) + 리팩토링
- 💡 TDD란, 프로그래밍 의사 결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다.
- 🚨 TDD의 아이러니 중 하나는 테스트 기술이 아니라는 점이다!
- TDD는 분석 기술이며, 설계 기술이기도 하다.
❓ TDD를 왜 할까?
- 디버깅 시간을 줄여준다.
- 동작하는 문서 역할을 한다.
- 변화에 대한 두려움을 줄여준다.
🚴🏻 TDD 사이클
- 실패하는 테스트를 구현한다.
- 테스트가 성공하도록 프로덕션 코드를 구현한다.
- 프로덕션 코드와 테스트 코드를 리팩토링한다.
- 둘다 해야 한다.
- 테스트코드의 중복이 제거되기 때문.
- 기능이 변경이 되었을 때, 빠르게 개선할 수 있다.
- 둘다 해야 한다.
✔️ TDD 원칙
- 원칙 1 - 실패하는 단위 테스트를 작성할 때까지 프로덕션 코드(production code)를 작성하지 않는다.
- 원칙 2 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 원칙 3 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
✔️ 테스트하기 힘든 코드를 테스트 가능한 구조로 바꾸기
[예제 코드]
public class Car {
private static final int FORWARD_NUM = 4;
private static final int MAX_BOUND = 10;
private final String name;
private int position = 0;
public Car(final String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("자동차 이름은 값이 존재해야 합니다.");
}
this.name = name.trim();
}
public String getName() {
return name;
}
public int getPosition() {
return position;
}
public void move() {
if (getRandomNo() >= FORWARD_NUM)
this.position++;
}
private int getRandomNo() {
Random random = new Random();
return random.nextInt(MAX_BOUND);
}
}
위 코드에 대한 테스트 코드가 다음과 같다고 하자.
class CarTest {
@Test
public void 이동() {
Car car = new Car("ummchicken");
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
public void 정지() {
Car car = new Car("ummchicken");
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
}
🚨 어떨 때는 Pass하고, 어떨 때는 Fail한다.
이제 테스트 가능한 코드로 바꿔보자.
protected int getRandomNo() {
Random random = new Random();
return random.nextInt(MAX_BOUND);
}
- private에서 protected로 바꾼다.
- 누군가가 확장해서 변경 가능한 포인트로 만들어주기 위함이다.
- 즉, 테스트 가능한 구조를 만들기 위해서다.
테스트 코드를 바꿔보자.
@Test
public void 이동() {
Car car = new Car("ummchicken") {
@Override
protected int getRandomNo() {
return 4;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
public void 정지() {
Car car = new Car("ummchicken") {
@Override
protected int getRandomNo() {
return 3;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
- 항상 통과 가능한 테스트 코드가 완성된다.
- 가능한 테스트코드는 경계값을 테스트하는 게 좋다. (그 부분에서 오류가 많이 남)
'객제지향, TDD, 클린코드 > NextStep' 카테고리의 다른 글
'단위테스트'와 'TDD'에 대하여 (1) | 2023.03.13 |
---|