Skip to content
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

[자동차 경주] 정예림 미션 제출합니다. #75

Merged
merged 16 commits into from
Feb 18, 2025
Merged
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
9 changes: 9 additions & 0 deletions src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import controller.RacingCarApplication;

public class Main {
public static void main(String[] args) {

// 레이싱 게임 애플리케이션 실행

Choose a reason for hiding this comment

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

코드로 이미 설명이 된다면 주석이 굳이 필요할까도 고민해보셔도 좋을 것 같습니다. 다른 부분도 포함입니담

new RacingCarApplication().run();
}
}
27 changes: 27 additions & 0 deletions src/main/java/controller/RacingCarApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package controller;

import domain.Race;
import domain.RacingCar;
import view.InputView;
import view.ResultView;

import java.util.List;

public class RacingCarApplication {

// 레이싱 게임 실행
public void run() {
try {
List<RacingCar> cars = InputView.getCarNames();
int rounds = InputView.getRoundsFromUser();
Comment on lines +15 to +16

Choose a reason for hiding this comment

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

getCarNames에서는 FromUser를 안쓰지만, getRoundsFromUser에서는 쓰셨군요. 코드든 네이밍이든 일관성을 지키면 좋을 것 같습니다.


Race race = new Race(cars);
race.race(rounds);

ResultView.printRaceResults(race);
ResultView.printWinners(race.findWinners());
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
Comment on lines +14 to +25

Choose a reason for hiding this comment

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

이러한 방식은 좋지 않습니다.
InputView, Race, ResultView 중 어디에서 예외가 발생했는지 명확하지 않아서 디버깅이 힘들어집니다.

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

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

public class Race {
private final List<RacingCar> cars;

Choose a reason for hiding this comment

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

컬렉션은 null 안정성을 위해 필드에서 바로 초기화하는게 좋습니다.


public Race(List<RacingCar> cars) {
this.cars = new ArrayList<>(cars);
}

// 모든 자동차를 한 번씩 이동
public void moveCars() {
for (RacingCar car : cars) {
car.move();
}
}

public void race(int rounds) {
for (int i = 0; i < rounds; i++) {
moveCars();
}
}

public List<RacingCar> getCars() {
return cars;
}

// 자동차들 중 최대 위치를 반환
private int getMaxPosition() {
int max = 0;
for (RacingCar car : cars) {
if (car.getPosition() > max) {
max = car.getPosition();
}
}
return max;
}

// 최대 위치에 도달한 자동차 목록 반환
public List<RacingCar> findWinners() {
int maxPosition = getMaxPosition();
List<RacingCar> winners = new ArrayList<>();

for (RacingCar car : cars) {
if (car.getPosition() == maxPosition) {
winners.add(car);
}
}
return winners;
}
}
41 changes: 41 additions & 0 deletions src/main/java/domain/RacingCar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package domain;

import java.util.Random;

public class RacingCar {
private final String name;
private final Random random;
private int position;

// Random 객체를 생성자에서 받아옴
public RacingCar(String name, Random random) {
verifyCarName(name);
this.name = name;
this.random = random;
this.position = 0;
}

private void verifyCarName(String name) {
if (name.isEmpty() || name.length() > 5) {
throw new IllegalArgumentException("자동차 이름은 1~5자만 가능합니다.");

Choose a reason for hiding this comment

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

커스텀 예외에 대해서도 알아보시면 좋을 것 같아요

}
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}

// 난수 0~9중 4 이상이 나오면 전진
public boolean move() {
int randomValue = random.nextInt(10);
if (randomValue >= 4) {
position++;
return true; // 전진 성공
}
return false; // 전진 실패 (정지)
}
Comment on lines +33 to +40

Choose a reason for hiding this comment

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

전진 성공 실패로 인하여 로직이 달라지지 않는데 boolean을 반환해줄 이유가 있을까요?

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

import domain.RacingCar;

import java.util.List;
import java.util.Scanner;
import java.util.Random;
import java.util.ArrayList;

public class InputView {

Choose a reason for hiding this comment

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

이미 괜찮기는 한데, 해당 클래스의 메서드를 좀더 모듈화해도 좋을것같습니담

private static final Scanner scanner = new Scanner(System.in);
private static final Random random = new Random();

public static List<RacingCar> getCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)로 구분):");
String input = scanner.nextLine();
String[] carNames = input.split(",");
List<RacingCar> cars = new ArrayList<>();

for (String name : carNames) {
name = name.trim();
cars.add(new RacingCar(name, random));
}
return cars;
}

public static int getRoundsFromUser() {
System.out.println("시도할 횟수는 몇 회인가요?");
while (!scanner.hasNextInt()) {
System.out.println("숫자를 입력해야 합니다. 다시 입력하세요:");
scanner.nextLine();
}
Comment on lines +29 to +32

Choose a reason for hiding this comment

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

👍


int rounds = scanner.nextInt();
scanner.nextLine();

if (rounds <= 0) {
throw new IllegalArgumentException("시도 횟수는 1 이상의 정수여야 합니다.");
}
Comment on lines +37 to +39

Choose a reason for hiding this comment

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

해당 예외는 Race에 있는게 적절할 것 같습니다. 이유를 설명하는것은 숙제로 드리겠습니다 📖


return rounds;
}
}
26 changes: 26 additions & 0 deletions src/main/java/view/ResultView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package view;

import domain.Race;
import domain.RacingCar;

import java.util.List;

public class ResultView {
public static void printRaceResults(Race race) {
System.out.println("\n실행 결과:");
for (RacingCar car : race.getCars()) {
System.out.print(car.getName() + " : ");
System.out.println("-".repeat(car.getPosition())); // 위치만큼 '-' 출력
}
}

public static void printWinners(List<RacingCar> winners) {
System.out.println(String.join(", ", getWinnerNames(winners)) + "가 최종 우승했습니다.");
}

private static List<String> getWinnerNames(List<RacingCar> winners) {
return winners.stream()
.map(RacingCar::getName)
.toList();
}
}
98 changes: 98 additions & 0 deletions src/test/java/RaceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import java.util.List;
import java.util.Random;

import domain.Race;
import domain.RacingCar;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;

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

@DisplayName("우승 자동차 구하기 테스트")
class RaceTest {

Choose a reason for hiding this comment

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

해당 클래스는 들여쓰기 길이가 다른것 같습니다. 체크해주세요!

// 항상 전진하는 moveRandom 객체 생성
private static final Random moveRandom = new Random() {
@Override
public int nextInt(int bound) {
return 4;
}
};
// 항상 정지하는 stopRandom 객체 생성
private static final Random stopRandom = new Random() {
@Override
public int nextInt(int bound) {
return 3;
}
};
Comment on lines +14 to +27

Choose a reason for hiding this comment

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

👍 👍 좋은 방법이네요.
대신 클래스를 분리하거나 하면 좋을 것 같습니다.
RacingCar에서도 비슷한 로직이 있는것 같아서요

private RacingCar carA;
private RacingCar carB;
private Race race;

@BeforeEach
void setup() {
// CarA는 매 라운드마다 전진하고, CarB는 매 라운드마다 정지함

Choose a reason for hiding this comment

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

그렇다면 차 이름을 moveCar, stopCar 이렇게 지어도 좋았을 것 같아요.
주석은 정보를 제공하는 좋은 요소지만, 너무 많은 정보는 오히려 불편할수도있다고 생각해요

carA = new RacingCar("CarA", moveRandom);
carB = new RacingCar("CarB", stopRandom);
race = new Race(List.of(carA, carB));
}

@DisplayName("race()가 라운드 수만큼 자동차를 전진/정지 시키는가?")
@Test
void checkCarsMoveCountAfterRace() {

Choose a reason for hiding this comment

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

DisplayName을 사용하지않고 한글로 테스트명을 작성하는 것도 좋습니다

race.race(5);

assertThat(carA.getPosition())
.as("CarA가 5번의 라운드 동안 5번 전진해야 합니다.")
.isEqualTo(5);

assertThat(carB.getPosition())
.as("CarB가 5번의 라운드 동안 5번 정지해야 합니다.")
.isEqualTo(0);
}

@DisplayName("findWinners() 메서드가 올바른 우승자를 반환하는가?")
@Test
void verifyCorrectWinners() {
race.race(5);
List<RacingCar> winners = race.findWinners();

assertThat(winners)
.hasSize(1)
.extracting(RacingCar::getName)
.containsExactly("CarA");
}

@DisplayName("동점인 경우, 우승자가 여러 명이 나오는가?")
@Test
void checkMultipleWinners() {
// CarB도 항상 전진하도록 설정 (동점 상황)
carB = new RacingCar("CarB", moveRandom);
race = new Race(List.of(carA, carB));

race.race(5);
List<RacingCar> winners = race.findWinners();

assertThat(winners)
.hasSize(2)
.extracting(RacingCar::getName)
.containsExactly("CarA", "CarB");
}

@DisplayName("모든 자동차가 정지한 경우, 전부 우승자로 나오는가?")
@Test
void allCarsStopped_AllAreWinners() {
// CarA, CarB 둘 다 정지하도록 설정
carA = new RacingCar("CarA", stopRandom);
carB = new RacingCar("CarB", stopRandom);
race = new Race(List.of(carA, carB));

race.race(5);
List<RacingCar> winners = race.findWinners();

assertThat(winners)
.hasSize(2)
.extracting(RacingCar::getName)
.containsExactly("CarA", "CarB");
}
}
69 changes: 69 additions & 0 deletions src/test/java/RacingCarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import java.util.Random;

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

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

@DisplayName("움직이는 자동차 테스트")
class RacingCarTest {
private static final String TEST_CAR_NAME = "car";

// 고정된 난수 값을 반환하는 MockRandom 클래스 (Random 상속)
private static class MockRandom extends Random {
private final int mockedValue;

public MockRandom(int mockedValue) {
this.mockedValue = mockedValue;
}

@Override
public int nextInt(int bound) {
return mockedValue;
}
}

private RacingCar carWithMockRandom(int mockedValue) {
return new RacingCar(TEST_CAR_NAME, new MockRandom(mockedValue));
}

@DisplayName("자동차 이름이 5자를 초과하면 예외가 발생하는가?")
@Test
void checkCarNameIsNotOver5Letters() {
assertThatThrownBy(() -> new RacingCar("Boxster", new MockRandom(4)))

Choose a reason for hiding this comment

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

오.. 포르쉐 박스터..! 🚗

.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("자동차 이름은 1~5자만 가능합니다.");
}

@DisplayName("랜덤값이 4 이상일 경우, 항상 전진하는가?")
@Test
void moveWhenValueIsAtLeast4() {
int[] testValues = {4, 5, 6, 7, 8, 9}; // 전진해야 하는 테스트 값

for (int mockedValue : testValues) {
RacingCar car = carWithMockRandom(mockedValue);
boolean isMoved = car.move();

assertThat(isMoved)
.as("랜덤 값이 %d일 때 자동차가 전진해야 합니다.", mockedValue)
.isTrue();
}
Comment on lines +43 to +52

Choose a reason for hiding this comment

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

참신한 테스트 방식이네요 👍

}

@DisplayName("랜덤값이 3 이하일 경우, 항상 정지하는가?")
@Test
void stopWhenValueIsLessThan4() {
int[] testValues = {0, 1, 2, 3}; // 정지해야 하는 테스트 값

for (int mockedValue : testValues) {
RacingCar car = carWithMockRandom(mockedValue);
boolean isMoved = car.move();

assertThat(isMoved)
.as("랜덤 값이 %d일 때 자동차가 정지해야 합니다.", mockedValue)
.isFalse();
}
}
}