CodeGym /행동 /C# SELF /LINQ에서 Union,

LINQ에서 Union, Intersect, Except 연산

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

1. 소개

이제 진짜 재밌고 실무에서 자주 쓰는 주제로 넘어가자: LINQ의 집합 연산 — Union, Intersect, Except. 이걸로 컬렉션을 마치 집합론 하듯이 다룰 수 있어 (아니면 개발자 책상 위에 스티커 두 묶음 세는 거랑 비슷하다고 생각해도 돼 — 거의 똑같지). 혹시 집합론을 한 번도 안 들어봤거나 까먹었다면 좀 무섭게 들릴 수도 있는데, 사실 엄청 쉬운 개념이야: 두 개의 과일 자루가 있다고 생각해봐. 집합론은 두 자루에 어떤 과일이 같이 있고, 어떤 건 한 쪽에만 있고, 합치면 뭐가 나오는지 알아내는 방법이야. 스티커 놀이랑 똑같아: 다 합치거나, 공통된 것만 찾거나, 한 쪽에서만 있는 걸 빼거나. 그게 다야.

실습: 이 메서드들은 두 개의 다른 쿼리 결과를 합치거나, 공통된 요소를 찾거나, 컬렉션 간의 차이를 알아낼 때 진짜 편해. 예를 들면:

  • 판매 목록이랑 구매 목록에 있는 모든 상품의 전체 리스트 만들기.
  • 두 리스트에 모두 있는 상품 찾기 — 공통 항목.
  • 아니면 창고에만 있고 한 번도 팔린 적 없는 상품 찾기 — 누가 창고에서 썩고 있네!

실제 문제: 집합 연산은 어디서나 써: 구독자 필터링, 여러 그룹에 속한 유저 찾기, 유니크하거나 겹치는 주문 찾기, 서로 다른 쿼리 결과 비교 등등 진짜 많아.

2. Union 연산 — 컬렉션 합치기

진짜 간단하니까 바로 실습 들어가자. 예를 들어, 두 개의 상품 리스트가 있다고 해보자:

List<string> warehouseProducts = new List<string> { "우유", "빵", "치즈", "계란" };
List<string> recentlySold = new List<string> { "빵", "치즈", "살라미", "차" };

Union이 뭐 하는 거야?

Union적어도 하나의 컬렉션에 있는 유니크한 요소들을 반환해.
즉, 합집합이야.

var allProducts = warehouseProducts.Union(recentlySold);

foreach (var product in allProducts)
{
    Console.WriteLine(product);
}
// 출력: 우유, 빵, 치즈, 계란, 살라미, 차

이걸 시각적으로 보면 이렇게 돼:

창고 판매됨 Union (합집합)
우유 우유
치즈
치즈 살라미 치즈
계란 계란
살라미

집합론 용어로 Union은 "or" 연산이야: 어디든 있으면 다 줘. 차 종류가 다른 두 상자가 있다고 생각해봐 — 모든 맛을 다 마셔보고 싶으면, 중복된 차가 있어도 한 번만 마시게 돼.

Union자동으로 중복을 제거해 (요소 타입의 EqualsGetHashCode 구현에 따라). 만약 네가 직접 만든 클래스(예: Product)로 합집합을 쓴다면, 이 메서드들이 제대로 구현되어 있는지 꼭 확인해야 해. 아니면 Union이 이상하게 동작해서 똑같은 요소가 다르게 취급될 수도 있어!

꼭 기억해: 결과 컬렉션의 순서는 첫 번째 컬렉션의 순서를 유지하고, 새로운 요소는 두 번째 컬렉션에서 처음 나오는 순서대로 뒤에 붙어.

객체 예시

우리 "상점" 앱 계속 해보자. Product 리스트 두 개가 있어:

public class Product
{
    public string Name { get; set; }
    public string Category { get; set; }

    // Union/Intersect/Except 제대로 쓰려면 Equals랑 GetHashCode 잘 구현해야 해!
    public override bool Equals(object? obj) =>
        obj is Product other && Name == other.Name && Category == other.Category;

    public override int GetHashCode() => HashCode.Combine(Name, Category);
}

List<Product> stock = new()
{
    new Product { Name = "우유", Category = "유제품" },
    new Product { Name = "빵", Category = "빵류" },
};

List<Product> sold = new()
{
    new Product { Name = "빵", Category = "빵류" },
    new Product { Name = "살라미", Category = "소시지" },
};

var all = stock.Union(sold);

// 주의: 요소가 "겉보기엔" 똑같아도,
// 논리적으로 같으면 중복되지 않아. 

foreach (var p in all)
    Console.WriteLine($"{p.Name} ({p.Category})");

자주 하는 실수: Equals/GetHashCode를 오버라이드 안 하면, Union이 값이 똑같은 인스턴스도 다르게 봐!

3. Intersect 연산 — 컬렉션 교집합

Intersect모든 원본 컬렉션에 있는 요소만 반환해. 집합론에서 "and" 연산이야.

예시

아까 썼던 리스트 다시 써보자:

var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
    Console.WriteLine(product);
}
// 출력: 빵, 치즈

시각화:

창고 판매됨 Intersect (공통)
치즈 치즈 치즈

어디에 쓸까?

  • 오늘 창고에서 팔린 상품이 뭔지 알고 싶을 때.
  • 면접에서 "두 리스트의 교집합을 어떻게 찾을래?"라고 물어보면 — LINQ면 한 줄이면 끝!
  • 비즈니스 앱에서 여러 조건을 동시에 만족하는 필터링이 필요할 때 자주 써.

특징과 자주 하는 실수

컬렉션에 요소가 여러 번 있어도 결과엔 한 번만 나와.
복잡한 객체(예: Product 클래스)는 Equals/GetHashCode 구현이 또 중요해.

객체 예시

var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
    Console.WriteLine($"{p.Name} ({p.Category})");
// 출력: 빵 (빵류) 만 나옴

4. Except 연산 — 컬렉션 차집합

Except첫 번째 컬렉션에만 있고 두 번째엔 없는 요소만 반환해.

예시

var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
    Console.WriteLine(product);
}
// 출력: 우유, 계란

즉, 창고에만 있는한 번도 팔린 적 없는 상품이야.

시각화:

창고 판매됨 Except (창고만)
우유 우유
계란 계란
(생략)
치즈 치즈 (생략)

비유

이미 보낸 문서는 다 빼고, 네 손에만 남아있는 문서만 남기는 거랑 똑같아.

객체 예시

var unsold = stock.Except(sold);
foreach (var p in unsold)
    Console.WriteLine($"{p.Name} ({p.Category})");
// 출력: 우유 (유제품)

! 헷갈릴 수 있는 포인트

Except는 순서에 민감해: A.Except(B)B.Except(A)는 완전 달라! 첫 번째 컬렉션이 "빼는 대상", 두 번째가 "뺄 것"이야.

5. LINQ로 여러 집합 연산 조합하기

한 번의 연산으론 부족할 때도 있어. 예를 들어, 창고에만 있거나 판매된 것에만 있는 상품(둘 다엔 없는 것), 즉 "대칭 차집합"을 구하고 싶을 때.

대칭 차집합 ("XOR" 집합):

var onlyInOne = warehouseProducts.Except(recentlySold)
                   .Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
    Console.WriteLine(product);
}
// 출력: 우유, 계란, 살라미, 차

복잡한 로직은 LINQ 메서드 조합이 편해:

// "입고 예정" 리스트에만 있고, 창고나 판매된 곳엔 없는 상품 찾기
List<string> expected = new() { "커피", "차", "우유" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
    Console.WriteLine(product);
// 출력: 커피

6. 커스텀 타입과 IEqualityComparer 사용하기

가끔 모든 필드로 비교할 필요 없이, 예를 들어 상품 이름만 중요하고 카테고리는 신경 안 쓸 때가 있어. 이럴 땐 LINQ 메서드에 IEqualityComparer<T> 파라미터를 넘겨서 비교 방식을 직접 정할 수 있어.

커스텀 비교자 예시:

class ProductNameComparer : IEqualityComparer<Product>
{
    public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
    public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}

var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
    Console.WriteLine(p.Name); // 우유, 빵, 살라미

이건 모델 전체의 Equals/GetHashCode를 바꾸고 싶지 않고, 한 번만 특정 규칙으로 비교하고 싶을 때 특히 좋아.

7. 시각적 도식과 표

정리해보자 (리스트 AB 기준):

연산 결과
A.Union(B)
A 또는 B에 있는 모든 것 (유니크한 요소).
A.Intersect(B)
둘 다(AB)에 있는 모든 것.
A.Except(B)
A에만 있고 B엔 없는 모든 것.

도식 (Venn Diagram, 그림을 그릴 수 있다면):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: 두 원 안에 있는 모든 것.
  • Intersect: 교집합(가운데)만.
  • Except: A 원에만 있고 B랑 겹치지 않는 부분만.

8. Union, Intersect, Except 쓸 때 자주 하는 실수

실수 1: EqualsGetHashCode 없는 객체로 사용.
클래스에 이 메서드들이 오버라이드 안 되어 있으면, Union, Intersect, Except가 제대로 동작 안 해: 내용이 같은 객체도 다르게 취급돼. 그래서 결과가 엉뚱하게 나올 수 있어.

실수 2: IEqualityComparer 없이 일부 필드만 비교하려고 함.
예를 들어, 상품 이름만 비교하고 싶을 때 그냥 Intersect 쓰면 원하는 대로 안 돼. IEqualityComparer를 명시적으로 넘기지 않으면 결과가 기대랑 달라.

실수 3: 요소 순서에 대한 잘못된 기대.
많은 사람들이 결과 컬렉션이 합치거나 교집합한 순서를 유지한다고 생각하는데, 메서드마다 다 달라: Union은 첫 번째 컬렉션 순서를 유지하지만, IntersectExcept는 순서가 예측 불가야. 순서에 의존하지 않는 게 좋아.

실수 4: 큰 컬렉션에서 성능 무시.
데이터가 많으면 메서드가 느릴 수 있어. 미리 집계하거나 필터링하거나, HashSet 같은 해시 구조를 써서 연산을 빠르게 하는 걸 고려해.

2
과제
C# SELF, 레벨 33, 레슨 3
잠금
Intersect를 사용해서 공통 요소 찾기
Intersect를 사용해서 공통 요소 찾기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION