Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2066. C# - PublishSingleFile과 관련된 옵션 [링크 복사], [링크+제목 복사],
조회: 10006
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 5개 있습니다.)
(시리즈 글이 4개 있습니다.)
.NET Framework: 1081. Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면?
; https://www.sysnet.pe.kr/2/0/12733

.NET Framework: 2066. C# - PublishSingleFile과 관련된 옵션
; https://www.sysnet.pe.kr/2/0/13159

.NET Framework: 2067. C# - PublishSingleFile 적용 시 native/managed 모듈 통합 옵션
; https://www.sysnet.pe.kr/2/0/13160

.NET Framework: 2068. C# - PublishSingleFile로 배포한 이미지의 역어셈블 가능 여부 (난독화 필요성)
; https://www.sysnet.pe.kr/2/0/13161




C# - PublishSingleFile과 관련된 옵션

전에 이 옵션을 사용한 예제를 소개하긴 했지만,

Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면?
; https://www.sysnet.pe.kr/2/0/12733#prj_prop

세세하게 다루질 않아서 ^^ 이번 글에서 정리를 해보겠습니다. 관련해서 공식 문서도 있으니,

Single-file deployment and executable
; https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli#compress-assemblies-in-single-file-apps

참고하시고, 이하 모든 테스트는 닷넷 6 환경을 가정하고 설명합니다.




우선, 출력 파일을 하나로 모으기 위한 가장 기본적인 옵션은 PublishSingleFile입니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <PublishSingleFile>true</PublishSingleFile>
    </PropertyGroup>

</Project>

그런데, 위와 같이 구성하고 빌드하면,

e:\net6_pubone_sample> dotnet publish

다음과 같은 오류가 발생합니다.

C:\Program Files\dotnet\sdk\6.0.402\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(102,5): error NETSDK1097:
It is not supported to publish an application to a single-file without specifying a RuntimeIdentifier. You must either specify a RuntimeIdentifier or set PublishSingleFile to false. [E:\net6_pubone_sample\net6_pubone_sample.csproj]


즉, 빌드 출력을 하나로 모을려면 RuntimeIdentifier를 설정해야 하는 것입니다. 사실 당연합니다. 닷넷 런타임도 설치되지 않은 곳에서 단일 파일로 실행하려는 것이기 때문에 리눅스에서 실행할 것인지, 윈도우에서 실행할 것인지를 알아야 PE 포맷으로 빌드하든, ELF 포맷으로 빌드하든 할 수 있을 테니까요.

따라서 다음과 같이 대상 플랫폼을 설정하면 됩니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    </PropertyGroup>

</Project>

이제 배포("dotnet publish")하면, .\bin\Debug\net6.0\win-x64\publish 디렉터리에 EXE 파일 하나와 PDB 파일 하나가 생성됩니다.




경우에 따라 PDB 파일은 버리고 배포해도 되지만, 그래도 나중에 풀 덤프 등의 사후 디버깅을 해야 할 일이 생긴다면 함께 배포하는 것이 좋습니다. 하지만, 파일이 2개로 나뉘는 것은 번거로운데요, 다행히 마이크로소프트는 PDB를 실행 파일에 내장하는 옵션도 만들었습니다. 바로 그것이 DebugType입니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
    </PropertyGroup>

</Project>

이제 다시 배포("dotnet publish")하면, publish 디렉터리에 EXE 파일 하나만 출력되는 것을 확인할 수 있습니다.




대부분의 경우, PublishSingleFile로 배포하는 경우는 대상 컴퓨터에 닷넷 런타임 설치 여부에 상관없이 동작시키고 배포를 편리하게 하기 위해 선택했을 것입니다.

그런데, 재미있는 것은 PublishSingleFile도 런타임 설치 여부에 따라 다르게 패키징할 수 있다는 점입니다. 현재, 위의 설정으로 .NET 6 환경에서 출력된 실행 파일(윈도우의 경우 EXE)을 보면 크기가 60MB가 넘을 텐데요, 왜냐하면 닷넷 응용 프로그램을 실행하기 위한 Native + Managed 모듈을 모두 포함하고 있기 때문입니다.

