<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>정리정리</title>
    <link>https://wlsh44.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 5 Aug 2026 21:31:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>wlsh</managingEditor>
    <image>
      <title>정리정리</title>
      <url>https://tistory1.daumcdn.net/tistory/4773516/attach/6ed9051787f44e09a97ca81ce7fc9e53</url>
      <link>https://wlsh44.tistory.com</link>
    </image>
    <item>
      <title>좋은 커밋 메시지 생각해보기</title>
      <link>https://wlsh44.tistory.com/43</link>
      <description>&lt;p style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에 Github에 대해 다시 생각해 보는 기회가 생겼고, 그중에서도 좋은 커밋이란 무엇일까에 대해 고민을 하면서 찾게 된 세 가지를 정리해보려 한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. Atomic Commit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atomic Commit은 &lt;a href=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;A Developer&amp;rsquo;s Guide to Atomic Git Commits - Sandro Dzneladze&lt;/a&gt;의 글을 바탕으로 정리하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에서는 커밋을 &quot;더 이상 쪼갤 수 없는, 의미 있는 최소 단위&quot;로 정의한다. 그러기 위해서 다음과 같은 세 가지를 만족해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 하나의 논리적 변경만 담는다&lt;br /&gt;2. 커밋 이후에도 코드베이스가 동작하는 상태여야 한다&lt;br /&gt;3. 관심사를 섞지 않는다 (ex: feat: xxx기능 변경 및 리팩터링)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 함으로써 다음과 같은 7가지 이점을 얻을 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;안전성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋이 하나의 관심사를 가지고 있기 때문에 문제가 생겼을 때 revert와 cherry-pick이 쉬워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;품질&lt;/b&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;/li&gt;
&lt;li&gt;&lt;b&gt;계획&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능을 잘게 쪼개는 습관이 더 나은 설계로 이어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추적성&lt;/b&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;몇 달 후에도 &quot;왜 이렇게 바꿨는지&quot; 히스토리로 알 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협업&lt;/b&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;Merge conflits를 해결하기 쉬워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;git bisect와 같은 명령어로 버그를 찾는 과정이 Atomic Commit에서 더 빠르고 정확하게 작동함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;릴리즈 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확한 commit이 changelog 자동 생성 도구와 잘 맞음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 커밋 컨벤션&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;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Conventional Commits&lt;/a&gt;에서 정리된 커밋 컨벤션이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1773757614749&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;type&amp;gt;[optional scope]: &amp;lt;description&amp;gt;

[optional body]

[optional footer(s)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 다음과 같은 이점을 얻을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292e; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CHANGELOG를 자동으로 생성하기 위해서&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 커밋을 얼마나 자주 해야할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Atomic Commit의 개념은 이해하기 어렵지 않다. 다만 실제로 이를 실천하려고 할 때 생각보다 잘 되지 않는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 커밋을 Atomic 하게 만들기 위해서는 자주 커밋을 해야한다. 하지만&amp;nbsp;처음 접하는 도메인을 개발한다든지, 큰 규모의 리팩터링 작업같은 걸 하다 보면 Atomic하게 만들려고 노력해도 어느새 커밋이 돌이키기 힘들 정도로 거대해져 있는 경우가 많았다.&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://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/?tl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes&lt;/a&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;rarr; PR 올리기 전에 squash로 정리 &amp;rarr; PR&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&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;다만 무분별한 squash는 &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #412402; text-align: start;&quot;&gt;오히려 개발 과정의 사고 흐름과 맥락을 지워버린다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #412402; text-align: start;&quot;&gt;기능 단위가 크다면, 의미 있는 중간 커밋들을 억지로 하나로 뭉개면 히스토리가 오히려 불투명해질 수 있다. squash는 의미 없는 커밋을 정리하는 게 목적이지, 단순히 보기 좋게 작게 만드는 작업이 되어서는 안 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span style=&quot;color: #412402; text-align: start;&quot;&gt;정리&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 덕목으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;소통을 꼽는 사람이 많다. 그런데 소통이 꼭 말이나 글로만 이루어지는 건 아니다. 팀에서 일할 때, 우리가 가장 자주 마주치는 &quot;소통의 결과물&quot; 중 하나는 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;커밋 히스토리다.&lt;/p&gt;
&lt;p style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잘 작성된 커밋은 리뷰어가 맥락을 파악하는 시간을 줄여주고, 히스토리를 읽는 사람이 의도를 이해할 수 있게 해준다. 반대로 뭉텅이로 올라온 커밋은 상대방에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;알아서 파악해&quot;라고 말하는 것과 다르지 않다.&lt;/p&gt;
&lt;p style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;당연하다고 생각하면서도 잘 되지 않아 난장판이 된 나의 커밋 기록들을 보면서 반성하고 더 나은 커밋을 작성할 수 있도록 노력해야겠다는 생각을 했다.&lt;/p&gt;
&lt;p style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;참고자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773759436967&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;A Developer&amp;rsquo;s Guide to Atomic Git Commits&quot; data-og-description=&quot;An atomic commit is a Git commit that represents the smallest possible, meaningful change. It does exactly one thing &amp;mdash; and nothing more&amp;hellip;&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot; data-og-url=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/YrOoH/dJMb86O0H1c/Hk5B9vbzkQvRlDy4OHBWd1/img.png?width=1200&amp;amp;height=479&amp;amp;face=0_0_1200_479&quot;&gt;&lt;a href=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@sandrodz/a-developers-guide-to-atomic-git-commits-c7b873b39223&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/YrOoH/dJMb86O0H1c/Hk5B9vbzkQvRlDy4OHBWd1/img.png?width=1200&amp;amp;height=479&amp;amp;face=0_0_1200_479');&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;A Developer&amp;rsquo;s Guide to Atomic Git Commits&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An atomic commit is a Git commit that represents the smallest possible, meaningful change. It does exactly one thing &amp;mdash; and nothing more&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;&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.conventionalcommits.org/en/v1.0.0/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773759455110&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;Conventional Commits&quot; data-og-description=&quot;A specification for adding human and machine readable meaning to commit messages&quot; data-og-host=&quot;www.conventionalcommits.org&quot; data-og-source-url=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; data-og-url=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fPSa5/dJMb9eTOA0N/EJgFOem6Jnzc9rZDQ5MPsK/img.png?width=351&amp;amp;height=1509&amp;amp;face=0_0_351_1509&quot;&gt;&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fPSa5/dJMb9eTOA0N/EJgFOem6Jnzc9rZDQ5MPsK/img.png?width=351&amp;amp;height=1509&amp;amp;face=0_0_351_1509');&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;Conventional Commits&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A specification for adding human and machine readable meaning to commit messages&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.conventionalcommits.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773759487255&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;Reddit의 webdev 커뮤니티&quot; data-og-description=&quot;webdev 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요&quot; data-og-host=&quot;www.reddit.com&quot; data-og-source-url=&quot;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&quot; data-og-url=&quot;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&amp;amp;seeker-session=true&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/icGu5/dJMb81GV8n9/Kbz7nHsk76fNcADsBPL0VK/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584,https://scrap.kakaocdn.net/dn/VZSJv/dJMb8867JSL/OQbbJGpsayYKML1I3hILD1/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.reddit.com/r/webdev/comments/1d2bvri/how_often_to_commit/?tl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/icGu5/dJMb81GV8n9/Kbz7nHsk76fNcADsBPL0VK/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584,https://scrap.kakaocdn.net/dn/VZSJv/dJMb8867JSL/OQbbJGpsayYKML1I3hILD1/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584');&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;Reddit의 webdev 커뮤니티&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;webdev 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.reddit.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773759496770&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;Reddit의 cscareerquestions 커뮤니티&quot; data-og-description=&quot;cscareerquestions 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요&quot; data-og-host=&quot;www.reddit.com&quot; data-og-source-url=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/&quot; data-og-url=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/?seeker-session=true&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eGoMZh/dJMb85WSdoq/HmrG6S2KOWMiRZ9GYJrc6k/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584,https://scrap.kakaocdn.net/dn/bFOZkE/dJMb88F3NFM/5qG4FPNZKZuuNViHgTyorK/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.reddit.com/r/cscareerquestions/comments/10lrfr9/how_often_should_you_commit_changes/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eGoMZh/dJMb85WSdoq/HmrG6S2KOWMiRZ9GYJrc6k/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584,https://scrap.kakaocdn.net/dn/bFOZkE/dJMb88F3NFM/5qG4FPNZKZuuNViHgTyorK/img.jpg?width=1120&amp;amp;height=584&amp;amp;face=0_0_1120_584');&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;Reddit의 cscareerquestions 커뮤니티&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;cscareerquestions 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.reddit.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jaeheon.kr/257&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jaeheon.kr/257&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773759553079&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;quot;깃 커밋은 언제 하는 것이 가장 좋을까?&amp;quot; 들어가면서 커밋의 단위는 개발자들마다 생각이 각기 다를 것이다. 누구는 크게, 누구는 작게 말이다. 이 글에서는 커밋을 어느 단위로 할지에 대해 이&quot; data-og-host=&quot;jaeheon.kr&quot; data-og-source-url=&quot;https://jaeheon.kr/257&quot; data-og-url=&quot;https://jaeheon.kr/257&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cYFYRy/dJMb85WSdou/fGWuUkg2Kd7X9V8vkoIKr1/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/6XcU0/dJMb9aKD2h2/JnI0fkQ1tiGZwPfdCPz5CK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/mC7xC/dJMb84XXEFR/Kg6kQoESH8fzCudi0ud500/img.jpg?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000&quot;&gt;&lt;a href=&quot;https://jaeheon.kr/257&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jaeheon.kr/257&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cYFYRy/dJMb85WSdou/fGWuUkg2Kd7X9V8vkoIKr1/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/6XcU0/dJMb9aKD2h2/JnI0fkQ1tiGZwPfdCPz5CK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/mC7xC/dJMb84XXEFR/Kg6kQoESH8fzCudi0ud500/img.jpg?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000');&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;&quot;깃 커밋은 언제 하는 것이 가장 좋을까?&quot; 들어가면서 커밋의 단위는 개발자들마다 생각이 각기 다를 것이다. 누구는 크게, 누구는 작게 말이다. 이 글에서는 커밋을 어느 단위로 할지에 대해 이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jaeheon.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>협업</category>
      <category>Commit</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/43</guid>
      <comments>https://wlsh44.tistory.com/43#entry43comment</comments>
      <pubDate>Tue, 17 Mar 2026 23:54:09 +0900</pubDate>
    </item>
    <item>
      <title>프롬프팅에 관한 고찰</title>
      <link>https://wlsh44.tistory.com/42</link>
      <description>&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;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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM과 관련된 여러 레퍼런스를 살펴보면, 효과적인 프롬프트에는 공통적으로 담기는 요소들이 있다. 물론 모든 요소를 매번 다 넣어야 하는 건 아니다. 중요한 건 이 요소들이 각각 어떤 역할을 하는지 이해하고, 상황에 맞게 골라서 쓰는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맥락 (Context)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트를 작성할 때 가장 핵심이 되는 요소다. 실제로 요즘은 Prompt Engineering에 이어 &lt;b&gt;Context Engineering&lt;/b&gt;이 더 중요해지고 있는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 좋은 맥락을 제공하려면 최대한 자세하고 명확하게 설명해야 한다. &lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#general-principles&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Anthropic 블로그&lt;/a&gt;에서는 Claude를 &quot;맥락을 전혀 모르는 똑똑한 신입 직원&quot;에 비유하면서, &quot;이 프롬프트를 맥락을 잘 모르는 동료에게 보여줬을 때 헷갈린다면, AI도 헷갈린다&quot; 고 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 당연하게 여기는 배경지식을 AI는 전혀 모를 수 있다는 점을 명심해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할 (Role)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;마케터로서&quot;, &quot;시니어 개발자로서&quot;처럼 AI에게 역할을 부여하는 방식이다. 결국 AI에게 어떤 &lt;b&gt;관점&lt;/b&gt;으로 답해주길 원하는지 알려주는 요소다. 같은 질문이라도 역할에 따라 강조점과 깊이가 크게 달라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 알려진 프롬프트 기법을 상황에 맞게 활용하는 것도 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Few-shot&lt;/b&gt;은 원하는 결과물의 형태를 먼저 예시로 보여주는 방식이다. AI가 원하는 포맷이나 톤을 정확히 이해하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;a href=&quot;https://www.promptingguide.ai/techniques/fewshot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;promptingguide&lt;/a&gt;에 예제를 보면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[input] &lt;br /&gt;A &quot;whatpu&quot; is a small, furry animal native to Tanzania. An example of a sentence that uses the word whatpu is:&lt;br /&gt;We were traveling in Africa and we saw these very cute whatpus. &lt;br /&gt;To do a &quot;farduddle&quot; means to jump up and down really fast. An example of a sentence that uses the word farduddle is:&lt;br /&gt;&lt;br /&gt;[output] &lt;br /&gt;When we won the game, we all started to farduddle in celebration.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 한 개만 넣어줬을 뿐인데, AI는 길이와 구조, 어조를 그대로 따라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Chain of Thought(CoT)&lt;/b&gt; 는 원하는 답을 바로 내놓기 전에 사고 과정을 먼저 거치도록 유도하는 방식이다. 복잡한 문제일수록 효과적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzob9T/dJMcaaqWLe7/CyWIJBOyfa2Cw1jVeKBgZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzob9T/dJMcaaqWLe7/CyWIJBOyfa2Cw1jVeKBgZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzob9T/dJMcaaqWLe7/CyWIJBOyfa2Cw1jVeKBgZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzob9T%2FdJMcaaqWLe7%2FCyWIJBOyfa2Cw1jVeKBgZ1%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;944&quot; height=&quot;508&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;단계별로 생각해&quot;라는 지시 하나만 추가했는데, AI가 중간 계산 과정을 명시적으로 보여주면서 실수를 줄이게 된다.&lt;/p&gt;
&lt;h1&gt;같은 주제, 두 가지 프롬프트&lt;/h1&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;b&gt;첫 번째 프롬프트 &amp;mdash; 아무 맥락 없이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773039110654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Java에서 변수와 상수의 차이를 설명해줘.&lt;/code&gt;&lt;/pre&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;h2 data-ke-size=&quot;size26&quot;&gt;Java에서 변수(Variable)와 상수(Constant)의 차이점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수 (Variable)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 &lt;b&gt;변경할 수 있는&lt;/b&gt; 저장 공간입니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;int age = 25;
age = 30;  // ✅ 변경 가능

String name = &quot;Alice&quot;;
name = &quot;Bob&quot;;  // ✅ 변경 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상수 (Constant)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;final 키워드를 사용해 값을 &lt;b&gt;한 번만 할당&lt;/b&gt;할 수 있는 저장 공간입니다. 관례적으로 &lt;b&gt;대문자 + 언더스코어&lt;/b&gt;로 이름을 짓습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;final int MAX_SIZE = 100;
MAX_SIZE = 200;  // ❌ 컴파일 오류 발생!

