자바지기 박재성님이 운영하시는 NextStep 교육 웹사이트에서 수강한 자바 플레이 그라운드 with TDD, 클린코드에 대한 공부 기록을 남기려고 합니다. 이번 포스팅은 TDD를 사용해 자동차 경주 게임 구현 미션에 관한 포스팅입니다.
강의를 통해 느낀 점 (피드백을 보기 전)
우선 해당 단원에서 제시한 프로그래밍 요구사항은 다음과 같다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 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와 같은 클래스를 추가해 분리한다.
- 모든 원시 값과 문자열을 포장한다.
- 일급 컬렉션을 쓴다.
그렇다면 먼저 여기서 말하는 일급 컬렉션이란 무엇인가? 쉽게 말해 일급 컬렉션이란 컬렉션을 포장한 것이다.
모든 원시 값과 문자열을 포장한다는 의미도 똑같다. 아래 코드를 확인해보자.
public class Name{ //문자열을 포장하는 클래스
private String name;
//..생략
}
public class Position{ //정수형을 포장하는 클래스
private int position;
//..생략
}
public class Cars{ //일급 콜렉션
private List<Car> cars;
//..생략
}
다른 요구사항은 이전에 수행한 숫자 야구와 겹치는 부분이 많아서 충분히 이해할 수 있었다.
다음으로는 기능 요구사항을 살펴보았다.
TDD를 하면서 제일 중요하게 생각한 요구사항은 다음과 같다.
'전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우다.'
지난 과제인 숫자야구에서도 랜덤 값을 사용해 프로그램을 구현했는데, 랜덤 값이 들어간 경우에는 테스트를 하기가 까다로웠다.
먼저 제일 처음 작성한 도메인의 핵심 관계는 다음과 같다. (입력값과 출력 값과 승자 계산은 제외했다.)
이렇게 작성하면 Car, Position, Name 부분은 수월하게 TDD, 테스트를 작성할 수 있었다.
그러나 Cars와 CarRacingGame 부분에서는 RandomUtils 때문에 테스트를 작성하기 어려웠다.
특히 CarRacingGame 부분이 어려웠다. 그래서 테스트를 똑바로 작성하지 못한 것으로 기억한다.
일단 이렇게 구현을 마치고 피드백을 보았다.
피드백 영상을 시청한 후
보면서 머리가 띵한 부분이 몇 가지 있었다. 일단 기록하고 싶은 내용을 남기겠다.
- ToDoList를 먼저 작성해서 진행하자. 테스트 경계를 정하고, 도메인을 설계하자. 쉬운 것부터 어려운 것 순으로 진행.
- 보통 Controller -> Service -> Repository 순으로 의존성이 흐르는데 Repository부터 테스트를 작성하자.
- 선생님의 생각은 테스트를 위한 코드를 도메인에 추가해도 된다고 하셨다. 예를 들어 임의의 생성자?
- 레거시를 리팩토링할 때는 메서드의 시그니처를 안 바꿀 때가 많다. 이럴 때 리팩토링이 굉장히 힘들다.
이럴 때 문제를 잘 해결하고 코드를 리팩토링하는 것이 진짜 실력이다. - 변경이 많으면 인터페이스의 도입을 고려하자. 인스턴스의 변수의 수를 최소화하자.
- 테스트 가능한 코드와 테스트하기 힘든 코드를 분리하자.
- 객체와 객체를 비교하자. (equals & hash)
- 랜덤과 같은 로직은 테스트가 힘들다. 따라서 의존성이 많이 퍼져 있으면 테스트 작성이 힘든데 이는 좋은 설계가 아니다.
영향이 제일 적게 설계를 하자. - 콜백 함수 적용도 고려하자.
- 테스트가 힘들다면 익명 객체로 오버라이딩(상황에 따라 적절하게)해서 사용하자!!! (이 부분이 굉장히 충격적이었다.)
예시 코드는 다음과 같다.
Cars cars = new Cars(names){
@Override
public int getRandomNumber() {
return 7;
}
};
//원래는 랜덤 값을 반환하는 메서드인데, 임의의 값을 리턴하도록 오버라이딩한다.
//이렇게 하면 우리가 원하는대로, 테스트를 진행할 수 있다.
피드백을 보고 랜덤 로직의 영향을 줄이기 위해 의존성을 최대한 앞으로(?) 가져왔다. 이미지는 다음과 같다.
대신 랜덤한 값을 넘기기 위해 랜덤 값을 넘기는 List 변수를 추가해 이를 Cars 인스턴스에 전달했다.
또한 익명 클래스를 오버라이딩해 랜덤 값을 편하게 테스트할 수 있는 방법을 깨달아 이를 도입했다!!!
개인적으로 느낀점은 꼭 이렇게 다이어그램을 그려야겠다고 생각했다.
피드백 영상을 보고 난 후 TDD
사실 코딩테스트에서 자주 사용하는 정렬에 도움을 주는 클래스인 Comparable을 사용하면 다수의 우승자일 때 판단하는게 편리할 것 같지만(처음 구현 때는 이렇게 했다), 리팩토링한 부분에서는 for문을 두 번 돌리는 방식을 택했다.
이렇게 코드를 작성하면서 1등 비교에 꽤나 신경을 썼다.
get 메서드를 사용하지 않고 객체에 메시지를 보내려는 형식으로 작성을 하니 코드를 작성하면서 도중에 내가 짠 로직이지만 헷갈리는 부분이 있었다. 잠깐 살펴보면 Car 클래스의 비교 로직은 다음과 같다. Cars에서 for문을 돌리면서 이를 비교한다.
public class Car{
//..생략
public Car compareCar(Car comparedCar) {
if(comparedCar.isMoreThan(position)){
return comparedCar;
}
return this;
}
public boolean isMoreThan(Position comparedPosition){
if(comparedPosition.comparePosition(position) == position ) {
return true;
}
return false;
}
}
public class Position {
//..생략
public Position comparePosition(Position comparativePosition){
if(comparativePosition.isMoreThan(position)){
return comparativePosition;
}
return this;
}
private boolean isMoreThan(int comparativePosition) {
return position > comparativePosition;
}
}
나름 의존성도 잘 빼고(?), get 메서드 사용도 줄이면서 리팩토링을 마무리한 것 같다. 뿌듯하다.
다시 도전한 자동차 경주 게임 구현 미션 코드는 아래와 같다.
https://github.com/happysubin/java-racingcar-playground/tree/challenge
구현부분에서 많이 생략한 부분이 있습니다. 과제를 직접해보면 큰 도움이 되리라고 생각합니다!
이상으로 포스팅을 마칩니다. 감사합니다.
댓글