Skip to content

[자동차 경주] 이상현 미션 제출합니다. #120

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

Merged
merged 33 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
679d6a3
docs: 1단계 요구사항 정리
idealHyun May 3, 2025
a81fdd9
feat: 이름을 가지는 Car 객체 생성
idealHyun May 4, 2025
2c5a032
feat: RandomNumberGenerator 생성
idealHyun May 4, 2025
f29d936
feat: Car 움직이는 메소드 추가
idealHyun May 4, 2025
a70fdba
test: Car 움직이는 기능 테스트
idealHyun May 4, 2025
74bdd3a
docs: 2단계 요구사항 정리
idealHyun May 4, 2025
8668af9
feat: CarRaceGame 객체 생성
idealHyun May 4, 2025
f2530c6
feat: NumberGenerator 인터페이스 생성
idealHyun May 4, 2025
b1b5dbf
feat: Mockito 의존성 추가
idealHyun May 4, 2025
4d38a71
fix: CarRaceGame 생성자에 numberGenerator 주입 추가
idealHyun May 4, 2025
726078e
test: 우승자 확인을 위한 테스트 코드 추가
idealHyun May 4, 2025
5bad0a3
dos: 3단계 요구사항 정리
idealHyun May 4, 2025
c7b2261
fix: CarRaceGame -> CarRace 클래스 이름 수정
idealHyun May 4, 2025
7e71b3c
feat: 자동차 이름 유효성 검증 추가
idealHyun May 5, 2025
6e4819f
test: 자동차 이름 테스트 추가
idealHyun May 5, 2025
660f552
fix: CarRace 클래스 게임 라운드 변수를 외부에서 넘겨주게 변경
idealHyun May 6, 2025
a60dd48
feat: InputView, OutputView 클래스 구현
idealHyun May 6, 2025
eaecd52
feat: CarNameParser 객체 생성
idealHyun May 6, 2025
88dcf42
test: CarNameParser 테스트 코드 생성
idealHyun May 6, 2025
e2ea68b
feat: CarRaceApp 생성
idealHyun May 6, 2025
9f3e933
feat: 실행결과 출력 기능 추가
idealHyun May 6, 2025
1c62c88
feat: 우승자 출력 기능 추가
idealHyun May 6, 2025
4dba7c5
refactor: 겹치는 함수 이름 변경
idealHyun May 6, 2025
26fa5d5
refactor: 함수 묶어내기
idealHyun May 6, 2025
6494866
docs: 4단계 요구사항 정리
idealHyun May 6, 2025
e9887c8
feat: 자동차 이름 6자 이상에 대한 예외 생성
idealHyun May 7, 2025
a9dccde
test: 전체적인 애플리케이션 결과 테스트 추가
idealHyun May 7, 2025
5409d06
test: 게임 라운드 입력에 정수가 아니면 예외 발생
idealHyun May 7, 2025
56c62d3
fix: 랜덤값 생성 코드 수정
idealHyun May 7, 2025
b4c3bfd
style: 코드 스타일 적용
idealHyun May 9, 2025
460375e
fix: 상수 네이밍 컨벤션 적용
idealHyun May 9, 2025
eb9ee56
fix: 상수 처리
idealHyun May 9, 2025
f76307b
fix: 상수 이름 수정 및 문자열 위치 변경
idealHyun May 10, 2025
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 자동차 경주 - 초간단 애플리케이션

## 1단계 요구사항
- 자동차는 이름과 거리를 가지고 있다.
- 자동차는 움직일 수 있다.
- 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
- 0 ~ 9 사이의 random 한 값을 생성시키는 객체가 필요하다.

## 2단계 요구사항
- n대의 자동차가 참여할 수 있다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 구할 수 있다. 우승자는 한 명 이상일 수 있다.
- 경주 게임이라는 객체가 필요해보인다.
- 생성자로 n대의 자동차 객체가 들어간다
- 게임을 진행하는 메소드가 필요해보인다.
- 주어진 횟수가 끝나면 자동차끼리의 distance를 비교하여 자동차 이름 리스트를 반환하고 저장한다.

