PENreport 네트워크 패턴을 이용한 채팅 서버 구현 1 네트워크 패턴 오버뷰 OPENreport 디자인 패턴이란 이미 많은 이들이 고민했던 문제에 대한 해결책을 제시하고 이를 정리한 것을 말한다. 대부분의 프로그 래머들이 디자인 패턴에 관심을 가지고 이를 학습하면서 이용하고 있지만, 아주 기본적인 패턴만 알고 있을 뿐 특정 도메 인 관련 패턴은 쉽게 접하지 못하는 것이 현실이다. 네트워크 패턴을 이용한 채팅 서버 구현 첫 번째 시간은 네트워크 서 버 개발에 도움을 주는 기초 패턴에 대해 배워보도록 하자. 연 재 순 서 1회 2006. 04 네트워크 패턴 오버뷰 2회 2006. 05 Reactor를 이용한 채팅 서버 구현 3회 2006. 06 Proactor를 이용한 채팅 서버 구현 연재 가이드 운영체제 윈도우 98/2000/XP, LINUX, FreeBSD 개발도구 비주얼 C++ / GCC 기반지식 네트워크 응용분야 네트워크 패턴 프로그래밍 강대명 charsyam@hanmail.net 코드 한 줄을 더 보는 것보다 재미난 소설 한 편을 더 읽는 것이 프로그래머답다고 생각하는 만 4년차 프로그래머. 현재 자신의 부족함을 한없이 느끼면서 다시 한 걸음 한 걸음 전진하고 있다. 현재 파이널데이터에서 컴퓨터 포렌식 분야를 연구하고 있다. 대부분의 프로그래머들은 일반적인 GOF 패턴과 달리 네트워 크 관련 패턴은 쉽게 접근하지 못한다. 네트워크 관련 패턴이 일 반적인 GOF 패턴보다 규모가 크고 이해하기 어려울 것이라고 생각하기 때문이다. 이제부터 네트워크 관련 핵심 패턴들을 하나 씩 알아보자. 네트워크 패턴 오버뷰-태초의 패턴 첫 번째 단계, 소규모의 개발 회사가 있다고 가정하자. 여기서 독자는 프로그래머로 대표의 지시에 의해 웹 서버를 개발하기로 하고 대표와 대화를 시도한다. 독자 : 원하는 스펙을 말씀해주십시오. 대표 : 윈도우에서만 작동하면 되고 다중 사용자를 고려할 필요 는 없네. 일반적인 웹 서버면 되네. 대표와의 대화가 끝나고 독자는 <표 1>과 같은 스펙 문서를 작성 한다. <표 1>의 스펙을 토대로, <리스트 1>처럼 간단한 코드를 작성 했다. 2단계는 다시 대표에게 보고하는 일이다. 독자 : 대표님, 전에 말씀하신 웹 서버의 기능을 모두 구현했습 니다. 프로젝트 지원 운영체제 개발툴 필요 기능 고려 사항 <표 1> 웹 서버 구축을 위한 스펙 <리스트 1> <표 1>의 스펙을 토대로 작성한 코드 #include <windows.h> 웹서버개발 윈도우 비주얼 C++ 일반적인 HTTP/1.0 프로토콜 처리 다중 사용자를 고려하지 않음(단일 유저) int main( int argc, char *argv[] ) SOCKET acceptor, client;... 중략 client = ::accept( acceptor,... ); ::recv( client,... ); ::send( client,... ); return... micro software 2006+4
네트워크 패턴 오버뷰 (간단한 데모 후) 대표 : 그런데, 지금 생각해보니 단일 사용자 환경은 너무 미흡하 네. 다중 사용자를 지원해야겠네. 그리고 윈도우 외 리눅 스나 유닉스 환경에서 작동했으면 좋겠네. <표 2>와 같은 스펙 문서를 다시 작성한다. 프로젝트 웹서버개발 지원 운영체제 윈도우, 리눅스, 유닉스 개발툴 비주얼 C++, gcc 1. 일반적인 HTTP/1.0 프로토콜 처리 필요 기능 2. 다중 유저 처리 - Select를 사용한다. 1. 다중 운영체제 지원 고려 사항 - #ifdef, 를 이용한다. <표 2> 다중 사용자를 고려한 웹 서버 스펙 대표의 요구 조건을 만족시키기 위해서 <리스트 2>와 같이 코 드를 작성한다. 즉, #if 등을 이용하여 옵션만 바꿔주면 자동으로 해당 운영체제에 맞게끔 컴파일되도록 하는 것이다. 그리고 select를 이용해 다중 사용자를 지원할 수 있도록 수정했다. 그런데 이렇 게 수정을 하고 나니 수정할 때마다 일일이 코드를 변경해야 하 는 문제가 생긴다. <리스트 3>의 코드를 보자. <리스트 2> 다중 사용자를 고려한 코드 #include <windows.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.hh> #include <netinet/in.h> #include <netdb.h> int main( int argc, char *argv[] ) WSAStartup();... int ret; ret = select( 0, &readfs, NULL, NULL ); ret = select( MAX_HANDLE+1, &readfs, NULL, NULL ); for(i = 0; I < MAX_HANDLE; i++) if ( FD_ISSET( sock_, &readfs ) ) recv(sock_, data, size, 0 ); DoHTTP10(data); Lock을 거는 순간마다 EnterCriticalSection이나 Leave CriticalSection을 #ifdef로 감싸 추가해야 한다. 그런데 만약 윈 도우의 Lock을 다른 것으로 바꾼다면 어떤 일이 발생할까. 일일 이 모든 부분을 수정해야 한다. 게다가 특정 한 부분을 잘못 수정 한다면, 굳이 말하지 않아도 어떤 결과를 초래할지 잘 알 것이다. 버그가 발생하더라도 해당 부분을 찾기란 쉽지 않고 새로운 부분 이 추가될 경우 손봐야 할 부분이 점점 더 많아지는 것은 당연지 사다. 그럼 어떤 방법으로 이런 문제를 해결할 수 있을까? 윈도우의 CriticalSection이나 리눅스의 Mutex는 동작 방식이 Lock을 획 득하고 해제하는 과정으로 비슷하다. 이런 부분에서 뭔가 추상적 인 공통부분을 만들어낼 수 있지 않을까. 이런 생각이 떠오른다 면, 다음과 같은 방법을 생각해볼 수 있다. 먼저 CLock이라는 클 래스를 만든다. 그리고 실제 코드에서 CLock 클래스를 사용하 고, CLock 클래스 안의 각각의 컴파일 옵션에 따라서 다르게 동 작하게 한다. <리스트 4>의 CLock 클래스를 보자. <리스트 4>에서 보는 것처럼 CLock 클래스를 이용할 경우 <리스트 3> CRITICAL_SECTION lock; pthread_mutex_t lock;... //Lock을걸때마다 아래 부분이 반복되어야 한다. EnterCriticalSection( &lock ); pthread_mutex_lock( &lock );... LeaveCriticalSection( &lock ); pthread_mutex_unlock( &lock ); Lock의 기능을 수정하면 CLock의 Lock 함수만 수정하면 된다. 새로운 형태의 Lock을 지원해야 하더라도 이 부분만 변경하면 동작하게 된다. 즉, 변경 부분이 최소화되는 것이다. 이런 방식으 micro software 2006+4
open report <리스트 4> class CLock void Lock() EnterCriticalSection( &lock ); pthread_mutex_lock( &lock ); void Unlock() LeaveCriticalSection( &lock ); pthread_mutex_unlock( &lock ); private: CRITICAL_SECTION lock; pthread_mutex_t lock; ; int main( int argc, char *argv[] )... CLock lock; lock.lock();... lock.unlock(); 독자 : 대표님, 전에 말씀하신 웹 서버를 윈도우와 리눅스 환경에 서 모두 동작하도록 수정했습니다. 그리고 다중 사용자를 지원하도록 했습니다. 대표 : 수고했소. 그런데 지금의 기능만으로는 부족한 부분이 많 으니 HTTP/1.1도 추가하세요. 요구조건이 또다시 변경되었다. 스펙 문서를 <표 3>과 같이 수 정한다. 프로젝트 웹서버개발 지원 운영체제 윈도우, 리눅스, 유닉스 개발툴 비주얼 C++, gcc 1. 일반적인 HTTP/1.0 프로토콜 처리 2. 다중 사용자 지원 필요 기능 - Select를 사용한다. 3. HTTP/1.1 처리 고려 사항 <표 3> HTTP/1.1을 지원하도록 프로젝트 변경 다시 코드를 수정해보자. <리스트 5> HTTP/1.1을 지원하도록 수정한 코드 for(i = 0; i < MAX_HANDLE; i++) if ( FD_ISSET( sock_, &readfs ) ) recv(sock_, data, size, 0 ); if( IsHttp10( data ) ) DoHTTP10(data); if( IsHttp11( data ) ) DoHTTP11(data); CLock Client Lock_ Lock() pthread_mutex_lock() Unlock() pthread_mutex_unlock() <그림 1> Wrapper Facade를 적용한 CLock 클래스 로 사용하는 API를 #ifdef, 등의 특정 클래스로 묶어 변경 을 최소화시켜주는 것을 Wrapper Facade 패턴이라고 한다. 특정 클래스 이용으로 수정 단계 최소화 세 번째 단계로 대표에게 수정한 프로젝트의 진행 사항을 보고 한다. <리스트 5>와 같은 코드로 기능을 구현하고 나니, 다시 큰 의 문이 생긴다. 코드가 이벤트를 처리하는 부분과 로직 부분이 서 로 섞여 있다. 과연 이런 방식의 코드 변경은 안전한가. 만약 Select 대신 다른 함수를 사용해야 한다면? 또한, 각 소켓에 따 라 따른 작업이 필요할 수도 있다. 그렇다면 이 문제를 어떻게 해결해야 할까. 이벤트를 처리하는 부분과 로직 부분을 분리한 다면 어떨까. 우선 문제를 정리해보자. 이벤트를 처리하는 부분은 select다. select의 역할은 이벤트에 대해 Demultiplexing을 한다. 즉, 어떤 소켓에 이벤트가 발생했는지를 순차적으로 만들고 알려주는 일 을 한다(여기서 순차적의 의미는 select를 호출하면, FDSET이 라는 구조체에 해당 소켓의 이벤트가 발생했는지의 여부를 순서 대로 검사하여 알 수 있다는 것이다). 로직 부분은 당연히 실제 micro software 2006+4
네트워크 패턴 오버뷰 <리스트 6> typedef SOCKET typedef int EVENT_HANDLE; EVENT_HANDLE; if(fd_isset( i, read_fds ) handletable_->handle_event(); return -1; class EventHandler virtual int handle_event() int ret = recv( handle_, data, size, 0 ); DoHttp10(data); return -1; ; EVENT_HANDLE get_handle() return handle_; EVENT_HANDLE handle_; class HandleProcessor virtual int handle_events() fd_set read_fds; int ret = select( max_handle_size_, &read_fds, 0, 0 ); for( int i = 0; i < max_handle_size_; i++ ) virtual int register_handle( EventHandler *_eventhandler ) if ( IsSet( handles_[_eventhandler->get_handle()] ) ) return 0; handles_[_eventhandler->get_handle()] = _eventhandler->get_handle(); handletable.insert( MyEventHandlerTable::value_type( _eventhandler- >get_handle(), _eventhandler )); return 1; private: std::map<event_handle, EventHandler *> MyEventHandler Table; EVENT_HANDLE handles_[1024]; MyEventHandlerTable handletable_; ; recv 부분과 DoHTTP10 및 DoHTTP11 부분으로 볼 수 있다. <리스트 5>의 해당 부분을 분리한 <리스트 6>의 코드를 보자. EventHandler와 HandleProcessor로 나눠져 있다. 그리고 HandleProcess 안 에 서 select를 이 용 해 이벤트를 Demultiplexing하고, 실제 작업은 EventHandler를 호출하여 처리한다. 이로써 select 이외의 다른 Demultiplexing 메소드를 사용한다면 HandleProcess만 수정하면 되고, 각 소켓에 따라 다 른 작업을 부여하고 싶다면 EventHandler 부분만 수정하면 된 다. 이런 작업에 대한 자세한 내용을 설명하는 패턴이 reactor 패 턴이다. 단순히 reactor 패턴만 배워서는 전체를 이해하기 힘들 지만, 뒤에서 다루는 몇 가지 패턴을 함께 익히면 서로 밀접하게 연관된다는 것을 알 수 있다. 자, 다시 대표를 만나러 가자. 응답성 증가를 위한 방법 독자 : 다중 사용자 처리와 HTTP/1.1 기능을 추가했습니다. (며칠 후) Reactor EventHandler Dispatch +handle_events() +handle_ +register_handler() +handle_event() <<use>> Synchronous Event Demultiplexer +select() <그림 2> Reactor Pattern owns EVENT_HANDLE DoSomethingHandler notifiles +handle_event() 프로젝트 웹서버개발 지원 운영체제 윈도우, 리눅스, 유닉스 개발툴 비주얼 C++, gcc 필요 기능 1. 일반적인 HTTP/1.0 프로토콜 처리 2. 다중 유저 처리 - Select를 사용한다. 3. HTTP/1.1 처리 고려 사항 1. 응답성 증가(thread 사용) <표 4> 응답 속도 개선을 위한 스펙 micro software 2006+4
open report 이러한 문제점도 발생하게 되는 것이다. Worker Thread 1 Worker Thread 2 Worker Thread 3 그렇다면 이 부분은 어떻게 해결해야 할 <<get>> <<get>> 까. 항상 핵심 이유에 대해서 고민하자. 위 Request Queue 에서 문제가 되는 경우는 긴 시간을 요하 는 이벤트를 처리할 때 다음 사용자가 대 <<put>> 기하는 시간이 많다는 것이다. 그렇다면, 실제로 Reactor 부분에서는 발생하는 이 Reactor Socket Event Source 벤트를 공유 큐에 저장하고 다른 부분에 <그림 3> 응답성 증가를 위한 방법 서 이를 처리하게 한다면 문제의 상당 부 분을 해결할 수 있을 것이다. Worker Thread 1 Worker Thread 2 Worker Thread 3 << get >> :: Lock(), Unlock() Request Queue << put >> :: Lock(), Unlock() Reactor Socket Event Source <그림 4> Lock의 사용 <그림 3>과 같은 구조에서는 실제로 Reactor에서는 큐에 데이터를 집어넣기 만 하고, Worker Thread가 큐에서 하나 씩 이벤트를 끌어와 처리하므로, 고른 사 용자 응답성을 기대할 수 있다. 이처럼 이 벤트를 핸들링하는 비동기(이벤트가 언제 발생할지 모르므로) 계층과, 그 이벤트들 을 저장해두는 공유 큐 계층, 그 이벤트를 처리하는 동기 계층으로 각각 나눠서 처 리하는 것을 Half-Sync/Half-Async 패턴 이라고 한다. 이벤트를 핸들링하는 부분 대표 : 경쟁사 제품은 우리보다 훨씬 빠르던데 성능적인 개선이 의 문제는 비동기 계층만 수정하면 되고, 실제 작업을 처리하는 필요하네. 경쟁사 제품 정도의 속도를 지원했으면 하네. 사 부분은 이벤트를 처리하는 동기 계층만 수정하면 되므로 Half- 용자의 의견으로는 바로 결과가 나오지 않는다고 하는군. Sync/Half-Async 패턴을 적용하면 어느 정도는 성능 유지보수 가 편리해진다. 대표의 의견을 반영한 스펙 문서는 <표 4>와 같다. 그렇다면 <그림 3>의 구조에서는 또 다른 문제가 없을까. 공유 큐를 사용하므로 <그림 3>은 <그림 4>처럼 Locking이 꼭 들어가 먼저 경쟁사 보다 성능이 떨어지는 이유를 한번 생각해보자. 야만 한다. 그렇지 않으면 Request Queue에 접근하는 스레드가 이번 프로젝트에서 Reactor를 쓰면서 select를 이용했다. select 여러 개가 되어 여러 스레드가 동시에 Request Queue에 접근한 는 각 이벤트를 Demultiplexing해서, 순차적으로 처리할 수 있 다면 Race Condition이 발생할 수도 있기 때문이다. 도록 해준다. 여기에 문제가 숨어 있다. 예를 들어 5개의 이벤트 그러므로 Reactor에서 Request Queue에 데이터를 집어넣기 가 발생했다고 가정하자. 여기서는 select를 이용해 순차적으로 위해서라도 Lock, Unlock을 해줘야 하며 Worker Thread에서 실행할 수 했다. 그런데 두 번째 발생한 이벤트가 상당한 시간이 데이터를 가져가기 위해서는 Lock, Unlock을 호출해야만 한다. 소요되는 이벤트라고 할 때 2번을 처리하는 동안 3, 4, 5번 이벤 트는 전혀 처리되지 않는다. 따라서 3, 4, 5번째 이벤트와 관련된 <리스트 7>과 같이 Lock을 사용할 때, 현재 Request Queue 사용자는 장시간 기다려야 할 것이다. 게다가 4번 이벤트를 처리 에 아무런 데이터가 없을 경우 Worker Thread에서 데이터를 하는 데도 많은 시간이 걸린다면 5번 이벤트와 관련된 사용자는 가져오려고 한다면 어떻게 될까. 무작정 데이터가 들어올 때까 보다 더 오랜 시간을 기다려야만 결과를 볼 수 있다. 순차적으로 지 기다리도록 구현되어 있다면, DeadLock 상황이 발생한다. 만들어주는 것은 처리하기 쉽다는 장점을 가져다주지만, 반대로 왜냐하면 Lock을 획득하고 무작정 기다리는데, Reactor에서 micro software 2006+4
네트워크 패턴 오버뷰 <리스트 7> Lock, Unlock의 사용 - Reactor - lock.lock(); requestqueue_.put( data ); lock.unlock(); - Worker Thread N - lock.lock(); requestqueue_.get( data ); lock.unlock(); Request Queue에 데이터를 집어넣기 위해서는 Lock을 획득해 야 하므로 더 이상 진행될 수가 없다. 그러므로 이런 문제 역시 해결해야 한다. 해결 방법은 여러 가지가 있을 수 있다. 먼저 현 재 Request Queue에 데이터가 있을 때만 Lock을 걸고 가져가 게할수있다. <리스트 8> Request Queue에 데이터가 있을 때만 가져가게 하는 코드 if (!requestqueue_.empty() ) lock_.lock(); requestqueue_.get( data ); lock_.unlock(); DoHTTP10(data); 다만 <리스트 8>은 Context-Switching이라는 것 때문에 문제 가 생긴다. Race Condition이 생길 가능성이 존재하기 때문이 다. 즉, requestqueue_.empty()를 검사할 때는 큐에 데이터가 존재하다가 검사가 끝난 다음 Lock을 걸기 직전 Context- Switching이 일어나서 다른 스레드에서 큐의 데이터를 가져갈 수 있기 때문이다. 그럼 다음의 코드는 어떨까? <리스트 9> 먼저 인터럽트처럼 현재의 이벤트와 관련해 알려주는 메커니즘 이 필요하다. 윈도우 쪽에는 Event가 존재하고 유닉스 계통에는 pthread_condition_t라는 것이 있다. 동작 원리는 상당히 간단하다. 먼저 put(데이터를 집어넣을 때)는 Lock을 획득하고, 데이터를 큐에 집어넣은 다음 이벤트 객 체에 Notify를 보낸다. 즉, 현재 이벤트가 발생했다는 것을 알려 주는 것이다. 그리고 get 역시 먼저 Lock을 획득하고 현재 데이 터가 비었는지를 검사한다. 데이터가 있다면 해당 데이터를 가져 오면 되고, 데이터가 없다면 현재의 Lock을 풀고 누군가가 큐에 데이터를 넣기를 기다린다(put에서의 notify()가 현재 큐에 데이 터를 넣었다는 것을 알려준다). 큐에 데이터가 들어오면 그때 작 동하면서 Lock을 다시 획득하고, 큐에 데이터를 가져온 다음 Lock을 풀면 된다. <리스트 10>과 같은 코드로 동작하게 된다. <리스트 10> Monitor Object 패턴 T get() mutex_.lock(); if ( empty_i() ) mutex_.unlock(); not_empty_.wait( &mutex_ ); T data = get_i(); mutex_.unlock(); return data; void put( const T &msg ) mutex_.lock(); put_i( msg ); not_empty_.notify(); mutex_.unlock(); lock_.lock(); if (!requestqueue_.empty() ) requestqueue_.get( data ); lock_.unlock(); DoHTTP10(data); else lock_.unlock(); Reactor <<put>> Request Queue +put() +get() <<get>> Worker Thread <리스트 9>는 프로젝트가 지향하는 목표에 만족할 만한 코드 이다. 물론 실제로 이것을 호출하는 부분에서 실패할 경우, 그 부 분을 처리하는 작업이 복잡해진다. 그리고 그때마다 다시 Lock 을 획득했다 풀어주는 것도 귀찮은 작업이다. 이를 해결하려면 Monitor Condition +walt() +notify() <그림 5> Monitor Object 패턴 Monitor Lock +Lock() +Unlock() micro software 2006+4
open report 이런 방식을 Monitor Object 패턴이라 고 한다. Monitor Object는 아주 간단한 Lock 매니저의 역할을 하며, 이전과 가장 크게 다른 점은 DeadLock이 없어지고, get에서 데이터가 들어오기를 기다리는 동안에 put 함수를 통해 데이터를 추가할 수 있다는 것과 큐 밖에서 다른 처리를 해 줄 필요가 없어진다는 것이다. 이로 인해 처리 성능이 좀 더 향상된다. get_i, put_i 등은 Thread Specific Interface Pattern 을 적용한 것인데, 자세한 내용은 다음 시 간에 설명하도록 하겠다. <<uses>> Web Server <<uses>> <<invokes>> <<uses>> Windows NT Operating System +execute_async_operation() Asynchronous Operation +AcceptEx() +ReadFile() +WriteFile() +TransmitFile() EVENT_HANDLE Completion Handler +handle_event() <<enqueues>> <<executes>> <<Demultiplexing & Dispatches>> I/O Completion Port Asynchronous Event Demultiplexer Proactor DoSomethingHandler <<dequeues>> +GetQueuedCompletionStatus() +Handle_events() 비동기 이벤트 처리 독자 : 대표님이 말씀하신 대로 모두 수정했습니다. 대표 : 수고했네. 이제 좀 쉬게나. 독자 : 아, 그런데 좀 더 성능을 개선할 방법이 있을 듯합니다. 조금만 시간을 더 주십시오. 대표 : 그래? 그렇다면 한 번 더 시도해보게나. 이번에는 요구 조건을 변경한 클라이언트가 독자 자신이 되었 다. 여러 가지 패턴을 적용하여 성능과 구조를 개선했으나, 실제 로좀더개선의여지가있다. 항상동기적인 것보다는 비동기적 인 부분이 빠르다. 대신 프로그래밍은 더 어려워진다. 이벤트를 처리하는 Reactor의 경우 프로그램이 동기적으로 동작하게 만들 어주므로, 실제 비동기적으로 데이터를 처리하는 것보다 성능이 약간 떨어지게 된다. 프로젝트 웹서버개발 지원 운영체제 윈도우, 리눅스, 유닉스 개발툴 비주얼 C++, gcc 필요 기능 1. 일반적인 HTTP/1.0 프로토콜 처리 2. 다중 유저 처리 - Select를 사용한다. 3. HTTP/1.1 처리 1. 응답성 증가(thread의 사용) 고려 사항 2. 비동기 이벤트 처리 <표 5> 비동기 이벤트 처리 개선 <그림 6> Proactor 패턴 실제 구현 과정은 복잡하지만 개념상으로 Proactor가 Reactor 가 어떤 것인지 알고 있다면 상당히 간단하게 이해할 수 있다. Reactor에서 각 EventHandler의 handle_event()를 호출하는 것은 사용자였으나 Proactor에서는 그 역할을 Proactor가 담당 한다. 간단하게 설명하자면 select 대신 앞부분은 윈도우에서는 IOCP가 대신하게 된다(유닉스 계열에서는 I/O Signal을 통해 구현할 수 있지만 아직까지 Proactor는 윈도우 계열이 뛰어난 것 으로 판단된다 ). select는 우리가 호출할 때마다 전체를 Demultiplexing하여 하 나씩 실행시켜주면 됐지만 IOCP에서는 해당 이벤트가 발생한 개 개에 대해 비동기적으로 알려준다. 실제로 윈도우에서 IOCP는 가장 좋은 성능을 제공하는 Asyncronous Event Demultiplexer 패턴 중독증 패턴이나 템플릿 같은 것을 처음 배우는 경우 기능이나 효용성에 크게 놀란다. 그래서 모든 일을 할 때 패턴을 적용해야 한다는 생각 때문에 필 요하지 않은 부분까지도 무작정 패턴을 적용하고 보는 사례가 비일비재 하다. 윈도우에서만 돌아가는 프로그램에 다른 운영체제까지 지원하는 코드를 추가하는 것은 전혀 의미가 없다. 그런데 주변에서 이런 상황을 심심찮게 접하게 된다(사실 필자도 자주 그런 문제를 일으키는 사람 중 한 명이다). 이를 패턴 중독증이라고 부르는데, Refactoring이나 모든 책에서 주의를 요하는 Over Engineering을 일으키는 원인이다. 즉, 혼 자 살 집을 고층 빌딩으로 짓는다든지, 닭 잡는 데 소 잡는 칼을 쓴다든 지 등의 문제가 발생한다. 패턴 중독증은 패턴을 제대로 이해하지 못하는 데서 생기는 문제로 보는 것이 옳다. 패턴이 항상 옳은 최상의 해결책은 아니다. 좋은 해결책들 중 하나일 뿐이다. 그리고 패턴에도 장단점이 존재한다. 언제나 항상 좋은 것만은 아니라는 것이다. 프로그램이라는 것은 워낙 다양하기 때문에 특 정 패턴이 여기서는 아주 좋은 해결책이 될 수 있지만 반대로 다른 곳에 서는 단점으로 작용할 수 있다. 결국 진행 중인 프로젝트의 목적과 성향 을 잘 파악하고 자신만의 해결책을 적용하거나, 패턴의 장단점을 잘 이 해하고 사용해도 좋다고 생각될 때 적용해야 한다. 다양한 선택사항 가 운데 하나일 뿐이기 때문이다. micro software 2006+4
네트워크 패턴 오버뷰 라고 보면 되겠다. 그리고 실제로 Half-Sync/ Half-Async 부분 에서 앞부분의 Reactor만 Proactor로 바꿔도 잘 동작한다. 지금까지 간략한 사례를 통해 몇 가지 중요한 패턴들을 살펴보 았다. 맛보기 정도에 불과하지만 나름대로 주의해서 사용한다면 효과는 기대 이상이다. 그럼 이러한 패턴에 대해서 자세히 알아 보자. 패턴의 종류 Wrapper Facade 패턴 Wrapper Facade 패턴을 한마디로 정의하면 Non-Object Oriented APIs를 Wrapping하는 클래스를 만드는 것이다(Non- Object Oriented APIs : 특정 기능을 제공하기 위해 운영체제 수준이나 라이브러리에서 제공하는 API 함수들을 의미한다. Socket I/O 함수라든지 스레드, 동기화 관련 함수들이 이에 속한 다). 단순히 타 운영체제에서 동작시키기 위해 #ifdef와 를 이용하여 하나의 클래스를 만드는 것만이 아니다. Non-Object Oriented APIs를 Wrapper Facade 패턴에 적용해 Wrapping 클래스를 만들면 다음과 같은 장점이 생긴다. Wrapper Facade의 Method를 호출하면 내부적으로는 API Function A와 API Function B를 순차적으로 호출하고 그 결과 를 다시 알려준다. Client <그림 7> Wrapper Facade Application 1 : method() Wrapper Facade Data Method1()... Wrapper Facade MethodN() 2 : FunctionA() <그림 8> Wrapper Facade의 동작 흐름 API Function A 3 : FunctionB() API Function A() API Function B() API Function C() API Function B 간결한 코드가 복잡한 코드보다 개발이나 유지보수가 편리 하므로 안정성이 높다. 코드가 간결하다는 것은 Non-Object Oriented APIs를 직접 사용하는 것보다 한 단계 추상화되어 있 거나, 클래스 생성자나 파괴자가 미리 작업해야 하는 전처리나 후처리 작업들을 자동으로 처리하는 등의 편리성과 효용성을 제 공한다. 다른 운영체제로 포팅이 쉬워진다. low-level APIs를 직접 적으로 사용하면 해당 부분을 모두 수정해야 하지만 Wrapper Facade 패턴으로 클래스를 만들면 해당 클래스만 같은 기능을 하도록 수정하면 된다. 이제 <리스트 11>를 보자. 소켓 주소를 나타내는 sockaddr_in 구조체와 관련해 Wrapper Facade 패턴을 적용하여 만든 CS_INET_Addr 클래스이다. Wrapper Facade 패턴을 적용하는 방법은 다음과 같다. 먼저 low-level APIs를 목적이나, 추상화 정도에 따라 구분 한다. <리스트 11>에서 CS_INET_ADDR 클래스를 보면 htons, gethostbyname, inet_addr 등의 기본적인 API를 볼 수 있다. 소 켓 어드레스 구조체에 Wrapper Facade를 적용하기 위해서는 관련 API를 조사하고 분류해둬야 한다. 프로그램의 유지보수가 쉬워진다. 기능적인 면에서 프로그램을 이해하고, 유지보수가 쉽다. low-level APIs를 바로 사용하는 것보다 클래스화하면 비슷하거 나 같은 목적에 의해 클래스화되기 때문에 이해하고 사용하기가 쉬워진다. 분류한 APIs를 하나의 기능에 맞춰 Wrapper Facade안에 넣는다. <리스트 11>의 get_ip 함수의 경우 도메인 주소를 넘겨주 면 이를 내부적으로 처리하는 인터넷 주소로 바꿔 돌려주는 기능 을 한다. 이에 필요한 API는 gethostbyname, inet_addr 등이 있 다. 이때 필수적으로 병행해야 할 작업은 생성자나 소멸자로 만 들어두면 된다. <그림 7>과 <그림 8>처럼 외부에서는 Wrapper Facade의 외 형만 볼 수 있으므로 안에서 어떤 동작을 하는지는 알 수 없다. <리스트 12>는 소켓 관련 API를 이용해 간단하게 Wrapper Facade로 만든 것이다. micro software 2006+4
open report <리스트 11> CS_INET_Addr 클래스 #ifndef #define CS_ADDR_H CS_ADDR_H SOCKADDR_IN ) ); class CS_Addr enum CS_ADDR_TYPE addrtype_inet, addrtype_unix, addrtype_pipe ; virtual ~CS_Addr() CS_Addr() CS_Addr( CS_ADDR_TYPE _addrtype, int _addr_size ) : addr_type_(_addrtype), addr_size_(_addr_size) int CS_ADDR_TYPE get_addr_size() const return addr_ size_; get_addr_type() const return addr_ type_; virtual const void* get_addr() const = 0; virtual int get_type() const = 0; protected: int addr_size_; CS_ADDR_TYPE addr_type_; ; lass CS_INET_Addr : public CS_Addr CS_INET_Addr( WORD _port, const char *_ip = NULL, int _type = AF_INET ) : CS_Addr( CS_Addr::addrType_INET, sizeof( SOCKADDR_IN ) ) reset(); set( _port, _ip, _type ); CS_INET_Addr() : CS_Addr() reset(); void operator =(const CS_INET_Addr &_rhs) memcpy( &inet_addr_, &_rhs.inet_addr_, sizeof( virtual ~CS_INET_Addr() int get_type() const return inet_addr_.sin_family; const void *get_addr() const return &inet_addr_; void set( WORD _port, const char *_ip, int _type ) inet_addr_.sin_family = _type; if( NULL!= _ip ) inet_addr_.sin_addr.s_addr = get_ip( _ip ); else inet_addr_.sin_addr.s_addr = INADDR_ANY; inet_addr_.sin_port = htons( _port ); void reset() memset( &inet_addr_, 0, sizeof(sockaddr_in) ); protected: SOCKADDR_IN inet_addr_; DWORD get_ip(const char *_ip) struct hostent *phost; DWORD saddr; ; saddr = inet_addr(_ip); if ( INADDR_NONE == saddr ) phost = gethostbyname( _ip ); if ( phost ) saddr = ((IN_ADDR *)phost->h_addr)->s_addr; return saddr; /* cs_addr.h */ <리스트 12> API를 이용한 Wrapper Facade class CS_Socket_IO CS_Socket_IO( const CS_Socket_IO &_rhs ) if ( this == &_rhs ) return; sock_ = _rhs.sock_; addr_ = _rhs.addr_; CS_Socket_IO( const CS_INET_Addr &_addr ) if ( false == Create( _addr ) ) return; CS_Socket_IO() micro software 2006+4
네트워크 패턴 오버뷰 bool Create( const CS_INET_Addr &_addr ) sock_ = socket( _addr.get_type(), SOCK_STREAM, IPPROTO_TCP ); memcpy( &addr_, &_addr, sizeof( CS_INET_Addr ) ); bopen_ = false; if ( -1!= sock_ ) bopen_ = true; return bopen_; CS_SOCKET get_sock() const return sock_; void set_sock(cs_socket _sock) sock_ = _sock; bool send( const char *_data, int _size, int *_sended ) *_sended = ::send( sock_, _data, _size, 0 ); if ( 0 == *_sended -1 == *_sended ) return false; return true; bool recv( char *_data, int _size, int *_recved ) *_recved = ::recv( sock_, _data, _size, 0 ); if ( 0 == *_recved -1 == *_recved ) return false; return true; void close() #ifdef _CS_WIN32_ closesocket( sock_ ); #elif defined(_cs_unix_) ::close( sock_ ); private: CS_INET_Addr CS_SOCKET bool ; addr_; sock_; bopen_; <리스트 13> Wrapper Facade 패턴 적용 예 #define #include #include #include _CS_UNIX_ <stdio.h> "cs_socket_io.h" "cs_acceptor.h" int main( int argc, char *argv[] ) CS_INET_Addr addr( 11111 ); CS_Socket_Acceptor acceptor(addr); for(;;) CS_Socket_IO sock; acceptor.accept(sock); int sended = 0; sock.send("test\r\n",6, &sended); sock.close(); 진다. 또 장대하게 Wrapper Facade 패턴이라 언급했으나 MFC의 CSocket 비슷하게 만들어 쓰는 것처럼, 나름대로 특정 기능을 쓰기 편하게 하려는 목적으로 API를 클래스로 묶는 것도 Wrapper Facade라 할 수 있다. 다시금 강조하지만 패턴이 전 부는 아니다. 패턴이 왜 필요하고 어떤 식으로 문제를 해결해야 하는지, 그리고 장단점은 무엇인지 항상 고민해야만 한다. 아울 러 해결책을 통해 더 좋은 결과물을 만들도록 노력해야 한다. 다 음 호에서는 실제 Reactor 패턴을 비롯한 몇 가지 패턴에 대해 자세히 알아보고, 해당 패턴을 응용하여 간단한 채팅 서버를 만 들자. 정리 이상우 aspen@imaso.co.kr return 1; 이를 적용하면 <리스트 13>과 같이 몇 줄만으로도 아주 간단하 게 네트워크 서버를 만들 수 있다. 단, 소스에 Wrapper Facade 패턴을 적용하지 않는다면 상당히 긴 코드가 될 수밖에 없다. 이처럼 많은 장점을 제공하는 Wrapper Facade 패턴임에도 항상 몇 단계 걸쳐 실제 API가 실행되므로 그만큼 성능이 떨어 참고자료 Pattern Oriented Software Architectur - Volume 2 GOF Design Pattern micro software 2006+4