소스 코드 다운로드시 코드의 Naver Open API Key 를 변경하여 사용하시기 바랍니다.

드디어 5회차까지 왔네요. 이전까지는 Custom LINQ Provider 를 만들기 위해 몸풀기 과정이었다면, 이번 시간에는 이것을 응용한 LINQ To Naver Open API 를 만들어 볼 것입니다. 아직 Custom LINQ Provider 에 대한 품새가 잘 그려지지 않는다면, 이번 예제를 통해 왜 Custom LINQ Provider 가 유용한 지에 대해 잘 알 수 있을 거라고 생각합니다.
 




 
LINQ To Naver Open API

우선 이 LINQ To Naver Open API 를 만들면서 몇 가지 고민을 하게 되었습니다.
 
1.      Naver Open API 는 조건 검색을 지원하지 않는다.
2.      query 파라메터를 통해 where 절의 조건을 넣게 되면, 수많은 쿼리 parsing 이 필요하다. 그렇기 때문에 구현하기 위해 좀 더 많은 시간이 필요하다.
3.     query 를 검색 한 후에, 검색 결과에 대해 where 조건을 만족시킨다. 현재 이 방법으로 구현되었으며, 기본적인 Lambda 식을 이용하기 때문에 구현하기 쉽다
 
.현재 3번의 방법으로 구현이 되었습니다. 그럼 어떤 모습으로 구현되었는지 한번 볼까요?
 
 
어떤가요?? 굉장히 편해하지 않습니까? 다음과 같이 블로그 검색도 지원합니다.
 
 
아래는 비디오 검색을 사용하고, 그 결과를 보여줍니다.
 
 
예제로 보여드리는 이 LINQ To Naver Open API 는 다음과 같은 검색을 지원합니다.
 
1.      NaverOpenAPIModel.Blog – 블로그 검색
2.      NaverOpenAPIModel.Kin – 지식in 검색
3.      NaverOpenAPIModel.WebKR – 웹 문서 검색
4.      NaverOpenAPIModel.Cafe – 카페 검색
5.      NaverOpenAPIModel.Shop – 쇼핑 검색
6.      NaverOpenAPIModel.Encyc – 백과사전 검색
7.      NaverOpenAPIModel.Krdic – 국어사전 검색
8.      NaverOpenAPIModel.Jpdic – 일어사전 검색
9.      NaverOpenAPIModel.Endic – 영어사전 검색
10.   NaverOpenAPIModel.News – 뉴스 검색
11.   NaverOpenAPIModel.Local – 지역검색
12.   NaverOpenAPIModel.Video – 동영상 검색
13.   NaverOpenAPIModel.Image – 이미지 검색
 
이중 책 검색과 실시간 검색어는 Request XML 스키마가 조금 달라서, 귀찮아서 빼버렸습니다 ㅡ,.ㅡ;
 
Context 클래스를 제네릭 클래스로 변경하면서 쉽게 LINQ 쿼리를 만들 수 있습니다.
 
 
쓸만한 LINQ To Naver Open API 가 되지 않았나 생각합니다.
이번 포스팅은 코드가 한 줄도 없는 포스팅이 되었네요. 코드는 첨부파일을 참고하시면 될 것 같고, 부수적인 내용은 1,2,3,4 회차 포스팅을 참고 하시면, 특별한 설명이 없어도 될 것 같습니다.
 
 
연재를 마치며…
 
처음에 외국 문서를 그냥 번역 또는 각색해서 올릴까 라고 생각했었지만, 제대로 된 Custom LINQ Provider 를 직접 손대기에는 난이도가 높았습니다. 외국 문서에 비해 비록 디테일이 떨어지지만, LINQ 의 내부를 훓어 보기엔 충분하다고 생각이 듭니다.
 
첨부된 LINQ To Naver Open API 소스코드를 참고하시면, 실제 웹 서비스에서 적용해도 될 만큼 다양한 LINQ Provider 가 나올 수 있길 기대해 봅니다. ( 다양한 테스트는 하지 못해서, 버그가 있을 수도 있습니다 )
 