## 3단계 요구사항
- 메인 메서드를 추가하여 실행 가능한 애플리케이션으로 만든다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 출력문에 대한 문자열 관리
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 자동차 생성시 이름 길이에 대한 유효성 검증
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 이동 횟수가 정수인지 유효성 검증

## 4단계 요구사항 정리
- 게임 시도 횟수 입력 시 정수가 아니면 예외 처리에 대한 문구 추가하기
- 모든 로직에 단위 테스트를 구현
- 예외 사항 더 추상화하여 처리해보기
- 자동차 이름 비었을 때
- 자동차 이름 6자 이상일 때

## 프로그래밍 요구사항

- 자동차가 움직이는 기능이 의도대로 동작하는지 테스트한다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 Java Style Guide을 원칙으로 한다.
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- else 예약어를 쓰지 않는다.
- else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다.
- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.25.1')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
testImplementation('org.mockito:mockito-core:5.11.0')

Choose a reason for hiding this comment

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

오 mockito를 쓰셨군요...

testImplementation('org.mockito:mockito-junit-jupiter:5.11.0')
}

test {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/CarRaceApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import domain.Car;
import domain.CarNameParser;
import domain.CarRace;
import domain.NumberGenerator;
import domain.NumberGeneratorImpl.RandomNumberGenerator;
import java.util.List;
import view.InputView;
import view.OutputView;

public class CarRaceApp {
public static void main(String[] args) {
final InputView inputView = new InputView();
final OutputView outputView = new OutputView();
final CarNameParser carNameParser = new CarNameParser();
final NumberGenerator numberGenerator = new RandomNumberGenerator();

List<Car> cars = getCars(outputView, inputView, carNameParser);
int gameRounds = getGameRounds(outputView, inputView);
final CarRace carRace = new CarRace(cars, gameRounds, numberGenerator);
carRace.start();

outputView.printGameResult(carRace.getGameRoundsOutput());
outputView.printWinnerCarNames(carRace.getWinnerCarNames());
}

private static int getGameRounds(OutputView outputView, InputView inputView) {
outputView.printInputGameRounds();
return inputView.readGameRounds();
}

private static List<Car> getCars(OutputView outputView, InputView inputView,
CarNameParser carNameParser) {
outputView.printInputCarsName();
String carNames = inputView.readCarNames();
return carNameParser.parse(carNames).stream().map(Car::new).toList();
}
}
42 changes: 42 additions & 0 deletions src/main/java/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package domain;

import exception.CarNameTooLongException;

public class Car {
private String name;
private int distance = 0;
private final int MOVE_STANDARD_NUMBER = 4;
private final int MAX_CAR_NAME_LENGTH = 5;

public Car(String name){

Choose a reason for hiding this comment

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

생성자에서 validate 함수를 호출하는건 좋은 것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

특정 값을 입력받을 때, 입력 단계(InputView)에서는 데이터 타입(정수, 실수, 문자열 등)에 대한 포맷 유효성을 검증하고, 이렇게 검증된 값을 생성자를 통해 객체(Car)로 전달한 뒤, 생성자 내부에서는 객체 규칙(이름 5자 넘기지 않기)에 맞춰서 유효성 검증을 수행하는 방식이 적절한게 맞을까요?

Choose a reason for hiding this comment

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

좋은 접근 방법입니다. 👍
왜 그렇게 나눴는지에 대해서 질문을 던져보고 더 디테일하게 상현님의 생각을 듣고싶지만, 리뷰 핑퐁의 시간이 모자란 관계로 제 생각을 전달드리겠습니다.

유효성을 검증하는 것 또한 일종의 책임입니다. 그리고 책임이 어딨는지 알기 도와주는 방법은 유사한 요구사항이 변경되었을 떄, 그리고 그 책임을 맡는 클래스가 바뀌었을 때 를 생각해보면 됩니다.

View는 입출력에 대한 책임, Car는 자동차 라는 도메인 모델에 대한 책임을 갖고 있습니다.

만약 입력이 console이 아닌 웹 요청으로 바뀌면 어떨까요? 아니면 gui의 클릭으로 바뀐다던가?
그런 경우를 생각해보면 데이터 타입(정수, 실수, 문자열 등)에 대한 포맷 유효성은 InputView에 있는게 맞을 것입니다.

반대로, 객체 규칙(이름 5자 넘기지 않기)이 InputView에 있는데 입력이 gui로 바뀌면 어떻게 될까요?
아마 기존 (이름 5자 넘기지 않기) 를 위한 로직이 복붙될 것 같습니다. 그렇다는 것은 객체 규칙이 InputView의 책임이 아니란 말도 되죠.

결론적으로 생각해주신 방향과 저도 동일하게 생각합니다!

validate(name);
this.name = name;
}

private void validate(String name) {
if(name.length() > MAX_CAR_NAME_LENGTH ){

Choose a reason for hiding this comment

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

discord에서 이야기했던 것처럼 포맷팅을 한번 해보면 좋을 것 같아요.

xml을 등록하고 reformat code를 하면 알아서 다 바뀐답니당

Copy link
Author

Choose a reason for hiding this comment

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

적용해보겠습니다!

throw new CarNameTooLongException("자동차의 이름은 5글자 이하여야합니다.");

Choose a reason for hiding this comment

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

어떤건 customException이고 어떤건 그냥 IllegalArgException을 쓰셨군요. 기준이 어떤건가요?

Copy link
Author

Choose a reason for hiding this comment

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

자동차 이름의 길이가 특정 글자수를 초과하는 경우는 자동차 모델의 특징에 해당해서 예외를 구분해주는 것이 더 좋다고 생각했습니다. null 값에 대한 예외는 예상 가능한 예외라 생각하여 IllegalArgumentException을 사용해 처리해도 될것이라 생각했습니다.

}
if(name.trim().isEmpty()){
throw new IllegalArgumentException("자동차의 이름은 공백이 될 수 없습니다.");
}
}

public void tryMoveByNumber(int number){
if(number >= MOVE_STANDARD_NUMBER){
move();
}
}

private void move(){
distance+=1;
}

public int getDistance(){
return distance;
}

public String getName(){
return name;
}
}
14 changes: 14 additions & 0 deletions src/main/java/domain/CarNameParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package domain;

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

public class CarNameParser {

Choose a reason for hiding this comment

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

CarNameparser는 어떤 의도로 만들었고, 지금상황에서 필요할까요?

Copy link
Author

Choose a reason for hiding this comment

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

InputView에서 자동차 이름과 쉼표로 이루어진 문자열을 받고, 쉼표를 기준으로 이름들을 분리해주는 행위를 누군가 해야하는데 InputView는 입력만 받는 역할을 해야된다고 생각했고, 이 역할을 맡길 객체가 떠오르지 않아서 CarNameparser 라는 객체를 만들어서 역할을 분리하려했습니다.
위의 역할 분리가 적절하지 않았다면 불필요한 객체인 것 같습니다.

이러한 의도를 가졌었는데, 의도가 적절한 것일까요?
객체지향은 이러한 역할(책임?)을 객체에게 잘 분리해야한다고 생각했는데 분리할 때 어떤 점을 고려하면 좋을까요?

Choose a reason for hiding this comment

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

의도를 가지고 분리했다면 좋다고 생각합니다.

각 클래스가 얼마나 역할에 대한 책임을 가질지는 늘 어렵습니다. 특히 상황에 따라 이게 변경되기도 하니까 말이죠.

저는 앞서 코멘트를 남겼듯이 책임을 분리할 때 추후 요구사항이 변경되었을 때 변경되어야 하는 코드의 범위가 적절한지?로 고려하는 것 같습니다.
그런 면에서 나쁘지 않다고 볼 수 있습니다. 추후에 파싱로직이 복잡해지거나, 추가 되는 경우엔 잘 쪼갰다고 볼 수 있겠죠.

하지만, 객체지향에서 간과되는 점이 있는 것은 새로운 클래스를 만드는 것 그리고 과도한 추상화 또한 관리 비용입니다. 아마 이 말이 잘 안 와닿을 수도 있을 것 같지만 그렇습니다.

오버엔지니어링이란 키워들르 한 번 알아보시면 좋을 것 같습니다.

Choose a reason for hiding this comment

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

추상화와 클래스를 쪼개는 것은 미래에 코드가 변경되었을 때 이 변경을 쉽게하기 위해 미리 현재에 지불하는 비용 같은 것입니다.

앞서 말했듯이 추후에 파싱로직이 복잡해지거나, 추가 되는 경우엔 잘 쪼갰다고 볼 수 있겠죠.

하지만, 그런 경우가 안 온다면 어떨까요? 갑자기 그냥 파싱로직이 없어지거나 애당초 입력을 할 때 객체처럼 들어온다면?
spring에 대해서 아시는지 기억이 잘 안나지만, 추후 배우는 spring 에서는 웹요청을 자동으로 객체에 매핑해줍니다. 이런 경우 CarNameParser는 무의미해지겠죠. 그냥 지워질 것입니다.
심지어 이를 유지보수하기 위해 테스트코드까지 짰다면 이 테스트코드에 투자한 시간 또한 무의미해질 수도 있습니다.

그런 클래스를 분리할 시간에, 우리는 다른 코드 한 자 치는게 더 나은 선택일 수도 있습니다. 개발자의 시간은 곧 돈과 다름 없거든요.

그렇기에 우리는 추후에 변경될 가능성 그리고 추후에 변경되었을 때의 이펙트를 생각하면서 쪼개야합니다.

private final String DELIMITER=",";

Choose a reason for hiding this comment

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

이 값이 불변이라면 다른 방식으로 표현해보면 어떨까요??

static을 붙인 변수와 붙이지 않은 변수는 실제로 어떤 차이가 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

private static final String DELIMITER = ","; 으로 수정해야할 것 같습니다.
static을 붙이지 않으면 객체마다 해당 변수 메모리를 차지해서 메모리가 낭비됩니다.


public List<String> parse(String carNames) {
return Arrays.stream(carNames.trim().split(DELIMITER))
.map(String::trim)
.toList();
}
}
57 changes: 57 additions & 0 deletions src/main/java/domain/CarRace.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package domain;

import java.util.List;

public class CarRace {

private final List<Car> cars;
private final int gameRounds;
private final NumberGenerator numberGenerator;
private final StringBuilder gameRoundsOutput = new StringBuilder("\n실행결과");

Choose a reason for hiding this comment

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

실행결과와 같은 문구, 이 output과 관련된 내용은 다른 클래스에 옮겨보는게 어떨까요?

private final String DISTANCE_EXPRESSION = "-";
private List<String> winnerCarNames;

public CarRace(List<Car> cars, int gameRounds, NumberGenerator numberGenerator) {
this.cars = cars;
this.gameRounds = gameRounds;
this.numberGenerator = numberGenerator;
}

public void start() {
playRounds();
int maxDistance = getMaxDistanceFromCars();
winnerCarNames = getWinnerCarNamesForMaxDistance(maxDistance);
}

private void playRounds() {
for (int i = 0; i < gameRounds; i++) {

Choose a reason for hiding this comment

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

indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

요런 요구사항이 있었습니다. 왜 이런 요구사항이 있을까요? 그리고 한번 지켜보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

코드를 읽는 사람의 입장에서 indent의 깊이가 깊어질수록 가독성이 나쁘고, 로직이 복잡해진다고 느낍니다. 그러면 유지보수가 힘들어지기 때문인 것 같습니다.
마지막에 요구사항을 충족했는지 다시 한번 체크하겠습니다. 😭

cars.forEach(car -> {
car.tryMoveByNumber(numberGenerator.generate());
recordOutput(car);
});
gameRoundsOutput.append("\n");
}
}

private void recordOutput(Car car) {

Choose a reason for hiding this comment

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

요것도 출력과 관련있는 내용인 것 같아요. 만약에, 저희가 gui를 제공하도록 바뀌어야한다고 하면 어디가 영향을 받을까요?

Copy link
Author

Choose a reason for hiding this comment

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

private final StringBuilder gameRoundsOutput, recordOutput() 들이 영향을 받을 것 같습니다.
이러한 부분들이 carRace의 상태라 생각했었는데 출력을 위한 형태였던 것 같습니다.
그럼 이러한 로직들은 OutputView에서 carRace의 상태를 가지고 문자열로 바꿔서 출력하는 형태로 진행하는게 더 적절한건가요?

Choose a reason for hiding this comment

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

추후 출력이 변경되었을 때의 생각을 해보면 됩니다.

현재 상태라면 만약 출력형식이 gui 또는 웹 응답 형태가 되었을 때, CarRace의 코드 또한 바뀌어야 할 것입니다. 이러한 형태는 그리 좋지 못합니다.

우리가 생각했을 때 출력에 대한 책임은 OutputView가 온전히 들고있어야 하는데, 그렇지 못하단 이야기거든요. CarRace도 들고있는 것이죠.

Choose a reason for hiding this comment

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

이러한 로직들은 OutputView에서 carRace의 상태를 가지고 문자열로 바꿔서 출력하는 형태로 진행하는게 더 적절한건가요?

좀더 핑퐁을 하면서 유도하고 싶지만 우리는 시간이 많지 않으니 😅
넵 그렇습니다.

view가 domain형태로 된 객체를 직접 다룸으로써 출력에 대한 책임을 온전히 가질 수 있습니다.
우리는 내부 연산결과를 우리가 약속한 CarRace라는 도메인 객체로 OutputView에 제공하면서, view에 대한 의존을 끊을 수 있는거죠.

이에 대해서 깊게 생각해보시면 좋을 것 같습니다. 이번 단원의 주요한 내용이거든요. 4단계의 힌트에 대해서 파보는 걸 추천드립니다.

gameRoundsOutput.append("\n").append(car.getName()).append(" : ")
.append(DISTANCE_EXPRESSION.repeat(car.getDistance()));
}

private int getMaxDistanceFromCars() {
return cars.stream().mapToInt(Car::getDistance).max().orElse(0);
}

private List<String> getWinnerCarNamesForMaxDistance(int maxDistance) {
return cars.stream().filter(car -> car.getDistance() == maxDistance).map(Car::getName)
.toList();
}

public List<String> getWinnerCarNames() {
return winnerCarNames;
}

public String getGameRoundsOutput() {
return gameRoundsOutput.toString();
}
}
5 changes: 5 additions & 0 deletions src/main/java/domain/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package domain;

public interface NumberGenerator {

Choose a reason for hiding this comment

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

NumberGenerator라는 Interface를 만들었군요 혹시 이 Interface는 어떻게 쓰이나요?

Copy link
Author

Choose a reason for hiding this comment

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

추상화를 위한 목적보다는 랜덤한 값을 생성하는 메서드를 테스트 가능하게 만들기 위해 인터페이스로 분리했습니다.

Choose a reason for hiding this comment

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

@mock을 사용하기 위함이라는 말로 이해했습니당. 맞을까요?
하지만, @mock은 Interface가 아닌 클래스로도 사용할 수 있어요.

int generate();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package domain.NumberGeneratorImpl;

import domain.NumberGenerator;
import java.util.Random;

public class RandomNumberGenerator implements NumberGenerator {
private final int MAX_RANDOM_NUMBER = 10;

Choose a reason for hiding this comment

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

위의 값들은 static을 안 썼고, 아래 Random은 static을 썼군요. 어떤 차이가 있나요?

그리고 자바에서 static이 붙고 안붙는게 변수이름에 어떤식으로 적용이 될까요?
(참고 자료 : 구글 자바 네이밍 컨벤션)

Copy link
Author

Choose a reason for hiding this comment

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

static을 붙이면 해당 객체를 생성할 때 하나의 인스턴스를 공유합니다. 상수로 작성하려했다면 static을 붙여야했습니다.

private static finalUPPER_SNAKE_CASE, private finallowerCamelCase로 작성해야합니다. 컨벤션을 완전 무시하고 작성했었네요. 😢
또한, static final 이더라도 내용이 가변될 수 있다면 lowerCamelCase로 작성해야합니다.

private final int MIN_RANDOM_NUMBER = 0;
private static final Random random = new Random();

@Override
public int generate() {
return random.nextInt(MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER;
}
}
7 changes: 7 additions & 0 deletions src/main/java/exception/CarNameTooLongException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package exception;

public class CarNameTooLongException extends IllegalArgumentException {
public CarNameTooLongException(String message) {
super(message);
}
}
20 changes: 20 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package view;

import java.util.InputMismatchException;
import java.util.Scanner;

public class InputView {
Scanner scanner = new Scanner(System.in);

public String readCarNames() {
return scanner.nextLine();
}

public int readGameRounds() {
try {
return scanner.nextInt();
} catch (InputMismatchException e) {
throw new IllegalArgumentException("정수가 아닌 입력입니다.");
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package view;

import java.util.List;

public class OutputView {
private final String WINNER_DELIMITER = ", ";

public void printInputCarsName(){
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
}

public void printInputGameRounds(){
System.out.println("시도할 회수는 몇회인가요?");
}

public void printGameResult(String gameResult){
System.out.println(gameResult);
}

public void printWinnerCarNames(List<String> carNames){
System.out.print(String.join(WINNER_DELIMITER, carNames));
System.out.println("가 최종 우승했습니다.");
}
}
62 changes: 62 additions & 0 deletions src/test/java/CarRaceAppTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import domain.Car;
import domain.CarNameParser;
import domain.CarRace;
import domain.NumberGenerator;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import view.OutputView;

public class CarRaceAppTest {
@Mock
NumberGenerator randomNumberGenerator;

@Test
@DisplayName("자동차 경주 애플리케이션 출력 테스트")
void carRaceTest() {
// Given
MockitoAnnotations.openMocks(this);
int gameRounds = 3;
String carNamesInput = "a,b";
CarNameParser carNameParser = new CarNameParser();
List<Car> cars = carNameParser.parse(carNamesInput).stream().map(Car::new).toList();
when(randomNumberGenerator.generate()).thenReturn(1, 5, 1, 5, 1, 5);

ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
OutputView outputView = new OutputView();
CarRace carRace = new CarRace(cars, gameRounds, randomNumberGenerator);

// When
carRace.start();
outputView.printGameResult(carRace.getGameRoundsOutput());
outputView.printWinnerCarNames(carRace.getWinnerCarNames());

// Then
String output = outContent.toString();
String[] lines = output.split("\\R");
assertThat(lines).containsExactly(
"",
"실행결과",
"a : ",
"b : -",
"",
"a : ",
"b : --",
"",
"a : ",
"b : ---",
"",
"b가 최종 우승했습니다."
);

System.setOut(System.out);
}
}
25 changes: 25 additions & 0 deletions src/test/java/domain/CarNameParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package domain;

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

import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class CarNameParserTest {
@ParameterizedTest
@ValueSource(strings = {"a,b,c", " a, b,c "," a , b, c"})
@DisplayName("자동차 문자열 파싱 테스트")
void splitCarNamesString(String carNamesInput) {
// Given
CarNameParser carNameParser = new CarNameParser();
List<String> expected = List.of("a", "b", "c");

// When
List<String> actual = carNameParser.parse(carNamesInput);

// Then
assertThat(actual).isEqualTo(expected);
}
}
Loading