개발일기
다시 작성해보는 TDD RacingCar 본문
나는 해당 프로젝트를 다시 작성하며 몇 가지 포인트에 힘을 쓰며 코딩을 하였다.
1. 다시 만드는 레이싱 게임은 원자 값 또한 객체로 포장하려고 노력하였다.
이러한 노력 덕분에 얻을 수 있었던 이 점은 이름이나 이동하는 숫자에 대한 여부를 Parsing 해주는 부분에서 해당 객체에 책임을 부여하여 조금 더 코드가 객체지향적으로 변경되었던거 같다..!
2. Oracle에 나와있는 자바 표준 스타일을 지키며 코딩하려고 노력하였다.
3. indent 들여쓰기를 3이상 만들지 않으려고 노력하였다. 예를 들어 for 문 안에 if문을 작성하는 행위
4. 중복되는 테스트를 작성하지 않으려고 노력하였다. 이유는 테스트 코드 또한 요구 사항이 변경하면 유지보수 해야하는 코드이기 때문에 중복하여 테스트를 작성하지 않으려 노력을 많이했다.
Car 클래스 코드
package racingcar.model.car;
import racingcar.model.number.MoveNumber;
import racingcar.model.name.Name;
import racingcar.strategy.Strategy;
import java.util.Objects;
public class Car {
private final Name name;
private final MoveNumber moveNumber;
public Car(String name) {
this(new Name(name), new MoveNumber(0));
}
public Car(String name, int number) {
this(new Name(name), new MoveNumber(number));
}
public Car(Name name, MoveNumber moveNumber) {
this.name = name;
this.moveNumber = moveNumber;
}
public void move(int number) {
this.moveNumber.move(number);
}
public String getName() {
return name.getName();
}
public int getMoveNumber() {
return moveNumber.getNumber();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Objects.equals(name, car.name) && Objects.equals(moveNumber, car.moveNumber);
}
@Override
public int hashCode() {
return Objects.hash(name, moveNumber);
}
@Override
public String toString() {
return "Car{" +
"name=" + name +
", moveNumber=" + moveNumber +
'}';
}
}
생성자가 꽤 많은데 이또한 테스트 코드를 간단하게 비교해주기 위하여 생성자를 만들어주었다.
자바코드 스타일에서 확인하면 toString()은 만들어주지 않아도 EqualsAndHascode는 반드시 만들라는 가이드라인이 있어 해당 방식으로 진행해주었다.
파일 끝에 한줄을 띄워주는 곳도 컨벤션이다.
Cars 클래스
package racingcar.model.car;
import racingcar.strategy.Strategy;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class Cars {
private static final String CARS_ERROR_MESSAGE = "자동차 이름은 적어도 두 개 입력해야 합니다.";
private static final int MIN_CARS_SIZE = 2;
private final List<Car> cars;
public Cars(String names) {
this(parse(split(names)));
}
public Cars(String[] names) {
this(parse(names));
}
public Cars(List<Car> cars) {
this.cars = cars;
}
private static String[] split(String names) {
return names.split(",");
}
private static List<Car> parse(String[] names) {
return isValid(Arrays.stream(names)
.map(Car::new)
.collect(Collectors.toList()));
}
private static List<Car> isValid(List<Car> cars) {
if (cars.size() < MIN_CARS_SIZE) {
throw new IllegalArgumentException(CARS_ERROR_MESSAGE);
}
return cars;
}
public void moves(Strategy strategy) {
cars.forEach(car -> car.move(strategy.random()));
}
public List<Car> getCars() {
return this.cars;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cars cars1 = (Cars) o;
return Objects.equals(cars, cars1.cars);
}
@Override
public int hashCode() {
return Objects.hash(cars);
}
@Override
public String toString() {
return "Cars{" +
"cars=" + cars +
'}';
}
}
Cars클래스는 car클래스의 일급 컬렉션이다. 누군가는 Cars라는 클래스 이름이 적절하지 않다고 말해준 피드백을 받았다.
사실 나는 그렇게 생각하지 않아서 CarGroup이라는 클래스명보다는 Cars가 훨씬 간결하고 알기 쉽고 일급 컬렉션 관련 글들을 확인해보니 대부분 일급컬렉션은 s를 붙여서 사용하기에 나는 이런식으로 만들어주었다.
일급 컬렉션을 만들어줌으로써의 장점은 정말 많다. 여러개의 car객체가 공통적으로 행위하는 메소드에 대해서 하나의 메소드를 만들어 해당 메소드가 어떤 일을 작동하는지 큰 틀에서 보기 쉬우며, 요구사항의 변경이 있을 때에도 안에 있는 메소드를 약간의 수정을 통해서 변경할 수 있기에 엄청난 장점이 있다.
Name 클래스
package racingcar.model.name;
import java.util.Objects;
public class Name {
private static final int NAME_MAX_LENGTH = 5;
private static final String NAME_ERROR_MESSAGE = "이름은 " + NAME_MAX_LENGTH + "글자 이하로 입력해주세요.";
private static final String ERROR_EMPTY_NAME_MESSAGE = "자동차 이름이 공백으로 입력되었습니다.";
private final String name;
public Name(String name) {
this.name = isValid(name);
}
private String isValid(String name){
if(name.isBlank()) {
throw new IllegalArgumentException(ERROR_EMPTY_NAME_MESSAGE);
}
if(name.length() > NAME_MAX_LENGTH) {
throw new IllegalArgumentException(NAME_ERROR_MESSAGE);
}
return name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Name name1 = (Name) o;
return Objects.equals(name, name1.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public String toString() {
return "Name{" +
"name='" + name + '\'' +
'}';
}
}
static final 과 일반 필드 값을 한 줄 띄워쓰기를 통해서 구현해주었다. 자바는 한줄한줄 띄워쓰기도 컨벤션이고 의미가 있기에 함부로 Enter로 변수명간에 한줄을 띄우는 것을 조심해서 하는게 좋다는 피드백을 받았다. 여러가지 글들을 찾아보고 컨벤션을 찾아 본 결과 피드백해준 내용이 정말 맞았고, 앞으로는 의미없는 띄워쓰기는 하지 않아야겠다.
아마 해당 내용은 자바만 해당하는 것은 아닐듯하다.
MoveNumber 클래스
package racingcar.model.number;
import java.util.Objects;
public class MoveNumber {
private static final int FRONT_MOVE_NUMBER = 4;
private int number;
public MoveNumber(int number) {
this.number = number;
}
public void move(int number) {
if(number < FRONT_MOVE_NUMBER) {
return ;
}
this.number += 1;
}
public int getNumber() {
return number;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MoveNumber that = (MoveNumber) o;
return number == that.number;
}
@Override
public int hashCode() {
return Objects.hash(number);
}
@Override
public String toString() {
return "MoveNumber{" +
"number=" + number +
'}';
}
}
실질적으로 이동을 담당하는 클래스이다.
코드는 한번 살펴보면 정말 잘 알 수 있기에 생략하고 넘어가겠다.
Winners 클래스
package racingcar.model.winner;
import racingcar.model.car.Car;
import racingcar.model.car.Cars;
import java.util.List;
import java.util.stream.Collectors;
public class Winners {
private int max = Integer.MIN_VALUE;
private final Cars cars;
public Winners(Cars cars) {
this.cars = cars;
}
public List<String> getRacingGameWinners() {
max();
return cars.getCars().stream()
.filter(this::isValid)
.map(Car::getName)
.collect(Collectors.toList());
}
private boolean isValid(Car car) {
return car.getMoveNumber() == max;
}
private void max() {
cars.getCars().stream()
.map(Car::getMoveNumber)
.forEach(moveNumber -> max = Math.max(max, moveNumber));
}
}
우승자를 확인해주는 클래스이다.
우승자는 max값인지 그러고 동일한 최대값을 가지고 있는 유저는 동시에 출력해야하기에 해당 방식으로 코드를 구현하였다.
package racingcar.strategy;
import camp.nextstep.edu.missionutils.Randoms;
public class MoveStrategy implements Strategy{
@Override
public int random() {
return Randoms.pickNumberInRange(0, 9);
}
}
package racingcar.strategy;
@FunctionalInterface
public interface Strategy {
int random();
}
이동 전략 코드이다. 해당 코드는 Randoms Utill은 우테코에서 만들어준 코드 기반이기에 일반 자바 랜덤함수와 다르다.
package racingcar.view;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
private static final String CAR_NAME_QUESTION = "경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분)";
private static final String CAR_TRY_QUESTION = "시도할 횟수는 몇 회인가요?";
private static final String TRY_COUNT_ERROR_MESSAGE = "시도 횟수는 양수여야 합니다.";
private static final int MIN_TRY_COUNT = 1;
public InputView() {
}
public String inputCarNames() {
System.out.println(CAR_NAME_QUESTION);
return Console.readLine();
}
public static int inputTryCount() {
System.out.println(CAR_TRY_QUESTION);
int tryCount = Integer.parseInt(Console.readLine());
validateTryCount(tryCount);
return tryCount;
}
public static void validateTryCount(int tryCount) {
if(tryCount < MIN_TRY_COUNT) {
throw new IllegalArgumentException(TRY_COUNT_ERROR_MESSAGE);
}
}
}
package racingcar.view;
import racingcar.model.car.Car;
import racingcar.model.car.Cars;
import racingcar.model.winner.Winners;
import racingcar.strategy.MoveStrategy;
import java.util.List;
public class ResultView {
private static final String RESULT_SENTENCE = "실행 결과";
private static final String WINNER_SENTENCE = "최종 우승자 : ";
private static final String CAR_MOVE_DELIMITER = " : ";
private static final String CAR_WINNER_DELIMITER = ", ";
private static final String MOVE_MARKER = "-";
private final Cars cars;
public ResultView(Cars cars) {
this.cars = cars;
}
public void moveCarResultView(int tryCount) {
System.out.println();
System.out.println(RESULT_SENTENCE);
for (int i = 0; i < tryCount; i++) {
cars.moves(new MoveStrategy());
makeResultView();
}
}
private void makeResultView() {
for (Car car : cars.getCars()) {
System.out.print(car.getName() + CAR_MOVE_DELIMITER);
makeMoveMakerResultView(car);
System.out.println();
}
System.out.println();
}
public void makeMoveMakerResultView(Car car) {
for (int i = 0; i < car.getMoveNumber(); i++) {
System.out.print(MOVE_MARKER);
}
}
public void winnerResultView() {
Winners winners = new Winners(cars);
List<String> racingGameWinners = winners.getRacingGameWinners();
System.out.print(WINNER_SENTENCE);
String result = String.join(CAR_WINNER_DELIMITER, racingGameWinners);
System.out.println(result);
}
}
CarsTest 클래스
package racingcar.model.car;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
public class CarsTest {
@Test
void 자동차_이름_저장_테스트(){
Cars cars = new Cars("phob,leo,jade,bobo");
assertThat(cars).isEqualTo(new Cars(new String[]{"phob","leo","jade","bobo"}));
}
@Test
void 자동차_이름_길이_5글자_에러_테스트() {
assertThatThrownBy(() -> new Cars("phobiq,leo,jade"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageMatching("이름은 5글자 이하로 입력해주세요.");
}
@Test
void 자동차_이름_에러_테스트() {
assertThatThrownBy(() -> new Cars("phobi"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void 자동차_이동_테스트() {
Cars cars = new Cars("phobi,leo,luna");
cars.moves(()-> {return 4;});
assertThat(cars).isEqualTo(new Cars(List.of(new Car("phobi",1), new Car("leo", 1), new Car("luna",1))));
}
@Test
void 자동차_이동_실패_테스트() {
Cars cars = new Cars("phobi,leo,luna");
cars.moves(()-> {return 3;});
assertThat(cars).isEqualTo(new Cars(List.of(new Car("phobi",0), new Car("leo", 0), new Car("luna",0))));
}
}
package racingcar.model.car;
import racingcar.model.car.Car;
import racingcar.model.number.MoveNumber;
import racingcar.model.name.Name;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class CarTest {
@Test
void 자동차_이동_테스트() {
Car car = new Car("pobi");
car.move(4);
assertThat(car).isEqualTo(new Car(new Name("pobi"),new MoveNumber(1)));
}
}
package racingcar.model.winner;
import org.junit.jupiter.api.Test;
import racingcar.model.car.Cars;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
public class WinnersTest {
@Test
void 우승자_판별_테스트() {
Cars cars = new Cars("phobi,strag,seoun");
cars.moves(() -> {return 4;});
Winners winners = new Winners(cars);
assertThat(winners.getRacingGameWinners()).isEqualTo(List.of("phobi", "strag", "seoun"));
}
}
생성자를 몇 개 추가했을 뿐인데 테스트 코드를 훨씬 쉽게 작성할 수 있었습니다. 만약 생성자를 사용하지 않고 테스트를 작성하려고 한다면, 모든 값을 new로 생성해야 해서 코드가 복잡해지고 가독성이 떨어질 것입니다. 직접 생성자 없이 테스트 코드를 작성해 보면 그 차이를 확실히 느낄 수 있을 것입니다.
마지막으로, 저는 원래 NextStep 강의를 듣고 복습 차원에서 우아한테크코스를 신청했습니다. 하지만 멋쟁이사자처럼 멘토링을 진행한 이후, 부트캠프 교육을 받는 것이 저에게 맞는지 고민이 많았습니다. 여러 고민 끝에, 1년을 준비하기보다는 이전 직장보다 더 나은 회사로 취업을 목표로 하기로 했습니다.
아직 부족한 부분이 많아 올해까지는 학습과 준비에 집중하고, 내년 초부터 본격적으로 이력서를 작성하고 취업 활동을 시작하려고 합니다. 이 글을 읽는 여러분들도 모두 힘내시길 바랍니다! 화이팅! 💪
끝으로 자바는 정말정말 중요하다는 것을 다시 한번 깨닫게 되었습니다. 이 글을 읽는 여러분들도 프레임워크, 라이브러리 , 그럴듯한 기술에 현혹되지 않고 기초부터 천천히하면 좋을거 같습니다. 경험에서 나오는 말입니다. ㅠㅠ
다음 글은 로또로 찾아 뵙겠습니다.
'TDD' 카테고리의 다른 글
감면 혜택 및 취등록세 계산 관련 테스트 코드 작성기 (0) | 2025.04.18 |
---|---|
4단계 : racing car 리팩토링 (6) | 2024.10.28 |
3단계 RacingCar TDD (1) | 2024.10.07 |
TDD 1단계 구현 (0) | 2024.09.27 |