# Effective 4장 - 2 ## 각 장별 요약 - 1장 : C# 언어 요소 - 2장 : .NET 리소스 관리 - 3장 : 제네릭 활용 - 4장 : LINQ 활용 - 5장 : 예외 처리 --- ## 4 장 : LINQ 활용 > LINQ 및 쿼리 문법과 관련된 내용을 주로 다룬다. ### 아이템 37 : 쿼리를 사용할 때는 즉시 평가보다 지연 평가가 낫다 - 쿼리를 정의한다고 해서 결과 데이터나 시퀀스를 즉각적으로 얻어오는 것은 아니다. - 쿼리를 정의한다는 것은 절차만 정의하는 것에 지나지 않는다. - 즉, 쿼리의 결과를 이용하여 순회를 수행해야만 결과가 생성된다. 이를 지연평가라고 한다. - 하지만 일반 변수를 사용하는 것처럼 즉각적으로 그 값을 얻어와야 할 때도 있다. 이를 즉시평가라고 한다. - 따라서 데이터를 정의할 때, 쿼리의 수행시간이 어느정도 있다는 것을 인지해야한다. - ex) ```csharp= public static void Main() { // 지연 평가 var sequence = from number in AllNumbers() select number; // 10개의 시퀀스를 가져옴 var results = sequence.Take(10); var hashSet = new HashSet<string>(); foreach (var result in results) { // 엔터를 입력하는 시간에 맞추어 다음 시퀀스로 진행 Console.WriteLine("Press Enter"); Console.ReadLine(); if (!hashSet.Contains(result)) hashSet.Add(result); } // 여러개의 결과값 도출 Console.WriteLine(hashSet.Count); } static IEnumerable<string> AllNumbers() { var number = 0; while (number < 10) { number++; yield return DateTime.Now.ToString(); } } ``` - 하지만 쿼리를 이용해서 즉시평가 값을 기대하는 경우에는 `ToArray()` 또는 `ToList()` 를 사용하면 된다. ```csharp= public static void Main() { var sequence = from number in AllNumbers() select number; // 10개의 시퀀스를 가져옴, 즉시 평가 var results = sequence.Take(10).ToList(); var hashSet = new HashSet<string>(); foreach (var result in results) { Console.WriteLine("Press Enter"); Console.ReadLine(); if (!hashSet.Contains(result)) hashSet.Add(result); } // 엔터를 느리게 치더라도 한개의 결과값만 도출 Console.WriteLine(hashSet.Count); } static IEnumerable<string> AllNumbers() { var number = 0; while (number < 10) { number++; yield return DateTime.Now.ToString(); } } ``` --- ### 아이템 38 : 메서드보다 람다 표현식이 낫다 - 람다 표현식을 이용하여 코드를 작성하다 보면 동일한 코드를 반복하는 경우가 자주 발생한다. - 따라서 이렇게 반복되는 코드를 따로 분리해서 사용하면 좋다고 생각할것이다. - 하지만 이때, 람다와 함께 변환되는 `LINQ` 표현식에 주의가 필요하다. - `LINQ to Objects` 의 통상 쿼리 표현식 내의 람다 표현식은 델리게이트로 변환되어 수행된다. - `LINQ to Objects` 는 지역 데이터저장소에 대하여 쿼리를 수행하는 방법인데, 일반적으로 컬렉션 내에 저장된 요소를 쿼리할 때 사용한다. - `LINQ to SQL` 의 경우에는 람다 표현식을 활용하여 표현식 트리를 만들고, 향후 이를 파싱하여 완전히 다른 구문을 생성한 후, 그 결과를 다른 환경에서 수행하기도 한다. - `LINQ to SQL` 은 표현식 트리를 파싱하여 적절한 `T-SQL` 쿼리를 생성한 후 이를 데이터베이스에 전달한다. - 다양한 데이터 소스에 대하여 재사용 가능한 라이브러리를 만드는 경우라면 이러한 상황은 반드시 고려돼야 한다. - 이를 해결하기 위해 람다 표현식을 포함하는 간단한 메서드들을 작성하여 쿼리의 일부분을 대체할 수 있다. - 이러한 메서드들은 반드시 입력 시퀀스를 취하도록 작성 되어야 하며 `yield return` 키워드를 이용하여 시퀀스를 반환해야한다. --- ### 아이템 39 : function 과 action 내에서는 예외가 발생하지 않도록 하라 - 일련의 값을 순차적으로 처리하는 코드가 중간 어디쯤에서 예외를 일으키면 상태를 복구할 수 없는 문제에 봉착한다. - 처리가 완료된 요소가 몇 개인지를 알 수도 없고 따라서 원복해야 할 요소가 무엇인지도 알 수없다. - 결국 프로그램의 상태를 전혀 원복할 수 없게 된다. - 이 문제는 코드가 시퀀스 내에 포함된 요소의 값을 직접 수정하기 때문에 발생한다. - 이러한 작업은 언제든 예외를 유발할 가능성이 있으며 오류 발생시에도 무슨일이 발생했는지 자세히 알아낼 도리가 없다. - 문제 상황을 해결하려면 메서드가 작업을 완전히 완료하기 전까지는 최소한 외부에서 바라보는 프로그램의 상태가 변경되지 않도록 보장하면 된다. - 해결 방법 1. 람다 표현식으로 나타낸 액션 메서드가 절대 예외를 발생시키지 않도록 하는 방법 - 대부분의 경우 시퀀스 내의 개별 요소들을 수정하기 이전에 오류 가능성을 사전에 테스트해볼 수 있다. - 만약 오류가 발생할 가능성이 있다면 아무런 작업도 수행하지 않고 자연스럽게 다음 요소로 넘어가도록 작성하는 것도 방법이 될 수 있다. - 데이터가 일관성을 가지지 못해서 오류가 발생하는 경우 데이터에 필터를 적용시켜보자 2. 그럼에도 불구하고 예외가 발생 할때는 강력한 방어조치를 해야한다. - 원본 시퀀스의 복사본을 마련해두고 해당 복사본에 대해 알고리즘을 적용한 후, 모든 작업이 완료된 경우에만 원본 시퀀스를 대체하는 방식이다. - 예외가 발생하지 않은 경우에 한해서 전체 시퀀스를 변경하는것으로 상당히 유용하다. --- ### 아이템 40 : 지연 수행과 즉시 수행을 구분하라 - 선언적 코드는 해설적이며 무슨 작업을 해야하는지를 정의한다. - 명령형 코드는 어떻게 작업을 수행해야 하는지를 단계별로 세분화해서 기술한다. ```csharp= // 명령형 var answer = DoStuff(Method1(), Method2(), Method3()); // 진행 순서 // Method1() -> Method2() -> Method3() -> DoStuff() // 선언형 var answer = DoStuff(() => Method1(), () => Method2(), () => Method3()); // 진행 순서 // DoStuff() -> 각 매개변수가 호출될 때마다 알맞는 Method() 를 호출 ``` - 두 가지 개발 방법 모두 프로그램을 작성하는데 흔히 사용되는 유효한 방법이지만 이 둘을 섞어 사용하는 경우 예기치 않은 결과가 발생할 수도 있다. - 이렇게 메서드를 매개변수로 넘기게 되면 데이터와 메서드가 서로 상호 대체 가능하다. - 하지만 다음과 같은 차이점이 있으니 주의하자 | 분류 |데이터 | 메서드 | | -- | -- | -- | | 평가 방식 |즉시 평가|지연평가| |Side Effect| X | 발생할 수 있음| --- - 지연수행과 즉시수행차이를 최소화하기위해서는 `immutable` 타입을 사용할 수 있다. - `immutable` 타입은 그 값을 수정할 수 없을 뿐더러 부수효과가 없다. - `immutable` 한 메서드는 항상 결과가 동일하도록 매개변수 또한 `immutable`하게 제한해야한다. - 계산 비용을 줄이기 위해 데이터를 캐시하는 등 위 두가지 코드의 장점을 섞어서 메서드의 효율을 높일 수 있다. --- ### 아이템 41 : 값비싼 리소스를 캡처하지 말라 - 클로저는 클로저에 바인딩된 변수를 포함하는 객체를 생성한다. - 바인딩된 변수의 수명이 길어지면 깜짝 놀랄만한 문제를 일으킬 수 있으므로 주의해야 한다. - 지역변수는 선언된 블록 내에서만 유효하며 그 블록을 벗어나면 향후 가비지 수집기에 의해서 정리될 것이라 생각할 수 있다. - 하지만 클로저와 캡처된 변수는 이러한 규칙을 벗어난다. - 클로저 내에서 변수를 캡처하면 변수를 사용하는 마지막 델리게이트가 가비지화 될 때까지 객체의 수명이 늘어나게 된다. - 다행히도 특별히 무거운 리소스를 참조하는 변수가 아니라면 다른 변수처럼 적절한 시점에 가비지수집이 될 것이다. - 지역변수가 단순히 메모리 리소스만 사용하는 경우라면 더더욱 신경 쓸 필요가 없다. - 하지만 일부 변수들은 무거운 리소스를 참조하고 있을 수 있다. - 통상 이러한 타입들은 리소스를 명시적으로 해제하기 위해서 `IDisposable` 을 구현하고 있다. - 이 경우 해당 리소스르 이용하여 결괏값을 획득했다면 그 즉시 리소스를 정리할 수 있다. --- ### 아이템 42 : `IEnumerable<T>` 데이터 소스와 `IQueryalbe<T>` 데이터 소스를 구분하라 - `IQueryable<T>` 와 `IEnumerable<T>` 는 거의 동일한 API 정의를 가진다. - 따라서 이 두 인터페이스는 상호 교환 가능하다고 생각할 것이며 실제로도 대부분 그렇다. - 사실 둘은 동작 방식도 매우 다르고 성능 차이도 크게 난다. - `IQueryable<T>` 를 사용한 쿼리 구문은 `LINQ to SQL` 라이브러리가 모든 쿼리문을 결합하여 단번에 T-SQL 결과물을 생성한다. - 이후 단 한차례 데이터베이스를 호출하여 결과를 가져온다. - `IEnumerable<T>` 를 사용한 쿼리 구문은 수행된쿼리문이 `IEnumerable<T>` 시퀀스를 반환하므로 그 다음 작업은 `LINQ to Objects` 구현체와 델리게이트를 이용하여 수행된다. - `IEnumerable<T>` 는 데이터베이스 객체를 `IEnumerable<T>` 시퀀스로 변경하기 때문에 데이터베이스가 아니라 로컬 컴퓨터에서 더 많은 작업을 수행한다. - 둘의 차이로 인해 일부 쿼리들은 둘 중 어느 한쪽에 대해서만 올바르게 동작하는 경우도 있다. - 간혹 특정 메서드가 동일한 T 타입에 대해서 `IEnumerable<T>` 와 `IQueryable<T>` 를 이용한 쿼리를 모두 지원해야 하는 경우가 있다. - 이 경우 코드 중복이 발생할 가능성이 높은데, 이럴 때는 `AsQueryable()` 를 사용하여 `IEnumerable<T>` 를 `IQueryable<T>` 로 변경하면 중복을 제거할 수 있다. - `AsQueryable()` 는 시퀀스의 런타임 타입을 확인한다. - 이후 `IQueryable<T>` 이면 `IQueryable<T>` 를 반환하고 `IEnumerable<T>` 면 `LINQ to Objects` 를 사용하여 `IQueryable<T>` 를 구현한 래퍼를 생성하여 반환한다. --- ### 아이템 43 : 쿼리 결과의 의미를 명확히 강제하고, `Single()` 과 `First()` 를 사용하라 - LINQ 라이브러리를 빠르게 살펴보면 이것이 일련의 데이터 세트, 즉 시퀀스를 반환하도록 설계됐다고 생각할 것이다. - 하지만 LINQ 는 단일의 값을 반환하는 메서드도 포함하고 있다. - 이런 메서드들은 다른 메서드와는 달리 쿼리의 결과를 이용하되 단일의 스칼라 값을 반환해주는데, 이를통해 개발자의 의도나 기대를 손쉽게 표현할 수 있도록 도와준다. - `Single()` - 해당 메서드는 은 정확히 1개의 요소만 반환한다. - 만약 쿼리 결과에 어떤 요소도 포함되지 않거나 혹은 여러개의 요소가 포함되는 경우에는 예외를 유발한다. - 이는 개발자의 의도를 정확히 드러내기 위한 강력한 수단이 되기도 한다. - 특정 쿼리가 정확히 1개의 요소만을 반환해야한다고 강력히 제한하고 싶다면 `Single()` 을 사용하면 된다. - `SingleOrDefault()` - 쿼리의 결과가 어떠한 요소도 포함하지 않거나 하나의 요소만을 반환하는 경우 - 얻어오려는 요소가 시퀀스 상에서 몇번째에 위치하는지 정확히 알고 있다면 `Skip()` 과 `First()` 를 이용하자 --- ### 아이템 44 : 바인딩된 변수는 수정하지 말라 - C# 컴파일러는 쿼리 표현식과 람다 표현식 모두 정적 델리게이트나 인스턴스 델리게이트, 혹은 클로저로 변환한다. - 이 중 어떤것을 선택하느냐는 람다 본문의 코드를 어떻게 작성했느냐에 따라 결정된다. - 인스턴스 변수나 지역변수에 전혀 접근하지 않았을 경우 정적 델리게이트로 변환한다. - 인스턴스 변수에만 접근했을 경우 인스턴스 델리게이트로 변환한다. - 지역변수로 접근했을 경우 클로저로 변환한다. - 이 때, 여러개의 쿼리를 연이어 수행하여 바인딩된 변수의 값을 수정하게 되면 지연 수행과 컴파일러의 클로저 구현 특성 때문에 예상치 않은 문제가 발생할 수 있다. - 따라서 클로저에 의해 캡처되어 바인딩된 변수는 수정하지 않는 것이 좋다. --- ### 느낀점 > LINQ 단원의 마지막 부분이었다. > 시퀀셜한 데이터를 얻어오기에는 구문별로 조합과 관리가 용이해 > 좋은 라이브러리임에 틀림 없지만, 여러 주의해야할 부분이 많다고 느꼈다. > C# 컴파일러가 내부에서 쿼리구문을 > 델리게이트나 클로저로 변환해주는 기준과 원리를 알 수 있어서 재미있었다. > 이제 마지막 예외처리 단원이 남았는데, > 폭 넓은 이해를 위해서는 다 읽고나서 몇번 더 회독을 해야 할 것 같다.