그 동안 읽어주신 분들께 감사드립니다.
Posted by 땡초 POWERUMC

댓글을 달아 주세요




 
이번에는 쿼리를 이용하여 원격 개체 탐색을 하는 방법에 대해서 알아보겠습니다. 이 파트는 마치 LINQ To SQL 과 비슷하긴 하지만, 원격 개체라는 것의 대상을 SQL 서버에만 두는 것이 아니라는 것을 명심하셔야 합니다. 이번 예제는 SQL 서버를 이용하여 쿼리를 탐색하는 것이지만, 이 다음 파트인 LINQ To Naver Open API 를 보시면, 다양한 원격 개체에 접근 할 수 있다는 것을 알 수 있을 것입니다.

 
 
SampleContext 에 쿼리 Log 프로퍼티 추가
                                                  
SampleContext 의 소스는 2회차의 소스와 똑같습니다. 다만, 원격 탐색을 하기 위해 질의식을 어떻게 만들었는지 알 수 있도록 Log 프로퍼티를 추가합니다. 원격 서버에 원하는 데이터를 가져올 수 있도록, 질의를 해야 하는데, 그것이 SQL 서버면, SQL 쿼리식이 될 것이고, 또는 WMI 통한다라고 하면, WMI 쿼리식이 될 것입니다.
 
public class SampleContext : IQueryable<Person>
{
    // 생략
 
       public string Log
       {
             get { return this.provider.sbLog.ToString(); }
       }
}
 
 
SampleProvider 의 프로퍼티 추가와 Visitor 클래스 만들기
 
아래의 StringBuilder 는 쿼리식을 만들기 위한 객체입니다.
 
public class SampleProvider : IQueryProvider
{
    // 생략
 
       public StringBuilder sbLog = new StringBuilder();
}
 
그리고 IProvidor 의 Execute<T> 메서드의 내용을 변경하고자 합니다. 우선 테스트용으로 이렇게 작성하였고, 실제 원격 개체에 연결하기 위해 이후에 다시 이 메서드의 코드는 변경할 예정입니다.
 
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());
 
       TranslateExpression trans = new TranslateExpression();
       sbLog.Append( trans.Translate(expression) );
 
       return (TResult)r.GetEnumerator();
}
 
이전 소스와 비교해 보았을 때, 약간 틀린 점이 있습니다.
 
TranslateExpression trans = new TranslateExpression();
sbLog.Append( trans.Translate(expression) );
 
바로 이 부분인데, TranslateExpression 클래스는 C# 3.0 의 쿼리식을 실제 원격 서버에서 질의 할 수 있는 쿼리식으로 변경하는 클래스입니다. Translate() 메서드를 통해 LINQ 식을 텍스트로 변환하는 것입니다.
 
Visitor 패턴이란?
패턴을 구분할 때 Visitor 패턴은 행위 패턴에 속합니다. 간접적으로 클래스에 다형적인 기능을 추가합니다. 즉, 새로운 클래스를 많이 추가하기를 원치 안을 경우 선택하면 좋은 대안이 될 수 있습니다.
 
 
TranslateExpression 클래스
 
소스코드를 전체로 보면 좋겠지만, LINQ 의 내부를 살펴보는 기초적인 포스팅이니, 메서드별로 구분하여 설명 드리고자 합니다.
 
TranslateExpression 클래스의 맴버와 생성자는 다음과 같습니다. StringBuilder 의 sb 맴버는 하나하나의 식을 분석하여 쿼리를 만들 것입니다. 그리고 생성자의 expression 은 LINQ 의 표현식이겠죠?
 
생성자
 
public class TranslateExpression
{
       private StringBuilder sb   = new StringBuilder();
 
       public string Translate(Expression expression)
       {
             this.Visit(expression);
 
             return sb.ToString();
       }
}
 
Visit 메서드
 
Visit 메서드는 이 클래스에서 상당히 중요한 부분입니다. 위에 말씀드린 Visitor 패턴을 구현하고 있지요. Visitor 패턴은 다른 패턴과 유사한 점이 많기 때문에, 이것은 Interpreter 패턴처럼 보일 수도 있고, 확장한다면 Composite 패턴과도 같아 보일 수 있습니다. 하지만 패턴은 코드의 내용 보다는 관점을 어떻게 보느냐가 더 중요합니다.
 
