윈도우즈를 위한 웹 기반의 XGE 서비스

개발하고 있는 GIS엔진인 XGE에 대한 웹서비스 개발 중간에 정리도 할겸, 글을 올려봅니다. 초반에 방향을 잘못잡아 헤매기는 했지만… 다른 방향을 선택해 개발에 박차를 가하고 있습니다.

윈도우즈 운영체제 기반의 XGE 웹서비스는 IIS를 웹서버로 하는 ISAPI Extension으로 개발 되었습니다. 참고로, 처음에 의도했던 바는 데이터 교환의 표준인 XML을 기반으로 하며 SOAP 프로토콜을 따르는 WebService을 기반으로 개발하려고 하였으나, 1메가 이상의 바이너리 데이터를 Base64 방식으로 XML로 인코딩하는데 소요되는 시간(6초 이상)이 문제가 되어 그 차선책으로 ISAPI를 선택하게 되었습니다.

ISAPI가 최신 기술인 WebService에 비교해 매우 과거의 기술이기는 하지만, 윈도우즈 운영체계에서 웹을 기반으로 하는 서비스 기술중에서는 가장 심플하고 빠른 기술이며, 윈도우즈의 WebService 역시 그 밑바닦은 ISAPI 기술을 이용해 구현되었고, .NET 기술중의 하나인 ASP.NET을 처리하는 모듈 역시 ISAPI로 구현되었습니다.

이러한 상황으로 미루어… 윈도우즈 운영체제 기반이라면, 웹서비스 모듈을 개발하는데 ISAPI를 선택해도 전혀 무리가 없다고 판단되었습니다. 여타 다른, 특히 유닉스 계열의 플렛폼을 지원하지 못한다는 최악의 시나라이오가 있기는 하지만 말입니다.

XGE 웹서비스를 개발할때 고려했던 점은 아래와 같습니다.

  1. 매우 구조가 간단하다.
  2. 어떤 상황에서도 죽지 않는다.
  3. 요청에 대해 빠르게 응답한다.

순서를 뒤집에 이야기 해보면, 요청에 빠르게 응답하기 위해 ISAPI를 선택했으며… XGE의 특성상 지도 데이터를 빠르게 쿼리하는 것이 중요해서, 이미 빠르게 필요한 지역에 대한 데이터를 수집할 수 있도록 개발되어 나름대로 요청에 대해 빠르게 응답(최악의 상황에서도 0.02초 이내)합니다.

그리고 어떤 상황에서도 죽지 않도록 하기 위해 작업 단위를 프로세스 기반으로 적절히 분배했습니다. 즉 ISAPI로 개발한 XGE 웹서비스는 클라이언트의 요청을 받고 이 요청에 대한 데이터 수집을 직접 하지 않고 DataSource라는 별도의 프로세스로 책임을 위임하고 이렇게 수집된 데이터를 다시 받아서 클라이언트로 전송해주는 역활만 합니다. 즉, ISAPI로 개발된 XGE 웹서비스는 다음과 같은 일만을 합니다.

  1. 요청을 받는다.
  2. 요청에 대한 처리를 별도의 프로세스에게 위임한다.
  3. 요청에 대한 처리 결과를 받아 보내준다.

