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

[1단계 - 웹 자동차 경주] 히이로(문제웅) 미션 제출합니다. #53

Merged
merged 7 commits into from
Apr 13, 2023
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
# jwp-racingcar

# 자동차 경주 구현

## 💡프로젝트 개요
- 자동차 경주 프로젝트는 경주할 자동차 이름과 시도할 횟수를 입력받은 후 전진 과정을 통해 가장 많이 전진한 자동차가 우승을 하는 프로젝트이다. 이 때 자동차 경주 게임을 진행하고 최종 우승 결과를 출력해야한다.
- 진행한 게임 결과는 DB에 저장된다.
---


## 📋 구현 기능 목록

## 프로그램 전체 로직
- ✅자동차 이름에 대한 사용자 입력을 받는다.
- ✅총 시도 횟수에 대한 사용자 입력을 받는다.
- ✅자동차 경주를 진행한다.
- ✅DB에 결과 값들을 저장한다.
- ✅경주 결과를 출력한다.


## domain(model)
- ✅자동차 정보를 저장하고 관리한다.
- ✅각 자동차 이름은 1자 이상 5자 이하여야 한다.
- ✅자동차의 위치를 증가 시킬 수 있다.
- ✅자동차의 전진 여부를 판단한다.
- ✅random 값이 4 이상일 경우 전진한다.
- ✅random 값이 3 이하일 경우 정지한다.
- ✅생성된 자동차들의 정보를 저장하고 관리한다.
- ✅자동차 대수는 1대 이상이어야 한다.
- ✅우승한 자동차를 제공한다.
- ✅모든 자동차들이 전진을 1회 시도한다.
- ✅random 하게 0~9 범위 내에 한 자리 숫자를 반환한다.
- ✅자동차 경주를 진행한다.
- ✅몇 라운드를 진행할 지 저장한다.
- ✅라운드가 1 이상의 값으로 주어지는지 검증한다.
- ✅현재 게임진행 현황을 통해 게임 종료 여부를 판단한다.


## view
### 입력
- ✅자동차 이름 입력받기
- ✅총 시도 횟수 입력받기
- ✅시도 횟수 입력값은 숫자여야 한다.
- ✅총 시도 횟수는 1 이상의 양의 정수 값이어야 한다.

### 출력
- ✅프로그램 실행 결과
- ✅최종 결과 문구 출력
- ✅공동 우승자 발생 경우 고려


## 예외 처리
- ✅자동차 이름이 0자인 경우 고려하기
- ✅총 시도 횟수 입력값이 양의 정수가 아닌 경우 고려하기
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

runtimeOnly 'com.h2database:h2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
19 changes: 19 additions & 0 deletions src/main/java/racingcar/ConsoleRacingCarApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package racingcar;

import racingcar.controller.ConsoleRacingCarController;
import racingcar.domain.RandomNumberGenerator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.Scanner;

