티스토리 뷰
객체지향 설명
본론에 앞서 객체지향에서 필수적으로 알아야할 것 세가지를 설명한다.
1 . 추상화
1. 카테고라이즈 : 분류를 일정한 기준을 통해 묶는 것
분류.. classification, 예전에 쓴 프로토타입글이 생각난다. 객체지향에서 절대 빠트릴 수 없는 것 중 한가지다. 어른과 아이, 여자와 남자, 초급자와 중급자처럼 말이다. 이렇게 예시를 드니 좀 감이 안올수 있어서 실제 코딩할 때 예시를 들어보겠다. 블로그에는 관리자와 참여자로 나눌 수 있다. 너무 쉬워보이나?? 개인적으로는 주니어기준으로 쉽지 않은 일이라고 생각한다. 실제 회사에서 코딩을 하다보면 분류를 하는 방법이 굉장히 다양하게 나오고 어떻게 분류를 하느냐에 따라서 코드 퀄리티가 달라진다. 절대 쉬운일은 아니다. 무의식중에 사용하고 있지만 별 생각없이 코딩하면 간과하기 쉬운 내용중 한가지다.
2. 모델링 : 표현하고 싶은 것만 표현, 기억해야 할 것만 기억. 복잡한 현실세계를 이해 x.
객체지향의 사실과 오해가 생각난다. 복잡한 현실세계를 이해하지 않고 새로운 세계를 만든다.(이상한 나라의 앨리스)
https://ms3864.tistory.com/415?category=1028645
이게 진짜 어렵다. 예전에 면접볼때 이런 얘기를 한 적이 있다. 현실 세계에서 우리가 필요한 것을 적절히 뽑아내야한다는... 그리고 그때 면접은 붙었지만 쉽지 않은 면접이였다.
누군가는 pm이나 기획자가 전달한 내용을 개발자는 코딩만 하면 끝나는게 아니냐고 물을수도 있다. 관점에 따라 조금 다르지만 그렇다고는 할 수 없다. 기획서를 받는다고 할지라도 기획서에 모든 내용이 정확하게 적혀있지는 않다. 그걸 어떻게 구현하는지는 개발자의 능력에 달렸다. 글을 적다보니 데이터베이스 설계가 생각이 났다.
https://ms3864.tistory.com/250
또한 기획서를 100퍼센트 신뢰할 수는 없다. 부정적으로 하는 말이 아니라 개발자도 실수하고 기획자, pm, 디자이너도 실수한다. 항상 실수하지 않을거라는 보장이 없다. 어떻게 보면 개발자가 기획자보다 더 잘못된 것을 찾기 쉽다. 실제 코딩하는 사람이니 말이다. 특히나 프론트엔드 개발자는 백엔드, 디자이너와 모두 협업하기 때문에 전체적인 플로우를 가장 알기 쉬운 직군이다. 이건 기획서에 없으니까 나는 몰라~~ 라는 마인드를 가지면 안된다. 나도 회사에서 정책이나 디자인을 보다보면 잘못된 부분 혹은 놓친 부분이 보인다. 그럴때 타 직군 분들과 얘기를 한다. 또한 인프런의 이동욱님이 이런 내용을 블로그에 적기도 하셨다.
https://jojoldu.tistory.com/667?category=689637
3. 집합(grouping) : 그냥 모아놓은 것
말 그대로 그냥 모아놓ㅌ은 것이다. 왜 카테고라이즈가 있는데 그냥 모아놓는 작업이 필요할까?? 현실 세계를 생각해보자. 대학교에서 교수님이 출석부를 보고 조를 나눴다고 생각해보자. 아무런 의미 없이 grouping을 한 것이다. 하지만 이게 아무런 의미가 없지는 않다. 내가 1조라면 1조끼리 모여서 회의를 하고 발표를 해야한다. 이런식으로 분류되지 않지만 모아놓는 작업이 때때로 필요하다.
ps. 추상화 기법이 몇가지 더 있지만 세가지만 일단 알아두라고 한다. 나는 너무 많은 것을 한번에 배우는게 좋다고 생각하지 않는다. 적당히 끊어갈 줄 알아야한다. 아마 생활코딩의 이고잉님도 그런말을 자주 하는 걸로 알고 있다. 우리는 클래스를 배운다고 혹은 생성자 함수, 프로토타입을 배운다고 객체지향적으로 코드를 짤 수 없다. 그러면 추상화를 배웠으니 객체지향 언어를 사용할 수 있게 된걸까?? 그것도 아니다. 그저 추상화 기법을 배웠을 뿐이다.
2. 객체지향의 학술적 의미
그러면 객체지향이란 도대체 뭘까?? 학술적 의미를 살펴보자
1. 대체가능성
대체가능해야 한다.
자식이 부모를 대체가능하고 구상클래스가 인터페이스를 대체하고, 추상클래스 대체할 수 있어야 한다.
2. 내적 동질성
클래스의 상속을 생각해보자. a클래스를 만들고 a를 상속한 b클래스를 만들고 b를 상속한 c 클래스를 만들자. 그리고 모두 hi라는 메소드를 가지고 있다. c에서 hi라는 메소드를 실행할 때 어떤 클래스의 메소드가 실행되야할까?? 우리는 모두 답을 알고 있다. 하지만 맨 위의 메소드를 가져와도 말이되고 맨 아래의 메소드를 가져와도 말이된다. 언어가 정하기 나름이다. 근데 그러면 너무 헷갈리지 않을까?? 그래서 그냥 약속을 했다. 객체 본질은 변하지 않고 태생을 그대로 유지하려는 성질을 가지기로. 즉 자바스크립트로 예를들면 c에서 메소드를 찾고 없는경우에만 __proto__를 통해서 위로 올라간다.
2가지가 지원되야지 객체지향 언어라고 부를 수 있다. 지원하지 않는다면 객체지향 언어가 아니다.
3. 객체지향에서 쌍방끼리 지켜야할룰
1. 은닉
은닉은 말그대로 아무것도 보여주지 않는 것이다. 안을 못보게 해주는 언어들이 있다.(안해주는 언어도 있다.)
2. 캡슐화
캡슐화는 필수적으로 필요하다. 보다 지식이 없게 하는데 목적이 있다. 마치 '엄마 나 놀러갔다올게요~~'와 같다. 강사님의 예시다. 정말 예시를 잘 든것같다. ㅎㅎ
역할
역할은 권한과 책임을 가진다. 그런데 권한은 가지고 책임을 가지지 않는 경우가 있다. 군대라던가 회사라던가...(내 경험은 아니다) 그런 일이 발생하면 당연하게도 문제가 발생한다. 이건 객체 지향 세계에서도 마찬가지다. 역할을 잘 나누고 권한과 책임을 가지게 해야한다. 어느 한쪽만 가지면 문제가 생긴다.
강사님은 역할만 나눌줄알면 고급개발자라고 했다. 그리고 학습을 했을 때 정말 빠르면 2년, 보통 5년 느리면 그 이후에도 성과가 보이지 않는다고 했다. 지금부터 열심히 해도 빨라야 2년, 그리고 늦으면 5년이 되어도 아얘 눈에 보이는 지표가 없을 수도 있다는 소리다. 조금 막막해보이기도 하다. 새로 나온 기술은 적응하는데 한달도 걸리지않고(경우에 따라 다르지만 기본 문법에 사용법정도는 이시간이면 충분하다) cs 과목 하나를 잡아도 한달동안 열심히 한다고 했을 때 꽤 눈에 보일만큼 발전한 것을 볼 수 있다. 그런데 2년, 5년은 얘기가 다르다. 어쩌면 내가 지금 객체지향 공부를 하는게 5년 뒤에도 의미가 없을수도 있다는 얘기다. 그렇다고해서 이 공부를 멈출생각은 없다. 애초에 개발을 만만하게 보지도 않았고 지금 이런 공부를 한다고해서 바로 달라질거라고 생각하지도 않았다. 일단은 계속 할 생각이다.
본론
카테고라이즈
테트리스를 처음에 다음과 같이 나눌 수 있다.
여기까지만 보고 어떻게 할지 생각을 해보자.
게임은 스테이지, 점수, 패널 등 여러가지 상태를 가진다. 이걸 하나의 모델로 생각하자. 그리고 각각의 컴포넌트에서는 모델의 상태를 변경시킨다. 이때 각각의 컴포넌트는 필요한 모델의 정보만 알면 된다. 그리고 컴포넌트에서 직접적으로 모델의 상태를 변경시키면 어떻게 될까?? 컴포넌트가 굉장히 복잡해진다. 컨테이너에서 이런 역할을 대신해줘서 모델과 뷰의 결합을 느슨하게 해주면 좋을 것 같다. 그리고 모델의 데이터(상태)가 변경될때 어떻게 전달해야할까?? 옵저버패턴을 이용하면 된다. 지금 우리는 mvc, flux 패턴까지 생각했다. 그리고 이걸 지원하는 라이브러리 혹은 프레임워크들을 생각해보자. 리액트 리덕스다. 리액트대신 뷰, 앵귤러가 될 수도 있고 리덕스 대신 mobx, recoil이 될 수도 있다.
중요한 건 리액트, 리덕스가 아니다. 코드의 완성도나 최적화같은 부분에서는 차이가 나겠지만 테트리스정도의 프로젝트를 만드는데 필요한 리액트, 리덕스는 간단하게 만들 수 있다. 그리고 그 이전에 객체지향적으로 생각을 하는게 더 중요하다. 코드구현하는것도 쉬운건 아니지만 그보다 더 본질적인 것을 생각해야한다. 무작정 돌아가게 만드는 코드는 누구나 만들 수 있다. 하지만 그걸 잘 만드는건 어렵다. 잘 만드는게 중요한가?? 그냥 코드가 돌아가면 끝 아닌가?? 라고 생각할 수도 있겠지만 개발은 그렇게 단순하지 않다. 실제 회사에서 하는 코딩은 대부분이 유지보수이고 새로운 프로젝트를 만드는 경우는 드물다. 그렇기 때문에 1년이 지나고 봐도 이해할 수 있고 신입이 와도 그 코드를 빠르게 파악할 수 있게 만들어야 한다. 그리고 기획이 크게 바뀌었다면 상황에 따라 필수적으로 리팩토링도 해야한다. 그렇지 않고 기능만 추가한다면 아무리 처음에 잘 만들었어도 기능이 덕지덕지 붙어있게 되고 결국 안좋은 코드가 되어버린다.
말은 쉽다. 나는 지금 그런 능력이 없다. 강사님도 그걸 알고 있고 그렇기 때문에 테트리스를 통해 보여주려는 것 같다.
유틸함수
들어가기에 앞서 강사님이 사용하는 함수들을 적어놓겠다. 어려운건 없어서 설명은 생략
근데 변수명을 너무 막 지은것같다는 생각이 든다. 이게 클린코드수업은 아니지만 s, v처럼 써놓으면 가독성이 안좋다. 물론 지나치게 간단한 함수이기 때문에 쉽게 유추할 수 있기는 하다.
const prop = (target, v) => Object.assign(target, v);
const el = (el) => document.createElement(el);
const back = (s, v) => (s.backgroundColor = v);
스테이지
스테이지 클래스는 last, min, max, listener를 인자로 받는다. last는 마지막 라운드, min, max는 테트리스 속도, listener는 특정 메소드 실행 이후에 실행시킬 함수다.
내가 코드를 짠다면listener를 만들지 않고 옵저버패턴을 이용할 것 같다. 아마 강사님도 혼자서 코드를 만든다면 디자인패턴을 적용할 것이다. 참고로 다음 수업이 디자인 패턴과 뷰패턴이다. 그 수업을 하면서 저번에 만든 테트리스를 개선해봐라 라고 숙제를 낼것같은 느낌적인 느낌이 드다.
내 생각보다 신경쓰는 부분이 꽤 많았다. 라운드나 내려오는 속도는 생각도 못했다. 저번에도 한번 말했지만 다시 강조했는데 절대 현실 세계 내용을 축소시키면 안된다고 했다. 복잡한 현실세계를 필요한 부분만 뽑아내는 능력은 필요하지만 필요한 정보를 없애면 안된다. 한번씩 뼈맞는 느낌이 들때가 있다. 이것도 알면서 조금 귀찮은 것도 사실이라서..
getter를 만들지 않았지만 나중에 현재 스테이지의 정보를 가져올 때 getter 함수로 this.curr값을 보여줄 것이다. 이건 사실 은닉이 아니다. getter를 만들어도 직접 접근이 가능하니 말이다. 그러면 클로저를 사용해야할까?? 클로저로 은닉을 사용해도 괜찮지만 지금은 private 속성을 지원하기 때문에 클로저를 사용하지 않아도 상관없다. 그리고 ts를 사용한다면 private, readonly를 모두 지원하기 때문에 getter 함수를 만들필요도 없지않나?? 생각이 든다. 근데 동시에 그래도 필요하지 않나?? 라는 생각이 드는데 음... 좀 더 생각을 해봐야겠다.
next메서드에서 Stage.last는 뭐지?? 오탄가?? static으로 last를 선언하지 않았고 선언할 이유도 없다. 일단 냅두고 별다른 이유가 없다면 나중에 수정하겠다.
const Stage = class {
constructor(last, min, max, listener) {
prop(this, { last, min, max, listener });
}
clear() {
this.curr = 0;
this.next();
}
next() {
if (this.curr++ < Stage.last) {
const rate = (this.curr - 1) / (this.last - 1);
this.speed = this.min + (this.max - this.min) * (1 - rate);
this.listener();
}
}
};
스코어
테트리스에서 라인을 지울때 마다 점수를 올린다. 그런데 스테이지별로 올리는 점수가 다르다. 그래서 stage에 접근을 해야한다.
지금 위임과 협력을 했다. 항상 내가 하는일이 맞는지, 이 객체가 하는 역할이 맞는지 체크해야한다. 백엔드에서 데이터가 잘못넘어왔다고 백엔드를 내가 건들면 안된다. 물론 지금 회사에서는 잘못된 부분이 있다면 얘기하고 수정을 요청할 수도 있다. 하지만 기본적으로는 자신의 역할과 책임에 충실해야 한다. 이걸 R&R(Role and Responsibilities)이라고 한다. 객체지향에서는 R&R이 중요하다.
스테이지의 current값을 가져와서 score에서 구현할 수도 있지만 이걸 stage에서 한 이유가 뭘까??stage만이 current라는 내장된 값을 가지고 있다. 그렇기 때문에 책임을 넘겼다. line같은 경우는 인자로 넘기면 그만이지만 current값을 넘겨받기 위해서는 stage에서 getter로 curr값을 가져와야한다. 그런데 한편으로는 다른 생각도 든다. score라는 클래스를 만들었는데 점수를 얻기위해서 stage 클래스에 접근하고 거기서 계산을 하는게 맞는건가?? 그냥 stage에서 curr값을 가져와서 score 클래스 내부 메서드로 처리하면 훨씬 가독성도 높아지지 않나? 라는 생각이 든다.
만약 리덕스를 사용한다면 게임이라는 store에 stage, score를 저장할 것이고 게임상태에서 불러오며 되기 때문에 지금과 같은 고민은 하지 않게 된다. 그런데 그게 좋은 현상일까?? 결국 분리하지 않고 합쳐놨을 뿐이다. 합쳐놨으니 위임, 협력이 줄어든거다. 아니 조금 다르게 생각을 해보자. 지금은 간단한 예시를 들었을 뿐이고 리덕스 사가를 사용할 때만봐도 여러 스토어에 접근해야하는 경우가 있다. 결국 본질은 같다.
다시 생각을 해봤을 때 그래도 stage에서 하는게 더 좋다는 생각이 든다. 그리고 이걸 설명하면서 이걸 잘하면 고급개발자가 되고 이걸 못하면 엉망진창인 소스가 된다고 말했다. 이미 여러번 말했다. 역할만 분리하면 고급 개발자라고 말이다. 코드로보니까 좀 와닿는다.
const Stage = class {
~~
score(line) {
return parseInt(this.curr * 5 * 2 ** line);
}
};
const Score = class {
constructor(listener) {
prop(this, {listener });
}
clear() {
this.curr = this.total = 0;
}
add(line, stage) {
const score = stage.score(line);
this.curr += score;
this.total += score;
this.listener();
}
};
참고로 함수에서는 인자와 지역변수만 존재한다.(자유변수제외) 그런데 객체지향에서는 컨텍스트 변수를 이용할 수 있다. 그리고 함수지향에서는 자유변수를 이용한다.
객체지향을 통해서 클래스의 인스턴스를 만드는 행위는 함수지향으로 생각하면 필요한 자유변수를 가둬둔채로 함수를 만들어서 return하는 행위와 똑같다. 결국 객체지향이든 함수지향이든 자유롭게 서로 변환할 수 있어야된다. 라는게 의도인 것 같다.
그래서 뭔가 잘못됬다는 생각이 드는가?? oop 1회차 리뷰에서 나왔던 내용이다. coupling(결합도). 사실 강한 결합도는 아니다. 그런데 잘 생각해보자. 게임에서 스테이지 관리자와 스코어 관리자는 관계가 하나만 있으면 된다. 지금 코드를 보면 점수를 더할 때 마다 스테이지를 새롭게 만든다. 이건 잘못됬다. 즉 인자로 받으면 안된다.
const Score = class {
constructor(stage, listener) {
prop(this, { stage, listener });
}
add(line) {
const score = this.stage.score(line);
}
};
블록
회전축을 먼저 정해야한다. 현재 테트리스는 좌상단을 회전축으로 정했다고 한다.(세계대회기준?)
테트리스 블록은 회전한다. 이때 회전하는 모든 경우의 블록을 메모리에 저장할 수도 있고 하나만 만들고 연산을 통해서 블록을 만들 수도 있다. 예전에는 메모리가 부족했지 때문에 연산을 중심으로 만들었지만, 지금은 메모리가 풍부하고 cpu 비용을 아끼기 위해서 메모리를 늘리고 연산을 없애려고 한다.
추상클래스 블록
특별한 건 없고 getBlock은 자식에서 override할것이기 때문에 직접 사용한다면 에러를 뱉게한다.
const Block = class {
constructor(color) {
prop(this, { color, rotate: 0 });
}
left() {
if (--this.rotate < 0) this.rotate = 3;
}
right() {
if (++this.rotate > 3) this.rotate = 0;
}
getBlock() {
throw 'override!';
}
};
구상클래스 블록
rotate를 상속받아서 다음과 같이 만들 수 있다.
const blocks = [
class Block1 extends Block {
constructor() {
super('#f8cbad');
}
getBlock() {
return this.rotate % 2 ? [[1], [1], [1], [1]] : [[1, 1, 1, 1]];
}
},
class extends Block {
constructor() {
super('#ffe699');
}
getBlock() {
switch (this.rotate) {
case 0:
return [
[0, 1, 0],
[1, 1, 1],
];
case 1:
return [
[1, 0],
[1, 1],
[1, 0],
];
case 2:
return [
[1, 1, 1],
[0, 1, 0],
];
case 3:
return [
[0, 1],
[1, 1],
[0, 1],
];
}
}
},
];
그런데 우리가 충분히 추상화를 한걸까?? 한번 생각해보자
먼저 부모의 rotate값을 자식이 알고 있다. 이건 은닉을 깨먹고 있는 것이다. 부모자식이라도 은닉, 캡슐화가 필요하다.
강사님이 자식이 부모의 재산을 뒷조사해서 알아내고 이런 얘기를 했다. 한마디로 콩가루집안이라는 소리고 지금 코드는 콩가루 코드라는 소리다. 사실 지금 작성한 방법보다 개선할 방법은 생각해낼 수 있지만 부모의 컨텍스트값을 가져오고 연산하는게 문제라고는 생각을 못했다. 고급개발자는 이런 것들을 바로바로 알 수 있지만 초급 개발자들은 코드의 역할을 의인화시켜서 생각해야 한다.
또한 계속해서 배열을 생성하고 있다. getBlock을 호출할 때마다 데이터를 만드는데 이건 정적데이터여도 문제가 없다. 즉 컨텍스트 데이터가 더 나아가서는 static 데이터가 되야한다.
이제 바꿔보자
이게 카테고라이즈를 한거다. 도메인 수준에서 파악해보면 블록간에 차이는 색과 블록 두가지다.
const Block = class {
constructor(color, ...blocks) {
prop(this, { color, rotate: 0, blocks, count: blocks.length - 1 });
}
left() {
if (--this.rotate < 0) this.rotate = count;
}
right() {
if (++this.rotate > count) this.rotate = 0;
}
getBlock() {
return this.blocks[this.rotate];
}
};
const blocks = [
class extends Block {
constructor() {
super('#f8cbad', [[1], [1], [1], [1]], [[1, 1, 1, 1]]);
}
}
];
Data
이제 Data 클래스를 만들자. 조금 특이하게 배열을 상속했다. 이를 마커 클래스라고 부른다. 배열만으로는 확인할 수 없기 때문에 상속받아서 새롭게 만든것이다. es5까지는 상속을 통해서 프로토타입을 만들어도 만들어지는 객체는 오브젝트다. 근데 es6에서 클래스를 사용해서 만들면 부모께 만들어진다. 이걸 홈 오브젝트라고 한다. 이 대상은 코어 객체다. 즉 Data는 진짜 배열이 된다. (es5에서는 진짜 배열이 아니였다.)
es6 클래스는 대체 불가능하다. this를 바인딩해서 바꾸는 능력이 있다. 부모가 먼저만들어지고 자식을 타면서 this를 바꾼다. 이게 핵심적인 기능이다. es6 이전에는 this를 바꿀 수 없었다. 나는 클래스가 프로토타입, 생성자 함수의 syntax sugar말고도 추가적인 기능이 있다고는 알았지만 자세히 찾아보지 않았다. 이번기회에 조금 알게되었는데 이건 따로 블로그에 글을 정리해서 올려야겠다.
const Data = class extends Array {
constructor(row, col) {
prop(this, { row, col });
}
};
Renderer
이제 범용 렌더링 처리기를 만들자. canvas로 만들든 table로 만들든 기본은 같다.
아래 코드를 먼저 보자. clear와 _render는 자식에서 오버라이드할것이다. 그리고 render에서는 _render를 호출한다. 잘 생각해보면 부모에서 render를 호출하고 _render는 자식에서 호출된다. 이게 내적동질성을 이용한 코드다. 이걸 템플릿 메소드 패턴이라고 한다.
const Renderer = class {
constructor(col, row) {
prop(this, { col, row, blocks: [] });
while (row--) this.blocks.push([]);
}
clear() {
throw 'override';
}
render(data) {
if (!(data instanceof Data)) throw 'invalide data';
this._render(data);
}
_render(data) {
throw 'override!';
}
};
Table Renderer
const TableRenderer = class extends Renderer {
constructor(base, back, col, row) {
super(col, row);
this.back = back;
while (row--) {
const tr = base.appendChild(el('tr')),
curr = [];
this.blocks.push(curr);
let i = col;
while (i--) curr.push(tr.appendChild(el('td')).style);
}
this.clear();
}
clear() {
this.blocks.forEach((curr) => curr.foReach((s) => back(s, this.back)));
}
_render(v) {
this.blocks.forEach((curr, row) =>
curr.forEach((s, col) => back(s, v[row][col]))
);
}
};
Canvas Renderer
const CanvasRenderer = class extends Renderer {
constructor(base, back, col, row) {
super(col, row);
prop(this, {
width: (base.width = parseInt(base.style.width)),
height: (base.height = parseInt(base.style.height)),
cellSize: [base.width / col, base.hegiht / row],
ctx: base.getContext('2d'),
});
}
_render(v) {
const {
ctx,
cellSize: [w, h],
} = this;
ctx.clearRect(0, 0, this.width, this.height);
let i = this.row;
while (i--) {
let j = this.col;
while (j--) {
ctx.fillStyle = v[i][j];
ctx.fillRect(j * w, i * h, w, h);
}
}
}
};
도메인패턴 vs 트랜지션 스크립트 패턴
패턴이 두가지가 나왔다. 우리는 도메인 패턴을 지향해야한다.
table renderer와 canvas renderer를 생각해보자. 이 둘은 브라우저 네이티브 객체를 알고 있다. 즉 나머지는 dom에 대한 지식이 없다. 이말은 다른 네이티브 시스템이 와도 재사용이 가능하다는 것이다. 이때 네이티브 객체가 아닌 나머지를 도메인 객체라고 부른다.
패널 아래도 마찬가지로 네이티브 레이어가 생긴다. 그래서 범용패널을 만든것이다. 범용 렌더링 처리기도 마찬가지다. 이제 이렇게 나눈 이유가 조금 보이기 시작했나요? 라고 강사님이 말한다. 음.... 설명은 밑에서
그리고 네이티브와 도메인 사이는 프로토콜이 들어갈 확률이 높다.
또한 다음에 추상팩토리메서드를 배운다고 한다. 일단 최우선은 도메인을 넓히고 네이티브 역할을 줄이고 렌더러를 줄여야한다.
스터디
내가 스터디를 하자고 슬렉에 올렸는데 감사하게도 두명이나 참여하기로 했다. 첫번째 시간에는 테트리스의 개요 약 40분 정도까지 듣고 스터디를 했고 두번째 시간에는 마지막까지 다 듣고 스터디를 했다.
첫번째 스터디
40분만 들었고 코드도 작성하지 않았는데 무슨 얘기를 해야할까?? 라고 생각도 했지만 생각보다 할 얘기가 많았다. 강의의 내용에 대해서 얘기하고 우리가 잘못 사용하고 있던것들 그리고 회사 코드에서 개선해야 할 것들을 얘기했다. 그중에서도 grouping에대한 얘기를 많이 했다. 실제 백엔드 코드를 기반으로 생각해봤는데 쉽지 않았다. 간단하게 말하면 타입에따라 텍스트를 바꿔줘야하는데 이게 너무 복잡했다. 토이 프로젝트라면 절대 없을 상황인데 생각보다 비지니스는 복잡했다. 회사 코드라서 적지는 못하지만 그중 한가지만 예를들면 switch case문에서 10개의 타입인 경우에 a로 바꾸고 10개의 타입인 경우에 b로 바꾸고 이런 코드들이 있었다. 사실 그냥 봤을 때 잘못된 코드는 아니다. 다만 경우가 너무 많아지다보니까 덕지덕지 붙인 형태가 되고 유지보수 하기가 어렵게 된다. 이걸 grouping으로 봐야하는지도 약간 의문이다. 카테고라이즈가 아니니 그룹핑으로 볼 수 있는건가?? 맞는것같기도 하다. 어쨋든 연관없어 보이는 것들을 묶어서 처리를 해주는거니 말이다. 나는 사실 여기서 함수 혹은 변수만 분리해도 꽤나 간단해질 수 있다고 생각한다. 그게 근본적인 해결책은 아닐 수 있다. 하지만 명령형 프로그래밍에서 벗어나는 것도 충분히 의미가 있지 않을까?? 물론 기존에도 함수분리가 되어있다. 다만 조금 더 개선할부분이 있다고 생각한다. 함수분리는 가장 기본적이면서도 어렵다고 생각한다. 결국 이것도 역할과 책임을 나누는 일이다. 너무 불필요하게 나눠도 안되고 나누지 않아도 문제다. 현실 세계는 너무나 복잡하고 그걸 객체지향 세계로 표현하는건 너무나도 어려운일이다...
그리고 어떻게 만들건지 개요를 짜봤다. 사실 나는 우테캠에서 라이브러리를 사용하지 않고 코딩을 많이 해봤기 때문에 대충 그려졌다. 물론 그게 역할과 책임을 잘 나눈것은 아니다. 그냥 전체적인 방향성 정도?? 그러면서 자연스럽게 react, redux가 나온다. 그리고 컴포넌트를 어떻게 만드는지 그런것들에 대해서도 간단하게 칠판에 설명을 했다. 이렇게 이것저것 얘기하다보니 아마 2시간? 정도 한 것 같다.
두번째 스터디
이번에는 강의에서 코드를 작성했기 때문에 조금 더 명확한 얘기를 할 수 있었다. 먼저 나왔던 얘기는(내가 제시한) 스테이지와 스코어의 의존성 문제다. 스테이지에서 현재 스테이지 정보를 가지고 있기 때문에 점수를 스테이지에서 계산하고 스코어에서 스테이지의 메서드에 접근해서 가져온다. 처음에 뭐지? 했는데 그렇게 만든 이유는 이해를 했다.(위에 적어놓음) 그런데 만약 엄청 복잡한 프로젝트라서 현재 스테이지에 의존하는 여러 상태가 필요하다면 어떻게 해야할까?? 사실 speed는 스테이지의 컨텍스트가 가지고 있다. 그런데 점수처럼 독립되어 있지만 현재 스테이지 정보에 따른 계산값이 필요하다면?? 이게 100개라고 해보자. 현실세계는 복잡하니까. 그러면 100개의 메서드가 필요하다. 이게 맞는걸까?? 옵저버패턴을 이용해서 각각의 클레스에 스테이지값을 가져오면 스테이지도 복잡해지지 않고 각각의 클래스도 분리가된다. 얘기를 해봤는데 잘 모르겠다. 여기서 또 유틸함수를 도입하면 어떨까?? 예전에 코드리뷰를 받을때 클래스에서 컨텍스트 값을 직접 이용하지 않는다면 따로 함수로 분리하는게 좋겠다는 조언을 받았다. 클래스가 거대해지면 파악하기가 어려워지니 말이다.
코드는 상황에 맞게 리팩토링해야한다고 말한다. 토스 유튜브에서 진유림님의 클린 코드 영상에 나온 말이다. 이것도 그런 상황인걸까??
Data 클래스를 보면서 클래스가 프로토타입, 생성자함수로 만든것과 어떻게 다른지 얘기를 했다. 영상을 봤지만 막상 설명하려니 잘 생각나지 않았다. 다행히 금방 생각해냈는데(영상에서 말한거라 엄밀히 말하면 내가 생각한게 아니다) 클래스의 상속은 부모로부터 this를 바꾸면서 자식으로 내려가는데 생성자함수, call을 이용한 상속은 자식에서 부모의 프로토타입을 만들고 연결한다. this를 바꿀수도 없고 말이다. (오버라이드와는 다른 의미다)
근데 보니까 Data 클래스를 잘못만든것같다. 아니 내가 초급개발자라 이해를 못하는건가?? 아래코드처럼 만들려고 한게 의도 아니였을까??
const Data = class extends Array {
constructor(row, col) {
super({row,col})
}
};
그리고 블록 배열을 가지고 어떻게 테트리스를 표현하냐는 얘기가 나왔다. 내 생각에는 기본 n*n의 2차원 배열이 있고 하나의 중심점(혹은 회전축)을 가지고 이 중심점의 행,열 상태를 가진다. 그리고 일정시간마다 행이 증가하고 왼쪽으로가면 열이 감소, 오른쪽으로 가면 열이 증가 이런식으로 움직인다. 마지막으로 이 중심점의 행,열과 블록 배열만 있으면 그려줄 수 있지 않을까?? 하고 생각한다. 그리고 회전할때는 board를 넘어가는지 체크만 해주면 된다.
이런 얘기들을 하다보니 2시간정도 시간이 지났다. 다음 시간에는 3기가 아니라 2기 강의를 듣는다. 왜냐하면 갑자기 테트리스를 만들다가 투두리스트로 넘어가서.. 애초에 이건 테트리스 스터디라서 그렇게 하기로 했다. 그게 끝나고 나는 다시 3기로 돌아와서 투두리스트를 들어야겠다.
'강의 > 코드스피츠' 카테고리의 다른 글
코드스피츠 2rd-3 ES6+ 함수와 OOP 6회차 리뷰 (0) | 2022.05.30 |
---|---|
코드스피츠 2rd-3 ES6+ 함수와 OOP 5회차 리뷰 (0) | 2022.05.21 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 3회차 리뷰 (0) | 2022.04.10 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 2회차 리뷰 (0) | 2022.03.26 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 1회차 리뷰 (0) | 2022.03.24 |