Join LINQ의 작업

두 데이터 소스를 조인하는 것은 한 데이터 소스의 개체를 공통 특성을 공유하는 다른 데이터 소스의 개체와 연결하는 것입니다.

서로 간의 관계를 직접 적용할 수 없는 데이터 원본을 대상으로 하는 쿼리에서는 Join이 중요한 작업입니다. 개체 지향 프로그래밍에서 조인한다는 것은 모델링되지 않은 개체 간에 상관 관계가 있음을 의미할 수 있습니다(예: 단방향 관계에서 반대 방향을 사용). 단방향 관계의 예로는 Student 클래스가 주를 나타내는 Department 형식 속성을 포함하는데 Department 클래스는 Student 개체의 컬렉션인 속성을 포함하지 않는 경우를 들 수 있습니다. Department 개체 목록이 있는 경우 각 부서의 모든 학생을 찾으려면 조인 작업을 사용하면 됩니다.

LINQ 프레임워크에 제공되는 조인 메서드는 JoinGroupJoin입니다. 이러한 메서드는 키가 같은지 여부에 따라 두 데이터 소스의 일치 여부를 확인하는 조인인 동등 조인을 수행합니다. 비교를 위해 Transact-SQL에서는 equals이 아닌 less than 연산자와 같은 조인 연산자를 지원합니다. 관계형 데이터베이스 용어에서 Join은 내부 조인을 구현합니다. 내부 조인은 다른 데이터 집합에 일치하는 항목이 있는 개체만 반환하는 유형의 조인입니다. GroupJoin 메서드에는 관계형 데이터베이스 측면에 직접 상응하는 기능이 없지만 내부 조인 및 왼쪽 우선 외부 조인의 상위 집합을 구현합니다. 왼쪽 우선 외부 조인은 다른 데이터 소스에 서로 관련된 요소가 없더라도 첫 번째(왼쪽) 데이터 소스의 각 요소를 반환하는 조인입니다.

다음 그림에서는 내부 조인 또는 왼쪽 우선 외부 조인에 포함된 두 집합 및 해당 집합 내의 요소를 개념적으로 보여줍니다.

Two overlapping circles showing inner/outer.

메서드

메서드 이름 설명 C# 쿼리 식 구문 추가 정보
Join 키 선택기 함수를 기준으로 두 시퀀스를 Join한 다음 값 쌍을 추출합니다. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin 키 선택기 함수를 기준으로 두 시퀀스를 Join한 다음 결과로 생성된 일치 항목을 요소마다 그룹화합니다. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

이 문서의 다음 예제에서는 이 영역에 대한 공통 데이터 원본을 사용합니다.

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}
public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

Student 점수에는 성적 수준, 기본 부서 및 일련의 점수가 있습니다. Teacher에는 City 교사가 수업을 하는 캠퍼스를 식별하는 속성도 있습니다. Department에는 부서장 역할을 하는 Teacher에 대한 참조와 이름이 있습니다.

다음 예제에서는 join … in … on … equals … 절을 사용하여 특정 값을 기준으로 두 시퀀스를 조인합니다.

var query = from student in students
            join department in departments on student.DepartmentID equals department.ID
            select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

위의 쿼리는 다음 코드와 같이 메서드 구문을 사용하여 표현할 수 있습니다.

var query = students.Join(departments,
    student => student.DepartmentID, department => department.ID,
    (student, department) => new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name });

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

다음 예제에서는 join … in … on … equals … into … 절을 사용하여 특정 값을 기준으로 두 시퀀스를 조인하고 각 요소에 대해 결과 일치 항목을 그룹화합니다.

IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
                    join student in students on department.ID equals student.DepartmentID into studentGroup
                    select studentGroup;

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

위의 쿼리는 다음 예제와 같이 메서드 구문을 사용하여 표현할 수 있습니다.

// Join department and student based on DepartmentId and grouping result
IEnumerable<IEnumerable<Student>> studentGroups = departments.GroupJoin(students,
    department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => studentGroup);

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

내부 조인 수행

