Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN Home   MSDN Home

즉석으로 UI 만들기

.NET Framework를 사용하여 런타임에 사용자 지정 컨트롤 생성 및 실행



이 문서 내용:
  • 런타임 코드 생성
  • Reflection.Emit을 사용하여 클래스 생성
  • System.CodeDom을 사용하여 클래스 생성
  • 런타임에 Windows Forms 및 ASP.NET 컨트롤 만들기
이 문서에는 다음 기술이 사용됩니다:
Windows Forms, ASP.NET, .NET Framework, C#


Code download available at:
CodeGeneration.exe (146KB)

icrosoft .NET Framework에서 간과하기 쉬운 기능 중 하나는 런타임에 사용자 지정 코드를 생성, 컴파일 및 실행할 수 있는 기능입니다. 예를 들어, 이 기능은 XML 데이터 serialization을 수행하는 동안이나 정규식을 사용하는 과정에 사용할 수 있습니다. 정규식을 사용할 때는 식 계산 기능이 런타임에 실행됩니다.

이 기사에서는 런타임 코드 생성 기능을 사용할 수 있는 또 다른 분야인 UI 컨트롤 만들기에 대해 설명합니다. 이러한 코드를 한 번 생성해 둔 다음 필요할 때 다시 사용하면 폼이나 페이지가 요청될 때 매번 컨트롤을 생성하는 것보다 훨씬 효율적입니다.

이 기능은 사용자 구성이 가능한 필드가 있는 모든 응용 프로그램에 적용할 수 있습니다. 사용자 구성 가능한 필드의 예로는 최종 사용자가 화면에 표시할 데이터 항목을 선택할 수 있는 필드를 들 수 있습니다. 일반적으로 사용자 지정 폼은 XML을 사용하여 정의합니다. 사용자 지정된 폼은 런타임에 구문 분석되어 페이지가 로드될 때 UI를 동적으로 생성합니다. 그러나 폼이 표시될 때마다(서버 시나리오에서는 각 사용자에 대해) 구문 분석이 빈번하게 수행되기 때문에 응용 프로그램에 대한 불필요한 로드가 발생합니다.

이 기사에서는 런타임 코드 생성 기능을 사용하여 런타임에 컨트롤을 생성, 로드 및 실행하는 방법을 자세한 예제를 통해 설명하겠습니다. 여기서 설명하는 예제는 .NET Framework 1.x 및 .NET Framework 2.0에도 동일하게 적용할 수 있습니다. Framework 2.0에는 Reflection 네임스페이스에 대한 중요한 추가 사항이 있지만 이러한 변경 내용으로 인해 이 기사에서 설명하는 솔루션의 기능을 사용할 수 없는 것은 아닙니다.


코드 생성 기본 사항

대부분의 응용 프로그램에서는 필드를 추가하거나 기존 필드의 순서 및 위치를 변경하는 방식으로 사용자 지정할 수 있는 사용자 인터페이스(UI)를 제공합니다. 이 작업은 보통 다음 두 가지 방법 중 하나를 사용하여 수행합니다.

  • 사용자가 Visual Studio에서 사용자 인터페이스를 편집하여 변경합니다
  • 응용 프로그램이 보통 XML 파일에 저장되어 있는 구성 데이터의 일부 폼을 기준으로 런타임에 컨트롤을 생성합니다.

그러나 이 두 솔루션은 모두 이상적이지 못합니다. 실제로 필요한 것은 사용자가 직접 만든 솔루션의 뛰어난 성능과 런타임에 컨트롤을 생성하는 응용 프로그램의 융통성을 제공하는 위의 두 방법을 조합한 형태이기 때문입니다.

런타임에 컨트롤을 생성하는 데는 Reflection.Emit 및 System.CodeDom의 두 가지 메서드가 지원됩니다. IL(Intermediate Language)에 익숙해야 사용할 수 있는 Reflection.Emit은 명시적으로 지정된 IL 명령을 사용하여 관리되는 어셈블리를 생성합니다. System.CodeDom은 코드의 개체 모델을 사용하여 소스 코드를 내보냅니다. 그러면 소스 코드는 IL로 컴파일되어 실행됩니다. 이 기사에서는 이 두 메서드를 모두 다룹니다.

사실 CodeDOM 방식의 변형인 다른 메서드도 있습니다. 이 메서드는 개체 모델을 사용하여 소스 코드를 생성하는 대신 사용자가 수동으로 소스 코드를 생성(.cs 또는 .vb 파일을 직접 작성)한 다음 런타임에 해당 소스 코드를 컴파일하는 데 사용됩니다. 이 메서드를 사용해도 되지만 수동 솔루션보다는 앞서 언급한 메커니즘을 사용하는 것이 좋습니다.


Reflection.Emit을 사용하여 클래스 생성

Reflection.Emit 네임스페이스를 사용하면 완전히 일시적인 어셈블리를 생성할 수 있습니다. 즉, 이러한 어셈블리는 디스크에 저장하지 않아도 메모리에서 생성하여 실행할 수 있습니다. 그러나 필요한 경우에는 디스크에 쓰는 옵션도 사용할 수 있습니다. 이에 대해서는 이 기사 뒷부분에서 간략하게 설명합니다.

Reflection.Emit을 사용하여 클래스를 생성하려면 다음 단계를 수행해야 합니다.

  • 동적 어셈블리를 정의합니다.
  • 해당 어셈블리 내에 모듈을 만듭니다.
  • 해당 기본 클래스 및 인터페이스에서 형식을 파생시켜 해당 모듈에 형식을 정의하고, 해당 형식에 메서드를 만들고, 각 메서드에 대해 ILGenerator를 가져오고, 적절한 IL opcode를 내보내고, 형식을 보존합니다.
  • 나중에 사용하기 위해 어셈블리를 저장합니다(옵션).

