EF Core 10의 새로운 기능: LINQ의 LeftJoin 및 RightJoin 연산자 | Milan Jovanović


ef-core-10 linq #LeftJoin #RightJoin #데이터베이스

핵심 요약

EF Core 10은 LeftJoin과 RightJoin을 처음으로 LINQ에 직접 도입하여, 데이터베이스 개발에서 가장 자주 사용되는 LEFT JOIN과 RIGHT JOIN을 훨씬 간편하게 표현할 수 있게 했습니다. 기존에는 GroupJoin과 DefaultIfEmpty를 조합해야만 했던 복잡한 과정이 이제 단 하나의 메서드 호출로 의도를 명확하게 드러낼 수 있게 되었으며, 코드의 가독성 향상과 버그 감소를 동시에 달성합니다. 모든 사용자를 보여주되 선택적 프로필 설정은 포함하거나, 모든 제품을 보여주되 리뷰는 없을 수 있는 상황 같이 실무에서 매우 빈번하게 필요한 작업입니다.


상세 분석

1. LEFT JOIN의 개념과 필요성

LEFT JOIN의 정의

LEFT JOIN은 왼쪽 테이블의 모든 행을 반환하고, 오른쪽 테이블에서 일치하는 행만 가져오는 연산입니다. 일치하는 행이 없으면 오른쪽 테이블의 값은 NULL이 됩니다. 이는 기본 INNER JOIN과 달리 일치하지 않는 왼쪽 데이터도 결과에 포함되므로, 데이터 손실이 발생하지 않습니다.

활용 시나리오

  • 모든 제품 조회: 리뷰가 없는 제품도 함께 표시
  • 모든 사용자 조회: 프로필 설정이 없는 사용자도 포함
  • 모든 주문 조회: 배송 정보가 없는 주문도 함께 표시
  • 모든 직원 조회: 할당된 부서가 없는 직원도 포함

2. 기존 방식: GroupJoin + DefaultIfEmpty

비효율성의 원인

EF Core 9 이전에는 LEFT JOIN을 표현하려면 GroupJoin 메서드로 먼저 그룹화한 후, DefaultIfEmpty로 빈 그룹을 처리하고, 마지막으로 SelectMany로 평탄화해야 했습니다. 이 세 단계의 복합 연산이 필요했기 때문에 개발자의 의도가 코드에 명확하게 드러나지 않았습니다.

쿼리 문법 예제

var query =
    from product in dbContext.Products
    join review in dbContext.Reviews on product.Id equals review.ProductId into reviewGroup
    from subReview in reviewGroup.DefaultIfEmpty()
    orderby product.Id, subReview.Id
    select new
    {
        ProductId = product.Id,
        product.Name,
        product.Price,
        ReviewId = (int?)subReview.Id ?? 0,
        Rating = (int?)subReview.Rating ?? 0,
        Comment = subReview.Comment ?? "N/A"
    };

생성되는 SQL:

SELECT
    p."Id" AS "ProductId",
    p."Name",
    p."Price",
    COALESCE(r."Id", 0) AS "ReviewId",
    COALESCE(r."Rating", 0) AS "Rating",
    COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
ORDER BY p."Id", COALESCE(r."Id", 0)

메서드 문법 예제

