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

[우아한 테크코스 3기] LEVEL 1 회고 (58일차)

제이온 (Jayon) 2021. 3. 31.

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

 

오늘은 온라인이지만, 대림역에서 크루들과 만나서 모각코를 진행했습니다.

 

 

데일리 미팅

오늘의 데일리 미팅 진행자는 피카였습니다. 피카는 'LEVEL 1 동안 자신이 얼마나 성장하였는지'에 대해 이야기하자고 제안했습니다. 크루들은 대부분 개발적인 부분에서 성장과 친목적인 부분에서 성장을 하였다고 말해 주었고, 저 또한 이에 속했습니다.

 

처음에는 일급 컬렉션, 원시값 포장, 전략 패턴, DTO 등 생소한 용어가 너무나 많았고 필요성도 잘 느끼지 못했습니다. 하지만, 이제는 이들을 사용하면 장점이 무엇이고 자연스럽게 응용할 수 있게된 개념이 많아졌습니다.

 

또한, 평소에도 성실하게 살려고 노력을 많이 했는데, 이번에는 잘하는 크루에게 자극을 받아서 인생에서 가장 열심히 살고 있는 것 같습니다. 1일 1회고도 어쩌면 레벨 1에서 하겠다고 말을 안했으면, 이렇게까지 매일 쓰지는 못했을 것 같습니다.

 

마지막으로, 처음에는 우테코를 온라인으로만 하면서 귀중한 크루들과 친해지지 못할까봐 걱정이 되었는데, 오프라인으로 루터회관에 나오자마자 다양한 크루들과 친해질 수 있어서 이것또한 성장이라고 생각했습니다.

 

 

체스 5단계 - 오전

데일리 미팅이후에는 오전동안 웹 페이지를 리로드하더라도 게임 진행 상황이 유지될 수 있도록 구현하였습니다. 

 

 

public class BoardAndPieceDto implements ResponseDto {

    private final BoardDto boardDto;
    private final List<PieceDto> pieceDtos;

    public BoardAndPieceDto(final BoardDto boardDto, List<PieceDto> pieceDtos) {
        this.boardDto = boardDto;
        this.pieceDtos = new ArrayList<>(pieceDtos);
    }
}

 

 

서버단에서 위와 같은 Dto 객체를 만들어서 프론트에게 넘겨 주었고, 프론트는 해당 객체의 BoardDto와 PieceDto를 사용하는 방식입니다.

 

 

<BoardDto>

public class BoardDto implements ResponseDto {

    private final String team;
    private final boolean isGameOver;

    public BoardDto(final String team, final boolean isGameOver) {
        this.team = team;
        this.isGameOver = isGameOver;
    }

    public String team() {
        return team;
    }

    public boolean isGameOver() {
        return isGameOver;
    }
}

 

 

<PieceDto>

public class PieceDto implements ResponseDto {

    private final String position;
    private final String name;

    public PieceDto(final String position, final String name) {
        this.position = position;
        this.name = name;
    }
}

 

 

그리고 이들을 사용하여 8 x 8 배열을 만들었습니다. js 코드를 봅시다.

 

 

async function createChessBoard() {
    const response = await fetch("/start")
    .then(res => res.json())

    const responseDto = response.responseDto;
    const boardDto = responseDto.boardDto;
    let pieces = [];
    for (let i = 0; i < 64; i += 8) {
        pieces.push(responseDto.pieceDtos.slice(i, i + 8));
        pieces[i / 8] = pieces[i / 8].map(x => x.name);
    }
    makeTable(pieces, boardDto);
    addClickEventListener();
}

 

 

BoardDto는 현재 턴이 누구인지와 게임 종료 여부를 판단하기 위해 사용하고, PieceDto는 특정 체스말이 어디에 위치해있는지 알기 위해 사용합니다. 처음에는 64개의 데이터가 1차원 배열로 담기므로 slice() 함수를 통해 8 x 8로 나눴습니다. 그리고 현재 pieces에 담긴 요소는 PieceDto이므로 name을 담아주기 위하여 map() 함수를 사용했습니다.

 