만약, 대상 컴퓨터에 닷넷 런타임이 설치되어 있다는 것을 가정한다면 이건 너무 비효율적인 크기인데요, 이를 위해 선택할 수 있는 옵션이 바로 SelfContained입니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
        <SelfContained>false</SelfContained>
    </PropertyGroup>

</Project>

위와 같이 SelfContained를 false로 주고 다시 "dotnet publish"를 실행하면, 동일하게 실행 파일이 publish 디렉터리에 생성되지만 그 크기가 160KB 정도로 대폭 축소됩니다. 하지만, 대개의 경우 아마도 이 옵션을 원치는 않을 테니, 이후 테스트에서는 빼고 설명하겠습니다.




현재 (SelfContained를 true로 다시 바꾸고 빌드한 경우) publish 디렉터리에 있는 EXE는 내부에 native와 managed 모듈을 모두 포함하고 있는데요. 따라서 여전히 managed 모듈은 실행 시 JIT 컴파일을 필요로 하는 IL 형태로 포함돼 있는 상태입니다.

이러한 JIT 컴파일 단계를 실행 시가 아닌, 빌드 시에 미리(AOT compilation) 해놓을 수 있는 옵션이 바로 PublishReadyToRun입니다. 그래서 이것을 설정해 두면,

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
        <PublishReadyToRun>true</PublishReadyToRun>
    </PropertyGroup>

</Project>

JIT 컴파일러를 거치지 않고 실행 시 곧바로 실행됩니다. 그런데, 이게 완전한 유형의 native code를 포함하지는 않습니다. 문서에 보면,

R2R binaries improve startup performance by reducing the amount of work the just-in-time (JIT) compiler needs to do as your application loads. The binaries contain similar native code compared to what the JIT would produce. However, R2R binaries are larger because they contain both intermediate language (IL) code, which is still needed for some scenarios, and the native version of the same code. R2R is only available when you publish an app that targets specific runtime environments (RID) such as Linux x64 or Windows x64.


뭐랄까, 과거 .NET Framework 시절의 NGen과 유사한 듯한데요, R2R 옵션으로 빌드하면 실행 파일에 IL 코드와 그것을 미리 빌드한 native 코드가 함께 포함됩니다. 특정 경우에 IL 코드가 필요해서 그렇다고 하는데, .NET Profiler로 돌려보면 사실 대부분의 경우 JIT 컴파일이 발생하지 않습니다.

어쨌든, 결국 이로 인해 실행 파일의 크기가 2~3배 증가할 수 있다고 합니다. 그래서 초기 로딩 속도가 디스크 읽기 작업으로 느려지고, working set도 증가하는 부작용으로 이것은 다시 로딩 속도에 영향을 미칩니다.

사실 R2R(Ready-to-Run)을 설정하는 가장 큰 이유가 초기 로딩 속도를 줄여보려는 것이므로, 가능한 자신의 애플리케이션 상황에 맞게 적절한 테스트를 해보시는 것도 좋겠습니다.

개인적으로, 디버깅 등의 목적으로 인해 R2R을 별로 좋아하지 않기 때문에 이후 테스트에서는 빼는 걸로 가정하겠습니다.




빌드 시점에, publish를 위해 준비된 기본 native + managed 모듈은 .\bin\Debug\net6.0\win-x64 디렉터리에 출력이 됩니다. 재미있는 건, 그 디렉터리의 모든 내용을 압축하면 약 35MB 정도의 크기가 나옵니다. 그런데, publish에 있는 실행 파일은 무려 60MB가 넘는 크기입니다.

왜냐하면, 그 파일에는 native와 managed 코드를 모두 (압축 형태가 아닌) 원본 그대로 담고 있기 때문입니다.

그렇다면, managed 코드를 압축 형태로 담는 경우 파일 크기가 좀 더 작아지겠군요. ^^ 바로 그런 용도로 제공하는 옵션이 EnableCompressionInSingleFile입니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
        <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
    </PropertyGroup>

