Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
69d278c
feat: Car 클래스 작성
JanooGwan Sep 22, 2025
9229ec4
feat: 게임 정보 입력 & 관련 DTO 작성
JanooGwan Sep 22, 2025
685fe5a
feat: 레이스 상황 출력 관련 DTO 작성
JanooGwan Sep 22, 2025
40da36d
feat: 레이스 최종 결과 출력 관련 DTO 작성
JanooGwan Sep 22, 2025
61d357e
refactor: Car 클래스에 static 멤버 변수 추가
JanooGwan Sep 22, 2025
7bbb90e
feat: InputView & OutputView 작성
JanooGwan Sep 22, 2025
f64079a
feat: StringUtils 작성
JanooGwan Sep 22, 2025
a7ff546
refactor: StringUtils - 문자열을 이용해 Car들을 만드는 메소드 추가
JanooGwan Sep 22, 2025
45ea126
refactor: OutputView - 경기 상황, 경기 결과 출력 메소드 추가
JanooGwan Sep 22, 2025
37fa75d
feat: RacingController 작성
JanooGwan Sep 22, 2025
3225b97
feat: RaceManager 작성
JanooGwan Sep 22, 2025
8b31444
refactor: RacingController, RaceManager 수정
JanooGwan Sep 22, 2025
50e6030
refactor: InputView 수정
JanooGwan Sep 22, 2025
279705a
feat: CheckCarName 클래스 작성
JanooGwan Sep 22, 2025
a2c3577
refactor: CheckCarName 수정
JanooGwan Sep 22, 2025
8d25826
refactor: InputView 수정
JanooGwan Sep 22, 2025
d728091
feat: CheckTryCount 클래스 작성
JanooGwan Sep 22, 2025
7da49d6
refactor: RacingController - 유효성 검증 관련 수정
JanooGwan Sep 22, 2025
84f9b6f
style: 불필요한 공백 제거
JanooGwan Sep 26, 2025
7313a94
refactor: 자동차 이름 검증 로직 - 생성자 영역으로 이동
JanooGwan Sep 26, 2025
7fba792
style: 접근 제어자(private) 추가
JanooGwan Sep 26, 2025
d7231b8
refactor: 유효성 검증 관련 클래스 이름 변경(Validator)
JanooGwan Sep 26, 2025
cb66dfc
style: 불필요한 공백 제거
JanooGwan Sep 26, 2025
014e73c
refactor: Iterator 관련 부분 Stream을 활용한 형태로 변경
JanooGwan Sep 26, 2025
f1e0633
refactor: StringUtils - Stream을 활용한 형태로 변경
JanooGwan Sep 26, 2025
2ac8837
refactor: RacingController의 생성자 - 외부에서 객체를 주입받도록 변경
JanooGwan Sep 26, 2025
4a8e1f0
feat: DTO - RaceInfoRequest 작성
JanooGwan Sep 28, 2025
a96b0a3
feat: DTO - RaceInfoResponse 작성
JanooGwan Sep 28, 2025
b6e5763
refactor: StringUtils - DTO 변경으로 인한 수정
JanooGwan Sep 28, 2025
903d754
refactor: StringUtils 반환 타입 변경 및 관련 코드 수정
JanooGwan Sep 28, 2025
12502f4
refactor: RacingController - RaceInfoRequest(DTO)를 활용한 형태로 변경
JanooGwan Sep 28, 2025
dcede87
refactor: StringUtils - String 기반 Car 생성 메소드 수정
JanooGwan Sep 29, 2025
b93d0f6
refactor: RacingController - StringUtils의 메소드를 활용하여 Car 생성하도록 변경
JanooGwan Sep 29, 2025
593e6cc
refactor: TryCountValidator - isNumber 메소드 추가
JanooGwan Sep 29, 2025
0b2d351
refactor: InputView - getNumInput() 삭제
JanooGwan Sep 29, 2025
69b3c01
refactor: RacingController - tryCount 관련 수정
JanooGwan Sep 29, 2025
dc2bbdd
style: 불필요한 공백 제거
JanooGwan Sep 29, 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
30 changes: 30 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## 구현해야 하는 기능 목록

### 자동차
- [x] 클래스 기본 틀 작성(변수, 생성자 등)
- [x] 랜덤 수(0~9) 반환 메소드
- [x] 자동차를 한 칸 전진하는 메소드

### 입력
- [x] 자동차 이름 & 입력 횟수 입력
- [x] 입력받은 데이터 전달

### 출력
- [x] 라운드 진행 시마다 경기 진행 상황 출력
- [x] 경주 종료 후 최종 결과 및 우승자 출력

### 예외 처리
- [x] 자동차 이름 관련
- [x] 시도 횟수 관련


