본문 바로가기

우테코

우테코 2주차 - [자동차 경주] : 기능 구현

 

기능 구현 목록

 

## 기능 구현 목록

1. 자동차 이름 입력 받기[1]
- 자동차 이름이 조건에 맞는지 확인하는 기능[1]
- 쉼표를 기준으로 자동차 이름 목록을 문자열 리스트로 저장[1]
   
2. 시도 회수 입력받기[1]
- 숫자가 정확히 입력되었는지 확인하는 기능[1]

3. 자동차
- 자동차 이름 생성하기[1]
- 자동차 이동거리 생성하기[1]
- 자동차 전진여부 함수 생성하기[1]

4. 자동차 목록
- 자동차 목록 생성하기[1]
- 자동차 목록에 자동차 추가하기[1]
- 턴마다 매턴 자동차 전진하는 함수[1]
- 매 턴의 결과 출력함수[1]
- 우승자 목록을 알려주는 함수[1]

5. 레이싱 게임
- 자동차 목록 생성
   - 자동차 생성
- 입력된 턴수 만큼 반복
   - 자동차 전진
   - 턴별 실행결과 출력
- 우승자 목록 생성
   - 우승자 목록 출력

 

 


1. Car : 자동차 이름

 

먼저 각 자동차 클래스를 구현해주었다.

 

관련 기능

3. 자동차
- 자동차 이름 생성하기[1]
- 자동차 이동거리 생성하기[1]
- 자동차 전진여부 함수 생성하기[1]

 

멤버 변수

- name : 이름

- position : 이동거리

 

메소드

- go : 전진여부 결정

- getName : 이름 접근자

- getPosition: 이동거리 접근자

 

서브 메소드

-RandomNumberGenerator : 0-9 숫자를 랜덤으로 생성하고 전진여부 boolean반환

 

>>구현 코드

package domain;

import camp.nextstep.edu.missionutils.Randoms;

public class Car {

    private final String name;
    private int position = 0;
	
    public Car(String name) {
        this.name = name;
    }
	
    //전진여부 결정
    public void go() {
        if (RandomNumberGenerator()) {
            position++;
        }
    }

	// 위치 접근자
    public int getPosition() {
        return position;
    }
	
    //이름 접근자
    public String getName() {
        return name;
    }

    private boolean RandomNumberGenerator() {
        int num = Randoms.pickNumberInRange(0, 9);
        return (num >= 4);
    }


}

 

=> 처음에는 go메소드 안에 Random.pickNumberInRange(0,9)를 넣었으나 depth가 깊어졌다.

=> 따라서 RandomNumberGenerator를 통해 Go or Stay를 결정하는 boolen 값을 반환하는 함수와

=> 위치정보를 갱신해주는 go함수를 분리하였다.

 


2. CarList : 자동차 목록 객체

 

다음으로는 Car 객체를 List 형태로 나열한 CarList 객체를 구현해주었다.

 

>>관련 기능

4. 자동차 목록
- 자동차 목록 생성하기[1]
- 자동차 목록에 자동차 추가하기[1]
- 턴마다 매턴 자동차 전진하는 함수[1]
- 매 턴의 결과 출력함수[1]
- 우승자 목록을 알려주는 함수[1]

 

 

멤버 변수

cars : CAR 객체 리스트

 

메소드

- 생성자 :  자동차 목록 생성하기

- turnGoOrStay :매턴마다 자동차 전진

- getWinnerNames : 우승자 목록을 알려주는 함수

 

서브 메소드

- getCarsPositions : 자동차의 위치정보 리스트 반환

- ArrayMax : 열에서 최대값 반환

 

 

우선 ArrayList<Car> 멤버 변수 선언을 해주었다.

    private final ArrayList<Car> cars = new ArrayList<>();

 

 

# 자동차 목록 리스트

이후, input으로 주어지는 자동차 이름 리스트 기반으로

