<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발냥발</title>
    <link>https://hyeo-noo.tistory.com/</link>
    <description>파랑새는 찾지않아</description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 12:32:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Henu</managingEditor>
    <image>
      <title>개발냥발</title>
      <url>https://tistory1.daumcdn.net/tistory/3975507/attach/89f86069f1084e3cbef655340b3414fa</url>
      <link>https://hyeo-noo.tistory.com</link>
    </image>
    <item>
      <title>[LeetCode] 1448. Count Good Nodes in Binary Tree (C#)</title>
      <link>https://hyeo-noo.tistory.com/432</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/count-good-nodes-in-binary-tree/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leetcode.com/problems/count-good-nodes-in-binary-tree/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Root 노드부터 현재 노드(X)까지 이동하는 동안 자신(X)보다 큰 값을 가진 노드가 없는 경우 X는 GoodNode가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 주어진 이진 트리의 GoodNode 개수를 구하는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 풀이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 노드의 값이 지금까지 지나온 값들과 비교해 같거나 크면 GoodNode 카운팅&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715947354465&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace PS.LeetCode;

public class Solution_1448_1
{
    public int GoodNodes(TreeNode root)
    {
        var result = 0;
        Dfs(root, root.val, ref result);

        return result;
    }

    void Dfs(TreeNode cur, int largestVal, ref int goodCnt)
    {
        if (cur == null)
        {
            return;
        }

        if (cur.val &amp;gt;= largestVal)
        {
            goodCnt++;
        }

        largestVal = Math.Max(largestVal, cur.val);
        
        Dfs(cur.left, largestVal, ref goodCnt);
        Dfs(cur.right, largestVal, ref goodCnt);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/LeetCode</category>
      <category>binary tree</category>
      <category>c#</category>
      <category>DFS</category>
      <category>leetcode</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/432</guid>
      <comments>https://hyeo-noo.tistory.com/432#entry432comment</comments>
      <pubDate>Fri, 17 May 2024 21:08:31 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode] 328. Odd Even Linked List (C#)</title>
      <link>https://hyeo-noo.tistory.com/431</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/odd-even-linked-list/description/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leetcode.com/problems/odd-even-linked-list/description/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0JaUJ/btsHtQ9bzIp/pl2I7W3ZWkTtmeCWmG4it0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0JaUJ/btsHtQ9bzIp/pl2I7W3ZWkTtmeCWmG4it0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0JaUJ/btsHtQ9bzIp/pl2I7W3ZWkTtmeCWmG4it0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0JaUJ%2FbtsHtQ9bzIp%2Fpl2I7W3ZWkTtmeCWmG4it0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;187&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홀수번째 Node를 앞으로 당겨오고, 짝수번째 Node를 뒤로 밀어주는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;제한 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공간 복잡도 : O(1)&lt;/li&gt;
&lt;li&gt;시간 복잡도 : O(N)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 풀이&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 이상 Odd 노드가 없을 때까지 Odd는 Odd끼리, Even은 Even끼리 연결&lt;/li&gt;
&lt;li&gt;마지막에 Odd의 tail과 Even의 head를 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715947128204&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace PS.LeetCode;

public class Solution_328_1
{
    public class ListNode
    {
        public int val;
        public ListNode next;

        public ListNode(int val = 0, ListNode next = null)
        {
            this.val = val;
            this.next = next;
        }
    }
    
    public ListNode OddEvenList(ListNode head)
    {
        if (head == null)
        {
            return head;
        }
        
        var oddNode = head;
        var evenHead = head.next;
        
        while (oddNode.next?.next != null)
        {
            var evenNode = oddNode.next;
            oddNode.next = oddNode.next.next;
            evenNode.next = oddNode.next.next;
            oddNode = oddNode.next;
        }

        oddNode.next = evenHead;
        
        return head;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/LeetCode</category>
      <category>c#</category>
      <category>leetcode</category>
      <category>Linked list</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/431</guid>
      <comments>https://hyeo-noo.tistory.com/431#entry431comment</comments>
      <pubDate>Fri, 17 May 2024 20:59:35 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode] 2095. Delete the Middle Node of a Linked List (C#)</title>
      <link>https://hyeo-noo.tistory.com/430</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;linkedlist의 head가 주어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 linkedlist의 middle 노드를 삭제하고, 변경된 리스트의 head를 반환하는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;middle 노드의 기준은 리스트의 사이즈를 n이라고 가정하고 [ n / 2 ] 번째 노드를 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 원 포인터를 사용한 풀이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;middle 노드가 몇 번째인지 확인하는 과정이 들어간다.&lt;/li&gt;
&lt;li&gt;middle 노드 직전에서 순회를 멈추고 다다음 노드를 Next 로 바라본다.&lt;/li&gt;
&lt;li&gt;원소가 1개인 경우는 예외 케이스로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715504554257&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ListNode {
    public int val;
    public ListNode next;
    public ListNode(int val=0, ListNode next = null) 
    {
        this.val = val;
        this.next = next;
    }
}

public class Solution
{
    public ListNode DeleteMiddle(ListNode head)
    {
        if (head.next == null)
        {
            return null;
        }
        
        var current = head;
        var listLength = 0;
        while (current != null)
        {
            listLength++;
            current = current.next;
        }

        var togo = listLength / 2;
        current = head;
        while (togo-- != 1)
        {
            current = current.next;
        }
        
        current.next = current.next.next;
        
        return head;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 투포인터를 사용한 풀이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나씩 이동하는 slow 포인터와&amp;nbsp;2칸씩 이동하는 fast 포인터를 함께 사용한다.&lt;/li&gt;
&lt;li&gt;fast 포인터가 더 이상 움직이지 못하는 시점이 slow 포인터의 middle 직전 노드가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715504598299&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Solution
{
    public ListNode DeleteMiddle(ListNode head)
    {
        var slowPrev = new ListNode(-1);
        slowPrev.next = head;
        
        var slow = slowPrev;
        var fast = head;

        while (fast?.next != null)
        {
            slow = slow.next;
            fast = fast.next.next;
        }

        slow.next = slow.next.next;

        return slowPrev.next;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/LeetCode</category>
      <category>leetcode</category>
      <category>Linked list</category>
      <category>Pointer</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/430</guid>
      <comments>https://hyeo-noo.tistory.com/430#entry430comment</comments>
      <pubDate>Sun, 12 May 2024 18:16:53 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode] 238. Product of Array Except Self (C#)</title>
      <link>https://hyeo-noo.tistory.com/429</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/product-of-array-except-self/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leetcode.com/problems/product-of-array-except-self/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;nums[i] 자신을 제외한 다른 모든 원소의 곱을  answer[i] 에 입력하여 반환하는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nums 배열 원소의 prefix product 값과 suffix product 값은 32bit 정수값이 되도록 주어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;제한사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간복잡도 O(n)&lt;/li&gt;
&lt;li&gt;나눗셈 연산 사용 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 나눗셈 연산을 사용했을 때의 답안&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과가 0 이 되는 경우를 분리하여 코드를 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번에 이해하기 어려운 조건식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;list 배열을 사용하여 불필요한 Array 변환 과정이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715419826061&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Solution {
    public int[] ProductExceptSelf(int[] nums)
    {
        var productAll = 1;
        var zeroNumCnt = 0;

        foreach (var num in nums)
        {
            if (num is 0)
            {
                zeroNumCnt++;
                continue;
            }

            productAll *= num;
        }

        // except case.
        if (zeroNumCnt &amp;gt; 1)
        {
            return new int[nums.Length];
        }

        // normal case.
        var result = new List&amp;lt;int&amp;gt;();
        foreach (var num in nums)
        {
            if (num is 0)
            {
                result.Add(productAll);
            }
            else
            {
                if (zeroNumCnt == 1)
                {
                    result.Add(0);
                }
                else
                {
                    result.Add(productAll / num);
                }
            }
        }

        return result.ToArray();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 나눗셈 연산을 사용하지 않는 답안&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;i 번째 요소까지의 prefix 곱, suffix 곱을 배열로 저장하는 과정이 각각 필요함&lt;/li&gt;
&lt;li&gt;결과는 prefix[i] * suffix[i] 연산 만으로 간결하게 구할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715498347734&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Solution
{
    public int[] ProductExceptSelf(int[] nums)
    {
        var n = nums.Length;
        var prefix = new int[n];
        var suffix = new int[n];
        prefix[0] = 1;
        suffix[n - 1] = 1;

        for (var i = 1; i &amp;lt; n; i++)
        {
            prefix[i] = prefix[i - 1] * nums[i - 1];
        }
        
        for (var i = n - 2; i &amp;gt;= 0; i--)
        {
            suffix[i] = suffix[i + 1] * nums[i + 1];
        }

        var result = new int[n];
        for (var i = 0; i &amp;lt; n; i++)
        {
            result[i] = prefix[i] * suffix[i];
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 2번 답안의 확장 버전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suffix 또는 prefix 곱 중 하나만 저장하면 된다는 생각에서 착안된 풀이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;suffix 곱을 배열로 저장&lt;/li&gt;
&lt;li&gt;result[i] 를 구함과 동시에 prefix 곱을 계산&lt;/li&gt;
&lt;li&gt;2번 답안보다 더 적은 반복 횟수와 적은 메모리 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715499119693&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Solution
{
    public int[] ProductExceptSelf(int[] nums)
    {
        var n = nums.Length;
        var suffixProduct = new int[n];
        var result = new int[n];

        var sp = 1;
        for (var i = n - 1; i &amp;gt;= 0; i--)
        {
            suffixProduct[i] = sp;
            sp *= nums[i];
        }

        sp = 1;
        for (var i = 0; i &amp;lt; n; i++)
        {
            result[i] = sp * suffixProduct[i];
            sp *= nums[i];
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/LeetCode</category>
      <category>array</category>
      <category>leetcode</category>
      <category>prefix product</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/429</guid>
      <comments>https://hyeo-noo.tistory.com/429#entry429comment</comments>
      <pubDate>Sun, 12 May 2024 16:46:20 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 4. 요약 (2)</title>
      <link>https://hyeo-noo.tistory.com/428</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 40 : 동기화에는 lock()을 최우선으로 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드들은 서로 통신할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 같은 애플리케이션에 속한 스레드들은 데이터를 안전하게 주고받을 수 있는 수단이 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 무결성 오류로 인한 잠재적 문제를 피하려면 모든 공유 데이터의 상태가 일관되게 유지되고 있음을 확신할 수 있어야 하고, 그렇게 하려면 &lt;b&gt;동기화 요소(synchronization primitive)&lt;/b&gt;를 사용해서 공유 데이터에 대한 접근을 제어해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동기화 요소&lt;/b&gt;는 특정 스레드가 임계 영역 내에서 연산을 수행하는 동안 다른 스레드로부터 이를 보호하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 C# 에서는 동기화 작업을 수월하게 할 수 있도록&amp;nbsp;&lt;b&gt;lock()&lt;/b&gt;&amp;nbsp;블록을 사용해서 임계 영역을 지정하고, 동기화를 올바르게 제어할 수 있는 코드를 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;private readonly object _lock = new object();
private int _total = 0;

public void IncrementTotal()
{
    lock(_lock)
    {
        _total++;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lock&lt;/b&gt;은 특정 객체에 대한 배타적인 모니터를 획득한 후, 락을 해제하기 전까지 다른 스레드가 그 객체에 접근하지 못하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말을 풀어서 자세히 설명하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의&amp;nbsp;&lt;b&gt;lock&lt;/b&gt; 문에 의해 보호되는 코드 영역은 &lt;b&gt;한 시점에 단 하나의 스레드에 의해서만 실행&lt;/b&gt;될 수 있음을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;&lt;b&gt;배타적인&lt;/b&gt;&quot;이란 &quot;&lt;b&gt;상호 배타적인(mutually exclusive)&lt;/b&gt;&quot;의 줄임말로, 다른 스레드들이 동시에 같은 자원에 접근하는 것을 방지하는 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;C#의 lock&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#에서 &lt;b&gt;lock&lt;/b&gt; 키워드는 내부적으로 &lt;b&gt;Monitor.Enter&lt;/b&gt;와 &lt;b&gt;Monitor.Exit&lt;/b&gt; 메서드를 사용하여 작동한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드가 &lt;b&gt;lock&lt;/b&gt;으로 보호되는 영역에 진입하려고 할 때, &lt;b&gt;Monitor.Enter&lt;/b&gt; 메서드는 해당 스레드가 사용하려는 객체의 모니터(lock 객체)에 대한 소유권을 가지려고 시도한다.&lt;/li&gt;
&lt;li&gt;만약 다른 스레드가 이미 모니터를 소유하고 있다면, 이 스레드는 대기 상태로 전환되어, 모니터가 사용 가능해질 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;모니터가 사용 가능해지면, 대기 중이던 스레드 중 하나가 모니터의 소유권을 얻어 해당 코드 영역을 실행할 수 있게 된다.&lt;/li&gt;
&lt;li&gt;스레드가 &lt;b&gt;lock&lt;/b&gt; 영역을 벗어날 때 &lt;b&gt;Monitor.Exit&lt;/b&gt; 메서드가 호출되어, 모니터의 소유권을 해제하고 다른 스레드가 해당 모니터를 획득할 수 있게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 과정을 &lt;b&gt;lock&lt;/b&gt; 이라는 키워드 하나로 사용할 수 있게 해주며, 개발자가 일반적으로 저지르는 실수를 줄여주는 다양한 검증 기능도 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;lock의 검증 기능&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 소유권을 행사할 객체의 타입이 값 타입이 아니라 참조 타입인지 확인하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 참조타입인지 확인해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Monitor.Enter&lt;/b&gt; 의 원형을 보면 매개변수로 Object 타입을 취한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 만약 값 타입이 들어온다면 그 값을 &lt;b&gt;박싱한 객체를 소유&lt;/b&gt;하게 되고, 이 부분이 첫 번째 버그를 유발하는 곳이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1번 스레드가 임계 영역에 들어와 락을 얻고(값 타입이 박싱된 객체) 코드를 수행하는 중에, 2번 스레드가 임계 영역에 들어왔다고 가정하자. 그럼 2번 스레드도 락을 얻는데 성공하게 된다. 2번 스레드의 락도 값 타입을 박싱한 또다른 객체이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황이 발생하기 때문에 &lt;b&gt;Monitor.Enter&lt;/b&gt; 에 전달되는 건 반드시 참조타입이어야 하고 &lt;b&gt;lock&lt;/b&gt;은 이를 검증해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 락을 반환하는 부분에서 두번째 버그가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 스레드들 중 하나가 락을 해제하려고 하면 &lt;b&gt;SynchronizationLockException&lt;/b&gt;을 던질 것이다. &lt;b&gt;Monitor.Exit&lt;/b&gt; 메서드가 해제하려는 락 객체와 &lt;b&gt;Monitor.Enter&lt;/b&gt; 에서 얻은 락 객체가 다르기 때문이다. &lt;b&gt;Monitor.Exit&lt;/b&gt; 메서드도 값 타입을 박싱한 락 객체를 얻기 때문에 이런 결과가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 따로 호출되는 Enter와 Exit 메서드에 대해 서로 다른 락 객체를 바라보게되는 오류를 방지해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 바로 위에서 말한 두번째 버그의 방지 방법이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 꼭 &lt;b&gt;lock&lt;/b&gt; 만을 사용해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에 대한 단일 연산을 동기화 하는게 목적이라면 &lt;b&gt;System.Threading.Interlocked&lt;/b&gt; 클래스를 사용해서 동기화하는 것이 더 효율적이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public void IncrementTotal() =&amp;gt; total++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 메서드는 멀티 스레드 환경에서 데이터 일관성이 깨질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;Interlocked&lt;/b&gt; 클래스는 여러 메서드를 지원하는데, 위와 같은 수치 증가에 대한 동기화 문제를 해결하기 위해서 아래와 같이&amp;nbsp;&lt;b&gt;Interlocked.Increment()&lt;/b&gt; 메서드를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public void IncrementTotal() =&amp;gt; Interlocked.Increment(ref total);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 Interlocked 클래스의 내장 데이터 타입용 메서드는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Interlocked.Decrement()&lt;/b&gt; &amp;rarr; 값을 감소시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interlocked.Exchange()&lt;/b&gt; &amp;rarr; 값을 새로운 값으로 바꾼 후 바꾸기 전의 값을 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interlocked.CompareExchange()&lt;/b&gt; &amp;rarr; 공유 데이터의 일부를 읽어서 비교하는 값과 같은 경우에만 값을 갱신한다. 어떤 경우든 &lt;b&gt;CompareExchange()&lt;/b&gt; 메서드는 이전 값을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 41 : 락은 가능한 한 좁은 범위에 적용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에 동기화 요소를 사용하는 곳이 많으면 많을수록 &lt;b&gt;교착상태와 락 유실&lt;/b&gt; 등의 동시성 프로그래밍 문제를 피하기 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살펴봐야 할 곳이 늘어나면 문제를 놓치기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 코드를 예시로 살펴보자&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public class LockingExample
{
    public void MyMethod()
    {
        lock (this)
        {
            // ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var x = new LockingExample();
lock (x)
{
    x.MyMethod();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구성으로 작성된 lock 전략은 데드락에 빠지기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LockingExample&lt;/b&gt; 객체로 락을 획득하고, &lt;b&gt;MyMethod()&lt;/b&gt; 내부에서는 &lt;b&gt;똑같은 객체로 또 다른 락을 얻으려고 하기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A 스레드&lt;/b&gt;에서 &lt;b&gt;LockingExample&lt;/b&gt; 객체를 생성하고 lock 을 획득한 다음,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B 스레드&lt;/b&gt;에서 프로그램 내의 전혀 다른 부분에서 &lt;b&gt;LockingExample&lt;/b&gt; 객체를 생성하고 &lt;b&gt;lock&lt;/b&gt; 을 획득하는 경우 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MyMethod() &lt;/b&gt;메서드 내부에서 &lt;b&gt;this&lt;/b&gt;에 대한 &lt;b&gt;lock&lt;/b&gt;을 소유해야하는데, 이때 어디에서 락을 요청했는지 프로그램이 찾기 어렵기 때문에 데드락이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 최선의 방법은 &lt;b&gt;private Object&lt;/b&gt; 변수를 동기화 핸들로 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Item 40의 가장 처음 부분에서 보았던 코드가 최선의 방법이었던 것이다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;private readonly object _lock = new object();
private int _total = 0;

public void IncrementTotal()
{
    lock(_lock)
    {
        _total++;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 &lt;b&gt;_lock&lt;/b&gt; 필드를 &lt;b&gt;동기화 핸들&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;락을 효율적으로 사용할 수 있는 지침&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 안에서 서로 다른 값을 위해 여러 개의 동기화 핸들이 필요하다면, 그 클래스를 여러 클래스로 분할해야 한다는 강력한 신호이다.&lt;/b&gt; 즉, 접근을 통제해야 할 변수가 몇 개 있는데 각 변수에 각기 다른 락을 사용해야 한다면 해당 클래스를 서로 다른 역할을 수행하는 여러 개의 클래스로 분할해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 개의 타입을 하나의 유닛으로 볼 수 있다면 동기화를 제어하기가 훨씬 수월하다.&lt;/b&gt; 이 경우 동기화 핸들을 하나만 이용해도 공유 리소스를 보호할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 42 : 잠긴 영역에서는 외부 코드 호출을 삼가라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 사용해 보호받는 코드에서 외부 코드를 호출하는 경우, 다른 스레드가 애플리케이션을 데드락 상태에 빠뜨릴 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시 코드를 통해 외부 코드 호출을 삼가야 하는 이유를 알아보자.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class WorkerClass
{
    private readonly object _syncHandle = new object();
    private int _progressCounter = 0;

    public int Progress
    {
        get
        {
            lock (_syncHandle)
            {
                return _progressCounter;
            }
        }
    }
    
    public event EventHandler&amp;lt;EventArgs&amp;gt; RaiseProgress;

    public void DoWork()
    {
        for (var cnt = 0; cnt &amp;lt; 100; cnt++)
        {
            lock (_syncHandle)
            {
                Thread.Sleep(100);
                _progressCounter++;
                RaiseProgress?.Invoke(this, EventArgs.Empty);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;void OnRaiseProgress(object sender, EventArgs e)
{
    if (sender is WorkerClass engine)
    {
        Console.WriteLine(engine.Progress);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드를 사용하면서 문제가 없었다면 그건 그냥 여태까지 운이 좋았던 것 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제가 되는 코드인지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DoWork()&lt;/b&gt; 메서드에서 &lt;b&gt;for&lt;/b&gt; 문을 순회하며 락을 얻고, 보호된 코드에서 매번 이벤트를 트리거하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RaiseProgress?.Invoke(this, EventArgs.Empty);&lt;/b&gt; 이 코드는 이벤트 핸들러를 호출하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 핸들러가 수행되면 &lt;b&gt;Progress&lt;/b&gt; 프로퍼티에 접근하게 되는데, 이때 데드락이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러를 실행하기 위한 &lt;b&gt;for&lt;/b&gt;문 내부에서 &lt;b&gt;_syncHandler&lt;/b&gt; 를 통해 락을 얻고, 실행된 핸들러에서 &lt;b&gt;Progress&lt;/b&gt;에 접근하기 위해 똑같은 객체에 대해 락을 얻기를 시도하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 데드락 문제는 디버깅하기 매우 어려운데 그 이유를 아래 콜스택을 통해 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드가 윈도우 폼 애플리케이션 코드라고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이벤트 핸들러를 UI 스레드로 다시 마샬링 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 필요하다면&lt;b&gt; Control.Invoke()&lt;/b&gt; 를 통해서 수행해야 할 델리게이트를 &lt;b&gt;UI&lt;/b&gt; 스레드로 마샬링한다. 그리고 Control.Invoke()는 수행해야 할 델리게이트가 끝날 때까지 원본 스레드를 블로킹한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 71.9767%; height: 253px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;메서드&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;DoWork&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;백그라운드 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;RaiseProgress&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;백그라운드 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;OnUpdateProgress&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;백그라운드 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;OnRaiseProgress&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;백그라운드 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Control.Invoke&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;백그라운드 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;UpdateUI&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;메인 스레드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Progress()&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;메인 스레드(데드락)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 시나리오는 첫 번째 &lt;b&gt;lock&lt;/b&gt; 과 두 번째 &lt;b&gt;lock&lt;/b&gt; 사이에 수많은 메서드가 존재한다. 메서드가 많아질 수록 통제권 밖의 코드를 언제 실행했는지는 알기 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 예시로 보고 있는 시나리오는 문제의 원인인 외부 코드를 찾기가 비교적 수월한 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 대부분의 클래스에 숨어있는 외부 코드의 근원지인 가상메서드는 찾기가 쉽지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메서드는 파생 클래스가 재정의한 것일 수도 있고, 파생클래스가 자신의 상위 클래스에 정의된 public 혹은 protected 메서드를 모두 호출할 수도 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 식으로 외부 코드가 관여하든 그 패턴은 모두 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 클래스가 락을 얻고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 동기화된 영역 안에서 제어권 밖에 있는 코드 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황이 발생하지 않도록 사전에 차단해야하므로 lock 영역 내에서는 절대 외부 코드를 호출해서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>c#</category>
      <category>Lock</category>
      <category>more effective c#</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/428</guid>
      <comments>https://hyeo-noo.tistory.com/428#entry428comment</comments>
      <pubDate>Sun, 28 Apr 2024 22:00:47 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 4. 요약 (1)</title>
      <link>https://hyeo-noo.tistory.com/427</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 35 : PLINQ가 병렬 알고리즘을 구현하는 방법을 이해하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PLINQ 를 사용하면 멀티코어 프로그래밍에 쉽게 접근할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터 접근을 위해 언제 동기화돼야 하는지 알아야 한다.&lt;/li&gt;
&lt;li&gt;ParallelEnumerable에 선언된 병렬 버전과 순차 버전 메서드의 효과를 측정해야 한다.&lt;/li&gt;
&lt;li&gt;개별 요소들을 반드시 순차적으로 접근해야만 하는 메서드가 있다는 것을 알아야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;PLINQ를 사용하는 예시&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;150 보다 작은 모든 수에 대해 n! 을 계산하는 쿼리&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var nums = data.Where(d =&amp;gt; d &amp;lt; 150).Select(n =&amp;gt; Factorial(n));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인의 첫 번째 메서드에 &lt;b&gt;AsParallel&lt;/b&gt;()을 추가하면 병렬 연산이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var nums = data.AsParallel().Where(d =&amp;gt; d &amp;lt; 150).Select(n =&amp;gt; Factorial(n));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AsParallel&lt;/b&gt;()은 수행할 연산을 멀티 코어에서 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 반환 값은 &lt;b&gt;IParallelEnumerable&lt;/b&gt;() 이고 &lt;b&gt;Enumerable&lt;/b&gt; 클래스의 확장 메서드와 거의 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순히 &lt;b&gt;LINQ&lt;/b&gt; 패턴을 그대로 사용하는 것 만으로도 &lt;b&gt;PLINQ&lt;/b&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 위의 예시처럼 공유 데이터가 없고 결과의 순서도 중요하지 않기 때문에 쉽게 적용할 수 있다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 병렬 쿼리는 분할(partitioning)과정부터 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PLINQ는 데이터를 분할한 후, 쿼리를 실행하기 위해 생성한 태스크들에게 분할된 요소들을 분배해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분할에 너무 많은 시간을 사용해선 안된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PLINQ의 분할 알고리즘&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 범위 분할(Range Partitioning)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 범위를 태스크 수로 나누고, 나눈 집합을 각 태스크에 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 1,000 개의 값을 쿼드 코어에서 실행한다면 범위를 4분할 하여 각각 요소가 250개인 집합 4개를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 시퀀스에 몇 개의 요소가 있는지 알 수 있는 경우만 사용할 수 있고, 순서를 구분하는 인덱스를 지원해야 한다. 즉 IList&amp;lt;T&amp;gt; 인터페이스를 지원하는 시퀀스가 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 덩어리 분할(Chunk Partitioning)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션의 데이터를 작은 청크로 나눠서 각 청크를 병렬로 처리하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 작은 크기의 청크로 나누는 것부터 시작해야 하는데, 그래야 시퀀스를 하나의 태스크로 할당하는 사태를 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 줄 단위 분할(Striped Partitioning)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 스레드는 N개의 요소를 건너뛴 후 그다음 M개의 요소를 처리한다. M개를 다 처리한 후에는 다시 N개를 건너뛴다. 마치 줄무늬를 그리는 상황을 상상하면 이해하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 해시 분할(Hash Partitioning)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 분할은 Join, GroupJoin, GroupBy, Distinct, Except, Union, Intersect를 사용하는 쿼리를 위해 특별히 설계된 알고리즘이다. 이런 쿼리들은 비교적 무거운 연산이므로 특별한 분할 알고리즘을 사용하면 병렬화 성능을 크게 개선할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;태스크 병렬화 알고리즘 (분할 알고리즘과 별개)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 파이프라이닝&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라이닝은 태스크를 여러 단계로 나누고, 각 단계가 연속적으로 데이터를 처리하도록 구성하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계는 동시에 다른 데이터 항목을 처리할 수 있기 때문에 데이터 스트림 처리나 요청 처리 같은 상황에서 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 스레드가 순회 과정(foreach 블록, 쿼리 시퀀스)을 처리하고 시퀀스의 각 요소를 처리하는 쿼리는 N개의 스레드가 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 스톱앤고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스톱앤고는 태스크가 여러 단계로 나누어져 있으며, 각 단계 사이에 동기화 포인트를 두는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ToList(), ToArray() 등을 사용해 쿼리 결과를 즉시 반환할 필요가 있을 때 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스톱앤고는 병렬 처리의 이점을 제공하면서도, 단계 간의 엄격한 동기화를 유지할 필요가 있는 경우에 적합하다. 대신 많은 메모리를 사용한다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 역열거형&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 생성하지는 않고, 모든 쿼리식의 결과에 다른 액션을 취하는 경우에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 평가 쿼리에 대해서 쿼리 결과를 처리할 때 액션이 다를 수 있는데, 이때 필요할 것이 역열거형 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[ Item 36 : 예외를 염두에 두고 병렬 알고리즘을 만들라 ]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 스레드는 언제든 예외를 일으킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 스레드에서 발생하는 예외는 매우 복잡한데, 예외는 스레드의 경계를 넘어서 다른 스레드로 전달될 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 스택이 계속 이어지지 않기 때문에, 다른 스레드를 호출한 스레드는 거기서 발생한 예외를 가져오거나 예외 관련 처리를 할 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 아이템에서&lt;b&gt; 병렬 처리 중에 발생한 예외를 처리하는 대표적인 방법을 살펴보자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;외부 스레드에서 예외가 발생한 경우&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;복원할 수 있는 예외만 캐치하기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외가 백그라운드 작업을 벗어나지 못하게 하기&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private static Task&amp;lt;byte[]&amp;gt; StartDownload(string url)
{
    var tcs = new TaskCompletionSource&amp;lt;byte[]&amp;gt;();
    var wc = new WebClient();
    wc.DownloadDataCompleted += (sender, e) =&amp;gt;
    {
        if (e.UserState == tcs)
        {
            if (e.Cancelled)
            {
                tcs.TrySetCanceled();

            }
        }
        else if (e.Error != null)
        {
            if (e.Error is WebException)
            {
                tcs.TrySetResult(new byte[0]);
            }
            else
            {
                tcs.TrySetException(e.Error);
            }
        }
        else
        {
            tcs.TrySetResult(e.Result);
        }
    };
    
    wc.DownloadDataAsync(new Uri(url), tcs);
    return tcs.Task;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebException이 발생하면 0바이트를 읽었다는 사실을 결과로 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다른 예외가 발생하면 일반적인 방법으로 예외를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 병렬 작업을 수행시키는 스레드는 &lt;b&gt;AggregateException 예외에 대해서 대응하는 작업이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AggregateException : 비동기 태스크를 수행하는 스레드에서 발생한 예외들을 모아둔 클래스&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 LINQ를 사용하면 지연 연산으로 인해 쿼리의 결과를 실제로 사용하는 곳에서 평가가 이루어지기 때문에 사용하는 곳만 try-catch로 감싸는 작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 PLINQ를 사용하면 쿼리를 정의한 부분도 try-catch 블록으로 감싸야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 37 : 스레드를 생성하지 말고 스레드 풀을 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.NET의 스레드 풀은 스레드 리소스 관리를 위해 필요한 다양한 처리를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 기반 라이브러리에서 &lt;b&gt;Task.Run&lt;/b&gt;() 으로 태스크를 수행하면 이 스레드 풀을 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스레드 풀의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 스레드 풀 내부의 스레드 수는 가용 스레드의 개수를 최대화하고, 할당 후 사용되지 않는 리소스의 개수를 최소화하는 방식으로 조율된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀은 작업을 수행할 준비가 된 스레드를 재사용한다. 수동으로 생성한다면 작업마다 새로운 스레드를 만들기 때문에 스레드 생성, 폐기 비용이 스레드 풀 관리 비용보다 비싸진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암묵적 스레딩이라고도 부르며 사용한 스레드를 빠르게 작업에 할당해주는 것이 스레드 풀의 주요 목적이다. - &amp;lsquo;요청을 던지고 잊으라&amp;rsquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 스레드 풀은 태스크의 생명 주기를 자동으로 관리해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태스크가 종료되더라도 스레드는 제거되지 않으며, 다른 태스크를 처리할 수 있는 상태로 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 스레드 풀의 활성 태스크 수는 시스템이 관리한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 시스템이 리소스를 거의 다 소비한 상태라면 스레드 풀은 새로운 태스크를 시작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 시스템 리소스에 여유가 많다면 스레드 풀에서 추가 태스크를 바로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스레드 풀이 알아서 부하를 분산하기 때문에 부하 분산 로직을 직접 작성할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 38 : 스레드 간 커뮤니케이션에는 BackgroundWorker를 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BackgroundWorker&lt;/b&gt; 는 UI 작업을 돕기 위해서 &lt;b&gt;System.ComponentModel.Component&lt;/b&gt; 클래스를 기반으로 만들어진 컴포넌트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백그라운드 스레드와 포그라운드 스레드 간 커뮤니케이션(작업 완료 감지, 진행 상태 추적, 일시정지, 취소 등&lt;/b&gt;)을 위해서라면 &lt;b&gt;BackgroundWorker&lt;/b&gt; 컴포넌트를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var backgroundWorkerExample = new BackgroundWorker();
backgroundWorkerExample.DoWork += (sender, args) =&amp;gt;
{
    // 작업 내용
};
backgroundWorkerExample.RunWorkerAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BackgroundWorker&lt;/b&gt; 는 포그라운드 스레드와 백그라운드 스레드 사이의 커뮤니케이션을 위해 이벤트를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포그라운드 스레드가 요청을 보내면 &lt;b&gt;BackgroundWorker&lt;/b&gt; 가 백그라운드 스레드 상에서 &lt;b&gt;DoWork&lt;/b&gt; 이벤트를 발생시킨다. 그리고 DoWork 이벤트 핸들러가 주어진 매개변수를 읽어 작업을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 스레드가 작업을 끝내면(DoWork에 등록된 핸들러 종료), BackgroundWorker 가 포그라운드 스레드에게 &lt;b&gt;RunWorkerCompleted&lt;/b&gt; 이벤트를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 포그라운드 스레드는 백그라운드 스레드가 완료된 후의 후속 처리를 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt3Z2G/btsGMStVLLM/S50PrF6SpedfzaY7Wo3yN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt3Z2G/btsGMStVLLM/S50PrF6SpedfzaY7Wo3yN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt3Z2G/btsGMStVLLM/S50PrF6SpedfzaY7Wo3yN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt3Z2G%2FbtsGMStVLLM%2FS50PrF6SpedfzaY7Wo3yN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;480&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BackgroundWorker&lt;/b&gt; 의 이벤트 외에도, &lt;b&gt;프로퍼티&lt;/b&gt;를 이용하면 백그라운드, 포그라운드 스레드의 상호작용 방법을 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;WorkerRepostsProgress&lt;/b&gt; &lt;b&gt;프로퍼티&lt;/b&gt;는 BackgroundWorker 에게 &lt;b&gt;진행 상태를 정기적으로 포그라운드 스레드에게 보고&lt;/b&gt; 한다는 사실을 알릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJCTot/btsGPOJ8RTr/0VqpnGTe2ylKl55EiWSnt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJCTot/btsGPOJ8RTr/0VqpnGTe2ylKl55EiWSnt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJCTot/btsGPOJ8RTr/0VqpnGTe2ylKl55EiWSnt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJCTot%2FbtsGPOJ8RTr%2F0VqpnGTe2ylKl55EiWSnt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;390&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BackgroundWorker 클래스는 &lt;b&gt;현재 태스크의 취소 요청, 진행 상태 보고, 완료 보고, 오류 보고&lt;/b&gt; 등을 위한 다수의 이벤트를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포그라운드 태스크의 코드는 이러한 추가 이벤트를 요청하면 되고, 관련 이벤트 핸들러를 등록해야 한다.&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>c#</category>
      <category>more effective c#</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/427</guid>
      <comments>https://hyeo-noo.tistory.com/427#entry427comment</comments>
      <pubDate>Sun, 21 Apr 2024 22:35:49 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 3. 요약 (2)</title>
      <link>https://hyeo-noo.tistory.com/426</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 31 : 불필요한 콘텍스트 마샬링을 피하라 ]&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현재 아이템은 SynchronizationContext와 관련된 성능 문제를 다룬다.&lt;br /&gt;특히, 멀티스레딩 환경에서 특정 Context로의 데이터 전달이나 메서드 호출이 필요할 때 발생하는&lt;br /&gt;마샬링 오버헤드를 최소화하는 방법을 집중해서 다룬다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;lsquo;자유 코드&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 context에서도 실행될 수 있는 코드&lt;/li&gt;
&lt;li&gt;우리가 작성하는 대부분의 코드는 자유 코드이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;lsquo;Context 인식 코드&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 &lt;b&gt;SynchronizationContext&lt;/b&gt; 에서만 실행될 수 있는 코드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GUI 애플리케이션&lt;/b&gt;에서 UI 컨트롤과 상호작용하는 코드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹 애플리케이션&lt;/b&gt;에서 HTTPContext 등의 클래스와 상호작용하는 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;콘텍스트 마샬링이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 스레드에서 다른 스레드로 데이터를 전달하거나 메서드 호출을 위임할 때 사용되는 프로세스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 UI 작업과 같이 특정 스레드에서만 수행되어야 하는 작업을 다른 스레드에서 요청할 때 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캡처된 콘텍스트&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;await&lt;/b&gt; 코드를 실행시키는 시점의 콘텍스트&lt;br /&gt;&lt;br /&gt;&lt;b&gt;컨티뉴에이션&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;await&lt;/b&gt; 이후 진행될 코드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 수행될 때 매번 캡처된 콘텍스트에서 컨티뉴에이션이 수행된다고 해도 항상 큰 문제를 일으키지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 캡처된 콘텍스트로 매번 컨티뉴에시션을 수행하는 방식이 누적되면 복합적인 문제를 일으킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨티뉴에이션을 늘 캡처된 콘텍스트에서 실행하기 때문에, 컨티뉴에이션을 다른 스레드에 위임할 기회가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 GUI 애플리케이션에서는 UI 가 느려질 수 있고, 웹 애플리케이션에서는 시간당 처리량이 줄어들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;GUI 애플리케이션에서는 데드락이 발생할 가능성이 커지고 웹에서는 스레드 풀을 온전히 활용할 수 없게 되는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방안&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ConfigureAwait() 사용하기 :&lt;/b&gt; &lt;b&gt;await&lt;/b&gt; 키워드를 사용할 때 &lt;b&gt;ConfigureAwait(false)&lt;/b&gt;를 사용하면, 비동기 작업이 완료된 후 원래의 &lt;b&gt;SynchronizationContext&lt;/b&gt;로 돌아가지 않도록 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 작업 분리 :&lt;/b&gt; 계산 작업이나 네트워크 요청과 같은 백그라운드 작업은 가능한 한 UI 스레드와 분리하여 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public async Task&amp;lt;Config&amp;gt; ReadConfig(string uri)
{
    var result = await DownloadAsync(uri).ConfigureAwait(false); // 1.
    var items = XElement.Parse(result);
    
    // 콘텍스트 자유 코드
    // ...

    if (configUrl != null)
    {
        result = await DownloadAsync(uri).ConfigureAwait(false); // 2.
        var config = await ParseConfigAsync(result).ConfigureAwait(false); // 3.

        return config;
    }

    return new Config();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 &lt;b&gt;await&lt;/b&gt;에 도달하면 컨티뉴에이션이 기본 콘텍스트에서 실행될 것이라 생각하여, 뒤에 이어지는 비동기 호출에서는 &lt;b&gt;ConfigureAwait()&lt;/b&gt;를 다시 사용하지 않아도 될 것이라 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이건 잘못된 생각이다. 만약 첫 번째 태스크가 위 메서드처럼 동기식으로 완료된다면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기식으로 이전 메서드가 종료된다면 이후의 코드도 동기 메서드가 시작될 때 캡쳐된 콘텍스트에서 동기적으로 재개될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우엔 다음 &lt;b&gt;await 작업&lt;/b&gt;도 여전히 캡처된 콘텍스트에서 실행될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이후의 비동기 호출도 기본 컨텍스트에서 수행되지 못하고, 캡처된 콘텍스트에서 수행될 수 있는 상황이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이런 이유 때문에 컨티뉴에이션이 콘텍스트 자유 코드라면 항상 &lt;b&gt;ConfigureAwait(false)&lt;/b&gt;를 사용하여 기본 콘텍스트에서 수행되도록 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 32 : 비동기 작업은 태스크 객체를 사용해 구성하라 ]&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;태스크는 다른 리소스(주로 스레드)에 작업을 위임할 수 있도록 추상화한 개념이다.&lt;/b&gt; &lt;br /&gt;&lt;b&gt;Task 타입&lt;/b&gt;은 객체이므로 메서드와 프로퍼티를 사용해서 조작할 수 있고, 여러 태스크를 모아서 거대한 태스크로 묶을 수도 있다.&lt;br /&gt;그리고 이렇게 묶인 태스크를 병렬 또는 순서대로 실행할 수도 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Task.WhenAll()&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public async Task&amp;lt;IEnumerable&amp;lt;string&amp;gt;&amp;gt; ReadStockNameAsync(IEnumerable&amp;lt;string&amp;gt; symbols)
{
    var resultTasks = new List&amp;lt;Task&amp;lt;string&amp;gt;&amp;gt;();

    foreach (var symbol in symbols)
    {
        resultTasks.Add(ReadSymbolAsync(symbol));
    }

    // 모든 Task 동시 수행
    var results = await Task.WhenAll(resultTasks);
    return results;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WhenAll()&lt;/b&gt; 을 사용하면 독립적인 태스크들을 동시에 시작시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시작된 태스크가 모드 완료될 때까지 호출 스레드가 블로킹&lt;/b&gt;된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Task.WhenAll()&lt;/b&gt;은 완료된 (실패 태스크 포함) 모든 태스크를 배열에 저장해 반환한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Task.WhenAny()&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public async Task&amp;lt;string&amp;gt; ReadStockNameOneAsync(IEnumerable&amp;lt;string&amp;gt; symbols)
{
    var resultTasks = new List&amp;lt;Task&amp;lt;string&amp;gt;&amp;gt;();

    foreach (var symbol in symbols)
    {
        resultTasks.Add(ReadSymbolAsync(symbol));
    }

    var whenAny = await Task.WhenAny(resultTasks);
    return await whenAny;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WhenAny()&lt;/b&gt; 도 &lt;b&gt;WhenAll()&lt;/b&gt;과 동일하게 태스크를 동시에 시작시킬 수 있고 &lt;b&gt;호출 스레드가 블로킹&lt;/b&gt; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 점은 여러 태스크들 중 &lt;b&gt;가장 먼저 완료되는 태스크가 생긴하면 호출 스레드의 블로킹이 해제되고 해당 태스크만 반환&lt;/b&gt;된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WhenAll&lt;/b&gt; 과 다르게 &lt;b&gt;WhenAny&lt;/b&gt; 는 &lt;b&gt;Task&lt;/b&gt;가 한번 더 감싸져 있기 때문에 값을 가져올 때 한번 더 &lt;b&gt;await&lt;/b&gt;를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TaskCompletionSource&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Task&lt;/b&gt; 객체를 명시적으로 만들지 않으면서 비동기 메서드를 구현한 것처럼 만들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 &lt;b&gt;출발지 태스크들과 목적지 태스크들 사이에 길을 연결&lt;/b&gt;하는 데 쓰인다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class UserService
{
    private TaskCompletionSource&amp;lt;bool&amp;gt; _tcs;

    public Task&amp;lt;bool&amp;gt; WaitForUserConfirmation()
    {
        _tcs = new TaskCompletionSource&amp;lt;bool&amp;gt;();
        return _tcs.Task;
    }

    public void OnUserActionCompleted(bool isConfirmed)
    {
        _tcs.SetResult(isConfirmed);
    }
}

public class Application
{
    private readonly UserService _userService;
    
    public async Task Run()
    {
        Task&amp;lt;bool&amp;gt; confirmationTask = _userService.WaitForUserConfirmationAsync();

        // 사용자의 확인을 기다린다.
        bool confirmed = await confirmationTask;

        if (confirmed)
        {
            Console.WriteLine(&quot;사용자 확인됨.&quot;);
        }
        else
        {
            Console.WriteLine(&quot;사용자가 확인하지 않음.&quot;);
        }
    }

    public void OnUserAction(bool isConfirmed)
    {
        // 유저가 특정 버튼을 누르는 등의 확인 액션이 들어왔을 때 수행한다.
        _userService.OnUserActionCompleted(isConfirmed);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Run()&amp;nbsp;메서드&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자의 확인을 기다리기 시작한다.&lt;/li&gt;
&lt;li&gt;사용자의 확인 여부에 따라 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;사용자가 작업을 완료하면 &lt;b&gt;OnUserAction()&lt;/b&gt;이 호출되어 대기 중인 태스크에 결과를 제공한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TaskCompletionSource&amp;lt;T&amp;gt;&lt;/b&gt;를&amp;nbsp;사용하면&amp;nbsp;&lt;b&gt;태스크 완료 시점과 방법을 제어&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태스크를 완료시키는 방식은 3가지가 있는데( &lt;b&gt;SetResult , SetCanceled, SetException&lt;/b&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 사용해서 반드시 태스크를 완료 시켜주도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 태스크를 기다리는 스레드가 &lt;b&gt;무기한 블로킹&lt;/b&gt; 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 33 : 태스크 취소 프로토콜 구현을 고려하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;태스크 기반 비동기 프로그래밍 모델(TAP)&lt;/b&gt;은 진행을 취소하거나 보고하기 위한 표준 API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API는 필수는 아니지만, 비동기 작업이 진행 상황을 효과적으로 보고하거나 작업을 취소할 수 있으려면 올바르게 구현돼야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 처리를 위해 연속된 5개의 웹 요청을 서로 다른 서비스에 각기 전달하는 상황을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태스크 취소 프로토콜이 적용되기에 적절한 &amp;lsquo;&lt;b&gt;급여 지급 프로그램&lt;/b&gt;&amp;rsquo;을 예시로 들어보았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;임직원 목록과 임직원별 근무 시간을 알려주는 웹 서비스를 호출한다.&lt;/li&gt;
&lt;li&gt;세금을 계산해 신고하는 웹 서비스를 호출한다.&lt;/li&gt;
&lt;li&gt;급여명세서를 생성해서 임직원에게 이메일로 전송하는 웹 서비스를 호출한다.&lt;/li&gt;
&lt;li&gt;급여를 송금하는 웹 서비스를 호출한다.&lt;/li&gt;
&lt;li&gt;급여 지급을 마감하는 웹 서비스를 호출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이 5개의 서비스가 20% 씩의 작업을 수행한다고 가정하고, 각 단계가 끝날 때마다 프로그램의 진행 상황을 보고하기 위해 보고용 작업을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;취소 API를 구현하여 &lt;b&gt;4번째 과정(급여 송금)이 시작되기 전이라면 급여 처리를 취소&lt;/b&gt;할 수 있다. 하지만 돈이 송금된 이후에는 더 이상 취소가 불가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CancellationTokenSource&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CancellationTokenSource&lt;/b&gt;는 취소 요청을 발생시킬 수 있는 메커니즘을 제공하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CancellationToken&lt;/b&gt;은 취소 요청을 받아들이는 방법을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CancellationTokenSource 를 사용하는 메커니즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;CancellationTokenSource 생성 :&lt;/b&gt; 취소 가능한 태스크를 시작하기 전에 &lt;b&gt;CancellationTokenSource&lt;/b&gt; 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CancellationToken 전달 : &lt;/b&gt;생성된 &lt;b&gt;CancellationToken 객체를 비동기 메서드에 전달&lt;/b&gt;하여, 해당 메서드 내에서 취소 요청을 확인할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;취소 요청 감지 :&lt;/b&gt; 비동기 메서드 내에서는 &lt;b&gt;CancellationToken&lt;/b&gt;의 &lt;b&gt;ThrowIfCancellationRequested&lt;/b&gt; 메서드를 주기적으로 호출하여 취소 요청이 있는지 확인한다. 취소 요청이 감지되면 &lt;b&gt;OperationCanceledException&lt;/b&gt;이 발생하여 태스크가 취소된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;취소 요청 처리 :&lt;/b&gt; 비동기 메서드는 취소 요청을 적절히 처리하여 리소스를 정리하고, 필요한 경우 사용자에게 취소되었음을 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CancellationTokenSource의 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 및 동기 작업의 개수에 제한없이 동일한 취소 토큰을 전달할 수 있다.&lt;/li&gt;
&lt;li&gt;취소를 요청할 수 있는지 여부 및 적용되는 시기 등을 완전히 제어할 수 있다.&lt;/li&gt;
&lt;li&gt;API를 사용하는 코드는 취소 요청이 전파되는 비동기 호출을 선택적으로 결정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public async Task RunPayRollAsync(CancellationToken ct, IProgress&amp;lt;int&amp;gt; progress)
{
    progress?.Report(0);
    
    // Step 1: 근무 시간과 급여 계산
    ...
    ct.ThrowIfCancellationRequested();
    progress?.Report(20);
    
    // Step 2: 세금 계산 및 신고
    ...
    ct.ThrowIfCancellationRequested();
    progress?.Report(40);
    
    // Step 3: 급여명세서 생성 및 이메일 전송
    ...
    ct.ThrowIfCancellationRequested();
    progress?.Report(60);
    
    // Step 4: 급여 송금
    ...
    progress?.Report(80);
    
    // Step 5: 급여 지급 기간 마감
    ...
    progress?.Report(100);
    ct.ThrowIfCancellationRequested();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RunPayRollAsync&lt;/b&gt; 메서드 내부에는 &lt;b&gt;CancellationToken&lt;/b&gt; 에 취소 요청이 들어온 경우 예외를 발생시키는 코드가 적절한 위치에 작성되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 호출자 코드
var cts = new CancellationTokenSource();
var progress = new Progress&amp;lt;int&amp;gt;();

await RunPayRollAsync(cts.Token, progress);

if (cancelRequested)
{
    cts.Cancel();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출자는 &lt;b&gt;CancellationTokenSource&lt;/b&gt;를 사용해서 취소를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TaskCompletionSource&lt;/b&gt; 처럼 이 클래스는 &lt;b&gt;취소를 요청하는 코드와 취소할 작업이 있는 코드 사이를 중개&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 34 : 비동기 메서드의 반환값을 캐시하는 경우 ValueTask&amp;lt;T&amp;gt;를 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 비동기 코드의 반환 타입은 &lt;b&gt;Task&lt;/b&gt; 또는 &lt;b&gt;Task&amp;lt;T&amp;gt;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 때에 따라서 &lt;b&gt;Task&lt;/b&gt; 타입 때문에 성능이 떨어지기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 비동기 호출을 빽빽한 for 루프나 자주 호출되는 코드에서 사용하는 경우가 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드용으로 &lt;b&gt;Task&lt;/b&gt; 인스턴스를 생성하고 사용하는 비용이 부담될 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 .NET은 &lt;b&gt;ValueTask&amp;lt;T&amp;gt;&lt;/b&gt; 라는 새로운 타입을 제공하는데 이는 기존 &lt;b&gt;Task&lt;/b&gt; 보다 효율이 더 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 타입은 값 타입이어서 추가로 메모리를 할당할 필요가 없다(회수 비용을 줄여준다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;ValueTask&amp;lt;T&amp;gt;&lt;/b&gt; 타입은 비동기 메서드의 결과를 캐싱해놓고 나중에 사용할 경우 최고의 효율을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class CacheExample
{
    private Dictionary&amp;lt;string, string&amp;gt; cache = new Dictionary&amp;lt;string, string&amp;gt;();

    public CacheExample()
    {
        // 캐시 초기화 예시
        cache[&quot;key1&quot;] = &quot;value1&quot;;
        cache[&quot;key2&quot;] = &quot;value2&quot;;
    }

    // 캐시에서 데이터를 가져오는 메서드
    public ValueTask&amp;lt;string&amp;gt; GetDataAsync(string key)
    {
        // 캐시에 데이터가 존재하는 경우, 즉시 ValueTask로 결과를 반환
        if (cache.ContainsKey(key))
        {
            return new ValueTask&amp;lt;string&amp;gt;(cache[key]);
        }
        // 캐시에 데이터가 없는 경우, 비동기 작업으로 데이터를 로드
        else
        {
            return new ValueTask&amp;lt;string&amp;gt;(LoadDataAsync(key));
        }
    }

    // 데이터를 비동기적으로 로드하는 메서드
    private async Task&amp;lt;string&amp;gt; LoadDataAsync(string key)
    {
        await Task.Delay(100); // 데이터 로드를 위한 대기
        string loadedData = $&quot;loaded_{key}&quot;;
        cache[key] = loadedData; // 로드된 데이터를 캐시에 추가
        return loadedData;
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        var cacheExample = new CacheExample();

        // 캐시에 존재하는 데이터 요청
        string value1 = await cacheExample.GetDataAsync(&quot;key1&quot;);
        Console.WriteLine(value1); // &quot;value1&quot;

        // 캐시에 존재하지 않는 데이터 요청, 데이터 로드 과정 거침
        string value2 = await cacheExample.GetDataAsync(&quot;key3&quot;);
        Console.WriteLine(value2); // &quot;loaded_key3&quot;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;GetDataAsync&lt;/b&gt; 메서드는 비동기 메서드가 아니고, 대신 &lt;b&gt;ValueTask&lt;/b&gt;를 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ValueTask&lt;/b&gt;는 &lt;b&gt;Task&lt;/b&gt;를 인수로 받는 생성자를 제공하고 이를 통해 &lt;b&gt;await&lt;/b&gt; 작업을 내부적으로 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ValueTask&lt;/b&gt; 타입은 태스크 객체를 생성하기 위한 작업이 성능의 병목으로 밝혀졌을 때 사용할 수 있는 최적화 수단이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 비동기 메서드에는 여전히 &lt;b&gt;Task&lt;/b&gt; 타입이 가장 적절하고, 메모리 할당이 성능의 병목이 되지 않는다면 &lt;b&gt;Task&lt;/b&gt;와 &lt;b&gt;Task&amp;lt;T&amp;gt;&lt;/b&gt;를 사용하는 것이 좋다.&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>async</category>
      <category>await</category>
      <category>c#</category>
      <category>more effective c#</category>
      <category>비동기</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/426</guid>
      <comments>https://hyeo-noo.tistory.com/426#entry426comment</comments>
      <pubDate>Sun, 7 Apr 2024 22:50:45 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 3. 요약 (1)</title>
      <link>https://hyeo-noo.tistory.com/425</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 27 : 비동기 작업에는 비동기 메서드를 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 메서드에서는 코드들이 작성한 순서대로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 비동기 메서드에서는 꼭 그렇지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드는 내부 코드를 모두 수행하기 전에 미리 반환될 수 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 요청한 비동기 작업이 완료되는 시점에 맞추어 수행을 중단했던 지점부터 다시 수행을 이어간다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;private async Task SomeMethodAsync()
{
    Console.WriteLine($&quot;Entering {nameof(SomeMethodAsync)}&quot;);
    var awaitable = SomeMethodReturningTask();

    Console.WriteLine($&quot;In {nameof(SomeMethodAsync)}, before the await&quot;);
    var result = await awaitable;
    Console.WriteLine($&quot;In {nameof(SomeMethodAsync)}, after the await&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SomeMethodReturningTask()&lt;/b&gt;을 수행하는 부분을 만나면 해당 메서드가 실행되고, 결과 값을 받을 수 있는 Task 객체를 awaitable 변수에 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. await 명령어에 도달했을 때 비동기 메서드가 완료된 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자연스럽게 결과 값이 &lt;b&gt;result&lt;/b&gt;에 할당되고 동기식으로 나머지가 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. await 명령어에 도달했을 때 비동기 메서드가 완료되지 않은 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선 메서드에서 빠져나오고, 다른 작업을 이어간다. &lt;br /&gt;이후 &lt;b&gt;Task&lt;/b&gt;가 완료되면 &lt;b&gt;await&lt;/b&gt; 쪽에 작업이 완료되었다는 신호를 보내고 중단된 지점부터 다시 코드가 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드 호출 시 &lt;b&gt;이전에 로직을 수행하던 환경을 되돌리는 작업&lt;/b&gt;이 중요한데, 이와 관련된 일련의 과정은 &lt;b&gt;SynchronizationContext&lt;/b&gt; 클래스가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 대기 중이던 &lt;b&gt;Task&lt;/b&gt;가 완료되어 비동기 메서드 내에서 코드 수행을 재개할 때, 이전 수행 환경과 컨텍스트를 복원하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, GUI 애플리케이션의 경우 &lt;b&gt;Dispatcher&lt;/b&gt;를 이용하여 Task를 스케줄링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션의 경우 &lt;b&gt;QueueUserWorkItem&lt;/b&gt;을 사용해 스케줄링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 대기 중인 비동기 태스크에서 예외가 발생하면 &lt;b&gt;SynchronizationContext&lt;/b&gt;로 예외를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 비동기 메서드를 사용하는데 &lt;b&gt;await&lt;/b&gt; 를 사용하지 않아서 되돌아갈 곳이 없다면 &lt;b&gt;SynchronizationContext&lt;/b&gt; 의 예외를 받을 수 없게 된다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public void ExceptionTest()
{
    try
    {
        // _ = at.SomeMethodAsync(); // 이 경우엔 내부에서 발생한 예외를 catch 할 수 없다.

        await at.SomeMethodAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 &lt;b&gt;Task&lt;/b&gt;는 대기(&lt;b&gt;await&lt;/b&gt;)하는 것이 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 28 : async void 메서드는 절대 작성하지 말라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async void&lt;/b&gt; 로 메서드를 작성하면 이 메서드가 던지는 예외를 호출 측에서 잡지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Item 27&lt;/b&gt;에서 봤듯이 &lt;b&gt;비동기 메서드는 Task 객체를 통해 예외를 보고한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드가 작업을 수행하다 예외를 던져서 &lt;b&gt;Task&lt;/b&gt; 객체가 오류 상태가 되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;awiat&lt;/b&gt;를 호출한 코드가 다시 스케줄링 될 때 예외가 발생하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async void 의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;async void&lt;/b&gt;로 선언된 메서드인 경우 Task 객체를 전달하지 않기 때문에 호출자에게 예외를 전파할 수 있는 방법이 없다.&lt;br /&gt;&lt;b&gt;async void&lt;/b&gt; 메서드 &amp;rarr; &lt;b&gt;Fire And Forgot&lt;/b&gt;(실행하면 잊어라)의 성격을 띈다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;async void&lt;/b&gt; 메서드 실행 중 예외가 발생하면 해당 &lt;b&gt;SynchronizationContext&lt;/b&gt; 에서 수행 중인 스레드를 중단해 버린다. &lt;br /&gt;이때 사용자는 어떤 통지도 받지 못하고, 어떤 예외 처리기도 수행하지 못하고 스레드가 감쪽같이 사라지는 걸 볼 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public async void SetSessionState()
{
    var config = await ReadConfigFromNetwork();
    CurrentUser = config.User;
}

public void Test()
{
    var manager = new SessionManager();
    manager.SetSessionState();

    await Task.Delay(1000);
    Assert.Equal(t.User, &quot;User&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SetSessionState()&lt;/b&gt; 를 테스트 하기 위한 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트 코드는 &lt;b&gt;await Task.Delay(1000)&lt;/b&gt; 라인 때문에 안좋은 코드가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 delay 직전의 &lt;b&gt;t.SetSessionState()&lt;/b&gt; 수행 시 비동기 메서드가 언제 끝날 지 모르기 때문에 1초가 충분할 수도, 그렇지 않을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;async void&lt;/b&gt; 메서드가 언제 끝날 지 모르는 크리티컬한 문제 때문에 테스트 결과가 항상 바뀔 수 있는 코드이고 비동기를 제대로 이해하지 못한 테스트 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async void&lt;/b&gt; 를 사용하면 안되는 이유를 지금까지 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 &lt;b&gt;async Task&lt;/b&gt; 메서드를 생성해야 한다고 알게 되었을 텐데, &lt;b&gt;그럼에도 async void 가 허용되는 이유는 뭘까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;b&gt;async, await&lt;/b&gt; 가 도입되기 전에 규칙이 확립된 &amp;lsquo;&lt;b&gt;이벤트 핸들러&lt;/b&gt;&amp;rsquo;에서 사용해야하기 때문이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이벤트 핸들러에 비동기로 접근해야하는 상황이 있는건가?&lt;/li&gt;
&lt;li&gt;이벤트 핸들러는 일반적으로 사용자가 호출하는 코드가 아닌데, 호출측으로 반환된 &lt;b&gt;Task&lt;/b&gt;로 무엇을 해야하는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 이벤트가 async, await를 고려하지 못하고 만들어졌기 때문에 async void 이벤트 핸들러가 작성될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 비동기 이벤트 핸들러를 가능한 안전하게 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;private async void OnCommand(object sender, RoutedEventArgs e)
{
    ...

    try
    {
        // 주요 로직 실행
    }
    catch (Exception e)
    {
        // 예외 기록, throw 하지 않음
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 단순히 모든 예외를 기록하고 이전과 같이 실행을 이어가도 문제가 없을 것이라 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 경우에 이렇게 처리하더라도 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 만약 걷잡을 수 없는 수준의 문제가 발생하여 예외가 발생하여 시스템으로 전달해야 한다면? 그리고 특정 예외는 복구할 수 있다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는&amp;nbsp;&lt;b&gt;FileNotFoundException&lt;/b&gt;은 복구할 수 있지만, 그 외의 예외에 대해서는 대처할 수 없는 상황에 사용할 수 있는 코드이다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public static async void FireAndForgot&amp;lt;TException&amp;gt;(
    this Task task,
    Action&amp;lt;TException&amp;gt; recovery,
    Func&amp;lt;Exception, bool&amp;gt; onError) where TException : Exception
{
    try
    {
        await task;
    }
    catch (Exception ex) when (onError(ex))
    {
        
    }
    catch (TException ex2)
    {
        recovery(ex2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;b&gt;Task&lt;/b&gt;에 대한 &lt;b&gt;확장 메서드&lt;/b&gt;를 작성해서 &lt;b&gt;async void&lt;/b&gt; 에 대해 일반적이고 재사용 가능한 예외처리를 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 29 : 동기, 비동기 메서드를 함께 사용해서는 안 된다 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드를 사용한다는 것은 작업이 오래 걸릴 수 있으니 호출자 스레드는 그동안 다른 유용한 일을 하라는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 메서드는 작업을 수행하는 시간에 상관없이 호출자와 같은 리소스를 사용해서 모든 작업을 완료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 호출자는 동기 작업이 끝날 때까지 멈춰 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 작업이 완료될 때까지 기다리는 동기 메서드를 만들지 말라.&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비동기 코드를 감싼 동기 코드가 문제를 일으키는 원인 3가지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 서로 다른 예외 처리 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;/b&gt;비동기 태스크에서 다양한 예외가 발생할 수 있기 때문에 &lt;b&gt;Task&lt;/b&gt; 클래스 내에는 여러 예외를 담기 위한 리스트를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 예외가 발생한 비동기 작업이 종료되어 &lt;b&gt;await&lt;/b&gt; 명령이 호출되는 시점에 리스트에 예외들이 담겨있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실패한 태스크에 대해 &lt;b&gt;Task.Wait()&lt;/b&gt; 을 호출하거나 &lt;b&gt;Task.Result&lt;/b&gt; 를 읽으려 하면 모든 예외를 담은 &lt;b&gt;AggregateException&lt;/b&gt; 이 던져진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 &lt;b&gt;AggregateException&lt;/b&gt;을 캐치하여 리스트의 예외들을 꺼내봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public async Task&amp;lt;int&amp;gt; ComputeUsageAsync()
{
    try
    {
        var a = await GetAAsync();
        var b = await GetBAsync();
        return a + b;
    }
    catch (KeyNotFoundException e)
    {
        return 0;
    }
}

public int ComputeUsage()
{
    try
    {
        var a = GetAAsync().Result;
        var b = GetBAsync().Result;
        return a + b;
    }
    catch (AggregateException e) when (e.InnerExceptions.FirstOrDefault()?.GetType() 
    == typeof(KeyNotFoundException))
    {
        return 0;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 메서드를 비교해서 보면 왜 비동기 메서드의 결과를 &lt;b&gt;.Result&lt;/b&gt;로 가져오면 안되는 지 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.Wait()&lt;/b&gt;를 사용하면 안되는 이유도 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 잠재적 데드락 위험성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public async Task SimulateWorkAsync()
{
    await Task.Delay(1000);
}

public void SyncOverAsyncDeadlock()
{
    var delayTask = SimulateWorkAsync();
    delayTask.Wait();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 일반적인 콘솔 애플리케이션에서는 문제없이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔의 &lt;b&gt;SynchronizationContext&lt;/b&gt;는 스레드 풀에서 여러 스레드를 가져와 사용하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 GUI나 웹 프레임워크에서는 &lt;b&gt;SynchronizationContext&lt;/b&gt; 스레드를 하나만 가지기 때문에 이런 상황에서 &lt;b&gt;데드락에 걸리게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 WPF 같은 GUI 프레임 워크는 &lt;b&gt;SynchronizationContext&lt;/b&gt;로 &lt;b&gt;메인 스레드(== UI 스레드)&lt;/b&gt;만을 사용하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락이 걸리는 과정을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 만약 &lt;b&gt;SyncOverAsyncDeadlock&lt;/b&gt;&amp;nbsp;메서드가&lt;b&gt; UI 스레드에&lt;/b&gt;서 호출되면, &lt;b&gt;UI 스레드&lt;/b&gt;는&amp;nbsp;&lt;b&gt;SimulateWorkAsync&lt;/b&gt;의 완료를 동기적으로 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 그리고&lt;b&gt; SimulateWorkAsync&lt;/b&gt;&amp;nbsp;내부에서 &lt;b&gt;Task.Delay&lt;/b&gt;를 &lt;b&gt;await&lt;/b&gt; 하고 있기 때문에 &lt;b&gt;Task.Delay&lt;/b&gt; 이후의 작업을 UI 스레드에서 실행하려고 스레드를 기다리게 된다. (&lt;b&gt;SimulateWorkAsync &lt;/b&gt;를 UI 스레드가 실행했기 때문이다)&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&lt;b&gt; Wait()&lt;/b&gt; 를 사용해서 &lt;b&gt;SimulateWorkAsync&lt;/b&gt;의 완료를 기다리고 있기 때문에 &lt;b&gt;await&lt;/b&gt;의 완료 이후 연속된 작업을 UI 스레드에서 실행할 수 없다. UI 스레드가 이미 Wait 호출로 인해 차단되어 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 결과적으로, &lt;b&gt;await&lt;/b&gt;는 UI 스레드로 돌아가려고 하지만 UI 스레드는 이미 대기 상태이므로 &lt;b&gt;await&lt;/b&gt;는 계속 대기 상태에 머물게 되고, UI 스레드도 &lt;b&gt;Task&lt;/b&gt;의 완료를 기다리며 차단된 상태로 남아 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Wait(), Result&lt;/b&gt; 를 사용하지 않고, 비동기 메서드를 &lt;b&gt;await&lt;/b&gt; 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SyncOverAsyncDeadlock&lt;/b&gt;를 &lt;b&gt;async&lt;/b&gt;로 만들고 &lt;b&gt;await delayTask&lt;/b&gt; 를 통해 호출하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 리소스 낭비&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업을 동기적으로 대기하게 만들면, 스레드가 블로킹되어 리소스가 낭비된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 GUI 앱에서 UI 스레드가 블로킹되면 앱이 뚝뚝 끊기고 멈추는 현상이 자주 발생하고, 웹 애플리케이션에서는 동시에 많은 요청을 처리해야 하는데 성능이 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수행 시간이 오래 걸리는 CPU 중심 작업을 비동기로 수행하지 말라.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 프로그래밍의 핵심 목적은&lt;b&gt; I/O 작업(파일 접근, 네트워크 요청)처럼 대기 시간이 긴 작업을 최적화&lt;/b&gt; 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 작업은 실제로 CPU가 아무것도 하지 않고 대기하는 상황을 만들기 때문에 반드시 비동기 프로그래밍이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;CPU Bound 작업(복잡한 계산, 데이터 처리)&lt;/b&gt;은 CPU 작업을 집중적으로 사용하기 때문에 굳이 비동기로 처리하면 오히려 여러 문제를 일으킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 리소스 낭비&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 작업을 비동기로 처리하면, 해당 작업을 실행하는 동안에도 CPU는 계속해서 활성화 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업은 대기 상태에 들어간 동안 스레드를 반환하여 다른 작업에 사용할 수 있게 하지만, CPU 중심 작업에서는 CPU가 계속 일하고 있기 때문에 이러한 비동기의 이점을 활용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 성능 저하&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드는 작업을 백그라운드 스레드로 옮기고, 완료될 때까지 기다리는 오버헤드를 일으킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 중심 작업의 경우 이런 컨텍스트 스위칭같은 오버헤드가 오히려 성능을 저하시킬 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 작업은 가능한 한 연속적으로 처리되어야 최적의 성능을 발휘할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 메서드를 이용해 작업을 분리하다 보면 그 여파가 애플리케이션 전반으로 퍼지게 된다. &lt;br /&gt;비동기 API를 하나 추가하면 그 메서드를 활용해야 하는 다른 메서드들도 비동기가 될 것이기 때문이다. &lt;br /&gt;이건 올바른 현상이며 콜스택에 비동기 메서드의 비중이 점점 늘어나게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 30 : 비동기 메서드를 사용해서 스레드 생성과 콘텍스트 전환을 피하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 비동기 작업이 서로 다른 스레드에서 수행된다고 착각하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그럻게 수행될 수 있지만, 비동기 작업이라도 새로운 스레드를 생성하지 않는 경우도 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 입출력은 비동기이지만 스레드가 아닌 I/O 완료 포트를 사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹 요청도 비동기이지만 스레드가 아닌 네트워크 인터럽트를 사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 태스크를 비동기로 수행하면 스레드가 필요하지 않으므로, 스레드를 다른 작업에 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 스레드에 작업을 위임한다는 것은, 작업을 요청한 스레드를 자유롭게 해주기 위해서 다른 스레드를 생성하는 것이다. (자유를 위해 스레드 생성 비용 지불)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 작업과 스레드 생성&lt;/b&gt;에 관해서 2개의 관점에서 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. GUI 애플리케이션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업 시 새 스레드를 생성하는 작업은 GUI 애플리케이션에서 &lt;b&gt;UI 스레드&lt;/b&gt;를 위해 많이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI 애플리케이션의 사용자는 UI를 통해 수행한 작업이 완료되기 전이라도, 여전히 다른 UI와의 상호작용이 부드럽게 이어지길 원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 UI 스레드가 사용자의 요청 하나를 처리하기 위해 수 초 이상 걸린다면 사용자는 마치 고장난 것 같은 화면을 보게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 작업을 위임받아 대신 수행해줄 다른 리소스(워커 스레드)가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 UI 스레드는 오래 걸리는 작업을 워커 스레드로 넘겨줄 수 있고, UI는 사용자의 다른 입력에 즉각적으로 반응할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 서버 애플리케이션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 커다란 CPU Bound 작업이 들어왔다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태스크를 처리하기 위해 다른 스레드를 생성해 스레드 풀에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 태스크를 호출한 스레드는 할 일이 없으므로 스레드 풀로 수거되어 다른 일을 할당받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 때문에 오버헤드가 더 커지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 작업을 수행하던 스레드를 스레드 풀로 수거하려면 &lt;b&gt;SynchronizationContext&lt;/b&gt; 에 웹 요청에 대한 상태를 저장해두어야 하고, 이후 태스크가 완료되면 저장된 상태를 복원해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;별다른 리소스 절약도 없고 2번의 컨텍스트 전환만 초래&lt;/b&gt;하게 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;b&gt;CPU Bound 작업을 장시간 수행해야 한다면, 해당 작업을 다른 프로세스 잡이나 다른 머신에서 수행하도록 하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>async</category>
      <category>await</category>
      <category>c#</category>
      <category>more effective c#</category>
      <category>task</category>
      <category>비동기 프로그래밍</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/425</guid>
      <comments>https://hyeo-noo.tistory.com/425#entry425comment</comments>
      <pubDate>Sun, 31 Mar 2024 21:52:21 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 2. 요약 (2)</title>
      <link>https://hyeo-noo.tistory.com/424</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 19 : 베이스 클래스에 정의된 메서드를 오버로드해서는 안된다 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 클래스에서 정의된 메서드를 파생 클래스에서 오버로드하면 어떤 메서드가 호출될지 정확히 이해하기가 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 명확한 메서드 이름을 사용한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 명확하게 설명하는 이름을 지어주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 오버라이딩을 통해 기능을 확장한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 메서드의 기능을 수정하거나 확장해야 할 때는, 오버라이딩을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 메서드 오버로딩 최소화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 클래스의 메서드와 동일한 이름을 가진 새로운 메서드를 추가할 필요가 있을 때는, 혼란을 피하기 위해 가능한 한 메서드 오버로딩을 피하고 다른 이름을 지어주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 20 : 이벤트가 런타임 시 객체 간의 결합도를 증가시킨다는 것을 이해하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반 API에는 결합도를 증가시킬 수 있는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 이벤트의 매개변수 타입이 진행 상태를 포함하는 경우를 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class WorkerEngine
{
    public event EventHandler&amp;lt;WorkerEventArgs&amp;gt; OnProgress; 
    public void DoLotsOfStuff()
    {
        for (int i = 0; i &amp;lt; 100; i++)
        {
            SomeWork();
            var args = new WorkerEventArgs();
            args.Percent = i;
            OnProgress?.Invoke(this, args);
            if (args.Cancel)
            {
                return;
            }
        }
    }
}

public class WorkerEventArgs : EventArgs
{
    public int Percent { get; set; }
    public bool Cancel { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 &lt;b&gt;OnProgress&lt;/b&gt; 이벤트를 구독하는 다수의 이벤트 핸들러를 하나로 묶어버리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 첫 번째 이벤트 핸들러가 취소 요청을 하고, 다음 이벤트 핸들러는 이벤트 값을 바꾸려고 할 때, 코드가 꼬여서 생각한대로 동작하지 않을 가능성이 높아지기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 여러 개의 이벤트 핸들러가 이벤트에 결합되어 있고 이벤트의 매개변수가 변경가능하다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 수행되는 이벤트 핸들러는 이전에 수행된 변경사항을 모두 가지고 실행되어 버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class WorkerEventArgs : EventArgs
{
    public int Percent { get; set; }
    public bool Cancel { get; private set; }

    public void RequestCancel()
    {
        Cancel = true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cancel&lt;/b&gt;의 접근 제한자를 변경해서 &lt;b&gt;Cancel&lt;/b&gt; 값이 &lt;b&gt;true&lt;/b&gt;가 된 이후에는 어떤 이벤트 핸들러도 이 값을 수정할 수 없도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 늘 이렇게 코드를 수정할 수 있는게 아니기 때문에 2가지 방법을 추가로 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스를 하나 정의해서 상속받은 후 그 인터페이스의 메서드를 호출해주는 방법&lt;/li&gt;
&lt;li&gt;이벤트를 수신하기 위한 델리게이트를 정의하고 이 타입의 객체를 취해서 호출해주는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;b&gt;다중 이벤트 핸들러 지원 여부&lt;/b&gt;와 &lt;b&gt;Cancel&lt;/b&gt; 값을 어떻게 처리할지 등을 결정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결합도 증가로 인해 이벤트 해제의 중요성도 같이 증가한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 메모리 누수의 위험&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자가 이벤트 생성자에 대한 참조를 유지하고 있기 때문에, 생성자의 생명 주기가 끝나더라도 가비지 컬렉터(GC)에 의해 회수되지 않을 수 있다. 즉, 구독자가 계속해서 생성자를 참조하고 있으면, 생성자는 메모리에서 해제되지 않아 메모리 누수가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 의도되지 않은 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트가 핸들러가 제거되지 않았다면, 생성자는 여전히 이벤트를 발생시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 이벤트가 발생할 때마다 구독자의 핸들러를 호출할 것인데, 이러한 동작은 이벤트 구독자가 삭제된 후에도 계속되어서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이벤트 구독자가 더 이상 이벤트를 수신할 필요가 없을 때(구독자를 삭제할 때), 반드시 이벤트 핸들러의 연결을 끊은 후에 삭제되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 21 : 이벤트는 가상으로 선언하지 말라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 이벤트는 프로퍼티 이벤트와 동일하게, &lt;b&gt;내부적으로 이벤트 핸들러를 보관하는&lt;/b&gt; &lt;b&gt;private 필드를 생성&lt;/b&gt;하고 &lt;b&gt;add(), remove()&lt;/b&gt; 같은 함수도 생성해준다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public abstract class WorkerEngineBase
{
    public virtual event EventHandler&amp;lt;WorkerEventArgs&amp;gt; OnProgress; 
}

public class WorkerEngineDerived : WorkerEngineBase
{
    public override event EventHandler&amp;lt;WorkerEventArgs&amp;gt; OnProgress;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오버라이딩 된 파생클래스의 이벤트에 핸들러를 추가하면 베이스 클래스에는 핸들러가 추가되지 않고, 파생 클래스의 핸들러 저장 필드에 핸들러가 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 베이스 클래스에서 이벤트를 발생 시키더라도 아무런 일이 일어나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 아래와 같이 파생 클래스에 &lt;b&gt;add()&lt;/b&gt;와&lt;b&gt; remove()&lt;/b&gt; 접근자를 명시적으로 작성할 수 있는데,&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public override event EventHandler&amp;lt;WorkerEventArgs&amp;gt; OnProgress
    {
        add =&amp;gt; base.OnProgress += value;
        remove =&amp;gt; base.OnProgress -= value;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러다 보면 오히려 클래스간의 결합도가 높아지고 가상 이벤트는 사용하지 않아야 함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 22 : 명확하고 간결하며 완결된 메서드 그룹을 생성하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 의미가 명확한 이름 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드와 그 매개변수의 이름은 수행하는 작업과 기대하는 결과를 명확히 가지고 있어야 한다. 이름만 보고도 메서드의 기능을 유추할 수 있어야 한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 일관된 용어 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 API 내에서 동일한 개념에 대해 일관된 용어를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 메서드 오버로드 최소화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 가능한 오버로드들 중 컴파일러가 어느 것을 선택할지를 사용자가 직관적으로 알 수 있어야 한다. 오버로드 메서드가 추가될 수록 유용해지기보다 복잡성만 커지기 때문에 오버로드는 최소화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 23 : 생성자, 변경자, 이벤트 핸들러를 위해 partial 클래스와 메서드를 제공하라 ]&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partial 클래스와 메서드는 코드의 관리성과 확장성을 높여주는 강력한 기능이다. &lt;br /&gt;특히 자동 생성된 코드와 개발자가 작성하는 코드를 분리하고자 할 때 유용하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Partial 클래스의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 클래스나 복잡한 클래스를 여러 파일로 분리하여 관리할 수 있다. 덕분에 코드의 가독성을 높이고, 여러 개발자가 동시에 작업하기 쉽게 해준다.&lt;/li&gt;
&lt;li&gt;UI 디자이너나 다른 코드 생성 도구에 의해 자동 생성된 코드와 개발자가 작성하는 코드를 분리할 수 있다. &lt;b&gt;partial&lt;/b&gt;을 사용해 자동 생성된 코드가 개발자의 코드와 혼합되어 꼬이는 것을 방지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Partial 메서드의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 필요에 따라 선택적으로 구현할 수 있는 메서드를 제공할 수 있다. 메서드의 선언은 있지만 구현은 없는 상태로 남겨둘 수 있으며, 이 경우 컴파일러가 해당 메서드 호출을 자동으로 제거한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partial 클래스와 메서드를 남용하면 코드 구조 파악이 어려워질 수 있다.&lt;/li&gt;
&lt;li&gt;자동 생성된 코드를 절대 수정해서는 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 24 : 설계 선택지를 제한하는 ICloneable은 사용을 피하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ICloneable의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ICloneable&lt;/b&gt; 인터페이스가 제공하는 &lt;b&gt;Clone&lt;/b&gt; 메서드는 얕은 복사와 깊은 복사 중 어떤 것을 수행해야 하는지 명시하지 않는다. 때문에 구현체에 따라 동작이 달라지며 예상치 못한 동작이 수행될 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Clone&lt;/b&gt; 메서드는 &lt;b&gt;object&lt;/b&gt; 타입을 반환하기 때문에, 반환된 객체를 해당 타입으로 캐스팅해야 한다. 이 과정에서 타입 불일치로 인한 런타임 에러가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ICloneable&lt;/b&gt; 인터페이스는 그 설계상의 모호성과 타입 안정성 문제가 있기 때문에 사용하지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 전체 계층 구조에서 ICloneable을 반드시 구현해야 하는 경우라면 &lt;b&gt;추상 protected Clone()&lt;/b&gt; 메서드를 만들어서 모든 파생 클래스가 강제로 이를 구현하도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 정 사용하고 싶다면 최하단의 파생 클래스에만 추가하도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 25 : 배열 매개변수에는 params 배열만 사용해야 한다 ]&lt;/h2&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var labels = new string[] { &quot;one&quot;, &quot;two&quot;, &quot;three&quot;, &quot;four&quot;, &quot;five&quot; };
ReplaceIndices(labels);

private void ReplaceIndices(object[] param)
{
    for (int i = 0; i &amp;lt; param.Length; i++)
    {
        param[i] = &quot;aa&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 자체는 값(Call by Value) 으로 전달되지만 배열의 원소들은 여전히 참조타입으로 전달(Call by Reference)된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 배열을 전달받은 메서드 내에서 배열의 내용을 임의로 수정해버릴 수 있는 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;params&amp;nbsp;배열의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;params 키워드를 이용하면 사용자는 배열을 명시적으로 생성하지 않고, 메서드에 여러 인자를 직접 전달할 수 있다.&lt;/li&gt;
&lt;li&gt;메서드가 다양한 수의 인자를 받을 수 있게 할 수 있으므로 재사용성이 크게 증가한다. 특히 다양한 인자를 받아 처리해야하는 유틸 메서드에서 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public void LogMessages(params string[] messages)
{
    foreach (var message in messages)
    {
        Console.WriteLine(message);
    }
}

LogMessages(&quot;메시지1&quot;, &quot;메시지2&quot;, &quot;메시지3&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;params 키워드 사용 시 매 호출마다 새로운 배열이 생성된다. 성능이 중요한 상황에서는 이를 고려해야 한다.&lt;/li&gt;
&lt;li&gt;배열 전달 시 타입이 엄격히 고려되지 않기 때문에 런타임에 &lt;b&gt;타입미스매치&lt;/b&gt; 에러 등이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 대안으로, 매개 변수가 시퀀스를 나타낸다면 &lt;b&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/b&gt; 매개변수를 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 26 : 지역 함수를 사용해서 반복자와 비동기 메서드의 오류를 즉시 보고하라 ]&lt;/h2&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public static IEnumerable&amp;lt;T&amp;gt; GenerateSample&amp;lt;T&amp;gt;(IEnumerable&amp;lt;T&amp;gt; sequence, int sampleFrequency)
{
    if (sequence == null)
    {
        throw new ArgumentException(&quot;Source sequence cannot be null&quot;, paramName: nameof(sequence));
    }

    if (sampleFrequency &amp;lt; 1)
    {
        throw new ArgumentException(&quot;Sample frequency must be a positive integer&quot;, paramName: nameof(sampleFrequency));
    }

    var index = 0;
    foreach (var item in sequence)
    {
        if (index % sampleFrequency == 0)
        {
            yield return item;
        }
    }
}

var sequence = new List&amp;lt;int&amp;gt;() { 1, 2, 3, 4, 5 };
Console.WriteLine(&quot;Exception not thrown yet!&quot;);
var samples = GenerateSample(sequence, -8);

foreach (var element in samples) // 실제 사용할 때 예외가 발생한다.
{
    Console.WriteLine(element);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 처럼 &lt;b&gt;IEnumerable&lt;/b&gt;을 반환하는 메서드는 &lt;b&gt;실제로 실행되기 전까지 오류를 검출하지 못할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IEnumerable&lt;/b&gt; 메서드는 실제로 시퀀스를 순회하거나 요소를 사용하려고 할 때 평가되기 때문이다. (&lt;b&gt;Lazy 연산 개념&lt;/b&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public static IEnumerable&amp;lt;T&amp;gt; GenerateSample&amp;lt;T&amp;gt;(IEnumerable&amp;lt;T&amp;gt; sequence, int sampleFrequency)
{
    if (sequence == null)
    {
        throw new ArgumentException(&quot;Source sequence cannot be null&quot;, paramName: nameof(sequence));
    }
    
    if (sampleFrequency &amp;lt; 1)
    {
        throw new ArgumentException(&quot;Sample frequency must be a positive integer&quot;, paramName: nameof(sampleFrequency));
    }

    return GenerateSampleImpl(); // 지역 함수 사용

    IEnumerable&amp;lt;T&amp;gt; GenerateSampleImpl()
    {
        int index = 0;
        foreach (T item in sequence)
        {
            if (index % sampleFrequency == 0)
                yield return item;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; GenerateSampleImpl()&lt;/b&gt;&amp;nbsp;처럼 지역함수를 정의해서 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역 함수는 메서드 내에 정의된 함수로, 이를 사용하면 메서드의 실행을 시작하는 시점에서 바로 &lt;b&gt;파라미터 검증&lt;/b&gt; 같은 오류 검출 로직을 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 기법을 비동기 메서드에도 적용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public static Task&amp;lt;string&amp;gt; LoadMessageFinal&amp;lt;T&amp;gt;(string userName)
{
    if (string.IsNullOrEmpty(userName))
    {
        throw new ArgumentNullException(nameof(userName));
    }
    
    return LoadMessageImpl();

    async Task&amp;lt;string&amp;gt; LoadMessageImpl()
    {
        var settings = await Loaduser();
        
        var message = &quot;message : &quot; + settings.Message ?? &quot;Empty&quot;;
        return message;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 코드를 작성하면 다른 메서드처럼 호출 도중 발생한 오류를 즉시 &lt;b&gt;throw&lt;/b&gt; 할 수 있으므로 문제를 해결하기 쉬워진다.&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>c#</category>
      <category>more effective c#</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/424</guid>
      <comments>https://hyeo-noo.tistory.com/424#entry424comment</comments>
      <pubDate>Sun, 24 Mar 2024 22:06:11 +0900</pubDate>
    </item>
    <item>
      <title>[More Effective C#] Chapter 2. 요약 (1)</title>
      <link>https://hyeo-noo.tistory.com/423</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 11 : API에는 변환 연산자를 작성하지 말라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 다른 타입을 원하는 커스텀 타입으로 변환하고 싶을 때는 생성자를 사용하라. &lt;/b&gt;&lt;br /&gt;&lt;b&gt;생성자는 새로운 객체를 만든다는 사실을 명확히 알려준다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class Circle
{
	...
	
	static public implicit operator Ellipse(Circle c)
	{
		return new Ellipse(...)
	}
}

public static void Flatten(Ellipse e)
{
	e.r1 /= 2;
}

var c = new Circle(...);
Flatten(c); // Circle -&amp;gt; Ellipse 로 암묵적 변환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위와 같은 상황에서 Flatten() 함수는 암묵적 변환 과정에서 새롭게 생성된 Ellipse 객체를 매개변수로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 임시 객체는 Flatten() 함수에 의해 변경되지만, 그 즉시 &lt;b&gt;가비지&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flatten() 함수에 의한 변환 결과는 오직 임시 객체에만 영향을 미친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;변환 연산자의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가독성 저하: 코드에서 변환 연산자가 사용될 때, 그것이 명시적인지 암시적인지 구분하기 어려워 코드의 의도를 파악하기 어려울 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상치 못한 동작: 변환 연산자가 암시적으로 호출되면서 예상치 못한 결과를 초래할 수 있다. &lt;/b&gt;&lt;br /&gt;&lt;b&gt;특히, 다른 타입으로의 변환이 자동으로 이루어질 때, 그 과정에서 데이터 손실이나 변형이 발생할 위험이 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅 어려움: 변환 연산자 때문에 발생한 문제는 디버깅하기 어렵다. 변환 연산자가 암시적으로 호출되는 경우, 코드의 흐름을 따라가기가 더 복잡해진다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flatten() 에 객체를 전달해 봐야 변경 사항이 적용되지 않기 때문에 아래와 같이 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var e = new Ellipse(c);
Flatten(e);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circle을 명시적으로 Ellipse 객체로 생성해준 후 Flatten()을 적용한 모습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;명시적으로 변환을 보여주거나, 팩토리 메서드 패턴등을 사용해 변환 과정을 명확히 보여주는 것이 중요&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 12 : 선택적 매개변수를 사용하여 메서드 오버로드를 최소화하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택적 매개변수와 명명된 매개변수를 사용하면 메서드 오버로드를 최소화 하면서도 유연성을 추구할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명명된 매개변수 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;PrintOrderDetails(orderNum: 31, productName: &quot;Red Mug&quot;, sellerName: &quot;Gift Shop&quot;);
PrintOrderDetails(productName: &quot;Red Mug&quot;, sellerName: &quot;Gift Shop&quot;, orderNum: 31);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택적 매개변수 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 필수 매개변수 1개와 선택적 매개변수 2개
public void ExampleMethod(int required, string optionalstr = &quot;default string&quot;,
    int optionalint = 10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 메서드의 매개변수 이름은 public 인터페이스로 취급하라.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Setname(lastName: &quot;c&quot;, firstName: &quot;h&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SetName()의 매개변수 이름을 다음과 같이 변경했다고 하자.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public void SetName(string last, string first);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SetName()을 기존에 호출하던 모든 어셈블리는 문제없이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수의 이름은 &lt;b&gt;IL&lt;/b&gt;에 저장되는데, 메서드 호출부가 아닌 메서드 정의부에 포함되기 때문에 이름을 변경하더라도 기존에 컴파일된 다른 어셈블리에는 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 호출 코드에 일부 수정이 있어 다시 컴파일 하려고 하면 이름 변경 때문에 컴파일 에러가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 초기 배포 시에는 사용자가 원하는 매개변수의 조합을 다양하게 활용할 수 있도록 선택적 매개변수와 명명된 매개변수를 충분히 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이후에 기존 메서드에 매개변수를 추가해야 한다면 이 부분은 반드시 메서드 오버로드 형태로 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 13 : 타입의 가시성을 제한하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. public 인터페이스로는 가능한 적은 수의 클래스만 노출하라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 작성 시 뭔가를 알 필요가 없다는 사실은 개발할 때 크게 도움이 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 타입이 어디서 사용될 지를 신중히 고민해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 인터페이스는 public, 구현체는 internal 로 사용하면 다른 어셈블리에 영향을 주지 않고 추가할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 팩토리 패턴을 사용해서 적절한 구현체를 생성할 때, 어셈블리 밖에서는 인터페이스만 보이고, 구현체들은 어셈블리 내부에서만 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인터페이스를 통해 다른 어셈블리에서 정의된 클래스의 행동을 사용할 수 있지만, 실제로 그 구현은 해당 어셈블리 내부에서만 알고 있어야 하는 경우에 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 클래스의 가시범위를 제한함으로서 시스템을 업데이트 하거나 확장할 때 변경해야할 코드를 최소화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 14 : 상속보다는 인터페이스를 정의하고 구현하는 것이 낫다 ]&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속 : Is a (~는 ~이다) &lt;br /&gt;인터페이스 : behave like (~처럼 동작한다)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 베이스 클래스와 인터페이스 중 어떤 것을 선택할 것인가에 대한 문제는 향후 추상화를 어떻게 지원할 것인지에 대한 결정에 달려있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스는 해당 인터페이스를 구현하는 &lt;b&gt;타입을 어떻게 사용할지에 대한 계약&lt;/b&gt;과 같아서 한번 배포되고 나면 그 이후에 변경하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 베이스 클래스는 배포 이후에도 기능을 확장할 수 있으며, 이렇게 확장된 내용은 모든 파생 클래스에 즉각 반영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 사용할 상황을 나누자면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;관련된 클래스들의 공통 동작을 기술하고 구현한다면 베이스 클래스를 생성하고 상속을 사용하자.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성을 높여 범용적인 타입을 만들고 싶다면 인터페이스를 사용하자.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 둘은 서로 대치되는 기능이 아니므로 함께 잘 사용하는것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 베이스 클래스 최상단에 인터페이스가 있는 경우처럼 함께 사용하는 상황이 많다. (Item 15)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 재사용성을 높이기 위해서 제네릭 인터페이스를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 닷넷에서 제공하는 &lt;b&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/b&gt; 인터페이스가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IEnumerable&amp;lt;T&amp;gt; 를 구현한 클래스는 System.Linq.Enumerable 에 정의된 모든 확장 메서드를 지원하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 클래스는 대표적으로 Linq의 쿼리 메서드인 Select, Where 등을 사용할 수 있고, Linq를 활용해 데이터를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 15 : 인터페이스 메서드와 가상 메서드의 차이를 이해하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 구현하는 것과 가상 함수를 재정의하는 것은 용도도 다르고 개념도 다르다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 클래스의 추상 멤버를 구현하려면 반드시 가상화가 필요하지만 인터페이스 멤버의 경우에는 항상 가상화가 필요하지는 않다. &lt;br /&gt;그리고 인터페이스는 명시적으로도 구현할 수 있어 클래스의 public 멤버와 달리 어느 정도 숨길 수도 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;인터페이스 메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스 메서드는 인터페이스에 선언되며, 구현 클래스에서 반드시 구현해야 한다&lt;/li&gt;
&lt;li&gt;인터페이스는 구현을 제공하지 않으며, 모든 메서드는 기본적으로 추상 메서드이다.&lt;/li&gt;
&lt;li&gt;인터페이스를 통해 다형성을 구현할 수 있으며, 다른 클래스가 이를 구현할 때는 인터페이스에 정의된 모든 메서드를 구현해야 한다.&lt;/li&gt;
&lt;li&gt;인터페이스를 사용하면 클래스의 기능을 확장하는 데 유용하다. 다양한 클래스가 동일한 인터페이스를 구현함으로써, 다형성을 통한 유연성을 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;가상 메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상 메서드는 클래스 내에 선언되며,&amp;nbsp;virtual&amp;nbsp;키워드를 사용하여 선언한다.&lt;/li&gt;
&lt;li&gt;가상 메서드는 기본 구현을 제공하지만, 파생 클래스에서&amp;nbsp;override&amp;nbsp;키워드를 사용하여 재정의할 수 있다.&lt;/li&gt;
&lt;li&gt;가상 메서드를 사용하면, 파생 클래스에서 부모 클래스의 메서드 구현을 변경할 수 있어, 코드의 재사용성과 유연성이 증가한다.&lt;/li&gt;
&lt;li&gt;가상 메서드는 실행 시간에 결정되는 동적 바인딩을 사용한다. 이는 객체의 실제 유형(생성 시 결정)에 따라 호출되는 메서드가 결정되는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스 메서드는 구현을 제공하지 않지만, 가상 메서드는 기본 구현을 제공한다.&lt;/li&gt;
&lt;li&gt;인터페이스 메서드는 다형성을 제공하는 데 중점을 두고, 여러 클래스가 같은 인터페이스를 구현할 수 있도록 한다. &lt;br /&gt;반면, 가상 메서드는 상속을 통해 클래스 계층 내에서 메서드의 구현을 커스터마이징할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;가상 메서드는 동적 바인딩을 사용하며, 실행 시간에 메서드를 결정한다. &lt;br /&gt;인터페이스 메서드도 동적 바인딩을 사용하지만, 구현 클래스에서 명시적으로 메서드를 구현해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;interface IMessage
{
	void Message();
}

public class MyClass : IMessage
{
	public void Message() =&amp;gt; ...;
}

public class MyDerivedClass : MyClass, IMessage
{
	public new void Message() =&amp;gt; ...;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상속, 구현 관계인 경우 &lt;b&gt;MyDerivedClass.Message()&lt;/b&gt; 메서드는 아직 new 키워드가 필요하다. 따라서 아래와 같은 문제가 발생하는데&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MyDerivedClass d = new MyDerivedClass();
d.Message(); // &quot;MyDerivedClass&quot; 출력
IMessage m = d as IMessage;
m.Message(); // &quot;MyDerivedClass&quot; 출력
MyClass b = d;
b.Message(); // &quot;MyClass&quot; 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법은 베이스 클래스의 Message() 메서드를 가상(virtual)으로 선언하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class MyClass : IMessage
{
	public virtual void Message() =&amp;gt; ...;
}

public class MyDerivedClass : MyClass
{
	public override void Message() =&amp;gt; ...;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 MyDerivedClass, MyClass, IMessage 참조 중 어느 것이든 항상 오버라이딩 된 버전이 호출된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메서드는 실행 시간에 결정되는 동적 바인딩을 사용한다. 이는 객체의 실제 유형에 따라 호출되는 메서드가 결정되는 방식이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 함수를 더 적극적으로 사용하고 싶다면 MyClass 를 추상 클래스로 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public abstract class MyClass : IMessage
{
	public abstract void Message();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스 내의 메서드를 실제로 구현하지 않으면서도, 인터페이스를 구현한 것처럼 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 16 : 상태 전달을 위한 이벤트 패턴을 구현하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 객체나 시스템의 상태 변화를 알리기 위해 사용되며, &lt;br /&gt;&lt;b&gt;특히 GUI 프로그래밍, 실시간 시스템, 네트워크 통신 등에서 중요한 역할을 한다.&lt;br /&gt;&lt;/b&gt;예를 들어, 사용자의 클릭, 시스템의 상태 변화 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;이벤트 프로듀서(이벤트를 발생시키는 객체)와 이벤트 컨슈머(이벤트에 반응하는 객체) 사이의 결합도를 낮춰, 코드의 유연성과 재사용성을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;상태 전달을 위한 이벤트 패턴 구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전달 이벤트 패턴은 이벤트가 발생했을 때, 이벤트와 함께 추가적인 정보(상태)를 전달하고자 할 때 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 핸들러 메서드는 일반적으로 두 개의 매개변수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;object sender&lt;/b&gt;와&amp;nbsp;&lt;b&gt;EventArgs e&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sender&lt;/b&gt;는 이벤트를 발생시킨 객체,&amp;nbsp;&lt;b&gt;e&lt;/b&gt;는 이벤트와 함께 전달되는 추가 정보를 담고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EventArgs&lt;/b&gt;&amp;nbsp;클래스를 상속받아 필요한 상태 정보를 포함하는 커스텀 클래스를 만들어 사용할 수 있고 이를 통해 이벤트 컨슈머가 필요한 정보를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구현 요령&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요에 따라 커스텀&amp;nbsp;&lt;b&gt;EventArgs&lt;/b&gt;&amp;nbsp;클래스를 정의하여 이벤트와 함께 전달할 상태 정보를 구체화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 구독 및 발행은&amp;nbsp;&lt;b&gt;+=&lt;/b&gt;&amp;nbsp;연산자와&amp;nbsp;&lt;b&gt;-=&lt;/b&gt;&amp;nbsp;연산자를 사용하여 간단히 할 수 있으며, 이벤트를 발생시킬 때는&amp;nbsp;&lt;b&gt;?.Invoke&lt;/b&gt;&amp;nbsp;메서드를 사용하여 안전하게 이벤트 핸들러가 존재하는지 확인한 후 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 사용하게 되면 이벤트를 전달하는 측과 수신하는 측을 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이벤트를 전달하는 측의 코드를 개발할 때, 수신 측을 고려할 필요가 없으므로 완전히 독립적으로 개발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 17 : 내부 객체를 참조로 반환해서는 안 된다 ]&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 캡슐화와 데이터 무결성을 유지하는 데 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 클래스 내부에 List&amp;lt;T&amp;gt;와 같은 컬렉션이 있을 때, 이 컬렉션을 외부에 그대로 반환해서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환된 컬렉션을 통해 외부에서 항목을 추가하거나 제거함으로써, 클래스의 내부 상태가 예상치 못하게 변경될 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 값 타입을 반환하라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 타입은 클라이언트가 프로퍼티를 통해 내부 객체에 접근할 때 복사본을 넘기기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 불변 객체를 반환하라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.String과 같은 변경 불가능한 타입을 반환하는 한 내부 상태는 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 읽기 전용 인터페이스를 제공하라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 컬렉션에 대한 읽기 전용 인터페이스를 제공하여 외부에서는 조회만 가능하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 List&amp;lt;T&amp;gt; 를 IEnumerable&amp;lt;T&amp;gt; 인터페이스의 참조로 노출하는 것도 이러한 전략의 예시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 래퍼(wrapper)객체를 만들어서 전달하라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 객체로의 접근을 최소화하도록 래퍼 객체를 만들고, 그 래퍼의 인스턴스를 전달하는 방법이다. ReadOnlyCollection&amp;lt;T&amp;gt; 타입은 컬렉션을 래핑하여 읽기 전용 버전으로 노출하는 표준 방법을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[ Item 18 : 이벤트 핸들러보다는 오버라이딩을 사용하라 ]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이벤트 핸들러와 오버라이딩이 서로 비교가 된다는 것 자체가 이해가 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 이 둘을 비교하는 것 자체가 특정 상황에 국한된 것이라고 생각하니 내용이 눈에 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이벤트 핸들러보다는 오버라이딩을 사용하라&quot; 이 말이 적용될 수 있는 상황을 살펴보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 핸들러는 주로 이벤트에 반응하는 로직을 구현할 때 사용된다.&lt;/li&gt;
&lt;li&gt;오버라이딩은 상속받은 메서드의 동작을 변경하고자 할 때 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;사용 상황&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 핸들러는 사용자의 입력, 시스템 이벤트 등에 반응해야 할 때 주로 사용된다.&lt;/li&gt;
&lt;li&gt;오버라이딩은 객체 지향 설계에서 다형성을 구현하거나 기존 동작을 수정할 때 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 처리를 위해 오버라이딩을 사용한다는 말은, 특정 이벤트가 발생했을 때 그에 대한 반응으로 특정 메서드의 동작을 오버라이딩하여 사용한다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 특히 &lt;b&gt;WPF 같은 GUI 프로그래밍에서 자주 볼 수 있는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, WPF 버튼 클릭 이벤트를 처리하고 싶은 상황이다. 버튼에 클릭 리스너를 설정하고, 그 안에서 &lt;b&gt;OnMouseDown&lt;/b&gt; 메서드를 오버라이딩하여 버튼이 클릭되었을 때 원하는 동작을 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;protected override void OnMouseDown(MouseButtonEventArgs e)
{
	DoMouseThings(e);
	base.OnMouseDown(e);
{&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;이벤트 핸들러의 단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 발생 시마다 핸들러를 찾아 호출하는 과정에서 추가적인 오버헤드가 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러를 잘못 작성해 예외를 던진 경우 이벤트 체인에 있는 다른 핸들러가 호출되지 않는다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러의 관리와 디버깅이 복잡해질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 오버라이딩을 사용할 수 있다. 보통 상속 가능한 클래스에서는 이벤트를 처리하는 메서드를 오버라이드하여 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 컴파일 타임에 결정되므로, 런타임에 발생하는 오버헤드를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 성능과 코드 복잡성에 민감한 상황에서는 오버라이딩을 고려해야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Lang/C#</category>
      <category>c#</category>
      <category>event</category>
      <category>more effective c#</category>
      <author>Henu</author>
      <guid isPermaLink="true">https://hyeo-noo.tistory.com/423</guid>
      <comments>https://hyeo-noo.tistory.com/423#entry423comment</comments>
      <pubDate>Sun, 17 Mar 2024 19:04:37 +0900</pubDate>
    </item>
  </channel>
</rss>