CodeGym /행동 /C# SELF /그룹 조인 group join 사용하기...

그룹 조인 group join 사용하기

C# SELF
레벨 33 , 레슨 1
사용 가능

1. 소개

고전적인 문제를 상상해봐: 논리적으로 연결된 두 개의 컬렉션이 있어. 예를 들어, 상품 카테고리 리스트와 실제 상품 리스트. 각 카테고리마다 그 카테고리에 속한 모든 상품을 가져와야 해. 또는 회사 부서 리스트와 직원 리스트가 있는데, 각 부서별로 모든 직원을 출력해야 할 때도 있지.

SQL에서는 이걸 "그룹 조인"(GROUP JOIN 또는 더 정확히는 LEFT OUTER JOIN + 그룹핑)이라고 해. LINQ에서는 이걸 위한 특별한 연산자 GroupJoin이 있어. 이건 일반 Join처럼 왼쪽의 각 레코드에 오른쪽의 하나만 붙이는 게 아니라, 키로 그룹핑하는 거랑 중간쯤 되는 느낌이야. GroupJoin은 한 컬렉션의 각 요소를 두 번째 컬렉션에서 연결된 모든 요소 컬렉션 형태로 묶어줘.

비유

일반 Join이 “아빠와 아들”을 성으로 연결해서 쌍을 만드는 거라면, GroupJoin은 각 아빠 밑에 모든 자식 리스트를 붙여서 트리 구조를 만드는 거야.

도식적으로


           카테고리                   상품
        +--------------+         +---------------------+
        | Id | 이름    |         | 이름      | CatId   |
        +----+---------+         +-----------+---------+
        | 1  | 빵      |   --->  | 바톤      | 1       |
        | 2  | 음료    |         | 소시지    | 3       |
        | 3  | 고기    |         | 펩시      | 2       |
        |    |         |         | 차        | 2       |
        +----+---------+         +-----------+---------+

GroupJoin 후:

  • 빵 — [바톤]
  • 음료 — [펩시, 차]
  • 고기 — [소시지]

2. 메서드 시그니처와 기본 개념

확장 메서드


public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                   // "외부" 컬렉션 (예: 카테고리)
    IEnumerable<TInner> inner,                        // "내부" 컬렉션 (예: 상품)
    Func<TOuter, TKey> outerKeySelector,              // 외부 요소에서 키 추출
    Func<TInner, TKey> innerKeySelector,              // 내부 요소에서 키 추출
    Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // 결과 객체/레코드 만드는 팩토리
)
  • outer: 순회할 컬렉션, 여기에 요소들이 붙음 (예: 카테고리).
  • inner: 붙일 요소를 고르는 컬렉션 (예: 상품).
  • outerKeySelector: "왼쪽" 요소에서 키를 반환하는 람다.
  • innerKeySelector: "오른쪽" 요소에서 키를 반환하는 람다.
  • resultSelector: 각 쌍(왼쪽+오른쪽 그룹)에 대해 결과가 어떻게 생길지 정하는 함수.

3. 실전 예제: 카테고리와 상품

예를 들어, 이런 모델이 있다고 해보자:


public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Product
{
    public string Name { get; set; }
    public int CategoryId { get; set; }
}

예제용 컬렉션:


var categories = new List<Category>
{
    new Category { Id = 1, Name = "빵" },
    new Category { Id = 2, Name = "음료" },
    new Category { Id = 3, Name = "고기" }
};

var products = new List<Product>
{
    new Product { Name = "바톤", CategoryId = 1 },
    new Product { Name = "펩시", CategoryId = 2 },
    new Product { Name = "차", CategoryId = 2 },
    new Product { Name = "소시지", CategoryId = 3 }
};

GroupJoin 사용하기 (메서드 문법)