자동차 객체를 만들고, 이를 리스트 형태로 추가했다.

=> 이를 위해 input 데이터를  처리할 때 String [] 형태로 파싱해야겠다고 생각했다.

#생성자 => 자동차 목록 리스트 만들기

 public CarList(String[] cars) {
        for (String car : cars) {
            this.cars.add(new Car(car));
        }
    }

 

 

# 각 턴의 전진여부 결정하는 메소드

다음으로 각 턴마다 전진여부를 진행하는 함수였다.

이미 Car 객체에 턴을 진행하는 go 메소드가 있으니

멤버변수 리스트의 Car 객체를 하나씩 돌면서 go 메소드를 call해주었다.

 //각 턴의 전진여부를 결정하는 메소드
  public ArrayList<Car> turnGoOrStay() {
        for (Car car : cars) {
            car.go();
        }
        return cars;
    }

 

 

# 우승자 목록 출력하는 메소드

여기서 가장 골치가 아팠다.

대충 흐름을 생각해보면

 

1) 각 객체별 Position 정수 리스트를 만듬

2) Posiion 최대값을 구함

3) Position 최대값과 같은 Position을 지닌 객체 확인

4) 우승자 이름 리스트 반환

 

그냥 봐도 여러 기능이 합쳐져 있었다.

 

따라서 메소드를 나누어

- getCarsPositions :  Position 정수 리스트를 만들어주는 private 메소드

- ArrayMax :정수 리스트의 최대값을 구하는 메소드

를 먼저 구현했다.

 

// 각 Car의 포지션 정수리스트
private ArrayList<Integer> getCarsPositions() {
        ArrayList<Integer> positionArray = new ArrayList<>();
        for (Car car : cars) {
            positionArray.add(car.getPosition());
        }
        return positionArray;
    }

// 정수 리스트의 최대값 반환
private static int ArrayMax(ArrayList<Integer> positions) {
        return Collections.max(positions);

    }

 

이후 이를 결합하여 우승자 목록 출력함수를 구현했다.

  public List<String> getWinnerNames() {
  		//Position 최대값 구하기
        int maxPosition = ArrayMax(getCarsPositions());
        
        //우승자 이름 담을 리스트
        List<String> winners = new ArrayList<>();
        
        //Position 최대값과 같은 객체만 고르기
        List<Car> winsCars = cars.stream().filter(car -> car.getPosition() == maxPosition).collect(Collectors.toList());
        
        // 우승자 이름 담아주기
        for (Car car : winsCars) {
            winners.add(car.getName());
        }
        return winners;
    }

 

-> 필터링을 할 때, depth를 줄이기 위해 stream을 활용해보았다.

 

참고한 링크:

https://isntyet.github.io/java/java-stream-%EC%A0%95%EB%A6%AC(filter)/

 

java stream 정리(filter)

Repository = java-practice

isntyet.github.io

 

>>구현된 CarList 기능들

package domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class CarList {
    private final ArrayList<Car> cars = new ArrayList<>();

    public CarList(String[] cars) {
        for (String car : cars) {
            this.cars.add(new Car(car));
        }
    }

    public ArrayList<Car> turnGoOrStay() {
        for (Car car : cars) {
            car.go();
        }
        return cars;
    }


    public List<String> getWinnerNames() {
        int maxPosition = ArrayMax(getCarsPositions());
        List<String> winners = new ArrayList<>();
        List<Car> winsCars = cars.stream().filter(car -> car.getPosition() == maxPosition).collect(Collectors.toList());
        for (Car car : winsCars) {
            winners.add(car.getName());
        }
        return winners;
    }

    private ArrayList<Integer> getCarsPositions() {
        ArrayList<Integer> positionArray = new ArrayList<>();
        for (Car car : cars) {
            positionArray.add(car.getPosition());
        }
        return positionArray;
    }

    private static int ArrayMax(ArrayList<Integer> positions) {
        return Collections.max(positions);

    }
}

 


