커스텀 마샬링

.NET에서 기본적인 마샬링 방식으로는 대부분의 언매니지드 코드와 상호작용할 수 있지만, 때로는 보다 복잡한 데이터 구조나 특정 요구 사항에 맞춘 사용자 정의 마샬링(Custom Marshaling)이 필요할 수 있습니다. 기본 마샬링으로 해결되지 않는 성능 문제나 데이터 처리 방식을 커스터마이징하기 위해 ICustomMarshaler 인터페이스를 활용할 수 있습니다. 이 글에서는 사용자 정의 마샬링이 필요한 상황과 이를 구현하는 방법을 살펴보고, 다양한 예제와 함께 설명합니다.

왜 Custom Marshaling이 필요한가?

Custom Marshaling은 기본 마샬링 방식으로는 효율적으로 처리할 수 없는 복잡한 데이터 구조나 특수한 요구 사항을 다룰 때 필요합니다. 다음과 같은 상황에서 유용합니다:

  • 복잡한 구조체 처리: 기본 마샬링으로는 처리하기 어려운 중첩된 구조체나 포인터 배열과 같은 데이터 구조.
  • 성능 최적화: 기본 마샬링의 성능이 부족할 때, 커스터마이징을 통해 마샬링 성능을 개선.
  • 메모리 관리: 특정 방식으로 메모리를 할당 및 해제해야 하는 경우.
  • 데이터 타입 변환: 언매니지드 코드에서 제공하는 복잡한 데이터 타입을 매니지드 환경에 맞게 변환해야 할 때.

ICustomMarshaler 인터페이스 소개

.NET에서는 ICustomMarshaler 인터페이스를 사용하여 사용자 정의 마샬링을 구현할 수 있습니다. 이 인터페이스는 매니지드 및 언매니지드 데이터 간의 변환 방식을 직접 제어할 수 있게 해줍니다.

public interface ICustomMarshaler
{
    object MarshalNativeToManaged(IntPtr pNativeData);
    IntPtr MarshalManagedToNative(object managedObj);
    void CleanUpNativeData(IntPtr pNativeData);
    void CleanUpManagedData(object managedObj);
    int GetNativeDataSize();
}

주요 메서드 설명:

  • MarshalNativeToManaged: 언매니지드 데이터를 매니지드 데이터로 변환하는 메서드.
  • MarshalManagedToNative: 매니지드 데이터를 언매니지드 데이터로 변환하는 메서드.
  • CleanUpNativeData: 언매니지드 데이터의 정리를 담당.
  • CleanUpManagedData: 매니지드 데이터의 정리를 담당.
  • GetNativeDataSize: 언매니지드 데이터의 크기를 반환.

Custom Marshaler 예제

예제 1: 문자열 배열을 마샬링하는 Custom Marshaler

다음은 C에서 사용되는 char 타입의 문자열 배열을 C#의 문자열 배열로 변환하는 Custom Marshaler 예제입니다.

using System;
using System.Runtime.InteropServices;
public class StringArrayMarshaler : ICustomMarshaler
{
    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        // IntPtr 배열에서 C의 char를 C#의 string[]으로 변환
        int length = 0;
        while (Marshal.ReadIntPtr(pNativeData, length * IntPtr.Size) != IntPtr.Zero)
        {
            length++;
        }
        string[] managedArray = new string[length];
        for (int i = 0; i < length; i++)
        {
            IntPtr pString = Marshal.ReadIntPtr(pNativeData, i * IntPtr.Size);
            managedArray[i] = Marshal.PtrToStringAnsi(pString);
        }
        return managedArray;
    }
    public IntPtr MarshalManagedToNative(object managedObj)
    {
        string[] managedArray = (string[])managedObj;
        IntPtr pNativeArray = Marshal.AllocHGlobal((managedArray.Length + 1) * IntPtr.Size);
        for (int i = 0; i < managedArray.Length; i++)
        {
            IntPtr pString = Marshal.StringToHGlobalAnsi(managedArray[i]);
            Marshal.WriteIntPtr(pNativeArray, i * IntPtr.Size, pString);
        }
        Marshal.WriteIntPtr(pNativeArray, managedArray.Length * IntPtr.Size, IntPtr.Zero);
        return pNativeArray;
    }
    public void CleanUpNativeData(IntPtr pNativeData)
    {
        int length = 0;
        while (Marshal.ReadIntPtr(pNativeData, length * IntPtr.Size) != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(Marshal.ReadIntPtr(pNativeData, length * IntPtr.Size));
            length++;
        }
        Marshal.FreeHGlobal(pNativeData);
    }
    public void CleanUpManagedData(object managedObj)
    {
        // 매니지드 데이터는 자동으로 GC에 의해 처리되므로 특별한 클린업 필요 없음
    }
    public int GetNativeDataSize()
    {
        return IntPtr.Size;
    }
}

