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

[라빈] 체스 웹/DB 미션 제출합니다 #133

Merged
merged 82 commits into from
Apr 19, 2020

Conversation

giantim
Copy link

@giantim giantim commented Apr 6, 2020

화투 안녕하세요! 라빈입니다.
체스 미션 1단계 머지 해주시면서 남겨주셨던 피드백 먼저 반영하고 웹과 DB 구현했습니다.

이번에 처음으로 DAO 라는 개념에 대해 알게 되었고 적용해서 프로그램을 구현해 봤습니다. 잘못 사용한 부분이 있다면 조언 부탁드리겠습니다!

감사합니다!

giantim and others added 30 commits March 24, 2020 14:06
    - 게임을 진행하는 ChessRunner 클래스 구현
Copy link

@phs1116 phs1116 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 27 to 36
Map<String, Object> model = new HashMap<>();

webChessController.start();
List<TileDto> tileDtos = webChessController.getTiles();
TeamDto teamDto = webChessController.getCurrentTeam();

model.put("tiles", tileDtos);
model.put("currentTeam", teamDto);

return render(model, "game.html");
Copy link

Choose a reason for hiding this comment

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

Controller는 model에 대한 매핑과 어떤 뷰를 노출할지에 대한 책임까지 가지고있습니다.
이러한 로직들도 컨트롤러에 넘기면 어떨까요? :)

Copy link
Author

Choose a reason for hiding this comment

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

컨트롤러는 뷰를 선택하는 역할을 하도록 하고 내부 로직은 서비스 레이어를 만들어서 처리하도록 수정했습니다

Comment on lines 29 to 31
ChessBoardDAO chessBoardDAO = new ChessBoardDAO();
CurrentTeamDAO currentTeamDAO = new CurrentTeamDAO();
PieceDAO pieceDAO = new PieceDAO();
Copy link

Choose a reason for hiding this comment

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

DAO는 매번 재생성 될 필요가 없는 객체인데요.
생성해서 재사용하도록 수정해보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

서비스 레이어에서 한번만 DAO 를 생성해서 재사용 하도록 수정했습니다.

Comment on lines 23 to 26
private ChessRunner chessRunner;
private ChessBoard chessBoard;
private CurrentTeam currentTeam;
private PieceOnBoards originalPieces;
Copy link

Choose a reason for hiding this comment

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

웹 컨트롤러 상에서는 상태를 가지는 객체를 들고있지 않도록 하는게 좋아요.
여러 쓰레드에서 접근하기 때문에 동기화 문제가 발생할 여지가 있습니다. :)
DAO를 통해 데이터를 꺼내가며 생성하도록 구현해보는게 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

컨트롤러가 상태를 갖는 객체를 갖지 않도록 서비스 레이어로 이동했습니다.

Comment on lines 20 to 21
PreparedStatement pstmt = con.prepareStatement(query);
ResultSet rs = pstmt.executeQuery();
Copy link

Choose a reason for hiding this comment

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

두 객체에 대해서도 꼭 close 해주세요. :)
try-with-resources를 참고해봐도 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

알려주신 내용을 참고해서 트랜잭션 처리 로직을 변경했습니다. 감사합니다 😀

import java.util.List;

public class PieceDAO {
public void addPiece(int chessBoardId, List<TileDto> tileDtos) throws SQLException {
Copy link

Choose a reason for hiding this comment

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

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.

DAO 에서는 데이터베이스에 저장된 데이터와 매핑되는 모델만을 이용하도록 변경했습니다.

pieceDAO.deletePiece(deletedPiece);
}
Optional<PieceOnBoard> sourcePiece = this.originalPieces.find(source);
pieceDAO.updatePiece(sourcePiece.get(), target);
Copy link

Choose a reason for hiding this comment

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

Optional.get은 왠만하면 지양하는 편이 좋아요. :)
get보다는 그외 제공하는 메서드를 통해 null에 대해서도 처리해주세요.

Copy link
Author

Choose a reason for hiding this comment

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

Optional 에서 get() 을 호출하기 전 isPresent() 를 먼저 호출해서 null 처리를 해주었습니다.

@@ -39,4 +39,26 @@
public Map<Position, Piece> initialize() {
return InitialBishop.initializeBishops();
}