다만, 서버에서 넘겨줄 때 코드가 꽤 더럽습니다.

 

 

    public Response start() {
        final BoardDto boardDto = new BoardDto(chessGame.nowTurn().teamName(), chessGame.isGameOver());
        final Map<Position, Piece> chessBoard = chessGame.board().unwrap();
        final List<PieceDto> pieceDtos = new ArrayList<>();
        chessBoard.forEach((position, piece) -> {
            final String positionValue = position.horizontal().symbol() + position.vertical().symbol();
            String name = "";
            if (!piece.name().equals(".")) {
                if (piece.team().teamName().equals("Black")) {
                    name = "B" + piece.name();
                } else {
                    name = "W" + piece.name().toUpperCase();
                }
            }
            pieceDtos.add(new PieceDto(positionValue, name));
        });
        return new Response("200", "성공", new BoardAndPieceDto(boardDto, pieceDtos));
    }

 

 

어떻게 프론트에서 좀 편하라고 가공을 해 놓은 것인데, 오늘은 급하게 기능 구현을 하기 위해서 리팩토링을 하지는 않았습니다.

 

여기까지는 오전에 한 일이고, 점심 회식 이후 대림역에 가서는 DB 연결을 하기 위해 온갖 노력을 다했습니다.

 

 

체스 5단계 - 오후

체스 5단계의 핵심은 서버를 재시작하더라도 이전에 하던 게임을 이어서 할 수 있어야 한다는 것입니다. 즉, 게임 중간 중간 상황을 DB에 담아야 합니다. 저는 오전에 웹 사이트에서 재시작하더라도 게임 상황을 이어서하는 것과 비슷한 맥락이라고 판단했고, Dao도 Dto와 유사하게 작성했습니다.

 

 

public class PieceDao {

    private static final Map<String, Piece> PIECES = new HashMap<>();

    static {
        PIECES.put("p", new Pawn(Team.WHITE));
        PIECES.put("P", new Pawn(Team.BLACK));
        PIECES.put("q", new Queen(Team.WHITE));
        PIECES.put("Q", new Queen(Team.BLACK));
        PIECES.put("r", new Rook(Team.WHITE));
        PIECES.put("R", new Rook(Team.BLACK));
        PIECES.put("b", new Bishop(Team.WHITE));
        PIECES.put("B", new Bishop(Team.BLACK));
        PIECES.put("k", new King(Team.WHITE));
        PIECES.put("K", new King(Team.BLACK));
        PIECES.put("n", new Knight(Team.WHITE));
        PIECES.put("N", new Knight(Team.BLACK));
        PIECES.put(".", Blank.getInstance());
    }

    private final Connection conn;

    public PieceDao() {
        conn = ConnectionSetup.getConnection();
    }

    public Map<Position, Piece> load() throws SQLException {
        final String query = "SELECT * FROM pieces";
        final PreparedStatement pstmt = conn.prepareStatement(query);
        final ResultSet rs = pstmt.executeQuery();

        if (!rs.next()) {
            return null;
        }

        final Map<Position, Piece> pieces = new TreeMap<>();
        do {
            final String positionValue = rs.getString("position");
            final Position position = new Position(positionValue.split("")[0],
                positionValue.split("")[1]);
            final String name = rs.getString("name");
            pieces.put(position, PIECES.get(name));
        } while (rs.next());

        return pieces;
    }

    public void save(final Map<Position, Piece> pieces) throws SQLException {
        deleteAll();
        for (final Position position : pieces.keySet()) {
            savePiece(position, pieces.get(position));
        }
    }

    private void savePiece(final Position position, final Piece piece) throws SQLException {
        String query = "INSERT INTO pieces VALUES (?, ?)";
        PreparedStatement pstmt = conn.prepareStatement(query);
        pstmt.setString(1, position.horizontal().symbol() + position.vertical().symbol());
        pstmt.setString(2, piece.name());
        pstmt.executeUpdate();
    }

    public void deleteAll() throws SQLException {
        String query = "DELETE FROM pieces";
        PreparedStatement pstmt = conn.prepareStatement(query);
        pstmt.executeUpdate();
    }
}

 

 

