join 절
join 절 (C# 참조)
이번에는 LINQ의 join 절을 살펴볼게요. 객체 모델에서 서로 직접적인 관계가 없는 서로 다른 소스 시퀀스의 요소를 묶을 때 join 절을 사용해요. 두 시퀀스 안의 요소가 서로 같다고 비교할 수 있는 어떤 값을 공유하기만 하면 되는데, 어렵게 들릴 수 있으니 예를 들어볼게요. 어떤 식품 유통업체가 특정 제품의 공급자 목록과 구매자 목록을 따로 들고 있다고 해볼게요. join 절을 쓰면 "같은 지역에 있는" 이 제품의 공급자와 구매자 목록을 한 번에 만들 수 있어요.
join 절은 입력으로 두 개의 소스 시퀀스를 받아요. 각 시퀀스의 요소는 다른 시퀀스의 대응하는 속성과 비교할 수 있는 속성이어야 하거나, 그런 속성을 포함하고 있어야 해요. join 절은 특별한 equals 키워드를 사용해서 지정한 키가 같은지 비교해요. join 절이 수행하는 모든 조인은 동일 조인(equijoin)이에요. join 절의 출력 모양은 수행하는 조인의 종류에 따라 달라지고, 가장 흔한 조인 유형은 다음 세 가지예요.
- 내부 조인(inner join)
- 그룹 조인(group join)
- 왼쪽 우선 조인(left outer join)
내부 조인
아래 예시는 간단한 내부 동일 조인을 보여줘요. 이 쿼리는 "제품 이름 / 범주" 쌍의 평면 시퀀스를 만들어내요. 같은 범주 문자열이 여러 요소에 나타날 수 있고, categories의 어떤 요소에 매칭되는 products가 없다면 그 범주는 결과에 나타나지 않아요.
:::code language="csharp" source="./snippets/join.cs" id="24":::
자세한 내용은 내부 조인 수행 문서를 참고해요.
그룹 조인
into 식이 따라오는 join 절을 그룹 조인이라고 불러요.
:::code language="csharp" source="./snippets/join.cs" id="25":::
그룹 조인은 계층적 결과 시퀀스를 만들어내는데, 왼쪽 소스 시퀀스의 요소를 오른쪽 소스 시퀀스의 하나 이상의 매칭 요소와 연결해요. 그룹 조인은 관계형 용어로는 대응하는 개념이 없어요. 본질적으로 객체 배열들의 시퀀스라고 보면 돼요.
오른쪽 소스 시퀀스에 왼쪽 요소와 매칭되는 요소가 하나도 없다면, join 절은 그 항목에 대해 빈 배열을 만들어내요. 그래서 그룹 조인도 기본적으로는 내부 동일 조인인데, 결과 시퀀스가 그룹으로 묶여 있다는 점만 다르다고 이해하면 돼요.
그룹 조인의 결과를 그냥 선택하면 항목에는 접근할 수 있어도, 이 항목들이 어떤 키로 매칭됐는지는 알 수 없어요. 그래서 앞선 예시처럼 그룹 조인의 결과를 키 이름도 함께 담은 새 형식으로 선택하는 편이 일반적으로 더 유용해요.
물론 그룹 조인의 결과를 또 다른 하위 쿼리의 생성자(제너레이터)로 사용할 수도 있어요.
:::code language="csharp" source="./snippets/join.cs" id="26":::
자세한 내용은 그룹 조인 수행 문서를 참고해요.
왼쪽 우선 조인
왼쪽 우선 조인은 오른쪽 시퀀스에 매칭되는 요소가 없더라도 왼쪽 소스 시퀀스의 모든 요소를 반환해요. LINQ에서 왼쪽 우선 조인을 수행하려면 DefaultIfEmpty 메서드를 그룹 조인과 함께 사용해서, 왼쪽 요소에 매칭되는 요소가 없을 때 만들어낼 기본 오른쪽 요소를 지정해줘요. 참조 형식이라면 기본값으로 null을 쓸 수 있고, 사용자 정의 기본 형식을 지정할 수도 있어요. 아래 예시는 사용자 정의 기본 형식을 보여줘요.
:::code language="csharp" source="./snippets/join.cs" id="27":::
자세한 내용은 왼쪽 우선 조인 수행 문서를 참고해요.
equals 연산자
join 절은 동일 조인을 수행해요. 다시 말하면, 매칭은 두 키의 같음에만 기반할 수 있어요. "크다"나 "같지 않다" 같은 다른 종류의 비교는 지원되지 않아요. 모든 조인이 동일 조인임을 분명히 하기 위해, join 절은 == 연산자 대신 equals 키워드를 사용해요. equals 키워드는 join 절에서만 쓸 수 있고, 중요한 몇 가지 점에서 == 연산자와 달라요.
문자열을 비교할 때 equals는 값으로 비교하는 오버로드가 있는 반면, == 연산자는 참조 같음을 사용해요. 양쪽이 동일한 문자열 변수일 때는 equals와 == 모두 같은 결과, 즉 true를 돌려줘요. 프로그램이 두 개 이상의 동등한 문자열 변수를 선언하면 컴파일러가 그 변수들을 모두 같은 위치에 저장하기 때문이에요. 이를 *인터닝(interning)*이라고 불러요.
또 다른 중요한 차이는 null 비교예요. equals 연산자에서는 null equals null이 false로 평가되는 반면, == 연산자는 true로 평가해요. 마지막으로 범위(스코프) 동작도 달라요. equals에서는 왼쪽 키가 외부 소스 시퀀스를 사용하고, 오른쪽 키가 내부 소스를 사용해요. 외부 소스는 equals 왼쪽에서만, 내부 소스 시퀀스는 오른쪽에서만 범위에 들어와요.
동일 조인이 아닌 조인
여러 개의 from 절을 사용해서 새 시퀀스를 쿼리에 독립적으로 도입하면, 동일 조인이 아닌 조인이나 교차 조인, 그 외 사용자 지정 조인 연산도 수행할 수 있어요. 자세한 내용은 사용자 지정 조인 연산 수행 문서를 참고해요.
객체 컬렉션의 조인 vs. 관계형 테이블의 조인
LINQ 쿼리 식에서는 객체 컬렉션에 조인 연산을 수행해요. 객체 컬렉션을 관계형 테이블 두 개와 완전히 똑같은 방식으로 조인할 수는 없어요. LINQ에서는 두 소스 시퀀스에 관계가 없을 때만 명시적인 join 절이 필요해요.
LINQ to SQL을 다룰 때는 객체 모델이 외래 키 테이블을 기본 테이블의 속성으로 표현해요. 예를 들어 Northwind 데이터베이스에서 Customer 테이블은 Orders 테이블과 외래 키 관계를 가지고 있어요. 이 테이블들을 객체 모델에 매핑하면 Customer 클래스에는 해당 Customer와 연결된 Orders 컬렉션을 담는 Orders 속성이 생겨요. 사실상 조인이 이미 이뤄져 있는 셈이에요.
LINQ to SQL 맥락에서 관련 테이블을 걸쳐 쿼리하는 방법에 대한 자세한 내용은 방법: 데이터베이스 관계 매핑 문서를 참고해요.
복합 키
복합 키를 사용하면 여러 값의 같음을 검사할 수 있어요. 자세한 내용은 복합 키를 사용한 조인 문서를 참고해요. group 절에서도 복합 키를 쓸 수 있어요.
예시
아래 예시는 동일한 데이터 원본에 동일한 매칭 키를 사용해서 내부 조인, 그룹 조인, 왼쪽 우선 조인의 결과를 비교해요. 콘솔 표시에서 결과를 명확히 하기 위해 몇 가지 추가 코드가 들어가 있어요.
:::code language="csharp" source="./snippets/join.cs" id="23":::
설명
into가 뒤따르지 않는 join 절은 xref:System.Linq.Enumerable.Join* 메서드 호출로 변환돼요. into가 뒤따르는 join 절은 xref:System.Linq.Enumerable.GroupJoin* 메서드 호출로 변환돼요.