블로그 이미지
fiadot_old

칼퇴근을 위한 게임 서버 개발 방법론에 대한 심도있는 고찰 및 성찰을 위한 블로그!

Rss feed Tistory
Technical Article 2009. 2. 10. 16:27

Visual Studio 2005 단축키 설정이 제대로 바인딩 안될때 해결방법

Visual Studio 2005에서 도구-설정 가져오기 및 내보내기를 통해서 사용자가 지정한 단축키를 내보내기를 하고 재설치를 하거나 다른 컴퓨터에서 가져오기를 했을때 키보드 바인딩이 제대로 안되는 경험을 해보았을 것이다.

내보내기(export)한 파일(.vssettings)를 보게되면 지정된 단축키와 제거된 단축키와 관련된 전체 히스토리까지 저장되서 가져오기를 했을때 제대로 처리가 안된것이다.


1. 이를 방지하기 위해서 키보드 바인딩만 내보내기를 한다.

사용자 삽입 이미지

2. 저장된 파일을 텍스트 에디터를 이용해 Shortcut Command만 남기고 모두 삭제한다. 중복된 부분도 보이는데 이런 부분은 현재 사용중인것만 남기고 삭제한다.

3. 그리고 단축키를 제외한 파일을 만들어서 단축키와 환경설정에 대해서 별도로 관리를 하면 된다.
LEEGUNHO_VS2005_Keybinding.vssettings
LEEGUNHO_VS2005_WithoutKeyBinding.vssettings


■ 지정 단축키 ■

[code]
    <UserShortcuts>
     <Shortcut Command="VisualAssistX.SurroundSelectionWithComment" Scope="전역">Ctrl+/</Shortcut>
     <Shortcut Command="VisualAssistX.ReparseCurrentFile" Scope="전역">Ctrl+Shift+W</Shortcut>
     <Shortcut Command="Project.SetasStartUpProject" Scope="전역">Ctrl+Shift+'</Shortcut>
     <Shortcut Command="Window.CloseAllDocuments" Scope="전역">Ctrl+Shift+C</Shortcut>  
                   <Shortcut Command="Build.BuildSelection" Scope="전역">F7</Shortcut>
     <Shortcut Command="Build.RebuildSelection" Scope="전역">Ctrl+Shift+A</Shortcut>    
     <Shortcut Command="Project.AddNewItem" Scope="전역">Ctrl+Shift+N</Shortcut>
     <Shortcut Command="Project.AddClass" Scope="전역">Ctrl+Shift+M</Shortcut>
     <Shortcut Command="ClassViewContextMenus.ClassViewProject.Debug.Startnewinstance" Scope="전역">Ctrl+Alt+F6</Shortcut>
     <Shortcut Command="View.ErrorList" Scope="전역">Ctrl+1</Shortcut>
     <Shortcut Command="View.Output" Scope="전역">Ctrl+2</Shortcut>
     <Shortcut Command="View.FindResults1" Scope="전역">Ctrl+3</Shortcut>
     <Shortcut Command="View.CodeDefinitionWindow" Scope="전역">Ctrl+4</Shortcut>
     <Shortcut Command="File.GetLatestVersion" Scope="전역">Alt+1</Shortcut>
     <Shortcut Command="CheckIn" Scope="전역">Alt+2</Shortcut>
     <Shortcut Command="View.PendingCheckins" Scope="전역">Alt+3</Shortcut>
    </UserShortcuts>
[/code]

,
Technical Article 2007. 9. 28. 22:58

OLEDB의 Connection Pool에 대한 기본값 사용시 4가지 방법에 대한 수행속도 비교

MSSQL2K와 OLEDB Provider를 사용하여 VS6기반 ATL을 사용하여

OLEDB Wrapper를 구성해서 게임용 DB서버에서 사용하고 있었다.


올 중순부터  VS2005로 넘어오고 MSSQL2005로 바꾸면서,

기존의 네트워크 모듈부분을 뜯어고치면서

부분적으로 COM으로 구성하던 중,

DB쪽을 손보면서 OLEDB Wrapper에 대해 코드 리뷰를 하게 되었다.


