상세 컨텐츠

본문 제목

[C#] LINQ

.NET/C#

by 클리엘 클리엘 2021. 10. 19. 15:01

본문

728x90

LINQ는 Language INtegrated Query로서 '데이터'를 질의하는데 목적이 있습니다. 우리가 흔히 DB를 대상으로 어떤 데이터를 추출할 때 SQL이라는 언어를 사용해 데이터를 질의하는데 SQL이 DB에 한정된 것이라면 LINQ는 프로그램 안에 존재하는 단순 배열까지 포함하여 IEnumerable<T>인터페이스를 상속하는 모든 개체가 질의의 대상이 될 수 있습니다.

 

예를 들어 아래와 같은 형태의 데이터집합이 존재한다고 가정했을 때

class Program
{
    static void Main(string[] args)
    {
        List<PhoneBook> pb = new List<PhoneBook>{ new PhoneBook { Idx = 1, Name = "홍길동" }, new PhoneBook { Idx = 2, Name = "홍길순" }, new PhoneBook { Idx = 3, Name = "홍길남" } };
    }
}

class PhoneBook
{
    public int Idx { get;set; }
    public string Name { get;set; }
}

LINQ가 없는 상태에서 Idx가 2번인 사람의 이름을 가져오려면 foreach나 for문을 통해 배열을 반복하면서 값을 찾는 게 일반적인 방법이지만 LINQ를 사용하면 간단하고도 명료하게 pb라는 변수로부터 데이터를 질의할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<PhoneBook> pb = new List<PhoneBook>{
            new PhoneBook { Idx = 1, Name = "홍길동" },
            new PhoneBook { Idx = 2, Name = "홍길순" },
            new PhoneBook { Idx = 3, Name = "홍길남" } };

        PhoneBook p = (from member in pb where member.Idx == 2 select member).SingleOrDefault(); //Idx가 2번인 요소 추출

        WriteLine(p.Name); //홍길순
    }
}

위와 같은 방법으로 데이터를 가진 객체를 대상으로 직접 원하는 데이터에 대한 질의가 가능합니다.

 

● from

 

데이터를 추출할 원본과 원본에서 가져온 각 요소를 지정할 범위 변수를 지정합니다. 예제에서 범위 변수가 member이고 추출 대상이 pb에 해당합니다. '변수'라고 표현했지만 실제 member가 값을 저장하지는 않고 원본을 지정하는 용도로만 사용됩니다. 또한 모든 타입이 원본으로 지정될 수 있는 것은 아니고 반드시 IEnumerable<T>인터페이스를 상속한 형식이어야 합니다.

 

예제에서 사용된 pb는 List<PhoneBook>형식으로 List<T>가 이미 IEnumerable<T>인터페이스를 상속한 형식이므로 LINQ의 원본으로 지정될 수 있습니다.

 

from은 추출할 데이터 원본이 다수인 경우 그만큼 여러 개가 사용될 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<PhoneBook> pb = new List<PhoneBook>{
            new PhoneBook { Idx = 1, Name = "홍길동", Favorite = new string[] { "www.naver.com", "www.daum.net" } },
            new PhoneBook { Idx = 2, Name = "홍길순", Favorite = new string[] { "www.daum.net", "www.bing.com" } },
            new PhoneBook { Idx = 3, Name = "홍길남", Favorite = new string[] { "www.yahoo.com", "www.cliel.com" } } };

        var p = from member in pb
                from favorite in member.Favorite
                where favorite == "www.daum.net"
                select new { Name = member.Name, favorite = favorite };

        foreach(var e in p)
        {
            WriteLine(e.Name);
        }
    }
}

class PhoneBook
{
    public int Idx { get;set; }
    public string Name { get;set; }
    public string[] Favorite { get;set; }
}

PhoneBook에 Favorite라는 문자열 배열 형식의 필드가 추가되었고 이 부분을 필터링하기 위해 첫 번째 from에서 원본을 지정한 뒤 두 번째 from에서 원본으로부터의 Favorite를 가져올 수 있도록 하였습니다.

 

