.NET Framework에서 .NET으로 마이그레이션하기 전 체크리스트
· 업데이트: · Go Komura · .NET, .NET Framework, C#, 현대화, Windows 개발, 마이그레이션
수정 이력(7건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635142)
- 마이그레이션 판단의 분기, 착수 전 정비 순서, 종류별 난이도, 미지원 기술 점검, shared library를 자르는 방식 등을 그림으로도 따라갈 수 있도록 Mermaid 그림 32점을 추가했습니다(본문 500〜750자당 1그림 규약에 맞춘 것입니다). 본문 문장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- 배포 중인 Excel 내용(시트 구성, 카테고리 수와 항목 수, 기입란 용도)을 실제 파일을 확인한 뒤 추가했습니다. 마이그레이션에 쓰는 도구 절을 신설하고, Visual Studio의 packages.config 변환, 권장되지 않게 된 .NET Upgrade Assistant, 수작업 선택지를 표로 정리했습니다. 약어 초출을 풀어 쓰고, grep 실제 명령과 찾은 뒤의 다음 한 수 표를 추가했습니다.
- .NET 9는 표준 기간 지원(STS)인데 .NET 8과 같은 날에 지원이 끝나는 이유(STS 기간이 18개월에서 24개월로 연장된 것)를 보완하고, 참고 자료에 공식 수명 주기 목록을 추가했습니다. 날짜 자체는 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635141)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「.NET Framework에서 .NET으로 마이그레이션하기 전 체크리스트」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635141 https://comcomponent.com/ko/blog/dotnet-framework-to-dotnet-premigration-checklist/
- DOI(최신 버전)
- 10.5281/zenodo.21635141
- DOI(이 버전)
- 10.5281/zenodo.22217438
일본어·영어 시트가 포함된 Excel 체크리스트 다운로드
이 파일은 Checklist-ja와 Checklist-en 두 시트로 되어 있으며, 13장의 착수 전 체크리스트를 방침 / 현재 .NET Framework 쪽 정비 / 앱 종류와 기술 선정 / 미지원 기술·주의 API / Windows 전용 의존성 / shared library와 데이터 액세스 / 운영과 build의 7개 카테고리 34개 항목으로 풀어 놓은 것입니다. Status와 Notes 열은 비어 있으므로 프로젝트별 점검표로 그대로 쓸 수 있습니다. 글을 읽기 전에 한 번 보면 13장까지의 흐름을 잡기 쉽습니다.
.csproj의 TargetFramework를 net10.0으로 바꾸고, NuGet을 몇 개 업데이트해서 빌드가 통과하면 끝.
……그런 마이그레이션이라면 꽤 평화롭습니다. 실제로는 그렇게 되지 않는 경우가 더 많습니다.
.NET Framework 현장에는 System.Web, WCF, Web Forms, 오래된 packages.config, web.config.install.xdt, 네이티브 DLL, COM / ActiveX, 디자인 타임에만 동작하는 서드파티 컨트롤, 암묵적인 x86 전제, 디자이너 의존 ResX, 오래된 시리얼라이저처럼 평소에는 의식하지 않는 전제가 여럿 잠들어 있습니다.
그래서 .NET Framework에서 .NET으로의 마이그레이션에서 정말 중요한 것은 구현에 들어가기 전의 점검입니다. 착수 전에 쟁점을 나눌 수 있으면, 마이그레이션은 「큰 도박」이 아니라 「순서대로 해소하는 작업」이 됩니다.
flowchart TB
accTitle: 점검이 마이그레이션의 성격을 바꾼다
accDescr: 구현에 들어가기 전에 점검해 쟁점을 나눌 수 있으면 마이그레이션은 큰 도박이 아니라 순서대로 해소하는 작업이 됨을 보여주는 그림.
hid1["잠들어 있는 암묵적 전제들"] --> inv1["착수 전 점검으로 쟁점을 나눈다"]
inv1 --> tsk1["순서대로 해소하는 작업이 된다"]
hid1 -.->|"점검을 건너뛰면"| bet1["마이그레이션이 큰 도박이 된다"]
그림1: 착수 전에 쟁점을 나눌 수 있는지에 따라, 마이그레이션은 도박이 될 수도 작업이 될 수도 있다.
이 글에서는 기존 .NET Framework 4.x 업무 애플리케이션을 현재 .NET으로 마이그레이션하기 전에 무엇을 확인해 둘지를 정리합니다. 주된 대상은 다음입니다.
- 클래스 라이브러리
- 콘솔 앱
- Windows 서비스
- WinForms / WPF
- ASP.NET Framework(MVC / Web API / Web Forms)
- WCF를 쓰는 앱
- EF6을 쓰는 앱
작성 시점은 2026-03-15입니다. 지원 기간이나 공식 도구의 권장은 바뀌므로, 날짜가 떨어진 시점에 읽는다면 공식 정보도 함께 확인하세요.
1. 먼저 결론
처음에 빠지기 어려운 결론만 둡니다.
- 마이그레이션 전에 .NET Framework 쪽을 정리하는 것이 먼저입니다. Microsoft 공식 가이드에서도 이식 전에 .NET Framework 4.7.2 이상으로 올리고,
PackageReference화, SDK 스타일화, 의존성 업데이트를 먼저 마칠 것을 권장합니다. - 난이도는 코드 양보다 앱 모델로 정해집니다. 클래스 라이브러리나 콘솔은 비교적 가볍고, ASP.NET Framework, Web Forms, WCF 서버, WF(Workflow Foundation)는 무거워지기 쉽습니다.
- WinForms / WPF는 .NET으로 옮겨도 Windows 전용입니다. 여기를 오해하면 마이그레이션했는데 Linux 컨테이너에 올라가지 않는, 흔한 함정을 밟습니다.
- ASP.NET Framework → ASP.NET Core는 사실상 아키텍처 마이그레이션입니다. 소규모 앱이면 한 번에 갈 수도 있지만, 큰 프로덕션 계열은 단계 마이그레이션을 전제로 하는 편이 안전합니다.
- WCF와 EF6은 runtime 마이그레이션과 분리할 수 있는 경우가 있습니다. WCF 클라이언트는 .NET용 지원 패키지가 있고, EF6은 modern .NET으로 옮긴 뒤에 EF Core로 따로 마이그레이션할 수 있습니다.
- 반면 AppDomain 생성, .NET Remoting, CAS(Code Access Security, 코드 액세스 보안), COM+, Workflow Foundation, BinaryFormatter 의존성은 적신호입니다. 먼저 찾지 않으면 뒤에서 작업량이 폭발합니다.
packages.config/install.ps1/XDT(XML Document Transform,web.config등을 변환하는 메커니즘) /content자산 / 네이티브 DLL / COM / ActiveX / x86 전제는 빌드는 통과해도 런타임이나 디자인 타임에 실패하기 쉬우므로 착수 전에 훑습니다.- 2026-03 시점에서는 .NET 10이 LTS입니다. 신규 마이그레이션의 착지점은 기본적으로 현행 LTS를 기준으로 생각하는 것이 자연스럽습니다.
- 테스트, 계측, 롤백 준비가 없는 마이그레이션은 위험합니다. 마이그레이션은 구현 작업이라기보다, 전제 조건을 하나씩 드러내는 작업입니다.
이 글의 지식 맵
.NET Framework에서 .NET으로의 이행은 구현보다 앞서는 사전 점검이 핵심이며, packages.config의 후속인 PackageReference나 SDK 스타일 프로젝트로의 전환은 이행보다 먼저 끝내 두어야 할 작업으로 여겨집니다. AppDomain의 신규 생성이나 .NET Remoting, COM+, Workflow Foundation은 .NET과 양립하지 않으며, BinaryFormatter도 상황에 따라서는 양립하지 않습니다. WCF는 클라이언트와 서버에서 사정이 다르며, 서버 쪽은 CoreWCF나 gRPC로의 재설계가 필요해집니다. ASP.NET Framework의 후속은 ASP.NET Core지만 Web Forms는 같은 app model이 아니며, EF6에서 EF Core로의 이행은 runtime 이행과 분리해 뒤로 미루는 것이 안전하다고 여겨집니다. WinForms/WPF는 이행할 수 있어도 Windows 전용 그대로이며, Windows 전용 API 의존은 Windows Compatibility Pack으로 완화할 수 있습니다.
flowchart LR
accTitle: .NET Framework에서 .NET으로의 이행 전 체크리스트 지식 맵
accDescr: .NET Framework에서 .NET으로 이행하기 전에 정리해야 할 의존 관계 형식·비대응 기술·WCF나 EF6의 취급·Windows 전용 API 의존의 관계를 보여주는 그림
framework_to_dotnet_migration[".NET Framework에서 .NET으로 마이그레이션"]
dotnet_framework[".NET Framework"]
dotnet[".NET(Core 이후)"]
windows_compat_pack["Windows 호환 팩"]
package_reference["PackageReference"]
packages_config["packages.config"]
sdk_style_project["SDK 스타일 프로젝트"]
dotnet_upgrade_assistant[".NET Upgrade Assistant"]
github_copilot_modernization["GitHub Copilot 현대화"]
net_standard_2_0[".NET Standard 2.0"]
appdomain["AppDomain 생성"]
dotnet_remoting[".NET Remoting"]
com_plus["COM+(System.EnterpriseServices)"]
workflow_foundation["Workflow Foundation"]
binaryformatter["BinaryFormatter"]
wcf_server["WCF 서버(WCF 서비스 호스트)"]
corewcf["CoreWCF"]
grpc["gRPC"]
ef_core["EF Core"]
ef6["EF6(Entity Framework 6)"]
asp_net_core["ASP.NET Core"]
asp_net_framework["ASP.NET Framework(MVC/Web API)"]
web_forms["ASP.NET Web Forms"]
system_web["System.Web / HttpContext.Current"]
wpf["WPF"]
windows_forms["Windows Forms"]
windows_only_api_dependency["Windows 전용 API 의존"]
wcf_client["WCF 클라이언트"]
framework_to_dotnet_migration -->|"전제로 한다"| dotnet_framework
dotnet -->|"의 후속"| dotnet_framework
framework_to_dotnet_migration -.->|"이용한다"| windows_compat_pack
package_reference -->|"의 후속"| packages_config
sdk_style_project -->|"보다 먼저 해야 한다"| framework_to_dotnet_migration
package_reference -->|"보다 먼저 해야 한다"| framework_to_dotnet_migration
dotnet_upgrade_assistant -->|"사용은 비권장"| framework_to_dotnet_migration
github_copilot_modernization -->|"권장되는 대응"| framework_to_dotnet_migration
framework_to_dotnet_migration -.->|"이용한다"| net_standard_2_0
appdomain -->|"양립하지 않는다"| dotnet
dotnet_remoting -->|"양립하지 않는다"| dotnet
com_plus -->|"양립하지 않는다"| dotnet
workflow_foundation -->|"양립하지 않는다"| dotnet
binaryformatter -.->|"양립하지 않는다"| dotnet
wcf_server -.->|"전제로 한다"| corewcf
wcf_server -.->|"전제로 한다"| grpc
corewcf -->|"구현을 담당한다"| wcf_server
ef_core -->|"의 후속"| ef6
framework_to_dotnet_migration -->|"보다 먼저 해야 한다"| ef_core
asp_net_core -->|"의 후속"| asp_net_framework
web_forms -.->|"양립하지 않는다"| asp_net_core
system_web -->|"양립하지 않는다"| dotnet
wpf -.->|"이용한다"| binaryformatter
windows_forms -.->|"이용한다"| binaryformatter
wpf -.->|"이용한다"| windows_only_api_dependency
windows_forms -.->|"이용한다"| windows_only_api_dependency
windows_compat_pack -.->|"완화한다"| windows_only_api_dependency
dotnet -.->|"이용한다"| wcf_client
wcf_server -.->|"전제로 한다"| dotnet_framework
asp_net_framework -->|"전제로 한다"| dotnet_framework
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 30건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 애초에 「지금 당장 마이그레이션해야 하는가」를 먼저 정한다
처음에 정해야 할 것은 「어떻게 마이그레이션할 것인가」가 아니라, 정말로 지금 이 앱을 마이그레이션해야 하는가입니다.
여기를 모호하게 두면, 기술적으로는 맞지만 사업적으로는 너무 무거운 마이그레이션이 되거나, 반대로 분명히 옮기는 편이 좋은데 너무 미루는 판단이 되기 쉽습니다.
flowchart TB
accTitle: 처음에 정하는 질문
accDescr: 어떻게 마이그레이션할지보다 먼저, 정말로 지금 이 앱을 마이그레이션해야 하는지를 정해야 하며, 여기가 모호하면 사업적으로 너무 무거운 마이그레이션이나 지나친 연기가 되기 쉬움을 보여주는 그림.
q1["정말로 지금, 마이그레이션해야 하는가"] -->|"먼저 정한다"| how1["어떻게 마이그레이션할 것인가"]
q1 -.->|"모호한 채로 진행하면"| bad1["너무 무거운 마이그레이션 또는 지나친 연기"]
그림2: 「어떻게 마이그레이션할 것인가」보다 먼저 「지금 마이그레이션해야 하는가」를 정하지 않으면, 판단이 어느 한쪽으로 기울어진다.
2.1 .NET Framework에 남는 판단도 흔히 있을 수 있다
.NET Framework 4.8.1은 지원되는 Windows에 설치되어 있는 한 계속 지원됩니다. 즉, 지금 당장 전부를 modern .NET으로 바꾸지 않으면 바로 위험하다는 단순한 이야기가 아닙니다.
다만 잔류에는 분명한 제약이 있습니다.
- Windows 전용에서 벗어날 수 없다
- ASP.NET Web Forms나 오래된 서버 스택을 계속 안고 간다
- 새로운 .NET의 성능 개선, 언어 기능, 에코시스템의 혜택을 받기 어렵다
- 클라우드, 컨테이너, CI/CD의 요즘 전제와 어긋나기 쉽다
반대로 이런 의존성이 강하다면, 당장은 .NET Framework 4.8.1로 맞춰 안정 운영하면서 별도 라인으로 교체 계획을 세우는 것은 합리적입니다.
flowchart TB
accTitle: 잔류라는 합리적인 선택
accDescr: Web Forms나 WCF 서버 호환, COM+ 같은 강한 레거시 의존성이 있으면, 당장은 .NET Framework 4.8.1로 맞춰 안정 운영하면서 별도 라인으로 교체 계획을 세우는 것이 합리적임을 보여주는 그림.
dep1["강한 레거시 의존성이 있다"] --> stay1["당장은 4.8.1로 맞춰 안정 운영"]
stay1 --> plan1["별도 라인으로 교체 계획"]
stay1 -.-> lim1["Windows 전용 등의 제약은 남는다"]
그림3: 의존성이 깊으면 4.8.1에서의 안정 운영과 교체 계획의 투 트랙이 현실적이다.
- Web Forms 화면 자산이 대량이다
- WCF 서버 호환을 엄격히 유지해야 한다
- Workflow Foundation이나 COM+ 의존이 깊다
- 서드파티 디자인 타임 구성 요소가 modern .NET 미지원이다
- 비즈니스상 큰 사양 변경이 허용되지 않는다
2.2 선택지마다 무엇이 바뀌는가
| 선택 | 무엇이 좋아지는가 | 무엇이 남는가 / 잃는가 | 맞는 경우 |
|---|---|---|---|
| .NET Framework 4.8.1에 남는다 | 기존 자산을 깨지 않고 안정 운영하기 쉽다 | Windows 전용, 오래된 앱 모델, 현대화의 한계 | 강한 레거시 의존성이 있고, 지금은 사업을 우선해 안정 운영하고 싶다 |
| modern .NET으로 옮기되 Windows에 남는다 | 런타임과 툴체인을 modern화할 수 있다. 성능·개발 경험·SDK 스타일의 이점이 크다 | Windows API 의존성은 남는다. 크로스 플랫폼이 되지는 않는다 | WinForms / WPF, Windows Service, Windows API를 쓰는 업무 앱 |
| modern .NET으로 옮기고, 장차 Linux / 컨테이너 / 클라우드도 염두에 둔다 | 배포 대상의 자유도가 올라간다. 운영 모델도 새로 만들기 쉽다 | Windows 전용 API나 app model을 먼저 벗겨야 한다 | 서버 사이드를 클라우드에 맞추고 싶다, 인프라도 새로 하고 싶다 |
중요한 것은 마이그레이션하고 싶은가가 아니라 마이그레이션 후 어디에 착지하고 싶은가를 먼저 정하는 것입니다.
flowchart TB
accTitle: 정하는 것은 착지점
accDescr: 마이그레이션하고 싶은지가 아니라, 마이그레이션 후 어디에 착지하고 싶은지를 먼저 정하는 것이 중요함을 보여주는 그림.
ask1["마이그레이션하고 싶은가"] -.->|"질문으로 충분하지 않다"| dec1["먼저 정할 것"]
land1["마이그레이션 후 어디에 착지하고 싶은가"] -->|"이것을 정한다"| dec1
dec1 --> path1["선택지와 볼 쟁점이 정해진다"]
그림4: 질문을 「마이그레이션하고 싶은가」에서 「어디에 착지하고 싶은가」로 바꾸면 선택지가 정해진다.
3. 먼저 정해 둘 네 가지 방침
3.1 착지점의 .NET 버전
작성 시점에서는 Microsoft 지원 정책상 .NET 10이 LTS입니다. 한편 .NET 8 LTS와 .NET 9 STS는 둘 다 2026-11-10에 지원이 종료될 예정입니다. STS인데 LTS와 같은 날짜가 되는 것은, STS 지원 기간이 18개월에서 24개월로 연장되었기 때문입니다. 「STS는 18개월」이라는 이전 전제로 세면 .NET 9는 2026-05-12 종료로 보이므로, 날짜를 근거로 할 때는 공식 수명 주기 정보로 확인하세요.
그래서 이제부터 새로 .NET Framework에서 마이그레이션한다면, 특별한 사정이 없는 한 현행 LTS를 착지점으로 삼는 것이 자연스럽습니다.
여기에서의 실무적 생각은 단순합니다.
- 짧게 끝내고 싶은 작은 마이그레이션이면 현행 LTS에 바로 착지
- 장기 운영을 전제로 한 기간계 시스템이어도 역시 현행 LTS를 기본으로 생각한다
- 「기존 라이브러리 사정으로 직전 LTS로 하고 싶다」는 사정으로 있을 수 있지만, 언제까지 유지되는지를 날짜로 보고 판단한다
flowchart TB
accTitle: 착지 버전의 생각
accDescr: 작은 마이그레이션이든 기간계 시스템이든 현행 LTS를 기본으로 하고, 기존 라이브러리 사정으로 직전 LTS를 원할 때는 언제까지 유지되는지를 날짜로 보고 판단한다는 생각을 보여주는 그림.
tgt1["착지점의 .NET을 고른다"] -->|"기본"| lts1["현행 LTS에 착지"]
tgt1 -->|"기존 라이브러리 사정"| old1["직전 LTS도 검토"]
old1 --> dt1["유지 기한을 날짜로 확인하고 판단"]
그림5: 착지점은 현행 LTS가 기본이고, 이전 LTS를 고른다면 유지 기한 날짜를 근거로 한다.
3.2 Windows 전용으로 갈 것인가, 장차 크로스 플랫폼을 노릴 것인가
이 판단에 따라 볼 쟁점이 크게 달라집니다.
- Windows 전용으로 가면 WPF / WinForms나 Windows Compatibility Pack을 써서, 우선 runtime을 modern화하는 현실적인 길을 택할 수 있습니다.
- 장차 Linux / 컨테이너화도 노린다면
System.Drawing.Common, 레지스트리, WMI, EventLog, Windows Service, COM, Office Interop 같은 Windows 전제 API를 이른 단계에서 점검해야 합니다.
여기를 정하지 않고 마이그레이션을 시작하면, 중간에 「Windows 고정이 맞았나」「아니, 컨테이너에 올리고 싶었던 거였지」처럼 이야기가 꼬입니다.
flowchart TB
accTitle: Windows 전용인가 크로스 플랫폼인가의 분기
accDescr: Windows 전용으로 가면 호환 팩으로 먼저 runtime을 modern화하는 현실적인 길을 택할 수 있고, 장차 Linux나 컨테이너화를 노리면 Windows 전제 API를 이른 단계에서 점검해야 한다는 분기를 보여주는 그림.
aim1["어느 쪽을 노릴 것인가"] -->|"Windows 전용으로"| wn1["호환 팩으로 runtime을 modern화"]
aim1 -->|"장차는 컨테이너도"| xp1["Windows 전제 API를 일찍 점검"]
aim1 -.->|"정하지 않고 시작한다"| twist1["중간에 이야기가 꼬인다"]
그림6: 이 분기를 먼저 정하지 않으면 볼 쟁점이 정해지지 않아 중간에 꼬인다.
3.3 한 번에 할 것인가, 단계 마이그레이션으로 할 것인가
마이그레이션 형태는 크게 세 가지입니다.
- in-place에 가까운 일괄 마이그레이션
- side-by-side로 신구를 나란히 두는 마이그레이션
- route / library 단위로 조금씩 맞추는 단계 마이그레이션
특히 ASP.NET Framework 앱에서는 Microsoft 가이드에도 incremental migration이 분명히 안내되어 있습니다. 프로덕션을 멈추고 싶지 않고, 기능 수가 많고, 주변 의존성이 많다는 조건이라면, 처음부터 단계 마이그레이션을 전제로 설계하는 편이 무리가 없습니다.
flowchart TB
accTitle: 마이그레이션의 세 가지 형태
accDescr: 마이그레이션에는 in-place에 가까운 일괄 마이그레이션, side-by-side로 신구를 나란히 두는 마이그레이션, route나 library 단위로 조금씩 맞추는 단계 마이그레이션의 세 가지가 있으며, 프로덕션을 멈추고 싶지 않은 조건에서는 단계 마이그레이션 전제가 무리가 없음을 보여주는 그림.
typ1["마이그레이션 형태를 고른다"] --> t1["일괄 마이그레이션(in-place에 가깝게)"]
typ1 --> t2["side-by-side로 신구 병행"]
typ1 --> t3["route / library 단위의 단계 마이그레이션"]
t3 -.-> fit1["프로덕션을 멈추고 싶지 않다면 이것이 전제"]
그림7: 형태는 세 가지이고, 기능이 많아 멈출 수 없는 프로덕션 계열은 단계 마이그레이션을 전제로 둔다.
3.4 무엇을 「이번 마이그레이션 대상에서 뺄 것인가」
마이그레이션이 실패하기 쉬운 이유는 할 일을 너무 많이 담기 때문입니다.
예를 들어 다음을 동시에 하면 무거워지기 쉽습니다.
- .NET Framework → .NET
- ASP.NET Framework → ASP.NET Core
- EF6 → EF Core
- Windows 서버 → Linux 컨테이너
- 인증 기반 변경
- 로그 / 감시 기반 변경
- 데이터베이스 마이그레이션
물론 전부 언젠가는 필요할 수 있습니다. 다만 동시에 해야 하는가는 별개입니다.
현실에서는 다음처럼 분리하는 편이 잘 됩니다.
- 먼저 runtime과 프로젝트 구조를 modern화한다
- 그 위에서 app model을 옮긴다
- 마지막에 ORM, 인증, 클라우드, 감시를 업데이트한다
flowchart TB
accTitle: 너무 담지 않는 분리 순서
accDescr: 먼저 runtime과 프로젝트 구조를 modern화하고, 그 위에서 app model을 옮기고, 마지막에 ORM이나 인증, 클라우드, 감시를 업데이트하는 분리 순서를 보여주는 그림.
d1["runtime과 구조를 modern화"] --> d2["app model을 옮긴다"]
d2 --> d3["ORM·인증·클라우드·감시를 업데이트"]
d1 -.->|"동시에 담으면"| hv1["마이그레이션이 무거워져 실패하기 쉽다"]
그림8: 전부를 동시에 하지 않고, runtime·app model·주변의 순으로 분리하면 무거워지지 않는다.
4. 착수 전에 갖춰 둘 기반
Microsoft의 이식 전 가이드는 꽤 실무적입니다. 요컨대 마이그레이션 전에 지금의 .NET Framework 프로젝트를 modern한 입구로 맞춰 두라는 이야기입니다.
4.1 .NET Framework 4.7.2 이상으로 올린다
공식 가이드에서는 이식 전에 .NET Framework 4.7.2 이상을 타깃으로 할 것을 권장합니다. 이유는 .NET Standard가 기존 API를 그대로 갖지 않을 때도, 더 새로운 API 대체로 맞추기 쉬워지기 때문입니다.
실무에서는 가능하면 4.8.1을 기준으로 생각하는 편이 이해하기 쉽습니다.
- 지원 관점에서 자연스럽다
- .NET Framework 쪽의 최종 안정점으로 다루기 쉽다
- 「먼저 현행 Framework 쪽에서 정리한다」는 방침을 세우기 쉽다
이것을 먼저 하면 무엇이 바뀌는가
.NET Standard 2.0공유 라이브러리 다루기가 안정되기 쉬워진다- 오래된 런타임에서 오는 노이즈를 먼저 줄일 수 있다
- 호환성 문제를 「Framework의 오래됨」과 「modern .NET화」 중 어느 쪽이 원인인지 구분하기 쉬워진다
4.2 PackageReference로 맞춘다
이식 전 가이드에서는 참조를 PackageReference 형식으로 맞출 것을 권장합니다.
이것을 먼저 해 두면 의존성 관리의 가시성이 크게 좋아집니다.
PackageReference로 하면 무엇이 바뀌는가
- 패키지 참조가
csproj에 모인다 - 전이 의존성이 보기 쉬워진다
- restore 전제가 modern .NET 쪽과 맞춰진다
- CLI / CI와의 궁합이 좋아진다
다만 여기에는 함정이 있습니다.
전형적인 함정
NuGet 공식 문서에는 packages.config에서 PackageReference로 옮길 때의 다음 제약이 명시되어 있습니다.
- Visual Studio의 built-in 마이그레이션은 ASP.NET 프로젝트에서 쓸 수 없다
install.ps1/uninstall.ps1에 의존하는 패키지는 기대대로 동작하지 않을 수 있다content폴더의 자산은 무시될 수 있다web.config.install.xdt같은 XDT 변환은 적용되지 않는다lib바로 아래 어셈블리 구성이 오래된 패키지는 잘 해석되지 않을 수 있다
즉 패키지 형식만 바꾼다고 생각하지 않는 편이 좋습니다.
특히 classic ASP.NET은 NuGet 패키지 설치 시 web.config를 바꾸는 문화가 꽤 있었기 때문에, 마이그레이션 때 암묵적 전제가 드러나기 쉽습니다.
flowchart TB
accTitle: PackageReference화에서 드러나는 암묵적 전제
accDescr: packages.config에서 PackageReference로 옮기면 install.ps1이나 XDT 변환, content 폴더 자산이 적용되지 않는 경우가 있어, 설치 시 web.config를 바꾸던 암묵적 전제가 드러나기 쉬움을 보여주는 그림.
cv1["PackageReference로 변환"] --> np1["install.ps1이 동작하지 않을 수 있다"]
cv1 --> nx1["XDT 변환은 적용되지 않는다"]
cv1 --> nc1["content 자산이 무시될 수 있다"]
np1 --> exp1["암묵적 전제가 드러난다"]
nx1 --> exp1
nc1 --> exp1
그림9: 형식 변환만이라고 생각해도, 설치 시 장치에 기대던 전제가 한꺼번에 드러난다.
무엇으로 변환하는가
수작업만은 아닙니다. 다만 도구마다 커버 범위가 다릅니다.
| 도구 | 할 수 있는 일 | 전제와 주의점 |
|---|---|---|
| Visual Studio의 변환 기능 | 솔루션 탐색기에서 「참조」 또는 packages.config를 마우스 오른쪽 클릭하고 Migrate packages.config to PackageReference...를 고르면 변환됩니다 |
Visual Studio 2017 15.7 이후. ASP.NET과 C++ 프로젝트에서는 쓸 수 없습니다. 메뉴에 나오지 않으면 NuGet 복원이나 패키지 관리자를 한 번 연 뒤 다시 마우스 오른쪽 클릭합니다 |
| .NET Upgrade Assistant | 프로젝트 분석과 업그레이드를 한꺼번에 합니다 | 공식 문서상 비권장이며, GitHub Copilot 앱 현대화를 쓰도록 안내됩니다 |
| GitHub Copilot 앱 현대화 | 평가, 계획, 코드 수정, 검증까지 지원합니다. 현행 공식 안내의 중심입니다 | 4.5와 같이 Visual Studio와 Copilot, C# 코드가 전제입니다 |
| 수작업 SDK 스타일화 | 새 csproj를 만들고 필요한 항목만 옮깁니다 |
파일 수가 적은 프로젝트라면 이것이 가장 빠를 수도 있습니다 |
Visual Studio의 변환 기능은 실행 전에 프로젝트 백업을 만들고, 마지막에 변환 보고서(최상위 의존성, 전이 의존성, 검출된 호환성 문제)를 냅니다. 되돌리려면 백업 폴더에서 csproj와 packages.config를 다시 쓰고, 패키지 관리자 콘솔에서 update-package -reinstall을 실행하는 절차가 문서에 적혀 있습니다.
즉 다시 할 수 있는 형태로 시험할 수 있다는 것입니다. 착수 전 견적에서는 먼저 프로젝트 하나만 변환해 보고서를 읽는 편이 빠릅니다.
flowchart TB
accTitle: 변환을 다시 할 수 있는 형태로 시험한다
accDescr: Visual Studio의 변환 기능은 백업을 만든 뒤 변환하고 변환 보고서를 내므로, 먼저 프로젝트 하나만 변환해 보고서를 읽고 필요하면 백업에서 되돌릴 수 있음을 보여주는 그림.
bk1["백업이 만들어진다"] --> cv2["프로젝트 하나만 변환"]
cv2 --> rp1["변환 보고서를 읽는다"]
rp1 -->|"문제가 있으면"| rv1["되돌리고 재설치"]
rp1 -->|"문제가 없으면"| go1["다른 프로젝트로 넓힌다"]
그림10: 변환은 백업과 보고서가 있으므로, 먼저 한 건으로 감을 잡는다.
4.3 SDK 스타일로 맞춘다
이식 전 가이드에서는 SDK 스타일 프로젝트 형식으로의 변환도 권장합니다.
이것은 꽤 효과가 큽니다.
SDK 스타일로 하면 무엇이 바뀌는가
csproj가 크게 간결해진다PackageReference와 궁합이 좋다- multi-targeting하기 쉽다
dotnet build/dotnet test/dotnet publish를 중심으로 한 CI/CD로 맞추기 쉽다- modern .NET 쪽 구성에 가까워져 후반 차이가 줄어든다
반대로 말하면 오래된 csproj와 오래된 NuGet 관리 그대로 갑자기 modern .NET으로 뛰면 차이가 너무 크다는 것입니다.
flowchart TB
accTitle: SDK 스타일화로 차이를 줄인다
accDescr: 오래된 csproj와 오래된 NuGet 관리 그대로 갑자기 modern .NET으로 뛰면 차이가 너무 크므로, 먼저 SDK 스타일로 맞춰 modern .NET 쪽 구성에 가깝게 하고 후반 차이를 줄임을 보여주는 그림.
oldp["오래된 csproj + 오래된 NuGet 관리"] -.->|"갑자기 뛴다"| big1["차이가 너무 크다"]
oldp -->|"먼저 SDK 스타일화"| sdk1["modern 쪽 구성에 가까워진다"]
sdk1 --> small1["후반 차이가 줄어든다"]
그림11: SDK 스타일화를 끼우면 modern .NET으로 뛸 때의 차이가 한 단계 작아진다.
4.4 의존성을 먼저 업데이트한다
이것도 공식 가이드대로이지만, 의존성은 사용 가능한 최신 버전으로 맞추고, 가능하면 .NET Standard 대응 판으로 맞춥니다.
이것을 먼저 하는 의미
- 「이 패키지는 modern .NET에서 쓸 수 있는가」가 빨리 드러난다
- 오래된 의존성이 노이즈가 되는 것을 막을 수 있다
- shared library를
netstandard2.0화하기 쉬워진다 - 이후 마이그레이션 작업을 「코드 이식」에 집중시키기 쉽다
flowchart TB
accTitle: 의존성을 먼저 업데이트하는 의미
accDescr: 의존성을 최신에 가깝게 먼저 업데이트해 두면 modern .NET에서 쓸 수 있는지가 빨리 드러나고, 오래된 의존성의 노이즈를 피할 수 있으며, 이후 마이그레이션 작업을 코드 이식에 집중시킬 수 있음을 보여주는 그림.
upd1["의존성을 먼저 최신으로"] --> know1["modern .NET 대응이 빨리 드러난다"]
upd1 --> noise1["오래된 의존성의 노이즈를 줄일 수 있다"]
know1 --> foc1["이후는 코드 이식에 집중할 수 있다"]
noise1 --> foc1
그림12: 의존성 업데이트를 먼저 마칠수록, 마이그레이션 본체의 작업은 이식 그 자체에 집중할 수 있다.
4.5 공식 도구의 전제도 확인해 둔다
2026-03 시점에서는 Microsoft 안내의 무게 중심이 GitHub Copilot 앱 현대화 쪽으로 옮겨 있습니다. 기존의 마이그레이션 지원 도구만 전제로 하기보다, 평가, 계획, 코드 수정, 검증까지 포함한 일련의 지원 흐름으로 보는 편이 실무에 맞습니다.
다만 현행 문서에서는 Visual Studio 2026 또는 지원 중인 Visual Studio 2022 계열, GitHub Copilot, 그리고 C# 코드가 전제입니다.
이 확인이 필요한 이유
- 공식 도구에 무엇을 기대할 수 있는지가 바뀐다
- 팀의 IDE / build agent / 확장 기능 전제를 맞출 수 있다
- VB.NET 솔루션에서는 자동화에 너무 기대하지 않는다는 판단을 할 수 있다
VB.NET이 섞인 현장은 드물지 않습니다. 그래서 「최신 공식 도구가 어디까지 도와줄 수 있는가」를 처음에 확인해 둘 가치가 있습니다.
flowchart TB
accTitle: 공식 도구의 현재 위치와 전제
accDescr: .NET Upgrade Assistant는 비권장이 되었고 안내의 무게 중심은 GitHub Copilot 앱 현대화로 옮겨 있지만, Visual Studio와 Copilot과 C# 코드가 전제이므로 VB.NET 솔루션에서는 자동화에 너무 기대하지 않는 판단이 필요함을 보여주는 그림.
ua1[".NET Upgrade Assistant"] -->|"비권장·안내 대상"| cp1["GitHub Copilot 앱 현대화"]
cp1 --> pre1["전제: Visual Studio + Copilot + C#"]
pre1 -.->|"VB.NET이 섞이면"| vb1["자동화에 너무 기대하지 않는다"]
그림13: 도구의 무게 중심은 Copilot 앱 현대화로 옮겨 있으며, 그 전제에 맞지 않는 현장은 손질이 필요하다.
5. 프로젝트 종류별 난이도를 가늠한다
마이그레이션은 「.NET Framework에서 .NET으로」로 한 덩어리로 말하기 쉽지만, 현실에서는 프로젝트 종류마다 다른 게임입니다.
5.1 대략적인 난이도 감
| 종류 | 난이도 감 | 주된 쟁점 |
|---|---|---|
| 클래스 라이브러리 | 낮음〜중간 | API 호환성, 의존성, 타깃 분할 |
| 콘솔 / 배치 / 일부 Windows Service | 낮음〜중간 | 배포 방식, 네이티브 의존성, 설정 |
| WinForms / WPF | 중간 | Windows 전용 그대로, 디자이너, 서드파티 UI, BinaryFormatter 주변 |
| ASP.NET MVC / Web API | 중간〜높음 | ASP.NET Core로의 app model 마이그레이션, 인증, 세션, 설정, DI |
| ASP.NET Web Forms | 높음 | 화면 모델 차이가 크다, UI 계층 치환 전제 |
| WCF 클라이언트 | 중간 | 패키지 교체, 계약, 구성 |
| WCF 서버 | 높음 | CoreWCF인가 gRPC / HTTP API 재설계인가 |
| EF6 → EF Core 동시 실시 | 높음 | ORM이 다른 물건, 동작 차이, 마이그레이션 이력 |
5.2 클래스 라이브러리는 「공유 경계」를 어떻게 자를지가 열쇠다
클래스 라이브러리는 비교적 옮기기 쉽습니다. 다만 그것은 정말로 라이브러리가 라이브러리답게 분리되어 있을 때에 한합니다.
다음과 같은 의존성이 있으면 난이도가 올라갑니다.
System.Web을 건드린다HttpContext.Current를 직접 본다- WPF / WinForms 형을 공개 API에 포함한다
- 레지스트리, WMI, EventLog 등 Windows API에 너무 기대고 있다
AppDomain이나 Remoting에 의존한다
비즈니스 로직만 잘라낼 수 있으면 가볍고, 앱 모델까지 끌어안고 있으면 무겁다고 보면 이해하기 쉽습니다.
flowchart TB
accTitle: 클래스 라이브러리 무게의 보는 법
accDescr: 비즈니스 로직만 잘라낼 수 있는 클래스 라이브러리는 가볍고, System.Web이나 UI 형, Windows API, AppDomain 등 앱 모델까지 끌어안고 있으면 무거워짐을 보여주는 그림.
lib1["클래스 라이브러리를 본다"] -->|"로직만 잘라낼 수 있다"| lt1["마이그레이션은 가볍다"]
lib1 -->|"앱 모델을 끌어안고 있다"| hv2["마이그레이션은 무겁다"]
hv2 -.-> sm1["System.Web·UI 형·Windows API 의존성 등"]
그림14: 라이브러리 난이도는 줄 수가 아니라, 앱 모델을 얼마나 끌어안고 있는지로 정해진다.
5.3 WinForms / WPF는 마이그레이션하기 쉽지만 Windows 전용이다
WinForms와 WPF는 .NET으로 마이그레이션할 수 있습니다. 다만 둘 다 Windows 전용 프레임워크 그대로입니다.
여기서 기대치를 잘못 잡으면 위험합니다.
- 좋아지는 것
- modern .NET의 런타임, 언어, SDK 스타일에 올라탈 수 있다
- CI/CD나 package 관리를 요즘에 맞추기 쉽다
- 일부 성능·유지보수성 개선을 얻기 쉽다
- 바뀌지 않는 것
- Windows 전용이라는 점
- UI 컨트롤이나 디자인 타임 구성 요소의 궁합 문제가 남는다는 점
- ActiveX / COM / 네이티브 DLL 문제가 사라지지 않는다는 점
또한 WinForms / WPF는 BinaryFormatter의 영향 확인이 필요한 경우가 있습니다. 특히 clipboard, drag & drop, ResX, 디자인 타임 직렬화에 custom type이 끼면, target을 .NET 9 이후로 올렸을 때 표면화하기 쉽습니다.
flowchart TB
accTitle: WinForms / WPF 마이그레이션의 기대치
accDescr: WinForms와 WPF는 .NET으로 옮길 수 있고 modern .NET의 런타임이나 언어, SDK 스타일에는 올라탈 수 있지만, Windows 전용인 것은 바뀌지 않으며 BinaryFormatter의 영향 확인이 필요한 경우도 있음을 보여주는 그림.
ui1["WinForms / WPF를 마이그레이션"] --> gain1["modern .NET의 이점에 올라탈 수 있다"]
ui1 --> keep1["Windows 전용 그대로"]
ui1 -.-> bfc1["BinaryFormatter 확인이 필요하다"]
그림15: 데스크톱 UI는 옮길 수 있지만, Windows 전용이라는 성질은 그대로 가져간다.
5.4 ASP.NET Framework는 「runtime 마이그레이션」이 아니라 「app model 마이그레이션」이다
ASP.NET Framework에서 ASP.NET Core로의 마이그레이션은 Microsoft 가이드에서도 non-trivial이라고 명시합니다. 이것은 단순히 API 이름이 바뀌기 때문이 아니라, 전제가 되는 아키텍처가 다르기 때문입니다.
차이가 나기 쉬운 곳은 다음입니다.
- Hosting model
- Middleware pipeline
- Request processing model
- Session / Cache
- Authentication / Authorization
- Configuration
- Dependency Injection
- Logging / Monitoring
ASP.NET Framework 앱에서 먼저 확인하고 싶은 것은 다음입니다.
- 어느 route / endpoint부터 먼저 옮길 수 있는가
System.Web의존성을 shared library에서 뗄 수 있는가- 인증 / 세션 / 예외 처리 / 로그를 어떻게 맞출 것인가
- 프로덕션을 멈추지 않고 단계 마이그레이션할 것인가
특히 큰 앱은 처음부터 incremental migration을 전제로 생각하는 편이 현실적입니다.
flowchart TB
accTitle: ASP.NET 마이그레이션은 app model 마이그레이션
accDescr: ASP.NET Framework에서 ASP.NET Core로의 마이그레이션은 API 이름 교체가 아니라 hosting이나 middleware, 인증, 구성, DI 같은 전제 아키텍처가 바뀌는 app model 마이그레이션이며, 큰 앱은 단계 마이그레이션 전제가 현실적임을 보여주는 그림.
an1["ASP.NET Framework → Core"] --> am1["전제 아키텍처가 바뀐다"]
am1 --> pts1["hosting·middleware·인증·구성·DI"]
am1 -->|"큰 앱"| inc1["incremental migration 전제로 계획"]
그림16: ASP.NET 마이그레이션은 runtime이 아니라 app model의 마이그레이션이며, 규모가 클수록 단계 전제가 된다.
5.5 Web Forms는 「자산 마이그레이션」이 아니라 「책임 분해」부터 들어간다
Web Forms는 ASP.NET Core와 같은 app model이 아닙니다. 그래서 견적상 화면 자산을 그대로 가져갈 수 있다는 전제로 생각하지 않는 편이 안전합니다.
실무에서는 먼저 다음 분해부터 들어가는 경우가 많습니다.
- 화면 로직과 비즈니스 로직을 나눈다
Page/UserControl/ViewState에 묻힌 책임을 분해한다- 업무 로직이나 데이터 액세스를 shared library로 빼낸다
- UI는 Razor Pages / MVC / Blazor 등 다른 모델로 재구성한다
즉 Web Forms 건은 runtime 마이그레이션보다 먼저 책임 분해 계획이 있는지가 중요합니다.
flowchart TB
accTitle: Web Forms는 책임 분해부터 들어간다
accDescr: Web Forms는 ASP.NET Core와 같은 app model이 아니므로, 화면 로직과 비즈니스 로직을 나누고, 로직을 shared library로 빼내고, UI는 다른 모델로 재구성하는 책임 분해부터 들어감을 보여주는 그림.
wf1["Web Forms 화면 자산"] --> sep1["화면 로직과 업무 로직을 나눈다"]
sep1 --> mv1["로직을 shared library로 빼낸다"]
mv1 --> re1["UI는 다른 모델로 재구성"]
wf1 -.->|"그대로 가져갈 수 있다는 전제"| ngw1["견적이 무너진다"]
그림17: Web Forms는 자산 마이그레이션이 아니라, 책임을 분해한 뒤 다른 모델로 다시 짜는 전제로 본다.
5.6 WCF 클라이언트와 WCF 서버는 나눠서 생각한다
여기는 한데 묶지 않는 편이 좋습니다.
WCF 클라이언트
WCF Client에는 modern .NET용 지원되는 NuGet 패키지가 있습니다. 그래서 WCF를 호출하는 쪽만이면 보기보다 무겁지 않은 경우가 있습니다.
WCF 서버
한편 WCF 서비스를 호스트하는 쪽은 다릅니다. Microsoft 가이드에서는 modern화 경로로 크게 다음 두 가지가 안내됩니다.
- CoreWCF를 써서 기존 클라이언트 호환을 유지하는 방향
- gRPC 등 modern한 RPC / HTTP 기반으로 맞추는 방향
다만 CoreWCF는 WCF의 전부를 그대로 가져오는 것이 아니라 서브셋입니다. 기존 클라이언트와의 호환 유지에는 맞지만, 코드 변경과 테스트는 전제입니다.
flowchart TB
accTitle: WCF는 클라이언트와 서버에서 따로 평가한다
accDescr: WCF 클라이언트는 modern .NET용 지원 패키지가 있어 보기보다 무겁지 않은 경우가 있는 반면, 서버 쪽은 호환 유지의 CoreWCF인지 gRPC 등으로의 재설계인지를 골라야 함을 보여주는 그림.
wcf1["WCF를 어느 쪽에서 쓰는가"] -->|"클라이언트"| cl1["지원되는 패키지로 마이그레이션"]
wcf1 -->|"서버"| sv1["경로를 고른다"]
sv1 -->|"호환 유지"| cw1["CoreWCF(서브셋)"]
sv1 -->|"재설계"| gr1["gRPC / HTTP API"]
그림18: 호출하는 쪽과 호스트하는 쪽은 난이도와 선택지가 다르므로, 한데 묶어 견적하지 않는다.
6. .NET에서 쓸 수 없거나 그대로는 막히기 쉬운 기술을 훑는다
이것은 착수 전에 반드시 해 두고 싶은 부분입니다. Microsoft에는 .NET Framework에서는 쓸 수 있었지만 .NET 6+에서는 쓸 수 없는 기술 목록이 있습니다.
6.1 적신호가 되기 쉬운 기술
| 기술 | .NET에서의 상태 | 어떻게 봐야 하는가 |
|---|---|---|
AppDomain.CreateDomain 등 AppDomain 생성 |
미지원 | 격리는 별도 프로세스 / 컨테이너 / AssemblyLoadContext로 생각한다 |
| .NET Remoting | 미지원 | IPC, HTTP, gRPC, Socket, Pipe 등으로 재설계 |
| CAS / Security Transparency | 보안 경계로서 미지원 | OS / 컨테이너 / 권한 분리로 생각한다 |
System.EnterpriseServices(COM+) |
미지원 | COM+ 전제 설계를 분리·치환한다 |
| Workflow Foundation | 미지원 | CoreWF 등 대체를 포함해 별도 견적으로 생각한다 |
| WCF server | built-in으로는 그대로가 아니다 | CoreWCF인지 gRPC인지를 고른다 |
| BinaryFormatter | .NET 9 이후에는 구현이 항상 예외 | 시리얼라이저 마이그레이션, ResX / clipboard / drag & drop 감사 |
6.2 AppDomain은 「일부 API가 남아 있어도 생성은 다른 문제」다
AppDomain 주변은 조금 복잡합니다.
.NET에서도 일부 API surface는 남아 있지만, 새 AppDomain을 만들어 격리한다는 쓰임새는 지원되지 않습니다.
그래서 다음 용도로 AppDomain을 쓰고 있었다면 재설계가 필요합니다.
- 플러그인 격리
- 동적 로드의 폐기
- 부분 신뢰 코드의 격리
- 일시적인 실행 환경의 분리
마이그레이션 전에 봐야 할 것은 AppDomain이라는 단어가 나오는지만이 아니라, 무엇을 위해 AppDomain을 쓰고 있었는지입니다.
flowchart TB
accTitle: AppDomain 쓰임새로 판단이 바뀐다
accDescr: AppDomain은 일부 API가 남아 있어도 새 AppDomain을 만들어 격리하는 쓰임새는 지원되지 않으므로, 형만 쓰는지 격리 목적인지를 가리고, 격리 목적이면 별도 프로세스나 AssemblyLoadContext로 재설계함을 보여주는 그림.
ad1["AppDomain이 나왔다"] -->|"형이나 정보 참조만"| okc1["그대로 가는 경우가 많다"]
ad1 -->|"만들어 격리하고 있었다"| rd1["재설계가 필요하다"]
rd1 --> alt1["별도 프로세스 / 컨테이너 / AssemblyLoadContext"]
그림19: 단어의 유무가 아니라 「무엇을 위해 쓰고 있었는지」로 재설계 필요 여부가 갈린다.
6.3 Remoting은 「보기보다 깊다」
Remoting은 물론이고, delegate의 BeginInvoke() / EndInvoke() 호출 같은 비동기 델리게이트도 영향 범위에 들어갈 수 있습니다. 이것은 Remoting 그 자체는 아니지만, modern .NET에서는 지원되지 않으므로 마이그레이션 전에 훑을 필요가 있습니다.
그래서 검색할 때는 다음도 함께 보는 편이 안전합니다.
System.Runtime.RemotingMarshalByRefObjectRealProxyBeginInvoke(/EndInvoke(
6.4 BinaryFormatter는 target version에서 갑자기 전면에 드러난다
BinaryFormatter는 오래된 코드베이스일수록 「의식하지 않고 쓰고 있는」 경우가 있습니다.
- 영속 데이터
- 캐시
- 세션 저장
- 플러그인 상태
- clipboard / drag & drop
- ResX
- WinForms / WPF 디자이너 주변
.NET 9 이후 BinaryFormatter는 runtime에 구현이 포함되지 않고, API는 항상 PlatformNotSupportedException을 던집니다.
즉 이것은 「나중에 생각」이 아니라, target version을 정한 시점에 먼저 감사할 쟁점입니다.
flowchart TB
accTitle: BinaryFormatter가 전면에 드러나는 경로
accDescr: BinaryFormatter는 영속 데이터나 ResX, clipboard 등에서 의식 없이 쓰이기 쉽고, .NET 9 이후에는 API가 항상 예외를 던지므로 target version을 정한 시점에 먼저 감사해야 하는 쟁점임을 보여주는 그림.
hid2["의식 없는 이용(ResX·clipboard 등)"] --> tg1["target을 .NET 9 이후로 정한다"]
tg1 --> ex1["API가 항상 예외를 던진다"]
ex1 --> aud1["정한 시점에 먼저 감사한다"]
그림20: BinaryFormatter는 target을 정한 순간에 효과가 나타나므로, 미루지 말고 먼저 감사한다.
6.5 먼저 grep해 두고 싶은 검색어
착수 전에 솔루션 전체에 다음 단어로 검색만 해도 풍경이 꽤 바뀝니다.
System.Web
HttpContext.Current
System.Runtime.Remoting
MarshalByRefObject
AppDomain
BinaryFormatter
ServiceHost
ChannelFactory
System.EnterpriseServices
Workflow
packages.config
web.config.install.xdt
install.ps1
DllImport
AxInterop
Microsoft.Office.Interop
이것들이 하나라도 나왔다고 바로 탈락이라는 뜻은 아닙니다. 어디가 표준 경로로 마이그레이션되고, 어디가 별도 트랙이 되는지를 알기 위한 지도입니다.
실제 검색 명령
도구는 rg(ripgrep)이든 PowerShell이든 상관없습니다.
# PowerShell. 솔루션 아래에서 한꺼번에 검색한다
Get-ChildItem -Recurse -Include *.cs,*.vb,*.config,*.csproj,*.vbproj |
Select-String -Pattern 'BinaryFormatter|System\.Runtime\.Remoting|MarshalByRefObject|AppDomain\.CreateDomain' |
Select-Object Path, LineNumber, Line
# ripgrep. 먼저 건수로 감을 잡고 싶을 때
rg -n --stats "BinaryFormatter|System\.Runtime\.Remoting|AppDomain\.CreateDomain" --glob "*.cs" --glob "*.vb"
찾았으면 다음에 무엇을 하는가
검색은 입구이고, 그다음 판단이 본론입니다. 단어마다 다음에 볼 곳이 정해져 있습니다.
| 나온 단어 | 다음에 확인할 것 | 착지점 |
|---|---|---|
System.Web / HttpContext.Current |
shared library에서 밖으로 빼낼 수 있는가. 호출 쪽에서 DTO로 다시 담을 수 있는가 | 8.5의 전략으로 뗀다 |
System.Runtime.Remoting / MarshalByRefObject / BeginInvoke |
무엇을 위한 원격 호출이었는가. 프로세스 간인가, 머신 간인가, 단순한 비동기 호출인가 | IPC, HTTP, gRPC, Task 기반으로 재설계 |
AppDomain |
형 이름만 쓰는지, CreateDomain으로 격리하는지 |
격리 목적이면 별도 프로세스나 AssemblyLoadContext(6.2) |
BinaryFormatter |
쓴 데이터를 나중에 읽어야 하는가. ResX나 clipboard를 통해 간접적으로 쓰지 않는가 | 다른 시리얼라이저로 옮기고, 기존 데이터 읽기 계획도 세운다(6.4) |
ServiceHost / ChannelFactory |
호스트 쪽인가 클라이언트 쪽인가 | 5.6과 같이 나눠서 평가한다 |
packages.config / install.ps1 / web.config.install.xdt |
설치 시 무엇을 바꾸고 있었는가 | 4.2의 함정. 설정을 명시적으로 다시 가진다 |
DllImport / AxInterop / Microsoft.Office.Interop |
비트 수 전제와 런타임에 필요한 파일 전부 | 9.4의 bitness 확인으로 |
여기까지 하면 「마이그레이션할 수 있는가」가 아니라 「어느 것이 표준 경로이고, 어느 것이 별도 견적인가」의 목록이 됩니다. 착수 전에 필요한 것은 바로 이런 형태의 목록입니다.
flowchart TB
accTitle: 검색에서 목록으로
accDescr: 검색은 입구이며, 나온 단어마다 다음에 확인할 곳을 따라가면 어느 것이 표준 경로로 마이그레이션되고 어느 것이 별도 견적이 되는지의 목록으로 바뀜을 보여주는 그림.
grep1["검색어로 솔루션을 훑는다"] --> nxt1["단어마다 다음 확인 장소를 따라간다"]
nxt1 --> map1["표준 경로와 별도 견적의 목록이 된다"]
grep1 -.-> note2["나와도 바로 탈락은 아니다"]
그림21: 검색 그 자체는 입구이고, 판단까지 나아가면 착수 전에 필요한 목록 형태가 된다.
7. Windows 전용 전제를 어디까지 허용할지 정한다
마이그레이션에서 자주 나오는 오해가 「.NET으로 옮기면 크로스 플랫폼이 된다」는 것입니다. 그런 마법은 없습니다. 앱이 Windows에 깊게 묶여 있으면, 마이그레이션 후에도 보통 Windows 전용입니다.
7.1 Windows 전용으로 옮기는 것은 충분히 현실적이다
Microsoft에는 Windows Compatibility Pack이 있어, 레지스트리, WMI, EventLog, Windows Service, Directory Services 등 많은 Windows 계열 API를 modern .NET에서 쓸 수 있게 하는 수단이 있습니다.
이 존재는 실무에서 큽니다.
- 먼저 modern .NET으로 옮기고 싶다
- 하지만 당장은 Windows에서 나오지 않는다
- 그래서 Windows API 의존성은 일단 허용하고 싶다
는 현장에서는 유력한 선택지가 됩니다.
마이그레이션의 첫 목표가 반드시 크로스 플랫폼화일 필요는 없습니다.
7.2 다만 Windows 전용 API는 「나중에 돌아오는 빚」이기도 하다
Windows Compatibility Pack이 있다고 해서 무엇이든 안심은 아닙니다.
- Linux 컨테이너에 올리고 싶다
- Kubernetes 전제로 돌리고 싶다
- macOS / Linux 개발자도 같은 build를 돌리고 싶다
- 장차 클라우드에서 Windows VM을 줄이고 싶다
같은 목표가 있다면, Windows API 의존성은 지금부터 가시화해 두는 편이 좋습니다.
flowchart TB
accTitle: 호환 팩의 쓰임새와 빚
accDescr: 당장은 Windows에서 나오지 않으면 Windows 호환 팩으로 Windows API 의존성을 일단 허용하고 modern .NET으로 옮길 수 있지만, 장차 컨테이너나 클라우드를 노린다면 그 의존성은 나중에 돌아오는 빚으로 지금부터 가시화해야 함을 보여주는 그림.
now1["먼저 modern .NET으로 옮기고 싶다"] -->|"당장은 Windows 그대로"| pack1["호환 팩으로 의존성을 일단 허용"]
pack1 -.->|"장차 컨테이너·클라우드를 노린다면"| debt1["의존성은 나중에 돌아오는 빚"]
debt1 --> vis1["지금부터 가시화해 둔다"]
그림22: 호환 팩은 현실적인 다리지만, 그 의존성은 목표에 따라 빚이 되기도 하므로 가시화해 둔다.
7.3 System.Drawing.Common은 특히 오해하기 쉽다
System.Drawing.Common은 .NET 6 이후 Windows 전용 라이브러리입니다.
이미지 처리나 문자 그리기에 쓰는 코드가 있다면, 먼저 어느 쪽으로 갈지를 정해야 합니다.
- Windows 그대로 운영할 것인가
- 장차 Linux / macOS에서도 돌리고 싶은가
전자라면 당장 그대로여도 되는 경우가 있습니다. 후자라면 SkiaSharp나 ImageSharp 등으로의 교체를 마이그레이션 계획에 처음부터 넣어야 합니다.
flowchart TB
accTitle: System.Drawing.Common의 분기
accDescr: System.Drawing.Common은 .NET 6 이후 Windows 전용 라이브러리이므로, Windows 그대로 운영하면 당장 그대로여도 되는 경우가 있고, 장차 Linux나 macOS에서도 돌리려면 SkiaSharp나 ImageSharp로의 교체를 계획에 처음부터 넣어야 함을 보여주는 그림.
sd1["System.Drawing.Common을 쓰고 있다"] -->|"Windows 그대로 운영"| ok4["당장 그대로여도 되는 경우가 있다"]
sd1 -->|"Linux / macOS에서도 돌리고 싶다"| repl1["SkiaSharp / ImageSharp로 교체"]
repl1 --> plan2["마이그레이션 계획에 처음부터 넣는다"]
그림23: System.Drawing.Common은 Windows 전용이므로, 착지점에 따라 다루기가 처음부터 갈린다.
7.4 Windows 고정을 나타내는 대표적인 냄새
다음과 같은 참조나 API가 있을 때는 「적어도 처음에는 Windows 전용으로 옮긴다」는 전제로 견적하는 편이 안전합니다.
Microsoft.Win32.RegistrySystem.ManagementSystem.Diagnostics.EventLogSystem.ServiceProcessSystem.DirectoryServicesSystem.DrawingDllImport/ P/Invoke- COM 참조
AxInterop.*Microsoft.Office.Interop.*
8. 공유 라이브러리를 자르는 방식에 따라 난이도가 바뀐다
큰 솔루션에서는 마이그레이션의 성패가 shared library를 어떻게 자르는지로 정해진다고 해도 과언이 아닙니다.
8.1 먼저 분류한다
라이브러리는 크게 세 종류로 나누면 정리하기 쉽습니다.
- 순수한 업무 로직 / 도메인 로직
- 앱 모델에 조금 의존하는 중간층
- UI / Web / Windows API에 밀착한 층
이 중 가장 먼저 옮겨야 할 것은 1입니다.
- 계산
- 규칙 판정
- DTO / 계약
- 도메인 서비스
- 단순한 데이터 변환
여기가 깨끗이 빠지면 난이도가 한순간에 내려갑니다.
flowchart TB
accTitle: 라이브러리의 세 분류와 옮기는 순서
accDescr: 라이브러리를 순수한 업무 로직, 앱 모델에 조금 의존하는 중간층, UI나 Windows API에 밀착한 층의 세 종류로 나누고, 가장 먼저 옮기는 것은 순수한 업무 로직임을 보여주는 그림.
c1["순수한 업무 로직"] -->|"가장 먼저 옮긴다"| ez1["깨끗이 빠지면 난이도가 내려간다"]
c2["앱 모델 쪽 중간층"] -->|"그다음"| ez1
c3["UI·Windows API 밀착층"] -->|"마지막"| ez1
그림24: 라이브러리는 세 층으로 분류하고, 순수한 로직부터 차례로 구해 낸다.
8.2 netstandard2.0은 지금도 유효한 다리다
Microsoft 가이던스에서는 .NET Framework 쪽과도 공존해야 하는 shared library라면 먼저 .NET Standard 2.0을 생각하는 것이 기본입니다.
여기서 중요한 것은 두 가지입니다.
- .NET Framework는
.NET Standard 2.1을 지원하지 않는다 - shared library를 old / new 양쪽에서 참조하고 싶다면 2.0이 현실적인 해가 되기 쉽다
flowchart TB
accTitle: netstandard2.0이라는 다리
accDescr: .NET Framework는 .NET Standard 2.1을 지원하지 않으므로, shared library를 신구 양쪽에서 참조하고 싶다면 .NET Standard 2.0이 현실적인 해가 되기 쉬움을 보여주는 그림.
fw1[".NET Framework 쪽"] -->|"참조할 수 있다"| ns1["netstandard2.0 shared library"]
mn1["modern .NET 쪽"] -->|"참조할 수 있다"| ns1
ns21["netstandard2.1"] -.->|"Framework는 미지원"| fw1
그림25: 신구 양쪽에서 참조하는 다리로는 2.1이 아니라 2.0이 현실적인 해가 된다.
8.3 netstandard2.0으로 하면 무엇이 바뀌는가
| 방침 | 어떻게 바뀌는가 | 맞는 경우 | 주의점 |
|---|---|---|---|
netstandard2.0화 |
old / new 양쪽에서 참조하기 쉽다 | 순수한 업무 로직, 공통 계약, 유틸리티 | app model 고유 API는 올릴 수 없다 |
multi-target(예: net48;net10.0) |
공통 코드를 유지하면서 환경별 차이를 가질 수 있다 | 환경 차이가 조금 있는 라이브러리 | 조건 분기나 build 관리가 늘어난다 |
바로 net10.0 전용화 |
장차는 가장 깨끗하다 | old / new 공존이 필요 없는 신규 층 | .NET Framework에서는 참조할 수 없다 |
8.4 호환 모드는 만능이 아니다
.NET Standard 2.0에는 .NET Framework 라이브러리를 참조하는 호환 모드가 있습니다. 다만 이것은 무엇이든 투명하게 동작하는 마법이 아닙니다.
예를 들어 WPF 같은 app model 고유 API를 전제로 한 라이브러리는 보통 어렵습니다. 즉 shared library라고 해도, 정말로 shared할 수 있는 책임만으로 좁히는 것이 중요합니다.
8.5 ASP.NET 계열 라이브러리는 System.Web을 뗄 수 있는지가 승부다
ASP.NET Framework를 단계 마이그레이션하는 경우, shared library가 HttpContext.Current나 System.Web에 직접 매달려 있으면 꽤 힘듭니다.
이때의 기본 전략은 다음 중 하나입니다.
System.Web의존성을 인터페이스 밖으로 밀어낸다HttpContext에서 온 정보를 DTO로 받도록 바꾼다- 마이그레이션 과도기에 adapter를 쓴다
- 그래도 안 되면 multi-target으로 단계적으로 뗀다
8.6 라이브러리는 leaf-first로 올린다
ASP.NET incremental migration 가이드에서는 supporting library를 postorder depth-first, 즉 잎부터 차례로 올리라고 명시합니다.
이것은 Web에 한정되지 않고 일반 솔루션에서도 꽤 유효합니다.
- 의존 대상이 먼저 올라가 있어 상위 층의 가시성이 좋아진다
- 호환성 문제를 국지화하기 쉽다
- 라이브러리 단위로 테스트하기 쉽다
flowchart TB
accTitle: leaf-first로 올린다
accDescr: supporting library를 의존의 잎부터 차례로 올리면 의존 대상이 먼저 올라가 있어 상위 층의 가시성이 좋아지고, 호환성 문제를 국지화해 라이브러리 단위로 테스트하기 쉬워짐을 보여주는 그림.
lf1["의존의 잎 라이브러리를 먼저 올린다"] --> lf2["의존 대상이 끝난 중간층을 올린다"]
lf2 --> lf3["마지막에 상위 앱 층"]
lf1 -.-> loc1["문제를 국지화해 테스트할 수 있다"]
그림26: 잎부터 차례로 올리면, 상위 층에 착수할 무렵에는 발밑이 맞춰져 있다.
9. NuGet / 외부 의존성 / 서드파티 구성 요소를 점검한다
여기를 대충 하면 마이그레이션 후반에 가장 아픕니다.
9.1 의존성은 네 종류로 나누면 정리하기 쉽다
- 공개 NuGet 패키지
- 사내 private package / internal library
- 로컬 DLL 참조
- COM / ActiveX / 네이티브 DLL / SDK
이 중 1만 봐서는 부족합니다. 정말 위험한 것은 3과 4입니다.
flowchart TB
accTitle: 의존성의 네 분류와 위험도
accDescr: 의존성은 공개 NuGet, 사내 패키지, 로컬 DLL 참조, COM이나 ActiveX나 네이티브 DLL의 네 종류로 나뉘며, 공개 NuGet만 봐서는 부족하고 정말 위험한 것은 로컬 DLL과 COM·네이티브 계열임을 보여주는 그림.
dp1["의존성을 점검한다"] --> k1["공개 NuGet·사내 패키지"]
dp1 --> k3["로컬 DLL 참조"]
dp1 --> k4["COM / ActiveX / 네이티브 DLL"]
k3 --> dg1["정말 위험한 것은 이쪽"]
k4 --> dg1
그림27: 공개 NuGet만 세어도 부족하고, 로컬 DLL과 COM·네이티브 계열이야말로 먼저 훑는다.
9.2 의존성마다 확인하고 싶은 것
각 의존성에 대해 최소한 여기까지는 확인합니다.
- modern .NET을 타깃으로 하는가
PackageReference에 대응하는가- SDK 스타일에서 문제없는가
- x86 / x64 / ARM64 제약은 없는가
- 디자인 타임 도구나 Visual Studio 확장에 의존하지 않는가
- install script / config transform을 전제로 하지 않는가
- 지원이 계속되는가
9.3 서드파티 UI / 리포트 / 디자인 타임 구성 요소는 별도로 견적한다
WinForms / WPF / ASP.NET 마이그레이션에서는 여기가 꽤 큽니다.
- 그리드
- 리포트 엔진
- PDF 출력 구성 요소
- 그래프 구성 요소
- 디자이너 통합형 UI 라이브러리
- ActiveX 래퍼
이들은 runtime만이 아니라 디자인 타임 지원이 얽힙니다. 마이그레이션 견적에서 「컴파일되는가」만 보면 보통 빗나갑니다.
9.4 네이티브 DLL과 bitness는 반드시 본다
.NET Framework 시대에 AnyCPU로 돌아가는 것처럼 보여도, 실제로는 이런 것에 의존하는 경우가 있습니다.
x86고정 COM- 32bit 전용 ActiveX
- 특정 버전의 VC++ Runtime
- 서명된 네이티브 DLL
이 부분은 modern .NET화가 갑자기 만든 문제가 아니라, 원래 있던 제약이 표면화하는 것뿐입니다. 그래서 마이그레이션 전에 가시화해 둘 가치가 있습니다.
flowchart TB
accTitle: bitness 제약의 표면화
accDescr: AnyCPU로 돌아가는 것처럼 보여도 x86 고정 COM이나 32bit 전용 ActiveX, 특정 버전의 VC++ Runtime 등에 의존하는 경우가 있으며, 이들은 원래 있던 제약이 modern .NET화에서 표면화하는 것뿐임을 보여주는 그림.
any1["AnyCPU로 돌아가는 것처럼 보였다"] -.-> hid3["x86 고정 COM·32bit ActiveX 등에 의존"]
hid3 --> surf1["마이그레이션에서 원래의 제약이 표면화한다"]
surf1 --> pre2["마이그레이션 전에 가시화해 둔다"]
그림28: bitness 문제는 마이그레이션이 만드는 것이 아니라, 원래의 제약이 보이게 될 뿐이다.
10. EF6, 시리얼라이저, 데이터 쪽을 별도 문제로 다룬다
runtime 마이그레이션과 데이터 액세스나 직렬화의 재설계는 되도록 별도 문제로 다루는 편이 잘 됩니다.
10.1 EF6 → EF Core는 직접 업그레이드가 아니다
Microsoft EF 가이드에서도 EF Core는 EF6의 total rewrite이며, direct upgrade path는 없다고 합니다.
그래서 EF6을 쓰는 앱에서는 다음 순서가 현실적입니다.
- 먼저 modern .NET으로 옮긴다
- 필요하면 EF6을 유지한 채로 앱을 돌린다
- 그다음에 EF Core로 별도 프로젝트로서 마이그레이션한다
runtime migration과 ORM migration을 같이 하지 않는다. 이것만으로 난이도는 꽤 내려갑니다.
flowchart TB
accTitle: EF6과 runtime의 분리 마이그레이션
accDescr: EF Core는 EF6의 전면 재작성이라 직접 업그레이드 경로가 없으므로, 먼저 modern .NET으로 옮기고 필요하면 EF6을 유지한 채로 돌린 뒤, 나중에 별도 프로젝트로 EF Core로 마이그레이션하는 순서가 현실적임을 보여주는 그림.
e1["먼저 modern .NET으로 옮긴다"] --> e2["필요하면 EF6을 유지한 채로 돌린다"]
e2 --> e3["나중에 EF Core로 별도 프로젝트로서 마이그레이션"]
e1 -.->|"동시에 하면"| mix1["ORM의 동작 차이가 섞여 무거워진다"]
그림29: runtime과 ORM은 동시에 움직이지 않고, EF6을 남긴 채로 먼저 runtime을 옮기는 것이 현실적이다.
10.2 EF6을 남기면 무엇이 바뀌는가
- 좋은 점
- 데이터 액세스 층의 차이를 뒤로 미룰 수 있다
- business logic이나 app model 마이그레이션에 집중할 수 있다
- 「EF Core의 동작 차이」가 섞이지 않는다
- 주의점
- 신규 개발 관점에서는 EF Core가 본류다
- EF6 Designer / EDMX 이용 형태에는 별도의 제약이 있다
10.3 EDMX 기반 EF6은 「디자인 타임」까지 본다
EF6 문서에는 EF Designer는 .NET / .NET Standard 프로젝트나 SDK 스타일 .NET Framework 프로젝트에서 직접 지원되지 않는다고 적혀 있습니다.
EDMX 기반 앱에서는 이 세 가지를 나눠서 생각해야 합니다.
- 런타임에 동작하는가
- Designer를 쓸 수 있는가
- 생성 코드를 어떻게 다룰 것인가
EDMX를 많이 쓰고 있다면, 이것은 처음에 견적에 넣어 두는 편이 안전합니다.
10.4 BinaryFormatter나 독자 직렬화는 「숨은 의존성」이 되기 쉽다
시리얼라이저는 코드 검색만으로는 놓치기 쉽습니다.
- 영속 포맷
- 메시징
- 캐시
- 구 WCF / SOAP 계약
- ResX
- 클립보드 / drag & drop
이 부분은 데이터 호환성도 얽힙니다. 즉 단순히 「빌드됐는가」가 아니라, 오래된 데이터를 읽을 수 있는가까지 확인이 필요합니다.
11. 구성, 배포, 운영, CI/CD까지 마이그레이션 대상에 넣는다
마이그레이션 대상은 소스 코드만이 아닙니다.
11.1 설정 파일
.NET Framework 쪽에서는 app.config / web.config에 꽤 많은 것이 올라가 있는 경우가 있습니다.
- connection string
- custom config section
- WCF endpoint 설정
- binding redirect
- diagnostics
- ASP.NET의 각종 설정
- package install 시의 transform 결과
modern .NET 쪽에서는 구성을 가지는 방식이나 읽기 경로가 바뀌는 장면이 있습니다. 그래서 「설정 파일은 나중에」는 위험합니다.
처음에 할 일은 설정의 점검입니다.
- 설정 파일에 무엇이 있는가
- 어느 것이 앱 시작 시 필수인가
- 어느 것이 환경 차이인가
- 어느 것이 NuGet이나 installer에 의해 자동 주입되었는가
11.2 배포 방식
배포 형태도 착수 전에 봅니다.
- IIS 아래인가
- Windows Service인가
- Scheduled Task인가
- ClickOnce / MSI / 독자 installer인가
- 온프레미스 서버 전제인가
- self-contained / framework-dependent 중 어느 쪽이 맞는가
실행 본체는 옮겨도 배포와 기동 메커니즘이 오래된 전제 그대로이면 마지막에 막힙니다.
11.3 로그, 감시, 운영 절차
운영 쪽도 놓치기 쉽습니다.
- Windows Event Log 전제인가
- Performance Counter를 보는가
- WMI 기반 감시인가
- 서비스 계정이나 권한이 고정인가
- 로그 출력처가 로컬 파일 전제인가
마이그레이션 후 코드는 돌아도 운영이 돌아가지 않는 일은 흔히 있습니다.
11.4 CI/CD와 build agent
마이그레이션 전에 다음도 확인합니다.
- build agent에 필요한 .NET SDK가 들어가는가
nuget.exe/msbuild.exe전제 pipeline을 어떻게 할 것인가dotnetCLI 기반으로 맞출 것인가- 테스트 실행, coverage, publish 잡을 어떻게 업데이트할 것인가
- 사내 템플릿이나 reusable pipeline이 오래된 형식 전제가 아닌가
사람 로컬 환경에서는 도는데 CI가 죽는다는 마이그레이션의 단골입니다.
flowchart TB
accTitle: 마이그레이션 대상은 코드만이 아니다
accDescr: 실행 본체를 옮겨도 설정 파일, 배포 방식, 로그나 감시 같은 운영, CI/CD와 build agent가 오래된 전제 그대로이면 마지막에 막히므로 이들도 마이그레이션 대상에 넣음을 보여주는 그림.
code1["소스 코드 마이그레이션"] --> also1["그것만으로는 끝나지 않는다"]
also1 --> cfg1["설정 파일과 배포 방식"]
also1 --> ops1["로그·감시·운영 절차"]
also1 --> ci1["CI/CD와 build agent"]
ci1 -.-> gotcha1["로컬에서는 도는데 CI가 죽는다"]
그림30: 설정·배포·운영·CI/CD까지 넣어야 비로소 마이그레이션 대상을 센 것이 된다.
12. 현실적인 마이그레이션 진행 방법
여기까지의 쟁점을 바탕으로 하면, 현실적인 진행은 대체로 다음 형태에 떨어집니다.
12.1 먼저 현행 Framework 쪽을 갖춘다
- .NET Framework 4.7.2 이상, 가능하면 4.8.1로 맞춘다
- 의존성을 올린다
packages.config를 다시 본다- 가능한 범위에서
PackageReference와 SDK 스타일로 맞춘다 - 현행 앱이 그 상태에서 제대로 동작하는지 확인한다
여기만 해도 이후 차이가 꽤 줄어듭니다.
12.2 shared library를 먼저 구해 낸다
다음에 업무 로직이나 공통 계약을 netstandard2.0 또는 multi-target으로 맞춥니다.
올리는 순서는 leaf-first가 기본입니다.
12.3 앱 본체는 app model마다 전략을 바꾼다
- 클래스 라이브러리 / 콘솔 / 일부 서비스 비교적 곧장 진행하기 쉽다
- WinForms / WPF Windows 전용으로 modern화한다
- ASP.NET MVC / Web API 작으면 일괄, 무거우면 단계 마이그레이션
- Web Forms 화면 자산 치환을 전제로, shared logic을 먼저 빼낸다
- WCF 서버 CoreWCF 유지인지 gRPC 재설계인지를 먼저 정한다
12.4 「한 번에 전부 하지 않는다」를 지킨다
특히 피하고 싶은 것은 이런 조합입니다.
- runtime 마이그레이션 + ORM 전면 교체
- runtime 마이그레이션 + 인증 기반 변경
- runtime 마이그레이션 + 클라우드 전면 마이그레이션
- runtime 마이그레이션 + 감시 기반 변경
- runtime 마이그레이션 + UI 프레임워크 쇄신
전부 필요해도 같은 스프린트에 쌓지 않는 편이 대개 잘 됩니다.
12.5 테스트와 베이스라인을 잡고 나서 움직인다
최소한 이 정도는 준비한 뒤에 움직이고 싶은 부분입니다.
- 단위 테스트
- 주요 업무 흐름의 결합 테스트
- 대표적인 화면 / API의 스냅샷적 확인
- 성능 베이스라인
- 주요 로그 확인 방법
- 롤백 절차
마이그레이션 후 「무엇이 깨졌는지」를 특정할 수 없는 상태로 진행하는 것은 꽤 위험합니다.
flowchart TB
accTitle: 움직이기 전에 준비할 것
accDescr: 테스트와 성능 베이스라인, 로그 확인 방법, 롤백 절차를 준비한 뒤 마이그레이션에 움직이면 마이그레이션 후 무엇이 깨졌는지를 특정할 수 있고, 준비 없이 진행하는 것은 위험함을 보여주는 그림.
prep1["테스트와 베이스라인을 준비"] --> mv2["그다음에 마이그레이션에 움직인다"]
mv2 --> det1["무엇이 깨졌는지를 특정할 수 있다"]
non2["준비하지 않은 채로 진행"] -.-> risk1["깨져도 특정하지 못해 위험"]
그림31: 마이그레이션은 움직이기 전 준비로 정해지며, 베이스라인이 있어야 비로소 깨진 곳을 특정할 수 있다.
13. 착수 전 체크리스트
프로젝트 관리에 그대로 붙일 수 있는 형태로 둡니다.
13.1 방침
- 왜 마이그레이션하는지를 한 문장으로 말할 수 있다
- 이번 착지점이 Windows 전용 modern .NET인지, 장차 크로스 플랫폼화인지 정해져 있다
- target .NET 버전을 정했다
- 이번 범위에 넣지 않을 것(EF Core화, 인증 쇄신, 클라우드 전면 마이그레이션 등)이 정해져 있다
13.2 현행 .NET Framework 쪽 정비
- .NET Framework 4.7.2 이상, 가능하면 4.8.1로 맞췄다
- 의존성을 최신에 가깝게 올렸다
packages.config유무를 훑었다PackageReference화 가능 여부를 확인했다- SDK 스타일화 가능 여부를 확인했다
- 현행 앱이 그 상태에서 빌드·기동·테스트된다
13.3 앱 종류와 기술 선정
- 클래스 라이브러리 / 데스크톱 / Web / WCF 등 종류마다 난이도를 나눴다
- WinForms / WPF가 Windows 전용임을 이해하고 있다
- ASP.NET Framework는 ASP.NET Core로의 app model 마이그레이션임을 이해하고 있다
- Web Forms UI 층 치환을 견적에 넣었다
- WCF는 클라이언트와 서버를 나눠 평가했다
13.4 미지원 기술·주의 API
AppDomain의존성을 훑었다- Remoting /
MarshalByRefObject/BeginInvoke/EndInvoke를 훑었다 - CAS / Security Transparency / COM+ / WF를 훑었다
- BinaryFormatter 의존성을 훑었다
System.Web의존성을 훑었다
13.5 Windows 전용 의존성
- 레지스트리, WMI, EventLog, Windows Service, Directory Services 이용을 훑었다
System.Drawing.Common이용을 훑었다- COM / ActiveX / Office Interop / P/Invoke / 네이티브 DLL을 훑었다
- x86 / x64 / ARM64 제약을 확인했다
13.6 shared library와 데이터 액세스
- shared library를 업무 로직 / app model 밀착층으로 분류했다
netstandard2.0화할 수 있는 것을 훑었다- multi-target이 필요한 라이브러리를 훑었다
- EF6을 남기고 runtime만 먼저 옮길 수 있는지 판단했다
- EDMX / Designer 의존성을 확인했다
13.7 운영과 build
- 설정 파일을 점검했다
- 배포 방식(IIS / Service / MSI / ClickOnce 등)을 확인했다
- 로그 / 감시 / 권한 / 실행 계정 전제를 확인했다
- CI/CD와 build agent 업데이트가 필요한지 확인했다
- 롤백 절차를 만들었다
14. 정리
.NET Framework에서 .NET으로의 마이그레이션에서 중요한 것은 「어느 명령으로 옮길 것인가」보다 무엇이 그대로 가고, 무엇이 별도 문제인지를 착수 전에 꿰뚫는 것입니다.
포인트를 좁히면 이 여섯 가지입니다.
- 마이그레이션 전에 .NET Framework 쪽을 정리한다
- 앱 모델마다 난이도를 나눈다
- 쓸 수 없는 기술을 먼저 훑는다
- Windows 전용 전제를 어디까지 허용할지 정한다
- shared library를 어떻게 자를지 정한다
- ORM, 인증, 클라우드 마이그레이션을 동시에 너무 담지 않는다
마이그레이션은 처음 일주일 동안 견적 정밀도가 꽤 바뀝니다. 거꾸로 말하면, 그 일주일에 쟁점을 정리할 수 있으면 후반은 꽤 평범한 개발로 맞출 수 있습니다.
flowchart TB
accTitle: 처음 일주일의 쓰임
accDescr: 처음 일주일에 무엇이 그대로 가고 무엇이 별도 문제인지의 쟁점을 정리할 수 있으면 견적 정밀도가 올라가고, 후반은 꽤 평범한 개발로 맞출 수 있음을 보여주는 그림.
wk1["처음 일주일에 쟁점을 정리"] --> est1["견적 정밀도가 올라간다"]
est1 --> nor1["후반은 평범한 개발로 맞출 수 있다"]
wk1 -.-> pts2["그대로 갈 것과 별도 문제의 구분"]
그림32: 처음 일주일을 점검에 쓸 수 있는지가 마이그레이션 전체의 정밀도를 정한다.
「먼저 net10.0으로 바꿔 보자」는 탐색으로는 나쁘지 않습니다.
다만 프로덕션 마이그레이션으로는 그 전에 볼 곳이 있다. 이 글에서 정리한 것은 바로 그 부분입니다.
15. 참고 자료
- 코드를 이식하기 위한 전제 조건
- .NET Framework에서 .NET으로의 이식 개요
- .NET 6 이후에는 사용할 수 없는 .NET Framework 기술
- GitHub Copilot 앱 현대화란
- GitHub Copilot 앱 현대화 설치
- .NET Upgrade Assistant 개요 - 현재는 비권장이며, GitHub Copilot 앱 현대화가 안내됩니다.
- 공식 .NET 지원 정책
- Microsoft .NET and .NET Core 수명 주기 - 각 버전의 지원 시작일과 종료일 목록.
- .NET Framework 공식 지원 정책
- 도구를 사용해 ASP.NET Framework를 ASP.NET Core로 마이그레이션하기
- ASP.NET Framework에서 ASP.NET Core로의 마이그레이션
- Get started with incremental ASP.NET to ASP.NET Core migration
- Use the Windows Compatibility Pack to port code to .NET
- .NET Standard
- Cross-platform targeting for .NET libraries
- packages.config에서 PackageReference로 마이그레이션하기
- PackageReference in project files
- BinaryFormatter migration guide
- Windows Forms를 위한 BinaryFormatter 마이그레이션 가이드
- WCF Client Support Policy
- CoreWCF Support Policy
- Why migrate WCF to ASP.NET Core gRPC
- Port from EF6 to EF Core
- EF6의 새로운 기능
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
업무 앱의 DB 스키마를 버전 관리한다 ── 「고객사마다 DB가 다르다」를 막는 마이그레이션 실무
고객사마다 흩어진 업무 앱의 DB 스키마를 버전 관리하는 실무 가이드. PRAGMA user_version과 전진 마이그레이션의 C# 구현, EF Core Migrations·DbUp·자체 구현 판단표, 2단계 릴리스까지 정리합니다.
VB6 앱은 언제까지 동작하는가 ── 런타임 지원 현황과 현실적인 .NET 이전 진행 방법
VB6 앱은 언제까지 동작할까요. 런타임은 Windows 11에서도 동작 대상이고 IDE는 지원이 종료된 현황을 정리하고, 전면 재작성·자동 변환·단계 이전의 판단표, 이전 전 자산 파악, VB6와 .NET의 비호환까지 정리합니다.
WMI/CIM을 C#·PowerShell에서 쓰기 ── 하드웨어 정보 가져오기·프로세스 모니터링·원격 조회의 실무 가이드
PC 시리얼 번호 조회, 디스크 여유 공간 모니터링, 프로세스 시작 감지의 흔한 답이 WMI/CIM입니다. Get-CimInstance 등 CIM cmdlet 사용법과 구 Get-WmiObject에서의 이전, C#의 System.Managemen...
MSMQ는 언제까지 쓸 수 있는가 ── 「deprecated조차 아닌」 레거시 큐의 마이그레이션 판단
MSMQ는 공식 deprecated 목록에 없는 한편, System.Messaging은 .NET Framework에만 있어 .NET으로의 이전을 가로막습니다. 폐지 소문과 실제 현재 위치를 사실로 정리하고, 계속 쓸지·옮길지의 판단 기준과 이전 ...
업무 시스템의 코드 설계 ── 상품 코드·고객 코드를 정하는 법과 check digit
상품 코드·고객 코드 등 업무 시스템의 코드 체계를 정하는 실무 가이드. 유의미 코드와 무의미 일련번호 판단표, JAN·Luhn 등의 check digit 산식과 C# 구현, Excel의 선행 0 소실 대책, 자릿수 초과와 이행까지 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
기존 자산 활용 & 이관 지원
.NET Framework, Web Forms, WCF, COM / ActiveX, 오래된 NuGet 운영이 얽힌 기존 자산 정리라서, 레거시 자산 마이그레이션 상담 주제로 잘 맞는 내용입니다.
기술 상담 & 설계 리뷰
마이그레이션 범위, 단계 마이그레이션을 자르는 방식, Windows 전용 전제를 어디까지 허용할지를 착수 전에 정리하고 싶다면, 기술 상담·설계 리뷰로 진행하기 쉬운 주제입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- .NET Framework에서 .NET으로 마이그레이션하기 전에 먼저 무엇을 해야 하나요?
- 구현에 들어가기 전에 .NET Framework 쪽 정리를 먼저 마칩니다. Microsoft 공식 가이드에서도 이식 전에 .NET Framework 4.7.2 이상(실무에서는 가능하면 4.8.1)으로 올리고, packages.config를 PackageReference로 바꾸고, SDK 스타일 프로젝트 형식으로 맞추고, 의존성을 최신에 가깝게 업데이트하는 것을 권장합니다. 이 작업을 먼저 해 두면 이후 modern .NET화의 차이가 크게 줄고, 호환성 문제의 원인이 Framework의 오래됨인지 .NET화인지 구분하기 쉬워집니다. 더불어 AppDomain, Remoting, BinaryFormatter 같은 미지원 기술 점검도 착수 전에 합니다.
- .NET Framework에 그대로 두는 판단도 있을 수 있나요?
- 흔히 있습니다. .NET Framework 4.8.1은 지원되는 Windows에 설치되어 있는 한 계속 지원되므로, 지금 당장 전부를 modern .NET으로 바꾸지 않으면 바로 위험하다는 이야기는 아닙니다. Web Forms 화면 자산이 대량인 경우, WCF 서버 호환을 엄격히 유지해야 하는 경우, Workflow Foundation이나 COM+ 의존이 깊은 경우, 서드파티 디자인 타임 구성 요소가 미지원인 경우라면, 당장은 4.8.1로 맞춰 안정 운영하면서 별도 라인으로 교체 계획을 세우는 편이 합리적입니다. 다만 Windows 전용에서 벗어날 수 없고, 새로운 .NET의 성능이나 언어 기능을 누리기 어렵다는 제약은 남습니다.
- .NET으로 옮기지 못하거나 막히기 쉬운 기술은 무엇인가요?
- AppDomain 생성, .NET Remoting, CAS(코드 액세스 보안), COM+(System.EnterpriseServices), Workflow Foundation은 modern .NET에서 미지원이라 재설계가 필요한 적신호입니다. WCF 서버는 built-in으로는 그대로 동작하지 않아 CoreWCF 또는 gRPC / HTTP API로 재설계를 고르게 됩니다. BinaryFormatter는 .NET 9 이후 구현이 포함되지 않고 항상 예외를 던지므로, 영속 데이터와 ResX, clipboard / drag & drop까지 포함한 감사가 필요합니다. 그 밖에 packages.config의 install.ps1 / XDT 변환, 네이티브 DLL, COM / ActiveX, x86 전제도 빌드는 통과해도 런타임에 실패하기 쉬운 주의 지점입니다.
- WinForms나 WPF 앱을 .NET으로 마이그레이션하면 크로스 플랫폼이 되나요?
- 되지 않습니다. WinForms / WPF는 .NET으로 옮겨도 Windows 전용 프레임워크입니다. 마이그레이션으로 얻는 것은 modern .NET 런타임·언어 기능·SDK 스타일·CI/CD와의 궁합 같은 이점이지, Linux 컨테이너에 올라가지는 않습니다. 앞으로 Linux / 컨테이너화를 노린다면 System.Drawing.Common, 레지스트리, WMI, EventLog, Windows Service, COM, Office Interop 같은 Windows 전제 API를 이른 단계에서 점검해야 합니다. 반대로 Windows 전용으로 간다면 Windows Compatibility Pack으로 먼저 runtime을 modern화하는 현실적인 길을 택할 수 있습니다.