기존 구조에서는 DBMS와의 연결은 단일하게 하고

Session을 DataSource에서 얻어와서 Accessor에서 사용하는 방식이였다.

즉, Stored Procedure나 query가 호출될때 session을 새로 생성하는 방식이다.

당시에는 MSSQL2K에서 Connection Pooling을 기본적으로 지원하기 때문에

Connection에 대해 크게 신경을 안쓰고 작성 했었는데, 이부분이 살짝 걸리긴 했다.

다른 방식에 대해서 성능 테스트를 해보지 못하고,

시간에 쫒겨서 만들었기 때문에  한번 검증해 보고자 테스트 코드를 만들어봤다.



환경은 Windows XP+SP2, MSSQL2005+SP1,

VS2005+SP1, MS OLEDB Provier for SQL Server 이다.

Connection스트링에 대한 옵션은 default값으로 하였다.

4가지 경우에 대한 성능 테스트를 진행해보았다.

공통적인 부분은 스레드 2개를 통해 SP(Stored Procedure)를 호출하는 consumer를 통해

recordset을 fetch하는 부분이다.

* TACCSSOR는 CCommand<CAccessor<CS_SP명> >을 상속한 클래스 인스턴스

 Case1>  Wizard를 통해 생성된 ATL OLEDB 소비자(consumer)를 그대로 사용 했다.
 ThreadFunc1()
 {
  TACCSSOR.OpenAll();  
 ...read...
 TACCSSOR.CloseDataSource();
 }

 Case2> DataSource 연결 1개. DataSource를 통해 Session 연결 1개.
 ThreadFunc2()
 {
 TACCESSOR.Open(미리 생성해둔 Session전달);
 ...read...
 TACCESSOR.Close();
}

 Case3> DataSource 연결 1개.  세션은 매번 생성
 ThreadFunc3()
 {
 CSession  session;
 _session.Open(DataSource); // DataSource를 인자로  Session생성
 TACCESSOR.Open(미리 생성해둔 Session전달);
 ...read...
 TACCESSOR.Close();
}

Case4> DataSource 연결1개, 자체 세션 풀 구성
ThreadFunc4()
{
CSession *pSession = GetSession();
 TACCESSOR.Open(*pSession);
 ...read...
 TACCESSOR.Close();
FreeSession(pSession);
}

즉, 기존에 구성했던 방식은 Case2에 해당된다.

과연 어떤 결과가 나올것인가?? 하하하~ 흥분 흥분 *--*;;

각각의 케이스에 대해 ThreadFunX를 1000회씩 실행한 결과이다.


const int DBPOOL_CASE = 1;   // 9567, 9511, 9560
const int DBPOOL_CASE = 2;   // 4388, 4402, 4375
const int DBPOOL_CASE = 3;   // 1162, 971, 977
const int DBPOOL_CASE = 4;   // 470, 530, 468

왜 이런 결과가 나온것일까??

Case1의 기본으로 생성해주는 consumer코드는 매번 DataSource를 연결하지만

Connection Pool이 구성되어 있다면 Case 2와 크게 차이가 없어야 맞다.

하지만, OpenAll 메소드 안에서 Propertie들에 대해 셋팅하는 부분과 OpenRowset을 호출하는

몇단계의 함수콜이 더 들어가게 된다. 이부분에서 시간을 소요하게 되는것이다.

Case2가 Case3보다 느린이유는 DataSource와 Session을 단일하게 사용하게 되면서

Bottle neck이 발생했다고 보는게 맞다.

(추가적으로 얻을수 있는 정보는 Session이 하나라도 자동적으로 Serialize되기때문에
 
 lock이 필요 없다는것. ^^)

Case3은 세션을 Datasource에서 가져오기 때문에 사용중인 세션에 SP를 호출하는

우를 범하지 않아서 계속 지역변수로써 Session을 생성하지만 더 빠른것이다.



Connection Pool을 직접적으로 체감하기 위해 스레드생성 이전부터 루프를 돌려 테스트를 진행해 보았다.

10회 // 4296, 3285, 1082, 588
20회 // 8801, 5908, 2118, 1089
50회 // 21883, 7544, 3257, 2585

