<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>끝나지 않는 프로그래밍 일기</title>
    <link>https://exynoa.tistory.com/</link>
    <description>아무리 어려운 내용이라도 쉽게 설명할 수 있을 때 비로소 아는 것이라 굳게 믿고 있습니다. (이메일: binarybard@proton.me)</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 03:52:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LAYER6AI</managingEditor>
    <image>
      <title>끝나지 않는 프로그래밍 일기</title>
      <url>https://t1.daumcdn.net/cfile/tistory/2342A13A585E759B1E</url>
      <link>https://exynoa.tistory.com</link>
    </image>
    <item>
      <title>프로그램 제작 의뢰를 받습니다.</title>
      <link>https://exynoa.tistory.com/300</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DoXp5/btsM4ecMQbT/zSIoWGHLQx73QdfSfkcUW0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DoXp5/btsM4ecMQbT/zSIoWGHLQx73QdfSfkcUW0/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DoXp5/btsM4ecMQbT/zSIoWGHLQx73QdfSfkcUW0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDoXp5%2FbtsM4ecMQbT%2FzSIoWGHLQx73QdfSfkcUW0%2Ftfile.dat&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;394&quot; height=&quot;274&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 제작의뢰를 모두 받습니다. 게임 핵이나 매크로를 제외한&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;의뢰에 관심 있으시면 카카오톡 su6net로 대화를 걸어주시거나, poporo@poporo.dev로 관련 메일을 보내주시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 아래의 오픈채팅방으로 들어오셔서 대화를 걸어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://open.kakao.com/o/sQj0ISoh&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://open.kakao.com/o/sQj0ISoh&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하기 전에는 의뢰자와 충분한 대화를 거친 뒤에 진행되며, 설계가 마무리되면 개발에 바로 착수하고 개발을 마친 뒤에 최종 검토를 합니다. 최종 검토를 거치면 프로젝트가 완료되었다고 쪽지, 메일, 문자 등의 연락 수단으로 통보를 해드립니다.&lt;/p&gt;</description>
      <category>잡담</category>
      <category>개발</category>
      <category>웹</category>
      <category>의뢰</category>
      <category>프로그램</category>
      <category>프리랜서</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/300</guid>
      <comments>https://exynoa.tistory.com/300#entry300comment</comments>
      <pubDate>Tue, 1 Apr 2025 20:42:36 +0900</pubDate>
    </item>
    <item>
      <title>어이가 없는 Github의 행보</title>
      <link>https://exynoa.tistory.com/404</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;수단을&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;깃허브를 통해서 다른 여러 서비스에 소셜 로그인을 하고 있으며, 깃허브 코파일럿 같은 유료 서비스를 몇 달째 구독하고 있는 상태기도 하고, 프로젝트나 스터디를 한참 진행 중에 있는 상황이었기 때문에 이러한 상황이 더 크리티컬하게 다가왔다. 깃허브를 포트폴리오의 일부로 제출하기도 하는 입장에서는 더더욱 답이 없는 상황이었다.&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;트럭이었다.&amp;nbsp;심지어&amp;nbsp;2009년에&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;h3 data-ke-size=&quot;size23&quot;&gt;정지 사례 살펴보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 계정을 실수로 정지? 대단하다 깃허브! 적어도 자동화된 스팸 감지 시스템이 오작동할 가능성을 대해서 원래 사용자가 빠르게 계정을 복구할 수 있는 최소한의 프로세스라도 있어야 하는 게 아닌가?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled(2).png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfTMuo/btsHL5dCzc2/z6RDKt3BLuCIVq5032qgv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfTMuo/btsHL5dCzc2/z6RDKt3BLuCIVq5032qgv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfTMuo/btsHL5dCzc2/z6RDKt3BLuCIVq5032qgv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfTMuo%2FbtsHL5dCzc2%2Fz6RDKt3BLuCIVq5032qgv0%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;794&quot; height=&quot;339&quot; data-filename=&quot;Untitled(2).png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled(1).png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UX3H7/btsHLvRoE3N/gUVRsEv5Y96XagRVHYqOPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UX3H7/btsHLvRoE3N/gUVRsEv5Y96XagRVHYqOPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UX3H7/btsHLvRoE3N/gUVRsEv5Y96XagRVHYqOPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUX3H7%2FbtsHLvRoE3N%2FgUVRsEv5Y96XagRVHYqOPk%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;809&quot; height=&quot;170&quot; data-filename=&quot;Untitled(1).png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eav3DI/btsHLOXuP4G/eMWDmWfhAGstRBotrMlBCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eav3DI/btsHLOXuP4G/eMWDmWfhAGstRBotrMlBCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eav3DI/btsHLOXuP4G/eMWDmWfhAGstRBotrMlBCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feav3DI%2FbtsHLOXuP4G%2FeMWDmWfhAGstRBotrMlBCK%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;808&quot; height=&quot;130&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃허브의 커뮤니티 허브나 서브레딧을 가면 더 암담한 이야기들을 들을 수 있었으며, 계정이 갑작스레 정지되었다는 이야기는 심심치 않게 찾아볼 수 있었다. 마이크로소프트가 깃허브를 인수한 후에 변한 줄 알았으나, 지원팀이 타 기업보다 매우 심각하게 아쉬웠던 부분은 깃허브의 본성이었던 모양이다. 지금의 깃허브의 위상이 수많은 무료 사용자들의 헌신과 기여로 쌓아올려졌다는 점을 생각하면 참으로 애석한 일이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;안녕하세요. 저는 이용 약관 위반으로 계정 정지 통보를 받았지만, 왜 그런지 이해할 수 없습니다. 4월 2일에 지원 요청을 제출했습니다. 그럼에도 불구하고 여전히 Copilot 이용료가 청구되고 있습니다. 지원 요청이 많아 신속하게 응답하기 어려운 점은 이해하지만, 9일이 지나도록 응답이 없고 계속해서 요금을 청구하는 것은 공정하지 않다고 생각합니다. 저는 매일 제 계정에 의존하고 있습니다. 지원팀에서 제 계정의 평판을 신속하게 복구할 수 있는 방법을 안내해 주시면 감사하겠습니다. 계정 복구가 불가능하다면 청구를 중지해 주시기 바랍니다. 저는 규정을 준수하는 사용자로서 이러한 처사가 모욕적이라고 느낍니다. 정말 실망스럽습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;안녕하세요, 여러분.&lt;br /&gt;&lt;br /&gt;3월 25일에 GitHub가 아무 이유 없이, 이메일 통보도 없이 제 계정을 정지시켰습니다. 그들에게 연락했지만, 오늘(5월 28일)까지도 답변을 받지 못했습니다. 한 달 전에 제 친구가 이 문제를 언급하는 글을 이 커뮤니티에 올렸는데, 관리자가 &quot;여기는 공개 포럼이며, 커뮤니티가 티켓을 지원하거나 신속히 처리하는 데 도움을 줄 수는 없지만, &lt;br /&gt;티켓이 적절한 부서에 전달된 것은 확실합니다. 우리의 팀 리소스와 앞선 티켓의 수에 따라 접수된 순서대로 답변이 제공될 것입니다.&quot;라는 답변과 함께 스레드를 닫았습니다.&lt;br /&gt;&lt;br /&gt;현재 2개월이 지난 시점에서 GitHub가 답변하거나 조사할 것이라고 믿기 어렵습니다. 기다리는 것 외에 제가 할 수 있는 일이 무엇인지 알려주세요. 기다리는 것에 정말 지쳤습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;안녕하세요,&lt;br /&gt;&lt;br /&gt;저는 지난 한 달 동안 제 다른 계정이 왜 정지되었는지에 대한 GitHub의 답변을 기다리며 인내심을 가지고 기다렸습니다. 도움말 문서의 권장대로 티켓을 열었고, 그 계정에 무슨 일이 일어났는지에 대한 최소한의 정보라도 얻기 위해 기다리고 있었습니다. 정말 안타까운 상황이며, 이 일을 겪으면서 너무 좌절하지 않으려 노력해왔지만, 그동안 이와 같은 상황을 겪은 많은 GitHub 사용자들이 있다는 것을 알게 되었습니다. 그래서 저는 더 이상 GitHub를 사용하지 않기로 했습니다. 사전 경고 없이 계정을 차단할 수 있는 서비스를 기반으로 내 저장소를 관리할 수는 없습니다.&lt;br /&gt;&lt;br /&gt;이는 오픈 소스 커뮤니티의 주요 후원자라고 주장하는 서비스가 사람들의 저장소에 대해 지나치게 통제하려는 행위입니다. 제 저장소의 로컬 복사본을 가지고 있지 않았다면, 수개월의 작업이 담긴 코드를 효과적으로 도둑맞은 셈입니다. 단지 이유와 시스템이 제가 위반했다고 생각하는 특정 조건이나 조항에 대한 상세한 내용을 이메일로 보내주셨더라면 이렇게까지 좌절하지 않았을 것입니다. 그리고 그 소위 위반 사항에 대한 해결책을 찾으려고 했다면, 이는 존중할 만한 절차입니다.&lt;br /&gt;&lt;br /&gt;저는 정말로 이 문제가 자동 플래그 지정 및 경고나 정보 없이 계정을 정지하는 방법론이 일반적인 것이 아니길 바랍니다. 아마도 제가 이 불행한 경험을 겪은 소수의 사람들 중 하나이기를 바랍니다. 그래도 저는 GitHub Copilot의 열렬한 팬이었고, 그 서비스를 칭찬하며 매달 구독료를 지불해왔지만, 이번 사건 이후로 그 서비스를 취소하고 대안을 찾았으며, 다른 사람들에게도 GitHub를 조심하라고 말하고 있습니다. 정말 안타깝게도, GitHub가 예전에는 개발자들 사이에서 매우 인기 있는 서비스였다는 점에서 이번 일이 더욱 실망스럽습니다.&lt;br /&gt;&lt;br /&gt;진심으로,&lt;br /&gt;실망한 사용자.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이건 정말 황당하네요.&lt;br /&gt;&lt;br /&gt;제 계정이 정지되었습니다. 솔직히 제가 무엇을 잘못했는지 모르겠고, 한 달 넘게 지원팀의 응답을 기다리고 있습니다. 답답하긴 하지만, 저는 GitHub/Copilot을 자주 사용하지 않아서 지원팀과 연락이 닿을 때까지 기다리는 것이 저에게 큰 문제는 아닙니다. 그냥 시험 삼아 써보려던 거니까요.&lt;br /&gt;&lt;br /&gt;하지만 정말 신경 쓰이는 건, 사용하지도 못하는 계정에 대해 Copilot 월 사용료 $10가 청구되었다는 점입니다. 정지된 계정은 요금이 일시 중지될 거라고 생각했는데요. 괜찮습니다, 로그인해서 구독을 취소하면 되겠죠. 하지만 잠깐, 로그인도 안 되는데 어떻게 취소하죠? 그럼 이제 어떻게 해야 하나요? 카드 결제를 취소해야 하나요, 아니면 지원팀이 제 티켓에 응답할 때까지 매달 $10씩 그냥 빼앗기는 건가요? 이건 그냥 명백한 도둑질 아닌가요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 소개한 사례들은 극히 일부다. 여기서 모든 것을 나열하려고 하면 게시글의 스크롤이 사라질 것 같아서 여기까지만 작성하겠다. 여담이지만 깃허브의 공동 설립자이자 전 CEO였던 사람도 아무런 이유 통보 없이 깃허브에서 정지를 먹었다고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled(3).png&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3NQMX/btsHMA5s8cJ/9wWeYE56m4we1KL9ud6J7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3NQMX/btsHMA5s8cJ/9wWeYE56m4we1KL9ud6J7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3NQMX/btsHMA5s8cJ/9wWeYE56m4we1KL9ud6J7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3NQMX%2FbtsHMA5s8cJ%2F9wWeYE56m4we1KL9ud6J7k%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;482&quot; height=&quot;589&quot; data-filename=&quot;Untitled(3).png&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 최근 깃허브의 지원팀이 보여줬던 것과, 레딧의 지원팀이 보여줬던 것들이 너무 극명하게 차이가 나서, 깃허브에 대해 더더욱 실망감이 들었던 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이번 사건으로 느낀 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 보안에 신경쓰고 있었다고 생각했으나 그게 아니었던 모양이다. 모든 사이트는 2단계 인증으로 관리를 하고, 비밀번호는 되도록 소문자, 대문자, 숫자, 특수문자 조합으로 20자를 사용하고 있었다. 중요한 사이트들은 모두 중복된 비밀번호가 하나도 없었으며, 링크를 들어갈 일이 생기면 충분히 신뢰할 수 있는 도메인인 경우에만 접속을 했었다. 브라우저를 종료할 때는 쿠키나 사이트 데이터를 모조리 삭제하게끔 설정도 했고, 의심가는 프로그램이나 링크는 최대한 샌드박스 환경에서 실행할 정도로 나름 신경을 쓴 만큼 이번 사건은 내게 크게 다가왔다. 하지만 결국 내가 미숙했기에 이런 사건으로 이어지지 않았나 싶다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 서비스에 대해 과도하게 의존함&lt;/li&gt;
&lt;li&gt;중요한 데이터들에 대해 정기적인 백업을 수행하지 않았음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github 하나만 비정상적인 접근 기록이 있었다면 아마도 PAT이 탈취된 것이라 생각했을 것 같지만, 레딧도 비슷한 시기에 비정상적인 접근 기록을 확인해서 RAT이나 세션 탈취를 의심해볼 수 밖에 없을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6L0s5/btsHLyHn70d/0bk6vqJcIldcMMoeWlyaY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6L0s5/btsHLyHn70d/0bk6vqJcIldcMMoeWlyaY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6L0s5/btsHLyHn70d/0bk6vqJcIldcMMoeWlyaY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6L0s5%2FbtsHLyHn70d%2F0bk6vqJcIldcMMoeWlyaY0%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;559&quot; height=&quot;251&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 원인인지 짐작가는 곳은 없었지만, 이번 일을 계기로 고쳐야 할 부분들이 보이기 시작했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 백업 솔루션을 통해 주기적으로 중요한 데이터를 백업해야 함&lt;/li&gt;
&lt;li&gt;서비스가 갑작스레 중단될 수 있다는 부분을 감안해서 한 서비스에만 의존하지 않도록 해야 함&lt;/li&gt;
&lt;li&gt;검증된 확장 기능만을 사용하고, 신뢰할 수 없는 링크나 프로그램에 대해서 더 각별하게 주의를 해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담이지만, 문서 관리에는 &lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Obsidian&lt;/b&gt;&lt;/a&gt;을 사용하고 있는데 간혹 사람들이 자주 사용하고 있는 플러그인에 악성 코드가 포함되는 경우도 있다고 들었어서 확장 기능을 지원하는 소프트웨어는 가급적이면 샌드박스 환경에서 실행해야 겠다고 생각이 들었다. 사람들이 자주 사용하는 프로그램들에 대해서 너무 의심없이 받아들이기도 했던 것 같아서 다시 한 번 조심해야겠다.&lt;/p&gt;</description>
      <category>잡담</category>
      <category>Github</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/404</guid>
      <comments>https://exynoa.tistory.com/404#entry404comment</comments>
      <pubDate>Mon, 3 Jun 2024 04:27:10 +0900</pubDate>
    </item>
    <item>
      <title>번외편. ConcurrentHashMap</title>
      <link>https://exynoa.tistory.com/403</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDk2FF/btsajeuWjy9/yV1iW9RYG3qYAWqXtzakj1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDk2FF/btsajeuWjy9/yV1iW9RYG3qYAWqXtzakj1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDk2FF/btsajeuWjy9/yV1iW9RYG3qYAWqXtzakj1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDk2FF%2FbtsajeuWjy9%2FyV1iW9RYG3qYAWqXtzakj1%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;486&quot; height=&quot;304&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ConcurrentHashMap&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap은 동시성을 지원하는 해시맵 클래스입니다. 기본 HashMap 클래스와 마찬가지로 키-값 쌍을 저장할 수 있으나, ConcurrentHashMap은 멀티스레드 환경에서 동시성을 지원하기 위해 설계되었습니다. 멀티스레드 애플리케이션에서 여러 스레드가 동시에 맵에서 데이터를 읽고 쓰거나, 따로 동기화 블록을 사용하는 대신 ConcurrentHashMap이 제공하는 동시성 제어 메커니즘을 활용하려면 ConcurrentHashMap을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681574858767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ConcurrentHashMap&amp;lt;K,V&amp;gt; extends AbstractMap&amp;lt;K,V&amp;gt;  
