C# LINQ

2025. 2. 21. 00:22개발/C#

이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.


 

코드를 작성하다 보면 데이터를 탐색하는 경우가 많습니다. C를 배울때는 반복문으로 데이터를 순회하며, 조건문을 사용해 데이터가 유효한지 검사하는 방식으로 탐색했습니다. C++을 배우면서 부터는 STL 라이브러리의 메서드 혹은 전역함수를 사용하여 탐색하였습니다. 그리고 C#은 특별한 데이터 핸들링 기술을 지원합니다.

 

LINQ

Language Integrated Query의 약자로 데이터를 쉽게 쿼리할 수 있는 데이터 핸들링 기술입니다. SQL을 공부해보신 분들은 익숙한 SQL 스타일의 쿼리를 C#에서도 사용할 수 있게 해 주어 다양한 데이터 소스에서 간결하게 데이터를 추출할 수 있게 합니다.

 

int[] scores = [83, 95, 97, 92, 81, 60];

List<int> rank = new List<int>();
foreach (int score in scores)
{
    if (score > 80)
    	rank.Add(score);
}

rank.Sort(
    (score1, score2) =>
    {
    	return score2 - score1;
    });
    
foreach (int score in rank)
{
	Console.Write(score + " "); // print: 97 95 92 83 81
}

 

위 예시는 int 배열안에서 80이상인 점수를 탐색하여 추출하는 코드입니다. int 배열을 foreach문으로 순회하며 조건에 맞는 데이터를 찾고 새롭게 정리할 List 컬렉션에 저장하여 추출하는 방식입니다. 잘못된 점은 없지만 LINQ 코드와 차이를 보여주기 위한 예시입니다. 아래는 LINQ를 사용하여 작성한 예시 입니다.

int[] scores = [83, 95, 97, 92, 81, 60];

IEnumerable<int> rank =
    from score in scores
    where score > 80
    orderby score descending
    select score;
    
foreach (int score in rank)
{
	Console.Write(score + " "); // print: 97 95 92 83 81
}

 

확실히 더 간결한 코드를 작성할 수 있으며, SQL 스타일을 알고 있다면 한 눈에 코드의 목적을 이해 할 수 있을 것입니다. 여기서 IEnumerable는 반복을 지원하는 인터페이스 입니다. 이 인터페이스 상속받은 파생 클래스는 foreach 반복문으로 사용할 수 있으며, 이미 배열을 포함한 다양한 컬렉션이 IEnumerable 인터페이스를 상속 받았습니다. 기본적으로 LINQ는 배열 혹은 컬렉션을 데이터 소스로 사용하면 IEnumerable<T>를 반환합니다. 반환 타입에 대해 신경쓰고 싶지 않다면 var 키워드를 사용할 수 있습니다. 

 

그리고 위 예시의 방법은 쿼리를 기반으로 한 표현식을 사용했지만 메서드를 기반으로 한 메서드 체인 방식도 존재합니다.

int[] scores = [83, 95, 97, 92, 81, 60];

var rank = scores.Where(score => score > 80)
    .OrderByDescending(score => score);

foreach (int score in rank)
{
    Console.Write(score + " "); // print: 97 95 92 83 81
}

 

위 예시 중 편한 방식을 택하여 사용하면 됩니다. 참고로 SQL 쿼리 표현식은 컴파일러가 메서드 체인 방식으로 변환하여 실행합니다.

 

무명 형식

이름 없는 클래스를 생성하고 즉시 인스턴스를 만들 수 있는 기능으로, 무명 함수와 비슷하게 일시적으로 데이터를 관리하기 위해 사용됩니다. 그래서 주로 LINQ의 반환형으로 사용하게 됩니다. LINQselect를 통해 임의적으로 반환값을 수정할 수 하거나 무명 타입을 사용하여 객체의 형태를 가공하여 반환하는 것 입니다.

var person = new { Name = "Alice", Age = 24, Height = 156 };

 

