티스토리 뷰




 
지난 시간에 이어, 이번 시간에는 실제 Provider 를 구현해 보도록 하겠습니다. 지난 시간에 언급하였듯이, 실제 쿼리식을 해석하고, 동작이 가능한 형태로 바꾸는 작업을 아래의 Provider 에서 할 수 있습니다.
 
IQueryProvider
 
실제로 외부 서버나 서비스 등에 필요한 쿼리를 만들 수 있는 인터페이스입니다. 이 인터페이스는 4개의 메서드를 지원합니다. 중복된(제너릭/비제너릭) 메서드를 제외하면 2개의 메서드만 제대로 구현하시면 됩니다.
 
우선 소스부터 나갑니다.
 
public class SampleProvider : IQueryProvider
{
       private SampleContext context;
 
       public SampleProvider(SampleContext context)
       {
             this.context = context;
       }
 
       #region IQueryProvider Members
 
       public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
       {
             return (IQueryable<TElement>)context;
       }
 
       public IQueryable CreateQuery(Expression expression)
       {
             throw new NotImplementedException();
       }
 
       public TResult Execute<TResult>(Expression expression)
       {
             var exp             = expression as MethodCallExpression;
             var func     = (exp.Arguments[1] as UnaryExpression).Operand as Expression<Func<Person, bool>>;
             var lambda   = Expression.Lambda<Func<Person, bool>>(func.Body, func.Parameters[0]);
 
             var r = context.DataSource.Where(lambda.Compile());
 
             return (TResult)r.GetEnumerator();
       }
 
       public object Execute(Expression expression)
       {
             throw new NotImplementedException();
       }
 
       #endregion
}
 
소스에는 특별한 주석은 없으며, 차근차근 서술식으로 의문을 풀어드리도록 하겠습니다.
 
 
Constructor
이전에 봤던 SampleContext 객체에서 Provider 프로퍼티를 보셨을 겁니다. 프로퍼티는 다음과 같이 정의되어 있습니다.
 
public IQueryProvider Provider
{
        get
        {
               if( provider == null )
                       provider = new SampleProvider(this);
 
               return provider;
        }
}
 
SampleProvider 에서 SampleContext 에 정의된 DataSource 나, 다음 회차에서 볼 원격 탐색을 하거나, 로그를 남길 수 있도록 하기 위해 SampleContext 를 참조해야 합니다.
 
CreateQuery<T> Method
이 메서드는 Expression, 즉 표현식의 인자를 받는 메서드입니다. IQueryable<TElement> 를 리턴하며, TElement 의 타입은 Person 이 되겠습니다.
 
context.expression = expression;
return (IQueryable<TElement>)context;
 
와 같이 매우 보잘 것 없는 내용으로 구현되었습니다. 하지만, 원격 개체에 연결하기 위한 쿼리를 만들기 위해서 이곳에서 실제 SQL 쿼리의 “SELECT” 구문과 같은 쿼리를 만드시면 됩니다.
 
Execute<T> Method
쿼리식을 이곳에서 해석해서 실행합니다. SampleContext 객체에서 GetEnumerator 메서드는
 
public IEnumerator<Person> GetEnumerator()
{
        return provider.Execute<IEnumerator<Person>>(this.expression);
}
 
보시는 것과 같이, SampleProvider 의 Execute 메서드를 호출합니다. 리턴 타입이 IEnumerator<Person> 인 것을 미루어 보아, 쿼리 호출 후 반환되는 결과값을 리턴하는 것이라는 것을 짐작할 수 있습니다.
 
여기에서도 어김없이 몇 가지 재미있는 표현식(Expression) 이 나옵니다.
 
먼저, MethodCallExpression 을 보겠습니다. 이 Expression 은 쿼리식에서 호출한 메서드를 가져오는 녀석입니다. 아래의 샘플을 보겠습니다.
 