final double PI = 3.14159;
PI = 3.0;  // ❌ 컴파일 오류 발생!
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 비교&lt;/h3&gt;
구분 변수 상수
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;키워드&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;값 변경&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네이밍&lt;/td&gt;
&lt;td&gt;camelCase&lt;/td&gt;
&lt;td&gt;UPPER_SNAKE_CASE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;변하는 데이터&lt;/td&gt;
&lt;td&gt;고정된 값 (&amp;pi;, 최대값 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 상수 (static final)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 전체에서 공유하는 상수는 static final을 함께 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class AppConfig {
    public static final String APP_NAME = &quot;MyApp&quot;;
    public static final int MAX_USERS = 1000;
}

// 어디서든 접근 가능
System.out.println(AppConfig.APP_NAME);  // &quot;MyApp&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 상수를 써야 할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;매직 넘버 방지&lt;/b&gt; &amp;mdash; if (score &amp;gt;= 100) 대신 if (score &amp;gt;= MAX_SCORE)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성&lt;/b&gt; &amp;mdash; 값을 한 곳에서만 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가독성&lt;/b&gt; &amp;mdash; 숫자보다 이름이 의미를 명확히 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&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;역할도, 독자도, 목적도, 형식도 없기 때문에 AI는 가장 일반적인 답변을 한다. 변수와 상수의 정의, 간단한 Java 코드 예시, final 키워드의 용도 정도를 담은 깔끔하지만 평범한 답변이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째 프롬프트 &amp;mdash; 맥락과 구조를 담아서&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;역할
너는 10년 이상의 경력을 가진 JVM 전문 엔지니어야. 너는 JVM 내부 동작 원리, 바이트코드 분석, 메모리 구조에 정통하며, 복잡한 개념을 명확한 코드 예시와 함께 설명하는 데 탁월해. 

상황
나는 Java 변수와 상수의 표면적인 문법 차이는 알고 있지만, JVM이 내부적으로 이 둘을 어떻게 다르게 처리하는지 깊이 이해하고 싶어. 특히 성능 최적화 관점에서 상수와 변수의 차이점을 확인하고 싶어.

설명 순서
아래 순서대로 단계적으로 사고하며 설명해줘. 각 단계를 건너뛰지 말고, 이전 단계의 개념을 바탕으로 다음 단계를 설명해: 
Step 1 &amp;mdash; JVM 메모리 구조 매핑에서 각 선언 방식이 JVM의 Stack / Heap / Method Area(MetaSpace) / 상수 풀(Constant Pool) 중 어디에 저장되는지 설명하고, 다이어그램 형태로 시각화해줘. 
Step 2 &amp;mdash; 컴파일 타임 vs 런타임 처리에서 메모리 배치가 컴파일 시점에 어떻게 결정되는지, `javap -c`로 확인할 수 있는 바이트코드 예시를 들어 변수 처리 방식의 차이를 보여줘. 
Step 3 &amp;mdash; `final` 참조변수가 왜 진정한 상수가 아닌지 설명해줘. 참조 불변과 객체 상태 불변의 차이를 코드로 증명하고, 완전한 불변 객체(Immutable Object)를 만드는 방법을 제시해줘. 
Step 4 &amp;mdash; JIT 컴파일러가 상수를 어떻게 최적화하는지 설명해줘. Dead Code Elimination, Constant Folding 등 실제 최적화 기법과 변수 대비 성능 차이를 구체적인 수치나 예시로 보여줘. 

출력 형식
- 각 Step의 시작에 &quot;Step N에서 다루는 핵심 질문&quot; 을 한 줄로 명시해줘
- 각 Step 말미에 &quot;이 단계의 핵심 요약&quot; 을 bullet point 3개 이내로 정리해줘
- 전체 답변 마지막에 4단계를 관통하는 한 줄 결론을 작성해줘
&lt;/code&gt;&lt;/pre&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;h1&gt;JVM 관점에서의 변수 vs 상수: 심층 분석&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step 1 &amp;mdash; JVM 메모리 구조 매핑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1에서 다루는 핵심 질문:&lt;/b&gt; 각 선언 방식(변수/상수)은 JVM의 어느 메모리 영역에 저장되는가?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분석 대상 코드&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class MemoryDemo {
    static final int CONST = 100;          // 컴파일 타임 상수
    static final String LABEL = &quot;hello&quot;;   // String 상수
    static int staticVar = 200;            // static 변수
    int instanceVar = 300;                 // 인스턴스 변수

    void method() {
        final int localConst = 42;         // 지역 상수
        int localVar = 99;                 // 지역 변수
        MemoryDemo obj = new MemoryDemo(); // 객체 생성
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JVM 메모리 배치 다이어그램&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                     JVM Memory Architecture                         │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │              Method Area (MetaSpace, Java 8+)                │  │
│  │                                                              │  │
│  │  ┌─────────────────────────────────────────────────────┐    │  │
│  │  │           Runtime Constant Pool                     │    │  │
│  │  │                                                     │    │  │
│  │  │  ┌──────────────────────────────────────────────┐  │    │  │
│  │  │  │  String Pool (Interned Strings)              │  │    │  │
│  │  │  │   &quot;hello&quot; ──────────────────────────────┐   │  │    │  │
│  │  │  └──────────────────────────────────────────┘  │   │    │  │
│  │  │                                                 │   │    │  │
│  │  │  [#1] CONST = 100     &amp;larr; int 리터럴 직접 저장   │   │    │  │
│  │  │  [#2] LABEL ref ──────────────────────────────►│   │    │  │
│  │  │  [#3] staticVar = 200 &amp;larr; 참조(런타임 변경 가능) │   │    │  │
│  │  └─────────────────────────────────────────────────┘    │  │
│  │                                                          │  │
│  │  MemoryDemo.class 메타데이터                             │  │
│  │  - 필드 정보: CONST, LABEL, staticVar, instanceVar       │  │
│  │  - 메서드 바이트코드: method()                           │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                        Heap                                  │  │
│  │                                                              │  │
│  │   ┌──────────────────────────────────────────┐              │  │
│  │   │  MemoryDemo 인스턴스 (new MemoryDemo())   │              │  │
│  │   │  ┌────────────────────────────────────┐  │              │  │
│  │   │  │  instanceVar = 300  (int, 4 bytes) │  │              │  │
│  │   │  │  [object header: 12~16 bytes]      │  │              │  │
│  │   │  └────────────────────────────────────┘  │              │  │
│  │   └──────────────────────────────────────────┘              │  │
│  │                                                              │  │
│  │   ※ static final int CONST &amp;rarr; Heap에 객체 없음!             │  │
│  │     컴파일러가 리터럴로 인라이닝 처리                       │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │              JVM Stack (Thread마다 독립)                     │  │
│  │                                                              │  │
│  │   ┌──────────────────────────────────────────────────────┐  │  │
│  │   │  Stack Frame: method() 호출                          │  │  │
│  │   │  ┌────────────────────────────────────────────────┐  │  │  │
│  │   │  │  Local Variable Array                          │  │  │  │
│  │   │  │  [0] this      &amp;rarr; Heap의 MemoryDemo 인스턴스 참조│  │  │  │
│  │   │  │  [1] localConst = 42   &amp;larr; primitive, 값 직접 저장│  │  │  │
│  │   │  │  [2] localVar  = 99    &amp;larr; primitive, 값 직접 저장│  │  │  │
│  │   │  │  [3] obj       &amp;rarr; Heap의 새 인스턴스 참조        │  │  │  │
│  │   │  └────────────────────────────────────────────────┘  │  │  │
│  │   │  Operand Stack (연산 중간값)                          │  │  │
│  │   └──────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

  저장 위치 요약
  ┌────────────────────────┬────────────────────────────────────────┐
  │      선언 방식         │           저장 위치                    │
  ├────────────────────────┼────────────────────────────────────────┤
  │ static final int       │ 컴파일 후 사용처에 인라이닝 &amp;rarr;          │
  │                        │ Runtime Constant Pool 참조 자체가 제거 │
  ├────────────────────────┼────────────────────────────────────────┤
  │ static final String    │ String Pool (Heap 내 별도 관리 영역)   │
  ├────────────────────────┼────────────────────────────────────────┤
  │ static int (변수)      │ Method Area (클래스 로딩 시 할당)      │
  ├────────────────────────┼────────────────────────────────────────┤
  │ int instanceVar        │ Heap (객체 생성 시 함께 할당)          │
  ├────────────────────────┼────────────────────────────────────────┤
  │ final int localConst   │ Stack Frame의 Local Variable Array     │
  ├────────────────────────┼────────────────────────────────────────┤
  │ int localVar           │ Stack Frame의 Local Variable Array     │
  └────────────────────────┴────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 포인트:&lt;/b&gt; static final int 처럼 primitive + 리터럴 조합의 상수는 &lt;b&gt;사용하는 클래스의 바이트코드에 값이 직접 박혀버린다(인라이닝)&lt;/b&gt;. 런타임에 상수 풀을 참조하는 과정 자체가 없어진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 단계의 핵심 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;primitive static final은 컴파일 타임에 인라이닝되어 런타임 메모리 참조가 발생하지 않음&lt;/li&gt;
&lt;li&gt;인스턴스 변수는 Heap, 지역 변수/상수는 Stack에 저장되며 저장 위치는 타입이 아닌 &lt;b&gt;선언 위치&lt;/b&gt;로 결정됨&lt;/li&gt;
&lt;li&gt;static final String은 String Pool을 통해 Heap에 단 하나의 인스턴스만 유지됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step 2 &amp;mdash; 컴파일 타임 vs 런타임 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2에서 다루는 핵심 질문:&lt;/b&gt; 상수 인라이닝은 바이트코드 레벨에서 실제로 어떻게 보이는가?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분석 대상 코드&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class BytecodeDemo {
    static final int CONST_VAL = 100;   // 컴파일 타임 상수
    static int runtimeVar = 100;        // 런타임 변수

    public static int useConst() {
        return CONST_VAL * 2;           // 상수 사용
    }

    public static int useVar() {
        return runtimeVar * 2;          // 변수 사용
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;javap -c 바이트코드 비교&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// ──────────────────────────────────────────────────
// javap -c BytecodeDemo
// ──────────────────────────────────────────────────

public static int useConst();
  Code:
     0: bipush        100    // &amp;larr; CONST_VAL = 100이 리터럴로 직접 삽입됨!
                             //   getstatic 명령어가 없다 = 메모리 참조 없음
     2: iconst_2             // 상수 2를 스택에 push
     3: imul                 // 곱셈 수행
     4: ireturn

// ──────────────────────────────────────────────────

public static int useVar();
  Code:
     0: getstatic     #7     // &amp;larr; Field runtimeVar:I
                             //   Method Area에서 값을 가져오는 명령어 실행
                             //   메모리 참조가 반드시 발생!
     3: iconst_2
     4: imul
     5: ireturn
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인라이닝의 파급 효과 &amp;mdash; 크로스 클래스 컴파일&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 파일 1: Constants.java
public class Constants {
    public static final int MAX = 500;
}

// 파일 2: App.java
public class App {
    public static void main(String[] args) {
        System.out.println(Constants.MAX);  // 상수 참조
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// App.class의 바이트코드 (javap -c App)

public static void main(java.lang.String[]);
  Code:
     0: getstatic     #7     // PrintStream 참조
     3: sipush        500    // &amp;larr; Constants.MAX가 500으로 직접 인라이닝됨!
                             //   Constants 클래스를 로드조차 안 할 수도 있다
     6: invokevirtual #13    // println
     9: return
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;실무 함정:&lt;/b&gt; Constants.MAX를 500 &amp;rarr; 1000으로 바꾸고 Constants.java만 재컴파일하면, &lt;b&gt;App.class에는 여전히 500이 박혀있다&lt;/b&gt;. App.java도 반드시 함께 재컴파일해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴파일 타임 상수 판별 조건&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public class CompileTimeCheck {

    // ✅ 컴파일 타임 상수 (인라이닝 O)
    static final int A = 10;
    static final int B = 20;
    static final int C = A + B;          // 상수끼리의 연산도 인라이닝
    static final String S = &quot;Hi&quot; + &quot;!&quot;;  // 문자열 리터럴 연결도 인라이닝

    // ❌ 컴파일 타임 상수 아님 (인라이닝 X)
    static final int D = new Random().nextInt();  // 런타임에 값 결정
    static final String T = new String(&quot;Hi&quot;);     // new 키워드 사용
    static final int[] ARR = {1, 2, 3};           // 배열 (참조 타입)
    final int E;  // 생성자에서 초기화 &amp;rarr; 컴파일 타임에 값 미확정
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// 바이트코드에서의 차이

// C = A + B &amp;rarr; static final int C = 30
ldc           #30     // 30으로 인라이닝

// D = new Random().nextInt()
new           #Random
invokespecial &amp;lt;init&amp;gt;
invokevirtual nextInt  // 매번 실행마다 메서드 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 단계의 핵심 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static final primitive/String 리터럴은 getstatic 없이 피연산자 스택에 값이 직접 push됨 &amp;rarr; 메모리 참조 비용 제로&lt;/li&gt;
&lt;li&gt;상수끼리의 산술/문자열 연결 연산도 컴파일러가 미리 계산해 단일 리터럴로 치환함&lt;/li&gt;
&lt;li&gt;다른 클래스의 상수를 인라이닝한 후 원본만 재컴파일하면 불일치 버그 발생 &amp;rarr; 전체 재빌드 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step 3 &amp;mdash; 불변성의 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 3에서 다루는 핵심 질문:&lt;/b&gt; final 참조변수는 왜 진정한 상수가 아닌가?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참조 불변 &amp;ne; 객체 상태 불변&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

public class FinalTrap {

    // final은 &quot;이 변수가 다른 객체를 가리키는 것&quot;을 막을 뿐이다
    static final List&amp;lt;String&amp;gt; NAMES = new ArrayList&amp;lt;&amp;gt;();

    public static void main(String[] args) {

        // ❌ 참조 변경 &amp;rarr; 컴파일 에러
        // NAMES = new ArrayList&amp;lt;&amp;gt;();  // cannot assign a value to final variable

        // ✅ 객체 내부 상태 변경 &amp;rarr; 완벽하게 허용됨!
        NAMES.add(&quot;Alice&quot;);
        NAMES.add(&quot;Bob&quot;);
        NAMES.remove(&quot;Alice&quot;);
        System.out.println(NAMES);  // [Bob]

        // ──────────────────────────────────────────
        // JVM 메모리 관점에서 보면:
        //
        //  Stack/Method Area           Heap
        //  ┌──────────────┐          ┌────────────────────┐
        //  │ NAMES = ref ─┼─────────►│ ArrayList 인스턴스 │
        //  │  (변경 불가) │          │  [&quot;Bob&quot;]           │
        //  └──────────────┘          │  (변경 자유)       │
        //                            └────────────────────┘
        //
        //  final이 잠그는 것: 화살표(참조값)
        //  final이 못 잠그는 것: 화살표가 가리키는 객체의 내부
        // ──────────────────────────────────────────
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;완전한 불변 객체(Immutable Object) 설계&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// 불변 객체의 5가지 원칙을 모두 적용한 예시
public final class ImmutableOrder {      // 1. 클래스를 final &amp;rarr; 상속 차단

    private final String orderId;        // 2. 모든 필드를 private final
    private final int amount;
    private final List&amp;lt;String&amp;gt; items;    // 3. 가변 참조 타입 필드는 방어적 복사 필요

    public ImmutableOrder(String orderId, int amount, List&amp;lt;String&amp;gt; items) {
        this.orderId = orderId;
        this.amount = amount;
        // 4. 생성자에서 방어적 복사 (외부 리스트 변경이 내부에 영향 못 주도록)
        this.items = new ArrayList&amp;lt;&amp;gt;(items);
    }

    public String getOrderId() { return orderId; }
    public int getAmount()     { return amount; }

    // 5. getter에서도 방어적 복사 (내부 리스트를 직접 반환하지 않음)
    public List&amp;lt;String&amp;gt; getItems() {
        return Collections.unmodifiableList(items);
    }

    // ❌ setter 없음
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 방어적 복사의 필요성 증명
public class DefensiveCopyDemo {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; original = new ArrayList&amp;lt;&amp;gt;(List.of(&quot;A&quot;, &quot;B&quot;));
        ImmutableOrder order = new ImmutableOrder(&quot;ORD-001&quot;, 5000, original);

        // 외부에서 original 리스트를 변조 시도
        original.add(&quot;C&quot;);

        // 방어적 복사 덕분에 order 내부는 변하지 않음
        System.out.println(order.getItems()); // [A, B] &amp;larr; C가 없다!

        // getter가 반환한 리스트 변조 시도
        List&amp;lt;String&amp;gt; returned = order.getItems();
        try {
            returned.add(&quot;D&quot;); // UnsupportedOperationException 발생!
        } catch (UnsupportedOperationException e) {
            System.out.println(&quot;외부에서 변경 불가 확인&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;record를 활용한 현대적 불변 객체 (Java 16+)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// record는 위의 5가지 원칙을 컴파일러가 자동 적용
// final 클래스, private final 필드, 생성자, getter 자동 생성
public record ImmutablePoint(int x, int y) {
    // 가변 필드가 있다면 compact constructor로 방어적 복사
    // public ImmutablePoint { items = List.copyOf(items); }
}

// 사용
ImmutablePoint p = new ImmutablePoint(3, 4);
// p.x = 10; ❌ 컴파일 에러
System.out.println(p.x()); // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 단계의 핵심 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;final은 &lt;b&gt;참조값(메모리 주소)&lt;/b&gt; 의 변경만 막으며, 참조가 가리키는 객체의 상태는 무방비 상태&lt;/li&gt;
&lt;li&gt;진정한 불변 객체를 위해서는 final 클래스 + private final 필드 + 방어적 복사(생성자/getter) 3박자가 필요&lt;/li&gt;
&lt;li&gt;Java 16+ record는 불변 객체 보일러플레이트를 컴파일러 수준에서 자동화함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step 4 &amp;mdash; JIT 최적화와 성능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 4에서 다루는 핵심 질문:&lt;/b&gt; JIT 컴파일러는 상수를 활용해 런타임에 어떤 추가 최적화를 수행하는가?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JIT가 개입하는 시점&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;소스코드 &amp;rarr; [javac] &amp;rarr; 바이트코드(.class) &amp;rarr; [인터프리터] &amp;rarr; 실행
                                              &amp;darr;
                                         호출 횟수 임계치 초과
                                         (기본값 ~10,000회)
                                              &amp;darr;
                                         [JIT 컴파일러]
                                         &amp;rarr; 네이티브 머신코드
                                         &amp;rarr; 최적화 적용
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적화 기법 1: Constant Folding (상수 접기)&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class ConstantFolding {
    static final int WIDTH  = 1920;
    static final int HEIGHT = 1080;

    public static int getPixelCount() {
        // 개발자 눈에는 곱셈처럼 보이지만...
        return WIDTH * HEIGHT;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// JIT 최적화 후 실제 실행되는 의사 코드
public static int getPixelCount() {
    return 2073600;  // 1920 * 1080을 컴파일/JIT 타임에 미리 계산
                     // 런타임 곱셈 연산: 0회
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적화 기법 2: Dead Code Elimination (죽은 코드 제거)&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class DeadCodeDemo {
    static final boolean DEBUG = false;  // 배포 환경

    public static void process(String data) {

        // JIT는 DEBUG가 절대 true가 될 수 없음을 알고 있다
        // &amp;rarr; 이 블록 전체를 네이티브 코드 생성 시 아예 제외
        if (DEBUG) {
            System.out.println(&quot;Processing: &quot; + data);  // &amp;larr; 생성된 기계어 없음
            validateInput(data);                         // &amp;larr; 생성된 기계어 없음
        }

        // 실제 실행 코드만 네이티브로 변환됨
        doActualWork(data);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 변수였다면? 매 실행마다 분기 비용 발생
public class DeadCodeWithVar {
    static boolean debug = false;  // final 없음

    public static void process(String data) {
        // JIT가 &quot;항상 false&quot;를 보장할 수 없음
        // &amp;rarr; 분기문(branch instruction) 이 네이티브 코드에 반드시 포함됨
        // &amp;rarr; CPU 분기 예측 비용 발생 가능
        if (debug) {
            System.out.println(&quot;Processing: &quot; + data);
        }
        doActualWork(data);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적화 기법 3: Loop Unrolling + Constant 조합&lt;/h3&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;public class LoopOptimization {
    static final int BATCH_SIZE = 4;  // 상수

    public static long sumArray(int[] arr) {
        long sum = 0;
        for (int i = 0; i &amp;lt; arr.length; i++) {
            sum += arr[i];
        }
        return sum;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// BATCH_SIZE 같은 상수를 JIT가 인지하면
// Loop Unrolling 최적화 적용 가능:

// 원본 루프 (4번 반복)
sum += arr[0]; sum += arr[1]; sum += arr[2]; sum += arr[3];

// JIT가 펼쳐서 생성한 의사 네이티브 코드:
// &amp;rarr; 루프 조건 체크(i &amp;lt; length) 4번 &amp;rarr; 1번으로 감소
// &amp;rarr; 분기 명령어 4번 &amp;rarr; 1번으로 감소
// &amp;rarr; CPU 파이프라인 효율 극대화
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 비교 벤치마크 (JMH 기반)&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class ConstVsVarBenchmark {

    // 상수
    static final int CONST_MULTIPLIER = 1000;

    // 변수
    static int varMultiplier = 1000;

    int value = 42;

    @Benchmark
    public int multiplyConst() {
        return value * CONST_MULTIPLIER;  // JIT: Constant Folding 적용
    }

    @Benchmark
    public int multiplyVar() {
        return value * varMultiplier;     // JIT: 매번 메모리에서 읽어야 함
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// 일반적인 JMH 실행 결과 (참고값, 환경마다 다름)
//
// ┌───────────────────────────────┬──────────────┬────────────┐
// │         Benchmark             │  Score(ns/op)│    비고    │
// ├───────────────────────────────┼──────────────┼────────────┤
// │ multiplyConst (static final)  │    ~0.3 ns   │ 레지스터   │
// │                               │              │ 상수 직접  │
// │                               │              │ 연산       │
// ├───────────────────────────────┼──────────────┼────────────┤
// │ multiplyVar   (static int)    │    ~1.2 ns   │ 메모리     │
// │                               │              │ 접근 포함  │
// └───────────────────────────────┴──────────────┴────────────┘
//
// &amp;rarr; 단순 산술에서도 약 4배 차이
//   핫루프(10억회 반복)에서는 이 차이가 초 단위로 누적됨
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JIT 최적화 체인 전체 그림&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;static final int LIMIT = 100;

for (int i = 0; i &amp;lt; LIMIT; i++) {
    if (LIMIT &amp;gt; 50) {       // 항상 true
        process(i);
    }
}

       &amp;darr; JIT 최적화 체인

① Constant Folding:   LIMIT &amp;gt; 50  &amp;rarr;  true
② Dead Code Elim:     if(true) 제거  &amp;rarr;  분기문 삭제
③ Loop Unrolling:     i &amp;lt; 100 반복 구조 펼치기
④ Inlining:           process()가 단순하면 호출 오버헤드 제거

// 최종 네이티브 코드 의사 표현:
process(0); process(1); process(2); ... // 분기 없는 직선 코드
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 단계의 핵심 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JIT는 static final을 통해 값이 절대 불변임을 확신하므로 Constant Folding, Dead Code Elimination을 공격적으로 적용 가능&lt;/li&gt;
&lt;li&gt;static 변수는 다른 스레드가 언제든 변경할 수 있어 JIT가 동일한 최적화를 적용하지 못함&lt;/li&gt;
&lt;li&gt;최적화들은 단독이 아닌 &lt;b&gt;체인&lt;/b&gt;으로 작동하며, 상수 하나가 이후 Inlining/Loop Unrolling까지 연쇄적으로 트리거함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 결론&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상수(static final)는 단순한 문법 제약이 아니라, 컴파일러에게는 인라이닝 신호를, JVM 메모리 구조에는 참조 비용 제거를, JIT에게는 무제한 최적화 허가증을 동시에 부여하는 성능 계약이다. 단, final 참조 타입은 이 계약의 적용 범위 밖에 있으므로 방어적 복사까지 갖춰야 비로소 완전한 불변성이 달성된다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&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;결과는 비교할 수 없을 만큼 달라진다. JVM 메모리 구조 다이어그램, javap -c 바이트코드 비교, final 참조변수의 함정, JIT 최적화 체인까지 같은 주제인데도 전혀 다른 깊이의 답변이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 프롬프트의 차이를 만든 요소들을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 단순히 &quot;시니어 개발자&quot;가 아닌 &quot;JVM 전문 엔지니어&quot;로 특정지어, 답변의 관점을 JVM과 메모리로 한정했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상황 (맥락)&lt;/b&gt;: 내가 어디까지 알고 있는지를 명확히 전달해 AI가 불필요한 기초 설명을 건너뛰고 핵심에 집중하게 했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설명 순서 (CoT)&lt;/b&gt;: 단계적으로 사고하도록 유도해 각 개념이 유기적으로 연결되는 구조적인 답변을 이끌어냈다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출력 형식&lt;/b&gt;: 원하는 형태를 미리 지정해 답변을 읽기 쉽고 참고하기 좋게 만들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현실에서의 질문과 프롬프트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 엔지니어링에 대한 글을 보다 보면, 처음 만나는 사람에게 질문하는 법과 본질적으로 다르지 않다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이 제대로 전달되려면 최소한의 전제를 먼저 공유해야 한다. &quot;이거 어떻게 해요?&quot;, &amp;ldquo;이거 왜 안 돼요?&amp;rdquo; 라는 질문은 맥락 없이는 아무것도 전달하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 AI는 처음 만난 사람과 같다고 생각한다. 나에 대한 맥락이 없기 때문에 내가 먼저 채워줘야 한다. 그리고 그 맥락을 잘 설정하려면 결국 내가 무엇을 알고 모르는지, 무엇을 알고싶어 하는지, 즉 메타인지가 중요한 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.promptingguide.ai/techniques/fewshot&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.promptingguide.ai/techniques/fewshot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773139421039&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;Few-Shot Prompting &amp;ndash; Nextra&quot; data-og-description=&quot;A Comprehensive Overview of Prompt Engineering&quot; data-og-host=&quot;www.promptingguide.ai&quot; data-og-source-url=&quot;https://www.promptingguide.ai/techniques/fewshot&quot; data-og-url=&quot;https://www.promptingguide.ai/techniques/fewshot&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.promptingguide.ai/techniques/fewshot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.promptingguide.ai/techniques/fewshot&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;Few-Shot Prompting &amp;ndash; Nextra&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Comprehensive Overview of Prompt Engineering&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.promptingguide.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#general-principles&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773139444001&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;Prompting best practices&quot; data-og-description=&quot;Comprehensive guide to prompt engineering techniques for Claude's latest models, covering clarity, examples, XML structuring, thinking, and agentic systems.&quot; data-og-host=&quot;platform.claude.com&quot; data-og-source-url=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#general-principles&quot; data-og-url=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bsJfIM/dJMb88665jO/DST1VaeqI7E9j1q3yXdBFk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zFNQN/dJMb8WMnOHx/YHkGwMA5WmNCFkAKps44OK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#general-principles&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#general-principles&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bsJfIM/dJMb88665jO/DST1VaeqI7E9j1q3yXdBFk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zFNQN/dJMb8WMnOHx/YHkGwMA5WmNCFkAKps44OK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Prompting best practices&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Comprehensive guide to prompt engineering techniques for Claude's latest models, covering clarity, examples, XML structuring, thinking, and agentic systems.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;platform.claude.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>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/42</guid>
      <comments>https://wlsh44.tistory.com/42#entry42comment</comments>
      <pubDate>Mon, 9 Mar 2026 15:57:59 +0900</pubDate>
    </item>
    <item>
      <title>소소한 스프링 레디스 컨트리뷰트 이야기</title>
      <link>https://wlsh44.tistory.com/41</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 새로 업데이트된 Spring boot 4.0.0를 이용해 코드를 작성해보고 있었는데, Spring Redis를 사용하는 코드에서 문제가 발생하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1770645477649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; results = stringRedisTemplate.executePipelined(
  new RedisCallback&amp;lt;Object&amp;gt;() {
    @Override
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
      StringRedisConnection stringRedisConn = (StringRedisConnection) connection; // 예외 발생 부분
      ...
      return null;
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 Spring Redis Docs의 Pipelining 부분에 예제 코드로 나와있을 만큼 간단하면서 검증이 된 코드였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Spring Boot 3.x 버전에서도 잘 동작하는 것을 알고 있었기 때문에 4.0으로 올라가면서 문제가 발생했을 거라는 생각이 들어 디버깅을 해봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디버깅&lt;/h2&gt;
&lt;pre id=&quot;code_1770645513589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java.lang.ClassCastException: class jdk.proxy3.$Proxy128 cannot be cast to class org.springframework.data.redis.connection.StringRedisConnection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 doInRedis메서드의 파라미터로 넘어온 RedisConnection을 StringRedisConnection으로 캐스팅하는 과정에서 ClassCastException이 발생했으므로 RedisConnection의 구체 클래스를 만드는 부분을 찾아봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JSqIk/dJMcagxCX28/UpkmOyYHfo30Mkw8tqipC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JSqIk/dJMcagxCX28/UpkmOyYHfo30Mkw8tqipC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JSqIk/dJMcagxCX28/UpkmOyYHfo30Mkw8tqipC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJSqIk%2FdJMcagxCX28%2FUpkmOyYHfo30Mkw8tqipC1%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;885&quot; height=&quot;399&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisTemplate의 executePipelined 메서드를 타고 들어가다보면 execute 메서드에서 RedisConnection을 전처리하는 코드를 찾을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNFz3j/dJMcajnyjM8/J6Sk8Hq5p1lvg6uvglc83k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNFz3j/dJMcajnyjM8/J6Sk8Hq5p1lvg6uvglc83k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNFz3j/dJMcajnyjM8/J6Sk8Hq5p1lvg6uvglc83k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNFz3j%2FdJMcajnyjM8%2FJ6Sk8Hq5p1lvg6uvglc83k%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;896&quot; height=&quot;221&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 RedisTemplate에서는 해당 메서드에 들어가면 별다른 작업 없이 그대로 connection을 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmX0eT/dJMcajuk5gx/HO4rnpwPeVPCsjOK1uK2P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmX0eT/dJMcajuk5gx/HO4rnpwPeVPCsjOK1uK2P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmX0eT/dJMcajuk5gx/HO4rnpwPeVPCsjOK1uK2P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmX0eT%2FdJMcajuk5gx%2FHO4rnpwPeVPCsjOK1uK2P0%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;838&quot; height=&quot;376&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Spring Boot 3.x 버전에서는 RedisTemplate를 상속한 StringRedisTemplate에서 차이가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StringRedisTemplate에서 이 메서드를 오버라이딩하여 새로운 프록시 connection을 생성하여 반환하기 때문에 위의 코드에서 StringRedisConnection으로 캐스팅이 가능했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;725&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/94wKu/dJMcaflcGnk/ZvunPxRFKGemaaTpvfcRX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/94wKu/dJMcaflcGnk/ZvunPxRFKGemaaTpvfcRX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/94wKu/dJMcaflcGnk/ZvunPxRFKGemaaTpvfcRX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F94wKu%2FdJMcaflcGnk%2FZvunPxRFKGemaaTpvfcRX0%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;1223&quot; height=&quot;725&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;725&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 부분이 필요없다고 판단했는지, 4.0으로 넘어오면서 해당 부분을 삭제한 커밋(&lt;a href=&quot;https://github.com/spring-projects/spring-data-redis/commit/3a930ceeb521753b11f33cdbfba6474118928824&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3a930ce&lt;/a&gt;)을 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변경 때문에 RedisTemplate에서 그대로 리턴된 connection을 StringRedisConnection으로 캐스팅하는 과정에서 예외가 발생하게 되었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docs에도 여전히 예제 코드로 캐스팅을 하는 부분이 남아있었기 때문에 이 부분을 수정하면 좋을 것 같다는 생각을 했다.&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;856&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGCJ1x/dJMcajuk5hu/HlxLxRzZodedoe4X53nS7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGCJ1x/dJMcajuk5hu/HlxLxRzZodedoe4X53nS7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGCJ1x/dJMcajuk5hu/HlxLxRzZodedoe4X53nS7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGCJ1x%2FdJMcajuk5hu%2FHlxLxRzZodedoe4X53nS7k%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;856&quot; height=&quot;373&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트코드에서도 단순히 외부에서 StringRedisConnection 프록시 객체를 만들어 사용한 부분을 보고, 예제 코드도 테스트 코드와 결이 비슷하다 생각해 동일하게 수정 후 PR을 올렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpDgRU/dJMb996mMFT/mD8gpZhDaulkzfCIpldvgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpDgRU/dJMb996mMFT/mD8gpZhDaulkzfCIpldvgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpDgRU/dJMb996mMFT/mD8gpZhDaulkzfCIpldvgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpDgRU%2FdJMb996mMFT%2FmD8gpZhDaulkzfCIpldvgk%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;907&quot; height=&quot;310&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;310&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;4704&quot; data-origin-height=&quot;3054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VNcCM/dJMcahJ6Z9L/Pa0o6dXhdcA5Rbz6o8Nuf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VNcCM/dJMcahJ6Z9L/Pa0o6dXhdcA5Rbz6o8Nuf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VNcCM/dJMcahJ6Z9L/Pa0o6dXhdcA5Rbz6o8Nuf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVNcCM%2FdJMcahJ6Z9L%2FPa0o6dXhdcA5Rbz6o8Nuf0%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;4704&quot; height=&quot;3054&quot; data-origin-width=&quot;4704&quot; data-origin-height=&quot;3054&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;정말 별거 아닌 내용이기도 하고 PR올릴 때 엄청 떨리고 무섭기도 했지만 많은 사람들이 사용하는 코드에 조금이나마 기여를 했다는 생각에 보람찼다!&lt;/p&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 title=&quot;PR 링크&quot; href=&quot;https://github.com/spring-projects/spring-data-redis/pull/3281&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-data-redis/pull/3281&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770646138013&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;Fix example code in Pipelining docs by wlsh44 &amp;middot; Pull Request #3281 &amp;middot; spring-projects/spring-data-redis&quot; data-og-description=&quot;Issue When executing the example code from the Pipelining documentation using Spring Data Redis 4.0.0, a ClassCastException is thrown while attempting to cast RedisConnection to StringRedisConnecti...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-data-redis/pull/3281&quot; data-og-url=&quot;https://github.com/spring-projects/spring-data-redis/pull/3281&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ehfPUK/dJMb9jOiMsE/ilFBqa06tDT3aJg3Z0vvJk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/iwCci/dJMb9iaMVUv/zpNL2nrFJKI4l2sgvdNxUK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-redis/pull/3281&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-data-redis/pull/3281&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ehfPUK/dJMb9jOiMsE/ilFBqa06tDT3aJg3Z0vvJk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/iwCci/dJMb9iaMVUv/zpNL2nrFJKI4l2sgvdNxUK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;Fix example code in Pipelining docs by wlsh44 &amp;middot; Pull Request #3281 &amp;middot; spring-projects/spring-data-redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Issue When executing the example code from the Pipelining documentation using Spring Data Redis 4.0.0, a ClassCastException is thrown while attempting to cast RedisConnection to StringRedisConnecti...&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;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770646190202&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;Pipelining :: Spring Data Redis&quot; data-og-description=&quot;The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. LettuceConnectionFactory factory = // ... factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot; data-og-url=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.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;Pipelining :: Spring Data Redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. LettuceConnectionFactory factory = // ... factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&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>
      <category>redis</category>
      <category>spring</category>
      <category>오픈소스</category>
      <category>컨트리뷰터</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/41</guid>
      <comments>https://wlsh44.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 9 Feb 2026 23:15:03 +0900</pubDate>
    </item>
    <item>
      <title>webSocket convertAndSendToUser 예외 처리 동작과정 및 삽질기</title>
      <link>https://wlsh44.tistory.com/40</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;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;subscribe 과정에서 예외가 발생할 경우, 구독에 실패하며 예외 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 동작하는 최종 코드입니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final AuthChannelInterceptor authChannelInterceptor;
    private final StompExceptionHandler stompExceptionHandler;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.setErrorHandler(stompExceptionHandler)
                .addEndpoint(&quot;/chat-game&quot;)
                .addInterceptors()
                .setAllowedOriginPatterns(&quot;*&quot;);
//                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker(&quot;/sub&quot;, &quot;/queue&quot;);
        registry.setApplicationDestinationPrefixes(&quot;/pub&quot;, &quot;/sub&quot;);
        registry.setUserDestinationPrefix(&quot;/user&quot;);
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(authChannelInterceptor);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class WebSocketGlobalExceptionHandler {

    private final SimpMessageSendingOperations messageTemplate;

    @MessageExceptionHandler(IllegalArgumentException.class)
    public void handle(IllegalArgumentException ex, @Header(StompHeaderAccessor.SESSION_ID_HEADER) String sessionId) {
        log.info(ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());

        SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        accessor.setContentType(MimeTypeUtils.APPLICATION_JSON);
        accessor.setSessionId(sessionId);

        messageTemplate.convertAndSendToUser(
                sessionId,
                &quot;/queue/errors&quot;,
                errorResponse,
                accessor.getMessageHeaders()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Controller
@RequiredArgsConstructor
public class PlayerStompController {

    @SubscribeMapping(&quot;/room/{roomId}&quot;)
    public void connect(
            @DestinationVariable(&quot;roomId&quot;) Long roomId,
            @Header(StompHeaderAccessor.SESSION_ID_HEADER) String sessionId,
            @Authenticated AuthContext authContext
    ) {
		new IllegalArgumentException(&quot;방 입장 실패&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4024&quot; data-origin-height=&quot;2294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxNv6/btsPWuiBTsJ/TkcSUCEnR5Urbu5m0tqDxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxNv6/btsPWuiBTsJ/TkcSUCEnR5Urbu5m0tqDxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxNv6/btsPWuiBTsJ/TkcSUCEnR5Urbu5m0tqDxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxNv6%2FbtsPWuiBTsJ%2FTkcSUCEnR5Urbu5m0tqDxK%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;4024&quot; height=&quot;2294&quot; data-origin-width=&quot;4024&quot; data-origin-height=&quot;2294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;convertAndSendToUser 동작 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 삽질을 하면서 알아낸 convertAndSendToUser의 동작 과정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLRLaX/btsPWVG8xi8/ngSABV3LJ4wNbjGOnZBr1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLRLaX/btsPWVG8xi8/ngSABV3LJ4wNbjGOnZBr1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLRLaX/btsPWVG8xi8/ngSABV3LJ4wNbjGOnZBr1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLRLaX%2FbtsPWVG8xi8%2FngSABV3LJ4wNbjGOnZBr1k%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;1091&quot; height=&quot;508&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음 SimpMessageSendingOperations의 구현체인 SimpMessagingTemplate에서 convertAndSendToUser를 보면 config에서 설정한 destinationPrefix와 파라미터로 넘어온 user(현재는 sessionId), destination인 /queue/errors를 붙여 /{prefix}/{userName}/queue/errors 형식의 새로운 destination을 만듭니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx1mQG/btsPXtDCmHk/4dKLfHUP0N8FpOp4G3szjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx1mQG/btsPXtDCmHk/4dKLfHUP0N8FpOp4G3szjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx1mQG/btsPXtDCmHk/4dKLfHUP0N8FpOp4G3szjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx1mQG%2FbtsPXtDCmHk%2F4dKLfHUP0N8FpOp4G3szjk%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;824&quot; height=&quot;423&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvmDsv/btsPV1HSDZl/fj34Q1C9Qvn7bqKceQ7VH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvmDsv/btsPV1HSDZl/fj34Q1C9Qvn7bqKceQ7VH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvmDsv/btsPV1HSDZl/fj34Q1C9Qvn7bqKceQ7VH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvmDsv%2FbtsPV1HSDZl%2Ffj34Q1C9Qvn7bqKceQ7VH1%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;972&quot; height=&quot;293&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 convertAndSend 메서드를 계속 따라가다 보면 어떤 handler를 통해 처리를 할지 정하는 코드가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler에는 config 설정에 따라 SimpleBrokerMessageHandler와 UserDestinationMessageHandler가 존재하는데, 특정 유저에게 메시지를 보내는 부분을 보기 위해서는 UserDestinationMessageHandler를 봐야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIIeqp/btsPWmrkhjt/Zcx2NMHqkroqAsx0pdRiP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIIeqp/btsPWmrkhjt/Zcx2NMHqkroqAsx0pdRiP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIIeqp/btsPWmrkhjt/Zcx2NMHqkroqAsx0pdRiP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIIeqp%2FbtsPWmrkhjt%2FZcx2NMHqkroqAsx0pdRiP0%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;1082&quot; height=&quot;361&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;361&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;1095&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bef2EN/btsPVEFKq7Y/4bZzb6HcsQKnf6hsdXy0s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bef2EN/btsPVEFKq7Y/4bZzb6HcsQKnf6hsdXy0s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bef2EN/btsPVEFKq7Y/4bZzb6HcsQKnf6hsdXy0s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbef2EN%2FbtsPVEFKq7Y%2F4bZzb6HcsQKnf6hsdXy0s1%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;1095&quot; height=&quot;504&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;504&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;1095&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BRtzO/btsPUppoWxY/mwNJBqoc0J2v3YqnQkmUZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BRtzO/btsPUppoWxY/mwNJBqoc0J2v3YqnQkmUZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BRtzO/btsPUppoWxY/mwNJBqoc0J2v3YqnQkmUZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBRtzO%2FbtsPUppoWxY%2FmwNJBqoc0J2v3YqnQkmUZ0%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;1095&quot; height=&quot;247&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 UserDestinationMessageHandler의 resolveDestination을 따라가면 parseMessage 메서드를 볼 수 있는데, 여기에서 /{prefix}/{userName}/queue/errors을 /queue/errors-{prefix}{userName}의 형태로 다시 변경을 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B33D0/btsPUr1QtPk/AP8HzQNvkwzKrKYaZKT2C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B33D0/btsPUr1QtPk/AP8HzQNvkwzKrKYaZKT2C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B33D0/btsPUr1QtPk/AP8HzQNvkwzKrKYaZKT2C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB33D0%2FbtsPUr1QtPk%2FAP8HzQNvkwzKrKYaZKT2C0%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;1088&quot; height=&quot;492&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;492&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;1098&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NUWSe/btsPVj9IuHn/gipnsdSVLKzq1sgadUh85K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NUWSe/btsPVj9IuHn/gipnsdSVLKzq1sgadUh85K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NUWSe/btsPVj9IuHn/gipnsdSVLKzq1sgadUh85K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNUWSe%2FbtsPVj9IuHn%2FgipnsdSVLKzq1sgadUh85K%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;1098&quot; height=&quot;418&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 handleMessage를 계속 따라가다 보면 SimpBrokerMessageHandler의 handleMessageInternal에서&amp;nbsp; checkDestinationPrefix를 통해 config에서 enableSimpleBroker로 지정했던 prefix들을 체크하는 과정을 거칩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsR6TP/btsPTQHg2QZ/MesCTeQetGMPQV14d086H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsR6TP/btsPTQHg2QZ/MesCTeQetGMPQV14d086H0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsR6TP/btsPTQHg2QZ/MesCTeQetGMPQV14d086H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsR6TP%2FbtsPTQHg2QZ%2FMesCTeQetGMPQV14d086H0%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;1775&quot; height=&quot;191&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;191&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. enableSimpleBroker 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-1. /queue 추가 x&lt;/h4&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt;  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
//      registry.enableSimpleBroker(&quot;/sub&quot;, &quot;/queue&quot;);
      registry.enableSimpleBroker(&quot;/sub&quot;);
      registry.setApplicationDestinationPrefixes(&quot;/pub&quot;, &quot;/sub&quot;);
      registry.setUserDestinationPrefix(&quot;/user&quot;);
  }
&lt;/code&gt;&lt;/pre&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;1164&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2pB5N/btsPVcQhPcv/D63bxDUR0zxVKCsqXAoLgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2pB5N/btsPVcQhPcv/D63bxDUR0zxVKCsqXAoLgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2pB5N/btsPVcQhPcv/D63bxDUR0zxVKCsqXAoLgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2pB5N%2FbtsPVcQhPcv%2FD63bxDUR0zxVKCsqXAoLgK%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;1164&quot; height=&quot;331&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 checkDestinationPrefix에서 계속 false가 나와 메시지를 보내지 않았습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-2. /user 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 디버깅하다가 만약 enableSimpleBroker에 destinationPrefix를 추가하면 어떻게 될까 궁금해져서 해본 삽질입니다.&lt;/p&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt;  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
//      registry.enableSimpleBroker(&quot;/sub&quot;, &quot;/queue&quot;);
      registry.enableSimpleBroker(&quot;/sub&quot;, &quot;/queue&quot;, &quot;/user&quot;);
      registry.setApplicationDestinationPrefixes(&quot;/pub&quot;, &quot;/sub&quot;);
      registry.setUserDestinationPrefix(&quot;/user&quot;);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 하면 안 되는지 알기 위해서는&amp;nbsp;먼저 구독 과정을 알아봐야 합니다.&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;934&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u6dm7/btsPUgMAqza/X10vGKr3grIrp9vheugEwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u6dm7/btsPUgMAqza/X10vGKr3grIrp9vheugEwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u6dm7/btsPUgMAqza/X10vGKr3grIrp9vheugEwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu6dm7%2FbtsPUgMAqza%2FX10vGKr3grIrp9vheugEwK%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;934&quot; height=&quot;429&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;429&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;1067&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0VKah/btsPUVgXvpr/Qy4znP6RKaKDbfNmRzCYw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0VKah/btsPUVgXvpr/Qy4znP6RKaKDbfNmRzCYw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0VKah/btsPUVgXvpr/Qy4znP6RKaKDbfNmRzCYw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0VKah%2FbtsPUVgXvpr%2FQy4znP6RKaKDbfNmRzCYw0%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;1067&quot; height=&quot;361&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 /user/queue/errors에 대해 구독 요청을 보내면 handler를 선택하는 과정에서 UserDestinationMessageHandler 이전에 SimpleBrokerMessageHandler에서 먼저 /user/queue/errors에 대해서도 구독 등록 과정을 처리하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 상황이라면 위처럼 checkDestinationPrefix 메서드에서 /user/queue/errors 자체에 대한 처리를 실패하기 때문에 구독 등록을 하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/py5ec/btsPWtYh1lS/spK6lECLZ7HFcjPrPJW5Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/py5ec/btsPWtYh1lS/spK6lECLZ7HFcjPrPJW5Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/py5ec/btsPWtYh1lS/spK6lECLZ7HFcjPrPJW5Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpy5ec%2FbtsPWtYh1lS%2FspK6lECLZ7HFcjPrPJW5Pk%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;1004&quot; height=&quot;348&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 /queue/errors-{prefix}{userName} 형식으로 변경해 위의 과정을 다시 거치고, subscriptionRegistry에 구독 등록을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 enableSimpleBroker에 prefix를 추가하면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCLwgj/btsPURr4VIT/SY8DovvIKHVQLpEs6Z6mhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCLwgj/btsPURr4VIT/SY8DovvIKHVQLpEs6Z6mhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCLwgj/btsPURr4VIT/SY8DovvIKHVQLpEs6Z6mhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCLwgj%2FbtsPURr4VIT%2FSY8DovvIKHVQLpEs6Z6mhK%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;1131&quot; height=&quot;349&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;349&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;1081&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qY9kd/btsPWskMEGV/s0XIREKb2UGqBCK7GrnuZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qY9kd/btsPWskMEGV/s0XIREKb2UGqBCK7GrnuZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qY9kd/btsPWskMEGV/s0XIREKb2UGqBCK7GrnuZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqY9kd%2FbtsPWskMEGV%2Fs0XIREKb2UGqBCK7GrnuZ0%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;1081&quot; height=&quot;73&quot; data-origin-width=&quot;1081&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleBrokerMessageHandler에서 /user/queue/errors를 처리하는 부분에서 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 여기서 걸러져야 했을 경로가 true가 되기 때문에 /user/queue/errors가 subscriptionRegistry에 등록이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIq7Xx/btsPUOhNzmD/IctEQorKKFtN6n2yokh6W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIq7Xx/btsPUOhNzmD/IctEQorKKFtN6n2yokh6W0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIq7Xx/btsPUOhNzmD/IctEQorKKFtN6n2yokh6W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIq7Xx%2FbtsPUOhNzmD%2FIctEQorKKFtN6n2yokh6W0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;361&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 어차피 /user/queue/errors도 등록되고 /queue/errors-{prefix}{userName}도 등록되면 상관없는 거 아닌가 할 수 있지만, 위에 있는 addSubscription 메서드를 보면 아니라는 것을 금방 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 요청할 때 생기는 sub-0과 같은 id를 key값으로 putIfAbsent 메서드를 사용하고 있기 때문에 /user/queue/errors가 먼저 등록이 되면 동일 요청의 /queue/errors-{prefix}{userName}에 대해서는 구독 등록이 되지 않게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHKcwx/btsPUdCfolL/26Rp6XXVwO107tSiF5og51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHKcwx/btsPUdCfolL/26Rp6XXVwO107tSiF5og51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHKcwx/btsPUdCfolL/26Rp6XXVwO107tSiF5og51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHKcwx%2FbtsPUdCfolL%2F26Rp6XXVwO107tSiF5og51%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;1211&quot; height=&quot;407&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;407&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;1016&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlunCh/btsPW2MUyhP/ZA2kRQUJcXhdA7DKpbIq5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlunCh/btsPW2MUyhP/ZA2kRQUJcXhdA7DKpbIq5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlunCh/btsPW2MUyhP/ZA2kRQUJcXhdA7DKpbIq5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlunCh%2FbtsPW2MUyhP%2FZA2kRQUJcXhdA7DKpbIq5K%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;1016&quot; height=&quot;175&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 동일 구독 id에 대하여 /user/queue/errors를 반환하게 되고, 이는 해당 유저의 destination과 다른 값이기 때문에 메시지를 보내는데 실패하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. convertAndSendToUser&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. convertAndSendToUser API&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@MessageExceptionHandler(IllegalArgumentException.class)
public void handle(IllegalArgumentException ex, StompHeaderAccessor accessor) {
    log.info(ex.getMessage());
    ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
    String sessionId = requireNonNull(accessor.getSessionId());
    //messageTemplate.convertAndSendToUser(sessionId, &quot;/queue/errors&quot;, errorResponse, accessor.getMessageHeaders());
    messageTemplate.convertAndSendToUser(sessionId, &quot;/queue/errors&quot;, errorResponse);
}
&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;처음에는 별생각 없이 convertAndSendToUser이라는 메서드가 있다는 것만 알고 헤더 없이 위의 코드처럼 작성을 했었습니다.&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;941&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dioRif/btsPWZQbncd/SBCPBoOEomKKOonLHPmSok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dioRif/btsPWZQbncd/SBCPBoOEomKKOonLHPmSok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dioRif/btsPWZQbncd/SBCPBoOEomKKOonLHPmSok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdioRif%2FbtsPWZQbncd%2FSBCPBoOEomKKOonLHPmSok%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;941&quot; height=&quot;449&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;449&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;943&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJLa0Y/btsPTP2Jq0s/MJkKMwuhPRCyk2Yi62YYE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJLa0Y/btsPTP2Jq0s/MJkKMwuhPRCyk2Yi62YYE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJLa0Y/btsPTP2Jq0s/MJkKMwuhPRCyk2Yi62YYE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJLa0Y%2FbtsPTP2Jq0s%2FMJkKMwuhPRCyk2Yi62YYE1%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;943&quot; height=&quot;209&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;209&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;942&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AG29f/btsPWiP7LNT/qIwk9CbYQAKdCuPvTKiQxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AG29f/btsPWiP7LNT/qIwk9CbYQAKdCuPvTKiQxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AG29f/btsPWiP7LNT/qIwk9CbYQAKdCuPvTKiQxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAG29f%2FbtsPWiP7LNT%2FqIwk9CbYQAKdCuPvTKiQxk%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;942&quot; height=&quot;330&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 convertAndSendToUser를 거치다 보면 넘어온 파라미터로 Message 객체를 만들게 되는데, 이때 제가 사용한 convertAndSendToUser는 header를 null로 넘기기 때문에 doConvert 메서드 안에서 기본 MessageHeaders를 만들어 사용하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xcOpA/btsPWWF3xuU/9O0vZ4UejrJWvX5UoKqxNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xcOpA/btsPWWF3xuU/9O0vZ4UejrJWvX5UoKqxNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xcOpA/btsPWWF3xuU/9O0vZ4UejrJWvX5UoKqxNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxcOpA%2FbtsPWWF3xuU%2F9O0vZ4UejrJWvX5UoKqxNk%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;1179&quot; height=&quot;488&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;488&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;934&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdPhw/btsPUIvxefQ/kafEK1TgBLwo91TuNKHSh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdPhw/btsPUIvxefQ/kafEK1TgBLwo91TuNKHSh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdPhw/btsPUIvxefQ/kafEK1TgBLwo91TuNKHSh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdPhw%2FbtsPUIvxefQ%2FkafEK1TgBLwo91TuNKHSh0%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;934&quot; height=&quot;454&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 MessageHeaders에는 유저 정보가 들어있지 않기 때문에 targetDestination을 만들지 못하고 메시지를 보내지 못한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. STOMP header&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@MessageExceptionHandler(IllegalArgumentException.class)
public void handle(IllegalArgumentException ex, StompHeaderAccessor accessor) {
    log.info(ex.getMessage());
    ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
    String sessionId = requireNonNull(accessor.getSessionId());
    messageTemplate.convertAndSendToUser(sessionId, &quot;/queue/errors&quot;, errorResponse, accessor.getMessageHeaders());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 삽질로는 요청 때 들어왔던 accessor를 재활용했을 때 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, MESSAGE 요청에서 발생한 예외를 처리할 때는 문제가 되지 않지만 SUBSCRIBE를 할 때 발생한 예외의 경우 문제가 생깁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QITCl/btsPTy7WRWk/2L67WhufgAzx1kXnkreLSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QITCl/btsPTy7WRWk/2L67WhufgAzx1kXnkreLSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QITCl/btsPTy7WRWk/2L67WhufgAzx1kXnkreLSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQITCl%2FbtsPTy7WRWk%2F2L67WhufgAzx1kXnkreLSK%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;835&quot; height=&quot;417&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;417&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;1709&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF3XJA/btsPUQtbHIo/mP5Xmmk8lkgY5wAiRnxyN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF3XJA/btsPUQtbHIo/mP5Xmmk8lkgY5wAiRnxyN1/img.png&quot; data-alt=&quot;/{userName}/queue/errors-{prefix}{userName} 형식으로 파싱됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF3XJA/btsPUQtbHIo/mP5Xmmk8lkgY5wAiRnxyN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF3XJA%2FbtsPUQtbHIo%2FmP5Xmmk8lkgY5wAiRnxyN1%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;1709&quot; height=&quot;207&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/{userName}/queue/errors-{prefix}{userName} 형식으로 파싱됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 간단한데, 우선 header에 messageType이 SUBSCRIBE이기 때문에 parseSubscriptionMessage에서 메시지를 처리하게 되기 때문에 targetDestination이 올바르게 파싱 되지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;억지로 simpMessageType을 MESSAGE로 바꾼다 하더라도 현재 Message의 stompCommand가 Subscribe이기 때문에 프런트에서 올바르게 처리할 수가 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;번외&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;convertAndSendToUser의 user&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dawGaM/btsPViv9GjD/EabBxtTcBlBUiJxQgc2oqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dawGaM/btsPViv9GjD/EabBxtTcBlBUiJxQgc2oqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dawGaM/btsPViv9GjD/EabBxtTcBlBUiJxQgc2oqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdawGaM%2FbtsPViv9GjD%2FEabBxtTcBlBUiJxQgc2oqK%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;859&quot; height=&quot;288&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 user에 어떤 값을 넣어야 하는지 몰라서 삽질을 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwSCRT/btsPTCJjH7P/RujcLLvch6tf1qFhI9U8Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwSCRT/btsPTCJjH7P/RujcLLvch6tf1qFhI9U8Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwSCRT/btsPTCJjH7P/RujcLLvch6tf1qFhI9U8Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwSCRT%2FbtsPTCJjH7P%2FRujcLLvch6tf1qFhI9U8Wk%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;1087&quot; height=&quot;578&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 삽질한 결과, 기본적으로는 sessionId를 user 값으로 사용하면 되며, sessionId가 싫다면 SimpMessageHeaderAccessor에 setUser 메서드를 통해 Principal 객체를 등록하여 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 userName과 sessionId가 같지 않다면 getSessionIdsByUser 메서드를 통해 sessionId를 가져오는 작업을 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class AuthChannelInterceptor implements ChannelInterceptor {

    private final TokenExtractor tokenExtractor;
    private final JwtProvider jwtProvider;

    @Override
    public Message&amp;lt;?&amp;gt; preSend(Message&amp;lt;?&amp;gt; message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        ...

        accessor.setUser((Principal) () -&amp;gt; jwtClaim.id());

        return message;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@MessageExceptionHandler(IllegalArgumentException.class)
public void handle(Principal principal, IllegalArgumentException ex, @Header(StompHeaderAccessor.SESSION_ID_HEADER) String sessionId) {
      log.info(ex.getMessage());
      ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());

      SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
      accessor.setContentType(MimeTypeUtils.APPLICATION_JSON);
      accessor.setSessionId(sessionId);

      messageTemplate.convertAndSendToUser(
              principal.getName(),
              &quot;/queue/errors&quot;,
              errorResponse,
              accessor.getMessageHeaders()
      );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위처럼 작성할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;a href=&quot;https://jiangxy.github.io/websocket-debug-tool/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jiangxy.github.io/websocket-debug-tool/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>convertAndSendToUser</category>
      <category>websocket</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/40</guid>
      <comments>https://wlsh44.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 18 Aug 2025 12:26:23 +0900</pubDate>
    </item>
    <item>
      <title>@NaturalId와 캐싱 및 동작 방식</title>
      <link>https://wlsh44.tistory.com/39</link>
      <description>&lt;h1&gt;@NaturalId&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NaturalId는 NaturalId(자연 키) 임을 명시해 주고, 캐싱과 같은 추가적인 기능을 제공하는 하이버네이트의 어노테이션입니다.&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의 어노테이션이 아니기 때문에 캐싱을 사용하려면 하이버네이트의 API를 사용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.hibernate.annotations.NaturalId;

@Entity
@Getter
@NoArgsConstructor
public class Member {

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

    @NaturalId
    private String email;

    public Member(String email) {
        this.email = email;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager entityManager;
    private final MemberSpringJpaRepository memberJpaRepository;

    public Member save(Member member) {
        return memberJpaRepository.save(member);
    }

    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        return memberJpaRepository.findById(id);
    }

    public Optional&amp;lt;Member&amp;gt; findByEmail(String email) {
        return memberJpaRepository.findByEmail(email);
    }

    /**
     * NaturalId 전용 API를 이용한 조회 메서드
     */
    public Optional&amp;lt;Member&amp;gt; findByEmailWithNatural(String email) {
        return entityManager.unwrap(Session.class)
                .bySimpleNaturalId(Member.class)
                .loadOptional(email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 API를 사용하기 위해 findByEmailWithNatural 메서드에 있는 것처럼 SessionImpl을 가져와서 메서드를 이용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 JPA의 쿼리와 다른 점을 확인해보기 위해 두 메서드를 만들었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findByEmail: Spring Data Jpa에서 제공하는 이메일 조회 쿼리&lt;/li&gt;
&lt;li&gt;findByEmailWithNatural: 하이버네이트의 Natural Id 캐싱을 이용한 이메일 조회 쿼리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@Transactional
record MemberCacheTest(
    MemberRepository memberRepository,
    EntityManager em
) {

    @Test
    void 쿼리가_한_번_발생해야_함1() throws Exception {
        // given
        Member member = memberRepository.save(new Member(&quot;email@email.com&quot;));
        em.flush();
        em.clear();

        // when then
        memberRepository.findById(member.getId()).orElseThrow();
        memberRepository.findById(member.getId()).orElseThrow();
    }

    @Test
    void 단순_조회하는_경우_쿼리가_두_번_발생해야_함() throws Exception {
        // given
        String email = &quot;email@email.com&quot;;
        memberRepository.save(new Member(email));
        em.flush();
        em.clear();

        // when then
        memberRepository.findByEmail(email).orElseThrow();
        memberRepository.findByEmail(email).orElseThrow();
    }

    @Test
    void naturalId용_api로_조회하는_경우_쿼리가_한_번_발생해야_함() throws Exception {
        // given
        String email = &quot;email@email.com&quot;;
        Member member = memberRepository.save(new Member(email));
        em.flush();
        em.clear();

        // when then
        memberRepository.findById(member.getId()).orElseThrow();
        memberRepository.findByEmailWithNatural(email).orElseThrow();
    }

}&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;하나씩 실행해보면서 결과를 확인해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void 쿼리가_한_번_발생해야_함1() throws Exception {
    // given
    Member member = memberRepository.save(new Member(&quot;email@email.com&quot;));
    em.flush();
    em.clear();

    // when then
    memberRepository.findById(member.getId()).orElseThrow();
    memberRepository.findById(member.getId()).orElseThrow();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFbsP5/btsPhuX1FOB/kqMtLQgOeI51PN1aaoenp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFbsP5/btsPhuX1FOB/kqMtLQgOeI51PN1aaoenp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFbsP5/btsPhuX1FOB/kqMtLQgOeI51PN1aaoenp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFbsP5%2FbtsPhuX1FOB%2FkqMtLQgOeI51PN1aaoenp1%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;973&quot; height=&quot;360&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jpa는 기본적으로 같은 세션 안에서 데이터를 조회할 경우 1차 캐시에 엔티티를 저장하기 때문에 조회를 통해 캐시에 저장된 데이터를 Id 값을 통해 다시 조회를 하게 되면 쿼리가 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위와 같은 케이스에서는 첫 번째 조회 시 쿼리가 한 번 발생하고 이후 조회에서는 캐시에 있는 데이터를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Test
void 단순_조회하는_경우_쿼리가_두_번_발생해야_함() throws Exception {
	  // given
    String email = &quot;email@email.com&quot;;
    Member member = memberRepository.save(new Member(email));
    em.flush();
    em.clear();

    // when then
    memberRepository.findById(member.getId()).orElseThrow();
    memberRepository.findByEmail(email).orElseThrow();
}
&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;1053&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2YLuH/btsPh7uvQey/DOvbNSkcj7LJugbKwukJJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2YLuH/btsPh7uvQey/DOvbNSkcj7LJugbKwukJJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2YLuH/btsPh7uvQey/DOvbNSkcj7LJugbKwukJJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2YLuH%2FbtsPh7uvQey%2FDOvbNSkcj7LJugbKwukJJ0%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;1053&quot; height=&quot;368&quot; data-origin-width=&quot;1053&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;단순 Spring Data를 이용하여 @NaturalId를 조회하는 경우에는 일반적으로 쿼리가 두 번 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Test
void naturalId용_api로_조회하는_경우_쿼리가_한_번_발생해야_함() throws Exception {
    // given
    String email = &quot;email@email.com&quot;;
    memberRepository.save(new Member(email));
    em.flush();
    em.clear();

    // when then
    memberRepository.findById(member.getId()).orElseThrow();
    memberRepository.findByEmailWithNatural(email).orElseThrow();
}
&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;1010&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7B2Yt/btsPgszY2SP/UZF1ftN0n5nY78BfAOeUek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7B2Yt/btsPgszY2SP/UZF1ftN0n5nY78BfAOeUek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7B2Yt/btsPgszY2SP/UZF1ftN0n5nY78BfAOeUek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7B2Yt%2FbtsPgszY2SP%2FUZF1ftN0n5nY78BfAOeUek%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;1010&quot; height=&quot;370&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate의 naturalId 조회용 api를 사용하는 경우 1차 캐시에 저장된 데이터를 먼저 조회하기 때문에 쿼리가 한 번만 발생한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 방식&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Optional&amp;lt;Member&amp;gt; findByEmailWithNatural(String email) {
        return entityManager.unwrap(Session.class)
                .bySimpleNaturalId(Member.class)
                .loadOptional(email);
}&lt;/code&gt;&lt;/pre&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;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0FEx1/btsPhuDKGnf/pfBDXyowx80MUH9GEMGhs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0FEx1/btsPhuDKGnf/pfBDXyowx80MUH9GEMGhs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0FEx1/btsPhuDKGnf/pfBDXyowx80MUH9GEMGhs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0FEx1%2FbtsPhuDKGnf%2FpfBDXyowx80MUH9GEMGhs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&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;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR0V1Q/btsPfGFyvBg/i6G8fIa8IQecBtDW7KSMF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR0V1Q/btsPfGFyvBg/i6G8fIa8IQecBtDW7KSMF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR0V1Q/btsPfGFyvBg/i6G8fIa8IQecBtDW7KSMF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR0V1Q%2FbtsPfGFyvBg%2Fi6G8fIa8IQecBtDW7KSMF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 bySimpleNaturalId(Member.class) 메서드를 따라가면 BaseNaturalIdLoadAccessImpl를 상속받은 SimpleNaturalIdLoadAccessImpl객체를 만드는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQuC17/btsPgUiBI2P/GckFtvNp63d7Ax9eU0lMbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQuC17/btsPgUiBI2P/GckFtvNp63d7Ax9eU0lMbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQuC17/btsPgUiBI2P/GckFtvNp63d7Ax9eU0lMbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQuC17%2FbtsPgUiBI2P%2FGckFtvNp63d7Ax9eU0lMbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 loadOptional(email) 부분을 계속 따라가면 위에서 확인했던 BaseNaturalIdLoadAccessImpl에서 핵심 구현 메서드인 doLoad를 찾을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tBqOz/btsPgidbJnx/o2Cy7BuD3n50bpbNK3hpkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tBqOz/btsPgidbJnx/o2Cy7BuD3n50bpbNK3hpkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tBqOz/btsPgidbJnx/o2Cy7BuD3n50bpbNK3hpkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtBqOz%2FbtsPgidbJnx%2Fo2Cy7BuD3n50bpbNK3hpkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심이 되는 부분은 157줄과 166줄입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;157줄에서 파라미터로 넘긴 naturalId 값을 통해 캐시에 데이터가 있는지 확인하고 있으면 해당 엔티티의 Id 값을, 없으면 null 값을 리턴합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 값을 가지고 166줄에서 Id 값이 있으면 캐시에서, 없으면 기존 파라미터로 넘어온 값을 통해 쿼리를 만들어 db에서 데이터를 가져오는 작업을 하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Test
void naturalId용_api로_조회하는_경우_쿼리가_한_번_발생해야_함2() throws Exception {
    // given
    String email = &quot;email@email.com&quot;;
    Member member = memberRepository.save(new Member(email));
    em.flush();
    em.clear();

    // when then
    memberRepository.findByEmailWithNatural(email).orElseThrow();
    memberRepository.findByEmailWithNatural(email).orElseThrow();
}
&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;처음 데이터를 조회할 때는 1차 캐시에 엔티티가 없기 때문에 쿼리가 발생하고, 두 번째 조회에는 캐시에서 데이터를 가져오기 때문에 쿼리가 발생하지 않아야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ojLeV/btsPh5KfSY0/ebZ0DRjOrtqUnQFayiR341/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ojLeV/btsPh5KfSY0/ebZ0DRjOrtqUnQFayiR341/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ojLeV/btsPh5KfSY0/ebZ0DRjOrtqUnQFayiR341/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FojLeV%2FbtsPh5KfSY0%2FebZ0DRjOrtqUnQFayiR341%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 조회를 하는 경우 캐시에 데이터가 없기 때문에 cachedResolution값이 null이고, db에서 데이터를 가져오는 방식을 선택하기 때문에 쿼리가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu4eLy/btsPh7gZYYb/iCKSTpVYVaXE8hT5tm5Bgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu4eLy/btsPh7gZYYb/iCKSTpVYVaXE8hT5tm5Bgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu4eLy/btsPh7gZYYb/iCKSTpVYVaXE8hT5tm5Bgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu4eLy%2FbtsPh7gZYYb%2FiCKSTpVYVaXE8hT5tm5Bgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 조회하는 경우 캐시에 엔티티가 있기 때문에 cachedResolution이 Id 값인 1로 리턴되고, 해당 값을 가지고getIdentifierLoadAccess().load(cachedResolution)를 통해 캐시에서 데이터를 가져오기 때문에 쿼리가 추가로 발생하지 않는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/17221/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/17221/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thorben-janssen.com/naturalid-good-way-persist-natural-ids-hibernate/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://thorben-janssen.com/naturalid-good-way-persist-natural-ids-hibernate/&lt;/a&gt;&lt;/p&gt;</description>
      <category>JPA</category>
      <category>JPA</category>
      <category>naturalid</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/39</guid>
      <comments>https://wlsh44.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 14 Jul 2025 10:49:29 +0900</pubDate>
    </item>
    <item>
      <title>Intellij에서 Spring 프로젝트 이름 변경</title>
      <link>https://wlsh44.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1870&quot; data-origin-height=&quot;1652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qNTcQ/btsNZZZvsBL/iPzDnxSp5vxDwOXVSHKMM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qNTcQ/btsNZZZvsBL/iPzDnxSp5vxDwOXVSHKMM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qNTcQ/btsNZZZvsBL/iPzDnxSp5vxDwOXVSHKMM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqNTcQ%2FbtsNZZZvsBL%2FiPzDnxSp5vxDwOXVSHKMM0%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;575&quot; height=&quot;508&quot; data-origin-width=&quot;1870&quot; data-origin-height=&quot;1652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;demo 프로젝트를 생성하고 프로젝트 이름을 변경해 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;settings.gradle에 있는 rootProject.name을 변경하려는 프로젝트 이름으로 변경하고 gradle refresh를 해줍니다.&lt;/p&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;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LirW1/btsNZbzz4VM/htVnzzlqcnrk89fCWb7UXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LirW1/btsNZbzz4VM/htVnzzlqcnrk89fCWb7UXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LirW1/btsNZbzz4VM/htVnzzlqcnrk89fCWb7UXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLirW1%2FbtsNZbzz4VM%2FhtVnzzlqcnrk89fCWb7UXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;293&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;394&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;730&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGogp5/btsNYU5SEH3/htP4wujCnkD7mT6UbBhpT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGogp5/btsNYU5SEH3/htP4wujCnkD7mT6UbBhpT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGogp5/btsNYU5SEH3/htP4wujCnkD7mT6UbBhpT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGogp5%2FbtsNYU5SEH3%2FhtP4wujCnkD7mT6UbBhpT0%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;537&quot; height=&quot;300&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;408&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;사실 저 부분은 Intellij에서 인식하는 프로젝트 이름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intellij의 프로젝트 설정(command + ;)에 들어가서 프로젝트 이름을 변경해도 위와 같이 보이게 되지만 settings.gradle의 값을 변경해 줘도 Intellij에서 자동으로 프로젝트 이름을 변경해 주기 때문에 따로 설정할 필요는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 .idea/.name 파일도 Intellij를 종료 후 다시 프로젝트를 불러오면 현재 프로젝트 이름에 맞게 변경이 되므로 따로 수정할 필요는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 settings.gradle을 수정하고도 Intellij에서 인식을 제대로 못한다면 위에서 언급한 두 설정을 바꿔주시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 11.59.48.png&quot; data-origin-width=&quot;2272&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmLuLa/btsNYBlaFoE/xblT2SsqUrqz2KckyKgQY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmLuLa/btsNYBlaFoE/xblT2SsqUrqz2KckyKgQY0/img.png&quot; data-alt=&quot;프로젝트 설정(command + ;)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmLuLa/btsNYBlaFoE/xblT2SsqUrqz2KckyKgQY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmLuLa%2FbtsNYBlaFoE%2FxblT2SsqUrqz2KckyKgQY0%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;664&quot; height=&quot;561&quot; data-filename=&quot;스크린샷 2025-05-16 오전 11.59.48.png&quot; data-origin-width=&quot;2272&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 설정(command + ;)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xm41b/btsNYufvzHV/2zA2sgXnDkAHyOewXdW7A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xm41b/btsNYufvzHV/2zA2sgXnDkAHyOewXdW7A0/img.png&quot; data-alt=&quot;.name&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xm41b/btsNYufvzHV/2zA2sgXnDkAHyOewXdW7A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXm41b%2FbtsNYufvzHV%2F2zA2sgXnDkAHyOewXdW7A0%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;487&quot; height=&quot;265&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.name&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디렉터리 이름까지 변경하는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 디렉터리 이름까지 변경하고 싶은 분들만 참고해 주시고, 굳이 진행하지 않으셔도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intellij 종료 후 디렉터리 이름 변경 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;192&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVsokC/btsNYnUWrVs/IvhpWiFGQpMaFXMIXr9KHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVsokC/btsNYnUWrVs/IvhpWiFGQpMaFXMIXr9KHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVsokC/btsNYnUWrVs/IvhpWiFGQpMaFXMIXr9KHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVsokC%2FbtsNYnUWrVs%2FIvhpWiFGQpMaFXMIXr9KHK%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;192&quot; height=&quot;210&quot; data-origin-width=&quot;192&quot; data-origin-height=&quot;210&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;700&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Un0rC/btsNZ1CSbwd/RSapHqkRXQQVyzHviDaPdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Un0rC/btsNZ1CSbwd/RSapHqkRXQQVyzHviDaPdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Un0rC/btsNZ1CSbwd/RSapHqkRXQQVyzHviDaPdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUn0rC%2FbtsNZ1CSbwd%2FRSapHqkRXQQVyzHviDaPdk%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;700&quot; height=&quot;424&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제대로 Intellij가 인식하는 경우 (디렉터리 이름 [프로젝트 이름])이었던 부분이 하나로 보이게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Intellij 종료없이 디렉터리 이름을 변경하고 Intellij에서 프로젝트를 불러오면 Intellij가 종료해야 할 프로젝트를 찾지 못하고 오히려 기존 프로젝트 이름을 가진 디렉터리를 새로 만들면서 렉이 걸리는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Intellij를 강제 종료 하고 다시 프로젝트를 불러오면 해결됩니다.&lt;/p&gt;</description>
      <category>에디터</category>
      <category>intellij</category>
      <category>프로젝트 이름 변경</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/38</guid>
      <comments>https://wlsh44.tistory.com/38#entry38comment</comments>
      <pubDate>Fri, 16 May 2025 12:12:30 +0900</pubDate>
    </item>
    <item>
      <title>[Java] assert 알아보기</title>
      <link>https://wlsh44.tistory.com/37</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Assert란&lt;/h3&gt;
&lt;pre id=&quot;code_1716178423485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assert 조건식;			//기본 형식
assert 조건식 : 에러 메시지;		//메시지 포함 형식

assert age &amp;gt;= 0;
assert age &amp;gt;= 0 : &quot;나이는 음수일 수 없습니다&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assert는 jdk 1.4에서 추가된 키워드로, 코드가 논리적으로 올바르게 작동하는지 확인하는 데 사용합니다.&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;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Assert, Exception 차이&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;1. assert의 쓰임새&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 if - exception의 간결한 표현이라고 생각할 수 있지만 둘의 쓰임새는 많이 다릅니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마틴 파울러의 리팩토링 책에서도 assert의 사용법에 대해 다음처럼 언급합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;bull; 코드에서 기본적으로 가정하고 있는 조건들을 주석이 아닌&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;nbsp;Assertion을 사용해서 보다 명시적으로 나타낼 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;bull;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Assertion은 '반드시  참이어야 하는' 가정을 검사할 때 사용한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;bull; Assertion이 없어도 &lt;/span&gt;프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성돼야 한다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;bull;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Assertion은 프로그램이 어떤 상태를 가정하는지 다른 개발자에게 알려주는 훌륭한 의사 소통 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분만 보더라도 exception처럼 비정상적인 흐름을 정상적인 흐름으로 돌리거나 하는 방식과는 쓰임새가 조금 다르다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;2. 컴파일 옵션&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;assert는 exception과는 다르게 기본적으로 비활성화 되어있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3976&quot; data-origin-height=&quot;2246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX8Kcp/btsHwbxRjNA/xY1ACdvLa3WNHcm2XGNSt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX8Kcp/btsHwbxRjNA/xY1ACdvLa3WNHcm2XGNSt1/img.png&quot; data-alt=&quot;프로그램이 문제없이 실행됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX8Kcp/btsHwbxRjNA/xY1ACdvLa3WNHcm2XGNSt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX8Kcp%2FbtsHwbxRjNA%2FxY1ACdvLa3WNHcm2XGNSt1%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;3976&quot; height=&quot;2246&quot; data-origin-width=&quot;3976&quot; data-origin-height=&quot;2246&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로그램이 문제없이 실행됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이는 jdk 1.4 이전 버전에서는 변수나 메서드 등에 assert라는 단어를 사용할 수 있었기 때문에, JVM에서는 assert를 추가하면서 이전 버전과 호환성을 맞추기 위해 해당 키워드를 비활성화시켜놓았다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;따라서 assert 키워드를 활성화하기 위해서는 컴파일 옵션에 &lt;b&gt;-ea&lt;/b&gt; (enable assertions)을 추가하여 사용해야 합니다. (어쩌면 이런 이유 때문에 assert는 개발용, 디버깅용으로만 사용되는 것 같기도 하네요)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xBo02/btsHt5e5YFu/geKnCBpayjxYBWU0iYT9O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xBo02/btsHt5e5YFu/geKnCBpayjxYBWU0iYT9O1/img.png&quot; data-alt=&quot;Modifiy options -&amp;amp;gt; Java -&amp;amp;gt; Add VM Options -&amp;amp;gt; -ea 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xBo02/btsHt5e5YFu/geKnCBpayjxYBWU0iYT9O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxBo02%2FbtsHt5e5YFu%2FgeKnCBpayjxYBWU0iYT9O1%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;1152&quot; height=&quot;788&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Modifiy options -&amp;gt; Java -&amp;gt; Add VM Options -&amp;gt; -ea 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Assert는 Error&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번은 1번의 연장선으로, assert는 Exception이 아닌 Error라는 점에서 차이점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assert에서 검증을 실패하면 AssertionError가 발생합니다.&lt;/p&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;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvVANq/btsHvZLn9ny/qQyqj27XgA10sEEgFWSe3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvVANq/btsHvZLn9ny/qQyqj27XgA10sEEgFWSe3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvVANq/btsHvZLn9ny/qQyqj27XgA10sEEgFWSe3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvVANq%2FbtsHvZLn9ny%2FqQyqj27XgA10sEEgFWSe3K%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;104&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java를 공부하면 Exception과 Error의 차이를 꼭 한 번쯤은 들어보게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/txGOg/btsHt0dXn50/jkOjqixwiPFyZ5BLvuBGF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/txGOg/btsHt0dXn50/jkOjqixwiPFyZ5BLvuBGF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/txGOg/btsHt0dXn50/jkOjqixwiPFyZ5BLvuBGF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtxGOg%2FbtsHt0dXn50%2FjkOjqixwiPFyZ5BLvuBGF1%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;500&quot; height=&quot;458&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Exception은 프로그램에 비정상적인 흐름이 발견되거나, 잘못된 값이 넘어오는 경우에 사용합니다. 대표적으로 비즈니스 로직의 예외를 잡아내는 데 사용하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 반면, Error는 StackOverFlowError나 OutOfMemoryError처럼 복구가 불가능한 치명적인 오류가 발생했을 때 사용됩니다. 그렇기 때문에 assert는 1번에서 언급한 것처럼 '반드시 참이어야 하는' 부분에서만 사용해야 하며, 이를 복구시키려는 코드를 작성을 권장하지 않는다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 assert는 다른 Error보다 개발자의 실수로 발생하기 더 쉽고, 이를 처리할 코드가 없다면 시스템이 종료되기 때문에 assert는 개발 단계나 디버깅 모드에서만 주로 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assert와 exception의 쓰임새가 많이 다르고, 실제 운영 단계에서는 assert를 사용하지 않도록 옵션을 끈다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 assert를 꼭 사용하고 싶다! 하시면 Spring의 Assert에 대해서도 한 번 알아보시는 것을 추천드립니다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>assert</category>
      <category>Exception</category>
      <category>Java</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/37</guid>
      <comments>https://wlsh44.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 20 May 2024 14:49:53 +0900</pubDate>
    </item>
    <item>
      <title>스프링6 @RequestMapping Handler 등록 방식 변화 (feat. @Controller)</title>
      <link>https://wlsh44.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 인프런의 스프링 강의 예제를 스프링 부트 3.1로 따라 치다가 겪은 문제점과 알게 된 점이 있어 정리해 보고자 글을 작성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 문제를 겪은 예제 코드는 다음과 같았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700124273837&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping
@ResponseBody
public interface OrderControllerV1 {

    @GetMapping(&quot;/v1/request&quot;)
    String request(@RequestParam(&quot;itemId&quot;) String itemId);

}

public class OrderControllerV1Impl implements OrderControllerV1 {

    @Override
    public String request(String itemId) {
        return &quot;ok&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP를 적용하기 전의 과정을 보여주기 위한 예제이기 때문에 따로 @Component가 포함된 @Controller 어노테이션을 사용하지 않고&amp;nbsp; 직접 controller 계층에도 인터페이스를 구현하는 예제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이상하게 간단한 예제였음에도 불구하고 예제를 잘 따라치고 있는지 확인해 보니 404 예외가 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9S23f/btsAuvKt8UB/VF6YC5HcfS8q1fmNqYoLNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9S23f/btsAuvKt8UB/VF6YC5HcfS8q1fmNqYoLNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9S23f/btsAuvKt8UB/VF6YC5HcfS8q1fmNqYoLNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9S23f%2FbtsAuvKt8UB%2FVF6YC5HcfS8q1fmNqYoLNk%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;591&quot; height=&quot;189&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 제공한 코드에서는 잘 작동해서 스프링 버전의 차이에 따른 문제가 있을거라 생각해 2.7 버전으로 내린 후 다시 실행을 해보니 잘 작동을 하였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;//스프링은 @Controller 또는 @RequestMapping 이 있어야 스프링 컨트롤러로 인식&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 어떤 차이가 있는지 고민을 하다가 다운받은 예제 코드에 위처럼 적혀있는 부분이 보여서 이에 대한 차이가 생겼다고 판단해 해당 키워드를 기반으로 검색을 하다가 관련된 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/22154&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이슈&lt;/a&gt;를 찾게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckH8nx/btsAnzgRyQX/9qgZVqHd43gUkfgIPRyjKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckH8nx/btsAnzgRyQX/9qgZVqHd43gUkfgIPRyjKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckH8nx/btsAnzgRyQX/9qgZVqHd43gUkfgIPRyjKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckH8nx%2FbtsAnzgRyQX%2F9qgZVqHd43gUkfgIPRyjKK%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;921&quot; height=&quot;253&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 스프링 6부터는 @Controller 없이는 @RequestMapping이 붙은 클래스는 handler로 등록이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스프링 6을 사용하는 스프링 부트 3에서는 처음에 봤던 예시가 동작하지 않았던 것을 알 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디버깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 어떻게 구현이 바뀌었는지 보고싶어 디버깅을 했고 생각보다 쉽게 찾을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwtMhx/btsAroL6Sgg/j40hmQTGkjeG7YRzAjgWT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwtMhx/btsAroL6Sgg/j40hmQTGkjeG7YRzAjgWT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwtMhx/btsAroL6Sgg/j40hmQTGkjeG7YRzAjgWT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwtMhx%2FbtsAroL6Sgg%2Fj40hmQTGkjeG7YRzAjgWT0%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;1126&quot; height=&quot;332&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler를 관리하는 AbstractHandlerMethodMapping 클래스의 processCandidateBean 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 isHandler라는 추상 메서드를 사용하는데, 해당 메서드를 구현한 RequestHandlerMethodMapping 클래스를 확인해 보면&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;670&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4FX1/btsAsxok4ta/kNpE3KIvN8mqesy6Rrps0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4FX1/btsAsxok4ta/kNpE3KIvN8mqesy6Rrps0k/img.png&quot; data-alt=&quot;스프링 5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4FX1/btsAsxok4ta/kNpE3KIvN8mqesy6Rrps0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4FX1%2FbtsAsxok4ta%2FkNpE3KIvN8mqesy6Rrps0k%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;670&quot; height=&quot;198&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링 5&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 5에서는 위와 같이 구현이 되어있는 것을 볼 수 있지만, 스프링 6에서는 @RequestMapping이 제거가 되어 있는 것을 볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHusEv/btsAqv5RCPe/zH5HsOeqSgcDSjiZexcwB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHusEv/btsAqv5RCPe/zH5HsOeqSgcDSjiZexcwB1/img.png&quot; data-alt=&quot;스프링 6&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHusEv/btsAqv5RCPe/zH5HsOeqSgcDSjiZexcwB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHusEv%2FbtsAqv5RCPe%2FzH5HsOeqSgcDSjiZexcwB1%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;617&quot; height=&quot;153&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스프링 6&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링6, 스프링 부트 3부터는 @RequestMapping은 @Controller나 @RestController 없이는 handler 등록이 되지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/22154&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-framework/issues/22154&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@controller</category>
      <category>@RequestMapping</category>
      <category>spring</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/36</guid>
      <comments>https://wlsh44.tistory.com/36#entry36comment</comments>
      <pubDate>Thu, 16 Nov 2023 19:51:38 +0900</pubDate>
    </item>
    <item>
      <title>게시글 좋아요 기능 개선기 (동시성, 데드락)</title>
      <link>https://wlsh44.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트로 간단한 sns를 만들면서, 게시글에 좋아요를 누르는 기능을 만드는 과정에서 생긴 여러 문제점과 개선하는 과정을 기록해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본적인 좋아요 기능 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 게시글(Post)과 좋아요(Like)가 일대다 연관관계에 있는 상태로 구현을 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686292773560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseTimeEntity {

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

    private int likeCount;
    
    public void increaseLikeCount() {
    	this.likeCount++;
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686292809894&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Like extends BaseTimeEntity {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;post_id&quot;)
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음처럼 LikeService를 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686292991477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class LikeService {

    private final MemberRepository memberRepository;
    private final PostRepository postRepository;
    private final LikeRepository likeRepository;

    public void like(Long memberId, Long postId) {
        Post post = getPost(postId);
        Member member = getMember(memberId);

        validateAlreadyLikedPost(memberId, postId);

    	likeRepository.save(new Like(post, member));
        post.increaseLikeCount();
    }

    private void validateAlreadyLikedPost(Long memberId, Long postId) {
        if (likeRepository.existsByMemberIdAndPostId(memberId, postId)) {
            throw new AlreadyLikedPostException(postId, memberId);
        }
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직은 단순히 memberId와 postId를 통해 Like가 존재하는지 확인한 후, 없으면 Like를 저장하고 Post의 likeCount를 늘리는 방식으로 흘러갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJ6oE/btsjiezLo0D/50I24kNYIlgiKtQpk7xQi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJ6oE/btsjiezLo0D/50I24kNYIlgiKtQpk7xQi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJ6oE/btsjiezLo0D/50I24kNYIlgiKtQpk7xQi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJ6oE%2FbtsjiezLo0D%2F50I24kNYIlgiKtQpk7xQi0%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;1485&quot; height=&quot;804&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성한 후 확인을 해보면 문제없이 성공한 것을 볼 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 - 동시성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 처음 sns를 설계할 때 여러 유저들이 사용할 것을 가정했고,&amp;nbsp;여러 시나리오 중 많은 사람들이 동시에 좋아요를 누르면 어떻게 될까 라는 의문을 가졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 100명이 거의 동시에 클릭을 했을 때를 가정한 테스트 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686294432580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;100명이 동시에 좋아요를 누를 경우 100개의 좋아요와 likeCount가 100개 올라야 함&quot;)
void likeTest_100_differentMembers() throws Exception {
    //given
    int threadCount = 100;
    Member member = memberRepository.save(getBasicMember());
    Post post = postRepository.save(getBasicPost(member));
    List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
    for (int i = 0; i &amp;lt; threadCount; i++) {
        members.add(memberRepository.save(getBasicMember()));
    }

    ExecutorService executorService = Executors.newFixedThreadPool(32);
    CountDownLatch latch = new CountDownLatch(threadCount);

    //when
    for (int i = 0; i &amp;lt; threadCount; i++) {
        int finalI = i;
        executorService.submit(() -&amp;gt; {
            try {
                likeService.like(members.get(finalI).getId(), post.getId());
            } finally {
                latch.countDown();
            }
        });
    }
    latch.await();

    //then
    List&amp;lt;Like&amp;gt; likes = likeRepository.findAll();
    Post findPost = postRepository.findById(post.getId()).get();
    assertThat(likes).hasSize(threadCount);
    assertThat(findPost.getLikeCount()).isEqualTo(threadCount);
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선 100명이 동시에 요청을 하기 위해 100명의 유저를 만듭니다.&lt;/li&gt;
&lt;li&gt;병렬 처리를 위한 ExecuterService와 모든 스레드들이 끝나는 것을 기다리기 위한 CountDownLatch를 만듭니다.&lt;/li&gt;
&lt;li&gt;100명이 동시에 하나의 게시글에 좋아요를 누릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 올바르게 구현이 되었다면 실제 db에 저장된 좋아요의 수와 게시글의 좋아요 수가 100개가 되어야 합니다.&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;967&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5eiRC/btsjhnX9xDC/9vnpc1wssT3ExAs7b4D0Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5eiRC/btsjhnX9xDC/9vnpc1wssT3ExAs7b4D0Yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5eiRC/btsjhnX9xDC/9vnpc1wssT3ExAs7b4D0Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5eiRC%2FbtsjhnX9xDC%2F9vnpc1wssT3ExAs7b4D0Yk%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;967&quot; height=&quot;283&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 85개의 좋아요 수가 증발한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 데이터 부정합이 발생하는 이유는 좋아요를 누를 때 Post를 select 한 후 그때 읽은 likeCount를 이용해 값을 update를 하기 때문에, 동시에 여러 트랜잭션이 값을 읽게 되었을 때 가장 마지막 commit의 결과가 최종적으로 반영되기 때문입니다.&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;1178&quot; data-origin-height=&quot;881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V2got/btsjnaWUTyB/09T55okhnweFtYk5b0Tcj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V2got/btsjnaWUTyB/09T55okhnweFtYk5b0Tcj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V2got/btsjnaWUTyB/09T55okhnweFtYk5b0Tcj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV2got%2FbtsjnaWUTyB%2F09T55okhnweFtYk5b0Tcj1%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;1178&quot; height=&quot;881&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;881&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;update 쿼리에서 likeCount가 0 + 1로 된 이유는 JPA의 select를 통한 데이터를 기준으로 더티 체킹이 진행됐고, 그에 따른 쿼리가 만들어졌기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;update 쿼리를 통한 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 구현의 문제점을 해결하는 방법으로는 update 쿼리를 직접 실행하는 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향과 조금 멀어지는 방식이지만 가장 쉽게 해결할 수 있어 해당 방법으로 코드를 수정해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686351674717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PostRepository extends JpaRepository&amp;lt;Post, Long&amp;gt; {

    @Modifying(clearAutomatically = true)
    @Query(&quot;update Post p set p.likeCount = p.likeCount + 1 where p.id = :postId&quot;)
    void increaseLikeCount(@Param(&quot;postId&quot;) Long postId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686351715354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void like(Long memberId, Long postId) {
    Post post = getPost(postId);
    Member member = getMember(memberId);

    validateAlreadyLikedPost(memberId, postId);

    likeRepository.save(new Like(post, member));
    postRepository.increaseLikeCount(postId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 직접 likeCount = likeCount + 1 이라는 쿼리를 작성하면, 영속성 컨텍스트에서 관리하는 데이터가 아닌 db에서 관리하는 데이터를 기준으로 수정이 이뤄지기 때문에 올바른 데이터 변경을 기대할 수 있게 됩니다.&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;1283&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNiNSA/btsjkFRp5OY/yrGDYsqNlwHC18wFDUlkTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNiNSA/btsjkFRp5OY/yrGDYsqNlwHC18wFDUlkTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNiNSA/btsjkFRp5OY/yrGDYsqNlwHC18wFDUlkTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNiNSA%2FbtsjkFRp5OY%2FyrGDYsqNlwHC18wFDUlkTk%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;1283&quot; height=&quot;329&quot; data-origin-width=&quot;1283&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기대한 대로 likeCount가 100으로 잘 저장이 된 모습을 볼 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 - 데드락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기서 해결이 된 줄 알고&amp;nbsp;해당 코드로 변경 후 성능 테스트를 진행하는데 테스트가 도중에 멈춰버리는 일이 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccJeG4/btsjkiPlkiG/r4wZYyuXwKu8cdPgCFfe00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccJeG4/btsjkiPlkiG/r4wZYyuXwKu8cdPgCFfe00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccJeG4/btsjkiPlkiG/r4wZYyuXwKu8cdPgCFfe00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccJeG4%2FbtsjkiPlkiG%2Fr4wZYyuXwKu8cdPgCFfe00%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;1343&quot; height=&quot;357&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 살펴보니 MySQLTransactionRollbackException 이 발생했고, 원인은 데드락이 생겼다는 이유였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디에서 데드락이 걸렸는지 확인해 보기 위해 SHOW ENGINE INNODB STATUS 명령어를 통해 확인을 해본 결과, 게시글에서 데드락이 걸렸다는 내용을 볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;1132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvVS1u/btsjFRQ63V2/CQdLGiA1YzQVYXznkJKxb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvVS1u/btsjFRQ63V2/CQdLGiA1YzQVYXznkJKxb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvVS1u/btsjFRQ63V2/CQdLGiA1YzQVYXznkJKxb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvVS1u%2FbtsjFRQ63V2%2FCQdLGiA1YzQVYXznkJKxb0%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;1116&quot; height=&quot;1132&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;1132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 이 상황을 봤을 때는 게시글에 베타 락(X Lock)이 걸리는 것은 이해를 할 수 있었지만 처음에 공유 락(S Lock)이 걸리는 이유에 대해서는 알 수가 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 발생하나 찾아보니 MySQL의 InnoDB에 대한 이해도가 낮아서 생긴 일이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 insert, delete, update 쿼리를 실행하면 foreign key에 대해서 올바른 데이터가 존재하는지 확인하는 과정을 거칩니다. 그 과정에서 InnoDB는 foreign key로 참조된 레코드에 대해 공유 락을 걸게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;In an SQL statement that inserts, deletes, or updates many rows, foreign key constraints (like unique constraints) are checked row-by-row. &lt;b&gt;When performing foreign key checks, InnoDB sets shared row-level locks&lt;/b&gt; on child or parent records that it must examine. &lt;a href=&quot;https://dev.mysql.com/doc/mysql-reslimits-excerpt/5.7/en/ansi-diff-foreign-keys.html#:~:text=In%20an%20SQL%20statement%20that%20inserts%2C%20deletes%2C%20or%20updates%20many%20rows%2C%20foreign%20key%20constraints%20(like%20unique%20constraints)%20are%20checked%20row%2Dby%2Drow.%20When%20performing%20foreign%20key%20checks%2C%20InnoDB%20sets%20shared%20row%2Dlevel%20locks%20on%20child%20or%20parent%20records%20that%20it%20must%20examine.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 테스트가 성공한 이유는 h2 데이터베이스 환경으로 돌아갔기 때문이었던 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 디비를 MySQL로 변경을 하고 예외가 발생하는 것을 확인하기 위한 새로운 테스트 코드를 추가했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686572106023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;데드락 예외가 발생해야 함&quot;)
void deadlockTest() throws Exception {
    Member member = memberRepository.save(getBasicMember());
    Post post = postRepository.save(getBasicPost(member));
    ExecutorService executorService = Executors.newFixedThreadPool(2);

    while (true) {
        Member member1 = memberRepository.save(getBasicMember());
        Member member2 = memberRepository.save(getBasicMember());
        Future&amp;lt;?&amp;gt; submit1 = executorService.submit(() -&amp;gt; likeService.like(member1.getId(), post.getId()));
        Future&amp;lt;?&amp;gt; submit2 = executorService.submit(() -&amp;gt; likeService.like(member2.getId(), post.getId()));
        try {
            submit1.get();
            submit2.get();
        } catch (Exception e) {
            executorService.shutdown();
            throw e;
        }
    }
}&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;1916&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rHm7w/btsjILWTp3H/0YCYkMZmrvKwiH1AQe4JV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rHm7w/btsjILWTp3H/0YCYkMZmrvKwiH1AQe4JV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rHm7w/btsjILWTp3H/0YCYkMZmrvKwiH1AQe4JV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrHm7w%2FbtsjILWTp3H%2F0YCYkMZmrvKwiH1AQe4JV1%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;1916&quot; height=&quot;510&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 디비로 해당 테스트를 돌리면 무한 루프에 빠지지만, MySQL로 돌리면 금세 데드락이 걸리면서 테스트가 종료되는 모습을 볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 나온 대로 likes가 insert 될 때 foreign key가 걸린 레코드에 대해 공유 락이 걸리는지도 확인을 해보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4ROj/btsjwrMsmEH/gPR8U7xNyRawpoIfzF8kvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4ROj/btsjwrMsmEH/gPR8U7xNyRawpoIfzF8kvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4ROj/btsjwrMsmEH/gPR8U7xNyRawpoIfzF8kvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4ROj%2FbtsjwrMsmEH%2FgPR8U7xNyRawpoIfzF8kvk%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;1760&quot; height=&quot;658&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에서 설명한 것처럼, 실제로 likes 테이블에 insert를 하면 foreign key 제약사항이 걸려있는 post 레코드에 대해 공유 락이 걸려있었습니다.&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;1262&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDfN2u/btsjFWdU3CR/hKVANlwEPGFLyI1ud8hzo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDfN2u/btsjFWdU3CR/hKVANlwEPGFLyI1ud8hzo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDfN2u/btsjFWdU3CR/hKVANlwEPGFLyI1ud8hzo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDfN2u%2FbtsjFWdU3CR%2FhKVANlwEPGFLyI1ud8hzo0%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;1262&quot; height=&quot;938&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림처럼 서로 다른 트랜잭션이 동시에 likes 테이블에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;같은 post를 foreign key로 하는 데이터를&amp;nbsp;&lt;/span&gt;insert 할 경우, 해당 post에 두 개의 공유 락이 걸리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 post를 수정하는 트랜잭션에서는 다른 트랜잭션에서 공유 락을 걸었기 때문에 wait을 하게 되고, 추후에 다른 트랜잭션에서 수정을 하는 쿼리를 작성하면 데드락이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락이 발생했음을 감지한 트랜잭션은 롤백되며, 덕분에 wait을 하던 트랜잭션에서는 수정을 할 수 있게 됩니다.&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;1578&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAVZUc/btsjBs5J2A8/xCNJ3Xa2EBh4HXkNQPPoI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAVZUc/btsjBs5J2A8/xCNJ3Xa2EBh4HXkNQPPoI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAVZUc/btsjBs5J2A8/xCNJ3Xa2EBh4HXkNQPPoI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAVZUc%2FbtsjBs5J2A8%2FxCNJ3Xa2EBh4HXkNQPPoI1%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;1578&quot; height=&quot;944&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;select for update를 이용한 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 애초부터 post를 조회할 때 베타 락(X Lock)을 걸면 문제를 쉽게 해결할 수 있지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 지원하는 락 중 하나인 @Lock(LockModeType=PESSIMISTIC_WRITE)를 이용하여 조회를 하면 select for update 쿼리가 작성되어 해당 레코드에 베타 락을 걸 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 조회할 때 베타 락을 건다면 맨 처음에 문제가 되었던 post.increaseLikeCount 메서드도 다시 사용을 할 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686559502538&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PostRepository extends JpaRepository&amp;lt;Post, Long&amp;gt; {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select p from Post p where p.id = :postId&quot;)
    Optional&amp;lt;Post&amp;gt; findByIdWithPessimisticLock(@Param(&quot;postId&quot;) Long postId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686559683110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void like(Long memberId, Long postId) {
    Post post = getPost(postId);
    Member member = getMember(memberId);

    validateAlreadyLikedPost(memberId, postId);

    likeRepository.save(new Like(post, member));
    post.increaseLikeCount();

}

private Post getPost(Long postId) {
    return postRepository.findByIdWithPessimisticLock(postId)
            .orElseThrow(() -&amp;gt; new PostNotFoundException(postId));
}&lt;/code&gt;&lt;/pre&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;1160&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY49jL/btsjwrTyiR5/0qLwz0wmGDpQJkWPpFBoYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY49jL/btsjwrTyiR5/0qLwz0wmGDpQJkWPpFBoYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY49jL/btsjwrTyiR5/0qLwz0wmGDpQJkWPpFBoYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY49jL%2FbtsjwrTyiR5%2F0qLwz0wmGDpQJkWPpFBoYk%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;1160&quot; height=&quot;365&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 기대했던 대로 테스트를 통과한 것과, post를 조회할 때 for update가 마지막에 붙은 쿼리가 작성되는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 - 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 소요 시간을 보면 뭔가 문제가 있다는 걸 알 수 있습니다. 테스트를 한 번 하는데 무려 1.2초나 걸린 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 데드락이 걸리는 구현이었지만, 두 번째 구현의 소요 시간인 704ms와 비교를 해보면 시간이 거의 두 배나 증가한 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구현을 기반으로 ngrinder를 이용해 성능 테스트를 진행을 해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhdbht/btsjAvVTatb/0RaFWsC3smdNfKZRyuCb3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhdbht/btsjAvVTatb/0RaFWsC3smdNfKZRyuCb3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhdbht/btsjAvVTatb/0RaFWsC3smdNfKZRyuCb3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhdbht%2FbtsjAvVTatb%2F0RaFWsC3smdNfKZRyuCb3K%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;1210&quot; height=&quot;562&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vuser를 30명으로 두고 테스트를 했을 때 TPS가 53, MTT가 357ms 정도가 나왔고, 로컬 환경에서 테스트를 돌렸기 때문에 높은 퍼포먼스를 기대할 수 없는 것을 감안해도 아주 낮은 성능을 보이는 것을 짐작할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿼리 순서 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 조금 더 근본적인 문제를 생각해 보면 likes를 insert 할 때 post 레코드에 공유 락이 걸리는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말하면, 다른 트랜잭션에서 공유 락을 걸어둔 post 레코드에 대해 update를 하기 때문에 데드락이 생기는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 처음부터 post를 먼저 update를 하면 문제를 해결할 수 있다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 되면 잠시 update를 할 post에 베타 락이 걸리겠지만, 수정에 성공하면 바로 락이 풀리기 때문에 다른 트랜잭션에서 데드락이 걸릴 가능성이 사라지게 된다고 판단했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686573111768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void like(Long memberId, Long postId) {
    Post post = getPost(postId);
    Member member = getMember(memberId);

    validateAlreadyLikedPost(memberId, postId);

    postRepository.increaseLikeCount(postId);
    likeRepository.save(new Like(post, member));
}

private Post getPost(Long postId) {
    return postRepository.findById(postId)
            .orElseThrow(() -&amp;gt; new PostNotFoundException(postId));
}&lt;/code&gt;&lt;/pre&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;1203&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSl5Od/btsjG0mT4T7/afr2eq6RuuAGKkS4pRkS01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSl5Od/btsjG0mT4T7/afr2eq6RuuAGKkS4pRkS01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSl5Od/btsjG0mT4T7/afr2eq6RuuAGKkS4pRkS01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSl5Od%2FbtsjG0mT4T7%2Fafr2eq6RuuAGKkS4pRkS01%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;1203&quot; height=&quot;555&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 75px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 20px;&quot;&gt;Pessimistic Write&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 20px;&quot;&gt;Insert 쿼리 -&amp;gt; Save 쿼리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 18px;&quot;&gt;TPS&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px; text-align: center;&quot;&gt;56.2&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px; text-align: center;&quot;&gt;70.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 19px;&quot;&gt;Peak TPS&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;68.0&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px; text-align: center;&quot;&gt;87.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; text-align: center; height: 18px;&quot;&gt;MTT(ms)&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px; text-align: center;&quot;&gt;356.88&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px; text-align: center;&quot;&gt;260.87&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 코드 두 줄의 순서를 바꾼 것만으로도 성능이 눈에 띄게 올라갔습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 - 가독성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이건 큰 문제가 아닐 수도 있지만 그래도 문제라면 문제라고 할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 코드의 흐름을 따라가다 보면 likes 생성 -&amp;gt; likeCount 증가라는 흐름이 자연스럽게 느껴지기 때문이죠.&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;increaseLikeCount 트랜잭션 분리&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;물론 맨 처음 코드처럼 100명이 좋아요를 눌렀을 때 85%가 사라지는 것은 문제가 되지만, 그정도의 수준이 아니라면 '좋아요 수'의 정합성은 지금의 서비스에서 크게 중요하지 않다고 판단을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약간의 오차가 있어도 된다는 말은 트랜잭션에서 분리를 해도 된다는 뜻이고, 그래서 이벤트를 이용해 기능을 분리하는 방식으로 구현을 변경했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686576857604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class PostLikeCountIncreasedEvent {
    private final Long postId;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686577059667&quot; class=&quot;reasonml&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;@Service
@Transactional
@RequiredArgsConstructor
public class LikeService {

    //...
    private final ApplicationEventPublisher eventPublisher;

    public void like(Long memberId, Long postId) {
        Post post = getPost(postId);
        Member member = getMember(memberId);

        validateAlreadyLikedPost(memberId, postId);

        likeRepository.save(new Like(post, member));
        eventPublisher.publishEvent(new PostLikeCountIncreasedEvent(postId));
    }
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686576902013&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class PostEventHandler {

    private final PostRepository postRepository;

    @Async
    @Transactional
    @TransactionalEventListener
    public void increaseLikeCount(PostLikeCountIncreasedEvent event) {
        Long postId = event.getPostId();

        postRepository.increaseLikeCount(postId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현을 하면 코드의 흐름도 지킬 수 있을뿐더러 likeCount를 업데이트하는 부분을 비동기로 처리하기 때문에 유저의 응답 시간에 영향을 미치지 않게 됩니다.&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;1207&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2LuiE/btsjJU0ofwd/fSw3Vbvtck34tmmQfCZE91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2LuiE/btsjJU0ofwd/fSw3Vbvtck34tmmQfCZE91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2LuiE/btsjJU0ofwd/fSw3Vbvtck34tmmQfCZE91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2LuiE%2FbtsjJU0ofwd%2FfSw3Vbvtck34tmmQfCZE91%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;1207&quot; height=&quot;555&quot; data-origin-width=&quot;1207&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 75px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: start;&quot;&gt;Insert 쿼리 -&amp;gt; Save 쿼리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 20px;&quot;&gt;트랜잭션 분리 및 이벤트 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;TPS&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;70.3&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;62.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;Peak TPS&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;87.5&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 19px;&quot;&gt;76.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;MTT(ms)&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;260.87&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;231.01&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 트랜잭션을 분리했기 때문인지 TPS는 전보다 떨어졌지만 응답 속도인 MTT는 조금 향상한 모습을 볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 보니 테스트들에서 에러가 꽤나 많이 나왔지만 이는 ngrinder의 동시성 문제로 validateAlreadyLikedPost에서 예외가 발생했기 때문이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 원인이 무엇인지는 알아내지 못했지만 HTTPRequest가 static으로밖에 구현을 못해서 동일한 요청을 여러 번 보내는 상황이 만들어졌던 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 - 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 그래도 동시성 테스트 때문에 비동기 처리를 하는데 각각의 스레드에서 또 비동기로 새로운 이벤트를 호출하고 있기 때문에 테스트 코드가 점점 엉망이 되게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1686584185044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;100명이 동시에 좋아요를 누를 경우 100개의 좋아요와 likeCount가 100개 올라야 함&quot;)
void likeTest_100_differentMembers() throws Exception {
    //given
    int threadCount = 100;
    Member member = memberRepository.save(getBasicMember());
    Post post = postRepository.save(getBasicPost(member));
    List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
    for (int i = 0; i &amp;lt; threadCount; i++) {
        members.add(memberRepository.save(getBasicMember()));
    }

    ExecutorService executorService = Executors.newFixedThreadPool(32);
    CountDownLatch latch = new CountDownLatch(threadCount);

    //when
    for (int i = 0; i &amp;lt; threadCount; i++) {
        int finalI = i;
        executorService.submit(() -&amp;gt; {
            try {
                likeService.like(members.get(finalI).getId(), post.getId());
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                latch.countDown();
            }
        });
    }
    latch.await();

    //then
    List&amp;lt;Like&amp;gt; likes = likeRepository.findAll();
    Post findPost = postRepository.findById(post.getId()).get();
    assertThat(likes).hasSize(threadCount);
    assertThat(findPost.getLikeCount()).isEqualTo(threadCount);
}&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;1193&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2xPJB/btsjFSiWkSj/HwizYIyXGjZJAjbtISf7VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2xPJB/btsjFSiWkSj/HwizYIyXGjZJAjbtISf7VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2xPJB/btsjFSiWkSj/HwizYIyXGjZJAjbtISf7VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2xPJB%2FbtsjFSiWkSj%2FHwizYIyXGjZJAjbtISf7VK%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;1193&quot; height=&quot;529&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 테스트 코드에서 likeService.like 후에 해당 스레드에 1초를 sleep 하는 코드를 추가하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 해당 스레드가 likes를 생성하고 바로 종료되면서 likeCount를 증가시키는 이벤트가 끝나기 전에 latch의 수가 0이 되어 테스트가 종료되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이건 테스트 db를 MySQL로 바꾸면서 발생하는 문제점인데, 데이터를 저장하거나 수정하는 쿼리가 테스트 스레드에서 이뤄지지 않기 때문에 데이터가 롤백되지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 저 같은 경우는 여기에서는 작성하지 않았지만 따로 테스트가 끝나면 h2 디비를 초기화하는 클래스를 구현을 해뒀는데, 거기에서 h2 디비만의 SQL 문법을 사용했기 때문에 따로 MySQL을 사용하는 테스트 코드만 따로 모아 롤백하는 번거로운 작업을 추가해야 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 게시글 좋아요 기능을 구현하면서 겪은 문제들과 해결하거나 수정했던 제 경험들을 기록해 봤습니다.&lt;/p&gt;
&lt;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;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kapentaz.github.io/jpa/mysql/JPA-@OneToMany%EC%99%80-foreign-key-%EA%B7%B8%EB%A6%AC%EA%B3%A0-deadlock/#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kapentaz.github.io/jpa/mysql/JPA-@OneToMany%EC%99%80-foreign-key-%EA%B7%B8%EB%A6%AC%EA%B3%A0-deadlock/#&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/mysql-reslimits-excerpt/5.7/en/ansi-diff-foreign-keys.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dev.mysql.com/doc/mysql-reslimits-excerpt/5.7/en/ansi-diff-foreign-keys.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codechacha.com/ko/java-countdownlatch/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://codechacha.com/ko/java-countdownlatch/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thalals.tistory.com/370&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://thalals.tistory.com/370&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 기록</category>
      <category>Cannotacquirelockexception</category>
      <category>MySQLTransactionRollbackException</category>
      <category>데드락</category>
      <category>동시성</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/34</guid>
      <comments>https://wlsh44.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 13 Jun 2023 00:57:58 +0900</pubDate>
    </item>
    <item>
      <title>nGrinder [WARNING] The test is finished but has no TPS</title>
      <link>https://wlsh44.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 nGrinder를 이용해서 사이드 프로젝트 성능 테스트를 하다가 다음과 같은 로그가 발생하면서 TPS 그래프가 정상적으로 그려지지 않았던 일이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;713&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLCyWa/btsglQ19WQ0/YUQw2qkv8OwCtdMjQWYMR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLCyWa/btsglQ19WQ0/YUQw2qkv8OwCtdMjQWYMR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLCyWa/btsglQ19WQ0/YUQw2qkv8OwCtdMjQWYMR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLCyWa%2FbtsglQ19WQ0%2FYUQw2qkv8OwCtdMjQWYMR0%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;1215&quot; height=&quot;713&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;713&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[WARNING] The test is finished but has no TPS라는 문구가 뜨면서 제대로 처리가 되지 않는 경우였습니다.&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;724&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkbVE/btsglR7RmOH/dFaV2dySUPog9KVdrrZEHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkbVE/btsglR7RmOH/dFaV2dySUPog9KVdrrZEHk/img.png&quot; data-alt=&quot;..?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkbVE/btsglR7RmOH/dFaV2dySUPog9KVdrrZEHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpkbVE%2FbtsglR7RmOH%2FdFaV2dySUPog9KVdrrZEHk%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;724&quot; height=&quot;203&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;203&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;그런데 기대와는 달리 테스트가 끝났다는 received a stop message 로그를 띄운 후, communication shut down 로그만 달랑 내버려둔 채 로그 파일이 끝났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 문젠가 싶었지만 테스트한 스크립트는 테스트 서버 주소를 로컬에서 네이버 클라우드 서버로 바꾼거라 스크립트가 잘못되지는 않았을 거라 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽도 NCP 서버의 모든 IP에 대해 80 포트와 443 포트를 잘 열어둔 것도 다시 한번 확인을 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 포스트맨으로 테스트를 했을 때 잘 동작했고, 위의 로그에서도 테스트 시 200응답을 받았기 때문에 방화벽 문제는 절대 아니라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 구글링을 하다가 저와 비슷한 ngrinder 이슈와 해결 방법을 찾게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 &lt;b&gt;Groovy말고 Jython으로 스크립트를 작성하라&lt;/b&gt;는 것이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/90zUH/btsgkiS1jaT/l7DqUSCiwEHBL2OaLUXZa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/90zUH/btsgkiS1jaT/l7DqUSCiwEHBL2OaLUXZa0/img.png&quot; data-alt=&quot;구글링으로 왔습니다 감사합니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/90zUH/btsgkiS1jaT/l7DqUSCiwEHBL2OaLUXZa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F90zUH%2FbtsgkiS1jaT%2Fl7DqUSCiwEHBL2OaLUXZa0%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;1004&quot; height=&quot;255&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;255&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;솔직히 처음에는 반신반의하면서 jython으로 스크립트를 다시 작성하고 테스트를 돌려봤더니..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q5GDc/btsgmFsAU0i/hvhGqu17lFsLKpSkd4eavK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q5GDc/btsgmFsAU0i/hvhGqu17lFsLKpSkd4eavK/img.png&quot; data-alt=&quot;이게 되네&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q5GDc/btsgmFsAU0i/hvhGqu17lFsLKpSkd4eavK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq5GDc%2FbtsgmFsAU0i%2FhvhGqu17lFsLKpSkd4eavK%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;1213&quot; height=&quot;556&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이게 되네&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여담&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nGrinder를 처음 써보고 있는데 개인적으로는 많이 힘드네요.. 분명히 쉬운 건 맞는 것 같은데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nGrinder도 커뮤니티가 더 활성화되고 자료도 더 많아지면 참 좋겠다는 생각이 들었습니다  &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/naver/ngrinder/discussions/886&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/naver/ngrinder/discussions/886&lt;/a&gt;&lt;/p&gt;</description>
      <category>ngrinder</category>
      <author>wlsh</author>
      <guid isPermaLink="true">https://wlsh44.tistory.com/33</guid>
      <comments>https://wlsh44.tistory.com/33#entry33comment</comments>
      <pubDate>Thu, 18 May 2023 03:14:46 +0900</pubDate>
    </item>
  </channel>
</rss>