2010년 8월 30일 월요일

패킷 제네레이터 기본 컨셉

저번에는 SendPacket 함수까지 알아보았다

이제는 받기 함수를 소개할 차례인데

참 어려운 고민이 많았다

일단 서버를 생각한다면  IOCP , select, epoll , kqueue 등등  다양한 소켓이 존재했고
그리고 소켓을 캡슐화한 다른 형태의 라이브러리까지 생각한다면 ,
그런것을 고려해 코드를 생성하기는 매우 어려운 일이었다

게다가  Send쪽은 SendPacket은  void* 와 size 를 인자로  하나의 함수만 어플리케이션쪽에서
정의해 주면 끝났지만

받는쪽은 반대로 void* 와 size 를 받아
패킷을 분석해  PT_INTERFACE로 정의되어 있는 구조체를 인자에 맞게 함수를 호출해줘야 했기때문에
더더욱 쉽게 자동화 하긴 어려웠다

그래서  가장 덜 손이 갈수 있겠다 싶은 방법을 생각해 보았다

먼저 해야할것이

 소켓에서 데이타를 받은 데이터를 stream에 입력하고 enumID에 맞게 PT_INTERFACE에 정의한
구조체로 변환 하는 작업이다

예를 들면 이러한 함수 일것이다

// 이함수는 AC_LogIn 이 데이터가 어떻게 쓰일지 모르기에 , 선언만 되어 있음
void ProcessPacket(SOCKET_ID socketID, const AC_LogIn& ptData);

void RecvPacketAC_LogIn(SOCKET_ID socketID,int8* pBuffer,uint32 bufferSize)
{
AC_LogIn ptData;
StreamReaderSelfBuffer stream(pBuffer,bufferSize);
stream & ptData;
ProcessPacket(socketID,ptData);
}

여기서 보면  내부에서 ProcessPacket 함수가 다시 호출되는데 이것은 스트림에서 구조체로
변환한다음  이 구조체를 사용하는 함수를 호출해주는 역활이다

이 구조체로 무엇을 할지 모르기때문에  이 함수의 구현은 어플리케이션 단에서 해줘야한다
그러므로 패킷 생성기에서 생성된 코드를 보면  선언만 되어 있고 구현은 없다

마치 SendPacket를 어플리케이션 단에서 다시 정의해줬던것과 같은것이다
따라서 저러한 함수를 어플리케이션 단에서 구현해줘야 한다  

자 그럼...  어떻게 저러한 함수가 호출하도록 만들것인가를 살펴보자

일단 저함수를 호출할수 있도록 Map에 담아 가져오기로 한다
enumID 를 키로하고   void (SOCKET_ID ,int8* ,uint32 ) 의 함수포인터를 가지는 Map 가져오기로 하는것이다

void GetFuncMap_gene_recv_PT_AC_Data(RecvPacketMap& ptRecvMap);

그럼 이제 이 Map 담긴 함수를 호출해야 하는데   아주 간략하게 코드로  보자면 이렇게 될것이다


// 스트림을 구조체로 바꿔 준후   구조체를 사용하는 함수를 호출해주는 Map얻어오기
// 자동 생성된 코드에서 이 함수가 선언되어 있다
GetFuncMap_gene_recv_PT_AC_Data(g_funcRecvMap);


//------------------------------------------------------------------------------
// 소켓으로부터 버퍼 얻어오기  ( 간단히 만든거므로 딴지 금지 )
// 전체 크기 얻어 오기
uint32 totalSize;
ptEnumType enumID;
int recvSize = recv(g_sockfd,(char*)&totalSize,HEADER_SIZE,0);
if(recvSize <= 0)
 break;

// enum 얻어오기
PG_ASSERT(recvSize == HEADER_SIZE);
recvSize = recv(g_sockfd,(char*)&enumID,PT_ENUM_TYPE_SIZE,0);
if(recvSize <= 0)
 break;

PG_ASSERT(recvSize == PT_ENUM_TYPE_SIZE);

// 데이타 버퍼 얻어오기
char recvBuf[MAX_PACKETLEN];
uint32 ptSize = totalSize - PT_ENUM_TYPE_SIZE;
recvSize = recv(g_sockfd,recvBuf,ptSize,0);
if(recvSize <= 0)
 break;

PG_ASSERT(recvSize == ptSize);
//------------------------------------------------------------------------------


// 위 루틴으로부터  enumID 와 버퍼  버퍼크기를 얻어올수 있었다

try
{
// check enumID valid
RecvPacketMap::iterator it = g_funcRecvMap.find(enumID);

if( it != g_funcRecvMap.end())
g_funcRecvMap[enumID](g_sockfd,recvBuf,ptSize);
}
catch(StreamExceptionUnderflow* )
{
// 패킷 Stream Underflow
}

g_funcRecvMap   함수포인터 맵에 enumID를 넘겨주면
위에서 스트림을 구조체로 변환해주는  함수포인터를 넘겨 받게 된다

그리고 그 넘겨 받은 함수에  버퍼와 크기를 넘겨주면 자동으로  
( g_funcRecvMap[enumID](g_sockfd,recvBuf,ptSize); )

위에서 말한
void ProcessPacket(SOCKET_ID socketID, const AC_LogIn& ptData);
이러한 함수가 호출된다

단 패킷 생성기에서는 선언만 만들기 때문에 구현은 어플리케이션단에서
정의해주어야 하는것이다

자동으로 많은것을 만들려주다보면  소켓 구조에 의존성을 가지게 되어
재활용율이 떨어지게 되므로  이정도까지가 최선이라 생각이 되었다

필요하다면  코드를 수정해 더  필요한만큼 만들어내면  될것이지만
가장 최적으로 재활용 될만한 수준에서 공개하는게 바람직할거 같아 여기까지 해주는 것으로
일단락 하였다

참 그리고 패킷 제네레이터가 해줘야 할 중요한 작업이 하나더 있는데
PT_INTERFACE의 내용 구성물이 바뀔때마다 ( 멤버 데이터의 순서 바뀜 혹은 추가 삭제 )  
버전업을 시켜 줘야 한다 그래서 버전이 다른 서버/클라이언트 간에 통신을 막아주어야 한다

예전 XX 게임에서는 빌드 할때마다 버전업을 시켜 , 클라이언트만 빌드해도 버전업이되어  
서버를 내리고 다시 버전업 시킨 서버를 올려야하는 시스템이 있었다

프로토콜이 바뀌지 않는 이상 그럴 필요는 없으므로 버전은 프로토콜이 바뀔때만 올라가야한다
따라서 패킷 제네레이터는 PT_INTERFACE의 스트림에
담는 구조가 바뀌었는지 파악하기때문에
자동으로 프로토콜 버전업 관리도 하게 된다  
----------------------------------------

 다음버전에는 좀더 최적화된 버전으로 코드를 생성할수도 있게 옵션을 추가 할 예정이다
 
-------

참고 사항 :

스트림 클래스를 boost의 serialization을 쓰려 했으나 너무 무겁고 ,  파일 쓰기에 중심을 맞춘거 같아
스트림 코드를 재작성하였다  ,  그리고 제가 주말에만 취미로 작업을 하는데
일요일 회사 문이 잠겨 있어 T_T    문서화 작업을 끝내지 못해 공개를 조금 미루게 될거 같다


2010년 8월 27일 금요일

패킷 생성기가 왜 필요한가?

저번 글에서   전통적인 두가지 패킷 처리 방법을 보았다

두 방법다 장점과 단점이 존재 하며  ,  완벽한 방법은 없어 보인다

하지만 자연스럽게 합쳐질 수 있는 방법은 없을까?

안타깝게도  두 방법은 공존할수 없는 방법이다 , 하나는 컴파일타임 기반 코드이고
하나는 런타임에 기반코드이기에 공존해 쓸수가 없는것이다

솔직히 근본적으로 C++ 언어차원에서는 해결할 방법이없다
그래서 수많은 프로젝트에서 이 전통전인 방법들을 고수하고 있는게 아닌가?

이쯤 되고 보면 , 이렇게 된거 하나를 선택하고 단점을 줄일 방법을 연구하는게 빠를거 같다

"남들이라고 별수 있겠어?" 라고 속편히 생각하면  이게 가장 현명한 생각인것인 같다는 생각도
드는게 인지상정이다

하지만  구조체를 선택해도 가변적인 처리때문에  코드량 및 복잡도가 높아지고
스트림 처리를 선택하더라도  패킷이 복잡하고나 양이 많아지면 마찬가지로 코드량이
많아지고 복잡해지는건 매한가지다

"나와 내 동료들은  실수 따윈 하지 않아!" 라는 가정하더라도 결국 많은 양의 단순 노가다에 앞에는
장사가 없다. 노가다는 인간의 본성을 거슬리는 일이기때문이다
(공산주의가 망한 해묵은 이유를 설명하진 않아도 되겠지)

"그냥 몸으로 때워!"의 마인드를 가진 팀장이라면 , 팀원들이 생각하며 생산적인 일을
하기보단  노가다하는 시간이 많을것이다
(새벽2-3까지 매일같이 해도 답 안나오는 프로젝트가 아직도 기억이....)

게임 디자이너너 마저도  "단순 노가다는 죽기보다 싫다"며 프로그래머에게 자동화를 요구해
오는 광경은 어렵지 않게 볼수 있다
[ 심지어 맥스 스크립트로 자신의 작업을 자동화해버리는 디자이너도 있다 ]

결론적으로 말해 인간의 본성을 거슬르는 노가다로 프로젝트를 운영하게 되면
프로젝트가 제대로  돌아갈리가 없다

근데 잠시만 디자이너가 자동화?를 요구한다고?

갑자기 드는 생각으로 디자이너의 편의를 위해 자동화 처리를 해주면서  정작 왜 자신의 일인
코드는 자동화 시키지 못하는지 의아스럽기만 하다

굳이 변명을 하자면   C++언어밖에 모르는 입장에선 C++로 디자이너의 일은 자동화 해줄수 있지만
정작 자신은 C++언어의 문법에 묶여 C++ 언어에서 할수 있는거 밖에 생각할수 없지 않은가?
 
그래도 미약하지만 C언어의 표준 전처리기인 메크로를 써 조금이라도 더 편하게 쓸수 있지 않을까라는
생각이 든다

"오 메크로! 그럴듯한데?" 단순 반복작업을 어떻게든 줄여줄수 있는것처럼 느껴진다
메크로로 인해 코드가 어려워 보이겠지만  이 노가다만 줄일 수 있다면 영혼이라도 팔겠다는 심정으로
#define Oh_My_God_Packet  ....
#define Damn_it  ....
#define WASTE_OF_TIME....
이런 코드를 짜기 시작하지만  끝도 없이 정의해야 될것들이 생겨난다.

게다가  이건 내가봐도 외계어가 된 느낌이다 ,
메크로를 정의해 패킷 처리 노가다를 줄여보려 외계어 정의에 더 스트레스 받는 느낌이
이건 뭔가 아니다 싶다

역시 없쩔수 없이  노가다를 인정하겠다는 마음으로  패킷 처리 코드를 짜지만
 , ctrl + V 를 한 코드에서 버그가 다시 발생하자  , 마음이 약해진다

그러는 문득! 실용주의 프로그래머에서 읽었던  자동화가 떠오른다

내용이 좋아 3-4번을 읽었으면서 막상 왜 그 자동화를 실천하지 못했을까?
[물론 그 책은 5년전에 읽었고 이 생각은 4년전부터 해왔다, 생각은 하고 있었으나 역시 실천이 T_T ]

실용주의 프로그래머 원칙
코드를 작성하는 코드를 작성하라

오오라 바로 이것인것이다
[신대륙을 발견한 콜롬버스의 달걀 깨기 처럼 C++ 문법의 한계를 넘어서는게 중요하다]

혹시 누군가 이런걸 만들어 놓지 않았을까 하는 마음에  구글에서 검색을 해보지만
C++에서 맘에 드는 방식으로 바로 쓸수 있는건 드물어 보였다

구글 코드에서 XML로 정의해 C++ 코드를 생성해 내는게 있었지만
XML로 복잡하게 정의해야 했고 , XML 정의가 직관적으로 바로 눈에 들어오진 않았다

그리고 그외 비슷한 것들을 보았지만 내맘에 쏙드는게 없었다

마치 fix_vector를 만들때 기분이랄까?

역시 직접 만들어야겠다는 생각에  프로토콜을 직관적인  구조체로 정의하고  그 코드를 파싱해서
stream 에 입출력하는 코드를 작성하는 생성기를 만들었다

그토록 원한  고전적인  패킷 처리 방법인 구조체 처리 방법과 스트림 처리의 장점을 동시에
가져갈수 있게 된것이다

분명 글 앞단에선 이것이 불가능하다면서  지금은 어떻게 가능하다고 하는 이야기일까?

