C# 3.3 · 3부. 중급 문법

LINQ 기초

데이터를 조건에 따라 선별하고 수집합니다. LINQ의 쿼리 구문과 메서드 구문을 함께 확인합니다.

예상 학습 시간 18분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

어떻게 찾는지 말고, 무엇을 원하는지 적기

목록에서 조건에 맞는 것만 선별하고, 정렬하고, 필요한 값만 꺼내는 일은 아주 자주 합니다. 1.6foreach 로 적으면 이런 모양이 됩니다.

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의 컬렉션에 모두 같은 방법으로 사용합니다.
  • 연산을 이어 붙여 원하는 만큼 쌓을 수 있습니다.
  • 중간 변수와 임시 목록이 사라집니다.
최소 예제 · 편집 가능

선별하고 정렬하고 꺼내기

아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.

Program.cs
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);
출력
감 800원 사과 1500원 우유 2500원 바나나 3000원 평균 3,160원

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

p => p.Price <= 3000람다식입니다 — "p 를 받아 이 조건을 판정한다" 는 뜻의 짧은 메서드입니다. 3.5에서 자세히 다루므로, 지금은 조건을 값처럼 넘기는 것이라고만 보면 됩니다.

상세 사용법

foreach 로 적을 때와 무엇이 다른가

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;
LINQ 로 적으면
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()

SkipTake 를 이어 붙이면 페이지 나누기가 됩니다 — .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 를 그대로 사용할 수 있습니다.

없을 때 어떻게 할지 정하기

FirstFirstOrDefault맞는 것이 없을 때 갈립니다.

products.First(p => p.Price > 100_000);
// InvalidOperationException — 없으면 예외입니다

products.FirstOrDefault(p => p.Price > 100_000);
// null — 없을 수 있다고 보는 쪽입니다

"반드시 있어야 하는가" 로 선택하세요. 없으면 잘못된 상태라면 First 가 낫습니다 — 그 자리에서 예외가 발생하므로 원인을 바로 알 수 있습니다. 없는 것이 정상이라면 FirstOrDefault 로 받아 null 을 확인합니다. Single·SingleOrDefault둘 이상이어도 예외라, 하나뿐임을 확인하고 싶을 때 사용합니다.

지연 실행 — 가장 조심할 대목

WhereSelect 를 적는 순간에는 아무 일도 일어나지 않습니다. 무엇을 할지 적어 둘 뿐이고, 실제로 도는 것은 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()맞는 것을 찾는 즉시 멈춥니다 — 뒤쪽은 아예 보지 않습니다. 백만 개짜리 목록이어도 앞에서 찾으면 거기서 끝납니다.

버전 배지

이 문법, 몇 버전부터 사용할 수 있나요

1.0
2.0
3.0
6.0
7.0
8.0
9.0
11.0
14.0
  • 3.0LINQ · 쿼리 구문 · 람다식 · 확장 메서드 · var
C# 3.0+

LINQ는 혼자 들어온 문법이 아닙니다. C# 3.0은 Where 같은 연산을 이미 있는 형식에 덧붙이는 확장 메서드, 조건을 값처럼 넘기는 람다식, 결과 형식을 적지 않아도 되는 var, 그리고 Select 로 만든 새 모양을 담을 익명 형식을 한꺼번에 들여왔습니다. LINQ를 쓰기 위해 그 넷이 필요했던 것입니다.

실습 문제

직접 해보기

1. 합격자만 선별해 평균 내기 난이도 하

점수 배열에서 60점 이상인 것만 선별한 뒤, 몇 명인지와 그들의 평균을 출력해보세요. 합격자가 없을 때 Average 가 어떻게 되는지도 확인해보세요.

선별한 결과를 두 번 사용하므로 ToList 로 한 번 확정해 두는 편이 낫습니다. 빈 목록에 Average 를 부르면 예외가 발생하니 Count 를 먼저 보세요.
int[] scores = [45, 78, 92, 55, 60, 88]; // 두 번 사용하므로 여기서 확정합니다 var passed = scores.Where(s => s >= 60).ToList(); if (passed.Count == 0) { Console.WriteLine("합격자가 없습니다"); } else { Console.WriteLine($"합격 {passed.Count}명"); Console.WriteLine($"평균 {passed.Average():F1}점"); } // 합격 4명 // 평균 79.5점
2. 분류별로 묶어 집계하기 난이도 중

상품 목록을 분류별로 묶어 각 분류의 개수와 합계를 출력하되, 합계가 큰 분류부터 나오게 해보세요.

GroupBy 로 묶은 뒤 그 결과에 다시 OrderByDescending 을 이어 붙일 수 있습니다. 묶음 하나하나가 목록이라 Sum 을 그대로 부를 수 있습니다.
var products = new List<Product> { new("사과", 1500, "과일"), new("바나나", 3000, "과일"), new("포도", 8000, "과일"), new("감", 800, "과일"), new("우유", 2500, "유제품"), new("치즈", 9000, "유제품") }; var summary = products .GroupBy(p => p.Category) .Select(g => new { Category = g.Key, Count = g.Count(), Total = g.Sum(p => p.Price) }) .OrderByDescending(x => x.Total); foreach (var s in summary) Console.WriteLine($"{s.Category} : {s.Count}개, 합계 {s.Total:N0}원"); // 과일 : 4개, 합계 13,300원 ← 개수가 아니라 합계 순입니다 // 유제품 : 2개, 합계 11,500원 // record Product(string Name, int Price, string Category); // new { … } 는 익명 형식입니다 — 이름 없이 그 자리에서 // 필요한 값만 묶어 담습니다(C# 3.0).
요약
  • LINQ는 절차가 아니라 조건으로 적습니다. Where·Select·OrderBy 를 이어 붙입니다.
  • 메서드 구문과 쿼리 구문은 결과가 같습니다. 실무에서는 메서드 구문이 훨씬 많이 사용됩니다.
  • First 는 없으면 예외, FirstOrDefaultnull 입니다. 반드시 있어야 하는가로 선택하세요.
  • 쿼리를 적는 시점에는 실행되지 않습니다. 탐색할 때마다 다시 계산하므로, 여러 번 사용한다면 ToList 로 확정하세요.
  • 반대로 First찾는 즉시 멈춥니다. 뒤쪽은 보지 않습니다.

C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.