상세 컨텐츠

본문 제목

[C#] LINQ

.NET/C#

by 클리엘 클리엘 2021. 4. 22. 17:16

본문

728x90

1. 데이터 추출

 

LINQ는 Language INtegrated Query의 약자로 통합 질의 언어를 말합니다. 질의라는 것은 데이터를 가져오기 위해 뭔가를 요청하는 것인데 데이터를 요청하는 대상은 IEnumerable<T>상속하는 열거 가능한 데이터입니다.

 

예를 들어 다음과 같은 클래스가 존재합니다.

class Student
{
	public string Name { get; set; } = string.Empty;
	public int Math { get; set; } = 0;
	public int Eng { get; set; } = 0;
}

위와 같은 클래스에 기반하여 열거가능한 List형태의 데이터를 만들게 되면 아래와 비슷하게 만들어질 것입니다.

List<Student> students = new List<Student>();
students.Add(new Student { Name = "홍길동", Math = 90, Eng = 70 });
students.Add(new Student { Name = "홍길순", Math = 75, Eng = 60 });
students.Add(new Student { Name = "홍길남", Math = 80, Eng = 75 });

LINQ는 우선 어떤 데이터에서 값을 가져올 것인가를 생각해야 합니다. 위 예제의 경우에는 students가 될 것이고 from 절을 통해 그 대상을 지정합니다. 참고로 아래 예제에서 s는 foreach에서 foreach(s in students)를 구현할때의 s와 같습니다.

var student = from s in students

다음으로는 어떤 데이터를 가져올것인가를 생각해야 하는데 이 조건은 where 절을 통해서 지정합니다. 예제에서는 Math가 90인 데이터만 가져오도록 하였습니다.

var student = from s in students
			where s.Math == 90

마지막으로 위에서 최종적으로 추출된 데이터중 어떤 항목을 가져올것인가를 지정하면 됩니다. 이 부분은 select를 통해 구현되며 예제에서는 s를 그대로 반환함으로써 들어온 개체 전체를 지정하였습니다.

var student = from s in students
			where s.Math == 90
			select s;

select를 통해 지정한 객체는 다시 Enumerable형식으로 반환되며 이 값은 var를 통해 선언된 student변수로 들어가게 됩니다. 즉, 마지막 select가 student변수의 형식을 결정하게 되는 것입니다.

foreach (Student s in student)
	Console.WriteLine($"{s.Name}");

예제는 s만 붙여 student 개체자체를 통째로 넘기는 방법을 사용했지만 필요하다면 s.Name처럼 특정한 값만을 추출해 넘겨줄 수도 있습니다.

var student = from s in students
			orderby s.Math ascending
			select s.Name;

foreach (string s in student)
	Console.WriteLine($"{s}");
}

혹은 무명형식을 빌려 아예 새로운 타입을 만들어 반환하는 것도 가능합니다.

var student = from s in students
			orderby s.Math ascending
			select new { s_name = s.Name, point = s.Math};

foreach (var s in student)
	Console.WriteLine($"{s.s_name}");
}

Linq안에서는 필요한 경우 let을 통해 변수를 생성해 사용할 수 있습니다. 마치 var변수와 비슷해 보이기도 합니다.

var student = from s in students
			orderby s.Math ascending
			let std = s
			select new { s_name = std.Name, point = std.Math};

foreach (var s in student)
	Console.WriteLine($"{s.s_name}");
}

from, where, select가 조합된 LINQ를 통해서 데이터를 추출했습니다. Database의 SQL을 쓰는 경우와 거의 비슷합니다. 데이터를 정렬하는 것도 예컨대 MS-SQL에서는 order by를 사용했는데 LINQ에서도 이와 비슷하게 orderby를 사용할 수 있습니다.

var student = from s in students
			orderby s.Math ascending
			select s;

orderby는 데이터를 정렬하며 ascending는 오름차순정렬임을 의미합니다. 오름차순은 기본이기에 생략이 가능하지만 내림차순을 지정하고자 한다면 명시적으로 descending를 줘야 합니다.

 

또한 SQL 쿼리에서 데이터 그룹화를 위해 사용되는 group by절 또한 LINQ에서 비슷한 목적으로 사용할 수 있습니다. LINQ에서는 group [from변수] by [기준] into [group 변수] 형식입니다.

var student = from s in students
			group s by s.Math > 80 into g
			select new { isGroup = g.Key, stds = g };

foreach (var s in student) {
	Console.WriteLine($"분류기준 - {s.isGroup}");

	foreach (var std in s.stds) {
		Console.WriteLine($"{std.Name} - {std.Math}");
	}
}

from변수는 위에서 from으로 가져온 변수를 의미합니다. 따라서 예제에서는 s가 오게 되며 by절 다음에는 group화를 수행할 기준이 오게 됩니다. 예제에서는 이 기준으로 Math점수가 80 이상인 것과 그렇지 않은 것을 기준으로 정했습니다. 마지막으로 into는 by에 의해 정해진 기준으로 그룹화된 데이터가 담길 변수입니다.

 

마지막 select에서는 무명 형식으로 데이터를 추출하고 있는데 위에서 group화된 데이터를 변수 g로 저장하고 있습니다. 'group화된 데이터'라는 말에 주목해 주세요. by는 Math가 '80이상인가 아닌가'에 대한 기준을 제시하고 있으며 g에는 이 기준에 의해 나누어진 80이상인 컬렉션과 그렇지 않은 컬렉션을 모두 가지고 있습니다. 그리고 이 컬렉션들은 Key속성으로 구분할 수 있는데 이에 대한 처리를 이어지는 foreach문에서 그대로 적용하고 있습니다.

 

isGroup은 기준에 합당한 컬렉션이면 true값을 그렇지 않으면 false값을 가지고 있고 true냐 false냐에 따라 그에 해당하는 컬렉션들을 따로따로 모아 두고 있는 것입니다. 그래서 예제에서는 기준에 따라 저장된 별개의 컬렉션을 나열하기 위해 2번의 foreach문을 사용하고 있습니다.

 


2. 데이터 조인

 

데이터를 추출할 때 비교할 컬렉션이 하나 이상이라면 join을 통해 이 문제를 해결할 수 있습니다. 예제를 위해 아래와 같은 클래스를 추가하고

class School
{
	public int Grade { get; set; } = 0;
	public int Number { get; set; } = 0;
	public string Name { get;set; } = string.Empty;
}

데이터를 생성합니다.

List<School> schools = new List<School>();
schools.Add(new School { Grade = 3, Number = 10, Name = "홍길동" });
schools.Add(new School { Grade = 4, Number = 8, Name = "홍길순" });
schools.Add(new School { Grade = 5, Number = 13, Name = "홍길남" });

참고로 기존의 Student 리스트에도 아래와 같이 '홍길석'이라는 요소를 추가합니다.

List<Student> students = new List<Student>();
students.Add(new Student { Name = "홍길동", Math = 90, Eng = 70 });
students.Add(new Student { Name = "홍길순", Math = 75, Eng = 60 });
students.Add(new Student { Name = "홍길남", Math = 80, Eng = 75 });
students.Add(new Student { Name = "홍길석", Math = 65, Eng = 70 });

자! 이제 join을 사용해볼 준비가 완료되었습니다. students 리스트에서 명단을 가져올 때 각 학생의 학년과 번호를 가져와야 한다면 다음과 같이 join문을 만들 수 있습니다.

var student = from std in students
			join sch in schools on std.Name equals sch.Name
			select new {
				grade = sch.Grade,
				Number = sch.Number,
				Name = std.Name
			};

join을 통해 2개의 컬렉션을 지정하고 on이하 구문에 데이터를 join 할 기준을 지정해 줍니다. 예제에서는 2개의 컬렉션이 공통으로 존재하는 Name속성을 가지고 같은 데이터끼리 join을 처리하고 있습니다. 예제에서 사용된 equals 구문이 같은 데이터를 가져오도록 하는 것이며 아쉽게도 ~보다 크거나 ~보다 작다와 같은 조건은 지정할 수 없습니다.

 

위 결과를 보면 students에 새로 추가한 '홍길석'이라는 요소는 결과에 표시되지 않았음을 알 수 있습니다. students의 '홍길석'은 schools에는 포함되지 않았기 때문에 join의 결과로 나올 수 없기 때문입니다. 하지만 경우에 따라 모든 데이터를 가져와야 하는 상황이 발생할 수 있는데 이때는 DefaultIfEmpty를 사용합니다.

var student = from std in students
			join sch in schools on std.Name equals sch.Name into tmp
			from sch in tmp.DefaultIfEmpty(new School() { Grade = 0, Number = 0 })
			select new {
				grade = sch.Grade,
				Number = sch.Number,
				Name = std.Name
			};

기존의 join에서 크게 벗어나지는 않았습니다. 다만 into를 통해 배열을 저장할 임시 변수를 하나 만들고 임시 변수에서 DefaultIfEmpty연산을 사용한 것이 전부입니다.

 

students안에서 '홍길석'이라는 데이터를 가져와야 하는 상황에서 보면 schools 컬렉션에는 '홍길석'에 대한 데이터가 존재하지 않으므로 DefaultIfEmpty를 통해 join 되지 않는 데이터에 대한 임시 컬렉션을 생성하여 이들을 결합해 결과를 추출하는 것입니다.

 

이는 SQL에서 외부 조인과 비슷한 처리를 수행하는 것입니다. 다만 LINQ에서는 외부조인 중에서 왼쪽 조인 형태만 가능합니다.


3. LINQ 확장 메서드

 

LINQ구문은 컴파일러에 의해 System.Linq 네임스페이스에 정의되어 있는 IEnumerable<T>의 확장 메서드로 변환되어 처리됩니다.

 

예를 들어 students는 IEnumerable<T>의 파생 형식인 List 컬렉션이므로 다음과 같이 확장 메서드를 통한 LINQ연산을 수행할 수 있는 것입니다.

var student = students.Where(x => x.Name == "홍길동");

LINQ의 확장 메서드 종류 및 기능에 관해서는 다음 링크를 참고해 주세요.

 

Enumerable 클래스 (System.Linq) | Microsoft Docs

 

Enumerable 클래스 (System.Linq)

IEnumerable을 구현하는 개체를 쿼리하기 위한 static(Visual Basic의 경우 Shared) 메서드 집합을 제공합니다.Provides a set of static (Shared in Visual Basic) methods for querying objects that implement IEnumerable.

docs.microsoft.com

위의 확장 메서드를 사용하면 단순한 LINQ구분의 사용만으로 처리하기 힘든 결과를 손쉽게 얻을 수 있습니다. 예를 들어 어떠한 값의 평균치를 구하려 한다면

List<Student> students = new List<Student>();
students.Add(new Student { Name = "홍길동", Math = 90, Eng = 70 });
students.Add(new Student { Name = "홍길순", Math = 75, Eng = 60 });
students.Add(new Student { Name = "홍길남", Math = 80, Eng = 75 });
students.Add(new Student { Name = "홍길석", Math = 65, Eng = 70 });

double am = students.Average(x => x.Math);

Console.WriteLine(am);

Average메서드를 사용하는 것만으로 원하는 값을 추출할 수 있습니다.

728x90

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

[C#] 파일과 디렉토리  (0) 2021.05.03
[C#] dynamic  (0) 2021.04.30
[C#] LINQ  (0) 2021.04.22
[C#] 리플렉션  (0) 2021.04.19
[C#] 대리자(Delegate)  (0) 2021.04.15
[C#] 이벤트(Event)  (0) 2021.04.12

관련글 더보기

댓글 영역