관계형 데이터베이스 용어에서 내부 조인은 첫 번째 컬렉션의 각 요소가 두 번째 컬렉션에서 일치하는 모든 요소에 대해 한 번 표시되는 결과 집합을 생성합니다. 첫 번째 컬렉션의 요소에 일치하는 요소가 없는 경우에는 결과 집합에 표시되지 않습니다. C#에서 join 절에 의해 호출되는 Join 메서드는 내부 조인을 구현합니다. 다음 예제에서는 내부 조인의 네 가지 변형을 수행하는 방법을 보여 줍니다.

  • 단순 키에 따라 두 데이터 소스의 요소를 상호 연결하는 간단한 내부 조인
  • 복합 키에 따라 두 데이터 소스의 요소를 상호 연결하는 내부 조인. 둘 이상의 값으로 구성된 키인 복합 키를 사용하면 둘 이상의 속성에 따라 요소를 상호 연결할 수 있습니다.
  • 연속 조인 작업이 서로 추가되는 여러 조인
  • 그룹 조인을 사용하여 구현되는 내부 조인

단일 키 조인

다음 예제에서는 해당 TeacherIdTeacher가 일치하는 DeparmentTeacher와 일치시킵니다. C#의 select 절은 결과 개체의 모양을 정의합니다. 다음 예제에서 결과 개체는 부서 이름과 부서를 이끄는 교사의 이름으로 구성된 익명 형식입니다.

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

Join 메서드 구문을 사용하여 동일한 결과를 얻을 수 있습니다.

var query = teachers
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
        (teacher, department) =>
        new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

부서장이 아닌 교사는 최종 결과에 나타나지 않습니다.

복합 키 조인

속성 하나만 기준으로 요소를 상호 연결하는 대신 복합 키를 사용하여 여러 속성을 기준으로 요소를 비교할 수 있습니다. 비교하려는 속성으로 구성된 무명 형식을 반환할 각 컬렉션에 대한 키 선택기 함수를 지정합니다. 속성에 레이블을 지정하는 경우 각 키의 무명 형식에 동일한 레이블이 있어야 합니다. 또한 속성은 동일한 순서로 나타나야 합니다.

다음 예제에서는 Teacher 개체 목록과 Student 개체 목록을 사용하여 학생이기도 한 교사를 확인합니다. 이러한 두 형식에는 각 사람의 이름과 성을 나타내는 속성이 있습니다. 각 목록의 요소에서 조인 키를 만드는 함수는 속성으로 구성된 무명 형식을 반환합니다. 조인 작업은 이러한 복합 키가 같은지 비교하고 각 목록에서 이름과 성이 둘 다 일치하는 개체 쌍을 반환합니다.

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

string result = "The following people are both teachers and students:\r\n";
foreach (string name in query)
{
    result += $"{name}\r\n";
}
Console.Write(result);

다음 예제와 같이 Join 메서드를 사용할 수 있습니다.

IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

Console.WriteLine("The following people are both teachers and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

여러 조인

개수에 제한없이 조인 작업을 서로 추가하여 여러 조인을 수행할 수 있습니다. C#의 각 join 절은 지정된 데이터 소스를 이전 조인의 결과와 상호 연결합니다.

첫 번째 join 절은 개체와 Department 개체의 ID와 일치하는 Student 개체의 DepartmentID에 따라 학생과 부서를 일치시킵니다. Student 개체 및 Department 개체를 포함하는 무명 형식의 시퀀스를 반환합니다.

두 번째 join 절은 부서장 ID와 일치하는 해당 교사의 ID를 토대로 첫 번째 조인에서 반환된 익명 형식과 Teacher 개체 간의 상관 관계를 지정합니다. 학생 이름, 부서 이름 및 부서장 이름을 포함하는 익명 형식의 시퀀스를 반환합니다. 이 작업은 내부 조인이기 때문에 두 번째 데이터 원본에 일치 항목이 있는 첫 번째 데이터 원본의 개체만 반환됩니다.

// The first join matches Department.ID and Student.DepartmentID from the list of students and
// departments, based on a common ID. The second join matches teachers who lead departments
// with the students studying in that department.
var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

여러 Join 메서드를 사용하는 동등한 방법은 익명 형식과 동일한 방법을 사용합니다.

var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

그룹화된 조인을 사용하는 내부 조인

다음 예제에서는 그룹 조인을 사용하여 내부 조인을 구현하는 방법을 보여 줍니다. Department 개체 목록은 Student.DepartmentID 속성과 일치하는 Department.ID를 기준으로 Student 개체 목록에 그룹 조인됩니다. 그룹 조인은 각 그룹이 Department 개체 및 일치하는 Student 개체 시퀀스로 구성된 중간 그룹 컬렉션을 만듭니다. 두 번째 from 절은 이 시퀀스를 하나의 긴 시퀀스로 결합하거나 평면화합니다. select 절은 최종 시퀀스의 요소 형식을 지정합니다. 이 형식은 학생 이름과 일치하는 부서 이름으로 구성된 익명 형식입니다.

var query1 =
    from department in departments
    join student in students on department.ID equals student.DepartmentID into gj
    from subStudent in gj
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
    };
Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

