본문 바로가기

강의/TDD, Clean Code with Java 12기

3단계 자동차 경주 (5일차)

기능 요구사항

  • 초간단 자동차 경주 게임을 구현한다.
  • 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
  • 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다.
  • 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다.

실행 결과

  • 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다.

자동차 대수는 몇 대 인가요? 3

시도할 회수는 몇 회 인가요? 5

실행 결과

-

-

-

 

--

-

--

 

---

--

---

 

----

---

----

 

----

----

-----

 

힌트

  • 값을 입력 받는 API는 Scanner를 이용한다.

Scanner scanner = new Scanner(System.in); String value = scanner.nextLine(); int number = scanner.nextInt();

  • 랜덤 값은 자바 java.util.Random 클래스의 nextInt(10) 메소드를 활용한다.

프로그래밍 요구사항

  • 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.

기능 목록 및 commit 로그 요구사항

  • 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다.
  • git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

AngularJS Commit Message Conventions 중

  • commit message 종류를 다음과 같이 구분

feat (feature) fix (bug fix) docs (documentation) style (formatting, missing semi colons, …) refactor test (when adding missing tests) chore (maintain)

 


요구사항 정리

# 자동차 경주 게임
## 진행 방법
* 자동차 경주 게임 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)

### ToDo
1. 사용자 입력 받기

2) 메세지 출력: 자동차 대수는 몇 대 인가요?
3) 사용자 값 입력: 3 (가시성을 높이기 위해 입력할 수 있는 자동차 대수의 범위는 1 ~ 5 정함.)
    3-1) 값의 범위를 벗어나는 값을 입력할 경우 에러메세지와 함께 재입력을 받음.
4) 메세지 출력: 시도할 회수는 몇 회 인가요?
5) 사용자 값 입력: 5 (가시성을 높이기 위해 시도 회수의 범위는 1 ~ 10으로 정함.)
    5-1) 값의 범위를 벗어나는 값을 입력할 경우 에러메세지와 함께 재입력을 받음.

 

2. 로직 구현

1) 주어진 자동차의 수 만큼 랜덤값을 뽑음. (랜덤 값의 범위는 0~9)
2) 랜덤 값의 따라 1칸 전진 혹은 멈춤.
    2-1) 랜덤 값이 0~3 사이인 경우 멈춤.
    2-2) 랜덤 값이 4~9 사이인 경우 1칸 전진.
3) 현재 자동차의 위치 상태를 표시
4) 시도 회수 -= 1
    4-1) 시도 회수가 0일 경우 다음 단계로 이동.
    4-2) 시도 회수가 1이상 일 경우 로직 1단계로 이동.

## Done


프로젝트 진행

사용자 입력 받기

먼저 input에 관련된 부분부터 진행해 보았다.

README에서 사용자 입력 받기 부분이다.

 

package racing;

import java.util.Scanner;

public class RacingCarInput {
    public void requestInput() throws IllegalArgumentException{
        Scanner sc = new Scanner(System.in);
        System.out.println("자동차 대수는 몇 대 인가요?");
        validInputCheck_NumOfCar(sc.nextInt());
        System.out.println("시도할 회수는 몇 회 인가요?");
        validInputCheck_CycleOfRacing(sc.nextInt());
    }

    public void validInputCheck_NumOfCar(int cnt) {
        final String errorMessage = "자동차의 수가 너무 적거나 많습니다. (range: 1~5)";
        final int threthold = 5;
        if (0 >= cnt || threthold < cnt ) {
            throw new IllegalArgumentException(errorMessage);
        }
    }

    public void validInputCheck_CycleOfRacing(int cnt) {
        final String errorMessage = "시도할 회수가 너무 적거나 많습니다. (range: 1~10)";
        final int threthold = 10;
        if (0 >= cnt || threthold < cnt ) {
            throw new IllegalArgumentException(errorMessage);
        }
    }
}

이부분을 구성할 때 각각의 메소드에서 받아야 하는 인자를 어디까지로 구성을 해야 할 지 고민이 되었다.

validInputChec_NumofCar 부분은 차량의 대수를 확인하고, 차량의 대수가 범위 안에 존재하는 값인지 확인을 하는 부분인다.

이때 인자로 받을 수 있는 부분은 사용자가 입력한 차량의 대수, 범위의 최대값 ( 범위가 0~5 이면 5를 의미 ),  에러메세지이다.

 

