Skip to content

[ 자동차 경주 - 초간단 애플리케이션 ] 미션 제출 #134

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 13 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from 12 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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### 1단계
- 자동차는 이름을 갖는다.
- 자동차는 0-9 사이 랜덤 값을 구한 후 4이상이면 전진, 3이하면 멈춘다.
- 3항 연산자를 쓰지 않는다.
- else 예약어, switch/case 허용하지 않는다.
- 메소드 길이가 15라인이 넘어가지 않도록 구현한다
- 메인 메소드를 만들지 않는다.
- 테스트 코드 작성한다.

### 2단계
- n대의 자동차가 참여가능하다.
- 주어진 횟수동안 자동차 경주 게임을 완료한 후 누가 우승했는지 알 수 있다.
- 우승자는 한 명 이상일 수 있다.
- 나머진 1단계와 유사하다.

### 3단계
- 자동차에 이름 부여가능하고, 전진하는 자동차 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 , 기준으로 구분하며 이름은 5자 이하만 가능하다.
Copy link

Choose a reason for hiding this comment

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

요구사항이 충족되지 않은것으로 보여요!

Copy link

Choose a reason for hiding this comment

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

추가적으로 현재는 자동차 이름이 중복될 수 있는데요. 고유한 자동차 이름을 갖게 변경해보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

InputView 에서 5자 초과 이름, 중복되는 자동차 입력 시 예외를 던지게 수정하였고, InputViewTest에서 이를 확인했습니다.

- 메인 메소드를 추가한다.

### 4단계
- 모든 로직에 단위 테스트를 구현한다.
- mvc 패턴으로 리팩터링한다.

### 개인적 목표
1. 주석이 없어도 읽기 좋은 코드를 짜보자.
2. 메서드가 한가지 기능만 갖도록 분리하다가 따로 관리하는게 좋을 것 같으면 클래스로 구현하자.
3. 단위 테스트 핵심 원칙을 따르자.
4. 개념에 머무르고 있었던 객체 지향적 개념을 활용하여 코드를 작성해보자.
5. 발생할 수 있는 예외 케이스를 고려하여 방어적인 코드를 작성하고, 이를 테스트 코드로 확인하자.
Comment on lines +25 to +30
Copy link

Choose a reason for hiding this comment

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

개인적인 목표도 적어주셨군요👍



1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.25.1')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
developmentOnly('org.springframework.boot:spring-boot-devtools')
Copy link

Choose a reason for hiding this comment

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

어떤 이유로 추가해주섰나요?

Copy link
Author

Choose a reason for hiding this comment

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

잘 기억이 나지 않습니다만 아마 Spring Boot DevTools is not configured. 문구가 떠서 추가한 것 같아요!

Copy link

Choose a reason for hiding this comment

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

어떤 이유로 발생한건지 찾아보시면 좋을 것 같아요!

}

