Skip to content

Commit

Permalink
[2단계 - 웹 자동차 경주] 테오(최우성) 미션 제출합니다. (#133)
Browse files Browse the repository at this point in the history
* refactor: 컨트롤러를 RestController로 변경

* refactor: 일반 객체가 아닌 ResponseEntity로 응답하도록 변경

* refactor: 변수명이 과도하게 의미를 내포하지 않도록 변경

* refactor: DTO 클래스명에서 보다 명확한 의미를 전달하도록 변경

* refactor: 구현체 메소드에 Override 명시

* fix: Multiple Key를 가져오던 문제 수정

* test: Service 객체의 생성 책임을 컨테이너에 위임, 더미 객체 삭제

* refactor: 테이블을 생성하는 sql 파일 네이밍 변경

* refactor: SpringBootTest을 JDBCTest로 변경

* feat: 예외 핸들링 메커니즘 생성

* docs: 1단계 리팩토링 기록 작성

* docs: 2단계 요구사항 정리

* refactor: DAO가 하나의 테이블에 매핑되도록 변경

* feat: 플레이 이력 조회에 대한 웹 요청/응답이 가능하다.

* refactor: dto 패키지 이름 repository로 변경

* chore: 기존 콘솔 애플리케이션 컨트롤러 복구

* refactor: 조인 기능을 하는 메소드를 stream을 활용하도록 변경

* feat: Repository 추상화, 메모리 Repository 생성

* feat: 콘솔 어플리케이션이 기존 로직을 재사용하도록 변경

* chore: 패키지 추가 분리

* docs: 리팩토링 기록 작성

* refactor: 상수 분리

* chore: 클래스명을 명확하게 변경

* refactor: ExceptionAdvice 역할에 따라 추가 분리

* refactor: ReponseEntity의 badRequest 메소드를 이용하도록 변경

* chore: 패키지 구조 수정, entity를 persistence 하위로 변경

* chore: 메소드명 수정

* feat: `@Valid`를 통해 검출된 예외도 메세지를 반환하도록 수정

* feat: Service의 메소드를 Transactional하게 변경

* refactor: RacingGame 도메인 객체 구조 변경

* refactor: Key 값을 Number가 아닌 Long으로 반환하도록 변경

* refactor: 도메인에 시도 횟수를 반환하는 getter 생성

* refactor: 레이어 간 메세지 정의

Repository - DAO -> Entity
Service - Repository -> Domain
Controller - Service -> Domain
Client - Controller -> DTO

* refactor: 컨트롤러 간 공통적인 변환 로직 분리

* chore: 클래스명 변경

* test: 프로덕션 코드 수정으로 인한 테스트 코드 수정

* refactor: Service에서 DTO가 아닌 도메인 객체를 반환하도록 수정

* test: Repository Mock을 통한 테스트 개선

* chore: 사용되지 않는 메소드 제거

* docs: 리팩토링 기록 작성

* feat: 예외처리 시 로깅 기능 추가

* refactor: 쿼리를 매번 for문에서 생성하지 않도록 변경

* refactor: key를 Long으로 형변환할 때, `getKeyAs` 메소드를 사용하도록 변경

* chore: 메소드명, 클래스명 수정

* chore: DTO 패키지를 Presentation Layer로 이동

* chore: Layered Architecture에 따라 패키지 이름 구성

* chore: Layered Architecture에 따라 패키지 변경

* chore: 도메인 제약조건에 맞게 스키마 재설계

* refactor: 도메인 코드 개선

* chore: 메소드명 변경

* refactor: 키를 Long이 아닌 Integer로 받도록 변경

* chore: 커스텀 예외 제거에 따른 ExceptionHandler 변경

* fix: 컬럼 사이즈가 작아 우승자를 담지 못하는 문제 해결

* fix: 게임 이력이 제대로 복원되지 않던 문제 해결

* test: 도메인 구조 수정에 따른 테스트 변경

* docs: 고민사항 및 리팩토링 기록 작성

* chore: 변수명 변경

* refactor: 엔티티의 속성을 모두 Wrapper 타입으로 변경

null 지정 가능해짐

* refactor: 메소드 분리, 가독성 개선

* refactor: 불필요한 ControllerAdvice Order 지정 삭제

스프링에서는 여러 ExceptionHandler가 있을 때, 가장 구체적인 ExceptionHandler를 선택함

* refactor: 프로시저의 성격을 가지는 객체를 static하게 변경, 메소드명 변경

* chore: 불필요한 개행 삭제

* docs: 고민사항 및 리팩토링 기록 작성
  • Loading branch information
woosung1223 authored Apr 26, 2023
1 parent 2d18c57 commit 5e66441
Show file tree
Hide file tree
Showing 47 changed files with 972 additions and 464 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
- [x] 정의되지 않은 경로로 요청하는 경우 `404`를 반환한다.
- [x] 정의되지 않은 HTTP 메서드를 호출했을 때는 `405`를 반환한다.
- [x] 서버 내부에서 에러가 발생했을 때는 `500`을 반환한다.
- [x] 플레이 이력 조회에 대한 웹 요청을 받을 수 있다.
- [x] `/plays``GET` 요청을 보낼 시 응답한다.
- [x] 플레이 이력 조회에 대한 웹 응답을 전달할 수 있다.
- [x] JSON 형태로 전달한다.
- [x] 우승자들의 이름을 전달한다.
- [x] 참여자들의 정보를 전달한다.

## DB 연동

Expand All @@ -88,3 +94,59 @@
- [ ] 이너클래스로 `CarDTO`를 둘지, 외부 객체로 뺼지
- [ ] 어느 기준으로 DTO를 생성해야 할지
- [ ] Controller - Service, Service - Dao 사이마다 DTO를 새로 정의해야 하는지(필드가 새롭게 추가되는 경우)
- [ ] DAO를 하나의 테이블씩 매핑시키고, 이를 Repository에서 Join하려고 했으나 SELECT문으로 받기 위한 정보들을 또 VO로 객체를 만들어야 할까?
- [ ] 테이블과 매핑이 되는 객체가 필요한 이 시점에서 Entity라는 객체가 필요하게 되는 것일까?
- [ ] 도메인 객체의 의존성 주입은 누가 하는게 옳을까?
- [ ] 예를 들어, `RandomNumberGenerator`는 누가 주입 책임을 갖는게 맞을까?
- [ ] Repository에서 Entity 정보를 토대로 도메인 객체를 조합할 때, 이전 도메인 구조에서는 조합이 불가능하다는 것을 꺠달음
- [ ] `Car` 객체의 `Position`을 조합해 `RacingGame` 객체를 만들 수 없음
- [ ] 따라서, **웹 어플리케이션에 맞는** 도메인 구조를 만드는 것이 중요해 보임
- [ ] 어떻게 해야 할까?

---

# 리팩토링 기록

- [x] 컨트롤러 `RestController`로 변경
- [x] 클라이언트에게 `ResponseEntity`로 응답하도록 변경
- [x] 보다 상세한 응답 코드, 헤더 등을 전달할 수 있음
- [x] DTO 클래스명이 `DTO`라는 키워드를 내포하지 않도록 변경
- [x] 명확하게 비즈니스적인 표현이 가능해짐
- [x] 빈 객체의 테스트에서 생성 책임을 컨테이너에게 위임
- [x] 생성 책임을 위임함에 따라 주입할 더미 객체를 만들 필요가 사라짐
- [x] `@JDBCTest`를 사용해 table을 삭제, 생성하는 과정을 명시하지 않도록 변경
- [x] `@JDBCTest`는 내부적으로 `Transactional`
- [x] 기본적으로 테스트 메소드마다 `Rollback`을 수행함
- [x] 따라서 `CREATE TABLE`, `DROP TABLE` 과정을 수동으로 할 필요가 없음
- [x] `RestControllerAdvice`, `ExceptionHandler`를 이용한 전역 예외처리
- [x] `RestController`의 경우 `RestControllerAdvice`를 사용해야 함
- [x] 두 어노테이션의 차이는 `RestController``Controller`의 차이와 같음
- [x] `ExceptionResponse` DTO 사용을 통한 예외 메세지 구조화
- [ ] DAO가 하나의 테이블에 매핑되도록 변경
- [ ] 직관적이라 좋은데, 이것이 좋은 구조인지는 고민해봐야 할 듯
- [ ] 콘솔 어플리케이션이 웹 Service, Repository 를 재사용하도록 변경
- [ ] ControllerAdvice를 역할에 따라 추가 분리
- [ ] 예외도 성격이 존재하기 때문에(도메인이거나, 아니거나 등) 이에 따라 분리하자는 취지
- [ ] `@Order` 어노테이션을 사용해 우선순위를 조정했는데, 보다 좋은 방법이 있을까? 추가 고민해보기
- [ ] https://velog.io/@xodud001/%ED%98%B8%EC%B6%9C%EB%90%A0-%EA%B1%B0%EB%9D%BC-%EC%98%88%EC%83%81%ED%96%88%EB%8D%98-ExceptionHandler%EA%B0%80-%EC%9D%BC%EC%9D%84-%EC%95%88-%ED%95%98%EB%84%A4
- [ ] 알고보니 스프링에서는 가장 구체적인 예외 핸들러를 사용함. 위 정보는 거짓.
- [ ] `@Transactional` 어노테이션 사용
- [ ] 여러 테이블에 접근하는 로직이 있으므로 원자성을 보장해야 함
- [ ] 계층 간 전달되는 데이터 형식을 고정시킴
- [ ] Repository - DAO -> Entity
- [ ] Service - Repository -> Domain
- [ ] Controller - Service -> Domain
- [ ] Client - Controller -> DTO
- [ ] 이 과정을 통해 Layered Architecture를 따를 수 있었음
- [ ] 계층 간 `Converter` 객체를 둘지에 대한 고민을 해봐야겠음
- [ ] Mock을 통한 테스트 개선
- [ ] 추후 따로 학습이 필요함
- [ ] Service에서 DTO가 아닌 도메인 객체를 반환하도록 수정
- [ ] 재사용성이 증가됨
- [ ] 특정한 문맥(DTO)에만 고정되지 않을 수 있음
- [ ] 예외처리 시 Logger 사용
- [ ] `sout`과 다르게 환경에 구속되지 않는다(파일 등에도 적용가능)
- [ ] 멀티스레딩 환경에서도 더 좋은 성능을 낼 수 있다
- [ ] 스레드 정보 등 더 많은 정보를 로그에 남기고 싶은 경우 사용할 수 있음
- [ ] 엔티티에서 필드를 모두 Wrapper 타입으로 변경
- [ ] 원시타입을 사용하면 null을 표현할 수 없음
17 changes: 17 additions & 0 deletions src/main/java/racingcar/ConsoleRacingGameApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package racingcar;

import racingcar.presentation.ConsoleRacingGameController;
import racingcar.view.input.InputView;
import racingcar.view.output.ConsoleView;

import java.util.Scanner;

public class ConsoleRacingGameApplication {

public static void main(String[] args) {
new ConsoleRacingGameController(
new InputView(new Scanner(System.in)),
new ConsoleView()
).start();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RacingCarApplication {
public class WebRacingGameApplication {

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

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

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import racingcar.domain.Car;
import racingcar.domain.RacingGame;
import racingcar.domain.RandomNumberGenerator;
import racingcar.persistence.repository.GameRepository;
import racingcar.presentation.dto.RacingGameRequest;

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

@Service
public class RacingGameService {

private final GameRepository gameRepository;

public RacingGameService(final GameRepository gameRepository) {
this.gameRepository = gameRepository;
}

@Transactional
public RacingGame playRacingGame(final RacingGameRequest racingGameRequest) {
RacingGame racingGame = createRacingGame(racingGameRequest);
racingGame.start();
gameRepository.saveGame(racingGame);
return racingGame;
}

private RacingGame createRacingGame(final RacingGameRequest racingGameRequest) {
return new RacingGame(
toCars(racingGameRequest.getNames()),
racingGameRequest.getCount(),
new RandomNumberGenerator()
);
}

private List<Car> toCars(final String cars) {
return Arrays.stream(cars.split(","))
.map(name -> new Car(name, 0))
.collect(Collectors.toList());
}

public List<RacingGame> readAllGames() {
return gameRepository.selectAllGames();
}
}
29 changes: 0 additions & 29 deletions src/main/java/racingcar/controller/RacingController.java

This file was deleted.

41 changes: 0 additions & 41 deletions src/main/java/racingcar/dao/JdbcTemplateRacingGameDao.java

This file was deleted.

12 changes: 0 additions & 12 deletions src/main/java/racingcar/dao/RacingGameDao.java

This file was deleted.

10 changes: 5 additions & 5 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class Car {
private final CarName carName;
private final Position position;

public Car(String name, int position) {
public Car(final String name, final int position) {
this.carName = new CarName(name);
this.position = new Position(position);
}
Expand All @@ -14,12 +14,12 @@ public void move() {
position.moveForward();
}

public int comparePosition(Car otherCar) {
return this.getPosition() - otherCar.getPosition();
public boolean hasSamePositionWith(final Car otherCar) {
return comparePosition(otherCar) == 0;
}

public boolean hasSamePositionWith(Car otherCar) {
return comparePosition(otherCar) == 0;
public int comparePosition(final Car otherCar) {
return this.getPosition() - otherCar.getPosition();
}

public int getPosition() {
Expand Down
19 changes: 8 additions & 11 deletions src/main/java/racingcar/domain/CarName.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
package racingcar.domain;

import racingcar.exception.CarNameBlankException;
import racingcar.exception.CarNameLengthException;

public class CarName {

private static final int MAX_CAR_NAME_LENGTH = 5;

private final String carName;

public CarName(String carName) {
public CarName(final String carName) {
validateCarName(carName);
this.carName = carName;
}

private void validateCarName(String carName) {
validateCarNameIsNotEmpty(carName);
validateCarNameLength(carName);
private void validateCarName(final String carName) {
validateNotBlank(carName);
validateProperLength(carName);
}

private void validateCarNameIsNotEmpty(String carName) {
private void validateNotBlank(final String carName) {
if (carName.isBlank()) {
throw new CarNameBlankException();
throw new IllegalArgumentException("자동차의 이름은 공백이면 안됩니다.");
}
}

private void validateCarNameLength(String carName) {
private void validateProperLength(final String carName) {
if (carName.length() > MAX_CAR_NAME_LENGTH) {
throw new CarNameLengthException();
throw new IllegalArgumentException("[ERROR] 자동차 이름의 길이는 1자 이상, 5자 이하여야 합니다.");
}
}

Expand Down
16 changes: 11 additions & 5 deletions src/main/java/racingcar/domain/Coin.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

public class Coin {

private int remaining;
private final int given;
private int used;

public Coin(int remaining) {
this.remaining = remaining;
public Coin(final int given) {
this.given = given;
this.used = 0;
}

public void use() {
if (isLeft()) {
remaining--;
used++;
}
}

public boolean isLeft() {
return remaining > 0;
return used < given;
}

public int getGiven() {
return given;
}
}
10 changes: 4 additions & 6 deletions src/main/java/racingcar/domain/Position.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
package racingcar.domain;

import racingcar.exception.PositionInvalidException;

public class Position {

private int position;

public Position(int position) {
public Position(final int position) {
validatePosition(position);
this.position = position;
}

private void validatePosition(int position) {
private void validatePosition(final int position) {
validatePositionIsNotNegative(position);
}

private void validatePositionIsNotNegative(int position) {
private void validatePositionIsNotNegative(final int position) {
if (position < 0) {
throw new PositionInvalidException();
throw new IllegalArgumentException("자동차의 위치는 음수일 수 없습니다.");
}
}

Expand Down
Loading

0 comments on commit 5e66441

Please sign in to comment.