Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
361cbb7
docs(README): add feature list for racing car
sun007021 Oct 27, 2025
a6913f9
test(ApplicationTest): add various tests for racing car functionality
sun007021 Oct 27, 2025
8704960
test(RacingGame): add unit tests for racing game functionality
sun007021 Oct 27, 2025
f6e8f0b
test(InputView): add unit tests for input parsing functionality
sun007021 Oct 27, 2025
d4e7058
test(CarTest): add unit tests for car creation and movement functiona…
sun007021 Oct 27, 2025
c7e2ea1
test(InputValidator): add unit tests for car name and round count val…
sun007021 Oct 27, 2025
6056a21
test(OutputView): add unit tests for car position and winner formatting
sun007021 Oct 27, 2025
f8c9348
feat(Car): implement car creation and save current position
sun007021 Oct 27, 2025
7478b65
feat(Car): add moveForward and getPosition methods
sun007021 Oct 27, 2025
49b3342
test(test): add stub to build test
sun007021 Oct 27, 2025
0084494
feat(InputView): implement car names and round count input methods
sun007021 Oct 27, 2025
48b95f5
feat(InputValidator): implement car names and round count validation …
sun007021 Oct 27, 2025
61e8be3
feat(OutputView): add feature to print each round result and final wi…
sun007021 Oct 27, 2025
f8ed21d
feat(RacingGame): implement basic racing game structure and round pla…
sun007021 Oct 27, 2025
97fed3b
feat(RacingGame): implement random movement logic for cars
sun007021 Oct 27, 2025
21cc2f5
feat(RacingGame): add rounds parameter and implement play method for …
sun007021 Oct 27, 2025
3d4d054
feat(RacingGame): implement winner determination logic based on maxim…
sun007021 Oct 27, 2025
0a93e13
feat: integrate application
sun007021 Oct 27, 2025
0ecfc06
fix: delete enter when print result
sun007021 Oct 27, 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
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
# java-racingcar-precourse
## 기능 구현 목록

### 1. 입력 기능
- [x] 자동차 이름 입력 받기
- [x] 시도 횟수 입력 받기

### 2. 입력 검증 기능
**자동차 이름 검증**
- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리
- [x] 자동차 이름이 5자를 초과하는 경우 예외 처리
- [x] 자동차 이름이 빈 문자열인 경우 예외 처리
- [x] 자동차 이름이 공백만 있는 경우 예외 처리

**시도 횟수 검증**
- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리
- [x] 숫자가 아닌 값 입력 시 예외 처리
- [x] 0 이하의 값 입력 시 예외 처리
- [x] 정수 범위를 벗어나는 값 입력 시 예외 처리

### 3. 입력 파싱 기능
- [x] 쉼표(,)를 기준으로 자동차 이름 분리
- [x] 입력받은 시도 횟수를 정수로 변환

### 4. 자동차 기능
- [x] 자동차 객체 생성 (이름 저장)
- [x] 자동차의 현재 위치 저장
- [x] 자동차 전진 기능
- [x] 자동차의 현재 위치 조회

### 5. 게임 진행 기능
- [x] 게임 라운드 반복 실행 (n회)
- [x] 각 라운드마다 모든 자동차에 대해 이동 시도
- [x] 0~9 사이의 무작위 값 생성
- [x] 무작위 값이 4 이상일 경우 자동차 전진

### 6. 우승자 판별 기능
- [x] 모든 자동차 중 최대 이동 거리 찾기
- [x] 최대 이동 거리를 가진 자동차들을 우승자로 선정

### 7. 출력 기능
- [x] 각 라운드 후 모든 자동차의 이름과 위치 출력
- [x] 자동차 위치를 `-` 기호로 표시
- [x] 우승자 이름 출력 (단독 우승자)
- [x] 우승자 이름 출력 (공동 우승자, 쉼표로 구분)

---

5 changes: 4 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package racingcar;

import racingcar.controller.RacingGameController;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
RacingGameController controller = new RacingGameController();
controller.run();
}
}
53 changes: 53 additions & 0 deletions src/main/java/racingcar/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package racingcar.controller;

import racingcar.domain.Car;
import racingcar.domain.RacingGame;
import racingcar.validator.InputValidator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.List;