Expression 클래스는 ExpressionType 의 열거형 맴버를 가지고 있습니다. 현재의 Expression 이 어떤 표현식을 가지고 있는지 명확히 나타내고 있습니다. 적당히 쿼리가 만들어 질 수 있는 정도만을 구현하였기 때문에 JOIN 이나 GROUPING 기능은 수행할 수 없답니다.
 
protected Expression Visit(Expression expression)
{
       switch (expression.NodeType)
       {
             case ExpressionType.Call:
                    return this.VisitCall((MethodCallExpression)expression);
 
             case ExpressionType.Constant:
                    return this.VisitConstant((ConstantExpression)expression);
 
             case ExpressionType.Lambda:
                    return this.VisitLambda((LambdaExpression)expression);
 
             case ExpressionType.MemberAccess:
                    return this.VisitMember((MemberExpression)expression);
 
             default:
                    throw new Exception( string.Format("{0} 지원하지않습니다", expression.NodeType));
       }
}
 
VisitCall 메서드
 
이 메서드는 MethodCallExpression 표현식을 분석합니다. LINQ 식의 모든 C# 메서드는 이것의 대상이 되는 것입니다.
 
MethodCallExpression 의 메서드는 하나 이상의 상수나 변수를 포함하거나, 리턴 타입이 있어야 합니다. 즉, void 형의 C# 메서드는 LINQ 식에 포함이 될 수 없습니다.
 
 
protected virtual Expression VisitCall(MethodCallExpression mce)
{
       switch (mce.Method.Name)
       {
             case "Where":
                    sb.Append("SELECT * FROM").Append( Environment.NewLine );
                    this.Visit(mce.Arguments[0]);
                    sb.Append(" AS T WHERE ");
 
                    UnaryExpression ue         = mce.Arguments[1] as UnaryExpression;
                    LambdaExpression le        = ue.Operand as LambdaExpression;
 
 
                    BinaryExpression be        = le.Body as BinaryExpression;
            if (be != null)
                this.VisitBinary(be);
                           break;
 
             case "StartsWith":
                    this.Visit(mce.Object);
                    sb.AppendFormat(" LIKE '{0}%'", mce.Arguments[0].ToString().Replace("\"",""));
                    break;
       }
 
       return mce;
}
 
mce.Arguments[1]
 
를 디버깅 해보면,
 
{r => (((r.Age >= 20) && (r.Age <= 30)) && r.Name.StartsWith("엄"))}
 
위와 같이 마치 람다식과 같이 생겼습니다. 하지만, 이것의 NodeType 은 Lambda 가 아닌 Quote 입니다. Quote 는 상수값이 포함된 표현식입니다. 이것의 피연산자를 람다표현으로 바꾸는 구문이
 
UnaryExpression ue         = mce.Arguments[1] as UnaryExpression;
LambdaExpression le        = ue.Operand as LambdaExpression;
 
이렇게 되고, BinaryExpression 으로 다시 표현이 가능할 경우, BinaryVisit 을 수행하게 됩니다.
 
그리고,
 
case "StartsWith":
       this.Visit(mce.Object);
       sb.AppendFormat(" LIKE '{0}%'", mce.Arguments[0].ToString().Replace("\"",""));
       break;
 
위 코드는 StartsWith 의 메서드를 SQL 쿼리식과 같이 LIKE ‘xxx%’ 처럼 바꾸는 역할을 하게 됩니다.
 
VisitLambda 메서드
 
VisitLambda 메서드는 LambdaExpression 을 분석합니다. LambdaExpression 은 Body 속성이 있으며, 이 Body 는 여러가지의 NodeType 이 올 수 있습니다. 현재 소스에서는 특별한 기능을 하지 않습니다.
 
protected virtual Expression VisitLambda(LambdaExpression le)
{
       return le;
}
 
 
VisitConstant 메서드
 