● where

 

원하는 데이터를 추출하기 위한 조건 연산자입니다. 예제에서는 member범위 변수에서 Idx값을 2로 지정해 Idx가 2인 요소만 가져오도록 하고 있습니다.

 

● select

 

추출한 결과를 가져오기 위한 것으로 범위 변수를 통해 전체 요소를 지정하거나 마침표(.)를 통해 특정 요소의 값만을 가져올 수 있습니다. 이때 범위 변수 전체를 지정하면 PhoneBook과 형식이 일치하므로 List<PhoneBook>혹은 PhoneBook형식으로 가져올 수 있지만 만약 member.Name처럼 지정하면 결괏값은 IEnumerable<string>형식 혹은 string형식이 됩니다.

var p = (from member in pb orderby member.Idx descending select member.Name);

foreach(string e in p) {
    WriteLine(e);
}

만약 필요한 특정 형식이 있다면 무명 형식을 통해 새로운 형식을 만들어 내는 것도 가능합니다.

var p = (from member in pb orderby member.Idx descending select new { index = member.Idx, title = member.Name });

foreach(var e in p) {
    WriteLine(e.title);
}

● orderby

 

추출한 데이터를 가져올 때 정렬 방식을 지정합니다. 예를 들어 위 예제에서 Idx가 높은 것부터 순서대로 요소를 가져오고 싶다면 다음과 같이 orderby를 지정할 수 있습니다.

var p = (from member in pb orderby member.Idx descending select member);

foreach(PhoneBook e in p) {
    WriteLine(e.Idx); //3, 2, 1
}

orderby만 사용(ascending이 기본)하면 오름차순이고 내림차순을 적용하려면 descending을 사용합니다.

 

● group by

 

추출된 데이터를 분류하는 데 사용됩니다. 예를 들어 아래와 같은 데이터가 존재할 때

class Program
{
    static void Main(string[] args)
    {
        List<Student> std = new List<Student> {
            new Student { Name = "홍길동", Score = 80 },
            new Student { Name = "홍길순", Score = 60 },
            new Student { Name = "홍길남", Score = 100 },
            new Student { Name = "홍길석", Score = 80 },
            new Student { Name = "홍길문", Score = 70 }
        };
    }
}

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

Score가 80 이상인 사람과 그렇지 않은 사람들을 분류하려면 group by를 다음과 같이 사용할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<Student> std = new List<Student> {
            new Student { Name = "홍길동", Score = 80 },
            new Student { Name = "홍길순", Score = 60 },
            new Student { Name = "홍길남", Score = 100 },
            new Student { Name = "홍길석", Score = 80 },
            new Student { Name = "홍길문", Score = 70 }
        };

        var result = from student in std
                    group student by student.Score >= 80 into g
                    select new { Key = g.Key, students = g };

        foreach(var e in result)
        {
            WriteLine($"{e.Key}");
            
            foreach (var item in e.students)
            {
                WriteLine($"{ item.Name }");
            };
        }
    }
}

group다음에는 범위 번수를 위치시키고 by다음에 범위 변수의 분류기준을 지정합니다. 기준에 의해 분류된 결과는 into 다음에 오는 그룹 변수로 가져오게 됩니다. select 에서는 이 그룹 변수에서 key를 따로 저장하고 있는데 이 key로 해당 데이터가 분류기준에 해당되는지의 여부를 bool값으로 판단할 수 있습니다.

● join

 

SQL의 join과 동일한 개념으로 서로 다른 2개의 데이터 원본을 공통된 값으로 연결하도록 합니다.

 

join의 기능을 확인해 보기 위해 우선 2개의 데이터원본을 만들어 보겠습니다.