무명 타입은 위 와 같이 선언할 수 있습니다. 위 예시에서 보이는 것 처럼 무명타입은 var 키워드를 사용해서 반환 받을 수 있습니다. 또한 무명 타입은 읽기 전용(Immutable) 으로 get 프로퍼티만을 가진 상태로 반환된기에 값을 변경할 수 없습니다. 이제 다음 예시를 보며 LINQ와 무명 타입의 사용법을 확인하겠습니다. 

using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main()
    {
        Student[] students =  {
            new Student(){ Name = "Alice", Age = 18 },
            new Student(){ Name = "Bob", Age = 21 },
            new Student(){ Name = "Charlie", Age = 22 }
        };

        var result = from s in students
                     where s.Age >= 21
                     select new { s.Name, IsAdult = true };

        foreach (var student in result)
        {
            Console.WriteLine($"{student.Name} - Adult: {student.IsAdult}");
        }
    }
}

 

Bob - Adult: True
Charlie - Adult: True

 

위 예시는 21살 이상인 성인 학생을 추출하는 LINQ코드로, 특이한 점은 select에 무명타입을 사용하여 이름과 성인여부를 담는 객체로 반환 한다는 것 입니다. 이렇게 무명 타입을 통해 Student 클래스에 포함되지 않은 성인여부를 추가적으로 담아 반환 할 수 있는 것 입니다.

 

group by

 

LINQ에서도 group by를 사용 할 수 있습니다. group by는 그룹핑을 통해 해당 데이터 소스를 그룹으로 나누어 처리하는 기능입니다. 아래 예시를 읽어 보시면 이해하기 쉬우실 겁니다.

using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main()
    {
        Student[] students =  {
            new Student(){ Name = "Alice", Age = 18 },
            new Student(){ Name = "Bob", Age = 34 },
            new Student(){ Name = "Charlie", Age = 22 },
            new Student(){ Name = "John", Age = 15 },
            new Student(){ Name = "Jane", Age = 31 },
            new Student(){ Name = "David", Age = 22 }
        };

        var result = from s in students
                     group s by s.Age / 10 * 10 into ageGroup
                     orderby ageGroup.Key
                     select new
                     {
                         AgeRange = ageGroup.Key,
                         Students = ageGroup
                     };

        foreach (var group in result)
        {
            Console.WriteLine($"Age Range: {group.AgeRange}");
            foreach (var student in group.Students)
            {
                Console.WriteLine($"  {student.Name}, Age: {student.Age}");
            }
        }
    }
}

 

Age Range: 10
  Alice, Age: 18
  John, Age: 15
Age Range: 20
  Charlie, Age: 22
  David, Age: 22
Age Range: 30
  Bob, Age: 34
  Jane, Age: 31

 

위 예시는 학생들을 나이대 별로 묶어 분류하는 코드입니다

group { Var } by { Key } into { Group }

 

위 형식으로 사용할 수 있으며 Var는 범위 변수를, Key는 그룹핑 기준을 넣으면 됩니다. 여기서는 10으로 나누고 곱하여 나이대 별로 (10, 20, 30) 그룹핑하였습니다. 그리고 마지막에 Group은 그룹핑된 그룹을 받을 변수를 넣으면 됩니다.

 

그리고 group by를 사용한 반환값은 IGrouping 인터페이스 형식을 사용해서 반환됩니다. IGrouping 또한 IEnumerable을 상속받았기 때문에 foreach문을 사용할 수 있습니다.

 

join

두 개의 데이터 소스를 연결지어 데이터를 출력할 수 있습니다. SQL 처럼 내부조인, 외부조인이 가능하며 아래 예시를 통해 사용법을 알아보겠습니다.

using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Subject
{
    public string Name { get; set; }
    public string StudentName { get; set; }
}

