Concurrency Runtime
– 동기화 객체 1. ( critical_section & reader_writer_lock )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 기본적으로 Asynchronous Agents Library( 이하 AAL ) 에서 제공하는 message block 들은 스레드에 안전합니다. 이 말은 내부적으로 동기화를 구현하고 있다는 말입니다.

 하지만 Concurrency Runtime 을 이용한 동시 프로그래밍을 할 때 AAL 의 message block 을 사용하지 않고 일반적인 사용자 변수들을 사용할 수 있습니다. 하지만 이런 변수들은 동기화가 되지 않아 직접 동기화를 해 주어야 합니다.

 이 때 Windows API 의 유저 모드 혹은 커널 모드의 동기화 객체를 사용할 수 있지만, Concurrency Runtime 에서 제공하는 동기화 객체를 쉽게 사용할 수 있습니다.

 이번 글에서는 Concurrency Runtime 에서 제공하는 동기화 객체들에 대해서 알아보도록 하겠습니다.

 

critical_section

 임계 영역을 의미하는 객체로, 어떤 영역을 잠그고 풀 수 있습니다. 잠긴 영역에 도달한 다른 스레드는 잠김이 풀릴 때까지 기다려야 합니다. 그러므로 해당 영역을 모두 사용한 후에는 다른 스레드도 사용할 수 있도록 풀어주어야 합니다.

 이 기능은 Windows API 에서 제공하는 유저 모드 동기화 객체인 CRITICAL_SECTION 과 같은 기능을 합니다.

 

멤버 함수

 생성자와 소멸자를 제외한 public 인 멤버 함수들에 대해서 알아보겠습니다.

 

void lock()

 해당 영역을 잠급니다. 잠근다는 의미는 영역의 접근 권한을 획득한다는 의미도 있습니다. 잠긴 영역에 도달한 다른 스레드들은 잠김이 풀릴 때까지 기다려야 합니다.

 

bool try_lock()

 해당 영역의 접근을 시도합니다. 이미 다른 스레드에 의해 잠겨있더라도 풀릴 때까지 기다리지 않고 반환합니다.

 이미 다른 스레드에 의해 잠겨있다면 false 를, 아니면 true 를 반환합니다.

 

void unlock()

 접근 영역의 접근 권한을 반환합니다. 즉, 잠금을 해제합니다. 이 함수가 호출된 후에 다른 스레드가 이 영역에 접근할 수 있습니다.

 

native_handle_type native_handle()

 native_handle_typecritical_section 의 참조입니다. 결국, 이 함수는 자기 자신의 참조를 반환합니다.

 

범위 잠금

 범위 잠금이란 코드 내의 블럭의 시작부터 블럭이 끝날 때까지 잠그는 것을 말합니다. 이것은 생성자에서 잠그고, 소멸자에서 잠금을 해제하는 원리( RAII – Resource Acquisition Is Initialization )로 구현되어 있으며, 굳이 unlock() 을 호출하지 않아도 된다는 편리함이 있습니다.

 

critical_section::scoped_lock

 critical_section 객체를 범위 잠금 객체로 사용합니다. 생성자로 critical_section 의 참조를 넘겨주어야 합니다. 스택에 할당되고 가까운 코드 블럭이 끝날 때, 소멸자가 호출되어 잠금이 해제됩니다.

 

reader_writer_lock

 reader_writer_lock 은 쓰기 접근 권한과 읽기 접근 권한을 구분하여 획득하고 잠글 수 있는 객체입니다.

한 스레드가 쓰기 접근 권한을 획득하여 잠긴다면, 다른 스레드의 모든 쓰기 / 읽기 접근 권한 획득시도가 실패하며 기다리게 됩니다.

 만약 한 스레드가 읽기 접근 권한을 획득하여 잠긴다면, 다른 스레드가 읽기 접근 권한 획득 시도 시, 동시에 읽기 접근 권한을 획득 가능합니다.

 위에 설명한 것처럼 2 가지 종류의 접근 권한이 있지만, 잠기는 것은 단 하나의 객체라는 것을 명심하셔야 합니다. 이 사실을 잊으면 헷갈릴 수 있습니다.

 reader_writer_lock 의 이와 같은 기능은 Windows API 의 SRW( Slim Reader Writer ) 잠금과 유사합니다. 다른 점이 있다면 reader_writer_lock 은 기본 잠금이 쓰기 잠금으로 되어 있고, 잠겼을 때 다른 스레드들이 기다린 순서대로 접근 권한을 얻는다는 것입니다. 즉, Windows SRW 는 기본 잠금이라는 개념이 없고, 기다린 스레드들 중 어떤 순서로 접근 권한을 얻는지 알 수 없습니다.

 

멤버 함수

 생성자와 소멸자를 제외한 public 인 멤버 함수들에 대해서 알아보겠습니다.

 

void lock()

 기본 잠금으로 쓰기 접근 권한을 획득하고 잠급니다. 이미 다른 스레드에서 접근 권한을 획득하여 잠겨 있다면 해제될 때까지 기다립니다.

 이 함수로 잠길 경우, lock() 과 읽기 접근 권한 획득 함수인 lock_read() 으로 접근 권한을 획득하려 하더라도 획득할 수 없고, 잠금이 해제될 때까지 기다리게 됩니다.

 

bool try_lock()

 쓰기 접근 권한 획득을 시도합니다. 이미 다른 스레드가 접근 권한을 획득하여 잠겼을 경우에는 false 를 반환하고, 접근 권한을 획득하게 되면 true 를 반환합니다. 즉, 대기하지 않습니다.

 

void lock_read()

 읽기 잠금으로 읽기 접근 권한 획득을 합니다. 다른 스레드에서 lock_read() 로 읽기 접근 권한 획득을 시도하면 이미 다른 스레드가 읽기 접근 권한을 획득하였다고 하더라도 동시에 읽기 접근 권한을 획득할 수 있습니다.

 다른 스레드에서 lock() 으로 쓰기 접근 권한 획득을 시도한다면, 이 스레드는 모든 읽기 권한으로 인한 잠금이 해제될 때까지 기다리게 됩니다.

 

bool try_lock_read()

 읽기 접근 권한 획득을 시도합니다. 이미 다른 접근 권한을 획득하여 잠겼을 경우에는 false 를 반환하고, 접근 궈한을 획득하게 되면 true 를 반환합니다. 즉, 대기하지 않습니다.

 

void unlock()

 어떤 잠금이든 해제합니다.

 

범위 잠금

 critical_section 과 마찬가지로 범위 잠금을 지원합니다.

 

reader_writer_lock::scoped_lock

 critical_section 과 같습니다. 다른 점이 있다면 이 잠금 객체는 쓰기 잠금의 범위 잠금 객체입니다.

 

reader_writer_lock:scoped_lock_read

 이 잠금 객체는 읽기 잠금의 범위 잠금 객체입니다.

 

마치는 글

 Concurrency Runtime 에서 제공하는 동기화 객체은 critical_sectionreader_writer_lock 에 대해서 알아보았습니다.

 reader_writer_lock 이 동시 읽기 접근이 가능하므로 critical_section 보다는 상황에 따라 성능이 좋을 수 있습니다.

 다음 글에서는 동기화 객체 중 마지막 하나, event 에 대해서 알아보도록 하겠습니다.

[STL] 4. make_shared

C++0x 2010. 8. 12. 12:00 Posted by 알 수 없는 사용자

안녕하세요. 방수철입니다.
이번 글에서는 make_shared 에 대해서 설명드리겠습니다.

VS2010 C++에서는 다음과 같은 구문을 사용할 수 있게 되었습니다.


  1 auto sp = make_shared<int>(1);

이전 버전들에서 위와 같은 기능을 하는 코드는 다음과 같습니다.


  1 shared_ptr<int> sp(new int(1));

make_shared<T>를 사용하는 것이 여러 장점이 있습니다.
첫째, 편리함. 위의 예제에서 처럼 자료형의 이름을 한번이라도 덜 입력해도 됩니다.
두번째, 안정성. 포인터와 객체가 동시에 생성되기 때문에 익히 알려줘 왔던 shared_pointer의 unnamed leak를 방지할 수 있습니다.
마지막으로, 효율성. 이전 버전에서 처럼 shared_ptr을 선언하면 동적할당이 두 번 일어나게 되지만, make_shared<T>를 사용하게 되면 한 번만 일어나게 됩니다.

참고 :
http://msdn.microsoft.com/en-us/library/ee410595.aspx
        

[Step. 09] 델리게이트 (delegate)

C++/CLI 2010. 8. 12. 09:30 Posted by 알 수 없는 사용자
델리게이트는 비관리코드에서는 함수 포인터와 같은 기능을 합니다.

 

#include <iostream>

delegate void TestDelegate();

ref class TEST

{

public:

void TestMethod()

{

  System::Console::WriteLine(“TEST::TestMethod()”);

}

};

 

int main()

{

TEST^ test = gcnew TEST();

TestDelegate^ testDelegate = gcnew TestDelegate( test, &TEST::TestMethod );

 testDelegate();


 getchar();

    return 0;

};

 

 

정적 멤버함수의 델리게이트

 

#include <iostream>

 delegate void TestDelegate();

 ref class TEST

{

public:

static void TestMethod()

{

  System::Console::WriteLine(“TEST::TestMethod()”);

}

};

 

int main()

{

TestDelegate^ testDelegate = gcnew TestDelegate( &TEST::TestMethod );

 

testDelegate();

 

getchar();

return 0;

};

 

 

복수 개의 델리게이트

 

델리게이트는 하나가 아닌 여러 개를 설정할 있습니다.

 

#include <iostream>

delegate void TestDelegate();

 

ref class TESTA

{

public:

void TestMethod()

{

  System::Console::WriteLine(“TESTA::TestMethod()”);

}

};

 

ref class TESTB

{

public:

static void TestMethod()

{

  System::Console::WriteLine(“TESTB::TestMethod()”);

}

};

 

ref class TESTC

{

public:

void operator()()

{

  System::Console::WriteLine(“TESTC::operator()”);

}

};

 

int main()

{

TestDelegate^ testDelegate;

TESTA testA;

TESTC testC;

 

testDelegate = gcnew TestDelegate( testA, &TESTA::TestMethod );

testDelegate += gcnew TestDelegate(&TESTB::TestMethod );

testDelegate += gcnew TestDelegate( testC, &TESTC::operator );

 

testDelegate();

 

getchar();

return 0;

};

 

 

델리게이트의 비교와 삭제

 ==으로 델리게이트를 비교하면 이것은 핸들의 비교가 아닌 델리게이트가 가지고 있는 함수를 비교하는 것입니다. -=을 사용하여 설정한 델리게이트를 제거할 수도 있습니다.

using namespace System;

delegate void MyDele(int);


void func1(int i)

{

    Console::WriteLine("func1");

}


void func2(int j)

{

    Console::WriteLine("func2");

}


int main()

{

    MyDele^ dele;

    dele += gcnew MyDele(&func1);

    dele += gcnew MyDele(&func2);

    MyDele^ dele2 = gcnew MyDele(func1);

    dele2 += gcnew MyDele(&func2);

    if ( dele == dele2 )  {

        Console::WriteLine("TRUE");

    } else {

        Console::WriteLine("FALSE");

    }

   

    dele -= gcnew MyDele(&func1); 

   

    dele(1); 

   

   return 0;

}

< 예제 출처 : http://cppcli.shacknet.nu/cli:delegate >

 

위의 예의 델리게이트들은 모두 void를 반환하고 파라미터가 없는 것인데 당연하듯이 반환 값이나 파라미터를 가질 수 있습니다.




델리게이트의 비동기 실행

 

델리게이트는 비동기 실행을 지원합니다. 비동기 실행은 처리를 요청한 후 종료를 기다리지 않은 호출한 곳으로 제어를 넘겨줍니다. 델리게이트 함수가 긴 시간을 필요로 하는 작업인 경우 비동기 실행을 이용하면 프로그램의 응답성을 높일 수 있습니다.

 

델리게이트의 비동기 실행은 스레드를 사용합니다. 이런 경우 비동기 실행을 할 때마다 스레드의 생성과 소멸에 부담을 느낄 수도 있지만 델리게이트는 닷넷의 기능을 잘 활용하여 스레드를 생성/삭제하지 않고 스레드 풀에 있는 스레드를 사용하므로 스레드 사용에 대한 부담이 작습니다.

 

비동기 실행을 할 때는 주의해야 할 점이 있습니다. 비동기 실행을 하는 경우 델리게이트에는 꼭 하나의 함수만 등록해야 합니다. 만약 2개 이상 등록하였다면 예외가 발생합니다.

 

비동기 실행은 BeginInvoke()를 사용하고, 만약 종료를 기다리고 싶다면 EndInvoke()를 사용합니다.

 

#include "stdafx.h"

#include <iostream>

 

using namespace System;

 

delegate void MyDele(void);

 

void myfunc(void)

{

    System::Threading::Thread::Sleep(3000);

}

 

 

int main(array<System::String ^> ^args)

{

     MyDele^ dele = gcnew MyDele(&myfunc);

            Console::WriteLine(L"1");

     IAsyncResult^ result = dele->BeginInvoke(nullptr,nullptr);

            Console::WriteLine(L"2");

    dele->EndInvoke(result);

            Console::WriteLine(L"3");

   

    getchar();

    return 0;

}

 

위 코드를 실행하면 '2'가 찍힌 이후 3초가 지난 이후에 3이 찍힙니다.





참고
http://cppcli.shacknet.nu/cli:delegate
http://cppcli.shacknet.nu/cli:delegate%E3%81%9D%E3%81%AE2



Asynchronous Agents Library – message block 8. ( timer )

VC++ 10 Concurrency Runtime 2010. 8. 12. 08:30 Posted by 알 수 없는 사용자

Asynchronous Agents Library
– message block 8. ( timer )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 이번 글에서 설명하는 timer 를 마지막으로 Asynchronous Agents Library( 이하 AAL ) 에서 제공하는 모든 message block 들에 대한 소개를 마칩니다.

  • unbouned_buffer
  • overwrite_buffer
  • single_assignment
  • call
  • transformer
  • choice
  • join
  • multitype_join
  • timer

   각각의 message block 들의 특징을 파악하고 필요한 때에 적합한 것들을 사용하시기 바랍니다.

 

timer< _Type >

 timer 는 message 의 전송을 연기하거나, 주기적으로 같은 message 를 전송하는 역할을 합니다.

 안타깝게도 하나의 timer 는 같은 message 밖에 전송할 수 없게 구현되어 있습니다. 또한 하나의 message block 에만 연결하여 전송할 수 있습니다.

 timercall 또는 transformer 를 연결한다면 좀 더 유연하게 확장할 수 있을 것입니다.

 

상태

 timer 는 총 4가지의 상태를 갖습니다.

  • Initialized – 생성된 후, 아직 시작하지 않은 상태.
  • Started – 시작된 상태.
  • Paused – 일시 정지된 상태.
  • Stopped – 정지한 상태.

 

멤버 함수

 생성자 및 public 인 멤버 함수들 중 인터페이스를 재정의한 함수들을 제외하고 알아보도록 하겠습니다.

 

생성자

timer(unsigned int _Ms, _Type const& _Value, ITarget<_Type> *_PTarget = NULL, bool _Repeating = false)

[ 코드1. timer 의 생성자 ]

 첫 번째 매개변수인 _Ms 는 연기되는 시간 또는 주기적인 시간으로 밀리 초를 받습니다.

 두 번째 매개변수인 _Value 는 전송할 message 입니다.

 세 번째 매개변수인 _PTarget 은 전송한 message 를 받을 대상 message block 입니다. 기본 값이 NULL 인데 NULL 인 경우에는 생성된 이후에 link_target() 을 이용하여 연결할 수 있습니다.

 여기서 주의해야 할 점은 timer 는 단 하나의 message block 만을 연결할 수 있다는 것입니다. link_target() 으로 여러 message block 들을 연결할 경우, 런타임 에러를 보게 될 것입니다.

 네 번째 매개변수인 message 의 전송을 연기 또는 주기적으로 전송하는 것을 선택합니다. true 일 경우 주기적으로 전송하며, false 는 연기 후, 단 한번만 전송됩니다.

 

void start()

 start() 는 timer 를 시작하는 함수로 생성된 후인 Initialized 또는 일시 정지된 Paused 상태에서만 동작합니다. 그리고 Started 상태가 됩니다.

 

void stop()

 stop() 은 timer 를 정지하는 함수입니다. 상태가 Stopped 가 되고, 해당 timer 를 다시 사용할 수 없습니다.

 

void pause()

 pause() 는 timer 를 일시 정지하는 함수입니다. 만약 주기적인 message 전송이 아니라 message 의 전송을 연기하도록 생성했다면, stop() 을 호출합니다. 그렇지 않을 경우, 내부 타이머를 멈추고, 상태를 Paused 로 변경합니다.

 

예제

 timer 의 특징을 알아보기 쉽도록 예제를 구현해보겠습니다.

 

시나리오

 채팅 중에 관리자에 의해서 사용자에게 전송되는 공지 사항을 시뮬레이션 해보도록 하겠습니다.

 

코드

#include <iostream>
#include <string>
#include <array>
#include <agents.h>

using namespace std;
using namespace Concurrency;

// 채팅 메시지 구조체
struct Message
{
	wstring		id;
	wstring		message;
};

// 채팅 메시지를 입력받는 agent.
class Typer
	: public agent
{
public:
	Typer( ITarget< Message >& buffer, const wstring id )
		: buffer( buffer )
		, isAvailable( true )
		, id( id ) { }

protected:
	void run()
	{
		while( this->isAvailable )
		{
			Message sendingMessage = { this->id };

			getline( wcin, sendingMessage.message );			

			send( this->buffer, sendingMessage );
		}		

		this->done();
	}

private:
	ITarget< Message >&		buffer;
	bool					isAvailable;
	wstring					id;
};

