블로그 내 검색

2013. 4. 30.

[Book :: Effective Java] 자바에서의 전략 패턴 구현과 함수객체

What is a 'Function-Object'?

자바는 객체지향을 구현하기 위해 모든 것을 클래스라는 일종의 인스턴스 템플릿으로 구현하도록 되어 있다.

단순한 유틸성 기능을 구현할 때에도 XXXUtils 이라는 뭔가 우주에서 날아온 듯한 이름으로 클래스를 만들고 그 안에 static을 사용한 메서드로 기능을 구현한다.

클래스는 자바에서 절대적이다.
자바는 언어 차원에서 모든 걸 클래스화 해야만 하며, 이러한 점은 여러 상황에서 불편한 점이 있을때가 많다.

간단한 예를 들어보자.
공식 API의 Camparator 는 정렬을 위한 함수 객체 인터페이스이다. 자바에서는 정렬 전략으로 사용하기 위해 저 클래스를 풀로 구현해야 한다.

이런 식으로 말이다.
// 이러한 문법적 껍데기를 반드시 선언해야 한다.
public class StringLengthComparator implements Comparator {

    // 우리가 필요한건 이 메소드의 기능 뿐인데도...
    public int compare(String s1, String s2) {
      return s1.length() - s2.length();
    }
}
// 아래와 같이 사용
Arrays.sort(array, new StringLengthComparator());
하지만 일부 언어에서는 함수 포인터(function pointer), 위임(delegate), 람다식(lamda expression) 또는 이와 유사한 기능을 제공하여 프로그램에서 특정 함수의 호출을 저장하거나 전달할 수 있다.
// C의 함수 포인터 방식
#include <stdio.h>
void hello(char *name) {
    printf ("Hi %s\n", name);
}

int main() {
    // 반환값이 void 이고 매개변수가 캐릭터인 함수 포인터 선언
    void (*Func)(char *);

    // 그 포인터에 hello 가르키게 함
    Func = hello;

    // Func 는 변수로 사용할 수 있지만 본질은 함수이다.
    // 함수 포인터 실행
    Func("test");
}

/* ============================================================ */

// C#의 위임 방식
// delegate 타입 선언
public delegate void DoSomething(string command);

// 위임할 메서드
public void Action(string direction){
    // 메서드 구현
}
...
...
// delegate 생성
DoSomething done = new DoSomething(Action);

// 위임 객체를 통해 변수로서 함수를 취급가능하다.
done("left");

/* ============================================================ */ 

// 자바스크립트의 익명 람다 함수
// each의 람다 함수를 지정
$.each(function(key, value) {
    // each 구현
});
그렇다면, 위 기능을 새로 구현해보자.
javascript 에서는 익명 함수를 지원하며, 위와 같은 구현이 아주 간단하다.
// 익명 함수를 사용한 배열 정렬법
arrayObject.sort(function(s1, s2) {
    return s1.length - s2.length;  
})
자바는 위 예제의 자바스크립트처럼 함수(혹은 메서드)만을 생성할 수 없고 반드시 클래스와 쌍으로 움직여야 한다.

그래서 새로운 업무 방식이 생겨 객체에 특정한 알고리즘을 교체하려면 아예 그 객체를 새로 만들거나(설마..), 그 업무 방식에 맞는 새로운 '알고리즘을 구현한 메소드를 포함한 클래스' 를 생성하여 처리 메서드로 전달해야 한다.

위에 굵은 글씨로 표현한 클래스 객체를 '함수 객체' 라고 부른다.

보통은 메서드를 하나만 가지는 인터페이스 형식으로 구현되나, 이 함수 객체를 사용하는 측에 따라서는 메서드 여러개가 구현되어 있을 수도 있다.

전략 패턴 (Strategy Pattern)

함수 객체가 제일 많이 쓰이는 곳은 역시 전략 패턴이다.

전략 패턴(Strategy Pattern) 은 기본적인 목적인 비슷하지만 흐름에서 변화하거나 교체할 수 있는 부분을 따로 분리하여 쉽게 변화시키고 새로 지정할 수 있게 해주는 패턴이다. 여러 메서드를 하나의 클래스에 집어넣은 것보다 유연성이 좋고 잘 디자인 된 전략은 다른 객체에서도 활용할 수 있다.

자바의 전략 패턴은 함수 객체를 사용해야 가능하다.
위에 예시로 든 Comparator 는 전략 패턴의 모범생 같은 구현이라고 볼 수 있겠다.

이제 예제와 함께 더 살펴보자.
만일 한 어느 학원 수강생 처리시스템에서 모든 등록 수강생의 주소를 새로운 주소로 바꾸는 작업을 해야 한다고 가정한다.

보통은 이렇게 처리할 것이다.
public List<Student> changeNewAddress(List<Student> student) {
    for(Student s : student) {
        String newAddr = getNewAddress(s.getAddress);
        s.setAddress(s);
    }
    return student;
}
수강생의 주소를 바꾸는 일 외에 학생들의 프로필 사진도 전부 섬네일화 해야 하는 작업이 생겼다고 가정해보자. 

그렇다면 간단한 구현으로는 새로운 createProfilePhotoToThumb 메소드가 생길 것이다.

하지만 이렇게 접근하지 말고 다른 방법을 써 보자.

일단, 하려는 작업은 둘다 학생에 대한 배치 처리이다.
학생 하나하나를 순회하며 해당 학생의 특정 필드에 대해 작업을 수행한다.
여기서 "새로운 주소 변경" 과 "섬네일 만들기" 는 전략으로 볼 수 있고 위에서 설명한 함수 객체로 구현해볼 수 있다.

그렇다면 먼저 함수 객체 인터페이스는,
interface Transfer<T> {
    T transfer(T value);
}
그렇다면 전략 객체 구현은,
// 들어온 주소 문자열을 새 주소 변경하는 함수 객체 클래스.
class ChangeAdressJob implements Transfer<Student> {
    public Student transfer(Student address) {
        // 구 주소를 새 주소로 바꾸는 로직
    }
}

// 들어온 사진을 섬네일화 하는 함수 객체 클래스
class CreateThumbnail implements Transfer<Student> {
    public Student transfer(Student photo) {
        // 프로필 파일을 분석하여 해당 파일을 섬네일을 만듬
    }
}
그렇다면 이제 저 함수 객체를 사용하는 클래스는 다음과 같을 것이다.
class StudentManager {

    private final List&;ltTransfer> transferStrategy = new LinkedList<Transfer>();

    public List<Student> loadStudents() { ... }

    // 배치 처리.
    // 학생 리스트에 대해 전략 함수 객체(Transfer 구현체)를 하나하나 실행함.
    public void executeBatch(List<Student> students) {
        for(Student s : students) {
            for(Transfer t : transferStrategy) {
                t.transfer(s);
            }            
        }
    }
    
    // 전략 객체를 전략 셋에 추가한다.
    public void addTransfer(Transfer t) {
        transferStrategy.add(t);
    }

    public static void main() {

        StudentManager manager = new StudentManager();
        List<student> students = manager.loadStudents();

        manager.addTransfer(new ChangeAdressJob());
        manager.addTransfer(new CreateThumbnail());

        manager.executeBatch(students);
    }
}

2013. 4. 20.

RequireJS – JavaScript 파일 및 모듈 로더


RequireJS?

RequireJS는 JavaScript 파일과 모듈 로더이다.
RequireJS는 브라우저에 최적화되어 있지만 Rhino나 NodeJS등의 환경에서도 사용할 수 있다. RequireJS같은 모듈 로더를 사용하면 당신의 코드의 성능과 품질이 좋아질 것이다
JavaPython 같은 잘 정립된 언어에서는 객체의 모듈화를 언어 자체에서 잘 지원하지만 JavaScript는 그렇지 않다. 

또한, 프로그램이 커질수록 스크립트가 중복되어 발생하는 경우가 발생할 수 있고 그런 경우에 코드를 common.js, event-binder.js 등의 코드로 나누어 관리하는경우가 많아지는데, 이 경우에도 분리한 코드를 잘 로딩하지 않으면 코드간 의존성이 망가지며 프로그램 자체의 구성이 엉성해지는 경우도 많다. 

