[C#] LINQ(Language INtegrated Query) - 3. comprehension syntax과 thread를 통한 응답성 향상, XML 다루기
3. LINQ query comprehension syntax
C# 3.0 때 마이크로소프트는 SQL과 같은 쿼리 구문을 LINQ 쿼리문으로 작성하는 방법을 토대로 개발자들이 더 익숙하게 LINQ구문을 작성하도록 하는 몇 가지 키워드를 소개했습니다.
예를 들어 다음과 같이 특정 Item을 필터링하는 메서드를 사용한 경우
using (Northwind db = new())
{
var result = db.Products.Where(x => x.ProductId == 3).SingleOrDefault();
Console.WriteLine(result.ProductName);
}
query comprehension 구문을 통해 위와 같은 동작을 수행할 수 있습니다.
using (Northwind db = new())
{
var query = from Product in db.Products
where Product.ProductId == 3
select Product.ProductName;
var result = query.SingleOrDefault();
Console.WriteLine(result);
}
사실 위와 같은 구문자체가 직접적으로 동작하는 것은 아니고 컴파일러가 위와 같은 구문을 동일한 확장 메서드와 람다식으로 다시 변환하여 실행하게 됩니다.
예제에서 select 키워드는 query comprehension 구문에서는 필수적이지만 LINQ 확장메서드에 Select() 메서드는 선택적으로 사용되며 Select() 메서드를 사용하지 않게 되면 모든 Item을 선택한 것이라 가정합니다.
물론 모든 LINQ 확장메서드가 그에 대응되는 키워드를 가지고 있지는 않은데 예를 들어 많은 데이터를 나열하는 경우 일반적으로 페이징을 구현하는 데 사용되는 Skip이나 Take 같은 확장 메서드의 경우가 그렇습니다. 다행스럽게도 이러한 메서드는 uery comprehension 구문과 결합하여 사용할 수 있습니다.
using (Northwind db = new())
{
var query = (from Product in db.Products where Product.ProductId <= 20 select Product).Take(10); //10건만 가져오기
foreach(var item in query)
{
Console.WriteLine(item.ProductName);
}
}
4. 병렬 LINQ를 사용한 multiple thread 구현
기본적으로 하나의 thread가 하나의 LINQ를 실행하지만 PLINQ(Parallel LINQ)를 통해 비교적 쉬운 방법으로 LINQ를 실행하는데 multiple thread를 사용할 수 있습니다.
Stopwatch watch = new();
watch.Start();
int max = 25;
IEnumerable<int> numbers = Enumerable.Range(start: 1, count: max);
int[] fibonacciNumbers = numbers.Select(number => Fibonacci(number)).ToArray();
watch.Stop();
Console.WriteLine($"{watch.ElapsedMilliseconds:#,##0} 경과시간(millisecond)");
foreach (int number in fibonacciNumbers)
{
Console.Write($" {number}");
}
static int Fibonacci(int term) =>
term switch
{
1 => 0,
2 => 1,
_ => Fibonacci(term - 1) + Fibonacci(term - 2)
};
위 예제는 single thread를 사용해 45수의 피보나치수열을 계산하는 예제입니다. intel 8세대 i7(6core 16 thread)에서 예제의 실행결과 계산을 모두 마치는데 대략 20초정도가 걸렸습니다.
20,477 경과시간(millisecond)
01123581321345589144233377610987159725844181676510946177112865746368750251213931964183178115142298320401346269217830935245785702887922746514930352241578173908816963245986102334155165580141267914296433 494437701408733 |
single thread의 경우에는 CPU의 모든 Core와 thread자원을 균등하게 사용하지 않고 단 하나의 thread만 집중적으로 사용하게 됩니다. 또한 화면에는 표시되지 않았지만 CPU 사용율도 대략 15% 정도를 유지했습니다.
이러한 LINQ의 실행을 병렬로 수행하려면 그저 AsParallel() 메서드를 호출하는 것으로 끝낼 수 있습니다.
int[] fibonacciNumbers = numbers.AsParallel().Select(number => Fibonacci(number)).ToArray();
주의할점은 절대 AsParallel() 메서드를 query의 끝에서 호출해서는 안된다는 것입니다. 제대로 된 multiple thread를 구현하려면 AsParallel() 메서드를 호출한 이후 최소 하나 이상의 연산을 수행해야 합니다. 그렇지 않으면 아무런 효과도 기대할 수 없습니다.
multiple thread의 경우에는 확실히 높은 CPU의 사용율과 여러 thread를 균등하게 분산함으로써 CPU를 효율적으로 사용한다는 사실을 확인할 수 있습니다.
5. LINQ 확장 메서드 만들기
기존에 존재하는 LINQ 확장 메서드만으로도 왠만한 작업은 충분히 가능하지만 경우에 따라 해당 프로젝트에서 필요로 하는 특별한 기능을 실행하는 메서드를 만들어야 할지도 모릅니다. 이러한 경우 IEnumerable<T>에 대한 확장 메서드를 구현할 수 있습니다.
Average() 메서드는 평균을 구하는 메서드인데 계산방식이 Mean으로서 모든 수를 합하고 수의 Count값을 나눔으로써 평균을 산출합니다. 만약 가장 일반적으로 사용되는 수를 평균값으로 계산하는 Mode에 대한 메서드가 필요하다고 가정한다면 아래와 같이 구현해 줄 수 있습니다.
public static class MyLinqExtensions
{
public static decimal? Mode(this IEnumerable<decimal?> sequence)
{
var grouped = sequence.GroupBy(item => item);
var orderedGroups = grouped.OrderByDescending(group => group.Count());
return orderedGroups.FirstOrDefault()?.Key;
}
public static decimal? Mode<T>(this IEnumerable<T> sequence, Func<T, decimal?> selector)
{
return sequence.Select(selector).Mode();
}
}
그리고 위와 같은 확장메서드는 다른 확장 메서드와 동일한 방법으로 사용할 수 있습니다.
using (Northwind db = new())
{
var result = db.Products.Mode(x => x.UnitPrice);
Console.WriteLine(result);
}
6. LINQ to XML
LINQ to XML는 XML에 대한 공급자이며 XML 문서를 탐색하고 조작하는 데 사용될 수 있습니다.
(1) XML 생성하기
우선 LINQ to XML을 사용하기 위해 아래와 같은 방법으로 임의의 XML을 생성하도록 합니다.
using (Northwind db = new())
{
var xmlProduct = db.Products.ToList();
XElement xml = new("products",
from p in xmlProduct
select new XElement("product",
new XAttribute("id", p.ProductId),
new XAttribute("price", p.UnitPrice),
new XElement("name", p.ProductName)));
Console.WriteLine(xml.ToString());
}
위 예제에서 생성하는 XML의 구조는 다음과 같습니다.
<products>
<product id="1" price="18.0000">
<name>Chai</name>
</product>
<product id="2" price="19.0000">
<name>Chang</name>
</product>
<product id="3" price="10.0000">
<name>Aniseed Syrup</name>
</product>
(2) XML 쿼리하기
LINQ를 통해 XML을 쿼리 하는 방법에 대해 살펴보기 위해 위에서 생성된 XML을 sample.xml이라는 파일로 저장합니다. 그리고 다음과 같이 생성된 XML 문서 전체를 읽어 들이는 LINQ를 구현합니다.
XDocument doc = XDocument.Load(@"sample.xml");
var products = doc.Descendants("products").Descendants("product").Select(node => new
{
id = node.Attribute("id")?.Value,
price = node.Attribute("price")?.Value,
name = node.Element("name")?.Value
}).ToList();
foreach (var item in products)
{
Console.WriteLine($"{item.name}: {item.price}");
}
Load() 메서드에서 지정된 xml파일은 프로그램이 실행되는 경로와 같은 위치에 있다고 가정한 것입니다. 따라서 생성된 xml파일의 위치가 다르다면 해당 위치를 적절히 지정해야 합니다.
생성된 XML은 최상위 products와 하위 product요소로 나뉘어 있으므로 Descendants() 메서드를 통해 해당 요소를 찾아갈 수 있도록 지정하고 말단 node를 가져와 속성과 내부 요소 값을 가져오도록 하였습니다.