ActiveX

ActiveX Server Component with ATL/MFC

디버그정 2008. 9. 8. 16:39

      주의: 아래 내용 중 CSocket 을 사용하는 부분은 잘못되었음. 시간이 나는 대로 고쳐 놓겠지만 MFC CWinThread 의 Message pumping 을 사용하지 않는 프로그램에서는 CSocket 과 같은 Synchronous I/O 를 사용할 수 없다. 고치기 전까지, Server Object 를 만드는 부분만 유의해서 봐 주기 바란다.

  1. 들어가기

      Web 상에서 실행되는 프로그램을 NT 에서 만든다고 하면 대부분 VBScript 을 사용한 ASP 프로그래밍을 떠올릴 것이다. 하지만 프로그램에 필요한 모든 로직을 VBScript 로만 구현한다면 소스 코드가 그대로 드러날 뿐 아니라 실행 속도도 느리게 된다. 물론 VBScript 와 같은 쉬운 스크립트 언어를 사용하여 프로그래밍을 하는 것은 언어가 배우기 쉽고 디버깅이 용이하기 때문에 개발 기간이 단축되는 것은 물론이다. 그러나 어떤 특정한 작업이 스크립트 언어로는 구현하기 어렵거나, 수행 시간이 문제가 된다면, ActiveX Server Component 로 구현하는 것을 고려해 볼 수 있다.

      Component 로 구현하는 것 외에도 CGI 나 ISAPI 등을 고려해 볼 수도 있는데, 이 방법의 단점은 레이아웃을 변경하고자 할 때는 프로그램을 재컴파일해야 한다는 데 있다. 프로그램 본연의 기능인 Business Logic 은 Component 로 개발하고 사용자에게 보여지는 레이아웃, 즉 HTML 코드를 만드는 것은 ASP 에서 스크립트 언어로 작성하는 것이 바람직하다.

      이 글에서는 이같은 Component 를 작성하는데 있어서 가장 손쉬운 방법으로 Visual Studio 의 ATL 을 꼽았다. 또한, CSocket 이나 CArchive 와 같은 편리한 클래스 들을 제공하는 MFC 를 함께 사용하도록 한다. 필자는 MFC 와 함께 사용할 수 있도록 한 CRegExp 라는 Regular Expression 라이브러리를 즐겨 사용하므로 이런 방법을 더욱 선호하게 되었다.

  2. 쉬운 Debugging 을 위하여

      필자는 Visual Studio 6.0 을 사용했는데, 그 이전 버전에서도 그리 틀리지 않게 작업할 수 있으리라고 믿고 진행하도록 한다. 혹 다른 점이 발견되면 필자에게 연락을 해 주면 고맙겠다.

      ActiveX Server Component 는 디버깅을 하기가 매우 어렵다. 왜냐하면 새로 프로그램을 빌드하고 나면 먼저 사용하던 파일에 덮어쓰기를 해야 하는데, IIS 가 그 컴포넌트를 사용중이기 때문에 파일을 삭제하거나 덮어쓰기 하는 것이 불가능하다. 따라서 Web Service 를 중단한 다음 덮어쓰기를 하고, 그리고 다시 서비스를 시작해야 한다. 간단한 Exe 프로그램을 만들 때는 컴퓨터만 좋다면 상수 하나 바꾸고 빌드해서 테스트하고 하는 것이 취향대로 가능하지만, 프로그램을 조금 바꿨다고 해서 서비스를 중단했다가 다시 실행시키는 것은 꽤 귀찮은 작업이다.

      따라서 이 글에서 다룰 방법은 원래의 로직을 담은 프로젝트를 만들고, 그것의 Wrapper 로 두 가지를 두는 것이다. 하나는 원래 만들려고 하는 ActiveX Server Component DLL 이고, 또 하나는 디버깅을 위한 테스트 프로그램이다. 테스트 프로그램을 통해서 충분히 검증을 한 다음 동작이 마음에 들면 그때 가서 DLL 을 빌드해서 덮어쓰면 비교적 수월하게 디버깅을 할 수 있게 된다.

      물론 다른 방법으로 디버깅할 수도 있다. Component 를 만들 때 Server Component 로서만 동작하도록 만들지 않고 'Full Control' 등으로 만든 다음 Visual Basic 에서 호출하여 Trace 등으로 디버깅을 하는 방법도 보았다. 여기서는 Web Page 에서만 사용할 수 있는 가벼운 DLL 로 만들도록 한다.

  3. Workspace 만들기

      이 글에서 만들어 볼 프로젝트는 웹 서버에 접속해서 서버의 종류를 알아내는 프로그램이다. 이 프로젝트를 뼈대로 하여 훌륭한 프로젝트들이 많이 만들어지길 기원한다.

      먼저 Visual Studio 의 File|New...|[Workspace] 메뉴를 사용하여 Workspace 를 만든다. 이름은 편의상 AxSComp 로 한다. 그 다음 Workspace window 의 Workspace 'AxSComp': 0 project(s) 라고 나온 부분에 마우스 오른쪽 버튼을 누르고 Add New Project to workspace... 를 선택한다. 프로젝트 종류는 맨 아래에 있는 Win32 Static Library 로 하고 이름은 Comp 로 하자. MFC 를 사용할 것이므로 MFC 사용에 체크를 한다. 빠른 빌드를 위해 Precompiled Header 도 체크한다. 물론 하드디스크가 부족하다면 안 해도 된다.

      Workspace window 를 ClassView 로 전환하고 Comp classes 에 마우스 오른쪽 버튼을 눌러 New Class... 를 선택하자. 클래스 이름은 CServerDetector 로 한다. 이 클래스가 앞으로 ASP 에서 불러 사용할 클래스가 되므로 이것을 잘 디자인하도록 한다. 멤버와 상수를 다음과 같이 정의한다.

      ServerDetector.h 중에서
         1: class CServerDetector
         2: {
         3: public:
         4:     int Detect();
         5:     CServerDetector();
         6:     virtual ~CServerDetector();
         7: 
         8:     CString m_strServer;
         9:     int m_nPort;
        10: 
        11:     CString m_strWebServer;
        12: 
        13:     enum _error {
        14:         OK, SOCKET_NOT_CREATED, SERVER_UNKNOWN,
        15:         NOT_RESPONDING, UNRECOGNIZED, UNKNOWN
        16:     };
        17: }; 
      ServerDetector.cpp 중에서
         1: int CServerDetector::Detect()
         2: {
         3:     CSocket sock;
         4:     BOOL bRet;
         5:     bRet = sock.Create();
         6:     if (!bRet) {
         7:         return SOCKET_NOT_CREATED;
         8:     }
         9:     bRet = sock.Connect(m_strServer, m_nPort);
        10:     if (!bRet) {
        11:         DWORD nError = GetLastError();
        12:         switch (nError) {
        13:         case WSAEINVAL:
        14:             return SERVER_UNKNOWN;
        15:         case WSAETIMEDOUT:
        16:             return NOT_RESPONDING;
        17:         default:
        18:             return UNKNOWN;
        19:         }
        20:     }
        21: 
        22:     CString strReq(_T("HEAD / HTTP/1.0\n\n"));
        23:     sock.Send((LPVOID)(LPCTSTR)strReq, strReq.GetLength());
        24:     // 에러 체크는 생략한다.
        25:     // 위에 Connect 실패와 같은 요령으로 에러 코드를 리턴한다.
        26: 
        27:     m_strWebServer = _T("");
        28: 
        29:     CSocketFile sf(&sock);
        30:     CArchive ar(&sf, CArchive::load);
        31: 
        32:     CString str;
        33:     try {
        34:         while (ar.ReadString(str)) {
        35:             if (str.Left(8).Compare(_T("Server: ")) == 0) {
        36:                 m_strWebServer = str.Mid(9);
        37:             }
        38:         }
        39:     } catch (CArchiveException e) {
        40:         // EOF 일 때 Exception 이 발생하므로 catch 한다.
        41:         // Error 체크는 생략한다.
        42:     }
        43: 
        44:     if (m_strWebServer.GetLength() == 0) {
        45:         return UNRECOGNIZED;
        46:     }
        47:     return OK;
        48: } 

      생성자에서 m_nPort 는 80을 기본 값으로 가지도록 하고, MFC Socket 을 사용하므로 stdafx.h 에 #include <afxsock.h> 를 넣는 것을 잊지 않도록 한다.

  4. Debugging 용 MFC Application 만들기

      자.. 이제 기본 기능이 만들어졌다. 이 기능을 테스트하기 위해 Dialog-based MFC Application 을 만들 차례다. Workspace 의 FileView 에서 Workspace 'AxSComp': 1 project(s) 라고 되어 있는 부분에 마우스 오른쪽 버튼을 누르고 Add New Project to workspace... 를 선택한다. Project 탭의 MFC AppWizard (exe) 를 선택하고 Project name 은 AxMFCTest 라고 하자. 다음 단계에서 Dialog based 를 선택하고 다음 단계에 Windows Sockets 를 체크하자. 그 다음 단계에서는 As a statically linked library 를 선택하고 Finish 로 간다.

      다이얼로그는 다음과 같은 모양이 되도록 수정한다. 세 개의 에디트 컨트롤을 각각 CString m_strServer, int m_nPort, CEdit m_out 의 이름을 부여했다. 두 버튼은 OK 와 Cancel 의 Caption 만 수정한 것이다.

      CAxMFCTestDLG::OnOK() 를 다음과 같이 Override 한다.

      AxMFCTestDlg.cpp 중에서
      void CAxMFCTestDlg::OnOK() 
      {
      	CWaitCursor cursor;
      	UpdateData();
      
      	CServerDetector *psd = new CServerDetector();
      	psd->m_strServer = m_strServer;
      	psd->m_nPort = m_nPort;
      	psd->Detect();
      	CString strWebServer = psd->m_strWebServer;
      	m_out.SetWindowText(strWebServer);
      	delete psd;
      	//CDialog::OnOK();
      }

      ServerDetector.h 를 include 하고 이를 위해 다음 세팅을 한다.

          Project
            -> Project Settings
              -> Settings For: All Configuration,
                -> AxMFCTest
                  -> C/C++ 탭
                    -> Preprocessor
                      -> Additional include directories: ..\Comp
                -> Comp
                  -> General 탭
                    -> Microsoft Foundation Classes: Use MFC in a Static Library
            -> Dependencies...
              -> Select project: AxMFCTest
                -> Dependent on: Comp (check)
         
      또한, CAxMFCTestDlg::CAxMFCTestDlg() 의 AFX_DATA_INIT 에서 m_nPort 는 80 을 초기값으로 가지도록 한다.
  5. Debugging 하기

      프로그램을 실행시켜서 서버 이름에 www.microsoft.com, 포트에 80 을 넣고 Detect 해 보면, Web Server 란에 icrosoft-IIS/5.0 라는 문자열이 나올 것이다. 그렇다. 필자는 버그를 미리 숨겨 둔 것이다. OnOK() 에 break point 를 걸어 놓고 디버깅을 하자. 물론 이런 과정을 통하지 않고서도 똑똑한 여러분들은 CServerDetector::Detect() 에서 웹서버 이름을 받아 오는 부분에 문제가 있음을 알게 될지도 모른다. str.Mid(9) 라고 되어 있던 부분을 str.Mid(8) 로 수정하자.

      이제 원하는 프로그램이 완성되었다 :)

  6. ATL Wrapper 만들기

      여기까지는 쉽게 하던 대로 하면 되었지만, 이제부터는 처음 하는 부분일지도 모르니 신경써서 잘 따라오기 바란다.

      자.. 다시 Workspace 의 FileView 에서 Workspace 에 새로운 프로젝트를 추가하자. Project 의 종류는 ATL COM AppWizard 이고 이름은 WebServerDetector 라고 해 보자. 나중에 ASP 안에서 Server.CreateObject("Appname.Classname") 을 호출할 때 Appname 이 되는 부분의 이름이다. 다음 페이지에서는 DLL 을 선택하고 Support MFC 를 체크한다.

      컴포넌트가 되기 위한 기본적인 파일들이 생성되었다. 프로젝트가 만들어진 다음 Project | Dependencies... 메뉴에서 Comp 에 의존하도록 설정하고 나면, 이제 아까의 Classname 에 해당하는 것을 만들 차례이다.

      Workspace window 를 ClassView 로 바꾸고 방금 생성된 WebServerDetector classes 에 오른쪽 버튼을 눌러 New ATL Object... 를 선택한다. Category 는 Objects, Objects 는 ActiveX Server Component 를 선택한다.

      다음 단계에서 Short Name: 에 Detector 라고 입력해 보자. CDetector, IDetector 등등의 이름들과 파일들이 자동으로 생성되는 것을 볼 수 있다. 다음으로 넘어가기 전에 ASP 탭에 들어가 보면 필요한 Object 를 체크하는 부분이 나온다. 필요하면 사용하는데, 지금은 모두 끄자. OnStartPage/OnEndPage 는 체크인 상태로 둔다.

      먼저 이 CDetector 라는 서버 객체에 위에서 구현한 CServerDetector 를 넣자. 헤더에 멤버로 CServerDetector *m_psd 를 넣고, 생성자에서는 NULL 로 초기화해 주며, OnStartPage 에서 할당받고, OnEndPage 에서 delete 해 준다. 만일을 위해서 할당받기 전에 이미 값이 있으면 delete 해 주고,(또는 할당을 받지 않고) OnEndPage 에서도 delete 하기 전에 NULL 이지는 않은지 확인해 주는 것이 좋겠다. 물론 필요한 파일을 include 하고 Project Settings 에서 Additional include path 를 설정하는 것을 잊지 않는다.

      OnStartPage() 에서는 AfxSocketInit() 를 호출해 주도록 하고, StdAfx.h 에서 afxsock.h 를 include 하도록 한다.

  7. Member 만들기

      자, 이제 Property 와 Method 를 만들어 넣을 것이다. Property 는 CServerDetector 가 가지는 것과 같은 서버 이름과 포트, 웹 서버 이름이고, Method 는 Detect 가 될 것이다. 먼저 Server 라는 Property 부터 만들어 보자.

      ClassView 의 IDetector 에 마우스 오른쪽 버튼을 눌러 보면 Add Property... 가 있다. 여기서 Property 라는 이름의 두 개의 컨트롤이 있는데, 콤보박스에서는 BSTR 을 고르고 에디트 박스에는 Server 라고 입력한다. 그러면 CDetector 클래스 안에 IDetector 구현 부분에 get_Server(BSTR *pVal) 과 put_Server(BSTR newVal) 이 생겼을 것이다.

      MFC 의 CString 에 이미 BSTR 을 지원하는 기능이 들어가 있으므로 쉽게 인자를 전달받을 수 있다. get_Server() 에서도 *pVal = m_psd->m_strServer.AllocSysString(); 이라고 해 주고, put_Server() 에서는 CString str(newVal); m_psd->m_strServer = str; 과 같은 간단한 문장으로 모든 것을 마친다. 물론 m_psd 가 NULL 인지는 확인해 주는 것이 좋으며 여기서는 에러 체크를 생략한다.

      비슷한 방법으로 Port 라는 Property 를 설정하자. Add Property... 에서 콤보박스에 long / 에디트 박스에 Port 라고 하자. get_Port() 는 *pVal = m_psd->m_nPort; put_Port() 는 m_psd->m_nPort = newVal; 이것만으로 족하다.

      다음은 WebServer 인데, 이것은 ReadOnly 이면 되기 때문에 put 함수가 필요 없다. Add Property... 다이얼로그에서 put 의 체크를 끄고 Property Type 에 BSTR 을, Property Name 에 WebServer 를 지정하면 된다. 그리고, 이 Property 를 Default Property 로 지정하자. 지정하는 방법은, ClassView 의 WebServerDetector classes 아래에 IDetector 가 있고 그 아래의 WebServer 를 클릭해 보면 WebServerDetector.idl 파일이 나오는데, WebServer Property 에 [propget, id(3), helpstring... 와 같이 씌어 있다. 여기서 id 를 3에서 0 으로 바꾸어 주면 이 Property 가 Default Property 가 된다. 구현은 get_Server() 와 같은 방법으로 한다.

      다음은 Detect() 라는 Method 를 만들어 넣자. 구현이 어렵지는 않으니 VARIANT 도 익힐 겸 리턴값은 VARIANT 로 하도록 하자. Add Method... 에서 Method Name 에 Detect 라고 하고, Parameters: 에 [out, retval] LPVARIANT pvRet 라고 입력한다. 그리고 구현은 다음과 같이 한다.

      Detector.cpp 중에서
         1: STDMETHODIMP CDetector::Detect(LPVARIANT pvRet)
         2: {
         3:     AFX_MANAGE_STATE(AfxGetStaticModuleState())
         4: 
         5:     VariantInit(pvRet);
         6: 
         7:     int ret = m_psd->Detect();
         8:     if (ret == 0) {
         9:         pvRet->vt = VT_I4;
        10:         pvRet->lVal = 0;
        11: 
        12:         return S_OK;
        13:     }
        14: 
        15:     CString strMsg;
        16: 
        17:     switch (ret) {
        18:     case CServerDetector::SOCKET_NOT_CREATED:
        19:         strMsg = _T("-1; Socket Not Created");
        20:         break;
        21:     case CServerDetector::SERVER_UNKNOWN:
        22:         strMsg = _T("-2; Unknown Server:");
        23:         break;
        24:     case CServerDetector::NOT_RESPONDING:
        25:         strMsg.Format(_T("-3; Server %s / Port %d Not Responding"),
        26:             (LPCTSTR)m_psd->m_strServer, m_psd->m_nPort);
        27:         break;
        28:     case CServerDetector::UNRECOGNIZED:
        29:         strMsg = "-4; Cannot Recognize Web Server Name";
        30:         break;
        31:     case CServerDetector::UNKNOWN:
        32:         strMsg.Format(
        33:             _T("-5; Unexpected Error. Object Error: %d, System Error: %d"),
        34:             ret, GetLastError());
        35:         break;
        36:     }
        37: 
        38:     pvRet->vt = VT_BSTR;
        39:     pvRet->bstrVal = strMsg.AllocSysString();
        40: 
        41:     return S_OK;
        42: }

  8. Build, Install, Test

      Set Active Configuration 에서 ReleaseMinDependency 라는 것을 선택했는데, ReleaseMinSize 와의 차이는 이름으로 짐작할 뿐 정확히는 알 수 없었다. 혹시 다른 방법으로 해결한 사람이 있으면 연락 바란다.

      주의할 것 중 하나는, 세 개의 프로젝트에서 모두 MFC 를 Static Link 로 하라는 것이다. 물론 Dynamic Link 로 해도 되지만 MFC 를 사용하는 양도 많지 않은데 굳이 무거운 MFC DLL 을 붙여서 배포할 필요는 없기 때문이다. 어쨌든 세 개의 프로젝트가 모두 같은 형태가 아니면 Linker 가 Duplicate 어쩌구 하는 에러 (LNK 2005) 를 내게 될 것이다.

      IIS 가 설치되어 있는 시스템으로 WebServerDetector.DLL 을 이동한 다음 regsvr32.exe 를 이용해서 등록한다. 이때 주의할 점은 DLL 을 바탕 화면에 놓고 등록하지 말라는 것이다. 바탕 화면에 두면 나중에 객체를 생성하지 못한다.

      만일 다시 빌드할 일이 생긴다면 제어판->서비스 에서 IIS Admin Service 라는 것을 중지시킨 후 덮어쓰면 된다. 덮어쓴 다음에 또 regsvr32 를 할 필요는 없다. 덮어쓴 다음 World Wide Web Publishing Service 를 다시 시작해 주면 변경된 것을 테스트해 볼 수 있다.

      Detector.cpp 중에서
      <%
          Set sname = Request("server")
          Set port = Request("port")
          If port = "" Then
              port = 80
          End If
      %><HTML>
       <HEAD><TITLE>Web Server Detector</TITLE></HEAD>
       <BODY>
       <FORM METHOD=get ACTION='<%=Request.ServerVariables("SCRIPT_NAME")%>'>
        Server Name:
        <INPUT TYPE=text NAME=server VALUE='<%=sname%>'>
        Port:
        <INPUT TYPE=text NAME=port VALUE='<%=port%>' SIZE=3>
        <INPUT TYPE=submit VALUE='Detect'>
       </FORM>
      <%
        If sname <> "" Then
          Set wsd = Server.CreateObject("WebServerDetector.Detector")
          wsd.Server = sname
          wsd.Port = port
          ret = wsd.Detect
          If ret = "0" Then
              suc = "successful"
          Else
              suc = "false"
          End If
      %>
      <P>
      Server ( <%=wsd.Server%> : <%=wsd.Port%> ) : <%=wsd.WebServer%>
      <P>
      Return Value: <%=suc%>, <%=ret%>
      <P>
      
      <%
        End If
      %>
      
      

  9. Upgrade

      위와 같이 프로그램한 것은 WSDetect.asp 에서 확인해 볼 수 있다.(주: 서버를 옮기면서 테스트 페이지는 삭제됨)
      이런 작업은 ASP 에 기본으로 들어 있는 기능만으로는 구현할 수 없는 것이다.

      필자는 위에 적은 것에서 약간을 더 수정한 것을 올려 두었다. Connection Refused 를 추가하였고, Unknown Error 일 때는 Win32 Socket 에러 종류를 리턴하도록 했다. Wrapper 를 잘 만들어 두면 에러 처리까지 일관되게 볼 수 있을 것이다.

      한 가지 더. 위에서 WebServer 라는 것을 Default Property 로 두었다. 따라서 ASP 페이지 안에서 <%=wsd.WebServer%> 라고 한 대신에 그냥 <%=wsd%> 라고 해도 된다.

  10. Acknowledgement / License

      위와 같은 프로그램을 하고 이와 같은 글을 쓸 수 있도록 도와 준 김신모, 이현주, 용지현, 한승훈, 박종범님께 감사드립니다. 또한, 소스 출력 부분은 RSP 1.3.1k 의 Export to HTML 기능을 사용하였습니다.

      이 문서의 저작권은 저(김기용)에게 있으며, 이 문서를 상업적으로 사용할 수 없습니다. 누구라도 자유롭게 배포/인용할 수 있으며, 수정한 경우 원저자, 수정한 사람, 수정한 부분을 명시해 주어야 합니다.