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단계 - 웹 자동차 경주] 밀리(김미성) 미션 제출합니다. #94

Merged
merged 21 commits into from
Apr 14, 2023

Conversation

miseongk
Copy link

안녕하세요 디디! 5기 밀리입니다 😊
리뷰 잘 부탁드립니다!

스프링을 사용해보니 공부해야할 부분이 정말 많은 것 같습니다 🥲
질문은 따로 아래에 남겨놓겠습니다!
감사합니다!

@miseongk
Copy link
Author

궁금한 점

  • 컨트롤러의 한 메서드(api)에서 서비스의 여러 메서드를 호출해도 되는지 궁금합니다. 현재 요구사항에서는 게임을 실행하면 우승자와 자동차들의 상태 조회까지 모두 실행하여 응답을 주기 때문에 어쩔 수 없이 컨트롤러의 play()에서 서비스의 여러 메서드를 호출했습니다. 하지만 일반적인 상황에서는 컨트롤러의 한 메서드에서 여러 메서드를 호출하는 것을 지양해야 하는지 궁금합니다.

    제가 그렇게 생각한 이유는 다음과 같습니다. 컨트롤러는 단순히 웹 요청을 받아서 처리하는 역할인데, 서비스의 메소드를 순서에 맞게 호출하는 로직이 컨트롤러에 들어가는게 어색하다고 느껴졌기 때문입니다. 그리고 컨트롤러가 너무 커지는 느낌이 들었습니다.

  • 계층간 전달을 위해서 DTO를 사용합니다. 도메인에서 필요한 데이터만 골라 DTO를 만들어서 전달하게 되는데, 현재 규모의 프로젝트는 DTO 클래스를 1~2개 정도만 만들면 되지만, 프로젝트 규모가 커진다면 DTO의 수가 많이 증가할 것 같습니다. 현업에서는 규모가 커진다면 DTO를 어떻게 관리하나요? 계층간 전달을 무조건 DTO로 해야하는 건가요??

  • DAO 테스트를 할 때 값을 제대로 추가했거나 업데이트 했는지 확인하기 위해 조회하는 메서드 (CarDaofindCar메서드)를 따로 추가했습니다. 이렇게 테스트를 위한 메서드를 만드는 것을 지양해야 하는데, 그렇다면 데이터베이스에 정확한 값이 들어갔는지 테스트를 할 수 없습니다. 아니면 DAO 테스트를 메서드가 오류없이 잘 실행됐는지만 확인해도 될지 궁금합니다.

  • 입력값 검증을 위해 Validation 의존성을 추가해서 사용했습니다. 만약 사용하지 않는다면 일반적으로 어떻게 입력값을 검증하는지 궁금합니다. 컨트롤러에서 입력값 검증을 하거나, DTO(GameRequest)에서 따로 검증 로직을 추가하면 될까요?

Copy link

@fucct fucct left a comment

Choose a reason for hiding this comment

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

안녕하세요 밀리.

전체적으로 잘 작성해주셔서 가벼운 피드백만 남겼습니다 ㅎㅎ. 확인 부탁드려요.
그리고 질문주신 부분들을 살펴보자면,

첫번째로 컨트롤러가 서비스의 여러 메서드를 호출해도 되는지 질문주셨는데, 개인적으로는 하나만 호출하는게 좋다고 생각합니다.
이는 서비스 메서드 단위를 고민해봐야하는데요, 서비스는 도메인 행위 단위보다 큰 범위라고 생각해서 사용자의 특정 행위를 단위로 메서드가 작성하는 편입니다. 레이싱 게임의 경우 "레이싱 게임 수행" 자체가 하나의 서비스 메서드 단위이며, 요청을 통한 게임 생성, 게임 진행, 결과 출력 까지 하나의 메서드로 진행된다고 생각합니다.

만약 이번 레이싱게임 미션에서 사용자가 게임 생성, 게임진행, 결과 출력을 3단계로 걸쳐서 할수 있었다면, Controller의 path도 3단계로 쪼개지고, 그에 따라 Service도 3개의 메서드가 생기도록 설계할거 같습니다.

물론 이건 저만의 기준이고, 밀리가 고민해보시고 어떤게 가독성과 유지보수 측면에서 좋은지 고민해보시면 좋을것 같아요.

두번째로 계층간 DTO사용은, 실제 현업에서도 DTO가 굉장히 많아지는게 맞습니다. 우선 DTO를 만드는 이유는 외부 요구사항으로부터 도메인의 변경을 막기 위함인데요. View의 요구사항으로부터 도메인 변경을 막기위해 Controller단에는 필수적으로 Dto를 사용하는것 같습니다.
특히, Controller에서 사용하는 DTO들은 모든 Controller메서드마다 만들어지는데요, 만약 공용으로 사용할 경우 특정 API 의 스펙 변경이 다른 API에 영향을 미칠 수 있기 때문에, 동일한 요청/응답이더라도 될수 있으면 API 마다 DTO를 만드는 편이 좋습니다. 다만, DAO와 통신할때는 일반적으로 테이블과 도메인의 형태가 거의 비슷하고, 변경가능성이 적기 때문에 도메인 객체를 직접 활용하는 편입니다.

세번째로 Dao 테스트를 위해서 find 메서드를 추가하는것은 현재 미션의 특징 때문인것 같습니다.
데이터들이 저장되는 이유는 조회하기 위해서입니다. 따라서 일반적인 애플리케이션이라면 테스트 작성하기 전에 find 메서드가 있을거에요. 그러니 걱정안하셔도 됩니다.

네번째로 Validation같은 경우는, Controller레이어나, Service 레이어 중에서 적절하다고 생각되시는 부분에서 검증을 해주시면 됩니다. 어느쪽이든 저는 괜찮다고 생각합니다.

코멘트를 적다보니 너무 과한 내용을 설명드린게 아닌가 싶네요. 적절히 그런게 있구나~정도로 알아주시면 될것같습니다.
피드백은 가벼운 내용이니 2단계 진행하실때 반영부탁드려요 :)

수고 많으셨습니다~

import racingcar.service.RacingCarService;