Expression<Func<string,string,bool>> func = (s1,s2) => ( s1.Substring(0,2) == s2 );
BinaryExpression be        = func.Body as BinaryExpression;
MethodCallExpression me    = be.Left as MethodCallExpression;
Console.WriteLine(me.Method.Name);

이항 연산식의s1.Substring(0,2) == s2” 좌측부분을 가져와서 이것을 MethodCallExpression 으로 변환하여 실행 메서드 내용을 가져옵니다. 감이 잡히시나요?
 
답은
 
Substring
 
됩니다.
 
이런 방법으로 실제 메서드를 원격 서버의 쿼리식에 맞도록 변환 할 있습니다.
 
위의 func 아래와 같이 바꾸어
 
Expression<Func<string,string,bool>> func = (s1,s2) => ( s1.ToUpper() == s2 );
 
결과는 “ToUpper” 출력 되지만, MSSQL 쿼리로 “UPPER” 변환 하는 것과 같이 쿼리식을 자유자제로 변형 할 있습니다.
 
그럼, 다음으로 나오는 UnaryExpression 보겠습니다.표현식은 단항 연산자가 있는 식을 나타냅니다. bool 연산식에서 부정 NOT 등과 같이 하나의 피연산자를 사용하는 bool 연산자입니다.
 
들어가기 전, 문제를 하나내겠습니다. UnaryExpression 단항 연산자를 나타내는 표현식 클래스입니다. 그럼, 이항 연산자를 나타내는 클래스는??
 
 
그렇습니다. 이항 연산을 나타내는 클래스는 BinaryExpression 입니다. 후후,,, 기억하고계시는군요.
 
다음은 UmcBlog Article 테이블입니다.
 
r[
그림2] UmcBlog Article 테이블
 
UmcBlogDataContext db      = new UmcBlogDataContext();
var query    = from article in db.Articles
              where article.Title.Contains("Umc")
              select article;
 
MethodCallExpression me    = query.Expression as MethodCallExpression;
UnaryExpression ue         = me.Arguments[1] as UnaryExpression;
Console.WriteLine(me.Method.Name);
Console.WriteLine(ue.Operand);
 
이것의 결과는 아래와 같습니다.
 
Where
article >= article.Title.Contains(“Umc”)
 
위의 UnaryExpression 결과가 이렇게 나오는지 이해가 가지 않습니다. 분명 단항 연산자는 피연산자가 하나일 경우를 일컷습니다. UnaryExpression.Operand 속성은 단항 연산의 피연산자를 가져온다고 했는데, 위의 결과는 연산자가 LambdaExpression 말하는 같군요. 피연산자를 LambdaExpression 대리자인 “article” 일컷는 것이라 생각되지만 확실히 감이 서질 않습니다. 누가 아시는 분 답변 좀 부탁합니다,.;
 
LambdaExpression
이것은 우리가 흔히 사용하는 바로 람다식표현하는 Expression 입니다. 여러가지 Expression 조합해서 약간의(?) 동적으로 람다식을 만들어 수도 있습니다. (준비된 표현식을 이용하여…)
 
Expression<Func<string,string,string>> func   = ( s1, s2 ) => ( s1 + s2 );
var le       = Expression.Lambda(func.Body, func.Parameters.ToArray());
 
var result1 = func.Compile();
var result2 = le.Compile();
 
Console.WriteLine( result1("Umc","Blog" ) );
Console.WriteLine( result2.DynamicInvoke("Umc","Blog") );
 
func 표현식을 명시적으로 컴파일되며, le 표현식을 이용해 람다식을 만든것입니다.
 
위에 보이는 Compile() 메서드를이용하여 IL 코드로변환하게됩니다.
 
IL (Intermediate Language)코드란?
.NET 에서 MSIL 이라고도 부릅니다. 컴파일러에 의해 실행이 가능하도록 중간 언어로 컴파일 것을 말합니다. 비로소, IL 코드는 .NET Framework CLR(Common Language Runtime) JIT(Just-In-Time) 컴파일러에 의해 Native 코드로 컴파일이 됩니다.
 
