뇌를 자극하는 C# 4.0 프로그래밍/ LINQ

데이터! 데이터! 데이터!

  • LINQ는 C# 언어에 통합된 데이터 질의 기능을 의미한다.

LINQ의 기본: from, where, orderby, select

from

// from 절 사용 방식
from <범위 변수> in <데이터 원본>

// from 절 사용 예시
int[] number = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = from n in numbers
             where n % 2 == 0
             orderby n
             select n;
  • 모든 LINQ 쿼리식(Query Expression)은 반드시 from 절로 시작한다. 우리는 쿼리식의 대상이 될 데이터 원본과 데이터 원본 안에 들어있는 각 요소 데이터를 나타내는 범위 변수를 from 절에서 지정해 줘야 한다.
  • 이때 from의 데이터 원본은 아무 형식이나 사용할 수는 없고, IEnumerable<T> 인터페이스를 상속하는 형식이어야만 한다.
    • 배열과 컬렉션은 IEnumerable<T>을 상속하고 있기 때문에 모두 from 절의 데이터 원본으로 사용할 수 있다.
    • foreach의 반복 변수는 데이터 원본으로부터 데이터를 담아내지만, LINQ의 범위 변수는 실제로 데이터를 담지는 않는다. 크래서 쿼리식 외부에서 선언된 변수에 범위 변수의 데이터를 복사해 넣는다거나 하는 일은 할 수 없다. 범위 변수는 오로지 LINQ 질의 안에서만 통용된다.

where

// where 절 사용 예시
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 porfiles = from profile in arrProfile
               where profile.Height < 175
               select profile;
  • where는 필터 역할을 하는 연산자이다. from절이 데이터 원본으로부터 뽑아낸 범위 변수가 가져야 하는 조건을 where 연산자에게 매개 변수로 입력하면 LINQ는 해당 조건에 부합하는 데이터만 걸러낸다.

orderby

// orderby는 기본적으로 데이터를 오름차순으로 정렬한다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile;

// ascending은 오름차순을 명시적으로 기술한다
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height ascending
               select profile;

// 내림차순은 descending을 기술하면 된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height descending
               select profile;

select

// select는 최종 결과를 추출해 내는 키워드이다.
// 추출된 데이터는 IEnumerable<T> 형태로 반환된다.
// 아래의 경우 IEnumerable<Profile>이 반환된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile;

// 프로퍼티만 추출할 수도 있다.
// 이 프로퍼티는 IEnumerable<string> 형식으로 반환된다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select profile.Name;

// select 문은 무명 형식을 이용해서 새로운 형식을 즉석에서 만들어 낼 수도 있다.
var porfiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select new { Name = profile.Name, InchHeight = profile.Height * 0.393 };

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

// 여러 개의 데이터 원본에 접근하는 예시
class Class
{
    public string Name { get; set; }
    public int[] Score { get; set; }
}

Class[] arrClass = 
{
    new Class(){ Name = "연두반", Score = new int[]{ 99, 80, 70, 24 }},
    new Class(){ Name = "분홍반", Score = new int[]{ 60, 45, 87, 72 }},
    new Class(){ Name = "파랑반", Score = new int[]{ 92, 30, 85, 94 }},
    new Class(){ Name = "노랑반", Score = new int[]{ 90, 88, 0, 17 }},
}

// 여러 개의 데이터에 접근하고 싶다면 for문 처럼 from을 중첩해서 사용하면 된다.
var classes = from c in arrClass // 첫 번째 데이터 원본
              from s in c.Score // 두 번째 데이터 원본
              where s < 60
              select new { c.Name, Lowest = s };

group by로 데이터 분류하기

// group by 선언 방법
group (from에서 뽑아낸 범위 변수) by (분류 기준) into (그룹 변수)

// group by 사용 예시
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 listProfile = from profile in arrProfile
                  group profile by profile.Height < 175 into g
                  select new { GroupKey = g.key, Profiles = g };

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

내부 조인

// 내부 조인 선언 방법
from a in A
join b in b on a.xxxx equals b.yyyy

// 내부 조인 사용 예시
class Profile
{
    public string Name { get; set; }
    public int Height { get; set; }
}

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

