2026년 6월 2일 화요일

닷넷의 네이티브 AOT

C# .NET으로 프로그램을 개발하면 EXE이든 DLL이든 기본적으로 IL(Intermediate Language) 형태로 빌드된다.

이 IL 코드는 실행 시 JIT 컴파일러에 의해 기계어로 변환되는데, 문제는 IL 자체가 매우 높은 수준의 중간 언어라는 점이다.

따라서 ILSpy, dnSpy 같은 디컴파일러를 사용하면 상당 부분 원본 소스코드 수준으로 복원이 가능하다.

그동안 이러한 문제를 해결하기 위한 방법은 사실상 코드 난독화 정도가 전부였다.

하지만 .NET 8부터 Native AOT가 본격적으로 지원되면서 상황이 달라졌다.

Native AOT를 사용하면 IL이 아닌 네이티브 기계어로 직접 빌드할 수 있으며, 이를 통해 다음과 같은 장점을 얻을 수 있다.

- 실행 성능 향상
- 빠른 시작 속도
- 소스코드 노출 위험 감소
- 역공학 난이도 증가

물론 Reflection, 런타임 코드 생성 등의 기능 사용에 제약이 생기므로 개발 방식에도 변화가 필요하다.

개인적으로는 프로그램 전체를 Native AOT로 전환하기보다는, 보안이 중요한 핵심 비즈니스 로직만 별도의 DLL로 분리하여 Native AOT로 빌드하는 방식을 추천한다.

이 글에서는 Native AOT DLL을 만드는 방법을 정리해본다.



개발 환경

- .NET 9 이상 권장
- Visual Studio 2022
- Windows 11 SDK
- MSVC Build Tools



1. 프로젝트 생성

클래스 라이브러리 프로젝트를 생성한 후 csproj 파일을 다음과 같이 수정한다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <Platforms>x64</Platforms>
    <PlatformTarget>x64</PlatformTarget>
    <TrimMode>partial</TrimMode>
  </PropertyGroup>
</Project>

여기서 가장 중요한 옵션은 PublishAot 이다.

이 옵션을 활성화하면 Native AOT 빌드가 가능해진다.



2. 외부에서 호출할 함수 작성

예를 들어 문자열과 정수를 입력받고 정수를 반환하는 함수라면 다음과 같이 작성할 수 있다.

using System.Runtime.InteropServices;

public static class NativeExports
{
    [UnmanagedCallersOnly(EntryPoint = "CoreCode")]
    public static int CoreCode(IntPtr a_str, int a_value)
    {
        string? text = Marshal.PtrToStringAnsi(a_str);

        return a_value + (text?.Length ?? 0);
    }
}

UnmanagedCallersOnly 특성을 사용하면 함수가 Native DLL의 Export 함수처럼 동작하게 된다.



데이터 교환 시 주의사항

Native DLL 경계를 넘어 데이터를 주고받을 때는 가능한 한 원시 타입을 사용하는 것이 좋다.

예를 들면 다음과 같은 타입들이다.

int
bool
double
long
IntPtr

반면 다음과 같은 타입은 권장하지 않는다.

class
record
struct
List<T>
Dictionary<TKey,TValue>

물론 Marshal 처리를 통해 사용은 가능하지만, 메모리 관리와 버전 호환성 문제가 발생할 가능성이 높다.

복잡한 데이터를 전달해야 한다면 JSON 문자열을 사용하는 것이 가장 단순하고 안정적이다.

예를 들어

{
  "Id":100,
  "Name":"Kim",
  "Value":1234
}

형태로 JSON 문자열을 반환하고 호출 측에서 역직렬화하여 사용하는 방법을 추천한다.

특히 다른 언어(C++, Python, Delphi 등)와 연동할 경우 JSON 방식은 매우 높은 호환성을 제공한다.



3. 빌드 환경 준비

Native AOT는 내부적으로 네이티브 링커를 사용하기 때문에 C++ 빌드 도구가 필요하다.

Visual Studio Installer에서 다음 항목을 설치한다.

워크로드

- C++를 사용한 데스크톱 개발

개별 구성 요소

- MSVC v143 - VS 2022 C++ x64/x86 빌드 도구
- Windows 11 SDK

설치 후 다음 콘솔을 실행한다.

x64 Native Tools Command Prompt for VS 2022



4. Native AOT DLL 빌드

위 콘솔에서 다음 명령을 실행한다.

dotnet publish -c Release -r win-x64

빌드가 완료되면 publish 폴더에 Native DLL이 생성된다.



5. DLL 사용하기

생성된 DLL을 사용하는 프로젝트에 복사한 후 다음과 같이 P/Invoke로 연결한다.

[DllImport("MyTestAot.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern int CoreCode([MarshalAs(UnmanagedType.LPStr)] string a_str, int a_value);

이후 일반 함수처럼 호출할 수 있다.

int result = CoreCode("Hello", 100);



마무리

Native AOT는 단순히 실행 속도를 높이는 기술만은 아니다.

.NET 개발자가 C++로 전환하지 않고도 네이티브 바이너리를 생성할 수 있다는 점에서 매우 큰 의미를 가진다.

특히 다음과 같은 영역에 적용하면 효과적이다.

- 라이선스 검증
- 암호화 모듈
- 핵심 비즈니스 로직
- 영상 처리 알고리즘
- AI 후처리 로직
- 독자적인 계산 엔진

프로그램 전체를 Native AOT로 만드는 것보다, 보호가 필요한 핵심 모듈만 Native AOT DLL로 분리하는 것이 현실적인 활용 방법이라고 생각한다.

댓글 없음:

댓글 쓰기