3-1. Input : 자동차 이름

다음으로 자동차 이름을 입력받는 클래스를 정의하였다.

 

처음 프로그램 구현할 때 생각했던 것처럼

depth를 줄이기 위해

 

InputNames로 이름을 입력받을 클래스와

NameValidiator로 이름의 적법성을 검사할 클래스로 나누었다.


1) NameValidiator : 이름입력 적법성 검사

 

>>관련 기능

## 예외상황
1) 자동차의 이름 입력
    - 한개의 자동차 이름이 5자 이상일 때[1]
    - 쉼표로 구분될 것[1]
    - 중복된 자동차 이름이 있을 때[1]
    - 빈 값 체크[1]
    - 쉼표로 끝나는 경우[1]

 

먼저 NameValidiator를 정의해주었다.

 

Validator는 크게 2가지로 구분했는데

- 전체 String 단위 검사 : 입력값이 없을 때 + 쉼표로 끝날때

- 각 이름별 검사 : 중복이름 + 5자가 넘을 때 + 문자가 아닌 값 

 

먼저 String 전체 단위 검사는 생성자와 함께 검사하게 설계했다.

	//생성자와 함께 전체 String 단위 검사
    NameValidiator(String inputCarNames) {
        checkEmpty(inputCarNames);
        checkEndDelimiter(inputCarNames);
    }
	
    //쉼표로 끝날 때
    private void checkEndDelimiter(String inputCarNames) {
        if (inputCarNames.charAt(inputCarNames.length() - 1) == ',') {
            throw new IllegalArgumentException(END_DELIMITER_ERROR);
        }
    }
	
    //입력값이 없을 때
    private void checkEmpty(String inputCarNames) {
        if (inputCarNames.isEmpty()) {
            throw new IllegalArgumentException(NONE_INPUT_ERROR);
        }
    }

 

 

다음으로 String [] 로 파싱된 이름 리스트를 검사할 수 있는 메소드를 만들었다.

- 중복검사

- 5자 이내인지

- 문자로만 구성되어있는지

 

여기서 문자로만 구성되어있는지는 처음에는

아스키코드 ord 값을 통해 검사하려고 했다.

 

그러나 정규표현식을 java에서도 지원해줘서 이걸로 대체했다.

https://codechacha.com/ko/java-regex/

 

Java - 정규표현식(regex), 다양한 예제로 쉽게 이해하기

정규표현식(Regular expressions), Regex는 문자열에서 어떤 패턴을 찾는데 도움을 줍니다. Regex의 Metacharacters, Quantifiers, Grouping에 대해서 정리하였고 다양한 예제로 설명합니다. Regex는 대부분 알고 있지

codechacha.com

 

>>구현한 기능 코드

   public static final Pattern namesStringPattern = Pattern.compile("^[가-힣\\w]+[가-힣\\w" + "," + "]*[가-힣\\w]$");

   
   //중복되었는지 => 고유값 개수가 리스트 길이와 같은지
   public void checkDuplicate(String[] inputCarNamesArray) {
        if (inputCarNamesArray.length != Arrays.stream(inputCarNamesArray).distinct().count()) {
            throw new IllegalArgumentException(DUPLICATED_ERROR);
        }
    }
	
    //빈값이거나 5이상인 길이검사
    public void checkEmptyorLongNames(String[] inputCarNamesArray) {
        for (String carNames : inputCarNamesArray) {
            if (carNames.isEmpty() || carNames.length() > NAME_MAX_SIZE) {
                throw new IllegalArgumentException(WRONG_INPUT_ERROR);
            }
        }
    }
	
    //regex를 통한 형식 검사
    public void checkRightName(String[] inputCarNamesArray) {
        for (String carName : inputCarNamesArray) {
            try {
                matches(carName);
            } catch (IllegalArgumentException e) {
                e.getMessage();
            }

        }
    }

    private void matches(String carName) {
        if (!namesStringPattern.matcher(carName).matches()) {
            throw new IllegalArgumentException(WRONG_INPUT_ERROR);
        }

    }

 

 

2) InputNames : 이름 파싱

 

앞서 CarList 객체에서는 String [] 배열로 이름을 받아 Car 리스트를 생성했다.

따라서, 이름을 쉼표를 기준으로 문자열 배열로 반환해주어야 한다.

 

그리고 그 과정에서 NameValidator를 통해 적법성 검사를 하게 해주었다.

 

정리하면

- 이름 쉼표기준으로 구분

- 적법성 검사

- 문자열 배열로 반환

 

 

 

>> 구현한 기능 코드

public class InputNames {

    public static String[] carInput() {
        System.out.println(INPUT_NAME_MESSAGE);
        String cars = Console.readLine();

        return ParsingInput(cars);
    }

    private static String[] ParsingInput(String cars) {
    	
        NameValidiator check = new NameValidiator(cars);
        //쉼표기준 구분
        String[] InputCarNamesArray = cars.split(",");
		
        //적법성 검사
        check.checkDuplicate(InputCarNamesArray);
        check.checkEmptyorLongNames(InputCarNamesArray);
        check.checkRightName(InputCarNamesArray);
		
        //문자열 리스트 반환
        return InputCarNamesArray;
    }

}

 

앞서도 이야기했지만 이렇게 한 이유는 추가기능요구사안인 depth를 줄이기 위해서였다.

그리고 정확히 그 효과를 볼 수 있어 뿌듯했다.

 

적법성 검사가 잘 읽히는 메소드로 단 3줄로 정리되었다.

 


3-2. Input : 턴 수 입력

턴 수를 입력하는 것도 비슷하게 둘로 나누었다.

- InputTurn :실제 콘솔에서 입력받는 클래스

- TurnValidiaotor : 입력된 값이 적법한지 검사하는 클래스

 

>>관련 기능

2) 횟수 입력 예외사항
    - 입력된 횟수가 숫자가 아닐 때[1]
    - 입력된 횟수가 양수가 아닐 때[1]

 

 

1) TurnValidiator : 적법성 검사 클래스

- 만약 숫자로 변환할 수 있다면 => 1이상의 양수인지 검사

- 숫자로 변환할 수 없다면 => 오류 발생

 

>>기능 구현

public class TurnValidiator {
    public static final Pattern turnNumPattern = Pattern.compile("^[1-9][0-9]*$");
	
    //숫자이고 양수인지 검사
    public TurnValidiator(String num) {
        int newnum = checkNumber(num);
        if (newnum < 0) {
            throw new IllegalArgumentException(NEGATIVE_INPUT_ERROR);
        }

    }
	
    // 숫자로만 이루어져 있는지 검사
    private int checkNumber(String turnNum) {
        matches(turnNum);
        return Integer.parseInt(turnNum);

    }
	
    // regex와 맞는지 검사하는 메소드
    private void matches(String turnNum) {
        if (!turnNumPattern.matcher(turnNum).matches()) {
            throw new IllegalArgumentException(WRONG_INPUT_ERROR);
        }

    }


}

 

=> 검사과정은 생성자에서 모두 이루어지도록 캡슐화하였다.

=> 다만 지금 다시 코드를 보니 정규표현식 대신 try- Integer.parseInt / catch -오류 발생 식으로 하면

더 깔끔한 코드가 될 수 있을 것 같다는 생각이 든다.

 


2) InputTurn : 턴수를 입력받기

이제 턴수를 입력받는 클래스만 구현하면 input 쪽은 모든 기능이 구현된다.

 

흐름을 보면

- turn을 입력받는다.

- 적법성 검사를 한다

- 정수로 변환한다.

 

public class InputTurn {
	
    
    public static int turnInput() {
    	//입력받기
        System.out.println(INPUT_TRY_CNT);
        //적법성검사 + 정수반환
        return inputToInt(Console.readLine());
    }