다시 설명하면
전 처리 단계때  구조체를 보고  send recv  함수를 만들어 낸다면 (제대로 작성했따면)
컴파일 타임 체크 자체가 필요가 없고 ,  send recv 함수 내에는  구조체의 내부 데이터를
스트림에 담아 소켓 버퍼에 보내는 함수를 호출한다면 스트림의 장점을 다 취하는것이다

예를 들어 보겠다
// 파싱의 편의상 및  프로토콜이라는 직관성을 위해  struct 메크로로 재정의 했다

-----------------------------------------------------------------------

#define PT_INTERFACE  struct
#define PG_VECTOR     std::vector

// 이것이 패킷 정의다
PT_INTERFACE CS_LOG_IN
{
    String id;
    String password;
    PG_VECTOR<String> itemList;
};
-----------------------------------------------------------------------

"이건 구조체 방법이 아닌가?" 싶은데  가변 크기를 지정하는 vector가 들어가 있다
vector를 봤을적엔 구조체 방법도 아닌것고 그렇다고 스트림 방법도 아닌거 같다

자 이걸 보고 만약 이런 코드를 생성한다면 어떨까?

void SendPacket(SOCKET socket,void* pBuffer,uint32 bufSize);

void SendPacket(SOCKET socket , const CS_LOG_IN& packet )
{
        // 이 스트림 클래스엔 String과 Vector를 처리할수 있는 operator << 가 정의 되어 있다
PacketStream stream;
stream << packet.id;
stream << packet.password;
stream << packet.itemList;
SendPacket(socket,stream.GetBuffer(),stream.GetSize());
}

감이 있다면 바로 느꼈을것이다.  바로 이거라고....

구조체로 패킷을 정의하고 , 코드 생성기로 컴파일 타임체크의 역활 이상을 대신하며
스트림의 장점을 가지는 이런 코드 생성..... 아 내가 원한것이 바로 이런것인것이다

참고로
void SendPacket(SOCKET socket,void* pBuffer,uint32 bufSize)  이 함수는
선언만 되어 있고 구현은 없다  

이유는 프로젝트마다 선택되는 소켓의 처리 방식이 다를수 있으며
암호화가 추가될수도 있기에  특정 코드의 종속성을 방지 하기 위해
어플리케이션 코드단에서 정의를 해줘야 하기때문이다

예를 들면
void SendPacket(SOCKET socket,void* pBuffer,uint32 bufSize)  
{
//간단한 XOR 암호
XOR_Encode(pBuffer,bufSize);
// 현재는 간단히 버클리 소켓을 쓴다
send(socket,(char*)pBuffer,bufSize,0);
}

 패킷 생성기가 유연하고 독립성을 가지려면  위와 같이 어플리케이션 종속적인 코드를
피해야 재활용 가능한 패킷 생성기를 이용할수 있게 된다

to be continue...

2010년 8월 26일 목요일

Packet Generator 패킷 생성기

온라인 프로그램 패킷의 전통적 처리 방법 ( 장 단 점 )

온라인 프로그램을 짜는 프로그래머라면   누구나 프로토콜을 정의해 패킷을 만들고

클라이언트와 서버간에 통신하는 모듈을 만들어보았을것이다

그리고 이것은 일찍감치 프로그래머들에게 의해  2가지 패턴으로 정형화 되었는데 .....

첫째로는  구조체를 이용하는 방법이고 두째로는 스트림 클래스를 이용하는 방법이다

잘 이해가 안갈수 있으니 예를한번 살펴보자.

첫번째 방법: 구조체 이용하기

struct HEADER
{
int type;
int size;
}

enum {ID_CS_LOG_IN = 25 };

struct CS_LOG_IN : public HEADER
{
CS_LOG_IN() { type = ID_CS_LOGIN; size = sizeof(*this); }
char id[32];
char password[32];
};


이렇게 struct를 정의하고 해당하는 enum값을 정의 한다음
struct 의 생성자에서  필요한 값을 넣어준다

그다음  내부 내용을 복사해 채워 넣은후 소켓함수로 데이타를 보내는게 일반적인것이다

CS_LOG_IN packet;
strcpy(id,"myID");
strcpy(password,"mypassword")

SendPacket(socket,(char*)&packet,sizeof(CS_LOG_IN));

받는쪽도 그냥 통째로 받아 헤더의 type를 보고 CS_LOG_IN 으로 캐스팅 처리하면 된다


장점 :
1. 대단히 직관적이고 쉽다.
프로토콜 패킷정의를   구조체로 볼수 있고, 구조체는 누구나 쉽게 바로 파악할수 있기 때문이다

2. 직관적이기 때문에 고전적으로 많이 쓰였다. 따라서 누구나 쉽게 소스를 구할수 있고
편하게 접근 할수 있다

3. 컴파일 타임때 구조체 크기가 결정되므로 패킷의 버퍼 오버플로우를 컴파일 타임때 체크할수 있다

4. 구조체 내용이 바뀌면  컴파일타임때 패킷을 쓰는 부분에서 에러를 내뱃어주므로
   컴파일때 패킷처리의 오류를 고치기 쉽다

단점 :
1.  일단 구조체 이므로 바이트 정렬에 신경 써야 한다.
컴파일러는 기본 정렬이 4-16 사이에  정의 되어 있으므로
#pragma pack(1) 같은  컴파일러에 영향주는 옵션을 줘야 하고
저걸 제대로 안했을경우 (클라이언트/서버) 구조체 데이터가 서로 호환이 안될수도 있기에
바이트 정렬 자체를 신경써야 한다는거 자체가 부담스럽다

2. 1번과 비슷한 문제지만 서버와 클라이언트의 플레폼이 다를 경우 CPU의 (빅/리틀)엔디안
때문에 호환이 되지 않는 경우가 발생할수도 있다.  Marshal(마샬) 같은 작업이 필요할수가 있는것이다

3. 구조체는 컴파일 타임때 크기가 결정되므로  항상 최대 크기의 데이터를 보내야한다
예를 들면  유저가 채팅방에 0-100명까지 들어갈수 있다고 보자
입장할때 실제 방안에 있는 사람 숫자만큼만 데이터를 줄수 있어야 하지만
구조체는 항상 최대 크기인 100명을 소켓에 출력할수 밖에 없다

따라서 런타임시에 , 가변적인 크기에 대응하기 위해선
꽁수를 써야한다(이건 설명을 생략),  하지만  근본적으로 구조체 방법은  컴파일타임때 결정되는
구조이므로 꽁수로써 쓰다보면  프로토콜 자체가 점점 복잡해지며 처리 방법이 지저분해진다
패킷이 가변적인지 아닌지를 신경 써야 하는것이다.  