RequireJS는 자바스크립트 파일을 동적으로 로딩할 수 있고, AMD 모듈화를 적용한 코드라면 모듈로서도 사용할 수 있으며 (그렇지 않아도 사용할 수 있는 방법이 있다 아래에서 더 살펴보자) 자바스크립트 코드간의 의존성도 줄 수 있다. 

또, 앞서 말했듯이 어떠한 자바스크립트 코드가 실행되려면 다른 스크립트가 먼저 로딩되어야 한다거나 하는 경우가 있는데, 자칫 스크립트 로딩의 순서가 꼬일 경우 에러를 뱉어내며 동작하지 않을 수 있다. 

RequireJS를 사용하면 코드간 의존성을 줌으로서 아예 그러한 경우를 막을 수 있고, 좀 더 체계적인 소스코드를 만들어낼 수 있다. 장점만을 죽 늘어놓았는데 자세한건 좀 더 살펴보자.  

JavaScript에게 모듈이란

먼저 모듈에 대해 간단히 짚고 넘어가보자. 

모듈의 개념은 Divide and Conquer 로 설명되는 각 기능(알고리즘)의 분할과 그 분할의 결합으로 생각해볼 수 있다. 

보통의 성숙된 언어에서는 이러한 모듈화를 언어 차원에서 지원하고 있는데, 예를 들어  java의 경우에는 모듈이 instance로 생성되어지며 모듈끼리의 구분은 package로 구분된다. 

그리고 모듈의 구현은 접근 제어자(private, public 등)의 사용으로 캡슐화를 보장하며, 필요한 것만 공개해서 그 모듈을 사용하려는 사용자가 쓸데없이 많은 지식을 가질 필요 없이 부담없이 사용할 수 있다.

JavaScript는 이러한 모듈화나 접근 제어를 언어레벨에서 명시적인 지원을 하지 않으며 package 등의 구분도 없기에... 파일간의 변수 충돌이나 전역공간 변수 난립(global scope pollution)등의 문제가 발생할 요지가 많다. 

보통 이러한 경우를 고려한 JavaScript의 일반적인 모듈 구현은 다음과 같다
(function(exports) {
  "use strict";

  // private 변수
  var name = "I'am a module";

  // 외부에 공개할 public 객체
  var pub = {};

  pub.getName() {
    return name;
  }

  // 외부 공개
  exports.aModule = pub;

})(exports || global);

(exports || global).aModule.getName(); // I'm a module
이러한 구현은 변수를 private 화 할 수 있으며 그로 인한 캡슐화로 모듈 사용이 쉬운 장점이 있지만, 여러개의 모듈을 선언하면서 exports 객체에 프로퍼티가 겹칠 경우 앞서 선언된 공개 속성은 덮어써지는 문제가 있고,  모듈간 의존성이 있을때 의존성을 정의하기가 매우 어렵다. 

그리고 익명 함수와 exports 객체를 사용하는 애매한 코드로 인해 눈에 잘 들어오지 않는다. 이러한 경우를 해결하기 위해 여러 Module Loader가 공개되어 있는데,  그 중 하나가 RequireJS이다. RequireJS에서는 모듈의 고유성과 의존성을 잘 지원하고 있다. 

RequireJS는 AMD 모듈 로딩 표준을 따르기에 기본적으로 모든 모듈이 비동기적이다.

모든 모듈은 비동기적으로 명시적으로 요청하거나 의존성이 있을 때 로딩(Lazy Loading) 된다. 필요한 자바스크립트 파일을 어플리케이션 시작 전 전부 로딩하지 않고, 실제 필요한 때 (사용자의 입력이나 메소드 호출 등의 특별한 경우) 에 로딩하게 할 수 있어서 전체적인 페이지의 속도 향상에도 도움이 된다.  

 

설치법

설치법은 간단하다. 아래와 같이 그냥 스크립트 태그를 쓰면 끝이다.
<script type="text/javascript" data-main="main" src="/js/lib/requirejs.js"></script>
위 태그를 주의깊게 보면 data-main 이라는 속성에 main 이라는 값이 할당되어 있는데, 이 속성은 옵션으로 이 속성을 주게 되면 requirejs가 전부 로딩되면 저 경로의 속성 이름에 해당하는 파일을 자동으로 로딩한 뒤 실행한다. 