public class ConsoleRacingCarApplication {

public static void main(String[] args) {
ConsoleRacingCarController controller = new ConsoleRacingCarController(new InputView(new Scanner(System.in)),
new OutputView(),
new RandomNumberGenerator());

controller.run();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RacingCarApplication {
public class WebRacingCarApplication {

public static void main(String[] args) {
SpringApplication.run(RacingCarApplication.class, args);
SpringApplication.run(WebRacingCarApplication.class, args);
}

}
47 changes: 47 additions & 0 deletions src/main/java/racingcar/controller/ConsoleRacingCarController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package racingcar.controller;

import racingcar.domain.Car;
import racingcar.domain.Cars;
import racingcar.domain.NumberGenerator;
import racingcar.domain.RacingGame;
import racingcar.view.InputView;
import racingcar.view.OutputView;

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

public class ConsoleRacingCarController {
Copy link

Choose a reason for hiding this comment

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

Controller, Service, Repository 등 Layered Architecture 구조를 사용하게 되면서 각 계층에 대해 테스트를 어떻게 진행해야 하는지 큰 방향에 대한 감을 잡고 싶습니다.
그리고 테스트에 DB의 영향을 우회하기 위해 테스트 객체들을 Mocking해야 할 것 같은데 이런 부분도 어떤 방식으로 진행하면 좋을지 여쭤보고 싶습니다.

이 글을 읽어보시면 좋을 것 같아요. 이 글에서 키워드를 잡고 하나씩 공부해보시면 좋을 것 같네요. 다만, 아마 다음 미션 쯤에 다룰 내용도 있을 거라 이번 미션에서는 그냥 가볍게 알아두시면 좋을 것 같습니다.

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 InputView inputView;
private final OutputView outputView;
private final NumberGenerator numberGenerator;

public ConsoleRacingCarController(InputView inputView, OutputView outputView, NumberGenerator numberGenerator) {
this.inputView = inputView;
this.outputView = outputView;
this.numberGenerator = numberGenerator;
}

public void run() {
Cars cars = generateCars(inputView.readCarNames());
int round = inputView.readRacingRound();
playGame(cars, round);
}

private Cars generateCars(List<String> carNames) {
List<Car> carInstances = new ArrayList<>();
for (String name : carNames) {
carInstances.add(new Car(name, numberGenerator));
}
return new Cars(carInstances);
}

private void playGame(Cars cars, int round) {
RacingGame racingGame = new RacingGame(cars, round);
outputView.printResultMessage();
while (!racingGame.isGameEnded()) {
outputView.printRoundResult(racingGame.playOneRound());
}
outputView.printFinalResult(racingGame.findWinnerCars());
}
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/controller/WebRacingCarController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import racingcar.dto.request.RacingStartRequest;
import racingcar.dto.response.RacingResultResponse;
import racingcar.service.RacingCarService;

@RestController
public class WebRacingCarController {

private final RacingCarService racingCarService;

public WebRacingCarController(RacingCarService racingCarService) {
this.racingCarService = racingCarService;
}

@PostMapping("/plays")
public ResponseEntity<RacingResultResponse> play(@RequestBody RacingStartRequest racingStartRequest) {
return ResponseEntity.ok(racingCarService.play(racingStartRequest));
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/dao/CarDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.dao;

import racingcar.dto.CarDto;

import java.util.List;

public interface CarDao {

void save(int gameId, List<CarDto> carDtos);
}
35 changes: 35 additions & 0 deletions src/main/java/racingcar/dao/H2CarDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package racingcar.dao;

import org.springframework.stereotype.Repository;
import racingcar.dto.CarDto;
import racingcar.utils.ConnectionProvider;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

@Repository
public class H2CarDao implements CarDao {

@Override
public void save(int gameId, List<CarDto> carDtos) {
Copy link

Choose a reason for hiding this comment

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

Dao에 도메인을 넘겨도 되는가?

저는 View에 도메인을 넘겨도 되는가와 이 질문은 동일하다고 생각해요.
단순히 도메인에서 데이터만을 뽑아내는 거면 도메인을 넘겨도 괜찮다고 생각합니다.
도메인에서 로직을 호출한 결과값이 필요하거나 여러 값들이 합쳐진다면 DTO를 만드는 편이 낫겠죠.


DB를 다루는 코드와 domain은 분리해야 각종 변경으로부터 domain을 지킬 수 있다고만 생각해왔던 저는 결국 페어를 완전히 설득하는데 실패했었습니다.

저도 동의하는 바입니다. 히이로의 경험을 쌓아가다 보면 좀 더 명확한 근거를 들 수 있을 거에요.
다만 현재 구현 방식이 이 철학을 잘 구현했는지는 조금 의문이 들기도 하네요. 레이어를 여러 층으로 나누는 건 분명한 이유가 필요합니다. 나눠야하는 분명한 이유를 들지 못한다면 합치는 게 더 나은 방식이라는 생각을 해보시면 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

다만 현재 구현 방식이 이 철학을 잘 구현했는지는 조금 의문이 들기도 하네요. 레이어를 여러 층으로 나누는 건 분명한 이유가 필요합니다. 나눠야하는 분명한 이유를 들지 못한다면 합치는 게 더 나은 방식이라는 생각을 해보시면 좋을 것 같아요.

위에서 Repository와 Dao에 대해서 리뷰를 받고 정리하기는 했는데 해당 부분에 대한 리뷰였을까요? 혹시 이 부분이 아니었다면 어떤 부분에 대한 리뷰였는지 좀 더 자세한 설명을 부탁드리고 싶습니다! 🙇

Copy link

Choose a reason for hiding this comment

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

2단계에서 해당 부분들이 잘 해결된 것 같네요 👍

String sql = "INSERT INTO CAR(game_id, name, position, is_win) VALUES (?,?,?,?)";

try (Connection connection = ConnectionProvider.getConnection()) {
for (CarDto carDto : carDtos) {
PreparedStatement ps = connection.prepareStatement(sql);

ps.setInt(1, gameId);
ps.setString(2, carDto.getName());
ps.setInt(3, carDto.getPosition());
ps.setBoolean(4, carDto.isWin());
ps.executeUpdate();

ps.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/racingcar/dao/H2RacingGameDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.dao;

import org.springframework.stereotype.Repository;
import racingcar.utils.ConnectionProvider;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

@Repository
public class H2RacingGameDao implements RacingGameDao {

@Override
public int save(int count) {
String sql = "INSERT INTO RACING_GAME(count) VALUES (?)";

try (Connection connection = ConnectionProvider.getConnection();
Comment on lines +12 to +19
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.

넵 이번에는 학습테스트를 먼저 진행하지 않고 바로 1단계 미션을 먼저 진행했었어서 JdbcTemplate 기능을 활용하지 못했네요... 2단계 때 수정해보도록 하겠습니다!

PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setInt(1, count);
ps.executeUpdate();

ResultSet rs = ps.getGeneratedKeys();

if (rs.next()) {
return rs.getInt(1);
}

throw new IllegalStateException();

} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
6 changes: 6 additions & 0 deletions src/main/java/racingcar/dao/RacingGameDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racingcar.dao;

public interface RacingGameDao {

int save(int count);
}
66 changes: 66 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package racingcar.domain;

import java.util.Objects;

public class Car implements Comparable<Car> {

private static final int NAME_MIN_LENGTH = 1;
private static final int NAME_MAX_LENGTH = 5;
private static final int MOVABLE_MIN_NUMBER = 4;
private static final String NAME_LENGTH_ERROR = "[ERROR] 자동차 이름은 1자 이상 5자 이하여야 합니다.";

private String name;
private int position;
private NumberGenerator numberGenerator;

public Car(String name, NumberGenerator numberGenerator) {
name = name.trim();
validateName(name);
this.name = name;
this.position = 0;
this.numberGenerator = numberGenerator;
}

public void goForward() {
if (numberGenerator.generate() >= MOVABLE_MIN_NUMBER) {
position++;
}
}

public boolean isSamePosition(Car otherCar) {
return this.position == otherCar.position;
}

public int getPosition() {
return this.position;
}

public String getName() {
return this.name;
}

@Override
public int compareTo(Car otherCar) {
return otherCar.position - this.position;
}

private void validateName(String name) {
if (name.length() < NAME_MIN_LENGTH || name.length() > NAME_MAX_LENGTH) {
throw new IllegalArgumentException(NAME_LENGTH_ERROR);
}
}

@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
Car car = (Car) other;
return position == car.position && Objects.equals(name, car.name);
}

@Override
public int hashCode() {
return Objects.hash(name, position);
}
}

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

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

public class Cars {

private static final int CARS_MIN_SIZE = 1;
private static final String CARS_SIZE_ERROR = "[ERROR] 자동차 대수는 1이상이어야 합니다.";

private List<Car> cars;

public Cars(List<Car> cars) {
validateCarsSize(cars);
this.cars = new ArrayList<>(cars);
}

public List<Car> findAllWinner() {
Car maxPositionCar = findMaxPositionCar();
return cars.stream()
.filter(car -> car.isSamePosition(maxPositionCar))
.collect(Collectors.toList());
}

public List<Car> moveEachCar() {
for (Car car : cars) {
car.goForward();
}
return new ArrayList<>(cars);
}

// Todo : 나중에 중복되는 이름은 입력되면 예외처리 되도록 구현해야 한다.
private void validateCarsSize(List<Car> cars) {
if (cars.size() < CARS_MIN_SIZE) {
throw new IllegalArgumentException(CARS_SIZE_ERROR);
}
}

private Car findMaxPositionCar() {
cars.sort(Car::compareTo);
return cars.get(0);
}

public List<Car> getCars() {
return cars;
}
}
7 changes: 7 additions & 0 deletions src/main/java/racingcar/domain/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package racingcar.domain;

@FunctionalInterface
public interface NumberGenerator {

int generate();
}
Loading