(C#) CodeDOM 으로 동적 코드 자동생성

 

.NET Framework 에서 지원되는 CodeCompileUnit 클래스는 동적으로 코드생성을 가능하게 지원합니다.
.NET Core 이상에서는 Roslyn 기술을 사용해서 동적 코드 생성 처리 방법도 있지만 간단하게 CodeCompileUnit 클래스와 CodeDomProvider 클래스 사용으로 동적 코드 생성을 처리 할 수 있습니다.

이 글에서 다루는 코드는 다음 Repository에서 확인할 수 있습니다.
code_check - CodeDOM_Example

컴파일 단위 만들기

CodeCompileUnit 클래스는 소스 코드를 모델링하는 CodeDOM 객체 그래프 생성을 위한 컴파일 단위 입니다.
CodeCompileUnit 클래스를 통해 네임스페이스 및 어셈블리에 대한 참조를 설정할 수 있습니다.

네임스페이스 정의

네임스페이스는 CodeNamespace 클래스로 정의할 수 있습니다.

// 네임스페이스 설정
CodeNamespace samples = new CodeNamespace("CodeDOMSample");

그리고 CodeNamespaceImport 클래스로 Import 설정이 가능합니다.

// Import 설정
samples.Imports.Add(new CodeNamespaceImport("System"));

이렇게 설정한 네임스페이스는 CodeDOM 객체 그래프에 연결할 수 있습니다.

// 객체 그래프에 네임스페이스 추가
_targetUnit.Namespaces.Add(samples);

형식 정의

CodeTypeDeclaration 클래스는 형식을 정의할 수 있는데 클래스, 구조체, 열거형 등을 정의하고 네임스페이스 형식에 추가할 수 있습니다.

// 클래스 이름 설정
_targetClass = new CodeTypeDeclaration("CodeDOMCreatedClass");
_targetClass.IsClass = true;
// public 접근자로 설정
_targetClass.TypeAttributes =
TypeAttributes.Public;
// 네임스페이스에 추가
samples.Types.Add(_targetClass);

맴버 필드 정의

CodeMemberField 클래스를 사용해서 맴버 필드의 특성을 지정하고 CodeTypeDeclaration 클래스에 추가하여 사용 할 수 있습니다.

// Private 필드 생성
CodeMemberField ageValueField = new CodeMemberField();
// Private 접근자로 설정
ageValueField.Attributes = MemberAttributes.Private;
// 필드 이름
ageValueField.Name = "_ageValue";
// Int16 타입으로 설정
ageValueField.Type = new CodeTypeReference(typeof(System.Int16));
// 주석 추가
ageValueField.Comments.Add(new CodeCommentStatement("나이"));
// 클래스에 맴버 필드로 추가
_targetClass.Members.Add(ageValueField);

속성 필드 정의

CodeMemberProperty 클래스를 사용해서 속성의 특성을 지정하고 CodeTypeDeclaration 클래스에 추가하여 사용 할 수 있습니다.
속성의 get / set 필드 설정은 CodeMemberProperty 클래스의 GetStatements 속성과 SetStatements 속성으로 정의할 수 있으며, 동적인 코드 연산 처리는 CodeBinaryOperatorExpression 클래스로 표현 가능합니다.

// Age 속성 생성
CodeMemberProperty ageProperty = new CodeMemberProperty();
// public 접근자로 설정
ageProperty.Attributes =
  MemberAttributes.Public | MemberAttributes.Final;
  // 속성 이름
  ageProperty.Name = "Age";
  // get 지정
  ageProperty.HasGet = true;
  // Int16 타입으로 설정
  ageProperty.Type = new CodeTypeReference(typeof(System.Int16));
  // 주석 추가
  ageProperty.Comments.Add(new CodeCommentStatement("나이"));

// get 반환 필드 지정 [_ageValue]
ageProperty.GetStatements.Add(new CodeMethodReturnStatement(
  new CodeFieldReferenceExpression(
  new CodeThisReferenceExpression(), "_ageValue")));

// 클래스에 속성 추가
_targetClass.Members.Add(ageProperty);


// 읽기 전용 속성 생성
CodeMemberProperty introductionProperty = new CodeMemberProperty();
introductionProperty.Attributes =
  MemberAttributes.Public | MemberAttributes.Final;
introductionProperty.Name = "Introduction";
introductionProperty.HasGet = true;
introductionProperty.Type = new CodeTypeReference(typeof(System.String));
introductionProperty.Comments.Add(new CodeCommentStatement("읽기 전용 속성 입니다."));

// get 코드 연산
CodeBinaryOperatorExpression introductionExpression =
  new CodeBinaryOperatorExpression(
    new CodeFieldReferenceExpression(
      new CodeThisReferenceExpression(), "_ageValue"),
    CodeBinaryOperatorType.Add,
    new CodeFieldReferenceExpression(
      new CodeThisReferenceExpression(), "_nameValue"));

// get 반환 필드 지정 [위에서 처리한 연산 코드 introductionExpression]
introductionProperty.GetStatements.Add(
  new CodeMethodReturnStatement(introductionExpression));

// 클래스에 속성 추가
_targetClass.Members.Add(introductionProperty);

메서드 정의

CodeMemberMethod 클래스를 사용해서 메서드 특성을 지정하고 CodeTypeDeclaration 클래스에 추가하여 사용 할 수 있습니다.
메서드 내에서 로컬 변수 정의, 반환 코드 연산처리는 CodeFieldReferenceExpression 클래스와 CodeMethodReturnStatement 클래스로 처리할 수 있습니다.

// Result 메서드 생성
CodeMemberMethod resultMethod = new CodeMemberMethod();
// public 접근자로 설정
resultMethod.Attributes =
  MemberAttributes.Public;
// 메서드 이름
resultMethod.Name = "Result";
// Return 타입 String 지정
resultMethod.ReturnType =
  new CodeTypeReference(typeof(System.String));

// 반환 값
string formattedOutput = "설정된 이름과 나이는" + Environment.NewLine +
  "{0}, {1} 입니다.";

// 로컬 변수 정의
CodeFieldReferenceExpression ageReference =
  new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Age");

CodeFieldReferenceExpression nameReference =
  new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Name");

  CodeMethodReturnStatement returnStatement = new CodeMethodReturnStatement();

  // Return 코드 연산
  returnStatement.Expression =
    new CodeMethodInvokeExpression(
      new CodeTypeReferenceExpression("System.String"), "Format",
      new CodePrimitiveExpression(formattedOutput), ageReference, nameReference);

resultMethod.Statements.Add(returnStatement);
_targetClass.Members.Add(resultMethod);

생성자 추가

CodeConstructor 클래스를 사용해서 생성자 특성을 지정하고 CodeTypeDeclaration 클래스에 추가하여 사용 할 수 있습니다.
파라메터 정의는 CodeConstructor 클래스의 Parameters 속성으로 설정할 수 있는데 CodeParameterDeclarationExpression 클래스로 타입과 이름 지정이 가능합니다.

// 생성자 생성
CodeConstructor constructor = new CodeConstructor();
// public 접근자로 설정
constructor.Attributes =
  MemberAttributes.Public | MemberAttributes.Final;

// Add parameters.
constructor.Parameters.Add(new CodeParameterDeclarationExpression(
  typeof(System.Int16), "age"));
constructor.Parameters.Add(new CodeParameterDeclarationExpression(
  typeof(System.String), "name"));

// 맴버 필드 초기화
// 생성자 파라메터 age 설정 후 ageReference[_age]에 대입
CodeFieldReferenceExpression ageReference =
  new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_ageValue");

constructor.Statements.Add(new CodeAssignStatement(ageReference, new CodeArgumentReferenceExpression("age")));

// 생성자 파라메터 name 설정 후 nameReference[_name]에 대입
CodeFieldReferenceExpression nameReference =
  new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_nameValue");

constructor.Statements.Add(new CodeAssignStatement(nameReference, new CodeArgumentReferenceExpression("name")));
_targetClass.Members.Add(constructor);

CodeExpression

이 밖에도 CodeExpression 클래스를 상속받고 있는 기타 표현식 클래스를 활용하여 동적 코드 생성처리가 가능합니다.
예를 들어 CodeObjectCreateExpression
클래스로 객체를 인스턴스화 할 수 있고,
CodeMethodInvokeExpression 클래스로 특정 메서드 호출 코드를 작성할 수 있고,
CodeTypeReferenceExpression
클래스로 조합으로 네임스페이스.클래스 특정 코드를 작성해서 메서드 호출 표현 또한 가능합니다.

CodeDomProvider

CodeDomProvider 추상 클래스는 지금까지 CodeCompileUnit 에 추가한 CodeDOM 객체 그래프를 실제 소스코드로 변환해 주는 기능과 컴파일러 기능을 제공합니다.
GenerateCodeFromCompileUnit() 메서드로 CodeDOM 객체를 소스코드로 자동 생성할 수 있으며, CompileAssemblyFromSource() 메서드로 런타임시 컴파일하여 그 결과를 출력할 수 도 있습니다.
지금까지 만든 각각 정의한 동적 코드 표현식을 다음과 같이 CodeDomProvider 클래스로 코드 생성 및 동적 컴파일을 통해 결과를 출력 해보겠습니다.

[동적 코드 파일 생성]

private const string _outputFileName = "SampleCode.cs";

CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BracingStyle = "C";
using (StreamWriter sourceWriter = new StreamWriter(_outputFileName))
{
  provider.GenerateCodeFromCompileUnit(_targetUnit, sourceWriter, options);
}

[동적 코드 String 생성]

CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BracingStyle = "C";
// 자동 코드 생성 String
StringBuilder builder = new StringBuilder();
using (StringWriter sourceWriter = new StringWriter(builder))
{
  provider.GenerateCodeFromCompileUnit(_targetUnit, sourceWriter, options);
}

[자동 생성된 CodeDOMSample.CodeDOMCreatedClass 파일]
[SampleCode.cs]

//------------------------------------------------------------------------------
// <auto-generated>
//     이 코드는 도구를 사용하여 생성되었습니다.
//     런타임 버전:4.0.30319.42000
//
//     파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
//     이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------

namespace CodeDOMSample
{
    using System;
    
    
    public class CodeDOMCreatedClass
    {
        
        // 나이
        private short _ageValue;
        
        // 이름
        private string _nameValue;
        
        public CodeDOMCreatedClass(short age, string name)
        {
            this._ageValue = age;
            this._nameValue = name;
        }
        
        // 나이
        public short Age
        {
            get
            {
                return this._ageValue;
            }
        }
        
        // 이름
        public string Name
        {
            get
            {
                return this._nameValue;
            }
        }
        
        // 읽기 전용 속성 입니다.
        public string Introduction
        {
            get
            {
                return (this._ageValue + this._nameValue);
            }
        }
        
        public virtual string Result()
        {
            return string.Format("설정된 이름과 나이는\r\n{0}, {1} 입니다.", this.Age, this.Name);
        }
    }
}

이렇게 생성된 코드를 다음과 같이 동적으로 컴파일 및 컴파일된 결과 어셈블리를 메모리에 로드해서 직접 호출이 가능합니다.

CodeDomProvider codeDom = CodeDomProvider.CreateProvider("CSharp");
// 메모리에 어셈블리 생성
CompilerParameters param = new CompilerParameters();
param.GenerateInMemory = true;

// 생성된 소스코드 컴파일
CompilerResults results =
  codeDom.CompileAssemblyFromSource(param, System.IO.File.ReadAllText("SampleCode.cs"));

// 컴파일 실패시
if (results.Errors.Count > 0)
{
  foreach (var err in results.Errors)
  {
    Console.WriteLine(err.ToString());
  }
  return;
}

// 어셈블리 로딩            
Type createdClassType = results.CompiledAssembly.GetType("CodeDOMSample.CodeDOMCreatedClass");
object createdClassObj = Activator.CreateInstance(createdClassType, (Int16)20, "테스트");

// 메서드 호출
var result = createdClassObj.GetType().GetMethod("Result").Invoke(createdClassObj, null);
Console.WriteLine(result);

[결과 화면]

설정된 이름과 나이는
20, 테스트 입니다.

어떻게 활용할 수 있나 ?

런타임시 동적 코드 자동 생성기술은 DB의 데이터를 객체 관계형 매핑으로 처리할시 자체적으로 Repository 클래스를 생성하여 공통적인 반복적인 코드를 조건에 맞게 자동 생성하고 주입시켜서 사용하거나, 공통 처리를 위해 가로채기(Interception) 구현이 필요할시 CodeDOM을 활용할 수 도 있습니다.
참고 - Virtual Method Interceptor


위 코드는 다음 Repository에서 확인할 수 있습니다.

code_check - CodeDOM_Example