이 메서드는 ConstantExpression 을 분석합니다. 신기하게도 잘 살펴보면 이 ConstantExpression.Value 는 SampleContext 를 참조하고 있습니다. 이놈은 테이블을 참조 하고 있지만, Constant 로 가장하고 있습니다. 이 ConstantExpression 의 ElementType 을 가져와서 매핑되는 테이블로 변환해 줍니다.
 
protected virtual Expression VisitConstant(ConstantExpression ce)
{
       IQueryable q = ce.Value as IQueryable;
       if (q is IQueryable)
       {
             sb.AppendFormat(" ( SELECT * FROM {0} ) ", q.ElementType.Name)
                    .Append( Environment.NewLine );
       }
       else
       {
             sb.AppendFormat(" {0} ", ce.Value);
       }
 
       return ce;
}
 
그래서 ConstantExpression 은 IQueryable 로 가장하고 있지 않을 경우, 일반적인 상수값으로 취급할 수 있습니다.
 
 
VisitMember 메서드
 
이 메서드는 MemberExpression 의 표현을 분석합니다. LINQ 쿼리식의 맴버는 모두 여기에 해당됩니다.
 
protected virtual Expression VisitMember(MemberExpression me)
{
       sb.Append(me.Member.Name);
 
       return me;
}
 
예를 들어,
 
var result = from r in context
                     where r.Age >= 20 && r.Age <= 30 && r.Name.StartsWith("")
                     select r;
 
와 같은 식의 MethodCallExpression 은 Where 절이 될 것이고, 이곳의 람다 표현식으로 각각의 연산식을 분석해 보면,
 
각각의 BinaryExpression 은
 
r.Age >= 20
r.Age <= 30
r.Name.StartsWith("")
 
이 됩니다. 이 BinaryExpression.Left 는 r.Age 로 표현이 되지만, 우리가 변환할 쿼리식에서 “r.Age” 의 “r.” 은 필요가 없습니다. 때문에, MemberExpression 의 Member.Name 을 통해 “Age” 와 같이 오직 맴버 이름만을 표현하도록 하고 있습니다.
 
VisitBinary 메서드 ( Update 2008/03/27 - 오타 수정 )
 
이 메서드는 BinaryExpression 의 표현을 분석합니다. BinaryExpression 의 Left/Right 를 구현하고 있으며, 이 두 피연산자를 쪼개어내어 결합하는 기능을 하고 있습니다.
 
중요한 것은 BinaryExpressoin 의 Left/Right 는 그 안에 또 다른 BinaryExpression 을 포함할 수 있습니다.
 
var result = from r in context
                     where r.Age >= 20 && r.Age <= 30 && r.Name.StartsWith("")
                     select r;
 
와 같은 식의 BinaryExpression 은
 
be.Left = {((r.Age >= 20) && (r.Age <= 30))}
be.Right = {r.Name.StartsWith("엄")}
 
가 될 수 있습니다. 때문에, 위의 각각의 Left/Right 대한 VisitBinary 를 수행해야 합니다. 즉,재귀호출과도 같죠.
 
protected virtual Expression VisitBinary(BinaryExpression be)
{
       if (be.Left is BinaryExpression)
             this.VisitBinary((BinaryExpression)be.Left);
       else
       {
             this.Visit(be.Left);
       }
 
    switch (be.NodeType)
    {
        case ExpressionType.GreaterThan:
            sb.Append(" > ");
                    break;
 
        case ExpressionType.GreaterThanOrEqual:
                    sb.Append(" >= ");
                    break;
 
             case ExpressionType.LessThan :
                    sb.Append(" < ");
                    break;
 
             case ExpressionType.LessThanOrEqual:
                    sb.Append(" <= ");
                    break;
 
        case ExpressionType.Equal:
            sb.Append(" = ");
                    break;
 
             case ExpressionType.And:
             case ExpressionType.AndAlso:
                    sb.Append(" AND " );
                    break;
 
             case ExpressionType.Or:
                    sb.Append(" OR ");
                    break;
 
             default:
                    throw new Exception( string.Format("{0} 형식은지원하지않습니다", be.NodeType) );
    }
 
       if (be.Right is BinaryExpression)
             this.VisitBinary((BinaryExpression)be.Right);
       else
       {
             this.Visit(be.Right);
       }
 
    return be;
}
 