1. 사용자가 입력한 차량의 대수

    - 이 부분은 무조건 들어가야 하는 부분이라고 생각한다. 고민할 필요가 없었다.

2. 범위의 최대값 및 에러메세지

    - 이 부분을 인자로 받게되면 메소드를 재활용하기에 용이하기 때문에 많은 고민이 되었다.

    - 그러나 최대값과 에러메세지는 일반적으로 변하지 않는 값이고, 인자가 많은 것을 최대한 피하기 위해  한개의 메소드를 사용하고 여러개의 인자를 받기 보다는, 인자를 줄이고 메소드를 나누기로 결정하였다.

 

코드를 살펴보다 보니 어제 공부한 enum으로 코드를 구성하면 좀 더 코드의 가독성이 좋아지지 않을까라는 생각이 들었다.

다시 enum을 이용하여 코드를 재구성해 보았다.

 

public enum RacingCarEnumInputCheck {
    NUM_OF_CAR(5,"자동차의 수가 너무 적거나 많습니다. (range: 1~5)"),
    CYCLE_OF_RACING(10, "시도할 회수가 너무 적거나 많습니다. (range: 1~10)");

    private final int threthold;
    private final String errorMessage;

    RacingCarEnumInputCheck(int threthold, String errorMessage) {
        this.threthold = threthold;
        this.errorMessage = errorMessage;
    }

    public void validInputCheck(int cnt) throws IllegalArgumentException {
        if (0 >= cnt || threthold < cnt ) {
            throw new IllegalArgumentException(errorMessage);
        }
    }
}

 

enum 클래스는 위와 같이 생성하였으며, enum을 실행하는 부분은 아래와 같이 코드를 구성하였다.

import java.util.Scanner;

public class RacingCarInput {
    public void requestInput() throws IllegalArgumentException{
        Scanner sc = new Scanner(System.in);
        System.out.println("자동차 대수는 몇 대 인가요?");
        RacingCarEnumInputCheck.valueOf("NUM_OF_CAR").validInputCheck(sc.nextInt());
        System.out.println("시도할 회수는 몇 회 인가요?");
        RacingCarEnumInputCheck.valueOf("CYCLE_OF_RACING").validInputCheck(sc.nextInt());
    }
}

 

enum을 잘 활용한 것인지는 모르겠지만, 중복된 코드를 개선할 수 있다는 부분과 유지 보수적인 측면에서  봤을 때 이전보다는 좋아진 코드라고 할 수 있을 것 같다.

 

1. 중복 코드 개선

    - validInputCheck_NumOfCar, validInputCheck_CycleOfRacing 두개의 메소드를 한개로 합칠 수 있게 됨

2. 유지 보수적인 측면

    - 새로운 기준이 생겼을 때 메소드를 추가할 필요 없이 enum 타입을 하나 더 추가해주면 된다.

 

enum을 적용할 수 있겠다고 생각해낸 것과, enum을 사용해서 코드를 직접 구현하여서 뿌듯한 하루인 것 같다.

아직 내 것인 것 처럼 체득한 단계는 아니지만 꾸준히 사용해 나가면 금방 익숙해 지리라 믿는다.

 

로직 구현

 

public class RacingCarOperator {
    int numOfCar;
    int numOfCycle;
    int[] carLocation;

    public RacingCarOperator(int[] racingInputInfo) {
        this.numOfCar = racingInputInfo[0];
        this.numOfCycle = racingInputInfo[1];
        carLocation = new int[this.numOfCar];
    }

    public void Run() {
        // 초기 위치 설정 (1)
        Random rd = new Random();
        for (int i = 0; i < numOfCar; i++) {
            carLocation[i] = 1;
        }

        // 4-1) 시도 회수가 0일 경우 다음 단계로 이동.
        // 4-2) 시도 회수가 1이상 일 경우 로직 1단계로 이동.
        while (numOfCycle != 0) {
            for (int i = 0; i < numOfCar; i++) {
                // 1) 주어진 자동차의 수 만큼 랜덤값을 뽑음. (랜덤 값의 범위는 0~9)
                int randNum = rd.nextInt(10);
                // 2) 랜덤 값의 따라 1칸 전진 혹은 멈춤.
                // 2-2) 랜덤 값이 4~9 사이인 경우 1칸 전진.
                // 2-1) 랜덤 값이 0~3 사이인 경우는 현상유지.
                if (randNum >= 4) {
                    carLocation[i] = carLocation[i] + 1;
                }
            }

            for (int i = 0; i < numOfCar; i++) {
                // 3) 현재 자동차의 위치 상태를 표시
                for (int j = 0; j < carLocation[i]; j++) {
                    System.out.print("-");
                }
                System.out.println();
            }
            System.out.println();
            // 4) 시도 회수 -= 1
            numOfCycle--;
        }
    }
}

 

구현한 로직의 메소드를 다시 작게 나눠보았다.

 

package racing;

import java.util.Random;

public class RacingCarOperator {
    int numOfCar;
    int numOfCycle;
    int[] carLocation;

    public RacingCarOperator(int[] racingInputInfo) {
        this.numOfCar = racingInputInfo[0];
        this.numOfCycle = racingInputInfo[1];
        carLocation = new int[this.numOfCar];
    }


    public void Run() {
        // 초기 위치 설정 (1)
        InitCarLocation();

        // 4-1) 시도 횟수가 0일 경우 다음 단계로 이동.
        // 4-2) 시도 횟수가 1이상 일 경우 로직 1단계로 이동.
        while (numOfCycle != 0) {
            MoveCarLocation();
            ShowCurrentCarLocation();

            // 4) 시도 횟수 -= 1
            numOfCycle--;
        }
    }

    private void InitCarLocation() {
        for (int i = 0; i < numOfCar; i++) {
            carLocation[i] = 1;
        }
    }

    private void MoveCarLocation() {
        Random rd = new Random();
        for (int i = 0; i < numOfCar; i++) {
            // 1) 주어진 자동차의 수 만큼 랜덤값을 뽑음. (랜덤 값의 범위는 0~9)
            int randNum = rd.nextInt(10);
            // 2) 랜덤 값의 따라 1칸 전진 혹은 멈춤.
            // 2-2) 랜덤 값이 4~9 사이인 경우 1칸 전진.
            // 2-1) 랜덤 값이 0~3 사이인 경우는 현상유지.
            if (randNum >= 4) {
                carLocation[i] = carLocation[i] + 1;
            }
        }
    }

    public void ShowCurrentCarLocation() {
        for (int i = 0; i < numOfCar; i++) {
            // 3) 현재 자동차의 위치 상태를 표시
            for (int j = 0; j < carLocation[i]; j++) {
                System.out.print("-");
            }
            System.out.println();
        }
        System.out.println();
    }
}

 


최종 코드 정리

 

RacingCarMain (실행 부분)

import racing.RacingCarInput;
import racing.RacingCarOperator;

import java.util.Scanner;

public class RacingCarMain {
    public static void main(String[] args) {
        RacingCarMain rcm = new RacingCarMain();
        rcm.run();
    }

    private void run() {
        RacingCarInput racingCarInput = new RacingCarInput();

        try {
            int[] racingInputInfo = racingCarInput.requestInput();
            RacingCarOperator racingCarOperator = new RacingCarOperator(racingInputInfo);
            racingCarOperator.Run();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("프로그램을 종료합니다.");
    }
}

RacingCarInput (사용자 입력을 받는 부분)

package racing;

import java.util.Scanner;

public class RacingCarInput {
    public int[] requestInput() throws IllegalArgumentException{
        int numOfCar;
        int numOfCycle;

        Scanner sc = new Scanner(System.in);
        System.out.println("자동차 대수는 몇 대 인가요?");
        numOfCar = sc.nextInt();
        RacingCarEnumInputCheck.valueOf("NUM_OF_CAR").validInputCheck(numOfCar);
        System.out.println("시도할 회수는 몇 회 인가요?");
        numOfCycle = sc.nextInt();
        RacingCarEnumInputCheck.valueOf("CYCLE_OF_RACING").validInputCheck(numOfCycle);
        return new int[] {numOfCar, numOfCycle};
    }
}

 

RacingCarEnumInputCheck (사용자 입력을 받는 부분, enum class)

package racing;

public enum RacingCarEnumInputCheck {
    NUM_OF_CAR(5,"자동차의 수가 너무 적거나 많습니다. (range: 1~5)"),
    CYCLE_OF_RACING(10, "시도할 회수가 너무 적거나 많습니다. (range: 1~10)");

    private final int threthold;
    private final String errorMessage;

    RacingCarEnumInputCheck(int threthold, String errorMessage) {
        this.threthold = threthold;
        this.errorMessage = errorMessage;
    }

    public void validInputCheck(int cnt) throws IllegalArgumentException {
        if (0 >= cnt || threthold < cnt ) {
            throw new IllegalArgumentException(errorMessage);
        }
    }
}

 

RacingCarOperator

package racing;

import java.util.Random;

public class RacingCarOperator {
    int numOfCar;
    int numOfCycle;
    int[] carLocation;

    public RacingCarOperator(int[] racingInputInfo) {
        this.numOfCar = racingInputInfo[0];
        this.numOfCycle = racingInputInfo[1];
        carLocation = new int[this.numOfCar];
    }


    public void Run() {
        // 초기 위치 설정 (1)
        InitCarLocation();

        // 4-1) 시도 횟수가 0일 경우 다음 단계로 이동.
        // 4-2) 시도 횟수가 1이상 일 경우 로직 1단계로 이동.
        while (numOfCycle != 0) {
            MoveCarLocation();
            ShowCurrentCarLocation();

            // 4) 시도 횟수 -= 1
            numOfCycle--;
        }
    }

    private void InitCarLocation() {
        for (int i = 0; i < numOfCar; i++) {
            carLocation[i] = 1;
        }
    }

    private void MoveCarLocation() {
        Random rd = new Random();
        for (int i = 0; i < numOfCar; i++) {
            // 1) 주어진 자동차의 수 만큼 랜덤값을 뽑음. (랜덤 값의 범위는 0~9)
            int randNum = rd.nextInt(10);
            // 2) 랜덤 값의 따라 1칸 전진 혹은 멈춤.
            // 2-2) 랜덤 값이 4~9 사이인 경우 1칸 전진.
            // 2-1) 랜덤 값이 0~3 사이인 경우는 현상유지.
            if (randNum >= 4) {
                carLocation[i] = carLocation[i] + 1;
            }
        }
    }

    public void ShowCurrentCarLocation() {
        for (int i = 0; i < numOfCar; i++) {
            // 3) 현재 자동차의 위치 상태를 표시
            for (int j = 0; j < carLocation[i]; j++) {
                System.out.print("-");
            }
            System.out.println();
        }
        System.out.println();
    }
}

 


멘토님에게... 

 

멘토님 안녕하세요.

자동차 경주 부분을 컨펌받으려고 합니다.

 

1. 클레스를 기준에 맞게 나눠보았습니다.

 

1.1 main (RacingCarMain 클래스)

    - 실행 부분입니다.

    - 최대한 실행부분과 관련된 것만 남기고 다른 것은 쓰지 않으려고 주의하였습니다.

    - try, catch 안에서 돌아가는 것이 뭔가 마음에 걸리나, 더 좋은 방법이 생각나지 않아 현 상태로 종결지었습니다.

 

1.2 사용자 입력을 받는 부분 (RacingCarInput, RacingCarEnumInputCheck)

    - 계산기 프로젝트를 진행하고 난 뒤 멘토님께서 enum에 대해서 좀 더 살펴보면 좋겠다고 하셔서, 어제저녁 enum에 대해서 공부하였으며, 이번 프로젝트에서 적용을 해 보았습니다.

    - 잘 활용한 것인지는 잘 모르겠으나, enum을 사용함으로 써 기존에 작성하던 코드 보다 유지 보수적인 측면에서 한결 더 좋아진 것 같다는 생각이 들었습니다.

 

1.3 로직 구현 (RacingCarOperator)

    - 최대한 메소드를 기능별로 나누는데 집중해 보았습니다.

    - 실행 부분, 랜덤값을 이용하여 차의 위치를 입력하는 부분, 차의 현재 위치를 보여주는 부분으로 나눠 보았습니다.

 

클래스 명과 메소드 명을 짓는 부분에서도 많은 문제점이 보이는 것 같습니다.

최선을 다해 '자동차 경주' 프로젝트에 대한 코드를 구현해 보았습니다만, 문제점이 많을 것으로 생각됩니다.

 

아! 그리고 RacingCarInputTest 클래스도 하나 만들었습니다.

테스트 코드 클래스를 3가지로 (입력값테스트, 입력값테스트실행1, 입력값테스트실행2) 나눠서 만들었습니다.

입력값테스트실행1 클래스에서 입력값테스트를 호출하는 형태인데, 이런식으로 테스트 코드를 짜는 것이 옳은 방법일까요??

확인 한번 부탁드립니다!

오늘도 너무 감사합니다.

항상 행복한 일이 가득하시길 바랍니다.

 

p.s. 매번 피드백을 정성스럽게 해주셔서 너무 감사합니다!! 행복하세요!!