[고급문법 익히기] 15장 LINQ

0

이것이 C#이다

목록 보기
26/26

15.1 LINQ(Language Integrated Query)

  • C# 언어에 통합된 데이터 질의 기능
  • 프로그래밍에서 많은 부분을 차지하는 데이터 작업의 효율을 향상
  • 데이터 질의란 데이터 집합에서 원하는 데이터를 찾는 작업
  • 데이터 질의의 기본 요소 :
    Form : 어떤 데이터 집합에서 찾을 것인가?
    Where : 어떤 조건으로 찾을 것인가?
    Select : 어떤 항목을 추출할 것인가?

15.2 LINQ의 기본

from

  • 쿼리식의 대상이될 데이터 원본과 원본 안에 들어있는 각 요소 데이터를 나타내는 범위변수를 지정하는 절

  • from <범위변수> in <데이터원본>의 형식으로 사용

  • 범위변수는 foreach(int x in arr)에서 int x와 하는 역할이 같다

  • 데이터 원본은 IEnumerable< T >인터페이스를 상속 하는 형식이어야만 한다 (배열,컬렉션은 이미 모두 상속하고 있다.)

LINQ의 범위변수
범위 변수는 실제로 데이터를 담지 않습니다. 그래서 쿼리식 외부에서 선언된 변수에 범위변수의 데이터를 복사해 넣는다던가 하는 일은 할 수 없습니다. 번위변수는 오로지 LINQ 질의 안에서만 통용되며,질의가 실행될 때 어떤 일이 일어날지를 묘사하기 위해 도입되었습니다.

from 절은 다음과 같이 사용합니다.
(from 절 밑부분은 신경쓰지 않아도 됩니다.)

int[] numbers = {1,2,3,4,5,6,7,8,9,10};

var result = from n in numbers 	//데이터 원본 numbers와 범위변수 n을 지정합니다.
			 where n % 2 == 0	//조건 : 짝수
             orderby n			//오름차순 정렬
             select n;			//결과값

where

  • 찾고자 하는 데이터를 필터
  • 범위 변수가 충족하는 조건을 매개 변수로 입력

where 절은 한마디로 필터 역할을 하는 연산자입니다. from 정이 데이터 원본으로부터 뽑아낸 범위변수가 가져야 하는 조건을 where 연산자에 인수로 입력하면 LINQ는 해당 조건에 부합하는 데이터만 걸러냅니다.

다음과 같은 데이터 원본이 있을때 Height가 175 미만인 객체들만 걸러낼 수 잇습니다.

Profile[] arrProfile = {
						new Profile(){name = "정우성", Height = 186},
                        new Profile(){name = "김태희", Height = 158},
                        new Profile(){name = "고현정", Height = 172},
                        new Profile(){name = "이문세", Height = 178},
                        new Profile(){name = "하하", Height = 171}
						};
                        
var profiles = 	from profile in arrProfile
				where profile.Height < 175	
                select profile;

orderby

  • 필터링된 데이터를 정렬
  • ascending(오름차순), descending(내림차순) 선택 가능
    • 아무것도 설정하지 않으면 ascending로 기본 적용됨

orderby는 정렬을 수행하는 연산자입니다.
바로 예제를 보겠습니다.

var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height				//오름차순 기본 적용
                select profile;
                
var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height ascending	//오름차순 명시 
                select profile;
                
var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height descending	//내림차순 명시 
                select profile;

select

  • 최종 결과를 추출하는 쿼리식의 마침표같은 존재
  • LINQ질의 결과인 IEnumerable< T > 객체의 매개변수 T는 select문에 의해 결정

select 절은 최종 결과를 추출하는 쿼리식의 마침표같은 존재입니다.

var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height		
                select profile;					//추출한 결과를 profiles에 반환합니다

이때 중요한 것은 var 형식으로 선언된 profiles의 실제 형식입니다.
LINQ 질의결과는 IEnumerable< T >로 반환되는데, 이때 형식 매개변수 T는 select문에 의해 결정됩니다.

