Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,107 @@
# java-racingcar-precourse
# java-racingCar-precourse

## 기능 요구 사항
초간단 자동차 경주 게임을 구현한다.

- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.

## 입출력 요구 사항
**입력**
- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분)
```
pobi,woni,jun
```

- 시도할 회수
```
5
```

## 출력
**각 차수별 실행 결과**
```
pobi : --
woni : ----
jun : ---
```

**단독 우승자 안내 문구**
```
최종 우승자 : pobi
```

**공동 우승자 안내 문구**
```
최종 우승자 : pobi, jun
```

**실행 결과 예시**
```
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 회수는 몇회인가요?
5

실행 결과
pobi : -
woni :
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---

pobi : ----
woni : ---
jun : ----

pobi : -----
woni : ----
jun : -----

최종 우승자 : pobi, jun
```

## Java - 프로그래밍 요구 사항

- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.**
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다.
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.**
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.

### 추가된 요구 사항

- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다.

### 라이브러리

- JDK에서 제공하는 Random 및 Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다.
- Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다.
- 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.

### 사용 예시

- 0에서 9까지의 정수 중 한 개의 정수 반환
```
Randoms.pickNumberInRange(0,9);
```
9 changes: 7 additions & 2 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package racingcar;

import racingcar.config.AppConfig;
import racingcar.controller.RacingGameController;

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