결론 :

구조체 방법은 나름 편리하기도 하지만 가변적인 문제는 치명적이다

따라서 네트웍 부하를 고려해야하는 온라인 게임에는 일반적으로 적합하지 않다

하지만 꽁수를 씀으로써  극복하려는 프로젝트를 상당히 많이 보았으며 상용 게임에서도

꽤 많이 쓰이곤 한다. 하지만 꽁수로 극복해야 하는 것들이 발목잡으며 상당히 거슬리곤 한다

익숙해지면 괜찬다고 하지만  ..........


결론적으로는  네트웍 대역폭을 별로 신경 쓸 필요 없는 프로젝트에서 대부분 많이 쓰인다

------------------------------------------------------------------------------------------------

두번째 방법: 스트림 클래스 이용하기

c++ 프로그래머라면 누구나  std::cout<<"hello world"<<std::endl;  프로그램을 짜봤으리라
의심치 않는다

하지만 누구나 스트림의 대표격인 cout를 쓰면서도 스트림 클래스을 제대로 이해하고  활용하는 사람들은
일반적으로 많진 않은 편이다 , 나 역시 스트림을 그리 즐겨 사용하진 않는다

그렇지만 패킷 처리에 있어서만큼은 필수라 할만큼 꼭 필요한 방법인데 예를 한번 보자

(스트림 클래스의 구현은 인터넷에 많이 있다 모르는분은 찾아보자)
class PacketStream
{
 // value to buffer
  PacketStream& operator << ( int  value );
 // buffer to value
  PacketStream& operator >> ( int  value );

        -------------------------------------------
 char m_buffer[1024];
}

enum { ID_LOBBY_USER_LIST = 26 };

struct User
{
 char id[32];
 int    flag;
}

--------------             보내는쪽             ------------------------

// 서버에 있는 유저 데이타 리스트
std::vector< User > m_serverUserList;

PacketStream stream;

// 먼저 ID를 입력해주고
stream << ID_LOBBY_USER_LIST;

// 가변 크기의 입력
stream << m_serverUserList.size();
for(size_t i=0; i < m_serverUserList.size(); ++i)
{
 // 갯수만큼 내용을 스트림에 입력
 stream << m_serverUserList.id;
 stream << m_serverUserList.flag;
}

SendPacket( socket, stream.GetBuffer(), stream.size() );

--------------------------------------------------------------------------

--------------             받는 쪽              ------------------------

PacketStream stream;

// 소켓으로 부터 복사된 버퍼를 받아 stream에 입력한다
stream.CopyBuffer( socketBuffer, socketSize);

// id 출력해 파싱한다
int  type;
stream >> type;

switch( type )
{
case ID_LOBBY_USER_LIST:
{
std::vector< User > m_clientUserList;
size_t userCount;
  stream >> userCount;
m_clientUserList.resize( userCount );

for(size_t i=0; i < m_serverUserList.size(); ++i)
{
stream >> m_clientUserList[i].id;
  stream >> m_clientUserList[i].flag;
  }
  }
}
--------------------------------------------------------------------------

참고 : stream에서 STL를 지원하게 만들면 stream 작업이 상당히 간편해진다


장점:
1. 구조체의 가장 큰단점인 고정크기로 부터 자유롭다

2. 고정크기에 자유롭다는것만으로 온라인게임에서 가장 적합한 크기의 패킷을
만들어 낼수가 있다

3. stream 클래스의 도움(
마샬링)
을 받으므로
 
구조체에서 나온 바이트 정렬과  cpu 엔디안구조에
자유롭다


단점 :  

1. 런타임때 크기를 알수 있기때문에  런타임시에 버퍼 오버플로우와 언더플로우를 체크해야한다

2. 정확히  보내는 쪽과 받는쪽이 짝을 이루어야 한다

실수로  한쪽만 고치던가 다르게 고치면  컴파일 타임때는 잡아낼수 없고

런타임때  스트림이 꼬여 크래쉬를 내거나  눈에 띄는 버그를 발생 시킬때나 인식할수 있으므로

실수는 치명적이다.

[ 예를 들면 보내는쪽과 받는쪽의 타입 크기가 다르게 실수로 설정하던가 순서가 바뀌던가 하는... ]

이러한 실수는 적당히 스트림이 꼬여버려 애매한 버그를 만들어 버리고
마치 잘못된 메모리 참조 마냥 ......  충격으로  다가오곤 한다.
[차라리 바로 크래쉬를 내주면 고맙다]

게다가 반복 노가다적인  패킷 설정 작업은 구조체 설정에 비해 상당히 불편한 편이며
패킷의 복잡도에 따라 나름 상당한 노가다 이기에 복사 붙여넣기를 무조건?하게 유발한다


또한 구조체에 비해 직관성이 떨어진다

따라서 작업을 하다보면  스트림 꼬이는 버그는 근본적으로 피하긴 어렵다

결론
:

가변적 크기를 다루는 내용이 많고 , 네트웍 대역폭 낭비가 적어야 하는 온라인 게임이라면
스트림 방법은 선택이 아니라  필수다

구조체 방법과 비교하자면

구조체방법에서는 꽁수로 구조체에 제한된 스트림 방식을 적용하므로 프로토콜 처리
복잡도가 올라간 반면

스트림 처리는 깔끔은 하지만 컴파일타임때 체크는 포기해야 하므로  버그가 괴롭다

그 버그와,  편리함 때문에,  온라인게임에서  프로토콜이 복잡해지는것을 감수하고도
 꽁수가 추가된 구조체 방법을 많이 이용하게 되는것이다
[하지만 꽁수를 볼때마다  스트림 방식이 근대적인 방법임을 확신하게 된다]

비율은 조사를 해보진 않았으나 회사를 옮기는곳마다  의외로 구조체 방법을 고수하는곳이 많았다


하지만  구조체와 스트림 방식의 장점을 모두 취할수 있고 , 단점은 날려 버릴 방법은 없을것인가?

그래서 생각한것이 바로 패킷 생성기다


지면 관계상 Packet Generator 이야기는 다음으로....




2010년 1월 15일 금요일

이런 의사 보셨나요? 하우스!


얼마전에 헬스장에서 일이다.

런링머신위에  뛰면서  TV를 보는데   Fox 채널에  하우스 라는 괴짜 의사을 소재로 한
드라마가 하고 있는것이었다  [지금 전편을 구해 보는중이다]

내용을 보면 , 하우스라는 의사가   괴질에 걸린 환자를 , 디버깅?을 통해  환자의 진짜 병이 무엇인지
알아내 치료하는것이었다

마치 프로그래머가  전혀 예측 못하고  , 잡기 힘든  황당한  버그에 맞서 [꼭 멀티쓰레드 디버깅 같은 ]
디버깅 하려는 모습과 같은 모습인것이다

그는 병의 진짜 이유를 위해  병의 증세를 악화시키기도 하지만  병의 원인을  분할 정복시작하는 모습은
분야는 다르지만  대단히 뛰어난 프로그래머를 보고 있는듯한 착각마저 들었다
[ 칠판에 현재 증상으로 생길수 있는 병들을 모두 나열하고 하나씩 체크해 제거하는 방식이 그중 하나다 ]

하우스를 중심으로 의사들끼리 병의 원인에 대해 토론하는 모습도
버그의 원인에 대해 토론하는 프로그래머들과 다를봐 없는것이었다

어떨때는 병의 증상과 원인이 전혀 다르기도 하는데 ,
C언어로 비유 하자면 ,  
메모리를 잘못쓴 코드는 따로 있는데 단지 그 잘못된 메모리를 정상적으로 이용하는 코드에서
뻑이 나버리는 그런 어려운 문제인것이다
 
그런 문제는  디버거로 보더라도  문제의 원인을 제대로 알기 힘들다

대체로 포인터를 잘못 사용한게 원인인데 , 코드를 짠 사람이 포인터를 어떻게 사용하는지에 대해 체크해
원인을 찾는게 차라리 빠르다
[ 일단 무조건 멀티쓰레드로 돌아가는게 있으면 그것부터 끄는게 도움이 되기도 한다 ]
[혹 메모리 값이 변할때 break가 걸리게 하는 방법도 대안이 되려나?]

그것과 비슷하게   미스테리한 괴질에 대해
하우스는 환자의 몸상태나 증세를 보기보단 , 그의 환경 및 습관
어떤것을 숨기고 있는가를 살펴보기 시작한다

또한 환자나 보호자의 거짓말 혹은 실수의 여부도 집중 추궁하기도 한다.
그리고 거기서 단서를 얻는다
[ 거기서 환자나 보호자의  거짓말들은    프로그래머가 버그에 대한 어설픈 변명? 혹은 거짓말을
지어내는것과 같아 보였다 , 나 역시 버그에 당황하면 어설픈 변명을 늘어놓았던 기억이 난다 ]

심지어 불법으로 환자의 집에  부하?의사들을 침입시켜 , 환자의 현재의 증상이 아닌 , 환자의 환경을
체크하기도 한다

프로그래밍으로 따진다면
애초부터  그런 증세가 유발될수밖에 없는  잘못된 환경(설계)로 인한것인가를 따져보는것을 더 중히 여기는것이다

그리고 드라마 내내 자주 반전이 있는데 , 원인을 알아 치료약을 투여해 치료가 된것처럼 보이지만 결국
다른 증상이 생겨버리던가 , 같은 증상이 재발해버리는것이다

프로그래밍에서보자면 ...
우리가 버그의 원인이 되는 코드를 찾았고  그 코드를 수정해 버그를 고쳤다고 생각했지만
결국  버그가 재발하는 모습과 똑같았다 [진짜 버그가 아닌것이다]


뭔가 통하는게 있지 않은가?  


재밌는것은 죽은 사람도 살려 냈다는 소문난 명의 이야기에서도 비슷한것을 찾을수 있다

위나라의 임금이 편작에게 묻는다.
"그대 삼형제 가운데 누가 제일 잘 병을 치료하는가?

" 큰 형님의 의술이 가장 훌륭하고 다음은 둘째 형님이며 저의 의술이 가장 비천합니다.

임금이 그 이유를 묻자 편작이 대답한 내용은 이러했다.


"큰 형님은 상대방이 아픔을 느끼지 전에 얼굴빛을 보고 그에게 장차 병이 있을 것임을 알아서 그가 병이 생기기도 전에 원인을 제거하여 줍니다. 그러므로 상대는 아파보지도 않은 상태에서 치료를 받게 되고 따라서 그간 자기의 고통을 제거해 주었다는 사실을 알지 못합니다. 큰 형이 명의로 소문나지 않은 이유는 여기에 있습니다.

둘째는 상대방이 병세가 미미한 상태에서 그의 병을 알고 치료를 해줍니다. 그러므로 이 경우의 환자도 둘째형이 자신의 큰 병을 낫게 해주었다고 생각하지 않습니다.

그러나 나는 병이 커지고 환자가 고통속에 신음할 때가 되어서야 비로소 병을 알아 보았습니다. 환자의 병이 심하므로 그의 맥을 짚어야 했으며 진기한 약을 먹이고 살을 도려내는 수술도 했습니다. 그런데 사람들은 나의 그러한 행위를 보고서야 비로소 내가 자신의 병을 고쳐주었다고 믿게 되었죠. 내가 명의로 소문이 나게 된 이유는 여기에 있습니다."


여기서 큰형님의 치료를 보면 애초부터 버그를 유발하지 않겠끔 설계를 한다고 볼수 있겠다

코드 컴플리트(Code Complete)의 저자 스티브 맥코넬도 초보 프로그래머 시절에는  머리에 생각나는데로 짜다가
버그때문에 고생하였고, 중급 프로그래머로 올라오고 나서야  디버깅에 고수가 되었다고 한다
하지만 , 디버깅을 잘하는건 프로젝트을 성공하는데 큰 도움이 되지 않았다고 한다
디버깅을 아무리 잘해도 , 버그가 계속 나오는 구조라면 무슨 의미가 있겠는가?
아예 디버깅을 할 필요가 없는  즉  아예 버그를 유발할 가능성이 적은 설계를 하는게 최고였다는것이다.  

그래서 인지 설계에 통달하지 않으면 쓸수 없는  Code Complete라는 책을  내지 않았겠는가?

여담이지만 ,예전 해커 한분을 본적이 있는데 그는 코드를 짜면  코드를 짜는 그 순간 모든것이 컴파일 타임때
모든 논리가 완벽하게 정리되어야 한다고 생각하신분이었다 , 한마디로 디버거가 필요 없다는 것...
수학처럼 완벽함을 추구하는 분이었다 .   컴파일타임에 모든 물리적(?), 논리적 에러까지 이미 처리가능한 (믿기는 힘들었지만)

구루들은  대체로(?) 디버깅 자체가 필요없는 설계자인것이다

여기서 우리는 의학에서의  진단과  프로그래밍의 디버깅에서 통하는점을 찾을수 있었다
마치 건축과 프로그래밍의 설계와의 공통점을 찾는것과 비슷한 느낌인것이다 [디자인 패턴]