예를 들어 앞의 LINQ 쿼리식은 배열로부터 Height가 175 미만인 Profile 객체를 골랐는데, 그 결과는 IEnumaeralbe< Profile > 형식이 됩니다. (var profile의 형식은 IEnumaeralbe< Profile >이 됨.)

//select문에서 반환 결과를 달리 하면 형식매개변수 < T >의 형식이 바뀌게 됩니다.

var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height		
                select profile;					//IEnumaeralbe< Profile > 형식 반환
              //select profile.name;			//IEnumaeralbe< string > 형식 반환
              //select profile.Height;			//IEnumaeralbe< int > 형식 반환

이뿐만 안닙니다. select문은 무명형식을 이용해서 새로운 형식을 즉석에서 만들 수 있습니다.

var profiles = 	from profile in arrProfile
				where profile.Height < 175
                orderby profile.Height		
                select new { Name = profile.Name, 
                			 InchHeight = profile.Height * 0.393 };		//무명 형식

아래 예제로 간단한 LINQ를 다뤄보겠습니다.

namespace SimpleLinq
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }

    internal class MainApp
    {
        static void Main(string[] args)
        {
            Profile[] arrProfile =
            {
                new Profile(){Name = "정우성", Height = 186},
                new Profile(){Name = "김태희", Height = 158},
                new Profile(){Name = "고현정", Height = 172},
                new Profile(){Name = "이문세", Height = 178},
                new Profile(){Name = "하하", Height = 171}
            };

            var profiles = from profile in arrProfile
                           where profile.Height < 175
                           orderby profile.Height
                           select new
                           {
                               Name = profile.Name,
                               InchHeight = profile.Height * 0.393
                           };

            foreach (var profile in profiles)
                Console.WriteLine($"{profile.Name}, {profile.InchHeight}");

        }
    }
}

출력
김태희, 62.094
하하, 67.203
고현정, 67.596

15.3 여러 개의 데이터 원본에 질의 하기

지금까지는 데이터 원본에 접근하기 위해서 from절을 사용했는데, 그때마다 한가지의 데이터 원본만 사용했었습니다. 여러개의 데이터 원본에 접근하려면 from을 중첩해서 사용하면 됩니다.

아래 예제에서 Teams 클래스에는 팀원의 이름과 성적이 들어갈 Scores 배열을 만들었습니다.

namespace _15._3_FromFrom
{
    class Teams
    {
        public string Name { get; set; }
        public int[] Scores { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Teams[] arrTeams = {
                new Teams(){Name = "철수", Scores = new int[]{99,80,70,24}},
                new Teams(){Name = "유리", Scores = new int[]{60,45,87,72}},
                new Teams(){Name = "맹구", Scores = new int[]{92,30,85,94}},
                new Teams(){Name = "훈이", Scores = new int[]{90,88,0,17}}
            };

            var failed = from c in arrTeams
                       from s in c.Scores					//arrTemas에 담겨있는 객체의 Scores에 접근
                       where s < 60
                       orderby s                            //점수를 기준으로 정렬
                       select new { c.Name, Lowest = s };   //무명 형식으로 반환

            foreach (var c in failed)
                Console.WriteLine($"{c.Name}, {c.Lowest}");
        }
    }
}

출력
훈이, 0
훈이, 17
철수, 24
맹구, 30
유리, 45

15.4 group by로 데이터 분류하기

  • 특정 기준으로 데이터를 분류(그룹화)
  • 형식은 group A by B int C
  • A에는 from 절에서 뽑아낸 범위변수를, B에는 분류기준을, C에는 그룹 변수를 위치시키면 됩니다.

분류 기준에 부합하면 그룹_변수.Key에 true 값이 담기고, 부합하지 않는 다면 false 값이 담깁니다.

