티스토리 뷰




 
이번에는 쿼리를 이용하여 원격 개체 탐색을 하는 방법에 대해서 알아보겠습니다. 이 파트는 마치 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 '%'
계속하려면아무키나누르십시오 . . .
 
이 과정에서 변경된 소스는 특별히 자세한 설명이 필요 없을 것 같아서 첨부된 소스코드를 참고 하시기 바랍니다.
 

댓글