Search

'ATL'에 해당되는 글 1건

  1. 2008.05.31 BackGroud of COM / ATL - 1

BackGroud of COM / ATL - 1

프로그래밍 2008.05.31 21:16 Posted by 아일레프

최근에 ATL과 COM을 공부하고 있습니다. msdn이나 블로그의 글을 찾아보아도 단순히 "따라하기"식의 방법만 말해주고 있기에 불만이 있었습니다. 그래서 갖은 방법을 써서 ATL에 관련된 책을 구했으니, 그것이 ATL internals와 inside ATL입니다. 특히 inside ATL책의 Chapter2에는 COM기술의 전반적인 배경을 말해주고 있는데 굉장히 많은 도움이 되어서 여러분들에게 나누어주려고 합니다. 아래의 내용은 inside ATL의 Chapter2의 내용을 번역, 정리한 것입니다.

The Component Object Model

여러분들은 대부분 ATL을 배우려고 하는 사람들이고 아마 ATL을 사용한 프로젝트를 하고 있거나 계획중에 있는 개발자일 것입니다. 어떠한 이유든지 이 책 혹은 다른 책의 ATL framework을 배우기 전에 반드시 이해해야 하는 것이 바로 Microsoft Component Object Model(COM)입니다.

대부분의 개발자들이 ATL의 목적을 잘못 이해하고 있습니다. 그리고 ATL이 COM에 대해 배워야 할 시간을 절약해준다고 생각합니다. 이처럼 잘못된 생각은 없습니다. 물론, 당신이 COM의 기본을 잘 이해하고 있다면 ATL은 많은 시간을 절약해 줄 수 있습니다. 하지만 COM의 가장 기본적인 것 조차 모른다면 ATL은 당신을 더욱 더 혼돈의 세계로 데리고 갈 것입니다. 여러분이 이해해야 하는 것은 implementation을 interface와 구분하는 방법과 그 목적에 있습니다. 만약 이것을 이해하지 못한다면 여러분은 결코 효율적인 COM model을 만들 수 없을 것입니다.

여기에서는 Microsoft's binary object model(COM , DCOM)이 어떻게 분산환경에서 잘 동작할 수 있는지 기본적인 개념에 대해 말하고자 합니다. 앞으로 COM interfaces, COM classes, class objects에 대해 다룰 것입니다.

 

The Atoms of COM

Interfaces

interfaces에 대해 이해하기 위해 간단한 시나리오를 생각해 봅시다. 여러분이 어떠한 필요때문에 간단한 스펠링 체킹 모듈을 C++로 작성해야 한다고 합시다. 다음과 같이 SpellChecker 클래스를 구현할 수 있을 것입니다.

// checker.h
struct tagTEXTBLOB {
    unsigned long nSizeIs;
    char* pBuffer;
};

class CSpellChecker {
    static int m_nRefCount; // How many others are using this?
    LPTEXTBLOB m_lpText;    // LPTEXTBLOB is defined elsewhere.
public:
    CSpellChecker(LPTEXTBLOB lpText);
    virtual ~CSpellChecker();
    void CheckIt();
};

// checker.cpp
CSpellChecker:: m_nRefCount = 0;

CSpellChecker::CSpellChecker(LPTEXTBLOB lpText) {
    m_lpText = lpText;
}
CSpellChecker::~CSpellChecker() {
    m_lpText = NULL;
}
void CSpellChecker::CheckIt() {
    // Parse the text blob, looking up each word,
    //  making corrections when necessary.
}

작성해 놓고 보니 만족스럽습니다. 괜찮은 클래스 같다는 생각이 드는 군요. 위 클래스를 활용하기 위해서는 다음과 같이 프로그램을 작성하면 되겠습니다.

// EditView.h
#include "checker.h"
void CEditorView::OnCheckSpelling() {
    LPTEXTBLOB lpTextBlob = GetRawText();
    if (lpTextBlob) {
        CSpellChecker spellChecker(lpTextBlob);
        spellChecker.CheckIt();
    }
}

괜찮군요. 하지만 우리의 목표는 이 것을 모든 사람이 사용하게 하는 것입니다. 나아가 이 클래스를 이용해 다른 사람들이 또 다른 프로그램을 작성하게 하는 것입니다. 어떻게 할 수 있을까요?

 

Try Static Linking

만약 여러분이 스펠링 체크 컴포넌트를 라이브러리로 배포한다면 소프트웨어 업체는 그 라이브러리를 static linking을 통해 그들의 프로그램에 넣을 수 있을 것입니다. 그러나 이 방법은 두 가지 단점이 있습니다.