@Override
public Map<Position, Piece> webInitialize(Map<String, String> pieceOnBoards) {
Copy link

Choose a reason for hiding this comment

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

webInitialize등 도메인에서 web에 대한 의존을 굳이 표현할 필요는 없을 것 같아요.

@Override
public Map<Position, Piece> webInitialize(Map<String, String> pieceOnBoards) {
Map<String, String> bishops = pieceOnBoards.entrySet().stream()
.filter(entry -> entry.getValue().substring(0, 1).toLowerCase().equals("b"))
Copy link

Choose a reason for hiding this comment

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

"b"등 하드코딩은 지양하는게 좋을 것 같네요. :)

Comment on lines 45 to 47
Map<String, String> knights = pieceOnBoards.entrySet().stream()
.filter(entry -> entry.getValue().substring(0, 1).toLowerCase().equals("n"))
.collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
Copy link

Choose a reason for hiding this comment

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

pieceOnBoard에 대해서 pieceUrl을 파싱하는게 아닌,
피스에 대한 정보 없이 이미지 이름 가지고 도메인을 풀어가는것보다는
pieceOnBoard자체에 어떤 피스인지에 대한 정보를 가지고있는게 맞지 않을까요?
도메인을 조금 더 드러나게 모델링해보아요.

Copy link
Author

Choose a reason for hiding this comment

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

데이터베이스에 저장되는 piece -> pieceOnBoard 로 변경하고 이미지가 아닌 pieceType, team 정보를 저장하도록 변경하였습니다.

.filter(td -> td.getPosition().equals(tile.position()))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
tileDto.setPieceImageUrl(tile.pieceImageUrl());
Copy link

Choose a reason for hiding this comment

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

setPieceImageUrl
세터는 되도록 지양하도록 구현해보아요. :)
다른 피드백에 드렸다시피 Tile 자체가 어떤 말인지에 대한 정보를 가지고있다면 이러한 주소는 굳이 가지고 있지 않아도 될 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

도메인이 드러나도록 모델링을 변경하니 Tile 클래스가 사라지게 되었습니다. 감사합니다!

@giantim
Copy link
Author

giantim commented Apr 13, 2020

화투 안녕하세요!
수정하고 추가한 내용이 많아서 피드백 반영이 조금 늦었습니다. 죄송합니다!
제가 생성한 데이터베이스 코드 첨부합니다. 감사합니다!

CREATE DATABASE webChess DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

USE webChess;

CREATE TABLE chessBoard (
		chessBoardId int NOT NULL AUTO_INCREMENT,
		PRIMARY KEY(chessBoardId));

CREATE TABLE currentTeam (
		currentTeamId int NOT NULL AUTO_INCREMENT,
		team VARCHAR(5) NOT NULL,
		chessBoardId int NOT NULL,
		PRIMARY KEY(currentTeamId),
		FOREIGN KEY(chessBoardId) REFERENCES chessBoard(chessBoardId));

CREATE TABLE pieceOnBoard (
		pieceId int NOT NULL AUTO_INCREMENT,
		position VARCHAR(2) NOT NULL,
		pieceType VARCHAR(10) NOT NULL,
                team VARCHAR(5) NOT NULL,
		chessBoardId int NOT NULL,
		PRIMARY KEY(pieceId),
		FOREIGN KEY(chessBoardId) REFERENCES chessBoard(chessBoardId));

CREATE TABLE player (
		playerId int NOT NULL AUTO_INCREMENT,
		whitePlayer VARCHAR(10) NOT NULL,
		blackPlayer VARCHAR(10) NOT NULL,
		chessBoardId int NOT NULL,
		PRIMARY KEY(playerId),
		FOREIGN KEY(chessBoardId) REFERENCES chessBoard(chessBoardId));

Copy link

@phs1116 phs1116 left a comment

Choose a reason for hiding this comment

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

안녕하세요! 라빈.
개발 잘해주셨어요.
몇가지 피드백 더 추가했는데 참고 부탁드릴게요. :)

PreparedStatement pstmt = con.prepareStatement(query)) {
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
Copy link

Choose a reason for hiding this comment

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

이펙티브 자바 아이템 77. 예외를 무시하지 말라
해당 케이스는 복구 불가능한 예외로 보이는데요, 예외를 무시하지 말고 커스텀 예외를 더 던져주는게 좋아보여요 :)

Copy link
Author

Choose a reason for hiding this comment

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

답변 주신 내용을 바탕으로 CustomSQLException 을 만들어서 예외 출력 페이지로 이동시켰습니다!

return chessBoard;
} catch (SQLException e) {
e.printStackTrace();
return null;
Copy link

Choose a reason for hiding this comment

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

데이터가 없는 경우에 대해서는 null 혹은 Optional을 사용할 수 있지만
해당 케이스에서는 예외를 새로 던져보아요. :)