var listProfile = from profile in arrProfile
                  join product in arrProduct on profile.Name equals product.Star
                  select new ( Name = profile.Name, Work = product.Title, Height = profile.Height );
  • 내부 조인(Inner 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; }
}

// 외부 조인시에는 조인을 수행한 후 그 결과를 임시 컬렉션에 저장하고, 이 임시 컬렉션에 대해 DefaultIfEmpty 연산을 수행해서 비어 있는 조인 결과에 빈 값을 채워 넣는다.
// DefaultIfEmpty 연산을 거친 임시 컬렉션에서 from 절을 통해 범위 변수를 뽑아내고 이 범위 변수와 기준 데이터 원본에서 뽑아낸 범위 변수를 이용해서 결과를 추출해 낸다.
var listProfile = from profile in arrProfile
                  join product in arrProduct on profile.Name equals product.Star into ps
                  from product in ps.DefaultIfEmpty(new Product(){Title="없음"})
                  select new ( Name = profile.Name, Work = product.Title, Height = profile.Height );
  • 외부 조인은 합집합과 비슷하다.
  • 외부 조인시 연결할 데이터 원본에 기준 데이터 원본의 데이터와 일치하는 데이터가 없다면 그 부분은 빈 값으로 결과를 채운다.
  • 참고) 원래 SQL에서 지원하는 외부 조인은 왼쪽 조인, 오른쪽 조인, 완전 조인이 있는데 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# 컴파일러는 위 쿼리식을 분석하여 다음과 같은 호출 코드로 번역한다.
// 다시 말해 아래와 같은 메소드 형식으로 작성해도 된다.
var profiles = arrProfile
                .Where( profile => profile.Height < 175 )
                .OrderBy( profile => profile.Height)
                .Select( profile => new { Name = profile.Name, InchHeight = profile.Height * 0.393 } );
  • LINQ는 .NET 언어 중에서도 C#과 비주얼베이직에서만 사용 가능하다. MS는 LINQ 쿼리식이 실행될 수 있도록 CLR을 개선하는 대신 C# 컴파일러와 VB 컴파일러를 업그레이드 했기 때문.
  • 프로그래머가 LINQ 쿼리식을 작성하면 C# 컴파일러는 위와 같은 쿼리를 분석해서 메소드 호출 코드로 만들어 낸다.

LINQ 표준 연산자

