<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>이로운 개발하기</title>
    <link>https://stir.tistory.com/</link>
    <description>불만하는 사람은 90명, 해결하는 사람은 9명, 리드하는 사람은 1명
음악과 낭만을 좋아합니다.</description>
    <language>ko</language>
    <pubDate>Wed, 13 May 2026 19:06:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>loose</managingEditor>
    <image>
      <title>이로운 개발하기</title>
      <url>https://tistory1.daumcdn.net/tistory/3487140/attach/6bd787e006fe4f8e9b987471c60bda56</url>
      <link>https://stir.tistory.com</link>
    </image>
    <item>
      <title>덜 신중하기</title>
      <link>https://stir.tistory.com/572</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Moltbot&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Moltbot 창시자인 Peter Steinberger의 AI 코딩 원칙이다.&lt;br&gt;&lt;br&gt;&lt;b&gt;1.&amp;nbsp;완벽주의를&amp;nbsp;버려야&amp;nbsp;AI와&amp;nbsp;일할&amp;nbsp;수&amp;nbsp;있다.&lt;/b&gt; &lt;br&gt;2.&amp;nbsp;AI가&amp;nbsp;스스로&amp;nbsp;검증하며&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;직접&amp;nbsp;수행하도록&amp;nbsp;설계한다. &lt;br&gt;3.&amp;nbsp;PR은&amp;nbsp;거절하고&amp;nbsp;아이디어만&amp;nbsp;프롬프트로&amp;nbsp;녹여낸다. &lt;br&gt;4.&amp;nbsp;코드&amp;nbsp;리뷰&amp;nbsp;대신&amp;nbsp;아키텍처&amp;nbsp;리뷰만&amp;nbsp;한다. &lt;br&gt;5.&amp;nbsp;몰입을&amp;nbsp;위해&amp;nbsp;AI를&amp;nbsp;병렬로&amp;nbsp;사용한다. &lt;br&gt;6.&amp;nbsp;계획&amp;nbsp;단계에&amp;nbsp;시간을&amp;nbsp;훨씬&amp;nbsp;많이&amp;nbsp;투입시킨다. &lt;br&gt;7.&amp;nbsp;필요하다면&amp;nbsp;모호하게&amp;nbsp;지시하여&amp;nbsp;창의성을&amp;nbsp;열어둔다. &lt;br&gt;8.&amp;nbsp;이제는&amp;nbsp;알고리즘을&amp;nbsp;좋아하는&amp;nbsp;개발자보다&amp;nbsp;제품&amp;nbsp;출시를&amp;nbsp;좋아하는&amp;nbsp;사람이&amp;nbsp;AI&amp;nbsp;적응에&amp;nbsp;유리하다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Google&lt;/b&gt;&lt;/h2&gt;&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=0nlNX94FcUE&quot; data-video-thumbnail=&quot;https://blog.kakaocdn.net/dna/1wJdE/dJMb8UHKIH6/AAAAAAAAAAAAAAAAAAAAAFGKBPyu_xeT13hWK6HVjg2SJvBaaec5CEaBWXEaCIQE/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1772290799&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=m0Tv2AIlaTIF9%2F%2FX4VDR8TiaHKY%3D&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-video-title=&quot;Big ideas begin here: Sergey Brin at Stanford&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/0nlNX94FcUE&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;구글의 창업자 세르게이 브린이 최근에 한 말이다.&lt;br&gt;세르게이 브린은 구글이 현대 AI의 기초가 된 &lt;b&gt;'트랜스포머(Transformer)'&lt;/b&gt; 논문을 세상에 내놓고도 주도권을 놓쳤던 과거를 솔직하게 반성했다.&lt;br&gt;구글이 당시에 Gemini와 같은 AI를 일찍 공개하지 않은 이유는 &lt;b&gt;'두려움'&lt;/b&gt; 때문이었다.&lt;br&gt;당시 구글 내부에서는 AI 챗봇이 엉뚱하거나 부적절한 답변을 할 경우 브랜드 이미지에 타격이 클 것을 우려해 출시를 망설였다.&lt;br&gt;그 사이 오픈AI(OpenAI)가 과감하게 치고 나간 것이다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Amazon&lt;/b&gt;&lt;/h2&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;1003&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oopzi/dJMcafS2Lqo/iYZODos9w7FI0cqL0VoCak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oopzi/dJMcafS2Lqo/iYZODos9w7FI0cqL0VoCak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oopzi/dJMcafS2Lqo/iYZODos9w7FI0cqL0VoCak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foopzi%2FdJMcafS2Lqo%2FiYZODos9w7FI0cqL0VoCak%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;584&quot; height=&quot;1003&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;1003&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;아마존에서는 입사 후 일주일 안에 코드 커밋을 한 사람이 장기적으로 훨씬 성장한다는 데이터가 있다고 한다.&lt;br&gt;소심하게 있고 완전히 능숙해지기 전까지(Than sitting back and wating to feel fully competent) 아무 것도 안하는 사람보다 일단 시도라도 해보는 사람이 성장한다는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 이런 글 저런 글들을 읽으면서 굴지의 기업들이라 불리는 곳에서 가져온 내용입니다.&lt;br&gt;여기서 공통된 주제를 찾을 수 있었는데 그것은 '과감함'입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 &quot;과감해져라&quot;라고 결론 지을거면 이 글을 쓰지도 않았습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;'과감함'이라는 개념이 세상에 없던 개념이 아닙니다.&lt;br&gt;왜 갑자기 이런 느낌의 글이나 말들이 각 회사들에서 떠돌게 된걸까요?&lt;br&gt;제 생각엔 '과감함'의 반대에 서 있는 '신중함'의 역할이 소멸되어가고 있다는 방증이기도 한 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;확실한건 AI의 등장으로 우리가 기존에 생각하고 신중했어야 할 문제들을 AI가 대신 해결해주고 있습니다.&lt;br&gt;그리고 그 결과에 대한 것만 검토하는 '날카로운 신중함'만 있으면 되는 것이죠.&lt;br&gt;아예 '신중함'이 필요없는 것이 아니라요.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 '날카로운 신중함'에 쏟았던 시간들을 제외하고 나서는 그 이외의 것에 투자하는 것이 중요한 세상이 오는 것 같습니다.&lt;br&gt;'과감함', '사람에 대한 이해'라든가요.&lt;br&gt;엔비디아의 젠슨 황도 이제는 사람을 이해하는 영역도 중요하다고 얘기한 것 처럼 말이죠.&lt;br&gt;&amp;nbsp;&lt;br&gt;직장은 말할 것도 없지만 사회 전반적인 영역에서 '신중함'이 필요 했던 영역들이 어떻게 바뀔지 상상은 안가지만 어떤 식으로라도 바뀌지 않을까 생각하고 있습니다.&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/572</guid>
      <comments>https://stir.tistory.com/572#entry572comment</comments>
      <pubDate>Sat, 7 Feb 2026 00:36:16 +0900</pubDate>
    </item>
    <item>
      <title>AI로 올바르게 학습하는 방법</title>
      <link>https://stir.tistory.com/571</link>
      <description>&lt;div style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-app=&quot;{&amp;quot;type&amp;quot;:&amp;quot;opengraph&amp;quot;,&amp;quot;openGraphData&amp;quot;:{&amp;quot;title&amp;quot;:&amp;quot;How AI Impacts Skill Formation&amp;quot;,&amp;quot;description&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;url&amp;quot;:&amp;quot;https://arxiv.org/html/2601.20245v1#S5&amp;quot;,&amp;quot;canonicalUrl&amp;quot;:&amp;quot;https://arxiv.org/html/2601.20245v1#S5&amp;quot;}}&quot;&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://arxiv.org/html/2601.20245v1#S5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://arxiv.org/html/2601.20245v1#S5&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 논문은 Claude를 개발한 Antrophic에서 2026년 1월 28일에 게재한 논문입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;'AI가 능력 개발에 끼치는 영향'에 대한 주제의 논문이고 이를 통해 개발 뿐만 아니라 AI를 통해 어떻게 올바르게 학습할 수 있는 것인지에 대한 정보를 공유하고자 올립니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;본 연구의 참가자들은 전문 프로그래머와 프리랜서 프로그래머입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;모든 참가자가 동일하게 모르고 있는 프로그래밍 지식에 대해 과제가 2개 주어지며 해당 과제를 AI와 협업하여 다양한 방법을 통해 학습 성과가 얼마나 차이나는지를 보여주고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;학습은 약 1시간 진행되며 실험이 끝나면 과제 해결 시간과 퀴즈를 통해 학습 성과를 &lt;span&gt;지표화 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 결과 &lt;b&gt;&lt;span style=&quot;color: #ec4c6a;&quot; data-fr-verified=&quot;true&quot;&gt;학습 &lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ec4c6a;&quot; data-fr-verified=&quot;true&quot;&gt;&lt;span&gt;성과가 &lt;/span&gt;&lt;span&gt;제일 &lt;/span&gt;&lt;span&gt;떨어지는 &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #ec4c6a;&quot; data-fr-verified=&quot;true&quot;&gt;것&lt;/span&gt;&lt;/b&gt; &lt;/span&gt;&lt;span&gt;부터 &lt;/span&gt;&lt;span&gt;하나씩 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;span&gt;반복적 AI 디버깅(Iterative AI Debugging)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AI 도움을 받아 반복적으로 문제 해결 또는 검증 (5~15회 쿼리)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 31분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 24%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;과제를 수행할 때 버그 발생 시 해당 부분에 대해 지속적으로 AI에게 반복적으로 도움을 받는 형식으로 진행하는 학습 과정입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;과제 해결 시간과 학습 성과가 가장 더딥니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 점진적 AI 의존(Progressive AI Reliance)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;설명: 첫 번째 과제에서는 질문을 하고, 두 번째 과제에서는 AI에 의존&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 22분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 35%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째 과제에 대해서는 질문을 하며 코드를 작성하고, 두 번째 과제에서는 전적으로 AI에 의존합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간은 줄고 학습 성과는 미약하게 상승한 것을 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. AI 위임(AI Delegation)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;설명: AI에게 코드 생성만 요청하고, 나온 코드를 그대로 답으로 붙여넣음&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 19.5분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 39%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;AI에게 코드 생성 전부를 맡기는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;모든 방법 중에 소요 시간은 가장 짧지만 학습 성과는 저조합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;하위 3위는 AI에 과도하게 의존하는 경향을 보였습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;상위 3위는 독립적인 사고를 한 경우에 학습 성과가 높았다는 점 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 개념 탐구(Conceptual Inquiry)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;설명: 오직 개념 관련 질문만 하고, 오류 해결은 독립적으로 수행&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 22분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 65%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개념에 관해서만 질문 하고 직접 과제를 독립적으로 수행합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 코드-설명 혼합(Hybrid Code-Explanation)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;설명: 코드 생성과 설명을 동시에 포함하는 질문&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 24분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 68%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 생성해달라고 요청하며 동시에 설명도 이해하는 방식으로 진행합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 생성 후 이해(Generation-Then-Comprehension)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;설명: AI로 코드 생성 후, 이해 중심의 질문 수행&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소요 시간: 24분&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 성과: 86%&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일단 전부 생성하고 이해 중심의 질문을 수행합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;압도적인 학습 성과와 소요 시간을 자랑합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 설명하자면 우리가 영어를 배우고 싶다고 한다면 ChatGPT에게 내가 생각한 문장을 하나 입력해서 이 문장 어색해? 라고 고쳐달라고 할게 아니라 일단 전체 대화본을 전부 적어달라고 한 뒤 해당 대화 본을 제가 이해하면서 그대로 타이핑하는게 지식을 얻는 효과가 더 두드러진다는 얘기입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/571</guid>
      <comments>https://stir.tistory.com/571#entry571comment</comments>
      <pubDate>Thu, 5 Feb 2026 22:52:49 +0900</pubDate>
    </item>
    <item>
      <title>작가가 되다</title>
      <link>https://stir.tistory.com/569</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpOHX4/dJMcabwgIYo/spoIpLTWkWikGsknYbjkf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpOHX4/dJMcabwgIYo/spoIpLTWkWikGsknYbjkf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpOHX4/dJMcabwgIYo/spoIpLTWkWikGsknYbjkf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpOHX4%2FdJMcabwgIYo%2FspoIpLTWkWikGsknYbjkf1%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;590&quot; height=&quot;392&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;브런치 스토리에서 작가가 되었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;누가 봐주는 걸 원하지도 않았고 모자라고 창피하지만 글을 쓰는 걸 좋아해서 쭉 써왔는데 작가라는 이름을 붙여주어서 괜히 기분이 좋았습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;앞으로 쓸 에세이들은 &lt;a href=&quot;https://brunch.co.kr/@82c176a6eeaa4b2&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;저의 브런치 스토리&lt;/span&gt;&lt;/a&gt;에서 작업 할 예정이에요.&lt;br&gt;더불어 블로그에 쓴 글 들도 하나하나 브런치 스토리에 올리면서 정교하게 다듬어가려고 합니다.&lt;br&gt;'카카오톡 더보기 - 브런치 스토리' 에서도 접속하여 확인 가능합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;감사했습니다.&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/569</guid>
      <comments>https://stir.tistory.com/569#entry569comment</comments>
      <pubDate>Sat, 31 Jan 2026 23:13:21 +0900</pubDate>
    </item>
    <item>
      <title>브람스를 좋아하세요&amp;hellip; - 프랑수아즈 사강</title>
      <link>https://stir.tistory.com/568</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;굴레&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;책을 덮으며 든 가장 첫 생각은 ‘이것은 무엇에 관한 기록인가’였다. 주인공 폴은 여러 관계를 거치며 자신의 존재를 소모하는 인물로 비춰졌기 때문이다. 전형적인 프랑스 여성의 삶을 거부하며 마르크와 이혼하고 독립을 쟁취했지만, 정작 로제와의 연애에는 고독한 독립만 있을 뿐 애정 어린 안정감은 없었다. 로제는 전형적인 회피형 인간의 정수였기 때문이다.&lt;br&gt;이때 등장한 14살 연하의 시몽은 폴의 결핍을 채워줄 헌신적인 사랑을 쏟아붓는다. 폴은 그에게서 충분한 애정을 얻지만, 젊은 시몽이 주는 열정 이면의 무게감과 책임감을 견디지 못하고 결국 다시 로제에게 돌아간다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;저녁 8시, 전화벨이 울렸다. 수화기를 들기도 전에 그녀는 로제가 무슨 말을 하려는지 알 수 있었다.&lt;br&gt;“미안해, 일 때문에 저녁 식사를 해야 해. 좀 늦을 것 같은데…”&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;소설의 끝에서 다시 울리는 로제의 전화는 비극적이다. 로제는 폴이라는 안식처를 되찾자마자 다시 다른 여자를 만나러 눈을 돌린다.&lt;br&gt;폴은 결국 자신을 파괴하는 익숙함을 선택하며 다시 한번 스스로에게 굴레를 씌우는 것으로 소설은 끝이 난다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;나는 누구인가&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;책의 제목인 ‘브람스를 좋아하세요…’는 시몽이 폴에게 던진 질문이다.&lt;br&gt;이 질문 앞에서 폴은 깊은 당혹감을 느낀다.&lt;br&gt;정작 자신이 무엇을 좋아하는지조차 모르는 사람이 되어버렸음을 깨달았기 때문이다. 이것이 소설 전체를 관통하는 주제다. 폴은 자신의 취향과 자아를 잃어버린 채, 자신의 결핍을 타인을 통해 채우는 데 급급해 보였다.&lt;br&gt;&lt;br&gt;독자로서 “이제 그만할 때도 되지 않았나” 싶을 정도로 그녀는 멈추지 않고 외부로부터의 구원을 갈구한다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;일체유심조&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;폴은 진정 무엇을 원했던 것일까?&lt;br&gt;마르크에게서는 ‘사회가 바라는 증명’을 얻었지만 ‘자유로운 나’를 꿈꿨고&lt;br&gt;로제에게서는 ‘자유로운 나’를 얻었지만 ‘혼자가 아닌 나’를 찾으려 했고&lt;br&gt;시몽에게서는 ‘혼자가 아닌 나’를 얻었지만 ‘막중한 책임감으로부터의 회피’를 원했다.&lt;br&gt;&lt;br&gt;그리하여 폴은 책임감으로부터 벗어나게 도와줬던 로제에게, 그리고 ‘결핍이 해결되었으리라 착각했던 그때의 나’를 꿈꿨기에 다시 로제에게로 돌아간 것이다.&lt;br&gt;모든 인간은 관계 속에서 고통받지만 사회적 동물로서 타인을 떠나 살 수는 없다. 여기서 불교의 ‘일체유심조(一切唯心造)’를 떠올려 본다. 모든 고통과 외로움은 결국 내 마음에서 비롯된다는 가르침이다. 폴은 자신의 결핍을 스스로 마주하지 못한 채 외부에서만 답을 찾으려 했던 태도에서 기인한다.&lt;br&gt;타인에게서만 필요를 충족하려 할 때 인간은 예속될 수밖에 없다. 진정으로 우뚝 서기 위해서는 내면의 결핍을 스스로 다독여야 한다. 폴은 결핍을 외부에서 ‘수혈’받으려 했기에 결국 공허한 회귀를 반복한 것이다.&lt;br&gt;이제 나에게 다시 묻는다.&lt;br&gt;“나는 무엇을 좋아하는가?”&lt;br&gt;“폴처럼 누군가로부터 무언가를 얻길 원하고 바라진 않는가?”&lt;br&gt;“내 인생의 운전대를 쥐고 있는 것은 누구인가?”&lt;br&gt;“누군가와 서로 의지하면서 운전 할 남는 에너지가 있는가?”&lt;br&gt;이 책은 나에게 이토록 다양한 질문들을 던지며 삶의 주체와 관계의 본질에 대해 깊이 사유하게 하는 힘이 있었다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br70a8/dJMcaconWOH/X8IH4yOOgjkketJ73xbrt0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br70a8/dJMcaconWOH/X8IH4yOOgjkketJ73xbrt0/img.jpg&quot; data-alt=&quot;국중박 갔다온 김에 찍은 반가사유상&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br70a8/dJMcaconWOH/X8IH4yOOgjkketJ73xbrt0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr70a8%2FdJMcaconWOH%2FX8IH4yOOgjkketJ73xbrt0%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;4284&quot; height=&quot;5712&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;국중박 갔다온 김에 찍은 반가사유상&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/568</guid>
      <comments>https://stir.tistory.com/568#entry568comment</comments>
      <pubDate>Mon, 26 Jan 2026 17:55:00 +0900</pubDate>
    </item>
    <item>
      <title>구의 증명 - 최진영</title>
      <link>https://stir.tistory.com/566</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 소설은 예술 소설에 가깝다고 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 자신이 우울하면 우울할 수록 이 책을 더 깊이 이해할 수 있을 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해리 장애&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해리 장애란 극심한 스트레스나 고통스러운 경험으로 인해 의식, 기억, 정체감 등이 붕괴되는 상태를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 전반적인 분위기는 마치 해리 장애를 겪는 사람의 내면을 그대로 옮겨 적은 듯한 느낌을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실과 감정, 자아의 경계가 흐려지고, 무엇이 정상이고 무엇이 비정상인지조차 구분되지 않는 상태 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만약 네가 먼저 죽는다면 나는 너를 먹을 거야. 그래야 너 없이도 죽지 않고 살 수 있어.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;사랑하는 사람을 먹는다&amp;rsquo;는 극단적인 설정 속에서 이 소설이 보여주는 감정은 사랑 같으면서도 사랑이 아니고, 집착 같으면서도 단순한 집착으로 설명되지 않는다.&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;br /&gt;이 세계에서는 희망도 절망도 명확하게 느껴지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;753&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;마치 이 혼란스러운 마음의 끝이 어디인지 끝까지 확인하고 싶다는 듯 이야기는 끝까지 엉망진창으로 마무리된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;엉망진창의 모순&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;이 책을 읽고 불쾌함을 느낀 독자도 많은 것 같다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;그러나 개인적으로 나는 이런 소설이 오히려 좋은 소설이라고 느낀다.&lt;br /&gt;어딘가에는 분명 이런 정신적 고통을 안고 살아가는 사람들이 존재할 것이다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;그게 잠깐일지라도 말이다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;누구나 쉽게 겪을 수 있는 예를 들면 내가 가장 사랑하는 누군가 세상을 떠났을 때 극심한 스트레스를 겪기도 한다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;오직 나와 사랑하는 사람 둘 뿐이라고 여기던 세계에서 사랑하는 사람이 내 옆에서 갑자기 죽었다고 생각하면 나는 어떤 생각을 할까.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;울다가 지쳐서 사망신고서를 작성하러가게될까, 머리를 벽에 찧으며 슬픔을 달랠까, 화장을 하든 묻든 해야하는데 그런건 또 어떻게 하랴 소중한 사람이 바스라지는 것을 어떻게 상상할 수 있을까.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;그런 측면에서 보면 이 소설의 감정은 어느정도 공감이 가능하다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;자아가 붕괴되는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 이런 스트레스는 개인의 정신적인 문제이든 불가항력적인 사회적 현상으로 인해서든 상관없이 나타난다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 고통 속에서도 사랑은 꿈틀댄다.&lt;/p&gt;
&lt;p data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-size=&quot;size16&quot;&gt;내가 내가 아닌 것 같다고 해서 사랑을 못하라는 법이 있으랴.&lt;/p&gt;
&lt;blockquote data-end=&quot;965&quot; data-start=&quot;774&quot; data-ke-style=&quot;style2&quot;&gt;행복하자고 같이 있자는 게 아니야. 불행해도 괜찮으니까 같이 있자는 거지.&lt;/blockquote&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;분명 구와 담은 성장해나가는 올바른 사랑을 했다고는 볼 수 없을 것이다.&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;한편으론 집착일 수도 있고, 불행을 같이 떠안는 것을 마치 사랑으로 포장하는 것 같기도 하다.&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;하지만 세상엔 어쩔 수 없는 감정이란 법도 있다.&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;유일하게 내가 이성을 버리고 지켜야하는 무언가도 있을 것이다.&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;소설이 꼭 아름답고 예쁜 이야기만 전해야 할까.&lt;br /&gt;불편하고 극단적이기에 오히려 소설다운 작품이며, 누군가에게는 위로가 될 수도 있다고 생각한다.&lt;/p&gt;
&lt;p data-end=&quot;1104&quot; data-start=&quot;967&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;현실에서는 쉽게 말할 수 없는 감정과 생각의 그 이상을 표현해낸 작품이 바로 '구의 증명'이다.&lt;/p&gt;
&lt;p data-end=&quot;1198&quot; data-start=&quot;1106&quot; data-ke-size=&quot;size16&quot;&gt;내용이 지나치게 기괴해 불쾌감을 준다는 점은 분명하지만 그 기괴함이 오히려 이 책의 메시지가 아닐까.&lt;/p&gt;
&lt;p data-end=&quot;1198&quot; data-start=&quot;1106&quot; data-ke-size=&quot;size16&quot;&gt;독자들이 느끼기에 &quot;난 이 정도는 아니다&quot; 싶을테니까 말이다.&lt;/p&gt;
&lt;p data-end=&quot;1198&quot; data-start=&quot;1106&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 구와 담은 당신의 추악한 모습보다 그 이상으로 대신 엉망진창이 되어 당신의 추악함을 별거 아니라는 듯 위로해준 모순이 아니겠는가.&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/566</guid>
      <comments>https://stir.tistory.com/566#entry566comment</comments>
      <pubDate>Fri, 23 Jan 2026 00:37:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] PQC(Post-Quantum Cryptography)</title>
      <link>https://stir.tistory.com/563</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;PQC의 필요성&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQC가 필요한 이유는 양자 컴퓨터가 RSA 알고리즘을 무력화시킬 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PQC는 양자 내성 암호(Post-Quantum Cryptography)의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Qiskit을 이용해서 IBM의 양자 컴퓨터를 이용했을 때 당시에는 지식이 모자라 양자 컴퓨터로만 구현이 가능한 BB84 알고리즘만이 유일한 해결 방안이지 않을까 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 PQC라는 개념이 있었으니 역시 당시에는 지식 부족이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 양자 내성 암호는 양자 컴퓨터가 아니어도 충분히 구현할 수 있는 개념이다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;PQC란&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차세대로 나올 JEP에서는 PQC를 포함하고 있지만 프로덕션 환경에서 이용하려면 아직은 한참 시기상조다.&lt;br /&gt;그럼 이용을 못하는 것인가? 그건 또 아닌게 현재는 Bouncy Castle의 PQC를 사용하는 것은 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bouncy Castle의 PQC를 이용하기 전 PQC가 어떤 의미를 가지고 있는지 일단 대략 알아볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 설명이 어질어질할테니 일단 대충 흐린 눈  &amp;zwj;  으로 봐도 된다.&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;해당 라이브러리에서 대표적으로 사용하는 PQC는 대표적으로 CRYSTALS-Kyber와 CRYSTALS-Dilithium 알고리즘을 사용한다.&lt;br /&gt;CRYSTALS는&amp;nbsp;Cryptographic&amp;nbsp;Suite&amp;nbsp;for&amp;nbsp;Algebraic&amp;nbsp;Lattices의&amp;nbsp;약자인데,&amp;nbsp;대수(代數)적&amp;nbsp;격자를&amp;nbsp;기반으로&amp;nbsp;한&amp;nbsp;암호&amp;nbsp;알고리즘들의&amp;nbsp;모음이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 핵심은 '격자 알고리즘'을 사용한다는 것이다.&lt;br /&gt;'격자'는&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;쉽게 말해서 바둑판 형태의 격자에서 특정 점 A로부터 B까지의 최단 거리를 구하는 알고리즘이 가장 쉬운 예제라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 바둑판 격자는 2차원인데 이 격자를 300차원으로 늘리면 계산 해야하는 방식은 엄청나게 복잡해질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 특성을 이용하여 양자컴퓨터가 쉽게 분석할 수 없도록 구현한 것이 격자 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까진 그래도 개념이 쉬운 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일반적인 격자도 충분히 차원을 늘려서 알고리즘을 만들 수는 있지만 여기서 PQC는 대수라는 개념이 들어간다.&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;br /&gt;쉽게 말하면 단순히 정수로 표현하는 것이 넘어서 대체하는 수(x, y와 같은 대체 문자)를 사용하는 수학을 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대수적 격자 기반 알고리즘은 대수학 중에 추상 대수학(&lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: start;&quot;&gt;abstract algebra&lt;/span&gt;)을 접목시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상대수학에서는 환, 체와 같은 개념이 있는데 그 중에서 '환(Ring)'이라는 개념이 일반 격자 알고리즘에 추가되는 것이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환을 설명하기 위한 가장 쉬운 예시는 더하기와 곱하기가 가능한 숫자들의 모음을 말할 수 있다.&lt;br /&gt;대표적으로&amp;nbsp;정수다.&amp;nbsp;정수와&amp;nbsp;정수를&amp;nbsp;곱해도&amp;nbsp;정수의&amp;nbsp;숫자&amp;nbsp;범위에&amp;nbsp;포함하기에&amp;nbsp;환이라고&amp;nbsp;불린다.&lt;br /&gt;일반적&amp;nbsp;격자는&amp;nbsp;규칙에&amp;nbsp;의해&amp;nbsp;덧셈&amp;nbsp;규칙은&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있지만&amp;nbsp;곱셈&amp;nbsp;규칙까지&amp;nbsp;추가하여&amp;nbsp;'환'이라는&amp;nbsp;대수적&amp;nbsp;특징을&amp;nbsp;결합한&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;곱셈 규칙을 만족하는 모든 원소로 이루어진 격자로 인해 알고리즘을 구현할 때 속도가 빨라진다는 점이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 대수적 격자는 대수적 규칙에 의해 정의된 공리에 의해 추출된 격자를 사용하는 것이 대수적 격자라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘기가 돌고 돌았을 수 있지만 복잡한 격자를 만드는 것은 일반적 격자를 만드는 것에도 충분히 적용시킬 수 있지만 대수적 격자를 쓰는 이유는 복잡한 격자를 만들기 위해서는 아니고 연산을 빠르게 하기 위해서다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CRYSTALS-Kyber와 CRYSTALS-Dilithium라는 용어에서 뒤에 Kyber와 Dilithium은 각각 스타워즈, 스타트렉에 나오는 가상 원소이며 복제가 불가능하며 안전한 성질을 띈 원소를 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 안전하다라는 의미를 붙인 것 그 이상 이하도 아니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 PQC 사용 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Kyber KeyPair 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768908014493&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class KyberKeyPairGenerator {

    public static KeyPair generate() throws Exception {
        Security.addProvider(new BouncyCastlePQCProvider());

        KeyPairGenerator kpg =
                KeyPairGenerator.getInstance(&quot;Kyber&quot;, &quot;BCPQC&quot;);

        // Kyber512 / Kyber768 / Kyber1024 중 선택
        kpg.initialize(512);

        return kpg.generateKeyPair();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; Encapsulation &lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768908037385&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class KyberEncapsulation {

    public static void encapsulate(KeyPair keyPair) throws Exception {
        Security.addProvider(new BouncyCastlePQCProvider());

        KEMGenerator kemGen =
                KEMGenerator.getInstance(&quot;Kyber&quot;, &quot;BCPQC&quot;);

        // AES용 공유 비밀 생성
        KEMGenerateSpec spec =
                new KEMGenerateSpec(keyPair.getPublic(), &quot;AES&quot;);

        var kemResult = kemGen.generateEncapsulated(spec);

        SecretKey sharedSecret = kemResult.getKey();
        byte[] encapsulation = kemResult.getEncapsulation();

        System.out.println(&quot;Shared Secret: &quot; + sharedSecret.getEncoded().length);
        System.out.println(&quot;Encapsulation: &quot; + encapsulation.length);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; Decapsulation &lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768908048493&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class KyberDecapsulation {

    public static SecretKey decapsulate(
            KeyPair keyPair,
            byte[] encapsulation
    ) throws Exception {

        Security.addProvider(new BouncyCastlePQCProvider());

        KEMExtractor extractor =
                KEMExtractor.getInstance(&quot;Kyber&quot;, &quot;BCPQC&quot;);

        KEMExtractSpec spec =
                new KEMExtractSpec(keyPair.getPrivate(), encapsulation, &quot;AES&quot;);

        return extractor.extractSecretKey(spec);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Code In Service&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768908090033&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class KyberService {

    public KeyPair generateKeyPair() throws Exception {
        return KyberKeyPairGenerator.generate();
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/563</guid>
      <comments>https://stir.tistory.com/563#entry563comment</comments>
      <pubDate>Tue, 20 Jan 2026 20:26:34 +0900</pubDate>
    </item>
    <item>
      <title>PQC(Post Quantum Cryptography) in Java</title>
      <link>https://stir.tistory.com/562</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;2026년 새 사업의 일환으로 양자 내성 암호의 시작을 알리는 소개글로 시작한다.&lt;br&gt;&lt;br&gt;Java 24(2025년 3월)에 도입된 두 개의 새로운 JEP는 자바에서 양자 컴퓨터의 공격으로 부터 막기 위한 포스트 양자 암호(Post-Quantum Cryptography, PQC)라는 주제를 다룬다. &lt;br&gt;해당 JEP는 다음과 같다.&lt;br&gt;JEP 496 – 양자 내성 모듈 격자 기반 키 캡슐화 메커니즘&lt;br&gt;JEP 497 – 양자 내성 모듈 격자 기반 디지털 서명 알고리즘&lt;br&gt;이상적인 암호 시스템은 두 당사자가 사전에 비공개 채널을 통해 키를 교환하는 방식이지만, 이는 대규모 환경이나 장거리 통신, 혹은 사전에 서로를 알지 못하는 당사자 간의 통신에서는 현실적으로 작동하지 않는다.&lt;br&gt;이 문제를 해결하기 위해 공개 키 암호화는 일방향 함수(one-way function), 또는 트랩도어 함수(trapdoor function)를 활용한다. &lt;br&gt;이를 통해 수신자는 송신자가 키(또는 메시지)를 암호화하는 데 사용할 수 있는 정보를 공개할 수 있지만, 해당 정보를 가진 제3자는 이를 복호화할 수 없다.&lt;br&gt;현재 사용되는 암호학적 보호 기법은 큰 수의 소인수분해와 같은 이산 수학 문제가 계산적으로 매우 어렵다는 가정에 기반한다. 숫자를 곱하는 것은 쉽지만 이를 소인수분해하는 것은 어렵다는 점이 일방향 함수의 핵심이며, 이것이 암호화에 유용한 이유다.&lt;br&gt;현재로서는 알려진 소인수분해 알고리즘 대부분이 계산 난이도 면에서 사실상 무차별 대입(brute force)과 크게 다르지 않다.&lt;br&gt;이러한 가정 덕분에 암호학자들은 키 캡슐화 메커니즘(KEM)을 깨고 대칭 키를 복구하는 데 필요한 계산 능력을 비교적 정확하게 추정할 수 있다. 그 결과, 무어의 법칙과 같은 컴퓨팅 성능 향상 추세를 고려하더라도 사용자들은 통신 보안에 대해 신뢰를 가질 수 있다.&lt;br&gt;그러나 최근 몇 년 사이 양자역학적 특성을 활용하는 컴퓨터가 등장하기 시작했다. 이때 핵심 개념은 큐비트(qubit)로, 이는 단순히 0 또는 1 중 하나가 아니라 두 상태의 확률적 결합을 동시에 나타낸다. 양자 컴퓨팅은 큐비트를 어느 한 상태로 확정(이를 “붕괴(collapse)”라고 함)하지 않은 채로 연산을 수행한다.&lt;br&gt;양자 컴퓨팅에 대한 추가적인 배경 지식은 전문 자료를 참고해야 한다.&lt;br&gt;미래의 대규모 양자 컴퓨터는 쇼어 알고리즘(Shor’s algorithm)과 같은 새로운 기법을 활용해 이산 로그 문제를 해결할 수 있으며, 그 결과 RSA(Rivest–Shamir–Adleman)나 디피-헬만(Diffie-Hellman), 심지어 타원 곡선 알고리즘을 포함한 널리 사용되는 공개 키 기반 알고리즘의 보안을 무력화할 수 있다.&lt;br&gt;&lt;br&gt;반면 ML-KEM은 미래의 양자 컴퓨팅 공격에도 안전하도록 설계되었다. 이는 “어려운 문제”에 대해 완전히 다른 접근 방식을 사용하기 때문이다. ML-KEM은 미국 국립표준기술연구소(NIST)에 의해 FIPS 203으로 표준화되었다.&lt;br&gt;ML-KEM은 격자 암호(lattice cryptography)의 한 형태를 사용한다. 이는 n차원 공간에 규칙적으로 배치된 점들로 구성된 n차원 격자를 활용하는 방식이다. 이 점들은 벡터로 볼 수 있으며, 서로 더해질 수 있어 이동군(translation group)을 형성한다.&lt;br&gt;이러한 수학적 기법을 활용해 안전한 암호 알고리즘을 만드는 강력한 방법 중 하나가 오류를 포함한 학습(Learning With Errors, LWE)이다. 이는 오류가 포함된 방정식 집합으로 비밀 정보를 표현하는 아이디어에 기반하며, 대략 2010년 이후 수학 연구 커뮤니티에서 본격적으로 등장했다.&lt;br&gt;현재로서는 대규모 양자 컴퓨터가 아직 존재하지 않는다. 가장 진보된 실험실 수준의 시스템이 최근 처음으로 RSA 암호에 적용되었지만, 이는 양자 기법을 사용해 50비트 정수를 소인수분해한 수준에 불과하다. 이는 실제 운영 시스템에서 사용하는 키 길이에 비하면 매우 짧다. 예를 들어 일반적인 자바 애플리케이션은 2048비트 키를 사용하는데, NIST는 이 키 길이가 최소 2030년까지 충분하다고 보고 있다.&lt;br&gt;그럼에도 불구하고 미국 정부는 민감한 정보를 처리하는 컴퓨터 시스템이 향후 10년 내에 ML-KEM과 기타 예정된 표준을 사용하도록 업그레이드해야 한다고 의무화했다. 예를 들어 NSA는 늦어도 2033년까지 완전한 포스트 양자 체계로 전환할 계획이다.&lt;br&gt;2024년 기준으로, 이론적으로는 국가 단위의 공격자가 향후 충분히 강력한 양자 컴퓨터가 등장할 것을 대비해, 현재의 암호화된 트래픽을 대량으로 수집·저장하고 있다가 미래에 이를 복호화할 가능성도 있다.&lt;br&gt;다시 말해, 즉각적인 위협은 존재하지 않는다. 이 표준들은 미래를 대비하고, 오늘날 전송되고 있는 취약한 트래픽의 양을 최소화하기 위한 것이다.&lt;br&gt;앞으로의 길은 결코 명확하지 않다. 한편으로는 오늘날의 키를 위협할 수준의 대규모 양자 컴퓨터를 만드는 데 심각한 기술적 난관이 존재하며, 이로 인해 실용적인 양자 해독기의 등장이 지연될 수도 있다. 다른 한편으로는 새로운 기술이 등장해 그 시점을 예상보다 훨씬 앞당길 가능성도 있다.&lt;br&gt;마지막으로 고려해야 할 점은, 인터넷 보안 표준의 역사에서 상호운용성 문제와 명확한 마이그레이션 경로의 부재, 이른바 프로토콜 고착화(protocol ossification) 문제가 반복적으로 나타났다는 사실이다. 이것 또한 지금부터 이러한 작업을 시작해야 하는 또 하나의 이유다.&lt;br&gt;Cloudflare의 최근 블로그 글은 TLS 1.3을 배포한 경험을 바탕으로, 현재의 PQC 상태와 이러한 상호운용성 문제를 함께 논의하고 있다.&lt;br&gt;어쨌든 자바는 수명과 보급 측면에서 매우 장기적인 플랫폼이므로, 완전한 표준화 이전이라 하더라도 포스트 양자 기능을 지원하기 시작하는 것이 필요하다. 이 두 개의 JEP는 고전적인 공개 키 암호를 넘어서는 세계로 나아가는 자바의 첫걸음을 의미한다.&lt;/p&gt;</description>
      <category>☕ Java</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/562</guid>
      <comments>https://stir.tistory.com/562#entry562comment</comments>
      <pubDate>Thu, 15 Jan 2026 11:58:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 이중 트랜잭션으로 인한 PessimisticLockingFailureException</title>
      <link>https://stir.tistory.com/557</link>
      <description>&lt;p data-end=&quot;343&quot; data-start=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;최근에 &lt;b&gt;PessimisticLockingFailureException&lt;/b&gt; 예외를 마주쳤다.&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;이번에 내가 겪은 사례는 조금 특이한 구조에서 발생했다.&lt;/p&gt;
&lt;h2 data-end=&quot;362&quot; data-start=&quot;350&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상황 요약&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;506&quot; data-start=&quot;364&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;364&quot;&gt;A 프로젝트가 B 프로젝트의 API를 호출한다.&lt;/li&gt;
&lt;li data-end=&quot;418&quot; data-start=&quot;393&quot;&gt;A와 B &lt;b&gt;모두 트랜잭션을 사용&lt;/b&gt;한다.&lt;/li&gt;
&lt;li data-end=&quot;506&quot; data-start=&quot;419&quot;&gt;B에서 JPA로 &lt;b&gt;flush()&lt;/b&gt; 를 수행했는데 이 시점에 &lt;b&gt;PessimisticLockingFailureException&lt;/b&gt; 이 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;692&quot; data-start=&quot;679&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;717&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;이중 트랜잭션&lt;/b&gt; 구조에 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;928&quot; data-start=&quot;719&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;752&quot; data-start=&quot;719&quot;&gt;A 프로젝트에서 트랜잭션이 열린 상태로 B를 호출한다.&lt;/li&gt;
&lt;li data-end=&quot;776&quot; data-start=&quot;753&quot;&gt;B에서도 트랜잭션이 별도로 시작된다.&lt;/li&gt;
&lt;li data-end=&quot;828&quot; data-start=&quot;777&quot;&gt;B에서 JPA가 엔티티를 비관적 락(PESSIMISTIC_WRITE)으로 조회한다.&lt;/li&gt;
&lt;li data-end=&quot;928&quot; data-start=&quot;829&quot;&gt;&lt;b&gt;flush() 시점에 락을 걸 수 없어서&lt;/b&gt; 예외 발생:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;928&quot; data-start=&quot;869&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;893&quot; data-start=&quot;869&quot;&gt;A 쪽 트랜잭션이 락을 보유하고 있거나,&lt;/li&gt;
&lt;li data-end=&quot;928&quot; data-start=&quot;897&quot;&gt;DB가 락 획득에 실패하거나 타임아웃이 발생한 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;930&quot; data-ke-size=&quot;size16&quot;&gt;이런 구조는 DB 입장에서는 &lt;b&gt;두 개의 독립된 트랜잭션&lt;/b&gt;이 같은 자원에 접근하려고 하는 것으로 보이기 때문에 충돌이 난다.&lt;/p&gt;
&lt;h2 data-end=&quot;1019&quot; data-start=&quot;1007&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방안&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 방법이 있겠지만 한쪽 Transaction을 해제하는 것으로 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 별거 아니라고 생각하지만 이거 때문에 삽질할 초보 개발자 분들을 위해 정리 해놓는다.&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/557</guid>
      <comments>https://stir.tistory.com/557#entry557comment</comments>
      <pubDate>Mon, 26 May 2025 23:42:11 +0900</pubDate>
    </item>
    <item>
      <title>SAGA 패턴, JTA 트랜잭션</title>
      <link>https://stir.tistory.com/552</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처를 도입하면 시스템은 점점 더 작고 독립된 서비스들의 집합으로 나뉘게 됩니다. 하지만 이로 인해 트랜잭션 처리는 복잡해집니다. 단일 DB 트랜잭션으로는 처리할 수 없는 문제가 발생하죠. 이 글에서는 마이크로서비스 환경에서 트랜잭션을 다루는 대표적인 방식인 Saga 패턴과 JTA를 비교하고, 코레오그래피 기반의 Saga 구현 방식도 함께 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SAGA&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Saga는 여러 개의 로컬 트랜잭션을 순차적으로 실행하면서 전체 작업을 처리하는 방식입니다. 각 단계가 성공하면 다음 단계로 넘어가고, 실패하면 이미 성공한 단계들에 대해 보상 트랜잭션(compensating transaction)을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748529706656&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderSagaService {

    @Autowired private OrderService orderService;
    @Autowired private PaymentService paymentService;
    @Autowired private InventoryService inventoryService;

    public void createOrderSaga(String orderId) {
        try {
            orderService.createOrder(orderId); // A
            paymentService.pay(orderId);       // B
            inventoryService.deductStock(orderId); // C

            System.out.println(&quot;✅ 주문 전체 처리 성공&quot;);

        } catch (Exception e) {
            System.out.println(&quot;❌ 오류 발생: &quot; + e.getMessage());

            // 보상 트랜잭션 (역순으로 호출)
            inventoryService.cancelStock(orderId); // optional
            paymentService.refund(orderId);
            orderService.cancelOrder(orderId);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orchestration 방식에서는 중앙 컨트롤러(예: OrderSagaService)가 모든 흐름을 제어합니다. 반면, Choreography 방식에서는 각 서비스가 이벤트를 구독하고, 반응적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Saga에서 Kafka를 이용해서 구현하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JTA&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JTA(Java Transaction API)는 2PC 기반으로 여러 리소스를 하나의 트랜잭션으로 묶을 수 있어 신뢰성은 높지만, 설정이 복잡하고 무거워 &lt;b&gt;모놀리식 또는 단일 JVM 환경&lt;/b&gt;에 적합하며, &lt;b&gt;마이크로서비스 환경&lt;/b&gt;에는 잘 맞지 않습니다.&lt;/p&gt;</description>
      <category>  Distributed System</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/552</guid>
      <comments>https://stir.tistory.com/552#entry552comment</comments>
      <pubDate>Sat, 29 Mar 2025 18:30:12 +0900</pubDate>
    </item>
    <item>
      <title>[ElasticSearch] 검색 방식, 장애 복구 정리(Circuit Breaker Exception)</title>
      <link>https://stir.tistory.com/551</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;검색 방식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;from 방식&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1748269051387&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET my_index/_search
{
  &quot;from&quot;: 100000,
  &quot;size&quot;: 10,
  &quot;sort&quot;: [{ &quot;timestamp&quot;: &quot;asc&quot; }]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;이 요청은 100,000번째 문서부터 10개를 반환하는 것이 목적입니다.&lt;br /&gt;&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지를 쉽게 이동할 수 있으나 깊은 페이지일수록 느립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;Scroll API&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1748269184509&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST my_index/_search?scroll=1m
{
  &quot;size&quot;: 1000,
  &quot;sort&quot;: [
    { &quot;timestamp&quot;: &quot;asc&quot; }
  ],
  &quot;_source&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이전 페이지 한 개만 유지하는 것이 아니라, 처음 요청 이후 모든 스냅샷 데이터를 유지하므로 백업 용도가 아니면 보통 사용하지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Search After&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1748269273992&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST my_index/_search
{
  &quot;size&quot;: 10,
  &quot;sort&quot;: [
    { &quot;timestamp&quot;: &quot;asc&quot; },
    { &quot;_id&quot;: &quot;asc&quot; }
  ],
  &quot;search_after&quot;: [1685081830000, &quot;doc-id-123&quot;],
  &quot;_source&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 대용량 검색에서 페이지네이션이 필요할 땐 Search After를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sort 값을 필수로 지정해줘야 하며 text 값을 sort값으로 지정할 시 field data cache값이 과도하게 할당될 수 있으므로 .keyword를 이용해서 field data caching을 피해야한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;장애 복구(개발계)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;{&quot;statusCode&quot;:401,&quot;error&quot;:&quot;Unauthorized&quot;,&quot;message&quot;:&quot;[circuit_breaking_exception] [parent] Data too large, data for [&amp;lt;http_request&amp;gt;] would be [1036689400/988.6mb], which is larger than the limit of [1020054732/972.7mb], real usage: [1036689400/988.6mb], new bytes reserved: [0/0b], usages [request=0/0b, fielddata=462101243/440.6mb, in_flight_requests=0/0b], with { bytes_wanted=1036689400 &amp;amp; bytes_limit=1020054732 &amp;amp; durability=\&quot;PERMANENT\&quot; }&quot;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발계에서 개발하다보면 Circuit Breaker Exception을 종종 마주치곤한다.&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;우선 Elastic Search는 JVM의 heap 메모리를 할당하는데, 만약 개발을 위해서 1GB로 heap을 설정하는 경우 아무 것도 동작시키지 않아도 상시 40% ~ 75%을 heap 메모리가 할당되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 명령어로 충분히 확인해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1748269503911&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET _nodes/stats/os&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제는 70%를 넘어가는 순간 Circuit Breaker Exception이 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 heap 사용량이 70%로 Default로 지정되어있기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1748269636439&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PUT _cluster/settings
{
  &quot;persistent&quot;: {
    &quot;indices.breaker.total.limit&quot;: &quot;95%&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해 95%로 설정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1748269669383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET _cluster/settings?include_defaults=true&amp;amp;pretty&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해 확인할 수 있다.(참고로 수동으로 퍼센테이지를 조절하기 전엔 위 API로는 노출되지 않는 값이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Field Data&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Search After에서 서술한 대로 Sort 값을 잘못 설정하면 Field Data가 과도하게 할당되어 Heap 메모리에 영향이 가니 아래 명령어로 확인 및 삭제를 해주는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1748269760095&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET _nodes/stats/indices/fielddata?pretty&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748269785048&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST _cache/clear?fielddata=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아예 위의 명령어로도 접근이 안된다면 DB를 껐다켜야한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1748270301701&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;sort&quot;: {
    &quot;_script&quot;: {
      &quot;type&quot;: &quot;number&quot;,
      &quot;script&quot;: {
        &quot;lang&quot;: &quot;painless&quot;,
        &quot;source&quot;: &quot;doc['title'].value.length()&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드데이터는 듣기만 하면 마치 굉장히 쓸모 없는 것 처럼 보여진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓸모 없는게 맞긴한데, 특수한 상황에선 필드 데이터가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에선 title의 길이별로 정렬하는 것인데 이럴땐 keyword로 해결이 불가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴때 필드데이터를 활성화시켜야 제대로 정렬이 된다.&amp;nbsp;&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/551</guid>
      <comments>https://stir.tistory.com/551#entry551comment</comments>
      <pubDate>Sat, 29 Mar 2025 18:29:15 +0900</pubDate>
    </item>
    <item>
      <title>코뿔소 - 외젠 이오네스코</title>
      <link>https://stir.tistory.com/549</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 책의 큰 흐름은 말도 안되는 소리를 하며 주위 사람들에게 강요하는 사람들이 코뿔소로 변해가는 집단주의, 전체주의 파시즘을 희화화 하는 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 내용은 대부분 자신의 생각이 잘못됐을지라도 다른 사람에게 자신의 생각을 강요하는 형태로 흘러간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 사회의 시스템을 만들어 내는 사람들을 의미하기도 하지만 사회의 시스템을 곧이 곧대로 따라가는 사람을 의미한다고 느끼기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽다보면 전체주의를 옹호하지 않더라도 어디까지가 전체주의인가에 대해 고찰하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 표현까지가 전체주의가 아닌 것이고 전체주의인 것일까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 작은 한마디도 누군가에게 전체주의 사상을 주입하는 것은 아닐까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이러니한 점은 책에서 나오는 강압적인 등장인물들은 모순적이라고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체주의에 따르지 않기 위해서 배우고 느꼈지만 그 결과가 다시 자신이 전체주의적 양상을 띄게 되는 듯한 느낌을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아는게 많을수록 모르는 사람에게 전달하고 강요하고 싶은 경우가 많은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 자신도 그랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사회 구성원으로서 어느정도의 룰은 지켜야한다고 생각하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과연 우리의 배움은 어떠한 영향을 가지도록 배우는 것이 좋을까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 배움이 전체주의적 사상을 가지지 않으려면 어떻게 하는 것이 좋을까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;극단적으로 배움을 느끼지 않는 것이 오히려 전체주의를 유도하지 않는 역설적인 의미가 되는 것일까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체주의를 따르지 않음으로서 겪는 고립은 어떻게 해결할 것인가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 대표적인 부조리극이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책에서 얻을 수 있는 점은 사회를 어떻게 바꿀 것인가가 아닌 개인의 태도에 중점을 두고 있는 듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주인공 베랑제처럼 부조리한 상황 속에서도 인간성을 잃지않는 것, 스스로 생각하는 것이 중요하다는 것을 알려주는 책이다. 사회는 어쩔 수 없이 그러한 부조리가 항상 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 그 모습을 바꿀 수도 없을 뿐더러 의미가 없기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 집단주의에 저항하는 개인의 태도가 많으면 많을수록 우리 사회는 한층 더 발전하지 않을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/549</guid>
      <comments>https://stir.tistory.com/549#entry549comment</comments>
      <pubDate>Sat, 29 Mar 2025 18:20:23 +0900</pubDate>
    </item>
    <item>
      <title>모순 - 양귀자</title>
      <link>https://stir.tistory.com/547</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;줄거리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쌍둥이인 어머니의 삶을 바라보는 주인공 안진진의 시선부터 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어머니의 자매였던 이모는 부자고 자신의 어머니는 가난하고 남편은 알콜중독자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 어머니를 보며 삶에 대한 다른 호기심까지도 다 거두어 버렸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 삶은 원래 이런 것이라고 인정한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인생은 탐구하는 것이 아니라 받아들여야만 하는 것, 이것이 사춘기의 주인공이 삶에 대해 내린 결론이었다. &lt;br /&gt;&lt;br /&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;이상한 일이지만, 아버지가 저지르는 그 많은&amp;nbsp;&amp;nbsp;악덕에도 불구하고 나는 아버지를 미워하지는 않았던&amp;nbsp;&amp;nbsp;것 같다. 어머니를 때리거나,&amp;nbsp;&amp;nbsp;밥상을 뒤엎거나, 파출소에서 전화가 와 집안을&amp;nbsp;&amp;nbsp;발칵 뒤집어 놓을 때는, 그래서 이모가&amp;nbsp;&amp;nbsp;달려와 우리를 데리고 이모 집으로 갈 때는&amp;nbsp;&amp;nbsp;자존심이 상해 입술을 악물며 아버지를 원망하긴 했어도 잠시였다.&lt;br /&gt;&lt;br /&gt;아버지의 그 망나니 짓에는 일종의 '품위'가 있었다.&lt;br /&gt;&lt;br /&gt;아버지는 부드러운가 하면 금방 사나웠고,&amp;nbsp;&amp;nbsp;따뜻한가 하면 당장 차가웠으며,&amp;nbsp;&amp;nbsp;웃고 있는가 하면 순간적으로 폭포수같이 눈물을 흘리는 사람이었다.&amp;nbsp;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;내가 안진진을 그렇게 괴롭혔나 생각하니 얼마나 가슴이 아프던지...한번 물어 보자. 안진진한테 나는 감옥이니?&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;인생의 부피를 늘려 주는 것은 행복이 아니고 오히려 우리가 그토록 피하려 애쓰는 불행이라는 중요한 교훈을 내게 가르쳐 준 주리였다.&amp;nbsp;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이모부 같은 사람을 비난하는 것보다는 이모의 낭만성을 나무라는 것이 내게는 훨씬&amp;nbsp;&amp;nbsp;쉽다. 그러나 내 어머니보다 이모를 더 사랑하는 이유도 바로 그 낭만성에 있음은 어떻게 설명할 수 있을까. 바로 그 이유 때문에 사랑을 시작했고, 바로 그 이유 때문에 미워하게 된다는, 인간이란 존재의 한 없는 모순...이모가 좋았으므로 나는&amp;nbsp;&amp;nbsp;이모에게 감염되기로 마음을 먹었다.&amp;nbsp;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;종지부에는 이모가 목숨을 끊고만다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부자였던 이모를 자신은 부러워했지만 이모는 반대로 주인공의 어머니를 부러워했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바쁜 삶, 싸우는 삶, 그 안에서 오는 치열한 행복을 느끼고 싶어했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;인간에게는 행복만큼 불행도 필수적인 것이다. 할 수 있다면 늘 같은 분량의 행복과 불행을 누려야 사는 것처럼 사는 것이라고 이모는 죽음으로 내게 가르쳐 주었다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 여자는 남자 둘 중 한명을 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낭만적인 남자와 부자인 남자, 남은 것은 어떤 종류의 불행과 행복을 택할&amp;nbsp;&amp;nbsp;것인지 그것을 결정하는&amp;nbsp;문제뿐이었다. &lt;br /&gt;&lt;br /&gt;주인공은 낭만을 추구하는 남자보다 부자인 남자를 선택했다.&lt;br /&gt;자신에게 없었던 것을 선택한 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행복은 절대적인 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/h2&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;&quot;인생은 탐구하면서 살아가는 것이 아니라, 살아가면서 탐구하는 것이다. 실수는 되풀이된다. 그것이 인생이다&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주인공은 결국에 자신에게 없던 돈을 선택하기 위해 안정적인 남자를 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 죽음으로 몰고갈지언정 자신에게 없었던 안정을 택하겠다는것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 독자들도 이 부분에서 꽤나 많이 놀랐을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상과 정반대의 남자를 선택했으니까 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 살면서 어떤 것이 싫고 좋은지를 꽤나 명확하게 느끼고 산다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 우리는 습관적으로 불행을 피하기 위해 좋은 것을 생각하기 마련인데, 이 책이 알려주는 것은 어떠한 선택이든 불행과 행복은 존재할 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 시간이 지나고 나서 다시 느낀 점은 행복을 얻기 위해선 불행을 부딪혀야한다는 점이다.&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/547</guid>
      <comments>https://stir.tistory.com/547#entry547comment</comments>
      <pubDate>Thu, 27 Mar 2025 22:37:30 +0900</pubDate>
    </item>
    <item>
      <title>부하 테스트로 알아본 적절한 DB 선택(RDBMS, OpenSearch, MongoDB)</title>
      <link>https://stir.tistory.com/546</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 NoSQL은 정규화가 필요하지 않은 데이터를 효율적으로 저장하거나 조회하기 위해 OpenSearch, MongoDB 등을 선택할 수 있다고 생각했습니다.&lt;br&gt;실제로 이 것에 관해 &lt;a href=&quot;https://stir.tistory.com/502&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Json 타입의 데이터를 어디에 넣을지 고민한 글&lt;/span&gt;&lt;/a&gt;도 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;물론 이 말도 틀린 말은 아닙니다.&lt;br&gt;하지만 이번에 DB에 대한 부하 테스트를 해보면서 NoSQL을 선택할 더 중요한 이유를 찾게 되었고 단순히 데이터의 형태에 따라 DB를 결정하는 것이 아닌 기능 요구 사항에 따라 적절한 DB를 어떻게 선택할 수 있을지 고민할 수 있게 되어서 글을 작성합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;일단 거두절미하고 바로 결과를 보겠습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부하 테스트 결과&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트는 K6로 진행했으며 5초마다 유저 사용량이 100명씩 증감되어 총 100~500명이 25초 동안 이용하는 시뮬레이션으로 진행했습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;요구 사항&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;테스트하고자 하는 요구 사항은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;회원 정보 현황 업데이트&lt;/b&gt;&lt;/span&gt;입니다.&lt;br&gt;특정 회원이 로그인을 한다면 로그인 된 시간, IP 등을 관리자가 볼 수 있도록 Update 하거나 첫 로그인이라면 Insert 하는 것입니다.&amp;nbsp;&amp;nbsp;&lt;br&gt;여러분들이라면 어떻게 구현하시겠나요?&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;userRepositry.save(user);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;가장 저에게 익숙한 JPA 코드가 먼저 생각나는데요.&lt;br&gt;save를 이용하면 Insert 뿐만 아니라 Update도 처리할 수 있기 때문에 깔끔하게 처리할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 사용량이 급격하게 많아져서 500명 혹은 5000명이 동시에 이용한다면 어떤 DB에 저장하면 좋을까요?&lt;br&gt;우선 해당 요구 사항을 MySQL을 이용해 처리해보겠습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RDBMS&lt;/b&gt;&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DzTM4/btsMuJd0b6i/sNKXq3ShpkmvlKXzRpcxu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DzTM4/btsMuJd0b6i/sNKXq3ShpkmvlKXzRpcxu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DzTM4/btsMuJd0b6i/sNKXq3ShpkmvlKXzRpcxu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDzTM4%2FbtsMuJd0b6i%2FsNKXq3ShpkmvlKXzRpcxu0%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;1198&quot; height=&quot;625&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 25초동안 100~500명의 사용자의 비즈니스 로직 처리가 8403건 완료되었고 최대로 오래 기다린 사람은 2.65초입니다.&lt;br&gt;만약 500명이 아닌 1000~5000명으로 늘린다면 어떻게 될까요?&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;635&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rd1QX/btsMxqjrgol/RKvXWNqe45CPTYDB5cSipk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rd1QX/btsMxqjrgol/RKvXWNqe45CPTYDB5cSipk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rd1QX/btsMxqjrgol/RKvXWNqe45CPTYDB5cSipk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frd1QX%2FbtsMxqjrgol%2FRKvXWNqe45CPTYDB5cSipk%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;1195&quot; height=&quot;635&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;635&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5000명이나 인입이 되었는데도 처리량이 그렇게 늘어나지 않은 11964건이 처리되고 최대로 오래 기다린 사람은 15.65초입니다.&lt;br&gt;물론 이 과정에서 Time-Out은 발생하지 않았고 모두 정상 처리 되었지만 사용량이 좀 더 많아진다면 Time-Out이 발생하는 사용자도 생길 것입니다.&lt;br&gt;이렇게 느리게 처리된 이유는 일단 DB 처리가 끝날 때까지 Tomcat Thread가 대기 상태에 놓여있게 되어서 그렇습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;같은 조건으로 OpenSearch를 테스트해보겠습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OpenSearch&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;OpenSearch는 Spring Boot에서 Kafka와 Logstash를 연동해서 처리하도록 구현되어 있습니다.&lt;br&gt;그러므로 Spring Boot에서 실제로 저장하는 로직이 존재하지 않기 때문에 비동기적 처리가 가능합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjK5Lj/btsMuMhugL1/mj0iT4xQ9b18qvBCdPQgs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjK5Lj/btsMuMhugL1/mj0iT4xQ9b18qvBCdPQgs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjK5Lj/btsMuMhugL1/mj0iT4xQ9b18qvBCdPQgs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjK5Lj%2FbtsMuMhugL1%2Fmj0iT4xQ9b18qvBCdPQgs1%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;1020&quot; height=&quot;538&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 500명의 사용자가 무려 733680건을 성공하게 되었습니다.&lt;br&gt;기다린 시간도 마이크로 세컨드로 소요된 시간도 굉장히 짧아진 것을 볼 수 있었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 개의 테스트를 비교하는 것 만으로 단순히 정규화 데이터인지 아닌지를 떠나서 어디에 저장하는 것이 좋을지 고민하게 되지 않나요?&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RDBMS에 저장하는 방식&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS부터 한번 생각해 보겠습니다.&lt;br&gt;DB 처리가 굉장히 느려서 Tomcat Thread가 묶이는 현상이 있었고 서버의 처리량이 느려진 걸 확인할 수 있었습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Kafka를 써볼까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 가장 직관적으로 떠올랐던 것은 Spring에서 처리할 데이터를 Kafka에 토픽을 생성해서 전달한 뒤 토픽을 소모하는 서버를 만들어서 DB에 간접적으로 저장하도록 구현하면 기존 서버의 부하는 해소할 수 있다고 생각했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 위와 같은 방식은 일반적인 RDBMS 처리 방식이 아닙니다.&lt;br&gt;결과적으로 일부분 비동기적인 특성을 이용한 것인데, 이렇게 사용해 버린다면 RDBMS의 특성이 몇 가지 사라지게 됩니다.&lt;br&gt;일단 문제점 몇 개를 살펴보겠습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실질적인 DB 부하가 개선되지 않는다.&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 나누는 것으로는 기존 서버의 부하는 해결됐지만 새로 만든 서버에서는 여전히 부하도 걸리고 실제 DB 처리가 느렸던 부분은 해결되지 않은 상태입니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;트랜잭션 처리가 의미가 없어집니다. &lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 문제는 RDBMS의 트랜잭션 처리가 일부분 의미가 사라집니다.&lt;br&gt;물론 서버를 나눠서 처리하는 것에도 ACID의 특성으로 인해 데이터의 무결성은 지킬 수 있지만 사용자가 데이터의 처리에 대한 반환 값을 받지 못한다는 것입니다.&lt;br&gt;다시 말해 클라이언트의 데이터는 이미 Kafka로 넘겨지고 끝났기 때문에 데이터의 처리가 트랜잭션 과정에서 실패를 했는지 성공했는지 알 수 없습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;물론 이런 경우는 대표적으로 Netty를 기반으로 한 WebFlux와 같은 비동기 프로그래밍을 사용하면 비동기적인 특성을 살리면서 데이터를 저장할 수도 있고 결과 값을 반환받을 수도 있습니다.&lt;br&gt;WebFlux와 같은 대체 프레임워크가 존재하는데 Kafka를 이용해 반쪽짜리(저장만 하고 결과 값을 받지 않는) 비동기를 구현하는 것은 오히려 복잡도만 올라가게 됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebFlux를 써볼까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 꺼낸 주제인 WebFlux를 이용해 보면 어떨까요?&lt;br&gt;실제로 서버의 부하는 줄어들겠지만 트랜잭션 처리를 하면서 데이터를 저장하는 과정은 여전히 느릴 수밖에 없습니다.&lt;br&gt;R2DBC에서는 DB를 쓰고 읽는 과정을 비동기로 하는 것뿐이지 실제 I/O 입력 과정은 여전히 느릴 테니까요.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 또 하나 알 수 있는 점은 Tomcat을 쓰든 WebFlux를 쓰든 사용자는 데이터의 결과를 보장받는다는 것입니다.&lt;br&gt;그게 RDBMS를 쓰는 것의 핵심적인 특성입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결과를 보장받아야만 하는 상황이라면 어떤 상황을 의미할까요?&lt;br&gt;&lt;br&gt;대표적으로 이체 과정이 그렇습니다.&lt;br&gt;만약에 이체 버튼을 눌렀는데 처리가 완료가 되지 않았지만 비동기적 처리를 해서 완료 페이지가 보인다면 사용자는 완료가 되었다고 판단할 수 있습니다.&lt;br&gt;그렇기에 이체 같은 중요한 시스템은 RDBMS를 사용해서 정말 완료되었을 때 확인할 수 있도록 설계해야 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이런 상황에서 발생하는 속도 문제는 Kafka가 아닌 WebFlux, Redis와 같은 캐시 시스템, 인덱스 설계, 쿼리문 등을 적절하게 섞어가며 효율적이게 만들고 부하가 또 발생한다면 시스템의 성능을 향상하는 방식으로 사용하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결론적으로 어떠한 방식이든 제가 처음에 원했던 요구 사항을 충족할만한 방법은 없습니다.&lt;br&gt;회원 정보 현황을 업데이트하는 것에는 캐시, 인덱스, 쿼리문 등이 필요한 영역은 아니고 WebFlux를 쓴다고 하더라도 비동기로 처리할 뿐이지 DB에서 트랜잭션을 이용해 데이터를 삽입하는 과정 자체가 느린 것은 해결되지 않기 때문입니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;OpenSearch에 저장하는 방식&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제 OpenSearch를 알아보겠습니다.&lt;br&gt;OpenSearch는 대표적으로 로그를 저장하는 데 쓰입니다.&lt;br&gt;RDBMS를 사용했을 때는 사용자가 데이터에 대한 Request &amp;gt; Save &amp;gt; Response의 Flow를 하나의 묶음으로 처리해야 하는 특성을 가졌다면 굳이 결과를 받지 않아도 되는 로그 시스템 같은 경우는 대부분 OpenSearch를 이용하게 됩니다.&lt;br&gt;사용자는 Request만 보내면 끝이니까요.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러므로 결론적으로 단방향으로 Insert만 하는 경우에는 OpenSearch를 쓰는 것이 좋습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;근데 요구 사항은 Insert가 아닌 Upsert였습니다.&lt;br&gt;이 경우에는 OpenSearch를 쓰는 것이 올바르지 않습니다.&lt;br&gt;OpenSearch는 Lucene 기반으로 만들어져 있으며 Lucene의 Segment 단위는 Immutable 한 특성을 유지하기 때문입니다.&lt;br&gt;즉, 한번 저장되면 변경되지 않고 유지되는 방식을 이용해 검색에서 최적화가 가능하도록 유지합니다.&lt;br&gt;그럼에도 불구하고 OpenSearch는 Update 기능을 제공하지만 만약에 Update를 하게 된다면 기존의 문서를 삭제하고 새 문서를 다시 만들게 됩니다.&lt;br&gt;그러니까 Delete &amp;gt; Insert 방식으로 이루어지게 되어서 전반적인 OpenSearch 성능 저하가 오게 됩니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MongoDB에 저장하는 방식&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 가장 효율적인 방식은 MongoDB라고 결론을 내렸습니다.&lt;br&gt;MongoDB는 CRUD를 자유롭게 지원하고 전통적인 RDBMS보다 고속 쓰기/읽기가 가능합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvggg/btsMvPdvfR8/7YOYHzJtvMBocQx8z6thF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvggg/btsMvPdvfR8/7YOYHzJtvMBocQx8z6thF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvggg/btsMvPdvfR8/7YOYHzJtvMBocQx8z6thF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnvggg%2FbtsMvPdvfR8%2F7YOYHzJtvMBocQx8z6thF1%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;1022&quot; height=&quot;365&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b14tmA/btsMx24dnUA/dipZYrZm7KxP3o2GEmCmW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b14tmA/btsMx24dnUA/dipZYrZm7KxP3o2GEmCmW0/img.png&quot; data-alt=&quot;https://www.designgurus.io/answers/detail/what-is-strong-vs-eventual-consistency&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b14tmA/btsMx24dnUA/dipZYrZm7KxP3o2GEmCmW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb14tmA%2FbtsMx24dnUA%2FdipZYrZm7KxP3o2GEmCmW0%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;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.designgurus.io/answers/detail/what-is-strong-vs-eventual-consistency&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기서 대표적으로 비교되는 것이 강력한 일관성과 최종 일관성인데요.&lt;br&gt;MongoDB와 OpenSearch가 대표적으로 최종 일관성 방식을 선택합니다.&lt;br&gt;데이터 저장을 하고 나면 바로 반영하는 것이 아니라 추후에 반영한다는 의미입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;강력한 일관성과 최종 일관성을 선택하는 것은 어느 정도 트레이드오프가 발생하고 &lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엄격한 데이터 정확성이 필요한 경우(예: 금융 시스템)에는 일반적으로 강력한 일관성을 선택하는 반면, 더 나은 성능과 가용성을 위해 일시적인 불일치를 허용할 수 있는 애플리케이션(예: SNS 피드)은 최종 일관성을 선택할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;RDBMS는 쓰기, 읽기가 하나의 흐름으로 이어지기 때문에 강력한 일관성을 유지하므로 사용자는 저장한 데이터를 바로 조회할 수 있지만 오히려 DB 처리 속도 저하가 생겼습니다.&lt;br&gt;MongoDB는 최종 일관성으로 쓰기 작업이 나중에 반영된다는 특성이 있지만 가용성과 속도 측면에서 장점이 있습니다.&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 최종 일관성은 성능과 가용성을 우선시하며, 즉각적인 데이터 일관성에 대한 트레이드오프가 있습니다.&lt;/span&gt;&lt;/span&gt; &lt;br&gt;&amp;nbsp;&lt;br&gt;결론적으로 처음의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;회원 정보 현황 업데이트&lt;/b&gt;&lt;/span&gt;에 대한 요구사항은 강력한 일관성이 필요하지도 않고 나중에 반영만 되면 상관없었습니다. 그리고 속도 측면과 Update 기능이 동시에 필요했기 때문에 MongoDB를 이용하는 것이 가장 효율적이었다는 것을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;정규화된 데이터라도 NoSQL을 이용할 수도 있고 비정형 데이터여도 RDBMS를 이용할 수 있습니다.&lt;br&gt;여기서 알 수 있는 점은 NoSQL의 기본이 되는 특징을 알 수 있는데요.&lt;br&gt;NoSQL은 비정형 데이터를 다루기 위해 만든 것이라기 보다는 고속 처리를 하기 위해 비정형 데이터를 사용한 것으로 생각할 수 있습니다.&lt;br&gt;그러므로 NoSQL의 선택의 기준은 NoSQL이 왜 만들어졌는지부터 알아야 더 좋은 선택이 될 수 있다는 걸 알게 되었습니다.&lt;/p&gt;</description>
      <category>  Distributed System</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/546</guid>
      <comments>https://stir.tistory.com/546#entry546comment</comments>
      <pubDate>Tue, 25 Feb 2025 11:24:09 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 개인 정리</title>
      <link>https://stir.tistory.com/543</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스프링 배치 소개&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스프링 배치는 Job 안에 여러개의 Step이 있는 구조다&lt;/p&gt;
&lt;pre id=&quot;code_1739535466895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Job myJob(JobRepository jobRepository, Step step1, Step step2) {
    return new JobBuilder(&quot;myJob&quot;, jobRepository)
            .start(step1)  // Step1 실행
            .next(step2)   // Step2 실행
            .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;JobRepository &lt;br /&gt;Job이 실행될 때마다 실행한 기록(성공/실패)과 관련된 상태 정보가 JobRepository에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장되는 위치는 기본적으로 인모메리 DB인 HSQL 메모리에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 mysql 의존성을 어떻게든 추가하면 거기다가 BATCH 관련 테이블을 저장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739542119435&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_batch
spring.datasource.username=root
spring.datasource.password=password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Step은 Tasklet 혹은 Chunk(Chunk Oriented Processing) 기반으로 나뉘며 Chunk는 대량 데이터를 처리하고 Tasklet은 단순한 로직을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Chuck&amp;nbsp;구성&amp;nbsp;요소&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader - DB, CSV, JDBC, JPA 등을 이용해 데이터를 읽는 역할을 한다.&lt;/li&gt;
&lt;li&gt;ItemProcessor - 가져온 데이터를 가공한다.&lt;/li&gt;
&lt;li&gt;ItemWriter - 가공된 데이터를 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 알 수 있는 점은 스프링 배치는 크게 Read(읽기), Process(처리), Write(쓰기) 작업으로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ItemReader, ItemProcessor, ItemWriter&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3개는 말 그대로 Item에 관한 처리를 한다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 표현하자면 Items에 대한 처리가 아니라 Item이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐면 Reader에서 데이터를 읽어올 때 한번에 읽어오는 것이 아니라 하나씩 읽어서 Processor로 던져서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 요구 조건에 따라 어떻게 처리해야할 지 결정해야하는 것인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 데이터를 읽어서 중복을 제거하고 변형하는 작업은 위 3가지 흐름을 이용하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 리스트에 대한 총합 갯수는 Tasklet을 이용하면 된다.&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;기존 Java를 알던 사람이라면 조금 당황스러운 부분은 Java 자료구조형인 List, Set 등을 이용하면 중복을 거르는 로직을 Spring Batch에서는 그렇게 이용하면 안된다. 기본적으로 한 저장 공간에 모든 것을 담아두고 사용하면 메모리 부하가 생기기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치는 빠르게 처리하는 것에도 목적이 있지만 메모리 부하를 관리하면서 통계 처리하는 데에도 의의가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 속도 뿐만 아니라 읽기 단위를 몇개로 할 지에 대한 균형을 잘 맞춰서 개발하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Tasklet&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1739535670769&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SimpleTasklet implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        // StepContribution에서 JobParameters 바로 가져오기
        JobParameters jobParameters = contribution.getStepExecution()
                                                  .getJobExecution()
                                                  .getJobParameters();
                                                  
	// JobParameters jobParameters = chunkContext.getStepContext().getJobParameters();
        // chunkContext 방식은 복잡함
        String name = jobParameters.getString(&quot;name&quot;);

        System.out.println(&quot;Hello, &quot; + name + &quot; with Tasklet!&quot;);

        return RepeatStatus.FINISHED;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tasklet에선 간단한 처리를 위해 사용하며 읽기, 처리, 쓰기를 모두 하나의 execute 메소드 안에서 구현한다.&lt;/p&gt;
&lt;h3 data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StepContribution과 ChunkContext &lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;StepContribution은 현재 실행 중인 &lt;b&gt;Step&lt;/b&gt;의 실행 상태를 관리하는 객체다.&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;ChunkContext는 현재 실행 중인 &lt;b&gt;Chunk&lt;/b&gt;의 실행 상태를 관리하는 객체다.&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;tasklet과 chunk는 서로 다른데 tasklet에서 ChunkContext를 쓰는 이유는 &lt;b&gt;Step이 항상 Chunk 기반으로 설계되어 있기 때문이다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;195&quot; data-start=&quot;168&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JobParamter&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1739539742366&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; JobParameters jobParameters = new JobParametersBuilder()
        .addLong(&quot;time&quot;, System.currentTimeMillis())
        .toJobParameters();

      jobLauncher.run(batchExecutionLogJob, jobParameters);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 job을 실행하기전에 Job Parameter를 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job Parameter란 스프링 배치의 Job에 동적 변수를 전달하는 행위를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tasklet에서 구현된 execute에서 StepContribution, ChunkContext에서 모두 jobParameter를 가져올 수 있다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Chunk&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk는 위에 설명했듯이 Read(읽기), Process(처리), Write(쓰기) 작업으로 나뉜다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메소드도 3개로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 내에서 3개를 순서대로 호출하는 형태다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StepScope 어노테이션&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1739536464239&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@StepScope
public StepScopeItemReader(@Value(&quot;#{jobParameters['filePath']}&quot;) String filePath) {
        System.out.println(&quot;Reading file from path: &quot; + filePath); // JobParameter 출력
        List&amp;lt;String&amp;gt; items = Arrays.asList(&quot;Line1&quot;, &quot;Line2&quot;, &quot;Line3&quot;);
        this.data = items.iterator();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위 코드는 Reader 메소드의 구현인데, 추가적으로 @StepScope를 붙이면 JobParameter를 넘겨받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@StepScope가 붙으면 Step이 실행되는 시점에 Bean을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk 방식에서 JobParameter를 사용하기 위해서는 이렇게 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 Reader는 Job 내에서 호출 시키는 형태인데, 거기서 위에서 설명한 StepContribution과 ChunkContext가 존재해서 거기서 사용해도 되지만 그건 tasklet에서 가져다 쓰는 방법이고 이건 Reader에서 갖다 쓰는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;StepScope의 프록시 객체&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b60UVQ/btsMjRoT6he/br83ookcaBaWs2jRFDTTLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b60UVQ/btsMjRoT6he/br83ookcaBaWs2jRFDTTLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b60UVQ/btsMjRoT6he/br83ookcaBaWs2jRFDTTLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb60UVQ%2FbtsMjRoT6he%2Fbr83ookcaBaWs2jRFDTTLk%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;589&quot; height=&quot;205&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 결론만 말하자면 위에 코드에서 @StepScope를 붙이는 경우 Return 타입을 ItemReader로 하면 안되고 구현체는 JpaPagingItemReader를 해야한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 테스트 코드 돌릴 때 &lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;ItemReader 타입을 리턴할 경우 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;@StepScope의 &lt;/span&gt;proxyMode = ScopedProxyMode.TARGET_CLASS&lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;로 인해서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;ItemReader 인터페이스의 프록시 객체&lt;/span&gt;&lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;를 생성하여 리턴하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;그 프록시 객체에는 Listener Annotation을 사용할 수 없기 때문에(예를 들어 &lt;span style=&quot;color: #24292e; text-align: start;&quot;&gt;@AfterStep, @BeforeStep) 구현 클래스로 리턴해줘야 한다고 함.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안하면 o.s.b.c.l.AbstractListenerFactoryBean&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;: org.springframework.batch.item.ItemReader is an interface.&amp;nbsp;&amp;nbsp;The implementing class will not be queried for annotation based listener configurations.&amp;nbsp;&amp;nbsp;If using @StepScope on a @Bean method, be sure to return the implementing class so listner annotations can be used. 이 에러 발생.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Chunk 구조 파헤치기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1739543147607&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   @Bean
    public Step dailyEventLogStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;dailyEventLogStep&quot;, jobRepository)
                .&amp;lt;SearchResponse, Long&amp;gt;chunk(1, transactionManager)
                .reader(eventLogReader(null))
                .processor(eventLogProcessor())
                .writer(eventLogWriter())
                .faultTolerant()
                .retry(Exception.class)
                .retryLimit(3)
                .build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- chunk(1, 이라고 선언하면 reader에서 데이터 조회해온 결과를 몇개씩 처리할 지 결정하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reader에서 100건을 가져오면 1건씩 처리하겠다는 의미다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- transactionManager는 reader, processor, writer 기능을 서로 분리해서 트랜잭션 관리하는 기능을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739543286614&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @Bean
    @StepScope
    public ItemReader&amp;lt;SearchResponse&amp;gt; eventLogReader(
            @Value(&quot;#{jobParameters[date]}&quot;) String dateStr) {
        return new ItemReader&amp;lt;&amp;gt;() {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 Reader의 코드인데, 오픈서치의 결과를 ItemReader로 묶어서 리턴하게 되면 자동으로 Processor에 결과를 던지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1739543853608&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class DailyEventLogJobConfigTest {

    @Mock
    private RestHighLevelClient openSearchClient;

    @InjectMocks
    private DailyEventLogJobConfig jobConfig;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InjectMocks는 실제 테스트 대상이고 거기서 필요한 openSearchClient가 있으니까 그걸 가짜로 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1739543907615&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class DailyEventLogJobConfig {

    private final RestHighLevelClient openSearchClient;
    private final StatisticsRepository statisticsRepository;

    @Value(&quot;${opensearch.index-prefix}&quot;)
    private String indexPrefix;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 보면 openSearchClient가 필요하다.&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;그리고 여기서 indexPrefix는 private인데 테스트 코드에서 직접 접근할 수 없ekrh gksek.&lt;/p&gt;
&lt;pre id=&quot;code_1739544077720&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @BeforeEach
    void setUp() {
        ReflectionTestUtils.setField(jobConfig, &quot;indexPrefix&quot;, &quot;test-index&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위와 같이 ReflectionTestUtils라는 Junit에 포함된 기능을 이용해 임의로 설정한다고 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;afterStep과 afterJob&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말그대로 Step과 Job이 끝날 때 실행된다.&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;검색하다보면 step의 요소는 ItemReader, Processor, Writer가 있다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이게 한 사이클이 돌았을 때를 &quot;하나의 step&quot;이라고 명명하진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐면&amp;nbsp;가령 하나의 step 내에서 청크 단위 10개로 9번 회전하는 로직이 있다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 ItemReader, Processor, Writer가 하나의 사이클을 9번 돈다는건데, 이게 그럼 하나의 Step이란 것은 9번이 아니라 Step안에서 처리되는 모든 과정을 하나의 Step이라고 한다.&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;그러므로 Step과 Job이 1개씩밖에없다면 afterStep과 afterJob은 실행시점이 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741924019978&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Job commandFilteringJob() throws Exception {
    return new JobBuilder(&quot;commandFilteringJob&quot;, jobRepository)
        .start(step1())        // 엑셀 파일 읽기 Step
        .next(step2())         // 데이터 검증 Step
        .next(step3())         // 결과 저장 Step
        .listener(jobCompletionListener())
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/543</guid>
      <comments>https://stir.tistory.com/543#entry543comment</comments>
      <pubDate>Fri, 14 Feb 2025 22:46:10 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Garbage Collection의 STW(Stop The World) 부하 테스트</title>
      <link>https://stir.tistory.com/542</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목표&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 가장 대표 격인 GC인 G1GC에서의 Stop The World를 빈번하게 유도해보고 초저지연 GC도 사용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얕게 배워본 초저지연 GC로 일단 테스트부터 해보며 어떤 상황일 때 ZGC를 사용해볼 법한지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JDK 17에서의 G1GC 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 JDK 17에서 아무 설정도 건드리지 않으면 G1GC로 선택이 되니 바로 테스트해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 10MB를 배열에 지속적으로 할당과 해제를 반복하는 것을 1000번 진행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739252408812&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 SIZE = 10 * 1024 * 1024 * 10; // 100MB씩 할당

public void createMemoryPressure() {
  // 메모리를 빠르게 할당하고 해제하면서 GC를 유도
  for (int i = 0; i &amp;lt; 1000; i++) {
    byte[] memoryPressure = new byte[SIZE]; // 10MB씩 할당
    // 할당된 메모리를 사용하고 나서 즉시 참조를 끊음
    // 메모리 할당 후 바로 GC가 발생할 수 있게 만듦
    memoryPressure = null;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 실행 시 -Xlog:gc*:file=gc.log를 추가하면 아래 로그를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[7.631s][info][gc] GC(3) Pause Young (Concurrent Start) (G1 Humongous Allocation) 955M-&amp;gt;14M(2048M) 2.893ms
[7.631s][info][gc] GC(4) Concurrent Undo Cycle
[7.634s][info][gc] GC(4) Concurrent Undo Cycle 2.383ms
[7.689s][info][gc] GC(5) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.727ms
[7.689s][info][gc] GC(6) Concurrent Undo Cycle
[7.691s][info][gc] GC(6) Concurrent Undo Cycle 1.661ms
[7.747s][info][gc] GC(7) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.901ms
[7.748s][info][gc] GC(8) Concurrent Undo Cycle
[7.749s][info][gc] GC(8) Concurrent Undo Cycle 1.731ms
[7.805s][info][gc] GC(9) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 2.622ms
[7.805s][info][gc] GC(10) Concurrent Undo Cycle
[7.806s][info][gc] GC(10) Concurrent Undo Cycle 1.642ms
[7.863s][info][gc] GC(11) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.739ms
[7.863s][info][gc] GC(12) Concurrent Undo Cycle
[7.865s][info][gc] GC(12) Concurrent Undo Cycle 1.691ms
[7.921s][info][gc] GC(13) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 2.457ms
[7.921s][info][gc] GC(14) Concurrent Undo Cycle
[7.923s][info][gc] GC(14) Concurrent Undo Cycle 1.761ms
[7.981s][info][gc] GC(15) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.817ms
[7.981s][info][gc] GC(16) Concurrent Undo Cycle
[7.983s][info][gc] GC(16) Concurrent Undo Cycle 1.647ms
[8.039s][info][gc] GC(17) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 2.147ms
[8.039s][info][gc] GC(18) Concurrent Undo Cycle
[8.040s][info][gc] GC(18) Concurrent Undo Cycle 1.567ms
[8.097s][info][gc] GC(19) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;15M(2048M) 1.883ms
[8.097s][info][gc] GC(20) Concurrent Undo Cycle
[8.099s][info][gc] GC(20) Concurrent Undo Cycle 1.652ms
[8.156s][info][gc] GC(21) Pause Young (Concurrent Start) (G1 Humongous Allocation) 917M-&amp;gt;14M(2048M) 2.039ms
[8.156s][info][gc] GC(22) Concurrent Undo Cycle
[8.158s][info][gc] GC(22) Concurrent Undo Cycle 1.643ms
[8.220s][info][gc] GC(23) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.825ms
[8.220s][info][gc] GC(24) Concurrent Undo Cycle
[8.222s][info][gc] GC(24) Concurrent Undo Cycle 1.610ms
[8.280s][info][gc] GC(25) Pause Young (Concurrent Start) (G1 Humongous Allocation) 916M-&amp;gt;14M(2048M) 1.981ms
[8.280s][info][gc] GC(26) Concurrent Undo Cycle
[8.282s][info][gc] GC(26) Concurrent Undo Cycle 1.752ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 STW를 포함해서 걸린 시간을 조회해 보면 전체 약 40ms 정도가 소요됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Humogous Allocation(거대 할당)과 같은 상황은 일반적이지 않은 테스트 방식이긴 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JDK 17의 &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;Shenandoah(&lt;/span&gt;셴언도우) GC&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셴언도우는 미국의 강이고 평온한 GC라는 뜻으로 굉장히 빠른 GC로 World가 멈추지 않는 현상을 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 설정으로 셴언도우 GC를 실행시킬 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739258347404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-Xms2g -Xmx2g -Xlog:gc:file=gc.log.2025-02-11 -XX:+UseShenandoahGC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해보면 JDK 17에서의 G1GC보다 5배 빠른 걸 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;다만, 셴언도우는 동시성 처리를 위해 CPU, 메모리 소비 증가 하므로 해당 부분을 염두에 두어야 합니다.&lt;/p&gt;
&lt;blockquote data-pm-slice=&quot;0 0 []&quot; data-ke-style=&quot;style3&quot;&gt;일단 여기서는 로그를 가져오지 않았습니다.&lt;br /&gt;어차피 2023년 9월 19일 프로덕션 릴리스로 Shenandoah 기능은 준비되지 않아 제거되고 있습니다.&lt;br /&gt;세대별 Shenandoah용 JEP 작성자인 Amazon의 Roman Kennke는 JDK 21 또는 Java 21에서 해당 기능을 제거하기로 결정했으며 Oracle 이 명시한 대로 향후 JDK 릴리스가 준비되면 이를 평가할 계획이라고 합니다.&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JDK 21의 Generational ZGC&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드 케이스를 똑같이 ZGC에서 실행해봐도 좋지만, 셴언도우와 ZGC는 컨셉 자체가 같기 때문에 더 일반적인 코드 케이스로 테스트 해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번엔 Redis에서 업데이트 정보를 가져가면서 Gzip 압축도 하고 암복호화도 하는 테스트를 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739258411326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-Xms2g -Xmx2g -Xlog:gc+phases:file=gc.log.2025-02-11 -XX:+UseZGC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이번엔 위와 같이 설정을 추가해줍니다. +phases를 붙여줘야 정지 시간(STW)을 알 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1740039937085&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;RFE: -Xlog:gc to show pause/concurrent phases duration and heap occupancy&quot; data-og-description=&quot;&quot; data-og-host=&quot;mail.openjdk.org&quot; data-og-source-url=&quot;https://mail.openjdk.org/pipermail/zgc-dev/2017-December/000031.html&quot; data-og-url=&quot;https://mail.openjdk.org/pipermail/zgc-dev/2017-December/000031.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://mail.openjdk.org/pipermail/zgc-dev/2017-December/000031.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mail.openjdk.org/pipermail/zgc-dev/2017-December/000031.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;RFE: -Xlog:gc to show pause/concurrent phases duration and heap occupancy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mail.openjdk.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1739258542440&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;  public String getUpdate(String updateId) {
    try {
      String base64EncryptedData = (String) redisTemplate.opsForHash().get(&quot;update:&quot; + updateId, &quot;content&quot;);
      if (base64EncryptedData == null) {
        return null; // Redis에 데이터가 없을 경우 null 반환
      }

      // 1. Base64 디코딩
      byte[] encryptedData = Base64.getUrlDecoder().decode(base64EncryptedData);

      // 2. 복호화
      byte[] decryptedData = ARIAUtil.decryptWithCBC(encryptedData);

      // 3. GZIP 압축 해제
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(decryptedData))) {
        byte[] buffer = new byte[1024];
        int len;
        while ((len = gzipInputStream.read(buffer)) != -1) {
          out.write(buffer, 0, len);
        }
      }

      // 4. 복호화된 데이터 반환
      return out.toString(&quot;UTF-8&quot;);

    } catch (Exception e) {
      e.printStackTrace();
      return null; // 예외 발생 시 null 반환
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하고 레디스에 약 4kb 데이터를 Hash로 넣어두고 k6로 10000명의 사용자가 조회하는 테스트 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740039973983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
    stages: [
        { duration: '5s', target: 100 },  
        { duration: '5s', target: 500 }, 
        { duration: '5s', target: 10000 }, 
        { duration: '5s', target: 500 }, 
        { duration: '5s', target: 100 },  
    ],
};
export default function () {
  let res = http.get('http://localhost:8080/test/12345');
  sleep(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이렇게 하면 제대로 부하 테스트가 안될 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Tomcat의 max-connections가 8192개로 잡혀있기 때문에 8192개가 넘어가는 순간부터 TimeOut이 발생합니다. 그러므로 application.yml에 max-connection을 늘려주고 시작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740040061688&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;k6 run script.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 아래의 코드는 G1GC에서 가져온 부하 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[59.105s][info][gc] GC(10) Pause Young (Normal) (G1 Evacuation Pause) 462M-&amp;gt;53M(2048M) 11.602ms&lt;br /&gt;[117.056 s][info][gc]&amp;nbsp;GC(11)&amp;nbsp;Pause&amp;nbsp;Young&amp;nbsp;(Normal)&amp;nbsp;(G1&amp;nbsp;Evacuation&amp;nbsp;Pause)&amp;nbsp;1252M-&amp;gt;60M(2048M)&amp;nbsp;13.200ms&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 생략했지만 총합 100ms 정도의 정지 시간이 발생했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JEP 439: Generational ZGC&lt;br /&gt;These benefits should come without significant throughput reduction compared to non-generational ZGC. The essential properties of non-generational ZGC should be preserved:Pause times should not exceed 1 millisecond,&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ZGC에 대한 공식 문서를 보면 STW는 1 &lt;span style=&quot;text-align: left;&quot;&gt;millisecond를 초과하면 안 된다고 설명되어 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZGC로 테스트해본 로그입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740040165265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[117.562s][info][gc,phases] GC(9) Pause Relocate Start 0.009ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;실제로 1ms 이하로 속도가 나오는 0.009ms로 너무나 명확하게 속도가 빨라진 것을 확인할 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;그러므로 ZGC 측면에서는 G1GC를 사용하는 것은 비효율적이라는 의미가 될 수 있습니다.&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;G1GC의 기본 GC 처리 지연 시간은 200ms입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 정도 시간 이내로 처리되는 것이 안전한 애플리케이션이라면 G1GC를 그대로 사용해도 상관없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼 사용할지 말지에 대한 판단 즉, G1GC가 부하가 오는 정도는 어떻게 파악하면 좋을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;테스트를 하기 위해서는 일단 G1GC의 처리 시간을 보면 안됩니다.(어차피 GC의 처리시간은 200ms이하로 고정일테니)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;방법은 결국에 Heap이 꽉차는지 안차는지만 체크를 하거나 Full GC가 얼마나 자주 일어나서 STW가 빈번하게 발생하는지를 찾아보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;-XX:+PrintGCDetails같은 설정으로 빈번하게 Heap이 꽉차는걸 볼 수 있다면 G1GC의 문제가 생기는 것으로 간주할 수 있습니다.&lt;br /&gt;G1GC는 작은 Region 영역으로 분리해서 GC를 처리하는데 그 영역이 꽉차있다면 분명히 문제가 생기는 것일 테니까요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ZGC는 대부분의 작업을 애플리케이션 실행(application threads)과 동시에(concurrently) 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740040357120&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; [197.362s][info][gc,phases] GC(108) Pause Mark Start 0.697ms
&amp;gt; [198.119s][info][gc,phases] GC(108) Concurrent Mark 757.226ms
&amp;gt; [198.135s][info][gc,phases] GC(108) Pause Mark End 0.802ms
&amp;gt; [198.136s][info][gc,phases] GC(108) Concurrent References Processing 1.461ms
&amp;gt; [198.149s][info][gc,phases] GC(108) Concurrent Reset Relocation Set 12.285ms
&amp;gt; [198.154s][info][gc,phases] GC(108) Concurrent Destroy Detached Pages 0.001ms
&amp;gt; [198.158s][info][gc,phases] GC(108) Concurrent Select Relocation Set 3.239ms
&amp;gt; [198.332s][info][gc,phases] GC(108) Concurrent Prepare Relocation Set 173.931ms
&amp;gt; [198.333s][info][gc,phases] GC(108) Pause Relocate Start 1.010ms
&amp;gt; [198.819s][info][gc,phases] GC(108) Concurrent Relocate 485.961ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실제로 대부분의 작업이 동시처리 되고 STW가 발생하는 부분은 최소 시간 내로 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이를 통해 애플리케이션의 일시 중지(Stop-The-World) 시간을 최소화합니다.&amp;nbsp; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다만,&amp;nbsp;완전히&amp;nbsp;stop-the-world를&amp;nbsp;피할&amp;nbsp;수는&amp;nbsp;없고,&amp;nbsp;매우&amp;nbsp;짧은&amp;nbsp;시간(1ms&amp;nbsp;이하) 동안의&amp;nbsp;일시&amp;nbsp;중지는&amp;nbsp;발생합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&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;STW&amp;nbsp;시간은&amp;nbsp;매우&amp;nbsp;짧고&amp;nbsp;대부분은&amp;nbsp;백그라운드에서&amp;nbsp;GC가&amp;nbsp;진행됩니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동시 처리하기 때문에 애플리케이션의 CPU나 Concurrent 작업을 위한 추가 메모리 overhead가 필요하기 때문에 최소 권장 힙 크기는 8GB고 최대는 16TB로 설정한다고 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;작은 힙에서는 GC의 오버헤드가 상대적으로 커질 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>☕ Java</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/542</guid>
      <comments>https://stir.tistory.com/542#entry542comment</comments>
      <pubDate>Tue, 11 Feb 2025 15:10:27 +0900</pubDate>
    </item>
    <item>
      <title>설국 - 가와바타 야스나리</title>
      <link>https://stir.tistory.com/540</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;허무함&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 길게 설명하려고 해도 도저히 담아낼 수 없을 공감을 전달해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 읽은 지는 좀 되었지만 글을 적지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살다 보면 외로움을 느낄 때가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가 옆에 없을 때라든가, 아니면 반대로 누군가 옆에 있어도 외로움을 느끼는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 계절이 돌아오듯 살면서 가끔씩 쉽게도 그런 걸 느끼게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외롭고 고독하면서도, 때로는 타인이 귀찮게 느껴지는 복잡한 감정들.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런 경험이 나에게 존재하는지 고찰해 본 적은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주인공은 열정적으로 사랑하는 두 여자의 모습을 바라보며 신기해하기도 하며 때로는 원하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 주인공의 마음에서는 허무함을 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 전달하고자 하는 것은 주인공의 외로움보다 더 근원적인 감정인 허무함이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외롭고 고독한 마음은 나의 인간적인 감정일 뿐, 사람으로서 느끼는 허무함을 이 책에서는 전달하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주인공은 두 여자를 좋아하면서 동경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신에게 있는 허무한 감정이 두 여자에게는 없어 보였을테니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여자들은 주인공의 외롭고 허무한 감정을 달래주는 수단이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에 이 책이 전달해 주는 것은 허무함에 대한 위로.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 허무함을 인식하고 나서 다시 순수하고 열정적일 수 있게 도와주는 책이었다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사랑에 대한 또 다른 형태가 있음도 공감해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에 주인공으로 등장하는 인물들은 흔히 말하는 결실이나 결과는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 것도 사랑의 한 형태라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이별을 해야 할 때를 직감했다는 것조차, 그건 사랑의 결과물이었다고 생각한다.&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/540</guid>
      <comments>https://stir.tistory.com/540#entry540comment</comments>
      <pubDate>Sat, 8 Feb 2025 17:38:28 +0900</pubDate>
    </item>
    <item>
      <title>젊은 베르테르의 슬픔 - 요한 볼프강 폰 괴테</title>
      <link>https://stir.tistory.com/537</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;줄거리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 책은 괴테의 이야기를 담은 책이며 전체적인 줄거리는 '짝사랑'이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주인공인 베르테르는 약혼자가 있는 여주인공 로테를 짝사랑했다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이루어질 수 없는 사랑, 지독한 상사병으로 인해 결국 버틸 수 없는 마음으로 자살하고만다.&lt;/span&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;현대적 의미&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 책의 내용을 모방하여 실제 죽은 사람도 있을 정도고 여자들은 로테처럼 사랑받길 원했다고도 한다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 책이 가져다주는 현대적 의미는 대단한데, &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사회적으로 존경받거나 유명한 사람을 모방하는 자살 시도를 말하는 '베르테르 효과', &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;롯데 기업의 '롯데'라는 이름은 이 책에서의 여주인공 이름을 말한다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 이 책이 1700년대 유명세를 떨쳤던 건 지나치게 이성주의를 강조하던 시대에 낭만과 본성에 집중한 책이기 때문이다. 오히려 지금을 살고 있는 현대인에게는 조금 재미가 없을 수도 있다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;당시에는 이런 주제가 없었으니 거의 주말 드라마 수준의 몰입도 있는 책이었을 것이다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;나의 생각&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그래서 나도 조금은 고리타분한 내용이라고 느꼈던 것도 부정할 수 없다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼에도 나는 이 책에서 얻고자 하는 생각을 분명히 하고 싶었는데, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 책의 옮긴이 이신 박찬기님께서 마지막에 적은 문장이 오히려 더 생각할 거리를 제공했다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;&quot;괴테 자신은 이 작품으로 자기 내면의 정신적 압박을 청산하고 다시 새로운 단계의 보다 높은 인생의 길을 걸었음을 우리는 잘 알고 있다.&quot;&lt;/span&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;과연 괴테는 이 고통으로 인해 얻은 것이 무엇일까 생각해 봤다.&lt;/span&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;고통을 처리하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;괴테는 소설을 통해 자신의 고통을 쏟아내고 감정적으로 해방하며 생각에 사로 잡히지 않는 방법을 배웠을 것이다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;비단 이것은 짝사랑뿐만 아니라 다른 고통도 포함된다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자신이 힘들면 적절하게 해소하는 방법도 알아야 한다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;쏟아내는 것뿐이랴, 그 뒤에는 같은 고통을 멀리하거나 피하는 방법도 충분히 생각할 수 있었을 것이다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 때로는 그런 고통을 봐주고 인정해 줄 수 있는 사람을 더 감사하게 여길 것이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;가끔은 일방적이지만 누군가를 지켜보는 데에서도 얻는 깨달음은 있다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;분명히 괴테는 자신의 내면의 소리에 집중함으로 인해 자기 자신을 돌아볼 수 있는 성숙함이 생겼었다고 표현할 수 있겠다.&lt;/span&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;관계의 이해&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;괴테는 그만큼 인간관계의 복잡성에 대해 느꼈을 테고 상대방의 마음을 더 잘 이해할 수 있었을 것이다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그와 동시에 그러한 복잡함 속에서 이성과 감정을 조화롭게 다스리는 것도 배울 수 있었을 것이다.&lt;/span&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;순수함의 예찬&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;베르테르는 정말 순수하게 로테를 이성적인 감정으로 사랑했다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나도 그런 아름다운 사랑을 굉장히 좋아하는데(베르테르도 굉장히 예찬하는 부분이 책에 존재한다.)&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;언제든지 순수하게 사랑할 수 있음에 대한 예찬이랄까, 나의 원초적인 감정을 이해받는 느낌이어서 좋았을 뿐만 아니라 이성적으로 생각해야만 사랑할 수 있는 사회적 이미지에서 벗어나, 원초적인 사랑을 하는 것에 대해 다시 한번 공감해 주는 책이라고 생각했다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;행복은 때로는 불행함을 주기도 한다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 주인공처럼 남은 건 고통뿐이라 생각하면 그건 정말 아쉬운 것이 아닐까.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;때로는 이루어질 수 없다면 고통을 쏟아내고 피하고 막으며 다시금 더 성숙한 인생을 사는 것이 더 바람직할 것이라는 데에는 결과적으론 나도 동의를 했다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/537</guid>
      <comments>https://stir.tistory.com/537#entry537comment</comments>
      <pubDate>Mon, 27 Jan 2025 02:11:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] RFC 7232 - Conditional Requests로 비용 및 부하 최적화 하기</title>
      <link>https://stir.tistory.com/536</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RFC 7232 - Conditional Requests는 &lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;브라우저가&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;불필요한&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;서버&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;요청을&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;최소화하고&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;네트워크&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;트래픽과&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;서버&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;자원을&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;절약할 수 있도록 도와주&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;는 인터넷 표준 정의 문서를 말합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;이 기능을 잘 사용한다면 AWS와 같은 클라우드 환경에서 비용 최적화 및 대용량 트래픽에서의 트래픽 개선도 가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;기능 소개&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot; data-contrast=&quot;none&quot; data-usefontface=&quot;true&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;If-Modified-Since의&amp;nbsp;시간&amp;nbsp;기준으로&amp;nbsp;업데이트된&amp;nbsp;것만&amp;nbsp;조회&amp;nbsp;가능​&amp;nbsp;​&lt;/li&gt;
&lt;li&gt;ETag와 Last-Modified 기준으로 기존에 받았던 정책 값을 캐시 해서 사용 가능​ ​&lt;/li&gt;
&lt;li&gt;브라우저의 캐시 자동 처리로 인해 서버에 불필요한 요청 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 위의 기능들은 굳이 규약을 안 지켜도 만들 수 있으나 규약을 지켜서 만든다면 좀 더 효율적인 코드 개발이 가능해지겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 활용 예시를 몇개 확인해 보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;활용 예시&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;If-Modified-Since의&amp;nbsp;시간&amp;nbsp;기준으로&amp;nbsp;업데이트된&amp;nbsp;것만&amp;nbsp;조회&amp;nbsp;가능&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/88Z1x/btsLUZU9JaL/teqsGTtIGoWXgUOo183RA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/88Z1x/btsLUZU9JaL/teqsGTtIGoWXgUOo183RA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/88Z1x/btsLUZU9JaL/teqsGTtIGoWXgUOo183RA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F88Z1x%2FbtsLUZU9JaL%2FteqsGTtIGoWXgUOo183RA0%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;1079&quot; height=&quot;475&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;475&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;근데 느껴지시겠지만 이건 굳이 Header가 아니어도 구현이 가능합니다.&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;b&gt;ETag와 Last-Modified 기준으로 기존에 받았던 값을 캐시 해서 사용 가능&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWa5sg/btsLUlShxCS/07PEcu4vOkghQvXpLQ2vnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWa5sg/btsLUlShxCS/07PEcu4vOkghQvXpLQ2vnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWa5sg/btsLUlShxCS/07PEcu4vOkghQvXpLQ2vnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWa5sg%2FbtsLUlShxCS%2F07PEcu4vOkghQvXpLQ2vnK%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;1234&quot; height=&quot;819&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 활용 예시는 ETag를 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETag에 응답 값에 대한 HashCode를 추출해서 사용자에게 넘겨줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 첫 번째 요청은 단순히 200 OK로 정상 응답으로 끝나지만 두 번째 요청부터는 클라이언트는 ETag 값을 다시 Header에 Last-Modified 값으로 보낸다면 서버는 이를 확인해 304 Not Modified를 리턴합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 브라우저에서는 304 Not Modifed를 확인하고 이전에 받았던 응답값을 그대로 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버는 Response로 응답 값을 제공해주지 않아도 된다는 점이 중요합니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f5f5f5; color: #000000; text-align: left;&quot;&gt;브라우저의&amp;nbsp;캐시&amp;nbsp;자동&amp;nbsp;처리로&amp;nbsp;인해&amp;nbsp;서버에&amp;nbsp;불필요한&amp;nbsp;요청&amp;nbsp;제어​​&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9tvMW/btsLWceR6m6/oyuqjKTtGRDg2GiQUlPXo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9tvMW/btsLWceR6m6/oyuqjKTtGRDg2GiQUlPXo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9tvMW/btsLWceR6m6/oyuqjKTtGRDg2GiQUlPXo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9tvMW%2FbtsLWceR6m6%2FoyuqjKTtGRDg2GiQUlPXo0%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;962&quot; height=&quot;878&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분도 마찬가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 Cache에 대한 시간을 설정을 해주고 사용자는 해당 캐싱 시간 동안 똑같은 API 요청을 한다면 disk에 저장된 캐시를 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, API 요청을 하더라도 아예 서버에 전달이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 굳이 어렵게 캐시 기능을 만들지 않아도 되겠습니다.&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/536</guid>
      <comments>https://stir.tistory.com/536#entry536comment</comments>
      <pubDate>Tue, 21 Jan 2025 17:23:52 +0900</pubDate>
    </item>
    <item>
      <title>HTTP/1.1 vs HTTP/2 - 동시 요청 처리 방법에 따른 대용량 트래픽 처리</title>
      <link>https://stir.tistory.com/535</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발에서는 요청을 동시적으로 처리해야 하는 경우가 빈번합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 HTTP 1.1과 HTTP 2의 처리 방식 차이를 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, Spring에서는 HTTP/2를 수동으로 활성화해야 사용 가능합니다. HTTP/2는 Server에 의존하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 HTTP/2는 TLS 위에서 작동하기 때문에 아래와 같은 키스토어를 만들어주고 시작해야합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;gt; keytool -genkeypair -alias multiflexing -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore src/main/resources/keystore.p12 -validity 3650 -storepass password -dname &quot;CN=localhost, OU=Dev, O=Organization, L=City, ST=State, C=KR&quot; -keypass password&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;HTTP 1.1&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HOL Blocking: HTTP 1.1은 기본적으로 하나의 TCP 연결에 하나의 요청만 처리하므로, 하나의 요청이 지연되면 다른 요청도 함께 지연되는 HOL Blocking 문제가 발생합니다. 하지만 실제로 개발해보면 지연 현상을 느낀 적이 없을 겁니다. 그런 경우는 보통 브라우저가 처리해주게 됩니다.&lt;/li&gt;
&lt;li&gt;브라우저의 처리: 최신 브라우저는 여러 요청을 병렬로 처리하기 위해 병렬 처리 건 마다 TCP 연결을 여러개 사용합니다. 이로 인해 요청들이 동시 처리되지만, 그만큼 부하가 발생할 수 있습니다. 즉, 연결이 많아지면 리소스 소비가 증가하고 성능이 저하될 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ch69VS/btsLTQx0zVi/UhCerDQ11R2IZYM6xL6RRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ch69VS/btsLTQx0zVi/UhCerDQ11R2IZYM6xL6RRk/img.png&quot; data-alt=&quot;https://victoriametrics.com/blog/go-http2/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ch69VS/btsLTQx0zVi/UhCerDQ11R2IZYM6xL6RRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fch69VS%2FbtsLTQx0zVi%2FUhCerDQ11R2IZYM6xL6RRk%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;718&quot; height=&quot;614&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://victoriametrics.com/blog/go-http2/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 여러 연결을 한번에 TCP로 연결해서(할 수 있는 만큼) 수립하되 모든 응답이 다 받아져야 그 다음 요청이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 3개중에 한개라도 지연되면 연결이 안되는 현상이 HOL Blocking입니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 비교할 수 있는 사이트는 아래와 같습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1737444936071&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;HTTP vs HTTPS Test&quot; data-og-description=&quot;Encrypted websites protect our privacy and are significantly faster. Run this test and prepare to be amazed. #HttpsEverywhere&quot; data-og-host=&quot;www.httpvshttps.com&quot; data-og-source-url=&quot;http://www.httpvshttps.com/&quot; data-og-url=&quot;http://www.httpvshttps.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Or6Tf/hyX4nbrjBF/rpYFzEADaPZ9gg3l7mWzJK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bzTMKv/hyX4wzlpQ0/Ws2pa1N3ew6FC5POcG6uD1/img.png?width=507&amp;amp;height=555&amp;amp;face=0_0_507_555&quot;&gt;&lt;a href=&quot;http://www.httpvshttps.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://www.httpvshttps.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Or6Tf/hyX4nbrjBF/rpYFzEADaPZ9gg3l7mWzJK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bzTMKv/hyX4wzlpQ0/Ws2pa1N3ew6FC5POcG6uD1/img.png?width=507&amp;amp;height=555&amp;amp;face=0_0_507_555');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;HTTP vs HTTPS Test&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Encrypted websites protect our privacy and are significantly faster. Run this test and prepare to be amazed. #HttpsEverywhere&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.httpvshttps.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;HTTP 2&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티플렉싱: HTTP/2는 단일 TCP 연결을 사용해 여러 요청을 병렬로 처리할 수 있습니다. 이를 멀티플렉싱이라고 하며, HOL Blocking 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;헤더 압축: 또한 HTTP/2는 헤더 압축 기능을 제공해, 요청/응답 데이터의 크기를 줄여 네트워크 비용을 절감하고 트래픽 속도를 향상시킵니다.&lt;/li&gt;
&lt;li&gt;대용량 트래픽: 대용량 트래픽을 처리할 때 HTTP/2는 성능과 자원 효율성 면에서 큰 장점을 제공합니다. 특히 단일 연결에서의 병렬 처리로 리소스 소모를 줄이고 성능을 최적화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;브라우저 호환성: 구형 브라우저는 HTTP/2를 지원하지 않을 수 있지만, 이런 경우 HTTP/1.1로 자동으로 전환되도록 개발하면 호환성 문제를 해결할 수 있습니다. 단, 이를 지원하려면 개발 분량이 추가로 필요할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 1.1은 여러 TCP 연결을 사용하여 동시 요청을 처리하지만, 연결 수가 많아지면 성능 저하와 리소스 소비가 증가할 수 있습니다.&lt;/li&gt;
&lt;li&gt;HTTP/2는 단일 TCP 연결로 여러 요청을 병렬 처리하고, 헤더 압축과 멀티플렉싱을 통해 성능과 효율성을 크게 개선할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대용량 트래픽을 처리하거나 성능 최적화가 중요한 프로젝트에서는 HTTP/2를 적극 고려하는 것이 좋습니다. 다만, 구형 브라우저 지원을 고려하면 HTTP/2와 HTTP/1.1을 함께 지원하는 방식으로 개발해야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과적으로 병렬 요청 처리나 헤더 압축이 큰 이점이 아니라면&lt;/b&gt;, 대용량 트래픽이라도 &lt;b&gt;HTTP/1.1&lt;/b&gt;로 충분히 처리할 수 있습니다.&lt;/p&gt;</description>
      <category> ️ Web</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/535</guid>
      <comments>https://stir.tistory.com/535#entry535comment</comments>
      <pubDate>Tue, 21 Jan 2025 15:22:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] gRPC 사용법</title>
      <link>https://stir.tistory.com/534</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/stir084/spring-grpc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에 코드가 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;gRPC란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 구글에서 만든 &lt;b&gt;원격 프로시저 호출(RPC)&lt;/b&gt; 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC는 원격 위치에 있는 서버에서 함수를 직접 실행하는 방식으로, 클라이언트가 서버의 함수를 마치 로컬 함수처럼 호출할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 저지연성을 위해 &lt;b&gt;HTTP/2&lt;/b&gt; 위에서 동작하며, 주로 &lt;b&gt;클라이언트 - 서버&lt;/b&gt; 간의 요청-응답을 처리하는데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 여기서 말하는 클라이언트-서버는 React와 Spring Boot처럼 웹 프론트엔드와 백엔드를 의미하는 것이 아니라, &lt;b&gt;마이크로서비스 아키텍처(MSA)&lt;/b&gt; 환경에서 분산된 시스템 간의 통신을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 데이터가 &lt;b&gt;바이너리 형식&lt;/b&gt;으로 처리되기 때문에, 사람이 읽기 어렵고 개발자가 아닌 일반 사용자와의 상호작용에는 적합하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면 서버 간의 초고속 데이터 전송에만 주로 쓰인다고 볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 gradle에 gRPC와 관련된 의존성을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737348646656&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'net.devh:grpc-server-spring-boot-starter:2.15.0.RELEASE'
implementation 'net.devh:grpc-client-spring-boot-starter:2.15.0.RELEASE'
implementation 'io.grpc:grpc-netty-shaded:1.58.0'
implementation 'io.grpc:grpc-protobuf:1.58.0'
implementation 'io.grpc:grpc-stub:1.58.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래와 같이 gRPC Server Port를 연결 해주면 HTTP/2 위에서 동작하도록 설정이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737348611766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;grpc:
  server:
    port: 9090&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정했다면 gRPC 통신에서 필요한 .proto 파일을 만들어야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 .proto 파일을 만들기 전에 Protocol Buffer가 무엇인지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;gRPC의 Protocol Buffer&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로토콜 버퍼(Protocol Buffers, Protobuf)는 네트워크를 통해 전송하기 위해 데이터를 이진(Binary) 형식으로 직렬화하고 역직렬화하는 것(이하 Serde - &lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Serializer/Deserialaizer로 표현함)&lt;/span&gt;을 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터를 바이너리 형식으로 변경 시 &lt;b&gt;전송 및 저장 시 데이터 크기를 줄이고, 성능을 향상&lt;/b&gt;시킬 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;흔히 개발 할 때 HTTP에서 Content-Type: json으로 설정하면 자동으로 Spring에서 Jackson Library가 Serde&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;를 해주는 개념을 gRPC에선 프로토콜 버퍼가 하는 것이라고 생각할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- HTTP는 주로 텍스트 형식(JSON, XML 등)으로 데이터를 전송합니다.&lt;br /&gt;- TCP는 바이트 스트림 형식으로 데이터를 처리하며 전송 시에는 바이너리 형식의 원시 데이터를 사용합니다.&lt;br /&gt;- gRPC는 처리, 전송 과정에서 모두 바이너리 형식으로 데이터를 전송하되, 프로토콜 버퍼를 사용해 데이터를 직렬화하고 역직렬화하여 더 효율적으로 통신합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;일반적인 HTTP나 TCP 통신에서도 프로토콜 버퍼를 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;예를 들어 HTTP 같은 경우 Content-Type: application/x-protobuf로 설정하게 되면 프로토콜 버퍼를 사용할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;하지만 데이터가 바이너리 형식이기 때문에 사용자 친화적이지 않아서 보통은 사용하지 않고 말 그대로 서버간의 통신을 위해서만 사용한다고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC 같은 경우는 프로토콜 버퍼를 이용하는 특수한 방식이라고 생각할 수 있습니다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;.proto 파일 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 .proto 파일은 개발자가 수동으로 작성하는 것이 가장 일반적인 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737347261358&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

option java_multiple_files = true;
option java_package = &quot;com.example.grpc.proto&quot;;

package com.example.grpc;

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse) {}
}

message UserRequest {
  int32 id = 1;
}

message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 키워드는 gRPC에서 사용할 원격 호출(RPC) 메서드를 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에선 UserService 클래스에 GetUser 메서드를 사용합니다.(아래에서 이것을 토대로 Java 파일을 만듭니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;message는 RPC 호출에서 주고받는 데이터의 구조를 정의합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvitQE/btsLSn3MR7U/pciWEdNBI2gzPGK58aLQ31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvitQE/btsLSn3MR7U/pciWEdNBI2gzPGK58aLQ31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvitQE/btsLSn3MR7U/pciWEdNBI2gzPGK58aLQ31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvitQE%2FbtsLSn3MR7U%2FpciWEdNBI2gzPGK58aLQ31%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;316&quot; height=&quot;280&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;프로젝트를 빌드하면 Protocol Buffer 컴파일러가 .proto 파일을 .protoc 파일로 컴파일 하면서 .proto 안에 들어있던 Service와 Message를 java 파일로 만들어서 사용합니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;message에서 각 필드에 붙는 숫자(예: 1, 2, 3)는 필드 번호(tag number)를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 번호는 &lt;b&gt;Protocol Buffers 직렬화&lt;/b&gt; 및 &lt;b&gt;역직렬화&lt;/b&gt; 과정에서 사용되는 중요한 요소입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;IntelliJ와 같은 IDE에서는 Protocol Buffer 플러그인을 제공하여, 프로토콜 버퍼 파일을 쉽게 작성하고 관리할 수 있습니다. 이 플러그인은 구문 강조, 의미론적 분석,&amp;nbsp;참조 탐색&amp;nbsp;등 다양한 기능을 제공합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RPC가 사용할 서비스 클래스 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 설명했던 대로 프로젝트를 한번 빌드하고 나면 .proto 파일에 따라 생성된&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserServiceGrpc.UserServiceImplBase를 상속해서 Java 파일을 만들어 줘야합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 .proto에서 정의한 내용과 매칭되도록 UserService를 만들어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737351184572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    
    @Override
    public void getUser(UserRequest request, StreamObserver&amp;lt;UserResponse&amp;gt; responseObserver) {
        // 샘플 사용자 데이터 생성
        UserResponse response = UserResponse.newBuilder()
                .setId(request.getId())
                .setName(&quot;John Doe&quot;)
                .setEmail(&quot;john.doe@example.com&quot;)
                .build();
                
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래와 같이 gRPC 요청을 해보면 정상적으로 응답하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUQuLJ/btsLSM9Ghd9/K7Xdb4uPqMDxQWo3LI1XS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUQuLJ/btsLSM9Ghd9/K7Xdb4uPqMDxQWo3LI1XS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUQuLJ/btsLSM9Ghd9/K7Xdb4uPqMDxQWo3LI1XS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUQuLJ%2FbtsLSM9Ghd9%2FK7Xdb4uPqMDxQWo3LI1XS1%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;863&quot; height=&quot;323&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;323&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;추가적인 개념&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;gRPC의 비동기 통신&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;gRPC는 HTTP/2의 멀티플렉싱을 기반으로 한 비동기적인 클라이언트-서버 통신을 지원합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마치 WebFlux처럼 클라이언트는 비동기적으로 데이터를 받을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;gRPC는 WebFlux를 대체하는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반은 맞고 반은 틀립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 처리 영역을 알아보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;WebFlux의 비동기 처리 영역&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프론트엔드-백엔드 간 통신 비동기&lt;/li&gt;
&lt;li&gt;백엔드 내부의 블로킹 작업(DB, 외부 API 등) 비동기 처리&lt;/li&gt;
&lt;li&gt;전체 애플리케이션의 반응형 스트림 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;gRPC의 비동기 통신 영역&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주로 서버-서버 간 통신의 비동기 처리(Request/Response 비동기 통신 패턴 지원)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 정리해볼 때 gRPC가 대체할 수 있는 부분은 Request, Response를 주고 받을 때의 비동기 통신이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/534</guid>
      <comments>https://stir.tistory.com/534#entry534comment</comments>
      <pubDate>Mon, 20 Jan 2025 14:53:09 +0900</pubDate>
    </item>
    <item>
      <title>인간 실격 - 다자이 오사무</title>
      <link>https://stir.tistory.com/532</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;작가의 삶&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다자이 오사무의 인간 실격을 읽었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다자이 오사무는 39살이라는 짧은 나이를 살면서 자살 시도를 5번을 하고 1948년, 결국에 성공했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;살아있는 동안 모든 것이 고통과 인내의 연속이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;행복이라는 감정은 불투명하고 그 행복 또한 대부분은 자신과 처지가 비슷한 누군가를 보며 연민하며 생기는 행복이었던 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그것을 알 수 있는 이유는 인간 실격에 나오는 주인공은 작가의 삶을 투영한 것이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;작가는 일본의 전쟁 패배로 인한 암흑기를 살았던 남자다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그로 인해 겪었던 생애의 지옥과 같은 삶들을 써 내려간 책이 바로 이 책이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;美의 정의&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인간 실격이라는 제목 그대로 사회적인 인간으로는 볼 수 없을 만큼 최악인 남자가 등장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아마 대부분 많은 사람들이 책의 시작부터 불쾌감을 느낄 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사회적으로 어울리지 못하는 사람들을 보면 솔직히 말해서 역겨운 기분을 느끼는 사람도 있으니까 말이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;더 나아가 자신의 연인이나 자식에게도 비사회적인 행동을 보기가 싫어 과도하게 사회적인 행동을 요구하는 것도 심심찮게 찾아볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그래서 이 책도 마찬가지로 생각하지 않고 읽으면 결국에 &quot;아 이 책 기분 더럽네. 배운 것도 없다&quot;라고 느끼기에도 충분할 정도의 책이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주인공은 누군가를 사랑할 수 없고 사랑받을 수도 없는 정신세계이지만 초월한 사람이 아니고서야 누구나 그러한들 사람의 인정으로부터 벗어날 수 없기에 그 사이에서 어떻게 생존해야 할지 철저하게 고민했던 사람이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주인공의 삶은 지속적으로 타인의 시선에 반응하는 사람이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 그 반응 속에서 자신이 느끼는 악랄한 생각들을 한치의 망설임 없이 생각해 냈다.(정확히는 작가가 쓴 것이지만)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이해하지 못할 타인과 타협하며 살아가는 삶과&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;내면은 썩어가고 있었던 것을 그대로 표현했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자신도 누군가를 인정할 수 없고 인정받지 못하는 상태의 고통이란 너무 컸을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;책의 처음부터 끝까지 자신의 생각과 관념을 폭죽 안에 넣어 터트리고 산화하기라도 하듯 인간으로서는 실격된 모습으로 우리에게 전달하고 끝낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 과정에서 자기 변명은 존재하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 책을 읽고자 했던 이유는 인간의 외로움에 대한 해결은 &quot;인정받음&quot;이라고 생각했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주인공도 역시 그 인정이라는 단어의 속박에서 벗어날 수가 없었기에 온갖 익살을 부리면서 살아갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;책의 처음에는 &quot;부끄럼 많은 생애를 보냈습니다&quot;로 시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;부끄럽기 때문에 창피했다는 것이 아니라 부끄럼 그 자체를 표현한 책이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;사회적인 죽음&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나조차도 한명의 사회인으로서 남의 시선도 맞춰주고 내면도 잘 볼 줄 아는 사람이라고 생각했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;근데 이 책은 이렇게 남의 시선을 철저히 무시하고 자신의 내면 만을 그려내는 것은 놀랍기만 하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;왜냐면 그건 한 명의 사회인으로서 죽음을 뜻하는 것이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;'사회적인 죽음'&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;별로 생각해본 적 없는 주제다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;정신적인 죽음과 신체적인 죽음은 있지만 사회적으로 죽는다는 것은 조금 어려운 주제다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 많은 정신적인 고통은 사회적인 문제로부터 오긴 하지만 말이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사회적인 죽음은 곧 사회적인 문제로부터 온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;작가가 살던 암흑기의 일본이 아닌 지금의 일본의 사회적인 문제는 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최근에 일본의 '빈 집 문제'에 관한 다큐멘터리를 본 적 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;핵심은 일본의 저출산 문제였는데, 이 저출산 문제가 야기되는 사회적 문제가 일본과 한국이 서로 너무 달랐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한국의 저출산 문제는 누구나 알 듯이 결혼, 젠더 갈등, 주거 문제 등에 대해 암울한 미래에 공감할 수 밖에 없는 구조다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;현재는 한국이 일본의 GDP도 따라잡은 상태이고 세금 문제, 물가 문제에 있어서도 훨씬 긍정적인 상태다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 일본은 한국이 가지고 있는 사회적인 문제가 전혀 존재하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 취업 문제에 있어서도 더 낙관적인 분위기다.(전반적으로 신입은 취업해서 배우자는 분위기)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이런 낙관적인 분위기 속에서도 아이를 낳지 않거나 결혼을 하지 않는 사람이 훨씬 많아졌는데, 그런 사람들은 사회적인 문제 속에서 부정적인 생각을 키우고 미래의 암울함을 생각하기보다 자신만의 행복을 찾는 다양함을 추구하기 때문이다.(물론 한국인 처럼 생각하는 사람도 더러 있겠지만)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;물론 일본 정부는 그 것 자체를 사회적인 문제로 바라보고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;어쨌든 일본 사회는 결혼을 하지 않았던 사람도 &quot;그 쪽이 행복하다면 결혼하지 뭐&quot;라든가, 그런 분위기다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부정적이기 때문에 싫다는 것과 다른 행복한 것도 있기 때문에 싫다는 것은 너무 극명한 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나는 한국에 살면서 사회적인 문제를 꽤나 인정하며 사는 사람 중에 한명이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;근데 재밌는 점은 군중심리 같은 건 쉽게 휩쓸리지 않는데, 이런 사회적인 거대한 문제는 쉽게 휩쓸리지 않았나라는 생각이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉, 나는 사회적인 구속력을 그대로 느끼는 사람 중 한명이라는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사회적인 기준에 맞지 않을 때 &quot;그렇게 살면 행복하지 않잖아&quot;라는 생각이 가득했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 실제면서 물리적으로도 일본이 더 살기 힘든데도 미래의 문제에 대해 낙관적이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 분명히 사회가 만든 심리의 영향이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사회적인 심리가 때로는 우리에게 도움이 될 때도 있지만(지금은 경쟁 심리의 긍정적 작용 밖에 떠오르지 않지만), 가끔은 이런 사회적인 모습을 말살시켜 놓은 채로 내가 원하는 행복이 어떤 것인지 알아야 할 필요가 있을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;안그러면 지금처럼 사회가 만들어놓은 부정적인 생각으로 인해 결혼을 기피하는 현상이 가득할테니까 말이다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그만큼 우리는 살아가면서 나의 내면을 모르고 살아가기도 하고 아니면 아예 모를지도 모른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결국에 나의 내면을 바라볼 줄 아는 사람은 나여야 하겠고 그게 산화이든 공감이든 나를 위로해 주는 것은 인생의 말미에서 나 자신 밖에 없지 않을까 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/532</guid>
      <comments>https://stir.tistory.com/532#entry532comment</comments>
      <pubDate>Fri, 17 Jan 2025 10:48:07 +0900</pubDate>
    </item>
    <item>
      <title>[Java] JMH(Java Microbenchmark Harness)를 이용한 성능 테스트</title>
      <link>https://stir.tistory.com/531</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 소스는 &lt;a href=&quot;https://github.com/stir084/JMH-Benchmark&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;JMH(Java Microbenchmarking Harness)는 Java 애플리케이션에서 작은 코드 조각, 즉 &quot;마이크로벤치마크&quot;의 성능을 정확하고 신뢰성 있게 측정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문자열을 +로 결합하는 방식과 StringBuilder의 속도 차이 등을 벤치마크 해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 synchronized, Lambda와 반복문. hashCode() &amp;amp; equals(), 배열 처리 방식(병렬, 순차), 기본 타입과 객체 타입 간 변환 성능 차이 등 작은 코드 조각에 대한 성능을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;요즘에는 컴퓨터 성능이 다 좋아서 웬만하면 신경을 쓰지 않아도 된다고 주장하는 경우도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 클라우드 환경에 대한 성능을 최소한으로 사용해서 비용을 아껴보는 전략이 될 수도 있고 대규모 시스템에서 자주 쓰는 메소드에 대한 최적화(Warmup 대상인 코드를 파악하는 경우 얼마나 빠른지 벤치마크)를 해볼 수도 있습니다.&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;Gogo~&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실행 방법과 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jmh gradle 설정을 하고 터미널에서 아래와 같이 입력하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736729258031&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew clean jmh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 실행 횟수는 gradle 파일에서 관리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736729473341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jmh {
    warmupIterations = 2 // (warmup 횟수)
    iterations = 3 // (성능 테스트 횟수)
    fork = 1 // (JVM 프로세스 개수)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 JMH가 수행하는 Warmup을 알아야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 처음에 서버에 있는 바이트 코드 내에 메소드를 인터파일러로 컴파일하다가 해당 메소드가 자주 호출될 것이라고 판단하면, Hot Spot Code로 인식하고&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;JIT Compiler로 컴파일을 시작하여 최적화 한 뒤에&lt;/span&gt;&amp;nbsp;네이티브 코드로 변환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 미리 강제로 하는 것을 Warmup이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 성능 테스트를 위해서는 Warmup을 미리 처리한 뒤에 실행은 JVM이 네이티브 코드로 한다고 볼 수 있습니다.&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;일반적으로 반복적이고, 최적화 가능한 특정 패턴을 가진 코드만을 대상으로 Warmup을 수행합니다.&lt;br /&gt;그럼 아래 내용에서 하나하나 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문자열 결합 1(+와 StringBuilder 비교)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1736729336557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Benchmark
  public String stringConcatenationWithPlus() {
    return &quot;Hello&quot; + &quot; &quot; + &quot;World&quot;;
  }

  @Benchmark
  public String stringConcatenationWithStringBuilder() {
    return new StringBuilder()
      .append(&quot;Hello&quot;)
      .append(&quot; &quot;)
      .append(&quot;World&quot;)
      .toString();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kxKyu/btsLLT1hMhW/CHX4FlibjFCheALO3fBF8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kxKyu/btsLLT1hMhW/CHX4FlibjFCheALO3fBF8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kxKyu/btsLLT1hMhW/CHX4FlibjFCheALO3fBF8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkxKyu%2FbtsLLT1hMhW%2FCHX4FlibjFCheALO3fBF8k%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;780&quot; height=&quot;163&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 첫번째 두번째 메소드에 대한 벤치마크 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 서버에서는 +로 문자열을 결합하는 것을 지양한다고 알고있는 경우도 있지만 이런 경우는 오히려 +로 결합하는 것이 더 빠른 것을 볼 수 있습니다. +로 문자열을 결합하는 것을 Warmup하고 실행했을 때는 0.7초로 줄어든 것을 확인할 수 있습니다. 즉, 최적화 시에 &quot;Hello&quot; + &quot;World&quot;와 같은 문자열 결합은 자주 사용되는 패턴으로 인식되어 미리 &quot;Hello World&quot;로 계산되어 미리 최적화될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면에 StringBuilder는 새로운 객체를 만들어야하고 속도에도 변화가 없는 것을 볼 수 있는데 StringBuilder 내부에서 동작하는 코드를 컴파일 타임 최적화하기 어렵기 때문에 StringBuilder는 최적화 코드가 아니란 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문자열 결합 2(랜덤으로 String 생성하기)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서는 자주 사용되고 반복적인 패턴을 미리 최적화해서 빨라졌다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 만약에 값이 변한다면 어떠한 결과가 나올까요?&lt;/p&gt;
&lt;pre id=&quot;code_1736730305092&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  private static final Random random = new Random();

  private String generateRandomString(int length) {
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i &amp;lt; length; i++) {
      sb.append((char) ('a' + random.nextInt(26))); // a-z 문자 랜덤
    }
    return sb.toString();
  }

  @Benchmark
  public String stringConcatenationWithPlus() {
    String str1 = generateRandomString(5);
    String str2 = generateRandomString(5);
    return str1 + &quot; &quot; + str2;
  }


  @Benchmark
  public String stringConcatenationWithStringBuilder() {
    String str1 = generateRandomString(5);
    String str2 = generateRandomString(5);
    return new StringBuilder()
      .append(str1)
      .append(&quot; &quot;)
      .append(str2)
      .toString();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck5q0l/btsLMrwtFHm/C5kNb5QmoOIxTv3nhEQEh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck5q0l/btsLMrwtFHm/C5kNb5QmoOIxTv3nhEQEh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck5q0l/btsLMrwtFHm/C5kNb5QmoOIxTv3nhEQEh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck5q0l%2FbtsLMrwtFHm%2FC5kNb5QmoOIxTv3nhEQEh1%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;763&quot; height=&quot;151&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예상한대로 최적화가 되지 않아서 속도가 오히려 더 느려진 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문자열 결합 3(StringBuilder가 더 좋은 경우)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 StringBuilder가 이제 결합이 많이 일어나는 경우에 더 좋다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 어느정도 결합이 발생해야 좋은지 결합을 강제로 100번 해서 테스트해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736730968181&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  private static final String str = &quot;a&quot;; // 1자 문자열

  @Benchmark
  public String stringConcatenationWithPlus() {
    String result = &quot;&quot;;
    for (int i = 0; i &amp;lt; 100; i++) {
      result += str;  // 문자열 결합을 100번 반복
    }
    return result;
  }

  @Benchmark
  public String stringConcatenationWithStringBuilder() {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i &amp;lt; 100; i++) {
      result.append(str);  // StringBuilder를 사용하여 100번 결합
    }
    return result.toString();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ICoVl/btsLJ8Me6PN/30jFU6o2UPWXD15KmT5tsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ICoVl/btsLJ8Me6PN/30jFU6o2UPWXD15KmT5tsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ICoVl/btsLJ8Me6PN/30jFU6o2UPWXD15KmT5tsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FICoVl%2FbtsLJ8Me6PN%2F30jFU6o2UPWXD15KmT5tsk%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;760&quot; height=&quot;163&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100번만 해도 +의 결과가 굉장히 안좋을 확인할 수 있겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; ArrayList vs LinkedList&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두개의 List의 성능 차이를 비교해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 기본적인 세팅을 해두고 벤치마크 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736744408541&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 SIZE = 100;
  private List&amp;lt;Integer&amp;gt; arrayList;
  private List&amp;lt;Integer&amp;gt; linkedList;

  @Setup(Level.Iteration)
  public void setup() {
    arrayList = new ArrayList&amp;lt;&amp;gt;(SIZE);
    linkedList = new LinkedList&amp;lt;&amp;gt;();
  }

  @TearDown(Level.Iteration)
  public void teardown() {
    arrayList.clear();
    linkedList.clear();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ArrayList에 데이터를 삽입해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736744415897&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Benchmark
  public void insertAtBeginningArrayList() {
    for (int i = 0; i &amp;lt; 100; i++) {
      arrayList.add(0, i);
    }
  }
  @Benchmark
  public void insertAtEndArrayList() {
    for (int i = 0; i &amp;lt; 100; i++) {
      arrayList.add(i);
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boPWS0/btsLLPrvghA/9lVMJBZCS1L431WDEd2igk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boPWS0/btsLLPrvghA/9lVMJBZCS1L431WDEd2igk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boPWS0/btsLLPrvghA/9lVMJBZCS1L431WDEd2igk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboPWS0%2FbtsLLPrvghA%2F9lVMJBZCS1L431WDEd2igk%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;157&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;157&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 방식은 0번째 인덱스에 100회에 걸쳐서 요소를 삽입합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 기존 0번째 들어있던 요소들을 모두 뒤로 밀어야 하기 때문에 성능이 나쁘게 나옵니다.&lt;br /&gt;하지만 두번째는 단순히 배열의 끝에 요소를 추가하는 작업은 일반적으로 O(1) 시간 복잡도를 가집니다&lt;/p&gt;
&lt;pre id=&quot;code_1736744484728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Benchmark
  public void insertAtBeginningLinkedList() {
    for (int i = 0; i &amp;lt; 100; i++) {
      linkedList.add(0, i);
    }
  }
  @Benchmark
  public void insertAtEndLinkedList() {
    for (int i = 0; i &amp;lt; SIZE; i++) {
      linkedList.add(i);
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0fjhU/btsLKaXWd0O/mb8FHmGOPnsILbZdXUN6Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0fjhU/btsLKaXWd0O/mb8FHmGOPnsILbZdXUN6Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0fjhU/btsLKaXWd0O/mb8FHmGOPnsILbZdXUN6Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0fjhU%2FbtsLKaXWd0O%2Fmb8FHmGOPnsILbZdXUN6Pk%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;799&quot; height=&quot;158&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 LinkedList에 삽입 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LinkedList는 이중 연결 리스트로 구현되어 있습니다. &lt;br /&gt;리스트의 앞에 요소를 추가할 때는, 단순히 새로운 노드를 생성하고, 기존 첫 번째 노드의 포인터를 새로운 노드로 연결하면 됩니다. 이 작업은 O(1) 시간에 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새 노드 생성과 링크 연결이 필요하기 때문에 ArrayList보다는 속도가 약간 늦게 나오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736744565852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Benchmark
  public int randomAccessArrayList() {
    // 리스트 채우기
    for (int i = 0; i &amp;lt; SIZE; i++) {
      arrayList.add(i);
    }
    // 1000번의 랜덤 접근
    int sum = 0;
    for (int i = 0; i &amp;lt; 10; i++) {
      sum += arrayList.get(i * (SIZE / 10));
    }
    return sum;
  }

  @Benchmark
  public int randomAccessLinkedList() {
    // 리스트 채우기
    for (int i = 0; i &amp;lt; SIZE; i++) {
      linkedList.add(i);
    }
    // 1000번의 랜덤 접근
    int sum = 0;
    for (int i = 0; i &amp;lt; 10; i++) {
      sum += linkedList.get(i * (SIZE / 10));
    }
    return sum;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpHvJB/btsLKBHxVdX/gBk97Z9BDfVcoQXoteKQzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpHvJB/btsLKBHxVdX/gBk97Z9BDfVcoQXoteKQzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpHvJB/btsLKBHxVdX/gBk97Z9BDfVcoQXoteKQzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpHvJB%2FbtsLKBHxVdX%2FgBk97Z9BDfVcoQXoteKQzk%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;766&quot; height=&quot;159&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayList는 인덱스를 사용해 O(1) 시간 복잡도로 접근합니다. &lt;br /&gt;LinkedList는 노드를 순회하며 접근하므로 O(n) 시간이 소요되므로 속도가 좀 더 느린 것을 확인할 수 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 자주 읽고 끝에만 삽입한다면 ArrayList가 유리&lt;/li&gt;
&lt;li&gt;데이터를 중간이나 앞에 자주 삽입/삭제한다면 LinkedList가 유리&lt;/li&gt;
&lt;li&gt;데이터를 인덱스로 자주 검색한다면 ArrayList가 압도적으로 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 데이터는 신빙성이 떨어질 수 있으므로(Warmup의 결과 등) 필요한 경우 직접해보면서 벤치마크를 해보는 것이 좋을 듯 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>☕ Java</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/531</guid>
      <comments>https://stir.tistory.com/531#entry531comment</comments>
      <pubDate>Mon, 13 Jan 2025 09:49:12 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Kafka Streams 사용법</title>
      <link>https://stir.tistory.com/530</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Streams를 사용하면 특정 시간동안 Kafka에 인입되는 데이터를 집계해서 통계치를 추출할 수 있습니다.&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;a href=&quot;https://github.com/stir084/kafka-streams&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에 올려놨습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 /a라는 메소드 요청에 대해 시간 별로 요청 건 수를 통계화해서 /b로 확인하는 코드입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3wCd5/btsLHsjGp39/OKpoQgspfH5DAfFNhkGgaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3wCd5/btsLHsjGp39/OKpoQgspfH5DAfFNhkGgaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3wCd5/btsLHsjGp39/OKpoQgspfH5DAfFNhkGgaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3wCd5%2FbtsLHsjGp39%2FOKpoQgspfH5DAfFNhkGgaK%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;520&quot; height=&quot;170&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과만 보면 /a라는 메소드는 14시에 2번, 15시에 4번 이용했다는 의미가 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736401844883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class ApiController {

    private final ApiService apiService;

    @PostMapping(&quot;/a&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; callApiA() {
        apiService.processApiCall();
        return ResponseEntity.ok(&quot;API A called successfully&quot;);
    }

    @GetMapping(&quot;/b&quot;)
    public ResponseEntity&amp;lt;Map&amp;lt;String, Long&amp;gt;&amp;gt; getHourlyStats() {
        return ResponseEntity.ok(apiService.getHourlyStats());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736401874368&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ApiService {

    private final KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;
    private final KafkaStreamsService kafkaStreamsService;
    private static final String TOPIC_NAME = &quot;api-calls&quot;;

    public void processApiCall() {
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
        kafkaTemplate.send(TOPIC_NAME, timestamp);
    }

    public Map&amp;lt;String, Long&amp;gt; getHourlyStats() {
        return kafkaStreamsService.getHourlyStats();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/a 메소드를 사용하면 api-calls 토픽에 timestamp를 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736401371509&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; private static final String TOPIC_NAME = &quot;api-calls&quot;;
    private static final String STORE_NAME = &quot;hourly-counts&quot;;
    private KafkaStreams kafkaStreams;

    @Autowired
    private StreamsBuilder streamsBuilder;

    @PostConstruct
    public void init() {
        KStream&amp;lt;String, String&amp;gt; stream = streamsBuilder.stream(TOPIC_NAME, 
            Consumed.with(Serdes.String(), Serdes.String())); 

        stream
            .groupBy((key, value) -&amp;gt; {
                LocalDateTime dateTime = LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
                return dateTime.format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd'T'HH&quot;));
            }, Grouped.with(Serdes.String(), Serdes.String()))
            .count(Materialized.as(STORE_NAME));

        Properties props = new Properties();
        props.put(StreamsConfig.APPLICATION_ID_CONFIG, &quot;api-stats-application&quot;); 
        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;);
        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());

        kafkaStreams = new KafkaStreams(streamsBuilder.build(), props);
        kafkaStreams.start();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본 개념&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; Kafka Streams 애플리케이션의 고유 ID&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Streams를 사용하면 애플리케이션이 사용하는 고유 ID를 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서는 api-stats-application이라고 명명했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID를 지정하는 이유는 간단하게는 서버가 여러 대일 경우 클러스터링이 가능하고 Kafka Streams 애플리케이션 자체가 하나의 소비자 그룹처럼 동작하기 위함입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Serde&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Serde는 '세르드' 혹은 '서드'라고 불리우는 Serialization과 Deserialization의 합성어입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 토픽을 소비할 때 직렬화, 역직렬화에 대한 Serializer를 설정하기 위함인데, 위와 같이 props에 설정해서 전역적으로 사용할 수도 있고 개별적으로 Consumed.with를 통해 옵션 값으로 설정해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Kafka Streams의 상태 저장소&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Streams의 핵심 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통계라고 하면 일정 시간 동안 데이터를 저장해야 하는데 이 때 사용하는 것이 상태 저장소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 저장소는 Kafka Streams가 실행되는 애플리케이션에 RocksDB라는 데이터베이스를 사용해서 디스크에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, Spring Boot를 이용한다면 Spring Boot가 실행되는 서버에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 Kafka Streams 의존성이 추가되면 RocksDB 의존성이 따라 붙습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zpUgh/btsLJxQ7tZ1/K1SnBAuJAJddP4qb3AXeRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zpUgh/btsLJxQ7tZ1/K1SnBAuJAJddP4qb3AXeRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zpUgh/btsLJxQ7tZ1/K1SnBAuJAJddP4qb3AXeRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzpUgh%2FbtsLJxQ7tZ1%2FK1SnBAuJAJddP4qb3AXeRK%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;405&quot; height=&quot;125&quot; data-origin-width=&quot;405&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 상태 저장소에 저장된 데이터는 일정 시간마다 Kafka 브로커에 전달되어 자동으로 생성된 Topic에 데이터를 저장하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서는 hourly-counts라는 상태 저장소(State Store) 를 명명해서 사용했기 때문에 changelog라는 특별한 토픽이 아래와 같이 자동 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bneyHJ/btsLJizTrHX/fylCZOyKYhmnuclbikY6Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bneyHJ/btsLJizTrHX/fylCZOyKYhmnuclbikY6Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bneyHJ/btsLJizTrHX/fylCZOyKYhmnuclbikY6Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbneyHJ%2FbtsLJizTrHX%2FfylCZOyKYhmnuclbikY6Ck%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;845&quot; height=&quot;101&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;changelog가 아닌 일반적으로 생성한 Topic은 바로 Kafka 브로커의 디스크에 저장되는 반면(쉽게 말해 카프카 서버에 저장되는 데이터), 상태 저장소는 우선 DB에 저장되었다가 Kafka 브로커에 데이터를 전달해서 토픽에 저장하는 방식이라고 생각하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 상태 저장소는 Kafka Streams 애플리케이션이 재시작될 때 복구되므로 데이터는 영구적으로 유지됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Distributed System</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/530</guid>
      <comments>https://stir.tistory.com/530#entry530comment</comments>
      <pubDate>Thu, 9 Jan 2025 15:46:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Redis 연결 관리 및 성능 최적화</title>
      <link>https://stir.tistory.com/527</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Connection Pool&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Redis를 사용할 때 기본적으로 &lt;b&gt;lazy connection&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Redis에 대한 최초 연결이 있을 때 뒤늦게 Connection Pool을 가져오는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736229923896&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class RedisWarmupController {
  @Autowired
  private RedisConnectionFactory redisConnectionFactory;
  @Autowired
  private StringRedisTemplate redisTemplate;


  @GetMapping(&quot;/redis-test&quot;)
  public String testRedisConnection() {
    long start = System.currentTimeMillis();
    redisTemplate.opsForValue().set(&quot;key&quot;, &quot;value&quot;);
    String value = redisTemplate.opsForValue().get(&quot;key&quot;);
    long end = System.currentTimeMillis();
    return &quot;Redis call took &quot; + (end - start) + &quot; ms. Value: &quot; + value;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rN8bL/btsLFLJRyKb/6qjUjrCq7DhfKBxKQXkkFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rN8bL/btsLFLJRyKb/6qjUjrCq7DhfKBxKQXkkFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rN8bL/btsLFLJRyKb/6qjUjrCq7DhfKBxKQXkkFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrN8bL%2FbtsLFLJRyKb%2F6qjUjrCq7DhfKBxKQXkkFK%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;547&quot; height=&quot;124&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 261ms가 소요되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736230729727&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostConstruct
public void init() {
  redisConnectionFactory.getConnection(); // 즉시 연결 초기화
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot가 시작 될 때 Connection Pool을 가져오는 방식으로 바꾸면 속도가 빠릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx5yzn/btsLGZ1p5HB/PJHopel6Kw15ZQOU6LeX5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx5yzn/btsLGZ1p5HB/PJHopel6Kw15ZQOU6LeX5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx5yzn/btsLGZ1p5HB/PJHopel6Kw15ZQOU6LeX5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx5yzn%2FbtsLGZ1p5HB%2FPJHopel6Kw15ZQOU6LeX5k%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;531&quot; height=&quot;139&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 항상 Eager하게 가져오는 것은 좋지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 사용을 하지 않을 수도 있기 때문에 미리 올려두면 리소스가 낭비되고 오히려 Spring Boot의 기동 시간이 느려지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;초기 연결 수립&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 결과에서 한번 더 요청을 하면 아래와 같이 속도가 더 빨라지는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AzWl9/btsLG2RnMRy/xWYfTXLWkU8chUR4iDl7eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AzWl9/btsLG2RnMRy/xWYfTXLWkU8chUR4iDl7eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AzWl9/btsLG2RnMRy/xWYfTXLWkU8chUR4iDl7eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAzWl9%2FbtsLG2RnMRy%2FxWYfTXLWkU8chUR4iDl7eK%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;527&quot; height=&quot;118&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 get 메소드를 처리하고나면 속도가 빨라진다는 것인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용해서 Spring Boot가 시작될 때 미리 조회를 해볼 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736229928901&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostConstruct
public void init() {
  redisTemplate.opsForValue().get(&quot;key&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 redis Server와의 핸드쉐이크를 연결 하는 과정 혹은 Redis Serializer를 캐쉬하는 과정에서 초기 연결 속도가 늦는 것으로 판단됩니다.(검색해도 잘 안나오는데 아시는 분 알려주세요.)&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/527</guid>
      <comments>https://stir.tistory.com/527#entry527comment</comments>
      <pubDate>Mon, 6 Jan 2025 16:37:21 +0900</pubDate>
    </item>
    <item>
      <title>2024년의 회고</title>
      <link>https://stir.tistory.com/522</link>
      <description>&lt;p data-end=&quot;69&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;어느덧 2024년이 끝나간다.&lt;/p&gt;
&lt;p data-end=&quot;249&quot; data-start=&quot;159&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;323&quot; data-start=&quot;251&quot; data-ke-size=&quot;size16&quot;&gt;과거의 회고에서는 성장이라는 단어로 두 발을 꽉 묶고 있었지만 지금은 그 단어가 때로는 지치는 상황을 만들기에 충분하다고 느낀다.&lt;/p&gt;
&lt;p data-end=&quot;323&quot; data-start=&quot;251&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;325&quot; data-ke-size=&quot;size16&quot;&gt;그런 감정 때문인지 모르겠지만 여러 주제를 가지고 2024년 회고를 작성했다가 지금은 다시 다 지워버리고 새롭게 작성하고 있다.&lt;br /&gt;결국 모든 주제는 '솔직'이라는 하나의 단어로 귀결되었다.&lt;/p&gt;
&lt;p data-end=&quot;603&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;행복의 기준은 누구나 다르다.&lt;br /&gt;하지만 올해 행복했었다고 자신 있게 말할 수 있는 사람은 그리 많지 않을 것이다.&lt;br /&gt;미래의 불확실성과 때로는 이해하지 못할 타인의 존재, 그리고 세상을 살아가며 지속적으로 발생하는 악연들이 우리를 가끔은 힘들게 했을 것이다.&lt;/p&gt;
&lt;h2 data-end=&quot;779&quot; data-start=&quot;605&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;솔직&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;779&quot; data-start=&quot;605&quot; data-ke-size=&quot;size16&quot;&gt;이러한 상황들을 지속적으로 부딪혀오면서 생각하게 된 단어는 '솔직'이다.&lt;br /&gt;올해 여러 부분에서 솔직하지 않은 상황을 많이 봤다.&lt;br /&gt;그리고 그런 상황은 현재의 상황이 위태할 때 가장 쉽게 발현된다고 느꼈다.&lt;br /&gt;위험한 상황에서는 모두 자신의 이익을 위한 이기적인 마음을 갖기 쉬운 법이다.&lt;/p&gt;
&lt;p data-end=&quot;870&quot; data-start=&quot;781&quot; data-ke-size=&quot;size16&quot;&gt;좀 더 직관적인 표현을 해보자면, 그런 위험한 상황 속에 서 있을 때는 자신도 위험한 사람이 되는 경향이 있다고 생각한다.&lt;br /&gt;즉, 닮는 경향이 있다는 말이다.&lt;/p&gt;
&lt;p data-end=&quot;870&quot; data-start=&quot;781&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;999&quot; data-start=&quot;872&quot; data-ke-size=&quot;size16&quot;&gt;살면서 양심이 허락하지 못하는 행동, 거짓된 마음이 가끔은 이득이라고 느껴질 때가 있다.&lt;br /&gt;실제로 그것이 단발성으로는 이득이 될 수도 있다.&lt;br /&gt;그런 행동은 타인에게 충분히 숨길 수 있고, 심지어 자신에게조차 숨길 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;1167&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;하지만 결국 자신에게 솔직하지 못하고 타인에게 솔직하지 못한 행동은 지속적으로 그 개체에게 조금 거슬릴 정도로 낫지 않는 흉터를 입히며 살아가는 행동이라는 사실을 알아야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1167&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우리도 우리 자신을 타인이라 여겨야 한다.&lt;br /&gt;자신에게 솔직하지 않은 행동에 대한 습관은 타인에게도 그대로 적용된다.&lt;/p&gt;
&lt;p data-end=&quot;1354&quot; data-start=&quot;1169&quot; data-ke-size=&quot;size16&quot;&gt;괜찮게 살고 있는 사람이 이기적이면 괜찮지 않게 살고 있는 사람은 더 힘들어질 수밖에 없다.&lt;br /&gt;찰나에 양심을 어겨가며 한 행동은 흉터가 생기지 않도록 좋은 행동으로 메워야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1354&quot; data-start=&quot;1169&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;주위의 환경이 힘든 상황이라고 해서 그 상황에 매몰되지 않아야 한다.&lt;br /&gt;그 상황에서 자신을 더 친절하게, 타인을 더 친절하게 대해야 한다.&lt;/p&gt;</description>
      <category>  Chitchat</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/522</guid>
      <comments>https://stir.tistory.com/522#entry522comment</comments>
      <pubDate>Sun, 29 Dec 2024 00:49:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 동시성(Concurrency) 이슈 - 그 외(3)</title>
      <link>https://stir.tistory.com/518</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ThreadLocal&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 동시성 문제가 발생해서 쓰는 기능은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 각 스레드마다 고유의 변수를 저장할 수 있게 해주는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 트랜잭션에서 컨텍스트를 유지하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크나 Hibernate 같은 ORM에서는 데이터베이스 트랜잭션이 여러 메서드 호출이나 계층을 넘나드는 동안에도 같은 트랜잭션 상태를 유지해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ThreadLocal이 중요한 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chqYPC/btsKwlFvLgC/cx8RpMkNR15VbyHrts9qAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chqYPC/btsKwlFvLgC/cx8RpMkNR15VbyHrts9qAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chqYPC/btsKwlFvLgC/cx8RpMkNR15VbyHrts9qAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchqYPC%2FbtsKwlFvLgC%2Fcx8RpMkNR15VbyHrts9qAk%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;693&quot; height=&quot;242&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;242&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;트랜잭션에 동기화에 관한 클래스를 살펴보면 ThreadLocal로 구현된 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 스레드에 고유의 변수를 저장하는 것 뿐인데, 왜 동시성 문제가 관련 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 동시성 문제가 발생하지 않도록 회피하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 스레드가 사용하는 변수를 사용하지 않도록, 즉, 동시성 문제를 처음부터 차단하는 것을 목표로 하는 기술입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis에서의 동시성 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Database 주제를 다룰 때 조회수 연산은 INCR 연산, 원자적 연산을 통해 조회수 증감을 올바르게 처리할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 현재 재고를 확인하는 재고 처리 시스템이라고 하면 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 간단하게 증감 연산을 동시성 문제가 발생하도록 만들어보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730781764483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public void incrementUsingRedisWithSleep(Long id) throws InterruptedException {
    String key = VIEW_COUNT_KEY_PREFIX + id;
    Long currentValue = redisTemplate.opsForValue().get(key); // 값을 가져와서
    // currentValue가 null이면 0으로 초기화
    if (currentValue == null) {
      currentValue = 0L;
    }
    Thread.sleep(30000); // 지연 발생
    redisTemplate.opsForValue().set(key, currentValue + 1); // 직접 증분한 값을 저장하며 갱신 분실 문제 발생
  }

  public void incrementUsingRedisWithoutSleep(Long id) {
    String key = VIEW_COUNT_KEY_PREFIX + id;
    redisTemplate.opsForValue().increment(key);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여태까지와 마찬가지로 첫번째 메소드와 두번째 메소드를 순서대로 실행시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS에서는 첫번째 연산은 Rollback 처리가 되었는데, Redis에서는 첫번째 메소드가 실행되어 덮어쓰기 현상 즉, 갱신 분실 문제가 발생합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 사용하더라도 위처럼 트랜잭션으로 데이터의 정합성이 필요한 영역이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 방법 1. 분산 락(Distributed Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴때는 분산 락을 이용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 분산된 환경에서도 락을 이용할 수 있는 방법 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 2대 이상인 경우에 백엔드에서 synchronized를 사용하면 2대에서 동시에 실행될 여지가 있기 때문에 DB 레벨에서 락을 거는 것이라고 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730872371197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public void incrementUsingLock(Long id) throws InterruptedException {
    String key = VIEW_COUNT_KEY_PREFIX + id;
    String lockKey = key + &quot;:lock&quot;;
    // 분산 락 설정
    Boolean lockAcquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, &quot;lock&quot;, Duration.ofSeconds(35));
    if (lockAcquired != null &amp;amp;&amp;amp; lockAcquired) {
      try {
        Long currentValue = stringRedisTemplate.opsForValue().get(key) != null ?
          Long.parseLong(stringRedisTemplate.opsForValue().get(key)) : 0L;
        Thread.sleep(30000); // 지연 발생
        stringRedisTemplate.opsForValue().set(key, String.valueOf(currentValue + 1));
      } finally {
        // 락 해제
        stringRedisTemplate.delete(lockKey);
      }
    } else {
      System.out.println(&quot;Unable to acquire lock, another process is handling the increment.&quot;);
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 lock을 획득한 lockAcquried를 통해서 접근하지 못하게 차단할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현하면 Redis에서도 복잡한 트랜잭션이 필요한 경우 유용하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/518</guid>
      <comments>https://stir.tistory.com/518#entry518comment</comments>
      <pubDate>Tue, 5 Nov 2024 10:55:05 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 동시성(Concurrency) 이슈 - Database(2)</title>
      <link>https://stir.tistory.com/517</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데이터베이스의 동시성 이슈&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 데이터베이스의 동시성 이슈에 대해 얘기해보겠습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데이터베이스의 특징(격리 레벨과 MVCC)&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 흔히 사용하는 RDBMS에서 데이터베이스가 어느 정도까지 동시성을 허용하는지 알 필요가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스를 사용한다고 모든 동시성 문제를 해결해줄 수 있는게 아니기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Read Uncommitted - 커밋되지 않은 업데이트 내역을 읽을 수 있다&lt;/li&gt;
&lt;li&gt;Read Committed - 커밋된 업데이트 내역을 읽을 수 있다. 다만 하나의 트랜잭션에서 조회를 2번할 때 다른 곳에서 커밋한 값이 조회될 수 있다(Non-Repeatable Read)&lt;/li&gt;
&lt;li&gt;Repeatable Read - 초기에 조회한 값은 다른 곳에서 변경하더라도 계속 같은 값이 조회된다. 데이터 변경 시 UNDO 영역에 돌려 놓을 수 있는 상태의 데이터를 백업하고 실제 레코드를 변경한다. 문제 발생시 UNDO LOG로 롤백한다. 하지만 팬텀 리드가 발생해서 하나의 트랜잭션이 동작하는 동안 insert된 것을 읽어올 수도 있다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Serializable - &lt;/span&gt;트랜잭션이&lt;span&gt; &lt;/span&gt;특정&lt;span&gt; &lt;/span&gt;테이블을&lt;span&gt; &lt;/span&gt;읽으면&lt;span&gt; &lt;/span&gt;다른&lt;span&gt; &lt;/span&gt;트랜잭션은&lt;span&gt; &lt;/span&gt;그&lt;span&gt; &lt;/span&gt;테이블의&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;추가&lt;span&gt;/&lt;/span&gt;변경&lt;span&gt;/&lt;/span&gt;삭제할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;없다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB의 기본 격리레벨은 Repeatable Read입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;격리레벨 중 Read Commited와 Repeatable Read는 MVCC(Multi Version Concurrency Content)라는 도구를 이용해서 위와 같은 특징을 가지는 것이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVCC가 가지고 있는 특징에은 쉽게 말해서 &quot;미리 본떠놓는 기능&quot;이라고 할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그것을 &quot;스냅샷&quot;이라고 하는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repeatable Read에서는 하나의 트랜잭션이 시작될 때 해당 테이블을 기준으로 스냅샷, 즉 미리 본떠놓고 시작합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 해당 스냅샷을 기반으로 읽기를 수행합니다. 그러면 실제 데이터베이스에 데이터가 변경되더라도 스냅샷을 이용해 데이터를 조회하니 일관성있게 읽는 것이 가능합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Read Committed에서는 하나의 트랜잭션이 시작될 때가 아니라 트랜잭션 내에서 데이터를 조회할 때 스냅샷을 생성하는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 스냅샷을 이용해서 다른 트랜잭션에서 커밋된 업데이트 내역을 읽을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVCC에는 UNDO LOG라는 것도 존재하는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 트랜잭션이든 데이터가 실제로 변경이 일어나면 UNDO LOG를 저장해놓고 실패 시 이 로그를 통해 데이터를 롤백시키는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 격리 레벨과 MVCC의 충분한 설명은 끝났고 Repeatable Read까지의 격리레벨은 어디까지 동시성 제어를 하는지 다시 설명해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repeatable Read는 결국에 A 트랜잭션 안에서 같은 조회 쿼리를 사용했을 때 일관성있는 쿼리 결과 값을 도출할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말해 데이터의 일관성을 보장할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 상태에서 어떤 동시성 문제가 남았는지 살펴보겠습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동시에 같은 레코드를 수정하면?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전 글에서 CAS(Compare-And-Swap)에 대해 알아봤습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CAS 알고리즘을 사용하면 데이터가 올바르게 처리되는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 DB에서 수많은 트랜잭션이 동시에 하나의 레코드를 동시에 수정하려고 하면 CAS 알고리즘처럼 올바르게 처리될까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아쉽게도 그렇지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Repeatable Read 이하의&amp;nbsp;&lt;/span&gt;격리&lt;span&gt; &lt;/span&gt;수준에서&lt;span&gt; &lt;/span&gt;두&lt;span&gt; &lt;/span&gt;트랜잭션&lt;span&gt; &lt;/span&gt;간&lt;span&gt; &lt;/span&gt;수정&lt;span&gt; &lt;/span&gt;충돌이&lt;span&gt; &lt;/span&gt;발생하면&lt;span&gt;, &lt;/span&gt;&lt;b&gt;대부분의&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;데이터베이스는&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;충돌을&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;감지하여&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;해당&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;트랜잭션을&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;실패&lt;/b&gt;시키고&lt;span&gt; &lt;/span&gt;롤백하는&lt;span&gt; &lt;/span&gt;방식을&lt;span&gt; &lt;/span&gt;택합니다&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다시 말해 데이터베이스에서 갱신 분실 문제(Lost Updated) 문제를 해결해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어서 조회수를 올리는 비즈니스 로직을 상상해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730358485774&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Transactional
  public void incrementWithSleep(Long id) throws InterruptedException {
    ViewCount viewCount = viewCountRepository.findById(id)
      .orElseThrow(() -&amp;gt; new RuntimeException(&quot;ViewCount not found&quot;));
    Thread.sleep(30000);
    viewCount.incrementCount();
  }

  @Transactional
  public void incrementWithoutSleep(Long id) {
    ViewCount viewCount = viewCountRepository.findById(id)
      .orElseThrow(() -&amp;gt; new RuntimeException(&quot;ViewCount not found&quot;));
    viewCount.incrementCount();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 A 클라이언트의 요청으로 incrementWithSleep 메소드가 실행됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션이 시작된 후 데이터를 읽어옵니다. 초기 조회수는 0입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 30초동안 Sleep 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Sleep하는 동안 B 클라이언트의 요청으로 같은 레코드의 데이터를 미리 0에서 1로 증감시킵니다다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 30초가 지나고나면 A 클라이언트의 요청은 이미 업데이트 되었다고 판단하기 때문에 롤백됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국에 2번을 실행했지만 count는 1이 저장됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동시에 클라이언트 2명이 요청을 했다면 한명의 트랜잭션은 롤백되는 현상이 생기겠죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;갱신 분실 문제라는 것은 데이터를 덮어 씌우는 것을 말하는데, 롤백되니까 갱신 분실 문제가 해결됐습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 덮어 씌우지 않는 문제는 해결됐지만 개발자가 원한건 2명의 요청이니까 count가 2가되어야겠죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해서 해결 방법은 아래와 같습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 방법 1. 비관적 락(Pessimistic Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 문제는 가장 간단하게 비관적 락을 사용해서 해결할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730361565387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 락을 설정
  @Query(&quot;SELECT v FROM ViewCount v WHERE v.id = ?1&quot;)
  ViewCount findWithLockById(Long id);
  
  @Transactional 
  public void incrementWithSleepAndLock(Long id) throws InterruptedException {
    ViewCount viewCount = viewCountRepository.findWithLockById(id); // 비관적 락으로 데이터 조회
    Thread.sleep(30000);
    viewCount.incrementCount(); // 조회수 증가
  }
  @Transactional
  public void incrementWithoutSleepAndLock(Long id) {
    ViewCount viewCount = viewCountRepository.findWithLockById(id); // 비관적 락으로 데이터 조회
    viewCount.incrementCount(); // 조회수 증가
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 똑같은 순서로 실행했을 때 하나의 트랜잭션이 findWithLockById를 사용하고 있다면 락이 걸리므로 다른 트랜잭션은 락이 끝날 때까지 대기합니다. 그러면 올바른 조회수 로직이 완성됩니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 방법 2. Redis&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1730706558117&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void incrementViewCountByRedis2(Long id) {
  String key = VIEW_COUNT_KEY_PREFIX + id;
  redisTemplate.opsForValue().increment(key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 위의 방법보다 무겁지만 또 다른 해결책 중 하나입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 위와 같이 간단한 INCR 연산은 원자적(Atomic) 연산으로 처리하기 때문에 기존 RDBMS의 ACID를 보장하는 트랜잭션 방식과 달리 데이터를 올바르게 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 Redis를 선택해야하는 이유는 하나 더 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;과부하인데요.&lt;/p&gt;
&lt;pre id=&quot;code_1730706761846&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB에서 위의 명령어를 수행하면 timeout 시간을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 비관적 락을 유지하더라도 락을 기다리는 시간이 timeout 시간을 초과하면 롤백 처리 시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 때는 Redis 인메모리 DB를 사용해서 빠르게 처리하면 좀 더 많은 사용자의 부하 처리가 가능하겠습니다.&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;또한 timeout 시간이 아니더라도 Spring의 기본 Thread 갯수(200개)가 모두 대기 중이라면 전체 부하가 걸리는 현상도 발생할 수 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;갱신 분실 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 제가 설명한 것중에 갱신 분실 문제가 해결된다고 했는데, 다시 등장한게 좀 의아할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 설명할 갱신 분실 문제는 프론트엔드의 갱신 분실 문제입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약에 두명의 사용자가 하나의 게시글을 동시에 수정하면 어떻게 될까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 경우에 한명이 미리 저장하고 그 다음 사용자가 다시 저장하려고 하면 덮어씌워지는 갱신 분실 문제 현상이 나타납니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 방법 1. 낙관적 락(Optimistic Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드에 Version 값을 추가하고 프론트엔드에 전달해서 해당 버전과 일치하지 않으면 예외 처리하고 사용자에게 이미 갱신된 게시글이 있다는 알림을 해주는 것으로 해결할 수 있습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;The last resort is to manually set an exclusive lock on all the necessary rows (SELECT FOR UPDATE) or even on the entire table (LOCK TABLE). This always works, but nullifies the benefits of multiversion concurrency&lt;br /&gt;&lt;br /&gt;개발자가 수동으로 모든 행(SELECT FOR UPDATE) 또는 전체 테이블(LOCK TABLE)에 대해 수동으로 단독 잠금을 설정하는 경우 작동은 하지만 다중 버전 동시성(MVCC)의 이점을 무효화합니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아직 미작성 한 부분&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;cyclicBarrier를 이용한 분산락, 스핀락&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/517</guid>
      <comments>https://stir.tistory.com/517#entry517comment</comments>
      <pubDate>Fri, 25 Oct 2024 16:47:03 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 동시성(Concurrency) 이슈 - 변수(1)</title>
      <link>https://stir.tistory.com/251</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;1172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6pIDl/btrNChzk2At/VgGsNykJoiTi3wZKRhYBC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6pIDl/btrNChzk2At/VgGsNykJoiTi3wZKRhYBC0/img.png&quot; data-alt=&quot;Concurrency에 관한 Dining Philosophers Problem&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6pIDl/btrNChzk2At/VgGsNykJoiTi3wZKRhYBC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6pIDl%2FbtrNChzk2At%2FVgGsNykJoiTi3wZKRhYBC0%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;450&quot; height=&quot;467&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;1172&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Concurrency에 관한 Dining Philosophers Problem&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동시성 이슈란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 이슈는 한 가지라고 단정할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 이슈의 종류의 예는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 스레드가 DB를 동시에 수정할 때 생기는 문제&lt;/li&gt;
&lt;li&gt;여러 개의 스레드가 하나의 변수를 공유해서 생기는 문제(공유하지 않도록 설계하는 방법)&lt;/li&gt;
&lt;li&gt;하나의 변수를 공유하도록 설계했지만 데이터의 정합성이 깨지는 문제&lt;/li&gt;
&lt;li&gt;갱신 분실 문제&lt;/li&gt;
&lt;li&gt;선착순, 조회수, 재고 관리 등 한번에 너무 많은 요청이 예상될 때 DB의 성능으로 인해 지연되다가 트랜잭션이 실패하는 문제&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 동시성 문제로 간주할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 동시성 이슈에 대해 단계적으로 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;변수에 대한 동시성 이슈 &lt;/b&gt;&lt;b&gt;발생 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수에서 동시성 이슈가 발견되는 것은 변수가 스레드마다 공유되는 상황에서 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 다음과 같은 경우에 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티스레드 환경에서의 공유 변수 상태 변경&lt;/li&gt;
&lt;li&gt;싱글톤 객체에서의 공유 변수 상태 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1729745827740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SharedCounter {
    static int count = 0; // 공유 변수
    
    public void increment() {
        count++; // 동시성 이슈 발생 가능
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 static 변수나 싱글톤 객체에서 생성된 전역 변수는 공유 상태에 놓이기 때문에 동시성 이슈가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 변수를 여러 스레드가 한번에 증감을 하려고 하면 증감의 결과가 올바르게 처리되지 않기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;올바르게 처리되지 않는 자세한 이유는 아래 Volatile 부분에서 소개하겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 문제는 공유 변수를 위와 같이 변수에 담아 관리하는 것이 아니라 ACID가 보장되는 Database에 삽입해서 관리하면 대부분의 문제는 발생하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 가끔 설계상 굳이 DB가&lt;/span&gt; 필요없다고 생각할 때 위처럼 변수로 백엔드 소스 내에서 관리하는 경우도 드물게 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 그런 측면이 아니더라도 동시성에 대한 지식도 얻을 겸 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수에 대한 동시성을 제어하기 위한 3가지 방법으로는 &lt;b&gt;Synchronized, Volatile, Concurrent.Atomic 패키지 클래스 사용&amp;nbsp;&lt;/b&gt;방식이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Synchronized 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729747183738&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int count;
public synchronized void increment() {
    count++;
}&lt;/code&gt;&lt;/pre&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;synchronized가 사용된 메서드는 하나의 스레드가 사용하고 있으면 다른 스레드는 사용하지 못합니다.&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;b&gt;Volatile 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729748930255&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Test {
	volatile int a = 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Volatile 변수는 가시성을 지켜주는 변수입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;컴퓨터 용어로 쓰이는 메모리 가시성(Memory Visibility)은 &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;한 스레드에서 변수의 값이 변경되면 다른 스레드가 그 변경된 값을 즉시 인식할 수 있는지를 의미합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;위에서 설명한 static 변수는 하나의 스레드에서 변수의 값을 변경하면 다른 스레드가 알지 못하는 문제가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨 말인지 자세하게 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예를 들어 A, B 사용자가 같은 데이터를 수정하고 있다고 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이때, A 사용자가 먼저 업데이트를 하더라도 B 사용자는 A 사용자로 인해 업데이트된 것을 알지 못합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉, 가시성이 없다는 것 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729827332094&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean running = true;

  public static void main(String[] args) {
    new VolatileTest().test();
  }

  public void test() {
    new Thread(() -&amp;gt; {
      int count = 0;
      while (running) { // CPU Cache에서 꺼내므로 아래의 Thread가 값을 바꾸더라도 무한 반복에 빠진다.
        count++;
      }
      System.out.println(&quot;Thread 1 finished. Counted up to &quot; + count);
    }
    ).start();

    new Thread(() -&amp;gt; {
      try {
        Thread.sleep(100);
      } catch (InterruptedException ignored) {
      }
      System.out.println(&quot;Thread 2 finishing&quot;);
      running = false;
    }
    ).start();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;좋은 코드 예제가 있어서 한번 확인해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 두 개의 스레드가 동시에 작동합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 첫 번째 스레드는 while(true)로 인해 무한 루프가 작동합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 두 번째 스레드는 running의 값을 false로 바꿔서 무한 루프 작동을 멈추려고 시도합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 하지만 첫 번째 스레드는 두 번째 스레드에서 변화된 값을 인지하지 못합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/01UFM/btsKjlM1x9A/KUPhWHRo0KET7jMkfV7ocK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/01UFM/btsKjlM1x9A/KUPhWHRo0KET7jMkfV7ocK/img.png&quot; data-alt=&quot;https://takeaction.github.io/Java-Memory-Model/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/01UFM/btsKjlM1x9A/KUPhWHRo0KET7jMkfV7ocK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F01UFM%2FbtsKjlM1x9A%2FKUPhWHRo0KET7jMkfV7ocK%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;507&quot; height=&quot;442&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://takeaction.github.io/Java-Memory-Model/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에 그림은 Java가 변수를 어떻게 처리하는지에 대한 메모리 모델입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java는 기본적으로 어떠한 요청이 들어오면&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메인 메모리(RAM)에서 실제 값인 데이터를 꺼내 Heap 혹은 Stack 자료 구조에 저장한 후에 CPU에서 캐시 해서 사용합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CPU에서 캐시 되는 문제로 인해 위의 예제 코드에서는 캐시 된 데이터를 들고 오는 현상이 발생됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;그래서 변화된 값을 인지하지 못하는 것이죠.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;하지만 volatile 변수는 CPU에서 캐시값을 사용하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;volatile은 한글로 &quot;휘발성&quot;이라는 뜻을 가집니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;volatile이라는 의미는 CPU를 거치지 않고 휘발성 메모리(RAM)를 이용해 변수를 처리하겠다는 의미입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드에서 running 변수를 volatile로 변경하면 캐시를 이용하지 않고 바로 메모리에 있는 데이터를 접근하기 때문에 첫번째 스레드는 무한루프가 종료됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 모든 변수를 volatile로 만들면 되지 않을까요? 그렇지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;변수는&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt; JVM이 최적화를 해서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CPU에서 캐쉬를 해야 접근도 빠르기&amp;nbsp;때문입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;volatile로 만든 변수는 JVM이 최적화 처리하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;volatile은 그럼 동시성 문제를 해결해 주는 키워드일까요? 그렇지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;volatile은 가시성을 지켜주는 것이지만 원자성을 지켜주지는 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이제 원자성이 지켜지지 않는 예제를 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729829772225&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private volatile int count = 0; // volatile로 선언

  public static void main(String[] args) {
    new VolatileTest2().test();
  }

  public void test() {
    Thread thread1 = new Thread(() -&amp;gt; {
      for (int i = 0; i &amp;lt; 10000; i++) {
        count++; // 원자성이 보장되지 않음
      }
      System.out.println(&quot;Thread 1 finished.&quot;);
    });

    Thread thread2 = new Thread(() -&amp;gt; {
      for (int i = 0; i &amp;lt; 10000; i++) {
        count++; // 원자성이 보장되지 않음
      }
      System.out.println(&quot;Thread 2 finished.&quot;);
    });

    thread1.start();
    thread2.start();

    try {
      thread1.join();
      thread2.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    System.out.println(&quot;Final count: &quot; + count);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 변수가 volatile로 설정되어 있고 각 스레드에서 count를 10000을 더 하는 로직입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 데이터는 메모리에 있는 데이터로 접근하지만(가시성) count는 20000이 보장되지 않을 때가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;173&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKkGTv/btsKjGbflpm/oe6IflAAq3hqnLkJWKpHQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKkGTv/btsKjGbflpm/oe6IflAAq3hqnLkJWKpHQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKkGTv/btsKjGbflpm/oe6IflAAq3hqnLkJWKpHQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKkGTv%2FbtsKjGbflpm%2Foe6IflAAq3hqnLkJWKpHQ1%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;173&quot; height=&quot;76&quot; data-origin-width=&quot;173&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원자성은 특정 연산이 중간에 끊기지 않고 완전히 수행된다는 것을 의미합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 스레드 A가 count 값을 읽고 5를 가져왔다고 가정합시다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 사이에 스레드 B가 count 값을 6으로 증가시키면, 스레드 A가 count에 6을 쓴다고 해도 결과적으로 count는 6이 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 스레드 A의 작업은 B의 작업으로 인해 무효화될 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러니 A의 작업은 연산이 수행되지 않았다고 보기 때문이 원자성이 없는 것 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 위에서 설명한 synchronized는 원자성과 가시성을 보장합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 속도가 느린 것뿐입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 원자성과 가시성이 전부 필요한 경우에 어떤 방식이 권장되는지 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Atomic 클래스(권장)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729749851929&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet(); // 원자적 연산
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atomic 클래스에서는 CAS(Compare-And-Swap) 알고리즘을 사용해서 원자성을 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 위의 예제에서 20000이라는 값은 보장될 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;CAS(Compare-And-Swap)&amp;nbsp;알고리즘의&amp;nbsp;&lt;/b&gt;특징&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CAS 알고리즘은 Current Value와 Expected Value를 사용합니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CAS 연산이 시작되기 전에 연산의 대상이 되는 값을 Expected Value에 저장합니다.(예를 들어 5)&lt;/li&gt;
&lt;li&gt;마지막에 데이터를 쓰기(저장) 직전에 메인 메모리에서 데이터를 읽어와서 Current Value에 담습니다.(예를 들어 6)&lt;/li&gt;
&lt;li&gt;두 개의 값을 비교해서 값이 같지 않은 경우에는 어디선가 한번 업데이트가 이루어졌다고 판단할 수 있기 때문에 이 경우 CAS 연산은 실패하고 아무런 변경도 이루어지지 않습니다.&lt;/li&gt;
&lt;li&gt;실패한 스레드는 바쁜 대기(Busy Waiting) 상태가 되어 성공적인 수행이 가능할 때까지 무한 반복합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;원자성에 대한 상상&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;조금 말이 복잡한데요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CAS 연산은 말 그대로 &quot;비교와 쓰기 작업을 한 번에 수행&quot;하는 특징이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 말이 무슨 말인지 반대 사례를 통해 원자성이 무너지는 상상을 해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;만약 Expected Value가 5고 Current Value도 5라고 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그럼 둘의 값은 같으니 업데이트를 할 수 있게 됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 같음을 비교하고 바로 어디선가 실제 메모리에 담긴 데이터를 5를 6으로 증감시키는 상상도 충분히 할 수 있는데요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;런 상황에서 5라는 데이터를 6으로 업데이트하려고 하면 이전처럼 당연히 원자성이 깨지게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 Compare-And-Swap이라는 말은 이 2개의 작업을 단 하나의 동작으로 묶어서 동작한다는 의미입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;쉽게 말해 &quot;같으면 업데이트를 한다&quot;는 2가지 동작이지만 &quot;같으면&quot;까지 검사를 했는데 그 사이에 다른 스레드가 6으로 업데이트하는 경우가 발생하지 않는다는 의미입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;근데 두 개의 동작을 하나의 동작으로 불가분의 관계로 연산을 하고 다른 작업이 방해하지 않도록 한다는 의미는 마치 '락'을 의미하는 것 같지 않나요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기 오해를 할 수 있는 부분은 여기서는 '락'이라는 표현을 쓰지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;락은 소프트웨어 개념에서 설명되는 것이고 CAS는 하드웨어 레벨에서 이루어지는 연산이기 때문에 락이라는 표현을 사용하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CAS 명령어는&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;CPU 차원에서 제공되는 기능입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;CAS 연산을 수행할 때 위처럼 Compare-And-Swap의 과정을 진행하는 동안 방해를 받지 않는 원자적 특성이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;멀티코어 CPU 환경에서는&amp;nbsp;&lt;b&gt;MESI(Modified, Exclusive, Shared, Invalid)&lt;/b&gt;&amp;nbsp;같은 캐시 일관성 프로토콜을 통해, 하나의 CPU 코어가 특정 메모리 주소에 대한 CAS 작업을 진행 중일 때 다른 코어들이 이 메모리에 접근하지 못하도록 제한합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;또&amp;nbsp;&lt;b&gt;메모리 장벽(Memory Barrier)&lt;/b&gt;을 이용해&amp;nbsp;CAS 연산이 중단 없이 완전히 끝날 때까지 다른 메모리 작업이 간섭하지 않도록 순서를 강제합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 특징들로 인해 '락'처럼 보이는 연산이 가능해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;CPU가 모든 계산을 원자성을 보장하도록 연산하지는 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Java에서 일반적인 변수도 원래 CPU의 ALU가 처리하지만 이건 원자성이 보장되지 않죠. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이건 CPU가 가진 명령어인 CAS만이 가진 장점이라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;CAS도 단점은 존재합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;바쁜 대기 현상(실패 발생 시)에서는 무한루프를 돌기 때문에 자원 소모량이 크다는 단점도 있고 ABA 문제라고 해서 값이 A에서 B로 변경된 후 다시 A로 돌아오면 CAS는 성공으로 판단하는 현상으로 인해 예상하지 못한 상태가 발생할 수 있습니다. &lt;br /&gt;이 문제를 해결하기 위해 AtomicStampedReference 클래스를 사용하여 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&quot;버전 번호&quot;와 함께 사용하기도 합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;ConcurrentHashMap(권장)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약에 변수가 HashMap으로 되어있다면 ConcurrentHashMap 클래스를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ConcurrentHashMap은 자바 8 이전에는 락 스트라이핑 방식인 세그먼트 락을 이용했다면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;지금은 CAS를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Volatile은 왜 존재할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atomic과 같은 좋은 기능이 있는데 왜 Volatile이 존재할까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;volatile 키워드는 메모리&lt;span&gt;&amp;nbsp;&lt;/span&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;예를 들어, 플래그(예: boolean running = true)와 같이 단순한 상태 변경이 필요한 경우에 volatile을 사용하면 충분합니다.&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;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Synchronized는 왜 존재할까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 통해 블록 내부의 모든 연산을 원자적으로 수행하고, 블록을 빠져나올 때 다른 스레드가 변경 사항을 볼 수 있도록 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 synchronized는 Atomic 클래스와 달리 여러 연산이 묶인 복잡한 연산에서도 원자성을 보장하는 데 유용합니다.&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/251</guid>
      <comments>https://stir.tistory.com/251#entry251comment</comments>
      <pubDate>Fri, 25 Oct 2024 16:45:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Web Push API(Push Notification) 구현 방법</title>
      <link>https://stir.tistory.com/516</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모든 소스 코드는 &lt;a href=&quot;https://github.com/stir084/High-Traffic-Control&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Web Push API란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 클라이언트로 비연결 기반으로 단방향 통신을 가능하게 하는 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비연결이라는 것은 WebSocket, SSE처럼 연결한 상태로 사용하는 기술이 아니라는 뜻 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 개념을 설명하자면 클라이언트는 VAPID Key를 이용해 서버에 구독 정보를 저장하고 서버는 후에 구독한 클라이언트에게 메시지를 전송하는 과정으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VAPID의 뜻을 풀어보면 &quot;자발적 애플리케이션 서버 식별(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Voluntary Application Server Identification)&lt;/span&gt;&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버가 브라우저에게 자발적으로 자신을 인증하는 방법을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xVjPu/btsKdLYO64x/RuZz17QX2tjjvMgkYgtBEk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xVjPu/btsKdLYO64x/RuZz17QX2tjjvMgkYgtBEk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xVjPu/btsKdLYO64x/RuZz17QX2tjjvMgkYgtBEk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxVjPu%2FbtsKdLYO64x%2FRuZz17QX2tjjvMgkYgtBEk%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;378&quot; height=&quot;567&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web Push API는 푸쉬 알림(Push Notification)의 일종이고 가장 많이 사용하는 경우는 앱에 Push을 보내는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이런 경우는 FCM(Firebase Cloud Messaging)을 이용해서 알림을 구현하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 소개하고자 하는 Web Push API는 말 그대로 Web에서만 작동하는 기능이며 특정 최신 브라우저에만 종속된다는 특징이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 특정 요구 사항에 따라 Web Push API와 FCM 중 선택할 수 있으며, 모바일 앱과 웹 모두를 지원해야 한다면 FCM을 사용하는 것이 일반적입니다.&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;Push Notification은 앱에 Push를 하는 기능 뿐만 아니라 실시간 데이터가 필요하지 않은 결제 시스템(결제 서버에 요청을 하고 나중에 결제가 완료되면 알림을 보내는 Paypal과 같은 시스템) 등에서도 많이 쓰이는 기능입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비연결 지향이기 때문에 보통의 Spring은 클라이언트가 서버에 요청하면 데이터에 대한 Return을 항상 기대하지만 이 경우엔 다른 서버가 Return할 수 있는 구조의 설계도 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;준비&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 환경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;푸쉬 알림&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;테스트는 https 서버에서만 가능하고 예외적으로 localhost에서도 테스트가 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;VAPID Key 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729496120930&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo npx web-push generate-vapid-keys&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1729496127448&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Public Key:
BCz1BNLNccwKDd9XwGJUnNNcKoluFigzD_5xRlehWtGinDRoESwgR63bHrhHvEcZydUj4qPWDk7YcDhmvisNmrM

Private Key:
3p2xHXA51O4LJVW7Og3svbiJDGEg5orhfM9vX8LYUX0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 vapid 표준에 맞는 key를 생성해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 생성하기 귀찮다면 테스트 용도로 위의 key를 사용해도 무방합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Boot에 의존성 추가&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729496182387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Web Push API
implementation group: 'nl.martijndwars', name: 'web-push', version: '5.1.1' 

// Web Push API가 사용하는 VAPID Key를 암호화 라이브러리
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.70'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;구현 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프론트엔드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 임의로 Thymeleaf를 사용해서 브라우저에서 service-worker.js, index.html을 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729500753143&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;self.addEventListener('push', function(event) { // 푸시 알림이 도착할 때 발생하는 push 이벤트를 리스닝하고, 해당 알림을 처리합니다.
    const data = event.data.json();

    const options = {
        body: data.body,
        icon: 'icon.png'
    };

    event.waitUntil( // 푸시 알림을 정상적으로 브라우저에 표시할 때까지 작업이 중단되지 않도록 보장하는 역할을 합니다.
        self.registration.showNotification(data.title, options) // Service Worker의 registration 객체에서 showNotification() 메서드를 호출하여 푸시 알림을 실제로 표시합니다.
    );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729500762880&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Push Notification Test&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;Push Notification Test&amp;lt;/h1&amp;gt;

&amp;lt;script&amp;gt;
    /**
     * VAPID 공개키 설정
     */
    const publicVapidKey = 'BCz1BNLNccwKDd9XwGJUnNNcKoluFigzD_5xRlehWtGinDRoESwgR63bHrhHvEcZydUj4qPWDk7YcDhmvisNmrM';

    /**
     * urlBase64ToUint8Array: 이 함수는 Base64 URL-safe 문자열을 Uint8Array로 변환하는 함수입니다.
     * 브라우저의 Push API는 applicationServerKey를 Uint8Array 형식으로 받아야 하기 때문에, 공개 키(Base64)를 이 형식으로 변환하는 역할을 합니다.
     */
    function urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
            .replace(/\-/g, '+')
            .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i &amp;lt; rawData.length; ++i) {
            outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
    }



    if ('serviceWorker' in navigator) { // 현재 브라우저가 Service Worker 기능을 지원하는지 확인하는 조건문(Service Worker는 백그라운드에서 푸시 알림을 처리하는 데 사용되며, 푸시 알림을 구현하려면 필수입니다.)
        Notification.requestPermission().then(permission =&amp;gt; { // 브라우저가 사용자에게 알림 권한을 요청합니다. 사용자가 권한을 허용하거나 거부할 수 있습니다.
            if (permission === 'granted') {
                navigator.serviceWorker.register('/service-worker.js') // 실제로 푸쉬 알림을 처리하는 service-worker.js파일을 등록합니다.
                    .then(function(registration) {
                        console.log('Service Worker 등록 성공:', registration);

                        return navigator.serviceWorker.ready; // Service Worker가 준비될 때까지 기다립니다.
                    })
                    .then(function(registration) { // service-worker.js파일이 성공적으로 등록되었을 때 반환하는 Service Worker 등록 정보
                        return registration.pushManager.subscribe({ // 아래의 옵션으로 Service Worker 구독
                            userVisibleOnly: true, // 이 옵션은 사용자에게 보이지 않는 백그라운드 알림이 아닌, 반드시 사용자가 볼 수 있는 알림만 받도록 설정합니다.
                            applicationServerKey: urlBase64ToUint8Array(publicVapidKey) // publicVapidKey를 변환한 값으로, VAPID 공개 키를 사용하여 서버가 인증된 푸시 알림을 보낼 수 있도록 합니다.
                        });
                    })
                    .then(function(subscription) {
                        console.log('푸시 알림 구독 성공:', subscription);

                        fetch('/subscribe', {
                            method: 'POST',
                            body: JSON.stringify(subscription), // 구독된 정보를 서버에 전송
                            headers: {
                                'Content-Type': 'application/json'
                            }
                        }).then(function(response) {
                            console.log('서버 응답:', response);
                        });
                    })
                    .catch(function(error) {
                        console.error('푸시 알림 구독 실패:', error);
                    });
            } else {
                console.error('알림 권한이 거부되었습니다.');
            }
        });
    }
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클라이언트(브라우저)가 서버에 &lt;b&gt;구독 정보를 생성할 때&lt;/b&gt; 브라우저는 해당 클라이언트에 고유한 &lt;b&gt;endpoint, keys&lt;/b&gt; (예: p256dh, auth)를 포함한 구독 정보를 자동으로 생성합니다. 그러므로 여러 사용자의 알림 시스템도 구현 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;백엔드&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Producer Server&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 구독 정보를 발행하는 서버라고 볼 수 있습니다.&lt;br /&gt;해당 구독 정보는 DB에 저장되는 것이 옳지만 지금은 임의로 Consumer 서버에 전역 변수로 할당하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729500814418&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @GetMapping(&quot;/index&quot;)
  public String home(Model model) {
    return &quot;index&quot;;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 백엔드 주소를 통해 index.html 페이지로 이동하도록 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동하자마자 위의 프론트엔드 로직이 실행되는데 올바르게 작동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 브라우저에서 알림 허용을 해줘야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창 좌측에 아이콘을 눌러서 아래와 같이 알림 허용을 해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mfdNc/btsKdoJt7vg/3ZP0TKmZJqs0FjZMZZvUI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mfdNc/btsKdoJt7vg/3ZP0TKmZJqs0FjZMZZvUI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mfdNc/btsKdoJt7vg/3ZP0TKmZJqs0FjZMZZvUI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmfdNc%2FbtsKdoJt7vg%2F3ZP0TKmZJqs0FjZMZZvUI1%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;308&quot; height=&quot;368&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 이유로 보통 웹에서는 자주 쓰이는 기능은 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이유는 사용자가 직접 알림을 허용해줘야하기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 크롬과 같은 브라우저는 사용자가 여러 사이트에서 반복적으로 알림을 차단해왔다면 자동으로 차단하는 기능도 내재하고 있기 때문에 굉장히 기능이 한정적이라는 것을 알 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PostMapping(&quot;/subscribe&quot;)
public ResponseEntity&amp;lt;String&amp;gt; subscribe(@RequestBody Subscription subscription) {

  webPushService.sendToConsumerServer(subscription);

  return ResponseEntity.ok(&quot;Subscription saved.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 코드에서 /subscribe를 통해 서버에 구독 정보를 저장할 수 있도록 API를 구현해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드에서 localhost:9090으로 요청한 정보는 다시 localhost:9091로 전달하여 9091 서버에서 구독 정보를 저장하도록 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Consumer Server&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1729501194764&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PushNotificationService {
  private final PushService pushService;

  static {
    // BouncyCastle 암호화 방법 등록
    if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
      Security.addProvider(new BouncyCastleProvider());
    }
  }
  public PushNotificationService() throws Exception {
    pushService = new PushService();
    // VAPID 키 등록
    pushService.setPublicKey(&quot;BCz1BNLNccwKDd9XwGJUnNNcKoluFigzD_5xRlehWtGinDRoESwgR63bHrhHvEcZydUj4qPWDk7YcDhmvisNmrM&quot;);
    pushService.setPrivateKey(&quot;3p2xHXA51O4LJVW7Og3svbiJDGEg5orhfM9vX8LYUX0&quot;);
  }

  public void sendPushNotification(Subscription subscription, String title, String body) throws Exception {
    String payload = &quot;{\&quot;title\&quot;: \&quot;&quot; + title + &quot;\&quot;, \&quot;body\&quot;: \&quot;&quot; + body + &quot;\&quot;}&quot;;

    Notification notification = new Notification(subscription, payload);
    HttpResponse response = pushService.send(notification);

    System.out.println(&quot;Push notification sent with status: &quot; + response.getStatusLine());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscription(구독 정보) 객체를 꺼내서 클라이언트에 역으로 알림을 보내는 구현입니다.&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/516</guid>
      <comments>https://stir.tistory.com/516#entry516comment</comments>
      <pubDate>Mon, 21 Oct 2024 18:00:46 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 영속성 프레임워크(JPA, R2DBC)에서 save 2번 진행 시 '생성 시간' 처리 방법</title>
      <link>https://stir.tistory.com/514</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 영속성 프레임워크(JPA, R2DBC)에서 save를 두 번 호출할 때 발생할 수 있는 '생성 시간' 처리 이슈와 그 해결 방법을 소개하고자 합니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;생성 시간 처리의 일반적인 방법들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드에서 직접 생성 시간 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 코드를 통해 직접 생성 시간을 설정하는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729145607095&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;entity.setCreateTime(LocalDateTime.now());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티의 createTime 필드에 LocalDateTime.now()와 같은 메서드를 호출하여 값을 할당하는 방식입니다.&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;b&gt;JPA의 @PrePersist 또는 @PreUpdate 사용&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729145336796&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class MyEntity {
    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updateTime = LocalDateTime.now();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 엔티티 라이프사이클 콜백 메서드를 사용하여 @PrePersist나 @PreUpdate를 적용하면, 엔티티가 저장되기 전에 자동으로 생성 시간과 수정 시간을 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 JPA에서 자주 사용하는 방법이며, R2DBC에서도 유사한 방식으로 적용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;데이터베이스에서 자동으로 생성 시간 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티를 데이터베이스에 삽입할 때, 테이블 스키마에서 createTime을 자동으로 생성되도록 설정하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, MySQL의 경우 테이블을 생성할 때 아래와 같이 createTime을 자동으로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729145657240&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE my_table (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 매우 깔끔하고 별도의 코드 수정 없이 생성 시간과 수정 시간을 자동으로 관리할 수 있어 자주 사용됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 발생 상황(save 2번)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 방식(데이터베이스에서 시간 자동 생성)도 깔끔하지만 영속성 프레임워크를 사용할 때는 문제가 발생할 여지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 하나의 비즈니스 로직에서 동일한 엔티티를 두 번 save하는 상황을 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 경우는 아니지만 save를 하고나서 추가적인 정보를 담고 다시 save를 할 수도 있는 것이죠.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번: &lt;b&gt;첫 번째 save 호출&lt;/b&gt; &amp;ndash; 영속성 컨텍스트 1차 캐시에 저장되지만 아직 DB에는 반영되지 않음.&lt;/li&gt;
&lt;li&gt;2번: &lt;b&gt;두 번째 save 호출&lt;/b&gt; &amp;ndash; 1차 캐시 값에 새로운 값을 넣어서 저장하려고 하지만, createTime 값이 없음.&lt;/li&gt;
&lt;li&gt;3번: &lt;b&gt;트랜잭션 커밋&lt;/b&gt; &amp;ndash; 두 번의 save가 끝나고 커밋 시 두개의 쿼리가 실행되면서 첫번째 쿼리에서는 createTime이 생성되지만 즉시 두번째 쿼리에서 createTime이 null로 반영됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 첫 번째 또는 두 번째 방법으로 되돌아갈 수 있습니다. 첫 번째 방법(코드에서 직접 시간 설정)은 이상적이지 않지만, 두 번째 방법(PrePersist나 PreUpdate 사용)은 좋은 대안입니다.&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;JPA를 사용하는 경우 flush()를 사용해 데이터를 먼저 삽입한 후 비즈니스 로직을 진행할 수 있습니다. 그러나 이 방법은 덜 우아한 코드를 초래할 수 있습니다. 비즈니스 로직 내에서 영속성 관리나 트랜잭션 관리를 혼합하면 코드가 복잡해지고 유지 관리가 어려워질 수 있습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거를 사용한 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;새로운 테이블과 트리거를 활용한 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저와 같은 경우에는 상태값에 따라 시간을 기록하고 싶었기 때문에 아래와 같이 구현해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 시간을 담는 로그 테이블을 생성한 후, 데이터베이스 트리거를 사용하여 상태 변경 시 시간을 기록하는 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729145435966&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TRIGGER status_change_trigger
    AFTER UPDATE ON your_table
    FOR EACH ROW
BEGIN
    IF NEW.status &amp;lt;&amp;gt; OLD.status THEN
        INSERT INTO status_log_table (record_id, status, update_time)
        VALUES (NEW.id, NEW.status, NOW());
    END IF;
END;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 트리거는 기존 테이블(your_table)에 상태 변경이 발생할 때마다 로그 테이블(status_log_table)에 상태와 시간을 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 save가 여러 번 발생하더라도 트리거에 의해 시간이 정확하게 관리되며, 확장성과 일관성을 유지할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기존 테이블과 트리거를 활용한 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 테이블을 만들지 않아도 사용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 시간 컬럼을 NOT NULL로 설정하고 아래와 같이 트리거를 생성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729145449460&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TRIGGER prevent_null_update
BEFORE UPDATE ON your_table
FOR EACH ROW
BEGIN
    IF NEW.create_time IS NULL THEN
        SET NEW.create_time = OLD.create_time;
    END IF;
END;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;업데이트 시 createTime 필드가 null이 삽입돼서 변경을 하려는 경우 기존 값을 유지하는 트리거를 만들 수 있습니다.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 save가 두 번 호출되더라도 createTime 필드가 유지되도록 보장할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 프레임워크에서 save를 두 번 호출하는 상황에서 발생하는 생성 시간 처리 문제는 여러 가지 방법으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 방식은 상황에 맞게 선택할 수 있으며, 특히 트리거를 사용한 방식은 확장성과 일관성을 유지하면서 문제를 해결할 수 있는 유용한 방법입니다.&lt;/p&gt;
&lt;figure id=&quot;og_1729147310130&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Handling &amp;lsquo;Creation Time&amp;rsquo; in Persistence Frameworks (JPA, R2DBC) When Saving Twice&quot; data-og-description=&quot;In this article, we&amp;rsquo;ll discuss the issue of handling &amp;lsquo;creation time&amp;rsquo; in persistence frameworks (JPA, R2DBC) when the save method is called&amp;hellip;&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@wnsgh1357/handling-creation-time-in-persistence-frameworks-jpa-r2dbc-when-saving-twice-4d72dfb648ad&quot; data-og-url=&quot;https://medium.com/@wnsgh1357/handling-creation-time-in-persistence-frameworks-jpa-r2dbc-when-saving-twice-4d72dfb648ad&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/weXKe/hyXhW7piby/1ZLaL4qyMnHCWBWEyZNrBk/img.png?width=1358&amp;amp;height=764&amp;amp;face=0_0_1358_764,https://scrap.kakaocdn.net/dn/VJdB8/hyXhTQlWwC/Rbe24XA7tYK318evA0DaG1/img.jpg?width=800&amp;amp;height=539&amp;amp;face=0_0_800_539&quot;&gt;&lt;a href=&quot;https://medium.com/@wnsgh1357/handling-creation-time-in-persistence-frameworks-jpa-r2dbc-when-saving-twice-4d72dfb648ad&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@wnsgh1357/handling-creation-time-in-persistence-frameworks-jpa-r2dbc-when-saving-twice-4d72dfb648ad&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/weXKe/hyXhW7piby/1ZLaL4qyMnHCWBWEyZNrBk/img.png?width=1358&amp;amp;height=764&amp;amp;face=0_0_1358_764,https://scrap.kakaocdn.net/dn/VJdB8/hyXhTQlWwC/Rbe24XA7tYK318evA0DaG1/img.jpg?width=800&amp;amp;height=539&amp;amp;face=0_0_800_539');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Handling &amp;lsquo;Creation Time&amp;rsquo; in Persistence Frameworks (JPA, R2DBC) When Saving Twice&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In this article, we&amp;rsquo;ll discuss the issue of handling &amp;lsquo;creation time&amp;rsquo; in persistence frameworks (JPA, R2DBC) when the save method is called&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/514</guid>
      <comments>https://stir.tistory.com/514#entry514comment</comments>
      <pubDate>Thu, 17 Oct 2024 15:24:09 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Json을 RDBMS(MySQL, PostgreSQL)에 저장해도 좋을까?</title>
      <link>https://stir.tistory.com/502</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우선 키 밸류 형태의 값을 저장하기 위한 대표적인 저장소는 NoSQL입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 어떠한 데이터를 저장하느냐에 따라 MySQL을 이용해도 무방한 경우가 있는데 여러 사이트를 참고한 결과를 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 조건을 다 통과한다면 RDBMS에서 사용해도 무방합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RDBMS에 저장하기 위한 조건&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 자주 업데이트 할 경우 성능에 영향을 끼친다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를&amp;nbsp;들어, 간단한 구조의 단순한&amp;nbsp;설정&amp;nbsp;데이터나&amp;nbsp;로그&amp;nbsp;데이터를&amp;nbsp;저장하는&amp;nbsp;경우,&amp;nbsp;MySQL의&amp;nbsp;JSON&amp;nbsp;컬럼으로&amp;nbsp;충분할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 수정&lt;span&gt; &lt;/span&gt;과정이&lt;span&gt; &lt;/span&gt;일반&lt;span&gt; SQL&lt;/span&gt;보다&lt;span&gt; &lt;/span&gt;복잡하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;3. 몽고 DB에 비해 집계 등 JSON에 대한 다양한 쿼리를 사용하기 어렵다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 저장되는 Json이 크다면 속도 저하가 발생한다. (&lt;a href=&quot;https://stir.tistory.com/501&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고&lt;/a&gt;)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼추 수천글자를 저장하는 케이스가 빈번하게 생긴다면 그 자체로 성능 저하가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대적으로 같은 Json의 크기여도 MongoDB는 BSON 형태로 2진 코드로 삽입해서 용량이 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 저장하는 데이터 자체가 MongoDB보다 용량이 크다(BSON is a binary-encoded serialization format that is more compact than raw JSON)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt; 6. 기존 시스템이 RDBNS인 경우 고려할 수 있다. 오히려 데이터베이스를 하나 더 늘리는 것은 트랜잭션을 일관성있게 보장하는 것이 어렵고 마이그레이션 작업도 더 까다로워진다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이나 PostgreSQL나 다른 SQL 데이터베이스에서 적당한 크기의 JSON을 저장하고 검색하는 것에는 전혀 문제가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건에 하나라도 부합하지 않는다면 NoSQL을 검토해볼 수 있겠습니다.&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/502</guid>
      <comments>https://stir.tistory.com/502#entry502comment</comments>
      <pubDate>Wed, 2 Oct 2024 15:31:33 +0900</pubDate>
    </item>
    <item>
      <title>인텔리제이 빌드가 느려졌다면..</title>
      <link>https://stir.tistory.com/510</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용이 정답은 아닐 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Shift + Ctrl + F8 (Mac: Shift + Cmd + F8)을 누릅니다.&lt;/li&gt;
&lt;li&gt;또는, 메뉴에서 **Run -&amp;gt; View Breakpoints...**를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정된 브레이크 포인트들을 모두 해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅 모드로 시작 시에 가끔 특정 브레이크 포인트가 빌드 속도에 엄청난 영향을 끼치는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727678010798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  main:
    lazy-initialization: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정도 로컬 환경 한정에서 꽤 도움 됩니다. 운영은 비추.&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <category>인텔리제이빌드느려짐</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/510</guid>
      <comments>https://stir.tistory.com/510#entry510comment</comments>
      <pubDate>Mon, 30 Sep 2024 15:31:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] DTO는 어디서 변환할까? (feat. DDD)</title>
      <link>https://stir.tistory.com/507</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;MVC와 헥사고날&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;일반적으로 규모가 작은 MVC 개발 방식(도메인 중심이 아닌 경우)은 일반적으로 모든 서비스 레이어에서 DTO를 파라미터로 받고 return도 DTO로 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;헥사고날에서는 계층 간의 시스템적인 완벽한 분리를 추구하기 때문에 서비스 레이어에서는 일반적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;도메인 객체&lt;/b&gt;&lt;/span&gt;만을 담당하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;  비교는 MVC와 헥사고날로 한 것이지만 더 넓게 보자면&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SQL 중심의 개발 방식 vs DDD 개발 방식&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;을 두고 얘기한 것 이기도 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;헥사고날의 코드 구조와 장점&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이제 헥사고날 코드를 잠깐 구경하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Member라는 도메인 객체가 있고 해당 객체를 서비스 레이어에서 다루는 코드 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1727832661323&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Member {
  private Long id;
  private String name;
  private MemberCategory memberCategory;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727832663234&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MemberCategory {
  private Long id;
  private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727402589151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class FindMemberApplication implements FindOneMemberUseCase {

    @Override
    public Optional&amp;lt;Member&amp;gt; findOne(String userId) {
        return memberFindOutputPort.findOne(userId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;findOne이라는 비즈니스 메소드는 Member라는 서비스 레이어에서 도메인 단위로 데이터를 처리하게 됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그러니까 Entity나 DTO는 접근이 불가능하도록 의도했습니다.(물론 여기서 DTO로 변환 처리의 영역을 서비스가 담당하는 것이라고 볼 수도 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이때는 마치 &quot;서비스 로직에서는 비즈니스만 처리하면 되지, 변환은 필요없잖아.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;느낌으로 개발을 하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 그래서 Service에서는 DTO를 리턴하는게 아니라 도메인 객체를 리턴하게 되니&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;코드가 더 깔끔해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;온전히 비즈니스 로직에만 집중할 수 있는데요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 서비스 레이어는 도메인이 중심이 되기 때문에 헥사고날 아키텍처가 추구하는 Controller, Service, Data Access 계층 간의 완벽한 분리가 가능해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 Service 계층은 다른 곳에 위치시켜도 사용할 수 있는 범용적인 레이어가 된 것 입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;헥사고날의 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;근데 이런식으로 개발을 하다 보면 원하는 데이터를 넘길 수 없는 문제가 생깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;우선 첫번째로 클라이언트에서 백엔드, 두번째로는 백엔드에서 데이터베이스를 생각해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;클라이언트 &amp;lt;-&amp;gt; 백엔드&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예를 들어 클라이언트가 받고자 하는 데이터는 어떠한 Entity에 특정 count 정보입니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Member라는 도메인이 있으니 Member가 몇명인지를 세는 것이라고 가정해볼게요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예를 들어 Member Category별로 Member가 몇명인지 구하는 로직이 필요하다고 합시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;근데 도메인 객체는 그 도메인의 특징만을 가져야 하니까 count를 도메인 객체 안에 넣을 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그러면 Response 객체가 그 책임을 가져야하는 것인데요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1727403025913&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberCategoryResponse {
  private Long count;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;위와 같은 형태가  되겠네요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;근데 문제는 Count를 하기 위해는 비즈니스 로직에서 처리하는 것이 맞는데, 비즈니스 로직인 Service는 도메인 객체를 리턴하니까 Response 객체에 접근하는 것이 불가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Controller에서 Count를 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 비즈니스 로직을 처리하는 과정을 컨트롤러에서 하는 것이니 책임의 문제가 생깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 말은 &quot;책임의 분리&quot;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여태까지 DTO의 변환 과정을 어디서 할지에 대한 고민은 헥사고날 관점에서 시스템의 분리에 가까웠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;근데 시스템의 분리를 지키려고 하니 오히려 책임의 문제가 생기는 현상은 결국에 시스템의 분리와 책임의 분리는 서로 상호 보완적이지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시스템의 분리를 지키고자 책임의 분리를 깨고싶어하는 개발자는 없을 겁니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 이 방식은 좋지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;집계 함수용 도메인(MemberAggregate)을 하나 더 만들어서 바로 서비스에서 리턴한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;DDD 에서는 Aggregate라고 해서 일련의 Entity와 Value Object의 그룹을 의미하는 개념이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Value Object가 이런 개념이라고 볼 수도 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별개의 Value Object를 만들어서 리턴하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD에서 이런 방법으로 해결하는 것이 일종의 관례이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 뭔가 count 하나 때문에 객체를 만든다는 것은 규모가 작을 경우에 괜히 복잡함이 늘어난 기분입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;백엔드 &amp;lt;-&amp;gt; 데이터베이스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 클라이언트와 백엔드에서만 발생하는 문제는 데이터베이스와 백엔드에서도 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 Entity를 리턴해서 서비스 레이어에 Domain으로 변환해서 반환해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 Member가 1000명이고 Member가 속한  Category의 종류가 5개라고 가정했을 때 보통 이런 경우는 데이터베이스에서 조인을 통해 한번에 조회하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 말이죠.&lt;/p&gt;
&lt;pre id=&quot;code_1728281839384&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Query(&quot;SELECT m.name, COUNT(p) FROM Member m LEFT JOIN Post p ON m.id = p.member.id GROUP BY m.id&quot;)
List&amp;lt;Object[]&amp;gt; findMemberPostCounts();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 우리는 Object라는 Return 객체로는 Domain 객체로 매핑할 수 없겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 이것도 마찬가지로  Value Object를 하나 더 만들어서 처리해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 아니라면 헥사고날에서는 도메인을 반환해야하는 것이 원칙이니 Member를 따로 조회하고 Category를 따로 조회한다음에 각각의 도메인을 조합해서 Count 계산을 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식은 헥사고날의 시스템 분리는 지켜낼 수 있지만 속도는 지켜낼 수 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에 도메인 중심으로 개발하기 위해서는 복잡성이 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 프로젝트였으면 모든 레이어에서 별개의 DTO를 반환했으면 쉽게 끝날 문제였습니다.&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;컨트롤러와 레파지토리에 종속되는 서비스냐(SQL 중심 개발) vs 복잡한 구성으로 시스템의 분리도 지킬 것이냐(DDD)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 정답은 없겠지만 둘 중 하나를 억지로 선택하라고 한다면 저는 웬만하면 전자의 손을 들겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마틴 파울러의 의견은 아래와 같습니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;A Service Layer defines an application&amp;rsquo;s boundary [Cockburn PloP] and its set of available operations from the perspective of interfacing client layers. It encapsulates the application&amp;rsquo;s business logic, controlling transactions and coor-dinating responses in the implementation of its operations.&lt;br /&gt;&lt;br /&gt;서비스 계층은 인터페이스 클라이언트 계층의 관점에서 애플리케이션의 경계와 사용 가능한 작업 집합을 정의합니다.&lt;br /&gt;이는 애플리케이션의 비즈니스 로직을 캡슐화하고, 트랜잭션을 제어하고, 운영 구현 시 응답을 조정합니다.&lt;br /&gt;&lt;br /&gt;According to Martin Fowler: the Service Layer defines the application's boundary; it encapsulates the domain. In other words, it protects the domain.&lt;br /&gt;&lt;br /&gt;Martin Fowler에 따르면 서비스 계층은 애플리케이션의 경계를 정의합니다. 도메인을 캡슐화합니다. 즉, 도메인을 보호합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 코드가 간단합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 확장성도 없고 유지보수 성도 떨어지는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 큰 프로젝트라면 DDD가 더 좋은 선택이 될 수 있지만 이 경우엔 학습 곡선이 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아인슈타인이 이런 말을 했죠.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;당신이 알고 있는 것을 당신의 할머니가 이해할 수 있도록 설명하지 못한다면, 당신은 그것을 진정으로 이해한 것이 아닙니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 DDD로 확장성 있게 코드를 만들고 시스템을 분리하려는 것은 추후의 유지보수 측면이 제일 강하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 아직까지는 제 생각에는 쉽고 단순하지만 유지보수도 쉬울 수 있는 방식이 제일 올바른 방식이 아닐까하는 생각입니다.&lt;/p&gt;</description>
      <category>☕ Java</category>
      <category>q</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/507</guid>
      <comments>https://stir.tistory.com/507#entry507comment</comments>
      <pubDate>Thu, 19 Sep 2024 17:57:02 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] R2DBC DatabaseClient 잘 사용하기</title>
      <link>https://stir.tistory.com/506</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;소개&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux에서 DB 조회를 할 때 선택지가 많지만 개인적으로 가장 접근이 쉬운 방법에 대한 효율적인 공통화 클래스를 소개하고자 합니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DB 조회 종류&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ReactiveCrudRepository&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1726743554678&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Mono&amp;lt;User&amp;gt; findById(ID id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 조회를 할 때 좋은 선택이 될 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Criteria&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1726729259362&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Flux&amp;lt;T&amp;gt; findAll(Criteria criteria, Class&amp;lt;T&amp;gt; domainType) {
        return r2dbcTemplate.select(domainType)
                .matching(query(criteria))
                .all();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 쿼리의 유연성이 떨어진고 가독성이 떨어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서도 Criteria로 쿼리하지 않듯이 이것도 복잡성으로 인해 사용하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;QueryDSL&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1726727517553&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - infobip/infobip-spring-data-querydsl: Infobip Spring Data Querydsl provides new functionality that enables the user to &quot; data-og-description=&quot;Infobip Spring Data Querydsl provides new functionality that enables the user to leverage the full power of Querydsl API on top of Spring Data repository infrastructure. - infobip/infobip-spring-da...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/infobip/infobip-spring-data-querydsl&quot; data-og-url=&quot;https://github.com/infobip/infobip-spring-data-querydsl&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/q0Kan/hyW6wzENLr/ZxiFQ6C5jaN0TO1k9PspUK/img.png?width=1200&amp;amp;height=600&amp;amp;face=913_88_1093_268&quot;&gt;&lt;a href=&quot;https://github.com/infobip/infobip-spring-data-querydsl&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/infobip/infobip-spring-data-querydsl&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/q0Kan/hyW6wzENLr/ZxiFQ6C5jaN0TO1k9PspUK/img.png?width=1200&amp;amp;height=600&amp;amp;face=913_88_1093_268');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - infobip/infobip-spring-data-querydsl: Infobip Spring Data Querydsl provides new functionality that enables the user to&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Infobip Spring Data Querydsl provides new functionality that enables the user to leverage the full power of Querydsl API on top of Spring Data repository infrastructure. - infobip/infobip-spring-da...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL처럼 사용할 수 있는 오픈소스가 있지만 안정성의 문제로 사용하기 꺼려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용성에 관한 문제는 다른 블로그에서도 비슷한 글들이 있고 아래에 개인적인 사견도 담았으니 참고 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1726728665066&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;'공통'과 '오픈 소스'의 차이&quot; data-og-description=&quot;굉장히 지극히 사견일 수 있는 글을 작성하고자 합니다.'공통'과&amp;nbsp;'오픈&amp;nbsp;소스'의&amp;nbsp;차이개발을 할 때 공통 함수나 공통 클래스를 만드는 일은 빈번합니다.(없으면 할말이 없지만..)공통 함수와 오&quot; data-og-host=&quot;stir.tistory.com&quot; data-og-source-url=&quot;https://stir.tistory.com/505&quot; data-og-url=&quot;https://stir.tistory.com/505&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bGW96S/hyW6wsT2qM/g8Fr678nWeI6nYRhXhkH8K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dpbkAu/hyW20blfgw/0M1m3kmszMqckugSm4ImN1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://stir.tistory.com/505&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stir.tistory.com/505&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bGW96S/hyW6wsT2qM/g8Fr678nWeI6nYRhXhkH8K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dpbkAu/hyW20blfgw/0M1m3kmszMqckugSm4ImN1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;'공통'과 '오픈 소스'의 차이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;굉장히 지극히 사견일 수 있는 글을 작성하고자 합니다.'공통'과&amp;nbsp;'오픈&amp;nbsp;소스'의&amp;nbsp;차이개발을 할 때 공통 함수나 공통 클래스를 만드는 일은 빈번합니다.(없으면 할말이 없지만..)공통 함수와 오&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stir.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DatabaseClient&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC를 사용하는 개발자들이 가장 많이 선택하는 것이 DatabaseClient를 사용해 쿼리를 그대로 하는 것이라 직관적이며 그나마 제일 효율적인 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726729625694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;databaseCleint.sql(&quot;&quot;&quot;
      SELECT T1.id as id,
             T1.product_id as product_id,
             T1.name as name,
             T2.name as category_name,
      FROM TABLE_1 T1
      LEFT JOIN TABLE_2 T2
      ON T1.category_id = T2.id
      &quot;&quot;&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DatabaseClient 효율적으로 사용하기(핵심)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비효율적인 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1726729578756&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default Flux&amp;lt;Product&amp;gt; findBy(String name, Pageable pageable, DatabaseClient databaseClient) {
StringBuilder sql = new StringBuilder(&quot;&quot;&quot;
      SELECT T1.id as id,
             T1.product_id as product_id,
             T1.name as name,
             T2.name as category_name,
      FROM TABLE_1 T1
      LEFT JOIN TABLE_2 T2
      ON T1.category_id = T2.id
      &quot;&quot;&quot;);

if (name != null &amp;amp;&amp;amp; !name.isEmpty()) {
    sql.append(&quot;WHERE T1.name LIKE :name &quot;);
}

String sortOrder = getSort(pageable);
if (!sortOrder.isEmpty()) {
    sql.append(&quot;ORDER BY &quot;).append(sortOrder).append(&quot; &quot;);
}

sql.append(&quot;LIMIT :limit OFFSET :offset&quot;);

DatabaseClient.GenericExecuteSpec executeSpec = databaseClient.sql(sql.toString())
    .bind(&quot;limit&quot;, pageable.getPageSize())
    .bind(&quot;offset&quot;, pageable.getOffset());

if (name != null &amp;amp;&amp;amp; !name.isEmpty()) {
    executeSpec = executeSpec.bind(&quot;name&quot;, &quot;%&quot; + name + &quot;%&quot;);
}

return executeSpec
    .map((row, rowMetadata) -&amp;gt; toEntity(row))
    .all();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드 공통 함수 없이&amp;nbsp; DatabaseClient만을 이용해 쿼리하는 방식입니다.&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;name에 대한 String Append를 해주기 위해 조건식을 사용했는데,&lt;br /&gt;executeSpec에 bind를 하기 위해 또 조건식을 중복으로 사용한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 String을 Append하면 될 수도 있지만 DatabaseClient가 이렇게 개발된 이유는 SQL Injection을 방지하기 위함입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;:을 이용한 콜론 방식의 바인딩을 이용한 쿼리는 데이터베이스가 쿼리를 먼저 파싱하고 그 후에 값을 바인딩 합니다.&lt;br /&gt;바인딩하고 나서 이스케이프 처리를 하기 때문에 특수문자가 들어갈 수 없습니다.&lt;br /&gt;결과적으로 내부에선 'data;with;semicolons' 이런식으로 구문처리가 아닌 String으로 처리합니다.&lt;/blockquote&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;이걸 어떻게 효율적으로 쓸 수 있을까 고민해보다가 처음에는 일부분만 QueryDSL처럼 빌더 패턴을 만들어볼까 했지만 이게 꽤나 한계점이 보여서 패스했습니다.&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;b&gt;효율적인 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1726730103943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default Flux&amp;lt;Product&amp;gt; findBy(String name, Pageable pageable, DatabaseClient databaseClient) {
SqlBuilder sqlBuilder = new SqlBuilder().append(&quot;&quot;&quot;
      SELECT T1.id as id,
             T1.product_id as product_id,
             T1.name as name,
             T2.name as category_name,
      FROM TABLE_1 T1
      LEFT JOIN TABLE_2 T2
      ON T1.category_id = T2.id
      &quot;&quot;&quot;);

    sqlBuilder.appendIfPresent(&quot;WHERE SP.name LIKE %:name%&quot;, name);
    sqlBuilder.appendIfPresent(&quot;ORDER BY &quot; + getSort(pageable));
    sqlBuilder.append(&quot;LIMIT :limit &quot;, pageable.getPageSize());
    sqlBuilder.append(&quot;OFFSET :offset &quot;, pageable.getOffset());

    return sqlBuilder.execute(databaseClient)
      .map((row, rowMetadata) -&amp;gt; toEntity(row))
      .all();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두둥.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 한눈에 들어오는 형태로 바뀌었습니다. 뭐 거의 배울 필요도 없을 정도죠.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 메소드가 default로 시작하는데 Repository 인터페이스에 담았기 때문에 그렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 JPA에서 Spring Data JPA와 QueryDSL를 하나의 파일에서 관리하는 방법을 참고하면 됩니다.&lt;br /&gt;저는 아직 비즈니스 로직 규모가 작아서 패스..&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여튼 위의 방식으로 사용하기 위해 SqlBuilder 클래스를 아래 적겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import lombok.Getter;
import org.springframework.r2dbc.core.DatabaseClient;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SqlBuilder {
  private final StringBuilder sql = new StringBuilder();
  @Getter
  private final Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();

  public SqlBuilder append(String sqlPart) {
    sql.append(sqlPart).append(&quot;\n&quot;);
    return this;
  }


  public SqlBuilder append(String sqlPart, Object paramValue) {
    sql.append(sqlPart).append(&quot;\n&quot;);
    extractParams(sqlPart).forEach(paramName -&amp;gt; {
      if (paramValue != null) {
        params.put(paramName, paramValue);
      }
    });
    return this;
  }

  public void appendIfPresent(String sqlPart, Object paramValue) {
    if (paramValue != null) {
      this.append(sqlPart, paramValue);
    }
  }

  private List&amp;lt;String&amp;gt; extractParams(String sqlPart) {
    List&amp;lt;String&amp;gt; paramNames = new ArrayList&amp;lt;&amp;gt;();
    Matcher matcher = Pattern.compile(&quot;:([\\w_]+)&quot;).matcher(sqlPart);
    while (matcher.find()) {
      paramNames.add(matcher.group(1));
    }
    return paramNames;
  }

  public DatabaseClient.GenericExecuteSpec execute(DatabaseClient databaseClient) {
    DatabaseClient.GenericExecuteSpec executeSpec = databaseClient.sql(sql.toString());
    for (Map.Entry&amp;lt;String, Object&amp;gt; entry : getParams().entrySet()) {
      executeSpec = executeSpec.bind(entry.getKey(), entry.getValue());
    }
    return executeSpec;
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 getSort 부분만 +로 연결한 방식을 이용했는데, 이는 ORDER BY는 콜론으로 데이터 바인딩이 불가능하기 떄문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 +로 연결하되 SQL Injection을 방지하는 코드도 개발했는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1728286266589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DatabaseClientUtils {
  public static String getSort(Pageable pageable) {
    String sort = pageable.getSort().isSorted() ?
      pageable.getSort().stream()
        .map(order -&amp;gt; String.format(&quot;%s %s&quot;, StringUtils.camelToSnake(order.getProperty()), order.getDirection().name()))
        .collect(Collectors.joining(&quot;, &quot;))
      : &quot;id ASC&quot;;  // 정렬 조건이 없을 때 기본값 설정
    if (!isValidInput(sort)) {
      throw new CommonExceptions.BadRequestException(&quot;Invalid sort field: &quot; + sort);
    }
    return sort;
  }

  public static boolean isValidInput(String input) { // Prevent SQL Injection
    return input != null &amp;amp;&amp;amp; input.matches(&quot;^[a-zA-Z0-9_ ]+$&quot;) &amp;amp;&amp;amp; !containsSQLKeywords(input);
  }
  private static final List&amp;lt;String&amp;gt; FORBIDDEN_KEYWORDS = Arrays.asList(
    &quot;select&quot;, &quot;drop&quot;, &quot;delete&quot;, &quot;insert&quot;, &quot;update&quot;, &quot;alter&quot;, &quot;--&quot;, &quot;;&quot;, &quot;exec&quot;, &quot;execute&quot;, &quot;union&quot;
  );
  public static boolean containsSQLKeywords(String input) {
    String lowerInput = input.toLowerCase();
    for (String keyword : FORBIDDEN_KEYWORDS) {
      if (lowerInput.contains(keyword)) {
        return true;
      }
    }
    return false;
  }
  public static &amp;lt;T&amp;gt; T getValue(Row row, String columnName, Class&amp;lt;T&amp;gt; type) {
    try {
      return row.get(columnName, type);
    } catch (NoSuchElementException e) {
      return null;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;다 적고나니까 이걸 누가쓰겠냐는 상상이..&amp;nbsp;&lt;/s&gt;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/506</guid>
      <comments>https://stir.tistory.com/506#entry506comment</comments>
      <pubDate>Thu, 19 Sep 2024 16:07:23 +0900</pubDate>
    </item>
    <item>
      <title>'공통'과 '오픈 소스'의 차이</title>
      <link>https://stir.tistory.com/505</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 지극히 사견일 수 있는 글을 작성하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;'공통'과&amp;nbsp;'오픈&amp;nbsp;소스'의&amp;nbsp;차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 할 때 공통 함수나 공통 클래스를 만드는 일은 빈번합니다.(&lt;s&gt;없으면 할말이 없지만..&lt;/s&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 함수와 오픈 소스의 공통점은 &quot;쓰기 쉽게 공통으로 만들어 놓은 무엇&quot;을 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 차이점은 뭘까요? 이 차이점에 대한 특징을 모르고 초보자들의 의욕만 앞선 실수가 등장 하는 경우가 꽤나 많이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;오픈 소스가 가지는 특징&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스는 신뢰성이 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐면 적어도 오랜기간 이상 사용해도 무너지지 않을만큼의 무언가가 있어야한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 완벽하게 만드는 것을 의미하는 게 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 조건은 꽤나 여러가지겠지만 지금 생각나는 것은 아래 2가지 정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 커뮤니티가 존재하는가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 누군가가 해당 오픈소스를 관리해주는가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 단 1명이 오픈 소스를 관리하고 있다면 그 오픈 소스를 신뢰할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저라면 쓰지 않을 겁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공통 함수가 가지는 특징&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 팀단위로 일을 하다보면 공통 함수를 누군가 만들어 놓은 경우가 많이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 공통 함수가 가져야 할 특징이라고 한다면 저의 생각은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 적게 배우고 쓰기 쉬워야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 더 이상의 수정의 여지 없이 그 자체로 완전해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 때 흔히 하는 실수는 내가 쓰기 편한 기능이 공통이라고 생각하는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남들도 편해야 공통의 조건이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 위 조건이 지켜지지 않는 경우에는 신규 입사자가 들어와서 그 공통 함수를 배우고 사용해야 하는 것은 꽤나 시간을 잡아 먹습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스는 누구나 아는 지식이 될 수 있기 때문에 범용성이 있지만 공통은 그런 조건을 가지면 안되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 조건이 지켜지지 않는 경우에 신규 입사자들은 해당 함수에 대한 수정의 책임도 갖게 됩니다. 비효율도 이런 비효율이 없겠죠.&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/505</guid>
      <comments>https://stir.tistory.com/505#entry505comment</comments>
      <pubDate>Thu, 19 Sep 2024 15:50:52 +0900</pubDate>
    </item>
    <item>
      <title>[InnoDB Engine] 페이지란?</title>
      <link>https://stir.tistory.com/501</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 행(Row)을 저장할 때 실제 물리적으로 디스크에 담기는 단위는 '페이지'로 관리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 페이지에는 여러개의 행이 담긴다고 볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지의 목적&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;효율적인 데이터 관리와 성능을 위해 데이터베이스 시스템에서 자주 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 단위로 데이터 접근을 최적화하면, 데이터 조회와 수정 시 성능을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 크기는 디스크 I/O를 줄이고, 캐시 메모리 사용을 최적화하는 데 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지는 메모리 내에서 효율적으로 데이터를 캐싱하고, 페이지를 통해 데이터 접근을 관리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지의 특징&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB에서는 데이터와 인덱스가 디스크에 저장될 때 페이지라는 블록 단위로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각&lt;/span&gt; &lt;span&gt;페이지는&lt;/span&gt; &lt;span&gt;고정된&lt;/span&gt; &lt;span&gt;크기를&lt;/span&gt; &lt;span&gt;가지며&lt;/span&gt;, &lt;span&gt;일반적으로&lt;/span&gt; 4KB, 8KB, 16KB, &lt;span&gt;또는&lt;/span&gt; 32KB&lt;span&gt;로&lt;/span&gt; &lt;span&gt;설정할&lt;/span&gt; &lt;span&gt;수&lt;/span&gt; &lt;span&gt;있습니다&lt;/span&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지 크기 확인&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1726125914408&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW VARIABLES LIKE 'innodb_page_size';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/McDWN/btsJAjaCjBf/aDoLbQSIGmRwDUY0GkU5f0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/McDWN/btsJAjaCjBf/aDoLbQSIGmRwDUY0GkU5f0/img.png&quot; data-alt=&quot;16KB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/McDWN/btsJAjaCjBf/aDoLbQSIGmRwDUY0GkU5f0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMcDWN%2FbtsJAjaCjBf%2FaDoLbQSIGmRwDUY0GkU5f0%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;306&quot; height=&quot;54&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;16KB&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;페이지 저장 방식의 종류&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;redundant&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이노 디비 초기 사용 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조&lt;/b&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;b&gt;compact&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;768 바이트만 데이터를 저장하고 나머지 데이터는 Off-Page에 저장하며 해당 데이터에 대한 포인터는 페이지에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Off-Page는 아래에서 다시 설명합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;dynamic&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;페이지 크기보다 작으면 데이터를 페이지 안에 저장하고 페이지 크기 보다 크면 Off-Page에 데이터를 추가적으로 저장합니다.&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;페이지 저장 방식 확인&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1726125711731&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT TABLE_NAME, ROW_FORMAT
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'mydatabase';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;140&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnMR7o/btsJAXYVRWt/C1eZSwwKUVlNH4fVRY1EwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnMR7o/btsJAXYVRWt/C1eZSwwKUVlNH4fVRY1EwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnMR7o/btsJAXYVRWt/C1eZSwwKUVlNH4fVRY1EwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnMR7o%2FbtsJAXYVRWt%2FC1eZSwwKUVlNH4fVRY1EwK%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;140&quot; height=&quot;57&quot; data-origin-width=&quot;140&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 페이지 저장 방식을 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overflow page(Off-Page)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 하나에 4KB라고 했을 때&lt;span&gt; &lt;/span&gt;가변&lt;span&gt; &lt;/span&gt;길이&lt;span&gt; &lt;/span&gt;열이&lt;span&gt; &lt;/span&gt;페이지&lt;span&gt; &lt;/span&gt;크기를&lt;span&gt; &lt;/span&gt;초과하면 페이지의&lt;span&gt; &lt;/span&gt;크기와&lt;span&gt; &lt;/span&gt;구조를&lt;span&gt; &lt;/span&gt;효율적으로&lt;span&gt; &lt;/span&gt;사용하기&lt;span&gt; &lt;/span&gt;위해&lt;span&gt; &lt;/span&gt;데이터의&lt;span&gt; &lt;/span&gt;일부만&lt;span&gt; &lt;/span&gt;페이지에&lt;span&gt; &lt;/span&gt;저장하고&lt;span&gt;, &lt;/span&gt;나머지&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;별도의 Off-Page에&lt;span&gt; &lt;/span&gt;저장합니다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 하면 페이지의 내부 공간을 절약할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;데이터 전체를 페이지에 저장하면 되는데 왜 Off-Page가 존재할까?&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;페이지 간에 데이터를 연속적으로 저장하는  것은 페이지의 목적에 맞지 않습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위에서 서술한 페이지의 목적을 보면 데이터를 잘 찾기 위한 기능을 위해서 존재하는 것이지 데이터를 저장하는 목적은 아닌 것을 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Off-page에 저장되는 것은 페이지의 효율적인 사용을 위함이고 Off-page에 저장된 데이터를 실제 조회하면 속도가 저하됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 페이지 내에 저장되는 것이 검색 성능에 더 유리하며, 페이지 캐시의 효율성을 높입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;페이지 상태 확인&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1726126130612&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 명령어를 이용하면 현재 할당된 페이지의 수, 수정된 페이지, 더러운 페이지(Dirty Page)의 비율을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dirty Page란 데이터베이스의 버퍼 풀에서 수정되었지만 아직 디스크에 기록되지 않은 페이지를 의미합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비율이 높다면&amp;nbsp;데이터베이스가 더 자주 디스크에 데이터를 플러시해야 한다는 것을 의미할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;생각해볼 수 있는 것&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 페이지 크기가 16kb라면 8192개의 글자를 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이보다 크기를 넘어버리면 Off-Page에 넘어가버리므로 속도 저하는 피해갈 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Json을 통째로 문자열로 치환해 컬럼에 저장한다면 데이터베이스 속도저하의 가능성이 높아지겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/501</guid>
      <comments>https://stir.tistory.com/501#entry501comment</comments>
      <pubDate>Thu, 12 Sep 2024 16:43:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Webflux에 Mongo 연결(+Kubernetes)</title>
      <link>https://stir.tistory.com/500</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Kubernetes에 Helm으로 Mongo 설치&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1725501838629&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;helm show values bitnami/mongodb &amp;gt; custom-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 명령어로 설치해도 되지만 사용자 ID나 PVC의 용량을 미리 설정하기 위해 미리 yaml 파일을 수정해서 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725501328542&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;auth:
  enabled: true
  rootUser: mongo-user
  rootPassword: mongo-password
  database: mydatabase
-------------------------------------
persistence:
  enabled: true
  size: 1Gi
-------------------------------------
service:
  type: LoadBalancer
  port: 27017
-------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 사용자 정보와 데이터베이스 명, PVC 크기, Service 타입을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 LoadBalancer로 설정합니다. &lt;/p&gt;
&lt;pre id=&quot;code_1725501791560&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install my-mongodb -f custom-values.yaml bitnami/mongodb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;업데이트&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1725501925704&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm upgrade my-mongodb -f custom-values.yaml bitnami/mongodb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;삭제&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1725501938639&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm uninstall my-mongodb&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;연결 정보 확인&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 kubectl get svc를 통해서 LoadBalancer의 External IP를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 아이피와 27017 그리고 위에서 설정한 사용자 ID와 패스워드가 연결정보가 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MongoDB 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Pod에 직접 접속&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k exec 명령어를 통해서 Pod에서 Mongo 명령어를 사용해볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725502056400&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mongosh admin -u myid -p mypw

use mydb;

db.createCollection(&quot;MyCollection&quot;);

db.ProvisioningInfo.insertOne( { &quot;id&quot;:&quot;1&quot;, &quot;name&quot;:&quot;stir&quot; });

db.ProvisioningInfo.find({});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인텔리제이에서 접속&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zdw6E/btsJqIB7V8T/sD2kSWj45reLPltQKvPekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zdw6E/btsJqIB7V8T/sD2kSWj45reLPltQKvPekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zdw6E/btsJqIB7V8T/sD2kSWj45reLPltQKvPekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzdw6E%2FbtsJqIB7V8T%2FsD2kSWj45reLPltQKvPekk%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;550&quot; height=&quot;278&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이에서도 똑같이 연결해서 사용 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Webflux에 Mongo 연결해서 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1725501707275&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-data-mongodb-reactive&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725501709856&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;data:
    mongodb:
      host: ExternalIp
      port: 27017
      database: mydb # 사용할 데이터베이스 이름
      username: myid # MongoDB 사용자 이름 (옵션, 필요 시)
      password: mypw # MongoDB 비밀번호 (옵션, 필요 시)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yaml 파일을 연결 정보에 있던 것으로 수정해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1725501662694&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
@Document(value=&quot;MyCollection&quot;)
public class MyCollection implements Serializable {
  private static final long serialVersionUID = 142466781L;

  @Id
  private String id;

  private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725501671991&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface MyCollectionRepository extends ReactiveMongoRepository&amp;lt;MyCollection, String&amp;gt; {
  Flux&amp;lt;MyCollection&amp;gt; findAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 Repository를 연결해서 사용하면 됩니다.&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/500</guid>
      <comments>https://stir.tistory.com/500#entry500comment</comments>
      <pubDate>Thu, 5 Sep 2024 11:12:25 +0900</pubDate>
    </item>
    <item>
      <title>[istio] 서비스 진입 경로 설정 방법과 우회 시 기존 룰 순서의 중요성</title>
      <link>https://stir.tistory.com/497</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;istio의 Virtual Service 간략한 소개&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 사전 지식을 함양하기 위해 Virtual Service에 대한 간략한 소개를 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;istio를 사용하는 경우 특정 도메인 접근 시 쿠버네티스에서 어떤 서비스 자원에 진입시킬 지 정의할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722574266358&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;k edit vs -n istio-system my-virtual-service&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722574179784&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; - corsPolicy:
      allowHeaders:
      - content-type
      - access-control-allow-origin
      allowMethods:
      - GET
      - POST
      - PATCH
      - PUT
      - DELETE
      - OPTIONS
      allowOrigins:
      - exact: '*'
    headers:
      request:
        set:
          Authorization: Bearer %REQ(x-auth-request-access-token)%
    match:
    - uri:
        prefix: /v1/my-first-pod
    route:
    - destination:
        host: my-first-svc.project.svc.cluster.local
        port:
          number: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;virtual service 자원 내에 위와 같이 /v1/my-first-pod로 진입시 my-first-svc라는 서비스에 연결하도록 설정할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서비스 연결 순서와 우회 계획&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 연결 방식은 사용자가 요청 시 OAuth2 -&amp;gt; my-first-svc -&amp;gt; my-second-svc 의 순서로 요청되는 형태라고 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2로 항상 인증을 거치고 그 다음에 my-first-svc에 진입합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이 OAuth2 인증과 my-first-svc를 거치지 않고 바로 my-second-svc로 진입하게 만들어 볼 것 입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;istio의 AuthorizationPolicy에서 &lt;/b&gt;&lt;b&gt;OAuth2 인증 패스&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Service가 특정 경로로 진입하는 것을 도와주는 역할이었다면 AuthorizationPolicy에서는 특정 경로로 진입 시 특정 기능을 거치지 않게 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722575803811&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;k edit authorizationpolicies.security.istio.io -n istio-system auth-admin-policy&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1722574332678&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spec:
  action: CUSTOM
  provider:
    name: oauth2-proxy-admin
  rules:
  - to:
    - operation:
        hosts:
        - my-site.com
        notPaths:
        - /auth
        - /v1/my-first-pod/bypass&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/v1/my-first-pod/bypass로 진입하면 이제 OAuth2 인증을 제외 시킬 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;특정 파드 진입 패스&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 OAuth2는 통과를 했으니 virtual service를 수정해서 my-first-svc를 통과시켜보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722574952766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;match:
    - uri:
        prefix: /v1/my-first-pod
    route:
    - destination:
        host: my-first-svc.project.svc.cluster.local
        port:
          number: 8080
match:
    - uri:
        prefix: /v1/my-first-pod/bypass
    route:
    - destination:
        host: my-second-svc.project.svc.cluster.local
        port:
          number: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/v1/my-first-pod/bypass로 진입시 my-second-svc로 바로 연결할 수 있도록 설정합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제의 원인과 해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정을 하면 /v1/my-first-pod /bypass 라는 특정 경로로 진입하는 경우 my-second-svc로 바로 연결할 수 있는 것처럼 보이기 때문에 정상작동 할 것이라고 예상할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만  문제는 my-second-svc가 아니라 my-first-svc부터 진입하게 되는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 Virtual Service의 설정에 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722575476956&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;match:
    - uri:
        prefix: /v1/my-first-pod/bypass
    route:
    - destination:
        host: my-second-svc.project.svc.cluster.local
        port:
          number: 8080
match:
    - uri:
        prefix: /v1/my-first-pod
    route:
    - destination:
        host: my-first-svc.project.svc.cluster.local
        port:
          number: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;/bypass로 진입하는 것을 더 위에 올려놔야 우선순위가 높 이 적용 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;설정이 완료됐다면 rollout을 해서 적용시키면 올바르게 우회를 시킬 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1722574829823&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;k rollout restart deployment -n istio-system istiod istio-gateway&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;1101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ljlOP/btsIUwAI5eY/vlpVxIJ8SFVJyvQGHU6QOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ljlOP/btsIUwAI5eY/vlpVxIJ8SFVJyvQGHU6QOK/img.png&quot; data-alt=&quot;회사에서 열심히 그림판으로 토의..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ljlOP/btsIUwAI5eY/vlpVxIJ8SFVJyvQGHU6QOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FljlOP%2FbtsIUwAI5eY%2FvlpVxIJ8SFVJyvQGHU6QOK%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;1986&quot; height=&quot;1101&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;1101&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>☸️ Kubernetes</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/497</guid>
      <comments>https://stir.tistory.com/497#entry497comment</comments>
      <pubDate>Fri, 2 Aug 2024 14:11:57 +0900</pubDate>
    </item>
    <item>
      <title>MaxScale 지식</title>
      <link>https://stir.tistory.com/496</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MaxScale은 MariaDB 에서 지원하는 DB Proxy 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HA proxy 와 동일하나 DB read/ write 쿼리를 지정한 db 서버로 &amp;nbsp;나눠주는 기능이 있는 등 DB에 더 특화되어있습니다.&lt;br /&gt;read / write 분산이 필요한 아키텍처에서 MaxScale 을 사용하면 효율적으로 분산 구조를 구성할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l9NYW/btsIEM59PGG/lrkHmaXSCV7wkp4ONKxAu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l9NYW/btsIEM59PGG/lrkHmaXSCV7wkp4ONKxAu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9NYW/btsIEM59PGG/lrkHmaXSCV7wkp4ONKxAu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl9NYW%2FbtsIEM59PGG%2FlrkHmaXSCV7wkp4ONKxAu1%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;713&quot; height=&quot;115&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;115&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/496</guid>
      <comments>https://stir.tistory.com/496#entry496comment</comments>
      <pubDate>Fri, 19 Jul 2024 10:26:22 +0900</pubDate>
    </item>
    <item>
      <title>[Mysql] No space left on Issue 해결</title>
      <link>https://stir.tistory.com/494</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 DDL, DML 명령어 수행 시 발생하는 이슈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시스템 및 디비 용량 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 용량 확인&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1718001819709&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    TABLE_NAME AS `Table`,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024) AS `Size (MB)`
FROM
    information_schema.TABLES
WHERE
        TABLE_SCHEMA = ' DB 이름'
ORDER BY
    (DATA_LENGTH + INDEX_LENGTH)
        DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시스템 용량 확인&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1718001837489&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;df -h /var/lib/mysql
du -sh ./*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Binary Log 파일의 용량이 과도한 것을 확인할 수 있으므로 삭제하면 된다.&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;추후 해당 이슈를 막으려면 /etc/mysql/my.cnf에 설정 파일을 수정해줘야하는데, 현재 환경이 쿠버네티스 환경이라 ConfigMap을 수정해야하므로 일단 패스.. 어차피 DML이기 때문이기도 함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;max_binlog_files=50 # mysql-bin 파일의 총 개수&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;max_binlog_size=100M # mysql-bin 파일의 사이즈&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;expire_logs_days=7&amp;nbsp;&amp;nbsp;&amp;nbsp;# mysql-bin 파일의 보관 날짜&lt;/span&gt;&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/494</guid>
      <comments>https://stir.tistory.com/494#entry494comment</comments>
      <pubDate>Mon, 10 Jun 2024 15:52:27 +0900</pubDate>
    </item>
    <item>
      <title>일본어 &amp;quot;어떻게&amp;quot; - 도얏테(どうやって), 도오(どう), 난토(なんと) 차이</title>
      <link>https://stir.tistory.com/492</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;도얏테(どうやって)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에키니도얏테이키마스까?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;駅にどうやって行きますか? 역으로 어떻게 갑니까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도얏테는 행위에 대한 동사를 물어보는 질문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 역으로 어떻게 가는지에 대한 답은 &quot;걸어서&quot;, &quot;뛰어서&quot; 등에 대한 행위에 대한 동사를 대답으로 표현할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;도오(どう)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도오오모이마스까?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;どう思いますか？- 어떻게 생각해요?&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;니혼고노뱅쿄와도오?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;日本語の勉強はどう？ - 일본어 공부는 어때?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도오는 현재 상황을 물어볼 때 쓰입니다. &quot;좋다&quot;, &quot;나쁘다&quot; 정도가 대표적이겠네요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;과거 상황을 물어볼 땐 どうだった(도우닷타)를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아타라시이 시고토 도우닷타?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;あたらしいしごと、どうだった - 새로운 일 어땠어?&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;난토(なんと)&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코코노에키와난토이이마스까?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ここの駅はなんと言いますか？ - 이 역은 어떻게 말합니까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;난토 역시 &quot;어떻게&quot;로 번역되지만 동사에 대한 질문이 아닌 명사 질문이라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대답은 &quot;서울역&quot;이 가장 대표적이겠네요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외 + 도시테(&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;どう&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;し&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;て)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가끔은 위의 예시를 섞어서 쓰는 케이스도 존재하지만 대부분은 안 어울릴 수 있으니 정확히 아는 것이 중요하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도시테도 어떻게라는 의미가 있지만 &quot;왜?&quot;라는 의미도 있기 때문에 &quot;어떻게&quot;라는 의미로는 사용하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도시테 이키마스까?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;どうして行くんですか？ - 왜 갑니까?&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/492</guid>
      <comments>https://stir.tistory.com/492#entry492comment</comments>
      <pubDate>Tue, 14 May 2024 01:12:31 +0900</pubDate>
    </item>
    <item>
      <title>혹독한 조언이 나를 살릴까?</title>
      <link>https://stir.tistory.com/480</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크에 있는 글을 가져온 것이다.&lt;/p&gt;
&lt;figure id=&quot;og_1710737067002&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;혹독한 조언이 나를 살릴까?&quot; data-og-description=&quot; &quot; data-og-host=&quot;web.archive.org&quot; data-og-source-url=&quot;https://web.archive.org/web/20200227120819/http://agile.egloos.com/5931859&quot; data-og-url=&quot;https://web.archive.org/web/20200227120819/http://agile.egloos.com/5931859&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFU2lx/hyVAJuNQ8A/iQMwWjVD7qUZuvmqSR0rT1/img.jpg?width=250&amp;amp;height=250&amp;amp;face=0_0_250_250&quot;&gt;&lt;a href=&quot;https://web.archive.org/web/20200227120819/http://agile.egloos.com/5931859&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://web.archive.org/web/20200227120819/http://agile.egloos.com/5931859&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFU2lx/hyVAJuNQ8A/iQMwWjVD7qUZuvmqSR0rT1/img.jpg?width=250&amp;amp;height=250&amp;amp;face=0_0_250_250');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;혹독한 조언이 나를 살릴까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;web.archive.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;100%&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #000000;&quot; width=&quot;90%&quot;&gt;
&lt;div id=&quot;LEFT&quot; style=&quot;color: #000000;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div style=&quot;color: #000000; text-align: left;&quot;&gt;우리 주변에서 종종 혹독한 조언, 소위&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;뼈 때리는 조언&lt;/b&gt;을 해주기로 유명한 사람들이 있습니다. 그리고 그걸 좋아해주는 사람도 흔치 않게 볼 수 있습니다. 반대로 이 혹독함에 대해 조금이라도 불평이 나오려고 하면 &quot;&lt;b&gt;좋은 약은 입에 쓰다&lt;/b&gt;&quot;는 아포리즘으로 이 폭력성을 정당화하기도 합니다. 그런데 정말 이런 혹독함이 우리를 성장하게 할까요.&lt;br /&gt;&lt;br /&gt;심리상담학계에는 전설적인 다큐멘타리가 하나 있습니다. 일명&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;글로리아 필름&lt;/b&gt;(Gloria Film)으로 불립니다. 1964년도에 촬영되었습니다.&lt;br /&gt;&lt;br /&gt;글로리아라는 일반 여성을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;당대 최고의 심리상담가 3명&lt;/b&gt;이 각기 약 30분씩 돌아가며 상담을 하는 모습을 담은 다큐멘타리입니다. 참고로 3명의 상담 후에는 글로리아에게 누구에게 상담을 받겠냐고 선택하게 했습니다.&lt;br /&gt;&lt;br /&gt;이 세명은 인간 중심 상담의 선구자 칼 로저스, 게슈탈트 치료의 선구자 프리츠 펄스, 합리정서행동 치료(REBT)의 선구자 알버트 엘리스입니다. 이름만 들어도 대단한 사람들인데, 한 사람을 두고 서로 어떻게 다른 방식으로 상담하는지를 비교해볼 수 있어 심리상담자들에게 매우 교육적인 영상입니다.&lt;br /&gt;&lt;br /&gt;그런데 이 필름은 상담학계에 나름 많은 논란을 만들어 냈습니다. 그 이유는, 예를 들면 칼 로저스는 굉장히 잘 들어주고 응원해주는 방식을 보여주는 반면, 프리츠 펄스는 직면하고 상대의 잘못을 드러내는 식으로 진행하는(&lt;a href=&quot;https://youtu.be/8y5tuJ3Sojc&quot;&gt;https://youtu.be/8y5tuJ3Sojc&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 영상을 볼 수 있다) 대조적인 상담방식에 대한 견해 차이에서 옵니다.&lt;br /&gt;&lt;br /&gt;영상을 보면 알겠지만 펄스는 글로리아를 화나게, 그리고 복종적으로 만들었습니다. 글로리아를 사기꾼(phony)이라고 말하고, 멍청한 척한다(playing stupid)고도 했습니다.&lt;br /&gt;&lt;br /&gt;글로리아 필름을 상담을 공부하는 학생들에게 보여주면 대부분 글로리아는 칼 로저스를 선택했을 거라고 생각하게 됩니다. 하지만 의외로 촬영이 끝나고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;글로리아는 프리츠 펄스를 선택&lt;/b&gt;합니다. 글로리아가 어떤 마음이었는지는 모르겠지만, 좋은 약은 입에 쓰다는 믿음에 의한 결정이 아니었을까 싶습니다 -- 그녀에게 선택의 이유를 묻자, 자신에게는 프리츠 펄스 방식이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가장 가치있을 것 같다&lt;/b&gt;(could be the most valuable to me)고 했습니다.&lt;br /&gt;&lt;br /&gt;사실 스토리는 여기가 끝이 아닙니다. 그러고 1년 후에 칼 로저스가 진행하는 워크숍에 글로리아가 참석하게 되고, 이 상담 영상을 함께 보다가 흥분해서 그자리에서 일어나 이런 말을 했다고 합니다. &quot;왜 저는 그 사람이 시키는 걸 다 한거죠?! 왜 그 사람이 저한테 그렇게 하도록 제가 허락한거죠?!&quot;(&quot;Why did I do all those things that he told me to do?! Why did I let him do that to me?!&quot;)[0]&lt;br /&gt;&lt;br /&gt;그리고 10년도 넘어서 글로리아는 당시 상담에 대해 이런 의견을 남겼습니다. &quot;짧은 세션 끝에서 저는 자신의 일부가 파괴된다고 느꼈어요. 그 세션 후에 저의 온전한 자아가 어떻게 산산히 부서졌는지요&quot;(&quot;I felt a bit of myself destroyed at the end of that short session ... How shattered my whole being felt after that session&quot;)[1] 그럼에도 불구하고 글로리아는 이 사람을 선택했었습니다.&lt;br /&gt;&lt;br /&gt;글로리아가 죽고나서 그녀의 딸이 어머니에 대한 이야기를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0000ff;&quot; href=&quot;https://web.archive.org/web/20200227120819/https://www.amazon.com/Living-Gloria-Films-daughters-memory/dp/1906254028&quot;&gt;책&lt;/a&gt;으로 냈습니다. 거기에는 글로리아가 칼 로저스랑 계속 인연을 이어 온 것, 얼마나 칼 로저스를 신뢰했는지 등이 나와있으며, 무엇보다도 어머니가 당시 촬영직후의 선택을 후회했다는 내용이 나옵니다.&lt;br /&gt;&lt;br /&gt;그 상담 이후 진정 글로리아를 성장하게 한 것은 칼 로저스와의 15년 동안(갑작스럽게 그녀가 사망할 때까지)의 애정어린 서신 교환 덕분이었다고 생각이 듭니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;글로리아를 비난하는 조언은 그녀를 성장시키지 못했습니다&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;코칭을 하면서 이런 환상을 갖고 있는 사람을 종종 보게 됩니다. 뭔가 자신의 잘못된 점을 공격 받아야 나에게 실제로 도움되는 걸 했다는 느낌을 받는 것. 한의원에 가서도 침으로 몇 군데 콕콕 쑤셔줘야 치료 좀 했다고 느끼는 거랑 비슷하지 않나 싶습니다.&lt;br /&gt;&lt;br /&gt;그렇다고 해서 칼 로저스 방식이 정말 글로리아에게 &quot;도움이 됐을까?&quot;하고 반문하는 분이 계실 수 있다고 생각합니다. 모레이라 등의 2011년 연구를 보면[2] 칼 로저스와 상담을 할 때 글로리아의 네러티브가 다른 두 상담자와 할 때에 비해 훨씬 더(1 표준편차 이상으로) 복잡하고 다면적입니다. 당연히 이럴 경우 상담효과가 좋습니다.&lt;br /&gt;&lt;br /&gt;심리상담학이 수십년 연구를 통해 결론을 내린 것이 하나라도 있다면 그것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내담자를 존중하지 않는 방식은 장기적으로 효과가 없거나 부정적이었다는 것&lt;/b&gt;이고, 동시에 내담자들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 방식에 현혹될 수도 있다&lt;/b&gt;는 것이지 않을까 합니다. 예컨대 중독상담 쪽에서도 내담자와 직면(당신은 틀렸다)하고 내담자를 비난하는 방식은 실제로 중독치료의 효과가 떨어졌습니다(관심있는 분들은 동기면담Motivational Interviewing 쪽의 연구를 참고하세요).&lt;br /&gt;&lt;br /&gt;혹시나 상담은 다른 이야기이고, 나는 &quot;교육&quot;을 얘기하는 거라고 반박하는 분이 있을까봐 또 다른 연구를 언급하고자 합니다. 넬리우스-화이트는 2007년에 출판한 자신의 논문[3]에서 355,325명의 학생, 14,851명의 선생, 2,439개의 학교를 대상으로 한 메타분석(119개의 연구, 1450개의 효과)을 했습니다. 선생님이 소위 칼 로저스 방식의 사람/학습자 중심이었는가(&lt;b&gt;무조건적인 긍정적 존중, 공감, 비지시성&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등)하는 것은 학생의 학업성취와 태도(인지적, 정서적, 행동적 전반에서)에 미치는 효과크기가 0.72이었습니다. 특히 비지시성(non-directivity 즉 학생에게 이래라 저래라 하지 않는 것)의 효과크기는 0.75에 달했습니다. 참고로 교육학 연구에서 효과크기가 0.7을 넘는 것은 좀처럼 보기 힘든 &quot;큰 효과&quot;에 속합니다.&lt;br /&gt;&lt;br /&gt;이 글로리아 필름에서 일반인들에게 교훈이 있다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쓰다고 꼭 몸에 좋은 것은 아니라는 것&lt;/b&gt;, 또 그걸 통한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;폭력성을 정당화하는 것을 경계해야 한다&lt;/b&gt;는 메세지가 아닐까 싶습니다.&lt;br /&gt;&lt;br /&gt;[0]&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.centerfortheperson.org/papers/gloriaa-historical-note.php?fbclid=IwAR3XgBqCg5AbmKTViMpFlArLsmzu7ksrYNMAacdfiKorBze-O0IR9FJ_-P8&quot;&gt;https://www.centerfortheperson.org/papers/gloriaa-historical-note.php?fbclid=IwAR3XgBqCg5AbmKTViMpFlArLsmzu7ksrYNMAacdfiKorBze-O0IR9FJ_-P8&lt;/a&gt;&lt;br /&gt;[1] Dolliver, R. H. (1991). Perls With Gloria Re‐reviewed: Gestalt Techniques and Perl's Practices. Journal of counseling &amp;amp; development, 69(4), 299-304. 에서 재인용&lt;br /&gt;[2] Moreira, P., Gon&amp;ccedil;alves, &amp;Oacute;. F., &amp;amp; Matias, C. (2011). Clients' narratives in psychotherapy and therapist's theoretical orientation: An exploratory analysis of Gloria's narratives with Rogers, Ellis and Perls.&lt;br /&gt;[3] Cornelius-White, J. (2007). Learner-centered teacher-student relationships are effective: A meta-analysis. Review of educational research, 77(1), 113-143.&lt;br /&gt;&lt;br /&gt;p.s. 그렇다고 해서 솔직한 피드백을 해주지 마라, 좋은 게 좋은거다는 이야기는 아닙니다. 대신 이럴 경우 굉장히 스킬풀한 접근이 필요합니다. 몇 가지만 언급하면,&lt;br /&gt;&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;조언보다는 정보. 조언은 &quot;당신은 간 수치가 얼마다. 지금 술을 끊지 않으면 죽는다. 당장 내가 시키는 대로 하는 게 좋다.&quot; 정보는 &quot;당신의 간 수치는 인구에서 몇 퍼센타일에 해당한다. 이럴 경우 사망률은 어떻고, 그래서 위험군으로 보고 있다.&quot; 이럴 경우 통상 권하는 방법은 이런 게 있다. 조언보다 정보의 형태로 주어졌을 때 내담자가 행동을 수정할 확률이 높다.&lt;/li&gt;
&lt;li&gt;나를 낮춘다(self-discount). &quot;내가 당신의 상황을 잘 알지는 못하지만&quot;, &quot;내가 하는 말이 이상하게 들릴 수도 있는데&quot; 등으로 나의 권위를 떨어뜨리는 경우 상대가 행동을 바꿀 확률이 높다.&lt;/li&gt;
&lt;li&gt;자율권 인정. 결정권은 결국 상대에게 있다는 걸 인정하고 강조하는 경우 상대는 행동을 바꿀 가능성이 높아진다. 예컨대, &quot;결국 당신이 결정할 일이지만&quot; 등.&lt;/li&gt;
&lt;li&gt;하나보다는 여럿. 제안을 해야 할 경우 한가지보다는 동시에 여러가지를 제시하는 것이 상대의 자율성을 덜 침해한다.&lt;/li&gt;
&lt;li&gt;끌어내고 제공하고 끌어내기(Elicit-Provide-Elicit). 먼저 상대가 해당 사안에 대해 어떤 경험을 했는지, 이미 뭘 알고 있는지, 어떤 시도를 했는지 등을 물어서 끌어낸다. 그 다음에 내가 추가적 정보를 제공한다. 그 후에 상대가 그에 대해 어떻게 생각하는지 명시적으로 묻는다(&quot;제 얘기를 들으니까 어떠세요?&quot; 등).&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;그런데 이런 걸 잘하려면 훈련이 많이 필요합니다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>  Chitchat</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/480</guid>
      <comments>https://stir.tistory.com/480#entry480comment</comments>
      <pubDate>Mon, 18 Mar 2024 13:44:31 +0900</pubDate>
    </item>
    <item>
      <title>리틀의 법칙</title>
      <link>https://stir.tistory.com/479</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;리틀의 법칙&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리틀의 법칙은 어플리케이션을 설계할 때 확장성을 고려하기 위한 계산 방식으로도 쓰일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리틀의 법칙은 원래는 유입량과 확장에 대한 명시적인 해결책을 얻기 위해 널리 사용되는 법칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공식&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;152&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSEMv0/btsFSb0SvCw/iZBwR0xGfHPqEqf5epxW51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSEMv0/btsFSb0SvCw/iZBwR0xGfHPqEqf5epxW51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSEMv0/btsFSb0SvCw/iZBwR0xGfHPqEqf5epxW51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSEMv0%2FbtsFSb0SvCw%2FiZBwR0xGfHPqEqf5epxW51%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;152&quot; height=&quot;60&quot; data-origin-width=&quot;152&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;공간 내 머무는 객체 수(L) = 객체의 공간 유입량(&amp;lambda;) &amp;times; 객체의 공간 내 머무는 시간(W)&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mE9al/btsFRtAJMzP/toy7phdG8jwoztKq9jloC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mE9al/btsFRtAJMzP/toy7phdG8jwoztKq9jloC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mE9al/btsFRtAJMzP/toy7phdG8jwoztKq9jloC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmE9al%2FbtsFRtAJMzP%2Ftoy7phdG8jwoztKq9jloC1%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;296&quot; height=&quot;56&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;56&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;고객이 시간당 10명의 비율로 도착하고 평균 0.5시간 동안 머문다고 가정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 언제든지 매장에 있는 평균 고객 수가 5명이 되어야 함을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어플리케이션 설계에 접목 시키기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 간단한 공식을 통해 어플리케이션에 필요한 쓰레드를 계산해볼 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;동시에 처리할 수 있는 쓰레드(L) = 초당 클라이언트 요청량(&amp;lambda;) &amp;times; 평균 요청 처리 시간(W)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리는 시간 당이 아닌 초 당 유입량을 계산하는 것이 가장 바람직해보이니 그에 맞는 비율로 계산해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청량이 초당 100건이고 평균 요청 처리 시간이 50ms일 경우 아래와 같은 계산 결과를 도출할 수 있습니다.&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;동시에 처리할 수 있는 쓰레드(5) = 초당 클라이언트 요청량(100) &amp;times; 평균 요청 처리 시간(0.05)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Thread가 나오기 직전 Tomcat Thread 200개에 맞춰서 극단적으로 계산하면 아래와 같습니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;서버에서의 처리는 빠르고 클라이언트의 유입량이 많은 경우&lt;br /&gt;&lt;br /&gt;동시에 처리할 수 있는 쓰레드(200) = 초당 클라이언트 요청량(4000) &amp;times; 평균 요청 처리 시간(0.05)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;서버에서 처리가 느리고 클라이언트의 유입량이 적은 경우&lt;br /&gt;&lt;br /&gt;동시에 처리할 수 있는 쓰레드(200) = 초당 클라이언트 요청량(20) &amp;times; 평균 요청 처리 시간(5)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 요청 처리 시간이 5초만 되고 초당 20명씩 접근해도 하나의 어플리케이션이 일을 100% 하고 있다는 뜻이 됩니다.&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/479</guid>
      <comments>https://stir.tistory.com/479#entry479comment</comments>
      <pubDate>Mon, 18 Mar 2024 10:48:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링에서 가상 스레드를 이용한 부하테스트</title>
      <link>https://stir.tistory.com/478</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;a thousand hearings aren&amp;rsquo;t worth one seeing, and a thousand seeings aren&amp;rsquo;t worth one doing&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Property 설정&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1710492334522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.threads.virtual.enabled=true
server.tomcat.threads.max=1
server.tomcat.threads.min-spare=1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 JDK 21을 이용할 때 가상 스레드를 활용하기 위해서는 spring.threads.virtual.enabled=true를 활성화해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하테스트를 위해 Thread는 1개로 유지합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드 작성&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1710492352150&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/test&quot;)
public String test() throws InterruptedException {
  Thread.sleep(50);
  return &quot;test&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히&amp;nbsp;위와&amp;nbsp;같은&amp;nbsp;Controller&amp;nbsp;Method&amp;nbsp;하나를&amp;nbsp;작성해보고&amp;nbsp;Gatling에&amp;nbsp;Scala&amp;nbsp;코드로&amp;nbsp;동시&amp;nbsp;1000번&amp;nbsp;요청을&amp;nbsp;진행하는&amp;nbsp;방식으로&amp;nbsp;작성한&amp;nbsp;뒤&amp;nbsp;부하&amp;nbsp;테스트를&amp;nbsp;진행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710492401662&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package test
import scala.concurrent.duration._
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._

class RecordedSimulation extends Simulation {

	val httpProtocol = http(&quot;testSimaulation&quot;)
		.get(&quot;http://127.0.0.1:8080/test&quot;)
    	.header(&quot;Client-Version&quot;, &quot;1&quot;)
    
	val scn = scenario(&quot;Scenario1&quot;)
		.exec(httpProtocol)
    
    setUp( 
        scn.inject(atOnceUsers(1000)) 
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부하 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기존 Tomcat Thread를 활용한 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcXrGp/btsFQWoX32n/kJVI7TD5zoE9KBrWiAbf60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcXrGp/btsFQWoX32n/kJVI7TD5zoE9KBrWiAbf60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcXrGp/btsFQWoX32n/kJVI7TD5zoE9KBrWiAbf60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcXrGp%2FbtsFQWoX32n%2FkJVI7TD5zoE9KBrWiAbf60%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;758&quot; height=&quot;356&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;356&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Virtual Thread를 활용한 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lqA8X/btsFN5BgFhR/xhsrO0gK7WD3eqS0FgDuw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lqA8X/btsFN5BgFhR/xhsrO0gK7WD3eqS0FgDuw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lqA8X/btsFN5BgFhR/xhsrO0gK7WD3eqS0FgDuw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlqA8X%2FbtsFN5BgFhR%2FxhsrO0gK7WD3eqS0FgDuw1%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;757&quot; height=&quot;406&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존&amp;nbsp;MVC&amp;nbsp;작업은&amp;nbsp;Tomcat&amp;nbsp;Thread&amp;nbsp;1개가&amp;nbsp;Sleep에&amp;nbsp;들어감에&amp;nbsp;따라&amp;nbsp;Sleep이&amp;nbsp;끝나야만&amp;nbsp;그&amp;nbsp;다음&amp;nbsp;작업이&amp;nbsp;처리&amp;nbsp;되므로&amp;nbsp;1000번째&amp;nbsp;요청인&amp;nbsp;하위&amp;nbsp;99퍼센트&amp;nbsp;요청은&amp;nbsp;55초째에&amp;nbsp;마무리된&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Thread&amp;nbsp;Sleep을&amp;nbsp;50ms로&amp;nbsp;줬으니&amp;nbsp;50ms&amp;nbsp;*&amp;nbsp;1000&amp;nbsp;=&amp;nbsp;50000ms)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;Virtual&amp;nbsp;Thread를&amp;nbsp;사용한&amp;nbsp;방식은&amp;nbsp;하위&amp;nbsp;99퍼센트의&amp;nbsp;요청도&amp;nbsp;0.4초안에&amp;nbsp;끝나는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜&amp;nbsp;이런걸까?&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상&amp;nbsp;스레드&amp;nbsp;개념은&amp;nbsp;JVM에서&amp;nbsp;사용하는&amp;nbsp;Thread에&amp;nbsp;여러개의&amp;nbsp;가상&amp;nbsp;스레드를&amp;nbsp;붙인&amp;nbsp;뒤&amp;nbsp;kernel에&amp;nbsp;넘겨서&amp;nbsp;사용하는&amp;nbsp;개념입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로&amp;nbsp;가상&amp;nbsp;스레드는&amp;nbsp;OS&amp;nbsp;Thread&amp;nbsp;내에서&amp;nbsp;실행됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;가상&amp;nbsp;스레드에서&amp;nbsp;실행되는&amp;nbsp;코드가&amp;nbsp;블로킹&amp;nbsp;I/O&amp;nbsp;작업을&amp;nbsp;호출하면&amp;nbsp;자바&amp;nbsp;런타임은&amp;nbsp;다시&amp;nbsp;시작될&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;때까지&amp;nbsp;가상&amp;nbsp;스레드를&amp;nbsp;일시&amp;nbsp;중지합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼&amp;nbsp;OS&amp;nbsp;Thread와&amp;nbsp;연결&amp;nbsp;됐었던&amp;nbsp;가상&amp;nbsp;스레드는&amp;nbsp;중지&amp;nbsp;상태에&amp;nbsp;돌아가지만&amp;nbsp;OS&amp;nbsp;Thread는&amp;nbsp;다른&amp;nbsp;작업을&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;구조라서&amp;nbsp;위와&amp;nbsp;같은&amp;nbsp;결과를&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다고&amp;nbsp;보면&amp;nbsp;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Spring</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/478</guid>
      <comments>https://stir.tistory.com/478#entry478comment</comments>
      <pubDate>Fri, 15 Mar 2024 17:47:21 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Apache Commons Library에서의 올바른 압축 해제 방법</title>
      <link>https://stir.tistory.com/477</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최근 사내 프로젝트에서 압축 파일 해제 코드가 올바르게 처리 되고 있지 않아 컨플루언스에 올린 내용을 블로그에도 올려보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;126&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특정 압축 파일을 업로드하여 자바 코드 내에서 압축 해제하는 경우 코드 동작이 정상적으로 진행되지 않는 현상&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;130&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;압축 대상 파일&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;zip, tar, tar.gz&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;What-type-is-Compressed-File?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;450&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;압축 파일의 실제 포맷은?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1710476473940&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;file resource.tar.gz&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;리눅스 계열에서 file 명령어를 통해 파일이 어떤 포맷인지 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;file에 대한 포맷을 확인하는 이유는 가령 사용자가 zip으로 압축 한 뒤에 임의로 확장자를 .tar.gz로 바꾸는 경우가 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;물론 이 경우에도 압축 파일로서의 동작은 잘 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 문제는 이 파일의 실체는 zip이다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;압축된 zip 파일을 .tar.gz로 강제로 바꾸는 경우 위의 명령어를 통해서&amp;nbsp; Zip archive data라는 Type으로 출력 된다. &amp;rarr; 실체는 .zip이라는 뜻&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;629&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그에 반해 tar 명령어로 묶은 tar.gz 파일은 POSIX tar archive(표준 약관을 따르는 tar 압축 파일)라는 Type으로 출력 된다. &amp;rarr; 실체는 .tar 파일이라는 뜻&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;해결-방법&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1950&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 코드에서 압축 파일의 포맷을 확인하는 방법(Magic Number)&lt;/b&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1710476769051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Magic numbers for files&quot; data-og-description=&quot;&quot; data-og-host=&quot;web.archive.org&quot; data-og-source-url=&quot;https://web.archive.org/web/20161113173723/http://www.astro.keele.ac.uk/oldusers/rno/Computing/File_magic.html&quot; data-og-url=&quot;https://web.archive.org/web/20161113173723/http://www.astro.keele.ac.uk/oldusers/rno/Computing/File_magic.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://web.archive.org/web/20161113173723/http://www.astro.keele.ac.uk/oldusers/rno/Computing/File_magic.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://web.archive.org/web/20161113173723/http://www.astro.keele.ac.uk/oldusers/rno/Computing/File_magic.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Magic numbers for files&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;web.archive.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1961&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 압축 파일이 어떤 포맷을 가지고 있는지 알았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1961&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 Apache Commons Library를 통해 해당 압축 파일의 유형에 맞게 압축을 해제해 줄 필요가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1961&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;우선 압축 파일이 어떤 유형인지 알기 위해서는 파일에 대한 매직 넘버를 확인해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1961&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특정 압축 파일에 따라 InputStream 내에 시작 되는 16진수가 다르므로 시작되는 16진수 문자를 파악해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-layout=&quot;custom&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 128px;&quot; border=&quot;1&quot; data-table-width=&quot;760&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;압축 형태&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;확장자&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;16진수&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;비고&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;pkzip format&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.zip&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;50 4b 03 04&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;tar (POSIX)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.tar&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;75 73 74 61 72 (start from 257 byte)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;257 byte 위치부터 ustar라는 글자를 가지고 있음.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;gzip format&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;.gz&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1f 8b&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 36px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;.tar.gz의 이중성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위에서 한번 설명했던 것을 포함해서 다시 한번 .tar.gz에 대한 특징을 설명할 필요가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고로 tar는 압축이 아닌 그냥 파일에 대한 묶음이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1. tar 명령어를 통해 압축해서 만든 .tar.gz&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1710476989464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tar -cvf resource1.tar.gz resource&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이런 경우에는 압축 유형이 tar (POSIX)로 분류되므로 자바에서 TarArchiveInputStream 클래스로 압축 해제 해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;2. 윈도우에서 zip 파일을 .tar.gz로 만든 경우&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이런 경우에는 실제 압축 유형은 zip이다. 그러므로 자바에서 매직 넘버를 확인 한 후에 ZipArchiveInputStream 클래스로 압축 해제 해주면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;3. 실제로 .tar를 .gz로 2번 압축 한 경우&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실제로 이런 경우의 .tar.gz는 별개의 특정 매직 넘버로 분류되지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그냥 마지막에 압축한 .gz에 대한 매직 넘버만이 확인되기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그러므로 마지막 gzip format에 대한 16진수 코드만 파악할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 유형은 GzipCompressorInputStream -&amp;gt; TarArchiveInputStream 순서의 클래스로 압축 해제 해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 구현 코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1710477418565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static InputStream getArchiveStream(InputStream inputStream) throws IOException {
    ArchiveInputStream tar = null;
    inputStream.mark(Integer.MAX_VALUE);
    try {
      tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream));
    } catch (IOException e) {
    }
    inputStream.reset(); // after the inputStream has been consumed once, it needs to be reset.
    CompressedType compressedType = retrieveCompressedTypeFromStream(inputStream);

    // converting from .zip should take precedence because while TAR can be converted to ZIP, the reverse is not possible.
    if (compressedType == CompressedType.PKZIP &amp;amp;&amp;amp; tar == null) {
      tar = new ZipArchiveInputStream(inputStream);
    }
    if (compressedType == CompressedType.TAR &amp;amp;&amp;amp; tar == null) {
      tar = new TarArchiveInputStream(inputStream);
    }
    if (tar == null) {
      throw new CommonExceptions.BadRequestException(&quot;Unsupported compression file format&quot;);
    }
    return tar;
}

public static CompressedType retrieveCompressedTypeFromStream(InputStream input) {
    try {
      CompressedType compressedType = null;
      input.mark(Integer.MAX_VALUE);
      byte[] bytes = new byte[300];
      input.read(bytes);
      if (bytes[0] == 0x50 &amp;amp;&amp;amp; bytes[1] == 0x4b &amp;amp;&amp;amp; bytes[2] == 0x03 &amp;amp;&amp;amp; bytes[3] == 0x04) {
        compressedType = CompressedType.PKZIP;
      } else if (bytes[257] == 0x75 &amp;amp;&amp;amp; bytes[258] == 0x73 &amp;amp;&amp;amp; bytes[259] == 0x74 &amp;amp;&amp;amp; bytes[260] == 0x61 &amp;amp;&amp;amp; bytes[261] == 0x72) { // ustar
        compressedType = CompressedType.TAR;
      } else if (bytes[0] == 0x1f &amp;amp;&amp;amp; bytes[1] == 0x8b) {
        compressedType = CompressedType.GZIP;
      }
      input.reset();
      return compressedType;
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2697&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2749&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;try문 안에는 실제로 .tar와 .gz로 2번 압축한 경우에 대한 압축 해제 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2749&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이런 경우엔 매직 넘버를 파악하는 방법이 없기 때문에 예외 처리를 통해 바로 패스시키는 방법을 선택했다.(분명 더 좋은 방법이 있을 것은 확실하다. 일단 비즈니스 로직이 아니기 때문에 패스.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2749&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 외는 매직 넘버가 파악이 되기 때문에 CompressedType을 구하는 메소드를 통해 포맷을 확인하고 압축 해제 해주면 된다.&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>☕ Java</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/477</guid>
      <comments>https://stir.tistory.com/477#entry477comment</comments>
      <pubDate>Fri, 15 Mar 2024 13:39:01 +0900</pubDate>
    </item>
    <item>
      <title>ElasticSearch 8 Windows 설치</title>
      <link>https://stir.tistory.com/471</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;전부터 윈도우 PC에 장난감처럼 놀려고 설치하다 종종 막혔었는데 윈도우 설치는 ES 8 버전이후로는 검색해도 잘 안돼서 내가 직접 포스팅하겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1708527665390&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Download Elasticsearch&quot; data-og-description=&quot;Download Elasticsearch or the complete Elastic Stack (formerly ELK stack) for free and start searching and analyzing in minutes with Elastic....&quot; data-og-host=&quot;www.elastic.co&quot; data-og-source-url=&quot;https://www.elastic.co/kr/downloads/elasticsearch&quot; data-og-url=&quot;https://www.elastic.co/kr/downloads/elasticsearch&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfVjSD/hyVmWnOrnX/CW07XHLNFTciF28xmf9dRK/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/cE6r29/hyVm3f9Vpm/xyIiqYEAl4wroV1ESq1dzk/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/kTyX5/hyVm4e733Z/4Nb4okWOCjHLnRkZfVwGA0/img.png?width=1441&amp;amp;height=841&amp;amp;face=0_0_1441_841&quot;&gt;&lt;a href=&quot;https://www.elastic.co/kr/downloads/elasticsearch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.elastic.co/kr/downloads/elasticsearch&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfVjSD/hyVmWnOrnX/CW07XHLNFTciF28xmf9dRK/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/cE6r29/hyVm3f9Vpm/xyIiqYEAl4wroV1ESq1dzk/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/kTyX5/hyVm4e733Z/4Nb4okWOCjHLnRkZfVwGA0/img.png?width=1441&amp;amp;height=841&amp;amp;face=0_0_1441_841');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Download Elasticsearch&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Download Elasticsearch or the complete Elastic Stack (formerly ELK stack) for free and start searching and analyzing in minutes with Elastic....&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.elastic.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실행 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 해제 후에 bin 폴더를 들어가면 각종 bat 확장자를 가진 파일들이 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;109&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkG2E0/btsFcjZwXQI/ys6cqCiWikAMgaKZ8KDQ6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkG2E0/btsFcjZwXQI/ys6cqCiWikAMgaKZ8KDQ6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkG2E0/btsFcjZwXQI/ys6cqCiWikAMgaKZ8KDQ6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkG2E0%2FbtsFcjZwXQI%2Fys6cqCiWikAMgaKZ8KDQ6k%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;109&quot; height=&quot;37&quot; data-origin-width=&quot;109&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bin폴더에서 cmd를 입력해서 명령 프롬프트를 띄워보자.(나름 꿀팁)&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;elasticsearch 를 입력하면 elasticsearch.bat 파일이 실행되면서 ES 서버가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수 초후에 실행이 완료되고 나면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https://localhost:9200으로 접속한다.(http아님)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjR9FH/btsE7uIylaX/sDqvxnczVYBIglIFDCSn6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjR9FH/btsE7uIylaX/sDqvxnczVYBIglIFDCSn6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjR9FH/btsE7uIylaX/sDqvxnczVYBIglIFDCSn6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjR9FH%2FbtsE7uIylaX%2FsDqvxnczVYBIglIFDCSn6K%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;309&quot; height=&quot;227&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기서 제일 막혔던 부분인데 다른 글들에서는 초기 사용자 이름과 비밀번호가 표시된다라든가.. 모르면 비밀번호 초기화를 하라든가 그런 내용만 있어서 제대로 알기 어려웠다.&lt;/p&gt;
&lt;pre id=&quot;code_1708527898917&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;elasticsearch-users useradd stir084 -p stir084 -r superuser&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 명령 프롬프트의 위의 명령어를 넣어서 user를 하나 추가해서 로그인해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1708528902655&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;elasticsearch-users roles stir084 -a superuser&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정은 있는 경우 역할만 추가해줄 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1708528854454&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;elasticsearch-users list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 명령어를 통해 계정이 있는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stir084/stir084로 접속해보자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rSOLi/btsE94Wz99v/LkvEfYXxdOF2aJgpK5eS90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rSOLi/btsE94Wz99v/LkvEfYXxdOF2aJgpK5eS90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rSOLi/btsE94Wz99v/LkvEfYXxdOF2aJgpK5eS90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrSOLi%2FbtsE94Wz99v%2FLkvEfYXxdOF2aJgpK5eS90%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;587&quot; height=&quot;425&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실습&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Kibana 혹은 curl로 다룰 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kibana는 귀찮으니 curl로 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1708528981347&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -u stir084:stir084 https://localhost:9200 -k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 https를 이용해야하므로 user정보를 입력해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인덱스 추가&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1708529110601&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -XPUT -u stir084:stir084 &quot;https://localhost:9200/my-index&quot; -k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은&lt;/p&gt;
&lt;pre id=&quot;code_1708529164768&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -XPUT -u stir084:stir084 &quot;https://localhost:9200/my-index&quot; -k -d '{
  &quot;settings&quot;: {
    &quot;number_of_shards&quot;: 1,
    &quot;number_of_replicas&quot;: 0
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;title&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;content&quot;: {
        &quot;type&quot;: &quot;text&quot;
      }
    }
  }
}'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스 조회&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1708529118957&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -XGET -u stir084:stir084 &quot;https://localhost:9200/_cat/indices?v&quot; -k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은&lt;/p&gt;
&lt;pre id=&quot;code_1708529179009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -XGET -u stir084:stir084 &quot;https://localhost:9200/my-index/_search?q=title:myTitle&quot; -k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.elastic.co/kr/downloads/kibana&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.elastic.co/kr/downloads/kibana&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708529636569&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Download Kibana Free | Get Started Now&quot; data-og-description=&quot;Download Kibana or the complete Elastic Stack (formerly ELK stack) for free and start visualizing, analyzing, and exploring your data with Elastic in minutes....&quot; data-og-host=&quot;www.elastic.co&quot; data-og-source-url=&quot;https://www.elastic.co/kr/downloads/kibana&quot; data-og-url=&quot;https://www.elastic.co/kr/downloads/kibana&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bblEmu/hyVm3HfBZa/4F92QSxJqEGN8FwkQAMKuk/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/U6QtC/hyVm4TMa0O/1FrRLLpVuyAtaNvT0X4pl0/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/cj0c8S/hyVmTEDjOM/qBm1TSL2ZbVKAIR5XHY29K/img.jpg?width=720&amp;amp;height=420&amp;amp;face=0_0_720_420&quot;&gt;&lt;a href=&quot;https://www.elastic.co/kr/downloads/kibana&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.elastic.co/kr/downloads/kibana&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bblEmu/hyVm3HfBZa/4F92QSxJqEGN8FwkQAMKuk/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/U6QtC/hyVm4TMa0O/1FrRLLpVuyAtaNvT0X4pl0/img.png?width=1600&amp;amp;height=837&amp;amp;face=0_0_1600_837,https://scrap.kakaocdn.net/dn/cj0c8S/hyVmTEDjOM/qBm1TSL2ZbVKAIR5XHY29K/img.jpg?width=720&amp;amp;height=420&amp;amp;face=0_0_720_420');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Download Kibana Free | Get Started Now&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Download Kibana or the complete Elastic Stack (formerly ELK stack) for free and start visualizing, analyzing, and exploring your data with Elastic in minutes....&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.elastic.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 설치후에 bin 폴더 내에 kibana.bat 실행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psLCw/btsFbT7Y1qc/6vvAKJOx5EDEfUxNa21kAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psLCw/btsFbT7Y1qc/6vvAKJOx5EDEfUxNa21kAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psLCw/btsFbT7Y1qc/6vvAKJOx5EDEfUxNa21kAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsLCw%2FbtsFbT7Y1qc%2F6vvAKJOx5EDEfUxNa21kAK%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;641&quot; height=&quot;583&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;583&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;ES에서 token을 발행해서 넣어줘야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1708533152033&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;elasticsearch-create-enrollment-token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령 프롬프트에 위와 같이 입력후 토큰을 넣어준다.&lt;/p&gt;</description>
      <category>  Database</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/471</guid>
      <comments>https://stir.tistory.com/471#entry471comment</comments>
      <pubDate>Thu, 22 Feb 2024 00:26:45 +0900</pubDate>
    </item>
    <item>
      <title>싯다르타 - 헤르만 헤세</title>
      <link>https://stir.tistory.com/466</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 어려운 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽다 보면 자연스럽게 싯다르타가 추구하는 진리를 나도 알고자 하는 열망이 가득해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 얻을 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 느낀 부분은 싯다르타가 외부 세계의 자극으로부터 얻는 모든 것들을 윤회라고 받아들였던 과정이 나에게는 마음의 편안함을 가져다주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재의 내 모습과 가장 많이 닮아있던 부분이었기 때문이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;윤회 - 해탈의 경지에 도달하지 못한 사람은 그 깨달음, 경지 또는 구원된 상태에 도달할 때까지 계속하여 이 세상으로 재탄생한다는 내용의 교리이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싯다르타는 윤회를 하며 계속해서 다시 태어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 윤회가 나쁜 것이라고 인식되진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 태어나서 얻는 지혜가 있었기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;속세에서 고통스러워하고 있는 것도 자신이고 행복해하는 것도 자신이고 사랑하는 것도 자신이며 그것은 곧 윤회다.&lt;/li&gt;
&lt;li&gt;죽음은 생명의 단절이 아닌 윤회다.&lt;/li&gt;
&lt;li&gt;강은 과거나 미래가 아닌 현재에만 존재하며 시간을 초월하며 윤회를 알려준다.&lt;/li&gt;
&lt;li&gt;아트만(개인의 내재)과 브라흐만(우주의 내재)은 본질적으로 같다(범아일여)&lt;/li&gt;
&lt;li&gt;모든 것은 하나의 단일성으로 연결된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 문장은 올바르게 이해하기 힘들지만 누군가는 책을 읽고 외부에서의 성공보단 내부의 마음을 집중하며 살아야 된다라고 느낄 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 느꼈던 것은 외부 세계와 상호 작용을 하며 기뻐하고 슬퍼하는 것조차도 내가 곧 우주, 우주가 곧 나이기 때문에 지극히 자연스러운 현상이고 이러한 삶들은 무상하며 허무한 것이 아니라고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 현재를 살아가는 삶이 고통이든 기쁨이든, 강한 욕망을 느끼는 무엇이든 지극히 자연스러운 것이고 이해할 수 있는 것이며 언젠가는 윤회를 통해 하나의 지혜로 자리잡을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 그것을 하나로 이해해 삼라만상을 느끼며 모든 것이 가치가 없는 것이 없다는 것을 느꼈을 때 그 과정에서 무상함과 허무함을 느낀다 할지라도 허무주의에 빠지지 않고 낙관적으로 즐겁게 살지 않을 이유가 없음을 느끼는 것이 현재로서 가장 충실해야 하는 것 아닌가 싶다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;그것은 자기 자신에게는 없는데 그들은 가지고 있는 한 가지, 즉 그들은 자신들의 삶에 중요성을 부여할 줄 안다는 사실 때문이었다. 그들은 기쁨도 불안함도 열렬하게 드러낼 줄 알았으며 영원한 열애의 불안하지만 달콤한 행복을 맛볼 줄 알았다.&quot;&lt;br /&gt;- 113p&amp;nbsp;&lt;/blockquote&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;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=oBIo2AyjNMo&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/YV8iR/hyVjk2XDNv/K6rdCBA1EIWcK9j0fI1Xgk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/oBIo2AyjNMo&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  문학</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/466</guid>
      <comments>https://stir.tistory.com/466#entry466comment</comments>
      <pubDate>Wed, 14 Feb 2024 02:12:14 +0900</pubDate>
    </item>
    <item>
      <title>인텔리제이 업데이트로 느낀 엔지니어의 가치관</title>
      <link>https://stir.tistory.com/462</link>
      <description>&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;이번에 회사에서 개발 도중에 재밌는 일이 생겨서 공유해보고자 써봅니다.&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제의 원인과 해결&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coY78L/btsEKGgg0Kb/7q5VbLrZ5HBdzCYwXCRaeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coY78L/btsEKGgg0Kb/7q5VbLrZ5HBdzCYwXCRaeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coY78L/btsEKGgg0Kb/7q5VbLrZ5HBdzCYwXCRaeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoY78L%2FbtsEKGgg0Kb%2F7q5VbLrZ5HBdzCYwXCRaeK%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;1280&quot; height=&quot;316&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 제품 소스를 보다가 위와 같은 경고를 보았는데요.(경고에 대한 내용을 해석하고자 하는 글은 아니니 이 부분은 넘기기로 하고 기술적인 내용은 빼겠습니다.) &lt;br /&gt;사실 위 소스가 정상적인 소스인 것을 알고 있었습니다. 제 블로그에도 위 케이스는 정상 케이스라고 적어놓기도 했구요.&lt;br /&gt;근데 저는 단지 길고 긴 @Service 코드에 저 부분만 Warning이 발생하는 것이 그다지 보기 좋지 않아 Warning이 안나오도록 개선을 했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pirvate -&amp;gt; @Transactional public&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 개선 아닌 개선을 했습니다. &lt;br /&gt;(개선이 아닌 이유는 private을 어거지로 public으로 만든 것이고 개선이라고 표현한 이유는 단순히 Warning을 없앤 것을 뜻합니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드 리뷰&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;대망의 코드리뷰 시간이 왔는데요. &lt;br /&gt;팀원분께서 이런 피드백을 주셨습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clVDwo/btsEKDRn0Y6/DDJokigiFwEPEKJ0fsYrhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clVDwo/btsEKDRn0Y6/DDJokigiFwEPEKJ0fsYrhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clVDwo/btsEKDRn0Y6/DDJokigiFwEPEKJ0fsYrhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclVDwo%2FbtsEKDRn0Y6%2FDDJokigiFwEPEKJ0fsYrhk%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;241&quot; height=&quot;37&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 피드백을 받고 서로의 PC를 직접 가서 눈으로 확인했습니다.&lt;br /&gt;당연하게도 &quot;어 이거 왜 뜨지?&quot;, &quot;어 이거 왜 안뜨지?&quot;가 오고가는 현장이었습니다.&lt;br /&gt;그래서 서로의 설정 쪽 Inspection이 잘못됐나 우선 살펴봤는데 아니었구요.&lt;br /&gt;&lt;br /&gt;알고보니 서로의 버전이 달랐는데요.&lt;br /&gt;혹시나 해서 인텔리제이 업데이트로 인해 버그가 생긴건가 했습니다.&lt;br /&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issue/IDEA-319210&quot;&gt;https://youtrack.jetbrains.com/issue/IDEA-319210&lt;/a&gt;&lt;br /&gt;관련해서 실제로 인텔리제이 Issue를 찾아보니 지금으로부터 약 7개월전에 이거 정상인데 왜 경고가 뜨냐는 Issue가 제기되었고 2023년 8월 2023.2 버전에서 위 Warning이 '개선'되었다는 것을 알게되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 결국에 제가 인텔리제이 업데이트를 하고 저 역시 Warning이 발생하지 않아 개선한 소스(이젠 개선한 소스가 아니라 Regression된 소스겠군요.)는 Rollback을 했습니다.&lt;br /&gt;여기서 곰곰이 생각해보니 제 자신에 대해 아쉬워지는 점이 많아지더군요.&lt;br /&gt;&lt;br /&gt;첫번째로는 IDE를 너무 신뢰했다는 점 입니다. &lt;br /&gt;개발 세계에서는 완벽한 기술이 없기에 나조차도 의심하고 툴도 의심을 했어야 했는데 그러지 못했다는 것 입니다. &lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;심지어 기존 소스가 잘 돌아갈 것이라는 추측을 했는데도&lt;/b&gt;&lt;/span&gt; 속으로 &quot;너가 Warning을 띄워주면 너가 맞겠지.&quot;라고 생각했거든요. &lt;br /&gt;&lt;br /&gt;두번째로는 기회가 있었기에 저도 인텔리제이 컨트리뷰터가 될 수 있었는데, 누군가는 이슈를 제기했고 누군가는 소스를 어거지로 수정했다는 것이 조금은 과장해서 진 느낌이었습니다.(&lt;s&gt;난 나를 안 믿고 IDE를 믿고, 넌 너를 믿고 IDE를 안 믿고..&lt;/s&gt;)&lt;br /&gt;&lt;br /&gt;그래서 느낀 점은 의심하고 안주하지 않는 엔지니어가 되자라고 다시 한번 되새김질하게 된 계기였습니다.(&lt;s&gt;아니 근데 인텔리하다면서..&lt;/s&gt;)&lt;/p&gt;</description>
      <category> ️ Etc</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/462</guid>
      <comments>https://stir.tistory.com/462#entry462comment</comments>
      <pubDate>Mon, 12 Feb 2024 00:10:09 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Companion Object</title>
      <link>https://stir.tistory.com/460</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 없이 정의 할 수 있는 객체다.&lt;/li&gt;
&lt;li&gt;싱글톤 역할을 한다.&lt;/li&gt;
&lt;li&gt;클래스 내부에 정의하지만 클래스 인스턴스와 별개로 존재한다.&lt;/li&gt;
&lt;li&gt;정적으로 접근이 가능하다.(상속 불가능)&lt;/li&gt;
&lt;li&gt;공통 메서드인 Util Class를 대체할 수 있다.&lt;/li&gt;
&lt;li&gt;공통 상수인 Enum 혹은 static을 관리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1706838231724&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyClass {
    companion object {
        fun createInstance(name: String): MyClass {
            // 인스턴스 생성 로직
            return MyClass(name)
        }

        const val VERSION = &quot;1.0.0&quot;
    }

    private val name: String

    constructor(name: String) {
        this.name = name
    }

    fun getName(): String {
        return name
    }
}

// Companion object 사용 예시
val instance = MyClass.createInstance(&quot;My Instance&quot;)
val version = MyClass.VERSION
println(instance.getName())
println(version)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>☕ Java</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/460</guid>
      <comments>https://stir.tistory.com/460#entry460comment</comments>
      <pubDate>Fri, 2 Feb 2024 10:44:09 +0900</pubDate>
    </item>
    <item>
      <title>개인 PC에 Kubernetes 환경 설치</title>
      <link>https://stir.tistory.com/456</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;운영 체제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정성있는 ubuntu 20.04 설치(아 설치할 때 영어로 설치할걸... ui가 한글이 될거같아서 그런건데 너무 불편하네)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 고정 IP 설정(마스터 노드만 진행함)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ubuntu&amp;nbsp;20.04&amp;nbsp;고정&amp;nbsp;IP(Static&amp;nbsp;IP)&amp;nbsp;설정 &lt;br /&gt;ctrl&amp;nbsp;alt&amp;nbsp;t로&amp;nbsp;터미널&amp;nbsp;실행 &lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;install&amp;nbsp;net-tools &lt;br /&gt;ifconfig&amp;nbsp;|&amp;nbsp;grep&amp;nbsp;inet&amp;nbsp;현재&amp;nbsp;IP&amp;nbsp;확인&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;192.168.219.103&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;route&amp;nbsp;-n&amp;nbsp;게이트웨이&amp;nbsp;ip&amp;nbsp;확인&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;192.168.219.1&lt;/span&gt;&lt;/b&gt;&amp;nbsp;게이트웨이가&amp;nbsp;연결된&amp;nbsp;NIC&amp;nbsp;정보도&amp;nbsp;확인&amp;nbsp;가능&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;enp1s0&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법1 이든 2든 무조건 설정해줘야하는 부분은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 IP 주소, 서브넷마스크, 게이트웨이 주소, 통신사 Nameserver다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 3개는 명령어로 조회를 해봤는데 NameServer는 Ubuntu Desktop에 설정에 들어갔을 때 현재 네트워크가 DHCP로 설정된 경우에 Nameserver IP가 조회돼서 해당 NameServer IP를 고정 IP 입력할 때 넣어주면 된다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(또 내부 아이피를 192.168.219.110으로 했는데 안됐음 103으로 그냥 유지하니까 됨)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 1. UI로 고정 IP 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우분투 데스크탑에서 설정 -&amp;gt; 네트워크 -&amp;gt; Ipv4 들어가서 설정하면 됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1419&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsCyMP/btsC32NQhfL/V9FeGogdkQWJDvd4hrJjv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsCyMP/btsC32NQhfL/V9FeGogdkQWJDvd4hrJjv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsCyMP/btsC32NQhfL/V9FeGogdkQWJDvd4hrJjv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsCyMP%2FbtsC32NQhfL%2FV9FeGogdkQWJDvd4hrJjv0%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;1419&quot; height=&quot;1338&quot; data-origin-width=&quot;1419&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 2. 설정 파일 수정으로 고정 IP 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://enowy.tistory.com/33&quot;&gt;https://enowy.tistory.com/33&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 위치에 파일을 열고 주석 후 아래 내용 추가&lt;br /&gt;/etc/netplan/00-installer-config.yaml&amp;nbsp; &lt;br /&gt;&lt;br /&gt;network: &lt;br /&gt;&amp;nbsp;&amp;nbsp;ethernets: &lt;br /&gt;&amp;nbsp; &amp;nbsp; enp1s0:&amp;nbsp;&amp;nbsp;# !!!!!!!!!!!!확인한 NIC 정보!!!!!!!!!!!!!!!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;addresses: [192.168.219.103/24]&amp;nbsp;&amp;nbsp;# IP 주소는 CIDR 표기법으로 기재한다. 잘 모르면 끝에 '/24' 기입한다. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;gateway4: 192.168.219.101&amp;nbsp; # 확인한 Gateway 정보 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;nameservers: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;addresses: [61.41.153.2,1.214.68.2] # Name server 주소를 쉼표로 구분하여 원하는 순서로 기재한다. &lt;br /&gt;&amp;nbsp;&amp;nbsp;version:&amp;nbsp;2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;sudo&amp;nbsp;systemctl&amp;nbsp;start&amp;nbsp;systemd-networkd&lt;br /&gt;sudo systemctl enable systemd-networkd &lt;br /&gt;sudo&amp;nbsp;netplan&amp;nbsp;apply&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSH 설치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;update &lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;install&amp;nbsp;openssh-server &lt;br /&gt;sudo&amp;nbsp;systemctl&amp;nbsp;enable&amp;nbsp;ssh &lt;br /&gt;sudo systemctl start ssh&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;vi및 vim 설치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt update&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt install vim&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;이거 안하니까 vi 돌리면 화살표 기능이 이상하게 작동함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;도커 설치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;update &lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;install&amp;nbsp;apt-transport-https&amp;nbsp;ca-certificates&amp;nbsp;curl&amp;nbsp;gnupg-agent&amp;nbsp;software-properties-common &lt;br /&gt;curl&amp;nbsp;-fsSL&amp;nbsp;&lt;a href=&quot;https://download.docker.com/linux/ubuntu/gpg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://download.docker.com/linux/ubuntu/gpg&lt;/a&gt;&amp;nbsp;|&amp;nbsp;sudo&amp;nbsp;apt-key&amp;nbsp;add&amp;nbsp;- &lt;br /&gt;sudo&amp;nbsp;add-apt-repository&amp;nbsp;&quot;deb&amp;nbsp;[arch=amd64]&amp;nbsp;&lt;a href=&quot;https://download.docker.com/linux/ubuntu&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://download.docker.com/linux/ubuntu&lt;/a&gt;&amp;nbsp;$(lsb_release&amp;nbsp;-cs)&amp;nbsp;stable&quot; &lt;br /&gt;&lt;br /&gt;sudo&amp;nbsp;apt&amp;nbsp;install&amp;nbsp;docker-ce&amp;nbsp;docker-ce-cli&amp;nbsp;containerd.io &lt;br /&gt;sudo&amp;nbsp;docker&amp;nbsp;version &lt;br /&gt;sudo&amp;nbsp;systemctl&amp;nbsp;enable&amp;nbsp;docker &lt;br /&gt;sudo&amp;nbsp;systemctl&amp;nbsp;start&amp;nbsp;docker &lt;br /&gt;sudo&amp;nbsp;systemctl&amp;nbsp;status&amp;nbsp;docker&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;쿠버네티스 설치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://confluence.curvc.com/pages/viewpage.action?pageId=98048155&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://confluence.curvc.com/pages/viewpage.action?pageId=98048155&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;각&amp;nbsp;노드&amp;nbsp;마다&amp;nbsp;실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;sudo&amp;nbsp;swapoff&amp;nbsp;-a&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;sudo&amp;nbsp;sed&amp;nbsp;-i&amp;nbsp;'/swap/s/^/#/'&amp;nbsp;/etc/fstab &lt;br /&gt;&lt;br /&gt;hostnamectl&amp;nbsp;set-hostname&amp;nbsp;k8s-master&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;참고&lt;br /&gt;$ hostnamectl set-hostname k8s-node01 &lt;br /&gt;$ kubeadm rejoin&lt;br /&gt;이렇게 하면 기존 노드 이름 변경도 가능&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(전체 복사해서 붙여 넣기만 하면됨)&lt;br /&gt;cat&amp;nbsp;&amp;lt;&amp;lt;EOF&amp;nbsp;|&amp;nbsp;sudo&amp;nbsp;tee&amp;nbsp;/etc/modules-load.d/k8s.conf &lt;br /&gt;br_netfilter &lt;br /&gt;EOF &lt;br /&gt;&lt;br /&gt;cat&amp;nbsp;&amp;lt;&amp;lt;EOF&amp;nbsp;|&amp;nbsp;sudo&amp;nbsp;tee&amp;nbsp;/etc/sysctl.d/k8s.conf &lt;br /&gt;net.bridge.bridge-nf-call-ip6tables&amp;nbsp;=&amp;nbsp;1 &lt;br /&gt;net.bridge.bridge-nf-call-iptables&amp;nbsp;=&amp;nbsp;1 &lt;br /&gt;EOF &lt;br /&gt;&lt;br /&gt;sudo&amp;nbsp;sysctl&amp;nbsp;--system &lt;br /&gt;&lt;br /&gt;sudo&amp;nbsp;apt-get&amp;nbsp;update &lt;br /&gt;sudo&amp;nbsp;apt-get&amp;nbsp;install&amp;nbsp;-y&amp;nbsp;apt-transport-https&amp;nbsp;ca-certificates&amp;nbsp;curl &lt;br /&gt;&lt;br /&gt;&lt;s&gt;sudo&amp;nbsp;curl&amp;nbsp;-fsSLo&amp;nbsp;/usr/share/keyrings/kubernetes-archive-keyring.gpg&amp;nbsp;&lt;a href=&quot;https://packages.cloud.google.com/apt/doc/apt-key.gpg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://packages.cloud.google.com/apt/doc/apt-key.gpg&lt;/a&gt;&lt;/s&gt;&lt;br /&gt;&lt;s&gt;echo&amp;nbsp;&quot;deb&amp;nbsp;[signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg]&amp;nbsp;&lt;a href=&quot;https://apt.kubernetes.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://apt.kubernetes.io/&lt;/a&gt;&amp;nbsp;kubernetes-xenial&amp;nbsp;main&quot;&amp;nbsp;|&amp;nbsp;sudo&amp;nbsp;tee&amp;nbsp;/etc/apt/sources.list.d/kubernetes.list &lt;/s&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;위 명령어는 공식 가이드 문서에 있는거지만 2023년 이후로 막힘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어 입력 후 apt update 하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GPG error public key is not available: NO_PUBKEY B53DC80D13EDEF05&lt;/blockquote&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;sudo&amp;nbsp;curl&amp;nbsp;-fsSLo&amp;nbsp;/usr/share/keyrings/kubernetes-archive-keyring.gpg&amp;nbsp;&lt;a href=&quot;https://dl.k8s.io/apt/doc/apt-key.gpg&quot;&gt;https://dl.k8s.io/apt/doc/apt-key.gpg&lt;/a&gt;&lt;br /&gt;echo&amp;nbsp;&quot;deb&amp;nbsp;[signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg]&amp;nbsp;&lt;a href=&quot;https://apt.kubernetes.io/&quot;&gt;https://apt.kubernetes.io/&lt;/a&gt;&amp;nbsp;kubernetes-xenial&amp;nbsp;main&quot;&amp;nbsp;|&amp;nbsp;sudo&amp;nbsp;tee&amp;nbsp;/etc/apt/sources.list.d/kubernetes.list&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;# sudo apt-get update -y&lt;br /&gt;#&amp;nbsp;sudo&amp;nbsp;apt-get&amp;nbsp;install&amp;nbsp;-y&amp;nbsp;kubelet&amp;nbsp;kubeadm&amp;nbsp;kubectl &lt;br /&gt;#&amp;nbsp;sudo&amp;nbsp;apt-mark&amp;nbsp;hold&amp;nbsp;kubelet&amp;nbsp;kubeadm&amp;nbsp;kubectl &lt;br /&gt;&lt;br /&gt;#&amp;nbsp;sudo&amp;nbsp;systemctl&amp;nbsp;enable&amp;nbsp;kubelet &lt;br /&gt;# sudo systemctl restart kubelet&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마스터 노드 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo kubeadm init&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 아래와 같이 에러 뜸&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704564831613&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;W1125 12:58:32.733485   26426 configset.go:348] WARNING: kubeadm cannot validate component configs for API groups [kubelet.config.k8s.io kubeproxy.config.k8s.io]
[init] Using Kubernetes version: v1.19.4
[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
        [ERROR CRI]: container runtime is not running: output: time=&quot;2020-11-25T12:58:32Z&quot; level=fatal msg=&quot;getting status of runtime failed: rpc error: code = Unimplemented desc = unknown service runtime.v1alpha2.RuntimeService&quot;
, error: exit status 1
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
To see the stack trace of this error execute with --v=5 or higher&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;user@k8s-master:~/$ sudo rm /etc/containerd/config.toml
user@k8s-master:~/$ sudo systemctl restart containerd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704566802670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user@k8s-master:~/$ sudo kubeadm init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init이 성공적이라면 아래와 같이 토큰 값 내뱉음(이걸 워커 노드에 그대로 입력하면 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후에 워커노드 더 추가하고 싶으면 kubeadm kubectl 설치하고 아래 토큰 그대로 복붙하면 될듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터노드는 무조건 고정 ip써야겠네..&lt;/p&gt;
&lt;pre id=&quot;code_1704564928819&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo kubeadm join 192.168.219.103:6443 --token yjrzlz.0lr5dfb82u01m90u \
        --discovery-token-ca-cert-hash sha256:dc6bcba93353de37c185f4c5666c86e46c5e85530ac1f149582e59d11330fda1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어는 kubectl 명령어를 쓰기 위해서 적어줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 내 계정으로 접속해서 아래 명령어를 치면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 root 계정에도 아래 명령어를 똑같이 입력해줘야함. 그래야 root에서도 k get pod가 됨.&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;sudo&amp;nbsp;passwd&amp;nbsp;root &lt;br /&gt;su&amp;nbsp;-&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 /.kube에서 .은 숨겨진 폴더라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$HOME은 자기만의 계정 홈 디렉토리를 말하는거라 각각 계정에 .kube 폴더를 만들어줘야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704564913664&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl&amp;nbsp;apply&amp;nbsp;-f&amp;nbsp;&lt;a href=&quot;https://github.com/weaveworks/weave/releases/download/v2.8.1/weave-daemonset-k8s-1.11.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/weaveworks/weave/releases/download/v2.8.1/weave-daemonset-k8s-1.11.yaml&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to connect to the server: dial tcp: lookup cloud.weave.works on 127.0.0.53:53: no such host&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어 뜨면 인터넷 안되는 거임 127.0.0.53은 DNS 서버주소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;워커 노드 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 토큰 그대로 복붙&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마스터 노드 설정 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tab으로 자동완성 기능&lt;/p&gt;
&lt;pre id=&quot;code_1704566918459&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;source &amp;lt;(kubectl completion bash)
echo &quot;source &amp;lt;(kubectl completion bash)&quot; &amp;gt;&amp;gt; ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;vi&amp;nbsp;~/.bashrc&lt;/p&gt;
&lt;pre id=&quot;code_1704567175905&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;alias k='kubectl'
alias kd='kubectl describe'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;워커 노드 설정 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;추후에 마스터 노드에서 ssh로 워커 노드에 접근하고 싶은 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 계정은 되는데 root 계정은 접근이 안될 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1704607348873&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo vi /etc/ssh/sshd_config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 파일을 열어서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f8fafd; color: #444746; text-align: start;&quot;&gt;PermitRootLogin yes&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 넣어주면 된다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CNI 설치하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kubectl&amp;nbsp;apply&amp;nbsp;-f&amp;nbsp;&lt;a href=&quot;https://docs.projectcalico.org/manifests/calico.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.projectcalico.org/manifests/calico.yaml&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;curl&amp;nbsp;-L&amp;nbsp;&lt;a href=&quot;https://github.com/projectcalico/calico/releases/download/v3.24.5/calicoctl-linux-amd64&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/projectcalico/calico/releases/download/v3.24.5/calicoctl-linux-amd64&lt;/a&gt;&amp;nbsp;-o&amp;nbsp;calicoctl &lt;br /&gt;chmod&amp;nbsp;700&amp;nbsp;calicoctl &lt;br /&gt;sudo&amp;nbsp;mv&amp;nbsp;calicoctl&amp;nbsp;/usr/bin/&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>☸️ Kubernetes</category>
      <author>loose</author>
      <guid isPermaLink="true">https://stir.tistory.com/456</guid>
      <comments>https://stir.tistory.com/456#entry456comment</comments>
      <pubDate>Sun, 7 Jan 2024 03:06:56 +0900</pubDate>
    </item>
  </channel>
</rss>