namespace GroupBy
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            Profile[] arrProfile = {
            new Profile(){ Name = "강백호", Height = 188 },
            new Profile(){ Name = "채치수", Height = 197 },
            new Profile(){ Name = "송태섭", Height = 168 },
            new Profile(){ Name = "서태웅", Height = 187 },
            new Profile(){ Name = "정대만", Height = 184 }
            };

            var listProfile = from profile in arrProfile
                              orderby profile.Height
                              group profile by profile.Height >= 185 into g		//185이상은 true, 이하는 false
                              select g;

            foreach (var Group in listProfile)
            {
                Console.WriteLine($" -- 185 이상인가? : {Group.Key}");

                foreach (var profile in Group)
                {
                    Console.WriteLine($"{profile.Name},{profile.Height}");
                }
            }
        }
    }
}

출력
-- 185 이상인가? : False
송태섭,168
정대만,184
-- 185 이상인가? : True
서태웅,187
강백호,188
채치수,197

15.5 두 데이터 원본을 연결하는 join

  • join은 두 데이터 원본을 연결하는 연산
  • 각 데이터 원본에서 특정 필드가 일치하는 데이터끼리 연결

내부 조인

  • 두 데이터 원본 사이에서 일치하는 데이터만 연결하여 반환
  • 내부 조인은 교잡합이자, SQL에서 Inner Join 방식

데이터 원본 A는 기준이 되며, A의 각 데이터는 Name과 Height 필드를 가지고 있습니다. 그리고 B는 A에 연결할 데이터 원본이며 Product와 Star 필드를 갖고 있습니다. 이제 배우의 이름, 작품, 키를 담는 새로운 컬렉션을 만들고 싶다면 A의 Name 필드와 B의 Star필드가 일치하는 데이터들만 연결됩니다.

내부조인은 join 키워드를 이용합니다.

from a in A
join b in B on a.XXXX equals b.YYYY

join의 on 키워드는 조인 조건을 수반합니다. 이때 on 절의 조건은 "동등(==)"만 허용됩니다. "~보다 작음(<=)","~보다 큼(>=)"같은 비교연산은 허락되지 않습니다. 동등은 equals라는 키워드로 사용됩니다.

아래 예제는 위 그림과 같이 Star와 Name을 내부 조인한 프로그램입니다. "하하"는 포함되지 않은 것을 참고해주세요

namespace _15._5_Join
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }

    class Product
    {
        public string Title { get; set; }
        public string Star { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            Profile[] arrProfile = {
            new Profile() { Name = "정우성", Height = 186 },
            new Profile() { Name = "김태희", Height = 158 },
            new Profile() { Name = "고현정", Height = 172 },
            new Profile() { Name = "이문세", Height = 178 },
            new Profile() { Name = "하하", Height = 171 }
            };

            Product[] arrProduct = {
            new Product() { Title = "비트", Star = "정우성" },
            new Product() { Title = "CF 다수", Star = "김태희" },
            new Product() { Title = "아이리스", Star = "김태희" },
            new Product() { Title = "모래시계", Star = "고현정" },
            new Product() { Title = "Solo 예찬", Star = "이문세" },
            };

            var listProfile =
                from pf in arrProfile
                join pd in arrProduct on pf.Name equals pd.Star
                select new
                {
                    Name = pf.Name,
                    Work = pd.Title,
                    Height = pf.Height
                };

            Console.WriteLine("--내부 조인 결과--");

            foreach (var profile in listProfile)
            {
                Console.WriteLine($"이름:{profile.Name},  작품:{profile.Work},  키:{profile.Height}");
            }

        }
    }
}

출력
--내부 조인 결과--
이름:정우성, 작품:비트, 키:186
이름:김태희, 작품:CF 다수, 키:158
이름:김태희, 작품:아이리스, 키:158
이름:고현정, 작품:모래시계, 키:172
이름:이문세, 작품:Solo 예찬, 키:178