    private static int inputToInt(String turnNum) {
        TurnValidiator check = new TurnValidiator(turnNum);
        return Integer.parseInt(turnNum);

    }
}

=> 여기서도 생성자에서 그냥 적법성검사 + 정수변환까지 모두 해줄 수 있도록 inputToInt 메소드를 분리했다.

=> 결국 최종적으로 구현할 Controller에서는 turnInput 하나만 호출하면

적법성 검사부터 정수반환까지 모든 것이 수행된다.

 

 

지금까지 구현한 input 기능은 다음과 같다.

1. 자동차 이름 입력 받기[1]
- 자동차 이름이 조건에 맞는지 확인하는 기능[1]
- 쉼표를 기준으로 자동차 이름 목록을 문자열 리스트로 저장[1]
   
2. 시도 회수 입력받기[1]
- 숫자가 정확히 입력되었는지 확인하는 기능[1]

output의 경우에도 두가지로 나누었다.

OutputCars : 각 턴별 결과 출력

OutputWinners : 최종 우승자 출력

4-1. OutputCars : 턴 별 결과 출력

- 출력 형식 : 객체이름 : position ----

 

CarList를 인수로 받아 각 Car 객체별 이름과 Position만큼 dash를 반복해주었다.

크게 특별한 내용은 없기에 패스

public class OutputCars {

    public static void printTurnResult(ArrayList<Car> carList) {
        for (Car car : carList) {
            System.out.printf("%s : %s%n", car.getName(), printPosition(car.getPosition()));
        }
        System.out.println();

    }

    private static String printPosition(int position) {
        String result = "";
        for (int i = 0; i < position; i++) {
            result += "-";
        }
        return result;

    }
}

4-2. OutputWinners : 우승자 출력

앞서 CarList 객체에서 우승자 목록을 반환하는 메소드를 구현해놓았다.

따라서, OutputWinners에서는 이 목록을 매개변수로 받아

출력형식에 맞게 반환하도록만 구현해주었다.

public class OutputWinners {
    public static void printWinners(List<String> winners) {
        String totalWinners = String.join(", ", winners);
        System.out.printf("최종 우승자 : %s%n", totalWinners);
    }
}

 

여기서 지난 1주차 공통피드백의 java API를 활용해보기도 하였다. (.join)

 


5. GameController : 레이싱 게임 구현

이제 준비는 모두 끝났다.

 

재료들을 조합하여 본격적으로 racing 게임의 흐름도를 구현한

GameController 객체를 구현해보자.

 

우리가 구현해야할 흐름도는 다음과 같다.

5. 레이싱 게임
- 자동차 목록 생성
   - 자동차 생성
- 입력된 턴수 만큼 반복
   - 자동차 전진
   - 턴별 실행결과 출력
- 우승자 목록 생성
   - 우승자 목록 출력

 

 

1) SetCars : 자동차 목록 생성

-- 자동차 이름입력받기

-- 입력한 이름리스트로 CarList 만들기

    public void setCars() {
        String[] nameArray = InputNames.carInput();
        carArray = new CarList(nameArray);
    }

 

참 많은 기능이 이 두줄에 함축되어 있다.

- 이름 적법성 검사

- 자동으로 문자열 파싱

- Car 객체 생성 후 리스트 반환

 

 

2) SetTurnNum : 턴수 세팅

: 턴수를 입력받아 세팅한다.

    public void setTurnNum() {
        this.turnNum = InputTurn.turnInput();
    }

- 적법성 검사

- 자동으로 정수 반환

 

 

3) play : 입력한 턴만큼 라운드 실행

- 턴 실행 : turnGoOrStay

- 턴별 결과 출력 : printTurnResult

 public void play() {
        System.out.println(RESULT_MESSAGE);
        for (int i = 0; i < turnNum; i++) {
            OutputCars.printTurnResult(carArray.turnGoOrStay());
        }

    }

 

