Tech/JavaScript2017.11.10 12:31

Javascript Unit Test를 수행하기 위해 Mocha, Jasmine, Qunit 중에 검토하다가 비교적 가장 최신 기술이고, Mock을 쓰기 편한 쪽이라 추측하여, Mocha의 사용법을 알아보았다. (Unit Test를 작성할 때, BDD, TDD 두 가지 스타일을 모두 제공한다는 점에서도 끌렸다.)


설치

  1. chai를 설치한다.

    npm install -g chai
    (or npm install chai --save-dev)
  2. mocha를 설치한다.

    npm install -g mocha



BDD Unit Test

  1. 모듈을 작성한다. (app.js)

    (function(exports) {
      "use strict";
     
      function Tester() {}
      exports.Tester = Tester;
     
      Tester.prototype = {
        sum: function (a, b) {
          return a + b;
        }
      };
    })(this);



  2. 테스트 코드를 작성한다. (app.test.js)

    var expect = chai.expect;
     
    describe("BDD Test"function() {
      describe("더하기 테스트"function() {
        it("2와 4를 합했으니, 6이 될 것이다."function() {
          var tester = new Tester();
          expect(tester.sum(2, 4)).to.equal(6);
        });
      });
    });



  3. 테스트를 수행 결과를 표시하기 위한 html을 작성한다. (index.html)

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>BDD unit test</title>
      <!-- mocha 스타일 적용 -->
      <link rel="stylesheet" media="all" href="../component/mocha/mocha.css">
    </head>
    <body>
      <div id="mocha"><p><a href=".">Index</a></p></div>
      <div id="messages"></div>
      <div id="fixtures"></div>
      <!-- mocha.js 로드 -->
      <script src="../component/mocha/mocha.js"></script>
      <!-- chai.js 로드 -->
      <script src="../component/chai/chai.js"></script>
      <script src="app.js"></script>
      <!-- bdd 스타일 UnitTest임을 설정함 -->
      <script>mocha.setup('bdd')</script>
      <script src="app.test.js"></script>
      <!-- UnitTest 수행 -->
      <script>mocha.run();</script>
    </body>
    </html>



  4. html을 실행시켜본다.

    1. 성공했을 때
    2. 실패했을 때

 

 

Command Line Test

  1. 모듈을 작성한다. (app.js)

    module.exports = {
      sum: function(a,b) {
        return a+b;
      }
    }



  2. 테스트 코드를 작성한다. (app.test.js)

    var chai = require('chai');
    var assert = chai.assert;
    var app = require('../app.js');
     
    describe('Sum Test'function() {
      it("1과 2를 더하면, 3이 될 것이다."function() {
        assert.equal(3, app.sum(1,2));
      });
    });



  3. Command Line으로 결과를 확인한다.

     

MockTest

  1. Sinon.JS를 다운로드 받는다. (사이트 주소 : http://sinonjs.org/)
    1. Document : http://sinonjs.org/docs/
    2. 참고 사이트 : http://elijahmanor.com/unit-test-like-a-secret-agent-with-sinon-js/
       
  2. 다운로드 받은 sinon.js를 추가한다. (index.html)

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>BDD unit test</title>
      <!-- mocha 스타일 적용 -->
      <link rel="stylesheet" media="all" href="../component/mocha/mocha.css">
    </head>
    <body>
      <div id="mocha"><p><a href=".">Index</a></p></div>
      <div id="messages"></div>
      <div id="fixtures"></div>
      <!-- mocha.js 로드 -->
      <script src="../component/mocha/mocha.js"></script>
      <!-- chai.js 로드 -->
      <script src="../component/chai/chai.js"></script>
      <!-- sinon.js 로드 -->
      <script src="../component/sinon/sinon-1.17.3.js"></script>
      <script src="app.js"></script>
      <!-- bdd 스타일 UnitTest임을 설정함 -->
      <script>mocha.setup('bdd')</script>
      <script src="app.test.js"></script>
      <!-- UnitTest 수행 -->
      <script>mocha.run();</script>
    </body>
    </html>



  3. 테스트를 위한 클래스를 정의한다. (person.js)

    (function(exports) {
      "use strict";
     
      function Person() {}
      exports.Person = Person;
     
      Person.prototype = {
        setName: function(name) {
          this.name = this.returnName(name);
        },
        returnName: function(name) {
          return name;
        }
      };
    })(this);



  4. Mock Test 코드를 작성한다. (person.test.js)
    (tick) "setName" 함수를 호출했을 때, "returnName" 함수를 호출하는지 확인한다.

    var expect = chai.expect;
     
    describe("Person"function() {
      describe("constructor"function() {
        it("Mock Test"function() {
          var person = new Person();
          var mock = sinon.mock(person);
     
          mock.expects("returnName").once().withExactArgs("Betty");
          person.setName("Betty");
     
          mock.verify();
          mock.restore();
        });
      });
    });



  5. 결과를 확인한다.



저작자 표시 비영리 변경 금지
신고

'Tech > JavaScript' 카테고리의 다른 글

[Mocha Unit Test]  (0) 2017.11.10
Javascript Task Runner : Grunt 의 개요 및 장점  (0) 2015.06.05
Posted by 벚꽃손님
Tech/Android 개발2015.07.09 18:29

Android Studio에서 Junit 기반 단위 테스트(Unit Test) 환경 구축하기

안드로이드 Instrumentation 테스트의 경우에는 안드로이드 장치 또는 시뮬레이터에 직접 테스트 코드를 올려서 테스트가 진행됩니다. 이러한 과정 때문에 비교적 느립니다. 다른 방식으로 데스크탑 환경에서도 테스트를 할 수 있습니다. 이 방식에서는 보통 Junit을 기반으로 다양한 Mocking 라이브러리 또는 Robolectric과 같은 프레임워크를 사용하여 테스트가 진행됩니다. 이 포스트에서는 이런 방식의 가장 기본이 되는 Junit 테스트 환경을 구축하는 방법에 대해 설명하겠습니다.


진행환경

* Window 8.1  /  * Android Studio 1.2.2


1. 프로젝트 생성

Blank Activity 프로젝트를 기본 옵션으로 하나 생성합니다.


2. Unit Test 폴더 구조 생성

안드로이드에서 Instrumentation 테스트의 경우는 src/androidTest/java를, Junit 테스트의 경우에는 src/test/java를 기본 테스트 폴더로 인식합니다. 

프로젝트를 생성하시면 기본적으로 androidTest 폴더가 생성되어있을 것입니다. 상단 좌측의 프로젝트 구조 보기를 'Project'로 선택하시고 androidTest폴더에서 우클릭 -> Refactor -> Rename 을 통해 test로 이름을 변경하면 됩니다.


3. 빌드 스크립트에 Junit 추가

app의 build.gradle 파일을 열어서 dependency에 testCompile 구문을 추가합니다.

dependencies {
  ...
  testCompile 'junit:junit:4.12'
  ...
} 

메뉴에서 Tools -> Android -> Sync Project with Gradle Files 를 선택해 Gradle 파일을 Sync 시켜 줍니다.


4. Test Artifact 설정

좌측 하단 Build Variant 탭을 클릭한 후 Test Artifact를 Unit Tests로 설정해 줍니다.

여기까지 하시면 안드로이드가 java 폴더를 테스트 폴더로 인식하여 첫번째 사진과 같이 초록색 아이콘 으로 표시되어야 합니다.


5. Unit Test 코드 작성

이제 환결 설정은 다 끝났습니다! 작동하는지 알아보기 위해 간단히 Calculator 클래스를 만들어서 두 정수를 더하는 메소드를 테스트 해보겠습니다.

아래와 같이 Calculator랑 CalculatorTest 클래스를 생성하고 코드를 써줍니다.

 Calculator.java

CalculatorTest.java 

public class Calculator {
  public int add(int a, int b) {
    return a+b;
  }
}

import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class CalculatorTest {
  @Test
  public void testAdd() {
    Calculator calculator = new Calculator(); 
    int actual = calculator.add(10, 10); 
    int expected  = 10+10;
    assertTrue(actual == expected);
  }
}


6. 테스트하기

테스트 java 폴더에서 우클릭 -> Run -> All Tests를 통해 테스트를 실행할 수 있습니다. 성공하면 아래와 같은 결과가 나옵니다.

끝!


참조

https://developer.android.com/training/testing/unit-testing/local-unit-tests.html
http://tools.android.com/tech-docs/unit-testing-support


신고
Posted by 비둘기야
Tech/Android 개발2015.06.19 17:02

SignalR 이란?

웹 서버와 브라우저 간의 실시간 양방향 통신을 위해 HTML5 표준인 WebSocket이 있습니다. 그러나 아직 WebSocket이 지원되지 않는 환경이 많습니다. Signal R은 MS에서 만든, 추상화를 통해 단일한 API로 서버와 클라이언트 간의 실시간 양방향 통신을 가능하게 해주는 라이브러리입니다. 내부적으로는 환경에 따라서 WebSocket을 사용하기도 하고 Long Poliing을 사용하기도 하는 등 다양한 방식을 사용합니다. 또한, 이러한 기능을 하는 대표적인 라이브러리로는 Node.js 기반의 Socket.IO가 있습니다! 


Android에서 SignalR 사용하기

만들려고 하는 예제

  • 서버와 클라이언트 허브에 각각 'hello' 라는 메소드가 있고 이 메소드는 문자열 하나를 인자로 받습니다.
  • 안드로이드 클라이언트에서 서버에 hello 메소드를 통해 문자열을 보내면 서버도 클라이언트로 hello 메소드를 통해 같은 문자열을 보냅니다.
  • 안드로이드 앱은 서버로부터 문자열을 받으면 Toast 메시지로 이를 보여줍니다.

구현하기 

1. 라이브러리 추가
signalR 자바 및 안드로이드 버전 클라이언트 라이브러리와 gson이 필요합니다. signalR 라이브러리들은 현재 jar을 공식적으로 받을 수 있는 곳은 없고 github 저장소에서 소스를 빌드하여 사용하셔야 합니다. gson은 로컬에 jar이 없어도 jcenter나 maven 저장소의 dependency를 지정함으로써 사용할 수 있긴합니다. 일단 이 예제를 진행하면서 빌드해 놓은 라이브러리 파일들과 bintray에서 받은 gson jar 파일을 첨부합니다. 

dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.google.code.gson:gson:2.3.1' compile files('libs/signalr-client-sdk.jar') compile files('libs/signalr-client-sdk-android.jar') }


2. Permission 추가 

네트워크 통신을 사용하므로 <uses-permission android:name="android.permission.INTERNET"/> 를 Android Manifest 파일에 추가해 줍니다.


3. 멤버변수 선언

public class MainActivity extends ActionBarActivity {

    HubConnection mConnection;
    HubProxy mHub;

MainActivity.java를 생성하고 통신의 주체가 되는 모듈인 HubConnection 과 HubProxy를 선언해 줍니다. 여러 곳에서 사용되기 때문에 이번 예제에서 간편함을 위해 멤버변수로 선언하였습니다.


4. 연결 설정 및 초기화

    private void initialize() {
        String serverUrl = "http://server.url";
        String hubName = "hubName";

        Platform.loadPlatformComponent(new AndroidPlatformComponent());
        mConnection = new HubConnection(serverUrl);
        mHub = mConnection.createHubProxy(hubName);
    }

SignalR 서버 Url과 서버에서 사용하는 hubName을 설정합니다. 


5. 메시지 받을 준비 하기

    private void prepareGetMessage() {
        mHub.on("hello", new SubscriptionHandler1<String>() {
            @Override
            public void run(final String msg) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_LONG).show();
                    }
                });
            }
        }, String.class);
    }

