LINQ(Language Integrated Query)는 시퀀스를 필터링, 변환, 정렬, 그룹화, 요약하는 메서드 라이브러리입니다. using System.Linq;를 추가하면 모든 배열, List<T>, Dictionary와 그 밖의 IEnumerable<T>에 Where, Select, OrderBy 같은 메서드가 생기며, 각각 요소 하나로 무엇을 할지 말해 주는 람다를 받습니다.
첫 쿼리: 메서드 구문과 쿼리 구문
LINQ에는 두 가지 표기법이 있습니다. 메서드 구문은 호출을 연결하고, 쿼리 구문은 SQL처럼 읽힙니다. 컴파일러가 쿼리 구문을 같은 메서드 호출로 바꾸므로 결과는 똑같습니다.
출력:
95, 91, 88
95, 91, 88
최신 코드에서는 메서드 구문이 더 흔하고, 일부 연산자(Take, Distinct, Count, First, ToList)는 메서드로만 존재합니다. 쿼리 구문은 조인과 let으로 중간 변수를 도입하는 쿼리에서 빛납니다. 이 페이지의 나머지는 메서드 구문을 쓰고, 쿼리 구문이 보탬이 되는 곳에서만 보여 줍니다.
Where와 Select: 필터링과 투영
Where는 조건에 맞는 요소를 남깁니다. Select는 각 요소를 다른 것으로 바꿉니다. 둘을 함께 쓰면 일상적인 쿼리 대부분이 해결됩니다.
출력:
Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug
Where는 요소를 절대 바꾸지 않고, Select는 개수를 절대 바꾸지 않습니다. new { p.Name, WithTax = ... }는 익명 형식, 즉 컴파일러가 대신 작성해 주는 클래스를 만들며, 그 속성은 일반 점 문법으로 읽습니다. 쓸 수 있는 이름이 없으므로 이를 담는 변수는 var로 선언해야 하고, 한 메서드 안에 머무는 결과에 어울립니다.
OrderBy, ThenBy와 여러 키로 정렬하기
출력:
Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana
OrderBy는 안정 정렬입니다. 키가 같은 요소는 원래 순서를 유지합니다. 두 번째 키에는 ThenBy나 ThenByDescending을 쓰세요. 두 번째 OrderBy는 모든 것을 자기 키로 다시 정렬해서 첫 키를 단순한 동점 판정으로 만듭니다. 우선순위가 반대가 됩니다. 쿼리 구문에서는 키를 쉼표로 구분합니다: orderby o.Customer, o.Total descending.
List<T>.Sort와 달리 OrderBy는 원본을 그대로 두고 새 시퀀스를 반환합니다.
First, Single, Last와 OrDefault 버전
이들은 시퀀스 대신 요소 하나를 반환합니다. 일반 버전은 반환할 것이 없으면 예외를 던지고, OrDefault 버전은 타입의 기본값을 반환합니다.
출력:
Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0
빈 시퀀스에서 First가 던지는 예외는 "Sequence contains no elements" 메시지(조건자에 일치하는 것이 없으면 "Sequence contains no matching element")를 가진 InvalidOperationException이며, 가장 많이 검색되는 C# 오류 중 하나입니다. "없음"이 정상적인 결과라면 FirstOrDefault를 쓰고 null을 확인하세요.
마지막 줄은 값 형식의 함정을 보여 줍니다. int 시퀀스에서 FirstOrDefault는 일치하는 것이 없으면 0을 반환하는데, 이는 진짜 0과 구분할 수 없습니다. Where로 필터링하고 먼저 Any()를 확인하거나, .NET 6 이상에서는 직접 기본값을 넘기세요: scores.FirstOrDefault(s => s > 100, -1).
Single은 "정확히 하나"를 위한 것입니다. 없거나 둘 이상이면 예외를 던지므로 쿼리이자 단언이기도 합니다.
Any, All, Count, Contains
출력:
True
True
True
3
True
True
Any()는 첫 일치 항목에서 멈추므로 "뭐라도 있나?"를 묻는 올바른 방법은 items.Any()입니다. items.Count() > 0은 원본이 리스트나 배열 같은 컬렉션이면 Count 속성을 읽지만, 쿼리나 다른 지연 시퀀스에서는 먼저 모든 요소를 훑습니다. 빈 시퀀스에 대한 All은 조건을 실패하는 요소가 없으므로 true를 반환합니다.
Sum, Average, Min, Max
출력:
Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException
빈 시퀀스의 Sum은 0이지만, 빈 숫자 시퀀스의 Average, Min, Max는 합리적인 답이 없으므로 InvalidOperationException을 던집니다. Any()로 막거나 먼저 DefaultIfEmpty()를 쓰세요: empty.DefaultIfEmpty(0).Average().
Distinct, Take, Skip
출력:
home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15
Distinct는 각 값의 첫 등장을 순서대로 남깁니다. Skip(n).Take(m)은 표준적인 페이지 나누기 패턴이고, TakeWhile/SkipWhile은 조건을 실패하는 첫 요소에서 멈추거나 시작합니다. Enumerable.Range(start, count)는 배열 없이 정수 시퀀스를 만듭니다.
GroupBy
GroupBy는 요소를 키에 따라 버킷에 나눠 담습니다. 각 그룹은 Key를 가지며 그 자체가 시퀀스이므로 집계할 수 있습니다.
출력:
North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev
그룹은 정렬된 순서가 아니라 키가 원본에 처음 나타나는 순서(North, South, East)로 나옵니다. 정렬이 필요하면 OrderBy(g => g.Key)를 추가하세요. 쿼리 구문에서 group X by K는 각 그룹에 무엇을 넣을지(여기서는 담당자 이름만) 고르고, into g는 쿼리를 이어 갈 수 있도록 그룹에 이름을 붙입니다.
ToList, ToArray, ToDictionary
To... 메서드는 쿼리를 실행하고 결과를 저장합니다.
출력:
ben@x.com
2
ToDictionary: ArgumentException
두 요소가 같은 키를 만들면 ToDictionary는 Dictionary.Add와 똑같이 ArgumentException을 던집니다. 중복이 예상된다면 먼저 그룹화하거나(wordCounts처럼), 각 키를 값의 시퀀스에 대응시키는 ToLookup을 쓰세요.
지연 실행
Where, Select, OrderBy, GroupBy를 비롯한 대부분의 연산자는 쿼리 객체를 반환하고, 무언가가 이를 열거하기 전까지 아무 일도 하지 않습니다. 여기서 볼 만한 결과가 두 가지 나옵니다:
출력:
25, 40, 99
converting 10
converting 25
converting 40
converting 99
Count: 4
converting 10
converting 25
converting 40
converting 99
Max: 9900
converting 10
converting 25
converting 40
converting 99
Cached: 4, 9900
첫 쿼리는 쿼리를 작성한 뒤에 추가된 99를 보았습니다. 필터가 string.Join이 열거할 때 실행되었기 때문입니다. 두 번째 쿼리는 Count()와 Max()가 각각 처음부터 열거했으므로 변환을 두 번 출력했습니다. 가벼운 람다라면 작업 낭비일 뿐이지만, 람다 안에 데이터베이스 쿼리, 파일 읽기, 난수가 있다면 두 번의 순회가 서로 다른 데이터를 반환할 수 있습니다. 결과를 두 번 이상 쓸 것이라면 ToList()나 ToArray()로 구체화하세요.
값 하나를 반환하는 연산자(Count, Sum, First, Any, Max)와 To... 메서드는 즉시 실행됩니다. 이 지연성이 어떻게 만들어지는지는 IEnumerable과 yield를 참고하세요.
빠른 참조
| 연산자 | 하는 일 | 반환 |
|---|---|---|
Where(x => cond) | 필터링 | 시퀀스(지연) |
Select(x => expr) | 각 요소 변환 | 시퀀스(지연) |
SelectMany(x => seq) | 중첩 시퀀스 평탄화 | 시퀀스(지연) |
OrderBy, ThenBy(+Descending) | 안정 정렬 | 시퀀스(지연) |
GroupBy(x => key) | 키로 나누기 | 그룹의 시퀀스(지연) |
Distinct, Take, Skip | 중복 제거, 페이지 나누기 | 시퀀스(지연) |
First, Single, Last | 요소 하나, 없으면 예외 | 요소 |
FirstOrDefault 등 | 요소 하나 또는 기본값 | 요소 |
Any, All, Contains | 검사 | bool |
Count, Sum, Min, Max, Average | 집계 | 숫자 |
ToList, ToArray, ToDictionary | 실행하고 저장 | 컬렉션 |
흔한 실수
- 비어 있을 수 있는 시퀀스에
First()쓰기. "Sequence contains no elements"를 던집니다.FirstOrDefault를 쓰고 확인하세요. OrderBy두 번 호출하기. 두 번째 키가 주 키가 됩니다.ThenBy를 쓰세요.- 쿼리를 여러 번 열거하기. 작업이 반복됩니다.
ToList()를 한 번 호출하세요. - 쿼리가 원본의 스냅샷을 찍으리라 기대하기. 작성할 때가 아니라 열거할 때 원본을 읽습니다.
- 빈 시퀀스에
Average,Min,Max쓰기. 예외를 던집니다.Any()나DefaultIfEmpty로 막으세요. - 요소 확인에
Count() > 0쓰기.Any()는 첫 요소에서 멈춥니다.
자주 묻는 질문
C#에서 LINQ란 무엇인가요?
LINQ(Language Integrated Query)는 Where, Select, OrderBy, GroupBy처럼 System.Linq에 있는 확장 메서드 모음으로, 배열, 리스트, 딕셔너리, 다른 쿼리의 결과 등 모든 IEnumerable<T>에 동작합니다. C#에는 컴파일러가 같은 메서드 호출로 바꿔 주는 쿼리 구문(from x in items where ... select ...)도 있습니다.
LINQ에서 Select와 Where의 차이는 무엇인가요?
Where는 필터링합니다. 람다가 true를 반환하는 요소를 그대로 남깁니다. Select는 투영합니다. 모든 요소를 속성이나 새 객체 같은 다른 것으로 바꾸며 개수는 그대로입니다. 둘은 흔히 연결해서 씁니다: people.Where(p => p.Age >= 18).Select(p => p.Name).
First와 FirstOrDefault의 차이는 무엇인가요?
First()는 첫 요소(또는 조건자에 대한 첫 일치 항목)를 반환하며, 없으면 InvalidOperationException("Sequence contains no elements" 또는 "no matching element")을 던집니다. FirstOrDefault()는 대신 타입의 기본값을 반환합니다. 클래스는 null, int는 0이므로 결과를 쓰기 전에 확인하세요.
LINQ로 두 필드 기준으로 정렬하려면 어떻게 하나요?
OrderBy 뒤에 ThenBy를 연결합니다: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). OrderBy를 두 번 호출하면 안 됩니다. 두 번째 호출이 전체 시퀀스를 다시 정렬하므로 그 키가 주 키가 되고 첫 키는 동점만 가려서, 의도와 반대가 됩니다.
LINQ의 GroupBy는 어떻게 동작하나요?
GroupBy(x => key)는 서로 다른 키마다 그룹 하나를 반환합니다. 각 그룹은 IGrouping<TKey, TElement>로, Key 속성을 가지며 그 자체가 그 키를 가진 요소들의 시퀀스이므로 Count(), Sum(), Select를 호출할 수 있습니다. 그룹은 키가 처음 나타나는 순서대로 나옵니다.
LINQ의 지연 실행이란 무엇인가요?
대부분의 LINQ 연산자는 호출할 때 실행되지 않고, 열거될 때마다(foreach, ToList(), Count() 등) 실행되는 쿼리를 반환합니다. 그래서 쿼리는 정의된 뒤에 원본에 생긴 변경을 보게 되고, 두 번 열거하면 작업도 두 번 합니다. 한 번 실행하고 결과를 보관하려면 ToList()나 ToArray()를 호출하세요.