Reflection.Emit을 사용하여 클래스를 생성하는 작업은 복잡하며, 지정된 소스 코드 작업(예: 메서드 호출)에 대해 보통 IL 코드를 여러 줄 생성해야 합니다. 여기에 대해서는 이 기사 뒷부분에서 다시 설명하겠습니다.


System.CodeDom을 사용하여 클래스 생성

System.CodeDom 네임스페이스는 형식 정의를 위한 언어 독립적인 방식을 제공합니다. 코드의 메모리 내 모델을 생성한 다음 코드 생성기를 사용하여 해당 모델의 소스 코드를 내보냅니다. 적절한 코드 생성기가 있으면 모든 언어에서 코드를 내보낼 수 있습니다. .NET Framework 재배포 가능 패키지에는 Visual Basic, C# 및 JScript용 코드 생성기가 포함되어 있습니다. Visual Studio가 설치되어 있으면 C++ 및 J#용 생성기도 사용할 수 있습니다.

CodeDOM을 사용하여 클래스를 생성하려면

  • 새 CodeCompileUnit을 만듭니다.
  • CodeCompileUnit에 네임스페이스를 추가합니다.
  • import 문을 적절하게 추가합니다.
  • 생성되는 클래스에 대해 CodeTypeDeclaration을 추가합니다.
  • 각 메서드에 CodeExpressions를 추가합니다.
  • 코드 컴파일러를 가져와 CodeCompileUnit을 컴파일합니다.
이 방법을 사용하면 Reflection.Emit을 사용할 때보다 수준 높은 개념을 사용할 수 있다는 이점이 있습니다.

클래스를 정의한 후에는 런타임에 해당 클래스의 인스턴스를 생성해야 합니다. 가장 일반적인 방법은 다음 코드 조각과 같이 Activator 클래스를 사용하여 형식을 로드하는 것입니다.

public Control LoadControl ( string typeName )
{
    return LoadControl(Type.GetType(typeName));
}

public Control LoadControl ( Type controlType )
{
    return Activator.CreateInstance(controlType) as Control;
}

여기서는 Type 클래스의 정적 GetType 멤버를 호출합니다. GetType은 해당 인수로 지정된 형식에 대한 .NET 형식 정보를 반환합니다. 그러면 Activator 클래스를 사용하여 해당 형식의 인스턴스가 생성됩니다. 지정된 형식이 mscorlib 또는 현재 실행 중인 어셈블리가 아닌 다른 어셈블리에 있는 경우에는 형식 이름을 어셈블리 이름으로 정규화해야 합니다. 관리되는 형식 이름을 문자열로 저장하는 경우에는 형식 네임스페이스로 부분 정규화할 수도 있고 형식 네임스페이스 및 형식이 저장된 어셈블리 이름으로 전체 정규화할 수도 있습니다. 다음은 어셈블리 정규화 형식 이름의 예제입니다.

TestControls.TestControl, TestAssembly

이 형식 이름은 TestAssembly라는 어셈블리로 컴파일되는 다음 코드의 형식에 해당합니다.

using System.Web.UI;

namespace TestControls
{
  public class TestControl : Control
  {
    ...
  }
}

컨트롤을 생성할 때는 보통 이 형식 이름을 SQL Server 데이터베이스 같은 영구적 저장소 매체에 저장합니다. 그러면 폼 또는 페이지를 렌더링할 때 Activator.CreateInstance를 사용하여 형식을 로드한 다음 해당 개체를 사용자에게 표시합니다.


생성된 어셈블리 저장

Reflection.Emit 또는 System.CodeDom을 사용하여 만든 어셈블리는 완전한 임시 어셈블리일 수도 있고, 나중에 사용하기 위해 디스크에서 생성할 수도 있습니다. 임시 어셈블리는 만들어지는 응용 프로그램 도메인의 수명이 유지될 동안만 존재합니다. 응용 프로그램 도메인이 언로드되면, 즉 응용 프로그램이 언로드되어 기본 응용 프로그램 도메인이 언로드되면 이러한 어셈블리도 메모리에서 언로드되어 더 이상 존재하지 않게 됩니다. 런타임에 이러한 어셈블리를 생성하려면 비용이 들기 때문에 생성된 어셈블리를 파일 시스템에 보관하고 필요할 때는 먼저 파일 시스템을 찾아보는 것이 좋습니다. 런타임에 파일 시스템에서 어셈블리를 찾을 수 없는 경우 다시 생성하면 됩니다. 물론 이 방법을 선택하는 경우에는 컨트롤의 정의 및 생성된 어셈블리를 동기화된 상태로 유지해야 합니다. 상용 응용 프로그램의 경우에는 생성해야 하는 모든 컨트롤에 대해 실행할 수 있는 관리 도구를 사용자에게 제공하고, 디스크 또는 SQL Server 내에 유지되는 단일 어셈블리에 이러한 컨트롤을 저장하도록 하는 것이 좋습니다.

또한 동적으로 생성되는 어셈블리 수를 가능한 한 최소(1개 권장)로 제한하는 것이 좋습니다. 즉, 컨트롤이 변경될 때마다 전체 컨트롤 집합을 생성해야 합니다. 그러나 이는 보통 자주 수행되지 않는 관리 작업이므로 성능 문제는 없습니다.


생성된 어셈블리 보안

또한 생성된 어셈블리와 관련된 보안 문제를 이해해야 합니다. 생성된 어셈블리는 독특한 방식으로 응용 프로그램에 위협을 줄 수 있기 때문에 이는 매우 중요합니다. 즉, 응용 프로그램이 동적으로 일부 형식을 로드하려는 경우 이론적으로는 이러한 형식이 임의의 코드를 실행할 수 있습니다.

생성된 코드를 보호하는 데 권장되는 방법은 생성된 모든 코드에 대해 제한적인 권한 집합을 할당하는 것입니다. .NET 구성 도구인 mscorcfg.msc 내에서 코드 그룹을 생성하여 이 작업을 수행할 수 있습니다.

