LINQ: 언어에 통합된 쿼리
LINQ: 언어에 통합된 쿼리
LINQ(Language-Integrated Query)는 쿼리 기능을 C# 언어 자체에 직접 통합한 기술 묶음이에요. 전통적으로 데이터를 대상으로 한 쿼리는 단순한 문자열로 표현돼서 컴파일 타임 형식 검사도, IntelliSense 지원도 받지 못했어요. 게다가 데이터 원본의 종류마다 다른 쿼리 언어를 따로 배워야 했죠 — SQL 데이터베이스, XML 문서, 각종 웹 서비스 등으로요. LINQ에서는 쿼리 자체가 클래스, 메서드, 이벤트처럼 일급 언어 구성 요소(first-class language construct)가 됩니다.
쿼리를 쓸 때 가장 눈에 띄는 "언어 통합" 부분은 바로 쿼리 식(query expression)이에요. 쿼리 식은 선언적인 쿼리 구문(query syntax) 으로 작성하며, 최소한의 코드로 데이터 원본에 필터링·정렬·그룹화 연산을 수행해요. 어떤 종류의 데이터 원본이든 같은 쿼리 식 패턴으로 조회하고 변환할 수 있죠.
다음은 완전한 쿼리 연산의 예시예요. 데이터 원본 만들기, 쿼리 식 정의하기, foreach 문에서 쿼리 실행하기까지 전체 과정을 보여 줍니다.
// Specify the data source.
int[] scores = [97, 92, 81, 60];
// Define the query expression.
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;
// Execute the query.
foreach (var i in scoreQuery)
{
Console.Write(i + " ");
}
// Output: 97 92 81
예제를 컴파일하려면 using System.Linq; 지시문을 추가해야 할 수도 있어요. 최신 .NET 버전은 암시적 using 덕분에 이 지시문을 전역 using으로 자동 추가해 주지만, 이전 버전에서는 소스에 직접 적어야 해요.
쿼리 식 개요
- 쿼리 식은 LINQ를 지원하는 어떤 데이터 원본에서든 데이터를 검사하고 변환해요. 예를 들어 단일 쿼리가 SQL 데이터베이스에서 데이터를 가져와 XML 스트림으로 출력할 수도 있어요.
- 쿼리 식은 친숙한 C# 언어 구성을 많이 사용해서 읽기 쉬워요.
- 쿼리 식 안의 변수는 모두 강력한 형식(strongly typed)이에요.
- 쿼리는
foreach문에서 쿼리 변수를 반복하기 전까지는 실행되지 않아요 (지연 실행). - 컴파일 타임에 컴파일러는 C# 사양의 규칙에 따라 쿼리 식을 표준 쿼리 연산자 메서드 호출로 변환해요. 쿼리 구문으로 쓸 수 있는 쿼리는 모두 메서드 구문으로도 표현할 수 있죠. 어떤 경우엔 쿼리 구문이, 다른 경우엔 메서드 구문이 더 읽기 쉬워요. 두 형식 사이에 의미적·성능적 차이는 없어요.
Enumerable.Count나Enumerable.Max같은 일부 쿼리 연산은 대응하는 쿼리 식 절이 없어서 반드시 메서드 호출로 표현해야 해요. 메서드 구문과 쿼리 구문은 여러 방식으로 결합할 수 있어요.- 쿼리 식은 적용되는 형식에 따라 식 트리(expression tree) 또는 대리자(delegate)로 컴파일돼요. 컴파일러는
IEnumerable<T>쿼리를 대리자로,IQueryable및IQueryable<T>쿼리를 식 트리로 컴파일합니다.
데이터 원본에서 LINQ 쿼리 활성화하기
인메모리 데이터는 두 가지 방법으로 LINQ 쿼리를 활성화할 수 있어요. 데이터가 IEnumerable<T>를 구현하는 형식이라면 LINQ to Objects로 조회하면 되고, IEnumerable<T> 인터페이스를 구현해서 열거를 가능하게 하는 게 타당하지 않다면 그 형식에 표준 쿼리 연산자 메서드를 직접 정의하거나 그 형식을 위한 확장 멤버로 정의하면 돼요. 표준 쿼리 연산자의 사용자 정의 구현은 결과를 반환할 때 지연 실행(deferred execution)을 사용해야 합니다.
원격 데이터에서 LINQ 쿼리를 활성화하는 가장 좋은 방법은 IQueryable<T> 인터페이스를 구현하는 거예요.
IQueryable LINQ 공급자
IQueryable<T>를 구현하는 LINQ 공급자는 복잡도가 매우 다양해요.
덜 복잡한 IQueryable 공급자는 웹 서비스의 단일 메서드에만 접근할 수도 있어요. 이런 공급자는 다루는 쿼리에서 특정 정보를 기대하기 때문에 데이터 원본에 특화돼 있고, 아마 단일 결과 형식을 노출하는 닫힌 형식 시스템을 가져요. 쿼리 실행의 대부분은 표준 쿼리 연산자의 Enumerable 구현을 이용해 로컬에서 일어나죠. 덜 복잡한 공급자는 쿼리를 나타내는 식 트리에서 메서드 호출 식 하나만 검사하고 나머지 쿼리 로직은 다른 곳에서 처리하게 둘 수도 있어요.
중간 복잡도의 IQueryable 공급자는 부분적으로 표현력 있는 쿼리 언어를 가진 데이터 원본을 대상으로 할 수 있어요. 웹 서비스를 대상으로 한다면 여러 메서드에 접근하고, 쿼리가 찾는 정보에 따라 어떤 메서드를 호출할지 선택할 수도 있죠. 중간 복잡도의 공급자는 단순한 공급자보다 풍부한 형식 시스템을 갖지만 여전히 고정된 형식 시스템이에요. 예를 들어 탐색할 수 있는 일대다 관계의 형식을 노출할 수는 있지만, 사용자 정의 형식을 위한 매핑 기술은 제공하지 않아요.
Entity Framework Core 공급자 같은 복잡한 IQueryable 공급자는 완전한 LINQ 쿼리를 SQL 같은 표현력 있는 쿼리 언어로 번역할 수 있어요. 복잡한 공급자는 쿼리의 더 넓은 범위의 질문을 다룰 수 있어서 더 일반적이죠. 또한 열린 형식 시스템을 가지므로 사용자 정의 형식을 매핑할 광범위한 인프라가 필요해요. 복잡한 공급자를 개발하는 데는 상당한 노력이 들어갑니다.