요청을 받고.. 그 결과를 보내주는 것 자체는 ISAPI에 있어서 매우 기본적인 연산입니다. 즉, XGE 웹서비스는 매우 기본적인 연산만을 한다는 것이지요. 이렇게 하면 XGE 웹서비스가 어떤 연산으로 인해 다운되는 일은 거의 없다고 볼 수 있습니다. 별다른 연산은 모두 다른 프로세스에게 위임을 하니까 말입니다. 그리고 연산을 위임받은 DataSource 프로세스는 별도의 실행파일로 만들었습니다. 원한다면 서비스 방식으로 만들어도 되겠지요. 좀더 편리한 UI 제작을 위해 어플리케이션 형태의 실행파일로 만들었고, 이 DataSource가 하는 일은 XGE 웹서비스가 요청한 지도 데이터를 조회해서 바이너리 데이터 덩어리로 묶고(Pack) 다시 XGE 웹서비스로 보내줍니다. 지도 데이터를 조회하는 연산은 내부적으로 다소 복잡한데, 어떤 잘못된 연산으로 DataSource 프로세스가 다운될 수가 있습니다.
위의 그림에서는 언급되지 않았지만, 이럴때를 위해 Monitor 프로세스가 존재하며 이 XGE Monitor 프로세스가 하는 일은 DataSource 프로세스가 살아있는지를 일정한 시간 간격으로 검사하고 죽어있다면 다시 DataSource 프로세스를 살려주는 역활을 합니다. 역활에 따라 적절하게 프로세스로 분리한 이러한 구조로 어떤 상황에서도 죽지 않는다라는 이상적인 목표에 많이 접근할 수 있을 것으로 판단됩니다. 끝으로 XGE 웹서비스는 매우 구조가 간단합니다. 구조가 간단하기에 안정성과 빠른 응답성은 물론이고 확장성의 이점까지 두루 얻을 수있습니다. 일단 위의 그림 역시 매우 간단한 구조인데, 이 그림을 구성하고 있는 XGE 서비스와 DataSource 프로세스에 대한 구조에 대한 그림을 살펴보면, 얼마나 간단한지 알 수 있을겁니다.
위의 그림은 UML 중 Class Diagram입니다. 실제로 개발한 프로젝트에서 사용한 클래스의 모든 것입니다. 물론 기능 확장등으로 몇가지 더 추가되겠지만.. 요청과 그에 대한 처리라는 관점에서 봤을때 새로운 클래스를 추가하는 상황은 최대한 배제하도록 설계가 되었습니다. 이러한 설계의 단순함으로 인해 전체적으로 구조가 간단해졌으며, 앞서 언급한 안정성, 빠른 응답성, 확정성의 이점을 얻을 있겠지요. (참고로 위의 UML은 StarUML로 작성했습니다. 국산 공개 소프트웨어)

당연한거 아냐? -_- 언제부턴가.. 썩… 당연하지 않은…

음력 새해… 첫출근의 시작에서… 열라 코딩하다며보면, 가끔 아래와 같은 문장을 기술한다.

const string& GetValueAsString()
{
   char buf[MAX_PATH];

   // buf에 이것저것 뒤지고 볶고 자시고 먹고~ 하다가....

   return buf;
}

반환값으로 임시 변수를 반환하는 것… 사용하기에 편리하고 융통성이 높은(?) stl의 string이기에 무심코…. 이리 문장을 작성하고는, …. 실행.  반환값을 살펴보면….. 바람과 함께 사라진 buf 값.. 위의 문장을 기술하고 단위 테스트를 거쳤다면 바로 문제점을 파악……

그렇지 않고 다음 프로세스로 넘어갔다면… @1#8$ 대략… 어디메가 문제련가… 하고 고심했을터…. 단위테스트의 중요성과 항상 기본을 잊지 말아야 한다는 깨달음.

방법 아시겠죠? 처음 저는 다소 버벅대긴 했지만….. 한번 쯤 고민해보시길~

VS2005, VS2008에서 VC++을 이용해 ISAPI 개발하기

VS2005에서 새로운 프로젝트 창에 ISAPI 프로젝트 항목이 제외되었습니다. 게다가 VS2008에는 그나마 있던 VC++로 개발가능한 WebService 프로젝트 항목도 빠져버렸습니다. 그렇다고 해서 완전이 빠진것은 아니고 새로운 프로젝트 창에서만 제외되었고, 기존의 프로젝트를 열어서 VS2005와 VS2008에서 작업이 가능합니다. 그렇다면 새로운 프로젝트로 ISAPI 개발을 VS2005 이상에서는 시작할 수 있을까요? ISAPI를 이용한 웹컴포넌트 개발이 비록 시대에 역행하는 행위이기는 하지만 어찌되었든 MS 플렛폼인 IIS 서버 웹기반의 서비스 기술로써 가장 퍼포먼스가 뛰어나므로, 아직 폐기 시키기에는 다소 아깝지 않나 하는 생각이 듭니다.

사실 처음에는 ISAPI를 쓰지 않고 대세인 XML 기반인 웹서비스를 사용하려고 하였습니다. 그 배경은 0.2 메가 정도되는 바이너리 데이터를 웹을 통해 주고 받아야합니다. 문제는 0.2메가 크기의 바이너리 데이터를 웹서비스가 사용하는 XML로 인코딩시킬 경우의 문제점입니다.

  • 원본 크기가 최대 4배까지 커진다.
  • 인코딩과 디코딩하는데 걸리는 시간이 매우 길다.

자세한 내용은 이 블로그의 웹서비스와 퍼포먼스(http://www.gisdeveloper.co.kr/entry/웹서비스와-퍼포먼스)를 참고해보시길 바랍니다. 이런 문제점으로 인해 ISAPI를 이용한 서버기술로 회귀하게 되었는데…. 문제는 Visual Studio 2005와 2008에서는 이 프로젝트가 눈에 보이지 않는다는 것입니다. 다행인것은 아예 제거된것이 아닙니다. 그래서 이 글에서는 간단하게나마 ISAPI를 Visual Studio 2005, 2008에서 개발하는 방법을 간단하게 정리한 글입니다. 여기서는 Visual Studio 2005에서 ISAPI Extension 개발에 대한 내용입니다.

1. 새로운 프로젝트 생성하기

정확히 말해 ISAPI는 일반적인 DLL일 뿐입니다. 여기서 몇가지 함수를 외부로 Export 시켜 IIS가 이 함수를 호출함으로써 서로 통신을 하게됩니다. 그 몇가지 함수가 무엇인지는 아래에 설명하겠습니다.

2. 프로젝트 위저드 설정하기
앞서 설명한 것처럼 응용프로그램 종류는 DLL로 지정하며 일단 빈 프로젝트로 생성합니다.

3. 모듈파일 추가하기
외부로 Export할 함수를 지정하기 위한 파일인 모듈파일을 추가합니다.

4. 모듈 파일이 Export할 함수명 지정하기

LIBRARY	"ChartService"

EXPORTS
	HttpExtensionProc
	GetExtensionVersion
	TerminateExtension

앞서 추가한 모듈파일인 module.def에 3개의 함수를 Export 하는데, 위처럼 HttpExtensionProc, GetExtensionVersion, TerminateExtension 입니다. 각 함수의 역활은 아래서 간단하게 살펴보겠습니다.

5. Export할 함수를 구현할 파일 추가하기
앞에서 언급한, 외부로 Export 할 함수를 구현할 cpp 파일 추가합니다.

6. Export할 함수 구현하기

#include 
#include 
#include 

BOOL WINAPI TerminateExtension(DWORD dwFlags)
{
	// 실제 구현
	return TRUE;
}

BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
	// 실제 구현
	return TRUE;
}

DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pecb)
{
	// 실제 구현
	return HSE_STATUS_SUCCESS;
}

이미 이 글을 읽어볼 요량이셨다면, .. 아마도 .. ISAPI은 한번쯤 개발해보셨다고 생각이되는데요.. 여기서는 일단 이 함수들의 주요목적에 대해서만 간단하게 설명드리겠습니다.

TerminateExtension은 서비스가 IIS로부터 Unload될때 실행되는 함수이고, GetExtensionVersion은 서비스가 IIS로부터 처음 Load될때 실행되는 함수입니다. 그리고 HttpExtensionProc는 궁극적으로 클라이언트가 해당 서비스를 요청할때마다 실행되는 함수입니다. 이 상태에서 컴파일을 하면 하나의 dll이 만들어집니다. 실제로 이를 자신의 관리하고 있는 PC의 IIS에 배포을 해야하는데, 그 과정은 아래와 같습니다.

7. IIS에 배포하기
컴파일되어 생성된 dll을 IIS의 폴더에 복사하기전에 기존의 서비스를 Unload 시켜(복사 전 언로드를 예로 지정)야하고, 복사할 폴더의 이름을 지정하고(가상 디렉터리 이름을 지정) 빌드에서 제외를 ‘아니오’로 지정하면 됩니다.

웹서비스와 퍼포먼스

원래 출처는 http://blog.naver.com/kaizer0720/80042740822 이며, 웹서비스가 바이너리 데이터 전송에 적합한지에 대한 의문점과 적합하지 않다면(적합하지 않다고 짐작했기에) 그 해결방안이 무엇인가에 대해 검색을 하다가 찾은 글입니다. 일반적으로 쉽게 이해할 수 있는 글인데요, 그중에 퍼포먼스에 대한 부분만 간단하게 나름대로 정리해 간추렸습니다.

웹서비스와 퍼포먼스

웹서비스는 SOAP 프로토콜을 사용하므로 SOAP 포로토콜이 가지고 있는 문제를 그대로 가지고 있다고 할 수 있습니다… 해서, SOAP 패킷에 대한 SOAP Envelope에 대한 압축을 푸는 시간과 SOAP이 사용하는 데이터 표현인 XML에 대한 파싱에 매우 많은 시간이 소모됩니다. 이를 정리하면…

  • SOAP 패킷에서 SOAP Envelope에 대한 압축을 포는 것에 많은 시간이 소모된다.
  • XML 파서를 이용하여 SOAP Envelope에 있는 XML 파싱에 많은 시간이 소모된다.

또한 앞에 언급한 XML은 실제 데이터를 기술하는 메타 데이터(Tag Element)가 붙음으로 중복적이며 비효율적이며 최적화에 대한 여지가 매우 적습니다.

특히 앞서 언급한 바이너리 데이터의 경우 SOAP은 원래의 하나의 바이트가 표현할 수 있는 256개(ASCII 코드의 개수)를 XML이라는 텍스트 포맷으로 표현하기 위해 화면상에 표현할 수 있는 것들로만 간추린 64개의 문자로 인코딩을 함으로써 원본의 크기보다 4배로 커지게 된다는 극심한 문제점이 발생합니다. 즉, 데이터의 크기가 커진다는 문제와 64개의 문자로 인코딩하는 연산이 필요로 합니다. 마찬가지로 이러한 문제는 서버측에서만 발생하는 것이 아니라 클라이언트 측에서도 4배로 커진 데이터를 받아 다시 원본으로 디코딩해줘야 합니다. 이를 정리하면…

  • 서버측에서 바이너리 데이터에 대해서 Base64로 인코딩하는 연산이 필요하다.
  • 인코딩 결과 원본의 크기에 대해 4배로 커진다.
  • 클라이언트 측에서 서버가 보낸 데이터를 다시 디코딩하는 연산이 필요하다.

그렇다면 이에 대한 해결책이 무엇일까.. 고민하게 됩니다. 실망스럽게도… 바이너리 데이터에 대한 Base64 인코딩 결과를 압축을 해서 데이터의 크기를 줄여 클라이언트로 보내라는 것이 전부입니다. 그렇다면 데이터의 크기는 줄어드는 장점은 있으나… 서버측에서 데이터를 인코딩하고 다시 압축한 후에 클라이언트로 전송하여 클라이언트에서 압축을 풀고 데이터를 디코딩하는 복잡한 연산을 필요로 하게됩니다. 사실 SOAP의 Envelope에 대한 문제점은 무시하고라도 인코딩과 디코딩.. 그리고 원본보다 4배(평균) 커진다는 문제점이 가장 염려스러운 부분이긴 합니다.

대용량 또는 작은 용량의 바이너리 데이터 전송에 웹서비스가 접합한 기술인지… 여러분은 어떻게 생각하십니까?

ATL 웹서비스에서 바이너리 데이터 전송에서의 메모리 해제

읽어보시고, 답변 아시는분.. 꼭 댓글 부탁드립니다~ ^^

위의 제목을 영어로 한다면….. 음.. The Release of Memory allocated for the Translating Binary Data in the ATL WebService.. 맞나?? ㅎㅎ 틀렸겠지요~ 무척 짧은 영어다보니…

이번주 월요일부터 오늘까지로 해서 ATL에서 웹서비스를 제작하는데 몇가지 문제점을 파악중입니다. 몇가지 문제점 중에 한가지 풀리지 않은 의문이 있는데요. 바로 웹서비스에서 바이너리 데이터를 클라이언트로 전송할때 서버측의 웹서비스에서 바이너리 데이터를 메모리에 할당하는데….. 이렇게 할당을 했으면 메모리에서 해제를 해야함은 당연합니다. 그런데, MSDN에서 제공하는 예제에서는 할당만 있을뿐 해제는 없더군요. 아래는 서버측(서비스부)에서 바이너리 데이터를 전송해주는 Test 함수의 모습입니다.

HRESULT CServiceXGE::HelloDump(ATLSOAP_BLOB *bstrOutput)
{
    bstrOutput->size = 1024*1024;
    bstrOutput->data = 
        (unsigned char *)malloc(bstrOutput->size);
    
    static bool bFlag = true;
    if(bFlag)
        strcpy((char *)bstrOutput->data, "This is HelloDump");
    else
        strcpy((char *)bstrOutput->data, "tHIS IS hELLOdUMP");
    
    bFlag = !bFlag;
    
    return HTTP_SUCCESS;
}