특정 멤버 조건 및 권한 집합으로 사용자 지정 코드 그룹을 정의할 수 있습니다. 예를 들어, 동적으로 생성된 모든 코드에 대해 어셈블리를 실행할 수는 있지만 코드가 수행할 수 있는 작업은 엄격하게 제한하는 Execute 등의 매우 제한적인 권한 집합을 할당하려는 경우가 있습니다. 코드 그룹은 멤버 조건을 통해 어셈블리에 적용되며, 결과적으로 이는 그룹에 포함할 내용을 정의하는 데 사용됩니다.

멤버 조건에는 여러 가지 형식이 있습니다. 그 중 이 예제에서 가장 유용하게 사용되는 형식은 디스크의 특정 디렉터리(예: "file://E:/Code Generation/bin/Debug/*"를 URL로 사용)를 기반으로 하여 멤버를 정의하는 데 사용할 수 있는 URL 권한입니다. 도구 내에서 정의된 권한 집합은 지정된 폴더 내에 저장된 모든 생성된 어셈블리에 적용됩니다.

컨트롤을 한 번 생성해 두었다가 런타임에 로드하면 폼의 XML 표현을 사용하고 런타임에 해당 표현을 평가하는 것보다 성능 면에서 이점이 있습니다. XML 표현을 로드하려면 일반적으로 XmlDocument를 인스턴스화 및 로드해야 하는데, 이 작업은 Activator.CreateInstance를 사용하더라도 일반 컨트롤을 인스턴스화하는 작업보다 속도가 느립니다.


Windows Forms 예제

이 섹션에서는 Reflection.Emit 및 System.CodeDom을 사용하여 런타임에 Windows Forms를 생성하는 방법을 보여 주는 예제를 제공합니다. 생성되는 컨트롤은 그림 1과 같이 정적 텍스트 상자로만 구성된 간단한 사용자 인터페이스를 표시합니다.

그림 1 컨트롤 생성 샘플 응용 프로그램
그림 1 컨트롤 생성 샘플 응용 프로그램

생성된 컨트롤은 빨간 선 안에 표시되는 부분입니다. 간단한 예제이기는 하지만 보다 완벽한 구현을 제공하기 위해 확장할 수 있는 필요한 개념은 모두 제공하고 있습니다.

이 기사에서는 그림 2의 코드와 비슷한 소스 코드를 가진 컨트롤을 생성합니다. 이 코드는 내보내는 코드의 양에 관계없이 사용할 수 있는 다음과 같은 여러 가지 개념을 보여 줍니다.

  • 클래스가 기본 클래스에서 파생됩니다.
  • 클래스에 개인 필드가 있습니다.
  • 클래스에 공용 기본 생성자가 있습니다.
  • 클래스에 개인 메서드가 있습니다.
  • 클래스가 속성 사용 방법을 보여 줍니다.
  • 클래스가 메서드 호출 방법을 보여 줍니다.
  • 클래스가 새 개체 인스턴스 생성 방법을 보여 줍니다.
위의 목록에 내보낼 수 있는 모든 기능이 나열되어 있는 것은 아니지만, 이를 기반으로 하여 원하는 작업을 수행할 수 있습니다.


Reflection.Emit을 사용하여 Windows Forms 컨트롤 생성

Reflection.Emit 네임스페이스는 적은 수의 클래스만을 포함하는 매우 낮은 수준의 API이므로 대부분의 까다로운 작업은 사용자가 직접 코드를 작성하여 수행해야 합니다. 코드를 섹션별로 살펴보고 각 섹션을 자세히 설명하겠습니다.

Reflection.Emit을 사용하는 작업은 까다롭습니다. 정확한 코드를 생성할 수 있는 실제적인 방법은 테스트 코드를 일부 작성하고 컴파일한 다음 ildasm 또는 Lutz Roeder의 .NET Reflector 같은 도구를 사용하여 생성된 IL을 검사하는 것뿐입니다. 다음 IL은 이 방법으로 생성한 것입니다. 일부 다른 코드를 리버스 엔지니어링하지 않고 사용자의 고유한 IL을 생성할 수 없다는 의미는 아닙니다. 그러나 IL을 기본 언어로 사용하는 개발자가 많지 않으므로 이 분야에 사용할 수 있는 기술은 거의 없고 기술 간에도 격차가 큽니다. IL에 대한 고급 지식이 필요한 경우에는 Serge Lidin의 저서 Inside Microsoft .NET IL Assembler(Microsoft Press, 2002)에 참조할 만한 좋은 내용이 있습니다. 이에 대한 내용을 설명하기 위해 코드를 어셈블리 내보내기, 형식 정의, 생성자 정의 및 InitializeComponent 메서드 정의의 네 가지 논리 섹션으로 구분했습니다.


어셈블리 내보내기

그림 3의 코드에서는 동적 어셈블리를 생성하고 해당 어셈블리에 모듈을 추가하며 어셈블리의 고유한 이름을 만듭니다. 먼저 어셈블리 이름이 생성됩니다. 이 예제에서는 GUID를 사용하여 어셈블리의 고유한 이름을 지정합니다.

그런 다음 AppDomain 클래스의 정적 DefineDynamicAssembly 메서드를 호출해야 합니다. DefineDynamicAssembly 함수에는 여러 가지 오버로드 버전이 있지만 여기서는 가장 간단한 재정의를 호출합니다. 다음으로는 모듈 이름 및 파일 이름을 정의하여 모듈 작성기 개체를 만듭니다. 마지막으로 다음 섹션에 있는 코드가 실행된 후 AssemblyBuilder.Save 메서드를 사용하여 어셈블리가 만들어집니다.


형식 정의

Reflection.Emit을 사용하여 형식을 만들려면 ModuleBuilder 클래스에서 DefineType 메서드 중 하나를 호출해야 합니다. 클래스, 해당 클래스의 기본 클래스 및 해당 클래스가 명시적으로 구현하는 인터페이스를 정의할 수 있는 여러 가지 메서드가 있습니다. 다음 코드 세그먼트에서는 System.Windows.Forms.UserControl에서 파생된 클래스를 정의하는 방법을 볼 수 있습니다.

