개발일기

4단계 : racing car 리팩토링 본문

TDD

4단계 : racing car 리팩토링

한둥둥 2024. 10. 28. 18:54

RacingCar 공부 회고..

 

리팩토링을 하면서 가장 우선적으로 일급 컬렉션을 사용하기 위하여 노력하였다. 일급 컬렉션의 장점은 일급 컬렉션을 사용하면서 기존에 car에 있던 코드들이 일급 컬렉션 안에 코드로 옮겨가지면서 조금 더 큰 틀을 쉽게 볼 수 있으며 OOP 규칙을 조금 더 잘지키면서 코드를 작성하는 느낌이 든다.

 

사실 OOP라는 말을 정말 많이 들었다. 근데 문득 "나는 OOP가 뭔지 잘 알고 있을까?" 라는 고민을 가지게 되었다.

 

OOP는 4가지 규칙을 가지고 있다. 

 

1) 캡슐화

- 캡슐화는 객체의 데이터와 메서드를 하나의 단위로 묶고, 외부로부터 내부 상태를 숨기는 개념이다. 

- 객체의 데이터는 private로 막아 외부에서 직접 접근을 불가능하도록 하고, getter, setter 메서드로 간접 접근을 가능하도록 하는 것

여기서 의문 ? setter 도 괜찮을걸까?

왜 setter를 사용하지 말아야하는가? 궁금함을 느꼈고 혼자서 스스로 고민을 해보았다. 그러고 정답인지는 모르겠다. 블로그 글도 찾아봤으며 책도 뒤져 보았다. 

setter를 사용하지 않으면 필드 상태 값을 불변 값을 보장 할 수 있다. 즉, private final int age 이런 식으로 값을 보장할 수 있다. 

그러면 '값을 어케 바꾸는데?' 라는 고민을 하게 된 것 같다. 

정답은 new 연산자를 사용하자였고, 사실 이것은 next-step을 하면서 알려주어서 생각 할 수 있었다. 

 

2) 상속

- 상속은 기존의 클래스의 속성과 메서드를 재사용하여 새로운 클래스를 만드는 개념이다. 즉, 상위 개념을 하위 개념에 적용하는 방법 이렇게 하면 코드의 재사용성을 늘려줄 수 있다. 

- 예를 들어, Car 클래스를 상속받아 ElectricCar 클래스와 GasCar 클래스를 만들 수 있습니다. 이렇게 하면 공통된 기능은 Car 클래스에 정의하고, 전기차와 가솔린 차에 특화된 기능만 각각의 클래스에서 구현할 수 있다.

 

사실 상속으로 적극적으로 사용하려고 노력하지만, 진짜 어렵다. 어떻게 하면 상속을 더 잘 사용할지 나도 많이 고민해봐야겠다.  

 

3) 다형성 

- 다형성은 동일한 인터페이스나 메서드를 다양한 방식으로 구현할 수 있게 해주는 것이다. 

 

다형성이야말로 테스트하기 어려운 것을 테스트하기 쉽게 만들어준다. 

 

4. 추상화

- 추상화는 객체의 중요한 속성만을 추출하여 정의하는 것이다. 

 

처음보다 뒤로 갈 수록 이야기가 적어졌지만, 코드를 보며 말할게 많기에 코드를 살펴보자. 

 

Car.class

package racing.car.domain;

import racing.car.model.CarName;
import racing.car.model.Position;

import java.util.Objects;

public class Car implements Movable {
    private final CarName name;
    private Position position;

    public Car(String name) {
        this(name, 0);
    }

    // 생성자에서 name을 반드시 받도록 강제
    public Car(String name, int position) {
        this.name = new CarName(name);
        this.position = new Position(position);
    }

    @Override
    public void move(int number) {
        position = position.increase(number);
    }

    public boolean isSame(int otherPosition) {
        return this.position.isSame(otherPosition);
    }

    public int max(int maxPosition) {
        return this.position.max(maxPosition);
    }

    public String getName() {
        return this.name.getName();
    }

    public int getPosition() {
        return this.position.getPosition();
    }

    @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(position, car.position);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, position);
    }

    @Override
    public String toString() {
        return name.toString();
    }



}

 

Position.class

package racing.car.model;

import java.util.Objects;

public class Position {
    private static final int MOVE_THRESHOLD = 4;
    private int position;

