-
Notifications
You must be signed in to change notification settings - Fork 903
[자동차 경주] 김현수 미션 제출합니다. #912
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
base: main
Are you sure you want to change the base?
Changes from all commits
9bed825
52920bd
9158ec5
1e9c278
b2ef9b4
afad636
c573cc9
652bd19
9bf478e
fefbc5c
ad8486b
7a27425
329f2f2
ba1c695
ffce06c
517673a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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` 후 종료 | ||
|
|
| 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); | ||
| } | ||
| } | ||
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 시작할 때 중복이 있는지 테스트를 안한 것 같은데, 이 코드가 다소 위험해보입니다.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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; | ||
| } | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 이상의 정수여야 합니다."); | ||
| } | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package racingcar; | ||
|
|
||
| public interface MovePolicy { | ||
| boolean canMove(int number); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package racingcar; | ||
|
|
||
| public interface NumberGenerator { | ||
| int next0to9(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 0~9 사이의 난수를 발생시킨다는 함수명 같은데 이를 굳이 인터페이스로 만든 이유가 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,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)); | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } |
| 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] 우승자를 계산할 자동차가 없습니다."); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 입력받은 시점에서, 자동차가 생성되어서 중복되는 코드인 것 같습니다.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } |
| 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[]{}); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
레이싱 카 게임 실행 흐름을 제어하는 클래스를 하나 만들고, main에서는 단순 진입점만 제공하는 것은 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 분들 코드를 보면서 Controller로 흐름을 제어하는 구조가 훨씬 명확하다는 걸 느꼈습니다...
말씀해주신 대로 이번 주 미션에서는 이런 구조적인 부분을 참고해서 적용해보겠습니다!