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

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

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

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

 

오늘은 제이슨 조에서의 첫 데일리 미팅, 영상 근로에서의 OT 등 재미난 이야기도 있지만, 시간 관계상 검프와의 페어 프로그래밍을 위주로 회고를 남겨야겠습니다.

 

 

스프링 입문 - 체스 미션 페어 프로그래밍

오늘은 어제 합의한 부분을 바탕으로 체스 도메인을 구현하였습니다. 물론, 아직 구현을 못한 부분이 남아있지만, 제가 이전에 짠 코드보다 한층 업그레이드되었다는 느낌을 받았습니다. 그리고 그 과정에서 새로운 인사이트를 얻은 것이 많았습니다. 기억나는 대로 소개해 보겠습니다.

 

 

    public static Optional<File> from(final String symbol) {
        return Arrays.stream(values())
                .filter(file -> file.symbol.equals(symbol))
                .findFirst();
    }

 

 

첫 번째는 특정 객체 내에서 예외 처리하지 않고, Optional로 반환하여 외부에서 예외 처리하는 것입니다. 기존에는 findFirst() 이후에 orElseThrow()를 통해 예외 처리를 하였지만, 검프의 의견에 따라 이를 Optional로 수정하였습니다. 그 이유는 File 내에서 orElseThrow()를 하게 되면, 특정 예외를 정해놓는 것이지만, Optional을 반환하면 외부에서 자유롭게 예외를 설정해 줄 수 있기 때문입니다. 꽤 흥미로운 주장이어서 바로 적용하였죠.

 

 

두 번째는 Source와 Target을 포장하는 것입니다. 저는 기존의 Position 타입인 Target과 Target 위치에 해당하는 체스말을 인자로 받아서 로직을 수행하는 메소드가 있었는데, 이 Source와 Target을 포장함으로써 해당 메소드의 책임이 분산되었습니다.

 

 

    @Override
    public boolean canMove(final Target target) {
        return isPossibleDirection(target) && (isOpponent(target) || target.isBlank());
    }

    private boolean isPossibleDirection(final Target target) {
        final List<Integer> result = target.subtract(new Source(this));
        return moveStrategies.strategies().stream()
            .anyMatch(moveStrategy -> moveStrategy.isSameDirection(result.get(0), result.get(1)));
    }

 

 

특히 가장 인상깊었던 부분은 체스말을 Source로 판단할 수 있으므로 Piece 내의 canMove 메소드의 인자를 단 하나로 줄일 수 있게 된 점입니다.

 

 

세 번째로 이동 전략을 enum이 아닌 클래스화한 점입니다. 어제 예고한 대로 체스말의 이동 전략을 검프의 의견을 수용하여 클래스화 하였습니다.

 

 

 

 

굉장히 많긴한데 어차피 검프가 과거에 노가다 잘 뛰어준거라서 재사용하기 편했습니다 ㅎㅎ

 

 

public class MoveStrategies {
    private final List<MoveStrategy> moveStrategies;

    public MoveStrategies(final List<MoveStrategy> moveStrategies) {
        this.moveStrategies = new ArrayList<>(moveStrategies);
    }

    public static MoveStrategies everyMoveStrategies() {
        return new MoveStrategies(Arrays.asList(new East(), new West(), new South(), new North(), new Northwest(), new Northeast(), new Southeast(), new Southwest()));
    }

    public static MoveStrategies knightMoveStrategies() {
        return new MoveStrategies(Arrays.asList(new UpLeft(), new UpRight(), new LeftUp(), new LeftDown(), new DownLeft(), new DownRight(), new RightUp(), new RightDown()));
    }

    public static MoveStrategies diagonalMoveStrategies() {
        return new MoveStrategies(Arrays.asList(new Northwest(), new Northeast(), new Southeast(), new Southwest()));
    }

    public static MoveStrategies orthogonalMoveStrategies() {
        return new MoveStrategies(Arrays.asList(new East(), new West(), new South(), new North()));
    }

    public static MoveStrategies pawnMoveStrategies() {
        return new MoveStrategies(Arrays.asList(new North(), new Northeast(), new Northwest(), new InitialPawnNorth()));
    }

    public List<MoveStrategy> strategies() {
        return Collections.unmodifiableList(moveStrategies);
    }
}

 

 

이러한 방식의 가장 큰 장점은 각 체스말이 갖는 이동 전략을 깔끔하게 정리해 줄 수 있다는 것입니다. 상하좌우, 나이트, 대각선 등등 이들을 반환하면 체스말이 적절한 상황에서 이동 전략을 받을 수 있습니다.

 

 

    @Override
    public boolean isSameDirection(final int fileDegree, final int rankDegree) {
        return this.fileDegree == fileDegree && this.rankDegree == rankDegree;
    }

 

 

또한, 각 이동 전략의 필드가 fileDegree와 rankDegree로 되어 있고, 이동 전략 하나 하나가 MoveStrategy 인스턴스이므로 fileDegree와 rankDegree를 이용하여 손쉽게 특정 방향이 같은지 판단할 수 있습니다.

 

 

마지막으로 move를 할 때마다 체스말을 새로 할당함으로써 체스말의 불변성을 보장했다는 것입니다.

 

 

    @Override
    public Piece move(final Target target) {
        return new Queen(this.color(), target.getPiece().position());
    }

 

 

이런 식으로 특정 체스말이 이동하면 단순히 해당 체스말의 위치를 바꾸는 것이 아니라, 새로운 객체를 반환하였습니다. 그리고 실질적으로 체스말을 관리하는 일급 컬렉션을 만들어서 불변을 보장했습니다.

 

 

public class Pieces {
    private final List<Piece> pieces;

    public Pieces(final List<Piece> pieces) {
        this.pieces = new ArrayList<>(pieces);
    }

    public List<Piece> unwrap() {
        return Collections.unmodifiableList(pieces);
    }

    public boolean canMove(final Source source, final Target target) {
        return source.canMove(target);
    }

    public void move(final Source source, final Target target) {
        Piece movedPiece = source.move(target);
        remove(source.getPiece().position());
        pieces.add(movedPiece);
    }

    public Optional<Piece> findPiece(final Position position) {
        return this.pieces.stream()
                .filter(piece -> piece.isSamePosition(position))
                .findAny();
    }

    public void remove(final Position position) {
        if (findPiece(position).isPresent()) {
            this.pieces.remove(findPiece(position).get());
        }
    }

    public List<Piece> pieces() {
        return Collections.unmodifiableList(pieces);
    }

    public boolean isBlackPieces() {
        return pieces.stream()
                .allMatch(Piece::isBlack);
    }

    public boolean isKingPosition(final Position position) {
        if (findPiece(position).isPresent()) {
            return findPiece(position).get().isKing();
        }
        return false;
    }
}

 

 

move() 메소드를 보면, 원래 source 위치였던 객체를 지우고 target 위치로 바뀐 객체를 새롭게 pieces 리스트에 넣는 것을 알 수 있습니다. 물론, 이 부분도 추후에 pieces 자체도 재할당하여 불변을 보장할 계획입니다.

 

이 외에도 인사이트를 얻은 내용이 있었지만, 워낙에 활동한 것이 많고 정신없이 스프링을 공부하는 바람에 잊어버린 부분도 상당수 있는 것이 아쉽습니다.

 

 

정리

오늘 오프라인으로 활동하면서 브라운 코치님이나 다른 크루들이 저에게 우려의 말씀을 많이 해 주셨습니다. 특히, 이번 미션은 스프링이 중점이니까 너무 체스 코드를 합치는 과정에 집중하지 말라는 식이었죠. 물론, 어느 부분 동의하지만, 저는 코드의 합의를 보지 않고 바로 남의 코드를 쓰게 되면 나중에 디버그할 때 문제가 생길 수 있다고 생각합니다. 또한, 여기까지 와서 코드를 합의하는 과정을 건너뛰기도 너무 아쉽습니다. 따라서, 이번 미션은 검프와 이야기한대로 제 소신껏 진행하려고 합니다.

댓글

추천 글