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 비교
| 연산 | 결과 | 사용 시나리오 |
|---|---|---|
|
평면 쌍 리스트 | 클래식 SQL JOIN. 각 쌍(매칭)이 별도의 행이야. |
|
하위 컬렉션이 있는 그룹 | "고객 + 그 사람의 모든 주문"을 "일대다" 구조로 얻고 싶을 때. |
초보자들은 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에서는 GroupJoin과 SelectMany 조합으로 구현해. (이건 다음 강의에서 다룰 거야) 기본 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은 매칭되는 쌍만 결과에 포함시키고, 매칭 없는 요소는 포함 안 해.
GO TO FULL VERSION