잼있는 결과가 나온다.

자동으로 생성해준 코드가 DataSource와 Session을 미리 연결해놨을때 보다

훨씬 빠른 속도를 보여준다.

왜 이런 결과가 나온것일까?

DataSource와 Session을 단일하게만 사용했기때문에 Pool을 제대로 활용 못한 결과이다.

즉, DataSource와 Session을 매번 생성하는 Case 1의 경우 Pool을 제대로 활용했기때문에

Case1보다 빠른 성능을 보여준다.

하지만 Case3이 가장 빠를수 밖에 없는 이유는 Connection Pool도 활용하면서

선형적인 작업에는 Session Pool이 사용되는 구조이기 때문에 좋은 성능을 보여준다.

Case4는 Session을 미리 생성해두고 사용하기 때문에 지역변수가 Stack쌓이고 정리되는

과정을 줄여 속도를 높이고 있다.



결론적으로, DataSource는 하나로, Session은 필요할때(SP나 Query를 호출할때)

생성해주는게 좋은 성능을 보여준다.

게다가  Session또한 Pool로 구성하면 더욱 좋은 성능을 낼수 있다.



< vector를 이용해 Session Pool을 구현한 급조 소스 >






추가적으로 ...

<< Connection Pool에 대한 기본 개념  >>

Pool?  자료나 공간에 대해 미리 구성을 한뒤에 뽑아서 사용하고 다시 집어넣는 형식을 말함.

Connection Pool? 상호간의 연결에 대한 풀을 구성해놓고 접속을 재사용하는것.

MSSQL에서의 Connection Pool의 특징

1. Database Connection에 대한 풀.
  즉, OLEDB Provider 기준으로 보자면 CDataSrc에 해당한다.

2. 프로세스 단위로 구성
  각 풀은 프로세스 마다 풀을 구성하게 된다.

3. default값으로 Connection pooling 한다.

조건? Connection String부분이 blank하나까지도 동일해야 한다.
        (UDL파일을 사용하여 실수를 줄일수 있다)

,
Technical Article 2007. 7. 24. 10:41

VS2005에서 Wizard를 통해 OLEDB Consumer생성시 Stored Procedure Accessor bind 실패 원인 분석

VS2005에서 Wizard를 통해 OLEDB Consumer생성시 Stored Procedure Accessor bind 실패 원인 분석

Visual Studio 6에서는 Insert - new ATL Object - Data Access - Consumer를 통해
Stored Procedure(이하SP)에 대한 Accessor를 자동으로 생성할수 있다.
이를 통해 다음과 같은 코드를 얻게된다.


// dboSBuddyList1.H : Declaration of the CdboSBuddyList1 class

#ifndef __DBOSBUDDYLIST1_H_
#define __DBOSBUDDYLIST1_H_

class CdboSBuddyList1Accessor
{
public:
 LONG m_RETURNVALUE;
 TCHAR m_userid[13];
 BYTE m_usertype;
 TCHAR m_coluserid[13];
 BYTE m_colusertype;
 TCHAR m_colnick[13];
 BYTE m_collev;
 TCHAR m_colcomment[61];

BEGIN_PARAM_MAP(CdboSBuddyList1Accessor)
 SET_PARAM_TYPE(DBPARAMIO_OUTPUT)
 COLUMN_ENTRY(1, m_RETURNVALUE)
 SET_PARAM_TYPE(DBPARAMIO_INPUT)
 COLUMN_ENTRY(2, m_userid)
 COLUMN_ENTRY(3, m_usertype)
END_PARAM_MAP()

BEGIN_COLUMN_MAP(CdboSBuddyList1Accessor)
 COLUMN_ENTRY(1, m_coluserid)
 COLUMN_ENTRY(2, m_colusertype)
 COLUMN_ENTRY(3, m_colnick)
 COLUMN_ENTRY(4, m_collev)
 COLUMN_ENTRY(5, m_colcomment)
END_COLUMN_MAP()

DEFINE_COMMAND(CdboSBuddyList1Accessor, _T("{ ? = CALL dbo.S_BuddyList;1 (?,?) }"))