var groupJoin = categories.GroupJoin(
    products,
    category => category.Id,                    // 카테고리 키
    product => product.CategoryId,              // 상품 키
    (category, prods) => new                   // 결과 즉석 생성
    {
        CategoryName = category.Name,
        Products = prods.Select(p => p.Name).ToList() // 이 카테고리의 상품 이름 리스트
    }
);

결과를 순회하는 방법:


foreach (var group in groupJoin)
{
    Console.WriteLine($"카테고리: {group.CategoryName}");
    foreach (var product in group.Products)
    {
        Console.WriteLine($"  - {product}");
    }
}

출력:

카테고리: 빵
  - 바톤
카테고리: 음료
  - 펩시
  - 차
카테고리: 고기
  - 소시지

4. GroupJoin: 쿼리 문법 (Query Syntax)

LINQ는 SQL과 비슷한 문법도 지원해. group join에는 join ... into ... 키워드를 쓰고, 이 쿼리는 위 예제랑 거의 똑같이 동작해.


var groupJoin2 = from c in categories
                 join p in products on c.Id equals p.CategoryId into prodGroup
                 select new
                 {
                     CategoryName = c.Name,
                     Products = prodGroup.Select(p => p.Name).ToList()
                 };

이건 LEFT OUTER JOIN ... GROUP BY가 들어간 SQL 쿼리랑 거의 똑같아.

시각적 도식: GroupJoin 동작 방식


[카테고리]         [상품]           그룹핑 (GroupJoin)

  빵   -------->   바톤          =>   빵:    [바톤]
  음료 -------->  펩시          =>   음료: [펩시, 차]
  음료 -------->  차
  고기   -------->   소시지        =>   고기:    [소시지]

각 카테고리는 자기만의 “주머니”(IEnumerable<Product>)를 받아서, 거기에 해당 카테고리의 모든 상품이 들어가.

5. 특징과 주의할 점

GroupJoin vs. 일반 Join

일반 JoinGroupJoin의 차이는 결과 개수야. Join은 매치마다 한 쌍씩 반환하지만, GroupJoin은 외부 컬렉션의 각 요소마다 하나씩 반환하고, 그 안에 매치된 모든 요소가 컬렉션으로 들어가.

만약 카테고리에 상품이 하나도 없으면, GroupJoin에서는 그 카테고리도 결과에 나오고, 상품 컬렉션만 비어 있어. 이건 SQL의 LEFT OUTER JOIN (왼쪽 외부 조인)과 똑같은 동작이야.

상품이 없는 카테고리 예시:


categories.Add(new Category { Id = 4, Name = "치즈" });

var groupJoin3 = categories.GroupJoin(
    products,
    c => c.Id,
    p => p.CategoryId,
    (c, prods) => new
    {
        CategoryName = c.Name,
        Products = prods.Select(p => p.Name).ToList()
    });

foreach (var group in groupJoin3)
{
    Console.WriteLine($"카테고리: {group.CategoryName}");
    if (group.Products.Count == 0)
        Console.WriteLine("  (상품 없음)");
    else
        foreach (var product in group.Products)
            Console.WriteLine($"  - {product}");
}
카테고리: 빵
  - 바톤
카테고리: 음료
  - 펩시
  - 차
카테고리: 고기
  - 소시지
카테고리: 치즈
  (상품 없음)

이런 시나리오는 비즈니스 앱에서 자주 필요해: 모든 카테고리(혹은 그룹)를 보여줘야 하는데, 어떤 그룹엔 요소가 하나도 없을 수도 있거든.

GroupBy로 구현? 노노!

많은 사람들이 GroupBy 두 번 써서 GroupJoin을 “에뮬레이션”하려고 하는데, 그럴 필요 없어 — GroupJoin이 바로 이걸 위해 만들어졌고, “왼쪽 조인”을 네이티브로 해줘.

6. 실제 데이터로 활용하기

이전 강의에 추가로, 우리 학습용 앱에 “각 카테고리별 상품 리스트” 리포트 출력 기능을 넣어보자. 이런 건 쇼핑몰, CRM, 회계/리포트 시스템에서 자주 필요해.