종류 메소드 이름 설명
정렬 OrderBy 오름차순으로 값을 정렬
정렬 OrderByDescending 내림차순으로 값을 정렬
정렬 ThenBy 오름차순으로 2차 정렬
정렬 ThenByDescending 내림차순으로 2차 정렬
정렬 Reverse 컬렉션의 요소를 거꾸로 뒤집는다.
집합 Distinct 중복 값을 제거
집합 Except 두 컬렉션 사이의 차집합을 반환
집합 Intersect 두 컬렉션 사이의 교집합을 반환
집합 Union 두 컬렉션 사이의 합집합을 반환
필터링 OfType 메소드의 형식 매개 변수로 형식 변환이 가능한 값들만 추출
필터링 Where 필터링할 조건을 평가하는 함수를 통과하는 값들만 추출
수량 연산 All 모든 요소가 임의의 조건을 모두 만족 시키는지를 평가. 결과는 true 또는 false
수량 연산 Any 모든 요소 중 단 하나의 요소라도 임의의 조건을 만족시키는지를 평가. 결과는 true 또는 false
수량 연산 Contains 명시한 요소가 포함되어 있는지를 평가. 결과는 true 또는 false
데이터 추출 Select 값을 추출하여 시퀀스를 만든다.
데이터 추출 SelectMany 여러 개의 데이터 원본으로부터 값을 추출하여 하나의 시퀀스를 만든다. 여러 개의 from절을 사용한다.
데이터 분할 Skip 시퀀스에서 지정한 위치까지 요소들을 건너 뛴다.
데이터 분할 SkipWhile 입력된 조건 함수를 만족시키는 요소들을 건너 뛴다.
데이터 분할 Take 시퀀스에서 지정한 요소까지 요소들을 취한다.
데이터 분할 TakeWhile 입력된 조건 함수를 만족시키는 요소들을 취한다.
데이터 결합 Join 공통 특성을 가진 서로 다른 두 개의 데이터 소스의 객체를 연결한다. 공통 특성을 키(Key)로 삼아, 키가 일치하는 두 객체를 쌍으로 추출한다.
데이터 결합 GroupJoin 기본적으로 Join 연산자와 같은 일을 하되, 조인 결과를 그룹으로 만들어 넣는다.
데이터 그룹화 GroupBy 공통된 특성을 공유하는 요소들을 각 그룹으로 묶는다. 각 그룹은 IGrouping 객체로 표현된다.
데이터 그룹화 ToLookup 키(key) 선택 함수를 이용하여 골라낸 요소들을 Lookup 형식의 객체에 삽입한다.
생성 DefaultIfEmpty 빈 컬렉션을 기본값이 할당된 싱글턴 컬렉션으로 바꾼다.
생성 Empty 비어 있는 컬렉션을 반환한다.
생성 Range 일정 범위의 숫자 시퀀스를 담고 있는 컬렉션을 생성한다.
생성 Repeat 같은 값이 반복되는 컬렉션을 생성한다.
동등 여부 평가 SequenceEqual 두 시퀀스가 서로 일치하는지를 평가한다.
요소 접근 ElementAt 컬렉션으로부터 임의의 인덱스에 존재한느 요소를 반환한다.
요소 접근 ElementAtOrDefault 컬렉션으로부터 임의의 인덱스에 존재하는 요소를 반환하되, 인덱스가 컬렉션의 범위를 벗어날 때 기본값을 반환한다.
요소 접근 First 컬렉션의 첫 번째 요소를 반환. 조건식이 있으면 조건을 만족 시키는 첫 번째 요소를 반환
요소 접근 FirstOrDefault First 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본 값을 반환
요소 접근 Last 컬렉션의 마지막 요소를 반환. 조건식이 있으면 조건을 만족 시키는 마지막 요소를 반환
요소 접근 LastOrDefault Last 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값을 반환
요소 접근 Single 컬렉션의 유일한 요소를 반환. 조건식이 있으면 조건을 만족시키는 유일한 요소를 반환
요소 접근 SingleOrDefault Single 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값을 반환
형식 변환 AsEnumerable 매개 변수를 IEnumerable로 형식 변환하여 반환
형식 변환 AsQueryable (일반화) IEnumerable 객체를 (일반화) IQueryable 형식으로 반환
형식 변환 Cast 컬렉션의 요소들을 특정 형식으로 반환
형식 변환 OfType 특정 형식으로 형식 변환할 수 있는 값만 걸러 낸다.
형식 변환 ToArray 컬렉션을 배열로 변환. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToDictionary 키 선택 함수에 근거해서 컬렉션의 요소를 Dictionary에 삽입. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToList 컬렉션을 List로 형식 변환한다. 이 메소드는 강제로 쿼리를 실행한다.
형식 변환 ToLookup 키 선택 함수에 근거해서 컬렉션의 요소를 Lookup에 삽입. 이 메소드는 강제로 쿼리를 실행한다.
연결 Concat 두 시퀀스를 하나의 시퀀스로 연결한다.
집계 Aggregate 컬렉션의 각 값에 대해 사용자가 정의한 집계 연산을 수행한다.
집계 Average 컬렉션의 각 값에 대한 평균을 계산한다.
집계 Count 컬렉션에서 조건에 부합하는 요소의 갯수를 센다.
집계 LongCount Count와 동일한 기능을 하지만 매우 큰 컬렉션을 대상으로 한다는 점이 다르다.
집계 Max 컬렉션에서 가장 큰 값을 반환한다.
집계 Min 컬렉션에서 가장 작은 값을 반환한다.
집계 Sum 컬렉션 내의 값의 합을 계산한다.
// LINQ 메소드 사용 예시
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 }
                        }

// 쿼리를 날려 데이터를 담은 후에 LINQ 메소드로 결과 출력                        
var profiles = from profile in arrProfile
               where profile.Height < 180
               select profile;

double Average1 = profiles.Average(profile => profile.Height);
Console.WriteLine(Average1); // 169.75 출력

// 쿼리식과 LINQ 메소드를 한 번에 사용
double Average2 = (from profile in arrProfile
                   where profile.Height < 180
                   select profile).Average(profile => profile.Height);
Console.WriteLine(Average2); // 169.75 출력
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기