var query = dbContext.Products
    .GroupJoin(
        dbContext.Reviews,
        product => product.Id,
        review => review.ProductId,
        (product, reviewList) => new { product, subgroup = reviewList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (joinedSet, review) => new
        {
            ProductId = joinedSet.product.Id,
            joinedSet.product.Name,
            joinedSet.product.Price,
            ReviewId = (int?)review!.Id ?? 0,
            Rating = (int?)review!.Rating ?? 0,
            Comment = review!.Comment ?? "N/A"
        })
    .OrderBy(result => result.ProductId)
    .ThenBy(result => result.ReviewId);

작동 원리

GroupJoin은 행을 매칭시키고, DefaultIfEmpty는 일치 항목이 없을 때 단일 기본값(null)을 삽입하여 왼쪽 행이 여전히 나타나도록 합니다. SelectMany로 중첩된 컬렉션을 평탄화합니다. 결과적으로 기술적으로는 정확하지만 코드 의도를 파악하기 어렵습니다.

3. 새로운 방식: EF Core 10의 LeftJoin

혁신적인 간소화

EF Core 10에서는 LeftJoin이 LINQ의 일급 연산자가 되어 직접 사용할 수 있게 되었습니다. 이제 개발자는 해야 할 일을 그냥 명확하게 표현하면 되며, EF Core가 이를 자동으로 LEFT JOIN SQL로 변환합니다.

메서드 문법 예제

var query = dbContext.Products
    .LeftJoin(
        dbContext.Reviews,
        product => product.Id,
        review => review.ProductId,
        (product, review) => new
        {
            ProductId = product.Id,
            product.Name,
            product.Price,
            ReviewId = (int?)review.Id ?? 0,
            Rating = (int?)review.Rating ?? 0,
            Comment = review.Comment ?? "N/A"
        })
    .OrderBy(x => x.ProductId)
    .ThenBy(x => x.ReviewId)

생성되는 SQL은 이전 예제와 완전히 동일합니다.

주요 개선 사항

  1. 의도의 명확성: LeftJoin을 보면 무엇을 하려는지 즉시 파악
  2. 코드 간결성: GroupJoin, DefaultIfEmpty, SelectMany 제거로 인한 코드량 대폭 감소
  3. 유지보수성 향상: 의도가 명확하면 버그 수정도 용이
  4. 동등한 성능: 생성되는 SQL이 동일하므로 성능상 차이 없음

C# 쿼리 문법 미지원

현재는 C# 쿼리 문법(from … select …) 형식에서 left join이나 right join 키워드를 지원하지 않습니다. 메서드 문법만 사용 가능합니다.

4. RightJoin: 반대편 관점

RightJoin의 역할

RightJoin은 오른쪽 테이블의 모든 행을 유지하고 왼쪽 테이블에서 일치하는 행만 가져옵니다. “반드시 보여줘야 하는” 측이 두 번째 시퀀스일 때 RightJoin이 더 자연스러운 표현입니다.

개념적 예제

var query = dbContext.Reviews
    .RightJoin(
        dbContext.Products,
        review => review.ProductId,
        product => product.Id,
        (review, product) => new
        {
            ProductId = product.Id,
            product.Name,
            product.Price,
            ReviewId = (int?)review.Id ?? 0,
            Rating = (int?)review.Rating ?? 0,
            Comment = review.Comment ?? "N/A"
        });

사용 사례: 리뷰에서 시작하여(모든 리뷰 유지), 일치하는 제품이 있으면 가져오는 리포팅 시나리오

생성되는 SQL

SELECT
    p."Id" AS "ProductId",
    p."Name",
    p."Price",
    COALESCE(r."Id", 0) AS "ReviewId",
    COALESCE(r."Rating", 0) AS "Rating",
    COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Reviews" AS r
RIGHT JOIN "Products" AS p ON r."ProductId" = p."Id"

5. 실전 적용과 최적화

배경 및 중요성

LEFT JOIN이 얼마나 자주 필요한지 생각해보면, 새로운 LeftJoin 메서드가 얼마나 귀중한지 알 수 있습니다. 이전에는 개발자들이 때로 올바른 LEFT JOIN을 피하고 두 개의 별도 쿼리로 대체하거나, 더 나쁘게는 INNER JOIN을 사용하여 데이터를 손실시키기도 했습니다. 이제는 더 이상 이런 타협이 불필요합니다.

실용적인 팁

  1. NULL 처리 필수: 프로젝션에서 NULL 가능 측을 반드시 가드 처리

    • 예: review.Comment ?? "N/A"
  2. 프로젝션 최소화: 필요한 컬럼만 선택하여 불필요한 데이터 전송 방지

  3. 인덱스 추가: 조인 키에 인덱스를 추가하여 쿼리 계획 개선

    • JOIN 성능은 인덱스 여부에 따라 크게 달라짐

의미 있는 변화

EF Core 10의 LeftJoin과 RightJoin으로, 코드가 최종적으로 우리의 정신 모델과 일치합니다. 개발자가 생각하는 대로 코드를 작성할 수 있게 되어 인지적 부담이 크게 줄어듭니다.


학습 리소스

1개의 좋아요