티스토리 뷰
1) 필드의 수를 줄이고 private 필드로 구현하기
이번 다리 건너기 미션에서 class 필드를 이용했다. 하지만 단순히 이용하는 것이 아니라 필드의 수를 최대한 줄이고 private 필드를 이용했다. 필드의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 또한 public 필드는 class 외부에서 읽히고 수정될 수 있기 때문에 외부에서 적절치 못한 접근으로부터 객체의 상태가 변경되어 버그가 발생할 확률을 높일 수 있다. 하지만 private 필드는 소속된 class에 고유한 스코프를 갖으며 '정보 은닉'을 한다. 정보 은닉의 장점은 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보를 보호하고, 객체 간의 상호 의존성, 즉 결합도를 낮추는 효과가 있다. 이런 장점을 위해 다리 건너기 미션에서 private 필드를 최대한 활용해보고 배웠다.
private class 필드는 # prefix를 추가해 아래와 같이 선언할 수 있다.
class BridgeGame {
#moveCount;
}
private 필드는 소속된 class에 고유한 스코프를 갖기 때문에 외부에서 접근이 불가능하다. 아래 코드에서 클래스 외부에서 moveCount에 접근했을 때 undefined가 출력된다. 이것은 외부에서 객체의 상태에 접근을 못하게 함으로써 의도치 않은 상태 변경을 막으며, 객체 간의 상호 의존성, 결합도를 낮추는 효과를 가지고 있다.
class BridgeGame {
#moveCount;
constructor(){
this.#moveCount = 1;
}
}
const bridgeGame = new BridgeGame();
console.log(bridgeGame.moveCount) // undefined
하지만 public 필드는 외부에서 접근이 가능하다. 이것은 외부에서 의도치 않은 접근으로부터 객체의 상태가 변경될 수 있어서 버그가 발생할 확률을 높여준다. (참고: MDN Private class features)
class BridgeGame {
moveCount;
constructor(){
this.moveCount = 1;
}
}
const bridgeGame = new BridgeGame();
console.log(bridgeGame.moveCount) // 1
2) 상태를 가지고 있는 class 내에서 일하도록 하기
저번 로또 미션의 Model 클래스에서 setter와 getter 메서드만 사용하여 객체를 제대로 활용하지 못한 것이 아쉬웠다. 상태를 활용한 로직은 하나도 없이 setter와 getter 메서드만을 이용해 외부에서 상태에 접근하여 상태 값을 변경하고, 상태 값을 받아와 class 밖에서 로직을 처리했다.
/* 3주차 로또 미션의 User 클래스 */
class User {
constructor() {
this.money = 0;
this.lottoNumbers = [];
}
getMoney() {
return this.money;
}
getLottoNumbers() {
return this.lottoNumbers;
}
setMoney(money) {
this.validateMoney(money);
this.money = Number(money);
}
}
그래서 이번 다리 건너기 미션에서는 객체가 제대로 된 역할을 할 수 있도록 상태 데이터를 꺼내 로직을 처리하지 않고, class 내에서 처리하도록 했다. BridgeGame 클래스에서 bridge, moveCount 상태를 정의하고, 2개의 상태에 관한 로직을 BridgeGame 클래스 내에서 처리하도록 구현하며 객체의 역할에 대해 고민하며 배울 수 있었다.
/* 4주차 다리건너기 미션의 User 클래스 */
class User {
#bridge;
#moveCount;
constructor(bridge, moveCount) {
this.#bridge = bridge;
this.#moveCount = moveCount;
}
isMove(direction) {
return direction === this.#bridge[this.#moveCount];
}
isCompletion() {
return this.#bridge.length === this.#moveCount;
}
move() {
this.#moveCount += 1;
}
retry() {
this.#moveCount = 0;
}
convertBridge(floor) {
return this.#bridge.slice(0, this.#moveCount).map((current) => {
if (current === floor) {
return GAME_RULE_SUCCESS;
}
return GAME_RULE_BLANK;
})
}
getConvertBridge() {
const upsideBridge = this.convertBridge(COMMAND.UPSIDE);
const downsideBridge = this.convertBridge(COMMAND.DOWNSIDE);
return { upsideBridge, downsideBridge }
}
}
3) 테스트하기 좋은 코드로 작성하기
이전 미션까지 단지 테스트 코드를 작성하는 것만 생각했다. 하지만 이번 다리 건너리 미션에서 공통 피드백을 받아 테스트하기 쉬운 코드에 대해 고민해보고 활용했다. 피드백을 참고하기 전에는 BridgeGame class 생성자 안에서 랜덤 값을 이용하는 BridgeMaker객체의 makeBridge 메서드를 사용했다. 그래서 테스트 코드를 작성할 때 BridgeGame class 생성자 내에서 무슨 값이 나올지 예측할 수 없어서 Bridge class의 테스트 코드 작성하는데 어려움을 겪었다.
하지만 테스트하기 쉬운 코드를 위해 BridgeGame class 생성자 내부의 함수를 외부로 분리했다. App 클래스에서 BridgeMaker객체의 makeBridge 메서드를 사용하여 랜덤 값을 Bridge class 생성자의 매개변수로 전달했다. 매개변수는 테스트 코드를 작성할 때 컨트롤할 수 있는 부분이기에 덕분에 BridgeGame class를 테스트할 수 있었다. 하지만 랜덤 값의 의존하는 class를 BridgeGame에서 App으로 바꾼 것이기 때문에 여전히 랜덤 값에 의존하는 문제는 남아있었다. 테스트하기 좋은 코드를 위해 어떻게 구조를 설계할지 조금 더 고민할 부분인 것 같다.
아래의 코드는 테스트하기 어려운 코드이다. 먼저 new BridgeGame을 이용하여 객체를 생성했다. 그 후 BridgeGame의 생성자 내부에서 BridgeMaker.makeBridge 메서드를 호출하여 랜덤 값을 받아와 this.#bridge에 할당했다.
/* App.js */
makeGame(length) {
this.#bridgeGame = new BridgeGame(length)
}
여기서 문제는 랜덤 값 때문에 BridgeGame의 bridge의 값을 예측할 수 없어서 테스트 코드를 작성하기 어려워진다.
/* BridgeGame.js */
class BridgeGame {
#bridge;
constructor(length) {
this.#bridge = BridgeMaker.makeBridge(Number(length), BridgeRandomNumberGenerator.generate)
}
}
그래서 테스트하기 쉬운 코드를 위해 랜덤 값에 의존하는 BridgeMaker.makeBridge 메서드를 클래스의 외부(App 클래스)로 분리하는 시도를 했다.
/* App.js */
makeGame(length) {
this.#bridge = BridgeMaker.makeBridge(Number(length), BridgeRandomNumberGenerator.generate)
this.#bridgeGame = new BridgeGame(bridge)
}
/* BridgeGame.js */
class BridgeGame {
#bridge;
constructor(bridge) {
this.#bridge = bridge
}
}
테스트 코드를 작성할 때 매개변수는 컨트롤할 수 있기 때문에 BridgeGame class를 아래와 같이 쉽게 테스트할 수 있다.
describe('BridgeGame 클래스 테스트', () => {
test('BridgeGame 객체를 만들고, 다리의 상태를 초기화한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'D', 'U']);
expect(bridgeGame.getBridge()).toEqual(['U', 'D', 'D', 'U'])
})
})
전체적인 테스트코드는 아래와 같다.
/* BridgeGameTest.js */
const makeBridgeGame = (bridge, moveCount = 0, retryCount = 1) =>
new BridgeGame(bridge, moveCount, retryCount);
describe('BridgeGame 클래스 테스트', () => {
test('BridgeGame 객체를 만들고, 다리, 이동 횟수, 재시도 횟수를 초기화 한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'D', 'U']);
expect(bridgeGame.getBridge()).toEqual(['U', 'D', 'D', 'U']);
expect(bridgeGame.getMoveCount()).toEqual(0);
expect(bridgeGame.getRetryCount()).toEqual(1);
});
test('처음에 이동할 수 있는 다리인지 확인한다.', () => {
const bridgeGame = makeBridgeGame(['U']);
const direction = 'U';
expect(bridgeGame.isMove(direction)).toBeTruthy();
});
test('다리를 건널 때 이동 횟수가 1씩 증가한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'U', 'D']);
const moveCount = [1, 2, 3, 4];
moveCount.forEach((count) => {
bridgeGame.move();
expect(bridgeGame.getMoveCount()).toEqual(count);
});
});
test('매번 이동할 수 있는 다리인지 확인한다.', () => {
const bridgeGame = makeBridgeGame(['D', 'D', 'U']);
expect(bridgeGame.isMove('D')).toBeTruthy();
bridgeGame.move();
expect(bridgeGame.isMove('D')).toBeTruthy();
bridgeGame.move();
expect(bridgeGame.isMove('D')).toBeFalsy();
});
test('현재 이동까지 성공한 위쪽 다리 모양을 변환한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'U'], 2);
const upsideBridge = bridgeGame.convertBridge('U');
expect(upsideBridge).toEqual(['O', ' ']);
});
test('현재 이동까지 성공한 아래쪽 다리 모양을 변환한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'U'], 2);
const downsideBridge = bridgeGame.convertBridge('D');
expect(downsideBridge).toEqual([' ', 'O']);
});
test('현재 이동까지 성공한 위,아래 다리 모양을 변환하여 한 번에 받아온다.', () => {
const bridgeGame = makeBridgeGame(['D', 'U', 'U', 'D'], 3);
const { upsideBridge, downsideBridge } = bridgeGame.getConvertedBridge();
expect(upsideBridge).toEqual([' ', 'O', 'O']);
expect(downsideBridge).toEqual(['O', ' ', ' ']);
});
test('이동 실패한 다리에 X로 표시한다.', () => {
const bridgeGame = makeBridgeGame(['D', 'U', 'D'], 2);
const upsideBridge = [' ', 'O'];
const downsideBridge = ['O', ' '];
const failBridge = bridgeGame.getFailureBridge({ upsideBridge, downsideBridge });
expect(failBridge.upsideBridge).toEqual([' ', 'O', 'X']);
expect(failBridge.downsideBridge).toEqual(['O', ' ', ' ']);
});
test('다리를 모두 건넜는지 확인한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'U']);
bridgeGame.move();
expect(bridgeGame.isCompletion()).toBeFalsy();
bridgeGame.move();
expect(bridgeGame.isCompletion()).toBeTruthy();
});
test('게임 재시작 시 이동 횟수를 초기화한다.', () => {
const bridgeGame = makeBridgeGame(['D', 'D', 'U'], 3);
bridgeGame.retry();
expect(bridgeGame.getMoveCount()).toEqual(0);
});
test('게임 재시작 시 시도 횟수가 1씩 증가한다.', () => {
const bridgeGame = makeBridgeGame(['U', 'D', 'U']);
const retryCount = [2, 3, 4, 5];
retryCount.forEach((count) => {
bridgeGame.retry();
expect(bridgeGame.getRetryCount()).toEqual(count);
});
});
});
[다리 건너기] 박근우 미션 제출합니다. by geunu97 · Pull Request #45 · woowacourse-precourse/javascript-bridge
📌 학습 목표 상태를 가지고 있는 class 내에서 일하도록 하기 필드의 수를 줄이고 private 필드로 구현하기 테스트하기 좋은 코드로 작성하기 🎯 과제를 통해 배운점 상태를 가지고 있는 class 내
github.com
'회고' 카테고리의 다른 글
[우아한테크코스 5기 FE] 2주차 프리코스 회고 (0) | 2022.11.08 |
---|
- Total
- Today
- Yesterday