test {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/racinggame/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racinggame;

import racinggame.controller.RacingGameController;

public class Main {
public static void main(String[] args) {
new RacingGameController().run();
}
}
32 changes: 32 additions & 0 deletions src/main/java/racinggame/controller/RacingGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package racinggame.controller;

import racinggame.model.RacingGame;
import racinggame.policy.decider.ThresholdBaseMoveDecider;
import racinggame.policy.evaluator.GreaterThanOrEqualThresholdEvaluator;
import racinggame.policy.strategy.MoveStrategy;
import racinggame.policy.strategy.OneStepMoveStrategy;
import racinggame.view.InputView;
import racinggame.view.OutputView;

import java.util.List;

public class RacingGameController {
public void run(){
List<String> names= InputView.readCarNames();
int moveCount = InputView.readMoveCount();

MoveStrategy strategy = new OneStepMoveStrategy(
new racinggame.policy.numbergenerator.ZeroToNineRandomGenerator(),
new ThresholdBaseMoveDecider(new GreaterThanOrEqualThresholdEvaluator(4)));
Copy link

Choose a reason for hiding this comment

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

MoveStrategy를 만들고 Car에 전략을 주입하여, Car에는 영향 없이 전략을 변경할 수 있는 구조로 만들어주셨군요!
여러가지 시도를 해보는 부분이 매우 좋네요😎

클래스를 세부적으로 나누는것은 어떤 트레이드오프가 있을까요? 구현해보면서 느낀점이 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

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

클래스가 단일 책임을 갖기에 유연한 교체와 확장이 가능하지만, 과해지면 각 클래스가 뭘 하는지 파악하는데 시간이 더 오래걸릴 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

아직 필요 없는 기능까지 미리 분리해두었더니 오히려 더 복잡해지는 것 같습니다.


RacingGame game = new RacingGame(names,moveCount,strategy);

for (int i = 0; i < moveCount; i++) {
game.moveOneTurn();
OutputView.printResult(game.getCars());
}

List<String> winners = game.getWinners();
OutputView.printWinner(winners);
}
}
27 changes: 27 additions & 0 deletions src/main/java/racinggame/model/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package racinggame.model;

import racinggame.policy.strategy.MoveStrategy;

public class Car {
private final String name;
private int movedDistance=0;
Copy link

Choose a reason for hiding this comment

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

final로 불변 변수를 명시해준 부분 좋습니다👍


private final MoveStrategy moveStrategy;

public Car(String name,MoveStrategy moveStrategy) {
this.name=name;
this.moveStrategy=moveStrategy;
}
Copy link

Choose a reason for hiding this comment

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

생성자에서 할당하는 변수값들에 대한 검증을 진행해보면 좋을 것 같아요. 어떤 장점이 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

객체를 생성한 시점부터 이름과 전략이 모두 설정된 상태를 유지할 수 있습니다!

Copy link
Author

Choose a reason for hiding this comment

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

이미 InputView에서 사용자 입력을 검증해주지만, 도메인 객체 자체가 항상 올바른 상태가 유지 되도록 2차 검증을 해줄 수 있습니다! 코드 수정 후 CarTest 에서 이를 확인해주었습니다.


public void move(){
int distance=moveStrategy.addStepSize();
movedDistance+=distance;
}

public int getMovedDistance() {
return movedDistance;
}
public String getName() {
return name;
}
}
54 changes: 54 additions & 0 deletions src/main/java/racinggame/model/RacingGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package racinggame.model;
import racinggame.policy.strategy.MoveStrategy;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class RacingGame {
private final List<Car> cars;
private final int moveCount;
Copy link

Choose a reason for hiding this comment

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

moveCount는 미사용되는것으로 보여요!

Copy link
Author

Choose a reason for hiding this comment

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

InputView 를 만들어주면서 이부분을 수정했어야 했는데 깜빡했습니다. 수정해뒀습니다!



public RacingGame(List<String> names, int moveCount, MoveStrategy moveStrategy) {
if (moveCount <= 0) {
throw new IllegalArgumentException("moveCount는 0보다 큰 값이여야 합니다.");
}
this.cars = createCars(names,moveStrategy);
this.moveCount = moveCount;
}

private void moveAllCars( ){
for (Car car : cars) {
car.move();
}
}

public void moveOneTurn() {
moveAllCars();
}
Copy link

Choose a reason for hiding this comment

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

moveAllCars()로 의미를 나타내주시려고 한 것 같은데, 내부 로직만으로도 그 의미가 전달될 것 같아요!

Copy link

Choose a reason for hiding this comment

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

전반적으로 메서드 순서에 대해 고민해보시고 변경해보시면 좋을 것 같아요!


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

public List<String> getWinners(){
int maxDistance=findMaxDistance();
return cars.stream().filter(car->car.getMovedDistance()==maxDistance).map(Car::getName).collect(Collectors.toList());
Copy link

Choose a reason for hiding this comment

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

method chaining을 사용하는 경우 개행을 이용하면 가독성을 높일 수 있어요!

}

private List<Car> createCars(List<String> names, MoveStrategy strategy) {
List<Car> cars = new ArrayList<>();
for(String name : names){
cars.add(new Car(name,strategy));
}
return cars;
}
Copy link

Choose a reason for hiding this comment

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

stream을 사용해보면 어떨까요? for문 과 stream의 장단점을 학습해보시면 좋을 것 같아요

private int findMaxDistance(){
int max=0;
for (Car car : cars) {
max=Math.max(max,car.getMovedDistance());
}
return max;
}
Copy link

Choose a reason for hiding this comment

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

위와 같습니다!

}
36 changes: 36 additions & 0 deletions src/main/java/racinggame/model/WinnerFinder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racinggame.model;

import java.util.List;
import java.util.stream.Collectors;

public class WinnerFinder {
Copy link

Choose a reason for hiding this comment

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

미사용되는걸로 보여요 👀

Copy link
Author

Choose a reason for hiding this comment

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

실수로 2단계를 건너뛰고 3단계 구현을 ( RacingGame ) 먼저 해뒀기 때문에 해당 클래스는 2단계 구현 내용을 보여주기 위해 별도로 작성한 클래스입니다!

Copy link
Author

Choose a reason for hiding this comment

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

이 클래스를 테스트해보기 위한 WinnerFindTest 클래스도 있는데 둘 다 삭제할까요!

Copy link

Choose a reason for hiding this comment

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

특정 단계를 위한 코드를 남겨두지 않으셔도 됩니다!
각 단계는 이런 순서로 진행하면 좋겠다를 의미하는거고 최종 완성된 코드는 하나라고 이해해주시면 될 것 같아요!

private final List<Car> cars;
private final int moveCount;
public WinnerFinder(List<Car> cars, int moveCount){
this.cars = cars;
this.moveCount = moveCount;
}

public void play(){
for(int i=0;i<moveCount;i++){
moveAllCars();
}
}
public List<String> getWinners(){
int maxDistance=maxDistance();
return cars.stream().filter(car->car.getMovedDistance()==maxDistance).map(Car::getName).collect(Collectors.toList());
}

private void moveAllCars( ){
for (Car car : cars) {
car.move();
}
}
private int maxDistance(){
int max=0;
for (Car car : cars) {
max=Math.max(max,car.getMovedDistance());
}
return max;
}
}
5 changes: 5 additions & 0 deletions src/main/java/racinggame/policy/decider/MoveDecider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racinggame.policy.decider;

public interface MoveDecider {
boolean canMove(int randomNumber);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package racinggame.policy.decider;

import racinggame.policy.evaluator.ThresholdEvaluator;

public class ThresholdBaseMoveDecider implements MoveDecider {
private final ThresholdEvaluator evaluator;

public ThresholdBaseMoveDecider(ThresholdEvaluator evaluator) {
this.evaluator = evaluator;
}

@Override
public boolean canMove(int randomNumber){
return evaluator.isSatisfied(randomNumber);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package racinggame.policy.evaluator;

public class GreaterThanOrEqualThresholdEvaluator implements ThresholdEvaluator {
Copy link

Choose a reason for hiding this comment

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

비교로직이 바뀔때마다 구현체를 만들어줘야할것 같은데, 어떠한 장단점이 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

새로운 비교 로직 ( GreaterThan 이나 LessThan )은 새 클래스만 추가하면 되어 이점이 있겠지만, 패키지 구조가 너무 복잡해질 수 있을 것 같습니다.

private final int threshold;

public GreaterThanOrEqualThresholdEvaluator(int threshold) {
this.threshold = threshold;
}
@Override
public boolean isSatisfied(int value){
return value >= threshold;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racinggame.policy.evaluator;

public interface ThresholdEvaluator {
boolean isSatisfied(int value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racinggame.policy.numbergenerator;

public interface RandomGenerator {
int generate();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racinggame.policy.numbergenerator;
import java.util.Random;

public class ZeroToNineRandomGenerator implements RandomGenerator {
private final Random random=new Random();

@Override
public int generate() {
return random.nextInt(10);
}
}
5 changes: 5 additions & 0 deletions src/main/java/racinggame/policy/strategy/MoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racinggame.policy.strategy;

public interface MoveStrategy {
int addStepSize();
}
23 changes: 23 additions & 0 deletions src/main/java/racinggame/policy/strategy/OneStepMoveStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racinggame.policy.strategy;

import racinggame.policy.decider.MoveDecider;

public class OneStepMoveStrategy implements MoveStrategy {
private static final int STEP_SIZE = 1;

private final racinggame.policy.numbergenerator.RandomGenerator randomGenerator;
private final MoveDecider moveDecider;

public OneStepMoveStrategy(racinggame.policy.numbergenerator.RandomGenerator randomGenerator, MoveDecider moveDecider) {
this.randomGenerator = randomGenerator;
this.moveDecider = moveDecider;
}
@Override
public int addStepSize(){
int randomNumber=randomGenerator.generate();
if(moveDecider.canMove(randomNumber)){
return STEP_SIZE;
}
return 0;
}
}
29 changes: 29 additions & 0 deletions src/main/java/racinggame/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package racinggame.view;

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class InputView {
// scanner는 공유자원
private static final Scanner scanner = new Scanner(System.in);
Copy link

Choose a reason for hiding this comment

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

(1) 왜 주석을 달아주셨나요?

(2) 신규 생성할 필요 없는 객체를 static으로 선언하여 재사용하는것 좋습니다! (어떤 장점이 있을까요?)

Copy link
Author

Choose a reason for hiding this comment

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

(1) 공부하다가 달아둔거라 별 다른 의미 없습니다! scanner가 상태를 갖고 변경되는 객체라고 생각하였는데, 내부 커서를 유지하며 한번 읽은 내용은 다시 읽지 않기 때문에 private static final 으로 선언 가능하다는 점을 배웠습니다.

(2) 생성자를 통해 객체를 만들면 힙에 객체가 할당되고, 일정 시간이 지나면 가비지 컬렉션가 이를 관리해준다고 알고 있습니다. static으로 선언해준다면 같은 인스턴스를 재사용하기 때문에 관리할 대상이 줄어드는 점이 장점입니다.

Copy link

Choose a reason for hiding this comment

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

학습용으로 주석을 달아주신거긴 하지만, 코드만으로도 의미가 명확할때는 주석을 최대한 달지 않는게 좋답니다!
코드 변경 시 주석도 함께 챙겨줘야한다는 관리포인트가 생기는거여서요.


public static List<String> readCarNames() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
String[] inputNames = scanner.nextLine().split(",");
Copy link

Choose a reason for hiding this comment

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

매직넘버/리터럴을 상수로 선언하여 이름을 부여해보는 연습을 해보면 어떨까요?

List<String> names = Arrays.stream(inputNames)
.map(String::trim)
.collect(Collectors.toList());
return names;
}
Copy link

Choose a reason for hiding this comment

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

names를 변수로 할당할 필요가 있을까요?

Copy link
Author

@qkrcodus qkrcodus May 15, 2025

Choose a reason for hiding this comment

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

자동차 이름의 고유성과 5자 이내 조건을 검증하기 위해서 코드를 수정하다 보니 names 가 필요해 보입니다!

Copy link

Choose a reason for hiding this comment

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

기존에는 변수로 할당하지 않고 바로 return해도 되어서 말씀드린거였어요!


public static int readMoveCount() {
System.out.println("시도할 회수는 몇회인가요?");
int moveCount = scanner.nextInt();
if (moveCount <= 0) {
throw new IllegalArgumentException("moveCount는 0보다 큰 값이여야 합니다.");
}
return moveCount;
}
}
20 changes: 20 additions & 0 deletions src/main/java/racinggame/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package racinggame.view;

import racinggame.model.Car;

import java.util.List;

public class OutputView {
public static void printResult(List<Car> cars) {
System.out.println("\n실행 결과");
for (Car car : cars) {
System.out.println(car.getName() + " : " + "-".repeat(car.getMovedDistance()));
}
System.out.println();
}

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

}
Loading