방어 로직 세팅하기
체스 미션은 4단계까지 존재했다.
1단계에서는 체스 보드 생성, 2단계에서는 체스 말 이동 구현, 3단계에서는 점수 기능 구현 등, 각각의 단계마다 요구하는 게 달랐다.
나는 2단계까지 구현을 마친 시점에서 적팀 말을 잡는 기능은 현재 단계의 요구사항에서 벗어난다 생각해서 구현하지 않았다.
하지만 리뷰어분이 '구현되지 않았다면 적절한 방어 로직이 세팅되었는지'를 여쭤봐 주셨다.
생각해보니 내가 만든 어플리케이션은 불완전했다.
말을 잡는 기능을 구현하지도 않았는데, 이동 위치에 적팀 말이 있는 경우 덮어씌우는게 가능했다.
실제 서비스는 이렇지 않을 것이다.
분명 시간적 제약 등으로 중요한 기능만 먼저 딜리버리해야 하는 시점이 존재할 것이고,
딜리버리되지 않은 기능들은 사용자에게 오동작으로 보이지 않게끔 하는 것이 중요하다.
모든 기능을 제공하는 것이 아니더라도, 적어도 제공하는 모든 기능은 잘 작동하도록 보장하자.
애플리케이션의 최종 목적은 서비스 제공이니 말이다.
중요한 입력에 대해 무시하지 마라
앞서 말했던 '방어 로직 세팅하기'와 비슷한 내용이다.
애플리케이션이 사용자와 대화를 하는 방식은 UI를 통해서다.
즉, 입력을 통해서만 대화를 할 수 있다.
그만큼 입력은 중요하다.
하지만 내가 1,2 단계때 구현했던 체스 게임 어플리케이션은 이동 위치에 같은 팀 말이 있는 경우
아무런 예외 메세지도 출력하지 않고, 이동도 불가능했다.
이런 경우는 사용자가 어떤 상황이 발생한 것인지, 무엇을 해야 하는지 예측할 수 없다.
사용자에 입장에서는 '내부적으로 어떤 변화가 일어났을까?' 에 대한 궁금증을 가지게 될 것이다.
따라서 중요한 입력에 대해서도 무시하지 말자!
final화
이번 미션부터는 final을 애용했다.
모든 인자 및 확장 가능성이 없는 클래스에 final을 붙였다!
사실 final을 붙일 수 있는 곳이라면 어디든지 붙이는 게 대부분의 경우에서 좋다고 생각한다.
런타임에서의 유연성이든, 컴파일 타임에서의 유연성이든.. 너무 유연하면 오히려 독이 될 수 있다.
유연하다는 것은 시스템이 다른 문맥으로 변하기 쉽다는 것을 의미하니까 말이다!
물론 유연성이 꼭 필요한 경우라면 final 키워드를 사용하지 않을 수도 있겠지만, 그런 경우를 제외하고는 final이 중요하다고 생각한다.
아직까지는 코드 라인이 길어진다는 단점 외에는 특별한 단점을 찾지 못했다.
getter 사용 금지?
이 부분에 대해서는 따로 작성해 둔 아티클이 있기에 첨부한다!
getter는 금기가 아니다
getter 사용은 과연 금기인가 우아한테크코스에 합류하고 객체지향과 클린코드라는 개념을 차차 알아가는 중이다. 좋은 코드를 만들기 위해서는 getter / setter 사용을 자제하라고 한다. 따라서 나
dev-ws.tistory.com
테스트는 필요한 부분이라면 모두
내가 만든 체스 게임 어플리케이션에서 각 기물(말)들은 이동할 수 있는지의 여부를 기울기 + 거리로 판단한다.
private static final Direction DIRECTION = new Direction(List.of(
Inclination.POSITIVE_INFINITY, Inclination.ONE, Inclination.MINUS_ONE
));
즉, 각 기물들은 위와 같이 고유한 DIRECTION(방향)을 가지고 있다.
여기서 Inclination.POSITIVE_INFINITY는 기울기가 양의 무한대인 경우를 의미한다.
내부적으로는 아래와 같이 Double.POSITIVE_INFINITY 를 사용한다.
public enum Inclination {
POSITIVE_INFINITY(Double.POSITIVE_INFINITY),
...
}
하지만 Double.POSITIVE_INFINITY에 대한 테스트는 따로 존재하지 않았다.
이 값을 다뤄서 연산을 진행해도 될지에 대한 확신도 없는 상태로 어플리케이션을 만들었었다.
리뷰어님이 '무한대 값을 사용하면 잘 작동하나요?' 라고 질문해주셨고,
생각해보니 코드 어디에서도 이 값을 다뤄서 연산이 올바르다는 것에 대한 확신을 얻을 수 없었다.
따라서 나는 '테스트는 불확실한 부분에 대해 모두 존재해야 한다'는 생각을 키우게 되었다.
정상 생성 테스트
리뷰어님은 객체의 정상 생성 테스트도 존재해야 한다! 라는 말씀을 해주셨다.
생각해보니 객체는 생성 / 사용이 분리되는 것이 일반적이다.
여태까지 사용에 대한 테스트만 했었는데, 생성이 잘 된다는 보장이 어디에 있을까?
'그냥 잘 생성 되겠지, 생성자에는 별 다른 로직이 없으니까.' 라는 안일한 생각을 가졌었다.
생성자도 코드이므로 수정될 여지가 있다.
물론 생성자는 의존성만 결정하는 경우가 일반적이지만.. 가능성이 닫혀있는 건 아니다.
따라서 어떤 기능에 대한 테스트를 할 때 생성 테스트를 먼저 하는 것이 올바르겠구나, 라고 생각하는 계기가 되었다.
아래는 King 객체를 위해 작성했던 '정상 생성 테스트'이다.
@Test
@DisplayName("킹은 기본 상태를 가진다")
void propertyTest() {
King king = new King(Color.WHITE);
assertThat(king.canJump()).isFalse();
assertThat(king.isPawn()).isFalse();
assertThat(king.isKing()).isTrue();
assertThat(king.getPoint()).isEqualTo(0);
assertThat(king.getColor() == Color.WHITE).isTrue();
}
명령 - 쿼리 분리
여태까지는 일급컬렉션을 사용하는 이유를 잘 몰랐었다.
그저 컬렉션에 이름을 부여할 수 있다는 점, 접근을 제한할 수 있다는 점만 와닿았다.
이번에 체스 미션을 진행하면서 나는 체스 보드를 이중 리스트로 관리했었다.
하지만 기물들의 이동, 제약조건등과 같은 로직을 컬렉션과 같은 위치에서 관리하다 보니 필연적으로 객체의 책임이 방대해졌다.
명령 - 쿼리와 핵심 비즈니스 로직이 섞이니 이해하기 어려운 코드가 되었다.
이에 리뷰어분은 일급 컬렉션으로 따로 빼내는 게 어떻냐고 말씀해주셨고, 적용해보니 보다 깔끔한 코드가 되었다.
public final class Board {
private final Map<Coordinate, Piece> pieceLocations;
public Board(final Map<Coordinate, Piece> pieceLocations) {
this.pieceLocations = pieceLocations;
}
...
}
View에서 도메인 의존성 제거
사실 미션 자체는 뷰가 변화하지 않는 영역이고, 감춰야 할 데이터도 없다고 생각해
여태까지는 도메인의 정보를 View에 그대로 넘겨주었다. (사실 귀찮았던 게 크다.. ㅠ)
하지만 이런 부분은 문제가 맞다.
도메인 객체를 View에 그대로 전달하는 경우는 없을 것이다.
따라서 미루고 미루다가, 체스 미션에서야 도메인 객체를 View에서 의존하지 않도록 하는 방향으로 리팩토링했다.
View에는 순수 String 값만 넘겨주었고, Domain -> String 매핑 작업은 따로 컨트롤러 단에서 진행했다.
// 개선 전 OutputView
public void printBoard(final ChessGame chessGame) {
StringBuilder stringBuilder = new StringBuilder();
for (Rank rank : chessGame.getBoard().getRanks()) {
stringBuilder.insert(0, makeRank(rank));
}
System.out.println(stringBuilder);
}
...
// 개선 후 OutputView
public void printBoard(final String parsedBoard) {
System.out.println(parsedBoard);
}
'우테코 5기' 카테고리의 다른 글
우아한테크코스 레벨1 회고 (6) | 2023.04.06 |
---|---|
[레벨 1 미션] 체스 학습 기록(2) (2) | 2023.04.06 |
[레벨 1 미션] 블랙잭 게임 학습 기록 (2) | 2023.04.04 |
레벨 1 레벨로그 (1) | 2023.03.30 |
[레벨 1 미션] 체스 미션 회고 (1) | 2023.03.29 |