BinaryExpression.NodeType 은 기본적인 덧셈(+),뺄셈(-) 외에도 &&, ||, 또는 비트연상 등의 연산도 포함될 수 있습니다. 특히, &&, || 와 같은 조건식을 AND,OR 로 변환해 주어야 할 필요가 있습니다.
 
 
한번 프로그램을 실행해 볼까요?
 
다음과 같은 코드입니다.
 
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 && r.Age <= 30 && r.Name.StartsWith("")
                      select r;
 
             result.ToList().ForEach( o=>Console.WriteLine(o.Name ));
 
             Console.WriteLine("----------------------------");
             Console.WriteLine( context.Log );
       }
 
결과는 다음과 같습니다.
 
엄준일
엄호희(내동생)
----------------------------
SELECT * FROM
 ( SELECT * FROM Person ) AS T
 WHERE Age >= 20 AND Age <= 30 AND Name LIKE '엄%'
계속하려면 아무 키나 누르십시오 . . .
 
어떻습니까? 제법 쓸만한 QueryProvider 가 되었지요?
 
 
원격 서버에 연결
 
우리는 원격 서버를 MS-SQL 서버로 실습을 할 것입니다. 실습을 위해 테이블을 만들고, 테스트 데이터를 만들도록 하겠습니다.
 
Person 클래스와 같은 스키마와 실습 데이터를 넣었습니다.
 
CREATE TABLE Person
(
[Name] VARCHAR(50) NOT NULL,
[Age] INT NOT NULL
)
 
INSERT INTO Person(Name,Age) VALUES ('엄준일',29)
INSERT INTO Person(Name,Age) VALUES ('엄호희(내동생)',26)
INSERT INTO Person(Name,Age) VALUES ('엄혜진(울누나)',31)
INSERT INTO Person(Name,Age) VALUES ('멍멍이',6)
INSERT INTO Person(Name,Age) VALUES ('발발이',5)
 
그리고 SampleContext 의 GetEnumerator<Person> 메서드를 다음과 같이 수정하였습니다.
 
public IEnumerator<Person> GetEnumerator()
{
       provider.Execute<IEnumerator<Person>>(this.expression);
 
       SqlConnection cn           = new SqlConnection("Server=xxxx.kr;DataBase=xxxx;UID=xxxx;PWD=xxxx");
       SqlCommand cm              = new SqlCommand( Log, cn);
       cm.CommandType                   = System.Data.CommandType.Text;
 
       cn.Open();
       Console.WriteLine("DataBase Connection!");
 
       SqlDataReader reader       = cm.ExecuteReader();
                   
       List<Person> list          = new List<Person>();
       while (reader.Read())
       {
             list.Add( new Person {
                    Name   = reader["Name"].ToString(),
                    Age    = Convert.ToInt32(reader["Age"])
             });
       }
 
       reader.Close();
       cn.Close();
 
       return list.GetEnumerator();
}
 
위와 같이 실제 데이터베이스를 연결하여 쿼리를 수행하도록 하였습니다.
 
결과는 다음과 같습니다.
 
DataBase Connection!
엄준일
엄호희(내동생)
----------------------------
SELECT * FROM ( SELECT * FROM Person ) AS T WHERE Age >= 20 AND Age <= 30 A
ND Name LIKE '%'
계속하려면아무키나누르십시오 . . .
 
이 과정에서 변경된 소스는 특별히 자세한 설명이 필요 없을 것 같아서 첨부된 소스코드를 참고 하시기 바랍니다.
 

Posted by 땡초 POWERUMC

댓글을 달아 주세요




 
지난 시간에 이어, 이번 시간에는 실제 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 통해 원격 개체를 탐색하는 방법에 대해 살펴 보도록하고, 이만 마치겠습니다. 이번 한주도 즐겁게 시작하시기 바랍니다^^//

Posted by 땡초 POWERUMC

댓글을 달아 주세요




 