경로는 절대 경로가 아니면 requirejs 기준의 상대 경로를 따른다. 모든 모듈의 경로 또한 requirejs가 로딩되는 경로에 상대적이나, 나중에 설정을 통해 바꿀 수 있다. 
  

모듈 이름과 File Scope

RequireJS에서 모든 모듈은 이름이 주어지며, 모듈 이름은 보통 파일 경로가 된다. 

파일 경로는 사실 FileSystem에서 유일하게 존재할 수 있으므로 같은 이름을 가진 모듈이면 모듈끼리 덮어써지거나 충돌할 일이 없어진다. 

예를 들어보자. 

  • 모듈 파일이 /js/controller/main.js 에 존재한다면 모듈 이름은 "/js/controller/main" 이 된다.
  • 모듈 파일이 /js/controller/list.js 로 새로운 모듈을 생성하여 저장하면 역시 모듈 이름은 "/js/controller/list" 가 된다. 
(JavaScript 파일이고, 상대 경로일 경우 .js가 생략된다. )

이제 실제 모듈을 정의하고 사용하는 방법을 알아보자.  

 

RequireJS 모듈 정의 및 사용

이러한 문법을 사용한다.
// 일반적인 RequireJS 모듈 정의
define(function() {

  "use strict";

  var exports = {
    version: "1.0"
  };

  var private1 = "private1";
  var private2 = "private2";

  exports.doSomething = function() { /* ... do something ... */ };
  exports.stopSomething = function() { /* ... stop something ... */ };

  return exports;
});
모듈의 정의에는 define 이라는 글로벌 함수를 사용하며 인자로 함수 하나를 받는데 그 함수에서 반환하는 객체가 모듈이 된다. 

인자 함수는 일종의 객체 팩토리인 셈이다. 

JavaScript는 함수 자체가 스코프를 생성하므로 이 안에서 필요한 만큼의 private 를 선언하고, 외부 공개가 필요한 객체나 함수는 return 으로 반환하면 된다. 다수를 공개하고 싶다면 객체 형식으로 묶어 반환하면 된다. 정리해보면 다음과 같은 코드이다.
// 의존성이 없는 모듈의 정의
define([팩토리 함수]);
그런데 잊은게 있다. 

분명 앞에서 RequireJS는 모듈간의 의존성을 정의하는 방법을 제공한다고 했다. 이대로는 아까 순수하게 JavaScript로 만든 모듈 코드와 별반 다를게 없어 보인다. 

물론 의존성을 줄 수 있다. 

팩토리 함수 앞 인자로 생성될 모듈이 "의존하고 있는 모듈 이름을 문자열로 담은 배열" 을 주면 된다.
// 의존성이 있는 모듈 정의
define([의존 모듈 이름 배열], [팩토리 함수]);
한번 실제 모듈 코드를 만들어 보자. 일단 파일 구조는 다음과 같다고 가정한다.
  • /main.js
  • /App.js
  • /sub/Logger.js
  • /sub/MainController.js
먼저 만들 것은 간단한 로그 모듈이다. 이 코드는 Logger.js 이다
// @file Logger.js
define(function() {

  "use strict";

  var console = global.console || window.console;
  var exports = {
    version: "0.1.0",
    author: "javarouka"
  };

  exports.log = function() {
    var args = Array.prototype.unshift.call(arguments, new Date().toLocaleString());
    console.log.apply(console, arguments);
  };

  return exports;

})
로그 모듈로 일반적인 로그에 앞에 로그가 찍히는 현재 날자를 추가해준다. 

팩토리 함수 안에 var로 선언된 것들은 private 으로 내부에서만 사용하며, exports 객체의 속성으로 지정된 log 함수만 공개되어 외부에서는 log를 통해 이 모듈을 사용할 수 있다. 