Type baseClass = typeof ( System.Windows.Forms.UserControl ) ;
TypeBuilder typeBuilder = moduleBuilder.DefineType( 
    "MyControls.MyControl", 
    TypeAttributes.Public | TypeAttributes.Class | 
    TypeAttributes.BeforeFieldInit, baseClass ) ;

DefineType을 사용하면 생성된 형식의 형식 특성을 지정할 수 있습니다. 이 경우 새 형식은 클래스입니다. 이는 공용으로 사용할 수 있도록 표시되며 런타임에 클래스를 초기화하도록 하지 않고도 정적 멤버를 호출할 수 있습니다.

그런 다음 레이블 컨트롤을 보관할 개인 필드를 만듭니다. 이 필드는 이름, 형식 및 특성이 단일 호출로 지정된다는 점에서 앞서 나왔던 코드와 비슷한 패턴을 따릅니다. 이 필드를 만드는 코드는 다음과 같습니다.

// 레이블 컨트롤을 위한 개인 필드를 만듭니다.
FieldBuilder labelField = typeBuilder.DefineField( 
    "_label", typeof( System.Windows.Forms.Label ) , 
    FieldAttributes.Private ); (참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.)


생성자 정의

형식에 내용을 추가하려면 앞에서 만든 개체인 TypeBuilder 클래스의 Define* 메서드 중 하나를 호출해야 합니다. 생성자, 이벤트, 필드, 메서드 및 기타 구문을 정의하기 위한 이러한 메서드에는 여러 가지가 있습니다.

그림 4에서는 DefineConstructor 메서드를 사용하여 매개 변수가 없는 공용 생성자를 만듭니다. DefineConstructor 메서드에 적절한 메서드 특성을 제공하고 이 인스턴스에서 매개 변수 배열이 비어 있도록 정의합니다. 생성자 코드에서 생성된 IL은 다음과 같습니다.

.method public hidebysig specialname rtspecialname instance void .ctor()
cil managed
{
   .maxstack 3
   L_0000: ldarg.0 
   L_0001: call instance void System.Windows.Forms.UserControl::.ctor()
   L_0006: ldarg.0 
   L_0007: call instance void 
           MyControls.MyControl::InitializeComponent()
   L_000c: ret 
}
생성된 IL과 C# 코드의 해당 IL을 내보낸 명령은 거의 일치합니다.

ConstructorBuilder 개체를 만든 후에는 해당 개체에서 ILGenerator를 가져올 수 있습니다. 이 개체를 사용하여 IL을 어셈블리로 내보낼 수 있습니다.

BeginScope 및 EndScope 메서드는 여기서 반드시 필요하지 않으며, 기본적으로 C#의 { 및 }와 같습니다. 이 범위 내에서 IL opcode를 내보냅니다.

CLR은 스택 기반 아키텍처이므로 첫 번째 opcode(Ldarg_0)는 인수(이 경우 현재 개체의 this 포인터)를 계산 스택으로 전달합니다. 두 번째 명령은 기본 클래스 생성자에 대한 명령입니다. 이 명령으로 인해 this 포인터가 스택 밖으로 이동하고 생성자가 호출되므로 다음 opcode는 this 포인터를 스택으로 로드하여 다음 호출을 준비합니다.

끝에서 두 번째 opcode는 InitializeComponent 메서드 호출이며 마지막 opcode는 생성자에서 반환된 것입니다.


InitializeComponent 메서드 정의

이 메서드는 다음과 같은 작업을 수행하므로 다른 메서드보다 사용 빈도가 높습니다.

  • 새 레이블 컨트롤 만들기
  • SuspendLayout 호출
  • 레이블 컨트롤의 모든 속성 설정
  • 사용자 컨트롤의 Control 컬렉션에 레이블 컨트롤 추가
  • 사용자 컨트롤의 초기 크기 설정
  • ResumeLayout 호출
이러한 작업은 그림 5에 요약되어 있습니다. 컨트롤을 생성하려면 많은 양의 코드를 작성해야 하며 오류가 발생하기 쉽습니다. 코드를 각각의 함수로 구분하면 보다 읽기 쉽도록 구성할 수 있습니다.

그림 5에 있는 대부분의 코드 섹션은 비슷한 작업을 수행합니다. 즉, Ldarg_0 opcode를 사용하여 this 포인터를 스택에 로드한 다음 매개 변수를 스택에 로드합니다. 그런 다음 속성 setter 호출 또는 일부 경우 메서드 호출에 매개 변수를 사용합니다. 다음은 컨트롤 크기를 설정하는 코드의 예제입니다.

initIL.Emit ( OpCodes.Ldarg_0 ) ;
initIL.Emit ( OpCodes.Ldc_I4, 328 ) ;
initIL.Emit ( OpCodes.Ldc_I4, 200 ) ;
initIL.Emit ( OpCodes.Newobj , 
    typeof ( Size ).GetConstructor( 
        new Type[] { typeof ( int ) , typeof ( int ) } ) ) ;
initIL.Emit ( OpCodes.Callvirt , controlClass.GetProperty("Size",
    typeof(Size)).GetSetMethod ( ) ) ;

this 포인터 뒤에 328 및 200의 두 정수를 붙여 스택에 로드한 다음 이 두 정수를 사용해 크기 개체를 생성하는 Size 생성자를 호출합니다. 이 크기 개체는 스택 맨 위에 반환됩니다. 그런 다음 컨트롤의 Size 속성을 가져오고 해당 속성의 setter를 검색합니다. 이 setter는 현재 스택에 있는 인수로 호출합니다. 그러면 다음 호출이 생성됩니다.

this.Size = new Size ( 328, 200 ) ;

이 솔루션은 상속 과정이 복잡하기 때문에 광범위하게는 사용하지 않는 것이 좋습니다. 다음 솔루션에서는 코드를 작성하는 사용자가 보다 쉽게 배울 수 있는 융통성 있는 IL 생성 방법을 제공합니다. 다른 CLR 대상 언어로 수행할 수 없는 까다로운 작업은 IL을 사용하여 수행할 수 있습니다. 그러나 대부분의 응용 프로그램에서 이러한 작업은 필요하지 않습니다.


System.CodeDom을 사용하여 Windows.Forms 컨트롤 생성

CodeDOM 네임스페이스는 프로그래밍 가능한 요약된 코드 모델을 제공합니다. 이 모델은 간단한 식을 사용하여 메모리에서 구축되며, 사용 가능한 .NET 코드 생성기 중 하나를 사용하여 소스 코드로 변환할 수 있습니다. Microsoft에서는 현재 C#, Visual Basic, C++, JScript, J# 및 MSIL용 코드 생성기를 제공합니다. 또한 다른 언어를 위한 타사 생성기도 사용 가능합니다.

각 단계를 직접 비교할 수 있도록 Reflection.Emit 예제에서 사용했던 것과 동일한 섹션으로 코드를 구분했습니다. 코드를 생성하는 두 메서드는 서로 호환되지 않습니다. 즉, CodeDOM을 동일한 어셈블리에서 내보낸 코드와 혼합할 수는 없습니다.


어셈블리 정의 및 컴파일

CodeDOM을 사용하기 위한 첫 번째 단계는 System.CodeDom 네임스페이스를 참조하는 것입니다. 그런 다음 어셈블리의 기반을 형성하는 CodeCompileUnit을 만들어야 합니다. CodeCompileUnit을 사용하여 네임스페이스를 추가하고 이러한 네임스페이스에 import 문을 추가하고 형식을 정의할 수 있습니다.

다음 코드에서는 특정 어셈블리를 만드는 방법을 보여 줍니다. 필자는 CodeCompileUnit을 만들고 여기에 MyControls 네임스페이스를 추가한 다음 코드에 사용된 모든 클래스에 대해 import 문을 정의했습니다. 그런 다음 네임스페이스는 코드 컴파일 단위에 추가됩니다.

// 코드 컴파일 단위 및 네임스페이스를 만듭니다.
CodeCompileUnit ccu = new CodeCompileUnit ( ) ;
CodeNamespace ns = new CodeNamespace ( "MyControls" ) ;

// imports 문을 네임스페이스에 추가합니다.
ns.Imports.Add ( new CodeNamespaceImport ( "System" ) ) ;
ns.Imports.Add ( new CodeNamespaceImport ( "System.Drawing" ) ) ;
ns.Imports.Add ( new CodeNamespaceImport ( "System.Windows.Forms" ) ) ;

// 코드 컴파일 단위에 네임스페이스를 추가합니다.
ccu.Namespaces.Add ( ns ) ;

형식 정의를 완료하고 나면 다음 코드를 통해 C# 코드 공급자를 사용하여 정의를 어셈블리로 컴파일할 수 있습니다. 이 섹션을 완료하기 위해 여기에 컴파일 단계를 추가했습니다. 그러나 논리적으로 이 작업은 어셈블리에 형식을 만든 후에 수행합니다. 이에 대해서는 이 기사 뒷부분에서 설명합니다.

CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider ( ) ;
ICodeCompiler compiler = provider.CreateGenerator ( ) as ICodeCompiler ;
CompilerParameters cp = new CompilerParameters ( new string[] { 
    "System.dll", "System.Windows.Forms.dll", "System.Drawing.dll" } ) ;
cp.GenerateInMemory = true;
cp.OutputAssembly = "AutoGenerated";
CompilerResults results = compiler.CompileAssemblyFromDom ( cp, ccu ) ;
여기서 CodeDomProvider가 C#용으로 만들어지며 CreateGenerator를 호출하여 ICodeCompiler 인터페이스가 반환됩니다. 그런 다음 이 어셈블리에서 참조해야 하는 라이브러리를 정의합니다. 이 CompilerParameters 클래스에서 일부 매개 변수를 설정한 다음 CompileAssemblyFromDom 메서드를 호출합니다. 이 코드에서 지정한 설정을 기반으로 하여 이름이 AutoGenerated인 어셈블리가 메모리에 생성됩니다.

CompilerResults 클래스에는 성공적으로 수행된 어셈블리 컴파일 작업에서 설정된 어셈블리 속성이 들어 있습니다. 이 어셈블리에서 형식을 로드하거나 필요한 작업을 수행할 수 있습니다.


CodeDOM으로 형식 정의

CodeDOM을 사용한 형식 만들기는 매우 쉽습니다. 다음 코드에서는 UserControl에서 파생되는 클래스를 만드는 방법을 보여 줍니다.

CodeTypeDeclaration ctd = new CodeTypeDeclaration ( "MyControl" ) ;
ctd.BaseTypes.Add ( typeof ( System.Windows.Forms.UserControl ) ) ;
ns.Types.Add ( ctd ) ;
CodeTypeDeclaration에는 형식이 파생되는 클래스를 포함하는 BaseTypes라는 컬렉션이 들어 있습니다. 또한 형식이 구현하는 인터페이스가 들어 있을 수 있습니다. 이는 임시 형식 컬렉션이지만 CLR은 단일 상속만을 지원하므로 여기에 둘 이상의 클래스를 사용할 수는 없습니다. 둘 이상의 기본 클래스를 사용하려고 하면 오류가 발생합니다.


생성자 정의

CodeTypeDeclaration이 있으면 다른 모든 코드는 이 정의에 추가됩니다. 다음 코드 조각에서는 생성자를 정의할 때 만들어진 ctd 변수를 사용합니다.

CodeConstructor constructor = new CodeConstructor ( ) ;
constructor.Statements.Add ( new CodeMethodInvokeExpression ( 
    new CodeThisReferenceExpression ( ) , 
        "InitializeComponent", emptyParams ) ) ;
