---
# System prepended metadata

title: Effective 4장 - 1

---

# 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` 구문을 사용해봤었는데, 동작하는 방식이 비슷해서 더 재미있었다.
> 또한 람다와 제네릭 타입지정을 통한 함수의 매개변수화 등
> 점점 활용도가 높은 개념들을 배웠다.
> 델리게이트를 활용하여 함수를 인자로 넘기면 함수를 구현하는 부분과
> 함수를 사용하는 부분을 나눌 수 있다고 생각이 들었고,
> 나아가 디자인패턴에도 적용이 가능한 구현 방식이지 않을까 생각이 든다.