LINQ
LINQ(링크)는 Language INtegrated Query의 약자로 C#과 같은 .NET 기반 언어에서 마치 SQL 구문으로 데이터를 읽어오는 것처럼 LINQ 고유 구문으로 데이터를 처리하는 기법을 말합니다.
> // LINQ: C# 언어에 직접 쿼리(Query) 기능을 통합하는 방식을 기반으로 하는 기술 집합 이름
CAUTION
LINQ의 발음은 처음 만든 개발자가 발음한 공식적인 발음인 링크로 읽습니다.
LINQ 소개
C#에서는 LINQ(Language Integrated Query) 이름의 특별한 프로그래밍 문법을 제공합니다. 예를 들어 배열 또는 리스트 등의 컬렉션 데이터를 반복문과 조건문을 사용하여 원하는 데이터를 구하는 방법을 아주 편리하게 구현할 수 있습니다. LINQ는 "링크"로 발음합니다.
LINQ는 다음과 같은 특징을 가집니다. 처음 보는 내용일 수 있으니, 가볍게 읽고 넘어갑니다.
- C#과 같은 언어에 DSL(Domain Specific Language)를 포함
- SQL 구문과 같은 코드를 C#에서 사용
- 데이터 소스에 따라 서로 다른 쿼리 사용
- SQL, XQuery/XPath 등
- LINQ를 통한 생산성의 향상
- 컴파일러의 Query 및 결과 체크
- Visual Studio에서의 인텔리센스 지원
- 메서드 문법(Method Syntax)과 쿼리 문법(Query Syntax)
- 메서드: System.Linq에서 제공하는 확장 메서드
- 쿼리: C# 언어 차원에서 제공하는 선언적인 구문
확장 메서드(Extension Method) 사용하기
닷넷에서는 특정 형태에 원래는 없던 기능을 덧붙이는 개념으로 확장 메서드(Extension Method)를 제공합니다. 예를 들어 정수 배열의 합계를 구하려면 if
문과 for
문을 사용하여 직접 합계 기능을 구해야 합니다. 하지만, 이를 확장 함수 또는 확장 메서드 개념으로 없던 기능을 추가할 수 있습니다. 닷넷에는 이러한 유용한 확장 메서드를 다수 제공합니다. 확장 메서드를 직접 만드는 방법은 뒤에서 자세히 알아보고 이번에는 LINQ에서 제공하는 확장 메서드를 먼저 사용해 보겠습니다.
닷넷의 주요 확장 메서드를 사용하려면 System.Linq
네임스페이스를 선언해 주어야 합니다.
최우선적으로 기억해 두어야 할 확장 메서드들은 다음과 같습니다. 숫자 배열 또는 컬렉션에 대해서 합계(Sum), 건수(Count), 평균(Average), 최댓값(Max), 최솟값(Min)을 구할 수 있습니다.
Sum()
: 숫자 배열 또는 컬렉션의 합계Count()
: 숫자 배열 또는 컬렉션의 건수Average()
: 숫자 배열 또는 컬렉션의 평균Max()
: 최댓값Min()
: 최솟값
Sum()
메서드로 배열의 합계 구하기
정수 배열 또는 컬렉션에 들어있는 데이터의 전체 합계를 구하는 내용을 살펴보겠습니다. 일반적인 환경이라면 if
문과 for
등의 조합으로 구할 수 있습니다.
C#의 LINQ에는 일반적인 배열과 컬렉션에 대한 연산에 대한 유용한 메서드를 제공합니다.
합계를 구할 때에는 Sum()
메서드를 사용할 수 있습니다.
다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqSum.cs
using System;
using System.Linq;
class LinqSum
{
static void Main()
{
int[] numbers = { 1, 2, 3 };
int sum = numbers.Sum();
Console.WriteLine($"numbers 배열 요소의 합: {sum}");
}
}
numbers 배열 요소의 합: 6
정수 배열인 numbers
의 전체 합계를 구하려면 Sum()
메서드를 사용합니다. Sum()
메서드의 결괏값을 sum
변수에 받아서 출력해보면 배열의 합이 저장됨을 알 수 있습니다. 이처럼 LINQ에서 제공하는 확장 메서드들을 사용하면 편리하게 합계, 건수, 평균 등을 구할 수 있습니다.
Count()
메서드로 배열의 건수 구하기
이번에는 Count()
확장 메서드를 통해서 정수 배열의 건수(개수)를 구해보겠습니다. 다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqCount.cs
using System;
using System.Linq;
class LinqCount
{
static void Main()
{
int[] numbers = { 1, 2, 3 };
int count = numbers.Count();
Console.WriteLine($"{nameof(numbers)} 배열 요소의 건수: {count}");
}
}
numbers 배열 요소의 건수: 3
사실 배열의 요소 수는 배열의 Length
속성을 통해서 구할 수 있습니다. 잠시 후에 알아보겠지만, Count()
메서드는 추가적으로 조건을 처리할 수 있는 기능을 더해 줍니다.
출력문에는 numbers
배열의 이름을 직접 출력하는 대신에 nameof()
연산자를 사용하여 numbers
배열 이름을 문자열로 출력해보았습니다. 이렇게 할 경우에는 numbers
배열 이름을 Visual Studio의 리팩터링 기능인 이름 바꾸기 기능으로 손쉽게 배열 이름 및 배열 이름이 문자열로 필요한 곳을 한꺼번에 바꿀 수 있는 장점을 가지게 됩니다.
Average()
메서드로 배열의 평균 구하기
이번에는 Average()
확장 메서드를 사용하여 배열 요소의 평균을 구하겠습니다. 다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqAverage.cs
using System.Linq;
using static System.Console;
class LinqAverage
{
static void Main()
{
int[] numbers = { 1, 3, 4 };
double average = numbers.Average();
WriteLine($"{nameof(numbers)} 배열 요소의 평균: {average:#,###.##}");
}
}
numbers 배열 요소의 평균: 2.67
정수 배열에 Average()
메서드를 호출하면 실수 형식의 평균값을 계산해줍니다. 평균 값이 담겨 있는 average
변수를 {average:#,###.##}
형태의 옵션(서식 지정자)을 주어 세자리마다 콤마로 표시하고 소수 두자리까지 표시하도록 하였습니다.
Max()
메서드로 컬렉션의 최댓값 구하기
이번에는 Max()
확장 메서드를 사용하여 컬렉션 요소 중 최댓값을 구하겠습니다. 다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqMax.cs
using System;
using System.Collections.Generic;
using System.Linq;
class LinqMax
{
static void Main()
{
var numbers = new List<int>() { 1, 2, 3 };
int max = numbers.Max();
Console.WriteLine($"{nameof(numbers)} 컬렉션의 최댓값: {max}");
}
}
numbers 컬렉션의 최댓값: 3
정수 배열과 마찬가지로 정수 컬렉션에 Max()
메서드를 호출하여 최댓값을 구할 수 있습니다.
Min()
메서드로 컬렉션의 최솟값 구하기
이번에는 Min()
확장 메서드를 사용하여 컬렉션 요소 중 최솟값을 구하겠습니다. 다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqMin.cs
using System;
using System.Collections.Generic;
using System.Linq;
class LinqMin
{
static void Main()
{
var numbers = new List<double> { 3.3, 2.2, 1.1 };
var min = numbers.Min();
Console.WriteLine($"{nameof(numbers)} 리스트의 최솟값: {min:.00}");
}
}
numbers 리스트의 최솟값: 1.10
정수 컬렉션과 마찬가지로 실수 컬렉션도 Min()
메서드를 사용하여 최솟값을 구할 수 있습니다.
Min()
과 Max()
로 최솟값과 최댓값 구하기
다시 한번 LINQ를 사용하여 최댓값과 최솟값 구하는 예제를 만들어 보겠습니다.
코드: MinAndMax.cs
using System;
using System.Linq;
class MinAndMax
{
static void Main()
{
int[] arr = { 1, 2, 3 };
int min = arr.Min(); // 1
int max = arr.Max(); // 3
Console.WriteLine($"최솟값: {min}, 최댓값: {max}");
}
}
최솟값: 1, 최댓값: 3
Min()
, Max()
와 같은 LINQ의 확장 메서드를 사용하면 for
문과 if
문의 조합으로 직접 알고리즘으로 구현해야 하는 최댓값과 최솟값을 쉽게 구할 수 있습니다. 참고로, 다음 장에서는 알고리즘을 사용하여 직접 코드를 작성하여 최댓값과 최솟값을 구하는 방법을 다룹니다.
화살표 연산자와 람다 식으로 조건 처리
LINQ에서 제공하는 확장 메서드들은 매개 변수로 람다 식(Lambda Expression)이라는 것을 받습니다. 람다 식은 화살표 연산자 또는 람다 연산자로 불리는 화살표 모양의 =>
기호를 사용합니다.
람다 식
람다 식(Lambda Expression)은 다른 말로 화살표 함수(Arrow Function)라고도 합니다.
화살표 연산자 또는 람다 연산자(Lambda Operator)로 표현되는 =>
연산자는 일반적으로 영어로 "goes to" 또는 "arrow"로 발음합니다. 우리말로 표현하면 "이동"의 뜻을 가집니다.
람다 식은 다음과 같이 2가지 모양으로 표현됩니다. 이 2가지 모양을 구분해서 식 람다(Expression Lambda)와 문 람다(Statement Lambda)로 표현하기도 합니다.
(입력 매개 변수) => 식
(입력 매개 변수) => { 문; }
실제 코드로는 람다 식은 아래와 같이 표현할 수 있습니다.
x => x + 1
x => { return x + 1; }
x => x * x
형태의 람다 식은 "x는 x * x로 이동"으로 읽을 수 있습니다.
람다 식을 만드는 것은 뒤에서 자세히 배우지만, 우선 미리보기로 다음 코드를 간단히 살펴보고 넘어가겠습니다. Func<T>
또는 Action<T>
를 사용하여 새롭게 만든 isEven()
함수와 greet()
함수를 사용하여 짝수를 판별하거나 이름을 출력하는 메서드를 만들어 본 내용입니다.
> // 식 람다 미리보기 코드
> Func<int, bool> isEven = x => x % 2 == 0;
> isEven(2)
true
> isEven(3)
false
> // 문 람다 미리보기 코드
> Action<string> greet = name => { var message = $"Hello {name}"; Console.WriteLine(message); };
> greet("You")
Hello You
다음 람다 식으로 어떤 number
값이 주어지면 해당 number
값을 2로 나누었을 때 0과 같은가를 판단합니다. 즉, 짝수인 데이터만을 가지고 옵니다. 입력 매개 변수에 해당하는 number
변수 명은 동적으로 원하는 이름으로 선언이 가능합니다. 일반적으로 줄여서 표현합니다.
number => number % 2 == 0
람다 식을 조금 어려운 말로 표현하면, 매개 변수로 전달된 이름이 없는 인라인 함수(이름없는 메서드)입니다. 즉, 람다 식 자체가 하나의 함수이고 함수를 가리키는 함수 포인터가 됩니다. 람다 식 자체를 만드는 방법은 나중에 살펴보고 지금은 람다 식을 사용하는 것만 집중하겠습니다.
LINQ에서는 이러한 모양의 람다 식과 Where()
확장 메서드를 사용하여 조건을 처리할 수 있습니다.
Where()
메서드의 결괏값은 IEnumerable<T>
입니다. IEnumerable<T>
는 List<T>
와 비슷하지만 읽기 전용 컬렉션입니다.
Where()
메서드로 IEnumerable<T>
형태의 데이터 가져오기
다음 코드는 Where()
메서드에 매개 변수로 람다 식을 제공하여 새로운 컬렉션을 가져옵니다.
코드: LinqWhere.cs
> int[] numbers = { 1, 2, 3, 4, 5 };
> IEnumerable<int> newNumbers = numbers.Where(number => number > 3);
> foreach (var n in newNumbers)
. {
. Console.WriteLine(n);
. }
4
5
람다 식(number => number > 3
)의 의미는 매개 변수가 들어오면 3보다 큰 데이터만을 가지고 와서 IEnumerable<int>
형식의 newNumbers
에 대입하여 출력합니다. 3보다 큰 4와 5만 출력됨을 알 수 있습니다.
모든 LINQ의 확장 메서드의 기본 결괏값인 IEnumerable<T>
부분은 var
키워드로 대체할 수 있습니다.
ToList()
메서드로 IEnumerable<T>
를 List<T>
로 변환하기
만약, 람다 식을 사용하는 Where()
와 같은 확장 메서드를 호출할 때 IEnumerable<T>
대신에 List<T>
형태로 받고자 할 때에는 ToList()
메서드를 한 번 더 호출해주면 됩니다.
코드: LinqWhereToList.cs
> int[] numbers = { 1, 2, 3, 4, 5 };
> List<int> newNumbers = numbers.Where(number => number > 3).ToList();
> foreach (var number in newNumbers)
. {
. Console.WriteLine(number);
. }
4
5
코드에서는 Where()
메서드의 결과인 IEnumerable<T>
를 ToList()
메서드를 한번 더 호출해서 List<T>
형태로 변환 후 사용하였습니다.
Where()
메서드로 조건 처리하기
Where()
확장 메서드는 람다 식을 사용하여 조건을 처리할 수 있습니다. 다음 코드를 보면 배열 또는 컬렉션에 Where()
메서드를 통해서 짝수만 가져온 후 다시 Sum()
메서드로 짝수만의 합계를 구할 수 있습니다.
코드: LinqSumEven.cs
> var numbers = new List<int> { 1, 2, 3 };
> numbers.Where(number => number % 2 == 0).Sum()
2
다음 코드는 람다 식의 입력 매개 변수를 n
으로 줄여서 표현하고 홀수만의 합계를 구합니다.
코드: LinqSumOdd.cs
> var numbers = new List<int> { 1, 2, 3 };
> numbers.Where(n => n % 2 == 1).Sum()
4
Where()
메서드에 매개 변수로 전달되는 람다 식의 매개 변수이름은 일반적으로 컬렉션의 첫 자를 따서 만드는 게 관례입니다. 예를 들어 numbers
컬렉션이면 n => n
형태를 사용하고 colors
컬렉션이면 c => c
형태를 주로 사용합니다.
정수 배열에서 홀수만 추출하는 예제를 만들어보겠습니다.
코드: LinqWhereMethod.cs
using System;
using System.Linq;
class LinqWhereMethod
{
static void Main()
{
int[] arr = { 1, 2, 3, 4, 5 };
// 배열에서 홀수만 추출: *람다 식* 사용
var q = arr.Where(num => num % 2 == 1);
foreach (var n in q)
{
Console.WriteLine(n); // 1, 3, 5
}
}
}
1
3
5
Where(num => num % 2 == 1)
형태를 사용하여 배열 또는 컬렉션에서 홀수인 데이터만 가져올 수 있습니다.
Where()
확장 메서드를 사용한 필터링
Where()
확장 메서드는 조건 처리 또는 원하는 데이터만을 가져오는 필터링 기능을 합니다.
코드: Filter.cs
using System;
using System.Linq;
class Filter
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
// `Where()` 확장 메서드를 사용한 필터링
var nums = numbers.Where(it => it % 2 == 0 && it > 3); // 짝수 && 3보다 큰
foreach (var num in nums)
{
Console.WriteLine(num);
}
}
}
4
Where(it => it % 2 == 0 && it > 3)
조건을 사용하여 짝수이면서 3의 배수인 데이터만을 필터링해서 가져옵니다. 이처럼 논리 연산자를 사용하여 하나 이상의 조건을 처리할 수 있습니다.
Count()
확장 메서드로 개수 구하기
Count()
와 같은 대부분의 확장 메서드들은 Where()
메서드를 사용하지 않고도 바로 매개 변수로 람다 식을 전달하여 조건을 처리할 수 있습니다.
코드: CountFunc.cs
using System;
using System.Linq;
class CountFunc
{
static void Main()
{
bool[] blns = { true, false, true, false, true };
Console.WriteLine(blns.Count()); // 5
Console.WriteLine(blns.Count(bln => bln == true)); // 3
Console.WriteLine(blns.Count(bln => bln == false)); // 2
}
}
5
3
2
Count()
메서드에 Count(bln => bln == true)
형태로 Where()
메서드 호출없이 바로 조건에 맞는 데이터의 건수를 구할 수 있습니다.
![INCLUDE 30.3.5 All()
메서드와 Any()
메서드로 조건 판단]
Any()
확장 메서드로 데이터가 있는지 확인하기
LINQ의 Any()
확장 메서드는 컬렉션(시퀀스)에 요소가 하나라도 있는지 확인하는 기능을 제공합니다. 이번에는 LINQ의 Any
확장 메서드를 사용해보겠습니다.
다음 내용을 C# Interactive에 입력한 뒤 실행해보세요.
코드: LinqAnyNumber.cs
> int[] arr = { 1, 2, 3 };
> bool bln = arr.Any(num => num == 2); // bool 값 반환
> bln
true
System.Linq
네임스페이스를 추가하면, 모든 컬렉션 개체에 Any()
메서드가 확장 메서드로 추가됩니다. Any()
메서드를 사용하면 해당 컬렉션에 조건에 맞는 데이터가 있는지 확인할 수 있습니다. arr.Any()
형태로 요청하면 데이터가 있는지 확인할 수 있고 arr.Any(람다식)
형태로 요청하면 람다 식 조건에 맞는 데이터가 있는지 확인할 수 있습니다.
Take()
와 Skip()
메서드로 필요한 건수의 데이터 가져오기
이번에는 LINQ의 Take()
확장 메서드를 사용해보겠습니다.
다음 내용을 C# Interactive에 입력한 뒤 실행해보세요.
코드: LinqTake.cs
> var data = Enumerable.Range(0, 100); // 0~99
> data.Take(5) // 앞에서 5개
TakeIterator { 0, 1, 2, 3, 4 }
> data.Where(n => n % 2 == 0).Take(5) // 짝수 5개
TakeIterator { 0, 2, 4, 6, 8 }
Skip()
메서드는 지정한 수만큼의 데이터를 제외한 컬렉션을 반환합니다. Skip()
과 Take()
메서드는 자주 함께 사용됩니다.
코드: LinqSkipTake.cs
> var data = Enumerable.Range(0, 100); // 0~99
> var next = data.Skip(10).Take(5); // 10개 제외하고 5개 가져오기
> next
TakeIterator { 10, 11, 12, 13, 14 }
Distinct()
확장 메서드로 중복 제거하기
이번에는 LINQ의 Distinct()
확장 메서드를 사용해보겠습니다. Distinct()
메서드를 사용하면 컬렉션(시퀀스)에서 중복된 데이터를 제거합니다.
코드: LinqDistinct.cs
> var data = Enumerable.Repeat(3, 5); // 3을 5개 저장
> var result = data.Distinct(); // Distinct()로 중복 제거
> result
DistinctIterator { 3 }
> int[] arr = { 2, 2, 3, 3, 3 }; // 2와 3을 중복해서 배열에 저장
> arr.Distinct()
DistinctIterator { 2, 3 }
컬렉션의 데이터에서 중복을 제거한 데이터를 가져오는 것을 직접 구현하는 것은 생각보다 쉽지 않습니다. 하지만, LINQ를 사용하면 Distinct()
메서드만 추가로 호출해주면 됩니다.
중복 제거
다음 코드를 실행하면, 중복된 값이 제거되고 유일한 값만 남습니다.
> string[] duplicate = { "2025-07-31", "2025-07-31", "삼계탕" };
> duplicate.Distinct() // 중복 제거
Enumerable.DistinctIterator<string> { "2025-07-31", "삼계탕" }
데이터 정렬과 검색
LINQ의 확장 메서드 중에서 데이터를 오름차순 정렬 또는 내림차순 정렬할 때에는 OrderBy()
와 OrderByDescending()
메서드를 사용합니다. 이 두 메서드의 매개 변수 역시 Where()
메서드와 마찬가지로 람다 식을 입력 받습니다.
OrderBy()
메서드로 문자열 컬렉션 오름차순 정렬하기
3개의 문자열 요소를 가지는 colors
배열을 ABC 또는 가나다 순서의 오름차순으로 정렬된 새로운 값을 얻고자 할 때 OrderBy()
확장 메서드를 사용할 수 있습니다.
코드: LinqOrderBy.cs
> string[] colors = { "Red", "Green", "Blue" };
> IEnumerable<string> sortedColors = colors.OrderBy(name => name);
> foreach (var color in sortedColors)
. {
. Console.WriteLine(color);
. }
Blue
Green
Red
OrderBy()
메서드는 매개 변수로 람다 식을 입력 받는데 정수 또는 문자열일 경우에는 name => name
또는 줄여서 c => c
형태만 사용하면 됩니다.
OrderBy()
와 같은 LINQ 확장 메서드의 반환값은 IEnumerable<string>
대신에 짧게 var
로 사용하는것도 좋습니다.
OrderByDescending()
메서드로 문자열 컬렉션 내림차순 정렬하기
OrderByDescending()
메서드는 컬렉션의 값을 내림차순으로 정렬하여 가져옵니다.
코드: LinqOrderByDescending.cs
> var colors = new List<string> { "Red", "Blue", "Green" };
> var sortedColors = colors.OrderByDescending(c => c);
> foreach (var color in sortedColors)
. {
. Console.WriteLine(color);
. }
Red
Green
Blue
OrderByDescending()
메서드는 배열 또는 컬렉션의 값을 내림 차순으로 정렬하여 가져옵니다. 나중에 알아보겠지만 정수 또는 문자열이 아닌 개체(Object) 형태의 데이터는 OrderBy()
메서드와 OrderByDescending()
메서드를 혼합해서 여러 정렬 조건을 만족한 데이터를 얻을 수 있게 됩니다.
확장 메서드 체이닝
메서드 체이닝처럼 확장 메서드도 체이닝으로 여러 번 호출 가능합니다.
코드: LinqChaining.cs
> List<string> names = new List<string> { ".NET", "C#", "TypeScript" };
> // 체이닝: 확장 메서드를 여러 개 사용
> var results = names.Where(name => name.Length > 2).OrderBy(n => n);
> foreach (var name in results)
. {
. Console.WriteLine(name);
. }
.NET
TypeScript
컬렉션 형태의 데이터에 대해 Where()
, OrderBy()
등의 LINQ의 확장 메서드들을 체이닝으로 여러 번 호출해서 사용할 수 있습니다. 일반적인 C# 프로그래밍에서 하나 이상의 확장 메서드를 함께 사용하는 경우는 자주 있습니다.
짝수인 데이터만 오름차순 정렬하기
Enumerable.Range()
메서드를 사용하여 1부터 10개의 정수를 저장한 numbers
컬렉션을 만들고 이 중에서 Where()
메서드로 짝수인 데이터만 가져온 후 다시 OrderBy()
메서드를 사용하여 오름차순 정렬한 데이터를 가져올 수 있습니다.
C# Interactive에서 직접 Where()
메서드와 OrderBy()
메서드의 실행 결과를 출력할 수 있습니다.
코드: LinqOrderByEven.cs
> var numbers = Enumerable.Range(1, 10);
> numbers
RangeIterator { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
> numbers.Where(n => n % 2 == 0).OrderBy(n => n)
OrderedEnumerable<int, int> { 2, 4, 6, 8, 10 }
결괏값 중에서 RangeIterator
와 OrderedEnumerable<T>
형태는 내부적으로 사용되는 코드라 몰라도 됩니다.
짝수인 데이터만 내림 차순 정렬하기
앞의 예제와 마찬가지로 이번에는 11부터 20까지의 정수를 가지고 짝수만 구한 후 이를 다시 내림차순 정렬한 데이터를 가져오는 코드는 다음과 같습니다.
코드: LinqOrderByDescendingEven.cs
> var numbers = Enumerable.Range(11, 10);
> numbers
RangeIterator { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }
> numbers.Where(n => n % 2 == 0).OrderByDescending(n => n)
OrderedEnumerable<int, int> { 20, 18, 16, 14, 12 }
특정 문자열을 포함하는 컬렉션 가져오기
문자열 배열 또는 컬렉션에서 특정 문자열을 포함하는 데이터만을 가져오는 검색 기능을 구현할 때에는 람다 식에서 Contains()
메서드를 추가로 호출합니다.
다음 코드는 영문자 'e' 하나라도 포함하는 리스트와 "ee" 문자열을 포함하는 리스트를 읽어옵니다.
코드: LinqSearch.cs
> var colors = new List<string> { "Red", "Green", "Blue" };
> var newColors = colors.Where(c => c.Contains("e"));
> foreach (var color in newColors)
. {
. Console.WriteLine(color);
. }
Red
Green
Blue
> var green = colors.Where(c => c.Contains("ee"));
> foreach (var c in green)
. {
. Console.WriteLine(c);
. }
Green
리스트 형태로 저장된 컬렉션에서 특정 문자열을 검색할 때 람다 식에서 Contains()
메서드를 사용하여 문자열을 포함하는 컬렉션을 가져올 수 있습니다.
LINQ를 사용하지 않고 문자열 컬렉션에서 특정 키워드에 해당하는 컬렉션만 따로 뽑는 작업을 for
문과 if
문으로 작성할 수 있지만 이를 LINQ를 사용하면 좀 더 편하게 진행할 수 있는 장점을 가지게 됩니다.
LINQ의 Where()
메서드에서 Contains()
를 사용하면 일반적으로 대소문자를 구분합니다. 만약, 대소문자를 구분하지 않고 검색을하고자할 때에는 다음 샘플 코드처럼 ToUpper()
또는 ToLower()
메서드를 사용하여 한쪽으로 바꾼 후 검색하면 대소문자를 구분하지 않고 값을 검색할 수 있습니다.
코드: LinqWhereContains.cs
> List<string> names = new List<string> { "ASP.NET", "Blazor", "C#" };
> names.Where(n => n.Contains("a"))
Enumerable.WhereListIterator<string> { "Blazor" }
> names.Where(n => n.Contains("A"))
Enumerable.WhereListIterator<string> { "ASP.NET" }
> names.Where(n => n.ToUpper().Contains("A"))
Enumerable.WhereListIterator<string> { "ASP.NET", "Blazor" }
Single()
확장 메서드와 SingleOrDefault()
확장 메서드
컬렉션에서 조건에 맞는 단 하나의 값을 가져오는 확장 메서드에는 Single()
과 SingleOrDefault()
가 있습니다.
Single()
:null
값이면 예외가 발생합니다. 즉, 에러가 발생합니다.SingleOrDefault()
: 값이 없으면null
값을 반환합니다.
다음 내용을 C# Interactive 코드 편집 창에 입력한 뒤 실행해보세요.
코드: LinqSingle.cs
> List<string> colors = new List<string> { "Red", "Green", "Blue" };
> string red = colors.Single(c => c == "Red");
> red
"Red"
> // 없는 데이터 요청시 예외 발생
> string black = colors.Single(color => color == "Black");
시퀀스에 일치하는 요소가 없습니다.
+ System.Linq.Enumerable.Single<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
> // 없는 데이터 요청시 null 값 반환
> string black = colors.SingleOrDefault(color => color == "Black");
> black
null
Single()
메서드는 컬렉션에 값이 없으면 위 실행 결과처럼 에러가 발생합니다. SingleOrDefault()
메서드는 에러는 나지 않고 null
값을 그대로 반환합니다.
First()
확장 메서드와 FirstOrDefault()
확장 메서드
앞서 살펴본 Single()
과 SingleOrDefault()
와 비슷하지만 하나 이상의 데이터 중에서 첫 번째 데이터를 가져옵니다. 즉, 컬렉션에서 첫번째 요소를 가져옵니다.
First()
: 첫번째 요소가 없으면 에러가 발생합니다.FirstOrDefault()
: 첫번째 요소가 없으면 기본값을 반환합니다.
다음 내용을 C# Interactive 코드 편집 창에 입력한 뒤 실행해보세요.
코드: LinqFirst.cs
> List<string> colors = new List<string> { "Red", "Green", "Blue" };
> colors.First(c => c == "Red")
"Red"
> colors.First(color => color == "Black")
시퀀스에 일치하는 요소가 없습니다.
+ System.Linq.Enumerable.First<TSource>(IEnumerable<TSource>, Func<TSource, bool>)
> colors.FirstOrDefault(color => color == "Black")
null
메서드 구문과 쿼리 구문
앞으로 우리는 개체 지향 프로그래밍 개념을 공부한 후에 LINQ의 더 많은 내용들을 학습하게 될 것입니다. 지금까지 살펴본 LINQ의 확장 메서드들을 통해서 몇 가지 알고리즘 처리를 손쉽게 진행했습니다. LINQ에는 이처럼 확장 메서드들을 사용하여 원하는 로직을 처리하는 방법과 동일한 기능을 하는 또 다른 문법인 쿼리 구문(Query Syntax)도 제공합니다.
- 메서드 구문(Method Syntax): 이번 장에서 지금까지 살펴본
Where()
메서드와 같은 메서드를 사용하여 컬렉션을 다루는 방법 - 쿼리 구문(Query Syntax):
from
,where
,select
와 같은 키워드를 사용하여 쿼리(Query) 형태로 컬렉션을 다루는 방법
1부터 10까지 정수 컬렉션을 다음과 같이 만들었다면 짝수 데이터만을 가져온 후 내림 차순으로 정렬하는 내용을 메서드 구문과 쿼리 구문으로 동일하게 처리할 수 있습니다.
> var numbers = Enumerable.Range(1, 10);
메서드 구문을 사용하면 다음과 같이 처리합니다.
> numbers.Where(n => n % 2 == 0).OrderByDescending(n => n)
OrderedEnumerable<int, int> { 10, 8, 6, 4, 2 }
위와 동일한 기능을 쿼리 구문을 사용하면 다음과 같이 코드를 작성할 수 있습니다.
> (from n in numbers where n % 2 == 0 orderby n descending select n)
OrderedEnumerable<int, int> { 10, 8, 6, 4, 2 }
처음으로 from
, where
, orderby
, descending
, select
의 5가지 키워드가 나왔지만 메서드 구문을 쿼리 구문으로 변경하면 이와 같은 형태가 되는구나 정도로 살펴보면 됩니다.
집계 함수의 결괏값은 다음과 같이 쿼리 구문과 메서드 구문을 함께 사용하여 구할 수 있습니다.
코드: QuerySyntaxMethodSyntax.cs
> var numbers = Enumerable.Range(1, 10);
> (from n in numbers where n % 2 == 0 select n).Sum()
30
> (from n in numbers where n % 2 == 0 select n).Count()
5
> (from n in numbers where n % 2 == 0 select n).Average()
6
> (from n in numbers where n % 2 == 0 select n).Max()
10
> (from n in numbers where n % 2 == 0 select n).Min()
2
쿼리 구문을 사용하여 컬렉션에서 짝수 데이터만 추출하기
다음 코드처럼 from
, where
, select
를 여러 줄에 걸쳐 작성할 수 있으며 where
절에서 조건 처리를 하여 짝수인 데이터만을 가져올 수 있습니다. 쿼리 구문은 새로운 변수인 q
변수에 담아서 사용할 수 있습니다.
코드: LinqFromWhereSelect.cs
> int[] arr = { 1, 2, 3, 4, 5 };
> var q =
. from a in arr
. where a % 2 == 0
. select a;
> q
Enumerable.WhereArrayIterator<int> { 2, 4 }
이번에는 LINQ의 쿼리 구문을 사용하여 배열에서 짝수 데이터만 추출하는 방법을 예제로 만들어보겠습니다.
코드: FromWhereSelect.cs
// LINQ(Language INtegrated Query)
using System;
using System.Linq;
class FromWhereSelect
{
static void Main()
{
int[] arr = { 1, 2, 3, 4, 5 };
var evenNumbers =
from num in arr
where num % 2 == 0
select num;
foreach (var number in evenNumbers)
{
Console.WriteLine($"짝수: {number}");
}
}
}
짝수: 2
짝수: 4
LINQ의 확장 메서드를 사용하는 방법과 별반 다를 것 없이 from
, where
, select
구문을 사용하여 배열에서 짝수만을 읽어오는 구문을 작성할 수 있습니다.
쿼리 구문을 사용하여 컬렉션 정렬하기
메서드 구문과 쿼리 구문을 사용하여 컬렉션에서 홀수만 가져온 후 오름 차순으로 정렬하여 출력하는 예제를 만들어 보겠습니다. 다음 코드를 편집기에 작성한 후 실행해보세요.
코드: LinqQuerySyntax.cs
// 메서드 구문(Method Syntax)과 쿼리 구문(Query Syntax) 비교
using System;
using System.Collections.Generic;
using System.Linq;
class LinqQuerySyntax
{
static void Main()
{
int[] numbers = { 3, 2, 1, 4, 5 };
//[1] 메서드 구문
IEnumerable<int> methodSyntax =
numbers.Where(n => n % 2 == 1).OrderBy(n => n);
foreach (var n in methodSyntax)
{
Console.WriteLine(n);
}
//[2] 쿼리 구문
IEnumerable<int> querySyntax =
from num in numbers
where num % 2 == 1
orderby num
select num;
foreach (var n in querySyntax)
{
Console.WriteLine(n);
}
}
}
1
3
5
1
3
5
[1]
번 구문의 메서드 구문을 사용하는 코드와 [2]
번 구문의 쿼리 구문을 사용하는 방식은 모양만 다를 뿐 실행 결과는 동일합니다. 쿼리 구문은 한 줄로 모두 작성해도 되고, 위 예제처럼 키워드별로 줄단위로 작성하여 가독성을 높일 수 있습니다.
Select()
확장 메서드를 사용하여 새로운 형태로 가공
LINQ에서 제공하는 Select()
확장 메서드는 컬렉션에서 새로운 형태의 데이터로 만들어 사용할 수있습니다.
코드: Map.cs
using System;
using System.Linq;
class Map
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
// `Select()` 확장 메서드를 사용하여 새로운 형태로 가공
var nums = numbers.Select(it => it * it);
foreach (var num in nums)
{
Console.WriteLine(num);
}
}
}
1
4
9
16
25
numbers
배열에서 하나씩 데이터를 조회해서 각각의 값을 곱한 새로운 형태인 nums
컬렉션을 Select()
확장 메서드를 통해서 생성해낼 수 있습니다. Select()
의 결괏값은 따로 클래스 이름이 정해지지 않은 익명 형식이기에 반드시 var
키워드와 함께 사용되어야 합니다.
Select()
확장 메서드는 n => new { Name = n }
형태를 사용하여 새로운 개체를 만들고 해당 개체의 Name
속성에 값을 대입하여 사용할 수도 있습니다. 아래 코드는 한번 정도 실행해본 후 넘어가도 됩니다.
코드: LinqSelect.cs
> var names = new List<string> { "홍길동", "백두산", "임꺽정" };
.
. // `Select()` 확장 메서드에서 익명 형식을 사용하기에 var로 받아야 함
. var nameObjects = names.Select(n => new { Name = n });
.
. foreach (var name in nameObjects)
. {
. Console.WriteLine(name.Name);
. }
홍길동
백두산
임꺽정
ForEach()
메서드로 반복 출력하기
LINQ 식에서 ForEach()
메서드를 사용하면 List<T>
형태를 갖는 리스트의 값 만큼 반복하는 코드를 작성할 수 있습니다. 따로 for
문 또는 foreach
문을 사용하지 않고 LINQ 식에 출력 코드를 포함하여 함수형 프로그래밍(Functional Programming) 스타일로 작성할 수 있습니다.
코드: LinqForEach.cs
> var numbers = new List<int>() { 10, 20, 30, 40, 50 };
> numbers.Where(n => n <= 20).ToList().ForEach(n => Console.WriteLine(n));
10
20
> var names = new List<string>() { "RedPlus", "Taeyo" };
> names.ForEach(n => Console.WriteLine(n));
RedPlus
Taeyo
위 코드는 20보다 작거나 같은 정수를 출력하는 코드를 foreach
문을 사용하지 않고 하나의 코드 흐름에 묶어서 관리하는 내용을 보여줍니다.
참고로, Visual Studio에서 ForEach()
메서드에 마우스 커서를 올려보면 List<T>
에 대한 반복 형태의 도움말을 볼 수 있습니다. 즉, ForEach()
메서드는 List<T>
에 대해서 사용이 가능한 메서드입니다.
LINQ의 Where()
메서드의 결괏값은 IEnumerable<T>
입니다. 만약, Where()
메서드의 결과를 바로 ForEach()
메서드를 사용하여 출력하고자 한다면 IEnumerable<T>
형태를 List<T>
형태로 변경해야 ForEach()
메서드가 사용이 가능합니다. 그래서 Where().ToList().ForEach()
형태를 사용했습니다. 당연히 names
변수는 그 자체가 List<T>
형태이기에 바로 ForEach()
메서드를 사용 가능합니다.
Aggregate
메서드
Aggregate
메서드는 LINQ에서 컬렉션의 모든 요소를 하나의 값으로 집계하는 데 사용됩니다. 컬렉션의 각 요소를 누적 처리하여 최종 결과를 반환합니다.
컬렉션의 첫 번째 요소를 초깃값으로 사용하고, 이후 요소를 누적하여 결과를 반환할 수 있습니다.
> var numbers = new[] { 1, 2, 3, 4 };
> var sum = numbers.Aggregate((accumulator, current) => accumulator + current);
> sum
10
초깃값을 명시적으로 설정하여 집계를 시작할 수도 있습니다. 초깃값은 집계 연산의 시작점으로 사용됩니다.
> var sumWithSeed =
. numbers.Aggregate(10, (accumulator, current) => accumulator + current);
> sumWithSeed
20
초깃값과 집계 연산을 수행한 후, 최종 결과를 변환하는 선택기를 추가로 지정할 수도 있습니다.
> var result = numbers.Aggregate(10, (accumulator, current)
. => accumulator + current, accumulator => $"총합: {accumulator}");
> result
"총합: 20"
컬렉션의 문자열을 공백으로 연결할 수도 있습니다.
> var words = new[] { "C#", "LINQ", "Aggregate" };
> var sentence =
. words.Aggregate((accumulator, word) => accumulator + " " + word);
> sentence
"C# LINQ Aggregate"
또한, 컬렉션의 최댓값을 계산할 수도 있습니다.
> var numbers = new[] { 5, 3, 8, 1, 4 };
> var max = numbers.Aggregate((accumulator, current)
. => accumulator > current ? accumulator : current);
> max
8
다음은 초깃값(시드값)을 사용하는 경우와 생략하는 경우의 동작 차이를 보여주는 코드입니다. 실행하면 동일한 결과를 반환하지만 내부 동작은 다릅니다.
using System;
using System.Linq;
class AggregateMethod
{
static void Main()
{
int[] numbers = { 1, 2, 3 };
// 초깃값이 0으로 설정된 경우
int sumWithSeed =
numbers.Aggregate(0, (accumulator, current) => accumulator + current);
// 초깃값이 생략된 경우 (첫 번째 요소가 초깃값이 됨)
int sumWithoutSeed =
numbers.Aggregate((accumulator, current) => accumulator + current);
Console.WriteLine($"Sum with seed(0): {sumWithSeed}"); // 출력: 6
Console.WriteLine($"Sum without seed: {sumWithoutSeed}"); // 출력: 6
}
}
Sum with seed (0): 6
Sum without seed: 6
초깃값이 설정된 경우, Aggregate(0, ...)
는 명시적으로 설정된 초깃값 0
을 사용하여 집계를 시작합니다. 이 경우, 집계는 0 + 1 + 2 + 3
순서로 진행되며 최종 결과는 6
이 됩니다.
초깃값이 생략된 경우, Aggregate(...)
는 컬렉션의 첫 번째 요소를 초깃값으로 사용합니다. 따라서 집계는 1 + 2 + 3
순서로 진행되며, 결과는 동일하게 6
입니다.
참고로, 두 방식은 비어 있는 컬렉션에서 동작이 달라집니다. 초깃값이 설정된 경우 초깃값 그대로 반환되지만, 초깃값이 생략된 경우에는 System.InvalidOperationException
예외가 발생합니다.
기타
Zip()
확장 메서드로 배열 병합하기
이번에는 LINQ의 Zip
확장 메서드를 사용해보겠습니다. 일반적인 환경에서는 중요하지 않은 메서드이므로 간단히 살펴보고 넘어갑니다.
코드: LinqZip.cs
> // LINQ Zip() 확장 메서드: 관련있는 2개의 시퀀스(컬렉션)를 묶어서 출력
> int[] numbers = { 1, 2, 3 };
> string[] words = { "하나", "둘" };
> var numbersAndWords = numbers.Zip(words, (first, second) => first + "-" + second);
> numbersAndWords
ZipIterator { "1-하나", "2-둘" }
Zip
확장 메서드는 2개의 배열 중 하나가 먼저 끝날 때까지 반복하면서 배열의 내용을 원하는 모양으로 병합할 수 있습니다. 위 예제에서는 numbers
배열은 3개의 요소를 갖지만, words
배열이 2개의 요소를 갖기에 2번의 반복으로 데이터를 병합하여 새로운 배열을 생성합니다.
기타 확장메서드
추가적으로 Microsoft Learn 사이트를 통해서 다음 확장 메서드들에 대해서도 관련 문서를 참조해 보면 좋습니다.
SelectMany()
TakeWhile()
참고 코드조각: DotNetKorea - SeminarController.cs
public ViewResult AllTags()
{
ViewBag.Message = "전체 태그 리스트";
// Enumerable.SelectMany 메서드
// 시퀀스의 각 요소를 IEnumerable<T>에 투영하고 결과 시퀀스를 단일 시퀀스로 평면화
var tags = Sessions.All.SelectMany(x => x.Tags).Distinct().OrderBy(x => x);
return View(tags);
}
장 요약
이번 강의는 다른 프로그래밍 언어와 달리 C#의 혁신적인 기능 중 하나인 LINQ를 다루었습니다. 아직 LINQ의 더 많은 기능들이 남아 있지만 배열과 컬렉션에서 데이터를 여러 가지 방식으로 조회하는 방법을 다루어 보았습니다. 앞으로 클래스와 개체를 다룬 후에 좀 더 다양한 LINQ의 확장 메서드와 쿼리 구문을 다루어 보도록 하겠습니다.