constructor.Attributes = MemberAttributes.Public ;
ctd.Members.Add ( constructor ) ;

생성자 및 CodeMemberMethod에서 파생된 다른 형식은 Statements라는 CodeStatementCollection 속성을 포함합니다. 여기에 CodeStatement 개체 형식으로 하나 이상의 문을 추가할 수 있습니다. CodeExpression을 추가하는 경우 CodeExpressionStatement 개체에 자동으로 래핑됩니다. 이 예제에서는 this.InitializeComponent를 호출하고 매개 변수를 전달하지 않는 CodeMethodInvokeExpression을 추가했습니다. 그런 다음 생성자에 공개적으로 액세스할 수 있도록 지정하고 마지막으로 생성자를 형식 정의에 추가합니다.


InitializeComponent 정의

그림 6에 나와 있는 InitializeComponent 메서드는 만들어야 하는 문이 많기 때문에 생성자보다 훨씬 복잡합니다. 그러나 이 코드는 길이는 길지만 해당 IL 코드보다 간단하게 작성할 수 있습니다. 코드 작동 방식을 살펴볼 수 있도록 코드에 대해 좀 더 자세히 알아보겠습니다. 먼저 SuspendLayout 호출을 살펴봅니다.

this.SuspendLayout ( ) ;
이 식은 CodeMethodInvokeExpression을 사용하고 메서드가 호출된 개체에 대한 참조를 전달하여 생성합니다. 그 뒤에는 메서드 이름과 해당 메서드로 전달해야 하는 매개 변수가 옵니다. CodeMethodInvokeExpression은 다음과 같이 호출됩니다.
CodeMethodInvokeExpression ( targetObject, methodName, parameters )
이 인스턴스에서 targetObject는 this이고 methodName은 SuspendLayout이며 매개 변수는 없으므로 코드에서 다음과 같이 정의됩니다. 참고로 emptyParams는 CodeExpression 형식의 빈 배열입니다.
initializeComponent.Statements.Add( 
  new CodeMethodInvokeExpression( 
  new CodeThisReferenceExpression(), "SuspendLayout", 
  emptyParams));

다음 예제에서는 속성을 특정 값으로 설정하는 방법을 보여 줍니다. 이 인스턴스에서 내보내는 코드는 다음과 같습니다.

_label.TabIndex = 0 ;
여기서는 두 매개 변수, 즉 왼쪽(lhs, 할당 대상) 및 오른쪽(rhs, 할당되는 개체)을 취하는 CodeAssignmentStatement를 사용해야 합니다. 이 인스턴스에서 lhs는 CodePropertyReferenceExpression이고 rhs는 CodePrimitiveExpression입니다.
initializeComponent.Statements.Add( 
    new CodeAssignStatement(
        // 이는 lhs를 형성하며
        // _label.TabIndex입니다.
        new CodePropertyReferenceExpression( 
            new CodeVariableReferenceExpression ( "_label" ), 
            "TabIndex"),
        // 이는 rhs입니다.
        new CodePrimitiveExpression ( 0 ) ) ) ;

CodePropertyReferenceExpression 자체도 속성이 정의되는 개체 및 속성 이름의 두 매개 변수를 사용합니다. 여기서는 _label 개체를 참조하는 CodeVariableReferenceExpression을 사용했으며 해당되는 속성을 TabIndex로 지정했습니다.

이를 CodeAssignStatement 호출로 둘러쌌으며, 이 호출은 해당 속성에 값 0을 할당합니다. 문자열 및 정수 등의 특정 기본 제공 형식에는 CodePrimitiveExpression을 사용할 수 있습니다.

열거된 값 예제에서 코드에 열거된 값을 사용하는 방법을 확인할 수 있습니다. 열거된 값은 열거된 형식에 필드로 저장됩니다. 그러므로 예제에서 AnchorStyles.Left 값과 같이 지정된 필드의 값을 가져오려면 CodeFieldReferenceExpression을 사용해야 합니다.

둘 이상의 필드가 조합된 값이 있는, 보다 복잡한 열거된 값의 경우에는 CodeBinaryOperatorExpression을 사용하여 이를 조합해야 합니다. 예를 들어, CodeBinaryOperatorExpression을 사용하여 필드 값을 조합할 수 있습니다. 이는 AnchorStyle 속성을 정의할 때 일반적으로 사용되는 방법입니다. 이러한 값은 보통 논리 OR을 사용하여 조합되며, field1 | field2 형식으로 코드에 내보내집니다.

생성된 코드 내에는 개체가 만들어져 적절한 속성으로 할당되는 여러 위치가 있습니다. 다음은 그 중 한 가지 예제입니다.

_label.Location = new System.Drawing.Point(8, 8);
그리고 다음은 이를 생성하는 코드입니다.
initializeComponent.Statements.Add( 
    new CodeAssignStatement(
      new CodePropertyReferenceExpression( // this._label에 해당함
          new CodeVariableReferenceExpression ( "_label" ), "Location"),
          new CodeObjectCreateExpression(  // new Point(8,8)에 해당함
              typeof ( System.Drawing.Point ) , 
              new CodeExpression[]{
                  new CodePrimitiveExpression ( 8 ) , 
                  new CodePrimitiveExpression ( 8 ) } ) ) );

일반적인 CodePropertyReferenceExpression을 사용하여 문의 lhs를 생성합니다. CodeObjectCreateExpression은 만들 개체의 형식으로 전달되며 매개 변수 컬렉션은 생성자로 전달됩니다. 레이블의 크기 또는 전체 컨트롤의 초기 크기를 설정할 때도 이와 비슷한 코드를 확인할 수 있습니다.


ASP.NET 예제