이전 시간에 Custom LINQ Provider 에 대해서 살며시 알아보았습니다. 이것을 다시 한번 요약하자면, .NET Framework 이 제공하는 공급자가 아닌, 개발자에 의해 통합 쿼리식을 이용하여 전혀 다른 데이터를 질의 하기 위해 사용합니다.
 
 
Entity Class 만들기
 
우리는 이 단원에서 간단한 Entity Class 를 이용할 것입니다. List<> 클래스에 간단한 데이터를 담을 수 있도록 선언된 클래스입니다.
 
public class Person
{
       public string Name { get; set; }
       public int Age      { get; set; }
}
 
 
Context 객체 만들기
 
우선 여기서 IQueryable<> 인터페이스를 살펴보겠습니다. MSDN 이 말하길 이 인터페이스는 “데이터 형식이 알려진 특정 데이터 소스에 대한 쿼리를 실행하는 기능을 제공합니다.” 라고 합니다. 물론, 제네릭 형식의 인터페이스므로 “데이터 형식이 알려진…” 이라고 하였으나, IQueryable 과 같은 비 제네릭 인터페이스는 제네릭이 아닌 IEnumerable 을 구현해야 합니다. 마찬가지로 IQueryable<> 인터페이스는 IEnumerable<> 을 구현해야 합니다.
 
이 Context 객체는 IQueryable<> 인터페이스를 구현함으로써, 쿼리식을 이용할 DataSource 에 대해 독자적인 Provider 로 식을 연산할 수 있습니다.
 
public class SampleContext : IQueryable<Person>
{
       internal Expression expression;
       private SampleProvider provider;
       public List<Person> DataSource { get; set; }
      
       #region IEnumerable<Person> Members
 
       public IEnumerator<Person> GetEnumerator()
       {
             return provider.Execute<IEnumerator<Person>>(this.expression);
       }
 
       #endregion
 
       #region IEnumerable Members
 
       System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
       {
             return (this as IEnumerable<Person>).GetEnumerator();
       }
 
       #endregion
 
       #region IQueryable Members
 
       public Type ElementType
       {
             get { return typeof(Person); }
       }                         
 
       public Expression Expression
       {
             get { return Expression.Constant(this); }
       }
 
       public IQueryProvider Provider
       {
             get
             {
                    if( provider == null )
                           provider = new SampleProvider(this);
 
                    return provider;
             }
       }
 
       #endregion
 
}
 
아까 잠시 설명드린 IQueryable<> 인터페이스가 제공하는 프로퍼티와 메서드는 모두 중요한 역할을 하고 있습니다.
 
Provider Property
우리는 DataSource 라는 속성에 특정 데이터를 넣게 되고, 이 데이터의 쿼리식을 연산할 Provider 의 객체를 리턴해야 합니다. 하나의 인스턴스에 하나의 객체만을 생성하기 위해서 null 체크를 하고, SampleContext 를 매개변수로 생성된 Provider 를 리턴합니다. 아직 구현되지 않았고, 잠시 후에 살펴보겠습니다.
 
Expression Property
Expression 속성은 Expression.Constant 라는 메서드를 사용합니다. 쿼리식을 표현하기 위해 Lambda Expression 과 같은 표현을 사용하게 되며, 이 Expression 은 다양하게 구문을 해석하기 위한 클래스를 제공해 줍니다.
 
기본적인 Expression 은 표현식을 처리할 수 있습니다.
 
Expression<Func<string,string,string>> ff = (s1,s2) => (s1 + s2);
Console.WriteLine( ff.Body );
 
위의결과는표현식의 Body(몸통) 부분을가져오는예입니다.
 
(s1 + s2)
 
신기하지 않나요?? ^^
 
가령, BinaryExpression 은 이항 연산과 관련된 식을 표현합니다. ( xxxExpression 은 Expression 을 상속 받습니다 ) +, *, EQ, AND 등이 여기에 속합니다.
 
Expression<Func<int,int,int>> f = (v1,v2) => v1 + v2;
BinaryExpression be = (BinaryExpression)f.Body;
Console.WriteLine( "{0} {1} {2}", be.Left, be.Right, be.NodeType );
 
