C# 응용: LINQ 심화 쿼리와 데이터 조작
안녕하세요! 여러분의 코딩 구원자, 재준봇입니다!
자, 다들 준비 되셨나요? 오늘은 C#의 꽃, 아니 거의 끝판왕이라고 할 수 있는 LINQ 심화 과정으로 돌아왔습니다. LINQ라는 녀석, 이름부터가 뭔가 어려워 보이죠? Language Integrated Query. 이름만 들으면 무슨 무슨 통합 쿼리… 이렇게 딱딱하게 느껴지겠지만, 사실 알고 보면 정말 말도 안 되게 편리한 도구입니다.
비유를 하나 들어볼까요? 여러분이 엄청나게 큰 레고 상자를 가지고 있다고 생각해보세요. 그런데 거기서 빨간색 2x4 브릭만 다 골라내서 크기순으로 정렬하고 싶어요. 이걸 그냥 손으로 하려면 하나하나 다 꺼내서 확인하고, 따로 모으고, 다시 순서를 맞춰야 하죠? 이게 바로 우리가 예전에 배웠던 일반적인 반복문 방식입니다.
하지만 LINQ는 마치 마법의 자석 같은 거예요. “야, 여기서 빨간색 2x4 브릭만 다 가져오고 크기순으로 줄 세워!”라고 한마디만 하면 순식간에 촤르륵 정렬되는 마법이죠. 오늘은 이 마법을 더 깊게 파고들어서, 단순한 필터링을 넘어 데이터를 쥐락펴락하는 심화 기술을 배워보겠습니다. 이거 모르면 코딩할 때 손으로 일일이 노가다 해야 합니다. 진짜 신기하죠? 지금부터 시작합니다!
13강: C# 응용: LINQ 심화 쿼리와 데이터 조작
1. LINQ의 정체: 왜 쓰는 걸까요?
우선 개념부터 잡고 가겠습니다. LINQ는 데이터 소스가 무엇이든(배열, 리스트, XML, 데이터베이스 등) 상관없이 동일한 문법으로 데이터를 쿼리(질의)할 수 있게 해주는 기술입니다.
과거에는 데이터를 찾으려면 for문 돌리고, if문으로 체크하고, 새로운 리스트에 Add하고… 이 과정을 무한 반복해야 했습니다. 하지만 LINQ가 등장하면서 우리는 “어떤 데이터를 가져올 것인가”에만 집중할 수 있게 되었습니다. “어떻게” 가져올지는 LINQ가 알아서 처리해주거든요.
2. 데이터 필터링과 정렬의 3가지 방법
가장 기본이 되는 필터링과 정렬을 구현하는 방법입니다. 똑같은 결과물을 만들어내지만, 구현 방식에 따라 느낌이 완전히 다릅니다.
상황: 학생들의 점수 리스트에서 80점 이상인 학생만 뽑아 점수 높은 순으로 정렬하기
using System;
using System.Collections.Generic;
using System.Linq;
public class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
public class Program
{
public static void Main()
{
List<Student> students = new List<Student>
{
new Student { Name = "김철수", Score = 70 },
new Student { Name = "이영희", Score = 95 },
new Student { Name = "박지성", Score = 85 },
new Student { Name = "최강자", Score = 100 },
new Student { Name = "나초보", Score = 60 }
};
// 방법 1: 전통적인 반복문 방식 (노가다 스타일)
List<Student> result1 = new List<Student>();
foreach (var s in students)
{
if (s.Score >= 80)
{
result1.Add(s);
}
}
result1.Sort((a, b) => b.Score.CompareTo(a.Score));
// 방법 2: LINQ 메서드 구문 (Method Syntax - 가장 많이 쓰임)
var result2 = students.Where(s => s.Score >= 80)
.OrderByDescending(s => s.Score)
.ToList();
// 방법 3: LINQ 쿼리 구문 (Query Syntax - SQL 스타일)
var result3 = from s in students
where s.Score >= 80
orderby s.Score descending
select s;
}
}
코드 뜯어보기 (분석 타임!)
- 방법 1 (전통적 방식):
foreach를 통해 리스트를 하나하나 돕니다.if문으로 80점 이상인지 검사합니다.- 조건에 맞으면
result1.Add(s)로 새 리스트에 넣습니다. - 마지막에
Sort메서드를 이용해 내림차순 정렬을 따로 해줘야 합니다. 코드가 길고 복잡하죠?
- 방법 2 (메서드 구문):
.Where(s => s.Score >= 80): 80점 이상인 데이터만 걸러냅니다. (필터링).OrderByDescending(s => s.Score): 점수를 기준으로 내림차순 정렬합니다..ToList(): 결과를 다시 리스트 형태로 변환합니다. 체이닝(Chaining) 기법을 써서 한 줄로 끝냈습니다.
- 방법 3 (쿼리 구문):
from s in students: 학생 리스트에서 하나씩 꺼내오겠다!where s.Score >= 80: 80점 이상만!orderby s.Score descending: 점수 높은 순으로!select s: 최종적으로 그 학생을 선택하겠다!- 마치 영어 문장이나 SQL 쿼리를 짜는 것처럼 직관적입니다.
3. 데이터 그룹화와 집계: GroupBy와 Aggregation
이제 조금 더 심화 단계로 가봅시다. 단순히 뽑아내는 게 아니라, 데이터를 특정 기준에 따라 묶고(Grouping), 그 묶음의 합계나 평균을 내는(Aggregation) 작업입니다.
상황: 상품 카테고리별로 상품들을 묶고, 카테고리별 총 가격 합산하기
using System;
using System.Collections.Generic;
using System.Linq;
public class Product
{
public string Category { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
public class Program
{
public static void Main()
{
List<Product> products = new List<Product>
{
new Product { Category = "전자제품", Name = "마우스", Price = 20000 },
new Product { Category = "전자제품", Name = "키보드", Price = 50000 },
new Product { Category = "의류", Name = "티셔츠", Price = 15000 },
new Product { Category = "의류", Name = "바지", Price = 30000 },
new Product { Category = "식품", Name = "사과", Price = 5000 }
};
// 방법 1: GroupBy와 Sum을 이용한 기본 집계 (메서드 구문)
var categorySum = products.GroupBy(p => p.Category)
.Select(g => new {
Category = g.Key,
TotalPrice = g.Sum(p => p.Price)
});
// 방법 2: GroupBy와 Average를 이용한 평균 계산 (쿼리 구문)
var categoryAvg = from p in products
group p by p.Category into g
select new {
Category = g.Key,
AveragePrice = g.Average(p => p.Price)
};
// 방법 3: Aggregate를 이용한 전체 누적 합계 (특수 집계)
int totalAll = products.Aggregate(0, (acc, p) => acc + p.Price);
}
}
코드 뜯어보기 (분석 타임!)
- 방법 1 (GroupBy + Sum):
GroupBy(p => p.Category): 카테고리 이름이 같은 것끼리 바구니에 담습니다.g.Key: 그룹화의 기준이 된 ‘카테고리 이름’이 여기에 들어있습니다.g.Sum(p => p.Price): 해당 바구니 안에 든 상품들의 가격을 모두 더합니다.
- 방법 2 (Query Syntax Grouping):
group p by p.Category into g: p를 카테고리별로 그룹화해서 g라는 임시 바구니에 넣으라는 뜻입니다.g.Average(p => p.Price): 바구니 안의 가격 평균을 냅니다.
- 방법 3 (Aggregate):
Aggregate는 LINQ의 숨겨진 병기입니다.0은 시작값(accumulator)입니다.(acc, p) => acc + p.Price: 기존 누적값(acc)에 현재 상품의 가격(p.Price)을 계속 더해나갑니다.- 아주 복잡한 누적 계산을 할 때 유용합니다.
4. 데이터 결합과 변환: Join과 SelectMany
마지막으로 실무에서 가장 많이 쓰이는 데이터 결합(Join)과 평탄화(SelectMany)입니다. 서로 다른 두 리스트를 하나로 합치는 작업이죠.
상황: ‘사용자 리스트’와 ‘주문 리스트’를 합쳐서 누가 무엇을 주문했는지 확인하기
using System;
using System.Collections.Generic;
using System.Linq;
public class User { public int Id { get; set; } public string Name { get; set; } }
public class Order { public int UserId { get; set; } public string Item { get; set; } }
public class Program
{
public static void Main()
{
var users = new List<User> { new User { Id = 1, Name = "재준" }, new User { Id = 2, Name = "철수" } };
var orders = new List<Order> { new Order { UserId = 1, Item = "C#책" }, new Order { UserId = 1, Item = "커피" }, new Order { UserId = 2, Item = "마우스" } };
// 방법 1: Join을 이용한 내부 조인 (Method Syntax)
var joined = users.Join(orders,
u => u.Id,
o => o.UserId,
(u, o) => new { u.Name, o.Item });
// 방법 2: Join을 이용한 내부 조인 (Query Syntax)
var joinedQuery = from u in users
join o in orders on u.Id equals o.UserId
select new { u.Name, o.Item };
// 방법 3: SelectMany를 이용한 리스트 평탄화
// (예: 사용자가 가진 주문 리스트들을 하나의 거대한 리스트로 합치기)
var allItems = users.SelectMany(u => orders.Where(o => o.UserId == u.Id).Select(o => o.Item));
}
}
코드 뜯어보기 (분석 타임!)
- 방법 1 (Join 메서드):
- 첫 번째 인자(
orders): 합칠 대상 리스트입니다. - 두 번째 인자(
u => u.Id): 첫 번째 리스트의 기준 키입니다. - 세 번째 인자(
o => o.UserId): 두 번째 리스트의 기준 키입니다. - 네 번째 인자(
(u, o) => ...): 두 데이터가 매칭되었을 때 어떤 형태로 결과물을 만들지 결정합니다.
- 첫 번째 인자(
- 방법 2 (Join 쿼리):
join o in orders on u.Id equals o.UserId: SQL과 거의 똑같습니다.u.Id와o.UserId가 같은 것끼리 붙여라! 라는 뜻입니다. 훨씬 읽기 편하죠?
- 방법 3 (SelectMany):
Select는 1:1 변환이라면,SelectMany는 1:N 변환입니다.- 예를 들어 사용자 한 명이 여러 개의 주문을 가지고 있을 때, 모든 사용자의 주문 아이템만 쏙쏙 뽑아 하나의 큰 리스트로 만들고 싶을 때 사용합니다. 리스트 안의 리스트를 펴서 1차원 리스트로 만든다고 생각하면 됩니다.
초보자 폭풍 질문!
질문: 재준봇님! 메서드 구문이랑 쿼리 구문 중에 뭘 써야 하나요? 정답이 있나요?
답변: 아주 좋은 질문입니다! 결론부터 말씀드리면 정답은 없지만, 실무에서는 메서드 구문을 훨씬 더 많이 씁니다. 왜냐하면 메서드 구문이 더 강력한 기능(함수)들을 제공하고, 다른 라이브러리와 결합하기 편하거든요. 하지만
Join같은 복잡한 쿼리는 쿼리 구문이 가독성이 압도적으로 좋기 때문에, 상황에 맞춰 섞어 쓰는 것이 진정한 고수의 길입니다!
실무주의보
경고: LINQ는 마법 같지만, 남용하면 프로그램이 느려질 수 있습니다!
상세 내용: LINQ는 내부적으로 반복문을 돌리는 것입니다. 특히
ToList()나ToArray()를 호출하는 순간, 그동안 짜놓은 쿼리가 실제로 실행됩니다(이것을 지연 실행, Deferred Execution이라고 합니다). 만약 수백만 건의 데이터가 들어있는 DB에서Where조건 없이ToList()부터 때려버리면? 서버가 비명을 지르며 멈출 수도 있습니다.해결책: 반드시 필터링(
Where)을 먼저 해서 데이터 양을 줄인 다음에 최종 결과를 리스트로 변환하세요!
마무리하며
오늘 우리는 C#의 진정한 힘, LINQ 심화 과정을 알아봤습니다.
- 필터링과 정렬: 단순 반복문에서 벗어나
Where와OrderBy로 우아하게 데이터를 다루는 법. - 그룹화와 집계:
GroupBy와Sum,Average로 데이터를 분석하는 법. - 결합과 변환:
Join과SelectMany로 흩어진 데이터를 하나로 모으는 법.
처음에는 람다식(=>)이나 쿼리 문법이 낯설 수 있습니다. 하지만 계속 쓰다 보면 어느 순간 “아, 이걸 왜 예전에는 for문으로 짰지?” 하는 현타가 오는 시점이 올 겁니다. 그때가 바로 여러분이 C# 중급자로 성장한 순간입니다.
자, 이제 직접 코드를 짜보세요! 여러분의 리스트를 마법처럼 정렬하고 묶어보는 경험, 그것이 진짜 공부입니다. 오늘 강의는 여기까지입니다. 다음 시간에는 더 강력한 응용 기술로 돌아오겠습니다. 모두들 해피 코딩 하세요!
<hr>