.NET Framework 코드를 .NET Core로 마이그레이션하는 과정에서 여러 가지 이슈를 만나게 된다. 오늘은 ConfigurationManager를 사용하면서 발생할 수 있는 유닛 테스트 관련 문제와 그 해결 방법을 정리해보려고 한다.
먼저 마이그레이션 시나리오를 살펴보자:
기존 .NET Framework에서 사용하던 구성 파일(app.config, web.config)을 .NET Core 환경에서도 그대로 사용하고 싶은 경우가 많다. appSettings뿐만 아니라 커스텀 configSection도 유지하고 싶다는 요구사항이 있었다. 다음은 실제 사용한 구성 파일의 예시다:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="CustomServiceConfigs" type="LibraryCore.CustomSectionHandler, LibraryCore"/>
</configSections>
<CustomServiceConfigs>
<ServiceItem name="apiService" priority="0" assembly="LibraryCore.ApiProvider, LibraryCore"/>
<ServiceItem name="cacheService" priority="1" assemblyLibraryCore.CacheProvider, LibraryCore"/>
</CustomServiceConfigs>
<appSettings>
<add key="environment" value="production"/>
</appSettings>
</configuration>
위와 같은 구성을 처리하기 위해 다음과 같은 단계를 진행했다:
1. NuGet 패키지 추가: System.Configuration.ConfigurationManager
NuGet 패키지 관리자에서 System.Configuration.ConfigurationManager를 검색하여 프로젝트에 추가한다.
2. 커스텀 Section 처리 코드 마이그레이션
기존 .NET Framework에서 작성했던 커스텀 Section 클래스와 구성 읽기 코드를 .NET Core에서도 그대로 사용할 수 있도록 한다.
먼저 커스텀 구성 데이터 클래스인 ServiceItem을 정의한다:
using System;
using System.Collections.Generic;
namespace LibraryCore
{
public class ServiceItem
{
public string ServiceName { get; set; }
public string AssemblyPath { get; set; }
public int Priority { get; set; }
}
}
그리고 XML 섹션을 파싱하는 핸들러 클래스 ServiceSectionHandler를 구현한다. 이 클래스는 System.Configuration.IConfigurationSectionHandler 인터페이스를 상속받는다:
using System;
using System.Collections.Generic;
using System.Xml;
namespace LibraryCore
{
public class ServiceSectionHandler : System.Configuration.IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section)
{
var items = new List<ServiceItem>();
foreach (XmlNode childNode in section.ChildNodes)
{
var item = new ServiceItem();
if (childNode.Attributes["name"] != null)
{
item.ServiceName = childNode.Attributes["name"].Value;
if (childNode.Attributes["priority"] != null)
{
item.Priority = Convert.ToInt32(childNode.Attributes["priority"].Value);
}
if (childNode.Attributes["assembly"] != null)
{
item.AssemblyPath = childNode.Attributes["assembly"].Value;
}
items.Add(item);
}
}
return items;
}
}
}
구성 관리를 위한 ServiceConfigManager 클래스를 작성한다:
public static class ServiceConfigManager
{
public static List<ServiceItem> GetServiceConfigurations()
{
var sectionData = System.Configuration.ConfigurationManager.GetSection("CustomServiceConfigs");
if (sectionData != null)
{
return sectionData as List<ServiceItem>;
}
return null;
}
}
위 코드는 .NET Standard 2.0 라이브러리 프로젝트에서 정상적으로 컴파일된다:
1>------ Build started: Project: LibraryCore, Configuration: Debug Any CPU ------
1>LibraryCore -> C:\Projects\NETCoreMigration\LibraryCore\bin\Debug\netstandard2.0\LibraryCore.dll
========== Build: 1 succeeded, 0 failed, 0 skipped ==========
3. 유닛 테스트 수정
MSTest 테스트 프로젝트를 추가하고 테스트 메서드를 작성한다:
[TestClass]
public class ConfigurationTests
{
[TestMethod]
public void ServiceConfigurationLoadTest()
{
var services = LibraryCore.ServiceConfigManager.GetServiceConfigurations();
Assert.IsNotNull(services);
}
}
테스트를 실행했을 때, 예상과 달리 services가 null로 반환되는 문제가 발생했다. 하지만 일반 콘솔 애플리케이션에서는 동일한 구성 파일이 정상적으로 로드된다.
두 프로젝트의 차이점을 비교해보니, 컴파일 후 생성되는 구성 파일 이름이 서로 다르게 동작하고 있었다. 원인을 파악하기 위해 검색을 진행한 결과, 다음과 같은 사실을 발견했다:
MSTest is running as testhost.dll, which means that ConfigurationManager is reading settings from testhost.dll.config when executing under .NET Core.
핵심 문제는 MSTest가 testhost.dll로 실행되어.ConfigurationManager가 testhost.dll.config 파일에서 설정을 읽어온다는 점이다. 따라서 app.config가 아닌 testhost.dll.config를 찾게 되는 것이다.
이 문제는 두 가지 방법으로 해결할 수 있다:
방법 1: 구성 파일 이름 변경
유닛 테스트 프로젝트의 app.config 파일을 testhost.dll.config로 renamed한다.
방법 2: 빌드 후 이벤트 사용
프로젝트 파일(.csproj)을 수정하여 빌드 후 이벤트를 통해 testhost.dll.config를 자동 생성하도록 한다:
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<Target Name="CopyConfigFile" AfterTargets="Build">
<Copy SourceFiles="$(OutDir)app.config" DestinationFiles="$(OutDir)testhost.dll.config" />
</Target>
두 방법 모두 테스트가 정상적으로 통과하는 것을 확인했다. .NET Framework에서 .NET Core로 마이그레이션 시 구성 파일 로딩과 관련된 이러한 차이점을 반드시 고려해야 한다.