위의결과는
 
v1 v2 Add
 
의 결과가 나옵니다.
 
그럼, ConstantExpression 의 예를 보도록 하죠. 이 Expression 은 상수 값이 있는 표현식을 처리할 수 있습니다.
 
Expression<Func<int, bool>> f = (v1) => ( v1 == 5 );
BinaryExpression be = (BinaryExpression)f.Body;
ConstantExpression ce = (ConstantExpression)be.Right;
Console.WriteLine( ce.Value );
 
결과는
 
5
 
가 됩니다.
 
그럼 문제를 하나 내 보도록 하겠습니다. 이후에 이 예제에서 나오게 될 LINQ 식입니다. 이 LINQ 식에서 ConstantExpression 은 어디에 해당할까요?
 
var result = from r in p
              where r.Age > 20
              select r;
 
설마 잘 모르시겠다고요?? ConstantExpression 은 상수의 값이 있는 식을 가져온다고 하였습니다. 저 쿼리식 중에 상수의 값이 어디 있는지 찾아보시면 답은 나올 겁니다.
 
ElementType Property
이 속성은 반복되는 식의 요소, 즉, 위에서 우리가 만들 Person 클래스는 List<Person> 객체에 담을 것이기 때문에, Person 클래스의 타입을 리턴하시면 됩니다.
 
GetEnumerator Methods
이 메서드도 굉장히 중요한 부분입니다. 잠시 예를 들어 보죠. 보통 DataBase 에서 SELECT 문을 이용하여 데이터를 조회할 때, 또는 프로시져 내의 SELECT 구문을 만나면 당장 이런저런 처리를 하여 결과를 뱉어냅니다.
 
그럼 여기서 또 하나의 문제를 내보겠습니다. 다음의 서로 다른 환경의 쿼리식을 10000번 반복했을 때, 수행시간은 누가 더 빠를까요?
1. 실제 Database 쿼리를 10000 번 수행
2. LINQ To SQL 쿼리식을 10000 번 수행
SELECT *
FROM p r
WHERE r.Age > 20
var result = from r in p
          where r.Age > 20
            select r;
 
언듯 생각하기에는, 둘의 결과는 비슷하리라 생각이 들기도 합니다. 2번은 Database Connection 이 맺어질 것이고, 더 느리면 느렸지, 빠르진 않다고 생각할 수 도 있겠죠. 좀 애매하네요 ^^;
 
하지만, 잠시 언급한 “비슷하다, 2번이 더 느릴것이다”라는 예상은 완전히 빗나갑니다.
 
1번의 경우 무지하게 오래 걸립니다. 왜냐하면, 질의에 대해 바로 바로 결과를 반환합니다.
2번의 경우 질의 결과를 반환하지 않습니다. 그렇기 때문에 굉장히 빠르게 10000번의 쿼리를 실행합니다.
 
 
LINQ 의 쿼리식은 다릅니다. 왜 2번이 답이 되는지 봅시다.
 
var result = from r in p
              where r.Age > 20
              select r;
 
의 쿼리식이 있을 경우, 예상과 다르게 런타임이 저 구문을 지날 때 result 는 어떠한 데이터의 질의 결과를 가지고 있지 않습니다. 즉, 질의에 대한 결과값은 전혀 없는, 그냥, SampleContext 객체일 뿐입니다. 런타임이 위의 쿼리식을 만나게 되면, Provider 에게 표현식을 위임하게 되고, 식을 해석만 합니다. 이것이 Database 에 연결된다고 할지라도, Database 에 요청할 쿼리식(SQL) 만을 만들 뿐입니다.
 
바로, 이 GetEnumerator 메서드가 호출되는 시점에, 데이터를 가공하여 결과를 반환하게 됩니다. 마찬가지로, Database 에 연결되는 작업이라면 이 시점에서 실제로 Database Connection 이 열리게 되고, 쿼리하여 결과를 반환할 수 있게 되는 것입니다.
 
 
다음 회차에서 IQueryProvider 인터페이스를 구현해 보겠습니다.
Posted by 땡초 POWERUMC