Hub의 on 메시지를 통해서 서버가 hello 메소드를 호출할 경우의 리스너를 등록합니다. 앞서 말씀드렸듯이 이 메소드는 인자를 하나만 받기 때문에 리스너로 SubscriptionHandler1을 사용하였습니다. 인자가 여러 개일 경우 개수에 따라서 SubscriptionHanlder2, SubscriptionHanlder3, ... 클래스를 사용하시고 알맞은 클래스 타입을 설정해주면 됩니다. 
( 안드로이드는 아직 Java8을 지원하지 않아 Lambda Expression을 사용하지 못하여 코드가 상당히 기네요ㅜㅜ ) 


6. 연결 성립하기

private void connectToServer() { try { SignalRFuture<Void> awaitConnection = mConnection.start(); awaitConnection.get(); Toast.makeText(this, "Connected", Toast.LENGTH_LONG).show(); } catch(Exception e) { Log.e("SignalR", "Failed to connect to server"); } }

동기 방식(Synchronous)으로 연결을 합니다. 따라서 Exception 없이 awaitConnect.get() 구문이 수행되고 나면 연결이 성립되었다고 볼 수 있습니다. 안드로이드에서 네트워크 통신 작업은 UI Thread에서 수행이 불가능하지만 위 메소드는 UI Thread에서도 호출이 가능합니다. 왜냐하면 실질적인 통신작업은 사실상 비동기로 이루어지고 awaitConnect.get()이 세마포어를 획득하기 위해 기다리는 식으로 구현되었기 때문입니다. 


7. 메시지 보내기

    private void sendMessage(String msg) {
        try {
            mHub.invoke("hello", msg).get();
        } catch( Exception e ){
            Log.e("SignalR", "Fail to send message");
        }
    }

Hub의 invoke 메소드를 통해 서버 Hub 메소드를 호출하는 식으로 메시지를 보냅니다. 이 역시 연결 성립할 때와 같은 방식으로 동기로 진행되며 UI Thread에서도 사용 가능합니다.


8. 연결 해지

    @Override
    protected void onDestroy() {
        mConnection.stop();
        super.onDestroy();
    }

stop 메소드를 통해 연결을 해지합니다. 이 예제에서는 앱의 onDestory가 호출 될 때 연결 해지를 하였습니다! 


9. 최종 결과 앱


지금까지 말씀드린 내용을 구현한 소스파일을 첨부합니다.

activity_main.xml

MainActivity.java


참조

Github : SignalR/java-client

Github : SignalR/java-samples

Getting Started with the Java SignalR SDK



신고
Posted by 비둘기야
Tech/JavaScript2015.06.05 18:07

기존에 Xinics에서는 HTML5 Javascript 어플리케이션 배포를 위해 빌드 시 Ant 스크립트를 사용해 왔었습니다. Ant 스크립트로 Javascript 빌드를 할 때의 여러 가지 한계와 불편한 점으로 인해 대안을 조사하였습니다. 그 일환으로 Grunt에 대해 조사한 내용을 공유합니다.

1. Grunt란? 

Grunt는 Javascript를 사용한 Task Runner입니다. 즉 반복적인 작업을 스크립트를 통해서 한 번에 수행하게 해주는 자동화 도구입니다. Javascript 앱 빌드의 경우에는 여러 가지 JS 및 CSS 파일을 하나로 합치고 minify 하며 또 어떤 파일은 배포 폴더로 복사하고 하는 과정들을 스크립트를 작성하여 한 줄의 명령어를 통해 자동화시킬 수 있습니다. Makefile, Ant 등의 Javascript 버전이라고 볼 수 있습니다. Grunt와 Grunt 플러그인들은 npm(Node.js Package Manager)을 통해 설치되고 관리됩니다. 즉 Node.js 모듈입니다. 자세한 내용은 공식 웹사이트에서 확인하실 수 있습니다.


2. Grunt의 장점

1. Javascript/Node.js 기반의 빌드 툴이다.

Javascript 및 Node.js는 활발하게 유지 보수되며 성장하고 있는 언어 및 플랫폼입니다. 따라서 안정성 및 전망에서 많은 기대를 할 수 있습니다. 


2. Javascript 어플리케이션에서 사용되는 다양한 기능들을 안정적인 플러그인으로 제공한다.

jshint, minify, uglify, less 등 Javascript 어플리케이션에서 주로 사용되는 대부분 기능을 플러그인으로 제공하고 있습니다. 또한, Ant보다 안정적입니다. 플러그인의 안정성과 관련하여 Xinics에서 Ant를 사용하는 동안은 몇 가지 문제점이 있었습니다. Ant 스크립트에서 Javascript minify를 위해 사용하는 Yui Compressor 최신 버전( 2.4.8 )은 Window 환경에서 PATH 문제로 인해 기본 동작을 하지 않는 이슈가 있었습니다.또한 Ant 스크립트에서 jshint를 지원하는 라이브러리 ant-jshint 는 더이상 진행되고 있지 않습니다(!)


3. 작성 및 유지보수가 쉬운 빌드 스크립트를 제공한다.