System.out.println("정상적으로 연결되었습니다.");
} catch (SQLException e) {
System.err.println("연결 오류:" + e.getMessage());
e.printStackTrace();
Copy link

Choose a reason for hiding this comment

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

연결 실패시에도 null을 돌려주어 null 위험성을 만드는 것 보다는 예외를 추가로 던져주는게 좋을 것 같아요. :)

Comment on lines 8 to 9
private String pieceType;
private String team;
Copy link

Choose a reason for hiding this comment

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

pieceType, team에 대해서도 enum으로 관리해도 될 것 같네요.
position도 하나의 문자열로 처리하기보다는 기존 enum을 사용해서 자료구조를 명확하게 드러내주는게 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

답변 주신 내용을 바탕으로 도메인을 드러내도록 표현하게 변경하였습니다 :)

for (PieceOnBoard pieceOnBoard : pieceOnBoards) {
String query = "INSERT INTO pieceOnBoard (position, pieceType, team, chessBoardId) "
+ "VALUES(?, ?, ?, ?)";
PreparedStatement pstmt = con.prepareStatement(query);
Copy link

Choose a reason for hiding this comment

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

PreparedStatement에 대해서도 꼭 close 해주세요!

Copy link
Author

Choose a reason for hiding this comment

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

try-with-resource 구문의 안쪽에 선언된 PreparedStatement, ResultSet 를 닫는 메서드를 추가하였습니다 :)

try (Connection con = ConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement(query);) {
pstmt.setInt(1, chessBoard.getChessBoardId());
ResultSet rs = pstmt.executeQuery();
Copy link

Choose a reason for hiding this comment

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

result set도 close 해주세요. :)

public Optional<PieceOnBoard> find(String position) {
Optional<PieceOnBoard> piece = Optional.ofNullable(this.pieceOnBoards.stream()
.filter(p -> p.getPosition().equals(position))
.findFirst()
Copy link

Choose a reason for hiding this comment

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

Optional을 굳이 추가로 래핑하지 않아도 findFirst 자체가 옵셔널을 리턴해요. :)

.filter(td -> td.getPosition().equals(entry.getKey().toString()))
.findFirst()
.orElseThrow(IllegalArgumentException::new)
.setPieceImageUrl(entry.getValue().toSymbol() + entry.getValue().teamName().toLowerCase());
Copy link

Choose a reason for hiding this comment

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

set을 사용하지 않을수 있지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

DTO 에서 사용하는 모든 setter 메서드를 제거하였습니다.

Comment on lines 111 to 112
BoardScoreDTO currentTeam = new BoardScoreDTO(calculateScore());
currentTeam.setTeam(this.currentTeam.name());
Copy link

Choose a reason for hiding this comment

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

굳이 setter를 사용하지 않아도 되지 않을까요? 그냥 생성자를 통해 넘겨줘도 될 것 같네요. :)

Comment on lines 26 to 28
private ChessBoard chessBoard;
private CurrentTeam currentTeam;
private PieceOnBoards originalPieces;
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.

chessBoardId 를 이용하여 해당 객체들의 생명 주기를 메서드 단위로 변경하였습니다.

@giantim
Copy link
Author

giantim commented Apr 16, 2020

화투 안녕하세요! 피드백 주신 내용 반영했습니다. 잘못 이해해서 반영한 부분이 있다면 지적 부탁드립니다!
항상 구체적이고 꼼꼼한 리뷰 감사합니다. 정말 많은 도움이 되고있습니다 :)

Copy link

@phs1116 phs1116 left a comment

Choose a reason for hiding this comment

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

안녕하세요, 라빈.
피드백 반영 잘해주셨어요!
이번 단계 머지할게요!
간단한 피드백 추가했는데 참고만 한번 부탁드려요!

Comment on lines +96 to +97
} catch (CustomSQLException e) {
model.put("errMessage", e.getMessage());
Copy link

Choose a reason for hiding this comment

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

그냥 모든 예외를 잡아 처리하도록 하여
http://sparkjava.com/documentation#error-handling
해당 기능을 활용했어도 좋았을 것 같네요. :)

do {
command = Command.of(inputView.askGameCommand());
} while (isEmptyCommand(command));
return command.get();
Copy link

Choose a reason for hiding this comment

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

Optional.get은 직접적으로 사용되지 않는게 좋아요.

}

private void updateChessBoard(int chessBoardId, String source, String target) {
PieceOnBoard piece = null;
Copy link

Choose a reason for hiding this comment

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

자료 선언구와 초기화 구는 최대한 같이 해주시는고 참조 변경은 최대한 없애는게 좋을 것 같아요. :)
두개를 선언하게 되더라도요. :)

@phs1116 phs1116 merged commit 86c76d6 into woowacourse:giantim Apr 19, 2020
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.

2 participants