class Program
{
    static void Main(string[] args)
    {
        List<School> sl = new List<School> {
            new School { classNumber = 1, className = "화성반" },
            new School { classNumber = 2, className = "수성반" },
            new School { classNumber = 3, className = "금성반" }
        };

        List<Student> st = new List<Student> {
            new Student { classNumber = 1, Name = "홍길동" },
            new Student { classNumber = 1, Name = "홍길순" },
            new Student { classNumber = 2, Name = "홍길남" },
            new Student { classNumber = 3, Name = "홍길석" },
            new Student { classNumber = 3, Name = "홍길병" }
        };
    }
}

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

class  School
{
    public int classNumber { get; set; }
    public string className { get; set; }
}

School과 Student에서 공통된 값은 classNumber에 있고 이를 이용해 join을 사용하여 2개의 데이터원본을 연결할 수 있습니다. 이러한 방법으로 각 반과 반에 소속된 이름을 다음과 같이 추출해 볼 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<School> sl = new List<School> {
            new School { classNumber = 1, className = "화성반" },
            new School { classNumber = 2, className = "수성반" },
            new School { classNumber = 3, className = "금성반" }
        };

        List<Student> st = new List<Student> {
            new Student { classNumber = 1, Name = "홍길동" },
            new Student { classNumber = 1, Name = "홍길순" },
            new Student { classNumber = 2, Name = "홍길남" },
            new Student { classNumber = 3, Name = "홍길석" },
            new Student { classNumber = 3, Name = "홍길병" }
        };

        var result = from scl in sl
                    join sdt in st on scl.classNumber equals sdt.classNumber
                    select new {
                        ClassName = scl.className,
                        StudentName = sdt.Name
                    };

        foreach (var item in result)
        {
            WriteLine($"{item.ClassName} - {item.StudentName}");
        }
    }
}

먼저 from을 통해 첫 번째 데이터 원본을 가져오고 join으로 두번째 데이터원본을 지정합니다. 그런 뒤 on이후에 각 데이터 원본에서 공통되는 필드를 가져와 비교를 수행하면 됩니다. 예제에서는 equals로 동일한 classNumber값을 통해 데이터를 연결하고 있습니다.

 

● outer join

 

일반적인 join은 일치하는 값에 해당하는 데이터만 가져옵니다. 위의 예제를 예로 든다면 만약 홍길남이라는 사람의 classNumber값이 2가 아닌 3이라면 결과에서 classNumber가 2번인 수성반은 포함되지 않을 것입니다. outer join은 외부 조인 방식으로 join을 사용하는 것으로서 일치하지 않는 값의 데이터가 존재하더라도 결과에는 모두 포함시켜 반환합니다.

class Program
{
    static void Main(string[] args)
    {
        List<School> sl = new List<School> {
            new School { classNumber = 1, className = "화성반" },
            new School { classNumber = 2, className = "수성반" },
            new School { classNumber = 3, className = "금성반" }
        };

        List<Student> st = new List<Student> {
            new Student { classNumber = 1, Name = "홍길동" },
            new Student { classNumber = 1, Name = "홍길순" },
            new Student { classNumber = 3, Name = "홍길남" }, //classNumber값을 3으로 변경
            new Student { classNumber = 3, Name = "홍길석" },
            new Student { classNumber = 3, Name = "홍길병" }
        };

        var result = from scl in sl
                    join sdt in st on scl.classNumber equals sdt.classNumber into ss
                    from sr in ss.DefaultIfEmpty()
                    select new {
                        ClassName = scl.className,
                        StudentName = sr.Name
                    };

        foreach (var item in result)
        {
            WriteLine($"{item.ClassName} - {item.StudentName}");
        }
    }
}

outer join은 일반적인 join에서의 결과를 담아둘 변수(ss)를 사용하고 ss에서 다시 결과를 가져오는 구조로 사용됩니다. 이때 ss변수는 sdt의 결과를 그대로 가져오므로 sdt에서 필요한 필드가 존재한다면 ss의 결과를 담고 있는 sr을 통해 필드를 지정해야 합니다.

 

