C# 고급 2편. 링크(LINQ)
링크(LINQ)
LINQ라고 들어보셨나요? 여기서 LINQ는 Language-Integrated Query의 약자로, 이는 통합된 질의 언어를 말합니다. 여기서 질의의 사전적 정의는 "의심나거나 모르는 점을 물음"이며, 이 정의 그대로 질의는 무엇에 대해 물어본다는 것입니다. 좀 더 자세히 말한다면, LINQ를 통해 컬렉션 형태를 띄는 모든 데이터에 질의를 할 수 있으며, 이 강력한 기능을 통해 복잡한 구문을 좀 더 간단하게 필터링하거나 정렬할 수 있다는 등의 특징을 지니고 있습니다. 어디 한번, LINQ가 어떤 강력한 기능을 지니고 있는지 천천히 살펴보도록 합시다.
List<int> intList = new List<int>();
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
foreach (int num in numbers)
{
if (num % 2 == 0)
intList.Add(num);
}
intList.Sort();
foreach (int num in intList)
Console.Write("{0} ", num);
결과:
위의 코드는 numbers 라는 정수형 배열에서 하나씩 요소가 짝수인지를 검사하여, 짝수면 intList에 추가하고 홀수면 추가하지 않고 넘어가는 식으로 코드가 작성되어 있습니다. 그리고 리스트의 정렬이 끝마친 후에는 리스트의 요소를 순차적으로 출력하고 있음을 알 수 있습니다. 그런데 이를, LINQ로 표현하면 어떻게 표현이 가능할까요?
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
where num % 2 == 0
orderby num
select num;
foreach (var i in data)
Console.Write("{0} ", i);
결과:
LINQ를 사용한다면 아주 간단하게 표현됨을 보실 수 있습니다. 이 예제 코드는 첫 예제 코드와 동일한 기능을 하며, 지금부터 위의 예제 코드에 쓰인 from과 in, where, orderby, select, group, join에 대해 간단히 알아보도록 하려고 합니다. 가장 처음으로, from에 대해 알아보도록 합시다.
from
우리가 LINQ 쿼리식을 작성하게 되면, 반드시 들어가야 할 녀석입니다. 이는, 모든 쿼리식이 from 절로 시작해야 한다는 것을 의미합니다. from 절은 아래와 같이 사용할 수 있습니다.
from 범위 변수 in 데이터 원본
위에서 범위 변수는 우리가 주로 사용하고 있는 foreach문의 반복 변수와 똑같은 녀석입니다. 단지 차이점이 있다면, 이 범위 변수는 반복 변수와는 다르게 실제로 데이터를 저장하지 않는다는 것입니다. 그리고 데이터 원본은, IEnumerable 또는 IEnumerable<T> 인터페이스를 상속하거나 IQueryable<T>와 같이 파생 인터페이스를 지원하는 형식이여야 합니다. 이 때, 배열은 IEnumerable<T>를 상속하므로 문제없이 데이터 원본으로 사용될 수 있었던 것입니다.
위에 쓰인 예제 코드에서는 어떻게 쓰였는지 다시 한번 살펴보도록 하겠습니다.
var data = from num in numbers
where num % 2 == 0
orderby num
select num;
위에서는 numbers라는 정수형 배열의 요소가 순차적으로 범위 변수인 num에 들어간다고 생각하면 이해하기 쉽습니다. (이 때, 위에서 말했듯이 실제로 데이터가 들어가는건 아닙니다. 이 말은 단지 이해를 돕기 위해서 하는 말이니 오해 없으시길 바랍니다.) 아직 where와 orderby, select는 유심히 살펴볼 필요는 없습니다. 이제부터 차근차근 알아가면 되니까요.
where
where 절은 필터와 같은 역할을 하며, 데이터 원본으로부터 순차적으로 요소를 가져오고, 그 요소가 범위 변수에 들어가게 되면 이 범위 변수를 조건식을 통해 걸러내는 역할을 합니다. 만약 요소에 대해 조건식이 거짓이면 요소를 반환하지 않으며, 참이면 요소를 반환하게 됩니다. 그리고 이 where 절은 여러개 사용이 가능하다고 합니다.
where 조건식
한번 아래와 같이 LINQ 쿼리식이 있다고 생각해봅시다.
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
where num < 10
orderby num
select num;
위의 코드는 numbers의 요소 중 10보다 작은 데이터만 뽑아낼 것이며, 10과 같거나 그보다 큰 데이터들은 걸러내버립니다. foreach문을 통해 결과를 본다면 "1 3 4 5 6 8 9"라고 출력될 것입니다. 만약 where를 제거하면 조건식으로 필터가 되지 않아서 "1 3 4 5 6 8 9 11 12 15 17 18 22"라고 출력될 것입니다.
orderby
orderby 절은 데이터의 정렬을 수행하며, 오름차순 혹은 내림차순으로 정렬된 결과를 보여줍니다. 기본적으로 오름차순 정렬 순서를 따르며, 컴마를 통해 둘 이상의 데이터를 한꺼번에 정렬시킬 수 있습니다. 오름차순인 경우에는 ascending 키워드를 뒤에 붙이며, 내림차순인 경우에는 descending 키워드를 뒤에 붙입니다. 이 때, 기본 정렬 순서는 오름차순이므로 따로 ascending 키워드를 붙이지 않아도 오름차순으로 데이터가 정렬됩니다.
만약 아래와 같이 LINQ 쿼리식이 존재하면 어떠한 결과가 출력될지 예상해봅시다.
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
orderby num descending
select num;
결과는 "22 18 17 15 12 11 9 8 6 5 4 3 1"이며, 컴파일 타임에 orderby 절은 사실 OrderBy 메서드를 호출한답니다.
select
select 절은 최종적인 결과를 뽑아내는 역할을 하며, 이 최종적인 결과는 앞에 있는 모든 절과 select 절을 거쳐서 나오는 것입니다. 그리고 LINQ 쿼리식은 from으로 시작했으면, select 절 또는 group 절로 끝나야만 합니다. 한번 아래의 예제를 살펴보도록 합시다.
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
where num < 10 && num % 2 == 0
orderby num
select num;
위의 코드에서는 배열 numbers의 요소 중에서 10 미만이며, 짝수인 요소만 뽑아옵니다. 그리고 select 절을 통해, 최종적인 결과를 뽑아냄과 동시에 data의 형식이 결정됩니다. 예제 코드를 하나 더 살펴보도록 합시다.
char[] chars = "str12i3!@$1ng".ToCharArray();
var data = from vchar in chars
where vchar >= 97 && 122 >= vchar
select vchar;
foreach(char i in data)
Console.Write("{0}", i);
결과:
위 예제는 아스키코드를 통해 소문자만 뽑아내는 코드입니다. 이 때, 쿼리의 형식은 IEnumerable<T>이며, 여기서 T는 타입을 말하고 이 타입은 select 절에 의해 결정이 됩니다. select 절에서 뽑아낸 데이터의 형식이 만약 char라면 IEnumerable<char>이 data의 형식이고, int라면 IEnumerable<int>가 data의 형식이 되는 것입니다. 저기서 var를 IEnumerable<char>로 바꾸어도 아무런 오류없이 똑같은 결과를 내보냅니다.
좀 더, 자세한 사항을 알고 싶으시면 여기를 클릭하세요. (MSDN으로 연결됩니다.)
group
group 절은 분류 기준에 따라 데이터를 분류하여 그룹화하여 그룹 개체를 반환합니다. 우리가 모양에 따라, 색깔에 따라, 맛 등의 기준에 따라 나누듯 LINQ 쿼리식에서도 group 절을 이용하여 데이터를 분류할 수 있다는 말입니다. group 절은 아래와 같이 사용합니다.
group A by B into C
여기서 A는 범위 변수를 말하며, B는 분류 기준, C는 그룹 변수를 말하는 것입니다. 참고로 추가적으로 쿼리 작업을 수행하려면 into 키워드가 필요하지만, 그렇지 않은 경우에는 into와 그룹 변수를 쓰지 않아도 괜찮습니다. 우선 좀 더 확실한 이해를 위해 group 절의 예제를 함께 살펴보도록 합시다.
List<student> listStudent = new List<student>
{
new Student() { Name = "김철수", Average = 78.5 },
new Student() { Name = "김영희", Average = 91.2 },
new Student() { Name = "홍길동", Average = 77.3 },
new Student() { Name = "김길수", Average = 80.8 }
};
var queryStudent = from student in listStudent
orderby student.Average
group student by student.Average < 80.0;
foreach (var studentGroup in queryStudent)
{
Console.WriteLine(studentGroup.Key ? "평균 80점 미만:" : "평균 80점 이상:");
foreach (var student in studentGroup)
Console.WriteLine("\t{0}: {1}점", student.Name, student.Average);
}
결과:
평균 80점 미만:
홍길동: 77.3점
김철수: 78.5점
평균 80점 이상:
김길수: 80.8점
김영희: 91.2점
위 예제는 리스트인 listStudent의 요소를 group 절의 분류 기준을 통해 분류하여 그룹화시키는 코드입니다. 여기서 분류 기준은 요소의 Average의 값이 80 미만인지 이상인지에 따라 분류하여 두 개의 그룹으로 나뉘게 됩니다. 여기선 논리식이 참인가 거짓인가에 따라 그룹을 나타내었습니다. 이번에는 그룹 변수를 통한 예제를 살펴보도록 하죠.
List<Student> listStudent = new List<Student>
{
new Student() { Name = "김철수", Average = 78.5 },
new Student() { Name = "김영희", Average = 91.2 },
new Student() { Name = "홍길동", Average = 77.3 },
new Student() { Name = "김길수", Average = 80.8 },
new Student() { Name = "김영순", Average = 54.2 },
new Student() { Name = "김상수", Average = 90.8 },
new Student() { Name = "이한수", Average = 61.4 }
};
var queryStudent = from student in listStudent
group student by (int)student.Average / 10 into g
orderby g.Key
select g;
foreach (var studentGroup in queryStudent)
{
int temp = studentGroup.Key * 10;
Console.WriteLine("{0}점과 {1}점의 사이:", temp, temp + 10);
foreach (var student in studentGroup)
Console.WriteLine("\t{0}: {1}점", student.Name, student.Average);
}
결과:
50점과 60점의 사이:
김영순: 54.2점
60점과 70점의 사이:
이한수: 61.4점
70점과 80점의 사이:
김철수: 78.5점
홍길동: 77.3점
80점과 90점의 사이:
김길수: 80.8점
90점과 100점의 사이:
김영희: 91.2점
김상수: 90.8점
위 예제는 리스트인 listStudent의 요소를 10으로 나눈 결과를 group 절을 통해 분류하고, 그룹 변수 g를 orderby 절을 통해 키의 정렬을 수행하고, select 절로 최종적인 결과를 내보냅니다. 그 다음에, 그룹의 Key는 분류 기준에서 나온 값을 Key로 쓰므로 평균을 정수형으로 캐스팅하고 10으로 나누었으니, 5, 6, 7, 8, 9.. 이런식으로 키가 있을 것이기에 10을 곱하거나 더하여 범위를 표현하였습니다. 이해 되시죠?
join
join 절은 직접 관계가 없는 두개의 데이터 원본을 연결시킬때 유용합니다. 두 데이터 원본을 어떻게 연결시키냐 하면, 각 원본의 요소가 서로 같은지 비교하여 일치한다면 데이터를 서로 연결시키고 일치하지 않는다면 연결을 시키지 않습니다. 우선 join 절의 형식에는 내부 조인과 외부 조인으로 나뉩니다. 내부 조인과 외부 조인의 차이점은 차차 살펴보도록 하고, join 절의 사용 형식을 보도록 합시다.
from a in A
join b in B on a-field equals b-field
위의 형식에서 미리 말씀드리길, a-field는 a의 필드를 말하며 b-field는 b의 필드를 말하는 것입니다. a는 기준이 되는 데이터이며, B는 비교할 대상 데이터입니다. 그리고 여기서는 두 키가 같은지 비교를 하는 연산 빼고는 사용할 수 없으며, == 연산자 대신에 equals 키워드를 사용합니다. 이 키워드는 join 절에서만 사용이 가능하다는 것도 기억해 두시기 바랍니다. 내부 조인과 외부 조인 예제에 쓰일 데이터는 아래와 같습니다.
List<MyAverage> listAverage = new List<MyAverage>
{
new MyAverage() { Name = "김철수", Average = 78.5 },
new MyAverage() { Name = "김영희", Average = 91.2 },
new MyAverage() { Name = "홍길동", Average = 77.3 },
new MyAverage() { Name = "김길수", Average = 80.8 },
new MyAverage() { Name = "김영순", Average = 54.2 },
new MyAverage() { Name = "김상수", Average = 90.8 },
new MyAverage() { Name = "이한수", Average = 61.4 }
};
List<MyHobby> listHobby = new List<MyHobby>
{
new MyHobby() { Name = "김영순", Hobby = "자전거 타기" },
new MyHobby() { Name = "홍길동", Hobby = "컴퓨터 게임" },
new MyHobby() { Name = "이한수", Hobby = "피아노 연주" },
new MyHobby() { Name = "김철수", Hobby = "축구" }
};
먼저, 내부 조인에 대한 예제 코드를 한번 보도록 합시다.
var queryStudent = from student in listAverage
join hobby in listHobby on student.Name equals hobby.Name
select new { Name = student.Name, Average = student.Average, Hobby = hobby.Hobby };
foreach (var studentGroup in queryStudent)
Console.WriteLine("이름: {0}\n\t평균: {1}점\n\t취미: {2}", studentGroup.Name, studentGroup.Average, studentGroup.Hobby);
결과:
이름: 김철수
평균: 78.5점
취미: 축구
이름: 홍길동
평균: 77.3점
취미: 컴퓨터 게임
이름: 김영순
평균: 54.2점
취미: 자전거 타기
이름: 이한수
평균: 61.4점
취미: 피아노 연주
위의 예제 코드를 살펴보면, 범위 변수인 student의 Name 필드와, 연결 대상 변수인 hobby의 Name 필드의 값이 서로 같은지 비교를 합니다. 만약 같으면, 무명 형식을 통해서 새로운 형식을 만들어 내어 Name 필드에는 student.Name의 값이, Average 필드에는 student.Average의 값이, Hobby 필드에는 hobby.Hobby의 값이 들어가게 되는 것입니다. 그리고 여기서 중요한데, 같지 않을 경우에는 join 절의 결과에 포함되지 않는다는 사실을 알아두셔야 합니다. "김영희, 김길수, 김상수" 이 3명의 데이터가 결과에 포함되지 않았다는 것을 보면 알 수 있습니다.
그리고 외부 조인의 예제 코드를 바로 살펴보도록 합시다.
var queryStudent = from student in listAverage
join hobby in listHobby on student.Name equals hobby.Name into sg
from hobby in sg.DefaultIfEmpty(new MyHobby() { Hobby = "없음" })
select new { Name = student.Name, Average = student.Average, Hobby = hobby.Hobby };
foreach (var studentGroup in queryStudent)
Console.WriteLine("이름: {0}\n\t평균: {1}점\n\t취미: {2}", studentGroup.Name, studentGroup.Average, studentGroup.Hobby);
결과 (중간 부분 생략):
이름: 김철수
평균: 78.5점
취미: 축구
이름: 김영희
평균: 91.2점
취미: 없음
..
이름: 김상수
평균: 90.8점
취미: 없음
이름: 이한수
평균: 61.4점
취미: 피아노 연주
위의 예제 코드를 보시면, into 키워드가 사용되었고, sg란 임시 그룹을 가지고 DefaultIfEmpty 메서드를 통해서 비어있는 join 절의 결과에 비어있는 값을 채워 넣습니다. 외부 조인은 내부 조인과 다르게 일치하지 않는 데이터는 필드의 값이 비어있기 때문에 DefaultIfEmpty 메서드를 통해서 기본값을 지정해주는 것이라고 할 수 있습니다.
LINQ 쿼리식을 사용해보니 확실히 편하긴 편하죠? LINQ 쿼리식에 대한 내용이 조금 부실했으나 이해가 되지 않은 내용은 덧글로 달아주시면 제가 아는 범위 내에서 힘껏 답글을 올려드리도록 하겠습니다. 오늘 강좌는 여기서 마무리 지으려고 합니다. 좀 더, LINQ 쿼리식에 대해 자세한 사항을 보고 싶으시면 MSDN 사이트를 참고하세요. 아래에 링크를 걸어드리겠습니다. 모두 수고하셨습니다!