[프로그래밍 노트] 메타데이터 중심 C# 프로그래밍

  • Post author:
  • Post category:칼럼
  • Post comments:0 Comments
  • Post last modified:December 12, 2010

변경 내역

  1. 2006.07.27 작성.

이 글은 월간 마이크로소프트웨어(일명 마소) 2006년 4월호 ‘프로그래밍 노트’ 칼럼에 기고한 글입니다. 편집 상의 실수로 섹션 이름이 ‘프로그래밍 노트’가 아닌 ‘이달의 테크 기법’으로 바뀌어서 출판됐습니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

메타데이터는 단순히 정보를 저장하는 것 이상이 실용성을 가진다. 메타데이터에 세부 내용을 부여하면 보다 유연한 소프트웨어를 개발할 수 있다. 여기에서는 C#을 이용해서 새의 노랫소리를 들려주는 예제 플레이어를 만드는 과정을 통해, 어떻게 하면 유연한 응용프로그램을 개발할 수 있는지에 대해 알아본다.

최재훈 | .NET과 MSSQL을 주로 다루는 개발자다. 이 분야에서 인정 받는 프로페셔널이 되기 위해 꾸준히 노력하고 있다. 객체지향과 데이터베이스에 관심을 갖고 있지만, 기술보다는 사람에 대한 믿음이 중요하다고 생각한다. 최근에는 문제해결법에 관심을 기울이고 있다.

.NET Framework에서 메타데이터라고 하면, 어셈블리의 클래스, 개체, 메서드 등에 대한 정보를 말한다. 그러나 여기서는 메타데이터의 본래 의미에 초점을 맞춘다. 메타데이터는 데이터를 기술한 데이터이다. 윈도우 계열 소프트웨어에서 많이 쓰이는 .ini 파일이나 블로그의 RSS 등에 쓰이는 XML이 대표적인 메타데이터 포맷이다. RSS의 경우를 생각해 보자면 블로그의 글은 데이터이고, RSS를 구성하는 XML 포맷은 데이터를 기술한 데이터가 된다.

일반적으로 어플리케이션 관점에서 메타데이터는 환경설정파일을 의미한다. 데이터베이스 연결문자, 서버응용프로그램의 포트 번호와 같이 컴파일타임이 아닌 런타임에 접근해야 하는 정보를 주로 설정파일에 저장한다. 만약 데이터베이스의 네트워크 주소가 127.0.0.1에서 1.0.0.127로 바뀌었다고 해서 소스코드를 다시 빌드해야 한다면 무척 괴로울 것이다.

그러나 메타데이터의 중요성은 정보를 저장하는 것 이상이다. 『실용주의 프로그래머』의 한 대목을 빌리자면, 코드에는 추상화를, 메타데이터에는 세부 내용을 부여함으로써 보다 유연한 소프트웨어를 개발할 수 있게 된다. 이어지는 글에서 XML과 리플렉션이라는 C#의 강력한 기능을 사용하여 어떻게 유연한 응용프로그램을 개발할 수 있는지 알아보도록 하겠다.

<리스트 1> Visual Studio Express Edition

이 글의 모든 예제는 Visual Studio C# 2005 Express Edition으로 작성됐다. 마이크로소프트사가 무료로 제공하고 있다. http://msdn.microsoft.com/vstudio/express/에서 다운로드 받아 설치하고 간단한 등록과정만 마치면 된다.

새의 노래를 들려주는 플레이어를 구현해 보자.

프로그래밍을 하다 보면, 어디선가 막히기 마련이다. 왜 이럴까?라고 고민하다 보면 어느새 날이 어두워지거나 밝아진다. 이럴 때는 끝없이 고민하기 보다는 잠시 바깥 공기를 쐬거나 눈 좀 붙이는 게 최고다. 그런 의미에서 새의 노래 소리를 들려주는 소프트웨어를 만들어보자. 기분전환을 하는데 도움이 될 것이다.