    public Position() {
    }

    public Position(int position) {
        this.position = position;
    }

    public Position increase(int number) {
        if (isMove(number)) {
            position++;
        }

        return new Position(position);
    }

    private boolean isMove(int number) {
        return number >= MOVE_THRESHOLD;
    }

    public int max(int maxPosition) {
        return Math.max(this.position, maxPosition);
    }

    public boolean isSame(int otherPosition) {
        return this.position == otherPosition;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Position position1 = (Position) o;
        return position == position1.position;
    }

    @Override
    public int hashCode() {
        return Objects.hash(position);
    }

    public int getPosition() {
        return this.position;
    }
}

 

내가 Car 클래스를 만들면서 고민했던 것은 어떻게하면 앞서 말했겠지만 "조금 더 간단하게 쉽게 구현 할 수 있을까?"

외에도 어떻게 하면 "객체에게 조금 더 불변성을 제공할까?" , setter메서드를 사용하지 않고 코딩, 최대한 getter메서드를 사용하지 않기였다. 

하지만 getter메서드는 값을 어쩔 수 없이 조회해야하기 때문에 사용하였다. 

여기서 중요한 점은 나는 position이라는 원래 int형 변수로 사용되어야 할 값을 Position이라는 객체를 사용하여 만들어주었다. 

이 코드에서 아쉬운 점은 'final 값을 Position 객체 안에 int형  을 줄 수 없었나?' 였고 new 를 통해서 충분히 가능하다는 결론이 나왔다. 

 

그래서 new Position을 통해서 객체를 아예 새롭게 반환하고 있는 increase메서드를 볼 수 있다. 

 

또한 Car에 Movable을 implements하여 구현하였고 이를 통해서 interface로 테스트하기 힘든 코드가 빠져버리기에 조금 더 테스트 하기 쉽게 작성해주었다. 사실 굳이 인터페이스가 아니여도 될 것이다는 생각을 나중에 하게되었다. 

 

CarName.class

package racing.car.model;

import java.util.Objects;

public class CarName {
    private static final String OVER_MESSAGE_ERROR = "자동차 이름이 5자글자를 초과하였습니다.";
    private static final int NAME_LIMIT_LENGTH = 5;
    private final String name;

    public CarName(String name) {
        check(name);
        this.name = name;
    }

    private void check(String name) {
        if (name.length() > NAME_LIMIT_LENGTH) {
            throw new IllegalArgumentException(OVER_MESSAGE_ERROR);
        }
    }

