Menu

C# LINQ: Where, Select, OrderBy, GroupBy 사용법과 예제

LINQ는 C#의 모든 컬렉션에 쿼리 연산자를 더해 줍니다. 메서드 구문과 쿼리 구문, Where로 필터링, Select로 투영, OrderBy와 ThenBy로 정렬, GroupBy로 그룹화, First와 FirstOrDefault의 함정, 집계, 그리고 지연 실행이 쿼리 실행 시점을 어떻게 바꾸는지 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

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()를 호출하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기