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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
# java-racingcar-precourse

## 과제 후기
이번 미션은 솔직히 크게 어렵진 않았는데, 그래도 다른 사람들처럼 클래스를 나누고 역할을 분리해서 구현해봤습니다.
확장성도 좀 고려하면서 구조를 짜봤고 테스트 코드도 직접 활용해보고 싶어서 GPT한테 진행 순서를 만들어 달라고 한 다음 제 스타일에 맞게 수정하면서 진행을 해봤습니다.
그렇게 하다 보니 기능 단위로 커밋하는 흐름이 좀 익숙해졌고 테스트를 통해 동작을 확인하면서 구현하는 게 확실히 편하다는 걸 느낀 미션이었던 거 같습니다~~

## 진행 순서 체크리스트
1. [x] docs(readme): 기능 목록/진행 순서 작성
2. [x] feat(domain): Car 생성 및 이름 검증(1~5자)
3. [x] test: Car 이름 검증 테스트(빈값/공백/5자 초과)
4. [x] feat(policy): MovePolicy(기본 n≥4) 추가
5. [x] test: MovePolicy 경계 테스트(3 정지, 4 전진)
6. [x] feat(io): InputView—자동차 이름 입력/파싱/검증
7. [x] feat(io): InputView—시도 횟수 입력/검증(정수, ≥1)
8. [x] feat(domain): RacingGame—시도 횟수만큼 진행(랜덤 주입)
9. [x] feat(io): OutputView—라운드별 `이름 : ---` 출력
10. [x] feat(domain): Winners—최대 위치 기반 우승자 목록 계산
11. [x] feat(io): 최종 우승자 출력(단독/복수)
12. [x] fix: 잘못된 입력 시 IllegalArgumentException 던지고 main에서 메시지 출력 후 종료
13. [x] test: 통합—기본 시나리오(테스트 제공값 통과: `pobi,woni`/`1`)
14. [x] refactor: 메서드로 분리해 가독성 높이기

## 기능 요구 요약
- 이름(≤5자, 쉼표 구분)과 시도 횟수 입력
- 0~9 무작위 수가 **4 이상**이면 전진
- 라운드별 결과 및 최종 우승자 출력(복수 가능)
- 잘못된 입력은 `IllegalArgumentException` 후 종료

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

import java.util.List;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
try {
List<Car> cars = InputView.readCars();
int attempts = InputView.readAttempts();

System.out.println();
System.out.println("실행 결과");

RacingGame game = createGame(cars);
play(game, cars, attempts);
printWinners(cars);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
throw e;
}
}

private static RacingGame createGame(List<Car> cars) {
MovePolicy policy = new DefaultMovePolicy();
NumberGenerator generator = new MissionUtilsGenerator();
return new RacingGame(cars, policy, generator);
}

private static void play(RacingGame game, List<Car> cars, int attempts) {
for (int i = 0; i < attempts; i++) {
game.step();
OutputView.printRound(cars);
}
}

private static void printWinners(List<Car> cars) {
List<String> winners = Winners.of(cars);
OutputView.printWinners(winners);
}
Comment on lines +23 to 39

Choose a reason for hiding this comment

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

레이싱 카 게임 실행 흐름을 제어하는 클래스를 하나 만들고, main에서는 단순 진입점만 제공하는 것은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

다른 분들 코드를 보면서 Controller로 흐름을 제어하는 구조가 훨씬 명확하다는 걸 느꼈습니다...
말씀해주신 대로 이번 주 미션에서는 이런 구조적인 부분을 참고해서 적용해보겠습니다!

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

import java.util.Objects;

public final class Car {
private static final int MIN_NAME_LENGTH = 1;
private static final int MAX_NAME_LENGTH = 5;

private final String name;
private int position = 0;

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

private String validateName(final String raw) {
if (raw == null) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 null일 수 없습니다.");
}
final String trimmed = raw.trim();
final int len = trimmed.length();
if (len < MIN_NAME_LENGTH || len > MAX_NAME_LENGTH) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 1~5자여야 합니다.");
}
return trimmed;
}