다음과 같이 GroupJoin 메서드를 사용하여 동일한 결과를 달성할 수 있습니다.

var queryMethod1 = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, gj) => new { department, gj })
    .SelectMany(departmentAndStudent => departmentAndStudent.gj,
        (departmentAndStudent, subStudent) => new
        {
            DepartmentName = departmentAndStudent.department.Name,
            StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
        });

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in queryMethod1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

결과는 into 절 없이 join 절을 사용해 내부 조인을 수행하여 얻은 결과 집합과 같습니다. 다음 코드는 이와 동등한 쿼리를 보여 줍니다.

var query2 = from department in departments
    join student in students on department.ID equals student.DepartmentID
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    };

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

체인을 방지하기 위해 다음과 같이 단일 Join 메서드를 사용할 수 있습니다.

var queryMethod2 = departments.Join(students, departments => departments.ID, student => student.DepartmentID,
    (department, student) => new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    });

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in queryMethod2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

그룹화 조인 수행

그룹 조인은 계층적 데이터 구조를 생성하는 데 유용합니다. 첫 번째 컬렉션의 각 요소와 두 번째 컬렉션에서 상관 관계가 지정된 요소 집합을 쌍으로 구성합니다.

참고 항목

첫 번째 컬렉션의 각 요소는 상관 관계가 지정된 요소가 두 번째 컬렉션에 있는지 여부에 관계없이 그룹 조인의 결과 집합에 표시됩니다. 상관 관계가 지정된 요소가 없는 경우 해당 요소에 대해 상관 관계가 지정된 요소의 시퀀스가 비어 있습니다. 따라서 결과 선택기에서 첫 번째 컬렉션의 모든 요소에 액세스할 수 있습니다. 이는 두 번째 컬렉션에 일치 항목이 없는 첫 번째 컬렉션의 요소에 액세스할 수 없는 비그룹 조인의 결과 선택기와 다릅니다.

Warning

Enumerable.GroupJoin에는 기존 관계형 데이터베이스 용어에 직접적으로 해당하는 항목이 없습니다. 그러나 이 메서드는 내부 조인 및 왼쪽 우선 외부 조인의 상위 집합을 구현합니다. 이러한 작업은 모두 그룹화된 조인과 관련하여 작성할 수 있습니다. 자세한 내용은 Entity Framework Core, GroupJoin을 참조하세요.

이 문서의 첫 번째 예제에서는 그룹 조인을 수행하는 방법을 보여 줍니다. 두 번째 예제에서는 그룹 조인을 사용하여 XML 요소를 만드는 방법을 보여 줍니다.

그룹 조인

다음 예제에서는 Student.DepartmentID 속성과 일치하는 Deoartment.ID에 따라 DepartmentStudent 형식 개체의 그룹 조인을 수행합니다. 각 일치 항목에 대한 요소 쌍을 생성하는 비그룹 조인과 달리 그룹 조인은 첫 번째 컬렉션의 각 요소에 대해 하나의 결과 개체(이 예제에서는 Department 개체)를 생성합니다. 두 번째 컬렉션의 해당 요소(이 예제에서는 Student 개체)는 컬렉션으로 그룹화됩니다. 마지막으로, 결과 선택기 함수는 Department.NameStudent 개체 컬렉션으로 구성된 각 일치 항목에 대해 무명 형식을 만듭니다.

