[jsdp] 객체지향 자바스크립트와 디자인패턴 #1

굉장히 방대한 주제인 객체지향과 자바스크립트를 결부한 시리즈 글입니다. 부족한 점이 많지만 그렇다고 정리하지 않으면 앞으로 나갈 수도 없겠죠. 꽤나 긴 시리즈라 약 8편 정도로 보고 있습니다.

객체지향적으로 자바스크립트를 바라보고 자바스크립트의 여러가지 특징을 어떻게 사용할 것인가를 다룹니다. 특히 디자인패턴을 중심으로 살펴보게 됩니다.

긴 호흡으로 같이 해주시면 감사하겠습니다.

 
 

남에게 코드를 설명할 때의 정당성top

비슷한 혹은 상이한 수준의 개발자가 자신의 코드가 옳다고 주장하면 무엇을 근거로 어느 쪽이 옳은건지 증명해야할까요?(물론 둘다 오류가 없다는 가정 하에 서로의 자부심이 가득하다면 말이죠.)

이 때는 가정으로부터 큰 모양을 만드는 서양식 철학사조가 매우 큰 역할을 해줍니다. 일종의 정교한 사기죠.

제 경우는 근대 철학사조와 켄트백옹의 모델을 섞어서 돈과 결부시키는 걸로 설득합니다. 그 모양은 다음과 같습니다.
Screenshot_2

기초철학부분

간략히 설명하면 우선 가장 기저에는 상대주의와 합리주의를 깔아둡니다. 이 개념은 매우 유효한데 부모-자식, 서비스-호스트, 풀-푸쉬 등의 쌍은 전부 상대적입니다. 어떤 입장에서 보는가에 따라 부모가 자식이기도 하고 자식이 부모이기도 한 셈입니다. 상대주의는 무엇보다 OOP에 중요한 추상층 개념에 큰 기둥을 세워줍니다. 이에 비해 합리주의는 기준점을 세울 때의 근거로 더할나위 없습니다. 왜 그 녀석을 중심으로 설계했는가 따위를 설명하는 모델로 합리주의적인 사고관이 쓸모있습니다.

실행시 고려할 부분

가치(value), 원칙(principle)은 켄트백옹의 구현패턴에서 빌려왔습니다. 가치는 결국 돈으로 환산되고 100%인건비 사업인 개발분야에서 돈은 시간과 동의어입니다. 따라서 개발하는 시간을 줄일 수 있는데 일조한다면 전부 가치라고 볼 수 있겠죠. 원칙 또한 가치에 기여하지만 특정 범위의 집단이 무조건적으로 지키는 프로토콜이라 이해할 수 있습니다. 원칙은 원칙으로서의 가치를 발휘합니다. 그리고 객체지향이니 당연히 객체지향의 도메인지식에 따른 다양한 세부사항이 있습니다. 5대 원칙이나 DI같은 것들이죠.
결국 본인의 코드가 철학적으로도 근거가 있고 실행 시에 이러한 요소를 갖고 있다는게 본인 코드의 정당성을 설득하기에 좋은 툴이 됩니다.

인간의 최종 선택지

그렇다곤 해도 인간이 수행합니다. 하고 싶지 않으면 안한는거고 한 번 싫다고 생각하면 무조건 싫은게 또한 인간입니다. 인간이야말로 개발자의 실체인 것이죠. 따라서 동기부여는 언제나 중요합니다. 위의 그림에는 대표적인 돈만 썼습니다만 그 외에도 직위, 미래, 취미, 취향 등이 전부 반영됩니다. 하지만 설득하는 층의 가장 마지막에 있는 만큼 그 설득하는 힘도 가장 떨어집니다. 왠만하면 동기점으로 남에게 자신의 코드를 설명하지 않는게 좋겠죠. 이 레이어의 설득은 주로 경영진이나 영업부서에서 합니다(돈이되요, 고객이 원해요..머 이런거죠)

최근 이 레이어의 설득 로직은 파울러옹같은 전설의 아키텍트가 개발자들을 배신하고 컨설팅을 주업으로 하시는지 DSL같은걸 널리 퍼트리면서 점점 정교해지고 있습니다.

 
 

개인적인 디자인패턴의 지향점top

객체지향을 통해 설계한 객체관계구조에서 디자인패턴은 매우 교과서적인 해법을 보여줍니다. 그 해법을 해석하는 방식은 사람마다 다른데 제 경우에는 매우 중요한 네 가지 지향점으로 분석합니다.

Screenshot_1

형독립성

