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

[Spring 체스 - 2단계] 써머(최혜원) 미션 제출합니다. #474

Merged
merged 56 commits into from May 4, 2022

Conversation

hyewoncc
Copy link

안녕하세요, 핀!
저번에 남겨주신 리뷰 감사히 잘 읽었습니다 🙇

DAO와 Repository의 차이에 대해 남겨주신 코멘트에 질문이 있는데요,
2단계로 넘어오면서 GameRepository가 PieceDao와 BoardDao를 갖게 했습니다
그러니 GameService에서 piece, board 테이블에 각각 저장을 실행하지 않고,
gameRepository.saveGame(roomId, board); 하는 식으로 컬렉션 처리책임을 GameRepository로 분리할 수 있었습니다

그런데 이렇게 하니 board, piece 테이블에 개별로 접근해야 하는 메서드들은 책임이 분리되지 않고, 그냥 클래스 계층만 한 단계 깊어지게 되더라고요
그래서 GameService에서 각 dao 까지 갖게 하려니 계층 구조에 일관성이 없어서 좋지 않다고 생각했습니다
일단 GameService는 GameRepository를 사용하도록, RoomService는 개별 RoomDao와 BoardDao를 사용하도록 뒀는데, 여기에 대해 추가로 참고할 만한 키워드가 있을까요?
저수준의 CRUD는 DAO, 컬렉션 처리는 Repository 까지는 이해했는데, 제 코드에 적용하려니 어디까지가 컬렉션 처리인가? 저수준의 CRUD만 필요한 경우 dao를 바로 사용해도 되는가? 같은 고민 때문에 쉽지 않네요

그리고 컨트롤러 테스트의 경우 추가하고 있었는데, mock mvc를 사용하지 않고 하려니 통합 테스트가 되고, 메서드 순서가 보장되지 않아 데이터 삽입 시점을 알 수 없고, 데이터를 삭제해도 auto_increment 설정 된 키값이 초기화 되지 않는 등의 문제 때문에 의도대로 잘 되지 않았습니다 😭...
그래서 mock mvc를 공부 중인데 제출이 너무 늦어지는 것 같아 우선 pr을 올립니다, 일단은 주석처리 해뒀어요

hyewoncc and others added 30 commits April 19, 2022 16:34
int boardId = boardRepository.getBoardIdByRoom(roomId);
Board board = loadBoard(boardId);
board.loadTurn(boardRepository.getTurn(boardId));
int boardId = gameRepository.findBoardIdByRoom(roomId);
Copy link
Author

Choose a reason for hiding this comment

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

이런 경우에는 BoardDao만 이용하면 되는데, GameRepository를 통하면서 결국 계층만 깊어졌다 느꼈습니다

Copy link

Choose a reason for hiding this comment

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

boardId를 단독으로 가져와서 다시 board를 조회할 필요가 있을까요?

아래와 같은 방식으로 할 수 있지 않을까요?

board = repository.findByRoomId(roomId);

Comment on lines 31 to 34
@GetMapping
public ResponseEntity<List<Map<String, String>>> findRooms() {
return ResponseEntity.ok(roomService.findRooms());
}
Copy link
Author

Choose a reason for hiding this comment

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

이 부분은 프론트에 방 목록을 보내주기 위해 필요한 부분인데요,
기존에 Room, RoomDto가 이미 있으나, 여기에는 모두 비밀번호를 나타나는 password 필드가 있습니다
그런데 api에 비밀번호를 담아 주고 싶지 않고, idname만 담는 dto 클래스를 만드려 하니,
그러면 반환 포맷에 변화가 생길 때 마다 dto를 추가해줘야 하나 싶어서 일단 Map 으로 만들었습니다...
dto를 추가해주는 게 맞을까요?

Copy link

Choose a reason for hiding this comment

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

dto도 각 레이어별 (controller, service, repository 등) 존재할 수 있고, 원하는 형태의 응답을 위해 여러 많은 dto가 추가되는 건 자연스러운 일이에요.

Map을 사용하면 단순히 메서드 시그니처만 봤을 때는 어떤 응답을 만들어 주는지 알기 어렵고, 모든 값을 넣을 수 있다보니 실수로 넣지 않아야 하는 값을 넣는 문제도 있을 수 있을 것 같아요.