댓글을 달아 주세요




 
 
LINQ 의 출연
 
정말 .NET 이라는 세상이 많이 좋아진 것 같습니다. 강력한 Visual Studio 2008 이란 개발 툴에 차세대 프레임웍인 .NET Framework 3.5 로 아직까지 그것의 모두를 채 맛보지 못했습니다. C# 3.0 의 여운이 가시기도 전에 Silverlight 2.0 Beta 의 출시로 다시 한번 RIA 업계를 강타하였고, 올해는 너무나도 할게 많은 한 해가 될 것 같습니다.
 
C# 3.0 의 특징은
l Lambda Expression ( 람다식 )
l Object Initialize ( 객체 초기화 )
l Extension Methods ( 확장 메서드 )
l Anonymous Type ( 익명 타입 )
l 등등…
 
바로, C# 3.0 의 LINQ 는 이러한 람다식이라든지, 확장 메서드 등을 이용한 종합 예술이라고 생각합니다. 때문에, 위의 C# 3.0 의 특징을 이해하지 못한다면, LINQ 를 효율적으로 사용하기에 한계가 있을 것입니다. 때문에, 위의 C# 3.0 의 새로운 기능에 대해서 아직 잘 모르시는 분은 의 C# 3.0 을 참고하십시오.

 
 
LINQ Provider
 
Provider 란 사전적 의미로 “공급자” 정도로 해석할 수 있습니다. LINQ 는 Language Integrated Query 로써, 언어 통합 쿼리를 뜻합니다. 그럼, LINQ Provider 는 쿼리식과 같은 언어적 구문을 해석할 수 있는 공급자라고 생각하시면 됩니다.
 
.NET 이 제공하는 LINQ Provider 는 다음과 같습니다.
l LINQ To Objects
l LINQ To Entities
l LINQ To SQL – MS SQL 만 지원함
l LINQ To XML
 
바로 LINQ(Language Integrated Query ) 는 이렇게 다양한 Provider 들을 서로 다른 쿼리식이 아닌, 통합된 쿼리식으로 C# 3.0 에서 사용할 수 있답니다.
특히, LINQ To SQL 은 Visual Studio 에 함께 통합되어, 이전에 Typed DataSet 과 같이 Drag&Drop 을 지원하며, 자동으로 Entity 클래스를 생성하고, 이것에 대한 Context 객체를 생성해 줌으로써 Typed DataSet 과 Entity 객체의 장점을 모두 갖추며, 더욱 강력한 언어적 기능을 제공해 줍니다.
 
하지만, 이것은 단지 시작에 불과합니다. 기본적으로 제공하는 LINQ Provider 는 몇 개 되지 않지만, 개발자가 직접 LINQ Provider 를 만듦으로써 모든 대상을 통합된 쿼리식을 사용할 수 있습니다. 이토록 경이로울 수가… +_+;
 
 
Custom LINQ Provider
 
이미 해외의 많은 개발자들이 Custom LINQ Provider 를 만들어 내놓았습니다.
대표적인 사례는… ( 님 로그 참조 )
 
 
정말 대단합니다. LINQ To SQL 에 이은 LINQ To Oracle 과 LINQ To WMI, LDAP, Active Directory 등등… 대부분이 Open Source 이기 때문에, 그냥 가져다 쓰기만 하면 될 정도입니다. 이런걸 보고 있자면, 정말 우리(?)보다 훨씬 앞서가고 있는 개발자들이 태반이네요 ㅋ.ㅋ;;
이미 .NET Framework 3.5 Beta 버젼부터 나온 소스들도 있기 때문에, 소스중 현재 정식 .NET Framework 3.5 와 Namespace 체계가 다른것도 있습니다. 조금만 수정해 주시면, 위의 소스들을 컴파일 하시는데 어렵지 않으실 겁니다.
 
하지만, 저도 뭐하나 해봐야 할 것 같긴한데… 다음시간에 Custom LINQ Provider 를 살짝 만들어 보겠습니다. 기대는 하지 말아 주세요~ ^^;
Posted by 땡초 POWERUMC

댓글을 달아 주세요