</Project>

이후 빌드하면 60MB의 실행 파일이 32MB 정도로 작아집니다. 물론, 이렇게 하면 초기 로딩 시 압축 해제를 해야 하므로 실행 속도가 느려질 수 있습니다. 하지만, 2배 가량 크기가 작아지니... 충분히 욕심나는 옵션입니다. ^^




크기 줄이는 옵션이 나왔으니, 하나 더 알아볼까요? ^^

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <!-- [생략] -->

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
        <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
        <PublishTrimmed>true</PublishTrimmed>
    </PropertyGroup>

</Project>

PublishTrimmed는 응용 프로그램에서 참조한 DLL의 모든 IL 코드를 추가하지 않고, 순수하게 응용 프로그램에서 사용 중인 타입/메서드만을 포함시킵니다. 그래서 이 옵션까지 적용하면 실행 파일의 크기가 거의 9MB 정도로 작아집니다. (물론, 기본 Console Application 프로젝트의 경우에 한해서입니다.)

PublishTrimmed는 컴파일 시에 추적이 가능한 타입/메서드만을 골라내는 것이므로, 런타임 중에 (Reflection 등을 통해) 로딩되는 타입은 알 수 없어 오류가 발생할 수 있습니다. 그 점을 감안해, 자신이 만드는 응용 프로그램의 내부 코드에 따라 사용하는 것이 좋습니다.




정리해 보면, 단일 파일 출력을 위해 제가 선호하는 옵션은 다음과 같은 정도입니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <PublishSingleFile>true</PublishSingleFile>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <DebugType>embedded</DebugType>
        <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
    </PropertyGroup>

</Project>

여기서, 단순 Console Application을 만든다면 PublishTrimmed을 추가합니다. (그리고, 이 모든 노력은 결국 PublishAot 빌드를 위한 초석이라는 점!)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/12/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2022-11-27 12시35분
[hong] 단일파일배포 관련 설정이 프로젝트 파일(csproj)이 아니라 Properties > PublishProfiles > FolderProfile.pubxml에서 설정하도록 바뀐거같습니다.
"게시" 화면에서 배포 모드, 대상 런타임, 단일파일생성 여부, ReadyToRun 컴파일 사용등을 설정하면 FolderProfile.pubxml에 저장되고, 기타 추가 옵션을 이곳에 넣어주면 됩니다.
[guest]
2022-11-27 08시30분
Pubxml은 예전부터 원래 비주얼 스튜디오에서 게시를 위한 프로파일 정보를 담고 있었습니다.

msbuild로 .pubxml 설정에 따른 배포 파일을 만드는 방법
; https://www.sysnet.pe.kr/2/0/11744

그러던 것이 코어로 오면서 일부 옵션이 csproj로도 추가된 것이라서 둘 다 지원하는 것이 맞습니다.
정성태
2023-11-22 11시08분
.NET Conf 2023 (Day 2) - Tiny, fast ASP.NET Core APIs with native AOT
; https://youtu.be/vU-iZcxbDUk?t=11099

var builder = WebApplication.CreateSlimBuilder(args);
// var builder = WebApplication.CreateEmptyBuilder(new());
builder.WebHost.UseKestralCore();
// builder.Services.AddRoutingCore();

var app = builder.Build();

// app.MapGet("/", () => "Hello World!");

app.Run();

How does native AOT work?
* C# is compiled to IL on build
* IL is compiled to platform code (e.g. x64) on publish
* Published app:
  - Has no JIT
  - Still contains a runtime & GC (is still "managed")
  - Is single-file
  - Is trimmed to reduce app size
  - Is OS & architecture specific, e.g., linux-x64

Impact of no JIT compilation
* No runtime code generation
  - No platform optimizations (can opt-in ahead of time)
  - No Dynamic PGO (no tiering)
* No Assembly.LoadFile
* No Expression compilation
* No Reflection.Emit

