LINQ That Ships: 더 빠르고 깔끔한 C# 쿼리를 위한 팁 | Bald. Bearded. Builder


LINQ 쿼리 최적화 기법

개요

LINQ 쿼리를 더 빠르고, 읽기 쉽고, 유지보수하기 쉽게 만들기 위한 실무에서 검증된 기법들을 소개한다.

1. 중간값 캐싱 활용

문제점

  • 동일한 계산을 여러 번 반복하여 성능 저하 발생

  • 데이터베이스 쿼리에서는 여러 번의 실행이나 성능 저하 야기

해결방법

let 키워드 사용

  • 중간값을 한 번 계산하고 재사용

  • WHERE 절과 SELECT 절에서 동일한 계산 결과를 활용

플루언트 구문에서의 프로젝션

  • 쿼리를 프로젝션하여 필요한 값을 미리 생성

  • 후속 필터링이나 프로젝션에서 해당 객체 재사용

핵심 원칙

재사용하고 싶은 값에는 반드시 이름을 부여하라

2. Aggregate 메서드의 올바른 사용법

문제점

  • 문자열 연결에 Aggregate 사용 시 성능 문제

  • + 연산자마다 새로운 문자열 할당 발생

해결방법

String.Join 사용

  • 단 한 번의 할당으로 처리

  • 읽기 쉽고 이해하기 쉬운 코드

  • 최적화가 극도로 잘 되어 있음

언제 Aggregate를 사용해야 하는가

  • Count, Sum, Average 등의 기본 집계 함수로는 불가능한 커스텀 로직

  • 체크섬 계산, 객체 병합, 새로운 상태 객체 생성 등

주의사항

Aggregate는 LINQ의 "만능 도구"이지만, 부적절한 사용은 비효율적

3. 그룹핑 최적화

문제점

  • 복잡한 그룹핑 로직으로 인한 가독성 저하

  • 두 번의 그룹핑으로 인한 비효율적인 SQL 생성

해결방법

GroupBy 메서드 활용

  • 두 개 이상의 컬럼으로 그룹핑 가능

  • 총계 등을 바로 프로젝션

  • 효율적인 SQL GROUP BY 문 생성

장점

  • 데이터베이스에서 하나의 GROUP BY 문으로 처리

  • 가독성 향상 및 성능 최적화

4. ToLookup vs GroupBy

GroupBy의 문제점

  • 각 foreach 루프에서 두 번씩 실행

  • 지연 실행(deferred execution)으로 인한 성능 저하

ToLookup 활용

  • 한 번만 구체화(materialize)

  • O(1) 성능 제공

  • 즉시 실행되어 “워밍업된 딕셔너리” 생성

5. 중첩 컬렉션 처리

문제점

  • 중첩된 배열 처리 시 복잡한 이중 루프 필요

  • 임시 리스트 생성 및 수동 중복 제거

해결방법

SelectMany 활용

  • 중첩 컬렉션을 평면화

  • Distinct와 결합하여 고유한 리스트 생성

원본 데이터 유지 방법

  • 원본 객체의 정보와 중첩 데이터를 함께 프로젝션

  • 새로운 객체로 제목과 태그 정보 모두 포함

6. AsEnumerable의 올바른 사용

문제점

  • EF Core에서 처리할 수 없는 C# 메서드 사용 시 런타임 에러 발생

  • 전체 데이터셋을 메모리로 로드하여 처리하는 최악의 경우

해결방법

AsEnumerable 사용

  • C# 코드가 필요한 지점에서 명시적 브레이크 포인트 제공

  • 이전 WHERE 조건까지만 데이터베이스에서 처리하고 메모리로 로드

주의사항

  • 수술용 메스이지 대형 망치가 아님

  • 가능한 한 필터링을 먼저 수행하여 데이터셋 크기 최소화

  • RAM 부하 방지를 위한 신중한 사용 필요

7. 쿼리 결과 캐싱

문제점

  • IQueryable을 여러 메서드에 전달 시 각각 별도로 실행

  • 데이터베이스에 대한 다중 호출 발생

해결방법

ToList() 활용

  • 비싼 쿼리를 한 번 실행하여 메모리에 캐시

  • 후속 메서드들은 캐시된 데이터 사용

  • 데이터베이스 호출 횟수 최소화

주의사항

  • 메모리 사용량 고려 필요

  • 사전 필터링으로 데이터셋 크기 제한

8. 페이지네이션 최적화

기본 원칙

Take와 Skip 사용 시 Order By 필수

  • Order By 없이 사용하면 일관성 없는 결과

  • 같은 레코드가 중복되거나 누락될 수 있음

고급 기법

키 기반 페이지네이션


where w.ID > key

  • Skip 없이 시작점 지정

  • 데이터베이스가 라인을 스캔하지 않고 바로 시작점부터 처리

  • API에서 키를 유지하거나 클라이언트로부터 받아 사용

장점

  • 안정적인 페이지네이션 결과

  • 대용량 데이터에서 성능 향상

9. 재사용 가능한 Expression Tree

문제점

  • 동일한 복잡한 필터를 여러 쿼리에 복사-붙여넣기

  • 규칙 변경 시 모든 위치에서 수정 필요

해결방법

Expression Tree 활용


Expression<Func<Person, bool>> isActiveRecently = // 표현식 정의

  • 재사용 가능한 필터 표현식 생성

  • LINQ가 SQL로 번역 가능

  • 중앙 집중식 관리로 유지보수성 향상

실용적 활용

  • 소프트 삭제 필터

  • 테넌트 격리

  • 필터 라이브러리 구성

장점

  • 전체 애플리케이션에서 일관된 필터링

  • 한 곳에서 변경으로 모든 곳에 적용

  • 대규모 프로젝트에서 필수적

실용적인 팁과 주의사항

성능 최적화

  • 중간값은 반드시 캐싱: 동일한 계산의 반복 방지

  • ToLookup 우선 고려: GroupBy보다 성능 우수

  • 사전 필터링: AsEnumerable이나 ToList 사용 전 데이터 크기 제한

가독성 향상

  • String.Join 사용: 문자열 연결 시 Aggregate 대신 사용

  • SelectMany 활용: 중첩 컬렉션 처리 시 복잡한 루프 대신 사용

  • 명시적 Order By: 페이지네이션에서 일관성 보장

메모리 관리

  • 신중한 ToList 사용: 대용량 데이터 시 메모리 부하 고려

  • AsEnumerable은 수술용 메스: 필요한 경우에만 최소한으로 사용

유지보수성

  • Expression Tree로 필터 중앙화: 재사용성과 일관성 확보

  • 키 기반 페이지네이션: API 성능 향상

결론

이러한 LINQ 최적화 기법들은 단순히 성능 향상뿐만 아니라 코드의 가독성과 확장성을 크게 개선한다. 특히 대규모 LINQ 코드베이스에서는 이러한 변경사항이 프로젝트 전체의 품질을 좌우하는 핵심 요소가 될 수 있다.

1개의 좋아요