연습: 텍스트 템플릿을 사용하여 코드 생성
코드 생성을 통해 강력하게 형식화되었지만 소스 모델이 변경될 때 쉽게 변경될 수 있는 프로그램 코드를 생성할 수 있습니다. 융통성이 더 많은 구성 파일을 받아들이지만 읽고 변경하기가 쉽지 않고 성능이 좋지 않은 코드를 생성하는 완전한 제네릭 프로그램을 작성하는 다른 방법과 이 방법을 비교해 보십시오. 이 연습에서는 코드 생성의 이점을 보여 줍니다.
XML을 읽을 수 있도록 형식화된 코드
System.Xml 네임스페이스는 XML 문서를 로드한 다음 메모리에서 자유롭게 탐색하기 위한 종합적인 도구를 제공합니다. 하지만 모든 노드의 형식이 동일하게 XmlNode이므로 잘못된 자식 노드 형식이나 잘못된 특성이 발생하는 등의 프로그래밍 실수를 하기가 매우 쉽습니다.
이 예제 프로젝트에서 템플릿은 샘플 XML 파일을 읽고 각 노드 형식에 해당하는 클래스를 생성합니다. 직접 작성된 코드에서 이러한 클래스를 사용하여 XML 파일을 탐색할 수 있습니다. 또한 동일한 노드 형식을 사용하는 다른 모든 파일에 대해 응용 프로그램을 실행할 수 있습니다. 샘플 XML 파일의 목적은 응용 프로그램에서 처리하도록 할 모든 노드 형식의 예를 제공하는 것입니다.
참고
Visual Studio에 포함된 xsd.exe 응용 프로그램은 XML 파일에서 강력하게 형식화된 클래스를 생성할 수 있습니다. 여기에 나와 있는 템플릿은 예제로 제공됩니다.
샘플 파일은 다음과 같습니다.
<?xml version="1.0" encoding="utf-8" ?>
<catalog>
<artist id ="Mike%20Nash" name="Mike Nash Quartet">
<song id ="MikeNashJazzBeforeTeatime">Jazz Before Teatime</song>
<song id ="MikeNashJazzAfterBreakfast">Jazz After Breakfast</song>
</artist>
<artist id ="Euan%20Garden" name="Euan Garden">
<song id ="GardenScottishCountry">Scottish Country Garden</song>
</artist>
</catalog>
이 연습에서 생성하는 프로젝트에서 다음과 같은 코드를 작성할 수 있으며, 사용자가 입력할 때 IntelliSense에서 올바른 특성과 자식 이름을 알려주는 메시지를 표시합니다.
Catalog catalog = new Catalog(xmlDocument);
foreach (Artist artist in catalog.Artist)
{
Console.WriteLine(artist.name);
foreach (Song song in artist.Song)
{
Console.WriteLine(" " + song.Text);
}
}
템플릿을 사용하지 않고 작성할 수 있는 형식화되지 않은 코드와 이 코드를 비교해 보십시오.
XmlNode catalog = xmlDocument.SelectSingleNode("catalog");
foreach (XmlNode artist in catalog.SelectNodes("artist"))
{
Console.WriteLine(artist.Attributes["name"].Value);
foreach (XmlNode song in artist.SelectNodes("song"))
{
Console.WriteLine(" " + song.InnerText);
}
}
강력하게 형식화된 버전에서 XML 스키마를 변경하면 클래스가 변경됩니다. 컴파일러는 변경해야 하는 응용 프로그램 코드의 부분을 강조 표시합니다. 제네릭 XML 코드를 사용하는 형식화되지 않은 버전에서는 이러한 지원이 없습니다.
이 프로젝트에서는 단일 템플릿 파일을 사용하여 형식화된 버전을 가능하게 만드는 클래스를 생성합니다.
프로젝트 설정
C# 프로젝트 만들기 또는 열기
이 방법은 모든 코드 프로젝트에 적용할 수 있습니다. 이 연습에서는 C# 프로젝트를 사용하며 테스트를 위해 콘솔 응용 프로그램을 사용합니다.
프로젝트를 만들려면
파일 메뉴에서 새로 만들기를 클릭한 다음 프로젝트를 클릭합니다.
Visual C# 노드를 클릭한 다음 템플릿 창에서 콘솔 응용 프로그램을 클릭합니다.
프로젝트에 프로토타입 XML 파일 추가
이 파일의 목적은 응용 프로그램에서 읽을 수 있도록 할 XML 노드 형식의 샘플을 제공하는 것입니다. 이 파일을 응용 프로그램 테스트에 사용할 수도 있습니다. 템플릿은 이 파일의 각 노드 형식에 대한 C# 클래스를 생성합니다.
이 파일은 템플릿에서 읽을 수 있도록 프로젝트의 일부여야 하지만 컴파일된 응용 프로그램으로 빌드되지 않습니다.
XML 파일을 추가하려면
솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가를 클릭한 다음 새 항목을 클릭합니다.
새 항목 추가 대화 상자의 템플릿 창에서 XML 파일을 선택합니다.
샘플 콘텐츠를 파일에 추가합니다.
이 연습에서 사용하기 위해 파일의 이름을 exampleXml.xml로 지정합니다. 파일의 콘텐츠를 이전 단원에 나와 있는 XML로 설정합니다.
..
테스트 코드 파일 추가
C# 파일을 프로젝트에 추가하고 이 파일 안에 작성할 코드의 샘플을 작성합니다. 예를 들면 다음과 같습니다.
using System;
namespace MyProject
{
class CodeGeneratorTest
{
public void TestMethod()
{
Catalog catalog = new Catalog(@"..\..\exampleXml.xml");
foreach (Artist artist in catalog.Artist)
{
Console.WriteLine(artist.name);
foreach (Song song in artist.Song)
{
Console.WriteLine(" " + song.Text);
} } } } }
이 단계에서 이 코드는 컴파일되지 않습니다. 템플릿을 작성하면서 이 코드가 컴파일되도록 하는 클래스를 생성합니다.
더욱 종합적인 테스트에서는 예제 XML 파일의 알려진 콘텐츠에 대해 이 테스트 함수의 출력을 검사할 수 있습니다. 그러나 이 연습에서는 테스트 메서드가 컴파일되는 것에 만족합니다.
텍스트 템플릿 파일 추가
텍스트 템플릿 파일을 추가하고 출력 확장명을 ".cs"로 설정합니다.
텍스트 템플릿 파일을 프로젝트에 추가하려면
솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가를 클릭한 다음 새 항목을 클릭합니다.
새 항목 추가 대화 상자의 템플릿 창에서 텍스트 템플릿을 선택합니다.
참고
전처리된 텍스트 템플릿이 아니라 텍스트 템플릿을 추가해야 합니다.
파일의 템플릿 지시문에서 hostspecific 특성을 true로 변경합니다.
이렇게 변경하면 템플릿 코드에서 Visual Studio 서비스에 액세스할 수 있습니다.
출력 지시문에서 템플릿이 C# 파일을 생성하도록 확장명 특성을 ".cs"로 변경합니다. Visual Basic 프로젝트에서는 ".vb"로 변경합니다.
파일을 저장합니다. 이 단계에서 텍스트 템플릿 파일에는 다음 코드가 포함됩니다.
<#@ template debug="false" hostspecific="true" language="C#" #> <#@ output extension=".cs" #>
.
.cs 파일이 솔루션 탐색기에서 템플릿 파일의 보조 파일로 나타납니다. 템플릿 파일의 이름 옆에 있는 [+]를 클릭하여 이 파일을 볼 수 있습니다. 이 파일은 템플릿 파일을 저장하거나 템플릿 파일에서 포커스를 이동할 때마다 템플릿 파일에서 생성됩니다. 생성된 파일은 프로젝트의 일부로 컴파일됩니다.
템플릿 파일을 개발하는 동안 편리하게 서로 나란히 볼 수 있도록 템플릿 파일과 생성된 파일의 창을 정렬합니다. 이렇게 하면 템플릿의 출력을 즉시 확인할 수 있습니다. 템플릿이 잘못된 C# 코드를 생성할 때 오류 메시지 창에 오류가 나타나는 것도 확인할 수 있습니다.
생성된 파일에서 직접 편집한 내용은 템플릿 파일을 저장할 때마다 모두 손실되므로 생성된 파일을 편집하지 않아야 하며 간단한 시험을 위해서만 편집해야 합니다. IntelliSense가 작동 중인 생성된 파일에서 작은 코드 조각을 시험해 본 다음 템플릿 파일에 복사하는 것이 유용한 경우가 있습니다.
텍스트 템플릿 개발
Agile 개발에 대한 최선의 권고에 따라 테스트 코드가 컴파일되고 올바르게 실행될 때까지 작은 단계로 나누어 템플릿을 개발하면서 각 단계에서 일부 오류를 해결할 것입니다.
생성할 코드의 프로토타입 생성
테스트 코드를 실행하려면 파일의 각 노드에 대한 클래스가 필요합니다. 따라서 일부 컴파일 오류는 다음 코드를 템플릿에 추가한 다음 템플릿을 저장하면 사라집니다.
class Catalog {}
class Artist {}
class Song {}
이 코드는 필요한 것을 확인하는 데 도움이 되지만 이러한 선언은 샘플 XML 파일의 노드 형식에서 생성되어야 합니다. 위의 시험용 코드를 템플릿에서 삭제하십시오.
모델 XML 파일에서 응용 프로그램 코드 생성
XML 파일을 읽고 클래스 선언을 생성하려면 템플릿 콘텐츠를 다음 템플릿 코드로 바꿉니다.
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#
XmlDocument doc = new XmlDocument();
// Replace this file path with yours:
doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
foreach (XmlNode node in doc.SelectNodes("//*"))
{
#>
public partial class <#= node.Name #> {}
<#
}
#>
파일 경로를 프로젝트의 올바른 경로로 바꿉니다.
코드 블록 구분 기호 <#...#>에 유의하십시오. 이러한 구분 기호는 텍스트를 생성하는 프로그램 코드의 조각을 묶습니다. 식 블록 구분 기호 <#=...#>은 문자열로 계산될 수 있는 식을 묶습니다.
응용 프로그램의 소스 코드를 생성하는 템플릿을 작성하는 경우 두 가지 프로그램 텍스트를 처리하게 됩니다. 코드 블록 구분 기호 내의 프로그램은 템플릿을 저장하거나 다른 창으로 포커스를 이동할 때마다 실행됩니다. 프로그램이 생성하는, 구분 기호 외부에 나타나는 텍스트는 생성된 파일에 복사되고 응용 프로그램 코드의 일부가 됩니다.
<#@assembly#> 지시문은 참조처럼 작동하여 템플릿 코드에서 어셈블리를 사용할 수 있게 합니다. 템플릿에 의해 표시되는 어셈블리 목록은 응용 프로그램 프로젝트의 참조 목록과 별개입니다.
<#@import#> 지시문은 using 문처럼 동작하여 가져온 네임스페이스에 있는 클래스의 약식 이름을 사용할 수 있도록 합니다.
이 템플릿이 코드를 생성하지만 예제 XML 파일의 모든 노드에 대한 클래스 선언을 생성하므로 <song> 노드의 인스턴스가 여러 개인 경우 song 클래스의 선언이 여러 개 나타납니다.
모델 파일을 읽은 다음 코드 생성
많은 텍스트 템플릿은 템플릿의 첫 번째 부분에서 소스 파일을 읽고 두 번째 부분에서 템플릿을 생성하는 패턴을 따릅니다. 예제 파일을 모두 읽어 예제 파일에 포함된 노드 형식을 요약한 다음 클래스 선언을 생성해야 합니다. Dictionary<>:를 사용할 수 있도록 <#@import#>가 하나 더 필요합니다.
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#
// Read the model file
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
Dictionary <string, string> nodeTypes =
new Dictionary<string, string>();
foreach (XmlNode node in doc.SelectNodes("//*"))
{
nodeTypes[node.Name] = "";
}
// Generate the code
foreach (string nodeName in nodeTypes.Keys)
{
#>
public partial class <#= nodeName #> {}
<#
}
#>
보조 메서드 추가
클래스 기능 컨트롤 블록은 보조 메서드를 정의할 수 있는 블록입니다. 이 블록은 <#+...#>으로 구분되며 파일의 마지막 블록으로 나타나야 합니다.
클래스 이름이 대문자로 시작하도록 하려면 템플릿의 마지막 부분을 다음 템플릿 코드로 바꿉니다.
// Generate the code
foreach (string nodeName in nodeTypes.Keys)
{
#>
public partial class <#= UpperInitial(nodeName) #> {}
<#
}
#>
<#+
private string UpperInitial(string name)
{ return name[0].ToString().ToUpperInvariant() + name.Substring(1); }
#>
이 단계에서 생성된 .cs 파일에는 다음 선언이 포함되어 있습니다.
public partial class Catalog {}
public partial class Artist {}
public partial class Song {}
동일한 방법을 사용하여 자식 노드, 특성 및 내부 텍스트의 속성과 같은 보다 세부적인 항목을 추가할 수 있습니다.
Visual Studio API 액세스
<#@template#> 지시문의 hostspecific 특성을 설정하면 템플릿에서 Visual Studio API에 액세스할 수 있습니다. 템플릿에서 이 API를 사용하여 프로젝트 파일의 위치를 얻을 수 있으므로 템플릿 코드에서 절대 파일 경로를 사용하지 않을 수 있습니다.
<#@ template debug="false" hostspecific="true" language="C#" #>
...
<#@ assembly name="EnvDTE" #>
...
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
.GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
텍스트 템플릿 완성
다음 템플릿 콘텐츠에서는 테스트 코드가 컴파일되고 실행될 수 있도록 하는 코드를 생성합니다.
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace MyProject
{
<#
// Map node name --> child name --> child node type
Dictionary<string, Dictionary<string, XmlNodeType>> nodeTypes = new Dictionary<string, Dictionary<string, XmlNodeType>>();
// The Visual Studio host, to get the local file path.
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
.GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
// Inspect all the nodes in the document.
// The example might contain many nodes of the same type,
// so make a dictionary of node types and their children.
foreach (XmlNode node in doc.SelectNodes("//*"))
{
Dictionary<string, XmlNodeType> subs = null;
if (!nodeTypes.TryGetValue(node.Name, out subs))
{
subs = new Dictionary<string, XmlNodeType>();
nodeTypes.Add(node.Name, subs);
}
foreach (XmlNode child in node.ChildNodes)
{
subs[child.Name] = child.NodeType;
}
foreach (XmlNode child in node.Attributes)
{
subs[child.Name] = child.NodeType;
}
}
// Generate a class for each node type.
foreach (string className in nodeTypes.Keys)
{
// Capitalize the first character of the name.
#>
partial class <#= UpperInitial(className) #>
{
private XmlNode thisNode;
public <#= UpperInitial(className) #>(XmlNode node)
{ thisNode = node; }
<#
// Generate a property for each child.
foreach (string childName in nodeTypes[className].Keys)
{
// Allow for different types of child.
switch (nodeTypes[className][childName])
{
// Child nodes:
case XmlNodeType.Element:
#>
public IEnumerable<<#=UpperInitial(childName)#>><#=UpperInitial(childName) #>
{
get
{
foreach (XmlNode node in
thisNode.SelectNodes("<#=childName#>"))
yield return new <#=UpperInitial(childName)#>(node);
} }
<#
break;
// Child attributes:
case XmlNodeType.Attribute:
#>
public string <#=childName #>
{ get { return thisNode.Attributes["<#=childName#>"].Value; } }
<#
break;
// Plain text:
case XmlNodeType.Text:
#>
public string Text { get { return thisNode.InnerText; } }
<#
break;
} // switch
} // foreach class child
// End of the generated class:
#>
}
<#
} // foreach class
// Add a constructor for the root class
// that accepts an XML filename.
string rootClassName = doc.SelectSingleNode("*").Name;
#>
partial class <#= UpperInitial(rootClassName) #>
{
public <#= UpperInitial(rootClassName) #>(string fileName)
{
XmlDocument doc = new XmlDocument();
doc.Load(fileName);
thisNode = doc.SelectSingleNode("<#=rootClassName#>");
}
}
}
<#+
private string UpperInitial(string name)
{
return name[0].ToString().ToUpperInvariant() + name.Substring(1);
}
#>
테스트 프로그램 실행
콘솔 응용 프로그램의 주 부분에서 다음 코드는 테스트 메서드를 실행합니다. F5 키를 눌러 디버그 모드에서 프로그램을 실행합니다.
using System;
namespace MyProject
{ class Program
{ static void Main(string[] args)
{ new CodeGeneratorTest().TestMethod();
// Allow user to see the output:
Console.ReadLine();
} } }
응용 프로그램 작성 및 업데이트
이제 제네릭 XML 코드를 사용하는 대신 생성된 클래스를 사용하여 강력하게 형식화된 스타일로 응용 프로그램을 작성할 수 있습니다.
XML 스키마가 변경되면 새 클래스를 쉽게 생성할 수 있습니다. 컴파일러는 응용 프로그램 코드를 업데이트해야 하는 위치를 개발자에게 알려줍니다.
예제 XML 파일이 변경될 때 클래스를 다시 생성하려면 솔루션 탐색기 도구 모음에서 모든 템플릿 변환을 클릭합니다.
결론
이 연습에서는 코드 생성의 몇 가지 방법과 이점을 보여 줍니다.
코드 생성은 모델에서 응용 프로그램의 소스 코드 일부를 만드는 것을 의미합니다. 모델은 응용 프로그램 도메인에 적합한 형태의 정보를 포함하며 응용 프로그램의 수명 주기 동안 변경될 수 있습니다.
강력한 형식화는 코드 생성의 한 가지 이점입니다. 모델이 사용자에게 보다 적합한 형태로 정보를 나타내는 반면에 생성된 코드는 응용 프로그램의 다른 부분에서 형식 집합을 사용하여 정보를 처리할 수 있도록 합니다.
IntelliSense와 컴파일러는 새 코드를 작성할 때와 스키마가 업데이트될 때 모델의 스키마를 따르는 코드를 만드는 데 도움이 됩니다.
복잡하지 않은 단일 템플릿 파일을 프로젝트에 추가하면 다음과 같은 이점을 얻을 수 있습니다.
텍스트 템플릿을 빠르고 증분적으로 개발하고 테스트할 수 있습니다.
이 연습에서 프로그램 코드는 응용 프로그램에서 처리할 XML 파일의 대표적 예인 모델의 인스턴스에서 실제로 생성됩니다. 더욱 공식적인 방법에서 XML 스키마는 .xsd 파일이나 도메인별 언어 정의의 형태로 된, 템플릿에 대한 입력입니다. 이 방법을 사용하면 템플릿에서 관계의 다중성과 같은 특성을 더욱 쉽게 확인할 수 있습니다.
텍스트 템플릿 문제 해결
오류 목록에 템플릿 변환 또는 컴파일 오류가 표시되거나 출력 파일이 올바르게 생성되지 않은 경우 TextTransform 유틸리티 사용하여 파일 생성에 설명된 방법으로 텍스트 템플릿 문제를 해결할 수 있습니다.