 // You may wish to call this function if you are inserting a record and wish to
 // initialize all the fields, if you are not going to explicitly set all of them.
 void ClearRecord()
 {
  memset(this, 0, sizeof(*this));
 }
};

class CdboSBuddyList1 : public CCommand<CAccessor<CdboSBuddyList1Accessor> >
{
public:
 HRESULT Open()
 {
  HRESULT  hr;

  hr = OpenDataSource();
  if (FAILED(hr))
   return hr;

  return OpenRowset();
 }
 HRESULT OpenDataSource()
 {
  HRESULT  hr;
  CDataSource db;
  CDBPropSet dbinit(DBPROPSET_DBINIT);

  dbinit.AddProperty(DBPROP_AUTH_PASSWORD, OLESTR("wing_access"));
  dbinit.AddProperty(DBPROP_AUTH_USERID, OLESTR("xxxxx"));
  dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("xxxxx"));
  dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("xx.xx.xx.xx"));
  dbinit.AddProperty(DBPROP_INIT_LCID, (long)1042);
  dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4);
  hr = db.Open(_T("SQLOLEDB.1"), &dbinit);
  if (FAILED(hr))
   return hr;

  return m_session.Open(db);
 }
 HRESULT OpenRowset()
 {
  return CCommand<CAccessor<CdboSBuddyList1Accessor> >::Open(m_session);
 }
 CSession m_session;
};

#endif // __DBOSBUDDYLIST1_H_


이를 사용 하는 방법은

#include "dboSBuddyList1.H"
#define DB_STRING_FIELD_COPY(dest, src)  _tcsncpy(dest, src, sizeof(dest))


void CDBTT2Dlg::OnButton1()
{
 // TODO: Add your control notification handler code here
 
 CdboSBuddyList1 rs;
 HRESULT hr;

 DB_STRING_FIELD_COPY(rs.m_userid,  _T("lovydayz"));
 rs.m_usertype = 0;

 hr = rs.Open();

 hr = rs.MoveFirst();

 rs.Close();

}



이런식이다. 아이디와 타입을 SP의 인자로 넘기고 레코드셋을 받아오는 SP이다.


VS2005에서는 추가-클래스-ATL OLEDB 소비자 를 통해서 생성하는데
SP에 대해서는  insert, delete, update옵션이 적용되지 않으면 Accessor인지, Table인지
설정하는 옵션이 disable되고 파일을 생성해준다.


class CS_BuddyListAccessor
{
public:

 // 다음 마법사 생성 데이터 멤버에는  열 맵의 해당
 // 필드에 대한 상태 값이 들어 있습니다. 이 값을
 // 사용하여 데이터베이스에서반환하는 NULL 값을
 // 보유하거나 컴파일러에서 오류를 반환할 때
 // 오류 정보를 보유할 수 있습니다. 이러한 필드 사용에
 // 대한 자세한 내용은 Visual C++ 설명서의
 //  "마법사 생성 접근자"에서 "필드 상태 데이터 멤버"를 참조하십시오.
 // 참고: 데이터를 설정/삽입하기 전에 이들 필드를 초기화해야 합니다.

 DBSTATUS m_dwuserid1Status;
 DBSTATUS m_dwusertype1Status;
 DBSTATUS m_dwnickStatus;
 DBSTATUS m_dwlevStatus;
 DBSTATUS m_dwcommentStatus;

 // 다음 마법사 생성 데이터 멤버에는 열 맵의 해당 필드에 대한
 // 길이 값이 들어 있습니다.
 // 참고: 가변 길이 열의 경우 데이터를 설정/삽입하기 전에
 //       이러한 필드를 초기화해야 합니다.

 DBLENGTH m_dwuserid1Length;
 DBLENGTH m_dwusertype1Length;
 DBLENGTH m_dwnickLength;
 DBLENGTH m_dwlevLength;
 DBLENGTH m_dwcommentLength;


 TCHAR m_userid1[13];
 BYTE m_usertype1;
 TCHAR m_nick[13];
 BYTE m_lev;
 TCHAR m_comment[61];