// 전달받은 메시지를 화면에 출력하는 agent.
class MessageDisplayer
	: public agent
{
public:
	MessageDisplayer( ISource< Message >& source )
		: source( source ) { }

protected:
	void run()
	{
		while( true )
		{
			Message receivedMessage = receive( this->source );

			wcout << L"[" << receivedMessage.id << L"]: " << receivedMessage.message << endl;
		}

		this->done();
	}

private:
	ISource< Message >&		source;
};

int main()
{
	// 시스템 로케일 정보로 설정.( 한글 깨짐 방지 )
	setlocale(LC_ALL, "");

	// 전달된 메시지를 보관하는 버퍼.
	unbounded_buffer< Message > messageBuffer;
	
	// 공지로 전달할 메시지.
	Message noticeMessage = { L"공지", L"2010년 8월 28일에 Visual Studio Camp 가 열립니다." };

	// 5초에 한 번씩 주기적으로 메시지를 전달할 timer
	timer< Message > notifier( 5000, noticeMessage, &messageBuffer, true );

	// timer 시작.
	notifier.start();

	// agent 들. 
	Typer typer( messageBuffer, L"MuMbi" );
	MessageDisplayer displayer( messageBuffer );

	array< agent*, 2 > agents = { &typer, &displayer };

	// agent 들 작업 시작.
	for_each( agents.begin(), agents.end(), []( agent* pAgent )
	{
		pAgent->start();
	} ) ;

	// agent 들 작업 종료까지 대기.
	agent::wait_for_all( agents.size(), &*agents.begin() );
}

[ 코드2. timer 를 이용한 주기적인 message 전송 예제 ]

 Typer 클래스는 사용자의 입력을 메시지 버퍼로 전송하는 역할을 하고, MessageDisplayer 클래스는 전송된 메시지들을 화면에 출력해주는 역할을 합니다.

 timer 는 5초마다 공지 사항을 메시지 버퍼로 전달합니다.

 결국 MessageDisplayer 클래스는 사용자의 메시지와 주기적으로 전달되는 메시지를 모두 화면에 출력하게 됩니다.

 

[ 그림1. timer 를 이용한 주기적인 message 전송 예제 결과 ]

[ 그림1. timer 를 이용한 주기적인 message 전송 예제 결과 ]

 

마치는 글

 이번 글을 마지막으로 AAL 에서 제공하는 모든 message block 들에 대해서 알아보았습니다.

 제공되는 message block 들은 각각 모두 사용할 상황이 다른 특징을 가지고 있고, 굉장히 유용합니다. 하지만 약간의 부족한 점들을 느끼고, 아쉬운 점이 보입니다.

 그런 점들을 보완하여 우리가 원하는 message block 을 만들 수 있습니다. 그렇게 만들어진 message block 은 스레드에 안전함을 보장해주어야 합니다. 

 다음글에서는 스레드 안전 보장을 위한 동기화 데이터 구조에 대해서 알아보도록 하겠습니다.

잊고 계셨을지도 모를 jqGrid 마지막편입니다.
이번 시간은 jqGrid를 이용하여 데이터를 추가, 편집, 삭제해보는 시간을 가져보도록 하겠습니다.


뷰페이지부터 보죠


지난 포스팅에 이어나갑니다. 먼저 가장 중요한 스크립트 부분을 보시면,

var updateDialog = {
                url: '<%= Url.Action("Update", "Home") %>'
                , closeAfterAdd: true
                , closeAfterEdit: true
                , modal: true
                , onclickSubmit: function (params) {
                    var ajaxData = {};
                    var list = $("#list");
                    var selectedRow = list.getGridParam("selrow");
                    rowData = list.getRowData(selectedRow);
                    ajaxData = { dirId: rowData.dirId };
                    return ajaxData;
                }
                , width: "400"
            };
            $.jgrid.nav.addtext = "추가";
            $.jgrid.nav.edittext = "편집";
            $.jgrid.nav.deltext = "삭제";
            $.jgrid.edit.addCaption = "전화번호부 추가";
            $.jgrid.edit.editCaption = "전화번호부 편집";
            $.jgrid.del.caption = "전화번호부 삭제";
            $.jgrid.del.msg = "정말 삭제하실거에요?";
            $("#list").jqGrid({
                url: '<%= Url.Action("EntityGridData", "Home") %>',
                datatype: 'json',
                mtype: 'POST',
                colNames: ['No', '이름', '전화번호', '이메일', '단축다이얼'],
                colModel: [
                  { name: 'dirId', index: 'dirId', width: 40, align: 'center', editable: true, editrules: { edithidden: false }, hidedlg: true, hidden: true },
                  { name: 'name', index: 'name', width: 200, align: 'left', editable: true, edittype: 'text', editrules: { required: true }, formoptions: { elmsuffix: ' *'} },
                  { name: 'phone', index: 'phone', width: 200, align: 'left', editable: true, edittype: 'text', editrules: { required: true }, formoptions: { elmsuffix: ' *'} },
                  { name: 'email', index: 'email', width: 300, align: 'left', editable: true, edittype: 'text', editrules: { required: true, email: true }, formoptions: { elmsuffix: ' *'} },
                  { name: 'speedDial', index: 'speedDial', width: 200, align: 'center', editable: true, edittype: 'text', editrules: { required: true }, formoptions: { elmsuffix: ' *'}}],
                pager: $('#pager'), 
                emptyrecords: "Nothing to display",             
                rowNum: 3,
                rowList: [3, 10, 20, 50],
                sortname: 'dirId',
                sortorder: "desc",
                viewrecords: true,
                caption: '전화번호부',
                ondblClickRow: function (rowid, iRow, iCol, e) {
                    $("#list").editGridRow(rowid, updateDialog);
                }
            }).navGrid('#pager',
                {
                    edit: true, add: true, del: true, search: false, refresh: true
                },
                updateDialog,
                updateDialog,
                updateDialog

            );

너무 많이 바뀌었나요? 그래도 알아보시죠? :) 굵은거~굵은거~

먼저 보이는것은 updateDialog가 보이네요. url도 보이고 submit 하고 ajax도 보이고.
데이터의 추가,편집,삭제시에 뜨는 팝업창에서 할일들이죠. Home 컨트롤러의 Update라는 액션메쏘드를 호출할거고요.
추가, 편집 후에는 창을 닫을 것이고(closeAfterAdd: true, closeAfterEdit: true), 모달창이고(modal: true), submit시 ajax를 사용하는 것 같아보이네요^^

다음으로 보이는게 $.jgrid.nav... 입니다. 이것은 지난 시간에도 말씀드렸던  grid.locale-en.js 을 수정하는 부분입니다. 이런 언어파일에는 디폴트값이 들어가 있다고 말씀드렸었죠? 기억하시죠? ;;

$.jgrid.edit = {
    addCaption: "Add Record",
    editCaption: "Edit Record",
    bSubmit: "Submit",
    bCancel: "Cancel",
    bClose: "Close",
    processData: "Processing...",
    msg: {
        required:"Field is required",
        number:"Please, enter valid number",
        minValue:"value must be greater than or equal to ",
        maxValue:"value must be less than or equal to",
        email: "is not a valid e-mail",
        integer: "Please, enter valid integer value",
        date: "Please, enter valid date value"
    }
};
$.jgrid.del = {
    caption: "Delete",
    msg: "Delete selected record(s)?",
    bSubmit: "Delete",
    bCancel: "Cancel",
    processData: "Processing..."
};
$.jgrid.nav = {
    edittext: " ",
    edittitle: "Edit selected row",
    addtext:" ",
    addtitle: "Add new row",
    deltext: " ",
    deltitle: "Delete selected row",
    searchtext: " ",
    searchtitle: "Find records",
    refreshtext: "",
    refreshtitle: "Reload Grid",
    alertcap: "Warning",
    alerttext: "Please, select row"
};

뷰페이지에서 수정한 부분은 두껍게 표시하였습니다.

$.jgrid.edit 의 msg중 required:"Field is required", email: "is not a valid e-mail" 이 부분이 뷰페이지의 colModel의 editrules: { required: true, email: true } 값과 매치가 되는 거죠.
또, grid의 네비게이션바에서 추가, 편집, 삭제 표시가 이미지로만 되어있었던 것을( $.jgrid.nav 의 add,edit,del 텍스트값이 빈값으로 되어있죠?) 뷰페이지에서 타이틀을 달아본겁니다.

나머지 부분도 재미있게 수정해서 테스트해보세요^^ 버튼 이름 변경, 필수값 에러 메시지, 경고메시지 등이 수정가능하네요. 



데이터를 처리하자


컨트롤러의 액션메쏘드가 추가되었겠죠? 다음은 HomeController의 추가된 Update 액션메쏘드입니다.

public ActionResult Update(TelDir telInfo, FormCollection formCollection)
{
    var operation = formCollection["oper"];
    if (operation.Equals("add"))
    {
       TelDir telData = new TelDir();
       telData.name = telInfo.name;
       telData.phone = telInfo.phone;
       telData.speedDial = telInfo.speedDial;
       telData.email = telInfo.email;
       _db.AddToTelDirSet(telData);
       _db.SaveChanges();
    }
    else if (operation.Equals("edit"))
    {
       var telData = _db.TelDirSet.First(m => m.dirId == telInfo.dirId);
       telData.name = telInfo.name;
       telData.phone = telInfo.phone;
       telData.speedDial = telInfo.speedDial;
       telData.email = telInfo.email;
       _db.SaveChanges();
    }
    else if (operation.Equals("del"))
    {
       var telData = _db.TelDirSet.First(m => m.dirId == telInfo.dirId);
       _db.DeleteObject(telData);
       _db.SaveChanges();
    }
    return Content("ok");
}

각 요청(add,edit,del) 대로 분기하여 처리하도록 하였습니다.



이제야 보는구나


실행을 해서 결과화면을 보면


멋드러지게 지난시간과는 다른 모습의 grid 가 있는 것을 확인할 수 있습니다. 추가,편집,삭제 버튼이 들어가 있고 옆에 리프레쉬 버튼도 있네요.

추가 버튼을 누르면


깔끔한 박스가 뜨네요. 전 슈퍼맨을 등록해보도록 하겠습니다. 값을 입력하지 않고 그냥 Submit 버튼을 살짝 눌러보시면 이름 입력하라고, 전화번호 입력하라고 아우성일겁니다. editrules: { required: true } 이것때문이죠. 매치된 메시지는 언어 스크립트에 있는 required:"Field is required" 이고요. 값을 정상적으로 입력후에 Submit을 하면...
바로바로 처리가 가능하네요^^ 모달창이 닫히는 부분은 위에서 설명드렸던 closeAfterAdd: true 때문인거죠. 편집하는 부분도 해볼까요? closeAfterEdit: truefalse로 하여 테스트해보세요. 모달창이 닫히지 않고 수정한 값들이 화면의 울렁거림 없이 처리가 되는 것을 확인하실 수 있습니다.

편집하는 부분을 테스트 하시려면 먼저 수정하려는 해당 로우(row)를 선택하신 후 편집 버튼을 클릭하시거나, 해당 로우를 더블클릭하시면 됩니다. 해보시죠?^^;


마지막으로 정말 삭제하실거에요? 를 본후 노는 시간을 마치도록 하겠습니다.




마무리요


정말 jquery 관련 플러그인들은 참 편리하네요. 이렇게 손쉽게 처리가 가능하니 말이죠.
암튼, 이번시간으로 jqGrid는 마치겠습니다. 뭐 한것도 없이 마친다고 하니 웃기네~ 라고 말씀을 해주셔도 마칠겁니다.^^;

아직은 무덥고, 다양한 날씨속에서 지내는 지금, 건강 꼭 챙기세요. 감사합니다^^

참고자료 : http://elijahmanor.com/webdevdotnet/post/jQuery-jqGrid-Plugin-Add-Edit-Delete-with-ASPNET-MVC.aspx

닷넷4.0에서 네이티브코드와 매나지드코드의 동거 part 2-1.

CLR 2010. 8. 10. 18:00 Posted by 알 수 없는 사용자
이번에는 저번 강좌에 이어서 클래스를 다뤄 보도록 하겠습니다.

약간 강좌가  외전으로 빠지는데요. ^^;

우선 명시적인 방법으로....

네이티브 코드에서 네이티브코드DLL로 작성된 클래스를 얻는 전통적인 방법부터 알아 보도록 하겠습니다.

class __declspec(dllexport) CTest {
public:
int Add(int a,int b);

static CTest *Create();
static void Delete(CTest* pobj);
};

int CTest::Add(int a,int b)
{
return a+b;
}

CTest* CTest::Create()
{
return new CTest();
}

void CTest::Delete(CTest* pobj)
{
delete pobj;
}

이와 같이 클래스를 선언해줍니다.  외부에서 동적으로 클래스를 참조하기 위해서는 ' __declspec(dllexport) ' 가 꼭 필요합니다.


사용하는 쪽에서는 다음과 같이 클래스를 정의만 해줍니다.



암시적접근은 정적 라이브러리 쓰는것과 동일하지만 명시적으로 접근 할때는 다음과 같이 펙토리 함수등을 통해서 객체를 직접 얻어와야 합니다.



정적 함수인 Create의 함수 포인터를 이용해서 객체에 대한 인스턴스를 직접 얻어옵니다. 이것을 this포인터로 사용합니다.
외부 참조 가능한 클래스멥버함수의 호출규약은 첫번째인자에 this포인터를 넘겨주도록 정해져있습니다. 

CTest::Add 함수의 포인터를 얻어와 첫번째 인자로 방금 얻은 객체 포인터를 넘겨줍니다.

pCalAdd(pCalc,10,7);

pCalc는 Create함수를 통해 얻어진 CTest클래스의 인스턴스포인터 입니다.
pCalAdd는 CTest::Add의 함수 포인터 입니다.

여기서  GetProcAddress 로 넘겨주는 함수이름이 [?Create@CTest@@SAPAV1@XZ] 처럼 이상한데요. 이유는 C++컴파일러가 외부 참조가능하도록 전통적인 c언어형식으로 함수를 다시 만들기 때문입니다.

원래는 2강좌로 마칠려고 했는데 쓰다보니 약간 두서 없이 방향이..... ㅡ.ㅡ;;;

다음번엔 진짜로 매나지드 코드와 네이티브 클래스 연결법에 대해서 알아보도록 하겠습니다. ^^;


[ALM-Test] 5. 테스트 계획

Agile Development 2010. 8. 10. 08:30 Posted by POWERUMC

테스트 계획은 테스트 작업을 시작하기 위한 가장 기초적인 명세서입니다. 테스트를 어떻게 진행하고 어떻게 결함을 발견할 것인지 계획을 갖고 테스트에 진입하는 것이죠. 테스트 계획 없이 테스트를 한다는 것은 아키텍처 없이 머리 속의 청사진으로 프로그램을 코드를 짜는 것과 마찬가지입니다. 그만큼 테스트 계획은 테스트에 있어서 첫 발을 내 딛는 중요한 작업입니다.

   

테스트 계획은 좋은 소프트웨어의 첫 걸음

아마 독자 여러분들 중에 개발 프로젝트를 한 두 번쯤 리딩(Leading) 해 보신 경험이 있다면 아실 겁니다. 좋은 설계과 좋은 계획만으로 좋은 소프트웨어가 나오는 것이 아니라는 것을…. 물론 좋은 소프트웨어를 위해 밑거름이 바로 계획입니다. 그리고 계획이 좋거나 나쁘다고 해서, 소프트웨어의 품질과 직결되는 문제는 아닐 수도 있습니다.

실제로 엉터리 계획을 가지고 소프트웨어를 만들어도, 정말 잘 되는 케이스(대외적으로)도 많습니다. 심지어 계획 없이 시작한 프로젝트도 좋은 소프트웨어(절대적으로 좋다는 것은 아닙니다)가 나올 수 도 있습니다. 반대로 아주 철저한 계획을 가지고 시작하더라도 소프트웨어의 품질이 엉망이기도 합니다.

최근 애자일(Agile) 방법론은, 설계 단계를 코딩(Coding) 으로 대체하기도 합니다. 계획 자체가 불필요한 산출물로 직결되는 경향이 많기 때문에, 바로 최종 산출물을 코드(Code) 로 바라보는 매우 간결한 방법론입니다. 그리고 설계 단계를 뛰어넘음으로써 발생하는 여러 가지 문제를 XP(eXtreme Programming) 에서 애자일 선언문 중 12가지 원칙으로 커버를 하고자 합니다.

 

깜놀~ MSDN 에서 애자일 방법론에 대한 설명이 있네요. (MSF v5.0과 관련된)
http://msdn.microsoft.com/ko-kr/library/dd997578.aspx

 

하지만 실제 현실에서는 어떤 특정한 계획 작업이 반드시 필요한 경우에 대부분입니다. 왜냐하면 지속적으로 소프트웨어가 운영되기 위해서는 과거의 이력이 쓸모 없기 보다는 적어도 필요 있는 경우가 대부분입니다. 설령 이력 관리가 되지 않은 문서라도 말입니다.

 

고객사의 소프트웨어의 산출물은 왜 관리가 안되는가?
몇 번의 컨설팅에 참여한 경험으로, 고객사의 산출물이 제대로 관리가 되지 않은 곳이 대부분이었습니다. 컨설팅을 수행하기 위해서 낯선 소프트웨어의 구조를 알기 위해, 산출물을 검토하면 실제로 현재와 다른 아키텍처나 구조를 가지고 있는 경우가 허다합니다.

왜 현재의 소프트웨어와 산출물이 일치하지 않는가에 대한 물음을 가질 수 있습니다. 산출물이 업데이트가 되지 않은 과거의 산출물이라면 특히 아키텍처나 구조를 파악하기 힘들 수 있거든요.

