1. 소개
일단, 일반적인 Select가 어떻게 동작하는지 기억해보자:
var names = users.Select(user => user.Name);
각 user마다 이름을 뽑아서 이름 리스트를 얻는 거지. 완전 간단! 근데 만약 모든 user의 모든 주문 리스트가 필요하다면?
var ordersList = users.Select(user => user.Orders);
여기서 무슨 일이 일어나냐면, 각 user마다 주문 리스트를 가져와. 즉, 결과는 컬렉션의 컬렉션(IEnumerable<List<Order>>)이야. 이건 마치 선반에 박스들이 있는 것처럼, 각 박스(user의 주문 리스트)를 하나씩 열어서 주문을 꺼내야 해.
우리는 이 모든 박스를 "풀어서" 그냥 주문 리스트 하나로 만들고 싶잖아. 이럴 때 SelectMany가 딱이야.
비유: 박스 풀기
모든 물건이 막 섞여있는 게 아니라 박스에 담겨 있다고 생각해봐. 각 박스는 예를 들어 한 user의 구매 목록이야. 모든 물건을 한 번에 보고 싶으면, 박스마다 하나씩 열어서 볼 필요 없이, 그냥 모든 박스 내용을 한 번에 쏟아버리면 되지! SelectMany가 바로 이런 느낌이야.
2. SelectMany란?
SelectMany는 LINQ 메서드 중 하나로, 컬렉션의 컬렉션을 하나의 "평평한" 컬렉션으로 만들어줘. 여기서 "평평하다"는 건, 결과 시퀀스에 더 이상 내부 리스트가 없고, 모든 중첩된 요소가 한 줄로 쭉 나오는 거야. 마치 러시아 인형(마트료시카) 없이, 그냥 긴 값의 체인만 남는 거지.
메서드 시그니처는 이렇게 생겼어:
IEnumerable<TResult> SelectMany<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TResult>> selector
)
동작 방식은 간단해: 컬렉션(예: user 리스트)이 있고, 각 요소에 또 다른 컬렉션(예: 그 user의 주문 리스트)이 들어있어. SelectMany는 각 user의 주문을 꺼내서, 모든 주문을 하나의 흐름으로 합쳐줘 — "포장" 없이, 중첩 없이. "리스트의 리스트"를 그냥 리스트로 바꿔야 할 때 완전 편해.
3. 간단한 예시
예시 1: user 리스트와 그들의 주문
우리 앱의 모델을 예로 들어볼게:
// 이전 강의에서 이미 만든 클래스라고 가정
class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
}
user 리스트가 있다고 해보자:
var users = new List<User>
{
new User
{
Name = "이반",
Orders = new List<Order>
{
new Order { Id = 1, ProductName = "책" },
new Order { Id = 2, ProductName = "펜" }
}
},
new User
{
Name = "마리야",
Orders = new List<Order>
{
new Order { Id = 3, ProductName = "노트북" }
}
}
};
문제: 모든 user가 한 번이라도 산 모든 상품 리스트를 얻고 싶어.
일반 Select:
var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>
이렇게 하면 결과가 "박스" 여러 개, 각 박스엔 user별 주문 리스트가 들어있어.
SelectMany:
var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>
이제 모든 주문이 한 "긴" 컬렉션으로 나와!
시각화
| user | 주문 |
|---|---|
| 이반 | 책, 펜 |
| 마리야 | 노트북 |
- Select 후: [[책, 펜], [노트북]] — 리스트의 리스트
- SelectMany 후: [책, 펜, 노트북] — 하나의 리스트
예시 2: 여러 단계 중첩 다루기
만약 주문에 상품 리스트(장바구니)가 있다면, SelectMany를 여러 번 쓸 수 있어 — 마치 마트료시카처럼!
class Product
{
public string Name { get; set; }
}
class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
var users = new List<User>
{
new User
{
Name = "표트르",
Orders = new List<Order>
{
new Order
{
Id = 10,
Products = new List<Product>
{
new Product { Name = "핸드폰" },
new Product { Name = "충전기" }
}
}
}
}
};
모든 user의 모든 주문의 모든 상품을 얻으려면, SelectMany를 두 번 써:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// IEnumerable<Product>
이렇게 하면 한 단계씩 "박스"를 풀 수 있어.
예시 3: Query Syntax에서 SelectMany 쓰기
SQL 스타일 LINQ(Query Syntax)가 더 편하다면, SelectMany는 from을 여러 번 써서 표현해:
var allProducts =
from user in users
from order in user.Orders
from product in order.Products
select product;
여기서 각 from은 이전에서 반환된 컬렉션에서 요소를 꺼내는 거야.
users
└─ user1
└─ order1
└─ product1, product2
└─ order2
└─ product3
└─ user2
└─ order3
└─ product4
Query Syntax는 모든 경로를 따라가서 하나의 긴 상품 리스트를 반환해.
4. 활용법과 팁
SelectMany로 변환과 필터링
SelectMany는 단순히 풀기만 하는 게 아니라, 동시에 필터링이나 변환도 할 수 있어. 예를 들어, 모든 주문에서 1000유로 넘는 상품만 뽑으려면:
var expensiveProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products)
.Where(p => p.Price > 1000);
아니면, SelectMany 안에 바로 로직을 넣어서(중첩 selector로):
var expensiveProducts = users
.SelectMany(u => u.Orders.SelectMany(o => o.Products))
.Where(p => p.Price > 1000);
둘 다 취향 차이야: 문제를 단계별로 나눌 수도 있고, 한 번에 할 수도 있어.
프로젝션이 있는 SelectMany 오버로드 사용하기
좀 더 "고급" 오버로드를 쓰면, 풀면서 바로 결과를 만들 수도 있어. 예: "user 이름 — 상품 이름" 쌍을 얻고 싶을 때:
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(order => order.Products),
(user, product) => new { user.Name, product.Name }
);
어떻게 동작하냐면, 첫 번째 파라미터 — user => user.Orders.SelectMany(order => order.Products) — user의 모든 상품을 풀고, 두 번째는 user와 product를 조합해서 새 객체를 만들어.
5. 중요한 포인트와 흔한 실수
SelectMany를 빼먹고, 평평한 시퀀스 대신 "박스" 배열을 받는 경우가 많아. 예를 들어, Select만 써서 모든 요소를 바로 다루지 못하는 상황이 생길 수 있지.
컬렉션이 중첩될수록 헷갈리기 쉬워. 규칙을 세워: 결과가 리스트의 리스트가 아니라 평평한 리스트여야 한다면, SelectMany를 써!
실전에서 어디에 쓸까?
객체-컬렉션이 있고, 각 요소마다 또 컬렉션이 있을 때(예: "user — 주문", "강좌 — 학생", "뉴스 — 댓글" 등), SelectMany를 계속 쓰게 될 거야. 면접에서도 자주 나오는 주제야: 초보들은 Select와 SelectMany를 잘 구분 못하고, 컬렉션을 제대로 못 풀기도 해.
실제 프로젝트에서도, 불필요한 중첩 없이 깔끔하고 빠른 코드를 만들 수 있어. .NET 플랫폼에서는 "겹겹이" 컬렉션을 다룰 때 표준이자 최적화된 방법이야.
Select와 SelectMany의 차이
Select는 구조를 유지해: user 컬렉션에 각 user마다 주문 리스트가 있으면, 결과는 컬렉션의 컬렉션 — 예를 들어 [[A, B], [C]]가 돼. 그룹별(예: user별)로 계속 작업하고 싶을 때 좋아.
반면, SelectMany는 "평평하게" 만들어 — 모든 중첩 컬렉션을 하나의 시퀀스로 합쳐: [A, B, C]. 그룹이 중요하지 않고, 내부 요소를 한 번에 다루고 싶을 때(예: 모든 상품 찾기 등) 유용해.
+---------------------+ +--------------------+
| users | | allOrders |
+---------------------+ +--------------------+
| 이반: [A, B] | --+ +-> Order A |
| 마리야: [C] | --+ | +-> Order B |
+---------------------+ | | +-> Order C |
| | +--------------------+
Select: | |
[[A, B], [C]] <--------+ +
|
SelectMany: <--------+
[A, B, C]
정리하자면:
- Select → "user별 주문 리스트 줘";
- SelectMany → "그냥 모든 주문 다 줘".
GO TO FULL VERSION