 // 인자들은 리턴되는 레코드셋보다 상단에 있어야 한다!!! - 이근호 070724
 LONG m_RETURN_VALUE;
 TCHAR m_userid[13];
 BYTE m_usertype;


 void GetRowsetProperties(CDBPropSet* pPropSet)
 {
  pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL);
  pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL);
  pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE);
 }

 HRESULT OpenDataSource()
 {
  CDataSource _db;
  HRESULT hr;
// #error Security Issue: The connection string may contain a password
// 아래 연결 문자열에 일반 텍스트 암호 및/또는
// 다른 중요한 정보가 포함되어 있을 수 있습니다.
// 보안 관련 문제가 있는지 연결 문자열을 검토한 후에 #error을(를) 제거하십시오.
// 다른 형식으로 암호를 저장하거나 다른 사용자 인증을 사용하십시오.
  hr = _db.OpenFromInitializationString(L"Provider=SQLOLEDB.1;Password=xxxxx;Persist Security Info=True;User ID=xxxxx;Initial Catalog=xxxx;Data Source=xx.xx.xx.xx;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LEEGUNHO;Use Encryption for Data=False;Tag with column collation when possible=False");
  if (FAILED(hr))
  {
#ifdef _DEBUG
   AtlTraceErrorRecords(hr);
#endif
   return hr;
  }
  return m_session.Open(_db);
 }

 void CloseDataSource()
 {
  m_session.Close();
 }

 operator const CSession&()
 {
  return m_session;
 }

 CSession m_session;

 // 일부 공급자와 관련된 몇몇 문제점을 해결하기 위해 아래 코드에서는
 // 공급자가 보고하는 것과 다른 순서로 열을 바인딩할 수 있습니다.

 BEGIN_COLUMN_MAP(CS_BuddyListAccessor)
  COLUMN_ENTRY_LENGTH_STATUS(1, m_userid1, m_dwuserid1Length, m_dwuserid1Status)
  COLUMN_ENTRY_LENGTH_STATUS(2, m_usertype1, m_dwusertype1Length, m_dwusertype1Status)
  COLUMN_ENTRY_LENGTH_STATUS(3, m_nick, m_dwnickLength, m_dwnickStatus)
  COLUMN_ENTRY_LENGTH_STATUS(4, m_lev, m_dwlevLength, m_dwlevStatus)
  COLUMN_ENTRY_LENGTH_STATUS(5, m_comment, m_dwcommentLength, m_dwcommentStatus)
 END_COLUMN_MAP()

 BEGIN_PARAM_MAP(CS_BuddyListAccessor)
  SET_PARAM_TYPE(DBPARAMIO_OUTPUT)
  COLUMN_ENTRY(1, m_RETURN_VALUE)
  SET_PARAM_TYPE(DBPARAMIO_INPUT)
  COLUMN_ENTRY(2, m_userid)
//  SET_PARAM_TYPE(DBPARAMIO_INPUT)
  COLUMN_ENTRY(3, m_usertype)
 END_PARAM_MAP()

 DEFINE_COMMAND_EX(CS_BuddyListAccessor, L"{ ? = CALL dbo.S_BuddyList;1 (?,?) }")

};

class CS_BuddyList : public CCommand<CAccessor<CS_BuddyListAccessor> >
{
public:
 HRESULT OpenAll()
 {
  HRESULT hr;
  hr = OpenDataSource();
  if (FAILED(hr))
   return hr;
  __if_exists(GetRowsetProperties)
  {
   CDBPropSet propset(DBPROPSET_ROWSET);
   __if_exists(HasBookmark)
   {
    if( HasBookmark() )
     propset.AddProperty(DBPROP_IRowsetLocate, true);
   }
   GetRowsetProperties(&propset);
   return OpenRowset(&propset);
  }
  __if_not_exists(GetRowsetProperties)
  {
   __if_exists(HasBookmark)
   {
    if( HasBookmark() )
    {
     CDBPropSet propset(DBPROPSET_ROWSET);
     propset.AddProperty(DBPROP_IRowsetLocate, true);
     return OpenRowset(&propset);
    }
   }
  }
  return OpenRowset();
 }