왠지 의학을 공부하고 싶은 생각마저 드는 이 드라마를 통해 무엇인가  배울수 있는 무언가를
찾을수 있을것만 같은 느낌이다 , 물론 재미와 감동 역시 빠질순 없다

아직 이 드라마를 보지 못햇다면 , 꼭 볼것을 추천한다
당신이 프로그래머라면   무엇인가 통하는 느낌을
받으리라......

여담으로

난 아직 버그를 유발하지 않을 최선의 설계는 알지 못하지만 assert로  인자 와 리턴 값 그리고
포인터 접근에 관한것 꼼꼼히 걸어둔다 , 최선의 설계를 알지못하면 이러한 방어적 프로그래밍이라도
잘해두는게 차선은 아닐까라는 생각도 해본다  


2010년 1월 13일 수요일

STL로 범한 죄악

신입때 opengl로 2D 엔진을 개발할때였다.

한글을 찍기 위해 조합형 한글 방식을 취했는데
한글자당 자음 모음 자음 형태로  찍어야 했으므로 글자당 필요한  인덱스가 3개가 필요 했었다

그리곤 , 아무 생각없이 인덱스를 받아  해당 택스쳐 위치를 찍어 한글을 만들어 냈는데...
글짜를 찍는 순간부터 cpu가 무려10%이상 오르는  황당한 경험하게되었다
( 디버깅 모드였던걸로 기억한다, 그래도 터무니 없긴 마찬가지;;)

아무리 봐도 글자가 이렇게 큰 부하가 걸릴거라고는 생각못했는데 , 너무 많이 부하가 걸리는것이었다
의심되는 코드를 찾아보아도 딱히 그럴만한 작업을 한게 없었다

아무리 눈으로 코드를 보더라도 , 눈으로는 확인할수 없어
나름 허접한 프로파일링 끝에 아래와 같은 코드가 모든 악의 근원인것을 알게되었다


// 겉보기에는 문제 없는 코드
void GetXXIndex( std::vector<int>* pIndexVec)
{
     pIndexVec->push_back(1)
     pIndexVec->push_back(2)
     pIndexVec->push_back(3)
}

겉 보기에는 아무런 문제가 없고 멀쩡한 함수처럼 보인다

하지만 저 함수가 일으킨 문제는 로직부하의 30%가 넘을만큼  터무니 없는 수치인것이다

도데체 왜 문제가 된것일까?

일단 가장 핵심적인 사항으로는 글자를 찍는 만큼 저 함수가 호출되므로 , 매 프레임당
수십에서 수백번까지 호출되어야 했다는 것이다
결국  글자가 많아질수록 저 함수 호출은 늘어나는 구조고 , 그 무지막지한 부하가 비례 했다는것이
결정적인 문제였다

하지만 단순히 함수호출이 많다고해서 부하가 크게 걸리린다는건 어불성설이고

일단  가장 상식적인 것으로는 컨테이너에 담길 갯수가 3개라는 것을 알면서도
가변 크기를 담는 vector를 선택한게 문제가 아닐까라는 결론에 이르게 된다 ,

물론 STL에서 권장하는 vector의 reseve 함수를 생략하긴 했다 [이놈의 귀차니즘...]
인지하고 있던 사항이지만  , 그래도 터무니 없지 않나 라는 생각이 들었던것이다

아니 이런 생각부터 잘못됐다 , 3개라는 걸 알고 있었으면 고정 크기 배열을 넘겼어야 하는것이
원칙인것이다

그렇다 정답을 알면서도 대충 vector를 남용한 결과를 다시 확인하기 위해
vector 의 소스를 보며  메모리 할당 정책을 다시 훌터봐야 했다

물론 STL책에서 이미 알고 있던 사항들을 재 확인하는것에 불과 했지만
[
   _Capacity + _Capacity / 2; // try to grow by 50%  
   그래도 작으면 늘어난 size만큼 capcity를 늘림
 ]
즉 현재 크기가 메모리 할당량보다 크면 , 메모리 할당양이  50% 크기가 증가하는 코드였던것이다
[이건 STL 구현마다 다르다]

따라서 3번의 push_back 모두  메모리 재할당이 일어나는 코드가 된것이다
재할당은 new delete를 필연적으로 불러오고 결국 저 함수는 new1[1] delete ,  new[2]  delete ,
new[3] delete 를 반복한 코드와 같다
[ 참고로 어느정도 크기가 되야 , 저런 무식한 할당에서 피할수 있게 된다 ,
예를 들어 100 이면 다음 크기는 150이 되므로 50번의 push_back으로부터  추가적인 메모리 할당을
피할수 있게 된다]

그런데 수십에서 수백번까지 지속적으로 반복해서 저 함수를 불리었으니  
매 프레임당 수백번까지 메모리 재할당를 하게 된것이다
[ 플레폼마다 힙메모리 관리가 다르기도 하고 , 성능 차이가 존재하기도 하기에
  힙관리의 부하의 차이는 당시 플레폼에 의존한다  ]

이 처참한 결과를 확인하고 단순히 vector를 고정 배열로 넘기자  그 무지막지한 cpu 사용량은 거의 사라져 버렸다
 그리고 그 함수의 호출 갯수에 비례해 늘어나던 cpu 소모량은 거의 발생하지 않았다
한마디로 고정배열을 넘기게 함수를 바꾸자    부하가 아예 없는 수준으로 바뀌었다

이 같은 경험으로 힙메모리의 무분별한 사용은  상상 이상의 부하를 초래한다는것을 깨달았고
말로만 듣던 new delete의 폐해를 당시 처음으로 느꼈었다
[그렇다고 , 어느 자료구조든 같은 문제를 앉고 있기때문에 STL만의 문제는 아닐것이다
문제는 역시 쓰는 사람일것이다]


오랜 고민 끝에 이 같은 문제에서도 ,  vector와 같은 인터페이스로 문제를 해결하고 싶은 욕심이났다
그래서 만든것이 fix_vector이다

[fix_vector 자료실 링크가 깨진관계로 다시 여기에 올립니다]

 개인적으로 라이브러리를 완벽하게 만드는건 어렵기때문에  기왕이면 검증된  라이브러리를
가져다 쓰려노력한다 , 하지만  fix_vector 같은건 절대 만나볼수가 없어 , 결국 직접 만들어버렸다

나름 배열이 사용되어야 할곳에 적절히 쓰이면 쓸만한거 같았다



2010년 1월 8일 금요일

STL은 정말 괜찮은 물건인가?


