<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>I'm Prostars</title>
    <link>https://prostars.net/</link>
    <description>prostars의 블로그</description>
    <language>ko</language>
    <pubDate>Sat, 4 Apr 2026 11:31:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>prostars</managingEditor>
    <image>
      <title>I'm Prostars</title>
      <url>https://tistory1.daumcdn.net/tistory/101962/attach/bfc065d4a95f4d2f96a3e8f074ece1d2</url>
      <link>https://prostars.net</link>
    </image>
    <item>
      <title>휴먼북 활동의 가벼운 회고</title>
      <link>https://prostars.net/366</link>
      <description>&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;2025년에는 5개의 중&amp;middot;고등학교 방문 강연을 했고, 한 번의 1:1 휴먼북 세션도 진행했다. 이런 활동 덕분인지 상현도서관 운영위원회 위원 선정도 되어 보고 다양한 경험을 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 강연을 듣고 개발자가 더 되고 싶었을지 '아, 저건 할 게 아닌 듯하다&amp;rsquo;라는 생각이 들었을지 기대 반, 걱정 반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 달 전에 휴먼북 서면 인터뷰는 SNS에 발행되고 끝난 줄 알았는데, 해당 내용이 곧 도서관 소식지에도 실린다고 하니 한 부 요청해서 받아볼 수 있으면 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴사 후 프리랜서를 하다 보니 휴먼북 등 다양한 활동을 자유롭게 할 수 있다는 면에서는 좋다. 내년에도 이 스탠스를 유지할지 다른 무언가를 할지 아직 미정인데 뭐 어떤가. 경험을 공유할 수 있다는 건 나름 보람도 있고 좋은 활동이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년에는&lt;span&gt; &lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;번의&lt;span&gt; &lt;/span&gt;요청을&lt;span&gt; &lt;/span&gt;받게&lt;span&gt; &lt;/span&gt;될지&lt;span&gt; &lt;/span&gt;모르겠지만&lt;span&gt;, &lt;/span&gt;휴먼북은&lt;span&gt; &lt;/span&gt;계속&lt;span&gt; &lt;/span&gt;활동&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;유지하려고&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;</description>
      <category>I'm prostars</category>
      <category>도서관</category>
      <category>휴먼라이브러리</category>
      <category>휴먼북</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/366</guid>
      <comments>https://prostars.net/366#entry366comment</comments>
      <pubDate>Fri, 28 Nov 2025 20:08:50 +0900</pubDate>
    </item>
    <item>
      <title>맥에서 듀얼 모니터 정렬 프리셋 단축키 설정</title>
      <link>https://prostars.net/365</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;맥북을&lt;span&gt; &lt;/span&gt;집과&lt;span&gt; &lt;/span&gt;회사와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;서로&lt;span&gt; &lt;/span&gt;다른&lt;span&gt; &lt;/span&gt;모니터를&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터로&lt;span&gt; &lt;/span&gt;연결할&lt;span&gt; &lt;/span&gt;땐&lt;span&gt; &lt;/span&gt;해당&lt;span&gt; &lt;/span&gt;모니터에&lt;span&gt; &lt;/span&gt;대한&lt;span&gt; &lt;/span&gt;마지막&lt;span&gt; &lt;/span&gt;정렬&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;자동으로&lt;span&gt; &lt;/span&gt;설정해준다&lt;span&gt;. &lt;/span&gt;이런&lt;span&gt; &lt;/span&gt;환경에서&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;때는&lt;span&gt; &lt;/span&gt;별도로&lt;span&gt; &lt;/span&gt;정렬&lt;span&gt; &lt;/span&gt;프리셋&lt;span&gt; &lt;/span&gt;기능이&lt;span&gt; &lt;/span&gt;필요하지&lt;span&gt; &lt;/span&gt;않다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만&lt;span&gt;, &lt;/span&gt;집에서&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;모니터를&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터로&lt;span&gt; &lt;/span&gt;사용하면서&lt;span&gt; &lt;/span&gt;맥북을&lt;span&gt; &lt;/span&gt;책상에&lt;span&gt; &lt;/span&gt;놓고&lt;span&gt; &lt;/span&gt;사용하기도&lt;span&gt; &lt;/span&gt;하고&lt;span&gt; &lt;/span&gt;거치대에&lt;span&gt; &lt;/span&gt;올려서&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;때는&lt;span&gt; &lt;/span&gt;매번&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬을&lt;span&gt; &lt;/span&gt;변경해야&lt;span&gt; &lt;/span&gt;하는&lt;span&gt; &lt;/span&gt;불편함이&lt;span&gt; &lt;/span&gt;있다&lt;span&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;하지만&lt;span&gt;, &lt;/span&gt;요즘&lt;span&gt; &lt;/span&gt;맥북을&lt;span&gt; &lt;/span&gt;바로&lt;span&gt; &lt;/span&gt;내&lt;span&gt; &lt;/span&gt;앞에&lt;span&gt; &lt;/span&gt;놓고&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;일이&lt;span&gt; &lt;/span&gt;많아지면서&lt;span&gt; &lt;/span&gt;하루에도&lt;span&gt; &lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;번씩&lt;span&gt; &lt;/span&gt;내렸다&lt;span&gt; &lt;/span&gt;올렸다&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;. &lt;/span&gt;이때마다&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬을&lt;span&gt; &lt;/span&gt;바꾸고&lt;span&gt;, &lt;/span&gt;맥북&lt;span&gt; &lt;/span&gt;모니터의&lt;span&gt; &lt;/span&gt;해상도를&lt;span&gt; &lt;/span&gt;기본&lt;span&gt; &lt;/span&gt;설정으로&lt;span&gt; &lt;/span&gt;변경하는&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;너무&lt;span&gt; &lt;/span&gt;번거로워서&lt;span&gt; &lt;/span&gt;두&lt;span&gt; &lt;/span&gt;개의&lt;span&gt; &lt;/span&gt;프리셋을&lt;span&gt; &lt;/span&gt;저장해두고&lt;span&gt; &lt;/span&gt;단축키를&lt;span&gt; &lt;/span&gt;사용하여&lt;span&gt; &lt;/span&gt;한&lt;span&gt; &lt;/span&gt;번에&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬과&lt;span&gt; &lt;/span&gt;해상도를&lt;span&gt; &lt;/span&gt;변경할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있게&lt;span&gt; &lt;/span&gt;설정하기로&lt;span&gt; &lt;/span&gt;했다&lt;span&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;/span&gt; &lt;span&gt;글은&lt;/span&gt; displayplacer &lt;span&gt;유틸리티와&lt;/span&gt; macOS&lt;span&gt;에서&lt;/span&gt; &lt;span&gt;기본&lt;/span&gt; &lt;span&gt;제공하는&lt;/span&gt; Automator&lt;span&gt;를&lt;/span&gt; 사용하여 확장 모니터 정렬 프리셋에 단축키를 지정해놓고 간편하게 확장 모니터 정렬 상태를 스위칭하는 방법을 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축어를&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;수도&lt;span&gt; &lt;/span&gt;있지만&lt;span&gt;, &lt;/span&gt;단축어를&lt;span&gt; &lt;/span&gt;사용하려면&lt;span&gt; '&lt;/span&gt;스크립트&lt;span&gt; &lt;/span&gt;실행&lt;span&gt; &lt;/span&gt;허용&lt;span&gt;&amp;rsquo;&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;활성화해야만&lt;span&gt; &lt;/span&gt;하므로&lt;span&gt; &lt;/span&gt;여기서는&lt;span&gt; Automator&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;사용하여&lt;span&gt; 2&lt;/span&gt;개의&lt;span&gt; &lt;/span&gt;프리셋을&lt;span&gt; &lt;/span&gt;만들고&lt;span&gt; &lt;/span&gt;단축키를&lt;span&gt; &lt;/span&gt;설정하는&lt;span&gt; &lt;/span&gt;과정을&lt;span&gt; &lt;/span&gt;단계별로&lt;span&gt; &lt;/span&gt;진행한다&lt;span&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;macOS Sequoia 15.7 환경을 기준으로 한다. 아직은 무서워서 macOS 26으로 업그레이드를 하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew&lt;span&gt;는&lt;/span&gt; &lt;span&gt;설치가&lt;/span&gt; &lt;span&gt;되어&lt;/span&gt; &lt;span&gt;있다고&lt;/span&gt; &lt;span&gt;가정한다&lt;/span&gt;. &lt;span&gt;설치되어&lt;/span&gt; &lt;span&gt;있지&lt;/span&gt; &lt;span&gt;않다면&lt;/span&gt;, &lt;a href=&quot;https://brew.sh&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://brew.sh&lt;/a&gt; &lt;span&gt;여기를&lt;/span&gt; &lt;span&gt;참고하여&lt;/span&gt; &lt;span&gt;간단히&lt;/span&gt; &lt;span&gt;설치할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &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 data-ke-size=&quot;size23&quot;&gt;displayplacer 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Homebrew&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;사용하여&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;간단히&lt;span&gt; &lt;/span&gt;설치할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758711292743&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ brew install displayplacer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew를 사용하여 displayplacer를 설치하면 추가 설정 없이 바로 displayplacer를 사용해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;현재의&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬&lt;span&gt; &lt;/span&gt;상태를&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758711365516&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ displayplacer list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew&lt;span&gt;를&lt;/span&gt; &lt;span&gt;사용하고&lt;/span&gt; &lt;span&gt;있지&lt;/span&gt; &lt;span&gt;않다면&lt;/span&gt;, &lt;a href=&quot;https://github.com/jakehilborn/displayplacer/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/jakehilborn/displayplacer/releases&lt;/a&gt; &lt;span&gt;에서&lt;/span&gt; &lt;span&gt;직접&lt;/span&gt; &lt;span&gt;바이너리를&lt;/span&gt; &lt;span&gt;받아서&lt;/span&gt; &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;수도&lt;/span&gt; &lt;span&gt;있지만&lt;/span&gt;, &lt;span&gt;페이지에&lt;/span&gt; &lt;span&gt;안내되어&lt;/span&gt; &lt;span&gt;있듯이&lt;/span&gt; &lt;span&gt;추가&lt;/span&gt; &lt;span&gt;설정이&lt;/span&gt; &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 data-ke-size=&quot;size23&quot;&gt;Automator 빠른 동작 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Automator&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;실행하고&lt;span&gt; '&lt;/span&gt;새&lt;span&gt; &lt;/span&gt;문서&lt;span&gt;&amp;rsquo;&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;클릭하면&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; '&lt;/span&gt;문서&lt;span&gt; &lt;/span&gt;유형&lt;span&gt; &lt;/span&gt;선택&lt;span&gt;' &lt;/span&gt;창이&lt;span&gt; &lt;/span&gt;열린다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 11.55.10.jpg&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nz6oM/btsQMKlXugR/rO8byx8F7NxvKU7yAuwxf1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nz6oM/btsQMKlXugR/rO8byx8F7NxvKU7yAuwxf1/img.jpg&quot; data-alt=&quot;Automator&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nz6oM/btsQMKlXugR/rO8byx8F7NxvKU7yAuwxf1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnz6oM%2FbtsQMKlXugR%2FrO8byx8F7NxvKU7yAuwxf1%2Fimg.jpg&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;600&quot; height=&quot;536&quot; data-filename=&quot;스크린샷 2025-09-23 11.55.10.jpg&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Automator&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;'&lt;/span&gt;빠른&lt;span&gt; &lt;/span&gt;동작&lt;span&gt;&amp;rsquo;&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;선택하면&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;창이&lt;span&gt; &lt;/span&gt;열린다&lt;span&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;아래와 같이 열린 창에서 '작업&amp;rsquo;흐름 수신&amp;rsquo;을 '입력 없음&amp;rsquo;으로 바꾸고, '선택 항목 위치&amp;rsquo;는 기본 값인 '모든 응용 프로그램&amp;rsquo;을 그대로 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽&lt;span&gt; &lt;/span&gt;패널에서&lt;span&gt; '&lt;/span&gt;유틸리티&lt;span&gt;&amp;rsquo;&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;선택하고&lt;span&gt;, '&lt;/span&gt;셸&lt;span&gt; &lt;/span&gt;스트립트&lt;span&gt; &lt;/span&gt;실행&lt;span&gt;&amp;rsquo;&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;더블&lt;span&gt; &lt;/span&gt;클릭하거나&lt;span&gt; &lt;/span&gt;오른쪽&lt;span&gt; &lt;/span&gt;빈&lt;span&gt; &lt;/span&gt;공간으로&lt;span&gt; &lt;/span&gt;드래그해서&lt;span&gt; &lt;/span&gt;작업&lt;span&gt; &lt;/span&gt;흐름에&lt;span&gt; '&lt;/span&gt;셸&lt;span&gt; &lt;/span&gt;스트립트&lt;span&gt; &lt;/span&gt;실행&lt;span&gt;&amp;rsquo;&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;추가해서&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;설정한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 12.10.56.jpg&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IAZap/btsQN1BdEzL/T2wLoTH1TNLQDePSbpky11/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IAZap/btsQN1BdEzL/T2wLoTH1TNLQDePSbpky11/img.jpg&quot; data-alt=&quot;Automator - 빠른 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IAZap/btsQN1BdEzL/T2wLoTH1TNLQDePSbpky11/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIAZap%2FbtsQN1BdEzL%2FT2wLoTH1TNLQDePSbpky11%2Fimg.jpg&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;2004&quot; height=&quot;716&quot; data-filename=&quot;스크린샷 2025-09-23 12.10.56.jpg&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Automator - 빠른 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Automator&lt;/span&gt;는&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;잠시&lt;span&gt; &lt;/span&gt;두고&lt;span&gt;, '&lt;/span&gt;시스템&lt;span&gt; &lt;/span&gt;설정&lt;span&gt;&amp;rsquo;&lt;/span&gt;의&lt;span&gt; '&lt;/span&gt;디스플레이&lt;span&gt;&amp;rsquo;&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬과&lt;span&gt; &lt;/span&gt;해상도&lt;span&gt; &lt;/span&gt;등을&lt;span&gt; &lt;/span&gt;첫&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋으로&lt;span&gt; &lt;/span&gt;저장할&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;설정한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 10.06.24.jpg&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vd6wW/btsQPfxX4uI/zxgGKQC4wDq431hDaI9Rm0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vd6wW/btsQPfxX4uI/zxgGKQC4wDq431hDaI9Rm0/img.jpg&quot; data-alt=&quot;거치대 사용 시 정렬&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vd6wW/btsQPfxX4uI/zxgGKQC4wDq431hDaI9Rm0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvd6wW%2FbtsQPfxX4uI%2FzxgGKQC4wDq431hDaI9Rm0%2Fimg.jpg&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;600&quot; height=&quot;373&quot; data-filename=&quot;스크린샷 2025-09-23 10.06.24.jpg&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;거치대 사용 시 정렬&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 10.08.30.jpg&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JbXrD/btsQNphfRkl/00KIpHdd4dKbETG43Lzzf1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JbXrD/btsQNphfRkl/00KIpHdd4dKbETG43Lzzf1/img.jpg&quot; data-alt=&quot;거치대 사용 시 내장 디스플레이의 해상도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JbXrD/btsQNphfRkl/00KIpHdd4dKbETG43Lzzf1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJbXrD%2FbtsQNphfRkl%2F00KIpHdd4dKbETG43Lzzf1%2Fimg.jpg&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;600&quot; height=&quot;197&quot; data-filename=&quot;스크린샷 2025-09-23 10.08.30.jpg&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;거치대 사용 시 내장 디스플레이의 해상도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의&lt;span&gt; &lt;/span&gt;경우는&lt;span&gt; &lt;/span&gt;위와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;정렬로&lt;span&gt; &lt;/span&gt;맥북의&lt;span&gt; &lt;/span&gt;내장&lt;span&gt; &lt;/span&gt;디스플레이를&lt;span&gt; '&lt;/span&gt;확장된&lt;span&gt; &lt;/span&gt;디스플레이&lt;span&gt;&amp;rsquo;&lt;/span&gt;로&lt;span&gt; &lt;/span&gt;사용하면서&lt;span&gt; &lt;/span&gt;한&lt;span&gt; &lt;/span&gt;단계&lt;span&gt; &lt;/span&gt;낮은&lt;span&gt; &lt;/span&gt;해상도를&lt;span&gt; &lt;/span&gt;첫&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋으로&lt;span&gt; &lt;/span&gt;사용한다&lt;span&gt;. &lt;/span&gt;이제&lt;span&gt; &lt;/span&gt;터미널에서&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; displayplacer&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;실행한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758713630971&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ displayplacer list
Persistent screen id: 749CEAFA-6416-4099-8397-2F7FA35B7E91
...
displayplacer &quot;id:749CEAFA-6416-4099-8397-2F7FA35B7E91 res:2560x1440 hz:60 color_depth:8 enabled:true scaling:on origin:(0,0) degree:0&quot; &quot;id:37D8832A-2D66-02CA-B9F7-8F30A301B230 res:1280x832 hz:60 color_depth:8 enabled:true scaling:on origin:(-1280,331) degree:0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재&lt;/span&gt; &lt;span&gt;모니터&lt;/span&gt; &lt;span&gt;구성&lt;/span&gt; &lt;span&gt;정보가&lt;/span&gt; &lt;span&gt;출력된다&lt;/span&gt;. &lt;span&gt;출력된&lt;/span&gt; &lt;span&gt;결과에서&lt;/span&gt; &lt;span&gt;가장&lt;/span&gt; &lt;span&gt;마지막에&lt;/span&gt; &lt;span&gt;출력된&lt;/span&gt; &lt;span&gt;정보&lt;/span&gt; 'displayplacer &quot;id:...degree:0&quot;'&lt;span&gt;가&lt;/span&gt; &lt;span&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;/span&gt; &lt;span&gt;터미널에서&lt;/span&gt; &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; displayplacer&lt;span&gt;를&lt;/span&gt; &lt;span&gt;실행한다&lt;/span&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1758713821649&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ which displayplacer
/opt/homebrew/bin/displayplacer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;displayplacer&lt;span&gt;가&lt;/span&gt; &lt;span&gt;설치된&lt;/span&gt; &lt;span&gt;절대&lt;/span&gt; &lt;span&gt;경로가&lt;/span&gt; &lt;span&gt;출력된다&lt;/span&gt;. &lt;span&gt;나의&lt;/span&gt; &lt;span&gt;경우&lt;/span&gt; '/opt/homebrew/bin/displayplacer&amp;rsquo;&lt;span&gt;로&lt;/span&gt; &lt;span&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;/span&gt;얻은&lt;span&gt; 2&lt;/span&gt;개의&lt;span&gt; &lt;/span&gt;정보를&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;조합한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758713879050&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/homebrew/bin/displayplacer &quot;id:749CEAFA-6416-4099-8397-2F7FA35B7E91 res:2560x1440 hz:60 color_depth:8 enabled:true scaling:on origin:(0,0) degree:0&quot; &quot;id:37D8832A-2D66-02CA-B9F7-8F30A301B230 res:1280x832 hz:60 color_depth:8 enabled:true scaling:on origin:(-1280,331) degree:0&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;span&gt; &lt;/span&gt;내용을&lt;span&gt; &lt;/span&gt;조금&lt;span&gt; &lt;/span&gt;전에&lt;span&gt; &lt;/span&gt;열어둔&lt;span&gt; Automator&lt;/span&gt;의&lt;span&gt; '&lt;/span&gt;셸&lt;span&gt; &lt;/span&gt;스크립트&lt;span&gt; &lt;/span&gt;실행&lt;span&gt;' &lt;/span&gt;창의&lt;span&gt; 'cat&amp;rsquo;&lt;/span&gt;이&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;자리에&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;넣는다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 18.46.46.jpg&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pgv4R/btsQLLTckQf/UFlXcpkom7JLOlvkkMlxZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pgv4R/btsQLLTckQf/UFlXcpkom7JLOlvkkMlxZ0/img.jpg&quot; data-alt=&quot;Automator - 빠른 동작 - 셸 스크립트 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pgv4R/btsQLLTckQf/UFlXcpkom7JLOlvkkMlxZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpgv4R%2FbtsQLLTckQf%2FUFlXcpkom7JLOlvkkMlxZ0%2Fimg.jpg&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;1212&quot; height=&quot;338&quot; data-filename=&quot;스크린샷 2025-09-23 18.46.46.jpg&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Automator - 빠른 동작 - 셸 스크립트 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&lt;span&gt; &lt;/span&gt;상태에서&lt;span&gt; &lt;/span&gt;적당한&lt;span&gt; &lt;/span&gt;이름으로&lt;span&gt; &lt;/span&gt;저장하면&lt;span&gt; &lt;/span&gt;되는데&lt;span&gt;, &lt;/span&gt;여기서는&lt;span&gt; 'DispPresetA&amp;rsquo;&lt;/span&gt;로&lt;span&gt; &lt;/span&gt;저장한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 18.48.25.jpg&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HiS4a/btsQNJs6OYS/eLxUuFG8YN5Ha0SK6Z4T50/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HiS4a/btsQNJs6OYS/eLxUuFG8YN5Ha0SK6Z4T50/img.jpg&quot; data-alt=&quot;'DispPresetA'라는 이름으로 저장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HiS4a/btsQNJs6OYS/eLxUuFG8YN5Ha0SK6Z4T50/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHiS4a%2FbtsQNJs6OYS%2FeLxUuFG8YN5Ha0SK6Z4T50%2Fimg.jpg&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;600&quot; height=&quot;139&quot; data-filename=&quot;스크린샷 2025-09-23 18.48.25.jpg&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'DispPresetA'라는 이름으로 저장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&lt;span&gt; &lt;/span&gt;해서&lt;span&gt; &lt;/span&gt;첫&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋&lt;span&gt; &lt;/span&gt;설정을&lt;span&gt; &lt;/span&gt;준비했고&lt;span&gt;, &lt;/span&gt;이제&lt;span&gt; &lt;/span&gt;두&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋&lt;span&gt; &lt;/span&gt;설정을&lt;span&gt; &lt;/span&gt;진행하자&lt;span&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;Automator&lt;span&gt;의&lt;/span&gt; &lt;span&gt;파일&lt;/span&gt; &lt;span&gt;메뉴에서&lt;/span&gt; &lt;span&gt;복제를&lt;/span&gt; &lt;span&gt;선택하고&lt;/span&gt; &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; 'DispPresetB&amp;rsquo;&lt;span&gt;라고&lt;/span&gt; &lt;span&gt;이름을&lt;/span&gt; &lt;span&gt;지정한다&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 21.40.16.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/62Se5/btsQMhddEyj/Ux9nVJ1Nj9kjasC1aJJZhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/62Se5/btsQMhddEyj/Ux9nVJ1Nj9kjasC1aJJZhK/img.jpg&quot; data-alt=&quot;'DispPresetB'라는 이름으로 복제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/62Se5/btsQMhddEyj/Ux9nVJ1Nj9kjasC1aJJZhK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F62Se5%2FbtsQMhddEyj%2FUx9nVJ1Nj9kjasC1aJJZhK%2Fimg.jpg&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;600&quot; height=&quot;201&quot; data-filename=&quot;스크린샷 2025-09-23 21.40.16.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'DispPresetB'라는 이름으로 복제&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시&lt;span&gt; Automator&lt;/span&gt;는&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;잠시&lt;span&gt; &lt;/span&gt;두고&lt;span&gt;, '&lt;/span&gt;시스템&lt;span&gt; &lt;/span&gt;설정&lt;span&gt;&amp;rsquo;&lt;/span&gt;의&lt;span&gt; '&lt;/span&gt;디스플레이&lt;span&gt;&amp;rsquo;&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬과&lt;span&gt; &lt;/span&gt;해상도&lt;span&gt; &lt;/span&gt;등을&lt;span&gt; &lt;/span&gt;두&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋으로&lt;span&gt; &lt;/span&gt;저장할&lt;span&gt; &lt;/span&gt;상태로&lt;span&gt; &lt;/span&gt;설정한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 10.06.35.jpg&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdLMYr/btsQN9rxjD9/jkzUqVQ9pqVCPObQRpYKr1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdLMYr/btsQN9rxjD9/jkzUqVQ9pqVCPObQRpYKr1/img.jpg&quot; data-alt=&quot;책상 위 사용 시 정렬&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdLMYr/btsQN9rxjD9/jkzUqVQ9pqVCPObQRpYKr1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdLMYr%2FbtsQN9rxjD9%2FjkzUqVQ9pqVCPObQRpYKr1%2Fimg.jpg&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;600&quot; height=&quot;373&quot; data-filename=&quot;스크린샷 2025-09-23 10.06.35.jpg&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;책상 위 사용 시 정렬&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 10.08.40.jpg&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbVbjw/btsQOGbuQm4/c25rYPc4R0AhK4bBTSK9n0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbVbjw/btsQOGbuQm4/c25rYPc4R0AhK4bBTSK9n0/img.jpg&quot; data-alt=&quot;책상 위 사용 시 내장 디스플레이의 해상도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbVbjw/btsQOGbuQm4/c25rYPc4R0AhK4bBTSK9n0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbVbjw%2FbtsQOGbuQm4%2Fc25rYPc4R0AhK4bBTSK9n0%2Fimg.jpg&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;600&quot; height=&quot;197&quot; data-filename=&quot;스크린샷 2025-09-23 10.08.40.jpg&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;책상 위 사용 시 내장 디스플레이의 해상도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의&lt;span&gt; &lt;/span&gt;경우는&lt;span&gt; &lt;/span&gt;위와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;정렬로&lt;span&gt; &lt;/span&gt;맥북의&lt;span&gt; &lt;/span&gt;내장&lt;span&gt; &lt;/span&gt;디스플레이를&lt;span&gt; '&lt;/span&gt;확장된&lt;span&gt; &lt;/span&gt;디스플레이&lt;span&gt;&amp;rsquo;&lt;/span&gt;로&lt;span&gt; &lt;/span&gt;사용하면서&lt;span&gt; &lt;/span&gt;기본&lt;span&gt; &lt;/span&gt;해상도를&lt;span&gt; &lt;/span&gt;두&lt;span&gt; &lt;/span&gt;번째&lt;span&gt; &lt;/span&gt;프리셋으로&lt;span&gt; &lt;/span&gt;사용한다&lt;span&gt;. &lt;/span&gt;다시&lt;span&gt; &lt;/span&gt;터미널에서&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; displayplacer&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;실행한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758714451887&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ displayplacer list
Persistent screen id: 749CEAFA-6416-4099-8397-2F7FA35B7E91
...
displayplacer &quot;id:749CEAFA-6416-4099-8397-2F7FA35B7E91 res:2560x1440 hz:60 color_depth:8 enabled:true scaling:on origin:(0,0) degree:0&quot; &quot;id:37D8832A-2D66-02CA-B9F7-8F30A301B230 res:1470x956 hz:60 color_depth:8 enabled:true scaling:on origin:(545,1440) degree:0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이&lt;/span&gt; &lt;span&gt;내용을&lt;/span&gt; &lt;span&gt;조금&lt;/span&gt; &lt;span&gt;전에&lt;/span&gt; &lt;span&gt;열어둔&lt;/span&gt; Automator 'DispPresetB&amp;rsquo;&lt;span&gt;의&lt;/span&gt; &lt;span&gt;셸&lt;/span&gt; &lt;span&gt;스크립트&lt;/span&gt; &lt;span&gt;실행&lt;/span&gt;' &lt;span&gt;창에&lt;/span&gt; &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; &lt;span&gt;넣는다&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 21.52.55.jpg&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t991p/btsQLBQEEzc/iKPCqH0A0YokxKYmhhf7U0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t991p/btsQLBQEEzc/iKPCqH0A0YokxKYmhhf7U0/img.jpg&quot; data-alt=&quot;Automator - 빠른 동작 - 셸 스크립트 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t991p/btsQLBQEEzc/iKPCqH0A0YokxKYmhhf7U0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft991p%2FbtsQLBQEEzc%2FiKPCqH0A0YokxKYmhhf7U0%2Fimg.jpg&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;1210&quot; height=&quot;336&quot; data-filename=&quot;스크린샷 2025-09-23 21.52.55.jpg&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Automator - 빠른 동작 - 셸 스크립트 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이&lt;/span&gt; &lt;span&gt;내용을&lt;/span&gt; &lt;span&gt;저장하고&lt;/span&gt; Automator&lt;span&gt;를&lt;/span&gt; &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 data-ke-size=&quot;size23&quot;&gt;단축키&amp;nbsp;지정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'&lt;span&gt;시스템&lt;/span&gt; &lt;span&gt;설정&lt;/span&gt;&amp;rsquo;&lt;span&gt;의&lt;/span&gt; '&lt;span&gt;키보드&lt;/span&gt;&amp;rsquo;&lt;span&gt;에서&lt;/span&gt; '&lt;span&gt;키보드&lt;/span&gt; &lt;span&gt;단축키&lt;/span&gt;...'&lt;span&gt;를&lt;/span&gt; &lt;span&gt;클릭하여&lt;/span&gt; &lt;span&gt;열린&lt;/span&gt; &lt;span&gt;창에서&lt;/span&gt; '&lt;span&gt;서비스&lt;/span&gt;&amp;rsquo;&lt;span&gt;를&lt;/span&gt; &lt;span&gt;선택하고&lt;/span&gt; &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; '&lt;span&gt;일반&lt;/span&gt;' &lt;span&gt;항목을&lt;/span&gt; &lt;span&gt;펼치면&lt;/span&gt; &lt;span&gt;위에서&lt;/span&gt; &lt;span&gt;생성한&lt;/span&gt; 'DispPresetA', 'DispPresetB&amp;rsquo;&lt;span&gt;가&lt;/span&gt; &lt;span&gt;보인다&lt;/span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 22.06.59.jpg&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FO2UJ/btsQMiiWGAx/WelxsJKsWNdIQGLS5kkogK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FO2UJ/btsQMiiWGAx/WelxsJKsWNdIQGLS5kkogK/img.jpg&quot; data-alt=&quot;시스템 설정 - 키보드 - 키보드 단축키 - 서비스 ((단축키 지정 전)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FO2UJ/btsQMiiWGAx/WelxsJKsWNdIQGLS5kkogK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFO2UJ%2FbtsQMiiWGAx%2FWelxsJKsWNdIQGLS5kkogK%2Fimg.jpg&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;600&quot; height=&quot;70&quot; data-filename=&quot;스크린샷 2025-09-23 22.06.59.jpg&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시스템 설정 - 키보드 - 키보드 단축키 - 서비스 ((단축키 지정 전)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'&lt;span&gt;없음&lt;/span&gt;' &lt;span&gt;부분을&lt;/span&gt; &lt;span&gt;더블&lt;/span&gt; &lt;span&gt;클릭하면&lt;/span&gt; &lt;span&gt;입력창이&lt;/span&gt; &lt;span&gt;열리는데&lt;/span&gt; &lt;span&gt;이때&lt;/span&gt;, &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;단축키를&lt;/span&gt; &lt;span&gt;누르면&lt;/span&gt; &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; &lt;span&gt;설정된다&lt;/span&gt;. &lt;span&gt;여기서는&lt;/span&gt; 'DispPresetA&amp;rsquo;&lt;span&gt;에&lt;/span&gt; '^⌥⌘1&amp;rsquo;&lt;span&gt;를&lt;/span&gt; &lt;span&gt;단축키로&lt;/span&gt; &lt;span&gt;지정하고&lt;/span&gt;, 'DispPresetB&amp;rsquo;&lt;span&gt;에&lt;/span&gt; '^⌥⌘2&amp;rsquo;&lt;span&gt;를&lt;/span&gt; &lt;span&gt;단축키로&lt;/span&gt; &lt;span&gt;지정한다&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-23 22.10.53.jpg&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bro1vd/btsQMpa6emv/nF286zLIKrVMnySeTiXJm0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bro1vd/btsQMpa6emv/nF286zLIKrVMnySeTiXJm0/img.jpg&quot; data-alt=&quot;시스템 설정 - 키보드 - 키보드 단축키 - 서비스 (단축키 지정 완료)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bro1vd/btsQMpa6emv/nF286zLIKrVMnySeTiXJm0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbro1vd%2FbtsQMpa6emv%2FnF286zLIKrVMnySeTiXJm0%2Fimg.jpg&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;600&quot; height=&quot;67&quot; data-filename=&quot;스크린샷 2025-09-23 22.10.53.jpg&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시스템 설정 - 키보드 - 키보드 단축키 - 서비스 (단축키 지정 완료)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 오픈 소스 유틸리티와 macOS에서 기본으로 제공되는 Automator를 조합해서 macOS가 기본으로 제공하지 않는 모니터 정렬 프리셋 기능을 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&lt;span&gt; &lt;/span&gt;지정한&lt;span&gt; &lt;/span&gt;단축키를&lt;span&gt; &lt;/span&gt;입력해&lt;span&gt; &lt;/span&gt;보면&lt;span&gt; &lt;/span&gt;확장&lt;span&gt; &lt;/span&gt;모니터&lt;span&gt; &lt;/span&gt;정렬과&lt;span&gt; &lt;/span&gt;해상도가&lt;span&gt; &lt;/span&gt;스위칭되는&lt;span&gt; &lt;/span&gt;것을&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;</description>
      <category>I'm prostars</category>
      <category>Automator</category>
      <category>displayplacer</category>
      <category>MacOS</category>
      <category>Preset</category>
      <category>shortcut</category>
      <category>단축키</category>
      <category>듀얼 모니터</category>
      <category>맥북</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/365</guid>
      <comments>https://prostars.net/365#entry365comment</comments>
      <pubDate>Thu, 25 Sep 2025 14:57:05 +0900</pubDate>
    </item>
    <item>
      <title>저의 첫 온라인 강의, 모든 파트가 오픈되었습니다.</title>
      <link>https://prostars.net/364</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 첫 온라인 강의가 이제 마지막 파트까지 모두 오픈되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 개념이나 기능을 프로젝트에 적용할 때마다 이해를 돕기 위한 예제를 만들면서 진행하였고, 이 과정에서 10여 개의 예제를 만들었습니다.&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개의 모놀리스 프로젝트로 시작한 채팅 시스템을 기능별로 스케일 아웃할 수 있도록 4개 역할(Authentication, Connection, Message, Push)로 분리하는 과정을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 마지막에는 로컬 환경에서 22개의 컨테이너와 8개의 서버 인스턴스로 채팅 시스템을 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;02-step4.webp&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDMYGh/btsOYfARo39/XWYtSENxExXgDlWFFEt7DK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDMYGh/btsOYfARo39/XWYtSENxExXgDlWFFEt7DK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDMYGh/btsOYfARo39/XWYtSENxExXgDlWFFEt7DK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDMYGh%2FbtsOYfARo39%2FXWYtSENxExXgDlWFFEt7DK%2Fimg.webp&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;960&quot; height=&quot;600&quot; data-filename=&quot;02-step4.webp&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;600&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;강의 파트 5에서 완성된 프로젝트는 약 12,000 라인의 코드를 가지면서 다양한 인프라스트럭처(Consul, Nginx, MySQL, Redis, Kafka)를 연동하는 단일 강의 예제 프로젝트로는 제법 큰 규모가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작할 때 20~30시간 정도로 예상했던 분량은 만들다 보니 약 55시간의 생각보다 긴 강의가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이&lt;span&gt; &lt;/span&gt;되는&lt;span&gt; &lt;/span&gt;내용이길&lt;span&gt; &lt;/span&gt;바랍니다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; 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;a href=&quot;https://fastcampus.co.kr/dev_online_chat&quot;&gt;https://fastcampus.co.kr/dev_online_chat&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751271857777&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&quot; data-og-description=&quot;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&quot; data-og-host=&quot;fastcampus.co.kr&quot; data-og-source-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot; data-og-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dLvuf1/hyZfYVgdKX/HvKWjvfAE4YNcLEgGb6Wrk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/cBQF1S/hyZfvFATX0/KVF2PbBz3EbfVwTqgDGEd0/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/nxa6h/hyZckMtcWl/5I0XhB81zu81KtyB4W2sH1/img.png?width=600&amp;amp;height=450&amp;amp;face=421_255_514_348&quot;&gt;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_chat&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dLvuf1/hyZfYVgdKX/HvKWjvfAE4YNcLEgGb6Wrk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/cBQF1S/hyZfvFATX0/KVF2PbBz3EbfVwTqgDGEd0/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/nxa6h/hyZckMtcWl/5I0XhB81zu81KtyB4W2sH1/img.png?width=600&amp;amp;height=450&amp;amp;face=421_255_514_348');&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fastcampus.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>I'm prostars</category>
      <category>Backend</category>
      <category>Java</category>
      <category>SpringBoot</category>
      <category>메시지 시스템</category>
      <category>백엔드</category>
      <category>분산 시스템</category>
      <category>온라인강의</category>
      <category>채팅 시스템</category>
      <category>패스트캠퍼스</category>
      <category>핸즈온</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/364</guid>
      <comments>https://prostars.net/364#entry364comment</comments>
      <pubDate>Tue, 1 Jul 2025 11:43:09 +0900</pubDate>
    </item>
    <item>
      <title>용인시 도서관 휴먼북 인터뷰</title>
      <link>https://prostars.net/363</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;용인시 도서관과 서면 인터뷰한 내용이 공개되어 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 4, 5월에 중고등학교 두 곳에서 개발자가 하는 일에 대해 강연했고, 8월까지 세 곳을 더 진행할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제작 중인 온라인 강의를 20시간 기준으로 시작할 때 프로토타이핑을 해보면서 강의 시간이 초과할 걸 예상은 했지만, 2배 이상 초과할 거라고는 예상하지 못했다.&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;a href=&quot;https://blog.naver.com/yonginlib/223886184676&quot;&gt;https://blog.naver.com/yonginlib/223886184676&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749021692067&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[휴먼북 인터뷰] 상현도서관 이상우 휴먼북을 소개합니다.&quot; data-og-description=&quot;1. 개발자란 무엇이며 개발자가 된 계기가 있으신가요? 프로그래머, 코더, 소프트웨어 엔지니어 등 개발자...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/yonginlib/223886184676&quot; data-og-url=&quot;https://blog.naver.com/yonginlib/223886184676&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/65ATA/hyY0ouQtbO/q9GHSGMgGtVZkvKioh7K7K/img.png?width=743&amp;amp;height=743&amp;amp;face=0_0_743_743&quot;&gt;&lt;a href=&quot;https://blog.naver.com/yonginlib/223886184676&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/yonginlib/223886184676&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/65ATA/hyY0ouQtbO/q9GHSGMgGtVZkvKioh7K7K/img.png?width=743&amp;amp;height=743&amp;amp;face=0_0_743_743');&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;[휴먼북 인터뷰] 상현도서관 이상우 휴먼북을 소개합니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 개발자란 무엇이며 개발자가 된 계기가 있으신가요? 프로그래머, 코더, 소프트웨어 엔지니어 등 개발자...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.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;30% 할인 코드명: 대규모채팅 (~25/6/15)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 링크: &lt;a href=&quot;https://buly.kr/Edt2csp&quot;&gt;https://buly.kr/Edt2csp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749021733596&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&quot; data-og-description=&quot;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&quot; data-og-host=&quot;fastcampus.co.kr&quot; data-og-source-url=&quot;https://buly.kr/Edt2csp&quot; data-og-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5Pq1x/hyY5a2t9hv/vCECzLDE7zorRwLCoLjQqk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/I9MCc/hyY5f3NEb0/QN93VrT0P79HhkEwNCcjCk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/dgtQsH/hyY5btyhDZ/wUPKFOQq0xDg04p0yLCy31/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157&quot;&gt;&lt;a href=&quot;https://buly.kr/Edt2csp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://buly.kr/Edt2csp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5Pq1x/hyY5a2t9hv/vCECzLDE7zorRwLCoLjQqk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/I9MCc/hyY5f3NEb0/QN93VrT0P79HhkEwNCcjCk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/dgtQsH/hyY5btyhDZ/wUPKFOQq0xDg04p0yLCy31/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157');&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fastcampus.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>I'm prostars</category>
      <category>경험공유</category>
      <category>도서관</category>
      <category>상현도서관</category>
      <category>용인시도서관</category>
      <category>자원봉사</category>
      <category>휴먼라이브러리</category>
      <category>휴먼북</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/363</guid>
      <comments>https://prostars.net/363#entry363comment</comments>
      <pubDate>Wed, 4 Jun 2025 16:24:55 +0900</pubDate>
    </item>
    <item>
      <title>Spring의 ConcurrentWebSocketSessionDecorator 소개</title>
      <link>https://prostars.net/362</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트에서 단순히 웹소켓을 사용하는 건 어렵지 않다. 스프링 부트 3.4.3 기준으로 기본 설정된 서블릿 컨테이너는 임베디드 톰캣이고, 모든 TCP 처리는 서블릿 컨테이너에서 처리한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&lt;span&gt; &lt;/span&gt;글에서는&lt;span&gt; &lt;/span&gt;스프링&lt;span&gt; &lt;/span&gt;부트에서&lt;span&gt; &lt;/span&gt;웹소켓을&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;때&lt;span&gt; &lt;/span&gt;멀티스레드가&lt;span&gt; &lt;/span&gt;하나의&lt;span&gt; &lt;/span&gt;세션에&lt;span&gt; &lt;/span&gt;동시에&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;전송할&lt;span&gt; &lt;/span&gt;때&lt;span&gt; &lt;/span&gt;발생하는&lt;span&gt; &lt;/span&gt;문제를&lt;span&gt; &lt;/span&gt;확인하고&lt;span&gt; &lt;/span&gt;대응하는&lt;span&gt; &lt;/span&gt;한&lt;span&gt; &lt;/span&gt;가지&lt;span&gt; &lt;/span&gt;방법을&lt;span&gt; &lt;/span&gt;소개한다&lt;span&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;여기서&amp;nbsp;사용하는&amp;nbsp;예제는&amp;nbsp;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_chat&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;나의 온라인 강의&lt;/a&gt;의 파트 2-챕터 2 'Rest API와 WebSocket의 기본&amp;rsquo; 중에서 '08. 채팅 프로젝트를 그룹 메시지로 확장하기&amp;rsquo;에 있는 코드에서 웹소켓에 대한 처리와 테스트 코드를 가져왔다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 Java 17에 Spring Boot 3.4를 사용하고, 통합 테스트 구성은&amp;nbsp;&amp;nbsp;Groovy 4.0에 Spock 2.4를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 &lt;a href=&quot;https://github.com/prostars/websocket-multi-thread-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;에 올라가 있다. Postman을&amp;nbsp;웹소켓&amp;nbsp;클라이언트로&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;Gradle&lt;span&gt; &lt;/span&gt;설정은&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745750471978&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
	id 'java'
	id 'groovy'
	id 'org.springframework.boot' version '3.4.3'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'net.prostars'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-websocket'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.spockframework:spock-core:2.4-M5-groovy-4.0'
	testImplementation 'org.spockframework:spock-spring:2.4-M5-groovy-4.0'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;build.gradle&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;/span&gt;부트를&lt;span&gt; &lt;/span&gt;사용하므로&lt;span&gt; main &lt;/span&gt;구성은&lt;span&gt; &lt;/span&gt;간단하게&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745750544621&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class WebsocketMultiThreadExample {

  public static void main(String[] args) {
    SpringApplication.run(WebsocketMultiThreadExample.class, args);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;WebsocketMultiThreadExample.java&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;간단한 그룹 채팅과 비슷한 동작을 구현하기 위해서 웹소켓을 텍스트 베이스로 사용한다. TextWebSocketHandler를 상속받아서 빈을 하나 만든다. 서버에 접속한 모든 웹소켓 세션은 ConcurrentHashMap을 사용해서 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 핸들러의 동작은 서버가 클라이언트에게 메시지를 받으면 이 메시지를 전송한 클라이언트를 제외한 나머지 모든 클라이언트에게 이 메시지를 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러의&lt;span&gt; &lt;/span&gt;전체&lt;span&gt; &lt;/span&gt;코드는&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745750627006&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class MessageHandler extends TextWebSocketHandler {

  private static final Logger log = LoggerFactory.getLogger(MessageHandler.class);
  protected final Map&amp;lt;String, WebSocketSession&amp;gt; sessions = new ConcurrentHashMap&amp;lt;&amp;gt;();

  @Override
  public void afterConnectionEstablished(WebSocketSession session) {
    sessions.put(session.getId(), session);
  }

  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
      throws IOException {
    WebSocketSession webSocketSession = sessions.remove(session.getId());
    if (webSocketSession != null) {
      webSocketSession.close();
    }
  }

  @Override
  protected void handleTextMessage(WebSocketSession senderSession, TextMessage message) {
    try {
      for (WebSocketSession session : sessions.values()) {
        if (!senderSession.getId().equals(session.getId())) {
          session.sendMessage(new TextMessage(message.getPayload()));
          log.info(&quot;Send {} to {}&quot;, message.getPayload(), session.getId());
        }
      }
    } catch (Exception ex) {
      log.error(&quot;Failed to send from {} error: {}&quot;, senderSession.getId(), ex.getMessage());
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;handler/MessageHandler.java&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러를 하나 준비했으므로, 아래와 같이 웹소켓 엔드포인트에 연결해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서는&lt;/span&gt; &lt;span&gt;엔드포인트를&lt;/span&gt; '/ws/v1/message' &lt;span&gt;로&lt;/span&gt; &lt;span&gt;설정했다&lt;/span&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1745750682013&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocket
public class WebSocketHandlerConfig implements WebSocketConfigurer {

  private final MessageHandler messageHandler;

  public WebSocketHandlerConfig(MessageHandler messageHandler) {
    this.messageHandler = messageHandler;
  }

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(messageHandler, &quot;/ws/v1/message&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;config/WebSocketHandlerConfig.java&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;일단 필요한 구성을 다 했으니, 서버를 실행할 수 있다. 서버를 실행하면 기본 포트인 8080 포트로 임베디드 톰캣이 실행된다.&lt;br /&gt;Postman을 웹소켓 클라이언트로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 'ws://localhost:8080/ws/v1/message'로 접속해서 텍스트 메시지를 보내고 받을 수 있다.&lt;br /&gt;여기서는&amp;nbsp;4개의&amp;nbsp;Postman을&amp;nbsp;열어서&amp;nbsp;모두&amp;nbsp;서버에&amp;nbsp;접속한&amp;nbsp;상태에서&amp;nbsp;'좌상단,&amp;nbsp;우상단,&amp;nbsp;좌하단,&amp;nbsp;우하단'&amp;nbsp;순서로&amp;nbsp;메시지를&amp;nbsp;보냈다.&lt;br /&gt;'좌상단&amp;rsquo;이&amp;nbsp;메시지를&amp;nbsp;보내면&amp;nbsp;나머지&amp;nbsp;3개의&amp;nbsp;클라이언트가&amp;nbsp;메시지를&amp;nbsp;받는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;postman.jpg&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mjVWJ/btsNCmN3IlY/pYrDOidHmIT7jayEw2nYS1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mjVWJ/btsNCmN3IlY/pYrDOidHmIT7jayEw2nYS1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mjVWJ/btsNCmN3IlY/pYrDOidHmIT7jayEw2nYS1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmjVWJ%2FbtsNCmN3IlY%2FpYrDOidHmIT7jayEw2nYS1%2Fimg.jpg&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;1568&quot; height=&quot;1416&quot; data-filename=&quot;postman.jpg&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자서 4개의 클라이언트를 왔다 갔다 하며 메시지를 보내보면 잘 동작한다. 이 속도로는 멀티스레드 문제가 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레드 문제가 발생하려면 최소한 2개의 스레드에서 거의 동시에 하나의 웹소켓 세션에 메시지 전송을 시도 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;이 테스트는 3개의 클라이언트를 준비하고, 3개의 클라이언트가 각각 메시지를 연속으로 보낸다. 정상 동작은 1개의 클라이언트가 보낸 메시지를 다른 2개의 클라이언트가 받아야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 메시지를 받지 못하면 블로킹 큐에서 꺼낼 때 지정한 타임아웃 시간을 초과하고 null을 받는다. 모든 클라이언트가 받은 메시지를 result에 모아서 null이 포함되어 있는지 확인한다. null이 포함되었다는 건 메시지를 받지 못한 클라이언트가 있다는 것이다. 이 조건을 테스트 통과 조건으로 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&lt;span&gt; &lt;/span&gt;설정하는&lt;span&gt; &lt;/span&gt;이유는&lt;span&gt; &lt;/span&gt;어떤&lt;span&gt; &lt;/span&gt;클라이언트가&lt;span&gt; &lt;/span&gt;어떤&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;못&lt;span&gt; &lt;/span&gt;받을지는&lt;span&gt; &lt;/span&gt;알&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;없고&lt;span&gt;, &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;클라이언트가&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;받는&lt;span&gt; &lt;/span&gt;정상&lt;span&gt; &lt;/span&gt;동작은&lt;span&gt; &lt;/span&gt;지금&lt;span&gt; &lt;/span&gt;구현에서는&lt;span&gt; &lt;/span&gt;테스트를&lt;span&gt; &lt;/span&gt;실행하는&lt;span&gt; &lt;/span&gt;장비의&lt;span&gt; &lt;/span&gt;성능이&lt;span&gt; &lt;/span&gt;매우&lt;span&gt; &lt;/span&gt;좋아야&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;. &lt;/span&gt;만약&lt;span&gt;, &lt;/span&gt;현재&lt;span&gt; &lt;/span&gt;구현에서&lt;span&gt; &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;클라이언트가&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;받아서&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;테스트가&lt;span&gt; &lt;/span&gt;실패한다면&lt;span&gt;, &lt;/span&gt;테스트에&lt;span&gt; &lt;/span&gt;참여하는&lt;span&gt; &lt;/span&gt;클라이언트를&lt;span&gt; &lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;개&lt;span&gt; &lt;/span&gt;더&lt;span&gt; &lt;/span&gt;추가해&lt;span&gt; &lt;/span&gt;보자&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745750916327&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(
        classes = WebsocketMultiThreadExample,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SendMessageMultiThreadSpec extends Specification {

    @LocalServerPort
    int port
    
    def clientA, clientB, clientC
    
    def cleanup() {
        clientA.session?.close()
        clientB.session?.close()
        clientC.session?.close()
    }

    def 'Group Chat Basic Test'() {
        given:
        def url = &quot;ws://localhost:${port}/ws/v1/message&quot;
        (clientA, clientB, clientC) = [createClint(url), createClint(url), createClint(url)]

        when:
        clientA.session.sendMessage(new TextMessage('clientA: 안녕하세요. A 입니다.'))
        clientB.session.sendMessage(new TextMessage('clientB: 안녕하세요. B 입니다.'))
        clientC.session.sendMessage(new TextMessage('clientC: 안녕하세요. C 입니다.'))

        then:
        def result = (0..1).collect { clientA.queue.poll(1, TimeUnit.SECONDS) }
        result &amp;lt;&amp;lt; (0..1).collect { clientB.queue.poll(1, TimeUnit.SECONDS) }
        result &amp;lt;&amp;lt; (0..1).collect { clientC.queue.poll(1, TimeUnit.SECONDS) }
        
        and:
        result.contains(null)
    }

    static def createClint(String url) {
        BlockingQueue&amp;lt;String&amp;gt; blockingQueue = new ArrayBlockingQueue&amp;lt;&amp;gt;(5)
        def client = new StandardWebSocketClient()
        def webSocketSession = client.execute(new TextWebSocketHandler() {
            @Override
            protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
                blockingQueue.put(message.payload)
            }
        }, url).get()

        [queue: blockingQueue, session: webSocketSession]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;SendMessageMultiThreadSpec.groovy&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;/span&gt;테스트를&lt;span&gt; &lt;/span&gt;실행하면&lt;span&gt; &lt;/span&gt;서버&lt;span&gt; &lt;/span&gt;입장에서는&lt;span&gt; &lt;/span&gt;메시지&lt;span&gt; 3&lt;/span&gt;개가&lt;span&gt; &lt;/span&gt;거의&lt;span&gt; &lt;/span&gt;동시에&lt;span&gt; &lt;/span&gt;들어오는&lt;span&gt; &lt;/span&gt;것으로&lt;span&gt; &lt;/span&gt;처리되고&lt;span&gt;, 3&lt;/span&gt;개의&lt;span&gt; &lt;/span&gt;스레드에서&lt;span&gt; &lt;/span&gt;각각&lt;span&gt; &lt;/span&gt;전송자를&lt;span&gt; &lt;/span&gt;제외한&lt;span&gt; &lt;/span&gt;현재&lt;span&gt; &lt;/span&gt;연결된&lt;span&gt; &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;세션으로&lt;span&gt; &lt;/span&gt;메시지&lt;span&gt; &lt;/span&gt;전송하려고&lt;span&gt; &lt;/span&gt;시도한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를&lt;span&gt; &lt;/span&gt;들어&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; clientA&lt;/span&gt;는&lt;span&gt; clientB&lt;/span&gt;와&lt;span&gt; clientC&lt;/span&gt;에게&lt;span&gt; &lt;/span&gt;보내고&lt;span&gt; clientB&lt;/span&gt;는&lt;span&gt; clientA&lt;/span&gt;와&lt;span&gt; clientC&lt;/span&gt;에게&lt;span&gt; &lt;/span&gt;보내려고&lt;span&gt; &lt;/span&gt;시도한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;multithread (3).jpg&quot; data-origin-width=&quot;335&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cig8GU/btsNBLOlfwN/4yLUW9AQS3k3haDYHbwH10/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cig8GU/btsNBLOlfwN/4yLUW9AQS3k3haDYHbwH10/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cig8GU/btsNBLOlfwN/4yLUW9AQS3k3haDYHbwH10/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcig8GU%2FbtsNBLOlfwN%2F4yLUW9AQS3k3haDYHbwH10%2Fimg.jpg&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;335&quot; height=&quot;205&quot; data-filename=&quot;multithread (3).jpg&quot; data-origin-width=&quot;335&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, clientA의 스레드와 clientB의 스레드에서 clientC의 웹소켓 세션에 거의 동시에 메시지를 전송하려고 sendMessage()를 호출하는 상황이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이&lt;/span&gt; &lt;span&gt;상황이&lt;/span&gt; &lt;span&gt;발생하면&lt;/span&gt;, &lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; &lt;span&gt;테스트&lt;/span&gt; &lt;span&gt;실행&lt;/span&gt; &lt;span&gt;중&lt;/span&gt; &lt;span&gt;서버&lt;/span&gt; &lt;span&gt;로그에서&lt;/span&gt; 'The remote endpoint was in state [TEXT_PARTIAL_WRITING] which is an invalid state for called method' &lt;span&gt;라는&lt;/span&gt; &lt;span&gt;에러&lt;/span&gt; &lt;span&gt;메시지를&lt;/span&gt; &lt;span&gt;확인할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있다&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;test1.jpg&quot; data-origin-width=&quot;1989&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYcqT6/btsNCpD20bc/yl6mUsnqw7XwXR196dkTHk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYcqT6/btsNCpD20bc/yl6mUsnqw7XwXR196dkTHk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYcqT6/btsNCpD20bc/yl6mUsnqw7XwXR196dkTHk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYcqT6%2FbtsNCpD20bc%2Fyl6mUsnqw7XwXR196dkTHk%2Fimg.jpg&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;1989&quot; height=&quot;428&quot; data-filename=&quot;test1.jpg&quot; data-origin-width=&quot;1989&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에러가 발생하는 이유는 Spring WebSocketSession 인터페이스에 있는 sendMessage() 메서드의 주석을 확인해 보면 알 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Note: The underlying standard WebSocket session (JSR-356) does not allow concurrent sending. Therefore, sending must be synchronized. To ensure that, one option is to wrap the WebSocketSession with the ConcurrentWebSocketSessionDecorator.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sendMessage()는 스레드 세이프하지 않다. 멀티스레드에서 하나의 웹소켓 세션에 sendMessage()로 메시지를 보내는 것은 허용되지 않는다고 명시되어 있다. 그리고, ConcurrentWebSocketSessionDecorator를 사용하는 방법이 있다고 안내한다.&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;이제 ConcurrentWebSocketSessionDecorator를 사용하면 스레드 세이프한지 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentWebSocketSessionDecorator()는 간단히 적용할 수 있다. 파라미터로 WebSocketSession과 전송 타임아웃과 전송 버퍼 크기만 설정해 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 타임아웃 5초, 전송 버퍼 제한은 100kb로 설정했다. 버퍼 제한을 초과했을 때의 처리는 예외를 던질지 가장 오래된 메시지를 버퍼에서 버릴지 동작을 선택할 수 있고, 설정을 생략하면 예외를 발생시키는 OverflowStrategy.TERMINATE 가 기본값으로 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임아웃에 대한 동작을 설정하는 기능은 없고 타임아웃이 발생하면 예외를 던진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아래와&lt;/span&gt; &lt;span&gt;같이&lt;/span&gt; MessageHandler&lt;span&gt;를&lt;/span&gt; &lt;span&gt;상속받아서&lt;/span&gt; ConcurrentMessageHandler &lt;span&gt;빈을&lt;/span&gt; &lt;span&gt;하나&lt;/span&gt; &lt;span&gt;만든다&lt;/span&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1745751643924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class ConcurrentMessageHandler extends MessageHandler {

  private static final Logger log = LoggerFactory.getLogger(ConcurrentMessageHandler.class);

  @Override
  public void afterConnectionEstablished(WebSocketSession session) {
    log.info(&quot;ConnectionEstablished: {}&quot;, session.getId());
    sessions.put(
        session.getId(), new ConcurrentWebSocketSessionDecorator(session, 5000, 100 * 1024));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;handler/ConcurrentMessageHandler.java&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;/span&gt;만든&lt;span&gt; &lt;/span&gt;핸들러를&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;새로운&lt;span&gt; &lt;/span&gt;엔드포인트&lt;span&gt; '/ws/v2/message&amp;rsquo;&lt;/span&gt;로&lt;span&gt; &lt;/span&gt;연결한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745751736421&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocket
@SuppressWarnings(&quot;unused&quot;)
public class WebSocketHandlerConfig implements WebSocketConfigurer {

  private final MessageHandler messageHandler;
  private final ConcurrentMessageHandler concurrentMessageHandler;

  public WebSocketHandlerConfig(
      MessageHandler messageHandler, ConcurrentMessageHandler concurrentMessageHandler) {
    this.messageHandler = messageHandler;
    this.concurrentMessageHandler = concurrentMessageHandler;
  }

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry
        .addHandler(messageHandler, &quot;/ws/v1/message&quot;)
        .addHandler(concurrentMessageHandler, &quot;/ws/v2/message&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;config/WebSocketHandlerConfig.java&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;이제 '/ws/v2/message&amp;rsquo;로 연결되는 웹소켓 세션은 스레드 세이프하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 동시성 상황에서 정상 동작을 검증할 테스트를 하나 추가한다. 이&lt;span&gt; &lt;/span&gt;테스트는&lt;span&gt; &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;클라이언트가&lt;span&gt; &lt;/span&gt;기대하는&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; 2&lt;/span&gt;개씩&lt;span&gt; &lt;/span&gt;모두&lt;span&gt; &lt;/span&gt;받아서&lt;span&gt; &lt;/span&gt;정확히&lt;span&gt; 6&lt;/span&gt;개의&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;받았는지까지&lt;span&gt; &lt;/span&gt;검증한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745751800701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def 'Group Chat Concurrent Test'() {
    given:
    def url = &quot;ws://localhost:${port}/ws/v2/message&quot;
    (clientA, clientB, clientC) = [createClint(url), createClint(url), createClint(url)]

    when:
    clientA.session.sendMessage(new TextMessage('clientA: 안녕하세요. A 입니다.'))
    clientB.session.sendMessage(new TextMessage('clientB: 안녕하세요. B 입니다.'))
    clientC.session.sendMessage(new TextMessage('clientC: 안녕하세요. C 입니다.'))

    then:
    def resultA = (0..1).findResults { clientA.queue.poll(1, TimeUnit.SECONDS) }.join('')
    def resultB = (0..1).findResults { clientB.queue.poll(1, TimeUnit.SECONDS) }.join('')
    def resultC = (0..1).findResults { clientC.queue.poll(1, TimeUnit.SECONDS) }.join('')
    resultA.contains('clientB') &amp;amp;&amp;amp; resultA.contains('clientC')
    resultB.contains('clientA') &amp;amp;&amp;amp; resultB.contains('clientC')
    resultC.contains('clientA') &amp;amp;&amp;amp; resultC.contains('clientB')

    and:
    clientA.queue.isEmpty()
    clientB.queue.isEmpty()
    clientC.queue.isEmpty()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;SendMessageMultiThreadSpec.groovy&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;이 테스트에서도 BlockingQueue에 테스트 클라이언트가 받은 메시지를 모았다가 모두 꺼내서 합친 후에 받은 내용을 확인하는 이유는 메시지를 받는 순서를 알 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 clientA, clientB, clientC 순서로 메시지를 전송한다고 서버에서 각 클라이언트의 요청을 멀티스레드로 처리할 때 테스트 코드에서 전송한 순서와 같은 순서로 처리된다는 보장은 없다. 실행해 보면 테스트는 통과한다.&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;ConcurrentWebSocketSessionDecorator는 지금 테스트 코드에서 사용한 것과 같은 BlockingQueue를 사용하여 멀티스레드에서 들어오는 전송 요청을 모두 큐에 넣어놓고 하나씩 꺼내서 전송해 준다. 구현체는 테스트 코드에서 간단히 사용한 ArrayBlockingQueue가 아니라 LinkedBlockingQueue를 사용한다. 내부 구현이 복잡하지 않으니 한번 열어서 보는 것도 좋다.&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;span&gt;이상으로&lt;/span&gt; ConcurrentWebSocketSessionDecorator&lt;span&gt;에&lt;/span&gt; &lt;span&gt;대한&lt;/span&gt; &lt;span&gt;소개를&lt;/span&gt; &lt;span&gt;마치면서&lt;/span&gt;, &lt;span&gt;제&lt;/span&gt; &lt;span&gt;강의&lt;/span&gt; &lt;span&gt;쿠폰을&lt;/span&gt; &lt;span&gt;첨부합니다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30% 할인 코드명: 채팅플랫폼 (~25/5/12) *결제&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;창에서&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;쿠폰&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;코드&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;'&lt;/span&gt;채팅플랫폼&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;'&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;입력.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 링크: &lt;a href=&quot;https://buly.kr/Edt2csp&quot;&gt;https://buly.kr/Edt2csp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745815267654&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&quot; data-og-description=&quot;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&quot; data-og-host=&quot;fastcampus.co.kr&quot; data-og-source-url=&quot;https://buly.kr/Edt2csp&quot; data-og-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cCxvt6/hyYMednz48/LQoZX8jK49lJ5oSWyWz5a0/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/byYkeb/hyYMY2gzbH/JDARpS66vnmlWvK5LTkyC1/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/e6c9E/hyYIboo73y/1sD3fmKKyhd4PrZTPrZSC0/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157&quot;&gt;&lt;a href=&quot;https://buly.kr/Edt2csp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://buly.kr/Edt2csp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cCxvt6/hyYMednz48/LQoZX8jK49lJ5oSWyWz5a0/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/byYkeb/hyYMY2gzbH/JDARpS66vnmlWvK5LTkyC1/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/e6c9E/hyYIboo73y/1sD3fmKKyhd4PrZTPrZSC0/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157');&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fastcampus.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Dev</category>
      <category>concurrentwebsocketsessiondecorator</category>
      <category>Groovy</category>
      <category>Multithread</category>
      <category>Spock</category>
      <category>spring</category>
      <category>websocket</category>
      <category>멀티스레드</category>
      <category>스포크</category>
      <category>스프링</category>
      <category>웹소켓</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/362</guid>
      <comments>https://prostars.net/362#entry362comment</comments>
      <pubDate>Mon, 28 Apr 2025 14:01:27 +0900</pubDate>
    </item>
    <item>
      <title>IntelliJ의 Groovy Console 소개</title>
      <link>https://prostars.net/361</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오래전에&amp;nbsp;&lt;a href=&quot;https://prostars.net/237&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;'IntelliJ&amp;nbsp;의&amp;nbsp;JShell&amp;nbsp;Console&amp;nbsp;을&amp;nbsp;활용하자&amp;rsquo;&lt;/a&gt; 라는 포스팅을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 'IntelliJ의 Groovy Console&amp;rsquo;을 소개한다. 이 글은 Java 환경에서 JShell 대신 Groovy Console 사용하는 방법을 설명한다.&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;/span&gt; &lt;a href=&quot;https://fastcampus.co.kr/dev_online_chat&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;온라인&lt;/span&gt; &lt;span&gt;강의&lt;/span&gt;&lt;/a&gt;에서 &lt;span&gt;파트&lt;/span&gt; 2-&lt;span&gt;챕터&lt;/span&gt; 1 '&lt;span&gt;테스트에&lt;/span&gt; &lt;span&gt;대한&lt;/span&gt; &lt;span&gt;이야기&lt;/span&gt;&amp;rsquo;&lt;span&gt;에&lt;/span&gt; &lt;span&gt;있는&lt;/span&gt; '04.&amp;nbsp;Groovy&amp;nbsp;Console&amp;nbsp;소개&amp;rsquo;&lt;span&gt;와&lt;/span&gt; &amp;rsquo;05. Spock &lt;span&gt;사용을&lt;/span&gt; &lt;span&gt;위한&lt;/span&gt; Groovy &lt;span&gt;기본&lt;/span&gt; &lt;span&gt;문법&lt;/span&gt;&amp;rsquo; 2개의 영상에 &lt;span&gt;있는&lt;/span&gt; &lt;span&gt;내용&lt;/span&gt; &lt;span&gt;중에서&lt;/span&gt; 'Groovy &lt;span&gt;기본&lt;/span&gt; &lt;span&gt;문법&lt;/span&gt;&amp;rsquo;&lt;span&gt;에&lt;/span&gt; &lt;span&gt;대한&lt;/span&gt; &lt;span&gt;내용은&lt;/span&gt; &lt;span&gt;제외하고&lt;/span&gt; 'Groovy Console'&lt;span&gt;에&lt;/span&gt; &lt;span&gt;대해서&lt;/span&gt; 일부 글로 &lt;span&gt;정리한 것으로,&lt;/span&gt;&amp;nbsp;Groovy Console&lt;span&gt;에서&lt;/span&gt; &lt;span&gt;자바&lt;/span&gt; &lt;span&gt;문법을&lt;/span&gt; &lt;span&gt;사용하여 진행하므로 Groovy를 몰라도 무방하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이&lt;/span&gt; &lt;span&gt;글의&lt;/span&gt; &lt;span&gt;말미에&lt;/span&gt; Java&lt;span&gt;의&lt;/span&gt; Lambda&lt;span&gt;를&lt;/span&gt; &lt;span&gt;받는&lt;/span&gt; Java &lt;span&gt;객체에&lt;/span&gt; Groovy&lt;span&gt;의&lt;/span&gt; Closure&lt;span&gt;를&lt;/span&gt; &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있는지&lt;/span&gt; &lt;span&gt;확인하는&lt;/span&gt; &lt;span&gt;코드에만&lt;/span&gt; Groovy &lt;span&gt;문법을&lt;/span&gt; &lt;span&gt;한번&lt;/span&gt; &lt;span&gt;사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;코드는&amp;nbsp;모두&amp;nbsp;&lt;a href=&quot;https://github.com/prostars/preview_groovy_console&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;에&amp;nbsp;올라가&amp;nbsp;있다.&lt;/span&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;Java New Project 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&lt;span&gt; IntelliJ&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;새&lt;span&gt; &lt;/span&gt;프로젝트를&lt;span&gt; &lt;/span&gt;하나&lt;span&gt; &lt;/span&gt;생성한다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot_1.png&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGz8LZ/btsMxHNYPcj/Yom5Y9Rof2IV24fRrk5LR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGz8LZ/btsMxHNYPcj/Yom5Y9Rof2IV24fRrk5LR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGz8LZ/btsMxHNYPcj/Yom5Y9Rof2IV24fRrk5LR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGz8LZ%2FbtsMxHNYPcj%2FYom5Y9Rof2IV24fRrk5LR0%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;600&quot; height=&quot;496&quot; data-filename=&quot;groovy_console_screenshot_1.png&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1326&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;span&gt; &lt;/span&gt;예제에서&lt;span&gt; Main.java &lt;/span&gt;파일은&lt;span&gt; &lt;/span&gt;사용하지&lt;span&gt; &lt;/span&gt;않으니&lt;span&gt; &lt;/span&gt;삭제한다.&lt;span&gt;&lt;/span&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;Groovy Console 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ 2024.3 버전 기준으로 Groovy 플러그인이 기본으로 활성화된 상태로 추가 설정 없이 Groovy Console을 바로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, IntelliJ의 도움말 문서 &lt;a href=&quot;https://www.jetbrains.com/help/idea/interactive-groovy-console.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;'Interactive Groovy console'&lt;/a&gt;에는 현재 열려있는 프로젝트 종속성에 Groovy 라이브러리가 포함되어 있지 않은 상태로 그루비 콘솔을 사용하면 번들로 제공되는 2.3.9 버전의 Groovy 가 사용될 수 있다고 설명되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;코드는&lt;span&gt; Groovy &lt;/span&gt;최신&lt;span&gt; &lt;/span&gt;버전이&lt;span&gt; &lt;/span&gt;필요하지&lt;span&gt; &lt;/span&gt;않으니&lt;span&gt;, IntelliJ&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;번들로&lt;span&gt; &lt;/span&gt;제공하는&lt;span&gt; Groovy&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;그대로&lt;span&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;IntelliJ&lt;span&gt;가&lt;/span&gt; &lt;span&gt;기본&lt;/span&gt; &lt;span&gt;제공하는&lt;/span&gt; Groovy Console&lt;span&gt;은&lt;/span&gt; &lt;span&gt;아래&lt;/span&gt; &lt;span&gt;스크린샷에&lt;/span&gt; &lt;span&gt;보이는&lt;/span&gt; &lt;span&gt;것처럼&lt;/span&gt; Main Menu&lt;span&gt;에서&lt;/span&gt; Tools&lt;span&gt;에&lt;/span&gt; 'Groovy Console&amp;rsquo;&lt;span&gt;이라는&lt;/span&gt; &lt;span&gt;메뉴명을&lt;/span&gt; &lt;span&gt;선택하여&lt;/span&gt; &lt;span&gt;실행할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot0.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HZ0Iy/btsMzPKILci/fkikH1aKaE07SkRLtwgxUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HZ0Iy/btsMzPKILci/fkikH1aKaE07SkRLtwgxUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HZ0Iy/btsMzPKILci/fkikH1aKaE07SkRLtwgxUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHZ0Iy%2FbtsMzPKILci%2FfkikH1aKaE07SkRLtwgxUK%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;250&quot; height=&quot;348&quot; data-filename=&quot;groovy_console_screenshot0.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Groovy Console&amp;rsquo;&lt;span&gt;이라는&lt;/span&gt; &lt;span&gt;메뉴를&lt;/span&gt; &lt;span&gt;선택하여&lt;/span&gt; &lt;span&gt;실행하면&lt;/span&gt; &lt;span&gt;아래&lt;/span&gt; &lt;span&gt;스크린샷에&lt;/span&gt; &lt;span&gt;보이는&lt;/span&gt; &lt;span&gt;것처럼&lt;/span&gt; 'groovy_console.groovy&amp;rsquo;&lt;span&gt;라는&lt;/span&gt; &lt;span&gt;파일명을&lt;/span&gt; &lt;span&gt;기본값으로&lt;/span&gt; &lt;span&gt;하여&lt;/span&gt; &lt;span&gt;파일이&lt;/span&gt; &lt;span&gt;하나&lt;/span&gt; 열린다. &lt;span&gt;파일 탭&lt;/span&gt; &lt;span&gt;아래에&lt;/span&gt; &lt;span&gt;있는&lt;/span&gt; &lt;span&gt;플레이&lt;/span&gt; &lt;span&gt;아이콘&lt;/span&gt; &lt;span&gt;옆에&lt;/span&gt; &amp;rsquo;Select Module...'&lt;span&gt;을&lt;/span&gt; &lt;span&gt;클릭해서&lt;/span&gt; Groovy Console&lt;span&gt;이&lt;/span&gt; &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;클래스&lt;/span&gt; &lt;span&gt;패스를&lt;/span&gt; &lt;span&gt;선택할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot.png&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5VT4u/btsMAIjU1PG/oi1kK1VdyarYQy9XKdcDmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5VT4u/btsMAIjU1PG/oi1kK1VdyarYQy9XKdcDmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5VT4u/btsMAIjU1PG/oi1kK1VdyarYQy9XKdcDmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5VT4u%2FbtsMAIjU1PG%2Foi1kK1VdyarYQy9XKdcDmk%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;600&quot; height=&quot;193&quot; data-filename=&quot;groovy_console_screenshot.png&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 스크린샷에 보이는 것처럼 3개의 선택지에서 Groovy Console이 사용할 클래스 패스로 main 모듈의 클래스 패스를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택하고&lt;span&gt; &lt;/span&gt;나면&lt;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;설정이&lt;span&gt; &lt;/span&gt;적용된다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screen2.png&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coxdvC/btsMzNMSUcM/KFkQ5kq0fy9bmkds6wEHBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coxdvC/btsMzNMSUcM/KFkQ5kq0fy9bmkds6wEHBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coxdvC/btsMzNMSUcM/KFkQ5kq0fy9bmkds6wEHBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoxdvC%2FbtsMzNMSUcM%2FKFkQ5kq0fy9bmkds6wEHBk%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;600&quot; height=&quot;67&quot; data-filename=&quot;groovy_console_screen2.png&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 하면, 이제 Groovy Console에서 main 모듈에 있는 클래스를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;다만, 소스 파일을 참조해서 사용하는 것이 아니라 클래스 패스에 있는 클래스 파일을 사용하는 것이라서 빌드가 먼저 되어있어야 한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고, 이 부분이 이전에 소개했던 'JShell Console&amp;rsquo;과 다른 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;'JShell Console&amp;rsquo;에서는 프로젝트의 클래스를 사용하려면 IntelliJ 프로젝트 설정에서 라이브러리 설정에 클래스 패스를 직접 등록해야 했지만, 'Groovy Console&amp;rsquo;은 이런 과정 없이 바로 사용할 수 있다.&lt;/span&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;먼저, Groovy Console이 정상 동작하는지 확인하기 위해서 간단히 'Hello World.'를 출력해 본다.&lt;/p&gt;
&lt;pre id=&quot;code_1740725362907&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;System.out.println(&quot;Hello World.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위의 코드를 열려있는 Groovy Console 편집 창에 입력합니다. 세미콜론은 생략할 수 있다. 플레이 아이콘을 클릭하거나 단축키 Command + Enter (MacOS 기준)를 입력하면 실행된다. 실행하면 아래와 같은 실행 결과를 확인할 수 있다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screen3.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uqDzv/btsMzY8CyU2/kdjQCgLnzDdWNkoZFmb0jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uqDzv/btsMzY8CyU2/kdjQCgLnzDdWNkoZFmb0jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uqDzv/btsMzY8CyU2/kdjQCgLnzDdWNkoZFmb0jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuqDzv%2FbtsMzY8CyU2%2FkdjQCgLnzDdWNkoZFmb0jk%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;400&quot; height=&quot;102&quot; data-filename=&quot;groovy_console_screen3.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&lt;span&gt; Groovy Console&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;코드가&lt;span&gt; &lt;/span&gt;필요하니&lt;span&gt;, &lt;/span&gt;간단한&lt;span&gt; &lt;/span&gt;도서관&lt;span&gt; &lt;/span&gt;예제를&lt;span&gt; &lt;/span&gt;하나&lt;span&gt; &lt;/span&gt;구성하자.&lt;/p&gt;
&lt;pre id=&quot;code_1740725504015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Book.java
package org.example;

public record Book(String isbn, String title, boolean available) {}&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;단독으로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;실행할&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;프로젝트가&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;아니므로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;BookRepository&lt;/span&gt;와&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;PushService를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;인터페이스로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;간단히&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1740725568185&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BookRepository.java
package org.example;

import java.util.Optional;

public interface BookRepository {
  Optional&amp;lt;Book&amp;gt; findBookByIsbn(String isbn);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1740725589495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PushService.java
package org.example;

public interface PushService {
  void notification(String message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1740725658388&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LibraryService.java
package org.example;

import java.util.Optional;

public class LibraryService {
  private final BookRepository bookRepository;
  private final PushService pushService;

  public LibraryService(BookRepository bookRepository, PushService pushService) {
    this.bookRepository = bookRepository;
    this.pushService = pushService;
  }

  public boolean isBookAvailable(String isbn) {
    return bookRepository
            .findBookByIsbn(isbn)
            .map(Book::available)
            .orElse(false);
  }

  public Optional&amp;lt;String&amp;gt; borrowBook(String isbn) {
    return bookRepository
        .findBookByIsbn(isbn)
        .filter(Book::available)
        .map(
            book -&amp;gt; {
              pushService.notification(
              	&quot;대출 완료: &quot; + book.title());
              return book.title();
            });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제&lt;/span&gt; Groovy Console&lt;span&gt;에서&lt;/span&gt; &lt;span&gt;사용해&lt;/span&gt; &lt;span&gt;볼&lt;/span&gt; &lt;span&gt;클래스가&lt;/span&gt; &lt;span&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;/span&gt; &lt;span&gt;언급했지만&lt;/span&gt;, BookRepository&lt;span&gt;와&lt;/span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;PushService는 &lt;span&gt;인터페이스만&lt;/span&gt; &lt;span&gt;있기에&lt;/span&gt; &lt;span&gt;지금&lt;/span&gt; &lt;span&gt;준비된&lt;/span&gt; &lt;span&gt;코드만으로는&lt;/span&gt; LibraryService&lt;span&gt;를&lt;/span&gt; &lt;span&gt;바로&lt;/span&gt; &lt;span&gt;생성하여&lt;/span&gt; &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;없다&lt;/span&gt;. &lt;span&gt;하지만&lt;/span&gt;, Groovy Console&lt;span&gt;과&lt;/span&gt; &lt;span&gt;같은&lt;/span&gt; REPL(Read-Eval-Print Loop)&lt;span&gt;은&lt;/span&gt; &lt;span&gt;빠른&lt;/span&gt; &lt;span&gt;프로토&lt;/span&gt;&lt;span&gt;타이핑을&lt;/span&gt; &lt;span&gt;위해서&lt;/span&gt; &lt;span&gt;임시로&lt;/span&gt; &lt;span&gt;사용할&lt;/span&gt; &lt;span&gt;코드&lt;/span&gt; &lt;span&gt;조각을&lt;/span&gt; &lt;span&gt;바로&lt;/span&gt; &lt;span&gt;만들어서&lt;/span&gt; &lt;span&gt;사용해&lt;/span&gt; &lt;span&gt;볼&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&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;&lt;span&gt;이제&lt;/span&gt; &lt;span&gt;위에서&lt;/span&gt; &lt;span&gt;열었던&lt;/span&gt; 'groovy_console.groovy' &lt;span&gt;파일로&lt;/span&gt; &lt;span&gt;돌아가서&lt;/span&gt;, &lt;span&gt;아래의&lt;/span&gt; &lt;span&gt;코드를&lt;/span&gt; &lt;span&gt;입력한다&lt;/span&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1740725982456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.example.Book
import org.example.BookRepository
import org.example.LibraryService
import org.example.PushService

class BookRepositoryImpl implements BookRepository {
    @Override
    Optional&amp;lt;Book&amp;gt; findBookByIsbn(String isbn) {
        return Optional.of(
        	new Book(&quot;1234&quot;, &quot;Groovy Console&quot;, true))
    }
}

class PushServiceImpl implements PushService {
    @Override
    void notification(String message) {
        System.out.println(&quot;pushed : &quot; + message)
    }
}

BookRepository bookRepository = new BookRepositoryImpl()
PushService pushService = 
	new PushServiceImpl()
LibraryService libraryService = 
	new LibraryService(bookRepository, pushService)

String isbn = &quot;1234&quot;
if (libraryService.isBookAvailable(isbn)) {
    libraryService.borrowBook(isbn)
            .ifPresentOrElse(title -&amp;gt; 
            	System.out.println(&quot;대출 도서 : &quot; + title),
                    () -&amp;gt; System.out.println(&quot;대출 불가&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 main 클래스 패스에 있는 클래스를 사용하여 LibraryService에 주입할 BookRepositoryImpl,&lt;span&gt;&amp;nbsp; &lt;/span&gt;PushServiceImpl 클래스를 모두 하나의 파일에서 정의할 수 있다. 그리고&lt;span&gt;, &lt;/span&gt;준비된&lt;span&gt; &lt;/span&gt;클래스를&lt;span&gt; &lt;/span&gt;사용하여&lt;span&gt; LibraryService &lt;/span&gt;객체를&lt;span&gt; &lt;/span&gt;생성할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&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;Groovy Console&lt;/span&gt;에서&lt;span&gt; &lt;/span&gt;실행하면&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;실행&lt;span&gt; &lt;/span&gt;결과를&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot4.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;1468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NVynI/btsMx4PT3pc/GK4rsW9kp36hrgZc8CRZj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NVynI/btsMx4PT3pc/GK4rsW9kp36hrgZc8CRZj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NVynI/btsMx4PT3pc/GK4rsW9kp36hrgZc8CRZj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNVynI%2FbtsMx4PT3pc%2FGK4rsW9kp36hrgZc8CRZj1%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;600&quot; height=&quot;650&quot; data-filename=&quot;groovy_console_screenshot4.png&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;1468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 구현한 인터페이스 2개가 모두 1개의 메서드만 가지고 있는 단일 추상 메서드(SAM, Single Abstract Method) 인터페이스이므로 람다로 대체할 수 있다. 우리는&lt;span&gt; REPL&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;사용하고&lt;span&gt; &lt;/span&gt;있으니&lt;span&gt; &lt;/span&gt;더&lt;span&gt; &lt;/span&gt;간결하게&lt;span&gt; &lt;/span&gt;수정할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있는지&lt;span&gt; &lt;/span&gt;바로&lt;span&gt; &lt;/span&gt;테스트해&lt;span&gt; &lt;/span&gt;볼&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&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;/span&gt;코드를&lt;span&gt; &lt;/span&gt;추가하고&lt;span&gt; &lt;/span&gt;실행한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740726122168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;println '--------------------------------------'
LibraryService libraryServiceUsingLambda = 
    new LibraryService(
        (String ignored) -&amp;gt; Optional&amp;lt;String&amp;gt;.of(
            new Book(&quot;1234&quot;, &quot;Java Lambda&quot;, true)),
        (String message) -&amp;gt; println(&quot;pushed : $message&quot;))
if (libraryServiceUsingLambda.isBookAvailable(isbn)) {
    libraryServiceUsingLambda.borrowBook(isbn)
            .ifPresentOrElse(title -&amp;gt;
            	System.out.println(&quot;대출 도서 : &quot; + title),
                    () -&amp;gt; System.out.println(&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;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같은&lt;span&gt; &lt;/span&gt;실행&lt;span&gt; &lt;/span&gt;결과를&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot5.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blheED/btsMxBtIhhi/YZheEgr12YrEdds5JYHmK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blheED/btsMxBtIhhi/YZheEgr12YrEdds5JYHmK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blheED/btsMxBtIhhi/YZheEgr12YrEdds5JYHmK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblheED%2FbtsMxBtIhhi%2FYZheEgr12YrEdds5JYHmK1%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;600&quot; height=&quot;330&quot; data-filename=&quot;groovy_console_screenshot5.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;788&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;span&gt; &lt;/span&gt;마지막으로&lt;span&gt; &lt;/span&gt;자바의&lt;span&gt; &lt;/span&gt;객체에&lt;span&gt; Lambda &lt;/span&gt;대신&lt;span&gt; Groovy&lt;/span&gt;의&lt;span&gt; Closure&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있는지&lt;span&gt; &lt;/span&gt;테스트해&lt;span&gt; &lt;/span&gt;보자&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740726217040&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;println '--------------------------------------'
LibraryService libraryServiceUsingClosure = new LibraryService(
        { Optional&amp;lt;String&amp;gt;.of(
        	new Book(&quot;1234&quot;, &quot;Groovy Closure&quot;, true)) },
        { println(&quot;pushed : $it&quot;) })
if (libraryServiceUsingClosure.isBookAvailable(isbn)) {
    libraryServiceUsingClosure.borrowBook(isbn)
            .ifPresentOrElse({ println(&quot;대출 도서 : &quot; + it) },
                    { System.out.println(&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;위의 코드는 자바의 Lambda를 사용한 부분을 Groovy의 Closure로 교체한 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행해보면&lt;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;모두&lt;span&gt; &lt;/span&gt;실행됨을&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot6.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFuZ2D/btsMz5ftBar/sBpxlieXYGLxJInEVEE6Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFuZ2D/btsMz5ftBar/sBpxlieXYGLxJInEVEE6Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFuZ2D/btsMz5ftBar/sBpxlieXYGLxJInEVEE6Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFuZ2D%2FbtsMz5ftBar%2FsBpxlieXYGLxJInEVEE6Bk%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;600&quot; height=&quot;390&quot; data-filename=&quot;groovy_console_screenshot6.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;814&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;이제 Groovy Console 에 코드가 제법 많아졌다. 매번 실행할 때마다 현재 Groovy Console에 있는 코드를 모두 실행하지 않고 일부 코드만 바로 실행해 볼 필요가 있을 때도 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴&lt;span&gt; &lt;/span&gt;때는&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;실행하고자&lt;span&gt; &lt;/span&gt;하는&lt;span&gt; &lt;/span&gt;코드만&lt;span&gt; &lt;/span&gt;선택해서&lt;span&gt; &lt;/span&gt;실행할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot7.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUqIR/btsMAFOgcIy/puYW1bJCmV9KJRIuCxYN10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUqIR/btsMAFOgcIy/puYW1bJCmV9KJRIuCxYN10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUqIR/btsMAFOgcIy/puYW1bJCmV9KJRIuCxYN10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkUqIR%2FbtsMAFOgcIy%2FpuYW1bJCmV9KJRIuCxYN10%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;600&quot; height=&quot;351&quot; data-filename=&quot;groovy_console_screenshot7.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;774&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;span&gt; &lt;/span&gt;블록을&lt;span&gt; &lt;/span&gt;선택하고&lt;span&gt; &lt;/span&gt;실행해&lt;span&gt; &lt;/span&gt;보면&lt;span&gt; &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; LibraryService&lt;/span&gt;의&lt;span&gt; &lt;/span&gt;객체가&lt;span&gt; &lt;/span&gt;생성되는&lt;span&gt; &lt;/span&gt;실행&lt;span&gt; &lt;/span&gt;결과를&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot8.png&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;1106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bszigq/btsMy2DEepE/KAa0khvnfFCTf8cQfP3uMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bszigq/btsMy2DEepE/KAa0khvnfFCTf8cQfP3uMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bszigq/btsMy2DEepE/KAa0khvnfFCTf8cQfP3uMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbszigq%2FbtsMy2DEepE%2FKAa0khvnfFCTf8cQfP3uMk%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;600&quot; height=&quot;503&quot; data-filename=&quot;groovy_console_screenshot8.png&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;1106&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;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;코드를&lt;span&gt; &lt;/span&gt;선택하여&lt;span&gt; &lt;/span&gt;실행하면&lt;span&gt; &lt;/span&gt;안 된다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot9.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BAPfA/btsMxJLO5UQ/DA5iJMkdtbEaVnpAMpeS2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BAPfA/btsMxJLO5UQ/DA5iJMkdtbEaVnpAMpeS2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BAPfA/btsMxJLO5UQ/DA5iJMkdtbEaVnpAMpeS2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBAPfA%2FbtsMxJLO5UQ%2FDA5iJMkdtbEaVnpAMpeS2k%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;600&quot; height=&quot;221&quot; data-filename=&quot;groovy_console_screenshot9.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;454&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;span&gt; &lt;/span&gt;선택한&lt;span&gt; &lt;/span&gt;코드&lt;span&gt; &lt;/span&gt;블록에&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;isbn &lt;/span&gt;변수&lt;span&gt; &lt;/span&gt;정의가&lt;span&gt; &lt;/span&gt;없기&lt;span&gt; &lt;/span&gt;때문에&lt;span&gt; &lt;/span&gt;에러가&lt;span&gt; &lt;/span&gt;발생한다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot10.png&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4pgEm/btsMyS2iAU8/lKvGlfE66W5Yv85ShAMnLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4pgEm/btsMyS2iAU8/lKvGlfE66W5Yv85ShAMnLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4pgEm/btsMyS2iAU8/lKvGlfE66W5Yv85ShAMnLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4pgEm%2FbtsMyS2iAU8%2FlKvGlfE66W5Yv85ShAMnLK%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;600&quot; height=&quot;298&quot; data-filename=&quot;groovy_console_screenshot10.png&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;856&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;span&gt; &lt;/span&gt;같이&lt;span&gt; isbn &lt;/span&gt;변수를&lt;span&gt; &lt;/span&gt;정의해주면&lt;span&gt; IntelliJ &lt;/span&gt;가&lt;span&gt; &lt;/span&gt;친절하게&lt;span&gt; &lt;/span&gt;이미&lt;span&gt; &lt;/span&gt;존재하는&lt;span&gt; &lt;/span&gt;변수라고&lt;span&gt; &lt;/span&gt;경고한다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot11.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9wJgN/btsMxDkEmZw/a8DsTsAK9U6vRMtB0VyU41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9wJgN/btsMxDkEmZw/a8DsTsAK9U6vRMtB0VyU41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9wJgN/btsMxDkEmZw/a8DsTsAK9U6vRMtB0VyU41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9wJgN%2FbtsMxDkEmZw%2Fa8DsTsAK9U6vRMtB0VyU41%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;600&quot; height=&quot;238&quot; data-filename=&quot;groovy_console_screenshot11.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;492&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;span&gt; &lt;/span&gt;상태에서&lt;span&gt; &lt;/span&gt;코드&lt;span&gt; &lt;/span&gt;블록&lt;span&gt; &lt;/span&gt;선택&lt;span&gt; &lt;/span&gt;없이&lt;span&gt; &lt;/span&gt;콘솔&lt;span&gt; &lt;/span&gt;전체를&lt;span&gt; &lt;/span&gt;실행하면&lt;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;경고받은&lt;span&gt; &lt;/span&gt;그대로&lt;span&gt; &lt;/span&gt;에러가&lt;span&gt; &lt;/span&gt;발생한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot12.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejh7hg/btsMzhAKgLM/M0EQrGgtVtDO2GXkKjtjF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejh7hg/btsMzhAKgLM/M0EQrGgtVtDO2GXkKjtjF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejh7hg/btsMzhAKgLM/M0EQrGgtVtDO2GXkKjtjF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fejh7hg%2FbtsMzhAKgLM%2FM0EQrGgtVtDO2GXkKjtjF0%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;600&quot; height=&quot;229&quot; data-filename=&quot;groovy_console_screenshot12.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;566&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;span&gt;, &lt;/span&gt;아래와&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;블록을&lt;span&gt; &lt;/span&gt;선택하고&lt;span&gt; &lt;/span&gt;실행하면&lt;span&gt; &lt;/span&gt;에러&lt;span&gt; &lt;/span&gt;없이&lt;span&gt; &lt;/span&gt;실행된다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot13.png&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d44aeR/btsMyT1dnHZ/gxo4PoVVFgKYGxW1aONS7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d44aeR/btsMyT1dnHZ/gxo4PoVVFgKYGxW1aONS7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d44aeR/btsMyT1dnHZ/gxo4PoVVFgKYGxW1aONS7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd44aeR%2FbtsMyT1dnHZ%2Fgxo4PoVVFgKYGxW1aONS7k%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;600&quot; height=&quot;259&quot; data-filename=&quot;groovy_console_screenshot13.png&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;536&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;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;선택한&lt;span&gt; &lt;/span&gt;코드&lt;span&gt; &lt;/span&gt;블록만&lt;span&gt; &lt;/span&gt;실행된&lt;span&gt; &lt;/span&gt;결과를&lt;span&gt; &lt;/span&gt;확인할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;groovy_console_screenshot14.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MYZop/btsMzQizh8f/ZfFkdfcZhuHsdeqfaAx8MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MYZop/btsMzQizh8f/ZfFkdfcZhuHsdeqfaAx8MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MYZop/btsMzQizh8f/ZfFkdfcZhuHsdeqfaAx8MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMYZop%2FbtsMzQizh8f%2FZfFkdfcZhuHsdeqfaAx8MK%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;600&quot; height=&quot;274&quot; data-filename=&quot;groovy_console_screenshot14.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;572&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;계속&lt;span&gt;&amp;nbsp;&lt;/span&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;/span&gt; IntelliJ&lt;span&gt;의&lt;/span&gt; Groovy Console&lt;span&gt;에&lt;/span&gt; &lt;span&gt;대한&lt;/span&gt; &lt;span&gt;소개를&lt;/span&gt; &lt;span&gt;마치면서, 제 강의 쿠폰을 첨부합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740727304442&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&quot; data-og-description=&quot;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&quot; data-og-host=&quot;fastcampus.co.kr&quot; data-og-source-url=&quot;https://bit.ly/43uEFGz&quot; data-og-url=&quot;https://fastcampus.co.kr/dev_online_chat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gBDXx/hyYmXiqMwh/McuRLYy8JDZhMIIdMglaHK/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/bkSrXB/hyYm0MXIx9/mvAWUsFnspYHk1vV1JAYLk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/ngw56/hyYjxr9RGl/gcJ8TsoKYBCWAG6x8uNMm1/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157&quot;&gt;&lt;a href=&quot;https://bit.ly/43uEFGz&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bit.ly/43uEFGz&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gBDXx/hyYmXiqMwh/McuRLYy8JDZhMIIdMglaHK/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/bkSrXB/hyYm0MXIx9/mvAWUsFnspYHk1vV1JAYLk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/ngw56/hyYjxr9RGl/gcJ8TsoKYBCWAG6x8uNMm1/img.png?width=400&amp;amp;height=300&amp;amp;face=154_67_237_157');&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;대규모 채팅 플랫폼으로 한 번에 끝내는 실전 대용량 트래픽 커버 완전판 | 패스트캠퍼스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전 카톡 서버 운영자가 알려주는 채팅 플랫폼 기반 '대용량' 트래픽 처리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fastcampus.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[FC 1위] 패스트캠퍼스 대용량 1위 기념! 강사 특별 할인 30% 쿠폰코드: PRDTEA250225_chat (~3/16)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Dev</category>
      <category>Groovy</category>
      <category>groovy console</category>
      <category>IntelliJ</category>
      <category>REPL</category>
      <category>그루비</category>
      <category>그루비 콘솔</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/361</guid>
      <comments>https://prostars.net/361#entry361comment</comments>
      <pubDate>Tue, 4 Mar 2025 14:12:43 +0900</pubDate>
    </item>
    <item>
      <title>저의 첫 온라인 강의가 론칭되었습니다.</title>
      <link>https://prostars.net/360</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;br /&gt;2025년&amp;nbsp;새해도&amp;nbsp;벌써&amp;nbsp;한&amp;nbsp;달이&amp;nbsp;지나가고&amp;nbsp;있네요.&lt;br /&gt;한동안&amp;nbsp;포스팅이&amp;nbsp;뜸했지만,&amp;nbsp;현재&amp;nbsp;작업&amp;nbsp;중인&amp;nbsp;자바&amp;nbsp;백엔드&amp;nbsp;개발&amp;nbsp;온라인&amp;nbsp;강의가&amp;nbsp;론칭되었습니다.&lt;br /&gt;페이지가&amp;nbsp;공식적으로&amp;nbsp;오픈되고&amp;nbsp;나니&amp;nbsp;막연한&amp;nbsp;부담감도&amp;nbsp;있지만,&amp;nbsp;더욱&amp;nbsp;집중해서&amp;nbsp;잘&amp;nbsp;만들어보겠습니다.&lt;br /&gt;이번에는&amp;nbsp;간단히&amp;nbsp;강의&amp;nbsp;소개&amp;nbsp;링크만&amp;nbsp;공유하고,&amp;nbsp;이후에는&amp;nbsp;강의&amp;nbsp;예제&amp;nbsp;중에서&amp;nbsp;하나씩&amp;nbsp;선정하여&amp;nbsp;포스팅하며&amp;nbsp;다시&amp;nbsp;소개해&amp;nbsp;보겠습니다.&lt;br /&gt;새해&amp;nbsp;복&amp;nbsp;많이&amp;nbsp;받으세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;lecture.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;408&quot;&gt;&lt;a href=&quot;https://bit.ly/4gYpARj&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yksbl/btsL1CEvnwP/7sdwXqvY10LKFCo5Bo9zS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyksbl%2FbtsL1CEvnwP%2F7sdwXqvY10LKFCo5Bo9zS1%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;1720&quot; height=&quot;408&quot; data-filename=&quot;lecture.png&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;쿠폰코드:&amp;nbsp;&amp;nbsp;PRDSAL250124_b&amp;nbsp;(~2/9)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>I'm prostars</category>
      <category>Backend</category>
      <category>Java</category>
      <category>SpringBoot</category>
      <category>메시지 시스템</category>
      <category>백엔드</category>
      <category>분산 시스템</category>
      <category>온라인강의</category>
      <category>채팅 시스템</category>
      <category>패스트캠퍼스</category>
      <category>핸즈온</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/360</guid>
      <comments>https://prostars.net/360#entry360comment</comments>
      <pubDate>Fri, 24 Jan 2025 17:01:42 +0900</pubDate>
    </item>
    <item>
      <title>츠바키 문구점</title>
      <link>https://prostars.net/359</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올해&lt;span&gt; &lt;/span&gt;들어&lt;span&gt; &lt;/span&gt;처음&lt;span&gt; &lt;/span&gt;읽은&lt;span&gt; &lt;/span&gt;소설이다&lt;span&gt;. &lt;/span&gt;어쩌다&lt;span&gt; &lt;/span&gt;보니&lt;span&gt; &lt;/span&gt;인문학과&lt;span&gt; &lt;/span&gt;기술서만&lt;span&gt; &lt;/span&gt;읽었네&lt;span&gt;. &lt;/span&gt;그렇다고&lt;span&gt; &lt;/span&gt;이번에&lt;span&gt; &lt;/span&gt;새로&lt;span&gt; &lt;/span&gt;산&lt;span&gt; &lt;/span&gt;책도&lt;span&gt; &lt;/span&gt;아니다&lt;span&gt;. &lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;년&lt;span&gt; &lt;/span&gt;전&lt;span&gt; NHN &lt;/span&gt;다니던&lt;span&gt; &lt;/span&gt;시절에&lt;span&gt; &lt;/span&gt;사내&lt;span&gt; &lt;/span&gt;이벤트로&lt;span&gt; &lt;/span&gt;받은&lt;span&gt; &lt;/span&gt;책으로&lt;span&gt; &lt;/span&gt;기억한다&lt;span&gt;. &lt;/span&gt;독서&lt;span&gt; &lt;/span&gt;모임&lt;span&gt; &lt;/span&gt;토론&lt;span&gt; &lt;/span&gt;주제를&lt;span&gt; &lt;/span&gt;발제해야&lt;span&gt; &lt;/span&gt;하는데&lt;span&gt;, &lt;/span&gt;겸사겸사&lt;span&gt; &lt;/span&gt;독서&lt;span&gt; &lt;/span&gt;후기도&lt;span&gt; &lt;/span&gt;적어본다&lt;span&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;/span&gt;영화&lt;span&gt; '&lt;/span&gt;리틀&lt;span&gt; &lt;/span&gt;포레스트&lt;span&gt;&amp;rsquo;&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;생각나는&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;책은&lt;span&gt; &lt;/span&gt;참&lt;span&gt; &lt;/span&gt;몽글몽글한 느낌이다&lt;span&gt;. &lt;/span&gt;내가&lt;span&gt; &lt;/span&gt;마지막으로&lt;span&gt; &lt;/span&gt;서류가&lt;span&gt; &lt;/span&gt;아닌&lt;span&gt; &lt;/span&gt;종이에&lt;span&gt; &lt;/span&gt;무언가를&lt;span&gt; &lt;/span&gt;적어본&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;언제인지도&lt;span&gt; &lt;/span&gt;가물가물하다&lt;span&gt;. &lt;/span&gt;업무&lt;span&gt; &lt;/span&gt;다이어리도&lt;span&gt; &lt;/span&gt;아이패드에&lt;span&gt; &lt;/span&gt;사용할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;터치펜이라는&lt;span&gt; &lt;/span&gt;물건을&lt;span&gt; &lt;/span&gt;한&lt;span&gt; 10&lt;/span&gt;년&lt;span&gt; &lt;/span&gt;전에&lt;span&gt; &lt;/span&gt;손에&lt;span&gt; &lt;/span&gt;넣은&lt;span&gt; &lt;/span&gt;이후로&lt;span&gt; &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;노트&lt;span&gt; &lt;/span&gt;정리는&lt;span&gt; &lt;/span&gt;디지털로&lt;span&gt; &lt;/span&gt;바뀌었다&lt;span&gt;. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_1115.jpg&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d13sMh/btsJcLd5Tkn/vsbvlZIHVwuA3ZoKKH3t9K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d13sMh/btsJcLd5Tkn/vsbvlZIHVwuA3ZoKKH3t9K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d13sMh/btsJcLd5Tkn/vsbvlZIHVwuA3ZoKKH3t9K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd13sMh%2FbtsJcLd5Tkn%2FvsbvlZIHVwuA3ZoKKH3t9K%2Fimg.jpg&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;286&quot; data-filename=&quot;IMG_1115.jpg&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시&lt;span&gt; &lt;/span&gt;디지털이라&lt;span&gt; 10&lt;/span&gt;년&lt;span&gt; &lt;/span&gt;전&lt;span&gt; &lt;/span&gt;회의&lt;span&gt; &lt;/span&gt;시간에&lt;span&gt; &lt;/span&gt;딴짓한&lt;span&gt; &lt;/span&gt;흔적을&lt;span&gt; &lt;/span&gt;바로&lt;span&gt; &lt;/span&gt;찾아서&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;글에&lt;span&gt; &lt;/span&gt;붙여&lt;span&gt; &lt;/span&gt;넣을&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&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;/span&gt;길었지만&lt;span&gt;, &lt;/span&gt;지금과&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;키보드조차&lt;span&gt; &lt;/span&gt;낯설어지는&lt;span&gt; &lt;/span&gt;모바일&lt;span&gt; &lt;/span&gt;시대에&lt;span&gt; &lt;/span&gt;손편지는&lt;span&gt; &lt;/span&gt;보내는&lt;span&gt; &lt;/span&gt;사람과&lt;span&gt; &lt;/span&gt;받는&lt;span&gt; &lt;/span&gt;사람&lt;span&gt; &lt;/span&gt;모두에게&lt;span&gt; &lt;/span&gt;강한&lt;span&gt; &lt;/span&gt;의미를&lt;span&gt; &lt;/span&gt;가지리라&lt;span&gt; &lt;/span&gt;생각한다&lt;span&gt;. &lt;/span&gt;직접&lt;span&gt; &lt;/span&gt;전하는&lt;span&gt; &lt;/span&gt;편지는&lt;span&gt; &lt;/span&gt;더&lt;span&gt; &lt;/span&gt;많은&lt;span&gt; &lt;/span&gt;용기가&lt;span&gt; &lt;/span&gt;필요할&lt;span&gt; &lt;/span&gt;것이고&lt;span&gt;, &lt;/span&gt;우편으로&lt;span&gt; &lt;/span&gt;전해지는&lt;span&gt; &lt;/span&gt;편지라면&lt;span&gt; &lt;/span&gt;주고받는&lt;span&gt; &lt;/span&gt;것도&lt;span&gt; &lt;/span&gt;인편을&lt;span&gt; &lt;/span&gt;통하며&lt;span&gt; &lt;/span&gt;제법&lt;span&gt; &lt;/span&gt;긴&lt;span&gt; &lt;/span&gt;시간이&lt;span&gt; &lt;/span&gt;걸린다&lt;span&gt;. &lt;/span&gt;당연히&lt;span&gt;, &lt;/span&gt;읽음&lt;span&gt; &lt;/span&gt;확인도&lt;span&gt; &lt;/span&gt;안&lt;span&gt; &lt;/span&gt;된다&lt;span&gt;. (&lt;/span&gt;손편지를&lt;span&gt; &lt;/span&gt;등기로&lt;span&gt; &lt;/span&gt;보낸다면&lt;span&gt; &lt;/span&gt;또&lt;span&gt; &lt;/span&gt;다르겠지만&lt;span&gt;...) &lt;/span&gt;그럼에도&lt;span&gt; &lt;/span&gt;아직도&lt;span&gt; &lt;/span&gt;손편지를&lt;span&gt; &lt;/span&gt;주고받는&lt;span&gt; &lt;/span&gt;분들이&lt;span&gt; &lt;/span&gt;있을&lt;span&gt; &lt;/span&gt;것이고&lt;span&gt;, &lt;/span&gt;손편지만이&lt;span&gt; &lt;/span&gt;주는&lt;span&gt; &lt;/span&gt;느낌을&lt;span&gt; &lt;/span&gt;이메일이&lt;span&gt; &lt;/span&gt;대체할&lt;span&gt; &lt;/span&gt;수는&lt;span&gt; &lt;/span&gt;없을&lt;span&gt; &lt;/span&gt;것이다&lt;span&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;/span&gt;그&lt;span&gt; &lt;/span&gt;손편지를&lt;span&gt; &lt;/span&gt;당사자가&lt;span&gt; &lt;/span&gt;직접&lt;span&gt; &lt;/span&gt;쓴&lt;span&gt; &lt;/span&gt;것이&lt;span&gt; &lt;/span&gt;아니라&lt;span&gt; &lt;/span&gt;다른&lt;span&gt; &lt;/span&gt;사람이&lt;span&gt; &lt;/span&gt;프로&lt;span&gt; &lt;/span&gt;대필가가&lt;span&gt; &lt;/span&gt;대필한&lt;span&gt; &lt;/span&gt;것이라면&lt;span&gt; &lt;/span&gt;어떨까&lt;span&gt;? &lt;/span&gt;글씨만이&lt;span&gt; &lt;/span&gt;아니라&lt;span&gt; &lt;/span&gt;내용까지&lt;span&gt; &lt;/span&gt;대필가를&lt;span&gt; &lt;/span&gt;통해&lt;span&gt; &lt;/span&gt;다듬어지고&lt;span&gt; &lt;/span&gt;대신&lt;span&gt; &lt;/span&gt;쓰인&lt;span&gt; &lt;/span&gt;것이라면&lt;span&gt; &lt;/span&gt;직접&lt;span&gt; &lt;/span&gt;쓴&lt;span&gt; &lt;/span&gt;손편지가&lt;span&gt; &lt;/span&gt;갖는&lt;span&gt; &lt;/span&gt;그&lt;span&gt; &lt;/span&gt;정성과&lt;span&gt; &lt;/span&gt;마음을&lt;span&gt; &lt;/span&gt;동일하게&lt;span&gt; &lt;/span&gt;가졌다고&lt;span&gt; &lt;/span&gt;할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있을까&lt;span&gt;? &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;책을&lt;span&gt; &lt;/span&gt;읽으면서&lt;span&gt; &lt;/span&gt;드는&lt;span&gt; &lt;/span&gt;생각&lt;span&gt; &lt;/span&gt;중&lt;span&gt; &lt;/span&gt;하나였다&lt;span&gt;. &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;질문에&lt;span&gt; &lt;/span&gt;대한&lt;span&gt; &lt;/span&gt;작가의&lt;span&gt; &lt;/span&gt;생각은&lt;span&gt; &lt;/span&gt;책&lt;span&gt; &lt;/span&gt;속에&lt;span&gt; &lt;/span&gt;등장한다&lt;span&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;/span&gt;지키며&lt;span&gt; &lt;/span&gt;잔잔하고&lt;span&gt; &lt;/span&gt;고즈넉하게&lt;span&gt; &lt;/span&gt;살아가는&lt;span&gt; &lt;/span&gt;주인공이&lt;span&gt; &lt;/span&gt;부러우면서도&lt;span&gt;, &lt;/span&gt;나는&lt;span&gt; &lt;/span&gt;저렇게&lt;span&gt; &lt;/span&gt;못&lt;span&gt; &lt;/span&gt;할&lt;span&gt; &lt;/span&gt;것&lt;span&gt; &lt;/span&gt;같다는&lt;span&gt; &lt;/span&gt;생각을&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;. &lt;/span&gt;어디까지&lt;span&gt; &lt;/span&gt;지키고&lt;span&gt; &lt;/span&gt;유지하는&lt;span&gt; &lt;/span&gt;것이&lt;span&gt; &lt;/span&gt;전통을&lt;span&gt; &lt;/span&gt;지키는&lt;span&gt; &lt;/span&gt;것일까&lt;span&gt;? &lt;/span&gt;무언가를&lt;span&gt; &lt;/span&gt;개선하여&lt;span&gt; &lt;/span&gt;발전시킨다면&lt;span&gt; &lt;/span&gt;전통을&lt;span&gt; &lt;/span&gt;버리는&lt;span&gt; &lt;/span&gt;것일까&lt;span&gt;? &lt;/span&gt;가볍다면&lt;span&gt; &lt;/span&gt;가벼운&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;책이&lt;span&gt; &lt;/span&gt;여러&lt;span&gt; &lt;/span&gt;가지&lt;span&gt; &lt;/span&gt;생각을&lt;span&gt; &lt;/span&gt;하게&lt;span&gt; &lt;/span&gt;해준다&lt;span&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;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724331314807&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;books.book&quot; data-og-title=&quot;츠바키 문구점&quot; data-og-description=&quot;섬세한 시선으로 사람들을 따뜻하게 위로하고 치유하는 힐링 소설을 통해 많은 사랑을 받고 있는 오가와 이토의 장편소설. 문구를 파는 평범한 가게처럼 보이지만, 사실은 대대로 편지를 대필&quot; data-og-host=&quot;www.aladin.co.kr&quot; data-og-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&quot; data-og-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GzHra/hyWSk8jvFm/N3sAnNvm7d2Et509ej5ttk/img.jpg?width=500&amp;amp;height=725&amp;amp;face=0_0_500_725,https://scrap.kakaocdn.net/dn/AnGLS/hyWSlsDyHt/Bb75UZukm2fBgvCtst0Gtk/img.jpg?width=500&amp;amp;height=725&amp;amp;face=0_0_500_725&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=117384579&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GzHra/hyWSk8jvFm/N3sAnNvm7d2Et509ej5ttk/img.jpg?width=500&amp;amp;height=725&amp;amp;face=0_0_500_725,https://scrap.kakaocdn.net/dn/AnGLS/hyWSlsDyHt/Bb75UZukm2fBgvCtst0Gtk/img.jpg?width=500&amp;amp;height=725&amp;amp;face=0_0_500_725');&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;츠바키 문구점&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;섬세한 시선으로 사람들을 따뜻하게 위로하고 치유하는 힐링 소설을 통해 많은 사랑을 받고 있는 오가와 이토의 장편소설. 문구를 파는 평범한 가게처럼 보이지만, 사실은 대대로 편지를 대필&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.aladin.co.kr&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;</description>
      <category>Book Shelf</category>
      <category>독서</category>
      <category>소설</category>
      <category>추천도서</category>
      <category>츠바키 문구점</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/359</guid>
      <comments>https://prostars.net/359#entry359comment</comments>
      <pubDate>Thu, 22 Aug 2024 21:50:38 +0900</pubDate>
    </item>
    <item>
      <title>휴먼 라이브러리 소개</title>
      <link>https://prostars.net/358</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;용인으로&amp;nbsp;이사&amp;nbsp;온&amp;nbsp;후로&amp;nbsp;상현&amp;nbsp;도서관을&amp;nbsp;애용하고&amp;nbsp;있다.&amp;nbsp;도서관&amp;nbsp;게시판에&amp;nbsp;붙어있는&amp;nbsp;안내문을&amp;nbsp;보고&amp;nbsp;휴먼&amp;nbsp;라이브러리라는&amp;nbsp;서비스가&amp;nbsp;있다는&amp;nbsp;것을&amp;nbsp;알고는&amp;nbsp;2022년에&amp;nbsp;휴먼북으로&amp;nbsp;등록했다.&amp;nbsp;휴먼북으로서의&amp;nbsp;첫&amp;nbsp;활동은&amp;nbsp;개인의&amp;nbsp;신청이&amp;nbsp;아닌&amp;nbsp;도서관의&amp;nbsp;요청으로&amp;nbsp;처인구에&amp;nbsp;있는&amp;nbsp;&lt;a href=&quot;https://prostars.net/332&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;포곡&amp;nbsp;고등학교의&amp;nbsp;툴박스(IT)&amp;nbsp;동아리&amp;nbsp;회원을&amp;nbsp;대상으로&amp;nbsp;하는&amp;nbsp;강연&lt;/a&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고는&lt;span&gt; &lt;/span&gt;신청자가&lt;span&gt; &lt;/span&gt;없어서&lt;span&gt; &lt;/span&gt;얼마&lt;span&gt; &lt;/span&gt;전에&lt;span&gt; &lt;/span&gt;처음으로&lt;span&gt; &lt;/span&gt;개인으로부터&lt;span&gt; &lt;/span&gt;열람&lt;span&gt; &lt;/span&gt;신청을&lt;span&gt; &lt;/span&gt;받기&lt;span&gt; &lt;/span&gt;전까지&lt;span&gt; &lt;/span&gt;잊고&lt;span&gt; &lt;/span&gt;있었다&lt;span&gt;. &lt;/span&gt;도서관의&lt;span&gt; &lt;/span&gt;세미나실에서&lt;span&gt; &lt;/span&gt;신청자를&lt;span&gt; &lt;/span&gt;만나&lt;span&gt; &lt;/span&gt;여러&lt;span&gt; &lt;/span&gt;질문에&lt;span&gt; &lt;/span&gt;답하면서&lt;span&gt; &lt;/span&gt;이야기를&lt;span&gt; &lt;/span&gt;나누었고&lt;span&gt;, &lt;/span&gt;다행히&lt;span&gt; &lt;/span&gt;도움이&lt;span&gt; &lt;/span&gt;되었다는&lt;span&gt; &lt;/span&gt;피드백을&lt;span&gt; &lt;/span&gt;받았다&lt;span&gt;. &lt;/span&gt;나의&lt;span&gt; &lt;/span&gt;경험과&lt;span&gt; &lt;/span&gt;생각이&lt;span&gt; &lt;/span&gt;누군가에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;도움이&lt;span&gt; &lt;/span&gt;된다는&lt;span&gt; &lt;/span&gt;것은&lt;span&gt; &lt;/span&gt;나에게도&lt;span&gt; &lt;/span&gt;의미&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;일이고&lt;span&gt;, &lt;/span&gt;대화하면서&lt;span&gt; &lt;/span&gt;나&lt;span&gt; &lt;/span&gt;역시&lt;span&gt; &lt;/span&gt;배우는&lt;span&gt; &lt;/span&gt;것이&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;조금은&amp;nbsp;더&amp;nbsp;많은&amp;nbsp;사람들이&amp;nbsp;이&amp;nbsp;서비스를&amp;nbsp;이용했으면&amp;nbsp;하는&amp;nbsp;마음에&amp;nbsp;휴먼북&amp;nbsp;서비스를&amp;nbsp;소개하려고&amp;nbsp;한다.&amp;nbsp;여기서&amp;nbsp;소개하는&amp;nbsp;것은&amp;nbsp;&lt;a href=&quot;https://lib.yongin.go.kr/intro/menu/10081/contents/41684/contents.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;용인시의&amp;nbsp;휴먼&amp;nbsp;라이브러리&amp;nbsp;서비스&lt;/a&gt;다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;service.jpg&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LLwuo/btsH0bZ8vNU/I776hnS4ygUtZBlTKHsQr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LLwuo/btsH0bZ8vNU/I776hnS4ygUtZBlTKHsQr0/img.jpg&quot; data-alt=&quot;휴먼 라이브러리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LLwuo/btsH0bZ8vNU/I776hnS4ygUtZBlTKHsQr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLLwuo%2FbtsH0bZ8vNU%2FI776hnS4ygUtZBlTKHsQr0%2Fimg.jpg&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;601&quot; height=&quot;615&quot; data-filename=&quot;service.jpg&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;휴먼 라이브러리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용인시 도서관 회원이라면, 여기 보이는 것처럼 다양한 분야의 휴먼북을 무료로 열람할 수 있고, 열람할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;시간과&lt;span&gt; &lt;/span&gt;장소는&lt;span&gt; &lt;/span&gt;휴먼북마다&lt;span&gt; &lt;/span&gt;다르다&lt;span&gt;. &lt;/span&gt;경기도민이면 바로 가입할 수 있다. 타지역은 추가 조건이 있다고 하는데, 도서관에 문의하면 안내받을 수 있다. 여기서 나는 &quot;IT/컴퓨터&quot; 분야에 &lt;a href=&quot;https://lib.yongin.go.kr/intro/menu/14107/program/30087/humanBookDetail.do?currentPageNo=1&amp;amp;searchCondition=title&amp;amp;searchReadingField=1200&amp;amp;searchReadingManageCd=ALL&amp;amp;humanBookApplyIdx=262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;개발자로 일한다는 것, 실무에선 무엇을 하나&quot;&lt;/a&gt;라는&lt;span&gt; &lt;/span&gt;제목으로&lt;span&gt; &lt;/span&gt;등록되어&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>I'm prostars</category>
      <category>개발자</category>
      <category>경험공유</category>
      <category>도서관</category>
      <category>사람책</category>
      <category>상현도서관</category>
      <category>자원봉사</category>
      <category>지식공유</category>
      <category>휴먼라이브러리</category>
      <category>휴먼북</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/358</guid>
      <comments>https://prostars.net/358#entry358comment</comments>
      <pubDate>Sun, 16 Jun 2024 19:52:05 +0900</pubDate>
    </item>
    <item>
      <title>Partitioner와 Multi Thread를 활용한 Spring Batch 성능 개선</title>
      <link>https://prostars.net/357</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;8비트&amp;nbsp;MSX로&amp;nbsp;컴퓨터를&amp;nbsp;배웠으나&amp;nbsp;나의&amp;nbsp;첫&amp;nbsp;컴퓨터는&amp;nbsp;IBM&amp;nbsp;XT였고,&amp;nbsp;꽤&amp;nbsp;오랜&amp;nbsp;기간&amp;nbsp;DOS를&amp;nbsp;사용했다.&amp;nbsp;그래서인지&amp;nbsp;아직도&amp;nbsp;배치하면&amp;nbsp;AUTOEXEC.BAT가&amp;nbsp;같이&amp;nbsp;생각난다.&amp;nbsp;이번에&amp;nbsp;정리할&amp;nbsp;내용은&amp;nbsp;많이&amp;nbsp;사용하는&amp;nbsp;스프링&amp;nbsp;배치의&amp;nbsp;성능&amp;nbsp;개선에&amp;nbsp;대한&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서비스를 운영하는 개발팀은 이미 다양한 배치를 운영하고 있을 것이고, 스프링 배치로 구현되었을 확률이 높다. 많은 배치는 서비스 사용량이 적은 새벽에 실행되고, 서비스가 작을 때는 성능에 민감하지 않아도 괜찮다. 하지만, 서비스가 커지고 배치가 처리해야 하는 데이터의 양이 증가하면서 배치의 실행 시간도 같이 증가할 것이다. 예를 들어 매일 새벽 3시에 시작하는 배치의 실행 시간이 점진적으로 증가하여 9시까지 실행되고 있다면 문제가 될 수 있고, 1시간 주기로 실행되는 배치의 실행 시간이 증가하여 1시간을 넘기면 문제가 된다. 즉, 성능을 개선해야 할 필요가 생긴 것이다. 글의&amp;nbsp;말미에서&amp;nbsp;성능&amp;nbsp;차이를&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 스프링 배치 예제가 배치 설명에 집중하기 위해 다루기 간편한 파일을 대상으로 I/O를 구성하고 인메모리 데이터 변환 정도로 예제를 구성한다. 하지만, 실무에서는 여러 데이터베이스와 외부 API를 사용하며 실행되는 배치들도 많다. DB를 대상으로 배치를 구성하면 배치의 성능을 개선했을 때 DB의 성능 그래프가 다르게 그려지는 것을 시각적으로 간단히 확인할 수 있다는 장점도 있다. 아래에서&lt;span&gt; &lt;/span&gt;볼&lt;span&gt; &lt;/span&gt;것이다&lt;span&gt;.&lt;/span&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;프로젝트 구성&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MySQL 8.3 - Docker Compose 3.1&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Source Database - UserNames&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Target Database - Nicknames&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Boot Web 2.7&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Nickname&amp;nbsp;Generator&amp;nbsp;API&amp;nbsp;Server&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Batch 4.3 - Spring Boot 2.7&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;DBInitializerBatch&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MigrationBatch&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드는&amp;nbsp;모두&amp;nbsp;GitHub에&amp;nbsp;올라가&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://github.com/prostars/SpringBatchMultiThreadedPartitions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SpringBatchMultiThreadedPartitions Repository&lt;/a&gt;에는 2개의&amp;nbsp;Batch의&amp;nbsp;코드와&amp;nbsp;Docker&amp;nbsp;Compose&amp;nbsp;구성이&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://github.com/prostars/NicknameGeneratorAPI&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NicknameGeneratorAPI Repository&lt;/a&gt;에는 API 서버의 코드가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이&amp;nbsp;글에서&amp;nbsp;다루는&amp;nbsp;범위&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 예제치고는 프로젝트 구성이 복잡해진 만큼 사용된 기술 스택이 많아졌다. 이 글을 읽는 데 필요한 배경지식으로 Java, Multi Thread, Spring Framework, Spring Batch, JPA, REST API, Docker Compose, Gradle에 대한 기본적인 내용을 이해하고 있으며 Docker Compose는 설치되어 있다고 가정한다. NicknameGeneratorAPI에 대한 내용은 설명하지 않고, JPA 관련 설명도 하지 않는다. SpringBatchMultiThreadedPartitions 예제에서 job 패키지에 대한 부분을 전체가 아닌 코드 조각을 가지고 중요한&amp;nbsp;부분만&amp;nbsp;설명하는&amp;nbsp;정도로&amp;nbsp;정리한다.&amp;nbsp;2개의 예제 프로젝트에는 21개의 테스트가 준비되어 있으니 참고 바란다.&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;환경&amp;nbsp;설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에&amp;nbsp;대한&amp;nbsp;이야기를&amp;nbsp;하기&amp;nbsp;전에&amp;nbsp;배치를&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;환경&amp;nbsp;설정을&amp;nbsp;먼저&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;API&amp;nbsp;Server&amp;nbsp;준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NicknameGeneratorAPI를 체크아웃 받고, 체크아웃 받은 디렉터리로 이동한다.&lt;br /&gt;아래와 같이 API Server를 쉽게 종료할 수 있도록 Gradle을 사용하여 API Server를 Foreground로 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1716218765423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% ./gradlew bootRun&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NicknameGeneratorAPI.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WVIym/btsHuqqjm8k/4Q0CyS4wdv0cmDmgNUjP20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WVIym/btsHuqqjm8k/4Q0CyS4wdv0cmDmgNUjP20/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WVIym/btsHuqqjm8k/4Q0CyS4wdv0cmDmgNUjP20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWVIym%2FbtsHuqqjm8k%2F4Q0CyS4wdv0cmDmgNUjP20%2Fimg.jpg&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;1311&quot; height=&quot;532&quot; data-filename=&quot;NicknameGeneratorAPI.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;532&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;NicknameGeneratorAPI에&amp;nbsp;대한&amp;nbsp;설명을&amp;nbsp;생략하지만,&amp;nbsp;코드가&amp;nbsp;길지&amp;nbsp;않고&amp;nbsp;단위&amp;nbsp;테스트가&amp;nbsp;준비되어&amp;nbsp;있으니,&amp;nbsp;코드를&amp;nbsp;파악하기는&amp;nbsp;쉬울&amp;nbsp;것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;testGenerateNicknameAppendsRandomCharsAndNumber.jpg&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYi5Fc/btsHuztM3Ki/XOaGCsHFLXrn7Oht6kKK01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYi5Fc/btsHuztM3Ki/XOaGCsHFLXrn7Oht6kKK01/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYi5Fc/btsHuztM3Ki/XOaGCsHFLXrn7Oht6kKK01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYi5Fc%2FbtsHuztM3Ki%2FXOaGCsHFLXrn7Oht6kKK01%2Fimg.jpg&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;458&quot; height=&quot;136&quot; data-filename=&quot;testGenerateNicknameAppendsRandomCharsAndNumber.jpg&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Database&amp;nbsp;준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운&amp;nbsp;터미널을&amp;nbsp;열어서&amp;nbsp;SpringBatchMultiThreadedPartitions&amp;nbsp;Repo를&amp;nbsp;체크아웃&amp;nbsp;받고,&amp;nbsp;체크아웃&amp;nbsp;받은&amp;nbsp;디렉터리로&amp;nbsp;이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와&amp;nbsp;같이&amp;nbsp;Docker&amp;nbsp;Compose와&amp;nbsp;Gradle을&amp;nbsp;사용하여&amp;nbsp;로컬에&amp;nbsp;2개의&amp;nbsp;DB를&amp;nbsp;컨테이너로&amp;nbsp;실행하고,&amp;nbsp;DB를&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;DB&amp;nbsp;Server&amp;nbsp;실행&lt;/p&gt;
&lt;pre id=&quot;code_1716247730597&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% docker-compose -f docker/docker-compose.yml up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DockerCompose.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buD65a/btsHwyNIsfO/4yNgM76KzkdHkoyPgovKP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buD65a/btsHwyNIsfO/4yNgM76KzkdHkoyPgovKP1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buD65a/btsHwyNIsfO/4yNgM76KzkdHkoyPgovKP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuD65a%2FbtsHwyNIsfO%2F4yNgM76KzkdHkoyPgovKP1%2Fimg.jpg&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;1311&quot; height=&quot;147&quot; data-filename=&quot;DockerCompose.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;147&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;DB&amp;nbsp;초기화&lt;/p&gt;
&lt;pre id=&quot;code_1716247790437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% ./gradlew bootRun --args='--spring.batch.job.names=DBInitializerJob'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DBInitializerBatch.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WjvIR/btsHu65NKyX/gYLBJgVSR7SPhI5Wg8RWyk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WjvIR/btsHu65NKyX/gYLBJgVSR7SPhI5Wg8RWyk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WjvIR/btsHu65NKyX/gYLBJgVSR7SPhI5Wg8RWyk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWjvIR%2FbtsHu65NKyX%2FgYLBJgVSR7SPhI5Wg8RWyk%2Fimg.jpg&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;1311&quot; height=&quot;582&quot; data-filename=&quot;DBInitializerBatch.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;582&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;DB&amp;nbsp;마이그레이션&lt;/p&gt;
&lt;pre id=&quot;code_1716247931219&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% ./gradlew bootRun --args='--spring.batch.job.names=MigrationJob'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v57si/btsHuQPFC02/Wz6bdIoGGv78O0jI7Bm21k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v57si/btsHuQPFC02/Wz6bdIoGGv78O0jI7Bm21k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v57si/btsHuQPFC02/Wz6bdIoGGv78O0jI7Bm21k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv57si%2FbtsHuQPFC02%2FWz6bdIoGGv78O0jI7Bm21k%2Fimg.jpg&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;1311&quot; height=&quot;580&quot; data-filename=&quot;MigrationBatch.jpg&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;580&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;MigrationJob 배치까지 정상 실행되었다면 환경 검증은 완료한 것이다. 참고로, NicknameGeneratorAPI는 5ms의 응답 지연을 강제하고 있다. IntelliJ에서&amp;nbsp;예제의&amp;nbsp;실행을&amp;nbsp;간소화하려고&amp;nbsp;잡&amp;nbsp;파라미터&amp;nbsp;입력&amp;nbsp;대신&amp;nbsp;@Value를&amp;nbsp;사용한&amp;nbsp;Property&amp;nbsp;Injection을&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;스프링&amp;nbsp;배치의&amp;nbsp;파티셔닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치에서 제공하는 파티셔너의 개념을 간단히 소개하면, 큰 범위를 커버하는 하나의 스탭을 여러 개의 작은 범위를 커버하는 서브 스탭으로 나누어서 실행하여 성능 향상을 기대한다. 이때 각 서브 스탭은 별도의 스레드에서 실행되며 각 서브 스탭은 완전한 스탭과 동일하게 동작한다. 파티셔닝을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있는지는&amp;nbsp;배치가&amp;nbsp;커버할&amp;nbsp;데이터를&amp;nbsp;나눌&amp;nbsp;수&amp;nbsp;있는가와&amp;nbsp;이렇게&amp;nbsp;나누어진&amp;nbsp;데이터가&amp;nbsp;독립적으로&amp;nbsp;처리될&amp;nbsp;수&amp;nbsp;있는가에&amp;nbsp;따라서&amp;nbsp;갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;조건을&amp;nbsp;만족하는&amp;nbsp;데이터라면&amp;nbsp;단순히&amp;nbsp;같은&amp;nbsp;배치&amp;nbsp;잡을&amp;nbsp;여러 개&amp;nbsp;실행해도&amp;nbsp;되지&amp;nbsp;않을까?&amp;nbsp;물론,&amp;nbsp;매번&amp;nbsp;각&amp;nbsp;배치&amp;nbsp;잡의&amp;nbsp;실행&amp;nbsp;파라미터로&amp;nbsp;직접&amp;nbsp;데이터를&amp;nbsp;나누어서&amp;nbsp;넣고&amp;nbsp;실행한다면&amp;nbsp;파티셔닝과&amp;nbsp;비슷하게&amp;nbsp;여러&amp;nbsp;개의&amp;nbsp;스탭이&amp;nbsp;병렬로&amp;nbsp;실행된다.&amp;nbsp;다만,&amp;nbsp;각&amp;nbsp;스탭만&amp;nbsp;실행되는&amp;nbsp;것이&amp;nbsp;아니라&amp;nbsp;잡&amp;nbsp;자체가&amp;nbsp;별도의&amp;nbsp;JVM&amp;nbsp;프로세스로&amp;nbsp;무겁게&amp;nbsp;실행되고&amp;nbsp;일부&amp;nbsp;잡이&amp;nbsp;실패한다면&amp;nbsp;개별적으로&amp;nbsp;직접&amp;nbsp;재실행해&amp;nbsp;줘야&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝 잡의 경우는 잡을 재실행하면 실패한 서브 스탭만 이어서 실행될 것이다. 물론, 이 동작은 여러 조건과 설정 상태에 따라 다르다. 또한, 데이터를 나누는 작업을 Partitioner 인터페이스를 구현하여 자동화할 수 있기에 매번 직접 데이터를 나누어서 각 잡을 별도로 실행하지 않아도 되고 각 서브 스탭은 프로세스보다 가벼운 스레드로 분리되어 실행된다. 로컬 파니셔닝에 대해서 다루는 만큼 헷갈릴 수 있는 grid 용어 사용을 글과 예제에서 배제했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더&amp;nbsp;자세한&amp;nbsp;설명은&amp;nbsp;&lt;a href=&quot;https://www.baeldung.com/spring-batch-partitioner&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/spring-batch-partitioner&lt;/a&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;멀티&amp;nbsp;스레드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 CompletableFuture를 사용하여 간단히 멀티 스레드를 사용할 수 있다. 이 예제에서는 파티셔너를 사용하여 이미 멀티 스레드 기반의 병렬 작업이 진행되는데 CompletableFuture를 이용한 멀티 스레드를 추가로 사용하는 이유는 ItemProcessor에서 REST API를 호출하고 대기하는 시간을 줄이기 위함이다. REST API를 비동기로 사용할 수 있는 다양한 방법 중에서, RestTemplate과 CompletableFuture 조합을 선택한 이유는 추상화 레벨이 낮아서 코드가 직관적이고 예제로 적당하다고 생각한다. 그리고,&amp;nbsp;파티션의&amp;nbsp;개수를&amp;nbsp;더&amp;nbsp;늘리면&amp;nbsp;되지&amp;nbsp;않을까라고&amp;nbsp;생각할&amp;nbsp;수도&amp;nbsp;있지만&amp;nbsp;단순히&amp;nbsp;파티션을&amp;nbsp;늘리면&amp;nbsp;각&amp;nbsp;서브&amp;nbsp;스탭이&amp;nbsp;가지는&amp;nbsp;ItemReader,&amp;nbsp;ItemProcessor,&amp;nbsp;ItemWriter&amp;nbsp;모두&amp;nbsp;늘어나며&amp;nbsp;각&amp;nbsp;스탭이&amp;nbsp;처리하는&amp;nbsp;구간이&amp;nbsp;너무&amp;nbsp;잘게&amp;nbsp;나누어진다.&amp;nbsp;이&amp;nbsp;예제에서&amp;nbsp;CompletableFuture를&amp;nbsp;사용하면서&amp;nbsp;기대하는&amp;nbsp;것은&amp;nbsp;청크&amp;nbsp;단위에서&amp;nbsp;각&amp;nbsp;아이템&amp;nbsp;처리에&amp;nbsp;병렬성을&amp;nbsp;부여하는&amp;nbsp;것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티셔너&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서&amp;nbsp;언급했듯이&amp;nbsp;파티셔닝을&amp;nbsp;하려면&amp;nbsp;배치가&amp;nbsp;커버할&amp;nbsp;데이터를&amp;nbsp;나눌&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;하고,&amp;nbsp;그&amp;nbsp;기준을&amp;nbsp;구현하여&amp;nbsp;각&amp;nbsp;서브&amp;nbsp;스탭이&amp;nbsp;작업할&amp;nbsp;구간을&amp;nbsp;설정해야&amp;nbsp;한다.&amp;nbsp;job&amp;nbsp;패키지에&amp;nbsp;있는&amp;nbsp;Partitioner&amp;nbsp;인터페이스를&amp;nbsp;구현한&amp;nbsp;RangePartitioner가&amp;nbsp;이&amp;nbsp;책임을&amp;nbsp;가지고&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;예제의&amp;nbsp;application.properties&amp;nbsp;파일에서&amp;nbsp;PK의&amp;nbsp;범위를&amp;nbsp;batch.range.begin,&amp;nbsp;batch.range.end로&amp;nbsp;설정하고,&amp;nbsp;몇&amp;nbsp;개의&amp;nbsp;파티션을&amp;nbsp;사용할지를&amp;nbsp;batch.partition.size로&amp;nbsp;설정하고&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;설정값을&amp;nbsp;기준으로&amp;nbsp;partition&amp;nbsp;메서드는&amp;nbsp;각&amp;nbsp;서브&amp;nbsp;스탭이&amp;nbsp;담당할&amp;nbsp;구간을&amp;nbsp;나누어서&amp;nbsp;설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RangePartitioner의&amp;nbsp;단위&amp;nbsp;테스트&amp;nbsp;RangePartitionerTest에는&amp;nbsp;3개의&amp;nbsp;테스트&amp;nbsp;시나리오가&amp;nbsp;있고,&amp;nbsp;기본적인&amp;nbsp;동작을&amp;nbsp;단위&amp;nbsp;테스트로&amp;nbsp;확인해&amp;nbsp;볼&amp;nbsp;수&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;RangePartitionerTest
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shouldThrowExceptionWhenPartitionSizeIsZeroOrNegative&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;shouldHaveEqualRangesForAllPartitions&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;shouldHaveOnePartitionWithRangeOneSmaller&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;여기서&amp;nbsp;간단히&amp;nbsp;하나만&amp;nbsp;보자면,&amp;nbsp;아래는&amp;nbsp;shouldHaveEqualRangesForAllPartitions&amp;nbsp;라는&amp;nbsp;단위&amp;nbsp;테스트의&amp;nbsp;코드다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1716286058704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void shouldHaveEqualRangesForAllPartitions() {
  // given 
  final int partitionSize = 3;

  // when
  final Map&amp;lt;String, ExecutionContext&amp;gt; partitions = partitioner.partition(partitionSize);

  // then
  final List&amp;lt;ExecutionContext&amp;gt; executionContexts = new ArrayList&amp;lt;&amp;gt;(partitions.values());
  long sum = IntStream.range(0, partitionSize)
      .mapToObj(executionContexts::get)
      .mapToLong(context -&amp;gt; context.getLong(&quot;subEnd&quot;) - context.getLong(&quot;subBegin&quot;) + 1)
      .sum();
  assertEquals(sum, partitioner.getEnd() - partitioner.getBegin() + 1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위&amp;nbsp;테스트는&amp;nbsp;전체&amp;nbsp;구간을&amp;nbsp;3개의&amp;nbsp;파티션으로&amp;nbsp;나누었을&amp;nbsp;때,&amp;nbsp;나누어진&amp;nbsp;파티션의&amp;nbsp;구간의&amp;nbsp;총합은&amp;nbsp;전체&amp;nbsp;구간과&amp;nbsp;동일해야&amp;nbsp;한다는&amp;nbsp;것을&amp;nbsp;검증한다.&amp;nbsp;테스트&amp;nbsp;코드에서&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있듯이&amp;nbsp;partition() 함수의&amp;nbsp;리턴&amp;nbsp;타입은&amp;nbsp;ExecutionContext을&amp;nbsp;값으로&amp;nbsp;가진&amp;nbsp;맵이고&amp;nbsp;이&amp;nbsp;Context에는&amp;nbsp;각&amp;nbsp;서브&amp;nbsp;스탭이&amp;nbsp;커버할&amp;nbsp;구간&amp;nbsp;정보가&amp;nbsp;담겨있다.&amp;nbsp;이&amp;nbsp;정보는&amp;nbsp;각&amp;nbsp;서브&amp;nbsp;스텝에서&amp;nbsp;직접&amp;nbsp;접근하여&amp;nbsp;읽어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PrepareTasklet의&amp;nbsp;execute()에서는&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;접근한다.&lt;/p&gt;
&lt;pre id=&quot;code_1716286162270&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExecutionContext executionContext =
    chunkContext.getStepContext().getStepExecution().getExecutionContext();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SourceItemReader의&amp;nbsp;beforeStep()에서는&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;접근한다.&lt;/p&gt;
&lt;pre id=&quot;code_1716290910124&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final ExecutionContext executionContext = stepExecution.getExecutionContext();&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;스레드&amp;nbsp;풀&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 2개의 스레드 풀을 사용한다. 하나는 파티셔너가 각 서브 스탭 실행에 사용할 스레드 풀로 PartitionerTaskExecutor에서 파티션 사이즈로 풀의 크기를 설정하고, 스레드 이름을 설정하는 등의 세부 설정을 위해 별도의 빈으로 구성한다. 나머지 하나는 TransformationItemProcessor가 RestAPI 호출을 비동기로 하기 위해 newFixedThreadPool로 청크 사이즈로 풀의 크기를 설정하고 별도의 설정 없이 간단히 생성한다.&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;배치&amp;nbsp;잡&amp;nbsp;구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에는&amp;nbsp;DB&amp;nbsp;초기화와&amp;nbsp;마이그레이션&amp;nbsp;대상&amp;nbsp;데이터를&amp;nbsp;준비하기&amp;nbsp;위한&amp;nbsp;DBInitializerJob과&amp;nbsp;실제&amp;nbsp;마이그레이션을&amp;nbsp;진행할&amp;nbsp;MigrationJob&amp;nbsp;이&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;2개의&amp;nbsp;잡은&amp;nbsp;구성이&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;DBInitializerJob&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CleanTablesStep과&amp;nbsp;PrepareDataMainStep으로&amp;nbsp;구성된&amp;nbsp;Tasklet&amp;nbsp;기반으로&amp;nbsp;청크를&amp;nbsp;사용하지&amp;nbsp;않는다.&amp;nbsp;CleanTablesStep은&amp;nbsp;CleanTablesTasklet을&amp;nbsp;사용하여&amp;nbsp;Source와&amp;nbsp;target&amp;nbsp;DB의&amp;nbsp;데이터를&amp;nbsp;삭제하고,&amp;nbsp;PrepareDataMainStep은&amp;nbsp;PrepareTasklet을&amp;nbsp;사용하여&amp;nbsp;Source&amp;nbsp;DB의&amp;nbsp;마이그레이션에&amp;nbsp;사용할&amp;nbsp;Dummy&amp;nbsp;Data를&amp;nbsp;채운다.&amp;nbsp;이때,&amp;nbsp;RangePartitioner를&amp;nbsp;사용하여&amp;nbsp;girdSize만큼&amp;nbsp;구간을&amp;nbsp;나누고&amp;nbsp;PartitionerTaskExecutor를&amp;nbsp;사용하여&amp;nbsp;각&amp;nbsp;PrepareDataSubStep를&amp;nbsp;멀티&amp;nbsp;스레드로&amp;nbsp;실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는&amp;nbsp;PrepareDataMainStep의&amp;nbsp;코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1716286440826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Step prepareDataMainStep() {
  return stepBuilderFactory.get(&quot;PrepareDataMainStep&quot;)
      .allowStartIfComplete(true) // 반복 실행해볼 수 있도록 추가한 설정이다.
      .partitioner(&quot;PrepareDataSubStep&quot;, partitioner) // 여기서 RangePartitioner를 사용하여 구간을 나눈다.
      .gridSize(partitionSize)
      .taskExecutor(taskExecutor) // 각 스탭이 사용할 스레드 풀을 설정한다. 
      .step(prepareDataSubStep()) // 각 스레드에서 실행할 스탭을 설정한다.
      .build();
}&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;설명한&amp;nbsp;파티셔너와&amp;nbsp;스레드&amp;nbsp;풀을&amp;nbsp;partitioner와&amp;nbsp;&amp;nbsp;taskExecutor로&amp;nbsp;주입받고,&amp;nbsp;PrepareDataMainStep에&amp;nbsp;설정한다.&amp;nbsp;PrepareDataMainStep은&amp;nbsp;이&amp;nbsp;두&amp;nbsp;가지를&amp;nbsp;활용하여&amp;nbsp;prepareDataSubStep을&amp;nbsp;멀티&amp;nbsp;스레드로&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;MigrationJob&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MigrationJob은&amp;nbsp;Tasklet&amp;nbsp;기반&amp;nbsp;잡과&amp;nbsp;달리&amp;nbsp;청크&amp;nbsp;기반으로&amp;nbsp;Reader,&amp;nbsp;Processor,&amp;nbsp;Writer&amp;nbsp;구성을&amp;nbsp;사용한다.&lt;br /&gt;MigrationJob&amp;nbsp;구성은&amp;nbsp;MigrationMainStep&amp;nbsp;하나로&amp;nbsp;되어있지만,&amp;nbsp;MigrationMainStep이&amp;nbsp;파티셔너&amp;nbsp;설정을&amp;nbsp;가지면서&amp;nbsp;MigrationSubStep을&amp;nbsp;위에서&amp;nbsp;설명한&amp;nbsp;것과&amp;nbsp;같은&amp;nbsp;방식의&amp;nbsp;멀티&amp;nbsp;스레드로&amp;nbsp;실행한다.&amp;nbsp;MigrationJob이&amp;nbsp;사용하는&amp;nbsp;Reader,&amp;nbsp;Processor,&amp;nbsp;Writer의&amp;nbsp;실제&amp;nbsp;구성은&amp;nbsp;MigrationSubStep에&amp;nbsp;있고,&amp;nbsp;MigrationMainStep은&amp;nbsp;파티셔너&amp;nbsp;역할을&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;Reader,&amp;nbsp;Processor,&amp;nbsp;Writer의&amp;nbsp;주요&amp;nbsp;동작을&amp;nbsp;하나씩&amp;nbsp;설명하면,&amp;nbsp;아래는&amp;nbsp;SourceItemReader의&amp;nbsp;read를&amp;nbsp;구현한&amp;nbsp;코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1716286585272&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public UserNameEntity read() {
  if (nextIdx &amp;gt;= userNameEntities.size()) {
    nextIdx = 0;
    if (!fetch()) {
      log.info(&quot;Finished&quot;);
      return null;
    }
  }
  
  final UserNameEntity item = userNameEntities.get(nextIdx);
  nextIdx++;
  return item;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 read 메서드는 매번 DB에 접근하지 않고 메모리에 캐싱된 데이터가 있는지 확인한다. 캐싱된 데이터가 없다면, 설정된 fetch-size만큼 DB를 조회하여 메모리에 캐싱한다. 이와 같이 read()가 호출될 때마다 DB를 조회하지 않도록 캐싱 방식을 적용하여 DB 접근을 줄이고, fetch-size를 적절히 설정하면서 DB 부하를 조절할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 return userNameEntities.get(nextIdx++); 과 같이 한 줄로 기술할 수 있는 코드를 나누어 쓴 것은 코드의 간결성보다 가독성이 중요하다고 생각하여서다.&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;TransformationItemProcessor는&amp;nbsp;위의&amp;nbsp;'멀티&amp;nbsp;스레드'&amp;nbsp;항목에서&amp;nbsp;설명한&amp;nbsp;청크&amp;nbsp;단위에서&amp;nbsp;각&amp;nbsp;아이템의&amp;nbsp;Rest&amp;nbsp;API&amp;nbsp;호출에&amp;nbsp;병렬성을&amp;nbsp;부여하기&amp;nbsp;위해서&amp;nbsp;별도의&amp;nbsp;스레드&amp;nbsp;풀을&amp;nbsp;사용한다.&amp;nbsp;예제의&amp;nbsp;청크&amp;nbsp;사이즈는&amp;nbsp;100으로&amp;nbsp;설정되어&amp;nbsp;있고,&amp;nbsp;스레드&amp;nbsp;풀의&amp;nbsp;크기는&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;청크&amp;nbsp;사이즈와&amp;nbsp;동일하게&amp;nbsp;설정하고&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716286686035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public TransformationItemProcessor(NicknameClientService nicknameClientService,
    @Value(&quot;${batch.chunk-size}&quot;) int chunkCount) {
  this.nicknameClientService = nicknameClientService;
  this.executor = Executors.newFixedThreadPool(chunkCount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&amp;nbsp;헷갈릴&amp;nbsp;수&amp;nbsp;있는데,&amp;nbsp;배치에서&amp;nbsp;사용하는&amp;nbsp;전체&amp;nbsp;스레드&amp;nbsp;풀의&amp;nbsp;사이즈가&amp;nbsp;100이 되는&amp;nbsp;것은&amp;nbsp;아니다.&amp;nbsp;각&amp;nbsp;파티션마다&amp;nbsp;MigrationSubStep이&amp;nbsp;실행된다.&amp;nbsp;예제의&amp;nbsp;파티션&amp;nbsp;사이즈는&amp;nbsp;10으로&amp;nbsp;설정되어&amp;nbsp;있으므로,&amp;nbsp;10&amp;nbsp;*&amp;nbsp;100&amp;nbsp;해서&amp;nbsp;전체&amp;nbsp;스레드&amp;nbsp;풀의&amp;nbsp;사이즈는&amp;nbsp;1000이&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;TransformationItemProcessor의&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;중&amp;nbsp;하나다.&lt;/p&gt;
&lt;pre id=&quot;code_1716286772651&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void testProcessWithNickname() {
  // given
  final UserNameEntity userNameEntity = new UserNameEntity(&quot;user123&quot;);
  final NicknameResponse response = new NicknameResponse(&quot;CoolUser123&quot;);
  when(nicknameClientService.generateNickname(any(NicknameRequest.class))).thenReturn(response);

  // when
  final CompletableFuture&amp;lt;UserNameWithNickEntity&amp;gt; result = processor.process(userNameEntity);

  // then
  assertNotNull(result);
  assertEquals(&quot;CoolUser123&quot;, result.join().getNick(), &quot;The nickname should be 'CoolUser123'&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위&amp;nbsp;테스트는&amp;nbsp;processor가&amp;nbsp;생성한&amp;nbsp;Future를&amp;nbsp;받아서&amp;nbsp;join()으로&amp;nbsp;해당&amp;nbsp;스레드&amp;nbsp;완료&amp;nbsp;처리&amp;nbsp;후의&amp;nbsp;결과를&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;TargetItemWriter의&amp;nbsp;write를&amp;nbsp;구현한&amp;nbsp;코드다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716286886955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void write(List&amp;lt;? extends CompletableFuture&amp;lt;UserNameWithNickEntity&amp;gt;&amp;gt; items) {
  CompletableFuture.allOf(items.toArray(new CompletableFuture[0])).join();

  final List&amp;lt;UserNameWithNickEntity&amp;gt; userNameWithNickEntities = items.stream()
      .map(future -&amp;gt; future.getNow(null))
      .collect(Collectors.toList());

  final List&amp;lt;UserNameWithNickEntity&amp;gt; savedItems = targetNickNameRepository.saveAll(userNameWithNickEntities);
  log.info(&quot;Chunk Finished - saved rows: {}&quot;, savedItems.size());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process가&amp;nbsp;청크&amp;nbsp;사이즈만큼&amp;nbsp;생성한&amp;nbsp;Future들의&amp;nbsp;완료&amp;nbsp;처리를&amp;nbsp;여기서&amp;nbsp;대기한다.&amp;nbsp;getNow(null)에서&amp;nbsp;null이&amp;nbsp;거슬릴&amp;nbsp;수도&amp;nbsp;있지만,&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;join&amp;nbsp;호출로&amp;nbsp;모든&amp;nbsp;Future가&amp;nbsp;완료되었으므로&amp;nbsp;저&amp;nbsp;null이&amp;nbsp;리턴될&amp;nbsp;일은&amp;nbsp;없다.&amp;nbsp;단,&amp;nbsp;Future가&amp;nbsp;예외를&amp;nbsp;캡처한&amp;nbsp;상태로&amp;nbsp;완료된&amp;nbsp;경우는&amp;nbsp;캡처된&amp;nbsp;예외가&amp;nbsp;여기서&amp;nbsp;터질&amp;nbsp;수&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;개발하면서 항상 염두에 두는 것이 있는데, 모든 것은 트레이드 오프라는 생각이다. 단순하고 명확한 배치 처리를 하지 않고, 굳이 이렇게 Partitioner와 CompletableFuture를 같이 사용하여 복잡도를 올렸다면 얻는 것이 있어야 한다. 파티션 사이즈와 스레드 풀의 사이즈를 변경하면서 각각의 성능 차이를 확인할 수 있다. 스크린샷은 MySQL Workbench의 Dashboard다. 배치가 DB에 주는 부하 상태를 간단히 확인할 수 있다. 대상 데이터의 크기는 100,000개다.&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;파티셔너만&amp;nbsp;사용하는&amp;nbsp;DBInitializerBatch로&amp;nbsp;파티션&amp;nbsp;사이즈를&amp;nbsp;1,&amp;nbsp;5,&amp;nbsp;10,&amp;nbsp;15로&amp;nbsp;바꾸면서&amp;nbsp;성능을&amp;nbsp;확인하면&amp;nbsp;아래와&amp;nbsp;같다.&amp;nbsp;스크린샷의&amp;nbsp;대상&amp;nbsp;DB는&amp;nbsp;'Source&amp;nbsp;Database&amp;nbsp;-&amp;nbsp;UserNames&amp;rsquo;다.&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;batch.partition.size=1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;12s&amp;nbsp;277ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;InitializerJob_partition_size_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HgxKY/btsHwC4zTcw/7lC206bJgQsVEaAl98fGD1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HgxKY/btsHwC4zTcw/7lC206bJgQsVEaAl98fGD1/img.jpg&quot; data-alt=&quot;batch.partition.size=1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HgxKY/btsHwC4zTcw/7lC206bJgQsVEaAl98fGD1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHgxKY%2FbtsHwC4zTcw%2F7lC206bJgQsVEaAl98fGD1%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;InitializerJob_partition_size_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=1&lt;/figcaption&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;batch.partition.size=5&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;5s&amp;nbsp;504ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;InitializerJob_partition_size_5.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JT8JI/btsHx0wkFDX/9KXdwMTKaQcXU5ZaFNZ6B0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JT8JI/btsHx0wkFDX/9KXdwMTKaQcXU5ZaFNZ6B0/img.jpg&quot; data-alt=&quot;batch.partition.size=5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JT8JI/btsHx0wkFDX/9KXdwMTKaQcXU5ZaFNZ6B0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJT8JI%2FbtsHx0wkFDX%2F9KXdwMTKaQcXU5ZaFNZ6B0%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;InitializerJob_partition_size_5.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=5&lt;/figcaption&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;batch.partition.size=10&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;4s&amp;nbsp;718ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;InitializerJob_partition_size_10.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1K8sL/btsHxFF8Egu/DlVIDVHJKoG7NKkTvMfU3k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1K8sL/btsHxFF8Egu/DlVIDVHJKoG7NKkTvMfU3k/img.jpg&quot; data-alt=&quot;batch.partition.size=10&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1K8sL/btsHxFF8Egu/DlVIDVHJKoG7NKkTvMfU3k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1K8sL%2FbtsHxFF8Egu%2FDlVIDVHJKoG7NKkTvMfU3k%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;InitializerJob_partition_size_10.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=10&lt;/figcaption&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;batch.partition.size=15&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;5s173ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;InitializerJob_partition_size_15.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmakjd/btsHwbTRqwt/9jktbCbO0ljksqNGOXgXJ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmakjd/btsHwbTRqwt/9jktbCbO0ljksqNGOXgXJ0/img.jpg&quot; data-alt=&quot;batch.partition.size=15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmakjd/btsHwbTRqwt/9jktbCbO0ljksqNGOXgXJ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmakjd%2FbtsHwbTRqwt%2F9jktbCbO0ljksqNGOXgXJ0%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;InitializerJob_partition_size_15.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=15&lt;/figcaption&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;각각의 성능 차이를 보면, 파티션을 크게 설정한다고 무조건 성능이 좋아지는 것은 아니라는 것을 알 수 있다. 배치가 실행되는 환경에 맞는 적절한 설정값을 찾아야 한다. 테스트를 단순하게 하려고 fetch-size와 chunk-size는 고정한 상태로 두었다. 여기서는&amp;nbsp;파티션&amp;nbsp;사이즈&amp;nbsp;5~10&amp;nbsp;정도에서&amp;nbsp;좋은&amp;nbsp;성능을&amp;nbsp;보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;멀티&amp;nbsp;스레드로&amp;nbsp;NicknameGeneratorAPI&amp;nbsp;서버에&amp;nbsp;요청을&amp;nbsp;보내고&amp;nbsp;응답을&amp;nbsp;받아서&amp;nbsp;처리하는&amp;nbsp;MigrationBatch로&amp;nbsp;스레드&amp;nbsp;풀&amp;nbsp;사이즈는&amp;nbsp;청크&amp;nbsp;사이즈와&amp;nbsp;같은&amp;nbsp;100&amp;nbsp;고정이고&amp;nbsp;파티션&amp;nbsp;사이즈를&amp;nbsp;1,&amp;nbsp;5,&amp;nbsp;10,&amp;nbsp;15로&amp;nbsp;바꾸면서&amp;nbsp;성능을&amp;nbsp;확인하면&amp;nbsp;아래와&amp;nbsp;같다.&amp;nbsp;이번&amp;nbsp;스크린샷의&amp;nbsp;대상&amp;nbsp;DB는&amp;nbsp;'Target&amp;nbsp;Database&amp;nbsp;-&amp;nbsp;Nicknames&amp;rsquo;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;batch.partition.size=1&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;30s&amp;nbsp;809ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch_partition_size_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfE6MK/btsHwf9JLhY/7tIg8vrBdbknwwVmvGJsH1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfE6MK/btsHwf9JLhY/7tIg8vrBdbknwwVmvGJsH1/img.jpg&quot; data-alt=&quot;batch.partition.size=1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfE6MK/btsHwf9JLhY/7tIg8vrBdbknwwVmvGJsH1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfE6MK%2FbtsHwf9JLhY%2F7tIg8vrBdbknwwVmvGJsH1%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;MigrationBatch_partition_size_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;batch.partition.size=5&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;17s&amp;nbsp;319ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch_partition_size_5.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XF2KR/btsHxJn30QV/tH2cWiVZJgFmH9IqCzxDKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XF2KR/btsHxJn30QV/tH2cWiVZJgFmH9IqCzxDKK/img.jpg&quot; data-alt=&quot;batch.partition.size=5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XF2KR/btsHxJn30QV/tH2cWiVZJgFmH9IqCzxDKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXF2KR%2FbtsHxJn30QV%2FtH2cWiVZJgFmH9IqCzxDKK%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;MigrationBatch_partition_size_5.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=5&lt;/figcaption&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;batch.partition.size=10&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;17s&amp;nbsp;529ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch_partition_size_10.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZkqmS/btsHwMMMwO0/GUabJ2J9T38FoKD1IxNhtK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZkqmS/btsHwMMMwO0/GUabJ2J9T38FoKD1IxNhtK/img.jpg&quot; data-alt=&quot;batch.partition.size=10&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZkqmS/btsHwMMMwO0/GUabJ2J9T38FoKD1IxNhtK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZkqmS%2FbtsHwMMMwO0%2FGUabJ2J9T38FoKD1IxNhtK%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;MigrationBatch_partition_size_10.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=10&lt;/figcaption&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;batch.partition.size=15&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;17s&amp;nbsp;529ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch_partition_size_15.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A2tIx/btsHxZxqrY0/NJXyLyB1RzkNm6xbABNiJK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A2tIx/btsHxZxqrY0/NJXyLyB1RzkNm6xbABNiJK/img.jpg&quot; data-alt=&quot;batch.partition.size=15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A2tIx/btsHxZxqrY0/NJXyLyB1RzkNm6xbABNiJK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA2tIx%2FbtsHxZxqrY0%2FNJXyLyB1RzkNm6xbABNiJK%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;MigrationBatch_partition_size_15.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=15&lt;/figcaption&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;DBInitializerBatch의 성능 결과와 비슷하게 분포되는 것을 알 수 있다. 처리 시간이 전체적으로 증가한 이유는 NicknameGeneratorAPI 서버가 응답 지연 5ms을 강제하고 있어서다. 이제&amp;nbsp;마지막으로&amp;nbsp;스레드&amp;nbsp;풀&amp;nbsp;사이즈를&amp;nbsp;1로&amp;nbsp;낮추어서&amp;nbsp;성능&amp;nbsp;차이를&amp;nbsp;보면&amp;nbsp;아래와&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;batch.partition.size=5 (with&amp;nbsp;thread&amp;nbsp;pool&amp;nbsp;size=1)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;2m&amp;nbsp;15s&amp;nbsp;162ms&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MigrationBatch_partition_size_5_thread_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dq3KMv/btsHwBEyVFO/7X4c13jiO13Y3ka6y97ri0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dq3KMv/btsHwBEyVFO/7X4c13jiO13Y3ka6y97ri0/img.jpg&quot; data-alt=&quot;batch.partition.size=5 (with thread pool size=1)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dq3KMv/btsHwBEyVFO/7X4c13jiO13Y3ka6y97ri0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdq3KMv%2FbtsHwBEyVFO%2F7X4c13jiO13Y3ka6y97ri0%2Fimg.jpg&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;1143&quot; height=&quot;737&quot; data-filename=&quot;MigrationBatch_partition_size_5_thread_1.jpg&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;batch.partition.size=5 (with thread pool size=1)&lt;/figcaption&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;스레드&amp;nbsp;풀&amp;nbsp;사이즈&amp;nbsp;낮추기&amp;nbsp;전의&amp;nbsp;실행&amp;nbsp;결과&amp;nbsp;중에서&amp;nbsp;파티션&amp;nbsp;사이즈&amp;nbsp;5일&amp;nbsp;때의&amp;nbsp;실행&amp;nbsp;시간이&amp;nbsp;17초인데&amp;nbsp;방금&amp;nbsp;확인한&amp;nbsp;2분&amp;nbsp;15초는&amp;nbsp;매우&amp;nbsp;큰&amp;nbsp;성능&amp;nbsp;차이가&amp;nbsp;생긴&amp;nbsp;것을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있다. 이&amp;nbsp;성능&amp;nbsp;차이로&amp;nbsp;파티셔너를&amp;nbsp;사용하면서도&amp;nbsp;멀티&amp;nbsp;스레드로&amp;nbsp;API의&amp;nbsp;응답&amp;nbsp;지연을&amp;nbsp;커버하는&amp;nbsp;것이&amp;nbsp;성능&amp;nbsp;개선에&amp;nbsp;도움이&amp;nbsp;된다는&amp;nbsp;것을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;정도면&amp;nbsp;복잡도&amp;nbsp;증가와&amp;nbsp;성능&amp;nbsp;개선의&amp;nbsp;트레이드오프를 할 만한&amp;nbsp;가치가&amp;nbsp;있다고&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지&amp;nbsp;설명한&amp;nbsp;부분은&amp;nbsp;스프링&amp;nbsp;배치에서&amp;nbsp;Partitioner와&amp;nbsp;CompletableFuture의&amp;nbsp;조합으로&amp;nbsp;하나의&amp;nbsp;JVM으로&amp;nbsp;더&amp;nbsp;효율적인&amp;nbsp;배치를&amp;nbsp;실행하는&amp;nbsp;방법의&amp;nbsp;하나다.&amp;nbsp;예제의&amp;nbsp;코드&amp;nbsp;구성에서&amp;nbsp;설명하지&amp;nbsp;않은&amp;nbsp;부분이&amp;nbsp;많지만,&amp;nbsp;이&amp;nbsp;글의&amp;nbsp;도입부에서&amp;nbsp;언급했듯이&amp;nbsp;모든&amp;nbsp;내용을&amp;nbsp;하나의&amp;nbsp;포스팅으로&amp;nbsp;정리하기에는&amp;nbsp;예제를&amp;nbsp;너무&amp;nbsp;크게&amp;nbsp;만들었다.&amp;nbsp;여기서&amp;nbsp;다루지&amp;nbsp;않은&amp;nbsp;내용은&amp;nbsp;다음&amp;nbsp;기회로&amp;nbsp;남긴다.&amp;nbsp;글을&amp;nbsp;마무리하면서&amp;nbsp;참고했던&amp;nbsp;책을&amp;nbsp;하나&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;nbsp;배치&amp;nbsp;완벽&amp;nbsp;가이드&amp;nbsp;2/e&amp;nbsp;&lt;/b&gt;&lt;br /&gt;스프링&amp;nbsp;배치의&amp;nbsp;새로운&amp;nbsp;버전이&amp;nbsp;이미&amp;nbsp;나온&amp;nbsp;상황이라&amp;nbsp;이&amp;nbsp;책은&amp;nbsp;이제&amp;nbsp;구버전을&amp;nbsp;다루고&amp;nbsp;있지만,&amp;nbsp;스프링&amp;nbsp;배치를&amp;nbsp;다룬&amp;nbsp;책이&amp;nbsp;달리&amp;nbsp;없고&amp;nbsp;책의&amp;nbsp;내용도&amp;nbsp;좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&quot;&gt;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1716287888180&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;books.book&quot; data-og-title=&quot;스프링 배치 완벽 가이드 2/e&quot; data-og-description=&quot;스프링 배치의 Hello, World!부터 최근 플랫폼의 발전에 따른 클라우드 네이티브 기술을 활용한 배치까지 폭넓은 스프링 배치 활용 방법과 이와 관련된 유용한 내용을 다룬다.&quot; data-og-host=&quot;www.aladin.co.kr&quot; data-og-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&quot; data-og-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oYHN5/hyV6asBWmr/Lb9QnHVz7r6tKKsmZB7SE0/img.jpg?width=500&amp;amp;height=625&amp;amp;face=0_0_500_625,https://scrap.kakaocdn.net/dn/U8wO3/hyV9XynhGN/uajaznzak60SPELggGvWok/img.jpg?width=500&amp;amp;height=625&amp;amp;face=0_0_500_625&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=269630446&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oYHN5/hyV6asBWmr/Lb9QnHVz7r6tKKsmZB7SE0/img.jpg?width=500&amp;amp;height=625&amp;amp;face=0_0_500_625,https://scrap.kakaocdn.net/dn/U8wO3/hyV9XynhGN/uajaznzak60SPELggGvWok/img.jpg?width=500&amp;amp;height=625&amp;amp;face=0_0_500_625');&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;스프링 배치 완벽 가이드 2/e&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 배치의 Hello, World!부터 최근 플랫폼의 발전에 따른 클라우드 네이티브 기술을 활용한 배치까지 폭넓은 스프링 배치 활용 방법과 이와 관련된 유용한 내용을 다룬다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.aladin.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Dev</category>
      <category>Multithread</category>
      <category>Partitioner</category>
      <category>SpringBatch</category>
      <category>SpringBoot</category>
      <category>멀티스레드</category>
      <category>성능</category>
      <category>스프링배치</category>
      <category>스프링부트</category>
      <category>최적화</category>
      <category>파티셔너</category>
      <author>prostars</author>
      <guid isPermaLink="true">https://prostars.net/357</guid>
      <comments>https://prostars.net/357#entry357comment</comments>
      <pubDate>Tue, 21 May 2024 20:56:38 +0900</pubDate>
    </item>
  </channel>
</rss>