이제 이 로그 모듈에 의존성이 있는 모듈을 만들어보자.
// @file MainController.js
// @dependency Logger.js
define(["sub/Logger"], function(logger) {

  "use strict";

  var exports = {
    type: "controller",
    name: "Main"
  };

  var bindEvent = function() {
    logger.log("bind event...")
    // do something
  };

  var view = function() {
    logger.log("render ui");
    // do something
  };

  exports.execute = function(routeParameters) {
    logger.log(exports.name + " controller execute...");
    // do something
  }

  return exports;

});
일단 팩토리 함수 앞에 인자로 의존성 모듈의 이름 배열을 주는데 상대 경로일 경우 .js 확장자를 생략해야 한다. "sub/Logger" 라고 주면 된다. 

주게 되면 팩토리 함수의 인자로 전달되는데 배열에 지정한 순서대로 전달된다. 

이 모듈에서는 logger 라는 변수로 사용하고 있다. 마지막으로 App.js.
// @file ApplicationContext.js
// @dependency sub/MainController.js
define(["sub/MainController"], function(main) {

  "use strict";

  // do something...

});
App.js 는 MainController.js에 의존성이 있다. App.js 가 로딩되려면 의존성에 따라
  1. Logger.js
  2. MainController.js
  3. App.js
순서대로 로딩되게 될 것이다. 

이제 실제 코드를 사용할 main.js 를 작성하자
// @file main.js
// @dependency App.js, sub/Logger.js
require([ "App", "sub/Logger" ], function(app, logger) {

  "use strict";

  app.start();
  logger.log("Application start");

}, function(err) {
  // ERROR handling
});
모듈 정의가 아닌 단순히 코드를 실행할 때는 require 함수를 사용한다. 

require 는 define과 비슷하게 첫번째 인자로 의존성, 두번째 인자로 실행 코드 함수, 세번째 인자는 옵션으로 에러 핸들러이다. 

실행 코드 함수에서 코드상에서 잡을 수 있는 오류가 나거나,  로딩에 실패할 경우 실행된다.  

여기서 짚고 넘어갈 것이 있다. Logger.js 는 두 부분에서 의존성이 있다. 

이 경우에는 먼저 로딩되는 모듈이나 코드에서 한번 로딩되면, 그 다음에는 모듈을 다시 로딩하지 않고 로딩된 모듈을 다시 사용한다. 

Java Spring의 싱글톤 레지스트리와 살짝 비슷하다. 

정리하면 다음과 같다.
  • define: 모듈을 정의할 때
  • require: 정의된 모듈에 의존성을 주는 코드를 작성할 때
이제 대략적인 사용법은 다 정리된 것 같다. 다음은 설정법을 살펴보자 
  

환경 설정

RequireJS는 몇가지 설정을 통해 사용자의 환경에 더욱 잘 조정해 맞출 수 있다. 

문법은 다음과 같다.
// 이 코드를 RequireJS가 로딩된 뒤 기타 모듈을 로딩하기 전에 둔다.
require.config({
  baseUrl: "/js/some/path", // 모듈을 로딩할 기본 패스를 지정한다.

  // 모듈의 기본 패스를 지정한다
  // 모듈의 이름과 실제 경로를 매핑할 수 있어 별칭(alias) 기능도 할 수 있다
  paths: {
    "javarouka": "modules/javarouka", // 이 모듈은 /js/some/path/module/javarouka.js 경로.

    // 모듈 패스를 배열로 주게 되면 먼저 앞의 URL로 로딩해보고 안되면 다음 경로에서 로딩한다.
    // CDN 등을 사용할 때 좋다.
    "YIHanghee": [
      "https://cdn.example.com/YIHanghee",
      "modules/YIHanghee"
     ]
  },
  waitSeconds: 15 // 모듈의 로딩 시간을 지정한다. 이 시간을 초과하면 Timeout Error 가 throw 된다
});
이러한 설정 파일은 다음과 같이 별도의 분리된 파일로 나눌 수 있다
<script type="text/javascript">
var require = {
  // 설정 내용
};
</script>
<script type="text/javascript" src="/js/requirejs.js"></script>
다음에는 RequireJS 형식으로 작성되지 않은 다른 코드를 RequireJS에 포함시켜 사용하며 코드간 의존성을 주는 방법을 알아보겠다.

 

구 소스코드(global-traditional)와 같이 사용하는 법 - Shim