Compile() 메서드는 아래와 같이 생겼답니다.
 
internal Delegate Compile(LambdaExpression lambda)
{
    this.lambdas.Clear();
    this.globals.Clear();
    int num2 = this.GenerateLambda(lambda);
    ExpressionCompiler.LambdaInfo info2 = this.lambdas[num2];
    ExpressionCompiler.ExecutionScope scope2 =
        new ExpressionCompiler.ExecutionScope(
                      null,
                      lambda,
                      this.lambdas.ToArray(),
                      this.globals.ToArray(),
                      info2.HasLiftedParameters);
    return info2.Method.CreateDelegate(lambda.Type, scope2)
}
private int GenerateLambda(LambdaExpression lambda)
{
    this.scope = new ExpressionCompiler.CompileScope(this.scope, lambda);
    DynamicMethod method2 = new DynamicMethod(“lambda_” + ExpressionCompiler.iMethod++,
                                              lambda.Body.Type,
                                              this.GetParameterTypes(lambda),
                                              typeof(ExpressionCompiler.ExecutionScope),
                                              true);
    ILGenerator generator2 = method2.GetILGenerator();
    this.GenerateInitLiftedParameters(generator2);
    this.Generate(generator2, lambda.Body, ExpressionCompiler.StackType.Value);
    generator2.Emit(OpCodes.Ret);
    int num2 = this.lambdas.Count;
    this.lambdas.Add(new ExpressionCompiler.LambdaInfo(lambda,
                                                       method2,
                                                       this.scope.HasLiftedParameters));
    this.scope = this.scope.Parent;
    return num2;
}
 
소스 코드가 중요한게 아니라, 바로 Compile() 메서드가 실행 코드로 변환해 준다는 것 입니다. 그래서, LambdaExpression 예제 코드의 실행 결과는
 
UmcBlog
UmcBlog
 
동일한 결과가 나타나게 됩니다. 때문에, SampleProvidier Execute<T> 메서드 내의
 
context.DataSource.Where(lambda.Compile());
 
코드는 어떠한 람다식이라도 실행 가능한 형태로 변환해주게 되며, DynamicInvoke 호출하여 동적으로 컴파일된 람다식을 실행하게됩니다.
 
 
SampleContext Provider 이용한 쿼리 만들기
 
여기까지해서 SampleContext SampleProvider 클래스에 대한 설명은 다 한같습니다. 그럼, Custom LINQ Provider 이용하여 LINQ 식을만들어보면
 
class Program
{
       static void Main(string[] args)
       {
             SampleContext context = new SampleContext() ;
             context.DataSource = new List<Person> {
                 new Person { Name="엄준일", Age=29},
                 new Person { Name="내동생", Age=26},
                    new Person { Name="울누나", Age=31},
                    new Person { Name="멍멍이", Age=6},
                    new Person { Name="발발이", Age=5}
             };
            
 
             var result = from r in context
                                  where r.Age > 20
                                  select r;
 
             result.ToList().ForEach( o=>Console.WriteLine(o.Name ));
       }
}
 
내용은 간단합니다. SampleContext 객체를 만들어, DataSource 임의의 데이터를 넣고, LINQ 쿼리식의 결과를 출력하는 예제입니다.
 
실제로 소스를 실행해 보면,
 
엄준일
내동생
울누나
 
출력이 되고, LINQ 쿼리식에 브레이크 포인터를 걸어 디버깅해 보면, 순차적으로 SampleContext SampleProvider 타면서 LINQ 쿼리식이 해석 되는 것을 있습니다.
 
[
그림3] 브레이크 포인터를 통해 LINQ 식의 Custom LINQ Provider 내부를 디버깅하는 화면
 
다음 시간에는, Custom LINQ Provider 통해 원격 개체를 탐색하는 방법에 대해 살펴 보도록하고, 이만 마치겠습니다. 이번 한주도 즐겁게 시작하시기 바랍니다^^//

댓글