LINQ 기초
데이터를 조건에 따라 선별하고 수집합니다. LINQ의 쿼리 구문과 메서드 구문을 함께 확인합니다.
어떻게 찾는지 말고, 무엇을 원하는지 적기
목록에서 조건에 맞는 것만 선별하고, 정렬하고, 필요한 값만 꺼내는 일은 아주 자주
합니다. 1.6의 foreach 로
적으면 이런 모양이 됩니다.
var result = new List<string>(); foreach (var p in products) { if (p.Price <= 3000) result.Add(p.Name); } result.Sort();
동작하지만 절반이 절차입니다 — 빈 목록을 만들고, 하나씩 탐색하고, 담고, 정렬합니다. 정작 하고 싶은 말은 "3000원 이하인 것의 이름을 싼 것부터" 한 줄뿐입니다.
LINQ(Language Integrated Query)는 그 한 줄을 그대로 적게 해 줍니다. 선별·정렬·변환·집계를 이름이 붙은 연산으로 이어 붙이는 방식이라, 코드가 절차가 아니라 조건으로 읽힙니다.
- 목록·배열·3.2의 컬렉션에 모두 같은 방법으로 사용합니다.
- 연산을 이어 붙여 원하는 만큼 쌓을 수 있습니다.
- 중간 변수와 임시 목록이 사라집니다.
선별하고 정렬하고 꺼내기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
var products = new List<Product> { new("사과", 1500), new("바나나", 3000), new("포도", 8000), new("감", 800), new("우유", 2500) }; // 3000원 이하만, 싼 것부터, 이름과 값만 var cheap = products .Where(p => p.Price <= 3000) .OrderBy(p => p.Price) .Select(p => $"{p.Name} {p.Price}원"); foreach (var line in cheap) Console.WriteLine(line); // 집계도 한 줄입니다 Console.WriteLine($"평균 {products.Average(p => p.Price):N0}원"); record Product(string Name, int Price);
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
p => p.Price <= 3000 은 람다식입니다 —
"p 를 받아 이 조건을 판정한다" 는 뜻의 짧은 메서드입니다.
3.5에서 자세히 다루므로, 지금은
조건을 값처럼 넘기는 것이라고만 보면 됩니다.
foreach 로 적을 때와 무엇이 다른가
var names = new List<string>(); var total = 0; foreach (var p in products) { if (p.Price > 3000) continue; names.Add(p.Name); total += p.Price; } names.Sort(); var average = names.Count == 0 ? 0 : total / names.Count;
var cheap = products .Where(p => p.Price <= 3000) .ToList(); var names = cheap .Select(p => p.Name) .Order() .ToList(); var average = cheap.Count == 0 ? 0 : cheap.Average(p => p.Price);
줄 수는 비슷합니다. 달라진 것은 읽었을 때 보이는 것입니다. 왼쪽은
continue 와 += 를 따라가며
머릿속으로 실행해야 무엇을 하는지 알 수 있고, 오른쪽은
Where·Select·Average
라는 이름이 곧 설명입니다.
자주 사용하는 연산
| 연산 | 하는 일 | 예시 |
|---|---|---|
| Where | 조건에 맞는 것만 선별하기 | .Where(p => p.Price < 3000) |
| Select | 다른 모양으로 바꾸기 | .Select(p => p.Name) |
| OrderBy ThenBy | 정렬 · 같은 값일 때 다음 기준 | .OrderBy(p => p.Price) |
| First FirstOrDefault | 맞는 것 하나 꺼내기 | .FirstOrDefault(p => p.Id == 3) |
| Any · All | 하나라도 · 모두 맞는가 | .Any(p => p.Price > 10000) |
| Count Sum · Average | 세기 · 집계 | .Sum(p => p.Price) |
| Take · Skip | 앞에서 몇 개 · 몇 개 건너뛰기 | .Skip(20).Take(10) |
| GroupBy | 기준별로 묶기 | .GroupBy(p => p.Category) |
| ToList ToArray | 결과를 확정해 담기 | .ToList() |
Skip 과 Take 를 이어 붙이면
페이지 나누기가 됩니다 — .Skip((page - 1) * size).Take(size)
입니다.
적는 방법이 둘입니다
var names = products .Where(p => p.Price <= 3000) .OrderBy(p => p.Price) .Select(p => p.Name);
var names = from p in products where p.Price <= 3000 orderby p.Price select p.Name;
결과가 완전히 같습니다. 쿼리 구문은 컴파일할 때 메서드 구문으로
바뀝니다. 다만 쿼리 구문에는 대응하는 키워드가 없는 연산이 많아
(Count·Any·Skip …)
결국 메서드를 섞게 됩니다. 실무에서는 메서드 구문이 훨씬 많이 쓰입니다.
여러 목록을 엮는 join 이 들어갈 때만 쿼리 구문이 읽기
좋습니다.
기준별로 묶기
foreach (var g in products.GroupBy(p => p.Category)) Console.WriteLine($"{g.Key} : {g.Count()}개, 평균 {g.Average(p => p.Price):N0}원"); // 과일 : 4개, 평균 3,325원 // 유제품 : 2개, 평균 5,750원
GroupBy 가 내주는 묶음마다
Key(묶은 기준)가 있고, 묶음 자체가 다시 목록이라
Count·Average 를 그대로
사용할 수 있습니다.
없을 때 어떻게 할지 정하기
First 와 FirstOrDefault 는
맞는 것이 없을 때 갈립니다.
products.First(p => p.Price > 100_000); // InvalidOperationException — 없으면 예외입니다 products.FirstOrDefault(p => p.Price > 100_000); // null — 없을 수 있다고 보는 쪽입니다
"반드시 있어야 하는가" 로 선택하세요. 없으면 잘못된 상태라면
First 가 낫습니다 — 그 자리에서 예외가 발생하므로 원인을
바로 알 수 있습니다. 없는 것이 정상이라면
FirstOrDefault 로 받아 null
을 확인합니다. Single·SingleOrDefault
는 둘 이상이어도 예외라, 하나뿐임을 확인하고 싶을 때 사용합니다.
지연 실행 — 가장 조심할 대목
Where 나 Select 를 적는 순간에는
아무 일도 일어나지 않습니다. 무엇을 할지 적어 둘 뿐이고, 실제로 도는
것은 foreach 로 탐색하거나
ToList 를 부를 때입니다.
var numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(n => n > 1); numbers.Add(10); // 쿼리를 만든 뒤에 추가했는데 Console.WriteLine(string.Join(", ", query)); // 2, 3, 10 — 나중에 넣은 것도 나옵니다 var frozen = numbers.Where(n => n > 1).ToList(); numbers.Add(20); Console.WriteLine(string.Join(", ", frozen)); // 2, 3, 10 — ToList 로 그 시점에 확정했습니다
그래서 탐색할 때마다 다시 돕니다. 조건식이 몇 번 불리는지 세어 봤습니다.
var calls = 0; var counted = numbers.Where(n => { calls++; return n > 1; }); Console.WriteLine(calls); // 0 — 만들기만 했을 때 foreach (var _ in counted) { } Console.WriteLine(calls); // 5 — 한 번 탐색한 뒤 foreach (var _ in counted) { } Console.WriteLine(calls); // 10 — 또 한 번 탐색한 뒤
같은 쿼리를 두 번 탐색하면 계산도 두 번 합니다. 조건식 안에서
데이터베이스를 부르거나 파일을 읽는다면 그만큼 되풀이됩니다. 결과를 여러 번
사용한다면 ToList 로 한 번 확정해 두세요.
반대로 지연 실행이 이득인 자리도 있습니다.
.Where(…).First() 는 맞는 것을 찾는 즉시
멈춥니다 — 뒤쪽은 아예 보지 않습니다. 백만 개짜리 목록이어도 앞에서 찾으면
거기서 끝납니다.
이 문법, 몇 버전부터 사용할 수 있나요
- 3.0LINQ · 쿼리 구문 · 람다식 · 확장 메서드 · var
LINQ는 혼자 들어온 문법이 아닙니다. C# 3.0은
Where 같은 연산을 이미 있는 형식에 덧붙이는
확장 메서드, 조건을 값처럼 넘기는 람다식,
결과 형식을 적지 않아도 되는 var, 그리고
Select 로 만든 새 모양을 담을
익명 형식을 한꺼번에 들여왔습니다.
LINQ를 쓰기 위해 그 넷이 필요했던 것입니다.
직접 해보기
점수 배열에서 60점 이상인 것만 선별한 뒤, 몇 명인지와 그들의 평균을
출력해보세요. 합격자가 없을 때 Average 가 어떻게
되는지도 확인해보세요.
상품 목록을 분류별로 묶어 각 분류의 개수와 합계를 출력하되, 합계가 큰 분류부터 나오게 해보세요.
- LINQ는 절차가 아니라 조건으로 적습니다.
Where·Select·OrderBy를 이어 붙입니다. - 메서드 구문과 쿼리 구문은 결과가 같습니다. 실무에서는 메서드 구문이 훨씬 많이 사용됩니다.
First는 없으면 예외,FirstOrDefault는null입니다. 반드시 있어야 하는가로 선택하세요.- 쿼리를 적는 시점에는 실행되지 않습니다. 탐색할 때마다 다시 계산하므로, 여러 번 사용한다면
ToList로 확정하세요. - 반대로
First는 찾는 즉시 멈춥니다. 뒤쪽은 보지 않습니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.