Skip to content

[최보현] 자동차 경주 미션 제출 #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: boya-go
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
04e2c45
docs: 1단계 요구사항 작성
boya-go May 8, 2025
884845c
feat: car 객체 정보 클래스 생성
boya-go May 8, 2025
72129c3
feat: 랜덤값 생성기 구현 및 car 객체 정보에 움직임 메서드 추가
boya-go May 8, 2025
34fcac4
refactor: 테스트를 위해 랜덤숫자 생성기능과 움직이는 기능 분리
boya-go May 8, 2025
fcaa1d5
test: 움직이는 자동차 테스트 작성
boya-go May 8, 2025
0c97bd5
docs: 1단계 요구사항 결과 및 2단계 요구사항 작성
boya-go May 8, 2025
933f644
refactor: 불필요하게 Random 객체 생성되는 것을 방지하기 위해 static final로 변경해서 선언
boya-go May 9, 2025
3a8ff9b
feat: 게임 실행해 우승자 반환하는 기능 작성, 기존 Car 클래스에 있던 랜덤숫자생성기를 인터페이스로 만듦
boya-go May 9, 2025
bbb7480
test: 우승 자동차 구하기 테스트 작성
boya-go May 9, 2025
8ebd4ee
docs: 2단계 요구사항 결과 및 3단계 요구사항 작성
boya-go May 9, 2025
27b8519
refactor: mvc패턴으로 수정 및 게임 실행 구현
boya-go May 9, 2025
1dd8c56
refactor: 전반적인 코드 리팩토링 및 테스트 케이스 추가
boya-go May 10, 2025
f21a690
docs: 3단계 요구사항 결과 및 4단계 요구사항, 결과 작성
boya-go May 10, 2025
51cd513
refactor: RacingGameController 구조 변경
boya-go May 14, 2025
960b262
test: 테스트에만 사용하는 FixedNumberGeneraotor 구현체 테스트 패키지로 이동, @Nested 사용해 논…
boya-go May 15, 2025
f5fe592
refactor: 변수에 값 할당 이전에 검증하도록 수정
boya-go May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 자동차 경주 - 초간단 애플리케이션

## [1] 움직이는 자동차

- 자동차에 관한 요구사항
- [x] 자동차는 이름을 가지고 있다.
- [x] 자동차는 움직일 수 있다.
- [x] 0에서 9 사이의 random 값을 구해 그 값이 **4 이상이면 전진**하고, **3 이하 값이면 멈춘다**.

---

## [2] 우승 자동차 구하기

- 기능 요구사항
- [x] n대의 자동차 참여 가능
- [x] 주어진 횟수동안 n대의 자동차는 전진 또는 멈출 수 있다.
- [x] 이동 조건은 앞의 [움직이는 자동차]와 동일
- [x] 경주 게임 완료 후 우승자를 구할 수 있다.
- [x] 우승자는 한 명 이상일 수 있다.

---

## [3] 게임 실행

- 기능 요구사항
- [x] 자동차 및 게임 방법은 앞의 [움직이는 자동차], [우승 자동차 구하기]와 동일
- [x] 사용자로부터 `이름`, `게임횟수` 를 입력받는다.
- [x] 자동차 이름은 쉼표(,) 기준으로 구분하며, 이름은 5자 이하만 가능하다.
- [x] 메인 메서드를 추가하여 실행 가능하게 만든다.

---

## [4] 리팩터링

- 요구사항
- [x] 단위 테스트를 구현한다.

---

- 코드 작성 요구사항
- [x] 자동차가 움직이는 기능이 의도대로 되는지 테스트한다.
- [x] 자바 코드 컨벤션 `Java Style Guide` 원칙으로 프로그래밍한다.
- [x] 3항 연산자를 쓰지 않는다.
- [x] else 예약어를 쓰지 않는다.
- [x] switch/case도 사용하지 않는다.
- [x] 함수(또는 메서드) 길이가 15라인을 넘어가지 않도록 구현한다.
- [x] 함수(또는 메서드)가 한 가지 일만 하도록 한다.
10 changes: 10 additions & 0 deletions src/main/java/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import controller.RacingGameController;

public class App {
public static void main(String[] args) {
RacingGameController racingGameController = new RacingGameController();

racingGameController.startRacingGame();

}
}
44 changes: 44 additions & 0 deletions src/main/java/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package controller;

import domain.Car;
import domain.CarRaceGame;
import domain.Generator.RandomNumberGenerator;
import view.inputView;
import view.outputView;