외부 조인

  • 외부 조인은 SQL에서 왼쪽(Left) 조인방식
  • 한쪽 데이터를 원본을 기준으로 삼은 상태에서 다른 데이터 원본과 결합하여 반환
    1. join 절을 이용해서 조인을 수행 후 그 결과를 임시 컬렉션에 저장
    2. DefaultIfEmpty 연산을 통해 임시 컬렉션의 비어있는 조인 결과에 빈 값 삽입
    3. DefaultIfEmpty 연산을 거친 임시 컬렉션에 다시 from 절을 통해 범위 변수 추출
    4. 범위 변수와 기준 데이터 원본에서 뽑아낸 범위 변수를 이용해서 최종 결과 추출
namespace _15._6_Outer_Join
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }

    class Product
    {
        public string Title { get; set; }
        public string Star { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            Profile[] arrProfile = {
            new Profile() { Name = "정우성", Height = 186 },
            new Profile() { Name = "김태희", Height = 158 },
            new Profile() { Name = "고현정", Height = 172 },
            new Profile() { Name = "이문세", Height = 178 },
            new Profile() { Name = "하하", Height = 171 }
            };

            Product[] arrProduct = {
            new Product() { Title = "비트", Star = "정우성" },
            new Product() { Title = "CF 다수", Star = "김태희" },
            new Product() { Title = "아이리스", Star = "김태희" },
            new Product() { Title = "모래시계", Star = "고현정" },
            new Product() { Title = "Solo 예찬", Star = "이문세" },
            };

            var listProfile =
                from pf in arrProfile
                join pd in arrProduct on pf.Name equals pd.Star into ps
                from pd in ps.DefaultIfEmpty(new Product() { Title = "없음"})
                select new
                {
                    Name = pf.Name,
                    Work = pd.Title,
                    Height = pf.Height
                };

            Console.WriteLine("--외부 조인 결과--");

            foreach (var profile in listProfile)
            {
                Console.WriteLine($"이름:{profile.Name},  작품:{profile.Work},  키:{profile.Height}");
            }

        }
    }
}

출력
--외부 조인 결과--
이름:정우성, 작품:비트, 키:186
이름:김태희, 작품:CF 다수, 키:158
이름:김태희, 작품:아이리스, 키:158
이름:고현정, 작품:모래시계, 키:172
이름:이문세, 작품:Solo 예찬, 키:178
이름:하하, 작품:없음, 키:171

15.6 LINQ의 비밀과 LINQ 연산자

LINQ를 컴파일러가 이해할 수 있는 이유

LINQ 쿼리식이 실행될수 있는 이유는 무엇일까요?
그저 컴파일러가 LINQ 쿼리문을 분석해서 일반적인 메소드 호출 코드로 만든것 뿐입니다.
예를 들어 다음과 같은 쿼리식이 있다고 했을때,

var profiles = from profile in arrProfile
			   where profile.Height < 175
               orderby profile.Height
               select new { Name = profile.Name, InchHeight = profile.Height * 0.393 };

C# 컴파일러는 다음과 같은 코드로 변형합니다.

using System.Linq;		//없으면 에러 발생

var profiles = arrProfile
				.Where( profile => profile.Height < 175)
                .OrderBy( profile => profile.height )
                .Select( profile => 
                	new { 
                        Name = profile.Name,
                		InchHeight = profile.Height * 0.393
                        });

from 절의 매개변수는 IEenumerable< T >의 파생형식이여야만 합니다.
배열은 IEenumerable< T >의 파생형식이며,IEenumerable< T >는 System.Collections.Generic 네임스페이스 소속입니다.
위에 보이는 .Where(), .OrderBy(), .Select()는 System.Linq 네임스페이스에 정의되어 있는 IEenumerable< T >의 확장 메소드 이기 때문에 System.Linq 네임스페이스를 선언하지 않으면 에러가 발생합니다.

LINQ 표준 연산자