하지만 이 상태로는 실행될 수 없는데 그 이유는 값이 일치하지 않는 classNumber가 2번인 수성반의 경우 때문입니다. classNumber가 2인 경우는 sr의 Name을 표시할 수 없게 되므로 오류가 발생하는데 이런 경우에는 DefaultEmpty()를 통해 새로운 객체를 지정해 줘야 합니다.

from sr in ss.DefaultIfEmpty(new Student { Name = "해당없음" })

● LINQ 확장 메서드

 

LINQ 식 자체는 CLR이 직접 이해할 수 있는 코드가 아니고 컴파일러가 컴파일할 때 CLR이 이해할 수 있는 코드로 변환해 줍니다. 이 때문에 현재까지 C#과 VB.NET에서만 LINQ사용이 가능합니다. 이들 언어의 컴파일러만이 LINQ를 변환해 주기 때문입니다.

 

LINQ를 변환하면 구문 자체를 System.Linq에 정의되어 있는 확장 메서드를 사용하는 코드로 변환해 주는데 이 확장메서드는 System.Linq네임스페이스를 참조하면 직접 사용할 수도 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<Student> std = new List<Student> {
            new Student { Name = "홍길동", Score = 80 },
            new Student { Name = "홍길순", Score = 60 },
            new Student { Name = "홍길남", Score = 100 },
            new Student { Name = "홍길석", Score = 80 },
            new Student { Name = "홍길문", Score = 70 }
        };

        // var result = from student in std
        //             group student by student.Score >= 80 into g
        //             select new { Key = g.Key, students = g };

        var result = std.GroupBy(x => x.Score >= 80).Select(x => new { Key = x.Key, students = x  });

        foreach(var e in result)
        {
            WriteLine($"{e.Key}");
            
            foreach (var item in e.students)
            {
                WriteLine($"{ item.Name }");
            };
        }
    }
}

주석 처리된 부분이 기존 LINQ이고 그 밑에 새롭게 작성된 코드가 System.Linq에 있는 확장메서드를 사용한 것입니다. 쿼리의 결과는 기존과 동일합니다.

 

위와 같은 방법으로 사용 가능한 메서드는 아래 글을 참고해 주시기 바랍니다.

표준 쿼리 연산자 개요(C#) | Microsoft Docs

 

표준 쿼리 연산자 개요(C#)

LINQ 표준 쿼리 연산자는 C#에서 필터링, 프로젝션, 집계, 정렬 등 다양한 쿼리 기능을 제공합니다.

docs.microsoft.com

물론 필요하다면 이러한 확장 메서드와 LINQ 식을 혼용해서도 사용할 수 있습니다.

List<Student> std = new List<Student> {
    new Student { Name = "홍길동", Score = 80 },
    new Student { Name = "홍길순", Score = 60 },
    new Student { Name = "홍길남", Score = 100 },
    new Student { Name = "홍길석", Score = 80 },
    new Student { Name = "홍길문", Score = 70 }
};

var result = (from student in std
            group student by student.Score >= 80 into g
            select new { Key = g.Key, students = g }).Take(2);

예제는 우선 LINQ식을 통해 Score가 80 이상인 데이터만 그룹화하여 가져옵니다. 이 결과는 IEnumerable<T>형식이므로 그 결과를 다시 묶어 다시 확장 메서드를 통해 상위 2건의 데이터만 가져오도록 하였습니다.

728x90

'.NET > C#' 카테고리의 다른 글

[C#] dynamic 형식  (0) 2021.10.21
[C#] 리플렉션과 애트리뷰트  (0) 2021.10.20
[C#] LINQ  (0) 2021.10.19
[C#] 람다식  (0) 2021.10.19
[C#] 대리자와 이벤트  (0) 2021.10.18
[C#] 예외처리  (0) 2021.10.15

태그

관련글 더보기

댓글 영역