메서드보다 람다 표현식이 낫다
람다 표현식을 이용하여 코드를 작성하다보면 동일한 코드를 반복하는 경우가 자주 발생한다. 다음의 코드도 바로 그러한 예다.
var allEmployees = FindAllEmployees();
// 20년 이상 근속자
var earlyFolks = from e in allEmployees
where e.Classification == EmployeeType.Salary
where e.YearsOfService >= 20
where e.MonthlySalary < 4000
select e;
// 20년 미만 근속자
var newest = from e in allEmployees
where e.Classification == EmployeeType.Salary
where e.YearsOfService < 20
where e.MonthlySalary < 4000
select e;
앞의 쿼리에서는 where 절을 여러 번에 걸쳐 나누어 썼지만 모든 조건을 결합하여 하나의 where 절로 변경할 수도 있을 것이다. 하지만 이렇게 코드를 수정한다고 해도 성능이 개선되기를 기대하기는 어렵다. 앞서 알아본 바와 같이 단순한 조건문의 경우 모두 인라인화될 가능성이 높기 때문이다.
동일한 람다 표현식이 반복 사용되는 것을 방지하기 위해서 재사용 가능한 메서드를 이용할 수도 있다. 이경우 코드를 다음과 같이 변경하면 된다.
// 메서드로 분리
private static bool LowPaidSalaried(Employee e) =>
e.MonthlySalary < 4000 && e.Classfication == EmployeeType.Salary;
// 20년 이상 근속자
var allEmployees = FindAllEmployees();
var elaryFolks = from e in AllEmployees
where LowPaidSalaries(e) &&
e.YearsOfService >= 20
select e;
// 20년 미만 근속자
var newest = from e in allEmployees
where LowPaidSalaries(e) &&
e.YearsOfService < 2-
select e;
간단한 예제다 보니 크게 바뀐 것 같지는 않지만 그래도 이전에 예제보다는 훨씬 나아 보인다. 이제 MonthlySalary 나 Classfication 조건이 바뀌면 한 군데만 수정하면 된다.
불행히도 메서드로 분리한 부분은 재사용 가능성이 낮아보인다. 그리고 두 번째 코드보다 첫번째 코드가 실제로는 재사용 가능성이 높다. 그 이유는 람다 표현식이 평가되고, 파싱되고 수행되는 일련의 과정 때문이다. 대부분의 개발자가 그러하듯 동일한 코드를 복사하여 사용하는 것은 만악의 근원이요 반드시 제거돼야 하는 부분이라고 생각할 것이다. 메서드로 공통 코드를 분리하면 코드가 더욱 간단해지고 나중에 수정할 부분도 하나이기 때문에 좋다고 생각할 것이다. 이는 좋은 소프트웨어 엔지니어링이란 무엇인가에 대한 매우 규범적인 이야기이기도 하다.
그러나 불행히도 이 또한 틀린 이야기다. 통상 쿼리 표현식 내의 람다 표현식은 델리게이트로 변한되어 수행된다. 다른 경우에는 람다 표현식을 활용하여 표현식 트리를 만들고, 향후 이를 파싱하여 완전히 다른 구문을 행성한 후, 그 결과를 다른 환경에서 수행하기도 한다.
당연한 이야기지만 라이브러리 전체에 걸쳐 반복 코드를 작성해도 좋다는 말이 아니라 쿼리 표현식과 람다가 관련된 경우에 한하여 기존 방식과는 조금 다른 형태의 빌딩 블록을 만들어서 사용해야 한다는 것이다. 앞의 코드 예제의 경우 다음과 같이 재사용 가능한 빌딩 블록을 만들 수 있다.
private static IQueryable<Employee> LowPaidSalariedFilter(this IQueryable<Emplyee> sequence) =>
from s in sequence
where s.Classification == EmployeeType.Salary &&
s.MonthlySalary < 4000
select s;
// 나머지 부분
var allEmployees = FindAllEmployees();
// 우선 필터링을 수행함
var salaried = allEmployees.LowPaidSalariedFilter();
// 20년 이상 근속자
var earlyFolks = salaried.Where(e => e.YearsOfService >= 20);
// 20년 미만 근속자
var newest = salaried.Where(e => e.YearsOfService < 20);
물론 모든 쿼리를 이처럼 단순하게 개선할 수 있는 것은 아니다. 람다 표현식이 중복적으로 나타나지 않도록 하기 위해서 동일하게 사용되는 로직을 분리하여 호출 체인의 앞쪽으로 이동시켜야 할수도 있다.
이터레이터 메서드는 컬렉션 내의 항목들을 실제로 순회하기 전까지는 호출되지 않는다는 사실을 다시 한번 상기하기 바란다. 즉, 람다 표현식을 포함하는 간단한 메서드들을 작성하여 쿼리의 일부분을 대체할 수 있다. 이러한 메서드들은 반드시 입력 시퀀스를 취하도록 작성되어야 하며, yield return 키워드를 이용하여 시퀀스를 반환해야 한다.
이 같은 패턴을 사용하면 원격자에게 수행할 새로운 표현식 트리를 생성하기 위해서 IQueryable을 사용하는 enumerator를 조합하여 사용할 수 있다. 즉, 앞의 예제와 같이 IQueryable을 사용하는 메서드를 이용하면 여러 단계에 걸쳐 쿼리를 수행하더라도 결과적으로 원격자에게 IQueryProvider 객체는 로컬에서 처리해야하는 부분을 따로 분리하지 않으며, 전체 쿼리를 단번에 처리한다.
다음으로 할 일은 앞서 잘게 쪼갠 코드를 재결합하여 응용프로그램 내에서 재사용할 수 있는 형태의 더 큰 쿼리를 만드는 것이다. 이러한 기법을 사용하면 첫 번째 예제에서 이야기한 코드 중복 문제를 피할 수 있을 뿐 아니라 쿼리가 실제로 수행되기 직전에 완성된 표현식 트리를 생성하도록 코드를 구조화할 수 있다.
쿼리를 작성할 때 람다 표현식을 본문으로 하는 조그만 빌딩 블록을 조합하여 완전한 쿼리를 작성할 수 있다. 각각의 빌딩 블록은 IEnumerable<T>와 IQueryable<T>를 활용하면 장점을 극대화할 수 있다.
함께 읽으면 좋은 글
출처
'프로그래밍 > Effective C#' 카테고리의 다른 글
Effective C# Item 12 : 할당 구문보다 멤버 초기화 구문이 좋다 (1) | 2023.10.20 |
---|---|
Effective C# Item 37 : 쿼리를 사용할 때는 즉시 평가보다 지연 평가가 낫다 (0) | 2023.10.11 |
Effective C# Item 34 : 함수를 매개변수로 사용하여 결합도를 낮추라 (0) | 2023.09.27 |
Effective C# Item 3 : 캐스트보다는 is, as가 좋다 (0) | 2023.09.26 |
Effective C# Item 7 : 델리게이트를 이용하여 콜백을 표현하라 (0) | 2023.09.26 |
댓글