implements ConcurrentMap&amp;lt;K,V&amp;gt;, Serializable {
	// ...
	// 주어진 키에 해당하는 값이 없거나 null이면,
	// mappingFunction을 사용해서 새 값을 계산하고 맵에 저장한다.
	// 이미 값이 존재하면 현재 값을 반환한다.
	public V computeIfAbsent(K key, Function&amp;lt;? super K, ? extends V&amp;gt; mappingFunction) { /* ... */ }
	// 주어진 키에 해당하는 값이 존재하면,
	// mappingFunction을 사용해서 새 값을 계산하고 맵에 저장한다.
	// 계산된 값이 null이면 키-값 쌍이 맵에서 제거된다.
	// 주어진 키에 값이 없으면 아무 일도 하지 않는다.
	public V computeIfPresent(K key, BiFunction&amp;lt;? super K, ? super V, ? extends V&amp;gt; remappingFunction) { /* ... */ }
	// 키에 대한 현재 값을 사용하여 remappingFunction을 통해서
	// 새 값을 계산하고 맵에 저장한다.
	// 계산된 값이 null이면 키-값 쌍이 맵에서 제거된다.
	public V compute(K key,  
	BiFunction&amp;lt;? super K, ? super V, ? extends V&amp;gt; remappingFunction) { /* ... */ }
	// 주어진 키에 해당하는 값이 없거나 null이면 주어진 값이 맵에 저장된다.
	// 이미 값이 존재하면 remappingFunction을 통해 주어진 값과 현재 값을 병합하고
	// 병합된 값을 맵에 저장한다.
	// 병합된 값이 null이면 키-값 쌍이 맵에서 제거된다.
	public V merge(K key, V value, BiFunction&amp;lt;? super V, ? super V, ? extends V&amp;gt; remappingFunction) { /* ... */ }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원하는 메서드는 HashMap 등과 같은 Map 인터페이스의 구현체와 거의 동일하기 때문에 새로 추가된 메서드만 살펴보도록 합시다. 기억이 안 난다면 &lt;a href=&quot;https://blog.hexabrain.net/386#map&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Map&lt;/a&gt; 부분을 다시 보고 오셔도 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 코드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;computeIfAbsent()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 computeIfAbsent() 메서드는 주어진 이름이 nameGroups에 없으면 해당 이름을 키로 하고 값으로 새 ArrayList를 초기화합니다. 그 후 해당 키의 값 리스트에 이름을 추가하는 예시다. 이렇게 하면 각 이름을 키로 하여 그룹화를 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681574973254&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class GroupingExample {
    public static void main(String[] args) {
        String[] names = {&quot;앨리스&quot;, &quot;밥&quot;, &quot;찰리&quot;, &quot;앨리스&quot;, &quot;밥&quot;, &quot;찰리&quot;, &quot;앨리스&quot;};

        ConcurrentHashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; nameGroups = new ConcurrentHashMap&amp;lt;&amp;gt;();

        for (String name : names) {
            // computeIfAbsent를 사용하여 nameGroups에 이름이 없으면 새 ArrayList를 생성하고 추가한다.
            nameGroups.computeIfAbsent(name, key -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(name);
        }

        System.out.println(nameGroups);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;computeIfPresent()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;computeIfPresent() 메서드는 키가 이미 존재할 때만 remappingFunction을 적용해서 값을 업데이트하는데 사용됩니다. 아래 예시에서는 각 이름의 출현 횟수를 기록해서 출력하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575022426&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.ConcurrentHashMap;

public class NameCounterExample {
    public static void main(String[] args) {
        String[] names = {&quot;앨리스&quot;, &quot;밥&quot;, &quot;찰리&quot;, &quot;앨리스&quot;, &quot;밥&quot;, &quot;찰리&quot;, &quot;앨리스&quot;};

        ConcurrentHashMap&amp;lt;String, Integer&amp;gt; nameCounts = new ConcurrentHashMap&amp;lt;&amp;gt;();

        // 먼저 초기 값을 설정한다.
        for (String name : names) {
            nameCounts.putIfAbsent(name, 0);
        }

        // computeIfPresent()를 사용하여 각 이름의 출현 횟수를 업데이트한다.
        for (String name : names) {
            nameCounts.computeIfPresent(name, (key, count) -&amp;gt; count + 1);
        }

        System.out.println(nameCounts);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;compute()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compute() 메서드는 키-값 쌍에 대해서 주어진 remappingFunction을 적용해 값을 업데이트합니다. 키가 존재하지 않으면 새로운 키-값 쌍을 추가하게 됩니다. 아래 예시에서는 규칙에 따라서 각 문자를 다른 문자로 치환하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575067147&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.ConcurrentHashMap;

public class StringReplacer {
    public static void main(String[] args) {
        ConcurrentHashMap&amp;lt;Character, Character&amp;gt; replacements = new ConcurrentHashMap&amp;lt;&amp;gt;();
        replacements.put('A', 'T');
        replacements.put('T', 'A');
        replacements.put('C', 'G');
        replacements.put('G', 'C');

        String input = &quot;ATCGTAGCTACGT&quot;;
        System.out.println(&quot;변경 전: &quot; + input);

        StringBuilder output = new StringBuilder();
        for (char c : input.toCharArray()) {
            output.append(replacements.compute(c, (key, value) -&amp;gt; value != null ? value : key));
        }

        System.out.println(&quot;변경 후: &quot; + output.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;merge()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 merge()를 사용해서 각 후보자에 대한 투표 수를 계산하고 있습니다. 다시 한번 살펴보면 merge() 메서드는 주어진 키에 해당하는 값이 없거나 null이면 주어진 값이 맵에 저장되고, 이미 값이 존재하면 remappingFunction을 통해 주어진 값과 현재 값을 병합하고 병합된 값을 맵에 저장하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575100468&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class VoteCounter {
    public static void main(String[] args) {
        ConcurrentHashMap&amp;lt;String, Integer&amp;gt; voteCounts = new ConcurrentHashMap&amp;lt;&amp;gt;();

        List&amp;lt;String&amp;gt; votes = Arrays.asList(&quot;앨리스&quot;, &quot;밥&quot;, &quot;앨리스&quot;, &quot;앨리스&quot;, &quot;찰리&quot;, &quot;밥&quot;, &quot;앨리스&quot;, &quot;밥&quot;);

        for (String vote : votes) {
            voteCounts.merge(vote, 1, Integer::sum);
        }

        System.out.println(&quot;투표 횟수:&quot;);
        for (String candidate : voteCounts.keySet()) {
            System.out.println(candidate + &quot;: &quot; + voteCounts.get(candidate));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 걸음 더 나아가기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap의 주요 설계 목표는 동시 읽기 가능(보통 get() 메서드, 그리고 이터레이터 관련 메서드)을 유지하면서 업데이트 간 충돌을 최소화하는 것입니다. 부차적인 목표는 공간 소비를 HashMap과 비슷하거나 더 낮게 유지하고, 많은 스레드가 빈 테이블에 초기 삽입을 빠른 속도로 수행할 수 있도록 지원하는 것입니다. ConcurrentHashMap의 내부를 살펴보기 전에 알아야 할 개념들을 다시 한번 빠르게 짚고 넘어가도록 해봅시다. 이미 기반 개념을 잘 알고 계신다면 바로 ConcurrentHashMap의 내부 구조로 건너뛰셔도 무방합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시(Hash)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해시 함수(Hash Function)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 함수(짧게 줄여서 해시)는 임의의 길이를 갖는 어떤 데이터를 입력받아 고정된 길이의 데이터로 매핑하는 역할을 하며, 이 해시 함수로 나온 결과 값을 보통 해시 값이라고 부릅니다. 해시 함수를 이용하면 큰 데이터 집합이나 복잡한 데이터 구조를 간단하게 표현하면서, 일정한 규칙에 따라서 인덱싱이나 검색 등의 작업을 쉽게 처리할 수 있습니다. 자바에서는 이런 해시 함수를 사용해서 키를 해시 값으로 변환하여 빠르게 키-값 쌍을 저장하고 검색할 수 있도록 하고 있습니다. 예를 들어서 해시 함수가 간단하게 \(h(x)=x \;mod\; 11\)이라고 한다면 100을 해시에 넣었을 때 1이라는 해시 값을 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuGWdv/btsaEhDBvwp/zPRs7XItF0U6MxMoKV7MA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuGWdv/btsaEhDBvwp/zPRs7XItF0U6MxMoKV7MA1/img.png&quot; data-alt=&quot;출처: https://en.wikipedia.org/wiki/Hash_function&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuGWdv/btsaEhDBvwp/zPRs7XItF0U6MxMoKV7MA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuGWdv%2FbtsaEhDBvwp%2FzPRs7XItF0U6MxMoKV7MA1%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;660&quot; height=&quot;506&quot; data-filename=&quot;hash_table.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;785&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://en.wikipedia.org/wiki/Hash_function&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 자바 String 클래스의 hashCode() 메서드를 가져온 것인데, 반복문을 통해 각 문자를 가져와서 해시 값을 갱신하는 것을 볼 수 있습니다. 직접 자바 코드에서 String 타입의 변수에 hashCode()를 호출하면 실제 해시 값을 얻을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681642395005&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static int hashCode(byte[] value) {
    int h = 0;
    int length = value.length &amp;gt;&amp;gt; 1;
    for (int i = 0; i &amp;lt; length; i++) {
        h = 31 * h + getChar(value, i);
    }
    return h;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해시 테이블(Hash Table)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블은 말 그대로 해시 함수를 사용해서 키를 해시 값으로 매핑하고, 이 해시 값을 인덱스로 삼아서 값을 저장하거나 검색하는데 효율적인 자료구조입니다. 이때 데이터가 저장되는 곳을 슬롯(slot) 혹은 버킷(bucket)이라고 부릅니다. 자바에서는 대표적으로 HashMap과 ConcurrentHashMap이 그렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_p.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KHpRv/btsahzNmYQV/eyq64FrhLPP6SKwATnvQK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KHpRv/btsahzNmYQV/eyq64FrhLPP6SKwATnvQK1/img.png&quot; data-alt=&quot;출처: https://en.wikipedia.org/wiki/Hash_table&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KHpRv/btsahzNmYQV/eyq64FrhLPP6SKwATnvQK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKHpRv%2FbtsahzNmYQV%2Feyq64FrhLPP6SKwATnvQK1%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;680&quot; height=&quot;497&quot; data-filename=&quot;hash_table_p.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://en.wikipedia.org/wiki/Hash_table&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림을 예로 들면 키 'John Smith'라는 문자열이 해시 함수를 거쳐 2라는 해시 값을 얻고 이를 버킷의 인덱스로 사용하게 됩니다. 2번 버킷에 John의 연락처인 '521-1234'가 저장된 것을 보실 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해시 충돌(Hash Collision)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블은 여러모로 유용한 자료구조이며 다양한 프로그래밍 언어에서 널리 사용되고 있지만, 해시 함수가 떠안고 있는 문제가 있습니다. 바로 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하게 되면서, 둘 이상의 키에 동일한 인덱스를 생성하는 '해시 충돌(hash collision)'을 피해 갈 수 없다는 것입니다. 만약 \(n\)개의 비둘기집이 있고, \(n+1\)마리의 비둘기가 있다면 적어도 한 비둘기집에서는 필연적으로 2마리 이상의 비둘기가 있어야 한다는 비둘기집 원리(pigeonhole principle)를 따르게 됩니다. 간단한 해시 테이블 구현 예시를 통해서 이를 확인해 보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681642303359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SimpleHashTable {
    private static final int TABLE_SIZE = 5;
    private final String[] table;

    public SimpleHashTable() {
        table = new String[TABLE_SIZE];
    }

    public void put(String key, String value) {
        int index = hash(key);
        table[index] = value;
    }

    public String get(String key) {
        int index = hash(key);
        return table[index];
    }

    private int hash(String key) {
        return key.hashCode() % TABLE_SIZE;
    }

    public static void main(String[] args) {
        SimpleHashTable hashTable = new SimpleHashTable();
        hashTable.put(&quot;사과&quot;, &quot;빨간색&quot;);
        hashTable.put(&quot;바나나&quot;, &quot;노란색&quot;);
        hashTable.put(&quot;망고&quot;, &quot;노란색&quot;);
        hashTable.put(&quot;포도&quot;, &quot;보라색&quot;);

        System.out.println(&quot;사과: &quot; + hashTable.get(&quot;사과&quot;));
        System.out.println(&quot;바나나: &quot; + hashTable.get(&quot;바나나&quot;));
        System.out.println(&quot;망고: &quot; + hashTable.get(&quot;망고&quot;));
        System.out.println(&quot;포도: &quot; + hashTable.get(&quot;포도&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 크기가 5인 해시 테이블에 4개의 키-값 쌍을 집어넣는 것을 볼 수 있습니다. 하지만 코드를 실행시키면 아래와 같이 우리의 기대와는 다른 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_HashTable_P02.png&quot; data-origin-width=&quot;2701&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DNv8y/btsajdiWXmu/1lK4KybYkd9Ors0Tmn0MOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DNv8y/btsajdiWXmu/1lK4KybYkd9Ors0Tmn0MOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DNv8y/btsajdiWXmu/1lK4KybYkd9Ors0Tmn0MOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDNv8y%2FbtsajdiWXmu%2F1lK4KybYkd9Ors0Tmn0MOK%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;2701&quot; height=&quot;731&quot; data-filename=&quot;Attachments_HashTable_P02.png&quot; data-origin-width=&quot;2701&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 '망고'와 '포도'의 해시 값이 2로 동일하여 해시 충돌이 발생해 2번 버킷에 저장된 '노란색'이란 값이 포도에 의해 '보라색'으로 변경된 것을 볼 수 있습니다. 물론 해시 함수 자체도 간단하고 인위적으로 해시 충돌을 유도하도록 작은 테이블 크기로 나눈 게 문제이지만 전하려던 바는 충분히 전해졌으리라 생각합니다. 이러한 해시 충돌을 피하기 위해서 어떤 방법을 사용할 수 있을까요? 개방 주소법(open addressing), 분리 연결법(separate chaining) 등 다양한 방법이 있지만 여기서는 ConcurrentHashMap 내부에서 사용하는 분리 연결법 위주로 보도록 하겠습니다.&lt;/p&gt;
&lt;div class=&quot;admonition hint&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;좋은 해시 함수는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 해시 함수는 해시 테이블의 인덱스를 고르게 분포시켜야 하며, 동일한 입력값에 대해서 항상 같은 해시 값을 반환해야 합니다. 또한 해시 테이블의 주요 장점 중 하나가 빠른 검색 속도이기 때문에 해시 함수의 계산 속도가 빠를수록 전체적인 성능이 향상됩니다. 그리고 입력값의 작은 변경에도 해시 값이 크게 바뀌어야 한다는 눈사태 효과(avalanche effect)를 확인할 수 있어야 합니다. 이를 통해서 유사한 입력값이 서로 다른 해시 값을 가지도록 만들어서 해시 충돌을 줄일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분리 연결법(Separate Chaining)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분리 연결법은 해시 충돌이 발생했을 때 이를 동일한 버킷에 저장하는데 이를 (일반적으로) 링크드 리스트의 형태로 저장하는 방법을 말합니다. 충돌이 일어날 경우, 아래 그림처럼 해당 인덱스의 버킷에 있는 연결된 자료구조에 새로운 키-값 쌍이 추가되게 됩니다. 이렇게 해서 서로 다른 키-값 쌍이 동일한 인덱스를 공유하더라도 저장 및 검색을 할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;separate chaining.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9xmmu/btsaw7OOrK6/6Ji3XPCKrQhK0hFzl0B60K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9xmmu/btsaw7OOrK6/6Ji3XPCKrQhK0hFzl0B60K/img.png&quot; data-alt=&quot;출처: https://en.wikipedia.org/wiki/Hash_table&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9xmmu/btsaw7OOrK6/6Ji3XPCKrQhK0hFzl0B60K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9xmmu%2Fbtsaw7OOrK6%2F6Ji3XPCKrQhK0hFzl0B60K%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;1920&quot; height=&quot;960&quot; data-filename=&quot;separate chaining.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://en.wikipedia.org/wiki/Hash_table&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서는 'John Smith'와 'Sandra Dee'의 인덱스가 152로 충돌한 것을 확인할 수 있는데, 이때 'Sandra Dee'를 나타내는 새로운 노드를 추가하고 이를 'John Smith' 뒤에 연결함으로써 해시 충돌을 처리하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분리 연결법을 통해서 간단하게 구현된 예시 코드를 살펴보도록 하겠습니다. 아래 코드에서는 해시 충돌을 더 잘 확인할 수 있도록 키의 해시 값을 TABLE_SIZE(여기서는 해시 테이블의 크기가 10)로 나눈 나머지의 값을 버킷의 인덱스로 사용하고 있습니다. 각 노드는 링크드 리스트 구조로 동작하도록 다음 노드에 대한 레퍼런스를 가지고 있음을 보실 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681633496848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomHashTable&amp;lt;K, V&amp;gt; {
    private static final int TABLE_SIZE = 10;
    private final Node&amp;lt;K, V&amp;gt;[] table;

    @SuppressWarnings(&quot;unchecked&quot;)
    public CustomHashTable() {
        table = (Node&amp;lt;K, V&amp;gt;[]) new Node&amp;lt;?, ?&amp;gt;[TABLE_SIZE];
    }

    public void put(K key, V value) {
        int index = hash(key);
        Node&amp;lt;K, V&amp;gt; newNode = new Node&amp;lt;&amp;gt;(key, value, null);

        if (table[index] == null) {
            table[index] = newNode;
        } else {
            Node&amp;lt;K, V&amp;gt; currentNode = table[index];
            while (currentNode.next != null) {
                if (currentNode.key.equals(key)) {
                    currentNode.value = value;
                    return;
                }
                currentNode = currentNode.next;
            }

            if (currentNode.key.equals(key)) {
                currentNode.value = value;
            } else {
                currentNode.next = newNode;
            }
        }
    }

    public V get(K key) {
        int index = hash(key);
        Node&amp;lt;K, V&amp;gt; currentNode = table[index];

        while (currentNode != null) {
            if (currentNode.key.equals(key)) {
                return currentNode.value;
            }
            currentNode = currentNode.next;
        }

        return null;
    }

    private int hash(K key) {
        return key.hashCode() % TABLE_SIZE;
    }

    public void printBuckets() {
        for (int i = 0; i &amp;lt; TABLE_SIZE; i++) {
            System.out.print(&quot;버킷 &quot; + i + &quot;: &quot;);
            Node&amp;lt;K, V&amp;gt; currentNode = table[i];
            while (currentNode != null) {
                System.out.print(&quot;(&quot; + currentNode.key + &quot;, &quot; + currentNode.value + &quot;) &quot;);
                currentNode = currentNode.next;
            }
            System.out.println();
        }
    }

    private static class Node&amp;lt;K, V&amp;gt; {
        K key;
        V value;
        Node&amp;lt;K, V&amp;gt; next;

        Node(K key, V value, Node&amp;lt;K, V&amp;gt; next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    public static void main(String[] args) {
        CustomHashTable&amp;lt;String, String&amp;gt; hashTable = new CustomHashTable&amp;lt;&amp;gt;();
        hashTable.put(&quot;red&quot;, &quot;#FF0000&quot;);
        hashTable.put(&quot;green&quot;, &quot;#00FF00&quot;);
        hashTable.put(&quot;blue&quot;, &quot;#0000FF&quot;);
        hashTable.put(&quot;white&quot;, &quot;#FFFFFF&quot;);

        hashTable.printBuckets();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해서 버킷의 상태를 그림으로 살펴보도록 해봅시다. 키 'red'와 'white'의 해시 값이 동일하여 해시 충돌이 난 것을 볼 수 있으며, 이를 5번 버킷에서 확인할 수 있습니다. 만약에 get() 메서드를 통해 키 white에 매핑된 값을 가져오려면 반복문을 통한 선형 탐색으로 가져오게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_SeparateChaining_P01.png&quot; data-origin-width=&quot;2363&quot; data-origin-height=&quot;1255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nh5Xf/btsayV8FDm9/cI63fKgQulvlwL3wfWpr5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nh5Xf/btsayV8FDm9/cI63fKgQulvlwL3wfWpr5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nh5Xf/btsayV8FDm9/cI63fKgQulvlwL3wfWpr5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnh5Xf%2FbtsayV8FDm9%2FcI63fKgQulvlwL3wfWpr5k%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;2363&quot; height=&quot;1255&quot; data-filename=&quot;Attachments_SeparateChaining_P01.png&quot; data-origin-width=&quot;2363&quot; data-origin-height=&quot;1255&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;여기서 충돌이 점점 더 잦아지면 어떻게 될까요? 한 버킷에 n개의 노드가 몰린 경우 n번의 루프를 돌아야 겨우 값을 가져올 수 있게 됩니다. 따라서 자바 8의 ConcurrentHashMap에서는 버킷의 노드 수에 따라서 링크드 리스트가 아닌 균형 탐색 트리(balanced search tree)로 구조를 변환하기도 합니다. 이처럼 버킷과 연결된 자료구조는 일반적으로 링크드 리스트가 사용되지만, 상황에 따라 가변 배열(dynamic array)이나 균형 탐색 트리 등이 사용될 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가시성과 원자성(Visibility and Atomicity)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레드 하면 빼놓을 수 없는 가시성과 원자성에 대해서 먼저 살펴보고 그 후에 ConcurrentHashMap 내부에서 사용되는 CAS 연산이 무엇인지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가시성(Visibility)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 단어 visibility는 '눈으로 볼 수 있는 정도, 알아볼 수 있는 정도'라는 의미를 가졌는데, 여기서 무엇을 볼 수 있다는 걸까요? 간단히 말하면 가시성은 한 스레드에서 공유 변수의 값을 변경했을 때 다른 그 스레드가 그 변경을 볼 수 있는지, 다시 말해서 그 변경된 값을 올바르게 읽어낼 수 있는지에 대한 여부를 나타냅니다. 한 스레드가 어떤 공유 변수의 값을 변경했다고 하더라도, 다른 스레드가 보는 값은 이전의 값일 수도 있다는 것입니다. 간단하게 아래의 예시를 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1681634178404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class VisibilityExample {
    private static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -&amp;gt; {
            int i = 0;
            while (!stop) {
                i++;
            }
            System.out.println(&quot;최종 i 값: &quot;+ i);
        });

        thread.start();

        Thread.sleep(1000);

        stop = true;
        System.out.println(&quot;메인 스레드가 stop 플래그를 true로 설정함&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제를 실행하면 어떤 결과가 나타날까요? 스레드는 정상적으로 stop 플래그의 변경을 확인하고 즉각적으로 반복문을 벗어날 수 있을까요? 예제를 실행해보면 컴파일러 혹은 사용하고 있는 JVM 구현체, 시스템 환경에 따라 정상적으로 i 값을 출력하고 종료되기도, 아니면 스레드가 계속 반복문을 돌면서 죽지 않아 프로그램이 종료되지 않는 것을 확인할 수 있습니다. 이러한 문제는 왜 발생하는 걸까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;memory_management.png&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;891&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wKUer/btsak3fUiuz/Trv8GcaZzZ2GcNhvwnfFnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wKUer/btsak3fUiuz/Trv8GcaZzZ2GcNhvwnfFnk/img.png&quot; data-alt=&quot;메모리 계층 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wKUer/btsak3fUiuz/Trv8GcaZzZ2GcNhvwnfFnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwKUer%2Fbtsak3fUiuz%2FTrv8GcaZzZ2GcNhvwnfFnk%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;486&quot; height=&quot;321&quot; data-filename=&quot;memory_management.png&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;891&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;그 이유는 바로 컴파일러가 모든 작업이 싱글 스레드 환경에서 실행된다고 가정하기 때문입니다. 컴파일러는 이러한 가정 하에 캐시나 메인 메모리에서 읽거나 쓰는 것보다 빠르기에 CPU 레지스터에 데이터를 로드하거나, 싱글 스레드 환경에서 동일한 결과를 보장한다면 명령어의 순서를 바꾸는 등 다양한 최적화를 시도할 수 있습니다. 이는 MESI 프로토콜을 통해 캐시 일관성을 유지하는 경우에도 동일한 문제가 일어납니다. 따라서 가시성으로 인한 문제를 피하기 위해서는 적절한 동기화나 volatile를 통해 모든 읽기/쓰기 작업이 로컬 레지스터를 건너뛰고 캐시에 바로 접근하도록 하고, 일부 컴파일러 최적화(예: 호이스팅)를 방지하여 가시성 문제를 해결해야만 합니다. 컴파일러는 실제로 루프 안에서 stop의 값을 변경하지 않으므로 !stop과 같은 식(expression)의 평가를 위로 끌어올릴 수 있습니다. 이렇게 하면 루프 내에서 해야 되는 작업이 줄어들어서 루프가 더 빠르게 실행됩니다. 따라서 다음과 같이 코드가 변경됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681679136844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ...
    if (!stop) {
    	while (true) {
        	i++;
    	}
    }
// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 싱글 스레드 환경에서는 이렇게 최적화해도 결과는 같으며, 최적화가 일어나는 부분을 아래의 어셈블리 코드에서 확인하실 수 있습니다. 명령 프롬프트에서 실행할 때 '-Djava.compiler=NONE' 옵션을 줘서 JIT 컴파일러를 사용하지 않도록 지시하면 컴파일러 최적화가 일어나지 않아서 정상적으로 프로그램이 종료되는 것을 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Visibility_P01.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmGEMK/btsakftSpCp/ivUIf6HhowzD1e7Or6aak0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmGEMK/btsakftSpCp/ivUIf6HhowzD1e7Or6aak0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmGEMK/btsakftSpCp/ivUIf6HhowzD1e7Or6aak0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmGEMK%2FbtsakftSpCp%2FivUIf6HhowzD1e7Or6aak0%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;956&quot; height=&quot;211&quot; data-filename=&quot;Attachments_Visibility_P01.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;volatile&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 키워드는 컴파일러의 일부 최적화(예: 재배열, 호이스팅 등)를 방지하고 어떤 이유로든 이 값을 레지스터든 캐시든 캐싱해서는 안 된다고 전할 수 있습니다. 간단하게 말하면 volatile 키워드는 모든 스레드가 해당 변수의 최신 값을 항상 확인할 수 있도록 보장해준다고 할 수 있습니다. 참고로 재배열에 관해서는 이미 스레드 2편에서 살펴봤으니 궁금하신 분들은 &lt;a href=&quot;https://blog.hexabrain.net/375#%ED%95%9C-%EA%B1%B8%EC%9D%8C-%EB%8D%94-%EB%82%98%EC%95%84%EA%B0%80%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이곳&lt;/a&gt;으로 이동해 주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1681634220851&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class VisibilityExample {
    private static volatile boolean stop = false;
	/* ... */
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 수정하면 가시성으로 인한 문제가 해결된 것을 볼 수 있습니다. 하지만 volatile로 선언했다고 하더라도 i++와 같이 여러 개의 연산으로 구성된 복합 연산을 원자적으로 만들지는 않으니 주의하도록 합시다. 즉, 원자성을 보장하지 않으며 가시성만 보장합니다. 여기서 원자성(atomicity)은 스레드 1편에서 살펴봤으니 궁금하신 분들은 &lt;a href=&quot;https://blog.hexabrain.net/126#%EC%9B%90%EC%9E%90%EC%84%B1atomicity&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이곳&lt;/a&gt;으로 이동해 주세요.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;원자성(atomicity)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원자성은 더 이상 쪼개질 수 없는 성질을 말하는데, 어떤 것이 원자성을 가지고 있다면 원자적(atomic)이라고 합니다. 덧붙여서, 원자적 연산(atomic operation)은 말 그대로 쪼갤 수 없는 연산을 말합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인 후 행동(Check-Then-Act) 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 &lt;a href=&quot;https://blog.hexabrain.net/126#check-then-act-%ED%8C%A8%ED%84%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스레드 1편&lt;/a&gt;에서 다뤘던 내용이지만 다시 한 번 짚고 넘어가도록 하겠습니다. 패턴의 이름 그대로 무언가를 검사한 뒤에 행동한다는 것인데, 코딩을 할 때면 아래와 같은 일을 빈번하게 하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681736808759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void increment() {
    if (counter &amp;lt; 10000) { // 확인(check)
        counter++; // 행동(act)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 만약에 만약 실행 중인 두 개의 스레드가 아래와 같은 순서로 접근한다면 'count가 10000보다 작은 경우에만' 이라는 조건을 붙였음에도 불구하고 count의 값이 10001, 10002 혹은 그 이상이 될 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Visi.png&quot; data-origin-width=&quot;3505&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yOZJU/btsaETknju7/QEQ9ZHl3QbaRAK15mgKR81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yOZJU/btsaETknju7/QEQ9ZHl3QbaRAK15mgKR81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yOZJU/btsaETknju7/QEQ9ZHl3QbaRAK15mgKR81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyOZJU%2FbtsaETknju7%2FQEQ9ZHl3QbaRAK15mgKR81%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;3505&quot; height=&quot;743&quot; data-filename=&quot;Visi.png&quot; data-origin-width=&quot;3505&quot; data-origin-height=&quot;743&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 한 번 실행해보도록 하겠습니다. 과연 계속해서 일관된 값을 출력해 낼 수 있을까요?&lt;/p&gt;
&lt;pre id=&quot;code_1681633851153&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CheckThenActExample {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter sharedCounter = new SharedCounter();

        Runnable incrementTask = () -&amp;gt; {
            for (int i = 0; i &amp;lt; 10000; i++) {
                sharedCounter.increment();
            }
        };

        Thread[] threads = new Thread[10];

        for (int i = 0; i &amp;lt; 10; i++) {
            threads[i] = new Thread(incrementTask);
            threads[i].start();
        }

        for (int i = 0; i &amp;lt; 10; i++) {
            threads[i].join();
        }

        System.out.println(&quot;최종 카운터 값: &quot; + sharedCounter.getCounter()); // 10000?
    }

    public static class SharedCounter {
    	// volatile은 가시성이나 순서 규칙을 보장하지만 check-then-act 자체가 원자적이지 않다.
        private volatile int counter = 0;

        public void increment() {
            if (counter &amp;lt; 10000) {
                counter++;
            }
        }

        public int getCounter() {
            return counter;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 컴파일 후 실행하면 항상 결과가 10000이 나오는 것은 아니라는 걸 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v2.png&quot; data-origin-width=&quot;2701&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L73YF/btsaSSxk6FO/QupQNwTZDKNW7HlgAskQkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L73YF/btsaSSxk6FO/QupQNwTZDKNW7HlgAskQkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L73YF/btsaSSxk6FO/QupQNwTZDKNW7HlgAskQkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL73YF%2FbtsaSSxk6FO%2FQupQNwTZDKNW7HlgAskQkK%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;2701&quot; height=&quot;467&quot; data-filename=&quot;v2.png&quot; data-origin-width=&quot;2701&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CAS(Compare and Swap) 연산&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 CAS 연산을 사용해서 방금 확인 후 행동 연산을 원자적 연산으로 만들어보도록 하겠습니다. 물론 아래와 같이 synchronized를 사용해서 해결을 할 수도 있겠지만, 이 경우에는 한 스레드가 해당 동기화 블록을 모두 점유하기 때문에 다른 스레드는 아무 작업을 하지 못하고 기다려야 하는 문제가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681741926007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static class SharedCounter {
    private volatile int counter = 0;

    public synchronized void increment() {
        if (counter &amp;lt; 10000) {
            counter++;
        }
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해서 성능 저하와 자원의 낭비가 일어날 수 있습니다. 이러한 문제를 해결하기 위해서 CAS 연산과 같이 락(lock)을 사용하지 않고 동시성 문제를 처리하는 논블로킹(non-blocking) 혹은 락 프리(lock-free) 방법이 등장하게 되었습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;락 프리(lock-free)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 락을 사용하지 않는 알고리즘입니다. 락은 한 번에 하나의 스레드만 특정 코드 블록에 접근할 수 있도록 하는 방법이지만, 이로 인해서 성능 저하나 데드락 등의 문제가 발생할 수 있습니다. 반면, 락 프리 알고리즘은 락을 사용하지 않고 원자적 연산(예: CAS)을 활용해서 여러 스레드 간의 동기화를 수행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAS 연산은 원자적 연산으로 이를 통해 락을 사용하지 않고도 동시성 문제를 해결할 수 있습니다. 아래와 같이 세 가지의 인자를 사용하게 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작업할 메모리 위치 (공유 변수) \(V\)&lt;/li&gt;
&lt;li&gt;예상하고 있는 값 \(A\)&lt;/li&gt;
&lt;li&gt;새로운 값 \(B\)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAS 연산의 동작을 말로 풀어서 설명해보면 &quot;\(V\)에 들어 있는 값이 \(A\)라고 생각하는데, 만약에 실제로 \(V\)의 값이 \(A\)라면 \(B\)라는 값으로 바꿔줘. 만약 \(V\)의 값이 \(A\)가 아니라면 아무 작업도 하지 말고 \(V\)의 값이 뭔지 알려줘.&quot;라는 것입니다. 이를 직접 코드로 구현하면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681823806587&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public synchronized int compareAndSwap(int expectedValue, int newValue) {
	int oldValue = value;
    if (oldValue == expectedValue)
    	value = newValue;
    return oldValue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여러 스레드가 동시에 CAS 연산을 이용하여 특정 변수의 값을 수정하려고 할 때, 오직 한 스레드만이 성공적으로 값을 변경할 수 있습니다. 나머지 스레드들은 변경에 실패하겠지만, 락을 획득하여 대기 상태에 머무르는 대신, 수정에 실패했음을 알리는 통보를 받고 다시 시도할 기회를 얻게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 CPU에서는 원자적인 CAS 연산을 기본적으로 지원하고 있는데, 자바 5 이전부터는 네이티브 코드를 작성하지 않는 한 하드웨어 프로세서의 CAS 연산을 호출할 수 없었지만 자바 5부터 java.util.concurrent.atomic 패키지의 Atomic 클래스를 통해서 하드웨어에서 지원하는 CAS 연산을 사용할 수 있게 되었습니다. CAS 연산을 직접적으로 지원하는 플랫폼의 경우에는 자바 프로그램을 실행할 때 CAS 연산 호출 부분을 직접 해당하는 기계어 코드(lock cmpxchg)로 변환해서 실행하게 됩니다. 만약 하드웨어에서 CAS 연산을 지원하지 않는 최악의 경우에는 JVM가 자체적으로 스핀 락(spin lock)을 사용해서 CAS 연산을 구현하게 됩니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;스핀 락(Spin Lock)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전을 의미하는 스핀(spin)이라는 이름에서도 알 수 있듯이, 스레드는 락을 획득하기 위해서 계속해서 회전하며 대기하는 것처럼 동작하는 것을 '스핀'이라고 부릅니다. 다시 말해서, 락을 이미 다른 스레드가 가져간 경우에 현재 스레드가 락이 풀릴 때까지 기다리면서 블록되지 않고 계속해서 루프를 돌며 락을 획득하려고 시도합니다(busy waiting). 락이 풀리면 스레드는 락을 획득하고 공유 리소스에 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1681634033746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.atomic.AtomicInteger;

public class CheckThenActExample {

    public static void main(String[] args) throws InterruptedException {
        SharedCounter sharedCounter = new SharedCounter();

        Runnable incrementTask = () -&amp;gt; {
            for (int i = 0; i &amp;lt; 10000; i++) {
                sharedCounter.increment();
            }
        };

        Thread[] threads = new Thread[10];

        for (int i = 0; i &amp;lt; 10; i++) {
            threads[i] = new Thread(incrementTask);
            threads[i].start();
        }

        for (int i = 0; i &amp;lt; 10; i++) {
            threads[i].join();
        }

        System.out.println(&quot;최종 카운터 값: &quot; + sharedCounter.getCounter()); // 10000
    }

    public static class SharedCounter {
        private AtomicInteger counter = new AtomicInteger(0);

        public void increment() {
            int current;
            do {
                current = counter.get();
            } while (!counter.compareAndSet(current, current &amp;lt; 10000 ? current + 1 : current));
        }

        public int getCounter() {
            return counter.get();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 실행해보면 항상 일관된 값을 출력하는 것을 확인하실 수 있습니다. JIT 컴파일러가 실제로 컴파일한 코드를 살펴보면 cmpxchg 명령어 앞에 lock prefix가 붙은 것을 볼 수 있는데 CPU 레벨에서 해당 연산이 원자적으로 수행되도록 지원한다는 것을 나타냅니다. 이 lock prefix는 하나의 명령어만을 보호하며, CPU 자체에서 구현되기 때문에 소프트웨어에서 추가적으로 구현할 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled-1.png&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qp6S7/btsaJVI0Jzs/NazYSkiRjOa8Kjfu1b9Rxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qp6S7/btsaJVI0Jzs/NazYSkiRjOa8Kjfu1b9Rxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qp6S7/btsaJVI0Jzs/NazYSkiRjOa8Kjfu1b9Rxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQp6S7%2FbtsaJVI0Jzs%2FNazYSkiRjOa8Kjfu1b9Rxk%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;1183&quot; height=&quot;435&quot; data-filename=&quot;Untitled-1.png&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AtomicInteger의 내부 살펴보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 AtomicInteger의 내부를 살펴보면 다음과 같습니다. value가 volatile로 선언된 것을 확인할 수 있고, compareAndSet() 메서드 내부에서는 Unsafe 클래스를 사용하고 있는 걸 볼 수 있습니다. 이 Unsafe 클래스에는 말 그대로 안전하지 않은 저수준 연산을 수행하는 네이티브 메서드들이 모여있습니다. 그 중에서 저희가 확인해 볼 부분은 compareAndSetInt() 네이티브 메서드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681742383370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AtomicInteger extends Number implements java.io.Serializable {
	private static final long VALUE
        = U.objectFieldOffset(AtomicInteger.class, &quot;value&quot;);
	private volatile int value;
    
	// 만약 현재 값이 예상 값과 같으면, 값이 새로운 값으로 원자적으로 설정되며,
    // 메모리 효과는 VarHandle.compareAndSet에 지정된 대로 적용된다.
    // 성공적인 경우 true를 반환하고, false가 반환되는 경우 실제 값이 예상 값과 같지 않았음을 나타낸다.
	public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }
    // ...
}

public final class Unsafe {
	// 이 메서드는 자바 변수를 원자적으로 업데이트한다.
    // 현재 값이 expected와 같다면, 변수 값을 x로 변경한다.
    // 이 연산은 volatile 읽기와 쓰기와 같은 메모리 의미론을 가지며,
    // 이 메서드는 C11의 atomic_compare_exchange_strong에 해당한다.
    @IntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenJDK의 핫스팟 내부를 살펴보면 아래와 같이 cmpxchg를 사용하고 있는 것을 확인할 수 있습니다. 아래 함수의 역할은 주어진 객체의 특정 필드에 대해서 CAS 연산을 수행하고 연산의 성공 여부를 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681742788839&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// compareAndSetInt() 메서드에 대한 네이티브 구현. (openjdk/jdk/src/hotspot/share/prims/unsafe.cpp#L742)
// cmpxchg(Compare-And-Exchange)는 공유 메모리 위치의 값을 비교한 뒤에,
// 그 값이 예상 값과 동일한 경우 새로운 값으로 교체한다.
// 이 과정은 원자적으로 수행되며, 중간 상태를 다른 스레드가 관찰할 수 없도록 보장한다.
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
  oop p = JNIHandles::resolve(obj);
  volatile jint* addr = (volatile jint*)index_oop_from_field_offset_long(p, offset);
  return Atomic::cmpxchg(addr, e, x) == e;
} UNSAFE_END&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAS 연산에는 ABA 문제가 있지만 이것까지 설명하면 내용이 너무 길어질 것 같아서 생략하도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바 8 이후 ConcurrentHashMap의 내부 구조&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;initialCapacity는 맵의 초기 용량, concurrencyLevel은 맵을 동시에 업데이트하는 스레드 수를 말합니다. 여기서 loadFactor는 해시 테이블 내에 저장된 요소의 수와 테이블 크기 사이의 비율을 말하는데, 이 값이 높을수록 이 비율이 높아져 충돌 가능성이 증가하여 성능 저하가 있을 수 있으나 메모리 사용량은 감소하게 됩니다. 참고로 이 값은 오로지 초기 테이블 용량에만 영향을 줍니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;적재율(load factor)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적재율은 위에서 언급한 대로 해시 테이블 내에 저장된 요소의 수와 테이블 크기 사이의 비율을 말합니다. 즉, 해시 테이블의 크기를 \(N\), 키의 개수를 \(K\)라고 했을 때 적재율은&amp;nbsp; \(\frac{K}{N}\)이 됩니다. 해시 테이블은 키 값을 인덱스로 사용하는 구조이기 때문에 적재율이 1보다 큰 해시 테이블의 경우에는 반드시 충돌이 발생하게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1681575286997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int MAXIMUM_CAPACITY = 1 &amp;lt;&amp;lt; 30;

public ConcurrentHashMap(int initialCapacity,
						 float loadFactor, int concurrencyLevel) {
	if (initialCapacity &amp;lt; concurrencyLevel)
		initialCapacity = concurrencyLevel;
	long size = (long)(1.0 + (long)initialCapacity / loadFactor);
	int cap = (size &amp;gt;= (long)MAXIMUM_CAPACITY) ?
		MAXIMUM_CAPACITY : tableSizeFor((int)size);
	this.sizeCtl = cap;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 tableSizeFor()는 주어진 용량보다 크거나 같은 가장 작은 2의 거듭제곱 값을 반환한게 됩니다. 즉, 내부 해시 테이블의 크기를 2의 거듭제곱으로 유지하는 것입니다. MAXIMUM_CAPACITY은 말 그대로 해시 테이블의 최대 용량으로 \(2^{30}\)까지만 허용합니다. 상위 2비트를 제어 목적으로 사용하고 있기 때문입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;spread()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 값을 더 고르게 분포시키기 위해서 사용되는 보조 메서드입니다. 즉, 해시 테이블에서 해시 충돌을 줄이고 성능을 개선시키는 역할을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575404918&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ConcurrentHashMap 내부에서 사용되는 특별한 해시 값들
static final int MOVED     = -1; // ForwardingNode
static final int TREEBIN   = -2; // TreeBin
static final int RESERVED  = -3; // ReservationNode
    
// 최상위 비트를 제외한 모든 비트가 1인 32비트 정수
static final int HASH_BITS = 0x7fffffff;

// 양수 범위 내에서 해시 값이 분포되도록 HASH_BITS와 AND 연산한다.
// ConcurrentHashMap에서 음수 해시 값은 특별한 의미를 가지기 때문이다.
static final int spread(int h) {
	// 상위 16비트와 하위 16비트를 섞어 해시 값의 분포를 개선한다.
	return (h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)) &amp;amp; HASH_BITS;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;tabAt()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentHashMap의 내부 테이블에서 주어진 인덱스 i에 있는 노드를 안전하게 읽어 오는 역할을 합니다. 이 메서드는 원자적(atomic)으로 값을 읽어 오기 때문에 동시성 문제없이 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575465938&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static final &amp;lt;K,V&amp;gt; Node&amp;lt;K,V&amp;gt; tabAt(Node&amp;lt;K,V&amp;gt;[] tab, int i) {
	// 내부에서 getReferenceVolatile() 메서드를 호출하는데
	// 주어진 객체 o에서 offset 위치에 있는 참조 값을 원자적으로 가져온다.
	// volatile은 가시성(visibility)과 메모리 순서를 보장하는 역할을 한다.
	return (Node&amp;lt;K,V&amp;gt;)U.getReferenceAcquire(tab, ((long)i &amp;lt;&amp;lt; ASHIFT) + ABASE);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;put()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 빈(bin)에는 노드의 리스트가 있으며, 대부분 리스트에는 0개 혹은 1개의 노드만 존재합니다. 여기서 핵심은 CAS(Compare And Swap) 연산을 통해서 구현되어 있으며, 각 빈마다 락 객체를 할당하는 것에 메모리 공간을 낭비하는 걸 피하기 위해 빈 리스트의 첫 번째 노드 자체를 락으로 사용하고 있습니다. 참고로 해시 버킷의 키-값 쌍 노드의 인덱스는 (n - 1) &amp;amp; hash로 계산할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681575526269&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// onlyIfAbsent가 false인 putVal().
// 키가 현재 맵에 없는 경우에도 해당 키로 값을 저장할 수 있다.
public V put(K key, V value) {  
	return putVal(key, value, false);  
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
	// 키와 값이 null일 수는 없다.
	if (key == null || value == null) throw new NullPointerException();
	// 키의 해시 값을 더 고르게 분포시킨다.
	int hash = spread(key.hashCode());
	// 현재 빈(즉, 버킷)의 노드 수를 추적하는 변수다.
	int binCount = 0;
	// 중간에 동적으로 크기가 변하거나(resizing), 해시 충돌 등을 이유로
	// 반복문을 계속 실행하면서 매번 확인하고 찾게 된다.
	for (Node&amp;lt;K,V&amp;gt;[] tab = table;;) {
		Node&amp;lt;K,V&amp;gt; f; int n, i, fh; K fk; V fv;
		// 테이블이 비어있거나 아직 초기화 되지 않은 경우 초기화시킨다.
		// 크기를 별도로 지정하지 않은 경우 DEFAULT_CAPACITY(16)을
		// 초기 테이블 크기로 사용한다.
		if (tab == null || (n = tab.length) == 0)
			tab = initTable();
		// 해당 해시에 대한 버킷이 비어있으면 새 노드를 추가하고 루프를 종료한다.
		else if ((f = tabAt(tab, i = (n - 1) &amp;amp; hash)) == null) {
			// 버킷이 비어있으면 CAS 연산을 통해 노드를 추가한다.
			if (casTabAt(tab, i, null, new Node&amp;lt;K,V&amp;gt;(hash, key, value)))
				break;
		}
		// 현재 테이블이 리사이징 중이면, 기존 테이블을 새로운 테이블로 전송시킨다.
		else if ((fh = f.hash) == MOVED)
			tab = helpTransfer(tab, f);
		// onlyIfAbsent가 참이면 해당 키가 없는 경우에만 값을 저장한다.
		else if (onlyIfAbsent
				 &amp;amp;&amp;amp; fh == hash
				 &amp;amp;&amp;amp; ((fk = f.key) == key || (fk != null &amp;amp;&amp;amp; key.equals(fk)))
				 &amp;amp;&amp;amp; (fv = f.val) != null)
			return fv;
		else {
			V oldVal = null;
			// 동기화를 통해 현재 빈(버킷)의 첫 번째 노드를 잠근다.
			synchronized (f) {
				// 노드가 잠겨 있을 때, 업데이트 전에 해당 노드가 여전히 첫 번째 노드인지 확인한다. 그렇지 않으면 다시 시도한다. (double checking)
				if (tabAt(tab, i) == f) {
					if (fh &amp;gt;= 0) {
						binCount = 1;
						// 현재 처리 중인 빈(버킷)의 링크드 리스트를 순회한다.
						for (Node&amp;lt;K,V&amp;gt; e = f;; ++binCount) {
							K ek;
							// 기존의 값을 새로운 값으로 교체한다.
							if (e.hash == hash &amp;amp;&amp;amp;
								((ek = e.key) == key ||
								 (ek != null &amp;amp;&amp;amp; key.equals(ek)))) {
								oldVal = e.val;
								if (!onlyIfAbsent)
									e.val = value;
								break;
							}
							// 기존의 값이 없는 경우 새 노드를 체인의 끝에 추가한다(separate chaining).
							Node&amp;lt;K,V&amp;gt; pred = e;
							if ((e = e.next) == null) {
								pred.next = new Node&amp;lt;K,V&amp;gt;(hash, key, value);
								break;
							}
						}
					}
					// TreeBin인 경우, 트리에 값을 추가하거나 기존 값을 갱신한다.
					else if (f instanceof TreeBin) {
						Node&amp;lt;K,V&amp;gt; p;
						binCount = 2;
						if ((p = ((TreeBin&amp;lt;K,V&amp;gt;)f).putTreeVal(hash, key,
													   value)) != null) {
							oldVal = p.val;
							if (!onlyIfAbsent)
								p.val = value;
						}
					}
					// ReservationNode는 여러 스레드가 동시에 똑같은 키에 computeIfAbsent와 같은
					// 연산을 수행하려고 할 때 충돌을 방지하기 위해서 '예약된 노드'임을 나타낸다.
					else if (f instanceof ReservationNode)
						throw new IllegalStateException(&quot;Recursive update&quot;);
				}
			}
			// 빈의 노드 수가 임계값(TREEIFY_THRESHOLD, 즉 8)을 넘어서면
			// 해당 빈의 자료구조를 링크드 리스트 대신 트리로 변환한다.
			// (참고로 해시 테이블의 길이가 MIN_TREEIFY_CAPACITY[64]보다 작다면, 테이블의 크기를 두 배로 늘리고 트리 변환 작업을 뒤로 연기함)
			// 이를 통해 성능 향상을 도모하고, 긴 링크드 리스트를 통해서
			// 검색을 해야했던 복잡도가 O(n)에서 O(logN)으로 줄어들게 된다.
			// 참고로 사용되는 트리는 레드-블랙 트리의 특수한 형태다.
			if (binCount != 0) {
				if (binCount &amp;gt;= TREEIFY_THRESHOLD)
					treeifyBin(tab, i);
				if (oldVal != null)
					return oldVal;
				break;
			}
		}
	}
	// ConcurrentHashMap의 크기가 1씩 증가한다.
	// 필요하면 현재 테이블을 확장할 필요가 있는지도 확인한다.
	addCount(1L, binCount);
	return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 빈(버킷)이 레드-블랙 트리(TreeBin)로 관리가 되거나 단순한 링크드 리스트로 관리가 되는 것을 확인할 수 있습니다. 탐색 효율을 높이기 위해 필요에 따라서 버킷의 링크드 리스트 구현을 레드-블랙 트리로 전환하기도 하며, 특수한 노드(ForwardingNode)로 현재 테이블이 확장 중임을 나타내거나, 아직 값은 할당되지 않았지만 계산 중인 노드라 예약된 노드(ReservationNode)도 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_ConcurrentHashMap_P10.png&quot; data-origin-width=&quot;3820&quot; data-origin-height=&quot;1616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o39VX/btsaip4yvQH/XesNKOZIa2CCVeMEsIJPNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o39VX/btsaip4yvQH/XesNKOZIa2CCVeMEsIJPNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o39VX/btsaip4yvQH/XesNKOZIa2CCVeMEsIJPNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo39VX%2Fbtsaip4yvQH%2FXesNKOZIa2CCVeMEsIJPNK%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;3820&quot; height=&quot;1616&quot; data-filename=&quot;Attachments_ConcurrentHashMap_P10.png&quot; data-origin-width=&quot;3820&quot; data-origin-height=&quot;1616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAS 연산(Compare and Swap)을 통해서 원자성(atomicity)을 보장하며, volatile을 통해 가시성(visibility)과 메모리 순서를 보장하게 되어 스레드 세이프하도록 구현이 된 것을 추가적으로 확인할 수 있습니다. 자바 7 이전에는 세그먼트 단위(기본적으로 16개의 세그먼트)로 독립적으로 잠겼지만, 자바 8 이후부터는 락 단위가 더 세분화되어서 해시 테이블 내에 있는 버킷의 첫 번째 노드를 잠그게 됩니다. 따라서 전보다 동시성이 증가했다고 볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java&amp;nbsp;Concurrency&amp;nbsp;in&amp;nbsp;Practice&lt;/a&gt;&amp;nbsp;by&amp;nbsp;Brian&amp;nbsp;Goetz,&amp;nbsp;Tim&amp;nbsp;Peierls,&amp;nbsp;Joshua&amp;nbsp;Bloch,&amp;nbsp;Joseph&amp;nbsp;Bowbeer,&amp;nbsp;David&amp;nbsp;Holmes,&amp;nbsp;and&amp;nbsp;Doug&amp;nbsp;Lea&amp;nbsp;(Addison-Wesley&amp;nbsp;Professional,&amp;nbsp;2006,&amp;nbsp;ISBN:&amp;nbsp;978-0321349606)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>동시성</category>
      <category>자바</category>
      <category>컬렉션</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/403</guid>
      <comments>https://exynoa.tistory.com/403#entry403comment</comments>
      <pubDate>Sun, 16 Apr 2023 01:20:52 +0900</pubDate>
    </item>
    <item>
      <title>31편. 스레드(Thread) (4)</title>
      <link>https://exynoa.tistory.com/401</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdlot/btr9ND2uLnj/CQAyu9ciIIgg5nacQmJ8Q1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdlot/btr9ND2uLnj/CQAyu9ciIIgg5nacQmJ8Q1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdlot/btr9ND2uLnj/CQAyu9ciIIgg5nacQmJ8Q1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsdlot%2Fbtr9ND2uLnj%2FCQAyu9ciIIgg5nacQmJ8Q1%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;486&quot; height=&quot;304&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Callable과 Future&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Callable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억을 되살려서 Runnable이 무엇이었는지 떠올려 봅시다. 전에 사용했던 Runnable은 실행이 끝난 후에 어떤 결과 값을 반환해 줄 수 없었으며, 예외가 발생할 수 있다고 throws 문을 통해서 표현할 수도 없었습니다. 하지만 Callable을 사용하면 결과 값도 돌려줄 수 있으며, 예외도 발생시킬 수 있도록 만들 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681017280045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Callable&amp;lt;V&amp;gt; {
	// 계산한 결과를 반환할 수 있는 메서드다.
	// 만약에 결과를 계산할 수 없으면 예외를 던질 수 있다.
	V call() throws Exception;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Thread는 Runnable 타입을 매개변수로 받고 있어서 Callable을 직접적으로 전달할 수는 없습니다. 그러면 어떻게 해야 할까요? 이런 경우에는 바로 FutureTask를 사용할 수 있습니다. 그 전에 설명의 편의와 이해를 돕기 위해서 블로킹/논블로킹, 동기/비동기가 무엇인지 설명을 하고, Future와 Task가 무엇인지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681017375026&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Thread implements Runnable {
	public Thread(Runnable target) {
		this(null, target, &quot;Thread-&quot; + nextThreadNum(), 0);
	}
	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블로킹과 논블로킹&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행의 흐름을 중단시키는지, 다시 말해서 다른 주체가 작업을 하고 있을 때 제어권이 나 자신에게 있는지에 따라서 블로킹(blocking)과 논블로킹(non-blocking)으로 나눌 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;admonition attention&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;동기와&amp;nbsp;블로킹,&amp;nbsp;그리고&amp;nbsp;비동기와&amp;nbsp;논블로킹&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 이어서 소개할 동기(synchronous)는 블로킹(blocking)과 자주 어울리고, 비동기(asynchrnous)는 논블로킹(non-blocking)과 자주 어울리지만 동기와 블로킹, 비동기와 논블로킹에는 맥락에 따라서 미묘한 차이가 존재합니다. 굳이 말하면 어디를 중요하게 보느냐와 같은 관점의 차이라고 할 수 있겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로킹(blocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실행의 흐름이 중단되고, 다른 사람이 하는 일이 끝날 때까지 기다린 후에 다시 자신의 작업을 이어간다면 블로킹(blocking)이라고 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_01.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QTG8u/btr8Xyt3KV0/fbc7WekKkJWlmjzvPOl3GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QTG8u/btr8Xyt3KV0/fbc7WekKkJWlmjzvPOl3GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QTG8u/btr8Xyt3KV0/fbc7WekKkJWlmjzvPOl3GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQTG8u%2Fbtr8Xyt3KV0%2Ffbc7WekKkJWlmjzvPOl3GK%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;581&quot; data-filename=&quot;Attachments_01.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 프로그래머의 관점으로 끌어들이면, 어떤 메서드를 호출하고 제어권이 자신에게 곧바로 돌아오는지 아닌지에 따라 블로킹이 발생할 수 있습니다. 만약 메서드가 실행 중에 다른 메서드를 호출하고, 호출된 메서드에서 다른 작업을 수행하지 못하고 제어권이 반환될 때까지 기다려야 한다면 블로킹이 발생한다고 할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;제어권(control flow)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램의 실행 흐름을 결정하는 권한을 말합니다. 프로그래밍에서 어떤 코드 블록이나 메서드가 실행되어야 할지를 결정하는 것이 제어권에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제어권을 넘긴다는 것은 코드 실행의 흐름을 다른 코드 블록이나 메서드에게 전달하는 것을 말합니다. 예를 들어서, 어떤 메서드를 호출하면 그 메서드에게 제어권을 넘겨줌으로써 해당 메서드의 코드를 실행할 수 있게 됩니다. 메서드의 실행이 모두 끝나면 제어권은 원래 위치로 돌아오게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1681066783626&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SynchronousBlockingExample {
    public static void main(String[] args) {
        System.out.println(&quot;블로킹 연산을 실행 중 ...&quot;);
        // 제어권이 상대방에게 넘어감
        synchronousBlocking();
        // 제어권이 자신에게 돌아옴
        System.out.println(&quot;블로킹 연산이 끝났습니다.&quot;);
    }

    public static void synchronousBlocking() {
        try {
            // 블로킹 연산을 시뮬레이션 하기 위해서 Thread.sleep() 사용
            Thread.sleep(5000); // 5초동안 블록됨
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;논블로킹(non-blocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실행의 흐름이 중단되지 않고, 다른 사람이 하는 작업과 무관하게 자신의 작업을 계속한다면 이는 논블로킹(non-blocking)이라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_02.png&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baLwKL/btr8UVCTeod/KhEwwKHVv1LyYIuJ0wLZDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baLwKL/btr8UVCTeod/KhEwwKHVv1LyYIuJ0wLZDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baLwKL/btr8UVCTeod/KhEwwKHVv1LyYIuJ0wLZDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaLwKL%2Fbtr8UVCTeod%2FKhEwwKHVv1LyYIuJ0wLZDK%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;473&quot; data-filename=&quot;Attachments_02.png&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 프로그래머의 관점으로 끌어들이면, 어떤 메서드를 호출하고 제어권을 곧바로 돌려받은 뒤 다른 작업을 수행할 수 있는 것을 의미합니다. 좀 더 자세하게 말하면 위 그림에서 A가 B를 호출했을 때 그 시점에서 가지고 올 수 있는 데이터(그게 전체 데이터인지 일부 데이터인지는 상관없이)를 가지고 즉각적으로 제어권을 돌려받게 됩니다. 설명의 편의를 위해서 &lt;a href=&quot;https://blog.hexabrain.net/402&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CompletableFuture&lt;/a&gt;와 &lt;a href=&quot;https://blog.hexabrain.net/382&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;람다식&lt;/a&gt;을 사용했는데 이에 대해서는 내용이 길어질 것 같아 별도의 게시글에서 다루도록 하겠습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;콜백(callback)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백(callback)은 실행되는 것을 목적으로 다른 객체의 메서드에 전달되는 객체를 말합니다. 매개변수로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메서드를 실행시키기 위해 사용됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1681066930327&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.CompletableFuture;

public class AsynchronousNonBlockingExample {
    public static void main(String[] args) {
        System.out.println(&quot;논블로킹 연산을 실행 중...&quot;);

        // 논블로킹 연산을 시뮬레이션 하기 위해서 CompletableFuture를 사용함
        CompletableFuture&amp;lt;Void&amp;gt; future = CompletableFuture.runAsync(() -&amp;gt; {
            try {
                Thread.sleep(5000); // 5초 동안 블로킹
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        future.whenComplete((result, error) -&amp;gt; {
            if (error != null) {
                System.err.println(&quot;논블로킹 연산 도중 에러가 발생함: &quot; + error.getMessage());
            } else {
                System.out.println(&quot;논블로킹 연산이 성공적으로 완료됨&quot;);
            }
        });

        System.out.println(&quot;메인 스레드는 계속해서 실행됨...&quot;);

        // 논블로킹 연산이 끝나기를 기다림
        // 이게 없으면 메인 스레드가 곧바로 종료되어 논블로킹 연산이 완료되는 걸 볼 수 없음
        future.join(); // 이것 자체는 블로킹 연산
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기와 비동기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로킹과 논블로킹이 실행의 흐름이 중단되는지 혹은 제어권이 나에게 있는지 없는지에 중점을 두는 반면에, (적어도 프로그래밍의 맥락에서) 동기와 비동기는 순차적으로 실행되는가와 같은 실행의 순서 혹은 처리해야 할 작업에 관심이 있는지 없는지와 같은 작업의 결과에 중점을 두고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동기(synchronous)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기(synchronous)에서는 순차적으로 작업이 실행됩니다. 즉, 작업의 순서가 보장된다고 할 수 있습니다. 이는 이전 단계가 끝나기 전까지는 다음 단계로는 넘어갈 수 없다는 의미입니다. 바꿔 말하면 처리해야 할 이전 작업이 완료되었는지에 대해 관심을 갖고 있다고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Synchronous_P06.png&quot; data-origin-width=&quot;2005&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDmYL/btr9NBDqd4V/6xOz9Nu7P4HO3mqICkKhNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDmYL/btr9NBDqd4V/6xOz9Nu7P4HO3mqICkKhNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDmYL/btr9NBDqd4V/6xOz9Nu7P4HO3mqICkKhNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDmYL%2Fbtr9NBDqd4V%2F6xOz9Nu7P4HO3mqICkKhNK%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;446&quot; data-filename=&quot;Attachments_Synchronous_P06.png&quot; data-origin-width=&quot;2005&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 아래와 같은 상황을 생각해봅시다. 여기에서는 손님이 자리를 찾고, 메뉴를 고르고, 음식을 주문하고, 계산을 하는 동기적인 상황을 보여주고 있습니다. 손님은 주인이 자리를 찾아주거나 주문을 처리하는 등의 일을 처리할 때까지 기다리며 다른 일을 하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Synchronous_P08.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;2952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dG4Ami/btr9N0XeW06/gUwEKNHm9gIIGvjNO1iI0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dG4Ami/btr9N0XeW06/gUwEKNHm9gIIGvjNO1iI0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dG4Ami/btr9N0XeW06/gUwEKNHm9gIIGvjNO1iI0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdG4Ami%2Fbtr9N0XeW06%2FgUwEKNHm9gIIGvjNO1iI0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;718&quot; data-filename=&quot;Attachments_Synchronous_P08.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;2952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 동기와 논블로킹의 조합을 보여주는 예시입니다. 다른 스레드의 작업을 모두 마칠 때까지 계속 끝났는지 물어보지만, 제어권을 바로 돌려받으면서 여전히 다른 일을 할 수 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681067264345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.*;

public class SynchronousNonBlockingExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable&amp;lt;String&amp;gt; task = () -&amp;gt; {
            System.out.println(&quot;작업이 시작됨...&quot;);
            TimeUnit.SECONDS.sleep(2); // 뭔가 긴 연산
            System.out.println(&quot;작업이 끝남...&quot;);
            return &quot;작업 결과&quot;;
        };

        Future&amp;lt;String&amp;gt; future = executor.submit(task);

        // 폴링(polling) 방식으로 상대방의 작업이 끝났는지 주기적으로 확인함
        while (!future.isDone()) {
            System.out.println(&quot;메인 스레드는 블록되지 않으며 여전히 다른 작업을 할 수 있음...&quot;);
            try {
                TimeUnit.MILLISECONDS.sleep(500); // 뭔가 작업함
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = future.get();
            System.out.println(&quot;작업 결과: &quot; + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;폴링(polling)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 장치(혹은 프로그램)가 충돌 회피 혹은 동기화 처리 등을 목적으로 다른 장치(혹은 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식을 말합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기(asynchronous)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기(asynchronous)에서는 순서가 흐트러질 수 있으며 엄격한 순서를 따를 필요가 없습니다. 다시 말해서 별도의 시작 시간과 종료 시간을 가질 수 있다는 말이며, 여기서는 처리해야 할 작업이 완료되었는지 완료되지 않았는지는 크게 관심이 없습니다. 작업이 완료됐다는 사실은 콜백을 호출하거나 인터럽트를 걸거나 하는 것과 같이 별도의 알림을 보내줌으로써 알려주게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Synchronous_P07.png&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edq3x7/btr9PeASpjJ/uTqQTrInWyoQXKqfm1Dkok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edq3x7/btr9PeASpjJ/uTqQTrInWyoQXKqfm1Dkok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edq3x7/btr9PeASpjJ/uTqQTrInWyoQXKqfm1Dkok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fedq3x7%2Fbtr9PeASpjJ%2FuTqQTrInWyoQXKqfm1Dkok%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;469&quot; data-filename=&quot;Attachments_Synchronous_P07.png&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 상황을 예시로 들어봅시다. 이 상황에서는 손님이 세탁소에 세탁물을 맡기고, 직원이 세탁 작업을 완료할 때까지 기다리지 않고 다른 일을 계속 진행하는 비동기적인 상황을 보여줍니다. 손님은 현재 세탁소 직원이 세탁을 모두 끝냈는지에 대해서는 크게 관심이 없습니다. 손님은 세탁 작업이 완료되는 동안 다른 일을 처리하고, 세탁물이 준비되면 문자 알림을 통해서 알게 됩니다. 손님이 문자를 확인하고 바로 세탁물을 가지러 가거나, 바쁜 상황이라 문자(콜백)를 확인하지 않고 다른 일을 계속 처리할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Synchronous_P09.png&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;1712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBfsT1/btr9PeHEoq1/m52lnlUOsUM1T5mDcgzwi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBfsT1/btr9PeHEoq1/m52lnlUOsUM1T5mDcgzwi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBfsT1/btr9PeHEoq1/m52lnlUOsUM1T5mDcgzwi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBfsT1%2Fbtr9PeHEoq1%2Fm52lnlUOsUM1T5mDcgzwi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;374&quot; data-filename=&quot;Attachments_Synchronous_P09.png&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;1712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;잠시만요,&amp;nbsp;비동기와&amp;nbsp;블로킹&amp;nbsp;조합은&amp;nbsp;어디갔나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기는 대개 블로킹, 비동기는 대개 논블로킹과 어울립니다. 사실상 동기와 논블로킹, 비동기와 블로킹은 흔히 있는 조합은 아닙니다. 특히 비동기와 블로킹 조합은 비효율적이라 더더욱 그렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_05.png&quot; data-origin-width=&quot;2145&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP58jf/btr8LsCgm8v/Eexgm8jVJXKWNMismNh1y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP58jf/btr8LsCgm8v/Eexgm8jVJXKWNMismNh1y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP58jf/btr8LsCgm8v/Eexgm8jVJXKWNMismNh1y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP58jf%2Fbtr8LsCgm8v%2FEexgm8jVJXKWNMismNh1y0%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;417&quot; data-filename=&quot;Attachments_05.png&quot; data-origin-width=&quot;2145&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로킹이라 제어권을 곧바로 돌려받지 못하지만, 비동기라 서로의 작업에 그렇게 크게 관심이 없습니다. 작업이 언제 끝나는지 관심이 없는데도 그 사람의 작업이 끝나야 다른 작업을 이어서 할 수 있습니다. 이는 사실상 동기와 블로킹 조합과 유사하게 진행됩니다. 보통 잘못된 코드 구조나 개발자의 실수 등으로 인해서 일어나는데, 대표적인 예시로 블로킹 방식의 MySQL 드라이버와 싱글 스레드 기반의 비동기 방식인 Node.js의 조합이 있다고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Future&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 돌아와서 Future를 살펴보도록 하겠습니다. Future는 비동기적으로 이루어지는 계산의 처리 결과를 나타냅니다. 다시 말해서 아직 계산되지 않았지만 &quot;미래(future)&quot;의 어느 시점에 사용할 수 있는 값을 나타냅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681288447403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Future&amp;lt;V&amp;gt; {
	// 현재 작업의 실행을 취소하려고 시도한다.
	// mayInterruptIfRunning가 true인 경우:
	// 작업을 중단하려고 시도하며 작업이 중단되면 InterruptedException이 발생할 수 있다.
	// mayInterruptIfRunning가 false인 경우:
	// 작업이 실행 중이지 않고 아직 시작되지 않았으면 작업을 취소한다.
	// 그러나 작업이 이미 실행 중이라면 작업을 중단하지 않고 계속 실행된다.
	boolean cancel(boolean mayInterruptIfRunning);

	// 이 작업이 정상적으로 완료되기 전에 취소된 경우 true를 반환한다.
	boolean isCancelled();

	// 이 작업이 완료되면 true를 반환한다.
	// 여기서 완료는 '정상적인 종료', '예외', '취소'로 인한 것일 수 있으며
	// 이 모든 경우에 이 메서드는 true를 반환한다.
	boolean isDone();

	// 비동기 작업의 결과를 반환한다.
	// 작업이 아직 완료되지 않았다면, 작업이 완료될 때까지 현재 스레드를 블록한다.
	// 작업 중 예외가 발생하면 ExecutionException이 발생한다.
	// 작업이 취소된 경우에는 CancellationException이 발생한다.
	V get() throws InterruptedException, ExecutionException,
					CancellationException; // 블로킹 호출

	// 지정된 시간만큼 작업의 결과를 기다린 후 반환한다.
	// 만약 작업이 완료되기 전에 시간이 초과되면 TimeoutException이 발생한다.
	V get(long timeout, TimeUnit unit) // 블로킹 호출
			throws InterruptedException, ExecutionException,
					CancellationException, TimeoutException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future를 통해서 특정 작업이 정상적으로 완료됐는지, 아니면 취소됐는지 등에 대한 정보를 확인할 수 있습니다. Future가 동작하는 사이클에서 염두에 두어야 할 점은, 한 번 지나간 상태는 되돌릴 수 없다는 점입니다. 일단 완료된 작업은 완료 상태에 영원히 머무르게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 Future를 사용해서 간단한 비동기 작업을 수행하고 있는 걸 볼 수 있습니다. 아래 예제에서는 두 개의 숫자를 더하는 작업을 비동기적으로 수행하고 결과를 가져옵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681289299453&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class FutureExample {
    public static void main(String[] args) {
    	// 스레드 풀에 대해서는 후반에 같이 살펴보겠습니다.
        // 지금은 그냥 스레드라고 생각을 해주세요.
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        // 비동기 작업 정의
        Callable&amp;lt;Integer&amp;gt; additionTask = new Callable&amp;lt;Integer&amp;gt;() {
            @Override
            public Integer call() throws Exception {
                int a = 5;
                int b = 7;
                int result = a + b;
                System.out.println(&quot;덧셈 작업이 완료되었습니다.&quot;);
                return result;
            }
        };

        // 비동기 작업 실행 및 결과를 저장하는 Future 객체 얻기
        Future&amp;lt;Integer&amp;gt; futureResult = executorService.submit(additionTask);

        while (!futureResult.isDone()) {
            System.out.println(&quot;덧셈 작업이 완료되기를 기다리는 중...&quot;);
            try {
                // 일정 시간동안 대기한 후 다시 작업 완료 여부를 확인한다.
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 비동기 작업 결과 가져오기
            Integer result = futureResult.get(1, TimeUnit.SECONDS);
            System.out.println(&quot;덧셈 결과: &quot; + result);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        } finally {
            // ExecutorService 종료
            executorService.shutdown();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FutureTask&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FutureTask는 Future, Runnable을 상속받는 RunnableFuture 인터페이스의 기본 구현체입니다. FutureTask도 Future와 마찬가지로 한 번 종료됨 상태에 이르고 나면 더 이상 상태가 바뀌는 일은 없습니다. FutureTask는 기타 시간이 많이 필요한 모든 작업이 있을 때 실제 결과가 필요한 시점 이전에 미리 작업을 실행시켜두는 용도로 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681292807062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface RunnableFuture&amp;lt;V&amp;gt; extends Runnable, Future&amp;lt;V&amp;gt; {
    void run();
}

public class FutureTask&amp;lt;V&amp;gt; implements RunnableFuture&amp;lt;V&amp;gt; {
	/* ... Future와 동일 ... */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 웹 페이지의 내용을 가져오는 작업을 비동기적으로 수행합니다. Callable 객체를 정의한 다음에 이를 FutureTask로 감싸서 Thread로 넘기고 있는 것을 볼 수 있습니다. 작업이 완료되면 futureTask.get()을 호출해서 웹 페이지의 내용을 가져올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681292838310&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class FutureTaskThreadNonBlockingExample {

    public static void main(String[] args) {
        // 비동기 작업 정의
        Callable&amp;lt;String&amp;gt; fetchWebPageTask = new Callable&amp;lt;String&amp;gt;() {
            @Override
            public String call() throws Exception {
                String urlToRead = &quot;https://www.example.com&quot;;
                URL url = new URL(urlToRead);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod(&quot;GET&quot;);
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuilder result = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    result.append(line);
                }
                reader.close();
                System.out.println(&quot;웹 페이지를 가져오는 작업이 완료되었습니다.&quot;);
                return result.toString();
            }
        };

        // FutureTask 객체 생성
        FutureTask&amp;lt;String&amp;gt; futureTask = new FutureTask&amp;lt;&amp;gt;(fetchWebPageTask);

        // 새로운 Thread를 생성하고 FutureTask를 실행한다.
        Thread taskThread = new Thread(futureTask);
        taskThread.start();

        // 다른 작업 수행 (예: 데이터 처리)
        System.out.println(&quot;다른 작업 중 ...&quot;);

        while (!futureTask.isDone()) {
            System.out.println(&quot;웹 페이지를 가져오는 작업이 완료되기를 기다리는 중...&quot;);
            try {
                // 일정 시간동안 대기한 후 다시 작업 완료 여부를 확인한다.
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 비동기 작업 결과 가져오기
            String webPageContent = futureTask.get();
            System.out.println(&quot;웹 페이지 내용: &quot; + webPageContent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드 풀(Thread Pool)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스레드 풀의 등장 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀은 도대체 무엇일까요? 스레드들이 뛰어노는 수영장(pool)? 얼추 비슷하다고 할 수 있습니다. 이를 스레드 풀의 이름과 수영장을 연관 지어서 좀 더 친숙한 예시를 들어보자면, 스레드 풀은 마치 여러 수영 선수들이 대기하고 있는 수영장과 비슷합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_ThreadPool_P01.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA44xJ/btr8LgVyXOq/qQgqrdQIBQiySfxIyGc8k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA44xJ/btr8LgVyXOq/qQgqrdQIBQiySfxIyGc8k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA44xJ/btr8LgVyXOq/qQgqrdQIBQiySfxIyGc8k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA44xJ%2Fbtr8LgVyXOq%2FqQgqrdQIBQiySfxIyGc8k0%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;312&quot; height=&quot;312&quot; data-filename=&quot;Attachments_ThreadPool_P01.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&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;스레드 풀도 이와 비슷한 원리로 동작합니다. 여러 작업을 동시에 처리해야 할 때, 매번 새로운 스레드를 생성하고 종료하는 것은 컴퓨터 입장에서 많은 시간과 자원을 소모합니다. 이런 문제를 해결하기 위해서 미리 생성된 스레드들을 '풀(pool)'에 보관하고, 작업이 발생할 때마다 스레드 풀에서 이용 가능한 스레드를 가져와 작업을 처리하게 됩니다. 작업이 끝난 스레드는 다시 스레드 풀로 돌아와서 다음 작업을 기다리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 스레드 풀을 사용함으로써, 프로그램은 재사용으로 스레드 생성과 종료에 드는 비용을 절약하고, 자원을 효율적으로 활용하여 전체 성능을 높일 수 있습니다. 거기에다가 만약에 어떤 요청이 들어와서 즉시 작업을 수행해야 할 때, 해당 요청을 처리할 스레드가 이미 만들어진 상태로 대기하고 있기 때문에 작업을 실행하는 데 딜레이가 발생하지 않아 전체적인 반응 속도도 올라가게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExecutorService&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 Executors와 ExecutorService를 이용하면 복잡한 스레드 관리와 병렬 작업 처리를 단순화하고, 간단하게 스레드 풀을 만들고 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutorService는 비동기 작업의 진행 상황을 추적하는 Future를 만들 수 있는 메서드와 종료를 제어하는 메서드를 제공합니다. 작업의 종료를 위해서 shutdown(), shutdownNow() 메서드를 제공하고, 일괄 작업 처리를 위해서 invokeAny(), invokeAll() 메서드를 사용할 수 있습니다. 또한 submit() 메서드를 통해 작업을 제출할 수 있습니다. 또한 submit()을 통해 작업을 제출할 수도 있습니다. 여기서 제출한다는 의미는 작업을 스레드 풀에 추가하여 스레드가 처리하도록 요청하는 것을 말합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681378986116&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ExecutorService extends Executor {
	// 실행 중인 모든 작업이 완료된 후에 스레드 풀을 종료하도록 요청한다.
	// 이 메서드가 호출된 후에는 새로운 작업을 제출(submit)할 수 없다.
	// 이 메서드는 실행 중인 작업이 종료될 때까지 기다리지 않는다.
    void shutdown();
    // 실행 중인 모든 작업을 중지하고 실행 대기 중이던 작업 리스트를 반환한다.
    // 이 메서드는 실행 중인 작업이 종료될 때까지 기다리지 않는다.
    List&amp;lt;Runnable&amp;gt; shutdownNow();

	// ExecutorService가 종료 요청을 받았는지 확인한다.
    boolean isShutdown();

	// ExecutorService가 완전히 종료되었는지 확인한다.
    boolean isTerminated();

	// 주어진 시간 동안 ExecutorService의 종료를 대기한다.
	// 이 메서드를 사용하면 스레드 풀이 종료될 때까지 대기한 다음 다른 작업을 계속할 수 있다.
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

	// ExecutorService에 작업을 제출하며, 작업이 스레드 풀에서 병렬로 실행된다.
	// 작업이 제출되면 ExecutorService는 작업을 큐에 추가하고
	// 사용 가능한 스레드가 있으면 작업을 실행한다.
	// submit() 메서드는 Future&amp;lt;T&amp;gt; 객체를 반환하며 이를 통해서
	// 작업의 결과를 확인하거나 작업이 완료되었는지 확인할 수 있다.
    &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Callable&amp;lt;T&amp;gt; task);
    &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Runnable task, T result);
    Future&amp;lt;?&amp;gt; submit(Runnable task);

	// 주어진 모든 작업을 실행하고 각 작업의 결과를 담은 Future&amp;lt;T&amp;gt; 객체의 리스트를 반환한다.
	// 이 메서드는 모든 작업이 완료될 때까지 블로킹된다.
    &amp;lt;T&amp;gt; List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)
        throws InterruptedException;
    &amp;lt;T&amp;gt; List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

	// 주어진 작업 목록 중 하나가 성공적으로 완료될 때까지 블로킹되며,
	// 완료된 작업의 결과를 반환한다. 이 메서드는 주어진 작업들을
	// 병렬로 실행하고, 가장 먼저 완료된 작업의 결과를 사용하려는 경우에 유용하다.
    &amp;lt;T&amp;gt; T invokeAny(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)
        throws InterruptedException, ExecutionException;
    &amp;lt;T&amp;gt; T invokeAny(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Executors&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 흔하게 사용하는 여러 가지 설정에 맞춰서 다양한 스레드 풀을 제공하고 있습니다. 미리 정의된 스레드 풀을 사용하려면 Executors 클래스를 사용할 수 있습니다. 아래의 메서드들을 차근차근 살펴보도록 하겠습니다. 참고로, Executors 유틸리티 클래스에서는 ExecutorService 뿐만 아니라 Executor, ScheduledExecutorService, ThreadFactory, Callable에 대한 팩토리 메서드도 제공하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;팩토리 메서드(factory method)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 생성하는 메서드를 말합니다. 이 메서드는 특정 인터페이스나 클래스의 인스턴스를 초기화하는 역할을 담당합니다. 팩토리 메서드를 사용하면 클래스 외부에서 생성자를 직접 호출하는 대신, 객체 생성에 필요한 로직을 캡슐화해서 객체 생성을 단순화하고 유연성을 제공할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;newFixedThreadPool&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 고정된 크기의 스레드 풀을 생성할 수 있습니다. 어떤 시점에서든 최대 nThreads개의 스레드만 작업을 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_ThreadPoolExecutor_P01.png&quot; data-origin-width=&quot;3640&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pLcXf/btr94u4B35E/rIIyLUdH4933dRlR5t4cS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pLcXf/btr94u4B35E/rIIyLUdH4933dRlR5t4cS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pLcXf/btr94u4B35E/rIIyLUdH4933dRlR5t4cS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpLcXf%2Fbtr94u4B35E%2FrIIyLUdH4933dRlR5t4cS1%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;3640&quot; height=&quot;1161&quot; data-filename=&quot;Attachments_ThreadPoolExecutor_P01.png&quot; data-origin-width=&quot;3640&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 스레드가 작업을 처리 중인 상태에서 추가 작업을 제출하면, 작업을 끝낸 스레드가 생길 때까지 크기가 무제한인 공유 작업 큐에서 대기하게 됩니다. 만약, 실행 중 오류로 인해 스레드가 종료되면 새로운 스레드가 생성되어 대기 중인 작업을 처리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681379204491&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads,
								  0L, TimeUnit.MILLISECONDS,
								  new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;());
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;newCachedThreadPool&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 캐시된 스레드 풀을 생성할 수 있습니다. 이 풀은 필요에 따라서 스레드를 생성하고, 사용되지 않는 스레드는 캐시에서 제거합니다. 다시 말해서, 풀에 갖고 있는 스레드의 수가 처리할 작업의 수보다 많아서 쉬는 스레드(60초 동안 사용되지 않는 스레드)가 많아지면 쉬는 스레드를 종료시키며, 처리할 작업의 수가 많아지면 필요한 만큼 스레드를 새로 생성하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681379236548&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
								  60L, TimeUnit.SECONDS,
								  new SynchronousQueue&amp;lt;Runnable&amp;gt;());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 ThreadPoolExecutor의 두 번째 인자는 maximumPoolSize인데 이는 풀에서 허용하는 최대 스레드의 수를 말합니다. 즉, 이게 Integer.MAX_VALUE라는 의미는 스레드의 수에 제한을 두지 않는다는 것입니다. 따라서 요청의 수가 많아지면 무한정으로 스레드를 생성할 수 있게 되므로 주의가 필요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;newSingleThreadExecutor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;newSingleThreadExecutor는 하나의 스레드를 사용하는 스레드 풀입니다. 즉, 한 번에 하나의 작업만 수행할 수 있습니다. 만약 작업 도중에 예외가 발생해서 비정상적으로 종료되면 새로운 스레드를 생성하여 나머지 작업을 실행합니다. 또한, 등록된 작업은 반드시 순차적으로 처리됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681379300141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService
		(new ThreadPoolExecutor(1, 1,
								0L, TimeUnit.MILLISECONDS,
								new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;newScheduledThreadPool&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;newScheduledThreadPool 스레드 풀은 주기적으로 실행하거나 일정 시간 이후에 실행하는 작업을 실행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681379335957&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
	return new ScheduledThreadPoolExecutor(corePoolSize);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 살펴보기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;고정된 스레드 풀을 사용하는 간단한 예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 newFixedThreadPool을 사용해서 고정된 크기의 스레드 풀을 사용하여 5개의 작업을 동시에 실행하는 코드를 볼 수 있습니다. 그 후, shutdown()을 호출해서 ExecutorService를 종료하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681380224296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // 고정 크기의 스레드 풀 생성 (크기: 5)
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 10개의 작업을 스레드 풀에 제출
        for (int i = 0; i &amp;lt; 10; i++) {
            final int taskId = i + 1;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + &quot; 스레드에서 작업 &quot; + taskId + &quot;을 실행 중입니다...&quot;);
                    try {
                        // 각 작업이 2초간 실행되도록 시뮬레이션
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + &quot; 스레드에서 작업 &quot; + taskId + &quot;을 완료했습니다.&quot;);
                }
            });
        }

        // 모든 작업이 완료되면 ExecutorService를 종료
        executorService.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;캐시된 스레드 풀을 사용하여 소수를 찾는 예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 newCachedThreadPool을 사용해서 여러 숫자 범위 내의 소수를 찾는 작업을 동시에 수행합니다. 각 작업은 주어진 범위 내에서 소수를 찾아 리스트에 추가하고 반환하게 됩니다. 작업 리스트가 완료되면 결과를 출력하고 ExecutorService가 종료됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681381274914&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class CachedThreadPoolPrimeExample {
    public static void main(String[] args) {
        // 캐시된 스레드 풀 생성
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 숫자 범위 목록 생성
        int[][] ranges = {{2, 1000000}, {1000001, 2000000}, {2000001, 3000000}, {3000001, 4000000}, {4000001, 5000000}};

        // Callable 작업 목록 생성
        List&amp;lt;Callable&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;&amp;gt; tasks = new ArrayList&amp;lt;&amp;gt;();
        for (int[] range : ranges) {
            tasks.add(new Callable&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;() {
                @Override
                public List&amp;lt;Integer&amp;gt; call() throws Exception {
                    return findPrimesInRange(range[0], range[1]);
                }
            });
        }

        // 작업 목록을 스레드 풀에 제출하고 결과를 처리
        long startTime = System.nanoTime();
        try {
            List&amp;lt;Future&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;&amp;gt; results = executorService.invokeAll(tasks);
            for (Future&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; result : results) {
                System.out.println(&quot;찾은 소수: &quot; + result.get());
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        long endTime = System.nanoTime();
        long durationInMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
        System.out.println(&quot;수행 시간: &quot; + durationInMillis + &quot;ms&quot;);

        // 모든 작업이 완료되면 ExecutorService를 종료
        executorService.shutdown();
    }

    // 주어진 범위에서 소수를 찾는 메서드
    public static List&amp;lt;Integer&amp;gt; findPrimesInRange(int start, int end) {
        List&amp;lt;Integer&amp;gt; primes = new ArrayList&amp;lt;&amp;gt;();
        for (int i = start; i &amp;lt;= end; i++) {
            if (isPrime(i)) {
                primes.add(i);
            }
        }
        return primes;
    }

    // 소수 판별 메서드
    public static boolean isPrime(int number) {
        if (number &amp;lt;= 1) {
            return false;
        }
        for (int i = 2; i &amp;lt;= Math.sqrt(number); i++) {
            if (number % i == 0) {
                return false;
            }
        }
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ThreadLocal&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal을 사용하면 손쉽게 스레드 로컬 변수를 만들 수 있습니다. 스레드 로컬 변수는 하나의 스레드에서만 사용할 수 있는 변수를 말합니다. ThreadLocal 클래스에는 get()과 set() 메서드가 있는데 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681300373093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ThreadLocal&amp;lt;T&amp;gt; {
	// 현재 실행 중인 스레드에서 최근에 set() 메서드를 호출해 저장했던 값을 가져온다.
	public T get() { /* ... */ }

	// 현재 실행 중인 스레드의 로컬 변수에 값을 설정한다.
	public void set(T value) { /* ... */ }

	// 현재 실행 중인 스레드의 로컬 변수 값을 제거한다.
	// 이렇게 하면 해당 변수에 대한 현재 스레드의 값이 사라지고,
	// 다음 get() 호출 시 initialValue() 메서드를 통해 초기값이 다시 설정된다.
	public void remove() { /* ... */ }
	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal의 내부를 보면 간단명료하게 만들어진 것을 볼 수 있습니다. 처음으로 get() 메서드가 호출되면 ThreadLocal의 값은 initialValue() 메서드가 반환하는 값으로 초기화됩니다. 그 후로는 ThreadLocal의 값은 각각의 스레드마다 독립적으로 유지되며, set() 메서드를 호출하여 값을 변경할 수 있습니다. 이때 set() 메서드를 호출한 스레드에서만 해당 값이 변경되고, 다른 스레드에는 영향을 주지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681300406397&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ThreadLocal&amp;lt;T&amp;gt; {
	// ...
	public T get() {
		// 현재 실행 중인 스레드를 가져온다.
		Thread t = Thread.currentThread();
		// 스레드 로컬 값을 저장하는데 사용되는 해시 맵이다.
		ThreadLocalMap map = getMap(t);
		if (map != null) {
			// 스레드 로컬 객체를 키로 키-값 쌍을 가져온다.
			ThreadLocalMap.Entry e = map.getEntry(this);
			if (e != null) {
				// 키(스레드 로컬 객체)에 매핑된 값을 반환한다.
				@SuppressWarnings(&quot;unchecked&quot;)
				T result = (T)e.value;
				return result;
			}
		}
		// 기본적으로 초기값은 null이다.
		return setInitialValue();
	}
	
	public void set(T value) {
		Thread t = Thread.currentThread();
		ThreadLocalMap map = getMap(t);
		if (map != null) {
			map.set(this, value);
		} else {
			createMap(t, value);
		}
	}

	// 이 스레드 로컬 변수에 대한 현재 스레드의 초기 값을 반환한다.
	// 스레드가 이전에 set() 메서드를 호출한 경우가 아니면,
	// 스레드가 get() 메서드를 통해 변수에 처음 접근할 때 이 메서드가 호출된다.
	// 이 구현은 null만 반환하며, null이 아닌 초기값을 가지고 싶다면
	// 개발자가 ThreadLocal을 상속받아서 오버라이딩해야 한다.
	protected T initialValue() {
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread 클래스 내부를 살펴보면 아래와 같이 threadLocals를 멤버로 갖고 있음을 볼 수 있습니다. ThreadLocal을 통해 저장했던 값들이 이 ThreadLocalMap으로 들어가게 됩니다. 이는 일종의 해시 맵으로 스레드 로컬 변수를 저장하기 위해 만들어진 간단한 자료구조입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681300448233&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Thread implements Runnable {
	// ...
	ThreadLocal.ThreadLocalMap threadLocals = null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 살펴보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 초기값을 반환하는 initialValue() 메서드를 오버라이딩하여 각 스레드가 고유한 ID를 가지도록 만들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1681300493511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ThreadLocalExample {
    // AtomicInteger를 사용하여 스레드 안전한 ID 생성기를 만든다.
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // ThreadLocal 변수를 생성하고 initialValue 메소드를 재정의하여 각 스레드마다 고유한 ID를 할당한다.
    private static final ThreadLocal&amp;lt;Integer&amp;gt; threadId = new ThreadLocal&amp;lt;Integer&amp;gt;() {
        @Override
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    // 현재 스레드의 ID를 반환하는 메서드
    public static int getThreadId() {
        return threadId.get();
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            int threadId = getThreadId();
            System.out.println(&quot;스레드 ID &quot; + threadId + &quot;: 작업 실행 중.&quot;);
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i &amp;lt; 10; i++) {
            executor.submit(new Task());
        }

        executor.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 대략적으로 ThreadLocalMap의 상태를 그려보면 다음과 같습니다. ThreadLocalMap은 ThreadLocal 변수를 키로 사용해서 각 스레드별로 저장되는 값을 관리하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_ThreadLocal_P01.png&quot; data-origin-width=&quot;4135&quot; data-origin-height=&quot;2353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbe2kS/btr9F99ycJj/yPQWERb5sqkNDnJbDHm3HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbe2kS/btr9F99ycJj/yPQWERb5sqkNDnJbDHm3HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbe2kS/btr9F99ycJj/yPQWERb5sqkNDnJbDHm3HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbe2kS%2Fbtr9F99ycJj%2FyPQWERb5sqkNDnJbDHm3HK%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;4135&quot; height=&quot;2353&quot; data-filename=&quot;Attachments_ThreadLocal_P01.png&quot; data-origin-width=&quot;4135&quot; data-origin-height=&quot;2353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 스레드마다 고유한 ID를 할당하기 때문에, ThreadLocalMap은 스레드 별로 고유한 ID를 저장하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;객체의 수명&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal의 값이 더 이상 필요하지 않을 때는 항상 값을 제거해야 합니다. 그렇지 않으면 ThreadLocal에 저장된 객체는 스레드가 죽을 때까지 메모리에서 해제되지 않으므로 메모리 누수가 일어날 수도 있습니다. 따라서 ThreadLocal을 사용한 뒤에는 remove() 메서드를 호출해서 값을 제거해주어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681309129199&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
  threadLocal.set(value);
  /* ... */
} finally {
	threadLocal.remove();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위의 스레드 ID 예시에서도 살펴봤지만, 스레드 풀을 사용하면 스레드가 재활용될 수 있기 때문에 이전에 설정한 값이 계속 남아있어 원치 않는 결과를 초래할 수 있습니다. 대표적인 예시가 스레드 풀을 사용하는 WAS의 경우인데 이전 요청에서 ThreadLocal에 저장했던 사용자의 정보가 다른 요청에서 유출될 수 있으므로 각별한 주의가 필요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;눈에&amp;nbsp;보이지&amp;nbsp;않는&amp;nbsp;연결&amp;nbsp;관계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 전역 변수가 아니지만 전역 변수처럼 동작할 수 있어서 프로그램 구조에 문제를 일으킬 수 있습니다. 메서드 인자로 값을 전달하는 대신 ThreadLocal을 사용해서 값을 전달하면 프로그램 구조가 허약해질 수 있습니다. 이렇게 되면 메서드 사이의 명시적인 연결 고리가 약해지고 코드의 가독성이 떨어지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 ThreadLocal 변수를 사용하면 객체 간 눈에 보이지 않는 연결 관계가 생기기 쉽습니다. 이로 인해서 코드의 의존성이 증가하고, 프로그램이 예기치 않은 방식으로 동작할 수 있습니다. 따라서 ThreadLocal 변수를 사용할 때는 주의가 필요하며, 객체 간의 의존성을 최소화하고 코드를 명확하게 구현하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>java</category>
      <category>동기</category>
      <category>비동기</category>
      <category>스레드</category>
      <category>자바</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/401</guid>
      <comments>https://exynoa.tistory.com/401#entry401comment</comments>
      <pubDate>Sun, 9 Apr 2023 07:26:40 +0900</pubDate>
    </item>
    <item>
      <title>30편. 스레드(Thread) (3)</title>
      <link>https://exynoa.tistory.com/377</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PCXly/btr8Mp4xAPO/QBKNYlAaJkshMA9XhXRsV0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PCXly/btr8Mp4xAPO/QBKNYlAaJkshMA9XhXRsV0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PCXly/btr8Mp4xAPO/QBKNYlAaJkshMA9XhXRsV0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPCXly%2Fbtr8Mp4xAPO%2FQBKNYlAaJkshMA9XhXRsV0%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;486&quot; height=&quot;304&quot; data-filename=&quot;img1.daumcdn.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드의 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 스레드는 총 6개의 상태(NEW, RUNNABLE, WAITING, TIMED_WAITING, BLOCKED, TERMINATED)를 가지고 있습니다. 참고로 자바의 스레드는 JVM(Java Virtual Machine, 자바 가상 머신) 위에서 돌아가며, 여기에 나와 있는 상태들은 가상 머신의 상태를 말하는 것입니다. 다시 말해서, 운영체제 스레드의 상태를 나타내는 것은 아닙니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NEW&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 스레드를 만들면 NEW 상태가 됩니다. 이 상태의 스레드는 아직 시작되지 않았으며, start() 메서드를 호출하며 스레드를 시작하면 RUNNABLE 상태로 들어가게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960817&quot; class=&quot;lasso&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread thread = new Thread(new ThreadA());
System.out.println(thread.getState()); // NEW&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RUNNABLE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 생성하고 start() 메서드를 호출하면 NEW에서 RUNNABLE 상태로 이동합니다. 자바에서는 '실행 가능한(Runnable)' 상태와 '실행 중(Running)' 상태가 RUNNABLE로 합쳐져 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960818&quot; class=&quot;lasso&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread thread = new Thread(new ThreadA());
thread.start();
System.out.println(thread.getState()); // 높은 확률로 RUNNABLE&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WAITING&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기 중인 스레드의 상태를 의미합니다. 아래에 있는 메서드 중 하나를 호출해서 스레드가 대기 상태에 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Object.wait() (시간 제한 없음)&lt;/li&gt;
&lt;li&gt;Thread.join() (시간 제한 없음)&lt;/li&gt;
&lt;li&gt;LockSupport.park()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기 중인 스레드는 다른 스레드가 특정 작업이 완료되기를 대기하고 있는 중입니다. 예를 들어서, 객체에서 Object.wait() 메서드를 호출한 후에 스레드는 해당 객체에서 Object.notify() 혹은 Object.notifyAll()을 호출하는 다른 스레드를 기다리고 있는 것입니다. Thread.join()을 호출한 스레드는 해당 스레드가 종료되기를 기다리고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680870080240&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class WaitingStateExample {
    public static void main(String[] args) throws InterruptedException {
        Thread workingThread = new Thread(new WorkingRunnable());
        Thread waitingThread = new Thread(new WaitingRunnable(workingThread));

        workingThread.start();
        waitingThread.start();

        Thread.sleep(500); // 충분한 시간동안 스레드들이 실행될 수 있게 기다림
        System.out.println(&quot;WaitingThread 현재 상태: &quot; + waitingThread.getState()); // WAITING

        workingThread.join();
        waitingThread.join();
    }

    public static class WorkingRunnable implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class WaitingRunnable implements Runnable {
        private final Thread workingThread;

        public WaitingRunnable(Thread workingThread) {
            this.workingThread = workingThread;
        }

        @Override
        public void run() {
            try {
                workingThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TIMED_WAITING&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 다른 스레드로부터 작업이 완료되기를 지정된 시간 동안 기다리는 상태를 말합니다. WAITING과 비슷한데 무한정 기다리는게 아니라 지정한 시간 만큼을 기다리고 있는 상태를 의미합니다. 아래의 메서드 중 하나를 호출해서 스레드가 TIMED_WAITING 상태에 있는 중일 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread.sleep()&lt;/li&gt;
&lt;li&gt;Object.wait() (시간 제한 있음)&lt;/li&gt;
&lt;li&gt;Thread.join() (시간 제한 있음)&lt;/li&gt;
&lt;li&gt;LockSupport.parkNanos()&lt;/li&gt;
&lt;li&gt;LockSupport.parkUntil()&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1680870123334&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TimedWaitingStateExample {

    public static void main(String[] args) throws InterruptedException {
        Thread timedWaitingThread = new Thread(new TimedWaitingTask());

        timedWaitingThread.start();

        Thread.sleep(500); // 충분한 시간동안 스레드가 실행될 수 있게 기다림
        System.out.println(&quot;TimedWaitingThread state: &quot; + timedWaitingThread.getState()); // TIMED_WAITING

        timedWaitingThread.join();
    }

    public static class TimedWaitingTask implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BLOCKED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 기다리며 블록된(차단된, blocked) 스레드 상태를 의미합니다. 블록된 상태의 스레드는 synchronized 블록 혹은 메서드에 진입하려고 했으나 다른 스레드가 이미 해당 락을 보유하고 있거나, Object.wait()을 호출한 후에 락을 포기하고 깨어난 뒤 다시 synchronized 블록 혹은 메서드에 진입하기 위해서 락을 기다리고 있는 상태를 말합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680870157424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BlockedStateExample {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new BlockedTask(), &quot;스레드 1&quot;);
        Thread thread2 = new Thread(new BlockedTask(), &quot;스레드 2&quot;);

        synchronized (lock) {
            thread1.start();
            Thread.sleep(100);
            
            thread2.start();
            Thread.sleep(1000); // 충분한 시간동안 스레드들이 실행될 수 있게 기다림

            System.out.println(&quot;스레드 1 상태: &quot; + thread1.getState()); // BLOCKED
            System.out.println(&quot;스레드 2 상태: &quot; + thread2.getState()); // BLOCKED
        }

        thread1.join();
        thread2.join();
    }

    public static class BlockedTask implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + &quot;이(가) 락을 얻었습니다.&quot;);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TERMINATED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TERMINATED 상태는 예외로 인해 스레드가 중단됐거나, 실행을 모두 끝낸 상태를 말합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680870179944&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TerminatedStateExample {

    public static class SimpleRunnable implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread simpleThread = new Thread(new SimpleRunnable());
        simpleThread.start();

        System.out.println(&quot;join() 전: &quot; + simpleThread.getState()); // 아마도 RUNNABLE, TIMED_WAITING 중 하나

        simpleThread.join();

        System.out.println(&quot;join() 후: &quot; + simpleThread.getState()); // TERMINATED
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드의 동기화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Private 락(Private Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 private 락의 예입니다. 조금 다른게 있다면 락 전용 객체를 하나 만들고, 이 객체를 외부에서 접근할 수 없도록 private로 지정한 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960819&quot; class=&quot;cs&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Counter {
    private int count = 0;
    private Object lock = new Object();

    public void increment() {
    	synchronized (lock) {
    		count++;
    	}
    }
    
    /* ... */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 동기화 메서드나 동기화 블록으로 충분한 것 같은데 왜 굳이 private 락 객체를 만들까요? 아래를 보면 그 이유를 알 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Counter {
    private int count = 0;

    public synchronized void increment() {
    	count++;
    }
    
    public int getValue() {
    	return count;
    }
}

class Worker implements Runnable {
    private Counter counter;

    public Worker(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        for (int i = 0; i &amp;lt; 10000; i++) {
            counter.increment();
        }
    }
}

public class PrivateLockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread worker = new Thread(new Worker(counter));

        worker.start();
        // 내부에서 무한정 대기하므로 객체 counter의 락이 풀리지 않음
        synchronized (counter) {
        	while (true) {
        		Thread.sleep(Integer.MAX_VALUE);
        	}
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드에서 객체 counter의 락을 획득하고 무한정 대기하면 자식 스레드는 시작되었음에도 불구하고 동기화 메서드인 increment()에 접근할 수 없습니다. 여기서 private 락 객체를 사용하면 외부에서 접근할 수 없기 때문에 이런 문제를 해결할 수 있습니다. 하지만 내부에서 프로그래머의 실수로 락 객체를 새로 할당할 수도 있으므로 아래와 같이 private final로 선언하는 것을 권장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960821&quot; class=&quot;processing&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final Object lock = new Object();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lock&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;synchronized와의 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화를 위해서 기존에 synchronized 키워드를 사용했었는데, 이 synchronized는 락을 블록 단위로 획득하거나 해제할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680760721375&quot; class=&quot;aspectj&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public synchronized void methodA() {
	// ...
}

public void methodB() {
	synchronized (this) {
		// ...
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여러 개의 락을 얻은 경우에는 락의 획득 순서와 반대로 락을 해제해야 된다는 특징이 있었습니다. 다시 말해서, 가장 마지막에 획득한 락을 가장 먼저 해제하고, 가장 먼저 획득한 락을 가장 마지막에 해제해야 했습니다. 그리고 락을 획득한 코드 블록이 끝날 때, 자동으로 모든 락이 해제되어야 했었습니다. 하지만 A의 락을 획득한 다음 B의 락을 획득하고, A의 락을 해제한 다음 C를 획득하고 B를 해제하는 식으로 락을 좀 더 유연하게 다뤄야 할 때도 있는데 이럴 때 Lock을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그뿐만이 아니라 Lock은 락을 확보할 때 시간 제한을 두거나, 스레드가 가지고 있는 락의 갯수를 확인하거나, 현재 스레드가 락을 소유하고 있는지 등 다양한 메서드를 제공하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680760771649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Lock {
	// 락을 획득한다. 만약 락을 얻을 수 없으면 현재 스레드는 
	// 블록 상태(BLOCKED)가 되고 락을 획득할 때까지 대기한다.
    void lock();

	// 현재 스레드가 인터럽트 상태가 아닐 때 락을 획득할 수 있다.
	// 만약에, 현재 스레드가 이 메서드에 진입할 때 인터럽트 상태가 설정된 경우
	// 혹은 락을 획득하는 동안 인터럽트가 발생하면 InterruptedException
	// 이 발생하고 현재 스레드의 인터럽트 상태가 지워진다.
    void lockInterruptibly() throws InterruptedException;

	// 바로 락을 시도하고 성공 여부를 boolean 타입으로 반환한다.
	// 예를 들어, 락을 사용할 수 있으면 즉시 획득하고 true를 반환한다.
    boolean tryLock();

	// 지정한 시간 내에 락을 얻을 수 있으면 현재 스레드가 락을 획득하고
	// true를 반환한다. 만약 지정한 대기 시간이 지난 경우에는 false를 반환한다.
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

	// 락을 해제한다.
    void unlock();

	// 현재 락 인스턴스와 연결된 Condition 객체를 반환한다.
    Condition newCondition();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주의사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 주의해야 할 점은 synchronized와는 달리 예외가 발생하면 락을 풀지 못하고 크리티컬 섹션(critical section)을 빠져나가는 일이 있을 수 있으므로 try-catch문이나 try-finally문을 사용하여 예외가 발생하더라도 락을 풀 수 있도록 만들어야 한다는 것입니다. 만약에 프로그래머의 실수로 이를 잊는다면 관련된 문제가 발생했을 때 락이 언제 어디서 풀렸는지 기록이 남는 게 아니므로 문제의 원인을 찾기가 힘들어집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680760813924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Lock lock = new ReentrantLock();
/* ... */
lock.lock();
try {
	// 크리티컬 섹션
} finally {
	lock.unlock();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;tryLock()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock()과 unlock()은 대충 짐작이 가셨을테니, 여기서는 tryLock()만 살펴보도록 하겠습니다. tryLock()은 위에서도 살펴봤지만, 바로 락을 시도하고 그에 대한 성공 여부를 반환하는 메서드입니다. 보통은 아래와 같은 형태로 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680760882847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Lock lock = ...;
if (lock.tryLock()) {
	try {
		// 크리티컬 섹션
	} finally {
		lock.unlock();
	}
} else {
	// 락을 획득할 수 없으면
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시 코드입니다. 매 실행마다 10이란 결과를 얻을 수도 있고 10 미만의 결과를 얻을 수도 있는 걸 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680760900400&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LockExample {
    private static final Lock lock = new ReentrantLock();
    public static int count = 0;

    public static Thread createNewThread() {
        return new Thread(() -&amp;gt; {
            if (lock.tryLock()) {
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(&quot;스레드 &quot; + Thread.currentThread().getName() + &quot;(이)가 락을 얻는 데 실패했습니다.&quot;);
            }
        });
    }

    public static void main(String[] args) {
        int threadCount = 10;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i &amp;lt; threads.length; i++)
            threads[i] = createNewThread();

        for (Thread value : threads)
            value.start();

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(&quot;count = &quot; + count);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReentrantLock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReentrantLock 클래스는 자바 5 이후로 java.util.concurrent.locks 패키지에 추가되었습니다. synchronized를 이용한 동기화는 한계가 있을 때 보다 확장된 기능들을 갖춘 ReentrantLock을 사용하는 걸 고려해볼 수 있습니다. 사용법은 아래와 같이 lock() 메서드를 호출하여 락을 얻은 뒤에, unlock() 메서드를 호출하여 락을 해제합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1644057960821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Lock lock = new ReentrantLock();
/* ... */
lock.lock();
try {
	// 크리티컬 섹션
} finally {
	lock.unlock();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공평성(fairness)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ReentrantLock 클래스의 생성자를 살펴보면 fair라는 boolean형 매개변수를 받는데, synchronized와는 달리 공평성(fairness)을 지원하기 때문입니다. 공정한 순서를 유지하기 위한 추가적인 작업이나 관리에 들어가는 오버헤드 부담과 같은 성능 저하 문제로 기본값은 false, 즉 불공평(non-fair) 모드로 되어 있습니다. 따라서 꼭 공평성이 필요한 경우가 아니라면 이를 true로 지정해 성능을 떨어뜨리는 결과를 얻을 필요는 없습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;공평성(fairness)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공평성의 의미는 이름 그대로 락의 획득 순서가 스레드들 사이에서 공평하게 이루어지는가를 나타냅니다. 즉, 기다리는 스레드들이 먼저 온 순서대로 자원에 액세스할 수 있다는 보장을 받게 됩니다. 반대로 공평성을 고려하지 않는 불공평(non-fair) 모드에서는 락의 획득 순서가 불규칙적으로 이루어질 수 있고, 어떤 스레드는 락을 오랫동안 기다려야 할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680789389023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 사용하는가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReentrantLock은 복잡한 동기화 요구 사항이 있을 때 사용할 수 있으나, synchronized와는 달리 락의 관리를 프로그래머에게 쥐여줌으로써 자칫 잘못하면 더 위험하다는 단점이 있습니다. 보통은 ReentrantLock에서 제공하는 기능이 필요한 게 아니면 간편하고 가독성이 좋은 synchronized를 사용하는 것을 권장합니다. ReentrantLock의 사용을 고려해볼 수 있는 것에는 아래와 같은 경우들이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;락을 확보할 때 타임아웃을 지정해야 하는 경우&lt;/li&gt;
&lt;li&gt;보호해야 하는 범위가 블록 단위를 벗어날 경우&lt;/li&gt;
&lt;li&gt;기다리는 스레드들이 먼저 온 순서대로 자원에 액세스해야 함이 보장되어야 하는 경우&lt;/li&gt;
&lt;li&gt;락을 확보하느라 대기 상태에 들어가 있을 때 인터럽트를 걸 수 있어야 하는 경우&lt;/li&gt;
&lt;li&gt;주기적으로 락 상태를 확인하면서 락을 획득하려고 시도하는 경우. 즉, 락 획득에 실패한 경우 스레드가 다른 작업을 수행하거나 주기적으로 락 상태를 확인하는 경우를 말한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CountDownLatch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CountDownLatch는 이름 그대로 걸쇠(latch)는 걸쇠인데, 이 걸쇠에 카운터가 붙어있어서 카운터에 0이 찍힐 때까지 호출 스레드를 블록시킬 수 있습니다. 다르게 비유하면 CountDownLatch가 관문의 역할을 한다고 할 수 있습니다. 먼저 D라는 작업을 하기 위해서는 다른 스레드가 작업 중인 A, B, C가 완료된 상태여야 하는데 이런 경우에 CountDownLatch를 사용할 수 있습니다(어느 순서로 완료되었는지는 중요하지 않음).&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_CountDownLatch_P01.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMC566/btr8KJhQ8hx/u6TkJUF7qL2T4bf6blGmN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMC566/btr8KJhQ8hx/u6TkJUF7qL2T4bf6blGmN0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMC566/btr8KJhQ8hx/u6TkJUF7qL2T4bf6blGmN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMC566%2Fbtr8KJhQ8hx%2Fu6TkJUF7qL2T4bf6blGmN0%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;596&quot; height=&quot;389&quot; data-filename=&quot;Attachments_CountDownLatch_P01.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 자세하게 말하면, CountDownLatch는 다른 스레드에서 수행 중인 작업들을 모두 완료할 때까지 대기할 수 있도록 해줍니다. 작업의 수를 미리 지정하고, 해당 작업이 모두 완료될 때까지 대기하는 스레드를 만들어서 CountDownLatch에 등록해 놓으면, 작업이 완료될 때마다 카운트가 감소하게 됩니다. 이 때 카운트가 0이 되면 대기 중인 스레드들은 모두 동시에 실행될 수 있게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680856971044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CountDownLatch {
	// ...
	// 현재 스레드가 인터럽트되지 않는 한,
	// 카운트가 0이 될 때까지 현재 스레드를 블록시킨다.
	// 현재 카운트가 0이면 이 메서드는 즉시 반환된다.
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

	// 현재 스레드가 인터럽트 되거나 지정한 대기 시간이 경과할 때까지 혹은
	// 해당 래치의 카운트가 0이 될 때까지 현재 스레드를 블록시킨다.
	// 현재 카운트가 0이면 이 메서드는 즉시 true를 반환한다.
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

	// 래치의 카운트를 1 감소시킨다. 래치의 카운트가 0이 되면 
	// await()을 호출하여 래치를 획득한 모든 스레드가 블록 상태에서 풀려난다.
	// 현재 카운트가 0이라면 아무 일도 일어나지 않는다.
    public void countDown() {
        sync.releaseShared(1);
    }

	// 래치의 현재 카운트를 반환한다.
    public long getCount() {
        return sync.getCount();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 흐름을 살펴봅시다. 먼저 래치의 초기 카운트를 설정해야 합니다. 참고로 이 값은 한 번만 설정할 수 있고, 이 값을 리셋시키는 다른 메서드는 존재하지 않습니다. 다시 말해서 래치는 재사용이 불가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680856993079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CountDownLatch latch = new CountDownLatch(4);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await() 메서드를 호출한 모든 스레드는 이 카운트가 0에 도달하거나 다른 스레드에 의해서 인터럽트 될 때까지 기다립니다. 작업 스레드는 작업을 모두 끝냈거나 사전 준비를 모두 마쳤으면 countDown() 메서드를 호출해서 카운트다운을 수행합니다. 카운트가 0에 도달하면 await()을 호출하고 블록됐던 스레드들이 실행되기 시작합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_CountDownLatch_P02.png&quot; data-origin-width=&quot;1685&quot; data-origin-height=&quot;1830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qIU5w/btr8GizoTSA/9BuzyS01ihBk5wTd68GpP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qIU5w/btr8GizoTSA/9BuzyS01ihBk5wTd68GpP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qIU5w/btr8GizoTSA/9BuzyS01ihBk5wTd68GpP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqIU5w%2Fbtr8GizoTSA%2F9BuzyS01ihBk5wTd68GpP1%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;420&quot; height=&quot;456&quot; data-filename=&quot;Attachments_CountDownLatch_P02.png&quot; data-origin-width=&quot;1685&quot; data-origin-height=&quot;1830&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;pre id=&quot;code_1680857127234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numOfTasks = 4;
        CountDownLatch latch = new CountDownLatch(numOfTasks);

        for (int i = 1; i &amp;lt;= numOfTasks; i++) {
            Task task = new Task(i, latch);
            new Thread(task).start();
        }

        latch.await();
        System.out.println(&quot;모든 작업이 완료되었습니다!&quot;);
    }
}

class Task implements Runnable {
    private final int taskId;
    private final CountDownLatch latch;

    public Task(int taskId, CountDownLatch latch) {
        this.taskId = taskId;
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println(&quot;작업 #&quot; + taskId + &quot;이 실행 중입니다...&quot;);
        // 시간이 오래 걸리는 작업을 수행하는 코드
        try {
            // 시뮬레이션을 위해 무작위로 스레드를 잠시 중지한다.
            Thread.sleep((int) (Math.random() * 3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;작업 #&quot; + taskId + &quot;이 완료되었습니다!&quot;);
        latch.countDown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CyclicBarrier&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CyclicBarrier를 사용하면 어떤 집합에 있는 스레드들이 서로가 공통 배리어 포인트에 도달할 때까지 기다릴 수 있도록 할 수 있습니다. 여기서 cyclic 은 스레드의 블록 상태가 풀린 후에 래치(latch)와는 다르게 다시 사용할 수 있기 때문에 순환(cyclic)이라는 단어가 붙은 것입니다. 그리고 barrier는 말 그대로 '장벽'이나 '벽'이라는 뜻을 가지고 있습니다. 따라서 CyclicBarrier는 스레드들이 서로를 기다리는 '벽'이라는 의미를 가진다고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기능은 병렬 처리나 멀티 스레드 환경에서 작업의 순서를 조절하거나 동기화하는 데에 유용하게 사용할 수 있습니다. 예를 들어, 모든 스레드가 특정 작업을 마치기 전까지 다음 작업으로 진행하지 않도록 하거나, 여러 개의 스레드가 각자 독립적으로 작업을 수행한 후에 모든 결과를 합치는 작업이 필요할 때 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680865082554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CyclicBarrier {
	// ...
	// 파티(스레드)의 수를 반환한다.
	// 즉, 배리어에 걸리기 전에 await() 메서드를 호출해야 하는 스레드의 수를 말한다.
	public int getParties() {
        return parties;
    }

	// 현재 배리어에 대해서 모든 파티(parties)가 await()을 호출할 때까지 대기한다. 
	// 현재 스레드가 마지막으로 도착하지 않은 경우, 아래 중 하나가
	// 일어날 때까지 현재 스레드가 블록된다.
	// (1) 마지막 스레드가 도착함
	// (2) 다른 스레드가 현재 스레드를 인터럽트함 
	// (3) 똑같이 배리어로 인해 블록 상태에 있는 다른 스레드 중 하나가 인터럽트됨
	// (4) 다른 스레드가 이 배리어의 reset() 메서드를 호출함
	// 여기서 (2), (3), (4)가 발생하면 BrokenBarrierException이 발생한다.
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

	// 위와 동일하나 모든 파티가 배리어에 도착하면 지정한 대기 시간이
	// 지날 때까지 대기한다.
	// 만약에 대기 시간이 경과해도 마지막 스레드가 도착하지 않으면
	// TimeoutException이 발생한다.
    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

	// 이 배리어가 중단되었는지에 대한 여부를 반환한다.
	// 즉, 한 개 이상의 스레드가 생성 이후나 마지막 리셋 이후
	// 인터럽트나 타임아웃으로 인해 이 배리어를 벗어났거나,
	// 작업 도중 예외가 발생하여 실패했다면 true를 반환한다.
    public boolean isBroken() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return generation.broken;
        } finally {
            lock.unlock();
        }
    }

	// 배리어를 초기 상태로 되돌린다.
	// 현재 배리어에서 대기 중인 스레드가 있으면 해당 스레드에
	// BrokenBarrierException이 발생한다.
    public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parties는 CyclicBarrier를 사용하는 스레드의 수를 의미합니다. await() 메서드는 다른 모든 스레드가 '벽'에 도달할 때까지 기다리게 하며, 모든 스레드가 도달하면 '벽'이 허물어지고 모든 스레드가 블록 상태에서 벗어나 다음 작업으로 진행할 수 있게 됩니다. 그 후, 배리어는 다시 초기 상태로 돌아가 다음 배리어 포인트를 준비하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_CountDownLatch_P03.png&quot; data-origin-width=&quot;2229&quot; data-origin-height=&quot;1890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9vjRu/btr8J0dD8Md/J57i8wIajyKMgPJgXTI6sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9vjRu/btr8J0dD8Md/J57i8wIajyKMgPJgXTI6sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9vjRu/btr8J0dD8Md/J57i8wIajyKMgPJgXTI6sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9vjRu%2Fbtr8J0dD8Md%2FJ57i8wIajyKMgPJgXTI6sk%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;560&quot; height=&quot;475&quot; data-filename=&quot;Attachments_CountDownLatch_P03.png&quot; data-origin-width=&quot;2229&quot; data-origin-height=&quot;1890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 CyclicBarrier는 여러 개의 작업을 동시에 처리하는데 유용하게 사용할 수 있습니다. 아래 예시에서는 3개의 스레드가 &quot;안녕하세요!&quot;를 출력한 뒤 무작위로 1초에서 5초 사이의 대기 시간을 갖습니다. 다른 스레드들이 작업을 모두 끝내야 배리어를 통과하는 것에 주목합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1680865166205&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ThreadLocalRandom;

public class SimpleCyclicBarrierExample {

    public static void main(String[] args) {
        int numOfThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numOfThreads, new BarrierAction());

        for (int i = 0; i &amp;lt; numOfThreads; i++) {
            new Thread(new Worker(barrier)).start();
        }
    }

    public static class BarrierAction implements Runnable {
        @Override
        public void run() {
            System.out.println(&quot;모든 스레드가 작업을 완료했습니다!&quot;);
        }
    }

    static class Worker implements Runnable {
        private final CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + &quot;: 안녕하세요!&quot;);

			// 시간이 오래 걸리는 작업을 수행하는 코드
            try {
	            // 시뮬레이션을 위해 무작위로 스레드를 잠시 중지한다.
                int randomDelay = ThreadLocalRandom.current().nextInt(1, 6) * 1000;
                Thread.sleep(randomDelay);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + &quot;이(가) 작업을 완료했습니다.&quot;);

            try {
                barrier.await();
                System.out.println(Thread.currentThread().getName() + &quot;이(가) 배리어를 통과했습니다.&quot;);
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세마포어(Semaphore)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운팅 세마포어(counting semaphore)는 특정 리소스를 동시에 사용하려는 스레드의 수를 제한하고자 할 때 사용합니다. 카운트가 0이 아닌 동안에는 스레드는 세마포어(semaphore)를 획득하고 작업을 진행할 수 있습니다. 스레드가 세마포어를 해제하면 카운트가 증가합니다. 기존에 봤던 동기화 기법들은 크리티컬 섹션에 하나의 스레드만 접근할 수 있었으나, 세마포어를 사용하면 여러 스레드가 크리티컬 섹션에 진입하도록 제한할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680875941560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Semaphore(int permits) {
	sync = new NonfairSync(permits);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세마포어를 사용할 때는 처음에 퍼밋(permits)을 지정해야 하는데, 여기서 퍼밋은 리소스에 대한 동시 접근을 제한하는 데 사용되는 개념입니다. 이름 그대로 일종의 가상 토큰 혹은 허가서라고 이해하면 됩니다. 예를 들어, 퍼밋 값이 1인 세마포어는 동시에 하나의 스레드만 리소스에 접근할 수 있도록 제한하며, 이를 바이너리 세마포어(항상 0과 1의 카운트를 가짐, binary semaphore)라고 부르고 우리가 기존에 봤던 동기화 기법(뮤텍스)과 유사하다고 할 수 있습니다. 만약에 퍼밋 값이 3이라면 동시에 최대 3개의 스레드가 리소스에 접근할 수 있도록 허용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 살펴보기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Semaphore_P01.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj4Ki5/btr8OVCa71d/95dCjiDKXNSOLnlzx02kCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj4Ki5/btr8OVCa71d/95dCjiDKXNSOLnlzx02kCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj4Ki5/btr8OVCa71d/95dCjiDKXNSOLnlzx02kCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj4Ki5%2Fbtr8OVCa71d%2F95dCjiDKXNSOLnlzx02kCK%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;2084&quot; height=&quot;352&quot; data-filename=&quot;Attachments_Semaphore_P01.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해서 예를 살펴봅시다. 예를 들어서, 공용 화장실에 3개의 칸이 있다고 가정해봅시다. 이 경우, 카운팅 세마포어의 퍼밋 값은 3이라고 할 수 있습니다. 이렇게 설정하면, 한 번에 최대 3명의 사람이 화장실을 이용할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Semaphore_P02.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgtxTr/btr8LsOwSjF/g2b1IqpCSDptZLirRBorQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgtxTr/btr8LsOwSjF/g2b1IqpCSDptZLirRBorQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgtxTr/btr8LsOwSjF/g2b1IqpCSDptZLirRBorQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgtxTr%2Fbtr8LsOwSjF%2Fg2b1IqpCSDptZLirRBorQ0%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;2084&quot; height=&quot;484&quot; data-filename=&quot;Attachments_Semaphore_P02.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세마포어의 acquire() 메서드는 화장실의 칸을 사용하려는 사람이 도착했을 때 호출됩니다. 사용 가능한 퍼밋(현재는 3)이 있으면 카운터가 감소하고 사용자는 화장실 칸을 사용할 수 있게 됩니다. 위와 같은 상황에서는 사용자가 화장실(크리티컬 섹션)로 진입해서 남은 퍼밋수가 2로 떨어진 것을 확인하실 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Semaphore_P03.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c78MOo/btr8LhTKuVs/pbMT6P5MKEyScD1hP0NUk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c78MOo/btr8LhTKuVs/pbMT6P5MKEyScD1hP0NUk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c78MOo/btr8LhTKuVs/pbMT6P5MKEyScD1hP0NUk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc78MOo%2Fbtr8LhTKuVs%2FpbMT6P5MKEyScD1hP0NUk1%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;2084&quot; height=&quot;763&quot; data-filename=&quot;Attachments_Semaphore_P03.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 화장실이 가득 차 있으면(카운터가 0이면), 다른 사용자는 누군가 화장실을 나올 때까지 기다려야 합니다. 세마포어의 release() 메서드는 화장실에서 나온 사용자가 호출합니다. 이 메서드가 호출되면 카운터가 증가하고, 기다리고 있는 다른 사용자가 화장실을 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Semaphore_P04.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBzjcS/btr8KIcZzV6/M34E4zPaqiR6XuPBA3kAI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBzjcS/btr8KIcZzV6/M34E4zPaqiR6XuPBA3kAI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBzjcS/btr8KIcZzV6/M34E4zPaqiR6XuPBA3kAI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBzjcS%2Fbtr8KIcZzV6%2FM34E4zPaqiR6XuPBA3kAI0%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;2084&quot; height=&quot;763&quot; data-filename=&quot;Attachments_Semaphore_P04.png&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 비유긴 했지만, 이처럼 카운팅 세마포어는 공유 리소스(혹은 공유 자원, 화장실 칸)의 동시 사용을 제한하고, 동시 사용자 수를 조절할 수 있게 해줍니다. 이를 통해서 화장실 내부에서의 동시 사용자 수를 안전하게 관리할 수 있게 됩니다. 이제 세마포어 클래스 내부를 살펴봅시다. 보통 acquire()와 release() 메서드가 주로 사용됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680876234475&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Semaphore implements java.io.Serializable {
	// 세마포어로부터 퍼밋(permit)을 획득하고, 사용 가능한 퍼밋이 없으면
	// 인터럽트 되거나 퍼밋을 얻을 수 있을 때까지 현재 스레드는 블록된다.
	// 퍼밋을 얻을 수 있으면 퍼밋의 수를 1만큼 감소시킨다.
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

	// 퍼밋을 해제하고 세마포어로 반환한다.
    public void release() {
        sync.releaseShared(1);
    }

	// 호출 시 사용 가능한 퍼밋이 있는 경우에만 퍼밋을 얻는다.
	// 퍼밋이 없는 경우 이 메서드는 즉시 false 값을 반환한다.
    public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) &amp;gt;= 0;
    }

	// 호출 시 사용 가능한 퍼밋이 있는 경우에 퍼밋을 얻는다.
	// 퍼밋이 없는 경우에는 지정한 시간 만큼 퍼밋을 얻기 위해 대기하다가,
	// 얻으면 true를 반환하고 얻지 못하면 false를 반환한다.
    public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

	// 세마포어에서 현재 사용 가능한 퍼밋의 수를 반환한다.
	// 이 메서드는 일반적으로 디버깅이나 테스트 목적으로 사용된다.
    public int availablePermits() {
        return sync.getPermits();
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;소유권(ownership)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 추가로 알아두어야 할 점은 기존에 살펴봤던 뮤텍스(mutex)와는 달리 세마포어에는 공유 리소스에 대한 소유권에 대한 개념이 없기 때문에, 다른 스레드가 락을 해제할 수도 있습니다. synchronized 같은 경우에는 리소스에 대한 소유권을 가진 스레드만이 해당 리소스에 대한 락을 해제할 수 있었습니다. release() 메서드의 자바독을 살펴보면 다음과 같은 문장을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680876271486&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Semaphore implements java.io.Serializable {
	// ...
	// ... 세마포어에서 퍼밋을 해제하려는 스레드가 반드시 acquire() 메서드를 호출하여
	// 해당 퍼밋을 해제할 필요는 없습니다. ...
	public void release() {
        sync.releaseShared(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드에서는 Resource 클래스가 Semaphore를 사용하여 동시에 사용 가능한 리소스의 수를 제한합니다. 여기서는 최대 3개의 스레드만이 동시에 리소스를 사용할 수 있습니다. 메인 메서드에서는 10개의 스레드를 생성하여 리소스를 사용하도록 요청하지만, 세마포어로 인해서 동시에 3개의 스레드만이 리소스를 사용할 수 있습니다. 나머지 스레드들은 사용 가능한 리소스가 생길 때까지 기다리게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680876337032&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static class Resource {
        private final Semaphore semaphore;

        public Resource(int permits) {
            semaphore = new Semaphore(permits);
        }

        public void use(CountDownLatch latch) {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + &quot;가 리소스를 사용하고 있습니다.&quot;);
                Thread.sleep(2000); // 실제 작업을 수행하는 코드
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + &quot;가 리소스를 해제했습니다.&quot;);
                semaphore.release();
                latch.countDown();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Resource resource = new Resource(3);
        CountDownLatch latch = new CountDownLatch(10);

        for (int i = 0; i &amp;lt; 10; i++) {
            Thread thread = new Thread(() -&amp;gt; resource.use(latch), i + &quot;번 스레드&quot;);
            thread.start();
        }

        // 모든 스레드가 종료될 때까지 기다립니다.
        latch.await();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 번외입니다. 아래의 클래스들은 관심이 있으신 분들만 읽어보시고, 다음 편으로 넘어가셔도 무방합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReadWriteLock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 락들은 동시성을 보장할 수는 있었으나, 공유 자원에 대한 접근을 하나의 스레드만 허용했기 때문에 여러 스레드가 동시에 읽기 작업을 수행하는 상황에서는 성능 문제가 일어날 수 있습니다. 그러나 대부분의 경우에는 데이터가 간간이 변경되기는 하지만 상대적으로 읽기 작업이 많이 일어나게 되는데, 이런 상황에서는 락의 조건을 조금 풀어서 읽기 연산은 여러 스레드에서 동시에 실행할 수 있도록 하면 성능이 향상되지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 요구사항에 대응하기 위해 등장한 것이 공유 자원에 대한 동시성 접근을 지원하는 새로운 락인 ReadWriteLock입니다. ReadWriteLock은 읽기 작업은 여러 스레드가 동시에 수행할 수 있지만, 쓰기 작업은 하나의 스레드만 수행할 수 있도록 제어하는 락입니다. 즉, 읽기 작업은 서로 간에 영향을 주지 않으므로 동시에 수행해도 문제가 없지만, 쓰기 작업은 하나의 스레드가 끝날 때까지 다른 스레드는 대기해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReadWriteLock은 ReentrantLock 클래스와 마찬가지로 java.util.concurrent 패키지에 속해 있으며, 아래와 같은 메소드를 제공합니다. 데이터를 변경하는 스레드가 없는 한 여러 스레드가 동시에 읽을 수 있으며, 한 번에 하나의 스레드만 데이터를 변경할 수 있기 때문에 쓰기 작업용 락이 해제될 때까지 다른 스레드(읽기 쓰레드와 쓰기 스레드 모두)는 블록됩니다. 반대로 다른 스레드가 읽는 동안에 쓰기 스레드가 데이터를 변경하려고 하면 쓰기 스레드도 읽기 작업용 락이 풀릴 때까지 블록됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680850406310&quot; class=&quot;cs&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ReadWriteLock {
    // 읽기 작업용 락을 반환한다.
    Lock readLock();

    // 쓰기 작업용 락을 반환한다.
    Lock writeLock();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 읽기 작업용 락이랑 쓰기 작업용 락이 있으니 내부적으로 두 개의 락 객체를 쓰고 있는건가 싶지만 내부적으로는 하나의 ReadWriteLock 객체가 사용됩니다. Lock과 동일하게 보통은 아래와 같이 사용하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680850406318&quot; class=&quot;cs&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
	// 크리티컬 섹션
} finally {
	lock.readLock().unlock();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 사용하는가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReadWriteLock은 캐시나 사전 같이 읽기 작업이 쓰기 작업보다 훨씬 많은 특정 상황에서 병렬 프로그램의 성능을 크게 높일 수 있도록 설계되었습니다(예: 초기에 데이터가 채워지고 이후에 드물게 수정되는 경우). 반대로 읽기 작업이 쓰기 작업보다 다소 적은 경우 혹은 읽기 작업이 너무 짧은 경우에는 읽기-쓰기 잠금 구현의 오버헤드 때문에 synchronized를 사용하는 게 더 간단하고 효율적일 수 있습니다. 실제로 ReadWriteLock이 현재 상황에 적합한지 아닌지는 성능 분석을 통해 판단해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exchanger&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchanger는 두 개의 스레드가 연결되는 배리어(barrier)이며, 배리어 포인트에 도달하면 양쪽의 스레드가 서로 갖고 있던 값을 교환합니다. 이를 통해서 스레드 사이에 안전한 데이터 교환을 보장할 수 있습니다. Exchanger를 사용하는 예시로는 데이터를 처리하는 스레드와 결과를 출력하는 스레드 사이에서 데이터 교환을 할 때 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680949280893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Exchanger&amp;lt;V&amp;gt; {
	// 다른 스레드가 교환 지점(exchange point)에 도달할 때까지 기다리고,
	// 그 후 상대 스레드에게 주어진 객체를 전달하고 상대 스레드가 보내온 객체를 수신한다.
    @SuppressWarnings(&quot;unchecked&quot;)
    public V exchange(V x) throws InterruptedException {
        // ...
    }

	// 위 메서드와 동일하지만 대기하다가 지정한 시간이 지나면,
	// TimeoutException이 발생한다.
    @SuppressWarnings(&quot;unchecked&quot;)
    public V exchange(V x, long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 exchange() 메서드를 호출하면 다른 스레드가 exchange() 메서드를 호출할 때까지 해당 스레드는 블록됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Exchanger_P01.png&quot; data-origin-width=&quot;2333&quot; data-origin-height=&quot;1243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMjCs2/btr8NDCbR36/TcZX18wHfmKyrHt6OcyLsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMjCs2/btr8NDCbR36/TcZX18wHfmKyrHt6OcyLsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMjCs2/btr8NDCbR36/TcZX18wHfmKyrHt6OcyLsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMjCs2%2Fbtr8NDCbR36%2FTcZX18wHfmKyrHt6OcyLsK%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;540&quot; height=&quot;288&quot; data-filename=&quot;Attachments_Exchanger_P01.png&quot; data-origin-width=&quot;2333&quot; data-origin-height=&quot;1243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 스레드가 exchange() 메서드를 호출하면 각 스레드는 상대방의 값과 자신의 값을 교환하고 반환합니다. 이러한 방식으로 두 개의 스레드가 서로 값을 교환할 수 있습니다.&lt;br /&gt;아래 예시에서는 Exchanger를 통해서 두 개의 스레드가 문자열을 교환하는 것을 볼 수 있습니다. 각 스레드에서 exchanger.exchange() 메서드를 호출하면 해당 스레드는 블로킹되어 다른 스레드가 동일한 메서드를 호출할 때까지 대기하게 됩니다. 두 스레드 모두 메서드를 호출하면 서로의 데이터를 교환하고 결과를 출력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680949350135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExchangerExample {
    public static void main(String[] args) {
        Exchanger&amp;lt;String&amp;gt; exchanger = new Exchanger&amp;lt;&amp;gt;();

        Thread threadA = new Thread(new ThreadA(exchanger));
        Thread threadB = new Thread(new ThreadB(exchanger));

        threadA.start();
        threadB.start();
    }

    public static class ThreadA implements Runnable {
        private final Exchanger&amp;lt;String&amp;gt; exchanger;

        public ThreadA(Exchanger&amp;lt;String&amp;gt; exchanger) {
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            String message = &quot;안녕, 스레드 B!&quot;;
            try {
                String exchangedMessage = exchanger.exchange(message);
                System.out.println(&quot;스레드 A가 받음: &quot; + exchangedMessage);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class ThreadB implements Runnable {
        private final Exchanger&amp;lt;String&amp;gt; exchanger;

        public ThreadB(Exchanger&amp;lt;String&amp;gt; exchanger) {
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            String message = &quot;안녕, 스레드 A!&quot;;
            try {
                String exchangedMessage = exchanger.exchange(message);
                System.out.println(&quot;스레드 B가 받음: &quot; + exchangedMessage);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Phaser&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phaser는 다시 사용할 수 있는 동기화 배리어(barrier)로, CyclicBarrier와 CountDownLatch와 비슷한 기능을 하지만 보다 더 유연한 기능을 제공합니다. Phaser는 여러 단계로 나뉘어진 작업을 수행할 때 유용합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680956650567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final int numberOfThreads = 4;
Phaser phaser = new Phaser(numberOfThreads);

public class Phaser {
	// ...
	// phase - 다음 단계(phase)로 진행하기 위해 필요한 파티(스레드)의 수
	public Phaser(int parties) {  
		this(null, parties);  
	}
	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phaser를 사용해서 특정 단계가 완료될 때까지 스레드가 대기하도록 할 수 있으며, 해당 단계가 끝나면 다음 단계로 넘어갈 수 있다는 점에서 CyclicBarrier와 유사합니다. 하지만 Phaser는 CyclicBarrier와 같이 고정된 수의 스레드를 기다리는 게 아니라, Phaser는 동기화에 참여하는 스레드의 수를 동적으로 조절할 수 있다는 점에서 차이가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680956743427&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Phaser {
	// ...
	// 동기화에 참여할 수 있는 스레드의 수를 늘린다.
    public int register() { /* ... */ }

	// 등록된 스레드가 현재 단계를 완료했음을 알리는 데 사용한다.
	// 모든 등록된 스레드가 arrive()를 호출하면 Phaser는 다음 단계로 진행한다.
	// 마치 CyclicBarrier의 await()과 비슷하다고 생각하면 된다.
	public int arrive() { /* ... */ }

	// 현재 스레드가 해당 단계를 완료했음을 알리고,
	// Phaser에서 스레드를 등록 해제한다. 즉, 스레드가 이후의 단계에
	// 참여하지 않게 된다. 이 메서드를 호출하면 Phaser에서 관리하는
	// 스레드의 수가 하나 감소하게 된다.
    public int arriveAndDeregister() { /* ... */ }

	// 이 메서드는 현재 스레드가 해당 단계를 완료했음을 알리고,
	// 다음 단계로 진행되기 전에 모든 등록된 스레드가 해당 단계를 완료할 때까지
	// 기다린다. 즉, 스레드는 다음 단계로 진행하기 전에 다른 스레드들이 현재
	// 단계를 완료할 때까지 블록된다.
    public int arriveAndAwaitAdvance() { /* ... */ }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 arrive() 메서드는 파티(보통 실행 스레드)가 어떤 작업(혹은 그 작업의 일부)을 완료했음을 의미합니다. 하지만 이 메서드는 현재 스레드가 단계가 끝날 때까지 다른 스레드를 기다리지 않습니다. 만약에 현재 단계(phase)를 마무리하고 다른 모든 스레드도 해당 단계를 완료할 때까지 기다리려면 arriveAndAwaitAdvance() 메서드를 호출해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Phaser_P01.png&quot; data-origin-width=&quot;3796&quot; data-origin-height=&quot;1935&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oaMvc/btr8NCwym2N/fUbmylNe13MydhuXTfjFSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oaMvc/btr8NCwym2N/fUbmylNe13MydhuXTfjFSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oaMvc/btr8NCwym2N/fUbmylNe13MydhuXTfjFSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoaMvc%2Fbtr8NCwym2N%2FfUbmylNe13MydhuXTfjFSk%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;3796&quot; height=&quot;1935&quot; data-filename=&quot;Attachments_Phaser_P01.png&quot; data-origin-width=&quot;3796&quot; data-origin-height=&quot;1935&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 이해를 돕기 위해서 아래의 예시 코드를 살펴봅시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1680956974715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PhaserExample {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(1); // 초기 파티 수를 1로 설정 (메인 스레드 포함)
        int numThreads = 3;

        for (int i = 0; i &amp;lt; numThreads; i++) {
            phaser.register(); // 새로운 스레드를 등록
            new Thread(new Task(phaser), (i + 1) + &quot;번 스레드&quot;).start();
        }
        System.out.println(&quot;현재 파티 수: &quot; + phaser.getRegisteredParties());

        // 모든 스레드가 각 단계를 완료할 때까지 메인 스레드가 기다림
        for (int phase = 0; phase &amp;lt; 3; phase++) {
            phaser.arriveAndAwaitAdvance();
            System.out.println((phase + 1) + &quot;번 단계가 완료되었습니다.&quot;);
        }

        System.out.println(&quot;현재 파티 수: &quot; + phaser.getRegisteredParties());
    }

    static class Task implements Runnable {
        private final Phaser phaser;

        Task(Phaser phaser) {
            this.phaser = phaser;
        }

        @Override
        public void run() {
            for (int phase = 0; phase &amp;lt; 3; phase++) {
                System.out.println(Thread.currentThread().getName() + &quot;는 &quot; + (phaser.getPhase() + 1) + &quot;번 단계에서 작업 중입니다...&quot;);
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (phase == 2) {
                    phaser.arriveAndDeregister(); // 마지막 단계에서 스레드를 해제
                } else {
                    phaser.arriveAndAwaitAdvance(); // 다른 스레드가 해당 단계를 완료할 때까지 기다림
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 초기 파티 수는 1로 설정되어 있습니다. 일반적으론 초기에 파티 수를 지정하지만, register() 메서드를 통해서 동기화에 참여할 파티 수를 동적으로 조절할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680957001496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Phaser phaser = new Phaser(1); // 초기 파티 수를 1로 설정 (메인 스레드 포함)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;phaser.register() 메서드를 통해서 새로운 파티(스레드)를 등록한 것을 볼 수 있습니다. 당연히 getRegisteredParties()로 현재 등록된 파티 수를 얻어오면 메인 스레드와 추가된 스레드 3개를 포함해서 4가 나올 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680957023898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int numThreads = 3;

for (int i = 0; i &amp;lt; numThreads; i++) {
	phaser.register(); // 새로운 스레드를 등록
	new Thread(new Task(phaser), (i + 1) + &quot;번 스레드&quot;).start();
}
System.out.println(&quot;현재 파티 수: &quot; + phaser.getRegisteredParties()); // 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 스레드가 단계를 각각 마무리 할 때까지 메인 스레드는 기다리게 됩니다. Phaser 내부에는 단계(phase)를 나타내는 내부 변수가 있으며, 이 값은 0 부터 시작하며 단계가 올라갈수록 이 값도 1씩 증가하게 됩니다. 이는 getPhase() 메서드로 얻어올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680957047995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int phase = 0; phase &amp;lt; 3; phase++) {
	phaser.arriveAndAwaitAdvance();
	System.out.println((phase + 1) + &quot;번 단계가 완료되었습니다.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 크게 주목할 부분은 아래 부분입니다. 0~1 단계에서는 arriveAndAwaitAdvance() 메서드를 호출하여 작업을 일찍 마친 스레드는 다른 스레드를 기다렸다가, 마지막 단계에서 모든 스레드는 도착했음을 알리고 arriveAndDeregister() 메서드를 호출하여 파티에서 빠져나가게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1680957076998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (phase == 2) {
	phaser.arriveAndDeregister(); // 마지막 단계에서 스레드를 해제
} else {
	phaser.arriveAndAwaitAdvance(); // 다른 스레드가 해당 단계를 완료할 때까지 기다림
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 마지막에 현재 파티 수를 찍어보면 4가 아닌 1(메인 스레드)이 찍힌 것을 볼 수가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680957090813&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;현재 파티 수: &quot; + phaser.getRegisteredParties()); // 1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Condition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Condition은 여러 스레드가 동시에 특정 상태를 기다리는 동안, 각각 다른 '기다리는 줄(대기 집합)'에 속할 수 있도록 해주는 도구라고 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 살펴보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서, 극장에서 영화 표를 구매하는 상황에 이를 비유해볼 수 있습니다. 극장에서 각기 다른 영화를 상영하고 있다고 가정해봅시다. 대기열을 하나만 사용한다면, 모든 영화를 보려는 사람들이 한 줄로 서서 기다려야 합니다. 이렇게 되면 영화별로 표를 구매하기 어렵고 비효율적일 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Condition_P01.png&quot; data-origin-width=&quot;1391&quot; data-origin-height=&quot;985&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx0ACG/btr8KLhaauI/9Mribe4Hqy9UgpaPirHgnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx0ACG/btr8KLhaauI/9Mribe4Hqy9UgpaPirHgnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx0ACG/btr8KLhaauI/9Mribe4Hqy9UgpaPirHgnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx0ACG%2Fbtr8KLhaauI%2F9Mribe4Hqy9UgpaPirHgnk%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;420&quot; height=&quot;297&quot; data-filename=&quot;Attachments_Condition_P01.png&quot; data-origin-width=&quot;1391&quot; data-origin-height=&quot;985&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Condition이 도입되면, 각 영화에 대해서 따로 대기열을 만들 수 있게 됩니다. 이렇게 하면 각 영화를 보려는 사람들이 해당 영화의 대기열에 서서 기다리게 할 수 있습니다. 이렇게 여러 대기열이 있을 때, 특정 영화의 표를 사려는 사람들은 그 영화에 대한 대기열에만 집중할 수 있으며, 이를 통해 효율적으로 표를 구매할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Condition_P02.png&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;1572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9v3Iq/btr8LOErNno/j2EVb5ATyhBWjSFpqmKCpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9v3Iq/btr8LOErNno/j2EVb5ATyhBWjSFpqmKCpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9v3Iq/btr8LOErNno/j2EVb5ATyhBWjSFpqmKCpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9v3Iq%2Fbtr8LOErNno%2Fj2EVb5ATyhBWjSFpqmKCpk%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;420&quot; height=&quot;321&quot; data-filename=&quot;Attachments_Condition_P02.png&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;1572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 마찬가지로 Condition은 여러 스레드가 특정 상태를 기다리는 동안 서로 다른 대기 집합(wait-sets)에 속할 수 있도록 하여, 효율적인 상호 작용과 작업 처리를 가능하도록 만들 수 있습니다. 예를 들어서 아래와 같은 예시 코드를 잠깐 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1680963374611&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MovieTheater {
	// Condition 객체는 lock 객체와 함께 사용된다.
    private final Lock lock = new ReentrantLock();
    // 아래 movie1Queue는 스레드들을 하나의 대기 집합으로 묶는 역할을 함
    private final Condition movie1Queue = lock.newCondition();
    // 현재 영화관에 1번 영화에 대한 남은 티켓 수
    private int movie1Tickets = 10;
    // ...
    
	public void buyTicketForMovie1() throws InterruptedException {
        lock.lock();
        try {
	        // 1번 영화의 티켓이 모자르다면
            while (movie1Tickets == 0) {
	            // 티켓이 새로 생길 때까지 무한정 대기한다. (signal)
                movie1Queue.await();
            }
            movie1Tickets--;
            System.out.println(&quot;영화 1 티켓 구매됨. 남은 티켓: &quot; + movie1Tickets);
        } finally {
            lock.unlock();
        }
    }

	// ...
    public void addTicketForMovie1() {
        lock.lock();
        try {
	        // 티켓을 추가하고
            movie1Tickets++;
            // 1번 영화 티켓 구매를 기다리는 사용자를 한 명 깨운다.
            movie1Queue.signal();
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메서드 살펴보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Condition 객체는 Lock 객체와 함께 사용되며, Lock 객체의 newCondition() 메서드를 호출하여 Condition 객체를 생성하게 됩니다. 이렇게 하면 하나의 락 객체에 대한 여러 대기 집합(wait-sets)을 가질 수 있게 되어서, 특정 상황에서 여러 스레드를 독립적으로 깨울 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680963416096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Condition 객체는 await(), signal(), signalAll() 메서드를 제공하며, 각각 대기 상태로 들어가기, 대기 중인 스레드 중 하나를 깨우기, 모든 대기 중인 스레드를 깨우는 역할을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680963430614&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Condition {
	// 신호(signal)가 오거나 인터럽트가 발생할 때까지
	// 현재 스레드를 대기 상태로 만든다.
    void await() throws InterruptedException;

	// 신호(signal)가 오기 전까지 대기 상태로 만든다.
    void awaitUninterruptibly();

	// 신호(signal)가 오거나 인터럽트가 발생하거나 혹은
	// 지정한 대기 시간이 경과할 때까지 현재 스레드를 대기 상태로 만든다.
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;

	// 대기 중인 스레드 중 하나를 깨운다.
    void signal();

	// 대기 중인 모든 스레드를 깨운다.
	// 현재 조건(condition)에 대기 중인 스레드들이 있으면 모두 깨어난다.
    void signalAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한 번 생산자-소비자 문제를 살펴보도록 하겠습니다. 여기에서는 Condition을 사용해서 문제를 해결합니다. 생산자는 물건을 버퍼에 넣고, 소비자는 버퍼에서 물건을 가져옵니다. 버퍼가 가득 찼을 때 생산자는 블록 상태로 전환되고, 버퍼가 비어 있을 때는 소비자가 블록 상태로 전환됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1680963472915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProducerConsumerExample {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(5);
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}

class Buffer {
    private final Queue&amp;lt;Integer&amp;gt; queue;
    private final int maxSize;
    private final Lock lock;
    private final Condition notFull;
    private final Condition notEmpty;

    public Buffer(int maxSize) {
        this.queue = new LinkedList&amp;lt;&amp;gt;();
        this.maxSize = maxSize;
        this.lock = new ReentrantLock();
        this.notFull = lock.newCondition();
        this.notEmpty = lock.newCondition();
    }

    public void put(int item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == maxSize) {
                notFull.await();
            }
            queue.add(item);
            System.out.println(&quot;생산됨: &quot; + item);
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public int get() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            int item = queue.poll();
            System.out.println(&quot;소비됨: &quot; + item);
            notFull.signalAll();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

class Producer implements Runnable {
    private final Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i &amp;lt; 10; i++) {
                buffer.put(i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer implements Runnable {
    private final Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i &amp;lt; 10; i++) {
                buffer.get();
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>java</category>
      <category>동기화</category>
      <category>스레드</category>
      <category>자바</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/377</guid>
      <comments>https://exynoa.tistory.com/377#entry377comment</comments>
      <pubDate>Fri, 7 Apr 2023 19:54:39 +0900</pubDate>
    </item>
    <item>
      <title>프로그래밍 관련 게시글 내용을 최신에 맞춰 업데이트 할 예정입니다.</title>
      <link>https://exynoa.tistory.com/364</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 2012년 쯤에 작성된 게시글들이 많아서 지금 와선 중요도가 떨어져 더 이상 사용되지 않고 사라진 기능을 소개하고 있거나 잘못된 내용을 적어둔 게시글들이 많은 것 같습니다. 따라서 부족한 내용은 더 보충하고 새롭게 소개된 기능들을 본문 내에 업데이트할 예정입니다. 이해가 힘들거나 잘못된 내용을 소개하고 있는 게시글을 댓글에 달아주시면 바로 확인하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;업데이트 예정인 게시글들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 관련 게시글을 모두 업데이트하면 Python, C#, C/C++ 관련 게시글도 업데이트를 할 예정입니다. 링크가 &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;파랗게&lt;/b&gt;&lt;/span&gt;&lt;/u&gt; 칠해진 부분은 최신으로 내용을 갱신한 것이며, &lt;u&gt;&lt;span style=&quot;color: #ee2323; --darkreader-inline-color: #ef3636;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;빨간색&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;으로 칠해진 부분은 추가로 올릴 것들, 아직 아무것도 칠해지지 않은 부분은 갱신이 아직 되지 않은 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 자바(JAVA) [2022년에 업데이트됨]&lt;/h4&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;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/78&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;1편. 자바의 소개&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.hexabrain.net/85&quot;&gt;2편. 개발 환경 구축하기&lt;/a&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/84&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;3편. 프로그램의 구성&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/86&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;4편. 변수와 타입&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/87&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;5편. 주석&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/91&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;6편. 연산자 (1)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/92&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;7편. 연산자 (2)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/95&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;8편. 제어문 (1)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/96&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;9편. 제어문 (2)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/97&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;10편. 반복문 (1)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/102&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;11편. 반복문 (2)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/103&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;12편. 메서드(Method)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/104&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;13편. 객체와 클래스(Objects and Classes)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/370&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;14편. 문자열(String)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/105&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;15편. 생성자(Constructor)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/111&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;16편. 배열(Array)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/112&quot;&gt;&lt;span style=&quot;color: #f89009;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;17편. 배열과 메서드, 다차원 배열&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/116&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;18편. 상속(Inheritance)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/119&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;19편. 제어자(Modifiers)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/120&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;20편. 패키지(Package)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/121&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;21편. 추상 클래스(Abstract Class)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/122&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;22편. 인터페이스(Interface)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/124&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;23편. 콘솔 입출력(Console input and output)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/125&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;24편. 예외 처리(Exception Handling)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.hexabrain.net/378&quot;&gt;&lt;span data-darkreader-inline-color=&quot;&quot;&gt;25편. 중첩 클래스(Nested Class)&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a href=&quot;https://blog.hexabrain.net/379&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;26편. 제네릭(Generic)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/386&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #ef3636;&quot;&gt;&lt;b&gt;27편. 컬렉션(Collections)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/126&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;28편. 스레드(Thread) (1)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/375&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;29편. 스레드(Thread) (2)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/377&quot;&gt;&lt;b&gt;30편. 스레드(Thread) (3)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.hexabrain.net/401&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;31편. 스레드(Thread) (4)&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/382&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;32편. 람다식(Lambda expression)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/127&quot;&gt;&lt;span style=&quot;color: #006dd7; --darkreader-inline-color: #ef3636;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;33편. 파일 입출력(File input and output)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/381&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;34편. 애노테이션(Annotation)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.hexabrain.net/383&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;35편. 스트림(Streams) (1)&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;36편. 스트림(Streams) (2): 병렬 스트림, Fork/Join 프레임워크, Spliterator&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/393&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #ef3636;&quot;&gt;&lt;b&gt;37편. 열거형(Enum Types)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #ef3636;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://blog.hexabrain.net/399&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #ef3636;&quot;&gt;38편. 레코드(Record)&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;39편. Optional&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://exynoa.tistory.com/403&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;번외편. ConcurrentHashMap&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;번외편. 리플렉션(Reflection)&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;번외편.&amp;nbsp;직렬화(Serialization)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;번외편. 봉인 클래스(Sealed Class)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;번외편. 가상 스레드(Virtual Thread)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hexabrain.net/369&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;번외편. JVM의 구조와 동작 원리&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Python&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파이썬 강좌 1편. 시작&lt;/li&gt;
&lt;li&gt;파이썬 강좌 2편. 간단한 문법 살펴보기&lt;/li&gt;
&lt;li&gt;파이썬 강좌 3편. 변수(Variable)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-1편. 수치 자료형(Numeric Data Type)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-2편. 문자열(String)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-3편. 리스트(List)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-4편. 튜플(Tuple)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-5편. 사전(Dictionary)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-6편. 부울(Bool)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 4-7편. 집합(Set)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 5편. 조건문(Condition Statements)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 6편. 반복문(Loop)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 7편. 함수(Function)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 번외편. 재귀 함수&lt;/li&gt;
&lt;li&gt;파이썬 강좌 8-1편. 클래스(Class)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 8-2편. 생성자와 소멸자(Constructor and Destructor)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 8-3편. 상속(Inheritance)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 8-4편. 연산자 오버로딩(Operator Overloading)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 9편. 모듈(Module)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 10-1편. 입출력(I/O)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 10-2편. 파일 입출력(File I/O)&lt;/li&gt;
&lt;li&gt;파이썬 강좌 11편. 예외 처리(Exception Handling)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. C#&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C# 강좌 1편. 시작&lt;/li&gt;
&lt;li&gt;C# 강좌 2편. Hello, world!&lt;/li&gt;
&lt;li&gt;C# 강좌 3편. 변수, 데이터 형식, 상수&lt;/li&gt;
&lt;li&gt;C# 강좌 4편. 연산자(Operators)&lt;/li&gt;
&lt;li&gt;C# 강좌 5편. 조건문(if, else, switch)&lt;/li&gt;
&lt;li&gt;C# 강좌 6편. 반복문(while, do, for, foreach)&lt;/li&gt;
&lt;li&gt;C# 강좌 7편. 무한 루프, 제어문(continue, break, goto)&lt;/li&gt;
&lt;li&gt;C# 강좌 8편. 메소드(Method)&lt;/li&gt;
&lt;li&gt;C# 강좌 9편. 배열(Array)&lt;/li&gt;
&lt;li&gt;C# 강좌 10편. 클래스(Class)&lt;/li&gt;
&lt;li&gt;C# 강좌 11편. 접근 제한자(Access Modifier), this&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;http://blog.hexabrain.net/141&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;b&gt;12편. 생성자(Constructors)&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;C# 강좌 13편. 클래스의 상속(Class inheritance)&lt;/li&gt;
&lt;li&gt;C# 강좌 14편. 확장 메소드, 분할 클래스, 중첩 클래스&lt;/li&gt;
&lt;li&gt;C# 강좌 15편. 구조체(Structures)&lt;/li&gt;
&lt;li&gt;C# 강좌 16편. 인터페이스(Interface)&lt;/li&gt;
&lt;li&gt;C# 강좌 17편. 예외 처리(Exception handling)&lt;/li&gt;
&lt;li&gt;C# 강좌 18편. 컬렉션(Collection)&lt;/li&gt;
&lt;li&gt;C# 강좌 19편. 델리게이트와 이벤트(Delegates and Events)&lt;/li&gt;
&lt;li&gt;C# 강좌 20편. 리플렉션과 애트리뷰트(Reflection and attributes)&lt;/li&gt;
&lt;li&gt;C# 강좌 21편. 프로퍼티(Property)&lt;/li&gt;
&lt;li&gt;C# 강좌 22편. 파일 입출력(File Input/Output)&lt;/li&gt;
&lt;li&gt;C# 고급 1편. 레지스트리(Registry)&lt;/li&gt;
&lt;li&gt;C# 고급 2편. 링크(LINQ)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. C++&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++ 강좌 1편. 새로운 시작, 컴파일러 소개&lt;/li&gt;
&lt;li&gt;C++ 강좌 2편. 기본 입출력 함수 둘러보기&lt;/li&gt;
&lt;li&gt;C++ 강좌 3편. 네임스페이스(namespace)&lt;/li&gt;
&lt;li&gt;C++ 강좌 4편. 함수 오버로딩(Function Overloading)&lt;/li&gt;
&lt;li&gt;C++ 강좌 5편. new, delete&lt;/li&gt;
&lt;li&gt;C++ 강좌 6편. 구조체의 확장&lt;/li&gt;
&lt;li&gt;C++ 강좌 7편. 클래스(class)&lt;/li&gt;
&lt;li&gt;C++ 강좌 8편. 생성자와 소멸자(Constructor and Destructor)&lt;/li&gt;
&lt;li&gt;C++ 강좌 9편. Bool, Inline&lt;/li&gt;
&lt;li&gt;C++ 강좌 10편. 참조자(Reference)&lt;/li&gt;
&lt;li&gt;C++ 강좌 11편. 프렌드(friend)&lt;/li&gt;
&lt;li&gt;C++ 강좌 12편. 상속(Inheritance)&lt;/li&gt;
&lt;li&gt;C++ 강좌 13편. 객체 배열과 객체 포인터 배열, this 포인터&lt;/li&gt;
&lt;li&gt;C++ 강좌 14편. 상속 오버라이딩과 가상 함수, 그리고 다중 상속&lt;/li&gt;
&lt;li&gt;C++ 강좌 15편. 연산자 오버로딩(Operator Overloding)&lt;/li&gt;
&lt;li&gt;C++ 강좌 16편. 템플릿(Template)&lt;/li&gt;
&lt;li&gt;C++ 강좌 17편. 예외 처리(Exception Handling)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>잡담</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/364</guid>
      <comments>https://exynoa.tistory.com/364#entry364comment</comments>
      <pubDate>Wed, 1 Jun 2022 04:28:11 +0900</pubDate>
    </item>
    <item>
      <title>invokedynamic의 내부 동작</title>
      <link>https://exynoa.tistory.com/400</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 게시글은 자바 8을 기준으로 작성되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Invokedynamic&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 코드를 떠올려봅시다. 람다의 타입은 무엇일까요? 람다는 int, double 같은 기본 타입이 아니므로 참조 타입, 즉 객체의 참조여야 합니다. 다시 말해서, Runnable을 구현하는 클래스의 인스턴스에 대한 참조여야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653199984209&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.company;

public class InvokeDynamicExample {
    public static void main(String [] args) {
        Runnable r = () -&amp;gt; System.out.println(&quot;Hello&quot;);
        r.run();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 javap로 뜯어보면 아래와 같은 바이트코드를 살펴볼 수 있습니다. 여기서는 명령 5~6에서 스택에 푸시된 람다 인스턴스의 참조를 지역 변수 1(즉,r)에 저장하는 것을 볼 수 있습니다. 이어서 명령 7에서 r.run()이 호출되는 것을 볼 수 있습니다. 관심사는 invokeinterface가 아니므로 invokedynamic 위주로 집중적으로 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200058868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(java.lang.String[]);
	// InvokeDynamic #0:run:()Ljava/lang/Runnable;
	0: invokedynamic #2
	5: astore_1
	6: aload_1
	// // InterfaceMethod java/lang/Runnable.run:()V
	7: invokeinterface #3, 1
	12: return&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JLS&lt;/a&gt;에서 확인할 수 있듯이 #20의 #0은 부트스트랩 메서드 테이블에 있는 부트스트랩 메서드(bootstrap_methods) 배열의 인덱스입니다. #30에는 메서드명과 메서드 디스크립터가 옵니다. 참고로 메서드 디스크립터는 매개변수 타입과 메서드의 반환 타입을 하나의 문자열로 나타낸 것입니다. 주석을 보면 쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Invokedynamic_P04.png&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLUnqX/btrCMYxPOd7/B02Y4vWlK2NfYmIPFPncYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLUnqX/btrCMYxPOd7/B02Y4vWlK2NfYmIPFPncYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLUnqX/btrCMYxPOd7/B02Y4vWlK2NfYmIPFPncYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLUnqX%2FbtrCMYxPOd7%2FB02Y4vWlK2NfYmIPFPncYk%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;1047&quot; height=&quot;659&quot; data-filename=&quot;Attachments_Invokedynamic_P04.png&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 내려보면 아래와 같은 내용을 확인할 수 있습니다. 이 부트스트랩 메서드는 동적으로 대상 메서드(target method)를 연결하는 invokedynamic의 핵심 부분입니다. 여기서 java.lang.invoke.LambdaMetafactory#metaFactory()가 어떤 메서드인지 살펴보기 전에 MethodHandle를 먼저 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200378776&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;InnerClasses:
  // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
  public static final #57= #56 of #60;
BootstrapMethods:
  // java.lang.invoke.LambdaMetafactory.metafactory()가 바로 부트스트랩 메서드이다.
  0: #27 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
  (Ljava/lang/invoke/MethodHandles$Lookup // caller
     ;Ljava/lang/String // invokedName
     ;Ljava/lang/invoke/MethodType // invokedType
     ;Ljava/lang/invoke/MethodType // samMethodType
     ;Ljava/lang/invoke/MethodHandle // implMethod
     ;Ljava/lang/invoke/MethodType // instantiatedMethodType
  ;)Ljava/lang/invoke/CallSite; // 반환 타입이 CallSite
    // 각각 samMethodType, implMethod, instantiatedMethodType으로 넘어간다.
    Method arguments:
      #28 ()V
      #29 REF_invokeStatic com/company/InvokeDynamicExample.lambda$main$0:()V
      #28 ()V&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MethodHandle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandle.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자바독&lt;/a&gt;을 살펴보면 &quot;메서드 핸들은 내재된 메서드, 생성자, 필드 혹은 이와 유사한 저수준의 연산에 대한 타입화된(typed) 참조이다.&quot;라고 나와 있습니다. 그리고 &quot;이를 직접 실행할 수도 있다&quot;는 내용도 찾아볼 수 있습니다. 다시 말해서 메서드, 생성자, 필드 등을 가리킬 수 있으며 직접 실행할 수도 있는 참조를 MethodHandle 이라는 타입으로 만든 것입니다. 그러면 이 메서드 핸들을 어떻게 가져올 수 있을까요? &lt;a href=&quot;https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자바 매거진&lt;/a&gt;에서 다음과 같은 내용을 확인할 수 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;... 메서드 핸들을 가져오려면 룩업 컨텍스트(lookup context)를 통해서 조회해야 합니다. 컨텍스트를 가져오는 일반적인 방법은 정적 헬퍼 메서드인 MethodHandles.lookup()을 호출하는 것입니다. 이 메서드는 현재 실행 중인 메서드를 기반으로 룩업 컨텍스트를 반환합니다. 이 컨텍스트에서 find*() 메서드 중 하나를 호출해서 메서드 핸들을 얻을 수 있습니다(예: findVirtual(), findConstructor()). ...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 메서드 핸들을 이용해서 정적 메서드인 String.format(String, Object...)를 호출해보도록 하겠습니다. 우선 룩업으로 메서드를 조회하고, String.format()의 디스크립터, 즉 반환 타입과 매개변수 타입을 순서대로 작성합니다. 이러한 타입 정보는 다음 행에서 String 클래스 내의 format() 메서드를 찾을 때 사용됩니다. 그 후에는 invokeExact()로 넘겨준 타입과 클래스, 메서드명과 정확히 일치하는 정적 메서드를 호출하여 결과를 돌려주게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200727687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class InvokeDynamicExample {
    public static void main(String [] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // String.format(String format, Object... args)
        MethodType type = MethodType.methodType(String.class, String.class, Object[].class);
        MethodHandle mh = lookup.findStatic(String.class, &quot;format&quot;, type);

		// String.format(&quot;Hello, %s!&quot;, &quot;World&quot;);
        String s = (String) mh.invokeExact(&quot;Hello, %s!&quot;, new Object[]{&quot;World&quot;});
        System.out.println(s); // Hello, World!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CallSite&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 이어서 CallSite를 살펴봅시다. 보다시피 CallSite는 MethodHandle을 담아두는 홀더 역할을 합니다. 이 메서드 핸들을 매개변수로 받는 생성자도 보입니다. 참고로, 여기서 타겟은 우리가 실행하길 원하는 대상 메서드(target method)를 말하는 것이며, 아직은 무엇인지 알 수 없으나 우리가 필요한 람다의 인스턴스를 반환하는 메서드가 될 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200757657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package java.lang.invoke;

abstract public class CallSite {
    static { MethodHandleImpl.initStatics(); }

	// 주의: JVM이 이 필드를 알고 있습니다. 변경하지 마세요.
    MethodHandle target;

	...

    CallSite(MethodHandle target) {
        target.type();  // 널 체크
        this.target = target;
    }

	public abstract MethodHandle getTarget();
	public abstract void setTarget(MethodHandle newTarget);
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 CallSite의 자바독을 살펴보면 다음과 같은 내용을 알 수 있습니다. 여기서 CallSite를 호출하면 MethodHandle을 통해서 이를 우리가 원하는 타겟 메서드에 위임합니다. 여기서 상수 콜사이트는 CallSite의 자식 클래스인 ConstantCallSite를 말하고, 이 클래스는 콜사이트에 연결된 타겟이 변경될 수 없고 영구적일 때 사용됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;CallSite에 연결된 invokedynamic 명령은 모든 호출을 사이트의 현재 타겟에 위임합니다. CallSite는 여러 개의 invokedynamic 명령과 연결될 수 있으며, 아무 명령과도 연결되지 않을 수도 있습니다. 어떤 경우건, &quot;동적 호출기(dynamic invoker)&quot;라고 불리는 연결된 메서드 핸들을 통해 호출될 수 있습니다. ... 가변 타겟이 필요하지 않은 경우(타겟 메서드가 변하지 않는 경우), invokedynamic 명령은 상수 콜사이트(constant call site)를 통해 영구적으로 바인딩될 수 있습니다. ...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 CallSite 추상 클래스 내부에 아래와 같이 콜사이트를 만드는 정적 메서드를 찾아볼 수 있습니다. 우리가 MethodHandle 예시에서 봤던 것처럼 호출자 정보를 가지고 룩업을 수행하며, 그 후 MethodHandle.invoke()에 룩업 컨텍스트와 메서드 이름, 메서드 디스크립터를 넘기고 이를 호출하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200822007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static CallSite makeSite(MethodHandle bootstrapMethod,  
						 // 피호출자 정보:  
						 String name, MethodType type,  
						 // (있는 경우) BSM을 위한 추가 인수  
						 Object info,  
						 // 호출자 정보:  
						 Class&amp;lt;?&amp;gt; callerClass) {  
	MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
	CallSite site;  
	try {
		...
		// 네이티브 메서드인 MethodHandle.invoke()는 invokeExact()와 거의 유사하게 동작한다. 다만 invokeExact()처럼 타입 검사가 엄격하지는 않다. invoke()는 타입이 정확하게 일치하지 않으면 필요한 타입으로 변환을 수행한다.
		if (info == null) {  
			binding = bootstrapMethod.invoke(caller, name, type);  
		} else if (!info.getClass().isArray()) {  
			binding = bootstrapMethod.invoke(caller, name, type, info);  
		} else {
			Object[] argv = (Object[]) info;
			...
			switch (argv.length) {
			// 부트스트랩 메서드를 호출한다.
			case 0:  
				binding = bootstrapMethod.invoke(caller, name, type);  
				break;  
			case 1:  
				binding = bootstrapMethod.invoke(caller, name, type,  
												 argv[0]);  
				break;
			...
		}
		if (binding instanceof CallSite) {  
		    site = (CallSite) binding;  
		}  else {  
		    throw new ClassCastException(&quot;bootstrap method failed to produce a CallSite&quot;);  
		}
		...
	} catch (Throwable ex) { ... }
	return site;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 bootstrapMethod는 부트스트랩 메서드에 대한 핸들입니다. 여기서 JVM은 LambdaMetafactory.metafactory(...)라고 하는 정적 부트스트랩 메서드를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200847872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BootstrapMethods:
  0: #27 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
  (Ljava/lang/invoke/MethodHandles$Lookup // caller
     ;Ljava/lang/String // invokedName
     ;Ljava/lang/invoke/MethodType // invokedType
     ;Ljava/lang/invoke/MethodType // samMethodType
     ;Ljava/lang/invoke/MethodHandle // implMethod
     ;Ljava/lang/invoke/MethodType // instantiatedMethodType
  ;)Ljava/lang/invoke/CallSite; // 반환 타입이 CallSite
    Method arguments:
      #28 ()V
      #29 REF_invokeStatic com/company/InvokeDynamicExample.lambda$main$0:()V
      #28 ()V&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LambdaMetafactory&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 java.lang.invoke.LambdaMetafactory 클래스에 있는 부트스트랩 메서드입니다. 여기서 마지막에 mf.buildCallSite()가 호출되면 내부에서 함수형 인터페이스를 구현하는 클래스가 동적으로 만들어지며, 최종적으로 타겟 메서드의 핸들이 들어간 CallSite를 반환합니다. 이렇게 반환된 CallSite(구체적으로는 ConstantCallSite)는 invokedynamic 명령과 연결되어 사용됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200883976&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이 예시에선 invokedName은 run이고, invokedType은 MethodType(Runnable.class)이다.
// 즉, invokedName은 함수형 인터페이스의 메서드 이름이고, invokedType은 함수형 인터페이스 타입이다.
public static CallSite metafactory(MethodHandles.Lookup caller,
								   String invokedName,
								   MethodType invokedType,
								   MethodType samMethodType,
								   MethodHandle implMethod,
								   MethodType instantiatedMethodType)
		throws LambdaConversionException {
	AbstractValidatingLambdaMetafactory mf;
	mf = new InnerClassLambdaMetafactory(caller, invokedType,
		 invokedName, samMethodType,
		 implMethod, instantiatedMethodType,
		 false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
	// metafactory 인수에 오류가 있는지 검증한다.
	mf.validateMetafactoryArgs();
	return mf.buildCallSite();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 세개를 먼저 살펴보면 samMethodType은 함수 객체에 구현될 메서드의 시그니처와 반환 타입입니다. 즉 함수형 인터페이스 Runnable에 있는 메서드 void run()이므로 ()V, 즉 MethodType(void.class)가 됩니다. 이는 아래의 부트스트랩 메서드 테이블의 메서드 인수에서도 확인했었습니다. 만약에 타겟 메서드가 제네릭 메서드라고 하면 samMethodType에는 타입이 소거된 후의 타입이 들어가는 반면에, instantiatedMethodType에는 실제 타입이 들어갑니다. void run()은 제네릭 메서드가 아니므로 samMethodType과 동일한 ()V, 즉 MethodType(void.class)가 됩니다. implMethod는 아래의 #29를 보면 짐작이 갈 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653200928347&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    Method arguments:
      #28 ()V
      #29 REF_invokeStatic com/company/InvokeDynamicExample.lambda$main$0:()V
      #28 ()V&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 lambda$main$0은 InvokeDynamicExample 클래스에 생성된 private static 메서드입니다. 내용을 보면 System.out.println(&quot;Hello&quot;)이고 이는 람다 본문에서 사용됨을 쉽게 알 수 있습니다. 여기서 implMethod은 람다 인스턴스에서 호출되어야 하는 구현 메서드임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_InvokeDynamic_P05.png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mtirb/btrCKV9H9tx/dhq5nDVomg26KbKF3quqZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mtirb/btrCKV9H9tx/dhq5nDVomg26KbKF3quqZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mtirb/btrCKV9H9tx/dhq5nDVomg26KbKF3quqZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmtirb%2FbtrCKV9H9tx%2Fdhq5nDVomg26KbKF3quqZ0%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;691&quot; height=&quot;233&quot; data-filename=&quot;Attachments_InvokeDynamic_P05.png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;InnerClassLambdaMetafactory&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 동적으로 클래스를 생성하고 정의하는 java.lang.invoke.InnerClassLambdaMetafactory 클래스를 잠깐만 살펴봅시다. 그 전에 이 클래스의 정적 초기화 블록을 살펴보면 아래와 같은 코드를 볼 수 있으며 함수형 인터페이스를 구현하는 클래스를 동적으로 생성하는 spinInnerClass()에서 만들어진 바이트 배열을 디스크로 덤프하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653205493366&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 생성된 클래스를 디스크에 덤프하거나 디버깅하기 위함
private static final ProxyClassesDumper dumper;

static {
	final String key = &quot;jdk.internal.lambda.dumpProxyClasses&quot;;
	String path = AccessController.doPrivileged(
			new GetPropertyAction(key), null,
			new PropertyPermission(key , &quot;read&quot;));
	// 경로가 비어있으면 따로 덤프를 뜨지는 않는다.
	dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
}

private Class&amp;lt;?&amp;gt; spinInnerClass() throws LambdaConversionException {
	...
	// 이 VM에서 생성된 클래스를 정의한다.
	final byte[] classBytes = cw.toByteArray();

	// 요청 시 디버깅을 위해 파일로 덤프
	if (dumper != null) {
		AccessController.doPrivileged(new PrivilegedAction&amp;lt;Void&amp;gt;() {
			@Override
			public Void run() {
				dumper.dumpClass(lambdaClassName, classBytes);
				return null;
			}
		}, null,
		new FilePermission(&quot;&amp;lt;&amp;lt;ALL FILES&amp;gt;&amp;gt;&quot;, &quot;read, write&quot;),
		new PropertyPermission(&quot;user.dir&quot;, &quot;read&quot;));
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 jdk.internal.lambda.dumpProxyClasses 프로퍼티에 적절한 경로를 지정하고 실행시켜봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Invokedynamic_P07.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oRVIs/btrCKhjNnKa/W2sJh2EGAU26QhNwJSfuO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oRVIs/btrCKhjNnKa/W2sJh2EGAU26QhNwJSfuO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oRVIs/btrCKhjNnKa/W2sJh2EGAU26QhNwJSfuO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoRVIs%2FbtrCKhjNnKa%2FW2sJh2EGAU26QhNwJSfuO0%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;1414&quot; height=&quot;469&quot; data-filename=&quot;Attachments_Invokedynamic_P07.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 뒤에 프로그램을 실행하면 해당 경로에 덤프 파일이 만들어진 것을 볼 수 있습니다. 덤프된 클래스의 내부를 보면 final로 선언되어 있고 Runnable 인터페이스를 구현하는 것을 볼 수 있습니다. run() 메서드를 보면 InvokeDynamicExample.lambda$main$0이 여기서 호출된다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653205566630&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.company;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class InvokeDynamicExample$$Lambda$1 implements Runnable {
	// private 생성자지만 spinInnerClass()에서 리플렉션 API를 통해 생성자로의 접근 검사를 막아 인스턴스를 생성한다.
    private InvokeDynamicExample$$Lambda$1() {
    }

    @Hidden
    public void run() {
        InvokeDynamicExample.lambda$main$0();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 InnerClassLambdaMetafactory의 buildCallSite() 메서드 내부를 보면 매개변수의 수에 따라서 분기하는 것이 보입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653209037313&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 그저 이미 만들어진 인스턴스를 반환하는 메서드 혹은
// 정적 팩토리 메서드에 대한 메서드 핸들이 콜사이트로 들어간다.
@Override
CallSite buildCallSite() throws LambdaConversionException {
	// 클래스를 동적으로 생성하고 정의하고 반환하는 부분
	final Class&amp;lt;?&amp;gt; innerClass = spinInnerClass();
	// 매개변수가 없으면 (람다에서 참조하는 외부 변수가 없으면)
	if (invokedType.parameterCount() == 0) {
		final Constructor&amp;lt;?&amp;gt;[] ctrs = AccessController.doPrivileged(
				new PrivilegedAction&amp;lt;Constructor&amp;lt;?&amp;gt;[]&amp;gt;() {
			@Override
			public Constructor&amp;lt;?&amp;gt;[] run() {
				Constructor&amp;lt;?&amp;gt;[] ctrs = innerClass.getDeclaredConstructors();
				if (ctrs.length == 1) {
					// 내부 클래스 생성자를 구현하는 람다는 private이므로
					// 변함없는 단일 인스턴스를 만들기 위해 생성자에 접근할 수 있도록 리플렉션을 통해 자바의 접근 검사를 막는다.
					ctrs[0].setAccessible(true);
				}
				return ctrs;
			}
				});
		...
		try {
			// 여기서 만든 인스턴스를 반환하는 메서드 핸들이 콜사이트로 넘어간다.
			Object inst = ctrs[0].newInstance();
			return new ConstantCallSite(MethodHandles.constant(samBase, inst));
		}
		catch (ReflectiveOperationException e) { ...}
	} else { // 매개변수가 있으면
		try {
			UNSAFE.ensureClassInitialized(innerClass);
			// 정적 팩토리 메서드에 대한 메서드 핸들이 콜사이트로 넘어간다.
			return new ConstantCallSite(
					MethodHandles.Lookup.IMPL_LOOKUP
						 .findStatic(innerClass, NAME_FACTORY, invokedType));
		}
		catch (ReflectiveOperationException e) { ... }
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 매개변수의 수는 생성된 클래스의 생성자로 넘어가는 매개변수를 말합니다. 보통 람다 내부에서 선언된 변수만 사용한다면 괜찮지만, 람다에서 외부 변수를 사용하는 경우에는 그 변수를 캡처해야 합니다. 보통 시시각각 변하는 화면을 캡처해서 정적인 화면을 얻을 수 있듯이, 람다(정확히는 클로저)도 자신을 둘러싸는 주변 환경을 캡처하여 람다가 사용하고 있는 외부 변수의 복사본을 만들어냅니다. 이는 람다 객체의 생존 범위와 지역 변수의 생존 범위가 다르기 때문이며, 사용 중이던 지역 변수가 자신을 둘러싸는 블록을 벗어나 소멸한다면 람다 내부에서 사용할 수 없게 되기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653209068146&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int localVar = 3;

Function&amp;lt;Integer, Integer&amp;gt; f = (x) -&amp;gt; (int) (localVar * x);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시로부터 생성되는 클래스는 다음과 같습니다. 지역 변수의 복사본을 만들기 위해서 생성자에 정수 값을 넘겨받는 부분이 보입니다. 또한 람다 내부에서 캡처한 값, 즉 복사본 arg$1을 수정할 수는 없습니다. 내부에서 final로 선언되었기 때문입니다. get$Lambda는 보다시피 buildCallSite()에서 사용되는 정적 팩토리 메서드고 CallSite 내의 MethodHandle과 연결되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1653209099682&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 컴파일러가 생성한 클래스 (InvokeDynamicExample$$Lambda$1.class)
// 생성된 클래스명은 룩업을 수행한 클래스의 이름 뒤에 ''$$Lambda$숫자'가 붙는다.
final class InvokeDynamicExample$$Lambda$1 implements Function {
    private final int arg$1;

    private InvokeDynamicExample$$Lambda$1(int var1) {
        this.arg$1 = var1;
    }

	// 이 정적 팩토리 메서드는 InnerClassLambdaMetafactory#spinInnerClass()에서 호출되는 InnerClassLambdaMetafactory#generateFactory()에서 만들어지며 이름은 NAME_FACTORY 상수에서 확인할 수 있다.
    private static Function get$Lambda(int var0) {
        return new InvokeDynamicExample$$Lambda$1(var0);
    }

    @Hidden
    public Object apply(Object var1) {
        return InvokeDynamicExample.lambda$main$0(this.arg$1, (Integer)var1);
    }
}

public class InvokeDynamicExample {
	public static void main(String [] args) throws Throwable {  
	    int localVar = 3;  

		// Function&amp;lt;Integer, Integer&amp;gt; f = InvokeDynamicExample$$Lambda$1.get$Lambda(localVar);와 비슷
	    Function&amp;lt;Integer, Integer&amp;gt; f = (x) -&amp;gt; (int) (localVar * x);
	    System.out.println(f.apply(5));
	}
	
    // 컴파일러가 생성한 메서드
	private static Integer lambda$main$0(int arg$1, Integer var1) {
	    return Integer.valueOf(arg$1 * var1.intValue());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 런타임에 CallSite는 부트스트랩 메서드를 통해서 타겟 메서드와 동적으로 연결되는 것을 확인할 수 있었습니다. 세부적으론 다양한 최적화가 들어갈 수 있으나 대략적인 과정을 간단하게 정리해보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Invokedynamic_P06 (2) copy.png&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNoJF3/btrCRQr6prd/Lfma0sVZ75qjPCGYKnUXN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNoJF3/btrCRQr6prd/Lfma0sVZ75qjPCGYKnUXN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNoJF3/btrCRQr6prd/Lfma0sVZ75qjPCGYKnUXN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNoJF3%2FbtrCRQr6prd%2FLfma0sVZ75qjPCGYKnUXN0%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;1065&quot; height=&quot;458&quot; data-filename=&quot;Attachments_Invokedynamic_P06 (2) copy.png&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 같은 invokedynamic 명령을 만날 때마다 위와 같은 과정을 반복해야 할까요? 물론 아닙니다. JVM은 캐싱을 통해서 해당 invokedynamic 명령을 볼 때마다 부트스트랩 메서드를 호출하지 않고 바로 실제로 호출될 메서드, 즉 타겟 메서드를 호출할 수 있습니다. 여기서 변경 사항이 없는 한 JVM은 계속해서 처음에 거쳤던 단계를 건너뛰게 됩니다. 아래의 예시의 출력 결과를 참고해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1653216670331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class InvokeDynamicExample {
    private static void hello() {
        Runnable r = () -&amp;gt; System.out.println(&quot;Hello&quot;);
        // (1): ...InvokeDynamicExample$$Lambda$1/1096979270@682
        System.out.println(&quot;(1): &quot; + r);
        r.run();

		// (2): ...InvokeDynamicExample$$Lambda$2/1023892928@214
        r = () -&amp;gt; System.out.println(&quot;Hello&quot;);
        System.out.println(&quot;(2): &quot; + r);
        r.run();
    }

    public static void main(String [] args) throws Throwable {
        System.out.println(&quot;첫 번째 hello() 호출: &quot;);
        hello();
        System.out.println();

        System.out.println(&quot;두 번째 hello() 호출:&quot;);
        hello();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Ben Evans&lt;/span&gt;, Understanding Java method invocation with invokedynamic&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>invokedynamic</category>
      <category>java</category>
      <category>JVM</category>
      <category>바이트코드</category>
      <category>자바</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/400</guid>
      <comments>https://exynoa.tistory.com/400#entry400comment</comments>
      <pubDate>Sun, 22 May 2022 15:21:40 +0900</pubDate>
    </item>
    <item>
      <title>38편. 레코드(Record)</title>
      <link>https://exynoa.tistory.com/399</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.net.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNRuJ6/btrCBpKamlA/seauLLdqUqxIKC8oOG5AK0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNRuJ6/btrCBpKamlA/seauLLdqUqxIKC8oOG5AK0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNRuJ6/btrCBpKamlA/seauLLdqUqxIKC8oOG5AK0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNRuJ6%2FbtrCBpKamlA%2FseauLLdqUqxIKC8oOG5AK0%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;486&quot; height=&quot;304&quot; data-filename=&quot;img1.daumcdn.net.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 데이터를 한쪽에서 다른 한쪽으로 전달하기 위해서만 사용되는 데이터 전송 객체(혹은 DTO)를 생각해봅시다. 이런 객체를 사용하는 이유는 다양한 집계 연산을 수행한 후의 결과를 담아두거나, 외부 시스템과 통신 시에 필요하지 않은 데이터를 제거하여 대역폭 사용량을 줄이기 위해, 세부 구현을 노출시키지 않기 위해서, 혹은 변경되지 말아야 하는 API 설계 상의 이유 등 다양한 이유가 있을 수 있습니다. 이를 제대로 구현하기 위해서는 (롬복이나 IDE의 도움을 받을 수도 있지만) 아래와 같이 게터(getter 혹은 accessor), equals(), hashCode(), toString() 처럼 계속 똑같은 구조의 코드를 반복해서 작성해야 했습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BookDto {  
    private String title;  
    private String author;  
    private String isbn;  
    private String publisher;  

    public String getTitle() {  
        return title;  
    }  

    public String getAuthor() {  
        return author;  
    }
    // ... 수많은 코드 ...
    @Override  
    public int hashCode() {  
        return Objects.hash(title, author, isbn, publisher);  
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이렇게 수십 줄의 지루한 코드를 계속해서 적거나 귀찮아서 일부 메서드를 생략하거나 대충 구현하는 것 대신에, 여기서 소개할 레코드를 사용하면 이러한 작업을 아래 코드 한 줄로 줄일 수 있습니다다. 정말 간단하지 않나요?&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record BookDto(String title, String author, String isbn, String publisher) { }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 14 이후부터 레코드(Record)라는 구문이 프리뷰 기능으로 추가되었고, 자바&amp;nbsp;16부터는&amp;nbsp;공식&amp;nbsp;기능이&amp;nbsp;되었습니다. 열거형과 마찬가지로 자바 클래스의 특별한 한 종류라고 바라볼 수 있습니다. 마치 클래스의 생성자와 같이 매개변수를 나열하면 되는데, 여기서는 이를 컴포넌트(component)라고 부릅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;record 레코드명(컴포넌트1, 컴포넌트2, ...) { }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스와의 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 레코드는 클래스의 특별한 한 종류라고 했습니다. 그러면 클래스와 비교했을 때 어떤 부분이 다른 걸까요? &lt;a href=&quot;https://openjdk.java.net/jeps/359&quot;&gt;JEP 359&lt;/a&gt;에서 확인할 수 있는 내용은 다음과 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드는 다른 클래스를 상속받을 수 없다.&lt;/li&gt;
&lt;li&gt;레코드에는 인스턴스 필드를 선언할 수 없다. 다르게 말하면 정적 필드는 가능하다.&lt;/li&gt;
&lt;li&gt;레코드를 abstract로 선언할 수 없으며 암시적으로 final로 선언된다.&lt;/li&gt;
&lt;li&gt;레코드의 컴포넌트는 암시적으로 final로 선언된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 클래스와 비슷한 점을 나열하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 내에서 레코드를 선언할 수 있다. 중첩된 레코드는 암시적으로 static으로 선언된다.&lt;/li&gt;
&lt;li&gt;제네릭 레코드를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;레코드는 클래스처럼 인터페이스를 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;new 키워드를 사용하여 레코드를 인스턴스화할 수 있다.&lt;/li&gt;
&lt;li&gt;레코드의 본문(body)에는 정적 필드, 정적 메서드, 정적 이니셜라이저, 생성자, 인스턴스 메서드, 중첩 타입(클래스, 인터페이스, 열거형 등)을 선언할 수 있다.&lt;/li&gt;
&lt;li&gt;레코드나 레코드의 각 컴포넌트에 애노테이션을 달 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 레코드를 클래스로 바꿔보면 대략 아래와 같은 모습일 것입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 레코드
public record Book(String title, String author, String isbn) { }

// 클래스
// 암시적으로 추상 클래스인 java.lang.Record를 상속받는다.
public final class Book extends java.lang.Record {
    // 레코드의 각 컴포넌트는 내부에서 private final인 인스턴스 필드로 선언된다.
    private final String title;
    private final String author;
    private final String isbn;

    // 레코드 내부에서 표준 생성자(canonical constructor)가 만들어진다.
    // 암시적으로 선언된 표준 생성자의 접근 제어자는 레코드의 접근 제어자와 동일하다.
    public Book(String title, String author, String isbn) {
        super();
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    // 기본 구현 toString(), hashCode(), equals()은 원하면 변경할 수 있다.
    @Override
    public final String toString() {
        // 내부 구현의 정확한 문자열 포맷은 향후 변경될 수도 있다.
        return &quot;Book[&quot; + this.title + &quot;, &quot; + this.author + &quot;, &quot; + this.isbn + &quot;]&quot;;
    }

    // 암시적 구현은 동일한 컴포넌트로부터 생성된 두 레코드는 해시 코드가 동일해야 한다.
    @Override
    public final int hashCode() {
        // 구현에 사용되는 정확한 알고리즘은 정해지지 않았으며 향후 변경될 수 있다.
        int result = title == null ? 0 : title.hashCode();  
        result = 31 * result + (author == null ? 0 : author.hashCode());  
        result = 31 * result + (isbn == null ? 0 : isbn.hashCode());  
        return result;  
    }

    // 암시적 구현은 두 레코드의 모든 컴포넌트가 서로 동일하면 true를 반환한다.
    @Override
    public final boolean equals(Object o) {
        // 구현에 사용되는 정확한 알고리즘은 정해지지 않았으며 향후 변경될 수 있다.
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(title, book.title) &amp;amp;&amp;amp; Objects.equals(author, book.author) &amp;amp;&amp;amp; Objects.equals(isbn, book.isbn);
    }

    // 컴포넌트명과 동일한 게터(getter)가 선언된다.
    public String title() {
        return this.title;
    }

    public String author() {
        return this.author;
    }

    public String isbn() {
        return this.isbn;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덧붙여서 필요하다면 로컬 클래스와 마찬가지로 로컬 레코드를 선언할 수도 있습니다. 로컬 레코드는 아래와 같이 로컬 클래스와 같이 메서드의 본문에 정의된 레코드를 말합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public class Foo {
    public void doSomething() {
        // ...
        record Bar(...) {
            // ...
        }
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴팩트 생성자(Compact Constructor)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 별도의 초기화 로직이 필요하다면 레코드 안에 표준 생성자를 만들 수도 있습니다. 이 생성자의 형태는 우리가 늘 봤던 형태라 익숙할 것입니다. 물론 내부에는 final로 선언된 인스턴스 필드가 있어서 생성자 안에서 모두 초기화해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record Book(String title, String author, String isbn) {    
    // 물론 이렇게 다른 생성자를 추가할 수도 있다.
    public Book(String title, String isbn) {
        this(title, &quot;Unknown&quot;, isbn);
    }

    public Book(String title, String author, String isbn) {
        // 조금 더 복잡한 초기화 로직 ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이러한 표준 생성자 말고도 컴팩트 생성자를 사용할 수도 있습니다. 아래와 같이 생성자 매개변수를 받는 부분이 사라진 형태입니다. 개발자가 일일이 명시적으로 인스턴스 필드를 초기화하지 않아도 컴팩트 생성자의 마지막에 초기화 구문이 자동으로 삽입됩니다. 그리고 표준 생성자와는 달리 컴팩트 생성자 내부에서는 인스턴스 필드에 접근을 할 수가 없으며, 접근하려고 하면 &quot;final 변수 'x'에 값을 할당할 수 없습니다.&quot;와 같은 에러 메시지를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public record Book(String title, String author, String isbn) {
    // public Book(String title, String author, String isbn) { ... }과 동일
    public Book {
        Objects.requireNonNull(title);
        Objects.requireNonNull(author);
        Objects.requireNonNull(isbn);
        // this.title = title;
        // this.author = author;
        // this.isbn = isbn;
    }

    // 여전히 아래와 같이 표준 생성자와 컴팩트 생성자를 혼용해서 쓸 수 있다.
    public Book(String title, String isbn) {
        this(title, &quot;Unknown&quot;, isbn);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 컴팩트 생성자에는 컴포넌트로 넘어온 객체를 불변으로 만들거나, 불변식(invariant)을 만족하는지 검사하는(예를 들어서 위와 같이 null이 넘어오지는 않았는지) 등의 작업을 하기에 적합합니다. &lt;a href=&quot;https://mail.openjdk.java.net/pipermail/amber-spec-experts/2020-July/002254.html&quot;&gt;메일링 리스트&lt;/a&gt;에서 발견한 설계자의 의도는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mail.openjdk.java.net/pipermail/amber-spec-experts/2020-July/002254.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개빈 비어만(Gavin Bierman)이 말하는 컴팩트 생성자의 의도&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 레코드 클래스에 표준 생성자를 명시적으로 제공해야 하는 이유는 인수의 값을 검증하거나 정규화하기 위함입니다. 레코드 클래스 선언의 가독성을 높이기 위해서 이러한 검증/정규화용 코드만 필요한 새로운 형태의 소형화된(compact) 표준 생성자 선언을 제공합니다. 예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652988289163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;record Rational(int num, int denom) { 
	Rational {
    	// 기약분수로 만들기 위해서 분모와 분자를 최대공약수(GCD)로 나눈다.
		int gcd = gcd(num, denom);
		num /= gcd;
		denom /= gcd;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴팩트 생성자 선언의 의도는 생성자 본문에 검증/정규화용 코드만 넣어야 한다는 것입니다. 나머지 초기화 코드는 컴파일러가 자동으로 수행합니다. 매개변수 목록은 레코드의 컴포넌트 목록에서 가져오기 때문에 컴팩트 생성자 선언에 필요하지 않습니다. 달리 말하면, 위의 선언은 기존 생성자의 형태를 사용하는 아래의 선언과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652988310428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;record Rational(int num, int denom) { 
	Rational(int num, int demon) {
		// 검증/정규화(Validation/Normalization)
		int gcd = gcd(num, denom);
		num /= gcd;
		denom /= gcd;
		// 초기화
		this.num = num;
		this.denom = denom;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레코드 간 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 레코드를 비교하는 간단한 예시를 잠깐 살펴봅시다. 비교 연산자(==)를 사용하면 참조(reference)를 비교하기 때문에 equals() 메서드를 사용해야 한다는 점을 잊지 맙시다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class RecordExamples {
    public static void main(String[] args) {
        // 클래스와 마찬가지로 new 연산자를 통해 레코드의 인스턴스를 생성한다.
        Book bookA = new Book(&quot;Book A&quot;, &quot;Author A&quot;);
        Book bookB = new Book(&quot;Book B&quot;, &quot;Author B&quot;);

        // 두 레코드의 간단한 비교
        System.out.println(&quot;bookA.hashCode() = &quot; + bookA.hashCode());
        System.out.println(&quot;bookB.hashCode() = &quot; + bookB.hashCode());
        if (bookA.equals(bookB)) {
            System.out.println(&quot;bookA와 bookB는 서로 같습니다.&quot;);
        }
        System.out.println();

        bookB = new Book(&quot;Book A&quot;, &quot;Author A&quot;);
        System.out.println(&quot;bookA.hashCode() = &quot; + bookA.hashCode());
        System.out.println(&quot;bookB.hashCode() = &quot; + bookB.hashCode());
        if (bookA.equals(bookB)) {
            System.out.println(&quot;bookA와 bookB는 서로 같습니다.&quot;);
        }
        System.out.println();

        // toString() 출력 살펴보기
        System.out.println(&quot;bookA = &quot; + bookA);
        System.out.println(&quot;bookB = &quot; + bookB);

        // 게터로 출력하기
        System.out.println(&quot;bookA.title() = &quot; + bookA.title());
        System.out.println(&quot;bookA.author() = &quot; + bookA.author());
    }

    /* static */ record Book(String title, String author) { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>DTO</category>
      <category>java</category>
      <category>레코드</category>
      <category>자바</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/399</guid>
      <comments>https://exynoa.tistory.com/399#entry399comment</comments>
      <pubDate>Fri, 20 May 2022 04:26:14 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 롬복(Project Lombok) 살펴보기</title>
      <link>https://exynoa.tistory.com/398</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Lombok.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ4vl5/btrCCfsHllz/hpKAKAt2gmVCAhdEzLkRJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ4vl5/btrCCfsHllz/hpKAKAt2gmVCAhdEzLkRJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ4vl5/btrCCfsHllz/hpKAKAt2gmVCAhdEzLkRJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ4vl5%2FbtrCCfsHllz%2FhpKAKAt2gmVCAhdEzLkRJ0%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;142&quot; data-filename=&quot;Lombok.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 롬복(Lombok)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectlombok.org/&quot;&gt;프로젝트 롬복(Project Lombok, 이하 롬복)&lt;/a&gt;은 게터(getter), 세터(setter), equals()/hashCode() 등과 같이 코드를 작성하면서 계속 비슷한 내용이 지루하게 반복되었던 코드들을 애노테이션 선언 하나로 간단하게 대체할 수 있도록 도와주는 자바 라이브러리입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 IntelliJ 위주로 살펴보므로, 최신 정보나 IntelliJ가 아닌 다른 IDE에서는 어떻게 설치하는지 알고 싶다면 &lt;a href=&quot;https://projectlombok.org/setup/overview&quot;&gt;공식 홈페이지&lt;/a&gt;에서 확인해보세요. IntelliJ 같은 경우는 2020.3 버전부터 롬복 플러그인이 내장되어 있어서 별도로 설정해줄 것이 없습니다. 의존성 설정은 직접 해도 되지만 아래와 같이 Alt+Enter를 눌러서 Context Actions에서 롬복(lombok)을 간단하게 클래스패스에 추가할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Lombok_P01.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcctci/btrCDdgR0Sw/gjGr2T99mYKHgr2zKdP4ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcctci/btrCDdgR0Sw/gjGr2T99mYKHgr2zKdP4ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcctci/btrCDdgR0Sw/gjGr2T99mYKHgr2zKdP4ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcctci%2FbtrCDdgR0Sw%2FgjGr2T99mYKHgr2zKdP4ZK%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;499&quot; height=&quot;150&quot; data-filename=&quot;Attachments_Lombok_P01.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Gradle&lt;/h4&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'

    testCompileOnly 'org.projectlombok:lombok:1.18.24'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Maven&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.18.24&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지원하는 애노테이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복에서 지원하는 모든 애노테이션을 보려면 &lt;a href=&quot;https://projectlombok.org/features/all&quot;&gt;이곳&lt;/a&gt;에서 살펴볼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;admonition hint&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;IntelliJ IDEA에 내장된 롬복 플러그인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복 플러그인을 사용하면 간단하게 롬복 애노테이션을 풀어서 원본 코드를 볼 수 있는데, IntelliJ 같은 경우는 2020.3 버전부터 롬복 플러그인이 기본적으로 내장되어 있으므로 소스 코드에서 우클릭 후 &quot;Refactor &amp;gt; Delombok&quot;을 누르면 바로 볼 수 있습니다. 또한 롬복을 사용하려면 &lt;b&gt;Settings -&amp;gt; Build, Execution, Deployment -&amp;gt; Compiler -&amp;gt; Annotation Processors&lt;/b&gt;에서 &lt;b&gt;Enable annotation processing&lt;/b&gt;을 눌러서 활성화해야 했지만, 플러그인이 내장되면서 자동으로 활성화되므로 그럴 필요가 없어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@NonNull&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머가 명시적으로 작성해야 했던 널 검사를 이 애노테이션으로 대체할 수 있습니다. 메서드나 생성자의 매개변수 뿐만 아니라 자바 14 이후로 추가된 레코드의 컴포넌트(component)에도 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 롬복 사용 전
class Foo {  
    private String name;  

    public Foo(Bar bar) {
        if (bar == null) {
            throw new NullPointerException(&quot;bar is marked non-null but is null&quot;);
        }
        this.name = bar.getName();
    }
}

// 롬복 사용 후
class Foo {
    private String name;

    public Foo(@NonNull Bar bar) {
        this.name = bar.getName();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생하는 예외 메시지를 살펴보면 다음과 같습니다. 자바 13 이전에는 예외의 기본 메시지가 비어있어서 원인을 좀처럼 파악하기가 어려웠지만, 자바 14 이후에는 더 자세하고 유용한 메시지를 제공합니다. 그래서 @NonNull이 다소 효용성은 떨어질 수는 있으나 여전히 문서화의 이점을 제공합니다. 이를 사용하면 굳이 코드 내부를 살펴보지 않고도 사용자에게 널을 전달하지 않아야 한다는 사실을 전달할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 롬복 사용 전 (자바 13 이전)
Exception in thread &quot;main&quot; java.lang.NullPointerException

// 롬복 사용 전 (자바 14 이후)
Exception in thread &quot;main&quot; java.lang.NullPointerException: Cannot invoke &quot;com.example.Bar.getName()&quot; because &quot;bar&quot; is null

// 롬복 사용 후
Exception in thread &quot;main&quot; java.lang.NullPointerException: bar is marked non-null but is null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 매개변수가 널(null)이라는 부분에 초점을 맞추기보다는 인수가 잘못되었음을 강조하고 싶을 때는 아래의 설정을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;; 기본값은 NullPointerException이다. 예외 메시지는 동일하다.
; IllegalArgumentException 이외에도 JDK, Guava를 설정할 수 있다.
lombok.nonNull.exceptionType = IllegalArgumentException&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Getter/@Setter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 애노테이션을 통해 게터(getter)와 세터(setter)를 추가할 수 있게 해줍니다. 관례적으로 게터의 접두사는 get이 붙고(boolean의 경우에는 is가 붙음), 세터의 접두사에는 set이 붙습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 롬복 사용 전
class Person {  
    private String name;  
    private int age;

    // getter 
    public String getName() {  
        return name;  
    }  

    public int getAge() {  
        return age;  
    }  

    // setter
    public void setName(String name) {  
        this.name = name;  
    }  

    public void setAge(int age) {  
        this.age = age;  
    }
}

// 롬복 사용 후
@Getter  
@Setter  
class Person {  
    private String name;  
    private int age;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 필드 선언에 달 수 있지만 위처럼 클래스 선언에도 달 수 있습니다. 클래스 선언에 달면 해당 클래스 내의 모든 인스턴스 필드에 애노테이션이 달린다고 생각하면 됩니다. 게터와 세터는 접근 수준(AccessLevel)을 따로 지정해주지 않으면 위와 같이 public이 지정됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@Setter
class Person {
    private String name;
    // AccessLevel에는 PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE, NONE이 있다.
    // AccessLevel.NONE을 붙이면 아무것도 생성하지 않는다.
    // 필드의 애노테이션이 클래스의 애노테이션보다 우선되므로,
    // 이 예시에서는 name의 세터는 생성되지만 age는 만들어지지 않는다.
    @Setter(AccessLevel.NONE) private int age;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게터, 세터와 관련된 롬복 설정 중 일부를 살펴보면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;; boolean 필드의 접두사를 is 대신에 get을 사용한다. 기본값은 false다.
lombok.getter.noIsPrefix = true

; 자바 빈 표준 접두사인 get, is, set을 앞에 붙이는 대신에 필드명과 동일한 이름을 
; 게터와 세터의 이름으로 사용한다. 기본값은 false다.
; 예를 들어서 필드 name은 name()으로 접근할 수 있게 된다.
lombok.accessors.fluent = true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ToString&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 애노테이션을 클래스 선언에 달면 롬복이 toString() 메서드의 구현을 자동으로 추가해줍니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 롬복 사용 전
class Foo {
    private String[] items;  
    private int maxItems;

    // ...
    @Override  
    public String toString() {  
        return &quot;Foo(items=&quot; + Arrays.deepToString(items) + &quot;, maxItems=&quot; + maxItems + &quot;)&quot;;  
    }
}

// 롬복 사용 후
@ToString
class Foo {
    private String[] items;  
    private int maxItems;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복 사용 전을 보면 알겠지만 롬복으로 구현되는 toString()이 호출되면 클래스명 뒤에 소괄호가 등장하고 그 안에 인스턴스 필드(정적 필드 제외)가 나열되는 식으로 출력합니다. 언뜻 보면 편해 보이기는 하지만 각별한 주의가 필요합니다. 아래와 같이 양방향 연관관계가 있으면 Foo가 bar.toString()을 호출하고 이어서 Bar가 foo.toString()을 호출하는 식으로 무한 재귀가 일어나서 java.lang.StackOverflowError이 발생할 수 있기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@ToString
class Foo {
    private String name;
    private Bar bar;
    // ...
}

@ToString
class Bar {
    private Foo foo;
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하려면 아래와 같이 문제가 되는 필드를 출력에서 제외하여 재귀가 일어나는 것을 중간에서 끊어야 합니다. 덧붙여서, 아래의 @ToString.Include는 메서드에도 사용할 수 있습니다. 즉, 인스턴스 필드 외에도 메서드의 반환 값도 같이 출력할 수 있습니다. 또한 코드에서 나타나는 순서가 아니라 별도의 순서가 필요하다면 @ToString.Include(rank = 1)과 같이 직접 순서를 지정해줄 수도 있습니다. 이때는 상위 랭크(더 큰 숫자)가 먼저 출력됩니다. 참고로 랭크(rank)의 기본값은 0입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ToString
class Foo {
    private String name;

    // 이 필드는 출력에서 제외된다(exclude).
    @ToString.Exclude
    private Bar bar;
    // ...
}

// 또는 ...

// 아래와 같이 지정하면 @ToString.Include가 달린 필드나 메서드만 출력한다.
@ToString(onlyExplicitlyIncluded = true)
class Foo {
    // 이 필드를 출력에 포함한다(include).
    @ToString.Include
    private String name;

    private Bar bar;
    // ...
}

// 또는 ...

// 아래와 같은 방법도 있지만 이 방법은 앞으로 사라질(deprecated) 예정이다.
@ToString(of = {&quot;name&quot;}) // 혹은 @ToString(exclude = {&quot;bar&quot;})
class Foo {
    private String name;
    private Bar bar;
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 @ToString에 사용할 수 있는 요소에는 아래와 같은 것들이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// 보통은 게터가 있으면 필드 접근 대신에 게터에서 값을 얻어오는데,
// 게터를 사용하지 않고 필드에서 직접 값을 가져오고 싶으면 아래와 같이 지정할 수 있다.
@ToString(doNotUseGetters = true) // 기본값 false

// 부모의 toString() 호출 결과도 포함하고 싶으면 아래와 같이 지정할 수 있다.
// 예) Child(super=Parent(name=&quot;Sam&quot;), name=&quot;John&quot;)
@ToString(callSuper = true)
class Child extends Parent { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 그대로 생성자에 관한 애노테이션들입니다. 인수가 없는 기본 생성자는 @NoArgsConstructor, 초기화가 필수적인 필드만 초기화하는 생성자는 @RequiredArgsConstructor, 모든 필드를 초기화하는 생성자는 @AllArgsConstructor를 사용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@NoArgsConstructor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 인수가 없는 기본 생성자를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 전
class Foo {
    private String name;
    private int age;

    public Foo() {
    }
    // ...
}

// 후
@NoArgsConstructor
class Foo {
    private String name;
    private int age;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 클래스 내에 final 필드가 있으면 당연히 에러가 발생하는데, 이를 무시하고 강제로 기본 생성자를 만들고 싶다면 아래와 같이 force를 사용하면 됩니다. 이때 모든 final 필드는 기본값(0, null, false)으로 초기화됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@NoArgsConstructor(force = true) // 기본값 false
class Foo {
    private final String name;
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@RequiredArgsConstructor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 초기화되지 않은 final 필드나 @NonNull 애노테이션이 붙은 필드의 초기화를 진행하는 생성자를 만듭니다. 즉, 이미 필드 선언에서 명시적으로 초기화된 final 필드나 @NonNull 필드는 생성자에 들어가지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 전
class Foo {
    private final String name;
    private final int age;
    private String address;

    public Foo(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // ...
}

// 후
@RequiredArgsConstructor  
class Foo {  
    private final String name;  
    private final int age;  
    private String address;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@AllArgsConstructor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 모든 필드를 초기화하는 생성자를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 전
class Foo {
    private final String name;
    private final int age;
    private String address;

    public Foo(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    // ...
}

// 후
@AllArgsConstructor  
class Foo {  
    private final String name;  
    private final int age;  
    private String address;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공통 요소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 살펴본 모든 생성자 관련 애노테이션에서 사용할 수 있는 요소들을 살펴봅시다. 생성자의 접근 제어를 별도로 지정하고 싶으면 아래와 같이 access를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;// 게터, 세터 예시에서 봤던 것처럼 아래와 같은 접근 수준을 지원한다.
// PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE, NONE
@NoArgsConstructor(access = AccessLevel.PROTECTED)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 staticName입니다. 이는 정적 팩토리 메서드를 생성할 때 사용되며, 그와 동시에 사용된 애노테이션에 따라 별도의 private 생성자가 만들어집니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 전
class Foo {  
    private String name;  
    private int length;  

    private Foo(String name, int length) {  
        this.name = name;  
        this.length = length;  
    }  

    public static Foo of(String name, int length) {  
        return new Foo(name, length);  
    }  
    // ...  
}

// 후
@AllArgsConstructor(staticName = &quot;of&quot;)  
class Foo {  
    private String name;  
    private int length;  
    // ...  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@EqualsAndHashCode&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름에서도 짐작 가듯이 동등성(equality) 비교에 사용되는 equals()과 해시를 사용하는 자료구조에서 사용되는 hashCode()를 만들어줍니다. 기본적으로 transient이나 static이 붙지 않은 모든 필드를 구현에 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 전
class Foo {
    private String name;
    private int age;

    // ...
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Foo)) return false;
        final Foo other = (Foo) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$name = this.name;
        final Object other$name = other.name;
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        if (this.age != other.age) return false;
        return true;
    }

    protected boolean canEqual(final Object other) {
        return other instanceof Foo;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $name = this.name;
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        result = result * PRIME + this.age;
        return result;
    }
}

// 후
@EqualsAndHashCode
class Foo {
    private String name;
    private int age;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 equals()나 hashCode()의 구현에 포함하거나 제외할 필드가 있으면 아래와 같이 @EqualsAndHashCode.Include 혹은 @EqualsAndHashCode.Exclude를 사용하면 됩니다. 덧붙여서, @EqualsAndHashCode.Include는 필드 뿐만 아니라 메서드에서도 사용할 수 있으며, rank를 통해서 equals() 내에서의 비교 순서와 hashCode() 내에서의 계산 순서를 변경할 수 있습니다. rank가 높은 것부터 먼저 처리하는데, 기본 타입의 기본 rank는 1000이고 기본 래퍼 타입(Int, Double, Float)의 경우에는 800으로 설정되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// name 필드를 포함하고 age 필드를 제외하는 세 가지 방법
class Foo {
    private String name;

    @EqualsAndHashCode.Exclude
    private int age;

    // ...
}

// 또는 ...

@EqualsAndHashCode(onlyExplicitlyIncluded = true)  
static class Foo {  
    @EqualsAndHashCode.Include  
    private String name;  

    private int age;  

    // ...
}

// 또는 ...

// 아래와 같은 방법도 있지만 이 방법은 앞으로 사라질(deprecated) 예정이다.
@EqualsAndHashCode(of = &quot;name&quot;) // 혹은 @...(exclude = &quot;age&quot;)
static class Foo {  
    private String name;  
    private int age;  

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 아래의 예시를 봅시다. childA와 childB 두 객체는 동등하지 않으므로(두 객체의 각 필드의 값이 일치하지 않으므로) 아래의 테스트를 통과해야 정상입니다. 하지만 테스트를 실행해보면 실패하고 두 객체가 동등하다는 메시지를 볼 수 있습니다. 이는 Child 클래스에 추가된 equals() 메서드가 Parent 클래스까지 고려하지 않고 Child 클래스 내의 필드만 고려하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class LombokTest {
    @Test
    void testEquality() {
        Child childA = new Child(&quot;A&quot;, 20, 100);
        Child childB = new Child(&quot;B&quot;, 23, 100);

        assertNotEquals(childA, childB);
    }

    @EqualsAndHashCode
    static class Parent {
        protected String name;
        protected int age;

        protected Parent(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }

    @EqualsAndHashCode
    static class Child extends Parent {
        private double height;

        public Child(String name, int age, double height) {
            super(name, age);
            this.height = height;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 경우에는 Child 클래스의 equals() 메서드 내부에서 Parent 클래스의 equals()도 호출하도록 아래와 같이 callSuper를 true로 지정해주어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@EqualsAndHashCode(callSuper = true) // 기본값은 false
static class Child extends Parent { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 아래와 같은 것들이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 보통은 게터가 있으면 필드 접근 대신에 게터에서 값을 얻어오는데,
// 게터를 사용하지 않고 필드에서 직접 값을 가져오고 싶으면 아래와 같이 지정할 수 있다.
@EqualsAndHashCode.Include(doNotUseGetters = true) // 기본값 false&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Data&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 애노테이션은 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode를 단 것과 동일합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 두 방법은 동일하다.
@Getter
@Setter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
class Foo {
    // ...
}

@Data
class Foo {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 @Data 하나로는 각 애노테이션을 세부적으로 지정하는 것은 할 수 없습니다. 예를 들어서, 여기서 일부 애노테이션만 빼고 싶다던가, 접근 수준 같은 자잘한 설정 등을 조금 변경하고 싶은 경우에는 어떻게 해야 할까요? 바로 아래와 같이 명시적으로 추가하면 기존 애노테이션에 덧씌울 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Data
@Setter(AccessLevel.NONE)
@ToString(callSuper = true)
static class Foo { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Value&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Value는 @Data의 불변 버전에 가깝습니다. @Getter @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) @AllArgsConstructor @ToString @EqualsAndHashCode를 단 것과 동일합니다. @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)은 아래를 보면 알겠지만 @NonFinal이 붙지 않은 각 인스턴스 필드에 final 한정자를 추가하고, @PackagePrivate이나 접근 제어자가 붙지 않은(즉, default인) 필드의 접근 수준을 private로 설정합니다. 불변 클래스를 상속받으면 불변이 깨질 수 있으므로 클래스 자체도 final로 선언됩니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 전
final class Foo {
    private final String name;
    private final int height;

    public Foo(String name, int height) { /* ... */ }
    public String getName() { /* ... */ }
    public int getHeight() { /* ... */ }
    public boolean equals(final Object o) { /* ... */ }
    public int hashCode() { /* ... */ }
    public String toString() { /* ... */ }
    // ...
}

// 후
@Value
class Foo {
    String name;
    int height;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Log&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 애노테이션을 클래스에 달면 롬복이 로거(logger) 필드를 따로 만들어줍니다. 로거의 이름은 log이고 static final로 선언되며 타입은 선택한 로거에 따라서 달라집니다. 지원하는 애노테이션에는 @Slf4j, @XSlf4j, @Log4j, @Log4j2, @Log, @CommonsLog, @Flogger, @JBossLog, @CustomLog가 있습니다. 구체적으로 어떻게 선언되는지는 &lt;a href=&quot;https://projectlombok.org/features/log&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에서 살펴봐 주세요.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 전
class Foo {
    private static final org.slf4j.Logger log = new org.slf4j.LoggerFactory.getLogger(Foo.class);

    public void doSomething() {
        log.info(&quot;Something happened&quot;);
    }
}

// 후
@Slf4j
class Foo {
    public void doSomething() {
        log.info(&quot;Something happened&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Synchronized&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 애노테이션을 메서드에 붙이면 자동으로 private 락과 동기화 블록을 만들어줍니다. 정적 메서드나 인스턴스 메서드 두 메서드에서 모두 사용할 수 있으며, 인스턴스 메서드에 달면 인스턴스 필드인 $lock으로 잠그고 정적 메서드에 달면 정적 필드인 $LOCK으로 잠급니다. 프로그래머가 만든 별도의 락 객체 lock이 있으면 @Synchronized(&quot;lock&quot;)와 같이 작성하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 전
class Foo {
    private static final Object $LOCK = new Object[0];
    private final Object $lock = new Object[0];

    public static void doSomething() {
        synchronized ($LOCK) {
            // ...
        }
    }

    public void doSomethingElse() {
        synchronized ($lock) {
            // ...
        }
    }
}

// 후
class Foo {
    @Synchronized
    public static void doSomething() {
        // ...
    }

    @Synchronized
    public void doSomethingElse() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>라이브러리</category>
      <category>롬복</category>
      <category>자바</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/398</guid>
      <comments>https://exynoa.tistory.com/398#entry398comment</comments>
      <pubDate>Thu, 19 May 2022 19:25:09 +0900</pubDate>
    </item>
    <item>
      <title>JVM. 클래스로더 서브시스템(Class Loader Subsystem)</title>
      <link>https://exynoa.tistory.com/397</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 RAM에 위치하며, 실행 중에 클래스로더 서브시스템을 이용하여 클래스 파일을 RAM으로 가져옵니다. 이를 자바의 동적 클래스 로딩 기능이라고 합니다. 이 과정은 컴파일 타임이 아니라 런타임에 일어나며, 처음으로 클래스를 참조할 때 클래스 파일(.class)을 로드하고, 링크하고, 초기화 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_JVM_P03.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKbibl/btrClmrIA8B/4kCzbMYQOq1jvWA6DGFdm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKbibl/btrClmrIA8B/4kCzbMYQOq1jvWA6DGFdm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKbibl/btrClmrIA8B/4kCzbMYQOq1jvWA6DGFdm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKbibl%2FbtrClmrIA8B%2F4kCzbMYQOq1jvWA6DGFdm0%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;768&quot; height=&quot;227&quot; data-filename=&quot;Attachments_JVM_P03.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로딩(Loading)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일된 클래스(.class 파일)을 메모리에 적재하는 것이 클래스로더(class loader)의 주요 작업입니다. 보통, 클래스 로딩 과정은 메인 클래스(즉, static main() 메서드 선언이 있는 클래스)를 로드하는 것부터 시작됩니다. 이외에도 클래스 로딩은 아래의 상황에서 일어날 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 클래스에 선언된 정적 메서드를 호출할 때 
Car.invokeStaticMethod(); // static void invo..() { ... }

// 클래스나 인터페이스에 선언된 정적 필드 접근 혹은 할당
Car.wheels = 4; // static int wheels;

// 클래스의 인스턴스를 만들 때 (명시적 생성, 역직렬화, 리플렉션 등)
Car car = new Car(&quot;BMW&quot;); // 명시적 생성
...

// 리플렉션 같은 특정 자바 SE 플랫폼 클래스 라이브러리에 있는 메서드를 호출하는 경우
Class&amp;lt;?&amp;gt; clazz = Class.forName(&quot;com.company.Car&quot;);
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 부모 클래스를 먼저 로딩해야 자식 클래스를 로딩할 수 있으므로 아래와 같은 상황도 있을 수 있습니다. 아래의 결과를 보면 클래스가 실행 초기에 모두 로딩되는 게 아니라 그 클래스 사용 시점에 로딩되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// (1) ClassLoadingExamples 클래스가 로드됨
// (2) 메인 메서드 진입
// (3) Car 클래스가 로드됨
// (4) Audi 클래스가 로드됨
// (5) Car 클래스의 생성자가 호출됨
// (6) Audi 클래스의 생성자가 호출됨
// (7) 메인 메서드 종료
public class ClassLoadingExamples {
    static { // (1)
        System.out.println(&quot;ClassLoadingExamples 클래스가 로드됨&quot;);
    }

    public static void main(String[] args) {
        System.out.println(&quot;메인 메서드 진입&quot;); // (2)
        Audi audi = new Audi(); // (3) ~ (6)
        System.out.println(&quot;메인 메서드 종료&quot;); // (7)
    }

    abstract static class Car {
        static { // (3)
            System.out.println(&quot;Car 클래스가 로드됨&quot;);
        }

        protected Car() { // (5)
            System.out.println(&quot;Car 클래스의 생성자가 호출됨&quot;);
        }
    }

    static class Audi extends Car {
        static { // (4)
            System.out.println(&quot;Audi 클래스가 로드됨&quot;);
        }

        public Audi() { // (6)
            System.out.println(&quot;Audi 클래스의 생성자가 호출됨&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스로더 서브시스템에는 부트스트랩 클래스로더, 확장 클래스로더, 애플리케이션 클래스로더(혹은 시스템 클래스로더)와 같이 3가지 유형의 클래스로더가 있으며, 클래스로더는 아래의 4가지 주요 원칙을 따릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가시성 원칙(Visibility Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 클래스로더는 부모 클래스로더가 로드한 클래스를 볼 수 있지만, 부모 클래스로더는 자식 클래스로더가 로드한 클래스를 볼 수 없다는 원칙입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_JVM_P02.png&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6rTIJ/btrCjVU5LXl/eiQM2GYAk103L0rEmSejQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6rTIJ/btrCjVU5LXl/eiQM2GYAk103L0rEmSejQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6rTIJ/btrCjVU5LXl/eiQM2GYAk103L0rEmSejQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6rTIJ%2FbtrCjVU5LXl%2FeiQM2GYAk103L0rEmSejQk%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;335&quot; height=&quot;421&quot; data-filename=&quot;Attachments_JVM_P02.png&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서, 확장 클래스로더를 통해 클래스 Vehicle.class를 로드했다고 하면 부모 클래스로더인 부트스트랩 클래스로더는 이를 볼 수 없으며, 확장 클래스로더와 그 자식 클래스로더인 애플리케이션 클래스로더만 볼 수 있습니다. 부트스트랩 클래스로더를 통해 해당 클래스를 로드하려고 하면 클래스를 찾을 수 없다는 java.lang.ClassNotFoundException 예외가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class ClassLoaderExample {
    public static void main(String args[]) {
        try {
            // 이 클래스의 클래스로더를 출력한다.
            System.out.println(&quot;ClassLoaderExample.getClass().getClassLoader(): &quot; + ClassLoaderExample.class.getClassLoader());

            // 확장 클래스로더를 통해서 이 클래스를 다시 로드한다.
            Class.forName(&quot;ClassLoaderExample&quot;, true, ClassLoaderExample.class.getClassLoader().getParent());
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 ClassLoaderExample의 클래스로더는 애플리케이션 클래스로더이고, 부모 클래스로더는 확장 클래스로더 입니다. 부모 클래스로더는 자식 클래스로더가 로드한 클래스를 볼 수 없으므로 예외 java.lang.ClassNotFoundException가 발생하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_JVM_P03.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZBPIQ/btrCjETBceK/oPW7iBeh52AwpYvLZvWFH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZBPIQ/btrCjETBceK/oPW7iBeh52AwpYvLZvWFH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZBPIQ/btrCjETBceK/oPW7iBeh52AwpYvLZvWFH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZBPIQ%2FbtrCjETBceK%2FoPW7iBeh52AwpYvLZvWFH1%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;787&quot; height=&quot;158&quot; data-filename=&quot;Attachments_JVM_P03.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유일성 원칙(Uniqueness Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모가 로드한 클래스를 자식 클래스로더가 다시 로드하지 않아야 하며 이미 로딩한 클래스를 다시 로드해서는 안 된다는 원칙입니다. 예를 들어서 부모인 확장 클래스로더가 이미 로드한 클래스를 자식인 애플리케이션 클래스로더가 로드해서는 안 된다는 것입니다. 이 원칙을 통해 클래스가 정확히 한 번만 로드할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위임 계층 원칙(Delegation Hierarchy Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 가시성 원칙과 유일성 원칙을 충족하기 위해서 JVM은 클래스 로딩 요청을 받을 클래스로더를 선택하기 위해 위임 계층을 따릅니다. 여기서 가장 아래에 있는 애플리케이션 클래스로더가 자기가 받은 클래스 로딩 요청을 부모인 확장 클래스로더에 위임한 다음, 확장 클래스로더가 다시 부모인 부트스트랩 클래스로더에 이를 위임합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_JVM_P04.png&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ePoEHO/btrCcFsXQeU/PJNuytE1DvZEwOyHa7UEW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ePoEHO/btrCcFsXQeU/PJNuytE1DvZEwOyHa7UEW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ePoEHO/btrCcFsXQeU/PJNuytE1DvZEwOyHa7UEW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FePoEHO%2FbtrCcFsXQeU%2FPJNuytE1DvZEwOyHa7UEW0%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;1241&quot; height=&quot;579&quot; data-filename=&quot;Attachments_JVM_P04.png&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청한 클래스가 부트스트랩 클래스패스(jdk/jre/lib)에 있으면 해당 클래스를 로드합니다. 없으면 요청을 확장 클래스로더가 위임하게 되는데, 요청한 클래스가 확장 클래스패스(jdk/jre/lib/ext)에 있으면 해당 클래스를 로드합니다. 없으면 요청을 애플리케이션 클래스로더에 위임합니다. 마지막으로 요청한 클래스가 애플리케이션 클래스패스에 있으면 해당 클래스를 로드합니다. 만약 이번에도 실패하면 런타임 예외인 java.lang.ClassNotFoundException가 발생합니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;클래스패스(classpath)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM이 프로그램을 실행할 때, 클래스 파일을 찾는 데 기준이 되는 파일 경로를 말합니다. 시스템의 모든 폴더를 JVM이 검사하도록 하는 것은 비현실적이므로 JVM에 찾아볼 파일 경로를 제공해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언로딩 금지 원칙(No Unloading Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스로더는 클래스를 로드할 수는 있지만 이미 로드한 클래스를 언로드(unload) 할 수 없습니다. 언로드 하는 것 대신에 현재 클래스로더를 제거하고 새로운 클래스로더를 만들 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스로더(Classloader)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스로더는 말 그대로 자바의 클래스를 로드하는 객체를 말합니다. 클래스로더를 통해서 런타임에 동적으로 클래스를 로드할 수 있으며, 보통은 패키지에 있는 클래스 파일(.class)을 사용해서 클래스를 로드합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부트스트랩&amp;nbsp;클래스로더(Bootstrap&amp;nbsp;Class&amp;nbsp;Loader)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 기본적으로 제공하는 API 등과 같은 표준 JDK 클래스들을 부트스트랩 클래스패스(%JAVA_HOME%/jre/lib)에 있는 rt.jar에서 로드합니다. 이 클래스로더는 C/C++와 같은 네이티브 언어로 구현되며 자바에서 모든 클래스로더의 부모 역할을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652739503421&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 자바 9 이후로는 클래스로더 구현이 변경되어서 더 이상 rt.jar에서 로드하지 않는다.
// 여전히 ..\jre\lib에서 저장되지만 문서화가 되지 않아서 향후 변경될 수 있다. (예: ..\jre\lib\modules)
// ..\jre\lib\resources.jar;..\jre\lib\rt.jar;..\jre\lib\jce.jar;...
System.out.println(System.getProperty(&quot;sun.boot.class.path&quot;));

// JVM 구현에 따라 다를 수 있지만 null은 보통 부트스트랩 클래스로더를 의미한다.
// 위에서도 언급했듯이 C/C++로 구현되기 때문에 자바에서 참조를 얻어올 수 없다.
System.out.println(ArrayList.class.getClassLoader()); // null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장&amp;nbsp;클래스로더(Extension&amp;nbsp;Class&amp;nbsp;Loader)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 로드 요청을 상위의 부트스트랩 클래스로더에 위임하고, 실패하면 확장 클래스패스(%JAVA_HOME%/jre/lib/ext나 환경 변수 java.ext.dirs에 지정된 경로)의 확장 디렉토리(예: 보안 확장 기능)에서 클래스를 로드합니다. 이 클래스로더는 sun.misc.Launcher$ExtClassLoader 클래스로 자바에 구현되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652741497611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ..\jre\lib\ext
System.out.println(System.getProperty(&quot;java.ext.dirs&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 9 이후부터는 확장 메커니즘이 제거되면서 확장 클래스로더의 이름이 플랫폼 클래스로더(Platform Class Loader)로 변경되었습니다. 클래스로더 계층은 변경 없이 그대로 유지됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652741704844&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// jdk.internal.loader.ClassLoaders$PlatformClassLoader@776ec8df
System.out.println(ClassLoader.getPlatformClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션&amp;nbsp;클래스로더(Application&amp;nbsp;Class&amp;nbsp;Loader)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스로더는 시스템 클래스로더(System Class Loader)라고 부르기도 합니다. CLASSPATH 환경 변수, 명령행 인수 -classpath나 -cp로 지정된 경로에서 클래스를 로드하는 역할을 합니다. 이 클래스로더는 sun.misc.Launcher$AppClassLoader 클래스로 자바에 구현되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652741966083&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링킹(Linking)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 로드된 클래스나 인터페이스, 그 직계 부모클래스나 인터페이스, 필요한 경우 요소 타입(배열 타입인 경우)을 검증하고, 준비하고, 해석하는 과정을 거칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스나 인터페이스는 링크되기 전에 완전히 로드되어야 한다.&lt;/li&gt;
&lt;li&gt;클래스나 인터페이스는 초기화하기 전에 완전히 검증되고 준비되어야 한다.&lt;/li&gt;
&lt;li&gt;만약 링크하는 도중 에러가 발생하면, 해당 에러와 관련이 있는 클래스나 인터페이스로 직접적으로든 간접적으로든 링크가 필요할 수도 있는 어떤 작업을 하는 지점에서 예외가 일어날 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크는 아래와 같이 3단계로 이루어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증(Verification)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스로더가 .class 파일의 바이트코드를 자바 언어 명세서(Java Language Specification)에 따라서 코드를 제대로 잘 작성했는지, JVM 규격에 따라 검증된 컴파일러에서 .class 파일이 생성되는지 등을 확인하여 .class 파일의 정확성을 보장합니다. 내부적으로 바이트코드 검증기(Bytecode verifier)가 이 과정을 담당합니다. 이 과정은 클래스를 로드하는 과정 중 가장 복잡한 테스트 과정이며, 가장 오랜 시간이 걸립니다. 링크로 인해 클래스를 로드하는 과정이 느려지지만 바이트코드를 실행할 때 이런 검사를 여러 번 수행할 필요가 없기 때문에 전반적으로 효율적이며 효과적입니다. 바이트코드 검증기는 검증이 실패하면 런타임 에러(java.lang.VerifyError)를 발생시킵니다. 예를 들어서, 아래와 같은 검사들을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심볼 테이블(symbol table)이 일관되고 올바른 형식인지 검사&lt;/li&gt;
&lt;li&gt;접근 지정자에 따른 접근 범위에서 메서드에 접근하고 있는지 검사&lt;/li&gt;
&lt;li&gt;메서드의 매개변수 수와 자료형이 올바른지 검사&lt;/li&gt;
&lt;li&gt;final 메서드와 클래스가 오버라이드 되지는 않았는지 검사&lt;/li&gt;
&lt;li&gt;변수를 읽기 전에 초기화되었는지 검사&lt;/li&gt;
&lt;li&gt;변수가 올바른 타입의 값인지 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;admonition hint&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;자바는 왜 안전한 언어인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 클래스 파일을 수동으로 변경하여 어떤 종류의 바이러스를 만들었다고 해봅시다. 그러면 바이트코드 검증기는 해당 클래스 파일을 검증된 컴파일러가 생성했는지에 대한 여부를 확인합니다. 만약 검증이 실패하면 런타임 에러 java.lang.VerifyError를 발생시키게 됩니다. 따라서 클래스 파일의 악의적인 혹은 유효하지 않은 변경을 미연에 방지할 수 있게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;준비(Preparation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 메서드 테이블 같이 JVM에서 쓰이는 자료구조나 정적 기억 영역(static storage)을 위해 메모리를 할당합니다. 이때 메모리가 부족하면 java.lang.OutOfMemoryError가 발생합니다. 이 단계에서 정적 필드가 만들어지고 기본값으로 초기화됩니다. 하지만 원래의 값은 초기화 단계에서 할당되므로 아직은 초기화 블록이나 초기화 코드는 실행되지 않습니다. 여기서 기본값은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_Class_P02.png&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VdGmM/btrCjFrxVM1/9M9IK8ZnXTQCM9NO1c23G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VdGmM/btrCjFrxVM1/9M9IK8ZnXTQCM9NO1c23G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VdGmM/btrCjFrxVM1/9M9IK8ZnXTQCM9NO1c23G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVdGmM%2FbtrCjFrxVM1%2F9M9IK8ZnXTQCM9NO1c23G0%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;593&quot; height=&quot;359&quot; data-filename=&quot;Attachments_Class_P02.png&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서, 아래와 같은 코드가 있으면 준비 과정에서 int형 정적 변수 a에 4바이트의 메모리 공간을 할당하고 기본값인 0으로 초기화합니다. 그리고 long형 정적 변수 b에는 8바이트의 메모리 공간을 할당하고 기본값인 0으로 초기화합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class Example {
    private static int a = 10;
    private static long b;

    // 정적 초기화 블록(static initialization block)
    static {
        b = 5;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해석(Resolution)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해석은 간단히 말하면 런타임 상수 풀(run-time constant pool)에 있는 심볼릭 참조(symbolic reference)를 직접 참조(direct reference)로 대체하는 과정입니다. 다시 말해서, 추상적인 기호를 구체적인 값으로 동적으로 결정하는 과정이라고 할 수 있습니다. JVM 명령인 anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, putstatic은 런타임 상수 풀에 있는 심볼릭 참조를 사용합니다. 이러한 명령어를 실행하려면 먼저 심볼릭 참조를 해석해야 합니다.&lt;/p&gt;
&lt;div class=&quot;admonition note&quot;&gt;
&lt;p class=&quot;admonition-title&quot; data-ke-size=&quot;size16&quot;&gt;심볼릭&amp;nbsp;참조(symbolic&amp;nbsp;reference)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심볼릭 참조는 참조된 항목에 관한 이름이나 기타 정보를 제공하는 문자열로, 실제 객체(변수, 메서드, 타입 등)를 가져오는 데 사용할 수 있습니다. 예를 들어서 아래의 코드를 생각해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1652680165215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (&quot;init&quot;.equals(opCode)) { /* ... */ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 바이트코드를 살펴보면 아래와 같을 것입니다. 여기서 #4를 보고 boolean java/lang/String.equals(Object)인지 어떻게 알 수 있을까요? 이를 가지고 런타임 상수 풀에 있는 심볼릭 참조를 통해 현재 클래스로더가 로드한 실제 클래스를 확인하고 클래스 인스턴스에 대한 참조를 반환하기 때문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652680204875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;3: ldc           #3   // String init
5: aload_1
6: invokevirtual #4   // Method java/lang/String.equals:(Ljava/lang/Object;)Z
9: ifeq          12&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상수 풀을 살펴보면 다음과 같습니다. 구체적인 값을 동적으로 결정한다는 것이, 프로그램 실행 중에 실제 객체를 결정한다는 것입니다. JVM은 이렇게 구한 직접 참조를 기억하고 있으므로, 다시 #4와 같은 참조를 맞닥뜨리는 경우 다시 심볼릭 참조를 가지고 직접 참조를 찾는 과정을 거치지 않아도 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652680242079&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Constant pool:
   #3 = String             #26       // init
   #4 = Methodref          #27.#28   // java/lang/String.equals:(Ljava/lang/Object;)Z
   ...
   #26 = Utf8               init
   #27 = Class              #31      // java/lang/String
   #28 = NameAndType        #32:#33  // equals:(Ljava/lang/Object;)Z
   ...
   #31 = Utf8               java/lang/String
   #32 = Utf8               equals
   #33 = Utf8               (Ljava/lang/Object;)Z&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계는 JVM 구현에 따라서 클래스를 검증할 때 한 번에 해석할 수도 있고(eager resolution), 당장 심볼릭 참조를 직접 참조로 바꿀 필요가 없다면 뒤로 밀려날 수 있습니다(lazy resolution). 따라서 준비 단계를 마치고 반드시 해석 단계가 일어나지는 않으며 이는 선택적 단계입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;초기화(Initialization)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 로드된 각 클래스나 인터페이스의 초기화 로직이 실행됩니다. 이 단계는 정적 변수는 코드에 명시된 원래 값이 할당되고, 정적 초기화 블록이 실행되는 클래스 로딩의 마지막 과정입니다. 이 작업은 클래스의 위에서 아래로, 클래스 계층 구조에서 부모에서 자식까지 한 줄씩 실행됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대략적인 흐름 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 아래와 같은 클래스가 어떤 과정을 거쳐서 로딩되는지 하나하나 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// Foo.java
package com.company;

public class Foo {
    int data;

    public Foo() { }

    public void doSomething() { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;클래스로더는&amp;nbsp;런타임에&amp;nbsp;com.company.Foo 클래스가 처음으로 참조될 때 애플리케이션 클래스로더가 이 클래스 로딩 요청을 받습니다. 클래스 로딩 과정은 자바독에 명시된 것처럼 아래와 같은 과정을 따릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로드하려는 클래스가 이미 로드된 것은 아닌지 확인한다.&lt;/li&gt;
&lt;li&gt;부모 클래스 로더의 loadClass() 메서드를 호출한다. 이때 부모 클래스로더가 없으면 JVM에 내장된 클래스 로더를 대신 사용한다.&lt;/li&gt;
&lt;li&gt;findClass(String) 메서드를 호출하여 클래스를 찾는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;protected Class&amp;lt;?&amp;gt; loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 첫째, 클래스가 이미 로드되지 않았는지 확인한다.
        Class&amp;lt;?&amp;gt; c = findLoadedClass(name);
        if (c == null) { // 로드된 게 없으면
            ...
            try {
                // 부모 클래스 로더에게 클래스 로딩 요청을 위임한다.
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // parent가 null이면 부트스트랩 클래스로더거나 
                    // 정말 부모 클래스로더가 없음을 의미한다.
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 클래스를 찾지 못하면 부모 클래스로더가
                // ClassNotFoundException 예외를 던진다.
            }

            if (c == null) {
                // 아직도 발견되지 않았으면 클래스를 찾기 위해서
                // findClass()를 호출한다.
                long t1 = System.nanoTime();
                c = findClass(name);
                ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 애플리케이션 클래스로더는 자신이 이미 해당 클래스를 로드했는지 확인하고 로드된 것이 없으면 클래스 로딩 요청을 부모 클래스로더인 확장 클래스로더에 위임합니다. 확장 클래스로더도 마찬가지로 클래스 로드 여부를 확인하고 없으면 부모인 부트스트랩 클래스로더로 요청을 위임합니다. 만약에 부트스트랩 클래스로더도 자신이 로드한 클래스 중 해당 클래스는 없다면 부트스트랩 클래스로더부터 시작하여 애플리케이션 클래스로더까지 자신의 클래스패스에서 클래스 파일(../com.company/Foo.class)을 찾기 시작합니다. 결국 애플리케이션 클래스로더가 자신의 클래스패스(예: IntelliJ 기준으로 /out/production/project-name)에서 클래스 파일을 발견하여 이 파일로부터 바이트 배열을 읽어들이고 해당 클래스 파일이 올바른 형식을 준수하고 있는지 빠짐없이 검증하기 시작합니다. 헥스 에디터로 Foo.class 파일을 열어보면 아래의 내용을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Attachments_JVM_P01 1.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b03b3W/btrB6JWphZ3/JOUnxP5akE0hK7lvRWn28K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b03b3W/btrB6JWphZ3/JOUnxP5akE0hK7lvRWn28K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b03b3W/btrB6JWphZ3/JOUnxP5akE0hK7lvRWn28K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb03b3W%2FbtrB6JWphZ3%2FJOUnxP5akE0hK7lvRWn28K%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;1308&quot; height=&quot;888&quot; data-filename=&quot;Attachments_JVM_P01 1.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 클래스 파일에 있는 명령어를 살펴보기 위해서 자바 클래스 파일 디스어셈블러(javap.exe) 혹은 &lt;a href=&quot;https://github.com/Konloch/bytecode-viewer&quot;&gt;바이트코드 뷰어&lt;/a&gt;, 아니면 &lt;a href=&quot;https://gcc.godbolt.org/&quot;&gt;컴파일러 익스플로러&lt;/a&gt;를 사용하면 쉽게 디스어셈블된 코드를 살펴볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class com.company.Foo {
  int data;

  public com.company.Foo(); // 클래스 Foo의 디폴트 생성자
    Code:
       // 여기서는 this를 피연산자 스택에 푸시한다.
       0: aload_0
       // 부모 클래스인 Object의 디폴트 생성자를 호출한다. (super())
       1: invokespecial #1                  // Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
       4: return

  public void doSomething();
    Code:
       0: return
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증이 안전하게 끝나면 그 다음은 준비 단계에서 클래스에 필요한 메모리 공간을 할당하고 정적 필드를 기본값으로 초기화합니다. Foo 클래스에 있는 인스턴스 필드 data의 초기화는 우리가 인스턴스를 만들 때 이루어집니다. 그리고 JVM 구현에 따라서 해석 단계를 거치거나 뒤로 지연될 수 있습니다. 마지막으로 초기화 단계를 거치게 되는데 이 단계에서는 Foo 클래스의 초기화 로직(예: 정적 초기화 블록 등)이 실행되고 클래스 로딩 과정이 끝나게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chapter 5. Loading, Linking, and Initializing, 2022&amp;nbsp;&lt;/a&gt;&lt;span style=&quot;color: #000000; --darkreader-inline-color: #e8e6e3;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/platform-engineer/understanding-jvm-architecture-22c0ddf09722&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Understanding&amp;nbsp;JVM&amp;nbsp;Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍 관련/자바</category>
      <category>java</category>
      <category>JVM</category>
      <category>검증</category>
      <category>로딩</category>
      <category>바이트코드</category>
      <category>서브시스템</category>
      <category>자바</category>
      <category>클래스로더</category>
      <category>해석</category>
      <author>LAYER6AI</author>
      <guid isPermaLink="true">https://exynoa.tistory.com/397</guid>
      <comments>https://exynoa.tistory.com/397#entry397comment</comments>
      <pubDate>Mon, 16 May 2022 14:29:34 +0900</pubDate>
    </item>
  </channel>
</rss>