숏컷 코드
// Join + GroupBy + projection
var result = players
.Join(
teams,
player => player.TeamId,
team => team.Id,
(player, team) => new { player.Name, TeamName = team.Name })
.GroupBy(x => x.TeamName)
.Select(group => new
{
Team = group.Key,
Members = group.Select(x => x.Name).ToList(),
Count = group.Count()
});문법
어떤 LINQ 연산을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 키 기준으로 묶기 | GroupBy |
| 두 시퀀스를 키로 연결 | Join |
| 매칭 없는 외부 항목도 유지 | GroupJoin |
| 중첩 컬렉션 펼치기 | SelectMany |
| 결과 모양 바꾸기 | Select projection |
GroupBy — IGrouping 시퀀스
GroupBy 는 키 기준으로 항목을 묶어 IEnumerable<IGrouping<TKey, TElement>> 를 반환합니다. 각 그룹(IGrouping)은 Key와 해당 키에 속하는 항목들의 시퀀스입니다.
// IGrouping<string, Player> 타입의 그룹 시퀀스
var byClass = players.GroupBy(p => p.Class);
foreach (var group in byClass)
{
Console.WriteLine($"직업: {group.Key}"); // Key
foreach (var p in group)
Console.WriteLine($" {p.Name}: {p.Score}"); // 그룹 항목들
}
// 집계 — 각 그룹의 통계
var stats = players
.GroupBy(p => p.Class)
.Select(g => new
{
Class = g.Key,
Count = g.Count(),
MaxScore = g.Max(p => p.Score),
AvgScore = g.Average(p => p.Score)
});주의: GroupBy는 지연 실행이지만 실제 그룹 접근 시 전체 시퀀스를 한 번 순회합니다. ToList() 후 GroupBy하는 것과 순서의 차이는 있지만 결과는 동일합니다.
// ❌ Count 계산 후 다시 같은 그룹을 또 순회
var data = players
.GroupBy(p => p.Class)
.Select(g => new
{
Count = g.Count(),
Names = g.Select(x => x.Name).ToList()
});
// ✅ 그룹 결과를 한 번 확정해 재사용
var data = players
.GroupBy(p => p.Class)
.Select(g =>
{
var members = g.ToList();
return new
{
Count = members.Count,
Names = members.Select(x => x.Name).ToList()
};
});Join — 해시 테이블 기반 연결
Join 은 두 시퀀스를 키 기준으로 연결합니다. 내부적으로 두 번째 시퀀스를 해시 테이블로 만들어 첫 번째 시퀀스의 각 항목과 O(1)으로 매칭합니다.
// inner join — 양쪽에 모두 있는 항목만
var result = players.Join(
teams,
player => player.TeamId, // outer key
team => team.Id, // inner key
(player, team) => new // 결과 형태
{
PlayerName = player.Name,
TeamName = team.Name
});
// GroupJoin — left outer join 효과
var withTeam = players.GroupJoin(
teams,
p => p.TeamId,
t => t.Id,
(player, teamGroup) => new
{
player.Name,
Team = teamGroup.FirstOrDefault()?.Name ?? "팀 없음"
});SelectMany — 중첩 컬렉션 평탄화
SelectMany 는 각 항목에서 컬렉션을 꺼내 하나의 평탄한 시퀀스로 만듭니다. SQL의 CROSS JOIN과 유사합니다.
// 각 팀의 멤버 목록을 하나의 시퀀스로
var allPlayers = teams.SelectMany(team => team.Members);
// 두 컬렉션의 카르테시안 곱
var combinations = teams.SelectMany(
team => players,
(team, player) => new { team.Name, player.Score });Projection — 결과 모양 재설계
Select 로 결과 타입을 재설계합니다. 익명 타입은 같은 메서드 안에서만 쓸 수 있고, 여러 계층에서 공유하려면 record나 class를 씁니다.
// 익명 타입 — 로컬 사용
var local = players.Select(p => new { p.Name, p.Score });
// record — API 반환이나 계층 간 전달
var dtos = players.Select(p => new PlayerDto(p.Name, p.Score)).ToList();
// 기존 record/class와 조합
record PlayerDto(string Name, int Score);체크포인트
| 연산 | 역할 | 내부 동작 |
|---|---|---|
GroupBy | 키 기준 그룹화 | 전체 시퀀스 1회 순회 |
Join | 키 기준 두 시퀀스 연결 | 내부 시퀀스 해시 테이블 |
GroupJoin | Left outer join | 매칭 없는 항목도 포함 |
SelectMany | 중첩 컬렉션 평탄화 | 각 항목의 컬렉션을 이어붙임 |
Select + 익명 타입 | 로컬 projection | 메서드 범위 내에서만 사용 가능 |
Select + record | 계층 간 projection | API 반환, DTO 변환 |
주의할 점
LINQ 체인이 길어질수록 현재 타입 추적이 어렵습니다. Join 후 GroupBy 하면 타입이 두 번 바뀝니다. IDE의 타입 힌트를 활용하거나, 중간 결과를 명시적 타입 변수에 저장해 가독성을 높이세요.
GroupBy는 지연 실행이지만 그룹 항목에 두 번 접근하면 두 번 계산됩니다. group.Count()와 group.Select(...) 를 함께 쓴다면 ToList()로 한 번 확정하는 편이 좋습니다.
참고 링크
2 sources