잊지 않겠습니다.


service (non-HTTP)

angularJS의 service code는 일반적으로 REST API에 대한 호출을 하고, 그 결과를 받는 것을 담당하지만, 값의 변경이나 특정값들의 display 형식으로의 변경등을 service로 만들어서, 코드의 응집력을 높일 수 있습니다. 섭씨온도를 화씨온도로 바꾸는 등의 계산 로직도 역시 이 영역에 들어가게 됩니다.

이러한 service code의 테스트는 다음 항목을 확인하면 됩니다.

input 값에 대하여 output 값이 예상대로 나왔는지. 
> toBe(value) 구문을 이용합니다.

기본적으로 matcher를 사용하는 것이 point입니다. 예상한 값을 기준으로 원하는 output이 정상적으로 나오는지를 확인하는 것이 좋습니다.

service (with HTTP)

REST API를 사용하는 service code의 테스트 방법에 대해서 알아보도록 하겠습니다. 일단 REST API를 호출한다면 외부의 HTTP resource를 이용하게 됩니다. 외부의 자원을 이용한다는 것은 그 외부의 자원에 대한 통제권은 우리가 개발한 application에는 없다는 말과 동일합니다. 따라서, 우리가 REST API를 이용하는 service에서 확인할 내용은 다음 두가지입니다.

* 원하는 URL로 정상적으로 호출하고 있는지?
* 전달하는 parameter가 API 호출 규약에 맞는지?

여기서 두가지의 역활이 나뉘게 됩니다. 먼저 원하는 URL로 호출하는 것은 당연히 service가 하는 일입니다. 그런데, 전달하는 parameter가 호출규약에 맞는지는 어떻게 확인하는 것이 좋을까요. 우리가 Spring을 이용한 개발을 진행하였을때를 생각해보면 쉽게 결론이 나옵니다. Validator객체를 만들어서 사용하는 것입니다.

따라서, REST API를 호출하는 service의 코드는 두개의 객체로 나뉘어지면 테스트 및 개발이 원활하게 됩니다. 이와 같은 형태는 마치 Spring MVC에서의 Controller 객체와 Validator 객체를 따로 개발하는 것과 동일한 패턴을 가지고 오게 됩니다. 객체지향이라는 것이 궁극적으로는 서로간에 할 일을 명확히 구분지어주고 그 분류대로 일을 진행하는 것이니까요.

먼저, 간단한 validator code는 다음과 같습니다.

'use strict';

/**
 * @ngdoc service
 * @name fmsmobilewebApp.ResultRegisterValidatorService
 * @description
 * # ResultRegisterValidatorService
 * Service in the fmsmobilewebApp.
 */
angular.module('fmsmobilewebApp').service('ResultRegisterValidatorService', function ResultRegisterValidatorService() {
  var self = this;

  self.validateStatus = function(data) {
    return angular.isDefined(data.status);
  };

  self.validateDepartment = function(data) {
    return angular.isDefined(data.departmentId);
  };

  self.validateOpResult = function(data) {
    var checked = angular.isDefined(data.opResult) &&
                  angular.isDefined(data.opResult.content) &&
                  angular.isDefined(data.opResult.startDate) &&
                  angular.isDefined(data.opResult.endDate);
    return checked;
  };
});

그리고 이에 대한 테스트 코드는 다음과 같습니다.

'use strict';

describe('Service: ResultRegisterValidatorService', function () {

  // load the service's module
  beforeEach(module('fmsmobilewebApp'));

  // instantiate service
  var service;
  beforeEach(inject(function (_ResultRegisterValidatorService_) {
    service = _ResultRegisterValidatorService_;
  }));

  it('validate for opResult', function() {
    var data = {};
    expect(service.validateOpResult(data)).toBe(false);
    data.opResult = {};
    expect(service.validateOpResult(data)).toBe(false);
    data.opResult.content = 'Content';
    expect(service.validateOpResult(data)).toBe(false);
    data.opResult.startDate = '2014-04-05 12:00';
    expect(service.validateOpResult(data)).toBe(false);
    data.opResult.endDate = '';
    expect(service.validateOpResult(data)).toBe(true);
  });

  it('validate for status', function() {
    var data = {};
    expect(service.validateStatus(data)).toBe(false);
    data.status = '0701';
    expect(service.validateStatus(data)).toBe(true);
  });

  it('validate for department', function() {
    var data = {};
    expect(service.validateDepartment(data)).toBe(false);
    data.departmentId = '0401';
    expect(service.validateDepartment(data)).toBe(true);
  });

});

Validator의 test code의 경우에는 기본적인 service code와 동일합니다. input에 따른 결과가 어떻게 나오는지만 확인하면 됩니다.

이제 가장 중요한 http에 대한 서비스 코드입니다. 우리는 test code를 작성하기 전에 REST API를 호출하는 행위에 대한 정의가 우선 필요합니다.
REST API를 호출하는 것은 다음과 같습니다.

  1. URL에 대한 HTTP request
  2. method에 따른 HTTP request
  3. HTTP response에 대한 결과값에 대한 parsing

결국은 우리가 확인할 것은 위 3가지입니다. 그리고 중점을 줘야지 되는 것을 따지자면 1, 2에 대한 확인입니다. 3에 대한 내용은 REST API의 호출 특성상, API 서버측에서 보내는 결과이기 때문에 그 결과값이 json format인 경우, 우리가 테스트를 해서 확인할 내용이 아닙니다. 우리는 그 결과값을 온전히 받아서 사용하는 client측에서만 생각하면 되기 때문에 1, 2번에 대한 내용만을 테스트한다면 우리가 원하는 test code를 작성할 수 있습니다.

angularJS는 angular-mock을 통해 $httpBackend 객체에 우리가 호출되는 URL, method를 확인할 수 있는 방법을 제공합니다. 일단 다음 상황을 가정해보도록 합니다.

GET /api/as/list

그리고, 이러한 url을 호출하는 API Service를 작성합니다.

function AsService(Restangular, AsServiceValidator) {
  var self = this;
  self.getList = function(status, department, func) {
    if(AsServiceValidator.checkStatus(status) && AsService.checkDepartment(department)) {
      Restangular.all('/api/as/list').get().then(function(jsonResult) {
        if(jsonResult.ok && func) {
          func(jsonResult);
        }
      });
      return true;
    } 
    return false;
  };
}

위 코드는 그 전에 이야기한 validator에 대한 체크 후, Restangular를 이용한 http call을 하고 있습니다. service에 대한 test code를 작성할 때, 반드시 $httpBackend에 대한 expect를 설정해야지 됩니다. 다음은 test code의 일부분입니다.

var httpBackend, service
beforeEach(inject(function($httpBackend, AsService) {
  httpBackend = $httpBackend;
  service = AsService;
}));

위 코드패턴을 이용해서 우리는 테스트의 대상이 되는 service와 $httpBackend를 얻어올 수 있습니다. 이제 우리가 원하는 API를 정상적으로 호출하고 있는지를 확인해보도록 하겠습니다.

it('as list REST API 호출', function() {
  httpBackend.expectGET(/\/api\/as\/list/\W*).respond('{ ok: true, data: null, message: data }');
  service.getList(status, department, function(jsonResult) {
    console.log(jsonResult);
  });
  httpBackend.flush();
});

REST API를 호출하는 service의 test code pattern은 다음과 같습니다.

  1. expectGET/expectPOST/expectDELETE/expectPUT method를 이용해서 request가 전달될 URL을 예상합니다.
  2. service를 호출합니다.
  3. httpBackend.flush() method를 호출합니다.

여기서 주의할점은 httpBackend.flush() 입니다. flush가 호출되게 되면, httpBackend는 httpRequest에 대한 response를 발생시킵니다. 만약에 expect에 정의되지 않은 http request가 발생되게 되면 exception을 발생하게 됩니다. REST API의 response를 명확히 알고 있다면, 그 결과값에 대한 처리 로직도 테스트가 가능합니다.

REST API service에 대한 test code작성방법에 대해서 다시 한번 정의해보도록 하겠습니다.

  1. input에 대한 validator의 분리가 필요 > validator 테스트 코드
  2. $httpBackend.expectXXX method를 이용한 REST API 호출 확인

이 두가지에 중점을 주면 이제 service code 역시 테스트가 가능하게 됩니다.


Posted by Y2K
,
filter의 경우에는 배열에 대한 조건을 처리하고 있기 때문에, 최대한 가벼울 필요가 있는 객체이기도 합니다. 또한, non-HTTP service의 경우에는 값의 계산이나 validation에 대한 처리가 일반적입니다.

filter의 테스트는 다음을 확인하면 됩니다.

input array data의 filtering이 정상적으로 되었는지
> count를 이용하는 것이 일반적입니다.

다음은 간단한 filter code입니다.

angular.module('fmsmobilewebApp').filter('planFilter', function () {
  return function (plans, status) {
    if(status == '' || status == '0') {
      return plans;
    } else {
      var result = [];
      angular.forEach(plans, function(plan) {
        if(plan.statusId == status) {
          result.push(plan);
        }
      });
      return result;
    }
  };
});

위와 같은 filter code의 경우, filter의 input 값을 개발자가 제어를 해서 처리하면 간단히 해결됩니다. 다음과 같은 방법으로요.

describe('Filter: planFilter', function () {
  beforeEach(module('fmsmobilewebApp'));
  var planFilter;
  var plans;

  beforeEach(inject(function ($filter) {
    planFilter = $filter('planFilter');
    plans = [{"section":"정기","sectionId":"1801","type":"일반","mainBody":"대성","classification":"보고관리","priority":"1일이내","planDate":"2014-09-02 16:00","statusId":"0805","name":"A 보고관리 (주 2회)","id":"33913","department":"관리팀","orderDate":"2014-09-11","status":"완료"},{"section":"정기","sectionId":"1801","type":"일반","mainBody":"대성","classification":"보고관리","priority":"1일이내","planDate":"2014-09-05 16:00","statusId":"0805","name":"A 보고관리 (주 2회)","id":"33914","department":"관리팀","orderDate":"2014-09-11","status":"완료"}];
  }));

  it('not filtered (status is empty)', function() {
    var count = plans.length;
    expect(planFilter(plans, '').length).toBe(count);
    expect(planFilter(plans, '0').length).toBe(count);
  });

  it('filtered plans', function() {
    var count = 0;
    var status = '0805';
    angular.forEach(plans, function(plan) {
      if(plan.statusId == status) {
        count++;
      }
    });
    expect(count).toBeGreaterThan(0);
    expect(planFilter(plans, status).length).toBe(count);
  });

});


Posted by Y2K
,
yo의 angular-generator를 이용한 angularJS web application을 테스트 하는 방법을 한번 정리해보도록 하겠습니다.

먼저, 테스트의 대상은 다음과 같습니다.

  • filter, service (non-HTTP)
  • service (HTTP)
  • directive
  • controller

angularJS로 만들어지는 객체들은 모두 test의 대상이 될 수 있습니다. 그렇지만, 저 순서가 조금 중요한 것이 기본적으로 service의 테스트 없이, controller의 테스트는 어렵습니다. 그리고 directive 역시 내부에서 service를 사용하고 있다면, service의 test가 우선이 되어야지 됩니다. 따라서, 위 순서와 비슷하게 dependency에 따른 test의 순서를 결정해보는 것이 좋습니다.

xUnit를 이용한 test와 마찬가지로, unit이라는 범위안에서 dependency가 외부 library(jquery, restangular etc..)등에만 의존이 되어 있는 객체들 부터 테스트를 진행하고, dependency가 내부 객체들에 의존되어 있다면, 그 객체들의 테스트가 우선되어야지 됩니다. 이러한 조건을 먼저 생각하는 것부터 테스트를 시작할 수 있습니다. 그리고, 이 순서는 일반적으로 위에 적어놓은 객체들의 순서로 진행되는 것이 일반적입니다.

환경 설정

karma-jasmine을 먼저 설치해야지 됩니다. 기본으로 설정되어 있는 jasmine test framework를 karma를 통해 실행하기 위한 기본 library입니다.

npm install karam-jasmine

설치후, 사용할 browser에 따른 launcher를 설치해야지 됩니다. 자신의 개발 환경에 맞는 launcher를 설치해주면 됩니다. window, linux, mac에 모두 설치가 가능한 web browser를 선택해주는 것이 개발환경을 구성하는 데 편합니다.

npm install karma-chrome-launcher
npm install karma-firefox-launcher
npm install karma-ie-launcher
npm install karma-safari-launcher

launcher의 경우, karam.conf.js 파일에 사용할 browser의 설정이 필요합니다.

browsers: ['Chrome']

karma로 테스트를 진행하기 위해서는 karma.conf.js 파일을 주의깊게 봐야지 됩니다. 테스트가 초기에 진행되지 않는 모든 이유는 바로 이 파일안에 있습니다. 먼저 확인해봐야지 될 것이 우리가 bower 또는 외부 상용 library들에 의존되는 application을 작성하고 있다는 것을 확인해야지 됩니다. 이는 모든 library들이 load 될 때에, 기본적인 library들이 먼저 loading 되어야지 되는 것을 의미합니다. 기본적인 library들을 loading하는 것을 karma에서는 files로 정의하고 있습니다. 또한, files에는 test의 target이 되는 library들과 unit test code역시 정의되어 있어야지 됩니다. 따라서, files는 다음과 같은 구성을 갖게 됩니다.

    files: [
      // bower components
      'app/bower_components/jquery/dist/jquery.js',
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/bower_components/angular-resource/angular-resource.js',
      'app/bower_components/angular-cookies/angular-cookies.js',
      'app/bower_components/angular-sanitize/angular-sanitize.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-touch/angular-touch.min.js',
      'app/bower_components/lodash/dist/lodash.min.js',
      'app/bower_components/mobile-angular-ui/dist/js/mobile-angular-ui.min.js',
      'app/bower_components/restangular/dist/restangular.min.js',
      'app/bower_components/angular-local-storage/angular-local-storage.min.js',
      // 3rd party library
      'app/library/kendo/js/kendo.all.min.js',
      // Test target
      'app/scripts/*.js',
      'app/scripts/**/*.js',
      // Test code
      'test/mock/**/*.js',
      'test/spec/**/*.js'
    ],

특히 jquery의 경우에는 사용하고 있는 경우, 최상단에 넣어주는 것이 좋습니다. 많은 library들이 jquery에 의존적이며, angularJS의 경우에는 내장된 jqLite가 jquery가 있는 경우 jquery를 사용하도록 되어 있기 때문입니다.

이제 karma를 구동할 grunt에 대한 설정을 해보도록 하겠습니다. yo를 이용한 web application을 구성하는 경우, 다음 3가지의 application이 모든 동작을 제어하게 됩니다.

  • yeoman : code에 대한 scafolding 제공, code의 기본적인 구조 제공. template을 이용한 code generater라고 이해하면 쉽습니다.
  • bower : gradle, maven과 같은 library dependency 관리 툴입니다. 이제 jquery를 얻기 위해서 home page에 들어가서 다운 받을 필요가 없습니다.
  • grunt : build tool입니다. test의 진행 또는 webserver의 시작등을 모두 grunt를 통해서 진행합니다.

grunt는 Gruntfile.js 파일을 기반으로 동작합니다. 여기서 karma에 관련된 설정만 처리하면 됩니다. 큰 변경은 필요없고, 개발과 test를 동시에 자동으로 처리하도록 설정하면 개발이 매우 편합니다.

    // Test settings
    karma: {
      unit: {
        configFile: 'karma.conf.js',
        singleRun: false,
        authWatch: true
      }
    }
  • singleRun(default : true) : test가 한번만 진행됩니다. 테스트를 연속적으로 계속 진행할 수 있도록 true로 수정해주면 좋습니다.
  • authWatch(default : false) : 파일의 변경이 있는 경우, test를 다시 진행합니다. 충분히 빠릅니다. 약 90여개의 test code의 경우, 1sec내로 테스트를 진행할 수 있습니다.

이제 테스트를 진행할 수 있는 환경 구성이 모두 마쳐졌습니다. 먼저 jasmine에 대한 소개를 간략하게 하고, 각 객체의 test를 하는 방법에 대하여 알아보도록 하겠습니다.

jasmine

jasmine은 javascript의 test framework로서, xUnit의 역활을 맡게 됩니다.
jasmine의 소개는 다음 url에서 확인해보실 수 있습니다. http://jasmine.github.io/2.0/introduction.html

기본적으로 jasmine은 suites를 기반으로 동작합니다. suites는 descirbe로 선언이 되며, 이 test code가 어떤 test code인지를 서술하는 기능을 갖습니다.

describe('sample test code suit', function() {

});

xUnit에서 사용되는 @Test/[Test]는 it으로 정의됩니다.

describe('sample test code suit', function() {
  it('sample test code unit', function() {
      // Sample Test code
  });
});

xUnit의 @SetUp/[SetUp]은 beforeEach로 정의 됩니다.

describe('sample test code suit', function() {
  beforeEach('pre-run codes each test codes', function() {
      // SetUp code
  });
  it('sample test code unit', function() {
      // Sample Test code
  });
});

xUnit의 @TearDown/[TearDown]은 afterEach로 정의됩니다.

describe('sample test code suit', function() {
  beforeEach('pre-run codes each test codes', function() {
      // SetUp code
  });
  it('sample test code unit', function() {
      // Sample Test code
  });
  afterEach('after-run codes each test codes', function() {

  });
});

beforeEach, afterEach의 경우 여러개가 있어도 상관 없으며, 여러 코드 블럭이 있는 경우, 위에서부터 아래로 code가 실행되게 됩니다.

기본적으로 xUnit에서 사용되는 Test code의 개념을 그대로 들고 왔기 때문에, 사용하기가 매우 편한것을 알 수 있습니다. jasmine은 이제 test code의 기계적인 확인을 위한 여러 matcher 들을 제공합니다. 다음은 matcher들의 종류입니다.