    public String getName(){
        return this.name;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CarName carName = (CarName) o;
        return Objects.equals(name, carName.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

 

차의 이름 또한 원자 값을 감싸주면서 많은 이점을 가져올 수 있었다. 

 

RacingGameController.class

package racing.car.controller;

import racing.car.model.Cars;
import racing.car.random.GenerateRandom;
import racing.car.view.InputView;
import racing.car.view.ResultView;

import java.util.ArrayList;
import java.util.Scanner;

public class RacingGameController {

    private static final Scanner scanner = new Scanner(System.in);
    private static final InputView INPUT_VIEW = new InputView(scanner);
    private static final ResultView RESULT_VIEW = new ResultView();
    private static final String INVALID_CAR_COUNT_MESSAGE = "게임을 진행하려면 자동차가 최소 2대 있어야 합니다.";
    private static final String INVALID_TRY_COUNT_MESSAGE = "게임을 진행하려면 시도 횟수는 1 이상이어야 합니다.";


    public static void main(String[] args) {
        RacingGameController game = new RacingGameController();
        game.play();
    }

    public void play() {
        INPUT_VIEW.carQuestion();
        String[] carNames = INPUT_VIEW.inputCar();

        Cars cars = new Cars(new ArrayList<>());

        initializeCars(carNames, cars);

        INPUT_VIEW.tryQuestion();
        int tryCount = validateTryCount(INPUT_VIEW.inputTry());

        for (int i = 0; i < tryCount; i++) {
            simulateRaceRound(cars);
        }

        RESULT_VIEW.outputWinnerView(cars.findWinner(max(cars)));
    }

    public void simulateRaceRound(Cars cars) {
        cars.move(new GenerateRandom());
        RESULT_VIEW.outputView(cars);
    }

    public int max(Cars cars) {
        return cars.getMaxPosition();
    }

    public void initializeCars(String[] carNames, Cars cars) {
        cars.addCars(carNames);
    }

    public int validateCarCount(int carCount) {
        if (carCount < 2) {
            throw new RuntimeException(INVALID_CAR_COUNT_MESSAGE);
        }
        return carCount;
    }

    public int validateTryCount(int tryCount) {
        if (tryCount <= 0) {
            throw new RuntimeException(INVALID_TRY_COUNT_MESSAGE);
        }
        return tryCount;
    }

}

 

해당 코드는 RacingGameController클래스이고 사실 이번 코드는 불완전한게 조금 더, 여기에 있는 메서드들을 객체 안으로 넣어 줄 수 있다는 생각을 하게 되었다. 그래서 완벽한 코드는 아니지만, 이렇게 보여주었다. 

추후에 코드를 다시 작성해보았는데, 어떻게 변할지 두근두근이다. 

 

 

RacingGameControllerTest

package racing.car.controller;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import racing.car.model.Cars;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;

public class RacingGameControllerTest {
    private RacingGameController game;

    @BeforeEach
    void setUp() {
        game = new RacingGameController();
    }

    @Test
    @DisplayName("initializeCars 메서드는 각 Car 객체를 올바르게 초기화해야 한다.")
    void initializeCars_메서드_테스트() {
        Cars cars = new Cars(new ArrayList<>());
        String[] carNames = {"leo", "seoun"};
        game.initializeCars(carNames, cars);


        assertThat(cars).isEqualTo(new Cars(List.of(new Car("leo", 0), new Car("seoun",0))));
    }

    @Test
    @DisplayName("validateCarCount 메서드는 유효한 자동차 개수 입력 시 해당 값을 반환해야 한다.")
    void validateCarCount_메서드_테스트_성공() {
        int carCount = 2;
        int result = game.validateCarCount(carCount);

        assertThat(result).isEqualTo(2);
    }

    @Test
    @DisplayName("validateCarCount 메서드는 자동차 개수가 2 미만일 경우 예외를 발생시켜야 한다.")
    void validateCarCount_메서드_테스트_실패() {
        int carCount = 0;

        assertThatThrownBy(() -> {
            game.validateCarCount(carCount);
        })
                .isInstanceOf(RuntimeException.class)
                .hasMessageMatching("게임을 진행하려면 자동차가 최소 2대 있어야 합니다.");

    }

    @Test
    @DisplayName("validateTryCount 메서드는 유효한 시도 횟수 입력 시 해당 값을 반환해야 한다.")
    void validateTryCount_메서드_테스트_성공() {
        int tryCount = 1;
        int result = game.validateTryCount(tryCount);

        assertThat(result).isEqualTo(1);
    }

    @Test
    @DisplayName("validateTryCount 메서드는 시도 횟수가 1 미만일 경우 예외를 발생시켜야 한다.")
    void validateTryCount_메서드_테스트_실패() {
        int tryCount = 0;

        assertThatThrownBy(() -> {
            game.validateTryCount(tryCount);
        })
                .isInstanceOf(RuntimeException.class)
                .hasMessageMatching("게임을 진행하려면 시도 횟수는 1 이상이어야 합니다.");
    }

    @Test
    @DisplayName("자동차 리스트에서 position 최대 값 구하기")
    void 자동차_max_메서드_테스트() {
        Cars cars = new Cars(List.of(new Car("dong", 3), new Car("duu", 4)));
        int max =game.max(cars);

        assertThat(max).isEqualTo(4);
    }
}

나는 이번 미션에서는 테스트 코드에 익숙해지기 위해서 모든 것들을 테스트하고자 마음을 먹었고 나의 컨셉이였다. 

하지만 솔직하게 Controller 비즈니스 로직이 있는거 같아서 조금 더 다시 작성한다면 잘 작성 할 수 있겠다는 생각을 가지며 TestCode를 작성했다. 

 

CarTest

package racing.car.doamin;

import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CarTest {

    @Test
    void 초기값_0_테스트() {
        Car car = new Car("seou");
        assertThat(car).extracting("position").isEqualTo(0);
    }

}

 

CarNameTest 클래스 

 

package racing.car.model;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.model.CarName;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CarNameTest {
    @Test
    @DisplayName("자동차_이름이_5글자_미만_성공")
    void 자동차_이름_5글자_미만_성공() {
        String name = "leo";
        CarName carName = new CarName(name);

        assertThat(carName).isEqualTo(new CarName("leo"));
    }

    @Test
    @DisplayName("자동차_이름이_5글자_초과_에러")
    void 자동차_이름_5글자_초과_에러() {

        assertThatThrownBy(() -> {
            new CarName("hanseounggyun");
        }).isInstanceOf(RuntimeException.class).hasMessageMatching("자동차 이름이 5자글자를 초과하였습니다.");
    }

}

 

CarsTest

package racing.car.model;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import racing.car.random.Random;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class CarsTest {

    @Test
    @DisplayName("Cars 리스트 최대값 반환")
    void 최대값_반환_테스트() {
        Cars cars = new Cars(List.of(new Car("dong", 5), new Car("pling", 4), new Car("iiwq", 3)));

        int max = cars.getMaxPosition();

        assertThat(max).isEqualTo(5);
    }

    @Test
    @DisplayName("Cars 클래스 addCars 메서드 초기값 선언 테스트")
    void 초기값_선언_테스트() {
        List<Car> initialCars = new ArrayList<>();
        Cars cars = new Cars(initialCars);

        cars.addCars(new String[]{"phobi", "covi", "dong"});

        assertThat(initialCars).isEqualTo(List.of(new Car("phobi",0), new Car("covi",0), new Car("dong",0)));
    }

    @Test
    @DisplayName("Cars 클래스 move 메서드 테스트")
    void move_메서드_테스트(){
        List<Car> initialCars = List.of(new Car("phobi", 0), new Car("doung", 0));
        Cars cars = new Cars(initialCars);
        cars.move(()-> 4);

        assertThat(initialCars).isEqualTo(List.of(new Car("phobi", 1), new Car("doung",1)));
    }

    @Test
    @DisplayName("max 최대 값5와 똑같은 우승자 찾기 메서드 테스트")
    void find_winner_우승자_찾기(){
        List<Car> initialCars = List.of(new Car("phobi", 5), new Car("doung", 3), new Car("bbj", 5), new Car("kim", 4));

        int maxPosition = 5;
        Cars cars = new Cars(initialCars);
        List<Car> winner = cars.findWinner(maxPosition);
        assertThat(winner).contains(new Car("phobi", maxPosition), new Car("bbj", maxPosition));
        assertThat(winner).hasSize(2);
    }

}

 

PositionTest

package racing.car.model;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;


class PositionTest {

    @Test
    @DisplayName("move 메서드는 3 이하인 경우 position은 증가하지 않는다.")
    void 앞으로_이동_4이하() {
        Position position = new Position();
        position.increase(3);

        assertThat(position).isEqualTo(new Position(0));

    }

    @Test
    @DisplayName("move 메서드는 4 이상인 경우 position이 1증가 된다.")
    void 앞으로_이동_4이상() {
        Position position = new Position();
        position.increase(4);

        assertThat(position).isEqualTo(new Position(1));
    }

    @Test
    @DisplayName("Position 클래스의 position 값을 비교하여 같으면 True, 다르면 False")
    void same_메서드_테스트() {
        Position position = new Position(1);

        assertThat(position.isSame(1)).isTrue();
        assertThat(position.isSame(0)).isFalse();
    }

    @Test
    @DisplayName("max 최대값 반환 테스트")
    void max_최대값_테스트() {
        Position position = new Position(5);
        int max = position.max(3);

        assertThat(max).isEqualTo(5);
    }
}

 

이렇게 작성하였고, 아직 부족한 것이 많았지만 늘었고, 코드를 작성할 때, 많은 고민을 하면서 작성하게 되었다는 것에 보람을 느끼며 마무리 할 수 있게 되었다. 

사실 중간 중간 느낀점은 더 많았던거 같지만.. 여기까지만 작성 다음에는 다시 작성한 RacingCar, Lotto TDD로 돌아올 예정이다.

'TDD' 카테고리의 다른 글

감면 혜택 및 취등록세 계산 관련 테스트 코드 작성기  (0) 2025.04.18
다시 작성해보는 TDD RacingCar  (0) 2024.11.19
3단계 RacingCar TDD  (1) 2024.10.07
TDD 1단계 구현  (0) 2024.09.27