우리가 ‘새’라는 객체로부터 기대하는 바가 무엇인지부터 생각해보자. ‘새’도 살아야 할 테니 ‘먹기’도 할거고, ‘날기’도 할 것이다. 하지만 우리는 새의 노래를 듣고 싶을 뿐이니, <리스트 2>에서와 같이 ‘노래하기’라는 행동만 정의하면 된다. 오리 울음소리가 두통을 더 심하게 만들 것 같지만, 다음 단계로 넘어가자. 노래 소리가 있어도 그것을 들려줄 플레이어가 없으면 아무런 소용이 없다. BirdPlayer 버전 1은 단순히 오리와 비둘기의 노래 소리를 번갈아 들려준다. (지면상 전체 소스코드 샘플은 별도로 제공한다.)

<리스트 2> 노래하는 새와 BirdPlayer 버전 1

public interface IBird
{
	void Sing();
}

public class Dove : IBird
{
	public void Sing()
	{
		Console.WriteLine("비둘기울음소리. 구구?");
	}
}

// 중략……

class BirdPlayer
{
	public void PlayNext()
	{
		// 중략……
	}
	// 중략……
}

이로써 플레이어로써의 기본적인 기능을 갖추게 됐다. 그러나 문제가 있다. 비둘기의 울음소리를 듣고 있다가 배달 온 피자 값을 지불하러 가야 돼서 플레이어를 껐다. 한 조각을 입에 물고 플레이어를 다시 켜니 ‘꽥꽥’ 기분 나쁜 오리소리가 들린다. 플레이어를 껐다 켰을 때, 이전에 듣고 있던 새소리를 들려주면 좋겠다. 그러기 위해 상태 정보를 저장하기 위한 응용프로그램 구성 파일(App.config)부터 만들기로 한다.

새소리 플레이어를 위한 환경설정파일을 만들어보자.

C#은 환경설정파일을 직접적으로 지원한다. VS Express는 두 종류의 설정파일 템플릿을 제공하는데, Application Configuration File과 Settings File이다. 별도의 템플릿이지만, 어느 것을 선택해도 App.config 파일을 생성한다. <리스트 3>는 BirdPlayer를 위한 구성파일이고, Bird라는 키 값에 듣고 있던 ‘새’ 객체의 정보가 할당되어 있다. 객체 정보는 ‘새’ 객체의 타입과 객체를 담고 있는 어셈블리 파일의 이름의 조합인데, 동적으로 객체를 생성하기 위해 필요한 최소한의 정보이다.

<리스트 3> BirdPlayer를 위한 구성파일

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<appSettings>
		<add key="Bird" value="MetaDataSample.Dove, MetaDataSample.exe"></add>
	</appSettings>
</configuration>
	

이러한 구성파일에 접근하기 위해서 많은 작업을 할 필요는 없다. 만약 구성 값에 접근하기 위해 매번 XmlReader를 사용해야 한다면 귀찮기 짝이 없을 것이다. .NET Framework는 기본적인 형태의 구성파일 포맷을 지원한다. .Net Framework 1.x에서는 System.Configuration.ConfigurationSettings.AppSettings을 통해 <appSettings> 섹션에 접근할 수 있었다. 그러나 2.0에서는 System.Configuration.ConfigurationManager.AppSettings으로 대체됐다. 만약 2.0에서 ConfigurationSettings.AppSettings을 사용한다면 소스코드를 빌드할 때, " ‘System.Configuration.ConfigurationSettings.AppSettings’ is obsolete: "와 같은 경고 메시지를 보게 될 것이다. 더 이상 사용하지 않는 기능이라는 뜻이다. 같은 네임스페이스 System.Configuration을 공유하지만, ConfigurationSettings는 공유어셈블리 System에 포함되어 있는 반면에 ConfigurationManager는 공유어셈블리 System.Configuration에 포함되어 있다. VS Express 프로젝트는 기본적으로 공유어셈블리 System.Configuration에 대한 참조를 갖고 있지 않으므로 [Add Reference] 메뉴에서 추가해줘야 한다.