### 고민 사항
- 자동차 정보를 입력받는 DTO를 어떻게 설계하는 것이 좋을까?
- 입력 텍스트 그대로(poni,woni,jun)를 DTO에 넣기
- 이름을 분리하여 List<String>에 넣기
- 이름을 분리한 것을 바탕으로 Car 객체들로 변환하고, List<Cars>에 넣기
- Util 패키지 관련
- 객체들을 new로 생성해서 사용하는 것이 좋은가?
- 아니면 객체를 별도로 생성하지 않고 객체 내부 메소드들을 모두 static 메소드로 만들어서 쓰는 것이 나은가?
- 시도 횟수를 입력할 때, 숫자가 아닌 다른 형태의 값을 입력했을 경우
- CheckTryCount 라는 별도의 클래스에서 이를 검증할 방법은 없을까?(현재 코드는 sc.nextInt() 하는 시점에 try-catch를 통해 예외처리 하고 있음)
11 changes: 10 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package racingcar;

import racingcar.controller.RacingController;
import racingcar.controller.dto.RaceInfoResponse;
import racingcar.util.RaceManager;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
RacingController racingController = new RacingController(new InputView(), new OutputView(), new RaceManager());

RaceInfoResponse raceInfo = racingController.getInfosBeforeRaceStart();
racingController.raceStart(raceInfo);
}
}
58 changes: 58 additions & 0 deletions src/main/java/racingcar/controller/RacingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package racingcar.controller;

import racingcar.controller.dto.RaceInfoRequest;
import racingcar.controller.dto.RaceInfoResponse;
import racingcar.util.TryCountValidator;
import racingcar.util.RaceManager;
import racingcar.util.StringUtils;
import racingcar.model.Car;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.List;

public class RacingController {
private final InputView inputView;
private final OutputView outputView;
private final RaceManager raceManager;

public RacingController(InputView inputView, OutputView outputView, RaceManager raceManager) {
this.inputView = inputView;
this.outputView = outputView;
this.raceManager = raceManager;
}

public RaceInfoResponse getInfosBeforeRaceStart() {
outputView.getCars();
String racerStr = inputView.getStringInput();

outputView.getTryCount();
String tryCnt = inputView.getStringInput();
TryCountValidator.checkTryCount(tryCnt);
int tryCount = Integer.parseInt(tryCnt);
Comment on lines +26 to +32
Copy link

Choose a reason for hiding this comment

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

RacingController에서 자동차 이름, 시도 횟수를 입력받는 메소드를 보시면 한 번에 두 입력을 받도록 하고 있어요. 이 부분은 분리가 가능하지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

네 맞습니다
보고서에도 작성했지만 이 부분을 분리한다면 아래 처럼 분리해보려고 하는데,

public String getCarNames() {
outputView.getCars();
String raceStr = inputView.getStringInput();
...
}

public String getTryCount() {
outputView.getTryCount();
String raceStr = inputView.getStringInput();
...
}

이렇게 한 다음 데이터를 처리하는 메소드를 아래 처럼 만들어서,

public RaceInfoResponse readyForRace(String carStrs, String tryCount) {
TryCountValidator.isPositive(tryCnt);
List carNames = StringUtils.splitByComma(carNames);
}

이렇게 작성하는 방식은 어떤가요??

Copy link

Choose a reason for hiding this comment

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

좋은거 같아요!
그리고 입력 받는 메소드 명도 get~~ 대신 다른 단어로 바꿔보는 것도 좋을 것 같아요


RaceInfoRequest raceInfoInput = new RaceInfoRequest(racerStr, tryCount);

List<String> carNames = StringUtils.splitByComma(raceInfoInput.carNames());

return new RaceInfoResponse(carNames, tryCount);
Copy link

Choose a reason for hiding this comment

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

new로 객체를 생성하는 것도 좋지만 정적 팩토리 메소드를 적용해보시면 가독성 측면에서 더 좋아질 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

보고서에서 첨부해 주신 링크 잘 읽었습니다
명확하게 알고 있지는 못했던 개념인데, 기존의 생성자 호출 방식에서 of, from 등을 활용하는 방식으로 다시 리팩토링 해보겠습니다..!

}

public void raceStart(RaceInfoResponse raceInfo) {
Copy link

Choose a reason for hiding this comment

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

raceStart() 메소드는 경주 시작을 의미하니 자동차의 움직임을 담당하는 책임을 지니고 있어요.
이 곳에서 자동차 리스트를 만들고, 우승자를 구하는 책임이 가중되는 것은 아닐까요?
하나의 메소드 호출만으로 모든 과정을 마치도록 하는 것이 아니라 메인 메소드에서 컨트롤러를 통해 입력, 자동차 객체 생성, 경주, 우승자 구하기 메소드를 각각 호출하는 형태에 대해서는 어떻게 생각하시나요??

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 대로 그런 부분들도 충분히 분리하여 코드 작성이 가능할 것 같습니다.
상술했듯이, 정보를 입력 받는 부분 / 입력받은 정보를 가공하는 부분 / 가공된 정보를 가지고 경주를 진행하는 부분
이렇게 세 가지로 나누면은 역할 분리가 좀 더 명확해질 것 같습니다

List<Car> cars = StringUtils.makeCarsUsingStrings(raceInfo.carNames());

outputView.printRaceStart();

for(int i = 0; i < raceInfo.gameCount(); ++i) {
raceManager.movingCars(cars);
outputView.printRaceStatus(cars);
}

finishRace(cars);
}

private void finishRace(List<Car> cars) {
List<String> winners = raceManager.findWinners(cars);
outputView.printRaceFinalStatus(winners);
}
}
7 changes: 7 additions & 0 deletions src/main/java/racingcar/controller/dto/RaceInfoRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.controller.dto;