LINQ만으로 대부분의 데이터 처리가 가능하지만, 아래의 LINQ 연산 메소드를 활용한다면 더욱 편리하게 사용할 수 있습니다.
그런 의미에서 표준 연산자를 알아보고 LINQ 쿼리식과 메소드를 함께 사용하는 방법을 알아 보겠습니다.

쿼리식과 메소드를 함께 사용하는 방법

다음과 같은 데이터 원본이 있다고 하고,LINQ를 통해 키가 180cm 미만인 연예인들의 키 평균을 구해보겠습니다.

Profile[] arrProfile = 
{
  new Profile(){Name="정우성", Height=186},
  new Profile(){Name="김태희", Height=158},
  new Profile(){Name="고현정", Height=172},
  new Profile(){Name="이문세", Height=178},
  new Profile(){Name="하하", Height=171}
};

var profiles = from pf in arrProfile
  			   where pf.Height < 180
               select profile;
                                    
//이제 profiles는 180 미만의 데이터만 갖고 있는 IEnumerable< Profile > 형식의 컬렉션입니다.
//따라서 LINQ를 사용할 수 있으며, profiles에 대해 다음과 같이 Average() 메소드를 호출할 겁니다.
  
double Average = profiles.Average(pf => pf.height);
  
Console.WriteLine(Average);							//169.75 출력

다음과 같이 Average()메소드를 호출하는 코드를 한문장으로 묶을 수도 있습니다.

double Average = (from pf in arrProfile
  				  where pf.Height < 180
                  select pf).Average(pf => pf.Height);
  
Console.WriteLine(Average);							//169.75 출력

예제 프로그램을 만들어 보겠습니다.

namespace _15._7_MinMaxArgLINQ
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }

    internal class MainApp
    {
        static void Main(string[] args)
        {
            Profile[] arrProfile =
            {
              new Profile(){Name="정우성", Height=186},
              new Profile(){Name="김태희", Height=158},
              new Profile(){Name="고현정", Height=172},
              new Profile(){Name="이문세", Height=178},
              new Profile(){Name="하하", Height=171}
            };

            var heightStat = from pf in arrProfile
                             group pf by pf.Height <= 175 into g
                             select new
                             {
                                 Group = g.Key == true ? "175미만" : "175이상",
                                 Count = g.Count(),
                                 Max = g.Max(pf => pf.Height),
                                 Min = g.Min(pf => pf.Height),
                                 Average = g.Average(pf => pf.Height)
                             };
            foreach (var stat in heightStat)
            {
                Console.WriteLine($"{stat.Group} - Count:{stat.Count}, Max{stat.Max}");
                Console.WriteLine($"Min:{stat.Min}, Average:{stat.Average}");
            }
        }
    }
}

출력
175이상 - Count:2, Max186
Min:178, Average:182
175미만 - Count:3, Max172
Min:158, Average:167

참고)
LINQ 쿼리에서 g.Max(pf => pf.Height)는 g 그룹 내의 Profile 객체들 중에서 키(Height)의 최대값을 구하는 부분입니다. 이 부분에서 pf => pf.Height는 람다 표현식(Lambda Expression)으로 사용되고, 매개 변수 pf는 그룹 내의 각 Profile 객체를 나타냅니다.

람다 표현식은 C#에서 익명 함수를 간단하게 표현하기 위한 방법으로 사용됩니다. (매개 변수) => (식 또는 문) 형태로 작성되며, 이 경우에는 pf라는 매개 변수를 받아 해당 Profile 객체의 Height 속성 값을 반환하는 식이 사용되었습니다.

따라서 g.Max(pf => pf.Height)는 g 그룹 내의 Profile 객체들 중에서 Height 속성의 최대값을 찾는 것을 의미합니다. Max 메서드는 그룹 내에서 최대값을 반환하므로, 해당 식을 사용하여 그룹의 최대 키 값을 구할 수 있습니다.

0개의 댓글