간단하고 직관적인 설정 및 빌드 스크립트를 제공합니다. 또한, Javascript 언어 기반으로 되어 있기에 만약 플러그인이 지원하지 않는 기능에 대해서는 직접 소스코드를 통해서 구현할 수 있다는 유연성도 가지고 있습니다. Ant 등의 다른 빌드 툴에 비해 어느 정도 간결한지는 아래 나타나 있습니다.


출처 : Whe We Use Node.js and Grunt to Build Javascript

4. 커뮤니티가 크고 활발하다.

Grunt 외에도 Node.js 기반으로 빌드하는 방식은 Gulp를 사용하는 방식과 npm 스크립트를 사용하는 방식이 있습니다. 앞서 거론한 장점들은 이 방식들도 가지고 있습니다. 그래서 Grunt, Gulp, npm 스크립트 중 어떤 것이 좋은지에 대해 다양한 의견이 있습니다. 자세한 내용은 아래 포스트들에서 살펴보실 수 있습니다.

The Battle of Build Scripts: Gulp vs Grunt
Why We should stop using Grunt & Gulp

그러나 Grunt가 확실히 장점을 가지는 부분은 가장 커뮤니티가 넓고 활발하다는 점입니다. 다양한 플러그인들이 개발되었고 많은 사용자층이 있으며 유지보수 및 업데이트가 잘 진행되고 있습니다. 또한, jQuery, Wordpress, Twitter 등 유명 라이브러리 및 플랫폼에서 빌드를 위해 Grunt를 사용한다는 것 또한 신뢰를 주는 부분입니다.

P.S 한국어 페이스북 커뮤니티도 있습니다! https://www.facebook.com/groups/gruntjs.kr/


신고

'Tech > JavaScript' 카테고리의 다른 글

[Mocha Unit Test]  (0) 2017.11.10
Javascript Task Runner : Grunt 의 개요 및 장점  (0) 2015.06.05
Posted by 비둘기야
Tech/Android 개발2015.04.13 10:31

알려진 안드로이드 HLS 스트리밍 이슈들

안드로이드에서 HTML video 태그를 이용하여 HLS를 서비스할 경우 정상적인 시청 환경을 제공할 수 없을 정도로 이슈가 많다는 것은 널리 알려진 사실입니다. 아래는 대표적인 HTML5 동영상 플레이어 중 하나인 JWPlayer에서 정리한 이슈 입니다.


Android 2.3 (Gingerbread)
  · 지원 안함
Android 3.0 (Honeycomb)
  · HLS 스트림이 장치와 충돌을 일으킨다.
Android 4.0 (ICS)
  · VOD에서 seek 불가능
  · 영상 가로세로 비율이 감지되지 않아 이미지 변형을 일으킨다.
  · 전체화면 진입시 영상이 처음부터 다시 시작된다.
Android 4.1+ (Jelly Bean)
  · 영상 가로세로 비율 이슈는 수정되었지만 여전히 seek이 불가능하다.
  · 크롬에서 HLS를 인식하지 못해 mimetype 을 감지하지 못한다.
  · 전체화면 진입 시 오류가 발생하고 장치가 멈춘다.


Seek 불가, 화면 비율 깨짐, 전체화면 진입시 오류 발생 등 굵직한 이슈들이 많이 보입니다. 


2015년 4월, 국내 주요 기기들에서의 안드로이드 HLS 이슈

위 JWPlayer 포스트는 약 2년전인 2013년 2월 27에 작성되었습니다. 모바일 세상에서의 변화의 속도를 감안하면 결코 짧지 않은 약 2년여의 시간이 지난 지금은 이슈들이 많이 없어졌을까요? 자이닉스에서 현재 사용되고 있는 국내 주요 기기들에서도 이슈들이 발생하는지 테스트를 진행해 보았습니다. 

테스트 환경

1) 서버 환경
 Wowza 3.6.2

2) 테스트 기기 목록

 번호

기기명 

 운영체제

제조사

발매일 

 Galaxy S3

 4.1 (Jelly Bean)

 삼성 

 2012.09

 Galaxy Note2

 4.4 (KitKat) 

 삼성

 2012.09

 Galaxy Note3

 4.4 (KitKat)

 삼성

 2013.09

 Galaxy S5

 5.0 (Lollipop) 

 삼성

 2014.03

 VEGA Racer

 4.1 (Jelly Bean) 

 팬텍

 2011.06

 VEGA IRON2

 4.4 (KitKat)

 팬텍

 2014.05

 Optimus LTE2

 4.1 (Jelly Bean) 

 LG

 2012.05

 LG G Pro2

 4.4 (KitKat)

 LG

 2014.02

 Nexus5

 5.1 (Lollipop)

 LG

 2013.10

* 위 테스트 기기들에 대해 기본 브라우저 및 Chrome 에서 진행
* 단 Nexus5는 Chrome이 기본 브라우저여서 Chrome만 진행함.
* Chrome 버전은 41로 테스트를 진행

테스트 결과

이슈 내용 

 발생한 환경

 영상이 가로세로 비율이 무시되고 화면에 꽉찬다.

 ① Galaxy S3-기본 브라우저
 
② Galaxy Note2-기본 브라우저

 전체화면[각주:1] 진입 시 영상의 끝으로 간다.

 ① Galaxy S3-기본 브라우저

 현재 시간이 계속 0초로 남아있다.

 ① Galaxy S3-Chrome
 
⑤ VEGA Racer-Chrome

 Seek이 불가능하다.

 ① Galaxy S3-Chrome
 ② Galaxy Note2-Chrome
 
⑤ VEGA Racer-Chrome

 영상 전체 길이가 짧아진다. ( 예: 59분 59초 영상 -> 56분 45초 )

 ② Galaxy Note2-기본 브라우저
 
③ Galaxy Note3-기본 브라우저, Chrome
 
⑤ VEGA Racer-Chrome
 
⑥ VEGA IRON2-기본,브라우저, Chrome

비교적 최신 기기들과 OS에서는 이슈가 없는 경향을 보이지만 테스트 표본이 작고 명시적으로 보장한다는 명세가 없어 확신은 할 수가 없습니다. 또한 나온지는 조금 지났어도 아직 제법 높은 점유율을 가지고 있는 Galaxy S3나 Note2등의 기기에서는 확실히 정상적인 시청이 불가능할 정도의 많은 이슈가 있습니다. 따라서 아직까지는 안드로이드 HTML5에서 HLS 스트리밍을 제공하기에는 많은 어려움이 따른다는 결론을 낼 수 있을 것 같습니다.


안드로이드 네이티브 App으로 HLS 스트리밍을 할 때의 이슈

안드로이드 HTML HLS 스트리밍의 대안으로써 네이티브 앱을 제작하는 방법이 종종 제안되고는 합니다. 간단히 시청만 가능한 프로토타입 앱[각주:2]을 제작하여 위와 같은 테스트 환경에서 확인해 보았습니다. 다수의 기기에서 어느정도 시청은 가능한 수준이라고 볼 수 있으나, 아래와 같은 공통적인 이슈가 발생하였습니다. 또한 아래 이슈들은 내장 플레이어에서도 동일하게 발생함을 확인하였습니다.

이슈내용
 * 영상 전체 길이가 짧게 나타난다. (59분 59초 영상 -> 56분 45초)
 * Seek시 Seek한 시간보다 뒤로 이동한다. ( 25:00 Seek -> 26:30으로 이동 )
 * 작게 측정된 전체 시간 이후의 시간을 재생하고 있는 동안은 Seek이 안 되는 현상 발생

발생 기기
 
① GalaxyS3② Galaxy Note2, ③ Galaxy Note3, ⑤ VEGA Racer


  1. 전체화면은 video 엘리먼트의 webkitEnterfullscreen 메소드를 의미합니다. [본문으로]
  2. 안드로이드 SDK의 VideoView를 사용 [본문으로]
신고
Posted by 비둘기야
Tech/닷넷 일반2014.06.07 19:58

Visual Studio에서 제공하는 테스트 도구에 NUNIT을 결합해서 사용할 수 있다면 정말 유용할 것입니다. 이번 웹 캐스트는 Visual Studio에서 제공하는 테스트 도구와 NUNIT 간의 연동을 도와주는 NUNIT Test Adapter의 사용 방법과 함께, Windows Forms 기반 응용프로그램에서 Visual Studio와 NUNIT Test Adapter를 결합하여 테스트를 진행할 수 있는 방법을 소개합니다.