public class RacingGameController {

public void run() {
List<String> carNames = readAndValidateCarNames();
int roundCount = readAndValidateRoundCount();

RacingGame game = new RacingGame(carNames, roundCount);

OutputView.printResultMessage();
playGameWithOutput(game, roundCount);

List<String> winners = game.getWinners();
OutputView.printWinners(winners);
}

private List<String> readAndValidateCarNames() {
List<String> carNames = InputView.readCarNames();
String input = String.join(",", carNames);
InputValidator.validateCarNames(input);
return carNames;
}
Comment on lines +26 to +31

Choose a reason for hiding this comment

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

개인적으론 메서드 이름에 and를 넣는건 지양하는 편입니다.
두가지 일을 동시에 한다는 인상을 주기에 충분하기 때문입니다!
또한 validate의 일은 이미 Validator에게 넘기셔서 read의 동작만 나타내는 네이밍을 하셔도 괜찮지 않을까 싶습니다~

Copy link
Author

Choose a reason for hiding this comment

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

그렇군요
감사합니다! 확실히 하나의 확실한 네이밍이 더 직관적인거 같네요

Choose a reason for hiding this comment

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

저도 and 주의해야겠어요. 감사합니다


private int readAndValidateRoundCount() {
int roundCount = InputView.readRoundCount();
InputValidator.validateRoundCount(String.valueOf(roundCount));
return roundCount;
}
Comment on lines +33 to +37

Choose a reason for hiding this comment

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

구조상 NumberFormatException이 날수도 있을것 같은데 readRountCount 하기전에 먼저 검증을 하셔야 오류를 피할수 있을것 같습니당!

Copy link
Author

Choose a reason for hiding this comment

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

넵 감사합니다!


private void playGameWithOutput(RacingGame game, int roundCount) {
for (int i = 0; i < roundCount; i++) {
game.playRound();
printRoundResult(game);
}
}
Comment on lines +39 to +44
Copy link

Choose a reason for hiding this comment

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

이 부분 메서드 작명에서 의도를 드러내신 부분 정말 잘하신 것 같아요! 👍

많은 분들이 playGame() 이렇게 작성하셨는데, Controller 본연의 역할(model과 view의 오케스트레이션)이 추상화되어 문제가 있다고 보였습니다. 선욱님은 그 부분을 명시해주신 부분이 인상깊어요


private void printRoundResult(RacingGame game) {
List<Car> cars = game.getCars();
for (Car car : cars) {
OutputView.printCarStatus(car.getName(), car.getPosition());
}
Comment on lines +48 to +50
Copy link

Choose a reason for hiding this comment

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

stream Api를 쓰는 것도 좋을 것 같습니다.

OutputView.printRoundResult();
}
}
Copy link

Choose a reason for hiding this comment

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

EOF 신경쓰면 좋을 것 같아요

Choose a reason for hiding this comment

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

RacingGame의 로그를 찍는 느낌이라 RacingGame내부에 작성하는건 어떤가요??

Copy link
Author

Choose a reason for hiding this comment

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

말씀하신대로 RacingGame에서 하는게 역할이 더 맞는거 같네요 감사합니다!

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

public class Car {
private static final int MAX_NAME_LENGTH = 5;
private static final int INITIAL_POSITION = 0;

private final String name;
private int position;

public Car(String name) {
validateName(name);
this.name = name;
this.position = INITIAL_POSITION;
}

private void validateName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다.");
}
if (name.isBlank()) {
throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다.");
}
if (name.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다.");
Copy link

Choose a reason for hiding this comment

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

5자 이하에서 "5"도 상수로 구성시키면 좋을 것 같아요.

Choose a reason for hiding this comment

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

DRY! 좋은 피드백인 것 같아요!

}
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}

public void moveForward() {
position++;
}
}
61 changes: 61 additions & 0 deletions src/main/java/racingcar/domain/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package racingcar.domain;

import camp.nextstep.edu.missionutils.Randoms;

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

public class RacingGame {
private static final int RANDOM_NUMBER_MIN = 0;
private static final int RANDOM_NUMBER_MAX = 9;
private static final int MOVE_FORWARD_THRESHOLD = 4;

private final List<Car> cars;
private final int rounds;

public RacingGame(List<String> carNames, int rounds) {
this.cars = carNames.stream()
.map(Car::new)
.collect(Collectors.toList());

Choose a reason for hiding this comment

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

자바 17부턴 .toList()만 사용하셔도 됩니다!

Copy link
Author

Choose a reason for hiding this comment

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

옛날 자료를 보고 공부했더니 몰랐네요 감사합니다!
최신 문서들 보고 문법공부를 더 해야겠네요

Copy link

Choose a reason for hiding this comment

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

현재 문법과 .toList()의 차이점이 있으니 사용하기 전에 한번 확인해보시면 좋을 것 같아요!

this.rounds = rounds;
}

public List<Car> getCars() {
Copy link
Author

Choose a reason for hiding this comment

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

다른사람들의 코드를 보고 스스로 개선할점
get메소드를 구현시에는 매우 주의해야함, 만약에 꼭 필요한 상황이라도 List로 반환하면 참조값을 반환하게되어 외부에서 값이 수정될 가능성도 있음을 인지하고 상황에 맞는 자료형으로 반환하기위해 노력할 것
https://velog.io/@backfox/getter-쓰지-말라고만-하고-가버리면-어떡해요

Choose a reason for hiding this comment

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

방어적 복사 좋습니다 :)

return new ArrayList<>(cars);
}

public void play() {
for (int i = 0; i < rounds; i++) {
playRound();
}
}
Comment on lines +28 to +32
Copy link

Choose a reason for hiding this comment

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

이 메소드는 테스트를 위한 메소드로 확인이 되는데,
테스트에 구현해서 사용하는게 더 좋을 것 같습니다!


public void playRound() {
for (Car car : cars) {
tryMove(car);
}
}

private void tryMove(Car car) {
Copy link

Choose a reason for hiding this comment

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

도메인 주도 설계적으로 생각해봤을 때, 이 함수는 Car 안으로 들어가는게 나아보여요.
이건 절차지향적인 설계에 좀 더 가까워보여요.

Copy link
Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

int randomNumber = Randoms.pickNumberInRange(RANDOM_NUMBER_MIN, RANDOM_NUMBER_MAX);
if (randomNumber >= MOVE_FORWARD_THRESHOLD) {
car.moveForward();
}
}

public List<String> getWinners() {
int maxPosition = findMaxPosition();
return cars.stream()
.filter(car -> car.getPosition() == maxPosition)
.map(Car::getName)
.collect(Collectors.toList());
}

private int findMaxPosition() {
return cars.stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);
}
}
52 changes: 52 additions & 0 deletions src/main/java/racingcar/validator/InputValidator.java
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.

피드백 감사합니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package racingcar.validator;

