1. 소개
컬렉션을 다룰 때 그냥 하나씩 돌면서 뭔가 하는 게 아니라, 전체 데이터에 대한 간단한 "요약"이 필요할 때가 많아. 예를 들면:
- 5점을 받은 학생이 몇 명인지 세기.
- 학교에 학생이 총 몇 명인지 알아보기.
- 시험에서 학생들이 받은 점수의 합계 구하기.
- 최고점, 최저점 찾기.
- 반 평균 점수 계산하기.
물론 이런 건 그냥 카운터 변수 두고 for문 돌려도 되지. 근데 솔직히 이런 단순한 작업에 코드 20줄 쓰는 거 귀찮잖아.
LINQ에는 집계 함수라는 게 있어 — 이미 만들어진 메서드들이고, 컬렉션을 받아서 모든 요소를 쭉 돌고, 결과(합계, 평균, 최대, 최소 등)를 바로 내놔. 가끔은 더 복잡한 것도 가능하고.
여기 "집계 함수 표" 간단 정리:
| 메서드 | 무엇을 하나 | 반환값 |
|---|---|---|
|
요소 개수 세기 | |
|
숫자 값의 합계 구하기 | 요소 타입에 따라 다름 (, , ...) |
|
평균값 계산 | 또는 다른 숫자 타입 |
|
최대값 찾기 | 컬렉션의 요소 |
|
최소값 찾기 | 컬렉션의 요소 |
이 메서드들은 전부 IEnumerable<T>를 구현한 컬렉션의 extension 메서드야. 자세한 건 LINQ 집계 메서드 공식 문서 참고!
2. 실습용 데이터 준비
계속 학생 관리용 간단한 앱으로 예제 해볼게. 이런 클래스가 있다고 치자:
// 학생 모델
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // 5점 만점 점수
public string Email { get; set; }
}
그리고 리스트:
// 기본 학생 리스트
List<Student> students = new List<Student>
{
new Student { Name = "이반", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "올가", Grade = 4, Email = "olga@example.com" },
new Student { Name = "아르템", Grade = 3, Email = "artem@example.com" },
new Student { Name = "다리야", Grade = 5, Email = "darya@example.com" },
new Student { Name = "표트르", Grade = 2, Email = "petr@example.com" }
};
3. 요소 개수 세기: Count()
가장 기본적인 통계
학생이 총 몇 명인지 알고 싶으면:
int totalStudents = students.Count(); // 5
Console.WriteLine($"총 학생 수: {totalStudents}");
LINQ 매직: 진짜 간단! Count()는 컬렉션의 요소 개수를 반환해.
조건부 개수 세기
그럼 5점 받은 학생은 몇 명?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"5점 받은 학생: {excellentStudents}");
여기선 Count에 람다를 넘겨서 조건에 맞는 학생만 세는 거야 (s.Grade == 5). LINQ 내부적으로 Where(...).Count()랑 똑같은데, 더 짧고 약간 더 효율적이야.
컬렉션이 비어 있으면?
컬렉션이 비어 있으면 Count는 0을 반환해 — 에러 안 나.
4. 값 합계 구하기: Sum()
모든 점수 합계 구하기
반 전체 점수 합계를 알고 싶으면:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"점수 합계: {sumOfGrades}");
Sum은 셀렉터(람다식)를 받아서 각 요소의 값을 뽑아와.
숫자 배열 합계
그냥 숫자 리스트면 셀렉터 필요 없음:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
흔한 실수와 주의점
컬렉션이 비어 있으면 Sum()은 숫자 타입이면 0을 반환해. 근데 nullable 타입(int?, double?)이면 Sum이 null값은 무시하고 계산해.
5. 평균값: Average()
반 평균 점수 구하기
클래식: 학생 평균 점수는?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"평균 점수: {averageGrade:F2}");
Average는 통계 낼 때 진짜 유용해. 반환값은 항상 double이야, 원래 값이 int여도. 소수점 잘려나가는 거 방지하려고.
요소가 하나도 없으면
컬렉션이 비어 있으면 Average()는 InvalidOperationException 예외를 던져. 이거 진짜 자주 당하는 함정이야: 컬렉션에 뭐가 있는지 확실하지 않으면 미리 체크해!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("평균 계산할 데이터 없음!");
숫자 배열 평균
double avg = numbers.Average();
6. 최대값과 최소값: Max()와 Min()
누가 반 에이스고, 누가 바닥인지
누가 반에서 점수 제일 높고, 누가... 음, 선생님 걱정 끼치는지 알고 싶으면:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"최고 점수: {maxGrade}");
Console.WriteLine($"최저 점수: {minGrade}");
그럼 그 주인공은 누구?
가끔은 점수만 말고, 그 점수 받은 학생 이름이 더 중요하지. 최고 점수 학생 객체 뽑기:
// 점수 내림차순 정렬 후 첫 번째 학생
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"최고 학생: {bestStudent.Name} ({bestStudent.Grade})");
최신 .NET 버전엔 MaxBy도 있어서 더 깔끔하게 할 수 있어. .NET 6부터 쓸 수 있고, .NET 9에선 더 편해졌어 (참고: MaxBy Microsoft Docs). 근데 MaxBy 없어도 정렬 + .First()로 충분히 가능.
함정과 차이점
컬렉션이 비어 있으면 Max()나 Min()도 InvalidOperationException 예외를 던져. (컬렉션 미리 안 체크하면 특히 조심!)
if (students.Any())
Console.WriteLine($"최고 점수: {students.Max(s => s.Grade)}");
else
Console.WriteLine("최대값 찾을 학생 없음.");
7. 집계 메서드 "체이닝" 예시
집계 함수는 필터링, 변환이랑 같이 자주 써:
// 5점 받은 학생 평균 (항상 5지만 예시로!)
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade);
// 4점 이상 학생 점수 합계
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// 유니크 이메일 개수 (혹시 몰라서)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
여기서 LINQ의 진짜 힘이 나와: 연산을 조합해서 코드가 엄청 간결하고 읽기 쉬워져. 네 고양이도 이해할 듯 (고양이가 Junior C# Developer라면).
8. "수작업" 방식과 비교: 왜 집계 함수 쓰는가?
LINQ랑 그냥 for문 돌리는 거랑 비교해보자. 예를 들어 평균 점수 구하기:
일반 방식:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
빈 컬렉션 처리도 LINQ가 훨씬 간단!
9. 유용한 팁
성능과 구현 특징 간단 정리
LINQ 집계 함수는 대부분의 LINQ 연산과 달리 즉시 실행돼! Sum(), Count(), Average(), Max(), Min()을 호출하면 그 순간 컬렉션을 다 돈다는 뜻. 결과는 컬렉션이 아니라 하나의 값이야.
이게 중요한 이유: 집계 함수 전에 무거운 필터나 변환을 했다면, 그 연산은 집계 함수 호출할 때 딱 한 번만 실행돼.
집계 함수도 query-syntax에서 쓸 수 있나?
LINQ 처음 배우면 "이거 query-syntax로도 다 쓸 수 있어?"라고 많이 물어봐. 짧게 말하면: query-syntax엔 집계 함수용 키워드가 없지만, method syntax랑 섞어서 쓰면 돼:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
괄호 안은 query-syntax, 마지막에 집계 함수 메서드 호출. 실제로 이렇게 많이 써!
10. LINQ 쓸 때 흔한 실수
실수 1: Average(), Max(), Min()을 빈 컬렉션에 쓰기.
컬렉션이 비어 있으면 Sum()이랑 Count()는 0을 잘 반환하지만, Average(), Max(), Min()은 예외를 던져. 이 함수들 쓸 땐 요소가 최소 하나 있는지 꼭 확인!
실수 2: 잘못된 시그니처의 람다 넘기기.
예를 들어, 집계 함수(Sum, Max 등)에 숫자 대신 문자열을 넘기면 컴파일 에러 나. 익명 메서드 쓸 때 특히 실수하기 쉬워.
실수 3: 요소 개수 체크 비효율적으로 하기.
Where(...).Count()는 먼저 새 컬렉션 만들고, 그 다음 개수 세. 대신 Count(predicate)를 쓰면 바로 조건에 맞는 개수만 세서 더 빨라.
실수 4: nullable 타입 집계할 때 특징 무시하기.
int?로 합계 구하면 Sum()이 null값은 무시해. 이게 맞는 동작이지만, 미리 생각 안 하면 결과가 예상과 다를 수 있어.
GO TO FULL VERSION