2단계까지 구현한 '체스말을 움직일 수 있는' 체스판에서,
King이 잡히면 게임을 종료하고,
그 전에 게임의 점수를 계산할 수 있는 기능을 만드는 것이 3단계였다.
👍 Controller 코드 이쁘게 정리하기
우선 나는 2단계까지 컨트롤러에서 명령을 받는 로직을 잘 구현하지 않았는데,
start, end, status는 명령을 쉽게 검증할 수 있지만 move b2 b4와 같은 명령은 명령어가 move인지를 먼저 검사한 후에 뒤에 따라오는 b2, b4와 같은 positions도 검증해야 하기 때문에 조금 어렵게 느껴졌다. 허브 호출!
쫌 잘한다고 소문이 좀 도는 허브를 한 번 데려와봤다.
명령어를 처음부터 List<String>의 타입으로 받으면 저장이 될테니 그렇게 바꾸면 된다는 것!
허브는 발생할 수 있는 상황에 대한 파악도 빠르고, 처음 보는 내 코드에 대한 이해도 굉장히 빨랐다.
또 배웠던 것은, 명령을 처리할 때 commandMapper를 만드는 법에 대해서도 배웠는데, 이 방법은 이전에 지토를 따라해서 이미 알고 있었지만 람다식을 더 편하게 사용하는 방법도 배운 것 같다. 처음에는 commandMapper의 value 타입을 Supplier 비스무리한걸로 했었는데, 나중에 좀 복잡하고 좀 더 이 value의 의미를 명확하게 하고 싶으면 함수형 인터페이스로 표현할 수 있다고 했다.
함수형 인터페이스 내에 아예 상수로 람다식을 넣어서 구현체를 미리 넣어놓는 방식도 신박했다. (물론 결과에 포함되지는 못했지만...) 또 람다식에 대해서 내가 테코톡 발표를 했었지만, 정말 유연하게 함수형 인터페이스를 화살표 섞인 한 줄로 구현해내는 것의 편리함은 이번에 처음 경험해본 것 같다.
public class ChessController {
private final Board board;
private final Map<ChessGameCommand, ChessGameAction> commandMapper;
public ChessController() {
this.board = new BoardFactory().createInitialBoard();
this.commandMapper = Map.of(
START, this::start,
MOVE, this::movePiece,
END, this::end
);
}
public void run() {
ChessGameCommand command = EMPTY;
while (command.isPlayable()) {
command = play();
}
}
private ChessGameCommand play() {
try {
List<String> commands = InputView.readCommand();
ChessGameCommand command = ChessGameCommand.from(commands.get(COMMAND_INDEX));
commandMapper.get(command).execute(commands);
return command;
} catch (RuntimeException exception) {
OutputView.printExceptionMessage(exception.getMessage());
return EMPTY;
}
}
private void start(final List<String> commands) {
validateCommandsSize(commands, DEFAULT_COMMAND_SIZE);
OutputView.printBoard(board.board());
}
private void movePiece(final List<String> commands) {
validateCommandsSize(commands, MOVE_COMMAND_SIZE);
Position from = searchPosition(commands.get(FROM_INDEX));
Position to = searchPosition(commands.get(TO_INDEX));
board.move(from, to);
OutputView.printBoard(board.board());
}
private void end(final List<String> strings) {
validateCommandsSize(strings, DEFAULT_COMMAND_SIZE);
}
private static void validateCommandsSize(final List<String> commands, final int moveCommandSize) {
if (commands.size() != moveCommandSize) {
throw new IllegalArgumentException("명령을 형식에 맞게 입력해 주세요!");
}
}
private Position searchPosition(final String command) {
int fromFile = command.charAt(0) - 'a' + 1;
int fromRank = command.charAt(1) - '0';
return new Position(fromFile, fromRank);
}
}
또 이전까지는 EMPTY와 같은 클래스를 잘 사용하지 않아서, 이것도 저것도 아닌 상태에 대해서 어떻게 해야 할지 감이 잘 오지 않아서 잘못된 입력에 대해 재귀를 많이 사용했었는데, 아무것도 아닌 상태를 종료 상태와 분리해서 지정하게 되면 아무것도 아닌 상태로 반복을 진행할 수 있다는 점에서 굉장히 유용했다.
public enum ChessGameCommand {
EMPTY,
START,
MOVE,
STATUS,
END,
;
public static final int COMMAND_INDEX = 0;
public static final int FROM_INDEX = 1;
public static final int TO_INDEX = 2;
public static final int DEFAULT_COMMAND_SIZE = 1;
public static final int MOVE_COMMAND_SIZE = 3;
public static ChessGameCommand from(String input) {
return Arrays.stream(values())
.filter(state -> state != EMPTY)
.filter(state -> state.name().equalsIgnoreCase(input))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("해당하는 명령어가 없음!"));
}
public boolean isPlayable() {
return this != END;
}
}
@FunctionalInterface
public interface ChessGameAction {
void execute(List<String> commands);
}
아래는 오늘 네오의 피드백 강의에서 했던 Q&A 내용 중 나에게 와닿는 질문과 대답만 정리해 봤당 ><
👍 객체 설계를 어떻게 할 것인가... 감이 잘 오지 않는다
객체 설계를 할 때, 아이디어가 더오르지 않고 구현하기 전까지 감이 오지 않는다면... 우선 구현을 해보면서 이상하더라도 어디가 이상한 지에 대해서 알게 되는 것을 통해서 아이디어를 얻을 수도 있다. 생각이 나지 않는다면 어쩔 수 없지만, 할 수 있는 것부터 계속 구현해보는 것이 방법인 것 같다. 엉망진창으로 만들더라도 만들어보면 도메인 지식이 늘어날 것이다. 미션은 엄청 잘 만드는 것이 목적이 아니라, 이 미션을 만들어서 배울 수 있다면 된다!
👍 단순히 호출만 하는 클래스를 어디까지 테스트해야 할까?
Piece를 관리하는 클래스 Pieces가 있다면, Piece만 테스트해야 할지 아니면 둘 다 테스트해야 할지 고민이다. Pieces의 코드는 현재 단순 호출만 하고 있지만, 언젠가 바뀔 수 있다. 중복해서 두는 것의 장점은 현재 단순 호출이더라도 이후에 내부 세부구현 변경에 문제가 있다는 것을 피드백 받을 수 있다는 장점이 있다.
하지만 단점도 있다. 테스트 코드를 통해 도메인에 대한 파악을 많이 하기도 하는데, 테스트 코드가 많아질 수록 점점 더 파악해야 하는 것이 많아진다. 또 Piece에 변화가 생겼을 때, 상위 객체에서도 변화가 일어나므로 지속적으로 신경을 써야 한다는 것이다.
적절히 장단점을 생각해서, 만드는 것이 좋은 것 같다.
네오는 그렇게 강박적으로 둘 다 만들지는 않는다고 했다.
👍 설계 시간과 깊이를 얼만큼 잡아야 할까?
네오는 설계하는 시간은 최소한으로 한다고 했다.
실제로 이번에 설계에 너무 공을 들여서 오래 했더니, 시간을 이미 오래 썼다는 생각에 문제가 발생해도 쉽게 고치기 어려웠고 그래서 데드라인을 맞추는 데에도 문제가 생겼었다.
👍 기본을 지키자
양질의 리뷰를 받으려면, 네이밍 컨벤션, 개행 등등 기본적인 것은 모두 지켜야 한다.
실제로 나는 클래스 변수와 인스턴스 변수 사이의 개행에 대한 피드백을 많이 받아왔다.
👍 양방향 의존성을 피해야 하는 이유..?
한 곳이 변경되면 다른 곳에서도 변경이 발생해 관리해야 할 부분이 많아져 유지보수에 좋지 않다.
의존성이 있다는 것은, 한 곳을 보았을 때 다른 쪽도 봐야 한다.
양방향의 문제점은 코드를 보았을 때 흐름을 따라가기 어려워 코드를 파악하기 어렵다는 것이다.
이렇게 복잡성이 높아지면 유지보수가 어려워지게 된다.
의존성에는 크게 런타임 의존성과 컴파일타임 의존성이 있다. 인터페이스를 사용해서 의존성을 끊었다고 생각해도, 런타임에서는 의존성이 있는 상태일 수도 있다. 양방향 의존성이 발생했다면 가능하다면 인터페이스를 활용해서 컴파일 타임 의존성이라도 줄이는 것이 좋다...! 패키지를 나누고, 레이어를 나누고 하는 많은 것들이 의존성을 줄이기 위한 노력에서 나온 것이다.
👍 상속은 왜 캡슐화를 깨는 걸까?
상속이 항상 캡슐화를 깬다고 생각하기보다는, 캡슐화를 깨뜨릴 확률이 높아진다고 생각하면 더 좋을 것 같다.
공통된 구현부가 필요할 때, 추상 클래스를 사용하고 구현부와 추상화된 부분을 나누어 사용해 왔다.
인터페이스의 default 메서드에 대해서 네오는 잘 사용하지 않는다고 한다. default 메서드가 들어가면서 인터페이스가 인터페이스가 아니게 되었다..... 자바에서 중요시하는 하위 호완성을 지키기 위해서 추가되었지만, default 메서드는 가능하면 쓰지 않는 것이 좋다.
상속을 받는 객체의 입장에서는 상속을 받는 상위 타입의 구현 메서드에 대해서 잘 알고 있기 때문에 캡슐화가 깨진다고 볼 수 있다. 구체 클래스에서 추상 클래스에 의존하는 것이 이상하기 때문에, 최대한 super() 메서드를 호출하는 것은 자제한다고 한다. 현업에서는 상속은 강결합이 되는 경향이 있어서 interface로는 안될 때 한 번 더 고민해 본다고 한다.
👍 나도 했던 설계에 대한 고민
우테코 사람들은 대부분 같은 내용으로 고민하는 것 같다.
이번 미션에서 우리 팀은 처음에 1번으로 진행을 했다.
장점은 Piece가 이동에 대한 책임을 가져서 좀 더 능동적인 체스말을 만들 수 있다는 것이었고,
단점은 Board에서 해당 위치에 어떤 말이 있는지 순회하면서 찾는 것이 조금 어려웠다는 것이다.
그래서 구현 도중에 2번 방법으로 바꾸게 되었는데, Piece에 능동적인 느낌이 많이 줄어들었지만 의존성을 완전히 끊어냈기 때문에 테스트 코드나 다른 모든 곳에서 Position을 새로 정의하지 않고도 Piece를 생성할 수 있게 되었다.
네오는 잘 재보고 해보라고 했다.
👍 우테코는 왜 else를 싫어할까?
else로 무언가를 처리해야 한다는 것은, 이미 그 메서드에서 두 가지 책임을 가지고 있는 것이다. 책임을 분배하라는 이유로 else를 사용하지 말라는 것이다.
https://prolog.techcourse.co.kr/studylogs/2942
https://nesoy.github.io/articles/2019-05/GRASP-Pattern
'우아한테크코스5기' 카테고리의 다른 글
[우테코] 레벨1을 끝내고 답해보자(1): 단위 테스트, 코드 품질 (0) | 2023.03.27 |
---|---|
[우테코] 블랙잭 미션 회고: 상태 패턴 사용, abstract/final 제어자, 중복되는 내용을 필드로 놓는 것에 대해.. 등등 (2) | 2023.03.24 |
[우테코] 체스 미션 1, 2단계 회고: 레벨1의 마지막 미션, 3인 페어 후기, 도메인의 예외 처리는 어디까지, TDD의 감잡기, 파라미터로 Optional을 받는 것은 안티패턴 등등... (11) | 2023.03.18 |
[우테코] 데이터베이스와 기초 SQL (0) | 2023.03.08 |
[우테코] 사다리 미션 2단계 회고: TDD, 원시값 포장, 객체의 책임... 너무 어려웡 (3) | 2023.02.27 |