1. 데이터 필터링
LINQ(Language Integrated Query)는 컬렉션의 데이터를 특정 조건에 따라 걸러내는 강력한 방법을 제공합니다. 주로 where 절을 사용하여 조건을 정의하며, 이는 쿼리 구문 또는 메서드 구문으로 작성될 수 있습니다.
쿼리 구문으로 필터링
public static void FilterDataQuerySyntax()
{
var eliteDrivers = from d in Formula1.GetChampions()
where d.Wins > 15 && (d.Country == "Brazil" || d.Country == "Austria")
select d;
foreach (var driver in eliteDrivers)
{
Console.WriteLine($"{driver:A}");
}
}
메서드 구문으로 필터링
public static void FilterDataMethodSyntax()
{
var eliteDrivers = Formula1.GetChampions()
.Where(d => d.Wins > 15 && (d.Country == "Brazil" || d.Country == "Austria"));
foreach (var driver in eliteDrivers)
{
Console.WriteLine($"{driver:A}");
}
}
2. 인덱스를 활용한 필터링
Where 메서드의 오버로드 중 하나는 현재 요소의 인덱스를 함께 전달받는 기능을 제공합니다. 이를 통해 요소의 값뿐만 아니라 컬렉션 내에서의 위치를 기반으로 필터링할 수 있습니다.
public static void FilterByIndex()
{
// 성이 'A'로 시작하고 인덱스가 홀수인 드라이버를 반환합니다.
var indexedDrivers = Formula1.GetChampions()
.Where((d, idx) => d.LastName.StartsWith("A") && idx % 2 != 0); // 인덱스 0부터 시작, 홀수 번째
foreach (var driver in indexedDrivers)
{
Console.WriteLine($"{driver:A}");
}
}
3. 타입 기반 필터링
OfType<T>() 확장 메서드는 혼합된 타입의 컬렉션에서 특정 타입의 요소만 추출할 때 유용합니다. 비제네릭 컬렉션이나 object 배열에서 특정 타입만 필터링할 때 자주 사용됩니다.
public static void FilterByType()
{
object[] mixedItems = { "apple", 123, 4.5, "banana", true, "cherry" };
// OfType<string>()을 사용하여 컬렉션에서 문자열 타입의 요소만 추출합니다.
var stringElements = mixedItems.OfType<string>();
foreach (var item in stringElements)
{
Console.WriteLine(item);
}
}
4. 복합 From 절 (SelectMany)
여러 개의 from 절을 사용하거나 SelectMany 메서드를 사용하면 중첩된 컬렉션의 요소를 단일 평면화된 시퀀스로 결합하여 쿼리할 수 있습니다. 이는 한 객체의 하위 컬렉션에 접근하여 필터링할 때 유용합니다.
쿼리 구문으로 복합 From
public static void CompoundFromQuerySyntax()
{
// 첫 번째 from 절은 Racer 객체를 가져오고, 두 번째 from 절은 각 Racer의 Cars 속성을 탐색합니다.
// Ferrari를 운전한 챔피언들을 성 기준으로 정렬하여 이름을 반환합니다.
var ferrariPilots = from driver in Formula1.GetChampions()
from carType in driver.Cars
where carType == "Ferrari"
orderby driver.LastName
select $"{driver.FirstName} {driver.LastName}";
foreach (var pilotName in ferrariPilots)
{
Console.WriteLine(pilotName);
}
}
메서드 구문으로 SelectMany
public static void SelectManyMethodSyntax()
{
// SelectMany는 각 드라이버와 그들이 운전한 차량을 짝지어 평면화된 시퀀스를 생성합니다.
// 이후 필터링, 정렬, 프로젝션을 수행합니다.
var ferrariPilots = Formula1.GetChampions()
.SelectMany(driver => driver.Cars, (driver, carType) => new { Driver = driver, CarType = carType })
.Where(item => item.CarType == "Ferrari")
.OrderBy(item => item.Driver.LastName)
.Select(item => $"{item.Driver.FirstName} {item.Driver.LastName}");
foreach (var pilotName in ferrariPilots)
{
Console.WriteLine(pilotName);
}
}
5. 데이터 정렬
orderby 절 또는 OrderBy, OrderByDescending, ThenBy, ThenByDescending 메서드를 사용하여 쿼리 결과를 정렬할 수 있습니다. 여러 기준에 따라 정렬할 수도 있습니다.
내림차순 정렬 (쿼리 구문)
public static void SortDescendingQuerySyntax()
{
Console.WriteLine("브라질 출신 챔피언들을 우승 횟수 내림차순으로 정렬:");
Console.WriteLine();
var brazilianChamps = from driver in Formula1.GetChampions()
where driver.Country == "Brazil"
orderby driver.Wins descending
select driver;
foreach (var driver in brazilianChamps)
{
Console.WriteLine($"{driver:A}");
}
}
내림차순 정렬 (메서드 구문)
public static void SortDescendingMethodSyntax()
{
Console.WriteLine("브라질 출신 챔피언들을 우승 횟수 내림차순으로 정렬:");
Console.WriteLine();
var brazilianChamps = Formula1.GetChampions()
.Where(driver => driver.Country == "Brazil")
.OrderByDescending(driver => driver.Wins);
foreach (var driver in brazilianChamps)
{
Console.WriteLine($"{driver:A}");
}
}
다중 정렬 (메서드 구문)
public static void SortByMultipleCriteria()
{
Console.WriteLine("국가, 성, 이름 순으로 정렬된 상위 7명의 챔피언:");
Console.WriteLine();
// 먼저 국가별, 다음으로 성별, 마지막으로 이름별로 정렬하고 상위 7개만 가져옵니다.
var topDriversSorted = Formula1.GetChampions()
.OrderBy(driver => driver.Country)
.ThenBy(driver => driver.LastName)
.ThenBy(driver => driver.FirstName)
.Take(7);
foreach (var driver in topDriversSorted)
{
Console.WriteLine($"{driver.Country}: {driver.LastName}, {driver.FirstName}");
}
}
6. 데이터 그룹화
group by 절 또는 GroupBy 메서드를 사용하여 공통된 키를 가진 요소를 그룹으로 묶을 수 있습니다. 그룹화된 결과에 추가적인 필터링이나 정렬을 적용할 수도 있습니다.
쿼리 구문으로 그룹화
public static void GroupDataQuerySyntax()
{
// Country 속성을 기준으로 드라이버를 그룹화하고, 그룹 내 드라이버 수가 2명 이상인 국가만 선택합니다.
// 이후 그룹의 크기 내림차순, 국가 이름 오름차순으로 정렬합니다.
var countryStats =
from driver in Formula1.GetChampions()
group driver by driver.Country into countryGroup
let driverCount = countryGroup.Count() // let 절을 활용하여 그룹 내 변수 선언
where driverCount >= 2
orderby driverCount descending, countryGroup.Key
select new
{
Nation = countryGroup.Key,
Champions = driverCount
};
foreach (var item in countryStats)
{
Console.WriteLine($"{item.Nation,-10} {item.Champions}");
}
}
메서드 구문으로 그룹화
public static void GroupDataMethodSyntax()
{
var countryStats = Formula1.GetChampions()
.GroupBy(driver => driver.Country)
.Select(g => new { Group = g, Count = g.Count() }) // 그룹과 개수를 포함하는 익명 타입 생성
.OrderByDescending(item => item.Count)
.ThenBy(item => item.Group.Key)
.Where(item => item.Count >= 2)
.Select(item => new
{
Nation = item.Group.Key,
Champions = item.Count
});
foreach (var item in countryStats)
{
Console.WriteLine($"{item.Nation,-10} {item.Champions}");
}
}
7. 중첩 객체 그룹화
그룹화된 결과 내에서 다시 쿼리를 수행하여 중첩된 객체 컬렉션을 생성할 수 있습니다. 예를 들어, 특정 국가의 드라이버 목록을 그룹화된 결과에 포함하는 경우입니다.
쿼리 구문으로 중첩 그룹화
public static void GroupNestedObjectsQuerySyntax()
{
var nationsWithDrivers = from driver in Formula1.GetChampions()
group driver by driver.Country into countryGroup
let count = countryGroup.Count()
where count >= 2
orderby count descending, countryGroup.Key
select new
{
Nation = countryGroup.Key,
TotalDrivers = count,
DriversList = from d in countryGroup
orderby d.LastName
select d.FirstName + " " + d.LastName
};
foreach (var item in nationsWithDrivers)
{
Console.WriteLine($"{item.Nation,-10} {item.TotalDrivers}");
foreach (var name in item.DriversList)
{
Console.Write($"{name}; ");
}
Console.WriteLine();
}
}
8. 내부 조인 (Inner Join)
join 절 또는 Join 메서드를 사용하여 두 개 이상의 컬렉션을 공통된 키를 기반으로 결합할 수 있습니다. 내부 조인은 양쪽 컬렉션 모두에서 일치하는 요소만 반환합니다.
쿼리 구문으로 내부 조인
public static void InnerJoinQuerySyntax()
{
// 각 드라이버 챔피언십 연도를 평면화합니다.
var driverChampionships = from d in Formula1.GetChampions()
from year in d.Years
select new
{
Year = year,
DriverName = d.FirstName + " " + d.LastName
};
// 각 컨스트럭터 챔피언십 연도를 평면화합니다.
var constructorChampionships = from team in Formula1.GetConstructorChampions()
from year in team.Years
select new
{
Year = year,
TeamName = team.Name
};
// 드라이버 챔피언십과 컨스트럭터 챔피언십을 연도 기준으로 조인합니다.
var combinedTitles = (from driverChamp in driverChampionships
join constructorChamp in constructorChampionships on driverChamp.Year equals constructorChamp.Year
orderby constructorChamp.Year
select new
{
driverChamp.Year,
WorldChampion = driverChamp.DriverName,
Constructor = constructorChamp.TeamName
}).Take(8); // 상위 8개 결과만 표시
Console.WriteLine("연도 월드 챔피언\t\t 컨스트럭터 우승팀");
foreach (var entry in combinedTitles)
{
Console.WriteLine($"{entry.Year}: {entry.WorldChampion,-20} {entry.Constructor}");
}
}
메서드 구문으로 내부 조인
public static void InnerJoinMethodSyntax()
{
var driverChampionships = Formula1.GetChampions()
.SelectMany(d => d.Years, (d, year) =>
new
{
Year = year,
DriverName = $"{d.FirstName} {d.LastName}"
});
var constructorChampionships = Formula1.GetConstructorChampions()
.SelectMany(team => team.Years, (team, year) =>
new
{
Year = year,
TeamName = team.Name
});
var combinedTitles = driverChampionships.Join(
constructorChampionships,
driverChamp => driverChamp.Year,
constructorChamp => constructorChamp.Year,
(driverChamp, constructorChamp) =>
new
{
driverChamp.Year,
WorldChampion = driverChamp.DriverName,
Constructor = constructorChamp.TeamName
}).OrderBy(entry => entry.Year).Take(8); // 상위 8개 결과만 표시
Console.WriteLine("연도 월드 챔피언\t\t 컨스트럭터 우승팀");
foreach (var entry in combinedTitles)
{
Console.WriteLine($"{entry.Year}: {entry.WorldChampion,-20} {entry.Constructor}");
}
}
9. 좌측 외부 조인 (Left Outer Join)
좌측 외부 조인은 첫 번째 컬렉션의 모든 요소를 포함하고, 두 번째 컬렉션에서 일치하는 요소가 없더라도 첫 번째 컬렉션의 요소는 유지합니다. 일치하는 항목이 없을 경우 두 번째 컬렉션의 요소는 기본값(null)으로 처리됩니다. GroupJoin과 DefaultIfEmpty를 조합하여 구현할 수 있습니다.
쿼리 구문으로 좌측 외부 조인
public static void LeftOuterJoinQuerySyntax()
{
var driverWins = from d in Formula1.GetChampions()
from year in d.Years
select new
{
Year = year,
Driver = d.FirstName + " " + d.LastName
};
var constructorWins = from team in Formula1.GetConstructorChampions()
from year in team.Years
select new
{
Year = year,
Team = team.Name
};
// 드라이버 우승 연도에 해당하는 컨스트럭터 우승팀이 없는 경우에도 드라이버 정보는 유지합니다.
var championshipSummary =
(from dw in driverWins
join cw in constructorWins on dw.Year equals cw.Year into teamResults
from cw in teamResults.DefaultIfEmpty()
orderby dw.Year
select new
{
dw.Year,
ChampionDriver = dw.Driver,
WinningConstructor = cw == null ? "미정 또는 해당 없음" : cw.Team
}).Take(12); // 상위 12개 결과만 표시
Console.WriteLine("연도 챔피언 드라이버\t 우승 컨스트럭터");
foreach (var item in championshipSummary)
{
Console.WriteLine($"{item.Year}: {item.ChampionDriver,-20} {item.WinningConstructor}");
}
}
메서드 구문으로 좌측 외부 조인
public static void LeftOuterJoinMethodSyntax()
{
var driverWins = Formula1.GetChampions()
.SelectMany(d => d.Years, (d, year) =>
new
{
Year = year,
Driver = $"{d.FirstName} {d.LastName}"
});
var constructorWins = Formula1.GetConstructorChampions()
.SelectMany(team => team.Years, (team, year) =>
new
{
Year = year,
Team = team.Name
});
var championshipSummary =
driverWins.GroupJoin(
constructorWins,
dw => dw.Year,
cw => cw.Year,
(dw, teamResults) => new
{
Year = dw.Year,
ChampionDriver = dw.Driver,
WinningConstructors = teamResults
})
.SelectMany(
item => item.WinningConstructors.DefaultIfEmpty(),
(dw, cw) => new
{
Year = dw.Year,
ChampionDriver = dw.ChampionDriver,
WinningConstructor = cw?.Team ?? "미정 또는 해당 없음"
})
.OrderBy(item => item.Year)
.Take(12); // 상위 12개 결과만 표시
Console.WriteLine("연도 챔피언 드라이버\t 우승 컨스트럭터");
foreach (var item in championshipSummary)
{
Console.WriteLine($"{item.Year}: {item.ChampionDriver,-20} {item.WinningConstructor}");
}
}
10. 그룹 조인 (Group Join)
그룹 조인은 한 컬렉션의 각 요소와 다른 컬렉션에서 일치하는 요소들을 하위 그룹으로 연결합니다. 이는 한 부모에 여러 자식 레코드를 연결할 때 유용합니다. `GroupJoin` 메서드는 쿼리 구문의 `join ... into` 절과 유사합니다.
public static void GroupJoinExample()
{
// 각 챔피언십 연도의 상위 3위 드라이버 정보를 평면화합니다.
var podiumFinishers = Formula1.GetChampionships()
.SelectMany(cs => new List<(int Year, int Position, string FirstName, string LastName)>
{
(cs.Year, Position: 1, FirstName: cs.First.FirstName(), LastName: cs.First.LastName()),
(cs.Year, Position: 2, FirstName: cs.Second.FirstName(), LastName: cs.Second.LastName()),
(cs.Year, Position: 3, FirstName: cs.Third.FirstName(), LastName: cs.Third.LastName())
});
// 모든 챔피언 드라이버에 대해, 해당 드라이버가 연도별 포디움에 올랐던 기록들을 그룹으로 연결합니다.
var driverPodiumRecords = Formula1.GetChampions()
.GroupJoin(podiumFinishers,
champ => (champ.FirstName, champ.LastName), // 복합 키
podium => (podium.FirstName, podium.LastName), // 복합 키
(champ, annualResults) =>
(champ.FirstName, champ.LastName, champ.Wins, champ.Starts, PodiumResults: annualResults));
foreach (var driverData in driverPodiumRecords)
{
Console.WriteLine($"{driverData.FirstName} {driverData.LastName} (우승: {driverData.Wins}, 출전: {driverData.Starts})");
foreach (var result in driverData.PodiumResults)
{
Console.WriteLine($"\t- {result.Year}년 {result.Position}위");
}
}
}
11. 집합 연산
LINQ는 두 컬렉션에 대한 합집합, 교집합, 차집합 등의 집합 연산을 수행하는 메서드를 제공합니다. 주요 메서드로는 Distinct(), Union(), Intersect(), Except()가 있습니다.
public static void SetOperationsExample()
{
IEnumerable<Racer> getDriversByCarBrand(string brand) =>
from r in Formula1.GetChampions()
from car in r.Cars
where car == brand
orderby r.LastName
select r;
Console.WriteLine("Ferrari와 McLaren 둘 다 운전한 월드 챔피언:");
// Ferrari를 운전한 드라이버와 McLaren을 운전한 드라이버의 교집합을 찾습니다.
foreach (var driver in getDriversByCarBrand("Ferrari").Intersect(getDriversByCarBrand("McLaren")))
{
Console.WriteLine(driver);
}
Console.WriteLine("\n챔피언은 아니지만 포디움에 올랐던 드라이버:");
// 모든 챔피언십 연도의 상위 3위 드라이버 정보 (RacerInfo 구조체가 필요하다고 가정)
// RacerInfo는 FirstName, LastName 속성을 가진다고 가정합니다.
var allPodiumFinishers = Formula1.GetChampionships().SelectMany(cs => new List<RacerInfo>()
{
new RacerInfo { Year = cs.Year, Position = 1, FirstName = cs.First.FirstName(), LastName = cs.First.LastName() },
new RacerInfo { Year = cs.Year, Position = 2, FirstName = cs.Second.FirstName(), LastName = cs.Second.LastName() },
new RacerInfo { Year = cs.Year, Position = 3, FirstName = cs.Third.FirstName(), LastName = cs.Third.LastName() }
});
var nonChampionsOnPodium = allPodiumFinishers.Select(p => new { p.FirstName, p.LastName })
.Distinct() // 중복 제거
.Except(Formula1.GetChampions().Select(c => new { c.FirstName, c.LastName }));
foreach (var driver in nonChampionsOnPodium)
{
Console.WriteLine($"{driver.FirstName} {driver.LastName}");
}
}
// 예시를 위한 RacerInfo 구조체 (실제 코드에서는 Formula1.cs에 정의되어 있다고 가정)
public struct RacerInfo
{
public int Year { get; set; }
public int Position { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
12. Zip 연산
Zip() 메서드는 두 개의 시퀀스를 병렬로 결합하여 새로운 시퀀스를 생성합니다. 각 시퀀스의 첫 번째 요소들을 짝지어 결합하고, 다음으로 두 번째 요소들을 짝지어 결합하는 식으로 진행됩니다. 두 시퀀스의 길이가 다를 경우, 더 짧은 시퀀스의 끝에 도달하면 결합을 멈춥니다.
public static void ZipOperationExample()
{
var italianDriverNames = from r in Formula1.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select r.FirstName + " " + r.LastName;
var italianDriverStarts = from r in Formula1.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select r.Starts;
// 이탈리아 드라이버의 이름과 출전 횟수를 병합합니다.
var combinedInfo = italianDriverNames.Zip(italianDriverStarts,
(name, starts) => $"{name}, 출전: {starts}회");
foreach (var info in combinedInfo)
{
Console.WriteLine(info);
}
}
13. 시퀀스 분할 (Partitioning)
Skip()과 Take() 메서드를 사용하여 시퀀스의 일부를 건너뛰거나 특정 개수만큼 가져와 시퀀스를 분할할 수 있습니다. 이는 페이징 기능을 구현할 때 주로 사용됩니다.
public static void PartitioningExample()
{
int entriesPerPage = 7; // 페이지당 항목 수
int totalChampions = Formula1.GetChampions().Count();
int numberOfPages = (int)Math.Ceiling(totalChampions / (double)entriesPerPage);
for (int page = 0; page < numberOfPages; page++)
{
Console.WriteLine($"--- 페이지 {page + 1} ---");
// 현재 페이지에 해당하는 항목만 건너뛰고 가져옵니다.
var paginatedDrivers =
(from driver in Formula1.GetChampions()
orderby driver.LastName, driver.FirstName
select driver.FirstName + " " + driver.LastName)
.Skip(page * entriesPerPage)
.Take(entriesPerPage);
foreach (var name in paginatedDrivers)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
}
14. 집계 연산자
LINQ는 시퀀스에 대한 통계 정보를 계산하는 다양한 집계 연산자를 제공합니다. Count(), Sum(), Min(), Max(), Average() 등이 대표적입니다.
Count() 예제
public static void AggregateCountExample()
{
// 3회 이상 챔피언십을 우승한 드라이버를 우승 횟수 내림차순, 성 오름차순으로 정렬합니다.
var multiTimeChampions = from r in Formula1.GetChampions()
let champYears = r.Years.Count()
where champYears >= 3
orderby champYears descending, r.LastName
select new
{
FullName = r.FirstName + " " + r.LastName,
ChampionshipsWon = champYears
};
foreach (var record in multiTimeChampions)
{
Console.WriteLine($"{record.FullName} (총 우승: {record.ChampionshipsWon}회)");
}
}
Sum() 예제
public static void AggregateSumExample()
{
// 각 국가별 총 우승 횟수를 계산하고, 우승 횟수 내림차순, 국가 이름 오름차순으로 정렬합니다.
var countryWinsSummary = (from countryGroup in
from r in Formula1.GetChampions()
group r by r.Country into countryGrouping
select new
{
Nation = countryGrouping.Key,
TotalWins = (from driver in countryGrouping
select driver.Wins).Sum()
}
orderby countryGroup.TotalWins descending, countryGroup.Nation
select countryGroup).Take(4); // 상위 4개 국가만 표시
foreach (var summary in countryWinsSummary)
{
Console.WriteLine($"{summary.Nation,-10} 총 우승: {summary.TotalWins}회");
}
}
15. 변환 연산자
변환 연산자는 쿼리 결과를 다른 형태의 컬렉션으로 변환하거나, 쿼리를 즉시 실행하여 결과를 얻습니다. ToList(), ToArray(), ToDictionary(), ToLookup(), Cast<T>() 등이 있습니다.
ToList() 예제
public static void ConvertToListExample()
{
// ToList()를 호출하여 쿼리를 즉시 실행하고 결과를 List<Racer>에 저장합니다.
List<Racer> highStartDrivers = (from r in Formula1.GetChampions()
where r.Starts > 200
orderby r.Starts descending
select r).ToList();
foreach (var driver in highStartDrivers)
{
Console.WriteLine($"{driver} (총 출전: {driver:S})");
}
}
ToLookup() 예제
Lookup<TKey, TElement>는 Dictionary<TKey, TValue>와 유사하지만, 하나의 키에 여러 값을 연결할 수 있는 다대일(one-to-many) 매핑을 지원합니다.
public static void ConvertToLookupExample()
{
// 드라이버가 운전했던 차량을 키로, 해당 드라이버 객체를 값으로 하는 Lookup을 생성합니다.
var carToDriverLookup = (from r in Formula1.GetChampions()
from car in r.Cars
select new
{
CarBrand = car,
Driver = r
}).ToLookup(item => item.CarBrand, item => item.Driver);
if (carToDriverLookup.Contains("Williams"))
{
Console.WriteLine("\nWilliams 팀에서 뛰었던 드라이버:");
foreach (var williamsDriver in carToDriverLookup["Williams"])
{
Console.WriteLine(williamsDriver);
}
}
}
Cast<T>() 예제
Cast<T>()는 비제네릭 컬렉션(예: ArrayList)의 요소를 특정 타입으로 변환하여 LINQ 쿼리를 적용할 수 있도록 합니다. 이는 컴파일 타임에 타입 안전성을 제공합니다.
public static void ConvertWithCastExample()
{
var untypedDrivers = new System.Collections.ArrayList(Formula1.GetChampions() as System.Collections.ICollection);
// ArrayList와 같은 비제네릭 컬렉션에 LINQ 쿼리를 사용하기 위해 Cast<Racer>()를 적용합니다.
var usDrivers = from r in untypedDrivers.Cast<Racer>()
where r.Country == "USA"
orderby r.Wins descending
select r;
Console.WriteLine("\n미국 출신 챔피언:");
foreach (var driver in usDrivers)
{
Console.WriteLine($"{driver:A}");
}
}
16. 생성 연산자
생성 연산자는 새로운 시퀀스를 생성하는 정적 메서드입니다. Enumerable.Range(), Enumerable.Empty<T>(), Enumerable.Repeat<T>() 등이 있으며, 이는 확장 메서드가 아닌 Enumerable 클래스의 정적 메서드입니다.
public static void GenerateSequenceRange()
{
// 10부터 시작하여 15개의 정수를 포함하는 시퀀스를 생성합니다.
var sequentialNumbers = Enumerable.Range(10, 15);
Console.WriteLine("생성된 시퀀스 (10부터 15개):");
foreach (var num in sequentialNumbers)
{
Console.Write($"{num} ");
}
Console.WriteLine();
// Enumerable.Empty<T>()는 빈 시퀀스를 반환하며,
// Enumerable.Repeat<T>(value, count)는 특정 값을 지정된 횟수만큼 반복하는 시퀀스를 생성합니다.
}