public record RaceInfoRequest(
String carNames,
int gameCount
) {
}
9 changes: 9 additions & 0 deletions src/main/java/racingcar/controller/dto/RaceInfoResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racingcar.controller.dto;

import java.util.List;

public record RaceInfoResponse(
List<String> carNames,
int gameCount
) {
}
32 changes: 32 additions & 0 deletions src/main/java/racingcar/model/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package racingcar.model;

import racingcar.util.CarNameValidator;

public class Car {
private static final int CAN_MOVE_STANDARD = 4;
Copy link

Choose a reason for hiding this comment

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

private 변수인데, 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.

우선 private는, 자동차가 움직이는 기준 값을 외부에 공개할 필요가 없기 때문에 private를 사용하였고,
인스턴스 생성 때마다 CAN_MOVE_STANDARD 변수를 생성할 필요는 없다고 판단하여서, 클래스 내에서 공유할 수 있도록 static을 사용하였습니다.


private int location;
private String name;

public Car(String name) {
CarNameValidator.checkName(name);
this.name = name;
this.location = 0;
}

public void move() {
if(isAbleToMove()) ++location;
}

private boolean isAbleToMove() {
return (int)(Math.random() * 10) >= CAN_MOVE_STANDARD;
}

public int getLocation() {
return location;
}

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

public class CarNameValidator {
private static final int NAME_MAX_LENGTH = 5;

public static void isNull(String name) {
if(name == null)
throw new IllegalArgumentException("이름에는 null 값이 들어갈 수 없습니다.");
}

public static void isBlank(String name) {
if(name.isBlank())
throw new IllegalArgumentException("이름은 공백일 수 없습니다.");
}

public static void isMoreThanMaxLength(String name) {
if(name.length() > NAME_MAX_LENGTH)
throw new IllegalArgumentException("이름은 5자를 넘을 수 없습니다.");
Comment on lines +7 to +18
Copy link

Choose a reason for hiding this comment

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

예외 발생 시 작성하는 메시지도 결국 매직 넘버라 상수화하거나 따로 ErrorMessage와 같은 이름의 Enum으로 분리할 수 있지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

이번 과제처럼 규모가 크지 않은 프로젝트에서는 Enum으로 분리해서 예외를 관리해도 괜찮겠다 생각했지만,
규모가 큰 프로젝트에서는 Enum으로 관리할 예외들이 너무 많아져서 관리하기 힘들어진다는 이야기를 들었던 것 같습니다.
때문에, Enum으로 예외를 관리하는 방법도 정말 좋은 방법이라 생각되지만,
Enum을 사용하지 않고 예외 처리를 할 수 있는 방법에 대해 생각하던 중에 우선 이렇게 예외 처리를 설계해봤습니다.

Copy link

@dh2906 dh2906 Sep 23, 2025

Choose a reason for hiding this comment

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

생각해보니 Enum에서 관리한다면 ErrorMessage.OVER_NAME_LENGTH.getMessage() 와 같이 코드가 길어질 수 있겠네요..!
그렇다면 예외 메시지를 관리하는 클래스를 만드는 것이 더 깔끔할 것 같습니당

}

public static void checkName(String name) {
Copy link

Choose a reason for hiding this comment

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

check~~ 라는 네이밍은 그저 값을 '확인'하기만 하는 의미로 많이 쓰여 값이 유효한지 검증하는 의미의 validate~~를 사용하는 것은 어떠세요??
비슷한 내용의 글 공유드립니다.
자료

Copy link
Author

Choose a reason for hiding this comment

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

비슷한 의미를 가지는 것처럼 보여도 디테일한 차이가 있었군요..!
말씀해주신 것처럼 유효성 검증이라는 의미에 맞게 validate 로 수정하도록 하겠습니다

Copy link

Choose a reason for hiding this comment

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

check는 해석하는 방식에 따라 의미가 크게 바뀔 수 있어서 웬만해서는 사용을 지양하는 편으로 알고있어요!

isNull(name);
isBlank(name);
isMoreThanMaxLength(name);
}
}
20 changes: 20 additions & 0 deletions src/main/java/racingcar/util/RaceManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package racingcar.util;

import racingcar.model.Car;

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

public class RaceManager {
public void movingCars(List<Car> cars) {
for(var c : cars) c.move();
}

public List<String> findWinners(List<Car> cars) {
int maxLocation = cars.stream().mapToInt(Car::getLocation).max().orElse(0);
Copy link

Choose a reason for hiding this comment

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

메소드 체이닝은 주로 두 번째부터 줄바꿈을 사용해주고 있어요.
아래 예시 참고 해주세요

Suggested change
int maxLocation = cars.stream().mapToInt(Car::getLocation).max().orElse(0);
int maxLocation = cars.stream()
.mapToInt(Car::getLocation)
.max()
.orElse(0);

Copy link
Author

Choose a reason for hiding this comment

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

앗 참고하겠습니다..


List<String> winners = cars.stream().filter(c -> c.getLocation() == maxLocation).map(Car::getName).toList();
Copy link

Choose a reason for hiding this comment

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

여기도 마찬가지로 줄바꿈 적용해주세요!


return winners;
}
}
20 changes: 20 additions & 0 deletions src/main/java/racingcar/util/StringUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package racingcar.util;

import racingcar.model.Car;

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

public class StringUtils {
public static List<String> splitByComma(String str) {
return Arrays.stream(str.split(",")).toList();
}

public static String NumToSticks(int count) {
Copy link

Choose a reason for hiding this comment

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

여기 메소드 명이 파스칼 케이스로 적용되어 있어서 카멜 케이스로 변경해주세요!

return "-".repeat(count);
}

public static List<Car> makeCarsUsingStrings(List<String> strs) {
return strs.stream().map(Car::new).toList();
}
}
21 changes: 21 additions & 0 deletions src/main/java/racingcar/util/TryCountValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.util;

public class TryCountValidator {
Copy link

Choose a reason for hiding this comment

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

TryCount 라는 일급 객체를 만들고 Car처럼 생성자에서 입력받은 값의 유효성을 검증하는 것에 대해서는 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

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

보고서에서 언급해 주셨듯이, 말씀해주신 방법으로 일급 객체를 만든 다음 생성자 부분에서 유효성 검증을 옮기는 방법을 시도해 봐야겠습니다.
그렇게 하면 RacingController에 있던 유효성 관련 내용들을 일관성 있게 tryCount, Car 모두 생성자에 옮길 수 있을 것 같습니다

public static void isNumber(String tryCnt) {
try {
Integer.parseInt(tryCnt);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("시도 횟수에 문자를 입력할 수 없습니다.");
}
}

public static void isPositive(int tryCnt) {
if(tryCnt < 0)
throw new IllegalArgumentException("시도 횟수는 자연수여야 합니다.");
}

public static void checkTryCount(String tryCnt) {
isNumber(tryCnt);
isPositive(Integer.parseInt(tryCnt));
}
}
11 changes: 11 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package racingcar.view;

import java.util.Scanner;

public class InputView {
private final Scanner sc = new Scanner(System.in);

public String getStringInput() {
return sc.nextLine();
}
}
31 changes: 31 additions & 0 deletions src/main/java/racingcar/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar.view;

import racingcar.util.StringUtils;
import racingcar.model.Car;

import java.util.List;

public class OutputView {
public void getCars() {
Copy link

Choose a reason for hiding this comment

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

이 메소드 이름은 자동차 리스트를 가져오는? 의미로 볼 수 있을 것 같아요.
출력문만 있다면 get~~보다는 print~~가 더 적합해 보입니다~!

Copy link
Author

Choose a reason for hiding this comment

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

맞습니다 출력에 관련된 내용이니 get 보다는 print 가 어울릴 것 같습니다 수정하겠습니다!

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

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

public void printRaceStart() {
System.out.println("\n실행 결과");
}

public void printRaceStatus(List<Car> cars) {
for(var c : cars) {
System.out.println(c.getName() + " : " + StringUtils.NumToSticks(c.getLocation()));
}
System.out.print('\n');
}

public void printRaceFinalStatus(List<String> winners) {
System.out.print("최종 우승자 : " + String.join(", ", winners));
}
}