matcher description
expect(target).toBe(value) value와 같은지를 확인합니다.
expect(target).toEqual(value) value와 같은지, 그리고 type이 동일한지를 비교합니다.
expect(target).toBeGreaterThan(value) value보다 값이 큰지를 비교합니다.
expect(target).toBeNull() target이 null인지 확인합니다.
expect(target).toBeDefined() target이 define되어 있는지 확인합니다.
expect(target).toBeTruthly() target이 true인지 확인합니다. 이는 if(target) 의 결과와 동일합니다.
expect(target).toBeFalsy() target이 false인지 확인합니다. 이는 if(!target) 의 결과와 동일합니다.
모든 matcher는 not을 가질 수 있습니다.
> expect(target).not.toBe(value);
> expect(target).not.toBeNull();

jasmine은 method에 대한 call 확인을 지원합니다. 하나의 method가 내부의 특정 method를 호출을 했는지, 호출의 input parameters들을 확인할 수 있습니다. 다음은 예시코드입니다.

  var obj = {
    method1 : function() {
      console.info('call method1');
      this.method2('test value');
    },
    method2 : function(value) {
      console.info(value);
    }
  };

  describe('obj test', function() {
    it('obj method2 called Test', function() {
      spyOn(obj, 'method2');
      obj.method1();
      expect(obj.method2).toHaveBeenCalled();
      expect(obj.method2).toHaveBeenCalledWith('test value');
    });
  });

spyOn 을 이용해서, 감시할 대상을 선정하고, method의 호출을 확인할 수 있습니다. spyOn을 이용한 method의 호출에 대한 matcher는 다음과 같습니다.

matcher description
expect(obj.method).toHaveBeenCalled() method가 호출이 되었는지 확인합니다.
expect(obj.method).toHaveBeenCalledWith(“args”) method가 특정 argument들이 전달되었는지 확인합니다.

Posted by Y2K
,
지금 저는 Gradle을 이용한 web application 배포시에 FTP를 사용하고 있습니다.

그런데, 이 부분에 조금 문제가 있는 것이… 기존 gradle의 ant ftp module의 경우 가끔씩 hang이 걸려서 FTP upload를 실패하는 경우가 왕왕 보입니다. 그리고 FTP upload 시에 아무런 로그가 표시되지 않고, hang이 걸려버려서 정상적으로 upload가 되고 있는지에 대한 확인이 불가능했습니다.

그래서, FTPClient를 이용해서 File upload를 한번 만들어봤습니다.

기본적으로 commons-net의 FTPClient를 이용해서 처리합니다. 먼저 buildscript에 common-net을 등록시킵니다.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'commons-net:commons-net:3.3'
        classpath 'commons-io:commons-io:2.4'
    }
}

다음은 FTPClient를 이용한 upload code입니다. groovy 스러운 문법은 전 아직 잘 안되더군요.;;


import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import org.apache.commons.net.PrintCommandListener
import org.apache.commons.net.ftp.FTP
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPReply
import org.apache.commons.io.IOUtils


void ftpUpload(ftpUrl, ftpUsername, ftpPassword, targetPath) {
    def ftp = new FTPClient()
    ftp.connect(ftpUrl)
    int reply = ftp.getReplyCode()
    if(!FTPReply.isPositiveCompletion(reply)) {
        ftp.disconnect()
        throw new Exception("Exception in connecting to FTP Server")
    }
    ftp.login(ftpUsername, ftpPassword)
    ftp.setFileType(FTP.BINARY_FILE_TYPE)
    ftp.changeWorkingDirectory(targetPath)
    for(File f : file(getDistPath()).listFiles()) {
        upload(f, ftp)
    }
    ftp.disconnect()
}

void upload(File src, FTPClient ftp) {
    if (src.isDirectory()) {
        ftp.makeDirectory(src.getName())
        ftp.changeWorkingDirectory(src.getName())
        for (File file : src.listFiles()) {
            upload(file, ftp);
        }
        ftp.changeToParentDirectory();
    }
    else {
        InputStream srcStream = null;
        try {
            def uploadCompleted = false
            while(!uploadCompleted) {
                srcStream = src.toURI().toURL().openStream()
                println 'upload : ' + src.getName()
                uploadCompleted = ftp.storeFile(src.getName(), srcStream)
                if(!uploadCompleted) {
                    println 'upload failed : retry this file ' + src.getName()
                    IOUtils.closeQuietly(uploadCompleted)
                }
            }
        } catch(Exception ex) {
            println ex.getMessage()
        }
        finally {
            IOUtils.closeQuietly(srcStream);
        }
    }
}

java 코드 같은 느낌입니다. groovy를 사용하고 있으면 좀 더 groovy 스러운 코드여야지 될 것 같은데요. ^^;;


Posted by Y2K
,

angularJS를 이용한 single document web application을 개발 할 때, 내용에 대한 정리가 필요해서 한번 정리해봅니다.

angularJS MVVM 이해하기


