CodeGym /행동 /C# SELF /컬렉션 합치기 join (

컬렉션 합치기 join ( Join)로

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

1. 소개

가끔 두 개의 다른 컬렉션을 다뤄야 할 때가 있는데, 이 둘이 어떤 공통된 특징으로 연결되어 있을 때가 있어. 예를 들어, 주문 리스트랑 따로 있는 고객 컬렉션이 있다고 해보자. 어떤 주문이 어떤 고객의 것인지 어떻게 알 수 있을까? 이럴 땐 두 컬렉션이 공통된 식별자 — 예를 들면 userId — 로 연결되어 있어야 해.

관계형 데이터베이스에서는 이런 문제를 JOIN 연산으로 해결하는데, 이건 서로 다른 테이블의 행을 키가 맞는 걸로 합쳐주는 거야. LINQ에도 똑같은 연산자가 있는데, 이름도 Join이야.

두 개의 테이블을 상상해봐: 첫 번째엔 도서관 회원 리스트랑 그들의 식별자가 있고, 두 번째엔 책 주문 리스트가 있는데, 각 주문마다 id로 회원이 지정되어 있어. join을 쓰면 이 테이블들을 id로 "붙여서", 결과로 "회원 + 그 사람의 주문" 쌍을 얻을 수 있지.

참고로, join이 "데이터베이스 전용"이라고 생각했다면, 그건 오해야! 프로그래밍에서 컬렉션 합치는 일은 진짜 자주 나오고, 특히 외부 시스템이나 구조화된 파일(JSON, XML, 그리고 회계에서 받은 Excel 표 같은 거) 다룰 때 꼭 필요해.

2. Join 메서드 시그니처와 동작 원리

Join 메서드는 이렇게 생겼어:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // 첫 번째 컬렉션 (외부)
    IEnumerable<TInner> inner,                      // 두 번째 컬렉션 (내부)
    Func<TOuter, TKey> outerKeySelector,            // 외부 컬렉션에서 키 뽑는 방법
    Func<TInner, TKey> innerKeySelector,            // 내부 컬렉션에서 키 뽑는 방법
    Func<TOuter, TInner, TResult> resultSelector)   // 결과(새 요소) 만드는 함수

처음 보면 좀 복잡해 보이지만, 곧 쉽게 이해할 수 있어.
동작 방식:
첫 번째 컬렉션(outer)의 각 요소마다 LINQ가 두 번째(inner)에서 키가 맞는 요소를 찾아. 키가 맞으면 resultSelector가 호출되고, 그 결과가 최종 컬렉션에 들어가.

3. 예시: 고객과 주문 합치기

클래스랑 컬렉션을 몇 개 만들어보자. 이건 우리가 수업 중에 만들고 있는 샵(가게) 앱의 논리적 연장선이야.


public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Product { get; set; } = "";
}

// 고객 컬렉션
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "바샤" },
    new Customer { Id = 2, Name = "페챠" },
    new Customer { Id = 3, Name = "마샤" },
};

// 주문 컬렉션
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "책" },
    new Order { Id = 102, CustomerId = 1, Product = "펜" },
    new Order { Id = 103, CustomerId = 2, Product = "노트" },
    new Order { Id = 104, CustomerId = 3, Product = "지우개" },
};

이제 모든 고객과 그들의 주문을 얻고 싶어. 예를 들어: "페챠가 책을 주문함", "페챠가 노트를 주문함" 이런 식으로 출력하고 싶지.

Join 적용하기


// customer.Id와 order.CustomerId로 고객과 주문 합치기
var query = customers.Join(
    orders,
    customer => customer.Id,          // 고객에서 키 뽑는 방법
    order => order.CustomerId,        // 주문에서 키 뽑는 방법
    (customer, order) => new          // 키가 맞는 쌍으로 새 객체 만들기
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// 결과 출력
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName}가 {item.Product}를 주문함");
}

출력 결과:

바샤가 펜를 주문함
페챠가 책를 주문함
페챠가 노트를 주문함
마샤가 지우개를 주문함

참고: 고객이 주문이 여러 개면, 주문마다 한 번씩 리스트에 나와. 이게 바로 정상 동작이야!

4. 표: Join vs GroupBy + SelectMany 비교

연산 결과 사용 시나리오
Join
평면 쌍 리스트 클래식 SQL JOIN. 각 쌍(매칭)이 별도의 행이야.
GroupBy + SelectMany
하위 컬렉션이 있는 그룹 "고객 + 그 사람의 모든 주문"을 "일대다" 구조로 얻고 싶을 때.

초보자들은 Join을 "고객 → 주문 리스트" 구조가 필요할 때도 쓰는데, Join은 행 단위로 동작해서 데이터를 그룹화하지 않아. 이럴 땐 GroupJoin을 쓰거나, GroupBy를 써야 해. (다음 강의에서 더 자세히 다룰 거야)

5. Query Syntax (SQL 스타일 LINQ)에서 Join 쓰기

LINQ에는 두 가지 문법이 있어: 메서드 체이닝(Method Syntax, 예: Join(...))이랑, SQL 비슷한 스타일(Query Syntax)이야.

특히 데이터베이스 경험이 있는 개발자들은 이 스타일이 더 직관적일 수 있어:


var query2 =
    from customer in customers
    join order in orders
        on customer.Id equals order.CustomerId
    select new
    {
        CustomerName = customer.Name,
        Product = order.Product
    };

foreach (var item in query2)
{
    Console.WriteLine($"{item.CustomerName}가 {item.Product}를 주문함");
}

참고:
Query Syntax에서는 equals 키워드를 써야 해 — == 연산자는 여기서 안 먹혀!
이거 신입 면접에서 자주 나오는 함정이야 😉

6. 중요한 디테일과 함정

가끔 컬렉션의 모든 요소가 결과에 안 들어가는 경우가 있어. 그 이유는 Join 메서드가 "inner join"만 구현하기 때문이야. 즉, 키가 맞는 요소만 합쳐져. 만약 어떤 고객이 주문이 없으면, 그 고객은 결과에 안 나와. 반대로, CustomerId가 있지만 고객 컬렉션에 없는 주문도 결과에 안 들어가.

근데 만약 주문이 없는 고객도 다 보고 싶으면? 이런 경우엔 "left join"이 필요한데, LINQ에서는 GroupJoinSelectMany 조합으로 구현해. (이건 다음 강의에서 다룰 거야) 기본 Join에서는 양쪽에 다 요소가 있어야 매칭이 되고, 없으면 결과에서 빠져.

7. 여러 키로 합치기 (composite key)

가끔 한 개가 아니라 여러 필드로 컬렉션을 합쳐야 할 때가 있어. 예: "상품코드 + 판매연도"로 상품과 판매를 연결하기.

LINQ에서는 익명 객체를 키로 만들어서 해결해:


var sales = ...; // 판매
var products = ...; // 상품

var query = products.Join(
    sales,
    prod => new { prod.Code, prod.Year },
    sale => new { sale.ProductCode, sale.Year },
    (prod, sale) => new { prod.Name, sale.Amount }
);

여기서 중요한 건, 익명 객체의 타입과 프로퍼티 이름이 똑같아야 해. 다르면, 값이 같아도 매칭이 안 돼.

8. 합쳐진 컬렉션은 어떻게 생겼나 — 도식

join 동작을 보여주는 아주 간단한 도식이야:


flowchart LR
    subgraph Customers
        A1["바샤 (Id=1)"]
        A2["페챠 (Id=2)"]
        A3["마샤 (Id=3)"]
    end
    subgraph Orders
        B1["펜 (CustomerId=1)"]
        B2["책 (CustomerId=2)"]
        B3["노트 (CustomerId=2)"]
        B4["지우개 (CustomerId=3)"]
    end

    A1-->|Id=1|B1
    A2-->|Id=2|B2
    A2-->|Id=2|B3
    A3-->|Id=3|B4
고객과 주문을 키로 연결한 도식

이 도식에서 볼 수 있듯, 각 고객은 키가 맞는 모든 주문과 연결돼.

9. 실수와 특징 요약

가장 흔한 실수 중 하나는 파라미터 순서를 헷갈리는 거야. 특히 두 컬렉션의 키 타입이 같으면 더더욱. 이럴 때 컴파일러나 LINQ가 에러를 안 내주고, 결과가 비거나 이상하게 나올 수 있어.

또 하나 자주 나오는 함정은 Join으로 "left join"을 하려고 하는 거야. 즉, 주문이 없는 고객도 결과에 남기고 싶을 때. 하지만 기본 Join은 매칭되는 쌍만 결과에 포함시키고, 매칭 없는 요소는 포함 안 해.

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