데모 앱에 코드 추가:


// Category, Product 클래스와 컬렉션이 이미 있다고 가정

Console.WriteLine("카테고리별 상품 리포트:");
var categoryReport = categories.GroupJoin(
    products,
    cat => cat.Id,
    prod => prod.CategoryId,
    (cat, prods) => new
    {
        cat.Name,
        ProductNames = prods.Select(p => p.Name).ToList()
    });

foreach (var row in categoryReport)
{
    Console.WriteLine($"카테고리: {row.Name}");
    if (row.ProductNames.Count == 0)
        Console.WriteLine("  (상품 없음)");
    else
        foreach (var prodName in row.ProductNames)
            Console.WriteLine($"  - {prodName}");
}

7. 중첩 그룹핑과 집계 함수 활용

GroupJoin은 집계 함수랑 조합해서 더 복잡한 리포트도 만들 수 있어.

예시: 각 카테고리별 상품 개수 세기


var reportWithCount = categories.GroupJoin(
    products,
    category => category.Id,
    product => product.CategoryId,
    (category, prods) => new
    {
        Category = category.Name,
        Count = prods.Count()   // 집계 함수!
    });

foreach (var rec in reportWithCount)
{
    Console.WriteLine($"{rec.Category}: {rec.Count}개 상품");
}

컬렉션 categoriesproducts에 이런 데이터가 있다고 해보자:


var categories = new[]
{
    new { Id = 1, Name = "과일" },
    new { Id = 2, Name = "채소" },
    new { Id = 3, Name = "유제품" }
};

var products = new[]
{
    new { Id = 1, Name = "사과", CategoryId = 1 },
    new { Id = 2, Name = "바나나", CategoryId = 1 },
    new { Id = 3, Name = "당근", CategoryId = 2 }
};

콘솔 출력은 이렇게 나와:

카테고리: 과일 — 2개 상품
카테고리: 채소 — 1개 상품
카테고리: 유제품 — 0개 상품

8. GroupJoin과 초보자들이 자주 하는 실수

흔한 실수 — GroupJoin 결과가 일반 Join처럼 플랫 테이블 쌍이라고 기대하는 거야. 여기서 "플랫"이란 각 행이 외부 요소 하나와 매치된 내부 요소 하나로 이루어진 구조(즉, INNER JOIN 후 SQL 테이블처럼)라는 뜻이야.

DB를 좀 만져본 사람들은 GroupJoinLEFT JOIN처럼 동작하길 기대하는데, 실제로는 쌍이 아니라 그룹을 반환해. 즉, 외부 컬렉션의 요소 + 관련된 내부 요소 컬렉션 — 즉, 중첩 구조야.

필요하면 중첩 컬렉션을 "펼쳐서" 써야 해 — 예를 들어, SelectMany로 평평한 쌍 시퀀스를 만들 수 있어.

또 다른 흔한 실수 — 매치가 없는 요소의 그룹이 그냥 빈 리스트라는 걸 잊는 거야. 이건 기본 동작이고, 버그가 아니야 — 근데 이걸 몰라서 출력에 "아무것도 안 나와서" 당황할 수 있어.

GroupJoin을 언제 쓰고, 언제 안 써야 할까?

GroupJoin을 써야 할 때:

  • 두 데이터셋(예: 부서와 직원, 카테고리와 상품)이 있고, 각 “부모”별로 모든 “자식”을 계층적으로 보여줘야 할 때.
  • 복잡한 리포트에서, “자식”이 없어도 모든 메인 요소를 보여줘야 할 때.
  • SQL LEFT OUTER JOIN + 키로 그룹핑이 필요할 때.

그냥 컬렉션을 교집합하거나, 매치마다 한 쌍만 필요하면 GroupJoin 말고 일반 Join을 써.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION