CodeGym /행동 /C# SELF /여러 소스 합치기: SelectMany

여러 소스 합치기: SelectMany in LINQ

C# SELF
레벨 33 , 레슨 2
사용 가능

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)가 더 편하다면, SelectManyfrom을 여러 번 써서 표현해:

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
SelectMany를 위한 중첩 컬렉션 구조도

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를 계속 쓰게 될 거야. 면접에서도 자주 나오는 주제야: 초보들은 SelectSelectMany를 잘 구분 못하고, 컬렉션을 제대로 못 풀기도 해.

실제 프로젝트에서도, 불필요한 중첩 없이 깔끔하고 빠른 코드를 만들 수 있어. .NET 플랫폼에서는 "겹겹이" 컬렉션을 다룰 때 표준이자 최적화된 방법이야.

SelectSelectMany의 차이

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]
SelectSelectMany의 차이 구조도

정리하자면:

  • Select → "user별 주문 리스트 줘";
  • SelectMany → "그냥 모든 주문 다 줘".
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION