336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

1. 동기

C#으로 COM 객체를 생성하고 처리 하는 프로세스를 개발 할 일이 있었다.
OCX의 쓰레기 동기화 문제로 하나의 프로세스에서 하나의 OCX를 사용 할 수 있도록 구성 해야 되는 상황이 있었고, 그로 인해 똑같은 일을 하는 OCX를 여러개 만들었다.
각각의 OCX는 GUID, 클래스 명 등이 달랐다.

이러다보니 OCX1을 참조하는 프로세스, OCX2를 참조하는 프로세스가 클래스명만 다르게 수정하여 컴파일 되야 하는 어처구니 없는 일이 생기고 만 것이다.
결국 OCX의 각 클래스명은 달라도 안에 포함 되어진 Property와 Method 명은 같으므로, 이 점을 착안하여 일반화를 시도했다.

2. 시행착오

2.1 .NET Generic

클래스명만 다르고 함수나 속성 값은 같기 때문에 class 일반화를 통해 이를 해결 하려고 했다.
public class Reg<T>
{
private T objOCX;
public Reg(string szGuid)
{
        Guid guid = new Guid(szGuid);
        Type objType = Type.GetTypeFromCLSID(guid, true);
        _reg = (T)Activator.CreateInstance(objType);
 }
 
생성까지는 좋았다. 하지만 문제는 각 멤버 접근하는 코드를 작성하던 도중 발생하였다.
public string ErrorMessage
{
    get
    {
        return (string)T.errMsg;
    }
}
public string MetaKey
{
    set
    {
    	  T.meta_key = value;
    }
}
컴파일하자 에러가 발생되었는데 이유는 간단하다.
C# Generic은 c++ templete과 큰 차이점이 하나 있는데 바로 C# Generic은 컴파일 타임에 타입 체킹을 한다는 것이다.
위의 경우 T 클래스가 meta_key나 errMsg 속성을 가지고 있는지 여부를 확인하고 없으면 컴파일이 되질 않는다.
그래서 T 클래스에 문제가 되는 속성이나 함수가 있다는 사실을 알리기 위해 다음과 같이 코드를 수정하였다.
public class Reg<T> where T : OCX1
{
		private T objOCX;
		public Reg(string szGuid)
		{
	        Guid guid = new Guid(szGuid);
	        Type objType = Type.GetTypeFromCLSID(guid, true);
	        _reg = (T)Activator.CreateInstance(objType);
        }
}
T에 OCX1 클래스를 넣어 컴파일 할 땐 잘 된다.
하지만 OCX2 클래스로 생성하는 코드를 작성하여 컴파일 해 보았다.
Reg<OCX2> reg = new Reg<OCX2>(szGuid);
타입캐스팅 에러가 난다.
왜냐하면 클래스 구조상 OCX2와 OCX1은 아무런 연관이 없으므로 타입 캐스팅이 불가능하다.
C#의 강력한 타입 체킹의 위용을 다시 한번 느끼게 해 준 순간이었다.
고민한 끝에 방향을 바꿨다. 참조해야 되는 OCX의 함수나 속성을 클래스내에서 직접 접근하는 것이 아니라 .NET Reflection을 이용해 런타임 시점에서 접근하도록 하여 컴파일과 코드 수행을 제대로 이뤄 질 수 있도록 구성하는 것이었다.

2.2 .NET Reflection

처음에는 .NET에서 제공하는 Reflection 기능을 사용하여 다음과 같이 GetProperty나 SetProperty와 같은 함수로 접근하여 처리 하려고 했으나 문제가 발생되었다.
object GetProperty(object obj, string szProPertyName)
{
	PropertyInfo pi = obj.GetType().GetProperty(szProPertyName);
	if (pi == null) return null;
	return pi.GetValue(obj, null);
}
원래 OCX 클래스가 가지고 있는 Property 값을 아무리 넣어도 pi가 null로 나오는 것이었다.
Method라고 상황이 다르진 않았다.
void InvokeMethod(object obj, string szMethodName, params object[] p)
{
	MethodInfo mi = obj.GetType().GetMethod(szMethodName,BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod);
	if (mi == null) return null;
	return mi.Invoke(obj, BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, p, CultureInfo.CurrentCulture);
}
마찬가지로 mi 값이 항상 null이 나오는 것이었다.
구글링도 해보고 BindingFlags를 바꿔보고 했는데 결국 되질 않았다.
문제는 단순했다. 아무리 OCX를 C#에서 참조하여 .NET 클래스로 매핑 되었어도 OCX 자체가 unmanaged 자원이기 때문에 Reflection으로는 Property나 Method가 검색되질 않았던 것이다.
결국 애초부터 unmanaged code에 대한 .NET Reflection 검색은 불가능했던 것이다. ㅠ_ㅠ
여기서 포기할순 없었다. 또 머리를 굴리고 구글링도 해 보았다. 고생 끝에 낙이 온다고 .NET에서 COM 개체의 속성 혹은 함수를 직접 호출 할 수 있는 기능을 제공하고 있었다.
함수의 이름 바로! InvokeMember였다.
InvokeMember 함수는 .Net Reflection 뿐만 아니라 OCX의 노출된 속성 또는 함수도 직접 호출 할 수 있는 기능을 제공하고 있었다. 역시 치밀한 마소!
어렵사리 만든 결과물을 이 OCX에만 국한되게 쓸순 없었기에 일반화 클래스를 만들어 다른 곳에서도 유연하게 사용 할 수 있도록 구성하였다.
public static class DynamicInvoker
{
    public static readonly BindingFlags MemberAccess = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Static;
    public static void SetProperty<T>(T obj, string szProPertyName, object objValue)
    {
        object objReturn = obj.GetType().InvokeMember(
        szProPertyName, BindingFlags.SetProperty, null, obj, new object[] { objValue });
    }
    public static object GetProperty<T>(T obj, string szProPertyName)
    {
        return obj.GetType().InvokeMember(
            szProPertyName, BindingFlags.GetProperty | MemberAccess, null, obj, null);
    }
    public static object InvokeMethod<T>(T obj, string szMethodName, params object[] p)
    {
        return obj.GetType().InvokeMember(
            szMethodName, BindingFlags.InvokeMethod | MemberAccess, null, obj, p);
    }
}
DynamicInvoker 클래스는 OCX 클래스의 setter, getter, 함수 호출을 지원하고 있다.

2.3 클래스 생성과 사용

여기서 끝난게 아니었다. 전에 언급했듯이 OCX의 클래스가 다르기 때문에 실제 사용하는 곳에서의 코드는 다음과 같이 사용 될 수 있다.

2.3.1 문제가 되는 코드

- OCX1 클래스 생성
Reg<ocx1> reg = new Reg<ocx1>(szguid);
- OCX2 클래스 생성
Reg<ocx2> reg = new Reg<ocx2>(szguid);
앞에서 열심히 해 놓았던 것들이 실제 사용 되는 곳에서 OCX 별로 나뉘게 되니 의미가 없게 되었다.
아흑! 또 포기 할 순 없다!
어차피 사용되는 곳에서 참조하는 함수와 속성은 같으므로 이를 인터페이스로 빼내고 Reg 클래스는 인터페이스 상속하여 구현하였다.
interface ISender
{
	string ErrorMessage();
	void MetaKey(string szMetakey);
}
class Reg<T> : ISender
{
    public string ErrorMessage
	{
	    get
	    {
	        return (string)DynamicInvoker.GetProperty<T>(_reg, "errMsg");
	    }
	}
	public string MetaKey
	{
	    set
	    {
	    	  DynamicInvoker.SetProperty<T>(_reg, "meta_key", value);
	    }
	}
}
ClassFactory 패턴으로 접근하여 인터페이스를 받아 온 후 인터페이스로 모든 기능을 사용 할 수 있도록 구성하였다.
private ISender GetSender(string szGUID)
{
    ISender sender = null;

    switch(szGUID)
    {
        case "{CC062AE7-B304-425F-8655-12FBFD2D15C6}" :
            sender = new Reg<imgreg._reg>(szGUID);
            break;
        case "{DDD7429B-1F17-4014-8EDB-170E7BDE959D}" :
            sender = new Reg<imgreg2._reg2>(szGUID);
            break;
        case "{EC4AC9C4-2268-499B-AE26-42CA6D49A71E}":
            sender = new Reg<imgreg3._reg3>(szGUID);
            break;
        case "{5D1C67EA-C608-4DB9-B3A7-1A29B8EE9991}":
            sender = new Reg<imgreg4._reg4>(szGUID);
            break;
        case "{21C9A1A9-FAD5-4729-A6EB-3662AD3F9969}":
            sender = new Reg<imgreg5._reg5>(szGUID);
            break;
        default :
            sender = new Reg<imgreg._reg>(szGUID);
            break;
    }
    
    return sender;
}
사용 하는 곳에서는 다음과 같이 사용하면 OCX별로 개체를 생성 할 필요가 없어진 것이다.
ISender sender = GetSender(szGuid);

3. 결론

시행착오를 거쳐 끝까지 오는데 1~2일은 족히 쓴거 같다. 부디 이 글이 다른 이들에게 보탬이 되어 나처럼 헤매지 않길 바란다.
그리고 지금 보니 코드를 좀더 세련되게 만들어야겠다는 생각이 든다. ㅋㅋ
그럼~




+ Recent posts