import racingcar.controller.RacingGameController;
import racingcar.service.RacingService;
import racingcar.util.RandomNumberGenerator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class AppConfig {
private final RacingGameController racingGameController;

public AppConfig() {
RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator();
InputView inputView = new InputView();
OutputView outputView = new OutputView();
RacingService racingService = new RacingService(randomNumberGenerator, outputView);
this.racingGameController = new RacingGameController(inputView, racingService);
}

public RacingGameController getRacingGameController() {
return racingGameController;
}
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.controller;

import racingcar.service.RacingService;
import racingcar.view.InputView;

import java.util.List;

public class RacingGameController {
private final InputView inputView;
private final RacingService racingService;

public RacingGameController(InputView inputView, RacingService racingService) {
this.inputView = inputView;
this.racingService = racingService;
}

public void run() {
List<String> carNames = inputView.getCarNames();
int attemptCount = inputView.getAttemptCount();

racingService.startRace(carNames, attemptCount);
}
}
33 changes: 33 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

도메인과 모델의 차이가 뭘까요.. MVC를 공부하며 모델을 이해했는데 도메인이 같은 역할로 사용되고 있는 거 같아서요..!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

저도 구현하면서 공부했는데 주 포인트는 비즈니스 로직의 포함여부입니다! 도메인 객체는 비즈니스 로직이 포함되어있는 일종의 개념단위?로 보시면 될 것 같아요! @KIM-GOING

모델 대신 도메인 객체를 사용한 이유는 자동차 객체 자체에 자동차의 이동도 같이포함시켜야 객체의 더 책임도 명확해지고, 유지보수도 쉬워질거라 판단했습니다. 물론 DTO방식으로 단순한 모델로 사용해도 되지만, 그럼 핵심동작인 이동같은 로직을 service에서 따로 처리해야하므로 자동차객체가 스스로 움직일 수 없어서 오히려 불편할 것 같다고 생각했습니다.

도메인, 도메인 객체, 모델 차이

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

import racingcar.util.RandomNumberGenerator;

public class Car {
private final String name;
private int position;
private final RandomNumberGenerator randomNumberGenerator;

public Car(String name, RandomNumberGenerator randomNumberGenerator) {
this.name = name;
this.position = 0;
this.randomNumberGenerator = randomNumberGenerator;
}

public void move() {
move(randomNumberGenerator.canMove()); // 기본적으로 랜덤한 이동
}

public void move(boolean canMove) {
if (canMove) {
position++;
}
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}
}
36 changes: 36 additions & 0 deletions src/main/java/racingcar/domain/Race.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.domain;

import racingcar.util.RandomNumberGenerator;

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

public class Race {
private final List<Car> cars;

public Race(List<String> carNames, RandomNumberGenerator randomNumberGenerator) {
this.cars = carNames.stream()
.map(name -> new Car(name, randomNumberGenerator))
.collect(Collectors.toList());
}

public void moveCars() {
cars.forEach(Car::move);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

확실히 스트림보단 보기 깔끔하네요
스트림은 가공, 변환할 때 쓰는 용도이니 단순 반복이라면 forEach를 쓰는게 좋아보여요

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

forEach를 처음 봐서 어떤건지 찾아봤는데 for문 보다 간단한 것 같아요! 새로운 것을 배워가요 감사합니다:)

}

public List<Car> getCars() {
return cars;
}

public List<String> getWinners() {
int maxPosition = cars.stream()
.mapToInt(Car::getPosition)
.max()
.orElse(0);

return cars.stream()
.filter(car -> car.getPosition() == maxPosition)
.map(Car::getName)
.collect(Collectors.toList());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는 for문 돌려서 maxPosition이랑 같은 값은 갖는 car를 찾았는데 filter로 간편하게 구할 수 있군요.. 배워갑니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 배워갑니다!..

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

import racingcar.domain.Race;
import racingcar.util.RandomNumberGenerator;
import racingcar.view.OutputView;

import java.util.List;
import java.util.stream.IntStream;

public class RacingService {
private final RandomNumberGenerator randomNumberGenerator;
private final OutputView outputView;

public RacingService(RandomNumberGenerator randomNumberGenerator, OutputView outputView) {
this.randomNumberGenerator = randomNumberGenerator;
this.outputView = outputView;
}

public void startRace(List<String> carNames, int attemptCount) {
Race race = new Race(carNames, randomNumberGenerator);
outputView.printStartMessage();
runRace(race, attemptCount);
outputView.printWinners(race.getWinners());
}

private void runRace(Race race, int attemptCount) {
IntStream.range(0, attemptCount)
.forEach(i -> {
race.moveCars();
outputView.printRaceStatus(race.getCars());
});
}
}
14 changes: 14 additions & 0 deletions src/main/java/racingcar/util/RandomNumberGenerator.java
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

랜덤값을 반환하는 파일을 따로 만드신 이유가 뭔가요?

Copy link
Copy Markdown
Author

@seulnan seulnan Feb 17, 2025

Choose a reason for hiding this comment

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

앗 책임분리를 하기위해서였습니다. (SRP적용) 랜덤값 생성은 자동차객체에 영향을 주는 것도 아니고, 레이싱 결과자체에 직접적인 영향을 주는것도 아닌 과정중의 하나라고 생각했습니다. 그래서 util로 분리하면 이 로직에 대한 테스트코드도 작성이 가능한데 시간부족이슈로 못했습니다.. @Regyung

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

import camp.nextstep.edu.missionutils.Randoms;

public class RandomNumberGenerator {
private static final int MIN = 0;
private static final int MAX = 9;
private static final int MOVE_THRESHOLD = 4;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MOVE_THRESHOLD를 변수로 만들어서 직관적으로 판단하기 더 좋아진 것 같습니다! 난슬님 코드를 처음 분석해봤는데 정말 많이 배워갑니다..

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

동의합니다. static과 final의 쓰임새도 이해해가고 있어요. 코드가 직관적이어 좋네요!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 상수화를 해서 보완을 해야겠네요! 저는 0, 9로만 하였는데 배워갑니다!...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 0,9로만 했었는데 배워가요! final도 처음 보는데 다음 미션때 필요하다면 써봐야겠어요!!


public boolean canMove() {
int randomValue = Randoms.pickNumberInRange(MIN, MAX);
return randomValue >= MOVE_THRESHOLD;
}
}
42 changes: 42 additions & 0 deletions src/main/java/racingcar/validator/InputValidator.java
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

에러 메세지를 Enum을 이용해서 정리하고 상수로 표현하는 것도 괜찮아 보여요!

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

import java.util.List;

public class InputValidator {
public static void validateCarNames(List<String> carNames) {
if (carNames.isEmpty()) {
throw new IllegalArgumentException("[ERROR] 자동차 이름을 입력해야 합니다.");
}
if (hasDuplicateNames(carNames)) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 중복될 수 없습니다.");
}
if (hasInvalidLength(carNames)) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 5자 이하만 가능합니다.");
}
if (carNames.stream().anyMatch(name -> !name.matches("^[a-zA-Z0-9]*$"))) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 영어 또는 숫자만 가능합니다.");
}
}