그런데 ReuireJS를 실무에 바로 적용하려면 어려움이 따른다. 바로 하위 호환성 문제이다.

기존의 구(global - traditional) 코드 대부분은 아마도 define이나 require를 사용하지 않은 소스코드가 많다. 그리고 그것들도 나름의 의존성을 가지고 있을 수 있다. 

이것들을 RequireJS에서도 온전히 사용하려면 그냥은 안된다. 이것을 지원하기 위해 RequireJS에는 shim 이라는 기능이 있다. 설정 파일에 shim 속성으로 미리 구 코드의 의존성을 정의할 수 있다. 

문법은 다음과 같다.
requirejs.config({
  paths: {
    // jquery 로딩 시 필요한 경로를 지정한다.
    'jquery': [ 
      '/js/jquery', 
      '//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min' 
    ],
    // underscore 도 마찬가지.
    'underscore': [ 
      '/js/underscore.min', 
      '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min' 
    ],
    // backbone
    'backbone': [
      'js/backbone.min',
      '//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min'
    ]
  },
  shim: {
    // underscore에 대한 shim 속성을 정의한다.
    'underscore': {
      // 반환될 객체는 exports속성으로 정의한다.
      // underscore는 아래와 같은 _ 이름으로 공개되는 것이 사용이 직관적이다.
      exports: function() { return _; }
    },
    // shim 속성의 키로 모듈 이름을 지정하고 내부 속성을 나열한다.
    // backbone은 underscore와 jquery 에 의존성이 있다.
    'backbone': {
      // 백본이 의존하는 underscore와 jquery를 deps 속성에 정의
      // 이름은 위에 이미 지정한 별칭(키 이름)으로 해도 된다.
      deps: ['underscore', 'jquery'],
      // 반환될 객체를 정의한다.
      // 문자열로 줄 경우,
      // 글로벌 객체에서 저 속성을 키로 하는 객체가 모듈이 된다.
      // 함수를 줄 경우,
      // 함수의 실행 결과로 리턴되는 객체가 모듈이 된다.
      exports: 'Backbone'
    }
  }
});
먼저 모듈이 로딩될 경로를 paths 속성의 키로 정의한 뒤 shim 속성에서 정의한 코드에 대한 의존성을 정의한다. 


"backbone" 키로 지정된 deps 속성에는 앞에서 했던 define 처럼 배열 형태로 의존성을 정의하고, exports 속성으로 팩토리 함수를 정의했다. 

이렇게 설정해두면 "backbone" 이라는 모듈 이름으로 RequireJS 모듈처럼 사용을 할 수 있다.
exports 속성에 문자열을 주면 그 문자열에 해당하는 전역의 속성이 define에서 팩토리 함수에서 리턴하는 객체가 되며, 함수를 줄 경우 반환되는 객체를 지정해줄 수 있다. 

exports에 함수를 지정하는 경우는 팩토리 함수와 동일하게 이 shim 모듈이 반환하는 모듈을 조정할 때 유용하다. 

가령 prototype.js와 jQuery를 같이  사용할 경우에는 $ 변수 충돌이 일어나므로 반드시 jQuery에서 prototype.js를 로딩하기 전에 jQuery.noConflict 를 호출해야 한다. 

이럴 경우 RequireJS에서는
"jquery": {
  exports: function() {
    var jQuery = jQuery.noConflict();
    return jQuery;
  }
},
"prototype": {
  deps: [ "jquery" ],
  exports: "$"
}
과 같은 방식을 적용할 수 있겠다. 두 모듈이 안전하게 로딩될 것이다.

 

마치며

JavaScript 는 사용하기 편한만큼 편함에 너무 의존하다보면 돌이킬 수 없는 스파게티코드나 중복 코드 발생이 많아질 수 있다. 

RequireJS는 이러한 경우의 한 대안이 될 수 있으며, 모듈 프로그래밍을 통해 좀 더 체계적인 프로그래밍을 가능하게 해 주며, 브라우저 지원도 IE6 이상부터 지원하는 괜찮은 호환율을 보여준다. 

지금 자신의 프로젝트를 보라. 

혹시 스파게티, 반복 코드가 보인다면, 바로 RequireJS를 한번 고려해볼 때다.