아래 이미지를 클릭하시면 웹 캐스트가 시작됩니다.


링크 바로 가기: http://cms.rkttu.com/em/5392cf5a56846

This web cast powered by XINICS SilverStream & Commons

저작자 표시 비영리 변경 금지
신고
Posted by 남정현 (rkttu.com)
Tech/닷넷 일반2013.10.25 22:00

NUnit의 GUI Runner는 여러 개의 테스트 유닛 프로젝트를 로드하여 동시에 테스트 결과를 시각적으로 확인할 수 있는 매우 유용한 유틸리티입니다. 그러나 한 가지 아쉬운 점이 있다면, Visual Studio와 완벽하게 통합되어있지는 않아서 단위 테스트 도중 변수의 상태를 확인하거나 디버깅을 하기에는 불편한 구조로 제작되어있다는 점입니다. 그래서 개인적으로 자주 애용하는 대안으로 Reflection을 사용하여 Test Fixture와 Test Case를 검색하여 자동으로 호출하는 유틸리티 클래스의 소스 코드를 https://github.com/rkttu/nunit-self-runner 에 게시하였습니다.

이 프로그램 코드는 NUnit Framework 어셈블리 외에 특별한 종속성이 없고 어떤 코드에서든 쉽게 붙여넣어 시작할 수 있습니다. 그러나 기능 상의 제약이 있는데 다음과 같은 유형의 Test Fixture나 Test Case에서는 작동하지 않습니다.

  • Test Fixture 생성 시 별도의 생성자 매개 변수가 필요한 경우
  • Test Method 실행 시 별도의 호출 매개 변수가 필요한 경우
  • private이나 protected, internal 멤버

이 소스 코드를 NUnit 클래스 라이브러리 프로젝트에 추가하고, 해당 NUnit 클래스 라이브러리를 컴파일하여 실행하면 다음과 같은 형태로 단위 테스트가 전개될 것입니다.

 

 

테스트에 실패하는 케이스, 즉 Exception이 발생하면 위와 같이 적색의 Test case failed 라는 문구가 나타나고 자세한 Stack Trace 결과가 노란색의 텍스트로 표시되어 시각적으로 구분을 쉽게 해줍니다. 그리고 실패했다는 사실을 알리기 위하여 테스트가 일시 중단되고, Enter 키를 누르면 계속 실행됩니다. 이 메시지를 확인하고 적절한 위치에 중단점을 설정하면 디버거가 해당 위치에서 중지되므로 좀 더 쉽게 문제를 진단할 수 있습니다.

 

 

반면 예외 없이 정상적으로 실행되는 테스트 케이스는 초록색의 Test case succeed 메시지를 표시하고 중단없이 계속 다음 테스트를 진행합니다. 그리고 한 Test Fixture의 실행이 완료되면 다시 사용자의 입력을 대기하는 상태로 들어가며, Enter 키를 누르면 다음 Test Fixture로 진행할 수 있으므로 인터랙티브하게 단위 테스트 결과를 확인할 수 있습니다. 

 

모든 테스트 Fixture의 실행이 끝난 이후에도 한 번 더 사용자의 입력을 기다립니다. 콘솔에 표시된 전체 내용을 리뷰하고 마지막으로 Enter 키를 누르면 프로그램이 완료됩니다.

저작자 표시 비영리 변경 금지
신고
Posted by 남정현 (rkttu.com)
Tech/닷넷 일반2013.10.16 22:00

C#에서 프로그램 코드를 전개하는 방법은 상대적으로 다른 언어에 비해 자유도가 높은 편입니다. 그렇지만 이런 기능들을 잘 모를 경우 코드 품질이 낮아질 수도 있고, 이해하기 어려운 코드가 되기 쉽습니다. 이러한 문제점을 극복할 수 있는 실용적 코드 작성 팁 몇 가지를 공유해보도록 하겠습니다.

양보하기 어려운 변수 작명을 만났다면?

코딩을 하다보면 그런 경우가 있습니다. 밖으로 드러내는 것이든, 안에서 사용하는 것이든 코드의 의도를 정확히 설명하기 위해서 양보하기 어려운 변수 작명을 고수해야 할 때가 있습니다. 이럴 때에는 고민하지 말고, 변수명 앞에 @ 기호를 지정해주기만 하면 됩니다. C#의 주요 키워드들 (상황에 따라 예약되는 키워드는 이 문제를 만날 가능성이 적습니다.) 상당수를 이 방법을 사용하여 약간 바꾸어 변수 작명으로 채용하는 것이 얼마든지 가능합니다.

string
    @abstract = string.Empty,    @as = string.Empty,    @base = string.Empty,    @bool = string.Empty,
    @break = string.Empty,    @byte = string.Empty,    @case = string.Empty,    @catch = string.Empty,
    @char = string.Empty,    @checked = string.Empty,    @class = string.Empty,    @const = string.Empty,
    @continue = string.Empty,    @decimal = string.Empty,    @default = string.Empty,    @delegate = string.Empty,
    @do = string.Empty,    @double = string.Empty,    @else = string.Empty,    @enum = string.Empty,
    @event = string.Empty,    @explicit = string.Empty,    @extern = string.Empty,    @false = string.Empty,
    @finally = string.Empty,    @fixed = string.Empty,    @float = string.Empty,    @for = string.Empty,
    @foreach = string.Empty,    @goto = string.Empty,    @if = string.Empty,    @implicit = string.Empty,
    @in = string.Empty,    @int = string.Empty,    @interface = string.Empty,    @internal = string.Empty,
    @is = string.Empty,    @lock = string.Empty,    @long = string.Empty,    @namespace = string.Empty,
    @new = string.Empty,    @null = string.Empty,    @object = string.Empty,    @operator = string.Empty,
    @out = string.Empty,    @override = string.Empty,    @params = string.Empty,    @private = string.Empty,
    @protected = string.Empty,    @public = string.Empty,    @readonly = string.Empty,    @ref = string.Empty,
    @return = string.Empty,    @sbyte = string.Empty,    @sealed = string.Empty,    @short = string.Empty,
    @sizeof = string.Empty,    @stackalloc = string.Empty,    @static = string.Empty,    @string = string.Empty,
    @struct = string.Empty,    @switch = string.Empty,    @this = string.Empty,    @throw = string.Empty,
    @true = string.Empty,    @try = string.Empty,    @typeof = string.Empty,    @uint = string.Empty,
    @ulong = string.Empty,    @unchecked = string.Empty,    @unsafe = string.Empty,    @ushort = string.Empty,
    @using = string.Empty,    @virtual = string.Empty,    @void = string.Empty,    @volatile = string.Empty,
    @while = string.Empty,    @__arglist = string.Empty,    @__refvalue = string.Empty,    @__makeref = string.Empty,
    @__reftype = string.Empty;

위의 코드를 컴파일하였을 때 사용하지 않는 변수라는 경고를 제외하고 컴파일에는 이상이 없음을 확인할 수 있습니다.

String.Join 메서드와 같이 시작과 끝에 구분 기호 (Delimiter)가 붙지 않는 문자열 더하기를 수행하는 방법

간혹 그런 경우가 있습니다. 기존 컬렉션으로부터 새로운 컬렉션을 만들면서 시작이나 끝에는 구분자 기호나 원소를 붙이지 않고 중간에만 원하는 내용을 삽입하고 싶을 때가 있는데, 이런 경우 인덱스를 사용하려고 하거나 굳이 배열로 변환하려는 노력을 하게 될 수 있는데, 이는 별로 바람직하지 않습니다. 대신, IEnumerator 인터페이스와 if 문 한번, while 문 한 번으로 나누어 반복문을 써주기만 하면 쉽게 문제가 해결됩니다. 참고로, C#의 foreach 문은 IEnumerator 인터페이스에 대한 포장입니다.

String.Join 메서드와 같은 기능을 하는 메서드를 만들기 위하여, 아래와 같이 코드를 작성할 수 있을 것입니다.

static string Join<T>(string delim, IEnumerable<T> cols)
{
    StringBuilder buffer = new StringBuilder();
    IEnumerator<T> @enum = cols.GetEnumerator();

    if (@enum.MoveNext())
        buffer.Append(@enum.Current);

    while (@enum.MoveNext())
    {
        buffer.Append(delim);
        buffer.Append(@enum.Current);
    }

    return buffer.ToString();
}