<리스트 3>는 <리스트 1>의 구성 값으로 ‘새’ 객체를 생성(Load)하거나, 지금 재생 중인 ‘새’의 객체 정보를 저장(Save)한다. 전에는 ‘새’의 기본기능은 ‘노래하기’ 밖에 없었기 때문에 인터페이스로 구현했다. 그러나 저장과 생성이라는 구체적인 기능이 필요해졌으므로 추상객체(abstract)로 수정한다. 보다시피 특정 값을 찾기 위해 복잡한 코드를 작성할 필요가 없다. 물론 보다 복잡한 형태의 메타데이터 포맷이 필요한 경우도 있다. 그때는 XmlReader나 XmlAttributes를 사용하면 된다.

<리스트 4> 생성과 저장 기능을 갖춘 새로운 ‘새’ 객체

public abstract class AbstractBird
{
	private const string keyName = "Bird";

	public abstract void Sing();

	public static AbstractBird Load()
	{
		// "Bird"라는 키값을 가진 구성값을 읽는다.
		string birdType = ConfigurationManager.AppSettings[keyName];

		// 구성값에 명시된IBird 인스턴스를 동적으로 생성한다.
		string[] parts = birdType.Split(',');
		string typeName = parts[0].Trim();
		string assemblyName = parts[1].Trim();

		Assembly assembly = Assembly.LoadFrom(assemblyName);
		return (AbstractBird)assembly.CreateInstance(typeName);
	}

	public static void Save(AbstractBird bird, string assemblyName)
	{
		// 구성파일을 가져온다.
		Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

		// 구성값을 변경하고 저장한다.
		config.AppSettings.Settings.Remove(keyName);
		config.AppSettings.Settings.Add(keyName, bird.GetType().FullName + ", " + assemblyName);
		config.Save(ConfigurationSaveMode.Modified);

		// 구성파일을 다시 읽는다.
		ConfigurationManager.RefreshSection("appSettings");
	}
}
	

<리스트 4>의 대부분은 설정파일을 읽고, 수정하기 위한 코드들이다. 메써드 AbstractBird.Load()의 마지막 두 줄만이 리플렉션 관련 코드일 뿐이다. 지역변수 assemblyName의 값은 MetaDataSample.exe가 되고, typeName의 값은 MetaDataSample.Dove가 된다. 즉, 두 줄의 Assembly 관련 코드는 MetaDataSample.exe에 구현되어 있는 Dove 객체를 동적으로 생성하게 된다.

예제와 같이 AbstractBird의 구현이 오리(Duck)와 비둘기(Dove) 밖에 없다면, 리플렉션을 이용한 동적 객체 생성이 꼭 필요한 것은 아니다. 키 Bird에 타입정보 대신에 Dove나 Duck 값만 할당될 수 있다고 생각해 보자. 이때 메써드 AbstractBird.Load()의 구현은 다음과 같을 것이다.

if (birdType == "Duck")
{
	return Duck();
}
else
{
	return Dove();
}
	

<리스트 4>의 구현보다 간단하다. 하지만 AbstractBird의 구현이 여러 개라면 어떨까? 공작새, 독수리 등 서울대공원에서 볼 수 있는 모든 새의 울음소리를 구현해놨다면, if/else if/else 문을 천번은 반복해야 할 것이다. 어지간히 귀찮은 일이 아닐 수 없다. BirdPlayer가 밤샘 작업에 지친 프로그래머들의 인기를 독차지해서 ‘밀림의 새’ 확장판이 나온다면 또 어떨까? MetaDataSample.exe의 소스코드를 수정한 다음, 재배포를 할 수도 있다. 하지만 리플렉션을 사용한다면, Expansion.dll에 앵무새(Mockingbird) 등의 노래 소리를 녹음해 놓아도 된다. 어셈블리와 객체 타입만 알면 동적으로 Mockingbird 객체를 생성할 수 있기 때문이다. 이런 식으로 리플렉션은 응용프로그램에 유연성을 부여한다.

이제 완성한 어플리케이션을 실행해 볼 때다. <리스트 3>에서와 같이 구성파일에 비둘기(Dove) 객체의 정보가 저장되어 있고 매번 비둘기와 오리의 노래를 한번씩 듣는다면, 다음과 같은 결과가 나온다.

<리스트 5> BirdPlayer 실행해보기

C:\BirdPlayer>MetaDataSample.exe
비둘기 울음 소리. 구구?
오리의 울음소리. 꽥꽥.