 HRESULT OpenRowset(DBPROPSET *pPropSet = NULL)
 {
  HRESULT hr = Open(m_session, NULL, pPropSet);
#ifdef _DEBUG
  if(FAILED(hr))
   AtlTraceErrorRecords(hr);
#endif
  return hr;
 }

 void CloseAll()
 {
  Close();
  ReleaseCommand();
  CloseDataSource();
 }
};


 

이와 같이 생성된 코드를 테스트 해보면 결과값을 얻어오지 못함을 알수 있다.

Bind부분에서 ATLASSERT(m_pAccessor != NULL) 에서 걸린다.


왜 바인딩을 못하는것인가??

Wizard page를 통해 생성한 코드가 제대로 실행이 안된다는것은 말이 안된다는 가정하에
문제에 접근해 보았다. ATL코드를 죽 훑어보며 DynamicAccssor를 사용하니 문제없이 되었다.
그렇다면 문제는 ATL코드 자체에 있거나 생성한 코드에 있다는 2가지 결론에 도달할수 있었다.

VS6에서 생성한 코드와 비교하며 매크로의 차이, 멤버변수의 차이, 메소드의 차이 등에 대해서
비교를 해보았다.
DEFINE_COMMAND가 DEFINE_COMMAND_EX로 변경되고 CALL시에 SP이름뒤에 ;가 붙는다는것 이외에는 별 문제가 없었다. 결국 binding시의 변수 순서에 문제가 있는것으로 생각하고,
몇번의 삽질을 통해 SP의 인자로 들어가는 멤버변수는 결과 레코드셋이 담기는 변수들 위에
위치해야 된다는 결과를 얻게 되었다.

거의 3일동안 삽질하다가 오늘도 안되면 그냥 DynamicAccessor를 이용해서 돌아가려고 했는데,
다행히 문제가 해결됐다 ^^;

,
Technical Article 2007. 7. 14. 15:29

VS2005에 추가된 키워드 for each

STL을 사용하다 전체 노드에 대해 모두 어떠한 함수를 적용해야 할때

for_each란 generic algorithm을 사용하게 된다.

그런데 이 부분이 좀 문제가 있는게 함수를 쓰던지 클래스를 새로 생성해서

operator()를 구현해줘야 한다.

심플하게 끝내자고 해놓은 것이 복잡도만 높이고 있는 형국이다.

이를 타계하기 위해서는 2가지 방법이 있다.

첫번째는 for_each에 대해 적용될 함수셋을 구현해놓는방법.

http://levites.pe.kr/tt/index.php?pl=20 

여기에 잘나와있던데 좀 불편하다.

두번째는 Visual Studio 2005에 추가된 키워드인 for each를 사용하는 방법이다.


class CBroadCast
{
public:
 CBroadCast(CPacket &refSendPkt) : m_refSendPkt(refSendPkt), m_nSendCnt(0)
 {
 
 }

 void operator()(CFiaSession* pNode)
 {
  pNode->SendPacket(m_refSendPkt);
  m_nSendCnt++;
 }


 CPacket &m_refSendPkt;
 int  m_nSendCnt;
};


 

int  CRoom::BroadCasting( CFiaSession *pSession, CPacket &refSendPkt)
{
..
CBroadCast bc(refSendPkt);
bc += for_each(m_vecUser.begin(), m_vecUser.end(), bc);
..
}


이런 형태로 STL for_each를 사용 했다면,

 int nCnt = 0;
 for each(CFiaSession* it in m_vecUser)
 {
  it->SendPacket(refSendPkt);
  nCnt +=1;
 }

2005에서는 이렇게 for each를 사용하면 된다.


일반적으로 iterator를 이용해서 순회하면서 for each의 형태로 구성 해서 사용했다.

하지만, OS종속적이며 퍼포먼스를 지향하는 서버 어플리케이션에서

OS호환성은 고려대상의 범위에서 벗어나기에, for each로 구현을 했다.


지은이햄의 테스트 결과로 "for_each보다 for each가 퍼포먼스 면에서도 우수했다"고 한다. ^^

코드면에서도 깔끔하고 성능도 나으니 안쓸이유가 없네..

,
TOTAL TODAY