디자인패턴에서는 특정 구상형에 의존하는 코드를 지양합니다. 해법으로 당연히 추상형을 추천합니다. 근데 자바스크립트는 엄격한 형관리 언어가 아니라서 딱히 이 부분을 크게 신경쓰지 않습니다. 따라서 이후 이어지는 시리즈 포스트에서도 딱히 이쪽 스킬을 중심으로 다루지는 않을 예정입니다.

문(statement)을 객체로 대체하기

제 입장에서 바라본 디자인패턴의 절반은 기존의 제어문을 객체구조로 대체하는 과정입니다. 자바같은 언어에서는 이 과정에서 숱한 커스텀 클래스가 탄생하지만 자바스크립트는 함수를 객체로 쓸 수 있어서 매우 간단하게 처리되는 장점이 있습니다. 이 부분을 매우 중심적으로 다룰 예정입니다.

확정적 코드

지속적인 통합이 각광받는 이유를 반대로 말하자면 컴파일을 성공시키는게 쉽지 않기 때문입니다. 이미 컴파일에 성공한 적이 있는 파일을 수정한 뒤 혹은 심지어 수정도 하지 않고도 다시 컴파일했을 때 잘된다라는 보장을 하기가 쉽지 않습니다. 지속적인 통합모델에서는 항상 컴파일을 시켜보면서 감시하는 식으로 문제를 해결합니다.

하지만 디자인패턴에서는 변화하는 부분과 아닌 부분을 확실하게 구분지어서 변화하지 않는 부분을 건드리지 않게 보호해줍니다. 켄트백옹이 말한 코드의 변화율관리쪽에서 변화율에 따른 코드를 몰아주는 효과를 극대화해서 확장되거나 세부기능이 추가되는 부분과 그렇지 않은 기저 부분을 분리하여 관리하게 해주죠. 따라서 디자인패턴을 심하게 쓰면 쓸수록 확정된 코드를 더 많이 얻을 수 있습니다.

또한 단지 기저층이 아니라 호스트코드의 일관성을 가져올 수 있습니다. 추상층이 변화되므로 실제 이를 사용하는 코드 상에서는 아무런 변화도 없는 것이죠. 예를들어 그림을 그리는 로직은 동일한데 렌더러가 canvas냐 webgl이냐만 결정해주면 똑같은 그림이 각기 다른 층에서 그려지는 것을 생각해볼 수 있습니다.

런타임 결정

자바스크립트야 정의에서부터 실행까지 전부 런타임에 이루어지지만 컴파일 타임에 런타임에 일어날 모든걸 결정해야하는 컴파일언어 입장에서는 런타임에 동적으로 무언가를 바꾸고 싶을 때가 많습니다.

최근 스프링등에서 리플렉션을 극도로 활용하는 것도 결국 이 현상이 더욱 심해졌음을 의미하는 측면도 있습니다. 디자인패턴은 전체 아키텍쳐를 컴파일타임에 확정짓지 않고 런타임에 확장할 수 있는 방법을 만드려는 방향성을 갖고 있습니다. 자바스크립트에 이를 이용하면 고정된 js파일과 확장된 js파일을 분리할 수 있는 구조를 제공하게 됩니다.

문을 객체화하기top

위의 네 가지 지향점 중 가장 중심으로 보는 이번 시리즈의 목적은 문을 객체로 대체하는 것입니다.

디자인패턴을 최초 소개할 때 단골로 등장하는 전략패턴(아마 헤드퍼스트때문에 더욱 심해진게 아닐까도 생각해봤습니다^^)을 제어문에서 서서히 객체구조화해가보죠. 우선 간단한 조건문에 기반한 함수의 작동을 보겠습니다.

function attack( weapon, option ){
	switch( weapon){
	case'sword':
		if( option ){
			//....
		}else{
			//....
		}
		break;
	case'bow':
		if( option ){
			//....
		}else{
			//....
		}
		break;
	}
}

공격을 실행하는 attack 함수는 무기타입과 옵션에 따라 다른 형태의 공격을 감행합니다. 이 함수의 문제는 관리가 어렵다는 것인데 구체적으로는 다음과 같습니다

  1. 확장이 어렵다 – 새로운 무기나 옵션이 추가할 때마다 코드가 길어지고 조건이 중첩된다.
  2. 수정이 어렵다 – attack 함수를 배포한 뒤에는 새로 attack 함수를 배포할 수 있을지 장담할 수 없고 특정 부분을 고친 뒤에도 전체 attack 함수의 작동이 전부 정상인지 담보할 수 없다.

전부를 한 번에 수정하면 어려우니 우선 각 무기별 처리를 함수로 빼보겠습니다.

function sword( opt ){
	if( opt ){
		//...
	}else{
		//...
	}
}

function bow( opt ){
	if( opt ){
		//...
	}else{
		//...
	}
}

이렇게 무기별로 분리되었으니 이제 attack 함수는 더이상 opt 에 대한 처리를 하지 않게 되었습니다.

function attack( weapon, opt ){
	switch( weapon ){
	case'sword': sword( opt ); break;
	case'bow': bow( opt ); break;
	}
}

하지만 자바스크립트에서 함수는 객체로서 인자에 보내질 수 있습니다. 그렇다면 sword 나 bow 함수를 직접 보내는 형태도 생각해볼 수 있을 것입니다.

function attack( weapon, opt ){
	weapon( opt );
}

attack( sword, 3 ); //직접 sword함수를 보냄!

하지만 이 마법은 실은 마법을 일으키지 않습니다. 프로그래밍에서 조건문이 등장하는 이유는 조건문이 필요하기 때문입니다. 제가 attack에서 조건문을 제거했다면 그 조건문은 어디로 간걸까요?

함수에서 빠져나와 함수를 호출하는 호스트코드로 이사가게 됩니다. 즉 다음과 같이 됩니다.

//이제 조건문은 함수 안에 있지 않고 호출하는 쪽으로 이동했다!
switch( weaponType ){
case'sword': attack( sword, opt ); break;
case'bow': attack( bow, opt ); break;
}

물론 해시맵을 이용하면 약간 완화할 수 있습니다. 조건문을 제거하는 유일한 방법으로 발견된 것은 라우팅테이블입니다. 간단히 무기함수의 라우팅테이블을 구성해 switch 문을 제거해보죠.

var weapons = {
	'sword': sword,
	'bow': bow
};

weapons[weaponType]( opt );

지금까지 일어난 코드의 변화에서 중요한 몇 가지 개념이 도출됩니다.

  1. 개별 로직을 함수로 분리한다(sword, bow함수)
  2. 조건문이 함수 내부에 있다면 외부로 옮긴다.
  3. 함수 외부의 조건문은 취약하므로 설정 기반의 라우터테이블로 변경하여 안정성을 높인다.

하지만 동일한 작동을 하는 코드를 이러한 객체구조로 바꾸면서 손쉽고 안전하게 무기를 추가하거나 무기의 옵션에 따른 기능을 추가할 수 있게 되었습니다.

즉 디자인패턴이나 객체지향구조는 사람을 위한 것입니다. 사람이 한 번에 감당할 복잡성을 줄이기 위한 장치입니다. 사실 컴터입장에는 최초의 코드가 더욱 빨리 실행됩니다.

다중포인터 참조와 함수스택구조라는 비용을 지불하고 객체구조물을 만들어 개별 로직을 격리하고 변수의 범위를 한정짓는 것으로 사람의 두뇌가 감당해야할 양을 줄여줌으로서 더욱 복잡하고 큰 프로그램을 짤 수 있게 해주는 것이죠.

지금까지의 과정으로 최초 이 단락의 주제였던 문(switch문)을 객체화 하는데 성공했습니다. 별거 아니죠 ^^;

 
 

2단 참조top

디자인패턴은 자바를 위해 만들어진 것이 아닙니다. 어찌보면 제 입장에서는 기존의 c에서 사용하던 다양한 포인터 기법을 자바에서는 사용할 수 없으니 어떻게 하면 포인터에 의존적이지 않는 로직으로 바꾸지 라는 고민의 결과물로 보일 때가 많습니다. 포인터의 기초적인 기술로 2단 참조라는 방법이 있습니다.

이는 간단히 예를 드는게 더 쉽습니다. 우선 기본적인 참조변수의 문제점을 살펴보죠.

var a = {}; //우선 a에 객체 참조를 넣었다

var b = a; //b는 a를 참조로 잡고 있다

a = []; //a는 새로운 객체의 참조를 잡고 있다.

console.log( b );//하지만 여전히 b는 최초의 {}를 참조로 잡고 있다

즉 b는 최초 a로부터 만들어진 참조변수인데 이 페어링(pairing)은 a가 갱신되면서 깨져버립니다. a의 변화에도 a, b간의 페어링을 유지하려면 어떻게 해야할까요?

var a = {data:{}};

var b = a;

a.data = []; //b.data == []

그렇습니다. 간단히 말하자면 a가 직접 객체를 참조하지 않고 그 객체를 참조하는 속성을 갖는 껍데기 객체를 참조하면 됩니다. 그러면 직접 대상 객체를 바꾸지 않고 속성의 참조를 바꾸게 됩니다.
b입장에서는 껍데기 객체인 a의 참조값이 변하지 않으므로 안전하게 변경된 data를 참조할 수 있습니다.

앞의 것과 비교해서 얻은 것과 잃은 것을 생각해볼까요?

  1. 얻은 것 – a의 진짜 객체가 새로 지정되어도 b가 페어링을 유지할 수 있다.
  2. 잃은 것 – 실행시 매번 참조의 참조를 통해 객체를 얻어야하므로 포인터 연산비용이 두배가 된다.

많은 경우 잃은 게 큰 문제가 안될 수도 있지만 성능기반의 그래픽스나 게임에서는 문제를 일으키기도 합니다.

이러한 응용은 단지 2단참조가 아니라 3, 4, 5 단으로 몇 단계나 깊이를 만들 수 있습니다. 각 객체의 변화율과 페어링을 어디까지 유지해야 하는가의 문제입니다.

이런 얘기를 왜 드리는가 하면 디자인패턴의 태반이 바로 이 기술을 사용합니다. 데코레이터패턴, 어뎁터 패턴 등 다수의 패턴이 바로 이 방법을 사용하게 됩니다..랄까 자바가 전반적으로 상속보다는 소유를 통한 구상을 권유하는 모든 트랜드는 실은 이 기술에 근간을 두고 있습니다.

또한 바로 이 기능이야말로 인터페이스 기술의 근간입니다. 외부에는 인터페이스를 노출하고 실제로는 구상함수가 작동하는 기능은 실은 2단참조 포인터에 의한 작동입니다. 이를 실제로 구현해보죠.

우선 간단히 run이라는 인터페이스를 갖는 그릇을 생각해보죠.

var interfaceTest = {
	run: null
};

간단히 테스트용으로 작성된 위의 인터페이스에 최초 지정된 run메서드의 역할은 인자로 함수를 받아 이후 그것을 본인의 run으로 교체해주는 기능만 제공합니다. 따라서 간단히 두배를 해주는 함수를 대입해보죠.

function double( val ){
	return val + val;
}

interfaceTest.run = double; //함수를 할당하여 run의 대상을 교체함

interfaceTest.run( 3 ); //6

최초 run에 함수를 대입하는 순간 더이상 run은 기존의 메서드가 아니라 double함수로 교체됩니다.

이러한 기능층은 일반적으로 추상층으로 감싸서 실제 사용자에겐 노출하지 않는게 안전해집니다. 따라서 인터페이스를 감추는 객체를 하나 만들어보죠.

var action = function( val ){
	interfaceTest.run( val );
};

action.change: function( runner ){
	interfaceTest.run = runner;
};

이제 사용자는 손쉽게 자신만의 run함수를 할당하는 구조에서 직접적인 코드에 노출되지 않고 함수형태로 작성하게 됩니다. 자바스크립트는 스코프를 통해 변수나 객체를 보호합니다. 따라서 이를 반영한 최종적인 action의 코드는 다음과 같습니다.

var action = (function(){
	var interfaceTest = {run: null};
	var action = function( val ){
		interfaceTest.run( val );
	};

	action.change = function( runner ){
		interfaceTest.run = runner;
	};

	return action;
})();

이제 change를 이용해 함수를 할당하고 사용해보죠.

action.change( function( val ){
	return val * val;
} );

action( 5 ); //25

action.change( function( val ){
	return val + val;
} );

action( 3 ); //6

이제 코드는 완전히 분리되어 변화하지 않는 action과 action에 함수를 지정하는 change설정부분, 마지막으로 이를 활용하는 호스트 코드로 나뉘어집니다. 위의 change코드를 개별 js로 만들어보죠.

//square.js

action.change( function( val ){
	return val * val;
} );

----------------------------------

//double.js

action.change( function( val ){
	return val + val;
} );

square.js, double.js 이렇게 두 개의 파일과 action을 담고 있는 act.js까지 세개의 파일이 생성되었습니다.

마지막으로 이를 모아서 각기 다른 html 에서 사용해봅시다.

Screenshot_3

위의 그림에서 act.js를 부르는 부분은 양쪽 html에 공통되고 act.js의 코드도 변하지 않습니다. 즉 레이어를 분리하여 수정하지 않아도 되는 부분을 분리해낸 셈이죠.

그에 비해 파란색으로 부른 script는 양쪽의 html에서 다른 부분입니다. 바로 이 부분이 런타임에 호스트코드에서 어떤 기능을 쓸지 선택한 부분입니다.

마지막으로 실제 이러한 라이브러리를 직접 사용하는 호스트코드는 오히려 동일합니다. 변화를 수용하는 기능층이 바뀌고 호스트코드가 동일하도록 고정된 것이죠. 물론 결과는 서로 다르게 나오게 되겠죠.

 
 

결론top

이 후의 시리즈 포스트에서는 이를 더욱 발전시켜 각 디자인패턴이 어떻게 다양한 제어문을 제거해가고 자바스크립트의 특징을 이용한 객체구조로 변경되어가는지 차근차근 살펴보겠습니다.