개요

LINQ의 주요 연산자를 개념과 동작 관점으로 정리함. 각 연산자가 입력 시퀀스를 어떻게 변환하는지, 결과 크기와 순서를 어떻게 보장하는지, 즉시 실행 여부와 예외 동작은 무엇인지에 초점을 둠. 설명은 가능한 한 단순한 정의와 함께 주의점 중심으로 구성함

기본 변환과 필터

  • Select

    • 한 요소를 다른 형태로 사상하는 투영 연산
    • 입력 개수 보존, 출력 개수는 입력과 동일
    • 프로퍼티 선택, 새 익명 객체 구성, 스칼라 변환 등에 사용
  • Where

    • 조건식이 true인 요소만 통과시키는 필터링 연산
    • 결과 개수는 입력 이상이 될 수 없으며 같거나 더 작거나 빈 시퀀스가 될 수 있음
    • 술어 평가가 false면 요소 제외, 지연 실행으로 조건이 충족될 때만 열거 진행
  • SelectMany

    • 중첩 컬렉션 평탄화 연산
    • 요소마다 하위 시퀀스를 꺼내 단일 차원의 시퀀스로 연결
    • 다대일 매핑이나 계층 구조를 단일 스트림으로 다루고 싶을 때 사용
  • Zip

    • 두 시퀀스를 병합 함수로 쌍을 만들어 결합
    • 둘 중 더 짧은 길이에 맞춰 결과 생성, 나머지는 무시
    • 인덱스 정렬된 데이터 병합, 키 없는 병렬 결합 시 유용
  • OrderBy / OrderByDescending

    • 주어진 키 선택자 또는 기본 비교 가능 타입 기준으로 정렬
    • 안정 정렬 특성 유지, 동일 키의 상대 순서 보존
    • 결과 개수 보존, 입력과 동일 개수
  • Distinct

    • 중복 제거로 유일 요소만 반환, 집합(Set) 유사 동작
    • 참조 타입은 기본적으로 참조 동일성 또는 Equals/GetHashCode 구현에 의존
    • 필요 시 IEqualityComparer 제공 권장

집계, 분할, 집합, 부울 질의

  • Aggregate

    • reduce로도 불림. 누적기와 사용자 정의 축약 함수를 기반으로 단일 값 도출
    • 합계, 평균, 최솟값/최댓값, 사용자 정의 집계 구성에 활용
    • 시작값(seed)에서 출발해 각 요소를 누적기에 반영
  • Chunk (.NET 6)

    • 지정 크기 단위로 하위 배열 묶음을 생성하는 유틸리티
    • 마지막 청크는 남은 크기만큼 생성될 수 있음
    • 배치 처리, 페이로드 분할에 유용
  • Union

    • 두 시퀀스의 합집합에서 유일 요소만 반환
    • 내부적으로 Distinct와 유사한 유일성 보장
    • 순서 보장은 구현과 소스에 따라 달라질 수 있으므로 후속 정렬 고려
  • Intersect

    • 교집합. 두 시퀀스에 모두 존재하는 유일 요소만 반환
    • 중복은 자동 제거, 유일성 기준은 동등 비교자에 따름
  • Any

    • 조건을 만족하는 요소가 하나라도 있으면 true를 반환하는 존재성 검사
    • 단 하나를 찾는 즉시 조기 종료, 전부 탐색하지 않음
  • All

    • 모든 요소가 조건을 만족해야 true를 반환하는 전역 조건 검사
    • 하나라도 불만족 발견 시 즉시 false로 종료
  • Append / Prepend

    • Append는 시퀀스 끝에 요소 추가, Prepend는 시작에 요소 추가
    • 원본을 변경하지 않고 새 시퀀스를 구성하는 체이닝 친화 동작

키 기반 변환, 고유성, 조회, 조인

  • MaxBy / MinBy

    • 프로젝션 키 기준으로 최댓값 또는 최솟값을 가지는 원본 객체 반환
    • 값 자체가 아닌 객체 선택에 유용, 동률일 경우 첫 발생 요소 반환
  • DistinctBy

    • 키 프로젝션 기준으로 유일성 판단하여 중복 제거
    • 객체 전체 동등성 대신 특정 속성 기준 유일 집합을 만들 때 사용
  • ToLookup

    • 키 하나가 여러 값(1 대 다)에 매핑되는 조회 구조 생성
    • 키 선택자와 값 선택자 제공, 값 선택자는 원본 전체 또는 일부 프로젝션 가능
    • 결과는 불변 조회 구조. 생성 시 전체를 한 번에 구성
    • 그룹 탐색 다건 조회에 적합
  • ToDictionary

    • 키가 하나의 값에만 매핑되는 사전 구조 생성(1 대 1)
    • 키 중복 시 예외 발생, 이후 변경 가능(예: Add)
    • 빠른 키 기반 단건 조회에 적합
  • Join

    • 두 시퀀스를 키 일치 기준으로 결합하여 결과 요소 생성
    • 기본 Join은 SQL의 inner join과 동일한 동작에 가깝게 동작
    • left outer join이 필요하면 GroupJoin과 DefaultIfEmpty 조합 사용 권장

슬라이싱, 타입 필터, 그룹, 위치 검색

  • Take

    • 앞에서부터 지정 개수만큼 요소를 취함
    • 남은 요소가 적으면 가능한 만큼만 반환
    • 페이징 구현 시 Skip과 조합
  • Skip

    • 앞에서부터 지정 개수만큼 요소를 건너뜀
    • 건너뛴 개수가 전체보다 크면 빈 시퀀스 반환
  • OfType

    • 요소 타입을 검사하여 지정 타입(상속 포함)만 필터링
    • 비제네릭 또는 이질적 컬렉션에서 특정 파생 타입만 추출할 때 유용
  • GroupBy

    • 키 프로젝션 기준으로 요소를 묶어 그룹 시퀀스를 생성
    • ToLookup과 유사하나 GroupBy는 질의 구성을 위한 그룹 표현을 만들고 필요 시 열거하며, ToLookup은 즉시 전체를 캐시해 조회 지향으로 사용
    • 그룹 단위 후처리나 추가 변환을 연쇄하기에 적합
  • Reverse

    • 현재 시퀀스의 순서를 반전한 뷰를 반환
    • 가능한 경우 지연 평가로 동작, 일부 소스는 물리적 역순 재구성 필요
  • First

    • 조건을 만족하는 첫 요소 반환, 없으면 예외 발생
    • 일치 요소를 찾는 즉시 종료
  • Single

    • 조건을 만족하는 요소가 정확히 하나여야 함
    • 첫 요소에서 즉시 반환하지 않으며 둘 이상 발견 시 예외, 없으면 예외
    • 유일성 보장이 필요한 시나리오에 사용
  • FirstOrDefault

    • 첫 요소가 없으면 기본값 반환
    • .NET 6부터 기본값을 직접 지정하는 오버로드 제공
  • SingleOrDefault

    • 요소가 없으면 기본값 반환, 둘 이상이면 예외
    • 유일성 검증과 기본값 전략을 함께 요구하는 경우에 사용

동작 원리와 구조 요약

  • 지연 실행

    • 대부분의 LINQ 연산자는 IEnumerable 기반에서 지연 실행됨. 열거될 때까지 실제 작업 미루는 특성
    • 집계 계열(Sum, Count, Aggregate 등)과 ToList, ToArray, ToDictionary, ToLookup 등 물질화 연산은 즉시 실행
  • 안정성 및 순서

    • OrderBy는 안정 정렬. 동일 키의 상대 순서 유지
    • 집합 연산(Union, Intersect)은 유일성에 집중하므로 순서 요구가 있으면 후속 정렬 고려
  • 동등성 비교

    • Distinct, Union, Intersect, DistinctBy 등은 동등성 비교자의 영향을 받음
    • 참조 타입에서 Equals/GetHashCode를 구현하거나 IEqualityComparer 제공 권장
  • 크기 변화 규칙

    • Select, OrderBy, Reverse는 개수 보존
    • Where, Distinct, DistinctBy는 개수 감소 가능
    • SelectMany는 평탄화로 개수가 증가할 수 있음
    • Zip은 더 짧은 입력 길이에 수렴

주의와 베스트 프랙티스

  • First/Single 선택 기준

    • 존재만 보장되면 First 또는 FirstOrDefault 선호, 유일성 보장이 비즈니스 규칙이면 Single 또는 SingleOrDefault 사용
    • Single은 둘 이상일 때 예외를 던지므로 런타임 비용과 실패 시나리오를 고려
  • 예외와 기본값

    • OrDefault 계열은 값 형식에서 기본값이 0 또는 default로 들어갈 수 있음. 이 값이 유효 데이터와 충돌할 수 있어 .NET 6 오버로드의 명시적 기본값 사용 고려
  • 조인 전략

    • 일반 Join은 inner join 동작. 왼쪽 기준 보존이 필요하면 GroupJoin + DefaultIfEmpty 조합으로 left outer join 구현 권장
    • 조인 키의 동등성 비교자를 명시하여 예기치 않은 매칭 누락 방지
  • GroupBy vs ToLookup 선택

    • 그룹을 만들어 후속 변환을 연쇄하고 싶으면 GroupBy
    • 즉시 전체를 키별로 캐싱해 빠른 조회가 목표면 ToLookup
  • 성능과 물질화

    • 거대한 시퀀스에서 Distinct/집합 연산은 해시 구조를 사용하므로 메모리 사용량 증가 가능. 스트리밍 처리와 메모리 제한 고려
    • 반복 열거가 많다면 ToList 등으로 한 번 물질화하여 중복 연산 비용 절감 고려
  • Zip과 길이 불일치

    • Zip은 둘 중 짧은 길이까지만 결합. 데이터 유실을 원치 않으면 길이 정합성 검사 또는 DefaultIfEmpty/Range 보정 전략 고려
  • Append/Prepend 체인

    • 체인 길이가 길어지면 지연 실행 비용과 다중 열거 가능성 고려. 최종적으로 리스트가 필요하면 한 번 물질화하여 고정 비용 지불

간단 예시 스니펫

  • 투영과 필터 결합 예시 items.Where(x => x.IsActive).Select(x => x.Name)
  • 평탄화 예시 parents.SelectMany(p => p.Children)
  • DistinctBy 키 기준 중복 제거 예시 items.DistinctBy(x => x.Id)
  • left outer join 개념 예시 GroupJoin(...).SelectMany(...DefaultIfEmpty(...))

마무리

LINQ 연산자는 입력을 어떻게 순회하고 비교하며 결괏값을 만드는지 이해하면 조합의 폭이 넓어짐. 핵심은 지연 실행, 동등성 비교, 정렬 안정성, 예외 동작, 물질화 타이밍. 여기 정리된 연산자의 정의와 주의점을 기준으로 시나리오에 맞게 선택하고 필요 시 비교자와 기본값, 조인 전략을 명시적으로 지정하는 습관 권장

참고자료