하지만, 굳이 산출물이 업데이트 되지 않더라도 누구를 탓할 수 있는 노릇은 아닙니다. 왜냐하면 업데이트가 되지 않은 산출물이라도 그 시대적인 배경과 당시의 이슈 등에 대해서 충분히 검토가 가능하기 때문입니다. 산출물이 업데이트 되지 않는 것은 무의미한 산출물이라고 말하는 사람들도 있지만, 꼭 나쁜 것은 아닙니다. 오히려 업데이트 되지 않은 산출물이 그 조직의 프로세스나 조직 구조를 파악하는데 더 도움이 되기도 하기 때문입니다.

   

테스트 계획, 어떻게 세워야 하나?

만약, 당신에게 소프트웨어의 특정 컴포넌트를 테스트를 해야 한다면, 바로 테스트의 계획을 어떻게 시작하냐 입니다. 필자의 생각으로는 개발보다 더 중요한 것이 바로 테스트라고 생각합니다. 잘못된 코딩을 검증할 수 있는 방법이 바로 테스트이기 때문입니다. 코드적인 오류나 비즈니스 프로세스적인 오류, 기술적인 오류 등 다양한 개발 코드에 잠재적인 오류가 내포되어 있습니다. 그것을 찾아내고 올바르게 검증하는 단계가 바로 테스트 단계입니다.

즉, 테스트를 하기 위한 목적과 목표가 뚜렸해야 합니다. 테스트 케이스는 테스트를 진행한 테스터(SDET) 에 의해 명확한 비전을 제시하며, 커뮤니케이션을 증진할 수 있습니다. 그렇기 때문에 테스트 케이스가 없는 테스트는 커뮤니케이션을 포기한 테스트와 다름 없습니다.

그럼 다음과 같은 단계로 테스트 계획을 진행할 수 있습니다.

  • 테스트 이름
    한 문장에 테스트에 대한 모든 요약 내용을 함축할 수 있어야 합니다.
  • 테스트 문제
    테스트 이름에 대한 설명입니다. 예를 들어, Stack Overflow 가 발생할 수 있다는 추측이나 테스트 목표를 서술합니다.
  • 분석
    개발자와 테스터의 업무는 전혀 다릅니다. 개발자는 문제 발생에 간과하여 넘어가는 경향이 있지만, 테스터는 그 문제를 밝혀내는 업무를 수행합니다. 문제가 되는 부분에 대해 왜 문제가 되는지 정곡을 찌르는 설명이나 추측을 서술합니다.
  • 설계
    실제 테스트가 수행될 경우, 어떤 파라메터와 결과 기대값을 갖는지에 대한 서술입니다. 테스트를 실제로 진행하게 될 과정을 서술하면 됩니다.
  • 오라클
    예상되는 결과에 대한 설명입니다. 즉, 설계 단계에서 왜 결과가 이렇게 나오게 될지에 대한 서술입니다.
  • 예제
    왜 결과 값이 이렇게 나오는지에 대한 예제나 코드 등을 나열하면 됩니다.
  • 함정과 계약
    예를 들면, 테스트에 사용된 파라메터가 항상 옳을 수는 없습니다. 즉, 업무 지식이 있는 사람과 없는 사람간에 테스트를 진행할 경우 테스트 결과에 대한 오차를 서술합니다. 왜냐하면 테스트 계획을 테스터와 비즈니스 업무 분석가 간에 시각 차이가 있을 수 있기 때문입니다.
  • 관련된 패턴
    이전에 말씀 드렸다시피 테스팅 패턴을 다양합니다. 어떤 테스팅 기법의 패턴으로 접근했는지에 대한 요약입니다.

위와 같이 테스트 계획을 수립할 수 있습니다. 하지만, 간결한 프로세스를 위해 몇 가지 항목은 건너뛰어도 괜찮다고 생각합니다. 필자가 테스팅 아티클을 쓰는 이유도 바로 이것 때문이죠. 우리나라 실정에 맞게 불필요 한 것은 제거하자.

필자의 경험으로 반드시 필요한 항목은 아래와 같습니다. 물론 필요한 경우 더 추가하셔도 좋습니다.

  • 테스트 이름
  • 테스트 문제
  • 함정과 계약

   

   

그렇다면 테스트 하는데 얼마나 걸리는데?

가장 민감한 사항입니다. 개발된 코드를 이용하여 테스트를 진행하는데 얼마만큼의 시간이 소요되는지에 대한 추정은 테스팅을 도입함에 있어서 비용과 직결되는 민감한 부분입니다. 왜 테스트를 진행해야 하는지조차 불분명하다면 차라리 전통적인 방법(수직적인 개발 방법론) 의 가장 마지막 단계에서 진행하는 것과 별반 다를 것이 없기 때문입니다.

개발자(SDE)가 자신의 코드를 테스트 하는 것과, 테스터(SDET) 가 테스트하는 것과 품질의 차이가 없다면 정말 테스트는 불품 없는 작업이기도 합니다. 하지만 분명한 것은 개발자의 시각과 테스터의 시각은 현저하게 다릅니다. 예를 들어, 개발자는 '사용자'가 이런 어처구니 없는 값을 입력하지 않을 거라고 단정하고 개발을 하지만, 테스터는 그렇지 않기 때문이죠. 이해하시죵? ^^

일반적으로 Microsoft 에서는 개발과 테스트의 시간을 1:1 로 봅니다. 즉, 개발 시간이 3시간이 걸리면, 테스트 시간도 3시간으로 예측합니다. 테스트에 소비되는 시간은 고객이 사용하는 소프트웨어의 결함에 대한 불쾌지수와 다름이 없습니다. 테스트가 적절한 패턴으로 정합적으로 수행되었다면, 그만큼 잠재적인 결함과 버그도 줄어들 것입니다.

   

테스트 산출물이 필요하다.

테스터(SDET) 가 테스트를 수행했더라도 잠재되어 있는 결함이나 버그는 여전할 수 있습니다. 테스트 패턴이 잘못된 접근법이나 방법을 사용했다면 잠재적인 치명적인 결함이나 버그로 이어질 수 있기 때문입니다. 그래서 특히 테스트에 대한 산출물이 테스터에게는 방어적이고 효율적인 수단이 될 수 있습니다.

가장 기본적으로 테스트는 코드의 모든 부분을 검증해야 합니다. 입력 값이 잘되었든, 잘못되었든 말이죠. 그리고 그 결과를 이력으로 저장하여 지속적으로 테스트의 검증이 올바르거나 효과적으로 진행되었다는 것도 기록이 필요합니다.

즉, 테스트를 진행해서 뭐가 좋아 졌는지의 수치적인 측정이 필요합니다.

  • 테스트 결과
  • 코드 커버러지
  • 스펙 종료 상태
  • 결함율 추이
  • 성능 테스트 결과

   

위의 항목에 대해서는 차근 차근 알아볼 예정입니다. 특히 주의할 사항은, 코드 커버러지가 100% 라도 테스트가 완전히 완료된 것은 아니라는 것입니다. 차후에 테스트 패턴에 대해서 알아보면서 다룰 예정입니다. 

Hello Windows Azure / Twitter 스타일 방명록 만들기 #2

Cloud 2010. 8. 10. 00:00 Posted by 알 수 없는 사용자

