1. 소개
혹시 "클래식" LINQ 쿼리에서 그룹화를 써본 적 있다면, 예를 들어:
var cityCounts = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
이렇게 단순히 그룹별 개수를 세는 것도 코드가 좀 장황하다는 걸 느꼈을 거야. .NET 9이 나오면서 Microsoft 팀이 개발자들의 삶을 좀 더 편하게 해주려고 LINQ에 자주 쓰이는 두 가지 메서드를 추가했어:
- CountBy — 키별로 요소 개수를 빠르게 셀 수 있는 방법이야.
- AggregateBy — 키별로 집계할 수 있는 범용 집계기(단순히 개수만 세는 게 아니라 합계, 기타 등등도 가능).
참고로, 이 메서드들은 커뮤니티에서 요청이 많아서 추가된 거고, MoreLINQ 같은 "고급" LINQ 라이브러리에는 이미 오래전부터 있었던 기능이야.
2. CountBy 메서드: 그룹별 간결한 카운트
설명
CountBy는 말 그대로 "그룹화 후 바로 개수 세기"야. 이 메서드는 "키-개수" 쌍의 컬렉션을 반환하는데, 이건 실제 데이터 분석에서 엄청 자주 쓰여: 인기 도시, 점수별 학생 수, 어떤 게 얼마나 자주 나오는지 등등.
시그니처 (간단히):
IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
실전에서 써보기
예시 1: 도시별 학생 수
예를 들어, 이런 클래스가 있다고 해보자:
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string City { get; set; }
}
그리고 우리 앱에는 학생 리스트가 있어:
var students = new List<Student>
{
new Student { Name = "이반", Grade = 5, City = "네온빌" },
new Student { Name = "안나", Grade = 4, City = "로스-산토스" },
new Student { Name = "이고르", Grade = 3, City = "네온빌" },
new Student { Name = "마리아", Grade = 5, City = "로즈워터" },
new Student { Name = "올렉", Grade = 4, City = "네온빌" }
};
.NET 8이나 그 이전에는 이렇게 써야 했지:
var group = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
foreach (var cityInfo in group)
{
Console.WriteLine($"{cityInfo.City}: {cityInfo.Count} 학생");
}
.NET 9에서는 이걸 한 줄로 끝낼 수 있어:
var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
Console.WriteLine($"{city}: {count} 학생");
}
맞아, 제대로 본 거야 — GroupBy도, Count()도 직접 쓸 필요 없어. 그냥 바로 끝!
예시 2: 점수별 학생 수
학과장이라면 꼭 궁금해할 만한 문제 — 우등생, 보통생이 몇 명인지 알고 싶을 때:
var gradeCounts = students.CountBy(s => s.Grade);
foreach (var (grade, count) in gradeCounts)
{
Console.WriteLine($"점수 {grade}: {count} 학생");
}
예시 3: 문자열에서 문자 빈도수
CountBy는 객체뿐 아니라 더 단순한 것에도 쓸 수 있어:
string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);
foreach (var (letter, count) in charFrequencies)
{
Console.WriteLine($"{letter} : {count}");
}
메모리에서 어떻게 보일까 (그림)
| 키 | Count (CountBy 전) | Count (CountBy 사용) |
|---|---|---|
|
3 | 3 |
|
1 | 1 |
|
1 | 1 |
이런 방식은 코드를 읽기 쉽게 만들고, 그룹화할 때 실수도 줄여줘.
자주 하는 실수와 주의점
CountBy는 (Key, Count) 튜플을 반환한다는 걸 잊지 마. 익명 타입이 아니라서 약간 덜 직관적으로 보일 수도 있지만, 훨씬 명확해. 그리고 Count의 타입은 항상 int로 반환돼 — 직접 타입을 지정할 수는 없어.
3. AggregateBy 메서드: 그룹별 만능 집계
설명
CountBy가 단순 카운트라면, AggregateBy는 진짜 만능 칼이야! 판매자별 매출 합계, 반별 최고 점수, 아니면 뭐든 누적기로 표현할 수 있는 건 다 가능해. 이 모든 걸 AggregateBy가 해줄 수 있어.
시그니처 (아주 간단히):
IEnumerable<(TKey Key, TAccumulate Result)> AggregateBy<TSource, TKey, TAccumulate>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func)
- keySelector — 어떤 기준으로 그룹화할지.
- seed — 누적의 초기값.
- func — 결과를 어떻게 누적할지 정의하는 함수.
실전에서 써보기
예시 1: 도시별 점수 합계
이렇게 복잡한 LINQ 대신:
var sums = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });
이렇게 쓸 수 있어:
var sums = students.AggregateBy(
s => s.City, // 도시별로 그룹화
0, // 초기 합계
(acc, s) => acc + s.Grade // 점수 누적
);
foreach (var (city, sum) in sums)
{
Console.WriteLine($"{city}: 점수 합계 = {sum}");
}
예시 2: 도시별 최고 점수
최대값을 구하고 싶으면 누적기만 바꿔주면 돼:
var maxByCity = students.AggregateBy(
s => s.City,
int.MinValue, // 초기값 — 최소값
(max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
Console.WriteLine($"{city}: 최고 점수 = {maxGrade}");
}
예시 3: 이름을 문자열로 모으기
학생 리스트를 업데이트해보자:
var namesByCity = students.AggregateBy(
s => s.City,
"", // 초기 문자열
(acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);
foreach (var (city, names) in namesByCity)
{
Console.WriteLine($"{city}: {names}");
}
내부 동작 그림 (스키마)
. {Student, Student, Student, ...}
|
| (city로 그룹화)
v
["네온빌"] ["로즈워터"] ["로스 산토스"]
| | |
| (누적) |
|___________________________|
|
v
{("네온빌", 합계), ("로즈워터", 합계), ...}
"예전" 방식과 비교
예전에는 이런 작업을 하려면 GroupBy + Select 그리고 그 안에 Sum, Aggregate 같은 걸 써야 했어. 이제 AggregateBy가 이 "고통"을 숨겨주고 코드를 짧고 명확하게 만들어줘.
실습: 앱에 적용해보기
우리 학습 프로젝트 — 학생 일기장. 여기에 도시별 평균 점수 리포트를 추가해보자.
// 평균 점수는 AggregateBy로 합계와 개수를 누적해서, 나중에 평균을 계산해
var avgByCity = students.AggregateBy(
s => s.City,
(Sum: 0, Count: 0),
(acc, s) => (acc.Sum + s.Grade, acc.Count + 1)
).Select(x => (x.Key, Average: (double)x.Result.Sum / x.Result.Count));
foreach (var (city, avg) in avgByCity)
{
Console.WriteLine($"{city}: 평균 점수 = {avg:F2}");
}
여기서는 합계와 개수를 튜플로 누적하고, 마지막에 합계를 개수로 나눠서 평균을 구해.
특징과 주의할 점
누적기가 참조 타입이면, AggregateBy 내부에서 같은 참조가 그룹 내 모든 반복에 사용되니까, 객체를 변형하지 않도록 주의해야 해. 그렇지 않으면 그룹 전체 결과에 영향을 줄 수 있어.
그리고 그룹화뿐 아니라 결과를 변환하고 싶으면 바로 .Select를 써도 돼. seed (초기값)는 의미에 맞게 잘 골라야 해. 예를 들어 문자열에 0을 쓰면 안 돼.
4. 예전 방식과 새 방식 비교
| 문제 | 예전 LINQ | 새 LINQ .NET 9 |
|---|---|---|
| 도시별 학생 수 | |
|
| 도시별 점수 합계 | |
|
| 도시별 최고 점수 | |
|
| 문자열에서 문자 빈도수 | |
|
실무와 실제 프로젝트에서의 장점
CountBy와 AggregateBy 같은 메서드는 통계 리포트 작성, 데이터 시각화 준비, 피벗 테이블 만들기 등등에서 정말 없어서는 안 될 존재야. 코드가 짧아지고, 읽기 쉬워지고, 무엇보다 신뢰성이 높아져 — 그룹화, 집계 논리에서 실수할 일이 줄어드니까.
주니어/미들급 면접에서 이런 LINQ 신기능을 알고 있으면 플랫폼 발전을 잘 따라가고 있다는 인상을 줄 수 있고, 실제 서비스에서는 직접 복잡한 코드 만들지 않고, 읽기 쉽고 유지보수 쉬운 구조를 쓸 수 있어.
GO TO FULL VERSION