public void moveForward() {
position++;
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}

@Override
public boolean equals(Object o) {

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.

다음 미션에서는 이런 데이터 일관성 부분도 좀 더 신경 써보겠습니다!

if (this == o) return true;
if (!(o instanceof Car)) return false;
Car car = (Car) o;
return position == car.position && name.equals(car.name);
}

@Override
public int hashCode() {
return Objects.hash(name, position);
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/DefaultMovePolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar;

public final class DefaultMovePolicy implements MovePolicy {
private static final int THRESHOLD = 4;

@Override
public boolean canMove(final int number) {
return number >= THRESHOLD;
}
}
53 changes: 53 additions & 0 deletions src/main/java/racingcar/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package racingcar;

import camp.nextstep.edu.missionutils.Console;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public final class InputView {
private InputView() {}

public static List<Car> readCars() {
System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
String line = Console.readLine();
return parseCarNames(line);
}

// 파싱(검증 포함)
public static List<Car> parseCarNames(final String line) {
if (line == null || line.trim().isEmpty()) {
throw new IllegalArgumentException("[ERROR] 자동차 이름을 입력해야 합니다.");
}
String[] tokens = Arrays.stream(line.split(","))
.map(String::trim)
.toArray(String[]::new);

List<Car> cars = new ArrayList<>();
for (String name : tokens) {
cars.add(new Car(name)); // Car가 1~5자 검증
}
if (cars.isEmpty()) {
throw new IllegalArgumentException("[ERROR] 유효한 자동차 이름이 없습니다.");
}
Comment on lines +31 to +33
Copy link

@JunHyung1206 JunHyung1206 Oct 27, 2025

Choose a reason for hiding this comment

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

만약 위의 반복문에서 오류를 throw를 한다고 하면 해당 코드는 필요 없는 것 같습니다.

return cars;
}

// 시도 횟수 읽기/파싱
public static int readAttempts() {
System.out.println("시도할 횟수는 몇 회인가요?");
String line = Console.readLine();
return parseAttempts(line);
}

public static int parseAttempts(final String line) {
try {
int n = Integer.parseInt(line.trim());
if (n < 1) throw new NumberFormatException();
return n;
} catch (Exception e) {
throw new IllegalArgumentException("[ERROR] 시도 횟수는 1 이상의 정수여야 합니다.");
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/MissionUtilsGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar;

import camp.nextstep.edu.missionutils.Randoms;

public final class MissionUtilsGenerator implements NumberGenerator {
@Override
public int next0to9() {
return Randoms.pickNumberInRange(0, 9);
}
}
5 changes: 5 additions & 0 deletions src/main/java/racingcar/MovePolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar;

public interface MovePolicy {
boolean canMove(int number);
}
5 changes: 5 additions & 0 deletions src/main/java/racingcar/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar;

public interface NumberGenerator {
int next0to9();

Choose a reason for hiding this comment

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

0~9 사이의 난수를 발생시킨다는 함수명 같은데 이를 굳이 인터페이스로 만든 이유가 궁금합니다.

Choose a reason for hiding this comment

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

그와 별개로 이를 주입해서, 난수를 제거하고 테스트 할땐 용이할 것 같습니다.

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

import java.util.List;

public final class OutputView {
private OutputView() {}

public static void printRound(final List<Car> cars) {
for (Car c : cars) {
System.out.println(c.getName() + " : " + "-".repeat(c.getPosition()));
}
System.out.println();
}

public static void printWinners(final List<String> winners) {
System.out.println("최종 우승자 : " + String.join(", ", winners));
}
}
38 changes: 38 additions & 0 deletions src/main/java/racingcar/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package racingcar;

import java.util.List;

public final class RacingGame {
private final List<Car> cars;
private final MovePolicy movePolicy;
private final NumberGenerator generator;

public RacingGame(final List<Car> cars,
final MovePolicy movePolicy,
final NumberGenerator generator) {
this.cars = cars;
this.movePolicy = movePolicy;
this.generator = generator;
}

// 한 라운드 진행
public void step() {
for (Car car : cars) {
int n = generator.next0to9();
if (movePolicy.canMove(n)) {
car.moveForward();
}
}
}

// 지정된 횟수 만큼만 진행
public void run(final int attempts) {
for (int i = 0; i < attempts; i++) {
step();
}
}

public List<Car> getCars() {
return cars;
}
}
28 changes: 28 additions & 0 deletions src/main/java/racingcar/Winners.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package racingcar;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public final class Winners {
private Winners() {}

public static List<String> of(final List<Car> cars) {
if (cars == null || cars.isEmpty()) {
throw new IllegalArgumentException("[ERROR] 우승자를 계산할 자동차가 없습니다.");

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.

말씀 듣고 보니 중복 검증이 InputView와 Winners 양쪽에서 일어나네요....
입력 시점에서만 검증하고, 도메인에서는 불필요한 중복을 제거하도록 개선해보겠습니다

}

int max = cars.stream()
.max(Comparator.comparingInt(Car::getPosition))
.map(Car::getPosition)
.orElse(0);

List<String> winners = new ArrayList<>();
for (Car car : cars) {
if (car.getPosition() == max) {
winners.add(car.getName());
}
}
return winners;
}
}
43 changes: 43 additions & 0 deletions src/test/java/racingcar/CarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package racingcar;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

class CarTest {

@Test
@DisplayName("유효한 이름(1~5자)으로 Car 생성 성공")
void create_with_valid_name() {
Car a = new Car("hsuu");
Car b = new Car("suu");
assertThat(a.getName()).isEqualTo("hsuu");
assertThat(b.getName()).isEqualTo("suu");
assertThat(a.getPosition()).isZero();
}

@Test
@DisplayName("이름이 비거나 공백만인 경우 예외 발생")
void create_with_blank_name_throws() {
assertThatThrownBy(() -> new Car(""))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new Car(" "))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
@DisplayName("이름이 5자를 초과하면 예외 발생")
void create_with_too_long_name_throws() {
assertThatThrownBy(() -> new Car("hsmygit")) // 7글자
.isInstanceOf(IllegalArgumentException.class);
}

@Test
@DisplayName("moveForward 호출 시 위치가 1 증가")
void move_forward_increments_position() {
Car car = new Car("pobi");
car.moveForward();
assertThat(car.getPosition()).isEqualTo(1);
}
}
49 changes: 49 additions & 0 deletions src/test/java/racingcar/IntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package racingcar;

import camp.nextstep.edu.missionutils.test.NsTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest;
import static org.assertj.core.api.Assertions.assertThat;

public class IntegrationTest extends NsTest {

@Test
@DisplayName("기존에 테스트 코드에 있던 시나리오: 두 차량, 1회 시도, 랜덤 4/3 -> pobi만 전진, pobi 우승")
void provided_like_scenario() {
assertRandomNumberInRangeTest(() -> {
run("pobi,woni", "1");
String out = output();
assertThat(out).contains("실행 결과");
assertThat(out).contains("pobi : -");
assertThat(out).contains("woni : ");
assertThat(out).contains("최종 우승자 : pobi");
},
4, 3
);
}

@Test
@DisplayName("세 차량, 2회 시도, 동점 우승자 출력 확인")
void three_cars_two_steps_tie_winners() {
// 라운드1: pobi(4->전진), woni(4->전진), jun(3->정지)
// 라운드2: pobi(3->정지), woni(4->전진), jun(4->전진)
assertRandomNumberInRangeTest(() -> {
run("pobi,woni,jun", "2");
String out = output();
// 라운드별 출력 존재 여부만 간단 검증
assertThat(out).contains("pobi : -");
// 최종: woni(2), pobi(1), jun(1) → 우승자: woni
assertThat(out).contains("최종 우승자 : woni");
},
4, 4, 3, // 라운드1: pobi, woni, jun
3, 4, 4 // 라운드2: pobi, woni, jun
);
}

@Override
public void runMain() {
Application.main(new String[]{});
}
}
Loading