우테코 프리코스 미션 #04 | Bridge Game

개요

미션 수행 github 링크

이번 프로젝트는 JS만으로 콘솔에서 아래와 같은 다리 건너기 게임을 구현하는 것이었다.

다리 건너기 게임을 시작합니다.

다리의 길이를 입력해주세요.
3

...

이동할 칸을 선택해주세요. (위: U, 아래: D)
D
[ O |   |   ]
[   | O | O ]

최종 게임 결과
[ O |   |   ]
[   | O | O ]

게임 성공 여부: 성공
총 시도한 횟수: 2

접근 방식

🔍 도메인 설계

img
도메인 설계

client 개발자가 보내는 메시지는 무엇일까의 관점에서 그 메시지를 현재 다리의 결과를 달라라고 생각했다.

currentResult()메시지를 처리할 객체를 현재 사용자의 위치와 정답 위치들을 알고있는 객체로 여겼고 이 객체에게 Bridge라는 이름을 붙여줬다.

자연스럽게 위치는 Position이 되었고 이 객체는 Bridge가 다른 Position과 동일한 위치인지 메시지를 보낼것이라고 판단했다.

최종적으로 해당결과에 대한 Result객체를 반환하도록 설계를 했다.


🔍 확장성있는 코드

지난 3주동안 객체지향적으로 코드를 작성하려 하면서 ‘절차 지향에 비해서 구조를 짜는데 복잡한데.. 이게 과연 변경하기 용이한 걸까?‘라는 생각을 했었다. 객체지향으로 작성한 코드가 확장과 유지 보수에 용이하다는 글들을 봤는데도 3주 동안 진행된 프로젝트에 새로운 기능이 추가되거나 기존의 기능이 변경되는 일이 없어서 그런지 이걸 느끼지는 못했던 것 같다.

그래서 이번 주차에는 ’기능 요구사항 외에 이 게임이 확장된다면..’ 이라는 생각으로 기능을 추가해보려다가 넓이가 3이상인 다리 게임이 가능해지는 것에 가능성을 열어두고 프로젝트를 진행하게 됐다.

//App.js
const bridgeGame = new BridgeGame(new ModeNormal());
bridgeGame.next();

최종적으로 위의 방식으로 코드가 실행될 수 있도록 작성했는데 strategy패턴을 적용해서 기존의 코드들은 수정 없이 새로운 Mode가 추가될 수 있도록 작성했다.

Refactoring

♻️ Strategy 패턴적용

게임에 Mode기능이 더해지기 위해서 기존에 작성한 Controller에서 넓이가 2인 경우에만 동작해야 하는 코드들을 분리해야 했다. 예를들어

...
this.#bridge = BridgeMaker.makeBridge(size, BridgeRandomNumberGenerator.generate);

this.#bridge.movePosition(new Position(positionSign));
...

위의 코드들은 모드에 따라서 다르게 동작해야한다. 그 이유는 0또는 1 랜덤 숫자를 만드는 generate메서드와 “UD”만으로 이루어진 결과를 반환하는 makeBridge는 이 경우만을 위해서 이기 때문이다.

또한 positionSign도 다리가 3개 이상인 경우 어떻게 될 지 모른다.

/** @interface */
class ModeStrategy {
	/**
	 * @param {string} positionSign
	 * @returns {Position} 
	 */
	createPosition(positionSign) {
	}
	/**
	 * @param {number} size
	 * @returns {Bridge} 
	 */
	createBridge(size) {}
}

위의 인터페이스를 통해 각 모드에 따라 Position을 만들어주는 메서드, Bridge를 만들어주는 메서드가 다르게 동작하도록 했다.

...
this.#bridge = this.#mode.createBridge(size);

this.#bridge.movePosition(this.#mode.createPosition(positionSign));
...

최종적으로 ModeStrategy에 해당 모드의 메서드를 호출하게 하니 확장성 있게 코드를 작성할 수 있었다. (HardMode가 추가되어도 파일 하나만 추가하면 된다)


♻️ State 패턴적용

게임을 크게 “시작”, “진행”, “재시도”, “끝” 4가지의 상태로 나눠 state패턴을 통해 구현했다. 기존의 코드와 차이점을 비교해보도록 하자.

class BridgeGame{
  //...
	move(positionSign) {
    this.#bridge.movePosition(positionSign);
		const curResult = this.#bridge.currentResult();
    if (curResult.isFailed()) { //(1) 재시도
      InputView.readGameCommand(this.retry.bind(this));
      return;
    }
    if (curResult.isCompelete()) { //(2) 끝
      OutputView.printResult(curResult.stringify(), this.#tryCount);
      return;
    }
    OutputView.printMap(curResult.stringify()); //진행
    InputView.readMoving(this.move.bind(this));
  }
  //...
}

위의 코드에서 (1), (2), (3) 부분을 보면 게임이 끝났는지 여부에 따라 동작하는 함수의 순서 및 종류가 달라지는 것을 확인할 수 있다.

이때 ‘현재 BridgeGame객체에 다음 상태객체를 넣어주고 next만 호출하면 되게 추상화해보자’ 라는 생각이 들었다.

class BridgeGame {
  //...
	move(positionSign) {
    this.#bridge.movePosition(this.#mode.createPosition(positionSign));
    const curResult = this.#bridge.currentResult();
		if (curResult.isFailed())
      this.#setState(new RetryState(this.retry.bind(this), this.next.bind(this)));
    if (curResult.isComplete())
      this.#setState(new EndState(curResult.stringify()));
    return curResult.stringify();
  }
  //...
}

뭔가 더 복잡해 보이기도 한다. 사실 이게 과연 좋은 방법이었을까? 에 대한 질문에 아직 답하지는 못했다.

하지만 여기서 각 분기에 대해 어떻게 처리할 지에 대한 것 보다 ‘어떤 상태가 되는지’에 대한 추상화가 된 것 같아서 만족하고 있다.

마치며

이번 주차에 특별하게 시도한 것이 있었다. 4시간 동안 내가 얼마나 할 수 있을지에 대해서 알아보는 것이었다.

4시간 타이머를 맞춰두고 가장 먼저 한 것은 요구사항들을 보며 내 체크 리스트를 작성해나가는 것이었다.

체크리스트를 작성하고 나서는 도메인 설계를 했다. 아이패드에 네모 박스들을 그리며 객체들끼리 주고 받는 메시지를 중점적으로 설계를 했다.

체크리스트를 작성하고 도메인 설계를 한 것까지 2시간 반이 걸렸다.

나머지 1시간 반 중에서 40분 동안 설계한 도메인들을 코드로 작성하고, 50분 동안 도메인들을 합성해 프로그램이 동작하도록 채워나갔다.

우선 4시간 동안 내가 예상했던 기능들을 모두 이행했다는 것에 놀랐고, 2주차 때 “완전한 상향식 개발은 없다”라고 단정 지었지만 그것을 실제로 경험해 봤다.

무작정 main 함수부터 작성하던 예전과는 달리 자연스럽게 내가 바라던 순서대로 개발을 진행했다. 눈에 띄는 성장을 확인할 수 있어서 너무 설렜고, 그만큼 더 열정이 생겼다.

아쉬운 점은 테스트코드를 작성하고 시작하지 않았다는 것… 테스트 작성하는 과정이 아직 익숙하지 않아 크게 자신감이 없는게 원인인 것 같다. 빠른 시일 내에 이것에 대해서 연습해봐야지..