import java.util.*;
import java.util.stream.Collectors;

public class RacingGameController {

public static final String DELIMITER = ",";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DELIMITER가 어떤 목적의 구분자인지 표현할 수 있게 상수명을 변경해보면 어떨까요?
추가적으로 input string에 대한 parsing 역할을 contoller에게 부여한 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문자열에서 구분자 , 를 기준으로 차 이름을 나누므로 CAR_NAME_DELIMITER 로 수정했습니다.

CarRaceGame 생성자에 List가 파라미터인데 리스트를 만들고 넣어줘야 한다고 생각해서 controller에 로직이 들어갔습니다. parsing 역할을 따로 클래스로 분리하여 도메인 패키지에 포함시키고 controller에서 그 클래스의 메서드를 이용하는 것으로 수정했습니다 !


public void startRacingGame(){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수(또는 메서드) 길이가 15라인을 넘어가지 않도록 구현한다!!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startRacingGame, printGameRounds , printWinners 메서드로 분리하여 수정했습니다 !

String names = inputView.enterCarNames();

List<Car> cars = Arrays.stream(names.split(DELIMITER))
.map(name -> new Car(name, new RandomNumberGenerator()))
.collect(Collectors.toList());

final CarRaceGame carRaceGame = new CarRaceGame(cars);

int round = inputView.enterRoundNumber();

carRaceGame.validateRoundNumber(round);

outputView.printGameResultTitle();

for (int i = 0; i < round; i++) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller에서 round를 사용하고 CarRaceGame에서는 round를 검증하고 있는데요.
이렇게 구조를 잡으신 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

round 변수 사용 이전에 검증하면 된다고 생각했습니다.

round 역시 Car 객체의 name과 마찬가지로 검증 후 할당하게끔 하고 싶은데 어떤 식으로 해야할지 잘 모르겠습니다 🥲


carRaceGame.playOneRound();
System.out.println();
outputView.printRoundResult(cars);

}

List<String> winnersName = carRaceGame.getWinnerNames();

outputView.printGameWinners(winnersName);

}
}
42 changes: 42 additions & 0 deletions src/main/java/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package domain;

import domain.Generator.NumberGenerator;

public class Car {

private String name;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불변인 변수는 final로 명시해주면 좋아요! (다른 클래스에도 함께 반영 부탁드려요)
왜 좋을지 생각해보시고 답변 달아주시면 좋을것같아요:)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final을 명시하면 재할당 할 수 없기 때문에 안전하고, 불변이라는 것을 명확하게 전달하기 때문에 가독성 면에서도 좋을 것 같습니다 !

private int position;
private final NumberGenerator generator;
private static final int MOVE_THRESHOLD = 4;

public Car(String name, NumberGenerator generator) {
validateName(name);
this.name = name.trim();
this.position = 0;
this.generator = generator;
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}

private void validateName(String name) {

if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 빈 값이 될 수 없습니다.");
}
if (name.length() > 5) {
throw new IllegalArgumentException("이름은 5자 이하만 가능합니다.");
}
}