class Program
{
    static void Main()
    {
        Student[] students =  {
            new Student(){ Name = "Alice", Age = 18 },
            new Student(){ Name = "Bob", Age = 34 },
            new Student(){ Name = "Charlie", Age = 22 },
            new Student(){ Name = "John", Age = 15 },
            new Student(){ Name = "Jane", Age = 31 },
            new Student(){ Name = "David", Age = 22 }
        };

        Subject[] subjects =  {
            new Subject(){ Name = "Math", StudentName = "Alice" },
            new Subject(){ Name = "Music", StudentName = "Alice" },
            new Subject(){ Name = "Sience", StudentName = "Charlie" },
            new Subject(){ Name = "Math", StudentName = "John" },
            new Subject(){ Name = "History", StudentName = "Jane" },
            new Subject(){ Name = "Language", StudentName = "David" }
        };

        var result = from student in students
                     join subject in subjects on student.Name equals subject.StudentName
                     select new
                     {
                         Name = student.Name,
                         Age = student.Age,
                         Subject = subject.Name
                     };

        foreach (var student in result)
        {
            Console.WriteLine($"{student.Name}, Age: {student.Age}, Subject: {student.Subject}");
        }
    }
}

 

 

Alice, Age: 18, Subject: Math
Alice, Age: 18, Subject: Music
Charlie, Age: 22, Subject: Sience
John, Age: 15, Subject: Math
Jane, Age: 31, Subject: History
David, Age: 22, Subject: Language

 

위 예시는 내부 조인을 통해 두개의 데이터 소스를 합쳐 결과를 출력한 것 입니다. 내부 조인이기 때문에 Bob은 해당하는 과목이 없어 출력에 없는 것을 확인할 수 있습니다.

from { a } in { A }
join { b } in { B } on { a.var } equals { b.var }

 

 위 형식을 사용하며, from 과 동일하게 범위 변수를 선언 후 on 을 통해 조건을 설정합니다. 위 예시에서는 학생 이름을 기준으로 조건을 설정하였고 과목에는 Bob의 이름이 없기 때문에 결과에서 제외 되었습니다.

 

외부 조인은 위 형식에서 DefaultIfEmpty 연산을 통해 한쪽 데이터 소스에 비어있는 값을 채워 넣는 것으로 결과에 빠진 Bob을 출력하는 것이 가능합니다.

from { a } in { A }
join { b } in { B } on { a.var } equals { b.var } into { C }
from { c } in C.DefaultIfEmpty(new BType(){EmptyFiled = "Default"}

 

위 형식을 통해 비어있는 필드에 값을 채워 넣어 결과에 포함시킬 수 있습니다. 아래 예시를 통해 확인해보겠습니다

var result = from student in students
    join subject in subjects on student.Name equals subject.StudentName into joinResult
    from s in joinResult.DefaultIfEmpty(new Subject() { Name = "-" })
    select new
    {
        Name = student.Name,
        Age = student.Age,
        Subject = s.Name
    };

.

Alice, Age: 18, Subject: Math
Alice, Age: 18, Subject: Music
Bob, Age: 34, Subject: -
Charlie, Age: 22, Subject: Sience
John, Age: 15, Subject: Math
Jane, Age: 31, Subject: History
David, Age: 22, Subject: Language

 

보기 쉽기 위해 LINQ 부분만 표시하였습니다. 내부 조인과 외부조인의 다른 점은 비어있는 값을 기본 값으로 채워 결과에 포함 한다는 것 입니다. 참고로 LINQ는 외부조인에서도 왼쪽 외부 조인만을 제공합니다. 위 예시를 설명드리면, into를 통해 join으로 합쳐진 과목 데이터를 얻어 아래에서 DefaultIfEmpty 연산을 통해 비어있는 값을 "-"으로 채워넣은 s를 사용해 결과를 반환한 것 입니다.

'개발 > C#' 카테고리의 다른 글

C# 애트리뷰트  (0) 2025.02.25
C# 리플렉션  (0) 2025.02.23
C# 이벤트  (0) 2025.02.20
C# 대리자, 익명 메서드 그리고 람다식  (0) 2025.02.18
C# 추상 그리고 인터페이스  (2) 2025.02.14