# Effective 4장 - 1 ## 각 장별 요약 - 1장 : C# 언어 요소 - 2장 : .NET 리소스 관리 - 3장 : 제네릭 활용 - 4장 : LINQ 활용 - 5장 : 예외 처리 --- ## 4 장 : LINQ 활용 > LINQ 및 쿼리 문법과 관련된 내용을 주로 다룬다. ### 아이템 29 : 컬렉션을 반환하기보다 이터레이터를 반환하는 것이 낫다 - 메서드를 작성하다 보면 단일의 객체를 반환하기 보다 일련의 시퀀스를 반환해야 하는 경우가 종종 있다. - 시퀀스를 반환하는 메서드를 작성해야 한다면 전체 컬렉션을 반환하기보다는 이터레이터를 반환하는 것이 좋다. - 이터레이터를 반환하면 이를 이용하여 다양한 작업을 좀 더 수월하게 수행할 수 있기 때문이다. - 이터레이터 메서드란 호출자가 요청한 시퀀스를 생성하기 위해서 `yield return` 문을 사용하는 메서드를 말한다. - ex ) ```csharp= public static IEnumerable<char> GenerateAlphabet() { var letter = 'a'; while (letter <= 'z') { yield return letter; letter++; } } ``` - 해당 메서드는 시퀀스 인스턴스를 생성하여 반환하게 된다. - 반환하는 시퀀스의 크기가 작을때는 문제가 없지만, 시퀀스의 크기가 크다면 문제가 발생한다. 1. 방대한 반환값을 저장하는 저장공간이 필요하다. 2. 사용자가 모든 시퀀스 중 일부만 필요할 수 있다. - 따라서 매개변수를 이용해 '필요할때 생성' 전략을 취해야한다. ```csharp= public static IEnumerable<char> GenerateAlphabet(char first, char last) { // 유효값 검사 if (first < 'a') throw new ArgumentException("First is smaller than 'a'"); if (first > 'z') throw new ArgumentException("First is larger than 'z'"); if (last < first) throw new ArgumentException("Last is smaller than First"); if (last > 'z') throw new ArgumentException("Last is larger than 'z'"); // 이터레이터 생성 var letter = first; while (letter <= last) { yield return letter; letter++; } } ``` - 위와 같이 매개변수를 이용하여 유효값 검사와 동시에 원하는 범위 내에서의 이터레이터를 반환할 수 있다. - 하지만 위 코드를 컴파일 해본다면 시퀀스의 첫 번째 요소의 요청이 매개변수가 유효한지 확인하는 코드 이전에 실행되는 단점이 있다. - 따라서 아래와 같이 private 메서드로 분리한다면 이를 더 쉽게 확인하고 수정할 수 있다. ```csharp= public static IEnumerable<char> GenerateAlphabet(char first, char last) { // 유효값 검사 ... // 이터레이터 생성 return GenerateAlphabetSumbset(first, last); } private static IEnumerable<char> GenerateAlphabetSumbset(char first, char last) { var letter = first; while (letter <= last) { yield return letter; letter++; } } ``` --- - 가끔은 전체 시퀀스를 반환하는것이 좋을 때가 있다. - 우리가 제작한 API가 시퀀스 유효성 검사의 계산 시간과 저장소 공간 비용이 더 클 경우에는 전체 시퀀스를 반환하는 것이 좋다. - 또한 사용자가 해당 반환값을 어떤 용도로 사용할지 예측할 수 없을때는 `IEnumerable<T>` 와 같은 인터페이스르 반환하는것이 좀 더 편리하다. - 이는 필요할 때마다 항목을 생성할 수도 있고, `ToList()` 나 `ToArray()` 를 이용하여 전체 시퀀스가 저장된 컬렉션을 생성할 수도 있기 때문이다. --- ### 아이템 30 : 루프보다 쿼리 구문이 낫다 - 코드 컨벤션에서 SQL 방식의 LINQ 는 사용하지 않는것을 알게되었다. - C#은 흐름을 제어할 수 있는 for, while, do/while, foreach 등의 다양한 반복 구문을 제공하기 때문에 일상적인 개발에는 부족함이 없다. - 하지만 쿼리 구문을 사용하는 것이 반복문을 사용하는 것 보다 더 나은 경우가 꽤 있다. > 0 부터 99 까지 수에 대한 ( X , Y ) 좌표를 생성하는 메소드 > X 와 Y 의 합이 100 보다 작은 경우에만 해당 쌍을 반환 > 반환할 때, 원점 (0, 0) 에서 떨어진 거리 순으로 정렬하여 객체를 반환 ```csharp= // 중첩 루프를 사용하여 작성한 메서드 private static IEnumerable<Tuple<int, int>> ProduceIndices() { var storage = new List<Tuple<int, int>>(); for (var x = 0; x < 100; x++) for (var y = 0; y < 100; y++) if (x + y < 100) storage.Add(Tuple.Create(x, y)); storage.Sort((p1, p2) => (p2.Item1 * p2.Item1 + p2.Item2 * p2.Item2).CompareTo( p1.Item1 * p1.Item1 + p1.Item2 * p1.Item2)); return storage; } // 쿼리 구문을 사용하여 작성한 메서드 private static IEnumerable<Tuple<int, int>> ProduceIndices() { return from x in Enumerable.Range(0, 100) from y in Enumerable.Range(0, 100) where (x + y) < 100 orderby (x * x + y * y) descending select Tuple.Create(x, y); } ``` - 쿼리 구문을 사용하면 프로그램의 논리를 명령형 방식에서 선언적인 방식으로 전환할 수 있다. - 쿼리 구문을 이용하면 질의의 내용을 구성할 수 있을 뿐 아니라 개별 항목에 대해 수행하려는 작업의 수행 시기를 연기할 수 있다. - 쿼리 구문이나 메서드 호출 구문 중 어던 방식을 쓰더라도 이러한 장점은 동일하다. ```csharp= // 메서드 호출 구문을 사용하여 작성한 메서드 private static IEnumerable<Tuple<int, int>> ProduceIndices() { return Enumerable.Range(0, 100). SelectMany(x => Enumerable.Range(0, 100), (x, y) => Tuple.Create(x, y)). Where(pt => pt.Item1 + pt.Item2 < 100). OrderByDescending(pt => pt.Item1 * pt.Item1 + pt.Item2 * pt.Item2); } ``` - 쿼리 구문의 성능이 반복 코드보다 느릴 수 있지만, 이를 개선하기 위해서는 LINQ에 대한 병렬 확장도 고려해볼 수 있다. - 쿼리 구문을 사용하면 병렬 수행을 위해 복잡한 추가 작업을 할 필요 없이 `.AsParallel()` 메서드를 호출하기만 하면 된다. - 반복 구문을 작성해야 한다면 이를 쿼리 구문으로 변경할 수 있는지 다시 한 번 생각해보자. --- ### 아이템 31 : 시퀀스에 사용할 수 있는 조합 가능한 API 를 작성하라 - 반복 구문이 필요한 경우는 단일 요소를 처리하기보다 여러 요소로 구성된 시퀀스를 처리하는 알고리즘을 작성하기 위한 경우가 대부분이다. - 이를 위해 foreach, for, while 등이 주로 많이 사용된다. - 일반적으로 반복 구문을 포함하는 메서드를 작성하는 경우는 다음과 같다. - 매개 변수로 컬렉션을 받아와서 컬렉션에 포함된 요소들을 살펴본다. - 해당 컬렉션 요소들의 내용을 수정한다. - 혹은 그중 일부만 필터링해서 또 다른 컬렉션에 그 결과를 저장한 후 반환한다. - 이와 같이 작업을 수행하는 것은 효율성에 문제가 있다. - 반면 이터레이터 메서드는 컬렉션을 한번만 순회하면 되므로 메서드 내에서 시퀀스 내의 개별 요소를 저장하기 위해 별도의 저장소를 마련할 필요가 없다. - ex ) ```csharp= // 이터레이터 메서드 public static IEnumerable<int> Unique(IEnumerable<int> nums) { var uniqueVals = new HashSet<int>(); foreach (var num in nums) { if (!uniqueVals.Contains(num)) { uniqueVals.Add(num); yield return num; } } } // 지연 평가 public static void Main() { var nums = new List<int>(); foreach (var num in Unique(nums)) Console.WriteLine(num); } ``` - 이러한 지연 수행 모델 덕분에 추가 저장소를 사용하지 않을 수 있으며, 각 메서드가 구현하고 있는 알고리즘의 조합 가능성도 높아진다. - 이터레이터 메서드는 N번째 요소가 요청됐을 때 비로소 N번째 결과를 생성하기 위해 코드를 실행한다. - 이터레이터 메서드를 사용하도록 라이브러리를 개선하면 다수의 CPU 코어에 서로 다른 작업을 할당할 수 있으므로 전체적인 성능이 더욱 개선된다. - 이터레이터 메소드의 순회 과정은 내부적으로 입력 시퀀스의 현재 위치를 계속 갱신해가면서 출력 시퀀스에 차례차례 결과를 반환하는 과정이다. - 이러한 메서드를 컨티뉴어블 메서드라고 한다. - 컨티뉴어블 메서드의 장점은 다음과 같다. - 시퀀스 내의 개별 요소에 대해 지연 평가 / 수행이 가능하다. - foreach 루프를 포함하고 있는 경우라도 조합 가능한 메서드로 만들 수 있다. - 각각의 이터레이터 메서드는 매우 잘게 쪼개어 작성할 수 있고, 이를 통해 여러 단계의 알고리즘을 쉽고 효과적으로 작성할 수 있다. --- ### 아이템 32 : Action, Predicate, Function 과 순회 방식을 분리하라 - 이전 아이템에서는 개별 데이터 타입에 대해 주안을 두기보다는 `yield return` 을 이용하여 시퀀스에 대해 사용 가능한 이터레이터 메서드를 만드는 방법을 살펴봤다. - 이번 아이템에서는 모든 또는 특정 요소에 대해 수행하는 메서드에 대해서 알아본다. - 메서드 중간 어디쯤에서의 로직을 따로 분리하는 방법은 공통 부분을 메서드로 분리하여 호출하거나 해당 함수를 매개변수로 전달받는 방법이 있다. - C# 에서는 함수를 인자로 넘겨주기 위해 델리게이트와 람다를 사용할 수 있다. ```csharp= // 이터레이터 메서드 + 델리게이트 Func<T> public static IEnumerable<T> Select<T>( IEnumerable<T> sequence, Func<T, T> method) { foreach (T element in sequence) yield return method(element); } // 람다로 정의 후 사용 List<int> myInts = new List<int>(); foreach (int i in Select(myInts, value => value * value)) Console.WriteLine(i); ``` - 즉, `시퀀스를 순회하는 것` 과 `시퀀스의 개별 요소에 대한 작업을 수행하는 것` 을 구분하면 좋다. --- ### 아이템 33 : 필요한 시점에 필요한 요소를 생성하라 - 이터레이터 메서드가 시퀀스를 출력하긴 하지만 입력 매개변수로 반드시 시퀀스를 전달받아야 할 필요는 없다. - 따라서 특정 입력값을 가지고 원하는 시퀀스를 만들어 낼 수 있기도 하다. - 이러한 성질을 이용하여 시퀀스가 필요한 컬렉션을 만들 때, 이터레이터 메서드를 팩토리 메서드로서 이용할 수 있다. ```csharp= // # 1 시퀀스 반환 일반 메서드 static IList<int> CreateSequence1(int num, int startAt, int stepBy) { var collection = new List<int>(num); for (int i = 0; i < num; i++) collection.Add(startAt + i * stepBy); return collection; } // # 2 시퀀스 반환 이터레이터 메서드 static IEnumerable<int> CreateSequence2(int num, int startAt, int stepBy) { for (var i = 0; i < num; i++) yield return startAt + i * stepBy; } public static void Main() { var data1 = new List<int>(CreateSequence1(100, 0, 5)); var data2 = new List<int>(CreateSequence2(100, 0, 5)); Console.WriteLine(data1.Count); // 100 Console.WriteLine(data2.Count); // 100 } ``` - 메서드 `#1` 과 메서드 `#2` 은 언뜻보기에 비슷해보여도 큰 차이를 지닌다. - `#1` 메서드는 호출 이후 시퀀스 생성을 중단할 수 없다. - 하지만 `#2` 메서드는 이터레이터의 지연 실행 덕분에 호출 이후 시퀀스 생성을 중단하고 다른 스레드로 넘어갈 수 있다. - 즉, 시퀀스를 필요할 때 만큼 생성해둘 수 있다는 점이 장점이다. --- ### 아이템 34 : 함수를 매개변수로 사용하여 결합도를 낮추라 - 클래스 내의 메서드를 정의하기 위해서는 베이스 클래스나 인터페이스를 정의하고 이렇게 정의된 내용을 기반으로 코딩을 할 수 있다. - 이러한 방식도 대체로 유효한 접근 방법이지만 함수를 매개변수로 취하는 방식을 활용한다면 기존의 컴포넌트나 라이브러리와 함께 사용해야하는 코드를 개발할 때 상당히 큰 도움이 된다. - 함수를 매개변수로 취한다는 것은 개발자가 더 이상 구상 타입을 작성할 필요가 없으며, 오히려 추상화된 정의를 통해 종속성을 다루는 것을 의미한다. - ex ) ```csharp= static IEnumerable<T> CreateSequence<T>(int num, Func<T> generator) { for (var i = 0; i < num; i++) yield return generator(); } public static void Main() { var startAt = 0; var stepBy = 5; var data = new List<int>(CreateSequence(100, () => startAt += stepBy)); Console.WriteLine(data.Count); // 100 } ``` - 인터페이스를 정의하거나 베이스 클래스를 만들어야하는 경우라면 위와 같이 함수를 매개변수로 취하는 제네릭 메서드를구현하는 것이 대안이 될 수 잇을지 반드시 검토해 보자. - 인터페이스 대신 델리게이트를 사용하는 이유는, 델리게이트가 타입을 구성하는 핵심 구성요소가 아니기 때문이다. - 함수를 매개변수로 사용하면 알고리즘과 알고리즘을 적용할 타입을 분리하는데 도움이 된다. - 이렇게 느슨하게 구성하려면 분리된 컴포넌트를 사용할 때 발생할 수 있는 오류를 처리하기위해 추가적인 작업을 해야하고, 도구의 지원을 포기해야 할 수도 있다. - 하지만 그만큼 유연성 측면에서는 장점이 있다. --- ### 아이템 35 : 확장 메서드는 절대 오버로드하지 마라 - 확장 메서드를 사용하는 대부분의 경우가 기존에 개발된 타입을 개선하기 위해서이지, 타입의 본질적인 동작 방식을 변경하기 위한 것은 아니다. - 따라서 타입을 확장하기 위하여 확장메서드를 사용하거나 네임스페이스를 달리하여 여러 버전의 확장메서드를 정의하면 안된다. - 잘못 사용하면 메서드 충돌로 인해 유지보수 비용이 급격히 증가한다. - ex ) ```csharp= using System; using Extension1; using Extension2; namespace Extension1 { public static class Extension { public static string Format(this string name) => "Extension1 Format " + name; } } namespace Extension2 { public static class Extension { public static string Format(this string name) => "Extension2 Format " + name; } } namespace ConsoleApp1 { class Program { public static void Main() { // 컴파일 에러 Console.WriteLine("Test".Format()); } } } ``` - 다음과 같이 네임스페이스가 다르고 메서드 명이 겹칠 경우 컴파일 오류가 일어날 수 있다. - 겹치지 않는다고 해도 `using` 구문이 바뀜에 따라 결과값이 달라질 수 있는 `side effect` 가 있다 - 따라서 추가하려는 기능이 타입 내에 포함되는 것이 적절한 경우에만 확장 메서드를 사용하자. - 동일한 원형의 확장 메서드를 여러개 만들어야 할 경우, 메서드의 이름을 달리하고 정적 메서드로 작성하자. --- ### 아이템 36 : 쿼리 표현식과 메서드 호출 구문이 어떻게 대응되는지 이해해라 - LINQ 는 쿼리 언어와 그 쿼리 언어를 일련의 메서드 집합으로 변환하는 2개의 핵심 구조를 기반으로한다. - C# 컴파일러는 쿼리 언어로 작성된 쿼리 표현식을 메서드 호출 구문으로 변환해준다. - 모든 쿼리 표현식은 하나 혹은 여러 개의 메서드로 매핑된다. - 이러한 매핑 관계는 두 가지 관점으로 분리해서 생각해봐야 한다. - 클래스 사용자의 관점에서 볼 때 쿼리 표현식은 단순히 메서드 호출 구문의 다른 표현 방법일 뿐이다. - 클래스 설계자의 관점에서는 기본 프레임워크에서 제공하는 메서드들이 어떻게 구현됐는지를 살펴보고 더 나은 방법으로 구현할 수 있을지를 판단해야한다. - 더나은 구현 방법이 없다면 기본 라이브러리를 그대로 사용하면 되겠지만, 개선의 가능성이 있다면 우선 쿼리 표현식이 메서드 호출 구문으로 어떻게 변환되는지를 완벽하게 이해해야한다. - 이러한 이해를 기반으로 메서드의 원형을 올바르게 정의해서 메서드 호출 방식으로의 변환이 올바르게 수행될 수 있도록 코드를 작성해야한다. --- - 다음 쿼리 구문이 어떻게 변환되는지 살펴보자. ```csharp= var numbers = new List<int> { 1, 2, 3, 4, 5 }; var results = from n in numbers where n <= 3 select n; ``` - `from n in numbers` 은 `numbers` 의 개별 요소들을 변수 `n` 에 바인딩한다. - `where n <= 3` 은 `Where()` 메서드의 필터를 지정하는데 실제로 `Where(n => n <= 3)` 으로 변환된다. - `Where()` 는 필터 이상의 역할을 수행하지 않는다. - 입력과 출력 시퀀스는 동일한 타입이어야 하고 입력 시퀀스의 요소를 수정하지 않는다. - `select n` 은 다른 표현식의 출력 시퀀스를 즉각적으로이어받아 `Select()` 를 호출하므로 셀렉트 메서드를 호출할 필요가 없다. - 하지만 `Select()` 메서드가 다른 표현식의 출력 시퀀스를즉각적으로 이어받는 경우가 아니라면 `Select()` 메서드를 삭제할 수 없다. - ex) ```csharp= var allNumbers = from n in numbers select n; // 컴파일 -> var allNumbers = numbers.Select(n => n); ``` - `Select()` 메서드는 입력값을 다른 타입으로 변환하는 용도로 사용되는데, 입력 시퀀스 내의 개별 요소에 대하여 각기 새로운 타입의 객체를 생성하여 출력 시퀀스로 내보낸다. --- ### 느낀점 > 쿼리구문과 이터레이터 메서드의 `yield return` 부분이 시퀀스에서 > 어떻게 동작하는지 배우는 부분이 재미있었다. > `Redux` 의 미들웨어인 `Redux-saga` 에서도 `action` 의 메시지를 기반으로 > `yield` 구문을 사용해봤었는데, 동작하는 방식이 비슷해서 더 재미있었다. > 또한 람다와 제네릭 타입지정을 통한 함수의 매개변수화 등 > 점점 활용도가 높은 개념들을 배웠다. > 델리게이트를 활용하여 함수를 인자로 넘기면 함수를 구현하는 부분과 > 함수를 사용하는 부분을 나눌 수 있다고 생각이 들었고, > 나아가 디자인패턴에도 적용이 가능한 구현 방식이지 않을까 생각이 든다.