(그림의 출처는 http://www.mimul.com 입니다.)

Spring MVC를 이용하고 있다면, MVC의 개념에 대해서는 잘 이해하고 있을겁니다. 개인적으로 javascript framework에서 이야기하는 MVVM이라는 것은 약간의 marketing 용어라는 느낌이 강하게 들기 때문에, 거의 기존의 MVC와 동일하게 생각하면 됩니다.

Model

데이터 관리 및 통신등의 역활을 담당합니다. 이는 Spring MVC를 이용한 개발시에 표현되는 @Service와 동일하게 생각해도 됩니다. Service와 Factory로 구성이 되며 이는 singleton 여부와 동일하게 생각하면 됩니다. Service는 singleton으로, factory는 prototype으로요.

주로 사용되는 것은 REST API를 호출하는 call layer로 구성이 되는 경우가 많습니다. 이에 대한 결과에 대한 caching과 logic을 처리하는 것이 주가 되는데 개인적으로 추천을 한다면 Restangular module을 사용하는 것이 보다 더 편하고 좋은 코드 가독성을 가지고 올 수 있습니다. (https://github.com/mgonto/restangular)

Controller

DOM event handling과 $scope를 이용한 DataBinding 역활을 담당합니다. Spring MVC에서의 Controller와 Model의 역활이 합쳐져 있다고 이해하시면 편합니다. @Controller : Controller class, Model: $scope 로 생각하시면 좋습니다. DataBinding과 DOM event handling을 모두 가지고 있기 때문에 $scope의 코드의 method 들은 최대한 간결하고 명확한 naming이 좋습니다.

View

HTML 영역입니다. View의 표현이 중복되는 것이 많아진다면 directive를 만들어서 처리를 하면 HTML의 재활용도를 높일 수 있습니다. 다만, 저같이 기억력이 안좋은 사람에게는 사용할 때마다 directive의 code를 다시 한번 확인해야지 되는 단점을 가질 수 도 있습니다.;

angularJS를 이용한 개발 시 유의점

Memory leak

이 상황은 single document application을 구성할 때, 가장 주의해야지 되는 내용입니다. 지금까지 javascript로 개발하는 상황과 많은 차이를 가지고 오는 부분이 이 memory leak 부분이라고 생각합니다. 이 부분에 대한 주의가 필요합니다. memory leak이 발생되는 상황임을 알 수 있는 방법은 chrome의 profile에서 memory snapshot을 얻어서 확인 할 수 있습니다. 일단 3가지 질문을 통해서 javascript의 memory leak이 발생되고 있는지를 확인 할 수 있습니다.

page가 너무 많은 memory를 사용하는 경우

너무나 많은 memory를 사용하고 있다면 chrome의 timeline memory view와 chrome task manager를 통해서 확인 할 수 있습니다. 경험적으로는 사용하지 않는 DOM을 계속해서 가지고 있는 경우. (unbind DOM, fragmented DOM)이 계속해서 생성이 되는 경우에 주로 이런 현상이 발생됩니다. 더 이상 필요없게 되는 DOM에 대한 event listener를 unbind 시키는 것이 매우! 중요합니다.

page가 memory leak에서 자유로운지

Chrome Profile에서 볼 수 있는 Object allocation tracker를 이용해서 javascript object의 할당을 확인해보는 것이 좋습니다. snapshot을 이용해서 GC가 일어날 때와 일어나지 않았을 때의 memory 차이를 알아보고 page가 이동이 된 후도 계속 남는 memory가 많은지를 확인해봐야지 됩니다.

Forcing GC가 자주 일어나는지

chrome timeline memory view를 이용하면 GC가 얼마나 자주 일어나는지를 확인할 수 있습니다. 이것이 자주 일어난다면 너무나 많은 객체들이 생성되고 있다는 것을 의미합니다. 객체를 너무나 많이 생성하고 있는 것에 대한 고민이 필요합니다.

위의 상황들중에서 가장 문제가 될 수 있는 것은 javascript의 fragmented DOM 또는 unbinded DOM에 대한 event listener로 인한 memory leak 입니다. 이는 jQuery UI를 angularJS single document application에서 사용하는 경우, 자주 발생할 수 있는데, 이는 각각의 component 들에 대한 event binding이 정상적으로 clear 되지 못해서 발생되는 문제입니다. 이 부분에 대해서는 다음 Blog post를 참조해보세요.

http://rinat.io/blog/angularjs-and-jquery-plugins-memory-leaks

이러한 unbinded DOM의 event listener를 해지하기 위해서는 $destroy event의 처리가 필요합니다.
다음과 같은 code snippet가 각 controller에서 위치해야지 됩니다. 이 부분은 사용되는 javascript UI framework 등에서 어떻게 memory에서 모든 객체들을 제거할 수 있는지에 대한 document를 참고하시는 것이 좋습니다. (jquery 기반의 UI component는 destroy() method를 갖는 것이 일반적입니다.)

$scope.on('$destroy', function() {
    var dateTimePicker = $scope.element('#dateTimePicker');
    if(dateTimePicker != null) {
        dateTimePicker = dateTimePicker.datetimepicker();
        dateTimePicker.destroy();
    }
});

또한, $destroy에서는 controller에서 사용되고 있는 모든 timer들의 해재역시 처리해주는 것이 좋습니다.

Directive의 올바른 이해와 사용

Directive를 올바르게 사용하는 것은 매우 중요한 문제입니다. 가장 우선적으로, Directive의 scope의 정의를 정확히 아는 것이 중요합니다. Controller의 $scope와 독립적인 scope가 생성 되나, 양방향 또는 단방향으로 controller와 통신을 하는 것이 가능합니다.

예를 들어, scope를 다음과 같이 선언할 수 있습니다.

    scope: {
      title: '@',
      parentCodeId: '@',
      data: '@',
      ngModel: '=',
      text: '=',
      name: '@',
      nullItem: '@',
      styleValue: '@'
    },

title, parentCodeId, data, name, nullItem, styleValue의 경우에는 attribute를 이용한 값 설정과 동일합니다. directive의 scope를 변경하는 것으로 controller가 변경되지 않습니다. 그렇지만, ngModel, text와 같이 ‘=’로 선언된 내용은 다릅니다. ‘=’로 선언된 내용은 directive를 통해서 수정된 값이 controller의 값을 변경시키게 됩니다.

이러한 양방향 DataBinding을 이용하게 되는 경우, Directive에서 event를 호출할 필요가 거의 없습니다. 최대한 event를 호출해서 처리하는 코드양을 줄이는 것이 속도면에서 유리합니다.

DataBinding의 처리

angularJS에서의 DataBinding은 $scope의 모든 변수에 대해서 이전의 값과 현재의 값을 비교하여, 값이 다른 경우 DOM을 갱신하는 방식입니다. $scope의 변수의 값을 비교하는 타이밍은 아래와 같습니다.

  • scope.$apply()가 호출될 때.
  • DOM event가 발생될 때. (onchange, onclick etc…)
  • http 응답이 발생된 경우. ($http, $resource)
  • url이 변경되는 경우
  • $timeout event가 발생되는 경우

이 부분에 대한 개선은 다음과 같이 처리하는 것이 좋습니다.

기본적인 angularJS module의 이용

jQuery와 angularJS를 혼용해서 사용하게 되는 경우가 많은데, 위와 같은 문제 때문에 최대한 angularJS의 모듈들을 사용하는 것이 좋습니다. 가장 좋은 것은 jQuery method를 사용하지 않는것이 좋습니다. 예를 들어 jQuery의 기본 $.ajax는 가장 자주 사용되는 http ajax 통신 method입니다. 그렇지만, 이 코드를 사용하는 경우, scope의 DOM update가 되지 않습니다. 따라서, $http service를 이용하는 것이 좋습니다.

또한, window의 경우에도 $window, document는 $document를 이용하는 것이 좋습니다. 그리고, WebSocket을 이용하는 경우도 역시 마찬가지의 문제가 발생됩니다. WebSocket의 promise를 반환하고 그 값을 얻어낼 때, $apply() method를 호출해주어야지 됩니다.

$watch 는 최대한 가볍게

$watch method는 잦은 호출이 발생될 수 있는 code입니다. 다음과 같은 처리가 반드시 필요합니다.

slow — Anything faster than 50ms is imperceptible to humans and thus can be considered as "instant".
limited — You can't really show more than about 2000 pieces of information to a human on a single page. Anything more than that is really bad UI, and humans can't process this anyway.

TIP

이 부분은 개인적으로 사용해보니 좋았던 TIP들입니다. 다른 분들은 다르게 느끼실 수 있는 부분입니다.

Parent Controller를 이용한 Controller common method의 공유

공통적으로 사용하고 싶은 method들을 MainController에 넣어두면 Controller를 작성하기가 매우 편해집니다. 예를 들어 back() 처리라던지, Confirm/Alert 과 같은 알람기능이나 resize에 대한 기본적인 처리와 같은것을 넣어두면 매우 유용하게 사용가능합니다. 이를 지원하기 위해서 기존 ng-view 상단에 ng-controller를 지정해서 처리해주면 됩니다.

    <div ng-controller="MainCtrl">
      <section ng-view="" onload="completedRender()"></section>
    </div>
angular.module('fmsPublicWebApp').controller('MainCtrl', function ($scope, $timeout, $window, Codeservice, $location) { 
    $scope.showGlobalMessage = function(type, title, message) {

    }
});
$http, $resource보다는 Restangular를 사용

bower를 이용해서 쉽게 추가 가능한 Restangular를 사용하면 좋은 가독성을 갖는 코드를 짜는 것이 가능합니다. 약간 옆으로 길어지는 코드 pattern이 생기긴 하지만, 직관적인 호출이라는 강점을 가질 수 있습니다.

jQuery Library를 사용할 때는 주의할것!

최대한 jquery에 의존하지 않고 사용하는 것이 더 편합니다. angularJS는 jqLite라는 jquery의 mini version을 가지고 있습니다. 기본적인 jQuery select와 같은 기능들은 대부분 가지고 있기 때문에 angular.element를 사용하는 것이 더 좋습니다.

그리고, jQuery UI component를 사용하게 되는 경우에는, directive로 새로 만들어서 사용하는 것을 강력 권장합니다. 위에서 이야기드린 것처럼, jQuery UI library의 destroy를 정확하게 호출하지 않는 경우에는 memory leak이라는 매우 미묘한 문제를 가지고 오게 됩니다. 개발이 한참 진행된 이후에 memory leak을 발견하게 되는 경우에는 찾기가 정말 힘들어질 수 있습니다.

$scope에는 외부에 노출되어야지 되는 변수 및 method만을 정의 한다.

$scope의 값은 angularjs의 dirty-search의 대상입니다. 불필요한 내부값 및 내부 method 들을 $scope에서 사용해서 굳이 성능저하를 만들 필요는 없습니다.

Summary

AngularJS는 정말로 재미있는 Framework입니다. 특히 single document application을 개발할 때, 그 장점을 더 살릴수 있습니다. 그렇지만, 다음 항목들에 대해서는 좀더 주의가 필요합니다.

  1. memory leak
  2. DataBinding의 이해
  3. 재사용 가능한 HTML과 Controller


Posted by Y2K
,

Gradle + yo deploy script

gradle을 이용한 build script 만들기 재미가 붙어, 이번에는 yo로 만든 web application을 auto-deploy를 지원하는 script를 한번 작성해봤습니다.

지원하는 시나리오는 다음과 같습니다.

  1. 이번에 작성한 2개의 public web과 mobile web을 모두 사용 가능
  2. 다음과 같은 process를 진행 가능해야지 됨
  • grunt build
  • svn에 새로 deploy할 web application의 압축본을 add & commit
  • ftp를 이용한 web application 배포

먼저, 여러 web application을 지원하기 위한 조건은 외부 parameter를 받는 방법이 가장 좋아서 target을 지정하기로 했습니다.

작성된 web aplication들의 구조는 다음과 같습니다.

.
├── admin-web
├── mobile-web
├── public-web
└── sample-web

여기서 sample-web의 경우에는 팀원들에게 같이 공유할 sample web application이기 때문에 배포 대상이 되지 못하고, admin, mobile, public의 경우에는 배포 대상이 될 수 있습니다. 간단히 이름을 기준으로 admin, mobile, public을 기준으로 삼고, 이를 parameter로 받으면 될 수 있다는 판단이 내려졌습니다.

이제 다음 조건들이 필요합니다.

  • grunt,svn 과 같은 command의 실행
  • grunt deploy가 된 후에 zip compress
  • ftp를 이용한 data copy

gradle을 이용한 command의 실행

기본적으로 gradle을 이용한 command는 type을 Exec로 잡아주면 처리가 가능합니다. 이와 같은 sub directory 내에서 실행되어야지 될 command는 workingDir 값을 지정해줘서 처리가 가능합니다. 다음은 grunt build를 하는 code입니다.

task build(type: Exec) {
    workingDir getWebDir()
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'grunt', 'build'
    } else {
        commandLine 'grunt', 'build'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

윈도우즈 계통과 linux/mac을 모두 지원하기 위해서 OS의 type을 설정해줘야지 됩니다. windows에서 cmd와 /c를 이용하는 것만을 주의하면 매우 간단한 코드입니다.

zip 파일 압축

zip 압축은 gradle에서 기본으로 제공하고 있는 Zip type을 이용하면 됩니다. zip type의 경우, from과 destinationDir만을 주의해서 처리하면 됩니다. 다음은 만들어진 distPath의 모든 내용을 yyyyMMddHHmm-hostname.zip 형식으로 압축을 하는 코드입니다.

task compressDist(type: Zip) {
    dependsOn 'build'

    from file(getDistPath())
    destinationDir file(deployPath)

    def now = new Date()
    def dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
    def hostname = InetAddress.getLocalHost().getHostName().toLowerCase()

    filename = String.format("%s-%s.zip", dateFormat.format(now), hostname)
    archiveName filename
}

FTP 파일 전송

gradle은 자체적으로 FTP를 지원하지 않습니다. 다만 ant 를 지원하고 있기 때문에, ant의 FTP를 이용하면 됩니다. 그런데 ant ftp의 경우에는 send시에 hang이 걸리는 버그를 가지고 있습니다. passive mode를 true로 설정하는 것으로 hang이 걸리는 경우를 조금 덜 하게 할 수 있긴 하지만, 그래도 send에서 hang이 걸리는 것을 완벽하게 막지는 못합니다. 이 부분에 대해서는 좀 더 논의가 필요할 것 같습니다. 다음은 ftp 전송 코드입니다.

task 'upload' {
    dependsOn 'commitSvn'
    doLast {
        def remoteDir = getFtpPath()
        ant {
            taskdef(name: 'ftp', classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP', classpath: configurations.ftpAntTask.asPath)
            ftp(action: 'mkdir', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword)
            ftp(action: 'delete', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset() { include(name: '**/*') }
            }
            ftp(action: 'send', remotedir: remoteDir, verbose: true,
                    depends: true, binary: true, passive: true,
                    server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset(dir: getDistPath()) {
                    include(name: '**/**/*')
                }
            }
        }
    }
}

각 build task에 대한 dependency를 추가하고, target argument를 받아서 처리하도록 코드 수정을 마져 완료한 최종 코드는 다음과 같습니다.

import org.apache.tools.ant.taskdefs.condition.Os

import java.text.SimpleDateFormat

configurations {
    ftpAntTask
}

repositories {
    mavenCentral()
}

dependencies {
    ftpAntTask("org.apache.ant:ant-commons-net:1.8.2") {
        module("commons-net:commons-net:1.4.1") {
            dependencies "oro:oro:2.0.8:jar"
        }
    }
}

def ftpUrl = '192.168.13.210'
def ftpUsername = 'ykyoon'
def ftpPassword = 'qwer12#$'
def filename = ''

def getDistPath() {
    return String.format('%s/dist', getWebDir())
}

def getWebDir() {
    return String.format('%s-web', project.target)
}

def getDeployPath() {
    return String.format('../deployed/%s-web/', project.target);
}

def getFtpPath() {
    return String.format('www-fms/%s', project.target);
}

task build(type: Exec) {
//    dependsOn 'upSvn'
    workingDir getWebDir()
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'grunt', 'build'
    } else {
        commandLine 'grunt', 'build'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task compressDist(type: Zip) {
    dependsOn 'build'

    from file(getDistPath())
    destinationDir file(deployPath)

    def now = new Date()
    def dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
    def hostname = InetAddress.getLocalHost().getHostName().toLowerCase()

    filename = String.format("%s-%s.zip", dateFormat.format(now), hostname)
    archiveName filename
}

task upSvn(type: Exec) {
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'up'
    } else {
        commandLine 'svn', 'up'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task addSvn(type: Exec) {
    dependsOn 'compressDist'

    String svnParam = getDeployPath() + filename
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'add', svnParam
    } else {
        commandLine 'svn', 'add', svnParam
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task commitSvn(type: Exec) {
    dependsOn 'addSvn'
    String svnParam = getDeployPath() + filename
    String svnLog = '-mCommit file before deployed'

    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'add', svnParam, svnLog
    } else {
        commandLine 'svn', 'commit', svnParam, svnLog
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task 'upload' {
    dependsOn 'commitSvn'
    doLast {
        def remoteDir = getFtpPath()
        ant {
            taskdef(name: 'ftp', classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP', classpath: configurations.ftpAntTask.asPath)
            ftp(action: 'mkdir', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword)
            ftp(action: 'delete', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset() { include(name: '**/*') }
            }
            ftp(action: 'send', remotedir: remoteDir, verbose: true,
                    depends: true, binary: true, passive: true,
                    server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset(dir: getDistPath()) {
                    include(name: '**/**/*')
                }
            }
        }
    }
}

task 'deploy' {
    dependsOn 'build'
    dependsOn 'upload'
    doLast {
        println 'grunt build and upload'
    }
}

task help << {
    println '--------- gradle web command help ----------------------'
    println 'gradle deploy -Ptarget=public : public web build'
    println 'gradle deploy -Ptarget=mobile : mobile web build'
    println '--------- end line -----------------------------------------'
}

모두들 즐거운 코딩 되세요. ^^

Posted by Y2K
,

HikariCP 소개

Java 2014. 8. 21. 17:10

HikariCP

BoneCP를 재치고 놀라운 속도를 자랑하는 DB Connection Pool입니다. BoneCP의 경우, Hibernate 4.x 버젼에서의 지원이 조금 애매해진 경향이 있습니다. (최신 버젼의 Hibernate에서는 에러가 발생합니다.) 반면에 HikariCP의 경우에는 Hibernate와의 통합 jar가 나오는 등, 계속해서 밀어주고 있다는 느낌이 강하게 듭니다.

GitHub page

https://github.com/brettwooldridge/HikariCP

Hibernate + HikariCP

build.gradle에 다음 dependency들을 추가합니다.

    compile 'org.slf4j:slf4j-api:1.7.5'
    compile 'com.zaxxer:HikariCP:2.0.1'
    compile 'org.javassist:javassist:3.18.2-GA'

hibernate.cfg.xml 파일에 다음 항목을 추가합니다.

<property name="connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property>
<property name="hibernate.hikari.dataSourceClassName">com.mysql.jdbc.jdbc2.optional.MysqlDataSource</property>
<property name="hibernate.hikari.dataSource.url">jdbc:mysql://localhost/test</property>
<property name="hibernate.hikari.dataSource.user">root</property>
<property name="hibernate.hikari.dataSource.password">qwer12#$</property>
<property name="hibernate.hikari.dataSource.cachePrepStmts">true</property>
<property name="hibernate.hikari.dataSource.prepStmtCacheSize">250</property>
<property name="hibernate.hikari.dataSource.prepStmtCacheSqlLimit">2048</property>
<property name="hibernate.hikari.dataSource.useServerPrepStmts">true</property>

Spring + HikariCP

@Configuration을 이용해서 HikariCP를 사용하는 법은 다음과 같습니다.

DataSource를 이용하는 경우 (ex: mssql server)
    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();

        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));

        dataSource.setDataSourceClassName("com.microsoft.sqlserver.jdbc.SQLServerDataSource");
        dataSource.addDataSourceProperty("url", env.getProperty(CONNECT_URL));

        int minConnection = Integer.parseInt(env.getProperty(CONNECT_MIN));
        dataSource.setMinimumIdle(minConnection);
        int maxConnection = Integer.parseInt(env.getProperty(CONNECT_MAX));
        dataSource.setMaximumPoolSize(maxConnection);

        return dataSource;
    }
Driver를 이용하는 경우 (ex: mssql server)
    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();

        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));
        dataSource.setDriverClassName(env.getProperty(CONNECT_DRIVER));
        dataSource.setJdbcUrl(env.getProperty(CONNECT_URL));

        int minConnection = Integer.parseInt(env.getProperty(CONNECT_MIN));
        dataSource.setMinimumIdle(minConnection);
        int maxConnection = Integer.parseInt(env.getProperty(CONNECT_MAX));
        dataSource.setMaximumPoolSize(maxConnection);

        return dataSource;
    }

HikariCP property

HikariCP의 property 설정은 다음과 같습니다.

autoCommit (default : true)

connection이 종료되거나 pool에 반환될 때, connection에 속해있는 transaction을 commit 할지를 결정합니다.

readOnly (default : false)

database connection을 readOnly mode로 open합니다. 이 설정은 database에서 지원하지 않는다면 readOnly가 아닌 상태로 open되기 때문에, 지원되는 database 목록을 확인해보고 사용해야지 됩니다.

transactionIsolation (default : none)

java.sql.Connection 에 지정된 Transaction Isolation을 지정합니다. 지정된 Transaction Isoluation은 다음과 같습니다.

  • Connection.TRANSACTION_NONE : transaction을 지원하지 않습니다.
  • Connection.TRANSACTION_READ_UNCOMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 commit되지 않은 값(dirty value)를 읽습니다.
  • Connection.TRANSACTION_READ_COMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 변경되지 않은 값을 읽습니다.
  • Connection.TRANSACTION_REPEATABLE_READ : 같은 transaction내에서 값을 또다시 읽을 때, 변경되기 전의 값을 읽습니다. TRANSACTION_READ_UNCOMMITTED 와 같이 사용될 수 없습니다.
  • Connection.TRANSACTION_SERIALIZABLE : dirty read를 지원하고, non-repeatable read를 지원합니다.

기본값을 각 Driver vendor의 JDBCDriver에서 지원하는 Transaction Isoluation을 따라갑니다. (none으로 설정시.)

category (default : none)

connection에서 연결할 category를 결정합니다. 값이 설정되지 않는 경우, JDBC Driver에서 설정된 기본 category를 지정하게 됩니다.

connectionTimeout(default: 30000 - 30 seconds)

connection 연결시도시 timeout out값을 설정합니다. 이 시간내로 connection을 연결하는데 실패하면 SQLException을 발생합니다.

idleTimeout(default : 600000 - 10 minutes)

connection Pool에 의하여 확보된 connection의 maximum idle time을 결정합니다. connection Pool에 의하여 확보된 connection이 사용되지 않고, Pool에 의해서만 이 시간동안 관리된 경우, connection을 DB에 반환하게 됩니다. 값을 0으로 설정하는 경우, 확보된 connection을 절대 반환하지 않습니다.

maxLifetime(default : 1800000 - 30 minutes)

connection Pool에 의하여 확보된 connection의 최대 생명주기를 지정합니다. connection을 얻어낸지, 이 시간이상되면 최근에 사용하고 있던 connection일지라도, connection을 close시킵니다. 사용중에 있던 connection은 close 시키지 않습니다. (사용이 마쳐지면 바로 close 됩니다.) HikariCP에서는 이 값을 30~60 minutes 사이의 값을 설정하라고 강력권고합니다. 값을 0로 설정하는 경우 lifetime은 무제한이 됩니다.

leakDetectionThreshold (default : 0)

connectionPool에서 반환된 connection의 올바른 반환이 이루어졌는지를 확인하는 thread의 갯수를 지정합니다. 이 값을 0로 지정하는 경우, leak detection을 disable 시키게 됩니다. 만약에 또다른 connection pool을 사용하고 있다면, 다른 connection pool에서 만들어진 connection을 leak으로 판단하고 connection을 닫아버릴 수 있습니다.

jdbc4ConnectionTest (default : true)

connection을 맺은다음, Connection.isValid() method를 호출해서 connection이 정상적인지를 확인합니다. 이 property는 다음에 나올 connectionTestQuery에 매우 밀접한 영향을 받습니다.

connectionTestQuery (default : none)

Connection.isValid() method를 지원하지 않는 ‘legacy’ database를 위한 빠른 query를 지정합니다. (ex: VALUES 1) jdbc4ConnectionTest가 더 유용하기 때문에 사용하지 않는 것이 좋습니다.

connectionInitSql (default : none)

새로운 connection이 생성되고, Pool에 추가되기 전에 실행될 SQL query를 지정합니다.

dataSourceClassName (default : none)

JDBC driver에서 지원되는 dataSourceClassName을 지정합니다. 이 값은 driverClassName이 지정된 경우, 지정할 필요가 없습니다.

dataSource (default : none)

사용자가 만든 dataSource를 Pool에 의하여 wrapped하는 것을 원하는 경우, 이 값을 지정하여 사용합니다. HikariCP는 이 문자열을 이용해서 reflection을 통해 dataSource를 생성합니다. 이 값이 설정되는 경우, dataSourceClassName, driverClassName 과 같은 값들은 모두 무시 됩니다.

driverClassName

HikariCP에서 사용할 DriverClass를 지정합니다. 이 값이 지정되는 경우, jdbcUrl이 반드시 설정되어야지 됩니다.

jdbcUrl

jdbcUrl을 지정합니다. driverClassName이 지정된 경우, jdbcUrl을 반드시 지정해줘야지 됩니다.

minimumIdle (default : maximumPoolSize)

connection Pool에서 유지할 최소한의 connection 갯수를 지정합니다. HikariCP에서는 최고의 performance를 위해 maximumPoolSize와 minimumIdle값을 같은 값으로 지정해서 connection Pool의 크기를 fix하는 것을 강력하게 권장합니다.

# maximumPoolSize

connection Pool에서 사용할 최대 connection 갯수를 지정합니다. 이 부분은 운영환경과 개발환경에 매우 밀접하게 연결되는 부분으로, 많은 테스트 및 운영이 필요합니다.

username

Connection을 얻어내기 위해서 사용되는 인증 이름을 넣습니다.

password

username과 쌍이 되는 비밀번호를 지정합니다.

poolName (default : auto-generated)

logging과 JMX management에서 지정할 pool의 이름을 지정합니다.

registerMbeans (default : false)

JMX management Beans에 등록되는 될지 여부를 지정합니다.

Posted by Y2K
,

Hibernate에서의 양방향 @OneToOne의 이용

Domain Model을 만들때, @OneToOne은 많은 의미를 갖습니다. Master-Detail 구조에서 Detail을 의미하기도 하고, Query를 만들어서 처리할 때, 많은 데이터를 한번에 읽기보다는 작은 데이터를 먼저 읽어서 표시하는데에 사용하는 등의 어찌보면 역정규화를 이용할 때 주로 사용됩니다. (개인적으로는 하나의 Big Table로 처리하는 것이 개발 상에서는 가장 편할 지도 모른다. 그런데 이렇게 하는 경우, Query를 사용해서 처리할 때 Query양이 많아지고 Table이 너무나 커져버리는 단점이 있습니다.) 또한, @OneToOne Relation은 Table을 이용한 BL의 확장에 큰 영향을 주게 됩니다. 새로운 BL Process가 생성되었을 때, 기존 Table을 변경하지 않는 방향으로 BL을 확장할 수 있기 때문에 현업에서는 자주 사용되는 방법입니다.

그런데, 여기에 문제가 있는 것이, 기본적으로 JPA에서는 @OneToOne은 모두 Early Loading으로만 처리가 가능하다는 점입니다. 이를 Lazy Loading으로 전환하기 위해서는 단방향처리만이 가능하게 처리해야지 됩니다. Parent에서 Key를 갖는 형식으로 말이지요.

그런데, 이와 같은 방법 역시 문제를 가지고 있습니다. 단방향처리만이 가능하다는 것은 기본적으로 처리하는 parent entity에서 FK를 가져야지 된다는 점입니다. Table 추가를 통한 Domain Logic의 확장에 있어서, 이는 기존 Table을 변경해야지 되는 큰 약점을 가지게 됩니다.

따라서, @OneToOne을 양방향으로 사용할 수 있는 방법을 반드시 제시할 수 있어야지 Domain Logic을 생성하는데 ORM을  사용하는 것이 될 수 있을 것입니다. 양방향(bidirection)으로 @OneToOne을 Lazy Loading으로 사용하기 위한 방법은 다음 3가지가 제시됩니다.

@OneToMany, @ManyToOne을 이용하는 방법

객체를 @OneToOne으로 사용하지 않고, Lazy Loading을 사용하기 위해서 다른 Relation으로 정의하고, Code상에서는 @OneToOne 처럼 사용하는 것입니다. 개인적으로 가장 싫어하는 방법입니다. 객체간의 관계를 객체만으로 명확히 보이는 ORM의 장점을 모두 무시하는 방법이라고 생각합니다. 또한 QueryDSL이나 HQL의 작성시에 매우 어렵게 되며, 생성되는 Query 역시 효율성이 떨어지는 Query를 생성하게 됩니다.

byte code instrument를 이용하는 방법

: 이는 CTW(compile time weaver)를 이용해서 처리하는 것입니다. Hibernate에서 제공하는 build time instrument를 이용해서 compile 시에 처리하는 방법입니다. 이에 대한 설명은 다음 Hibernate의 문서를 참고하는 것이 좋습니다. (ant 등 build Task가 별도로 필요합니다. ) -http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-fetching-lazyproperties
: 이 부분에 대한 설명은 Hibernate 4.x 대에서 제거되었습니다. 4.x에서의 byte code instrument가 지원되는지는 추가 확인이 필요합니다.
: Proxy를 사용하지 않기 때문에 다음과 같은 NoProxy 설정이 필요합니다.

@OneToOne(fetch = FetchType.LAZY, optional = true) @LazyToOne(LazyToOneOption.NO_PROXY) @JoinColumn(name = "customerPetitId", nullable = true) private CustomerPetit customerPetit;
runtime byte code instrument를 이용하는 방법

: 이 방법은 LTW(load time weaver)를 이용해서 처리하는 방법입니다. 이 방법을 사용하기 위해서는 full-blown JEE environment에서 구성되어야지 됩니다. tomcat, jboss, jetty와 같은 JEE 환경에서만 사용이 가능하다는 단점을 갖습니다. (JUnit Test 환경에서는 동작하지 않습니다.)
: hibernate.ejb.use_class_enhancer 설정을 true로 해주는 것으로 설정이 가능합니다. 이 방법 역시 @LazyToOne(LazyToOneOption.NO_PROXY) annotation이 필요합니다.
: Spring 환경에서도 사용이 가능합니다. 단, Lazy Loading시의 performance 이슈는 해결되어야지 됩니다.

위 3가지 방법이 JBoss에서 문서화된 방법들입니다. 이러한 방법 이외에 다른 방법을 하나 더 소개하고자 합니다. 이 방법은 @OneToMany, @ManyToOne 방법과 같이 HQL의 변경을 가지고 오지도 않고, build 시에 새로운 task를 생성할 필요가 없으며, LTW와 같이 성능 저하나 환경을 따르지도 않습니다.

FieldHandler, FieldHandled를 이용한 Bi-Direction @OneToOne

이 방법은 Hibernate의 내부에 이미 구현된 FieldHandler를 사용하는 방법입니다. FieldHandler는 객체에 값을 주입할 때, Hibernate core에서 사용되는 객체입니다. 또한 Hibernate는 FieldHandled interface를 구현한 객체의 경우, Hibernate에서 load될 때, 자동으로 주입시켜주기 때문에 우리가 별도로 개발할 필요는 없습니다.

구현하기 위한 조건은 다음 2가지입니다.

  1. @LazyToOne(LazyToOneOption.NO_PROXY) annotation
  2. Entity 객체에 FieldHandled interface 구현

구현되는 코드는 다음과 같습니다. (양방향이기 때문에 Parent, Child Entity에 모두 구현해야지 됩니다.)

public class OperationResult extends AbstractInsertUpdateEntity implements Serializable, FieldHandled {
    @OneToOne(fetch = FetchType.LAZY, optional = true)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    @JoinColumn(name = "operationId", nullable = true)
    private OperationPlan plan;

    public void setPlan(OperationPlan plan) {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.writeObject(this, "plan", this.plan, plan);
        } else {
            this.plan = plan;
        }
    }

    public OperationPlan getPlan() {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.readObject(this, "plan", plan);
        }
        return this.plan;
    }

    private FieldHandler fieldHandler;

    @Override
    public void setFieldHandler(FieldHandler handler) {
        this.fieldHandler = handler;
    }

    @Override
    public FieldHandler getFieldHandler() {
        return fieldHandler;
    }
}

이 구현자체는 byte instrument에서 compile/runtime 시에 동작하는 원리와 완전히 동일하게 동작합니다. byte code instrument를 이용한 compile/runtime 시의 동작이 위의 get/set method에 대한 AOP 동작을 주입하는 것이기 때문입니다.

개인적으로는 저는 이 방법을 추천합니다. 이 방법의 경우 다음과 같은 장점이 있습니다.

  1. @OneToMany, @ManyToOne과 같은 HQL을 변경시키는 Relation을 쓰지 않아도 됩니다.
  2. LTW와 같은 성능저하가 작습니다.
  3. CTW와 같이 build시에 따로 처리할 필요가 없습니다.

reference


Posted by Y2K
,

Hibernate @OneToOne

Java 2014. 8. 19. 01:50

Hibernate @OneToOne

  • 기본적으로 @OneToOne을 사용하지 않는 것이 좋다. @OneToOne의 경우에는 Lazy Loading에 심각한 문제가 있고, 이는 전체 객체에 대한 어마어마한 로딩을 가지고 오는 결과를 가지고 온다.
  • @SecondaryTable로 해결할수도 있으나, BL에 따라서 생각하는 것이 좋다.

기본적으로 다음 기준을 따른다.

  • parent가 되는 entity를 결정하고, 그 entity가 나중에 insert되는 senerio를 택한다.
  • 여러 table의 집합 정보를 가지게 된다면 그 table은 child로 구성한다.

예시

예를 들어 다음과 같은 BL이 존재한다면 Entity는 다음과 같이 구성되어야지 된다.

  • Book과 Note가 존재하고, 둘의 Summary를 지정한다.
  • Book, Note와 Summary는 @OneToOne 관계를 가지게 된다.

이럴때, Entity code의 구성은 다음과 같다.

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String title;

    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "bookId", nullable = false, unique = true)
    private Summary summary;
}