4) 우승자 목록 출력

    public void showWinner() {
        OutputWinners.printWinners(carArray.getWinnerNames());
    }

- 우승자 목록을 꺼내오고

-이 목록을 기반으로 출력형식에 맞게 출력한다.

 

 

OOP의 위력이 한번에 와닿았다.

 

짧은 코드들에 압축된 의미가 감동으로 와닿았다.

프로그램 흐름도 잘보였다.

 

이제 마지막으로 controller 생성자에 이 흐름을 종합해주면

  public GameController() {
        setCars();
        setTurnNum();
        play();
        showWinner();
    }

 

아름답다.

 

 

>> Controller 구현 기능

public class GameController {

    private CarList carArray;
    private int turnNum;

    public GameController() {
        setCars();
        setTurnNum();
        play();
        showWinner();
    }

    public void setCars() {
        String[] nameArray = InputNames.carInput();
        carArray = new CarList(nameArray);
    }

    public void setTurnNum() {
        this.turnNum = InputTurn.turnInput();
    }

    public void play() {
        System.out.println(RESULT_MESSAGE);
        for (int i = 0; i < turnNum; i++) {
            OutputCars.printTurnResult(carArray.turnGoOrStay());
        }

    }

    public void showWinner() {
        OutputWinners.printWinners(carArray.getWinnerNames());
    }

}

 

6. 상수 클래스 추가

 

에러메시지 및 notice message와 몇가지 변수를 상수화하여 분리하였다.

 

ErrorMessages

package message;

public class ErrorMessages {

    public static final String END_DELIMITER_ERROR = "입력값이 쉼표로 끝납니다.";
    public static final String NONE_INPUT_ERROR = "입력값이 없습니다.";
    public static final String DUPLICATED_ERROR = "중복된 이름값이 존재합니다.";
    public static final String WRONG_INPUT_ERROR = "잘못된 형식의 입력입니다.";
    public static final String NEGATIVE_INPUT_ERROR = "음수를 입력하셨습니다.";


}

 

NoticeMessages

public class NoticeMessages {
    public static final String INPUT_NAME_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)";
    public static final String INPUT_TRY_CNT = "시도할 회수는 몇회인가요?";
    public static final String RESULT_MESSAGE = "실행 결과";
}

 

Constants

package domain;

public class Constants {

    public static final int GO_NUMBER = 4;
    public static final int RANDOM_START_NUM =0;
    public static final int RANDOM_END_NUM =9;
}

 

이것으로 자동차 경주 기능구현이 모두 끝났다.

 

성장한 점 몇 가지가 명확했다.

 

1. OOP 개념을 조금 feel 할 수 있었다.

기존까지는 이해해왔다면 직접 기능들을 구현하며 객체지향적 프로그래밍의 아름다움을 feel했다.

특히 controller 구현과정만큼 신나는 과정은 없었다.

 

2. 소스트리 사용

 

git&github 기본강의를 듣고 소스트리를 적극적으로 사용했다.

터미널보다 commit 기록이 더 명확히 보여 좋았으며 커밋 메시지 작성에도 더 신경을 쓰게 되었다.

 

3. 상수클래스 분리 및 상수화

지난 jason 님의 강의를 듣고 프로그래밍을 하는 내내 어떤 변수들을 상수화 하는 것이 좋을지 신경을 많이 썼다. 상수 클ㄹ새분리는 물론이고 되도록 final을 붙이기 위해 노력했다.

 

4. 자동정렬기능 사용

: 구현이 완료되고 ide 자동정렬기능을 모두 한번씩 적용해주었다.

: 이렇게 편한 기능이 있었는데 왜 난 몰랐을까..

 

5. 이름 선정에 주의를 기울임

: 지난 1주차 공통피드백에서 강조한 내용인만큼 메소드에서 충분히 행위가 드러나게 명명했다.