자료구조와 STL를 제대로 공부를 해봤다면 정말  이런 질문 자체가 어이 없게 느껴질지 모르겠다

하지만 현실은 그렇지 않다  , 아직도 종종 꽤 많은 프로그래머입에서  
"STL 따위는  쓰지 말아야 한다"고 주장하는 글이 보이기 때문이다

다시 말하자면 한마디로   "STL 코드를 보면 지저분하고 별거 없는거 같다" 라는것이 그들의 주장인것이다
그리고는 자신이 짠 코드가 STL를 압도적으로 능가한다고 주장하는 경우가 일반적이다

과연 그럴까?

내가 겪어본 봐로는
그들의 주장은 STL의 익숙하지 않거나 자기의 코드에 대한 막연한  우월감 때문인 경우가 대부분이다

좀더 정확하게 말해
아마 STL 코드를  살짝보니 , 템플릿 도배에 지저분해보이고,  
"저정도는 내가 더 깔끔하게 짤 수 있어"  같은  치기어린 감정을 나타내는 주장 밖에 없다

워낙 많은 C++ 커뮤니티에 그런 사건이 있어왔기에 ,  어쩌면 이제는 식상한 이야기 일수 있으나
그래도 그나마 가장 나름의 논리를 가지고 있던 한분이 있어  링크를 걸어본다

gpg study에서  그나마 stl를 대안할 라이브러리를 준비했다고 주장해  장장 15페이지나 되는 토론을
벌였는데 ,  이글에서 각자  결론를 내려볼수 있겠지만  특정한 용도의 라이브러리를
 범용적인 STL에 적용하려니 문제가 되는게 일반적이었다


이부분을 보면 내가 C++의 최적화의 필독서라 불리는 Efficient C++의 내용을 인용하였는데
좀더 이분야의 전문가 글을 인용하는게 도움이 될거 같아  글을 올렸다
혹시라도 C++ 최적화에 관심이 있다면 반드시 읽어봐야 할 책이다
[나 역시 2번은 읽었다]


참고로 위 링크에서 내가 저렇게 길게  토론을 한 이유가 있는데
STL를 깔보는 프로그래머에게  , 쓸때 없는 소리를 더 이상 하고 싶지 않았기때문이다

헛소리를 하면 그냥 저 링크를 걸고 ,  저기 나온 내용을 이해를 하고도  STL를 깔수 있느냐라고
하면  아마 "게임 끝" 이 날테니 말이다

그래도 왠지  STL 험담을 하고 싶다면  제발 자료구조 책과 STL에 대한 책 한권쯤은 읽어봐라

자료구조책에 나와 있는 모든 내용을 C++ 템플릿으로 이렇게 완벽히 소화해낸 라이브러리가
있을까 하며 기쁨의 눈물을 흘릴것이다

또한 구루들이 컴파일 타임때 최대한 최적화한 STL를 만들때 얼마나 많은
고민을 했을까 라는 생각이 들때쯤이면 한층더 프로그래머로써 성숙해질 수 있는 게기가 되리라...

여담이지만
Effective C++  시리즈의 저자 스캇마이어는  More Effective C++ 에서 스마트 포인터에 대해
상당한 페이지를 소모하며 글을 작성하였고 그 라이브러리를 공개도 하였다

정말 멋지게 만들었음에도 , 그가 정말  C++ 바닥에서는 알아주는 구루임에도 불구하고

More Effective C++ 책 다음작품인  Effective STL책 에서  자기가 만든 스마트 포인터를 쓰지말고
boost의 스마트 포인터를 쓰라고 간곡히 권한다

자기도 심열을 기울여 만들었지만 ,  모든 상황을 체크할수 없었고 , 꾸준한 버그 리포팅을 받다가
결국 포기하였다는것이다

그만큼 표준 라이브러리나 검증된 라이브러리를 만들기 어렵다는 것인데 ,
STL 험담하기전에 자신의 실력를 한번 되돌아 보면 좋을거 같다

------------------------------------------------------------------------------
혹 Effective C++ 시리즈 책을 모르는다면  반드시 읽어보아라  
[  나 역시 평균 3번은 읽어보았다 ]
------------------------------------------------------------------------------

다음 편은 fix_vector를 다뤄보겠다

2010년 1월 6일 수요일

C++ 에서 힙메모리를 자동으로 관리하게 코드를 작성하자


아직도  C++은 동적메모리를  new delete로 관리 하여만 한다고 생각하는 사람이 많은것 같다

지금도 남이 짜 놓은 수 많은 코드속에서 new로 생성한 객체를  더미(raw)  포인터에 그냥 담고 ,  
"언제가 delete가 필요할때  완벽하게 delete 해줄수 있을거야" 라는 식의 코드를  보고 있는중이다
(오해는 없길 바란다 누굴 비난하기 위한 내용은 아니다)

이런 설계는 언제나 그렇듯 처음에는 무난한 ??? 설계가 될수 있으나
코드는 항상 수정되어야 하는 경우가 대부분이기때문에 수정할때마다
이 수많은 경우의 수를 다 파악해 delete 코드를 넣어주는 지옥과 같은 경험을 하게 된다

그래서 이러한 코드를 보게되면 ,코드 분석보단   " 아마 메모리 관리가 이렇게 힘들어서야... "
같은  반복된 생각만 든다

"이 중구 난방식의 new속에 , 도대체 delete를 어떻게 완벽히 기억해내어  처리해주지?"

 실용주의 프로그래머 책의 저자가  그러지 않았던가 ,  
'문제는  어떻게  완벽히 기억할수 있는게 아니라, 언제 잊어버릴것인가' 이다 [물론 정확한 인용은 아니다]

알고리즘의 핵심이 아닌상  , 잘잘하고 구차한 문법문제는  
잊어버려도 문제가 없는 코드가 구성되어야 좋은 코드라 할수 있는것이다.

자바가 C++ 프로그래머를 대량으로 흡수할때 1순위로 강조한것이 무엇인가?
가비지 컬랙션이 아니었던가?
쓸때없는 것에 머리 쓰지 않아도 되는건 대단한 강점인것이다

소실적에는 c++에 가비지 콜랙션을 만들수 있지 않을까하는  생각도 해보았으나
이런 당연하고도 고전적인 문제를 C++이 해결을 못했을리는 없다

믿을지 모르겠지만  나는 약 6년전 대학교때 STL과 BOOST를 공부한 이후 단한번도 직접 delete를 작성한적이
없다. 혹!? 모르나 아마 없을것이다
[물론 crt 디버그에서 메모리릭이라 투털거린적 역시 없다]