public static void validateAttemptCount(String input) {
if (!isNumeric(input)) {
throw new IllegalArgumentException("[ERROR] 유효한 숫자를 입력해야 합니다.");
}
int count = Integer.parseInt(input);
if (count <= 0) {
throw new IllegalArgumentException("[ERROR] 시도 횟수는 1 이상이어야 합니다.");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이렇게 예외케이스 메소드를 구분해서 하니까 가독성이 좋은 것 같아요

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

동의합니다. 많은 예외를 고려하고, 이를 가독성있는 코드로 작성하는 것이 대단하네요!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는 예외를 많이 고려하지 않았는데 이것 또한 많이 배워갑니다!...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

자동차 이름 중복은 생각 못했었는데 또 배워갑니다..!

private static boolean hasDuplicateNames(List<String> carNames) {
return carNames.size() != carNames.stream().distinct().count();
}

private static boolean hasInvalidLength(List<String> carNames) {
return carNames.stream().anyMatch(name -> name.length() > 5);
}

private static boolean isNumeric(String input) {
return input.matches("\\d+");
}
}
28 changes: 28 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package racingcar.view;

import camp.nextstep.edu.missionutils.Console;
import racingcar.validator.InputValidator;

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

public class InputView {
public List<String> getCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
String input = Console.readLine();
List<String> carNames = Arrays.stream(input.split(","))
.map(String::trim)
.filter(name -> !name.isEmpty()) // 빈 문자열 제거
.collect(Collectors.toList());
InputValidator.validateCarNames(carNames);
return carNames;
}

public int getAttemptCount() {
System.out.println("시도할 회수는 몇회인가요?");
String input = Console.readLine();
InputValidator.validateAttemptCount(input);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

validator를 만든게 이렇게 가독성을 올려주네요 ㄷㄷ

return Integer.parseInt(input);
}
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.view;

import java.util.stream.Collectors;
import racingcar.domain.Car;

import java.util.List;

public class OutputView {
public void printStartMessage() {
System.out.println("\n실행 결과");
}

public void printRaceStatus(List<Car> cars) {
String raceStatus = cars.stream()
.map(car -> car.getName() + " : " + "-".repeat(Math.max(0, car.getPosition())))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'-'를 반복할 때 그냥 getPosition()을 하는 게 아니라 Math.max(0, car.getPosition())을 하신 이유가 있으신가요?

Copy link
Copy Markdown
Author

@seulnan seulnan Feb 17, 2025

Choose a reason for hiding this comment

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

음..그저..범위를 제한설정해둬서 getPosition값이 음수일 경우라면 0으로 처리되게 하기위한 안전장치로 심어뒀습니다 @Regyung

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

음수일 경우를 신경써주신 이유가 따로 있으실까요?

Copy link
Copy Markdown
Author

@seulnan seulnan Feb 18, 2025

Choose a reason for hiding this comment

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

물론 음수일 경우를 신경쓴 의도가 있긴하지만, 정확히는 0부터 position값이 시작한다는 걸 명시화함으로써 가독성을 올리려는 목적이었습니다! @szoon2426

.collect(Collectors.joining("\n"));

System.out.println(raceStatus + "\n");
}

public void printWinners(List<String> winners) {
System.out.println("최종 우승자 : " + String.join(", ", winners));
}
}
Loading