위의 메서드를 이용하여 문자열의 각 문자들 사이에 쉼표를 붙이는 것을 쉽게 처리할 수 있습니다.

string modified = Join<char>(", ", "Hello guys!");
Console.WriteLine(modified);

H, e, l, l, o,  , g, u, y, s, !

현재 컴퓨터를 기준으로 언제나 유일한 값을 빠르게 만들어내는 방법

완벽한 의미에서의 유일성은 상당히 많은 Factor를 반영해야만 그 성격을 보장할 수 있습니다. 그러나, 대개의 경우 지구상에서 유일한 값을 만들어내는것 보다는, 현재 실행 중인 컴퓨터나 데이터베이스를 기준으로 유일한 값을 만들어내는 것 정도만으로도 충분히 목표를 달성할 수 있습니다. 이럴 경우에도 매번 GUID를 생성하거나, 데이터베이스의 Identity Seed를 사용하는 것은 비용이 많이 들고, 특히 데이터베이스의 Identity Seed는 데이터베이스마다 커스터마이징 정도의 차이가 있지만 대개는 생성된 값을 클라이언트 측에서 확인하기 어렵기 때문에 Round Trip을 유발합니다.

지금 소개하는 방법은 이러한 문제점을 극복하면서도 매우 빠른 실행 속도를 보장하는 유일 값 생성 방법입니다. 바로, 현재 시스템의 Tick Count를 그대로 이용하는 방법입니다. Tick Count는 100 나노초 단위이므로 일정한 수준에서의 유일성을 보장하기에는 충분한 밀도가 됩니다. 그리고 생성하는 값의 데이터 형식이 64비트 정수이므로 범위 또한 충분히 넓습니다.

long uniqueVal = DateTime.UtcNow.Ticks;

위와 같이 값을 얻어올 수 있고, 위의 값을 데이터베이스에 레코드를 추가할 때 힌트용으로 사용하는 열에 지정하면 삽입 즉시 조회할 수 있는 고유한 값이 되므로 프로그램 로직 개선에 많은 도움이 됩니다.

조건문의 분기를 임의로 결정하도록 만드는 방법

Modular Operator (%)의 기능과 특징을 아신다면 당연하게 받아들일 수 있는 내용이지만, 이런 특이한 상황에 대해서 유용하게 쓰일 수 있습니다. switch나 if/else 등의 조건문의 분기 자체를 임의 결정할 수 있도록 시뮬레이션해야 하는 상황에서 난수 값이 구체적으로 어떤지를 검색하거나 값을 한정하기 위해서 제약하는 것보다 더 손쉽고 이해하기 편한 시뮬레이션 방식을 % 연산자를 이용하여 쉽게 구현할 수 있습니다.

string modified = Join<char>(", ", "Hello guys!");
Random random = new Random();
char x = '\0';

for (int i = 0; i < 100; i++)
{
    switch (Char.ToUpperInvariant(modified[random.Next() % modified.Length]))
    {
        case 'H': x = 'i'; break;
        case 'E': x = 'f'; break;
        case 'L': x = 'm'; break;
        case 'O': x = 'p'; break;
        case ' ': x = '?'; break;
        case 'G': x = 'h'; break;
        case 'U': x = 'v'; break;
        case 'Y': x = 'z'; break;
        case 'S': x = 't'; break;
        case '!': x = '@'; break;
        case ',': x = '.'; break;
        default: x = ' '; break;
    }
    Console.Write(x);
}
Console.WriteLine();

위와 같이 % 기호 다음에 오는 operand로 컬렉션의 길이나 배열의 길이를 지정해주면, 배열의 요소를 임의로 고를 수 있어서 활용폭이 더 넓어집니다.

소스 코드에 특수문자나 CJK 문자를 안전하게 기록하고 다른 사람과 공유하는 방법

드문 경우이지만, 주석 이외에 프로그램의 실행에 실제로 영향을 줄 가능성이 있는 문자열이 영어나 숫자, 혹은 ASCII 범위의 문자가 아닐 경우 다른 환경이나 언어 구성에서 소스 코드 파일을 편집한 후 되돌려받았을 때 문자열이 깨지는 일이 자주 있습니다. 지금 이야기하는 방법은 사실 실용적이지는 않지만, 정말 중요하게 지켜야 할 리소스라면 지금 소개하는 방법을 이용하여 번거롭지만 확실하게 문자열 데이터를 지키는 것도 가능하니 한 번 고려해보시는 것도 좋을 것 같습니다.