public class InputValidator {
private static final int MAX_NAME_LENGTH = 5;

private InputValidator() {
}

public static void validateCarNames(String carNames) {
if (carNames == null || carNames.isEmpty()) {
throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다.");
}

String[] names = carNames.split(",", -1);
for (String name : names) {
validateSingleCarName(name.trim());
}
}

private static void validateSingleCarName(String name) {
if (name.isEmpty()) {
throw new IllegalArgumentException("자동차 이름은 빈 문자열일 수 없습니다.");
}
if (name.isBlank()) {
throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다.");
}
if (name.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다.");
}
}
Comment on lines +20 to +30
Copy link

@dohyunk58 dohyunk58 Oct 29, 2025

Choose a reason for hiding this comment

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

Car 객체 생성자에서 검증하는 것과 동일한 코드인데, Car 클래스 내부 검증이 중요하므로 검증 로직을 Car 생성자에 넣은 것으로 이해했습니다. 다만, 이후에 규칙을 바꾸는 경우 두 곳 모두를 변경해야 하므로 유지보수에서 문제가 발생할 것 같습니다.
저의 경우는 입력 시에는 split한 값에 대해 검증하지 않고 입력 후 Car 객체 생성 시 생성자에서 호출하여 위 부분을 검증하도록 했습니다.(물론 제가 정답은 아닙니다..!)

그리고 문자열 입력값 자체를 검증하는 기능도 넣어주셨는데 저는 이 부분을 깜빡했네요. 꼼꼼하게 예외처리하신 것 배워갑니다.

추가로 isBlank()에서도 isEmpty()와 같이 빈 문자열일때 true를 반환하지만, 빈 문자열에 대한 예외를 강조하기 위해서 따로 조건문을 두신 것으로 이해했습니다.

Copy link
Author

Choose a reason for hiding this comment

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

입력시에도 검증해야한다고 생각해 이렇게 작성했는데 말씀해주신 부분 생각해보니 코드 중복이 너무 심해질 수 있을거 같네요. 저도 객체에서만 검증하도록 하는것이 더 좋을거 같다는 생각이 듭니다.
피드백 감사합니다!!

isBlank는 말씀하신대로 두 경우의 차이에서 의도를 나타내기 위해 추가했는데 괜찮은 선택인지는 잘 모르겠네요

Choose a reason for hiding this comment

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

Fast Fail을 고려하신 것 같아서 전 보기 좋습니다!
하지만 목적을 명확히 구분하면 좋을 것 같습니다!


public static void validateRoundCount(String roundCount) {
if (roundCount == null || roundCount.isEmpty()) {
throw new IllegalArgumentException("시도 횟수는 null이거나 빈 문자열일 수 없습니다.");
}

if (roundCount.isBlank()) {
throw new IllegalArgumentException("시도 횟수는 공백일 수 없습니다.");
}

int count;

Choose a reason for hiding this comment

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

선언을 try 바깥에서 한 이유가 있을까요??

Copy link
Author

Choose a reason for hiding this comment

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

아마 밑에 if문에서도 사용하니까 try문 내용과 구분하려고 바깥에서 선언을 한거 같은데
try문에서 같이 선언해주는게 좋을까요?

Choose a reason for hiding this comment

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

코드 가독성이 더 좋아질 것 같아요 ㅎㅎ
그리고 앞선 리뷰와 마찬가지로 목적을 조금 더 구분하면, 코드가 더 명확해질 것 같아요!
현재는 DRY에 위배되는 것 같습니다!

try {
count = Integer.parseInt(roundCount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("시도 횟수는 숫자여야 합니다.");
}

if (count <= 0) {
throw new IllegalArgumentException("시도 횟수는 0보다 커야 합니다.");
Copy link

Choose a reason for hiding this comment

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

3주차부터는 enum을 사용하라고 하셨으니까 예외 메세지를 모아놓은 enum을 쓰면 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다! enum과 상수를 사용하도록 신경써야겠네요

}
}
}
37 changes: 37 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package racingcar.view;

import camp.nextstep.edu.missionutils.Console;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class InputView {
private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)";
private static final String ROUND_COUNT_INPUT_MESSAGE = "시도할 횟수는 몇 회인가요?";

private InputView() {
}

Choose a reason for hiding this comment

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

이런 static메서드를 사용할경우에는 생성자를 숨기는게 좋네요 하나 배워갑니다


public static List<String> readCarNames() {
System.out.println(CAR_NAMES_INPUT_MESSAGE);
String input = Console.readLine();
Copy link

Choose a reason for hiding this comment

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

우테코에서 제공하는 missionutils 라이브러리 안에 Console.close() 함수가 있다고 하더라구요. 입력이 다 끝난 뒤 혹은 main 마지막에 이 함수를 쓰는게 좋아보여요.

Choose a reason for hiding this comment

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

Console.close()는 JVM이 inputStream을 알아서 처리해주기 때문에, 명시적으로 표현하지 않아도 될 것 같아요!

return parseCarNames(input);
}

public static List<String> parseCarNames(String input) {
return Arrays.stream(input.split(","))
.map(String::trim)
.collect(Collectors.toList());
}

public static int readRoundCount() {
System.out.println(ROUND_COUNT_INPUT_MESSAGE);
String input = Console.readLine();
return parseRoundCount(input);
}

public static int parseRoundCount(String input) {
return Integer.parseInt(input);
}
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

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

파싱하기 전에 검증이 필요해보입니다!

}
40 changes: 40 additions & 0 deletions src/main/java/racingcar/view/OutputView.java
Copy link

Choose a reason for hiding this comment

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

문자 출력에 대한 역할 분리를 잘하신 것 같습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package racingcar.view;

import java.util.List;

public class OutputView {
private static final String RESULT_MESSAGE = "실행 결과";
private static final String WINNER_PREFIX = "최종 우승자 : ";
private static final String POSITION_SYMBOL = "-";
private static final String CAR_STATUS_FORMAT = "%s : %s";

Choose a reason for hiding this comment

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

출력 형식을 포맷 문자열로 정할수 있는거 잊고있었네요.

private static final String WINNER_DELIMITER = ", ";

private OutputView() {
}

public static void printResultMessage() {
System.out.println();
System.out.println(RESULT_MESSAGE);
}

public static void printCarStatus(String carName, int position) {
System.out.println(formatCarStatus(carName, position));
}

public static String formatCarStatus(String carName, int position) {
String positionDisplay = POSITION_SYMBOL.repeat(position);
return String.format(CAR_STATUS_FORMAT, carName, positionDisplay);
}

public static void printRoundResult() {
System.out.println();
}

public static void printWinners(List<String> winners) {
System.out.println(formatWinners(winners));
}

public static String formatWinners(List<String> winners) {
return WINNER_PREFIX + String.join(WINNER_DELIMITER, winners);
}
}
Loading