지난번 글 (2010/07/27 - [Cloud Development] - Hello Windows Azure / Twitter 스타일 방명록 만들기 #1)에 이어서 오늘 시간에는 ASP.NET MVC 2를 사용하는 Web Role 위에서 jQuery, jTemplate을 이용하여 기본적인 방명록 UI를 꾸며보고, 별 다른 Worker Role의 구현 없이 Windows Azure Table Storage를 경유하여 방명록의 글을 삽입, 삭제, 변경하는 기능을 구현해보기로 하겠습니다.

시작하기 전에 (2010.08.09 Update)

지난번 코드에서 누락되거나 교정될 필요가 있는 코드를 포함하여 업데이트를 할 부분이 있어 말씀을 전합니다. TwistDataSource.cs 파일의 내용을 다음과 같이 작성해야 하며, 지난번 코드에서 변경된 부분을 밑줄로 표시해두었습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure;
using System.Data.Services.Client;
using Microsoft.WindowsAzure.StorageClient;

namespace TwistBook.DataModel
{
    public class TwistDataSource
    {
        private static CloudStorageAccount storageAccount;
        private TwistDataServiceContext serviceContext;

        static TwistDataSource()
        {
            // 중요: 실제로 응용프로그램을 Cloud 환경에 배포할 때에는
            // Cloud Project 내의 다른 환경 설정 문자열을 이용하도록
            // 호출을 변경해야 합니다.
            storageAccount = CloudStorageAccount.DevelopmentStorageAccount;

            CloudTableClient.CreateTablesFromModel(
                typeof(TwistDataServiceContext),
                storageAccount.TableEndpoint.AbsoluteUri,
                storageAccount.Credentials);
        }

        public TwistDataSource()
        {
            this.serviceContext = new TwistDataServiceContext(storageAccount);
            this.serviceContext.RetryPolicy = RetryPolicies.Retry(
                3, TimeSpan.FromSeconds(1));
        }

        public DataServiceResponse Insert(TwistModel model)
        {
            this.serviceContext.AddObject(
                TwistDataServiceContext.TwistModelName,
                model);

            return this.serviceContext.SaveChangesWithRetries();
        }

        public IEnumerable<TwistModel> Select()
        {
            var results = from eachTwist in this.serviceContext.TwistModel
                          select eachTwist;

            var query = new CloudTableQuery<TwistModel>(
                results as DataServiceQuery<TwistModel>,
                RetryPolicies.Retry(3, TimeSpan.FromSeconds(1)));

            return query.Execute();
        }

        public DataServiceResponse Delete(TwistModel model)
        {
            // 이 부분의 코드가 삭제되었습니다.
            this.serviceContext.DeleteObject(model);
            return this.serviceContext.SaveChanges();
        }

        public DataServiceResponse Update(TwistModel model)
       
{
           
this.serviceContext.UpdateObject(model);
           
return this.serviceContext.SaveChanges();
        }

    }
}

Web Role 완성하기

1. ASP.NET MVC 2 응용프로그램의 특성을 잘 살리기 위하여 AJAX 기술을 활용하는 방식으로 예제를 설명하고자 합니다. 이를 위하여 필요한 것이 jQuery와 jTemplate 라이브러리인데, jQuery의 경우 ASP.NET MVC 2 프로젝트를 만들면 자동으로 아래의 Scripts 디렉터리에 1.4 버전이 번들링되어있으니 별도로 받으실 필요가 없습니다.

자바스크립트 라이브러리들의 경우, 근래 들어서는 4GL 개발 도구들의 영향으로 Debug Version과 Release Version 라이브러리를 각기 개별적으로 제공하는 경우가 늘었습니다. jQuery도 이러한 추세를 잘 따르고 있으며, 위의 화면에서 jquery-1.4.1-vsdoc.js 파일은 Debug 목적 + Visual Web Developer용 Intellisense 지원을 위한 버전이고, jquery-1.4.1.js 파일은 원래의 소스 코드가 있는 그대로 (as-is) 제공되는 버전입니다. 그리고 jquery-1.4.1.min.js 파일은 원래의 소스 코드에서 주석과 공백 제거, 변수명 최소화와 같은 Obfuscation Process를 포함한 Minified Process를 거친 전송에 최적화된 버전입니다.

자바스크립트 전송에 필요한 대역폭을 좀 더 아낄 필요가 있고, 접속하는 브라우저들이 모두 G-ZIP 압축 해제 기능을 지원한다는 점을 확신할 수 있다면, WSFU (Windows Service For Unix)나 Cygwin, GNU for Win32 등을 통해서 액세스할 수 있는 GZIP 압축 유틸리티를 이용하여 Minified Version을 GZIP 파일로 한 번 더 묶어서 이를 다운로드하도록 구성하는 것도 좋은 선택이 될 수 있습니다. WSFU는 http://www.microsoft.com/downloads/details.aspx?FamilyID=896c9688-601b-44f1-81a4-02878ff11778&DisplayLang=en 에서 다운로드 가능합니다.

2. jTemplate은 jQuery를 기반으로 만들어진 플러그인으로 HTML이나 XML 컨텐츠를 지정된 지시자에 맞추어 반복 생성하거나, 내용을 치환하거나, 수식을 계산하는 등의 복잡한 연산 작업을 가능하게 합니다. 특히 JSON (Java Script Object Notation) 기반의 데이터를 내려보내어줄 것이므로 이러한 기능은 필수적입니다. jTemplate은 http://plugins.jquery.com/project/jTemplates 에서 다운로드받으실 수 있고, 압축 파일을 다운로드받으면 아래와 유사한 형태로 나타납니다.

3. jquery-jtemplates.js 파일을 선택하여 ASP.NET MVC 2 프로젝트의 Scripts 디렉터리 아래로 복사합니다. jQuery 라이브러리와 같은 위치에 배치하여 불러오기 쉽도록 만들기 위한 선택입니다.

4. Visual Studio 솔루션 탐색기에서 방금 압축 해제한 jTemplate 라이브러리의 소스 코드를 추가해야 합니다. 솔루션 탐색기에서 Web Role 프로젝트 아래의 Scripts 디렉터리를 아래 그림과 같이 클릭하고 상단 도구 모음의 "모든 파일 표시" 버튼을 클릭하면 아직 등록되지 않은 jTemplate 라이브러리의 파일이 나타납니다.

5. jquery.jtemplates.js 파일을 오른쪽 버튼으로 클릭하고 "프로젝트에 포함" 메뉴를 클릭하면 솔루션의 일부로 편입됩니다. 이 때, jquery.jtemplates.js 파일을 오른쪽 버튼으로 클릭하고 속성 메뉴를 선택하여 나타나는 속성 창에서 빌드 작업이 "내용"으로 선택되어있는지 반드시 확인하여 주세요. "내용"으로 선택되어있지 않은 파일은 실제 배포 때 제외될 수도 있습니다.

6. 이제 마스터 페이지에 jQuery와 jTemplate 라이브러리를 추가해야 합니다. 여기서 마스터 페이지란 페이지 전반에 걸쳐서 기본 바탕이 되는 ASP.NET 사이트 수준의 골격 템플릿입니다. PowerPoint의 마스터 슬라이드와 비슷한 개념으로 이해해도 됩니다. 마스터 페이지는 Views 폴더 아래의 Shared 폴더 아래의 Site.Master 파일이며 아래와 같은 위치에 나타납니다.

7. Site.Master 파일을 열어서 아래와 같이 수정합니다. 원래 내용에서 수정된 부분을 굵게 표시하였으며 자세한 내용은 각주를 참조하여 주십시오.

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <link href="<%= Url.Content("~/Content/Site.css") %>" rel="stylesheet" type="text/css" /> [각주:1]
    <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery-1.4.1.min.js") %>"></script> [각주:2]
    <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery-jtemplates.js") %>"></script> [각주:3]

</head>

<body>
    <div class="page">

        <div id="header">
            <div id="title">
                <h1>내 MVC 응용 프로그램</h1>
            </div>
             
            <div id="logindisplay">
                <% Html.RenderPartial("LogOnUserControl"); %>
            </div>
           
            <div id="menucontainer">
           
                <ul id="menu">             
                    <li><%: Html.ActionLink("홈", "Index", "Home")%></li>
                    <li><%: Html.ActionLink("정보", "About", "Home")%></li>
                </ul>
           
            </div>
        </div>

        <div id="main">
            <asp:ContentPlaceHolder ID="MainContent" runat="server" />

            <div id="footer">
            </div>
        </div>
    </div>
</body>
</html>

8. 웹 페이지를 위한 기본 준비는 끝났습니다. 이제 Twitter Style의 방명록을 입력받을 수 있고 보여줄 수 있는 서비스를 만들기 위하여 서비스의 중심이 되는 Controller를 구성해보도록 하겠습니다. 편의를 위하여 HomeController를 편집하도록 하겠습니다. 솔루션 탐색기에서 TwistBook.WebRole 프로젝트의 Controllers 폴더 아래의 HomeController.cs 파일을 아래 그림과 같이 선택하여 엽니다.

9. ASP.NET MVC에서 컨트롤러 내에서 Public 접근자로 노출된 각각의 Method는 이전의 ASP.NET Web Form에 비유하였을 때 개별 처리기 (ASHX 파일)에서 웹 페이지를 결정하여 내보내는 것과 같은 개념으로 최초에 사용자가 페이지에 접근할 때나, 페이지의 FORM 태그로부터 응답이 되돌아온 시점에서 모두 사용이 가능합니다. 이러한 특성을 바탕으로, HomeController는 그 자체로 API의 역할을 수행할 수 있으며 역으로 페이지를 렌더링하기 위한 컨텐츠 단위로서의 역할도 수행이 가능합니다.

HomeController.cs 파일의 내용을 아래와 같이 수정합니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using TwistBook.DataModel;

namespace TwistBook.WebRole.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public HomeController()
            : base()
        {
        }

        public ActionResult Index()
        {
            ViewData["Message"] = "Windows Azure 방명록 예제";
            return View("Index"); [각주:4]
        }

        [HttpPost] [각주:5]
        public ActionResult RetrieveMessages()
        {
            var account = CloudStorageAccount.DevelopmentStorageAccount;
            var dataSource = new TwistDataSource();

            var results = from eachItem in dataSource.Select()
                          orderby eachItem.WrittenDate descending
                          select eachItem;

            return Json(results); [각주:6]
        }

        [HttpPost]
        public ActionResult AddMessage(string name, string message, string imageUrl)
        {
            var account = CloudStorageAccount.DevelopmentStorageAccount;
            var dataSource = new TwistDataSource();
            dataSource.Insert(new TwistModel()
            {
                WriterName = name,
                WrittenDate = DateTime.Now,
                MessageBody = message,
                ImageUrl = imageUrl
            }); [각주:7]

            return Index(); [각주:8]
        }

        public ActionResult UpdateMessage(string partitionKey, string rowKey) [각주:9]
        {
            var account = CloudStorageAccount.DevelopmentStorageAccount;
            var dataSource = new TwistDataSource();
            var results = from eachItem in dataSource.Select()
                          where eachItem.PartitionKey == partitionKey
                          where eachItem.RowKey == rowKey
                          select eachItem;

            ViewData["PartitionKey"] = partitionKey;
            ViewData["RowKey"] = rowKey; [각주:10]

            if (results.Count() > 0)
            {
                var result = results.First();
                ViewData["Name"] = result.WriterName;
                ViewData["Message"] = result.MessageBody; [각주:11]
            }

            return View();
        }

        [HttpPost]
        public ActionResult UpdateMessage(string partitionKey, string rowKey, string name, string message, string imageUrl) [각주:12]
        {
            var account = CloudStorageAccount.DevelopmentStorageAccount;
            var dataSource = new TwistDataSource();
            var results = from eachItem in dataSource.Select()
                          where eachItem.PartitionKey == partitionKey
                          where eachItem.RowKey == rowKey
                          select eachItem;

            if (results.Count() > 0)
            {
                var result = results.First();

                if (result != null)
                {
                    result.WriterName = name;
                    result.MessageBody = message;
                    result.WrittenDate = DateTime.Now;
                    result.ImageUrl = imageUrl;
                    dataSource.Update(result);
                    return View("PopupUpdateView"); [각주:13]
                }
                else
                    return View("PopupUpdateFailView");
            }
            else
                return View("PopupUpdateFailView"); [각주:14]
        }

        public ActionResult DeleteMessage(string partitionKey, string rowKey)
        {
            var account = CloudStorageAccount.DevelopmentStorageAccount;
            var dataSource = new TwistDataSource();
            var results = from eachItem in dataSource.Select()
                          where eachItem.PartitionKey == partitionKey
                          where eachItem.RowKey == rowKey
                          select eachItem;

            if (results.Count() > 0)
            {
                dataSource.Delete(results.First());
                return Index(); [각주:15]
            }
            else
                return Index();
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

10. 방명록의 기본 기능을 만들기 위하여 이제 Views 폴더 아래의 Home 폴더 아래의 Index.aspx 파일을 열어서 편집해야 합니다. 아래 그림과 같은 위치에 존재합니다.

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    홈 페이지
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <script type="text/javascript">
        $(document).ready(function () {
            $.ajax({
                type: 'POST', [각주:16]
                url: '<%= Url.Action("RetrieveMessages") %>', [각주:17]
                data: '{}',
                contentType: 'application/json; charset=utf-8',
                dataType: 'json', [각주:18]
                success: function (data) {
                    var targetDiv = $('#guestbookList');  [각주:19]
                    targetDiv.setTemplate($('#templateContent').html()); [각주:20]
                    targetDiv.processTemplate(data); [각주:21]
                }
            });
        });
    </script>
   
    <script type="text/html" id="templateContent">
    {#foreach $T as record}
    <div style="padding-bottom: 5px;">
        <img src="{$T.record.ImageUrl}" alt="" style="float: left; width: 100px;" />
        <div style="float: left; margin: 5px 5px 5px 5px;">
            <h3>RT @{$T.record.WriterName} {$T.record.MessageBody}</h3>
            <pre>{$T.record.WrittenDate} via cloud</pre>
            <a href="#" onclick="window.open('<%= Url.Content("~/Home/UpdateMessage") %>?partitionKey={$T.record.PartitionKey}&rowKey={$T.record.RowKey}', 'editWindow', 'location=1,status=1,scrollbars=1,width=300,height=200');">편집</a>
            &nbsp;|&nbsp;
            <a href="<%= Url.Content("~/Home/DeleteMessage") %>?partitionKey={$T.record.PartitionKey}&rowKey={$T.record.RowKey}" target="_self">삭제</a>
        </div>
        <div style="clear: both;"></div>
    </div>
    {#/for}
    </script> [각주:22]

    <h2><%= ViewData["Message"] %></h2>
    <div>
        <div>
            <% using (var form = Html.BeginForm("AddMessage", "Home", FormMethod.Post))
               { %> [각주:23]

               <%: Html.Label("이름") %>
               <%: Html.TextBox("name", "What is your name?") %> [각주:24]
               <br />

               <%: Html.TextArea("message", "Type your message here.", 3, 100, null) %>
               <br /> [각주:25]

               <input type="submit" value="보내기" />&nbsp;<input type="reset" value="초기화" /> [각주:26]
               <br />
            <% } %>
        </div>
        <br /><br />
        <div id="guestbookList"></div> [각주:27]
    </div>
</asp:Content>

11. 방명록 내용을 편집하기 위한 팝업 창을 위한 뷰와, 댓글 편집이 끝난 뒤 취할 동작을 프로그래밍한 자바스크립트 코드를 위한 뷰는 Partial View로 디자인해야 합니다. 이 중에서 우선 방명록 항목 편집을 위한 Partial View를 추가하기 위해, 솔루션 탐색기에서 Views 디렉터리 아래의 Home 디렉터리를 오른쪽 버튼으로 클릭하고, View 추가 메뉴를 아래 그림과 같이 선택합니다.

12. View의 이름은 UpdateMessage로 지정하고, Partial View에 체크하여 아래 대화 상자와 같이 옵션을 구성한 후 확인 버튼을 클릭합니다.

13. UpdateMessage.ascx 파일의 내용을 다음과 같이 작성합니다.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<div>
    <% using (var form = Html.BeginForm("UpdateMessage", "Home", FormMethod.Post))
       { %> [각주:28]

       <%: Html.Hidden("partitionKey", ViewData["PartitionKey"]) %>
       <%: Html.Hidden("rowKey", ViewData["RowKey"]) %> [각주:29]

       <%: Html.Label("이름") %> 
       <%: Html.TextBox("name", (string)ViewData["Name"]) %> [각주:30]
       <br />

       <%: Html.TextArea("message", (string)ViewData["Message"], 3, 100, null) %>[각주:31]
       <br />

       <input type="submit" />&nbsp;<input type="reset" /> [각주:32]
       <br />
    <% } %>
</div>

14. 이어서 솔루션 탐색기에서 Views 디렉터리 아래의 Home 디렉터리를 오른쪽 버튼으로 클릭하고, View 추가 메뉴를 11단계에서와 같이 선택합니다.

15. View의 이름은 PopupUpdateView로 지정하고, Partial View에 체크하여 아래 대화 상자와 같이 옵션을 구성한 후 확인 버튼을 클릭합니다.

16. PopupUpdateView.ascx 파일의 내용을 다음과 같이 작성합니다.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<script type="text/javascript">
    try {
        window.close(); [각주:33]
        if (window.opener && !window.opener.closed) {
            window.opener.location.href = '<%= Url.Content("~/Home/Index") %>'; [각주:34]
        }
    } catch (ex) {
    }
</script>

17. 이어서 솔루션 탐색기에서 Views 디렉터리 아래의 Home 디렉터리를 오른쪽 버튼으로 클릭하고, View 추가 메뉴를 11단계에서와 같이 선택합니다.

18. View의 이름은 PopupUpdateFailView로 지정하고, Partial View에 체크하여 아래 대화 상자와 같이 옵션을 구성한 후 확인 버튼을 클릭합니다.

19. PopupUpdateFailView.ascx 파일의 내용을 다음과 같이 작성합니다.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<h3>업데이트에 실패하였습니다.</h3>
<a href="#" onclick="window.close()">창 닫기</a>

20. 기본적인 방명록 글 남기기와 조회 기능이 올바르게 작동하는지 확인하기 위하여 시뮬레이터를 디버그 모드로 시작해야 합니다. 일반적인 응용프로그램 개발 때와 마찬가지로 F5키를 눌러서 디버그 모드로 시뮬레이터에 패키지를 배포하고 디버거를 연결할 수 있습니다. 이 때, 아래 그림과 같은 오류 메시지가 나타나면 관리자 권한이 아닌 상태에서 Visual Studio를 시작한 것이므로 Visual Studio를 종료한 뒤 "개발 도구 시작하기 및 프로젝트 생성하기" Chapter의 1단계를 참고하여 관리자 모드로 Visual Studio를 다시 시작해야 합니다.

21. 아래의 그림들에서처럼 기능들이 정상적으로 진행된다면 우선 이번 시간에 진행할 기본 기능들에 대한 소개와 작업이 끝난 것입니다.

이번 Article을 작성하면서 발견한 Windows Azure SDK 1.2에 대한 문제 한 가지

좀 더 완성에 가까워질수록 해결될 문제들 중에 한 가지가 될 예정이긴 하겠습니다만 실습하는 도중 불편함이 예상되어 제가 발견한 문제를 블로그 아티클을 통하여 미리 공유하고자 합니다. 간혹 Windows Azure Local Storage의 Table Storage에 아래와 같이 MBCS (Multi-Byte Character Set) 문자가 포함된 데이터를 삽입하려고 할 때 별 다른 까닭없이 HTTP/404 오류가 나타나는 경우가 있습니다.

사용자 코드에서 System.Data.Services.Client.DataServiceRequestException이(가) 처리되지 않았습니다.
  Message=이 요청을 처리하는 동안 오류가 발생했습니다.
  Source=Microsoft.WindowsAzure.StorageClient
  StackTrace:
       위치: Microsoft.WindowsAzure.StorageClient.Tasks.Task`1.get_Result()
       위치: Microsoft.WindowsAzure.StorageClient.Tasks.Task`1.ExecuteAndWait()
       위치: Microsoft.WindowsAzure.StorageClient.TaskImplHelper.ExecuteImplWithRetry[T](Func`2 impl, RetryPolicy policy)
       위치: Microsoft.WindowsAzure.StorageClient.TableServiceContext.SaveChangesWithRetries(SaveChangesOptions options)
       위치: Microsoft.WindowsAzure.StorageClient.TableServiceContext.SaveChangesWithRetries()
       위치: TwistBook.DataModel.TwistDataSource.Insert(TwistModel model) 파일 d:\users\남정현\documents\visual studio 2010\Projects\TwistBook\TwistBook.DataModel\TwistDataSource.cs:줄 42
       위치: TwistBook.WebRole.Controllers.HomeController.AddMessage(String name, String message, String imageUrl) 파일 d:\users\남정현\documents\visual studio 2010\Projects\TwistBook\TwistBook.WebRole\Controllers\HomeController.cs:줄 44
       위치: lambda_method(Closure , ControllerBase , Object[] )
       위치: System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)
       위치: System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters)
       위치: System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
       위치: System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClassd.<InvokeActionMethodWithFilters>b__a()
       위치: System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
  InnerException: System.Data.Services.Client.DataServiceClientException
       Message=<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code>InvalidInput</code>
  <message xml:lang="ko-KR">One of the request inputs is not valid.</message>
</error>
       Source=System.Data.Services.Client
       StatusCode=400
       StackTrace:
            위치: System.Data.Services.Client.DataServiceContext.SaveResult.<HandleBatchResponse>d__1e.MoveNext()
       InnerException:



실제 Windows Azure 실행 환경에서는 이러한 현상이 나타나지 않는 것으로 보입니다. 추후, 이러한 문제점을 해결할 수 있는 방안이 발견되면 별도의 업데이트 소식을 통하여 정보가 전달될 수 있도록 하겠습니다. 예제를 기반으로 테스트 패브릭 위에서 테스트하시는 동안에는 Table Storage에 한글, 히라가나, 카타카나, 번체, 간체, 한자 등의 데이터가 들어가지 않는 범위에서 테스트가 필요할 것 같습니다.

다음 시간에는

다음 시간에는 각 Role이 어떤 방법으로 Windows Azure 환경에서 실행되는지, Web Role과 Worker Role이 Cloud Computing 환경에서 상호 작용하고 통신하는 방법을 본격적으로 소개하고, 오늘 만든 Web Role을 어떤 방식으로 수정하게 될 것이고, Worker Role이 어떤 방식으로 데이터를 교환하게 될 것인지를 보여드릴 예정입니다. 그리고 이번 시간에 언급하지 않은 BLOB Storage에 이미지를 저장하고 가져오는 방법에 대해서도 소개하겠습니다. :-)

더운 여름 날씨에 건강 유의하시고, 활기찬 여름 보내시기 바랍니다. 감사합니다.

ps. Windows Azure Cafe (http://cafe.naver.com/wazure) 에서 2010년 8월 14일부터 본격적으로 Offline Study를 진행합니다. Windows Azure Platform의 학습에 관심있으신 개발자 여러분들의 많은 관심과 참여 부탁드리며, 아울러 Visual Studio 2010 공식 팀 블로그에서 Cloud Computing 관련 Article을 집필하실 열정적인 Blogger 여러분도 함께 모시고 있습니다. 이에 관련된 모든 상세한 내용은 Windows Azure Cafe를 통하여 저에게 연락 주시면 상세히 안내해드리겠습니다. 감사합니다. :-)

  1. 기본으로 제공되는 템플릿 코드로부터 수정한 부분으로, 마스터 페이지는 처리 과정 도중에 해석되는 파일이지만 브라우저의 입장에서 서비스를 하는 페이지가 아니기 때문에, 기본으로 제공되는 경로인 ../../Content/Stie.css는 잘못 해석될 가능성이 있습니다. 이를 예방하기 위하여 Inline Expression을 사용하여 Url.Content 메서드로 정확한 경로를 다시 가져오도록 만든 것입니다. [본문으로]
  2. 기본으로 제공되는 템플릿 코드로부터 수정한 부분으로, 마스터 페이지는 처리 과정 도중에 해석되는 파일이지만 브라우저의 입장에서 서비스를 하는 페이지가 아니기 때문에, 기본으로 제공되는 경로인 ../../Content/jquery-1.4.1.min.js는 잘못 해석될 가능성이 있습니다. 이를 예방하기 위하여 Inline Expression을 사용하여 Url.Content 메서드로 정확한 경로를 다시 가져오도록 만든 것입니다. [본문으로]
  3. 이번 시간에 JSON 기반의 데이터를 표현하기 위하여 사용할 jTemplate 라이브러리를 여기에서 지정합니다. 앞의 URL들과 마찬가지의 방법을 사용하여 정확하게 경로가 참조될 수 있도록 만들어줍니다. [본문으로]
  4. 기본 Index 메서드에서는 return View(); 로 호출하였지만, 다른 Controller Method에서 이 메서드를 호출하게 되는 경우, Index View가 아닌 Controller 그 자신의 View를 찾도록 기본 설계가 구성되어있기 때문에, 이를 방지하고 재 사용하기 쉬운 형태로 만들기 위해 특별히 "Index"라는 뷰 이름을 찾도록 명시한 것입니다. [본문으로]
  5. POST 요청에 의해서만 메시지가 JSON 형식으로 내려가도록 구성하기 위한 것으로, 필요에 따라이 Attribute를 누락하고, 아래의 Json 메서드 호출에서 AllowGet 인자를 지정하면 GET 요청에 의해서도 조회 결과가 JSON으로 반환될 수 있습니다. [본문으로]
  6. ActionResult 클래스를 상위 클래스로 두는 JSON Serialization Result 객체를 반환합니다. [본문으로]
  7. 데이터 삽입 직후 Commit 연산까지 한번에 처리하도록 설계된 메서드를 부르는 것입니다. [본문으로]
  8. 중요: 이 함수의 결과로 나타나는 View가 AddMessage가 아니라 Index입니다. [본문으로]
  9. 동일한 메서드에 대한 오버로드이지만, GET 방식으로 호출될 수 있고, 인자를 2개를 받도록 구성되어있으므로 이 버전의 메서드에서는 View를 렌더링하는데 사용됩니다. [본문으로]
  10. ViewData 컬렉션에 Update 동작을 구현하기 위해 필요한 정보를 다시 전달합니다. 나중에 View에서 이 정보를 참조하여 페이지를 렌더링하게 됩니다. [본문으로]
  11. 조회된 결과를 페이지 렌더링을 위하여 ViewData 컬렉션에 보관합니다. [본문으로]
  12. 동일한 버전의 UpdateMessage 메서드에 대한 오버로드이지만, POST 요청에만 동작하도록 설계된 버전의 Controller Method입니다. [본문으로]
  13. 편집을 마친 후, 미리 구성된 PopupUpdateView를 찾아 이동합니다. 이 뷰는 팝업창 형태로 열린 편집 창을 닫고, 팝업 창의 부모 (window.opener)를 새로 고침하도록 디자인된 뷰입니다. [본문으로]
  14. 업데이트에 실패할 경우 보여줄 View를 지정합니다. [본문으로]
  15. 데이터 삭제 후 Index 뷰를 다시 로드하도록 만들었습니다. [본문으로]
  16. XmlHttpRequest 객체를 이용하여 전송할 때 POST 방식으로 요청하는 것을 명시하고 있습니다. [본문으로]
  17. RetrieveMessages Controller Method를 정확히 찾을 수 있도록 전체 경로를 반환하는 함수를 사용하여 스크립트 위에 렌더링합니다. [본문으로]
  18. JSON 방식의 결과 집합이 필요함을 명시하고, JSON 방식으로 데이터를 받아들이도록 구성하고 있습니다. [본문으로]
  19. jTemplate 엔진으로 치환된 내용을 렌더링할 대상 div element를 찾습니다. [본문으로]
  20. 기준이 되는 템플릿 컨텐츠를 로드합니다. 이스케이프 문자로 복잡하게 처리하지 않고 편리하게 다룰 수 있도록 만들기 위하여, JavaScript나 VBScript로 해석되지 않도록 처리한 별도의 SCRIPT element로부터 로드하도록 구성하였습니다. [본문으로]
  21. jTemplate 엔진을 이용하여 주어진 데이터를 통해 렌더링을 시작합니다. [본문으로]
  22. 렌더링에 필요한 템플릿 코드가 이곳에 기술됩니다. 이 부분은 스크립트 태그 안에 있지만 스크립트 해석기에 의하여 처리되지는 않으며, 또한 시각적으로 드러나지도 않습니다. (as-is string으로 해석됩니다.) [본문으로]
  23. 메시지를 추가하기 위한 form 데이터를 구성하고 있습니다. [본문으로]
  24. Controller Method의 name 매개 변수와 이름을 같게 지정합니다. [본문으로]
  25. Controller Method의 message 매개 변수와 이름을 같게 지정합니다. [본문으로]
  26. 전송 버튼과 초기화 버튼이 trigger 역할을 하여 데이터를 전송하거나 리셋하는 역할을 합니다. [본문으로]
  27. 방명록 목록은 이 요소 아래에 rendering 될 것입니다. [본문으로]
  28. UpdateMessage의 POST 전송 대상을 찾아 업데이트 작업을 수행하도록 만듭니다. [본문으로]
  29. 링크에 의하여 GET 방식으로 전달된 매개 변수를 다시 렌더링하여 재사용합니다. [본문으로]
  30. ViewData에 저장된 기존 데이터를 꺼내옵니다. [본문으로]
  31. ViewData에 저장된 기존 데이터를 꺼내옵니다. [본문으로]
  32. 전송 버튼과 초기화 버튼이 trigger 역할을 하여 데이터를 전송하거나 리셋하는 역할을 합니다. [본문으로]
  33. 팝업 창을 닫습니다. [본문으로]
  34. 팝업 부모 창이 유효하다면, 정확한 Index View의 URL을 찾아 다시 로드하도록 만듭니다. [본문으로]

VS2010 C++ 프로젝트의 디렉토리 설정

Visual C++ 10 2010. 8. 9. 09:00 Posted by 알 수 없는 사용자

VS2008까지는 도구’ – ‘옵션메뉴를 통해서 VC++ 디렉토리를 설정하였습니다.

이렇게 설정한 정보는 모든 VC++ 프로젝트에 적용됩니다.

 

그러나 VS2010에서는 각 프로젝트 별로 VC++ 디렉토리를 설정합니다.

 


각 프로젝트 마다 독립적으로 설정을 할 수 있어서 편한 부분도 있지만 때로는 모든 프로젝트에 공통적으로 적용되어야 하는 경우는 매번 설정하는 것이 귀찮을 수 있습니다.

( 예로 DirectX boost 라이브러리 등 )

 

이런 경우 속성 매니저를 통해서 VC++ 디렉토리를 설정하면 모든 프로젝트에 공통적으로 적용할 수 있습니다.

 

1. 일단 아무 프로젝트 하나를 열어 놓습니다.

2. 메뉴의 보기’ -> ‘속성 관리자를 선택합니다.



3. 속성 관리자에서 ‘Microsoft.Cpp.Win32.user’를 더블 클릭해서 열어 놓습니다.


 

여기서 설정한 정보는 모든 프로젝트에 공통적으로 적용됩니다.

 

 

Asynchronous Agents Library
– message block 7. ( join & multitype_join )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 이 글에서는 동기화 메커니즘으로 사용할 수 있는 joinmultitype_join 에 대해서 알아보겠습니다. join 은 이전 글에서 소개한 choice 와 함께 동기화 메커니즘으로 사용할 수 있습니다.

 

join< _Type, _JType >

 join 은 연결된 모든 message block 에서 message 를 받아올 때까지 기다리는 역할을 합니다. join 의 이 역할을 수행하기 위해서는 전달 함수인 receive() 와 함께 사용해야 합니다.

 템플릿 매개변수인 _Type 은 message 의 타입입니다. 연결된 message block 들이 처리하는 message 타입으로 같은 타입이어야 합니다.

 템플릿 매개변수인 _JType 은 join 의 타입입니다. join 은 2가지 종류가 있습니다. 하나는 greedy 이고, 다른 하나는 non_greedy 입니다.

 _JType 에는 greedy 또는 non_greedy 를 사용할 수 있으며, 기본 템플릿 인자로 non_greedy 가 지정되어 있습니다.

 join 타입은 다음과 같은 enum 형으로 정의되어 있습니다.

enum join_type { 
    greedy = 0,
    non_greedy = 1 
};

[ 코드1. join 타입 ]

 두 타입의 join 모두 연결된 message block 들에서 message 를 받아올 때까지 기다리는 기능은 같습니다. 그렇다면 다른 점에 대해서 알아보겠습니다.

 기본 인자로 지정된 non_greedy 타입은 연결된 message block 들 중 어떤 message block 이 먼저 message 를 받아올 수 있는 상태가 되더라도 연결된 모든 message block 들이 모두 message 를 받아올 수 있는 상태가 될 때까지 message block 에 받아올 수 있는 message 들을 유지합니다. 연결된 모든 message block 들에서 message 를 받아올 수 있을 때가 되면 한꺼번에 message 들을 받아옵니다.

 반면 greedy 타입은 연결된 message block 들 중 어떤 message block 이 먼저 message 를 받아올 수 있는 상태가 되면 그 message 를 미리 받아옵니다. 이 때 이 message block 이 unbounded_buffer 라면 이로 인해 join 이 받아간 message 가 제거됩니다.

 

멤버 함수

 생성자와 소멸자를 제외한 public 인 멤버 함수가 없습니다. 하지만 ISource 인터페이스를 재정의한 link_target() 을 사용할 수 있으며, 이 함수는 join 과 다른 message block 을 연결하는데 사용하게 됩니다.

 가장 기본적인 생성자의 매개변수는 연결할 message block 의 개수입니다. 이 개수보다 실제로 연결된 message block 의 개수가 많으면 런타임에 에러를 발생하고, 적으면 교착상태에 빠지게 됩니다. 생성자의 매개변수로 지정된 message block 의 개수가 연결되어 message 를 받을 때까지 대기하기 때문입니다.

 

특징

 message block 이 join 을 연결할 경우 몇 가지의 특징들이 있습니다.

  • 하나의 message block 이 여러 개의 join 에 연결될 경우, 먼저 연결된 join 에 message 를 보냅니다.
  • 연결된 join 들 중 greedy join 이 있을 경우 greedy join 에게 먼저 message 를 보냅니다.
  • 연결된 join 들 중 greedy join 이 여러 개일 경우, 먼저 연결된 greedy join 에 message 를 보냅니다.

 

 또한 어떤 mesage block 들은 join 과 함께 사용할 수 없습니다.

  • single_assignment 는 단 한번만 send() 가 적용되고 이 후의 send() 는 무시되기 때문에 joinsingle_assignment 으로부터 message 를 한번만 받을 수 있다. 두 번 이상은 받을 수 없기 때문에 무한대기하게 되어 교착상태에 빠진다.
  • transformerjoin 에 연결한 후, 연결한 transformer 로 부터 message 를 받아가면 에러를 발생한다.

 

multitype_join< _TupleType, _JType >

 multitype_joinjoin 과 같은 기능을 하지만 다른 종류의 message 들을 받을 수 있습니다. 그래서 템플릿 매개변수가 message 타입이 아닌 tuple 타입을 받습니다. tuple 에 사용할 수 있는 타입은 choice 에서 사용하는 tuple 의 특징과 같습니다.

 multitype_join 의 타입은 join 과 마찬가지로 greedynon_greedy 를 사용할 수 있습니다.

 

멤버 함수

생성자와 소멸자를 제외한 public 인 멤버 함수는 없고, 인터페이스를 재정의한 멤버 함수들이 있습니다.

 

헬퍼 함수

 tuple 을 사용하는 choice 처럼 multitype_join 도 tuple 을 사용하므로 헬퍼 함수 없이는 굉장히 길고 복잡한 선언이 필요합니다.

그래서 다음과 같은 헬퍼 함수를 제공합니다.

unbounded_buffer< int > i;
unbounded_buffer< float > f;
unbounded_buffer< bool > b;

auto j = make_join( &i, &f, &b );

[ 코드2. 헬퍼 함수 make_join ]

 

예제

 join 의 특징을 살펴볼 수 있는 간략한 예제를 보겠습니다.

 

시나리오

 보통 온라인 게임에서 게임을 시작하게 되면 연결된 모든 클라이언트가 모두 로딩이 완료된 후 동시에 게임을 시작하게 됩니다. 그것을 join 을 이용해서 시뮬레이션 해보겠습니다.

 

코드

#include <iostream>
#include <array>
#include <random>
#include <agents.h>

using namespace std;
using namespace Concurrency;

const unsigned int CLIENT_COUNT = 10;

class Loader
	: public agent
{
public:
	~Loader()
	{
		this->isComplete.unlink_targets();
	}

	Loader( unsigned long id, join< bool >& synchronizer )
		: random_generator( id )
	{
		this->isComplete.link_target( &synchronizer );
	}

protected:
	void run()
	{
		this->Loading();

		send( this->isComplete, true );
		wcout << L"Load is complete." << endl;
		
		this->done();
	}

private:
	void Loading()
	{
		unsigned int loadingTime = random_generator() % 3000 + 1000;
		Concurrency::wait( loadingTime );
		wcout << L"Loading time: " << loadingTime / 1000.0f << L" sec..";
	}

private:	
	single_assignment< bool >	isComplete;
	mt19937						random_generator;
};

int main()
{	
	join< bool > synchronizer( CLIENT_COUNT );
	
	array< Loader, CLIENT_COUNT > loaderArray = {
		Loader( 1, synchronizer ),
		Loader( 2, synchronizer ),
		Loader( 3, synchronizer ),
		Loader( 4, synchronizer ),
		Loader( 5, synchronizer ),
		Loader( 6, synchronizer ),
		Loader( 7, synchronizer ),
		Loader( 8, synchronizer ),
		Loader( 9, synchronizer ),
		Loader( 10, synchronizer ),
	};

	for_each( loaderArray.begin(), loaderArray.end(), []( Loader& loader ) { loader.start(); } );

	receive( synchronizer );

	wcout << L"Game Start!" << endl;

	for_each( loaderArray.begin(), loaderArray.end(), []( Loader& loader ) { agent::wait( &loader ); } );
}

[ 코드3. join 을 이용한 동기화 예제 ]

 10개의 클라이언트 역할을 하는 agent가 있고, 각 클라이언트마다 로딩 속도가 다른 것을 시뮬레이션 하기 위해 난수를 사용하여 로딩 시간을 결정하였습니다.

 각 agent 는 완료를 나타내는 single_assignment 를 하나의 join 에 연결하여 로딩이 모두 완료될 때까지 기다립니다.

  join 에서 message 를 받는다는 것이 동기화가 완료된 것이므로 바로 게임을 시작합니다.

 

[ 그림1. join 을 이용한 동기화 예제 ]

[ 그림1. join 을 이용한 동기화 예제 ]

 

마치는 글

 이번 글에서 joinmultitype_join 에 대해서 알아보았습니다.

 choice 와 비교해서 choice 는 연결된 message block 들 중 먼저 들어온 message 가 있으면 바로 message 를 받을 수 있던 반면에 join 은 연결된 message block 들로부터 message 를 모두 받을 때까지 대기합니다.

 이 message block 들, choicejoin, multitype_join 은 Win32 API 의 WaitForMultipleObjects() 와 비슷한 기능을 한다는 것을 알 수 있습니다.

 이 message block 들의 특징은 외부 이벤트 객체가 아닌 데이터 자체가 이벤트로 바인딩되어 처리된다는 점입니다.

결론적으로 이 message block 들을 이용해 동기화를 구현할 수 있음을 알 수 있습니다.

[Step. 08] 프로퍼티 ( property )

C++/CLI 2010. 8. 6. 09:30 Posted by 알 수 없는 사용자

보통 비관리 클래스를 정의할 때 캡슐화를 위해서 멤버 변수는 최대한 private 접근으로 한 후 외부에서의 접근을 위해서 get set 멤버 함수를 정의합니다.

 

class Character

{

public:

…….

   void SetCharCd( const int nCharCd ) { m_nCharCd = nCharCd; }

   int GetCharCd() { return m_nCharCd; }

…….

private:

   int m_nCharCd;

   …….

};

 


클래스의 멤버를 하나 정의할 때마다 그에 대응하는 get, set 멤버 함수를 정의하는 것은 좀 귀찮은 일이기도 합니다. 그래서 관리코드에서는 이 작업을 쉽게 해주는 property가 생겼습니다.

 

ref class Character

{

public:

…….

    property int CharCd

{

   void set( int nCharCd ) { m_nCharCd = nCharCd; }

   int get() { return m_nCharCd; }

}  

…….

private:

   int m_nCharCd;

   …….

};

 

위 코드에서는 get set을 둘 다 정의 했는데 둘 중 하나만 정의 해도 괜찮습니다.

또 위의 set은 아주 간단하게 그냥 대입만 하고 있는데 좀 더 로직을 넣을 수도 있습니다.

 

ref class Character

{

public:

…….

    property int CharCd

{

   void set( int nCharCd )

{

  if( nCharCd < 0 ) {

     m_nCharCd = 0;

  } else {

     m_nCharCd = nCharCd;

  }

}

 

   int get() { return m_nCharCd; }

}  

…….

private:

   int m_nCharCd;

   …….

};

 

 


property 선언과 정의 나누기


일반적으로 클래스의 멤버 선언은 헤더 파일에 정의는 cpp 파일에 하듯이 property도 선언과 정의를 나눌 수 있습니다.

 

ref class Character

{

public:

…….

    property int CharCd

{

   void set( int nCharCd );

   int get();

}  

…….

private:

   int m_nCharCd;

   …….

};

 

 

void Character::CharCd::set( int nCharCd )

{

if( nCharCd < 0 ) {

m_nCharCd = 0;

} else {

   m_nCharCd = nCharCd;

  }

}

 

void Character::CharCd::get()

{

return m_nCharCd;

}

 



 

get set의 접근 지정자 다르게 하기

 

위에서 property를 정의할 때 get set은 모두 public 였습니다.

이것을 각각 다르게 접근 지정자를 정할 수 있습니다.

 

ref class Character

{

public:

…….

    property int CharCd

{

protected:

   void set( int nCharCd )

{

  if( nCharCd < 0 ) {

     m_nCharCd = 0;

  } else {

     m_nCharCd = nCharCd;

  }

}

 

    public:

   int get() { return m_nCharCd; }

}  

…….

private:

   int m_nCharCd;

   …….

};

 

 

 

 

참고

http://cppcli.shacknet.nu/cli:property

http://vene.wankuma.com/prog/CppCli_Property.aspx

 

 

Asynchronous Agents Library
– message block 6. ( choice )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 unbounded_buffer, overwrite_buffer 그리고  single_assignment 와 같이 message 를 보관하는 버퍼 기능의 message block 들과, call, transformer 와 같이 지정된 함수형 객체를 수행하는 기능의 message block 들에 대해서 알아보았습니다.

 이번 글에서는 message block 들의 상태를 기반으로 동작하는 message block 들 중 choice 에 대해서 알아보도록 하겠습니다.

 

choice< _TupleType >

 choice 의 특징은 지정된 message block 들 중 가장 먼저 messge 를 가져올 수 있는 message block 을 가리키는 기능입니다. 다시 설명하면 어떤 message 를 받을 준비가 된 message block 들을 choice 에 지정한 후, 지정된 message block 들 중 가장 먼저 message 를 받은 message block 을 choice 가 가리키게 됩니다.

 

tuple 의 조건

 choice 은 message 를 받을 준비가 된 message block 들을 지정할 때 tr1 의 tuple 을 사용합니다. 이 글의 주제가 tr1 이 아닌 만큼 tuple 에 대한 자세한 설명은 생략하겠습니다. 간략히 설명하면 std::pair 의 일반화된 객체라고 생각하시면 됩니다. std::pair 가 2개의 타입을 저장할 수 있다면 tuple 은 10개의 여러 가지 타입을 저장할 수 있습니다.

 그래서 다른 message block 과 달리 템플릿 매개변수가 message 타입이 아닌 여러 message block 들의 타입을 저장할 수 있는 tuple 의 타입입니다.

 하지만 아무 tuple 이나 사용할 수 있는 것이 아닙니다. 약간의 조건을 충족시키는 tuple 이어야만 합니다.

  • tuple 의 구성 요소로 source_type 이라는 typedef 를 가지고 있는 message block 들이어야 한다. ( message block 중 call 은 source_type 이 정의되어 있지 않기 때문에 사용할 수 없습니다. )
  • message block 들은 복사 생성자와 배정 연산자( = ) 의 사용을 금지( private )하고 있으므로 tuple 의 구성 요소로 포인터 타입을 사용해야 한다.

 

특징

 choice 는 여러 message block 들 중 가장 먼저 message 를 받은 message block 을 가리킵니다. 그 말은 choice 는 tuple 로 지정된 message block 들의 index 를 가지고 있다는 말입니다.

 그런데 그 index 를 저장하기 위한 변수로 내부적으로 single_assignment 를 사용합니다. 그 뜻은 single_assignment 는 단 한 번만 값을 저장할 수 있기 때문에 choice 는 가리키게 된 message block 의 index 를 변경할 수 없습니다. 즉, 일회성이라는 말이 됩니다.

 receive() 로 choice 의 message 를 받아오면 지정된 message block 들 중 하나라도 message 를 받을 때까지 기다리다가 message 를 받은 message block 이 생기면 그 message block 의 index 를 반환합니다.

 위의 특징을 응용하면 여러 message block 들 중 하나라도 message 를 받을 때까지 대기하는 기능으로 사용할 수 있습니다.

 

멤버 함수

 생성자와 소멸자를 제외한 public 인 멤버 함수 중 인터페이스로 재정의된 함수들을 제외한 함수들입니다.

인터페이스를 재정의한 멤버 함수들은 이전 글인 2010/07/10 - [Language Development/VC++ 10 Concurrency Runtime] - Asynchronous Agents Library – message block 1. ( 인터페이스 ) 를 참고하시기 바랍니다. 

 

bool has_value() const

 현재 message block 의 index 를 가지고 있는지 반환합니다. 내부적으로 single_assignmenthas_value() 를 호출합니다.

 

size_t index()

 가지고 있는 message block 의 index 를 반환합니다. 내부적으로 single_assignmentvalue() 를 호출하고 하고, 그 value() 는 receive() 를 사용하기 때문에 아직 index 를 가지고 있지 않다면 가질 때까지 대기하게 됩니다.

 

template <typename _Payload_type>
_Payload_type const & value()

 choice 가 가리키고 있는 index 의 message block 의 message 의 값을 반환합니다. message 의 타입을 명시적으로 호출해야 하기 때문에 사용하기 불편합니다.

 

헬퍼 함수

 choice 는 tuple 을 사용하기 때문에 선언이 복잡할 수 밖에 없습니다. 그래서 조금 더 쉽게 선언을 할 수 있도록 헬퍼 함수를 제공합니다.

unbounded_buffer< int > i;
unbounded_buffer< float > f;
unbounded_buffer< bool > b;

tuple< unbounded_buffer< int >*, unbounded_buffer< float >*, unbounded_buffer< bool >* > tp( &i, &f, &b );
choice< tuple< unbounded_buffer< int >*, unbounded_buffer< float >*, unbounded_buffer< bool >* > > ch( tp );

[ 코드1. 헬퍼 함수를 사용하지 않은 choice 선언 ]

 헬퍼 함수를 사용하지 않으면 템플릿 매개변수인 타입들을 모두 적어야 하기 때문에 굉장히 길어집니다. 특히나 message block 들의 타입이 굉장히 길기 때문에 더욱 불편합니다. typedef 를 사용하더라도 길기는 마찬가지 입니다.

 

unbounded_buffer< int > i;
unbounded_buffer< float > f;
unbounded_buffer< bool > b;

auto ch = make_choice( &i, &f, &b );

[ 코드2. 헬퍼 함수를 사용한 choice 선언 ]

 헬퍼 함수와 Visual studio 2010 에서 지원하는 C++0x 문법인 auto 를 사용하여 더욱 더 짧게 선언할 수 있습니다.

 

예제

 choice 의 특징을 쉽게 알아볼 수 있는 예제를 구현해보겠습니다.

 

시나리오

 두 agent 를 사용해서 어떤 agent 가 더 빨리 수행되는가를 육상에 비유하여 표현해보았습니다. choice 객체는 두 agent 중 빨리 수행한 쪽을 판별하는 역할을 합니다.

 

코드

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

class Runner
	: public agent
{
public:
	Runner( ITarget< bool >& target, const wstring& name )
		: target( target )
		, name( name ) { }

	const wstring& GetName() const
	{
		return this->name;
	}

protected:
	void run()
	{
		for( unsigned int i = 0; i < 1000; ++i )
		{
			if( 0 == i % 100 )
				wcout << this->name << L" - " << i << L"m." << endl;
		}

		wcout << this->name << L" - " << L"Finished!" << endl;

		asend( target, true );

		this->done();
	}

private:
	ITarget< bool >&	target;
	wstring				name;
};

int main()
{
	single_assignment< bool > isFinished[2];

	auto winnerRecoder = make_choice( &isFinished[0], &isFinished[1] );

	Runner runner[2] = { Runner( isFinished[0], L"Carl Lewis" ), Runner( isFinished[1], L"Usain Bolt" ) };
	runner[0].start();
	runner[1].start();

	agent* agents[2] = { &runner[0], &runner[1] };
	agent::wait_for_all( 2, agents );

	wcout << runner[ receive( winnerRecoder ) ].GetName() << L" is winner!" << endl;
}

[ 코드3. choice 를 사용한 먼저 수행된 agent 판별 예제 ]

 두 agent 에 육상 선수인 칼루이스와 우사인 볼트의 이름을 붙여보았습니다. 각 agent 는 수행이 완료되면 완료 message 를 single_assignment 에 보내고, choice 객체는 완료 message 가 먼저 도착한 message block 의 index 를 갖게 됩니다.

 그 index 를 이용해 해당 agent 의 육상 선수 이름을 출력합니다.

 이 예제의 결과는 스케쥴링에 따라 다르기 때문에 실행할 때마다 결과가 다를 수 있습니다.

 

[ 그림1. choice 를 사용한 먼저 수행된 agent 판별 예제 ]

[ 그림1. choice 를 사용한 먼저 수행된 agent 판별 예제 ]

 

마치는 글

 이번에는 tuple 을 이용하는 choice 에 대해서 알아보았습니다. Visual studio 2010 에서 지원하는 C++0x 문법을 사용하면 좀 더 쉽게 사용할 수 있습니다.

다음 글 또한 새로운 message block 에 대해서 알아보도록 하겠습니다.

Introducing Visual Studio LightSwitch! - Enjoy your development

Visual Studio 2010 2010. 8. 5. 22:30 Posted by 알 수 없는 사용자

안녕하세요. Visual C# MVP 남정현입니다. 요즈음 날씨가 매우 덥네요. :-)

이번 글은 지난번에 올렸던 2010/06/24 - [Visual Studio 2010/Visual Studio 2010] - Just for fun! / DreamSpark는 대학생 여러분을 위한 솔루션입니다. 글을 보충하고 최신 소식을 전달하기 위한 취지로 포스팅하는 글입니다. 소프트웨어 개발을 처음 시작하시는 분들께 역시 도움이 될 수 있도록 정보를 제공하기 위한 취지로 작성되었습니다. :-)

최근에 VS2010 Live!에서 소개된 Visual Studio LightSwitch라는 신 제품에 대해서 블로그 포스팅을 조촐하게 해봅니다. Visual Studio LightSwitch는 기존의 Visual Studio Express Edition과는 별도로 구분되는 제품으로 매우 신속하고 빠르게 새로운 형태의 소규모 데이터베이스 시스템과 통합하거나, 기존의 WCF RIA Service, SQL Server Database 등과 상호 작용하면서 C/R/U/D (Create/Read/Update/Delete) 및 검색 기능, Office 연동 기능을 지원하는 Rich Application의 생산을 가능하게 해주는 개발 도구입니다. Visual Studio LightSwitch는 2010년 8월 23일 이후부터 베타 버전으로 처음 http://www.microsoft.com/visualstudio/en-us/lightswitch 에서 공개될 예정에 있습니다.

가장 많은 웹 사이트와 가장 많은 도서에서 보여주는 C/R/U/D 기반의 응용프로그램들의 패턴을 Visual Studio LightSwitch는 최신의 UI 기술을 사용하여 그 어떤 도구들보다도 더 쉽게 프로그램이 제작될 수 있도록 도와줍니다. 단순히 Grid Control에서 데이터를 추가, 편집, 삭제할 수 있는 UI가 아니라, 여러분이 원하는 화면의 디자인을 미리 제공되는 데이터 바인딩과 함께 손쉽게 프로그래밍할 수 있도록 돕습니다.

위의 그림에서 보시는것과 같이, Visual Studio LightSwitch는 데이터 가공 방법과 범주에 따라 일반적으로 결정되는 화면의 패턴을 템플릿으로 제공하고 있습니다. 대부분의 경우, 이러한 요구 사항과 부합하는 화면들 내에서 선택이 가능하므로 단순한 데이터 조회 및 검색 화면에 들어가는 작업 소요 기간을 획기적으로 단축시킬 수 있습니다. 뿐만 아니라, 데이터 모델링에서는 단순한 시스템 데이터 형식 뿐만 아니라, 유효성 검사를 염두에 둔 모델링과 UI 연동이 가능하여 세세한 검사를 위한 코드 작성은 하지 않아도 되며, 기본으로 제공되는 Grid Control의 경우 필터링, 열의 재 배치, 행 정렬, 검색, 페이징이 Built-in 기능입니다. 그리고, 요즈음 Microsoft Application UI의 핵심적인 Trademark인 Ribbon Interface도 빠지지 않습니다. :-)

기존에 Windows Forms, Windows Presentation Foundation 기반의 개발 도구를 이용하여 Desktop 및 Office Application을 개발하는 동안 고객으로부터 가장 많이 받게 되는 요구 사항 중 하나인 Excel Export 기능이 Visual Studio LightSwitch를 기반으로 만들어지는 응용프로그램에서는 미리 제공되는 Built-in 기능 중의 하나입니다. 더 이상 Excel 상호운용성 코드나 Primary Interop Assembly를 찾아서 방황하지 않아도 됩니다. :-)

그리고 Visual Studio LightSwitch는 앞으로도 다양한 확장 패키지를 지원해 나갈 수 있으며, 이러한 확장 패키지를 기반으로하여 가까이는 시각 상의 심미적인 효과를 위한 확장, 하드웨어와의 연동 기능 뿐만 아니라 향후에는 Cloud Computing을 위한 응용프로그램 디자인까지도 염두에 둘 수 있습니다.

Visual Studio LightSwitch는 System Integration 개발 부문, Business Intelligence 확장 부문 등 기존에 특별한 기술적 이득 없이 기계적인 반복 작업을 바탕으로 하는 대부분의 소프트웨어 개발 작업에서 더 큰 진가를 발휘할 수 있을 것으로 예상됩니다. 향후에는 Chart, Graph, HTML Editing Control, Built-in Web Browser와 같이 일반적인 Desktop 및 Office Application에서 많이 사용되는 기능에 대한 Extension Pack이 추가될 것도 염두에 두어본다면 반복적인 작업으로부터 많은 일손을 덜어내어줄 수 있는 멋진 도구가 될 수 있을 것 같이 미래가 기대됩니다.

Cloud Computing과의 연계성에 있어서도 Visual Studio LightSwitch의 역할은 매우 클 것으로 보이며, 향후에 Cloud Computing과의 연계 기능에 대해서도 본격적으로 Topic을 다룰 수 있을 때, 상세하게 정보를 전달할 수 있도록 하겠습니다.

사이트 바로가기 및 키노트 동영상 구경하러 가기: http://www.microsoft.com/visualstudio/en-us/lightswitch

오늘도 좋은 하루 되세요. 감사합니다. :-)

WPF 리본 컨트롤 RTW 출시

WPF 2010. 8. 5. 12:00 Posted by 알 수 없는 사용자

오피스 2007 부터 적용되어온 리본 UI에 대해서는 이제 모르는 분이 없을 정도로 많이 대중화 되었습니다.

윈도우 7에서는 그림판, 워드패드 등의 기본 프로그램에도 리본 UI가 적용되어서 사용자에게 편리함을 주고 있습니다.

이러한 리본 UI를 적용해서 개발을 하고 싶을 때는 MFC를 이용한 방법이 있습니다.

  

MFC 프로젝트를 생성하고 마법사에서 오피스 스타일의 프로젝트를 선택하면 리본 UI가 적용되어 사용 할 수 있습니다.

이렇게 프로젝트를 생성하고 실행시켜보면 리본 UI가 적용된 것을 확인 할 수 있습니다.

MFC에서는 이렇게 기본적으로 지원을 해주었지만 또 다른 윈도우 어플리케이션 개발 방법인 WPF에서는 지금까지 정식으로 지원을 하지 못했습니다.

기본 컨트롤에서는 제공하지 못하고 따로 컨트롤이 제작되어 지원을 준비하고 있었는데 이번에 RTW 버전이 출시되어 WPF 기반의 응용 프로그램 개발에서 리본 UI를 쉽게 사용할 수 있게 되었습니다.

RTW 출시 이전 단계에서는 .dll 파일을 수동으로 참조해서 번거롭게 작업을 해야 했었는데 이번 RTW부터 많은 편의성이 향상되었습니다.

첫째,  .MSI 인스톨러를 지원하여 번거로운 작업을 하지 않아도 쉬운 설치로 사용 할 수 있게 개선되었습니다.

둘째, WPF 개발에 많이 사용되는 MVVM 패턴을 지원하여 최신의 개발 방법을 지원하고 있습니다.

셋째, WPF 개발을 할 때 Expression Blend 사용이 거의 필수적인데 Blend에서 디자인 타임에서 리본 UI를 지원하여 편리한 UI 개발을 할 수 있게 되었습니다.

넷째, Visual Studio와 Expression Blend에서 프로젝트 템플릿을 지원하여 리본 UI 스타일 개발을 쉽고 빠르게 해줍니다.

 

   1: <ribbon:RibbonWindow x:Class="WpfRibbonApplication1.MainWindow"
   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:         xmlns:ribbon="clr-namespace:Microsoft.Windows.Controls.Ribbon;assembly=RibbonControlsLibrary"
   5:         Title="MainWindow"
   6:         x:Name="RibbonWindow"
   7:         Width="640" Height="480">

프로젝트를 생성하고 xaml 코드를 살펴보면 ribbon 네임스페이스에 관련 컨트롤이 추가된 것을 볼 수 있습니다.

리본 UI 컨트롤은 ribbon 접두어를 사용하면 해당 컨트롤이 표시되게 됩니다.

그리고 일반 WPF 프로젝트는 루트 엘리먼트가 Window인데 리본 UI가 적용되면 RibbonWindow가 사용되는 것을 알 수 있습니다.

   1: <ribbon:Ribbon x:Name="Ribbon">
   2:     <ribbon:Ribbon.ApplicationMenu>
   3:         <ribbon:RibbonApplicationMenu SmallImageSource="Images\SmallIcon.png">
   4:             <ribbon:RibbonApplicationMenuItem Header="Hello _Ribbon"
   5:                                               x:Name="MenuItem1"
   6:                                               ImageSource="Images\LargeIcon.png"/>
   7:         </ribbon:RibbonApplicationMenu>
   8:     </ribbon:Ribbon.ApplicationMenu>
   9:     <ribbon:RibbonTab x:Name="HomeTab" 
  10:                       Header="Home">
  11:         <ribbon:RibbonGroup x:Name="Group1" 
  12:                             Header="Group1">
  13:             <ribbon:RibbonButton x:Name="Button1"
  14:                                  LargeImageSource="Images\LargeIcon.png"
  15:                                  Label="Button1" />
  16:  
  17:             <ribbon:RibbonButton x:Name="Button2"
  18:                                  SmallImageSource="Images\SmallIcon.png"
  19:                                  Label="Button2" /> 
  20:         </ribbon:RibbonGroup>
  21:         
  22:     </ribbon:RibbonTab>
  23: </ribbon:Ribbon> 

실제 UI부분의 코드를 보면 이전에는 일일이 직접 코딩을 해줘야 했던 부분이 자동적으로 가능하게 되어 정말 편리하게 변했습니다. 이제 리본 컨트롤을 사용하여 WPF 환경에서도 편리하게 UI 개발을 할 수 있게 되었습니다.

이번에는 간단한 소개 정도로 마치고 다음부터는 WPF에서 리본 컨트롤을 사용하는 세부적인 내용을 차근차근 알아가도록 하겠습니다.

※ WPF Ribbon 컨트롤 다운로드

[STL] 3. unique_ptr (2/2)

C++0x 2010. 8. 4. 12:00 Posted by 알 수 없는 사용자

안녕하세요. 지난 글에 이어 unique_ptr에 대해서 소개드리겠습니다.

auto_ptr의 완벽한 대체자 혹은 그 이상 

앞선 글에서 설명드렸듯이 unique_ptr은 auto_ptr이 deprecation으로 결정되면서 그 대체자 로서 제안되게 되었습니다. 필연적으로 auto_ptr의 모든 기능을 포함하고 있으며, 문법 또한 같습니다. 단, auto_ptr이 deprecation으로 결정되게 된 원인이었던 복사 문법을 제외됩니다.
아래 예제 코드는 auto_ptr의 기본적인 동작이 unique_ptr에서도 구현되어 있음을 보여줍니다.

  1     // 기본 생성자
  2     auto_ptr<int> ap;
  3     unique_ptr<int> up;
  4     // 포인터 생성자
  5     auto_ptr<int> autoPtr(new int);
  6     unique_ptr<int> uniquePtr(new int);
  7     // 역참조
  8     *autoPtr = 1;
  9     *uniquePtr = 2;
 10     // 초기화
 11     autoPtr.reset();
 12     uniquePtr.reset();

하지만 아래와 같은 문법에서 차이가 생깁니다.


  1 	auto_ptr<int> ap1(new int);
  2 	auto_ptr<int> ap2 = ap1; // OK
  3 	unique_ptr<int> up1(new int);
  4 	unique_ptr<int> up2(up1); // 컴파일 에러
  5 	unique_ptr<int> up2 = up1; // 컴파일 에러

 이 코드를 실행시켜보면, 4번째 줄에서 lvalue로 부터의 복사 생성자에서 에러가 발생합니다. 또한 5번째 줄에서 unique_ptr에 대한 복사 문법에 대한 컴파일 에러가 발생하는 것을 볼 수 있을 겁니다. 위의 코드를 올바르게 수정하면 다음과 같습니다.

  1 	unique_ptr<int> up1(new int);
  2 	unique_ptr<int> up2 = move(up1); // OK

삭제자(deleter) 지정하기

 특정한 삭제자 함수를 지정하지 않고 unique_ptr을 선언하는 경우에, unique_ptr은 객체가 new로 생성되었을 것을 가정하고 delete를 호출합니다. 하지만 unique_ptr을 선언할 때에 삭제자 함수를 특정함으로써 이를 바꿀 수 있습니다. 예를 들어, malloc으로 선언된 객체는 delete가 아닌 free로 삭제해야 합니다. free를 삭제자로 지정하는 방법은 아래와 같습니다.

  1 	int* intPtr = (int*)malloc(sizeof(int));
  2 	unique_ptr<int, void (*)(void*)> deleterTest(intPtr, std::free);

배열 객체 다루기

 unique_ptr에서는 배열 객체를 다룰 수 있도록 인터페이스를 추가하였습니다. 다음의 코드를 먼저 보겠습니다.


  1 	unique_ptr <int[]> intarr (new int[4]); //array version of unique_ptr
  2 	intarr[0]=10;
  3 	intarr[1]=20;
  4 	intarr[2]=30;
  5 	intarr[3]=40;

 intarr 객체는 int의 배열을 가지는 unique_ptr입니다. 하지만 [] 연산자를 구현하여 저장된 배열에 직접 접근할 수 있는 인터페이스를 구현되어 있습니다. 이 경우에, 선언부(줄 1)의 template에 들어간 []가 배열 객체를 다루는 unique_ptr임을 알려주게 됩니다.
 또한 위의 삭제자 지정하기에서 기본 삭제자가 delete가 된다고 했는데, 배열 객체를 다루는 unique_ptr은 delete []가 기본 삭제자가 됩니다.

참고 :
http://msdn.microsoft.com/en-us/library/ee410601.aspx
http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=401
http://www.devx.com/cplus/10MinuteSolution/39071/1954


[ALM-Test] 4. 테스터(SDET) 의 역할

Agile Development 2010. 8. 4. 08:30 Posted by POWERUMC

샘플 프로그램으로 시작해보자고!!

아주 간단한 Windows Forms 어플리케이션을 작성해 보았습니다. 실제로 실무에서는 이렇게 간단한 프로그램을 만드는 개발자도 없겠지만, 아주 간단한 것 부터 시작하여 테스트의 필요성과 테스터의 역할이 얼마나 중요한지 알 수 있는 시간이 되길 바랍니다.    

아래의 윈폼 어플리케이션은 숫자A와 숫자B 를 더하여 결과를 보여주는 프로그램입니다. 아래와 같이 간단하게 디자인을 하였습니다.

   

소스 코드는 더할나위 없이 간단합니다. 특별한 설명은 하지 않겠습니다.

이 프로그램으로 1과 2 값을 입력하면 당연히 3이라는 결과가 출력되어야 합니다 아래와 같이 말이죠.

프로그램이 완벽하지요?? 정말일까요?? 특히 프로그램을 개발하는 개발자의 시각은 테스터와 매우 다릅니다. 일반적으로 개발자에게 스팩(Spec)을 구현하는 명세서가 전달이 됩니다. 아래는 간단한 구현 명세서 입니다. (단, 화면 명세서가 아닙니다)

   

구현 명세서

제목

두 숫자를 입력 받고 합을 구하는 기능

기능

1. 테스트 박스 1에 숫자를 입력할 수 있다.
2. 텍스트 박스 2에 숫자를 입력할 수 있다.
3. 새로운 텍스트 박스에 텍스트 박스 1과 텍스트 박스 2의 합을 출력한다.

   

테스터(SDET) 의 힘이 발휘되는 순간!

개발자는 위의 명세서를 보고 두 숫자를 입력 받아 출력하는 프로그램을 아래와 같이 구현합니다.

사실상 동작에 아무런 문제점이 없지만, 테스터의 시각에서는 전혀 다릅니다. (물론 구현 명세서가 부정확하긴 합니다) 즉 숫자의 입력 범위가 매우 불확실합니다. 정수만 입력되는 Integer 값인지, 32Bit 부동 소수점을 표현하는 Float 인지 아무런 정보가 없기 때문이겠지요.

테스터는 바로 버그를 발생시킬 수 있는 프로그램의 코드나 사용성에 대해 즉각 테스트를 수행합니다. 개발 소스 코드가 제공이 된다면 해결 방안까지 제시할 수 있겠지만, 그렇지 않는다면 오로지 테스터의 경험과 실력에 의존할 수 밖에 없습니다. 그렇기 때문에 SDET 는 개발자(SDE) 와 동등한 기술 능력을 갖추거나 그 이상의 능력을 필요로 하게 됩니다.

위의 간단한 프로그램을 테스트하기 위해서는 SDET 는 테스트 케이스(Test Case) 를 작성합니다. 테스트 케이스(Test Case) 를 작성하는 목적은 기능이 올바르게 동작한다는 가정하에 잠재적인 버그(Bug) 를 유도하는 작업입니다. 그리고 테스트 케이스에 대해서는 차후에 자세히 다루도록 하겠습니다.

만약, SDET 가 아래와 같은 값을 입력했다면 어떻게 될까요? 즉시 프로그램은 오류를 뱉고 말 것입니다. 아래와 같이 말이죠.

   

물론, SDET 는 소프트웨어 버전이 매번 릴리즈 될 때마다 위와 같이 무식하게 수동 테스트(Manual Test) 를 수행하지 않을 것입니다. 이 수동 테스트를 개선하기 위해 자동화 테스트를 수행할 것이며, 이 부분 또한 차후에 설명할 예정입니다. 간단히 설명하자면 반복적으로 테스트를 수행한 가치나 목적이 있다는 자동화 테스트 대상이 될 수 있습니다.

위의 무식한 테스트 과정을 자동화 하기 위해 단위 테스트(Unit Test) 를 사용하여 아래와 같이 작성할 수 있습니다. (아래의 UI 테스트와 관련된 부분이며 마찬가지로 차후에 상세히 설명할 예정입니다, 단, 자동화 테스트 이외의 비기능 테스트가 무식하다는 것은 아닙니다.)

   

이 테스트는 아쉽지만, 무자비하게 오류를 발생합니다. 프로그램 소스 코드의 int.Parse 는 정수 값만 변환 가능하므로, 소수점이 포함된 "1.1" 값은 FormatException 을 발생하게 됩니다.

이런 결함에 대한 버그 리포트를 개발자에게 할당하게 되면, 아마도 아래와 같이 프로그램의 소스 코드가 int.Parse 에서 float.Parse 로 변경될 수 도 있겠지요.

위와 같이 코드를 수정하면 프로그램을 정상적으로 동적을 합니다. 아래와 같이 말이죠^^

   

정말 버그가 해결된 것일까?

FormatException 에 대해 SDET 가 테스트한 테스트 코드를 통해 개발자(SDE) 가 코드를 수정하였습니다. 그리고 원하는 테스트는 다행스럽게도 통과(Pass) 했습니다.

   

모든 소프트웨어는 그 품질을 향상하기 위해 테스트를 진행합니다. 하지만, 버그가 없는 소프트웨어란 있을 수가 없습니다. 위와 같은 다행스럽게 SDET 가 버그나 결함을 발견하였더라도 앞으로 발견될 잠재적인 버그는 언제나 소프트웨어가 내포하고 있습니다. 즉, 완벽한 소프트웨어는 없다는 것이지요. SDET 는 이러한 버그와 잠재적인 버그를 효과적으로 발견 해야하는 매우 중대한 업무를 수행하는 직군입니다.

현대의 소프트웨어는 인터넷의 발달로, 언제나 제작사에게 피드백을 건의하고 버그를 건의하는 시스템이 구축이 되어 있습니다. 그 중 Microsoft 의 대표적인 것이 아래와 같습니다.

  • CEIP(Customer Experience Improvement Program)

    고객 또는 사용자의 동의하에 고객 경험 개선 프로그램에 참여할 수 있습니다. 이 정보는 고객의 피드백을 수집하기 위한 정보로 활용이 됩니다.

  • WER(Windows Error Reporting)

    Microsoft Windows 제품은 실제 매우 광범위한 영역과 자원과 비용이 할당된 제품입니다. 이 제품에서 발생하는 오류나 버그를 수집하기 위해서 WER 프로그램이 윈도우 내부에 탑재가 되어 있습니다. 이 정보를 기반으로 윈도우의 차기 버전이나 패치 버전에서 우선 순위로 할당되는 중요한 정보로 활용이 됩니다.

  • CER(Corporate Error Reporting)

    일반 고객이 아닌 기업 대상으로 소프트웨어를 사용한다면, 기업 내부에서는 오류 정보가 Microsoft 로 전송되는 것을 원치 않을 경우가 많습니다. 왜냐하면 기업의 시스템의 배치, 소프트웨어의 활동 등이 기밀 정보가 될 수 있고, 매우 조심스러운 부분이기 때문입니다. CER 은 Microsoft 로 정보가 전송되지 않고 기업 내부에서 관찰할 수 있는 프로그램입니다.

  • 스마일 전송 프로그램 : 추후에 설명할 예정입니다. 간략하게 고객이나 사용자의 감성을 데이터베이스화 하고자 하는 Microsoft 사용성 향상을 위한 프로그램 입니다.

   

개발자 보다 더 똑똑한 테스터!

위의 int.Parse 를 float.Parse 로 바꿈으로써 버그가 해결되었다고 볼 수 있습니다. 하지만 진정으로 버그가 해결되었다고 볼 수 없기도 합니다. 왜냐하면 테스트 케이스를 만족하지만 다양한 부류 집단인 '베타 테스트'에서는 전혀 다른 결과가 나올 수 도 있으니까요. 그리고 테스터는 바로 이러한 버그의 발생을 아키텍처/코드/기능적인 부분을 고려하여 버그를 유도할 수 있습니다.

만약, 유능한 SDET 라면 Float 으로 인한 결과 값 버그를 아래와 같은 테스트 케이스로 작성할 수 있습니다.

   

왜 결과 값이 당연이 1이 되어야 하지만, 1.000054 라는 황당한 값이 나왔을까요? 바로 컴퓨터의 내부 연산이 2진수로 이루어 진다는 것을 모르는 개발자나 테스트는 위와 같은 오류를 예감하지 못할 것입니다. 저 또한 제니퍼소프트의 정성태 과장님을 통해 이러한 문제를 처음 알게 되었으니까요.

정성태 과장님의 설명에 의하면,

"2진수의 소수점 표현들이 자리수에 따라서 1/2, 1/4, 1/8, 1/16, 1/32, 1/64 와 같은 식으로 표현이 되는데, 십진수 0.5 는 다행히 정확하게 1/2 에 맞아 떨어지지만, 십진수 0.6 은... 겨우 0.1 차이일 뿐인데 0.10011001100110011001 와 같은 2진수로 되어 버립니다. 근데 이것도 근사치일 뿐이지 1001 패턴이 계속 무한 반복 되어버리죠. 정밀도를 높이면 0.6 은 0.010011001100110011001100110011001100110011001100110011 로 표현이 되어버립니다."
더 자세한 내용은 http://k.daum.net/qna/view.html?qid=3wykn&aid=3xIOa 참고하세요.

만약, 이러한 문제가 회계 업무나 우주 공학에 적용이 된다면, 수억, 수십, 수 조원이 투입된 프로젝트에서 불에 뻔하듯 우주선이 폭발하거나 궤도를 이탈할 수 도 있는 심각한 문제를 발생할 수 있습니다. 실제로 이러한 문제로 일부 고객은 손해 금액에 대하여 마이크로소프트를 대상으로 법적인 대응을 한 사례도 있습니다.

Microsoft 가 이 문제를 몰라서 그랬을까요? 그렇지는 않습니다. IEEE(Institute of Electrical and Electronics Engineers) 표준으로 사용되었던 C 언어와 Pascal 언어간의 원활한 데이터 전달을 위해 C# 의 Float 도 같은 방식의 연산이 사용된 것입니다. 더 자세한 내용은 http://support.microsoft.com/kb/42980/ko 를 참고하세요.

   

그럼 어떻게 해결하나?

위와 같이 int.Parse 는 결함을 유발하기 매우 쉽지만, float.Parse 의 경우 더 깊은 이해가 필요합니다. 만약 테스트 케이스(Test Case) 가 이것을 유추하지 못한다면 얼마 되지 않아 소송에 휘말릴 수 있는 충분한 근거가 되겠지요. 만약 구현 명세서를 간파한 SDET 라면 이 수치에 대한 근거를 요구할 것이며, 테스트 과정에서 IEEE 745에 대한 테스트를 수행했을 테니까요.

현명한 테스터라면 float.Parse 의 타입을 Decimal 로 변경하기를 권장할 것입니다. Decimal 은 부동 소수점의 오류나 반올림에 대한 이슈를 해결할 수 있는 구조체(Struct) 입니다. 즉, 아래와 같은 구현이 회계 업무에 버그가 없는 코드가 될 것입니다.

   

테스터의 역할

테스터(SDET) 는 위의 간단한 예시와 같이 다양한 테스트를 진행하는 역할을 수행합니다. 그리고 그 역할이 매우 단순하고 초보 개발자가 수행할 수 있는 단순한 작업이 아님을 알 수 있습니다.

테스트 케이스(Test Case) 를 만들고, 다양한 테스트 시나리오를 계획하는 테스트 계획(Test Planning) 을 함으로서 소프트웨어의 잠재적인 버그를 하나씩 제거하는 매우 막중한 임무를 수행하는 직군입니다. 그리고 SDET 의 역할이 개발자(SDE) 의 코드적인 목적을 정확하게 간파하지 못하거나, 제품 전체적인 아키텍처, 프로세스를 이해하지 못한다면 더욱 더 많은 문제에 봉착하게 됩니다.

본 회에서 SDET 가 가지는 역할을 강조하기 위한 것이며, 추후에 테스트를 수행하기 위한 공학적인 기법에 대해서 차근 차근 알아보도록 할 예정입니다.

테스터 역할 배경에 대한 오해

이전 단원에서 "왜 단위 테스트를 해야 하는가" 에 대해 이야기를 해 보았습니다.

[ALM-Test] 왜 단위 테스트를 해야 하는가? [1]
[ALM-Test] 왜 단위 테스트를 해야 하는가? [2]

그리고 테스터의 역할 정의를 아래와 같이 하였습니다.

  • 업무 도메인의 이해
  • 테스트 도구 사용
  • 전반적인 테스트 시스템 인프라 와 운영체제(OS) 의 이해

하지만 필자도 테스트에 대한 공부를 시작하면서 몇 가지 오해를 하였던 것이 사실이었습니다. 필자는 대부분 SI, 컨설팅, 솔루션 개발을 하였고, 개발자 대신에 테스트를 해 주는 역할이 테스트라는 엄청난 함정에 빠졌던 것입니다. 자칫 함정에 빠진 이유는 SI 개발과 SM 이라는 유지 보수 업무의 장기 선상으로만 빗댄 것이었죠. 더불어 테스터의 역할은 업무에 따라서 충분히 달라질 수 있다는 것을 알게 되었습니다.

아마 대부분의 IT 개발직에 몸 담으신 분들이라면 저와의 생각과 크게 다르지 않을 것이라고 생각합니다. 일반적으로 작은 조직에서는 테스터(Tester) 직무만 보유하지만, 특히 대기업, 솔루션 업체나 게임 업계에서는 QA(Quality Assurance-품질 보증) 팀을 자체적으로 보유하는 것으로 알고 있습니다.

QA(Quality Assurance) 팀은 주로 소프트웨어의 버그/결함을 발견하고 개선하기 위한 업무가 수행됩니다. 그렇다면 이 QA 팀의 테스터는 기술자인가? 아니면 그냥 그저 그런 테스터인가? 기술자라면 어느 정도의 기술 요구사항이 필요한지, 그냥 테스터라면 어떤 단순 업무가 수행되는지, 궁금하지 않으십니까?

   

소프트웨어 1위 업계는 어떻게 구성이 되었는가?

단연, 소프트웨어 1위 업계는 Microsoft 입니다. Microsoft 는 초기 솔루션(제품) 이 개발되기 시작하면서(당시 1979년 인턴부터 시작하여) 테스터를 모집하여 참여했다고 합니다. 초기에는 STE(Software Test Engineer) 와 SDE/T(Software Development Engineer in Test) 라는 두 개의 직함이 있었는데, STE와 SDE/T 의 직무는 사실상 약간의 차이를 보였습니다.

STE 의 직무는 테스트 플래닝(Test Planning), 핵심 테스트, 테스트 자동화와 같은 테스트를 리드하는 직무였고, SDE/T 는 테스트 코드 작성, 코드 리뷰, 테스팅 툴 개발 등 실제로 일선에서 테스팅을 수행하는 직무였습니다. 2002년도 부터 SDE/T 직함을 없애려고 하였고, 2005년도에 SDE/T 가 SDET 로 명명되었습니다. 실제로 당시 시대적 배경으로 암시해보면 STE 의 비중보다 SDET 의 비중과 직원 증가율이 높았습니다. 왜냐하면 STE 는 주로 자동화 테스팅과 관련된 직무를 수행했는데, 사실상 시대적인 기술 한계 등으로 많은 활성화가 되지 않았던 것 같습니다.

결국 SDET 로 테스트 관련 직군이 모두 통합이 되었지만, SDET 도 각 직급이 존재하였습니다.

  • SDET1 : 테스트 케이스를 구현
  • SDET2 : SDET1 과 큰 변화는 없지만, 영향력이 고객과 직접적으로 의견을 교환할 수 있습니다.
  • Senior Software Development in Test : 테스트와 관련된 아키텍처가 가능하고, 문제를 해결할 수 있는 능력
  • Principal Senior Software Development Engineer in Test : 조직을 관리하고 테스트 방법론, 테스트 기술 혁신을 제시
  • Partner Software Development Engineer in Test : 조직 및 제품 전반을 이해하고 기술 혁신을 선도

특히, 현재 Microsoft 의 SDET 의 인원은 9,000명 넘는 인원이 직무를 수행 중이며, Microsoft Office 2007 제품에는 3,000 여명의 SDET 가 투입되었다고 합니다. 아래는 2008년도 대비 전세계 80,000여명이 넘는 직원 중의 각 분야별로 인원입니다. 특히 '제품 개발 및 지원' 은 SDE/SDET/Op(Operations)/IPE(International Project Engineering-현지화) 등등 을 모두 합한 것으로 9,000명의 SDET 라는 것은 거의 SDE 보다 SDET 가 더 많다고 합니다.

Op(Operations)
IT 분야를 관리하고, 네트워크, 서버를 관리 및 유지보수 하는 분야입니다. 즉 Microsoft 의 인프라를 주로 담당하며, 효과적으로 비용을 절감하거나 품질 좋은 서비스를 제공하기 위한 분야입니다.

IPE(International Project Engineer)
Microsoft 제품은 대부분 다국어 제품으로 출시하는데 각 나라의 특성에 맞게 변형하는 역할 (참고 : 국제화(i18n, internationalization): (1) 그거 번역하는거 아냐?)

   

SDET 의 자격 요건!!

SDET 는 적어도 소프트웨어 업계 1인자인 Microsoft 에서는 매우 중요한 직무임이 틀림이 없습니다. 그것이 우리나라에서는 SDET 의 역할이 그리 중요하지 않거나, 빅3 SI 업계에서는 공학적인 측면만을 강조하고 있다는 느낌이 드는 것도 사실입니다. (적어도 .NET 분야에서는 말이죠^^;)  

SDET 는 소프트웨어나 제품이 출시하기 위해 매우 막중한 임무를 수행하는 직무입니다. 한 명의 SDET 가 최선을 다하지 못한다면 버그/결함이 고스란히 고객에게 영향을 미치게 될 것이 분명합니다. SDET 는 소위 우리나라의 '베타 테스터'와 같이 먼저 사용해보고 평가나 버그를 제작사에게 알려주는 것이 아닌, 버그/결함을 제거하는 사명을 띠는 매우 크리티컬한 분야입니다.

버그와 결함에 대해서는 다음 회차에 명확히 할 예정입니다.

그렇다면, SDET 는 어떤 자격 요건을 갖추어야 할까요?

  1. C# 과 같은 객체지향적 언어 또는 필요한 언어적인 기술을 완벽히 갖추어야 한다.
  2. 특정 버그/결함을 디버깅하거나 리팩토링/개선이 가능해야 한다.
  3. 제품에 대한 기술적인 배경 지식을 갖추어야 한다.
  4. 제품이나 도메인에 대한 전체적인 프로세스/아키텍처를 이해할 수 있어야 한다.
  5. 테스트 케이스를 계획하고 테스트 공학적인 지식을 갖추어야 한다.
  6. 다양한 방법/기법으로 테스트 케이스를 작성할 수 있어야 한다.
  7. 테스트 케이스를 작성하거나 테스트 자동화에 대한 지식이 있어야 한다.
  8. 개발, 기획, 운영 팀 등과 커뮤니케이션을 원활하게 할 수 있어야 한다.

만약, 이 중에서 5개 이상 스스로 가능하다고 판단되지 않는다면, 필자와 함께 "[ALM-Test]" 연재를 꾸준히 구독하길 바랍니다. 그리고 5개 이상 수행이 불가능하다면 이미 SDET 가 아닌, "베타 테스터"라고 자칭하셔야 합니다.

필자 또한 테스팅 분야에 관심이 많았지만, 본격적으로 막 뛰어는 풋내기나 다름이 없기 때문에, 꾸준히 저와 함께 정보를 공유하거나 경험이 많은 분들의 가르침이 필요하다고 생각을 합니다.^^

   

왜 테스팅 분야에 뛰어 들었나요?

저는 약 1.5년 전부터 애자일(Agile) 방법론과 프로세스에 매우 관심을 갖게 되었습니다. 그리고 결국 깨닫게 된 것은 애자일을 경험하면서 우리나리 현실과 부합되지 않는다는 것을 몸소 느꼈습니다. 물론 제가 잘못된 애자일을 수행했을 가능성도 없지 않겠지요. 애자일은 점진적이고 반복적인 프로세스로 단지 프로세스 뿐만 아니라 사람 그리고 인격을 강조한 기만한 방법론이지만, 사실상 우리나라에서는 조직의 작은 팀 내부에서만 불화음 없이 수행 가능하던 그저 그런 것이었습니다. (참고:애자일에 대한 고찰)

어떠한 고객도 관심 없는 XP(eXtreme Programming), Scrum 를 수행하면서 자기 만족 수준에 불과했으니까요. 하지만, 일부 애자일 프로세스가 권장하는 방법을 버리고, 개선/조합하니 왠걸??? ^^

어떤 미친 기업에서 SDE:SDET=1:1 비율로 배치가 가능하며, 아무리 좋은 사례를 가져와도 실패하고 맙니다. 뿌와쨔쨔님의 영어이야기 중 "한국 미국, 자기소개의 차이점?" 을 보면서 또 한번 느낀 것이, 조직 단위 뿐만 아니라, 개인 단위로 이미 상하관계가 형성된다는 것입니다. (물론 단지 이런 이유 뿐만은 아닙니다)

Visual Studio 와 .NET 은 이미 테스팅 분야에 혁신을 진행하고 있고, 우리나라에서 소외되는 테스팅 분야에서 많은 할 일이 남아 있을 것 같아요. 작은 경험이나마 공유하고 도움이 되는 실전 테스팅 기법에 대해서 차근 차근 공부하면서 여러분들에게 알려드릴 계획입니다. 그리고 함께 생각을 정리하는 시간이 되길 바랍니다.

Asynchronous Agents Library
– message block 5. ( transformer )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 이번 글에서는 transformer 라는 message block 에 대해서 알아봅니다.

 transformer 는 지난 글에서 알아본 call 과 유사하나 좀 더 유연합니다. call 과 비교해서 보시고, calltransformer 를 언제 사용해야 할지 구분해 보시기 바랍니다.

 

transformer< _Input, _Output >

 transformercall 과 마찬가지로 지정된 함수, 함수 객체를 수행하는 역할을 합니다. 하지만 call 과는 확실히 다릅니다. 이름에서도 알 수 있듯이 message 를 변환하는 기능을 가지고 있습니다. 이 기능이 call 과 어떻게 다른지 알아보겠습니다.

 

특징

 transformer 의 특징 중 첫째는 결과를 반환한다는 것입니다. 변환하는 기능을 가지고 있기 때문에 변환된 값을 내보내 주어야 하기 때문입니다. 템플릿 매개변수를 보고 이미 눈치채신 분도 있겠지만, transformer 를 선언할 때 입력되는 매개변수의 타입과 반환 타입을 명시해주어야 합니다.

 이로 인해 내부적으로 call 과 다른 점은 반환 값을 보관하는 큐( queue ) 를 포함하고 있습니다. call 은 작업들을 순차적으로 처리하기 위한 작업 큐를 가지고 있고, transformer 는 작업 큐 뿐만 아니라 반환 값을 보관하는 큐도 가지고 있습니다.

 반환 값을 보관하는 큐의 값을 꺼내오기 위해서 receive() 나 try_receive() 를 사용할 수 있습니다. 이 특징은 message 들을 전달, 보관하는 unbounded_buffer 와 비슷합니다. 하지만 unbounded_buffer 처럼 사용하시면 안됩니다.

 그 이유는 transformer 는 두 가지 기능, 변환 함수를 수행하는 기능과 변환된 값을 내보내는 기능을 가지고 있는데 이 둘을 동시에 처리할 수 없기 때문입니다. 변환 함수를 수행하는 중인 transformer 에서 변환된 값( 반환 값 )을 얻어내려 시도해도 변환 함수를 수행 중일 때에는 변환된 값을 내어주지 않아 원하는 시기에 값을 얻어낼 수 없습니다.

 원하는 시기에 값을 제대로 얻기 위해서는 변환된 값을 unbounded_bufferoverwrite_buffer 등과 같이 message 를 보관할 수 있는 message block 에 전달한 후, 그 message block 에서 값을 얻어와야 합니다.

 두 번째 특징은 다른 message block 들과 연결하여 변환된 반환 값을 스스로 전달할 수 있다는 것입니다. ISource 인터페이스의 link_target() 을 사용하는데, 이 기능을 사용하여 절차적인 작업들을 잘게 나누어 순차적으로 처리할 수 있습니다. 이러한 메커니즘을 파이프라인( pipeline ) 이라고 합니다.

 일반적인 C++ 프로그램에서도 파이프라인 개념은 이미 많이 사용되고 있습니다.

 FunctionA( FunctionB( FunctionC() ) ) 와 같이 진행되는 처리도 파이프라인으로 볼 수 있습니다. 하지만 transformer 를 이용한 파이프라인은 이와 차이점이 있습니다. 바로 비 동기 처리가 가능하다는 것입니다.

 하나의 파이프라인의 작업이 끝날 때까지 대기하지 않기 때문에, 여러 파이프라인의 작업이 동시에 수행할 될 수 있습니다.

 잠시 후, 이런 파이프라인을 이용하는 예제를 알아보도록 하겠습니다.

 

선언

template < class _Output, class _Input >
class transformer

[ 코드1. transformer 의 선언 ]

 transformercall 처럼 함수 타입을 지정할 수 없습니다.

 transformer 는 std::tr1::function<_Output(_Input const&)> 로 함수 타입이 고정되어 있습니다.

 

예제

 transformer 를 이용하여 파이프라인을 구성하여 처리하는 예제를 구현해보도록 하겠습니다.

 

시나리오

 문자열을 꾸미는 작업을 각 단계별로 나누어 파이프라인을 구성하고, 이것을 비 동기로 처리하는 시나리오입니다.

 

코드

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

// 문자열을 꾸며주는 helper class.
class DecorationHelper
{
public:
	static wstring Boiler( wstring food )
	{		
		wcout << L"boiling.." << endl;
		Concurrency::wait( 1000 );		
		return L"boiled " + food;
	}

	static wstring AddSugar( wstring food )
	{		
		wcout << L"adding sugar.." << endl;
		Concurrency::wait( 1000 );		
		return L"sweet " + food;
	}

	static wstring PutOnPlate( wstring food )
	{
		wcout << L"putting on a plate.." << endl;
		Concurrency::wait( 1000 );		
		return food + L" on a plate";
	}

	static wstring WrapUp( wstring food )
	{
		wcout << L"Wrapping the food up.." << endl;
		Concurrency::wait( 1000 );		
		return food + L".";
	}
};

int main()
{
	// 문자열 꾸밈 함수들을 수행하는 transformer message block 들.
	transformer< wstring, wstring > boiler( &DecorationHelper::Boiler );
	transformer< wstring, wstring > addSugar( &DecorationHelper::AddSugar );
	transformer< wstring, wstring > putOnPlate( &DecorationHelper::PutOnPlate );
	transformer< wstring, wstring > wrapUp( &DecorationHelper::WrapUp );

	// 최종 변환된 message 를 저장하는 message block.
	unbounded_buffer< wstring > result;

	// 꾸며진 문자열들을 다른 꾸밈 함수에 연결하여 파이프라인을 구성.
	boiler.link_target( &addSugar );
	addSugar.link_target( &putOnPlate );
	putOnPlate.link_target( &wrapUp );
	wrapUp.link_target( &result );

	// 구성된 pipeline 에 message 전달.
	asend( boiler, wstring( L"soup" ) );
	asend( boiler, wstring( L"noodle" ) );
	asend( boiler, wstring( L"water" ) );	

	// 최종 변환된 message 를 받아서 출력.
	while( true )
	{
		wcout << L"completed food: " << receive( result ) << endl;		
	}
}

[ 코드2. transformer 를 이용하여 파이프라인을 구성하고 비 동기로 처리하는 예제 ]

 문자열을 꾸미는 함수들을 각각 transformer 에 지정한 후, ISource 인터페이스인 link_target() 을 이용하여 파이프라인을 구성합니다.

 구성된 파이프라인의 첫 message block 인 boiler 객체에 인자로 사용할 문자열을 message 로 보내면 파이프라인이 비 동기로 처리됩니다.

 파이프라인의 마지막은 unbounded_buffer 를 사용하여 변환된 최종 값을 저장하고, 저장된 값들을 꺼내서 화면에 출력합니다.

 

[ 그림1. transformer 를 이용하여 파이프라인을 구성하고 비 동기로 처리하는 예제 ]

[ 그림1. transformer 를 이용하여 파이프라인을 구성하고 비 동기로 처리하는 예제 ]


 

마치는 글

  transformer 를 이용해 파이프라인을 구성하고 비 동기로 처리하는 방법을 알아보았습니다.

 지난 글에서 소개해드린 call 과는 확실히 다르다는 것을 알게 되셨을 것입니다.

 이 파이프라인 개념은 많은 곳에 적용되고 있으므로 유용하게 사용하실 수 있을 것입니다.

닷넷4.0에서 네이티브코드와 매나지드코드의 동거 part 1.

CLR 2010. 8. 2. 21:03 Posted by 알 수 없는 사용자
닷넷 4.0 환경에서 네이티브 코드 와 매나지드 코드를 연결하는 방법을 2 회정도로 나누어서 다뤄볼까합니다.

닷넷에 대한 기초적인 개념이 구조는 다른 좋은 강좌들이 많으니 넘어가기로 하겠습니다. 저는 실제로 응용사례를 통해서 방법을 소개 해볼까합니다.

기본 아이디어는 다음과 같습니다.




그림에서 처럼 네이티브로 작성된 DLL 네이티브 코드와 혼합된 DLL을 경유해서 닷넷 기반언어에서 사용하는 방법입니다.
게임개발시 이런 방법을 사용할경유 장점은 네이티브코드로된 게임엔진으로 툴제작시 mfc를 사용하지않고 좀더 다루기 쉬운 C#을 사용해서 툴을 만들수있다는 장점있습니다.
뿐만 아니라 게임에서 많이 쓰이는 루아나 파이썬대신 C#으로 스크립트언어로도 사용할수있다는 점이있습니다. C#을 스크립트언어로 사용할경우 VS강력한 IDE의 화력지원을 받아 좀더 효과적인 개발을 할수있기때문입니다.

먼저 간단하게 전역함수를 콜해보는 예제를 살펴 보도록 하겠습니다.



이런식으로 네이티브 DLL을 만든 다음 이것을 매나지드 코드 DLL로 한번더 랩핑을 해주어야합니다.


class 이름 앞에 ref라고 붙는데 이렇게 하면 닷넷 기반의 다른 언어들이 같이 쓸수잇는 클래스가 만들어 집니다. ref가 붙지 않으면 오직 네이티브에서 만 쓸 수 있는 클래스가 만들어 지고요.

 

C#쪽에서는 다음과 같이 사용을 할 수 있습니다.

다음에는 네이티브 코드쪽 class를 매나지드 코드 class로 랩핑하여 매나지드 언어에서 콜하는 방법을 알아보도록 하겠습니다.





SQL Azure와 Excel 2010의 PowerPivot

Cloud 2010. 8. 2. 17:12 Posted by 알 수 없는 사용자

Excel 2010 PowerPivot은 별도의 Add-in을 설치하면 Excel 2010에서 사용할 수 있는 도구입니다.

아래의 사이트에서 다음과 같은 내용을 얘기하고 있습니다.

http://www.powerpivot.co.kr/

 

PowerPivot for Excel은 많은 사용자들을 확보하고 있는 Microsoft Excel 내에서 가히 독보적인 연산 능력을 직접 발휘할 수 있도록 하는 데이터 분석 도구입니다. Office와 동일한 사용자 인터페이스, 피벗 테이블 및 피벗 차트 보기, 슬라이서 등과 같이 이미 잘 알고 있는 Excel 기능을 사용하여 손쉽게 데이터를 분석할 수 있습니다.

 

제가 말씀드릴 내용은 SQL Azure의 데이터를 Excel 2010 PowerPivot에서도 문제없이 접근할 수 있다는 것입니다.

아래 그림은 PowerPivot 창의 화면입니다.

*PowerPivot 탭은 PowerPivot을 설치해야 나옵니다.

 

[외부 데이터 가져오기] 그룹의 기타 원본을 클릭하면 아래와 같이 SQL Azure에 대한 내용을 볼 수 있습니다.



SQL Azure를 선택하고 서버이름과 인증 정보, 데이터베이스를 선택합니다.

(SQL Azure의 방화벽이 설정되어 있어야 연결할 수 있습니다.)



SQL Azure에 있는 테이블을 선택합니다. 여기서는 CategorySubCategory 를 선택합니다. 그리고 클라우드가 아닌 로컬 네트워크에 있는 Product 테이블을 선택해서 관계를 적용할 것입니다.로컬 네트워크의 테이블과 연결하는 것은 PowerPivot에서 다양한 데이터 원본을 통해(클라우드 포함해서) Self-분석이 가능하다는 것을 알아보기 위함입니다.


데이터를 가져오기 할 경우에 “XML 구문 분석에 대한 에러가 발생할 경우는 SQL Server 2008 R2 기능 팩의 Microsoft® SQL Server® 2008 R2 Microsoft® Analysis Services OLE DB 공급자를 다운로드 받아 설치하면 됩니다.

http://www.microsoft.com/downloads/details.aspx?displaylang=ko&FamilyID=ceb4346f-657f-4d28-83f5-aae0c5c83d52


잘 가져오면 아래와 같이 성공으로 나타납니다.



데이터 가져오기 결과는 아래와 같습니다.


클라우드 환경이 아닌 로컬 데이터베이스에 있는 Product 테이블을 가져오기 했습니다.



디자인 탭의 관계 만들기를 통해 PowerPivot 테이블 간의 관계(FK)를 정의했습니다.


클라우드의 SQL Azure 를 이용해서 피벗 테이블을 구성한 결과는 아래와 같습니다. 또한 Excel 2010의 슬라이서를 이용했습니다.



이상으로 SQL Azure의 활용 측면으로 Excel 2010의 PowerPivot을 이용해서 SQL Azure의 데이터를 연결하는 내용을 알아보았습니다.

다음은 SQL Reporting Services 에서 SQL Azure 데이터를 나타내보도록 하겠습니다.