JavaScript

ES6+ 함수와 자바스크립트 OOP의 원리

realhee 2022. 2. 23. 00:44
반응형

코드스피츠 강의를 듣고 복습하기 위해 포스팅을 정리해보았다.

 

사실 자바스크립트 엔진과 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를 쓰면 같은 함수 내에서 지역 변수 메모리가 해지되지 않은 채 변수 공유, 상태 유지하며 사용할 수 있다
    • 우리가 짠 코드의 이해도 상승
    • 루프 함수(재귀 함수)를 원하는 만큼만 동작시킬 수 있도록 제어 가능
    • 즉, 꼬리물기 최적화를 통해 루프로 만들면 이를 제어할 수 있다
반응형