Quick Reference
LINQ to Objects는 Where/Select로 조회식을 만들고, foreach·Any·First·ToList처럼 결과를 요구할 때 실행합니다. 현재 결과를 한 번 고정하려면 ToList/ToArray, 하나가 반드시 필요한지에 따라 First·FirstOrDefault·Single을 고릅니다. IQueryable<T>는 같은 문법이어도 provider가 번역하는 별도 경계입니다.
List<string> highScores = players
.Where(p => p.Score >= 1000) // 필터링
.OrderByDescending(p => p.Score) // 정렬
.Select(p => $"{p.Name}: {p.Score}") // 변환
.ToList(); // 즉시 실행 + List로 확정
var optionalTop = players.FirstOrDefault(p => p.Score >= 1000); // 없으면 null
var onlyMina = players.Single(p => p.Name == "Mina"); // 정확히 하나여야 함문법
어떤 LINQ 연산자를 먼저 알면 되나
입문 단계에서는 LINQ를 아래 네 묶음으로 보면 대부분의 조회 코드를 읽을 수 있습니다.
var filtered = users.Where(u => u.IsActive); // 필터링
var names = users.Select(u => u.Name); // 변환
var sorted = users.OrderBy(u => u.Name); // 정렬
bool hasAny = users.Any(); // 즉시 실행Where: 남길 항목 고르기Select: 다른 형태로 바꾸기OrderBy/ThenBy: 정렬하기Any,Count,First,ToList: 결과 확정하기
LINQ는 extension method의 집합
LINQ의 Where, Select, OrderBy 등은 System.Linq.Enumerable 클래스의 extension method입니다. using System.Linq;를 추가하면 IEnumerable<T>를 구현하는 모든 타입에서 사용할 수 있습니다.
// 이 두 호출은 완전히 동일
var r1 = players.Where(p => p.Score >= 1000);
var r2 = Enumerable.Where(players, p => p.Score >= 1000);지연 실행 — 열거할 때 계산된다
많은 LINQ to Objects 연산자는 체인을 만들 때 즉시 실행되지 않습니다. foreach, ToList(), ToArray(), Count() 같은 열거 트리거가 호출될 때 비로소 실행됩니다. 이것을 지연 실행(deferred execution) 이라고 합니다. 단, OrderBy처럼 지연된 상태로 만들어도 열거가 시작되면 입력 전체를 버퍼링하는 연산자가 있으므로 "지연 = 원소 하나씩 처리"로 해석하지 않습니다.
var query = players.Where(p => p.Score >= 1000); // 아직 실행 안 됨
players.Add(new Player("Late", 2000)); // 원본에 추가
foreach (var p in query) // 이 순간 실행 — Late도 포함됨
Console.WriteLine(p.Name);
// ToList()는 이 시점의 결과를 스냅샷으로 확정
var snapshot = query.ToList(); // 즉시 실행, 결과 고정// ❌ 지연 실행의 함정 — 매번 다시 계산
IEnumerable<Player> top = players.Where(p => p.Score >= 1000);
Console.WriteLine(top.Count()); // 쿼리 실행
Console.WriteLine(top.First()); // 쿼리 다시 실행 — 비효율
// ✅ ToList()로 한 번만 실행
List<Player> top = players.Where(p => p.Score >= 1000).ToList();핵심 연산자
// Where — 조건을 만족하는 항목만 남김
var adults = users.Where(u => u.Age >= 18);
// Select — 각 항목을 다른 형태로 변환
var names = users.Select(u => u.Name);
var dtos = users.Select(u => new UserDto(u.Id, u.Name));
// OrderBy / OrderByDescending
var sorted = users.OrderBy(u => u.Name)
.ThenByDescending(u => u.Age); // 보조 정렬
// Any — 조건을 만족하는 항목이 하나라도 있는지 (Count() > 0보다 빠름)
bool hasAdmin = users.Any(u => u.IsAdmin);
// All — 모든 항목이 조건을 만족하는지
bool allAdult = users.All(u => u.Age >= 18);
// First / FirstOrDefault
var firstAdmin = users.First(u => u.IsAdmin); // 없으면 예외
var optionalAdmin = users.FirstOrDefault(u => u.IsAdmin); // 참조형이면 없을 때 null
// Single — 정확히 하나여야 하는 경우
var onlyUser = users.Single(u => u.Id == 42); // 없거나 둘 이상이면 예외| 결과 계약 | 항목 없음 | 둘 이상 |
|---|---|---|
First | 예외 | 첫 항목 반환 |
FirstOrDefault | default | 첫 항목 반환 |
Single | 예외 | 예외 |
SingleOrDefault | default | 예외 |
// ❌ 존재 여부만 볼 때 Count() > 0는 끝까지 셀 수 있다
bool hasAdmin = users.Count(u => u.IsAdmin) > 0;
// ✅ Any()가 의도가 더 직접적이고 보통 더 빠르다
bool hasAdminFast = users.Any(u => u.IsAdmin);쿼리 문법 vs 메서드 문법
C#은 SQL과 유사한 쿼리 문법도 지원합니다. 컴파일러가 메서드 문법으로 변환하므로 결과는 동일합니다.
// 메서드 문법
var result = players.Where(p => p.Score >= 1000).Select(p => p.Name);
// 쿼리 문법 — 같은 결과
var result = from p in players
where p.Score >= 1000
select p.Name;쿼리 문법은 group, join, 복잡한 from이 나올 때 더 읽기 쉬운 경우가 많고, 단순 필터링과 변환은 메서드 문법이 더 짧습니다.
LINQ to Objects와 IQueryable은 같은 실행기가 아니다
IEnumerable<T>의 LINQ to Objects는 .NET 메서드를 메모리에서 실행합니다. IQueryable<T>는 식 트리를 DB/검색 provider가 번역할 수 있으며, 지원하지 않는 .NET 메서드·사용자 함수·문화권 동작은 번역 실패하거나 의도치 않게 클라이언트 쪽으로 넘어갈 수 있습니다. provider query를 AsEnumerable()로 바꾸면 그 이후 연산은 메모리에서 실행되지만, 이는 데이터를 물질화하지 않습니다.
LINQ 실행 흐름
<ZoomImage src="/images/csharp/linq-pipeline.svg" alt="원본 컬렉션에서 필터링과 변환을 거쳐 결과 리스트가 되는 LINQ 흐름도" caption="LINQ는 거대한 반복문 하나보다, 작은 변환 단계를 연결한 흐름으로 이해하면 훨씬 읽기 쉽습니다." />
LINQ 연산과 실행
| 연산자 | 역할 | 즉시/지연 |
|---|---|---|
Where | 필터링 | 지연 |
Select | 변환 | 지연 |
OrderBy / ThenBy | 정렬 | 지연 |
ToList() / ToArray() | 결과 확정 | 즉시 |
Any() | 존재 여부 | 즉시 |
Count() | 개수 | 즉시 |
First() / FirstOrDefault() | 첫 항목 | 즉시 |
주의할 점
LINQ 체인 안에 부작용(상태 변경, 로그 출력, 외부 변수 수정)을 넣지 마세요. 지연 실행 때문에 언제 실행되는지 예측하기 어렵고, 몇 번 실행되는지도 보장되지 않습니다.
"데이터를 어떻게 고르고 바꾸는가"가 주제라면 LINQ가 좋고, "여러 단계의 절차를 어떻게 실행하는가"가 주제라면 루프가 더 나은 경우가 많습니다.
First()는 없으면 예외를 던지고, FirstOrDefault()는 기본값을 돌려줍니다. 값이 반드시 있어야 하는지, 없어도 되는지를 먼저 정하고 선택하세요.
source가 null이면 대부분의 LINQ 연산자는 ArgumentNullException을 던집니다. null 허용 입력의 기본값을 조용히 만들기보다 호출 경계에서 null을 거부하거나 빈 시퀀스로 바꾼 이유를 명시합니다. ToList()는 컨테이너의 얕은 snapshot이므로 안의 참조 객체까지 복사하지는 않습니다.
참고 링크
2 sources