이제 런타임에 ASP.NET 컨트롤을 생성하는 방법을 살펴보겠습니다. 여기서 사용하는 코드는 Windows Forms 예제에서 사용하는 코드와 거의 비슷하므로 생략합니다. 이 코드는 이 기사에 포함된 다운로드에서 확인할 수 있습니다. 이 예제에서는 사용자가 입력한 문자열을 렌더링하는, WebControl에서 파생되는 서버 컨트롤을 만듭니다. 사이트를 실행하면 그림 7과 같이 텍스트를 입력한 다음 IL 또는 CodeDOM 예제를 생성할 수 있습니다.

그림 7 ASP.NET 컨트롤 생성
그림 7 ASP.NET 컨트롤 생성

단추 중 하나를 클릭하면 코드가 서버 컨트롤을 생성하여 패널 내로 로드합니다. 그러면 사이트는 그림 8과 같이 표시됩니다. 이를 가장 유용한 컨트롤이라고 할 수는 없지만, 이 컨트롤을 통해 ASP.NET 페이지에서 사용되는 컨트롤을 런타임에 생성하기 위한 원칙을 확인할 수 있습니다.

그림 8 웹 페이지에 배치된 생성된 컨트롤
그림 8 웹 페이지에 배치된 생성된 컨트롤

ASP.NET에는 서버 컨트롤 및 사용자 컨트롤의 두 가지 컨트롤 형식이 있습니다. 서버 컨트롤은 어셈블리 내에 있으며 보통 WebControl의 하위 클래스를 지정하여 만듭니다. 이러한 컨트롤은 최종 사용자에게 가장 적합한 디자인 타임 동작을 제공합니다.

반면 사용자 컨트롤은 .ascx 파일과 관련 코드 숨김 파일로 구성됩니다. .ascx는 컨트롤의 레이아웃을 정의하고 코드 숨김 파일은 동작을 정의합니다. 이 예제의 경우에는 서버 컨트롤을 생성하는 작업이 사용자 컨트롤을 생성하는 것보다 훨씬 쉬우므로 이 기사에서는 서버 컨트롤을 생성합니다.

이 예제와 Windows Forms 예제 간의 가장 큰 차이점은 컨트롤이 렌더링되는 방식입니다. Windows Forms 사용자 지정 컨트롤을 사용하는 경우 각 구성 컨트롤은 InitializeComponent 메서드 내에 추가됩니다. ASP.NET 컨트롤을 사용하는 경우에는 HTML 출력 렌더링을 위한 두 가지 옵션이 있습니다. 둘 중 하나는 Render 메서드를 재정의하고 HTML을 출력 스트림에 명시적으로 작성하여 컨테이너 컨트롤에 대해 모든 콘텐츠를 생성하는 것입니다. 그리고 다른 하나는 CreateChildControls 메서드 내에 추가되는 컨트롤을 활용하는 것입니다.

이 예제에서는 Render 메서드를 재정의하는 첫 번째 방법을 사용합니다. 이 예제는 간단하지만 런타임에 컨트롤을 정의하는 방법을 이해하기 위한 기본 사항을 제공합니다. 원하는 경우 추가적인 속성 및 렌더링 논리를 추가하여 예제를 확장할 수도 있습니다. 컨트롤은 다음 코드를 사용하여 런타임에 로드됩니다.

Control ctrl = Activator.CreateInstance ( t ) as Control ;
t.GetProperty("Text").SetValue ( ctrl, caption, new object[] { } ) ;
this.controlPlaceholder.Controls.Add ( ctrl ) ;
t 형식은 생성된 컨트롤을 나타냅니다. 필자는 Activator.CreateInstance를 사용하여 컨트롤을 인스턴스화했습니다. 다음 줄은 Text 속성 값을 설정하며 런타임 바인딩 예제를 보여 줍니다. 이에 대해서는 잠시 후에 설명합니다. 마지막 줄은 새로 만든 컨트롤을 페이지의 자리 표시자 컨트롤에 추가합니다.

사용자 지정 서버 컨트롤 만들기에 대한 자세한 내용을 알아보려는 분께는 Nikhil Kothari와 Vandana Datye가 함께 저술한 Developing Microsoft ASP.NET Server Controls and Components(Microsoft Press, 2002)를 추천해 드립니다. 개인적으로 이 책은 컨트롤 만들기의 모든 측면에 대해 빈틈없이 다루고 있으며 직접 서버 컨트롤을 정의하려는 경우에는 꼭 참조해야 하는 자료라고 생각합니다.

이 예제에서는 생성된 컨트롤을 디스크에 보관하지 않았습니다. 이렇게 한 데는 두 가지 이유가 있습니다. 가장 큰 이유는 컨트롤을 디스크에 보관하지 않고도 생성할 수 있음을 보여 드리기 위해서입니다. 컨트롤은 완전히 임시 상태일 수 있으므로 응용 프로그램이 다시 시작되면 삭제됩니다. 두 번째 이유는 응용 프로그램이 컨트롤을 디스크에 생성하도록 허용하는 경우 해당 응용 프로그램에 보안 결함이 발생할 수 있기 때문입니다.

응용 프로그램이 생성된 어셈블리를 디스크에 저장하는 경우 ASP.NET에는 DLL 파일을 디스크에 저장할 수 있는 권한이 있어야 합니다. 이 경우 파일을 다시 로드하여 사용자에게 데이터를 표시할 수 있습니다. 침입자가 이 디렉터리에 파일을 쓸 수 있는 권한을 얻으면 응용 프로그램에 코드를 주입할 수 있으며 그러면 심각한 결과가 발생할 수 있습니다.

컨트롤 생성을 허용하도록 응용 프로그램을 수정하는 경우에는 웹 사이트 내에서 실행하고 있지 않은 관리 도구 집합 부분을 만드는 것이 좋습니다. 예를 들면 Windows Forms 응용 프로그램 등이 있습니다. 이 도구 집합은 사용자 데이터베이스에서 컨트롤 정의를 읽고 컨트롤을 생성하여 디스크 또는 SQL Server 데이터베이스 내에 저장합니다. 그러면 ASP.NET 사이트는 이렇게 생성된 컨트롤을 런타임에 로드하면 됩니다.


데이터 바인딩