믿을수 없다고 ??

지금 말한 3가지만 사용한다면  앞으로 99.9%  delete를 직접 할 필요가 없다
1. 스트링
2. 자료구조
3. 스마트 포인터


1.  스트링
c언어 스트링은 가장 고전적이고 다루기 어려운 구조이다
스트링 특성상 가변적으로 크기가 변하는게 대부분이기때문에
동적 메모리를 활용하는 경우가 많고 , 경우에 따라서 관리가 복잡하다

하지만 스트링 객체를 쓰면 대부분 해소 되는데
표준 std::string이 아니더라도  스트링 객체를 활용한다면 구질 구질한 c언어 스트링으로 인한
new char[XX]같은  동적 메모리 사용을 피할수 있다

아직도 문자를 생성할때  new char[XX] 쓰고 있다면 반드시 표준std::string 를 이용해보자
다른 세상을 경험할수 있을것이다

그런데도 굳이 스트링 객체 자체를 new로 생성하는 몰지각스러운 코드를 만나면 답이 안나온다
스트링 객체를 대부분  그렇게 생성되어야 할 이유가 99%이상  없기때문이다

2. 자료구조
C++의 대표적인 자료구조로는 STL이 존재한다
STL이 객체를 다루는 방법이   복사를 이용하기때문에 주로 가벼운 객체를 주로 담게되나
어지간히 무겁지 않는한  복사가  new 로 할당해 담는 비용보다  훨씬 싼편에 속한다
만약 정말로 무겁거나 객체의 다형성을 위해 new를 이용해야 한다면 스마트포인터를 담으면 그만이다
 스캇마이어(Effective STL)가 그리 강조하지 않던가?

컴퓨터 공학생이라면 필수로 읽어봤을 자료구조책에   당골로 등장하는
vector list hash map deque 등 [이름은 조금 다를수도 있다] 을
STL를 이용하면 공짜로 이용할수 있으며
이것은 곧  가면적으로 크기가 변하는 컨테이너를 공짜로 사용할수 있다는 말이다

new를 이용하는 가장 핵심적인 이유중 한가지가 가변 크기때문 아니던가? 그것을 자동으로 STL
컨테이너가 해주기때문에 new를 쓸이 없어진다
[물론 hash나 set, map은 그뿐이 아니라 , 키로 데이터 관리까지 해주니까 두말할 나위가 없다]

그중 new를 이용한 배열의 경우

 vector를 이용하면 C의 배열과 100% 호환되기때문에 그대로 C API와 더불어 운영될수 있으며
컴파일러의 최적화 능력 [STL의 성능은 다음에 다루기로 하겠다]
까지 더해지면 vector의 편리함과 C배열과 같은 성능을  동시에 누릴수도 있는것이다
게다가 수백만명이 검증한 STL 자료구조의 안정성도 더불어 누릴것이다

3. 스마트 포인터
new 직접 사용에 대해 계속해서 부정적인 이야기를 했지만
반드시 직접 힙메모리를 이용해야 하는 경우가 존재한다
다형성이 존재하는 객체를 컨테이너에 담을때 부모형태로 포인터를 저장해 관리 할때는
대부분 new를 피하기 어렵다
그래서 생겨난것이 스마트 포인터이다
[ 물론 객체 크기가 커서 new로 생성해 스마트 포인터에 담는 경우는 예외이다 ]

스마트 포인터의 기본 이념은  new생성된 포인터를  객체가
참조 카운팅(std::auto_ptr 같은것은 제외) 관리해서 유지하는것이다

참조하는 객체가 전부 사라지면 그때에 delete 된다

가장 대표적인게 표준으로도 포함된 boost::shared_ptr이 될것이다 [boost.org의 doc를 참조하라]

기본적으로 가비지 컬랙션을 사용하는 언어들도 내부적으로는 참조 카운팅을 유지하고 있다
[원리는 같다는 것! ,다만 C++은 참조하는 객체가 사라질때 바로 제거 된고 , 다른 언어들은
언어의 가비지 컬랙션 정책에 따라 다를것이다 ]


물론 만능은 아니다
스마트 포인터를 이용할때   내부의 스마트포인가 서로를 포함하는 구조로 객체가 구성될 경우
참조 카운팅이 꼬여 메모리가 해제가 안될수 있는것이다

그것을 위해 weaken 어쩌구 방식의 스마트 포인터도 boost에서 지원하는데
그러한 꼬임은
그것은 C++의 스마트 포인터가 아니라 가비질 컬랙션을 지원하는
어떤 언어에서도  정상적인 방법으로 메모리를 해제할수가 없다

다른 언어에서도 boost의  weaken 어쩌고 같은
방식을 이용해야 한다 , 한마디로  상호 포함관계의 스마트 포인터는  정상인 논리 구조라 보기 힘들다고 생각한다

그것을 이용할 정도면  설계 자체가 이해하기 어려운 구조기때문에
설계를 바꾸는게 정답일것이다

---------------------------------------------------------------------------------------------------
참고로

boost의 shared_ptr은  delete 뿐 아니라 직접 제거자를 지정해 주어
delete 대신  다른 해제 방법을 이용할수 있게 해놓았다
new로 생성하지 않고 create나 destroy를 이용해야 하는 리소스도
대응해서 사용할수 있는것이다

혹 STL에 스마트 포인터를 담으면  참조할때  이터레이터 참조뿐 아니라 스마트 포인터를 참조해야하기에 **iterator를 쓰게되어 가독성이 떨어지 거슬리는분들이 존재할거라 생각된다
나 역시 그랬으니까

그건 boost의     boost/ptr_container/ptr_list.hpp 같은걸 이용하면 된다
부스트의 ptr_containter에는  new로 생성된 포인터를 직접 넣어주면
컨테이너에서 제거 될때 자동으로 delete를 해준다
 
----------------------------------------------------------------------------------------------------


STL 스마트 포인터 알게된지 이후 6년동안 delete 코드를 작성해본적이 없지만
힙메모리를 이용하고도  , delete 코드가 없다하면 아직도 믿지 못하는 사람이 많다

gpg study에서도 비슷한 발언을 했을적에  믿기 힘들다는 사람을 대다수인걸보면
얼마나 많은 사람들이  아직도  힙메모리를 직접 다루고 있는지 느껴지는듯했다


프로그래머라면 정말 중요한 알고리즘을 기억하는데만해도 머리에 담을 용량이 부족하지
않았던가?


다음 편엔 STL 자료구조에 대해 이야기 해보겠다
덤으로 fix_vector도 ~ ^^