첫번째는 static-linking 접근이 중복의 문제를 가지고 있다는 것입니다. 만약 여러분이 만든 라이브러리가 너무나 훌륭해서 많은 벤더들이 이 라이브러리를 이용해 많은 소프트웨어를 만들었다고 칩시다. 사용자들은 이 라이브러리를 사용한 3가지의 소프트웨어를 프로그램에 설치했구요. 이 경우에 사용자는 3가지의 스펠링 체커를 그의 디스크에 존재시켜야 합니다. 왜 낭비인지 알겠죠?

두번째 static-linking 접근의 문제점은 지나친 결합도에 있습니다. Static linking은 여러분이 스펠링 체크 기능을 변경하기 전까지만 괜찮게 동작합니다. 만약 여러분이 기막힌 스펠링 체크 알고리즘을 만들었다고 생각합시다. 또는 여러분이 만든 스펠링 체크 알고리즘에 안타깝게도 버그가 있어 수정해야 한다고 생각합시다. static-linking을 사용했기에 사용자는 전체의 프로그램을 다시 빌드해야 합니다. 혹은 전체 프로그램을 다시 다운 해야 할 것입니다. 사용자가 싫어할 것은 두말할 필요가 없습니다. static-linking이 아닌 다른 접근을 생각해 봐야 겠군요.

 

Dynamic Linking to the Rescue

Static linking의 단점을 해결할 수 있는 방법 중 하나는 DLL을 사용하는 것입니다. DLL은 하드디스크에서 호출되기를 기다리는 실행가능한 코드라고 할 수 있습니다. 클라이언트의 코드는 런타임에 DLL의 코드를 load할 수 있고 link할 수 있습니다. DLL은 여러프로그램이 공유하기에 중복의 문제를 해결할 수 있고 프로그램을 업데이트할 경우가 있다고 했을 때 DLL만 변경해주면 되기에 Static Linking 접근의 단점을 모두 해소할 수 있습니다. 위의 C++코드를 DLL로 export하려면 다음과 같이 코드를 작성하면 됩니다.

class _ _declspec(dllexport) CSpellChecker {
    static int m_nRefCount;
    LPTEXTBLOB m_lpText; // LPTEXTBLOB is defined elsewhere.

public:
    CSpellChecker(LPTEXTBLOB lpText);
    virtual ~CSpellChecker();

    void CheckIt();
};

차이점은 class를 선언할 때 __declspec(dllexport)를 사용한다는 것 밖에 없습니다. 여러분이 class를 이 방식으로 export할 때 모든 function과 static data member는 DLL's export list에 추가되게 됩니다. CSpellChecker 클래스를 사용하기 원하는 사용자는 단지 위 헤더파일을 그의 source 코드에 포함시키고 스펠링체크 dll이 사용가능한 path에 있는지 확인만 하면 됩니다. 정말 쉽군요. 하지만 다른 문제는 없을 까요?

The Downside of Exporting C++ clases

여러분이 위 스펠링 체크 클래스를 MS Visual C++을 사용해 만들었다고 합시다. 그리고 DLL로 배포했구요. 이 때 MS Visual C++를 사용하는 사용자는 여러분의 DLL을 활용할 수 있습니다. 그러나 만약 사용자가 Inprise 버전의 C++을 사용한다면 아쉽게도 그 사용자는 여러분의 DLL을 사용할 수 없습니다. 왜 그런걸까요?

C++의 강점은 type-safe linking이라는 것에 있는데요, C++ 클래스가 object code로 될때 클래스 멤버 function의 이름들은 mangled됩니다. mangled된다는 것은 이름이 function의 return type과 signature등의 정보로 장식(decorate)된다는 것입니다. 이렇게 하는 이유는 사용자의 코드와 object 코드가 안전하게 linking 될 수 있게 하기 위함입니다. 이 것을 type safe linking이라고 부릅니다. 그런데 문제는 Compiler 벤더들이 모두 통일된 방식으로 mangle을 하고 있지 않다는데 있습니다. 다음은 그 예입니다.

Symantec's Mangling

??0CSpellChecker@@QAE@PAX@Z
??1CSpellChecker@@UAE@XZ
??4CSpellChecker@@QAEAAV0@ABV0@@Z
??_GCSpellChecker@@UAEPAXI@Z
??_RCSpellChecker@@QAEAAV0@ABV0@@Z
?CheckIt@CSpellChecker@@QAEXXZ
?m_nRefCount@CSpellChecker@@0HA

Microsoft's Mangling

??0CSpellChecker@@QAE@ABV0@@Z
??0CSpellChecker@@QAE@PAX@Z
??1CSpellChecker@@UAE@XZ
??4CSpellChecker@@QAEAAV0@ABV0@@Z
??_7CSpellChecker@@6B@
??_ECSpellChecker@@UAEPAXI@Z
??_GCSpellChecker@@UAEPAXI@Z
?CheckIt@CSpellChecker@@QAEXXZ
?m_nRefCount@CSpellChecker@@0HA

