각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 1 회고 - 체스 4, 5단계 미션 2차 피드백을 받아보다 (65일차)

제이온 (Jayon) 2021. 4. 7.

안녕하세요? 제이온입니다.

 

드디어 길고 길었던 체스 미션이 merge되었습니다. 하지만, 약간의 피드백이 추가적으로 있어서 이 부분을 반영해 보았습니다.

 

 

체스 미션 4, 5단계 2차 피드백

 

 

첫 번째 피드백은 SQLException을 Dao 단에서 처리하는 것입니다. 저는 서비스까지 올린 다음, 사용하는 입장에서 예외를 처리해야한다고 생각했습니다. 하지만, 그것은 계속해서 위로 예외를 올리는 것이므로 SQLException 정도는 Dao 단에서 catch해도 된다고 합니다.

 

 

    public void deleteAll() {
        final String query = "TRUNCATE TABLE board";
        try (final Connection conn = ConnectionSetup.getConnection();
            final PreparedStatement pstmt = conn.prepareStatement(query)) {
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

 

 

이런식으로 바로 printStackTrac()를 작성해 주어도 무방하다고 합니다.

 

 

 

 

두 번째 피드백은 Optional을 통해 NPE를 회피하라는 것입니다.

 

 

    public Optional<BoardDto> load(final long id) {
        final String query = "SELECT * FROM board WHERE id = ?";

        try (final Connection conn = ConnectionSetup.getConnection();
            final PreparedStatement pstmt = conn.prepareStatement(query);
            final ResultSet rs = pstmt.executeQuery()) {
            pstmt.setLong(1, id);
            pstmt.executeUpdate();
            if (!rs.next()) {
                return Optional.empty();
            }

            final String team = rs.getString("team");
            final boolean isGameOver = rs.getBoolean("isGameOver");
            return Optional.of(new BoardDto(team, isGameOver));
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }

 

 

위와 같이 load() 메소드에서 특정 상황에 대해 null을 반환하지말고 Optional.empty()을 반환합니다.

 

 

    public void chessBoardInit() {
        final Map<Position, Piece> chessBoard = pieceDao.load()
            .orElseGet(() -> pieceDao.save(new TreeMap<>(new Board().unwrap())));

        final BoardDto boardDto = boardDao.load(1)
            .orElseGet(() -> boardDao.save(1, Team.WHITE.teamName(), false));

        chessGame = new ChessGame(new Board(chessBoard), boardDto.team(), boardDto.isGameOver());
    }

 

 

그리고 사용하는 입장에서 orElseGet()을 통해 해당 반환값이 null이거나 Optional.empty()면 람다 함수를 실행하는 것입니다. 이를 통해 null check을 피할 수 있게 되는 것이죠.

 

여담으로, 첫 번째 orElseGet() 내의 람다 함수를 보면 save의 인자로 TreeMap을 통해 새롭게 객체를 할당하는 것을 알 수 있습니다. 이것은 Board 객체의 unwrap 메소드의 반환값이 unmodifiableMap이기때문입니다.

 

 

    public void move(final Position source, final Position target, final Team team) {
        validateRightTurn(source, team);
        if (checkPath(source, target)) {
            chessBoard.put(target, chessBoard.get(source));
            chessBoard.put(source, Blank.getInstance());
            return;
        }
        throw new IllegalArgumentException("해당 위치로 이동할 수 없습니다.");
    }

 

 

만약, TreeMap을 통해 새롭게 객체를 할당하지 않는다면 해당 unmodifiableMap은 읽기 전용 맵이라서 이 맵으로 만들어진 ChessGame 객체의 Board는 move() 메소드를 수행할 수 없게 됩니다.

 

 

    public Response move(final MoveRequest moveRequest) {
        try {
            final Position source = Position.from(moveRequest.source());
            final Position target = Position.from(moveRequest.target());
            chessGame.move(source, target);
            changeStatusSaveData(source, target);
            return new Response(ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message());
        } catch (IllegalArgumentException | UnsupportedOperationException e) {
            return new Response(ResponseCode.ERROR.code(), e.getMessage());
        }
    }

 

 

결과적으로 위 메소드에서 읽기 전용 컬렉션을 수정할 때 발생하는 UnsupportedOperationException이 생깁니다. 문제는 제가 UnsupportedOperationException을 읽기 전용 컬렉션을 수정할 때에 대해 처리하려고 만들어 놓은 것이 아니라는 겁니다.

 

 

    @Override
    public boolean canMove(final Position source, final Position target, final Piece piece) {
        throw new UnsupportedOperationException("비어 있는 칸입니다.");
    }

 

 

Blank 객체를 사용자가 움직이려고할 때 해당 예외를 발생하게끔 디자인한 것이었죠. 하지만, 예상치 못하게 다른 예외가 잡히게 되어 e.getMessage()는 null이 반환됩니다. 이 null값이 프론트로 넘어가면서 e 객체는 undefined 타입으로 인식되었고, 저는 어떠한 에러로 인해 메시지가 undefined가 뜨는지 알 수가 없었습니다.

 

 

    if (response.code === 400) {
        alert(response.message);
        return;
    }

 

 

참고로 js 코드에서는 위처럼 에러가 발생하면 메시지 창을 띄우도록 설계했습니다. 일반적인 경우에는 체스말을 이동할 수 없다거나, 본인의 턴에 맞는 체스말을 이동하라거나 등의 메시지가 정상적으로 떠아하는데, null 에러 객체가 넘어왔을 때는 undefined 창이 뜨게 됩니다.

 

 

위와 같은 경험을 하면서 처음에는 unmodifiable 메소드를 쓰지 말아야겠다고 생각했으나, 제 주변 친구가 단순히 에러 핸들링이 어렵다는 이유만으로 unmodifiable 메소드 자체를 거부하려는 마인드는 좋지 않다고 충고해 주었습니다. 생각해 보면, 이 메소드 탓이 아니라 제가 에러 로그를 제대로 짜지 못한 것이 가장 큰 원인이었죠.

 

또한, 커스텀 익셉션의 필요성을 느끼게 되었습니다. 모든 익셉션에 대해서 커스텀을 사용하라는 것은 아니지만, 이렇게 다른 메소드의 익셉션과 겹칠 경우에는 에러 핸들링이 어렵기 때문에 적절한 상황에서는 커스텀 익셉션을 정의하여 사용해야겠다고 느꼈습니다.

 

 

 

 

마지막으로 WHERE 구를 통해 쿼리문을 작성하라는 것입니다. 지금은 하나의 게임만이 작동하고 있지만, 나중에 방이 생겨서 여러 개의 게임을 가동해야한다면 문제가 생길 수 있습니다. 따라서, 기본키를 공부하여 id를 도입하였습니다. 현재 board 테이블은 team과 isGameOver이 있는데, 여기에 id를 추가하였습니다. DDL 자체는 아래와 같습니다.

 

 

CREATE TABLE board (
    id BIGINT NOT NULL PRIMARY KEY,
    team VARCHAR(20) NOT NULL,
    isGameOver BOOLEAN NOT NULL
);

 

 

그리고 WHERE 구에 id 조건을 추가하였습니다.

 

 

정리

피드백은 적었지만, 이를 적용해 보면서 많은 것을 배울 수 있었습니다. 나머지 방학 기간동안은 DB와 웹 기초를 다지면서 레벨2부터 배울 스프링에 대비할 생각입니다.

 

완성된 체스 미션 코드는 이곳에서 확인하실 수 있습니다.

댓글

추천 글