@RestController
public class RacingCarController {
Copy link

Choose a reason for hiding this comment

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

이름이... ㅋㅋㅋ Web전용 Controller라는 네이밍으로 바꿔주시는게 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정했습니다!

public class GameRequest {

@NotBlank(message = "하나 이상의 이름을 입력해야 합니다.")
private final String names;
Copy link

Choose a reason for hiding this comment

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

List로 받는건 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

프론트에서 요청을 보낼 때 Requestbody의 값을 List로 받는것을 말씀하시는건가요??
그렇다면 프론트 단에서 names를 넘겨줄 때 List 형식으로 보내줘야 할 것 같은데, 그렇다면 js 코드를 수정하는 방법 밖에는 생각이 나지 않습니다..! 혹시 다른 방법이 있는걸까요?

Copy link

Choose a reason for hiding this comment

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

아.. js코드는 템플릿이었나보네요.. ㅎㅎ
저때는 js코드도 직접 작성하는부분이 있어서 몰랐습니다. 이 피드백은 넘어가셔도됩니다!


private static final int IS_WINNER = 1;
private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert insertActor;
Copy link

Choose a reason for hiding this comment

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

jdbcTemplate을 사용하지 않고 insertActor를 사용하시는 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

jdbcTemplate으로 insert를 하고 생성된 id 값을 받아오기 위해서는 keyholder를 사용해야하는데,
그 과정의 코드가 가독성이 좋지 않다고 생각하여 insertActor를 사용했습니다!

학습테스트를 진행하면서 jdbcTemplate의 update 메서드와 SimpleJdbcInsert의 insertActor가 역할은 같은데 왜 구분이 되어있을까 고민했었습니다. 결론은 jdbcTemplate의 update는 ?에 대응하는 값을 인자로 넣어주게 되는데, 이때 인자가 많을수록 실수할 확률이 높아진다고 생각했습니다. 반면 insertActor는 Map으로 필드값을 명시해주면서 put하게 되어 상대적으로 안전하다고 생각했습니다.

보통 insert 구문을 사용할 때 jdbcTemplate을 사용하나요??

Copy link

Choose a reason for hiding this comment

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

어떤걸 사용하셔도 괜찮습니다. 다만 JdbcTemplate로 insert를 할수 있도록 추상화 해놓았는데 어떤 이유로 쓰셨는지 궁금해서 여쭤밨습니다.

public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
Copy link

Choose a reason for hiding this comment

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

406 status가 맞을까요?

Copy link
Author

Choose a reason for hiding this comment

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

찾아보니 클라이언트의 잘못된 요청값은 400 status 를 보내는게 맞는 것 같습니다. 수정했습니다!

@Service
public class RacingCarService {

private static final String DELIMITER = ",";
Copy link

Choose a reason for hiding this comment

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

입력을 split하는 책임을 racingGameService 가 알아야할까요?

Copy link
Author

Choose a reason for hiding this comment

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

기존 콘솔 어플리케이션에서는 inputView에서 split을 하도록 구현했었습니다.
따라서 입력값에 대한 형태 변환은 서비스보다는 컨트롤러가 적합한 것 같습니다!
혹은 Car를 생성하는 CarFactory에서 split을 해도 좋을 것 같습니다. 하지만 기존 CarFactory를 수정하면 변경되는 기존 코드가 많아서 컨트롤러에서 하도록 수정했습니다!

Comment on lines +36 to +40
jdbcTemplate.execute(DROP_TABLE_CAR);
jdbcTemplate.execute(DROP_TABLE_GAME);
jdbcTemplate.execute(CREATE_TABLE_GAME);
jdbcTemplate.execute(CREATE_TABLE_CAR);

Copy link

Choose a reason for hiding this comment

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

@Sql 을 통해서도 하실수 있습니다 ㅎㅎ
사용법을 한번 공부해보시면 편하실거에요

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다!! 공부해보겠습니다!

import racingcar.strategy.MovingStrategy;

@Service
public class RacingCarService {
Copy link

Choose a reason for hiding this comment

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

Service 메서드도 테스트 부탁드립니다!

Comment on lines +29 to +35
int gameId = racingCarService.play(gameRequest);
List<CarDto> winners = racingCarService.findWinners(gameId);
String winnerNames = winners.stream()
.map(CarDto::getName)
.collect(Collectors.joining(DELIMITER));
List<CarDto> cars = racingCarService.findCars(gameId);

Copy link

Choose a reason for hiding this comment

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

service 메서드를 하나로 두는건 어떨까요?
service의 메서드는 하나의 비즈니스 수행 단위라고 봐주시면 좋을것 같습니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

질문했던 부분인데, 답변 정말 감사합니다!
현재는 여러 행동을 하나의 요청으로 처리하기 때문에 메서드 하나로 두게 되면 너무 많은 일을 한다고 생각해서 나눴던 것 같습니다!
저도 하나의 요청에 하나의 메서드만을 호출하는 것이 좋다고 생각합니다.
반영하여 수정하겠습니다!

@@ -0,0 +1,9 @@
package racingcar.strategy;

public class FixedMovingStrategy implements MovingStrategy {
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.

넵 패키지 이동했습니다!

}

List<Car> cars = racingGame.getCars();
convertDto(cars).forEach(car -> carDao.updatePosition(car, racingGame.getId()));
Copy link

Choose a reason for hiding this comment

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

Service - Dao 통신과 Controller - Service 통신 시 같은 객체를 공유하는건 위험할 수 있습니다. View의 요구사항에 변경에 따라 Dao로직에도 문제가 생길수도 있거든요.

그리고 개인적으로는 Dao 와 통신할때 굳이 Dto를 사용해야하나 싶기도 하네요 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

Service - Dao 통신과 Controller - Service 통신 시 같은 객체를 공유 하는 부분을 찾을 수 없어서 질문드립니다..!
컨트롤러에서 GameRequest를 서비스로 직접 전달한 후, 그 값들을 필요에 맞게 꺼내서 사용하였고, 직접 GameRequest를 Dao에 넘기는 부분이 없습니다. 디디가 말씀하신 공유하는 부분이 GameRequest가 아니라면 혹시 어떤 객체인가요??
혹시 아래 findWinners 메서드처럼 Dao에서 받아온 값을 그대로 컨트롤러로 전달하는 부분을 말씀하시는건가요?

public List<CarDto> findWinners(final int gameId) {
    return carDao.findWinners(gameId);
}

Copy link
Author

Choose a reason for hiding this comment

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

추가적으로, 그렇다면 디디는 Dao와 통신할 때 그냥 객체를 직접 넘기는 방식으로 구현하시나요?
Dto를 사용하는 이유는 계층 간 데이터 전달을 하기 위함이고, 계층간 데이터가 오고가면서 변질될 우려가 있기 때문에 사용해야 된다고 알고 있습니다!
혹시 Dao와 통신할 때 Dto를 굳이 사용하지 않아도 되는 이유를 알려주실 수 있나요?🤔

Copy link

Choose a reason for hiding this comment

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

코드 관리 측면에서 DTO를 생략할 수 있다고 생각했습니다.
이부분은 나중에 Repository 개념을 통해서 학습하실거라서, 아직은 고민 안해주셔도 될것 같아요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants