1. 소개
옛날 LINQ 메서드들은 마치 구내식당 밥 같아: "합계", "최소", "평균" 골라서 그냥 지나가는 거지. 근데 가끔은 뭔가 특별한 게 땡길 때가 있잖아? 그럴 때 Aggregate가 등장해 — 마치 네 레시피대로 요리해주는 셰프처럼.
함수형 프로그래밍이랑 비교하면, Aggregate는 Reduce (또는 fold)랑 똑같아: 컬렉션을 하나씩 돌면서 한 값으로 접어가는 거지. 어떻게 접을지는 네가 정하는 거고. 완전 자유, 완전 창의력 발휘!
Aggregate로 풀기 좋은 문제들:
- 복잡한 합계 구하기: 예를 들어 모든 숫자의 곱, 짝수/홀수만의 합, 또는 특별한 규칙으로 합 구하기.
- 문자열 합치기 (예: 짝수/홀수 인덱스마다 다른 구분자로 붙이기 등 직접 로직 짜서).
- 문자열 리포트 만들기 ("Markdown 리스트", HTML, 아무 커스텀 포맷 등).
- 상태가 변하는 컬렉션 만들기 (예: 리스트에서 특정 규칙으로 key를 뽑아 dictionary 만들기).
- 표준 집계 함수로는 안 되는 모든 복잡한 계산.
2. Aggregate 메서드 시그니처와 동작 원리
Microsoft 공식 문서에서 Enumerable.Aggregate를 살펴보자:
public static TAccumulate Aggregate<TSource, TAccumulate>(
this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func
)
여기서 중요한 건:
- source — 네가 집계할 컬렉션.
- seed — "초기값" (시작 누적값이라고 보면 돼).
- func — 두 개 인자 받는 함수: 누적값(acc), 현재 컬렉션 요소, 그리고 새로운 누적값을 반환.
더 간단한 오버로드도 있어:
public static TSource Aggregate<TSource>(
this IEnumerable<TSource> source,
Func<TSource, TSource, TSource> func
)
이 버전에서는 "초기값"이 컬렉션의 첫 번째 요소고, 그 다음부터 func가 두 번째부터 마지막까지 호출돼.
3. Aggregate 기본 예제
학생들한테 살짝 퀴즈: Aggregate로 Sum이나 Product도 만들 수 있다는 거 알고 있었어?
int[] numbers = { 2, 3, 4 };
// 합계 구하기
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = 누적값, val = 다음 요소
// 곱 구하기
int product = numbers.Aggregate((acc, val) => acc * val);
Console.WriteLine(sum); // 9
Console.WriteLine(product); // 24
딱 Sum이나 Multiply랑 비슷하지? 근데 우리가 직접 만드는 거야! 농담 삼아 말하면: Sum으로는 항상 합만 구할 수 있지만, Aggregate로는 합, "반합", "제곱근의 합" 뭐든 가능!
4. Aggregate로 문자열 합치기
모든 문자열을 하나로 붙이고, 콤마로 구분해보자 (마지막에 콤마 안 붙게):
string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// 결과: "C#, LINQ, rocks"
컬렉션이 비어있을 수도 있으면, seed(초기값)가 아주 유용해:
// 빈 문자열로 시작
string report = words.Aggregate(
"기술들: ",
(acc, word) => acc + word + "; ",
acc => acc.TrimEnd(' ', ';') // 마지막에 남는 ";" 제거
);
Console.WriteLine(report); // "기술들: C#; LINQ; rocks"
세 번째 인자 — 결과 변환 함수(result selector) — seed 오버로드에만 있어. 이건 마치 "디저트로 마무리"하는 느낌이지.
5. Aggregate를 실제 앱에서 쓰기
며칠째 만지고 있는 우리 연습용 앱을 떠올려보자. Student 클래스가 있다고 치자:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
학생 리스트:
var students = new List<Student>
{
new Student { Name = "알리사", Grade = 5 },
new Student { Name = "밥", Grade = 4 },
new Student { Name = "바사", Grade = 3 },
new Student { Name = "마리아", Grade = 5 }
};
문제: 이런 문자열을 만들어보자
"최고: 알리사, 마리아"
— 즉 Grade == 5인 학생들만.
초보자들이 보통 이렇게 하지:
var best = "";
foreach (var s in students)
{
if (s.Grade == 5)
best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("최고: " + best);
이제 LINQ 스타일로 Aggregate 써보자:
var bestStr = students
.Where(s => s.Grade == 5)
.Select(s => s.Name)
.Aggregate("최고: ", (acc, name) => acc + name + ", ")
.TrimEnd(',', ' ');
Console.WriteLine(bestStr);
장점: 코드 짧고, 읽기 쉽고. 네 상사가 LINQ를 몰라도 의도는 알 거야 (아니면 적어도 노력은 인정하겠지).
6. 더 복잡한 시나리오
사실 Aggregate의 누적값은 숫자나 문자열 말고도 뭐든 될 수 있어: dictionary, 네가 만든 클래스나 struct도 가능.
예시: 학생 리스트에서 각 점수별 학생 수 세기:
var gradeCounts = students.Aggregate(
new Dictionary<int, int>(),
(dict, student) => {
if (dict.ContainsKey(student.Grade))
dict[student.Grade]++;
else
dict[student.Grade] = 1;
return dict;
}
);
// 출력용:
foreach (var pair in gradeCounts)
{
Console.WriteLine($"점수 {pair.Key}: {pair.Value}명");
}
이 방식은 사실 GroupBy를 수동으로 구현한 거야. 그냥 GroupBy 쓰면 안 되냐고? 가끔은 LINQ 기본 메서드로 안 되는 특이한 집계가 필요할 때가 있어. 예를 들어 "바사"는 빼고 합치기, 리포트용으로 따로 가공하기 등.
7. 시각화: Aggregate 동작 방식 (플로우차트)
배열 { 2, 4, 3 }이 있고, 합계를 누적한다고 해보자:
acc: 2 (첫 번째 요소)
|
v
val: 4
acc = acc + val = 2 + 4 = 6
|
v
val: 3
acc = acc + val = 6 + 3 = 9
|
v
[모든 요소 처리 끝]
|
v
결과: 9
seed 오버로드도 비슷해:
seed: 0
|
v
val: 2
acc = 0 + 2 = 2
|
v
val: 4
acc = 2 + 4 = 6
|
v
val: 3
acc = 6 + 3 = 9
|
v
결과: 9
Aggregate와 다른 집계 메서드 비교
| 메서드 | 기본 동작 | 유연성 | 빈 컬렉션 처리 | 예시 |
|---|---|---|---|---|
|
숫자 합계 | 낮음 | 0 반환 | |
|
요소 개수 | 낮음 | 0 반환 | |
|
아무 계산 | 높음 | seed 필요 | |
|
문자열 합치기 | 중간 | 빈 배열이면 "" | |
8. 실전 팁, 흔한 실수, 주의할 점
Aggregate는 유연한 만큼, 잘못 쓰면 예상치 못한 결과가 나올 수 있어. 자주 하는 실수들:
프로그래머들이 seed(초기값)를 깜빡하고, 컬렉션이 비었을 때 seed 없는 오버로드를 쓰면 예외(InvalidOperationException)가 터진다는 걸 잊곤 해. 그래서 빈 컬렉션에는 seed 오버로드를 써:
var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // 잘 동작! 0 반환
문자열 누적할 때 Aggregate 쓰면, 마지막에 구분자(예: ",")가 남기 쉬워. .TrimEnd(',', ' ')로 잘라주거나, 단순히 문자열 합치기면 string.Join을 쓰는 게 더 나아.
변경 가능한 타입(예: List나 Dictionary)을 누적값으로 쓸 때는 조심: in-place로 바꾸면 모든 단계에서 같은 객체를 참조하게 돼. 병렬 연산이나 복사 기대할 때 이상한 결과가 나올 수 있어. 그래서 함수형 스타일로는 매 단계마다 새 객체를 반환하는 게 좋아.
다른 사람이 볼 코드라면, "멋있어 보이려고" Aggregate를 남발하지 마! 초보자한테는 foreach나 기본 집계가 훨씬 쉽거든. 단순한 건 Sum, Count, Join 쓰고, "특별한 텍스트 조합"이 필요할 때만 Aggregate를 써!
9. 실무, 면접, 프레임워크와의 연관성
실제 업계에서는 Aggregate가 특이한 계산이나 데이터 접기, 예를 들어 복잡한 리포트, 통계, 그래프, 해시 계산, 고유 id 생성, 컬렉션에서 UI 컴포넌트 만들기 등에 자주 쓰여.
면접에서는 LINQ 관련 질문이 꼭 나와: "배열 요소 합계 어떻게 구해?", "문자열 리스트를 한 줄로 합치려면?", "고유 요소 개수 세려면?" — LINQ, 특히 Aggregate로 푸는 걸 기대하지. 창의적인 문제도 나와: "LINQ로 짝수의 제곱합을 구하고, 그 과정을 설명하는 문자열을 반환해볼래?"
Entity Framework, Dapper, RavenDB 같은 유명 .NET 라이브러리/프레임워크에서는 Aggregate가 DB 쿼리에는 잘 안 쓰이지만, 코드 레벨에서 메모리 집계에는 아주 유용하게 쓰여.
GO TO FULL VERSION