예를 들어, 중국어 문자열 "我国屈指可数的财阀。" (우리나라 굴지의 재벌)이 소스 코드에 문자열로 저장되어있고 이 문자열을 인코딩 문제로부터 보호하기 위해서, 위의 문자열을 복사하여 LINQPAD에 아래의 인라인 식에 치환하여 넣습니다. (LINQPAD는 http://www.linqpad.net 에서 다운로드합니다.)

String.Join(", ", "paste here".Select(x => "0x" + ((int)x).ToString("X4")))

그러면 다음과 같은 결과가 나타납니다.

0x6211, 0x56FD, 0x5C48, 0x6307, 0x53EF, 0x6570, 0x7684, 0x8D22, 0x9600, 0x3002

이제 위의 내용을 new String(new char[] { 0x6211, 0x56FD, 0x5C48, 0x6307, 0x53EF, 0x6570, 0x7684, 0x8D22, 0x9600, 0x3002
 }); 와 같이 바꾸어서 소스 코드에 저장하면 실행 시 원래 문자열로 복원되면서도, 소스 코드 상의 문자열이 훼손될 걱정을 하지 않아도 됩니다. 단, 이 경우 소스 코드의 내용만으로는 실제로 어떤 문자열인지 파악하기 어려워진다는 장점이자 단점이 동시에 발생합니다. 장점으로는, 일종의 난독처리가 이루어진 셈이며, 단점으로는, 관리가 어려워진 셈이기 때문입니다.

조건문을 어떻게 관리하십니까?

조건문을 어떻게 작성하고 관리하는가에 대한 문제는 개인의 취향과 논리에 따라 매우 다양한 패턴이 존재합니다. 그러나 경험 상, 코드가 간결할 수록 유리하다는 것은 보편적으로 통하는 진리입니다. 개인적인 경험으로 유추해볼 때, 코드의 간결함은, 조건문이나 분기가 얼마나 단일 메서드 내에서 잘 관리되고 있는가에 대한 이야기로 바꾸어 말할 수도 있을 것 같습니다.

이런 방침에 따라, C나 C++ 스타일의 언어들은 중첩해서 사용하는 중괄호의 여닫음 횟수가 늘어날수록 복잡도가 크게 증가합니다. C#도 예외는 아닌데, 이런 이유때문에 저는 스스로 조건문이나 코딩 스타일을 나름의 원칙을 정하여 사용하고 있습니다.

우선, 단위 메서드를 작성하기에 앞서서 조건 검사를 할 때에는 부정적인 시나리오부터 먼저 확인합니다. 다음의 예를 들어보도록 하겠습니다.

public int Divide(int a, int b, out int z)
{
    z = 0;

    if (b != 0)
    {
        z = a % b;
        return a / b;
    }
    else
    {
        throw new DivideByZeroException();
    }
}

무난한 코드입니다. 하지만, 제가 볼 때에는 중괄호를 여닫을 필요가 없어보이는 코드입니다. 아래와 같이 정리하면 어떨까요?

public int Divide(int a, int b, out int z)
{
    z = 0;

    if (b == 0)
        throw new DivideByZeroException();

    z = a % b;
    return a / b;
}

요지는 이렇습니다. 이 메서드에서 우려하는 최악의 상황은 사실 매개 변수 b가 0으로 들어오는 경우입니다. 확실히 문제가 있음을 제기해야 한다면 이 경우를 따로 다루어야 하겠지요. 이를 위해서 b가 0으로 지정되었는지를 검사하여 메서드의 시선으로부터 그런 상황을 제거합니다. 그러면 남는 일은 오로지 나눗셈에 의한 나머지와 몫을 구하는 일이 됩니다. (참고로 z = 0을 서두에 지정한 것은 out 매개 변수에 대한 제약 때문에 그렇습니다. 메서드 본문 밖을 return에 의해서이든 throw에 의해서이든 빠져나가기 전에 반드시 out 매개 변수의 값은 초기화를 해야 합니다.)

그리고 중괄호를 많이 열게 될 개연성이 있는 또 다른 유형은 바로 IDisposable 변수를 다루기 위한 using 블럭입니다. 아래의 경우를 살펴보도록 하겠습니다.

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

using (WinFormModule mod = new WinFormModule(args.FirstOrDefault()))
{
    using (StandardKernel kern = new StandardKernel(mod))
    {
        Application.Run(kern.Get<ApplicationContext>());
        mod.FormName = "Form3";
        Application.Run(kern.Get<ApplicationContext>());
        mod.FormName = "Form2";
        Application.Run(kern.Get<ApplicationContext>());
    }
}

두 번 열 필요가 없어보이는데도 두 번이나 열었습니다. 위의 코드는 아래와 같이 깔끔하게 정리할 수 있습니다.

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

using (WinFormModule mod = new WinFormModule(args.FirstOrDefault()))
using (StandardKernel kern = new StandardKernel(mod))
{
    Application.Run(kern.Get<ApplicationContext>());
    mod.FormName = "Form3";
    Application.Run(kern.Get<ApplicationContext>());
    mod.FormName = "Form2";
    Application.Run(kern.Get<ApplicationContext>());
}

IDisposable.Dispose 메서드가 항상 모든 것을 앗아가기만 하는 것은 아니다.

직전에서 다룬 using과 IDisposable에 대한 흔한 오해는, IDisposable 형식의 참조를 using 문과 함께 사용할 때에는 반드시 using 문 내부에서만 선언해야 한다는 것입니다. 그러나 이 경우 문제가 발생하는 일이 있습니다. 아래의 경우를 살펴보도록 하지요.

using (MemoryStream memStream = new MemoryStream())
using (FileStream fileStream = File.OpenRead(@"WinFormDI.exe.config"))
{
    fileStream.CopyTo(memStream, 64000);
}
// memStream에 들어있는 내용은 어디서 찾을 수 있습니까?

주석 처리한 부분에서 memStream 변수를 접근해야 하는 이유는 간단합니다. 혹시 MemoryStream의 구현 상에 있을지 모르는 버퍼링 (물론 실제로는 그럴리 없습니다만)을 모두 끝내고 실제 스트림에 쓰여진 상태를 확보하고 싶은데, 막상 MemoryStream의 존재 자체를 알 수 없는 외곽 블록에서는 실행이 다 끝나고도 데이터에 접근할 수 없는 우스운 상황이 생깁니다. 위의 코드를 아래와 같이 고치면 의도대로 잘 작동합니다.

MemoryStream memStream;
using (memStream = new MemoryStream())
using (FileStream fileStream = File.OpenRead(@"WinFormDI.exe.config"))
{
    fileStream.CopyTo(memStream, 64000);
}
byte[] buffer = memStream.ToArray();
Console.WriteLine(Convert.ToBase64String(buffer));

사실, 위와 같이 memStream 변수를 밖으로 빼내어도 이상이 없습니다.

memStream은 using 블록 밖에서는 당연히 더 이상 데이터를 기록할 수 없도록 파기된 상태입니다. 하지만, 앞에서 이야기했듯이 IDisposable.Dispose 메서드가 모든 것을 소거하지는 않습니다. 즉, MemoryStream 내부의 byte 배열 버퍼는 여전히 유효합니다. 따라서, 그것의 참조를 Dispose 메서드가 불린 이후라도 가져와서 BASE64 인코딩으로 파일 내용을 인코딩하여 문자열로 바꾸려 했던 코드를 잘 실행할 수 있습니다.

바꾸어 말하면, 아래의 코드도 유효합니다.

MemoryStream memStream = new MemoryStream();
using (memStream)
using (FileStream fileStream = File.OpenRead(@"WinFormDI.exe.config"))
{
    fileStream.CopyTo(memStream, 64000);
}
byte[] buffer = memStream.ToArray();
Console.WriteLine(Convert.ToBase64String(buffer));

객체의 생성을 using 문 밖에서 처리하고, 사용하고픈 참조를 담고 있는 변수명을 지칭하기만 해도 같은 의미가 됩니다. using 문 밖으로 나가면 당연히 memStream은 Dispose 메서드가 호출된 상태가 됩니다.

저작자 표시 비영리 변경 금지
신고
Posted by 남정현 (rkttu.com)
TAG c#
Tech/닷넷 일반2013.10.02 18:10

응용프로그램을 배포하다보면 특정 조건 때문에 파일 하나로 만들어서 배포해야 할 경우가 있습니다. 사실 응용프로그램중에 EXE 파일 하나만으로 구성되는 경우가 드물지 않을까 싶은데, 지역화된 리소스만 집어넣어도 기본적으로 위성 어셈블리가 생성되기 때문이죠.


이번에 각종 DLL을 EXE에 임베드시키고 AssemblyResolve 이벤트를 사용해서 로드하는 방식을 사용해보았기에 정리해보려고 합니다.


실행시키면 아래와 같이 덩그라니 창이 뜨는 아주 단순한 WPF 응용프로그램입니다. (.NET 4 Client Profile 기반)


프로젝트 구조는 왼쪽 처럼 되어 있습니다. RESX 를 사용해서 문자열 리소스(여기서는 윈도우 제목 하나뿐이기 하지만...)를 지역화하고 있습니다. 리소스 어셈블리는 별도 프로젝트로 분리되어 있죠. 중립 언어는 en-US 로 AssemblyInfo.cs 에 설정이 되어 있고요.


기본적으로 이 프로젝트를 빌드해서 배포하려면 아래와 같은 파일들을 배포해야 합니다.

  • WpfSingSangSung.exe
  • WpfSingSangSung.Resources.dll
  • ko-KR/WpfSingSangSung.Resources.resources.dll
파일 하나로 만들어서 배포하려면 아래쪽 두 개의 리소스 DLL을 EXE 에 몰아넣어야 겠네요.


우선 빌드된 DLL 을 embed 시킬 것이므로, WpfSingSangSung 프로젝트 빌드 시에 자동으로 WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 복사할 필요가 없습니다. 아래 그림과 같이 프로젝트 참조에서 Copy Local 을 False 로 바꿔줍니다.

빌드가 성공하려면 어셈블리 참조 자체는 유지해주어야 합니다.


다음 단계는 빌드된 리소스 DLL을 둘 곳을 정하는 건데요, 개발 중에 Debug 로 빌드했다가 Release 로 빌드했다가 할텐데, 그 때 빌드된 리소스 DLL 이 업데이트 되어야 하니까, 빌드된 리소스 DLL 들을 정해진 장소로 복사해주도록 하겠습니다.

프로젝트 폴더 구조는 위와 같습니다. WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 bin/Result 폴더에 복사하도록 하죠. Post-build event 에 아래와 같이 설정해 줍니다.

xcopy "$(TargetPath)" "$(ProjectDir)bin\Result\" /Y
xcopy "$(TargetDir)ko-KR\$(ProjectName).resources.dll" "$(ProjectDir)bin\Result\ko-KR\" /Y

만약 다른 언어가 하나 더 추가된다면 ko-KR 과 동일한 패턴으로 복사하는 코드를 한 줄 넣어야겠죠.


이제 복사된 위치에 있는 리소스 DLL 들을 EXE 에 리소스로 임베드 시킵니다. 리소스로 임베드된 DLL 들만을 위해서 별도의 네임스페이스를 두는 것이 좋으니까, WpfSingSangSung 프로젝트에 폴더를 하나 추가하고, 리소스 DLL 들을 그냥 프로젝트에 추가하는 것이 아니라 Link 로 추가합니다.

바로 위 이미지는 DLL 을 추가하고, 리소스로 임베드시키는 작업이 다 된 상태입니다. 각 DLL 파일들의 Build Action 을 "Embedded Resource" 로 설정해준 것이 보이시죠? 그리고 또 하나 눈에 띄는 점은 ko-KR 폴더를 만들어서 한국어 리소스 DLL을 넣었다는 점이겠네요.


여기까지 해서 솔루션을 빌드하면, WpfSingSangSung.exe 에는 DLL들이 모두 리소스로 들어가 있는 상태입니다. 리소스 DLL을 찾아서 사용할 수 있게 해주는 기능이 없으니 실행은 아직 안되지만, ILSpy 로 열어보면 두 개의 리소스 DLL 이 리소스로서 포함되어 있는 것을 확인할 수가 있습니다.

이렇게 ILSpy 로 DLL의 리소스 이름을 확인해두면 뒤에 AssemblyResolve 이벤트 핸들러에서 어셈블리 로드하는 코드를 작성할 때 도움이 됩니다.


가장 중요한 내용으로 들어가기 전에, 기본적인 WPF 응용프로그램의 구조를 조금 바꿔서 코드 작성을 좀 쉽게 만들려고 합니다. 템플릿으로 WPF 응용프로그램을 생성하면 App.xaml 을 이용하는 형태로 만들어 주는데요, 이걸 지워버리고 Program.cs 를 만들어서 진입점으로 만들어주는거죠. 아래와 같이 Program.cs 를 만들어서 넣어줍니다.

    public class Program : Application
    {
        [STAThread]
        public static void Main(string[] args)
        {
            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }
    }


이제 AssemblyResolve 이벤트 핸들러만 만들어주면 끝납니다! 위에서 만들 Program.cs 에 코드 몇 줄만 추가하면 됩니다.

        public static void Main(string[] args)
        {
            // 이벤트 핸들러 연결
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }

        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            AssemblyName missingAssemblyName = new AssemblyName(args.Name);
            CultureInfo ci = missingAssemblyName.CultureInfo;

            // ILSpy 에서 확인한 형태로 리소스 이름을 만들어줍니다.
            string prefix "WpfSingSangSung.Embedded.";
            string culturePart = "ci.Name.Replace("-", "_") + ".";
            string resourceName = prefix + culturePart + missingAssemblyName.Name + ".dll";
            // 중립 리소스 요청인 경우에는 Culture 이름이 비어있습니다.
            if (ci.Name == string.Empty)
                resourceName = prefix + missingAssemblyName.Name + ".dll";

            // 리소스에서 읽은 바이너리를 Assembly로 전환하여 리턴합니다.
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            {
                byte[] bytes = new BinaryReader(stream).ReadBytes((int)stream.Length);
                return Assembly.Load(bytes);
            }
        }