C:\BirdPlayer>MetaDataSample.exe
오리의 울음소리. 꽥꽥.
비둘기 울음 소리. 구구?

BirdPlayer를 단위 테스트하고 배포해보자.

VS Express는 빌드 시에 app.config 파일을 타겟 디렉토리($(TargetDir))로 자동으로 복사해 넣는다. 이때 app.config는 자동으로 $(TargetFileName).config로 바뀌게 된다. 어셈블리 이름이 MyAssembly일 때, 응용 프로그램이라면 MyAssembly.exe.config가 되고 클래스 라이브러리라면 MyAssembly.dll.config가 된다. 만약 여러분이 사용하는 IDE가 이런 작업을 제대로 해주지 않는다면, ‘빌드 전 이벤트’를 작성하면 된다. NAnt와 같은 빌드 스크립트를 사용한다면, <copy> 태스크로 ‘빌드 전 이벤트’를 대신할 수 있다.

  • VS.NET 2003의 경우: copy "$(ProjectDir)App.config" "$(TargetPath).config"

  • NAnt의 경우: <copy file="app.config" tofile="${build.dir}${target.filename}.config" />

반드시 App.config의 이름을 변경해야 하는 것은 아니다. App.config든MyAssembly.dll.config든 간에 ConfigurationManager.AppSettings으로 접근할 수 있다. 그러나 다른 응용프로그램이 해당 어셈블리를 참조할 때는 문제가 발생할 수 있다. NUnit으로 MyAssembly.dll의 구성요소들을 단위 테스트할 때가 그런 경우 중 하나다. <리스트 3>은 키 값 "Bird"에 해당하는 구성 값을 찾지 못하면, 단위 테스트가 실패하도록 짜여 있다. 만약 MyAssembly.exe.config 대신 App.config로 파일 이름이 되어 있으면 테스트는 실패한다. 왜냐하면 이때 App.config는 MyAssembly.exe를 위한 구성파일이 아니라, NUnit에 속하는 것으로 취급되기 때문이다.

<리스트 6> NUnit으로 테스트해 보기

[TestFixture]
public class AppSettingsTest
{
	[Test]
	public void ReadBirdType()
	{
		string birdType = System.Configuration.ConfigurationManager.AppSettings["Bird"];
		NUnit.Framework.Assert.AreNotSame(birdType, null);
	}
}

단위 테스트 결과

마지막으로 이런 의문이 들 수 있다. 타겟 디렉토리에 App.config와 MyAssembly.exe.config이 모두 있다면, MyAssembly.exe는 어느 파일을 참조할까? 예상했겠지만 MyAssembly.exe.config가 App.config보다 우선 시 된다. 어려운 내용은 아니지만, 가끔 문제가 되기 때문에 주의해야 한다. MyAlarm.exe는 6시간마다 사용자의 그날 일정을 통보해 준다. 그러다 보니 가끔 약 먹어야 할 시간을 잊어버리는 문제가 발생했다. 1시간마다 경고음을 울리도록 설정파일을 수정한다. 그러나 약 먹을 시간이 지나도 경고음은 들리지 않고, 오늘도 약을 먹지 못한다. App.config가 보이기에 별다른 생각 없이 편집기를 연 것이 문제였다. 폴더에는 MyAlarm.exe.config도 있었지만, 알파벳 순서로 정렬된 탓에 App.config가 먼저 눈에 들어왔던 것이다.

결론

간단한 새소리 플레이어를 구현해봤다. 기능이 너무 단순해서, XML과 리플렉션이 소프트웨어의 어떤 유연함을 부여하는지 전부 보여주지는 못했다. 만약 상용 어플리케이션 급에서 사례를 찾아보고 싶다면, Patterns & practices를 방문해 보자. 이 사이트는 각종 라이브러리를 제공하는데, 매우 유연하게 디자인되어 있다. 예를 들어, 설정 파일을 수정하는 것만으로 콘솔에만 출력되던 로그 내역을 이벤트 로그나 파일 로그에 기록할 수 있다. 또한 미리 만들어져 있는 로그 객체만 동적으로 할당해서 쓰는 것이 아니라, 라이브러리를 사용하는 사람이 개발한 로그 객체도 사용할 수 있다.

Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments