새소식

Code Analysis/C# RX과 Linq

[LINQ] MSDN 내 LINQ 문서를 파해쳐보자! _ 01. 소개

  • -

유니티에서 DI 작업을 EcsRX와 Zenject를 통해 구현해 보려 했는데.. EcsRX는 C# Reactive Extension 를, RX는 LINQ를 기반으로 하고 있네요..

그래서 LINQ 부터! 차근 차근 개념을 적립하고자 이 포스팅을 작성해봅니다..

제목 그대로 MSDN에 기제 되어있는 LINQ 문서를 단순화 및 구체화하며 개념을 다질 것 이며, 연습은 본 목적인 Unity에서 안전하게 사용하기 위해 .NET Framework 3.5 환경에서 예제 작성을 통해 이뤄보겠습니다.

1. 개념

MSDN에선 "LINQ는 Language Intergated Query의 약자로 C# 언어에 직접 쿼리 기능을 통합한 방식을 기반으로 하는 기술 집합 이름"이라 정의했습니다.

본 기능은 .NET Framework 3.5 버전부터 지원되기 시작했으며, SQL Server 데이터베이스, XML 문서, ADO.NET 데이터 집합 및 IEnumerable 또는 제네릭 IEnumerable<T> 인터페이스를 지원하는 모든 객체 컬렉션을 C#에서 LINQ 쿼리 하나로 통합 작성 할 수 있게 해줍니다.

2. 용례

앞서 개념에서 설명했다 싶이 LINQ는 서로 다른 쿼리로 작성된 데이터 집합을 하나의 쿼리 언어로 처리하기 위해 개발됐습니다. 그럼 이 LINQ를 통해 어떤 일들을 할 수 있을까요?

기본적으론 데이터의 Sorting, Searching, Filtering 등.. 다양한 데이터 집합의 처리를 단순화 시켜주는데 사용됩니다. 앞으로 우리는 이런 기능을 유니티 예제를 통해서 알아 볼 것 입니다.

3. 작성 방법

모든 LINQ 쿼리 작업은 다음과 같이 3 단계로 나뉩니다.

    1. 데이터 소스 가져오기
    2. 쿼리 만들기
    3. 쿼리 실행
간단하게 Unity C#에서 integer 배열에서 짝수만을 선택 출력하는 쿼리를 작성해보면 아래 코드와 같습니다.
public class IntroToLinq : MonoBehaviour 
{
	// Use this for initialization
	void Start () 
        {
		//  LINQ의 3단계 : 
		//    1. Data Source
		int[] numbers = new int[7] { 1, 2, 3, 4, 5, 6, 7 };

		//    2. Query 작성.
		IEnumerable<int> numQuery = 
			from num in numbers
			where (num % 2 == 0)
			select num;

		//    3. Query 실행.
		foreach (int num in numQuery)
		{
			Debug.Log (num);
		}
	}
}

이 작업에서 주의해야 할 점은 2 단계 쿼리 작성 부분이 3 단계 실행 부분과 분리된다는 점입니다.
즉, 쿼리 변수를 만드는 작업만으론 데이터 검색이 되지 않는다는 것 입니다.

4. 데이터 소스

이전 개념에서 설명 드렸듯 각종 데이터 집합과 IEnumerable[각주:1]<T> 인터페이스로 작성된 데이터는 LINQ로 쿼리 할 수 있습니다.

이런식의 IEnumerable<T> 또는 제네릭 IQueryable<T> 같은 파생된 인터페이스를 지원하는 형식을 쿼리 가능 형식이라고 부릅니다.

이런 쿼리 가능 형식은 LINQ 데이터 소스로 사용하기 위한 별도의 처리가 필요하지 않습니다.

5. 쿼리

쿼리는 데이터 소스 또는 소스의 정보를 어떻게 처리 할 지를 지정합니다. 필요한 경우엔 쿼리에서 반환 전에 정보를 정렬, 그룹화 및 구체화하는 방법도 지정하기도 합니다.
이런 쿼리는 쿼리 변수에 저장됩니다.

이런 쿼리는 메서드 구문으로도 사용 할 수 있습니다.

앞서 이야기했던 쿼리 예제는 LINQ 선언적 쿼리 구문을 이용하여 작성됐습니다. 그러나 쿼리 구문은 컴파일 단계에서 .NET CLR(공용 언어 런타임)에 대한 메서드 호출로 변환되야 합니다.
이런 메서드 호출은 Where, Select, GroupBy, Join, Max, Average 등과 같은 표준 쿼리 연산자를 호출합니다. 따라서 사용자는 앞선 쿼리 구문 대신 메서드 구문을 사용하여 연산자를 직접 호출 할 수 있습니다.

쿼리 구문과 메서드 구문은 의미상 동일하지만 대부분 쿼리 구문이 가독성이 좋다고 여겨집니다. 하지만 일부 쿼리는 메서드 호출을 이용해야 합니다. 또한 System.Linq 네임스페이스의 표준 쿼리 연산자에 대한 참조 문서는 일반적으로 메서드 구문을 사용합니다.
따라서 LINQ 쿼리를 작성하기 시작한 경우에도 쿼리 및 쿼리 식 자체에서 메서드 구문을 사용하는 방법을 알면 유용합니다.

- 표준 쿼리 연산자 확장 메서드

다음 예제는 이전에 작성한 예제를 쿼리 식과 메서드 기반 쿼리로서 작성된, 의미상 동등한 쿼리를 보여 줍니다.

// Use this for initialization
	void Start ()
	{
		int[] numbers = { 5, 10, 8, 3, 6, 12 };

		//  Query 구문
		IEnumerable numQuery1 =
			from num in numbers
			where num %2 == 0
			orderby num
			select num;
		//  Method 구문
IEnumerable numQuery2 = numbers.Where (num => num % 2 == 0).OrderBy (n => n); foreach (int num in numQuery1) { Debug.Log ("Query : " + num); } foreach (int num in numQuery2) { Debug.Log ("Method : " + num); } }

메서드 기반 쿼리를 이해하기 위해 좀 더 자세히 살펴보면, Where절이 numbers의 인스턴스 메서드로 표현 된 것을 알 수 있습니다. 이 개체를 다시 호출하면 IEnumerable<int> 형식을 갖게 됩니다.
그럼 IEnumerable<T>는 Where 메서드를 갖고 있는 걸 까요? 정답은 아닙니다.
Visual Studio 나 LINQ에 대한 기능 지원을 하는 IDE에서 확인 가능한 Where, SelectMany 등.. 이런 다양한 함수들은 실제 정의되어 있는 것이 아닌, 확장 메서드[각주:2]라는 새로운 종류의 메서드로 구현됩니다.
확장 메서드는 기존 형식을 "확장"하며, 마치 형식에 대한 인스턴스 메서드 처럼 호출 할 수 있습니다. 표준 쿼리 연산자는 IEnumerable<T>를 확장하므로 위 예제에서 처럼 numbers.Where(...) 와 같이 작성 할 수 있던 거죠.

LINQ를 사용하기 전 확장 메서드에 대해 알아야 할 것은, 정확한 using 지시문을 사용하여 이를 응용 프로그램의 범주로 가져오는 방법뿐입니다. 그럼 응용프로그램 관점에선 일반 인스턴스 메서드와 확장 메서드가 동일해집니다. (LINQ에선 System.Linq를 가져옵시다!)

- 람다 식[각주:3]

람다 식은 대리자(delegate) 또는 식 트리 형식을 만드는데 사용 할 수 있는 익명 함수 입니다. 이런  람다 식을 이용하면 인수로 전달되거나 함수 호출 값으로 반환되는 로컬 함수를 쓸 수 있습니다. 이런 특징은 LINQ 쿼리 식 작성에 특히 유용합니다.

람다 식은 '=>' 으로 쓰이는 람다 연산자와 함께 사용됩니다. 위 예제에 Where과 OrderBy에서 사용된걸 확인 하실 수 있습니다.

해당 구문을 좀 더 살펴보면 연산자 왼쪽의 num 은 쿼리 식의 num에 해당하는 입력 변수입니다. 컴파일러가 numbers 가 IEnumerable<T> 형식이라는 것을 알고 있으므로 num 형식을 유추 할 수 있습니다. 이런 람다 식의 본문은 메서드 호출 및 복잡한 논리를 포함할 수 있습니다. 반환 값은 식 결과입니다.

6. 쿼리 실행

- 지연된 실행

앞에서 설명한 대로 쿼리 변수는 명령을 저장 할 뿐, 실제 쿼리 실행은 foreach 문에서 쿼리 변수가 반복될 때까지 지연됩니다.

이 개념을 "지연된 실행"이라 합니다.

이런 특성 덕분에 우리는 쿼리 변수 자체를 원하는 만큼 자주 실행 할 수 있으며, 최근 데이터를 검색하는 쿼리를 작성하고 일정 간격을 두고 쿼리를 반복 시행하여 매번 다른 결과를 얻는 응용도 할 수 있습니다.

- 즉시 실행 강제 적용

소스 요소 범위에 대해 집계 함수를 수행하는 쿼리는 먼저 해당 요소를 반복해야 합니다. 이러한 쿼리의 예로 Count, Max, Average 및 First가 있습니다.

이러한 쿼리는 다음과 같은 특징을 갖습니다.

      1. 명시적 foreach 문 없이 실행됩니다.
      2. IEnumerable 컬렉션이 아닌 단일 값을 반환합니다.

예시를 작성해보면,

// Use this for initialization
	void Start ()
	{
		int[] numbers = { 5, 10, 8, 3, 6, 12 };

		var evenNumQuery = 
			from num in numbers
			where (num % 2 == 0)
			select num;
		int evenNumCount = evenNumQuery.Count ();
	}

와 같이 Count() 작업을 즉시 호출 할 수 있습니다.

또한 모든 쿼리를 즉시 실행하고 그 결과를 캐싱하기 위해 ToList나 ToArray 메서드를 사용 할 수 도 있습니다.

// Use this for initialization
	void Start ()
	{
		int[] numbers = { 5, 10, 8, 3, 6, 12 };
		//  List 형태로 캐싱
		List numQuery2 = 
			(from num in numbers
			 where (num % 2 == 0)
			 select num).ToList();
		//  int[] 형태로 캐싱
		var numQuery3 = 
			(from num in numbers
			 where (num % 2 == 0)
			 select num).ToArray ();
	}



이상 LINQ에 대한 기본적인 소개가 끝났습니다.

중간 중간 더 자세히 알 필요가 있는 지식은 주석을 통해 표시했습니다만, 기회가 된다면 해당 내용들에 대해서도 따로 포스팅을 해보고 싶습니다.

이후 LINQ 포스팅에서는 소개를 넘어서 LINQ에 대한 보다 심층적인 용례와 기능을 확인하는 시간을 갖고 Unity 에서 쓸모있는 예제를 만들어보도록 하겠습니다.

  1. enumerator를 노출시켜주며, foreach를 통해 확인 할 수 있다. 이 타입으로 정의 될 수 있는 것들은 array, string, 각종 generic collection 등이 있으며 더 자세한 내용은 https://docs.microsoft.com/ko-kr/dotnet/api/system.collections.generic.ienumerable-1?view=netframework-4.7.1 여기서 확인 할 수 있다. [본문으로]
  2. 확장 메서드에 대한 보다 자세한 설명은 https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/extension-methods 에서 확인 할 수 있습니다. [본문으로]
  3. 람다 식에 대한 보다 자세한 설명은 https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions 에서 확인 하실 수 있습니다. [본문으로]
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.