Impact of trimming
* Unreferenced code (no callers) is removed
* No assembly or type scanning
* Code might be "kept" that isn't called at runtime due to API design

Other considerations
* No C++/CLI or COM
* Single-file
* Requires extra build-time pre-requisites
  - Visual Studio C++ tools on Windows
  - Clang on Linux
  - XCode on macOS
* Cannot publish cross-platform (consider Docker build)

Publishing options
* Framework-dependent (FDD) - Default
* Self-contained (SCD)
* Trimmed
* ReadyToRun (R2R)
* Single file
* Native AOT
   <PublishSelfContained>true</PublishSelfContained>
   <PublishTrimmed>true</PublishTrimmed>
   <PublishSingleFile>true</PublishSingleFile>
   <PublishReadyToRun>true</PublishReadyToRun>
   <PublishAot>true</PublishAot>

More publish options
* OptimizationPreference: size/speed
  - Use with PublishAot. Default is "size"
* InvariantGlobalization: true/false
  - Whether the runtime supports globalization features
* EventSourceSupport: true/false
  - Disabled for native AOT by default but enabled in new template
* ServerGarbageCollection: true/false
  - Default for ASP.NET Core projects
* GarbageCollectionAdaptionMode: 0/1
  - New "DATAS" GC mode, use in conjunction with Server GC
  - Default for native AOT published ASP.NET Core projects

ASP.NET Core native AOT support
* Initial target is cloud-focused API workloads
* Supported:
  - Kestrel HTTP server
  - Middleware
  - Minimal APIs (Request Delegate Generator)
  - gRPC
  - JWT Authentication
  - Authorization
* Not supported yet:
  - MVC/Web API
  - Razor/Blazor
  - SignalR

Data access
* ADO.NET itself is native AOT ready
* Supported providers as of .NET 8
  - PostgreSQL (Npgsql)
  - SQLite (Microsoft.Data.Sqlite)
* Supported ORMs:
  - Dapper.AOT https://aot.dapperlib.dev
  - Nanorm https://github.com/DamianEdwards/Nanorm
* Not supported yet:
  - Entity Framework Core (hopefully in .NET 9!)

JSON (System.Text.Json)
* JSON serialization requires extra configuration when using trimming and/or native AOT
* The JSON source generator must be used to generate a JsonSerializaerContext
* In ASP.NET Core the HTTP JsonOptions in DI must then be configured to use the generated context
  builder.Services.ConfigureHttpJsonOptions(options =>
  {
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
  });

New template: Web API (native AOT)
* New ASP.NET Core Web API (native AOT) project template
  - "dotnet new webapiaot" at the command line
* Pre-configured for native AOT
* Uses CreateSlimBuilder
* Uses Minimal APIs
* Uses JsonSerializerContext
* ~10MB published app size
* gRPC project template updated with native AOT option
정성태

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13454정성태11/23/20233789닷넷: 2167. C# - Qdrant Vector DB를 이용한 Embedding 벡터 값 보관/조회 (Azure OpenAI) [1]파일 다운로드1
13453정성태11/23/20233213오류 유형: 879. docker desktop 설치 시 "Invalid JSON string. (Exception from HRESULT: 0x83750007)"
13452정성태11/22/20233313닷넷: 2166. C# - Azure OpenAI API를 이용해 사용자가 제공하는 정보를 대상으로 검색하는 방법파일 다운로드1
13451정성태11/21/20233497닷넷: 2165. C# - Azure OpenAI API를 이용해 ChatGPT처럼 동작하는 콘솔 응용 프로그램 제작파일 다운로드1
13450정성태11/21/20233276닷넷: 2164. C# - Octokit을 이용한 GitHub Issue 검색파일 다운로드1
13449정성태11/21/20233404개발 환경 구성: 688. Azure OpenAI 서비스 신청 방법
13448정성태11/20/20233667닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20233585닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20233492닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20233897Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20233589닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20233518개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20233315개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20233761닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20233325Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20234154닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20233525닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20233655닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20233901닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20233768닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20233501스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20233063스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20233393오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20233799스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20233618닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20233843닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...