당장 Map을 사용하면 dto를 새로 만들지 않아도 되니까 편하겠지만 앞으로 이런 부분들이 계속해서 생겨나면 유지보수하는 작업이 어려워지지 않을까요?

Copy link

@lalize lalize left a comment

Choose a reason for hiding this comment

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

써머 🏖️ , 안녕하세요!
2단계 잘 구현해주셨네요 👍
몇가지 코멘트 남겨두었으니 확인 부탁드릴게요!
궁금한 점 있으시면 편하게 DM 주세요~

Comment on lines 28 to 29
gameRepository.deleteOldBoard(roomId);
return gameRepository.saveGame(roomId, board);
Copy link

Choose a reason for hiding this comment

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

save할 때 항상 이전 데이터의 삭제가 필요하면 saveGame 메서드에서 함께 처리하는 건 어떨까요?

Comment on lines 38 to 41
PieceDto pieceDto = gameRepository.findPiece(boardId, source).get();
deleteMovedPieceFromSource(boardId, source);
updateMovedPieceToTarget(boardId, target, pieceDto);
updateGameState(board, boardId);
Copy link

Choose a reason for hiding this comment

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

board, piece 테이블에 개별로 접근해야 하는 메서드들은 책임이 분리되지 않고, 그냥 클래스 계층만 한 단계 깊어지게 되더라고요

서비스에서 개별 테이블에 접근이 필요한 걸까요? #383 (comment)

move(...) {
  board = repository.find(...);
  board.move(...);
  repository.save(...);
}

pieces.put(Position.of(pieceDto.getPosition()), PieceFactory.build(pieceDto));
}
Board board = new Board(() -> pieces);
board.loadTurn(boardDao.getTurn(boardId));
Copy link

Choose a reason for hiding this comment

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

loadTurn대신 생성자에서 turn을 주입받으면 좋을 것 같아요.

int boardId = boardRepository.getBoardIdByRoom(roomId);
Board board = loadBoard(boardId);
board.loadTurn(boardRepository.getTurn(boardId));
int boardId = gameRepository.findBoardIdByRoom(roomId);
Copy link

Choose a reason for hiding this comment

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

boardId를 단독으로 가져와서 다시 board를 조회할 필요가 있을까요?

아래와 같은 방식으로 할 수 있지 않을까요?

board = repository.findByRoomId(roomId);

Comment on lines 31 to 34
@GetMapping
public ResponseEntity<List<Map<String, String>>> findRooms() {
return ResponseEntity.ok(roomService.findRooms());
}
Copy link

Choose a reason for hiding this comment

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

dto도 각 레이어별 (controller, service, repository 등) 존재할 수 있고, 원하는 형태의 응답을 위해 여러 많은 dto가 추가되는 건 자연스러운 일이에요.

Map을 사용하면 단순히 메서드 시그니처만 봤을 때는 어떤 응답을 만들어 주는지 알기 어렵고, 모든 값을 넣을 수 있다보니 실수로 넣지 않아야 하는 값을 넣는 문제도 있을 수 있을 것 같아요.

당장 Map을 사용하면 dto를 새로 만들지 않아도 되니까 편하겠지만 앞으로 이런 부분들이 계속해서 생겨나면 유지보수하는 작업이 어려워지지 않을까요?

@GetMapping(value = "/{roomId}", params = "command=load")
public ResponseEntity<BoardDto> loadGame(@PathVariable int roomId) {
return ResponseEntity.ok(gameService.loadGame(roomId));
@GetMapping(value = "/{roomId}/new")
Copy link

Choose a reason for hiding this comment

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

POST가 적절해 보이네요!

@DeleteMapping("/{roomId}")
public ResponseEntity<Map<String, String>> deleteRoom(@PathVariable int roomId, @RequestParam String password) {
roomService.delete(roomId, password);
return ResponseEntity.ok(Map.of("url", "/"));
Copy link

Choose a reason for hiding this comment

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

이 부분은 url을 내려주지 않아도 프론트가 어디로 이동해야 할지 알 수 있지 않을까요?

Comment on lines 30 to 38
Optional<Integer> boardId = boardDao.findBoardIdByRoom(roomId);
Optional<RoomDto> room = roomDao.findById(roomId);
checkPassword(room, password);
if (boardId.isEmpty()) {
roomDao.delete(roomId);
return;
}
return roomRepository.find(name).get();
checkGameEnd(boardId);
roomDao.delete(roomId);
Copy link

Choose a reason for hiding this comment

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

RoomService는 개별 RoomDao와 BoardDao를 사용하도록 뒀는데, 여기에 대해 추가로 참고할 만한 키워드가 있을까요?
저수준의 CRUD는 DAO, 컬렉션 처리는 Repository 까지는 이해했는데, 제 코드에 적용하려니 어디까지가 컬렉션 처리인가? 저수준의 CRUD만 필요한 경우 dao를 바로 사용해도 되는가? 같은 고민 때문에 쉽지 않네요

Repository는 객체를 다루고, Dao는 테이블을 다룬다고 보면, 객체 구조가 너무나 단순하여 Repository가 단순 by-pass로 보여도 객체를 저장, 수정, 조회, 삭제를 하는 건 Repository를 만들어 사용하는 게 역할에 맞는 책임이라고 생각해요.

체스 코드에서는 Piece는 특정 Board에 속하고 모든 비즈니스는 Board를 중심으로 일어나기 때문에 객체 컬렉션을 처리하는 데 PieceRepository를 통하는 게 아닌 BoardRepository(GameRepository)를 통해야 합니다. 하나의 유기체처럼 동작하는 객체마다 일반적으로 Repository 한개가 만들어진다고 보면 됩니다.

Optional<RoomDto> roomDto = roomRepository.find(name);
if (roomDto.isEmpty()) {
roomRepository.save(name);
public void delete(int roomId, String password) {
Copy link

Choose a reason for hiding this comment

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

dto와 getter로 비즈니스 로직을 수행하고 있는 것 같아요.
아래와 비슷한 구조로 변경해보면 어떨까요?

void delete(...) {
  room = repository.findById(roomId);
  if (!room.isCorrect(password) && ...) {
    throw ...
  }
  repository.delete(room);
}

import org.springframework.stereotype.Repository;

@Repository
public class GameRepository {
Copy link

Choose a reason for hiding this comment

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

👍

@hyewoncc
Copy link
Author

hyewoncc commented May 3, 2022

안녕하세요, 핀! 남겨주신 피드백 모두 반영했습니다 🙇
처음 리뷰를 받았을 때는 DAO, Repository 차이에 대해 감이 잘 오지 않았는데 핀의 리뷰를 통해서 잘 이해할 수 있었어요
둘을 협력하는 객체가 아닌 DB에 접근하는 수단으로만 봐서 1단계에는 다분히 절차지향적인 코드를 짜버렸는데,
피드백을 반영하면서 둘을 객체지향적 관점으로 볼 수 있게 되었습니다, 감사해요 👍 덕분에 많이 배웠습니다
마지막으로 하나 여쭤보고 싶은 게 있는데, 지금 Room 객체를 저장한 후, DB에서 자동 생성된 id 값을 받아오기 위해 일시적으로 id에 null 값을 넣어 Room을 생성하고 있습니다
그런데 필드값에 null이 들어가는 게 논리적으로 맞지 않다고 여겨집니다, 다른 사람이 보면 null 인지 모르고 id 값에 접근할 수도 있고요
이런 경우에 entity를 사용하거나 해야할까요..? 이번 미션에서 다른 크루들과 얘기하다 엔티티 얘기를 처음 들어서 궁금하네요

Copy link

@lalize lalize left a comment

Choose a reason for hiding this comment

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

써머 🏖️ , 안녕하세요!
리뷰 반영 잘 해주셨네요~ 👍
이번 미션은 여기서 마무리하도록 할게요!
궁금한 점 있으시면 언제든지 DM 주셔도 좋습니다~
고생하셨습니다~~

Comment on lines +3 to +10
public class Room {

public static final int NAME_LENGTH_LIMIT = 16;
public static final int PASSWORD_LENGTH_LIMIT = 16;

private long id;
private final String name;
private final String password;
Copy link

Choose a reason for hiding this comment

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

#474 (comment)

도메인과 엔티티를 분리하는 것도 좋고, id 생성을 DB에 의존하지 않고 코드 레벨에서 uuid를 생성하는 방법도 있을 것 같아요 ㅎㅎ
역시나 정답은 없다고 생각해요 :)

@lalize lalize merged commit 45b72af into woowacourse:hyewoncc May 4, 2022
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.

None yet

2 participants