<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>연향동큰손</title>
    <link>https://developerwoohyeon.tistory.com/</link>
    <description>https://github.com/yangwoohyeon</description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 13:52:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>연향동큰손</managingEditor>
    <image>
      <title>연향동큰손</title>
      <url>https://tistory1.daumcdn.net/tistory/6454719/attach/3c7b2248c5c54a8886777ee5c256c103</url>
      <link>https://developerwoohyeon.tistory.com</link>
    </image>
    <item>
      <title>[C#] onetime, oneway, twoway, onewaytosource</title>
      <link>https://developerwoohyeon.tistory.com/295</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;onetime&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MODE를 onetime으로 설정하게 되면 ViewModel의 요소를 최초에 한번 수정했을때는 View의 값이 변경되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 연결이 끊어지게 되면서 ViewModel의 요소를 변경하더라도 View에 반영되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;oneway&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;소스의 값 -&amp;gt; 인터페이스의 값 한 방향으로만 바인딩을 허용한다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;ViewModel에서 요소를 변경했을때 View에 변경된 내용이 반영되지만 View에서의 변경이 ViewModel에 반영되지는 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;twoway&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;ViewModel에서 요소를 변경했을때 View에&lt;span&gt; 변경된 내용이 반영되고, 반대로 View에서의 변경도 ViewModel에 반영 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;onewaytosource&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;oneway의 정 반대라고 보면 된다.&lt;/p&gt;</description>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/295</guid>
      <comments>https://developerwoohyeon.tistory.com/295#entry295comment</comments>
      <pubDate>Thu, 19 Feb 2026 15:36:30 +0900</pubDate>
    </item>
    <item>
      <title>[C#] LINQ(데이터 쿼리)</title>
      <link>https://developerwoohyeon.tistory.com/294</link>
      <description>&lt;p data-end=&quot;423&quot; data-start=&quot;387&quot; data-ke-size=&quot;size16&quot;&gt;LINQ (&lt;b&gt;Language Integrated Query)&lt;/b&gt;는&amp;nbsp;컬렉션을 &lt;b&gt;SQL처럼 질의&lt;/b&gt;하는 문법이다.&lt;/p&gt;
&lt;p data-end=&quot;423&quot; data-start=&quot;387&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;423&quot; data-start=&quot;387&quot; data-ke-size=&quot;size16&quot;&gt;foreach + if문을 통해 조건에 맞는 요소를 찾는 것을 구현하게 되면 코드가 길어지면서 가독성이 떨어지는 문제가 발생하지만 LINQ를 통해 구현하게 되면 코드 한줄을 통해 원하는 행위를 명확하게 표현할 수 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;423&quot; data-start=&quot;387&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Where&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Where는 컬렉션에서&lt;u&gt;&lt;b&gt; 조건을 만족하는 요소만 골라내는 필터&lt;/b&gt;&lt;/u&gt;다.&lt;/p&gt;
&lt;pre id=&quot;code_1771465660935&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;string&amp;gt; list = new List&amp;lt;string&amp;gt;();
list.Add(&quot;Apple&quot;);
list.Add(&quot;Grape&quot;);
list.Add(&quot;Banana&quot;);
list.Add(&quot;AAA&quot;);
list.Add(&quot;BDFA&quot;);


IEnumerable&amp;lt;string&amp;gt; q = list
    .Where(p =&amp;gt; p.StartsWith(&quot;A&quot;));
    
foreach(var item in q)
{
    Console.WriteLine(item); // Apple, AAA 출력
}&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Select&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5-2&quot; data-message-id=&quot;04c6f49a-ee32-4872-b60c-0888c18376ff&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;46&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Select&lt;/b&gt;는 컬렉션의 각 요소를 다른 값이나 형태로 변환하는 기능이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1771465950765&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;string&amp;gt; list = new List&amp;lt;string&amp;gt;();
list.Add(&quot;Apple&quot;);
list.Add(&quot;Grape&quot;);
list.Add(&quot;Banana&quot;);
list.Add(&quot;AAA&quot;);
list.Add(&quot;BDFA&quot;);


IEnumerable&amp;lt;string&amp;gt; q = list
    .Where(p =&amp;gt; p.StartsWith(&quot;A&quot;))
    .Select(p=&amp;gt;p.ToUpper());

foreach(var item in q)
{
    Console.WriteLine(item); // APPLE, AAA 출력
}&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Any&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션에 조건에 맞는 요소가 하나라도 있으면 true, 아니면 false를 반환&lt;/p&gt;
&lt;pre id=&quot;code_1771466592827&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool hasA = list.Any(q =&amp;gt; q.StartsWith(&quot;A&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FirstOrDefault&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 맞는 &lt;b&gt;첫 번째 요소&lt;/b&gt;를 가져오고, 없으면 기본값을 반환하는 메서드다.&lt;/p&gt;
&lt;pre id=&quot;code_1771466755037&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var ddd = list.FirstOrDefault(q =&amp;gt; q.StartsWith(&quot;A&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;값이 없는 경우 &amp;rarr; default(T)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;229&quot; data-start=&quot;189&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;204&quot; data-start=&quot;189&quot;&gt;참조 타입: null&lt;/li&gt;
&lt;li data-end=&quot;229&quot; data-start=&quot;207&quot;&gt;값 타입: 0, false 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/294</guid>
      <comments>https://developerwoohyeon.tistory.com/294#entry294comment</comments>
      <pubDate>Thu, 19 Feb 2026 11:11:04 +0900</pubDate>
    </item>
    <item>
      <title>스레딩</title>
      <link>https://developerwoohyeon.tistory.com/292</link>
      <description>&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;프로그램 실행 종료에 영향을 미치는 스레드를 가리켜 전경 스레드(foreground thread)라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배경 스레드(background) =&amp;gt; 프로그램 종료에 영향 X&lt;/p&gt;
&lt;pre id=&quot;code_1768286396843&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static void Main(string[] args)
{
    Thread t = new Thread(threadFunc);
    t.IsBackground = true;
    t.Start(); //실행 안됨
}

static void threadFunc()
{
    Console.WriteLine(&quot;60초 후에 프로그램 종료&quot;);
    Thread.Sleep(1000 * 10);

    Console.WriteLine(&quot;스레드 종료!&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768286672513&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; static void Main(string[] args)
 {
     Thread t = new Thread(threadFunc);
     t.IsBackground = true;
     t.Start();

     t.Join(); //t스레드 종료까지 대기
     Console.WriteLine(&quot;주 스레드 종료!&quot;);
 }

 static void threadFunc()
 {
     Console.WriteLine(&quot;10초 후에 프로그램 종료&quot;);
     Thread.Sleep(1000 * 10);

     Console.WriteLine(&quot;스레드 종료!&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;&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;&lt;b&gt;오래 걸리는 연산에 대한 중도 종료 기능 구현&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768288625899&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static void Main(string[] args)
{
    while (true)
    {
        Console.WriteLine(&quot;x입력 시 종료&quot;);
        string str = Console.ReadLine();
        if (str.Equals(&quot;x&quot;, StringComparison.OrdinalIgnoreCase) == true)
        {
            Console.WriteLine(&quot;프로그램 종료&quot;);
            break;
        }
        Thread t = new Thread(DDD);
        t.IsBackground = true; //t스레드는 프로세스 종료에 영향을 안미침
        t.Start(str);
    }
    Console.WriteLine(&quot;주 스레드 종료!&quot;);
}

static void DDD(object a)
{
    string s = &quot;&quot;;
    for(int i=0; i&amp;lt;100000; i++)
    {
        s = s+ &quot;1&quot;;
    }

    Console.WriteLine(&quot;DDD 종료&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgkhsL/dJMcadm9v6n/nuTjOfnydK1HhfqEYt4PX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgkhsL/dJMcadm9v6n/nuTjOfnydK1HhfqEYt4PX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgkhsL/dJMcadm9v6n/nuTjOfnydK1HhfqEYt4PX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgkhsL%2FdJMcadm9v6n%2FnuTjOfnydK1HhfqEYt4PX0%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;237&quot; height=&quot;219&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;219&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;위 코드를 보면 t스레드를 Background 스레드로 설정하였기 때문에 t스레드의 연산이 실행 중이더라도 x를 누르면 메인 스레드에서 while문을 탈출하고 프로그램이 종료되는 것을 확인할 수 있다.&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;반면 t.IsBackground = true를 주석처리하면 foreground 스레드로 설정이 되어 프로그램 종료에 영향을 미치는 스레드가 된다.&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;따라서 아래 사진과 같이 주 스레드가 종료되어도 프로그램이 종료되지 않고 t스레드가 종료되어야 프로그램이 완전히 종료되는 것을 확인 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6xhvl/dJMcacPmVr3/YKAfKvCsr01YsVNkdATpz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6xhvl/dJMcacPmVr3/YKAfKvCsr01YsVNkdATpz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6xhvl/dJMcacPmVr3/YKAfKvCsr01YsVNkdATpz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6xhvl%2FdJMcacPmVr3%2FYKAfKvCsr01YsVNkdATpz0%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;502&quot; height=&quot;363&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;363&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;멀티 스레드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼드코어 CPU에서는 총 4개의 스레드를 동시에 실행할 수 있지만, 윈도우 작업 관리자를 확인해보면 4개 이상의 프로세스가 동시에 실행중임을 확인 가능하다.&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;이것이 가능한 이유는 CPU가 하나의 스레드를 짧은 시간 동안 실행한 후 그 스레드를 멈추고 다음 스레드를 선택해서 실행하는 과정을 반복하기 때문이다.&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;공&lt;/b&gt;&lt;b&gt;유 리소스에 대한 동기화 처리&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/span&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;이를 위해서 BCL에서 &lt;b&gt;Monitor&lt;/b&gt; 클래스를 제공한다.&lt;/p&gt;</description>
      <category>C#</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/292</guid>
      <comments>https://developerwoohyeon.tistory.com/292#entry292comment</comments>
      <pubDate>Tue, 13 Jan 2026 16:58:54 +0900</pubDate>
    </item>
    <item>
      <title>BCL</title>
      <link>https://developerwoohyeon.tistory.com/289</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IsNullOrEmpty&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열이 비어있는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1768032543359&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string name = &quot;&quot;;
if(string.IsNullOrEmpty(name) == true)
{
    Console.WriteLine(&quot;비어있음.&quot;);
}
else
{
    Console.WriteLine(name);
}&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;h3 data-ke-size=&quot;size23&quot;&gt;System.DateTime&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간과 관련된 기능 제공&lt;/p&gt;
&lt;pre id=&quot;code_1768032922535&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DateTime now = DateTime.Now;
Console.WriteLine(now); //현재 날짜, 시간 출력

DateTime dayForChildren = new DateTime(now.Year, 5, 5);
Console.WriteLine(dayForChildren);

//출력 결과
2026-01-10 오후 5:14:33
2026-05-05 오전 12:00:00&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;System.TimeSpan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DateTime 타입에 대해 사칙 연산 중에서 유일하게 허용되는 것이 빼기 이며 빼기의 연산 결괏값은 2개의 DateTime사이의 시간 간격을 나타내는 TimeSpan으로 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1768033556169&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; DateTime endOfYear = new DateTime(DateTime.Now.Year, 12, 31);
 DateTime now = DateTime.Now;

 Console.WriteLine(&quot;오늘 날짜: &quot; + now);

 TimeSpan gap = endOfYear - now;
 Console.WriteLine(&quot;올해의 남은 날짜: &quot; + gap.TotalDays);&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;h3 data-ke-size=&quot;size23&quot;&gt;System.Diagnostics.Stopwatch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCL에서 제공하는 고정밀 시간 측정 클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※코드 특정 구간의 성능을 측정할 때 자주 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1768033704706&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stopwatch sw = new Stopwatch();

sw.Start();
// 측정하고 싶은 코드
Thread.Sleep(500);
sw.Stop();

Console.WriteLine(sw.ElapsedMilliseconds + &quot; ms&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;System.String&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;string타입에 대한 다양한 기능을 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768033971432&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;String 클래스 (System)&quot; data-og-description=&quot;텍스트를 UTF-16 코드 단위의 시퀀스로 나타냅니다.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&quot; data-og-url=&quot;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btXoTv/hyZRq4XFqb/l6lO57aTAbiFB7FlNXPrIK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/ko-kr/dotnet/api/system.string?view=net-8.0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btXoTv/hyZRq4XFqb/l6lO57aTAbiFB7FlNXPrIK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;String 클래스 (System)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;텍스트를 UTF-16 코드 단위의 시퀀스로 나타냅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드의 출력 결과는 모두 True이다.&lt;/p&gt;
&lt;pre id=&quot;code_1768034194355&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string txt = &quot;Hello&quot;;
Console.WriteLine(txt == &quot;Hello&quot;);
Console.WriteLine(txt.Equals(&quot;Hello&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;== 연산은 값 형식에서는 값 자체를 비교하고, 참조 형식에서는 주소를 비교하는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 txt == &quot;Hello&quot;가 True를 반환할까?&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;이유는 System.String에서 == 연산을 값을 비교할 수 있도록 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;u&gt;&lt;b&gt;오버로딩 &lt;/b&gt;&lt;/u&gt;&lt;/span&gt;했기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccga2J/dJMcabv8NAN/2n7g8t8swAbLF7JNn2HIJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccga2J/dJMcabv8NAN/2n7g8t8swAbLF7JNn2HIJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccga2J/dJMcabv8NAN/2n7g8t8swAbLF7JNn2HIJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccga2J%2FdJMcabv8NAN%2F2n7g8t8swAbLF7JNn2HIJk%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;580&quot; height=&quot;148&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Format&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1768035072458&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string str = &quot;Hello {0}: {1}&quot;;
string output = string.Format(str, &quot;World&quot;, &quot;Anderson&quot;);
Console.WriteLine(output);

//실행 결과
Hello World: Anderson&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; Format Item 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768035451780&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{index[,alignment][:formatString]}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소의미&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;534&quot; data-start=&quot;427&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;534&quot; data-start=&quot;451&quot;&gt;
&lt;tr data-end=&quot;470&quot; data-start=&quot;451&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;461&quot; data-start=&quot;451&quot;&gt;index&lt;/td&gt;
&lt;td data-end=&quot;470&quot; data-start=&quot;461&quot; data-col-size=&quot;sm&quot;&gt;인자 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;499&quot; data-start=&quot;471&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;485&quot; data-start=&quot;471&quot;&gt;alignment&lt;/td&gt;
&lt;td data-end=&quot;499&quot; data-start=&quot;485&quot; data-col-size=&quot;sm&quot;&gt;정렬 (생략 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;534&quot; data-start=&quot;500&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;517&quot; data-start=&quot;500&quot;&gt;formatString&lt;/td&gt;
&lt;td data-end=&quot;534&quot; data-start=&quot;517&quot; data-col-size=&quot;sm&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;System.Text.StringBuilder&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;string 타입은 불변 객체이기 때문에 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;문자열 변경에서의 문제점(StringBuilder X)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768036141796&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string txt = &quot;Hello World&quot;;
for(int i=0; i&amp;lt;300000; i++)
{
    txt = txt + &quot;1&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;힙에 Hello World 문자열 공간 할당&lt;/li&gt;
&lt;li&gt;스택에 txt변수의 주소를 저장&lt;/li&gt;
&lt;li&gt;txt + &quot;1&quot; 동작을 수행하기 위해 txt.Length + &quot;1&quot;.Length 크기의 메모리를 힙에 할당하고 txt와 &quot;1&quot;을 복사하여 붙힘&lt;/li&gt;
&lt;li&gt;3번 과정을 통해 새롭게 할당된 힙 주소를 스택에 저장&lt;/li&gt;
&lt;li&gt;3번 4번 과정을 30만번 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;StringBuilder를 통해 문자열을 더했을때&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1768036878751&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; string txt = &quot;Hello World&quot;;
 Stopwatch sw = Stopwatch.StartNew();

 for(int i=0; i&amp;lt;300000; i++)
 {
     txt = txt + &quot;1&quot;;
 }
 sw.Stop();
 Console.WriteLine(sw.ElapsedMilliseconds);

 string txt2 = &quot;Hello World&quot;;

 Stopwatch sw2 = Stopwatch.StartNew();
 StringBuilder sb = new StringBuilder();
 sb.Append(txt2);

 for (int i = 0; i &amp;lt; 300000; i++)
 {
     sb.Append(&quot;1&quot;);
 }
 string newText = sb.ToString();
 sw2.Stop();
 Console.WriteLine(sw2.ElapsedMilliseconds);&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;StringBuilder가 내부적으로 일정한 양의 메모리를 항당&lt;/li&gt;
&lt;li&gt;Append 메서드에 들어온 인자를 미리 할당한 메모리에 복사&lt;/li&gt;
&lt;li&gt;2번 과정만 30만번 반복, 만약 Append로 추가된 문자열이 미리 할당된 메모리보다 많아지면 여유분의 메모리를 할당&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1768036947552&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; +로 문자열을 더했을때 걸린 시간 : 23580 ms
StringBuilder로 문자열을 더했을때 걸린 시간 : 2 ms&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>C#</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/289</guid>
      <comments>https://developerwoohyeon.tistory.com/289#entry289comment</comments>
      <pubDate>Sat, 10 Jan 2026 18:23:57 +0900</pubDate>
    </item>
    <item>
      <title>[WPF] 계산기 만들기</title>
      <link>https://developerwoohyeon.tistory.com/288</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WPF&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows Presentation Foundation의 약자&lt;/li&gt;
&lt;li&gt;마이크로소프트에서 만든 UI 프레임워크&lt;/li&gt;
&lt;li&gt;.NET으로 빌드가 가능&lt;/li&gt;
&lt;/ul&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;b&gt;MVVM 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;model&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;View&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;ViewModel&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;View에서 발생하는 이벤트 감지하고, 해당 이벤트에 맞는 비즈니스 로직을 수행&lt;/li&gt;
&lt;li&gt;Model과 상호작용하여 데이터를 가져오거나 업데이터&lt;/li&gt;
&lt;li&gt;View에 데이터를 업데이터하는 역할&lt;/li&gt;
&lt;li&gt;View에 표시할 데이터를 가공하여 제공&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;MVVM패턴 동작과정&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCNpF1/dJMcafL4rot/4zvdi0af3UjKwPkwFKpRm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCNpF1/dJMcafL4rot/4zvdi0af3UjKwPkwFKpRm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCNpF1/dJMcafL4rot/4zvdi0af3UjKwPkwFKpRm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCNpF1%2FdJMcafL4rot%2F4zvdi0af3UjKwPkwFKpRm1%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;714&quot; height=&quot;396&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자의 Action이 View를 통해 들어온다.&lt;/li&gt;
&lt;li&gt;View에 Action이 들어오면 ViewModel에 Action을 전달&lt;/li&gt;
&lt;li&gt;ViewModel은 Model에게 데이터를 요청&lt;/li&gt;
&lt;li&gt;Model이 ViewModel에게 데이터를 전달&lt;/li&gt;
&lt;li&gt;ViewModel이 응답받은 데이터를 가공하여 저장&lt;/li&gt;
&lt;li&gt;View는 Data Binding을 이용해 UI를 갱신&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;MVVM 패턴의 장점&lt;/b&gt;&lt;br /&gt;- 뷰 로직과 비즈니스 로직을 분리하여 생산성 증가&lt;br /&gt;- 테스트 수월&lt;br /&gt;- 하나의 ViewModel을 여러 View가 공유할 수 있다( 하나의 로그인 ViewModel을 모바일뷰,PC뷰,테블릿뷰가 공유할 수 있다. (코드 재사용성 용이)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;MVVM 패턴의 단점&lt;/b&gt;&lt;br /&gt;- 설계가 복잡함&lt;br /&gt;- 데이터 바인딩으로 인한 메모리 소모가 심하다.&lt;br /&gt;- ViewModel 설계가 복잡함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;XAML&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eXtensible Markup Language&lt;/li&gt;
&lt;li&gt;WPF에서 View를 그리기위해 사용하는 언어&lt;/li&gt;
&lt;li&gt;Markup Language이기 때문에 태그를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;계산기 만들어보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partial : 하나의 클래스를 여러 파일에 나눠서 작성할 수 있게 해주는 키워드&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 프로젝트를 보면 UI와 로직을 분리해서 관리하고 이 둘이 합쳐져서 하나의 클래스를 정의하기 때문에 partial키워드를 넣어준다.&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;public partial class MainWindow : Window&lt;/b&gt; 부분을 보면 Window클래스를 상속받는 것을 확인할 수 있다.&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;Window클래스는 창 하나에 필요한 모든 기본 기능을 구현해둔 클래스이고 이 클래스를 MainWindow에서 상속받기 때문에 창을 만들 때 필요한 여러 값이나 기능을 사용할 수 있게 된다.&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;MainWindow 클래스를 생성할때 InitializeComponent()를 통해 XAML을 읽고, Display나 Button등의 컨트롤 객체를 생성하는 등의 초기화 작업이 이루어 진다.&lt;/p&gt;
&lt;pre id=&quot;code_1767960006524&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public MainWindow()
{
    InitializeComponent(); // XAML UI 초기화
}&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;size16&quot;&gt;값을 저장하기 위한 멤버 변수&lt;/p&gt;
&lt;pre id=&quot;code_1767960196265&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private double _currentValue = 0; // 현재 계산 값
private string _currentOperator = &quot;&quot;; // 현재 연산자
private bool _isNewEntry = 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Number_Click메서드&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;object sender : 이벤트를 발생시킨 객체
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜 object 타입일까? : 이벤트를 발생 시키는 객체가 Button, TextBox, Window등 다양할 수 있기 때문이다.&lt;/li&gt;
&lt;li&gt;왜 Button button = sender as Button; 와 같이 sender를 Button타입으로 바꿔줄까? :&amp;nbsp; sender가 object타입이기 때문에 버튼 클래스의 메서드나 속성에 접근하기 위해서 형변환을 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RoutedEventArgs e : WPF 이벤트에 대한 추가 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767960515802&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void Number_Click(object sender, RoutedEventArgs e)
{
    Button button = sender as Button;
    if (_isNewEntry) //지금 숫자 입력이 첫 숫자 입력인가?
    {
        Display.Text = button.Content.ToString(); //지금 누른 숫자를 화면에 넣음
        _isNewEntry = false;
    }
    else //숫자 입력중일때는 
    {
        Display.Text += button.Content.ToString(); //새 숫자를 뒤에 붙임.
    }
}&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;해당 메서드는 입력된 연산자에 따라 계산결과를 화면에 출력하는 메서드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767961490442&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void Calculate(double newValue)
{
    switch (_currentOperator)
    {
        case &quot;+&quot;:
            _currentValue += newValue;
            break;
        case &quot;-&quot;:
            _currentValue -= newValue;
            break;
        case &quot;&amp;times;&quot;:
            _currentValue *= newValue;
            break;
        case &quot;&amp;divide;&quot;:
            _currentValue /= newValue;
            break;
    }
    Display.Text = _currentValue.ToString();
}&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;연산자 버튼을 누를때 수행되는 Operator_Click 메서드&lt;/p&gt;
&lt;pre id=&quot;code_1768020543378&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void Operator_Click(object sender, RoutedEventArgs e) //연산자 버튼을 누를때 수행
{
    Button button = sender as Button;
    if (double.TryParse(Display.Text, out double newValue)) //화면에 있는 값을 숫자로 변환
    {
        if (!_isNewEntry)
        {
            Calculate(newValue);
        }
        _currentOperator = button.Content.ToString(); //현재 연산자 저장

        if (_currentOperator == &quot;%&quot;)
        {
            double percentValue = _currentValue * (newValue / 100);
            Calculate(percentValue);
            _currentOperator = &quot;&quot;;
        }
        else
        {
            _currentValue = newValue;
        }
        _isNewEntry = 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;/p&gt;
&lt;pre id=&quot;code_1768021207849&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; private void Result_Click(object sender, RoutedEventArgs e)
 {
     if(double.TryParse(Display.Text, out double newValue))
     {
         Calculate(newValue); //계산 수행
         _currentOperator = &quot;&quot;; //연산자 초기화
         _isNewEntry = 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;&amp;lt;전체 코드&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768021220495&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// &amp;lt;summary&amp;gt;
    /// MainWindow.xaml에 대한 상호 작용 논리
    /// &amp;lt;/summary&amp;gt;
    public partial class MainWindow : Window
    {
        private double _currentValue = 0; // 현재 계산 값
        private string _currentOperator = &quot;&quot;; // 현재 연산자
        private bool _isNewEntry = true; // 새 입력 여부

        public MainWindow()
        {
            InitializeComponent(); // XAML UI 초기화
        }

        private void Number_Click(object sender, RoutedEventArgs e)
        {
            Button button = sender as Button;
            if (_isNewEntry)
            {
                Display.Text = button.Content.ToString();
                _isNewEntry = false;
            }
            else
            {
                Display.Text += button.Content.ToString();
            }
        }

        private void Operator_Click(object sender, RoutedEventArgs e) //연산자 버튼을 누를때 수행
        {
            Button button = sender as Button;
            if (double.TryParse(Display.Text, out double newValue)) //화면에 있는 값을 숫자로 변환
            {
                if (!_isNewEntry)
                {
                    Calculate(newValue);
                }
                _currentOperator = button.Content.ToString(); //현재 연산자 저장

                if (_currentOperator == &quot;%&quot;)
                {
                    double percentValue = _currentValue * (newValue / 100);
                    Calculate(percentValue);
                    _currentOperator = &quot;&quot;;
                }
                else
                {
                    _currentValue = newValue; //지금 화면에 있는 값을 피연산 값으로 저장
                }
                _isNewEntry = true;

            }
        }


        private void Calculate(double newValue)
        {
            switch (_currentOperator)
            {
                case &quot;+&quot;:
                    _currentValue += newValue;
                    break;
                case &quot;-&quot;:
                    _currentValue -= newValue;
                    break;
                case &quot;&amp;times;&quot;:
                    _currentValue *= newValue;
                    break;
                case &quot;&amp;divide;&quot;:
                    _currentValue /= newValue;
                    break;
            }
            Display.Text = _currentValue.ToString();
        }


        //지우기 버튼: 초기화
        private void Clear_Click(object sender, RoutedEventArgs e)
        {
            _currentValue = 0;
            _currentOperator = &quot;&quot;;
            Display.Text = &quot;0&quot;;
            _isNewEntry = true;
        }

        private void Backspace_Click(object sender, RoutedEventArgs e)
        {
            if (Display.Text.Length &amp;gt; 1)
            {
                Display.Text = Display.Text.Substring(0, Display.Text.Length - 1); //지우기

            }
            else
            {
                Display.Text = &quot;0&quot;; //화면의 숫자는 0으로 설정
                _isNewEntry = true; // 값을 새롭게 받음
            }
        }

        private void Point_Click(object sender, RoutedEventArgs e)
        {
            if (_isNewEntry) //새롭게 숫자를 받아야 하는데 .을 찍으면 0.으로 표기
            {
                Display.Text = &quot;0.&quot;;
                _isNewEntry = false;
                return;
            }

            if (!Display.Text.Contains(&quot;.&quot;))
            {
                Display.Text += &quot;.&quot;;
                
            }
        }

        private void Result_Click(object sender, RoutedEventArgs e)
        {
            if(double.TryParse(Display.Text, out double newValue))
            {
                Calculate(newValue); //계산 수행
                _currentOperator = &quot;&quot;; //연산자 초기화
                _isNewEntry = true; //새로운 값을 받을 준비
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1767961414073&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Window x:Class=&quot;WpfApp1.MainWindow&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        Title=&quot;Calculator&quot;
        Height=&quot;520&quot;
        Width=&quot;360&quot;
        WindowStartupLocation=&quot;CenterScreen&quot;
        ResizeMode=&quot;NoResize&quot;&amp;gt;

    &amp;lt;Grid Margin=&quot;10&quot;&amp;gt;
        &amp;lt;Grid.ColumnDefinitions&amp;gt;
            &amp;lt;ColumnDefinition Width=&quot;99*&quot;/&amp;gt;
            &amp;lt;ColumnDefinition/&amp;gt;
        &amp;lt;/Grid.ColumnDefinitions&amp;gt;
        &amp;lt;!-- 전체 레이아웃: 표시창 + 버튼 --&amp;gt;
        &amp;lt;Grid.RowDefinitions&amp;gt;
            &amp;lt;RowDefinition Height=&quot;100&quot;/&amp;gt;
            &amp;lt;RowDefinition Height=&quot;*&quot;/&amp;gt;
        &amp;lt;/Grid.RowDefinitions&amp;gt;

        &amp;lt;!-- ================= 표시창 ================= --&amp;gt;
        &amp;lt;TextBox x:Name=&quot;Display&quot;
                 Grid.Row=&quot;0&quot;
                 Text=&quot;0&quot;
                 FontSize=&quot;36&quot;
                 IsReadOnly=&quot;True&quot;
                 TextAlignment=&quot;Right&quot;
                 VerticalContentAlignment=&quot;Center&quot;
                 Padding=&quot;10&quot;
                 Margin=&quot;0,0,0,10&quot; Grid.ColumnSpan=&quot;2&quot;/&amp;gt;

        &amp;lt;!-- ================= 버튼 영역 ================= --&amp;gt;
        &amp;lt;Grid Grid.Row=&quot;1&quot; Grid.ColumnSpan=&quot;2&quot;&amp;gt;
            &amp;lt;Grid.RowDefinitions&amp;gt;
                &amp;lt;RowDefinition/&amp;gt;
                &amp;lt;RowDefinition/&amp;gt;
                &amp;lt;RowDefinition/&amp;gt;
                &amp;lt;RowDefinition/&amp;gt;
                &amp;lt;RowDefinition/&amp;gt;
            &amp;lt;/Grid.RowDefinitions&amp;gt;

            &amp;lt;Grid.ColumnDefinitions&amp;gt;
                &amp;lt;ColumnDefinition/&amp;gt;
                &amp;lt;ColumnDefinition/&amp;gt;
                &amp;lt;ColumnDefinition/&amp;gt;
                &amp;lt;ColumnDefinition/&amp;gt;
            &amp;lt;/Grid.ColumnDefinitions&amp;gt;

            &amp;lt;!-- 1행 --&amp;gt;
            &amp;lt;Button Content=&quot;C&quot;  Grid.Row=&quot;0&quot; Grid.Column=&quot;0&quot; Click=&quot;Clear_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;CE&quot; Grid.Row=&quot;0&quot; Grid.Column=&quot;1&quot; Click=&quot;Clear_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;⌫&quot;  Grid.Row=&quot;0&quot; Grid.Column=&quot;2&quot; Click=&quot;Backspace_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;&amp;divide;&quot;  Grid.Row=&quot;0&quot; Grid.Column=&quot;3&quot; Click=&quot;Operator_Click&quot;/&amp;gt;

            &amp;lt;!-- 2행 --&amp;gt;
            &amp;lt;Button Content=&quot;7&quot; Grid.Row=&quot;1&quot; Grid.Column=&quot;0&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;8&quot; Grid.Row=&quot;1&quot; Grid.Column=&quot;1&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;9&quot; Grid.Row=&quot;1&quot; Grid.Column=&quot;2&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;&amp;times;&quot; Grid.Row=&quot;1&quot; Grid.Column=&quot;3&quot; Click=&quot;Operator_Click&quot;/&amp;gt;

            &amp;lt;!-- 3행 --&amp;gt;
            &amp;lt;Button Content=&quot;4&quot; Grid.Row=&quot;2&quot; Grid.Column=&quot;0&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;5&quot; Grid.Row=&quot;2&quot; Grid.Column=&quot;1&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;6&quot; Grid.Row=&quot;2&quot; Grid.Column=&quot;2&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;-&quot; Grid.Row=&quot;2&quot; Grid.Column=&quot;3&quot; Click=&quot;Operator_Click&quot;/&amp;gt;

            &amp;lt;!-- 4행 --&amp;gt;
            &amp;lt;Button Content=&quot;1&quot; Grid.Row=&quot;3&quot; Grid.Column=&quot;0&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;2&quot; Grid.Row=&quot;3&quot; Grid.Column=&quot;1&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;3&quot; Grid.Row=&quot;3&quot; Grid.Column=&quot;2&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;+&quot; Grid.Row=&quot;3&quot; Grid.Column=&quot;3&quot; Click=&quot;Operator_Click&quot;/&amp;gt;

            &amp;lt;!-- 5행 --&amp;gt;
            &amp;lt;Button Content=&quot;0&quot; Grid.Row=&quot;4&quot; Grid.Column=&quot;1&quot; Click=&quot;Number_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;.&quot; Grid.Row=&quot;4&quot; Grid.Column=&quot;2&quot; Click=&quot;Point_Click&quot;/&amp;gt;
            &amp;lt;Button Content=&quot;=&quot; Grid.Row=&quot;4&quot; Grid.Column=&quot;3&quot; Click=&quot;Result_Click&quot;/&amp;gt;
        &amp;lt;/Grid&amp;gt;
    &amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>WPF</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/288</guid>
      <comments>https://developerwoohyeon.tistory.com/288#entry288comment</comments>
      <pubDate>Fri, 9 Jan 2026 17:06:59 +0900</pubDate>
    </item>
    <item>
      <title>[C#] 대리자(delegate)</title>
      <link>https://developerwoohyeon.tistory.com/287</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;delegate는 메서드를 값처럼 전달하기 위해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 메서드를 변수처럼 저장하고, 매개변수로 전달하거나, 다른 메서드로부터 반환받을 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;대리자 사용 방법&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;대리자 선언&lt;/b&gt; : 대리자 타입을 선언하고, 대리자가 &lt;u&gt;&lt;b&gt;참조할 메서드의 반환 타입과 매개변수를 정의한다.&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대리자 인스턴스화&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;대리자는 .NET 고급 프로그래밍의 핵심을 이루며, 콜백 메커니즘 구현, 이벤트 처리, 비동기 프로그래밍 등 다양한 영역에서 광범위하게 활용된다.&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;size16&quot;&gt;콜백 메커니즘은 프로그램에서 어떤 작업이 완료된 후 특정 동작을 실행할 수 있게 하는 편리한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;i&gt;&lt;b&gt;&lt;u&gt;대리자를 사용하는 콜백 메커니즘 구현&lt;/u&gt;&lt;/b&gt;&lt;/i&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;&amp;lt;대리자 사용 전 코드&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768025471926&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace ConsoleApp1
{
    public interface ICallback
    {
        void OnComplete();
    }

    public class CallbackHandler : ICallback
    {
        public void OnComplete()
        {
            Console.WriteLine(&quot;콜백 메서드 호출&quot;);
        }
    }

    public class WithoutDelegate 
    { 
        public void ProcessTask(ICallback callback)
        {
            Console.WriteLine(&quot;Task is being processed&quot;);

            callback.OnComplete();
        }
    }


    public class Program
    {
        
        static void Main(string[] args)
        {
            WithoutDelegate wd = new WithoutDelegate();
            ICallback callback = new CallbackHandler();

            wd.ProcessTask(callback);
        }
    }
}&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;&lt;b&gt;&amp;lt;대리자를 사용한 콜백 메커니즘&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768028169579&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace ConsoleApp1
{
    public delegate void Callback();

    public class WithDelegate
    {
        public void ProcessTask(Callback callback) //대리자를 매개변수로 받는 메서드
        {
            Console.WriteLine(&quot;Task is being processed&quot;);

            //콜백 메서드 실행
            callback?.Invoke(); //?.는 null이면 호출X null이 아니면 호출
        }
    }
    public class Program
    {
        public static void MyCallback() //콜백 메서드
        {
            Console.WriteLine(&quot;Task completed. Now running the callback.&quot;);
        }

        static void Main(string[] args)
        {
            WithDelegate wd = new WithDelegate();

            //Callback callback = () =&amp;gt; Console.WriteLine(&quot;Task completed. Now running the callback.&quot;);
            Callback callback = new Callback(MyCallback);

            wd.ProcessTask(callback);
        }
    }
}&lt;/code&gt;&lt;/pre&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;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;&lt;b&gt;&amp;lt;인터페이스 기반 이벤트 처리&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768029988102&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace ConsoleApp1
{
    public interface IEventHandler
    {
        void HandleEvent();
    }

    public class EventHandler1 : IEventHandler
    {
        public void HandleEvent()
        {
            Console.WriteLine(&quot;EventHadler1 called&quot;);
        }
    }
    public class EventHandler2 : IEventHandler
    {
        public void HandleEvent()
        {
            Console.WriteLine(&quot;EventHadler2 called&quot;);
        }
    }

    public class WithoutDelegate
    {
        private List&amp;lt;IEventHandler&amp;gt; subscribers = new List&amp;lt;IEventHandler&amp;gt;();
        public void Subscribe(IEventHandler handler)
        {
            subscribers.Add(handler);
        }

        public void Unsubscribe(IEventHandler handler)
        {
            subscribers.Remove(handler);
        }
        public void TriggerEvent()
        {
            foreach (var handler in subscribers)
            {
                handler.HandleEvent();
            }
        }
        public class Program
        {
            static void Main(string[] args)
            {
                var publisher = new WithoutDelegate();

                var handler1 = new EventHandler1();
                var handler2 = new EventHandler2();

                publisher.Subscribe(handler1);
                publisher.Subscribe(handler2);

                publisher.TriggerEvent();

                publisher.Unsubscribe(handler1);

                publisher.TriggerEvent();
            }
        }
    }
}&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;&amp;lt;대리자 기반의 이벤트 처리&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768031870373&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace ConsoleApp1
{
    public class WithDelegate
    {
        public event Action MyEvent;

        public void TriggerEvent()
        {
            MyEvent?.Invoke();
        }
    }
    public class Program
    {
        static void Main(string[] args) 
        {
            WithDelegate wd = new WithDelegate();

            // 이벤트 핸들러, Action에 람다식(익명 메서드)을 대입
            Action handler1 = () =&amp;gt; Console.WriteLine(&quot;Event1 triggered!&quot;);
            Action handler2 = () =&amp;gt; Console.WriteLine(&quot;Event2 triggered!&quot;);

            // 이벤트 구독
            wd.MyEvent += handler1;
            wd.MyEvent += handler2;

            // 이벤트 발생
            wd.TriggerEvent();

            // 이벤트 구독 해제
            wd.MyEvent -= handler2;

            // 이벤트 발생
            wd.TriggerEvent();
        }
    }
}&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;&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;&amp;lt;사용 예제&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768024285131&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Program
{
    // 대리자 선언
    public delegate void MyDelegate(string message);

    // 대리자가 참조할 메서드
    public static void DisplayMessage(string message)
    {
        Console.WriteLine(message);
    }

    static void Main(string[] args)
    {
        // 대리자 인스턴스화
        MyDelegate del = new MyDelegate(DisplayMessage);

        // 대리자 호출
        del(&quot;안녕하세요~~&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1767857015121&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Project1;
using System;
using System.Diagnostics;



class Program
{
    delegate int Calc(int a, int b);

    int Add(int a, int b)
    {
        return a + b;
    }

    int Sub(int a, int b)
    {
        return a - b;
    }

    void Execute(Calc calc)
    {
        Console.WriteLine(calc(10, 5));
    }
    static void Main(string[] args)
    {
        Program p = new Program();

        Calc add = new Calc(p.Add);
        Calc sub = new Calc(p.Sub);

        p.Execute(add);
        p.Execute(sub);
    }
}&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;pre id=&quot;code_1767858762841&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Mathematics
{
    delegate int CalcDelegate(int x, int y);

    static int Add(int x, int y) { return x + y; }
    static int Sub(int x, int y) { return x - y; }
    static int Multiply(int x, int y) { return x * y; }
    static int Divide(int x, int y) { return x / y; }

    CalcDelegate[] methods;

    public Mathematics()
    {
        methods = new CalcDelegate[] { Add, Sub, Multiply, Divide };
    }

    public void Calculate(char opCode, int operand1, int operand2)
    {
        switch (opCode)
        {
            case '+':
                Console.WriteLine(methods[0](operand1,operand2));
                break;
            case '-':
                Console.WriteLine(methods[1](operand1, operand2));
                break;
            case '*':
                Console.WriteLine(methods[2](operand1, operand2));
                break;
            case '/':
                Console.WriteLine(methods[3](operand1, operand2));
                break;
        }
    }

    class Program
    {
        delegate void WorkDelegate(char arg1, int arg2, int arg3);
        public static void Main()
        {
            Mathematics math = new Mathematics();
            WorkDelegate work = math.Calculate;

            work('+', 10, 5);

        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/287</guid>
      <comments>https://developerwoohyeon.tistory.com/287#entry287comment</comments>
      <pubDate>Thu, 8 Jan 2026 17:01:44 +0900</pubDate>
    </item>
    <item>
      <title>[C#] Collection / Generic Collection</title>
      <link>https://developerwoohyeon.tistory.com/278</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Collection&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection은 여러 자료구조를 공통된 인터페이스와 규칙으로 묶어 놓은 &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;.Net 프레임워크에서 비제네릭 컬렉션 클래스를 담당하는 대표적인 네임스페이스이다.&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;Collection 주요 클래스&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;ArrayList : 크기가 가변적인 배열, 인덱스를 통해 데이터에 접근 가능&lt;/li&gt;
&lt;li&gt;Hashtable : Key-Value 저장&lt;/li&gt;
&lt;li&gt;Queue : FIFO&lt;/li&gt;
&lt;li&gt;Stack : LIFO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767502173534&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	// ArrayList 예제
        ArrayList al = new ArrayList();
        al.Add(10);
        al.Add(&quot;Hello&quot;);

        foreach(var item in al){
            Console.WriteLine(item);
        }

        // Hashtable 예제
        Hashtable table = new Hashtable();

        table[&quot;id&quot;] = 1;
        table[&quot;name&quot;] = &quot;우현&quot;;

        Console.WriteLine(&quot;ID: &quot; + table[&quot;id&quot;]);
        Console.WriteLine(&quot;Name: &quot; + table[&quot;name&quot;]);

        // Queue 예제
        Queue queue = new Queue();

        queue.Enqueue(1);
        queue.Enqueue(&quot;Hello&quot;);

        Console.WriteLine(queue.Dequeue());
        Console.WriteLine(queue.Dequeue());

        // Stack 예제
        Stack stack = new Stack();

        stack.Push(1);
        stack.Push(2);

        Console.WriteLine(stack.Pop());
        Console.WriteLine(stack.Pop());&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Generic Collection&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generic Collection은 Collection과 다르게 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;u&gt;&lt;b&gt;데이터 타입을 필수적으로 명시해야 한다.&lt;/b&gt;&lt;/u&gt;&lt;/span&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;박싱과 언박싱의 과정이 이루어지지 않기 때문에 성능이 올라간다.&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;Generic Collection 주요 클래스&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;b&gt;List&amp;lt;T&amp;gt;&lt;/b&gt; : 가변 크기 배열, 인덱스로 데이터에 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;Add(T item)&lt;/td&gt;
&lt;td&gt;맨 뒤에 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삽입&lt;/td&gt;
&lt;td&gt;Insert(int index, T item)&lt;/td&gt;
&lt;td&gt;중간 삽입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;Remove(T item)&lt;/td&gt;
&lt;td&gt;값으로 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;RemoveAt(int index)&lt;/td&gt;
&lt;td&gt;인덱스로 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회&lt;/td&gt;
&lt;td&gt;list[index]&lt;/td&gt;
&lt;td&gt;인덱스 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;Contains(T item)&lt;/td&gt;
&lt;td&gt;포함 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;IndexOf(T item)&lt;/td&gt;
&lt;td&gt;인덱스 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;요소 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화&lt;/td&gt;
&lt;td&gt;Clear()&lt;/td&gt;
&lt;td&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;&amp;nbsp;&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;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/b&gt; :&amp;nbsp; Key-Value 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;Add(key, value)&lt;/td&gt;
&lt;td&gt;Key-Value 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가/수정&lt;/td&gt;
&lt;td&gt;dict[key] = value&lt;/td&gt;
&lt;td&gt;없으면 추가, 있으면 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회&lt;/td&gt;
&lt;td&gt;dict[key]&lt;/td&gt;
&lt;td&gt;Key로 값 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;Remove(key)&lt;/td&gt;
&lt;td&gt;Key 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검사&lt;/td&gt;
&lt;td&gt;ContainsKey(key)&lt;/td&gt;
&lt;td&gt;Key 존재 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검사&lt;/td&gt;
&lt;td&gt;ContainsValue(value)&lt;/td&gt;
&lt;td&gt;Value 존재 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 키&lt;/td&gt;
&lt;td&gt;Keys&lt;/td&gt;
&lt;td&gt;Key 컬렉션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 값&lt;/td&gt;
&lt;td&gt;Values&lt;/td&gt;
&lt;td&gt;Value 컬렉션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&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;&amp;nbsp;&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;Queue&amp;lt;T&amp;gt;, Stack&amp;lt;T&amp;gt; :&amp;nbsp; Collection의 큐,스택과 동일한 저장방식, 다만 자료형을 미리 명시 해줘야 함.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue&amp;lt;T&amp;gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;Enqueue(T item)&lt;/td&gt;
&lt;td&gt;뒤에 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제거&lt;/td&gt;
&lt;td&gt;Dequeue()&lt;/td&gt;
&lt;td&gt;앞에서 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회&lt;/td&gt;
&lt;td&gt;Peek()&lt;/td&gt;
&lt;td&gt;제거 없이 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;요소 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화&lt;/td&gt;
&lt;td&gt;Clear()&lt;/td&gt;
&lt;td&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;Stack&amp;lt;T&amp;gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;Push(T item)&lt;/td&gt;
&lt;td&gt;맨 위에 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제거&lt;/td&gt;
&lt;td&gt;Pop()&lt;/td&gt;
&lt;td&gt;맨 위 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회&lt;/td&gt;
&lt;td&gt;Peek()&lt;/td&gt;
&lt;td&gt;제거 없이 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;요소 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화&lt;/td&gt;
&lt;td&gt;Clear()&lt;/td&gt;
&lt;td&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;&amp;nbsp;&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;HashSet&amp;lt;T&amp;gt; :&amp;nbsp; 중복 데이터 저장 허용X, 데이터 존재 여부를 빠르게 검사 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;Add(T item)&lt;/td&gt;
&lt;td&gt;중복 시 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;Remove(T item)&lt;/td&gt;
&lt;td&gt;값 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검사&lt;/td&gt;
&lt;td&gt;Contains(T item)&lt;/td&gt;
&lt;td&gt;존재 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;집합&lt;/td&gt;
&lt;td&gt;UnionWith()&lt;/td&gt;
&lt;td&gt;합집합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;집합&lt;/td&gt;
&lt;td&gt;IntersectWith()&lt;/td&gt;
&lt;td&gt;교집합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;집합&lt;/td&gt;
&lt;td&gt;ExceptWith()&lt;/td&gt;
&lt;td&gt;차집합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;요소 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화&lt;/td&gt;
&lt;td&gt;Clear()&lt;/td&gt;
&lt;td&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System.Collections.SortedList : 키값을 기준으로 정렬된 상태를 유지하며 저장(Add를 통해 데이터를 추가하면 자동으로 정렬 수행)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>C#</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/278</guid>
      <comments>https://developerwoohyeon.tistory.com/278#entry278comment</comments>
      <pubDate>Sun, 4 Jan 2026 15:35:03 +0900</pubDate>
    </item>
    <item>
      <title>Artillery 애플리케이션 부하 테스트</title>
      <link>https://developerwoohyeon.tistory.com/276</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxQWZS/dJMcadmO3lP/Wfbm60CKXua0MY6ole84AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxQWZS/dJMcadmO3lP/Wfbm60CKXua0MY6ole84AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxQWZS/dJMcadmO3lP/Wfbm60CKXua0MY6ole84AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxQWZS%2FdJMcadmO3lP%2FWfbm60CKXua0MY6ole84AK%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;300&quot; height=&quot;168&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Artillery란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Artillery는 node.js기반의 애플리케이션 부하 테스트 도구이며, HTTP,HTTPS, Websocket등의 &lt;b&gt;다양한 프로토콜을 지원&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 테스트 시나리오를 JSON 또는 YAML파일로 작성하여 테스트 하는 점이 특징이다.&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;기존에 사용하던 JMeter와 비교했을때 설치가 쉽지만 테스트 케이스를 YAML/JSON로 직접 작성해야 된다는 단점이 있다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 부분은 어느정도 사용해보면 적응할 수 있을 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Artillery 설치 방법&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1763550677139&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Set up Artillery CLI &amp;middot; Artillery&quot; data-og-description=&quot;Learn how to get up and running with Artillery with reference docs, guides, tutorials and other resources.&quot; data-og-host=&quot;www.artillery.io&quot; data-og-source-url=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot; data-og-url=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/23BmA/hyZN3V3RrE/9e39wik9xVKWAlkyZmaL9K/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/eTDIy/hyZNAUsZZz/2LZlCqfqhwiaiJgdfYa5L0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/23BmA/hyZN3V3RrE/9e39wik9xVKWAlkyZmaL9K/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/eTDIy/hyZNAUsZZz/2LZlCqfqhwiaiJgdfYa5L0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Set up Artillery CLI &amp;middot; Artillery&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to get up and running with Artillery with reference docs, guides, tutorials and other resources.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.artillery.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js가 미리 설치 되어있는 환경에서 위 홈페이지로 들어가면 나오는 명령어를 터미널에 입력해주면 다운로드가 진행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1763550951177&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g artillery@latest&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 시나리오 작성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 시나리오를 작성할때는 가상 사용자 수, 요청횟수, 초당 요청 수, 테스트 URL, HTTP Method 등을 선택할 수 있다.&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;이번 테스트에서 작성한 YAML파일은 다음과 같다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;config:
  target: &quot;서버 주소&quot;  # 테스트를 보낼 서버 주소
  phases:
    - duration: 100     # 테스트 실행 시간(초)
      arrivalRate: 10   # 시작 시 초당 요청 수
      rampTo: 100       # 점진적으로 증가할 최대 초당 요청 수
  payload:
    path: &quot;urls.csv&quot;    # 사용할 CSV 파일
    fields:
      - &quot;url&quot;           # CSV의 컬럼명 &amp;rarr; {{ url }} 변수로 사용

scenarios:
  - name: &quot;create shortenUrl&quot;   # 시나리오 이름
    flow:
      - post:
          url: &quot;/shortenUrl&quot;    # 호출할 API 경로
          json:
            originalUrl: &quot;{{ url }}&quot;   # CSV에서 읽은 url 값을 요청 body에 삽입&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 시나리오를 작성하면 테스트 수행 후 결과를 확인해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 바로 결과를 확인할 수도 있고, 테스트 결과를 파일로 저장할 수도 있지만, Artillery에 회원가입 후 API 키값을 이용하면 테스트 결과를 Artillery 홈페이지에서 테스트 결과를 다양한 시각화 자료와 함께 확인 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1763552885928&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;artillery run read-load-test.yaml --record --key &amp;lt;API_KEY&amp;gt;
# read-load-test.yaml 파일을 실행하고 실행 결과를 Artillery Cloud 대시보드에 기록(업로드)함

artillery run read-load-test.yaml -o read-load-report.json
# read-load-test.yaml 테스트 실행 후 결과를 read-load-report.json 파일로 로컬에 저장

artillery report --output read-load-report.html read-load-report.json
# read-load-report.json 파일을 html 리포트로 변환&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0KnR/dJMcacVN8xK/9lEKhz9iMrQoFZ5JfHVkk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0KnR/dJMcacVN8xK/9lEKhz9iMrQoFZ5JfHVkk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0KnR/dJMcacVN8xK/9lEKhz9iMrQoFZ5JfHVkk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw0KnR%2FdJMcacVN8xK%2F9lEKhz9iMrQoFZ5JfHVkk0%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;553&quot; height=&quot;480&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;min : 가장 빠른 응답 시간&lt;/li&gt;
&lt;li&gt;max : 가장 느린 응답 시간&lt;/li&gt;
&lt;li&gt;mean : 평균 응답시간&lt;/li&gt;
&lt;li&gt;median : 응답시간을 빠른 순으로 정렬했을 때 정확히 중간값&lt;/li&gt;
&lt;li&gt;p95 : 상위 95%가 이 시간 이하로 응답(5%는 이 시간보다 느렸다.)&lt;/li&gt;
&lt;li&gt;p99 : 상위 99%가 이 시간 이하로 응답(1%는 이 시간보다 느렸다.)&lt;/li&gt;
&lt;li&gt;vusers.failed : 실패한 가상 사용자&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Artillery 대시보드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Artillery에서 발급해주는 API Key값을 이용해 Artillery 대시보드에서 테스트 결과를 확인해볼 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Artillery 로그인&lt;/li&gt;
&lt;li&gt;Settings -&amp;gt; API Key 확인&lt;/li&gt;
&lt;li&gt;터미널에서 artillery run read-load-test.yaml --record --key &amp;lt;API_KEY&amp;gt; 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgwQFf/dJMcabbwJpr/sIeGqDkWeGojpNMf035CY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgwQFf/dJMcabbwJpr/sIeGqDkWeGojpNMf035CY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgwQFf/dJMcabbwJpr/sIeGqDkWeGojpNMf035CY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgwQFf%2FdJMcabbwJpr%2FsIeGqDkWeGojpNMf035CY0%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;745&quot; height=&quot;485&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BackEnd</category>
      <category>artillery</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/276</guid>
      <comments>https://developerwoohyeon.tistory.com/276#entry276comment</comments>
      <pubDate>Wed, 19 Nov 2025 22:22:57 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] N+1 문제</title>
      <link>https://developerwoohyeon.tistory.com/275</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC09Ia/dJMcacBqnTA/cX51dSirgWGGOXEgTrl681/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC09Ia/dJMcacBqnTA/cX51dSirgWGGOXEgTrl681/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC09Ia/dJMcacBqnTA/cX51dSirgWGGOXEgTrl681/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC09Ia%2FdJMcacBqnTA%2FcX51dSirgWGGOXEgTrl681%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;300&quot; height=&quot;168&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 문제는 ORM 기술에서 특정 객체를 대상으로 수행한 쿼리가 해당 객체가 가지고 있는 연관관계 또한 조회하게 되면서 &lt;u&gt;&lt;b&gt;N번의 추가적인 쿼리가 발생하는 문제&lt;/b&gt;&lt;/u&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;N+1문제로 인해 불필요하게 많은 쿼리를 보내면 성능 저하로 이어질 수 있기 때문에 적절한 방법을 통해 N+1을 예방하는 것이 중요하다.&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&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;(1:N)&lt;/b&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYLsCX/dJMcadG6dnR/I4YmAVElGi7qkIvkO2ukq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYLsCX/dJMcadG6dnR/I4YmAVElGi7qkIvkO2ukq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYLsCX/dJMcadG6dnR/I4YmAVElGi7qkIvkO2ukq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYLsCX%2FdJMcadG6dnR%2FI4YmAVElGi7qkIvkO2ukq1%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;341&quot; height=&quot;353&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;1248&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;&lt;b&gt;&amp;lt;CountryEntity&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Getter
@Setter
public class CountryEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String country;

    @OneToMany(mappedBy = &quot;countryEntity&quot;)
    private List&amp;lt;CityEntity&amp;gt; cityEntities = new ArrayList&amp;lt;&amp;gt;();

    public void addCityEntity(CityEntity cityEntity) {
        cityEntities.add(cityEntity);
        cityEntity.setCountryEntity(this);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;&amp;lt;CityEntity&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Getter
@Setter
public class CityEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String city;

    @ManyToOne
    private CountryEntity countryEntity;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;아래와 같이 3개의 Country와 각각의 City를 생성해주고 JPA를 이용해 조회를 했을때 쿼리가 몇번 수행되는지 확인해보면 N+1 문제가 발생하는지 확인해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1762492709133&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;korea : seoul, busan, daegu
france : paris
usa : newyork, chicago&lt;/code&gt;&lt;/pre&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;1. 국가(부모)를 모두 조회&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3s7N7/dJMcae0jkwN/ICKnTpzT25kYF7BJo4mbL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3s7N7/dJMcae0jkwN/ICKnTpzT25kYF7BJo4mbL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3s7N7/dJMcae0jkwN/ICKnTpzT25kYF7BJo4mbL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3s7N7%2FdJMcae0jkwN%2FICKnTpzT25kYF7BJo4mbL1%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;536&quot; height=&quot;33&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가 리스트(CountryEntity)를 조회할 때는 단순히 부모 엔티티만 가져오는 작업이기 때문에 데이터베이스에 쿼리가 한 번만 전달된다. 뷰 템플릿에서도 국가 정보만 출력하고 연관된 도시 컬렉션을 접근하지 않기 때문에 추가적인 쿼리는 발생하지 않는다.&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;2. 국가(부모) findAll&amp;nbsp; &lt;b&gt;&amp;rarr; 도시&lt;/b&gt;(자식) 접근&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n2S8L/dJMcaelHNJx/LRYuK9qOd7qYKKjG8oZr41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n2S8L/dJMcaelHNJx/LRYuK9qOd7qYKKjG8oZr41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n2S8L/dJMcaelHNJx/LRYuK9qOd7qYKKjG8oZr41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn2S8L%2FdJMcaelHNJx%2FLRYuK9qOd7qYKKjG8oZr41%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;987&quot; height=&quot;124&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가와 함께 각 국가에 속한 도시 목록까지 조회하려고 하면 상황이 달라진다. 먼저 국가 목록을 가져오는 쿼리가 한 번 실행되고, 이후 템플릿에서 cityEntities 컬렉션에 접근하는 순간 각 국가마다 해당 도시에 대한 지연 로딩 쿼리가 추가로 발생한다. 그 결과 국가 수(N)만큼 도시 조회 쿼리가 반복 실행되어 &lt;b&gt;총 1 + N개의 쿼리&lt;/b&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;3. 도시(자식)만 조회&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZD8p2/dJMcakzsqr2/EkrC6YSVI7i6aGZksyorS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZD8p2/dJMcakzsqr2/EkrC6YSVI7i6aGZksyorS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZD8p2/dJMcakzsqr2/EkrC6YSVI7i6aGZksyorS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZD8p2%2FdJMcakzsqr2%2FEkrC6YSVI7i6aGZksyorS1%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;819&quot; height=&quot;57&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;84&quot; data-ke-size=&quot;size16&quot;&gt;도시 엔티티를 단독으로 조회하더라도, @ManyToOne의 기본 Fetch 전략이 &lt;b&gt;EAGER&lt;/b&gt;이기 때문에 연관된 국가 엔티티도 즉시 함께 조회된다. 그 결과, 국가 정보를 조회할 의도가 없어도 JPA가 부모 엔티티를 자동으로 불러오기 위해 &lt;b&gt;추가 쿼리를 한 번 더 실행&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;84&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;283&quot; data-start=&quot;84&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lazy, Eager&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 연관관계를 가진 엔티티를 &lt;b&gt;언제 DB로 불러올지 결정&lt;/b&gt;하는 로딩 전략(Lazy,Eager)이 존재한다.&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;Lazy Loading (지연 로딩)&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;필요할 때까지 연관 엔티티를 DB에서 가져오지 않고 실제로 접근할 때 쿼리 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Eager Loading(즉시 로딩)&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; JPA JOIN별 default 값&lt;/b&gt;&lt;br /&gt;- &lt;b&gt;OneToOne&lt;/b&gt; : Eager&lt;br /&gt;- &lt;b&gt;ManyToOne&lt;/b&gt; : Eager&lt;br /&gt;- &lt;b&gt;OneToMany&lt;/b&gt; : Lazy&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;앞서 도시(자식)을 조회할때 국가(부모)를 조회하는 쿼리가 추가된 이유도 도시에 대한 연관 엔티티(국가)가 ManyToOne조인의 default값 Eager로 되어있었기 때문에 자동으로 조회 쿼리가 실행 되었다.&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;여기서 아래 코드와 같이 ManyToOne의 FetchType을 Lazy(지연 로딩)방식으로 변경해주면 연관 엔티티에 대한 접근이 없다면 쿼리를 추가적으로 실행하지 않게 된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
private CountryEntity countryEntity;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn76jj/dJMcafkB8hl/CEk4NTzk2SiV4a7lLiXfV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn76jj/dJMcafkB8hl/CEk4NTzk2SiV4a7lLiXfV1/img.png&quot; data-alt=&quot;도시에 대한 조회 쿼리만 실행된것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn76jj/dJMcafkB8hl/CEk4NTzk2SiV4a7lLiXfV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn76jj%2FdJMcafkB8hl%2FCEk4NTzk2SiV4a7lLiXfV1%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;828&quot; height=&quot;51&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도시에 대한 조회 쿼리만 실행된것을 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;연관관계 쿼리 단일화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 엔티티 조회 후 연관된 엔티티 접근 시 발생하는 추가 쿼리를 단건의 쿼리로 단일화 시키는 방법에는 다음과 같은 방법이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPQL @Query로 JOIN FETCH 작성&lt;/li&gt;
&lt;li&gt;QueryDSL로 fetchJoin()&lt;/li&gt;
&lt;li&gt;@EntityGraph&lt;/li&gt;
&lt;li&gt;Lazy 쪽 조회시 Batch 설정을 통한 IN절&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;JPQL JOIN FETCH 작성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN FETCH란 연관된 엔티티를 즉시 조인하여 한 번의 쿼리로 함께 로딩하는 JPQL 문법이다.&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;&amp;lt;기본 문법&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762498607422&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT e
FROM Parent e
JOIN FETCH e.children&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;왜냐하면 국가(부모)테이블의 연관 테이블(도시)가 OneToMany로 연결되어있었고 OneToMany의 디폴트 로딩값이 Lazy로딩이기 때문이다.&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;이 상황은 JPQL의 JOIN FETCH를 통해 해결 가능하다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;@Query(&quot;SELECT co FROM CountryEntity co &quot; + &quot;JOIN FETCH co.cityEntities ci&quot;)
List&amp;lt;CountryEntity&amp;gt; findAllFetch();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN FETCH를 통해 국가와 도시를 모두 한번에 로딩함으로써 추가 쿼리가 발생하지 않도록 할 수 있다.&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번의 쿼리만 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1305&quot; data-origin-height=&quot;34&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ExMyl/dJMcahCJuL9/6Scq3aaTa1mR680kutxZ70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ExMyl/dJMcahCJuL9/6Scq3aaTa1mR680kutxZ70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ExMyl/dJMcahCJuL9/6Scq3aaTa1mR680kutxZ70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FExMyl%2FdJMcahCJuL9%2F6Scq3aaTa1mR680kutxZ70%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;1305&quot; height=&quot;34&quot; data-origin-width=&quot;1305&quot; data-origin-height=&quot;34&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;하지만 이렇게 JOIN FETCH만 사용한다면 누락되는 데이터가 존재할 수 있다.&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;국가에 Germany를 넣고 도시를 추가하지 않게 되면 조회시 Germany가 조회되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(OneToMany에서 One은 존재하는데, Many가 존재하지 않는 경우)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf4vAS/dJMcaj1Crzn/b0lNe7cG7AsKa2t3nLM811/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf4vAS/dJMcaj1Crzn/b0lNe7cG7AsKa2t3nLM811/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf4vAS/dJMcaj1Crzn/b0lNe7cG7AsKa2t3nLM811/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf4vAS%2FdJMcaj1Crzn%2Fb0lNe7cG7AsKa2t3nLM811%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;292&quot; height=&quot;117&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면&amp;nbsp; 그냥 &lt;span&gt;&lt;b&gt;JOIN FETCH&lt;/b&gt;만 사용할 경우 &lt;b&gt;INNER JOIN&lt;/b&gt;으로 연관된게 있는 교집합이어야 조회가 되기 때문이다.&lt;/span&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;span&gt;만약 교집합이 안되는 데이터도 조회하고 싶다면 &lt;b&gt;LEFT JOIN FETCH을 사용하면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(&quot;SELECT co FROM CountryEntity co &quot;+&quot;LEFT JOIN FETCH co.cityEntities ci&quot;)
List&amp;lt;CountryEntity&amp;gt; findAllFetch();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wlx5j/dJMcag4Twbk/SofpgtRld4EtjE3n1lqp61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wlx5j/dJMcag4Twbk/SofpgtRld4EtjE3n1lqp61/img.png&quot; data-alt=&quot;LEFT JOIN FETCH&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wlx5j/dJMcag4Twbk/SofpgtRld4EtjE3n1lqp61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWlx5j%2FdJMcag4Twbk%2FSofpgtRld4EtjE3n1lqp61%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;232&quot; height=&quot;100&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LEFT JOIN FETCH&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;다중 OneToMany Fetch 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 엔티티에 2개 이상의 OneToMany가 포함되어 있는데 Fetch 조회를 할 경우 예외가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1767&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw08PN/dJMcaiBDLqH/2pFFjOK6ZzfLTVMqbvUg8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw08PN/dJMcaiBDLqH/2pFFjOK6ZzfLTVMqbvUg8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw08PN/dJMcaiBDLqH/2pFFjOK6ZzfLTVMqbvUg8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw08PN%2FdJMcaiBDLqH%2F2pFFjOK6ZzfLTVMqbvUg8K%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;1767&quot; height=&quot;253&quot; data-origin-width=&quot;1767&quot; data-origin-height=&quot;253&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;왜냐하면 JPA의 구현체인 Hibernate에서 2개 이상의 OneToMany Fetch를 강제로 막고 있기 때문이다.&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;여러개의 OneToMany를 Fetch Join하게 되면 조인 결과가 &lt;b&gt;카티션 곱(Cartesian Product)&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기준 엔티티의 OneToMany 필드에서 Set을 사용하여 중복을 제거&lt;/li&gt;
&lt;li&gt;한쪽만 Fetch, 나머지는 in절 : 이렇게 되면 1+N 쿼리는 아니지만 1+소수 개의 쿼리로 문제를 해결 가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(&quot;SELECT co FROM CountryEntity co &quot;+
        &quot;LEFT JOIN FETCH co.cityEntities ci &quot;+
        &quot;LEFT JOIN  co.religionEntities re&quot;)
List&amp;lt;CountryEntity&amp;gt; findAllFetch();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;@BatchSize(size = 250) //Lazy로딩할 때 한번에 250개씩 묶어서 로딩
@OneToMany(mappedBy = &quot;countryEntity&quot;)
private List&amp;lt;ReligionEntity&amp;gt; religionEntities = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 BatchSize를 250개로 설정하면 250개의 국가에 대해서 religionEntities 조회 쿼리는 단 1번만 실행된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OneToMany fetch시 페이지네이션&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(&quot;SELECT co FROM CountryEntity co &quot;+
        &quot;LEFT JOIN FETCH co.cityEntities ci &quot;)
List&amp;lt;CountryEntity&amp;gt; findAllFetch(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;One쪽 엔티티를 기준으로 OneToMany fetch시 페이지네이션을 함께 수행하게 되면 조회는 정상적으로 수행 되지만, &lt;b&gt;경고&lt;/b&gt;가 발생하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762513042878&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; WARN 4334 --- [1plusNtest] [nio-8080-exec-1] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory&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;경고문이 발생하는 이유는 Fethch Join이 적용된 상태에서 페이징을 수행하게 되면 Hibernate가 DB에 Limit/OFFSET을 적용할 수 없기 때문에 &lt;b&gt;모든 데이터를 조회한 뒤, 메모리에서 강제로 페이징을 수행&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;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;b&gt;batch in&lt;/b&gt; 절을 사용하여 1+N 개의 쿼리 중 N개의 쿼리에 대해 batch단위로 묶어 &lt;b&gt;1+소수개의 쿼리로 성능을 최적화 할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@BatchSize(size = 250)
@OneToMany(mappedBy = &quot;countryEntity&quot;)
private List&amp;lt;CityEntity&amp;gt; cityEntities = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;lt;기존의 Fetch Join방법 대신 findAll로 조회&amp;gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;CountryEntity&amp;gt; readCountryFetch(){
        Pageable pageable = PageRequest.of(0,5);
//      return countryRepository.findAllFetch(pageable);
        return countryRepository.findAll(pageable).stream().toList();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 실행 시 국가(부모)에 대한 목록을 조회하는 쿼리 실행 후 batch in을 통해 현재 페이지에 담긴 여러 국가들의 도시(자식)들을 BatchSize만큼 묶어서 일괄 조회하는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1762514439038&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hibernate: select ce1_0.id,ce1_0.country from CountryEntity ce1_0 limit ?,?
Hibernate: select ce1_0.countryEntity_id,ce1_0.id,ce1_0.city from CityEntity ce1_0 where ce1_0.countryEntity_id in (?......?)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단순하게 One(부모) 목록만 보여주고 연관된 Many(자식)한테는 접근을 안함&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;Lazy로딩 사용해도 무방하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;List&amp;lt;One&amp;gt; 목록 및 각 연관 Many로 접근 함&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 페이지네이션이 들어가는 경우 :&amp;nbsp; @BatchSize를 통한 IN절 쿼리 수행&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;- 다중 OneToMany 상황에서 각각의 OneToMany 데이터가 다 필요한 경우 : 하나만 JOIN FETCH, 나머지는 @BatchSize를 통한 IN절 쿼리 수행 OR 전체 @BatchSize를 통한 IN절 쿼리 수행&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;- SQL 조건으로 가져온 데이터가 다 필요한 경우 OR List&amp;lt;One&amp;gt;의 사이즈가 작고 Many로 접근 함 : JOIN FETCH 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Database</category>
      <category>JPA</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/275</guid>
      <comments>https://developerwoohyeon.tistory.com/275#entry275comment</comments>
      <pubDate>Fri, 7 Nov 2025 17:01:38 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 예외 처리 전략 [Custom Exception, ExecptionHandler]</title>
      <link>https://developerwoohyeon.tistory.com/273</link>
      <description>&lt;h3 id=&quot;-왜-custom-exception을-사용하는가&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Custom Exception&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커스텀 예외&lt;/b&gt;란 자바의 기본 예외(RuntimeException, IllegalArgumentException 등)를 상속받아 &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;try-catch&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;기본 예외를 서비스 계층에서 던지게 구현을 했다면 컨트롤러에서 try-catch문을 통해 모두 예외 상황에 맞는 HTTP 상태코드를 지정해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761110914649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/users&quot;)
public class UserController {

    private final UserService userService;

    @GetMapping(&quot;/{id}&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; getUser(@PathVariable Long id) {
        try {
            User user = userService.getUser(id);
            return ResponseEntity.ok(user);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of(&quot;message&quot;, e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of(&quot;message&quot;, &quot;서버 오류가 발생했습니다.&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;코드 중복 문제를 해결하고, 일관된 상태 메시지를 제공&lt;/b&gt;하기 위해 Custom Exception과 ExecptionHandler를 사용할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 과정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;BaseException&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BaseException extends RuntimeException {

    HttpStatus statusCode;
    String responseMessage;

    public BaseException(HttpStatus statusCode) {
        super();
        this.statusCode = statusCode;
    }

    public BaseException(HttpStatus statusCode, String responseMessage) {
        super(responseMessage);
        this.statusCode = statusCode;
        this.responseMessage = responseMessage;
    }

    public BaseException(ErrorStatus errorStatus) {
        super(errorStatus.getMessage());
        this.statusCode = errorStatus.getHttpStatus();
        this.responseMessage = errorStatus.getMessage();
    }

    public int getStatusCode() {
        return this.statusCode.value();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;RuntimeException을 상속&lt;/li&gt;
&lt;li&gt;HttpStatus, 에러 메시지를 받는 생성자&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;ErrorStatus&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 상황에 맞는 상태코드와 메시지를 한곳에서 관리하는 enum이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public enum ErrorStatus {
    

    private final HttpStatus httpStatus;
    private final String message;
    
    NOT_FOUND_USER(HttpStatus.NOT_FOUND,&quot;사용자를 찾을 수 없습니다.&quot;),
    NOT_FOUND_SAVED_JOB(HttpStatus.NOT_FOUND, &quot;저장 내역이 없습니다.&quot;),

	//...
    
    public int getStatusCode() {
        return this.httpStatus.value();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;Custom Exception&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaseException을 상속받은 구체적인 커스텀 예외 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상황에 맞는 ErrorStatus(상태코드, 에러 메시지)를 이용해 BaseExeption의 생성자를 호출해줌으로써 예외가 발생했을 때 &lt;b&gt;HTTP 상태코드와 에러 메시지를 자동으로 세팅&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class NotFoundException extends BaseException {

    public NotFoundException() {
        super(HttpStatus.NOT_FOUND);
    }

    public NotFoundException(String message) {
        super(HttpStatus.NOT_FOUND, message);
    }
    public NotFoundException(ErrorStatus errorStatus) {
        super(errorStatus);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;&lt;b&gt;GlobalExceptionHandler&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GlobalExceptionHandler 클래스는 &lt;b&gt;스프링 애플리케이션 전역에서 발생하는 모든 예외를 한 곳에서 처리하는 핵심 클래스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p id=&quot;33-controlleradvice--exceptionhandler&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@RestControllerAdvice와 @ExceptionHandler로 모든 컨트롤러에서 발생하는 예외를 감지하고 상황에 맞게 처리할 수 있도록 해준다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;@RestControllerAdvice&lt;/span&gt;&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;모든 @RestController에서 발생한 예외를 감지해 공통적으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;@ExceptionHandler&lt;/span&gt;&lt;/span&gt;&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;b&gt;예외 타입&lt;/b&gt;이 발생했을 때 실행할 &lt;b&gt;처리 로직을 지정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

    /** 커스텀 예외(BaseException) */
    @ExceptionHandler(BaseException.class)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;Void&amp;gt;&amp;gt; handleBase(BaseException ex) {
        final int code = ex.getStatusCode();
        final String msg = ex.getResponseMessage() != null
                ? ex.getResponseMessage()
                : HttpStatus.valueOf(code).getReasonPhrase();
        return ResponseEntity.status(code).body(ApiResponse.fail(code, msg));
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;의도적으로 커스텀 예외인 NotFoundException을 발생시켜보면 응답결과가 다음과 같이 상태코드, 메시지와 함께 출력되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lxkd3/dJMb9QFivl5/VF2XxyMpBxB0pr1T91qP9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lxkd3/dJMb9QFivl5/VF2XxyMpBxB0pr1T91qP9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lxkd3/dJMb9QFivl5/VF2XxyMpBxB0pr1T91qP9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flxkd3%2FdJMb9QFivl5%2FVF2XxyMpBxB0pr1T91qP9k%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;637&quot; height=&quot;196&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BackEnd/Spring Boot</category>
      <author>연향동큰손</author>
      <guid isPermaLink="true">https://developerwoohyeon.tistory.com/273</guid>
      <comments>https://developerwoohyeon.tistory.com/273#entry273comment</comments>
      <pubDate>Wed, 22 Oct 2025 16:01:38 +0900</pubDate>
    </item>
  </channel>
</rss>