SQL에 익숙한 개발자라면 NOT IN 절을 사용해 한 집합에는 있지만 다른 집합에는 없는 데이터를 조회하는 방법을 잘 알고 있을 것입니다. 그렇다면 C# LINQ에서는 이와 같은 기능을 어떻게 구현할 수 있을까요? 정답은 바로 Except 연산자입니다.
Except 연산자란 무엇인가?
Except 연산자는 IEnumerable<T> 인터페이스를 지원하는 데이터 소스를 대상으로 동작하도록 설계된 집합 연산자(Set Operator)입니다. 모든 LINQ 쿼리 식과 대부분의 LINQ 쿼리는 IEnumerable<T>를 반환하기 때문에, 이 연산자를 활용하면 LINQ 쿼리의 결과물에 대해 손쉽게 집합 연산을 수행할 수 있습니다.
Except 연산자의 동작 방식은 직관적입니다. 첫 번째 목록에는 존재하지만 두 번째 목록에는 없는 항목들만 반환합니다. 즉, SQL의 NOT IN 절과 동일한 역할을 수행하는 것입니다.
예제 1: Except 메서드 사용하기
가장 기본적인 사용법은 메서드 구문(Method Syntax)에서 Except를 호출하는 것입니다.
class Program{
static void Main(string[] args){
var listA = Enumerable.Range(1, 6);
var listB = new List<int> { 3, 4 };
var listC = listA.Except(listB);
foreach (var item in listC){
Console.WriteLine(item);
}
Console.ReadLine();
}
}위 예제에서는 두 개의 리스트를 준비했습니다. 리스트 A는 1부터 6까지의 숫자를 포함하고, 리스트 B는 3과 4를 포함합니다. 그런 다음 Except 연산자를 사용해 리스트 A 중에서 리스트 B에 존재하지 않는 요소만 추출합니다.
실행 결과
1 2 5 6
실행 결과를 보면 리스트 B에 포함된 3과 4가 제외되고 나머지 값만 출력된 것을 확인할 수 있습니다.
예제 2: SQL 스타일 쿼리 구문 사용하기
메서드 구문 대신 SQL과 유사한 쿼리 식(Query Syntax)을 선호한다면, Any 메서드와 논리 부정 연산자(!)를 조합하여 동일한 결과를 얻을 수 있습니다.
static void Main(string[] args){
var listA = Enumerable.Range(1, 6);
var listB = new List<int> { 3, 4 };
var listC = from c in listA
where !listB.Any(o => o == c)
select c;
foreach (var item in listC){
Console.WriteLine(item);
}
Console.ReadLine();
}실행 결과
1 2 5 6
두 방식 모두 동일한 결과를 출력하지만, 내부적으로 살펴보면 차이가 있습니다.
어떤 방식을 선택해야 할까?
- Except 연산자: 코드가 간결하고 의도가 명확하게 드러납니다. 내부적으로 해시 집합(HashSet)을 활용해 효율적으로 처리되므로 성능 면에서도 유리합니다.
- Any + 부정 조건: 복잡한 조건이나 커스텀 비교 로직이 필요할 때 더 유연하게 확장할 수 있습니다. 다만 요소 수가 많아질 경우 Except보다 성능이 떨어질 수 있습니다.
또한 참고할 점은, Except 연산자는 기본적으로 기본 형식(primitive type)의 같음 비교자(Default Equality Comparer)를 사용한다는 것입니다. 사용자 정의 클래스 객체를 비교해야 한다면 IEqualityComparer<T>를 구현하여 오버로드된 Except 메서드에 전달해야 올바른 결과를 얻을 수 있습니다.
마무리
C# LINQ에서 SQL의 NOT IN과 같은 기능이 필요하다면 Except 연산자가 가장 깔끔하고 효율적인 해답입니다. 간단한 숫자 목록부터 복잡한 객체 컬렉션까지, 적절한 비교자만 준비하면 두 데이터 집합 간의 차집합 연산을 손쉽게 처리할 수 있습니다.