소프트웨어 개발에서 소스코드는 특정 기능을 수행하거나(function), 특정 작업을 수행하는데 필요한 모든 것을 포함하는 구성요소(object)로 구성할 수 있습니다. Modular programming은 이러한 개념을 사용한 프로그래밍 방법입니다.
소프트웨어 공학에서 말하는 module design pattern은 modular programming에서 의미하는 module 개념이 지원되지 않는 언어에서 module을 사용하기 위해서 사용하는 pattern입니다. Module 이라는 개념은 모든 언어에서 지원되지 않기 때문입니다.
여러 종류의 언어에서 module design pattern을 구현하는 방법들이 있지만, 이 글에서는 Prototype-based language인 JavaScript에서의 module design pattern에 대해서 알아보겠습니다.
Module pattern은 원래 2003년 Richard Cornford를 포함한 많은 사람들에 의해 개발되었다고 합니다. 후에 Douglas Crockford의 강의에서 대중화되었습니다.
Module
Modules are an integral piece of any robust application’s architecture and typically help in keeping the units of code for a project both cleanly separated and organized.
Learning JavaScript Design Patterns 에서는 module을 위와 같이 정의합니다.
이를 번역하면 다음과 같습니다.
Module은 강력한 애플리케이션 아키텍처의 필수적인 요소이며, 일반적으로 프로젝트의 코드 단위를 깨끗하게 분리하여 구성하는 데 도움이 됩니다.
위의 정의에서 가장 중요한 부분은 프로젝트의 코드 단위를 깨끗하게 분리하여 구성 한다는 것입니다. JavaScript에서의 module은 Class라고 볼 수 있습니다. OOP에서 말하는 class의 특징중 하나는 encapsulation인데, 이는 특정 class의 member들을 다른 class에서 접근하지 못하도록 보호합니다. Module pattern은 ES6이전 class가 지원되지 않던 JavaScript에서도 class의 member에 대한 private과 public 설정을 가능하게 하였습니다.
Privacy
Module Pattern은 Closure{}를 사용해서 private 하거나 public 한 member variable/method들을 감싸서 외부로 유출되는 것을 방지함으로써 실수로 다른 개발자의 interface와 충돌하는 것을 막을 수 있습니다. Module Pattern을 사용해서 return 된 object는 public 한 API만 외부에서 볼 수 있게 하며 나머지 variable 및 method들은 (private 한) 비공개 상태가 됩니다.
이 pattern은 application이 아무리 무거운 작업 중이라도 public 한 API만 사용할 수 있도록 하며, 숨기길 원하는 logic은 외부에서 절대 접근할 수 없도록 도와줍니다. 이 pattern은 IIFE(Immediately invoked function express)를 사용해서 object를 return 합니다.
JavaScript는 다른 언어들과는 다르게 Privacy를 명시하지 않습니다. 그렇기 때문에 변수들에 private/public을 명시할 수 없지만, function scope를 사용해서 Module pattern의 개념을 흉내 낼 수 있습니다. Closure 덕분에 module 내에서 선언된 variable/method들은 module 내에서만 사용 가능하며, return 되는 object 내에 정의된 variable/method들만 외부에서 사용할 수 있습니다.
The Module Pattern
Module pattern은 원래 기존 소프트웨어 공학의 class에 대해서 private/public encapsulation을 제공하는 방법으로써 정의되었습니다.
JavaScript는 자체적으로 Class 개념이 없었고(ES6부터 사용하고 있지만 syntactic sugar) 특정 object의 member에 대한 private/public 설정 방법을 제공하지 않았습니다. 그래서 JavaScript는 Module pattern을 사용해서 object 내의 member variable/method들을 private/public 설정할 수 있도록 Class 개념을 모방 할 방법을 찾게 되었습니다. 덕분에 다른 object 혹은 script 내부의 member들과 충돌할 가능성이 줄어들게 되었습니다.
Module pattern의 코드를 살펴보겠습니다.
// using IIFE
var testModule = (function () {
var counter = 0;
return {
incrementCounter: function () {
return counter++;
},
resetCounter: function () {
console.log("counter value prior to reset: " + counter);
counter = 0;
}
};
})();
// namespace: testModule
testModule.incrementCounter();
testModule.resetCounter();
위의 코드는 Module pattern을 구현한 것입니다. incrementCounter와 resetCounter method 내부의 값은 외부의 다른 코드들이 절대 접근할 수 없습니다. counter 변수는 private 하게 동작되어 외부에서 절대 접근할 수 없고, 외부에서 접근할 수 있는 것은 두 method뿐입니다. 이 두 method는 namespece 되어있기 때문에, 해당 method들을 사용하는 부분에서는 module의 이름을 prefix로 붙여서 사용해야 합니다.
Module pattern을 처음 사용할 때, private/public을 구분해서 좀 더 쉽게 사용할 수 있도록 도와주는 template은 아래와 같습니다.
// using IIFE
var myNamespace = (function () {
var myPrivateVar, myPrivateMethod;
myPrivateVar = 0;
myPrivateMethod = function (foo) {
console.log(foo);
};
return {
myPublicVar: "foo",
myPublicFunction: function (bar) {
myPrivateVar++;
myPrivateMethod(bar);
}
};
})();
위의 예제 코드에서, IIFE는 object를 return하고, 이 object는 myNameSpace에 할당됩니다.
장점
Module pattern의 장점은 크게 두 가지로 볼 수 있습니다. 첫 번째로, 적어도 JavaScript에서는, OOP에 익숙한 개발자들에게 encapsulation에 대한 개념을 명확하게 줄 수 있습니다. 두 번째는, 앞서 계속 말했던 private을 제공한다는 것입니다. Module 내부에서는 private member에 접근할 수 있지만, 외부에서는 module의 private member에 절대 접근할 수 없습니다.
단점
첫 번째로, private/public이 설정된 member에 대해서 visibility를 변경하려면, 그 member가 사용된 혹은 사용될 모든 곳을 변경해야 한다는 것입니다. 즉, API를 변경하면 그 API가 사용된 부분도 모두 손을 봐야 한다는 것입니다. 두 번째로, module이 선언된 후에 method를 추가했을 경우, 해당 method는 private member에 접근할 수 없다는 것입니다. 물론 이런 경우가 생기지 않도록 module pattern을 제대로 사용한다면 문제가 생기지 않을 것입니다. 세 번째로, private member에 대한 unit test를 할 수 없다는 문제가 있습니다. (bug가 발생해서 hot fix를 해야 할 때, private member를 변경하는 것은 불가능에 가깝기 때문에 연관된 모든 public member들을 고쳐야 하는 단점이 있다고 하는데, 이는 이해가 잘 안 된다. Private member의 변경이 가져오는 side-effect를 모두 잡기 힘들다는 뜻인가?)
The Revealing Module Pattern
Revealing module pattern은 module pattern을 조금 개선한 것입니다.
아래의 코드는 revealing module pattern을 보여줍니다. 모든 public method/variable이 return되는 object literal에 있던것과 다르게, module의 본문에서 모두 구현이 됩니다. 그중에서 export하고싶은 method/variable을 원한다면 이름을 바꿔서 object literal로 return할 수 있습니다.
var myRevealingModule = (function () {
var privateCounter = 0;
function privateFunction() {
privateCounter++;
}
function publicFunction() {
publicIncrement();
}
function publicIncrement() {
privateFunction();
}
function publicGetCount(){
return privateCounter;
}
// Reveal public pointers to
// private functions and properties
return {
start: publicFunction,
increment: publicIncrement,
count: publicGetCount
};
})();
myRevealingModule.start();
장점
이러한 pattern을 통해 스크립트 구문이 보다 일관적으로 이루어질 수 있습니다. 또한 module의 마지막 부분에서(return object literal) 어떤 method/variable이 public으로 설정되어 있는지 명시되어 있기 때문에 가독성을 증가시킵니다.
단점
이 pattern의 단점은 다음과 같습니다.
var rmpUrlBuilder = (function() {
var _urlBase = "http://my.default.domain/";
var _build = function (relUrl) {
return _urlBase + relUrl;
};
return {
urlBase: _urlBase,
build: _build
}
})()
rmpUrlBuilder.urlBase = "http://stackoverflow.com";
// prints "http://my.default.domain/questions" not "http://stackoverflow.com/questions"
console.log(rmpUrlBuilder.build("/questions"));
위의 코드에서 revealing module pattern의 단점을 보여주고 있습니다. _urlBase와 _build는 object literal에서 public으로 명시되어서 return 됩니다. 그 후에 public variable인 urlBase 값을 변경했지만, 이 variable을 사용하는 build method에서는 변경된 값이 적용되지 않았습니다. 이는 원래의 module pattern에서는 생기지 않았던 문제입니다. 그러므로 revealing module pattern으로 만들어진 module이 원래 module pattern으로 만들어진 module보다 더 취약할 수 있으므로 사용 중에 주의해야 합니다.