-
Notifications
You must be signed in to change notification settings - Fork 903
[자동차 경주] 김도현 미션 제출합니다. #357
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
7de706a
e911344
c8cf202
3cefad6
685ca77
33b23c8
275f343
a1f8d87
351e494
6789ec3
3f9be96
c20ec79
7d59dd3
986235c
a307f98
3a64015
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,101 @@ | ||
| # java-racingcar-precourse | ||
|
|
||
| 우아한테크코스 8기 프리코스 2주차 괴제 내용입니다. | ||
|
|
||
| ## 프로젝트 설명 | ||
|
|
||
| 쉼표로 구분된 자동차 이름을 입력받아 경주를 준비, 시도할 횟수를 입력받아 입력된 횟수만큼 경주를 실행한다. | ||
| 각 자동차는 0-9 사이의 무작위 값을 부여받아 무작위 값이 4 이상일 경우 해당 자동차는 1칸 전진한다. | ||
| 각 라운드별 실행 결과를 실시간으로 출력한다. | ||
| 모든 경주가 완료된 후, 가장 멀리 이동한 자동차를 최종 우승자로 선정하여 출력한다. | ||
|
|
||
| ## 기능 목록 | ||
|
|
||
| 1. 입력 기능 | ||
| - [x] 자동차 이름을 쉼표 기준으로 입력받기 | ||
| - [x] 경주 횟수 입력받기 | ||
|
|
||
| 2. 예외 처리 | ||
|
|
||
| - [x] 자동차 이름이 5자를 초과하는 경우 | ||
| - [x] 자동차 이름이 null, 빈 문자열이거나 공백만으로 이루어진 경우 | ||
| - [x] 중복된 자동차 이름을 입력한 경우 | ||
| - [x] 경주 횟수에 정수가 아닌 값을 입력한 경우 | ||
| - [x] 경주 횟수에 자연수가 아닌 값을 입력한 경우 | ||
|
|
||
| 3. 메인 로직 | ||
|
|
||
| - [x] 입력받은 자동차 이름을 쉼표 기준으로 나누어 자동차 객체 목록 생성 | ||
| - [x] 무작위 값이 4 이상인 경우 자동차를 전진시키기 | ||
| - [x] 입력된 횟수만큼 자동차 경주를 실행하기 | ||
| - [x] 최종 우승자 결정하기(가장 먼 위치의 자동차를 찾아내기) | ||
|
|
||
| 4. 출력 기능 | ||
|
|
||
| - [x] 횟수별 자동차의 이름과 이동한 거리를 출력하기 | ||
| - [x] 최종 우승자 출력하기(공동 우승 시 쉼표로 구분) | ||
|
|
||
| ## 사용 기술 | ||
|
|
||
| - Language: Java 21 | ||
| - Build Tool: Gradle | ||
| - Test: JUnit 5, AssertJ | ||
| - Library: camp.nextstep.edu.missionutils (Console, Randoms) | ||
|
|
||
| ## 아키텍처 및 설계 원칙 | ||
|
|
||
| 본 프로젝트는 객체 지향 설계 원칙을 준수하도록 개발되었습니다. | ||
|
|
||
| ### 1. MVC 패턴 (Model-View-Controller) | ||
| Controller | ||
| - RacingGameController: View와 Model을 중재하며 게임의 전체 흐름(입력 ➔ 실행 ➔ 결과)을 제어 | ||
|
|
||
| Model | ||
| - Car: name과 position을 가지며 position을 1 증가시키는 move() 메서드를 갖는다 | ||
| - RacingGame: List<Car>를 관리하며 findWinners()와 같은 핵심 로직을 수행 | ||
| - CarFactory: 이름 문자열을 받아 List<Car> 생성하는 Factory 객체 | ||
| - Validator: 이름 및 횟수 검증 로직을 static 메서드로 제공하는 유틸리티 객체 | ||
|
|
||
| View | ||
| - InputView: 사용자의 콘솔 입력을 담당 | ||
| - OutputView: 콘솔 출력을 담당 | ||
|
|
||
| ### 2. TDD (Test-Driven Development) | ||
| Model 계층의 모든 비즈니스 로직과 예외 상황에 대해 JUnit 5와 AssertJ를 사용하여 단위 테스트를 우선 작성하고 이를 통과하는 코드를 구현했다. | ||
|
|
||
| ### 3. 의존성 주입 (Dependency Injection) | ||
| Application(main)이 InputView, OutputView, CarFactory 등 필요한 객체를 생성하여 RacingGameController의 생성자로 주입한다. | ||
| - Controller가 View의 구체적인 구현이 아닌 인스턴스에 의존하게 된다. | ||
| - Controller와 View 간의 결합도가 낮아지며 Controller의 테스트가 용이하다. | ||
|
|
||
| ### 4. 단일 책임 원칙 (SRP) | ||
|
|
||
| 각 객체가 하나의 책임만 갖도록 분리했습니다. | ||
| CarFactory는 '생성', Validator는 '검증', RacingGame은 '게임 로직', Controller는 '흐름 제어'의 책임을 각각 담당합니다. | ||
|
|
||
| ## 실행 방법 | ||
|
|
||
| 1. Application.main() 메서드를 실행 | ||
| 2. "경주할 자동차 이름을 입력하세요..."에 따라 이름을 입력한다.(이름은 쉼표로 구분한다) | ||
| 3. "시도할 횟수는 몇 회인가요?"에 따라 횟수를 입력한다. | ||
| 4. 실행 결과를 확인한다. | ||
|
|
||
| ## 프로젝트 구조 | ||
|
|
||
| ``` | ||
| ├── main | ||
| │ └── java | ||
| │ └── racingcar | ||
| │ ├── controller | ||
| │ │ └── RacingGameController.java # 게임의 전체 흐름(입력 ➔ 실행 ➔ 결과)을 제어 | ||
| │ ├── model | ||
| │ │ ├── Car.java # 자동차 name, position, move() 포함 | ||
| │ │ ├── CarFactory.java # List<Car>을 생성 | ||
| │ │ ├── RacingGame.java # findWinners()와 같은 핵심 로직을 수행 | ||
| │ │ └── Validator.java # 이름 및 횟수 검증 | ||
| │ ├── view | ||
| │ │ ├── InputView.java | ||
| │ │ └── OutputView.java | ||
| │ └── Application.java | ||
| └── test | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,19 @@ | ||
| package racingcar; | ||
|
|
||
| import racingcar.controller.RacingGameController; | ||
| import racingcar.model.CarFactory; | ||
| import racingcar.view.InputView; | ||
| import racingcar.view.OutputView; | ||
|
|
||
| public class Application { | ||
| public static void main(String[] args) { | ||
| // TODO: 프로그램 구현 | ||
| InputView inputView = new InputView(); | ||
| OutputView outputView = new OutputView(); | ||
| CarFactory carFactory = new CarFactory(); | ||
|
|
||
| RacingGameController racingGameController = new RacingGameController( | ||
| inputView, outputView, carFactory | ||
| ); | ||
| racingGameController.run(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package racingcar.controller; | ||
|
|
||
| import racingcar.model.Car; | ||
| import racingcar.model.CarFactory; | ||
| import racingcar.model.RacingGame; | ||
| import racingcar.view.InputView; | ||
| import racingcar.view.OutputView; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class RacingGameController { | ||
| private final InputView inputView; | ||
| private final OutputView outputView; | ||
| private final CarFactory carFactory; | ||
|
|
||
| public RacingGameController(InputView inputView, OutputView outputView, CarFactory carFactory) { | ||
| this.inputView = inputView; | ||
| this.outputView = outputView; | ||
| this.carFactory = carFactory; | ||
| } | ||
|
|
||
| public void run() { | ||
| // 1. 준비 | ||
| List<Car> cars = setupCars(); | ||
| int tryCount = setupTryCount(); | ||
| RacingGame game = new RacingGame(cars); | ||
| // 2. 실행 | ||
| playRacingGame(game, tryCount); | ||
| // 3. 결과 | ||
| showGameResult(game); | ||
| } | ||
|
|
||
| private List<Car> setupCars() { | ||
| String carNameInput = inputView.readCarNames(); | ||
| return carFactory.createCarList(carNameInput); | ||
| } | ||
|
Comment on lines
+33
to
+36
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. |
||
|
|
||
| private int setupTryCount() { | ||
| String tryCountInput = inputView.readTryCount(); | ||
| return Integer.parseInt(tryCountInput); | ||
| } | ||
|
|
||
| private void playRacingGame(RacingGame game, int tryCount) { | ||
|
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. Controller에 전체 게임 라운드를 반복하는 로직을 직접 구현하는 것보다 RacingGame에서 동작하는 것이 역할이 더 잘 분리될거 같다고 생각하는데 어떻게 생각하시나요?
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. 맞습니다. 저도 Controller가 반복을 수행하지 않는 것이 이상적이라고 생각합니다. |
||
| outputView.printResultHeader(); | ||
| for (int i = 0; i < tryCount; i++) { | ||
| game.runOneRound(); | ||
| outputView.printCurrentStatus(game.getCarList()); | ||
| } | ||
| } | ||
|
|
||
| private void showGameResult(RacingGame game) { | ||
| List<String> winners = game.findWinners(); | ||
| outputView.printWinner(winners); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package racingcar.model; | ||
|
|
||
| public class Car { | ||
| private final String name; | ||
| private int position; | ||
| private static final int INCREASE_POSITION = 1; | ||
|
|
||
| public Car(String name) { | ||
| this.name = name; | ||
| Validator.validateName(name); | ||
| } | ||
|
|
||
| public int getPosition() { | ||
| return this.position; | ||
| } | ||
|
|
||
| public String getName() { | ||
| return this.name; | ||
| } | ||
|
|
||
| public void move() { | ||
| this.position += INCREASE_POSITION; | ||
| } | ||
| } |
|
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. 변수 이름에 타입을 적지 않아야 합니다. nameList -> names 등 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package racingcar.model; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| public class CarFactory { | ||
| private static final String DELIMITER = ","; | ||
|
|
||
| public List<Car> createCarList(String carNameInput) { | ||
| String[] names = carNameInput.split(DELIMITER); | ||
| List<String> nameList = Arrays.asList(names); | ||
|
|
||
| Validator.validateDuplicateNames(nameList); | ||
|
|
||
| List<Car> carList = new ArrayList<>(); | ||
| for (String name : names) { | ||
| carList.add(new Car(name)); | ||
| } | ||
| return carList; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package racingcar.model; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import static camp.nextstep.edu.missionutils.Randoms.pickNumberInRange; | ||
|
|
||
| public class RacingGame { | ||
| private final List<Car> carList; | ||
| private static final int MIN_INCREASE_CONDITION = 4; | ||
|
|
||
| public RacingGame(List<Car> carList) { | ||
| this.carList = carList; | ||
| } | ||
|
|
||
| public List<String> findWinners() { | ||
| int maxPosition = findMaxPosition(); | ||
| List<String> winners = new ArrayList<>(); | ||
|
|
||
| for(Car car : carList) { | ||
| if(car.getPosition() == maxPosition) { | ||
| winners.add(car.getName()); | ||
| } | ||
| } | ||
| return winners; | ||
| } | ||
|
|
||
| private int findMaxPosition() { | ||
| int maxPosition = 0; | ||
| for (Car car : carList) { | ||
| maxPosition = Math.max(maxPosition, car.getPosition()); | ||
| } | ||
| return maxPosition; | ||
| } | ||
|
|
||
| public void runOneRound() { | ||
| for(Car car : carList) { | ||
| int randomNumber = pickNumberInRange(0,9); | ||
| if(randomNumber >= MIN_INCREASE_CONDITION) car.move(); | ||
| } | ||
| } | ||
|
|
||
| public List<Car> getCarList() { | ||
| return carList; | ||
| } | ||
|
Comment on lines
+42
to
+44
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. getter로 참조 가능한 객체를 반환하는 경우 수정 가능성이 있으므로 깊은 복사를 통해 수정을 방지해야 합니다. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package racingcar.model; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| public class Validator { | ||
| private static final int MAX_NAME_LENGTH = 5; | ||
|
|
||
| public static void validateName(String name) { | ||
| if(name == null) throw new IllegalArgumentException("이름은 null일 수 없습니다"); | ||
|
|
||
| if(name.isBlank()) throw new IllegalArgumentException("이름이 비어있습니다"); | ||
|
|
||
| if(name.length() > MAX_NAME_LENGTH) throw new IllegalArgumentException("이름은 5자 이하만 가능합니다"); | ||
| } | ||
|
|
||
| public static void validateTryCount(String tryCount) { | ||
| try { | ||
| int count = Integer.parseInt(tryCount); | ||
| if(count < 1) throw new IllegalArgumentException("입력값이 자연수가 아닙니다"); | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException("입력값이 정수가 아닙니다"); | ||
| } | ||
| } | ||
|
|
||
| public static void validateDuplicateNames(List<String> names) { | ||
| Set<String> uniqueNames = new HashSet<>(names); | ||
|
|
||
| if (uniqueNames.size() != names.size()) { | ||
|
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 new IllegalArgumentException("중복된 자동차 이름이 있습니다."); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package racingcar.view; | ||
|
|
||
| import racingcar.model.Validator; | ||
|
|
||
| import static camp.nextstep.edu.missionutils.Console.readLine; | ||
|
|
||
| public class InputView { | ||
| public String readCarNames() { | ||
| System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); | ||
|
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. 맞습니다. @sun007021 님의 PR에서 확인한 점으로 입력값 자체가 null이거나 공백인 경우 예외처리하는 기능이 필요합니다. |
||
| return readLine(); | ||
| } | ||
|
|
||
| public String readTryCount() { | ||
| System.out.println("시도할 횟수는 몇 회인가요?"); | ||
|
|
||
| String inputTryCount = readLine(); | ||
| Validator.validateTryCount(inputTryCount); | ||
|
|
||
| return inputTryCount; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package racingcar.view; | ||
|
|
||
| import racingcar.model.Car; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class OutputView { | ||
| public void printResultHeader() { | ||
| System.out.println("\n실행 결과"); | ||
| } | ||
|
|
||
| public void printCurrentStatus(List<Car> CarList) { | ||
| for (Car car : CarList) { | ||
| System.out.println(car.getName()+ " : " + "-".repeat(car.getPosition())); | ||
| } | ||
| System.out.println(); | ||
| } | ||
|
|
||
| public void printWinner(List<String> winners) { | ||
| System.out.print("최종 우승자 : "); | ||
| String result = String.join(", ", winners); | ||
| System.out.print(result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package racingcar.model; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| public class CarFactoryTest { | ||
|
|
||
| @DisplayName("쉼표로 구분된 문자열로 Car 리스트를 생성한다") | ||
| @Test | ||
| void createCarsFromNames() { | ||
| CarFactory carFactory = new CarFactory(); | ||
|
|
||
| String carNameInput = "pobi,woni,jun"; | ||
|
|
||
| List<Car> cars = carFactory.createCarList(carNameInput); | ||
|
|
||
| assertThat(cars).hasSize(3); | ||
|
|
||
| assertThat(cars) | ||
| .extracting(Car::getName) | ||
| .containsExactly("pobi", "woni", "jun"); | ||
| } | ||
| } |
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.
inputView를 바로 사용하지 않고 이렇게 함수로 한번 더 포장해서 사용했을때의 장점이 궁금합니다