이렇게 생성된 컨트롤을 페이지에 사용할 때는 백 엔드 데이터베이스 개체의 데이터를 컨트롤로 바인딩할 수 있어야 합니다. 데이터를 UI에 바인딩하기 위해 생성된 코드를 사용할 수도 있고(권장) 리플렉션을 사용할 수도 있습니다. 이 섹션에서는 두 가지 방법을 모두 설명합니다. 이 예제에서는 기본 비즈니스 개체의 속성과 사용자 인터페이스의 컨트롤이 일대일 대응한다고 가정합니다.

이 기사의 주된 목적은 고성능의 구성 가능한 솔루션을 만들기 위한 전략을 제공하는 것이므로, 데이터 바인딩 수행을 위한 코드 내보내기는 반드시 수행해야 하는 단계입니다. 컨트롤을 생성하는 동안 쉽게 데이터 바인딩 코드를 생성할 수 있습니다. 앞에서도 언급했지만 UI 컨트롤과 기본 비즈니스 개체 간에는 직접적인 일대일 대응 관계가 성립합니다. 생성되는 코드는 다음 코드 조각에서처럼 기본적으로 각 속성을 반복하며 컨트롤의 텍스트 값을 해당 속성의 값으로 설정합니다.

control.Text = businessObject.Text;
ASP.NET용으로 이 코드를 내보내는 적절한 방법은 Control 클래스에 정의되어 있는 표준 DataBind 메서드로 재정의하는 것입니다.

리플렉션을 사용하여 비즈니스 논리 구성 요소의 데이터를 가져와 컨트롤에 표시할 수 있습니다. Visual Studio를 사용하는 경우 컨트롤은 Bindable 특성을 속성 정의에 추가하여 데이터 바인딩에 대해 속성을 사용 가능한 것으로 정의합니다.

[Bindable(true)]
public string Text { get; set; }

그러므로 그림 9에 나와 있는 것과 같은 코드를 사용하여 컨트롤의 바인딩 가능한 속성을 모두 가져올 수 있습니다. 첫 번째 줄은 t 형식에 정의되어 있는 모든 공용 인스턴스 속성의 컬렉션을 요청합니다. BindingFlags.DeclaredOnly를 통해 직접 형식의 속성만 사용되도록 할 수 있습니다. BindingFlags.DeclaredOnly가 없으면 해당 형식에 대한 상속 계층 구조의 모든 속성이 반환됩니다.

그런 후에 이 코드는 이러한 속성을 반복하여 Bindable 특성을 포함하는 항목을 찾습니다. 개발자가 Bindable(false)을 사용했을 수도 있기 때문에 코드는 특히 각 속성에 대해 특성이 Bindable(true)로 정의되어 있는지 여부를 확인합니다.

이 바인딩 가능 속성 컬렉션을 사용하면 그림 10과 같은 코드를 작성하여 비즈니스 개체의 값을 기반으로 컨트롤 값을 설정할 수 있습니다.

이 함수는 컨트롤의 모든 바인딩 가능 속성을 반복하면서 각 속성에 대해 이름 및 반환 형식이 같은 속성이 비즈니스 개체에 있는지 확인합니다. 이러한 속성이 있는 경우 컨트롤의 속성은 비즈니스 개체의 속성 값으로 설정됩니다. 이와 유사한 코드를 사용하여 사용자가 UI에 입력하는 데이터를 읽은 다음 해당 데이터를 비즈니스 개체로 다시 보낼 수 있습니다.


코드 단순화

이제 샘플을 통해 내보내는 코드가 상당히 구체화되었습니다. 코드를 단순화하기 위해 기본 클래스(ASP.NET용 WebControl 또는 Windows Forms용 Control에서 파생됨)를 생성하고 컨트롤을 생성하는 까다로운 작업을 수행하도록 컨트롤에 함수를 포함할 수 있습니다. 예를 들어, 다음과 같이 함수 집합을 사용하여 컨트롤을 생성하는 메서드를 정의할 수 있습니다.

private Label CreateLabelControl ( string ID, string caption )
{
  Label newLabel = new Label ( ) ;
  newLabel.ID = ID;
  newLabel.Text = caption ;
  this.Controls.Add ( newLabel ) ;
}

모든 형식의 컨트롤을 만들기 위한 기본 클래스 함수를 만들 수 있습니다. 그러면 내보낸 코드는 컨트롤의 인스턴스 변수를 생성하고 이러한 기본 클래스 함수를 호출하여 컨트롤을 만드는 작업을 수행하면 됩니다.


결론

이 기사에서는 두 가지 스타일의 런타임 코드 생성 방법(Reflection.Emit 및 System.CodeDom)을 사용하여 Windows Forms 및 ASP.NET 응용 프로그램용 컨트롤을 생성하는 방법에 대해 설명했습니다. 또한 예제를 통해 이와 같이 서로 다른 두 방법에 대해 사용할 수 있는 기능을 일부 설명했습니다. 이제 이 기사에서 설명한 개념을 응용 프로그램에 직접 적용해 보십시오.

현재 XML 정의를 사용하여 인터페이스 컨트롤을 생성하는 경우, 이러한 방법을 사용하는 경우의 상대적인 성능에 관한 섹션 내용을 확인하여 CodeDOM 또는 Reflection.Emit 방식으로 변경하는 방안을 고려해 보시기 바랍니다. 컨트롤을 즉시 작성함으로써 얻을 수 있는 성능 향상을 경험한다면 투자할 만한 가치가 있음을 알게 될 것입니다.



Morgan Skinner 영국 Microsoft에서 C#, Windows Forms 및 ASP.NET 전문 응용 프로그램 개발 컨설턴트로 근무하고 있습니다. 그는 .NET 도입 당시부터 .NET 작업을 수행해 오고 있습니다. PSfD(Premier Support for Developers) (영문) 팀 블로그를 방문해 보십시오.

페이지 맨 위로페이지 맨 위로QJ: 051203

Microsoft