@Entity
public class Note {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String title;

    @OneToOne
    @JoinColumn(name = "noteId")
    private Summary summary;
}

@Entity
public class Summary {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String context;
}

위 entity로 Book을 추가하는 code는 다음과 같이 구성된다.

@Override
public Book add(String name) {
    SessionFactory sessionFactory = SessionUtils.build();
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    try {
        Book book = new Book();
        book.setTitle(name);

        Summary summary = new Summary();
        summary.setContext("CONTEXT FROM BOOK");
        book.setSummary(summary);
        session.save(book);
        transaction.commit();

        return book;
    } catch(Exception ex) {
        transaction.rollback();
        throw ex;
    } finally {
        session.close();
    }
}

위 코드가 실행되면, 다음과 같은 Query 결과를 보여준다.

Hibernate: insert into Summary (context) values (?)
Hibernate: insert into Book (bookId, title) values (?, ?)
Hibernate: select this_.id as id1_0_0_, this_.bookId as bookId3_0_0_, this_.title as title2_0_0_ from Book this_

DB로 생각하면, 먼저 insert될 정보가 main, parent가 되어야지 되고, child는 나중에 insert가 되어야지 된다고 생각하기 쉽다. 그렇지만, 이는 @OneToMany로 지정된 parent-child 구조에서 이렇게 되는 것이고, @OneToOne의 경우에는 child가 먼저 저장이 되어야지 되는 것을 명심하자. 이는 DB Table의 구조에 지대한 영향을 미치게 된다.

DB 구조

  • @OneToMany의 경우, child에서 parent PK를 갖는 구조가 되어야지 된다.
  • @OneToOne의 경우, parent에서 child PK를 갖는 구조가 되어야지 된다.

DB의 구조는 BL을 따라가기 때문에, 어떤 기준으로 검색을 해야지 되는지에 따라서 Table구조가 바뀐다면 위 원칙만을 기억하고 처리하면 가장 좋을 것 같다.

@OneToOne에서의 Lazy 문제

기본적으로 @OneToOne은 Early Loading을 하게 된다. 그 이유는 null 값이 가능한 OneToOne child를 Proxy화 할 수 없기 때문이다. (null이 아닌 proxy instance를 return하기 때문에 DB값의 null을 표현하는 것이 불가능하다.) 따라서 JPA 구현체는 기본적으로 @OneToOne에서 Lazy 를 허용하지 않고, 즉시 값을 읽어 들인다. Lazy를 설정할 수 있지만, 동작하지 않는다.

@OneToOne에서 Lazy Loading을 가능하게 하기 위해서는 다음과 같은 처리가 필요하다.

  • nullable이 허용되지 않는 @OneToOne 관계. (ex: Plan과 PlanResult)
  • 양방향이 아닌, 단방향 @OneToOne 관계. (parent -> child)
  • @PrimaryKeyJoin은 허용되지 않음.

위 3가지 조건을 모두 만족하는 code는 다음과 같다.

    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "bookId", nullable = false, unique = true)

양방향 @OneToOne Lazy loading entity

기본적으로 Lazy Loading을 위해서는 양방향 으로는 되지 않는다. 되게 하기 위해서는 다음 site들을 참고하길 바란다.

Summary

  • Hibernate에서 @OneToOne은 피할수 있으면 최대한 피하라. (@SecondTable과 같은 방법이 있다.)
  • @OneToMany와 @OneToOne은 parent, child의 저장 순서가 다르다.
    • @OneToOne에서는 child가 먼저 저장이 되어야지 되고, @OneToMany는 parent가 먼저 저장이 되어야지 된다.
  • @OneToOne을 Lazy loading하고자 하면, 반드시 다음 3가지 조건을 지켜야지 된다.
    • nullable이 허용되지 않는 @OneToOne 관계만이 허용된다. (ex: Plan과 PlanResult)
    • 양방향이 아닌, 단방향 @OneToOne 관계만이 허용된다. (parent -> child)
    • @PrimaryKeyJoin은 허용되지 않는다.


Posted by Y2K
,

Dependency Injection

DI란 사용될 Object들이 의존성을 갖는 여러 객체들을 직접 자신이 얻어내는 것을 의미한다. DI는 여러분의 code를 느슨한 결합상태로 만들고, 테스트 하기 쉽게 만들며, 읽기 쉽게 만들어준다.

Java의 Official standard인 JSR-330에서 정의된 DI에 대해서도 역시 알아볼 것이다.

Inject some knowledge - understanding IoC and DI

IoC와 DI의 개념은 매우 헛갈리며, 이 둘을 섞어서 이야기하는 경우가 매우 많다. 이 둘에 대한 내용을 좀더 알아보기로 한다.

IoC

만약에 IoC를 사용하고 있지 않다면, program의 logic은 함수의 조합에 의하여 조절될 것이다. 매우 정밀한 design에 의해서 꾸며진 이 함수들은 여러 Object들에 의해서 재사용이 되어가며 사용되고 있을것입니다.
IoC를 사용한다면, 이에 대한 “central control” 이라는 개념자체를 뒤집게 됩니다. 호출자의 code에 의해 다뤄지는 program의 실행으로 구성이 되며 모든 Program의 Logic은 호출되는 subroutine에 의하여 encapsulated 되게 됩니다.
이는 Design Pattern 중 Hollywood Principal과 동일합니다. 여러분의 code가 호출하는 것이 아닌, 어떤 곳에서 여러분의 code를 호출하는 방식으로의 변경을 의미하게 됩니다.

Text 기반의 mud game이 있고, 이를 GUI Framework로 감싼 Version이 있다고 가정해보도록 합시다. GUI Framework는 어떠한 Logic도 가지고 있지 않습니다. “LEFT Click” 이라는 event가 호출이 되면, 이에 따른 GO LEFT 라는 command가 실행이 되는 것 뿐입니다. 이에 대한 실질적인 로직은 모두 Text 기반의 mud game안에 들어가 있습니다.

IoC에 대해서 이야기한다면, 다른 개념으로 생성자에 대한 접근을 볼 수 있습니다. 우리가 일반적인 개발방식으로 Program Logic을 생성한다면, 많은 생성자를 Call 하는 Code내에서 생성하게 되는 것을 볼 수 있습니다. 그런데, 이와 같은 방식을 뒤집에서 이미 생성되어 있는, 또는 생성할 방법이 결정나 있는 Factory Pattern을 통해 구성되는 객체들로 만들어진다면 이는 객체에 대한 Control을 객체에게 넘겨준 IoC가 적용된 상태라고 할 수 있습니다.

DI (Dependency Injection)

DI는 IoC의 일부입니다. 이는 여러분의 code안에 있는 객체의 dependency를 code 바깥에서 code가 생성되거나 실행될 때, 주입(Inject)하는 것에 촛점이 맞춰져있습니다.

IoC Container를 Spring에서 이야기한다면, ApplicationContext가 그 역활을 합니다.
DI의 경우, @Autowired가 그 일을 담당하게 됩니다.

DI를 구성하는 경우 다음과 같은 장점을 갖습니다.

Loose coupling

많은 코드들 내부에서 가지고 있는 new를 통한 객체의 생성을 하지 않기 때문에, 객체에 대한 coupling이 작아지게 됩니다. interface를 통한 결합을 하게 된다면, 의존되는 객체에 대한 의존성을 제거하고, 객체의 행위에만 집중할 수 있게 됩니다.

Testability

Loose coupling을 통해, 단순화된 객체들은 Test를 행하기 쉽습니다. Loose coupling이 되지 않은 객체들은 각각의 생성자 및 모든 method의 호출 방법의 차이에 따른 Test가 매우 힘듭니다.

Greater cohesion (높은 응집성)

생성된 code들은 객체에 대한 생성방법이나 여러 부가적 initialize를 할 필요가 없기 때문에, Logic에 대한 높은 응집성을 갖게 됩니다. 이는, code에 대한 가독성을 높여주는 장점을 가지고 있습니다.

Reusable components

loose coupling에 의한 결과로서, 다른 객체에서 사용하기 쉬운 상태로 만들어주고, 이는 객체에 대한 재 사용성을 높여주게 됩니다.

Lighter code

이는 높은 응집성에 의하여 나온 결과입니다. dependency 된 객체에 대한 추가 코드는 더이상 존재하지 않고, 사용자가 작성한 code가 직접적으로 호출하는 부분만이 남겨져 있는 상태입니다. 가독성이 높아지고, 버그가 생길 구석이 좀 더 줄어들수 있습니다.

DI의 표준화

2004년 google Guice의 Bob Lee와 SpringSource의 Rod Johnson은 JSR-330(javax.inject)에 대하여 합의하게 됩니다. 이 합의로 인하여, 요구되는 Framework내에서 표준화된 DI를 사용할 수 있게 되었습니다. 가벼운 Guice Framework를 사용하다가, 보다 많은 기능을 가진 Spring Framework로의 전환이 자유롭게 된 것을 의미합니다.

실질적으로 위의 말은 거의 불가능에 가깝습니다. Guice와 Spring Framework의 성격이 IoC와 DI를 제공하는 것이 사실이지만, Project가 진행되어가는데 있어서, 기본적인 javax.inject의 기능만이 아닌 다른 기능을 이용해서 처리하게 되는 것이 일반적입니다. 왜냐면 javax.inject는 Guice와 Spring의 가장 기본적인 기능중 공통점만을 가지고 있기 때문입니다.
지금, javax를 이용하는 경우는, JNDI를 이용한 Resource의 관리 이외에는 크게 사용되고 있지 않습니다. Spring, Guice 만을 이용하게 되는 것이 일반적이지요.

@Inject annotation

@Inject annotation은 삽입(Inject)될 dependency를 지정합니다. @Inject는 다음과 같은 곳에서 사용가능합니다.

  • Constructor
  • Methods
  • Fields

@Qualifier annotation

@Qualifier annotation은 객체들의 identify를 지정합니다. 이는 뒤에 나올 @Named와 밀접한 관계를 갖습니다.

@Named annotation

@Inject와 같이 사용됩니다. @Qualifier에 의하여 지정된 이름을 가진 객체를 @Named에 의해서 지정하여 삽입(Inject)하게 됩니다.

@Scope annotation

삽입될 객체의 생명주기를 나타냅니다. 이는 다음과 같습니다.

  • @Scope가 선언되지 않는 경우, 삽입(Inject)가 발생할 때마다, 객체를 새로 생성하게 됩니다.
  • @Scope가 선언되는 경우, 삽입(Inject)가 발생될 때, 기존에 생성된 객체가 있는 경우에는 그 객체를 사용하게 됩니다. 이는 thread-safe해야지 될 필요성을 갖게 됩니다.

@Scope annotation은 각 IoC Framework에서 새롭게 정의되어서 사용되는 것이 일반적이니다. 또한 @Singleton annotation이 생긴후, JSR-330에서는 @Singleton을 이용한 객체 선언을 주로 해주는 경우가 더 많습니다.

@Singleton annotation

@Scope가 선언된것과 동일하게 동작합니다. 이는 거의 모든 DI Framework의 기본 동작입니다.

Provide< T > interface

T 객체에 대한 구현이 아닌 Provide<T>에 대한 구현을 요구하는 경우가 있습니다. 이런 경우, 다음과 같은 목적으로 사용되게 됩니다.

  • 여러 instance를 생성해서 사용하는 경우.
  • 객체를 사용할 때, Lazy Loading을 이용해서 객체를 얻어낼 필요가 있을 때.
  • circular dependency를 회피할 목적으로.

Google Guice를 이용한 DI sample code

/**
 * Created by ykyoon on 14. 8. 5.
 * code에 @Inject가 될 대상
 */
public class AgentFinder {
    public void doSomeThing() {
        System.out.println("Print SomeThing");
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * @Inject를 이용해서 서비스 받을 대상
 */
public class HollyWoodService {
    private AgentFinder agentFinder;

    @Inject
    public HollyWoodService(AgentFinder agentFinder) {
        this.agentFinder = agentFinder;
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * AgentFinder에 대한 dependency될 객체 설정 - Spring의 @Configuration과 거의 동일
 */
public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class);
    }
}

위 코드에서 보면 알수 있듯이, AbstractModule을 이용한 Configuration이 행해진다는 것에 유의할 필요가 있다. DI를 사용하는 code를 JUnit test code로 작성하면 다음과 같다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho = injector.getInstance(HollyWoodService.class);
        assertThat(ho, is(not(nullValue())));
    }
}

Google Guice의 AbstractModule을 보면 Spring의 @Configuration과 매우 유사합니다. 그리고 문법의 경우에는 .NET 진영의 ninject와 거의 유사합니다. (서로간에 영향을 받은건지, 아니면 ninject가 영향을 받은 건지는 잘 모르겠습니다.)

이번에는 객체의 생명주기를 결정하는 Scope입니다.

Scope를 이용한 객체 생명주기의 설정

AppModule내에서 객체의 생명주기를 결정 가능합니다.

public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class)
                .annotatedWith(Names.named("primary"))
                .to(AgentFinderImpl.class)
                .in(Singleton.class);
        bind(HollyWoodService.class);
    }
}

위 구성으로 다음 테스트 코드를 실해하면 결과는 다음과 같습니다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho1 = injector.getInstance(HollyWoodService.class);
        assertThat(ho1, is(not(nullValue())));
        HollyWoodService ho2 = injector.getInstance(HollyWoodService.class);
        System.out.println(ho1);
        System.out.println(ho2);
        assertThat(ho1 != ho2, is(true));

        System.out.println(ho1.getAgentFinder());
        System.out.println(ho2.getAgentFinder());
        assertThat(ho1.getAgentFinder() == ho2.getAgentFinder(), is(true));
    }
}
me.xyzlast.gg.hollywood.HollyWoodService@2b9627bc
me.xyzlast.gg.hollywood.HollyWoodService@65e2dbf3
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0

Singleton으로 설정된 객체는 호출될 때, 기존의 객체가 있는 경우에는 그 객체를 계속해서 사용하게 됩니다. 그렇지만, 아무것도 설정되지 않은 객체의 경우에는 마치 new를 통해서 생성되는 객체와 동일한 패턴을 따르게 됩니다.


Posted by Y2K
,