LOGIN • JOININ

extremely EASY


1... CGCII는 쉽고 간결합니다.


일단 간단한 서버를 작성한 예제를 한번 보겠습니다.

20000번 포트로 클라이언트로 부터 접속을 받는 예제를 한번 작성해 보겠습니다.

#include "stdafx.h"
#include "CGNetSocketTemplates.h"

// 1) Socket 클래스를 정의한다.
class CSocket : public CGNet::Socket::CTCP<>
{
	virtual void	OnConnect() override				{ Send(CCGBuffer(64)<<int()<<"Welcome"<<CGD::END_MESSAGE());}
	virtual void	OnDisconnect(uint32_t) override		{ printf("Disconnected\n");}
	virtual int		OnMessage(CGMSG& /*_MSG*/) override	{ printf("Message Received\n"); return	0;}
};

int _tmain(int /*argc*/, _TCHAR* /*argv*/[])
{
	// 2) CSocket을 사용하는 Acceptor 객체를 생성한다.
	auto	pacceptor	 = NEW<CGNet::CAcceptor <CSocket>>();

	// 3) Acceptor를 20000번 포트에 Listen하기 시작한다.
	pacceptor->Start(20000);

	// 4) ESC키를 누를 때까지 대기한다.
	while(_getch()!=27);

	return 0;
}
이것이 실제 동작하는 서버 코드의 전부입니다.  이 코드에서 작성한 내용은 간략히 설명하자면

① 먼저 소켓클래스를 정의합니다. 
       'CGNet::Socket::CTCP<>'를 상속받아 'CSocket' 클래스를 정의합니다.
       'CSocket' 의 'OnConnect()', 'OnDisconnect()', 'OnMessage()'함수를 재정의합니다. 
       (이함수들은 각각 접속되었을 때, 접속이 종료되었을 때, 메시지를 받았을 때 호출되는 함수입니다.)

 Acceptor를 생성해 20000번 포트로 접속 받기를 시작합니다.

      'main()'함수에서 'Accpetor' 객체에 'CSocket' 을 템플릿 인자로 넣어 선언합니다.
       그리고 20000번 포트로 인자로 넣어 'Start()'을 해 서버를 동작시켰습니다.


이것이 작성한 코드 내용입니다.

그러면 이 서버는


① 20000번 포트로 클라이언트의 접속 요청이 들어오면 'CSocket' 객체를 신규로 생성합니다.

② 그리고 새로 생성한 'CSocket'객체에 접속을 시도해온 클라이언트를 접속처리합니다.

③ 접속처리가 되면 'CSocket::OnConnect()'함수를 호출해 줍니다.

'CSocket::OnConnect()'함수에서는  64바이트의 버퍼를 할당받아  "Welcome" 문자열을 써넣은 후 전송합니다.

 메시지가 도착하면 'CSocket::OnMessage(...)'를 호출해 화면에 "Messag Received"를 출력해 줍니다.

⑥ 접속이 종료되면 'CSocket::OnDisconnect(...)'를 호출해 화면에"Disconnected"를  출력합니다.

⑦ ESC 키를 누르면 'Acceptor'를 종료하고 모든 접속을 끊고 소켓 객체들을 해제한 후 프로그램을 종료합니다.
     (특별히 'Acceptor'등을 명시적으로 Close해주지 않아도 자동적으로 소멸 처리가 됩니다.)


즉,  이 코드만으로 클라이언트 접속과 접속종료를 처리하며 필요한 메시지를 전송하고 받은 메시지를 처리하는 등 서버의 모든 처리가 가능하다는 것입니다.


이보다 더 간단할 수 있을까요?

물론 단순히 양적으로 적은 코드라고 해서 더 쉬운 개발을 의미하는 것이라고는 생각하지는 않습니다.



2... 코드량을 줄여주는 CGCII 간결함과 생산성의 원칙들

1) 처음 사용시 생성 'Construct On First Use'


많은  엔진이나  라이브러리들은 사용하기전 초기화 절차나 구조체에 복잡한 값을 넣어 설정하는  절차를 요구합니다. 
매번 동일한 내용으로 설정하는 통과 의례일 뿐이라도 반드시 빠트려선 안돼죠.

하지만 위의 CGCII로 작성된 예제 코드에는 초기화를 위한 코드나 설정 절차가 전혀 존재하지 않습니다.

단지 필요한 객체를 생성하고 사용만 했을 뿐입니다.


 이것은 초기화의 절차들이 모두   처음사용시 생성(‘Construct On first Use’)의 개념을 적용해 첫번째 사용 때 내부적으로 자동 초기화되도록 설계되어 있기 때문입니다.

  따라서 초기화를 굳이 하지 않아도 처음 사용할 때 필요한 기능들은 초기화 순서나 절차 등이 모두 자동적으로 필요한 것들만 기본 값으로 초기화가되며  종료될 때도 자동적으로 해제됩니다.


따라서 CGCII는 다른  설정이 필요한 경우가 아니라면 초기화를 할 필요도 알필요도 없습니다.

단지 필요한 객체의 생성하고 사용만 하면 됩니다.  


그렇다면 앗사리 설정된 대로만 초기화를 해야 하며 바꿀수는 없는 것일까요?
물론 아닙니다. CGCII는 기본 설정과 다른 설정을 해야하는 경우에만 그 값을 초기화해주면 됩니다.

이러한 설계 철학은 초기화 뿐만 아니라 설계의 곳곳에 적용되어져 있습니다.



2) 추상화된 객체 생성/소멸 (Abstracted Creation/Destroy)과 통합된 인터페이스 (Unified Interfaces)

pacceptor 객체는 'Start(20000)'를 호출해서 접속요청대기(Listen)를 시작합니다.

일반적인 경우 'Listen(20000)'이  의미상으로 더 합당할 수도 있습니다. 

그런데 왜  'Start()'이란 함수명을 사용했을까요?

 그 이유는 추상화를 통해 최대한 단일화된 인터페이스를 제공해주기 위함입니다. 즉 통합된 초기화를 지원하기 때문입니다.

Acceptor뿐만 아니라 초기화나 시작 코드가 필요한 모든 클래스들은 동일한 인터페이스를 가집니다.

'초기화'와 '소멸' '시작'과 '종료'를  Init-Start-Stop-Destroy로 추상화되어 통합처리 됩니다.

이런 통일된 인터페이스는 개발자로 하여금 일관성을 제공해줄뿐만 아니라 계층적 초기화나 통합 초기화 시스템 등을 통해 더 체계적으로 코딩이 가능합니다.


3) 스마트 포인터(Smart Pointer)와 RAII(Resource Acquisition Is Initialization)

위의 예제를 보면 'NEW<T>'를 사용해 동적할당한 부분은 있습니다.

이 'NEW<T>'는 C++의 기본문법인 'new T()'와 동일한 것입니다.
근데 NEW<T>를 통해 동적할당하는 부분만 있을 뿐  해제하는 코드는 없습니다.

이것은 객체의 할당과 해제를 '스마트 포인터''RAII'객체인 'CGPTR<T>'객체를  사용해 자동적으로 처리되기 때문입니다. 

동적 할당된 객체의 관리는 상당히 귀찮고 까다롭습니다. 이로 인해 실수를 유발하기도 합니다.

 CGCII에서는 동적 할당 객체들을 참조계수와 스마트포인터로 일일이 수동으로  처리할 필요없이 내부적으로 가비지 콜렉션 처리 됩니다.  

또 CGPool과 연동되어 처리되며 풀을 통한 할당과 해제도 내부적으로 자동처리 됩니다.  
뿐만 아니라 모든 사용량 혹은 할당량 등이 실시간 통계처리 되어 실시간 모니터링 및 관리가 가능합니다.

 pacceptor는 생성 후 종료할 때 Stop()를 호출해 주었습니다만 이 마져도 'CGPTR_DESTRORY<T>'를 사용하면 'Stop()'함수의 호출도 필요 없습니다. (이런 것들은 통합된 인터페이스로 가능합니다.)


즉, 동적 객체를 생성만 하면 그 이후에 소멸과 파괴 등에 대해서는 거의 신경을 쓰지 않아도  되 작업 효율 뿐만 아니라 작성 코드량도 획기적으로  줄여줍니다.



4) 템플릿 메타 프로그래밍

CGCII에서 편리한 프로그래밍을 제공해주는  중요한 요소중에 하나가 바로 현대 C++의 가장 큰 강점인 '템플릿 메타 프로그래밍'입니다.

CGCII에서는 매우 광범위하게 템플릿 메타 프로그래밍이 사용되었 간결한 코드의 작성이 가능하도록 해줍니다.

그 중 몇가지 예를 들어보자면...


첫번째 예,

위의 예제를 보면 동적할당을 위해 'new'를 사용하지 않고  'NEW<T>'라는 템플릿 함수를 사용했습니다.


	auto	pacceptor	 = NEW<CGNet::CAcceptor <CSocket>>();


그냥 new를 쓰면 되지 왜 이렇게 했을까요? 그냥 튀어보일려고? 

동적할당은 성능과 관리 등의 이유로 풀을 사용하는 경우도 있고 참조계수를 통해 객체를 관리하기도 하는 등 다양한 기능이 추가되며 신경써야 할 것들이 많아졌습니다.

이 NEW<T>를 사용하게 되면 내부적으로 'SFINAE(Substution Failure Is Not An Error)'로  이런 복잡한 것들에 대해 고려할 필요 없이 알아서 동적할당을 해줍니다.


-  먼저 T 클래스가 풀 할당이 설정된 클래스 인지를 확인해 풀할당 클래스라면 자동적으로 풀에서 할당해서 줍니다. 

-  풀 할당기능이 없다면 'new'를 사용해 동적할당을 해줍니다.

-  'ICGReferenceCount'객체가 구현되어 있지 않으면 자동으로 기본 구현을 추가해 객체를 생성해 줍니다.


 

이런 다양한 처리를 

'SFINAE'를 사용해 컴파일러가 자동으로 처리하도록 해주었습니다.

 


두번째 예, 

CGCII에서 비동기 실행을 요청하는 함수는 'POST_EXECUTE(...)'입니다.

근데 이때 템플릿 메타 프로그래밍을 사용하여 POST_EXECUTE의 파라메터로 입력되는 값에 따라 자동적으로 처리를 합니다.


① C형식의 static 전역 함수일 경우 

② C++의 객체 멤버 함수일 경우

③ C++11의 Lambda함수일 경우

④ C++ std::bind 혹은 boost:bind일 경우

⑤ CGCII의 'ICGExecutable' 객체인 경우

⑥ 함수의 파라미터 갯수, 리턴 형 상관없이 실행해 줍니다.


// ① C형식의 static 전역 함수일 경우
POST_EXECUTE(foo);

// ② C++의 객체 멤버 함수일 경우
POST_EXECUTE(NEW<CObject>, CObject::Run());

// ③ C++11의 Lambda함수일 경우
POST_EXECUTE([=]()
{
	printf("Hellow CGCII!!\n");
});

// ④ C++ std::bind 혹은 boost:bind일 경우 - void foo(int _x, float _y)
POST_EXECUTE(std::bind(foo, 10, 20.0f));

// ⑤ CGCII의 'ICGExecutable' 객체인 경우
POST_EXECUTE( NEW<CExecutable>());

// ⑥ 함수의 파라미터 갯수, 리턴 형 상관없이 실행해 줍니다.
POST_EXECUTE(foo, 100, 20.0f );


어떤 형의 인자가 넘겨지는 것인가에 따라 자동적으로 컴파일러가 처리할 함수를 결정해 실행합니다.

개발자는 굳이 어떤 형태인지 고려해서 프로그래밍 할 필요 없이 어떤 형태의 함수인지 신경쓸필요 없이 'callable'이면 어떤 것이든 POST_EXECUTE()함 호출해주면 자동으로 판단해 해당 처리 함수나 클래스를 자동적으로 선택해 실행해 준다는 것입니다.


5) 현대 C++(Mordern C++)의 지원

  CGCII는 1997년 최초 엔진 기초 안이 도입시부터 당시로써는 당시로써는 선도적으로  완전한 OOP 설계를 추구했으며 개발 편의성과 간결한 코드는 항상 주요한 고려대상이었습니다.


 그리고 오랜 시간동안 실무 개발 경험이 더해지며 지속적으로 구조는 다듬어져 왔습니다.


최근 현대 C++의 진화에 따른 업그레이드는  이전의 어떤 변화보다도 더 간결함과 손쉬운 프로그래밍이 가능하게 해주었습니다.
현대 C++에 추가된 람다(Lambda)와 클로저Closure), 템플릿 메타 프로그래밍, auto와 같은 키워드, 타입 디덕션(Type Deduction)과 같은 다양한 최신 프로그래밍 언어로의 진화가 가능하게 해 주었습니다.

CGCII는 8버전에서 과감히 C++11버전을 도입해  훨씬 더 간결하고 손쉬운 프로그래밍이 가능하게 되었습니다.
또 앞으로 CGCII는 C++14 혹은 C++17 등 표준이 적용됨에 따라 지속적으로 더욱더 편리하고 동작되도록 발전해 나갈 예정입니다.

※ 현대 C++은...
요즈음 C++은 모던 C++로의 대대적인 표준 업데이트가 진행되고 있습니다.
현대식 프로그래밍 언어의 파라다임을 대폭 포용해 2017년까지 단계적으로 표준화를 진행하고 있습니다. 
람다(Lambda)나 클로져(Closure), 템플릿메타프로그래밍(Template Meta Programming)
, 타입 디덕션(Type Deduction)과 같은 주요한 개념들의 추가와 템플릿 메타 프로그래밍의 강화로 기존의 C형식 즉 함수 형태로 제공되던 많은 표준함수들이 대거 템플릿 클래스의 형태로 대체되어 제공되기 시작했습니다. 
또 하드웨어 발전과 더불어 기본이 되어 버린 멀티코어 프로그래밍 지원 등과 쓰레드/동기화 시스템 그리고 네트워크 프로그래밍의 영역까지 표준화를 진행하고 있으며 편리성을 인정받은 많은 C++라이브러리들 역시 표준으로 대폭 수용되고 있습니다.
 이런 모던 C++로 개정을 통해  언어 차원의 '크로스 플랫폼' 구현에 한발 더 가까이 갈 것으로 예상되며 무엇보다 가장 중요한 변화는 바로 어떤 언어보다도 '간결하고 편리한 프로그래밍'이 가능해지고 있다는 것입니다.



6) 단 몇개의 클래스만 알면... 나머지는 자동

  CGCII에는 수백개의 클래스들이 있으며 수천개의 함수들이 있습니다. 그러나 이 대부분의 클래스에 대해서는  굳이 알 필요 없습니다.

단 몇개의 클래스와 함수만 이해하면 간단히 고성능의 서버를 프로그래밍할 수 있습니다.

왜냐면... 나머지 클래스와 함수들은 '템플릿 메타 프로그래밍'과 같은 다양한 내부적 처리를 통해 자동적으로 생성이 되고 자동적으로 처리가 되기 때문입니다.


7) 기타...

 앞에 설명된 것 외에도 CGCII는 깨알같이 각종 프로그래밍의 편리함 간편함을 제공해 주기 위한 시스템들이 있습니다.

단순히 글로 표현할 수 없는 개발자의 경험과 철학이 담긴 원칙들로 설계된 

깨알과 같은 것들이 무수히 많습니다.

  

CGCII에는 

 

직접 사용해보고 경험해보시면 아마 느낄수 있으시리라고 봅니다.




3... 눈에 보이지 않는 CGCII 간결함과 생산성 원칙들


1) '가로채기' 함수의 설계

서버엔진이나 라이브러리들이 특성상 접속시 혹은 접속종료 시 메시지를 받았을 때의 동작 등등을 정의하기 위해 콜백(Call-back)형태의 함수를 많이 사용합니다. 
하지만 별것 아닌 것 같아 보이는 '가로채기(Hook)' 혹은 '콜백함수(Call-back)' 설계의 작은 차이로도  사용하는 입장에서 편의성은 하늘과 땅만큼 차이가 날수 있습니다.

예를 들어 설명해 보자면.
// 새로운 접속이 들어 왔을 때 호출되는 콜백
void fCallback_OnConnect(ISession* _Info)
{
	// _Info 접속과 연관된 정보를 생성해서 매니저에 등록해 준다.
	m_managerSession.register(_Info, new new SESSION_DATA(_Info));
	...
}

// 메시지를 전송받았을 때 호출되는 콜백
void fCallback_OnMessage(ISession* _Info, MSG _msg)
{
	// 먼저 _Info로 해당하는 정보를 먼저 찾아야만 한다.
	SESSION_DATA* pSession= m_managerSession.find(_Info);
	...
	// 메시지 전달해 처리..
}

위와 같은 'Callback'함수의 정의가 있다면 'ISession*'값에 넘어 오는 값을 사용해 이에 해당하는 객체를 찾아야만 합니다.
이런 설계로는 객체지향 설계의 편의성을 제공받기가 힘듭니다만 이렇게 많이 설계를 합니다.
이런 설계는 엔진을 설계하는 입장에서는 편리하지만 사용자 입장에서는 많은 불편함이 있을수 있습니다.


CGCII은 설계를 조금 다르게 합니다. 
아래의 CSocket 정의를 보겠습니다.

class CSocket : public CGNet::Socket::CTCP<>
{
	// 여기에 이렇게 멤버 데이터로 추가만 하면
	SESSION_DATA	m_sessionData;

	virtual void OnConnect() override
	{
		// m_sessionData를 바로 쓸수 있다
		m_sessionData; ...
	}
	virtual void OnDisconnect(uint32_t /*_Reason*/) override
	{
	}
	virtual int OnMessage(CGMSG& /*_MSG*/) override
	{
		return	0;
	}
};


CGCII는 CSocket는 fCallback_OnConnect나 fCallback_OnDisconnect 콜백 함수처럼 어떤 소켓에 일어난 이벤트인가를 인자로 넘겨주는 것이 아니라 해당 객체의 멤버 함수인 OnConnect(), OnDisconnect()함수를 직접 호출하도록 설계했습니다.


위와 같이  CSocket의 멤버 데이터로 호출하게 되면


ISession에 해당하는 클라이언트를 따로 찾거나 관리하는 처리가 필요 없어집니다.  해당 객체의  OnConnect()함수 혹은 OnDisconnect()함수를 바로 호출해 주기 때문입니다.


차이는 이뿐이 아닙니다. 만약 ISession에 해당하는 객체의 SESSION_DATA 정보가 필요하다면 이 역시 찾아 주어야 합니다.
하지만 CGCII처럼 CSocket 객체의 함수를 직접 호출하게 되면 그냥 CSocket의 멤버함수로만 넣어주면 됩니다. 이는 OnConnect()함수나 OnDisconnect()함수에서 따로 찾지 않고도 바로 사용할 수 있습니다.

이 별 것 아닐 것 같아 보이는 이러한 사소한 설계의 차이는 실제 코드 구현에서 코드 량과 간결성에  상당한 영향을 미칠 수 있습니다.

실제 구현에서는 많은 기능들이 복합적으로 동작하기 때문에 사소한 구조의 차이나 인자 단 몇개 때문에 엄청난 코드의 복잡성을 유발하는 경우가 상당히 많기 때문입니다. 


생산성이란 예제로 보여진  단편적인 구현으로는 정확히 파악할 수 없습니다.

여러 상황에 맞춰 직접 구현해 보았을 때 그 설계의 편의성을 알수 있게 됩니다.

 CGCII는 단편적으로 보여지는 코드의 간결성이 아니라  구현 과정의 편리성도 최대한 고려되어 설계되 었기 때문에 CGCII로 구현했을 때 전체적인 구현 과정에서 간결함과 효율적 코딩이 가능하도록 해줍니다.


이론적인 설계와 실제 적용해서 사용할 때는 많은 차이가 있습니다. 
아무리 완벽하게 생각되는 설계도 막상 적용해서 사용해보면 불편하거나 미쳐 생각하지 못한 사소한 내용들이 많기 때문입니다.
CGCII는 직접 엔진을 사용해 대규모 서비스들을 개발해 보며 오랜 실무 경험을 통해 엔진을 다듬어 왔습니다.

또 아주 작은 편의성의 향상을 위해 아무리 많은 작업이 필요하다 해도  마다하지 않고 개선해 왔습니다.


CGCII는 사용상 편리함의 추구를 위해 최선을 다해 왔음을 자부하고 있습니다.