2023. 7. 27. 15:00

EF(Entity framework)를 사용하다 보면 가장 불편한 것이 정렬입니다.

 

앵? '.OrderBy', 'OrderByDescending' 하면 되는데요?

맞습니다.

 

문제는 일반적인 게시판의 정렬처럼 조건이 다양한 정렬의 경우 각각 오더바이(Orderby)를 따로 호출해야 한다는 것입니다.

 

 

1. 문제의 시작

아래는 일반적인 게시판을 정렬하기 위한 코드입니다.

switch (sColumn)
{
    case "idTestOrderBy":
        if(true == bAsc)
        {
            iqTO = iqTO.OrderBy(ob => ob.idTestOrderBy);
        }
        else
        {
            iqTO = iqTO.OrderByDescending(ob => ob.idTestOrderBy);
        }
        break;

    case "Str":
        if (true == bAsc)
        {
            iqTO = iqTO.OrderBy(ob => ob.Str);
        }
        else
        {
            iqTO = iqTO.OrderByDescending(ob => ob.Str);
        }
        break;

    case "Int":
        if (true == bAsc)
        {
            iqTO = iqTO.OrderBy(ob => ob.Int);
        }
        else
        {
            iqTO = iqTO.OrderByDescending(ob => ob.Int);
        }
        break;
}

 

 

컬럼이 몇 개 안 되는데도 엄청난 코드 길이를 볼 수 있습니다.

여기에 'if'문의 구조를 바꾸게 되면 컬럼 개수만큼 노가다를 해야 한다는 건 덤입니다 ㅎㅎㅎㅎ

 

 

2. 해결 방법 중 하나

이 문제를 해결하고자 많은 개발자가 다양한 방법을 연구했지만....

사실 그렇다 할 방법은 없었습니다.

 

이 포스팅에서 제시할 방법도 모두가 원하는 그런 방식은 아닙니다.

단지 지금까지 제가 찾았던 방식 중에 가장 원하는 것이 가까워서 소개합니다.

 

이 포스팅에서 소개할 방법은 'Asontu'님이 제시해 준 방법입니다.

참고 : asontu - A better way to do dynamic OrderBy() in C#

 

이 방법을 사용하면 위에서 보았던 코드를 아래와 같이 사용할 수 있습니다.

string sColumn = "Name";
bool bAsc = true;

IOrderBy? orderBy = null;

switch (sColumn)
{
    case "idTestOrderBy":
        orderBy = new OrderBy<TestOrderBy, int>(x => x.idTestOrderBy);
        break;

    case "Str":
        orderBy = new OrderBy<TestOrderBy, string>(x => x.Str);
        break;

    case "Int":
        orderBy = new OrderBy<TestOrderBy, int>(x => x.Int);
        break;
}

if (true == bAsc)
{
    iqTO = iqTO.OrderBy(orderBy!);
}
else
{
    iqTO = iqTO.OrderByDescending(orderBy!);
}

 

정렬할 대상을 실행식으로 받아서 이 계산식을 가지고 있다가 정렬 시 쿼리에 실행식을 전달하는 방식으로 구현됩니다.

 

 

3. 'Asontu' 제시 방법 구현하기

구현은 크게 실행식을 저장해 둘 개체와 'IOrderedQueryable'에 사용할 확장으로 구성됩니다.

 

3-1. 'IOrderBy' 구현

실행식 저장을 위한 인터페이스(interface)입니다.

/// <summary>
/// EF의 동적 정렬을 위한 실행 식 인터페이스
/// </summary>
public interface IOrderBy
{
    /// <summary>
    /// 사용할 실행식
    /// </summary>
    dynamic Expression { get; }
}

 

3-2. 'OrderBy' 구현

실행식을 저장해 둘 개체입니다.

정렬에 사용할 테이블 형식과 실행 식을 전달받아 저장합니다.

/// <summary>
/// EF의 동적 정렬을 위한 실행식 개체
/// </summary>
/// <typeparam name="TToOrder">정렬에 사용할 테이블 형식</typeparam>
/// <typeparam name="TBy">정렬에 사용할 컬럼의 형식</typeparam>
public class OrderBy<TToOrder, TBy> : IOrderBy
{
    /// <summary>
    /// 실행시킬 실행식 개체
    /// </summary>
    private readonly Expression<Func<TToOrder, TBy>> expression;

    /// <summary>
    /// 실행식 실행
    /// </summary>
    public dynamic Expression => this.expression;