위와 같이 pieceDao를 정의했습니다. 여기서 문제점은 DB의 name만 가지고 어떠한 체스말인지 확인하기 힘들었다는 것이었습니다. 그래서 부득이하게 체스말의 타입을 캐싱해 놓았습니다. 거의 하드 코딩한 것이라 상당히 보기 싫은데 이 부분은 내일 리팩토링해 봐야겠습니다.

 

 

public class BoardDao {
    private final Connection conn;

    public BoardDao() {
        conn = ConnectionSetup.getConnection();
    }

    public BoardDto load() throws SQLException {
        String query = "SELECT * FROM board";
        PreparedStatement pstmt = conn.prepareStatement(query);
        ResultSet rs = pstmt.executeQuery();

        if (!rs.next()) {
            return null;
        }
        return new BoardDto(rs.getString("team"), rs.getBoolean("isGameOver"));
    }

    public void save(final BoardDto boardDto) throws SQLException {
        deleteAll();
        String query = "INSERT INTO board VALUES (?, ?)";
        PreparedStatement pstmt = conn.prepareStatement(query);
        pstmt.setString(1, boardDto.team());
        pstmt.setBoolean(2, boardDto.isGameOver());
        pstmt.executeUpdate();
    }

    public void deleteAll() throws SQLException {
        String query = "DELETE FROM board";
        PreparedStatement pstmt = conn.prepareStatement(query);
        pstmt.executeUpdate();
    }
}

 

 

BoardDao도 비슷한 맥락으로 쿼리문을 작성했습니다. 문제는 이것을 어떻게 작용하느냐인데, 일단은 비효율적이지만 쉬운 방법을 택했습니다.

 

 

public void init() throws SQLException {
        if (pieceDao.load() == null) {
            pieceDao.save(new Board().unwrap());
        }
        if (boardDao.load() == null) {
            final String name = chessGame.nowTurn().teamName();
            boardDao.save(new BoardDto(name, chessGame.isGameOver()));
        }
        final Map<Position, Piece> chessBoard = pieceDao.load();
        final BoardDto boardDto = boardDao.load();
        chessGame = new ChessGame(new Board(chessBoard), boardDto.team(), boardDto.isGameOver());
    }

    public Response move(final MoveRequest moveRequest) {
        try {
            chessGame.move(getPositionByCommands(moveRequest.source().split("")),
                getPositionByCommands(moveRequest.target().split("")));

            if (chessGame.isKingDead()) {
                chessGame.changeGameOver();
            }
            pieceDao.save(chessGame.board().unwrap());
            chessGame.nextTurn();
            boardDao.save(new BoardDto(chessGame.nowTurn().teamName(), false));
            return new Response("200", "성공");
        } catch (UnsupportedOperationException | IllegalArgumentException | SQLException e) {
            return new Response("401", e.getMessage());
        }
    }

    public Response end() throws SQLException {
        if (chessGame.isGameOver()) {
            boardDao.save(new BoardDto(chessGame.nowTurn().teamName(), true));
            return new Response("212", "게임 종료");
        }
        return new Response("200", "게임 진행중");
    }

    public void restart() throws SQLException {
        chessGame = new ChessGame();
        pieceDao.deleteAll();
        boardDao.deleteAll();
    }

 

 

init() 메소드는 새로 할당이니까 그렇다고 치고, restart() 메소드도 초기화 느낌이니까 다 지우는 것이 납득은 갑니다. 다만, move() 메소드는 이동할 때마다 현재 보드 64칸의 정보를 다시 다 Insert를 합니다. 이것때문인지는 모르겠는데, 체스말을 이동할 때마다 프론트 상에서 약간의 딜레이가 발생했습니다. 내일은 변동된 좌표만 수정하도록 코드를 바꿔봐야겠습니다.

 

 

정리

6시 이후에는 다음 주에 군대를 가는 친구를 만나기 위해 먼저 카페에서 나갔습니다. 저에게 주어진 시간이 3시간 남짓이라서 DB까지 연결하기는 어려웠을 줄 알았는데, 다행히 기능 구현은 할 수 있었습니다. 군대 간 친구랑도 즐거운 시간을 보냈고, 5단계 기능도 완수해서 나름 만족한 하루였다고 생각합니다.

댓글

추천 글