Inprise's Mangling

@CSpellChecker@$bctr$qpv
@CSpellChecker@$bdtr$qv
@CSpellChecker@CheckIt$qv
@CSpellChecker@m_nRefCount

각 벤더마다 mangle하는 방식이 다르다는 것을 위에서 알 수 있습니다. Inprise로 빌드된 DLL과 Microsoft로 작성된 사용자의 코드와 적절히 링크되지 않는 이유는 이 이유 때문입니다.

Solving the Problem with Ordinals

위 문제를 해결하는 하나의 방법은 Ordinals를 사용하는 것입니다. 여러분은 DEF 파일을 사용해 각 exported member에 ordinal number를 배정할 수 있습니다. 그것에 의해 컴파일러를 위한 import library를 만들 수 있습니다. 이렇게 한다면 각 사용자들은 ordinal을 통해 각 멤버에 접근할 수 있습니다. 이 접근은 name mangling에 대한 문제를 해결했지만 큰 유지보수 문제를 야기합니다. 왜냐하면 여러분은 각각의  컴파일러에 대해 import library를 만들어 주어야 하고 DLL파일이 수정되었을 때에도 import library를 수정해주어야 되기 때문입니다.

 

Oops - Some More Problems

Type-safe linking 문제만이 전부가 아닙니다. 또다른 문제는 여러분이 class를 업데이트 할 때 발생합니다. 예를 들어서 여러분이 스펠링 체커를 변경해야할 필요가 생겼다고 합시다.  여러분은 사용자를 위해 10개의 빈번히 나오는 단어에 대한 캐시 기능을 추가하려고 하는데 이것을 수행하려면 여러분은 몇가지 데이터를 여러분의 class에 추가시켜야 합니다. 새로운 클래스의 선언은 다음과 같을 수 있습니다.

class _ _declspec(dllexport) CSpellChecker {
    static int m_nRefCount;
    LPTEXTBLOB m_lpText; // LPTEXTBLOB is defined elsewhere.
    LPSTR lpszFrequentWords[10];

public:
    CSpellChecker(LPTEXTBLOB lpText) {
        // Initialize.
    }
    virtual ~CSpellChecker() {
        // Tear down.
    }

    void CheckIt() {
        // Do the checking.
    }
    void AddToFrequentWordList(LPSTR lpszWord) {
        // Cache frequent words.
    }
};

새로운 데이터를 class에 추가했기에 class의 크기가 변했습니다. 현재의 클래스는 40bytes 이상이 될 것입니다. 게다가, 여러분은 class의 layout을 변경시켰습니다. 중요한 점은 사용자가 C++ 를 사용해 코딩을 할 때 클래스의 layout을 알아야 한다는 점에 있습니다. 여러분이 새로 변경된 DLL을 사용자에게 제공했는데 그 클래스의 layout이 변경되었다면 사용자는 자신의 코드를 다시 컴파일 해야할 것입니다. 또한 어떤 사용자는 새로 변경된 DLL을 사용하기 싫어한다면 어떤일 이 생길까요? 여러분은 어쩔 수 없이 각 사용자를 위해 다른 DLL을 제공해 주어야 할 것입니다. 여러분의 DLL에 문제가 생겼을 때 각 버전의 DLL을 점검해야 한다는 것은 당연하므로 또 다시 유지보수 문제가 고개를 드는 군요. 여러분의 컴퓨터에 다양한 버전의 MFC DLL이 존재한다면 그 이유는 바로 앞에서 설명한 그것 때문입니다. 결국 DLL을 사용한 배포가 효율 적으로 이루어지려면 다음의 2가지 사항이 만족되어야 합니다. 첫번째는 모두가 하나의 C++ 컴파일러를 사용해야 합니다. 두번째는 클래스의 크기와 layout이 절대 변하지 않아야 합니다. 이것이 만족될 수 있는 일은 결코 생기지 않을 것입니다.

이것 참 어렵습니다. 어떻게 해야 이 문제를 해결할 수 있을까요? class based programming으로는 결코 이 문제를 해결할 수 없습니다. 그 대신 interface-based programming 방법을 사용해야 합니다.  Interface-based programming은 클래스의 interface를 implementation과 분리시키는 것인데, 그것으로 인해 client code와 DLL의 coupling 오버헤드를 줄일 수 있습니다.

- interface-based programming이 왜 필요한지 설명하기 위해 먼길을 돌아왔습니다. 오늘은 여기까지 하도록 하죠. ㅋ

 

신고