From 11b5541b7d7d43f41d44cb5b45e5248a58b07869 Mon Sep 17 00:00:00 2001 From: kdongd Date: Wed, 28 Jan 2026 07:46:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?-=20=EC=9E=90=EB=8F=99=EC=B0=A8=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=9E=85=EB=A0=A5:=20=EC=89=BC=ED=91=9C(,)=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=EB=B6=84,=20?= =?UTF-8?q?=EA=B0=81=20=EC=9E=90=EB=8F=99=EC=B0=A8=EC=97=90=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B6=80=EC=97=AC,=205=EA=B8=80=EC=9E=90=20?= =?UTF-8?q?=EC=B4=88=EA=B3=BC=20=EB=B6=88=EA=B0=80=20-=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EA=B2=B0=EA=B3=BC=20=EC=B6=9C=EB=A0=A5:=20?= =?UTF-8?q?=EC=A0=84=EC=A7=84=20=ED=95=98=EB=8A=94=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=B6=9C=EB=A0=A5=20=EC=8B=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EB=8F=84=20=EA=B0=99=EC=9D=B4=20=EC=B6=9C=EB=A0=A5=20-=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5:=20=EA=B0=80=EC=9E=A5=20=EB=A9=80=EB=A6=AC=EA=B0=84?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=EC=B0=A8=20=ED=91=9C=EC=8B=9C,=20?= =?UTF-8?q?=EB=8B=A4=EC=88=98=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=ED=91=9C=EC=8B=9C=20-=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Car Class : 자동차 이름 5글자 초과 시 예외 처리 --- README.md | 11 +++--- src/main/java/carrace/Application.java | 5 +-- .../java/carrace/controller/RaceGame.java | 3 ++ src/main/java/carrace/domain/car/Car.java | 17 ++++++++++ .../java/carrace/domain/car/CarCreator.java | 18 ++++++---- src/main/java/carrace/domain/race/Race.java | 20 +++++++++++ .../java/carrace/service/InputService.java | 16 +++++++++ .../java/carrace/view/CarRaceInputView.java | 5 +++ .../java/carrace/view/CarRaceOutputView.java | 14 +++++++- .../carrace/domain/car/CarCreatorTest.java | 34 +++++++++++++++++++ src/test/java/carrace/domain/car/CarTest.java | 24 ++++++++++--- .../java/carrace/domain/race/RaceTest.java | 31 ++++++++++++++++- .../java/carrace/fixture/CarsFixture.java | 14 ++++---- .../java/carrace/fixture/StubInputView.java | 13 +++++++ .../carrace/service/InputServiceTest.java | 18 ++++++++++ 15 files changed, 215 insertions(+), 28 deletions(-) create mode 100644 src/test/java/carrace/domain/car/CarCreatorTest.java diff --git a/README.md b/README.md index c550c4c2a09..faa058790e9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # 자동차 경주 게임 -## 진행 방법 -* 자동차 경주 게임 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. -## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) \ No newline at end of file +## 구현할 기능 목록 +- 자동차 이름 입력: 쉼표(,) 기준으로 구분, 각 자동차에 이름 부여, 5글자 초과 불가 +- 실행 결과 출력: 전진 하는 자동차 출력 시 이름도 같이 출력 +- 최종 우승자 출력: 가장 멀리간 자동차 표시, 다수일 경우 모두 표시 diff --git a/src/main/java/carrace/Application.java b/src/main/java/carrace/Application.java index cdcaaaaf53c..1b04af5201b 100644 --- a/src/main/java/carrace/Application.java +++ b/src/main/java/carrace/Application.java @@ -21,14 +21,15 @@ public static void main(String[] args) { CarRaceOutputView outputView = new CarRaceOutputView(); InputService inputService = new InputService(inputView); - int carCount = inputService.readCarCount(); + String carName = inputService.readCarNames(); int rounds = inputService.readRounds(); - List cars = CarCreator.create(carCount); + List cars = CarCreator.create(carName); MoveCondition condition = new RandomMoveCondition(); Race race = new Race(cars); RaceGame game = new RaceGame(race, condition, outputView); game.play(rounds); + } } diff --git a/src/main/java/carrace/controller/RaceGame.java b/src/main/java/carrace/controller/RaceGame.java index 024a1359c80..44faeb53ef9 100644 --- a/src/main/java/carrace/controller/RaceGame.java +++ b/src/main/java/carrace/controller/RaceGame.java @@ -23,5 +23,8 @@ public void play(int rounds) { race.moveOnce(condition); outputView.printResult(race.getCars()); } + + outputView.printWinners(race.getWinners()); } + } diff --git a/src/main/java/carrace/domain/car/Car.java b/src/main/java/carrace/domain/car/Car.java index 94dd39d7952..04f1cd3505e 100644 --- a/src/main/java/carrace/domain/car/Car.java +++ b/src/main/java/carrace/domain/car/Car.java @@ -3,8 +3,25 @@ import carrace.domain.move.MoveCondition; public class Car { + private static final int MAX_NAME_LENGTH = 5; private Position position = new Position(); + private final String carName; + + public Car(String carName) { + validateName(carName); + this.carName = carName; + } + + private void validateName(String carName) { + if (carName == null || carName.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5글자를 초과할 수 없습니다."); + } + } + + public String getCarName() { + return carName; + } public void move(MoveCondition condition) { if (condition.canMove()) { diff --git a/src/main/java/carrace/domain/car/CarCreator.java b/src/main/java/carrace/domain/car/CarCreator.java index f47661c5e5f..cfb31b1ab25 100644 --- a/src/main/java/carrace/domain/car/CarCreator.java +++ b/src/main/java/carrace/domain/car/CarCreator.java @@ -1,13 +1,19 @@ package carrace.domain.car; + +import java.util.ArrayList; import java.util.List; -import java.util.stream.IntStream; public class CarCreator { - public static List create(int count) { - return IntStream.range(0,count) //정수 스트림 생성, 0부터 count -1까지 숫자를 차례대로 흘려보냄, ex) count = 3, stream = 0 -> 1 -> 2 - .mapToObj(i -> new Car()) //스트림에 흐르는 각 숫자 i를 Car 객체로 변환 - .toList(); // 스트림을 List로 수집, 이 순간에만 실제 실행됨(최종 연산) - } // ex) 결과 : List cars = [new Car(), new Car(), new Car()] + public static List create(String input) { + String[] names = input.split(","); + List cars = new ArrayList<>(); + + for (int i = 0; i < names.length; i++) { + String name = names[i].trim(); + cars.add(new Car(name)); + } + return cars; + } } diff --git a/src/main/java/carrace/domain/race/Race.java b/src/main/java/carrace/domain/race/Race.java index cb393f5649c..f438809e937 100644 --- a/src/main/java/carrace/domain/race/Race.java +++ b/src/main/java/carrace/domain/race/Race.java @@ -3,6 +3,7 @@ import carrace.domain.car.Car; import carrace.domain.move.MoveCondition; +import java.util.ArrayList; import java.util.List; public class Race { @@ -19,10 +20,29 @@ public void moveOnce(MoveCondition condition) { } } + public List getWinners() { + int maxPosition = 0; + + for (Car car : cars) { + if (car.getPosition() > maxPosition) { + maxPosition = car.getPosition(); + } + } + + List winners = new ArrayList<>(); + for (Car car : cars) { + if (car.getPosition() == maxPosition) { + winners.add(car); + } + } + return winners; + } + //내부 상태 보호를 위해 자동차 목록을 그대로 반환하지 않고 //읽기 전용 리스트(unmodifiableList)를 반환하도록 수정했습니다. public List getCars() { return List.copyOf(cars); } + } diff --git a/src/main/java/carrace/service/InputService.java b/src/main/java/carrace/service/InputService.java index 195905b99e2..9cf2ea52c18 100644 --- a/src/main/java/carrace/service/InputService.java +++ b/src/main/java/carrace/service/InputService.java @@ -10,6 +10,12 @@ public InputService(CarRaceInputView inputView) { this.inputView = inputView; } + public String readCarNames() { + String input = inputView.readCarNames(); + validateCarNames(input); + return input; + } + public int readCarCount() { int value = parseInt( inputView.readCarCount(), @@ -41,4 +47,14 @@ private int parseInt(String input, String errorMessage) { throw new IllegalArgumentException(errorMessage); } } + + private void validateCarNames(String input) { + String[] names = input.split(","); + for (String name : names) { + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 5) { + throw new IllegalArgumentException("자동차 이름은 5자를 초과할 수 없습니다."); + } + } + } } diff --git a/src/main/java/carrace/view/CarRaceInputView.java b/src/main/java/carrace/view/CarRaceInputView.java index 4d10a1815fb..171d54439e9 100644 --- a/src/main/java/carrace/view/CarRaceInputView.java +++ b/src/main/java/carrace/view/CarRaceInputView.java @@ -6,6 +6,11 @@ public class CarRaceInputView { private final Scanner scanner = new Scanner(System.in); + + public String readCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + return scanner.nextLine(); + } public String readCarCount() { System.out.println("자동차 대수는 몇 대인가요?"); return scanner.nextLine(); diff --git a/src/main/java/carrace/view/CarRaceOutputView.java b/src/main/java/carrace/view/CarRaceOutputView.java index df19efda111..2ef05d930cd 100644 --- a/src/main/java/carrace/view/CarRaceOutputView.java +++ b/src/main/java/carrace/view/CarRaceOutputView.java @@ -3,6 +3,7 @@ import carrace.domain.car.Car; import java.util.List; +import java.util.stream.Collectors; public class CarRaceOutputView { @@ -13,9 +14,20 @@ public void printStartMessage() { public void printResult(List cars) { for (Car car : cars) { - System.out.println("-".repeat(car.getPosition())); + System.out.println(car.getCarName() + " : " + "-".repeat(car.getPosition())); } System.out.println(); + + } + + public void printWinners(List winners) { + String winnerNames = winners.stream() + .map(Car::getCarName) + .collect(Collectors.joining(", ")); + + System.out.println(winnerNames + "가 최종 우승했습니다."); } } + + diff --git a/src/test/java/carrace/domain/car/CarCreatorTest.java b/src/test/java/carrace/domain/car/CarCreatorTest.java new file mode 100644 index 00000000000..817605c6b35 --- /dev/null +++ b/src/test/java/carrace/domain/car/CarCreatorTest.java @@ -0,0 +1,34 @@ +package carrace.domain.car; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CarCreatorTest { + + @Test + @DisplayName("쉼표 기준으로 자동차 생성") + void createCars_byComma() { + List cars = CarCreator.create("kkk,ddd,uuu"); + + assertEquals(3, cars.size()); + assertEquals("kkk", cars.get(0).getCarName()); + assertEquals("ddd", cars.get(1).getCarName()); + assertEquals("uuu", cars.get(2).getCarName()); + } + + @Test + @DisplayName("자동차 이름 앞뒤 공백 제거") + void createCars_trimNames() { + List cars = CarCreator.create("kkk,ddd,uuu"); + + assertEquals(3, cars.size()); + assertEquals("kkk", cars.get(0).getCarName()); + assertEquals("ddd", cars.get(1).getCarName()); + assertEquals("uuu", cars.get(2).getCarName()); + } +} + diff --git a/src/test/java/carrace/domain/car/CarTest.java b/src/test/java/carrace/domain/car/CarTest.java index 64b2689fec8..7ea6f229a8e 100644 --- a/src/test/java/carrace/domain/car/CarTest.java +++ b/src/test/java/carrace/domain/car/CarTest.java @@ -5,20 +5,21 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; public class CarTest { @Test @DisplayName("새 자동차는 기본 위치가 0") void newCar_defaultPosition_zero() { - Car car = new Car(); + Car car = new Car("kdu"); assertThat(car.getPosition()).isEqualTo(0); } @Test @DisplayName("이동 조건 충족 시 항상 위치는 1 증가") void move_alwaysMove_incrementsPosition() { - Car car = new Car(); + Car car = new Car("kdu"); car.move(MoveConditions.alwaysMove()); assertThat(car.getPosition()).isEqualTo(1); } @@ -26,7 +27,7 @@ void move_alwaysMove_incrementsPosition() { @Test @DisplayName("이동하지 않는 조건에서 자동차 위치는 그대로다") void move_neverMove_positionUnchanged() { - Car car = new Car(); + Car car = new Car("kdu"); car.move(MoveConditions.neverMove()); assertThat(car.getPosition()).isEqualTo(0); } @@ -34,9 +35,24 @@ void move_neverMove_positionUnchanged() { @Test @DisplayName("연속 이동 시 위치가 누적된다") void move_multipleTimes_positionAccumulates() { - Car car = new Car(); + Car car = new Car("kdu"); car.move(MoveConditions.alwaysMove()); car.move(MoveConditions.alwaysMove()); assertThat(car.getPosition()).isEqualTo(2); } + + @Test + @DisplayName("자동차는 이름을 가진다") + void car_has_name() { + Car car = new Car("kdu"); + assertThat(car.getCarName()).isEqualTo("kdu"); + } + + @Test + @DisplayName("이름이 5글자 초과면 예외") + void name_too_long_throws() { + assertThatThrownBy(() -> new Car("abcdef")) + .isInstanceOf(IllegalArgumentException.class); + } + } diff --git a/src/test/java/carrace/domain/race/RaceTest.java b/src/test/java/carrace/domain/race/RaceTest.java index d1546c22664..bc788c981f7 100644 --- a/src/test/java/carrace/domain/race/RaceTest.java +++ b/src/test/java/carrace/domain/race/RaceTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -46,9 +47,37 @@ void getCars_returnsUnmodifiableList() { assertThat(raceCars).containsExactlyElementsOf(cars); // 내부 리스트 수정 시도 시 예외 발생 확인 - assertThatThrownBy(() -> raceCars.add(new Car())) + assertThatThrownBy(() -> raceCars.add(new Car("kdu"))) .isInstanceOf(UnsupportedOperationException.class); } + + @Test + @DisplayName("우승자 1명일 때 getWinners 반환") + void getWinners_single() { + List cars = CarsFixture.threeCars(); + Race race = new Race(cars); + + cars.get(0).move(MoveConditions.alwaysMove()); + + List winners = race.getWinners(); + assertThat(winners).hasSize(1); + assertThat(winners.get(0)).isSameAs(cars.get(0)); + } + + @Test + @DisplayName("우승자가 여러명일 때 getWinners 반환") + void getWinners_multiple() { + List cars = CarsFixture.threeCars(); + Race race = new Race(cars); + + cars.get(0).move(MoveConditions.alwaysMove()); + cars.get(1).move(MoveConditions.alwaysMove()); + + List winners = race.getWinners(); + + assertThat(winners).hasSize(2); + assertThat(winners).contains(cars.get(0),cars.get(1)); + } } diff --git a/src/test/java/carrace/fixture/CarsFixture.java b/src/test/java/carrace/fixture/CarsFixture.java index 9daaa62acba..881d803a303 100644 --- a/src/test/java/carrace/fixture/CarsFixture.java +++ b/src/test/java/carrace/fixture/CarsFixture.java @@ -1,19 +1,19 @@ package carrace.fixture; import carrace.domain.car.Car; -import carrace.domain.car.CarCreator; import java.util.List; public class CarsFixture { - // 자동차 3대를 만드는 Fixture public static List threeCars() { - return CarCreator.create(3); + return List.of( + new Car("car1"), + new Car("car2"), + new Car("car3") + ); } - // 자동차 n대를 만드는 Fixture - public static List nCars(int n) { - return CarCreator.create(n); - } + } + diff --git a/src/test/java/carrace/fixture/StubInputView.java b/src/test/java/carrace/fixture/StubInputView.java index 79d8b4ffa20..2af5bfd7bfa 100644 --- a/src/test/java/carrace/fixture/StubInputView.java +++ b/src/test/java/carrace/fixture/StubInputView.java @@ -4,10 +4,18 @@ public class StubInputView extends CarRaceInputView { + private final String carName; private final String carCount; private final String rounds; public StubInputView(String carCount, String rounds) { + this.carName = ""; + this.carCount = carCount; + this.rounds = rounds; + } + + public StubInputView(String carName, String carCount, String rounds) { + this.carName = carName; this.carCount = carCount; this.rounds = rounds; } @@ -21,4 +29,9 @@ public String readCarCount() { public String readMoveCount() { return rounds; } + + @Override + public String readCarNames() { + return carName; + } } diff --git a/src/test/java/carrace/service/InputServiceTest.java b/src/test/java/carrace/service/InputServiceTest.java index eca8e00d738..c3bf5bb8e0b 100644 --- a/src/test/java/carrace/service/InputServiceTest.java +++ b/src/test/java/carrace/service/InputServiceTest.java @@ -28,4 +28,22 @@ void roundsNegativeThrows() { InputService service = new InputService(new StubInputView("3", "-1")); assertThrows(IllegalArgumentException.class, service::readRounds); } + + @Test + @DisplayName("자동차 이름이 5글자 초과이면 예외 발생") + void carNameTooLongThrows() { + StubInputView inputView = new StubInputView("TOOLONG","3"); + InputService service = new InputService(inputView); + + assertThrows(IllegalArgumentException.class, service::readCarNames); + } + + @Test + @DisplayName("자동차 이름 공백시 예외 발생") + void carNameBlankThrows() { + StubInputView inputView = new StubInputView(" , , ", "3"); + InputService service = new InputService(inputView); + + assertThrows(IllegalArgumentException.class, service::readCarNames); + } } From 4d546ad3a1939dbcec9f2f8e7eb11307297a44c1 Mon Sep 17 00:00:00 2001 From: kdongd <162573409+kdongd@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:11:14 +0900 Subject: [PATCH 2/4] Revise README for Step 2 implementation details Updated the README to include detailed implementation features and class responsibilities for the car racing game in Step 2. --- README.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index faa058790e9..47faa50d80d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,134 @@ -# 자동차 경주 게임 +# 자동차 경주 게임 - Step2 -## 구현할 기능 목록 -- 자동차 이름 입력: 쉼표(,) 기준으로 구분, 각 자동차에 이름 부여, 5글자 초과 불가 -- 실행 결과 출력: 전진 하는 자동차 출력 시 이름도 같이 출력 -- 최종 우승자 출력: 가장 멀리간 자동차 표시, 다수일 경우 모두 표시 +--- + +## Step2 구현 기능 목록 (요구사항 기반) + +- 자동차 이름 입력 + - 쉼표(,) 기준으로 이름을 구분한다. + - 각 자동차에 이름을 부여한다. + - 자동차 이름은 5글자 초과 불가 (예외 발생) + +- 실행 결과 출력 + - 라운드별 전진 결과 출력 시 자동차 **이름과 전진 표시**를 함께 출력한다. + +- 최종 우승자 출력 + - 가장 멀리 간 자동차(들)를 우승자로 출력한다. + - 동점일 경우 우승자를 모두 출력한다. + +--- + +## 패키지/클래스별 구현 내용 (작업 목록) + +### 1) 실행/흐름 제어 + +#### `carrace.Application` +- 애플리케이션 시작 지점 +- 입력 → 레이스 진행 → 결과 출력까지의 전체 흐름을 연결한다. + +#### `carrace.controller.RaceGame` +- 게임 진행(컨트롤) 책임 +- 입력값을 받아 도메인 객체를 생성하고, 레이스를 수행한 뒤 출력 뷰로 결과를 전달한다. + +--- + +### 2) 도메인 - 자동차 + +#### `carrace.domain.car.Car` +- **자동차 한 대의 상태(위치)와 행위(이동)**를 관리한다. +- `Car(String carName)` + - 자동차 생성 시 이름을 검증한다. + - **이름이 5글자 초과면 `IllegalArgumentException` 발생** (유효하지 않은 자동차는 생성되지 않도록 도메인 불변조건 보장) +- `move(MoveCondition condition)` + - 이동 조건이 참이면 위치를 한 칸 전진시킨다. +- `getPosition()` + - 현재 위치 값을 반환한다. +- `getCarName()` + - 자동차 이름을 반환한다. + +#### `carrace.domain.car.CarCreator` +- 입력(문자열)로부터 자동차 목록을 생성하는 책임 +- 쉼표로 구분된 이름들을 기반으로 `Car` 객체들을 생성한다. +- (이름 trim/분리 등 “생성에 필요한 전처리”가 있다면 이 클래스에서 수행) + +--- + +### 3) 도메인 - 레이스 + +#### `carrace.domain.race.Race` +- **자동차 목록을 가지고 레이스 진행/우승자 계산**을 담당한다. +- 라운드 진행 결과를 기반으로 최종 우승자를 계산한다. +- 동점(여러 명 우승) 상황을 고려해 우승자 목록을 반환/출력할 수 있도록 구현한다. + +--- + +### 4) 서비스 - 입력 처리 + +#### `carrace.service.InputService` +- `CarRaceInputView`로부터 입력을 읽어오고, 숫자 변환 및 기본 검증을 수행한다. +- `readCarNames()` + - 자동차 이름 입력을 받는다. + - 쉼표 기준으로 분리 후 각 요소를 trim 처리한다. + - 빈 이름이 포함되면 예외를 발생시킨다. +- `readCarCount()` + - 자동차 대수 입력을 숫자로 파싱한다. + - 숫자가 아니면 예외를 발생시킨다. + - 1 미만이면 예외를 발생시킨다. +- `readRounds()` + - 시도 횟수 입력을 숫자로 파싱한다. + - 숫자가 아니면 예외를 발생시킨다. + - 1 미만이면 예외를 발생시킨다. + +--- + +### 5) View - 입출력 + +#### `carrace.view.CarRaceInputView` +- 사용자 입력을 담당한다. +- 자동차 이름 / 자동차 대수 / 시도 횟수를 입력받는다. + +#### `carrace.view.CarRaceOutputView` +- 실행 결과 출력을 담당한다. +- 라운드별 실행 결과를 자동차 이름과 함께 출력한다. +- 최종 우승자를 출력한다(동점자 포함). + +--- + +## 테스트 목록 (클래스별 / 검증 로직 설명) + +### 1) 자동차 도메인 테스트 + +#### `src/test/java/carrace/domain/car/CarTest` +- 자동차의 이동 로직이 조건에 따라 정확히 동작하는지 검증한다. +- 자동차 이름 제약(5글자 초과 불가) 검증을 테스트한다. + +#### `src/test/java/carrace/domain/car/CarCreatorTest` +- 쉼표로 구분된 자동차 이름 입력이 자동차 객체 목록으로 정상 생성되는지 검증한다. +- 이름 trim/분리 로직이 기대한 대로 동작하는지 검증한다. + +--- + +### 2) 레이스 도메인 테스트 + +#### `src/test/java/carrace/domain/race/RaceTest` +- 우승자가 1명인 경우 우승자 계산이 정확한지 검증한다. +- 동점(우승자 다수) 상황에서 우승자들이 모두 반환/출력 대상이 되는지 검증한다. + +--- + +### 3) 입력 처리 테스트 + +#### `src/test/java/carrace/service/InputServiceTest` +- 자동차 대수/시도 횟수 입력의 숫자 파싱 및 범위(1 이상) 검증을 테스트한다. +- 자동차 이름 입력의 기본 검증(빈 값/구분자 처리)이 정상 동작하는지 검증한다. + +--- + +### 4) 테스트 유틸 + +#### `src/test/java/carrace/fixture/CarsFixture` +- 테스트에서 사용할 자동차 목록/상태를 쉽게 만들기 위한 픽스처 제공 + +#### `src/test/java/carrace/fixture/StubInputView` +- 입력 뷰를 대체하기 위한 스텁(고정 입력 제공) +- 입력 기반 테스트에서 예측 가능한 입력값을 주입하기 위해 사용 From c945a0b4371366a21c8559640ea714b40a61c05c Mon Sep 17 00:00:00 2001 From: kdongd Date: Tue, 3 Feb 2026 14:18:27 +0900 Subject: [PATCH 3/4] =?UTF-8?q?-=20Cars=EB=A5=BC=20=EC=9D=BC=EA=B8=89=20?= =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=EC=9C=BC=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=ED=95=98=EA=B3=A0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cars를 일급 컬렉션으로 리팩토링하고 테스트 코드 수정 --- .gitignore | 1 + src/main/java/carrace/Application.java | 7 +- .../java/carrace/domain/car/CarCreator.java | 19 ----- src/main/java/carrace/domain/car/Cars.java | 69 +++++++++++++++++++ src/main/java/carrace/domain/race/Race.java | 37 +++------- .../carrace/domain/car/CarCreatorTest.java | 34 --------- .../java/carrace/domain/car/CarsTest.java | 51 ++++++++++++++ .../java/carrace/domain/race/RaceTest.java | 26 +++---- .../java/carrace/fixture/CarsFixture.java | 13 ++-- 9 files changed, 151 insertions(+), 106 deletions(-) delete mode 100644 src/main/java/carrace/domain/car/CarCreator.java create mode 100644 src/main/java/carrace/domain/car/Cars.java delete mode 100644 src/test/java/carrace/domain/car/CarCreatorTest.java create mode 100644 src/test/java/carrace/domain/car/CarsTest.java diff --git a/.gitignore b/.gitignore index 249cf086af0..efbdf05078a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ out/ ### VS Code ### .vscode/ +.DS_Store diff --git a/src/main/java/carrace/Application.java b/src/main/java/carrace/Application.java index 1b04af5201b..37ec60f43eb 100644 --- a/src/main/java/carrace/Application.java +++ b/src/main/java/carrace/Application.java @@ -1,8 +1,7 @@ package carrace; import carrace.controller.RaceGame; -import carrace.domain.car.Car; -import carrace.domain.car.CarCreator; +import carrace.domain.car.Cars; import carrace.domain.move.MoveCondition; import carrace.domain.move.RandomMoveCondition; import carrace.domain.race.Race; @@ -10,7 +9,7 @@ import carrace.view.CarRaceOutputView; import carrace.service.InputService; -import java.util.List; + @@ -24,7 +23,7 @@ public static void main(String[] args) { String carName = inputService.readCarNames(); int rounds = inputService.readRounds(); - List cars = CarCreator.create(carName); + Cars cars = Cars.from(carName); MoveCondition condition = new RandomMoveCondition(); Race race = new Race(cars); diff --git a/src/main/java/carrace/domain/car/CarCreator.java b/src/main/java/carrace/domain/car/CarCreator.java deleted file mode 100644 index cfb31b1ab25..00000000000 --- a/src/main/java/carrace/domain/car/CarCreator.java +++ /dev/null @@ -1,19 +0,0 @@ -package carrace.domain.car; - - -import java.util.ArrayList; -import java.util.List; - -public class CarCreator { - - public static List create(String input) { - String[] names = input.split(","); - List cars = new ArrayList<>(); - - for (int i = 0; i < names.length; i++) { - String name = names[i].trim(); - cars.add(new Car(name)); - } - return cars; - } -} diff --git a/src/main/java/carrace/domain/car/Cars.java b/src/main/java/carrace/domain/car/Cars.java new file mode 100644 index 00000000000..f5e512081a6 --- /dev/null +++ b/src/main/java/carrace/domain/car/Cars.java @@ -0,0 +1,69 @@ +package carrace.domain.car; + + +import carrace.domain.move.MoveCondition; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Cars { + + private final List cars; + + public Cars(List cars) { + validate(cars); + this.cars = cars; + } + + public static Cars from(String input) { + String[] names = input.split(","); + List cars = new ArrayList<>(); + + for (String name : names) { + cars.add(new Car(name.trim())); + } + return new Cars(cars); + } + + private void validate(List cars) { + if (cars.isEmpty()) { + throw new IllegalArgumentException("자동차는 최소 1대 이상이어여 합니다."); + } + validateDuplicateNames(cars); + } + + private void validateDuplicateNames(List cars) { + Set names = new HashSet<>(); + for (Car car : cars) { + if (!names.add(car.getCarName())) { + throw new IllegalArgumentException("자동차 이름은 중복될 수 없습니다."); + } + } + } + + public void moveAll(MoveCondition condition) { + cars.forEach(car -> car.move(condition)); + } + + public List getWinners() { + int max = 0; + for (Car car : cars) { + max = Math.max(max, car.getPosition()); + } + + List winners = new ArrayList<>(); + for (Car car : cars) { + if (car.getPosition() == max) { + winners.add(car); + } + } + return winners; + } + + public List asList() { + return List.copyOf(cars); + } + +} diff --git a/src/main/java/carrace/domain/race/Race.java b/src/main/java/carrace/domain/race/Race.java index f438809e937..71aba20ec20 100644 --- a/src/main/java/carrace/domain/race/Race.java +++ b/src/main/java/carrace/domain/race/Race.java @@ -1,48 +1,29 @@ package carrace.domain.race; import carrace.domain.car.Car; +import carrace.domain.car.Cars; import carrace.domain.move.MoveCondition; -import java.util.ArrayList; + import java.util.List; public class Race { - private final List cars; + private final Cars cars; - public Race(List cars) { + public Race(Cars cars) { this.cars = cars; } public void moveOnce(MoveCondition condition) { - for (Car car : cars) { - car.move(condition); - } - } - - public List getWinners() { - int maxPosition = 0; - - for (Car car : cars) { - if (car.getPosition() > maxPosition) { - maxPosition = car.getPosition(); - } - } - - List winners = new ArrayList<>(); - for (Car car : cars) { - if (car.getPosition() == maxPosition) { - winners.add(car); - } - } - return winners; + cars.moveAll(condition); } - //내부 상태 보호를 위해 자동차 목록을 그대로 반환하지 않고 - //읽기 전용 리스트(unmodifiableList)를 반환하도록 수정했습니다. public List getCars() { - return List.copyOf(cars); + return cars.asList(); } - + public List getWinners() { + return cars.getWinners(); + } } diff --git a/src/test/java/carrace/domain/car/CarCreatorTest.java b/src/test/java/carrace/domain/car/CarCreatorTest.java deleted file mode 100644 index 817605c6b35..00000000000 --- a/src/test/java/carrace/domain/car/CarCreatorTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package carrace.domain.car; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class CarCreatorTest { - - @Test - @DisplayName("쉼표 기준으로 자동차 생성") - void createCars_byComma() { - List cars = CarCreator.create("kkk,ddd,uuu"); - - assertEquals(3, cars.size()); - assertEquals("kkk", cars.get(0).getCarName()); - assertEquals("ddd", cars.get(1).getCarName()); - assertEquals("uuu", cars.get(2).getCarName()); - } - - @Test - @DisplayName("자동차 이름 앞뒤 공백 제거") - void createCars_trimNames() { - List cars = CarCreator.create("kkk,ddd,uuu"); - - assertEquals(3, cars.size()); - assertEquals("kkk", cars.get(0).getCarName()); - assertEquals("ddd", cars.get(1).getCarName()); - assertEquals("uuu", cars.get(2).getCarName()); - } -} - diff --git a/src/test/java/carrace/domain/car/CarsTest.java b/src/test/java/carrace/domain/car/CarsTest.java new file mode 100644 index 00000000000..27234cee516 --- /dev/null +++ b/src/test/java/carrace/domain/car/CarsTest.java @@ -0,0 +1,51 @@ +package carrace.domain.car; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarsTest { + + @Test + @DisplayName("쉼표 기준으로 자동차 생성") + void createCars_byComma() { + Cars cars = Cars.from("kkk,ddd,uuu"); + + List list = cars.asList(); + assertEquals(3, list.size()); + assertEquals("kkk", list.get(0).getCarName()); + assertEquals("ddd", list.get(1).getCarName()); + assertEquals("uuu", list.get(2).getCarName()); + } + + @Test + @DisplayName("자동차 이름 앞뒤 공백 제거") + void createCars_trimNames() { + Cars cars = Cars.from(" kkk , ddd , uuu "); + + List list = cars.asList(); + assertEquals(3, list.size()); + assertEquals("kkk", list.get(0).getCarName()); + assertEquals("ddd", list.get(1).getCarName()); + assertEquals("uuu", list.get(2).getCarName()); + } + + @Test + @DisplayName("자동차가 1대도 없으면 예외 발생") + void cars_shouldNotBeEmpty() { + assertThatThrownBy(() -> Cars.from("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("자동차 이름이 중복되면 예외 발생") + void cars_shouldNotHaveDuplicateNames() { + assertThatThrownBy(() -> Cars.from("car1,car1")) + .isInstanceOf(IllegalArgumentException.class); + } +} + diff --git a/src/test/java/carrace/domain/race/RaceTest.java b/src/test/java/carrace/domain/race/RaceTest.java index bc788c981f7..e88ae3dfe19 100644 --- a/src/test/java/carrace/domain/race/RaceTest.java +++ b/src/test/java/carrace/domain/race/RaceTest.java @@ -1,12 +1,13 @@ package carrace.domain.race; import carrace.domain.car.Car; +import carrace.domain.car.Cars; import carrace.fixture.CarsFixture; import carrace.fixture.MoveConditions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.ArrayList; + import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -18,7 +19,7 @@ class RaceTest { @Test @DisplayName("Race는 생성시 자동차 목록을 가진다") void race_initialCars_listExists() { - List cars = CarsFixture.threeCars(); + Cars cars = CarsFixture.threeCars(); Race race = new Race(cars); assertThat(race.getCars()).hasSize(3); @@ -27,7 +28,7 @@ void race_initialCars_listExists() { @Test @DisplayName("moveOnce 호출 시 이동하지 않는 조건이면 자동차 위치는 그대로다") void moveOnce_neverMove_positionUnchanged() { - List cars = CarsFixture.threeCars(); + Cars cars = CarsFixture.threeCars(); Race race = new Race(cars); race.moveOnce(MoveConditions.neverMove()); @@ -40,13 +41,11 @@ void moveOnce_neverMove_positionUnchanged() { @Test @DisplayName("getCars 반환은 내부 리스트를 보호한다") void getCars_returnsUnmodifiableList() { - List cars = CarsFixture.threeCars(); + Cars cars = CarsFixture.threeCars(); Race race = new Race(cars); List raceCars = race.getCars(); - assertThat(raceCars).containsExactlyElementsOf(cars); - // 내부 리스트 수정 시도 시 예외 발생 확인 assertThatThrownBy(() -> raceCars.add(new Car("kdu"))) .isInstanceOf(UnsupportedOperationException.class); } @@ -54,29 +53,30 @@ void getCars_returnsUnmodifiableList() { @Test @DisplayName("우승자 1명일 때 getWinners 반환") void getWinners_single() { - List cars = CarsFixture.threeCars(); + Cars cars = CarsFixture.threeCars(); Race race = new Race(cars); - cars.get(0).move(MoveConditions.alwaysMove()); + cars.asList().get(0).move(MoveConditions.alwaysMove()); List winners = race.getWinners(); assertThat(winners).hasSize(1); - assertThat(winners.get(0)).isSameAs(cars.get(0)); + assertThat(winners.get(0).getCarName()).isEqualTo("car1"); } @Test @DisplayName("우승자가 여러명일 때 getWinners 반환") void getWinners_multiple() { - List cars = CarsFixture.threeCars(); + Cars cars = CarsFixture.threeCars(); Race race = new Race(cars); - cars.get(0).move(MoveConditions.alwaysMove()); - cars.get(1).move(MoveConditions.alwaysMove()); + cars.asList().get(0).move(MoveConditions.alwaysMove()); + cars.asList().get(1).move(MoveConditions.alwaysMove()); List winners = race.getWinners(); assertThat(winners).hasSize(2); - assertThat(winners).contains(cars.get(0),cars.get(1)); + assertThat(winners).extracting(Car::getCarName) + .contains("car1", "car2"); } } diff --git a/src/test/java/carrace/fixture/CarsFixture.java b/src/test/java/carrace/fixture/CarsFixture.java index 881d803a303..9ed9dfcfa4b 100644 --- a/src/test/java/carrace/fixture/CarsFixture.java +++ b/src/test/java/carrace/fixture/CarsFixture.java @@ -1,17 +1,14 @@ package carrace.fixture; -import carrace.domain.car.Car; -import java.util.List; +import carrace.domain.car.Cars; + + public class CarsFixture { - public static List threeCars() { - return List.of( - new Car("car1"), - new Car("car2"), - new Car("car3") - ); + public static Cars threeCars() { + return Cars.from("car1,car2,car3"); } From 3f46c269a49d6abbc244048eb4f2f31613066a4f Mon Sep 17 00:00:00 2001 From: kdongd Date: Wed, 4 Feb 2026 20:31:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=813?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Car, InputService 예외 검증 책임 분리 - getWinners 메서드 stream 사용 - Cars 오타 수정 - InputService 사용하지 않는 코드 삭제 - 변경사항에 맞게 테스트 코드 수정 및 추가 --- src/main/java/carrace/domain/car/Car.java | 5 ++- src/main/java/carrace/domain/car/Cars.java | 23 ++++++------- .../java/carrace/service/InputService.java | 34 +++++-------------- .../java/carrace/view/CarRaceInputView.java | 4 --- src/test/java/carrace/domain/car/CarTest.java | 15 +++++++- .../carrace/service/InputServiceTest.java | 23 ------------- 6 files changed, 38 insertions(+), 66 deletions(-) diff --git a/src/main/java/carrace/domain/car/Car.java b/src/main/java/carrace/domain/car/Car.java index 04f1cd3505e..707cc509da5 100644 --- a/src/main/java/carrace/domain/car/Car.java +++ b/src/main/java/carrace/domain/car/Car.java @@ -14,7 +14,10 @@ public Car(String carName) { } private void validateName(String carName) { - if (carName == null || carName.length() > MAX_NAME_LENGTH) { + if (carName == null || carName.isBlank()) { + throw new IllegalArgumentException("자동 이름은 비어있을 수 없습니다."); + } + if (carName.length() > MAX_NAME_LENGTH) { throw new IllegalArgumentException("자동차 이름은 5글자를 초과할 수 없습니다."); } } diff --git a/src/main/java/carrace/domain/car/Cars.java b/src/main/java/carrace/domain/car/Cars.java index f5e512081a6..f259306d3fb 100644 --- a/src/main/java/carrace/domain/car/Cars.java +++ b/src/main/java/carrace/domain/car/Cars.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Set; + public class Cars { private final List cars; @@ -29,7 +30,7 @@ public static Cars from(String input) { private void validate(List cars) { if (cars.isEmpty()) { - throw new IllegalArgumentException("자동차는 최소 1대 이상이어여 합니다."); + throw new IllegalArgumentException("자동차는 최소 1대 이상이어야 합니다."); } validateDuplicateNames(cars); } @@ -48,20 +49,18 @@ public void moveAll(MoveCondition condition) { } public List getWinners() { - int max = 0; - for (Car car : cars) { - max = Math.max(max, car.getPosition()); - } + int maxPosition = cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); - List winners = new ArrayList<>(); - for (Car car : cars) { - if (car.getPosition() == max) { - winners.add(car); - } - } - return winners; + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .toList(); } + + public List asList() { return List.copyOf(cars); } diff --git a/src/main/java/carrace/service/InputService.java b/src/main/java/carrace/service/InputService.java index 9cf2ea52c18..812f1fd168e 100644 --- a/src/main/java/carrace/service/InputService.java +++ b/src/main/java/carrace/service/InputService.java @@ -16,23 +16,8 @@ public String readCarNames() { return input; } - public int readCarCount() { - int value = parseInt( - inputView.readCarCount(), - "자동차 대수는 숫자여야 합니다." - ); - - if (value <= 0) { - throw new IllegalArgumentException("자동차는 1대 이상이어야 합니다."); - } - return value; - } - public int readRounds() { - int value = parseInt( - inputView.readMoveCount(), - "시도 횟수는 숫자여야 합니다." - ); + int value = parseInt(inputView.readMoveCount()); if (value <= 0) { throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); @@ -40,20 +25,19 @@ public int readRounds() { return value; } - private int parseInt(String input, String errorMessage) { - try { - return Integer.parseInt(input); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(errorMessage); - } + private int parseInt(String input) { + return Integer.parseInt(input); } private void validateCarNames(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("자동차 이름을 입력하세요"); + } + String[] names = input.split(","); for (String name : names) { - String trimmed = name.trim(); - if (trimmed.isEmpty() || trimmed.length() > 5) { - throw new IllegalArgumentException("자동차 이름은 5자를 초과할 수 없습니다."); + if (name.trim().isBlank()) { + throw new IllegalArgumentException("자동차 이름은 공백일 수 없습니다."); } } } diff --git a/src/main/java/carrace/view/CarRaceInputView.java b/src/main/java/carrace/view/CarRaceInputView.java index 171d54439e9..72c0d52119b 100644 --- a/src/main/java/carrace/view/CarRaceInputView.java +++ b/src/main/java/carrace/view/CarRaceInputView.java @@ -11,10 +11,6 @@ public String readCarNames() { System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); return scanner.nextLine(); } - public String readCarCount() { - System.out.println("자동차 대수는 몇 대인가요?"); - return scanner.nextLine(); - } public String readMoveCount() { System.out.println("시도할 횟수는 몇 회인가요?"); diff --git a/src/test/java/carrace/domain/car/CarTest.java b/src/test/java/carrace/domain/car/CarTest.java index 7ea6f229a8e..82a2735f3dd 100644 --- a/src/test/java/carrace/domain/car/CarTest.java +++ b/src/test/java/carrace/domain/car/CarTest.java @@ -3,7 +3,6 @@ import carrace.fixture.MoveConditions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -55,4 +54,18 @@ void name_too_long_throws() { .isInstanceOf(IllegalArgumentException.class); } + @Test + @DisplayName("이름이 null 이면 예외") + void name_null_throws() { + assertThatThrownBy(() -> new Car(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이름이 공백이면 예외") + void name_blank_throws() { + assertThatThrownBy(() -> new Car(" ")) + .isInstanceOf(IllegalArgumentException.class); + } + } diff --git a/src/test/java/carrace/service/InputServiceTest.java b/src/test/java/carrace/service/InputServiceTest.java index c3bf5bb8e0b..943d6d2c196 100644 --- a/src/test/java/carrace/service/InputServiceTest.java +++ b/src/test/java/carrace/service/InputServiceTest.java @@ -8,20 +8,6 @@ class InputServiceTest { - @DisplayName("자동차 수가 1이면 정상 입력 (경계값)") - @Test - void carCountOneIsValid() { - InputService service = new InputService(new StubInputView("1", "5")); - assertEquals(1, service.readCarCount()); - } - - @DisplayName("자동차 수가 0이면 예외 발생 (경계값)") - @Test - void carCountZeroThrows() { - InputService service = new InputService(new StubInputView("0", "5")); - assertThrows(IllegalArgumentException.class, service::readCarCount); - } - @DisplayName("시도 횟수가 음수면 예외 발생 (경계값)") @Test void roundsNegativeThrows() { @@ -29,15 +15,6 @@ void roundsNegativeThrows() { assertThrows(IllegalArgumentException.class, service::readRounds); } - @Test - @DisplayName("자동차 이름이 5글자 초과이면 예외 발생") - void carNameTooLongThrows() { - StubInputView inputView = new StubInputView("TOOLONG","3"); - InputService service = new InputService(inputView); - - assertThrows(IllegalArgumentException.class, service::readCarNames); - } - @Test @DisplayName("자동차 이름 공백시 예외 발생") void carNameBlankThrows() {