    /// <summary>
    /// 실행식을 전달받아 저장한다.
    /// </summary>
    /// <param name="expression">실행식</param>
    public OrderBy(Expression<Func<TToOrder, TBy>> expression)
    {
        this.expression = expression;
    }
}

 

3-3. 'IOrderedQueryable'에 정렬 확장 넣기

'IOrderedQueryable'에 실행식(IOrderBy로 생성된 개체)을 전달받아 정렬할 확장을 만들어 넣어 줍니다.

/// <summary>
/// EF 정렬의 확장 오버로드
/// </summary>
public static class OrderByExtensions
{
    /// <summary>
    /// 오름차순 정렬(1 → 2 → 3 → 4)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"></param>
    /// <param name="orderBy">실행식 개체</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, IOrderBy orderBy)
    {
        return Queryable.OrderBy(source, orderBy.Expression);
    }

    /// <summary>
    /// 내림차순 정렬(4 → 3 → 2 → 1)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"></param>
    /// <param name="orderBy">실행식 개체</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, IOrderBy orderBy)
    {
        return Queryable.OrderByDescending(source, orderBy.Expression);
    }

    /// <summary>
    /// 오름차순 정렬(1 → 2 → 3 → 4)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"></param>
    /// <param name="orderBy">실행식 개체</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, IOrderBy orderBy)
    {
        return Queryable.ThenBy(source, orderBy.Expression);
    }

    /// <summary>
    /// 내림차순 정렬(4 → 3 → 2 → 1)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"></param>
    /// <param name="orderBy">실행식 개체</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, IOrderBy orderBy)
    {
        return Queryable.ThenByDescending(source, orderBy.Expression);
    }
}

 

 

4. 사용하기

이제 변수로 'IOrderBy'를 선언하고

여기에 실행식을 넣어(new OrderBy)

원하는 정렬을 하면 됩니다(OrderBy, OrderByDescending)

 

구조는 아래와 같습니다.

IQueryable<[사용할 테이블] > iqTO = db1.TestOrderBy;
IOrderBy orderBy = null;
orderBy = new OrderBy<[사용할 테이블], [변수 타입]>(x => [정렬할 대상]);
iqTO = iqTO.OrderBy(orderBy);

 

테스트 코드는 다음과 같습니다.

IQueryable<TestOrderBy> iqTO = db1.TestOrderBy;

string sColumn = "Name";
bool bAsc = true;

IOrderBy? orderBy = null;

switch (sColumn)
{
    case "idTestOrderBy":
        orderBy = new OrderBy<TestOrderBy, int>(x => x.idTestOrderBy);
        break;

    case "Str":
        orderBy = new OrderBy<TestOrderBy, string>(x => x.Str);
        break;

    case "Int":
        orderBy = new OrderBy<TestOrderBy, int>(x => x.Int);
        break;
}

if (true == bAsc)
{
    iqTO = iqTO.OrderBy(orderBy!);
}
else
{
    iqTO = iqTO.OrderByDescending(orderBy!);
}

 

성능도 직접 EF를 다룰 때랑 차이가 없습니다.

여러 번 테스트한 결과 근소하게 'Asontu'님의 방식이 빨랐습니다.

(이유는 모르겠네요;;;)

 

 

단점

이 방법의 가장 큰 단점은 

사용할 테이블을 별도로 지정해야 한다는 것입니다.

 

테이블을 잘못 지정해도 컴파일 타임(compile time)에는 에러가 나지 않습니다.

상황에 따라서 빌드타임에 잡힐 수 도있고 런타임에 잡힐 수도 있습니다.

 

 

두 번째 단점은 사용할 변수 타입을 직접 지정해야 한다는 것입니다.

그래도 이건 컴파일 타임에 에러가 표시되므로 큰 불편함은 아니지만.......

원래 자동으로 처리되던 걸 수동으로 처리해야 하는 불편함은 있습니다.

 

 

마무리

테스트 프로젝트 : dang-gun/EntityFrameworkSample/DynamicOrderBy

 

이 방식은 코드가 간결한 편이고 속도 저하도 없다는 점이 가장 마음에 듭니다.

EF의 동적 정렬 방식으로 제시된 구현을 보면 속도 저하가 있는경우가 많기 때문입니다.

 

이 방식이 직접 EF를 정렬하는 것보다 근소하게나마 빠른 이유는 테이블과 변수형을 개발자가 지정해 주기 때문이 아닌가 생각되네요.

 

그래도 수년만에 그나마 마음에 드는 방법을 찾아서 기쁩니다.

지금까지는 마음에 드는 방법이 없어서 직접 EF로 정렬했거든요 ㅎㅎㅎㅎ