이렇게 만들어서 빌드한 EXE 를 각각 영어 환경과 한국어 환경에 EXE만 복사해서 실행해보면 환경에 맞게 윈도우 제목이 영어와 한국어로 표시되는 것을 확인할 수 있습니다. 성공이네요!


신고
Posted by wafe
Tech/닷넷 일반2013.09.17 20:00

 

C#이나 .NET Framework를 이용하여 프로그램 코드를 작성할 때, 실제로 실행 시간이 오래 걸리는 코드 뿐 아니라 입출력 작업이나 여러가지 외부적인 요인 (데이터베이스, 네트워크를 통한 원격 시스탬 액세스 등)에 의하여 요즈음은 언제 끝나는지 정확한 실행 시간을 측정할 수 없는, 실행 시간에 변수가 생기는 코드를 작성하는 일이 많습니다. 이러한 프로그래밍 코드는 늘 그렇듯이 사용자 인터페이스와는 친화적이지 않은 경우가 많습니다. (잘 아시다시피 큐를 기반으로 하는 사용자 인터페이스 메시징 처리에 방해가 되기 때문입니다.)

이러한 문제를 해결하기 위해서, Windows Forms에서는 BackgroundWorkerThread를 사용할 수 있고, Timer나 직접 Thread를 이용하는 경우도 있습니다. 하지만 잘못된 관점을 가지고 프로그램을 작성할 경우 관리하기 어려운 코드가 되는 문제가 있습니다. 좀 더 간결하고 알기 쉬운 비동기 코드를 만드려는 노력이 필요한 이유가 여기에 있습니다.

이번 글에서는 어렵게 스레드를 만들고 생명 주기를 관리하려는 노력 대신, 실제 로직에 집중해서 메서드의 실행을 비동기화하고, 원격에서 통제할 수 있는 명쾌한 방법을 하나 소개해볼까 합니다.

다음과 같은 임의의 메서드가 하나 있다고 가정하겠습니다. 컨셉을 이해하기 위함이므로 메서드 본문 안의 내용은 중요하지 않음을 미리 언급해두겠습니다.

public int CalcXYZ(int x, int y, int z) {
    return x * y * z;
}

.NET Framework 3.5부터 새로 추가된 LINQ는 Lambda Expression과 함께 좀 더 적극적인 Type Inference를 가능하게 하기 위하여, Action과 Func 대리자들의 가짓수가 매우 다양해졌습니다. 이전 버전의 .NET Framework에서는 위의 메서드를 포장하기 위하여 아래와 같이 대리자 형식을 따로 정의해야 했습니다.

public delegate int CalcXYZDelegate(int x, int y, int z);
...
private CalcXYZDelegate f;
...
f = this.CalcXYZ;

물론 위와 같이 대리자를 따로 정의해서 담아두어도 좋습니다. 하지만, 타이핑하고 관리해야 할 코드의 분량을 조금이라도 줄일 방법을 찾는게 좋을 것입니다. 이를 위해서, 반환 형식이 있는 메서드이므로 Func<T1, T2, T3, TResult> 대리자를 사용할 수 있을 것입니다.

private Func<int, int, int, int> f;
...
f  = this.CalcXYZ;

여기서 한 가지 기억해야 할 것은, 비동기 메서드를 만들기 위해서 비동기 작업을 발행하고 관리하는 주체로 대리자를 사용한다는 점입니다. 따라서, 대리자 인스턴스를 하나로 고정하기 위하여, private 멤버 변수로 선언하는 것이 중요합니다. 즉, 위의 코드를 전개하면 다음과 같은 클래스 선언이 나타난다고 할 수 있습니다.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
}

이제 위의 MyClass 정의에 Begin/End 짝 메서드를 정의해보겠습니다. Begin/End 메서드에 대해서 설명하면, BeginXXXX 메서드는 메서드의 호출을 시도하고 비동기 작업을 예약하는 역할을 하며, EndXXXX 메서드는 메서드 호출이 완료되었을 때 결과를 회수하는 역할을 합니다. 이 때, BeginXXXX 메서드에 전달하는 콜백 함수의 호출을 활용하거나, BeginXXXX 메서드가 반환하는 IAsyncResult 및 그 안에 들어있는 스레드 이벤트 동기화 객체가 실행 흐름을 관리하는데 매우 중요한 역할을 담당하게 됩니다.

위의 정의에 내용을 좀 더 추가하면 다음과 같습니다. 굵게 강조 표시한 부분을 확인해 주세요.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
    public IAsyncResult BeginCalcXYZ(int x, int y, int z, AsyncCallback callback, object result) {
        return f.BeginInvoke(x, y, z, callback, result);
    }
    public int EndCalcXYZ(IAsyncResult asyncResult) { return f.EndInvoke(asyncResult); }
}

규칙이 있음을 알 수 있습니다. 다시 살펴보면,

public <반환 형식> <메서드 이름>(<인수들>);

위의 메서드가 원형이라고 하면,

public IAsyncResult Begin<메서드 이름>(<인수들>, AsyncCallback callback, object result) { ... }
public <반환 형식> End<메서드 이름>(IAsyncResult asyncResult) { ... }

이와 같이 함수 호출의 시작과 끝을 관리하는 메서드로 분할하여 정의하고 있으며, 여기에 대한 실질적인 구현은 .NET Framework가 제공하는 대리자를 이용하여 처리하므로 어렵지 않게 비동기 메커니즘을 구현할 수 있게 되는 것입니다. 여기서 조금 더 응용하면, 만약 반환 형식이 void라면 그대로 적어도 무방하며 이 때에는 Func 대리자 대신 Action 대리자로 바꿔주면 됩니다. 인수들이 없다면? 당연히 생략하고 비동기 호출에서 필수적인 인자들만 맞추어 서술하면 끝납니다.

여기서 한 가지 더 이야기할 것은, 최근에 Windows 8 출시와 함께 WinRT가 등장하면서 급부상한 C# 5.0의 async 키워드와 TPL (Task Parallel Library)와의 상관 관계입니다. async 키워드를 직접 사용하지는 않는다 할지라도, WinRT를 자연스럽게 지원할 수 있는 방법이 바로 TPL에 대한 지원을 추가하는 것입니다. 바꾸어 말하면, async 키워드는 컴파일러의 서비스이므로, TPL에 대한 지원만 정확히 하고 있다면, C# 5.0 컴파일러가 TPL 기반의 비동기 코드를 손쉽게 작성할 수 있도록 도움을 준다는 의미도 됩니다.

위의 내용까지 구현했다면, TPL로 가는 길은 바로 코앞에 있는 셈입니다.

using System.Threading.Tasks;
...
public Task<int> CalcXYZAsync(int x, int y, int z) {
    return Task<int>.Factory.FromAsync(this.BeginCalcXYZ, this.EndCalcXYZ, x, y, z, this);
}

위와 같은 코드만 작성하면 Begin/End 패턴을 즉시 TPL 기반의 비동기 패턴으로 변환할 수 있습니다. 여기에서도 규칙을 찾을 수 있는데, 다음과 같습니다.

return Task<<반환 형식>>.Factory.FromAsync(Begin<메서드 이름>, End<메서드 이름>, <인수들>, <this 또는 null>);

여기서 Task 클래스에 제네릭 인자를 지정하는 이유는, 메서드의 반환 형식에 대한 형식 안정성을 지키기 위함입니다. 만약 반환 형식이 void라면 다음과 같이 제네릭 인자 자체를 생략하면 됩니다.

public Task SomeMethod() {
    return Task.Factory.FromAsync(Begin<메서드 이름>, End<메서드 이름>, <this 또는 null>);
}

위의 내용까지 포함한 수정된 MyClass 코드는 다음과 같습니다.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
    public IAsyncResult BeginCalcXYZ(int x, int y, int z, AsyncCallback callback, object result) {
        return f.BeginInvoke(x, y, z, callback, result);
    }
    public int EndCalcXYZ(IAsyncResult asyncResult) { return f.EndInvoke(asyncResult); }
    public Task<int> CalcXYZAsync(int x, int y, int z) {
        return Task<int>.Factory.FromAsync(this.BeginCalcXYZ, this.EndCalcXYZ, x, y, z, this);
    }
}

여기서 한 가지 염두에 두어야 할 것은, 기본 메서드의 매개 변수 갯수가 3개보다 많을 경우, FromAsync 메서드에서는 3개까지만 매개 변수를 전달할 수 있도록 선언이 되어 있기 때문에, IAsyncResult 형식의 객체를 받아서 Wrapping하거나, 템플릿 인자를 확장하거나, 중요도가 낮은 매개 변수들을 Dictionary 또는 별도의 POCO (Plain-old-CLR-object) 형식으로 정의하여 매개 변수로 사용하도록 하는 부수적인 절차가 필요합니다.

또한, 메서드 오버로딩을 비동기 메서드에도 적용하고자 하는 경우, 오버로딩에서 가장 핵심이 되는 메서드에 대해서 위의 패턴을 적용하고, 나머지는 핵심 메서드를 호출하는 과정을 동일하게 맞추어야 합니다. 예를 들어, 위의 CalcXYZ 메서드의 오버로딩으로 short 형식을 사용한다고 가정하면 다음과 같이 사용해야 함을 뜻합니다.

public class MyClass {
    // ...
    public int CalcXYZ(short x, short y, short z) { return CalcXYZ((int)x, (int)y, (int)z); }
    public IAsyncResult BeginCalcXYZ(short x, short y, short z, AsyncCallback callback, object result) {
        return
BeginCalcXYZ((int)x, (int)y, (int)z, callback, result);
    }
    // EndCalcXYZ는 따로 정의하지 않습니다.
    public Task<int> CalcXYZ(short x, short y, short z) { return CalcXYZAsync((int)x, (int)y, (int)z); }
    // ...
}

이제 위와 같이 정의했으니, 호출하고 사용하는 방법을 알아보도록 하겠습니다.

비동기 메서드는 기본적으로 실행을 예약하고, 콜백 메서드를 통하여 실행 완료 통지를 받았을 때 따로 실행하도록 하는 것이 기본입니다. 하지만, 비동기로 실행하는 것과는 별개로 실행 흐름을 동기화해야 하는 경우도 있을 수 있는데, 일반적인 메서드 호출과는 다른 특별한 기능을 하나 더 이용할 수 있습니다. 바로 Time out 개념을 사용할 수 있다는 것인데, 이것은 Thread를 직접 제어할 때와는 또 다른 이점입니다.

MyClass inst = new MyClass();
IAsyncResult res = inst.BeginCalcXYZ(1, 2, 3, null, null);
res.WaitHandle.WaitOne();
if (res.IsCompleted) {
    int val = inst.EndCalcXYZ(res);
    Console.WriteLine(val);
} else {
    throw new TimeoutException();
}

Begin/End 메서드는 위와 같이 사용합니다. 여기서 재미있는 부분은 콜백과 상태 관리 객체를 지정하지 않고 실행이 끝날 때 까지 기다리게 했다는 부분입니다. 그런데 한 가지 궁금증이 생깁니다. 이렇게 하면 비동기의 이점이 없는 것이 아닌가 하는 부분입니다.

그런데 WaitOne 메서드에 매개 변수를 하나 지정할 수 있습니다. 바로 밀리초 단위의 time out 대기 시간입니다. 이 값을 생략할 경우 자동으로 System.Threading.TimeOut.Infinite가 지정된 것과 같이 실행되며, 이 상수 필드의 값은 (-1)입니다. 즉, 실행이 끝날 때까지 이 코드를 실행하는 스레드의 실행을 동결한다는 것입니다. 이 값 대신 1000을 지정하면 1초 이내에 실행이 끝나지 않을 때 그 다음 코드로 바로 실행이 이어지는 것입니다. 여기서 IsCompleted 속성을 사용하여 실행이 정말 완료가 되었다면 결과를 회수하고, 그렇지 않으면 Timeout으로 처리할 수 있게 됩니다.

이러한 시나리오가 유용하게 쓰일 수 있는 곳은 매우 많습니다. 단순한 테스트 유닛 실행에서부터 네트워크 및 입출력 관련 처리에 이르기까지 매우 다양한데, 프로그램을 그저 응답 없음 상태로 내버려두는 것이 아니라 좀 더 주도적으로 실행 흐름을 관리할 수 있게 되는 것입니다.

Begin/End 대신 Async 메서드를 사용하는 경우, C# 5.0 이전의 언어를 사용할 경우 다음과 같이 코드를 작성할 수 있을 것입니다.

MyClass inst = new MyClass();
var res = inst.CalcXYZAsync(1,2,3);
res.Wait();
if (res.IsCompleted) {
    int val = res.Result;
    Console.WriteLine(val);
} else {
    throw new TimeoutException();
}

그리고 C# 5.0부터는 좀 더 간결하게 아래와 같이 코드를 작성할 수 있을 것입니다.

public async void Method1() {
    MyClass inst = new MyClass();
    int val = await inst.CalcXYZAsync(1,2,3);
    Console.WriteLine(val);
}

그런데 한 가지 남는 의문은, 전통적인 Event Driven 방식의 Windows 응용프로그램 개발 환경에서 단순히 이벤트 처리기를 사용하여 위의 코드를 직접 실행하면 프로그램이 굉장히 부자연스럽게 동작한다는 점입니다. 왜 그럴까요?

그 원인은 틀림없이 메시지를 처리하는 스레드에서 위의 코드를 실행하기 때문일 것입니다. 즉, 엄밀한 의미에서 모든 작업들은 비동기화 되어야하고, 비동기화된 스레드들의 상태를 모니터링하면서 사용자 인터페이스에 적절한 신호를 주어야 하는 셈입니다. 이렇게 본다면 GUI 프로그래밍은 매우 어려운 작업 중 하나가 될 수 있습니다. 그래서 기준이 하나 있어야 하는데, 바로 I/O Bound Operation에 한하여 위와 같이 작업을 비동기로 분할하여 상태 보고를 할 수 있도록 구성하는 방법을 적절히 사용해야 하는 것입니다. 그러한 방면으로 잘 포장된 것이 바로 BackgroundWorker 컴포넌트입니다.

어떤 방법을 사용하는가에 관계없이, 프로그래밍을 할 때에는 항상 실행 시간과 흐름 관리에 최선을 다해야 합니다. 디자인 패턴 이외에도, 시간이 오래 걸릴 수 있는 불확정성에 기대는 작업을 다룰 때 이러한 세밀한 노력이 얼마나 들어갔는지에 따라서 얼마나 완성도 높은 코드를 생산할 수 있는가가 결정될 것입니다.

저작자 표시 비영리 변경 금지
신고
Posted by 남정현 (rkttu.com)