var query = from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new
    {
        DepartmentName = department.Name,
        Students = studentGroup
    };

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

위의 예제에서 query 변수에는 각 요소가 부서의 이름과 해당 부서에서 학습하는 학생의 컬렉션을 포함하는 익명 형식인 목록을 만드는 쿼리가 포함되어 있습니다.

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

var query = departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, Students) => new { DepartmentName = department.Name, Students });

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

XML을 만들기 위한 그룹 조인

그룹 조인은 LINQ to XML을 사용하여 XML을 만드는 데 적합합니다. 다음 예제는 무명 형식을 만드는 대신 결과 선택기 함수가 조인된 개체를 나타내는 XML 요소를 만든다는 점을 제외하고 앞의 예제와 비슷합니다.

XElement departmentsAndStudents = new("DepartmentEnrollment",
    from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new XElement("Department",
        new XAttribute("Name", department.Name),
        from student in studentGroup
        select new XElement("Student",
            new XAttribute("FirstName", student.FirstName),
            new XAttribute("LastName", student.LastName)
        )
    )
);

Console.WriteLine(departmentsAndStudents);

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

XElement departmentsAndStudents = new("DepartmentEnrollment",
    departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, Students) => new XElement("Department",
            new XAttribute("Name", department.Name),
            from student in Students
            select new XElement("Student",
                new XAttribute("FirstName", student.FirstName),
                new XAttribute("LastName", student.LastName)
            )
        )
    )
);

Console.WriteLine(departmentsAndStudents);

왼쪽 우선 외부 조인 수행

왼쪽 우선 외부 조인은 두 번째 컬렉션에 상호 연결된 요소가 있는지 여부에 관계없이 첫 번째 컬렉션의 각 요소가 반환되는 조인입니다. LINQ를 통해 그룹 조인의 결과에서 DefaultIfEmpty 메서드를 호출하여 왼쪽 우선 외부 조인을 수행할 수 있습니다.

다음 예제에서는 그룹 조인의 결과에서 DefaultIfEmpty 메서드를 사용하여 왼쪽 우선 외부 조인을 수행하는 방법을 보여 줍니다.

두 컬렉션의 왼쪽 우선 외부 조인을 생성하는 첫 번째 단계는 그룹 조인을 사용하여 내부 조인을 수행하는 것입니다. 이 프로세스에 대한 설명은 내부 조인 수행을 참조하세요. 이 예제에서 Department 개체 목록은 학생의 DepartmentID와 일치하는 Department 개체의 ID를 기준으로 Student 개체 목록에 내부 조인됩니다.

두 번째 단계는 오른쪽 컬렉션에 일치하는 항목이 없는 경우에도 첫 번째(왼쪽) 컬렉션의 각 요소를 결과 집합에 포함하는 것입니다. 이렇게 하려면 그룹 조인에서 일치하는 요소의 각 시퀀스에 대해 DefaultIfEmpty를 호출합니다. 이 예제에서는 일치하는 Student 개체의 각 시퀀스에서 DefaultIfEmpty를 호출합니다. 메서드는 Department 개체에 대해 일치하는 Student 개체의 시퀀스가 비어 있는 경우 단일 기본값을 포함하는 컬렉션을 반환하여 각 Department 개체가 결과 컬렉션에 반환되도록 합니다.

참고 항목

참조 형식의 기본값은 null이므로 예제에서는 각 Student 컬렉션의 각 요소에 액세스하기 전에 null 참조를 확인합니다.

var query =
    from student in students
    join department in departments on student.DepartmentID equals department.ID into gj
    from subgroup in gj.DefaultIfEmpty()
    select new
    {
        student.FirstName,
        student.LastName,
        Department = subgroup?.Name ?? string.Empty
    };

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

var query = students.GroupJoin(departments, student => student.DepartmentID, department => department.ID,
    (student, department) => new { student, subgroup = department.DefaultIfEmpty() })
    .Select(gj => new
    {
        gj.student.FirstName,
        gj.student.LastName,
        Department = gj.subgroup?.FirstOrDefault()?.Name ?? string.Empty
    });

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

참고 항목