이 예제에서는 C의 char를 C#의 string[]로 마샬링하는 과정이 구현되었습니다. 언매니지드 메모리에서 char* 포인터 배열을 읽어 매니지드 환경의 문자열 배열로 변환하는 기능을 제공합니다.

예제 2: 복잡한 구조체 처리

복잡한 구조체가 포함된 데이터를 사용자 정의 마샬링으로 처리할 수 있습니다. 예를 들어, C++에서 전달된 구조체가 여러 포인터를 포함하는 경우를 처리할 수 있습니다.

[StructLayout(LayoutKind.Sequential)]
public struct ComplexStruct
{
    public IntPtr stringPointer; // C++의 char*와 대응
    public int number;
}
public class ComplexStructMarshaler : ICustomMarshaler
{
    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        ComplexStruct cs = Marshal.PtrToStructure<ComplexStruct>(pNativeData);
        var managedStruct = new ComplexStruct
        {
            stringPointer = cs.stringPointer,
            number = cs.number
        };
        // 문자열 포인터를 C#의 string으로 변환
        managedStruct.stringPointer = Marshal.StringToHGlobalAnsi(Marshal.PtrToStringAnsi(cs.stringPointer));
        return managedStruct;
    }
    public IntPtr MarshalManagedToNative(object managedObj)
    {
        ComplexStruct managedStruct = (ComplexStruct)managedObj;
        IntPtr nativePtr = Marshal.AllocHGlobal(Marshal.SizeOf<ComplexStruct>());
        Marshal.StructureToPtr(managedStruct, nativePtr, false);
        return nativePtr;
    }
    public void CleanUpNativeData(IntPtr pNativeData)
    {
        Marshal.DestroyStructure<ComplexStruct>(pNativeData);
        Marshal.FreeHGlobal(pNativeData);
    }
    public void CleanUpManagedData(object managedObj)
    {
        // 복잡한 구조체의 매니지드 데이터 처리
        ComplexStruct managedStruct = (ComplexStruct)managedObj;
        Marshal.FreeHGlobal(managedStruct.stringPointer);
    }
    public int GetNativeDataSize()
    {
        return Marshal.SizeOf<ComplexStruct>();
    }
}

이 예제에서는 C++에서 전달된 구조체를 매니지드 코드에서 처리하기 위한 Custom Marshaler를 구현했습니다. 문자열 포인터와 정수로 이루어진 구조체를 매니지드 환경에서 사용 가능한 형태로 변환합니다.

Custom Marshaling의 장점과 단점

Custom Marshaling은 복잡한 데이터 타입을 처리하고, 성능을 최적화할 수 있는 강력한 도구입니다. 하지만 이를 잘못 구현할 경우 메모리 누수나 잘못된 데이터 처리로 이어질 수 있습니다. Custom Marshaling을 사용할 때는 반드시 메모리 관리와 변환 과정에서 발생할 수 있는 문제에 대해 주의를 기울여야 합니다.

장점

  • 복잡한 데이터 구조를 처리할 수 있습니다.
  • 성능을 최적화할 수 있는 유연성을 제공합니다.
  • 기본 마샬링보다 더 많은 제어권을 가질 수 있습니다.

단점

  • 구현이 복잡하고 잘못된 메모리 관리를 할 경우 문제가 발생할 수 있습니다.
  • 성능 최적화를 위해 작성된 코드가 잘못될 경우, 오히려 성능이 저하될 수 있습니다.

결론

Custom Marshaling은 기본 마샬링으로 처리할 수 없는 복잡한 상황에서 매우 유용한 도구입니다. ICustomMarshaler 인터페이스를 사용하면, 데이터 타입 변환과 메모리 관리를 직접 제어할 수 있어 성능 최적화 및 특수한 데이터 처리 요구를 충족할 수 있습니다. 이를 사용하여 복잡한 문자열 배열이나 구조체를 안전하게 마샬링할 수 있으며, 성능 최적화에도 기여할 수 있습니다.