요즘 .NET으로 Console Application을 만드는 재미에 빠졌습니다. 비즈니스를 테스트하면서, 터미널 환경에서 직관적이고 가볍게 사용할 수 있으며, 터미널의 지원을 통해 CI/CD 자동화 도구에서 부담없이 실행가능하다는 점 때문이죠.
하지만 Visual Studio debug로 실행할 때는 잘 되어도, dotnet publish 직후에는 안될 수도 있습니다. 아직 debug과 release publish 차이를 잘 모르는 개발자도 있겠지만… 그것은 넘어가고…
아무튼 publish할 때는 단순 debug때와는 다르게 여러가지 추가적인 짓들을 할 수 있습니다.
우선은, Visual Studio를 통해 게시(Publish)를 눌렀을 때 게시 프로필을 통한 화면을 볼 수 있으며 이것은 dotnet publish와 완벽하게 호환됩니다. 물론 오히려 dotnet publish 명령어로 배포하는 것이 더 많고 강력한 옵션들을 사용할 수 있고, Visual Studio 2022로는 주요한 최적화 기능만 사용할 수 있습니다.
※ 쉬운 이해를 위해 일상적인 대화의 저속한 표현을 사용하였으니, 양해부탁드리겠습니다.
-sc|–self-contained={true|false}
이 중에서 배포 모드 의 자체 포함 기능은 저의 최애 기능입니다. dotnet publish 명령어를 사용할 때 --self-contained 라고 불리는 기능과 같은 기능입니다. 바로 Application에 .NET을 넣어버리는 기능으로, 사용자의 PC에 .NET이 설치되어 있지 않아도 .NET Application을 실행 가능한 멋진 기능입니다.
.NET을 설치하면 많은 저장공간이 필요한데, 용량이 그렇게까지 크지는 않은 걸보면 추측이지만 내가 생성한 .NET Assembly에서 필요한 기능만 쏙 뽑아서 합쳐지는게 아닐까 생각이 듭니다.
-p:PublishSingleFile={true|false}
MSBuild 옵션은 -p를 붙여서 위와 같이 사용해야한다고 하네요.
.NET을 빌드하면 기본적으로 Assembly 단위로 여러개의 .dll 파일이 생성됩니다. .NET Framework 시절에도 Fody를 통해 이 어셈블리들을 하나로 합쳐서 배포하는 방법은 존재했지만, .NET 부터는 공식 옵션으로 지원을 시작했습니다.
그리고 특히, 아까 앞서 말한대로, CI/CD용 system에는 단일 파일로 배포해주면 배포도 간결하기 때문에 빨리 배포해 버리고 잊어버릴 수 있는 뇌내 Fire And Forget을 적용할 수 있어 뇌의 과부화 뇌절 부담을 덜어줍니다. (물론 Assembly 단위로 다 나눠서 배포할 때 변경된 Assembly만 교체할 수 있다는 점은 매력적이지만, 지속적으로 Machine을 관리하는 사람이 없다면 이마저도 불편할 수 있다는 사실…)
물론 완벽하게 단일파일로 나오지는 않고 SkiaSharp 처럼 Native DLL이 필요한 기술적 한계가 존재하는 것들은 합쳐지지 않고 별도로 빠지니까 괜한 시간 버리지 마시고, 그냥 옵션만 켜서 배포하면 됩니다.
-p:ReadyToRun={true|false}
MSBuild 옵션은 -p를 붙여서 위와 같이 사용해야한다고 하네요.
ReadyToRun 옵션은 쉽게 말해서 코드를 보니, 미리 예측할 수 있는 Native 코드(운영체제의 자체 API) 호출 부분은 미리 컴파일해서 JIT의 코드 최초 1회성 코드 컴파일 부분조차 없에준다는 옵션입니다.
JIT 컴파일링의 원리는 다들 아시는대로, 쉽게말하자면, 프로그램을 빌드할 때 대충 기워서 만든 다음, 실행해서 프로세스가 되면 그때서야 비로소 배포한 시스템의 리소스를 끌어다가 디테일한 부분을 완성시켜나가는 방식입니다. 어느쪽에서 실행시간을 부담할지의 득과 실을 따진 트레이드 오프의 산물이죠.
다만 개발자들은 일정 수준이 올라가면 꼭, 편리하면서 또 자그마한 퍼포먼스들을 중첩시켜서 고성능을 챙기고 싶어하는… 네이버처럼 심플하고 화려하게 해주세요 같은 것들을 꼭 하고 싶어하기에, JIT 전용인 .NET 환경을 어떻게든 AOT 형태로 바꿔보려고 노력하기 시작했고(그놈의 C++과 Rust에 어떻게든 안밀려 볼려고…비교 좀 그만해 제발…), JIT의 산물인 IL과 System API 직접 호출할 수 있는 코드(원래는 런타임 중에 해야함)를 미리 빌드해버린…
IL 코드와 Native 코드가 혼재한 혼종을 탄생시켰고 그것이 ReadyToRun 입니다. 그래서 용량이 그냥 JIT으로 대충 기워서 빌드한 .NET Assembly보다 용량이 3배정도 많습니다. 다만 Native Code를 호출하는데 한하여서는 JIT 처럼 Runtime에서 코드 컴파일 및 실행이 없다보니, Warm Start가 아니라 Cold Start 면에서는 최적화가 되었다고 볼 수 있는 옵션입니다.
그리고 이 부분은 잘 모르는 개념이지만, Native Code를 빌드해주는 도구로서 CrossGen2 가 사용된다고 합니다. 그리고 ReadyToRun은 JIT을 사용할 수 있기 때문에 기존의 대다수의 JIT 기반으로 build된 Nuget Package들을 자유롭게 이용할 수 있는 것이 장점입니다.
시스템의 물리적 저장공간만 충분하다면 편의성과 퍼포먼스를 모두 챙길 수 있는 좋은 옵션이라고 생각합니다.
다만 왜인지는 모르겠는데, dotnet publish 를 통해 dotnet cli를 이용하면 ReadyToRun 옵션을 모든 아키텍쳐에 대해 사용할 수 있지만, Visual Studio 2022에서는 win-x86, win-x64, win-arm, win-arm64 같은 Windows OS를 대상으로 하는 아키텍쳐에 대해서만 활성화되고, Linux, MacOS(osx) 계열에서는 활성화되지 않습니다.
-p:PublishTrimmed={true|false}
MSBuild 옵션은 -p를 붙여서 위와 같이 사용해야한다고 하네요.
오늘 제가 이 글을 쓰게 만든 주요 원인입니다. 위 사진에 보면, 제가 체크하지 않은 옵션이 있습니다.
사용되지 않는 코드 자르기 옵션인데요. 저는 단순하게 이것이 Code Inline 최적화 수준의 직관적이고 단순한 최적화라고 생각했습니다. 주석같은 것은 Release 환경에서는 당연히 지워지고 빌드되겠지만…
사실 코드 자르기 라고하길래 무슨 의미인지 직관적으로 다가 오지 않아서 사용했던 것이 큽니다. 그리고 설명한 간단하게만 읽어보면 요약했을 때
“Assembly 용량을 줄인다.”
이렇게 다가왔습니다. 하지만 제약사항이 있던 것은 몰랐죠.
- 자체 포함 애플리케이션 트리밍 - .NET | Microsoft Learn
- 트리밍 경고 소개 - .NET | Microsoft Learn
- 알려진 다듬기 비호환성 - .NET | Microsoft Learn
Trim을 다듬기 라고네요…ㅎㅎ
저는 MiniExcel 을 사용하여 메모리가 들고 있는 정보를 Excel 문서로 내보내는 기능을 사용하면서 개발하다가, 개발 할 때는 잘되었지만, PublishTrimmed 옵션을 사용해서 dotnet publish 하니까 익명타입을 읽어 들일 수 없다는 오류가 발생 했습니다.
내가 만든 어셈블리는 Trimming 된 .NET App 제약사항에 걸리지 않더라도, 내가 사용하는 Nuget Package가 위 제약사항 해당된다면 문제가 됩니다. 그리고 MiniExcel 뿐만 아니라 많은 기존의 Nuget Package들이 Reflection Code Emit 방식을 사용하고 있었을 것이고 Source Generator 형식으로 넘어가지 못한 Package도 많을 것이기에 직접 꼭 테스트해보고 사용해야할 것 같습니다.