티스토리 뷰
강의 내용
이번 강의는 테트리스 게임로직을 만들거라는 생각과 달리 처음부터 다시 구현한다. 당연히 시간이 부족하기 때문에 이전 강의에서 설명했던 것들은 메소드명만 적거나 대충 넘어가고 개선할부분을 알려준다. 그리고 디자인패턴도 조금씩 얘기한다.
템플릿 메소드 패턴
템플릿 메소드 패턴이란 특정 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꾸는 패턴이다. 쉽게 말해서 추상클래스를 만들고 상속받은 하위 클래스에서 오버라이드를 하면 된다. 그런데 자바스크립트에서는 이걸 언어적으로 지원하지 않는다. 추상클래스는 딱히 만들방법이 없고(?) 오버라이드는 자식메소드앞에 _를 붙여서 구현하기도 한다. 그런데 이는 명시적이지 않다. 언어에서 지원을 하지 않는다면 지원하게 만들면 된다.
const ERR = (v) => {
throw v;
};
const OVERRIDE = (parent, method) => (typeof parent.prototype[method] === 'function' ? method : ERR());
const TMPL = (self, method, ...arg) => ('_' + method in self ? self['_' + method](...arg) : ERR());
const Subdata = class {
clear() {
TMPL(this, 'clear')();
}
};
const Stage = class extends SubData {
[HOOK(Subdata, 'clear')]() {
this.stage = 0;
this.isNext();
}
};
위와 같이 함수 2개만 만들어놓으면 템플릿메소드 페턴을 사용한다는 것을 명시적으로 알 수 있다. 이런 귀찮은 방법으로 코드를 작성하는 이유는 이 코드는 매일매일 보는 코드가 아니기 때문이다. 이 코드뿐만 아니라 어떤 코드라도 매일 매일 볼 수 는 없다. 미래의 나는 과거의 내가 무슨 생각을 했는지 알지 못한다. 코드로 말해야한다. 주석도 좋은 방법이지만 가장 좋은 방법은 코드로 말하는것이다. 사실 컴포넌트를 자바스크립트로 구현할때도 오버라이드를 했지만 위와같은 방법은 생각하지 못했다. 타입스크립트가 있으니까 자바스크립트로 어떻게 하는지 몰라도 돼
는 조금 위험한 생각인것같다. 얼마든지 구현하려면 구현할 수 있다.(타입스크립트는 abstract이 존재하기 때문에 실제 코딩을 할때는 위와같이 함수를 만들필요는 없다. 자바처럼 바로 템플릿메소드패턴을 적용할 수 있다.)
전략 패턴
전략 패턴은 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴이다.
보통 오리로 예시를 많이 든다. 나도 오리로 예시를 들어보겠다. 오리라는 super class를 만들고 여러가지 오리 클래스를 만든다고 생각해보자. 공통되는 상태, 메서드들은 상위 클래스에서 정의하고 상속받아 각각의 속성을 추가하고 특정 클래스에는 override도 하는 일반적인 형태다. 그런데 몇가지 문제가 발생한다.
- 대부분의 오리에 대해 fly 함수 구현이 필요해짐
- 앞으로 6개월마다 제품을 갱신하기로 함
- fly의 공통적인 동작을 조금 바꾸기 위해서는 모든 subclass에서 코드를 바꿔야 함
이런 경우에 어떻게 코드를 작성해야할까?? 대부분의 오리에 대해 fly 함수 구현이 필요하니 추상클래스에서 만들어버리면 일부 하위클래스에서 오버라이드를 해야하고 제품을 갱신하기 때문에 하위 클래스를 일일히 살펴봐야한다. 우리가 코딩 공부를 하면서 가장 먼저 배우지만 실무에 혹은 토이 프로젝트에 적용하지 못하는것이 있다. 바로 중복의 제거다. 음... 지금 상속받다가 문제가 생긴건지 무슨소리지??
라고 생각할수도 있다. 중복을 상황에 따라 적절하게 제거해야 한다. 그렇다면 중복을 제거해야하는 이유는 무엇일까?? 단순히 코드량을 줄이기 위해서? 아니다. 캡슐화다. 캡슐화를 하지 않으면 같은 작업을 몇번씩 반복해야하고 이는 유지보수하는 과정에서 버그를 유발한다. 테스트코드가 있다면 사전에 발견할 수 있지만 가장 좋은건 테스트하지 않아도 버그가 발생하기 어려운 환경을 만드는 것이다.
그리고 독립적인 환경을 제공해야한다. 지금은 하나의 클래스를 상속받는 형태이기 때문에 너무 의존적이다. 상속이 잘못됐다고 말하는것은 아니다. 다만 지금 경우에는 조금 더 독립적이여야한다는 것이다.
이럴때 전략패턴을 사용한다. A는 B이다
보다 A에는 B가 있다
가 더 좋은 경우가 있다.
아래는 스터디하는 팀원이 작성한 전략패턴 예시 코드이다.
class QuackQuack {
do() {
this.복잡한함수1();
this.복잡한함수2();
console.log('꽥꽥꽥');
}
복잡한함수1() {}
복잡한함수2() {}
}
class QuackQuack2 {
do() {
this.복잡한함수1();
console.log('꽥꽥꽥꽥');
}
복잡한함수1() {}
}
class NoFly {
do() {
console.log('난 못날아');
}
}
class DuckFactory {
static getDuck(type) {
switch (type) {
case '청둥오리':
return new Duck(new QuackQuack(), new NoFly());
case '오리종류1':
return new Duck(new QuackQuack2(), new Fly1());
case '오리종류2':
return new Duck(new QuackQuack2(), new Fly2());
case '오리종류3':
return new Duck(new QuackQuack2(), new Fly3());
case '오리종류4':
return new Duck(new QuackQuack2(), new Fly4());
case '오리종류5':
return new Duck(new QuackQuack2(), new Fly5());
}
}
}
class Duck {
quack;
fly;
constructor(quack, fly) {
this.quack = quack;
this.fly = fly;
}
quack() {
this.quack.do();
}
fly() {
this.fly.do();
}
}
class 청둥오리 extends Duck {}
function run() {
const 청둥오리 = DuckFactory.getDuck('청둥오리');
청둥오리.quack.do();
청둥오리.fly.do();
}
run();
정리
- 상속만으로 중복 코드를 없애려고 하면, subclass 별로 구현이 달라질 때 달라진 구현을 모두 override하여 변경해야한다
- 그렇다고 중복되는 부분을 모두 interface로 빼버리면 모든 subclass에서 구현 코드를 모두 써야한다
- 따라서 superclass에서 interface로 빼놓고, 해당 interface의 구현도 또다른 클래스로 빼놓으면 (행동을 클래스로 빼놓는 것) 중복 코드도 줄이면서(재사용성) 변경에 대응하기도 용이하다
출처 : head first design patterns, 사내 스터디 노션 페이지
상태 패턴
동일한 메서드가 상태에 따라 다르게 동작할 때가 있다. 이럴때 사용할 수 있는 패턴이 상태패턴이다. 전략패턴과 조금 비슷하기도 하다.
- 전략 패턴은 한 번 인스턴스를 생성하고 나면, 상태가 거의 바뀌지 않는 경우에 사용한다.
- 상태 패턴은 한 번 인스턴스를 생성하고 난 뒤, 상태를 바꾸는 경우가 빈번한 경우에 사용한다.
강사님은 테트리스 게임에 상태패턴을 사용하면 좋다고 했는데 스터디를 같이 하는 분은 동의하지 않았다. 상태에 따라 메서드가 하는 역할이 달라서 분기처리하는 경우에 사용하는데 테트리스는 전혀 그렇지 않다고 했다. 강사님이 이번 강의에서 상태패턴을 적용한 부분을 먼저 보자.
const Game = class {
constructor(basePanel, row, col) {}
addState(state, { init, render }, f) {
this.state[state] = f;
this.panel[panel] = new Panel(this, init, render);
}
};
return {
init() {
const game = new Game(10, 20, {
init() {
return sel('#stage');
},
render(base, game, panel, { base: el = panel.init() }) {
base.innerHTML = '';
const { base: el = panel.init() } = panel;
base.appendChild(el);
},
});
// 전략패턴, 상태패턴
game.addState(
Gametitle,
{
init(game, ...arg) {
sel('#title').style.display = 'block';
sel('#title.start').onclick = () => game.setState(Game.stageIntro);
return sel('#title');
},
render: null,
},
(_, { stage, score }) => {
stage.clear();
score.clear();
},
);
},
};
기존에는 게임에 메서드가 존재하고 game인스턴스를 생성할 때 인자를 넣고 그 인자값을 constructor에서 panel로 만들었다. 다음과 같이 말이다.(나는 가독성이 떨어진다고 생각해서 코드를 수정했지만 일단은 기존 코드를 가져왔다.)
const Game = class {
constructor(base, col, row, ...v) {
Object.assign(this, { base, col, row, state: {}, curr: 'title', score: new Score(), stage: new Stage() });
let i = 0;
while (i < v.length) this.state[v[i++]] = Panel.get(this, v[i++], v[i++]);
}
[s.title]() {
this.stage.clear();
this.score.clear();
}
const game = new Game(
sel('body'),
10,
20,
Game.title,
(game) => {
sel('#title .btn').onclick = (_) => game.setState(Game.stageIntro);
return sel('#title');
},
null,
사실 스터디를 할 당시에 디자인 패턴 공부를 제대로 하지 못했다. 강의듣고 테트리스 플레이 로직을 어떻게 만들어야할지 생각하다보니 시간이 부족했다. 지금 구글링도 해보고 혼자서 생각을 다시 해봤다. 테트리스는 상태에 따라 분기처리를 하지 않는다. 이건 분명하다.
잠깐 다른 얘기를 하겠다. 코드를 작성하거나 설계할 때 프로그램의 크기에 따라서 다르게 설계해야만 한다. 간단한 투두리스트 혹은 블로그를 만드는데 규모가 큰 프로젝트를 설계하는 것처럼 만들면 안된다. 그런데 혼자서 공부를 하거나 토이프로젝트를 만들때는 그렇게 규모가 큰 프로젝트를 만들 수 없다. 그러면 프로그램 규모가 커졌을 때 어떻게 해야할지는 언제 공부해야할까?? 회사에서도 당연히 배운다. 하지만 그걸로는 부족하다. 그래서 나는 굉장히 규모가 큰 프로젝트라고 생각하고 코딩을 하기로 했다.(항상 그렇다는 것은 아니다. 다만 지금 나에게는 이런 학습이 필요하다.)
잠깐 다른 얘기를 했는데 테트리스도 방금 한 얘기랑 접목시켜서 생각해보자. 테트리스의 상태는 정해져있고 클래스에서 메소드를 만들고 인스턴스를 만들때 특정 로직을 추가해주면 된다. 그런데 한달에 한번씩 아니 일주일에 한번씩 상태가 추가된다고 생각해보자. 그러면 클래스 메소드도 추가해야 하고 인스턴스를 생성하는 코드도 추가해야 한다. 한번에 추가를 하면 자주 업데이트를 해야하는 경우 장점이 있다. 이것과 비슷한 얘기를 스터디 때 했지만 그건 상태패턴과 관련없고 다른 얘기다 라는 말을 들었다. 그런것같기도 하다. 상태패턴은 이경우가 아닌데... 그런데 왜 강사님은 상태패턴 얘기를 했을까?? 이경우도 상태패턴의 종류라고 할 수 있는걸까?? 모르겠다. 아니 잠깐만... 게임클래스에 메소드를 만들고 인스턴스를 생성할 때 메서드에 해당하는 판넬값을 넣어주는 이유는 네이티브와 도메인을 분리하기 위해서다. 그런데 한번에 강사님이 한 방법으로 하면 전혀 분리되지 않는다. ㅁㄴㅇㄹㅁㄴㅇㄹ 머지??
테트리스 게임로직
강의에서 테트리스 게임로직을 전혀 다루지 않다니.... 뭔가 같이 테트리스 스터디를 하자고 한 팀원들에게 미안했다. 아 5회차때 잠깐 맛보기형식으로 보여주기는 했다. 어쨋든 스터디를 하기전에 어떻게 구현해야 하는지 생각을 해봤다. 일단 미리 만들어둔 TableRenderer, Data를 이용했다.
우리는 블록을 미리 생성해놨다. 회전하는 경우까지 말이다. 일단 쉽게 생각하자. 우리는 현재블록을 게임보드안에서 조정할 수 있다. position과 현재블록만 있으면 쉽게 조절할 수 있다. 현재블록을 [[1], [1], [1], [1]]
이런 블록일때(ㅣ자 블록) position이 {x:0,y:1}이라면 현재블록의 인덱스의 y값에 1을 더하고 x값에 0을 더하자. 참고로 현재 블록의 인덱스는 [[0,0],[1,0],[2,0],[3,]]
이다. 그리고 변화된 인덱스는 [[1,0],[2,0],[3,0]]
이다. 지금 변화된 인덱스와 현재 블록을 비교해서 현재블록의 값이 1이라면 그곳에 색을 칠하면 된다. 뭔가 글로 적으려니까 어려운데... 나중에 영상찍어야겠다. setInterval을 통해서 y값을 1씩 증가시켜주면 일정 시간마다 블록이 내려간다. 그리고 왼쪽방향키를 누르면 x값을 1빼고 오른쪽 방향키를 누르면 x에 1을 더하면 된다. 한칸씩 내리는거나 맨밑으로 내리는 함수는 어렵지 않아서 생략한다. 회전도 어렵지 않다. 그냥 만들어놓은 블록 클래스에서 right혹은 left를 호출하면 된다. 블록은 항상 x*y행렬이므로 어떻게 바뀌든지 포지션값만 있다면 쉽게 이동시킬 수 있다.
data, table renderer를 살펴보자. table renderer는 일단 두개만 만들어도 충분하다. 하나는 테트리스 보드판이고 다른 하나는 다음 블록이다. 주의해야 할 점은 테트리스 보드판에는 현재 우리가 움직이는 블록을 제외시켜야한다는 것이다. 현재보드판은 이미 블록이 쌓인 상태를 가지고 유동적으로 블록을 움직여야한다. 이것만 하면 끝이다. 딱히 어려운게 없다. 어려운건 객체지향적으로 코드를 설계하는것이지 테트리스 게임 로직이 아니다.
이런 정돈되지 않은 코드를 올리는게 부끄럽지만 일단 구현한 테트리스 링크를 올려놓았다. 수정해야될부분도 있고 인지하고 있다.
https://github.com/yoonminsang/code-spitz/blob/main/function-and-oop/tetris-ex/game.js
정리
강의 목적이 객체지향 공부인데 어쩌다보니 이번 강의는 디자인 패턴 얘기가 많이 나왔다. 객체지향을 하기 위해서 만든 디자인 패턴들이기때문에 어쩔수없는 것 같기도 하다. 디자인패턴의 공부 필요성도 조금 느꼈다. 일단은 테트리스를 강사님방법대로 구현해봤는데 마음에 안든다. 코드를 검증하지 않고 막 짜서 그대로 따라칠수는 없었다. 그래서 오히려 좋았던것같기도하다. 어쨋든 지금 배운 객체지향방법으로 나만의 테트리스를 만들어야겠다. 다음 스터디에서는 각자가 만든 테트리스를 보고 코드리뷰를 하면 좋을것같다. 근데 요즘 회사일이 바빠서 다들 해올수있을지는 모르겠다... 나도 완성은 했지만 아직 내 코드로 구현하지 않아서 시간이 좀 필요하다. 아마 빠르면 이번주 늦으면 다음주에 이 스터디도 끝이날것같다.
'강의 > 코드스피츠' 카테고리의 다른 글
코드스피츠 3rd-3 ES6+ 함수와 OOP 6회차 리뷰 (0) | 2022.10.01 |
---|---|
코드스피츠 3rd-3 ES6+ 함수와 OOP 5회차 리뷰 (2) | 2022.09.23 |
코드스피츠 2rd-3 ES6+ 함수와 OOP 5회차 리뷰 (0) | 2022.05.21 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 4회차 리뷰 (0) | 2022.04.30 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 3회차 리뷰 (0) | 2022.04.10 |