상세 컨텐츠

본문 제목

LINQ to SQL을 사용하는 Application의 성능향상을 위한 10가지 제안

Web Development/Others

by thankee 2009. 9. 8. 22:45

본문

LINQ to SQL을 이용하면 Data Access작업에서 (과거에 비해) 놀라울 정도의 생산성 향상을 가져옵니다. 하지만, 그만큼 성늠이 우수한가에 대한 의문을 가질 수 밖에 없는 것은 사실입니다. 하지만 몇 가지 Benchmark들을 확인하면, LINQ to SQL을 제대로 사용하면, ADO.NET SQL DataReader의 93%의 성능까지 낼 수 있다고 합니다.

따라서 여기에서는 LINQ to SQL을 이용하여 데이터를 조회하고 수정하는데 있어서 성능을 향상시킬 수 있는 10가지 중요한 Tunning Point를 정리해드립니다.

  1. DataContext의 ObjectTrackingEnabled 속성이 필요하지 않다면, Off로 설정하십시요.
    만약 당신이 데이터를 조회만 하고, 데이터를 수정하지 않는다면 DataTracking 기능은 필요치 않습니다. DataTracking는 Server Memory에 올려진 LINQ데이터의 변화를 추적하여 DataContext.SubmitChage()되는 순간 그 변화를 반영하는 기능을 합니다. 만약 데이터를 조회만 한다면 아래와 같이 사용하십시요.
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
         db.ObjectTrackingEnabled = false;
    }
  2. 전체 DB 개체들을 하나의 DataContext에 포함하지 마십시요.
    DataContext는 하나의 작업 단위를 표현해야지, 전체 DB를 표현해서는 안됩니다. 만약 당신의 DB개체 중에서 다른 개체와 연결되지 않고 독립적으로 동작하거나, 전혀 사용되지 않는 개체가 있다면, 그 개체들은 다른 DataContext로 분리하십시요. 만약 하나의 DataContext에 포함되어 있다면, 이러한 개체들은 DataContext를 선언할 때 불필요하게 메모리를 차지하며, DataConText의 CUD엔진이 사용하는 관리, 추적, 식별 비용을 증가시킬 뿐입니다.
    대신, 각 작업단위로 DataContext를 분리하는 것을 고려하십시요. 물론, Connection Pooling의 장점을 잃지 않기위해, 생성자를 이용하여 같은 Connection을 사용하도록 설정할 수 있습니다.
  3. 어디서든지 필요하다면 CompiledQuery를 이용하십시요.
    LINQ to SQL을 이용하여 표현식을 만들고 실행하기 위해서는 몇가지 단계가 존재합니다. 몇 가지 나열한다면 다음과 같습니다.
    1. Expression Tree의 생성
    2. Expression Tree를 SQL로 변환
    3. 생성된 SQL Query 실행
    4. 데이터 조회
    5. 조회된 데이터를 객체로 변환
    만약 당신이 같은 LINQ to SQL Query를 반복해서 사용한다고 할 때, 위의 과정 역시 반복된다고 한다면 그것은 불필요한 자원 낭비가 아닐 수 없습니다. 이 것이 바로 작은 System.Data.Linq 네임스페이스가 많은 자원을 소모하는 주 이유가 될 것입니다(같은 작업을 불필요하게 반복하는 것). CompiledQuery는 표현식을 Compile한 다음 이 것을 어딘가에 저장해둡니다. 그리고 같은 작업이 호출 되면 저장된 것을 통해 불필요한 Compile작업이 반복되는 것을 피하게 해줍니다. 이 기능은 정적 CompiledQuery.Compile메서드에 의해 실현할 수 있습니다. 예제는 다음과 같습니다.
    /*
     아래 NameSpance 선언 필요
     using System.Collections.Generic;
     using System.Data.Linq;
    */
    Func<DataClassesDataContext, IEnumerable<People>> func =
    	CompiledQuery.Compile<DataClassesDataContext, IEnumerable<People>> ((DataClassesDataContext db) => db.Peoples.Where<People>(t => t.PeopleName == "손대관"));
    이제 "func"는 Compiled Query로서 첫 실행시 단 한번만 Compile되게 됩니다. 다음 코드는 Compiled Query를 생성해서 Static Utility Class에 저장하는 예제입니다.
    /*
     아래 NameSpance 선언 필요
     using System.Collections.Generic;
     using System.Data.Linq;
    */
    public static class QueriesUtility
    {
    	public static Func<DataClassesDataContext, IEnumerable<People>> GetActivePeoples 
     	{
    		get
    		{
    			Func<DataClassesDataContext,IEnumerable<People>> func = 
    				CompiledQuery.Compile<DataClassesDataContext, IEnumerable<People>> ((DataClassesDataContext db)
    					=> db.Peoples.Where<People>(t => t.ActiveCheck = false));
    			return func;
    		}
    	}
    }
    이제 우리는 언제든지 위의 Compiled Query를 호출해서 사용할 수 있습니다.
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
    	IEnumerable<People> p = QueriesUtility.GetActivePeoples(db);
    	lblTest.Text = p.Count().ToString();
    }
    이렇게 저장하고 실행하는 것은 반복적인 호출 비용을 단 한 1번의 호출 비용으로 감소시켜줍니다. 위와 같은 Compiled Query를 많이 만들어 둔다고 성능이 저하되지 않는 가 걱정하실 필요도 없습니다. 모든 Compiled Query는 처음 실행 될 때 단 한번만 Compile됩니다.
  4. DataLoadOptions.AssociateWith를 사용하여 필요한 데이터만 조회하세요.
    우리는 Primary Key로 연결된 관련된 데이터들을 한번에 읽어들여야 할 때, Load 또는 LoadWith를 사용합니다. 하지만 대부분의 경우 추가적인 필터링을 하게 되는데요, 이 경우 DataLoadOption.AssociateWith라는 Geniric Method는 매우 유용합니다. 이 뿐만 아니라 데이터를 조회할 때 필요한 데이터만 조회하는 것은 성능향상을 위한 첫걸음 입니다.
  5. 필요하지 않다면 Optimistic Concurrency을 끄세요.
  6. DataContext에 의해서 생성되는 데이터 조회 Query를 지속적으로 확인하세요.
    데이터를 조회할 때, DataContext는 Query를 자동으로 생성하는데요, 실제로 사용되지 않는 불필요한 코드가 생성될 수 있습니다. 성능개선을 위해서는 생성되는 Query를 지속적으로 감시하고, 불필요한 코드가 생성되지 않도록 LINQ to SQL 표현식을 수정해야 할 것입니다. 우리는 DataContext의 Log 속성을 이용해서 생성된느 SQL문을 볼 수 있습니다.
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
    	System.IO.StreamWriter httpResponseStreamWriter = 
    		new StreamWriter(HttpContext.Current.Response.OutputStream);
    	db.Log = httpResponseStreamWriter;
    }
    위 코드는 생성된느 SQL문을 화면에 출력하는 코드입니다. Response개체 대신에 대상을 파일로 지정하는 것도 좋은 방법입니다.

    다른 방법은 'LINQ to SQL Debug Visualizer'를 사용하는 방법입니다. Debug 모드에서 손쉽게 생성된 SQL을 확인하고 실행할 수 있습니다. 'LINQ to SQL Debug Visualizer'에 대한 정보와 설치는 여기를 참조하세요.

  7. Attach()가 불필요한 Entity를 Context에 Attach하지 않도록 하세요
    조회된 Entity는 보통 DataContext에 연결되어 있으며, 내부적인 데이터 변화가 하나하나 추적되는 상태입니다. 이 상태에서는 Entity의 Update, Delete가 매우 쉽습니다. 하지만 Entity를 Serialize한 경우 처럼 DataContext와 Entity의 연결이 끊어진 상태도 존재합니다. 이럴 때 Attach를 통해 DataContext와 Entity를 연결하게 되는데, Entity를 Update 또는 Delete 할 경우에 Attach를 하는 것은 크게 문제가 되지 않습니다. 하지만 Update나 Delete하지 않음에도 Attach를 하는 경우가 있습니다. 이런 실수가 가장 많이 발생하는 경우가 바로 AttachAll()메서드로 Entity Collection을 DataContext로 연결하는 경우입니다.
    Object Tracking는 DataContext에 연결된 모든 개체의 Update, Delete를 추정하고 반영해주는 매우 멋진 기능이긴 하지만, 그만큼 많은 자원을 소모합니다. 따라서 위와 같이 Update, Delete가 되지 않는 Entity를 Attach하여 불필요하게 Entity가 추적되도록 하는 일은 최대한 피해야합니다.
  8. 불필요하게 Object Tracking가 작동하지 않는지 확인하세요
    다음과 같은 두 코드가 있을 때, 어느 쪽이 성능이 더 빠른지 생각해보라.
    예1)
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
    	IQueryable<People> peopleList = db.Peoples
    		.Select(t => t);
    }
    
  9. 예2)
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
    	var peopleList = db.Peoples
    		.Select(t => new
    		{
    			activeCheck = t.ActiveCheck,
    			deleteCheck = t.DeleteCheck
    		});
    }
    두 번째 표현식은 필요한 데이터를 걸러서 조회하고 있으며, 데이터 타입도 기존의 개체를 사용하는 것이 아닌 Var 타입을 사용하고 있기에, 첫번째 표현식이 더 빠를 것이라 생각할 수 있습니다. 하지만 실제로는 두번째 표현식이 더 자원을 적게 소모합니다. 첫 번째 표현식은 아직까지 Update, Delete 기능이 모두 지원하는 상태로 조회가 되므로 자원을 많이 소모합니다. 즉 첫번째 표현식은 Object Tracking가 동작하는 상태이며, 따라서 내부적으로는 데이터의 변화가 모두 일일이 추적되고 있는 상태입니다. 하지만 두번째 표현식 처럼 필요한 데이터를 걸러서 조회를 하게 되면, 조회된 List는 ReadOnly로 변하게 되며, 자연스럽게 보다 빠르게 동작합니다.
  10. 필요한 행만 걸러서 조회하세요.
    ListView에 데이터를 바인딩하고 Paging기능을 사용한다고 할 때, 우리는 Take와 Skip를 통해 필요한 페이지의 행만 조회할 수 있습니다. Skip, Take를 사용하지 않고 조회된 LINQ to SQL 결과를 바로 바인딩할 수 있지만, 그럴 경우 DB의 모든 데이터를 불러오기 때문에 행의 수에 비래하여 ListView의 속도도 느려집니다.
    아래는 해당페이지에 필요한 데이터만 조회하는 구문입니다.
    int intPageSize = 10;
    int intPageNum = 2;
    using (DataClassesDataContext context = new DataClassesDataContext())
    {
    	listView.DataSource = context.Peoples.Skip(intPageNum * intPageSize).Take(intPageSize);
    	listView.DataBind();
    }
    
  11. CompiledQuery를 오용하지 마세요.
    '어떻게 COmpiledQuery를 오용할 수 있는거지?'라는 생각을 하고 계신건 아닌지 모르겠네요. 여기서 하고자하는 말은 불필요한 최적화는 없어야 한다는 것입니다. CompiledQuery는 최소한 한번 이상 사용될 때에만 사용해야합니다.
    일반적인 LINQ to SQL 문장이 CompiledQuery 문장보다 더 빠릅니다. 왜냐하하면 CompiledQuery는 SQL Expression을 유지하고, 재사용하는 등의 모든 고려사항을 반영하기 위한 특별한 처리를 내부적으로 담고있기 때문입니다. 따라서 단순히 조회를 하는 일반적인 LINQ to SQL Expression 보다 무거울 수 밖에 없습니다.


관련글 더보기