ES6+ 함수와 자바스크립트 OOP의 원리
코드스피츠 강의를 듣고 복습하기 위해 포스팅을 정리해보았다.
사실 자바스크립트 엔진과 OOP는 다소 궁합이 안 맞는다. 그래서 개발자가 더욱 신경을 써야 하는 영역이다.
객체 지향적인 설계는 프로젝트 확장성에 큰 기여를 하기 때문에 초반 설계에 많은 시간을 들여야 하는 고된 작업이다.
코드 스피츠 수업은 두번째 수강인데 기초적인 개념을 넘어서 실무에 적용할 수 있을만한 견문을 넓힐 수 있는 좋은 강의였다.
1회차
- sub routine flow는 상대주의적 관점을 가진다
메인 플로우, 서브루틴 중 누구를 기준으로 보냐에 따라 달라진다
메인 플로우는 서브루틴이 시작하고 돌아와야 할 point 위치를 가진다
서브루틴이 중첩될수록 중첩된 서브루틴들의 point를 메모리에 모두 keep 한다 = call stack
LIFO 구조이기 때문에 stack memory를 사용하여 call stack이라 부르는 것
- ES6+에서는 함수가 아니라 루틴이 흐른다
클래스의 메소드에 정의하기 때문에 this(function)의 의미가 없다.
- value vs reference
value에 의한 할당은 복사본이기 때문에 의존성이 떨어진다. 즉, 사이드 이펙트가 없다. state safe !
서브루틴에서 return하는게 ref 값이면 메인 플로우 안에있는 원본 데이터를 변경시킬 가능성이 있다
따라서 되도록 서브루틴의 리턴값은 readonly로 반환하거나 새 객체로 만들어 반환한다
// ref 값 참조로 원본 데이터 변경시키는 예제
const routine = ref => ['a', 'b'].reduce((p,c)=>{
delete p[c];
return p; // a,b를 제거하여 return
}, ref);
const ref = {a:3, b:4, c:5, d:6};
const a = routine(ref);
ref === a; // true
// 원본을 유지하기 위해 readonly로 변경! state safe! (side effect 제거)
const routine = ({a, b, ...rest}) => rest; // destructuring
const ref = {a:3, b:4, c:5, d:6};
const a = routine(ref);
ref !== a // true
// ref 값 참조로 원본 데이터 변경시키는 예제
const routine = ref => {
const local = ref;
local.e = 7;
return local;
};
const ref = {a:3, b:4, c:5, d:6};
const a = routine(ref);
ref === a; // true
// 원본을 유지하기 위해 new object로 변경! state safe! (side effect 제거)
const routine = ref => ({...ref, e:7}); // 순서 중요
const ref = {a:3, b:4, c:5, d:6};
const a = routine(ref);
ref !== a // true
래리 콘스탄틴 : Structured Design (구조적 설계)
📍 아래로 갈수록 좋은 설계 구조
낮은 결합도(coupling) 높은 응집도(Cohesion) 를 추구해야 한다
결합도와 응집도는 대체로 정비례하므로 더욱 어렵다..
즉, 프로그래밍 설계 시 그 둘의 합치 최상을 구현해야한다 → 이것이 전문가의 영역과 역할
📍 결합도
content(-)
common(-)
external(-)
control(-)
stamp(-)
data(+)
📍 응집도
coinciental(-)
logical(/)
temporal(/)
procedural(/)
communicational(/)
sequential(/)
functional(+)
Coupling (for 낮은 결합도)
// 1. Content 강결합은 안좋다(-)
// A클래스 속성v가 변경되면 즉시 B클래스가 깨짐
const A = class{
constructor(v) {
this.v = v;
}
};
const B = class{
constructor(a){
this.v = a.v;
}
};
const b = new B(new A(3));
// 2. Common 강결합도 안좋다(-)
// Common클래스 변경 시 즉시 A,B 클래스가 깨짐
const Common = class{ // 전역 객체
constructor(v){
this.v = v;
}
};
const A = class{
constructor(c){
this.v = c.v;
}
};
const B = class{
constructor(c){
this.v = c.v;
}
};
const a = new A(new Common(3));
const b = new B(new Common(5));
// 3. External 강결합은 그나마 낫다(-) 실무에서 불가피하기때문에 극복의 영역이다..
// A, B클래스는 외부의 정의에 의존함. member의 json 구조가 변경되면 깨
const A = class{
constructor(member){this.v = member.name;}
};
const B = class{
constructor(member){this.v = member.age;}
}
fetch('/member').then(res=>res.json()).then(member => {
const a = new A(member);
const b = new B(member);
});
// 4. Control 강결합은 코드를 고쳐야 한다(-) **(factory pattern)**
// A 클래스 내부의 변화는 B클래스의 오작동을 유발
const A = class{
process(flag, v){
switch(flag){
case 1:return this.run1(v);
case 2:return this.run2(v);
case 3:return this.run3(v);
}
}
};
const B = class{
constructor(a){
this.a = a;
}
noop(){this.a.process(1);}
echo(data){
this.a.process(2, data);
}
};
const b = new B(new A());
b.noop();
b.echo();
// 5. Stamp 강결합 or 유사약결합(-)
// A와 B는 ref(count)로 통신함. ref에 의한 모든 문제가 발생할 수 있음
// 되도록 필요한 값만 넘겨야 한다
const A = class{
add(data){
data.count++;
}
};
const B = class{
constructor(counter){
this.counter = counter;
this.data = {a:1, count:0};
}
count(){
this.counter.add(this.data);
}
};
const b = new B(5);
b.count();
b.count();
// 6. Data 약결합은 좋다(+)
// A와 B는 value로 통신함. 모든 결합문제에서는 자유로워짐
const A = class{
add(count){
return count + 1;
}
};
const B = class{
constructor(counter){
this.counter = counter;
this.data = {a:1, count:0};
}
count(){
// 참조 형태를 value(값)으로 변경하면 결합도를 낮출 수 있다!
this.data.count =
this.counter.add(this.data.count);
}
};
const b = new B(5);
b.count();
b.count();
Cohesion (for 높은 응집도)
// 1. Coincidental(-)
// 아무런 관계가 없음. 다양한 이유로 수정됨
const Util = class{
static isConnect(){}
static log(){}
static isLogin(){}
};
// 2. Logical(/)
// 사람이 인지할 수 있는 논리적 집합. 언제나 일부만 사용됨
const Math = class{
static sin(r){}
static cos(r){}
static random(){}
static sqrt(v){}
};
// 3. Temporal(/) - 시간의 순서
// 시점을 기준으로 관계없는 로직을 묶음. 관계가 아니라 코드의 순서가 실행을 결정
// 역할에 맞는 함수에게 위임해야 함
const App = class{
init(){
this.db.init();
this.net.init();
this.asset.init();
this.ui.start();
}
};
// 4. Procedural(/)
// 외부에 반복되는 흐름을 대체하는 경우. 순서 정책 변화에 대응 불가
const Account = class{
login(){
p = this.ptoken();
s = this.stoken(p);
if(!s) this.newLogin();
else this.auth(s);
}
};
// 5. Communicational(/)
// 하나의 구조에 대해 다양한 작업이 모여있음. 권한과 역할들
const Array = class{
push(v){}
pop(){}
shift(){}
unshift(v){}
}
// 6. Sequential(/)
// 실행순서가 밀접하게 관계되며 같은 자료를 공유하거나 출력결과가 연계됨
const Account = class{
ptoken(){
return this.pk || (this.pk = IO.cookie.get("ptoken"));
}
stoken(){
if(this.sk) return this.sk;
if(this.pk){
const sk = Net.getSesstionFromPtoken(this.pk);
sk.then(v => this.sk);
}
}
auth(){
if(this.isLogin) return;
Net.auth(this.sk).then(v => this.isLogin)
}
};
// 7. Functional(+) **—> 우리의 `달성 목표`!**
// **역할모델에 충실하게 단일한 기능이 의존성 없이 생성된 경우**
2회차
- Spread Ref
참조 관계는 계속 전파되므로 상호 참조가 계속될수록 어디서 오염되는지 파악하기 어려워진다
- Sub Routine Chain
call stack
각 서브루틴의 메모리는 데이터를 return하지 않고 keep 하고 있다 (execution context)
- Tail Recursion (꼬리물기 최적화) 🌟
return point 아래에서는 메모리가 필요없어지므로 우리는 서브루틴을 최적화할 수 있다
→ 더 이상 call stack 이 없어지게 된다!
→ 효율적인 재귀 함수 구현 가능
→ 함수의 return point를 바꿔주는 것은 언어 수준으로만 구현할 수 있다.
- 제어문은 제어문을 통해서 Stack Clear 기능(ex. for문)
- Safari 지원 (call stack 무한대 가능) / Chrome, Edge 미지원
const sum = v => v + (v > 1 ? sum(v -1) : 0);
sum(3);
sum(v:3)
return 3 + sum(2)
sum(v:2)
return 2 :+ sum(1)
sum(v:1)
return 1+0모든 연산자는 스택 메모리를 유발하여 꼬리물기 최적화를 방해한다.
// 해당 재귀함수는 꼬리물기 최적화를 할 수 없다
// return point로 돌아와서 값을 더해주고 return해야 하기때문
// return과 함수 call만 남아야 tail recursion 가능
// JS에서 stack 메모리를 쓰지 않아 꼬리물기 최적화를 지원해주는 함수
// : 삼항 연산자, OR 연산자, AND 연산자
// 즉, tail recursion을 위해 연산을 인자로 넘긴다!
const sum = (v, prev = 0) => {
prev += v;
return (v > 1 ? sum(v-1, prev) : prev);
};
sum();
sum(v:3, prev:0)
return sum(2,3)
sum(v:2, prev:3)
return sum(1,5)
sum(v:1, prev:5)
return 6
- Tail Recursion To Loop
꼬리물기 최적화만 잘 짜놓으면, 재귀함수는 기계적으로 루프로 무조건 바꿀 수 있다
꼬리물기 최적화가 불가능한 언어의 경우, 굉장히 큰 재귀함수가 stack overflow로 죽어버리니까 우리는 기계적으로 루프문으로 바꿀 줄 알아야한다
// before(tail recursion)
const sum = (v, prev = 0) => {
prev += v;
return (v > 1 ? sum(v-1, prev) : prev);
};
// to loop!
const sum = (v) => {
let prev = 0;
while(v>1){
prev += v;
v--;
}
return prev;
}
- Closure 🌟🌟
- Static State : 루틴(함수)를 정적인 문으로 만드는 언어들의 특징
- Runtime State : 런타임 실행 중간에 루틴(함수) 생성Global > Main Flow > Routine
- 루틴을 만들면 내부 지역변수는 어디서 만들어진건지 같이 기록하는데 이것이 scope
- 내가 어떤 flow 상에서 만들어진건지 루틴이 알고있다
- 노란 박스에는 없는데 바깥에 있고 루틴이 인식하는 변수는 모두 자유변수
- 루틴에서 접근하는 바깥 스코프의 변수는 모두 free variables(자유변수)
- 루틴이 물고있기 때문에 자유변수는 수정이 불가능하다
- 루틴 안에서 자유변수를 다뤄서 자유변수가 변하지 못하게끔 갇히게 된다.
- Free variables가 close됐다 —> closure
- → 오직 런타임 중에 루틴을 만들 수 있는 언어에서만 발생한다
- Nested Closure(중첩된 클로저)
// closure chain
window.a = 3;
if(a == 3){
const b = 5;
const f1 = v => {
const c = 7;
if(a + b > c) {
return p => v + p + a + b + c;
} else {
return p => v + p + a + b;
}
};
}
- Shadowing
- 상위 변수명과 현재 루틴 변수명이 겹치면 가장 가까운 변수를 closure로 사용 = shadowing
- 중첩된 클로저에서 서브 루틴으로부터 자유 변수를 보호하는 유일한 방법
- namespace를 정의할 때 써야한다! (자유변수의 권한을 보호하기 위해)
const a = 3; // block if(a == 3){ const a = 5; // block const f1 = v => { const a = 7; console.log(a); }; }
- Co Routine
const generator = function*(a){ a++; yield a; a++; yield a; a++; yield a; }; const coroutine = generator(3); let result = 0; result += coroutine.next().value; // coroutine().value; console.log(result); // 4 result += coroutine.next().value; console.log(result); // 9 result += coroutine.next().value; console.log(result); // 15
- 서브루틴에서 yield 사용해서 suspesion(일시정지) 발생
- yield 지나고 난 뒤 coroutine.next() 형태로 다음 루틴 접근 가능
- 점점 공유해야 할 변수가 많아지면 여러 함수를 만들어야 하는데, yield를 쓰면 같은 함수 내에서 지역 변수 메모리가 해지되지 않은 채 변수 공유, 상태 유지하며 사용할 수 있다
- 우리가 짠 코드의 이해도 상승
- 루프 함수(재귀 함수)를 원하는 만큼만 동작시킬 수 있도록 제어 가능
- 즉, 꼬리물기 최적화를 통해 루프로 만들면 이를 제어할 수 있다