회고/우아한테크코스

나는 체스 미션을 통해 무엇을 얻었을까

 

상태패턴

2022.03.27 - [회고/우아한테크코스] - 나는 블랙잭 미션을 통해 무엇을 얻었을까

 

지난번 블랙잭 미션에서 네오의 상태패턴 강의를 듣고 다음 미션에 꼭 적용해야겠다고 생각했습니다.

 

체스미션에서는 기물마다 행마법이 달라야 합니다.

그리고 기물이 한번 움직였는지 아닌지에 따라서도 행마법이 다릅니다.

 

룩, 비숍, 나이트, 퀸, 킹, 폰 은 "기물"이라는 객체이지만 체스판 위에서 움직일 수 있는 행마법이 다릅니다.

게다가 폰은 한번도 움직이지 않았을 때만 앞으로 2칸을 움직일 수 있고, 한번이라도 움직였으면 1칸만 움직일 수 있습니다.

 

그래서 저는 기물들이 상태를 갖고있고 상태에다가 어디로 갈 수 있는지 물어보면 되겠다고 생각했습니다.

네, 현재 상태에 메서드를 요청하고 객체가 다음 상태를 갖도록 하는 것이 상태패턴이죠.

 

재등장한 오토마타

 

 

하지만 한번도 상태패턴을 적용한 개발을 해본 적이 없었으니 설계하는 단계에서 페어와 오래 고민했습니다.

상태패턴 설계에 대한 고민만 10시간정도 한 것 같네요.

 

그러다가 깨달았습니다.

"처음부터 완벽한 계층설계는 못한다. 필요한 계층만 짜놓고 구현중에 같은 특성들이 보이면 추상화 하자"

일단 현재 알고있는 지식상에서 설계를 하고 페어와 상의하며 구체화 했습니다.

설계대로 구현하며 이상한 부분이 있으면 계속 설계를 고쳐나갔습니다.

 

다시 생각해보면 처음 설계할땐 당연히 실패하는 것 같습니다.

실패를 해봐야 다음번엔 이대로하면 어떤 문제가 생길지 알게됩니다.

 

그렇게 설계를 고쳐나가 최종 설계가 완성되었습니다. (물론 이것도 피드백 받고 리팩토링하며 약간 변했습니다.)

최종인듯 최종아닌 설계

 

 

Piece 다이어그램
Piece는 PieceState를 필드로 갖고있다.

 

Piece.class 에서

아래와같이 상태 (PieceState state) 에 findMovablePositions() 메서드를 요청합니다.

    public List<Position> findMovablePositions(Position source, Map<Position, Piece> board) {
        return state.findMovablePositions(source, board);
    }

 

그러면 아래와 같이 PieceState를 Implements하는 구현체들에서 각자의 findMovablePositions()를 구현하면 됩니다.

PieceState 다이어그램

 

예시) PieceStated -> Started -> StartedPawn

public class StartedPawn extends Started {

    private final Direction forward;

    public StartedPawn(Direction forward) {
        this.forward = forward;
    }

    @Override
    public List<Position> findMovablePositions(Position source, Map<Position, Piece> board) {
        List<Position> positions = new ArrayList<>();
        Position next = source.findNext(forward);

        if (canOnlyMoveByOneStep(board, source, forward)) {
            positions.add(next);
        }

        if (canOnlyMoveByTwoStep(source, board, forward)) {
            positions.add(next.findNext(forward));
        }

        List<Position> killablePositions = getKillablePositions(source, board);
        positions.addAll(killablePositions);
        return positions;
    }

 

 

결론적으로, if문으로

1. 어떤 기물인지

2. 해당 기물이 한번 움직였는지

등을 if문으로 구분하지 않아도 각 Piece의 상태를 메서드 한번의 호출로 다르게 가져올 수 있게 되었습니다.

 

ChessBoard.class에서

piece.findMovablePositions(source, board); 만 부르면 됩니다.

그러면 해당 기물의 상태에 따라 다른 행마법이 반환 됩니다.

    private boolean canMove(Position source, Position target) {
        Piece piece = getPiece(source);
        List<Position> movablePositions = piece.findMovablePositions(source, board);

        return movablePositions.contains(target);
    }

 

 

 

 

양방향참조

두 객체가 서로의 참조값을 알고있었습니다.

 

"객체에 메세지를 보내라" 라는 요구사항에 너무 매몰되어있던 나머지 이런 문제가 발생한 것 같습니다.

 ChessBoard에서 Piece의 메서드를 사용합니다.

그런데 ChessBoard에서 사용하는 Piece의 메서드에서 ChessBoard의 메서드를 사용하기 위해 자신의 참조를 보내고 있습니다.

 

"piece에서 board를 다루려면 무조건 ChessBoard에 메세지를 보내야지"라고 생각한 나머지 이런 코드가 발생한 것입니다.

 

양방향 참조를 하게되면 서로의 메서드를 계속해서 호출할 수 있는 문제가 발생합니다.

 

ChessBoard에서 Piece의 메서드를 사용해야하고, Piece에서는 ChessBoard의 메서드를 사용해야하는데, 어떻게 수정을 해야할지 감이 잡히지 않았습니다.

 

그래서 한참을 고민하다 리뷰어분께 질문을 남겼습니다.

 

단지 Map<Position, Piece> board 를 다루는 것 뿐인데 굳이 ChessBoard 에서만 메서드를 실행할 필요는 없었습니다.

또한, Piece에서만 사용하는 ChessBoard의 메서드가 존재했습니다. Piece를 위해서만 존재하는 ChessBoard의 메서드는 리팩토링할 필요가 있는 코드라고 확신했습니다.

 

Piece에 Map 정보를 넘겨 Piece가 Map의 정보를 통해 판단할 수 있도록 Piece에 메서드를 구현했습니다.

이로인해 PieceState를 상속받는 여러 클래스들에서 발생하는 board관련 메서드들을 공통적으로 묶을 필요가 있었습니다.

저는 PieceState에서 board를 다루는 메서드들을 Started에 구현했고, Continuous, NonContinous를 추상메서드로 바꾸어 Started를 상속받도록 했습니다.

 

수정 전 : this로 자신의 참조를 보내고있음 (board 관련 메소드를 사용하기 위해)

    private boolean canMove(Position source, Position target) {
        Piece piece = getPiece(source);
        List<Position> movablePositions = piece.findMovablePositions(source, this);

        return movablePositions.contains(target);
    }

 

수정 후 : board (Map<Position, Piece>) 를 보내고있음 (board 관련 메서드를 Started에 구현)

    private boolean canMove(Position source, Position target) {
        Piece piece = getPiece(source);
        List<Position> movablePositions = piece.findMovablePositions(source, board);

        return movablePositions.contains(target);
    }

 

 

 

실제 코드를 확인하고싶으시다면?

https://github.com/dongho108/java-chess