그리고 아래는 서비스를 호출하는 클라이언트에 대한 코드입니다.

    cs_.PerformanceTest();

    ServiceXGE::CServiceXGE *pSvcXGE = new ServiceXGE::CServiceXGE();

    ATLSOAP_BLOB blobDta;
    HRESULT hRet = pSvcXGE->HelloDump(&blobDta);

    char szBuf[20];
    sprintf(szBuf, "size: %ld =>;", blobDta.size);

    cs_.Write(szBuf);
    cs_.Writeln((const char *)(blobDta.data));

    free(blobDta.data);

    delete pSvcXGE;

    cs_.PerformanceTest(FALSE);

클라이언트에서는 free가 있습니다!! 하지만… 서비스의 프로세스와 클라이언트의 프로세스가 별개일뿐만 아니라 서비스와 클라이언트가 동일한 PC일리도 없습니다. 즉, 서비스의 메모리 할당에 대한 해제가 아니라는 것이지요.

다시 돌아가서, malloc를 통해 클라이언트에게 전송할 데이터를 메모리에 할당을 했습니다. 그럼 적절한 시점에서 클라이언트로 메모리에 할당된 데이터가 쓩~~ 날라가겠지요. 이제 다 날라갔다면.. malloc의 짝꿍인 free 함수를 사용해서 해제를 해야는게 상식일겁니다. 하지만 MSDN의 예제나 구글에서 검색을 해봐도 할당만 했을뿐 해제를 하는 경우가 없습니다. 그럼 해제는 웹서비스가 알아서 해주나보다… 하면 될것을, 저는 아무래도 찜찜해서 이것저것 더 찾아보았습니다. 그러나… 결론은 역시 웹서비스가 알아서 해주나보다…로 결론이 날듯합니다. 역시 여전이 찝찝합니다. 사실 곰곰이 생각해보면 클라이언트로 전송해주기 위해 메모리를 할당하고 클라이언트로 전송이 끝나면 이제 할당받은 메모리를 해제해주는 흐름에 대한 개념은 절대 틀리지 않다고 봅니다. 그런데… 문제는 해제를 해줄 시점에 대한 코드부를 찾을 수 없다는 점입니다. 위의 코드에서 return HTTP_SUCCESS; 바로 직전에 free 함수를 호출하면 아직 클라이언트로 전송하지 않은 시점이기 때문에 에러가 발생하구요.. 그래서, 역시 데이터를 클라이언트로 전송할 시점이 내부적으로 깊이 숨겨져 있어, 그 시점에 접근할 수 없으므로 개발자가 메모리 할당은 했지만 해제는 자동으로 하는 것으로 생각이 굳혀졌습니다.

그래서 이번엔 서비스 측의 코드를 수정해 다른 테스트를 해보았습니다.

char buf[1024*1024];

HRESULT CServiceXGE::HelloDump(ATLSOAP_BLOB *bstrOutput)
{
   bstrOutput->size = 1024*1024;
   bstrOutput->data = buf;

   static bool bFlag = true;
   if(bFlag)
      strcpy((char *)bstrOutput->data, "This is HelloDump");
   else
      strcpy((char *)bstrOutput->data, "tHIS IS hELLOdUMP");

   bFlag = !bFlag;

   return HTTP_SUCCESS;
}

즉, 동적 할당이 아닌 정적 할당입니다. 실행해보면 잘… 됩니다. 물론 클라이언트 코드는 그대로입니다.사실 에러가 나주길 바랬습니다. 내부적으로 메모리 할당을 자동으로 해준다면 정적 메모리 주소에 대한 메모리 해제는 에러가 나야 하기 때문입니다. 그러나… 잘 됩니다. 그렇다면 내부적으로 메모리 해제를 자동으로 해주지 않는다는 말인데?? 라는 생각이 듭니다. 하지만 혹시 내부적으로 메모리가 스택에 있는지, 힙에 있는지를 파악해서 스택이면 메모리 해제를 하지 않고 힙일 경우에만 하는것이 아닌가라는 생각이 듭니다.

않되겠다 싶어서, 물 막고 품는 방식으로 확인을 해보았습니다. 방법은 클라이언트에서 수백번 서비스를 요청을 작업관리자의 메모리 사용량으로 확인을 해보았습니다. 물론 서비스는 동적 메모리 방식으로 말이지요. 수백번 요청이면 한 요청당 1MB 씩 할당하므로 수백MB의 메모리 릭이 발생할 것으로 판단했습니다. 결론은 메모리릭이 없다… 입니다. 그럼 역시 내부적으로 자동으로 메모리를 해제해주는구나.. 라는 판단이 옳은것같습니다. 하지만 역시 왠지 찝찝합니다. 그래서 좀더 검색을 해보던중 CodeProject에 좋은 예를 하나 발견했는데, 그 코드를 살펴보니 ATL에 메모리 관리자가 있다는 것을 알았습니다. 그 메모리 관리자를 사용해 코드를 다시 작성하면 아래와 같습니다.

HRESULT CServiceXGE::HelloDump(ATLSOAP_BLOB *bstrOutput)
{
    IAtlMemMgr *pMem = GetMemMgr();

    bstrOutput->size = 1024*1024;
    bstrOutput->data = 
             (unsigned char *)pMem->Allocate(bstrOutput->size);

    static bool bFlag = true;
    if(bFlag)
        strcpy((char *)bstrOutput->data, "This is HelloDump");
    else
        strcpy((char *)bstrOutput->data, "tHIS IS hELLOdUMP");

    bFlag = !bFlag;

    return HTTP_SUCCESS;
}

즉, 메모리 관리자를 얻어와서 이 메모리 관리자를 통해 필요한 만큼의 메모리 덩어리를 할당받습니다. 그리고 할당받은 메모리를 해제하는 역시 없습니다. 아마도 이 메모리 할당자가 알아서 메모리를 해제해주는 특별한 행동을 한다고 생각해도 좋을듯 싶습니다. 이제 위의 서비스에 대한 클라이언트 부분에 대한 코드입니다.

  cs_.PerformanceTest();

  ServiceXGE::CServiceXGE *pSvcXGE = new ServiceXGE::CServiceXGE();

  ATLSOAP_BLOB blobDta;
  HRESULT hRet = pSvcXGE->HelloDump(&blobDta);

  char szBuf[20];
  sprintf(szBuf, "size: %ld =>", blobDta.size);

  cs_.Write(szBuf);
  cs_.Writeln((const char *)(blobDta.data));

  IAtlMemMgr *pMem = pSvcXGE->GetMemMgr();
  pMem->Free(blobDta.data);

  delete pSvcXGE;

  cs_.PerformanceTest(FALSE);

수정된 것은 일단 서비스의 프록시를 통해 서비스의 메모리 관리자를 얻어와서 이 메모리 관리자를 통해 메모리를 해제합니다. 뭔가 딱딱 들어 맞는듯합니다. 위의 메모리 관리자의 메모리 해제하는 함수(Free)는 내부적으로 클라이언트의 메모리를 해제하는 것은 분명하고… 아마도, 또 다른 임무인, 자신의 서비스 프록시를 통해 서비스에게 데이터 전송이 끝났으니 아까 메모리 덩어리를 해제해도 좋다는 메세지를 날리는 것이 아닐까? 라는 짐작을 해봅니다. 하지만 이것은 어디까지나 짐작이고… 사실 솔찍히 말한다면, 이건 아닐거라고 봅니다. 하지만 제가 살펴본 ATL로 만든 웹서비스를 통해 바이너리 데이터를 전송하는 코드 중에서는 가장 세련되어 보입니다. 일단 문제 생기면 그때 다시 파악해 보렵니다.

명확하게 이것은 이렇게 저렇게 된다! 라고 명시된 책의 문구가 있으면 좋겠는데… ATL을 이용한 웹서비스의 개발은 대세가 아닌지라.. 그런 문구를 찾지 못해 이런 추측으로 마무리를 하네요. ^^; 참고로 Visual Studio 2008에서는 ATL을 이용해 웹서비스를 개발하는 프로젝트가 제거된듯합니다. 암만 찾아봐도 2008의 새로운 프로젝트에서는 나오질 않네요. 웹서비스를 만들기 위해서는 .NET을 이용하라.. 인가 봅니다.