public void movePosition() {
if (generator.generate() >= MOVE_THRESHOLD ) {
position++;
}
}
}
69 changes: 69 additions & 0 deletions src/main/java/domain/CarRaceGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package domain;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class CarRaceGame {

private List<Car> cars;
private List<String> winners;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winners를 멤버 변수로 같게한 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getWinnerNames() 메서드에서 바로 사용할 수 있게끔 하려고 했었습니다.

메서드 내부의 지역변수로 수정했습니다 !


public CarRaceGame(List<Car> cars) {
this.cars = cars;
notAllowDuplicatedCarNames();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수에 값을 할당하고 검증하는것과 검증 후 할당하는 것은 어떠한 차이가 있을까요?

Copy link
Author

@boya-go boya-go May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수에 값 할당 후 검증하면 만약 유효하지 않았을 시에도 변수에 그 값이 저장되기 때문에 안정적이지 못합니다.
목적에 맞게 변수에 값을 할당하기 위해서는 검증이 선행되어야 할 것 같습니다.

따라서 inputView에서 받아온 문자열에서 carNameList를 만들고 Set 자료형을 이용해 중복이 포함되었는지를 검증한 후에 Car 객체의 name 변수에 할당하도록 수정했습니다.


private void notAllowDuplicatedCarNames() {
List<String> carNames = cars.stream()
.map(Car::getName)
.collect(Collectors.toList());

Set<String> carNameSet = new HashSet<>(carNames); // 중복된 이름은 자동으로 제거됨

// 만약 리스트의 크기와 Set의 크기가 다르면 중복이 있다는 의미
if (carNames.size() != carNameSet.size()) {
throw new IllegalArgumentException("중복된 자동차 이름은 입력할 수 없습니다.");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 따로 주석을 달아주셨나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제하는 것을 깜빡했습니다 ! 😅

}

public void validateRoundNumber(int gameRound) {
if (gameRound < 1 ) {
throw new IllegalArgumentException("라운드는 한 번 이상 진행되어야 합니다.");
}
}

public void playOneRound() {
for (Car car : cars) {
car.movePosition();
}
}

private int getMaxDistance() {
int maxDistance = cars.stream().mapToInt(Car::getPosition).max().orElseThrow();

return maxDistance;
}

private List<String> getWinners(int maxDistance) {
List<String> winners = cars.stream().filter(car -> car.getPosition() == maxDistance).map(Car::getName).toList();

return winners;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method chaining 시 개행을 이용하면 가독성을 높일 수 있어요!
따로 사용할 필요 없는 경우 변수로 할당하지 않고 바로 return해보는건 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행하여 가독성을 높이고, 변수로 할당할 필요 없는 경우 바로 return하게끔 수정하였습니다 !


public void playRacingGame(int gameRound) {
for (int i=0; i < gameRound;i++) {
playOneRound();
}
}

public List<String> getWinnerNames() {
winners = getWinners(getMaxDistance());

if (winners == null) {
throw new IllegalStateException("게임이 실행되지 않았습니다.");
}
return winners;
}
}
16 changes: 16 additions & 0 deletions src/main/java/domain/Generator/FixedNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package domain.Generator;

public class FixedNumberGenerator implements NumberGenerator {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트에서만 사용하기 위한 구현체라면 test 패키지에 두는게 좋아요!
왜 좋을지 생각해보시고 답변 남겨주세요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트에만 사용한다는 것을 명시적으로 나타낼 수 있어 혼돈을 방지할 수 있을 것 같습니다.

private final int[] numbers;
private int index = 0;

public FixedNumberGenerator(int... numbers) {
this.numbers = numbers;
}

@Override
public int generate() {
return numbers[index++];
}

}
5 changes: 5 additions & 0 deletions src/main/java/domain/Generator/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package domain.Generator;

public interface NumberGenerator {
int generate();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

랜덤한 로직을 통제하기 위해 인터페이스를 통해 추상화한 부분 좋습니다!
이렇게 메서드 하나만을 가진 인터페이스는 "함수형 인터페이스"로 만들어볼 수 있는데요. 이에대해 학습해보시면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수형 인터페이스로 만들면 따로 클래스를 만들 필요 없이 람다식으로 간결하게 활용할 수 있군요! 🤔
다만 제 코드에서는 활용하기 힘들 것 같은데 맞나요...?

13 changes: 13 additions & 0 deletions src/main/java/domain/Generator/RandomNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package domain.Generator;

import java.util.Random;

public class RandomNumberGenerator implements NumberGenerator {

private static final Random RANDOM = new Random();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random 객체를 상수로 잘 만들어주셌네요👍
한번만 생성하고 재활용해도 되는 객체의 경우 상수로 만들어 활용하면 불필요한 객체생성 및 메모리를 줄일 수 있어서 좋습니다!


@Override
public int generate( ) {
return RANDOM.nextInt( 10 );
}
}
18 changes: 18 additions & 0 deletions src/main/java/view/inputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package view;

import java.util.Scanner;

public class inputView {

private static final Scanner scanner = new Scanner(System.in);

public static String enterCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
return scanner.nextLine();
}

public static int enterRoundNumber() {
System.out.println("시도할 회수는 몇회인가요?");
return scanner.nextInt();
}
}
26 changes: 26 additions & 0 deletions src/main/java/view/outputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package view;

import domain.Car;

import java.util.List;

public class outputView {

private static final String CAR_POSITION_EXPRESSION = "-";
private static final String WINNER_NAMES_DELIMITER = ",";

public static void printGameResultTitle() {
System.out.println("\n실행 결과");
}

public static void printRoundResult(List<Car> cars) {
for (Car car : cars) {
System.out.println(car.getName() + ": " + CAR_POSITION_EXPRESSION.repeat(car.getPosition()));
}
System.out.println();
}

public static void printGameWinners(List<String> winners) {
System.out.println(String.join(WINNER_NAMES_DELIMITER, winners) + "가 최종 우승했습니다.");
}
}
Loading