<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>초보개발자</title>
    <link>https://chobo-backend.tistory.com/</link>
    <description>꾸준하고 빠르게</description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 15:56:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>chobo99</managingEditor>
    <image>
      <title>초보개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/6877596/attach/b8839a35bbd948b4ac8044e31ca159a9</url>
      <link>https://chobo-backend.tistory.com</link>
    </image>
    <item>
      <title>25년을 마무리하며.. (Ft. 회고 + 내년 목표)</title>
      <link>https://chobo-backend.tistory.com/67</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 0. 25년을 마친 소감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;25년이 정말 너무나도 빠르게 지나가버렸다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 빠르게 지났는지 생각해보면 거의 매일 반복되는 일상에 적응해버려서 지나가는 시간들을 잘 못느꼈던 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 매일매일이 다음 중 하나였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 부캠 + 나머지 공부 + 잠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DH 합숙(플젝 + 개인 공부) + 잠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 카페 + 잠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디 놀러간 적도 없이 쉰다고 한다면 혼자 유튜브보거나 넷플정도..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉼없이 달려왔지만 사실 크게 힘든 건 못 느꼈다. 아무래도 개발이 천직이지 않을까ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취준 과정에서 계속되는 탈락이 조금 힘들었던 것 같긴한데 그래도 내가 열심히하면 안되는 건 없다고 믿는 편이라 최대한 긍정적으로 생각했었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 올해 목표였던 취뽀는 성공 + 내가 원해왔던 회사 기준에 맞는 곳으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길었던 취준의 마무리를 잘 지은 것 같아서 뿌듯하기도 하지만 아직 부족한게 많아 더 열심히 해야하지 않을까 싶다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각설하고 회고부터 해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 현대 소프티어 부트캠프(1~2월)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 파트로 참여했던 소프티어 부트캠프는 그동안 아무생각없이 기능만 잘 돌아가면 끝이지 않나라는 우매한 내 마인드를 고쳐먹게 되는 전환점이었다. 2월 말 최종평가 때 기아 현직자분들의 날카로운 질문들을 받아보며 얼마나 내가 얕게 공부해왔는지 깨닫게 되었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6OdK/dJMcac9wVSs/8SaeUJoNqMEjZaRdGh3Zv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6OdK/dJMcac9wVSs/8SaeUJoNqMEjZaRdGh3Zv0/img.png&quot; data-alt=&quot;최종 발표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6OdK/dJMcac9wVSs/8SaeUJoNqMEjZaRdGh3Zv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6OdK%2FdJMcac9wVSs%2F8SaeUJoNqMEjZaRdGh3Zv0%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;375&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종 발표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 함께했던 우리 '두부' 팀은 너무 좋은 팀원들이었다. 성격도 좋고 + 능력자(기획은 2명 전부&amp;nbsp;현차 전환까지 ㄷㄷ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 두달간 우리 팀만 점심을 계속 같이 먹을 정도로 팀 분위기가 좋았고, 그 덕분에 1년이 다된 지금까지도 주기적으로 만난다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1440&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o9eNe/dJMcahbYeA6/mwWnEm78dRDuBKWb9TpEvk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o9eNe/dJMcahbYeA6/mwWnEm78dRDuBKWb9TpEvk/img.jpg&quot; data-alt=&quot;점심먹다 찍은 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o9eNe/dJMcahbYeA6/mwWnEm78dRDuBKWb9TpEvk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo9eNe%2FdJMcahbYeA6%2FmwWnEm78dRDuBKWb9TpEvk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;342&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;점심먹다 찍은 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트캠프가 끝나고 3월 말쯤에 기아 최종면접을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 인생에서 기업 면접은 처음이라 준비가 너무나도 부족했고, 전공때 배웠던 CS 복습도 부족했던터라 나의 역량이 너무나 부족함을 느꼈다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과도 예상했던 대로 불합격,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wf2dD/dJMcafkVy3V/KA6JONXH3GzIMGkdK5KL20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wf2dD/dJMcafkVy3V/KA6JONXH3GzIMGkdK5KL20/img.jpg&quot; data-alt=&quot;ㅠㅠ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wf2dD/dJMcafkVy3V/KA6JONXH3GzIMGkdK5KL20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWf2dD%2FdJMcafkVy3V%2FKA6JONXH3GzIMGkdK5KL20%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;332&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ㅠㅠ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 아직 내가 원하는 기업(IT 서비스 기업)에 취업할 정도의 역량이 안된다는 것은 너무나 뼈저리게 느꼈다. 그리고 면접 경험도 부족하고 취준도 제대로 준비해본 경험이 없어서 어느정도로해야 좋은 기업에 갈지 감조차 잡히지 않았다.(총체적 난국) 이때부터 다시 처음부터 해보지는 생각으로 역량쌓기에 몰입했던 것 같다&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. DH 합숙(3~6월)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연합동아리 큐시즘 30기에서 만난 4명의 팀, 팀 이름은 드림헌터 -&amp;gt; Dream Hunter -&amp;gt; DH 다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 머니헌터(ㅋㅋㅋ)로 하려다가 너무 느낌이 없다라는 동아리원들의 평가를 받고, 남들이 꿈을 꿀 때 우리는 꿈을 이룬다(=사냥한다)는 의미를 담아서 만들었다. 그리고 우리의 목표는 '사용자가 단돈 천원이라도 낼 수 있는 서비스를 만들어보자'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~2월부터 시작해서 영어일기, AI 전화 영어 등등 여러가지 주제들을 기획 + 피봇을 반복했었고, 부트캠프가 끝난 3월 팀장 형이 학교에서 대여받았던 창업 공간이 남아있다는 소리를 들었다. 형이랑 앞으로 어떻게 할지에 대해 많은 이야기를 나누다가 이왕 프로젝트 하는 거 비대면으로 불편하게 하지말고 그냥 합숙하면서 제대로 해보자고 갑작스럽게 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LWwEj/dJMcaiBUeZs/u90O7LX5u2pCU9oShMVsx0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LWwEj/dJMcaiBUeZs/u90O7LX5u2pCU9oShMVsx0/img.jpg&quot; data-alt=&quot;짐싸들고 출발&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LWwEj/dJMcaiBUeZs/u90O7LX5u2pCU9oShMVsx0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLWwEj%2FdJMcaiBUeZs%2Fu90O7LX5u2pCU9oShMVsx0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;421&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;짐싸들고 출발&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 부모님께는 성공하고 오겠다고 말한 후 곧바로 짐싸들고 형과 합숙을 시작했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPO6GL/dJMcaiaTdhc/stRrrSzNk7Aykol7StJpkk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPO6GL/dJMcaiaTdhc/stRrrSzNk7Aykol7StJpkk/img.jpg&quot; data-alt=&quot;합숙&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPO6GL/dJMcaiaTdhc/stRrrSzNk7Aykol7StJpkk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPO6GL%2FdJMcaiaTdhc%2FstRrrSzNk7Aykol7StJpkk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;452&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;합숙&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠팡이랑 다이소에서 생필품들을 사고, 토퍼는 당근에서 나눔받아서 저기 안쪽 구석탱이에서 깔고 잤다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TNqKs/dJMcaihEKUM/lzan8w1UDPtGR7EK4yHGx0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TNqKs/dJMcaihEKUM/lzan8w1UDPtGR7EK4yHGx0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TNqKs/dJMcaihEKUM/lzan8w1UDPtGR7EK4yHGx0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTNqKs%2FdJMcaihEKUM%2Flzan8w1UDPtGR7EK4yHGx0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;369&quot; height=&quot;492&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화이트보드도 있어서 회의하기 참 좋았었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합숙하면서 좋았던 점은 생각나는게 있을 때마다 바로바로 이야기하면서 디벨롭시킬 수 있었고, 그러다보니 학교나 동아리에서 시간잡고 하는 회의보다 훨씬 더 빠르게 기획할 수 있었다. 이때 대면 작업의 매력을 많이 느겼던 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 생활패턴은 거의 군대랑 유사했다. 6시반에 기상해서 헬스장 갔다가, 학교에서 주는 천원의 아침밥먹고, 도서관 가서 각자 취준 + 숙소에서 플젝까지.. 형이랑 나랑 성향도 잘 맞아서 별다른 갈등없이 쭉 잘 지냈다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 운이 좋게 서비스기업 기술 면접 + 대기업 최종까지도 갔었지만 아직까지도 부족한 면접 경험 + 기술 역량 때문에 좋은 기회를 잡지 못했다 ㅠㅠ &lt;span style=&quot;background-color: #ffffff; color: #1c1c1c; text-align: start;&quot;&gt;준비된 자가 기회를 잡는다는 말이 계속 머릿속에 맴돌았다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;1422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v52uI/dJMcagqA6bh/BZF0sK5kOwRX9QI6fRppd0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v52uI/dJMcagqA6bh/BZF0sK5kOwRX9QI6fRppd0/img.jpg&quot; data-alt=&quot;불합1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v52uI/dJMcagqA6bh/BZF0sK5kOwRX9QI6fRppd0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv52uI%2FdJMcagqA6bh%2FBZF0sK5kOwRX9QI6fRppd0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;356&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;1422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;불합1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP9HmB/dJMcai9J7vX/W5gEJzspiUYLGHs8HPi6Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP9HmB/dJMcai9J7vX/W5gEJzspiUYLGHs8HPi6Ck/img.png&quot; data-alt=&quot;불합2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP9HmB/dJMcai9J7vX/W5gEJzspiUYLGHs8HPi6Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP9HmB%2FdJMcai9J7vX%2FW5gEJzspiUYLGHs8HPi6Ck%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;381&quot; height=&quot;286&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;불합2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 프로젝트로 넘어와서 이 세상에 없는 서비스를 만든다는 것은 너무나도 어려운 일이었다. 그래서 우리 팀 전부다 숙소로 모여서 각자 평소 일상들을 하나하나 전부 되짚어보며 뭐했는지 이야기를 나눴다. 그러다 찾았던 키워드는 '지각'이었는데 구현 가능성까지 고민해보며 결과적으로는 약속 과정에서의 불편함을 해결하는 서비스를 만들었고 올해 11월 드디어 출시까지 할 수 있었다. 서비스 출시가 늦어졌던 이유는 iOS의 알람 권한을 받는 과정과 안정적인 알람 기능 구현이 매우 복잡했는데, iOS 26 버전이 나오면서 Alarm kit 기능이 생겼고 그걸 활용해서 우리 서비스를 출시할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 이름은 '온닷(OnDot)'이고 영어로는 정시에 라는 뜻을 가진 'On The Dot'의 의미와 '그가 드디어 제시간에 온닷'이라는 한글의 웃긴 뜻도 있다. 서비스는 아래에서 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://linktr.ee/ondot.official&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://linktr.ee/ondot.official&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766934992817&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;Ondot.official | Instagram | Linktree&quot; data-og-description=&quot;등록만 해두세요! 준비부터 출발까지 챙겨줄게요. 나만의 스케줄 어시스턴트, 온닷&quot; data-og-host=&quot;linktr.ee&quot; data-og-source-url=&quot;https://linktr.ee/ondot.official&quot; data-og-url=&quot;https://linktr.ee/ondot.official&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cAw4Lv/hyZQxwDFc4/zOSZjAxcRwt1w2K3Km5cO1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/eyy5H/hyZPGAPyUS/z2xwpl0yO4z4yTZDRyor60/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://linktr.ee/ondot.official&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://linktr.ee/ondot.official&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cAw4Lv/hyZQxwDFc4/zOSZjAxcRwt1w2K3Km5cO1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/eyy5H/hyZPGAPyUS/z2xwpl0yO4z4yTZDRyor60/img.jpg?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;Ondot.official | Instagram | Linktree&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;등록만 해두세요! 준비부터 출발까지 챙겨줄게요. 나만의 스케줄 어시스턴트, 온닷&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;linktr.ee&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 &lt;b&gt;DH 팀원들이 다들 열심히 사는 멋있는 사람들&lt;/b&gt;이라 출시까지 할 수 있었던 것 같다. 비록 사용자에게 돈을 받겠다는 목표는 이루지 못했지만, 처음 들어보는 서비스이며 우리가 정의했던 불편함을 공감한다는 피드백을 많이 받을 수 있어서 좋았다. 사이드 프로젝트로 진행하면서 앞으로 더 홍보도 많이하고 사용자가 원하는 기능들도 추가해서 돈을 받을 수 있을 정도의 서비스까지 키워보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 삼성 SSAFY(7~12월)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숙소 만료일도 다가왔기에 다음 스텝을 고민했었다. 현재 돈을 벌고 있는 상황도 아니었고 무작장 돈만 쓰며 끝이 안보이는 취준을 할 수는 없었기에 안전장치가 필요했다. 여러 방안들을 고민해보다 결정한 것은 삼성에서 진행하는 싸피였다. 매월 100만원씩 지원금도 주고, 점심도 제공하며, 9~6를 역삼역 멀티캠퍼스에서 오프라인으로 진행하기에 나태해지는 것도 막고 생활습관까지 유지할 수 있었기 때문에 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLpLNU/dJMcagKUMbK/FjA0OT1PKmm7aMgkbp8Zdk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLpLNU/dJMcagKUMbK/FjA0OT1PKmm7aMgkbp8Zdk/img.jpg&quot; data-alt=&quot;싸피 옷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLpLNU/dJMcagKUMbK/FjA0OT1PKmm7aMgkbp8Zdk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLpLNU%2FdJMcagKUMbK%2FFjA0OT1PKmm7aMgkbp8Zdk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;304&quot; height=&quot;405&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;싸피 옷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싸피 활동을 진행하면서 가장 집중했던 것은 개인 프로젝트('바로')였다. 싸피가 시작하자마자 새로운 프로젝트를 기획하며 시작했었는데 그 이유는, 포트폴리오를 작성하려고 하다보니 내가 지금까지 해왔던 프로젝트들에서 기술적으로 깊이있게 어필할만한 것이 없었다. 그래서 큐시즘에서 만났던 친한 동생과 함께 둘이서 프로젝트를 진행했고 7~9월까지 3개월동안 하루죙일 거의 이것만 했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 서비스기업이었기에 한정된 시간안에서 선택과 집중이 필요했고, 그래서 3분기 공채는 버리고 포폴 + CS + 기술 역량 향상에 집중했다. 이때 기술적으로 정말 많이 성장한 기분 + 성취감으로 회사 지원을 하지 않고있음에도 전혀 불안하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 형(DH 팀장)도 비전공으로 싸피를 다녔었어서 거의 매일 6시에 싸피 끝나면 근처에서 밥먹고 카페가서 10시까지 각자 할 일을 했다. 형은 PM 직무를 꿈꾸지만 앞으로는 모두가 코딩을 해야할 시대가 올 것이라며 싸피에 참여한 그의 결정,, 정말 대단한 사람이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10월부터는 포폴 지속 개선 + 면접에 포커스를 맞췄다. 내 강점은 포폴이라고 생각했기에 이력(학벌, 학점, 자격증) + 자소서를 주로 보는 기업에는 거의 지원하지 않았다. 이 전략이 잘 들어맞았는지 서류 합격률은 40프로 정도였고, 내 약점이었던 면접은 여러번 실패를 겪으며 면접 복기를 통해 보완해나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사를 선택하는 가장 큰 기준은 다음과 같았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1순위는 성장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워라벨, 돈보다도 성장이 가장 중요했다.&lt;/b&gt; 실제 트래픽이 많이 발생하는 환경에서 어려운 문제들을 만나고, 여러 요구사항들을 여러 기술로 풀어내며 정말 많은 것을 배우는 것이 나한테는 너무나도 중요했다. 그리고 개발을 좋아하는 사람들이 모인 곳 + 주기적인 세션을 통해 기술 공유를 하는 곳 + 기술 블로그를 작성하는 곳 등등 개발에 진심인 회사를 원했다. 이러한 환경이라면 훨씬 더 빠르게 성장할 수 있다고 생각했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI로 인해 급변하는 상황속에서 빠르게 성장하지 않는 개발자는 몇년안에 AI에게 전부 대체될 것 같다는 개인적인 예측때문에 성장을 가장 중요하게 생각했던 것 같다. 그리고 젊었을 때 고생해야 나중에 편하지 않을까라는 생각도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 첫 커리어로 선택하게 된 회사는 처음에 목표로 했던 서비스 기업이기도 하며 나의 기준에 정말 딱 맞는 회사다. 준비도 많이 했지만 운도 많이 따라줬기에 좋은 결과가 있지 않았나 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q9oKT/dJMcabixhIi/U4OV9Nw7UfGVFDfZ5IOhOK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q9oKT/dJMcabixhIi/U4OV9Nw7UfGVFDfZ5IOhOK/img.jpg&quot; data-alt=&quot;열심히 할게요&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q9oKT/dJMcabixhIi/U4OV9Nw7UfGVFDfZ5IOhOK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ9oKT%2FdJMcabixhIi%2FU4OV9Nw7UfGVFDfZ5IOhOK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;428&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;열심히 할게요&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금와서 느끼는 것은 싸피가 없었다면 올해 안에 취뽀 절대 못했을 것 같다. 싸피의 커리큘럼보다도 오프라인에서 정해진 시간동안 몰입할 수 있는 환경 + 지원금, 취업 지원(설명회, 면접 컨설팅)으로 인한 안정감이 좋은 결과를 만들어 냈던 것 같다. 감사합니다 싸피!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 26년에는 뭘 하면 좋을까&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 아직 모르는게 너무 많은 것 같다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjhs8e/dJMcaacQSFm/XuhJ5EzIAjkxvTPPHcOgBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjhs8e/dJMcaacQSFm/XuhJ5EzIAjkxvTPPHcOgBK/img.png&quot; data-alt=&quot;더닝-크루거 효과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjhs8e/dJMcaacQSFm/XuhJ5EzIAjkxvTPPHcOgBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjhs8e%2FdJMcaacQSFm%2FXuhJ5EzIAjkxvTPPHcOgBK%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;445&quot; height=&quot;326&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;더닝-크루거 효과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저기 왼쪽에 무식자가 아니라 절망의 계곡에 있는거면 좋겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 회사일도 열심히 하면서 공부를 많이 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 회사 기술 스택부터 깊이있게&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어가서 1인분을 하려면 회사 기술 스택부터 열심히 공부해야한다. 기존에 공부해오던 것들과 다른 기술스택인 Dropwizard(Java 프레임워크), PostgreSQL, DynamoDB들을 기존에 내가 알던 기술 스택들과 어떤 차이점이 있는지 체크해보며 깊이있게 공부해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 도메인을 깊이있게&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 회사에 빠른 적응을 위해서 도메인 지식들을 제대로 이해하면서 습득해야한다. 팀의 1순위 목표인 '고객이 원하는 제품'을 만들기 위해서 가장 중요한 일이지 않나 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. AI 활용을 깊이있게&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 AI를 얼마나 잘 활용하냐에 따라서 2배부터 10배 이상까지 개인이 낼 수 있는 성과가 달라질 것이다. 그렇다면 결국 회사 입장에서는 AI 활용을 잘하는 개발자를 원할 수 밖에 없다. 따라서 미래에 모두가 원하는 개발자가 되기 위해 지금부터 열심히 여러 AI들을 활용해보며 나의 성과를 폭발적으로 향상시킬 방법에 대해 고민하고 사용해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 기술 블로그를 깊이있게&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년간 진행해왔던 기술 블로그 스터디를 계속 이어서 할 생각이다. 공부했던 것들을 기존의 프로젝트에 적용한 내용 or 깊이있게 공부해본 내용을 기록해보려고 한다. 그리고 더 높은 목표로는 회사 기술 블로그에 내가 쓴 내용을 업로드하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이렇게 4가지를 메인으로 26년을 시작해보려고 한다. 26년 12월쯤에는 저기 위(더닝-크루거)에 '깨달음의 오르막'에 한걸음 정도 올랐음면 좋겠다.&lt;/p&gt;</description>
      <category>회고록</category>
      <category>2025</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/67</guid>
      <comments>https://chobo-backend.tistory.com/67#entry67comment</comments>
      <pubDate>Sun, 28 Dec 2025 18:34:40 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 룩 상세 조회 기능 개선 (Ft. Redis 캐싱)</title>
      <link>https://chobo-backend.tistory.com/64</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 0. 개선 할 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x0ctt/btsQTwA62yT/ihnlgYQWVPG5FGOq8PKZa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x0ctt/btsQTwA62yT/ihnlgYQWVPG5FGOq8PKZa1/img.png&quot; data-alt=&quot;룩 상세 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x0ctt/btsQTwA62yT/ihnlgYQWVPG5FGOq8PKZa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx0ctt%2FbtsQTwA62yT%2FihnlgYQWVPG5FGOq8PKZa1%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;246&quot; height=&quot;529&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1858&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;룩 상세 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;룩 상세 조회 기능은 룩 이미지와 해당 룩에 착용된 상품 리스트를 모두 조회하는 기능이다. 이 기능은 내부적으로 5개의 테이블에서 데이터를 조회하고 있고, 생성된 룩은 생성자만 수정할 수 있어 데이터 변경 빈도가 낮다는 특성을 가지고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 WAS(SpringBoot)&lt;/b&gt;&amp;nbsp;: AWS EC2 t4g.xlarge(4 vCPU, 16GiB Memory)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 DB(MySQL)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;AWS EC2 t4g.medium(2 vCPU, 4GiB Memory)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;단일 Redis&lt;/b&gt;&amp;nbsp; : AWS EC2 t4g.medium(2 vCPU, 4GiB Memory)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Locust&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Pinpoint&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Prometheus + Grafana&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUser : 500&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ramp up : 100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run Time : 5m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Think Time : 0.2s&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGuPJ/btsQPWGdlwH/vmcluVKKkkBAo8nvtq1T7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGuPJ/btsQPWGdlwH/vmcluVKKkkBAo8nvtq1T7K/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGuPJ/btsQPWGdlwH/vmcluVKKkkBAo8nvtq1T7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGuPJ%2FbtsQPWGdlwH%2FvmcluVKKkkBAo8nvtq1T7K%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;384&quot; height=&quot;266&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfZzix/btsQQks1FSA/zBT6XnkmHxkDnKBanf1ABk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfZzix/btsQQks1FSA/zBT6XnkmHxkDnKBanf1ABk/img.png&quot; data-alt=&quot;CPU&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfZzix/btsQQks1FSA/zBT6XnkmHxkDnKBanf1ABk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfZzix%2FbtsQQks1FSA%2FzBT6XnkmHxkDnKBanf1ABk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;195&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CPU&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 534&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span&gt;&lt;span&gt; 1450&lt;/span&gt;ms&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt;&lt;span&gt;&lt;span&gt; 668&lt;/span&gt;ms&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부하테스트 결과 평균 응답 시간이 668ms로 개선이 필요해보인다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. Redis 캐싱 전략&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;룩 상세 조회의 경우 업데이트가 빈번하지 않아 캐시를 적용하기 좋다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;룩이 자주 변경되는 경우 데이터 불일치를 방지하기 위해 캐시를 더 빠르게 갱신해야 하는데 이는 캐시 적용의 효율성을 떨어뜨린다&lt;/b&gt;. 따라서 조회 기능이라 하더라도 데이터 변경 빈도가 높다면 캐시가 적합하지 않을 수 있으며 기능의 특성과 요구사항을 고려하여 캐시 적용 여부를 결정해야 한다. 다만 &lt;b&gt;현재 기능의 경우 룩 생성자만 변경 가능하여 업데이트 빈도가 낮아 캐시 적용에 적합하다고 판단&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;읽기 전략은 Cache-Aside&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 읽기 전략으로는 다음 두 가지를 고려했다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Read&amp;nbsp;Through&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Cache-Aside&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Read Through의 경우 Application 서버는 캐시만 바라보고 캐시 미스가 발생하면 캐시가 직접 DB를 통해 조회&lt;/b&gt;하는 방식이다. 이는 캐시에 장애가 생겼을 때 DB 조회 경로가 차단되어 곧바로 서비스 장애로 이어질 수 있다. 반면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Cache-Aside은 캐시 미스가 발생하더라도 Application 서버가 직접 DB에서 조회해 캐시에 저장&lt;/b&gt;하는 구조이다. 이 경우 Redis에 장애가 발생하더라도 애플리케이션이 DB에서 조회한 데이터를 응답할 수 있어 Redis의 장애가 서비스의 장애로 전파되지 않는다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가용성과 안정성을 고려하여 읽기 전략은 Cache-Aside 방식으로 결정&lt;/b&gt;했다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;쓰기 전략은 Write-Through&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쓰기 전략으로는 다음 3가지를 고려했다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Write-Behind&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Write-Around&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Write-Through&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Write-Behind의 경우 캐시에만 먼저 쓰고 DB에는 백그라운드에서 반영한다. 이 방식도 Read Through와 비슷하게 Redis에 문제가 생기는 경우 데이터 손실 위험이 있다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Write-Around의 경우 DB에만 쓰고 캐시는 업데이트 하지 않는 방식으로 캐시 미스가 나면 그때 DB에서 로드하는 방식&lt;/b&gt;이다. 마지막으로 &lt;b&gt;Write-Through는 DB와 캐시에 동시에 데이터를 쓰는 방식으로 데이터 일관성은 보장&lt;/b&gt;되지만 쓰기 성능이 느리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 데이터 손실 위험이 있는 Write-Behind는 제외했다. 현재 룩 상세 조회의 경우 룩을 생성한 사용자만 데이터 업데이트가 가능하다. 따라서 해당 사용자가 데이터를 변경한 경우 캐시까지 함께 변경해야 기존에 남아있던 캐시로 인해 데이터 불일치 현상이 발생하지 않는다. 따라서 &lt;b&gt;결과적으로는 성능이 낮더라도 데이터 정합성을 위해 Write-Through 방식을 선택&lt;/b&gt;했고 결과적으로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Cache-Aside + Write-Through 조합&lt;/b&gt;이 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. Cache Stampede&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;캐시가 만료된 상황에서 동시에 많은 트래픽이 몰린다면?&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 쇄도(Cache Stampede) 현상이 발생 가능하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐시가 만료되는 순간 동시에 대량의 요청이 DB로 몰리면서 DB 과부하를 일으키고 전체 서비스 장애&lt;/b&gt;로 이어질 수 있다. 해결 방안으로는 분산 락(Distributed Lock)을 활용하여 캐시 갱신 시점에 하나의 요청만 DB에 접근하도록 제어하는 방법이 있다. Redis의 SETNX 명령을 사용해 락을 획득한 스레드만 DB 조회를 수행하고, 나머지는 잠시 대기 후 갱신된 캐시를 사용하도록 구현할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Jitter 방식을 활용하여 캐시 만료 시간에 랜덤한 편차를 추가해 동시 만료를 방지하는 방법&lt;/b&gt;도 있다. Jitter는 캐시 TTL에 무작위 시간을 추가하여 모든 캐시가 동일한 시점에 만료되는 것을 방지한다. 예를 들어 30분 TTL에 &amp;plusmn;3분의 jitter를 적용하면 각 캐시가 27~33분 사이에 랜덤하게 만료되어 캐시 쇄도 현상을 자연스럽게 분산시킬 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;분산락으로 예방하자&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재는 모든 룩들이 일괄적으로 캐싱되는게 아니라 &lt;b&gt;개별적으로 조회가 발생함에 따라 캐싱되기 때문에 Jitter 방식의 효과가 제한적이라고 생각&lt;/b&gt;했다. 하지만 인기 룩의 경우 TTL이 만료되는 시점에 집중적인 트래픽이 발생하여 DB 과부하를 일으킬 가능성이 여전히 존재한다. 따라서 이러한 문제를 해결하기 위해 분산락 방식을 통해 DB 부하를 줄이는 방식을 선택했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RedisCacheWriter의&amp;nbsp;lockingRedisCacheWriter&amp;nbsp;메서드를&amp;nbsp;통해&amp;nbsp;쉽게&amp;nbsp;분산락을&amp;nbsp;적용&lt;/b&gt;할&amp;nbsp;수&amp;nbsp;있었다.&amp;nbsp;내부&amp;nbsp;구현을&amp;nbsp;간략히&amp;nbsp;설명하면&amp;nbsp;캐시&amp;nbsp;미스가&amp;nbsp;발생했을&amp;nbsp;때&amp;nbsp;Redis의&amp;nbsp;SET&amp;nbsp;명령어에&amp;nbsp;NX(키가&amp;nbsp;존재하지&amp;nbsp;않을&amp;nbsp;때만&amp;nbsp;설정)와&amp;nbsp;EX(만료시간&amp;nbsp;설정)&amp;nbsp;옵션을&amp;nbsp;사용하여&amp;nbsp;락을&amp;nbsp;획득한다.&amp;nbsp;락을&amp;nbsp;성공적으로&amp;nbsp;획득한&amp;nbsp;스레드만&amp;nbsp;DB에&amp;nbsp;접근하여&amp;nbsp;데이터를&amp;nbsp;조회하고&amp;nbsp;캐시에&amp;nbsp;저장하며,&amp;nbsp;락&amp;nbsp;획득에&amp;nbsp;실패한&amp;nbsp;다른&amp;nbsp;스레드들은&amp;nbsp;짧은&amp;nbsp;시간&amp;nbsp;대기&amp;nbsp;후&amp;nbsp;캐시에서&amp;nbsp;갱신된&amp;nbsp;데이터를&amp;nbsp;조회하게&amp;nbsp;된다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;동시에&amp;nbsp;&lt;b&gt;여러&amp;nbsp;요청이&amp;nbsp;DB에&amp;nbsp;접근하는&amp;nbsp;것을&amp;nbsp;방지하고&amp;nbsp;시스템&amp;nbsp;안정성을&amp;nbsp;확보&lt;/b&gt;할&amp;nbsp;수&amp;nbsp;있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. Cache Penetration&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;캐시 최신화를 위해 DB 조회 시 빈 값이라면?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cache Penetration&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문제로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;존재하지 않는 데이터에 대한 반복적인 요청이 캐시를 우회하여 지속적으로 DB에 부하&lt;/b&gt;를 가하는 상황이다. &amp;lsquo;값이 없음&amp;rsquo;을 캐싱함으로써 DB의 트래픽을 줄이려면 블룸 필터를 사용하는 방법이 있다. 블룸 필터를 사용하면 확률적으로 캐시 관통을 방지하지만 블룸 필터의 정합성이 깨진다면, 블룸 필터를 복구하기 위해 모든 캐시를 읽어야 해서 운영이 어렵다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: left;&quot;&gt;또 다른 방법으로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;널 오브젝트 패턴을 사용해서 &amp;lsquo;값이 없음&amp;rsquo;을 캐싱&lt;/b&gt;하는 방법이 있다. 객체 타입은 부재를 뜻하는 객체를 선언하여 사용하면 되지만, 원시 타입은 이 객체를 대체할 특정 값을 지정해야 한다. 예를 들어 양수만 존재하는 정수 타입의 데이터를 캐시할 때는 음수인 정수의 최솟값으로 '값이 없음'을 나타내기로 할 수 있다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;캐시 시스템 장애가 발생한다면?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 시스템 장애 시에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Circuit Breaker 패턴을 적용하여 캐시 장애를 감지하고 자동으로 DB 직접 조회로 전환&lt;/b&gt;해야 한다. 장애 감지 후 일정 시간 동안 캐시 호출을 차단하고, 주기적으로 캐시 상태를 확인하여 복구되면 다시 캐시를 사용하도록 한다. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Multi-layer Caching&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전략으로 Redis 장애 시 로컬 캐시(Caffeine)로 fallback하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐시 클러스터링&lt;/b&gt;을 통해 고가용성을 확보하는 것도 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;&lt;b&gt;Graceful Degradation 원칙에 따라 캐시 없이도 서비스가 동작할 수 있도록&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB 성능 최적화와 Connection Pool 튜닝을 병행해야 한다. 장애 상황에서는 비즈니스 적으로 비교적 덜 중요한 기능은 서비스를 중단하고 핵심 기능만 DB를 통해 운영이 될 수 있도록 하자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;개선 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경과 방식은 개선전과 동일하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUser : 500&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ramp up : 100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run Time : 5m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Think Time : 0.2s&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cctQ0U/btsQR4LeLpS/fZnkR9u797Q75ICRgpcuz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cctQ0U/btsQR4LeLpS/fZnkR9u797Q75ICRgpcuz0/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cctQ0U/btsQR4LeLpS/fZnkR9u797Q75ICRgpcuz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcctQ0U%2FbtsQR4LeLpS%2FfZnkR9u797Q75ICRgpcuz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;482&quot; height=&quot;314&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 1474&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 172&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 89&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;534&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1474&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;176.0% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1450ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;172&lt;/span&gt;&lt;/span&gt;ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;88.1% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;668ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;89&lt;/span&gt;&lt;/span&gt;ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;86.7%&amp;nbsp;&amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 534 &amp;rarr; 1474로 약 &lt;b&gt;176.0% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 전체 요청 중 99%가 1450ms &amp;rarr; 172ms 이내에 처리되도록 약 &lt;b&gt;88.1% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 668ms &amp;rarr; 89ms로 약 &lt;b&gt;86.7% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://toss.tech/article/cache-traffic-tip&quot;&gt;https://toss.tech/article/cache-traffic-tip&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/64</guid>
      <comments>https://chobo-backend.tistory.com/64#entry64comment</comments>
      <pubDate>Fri, 26 Sep 2025 02:01:58 +0900</pubDate>
    </item>
    <item>
      <title>[바로] AI 가상 피팅 기능 사용량 제한하기 (Ft. Token Bucket)</title>
      <link>https://chobo-backend.tistory.com/63</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 기능 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clQGtE/btsQ1Zcnm8E/uNLD4dPoAjirYNlukdovJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clQGtE/btsQ1Zcnm8E/uNLD4dPoAjirYNlukdovJ0/img.png&quot; data-alt=&quot;나노 바나나로 톰브라운 가디건을 입어보았다..!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clQGtE/btsQ1Zcnm8E/uNLD4dPoAjirYNlukdovJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclQGtE%2FbtsQ1Zcnm8E%2FuNLD4dPoAjirYNlukdovJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;480&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나노 바나나로 톰브라운 가디건을 입어보았다..!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소 &lt;b&gt;인터넷을 통해 옷을 구매하는 경우 정말 나와 어울리는지 입어볼 수 없다는 것이 문제&lt;/b&gt;였다. 따라서 최근 구글에서 출시한 나노 바나나 모델을 통해 사용자의 사진과 쇼핑몰의 옷 사진을 합성해 보여줄 수 있다면 &lt;b&gt;빠르게 가상으로 피팅하고 구매 결정을 빠르게 할 수 있게 도와 구매 전환율을 높일 수 있을 것이라 생각&lt;/b&gt;했다.(위 사진처럼 - 저는 톰브라운 가디건 없어요) 하지만 &lt;b&gt;구글의 나노 바나나 API는 비용이 들어가는 유료 API 였기에 무제한적으로 사용되는 것을 막기 위한 사용량 제한 로직이 필요&lt;/b&gt;했다. 그리고 그전에 사용자의 사진을 업로드 할 수 있는 기능도 필요했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 사진 업로드 기능 구현(Presigned URL)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 사용자의 사진을 업로드 할 수 있는 기능이 필요하다. S3를 통해 업로드하는 과정에는 3가지 방법이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 서버 경유 업로드 - 클라이언트가 서버로 파일을 보내고 서버가 해당 파일을 S3에 업로드&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 클라이언트 직접 업로드 - 클라이언트가 AWS SDK를 이용해 S3에 직접 파일을 업로드&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Presigned URL 방식 - 서버에게 PresignedURL을 받아 S3로 직접 업로드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 방식을 더 자세히 살펴보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.&amp;nbsp;서버&amp;nbsp;경유&amp;nbsp;업로드&lt;/b&gt;&lt;br /&gt;클라이언트가 서버로 파일을 전송하면 서버가 S3에 업로드하는 방식이다. &lt;b&gt;구현이 간단하고 서버에서 파일을 검증하거나 이미지 리사이징 같은 전처리를 할 수 있다는 장점&lt;/b&gt;이 있다. 하지만 파일이 클라이언트에서 서버로, 다시 서버에서 S3로 두 번 전송되기 때문에 &lt;b&gt;서버 부하가 크고 대역폭을 2배로 소비&lt;/b&gt;한다. 따라서 소량의 작은 파일을 다루거나 업로드 전 전처리가 반드시 필요한 경우에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;클라이언트&amp;nbsp;직접&amp;nbsp;업로드&amp;nbsp;(AWS&amp;nbsp;SDK)&lt;/b&gt;&lt;br /&gt;클라이언트가 AWS SDK를 사용해 S3에 직접 업로드하는 방식이다. &lt;b&gt;서버 부하가 전혀 없고 빠르다는 장점&lt;/b&gt;이 있지만, &lt;b&gt;AWS 자격 증명을 클라이언트에 노출해야 하는 심각한 보안 문제&lt;/b&gt;가 있다. AWS Cognito 같은 서비스를 사용하더라도 Presigned URL 방식과 같이 상세한 권한 관리가 어렵고, 클라이언트 코드에 AWS 리소스 사용 로직이 오픈된다는 점에서 보안상으로도 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;3.&amp;nbsp;Presigned&amp;nbsp;URL&amp;nbsp;방식&lt;/b&gt;&lt;br /&gt;서버가 임시 업로드 URL을 생성해 클라이언트에게 전달하고, 클라이언트는 그 URL로 S3에 직접 업로드하는 방식이다. &lt;b&gt;AWS 자격 증명을 노출하지 않으면서도 서버 부하를 최소화할 수 있어 보안과 성능의 균형&lt;/b&gt;이 가장 좋다. 또한&lt;b&gt; URL 생성 시 파일명, 크기, 만료시간, 컨텐츠 타입 등을 제한할 수 있어 세밀한 권한 제어&lt;/b&gt;가 가능하다. 다만 &lt;b&gt;URL 요청과 업로드라는 2단계 과정이 필요하고, 업로드 완료를 서버에 알리는 콜백 로직이 추가로 필요&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 방식 중에서는 서버 부하와 보안 문제를 고려하여 Presigned URL 방식을 선택했다. 다만 Presigned URL 방식을 선택했을 때의 문제점과 부가적인 문제점을 더 고려해 보아야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추후 S3 비용 문제를 고려한다면?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 비용 정책을 간략하게 정리하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 스토리지 비용 - $0.025 (GB당)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. PUT/POST 요청 비용 - $0.005 per 1,000&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. GET/SELECT 요청 비용 - $0.0004 per 1,000&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. S3 &amp;rarr; 인터넷 (다운로드) - $0.126 per GB (첫 10TB)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;월 사용자 10만 명에 각자 사진 10장(한장 당 1MB) 업로드 및 10번 조회&lt;/b&gt;를 했다고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;스토리지&lt;br /&gt;-&amp;nbsp;총&amp;nbsp;100만&amp;nbsp;장&amp;nbsp;&amp;times;&amp;nbsp;1MB&amp;nbsp;=&amp;nbsp;1TB&lt;br /&gt;-&amp;nbsp;스토리지&amp;nbsp;비용:&amp;nbsp;1,000GB&amp;nbsp;&amp;times;&amp;nbsp;$0.025&amp;nbsp;=&amp;nbsp;$25/월&lt;br /&gt;&lt;br /&gt;다운로드 (각 사진 평균 10회 조회)&lt;br /&gt;-&amp;nbsp;총&amp;nbsp;1,000만&amp;nbsp;회&amp;nbsp;&amp;times;&amp;nbsp;1MB&amp;nbsp;=&amp;nbsp;10TB&lt;br /&gt;-&amp;nbsp;전송&amp;nbsp;비용:&amp;nbsp;10,000GB&amp;nbsp;&amp;times;&amp;nbsp;$0.126&amp;nbsp;=&amp;nbsp;$1,260/월&lt;br /&gt;&lt;br /&gt;요청 비용&lt;br /&gt;-&amp;nbsp;PUT:&amp;nbsp;100만&amp;nbsp;/&amp;nbsp;1,000&amp;nbsp;&amp;times;&amp;nbsp;$0.005&amp;nbsp;=&amp;nbsp;$5&lt;br /&gt;-&amp;nbsp;GET:&amp;nbsp;1,000만&amp;nbsp;/&amp;nbsp;1,000&amp;nbsp;&amp;times;&amp;nbsp;$0.0004&amp;nbsp;=&amp;nbsp;$4&lt;br /&gt;&lt;br /&gt;───────────────────────────────────&lt;br /&gt;월&amp;nbsp;총&amp;nbsp;비용:&amp;nbsp;$1,294&amp;nbsp;(약&amp;nbsp;170만원)&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;S3 비용의 대부분이 다운로드 비용이다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사진과 같은 파일은 자주 변경되는 경우가 없기 때문에 &lt;b&gt;CDN과 같은 캐시를 두어서 다운로드 비용을 절약&lt;/b&gt;할 수 있다. CDN으로 AWS의 CloudFront를 사용할 때 비용은 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. CloudFront &amp;rarr; 인터넷 (다운로드) - $0.085 per GB (첫 10TB)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. HTTPS 요청 - $0.010&amp;nbsp;per&amp;nbsp;10,000&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;S3 다운로드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;- 10,000GB &amp;times; $0.126 = $1,260 &lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;CloudFront&lt;/b&gt;&lt;br /&gt;&lt;b&gt;- 10,000GB &amp;times; $0.085 = $850 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;- CloudFront 요청 비용: 약 $10 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;- 총: $860&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;&lt;b&gt;절감액: $400 (약 32%)&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 S3의 다운로드 비용을 절약하기 위해서 CDN을 사용할 수 있다. 하지만 현재는 시나리오만큼의 많은 양을 저장하고 요청하고 있지는 않기 때문에 추후에 비용 문제를 고려할만큼의 트래픽이 발생한다면 CDN 도입을 고려하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;업로드 완료 확인 누락 문제&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;클라이언트가 &lt;b&gt;S3에 업로드는 했지만 서버에 완료 알림을 보내지 않는 경우&lt;/b&gt;가 발생할 수 있다. 네트워크 끊김, 브라우저 닫기 등으로 콜백이 실패하면 S3에는 파일이 있지만 &lt;b&gt;DB에는 레코드가 없는 불일치 상태&lt;/b&gt;가 된다. 이 문제는 클라이언트 측 재시도 로직과 서버 측 정리 작업으로 대응할 수 있다. 클라이언트에서는 &lt;b&gt;완료 알림 전송을 여러 번 재시도하고, 임계치(예: 3~5회)까지 실패하면 업로드 실패로 간주하여 사용자에게 재업로드를 안내&lt;/b&gt;한다. 서버에서는 주기적인 배치 작업을 통해 DB에 레코드가 없는 고아 파일(orphaned files)을 탐지하고 자동으로 삭제하여 스토리지 비용 낭비를 방지하도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 처리율 제한 알고리즘(Token Bucket)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 알고리즘에는 여러가지 방식이 있었다. 해당 알고리즘을 선택하기 전에 현재 우리 서비스에 어떠한 방식이 필요했는지 체크해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 시간 당 몇번씩 사용 가능 횟수가 충전되는 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 특정 최대치까지는 충전가능(버스트 가능)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 최대한 적은 메모리 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘으로는 다음과 같은 것들이 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Token Bucket&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Fixed Window Counter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Sliding Window Log&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Sliding Window Counter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;1. Token Bucket&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 버킷 알고리즘은 &lt;b&gt;일정한 속도로 토큰이 버킷에 채워지고, 요청이 올 때마다 토큰을 소비하는 방식&lt;/b&gt;으로 최대 용량이 있어서 토큰이 넘치면 더 이상 쌓이지 않는다. 요청 시 토큰이 있으면 처리하고 토큰을 제거하며, 토큰이 없으면 요청을 거부한다. 장점으로는 &lt;b&gt;버스트 트래픽을 허용하면서도 장기적으로는 평균 처리율을 보장&lt;/b&gt;하고, &lt;b&gt;메모리 사용이 적으며(토큰 개수와 마지막 충전 시간만 저장)&lt;/b&gt;&amp;nbsp;구현이 직관적이다. 단점으로는 여러 등급별로 사용량을 다르게 설정한다면 코드 구현이 복잡해질 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Fixed Window Counter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 윈도우 카운터 알고리즘은 &lt;b&gt;시간을 고정된 구간으로 나누고 각 구간마다 요청 횟수를 카운트하는 방식&lt;/b&gt;이다. 예를 들어 1시간을 윈도우로 설정하면 매 정시마다 카운터가 리셋된다. 장점은 구현이 매우 간단하고, 메모리 사용이 적으며(카운터 하나만 필요) 이해하기 쉽다. 단점으로는 윈도우 경계에서 심각한 버스트 문제가 발생할 수 있다. 예를 들어 시간당 100회 제한이 있을 때 00:59에 100회, 01:01에 100회 요청하면 2분 동안 200회 처리가 가능해진다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Sliding Window Log&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이딩 윈도우 로그 알고리즘은 &lt;b&gt;모든 요청의 타임스탬프를 로그로 저장하고, 현재 시간으로부터 윈도우 크기만큼 이전의 요청들을 카운트&lt;/b&gt;하는 방식이다. 새 요청이 올 때마다 오래된 로그를 제거하고 현재 윈도우 내의 요청 개수를 확인한다. 장점은 가장 정확한 제한을 제공하며, 윈도우 경계 문제가 없다. 단점으로는 모든 요청의 타임스탬프를 저장해야 하므로 &lt;b&gt;메모리 사용량이 매우 크고&amp;nbsp;오래된 로그를 정리하는 연산이 필요&lt;/b&gt;하며 대용량 트래픽에서 성능 문제가 발생할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Sliding Window Counter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이딩 윈도우 카운터 알고리즘은 고정 윈도우와 슬라이딩 윈도우의 장점을 결합한 방식이다. &lt;b&gt;이전 윈도우와 현재 윈도우의 카운터를 가중 평균하여 현재 시점의 요청 수를 추정&lt;/b&gt;한다. 예를 들어 현재 시간이 윈도우의 30% 지점이라면 이전 윈도우의 70%와 현재 윈도우의 30%를 합산한다. 장점은 메모리 효율적이면서도(두 개의 카운터만 필요) 고정 윈도우의 경계 문제를 완화하고, 슬라이딩 로그보다 훨씬 가볍다. 단점으로는 &lt;b&gt;근사치를 사용하므로 완벽하게 정확하지 않고, 구현이 다소 복잡하며, 엄격한 제한이 필요한 경우 부적합&lt;/b&gt;할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과적으로는 Token Bucket!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제시된 요구사항에 모두 만족하는 알고리즘은 Token Bucket이다. &lt;b&gt;Token Bucket은 일정 속도로 자동 충전되는 개념을 직접적으로 구현하며, 버킷의 최대 용량으로 충전 상한선을 자연스럽게 표현&lt;/b&gt;할 수 있다. 메모리 측면에서도 현재 토큰 개수와 마지막 충전 시간 두 개의 값만 저장하면 되므로 효율적이다. Sliding Window Log는 모든 타임스탬프를 보관해야 하므로 메모리 사용량이 큽니다. Fixed Window Counter와 Sliding Window Counter는 충전 개념보다는 제한 개념에 가까워 의도와 맞지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>Rate Limiting</category>
      <category>S3</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/63</guid>
      <comments>https://chobo-backend.tistory.com/63#entry63comment</comments>
      <pubDate>Thu, 25 Sep 2025 16:09:43 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 일괄 주문 기능 개선 Vol.2 (Ft. Kafka, Transactional Outbox)</title>
      <link>https://chobo-backend.tistory.com/61</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 0. 개선할 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/56&quot;&gt;https://chobo-backend.tistory.com/56&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758804831267&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;[바로] 일괄 주문 기능 개선 Vol.1 (Ft. Eventual Consistency)&quot; data-og-description=&quot;https://chobo-backend.tistory.com/54 [바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/56&quot; data-og-url=&quot;https://chobo-backend.tistory.com/56&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cmvcuz/hyZJXBP7x3/ug6OWYcKVx1vF8VQwgYNi0/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/ZbnSG/hyZJYOh9Nw/JLKLlqZ11pDJmCmmBe28l0/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/bA8Cjj/hyZJSUQSur/dX3HVtlnAFvxY4dwdcsYCk/img.png?width=2412&amp;amp;height=1618&amp;amp;face=0_0_2412_1618&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/56&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/56&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cmvcuz/hyZJXBP7x3/ug6OWYcKVx1vF8VQwgYNi0/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/ZbnSG/hyZJYOh9Nw/JLKLlqZ11pDJmCmmBe28l0/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/bA8Cjj/hyZJSUQSur/dX3HVtlnAFvxY4dwdcsYCk/img.png?width=2412&amp;amp;height=1618&amp;amp;face=0_0_2412_1618');&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;[바로] 일괄 주문 기능 개선 Vol.1 (Ft. Eventual Consistency)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://chobo-backend.tistory.com/54 [바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서 일괄 주문을 개선하고 목표 성능에 도달하지 못했고 확장성을 고려해 개선을 더 진행해보려고 한다. 기능에 대해서 간단히 설명하면 사용자가 장바구니에 담은 여러 물품들을 한번에 주문하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 문제점 및 설계 고민&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring Event는 이벤트 유실 가능성 존재&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Event는 메모리 기반으로 동작하기 때문에 애플리케이션이 예기치 않게 종료되거나 장애가 발생하면 처리 중이던 이벤트가 유실 가능하다. 특히 현재 기능은 주문, 결제와 같은 비즈니스적으로 정말 중요한 이벤트이기 때문에 절대 유실되면 안된다. 하지만 현재는 이벤트 재처리나 복구 메커니즘이 없어 장애 상황에서 매우 취약한 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Scale-Out 및 MSA 고려 필요&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;Scale-out을 생각해보면&lt;/b&gt; 각 인스턴스는 독립적으로 동작하여 이벤트를 공유할 수 없는 구조적 한계가 있다. 또한 MSA로의 전환을 생각해보면 서비스 간 이벤트 기반 통신이 필수가 되었는데 Spring Event로는 JVM 경계를 넘나드는 이벤트 전달이 불가능하다. 즉, 초기 목표였던 Scale-Out 과 MSA를 고려한다는 목표를 위해서라도 현재 구조는 변화가 필요했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;대안으로는 Kafka!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ의 경우&lt;/b&gt; AMQP 프로토콜을 기반으로 하며 Exchange-Queue-Binding 구조를 통해 다양한 라우팅 패턴을 지원하고, manual acknowledgment 설정으로 메시지 유실 문제도 해결할 수 있다. 하지만 Scale-Out이 제한적이고 병렬 처리가 제한적이라&amp;nbsp;&lt;b&gt;대용량 트래픽 환경에서는 성능 한계&lt;/b&gt;가 있다. 또한 &lt;b&gt;컨슈머 그룹 개념이 없어&lt;/b&gt; 동일한 메시지를 여러 마이크로서비스가 독립적으로 소비하려면 각각 별도 큐를 생성해야 하는 복잡성이 있다. 그리고&amp;nbsp;&lt;b&gt;Redis Pub/Sub은&lt;/b&gt; 인메모리 기반의 초고속 성능을 제공하지만 &lt;b&gt;메시지 영속성이 전혀 보장되지 않고, 구독자 장애 시 메시지 재처리가 불가능&lt;/b&gt;한 구조적 한계가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과적으로 Kafka를 선택한 이유는&lt;/b&gt; 로그 기반 아키텍처로 디스크에 순차 쓰기하여 &lt;b&gt;대용량 스트리밍 환경에서 더 높은 처리량&lt;/b&gt;을 제공하면서도 replication factor를 통해 데이터 안전성을 보장하기 때문이다. 또한 파티션 단위로 메시지를 분산 저장해 수평 확장이 용이하고, 컨슈머 그룹별로 독립적인 오프셋 관리를 통해 같은 토픽을 여러 서비스가 각자의 속도로 소비할 수 있다. 특히 메시지 retention 기간 동안 재처리가 가능하고, exactly-once 처리를 지원해 정확한 전달을 보장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 1차 개선 (Kafka 도입)&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 WAS(SpringBoot)&lt;/b&gt;&amp;nbsp;: AWS EC2 t4g.xlarge(4 vCPU, 16GiB Memory)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 DB(MySQL)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;AWS EC2 t4g.medium(2 vCPU, 4GiB Memory)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;Kafka 단일 Broker&lt;/b&gt;&amp;nbsp; : AWS EC2 t4g.medium(2 vCPU, 4GiB Memory)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Locust&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Pinpoint&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Prometheus + Grafana&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUser : 500&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ramp up : 100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run TIme : 5m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Think Time : 1~3초 랜덤 적용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d427DG/btsQN7g7opl/QV3D8GTzhyb6OhHXvnBcC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d427DG/btsQN7g7opl/QV3D8GTzhyb6OhHXvnBcC1/img.png&quot; data-alt=&quot;Pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d427DG/btsQN7g7opl/QV3D8GTzhyb6OhHXvnBcC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd427DG%2FbtsQN7g7opl%2FQV3D8GTzhyb6OhHXvnBcC1%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;526&quot; height=&quot;344&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LqEqP/btsQPgkeLPc/jRtce2U5Z4h7eaxz5Frnck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LqEqP/btsQPgkeLPc/jRtce2U5Z4h7eaxz5Frnck/img.png&quot; data-alt=&quot;Grafana&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LqEqP/btsQPgkeLPc/jRtce2U5Z4h7eaxz5Frnck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLqEqP%2FbtsQPgkeLPc%2FjRtce2U5Z4h7eaxz5Frnck%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;259&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt;&lt;span&gt;&lt;span&gt; 241&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span&gt; 352ms&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt;&lt;span&gt; 53ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;108.7&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;241&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;121.7% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;402 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;352ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;12.4% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;159 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;53ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;66.7% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 개선 전은 일괄 주문 개선 Vol.1의 마지막 개선 수치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 108.7 &amp;rarr; 241로 약 &lt;b&gt;121.7% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 402ms &amp;rarr; 352ms로 약 &lt;b&gt;12.4% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 159ms &amp;rarr; 53ms로 약 &lt;b&gt;66.7% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 2차 개선 (Ft.&amp;nbsp;Transactional&amp;nbsp;Outbox&amp;nbsp;Pattern)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주문 발생 이벤트는 Kafka로 100% 전송이 되는가?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 주문 기능에서 Redis에서 재고를 차감한 후에 &lt;b&gt;주문 생성 이벤트를 발급해 DB의 재고 차감을 비동기로 진행&lt;/b&gt;하고 있다. 주문 기능은 데이터 일관성이 매우 중요하기에 도중에 이벤트가 유실되어서는 안된다. 그러나 현재 구조에서는 주문 트랜잭션이 Commit된 이후 이벤트를 발행하는 과정에서 &lt;b&gt;서버 장애나 Kafka에 장애가 발생할 경우 메시지 전달에 실패하여 이벤트가 유실될 위험&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 방법으로는 &lt;b&gt;Transactional Outbox Pattern&lt;/b&gt;이 있다. 이는 Kafka로 전송할 이벤트를 비즈니스 트랜잭션 내부에서 DB에 함께 저장하고, 별도의 프로세스가 주기적으로 모니터링해 kafka로 전송하는 방식이다. 장애로 인해 이벤트 전송이 실패하더라도 데이터베이스에 저장된 이벤트를 기반으로 재전송이 가능하므로 이벤트 유실 없이 안정적으로 메시지를 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Outbox 테이블을 Polling 방식을 조회 후 Kafka로 전송&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox에 저장된 메시지를 전송하는 방법에는 2가지가 있다. 먼저 Polling Publisher Pattern은 &lt;b&gt;주기적으로 Outbox 테이블을 조회하여 미발행된 메시지를 찾아 Kafka로 전송하는 방식&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;b&gt;으로 구현이 간단&lt;/b&gt;하다. 반면&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Transaction log tailing Pattern은 데이터베이스 테이블을 직접 조회하는 대신 MySQL의 트랜잭션 로그(binlog)를 실시간으로 읽어 변경사항을 메시지 브로커로 전송하는 방식&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이중에서는 비교적 구현이 간단한 Polling Publisher Pattern을 선택했다. &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;Transaction log tailing Pattern의 경우 CDC(Change Data Capture) 구성이 필요하고 추가적인 인프라 리소스와 복잡성이 요구되기 때문에, 우선 Polling Publisher Pattern으로 구현하여 검증해보기로 했&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;acks=all 설정으로 전송 성공 여부를 체크&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;acks 설정에는 acks=0, acks=1, acks=all &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;3가지 방법이 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;acks=0은 Producer가 메시지 전송 후 응답을 기다리지 않는다. acks=1의 경우 Leader 브로커만 저장하면 응답한다. 마지막으로 acks=all은 Leader와 모든 In-Sync Replica가 저장을 완료해야 응답한다. 따라서 &lt;b&gt;acks=all의 경우 메시지 전송 성공 여부를 거의 100% 보장할 수 있지만 모든 복제본의 저장을 기다려야 하므로 처리량이 낮다&lt;/b&gt;. 일단 주문 기능의 경우 데이터 일관성이 중요하기 때문에 acks=all로 구현하고 추후 성능 문제가 발생할 경우 acks=1를 고려하도록 하겠다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 개선 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 일괄 주문 기능의 아무런 개선이 없었을 때와 Vol.2 개선까지 마친 수치를 비교해 보겠다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;81&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;241&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;197.5% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1340 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;352ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;73.7% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;625 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;53ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;91.5%&amp;nbsp;&amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 81 &amp;rarr; 241로 약 &lt;b&gt;197.5% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 1340ms &amp;rarr; 352ms로 약 &lt;b&gt;73.7% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 625ms &amp;rarr; 53ms로 약 &lt;b&gt;91.5% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 개선을 마치며..&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과적으로 목표 성능 도달!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동시 사용자 수 500명 기준 평균 응답 시간 53ms에 도달하면서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Vol.1 첫 시작의 목표였던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;100ms 이하에 성공&lt;/b&gt;했다. 지금까지 개발을 해오면서 이정도로 딥하게 여러 과정들을 거치면서 성능 개선을 해본 적이 없었는데 이번 개선을 진행하면서 정말 많은 것을 배웠다. 먼저 성능 측정을 위한 부하 테스트, 모니터링에 필요한 여러 도구들을 직접 설치해보고 삽질도 해보면서(Pinpoint 설치가 너무 힘들었다..) 다음 프로젝트에서는 정말 빠르게 개선을 진행할 수 있을 것 같다. &lt;b&gt;무작정 Scale-Up 이나 Scale-Out 하지 않았던 것이 큰 도움이 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직 내가 인지하지 못하는 부분에서 허점이 분명히 있을 것이다. 테스트 과정에서 문제가 있었을 수도 있다. 또한 현재 주문 기능의 경우 결제, 배송 등등 부가적인 요소들을 제외한 상태로 실제 실무의 기능에 비하면 정말 쉬운 기능일 것이다. 따라서 이번 개선은 마쳤지만 현재 기능의 한계점과 추후 개선할 수 있는 방향성을 남겨본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;아키텍처에 SPOF(단일 장애 지점)이 많다&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 아키텍처는 &lt;b&gt;대부분의 경우 단일 노드로 이루어져있어 단일 장애 지점으로 하나만 장애가 생겨도 서비스 전체에 영향&lt;/b&gt;을 준다. 이는 매출에 큰 영향을 줄 수 있는 중대한 문제로 절대 발생해서는 안된다. 따라서 추후 개선에서는 SPOF를 만들지 않고 클러스터로 구축해보며 그 과정에서 생기는 문제들을 만나보고 싶다. 또한 홈서버를 구축해서 k8s 클러스터도 구축해 MSA에서의 문제들도 마주하고 싶다. (얼른 취업을 해서 실무에서의 복잡하고 어려운 문제들을 만나길... )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;MSA 환경에서의 분산 트랜잭션 관리?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E-Commerce 실제 주문 기능을 생각해본다면 1. 주문, 2. 재고 관리, 3. 결제, 4. 배송, 5. 알림 이 정도로 크게 나눌 수 있을 것이고 더 존재할 수도 있다. 그렇다면 MSA에서 해당 트랜잭션들이 어떻게 원자적으로 실행될 수 있을까? 대표적으로는 Saga 패턴을 사용한다는 것을 알 수 있었다. Saga 패턴도 기능의 종류에 따라 &lt;b&gt;Orchestration, Choreography 방식으로 나뉜다. &lt;/b&gt;간단하게 이야기하면 중앙에서 관리하냐 아니면 각 서비스가 독립적으로 관리하냐 차이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 다음 개선에서는 MSA로 서비스를 전환하고 SAGA패턴도 직접 구현해보면서 더 많은 트래픽을 견딜 수 있는 아키텍처를 구성해보려고 한다. 바로 진행하지는 않고 이번 하반기 취업에 실패하면 진행해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>Kafka</category>
      <category>spof</category>
      <category>Transational Outbox</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/61</guid>
      <comments>https://chobo-backend.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 15 Sep 2025 16:01:17 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 스와이프 기능 개선 (Ft. MongoDB, Tomcat Thread 튜닝)</title>
      <link>https://chobo-backend.tistory.com/59</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 0. 개선할 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작하기 전 이해를 위해 용어를 정의하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;룩(Look) : 유저의 전반적인 스타일을 볼 수 있는 사진&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;스와이프(API) : 룩에 좋아요 혹은 싫어요 반응을 남기는 기능으로 API를 뜻한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NvdY4/btsQtnqBfm0/SFxh6H6us2R2x1NL1GgTjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NvdY4/btsQtnqBfm0/SFxh6H6us2R2x1NL1GgTjK/img.png&quot; data-alt=&quot;스와이프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NvdY4/btsQtnqBfm0/SFxh6H6us2R2x1NL1GgTjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNvdY4%2FbtsQtnqBfm0%2FSFxh6H6us2R2x1NL1GgTjK%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;212&quot; height=&quot;356&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스와이프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 룩이 보이면 유저는 &lt;b&gt;왼쪽(싫어요) 오른쪽(좋아요) 스와이프를 통해 반응&lt;/b&gt;을 남길 수 있다. &lt;b&gt;'바로'서비스의 핵심 기능으로 많은 사용자가 지연없이 해당 기능을 이용할 수 있어야한다.&lt;/b&gt; 따라서 성능 개선이 필요했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;핵심 요구사항 정리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동시 사용자 수 500명의 트래픽을 견뎌낼 수 있어야 한다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 상황에서 스와이프 및 룩 조회는 평균응답시간이 200ms 내로 들어와야 한다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 문제점 및 설계 고민&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 부분을 개선할지 생각해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스와이프 시 좋아요 카운트는 어느 시점에 해야할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;스와이프 시 룩 반응 생성 및 룩 좋아요 개수 업데이트가 함께&lt;/b&gt; 이루어진다. 먼저 좋아요 개수를 룩에 따로 둔 이유는 &lt;b&gt;룩 조회 시 좋아요 개수를 세기위한 쿼리가 조회 성능에 악영향&lt;/b&gt;을 미친다 판단했기 때문이다. 다만 좋아요 개수를 동시성 문제 없이 정확히 세기 위해서는 Lock이 필요하고 이는 결국 성능에 영향을 준다. 문제점을 정리하면 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 좋아요 개수 카운트의 Lock으로 인한 대기 및 성능 저하(병목, 성능 저하)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 좋아요 개수 업데이트가 실패하면 스와이프도 실패한다(장애 전파)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말했듯 &lt;b&gt;스와이프는 '바로'의 핵심기능이기에 성능이 중요&lt;/b&gt;하다. 좋아요 개수를 실시간으로 Strong Consistency하게 보여주기 위해 성능을 포기하는 것은 사용자에게 매우 불쾌한 경험을 남길 수 있다. 또한 &lt;b&gt;좋아요 개수가 1~2초 늦게 반영되는 것은 사용자 경험 측면에서 크게 문제가 되지 않기&lt;/b&gt; 때문에 데이터 일관성을 포기하기로 판단했다. 따라서 좋아요 개수 업데이트 로직은 스와이프에서 분리하고 &lt;b&gt;Eventual Consistency를 확보하는 방향으로 변경&lt;/b&gt;하겠다. 이에 대한 내용은 다음 포스팅에 남기겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스와이프는 쓰기 작업이다. 쓰기 작업 성능을 향상시키려면 어떻게 해야할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 성능테스트를 진행해본 결과,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AFH9g/btsQsHwEmzq/5XjZgkjHM7hCKdwklZHUW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AFH9g/btsQsHwEmzq/5XjZgkjHM7hCKdwklZHUW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AFH9g/btsQsHwEmzq/5XjZgkjHM7hCKdwklZHUW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAFH9g%2FbtsQsHwEmzq%2F5XjZgkjHM7hCKdwklZHUW0%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;455&quot; height=&quot;299&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS : 439&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt; 852m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 483ms&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;TPS도 낮고 평균 응답시간, p99도 너무 낮았다. 따라서 개선이 필요했다.&lt;/span&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 여러 스와이프를 모아서 Bulk Insert로 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 메시지 큐 기반 비동기 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 인덱스 사용 최소화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Scale Up, Scale Out&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 모든 걸 적용한다고 하더라도 MySQL의 쓰기 속도는 한계가 있다. &lt;b&gt;대용량 트래픽을 견디려면 결국 DB 자체를 변경&lt;/b&gt;하는게 낫다고 생각했다. 선택지로는 Cassandra, Amazon DynamoDB 등등이 있었으나, 결국 많은 기업들에서 사용하고 있는 MongoDB로 정했다. 기존에 사용해본 경험도 있고 이번 기회에 더 알아가자는 생각으로 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;룩(Look)은 추후 스키마가 바뀔 가능성이 높다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패션 서비스 특성상 &lt;b&gt;룩 데이터는 지속적으로 변경&lt;/b&gt;될 수 밖에 없다. AI 기술이 발전하면서 이미지 분석 결과가 더 정교해지고, 새로운 카테고리가 생기며, 사용자 행동 패턴 분석을 위한 &lt;b&gt;추가 메타데이터가 필요&lt;/b&gt;해진다. 계절별 트렌드 분석, 색상 조합 패턴, 브랜드 선호도 등 비즈니스 요구사항이 늘어날 때마다 &lt;b&gt;새로운 필드가 필요&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 이런 변화에 대응하려면 매번 ALTER TABLE을 실행해야 하는데, 이는 대용량 테이블에서 수십 분간 테이블 락을 발생시켜 &lt;b&gt;서비스가 중단&lt;/b&gt;된다. 또한 컬럼을 추가할 때마다 기존 데이터에 대한 &lt;b&gt;마이그레이션 스크립트 작성, 롤백 계획 수립, 인덱스 재구성 등 복잡한 작업&lt;/b&gt;이 따른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;MongoDB는 스키마리스 구조로 필드 추가나 변경이 애플리케이션 코드 레벨에서 즉시 가능&lt;/b&gt;하다. 새로운 AI 분석 결과나 메타데이터를 추가할 때 기존 도큐먼트는 그대로 두고 새 도큐먼트부터 새 필드를 포함시키면 된다.&amp;nbsp;또한 룩 데이터는 본질적으로 이미지 메타데이터, 태그 배열, AI 분석 결과 등 중첩된 구조를 갖는다. MySQL에서는 이를 여러 테이블로 정규화해야 하지만, MongoDB는 하나의 도큐먼트로 자연스럽게 표현할 수 있어 &lt;b&gt;조회 성능도 개선&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;Document 구조가 지속적으로 변경되면서 Application 서버에서는 여러 버전에 대한 데이터 처리&lt;/b&gt;를 해야한다. 즉 DB에서는 빠르게 구조를 변경할 수 있지만 그 &lt;b&gt;관리에 대한 책임은 Application 서버&lt;/b&gt;가 져야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MongoDB는 조회시 Document 전체를 조회해야한다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'바로' 서비스에서 Look이 많이 사용되는 경우를 생각해보면 스와이프 조회로, 이에 필요한 것으로는 룩ID, 사진정도로 부가적인 메타데이터들이 필요하지는 않다. MongoDB가 읽기에 유리하다고 하지만 MySQL과 같은 복잡한 join 읽기는 성능이 좋지 않다. 따라서 하나의 Document에서 관리하는 것이 성능 측면에서 유리하다. 이와 관련된 문제점으로는 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;내부적인 전체 Document 읽기&lt;/b&gt;: MongoDB에서 Projection을 사용해 특정 필드만 반환받을 수는 있지만, &lt;b&gt;스토리지 엔진 레벨에서는 여전히 Document 전체를 디스크에서 읽어온 후&lt;/b&gt; 필요한 필드만 필터링하는 방식이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불필요한 디스크 I/O&lt;/b&gt;: 1MB 크기의 Look Document에서 10KB의 데이터(룩ID, 사진URL)만 필요해도 전체 1MB를 디스크에서 읽어야 하므로 &lt;b&gt;디스크 I/O 비용은 동일&lt;/b&gt;하게 발생한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대량 조회시 성능 저하&lt;/b&gt;: 스와이프로 수백 개의 Look을 조회할 때마다 각 Document의 모든 데이터를 디스크에서 읽어야 하므로 I/O 병목이 심화된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 메타데이터가 많지 않아 MongoDB로 마이그레이션 했을 때 성능이 잘 나오겠지만 서비스가 성장하면서 메타데이터들이 쌓이기 시작하면 MySQL의 성능이 좋아질 것이라 생각했다. 그리고 그 시점에서 MySQL로 다시 마이그레이션하기는 쉽지 않을 것이다. 일단 &lt;b&gt;스와이프 기능만 MongoDB를 사용하고 룩 조회시 성능에 문제가 있다면 커버링 인덱스나 캐싱을 고려&lt;/b&gt;해보도록 하겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 1차 개선 (좋아요 카운트 로직 제거)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 좋아요 개수 추가 로직을 제거해보았다. 테스트 결과는 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GiBN9/btsQu78EdEV/Mf8tR31qD2IIOWIRcnEPiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GiBN9/btsQu78EdEV/Mf8tR31qD2IIOWIRcnEPiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GiBN9/btsQu78EdEV/Mf8tR31qD2IIOWIRcnEPiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGiBN9%2FbtsQu78EdEV%2FMf8tR31qD2IIOWIRcnEPiK%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;492&quot; height=&quot;327&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS : 603&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt; 495&lt;/span&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt; 266&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;439&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;603&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;37.4% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;852m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;495&lt;/span&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;41.9% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;483ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;266&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;44.9% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 439 &amp;rarr; 603으로 약 &lt;b&gt;37.4% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 852ms &amp;rarr; 495ms로 약 &lt;b&gt;41.9% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 483ms &amp;rarr; 266ms로 약 &lt;b&gt;44.9% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게 성능이 개선되었다. 이어서 바로 MongoDB으로 넘어가겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 2차 개선 (MongoDB)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스와이프만 MongoDB로 마이그레이션을 진행했다. 먼저 테스트 결과는 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brRaNc/btsQwKsLyY0/b8FhsuK1c6xeMK0ZfRvBK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brRaNc/btsQwKsLyY0/b8FhsuK1c6xeMK0ZfRvBK0/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brRaNc/btsQwKsLyY0/b8FhsuK1c6xeMK0ZfRvBK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrRaNc%2FbtsQwKsLyY0%2Fb8FhsuK1c6xeMK0ZfRvBK0%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;319&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mpseJ/btsQyCNZnwh/WdzLYDCaKvqiOUS6b55Wgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mpseJ/btsQyCNZnwh/WdzLYDCaKvqiOUS6b55Wgk/img.png&quot; data-alt=&quot;p99 latency&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mpseJ/btsQyCNZnwh/WdzLYDCaKvqiOUS6b55Wgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmpseJ%2FbtsQyCNZnwh%2FWdzLYDCaKvqiOUS6b55Wgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;238&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;p99 latency&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS : 1030&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 240ms&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt; 171&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3662&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFkjY/btsQypainZY/QF4ZmXLOY8KnkituUl8OM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFkjY/btsQypainZY/QF4ZmXLOY8KnkituUl8OM1/img.png&quot; data-alt=&quot;Pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFkjY/btsQypainZY/QF4ZmXLOY8KnkituUl8OM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFkjY%2FbtsQypainZY%2FQF4ZmXLOY8KnkituUl8OM1%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;3662&quot; height=&quot;872&quot; data-origin-width=&quot;3662&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SPRING_TX가 2번 발생!!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinpoint를 살펴보던 중 다음을 발견할 수 있었다. &lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;b&gt;SPRING_TX가 2번 발생하는데 각각 13ms, 20ms으로 거슬렸다&lt;/b&gt;..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;@Transactional 범위를 최대한 줄이기 위해&lt;/span&gt; @Transactional을 domain layer에서 사용하고 있었다. 다만 이번 API의 경우 User랑 Look 무결성 체크를 위해 MySQL을 조회하고 있었는데, 한번의 커넥션을 위해 Application layer에서도 사용하고 있었다. 이 무결성 체크를 제거하면서 @Transacional 도 제거했어야 하는데 실수였다.&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;내부에 있는 &lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;@Transactional를 지우고 다시 성능 테스트를 해보겠다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6fam0/btsQvMLoDxS/8HO3alp7ujstL7KtNR4of1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6fam0/btsQvMLoDxS/8HO3alp7ujstL7KtNR4of1/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6fam0/btsQvMLoDxS/8HO3alp7ujstL7KtNR4of1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6fam0%2FbtsQvMLoDxS%2F8HO3alp7ujstL7KtNR4of1%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;585&quot; height=&quot;382&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS : 1053&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;234ms&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;165&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2738&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dm7CKB/btsQzAPNyGp/IwnCN22UYT2tjlLuai5ryk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dm7CKB/btsQzAPNyGp/IwnCN22UYT2tjlLuai5ryk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dm7CKB/btsQzAPNyGp/IwnCN22UYT2tjlLuai5ryk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdm7CKB%2FbtsQzAPNyGp%2FIwnCN22UYT2tjlLuai5ryk%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;735&quot; height=&quot;171&quot; data-origin-width=&quot;2738&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;내부 @Transactional은 작동안해야 하는거 아닌가?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;사진처럼 이번에는 한번만 발생했다. 하지만 중복으로 사용하더라도 실행을 완전히 스킵하는 방법이 없을 지 propagation 종류에 대해 더 알아보았다. 먼저&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;@Transactional의 Propagation 기본값은 직접 코드를 살펴보면 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757742897433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {

	                 ----중략----
    
    Propagation propagation() default Propagation.REQUIRED;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;b&gt;Propagation.REQUIRED은 기존 트랜잭션이 있으면 참여하고 없다면 새로 생성&lt;/b&gt;한다. 따라서 중복으로 설정하더라도 기존 트랜잭션에 참여하기에 &lt;b&gt;별도 시간 소요가 없을 것이라 생각했지만 위의 테스트처럼 시간은 소요&lt;/b&gt;된다. 시간될 때 내부 로직을 점검해서 왜 그런지 파악하는 시간을 가져야겠다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;또한 아주 약간의 성능개선이 있었지만 여전히 200ms이 넘는 작업들은 왜 그런지 파악했다. 그 결과 요청이 repository 까지 흘러들어오는 모든 클래스에는 시간이 부여되고 있었다. 2가지 의문이 들었다. 개선 작업을 마치고 아래 의문에 대한 추가적인 조사가 필요하다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;1. 최대한 효율적인 코드를 작성하는 방법엔 뭐가 있을까?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #020817; text-align: start;&quot;&gt;2. @Transactional 은 꼭 필요한가?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (1차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (2차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;603&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;1053&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;74.6%&amp;nbsp;&amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;495&lt;/span&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;234ms&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;52.7%&amp;nbsp;&amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;266&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;165&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;38.0%&amp;nbsp;&amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 603 &amp;rarr; 1053으로 약 &lt;b&gt;74.6% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 495ms &amp;rarr; 234ms로 약 &lt;b&gt;52.7% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 266ms &amp;rarr; 165ms로 약 &lt;b&gt;38.0% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 3차 개선 (MongoDB Connection Pool 튜닝)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 개선 이후 200ms가 넘는 요청을 확인해보았을 때 MongoDB 접근시에도 30ms 정도 딜레이가 있었다. 따라서 커넥션 풀 조정을 통해 최대한 줄일 수 있는 지 테스트를 진행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;vUsers : 1000명&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Duration : 3m&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sleep Time : 500ms&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Connection Pool SIze : 10, 20, 30, 40, 100 (5가지 테스트)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1194&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvzoJh/btsQzCmBt6r/OOKkfd5dNG5UbxD02oAgS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvzoJh/btsQzCmBt6r/OOKkfd5dNG5UbxD02oAgS1/img.png&quot; data-alt=&quot;TPS, Average Response Time&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvzoJh/btsQzCmBt6r/OOKkfd5dNG5UbxD02oAgS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvzoJh%2FbtsQzCmBt6r%2FOOKkfd5dNG5UbxD02oAgS1%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;595&quot; height=&quot;366&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS, Average Response Time&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxjL8h/btsQxvbizrb/8OuzeyDbcwj7m41OmBf8G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxjL8h/btsQxvbizrb/8OuzeyDbcwj7m41OmBf8G1/img.png&quot; data-alt=&quot;CPU Usage&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxjL8h/btsQxvbizrb/8OuzeyDbcwj7m41OmBf8G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxjL8h%2FbtsQxvbizrb%2F8OuzeyDbcwj7m41OmBf8G1%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;599&quot; height=&quot;370&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CPU Usage&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과는 표에서 알 수 있듯이 &lt;b&gt;유의미한 결과를 찾지는 못했다&lt;/b&gt;. 그에 대한 이유를 생각해보자면 해당 쓰기 작업의 경우 size pool이 10,20인 경우에도 충분히 처리할 수 있는 수준의 작업이었고, 결국 API 서버가 병목이었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dR2gH3/btsQzGP1jw7/hcmbizl4I9NsarY41RvsIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dR2gH3/btsQzGP1jw7/hcmbizl4I9NsarY41RvsIk/img.png&quot; data-alt=&quot;Grafana - API Server&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dR2gH3/btsQzGP1jw7/hcmbizl4I9NsarY41RvsIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdR2gH3%2FbtsQzGP1jw7%2Fhcmbizl4I9NsarY41RvsIk%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;661&quot; height=&quot;252&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana - API Server&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;439&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;1053&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;139.9% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;852m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;234ms&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;72.5% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;483ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;165&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;65.8% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 439 &amp;rarr; 1053으로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;139.9% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 852ms &amp;rarr; 234ms로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;72.5% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 483ms &amp;rarr; 165ms로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;65.8% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 4차 개선 (Tomcat Thread 튜닝)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 3차 개선까지 진행하고 마무리했었는데 Tomcat 쓰레드가 Max에 도달하는 부분에 대한 개선이 필요하다는 생각이 들어 추가적인 테스트를 진행한다. 다만 이번에는 세션 검사 로직을 제거하고 진행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 WAS(SpringBoot)&lt;/b&gt;&amp;nbsp;: AWS EC2 t4g.xlarge(4 vCPU, 16GiB Memory)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 DB(MongoDB)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;AWS EC2 t4g.medium(2 vCPU, 4GiB Memory)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Locust&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Pinpoint&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: Prometheus + Grafana&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp;테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUser : 500&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ramp up : 100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run TIme : 5m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Think Time : 300ms&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tomcat Max Thread Pool Size : 100, 200, 300, 400 (4가지)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muZZ3/btsQStjgUoq/HR7LSNRrCfxJdFNfrvoock/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muZZ3/btsQStjgUoq/HR7LSNRrCfxJdFNfrvoock/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muZZ3/btsQStjgUoq/HR7LSNRrCfxJdFNfrvoock/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuZZ3%2FbtsQStjgUoq%2FHR7LSNRrCfxJdFNfrvoock%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;339&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeepNq/btsQQ1Bnz6G/n7NWNbI7jCSJiJSO31soQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeepNq/btsQQ1Bnz6G/n7NWNbI7jCSJiJSO31soQ1/img.png&quot; data-alt=&quot;AVG, p99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeepNq/btsQQ1Bnz6G/n7NWNbI7jCSJiJSO31soQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeepNq%2FbtsQQ1Bnz6G%2Fn7NWNbI7jCSJiJSO31soQ1%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;498&quot; height=&quot;306&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AVG, p99&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Tomcat 쓰레드 풀 역시 사이즈에 비례에 성능이 좋아지지 않는다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰레드 풀 사이즈를 무작정 늘린다고 성능이 좋아지지 않았다. 오히려 미세하지만 성능이 좋지 않은 모습을 볼 수 있었다. CPU Context Switching 비용 증가가 성능에 악영향을 미쳤을 것이라 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Redis 세션 체크 로직이 병목이었다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 로직만 제거했을 뿐인데 평균 응답 시간이 전부 100ms 이하로 측정되었다. 이는 곧 세션을 검증하기 위한 Redis에서 병목이 발생함을 알 수 있었다. Redis를 현재 t4g.nana 스펙에서 사용하고 있었기에 성능 저하가 발생된 것으로 판단된다. 이러한 병목 해결로는 현재 Scale-Up이 가장 좋아보이기는 하지만 클러스터 구성에 대해서도 학습을 진행한 뒤 어떤 방식으로 개선할지 선택하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 6. 결과 정리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;439&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;1233&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;180.9% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;852m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;67m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;92.1% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;483ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;34&lt;/span&gt;ms&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;93.0% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 439 &amp;rarr; 1233으로 약 &lt;b&gt;180.9% 증가&lt;/b&gt; (2.8배)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 852ms &amp;rarr; 67ms로 약 &lt;b&gt;92.1% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 평균 응답 시간이 483ms &amp;rarr; 34ms로 약 &lt;b&gt;93.0% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;&quot;&gt;Redis에 대한 추가 학습 필요!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 목표성능 수치에는 도달했지만 세션 검증을 제외한 수치이기 때문에 추가적인 개선이 필요하다. 세션 검증 부분이 병목이 될 것이라고는 예상하지 못했기에 Redis에 내부 구조에 대한 추가학습과 JWT 전환도 다시 고민해야할 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>MongoDB</category>
      <category>Thread</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/59</guid>
      <comments>https://chobo-backend.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 11 Sep 2025 12:26:39 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 인기상품조회 기능 개선 (Ft. DB Connection Pool)</title>
      <link>https://chobo-backend.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;Products(상품) 테이블에는 180만개의 데이터&lt;/b&gt;가 적재되어있다. 180만개였던 이유는 무신사의 경우 1만개 브랜드가 입점해있었고 '바로'에서는 그의 5분의 1 수준인 &lt;b&gt;2000개 브랜드 기준으로 브랜드당 3개의 카테고리, 카테고리 당 300개의 상품&lt;/b&gt;을 둔다고 가정한 수치이다. 기능 개발을 마치고 API들을 테스트해보던 중 인기상품조회 API의 응답시간이 느리다는 것을 파악하게 되어 개선을 시작했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기능에 대해 좀 더 자세히 설명하자면 상품을 좋아요 개수 순서대로 보여주는 인기상품조회 기능이다. Cursor를 활용한 무한스크롤 방식으로 구현하였고, CurosrId로는 likeCount(좋아요 수), productId(상품 ID)를 복합적으로 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 먼저 개선 전 &lt;b&gt;부하테스트를 nGrinder로 진행&lt;/b&gt;했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAS(SpringBoot) 1대&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: AWS t4g.xlarge(4 vCPU, 16GIB Memory)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 1대&lt;/b&gt; : AWS t4g.micro(2 vCPU, 1GIB Memory)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: nGrinder&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Pinpoint&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Prometheus + Grafana&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUsers : 10명&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sleep Time : 1000ms&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;☠️ 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XJB2N/btsQm0uX2Lt/sPJ3xpoLp4QpLldhRAaQDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XJB2N/btsQm0uX2Lt/sPJ3xpoLp4QpLldhRAaQDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XJB2N/btsQm0uX2Lt/sPJ3xpoLp4QpLldhRAaQDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXJB2N%2FbtsQm0uX2Lt%2FsPJ3xpoLp4QpLldhRAaQDk%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;743&quot; height=&quot;433&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS : 1&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;s&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Average response time&amp;nbsp;:&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;9.2s&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TPS, 응답시간 모두 처참했다.. DB가 병목이었을 것이었기에 Grafana를 통해 MySQL까지 모니터링하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp0CQ3/btsQmUVSk5H/k31bE0uaxHJ4puFgPk9xZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp0CQ3/btsQmUVSk5H/k31bE0uaxHJ4puFgPk9xZk/img.png&quot; data-alt=&quot;Grafana(MySQL)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp0CQ3/btsQmUVSk5H/k31bE0uaxHJ4puFgPk9xZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp0CQ3%2FbtsQmUVSk5H%2Fk31bE0uaxHJ4puFgPk9xZk%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;741&quot; height=&quot;366&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(MySQL)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 전반적으로 많아보인다.. 하나하나씩 해결해보자. 일단 가장 처음으로는 Slow Query 부분을 살펴보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdGZrL/btsQmhYmxts/uQmSkhSO0DOC2Nx1N8IiCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdGZrL/btsQmhYmxts/uQmSkhSO0DOC2Nx1N8IiCk/img.png&quot; data-alt=&quot;Grafana(MySQL)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdGZrL/btsQmhYmxts/uQmSkhSO0DOC2Nx1N8IiCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdGZrL%2FbtsQmhYmxts%2FuQmSkhSO0DOC2Nx1N8IiCk%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;679&quot; height=&quot;245&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(MySQL)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Slow Query가 발생했던 것을 확인할 수 있다. 기존에 Slow Query 측정 기준을 1s로 설정을 해두었고, 카운트가 집계되는 것으로 보아 1s가 넘는 쿼리들이 발생하고 있다는 뜻이다. 그럼 이제부터 어떤 쿼리가 문제였는지 찾아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 원인 탐색&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Slow Query 로그 체크&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slow Query가 발생했기 때문에 Slow Query 로그부터 살펴보았고, 내용은 다음과 같았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757049954726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET timestamp=1757040717;
select p1_0.id,p1_0.created_at,p1_0.description,p1_0.likes_count,p1_0.modified_at,p1_0.product_name,p1_0.price,p1_0.quantity,p1_0.store_id,p1_0.thumbnail_url 
from products p1_0 
where (null is null or exists(select 1 from product_categories pc1_0 where pc1_0.product_id=p1_0.id and pc1_0.category_id=null)) and (null is null or p1_0.likes_count&amp;lt;null or (p1_0.likes_count=null and p1_0.id&amp;lt;null)) order by p1_0.likes_count desc,p1_0.id desc limit 22;
# Time: 2025-09-05T02:52:07.475843Z
# User@Host: baro[baro] @  [xx.xxx.xxx.xx]  Id:   873
# Query_time: 10.005079  Lock_time: 0.000002 Rows_sent: 22  Rows_examined: 1800022&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면, &lt;b&gt;현재 22개의 행을 찾기 위해 180만개의 행을 모두 찾는 Full Table Scan&lt;/b&gt;이 발생하고 있었다. 어떤 쿼리가 문제였는지 확인했기 때문에 이 쿼리가 내부적으로 어떻게 동작하고 있는지 파악하기 위해 MySQl의 실행 계획을 분석해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. EXPLAIN ANALYZE를 통한 실행 계획 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1757054616725&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-&amp;gt; Limit: 22 row(s)  (cost=190264 rows=22) (actual time=2878..2878 rows=22 loops=1)
    -&amp;gt; Sort row IDs: p1_0.likes_count DESC, p1_0.id DESC, limit input to 22 row(s) per chunk  (cost=190264 rows=1.78e+6) (actual time=2878..2878 rows=22 loops=1)
        -&amp;gt; Table scan on p1_0  (cost=190264 rows=1.78e+6) (actual time=0.0313..2512 rows=1.8e+6 loops=1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 실행 계획은 다음과 같다. 복잡해보이니 하나씩 확인해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;-&amp;gt; Limit: 22 row(s) (cost=190264 rows=22) (actual time=2878..2878 rows=22 loops=1)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작&lt;/b&gt;: 최종 결과를 22개 행으로 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상 비용&lt;/b&gt;: 190,264 (매우 높음)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 실행시간&lt;/b&gt;: 2878 - 2878 = 약 0ms - 거의 안걸림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 22개 행 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;-&amp;gt; Sort row IDs: p1_0.likes_count DESC, p1_0.id DESC, limit input to 22 row(s) per chunk (cost=190264 rows=1.78e+6) (actual time=2878..2878 rows=22 loops=1)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작&lt;/b&gt;: likes_count와 id를 내림차순으로 정렬하면서 상위 22개만 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 대상&lt;/b&gt;: 1,780,000개 행 (1.78e+6)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 실행시간&lt;/b&gt;: &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;2878 - 2878 = 약 0ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; - 거의 안걸림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최적화&lt;/b&gt;: &quot;limit input to 22 row(s) per chunk&quot;로 메모리 효율적 정렬 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;-&amp;gt; Table scan on p1_0 (cost=190264 rows=1.78e+6) (actual time=0.0313..2512 rows=1.8e+6 loops=1)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작&lt;/b&gt;: products 테이블 전체 스캔 (Full Table Scan)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 실행시간&lt;/b&gt;: 2,512 - 0.03 = 약 2500ms 소요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리량&lt;/b&gt;: 1,800,000개 행 모두 읽음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉 Full Table Scan 이 문제다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 180만개를 모두 읽는 Full Table Scan이 문제라고 판단해 먼제 이를 발생시키지 않기 위해 Index를 적용시키기로 결정하였다. 그 전에 현재 쿼리가 어떤 형태로 발생하는지 자세히 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. Query 살펴보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;먼저 쿼리를 자세히 살펴보자&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757076677601&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    p.id,
    p.created_at,
    p.description,
    p.likes_count,
    p.modified_at,
    p.product_name,
    p.price,
    p.quantity,
    p.store_id,
    p.thumbnail_url 
FROM products p 
WHERE 
    -- 카테고리 필터링: categoryId가 제공되면 해당 카테고리 상품만, 아니면 모든 상품
    (:categoryId IS NULL 
     OR EXISTS (
         SELECT 1 
         FROM product_categories pc 
         WHERE pc.product_id = p.id 
         AND pc.category_id = :categoryId
     )) 
    -- 커서 기반 페이징: 이전 페이지의 마지막 항목 이후부터 조회
    AND (:cursorLikes IS NULL 
         OR p.likes_count &amp;lt; :cursorLikes 
         OR (p.likes_count = :cursorLikes AND p.id &amp;lt; :cursorId))
ORDER BY 
    p.likes_count DESC,  -- 좋아요 수 내림차순 (인기순)
    p.id DESC           -- 같은 좋아요 수일 때 최신 순
LIMIT :pageSize;        -- Pageable에서 제공되는 페이지 크기&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;위의 Query가 아래의 JPQL에 의해서 발생하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757077684297&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select p from Product p
    where (:categoryId is null or exists (
             select 1 from ProductCategory pc
             where pc.product = p and pc.category.id = :categoryId ))
      and (:cursorLikes is null
         or p.likesCount &amp;lt; :cursorLikes
         or (p.likesCount = :cursorLikes and p.id &amp;lt; :cursorId))
    order by p.likesCount desc, p.id desc
&quot;&quot;&quot;)
fun findPopularProductsByCursor(
    @Param(&quot;categoryId&quot;) categoryId: Long?,
    @Param(&quot;cursorLikes&quot;) cursorLikes: Int?,
    @Param(&quot;cursorId&quot;) cursorId: Long?,
    pageable: Pageable,
): Slice&amp;lt;Product&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 조건부 카테고리 필터링(WHERE 절)&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;categoryId가 NULL이면 모든 상품 조회&lt;/li&gt;
&lt;li&gt;값이 있으면 해당 카테고리의 상품만 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 커서 기반 페이징(WHERE 절)&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;전통적인 OFFSET 방식 대신 커서 기반 방식 사용&lt;/li&gt;
&lt;li&gt;cursorLikes와 cursorId를 기준으로 다음 페이지 결정&lt;/li&gt;
&lt;li&gt;대용량 데이터에서 일관된 성능과 중복 방지 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 복합 정렬(ORDER BY 절)&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;1차: likes_count DESC (인기순)&lt;/li&gt;
&lt;li&gt;2차: id DESC (최신순 - 동일한 좋아요 수일 때)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 1차 개선 (복합 인덱스)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 &lt;b&gt;WHERE 절과 ORDER BY 절에서 공통적으로 사용되고 있는 likesCount와 productId 컬럼에 복합인덱스를 생성&lt;/b&gt;하였다. 이렇게 인덱스를 설계 한다면 MySQL 옵티마이저는 다음과 같은 최적화를 수행할 수 있다. 먼저 인덱스 스캔만으로 정렬된 결과를 얻을 수 있어 &lt;b&gt;별도의 filesort 작업이 불필요&lt;/b&gt;하다. 즉 복합인덱스 (likesCount, productId)가 생성됐을 때, 인덱스 자체가 이미 이 순서로 정렬되어 있기 때문에 ORDER BY likes_count DESC, id DESC 조건을 만족하는 데이터를 순차적으로 읽기만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;커서 기반 페이징의 WHERE 조건이 인덱스의 선두 컬럼들과 일치하여 효율적인 범위 스캔(Range Scan)이 가능&lt;/b&gt;하다. 만약 인덱스 구조와 조건이 일치하지 않는다면, MySQL은 인덱스를 활용하지 못하고 전체 테이블 스캔이나 비효율적인 인덱스 스캔을 수행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  목표 성능&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Concurrent Users(동시 사용자 수)&amp;nbsp;= 500명&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Average Response Time(평균 응답 시간)&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;200ms 이하&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TPS(Think Time 1000ms 기준)&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;=&lt;/span&gt; 500&amp;nbsp;TPS&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;b&gt;vUsers를&amp;nbsp;50명으로 진행&lt;/b&gt;했을 때 응답시간이 매우 길어져 time-out으로 인해 측정할 수가 없었고, 10명으로 최대한 줄여서 테스트를 진행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;vUsers : 10명&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sleep Time : 1000ms&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 개선 결과(Vusers : 10명)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2416&quot; data-origin-height=&quot;1694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AfHDj/btsQnp1TrFf/0R6RPI7t4NsOcPEV832Blk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AfHDj/btsQnp1TrFf/0R6RPI7t4NsOcPEV832Blk/img.png&quot; data-alt=&quot;nGrinder&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AfHDj/btsQnp1TrFf/0R6RPI7t4NsOcPEV832Blk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAfHDj%2FbtsQnp1TrFf%2F0R6RPI7t4NsOcPEV832Blk%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;720&quot; height=&quot;505&quot; data-origin-width=&quot;2416&quot; data-origin-height=&quot;1694&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;1436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5iTEQ/btsQnuB788q/sZZkpjT97INZM57DRN5jKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5iTEQ/btsQnuB788q/sZZkpjT97INZM57DRN5jKk/img.png&quot; data-alt=&quot;Grafana(MySQL)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5iTEQ/btsQnuB788q/sZZkpjT97INZM57DRN5jKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5iTEQ%2FbtsQnuB788q%2FsZZkpjT97INZM57DRN5jKk%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;357&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;1436&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(MySQL)&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;76&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;7,500% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;10000 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;66.8 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;99.3% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;9200 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;49 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;99.4% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 1 &amp;rarr; 76으로 약 &lt;b&gt;7,500% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 10,000ms &amp;rarr; 66.8ms로 약 &lt;b&gt;99.3% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 9,200ms &amp;rarr; 49ms로 약 &lt;b&gt;99.5% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스만으로 성능이 상당히 개선되었고 신난 마음으로 목표한 TPS 도달을 위해 바로 vUsers를 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;500명으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;높이고 부하테스트를 한번 더 진행하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUsers : 500명&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sleep Time : 1000ms&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 개선 결과(Vusers : 500명)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2406&quot; data-origin-height=&quot;1672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpi5xg/btsQmcPYoTn/6leZuhrchnjrLt66XbZ5L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpi5xg/btsQmcPYoTn/6leZuhrchnjrLt66XbZ5L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpi5xg/btsQmcPYoTn/6leZuhrchnjrLt66XbZ5L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpi5xg%2FbtsQmcPYoTn%2F6leZuhrchnjrLt66XbZ5L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;495&quot; data-origin-width=&quot;2406&quot; data-origin-height=&quot;1672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TPS : 115&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1.58s&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 635ms&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존보다 TPS가 상당히 개선되긴 하였지만 &lt;b&gt;아직 목표 TPS인 500까지 가려면 너무 멀었다&lt;/b&gt;. 일단 Application 서버 상태를 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Application Server&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;2916&quot; data-origin-height=&quot;1660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w6YoZ/btsQmbwJnGH/GXHqz5Zv0OxLI62B3Fnj21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w6YoZ/btsQmbwJnGH/GXHqz5Zv0OxLI62B3Fnj21/img.png&quot; data-alt=&quot;Grafana(Application Server)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w6YoZ/btsQmbwJnGH/GXHqz5Zv0OxLI62B3Fnj21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw6YoZ%2FbtsQmbwJnGH%2FGXHqz5Zv0OxLI62B3Fnj21%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;730&quot; height=&quot;416&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;1660&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(Application Server)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;MySQL&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;2920&quot; data-origin-height=&quot;1448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p8z5a/btsQlnqSgKZ/klYAKW2wyzkO7gSCVo7gO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p8z5a/btsQlnqSgKZ/klYAKW2wyzkO7gSCVo7gO1/img.png&quot; data-alt=&quot;Grafana(MySQL)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p8z5a/btsQlnqSgKZ/klYAKW2wyzkO7gSCVo7gO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp8z5a%2FbtsQlnqSgKZ%2FklYAKW2wyzkO7gSCVo7gO1%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;731&quot; height=&quot;362&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(MySQL)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2QM9b/btsQm20o1X2/Xmaf91M6xYd1FqJiO3LBX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2QM9b/btsQm20o1X2/Xmaf91M6xYd1FqJiO3LBX0/img.png&quot; data-alt=&quot;Grafana(Application Server)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2QM9b/btsQm20o1X2/Xmaf91M6xYd1FqJiO3LBX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2QM9b%2FbtsQm20o1X2%2FXmaf91M6xYd1FqJiO3LBX0%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;728&quot; height=&quot;166&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana(Application Server)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;InnoDB BP Hit Ratio를 보면 99.93%로 거의 모든 요청이 메모리에서 처리&lt;/b&gt;되고 있는 것을 확인할 수 있다. 즉 지금 MySQL은 매우 효율적으로 응답을 처리하고 있다는 것이다. 그리고 이후 여러 지표를 살펴보다 Application 서버의 HikariCP 통계를 볼 때 &lt;b&gt;최대 181개까지 connection을 기다리고 있는 것을 확인&lt;/b&gt;할 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 2차 개선 (DB Connection Pool Size 튜닝)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 HikariCP를 늘리면 해결이 될까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 늘리기 전의 생각으로는 쓰레드가 과도하게 많아지는 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CPU가 Context Switching을 하는데 작업 비율이 늘어나게 되어 성능이 저조&lt;/b&gt;해질 수 있다는 것은 이론적으로 알고 있었다. 기존에 HikariCP를 최소 8, 최대 16으로 설정했었는데, 이와 관련해서 자료를 찾던 중 HikariCP 공식문서에서 다음과 같은 수식을 찾을 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&lt;/a&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;connections = ((core_count * 2) + effective_spindle_count)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MySQL이 설치되어있는 EC2의 스펙은 t4g.micro로 2개의 코어&lt;/b&gt;를 가지고 있다. 그렇다면 위 공식에 의거하여 Connection 수를 4로 잡는다면 성능이 더 빨라질까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vUsers : 500명&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sleep Time : 1000ms&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HikariCP Size : 2, 4, 6, 8, 10, 12, 14, 16(8가지 각각 5번씩)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUoqfn/btsQnupPa4F/SCcyDH3b9nG4ASOFG7skJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUoqfn/btsQnupPa4F/SCcyDH3b9nG4ASOFG7skJ0/img.png&quot; data-alt=&quot;40번의 테스트...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUoqfn/btsQnupPa4F/SCcyDH3b9nG4ASOFG7skJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUoqfn%2FbtsQnupPa4F%2FSCcyDH3b9nG4ASOFG7skJ0%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;621&quot; height=&quot;441&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1346&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;40번의 테스트...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HikariCP Size 별로 총 40번의 테스트를 진행한 결과를 표로 정리해보았다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. TPS&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zkqvk/btsQnWM0Zk3/hwp7VmtC5strMTkH9CmFDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zkqvk/btsQnWM0Zk3/hwp7VmtC5strMTkH9CmFDK/img.png&quot; data-alt=&quot;TPS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zkqvk/btsQnWM0Zk3/hwp7VmtC5strMTkH9CmFDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZkqvk%2FbtsQnWM0Zk3%2Fhwp7VmtC5strMTkH9CmFDK%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;628&quot; height=&quot;294&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TPS&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;b&gt;2. Application 서버 CPU와 DB CPU 평균 사용률&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y6cD9/btsQpDZ84Vb/rlcUhu70FFOtOAZkyA33a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y6cD9/btsQpDZ84Vb/rlcUhu70FFOtOAZkyA33a0/img.png&quot; data-alt=&quot;CPU 사용률&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y6cD9/btsQpDZ84Vb/rlcUhu70FFOtOAZkyA33a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy6cD9%2FbtsQpDZ84Vb%2FrlcUhu70FFOtOAZkyA33a0%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;623&quot; height=&quot;291&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CPU 사용률&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;b&gt;3. Application 서버 Heap 메모리 사용량&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2046&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZJUF/btsQnVgmtIX/pwqz4hFzcmySLrjkLbjQ5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZJUF/btsQnVgmtIX/pwqz4hFzcmySLrjkLbjQ5K/img.png&quot; data-alt=&quot;Heap 메모리 사용량&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZJUF/btsQnVgmtIX/pwqz4hFzcmySLrjkLbjQ5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrZJUF%2FbtsQnVgmtIX%2Fpwqz4hFzcmySLrjkLbjQ5K%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;621&quot; height=&quot;295&quot; data-origin-width=&quot;2046&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Heap 메모리 사용량&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 보이겠지만 DB Connection Pool이 16일 때 테스트를 멈췄다. 애초에 4가 최적의 수치라 예상했기 때문에 8정도까지 실험을 진행하려 했었으나 계속 올라가는 TPS 때문에 계속 테스트를 진행했고 14,16을 거치며 유의미한 변화가 없었기에 그만두었다. 일단 여기서 드는 의문은 왜 Pool Size가 4일 때 가장 수치가 높지 않았을까이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 Pool Size를 (Core*2)개 이상으로 해도 TPS가 계속 올라갔을까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 실마리는 Pinpoint를 통해 찾을 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3288&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyPmjK/btsQnV8Blyu/6iUwrvnubrkepLh7LrwVWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyPmjK/btsQnV8Blyu/6iUwrvnubrkepLh7LrwVWK/img.png&quot; data-alt=&quot;pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyPmjK/btsQnV8Blyu/6iUwrvnubrkepLh7LrwVWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyPmjK%2FbtsQnV8Blyu%2F6iUwrvnubrkepLh7LrwVWK%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;893&quot; height=&quot;348&quot; data-origin-width=&quot;3288&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진을 보면 &lt;b&gt;getConnection() 부분에서 많은 시간이 소요되고 있었고 심지어 그게 2번 발생&lt;/b&gt;한다는 것을 확인할 수 있었다. Connection을 받은 이후에 실제 쿼리가 수행되는데 걸린 소요시간은 각각 3ms, 1ms로 매우 짧았고, 이는 &lt;b&gt;InnoDB BP Hit Ratio가 99.93%였기에 메모리만 사용되어 인메모리 DB인 Redis와 같은 성능&lt;/b&gt;을 낼 수 있었던 것이다. 즉 DB 자체는 병목이 아니었고 정리하면 아래와 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757183817525&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;getConnection() - 731ms (첫 번째)  &amp;larr; 병목!
├─ prepareStatement() - 1ms
└─ executeQuery() - 3ms         &amp;larr; 실제 쿼리는 빠름

getConnection() - 424ms (두 번째)  &amp;larr; 병목!
├─ prepareStatement() - 0ms
└─ executeQuery() - 1ms         &amp;larr; 실제 쿼리는 빠름&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;결국 DB는 InnoDB BP에서 hit되는 가벼운 작업만 수행되고 있었고 따라서 Conncetion Pool 사이즈를 높였을 때도 큰 부하없이 실행되었던 것이다. 실제로 위의&lt;b&gt; 2번째 차트를 보면 DB의 CPU 코어(2 vCPU)가 Application 서버(4 vCPU)에 비해 2배 낮음에도 CPU 사용률은 2배 혹은 그 이상 적게 사용&lt;/b&gt;하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 getConnection()은 2번 일어나고 있었을까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinpoint를 보며 getConnection()이 왜 2번 발생했는지 궁금했을 것이다. 먼저 기존에 작성했던 코드를 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757186631399&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.dh.baro.product.application

@Service
class ProductFacade(
    private val storeService: StoreService,
    private val productService: ProductService,
) {

    fun getPopularProducts(
        categoryId: Long?,
        cursorLikes: Int?,
        cursorId: Long?,
        size: Int,
    ): ProductSliceBundle {
        val productSlice = productQueryService.getPopularProducts(categoryId, cursorLikes, cursorId, size)
        val stores = storeService.getStoresByIds(productSlice.content.map { it.storeId }.toSet())
        return ProductSliceBundle(productSlice, stores)
    }
}&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;이처럼 @Transactional을 붙이지 않고 있었고 그 이유는, &lt;b&gt;MSA 분리를 고려한 설계&lt;/b&gt; 때문이었다. 따라서 &lt;b&gt;Application Layer에 여러 도메인의 정보를 불러와 조합하는 Facade 패턴을 사용&lt;/b&gt;하고 있었고, 추후 MSA로 분리된다면 storeService가 아니라 &lt;b&gt;API 호출 형식으로 변경될 것&lt;/b&gt;이다. 그 상황이 온다면 &lt;b&gt;@Transactional이 외부 API 호출까지 포함하게 되어 불필요하게 DB Connection을 점유&lt;/b&gt;하게 되기 때문에 제거를 했다. 또한 현재의 구조에서도 Connection 요청이 여러 번 발생하는 것이 문제가 된다는 점을 인지하지 못한 채, &lt;b&gt;단순히 Connection을 짧게 가져가면 좋겠다는 생각으로 이렇게 설계&lt;/b&gt;했다. 하지만 현재는 모놀리식 구조로 storeService를 가져와 사용하고 있었고 따라서 @Transactional로 묶어주는게 맞다는 판단이 들었다. 지금까지의 계층 구조를 정리해보면 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757188992207&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Application Layer:
  ProductController
    &amp;darr;
  ProductFacade &amp;larr; @Transactional(readOnly = true)(개선)
    &amp;darr;
Domain Layer:
  ProductQueryService / StoreService &amp;larr; @Transactional(readOnly = true)(기존)
    &amp;darr;
Spring/JPA Layer:
  TransactionAspectSupport.invokeWithinTransaction()
    &amp;darr;
  HikariDataSource.getConnection() &amp;larr; 병목 (731ms, 424ms)
    &amp;darr;
JDBC Layer:
  ConnectionImpl.prepareStatement() 
    &amp;darr;
  ClientPreparedStatement.executeQuery() &amp;larr; 실제 쿼리 실행 (3ms, 1ms)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 6. 3차 개선 (@Transactional 범위 확장)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@Transactional(readOnly = true) 범위를 확장시키기고 바로 테스트를 진행했다&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;1892&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQQaFZ/btsQorsCi5V/O6iRbrKOG0htOV3gO3qggk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQQaFZ/btsQorsCi5V/O6iRbrKOG0htOV3gO3qggk/img.png&quot; data-alt=&quot;nGrinder 부하테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQQaFZ/btsQorsCi5V/O6iRbrKOG0htOV3gO3qggk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQQaFZ%2FbtsQorsCi5V%2FO6iRbrKOG0htOV3gO3qggk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;175&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder 부하테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과 &lt;b&gt;HikariCP Size 16 기준으로 TPS가 257&lt;/b&gt;이 나오는 것을 확인할 수 있었다. &lt;b&gt;개선 전 TPS 248보다 3.6% 개선&lt;/b&gt;되었다. Pinpoint 확인 결과 아래와 같이 connection 요청을 한번만 하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nLWiX/btsQme9dg89/LXA6h8JVzMnFTGjqG5yROK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nLWiX/btsQme9dg89/LXA6h8JVzMnFTGjqG5yROK/img.png&quot; data-alt=&quot;Pinpoint - 줄어든 커넥션 요청&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nLWiX/btsQme9dg89/LXA6h8JVzMnFTGjqG5yROK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnLWiX%2FbtsQme9dg89%2FLXA6h8JVzMnFTGjqG5yROK%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;832&quot; height=&quot;358&quot; data-origin-width=&quot;2838&quot; data-origin-height=&quot;1222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinpoint - 줄어든 커넥션 요청&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 쓰레드에서는 하나의 Connection만 사용하는 것이 Connection을 얻고 반납하는 오버헤드를 줄일 수 있어 효과적&lt;/b&gt;일 것이다. 다만 HikariDataSource의 getConnection() 메서드에 대해 알아보던 중 @Transactional이 정확히 뭘 하는지에 대해 궁금해졌고, 현재처럼 조회만 하는 상황에서도 꼭 필요한 것인지에 대한 의문이 들었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 @Transactional은 DB Connection을 잡고 있기 위해 쓰는 것일까&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 정의를 생각해보면, @Transactional&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;은 spring에서 메서드의 원자성을 보장하며 JDBC의 연결을 관리하기 위해 정의된 annotation interface이다. 하지만 현재 상황의 경우에는 조회만 이루어지기 때문에 원자성은 필요가없다. 그럼 @Transactional를 쓰는게 아니라 Connection 잡아주는 무언가만 사용할 수는 없을까라는 생각이 들었다. 하지만 일단 이에 대해 자세히 알기 위해서 &lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;@Transactional이 어떻게 동작하는지 알 필요가 있다. 이에 대한 내용은 다음 포스팅에서 더 다뤄 보겠다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 7. 4차 개선 (N+1, test-query)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;@Transactional에 대해 조사하면서 추가적으로 발생시키는 쿼리에 대해 알아보기 위해 &lt;b&gt;MySQL general log&lt;/b&gt;를 살펴보았다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2492&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ch8lQy/btsQorfccy3/80LXhiUgKxtz2O3MfkKkVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ch8lQy/btsQorfccy3/80LXhiUgKxtz2O3MfkKkVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ch8lQy/btsQorfccy3/80LXhiUgKxtz2O3MfkKkVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fch8lQy%2FbtsQorfccy3%2F80LXhiUgKxtz2O3MfkKkVk%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;790&quot; height=&quot;410&quot; data-origin-width=&quot;2492&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;요약하면 store 조회 이후 store와 연관된 user들을 조회하는 쿼리가 N+1 형태로 나타나고 있었다. 하지만 아래 코드에서처럼 fetchType을 Lazy을 미리 설정해 두었던 터라 N+1이 발생하는 이유를 찾기가 힘들었다. 하지만 결과적으로 찾은 이유는 Kotlin에 아직 미숙한 실수였다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757251142860&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;stores&quot;)
class Store(
    @Id
    @Column(name = &quot;id&quot;)
    val id: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;owner_id&quot;, nullable = false)
    val owner: User,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt; Kotlin의 경우 기본적으로 &lt;b&gt;모든 class를 final로 선언하기 때문에 Hibernate가 Lazy Loading을 위한 프록시 객체를 생성할 수 없었던 것&lt;/b&gt;이다. 이로 인해 @OneToMany나 @ManyToOne 관계에서 &lt;b&gt;LAZY 설정이 무시되고 모든 연관 엔티티가 EAGER 방식으로 조회되어 N+1 문제가 발생&lt;/b&gt;했다. 따라서 이를 해결하기위해 다음과 같이 &lt;b&gt;allOpen 플러그인을 추가하여 JPA 관련 어노테이션이 적용된 클래스들을 자동으로 open 상태로 만들어, Hibernate가 프록시 객체를 생성&lt;/b&gt;할 수 있도록 하였다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757251232309&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;allOpen {
     annotation(&quot;jakarta.persistence.Entity&quot;)
     annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
     annotation(&quot;jakarta.persistence.Embeddable&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;위의 플러그인을 통해 N+1 문제를 해결하였고, 다음으로는 테스트를 위한 SELECT 1 쿼리가 계속 발생하는 것이 눈에 걸렸다. 이 쿼리의 경우 다음과 같이 설정해두었던 설정이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757251345158&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      connection-test-query: SELECT 1  &amp;larr; 제거!!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 쿼리를 사용하지 않더라도 &lt;b&gt;HikariCP는 JDBC4 호환 드라이버에서 제공하는 Connection.isValid() 메서드를 자동으로 사용하여 연결 상태를 검증&lt;/b&gt;한다. 이를 통해 별도의 SQL 쿼리 실행 없이 네트워크 레벨에서 연결 상태 확인하고, 드라이버별 최적화를 자동 적용한다. 실제로 hikari.pool의 poolBase 코드를 보면 isUseJdbc4Validation를 통해 검증하고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757253603970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class PoolBase {
	private final boolean isUseJdbc4Validation;

	boolean isConnectionDead(Connection connection) {
        try {
            this.setNetworkTimeout(connection, this.validationTimeout);

            boolean statement;
            try {
                int validationSeconds = (int)Math.max(1000L, this.validationTimeout) / 1000;
                if (!this.isUseJdbc4Validation) {
                    Statement statement = connection.createStatement();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjt7v2/btsQmPgQuVT/TI3FK6SFCb3W9uWfjgtG10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjt7v2/btsQmPgQuVT/TI3FK6SFCb3W9uWfjgtG10/img.png&quot; data-alt=&quot;nGrinder&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjt7v2/btsQmPgQuVT/TI3FK6SFCb3W9uWfjgtG10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjt7v2%2FbtsQmPgQuVT%2FTI3FK6SFCb3W9uWfjgtG10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;787&quot; height=&quot;140&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TPS : 660&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 518ms&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt; 294&lt;/span&gt;ms&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 TPS는 이미 상회하는 수치를 통해 성능이 상당히 개선된 모습을 확인할 수 있었다. 그리고 다시 &lt;b&gt;MySQL general log를 살펴본 결과 5개의 쿼리만 발생&lt;/b&gt;하는 것을 확인할 수있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vL3xy/btsQnSxoYcw/1H3ajQe78BIKkASUoVxhr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vL3xy/btsQnSxoYcw/1H3ajQe78BIKkASUoVxhr1/img.png&quot; data-alt=&quot;MySQL general log&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vL3xy/btsQnSxoYcw/1H3ajQe78BIKkASUoVxhr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvL3xy%2FbtsQnSxoYcw%2F1H3ajQe78BIKkASUoVxhr1%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;796&quot; height=&quot;100&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL general log&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 DB를 모니터링하였을 때, &lt;b&gt;Buffer Pool Usage가 한계에 도달&lt;/b&gt;했다는 것을 알 수 있었다. 이는 현재 성능 테스트에서만 문제가 없을 뿐 실제로는 다른 여러 페이지가 조회될 것이기 때문에 이에 대한 조치가 필요했다. 다만 현재 이미 목표 성능에 도달했고 아직 급한 일들이 많이 남았기 때문에 추후에 더 개선이 필요해보인다면 튜닝을 진행해보도록 하겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HMpfi/btsQorGePVY/Ox9rPtkVvfhjGoKkYfDjJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HMpfi/btsQorGePVY/Ox9rPtkVvfhjGoKkYfDjJ1/img.png&quot; data-alt=&quot;Grafana - MySQL&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HMpfi/btsQorGePVY/Ox9rPtkVvfhjGoKkYfDjJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHMpfi%2FbtsQorGePVY%2FOx9rPtkVvfhjGoKkYfDjJ1%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;803&quot; height=&quot;398&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana - MySQL&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 8. 최종 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최종적으로는 &lt;b&gt;CursorID가 있는 경우에 대해 테스트를 진행&lt;/b&gt;했다. 테스트는 아래와 같이 &lt;b&gt;직접 무한 스크롤을 해보며 사용되는 CursorId 7가지를 찾아서 등록해두었고, 해당 Id를 랜덤하게 하나만 선택해서 조회하도록하는 nGrinder 스크립트를 작성&lt;/b&gt;하였다. cursorLikes는 5000개로 고정하였다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757308108682&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RunWith(GrinderRunner)
class PopularProductsTest {
    public static GTest tPopularProducts = new GTest(1, &quot;get popular products&quot;)
    public static HTTPRequest httpReq = new HTTPRequest()
    public static NVPair[] headers = [ new NVPair(&quot;Content-Type&quot;,&quot;application/json&quot;) ] as NVPair[]
    static String BASE = &quot;http://xx.xxx.xxx.xxx:8080&quot;
    
    // cursorId 값들을 배열로 정의
    static def cursorIds = [1719248, 1590822, 1526003, 1426760, 1298440, 1192504, 1074706]
    static Random random = new Random()
    
    @BeforeClass
    static void beforeProcess() {
        HTTPPluginControl.getConnectionDefaults().setTimeout(60000)
        HTTPPluginControl.getConnectionDefaults().setFollowRedirects(true)
        HTTPPluginControl.getConnectionDefaults().setUseCookies(true)
        tPopularProducts.record(httpReq)
    }
    
    @Test
    void getPopularProducts() {
        try {
            // 랜덤으로 cursorId 선택
            def randomCursorId = cursorIds[random.nextInt(cursorIds.size())]
            
            // URL 파라미터 구성
            def url = &quot;${BASE}/products/popular?cursorId=${randomCursorId}&amp;amp;cursorLikes=5000&quot;
            
            def res = httpReq.GET(url, headers)
            grinder.statistics.forLastTest.setSuccess(res.statusCode == 200)
            if (res.statusCode != 200) {
                grinder.logger.error(&quot;unexpected status: {}, body={}&quot;, res.statusCode, res.getText())
            } else {
                grinder.logger.info(&quot;Request successful with cursorId: {}&quot;, randomCursorId)
            }
        } catch (Throwable t) {
            grinder.logger.error(&quot;request failed&quot;, t)
            grinder.statistics.forLastTest.setSuccess(false)
        }
        grinder.sleep(1000)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과는 다음과 같았다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4nS8/btsQqE5Wr8m/vcWbyGQYYFQzCqr4gUeZ4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4nS8/btsQqE5Wr8m/vcWbyGQYYFQzCqr4gUeZ4k/img.png&quot; data-alt=&quot;nGrinder&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4nS8/btsQqE5Wr8m/vcWbyGQYYFQzCqr4gUeZ4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4nS8%2FbtsQqE5Wr8m%2FvcWbyGQYYFQzCqr4gUeZ4k%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;762&quot; height=&quot;202&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TPS : 1604&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;p99 latency :&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt; 134&lt;/span&gt;ms&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Average response time&amp;nbsp;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt; 68&lt;/span&gt;&lt;/span&gt;ms&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;커서가 존재하는 두번째 페이지부터는 성능이 향상된다&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이전 테스트까지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Cursor가 없는 첫 페이지 조회만을 대상으로 했기 때문에, 현재 결과는 훨씬 더 향상 된 TPS&lt;/b&gt;를 보여주고 있다. Cursor가 없는 첫 페이지의 경우 WHERE 조건 절에서 필터링이 적용되지 않아 조회 대상 데이터가 많이지고, 이로 인해 성능이 저하되었다. 따라서 Cursor가 있는 두번째 페이지부터는 성능이 개선되기 때문에, 사용자 접근 빈도가 높을 것으로 예상되는 첫 페이지만 Redis를 통해 사전 캐싱해두면 전체적인 성능을 크게 향상시킬 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커서가 없는 첫 페이지는 캐싱하여 서비스 품질을 높인다?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다만 캐싱을 사용할지는 좀 더 고민해봐야 할 것 같다.&lt;/b&gt; 아무래도 인기 상품 조회 기능이라서 순서가 바뀔 가능성이 크고 따라서 데이터 불일치가 발생할 수 있다. 하지만 지금까지 성능 개선하는 과정들은 어느 기능에서나 활용가능한 문제 해결 방법이고, 이를 토대로 &lt;b&gt;다음 기능 구현에서는 이러한 테스트 없이 어느정도 성능 좋은 기능&lt;/b&gt;을 만들 수 있을 것 같다. 지금보다 더 개선한다고 가정했을 때는 어떤식으로 설계를 바꿔볼지 고민해보면 재밌을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 개선 부분은 추후 포스팅에 남길게요..&lt;/b&gt;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/58</guid>
      <comments>https://chobo-backend.tistory.com/58#entry58comment</comments>
      <pubDate>Fri, 5 Sep 2025 12:31:02 +0900</pubDate>
    </item>
    <item>
      <title>[바로] Redis의 Lua Script는 Atomic 하지 않다..?</title>
      <link>https://chobo-backend.tistory.com/57</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZsH1e/btsQmZvpp65/MJ18n6p1FpDYaWfrANgOnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZsH1e/btsQmZvpp65/MJ18n6p1FpDYaWfrANgOnk/img.png&quot; data-alt=&quot;Atomic 한거 맞아?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZsH1e/btsQmZvpp65/MJ18n6p1FpDYaWfrANgOnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZsH1e%2FbtsQmZvpp65%2FMJ18n6p1FpDYaWfrANgOnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;260&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Atomic 한거 맞아?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/56&quot;&gt;https://chobo-backend.tistory.com/56&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756975289001&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;[바로] 일괄 주문 기능 개선 #1 (Ft. Lua Script, 비동기)&quot; data-og-description=&quot;https://chobo-backend.tistory.com/54 [바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/56&quot; data-og-url=&quot;https://chobo-backend.tistory.com/56&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ro9qT/hyZGk5TSbt/ekMmLi7MKNYrHCpGk4YSO1/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/cNVjY2/hyZGVRMkgt/q9AoFBNGS96hzWMMGlEVLk/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/bfkvub/hyZGXPAKdL/v52S0jExERnIl13y6floYk/img.png?width=2414&amp;amp;height=1710&amp;amp;face=0_0_2414_1710&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/56&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/56&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ro9qT/hyZGk5TSbt/ekMmLi7MKNYrHCpGk4YSO1/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/cNVjY2/hyZGVRMkgt/q9AoFBNGS96hzWMMGlEVLk/img.jpg?width=800&amp;amp;height=532&amp;amp;face=72_63_381_155,https://scrap.kakaocdn.net/dn/bfkvub/hyZGXPAKdL/v52S0jExERnIl13y6floYk/img.png?width=2414&amp;amp;height=1710&amp;amp;face=0_0_2414_1710');&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;[바로] 일괄 주문 기능 개선 #1 (Ft. Lua Script, 비동기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://chobo-backend.tistory.com/54 [바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 전 포스팅에서 일괄 주문을 개선하면서 Redis로 가져온 재고 데이터가 부하테스트 이후 불일치하다는 것을 체크했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CabHS/btsQmFX6aNt/XVrzR7kksmiKwEyyOL5pMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CabHS/btsQmFX6aNt/XVrzR7kksmiKwEyyOL5pMk/img.png&quot; data-alt=&quot;MySQL&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CabHS/btsQmFX6aNt/XVrzR7kksmiKwEyyOL5pMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCabHS%2FbtsQmFX6aNt%2FXVrzR7kksmiKwEyyOL5pMk%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;483&quot; height=&quot;173&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9rZS8/btsQjzk9BnR/ZAGncoo9MkoowgSMphpuik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9rZS8/btsQjzk9BnR/ZAGncoo9MkoowgSMphpuik/img.png&quot; data-alt=&quot;재고 개수가 다른 Redis??&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9rZS8/btsQjzk9BnR/ZAGncoo9MkoowgSMphpuik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9rZS8%2FbtsQjzk9BnR%2FZAGncoo9MkoowgSMphpuik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;241&quot; height=&quot;164&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재고 개수가 다른 Redis??&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당황스러웠다.. DB에서 주문 수와 물품 수량은 데이터 불일치가 없었는데, Redis에서만 불일치하다는 것이 이해가 안갔다. 아주 많은 삽질을 하면서 결국 해결은 했지만, 본인의 로직에서 실수로 발생했던 것이었다.. 하지만 문제를 찾아가는 과정에서 알게된 것들을 남겨본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 해결 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. AFTER_ROLLBACK에 대한 오해&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존에 구현했던 롤백 로직을 의심했다. 왜냐하면 12번의 테스트를 거치며 Redis의 데이터가 DB보다 더 많이 감소되는 상황은 전혀 발견되지 않았기 때문이다. 먼저 Rollback 로직을 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756975648253&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
fun restoreInventoryOnRollback(event: InventoryDeductionRequestedEvent) {
    runCatching {
        inventoryRedisRepository.restoreStocks(event.items)
    }.onFailure { ex -&amp;gt;
        log.error(ErrorMessage.INVENTORY_RESTORE_ERROR.format(event.orderId), ex)
    }
}&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;코드를 보면 AFTER_ROLLBACK 단계에 진행하는 것을 알 수 있다. 이 과정이 진행되기전 메인로직 흐름은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. User(유저) 존재 여부 체크&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Product(상품) 조회&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Order(주문), OrderItem(주문 상품) 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 주문 목록에 따른 Redis 재고 감소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. Spring Event 발급&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Rollback이 불가능했고, 따라서 Rollback에 대한 영향을 가장 적게 받는 위치인 마지막 부분에 Redis 감소 로직을 두었다. 그리고 위의 Rollback 로직의 경우에는 Event 발급 이후에 문제가 생긴 경우 AFTER_ROLLBACK이 실행되는 줄 알았으나, Event 발급이 포함된 Transaction 전체에서 Rollback이 발생 할 경우 해당 로직 실행되는 것이었다. 따라서 Redis에 재고가 감소되지도 않았는데 원상복귀(+주문 수량)하는 상황이 발생할 수 있다는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Lua Script 도중 실패하는 것일까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물품 1,2,3번의 개수가 다른 상황이 잘 이해가 안갔다. 분명 아래의&lt;b&gt; Redis 공식 문서에 의하면 Redis는 Lua Script가 Atomic하게 실행된다는 것을 보장한다&lt;/b&gt;고 하고 있다. 그렇다면 흔히 알려진 트랜잭션 ACID에 에서 &lt;b&gt;A(Atomicity)의 의미는 하나의 트랜잭션이 전부 성공하거나 전부 실패하거나 원자적으로 동작한다는 뜻&lt;/b&gt;이다. 즉 이에 따르면, 현재 발생한 이 상황은 발생해서는 안되는 상황이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://redis.io/docs/latest/develop/programmability/eval-intro/#interacting-with-redis-from-a-script&quot;&gt;&lt;br /&gt;&lt;/a&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/programmability/eval-intro/#interacting-with-redis-from-a-script&quot;&gt;https://redis.io/docs/latest/develop/programmability/eval-intro/#interacting-with-redis-from-a-script&lt;/a&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Redis guarantees the script's atomic execution. While executing the script, all server activities are blocked during its entire runtime. These semantics mean that all of the script's effects either have yet to happen or had already happened.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 사례들을 찾아보던 중 본인과 비슷한 상황에 있는 글을 발견했다. 내용을 요약하면 Lua Script 도중 에러가 발생하면 기존까지 진행됐던 상황은 복구되지 않는다는 것이었다. 실제로 테스트를 통해 해당 현상이 발생한다는 것을 확인했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756989354293&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for i = 1, #KEYS do
    local key = KEYS[i]
    local deductAmount = tonumber(ARGV[i])
    
    -- i가 2일 때 에러 발생
    if i == 2 then
        error(&quot;Error occurred at iteration i=2&quot;)
    end

    redis.call('DECRBY', key, deductAmount)
end&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;i = 2에서 에러가 발생했어도 i = 1에서 재고 차감은 그대로 적용된다..!&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래에 현재 Redis에 열려있는 이슈를 통해 더 자세한 내용이 확인가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/redis/redis/issues/10576&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/redis/redis/issues/10576&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756983489016&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;Support rollback in Lua script &amp;middot; Issue #10576 &amp;middot; redis/redis&quot; data-og-description=&quot;The problem/use-case that the feature addresses My company uses Redis as a main database. Lua script is heavily used to execute complicated business logic on Redis to provide atomicity. The script ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/redis/redis/issues/10576&quot; data-og-url=&quot;https://github.com/redis/redis/issues/10576&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lgsCy/hyZG06E6Kk/l9vrOADIcnxdKSOZjP9YT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/gWxYp/hyZG2cju6f/F1nFL9NUfyCFFarK1zTFFK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/redis/redis/issues/10576&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/redis/redis/issues/10576&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lgsCy/hyZG06E6Kk/l9vrOADIcnxdKSOZjP9YT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/gWxYp/hyZG2cju6f/F1nFL9NUfyCFFarK1zTFFK/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;Support rollback in Lua script &amp;middot; Issue #10576 &amp;middot; redis/redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The problem/use-case that the feature addresses My company uses Redis as a main database. Lua script is heavily used to execute complicated business logic on Redis to provide atomicity. The script ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면 현재 부하테스트 과정 속에서 예상치 못한 에러로 인해 실패하는 것일까? 하지만 Redis 로그를 살펴보았지만 아래 이외의 별다른 에러 로그는 확인할 수 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1:C 04 Sep 2025 02:05:17.604 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;번외로 위 로그를 살펴보면 &lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;vm.overcommit_memory = 1 로 두어서 항상 overcommit을 허용하라는 로그이다. Redis의 경우 &lt;b&gt;AOF 파일 재작성하거나 replication 작업을 하는 상황에서 fork()를 사용&lt;/b&gt;하는데 이때 자식 프로세스가 부모의 전체 메모리를 복사할 수 있다고 가정하면 이론적으로 &lt;b&gt;2배의 메모리가 필요&lt;/b&gt;하다. 따라서&lt;b&gt; 메모리가 제한적인 환경이거나 AOF, Replication 같이 데이터 지속성이 중요한 경우 overcommit을 허용하는 것이 좋다&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 허무하게도 &lt;b&gt;Redis에 key가 없을 때 DB 최신화를 하는 과정이 문제&lt;/b&gt;였다. 아래의 기존 코드를 보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756985352844&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun executeScript(
    script: String,
    items: List&amp;lt;InventoryItem&amp;gt;,
    autoInitializeFromDb: Boolean,
    shouldThrowError: (Long) -&amp;gt; Boolean
): Boolean {
    val keys = items.map { getStockKey(it.productId) }
    val quantities = items.map { it.quantity.toString() }

    var retryCount = 0
    while (retryCount &amp;lt; MAX_RETRY_COUNT) {
        val redisScript = redissonClient.getScript(StringCodec.INSTANCE)

        val result = redisScript.eval&amp;lt;Long&amp;gt;(
            RScript.Mode.READ_WRITE,
            script,
            RScript.ReturnType.INTEGER,
            keys,
            *quantities.toTypedArray()
        )

        when (result) {
            MISSING_KEYS -&amp;gt; {
                if (autoInitializeFromDb) {
                    val productIds = items.map { it.productId }
                    initializeStocksFromDb(productIds)
                    retryCount++
                    continue
                }
                return shouldThrowError(result)
            }

            INVALID_AMOUNT -&amp;gt; throw IllegalArgumentException(ErrorMessage.INVALID_STOCK_AMOUNT.message)
            ERROR_WITH_ROLLBACK -&amp;gt; throw IllegalArgumentException(&quot;Error with rollback&quot;)

            else -&amp;gt; {
                return shouldThrowError(result)
            }
        }
    }

    throw IllegalStateException(ErrorMessage.INVENTORY_RETRY_EXCEEDED.format(MAX_RETRY_COUNT))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MISSING_KEYS를 처리하는 부분에서 다음과 같이 진행되면서 동시성 문제가 발생했던 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 재고 관리 로직 중에서 Key를 찾지 못한 여러 작업들 발생&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 이 작업들이 MISSING_KEYS 재처리 로직으로 몰림&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 가장 마지막에 도착한 작업이 비동기로 인해 전부 반영되지 않은 DB 데이터로 최신화시킴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 초반에 키를 초기화하고 재고 감소 처리한 작업들이 증발&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 상황을 막기 위해 &lt;b&gt;Lua Script를 통해 Key가 존재하는지 여부를 한번 더 체크하는 로직으로 아래와 같이 변경&lt;/b&gt;시키면서 데이터 정합성 문제는 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756985730287&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;local setCount = 0

for i = 1, #KEYS do
    local key = KEYS[i]
    local value = ARGV[i]
    
    if redis.call('EXISTS', key) == 0 then
        redis.call('SET', key, value)
        redis.call('EXPIRE', key, 43200) -- 12 hours
        setCount = setCount + 1
    end
end

return setCount&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. Redis의 Lua Script가 Atomic 하다?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Redis는 왜 자동 Rollback을 지원하지 않을까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 &lt;b&gt;Redis 공식문서에서 Atomic 하다는 것의 의미는 트랜잭션 ACID 특성 중에서 Isolation에 가깝다&lt;/b&gt;고 생각한다. 그럼 왜 Redis는 Transaction Rollback을 구현하지 않았을까? Redis 공식 블로그의 의하면 의견은 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redis.io/blog/you-dont-need-transaction-rollbacks-in-redis/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redis.io/blog/you-dont-need-transaction-rollbacks-in-redis/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. The snapshotting mechanism required to implement rollbacks would have a considerable computational cost. That extra complexity wouldn&amp;rsquo;t sit well with Redis&amp;rsquo; philosophy and ecosystem.&lt;br /&gt;&lt;br /&gt;2. Rollbacks can&amp;rsquo;t catch all errors. In the example above, we set &amp;ldquo;counter&amp;rdquo; to &amp;ldquo;banana&amp;rdquo; in order to show a blatant error, but in the real world the process that used the &amp;ldquo;counter&amp;rdquo; key in the wrong way might instead have deleted it, or put in a credit-card number, for example. Rollbacks would add a considerable amount of complexity and would still not fully solve the problem.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 요약하면, &lt;b&gt;MySQL의 MVCC와 같은 스냅샷 매커니즘은 많은 비용을 발생&lt;/b&gt;시키고 이는 빠른 응답을 목표로하는 Redis의 철학과 다르다. 또한 Rollback을 지원한다고 해서 모든 에러를 잡아낼 수 없기에 동의하지 않는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 Redis의 Lua Script는 Atomicity하지 않다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서의 말만 믿고 우리가 흔히 알고 있는 트랜잭션의 Atomicity(원자성)으로 착각하고 구현을 진행한다면 운영 도중 예상치 못한 이슈를 마주하게 될 것이다. 따라서 요구사항에 따라 &lt;b&gt;데이터 정합성이 매우 중요한 경우라면 AOF를 통해 데이터의 일관성과 무결성을 지켜야&lt;/b&gt;한다. 다만 '바로' 서비스의 경우 &lt;b&gt;물품의 재고 개수를 100% 일치시킬 필요는 없다&lt;/b&gt;고 생각한다. 일단 에러가 발생할 확률이 낮아보이며, &lt;b&gt;재고 데이터를 Redis에 TTL 없이 무제한 등록시킬 것도 아니기 때문에 주기적으로 최신화시켜주어 이를 해결&lt;/b&gt;할 수 있을 것이라 생각한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>atomic</category>
      <category>lua script</category>
      <category>redis</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/57</guid>
      <comments>https://chobo-backend.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 4 Sep 2025 17:38:27 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 일괄 주문 기능 개선 Vol.1 (Ft. Eventual Consistency, Lua Script)</title>
      <link>https://chobo-backend.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chobo-backend.tistory.com/54&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756261840393&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;[바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)&quot; data-og-description=&quot;상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서 예를 들면, 남은 재고는 10개였으나 12개의 주문이 발생할 수 있다. 이로 인해 사용자는 결&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/54&quot; data-og-url=&quot;https://chobo-backend.tistory.com/54&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mO9xk/hyZDX96eJg/OKjdZJPEarcxQ3sGUwhB00/img.jpg?width=300&amp;amp;height=205&amp;amp;face=0_0_300_205,https://scrap.kakaocdn.net/dn/wcXB8/hyZGgOlSP6/MCBPlABErnid9EQ1Zmukfk/img.jpg?width=300&amp;amp;height=205&amp;amp;face=0_0_300_205,https://scrap.kakaocdn.net/dn/iVnWm/hyZDR9UlvK/i8kBn7VZyllzA0Rx09UhN0/img.png?width=2400&amp;amp;height=1350&amp;amp;face=0_0_2400_1350&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/54&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mO9xk/hyZDX96eJg/OKjdZJPEarcxQ3sGUwhB00/img.jpg?width=300&amp;amp;height=205&amp;amp;face=0_0_300_205,https://scrap.kakaocdn.net/dn/wcXB8/hyZGgOlSP6/MCBPlABErnid9EQ1Zmukfk/img.jpg?width=300&amp;amp;height=205&amp;amp;face=0_0_300_205,https://scrap.kakaocdn.net/dn/iVnWm/hyZDR9UlvK/i8kBn7VZyllzA0Rx09UhN0/img.png?width=2400&amp;amp;height=1350&amp;amp;face=0_0_2400_1350');&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;[바로] 단일 주문 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능 중 하나이다. 먼저 재고 관리 측면에서 예를 들면, 남은 재고는 10개였으나 12개의 주문이 발생할 수 있다. 이로 인해 사용자는 결&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서 다음과 같은 고민들을 남기며 마쳤었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. DB Connection Pool을 늘린다면 개선 될까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 서버나 DB를 Scale-up 한다면 개선 될까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 비즈니스 로직이 훨씬 더 복잡해진다면 UPDATE로 원자적 주문이 가능할까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Redis를 도입하면 개선 될까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 여러가지 방법들을 생각해보며 이번 개선의 방향성을 잡아보려고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 0. 개선 방향성 잡기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. DB Connection Pool을 확장하면 개선이 될까??&lt;/b&gt;&lt;/h4&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;641&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yl3Tt/btsP8HWkj93/548ukKAgLDvLfnZacLV670/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yl3Tt/btsP8HWkj93/548ukKAgLDvLfnZacLV670/img.png&quot; data-alt=&quot;Grafana HikariCP&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yl3Tt/btsP8HWkj93/548ukKAgLDvLfnZacLV670/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyl3Tt%2FbtsP8HWkj93%2F548ukKAgLDvLfnZacLV670%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;493&quot; height=&quot;254&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Grafana HikariCP&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 품목 테스트 당시 그라파나(위)를 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;67개의 Thread가 Pending 상태에 머무르고 있는 것&lt;/b&gt;을 볼 수 있다. 그래서 현재 성능 병목의 원인 중 하나가 &lt;b&gt;DB&lt;span&gt;&amp;nbsp;&lt;/span&gt;Connection Pool 부족일 가능성&lt;/b&gt;이 있다. 따라서 Connection Pool 크기를 늘리면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;더 많은 요청이 동시에 DB에 접근할 수 있어 처리량 향상을 기대&lt;/b&gt;할 수 있다. 하지만 주문 API의 특성상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동일한 상품에 대한 동시 접근 시 Lock Contention이 발생&lt;/b&gt;하게 된다. 이는 Connection 수를 늘려도 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Lock 대기 시간으로 인해 성능 향상이 제한적&lt;/b&gt;일 수밖에 없을 것이다. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;과도한 Connection은 오히려 DB 서버에 부하를 가중시켜 전체적인 성능 저하를 일으킬 수 있다고 생각&lt;/b&gt;하여 제외하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 서버나 DB를 Scale-up 한다면 개선 될까&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버나 DB의 하드웨어 사양을 업그레이드하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CPU 처리 능력과 메모리 용량 증가로 인해 전반적인 성능 향상을 기대&lt;/b&gt;할 수 있다. 특히 DB 서버의 경우 더 빠른 디스크 I/O와 많은 메모리로 인해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿼리 처리 속도가 개선&lt;/b&gt;될 것이다. 그러나 재고 동시성 처리는 본질적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 무결성을 보장하기 위해 순차적 처리가 필요한 부분이 존재&lt;/b&gt;한다. 아무리 하드웨어 성능이 좋아져도 동일 상품에 대한 재고 차감은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Race Condition을 방지하기 위해 Lock을 사용&lt;/b&gt;해야 하므로, 순차적으로 진행되기에 성능 상한선이 존재한다. Scale-up은 분명 도움이 되겠지만 목표 TPS에 도달하지 못했을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;무제한적으로 Scale-up을 할 수는 없으며&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이는 근본적인 해결책이 되지 못한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;비즈니스 로직이 훨씬 더 복잡해진다면 UPDATE로 원자적 주문 처리가 가능할까&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쇼핑몰에서 주문 시 필요한 로직들을 살펴보면, 재고 차감 외에도 &lt;b&gt;쿠폰 사용 검증, 적립금 차감, 배송비 계산, 회원 등급별 할인 적용, 상품별 재고 분산 처리 등이 필요&lt;/b&gt;하다. 예를 들어, 고객이 여러 상품을 장바구니에 담고 10% 할인 쿠폰과 적립금 5000원을 사용하여 주문할 때를 생각해보겠다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 상품의 재고를 확인하고 차감해야 하는데, 상품 A는 서울 창고에 50개, 부산 창고에 30개가 있고, 상품 B는 대구 창고에만 20개가 있다면 여러 테이블에서 재고를 조회하고 업데이트해야 한다. 또한 동시에 쿠폰 테이블에서 해당 쿠폰이 유효한지, 이미 사용되었는지 확인하고 사용 상태로 변경해야 한다. 이후에 고객의 적립금 잔액을 확인하고 5000원을 차감한 후, 주문 금액에 따라 새로운 적립금을 계산해서 추가해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 복잡한 로직을 &lt;b&gt;단일 UPDATE 쿼리로 처리하려면 서브쿼리와 조인이 극도로 복잡해지며, 데이터베이스 성능이 급격히 저하&lt;/b&gt;된다. 그리고 중간에 하나라도 실패하면 전체 롤백이 어렵고, 어느 단계에서 실패했는지 파악하기도 어렵다. 또한 쿠폰 중복 사용이나 적립금 부족 같은 예외 상황에 대한 &lt;b&gt;세밀한 에러 처리가 불가능&lt;/b&gt;하다. 따라서 검증과 처리는 구분해야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Redis를 도입하면 개선 될까&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 도입하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;재고 조회와 업데이트 모두에서 큰 성능 개선을 기대&lt;/b&gt;할 수 있다. 재고 조회의 경우 Redis에서 먼저 확인하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐시 미스가 발생했을 때만 DB에 접근하므로 응답 시간이 대폭 단축&lt;/b&gt;된다. 더 중요한 것은 재고 차감 로직인데, Redis의 싱글 쓰레드로 동작하는 특성상&amp;nbsp;&lt;b&gt;DB 레벨의 복잡한 락 없이도 동시성을 제어할 수 있다&lt;/b&gt;. 재고 업데이트 시에도 Redis에서 먼저 처리한 후 DB로 비동기 동기화하면 사용자 응답 시간을 크게 줄일 수 있다. Redis는 메모리 기반이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB 대비 훨씬 빠른 처리&lt;/b&gt;가 가능하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Lua Script를 통해 복잡한 재고 로직도 원자적으로 처리&lt;/b&gt;할 수 있다. 다만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redis와 DB 간의 데이터 일관성을 보장&lt;/b&gt;하는 것이 중요한 부분일 것이다. 결론적으로 이러한 장점들을 종합하면 Redis 도입이 목표 TPS 달성에 가장 현실적이고 효과적인 방안으로 보인다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. Distributed Lock을 도입?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일괄 주문 흐름은 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 모든 상품 재고 검증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 모든 상품의 재고 차감&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 주문 접수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 주문 상품 등록&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 단일 UPDATE 방식으로 구현을 했었는데&lt;b&gt; 개선 방향성 3번(비즈니스 로직이 훨씬 더 복잡해진다면?)&lt;/b&gt;을 만족하려면 결국 검증과 재고 차감이 분리될 수 밖에 없다. 결국 &lt;b&gt;검증 시점에서 FOR UPDATE로 지속적으로 Lock을 걸고 재고 차감을 하지 않는 이상 동시성으로 인한 데이터 불일치가 발생할 수 밖에 없다&lt;/b&gt;. 여러 방법들을 찾아보던 중 이러한 재고 관리에 분산 락을 사용한다는 것을 알게되었고, 이게 정말 현재 상황에 맞는 해결책인지 체크해야했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 분산 락(Distributed Lock) 이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 락(Distributed Lock)&lt;/b&gt;은 여러 서버나 프로세스가 동시에 같은 자원에 접근하는 것을 제어하기 위해 사용하는 동기화 메커니즘이다. 일반적인 락과 달리 네트워크상의 여러 노드 간에 공유되는 락으로, Redis, Zookeeper, DB 등을 통해 구현된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서는 각 서버가 독립적으로 동작하기 때문에 &lt;b&gt;단일 서버의 synchronized나 ReentrantLock 같은 방법으로는 동시성 제어가 불가능&lt;/b&gt;하다. 예를 들어 A서버와 B서버가 동시에 같은 상품의 재고를 차감하려고 할 때, 각각의 로컬 &lt;b&gt;Lock&lt;/b&gt;으로는 서로를 인지할 수 없어 동시성 문제가 발생한다. 이때 &lt;b&gt;Redis 같은 외부 저장소에 락을 걸어두고 모든 서버가 이를 확인하도록 하여 한 번에 하나의 서버만 해당 작업을 수행할 수 있도록 보장&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 재고 차감, 쿠폰 발급, 순번 생성, 중복 결제 방지, 배치 작업의 중복 실행 방지 등과 같이 &lt;b&gt;정확히 한 번만 실행되어야 하는 중요한 비즈니스 로직에서 사용&lt;/b&gt;된다. 특히 MSA나 로드밸런싱된 다중 서버 환경에서 데이터 정합성을 보장하기 위해 필수적으로 활용되는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 분산 락 적용 후 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 분산락을 적용했다고 가정하고 구조를 다음과 같이 생각해 볼 수 있다. 먼저 애플리케이션 서버에서 &lt;b&gt;Redis를 통해 Lock을 획득한 후, Lock을 보유한 상태에서 DB에 데이터를 저장하고, 작업 완료 후 Lock을 해제하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgT8hs/btsP8bEGE7k/s4nQjzkbI3yaysFrbvsBP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgT8hs/btsP8bEGE7k/s4nQjzkbI3yaysFrbvsBP1/img.png&quot; data-alt=&quot;분산 락 메커니즘&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgT8hs/btsP8bEGE7k/s4nQjzkbI3yaysFrbvsBP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgT8hs%2FbtsP8bEGE7k%2Fs4nQjzkbI3yaysFrbvsBP1%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;598&quot; height=&quot;363&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분산 락 메커니즘&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식의 문제가 뭘까? 성능 개선이 되는 게 맞을까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 &lt;b&gt;이번 개선을 시작하게 된 가장 큰 이유는 결국 성능 문제&lt;/b&gt;였다. 그런데 위 방식을 보면 Redis를 통해 Lock을 할당받고 DB 작업을 끝마친 후 Lock을 반납하는 방식이다. 그럼 &lt;b&gt;결국 분산 락을 적용하기 전처럼 하나의 요청만 순차방식으로 진행되는 것은 똑같다&lt;/b&gt;. 따라서 성능측면에서 큰 효과를 낼 것 같지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;Split-Brain&lt;/b&gt; 문제가 발생할 가능성도 있다. 예를 들어, Lock을 획득 한 후 DB 작업을 수행하는 동안, &lt;b&gt;Lock을 보유한 서버가 네트워크 장애나 외부 API, GC STW 등으로 인해 응답이 지연될 수&lt;/b&gt; 있다. 이 경우 Lock은 TTL이 만료될 때까지 해제되지 않아 다른 서버들이 &lt;b&gt;불필요하게 대기&lt;/b&gt;하게 된다. 심지어 TTL이 만료된 후 Redis는 서버에 문제가 있다고 판단하고 Lock을 해제하지만, 원래 &lt;b&gt;서버가 복구되어 DB 작업을 계속 진행할 수 있어 동시성 문제가 발생할 수&lt;/b&gt; 있다. 따라서 현재 상황의 경우 UPDATE 쿼리로 동시성 문제가 해결 가능할 것으로 보여 분산 락은 사용하지 않기로 결정했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 성능을 위한 Eventual Consistency&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반드시 실시간으로 DB에 반영되어야 할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;현재 문제는 순차적으로 수행되는 DB 병목이 문제였고, 결국 DB 작업을 실시간으로 진행하지 않아야&lt;/b&gt; 성능이 개선될 수 있다. 이를 해결하기 위해 Redis에서 재고 관리를 수행하되, &lt;b&gt;DB에는 해당 변경사항을 나중에 반영해주는 방식&lt;/b&gt;을 생각했다. 다만 이는 &lt;b&gt;Eventual Consistency(궁극적 일관성)가 보장되어야 하는 구조&lt;/b&gt;로, &lt;b&gt;Redis와 DB 간의 데이터 일관성을 최종적으로 맞춰주는 메커니즘이 필요&lt;/b&gt;하다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;Over-Engineering을 피하자&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 방법에는 Spring Events or Message Queue 기반 비동기 처리(Kafka, RabbitMQ), Batch 작업을 통한 주기적 동기화, Change Data Capture(CDC), Event Sourcing 등 다양한 선택지가 있었다. 이 중에서 &lt;b&gt;Spring Events 방식&lt;/b&gt;이 현재 상황에서 가장 적합하다고 판단했다. &lt;b&gt;Kafka나 RabbitMQ 같은 메시지 큐는 다양한 기능을 제공하지만 별도의 인프라 구축과 운영 복잡성이 증가&lt;/b&gt;한다. &lt;b&gt;Batch 처리 방식은 구현이 간단하지만 실시간성이 떨어지고 데이터 불일치 기간이 길어질&lt;/b&gt; 수 있다. 그리고 CDC와 Event Sourcing은 매우 복잡한 구현이 예상되어 제외하였다. 반면 &lt;b&gt;Spring Events는 Over-Engineering을 피하고자 하는 목표에 부합하며, 구현상 적은 리소스가 드는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Events 방식의 장점으로는 &lt;b&gt;별도 인프라 없이 애플리케이션 내에서 비동기 처리가 가능&lt;/b&gt;하고, &lt;b&gt;설정과 구현이 간단&lt;/b&gt;하며, JVM 내부 처리로 &lt;b&gt;네트워크 오버헤드가 없다&lt;/b&gt;는 점이 있다. 또한 MQ에 비해&amp;nbsp;&lt;b&gt;디버깅이 용이하고 트랜잭션과의 연동도 자연스럽게 처리&lt;/b&gt;할 수 있다. 하지만 단점으로는&amp;nbsp;&lt;b&gt;단일 JVM 내에서만 동작하므로 다른 서버 인스턴스와 이벤트를 공유할 수 없고&lt;/b&gt;, &lt;b&gt;서버 장애 시 이벤트가 유실&lt;/b&gt;될 수 있다. 특히 분산 환경에서는 각 서버별로 독립적으로 이벤트가 처리되므로 &lt;b&gt;전체 시스템 관점에서의 순서 일관성은 보장되지 않는다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Redis는 동시성에서 자유롭다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일괄 주문 API의 재고 관리의 경우 동시성 문제는 싱글쓰레드를 사용하는 Redis 특징에 의해 보장되고 있다. 또한 &lt;b&gt;DB 동기화나 알림 발송 같은 부가 작업들은 순서가 정확할 필요가 없다&lt;/b&gt;고 생각했다. 최종적인 데이터만 일치하면 되는 Eventual Consistency 특성상, &lt;b&gt;이벤트 처리 순서보다는 모든 변경사항이 누락 없이 반영되는 것이 더 중요&lt;/b&gt;하다. 따라서 &lt;b&gt;Spring Events의 순서가 보장되지 않는 단점이 현재 요구사항에는 큰 영향을 주지 않으며, 단순함과 효율성이라는 장점이 더 크다고 판단&lt;/b&gt;했다. Redis와 관련된 내용은 아래에 자세히 정리해두었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/39&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chobo-backend.tistory.com/39&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756382775162&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;Redis의 동작 방식에 대해 아시나요?&quot; data-og-description=&quot;소프티어 프로젝트 최종 평가 당일 면접관 분께 사용한 기술에 대해 말씀드리게 되었다. Redis의 Geospatial Index 부분에서 여러 질문과 답이 오가던 중 Redis 동작 방식에 대해 여쭤보셨다. 당시 싱글&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/39&quot; data-og-url=&quot;https://chobo-backend.tistory.com/39&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qyVhp/hyZF7RBFTX/Eo4a1kghn7y6wAfkzFPYKK/img.jpg?width=720&amp;amp;height=577&amp;amp;face=0_0_720_577,https://scrap.kakaocdn.net/dn/c2fnbU/hyZC3DlnDq/y0EwWl9C5YGIwucPbFJWZK/img.jpg?width=720&amp;amp;height=577&amp;amp;face=0_0_720_577,https://scrap.kakaocdn.net/dn/bTgu1E/hyZGnz9RPu/LGsdUI2qVUBemfFBJtJ3d0/img.png?width=1388&amp;amp;height=632&amp;amp;face=0_0_1388_632&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/39&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/39&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qyVhp/hyZF7RBFTX/Eo4a1kghn7y6wAfkzFPYKK/img.jpg?width=720&amp;amp;height=577&amp;amp;face=0_0_720_577,https://scrap.kakaocdn.net/dn/c2fnbU/hyZC3DlnDq/y0EwWl9C5YGIwucPbFJWZK/img.jpg?width=720&amp;amp;height=577&amp;amp;face=0_0_720_577,https://scrap.kakaocdn.net/dn/bTgu1E/hyZGnz9RPu/LGsdUI2qVUBemfFBJtJ3d0/img.png?width=1388&amp;amp;height=632&amp;amp;face=0_0_1388_632');&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;Redis의 동작 방식에 대해 아시나요?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소프티어 프로젝트 최종 평가 당일 면접관 분께 사용한 기술에 대해 말씀드리게 되었다. Redis의 Geospatial Index 부분에서 여러 질문과 답이 오가던 중 Redis 동작 방식에 대해 여쭤보셨다. 당시 싱글&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Part 3. Lua Script 기반 재고 관리(+ 정합성 문제)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis Transaction과 Lua Script 중에서는 Lua Script를 선택했다. Redis Transaction의 경우 &lt;b&gt;Redis와 여러번 통신하며 낙관적 동시성 제어를 하기 때문에 Race Condition에 취약&lt;/b&gt;하다. 또한&amp;nbsp;&lt;span&gt;&lt;b&gt;조건부 로직 구현 및 실행 중 값 확인이 불가해 복잡한 비즈니스 로직 구현이 어려워&lt;/b&gt; 선택하지 않았다. 반면 Lua Script는 Redis 서버에서 원자적으로 실행되어 실행 중에는 다른 클라이언트의 접근을 완전히 차단해 Race Condition으로부터 안전하며 &lt;b&gt;복잡한 조건부 로직도 스크립트 내에서 자유롭게 구현할 수 있어 선택&lt;/b&gt;했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재고 감소 Lua Script 작성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1756791475943&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for i = 1, #KEYS do
    local key = KEYS[i]
    local deductAmount = tonumber(ARGV[i])
    
    local currentStock = redis.call('GET', key)
    if currentStock == false then
        return -1 -- MISSING_KEYS
    end
    
    currentStock = tonumber(currentStock)
    if currentStock &amp;lt; deductAmount then
        return -2 -- INSUFFICIENT_STOCK
    end
end

for i = 1, #KEYS do
    local key = KEYS[i]
    local deductAmount = tonumber(ARGV[i])

    redis.call('DECRBY', key, deductAmount)
end

return #KEYS&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 개의 상품들의 재고를 한번에 원자적으로 감소&lt;/b&gt;시키기 위해 다음과 같이 스크립트를 작성했다. &lt;b&gt;먼저 검증을 진행하고 이후에 감소를 진행하는 방식&lt;/b&gt;으로 구현했다. 다양한 에러처리를 위해 -1, -2, -3으로 나눴으며 &lt;b&gt;-1에 해당하는 MISSING_KEYS가 발생하는 경우는 현재 Redis에 해당 상품의 재고 데이터가 없는 경우 발생&lt;/b&gt;한다. 이때는 &lt;b&gt;DB를 조회하여 Redis에 반영한 후 재고 감소 로직을 재시도하는 과정을 거치도록 구현&lt;/b&gt;하였다. 이렇게 재고 감소 처리를 원자적으로 하는 것에는 성공했으나 실제 DB와 데이터 정합성 측면에서 문제가 없는지 다시 살펴봐야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;DB동기화를 비동기로 처리한다면 데이터 정합성 문제는??&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis에 적재된 재고 데이터가 TTL에 의해 만료처리 된 후 다시 DB를 통해 최신화하는 과정&lt;/b&gt;을 생각해보자. 이게 문제가 될 수 있는 부분은 DB 최신화를 진행하는 그 순간, &lt;b&gt;TTL이 만료되기 전에 진행된 다른 주문 요청에 의해서 DB에 비동기적으로 처리되고 있는 상황&lt;/b&gt;일 수 있다. 즉 DB에 있는 값이 주문이 전부 반영되지 않은 부정확한 데이터일 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 경우를 생각해보면, &lt;b&gt;주문을 처리하는 과정에서 도중에 실패하는 경우가 발생한다면 어떻게 될까?&lt;/b&gt; Redis Lua Script의 경우 &lt;b&gt;스크립트 도중에 실패하는 경우에는 Rollback&lt;/b&gt;&lt;b&gt;을 시켜주는 기능은 없다&lt;/b&gt;. 추가적인 조치가 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Rollback 도중 또 실패한다면..?&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Rollback을 하는 도중 Redis 장애에 의해 또 실패한다면 어떻게 될까? &lt;b&gt;무한 재시도&lt;/b&gt;를 해야하는 것일까? 아니면 &lt;b&gt;Rollback Event를 따로 저장해 Eventual Consistency를 만족&lt;/b&gt;하도록 해야할까? 여러 고민 결과, 성능 향상을 위해 Redis를 캐시 레이어로 도입했기 때문에 실제 데이터가 저장된 &lt;b&gt;메인 DB와의 실시간 일관성 보장은 현실적으로 어렵다고 판단&lt;/b&gt;했다. 만약 이러한 일관성을 취하기 위해서는 다시 성능을 포기해야하는 방법을 선택해야한다. 결국 &lt;b&gt;모든 장점들을 가진 은탄환 같은 완벽한 방법은 없기&lt;/b&gt;에 현재 구현하려는 상황, 요구사항을 생각해서 그에 적합한 방법을 택해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다시 돌아와서 Redis를 왜 도입했는가? 성능을 취하기 위해 도입했다. 즉, &lt;b&gt;성능을 취하는 방법인 만큼 실제 재고 데이터가 100% 정확하게 실시간으로 적용되는 것은 포기&lt;/b&gt;해야한다. 그럼 이 단점이 서비스상에서 괜찮은지를 따져보아야 한다. 처음 요구사항에서 &lt;b&gt;주문을 완료한 후 재고 수량을 초과하여 주문을 받아 이후에 주문이 취소가 되는 경우가 사용자에게 불쾌한 경험&lt;/b&gt;을 남길 수 있다고 말했었다. 그럼 재고관리가 제대로 되지 않는 경우의 수를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Redis에서 재고 차감 후 실패하는 경우(ex. 서버 다운)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Redis에 재고 데이터가 없어 &lt;/b&gt;&lt;b&gt;DB 최신화를 했지만, 주문이 전부 반영 되지 않은 데이터인 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Redis에서 재고 차감 후 실패하는 경우(ex. 서버 다운)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 트랜잭션 커밋 이전이기 때문에 &lt;b&gt;실패로 처리되어 사용자에게는 빠른 실패 응답&lt;/b&gt;을 줄 수 있다. 하지만 문제는 &lt;b&gt;DB는 롤백되어 재고차감이 되지 않았는데, Redis는 Rollback이&amp;nbsp;되지 않아 재고 불일치가 발생&lt;/b&gt;한다는 것이다. 이 경우 따라서 이를 위해 Redis 재고 복구 로직을 한번은 진행되도록 한다. 이마저도 다른 장애로 인해 반영되지 않았다면 이는 따로 조치를 취하지 않도록 하겠다. Redis는 성능을 취하기 위해서 사용하는 것이지 ACID를 완벽하게 지키기 위해 만들어진 것이 아니기 때문이다. 별도로 &lt;b&gt;Redis의 재고 데이터에 TTL을 두어 주기적으로 DB 최신화를 하는 방식으로 불일치를 줄이도록&lt;/b&gt; 하겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756794026000&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    fun recordInventoryDeductionEvent(event: InventoryDeductionRequestedEvent) {
        val payload = eventSerializer.serialize(event)
        val outboxMessage = OutboxMessage.init(INVENTORY_DEDUCTION_EVENT, payload)
        outboxMessageRepository.save(outboxMessage)
    }

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
fun restoreInventoryOnRollback(event: InventoryDeductionRequestedEvent) {
    runCatching {
        inventoryRedisRepository.restoreStocks(event.items)
    }.onFailure { ex -&amp;gt;
        log.error(ErrorMessage.INVENTORY_RESTORE_ERROR.format(event.orderId), ex)
    }
}&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;위처럼 Transactional Outbox 패턴을 적용해야하는 등 요구사항이 복잡해진다면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;TransactionPhase의 &lt;b&gt;BEFORE_COMMIT과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;AFTER_ROLLBACK을 활용해 비즈니스 로직에서 책임을 분리시켜 구현&lt;/b&gt;할 수 있다. 현재 상황의 경우 Redis 재고 차감을 가장 마지막 단계에 두면 &lt;b&gt;Rollback을 하지 않아도 되었기에 BEFORE_COMMIT 단계에 Redis 재고 차감을 두는 방식을 택해 해결하였다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Redis에 재고 데이터가 없어 DB 최신화를 했지만, 주문이 전부 반영 되지 않은 데이터인 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 정확한 데이터 최신화를 위해 DB에서 조회하였는데, 사실 그 데이터가 정확하지 않은 경우이다. 이를 해결하기 위한 방법으로는 &lt;b&gt;최신화할 당시 DB의 주문 상태를 체크해 완료되지 않은 주문 개수만큼 감소시켜 정확한 값을 얻는 방법&lt;/b&gt;이다. 하지만 이 역시 &lt;b&gt;조회하는 과정에서 시간이 소요되고, 서버가 Scale-out 된 경우라면 동시성 문제도 발생 가능하기 때문에 해당 방식은 사용하지 않았다&lt;/b&gt;. 대안으로 &lt;b&gt;주문이 적게 일어나는 시간대를 탐색하여 해당 시간에 최신화를 시켜주는 방식&lt;/b&gt;도 가능할 것 같다. 일단은 재고 데이터가 없을 때마다 최신화 하는 방법을 택하고, 추후에 이와 관련된 문제가 많이 발생한다면 다른 방법을 적용해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 개션 후 테스트&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  목표 성능&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Concurrent Users(동시 사용자 수)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;500명&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Average Response Time(평균 응답 시간)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;200ms 이하&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS(Think Time 100ms 기준)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1666 TPS&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAS(SpringBoot) 1대&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: AWS t4g.micro(2 vCPU, 1GIB Memory)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: nGrinder&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Pinpoint&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Prometheus + Grafana&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vUsers : 50명&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sleep Time : 500ms&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;일괄 주문 API - 3가지 물품 하나씩 구매하는 방식으로 진행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;일괄주문개선전리포트.png&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;1618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOXQr0/btsQkXTbjzs/47PLEOU91BrBZrrLFIJGY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOXQr0/btsQkXTbjzs/47PLEOU91BrBZrrLFIJGY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOXQr0/btsQkXTbjzs/47PLEOU91BrBZrrLFIJGY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOXQr0%2FbtsQkXTbjzs%2F47PLEOU91BrBZrrLFIJGY0%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;677&quot; height=&quot;454&quot; data-filename=&quot;일괄주문개선전리포트.png&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;1618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;일괄주문개선전CPU.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ee3zk/btsQlnqjJ8K/SkUxvUeDvPy32SBd89mpYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ee3zk/btsQlnqjJ8K/SkUxvUeDvPy32SBd89mpYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ee3zk/btsQlnqjJ8K/SkUxvUeDvPy32SBd89mpYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEe3zk%2FbtsQlnqjJ8K%2FSkUxvUeDvPy32SBd89mpYk%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;653&quot; height=&quot;230&quot; data-filename=&quot;일괄주문개선전CPU.png&quot; data-origin-width=&quot;1440&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 후&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;일괄주문개선후리포트.png&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVxmA7/btsQjUioMmg/IOfwW20FEs8N1fZ6fbkuv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVxmA7/btsQjUioMmg/IOfwW20FEs8N1fZ6fbkuv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVxmA7/btsQjUioMmg/IOfwW20FEs8N1fZ6fbkuv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVxmA7%2FbtsQjUioMmg%2FIOfwW20FEs8N1fZ6fbkuv1%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;672&quot; height=&quot;476&quot; data-filename=&quot;일괄주문개선후리포트.png&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;일괄주문개선후CPU.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/En455/btsQlSp5WG7/zAmZiwFsTifJcJ2G7C12yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/En455/btsQlSp5WG7/zAmZiwFsTifJcJ2G7C12yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/En455/btsQlSp5WG7/zAmZiwFsTifJcJ2G7C12yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEn455%2FbtsQlSp5WG7%2FzAmZiwFsTifJcJ2G7C12yk%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;636&quot; height=&quot;224&quot; data-filename=&quot;일괄주문개선후CPU.png&quot; data-origin-width=&quot;1444&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  개선 전 대비 결과 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;81&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;108.7&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;34.2% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1340 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;402 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;70.0% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;625 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;159 ms&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;74.6% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 81 &amp;rarr; 108.7로 약 &lt;b&gt;34.2% 증가&lt;/b&gt;하여 처리량 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 1340ms &amp;rarr; 402ms로 약 &lt;b&gt;70.0% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 평균 응답시간이 625ms &amp;rarr; 159ms로 약 &lt;b&gt;74.6% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 테스트 결과 해석&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AWS t4g.micro의 한계인가?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 개선 전보다 퍼센트 수치로 보면 나쁘지 않게 개선되었지만 &lt;b&gt;TPS가 108이기에 목표 TPS에 도달하려면 한참남았다&lt;/b&gt;. 응답시간은 상당히 개선되어서 TPS를 올리기 위해 더 높은 부하를 주어보았지만 서버가 바로 터졌다.. CPU도 상당히 높은 수치 관찰되는 것으로 보아 현재 서버 스펙상으로 목표 TPS를 맞추기에는 부담이 있는 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kafka와 같은 외부 메시지큐를 두어야 할까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하테스트를 수십번 진행하면서 자주 보았던 로그로는 비동기 풀이 꽉차 기본 톰캣 쓰레드풀로 넘어간다는 로그였다. RejectedExecutionHandler로 CallerRunsPolicy를 세팅해둔 상태였기 때문에 &lt;b&gt;쓰레드 풀이 넘친 이후로는 비동기가 아니라 동기로 진행되면서 응답시간에 영향을 미치는 것을 체크&lt;/b&gt;했다. 따라서 쓰레드 풀을 계속 늘려보며 테스트를 진행했을 때는 아래처럼 더 처참한 결과를 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1wbPZ/btsQl9ynWdd/dRmA5C91jRSfZNHWgkWg61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1wbPZ/btsQl9ynWdd/dRmA5C91jRSfZNHWgkWg61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1wbPZ/btsQl9ynWdd/dRmA5C91jRSfZNHWgkWg61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1wbPZ%2FbtsQl9ynWdd%2FdRmA5C91jRSfZNHWgkWg61%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;2400&quot; height=&quot;170&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 쓰레드 풀을 늘리는 것으로 해결이 안될까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰레드가 많을수록 서버는 쓰레드 생성에 더 많은 메모리를 할당해야하고, &lt;b&gt;쓰레드 수가 너무 많아지면 CPU는 대부분의 작업을 Context Switching을 하는데 소비&lt;/b&gt;한다. 하나의 &lt;b&gt;쓰레드당 1~2 mb 정도가 필요&lt;/b&gt;하고, 현재 서버 스펙상(RAM 1GB) 아무리 최대로 잡아도 500~1000개가 한계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제는 Scale-up 하자!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 개선부터 Redis까지 뒀지만 이 상태로 목표 TPS로 가는 것은 불가능해보인다. 여기서 Scale-up을 진행하고 더 필요하다면 Kafka를 도입해 추가 개선을 이어나가도록 하겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>lua script</category>
      <category>t4g.micro</category>
      <category>비동기</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/56</guid>
      <comments>https://chobo-backend.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 27 Aug 2025 11:47:28 +0900</pubDate>
    </item>
    <item>
      <title>[바로] DeadLock 범인 찾기 (Ft. 위험한 FK?)</title>
      <link>https://chobo-backend.tistory.com/55</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. DeadLock 현상 발생&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 품목 주문 API 부하테스트를 진행하면서 아래와 같이 응답이 실패함을 확인할 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JrDET/btsP62suyGD/FDDk3wUgozXr8Eb1koXlCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JrDET/btsP62suyGD/FDDk3wUgozXr8Eb1koXlCk/img.png&quot; data-alt=&quot;pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JrDET/btsP62suyGD/FDDk3wUgozXr8Eb1koXlCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJrDET%2FbtsP62suyGD%2FFDDk3wUgozXr8Eb1koXlCk%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;649&quot; height=&quot;472&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패의 원인을 찾는데는 APM 툴로 Pinpoint를 사용하고 있었기에 크게 어렵지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xQojU/btsP5oXzZqQ/kWC88Q9bL6tXcPcAm1SNn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xQojU/btsP5oXzZqQ/kWC88Q9bL6tXcPcAm1SNn1/img.png&quot; data-alt=&quot;pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xQojU/btsP5oXzZqQ/kWC88Q9bL6tXcPcAm1SNn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxQojU%2FbtsP5oXzZqQ%2FkWC88Q9bL6tXcPcAm1SNn1%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;2956&quot; height=&quot;1046&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링한 결과, DeadLock이 빈번하게 발생하고 있음을 확인했다. 위의 사진에서 보다시피 &lt;b&gt;CannotAcquireLockException&lt;/b&gt;이 반복적으로 나타나고 있으며, 특히 HikariCP 데이터베이스 연결 풀에서 &lt;b&gt;2,443ms 동안 대기하는 것을 볼 수 있다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;코난짤.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQSaoK/btsP8flMZDz/RNzE2nVC51XVNs6QoIPx51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQSaoK/btsP8flMZDz/RNzE2nVC51XVNs6QoIPx51/img.png&quot; data-alt=&quot;DeadLock의 범인은?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQSaoK/btsP8flMZDz/RNzE2nVC51XVNs6QoIPx51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQSaoK%2FbtsP8flMZDz%2FRNzE2nVC51XVNs6QoIPx51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;315&quot; data-filename=&quot;코난짤.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DeadLock의 범인은?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지금부터 DeadLock이 왜 발생했는지 찾아보자!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 문제 원인 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 주문 처리의 핵심 플로우는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yV9gJ/btsP4U3BzCN/y3uaAiLKE4sVUOy5c4WDlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yV9gJ/btsP4U3BzCN/y3uaAiLKE4sVUOy5c4WDlK/img.png&quot; data-alt=&quot;주문 처리 핵심 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yV9gJ/btsP4U3BzCN/y3uaAiLKE4sVUOy5c4WDlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyV9gJ%2FbtsP4U3BzCN%2Fy3uaAiLKE4sVUOy5c4WDlK%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;485&quot; height=&quot;379&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주문 처리 핵심 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orders와 Order_items 테이블에 순차적으로 INSERT를 수행한 후 Products 테이블의 재고를 UPDATE하는 과정에서 DeadLock이 발생했다. 특이점으로는 &lt;b&gt;Order_items 테이블의 경우 Orders와 Products를 Foreign Key(외래키)로 참조&lt;/b&gt;하고 있었다는 점이다. 이에 따라 &lt;b&gt;MySQL의 InnoDB Storage Engine에서 FK 제약조건을 적용할 경우 어떤 방식으로 동작하는지 조사&lt;/b&gt;했다. Real MySQL에 의하면 FK를 사용할 때 내부적으로 &lt;b&gt;데이터 무결성 보장을 위해서 Products에 S-Lock(공유 락)&lt;/b&gt;을 걸고 있음을 알 수 있었다. 이로 인해 다음와 같은 상황에서 DeadLock이 발생했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvKFNH/btsP54YSBOt/wkj3LH2PSlRPMYCvGWifsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvKFNH/btsP54YSBOt/wkj3LH2PSlRPMYCvGWifsk/img.png&quot; data-alt=&quot;데드락 발생 메커니즘&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvKFNH/btsP54YSBOt/wkj3LH2PSlRPMYCvGWifsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvKFNH%2FbtsP54YSBOt%2Fwkj3LH2PSlRPMYCvGWifsk%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;698&quot; height=&quot;441&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데드락 발생 메커니즘&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락 발생 메커니즘을 구체적으로 살펴보면 다음과 같다. 첫 번째 트랜잭션(T1)이 Orders와 Order_items를 INSERT할 때, Order_items 테이블의 product_id 외래키 제약조건으로 인해 &lt;b&gt;Products 테이블의 해당 레코드에 자동으로 S-Lock이 걸리게&lt;/b&gt; 된다. 동시에 두 번째 트랜잭션(T2)도 동일한 과정을 거쳐 Products 레코드에 S-Lock을 획득한다. &lt;b&gt;S-Lock의 경우 서로 호환 가능하여 Lock이 해제되지 않더라도 실행 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 문제는 이후에 발생한다. T1이 자신이 참조한 Products의 재고를 UPDATE하려 할 때 X-Lock(배타적 락)이 필요한데, T2가 이미 해당 Products에 S-Lock을 보유하고 있다. 이 경우 &lt;b&gt;X-Lock과 S-Lock은 충돌하기 때문에 T1은 X-Lock을 얻기 위해서 T2의 S-Lock이 해제될 때까지 대기 상태&lt;/b&gt;에 빠진다. 마찬가지로 T2도 자신의 Product를 UPDATE하려 할 때 T1의 S-Lock 때문에 대기하게 되어 &lt;b&gt;순환 대기 상태, 즉 데드락이 발생&lt;/b&gt;하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 아래의 &lt;b&gt;DeadLock 발생 조건 4가지 중 하나만 제거하면 DeadLock을 해결&lt;/b&gt;할 수 있게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 상호 배제 (Mutual Exclusion)&lt;/b&gt; : 한 번에 하나의 프로세스만 자원 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 점유와 대기 (Hold and Wait)&lt;/b&gt; : 자원을 점유한 채로 다른 자원을 기다림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 비선점 (No Preemption)&lt;/b&gt; : 다른 프로세스의 자원을 강제로 빼앗을 수 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 순환 대기 (Circular Wait)&lt;/b&gt; : 프로세스들이 서로의 자원을 기다리는 순환 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 여러 해결 방법 검토&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 작업 순서 변경 (순환 대기 제거)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order와 Order_items INSERT 전에 Product UPDATE를 먼저 수행하는 방식으로 순서를 변경하는 방안을 검토했다. 이 방법은 데드락의 네 가지 필요조건 중 &lt;b&gt;'순환 대기(Circular Wait)' 조건을 제거&lt;/b&gt;하는 접근이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8naMX/btsP6YEQrGv/qckh4Fwba6KgOtL0fVNa7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8naMX/btsP6YEQrGv/qckh4Fwba6KgOtL0fVNa7k/img.png&quot; data-alt=&quot;pinpoint&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8naMX/btsP6YEQrGv/qckh4Fwba6KgOtL0fVNa7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8naMX%2FbtsP6YEQrGv%2Fqckh4Fwba6KgOtL0fVNa7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;372&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pinpoint&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제로 73882건의 요청 결과 데드락은 발생하지 않았다&lt;/b&gt;. Product 테이블을 먼저 UPDATE하여 X-Lock을 획득한 후 해제하고, 이후에 Order와 Order_items를 INSERT하면 외래키 검증 시 S-Lock만 필요하게 되어 락의 획득 순서가 일관되게 유지되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 외래키 제약조건 제거 (점유와 대기 제거)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order_items 테이블의 product_id 외래키 제약조건을 제거하는 방안을 고려했다. 이는 데드락의 필요조건 중 &lt;b&gt;'점유와 대기(Hold and Wait)' 조건을 제거&lt;/b&gt;하는 방식이다. 외래키가 없으면 Order_items INSERT 시 Product 테이블에 대한 S-Lock이 자동으로 걸리지 않으므로, 트랜잭션이 Product 테이블에 대한 Lock을 미리 점유하지 않게 된다. 따라서 Product UPDATE 시 다른 트랜잭션의 S-Lock을 기다릴 필요가 없어져 &lt;b&gt;데드락이 근본적으로 해결&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. FK를 제거한다면?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키 제약조건은 데이터 무결성을 보장하는 중요한 기능이지만, 다음과 같은 &lt;b&gt;성능상 단점&lt;/b&gt;들이 존재한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자동 S-Lock 획득&lt;/b&gt;: 참조 무결성 검증 과정에서 참조되는 레코드에 자동으로 S-Lock이 걸리게 되어, 동시성 처리에 제약이 발생한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 Lock 보유 시간 증가&lt;/b&gt;: 외래키 검증으로 인한 추가적인 락 획득으로 전체 트랜잭션의 Lock 보유 시간이 길어진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INSERT/UPDATE 시 참조 테이블 조회&lt;/b&gt;: 외래키가 있는 테이블에 INSERT나 UPDATE를 수행할 때마다 참조 테이블의 존재 여부를 확인하기 위한 추가적인 조회가 발생한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE/UPDATE 시 연쇄 확인&lt;/b&gt;: 부모 테이블의 레코드를 삭제하거나 키를 변경할 때, 자식 테이블의 모든 관련 레코드를 확인해야 하는 오버헤드가 발생한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;FK를 제거한다면 참조 무결성을 해결해야한다!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 Product의 경우 &lt;b&gt;쇼핑몰 도메인 특성상 STORE_OWNER 혹은 ADMIN이 직접 관리하는 마스터 데이터&lt;/b&gt;이며, Product가 &lt;b&gt;갑작스럽게 삭제되는 경우는 발생 가능성이 낮다고 판단&lt;/b&gt;했다. 또한 정책상으로 한번 판매되었던 상품은 브랜드 측에서 판매하지 않더라도 삭제하는 것이 아니라 &lt;b&gt;품절로 처리하고 검색에서 조회되지 않도록&lt;/b&gt; 한다면, 사용자는 과거의 주문 내역에서 판매되지 않는 상품이더라도 상세정보를 확인할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무결성 문제는 &lt;b&gt;ORDER_ITEMS INSERT 이전에 별도의 SELECT 조회를 추가하여 Product 존재 여부를 판단&lt;/b&gt;하기로 했다. 이 방식의 장점은 &lt;b&gt;SELECT 조회 시에는 S-Lock이 걸리지 않아 다른 트랜잭션의 Product UPDATE 작업을 방해하지 않는다&lt;/b&gt;는 점이다. 따라서 데이터 무결성은 &lt;b&gt;Application 레벨에서 보장하면서도 성능상 이점을 가질 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 최종 결정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방안인 작업 순서 변경도 데드락을 해결할 수 있었지만, &lt;b&gt;FK 제약조건으로 인한 Lock Contention 발생 증가를 가장 크게 고려하여 최종적으로는 FK 제거를 선택&lt;/b&gt;했다. 이는 다음과 같은 이유에서다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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; : 외래키 제약조건 제거로 INSERT 시 &lt;b&gt;자동 S-Lock 획득을 방지&lt;/b&gt;하여 동시성 성능을 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인 특성&lt;/b&gt; : 쇼핑몰의 &lt;b&gt;Product는 관리자 관리 하에 있어 예기치 못한 삭제 가능성이 낮다&lt;/b&gt;. 즉, 존재하지 않는 Product와 같은 무결성 문제가 발생할 가능성이 낮다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Application 레벨 무결성 보장 가능&amp;nbsp;&lt;/b&gt;: 사전 SELECT 조회를 통한 존재 여부 확인으로 무결성 문제 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추후 MSA 분리 관점&lt;/b&gt; : Product와 Order는 서로 다른 Bounded Context에 존재하는 도메인으로, DDD(Domain-Driven Design) 관점에서 각각 &lt;b&gt;분리되어야 할 독립적인 비즈니스 영역&lt;/b&gt;이다. 즉 &lt;b&gt;FK를 분리해 Application Layer에서 다른 도메인을 조회해서 사용하는 방식은 추후 MSA 분리를 쉽게 만든다&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이를 도입한 결과, 성능 측면에서 &lt;b&gt;개선된 결과&lt;/b&gt;를 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAS(SpringBoot) 1대&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: AWS t4g.micro(2 vCPU, 1GIB Memory)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: nGrinder&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;APM 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Pinpoint&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Prometheus + Grafana&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEthc5/btsP7mMrwRn/6OwHLAAQK3jeN4f4MddUhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEthc5/btsP7mMrwRn/6OwHLAAQK3jeN4f4MddUhK/img.png&quot; data-alt=&quot;개선 후 nGrinder 부하테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEthc5/btsP7mMrwRn/6OwHLAAQK3jeN4f4MddUhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEthc5%2FbtsP7mMrwRn%2F6OwHLAAQK3jeN4f4MddUhK%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;1145&quot; height=&quot;212&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 후 nGrinder 부하테스트&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  FK 제거 전 후 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (제거 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (제거 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;179.5&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;187.4&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;4.4% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1.63s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1.44s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;11.66% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;0.939s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;0.792s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;15.66% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 179.5 &amp;rarr; 187.4로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;4.4% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 응답 지연 상위 1% 구간이 1.63s &amp;rarr; 1.44s로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;11.66% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 0.939s &amp;rarr; 0.792s로 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;15.66% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 &lt;b&gt;DeadLock&amp;nbsp;해결과 성능 최적화를 동시에 달성&lt;/b&gt;할 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>DEADLOCK</category>
      <category>lock</category>
      <category>MySQL</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/55</guid>
      <comments>https://chobo-backend.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 25 Aug 2025 15:10:44 +0900</pubDate>
    </item>
    <item>
      <title>[바로] 단일 주문 성능 개선 삽질기 (Ft. JPA save, FK)</title>
      <link>https://chobo-backend.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvNxPj/btsP9caR0nd/YUd72nokktyhTDNv8Vs8kk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvNxPj/btsP9caR0nd/YUd72nokktyhTDNv8Vs8kk/img.jpg&quot; data-alt=&quot;블랙프라이데이&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvNxPj/btsP9caR0nd/YUd72nokktyhTDNv8Vs8kk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvNxPj%2FbtsP9caR0nd%2FYUd72nokktyhTDNv8Vs8kk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;227&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;블랙프라이데이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상품을 주문하는 행위는 E-Commerce 도메인에서 가장 중요한 기능&lt;/b&gt; 중 하나이다. 먼저 재고 관리 측면에서 예를 들면, 남은 재고는 10개였으나 12개의 주문이 발생할 수 있다. 이로 인해 사용자는 결제까지 모두 완료한 이후에 &lt;b&gt;결제가 취소&lt;/b&gt;되는 상황을 겪거나 브랜드 측에서 &lt;b&gt;추가 발주를 진행&lt;/b&gt;해야하는 상황이 발생할 수 있다. 전자는 사용자에게 &lt;b&gt;서비스가 불쾌한 경험&lt;/b&gt;으로 남을 수 있고, 후자는 브랜드 측에서 &lt;b&gt;서비스에 불신&lt;/b&gt;을 가질 수 있으며 추가 발주라는 &lt;b&gt;예상치 못한 상황으로 인해 리소스&lt;/b&gt;가 발생할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;블랙프라이데이나 한정판 의류 같은 경우 매우 높은 트래픽이 발생 가능한 상황을 고려&lt;/b&gt;해야 하며, &lt;b&gt;동시성 문제로 인한 데이터 정합성 문제가 발생하면 안된다&lt;/b&gt;. 즉, 이를 해결하지 못한다면 서비스의 가치를 떨어뜨리고, 비즈니스적으로 문제를 발생시키며 이는 결국 매출감소로 이어질 수 있다. 반드시 해결해야 한다. 하지만 &lt;b&gt;성능과 데이터 정합성은 trade-off 관계&lt;/b&gt;로 모두를 만족시키기는 힘들다. 따라서 여러가지 방법들에 대한 테스트를 진행해보면서 개선 과정을 거쳐가려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 1. 테스트 환경과 개선 전 부하테스트 결과&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  목표 성능&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Concurrent Users(동시 사용자 수)&lt;/b&gt; = &lt;b&gt;500명&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Average Response Time(평균 응답 시간)&lt;/b&gt; = &lt;b&gt;100ms 이하&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 WAS(SpringBoot)&lt;/b&gt;&amp;nbsp;: AWS EC2 t4g.micro(2 vCPU, 1GiB Memory)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 DB(MySQL)&lt;/b&gt;&amp;nbsp;: AWS EC2 t4g.micro(2 vCPU, 1GiB Memory)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부하테스트 툴&lt;/b&gt; : nGrinder&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;APM 툴&lt;/b&gt; : Pinpoint&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Metric 수집 및 시각화 툴&lt;/b&gt; : Prometheus + Grafana&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 테스트 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vUsers : 변동&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Duration : 1m&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sleep Time : 100ms&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;번외로, 사이드 프로젝트에서 자주 사용하는 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;t4g.micro는 어느정도까지 트래픽을 견딜 수 있는지 궁금했다&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;☠️ 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sleep Time은 100ms&lt;/b&gt;로 고정시킨채 &lt;b&gt;vUsers 수만 바꿔가면서&lt;/b&gt; 부하테스트를 진행하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boW99y/btsPZhRWq6f/iJCiBJLxmPubV7gxeSqkkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boW99y/btsPZhRWq6f/iJCiBJLxmPubV7gxeSqkkk/img.png&quot; data-alt=&quot;테스트 환경 구성이 쉽지 않았다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boW99y/btsPZhRWq6f/iJCiBJLxmPubV7gxeSqkkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboW99y%2FbtsPZhRWq6f%2FiJCiBJLxmPubV7gxeSqkkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;466&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 환경 구성이 쉽지 않았다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, &lt;b&gt;vUsers가 200명, 500명 일 때는 서버가 곧바로 터지면서&lt;/b&gt; 측정하지 못했다. 이는 t4g.micro 성능이 낮아서 그런 것이라 판단했고, 로그를 분석한 결과 아래와 같은 에러를 확인할 수 있었다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755679900577&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ServletOutputStream failed to flush: java.io.IOException: Connection reset by peer&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에러 분석 결과, 클라이언트가 연결을 먼저 끊고 이후에 서버가 응답을 flush 할 때 응답이 더 이상 사용 불가하다고 판단하고 ClientAbortException 터트려 발생한 에러이다. 이러한 현상의 원인을 생각해보면 &lt;b&gt;서버의 성능 한계로 인한 응답 지연이 발생&lt;/b&gt;했고, 그로 인에 클라이언트(nGrinder)측에서 Time-out이 발생하여 연결을 먼저 끊은 것으로 보인다. 실제로 Grafana를 모니터링 한 결과, &lt;b&gt;Tomcat&lt;/b&gt; &lt;b&gt;쓰레드 수는 max인 200에 도달했었고 CPU 사용률은 93%&lt;/b&gt;까지 치솟았다. 또한 &lt;b&gt;주문 생성 API의 p99 latency가 30s로 측정&lt;/b&gt;된 점을 볼 때, 클라이언트 측에서 time-out이 발생할 수 밖에 없는 상황이었음을 알 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NfmyT/btsP1ivpil5/Je27JwbIT6Ke8LJH4GKmp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NfmyT/btsP1ivpil5/Je27JwbIT6Ke8LJH4GKmp1/img.png&quot; data-alt=&quot;힘들어보이는 서버..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NfmyT/btsP1ivpil5/Je27JwbIT6Ke8LJH4GKmp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNfmyT%2FbtsP1ivpil5%2FJe27JwbIT6Ke8LJH4GKmp1%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;650&quot; height=&quot;503&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;힘들어보이는 서버..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동시성 제어는 Good! 성능은 Bad..&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나머지 성공한 테스트들을 종합해보면, 가장 중요한 동시성 제어 측면에서는 완벽했다. 초기 설계 단계에서 해당 문제에 중점을 두고 맞췄었고 이를 위해 &lt;b&gt;비관적 락(for update)를 사용하여 동시성 제어 측면에서 우수한 결과&lt;/b&gt;를 만들어 낼 수 있었다. 그러나 성능 측면에서는 처참했다. &lt;b&gt;기존 성능 목표에 의하면 Think Time을 100ms로 설정했을 때 약 1666 TPS가 달성되어야 했다&lt;/b&gt;. 하지만 실제 테스트 결과 &lt;b&gt;동시 사용자 수가 50명일 때 최고 145 TPS&lt;/b&gt;에 그쳤으며, 이는 목표치에 크게 못 미치는 수준이었다. 목표 성능에 도달하기 위해 단순히 Scale-out을 적용하는 방법도 있었지만, 이는 추가적인 비용이 발생한다. 따라서 먼저 다양한 성능 개선 방안을 시도해보고, &lt;b&gt;더 이상 해결할 수 없는 한계에 도달했을 때 못하는 순간이 올 때 Scale-out을 고려&lt;/b&gt;하기로 결정했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고로 TPS 계산은 Little&amp;rsquo;s Law를 사용하였다.&lt;/blockquote&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-filename=&quot;blob&quot; data-origin-width=&quot;1163&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyk392/btsPXX05v5I/kV4G4ua7VqhX5E9XJg7S1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyk392/btsPXX05v5I/kV4G4ua7VqhX5E9XJg7S1k/img.png&quot; data-alt=&quot;Little&amp;amp;rsquo;s Law&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyk392/btsPXX05v5I/kV4G4ua7VqhX5E9XJg7S1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feyk392%2FbtsPXX05v5I%2FkV4G4ua7VqhX5E9XJg7S1k%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;595&quot; height=&quot;306&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1163&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Little&amp;rsquo;s Law&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 2. 1차 개선 (영속성 컨텍스트)&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;먼저, 상품을 주문하는 과정은 다음과 같은 플로우로 이루어진다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 재고 검증(Products)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 재고 차감(Products)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 주문 접수(Orders)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 주문 상품 등록(Order_items)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TPS 개선을 위해서 실제로 어떤 쿼리가 발생하고 있는지 확인해 보았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;select ... from users where id=1&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 요청을 한 사용자 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;select ... from products where id=1 for update&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 대상 상품 조회 + 동시성 제어를 위해 행 잠금(for update)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;select ... from orders left join order_items ... where o1_0.id=?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 특정 주문 + 주문 아이템 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;select ... from order_items where id=?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 특정 주문 아이템 단건 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insert into orders (...) values (...)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 신규 주문 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insert into order_items (...) values (...)&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 상세(아이템) 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;update products set ... where id=1&lt;/b&gt;&lt;br /&gt;&amp;rarr; 상품 재고 차감 및 상품 갱신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;update orders set ... where id=?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 정보 갱신 (modified_at 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리 중에서 3,4,8번 쿼리는 의도하지 않은 쿼리였고, 원인은 아래와 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. ID 직접 주입으로 인한 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'바로'에서는 ID 생성을 TSID를 통해 처리하고 있다. 자세한 이유는 아래 포스팅에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chobo-backend.tistory.com/52&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1755775269148&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;[바로] 분산 시스템에서 ID가 유일하려면?(Ft. Snowflake VS TSID 성능테스트)&quot; data-og-description=&quot;패션 플랫폼 &amp;lsquo;바로&amp;rsquo; 개발을 시작하면서 도메인을 구현하던 중 ID 생성 방식에 대해 고민하게 되었다. 기존에는 Auto Increment로 만들고 있었지만 이는 단일 서버의 환경에서만 안정적이지 않을까&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/52&quot; data-og-url=&quot;https://chobo-backend.tistory.com/52&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pdogK/hyZzLijKji/ynIxlIAjQKTDHhwzB4CJqk/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270,https://scrap.kakaocdn.net/dn/b9vyCw/hyZDUq7ESd/rKa41F7KAaHYHqdcpkBiH0/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270,https://scrap.kakaocdn.net/dn/5WPKo/hyZC3Pugg7/rk7ygmZC0qhokXfxZLafj1/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/52&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pdogK/hyZzLijKji/ynIxlIAjQKTDHhwzB4CJqk/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270,https://scrap.kakaocdn.net/dn/b9vyCw/hyZDUq7ESd/rKa41F7KAaHYHqdcpkBiH0/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270,https://scrap.kakaocdn.net/dn/5WPKo/hyZC3Pugg7/rk7ygmZC0qhokXfxZLafj1/img.png?width=439&amp;amp;height=270&amp;amp;face=0_0_439_270');&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;[바로] 분산 시스템에서 ID가 유일하려면?(Ft. Snowflake VS TSID 성능테스트)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;패션 플랫폼 &amp;lsquo;바로&amp;rsquo; 개발을 시작하면서 도메인을 구현하던 중 ID 생성 방식에 대해 고민하게 되었다. 기존에는 Auto Increment로 만들고 있었지만 이는 단일 서버의 환경에서만 안정적이지 않을까&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755775493510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public &amp;lt;S extends T&amp;gt; S save(S entity) {
    Assert.notNull(entity, &quot;Entity must not be null&quot;);
    if (this.entityInformation.isNew(entity)) {
        this.entityManager.persist(entity);
        return entity;
    } else {
        return (S)this.entityManager.merge(entity);
    }
}&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;JPA의 save과정에서는 해당 엔티티가 새로운 엔티티인지 판단하는 단계가 있다.&amp;nbsp; Hibernate는 &lt;b&gt;엔티티의 식별자(ID) 값의 존재 여부&lt;/b&gt;를 기준으로 엔티티 상태를 판단하는데, &lt;b&gt;ID가 null이면 새로운 엔티티로, null이 아니면 기존 엔티티로 간주&lt;/b&gt;한다. 따라서 현재 ID를 직접 주입하는 방식을 사용하고 있어 entityManager.merge(order)가 실행되었고, 이로 인해 &lt;b&gt;불필요한 select 쿼리가 추가로 발생&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 해결 방안 검토&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 &lt;b&gt;ID를&amp;nbsp;@PrePersist로 주입하는 방식을 먼저 고려&lt;/b&gt;했다. 하지만 이 방법은 ID 값을 나중에 주입해야 하므로 &lt;b&gt;val을 var로 변경&lt;/b&gt;해야 했다. ID는 절대 불변해야 하는 값이므로 안전성이 떨어진다고 판단하여 채택하지 않았다. 그래서 다른 방법으로 &lt;b&gt;Persistable 인터페이스를 직접 구현하여 isNew()의 로직을 변경하는 방식이 가장 안전하다고 판단&lt;/b&gt;했다. 기존에 id를 통해서 판단하는 것이 아니라 &lt;b&gt;createdAt을 기준으로 isNew()를 판단하도록 변경&lt;/b&gt;하면, ID를 var로 바꾸지 않아도 되어 최선의 방법이라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 최종 구현 방식&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755826669740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity(
    @CreatedDate
    @Column(name = &quot;created_at&quot;, columnDefinition = &quot;TIMESTAMP&quot;, nullable = false, updatable = false)
    var createdAt: Instant? = null,

    @LastModifiedDate
    @Column(name = &quot;modified_at&quot;, columnDefinition = &quot;TIMESTAMP&quot;)
    var modifiedAt: Instant? = null,
) : Persistable&amp;lt;Long&amp;gt; {

    override fun isNew(): Boolean = (this.createdAt == null)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 다음과 같이 쿼리 횟수가 감소하였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;select ... from users where id=1&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문하려는 사용자 정보 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;select ... from products where id=1 for update&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 대상 상품 조회 + 재고/동시성 제어 위해 &lt;b&gt;행 잠금(for update)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insert into orders ...&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 엔티티 저장 (주문 생성)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insert into order_items ...&lt;/b&gt;&lt;br /&gt;&amp;rarr; 주문 상세(상품별 내역) 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;update products set ... where id=1&lt;/b&gt;&lt;br /&gt;&amp;rarr; 상품 정보 수정 (재고 차감, 수정 시각 갱신 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 1차 개선 후 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 &lt;b&gt;CPU 사용률이 80%&lt;/b&gt;에 도달하는 지점인 &lt;b&gt;동시 사용자 100명을 기준으로 부하 테스트&lt;/b&gt;를 진행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;개선 전&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;2038&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HGEFj/btsP2fUBIa9/uAKMJzyZFEKqWEoYMHksJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HGEFj/btsP2fUBIa9/uAKMJzyZFEKqWEoYMHksJK/img.png&quot; data-alt=&quot;개선 전 nGrinder 부하테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HGEFj/btsP2fUBIa9/uAKMJzyZFEKqWEoYMHksJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHGEFj%2FbtsP2fUBIa9%2FuAKMJzyZFEKqWEoYMHksJK%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;826&quot; height=&quot;170&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 전 nGrinder 부하테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt; 121&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt; 2.72s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt; 1.21s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;개선 후&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;1896&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqmLzn/btsP42lAS4T/vLO3eKYp5KU1xkgl7e2KQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqmLzn/btsP42lAS4T/vLO3eKYp5KU1xkgl7e2KQ1/img.png&quot; data-alt=&quot;개선 후 nGrinder 부하테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqmLzn/btsP42lAS4T/vLO3eKYp5KU1xkgl7e2KQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqmLzn%2FbtsP42lAS4T%2FvLO3eKYp5KU1xkgl7e2KQ1%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;806&quot; height=&quot;179&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 후 nGrinder 부하테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PO0tr/btsP1WAOCcx/HYH6llKIWkfCiRBKBRKnbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PO0tr/btsP1WAOCcx/HYH6llKIWkfCiRBKBRKnbk/img.png&quot; data-alt=&quot;p99 latency&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PO0tr/btsP1WAOCcx/HYH6llKIWkfCiRBKBRKnbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPO0tr%2FbtsP1WAOCcx%2FHYH6llKIWkfCiRBKBRKnbk%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;561&quot; height=&quot;230&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;p99 latency&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt; 168&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt; 1.66s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt; 0.95s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 개선 전 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (개선 후)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;121&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;168&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;38.8% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;2.72 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1.66 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;38.9% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1.21 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;0.95 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;21.5% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS &lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 121 &amp;rarr; 168로 약 &lt;b&gt;39% 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency &lt;/b&gt;: 응답 지연 상위 1% 구간이 2.72s &amp;rarr; 1.66s로 약 &lt;b&gt;39% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;&lt;/b&gt;: 1.21s &amp;rarr; 0.95s로 약 &lt;b&gt;22% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. 1차 개선 결과 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 개선을 통해 상당한 성능 향상을 달성할 수 있었다. &lt;b&gt;TPS가 39% 증가하고 p99 latency가 39% 단축&lt;/b&gt;되면서 시스템의 처리 능력과 응답성이 크게 개선되었다. 특히 &lt;b&gt;평균 응답 시간도 22% 단축&lt;/b&gt;되어 전체 사용자가 체감할 수 있는 성능 향상을 이뤄냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선의 핵심 사항으로는 &lt;b&gt;JPA save 메커니즘에서 발생하는 불필요한 쿼리 제거&lt;/b&gt;에 있었다. ID를 직접 주입하는 방식으로 인해 Hibernate가 엔티티를 기존 엔티티로 판단하여 merge 과정에서 추가적인 select 쿼리가 발생했던 문제를 해결했다. Persistable 인터페이스 구현을 통해 createdAt 기준으로 isNew() 로직을 변경함으로써 &lt;b&gt;8개의 쿼리를 5개로 줄일 수 있었다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 쿼리 개수 감소만으로도 이정도의 성능 향상을 얻을 수 있다는 점이 인상적이었다. 특히 높은 동시성 환경에서는 불필요한 DB 접근이 전체 시스템 성능에 미치는 영향이 생각보다 크다는 것을 확인할 수 있었다. 물론 Lock을 사용하는 경우라 더 큰 영향을 주었을 것이지만, &lt;b&gt;ORM 사용 시 내부 동작 메커니즘에 대한 정확한 이해가 성능 최적화에 얼마나 중요&lt;/b&gt;한지 알게된 테스트였다. 하지만 여전히 목표 성능에는 미치지 못하였기에 2차 개선을 진행하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 3. 2차 개선 (&lt;b&gt;원자적 재고 감소 처리&lt;/b&gt;)&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 병목은 DB!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 성능을 더 높일 수 있는 방법을 고민했을 때, 결국 현재 &lt;b&gt;가장 큰 병목은 DB&lt;/b&gt;에서 발생한다고 판단했다. 내부적으로 비관적 락을 통해 상품을 조회하고 비즈니스 로직을 수행한 후 트랜잭션이 종료될 때(COMMIT 또는 ROLLBACK) 락이 해제되는 구조로 인해 Lock&lt;b&gt;&amp;nbsp;보유 시간이 필요 이상으로 길어지고&lt;/b&gt; 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 개선 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 락을 명시적(FOR UPDATE)으로 사용하지 않고 &lt;b&gt;UPDATE 문을 통해 한번에 변경사항을 적용&lt;/b&gt;한다면 상당한 개선이 가능할 것이라 생각했다. UPDATE 방식의 장점으로는 WHERE 절에 재고 수량 확인 조건 (stock &amp;gt;= ?)을 포함시켜, &lt;b&gt;재고 확인과 차감을 하나의 원자적 연산&lt;/b&gt;으로 묶을 수 있다는 점이다. 이때 MySQL이 UPDATE 과정에서 자동으로 &lt;b&gt;행 수준 X-Lock을 걸어 다른 트랜잭션의 동시 변경을 차단&lt;/b&gt;하므로 동시성 이슈가 발생하지 않는다. 이를 통해 UPDATE 문을 수행할 때만 Lock을 사용한다는 점에서 &lt;b&gt;Lock 보유 시간이 많이 줄어들어 큰 성능 향상이 있을 것으로 예상&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 2차 개선 후 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y23qK/btsP5JzUfLg/CrPfgmDAGfDhycuNxv83z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y23qK/btsP5JzUfLg/CrPfgmDAGfDhycuNxv83z0/img.png&quot; data-alt=&quot;nGrinder 부하테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y23qK/btsP5JzUfLg/CrPfgmDAGfDhycuNxv83z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy23qK%2FbtsP5JzUfLg%2FCrPfgmDAGfDhycuNxv83z0%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;784&quot; height=&quot;145&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder 부하테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daTx0B/btsP6fSQvdb/TsffCUOvZ17v5x8SNzmk00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daTx0B/btsP6fSQvdb/TsffCUOvZ17v5x8SNzmk00/img.png&quot; data-alt=&quot;Prometheus + Grafana&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daTx0B/btsP6fSQvdb/TsffCUOvZ17v5x8SNzmk00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaTx0B%2FbtsP6fSQvdb%2FTsffCUOvZ17v5x8SNzmk00%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;356&quot; height=&quot;254&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Prometheus + Grafana&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;179.5&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;1.63s&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;0.939s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 1차 개선 대비 성능 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (1차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (2차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;168&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;179.5&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;6.85% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1.66 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1.63s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;1.81% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;0.95 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;0.939s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;1.16% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS &lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 168 &amp;rarr; 179.5로 약 &lt;b&gt;6.85% 증가&lt;/b&gt;하여 처리량이 소폭 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency &lt;/b&gt;: 응답 지연 상위 1% 구간이 1.66s &amp;rarr; 1.63s로 약 &lt;b&gt;1.81% 단축&lt;/b&gt;되었으나 미미한 개선 수준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time &lt;/b&gt;: 0.95s &amp;rarr; 0.939s로 약 &lt;b&gt;1.16% 단축&lt;/b&gt;되어 미세한 성능 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 2차 개선 결과 정리&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;분명 많은 개선이 있어야 했지만 그러지 않았다.. 이유가 뭘까?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 개선에서는 &lt;b&gt;기대했던 것보다 상대적으로 소폭의 성능 향상&lt;/b&gt;을 보였다. TPS 6.85% 증가되었지만, 1차 개선에서 보여준 성능 향상에 비해서는 아쉬운 결과였다. 특히 p99 latency와 평균 응답 시간의 개선폭이 각각 1.81%, 1.16%에 그치면서 사용자 체감 성능 향상은 미미한 수준이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원자적 재고 감소 처리를 통한 개선 효과는 이론적으로는 상당할 것으로 예상&lt;/b&gt;되었다. SELECT FOR UPDATE 방식에서 단일 UPDATE 쿼리 방식으로 변경하여 Lock 보유 시간을 단축하고, 재고 확인과 차감을 하나의 원자적 연산으로 처리할 수 있었다. 하지만 &lt;b&gt;실제 성능 측정 결과는 예상보다 낮은 개선율&lt;/b&gt;을 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 결과가 나온 이유로는 &lt;b&gt;몇 가지를 추측&lt;/b&gt;해볼 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 1차 개선을 통해 이미 주요 병목 지점이 상당 부분 해결되어 &lt;b&gt;추가 개선의 여지가 제한적&lt;/b&gt;이었을 가능성이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. UPDATE 쿼리 방식도 결국 행 수준 락을 사용하며, &lt;b&gt;재고처리 검증 로직을 Application 레벨에서 DB 레벨로 위임했기에 실행 시간이 늘어나면서 개선 전과 비슷&lt;/b&gt;했을 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;의도치 않은 동작으로 인해 Products UPDATE 쿼리가 지연&lt;/b&gt;되고 있을 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 4. 3차 개선 (FK)&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 개선 후에도 여전히 성능 한계가 있다고 판단하여 3차 개선을 진행하게 되었다. 현재 order_items 테이블이 orders와 products 테이블을 참조하는 FK를 가지고 있는데, FK 제약 조건을 추가했을 때 MySQL 내부적으로 데이터 무결성을 위한 추가 동작이 있지 않을까 추측했다. 실제로 Real MySQL에 의하면 &lt;b&gt;InnoDB Storage Engine에서는 테이블의 변경(쓰기 잠금)이 발생한 경우에 부모 테이블이나 자식 테이블 모두 데이터가 있는지 체크하는 작업&lt;/b&gt;을 진행한다. 이로 인해 &lt;b&gt;참조하는 테이블 레코드에 S-Lock&lt;/b&gt;을 걸게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FK를 설정한 것 만으로 개발자가 의도하지 않게 &lt;b&gt;Lock이 여러 테이블로 전파되면서 Lock Contention이 심화&lt;/b&gt;되고, 특히 높은 동시성 환경에서는 &lt;b&gt;DeadLock 발생 빈도가 증가하는 문제가 관찰&lt;/b&gt;되었다. 자세한 내용은 아래 포스팅에 정리해 두었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chobo-backend.tistory.com/55&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756205307724&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;[바로] DeadLock 범인 찾기 (Ft. 위험한 FK?)&quot; data-og-description=&quot;Part 1. DeadLock 현상 발생단일 품목 주문 API 부하테스트를 진행하면서 아래와 같이 응답이 실패함을 확인할 수 있었다 실패의 원인을 찾는데는 APM 툴로 Pinpoint를 사용하고 있었기에 크게 어렵지 않&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/55&quot; data-og-url=&quot;https://chobo-backend.tistory.com/55&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3YOGx/hyZC8dgPjV/KgTQMKq3NkVgquyhOkuVG1/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bYM4zD/hyZC49KDVF/tk8y900s8eTVAWwNC4yowK/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bgtJ41/hyZC334XKt/zUfVdOtx3WmUH4cOkRH99K/img.png?width=2956&amp;amp;height=1046&amp;amp;face=0_0_2956_1046&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/55&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3YOGx/hyZC8dgPjV/KgTQMKq3NkVgquyhOkuVG1/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bYM4zD/hyZC49KDVF/tk8y900s8eTVAWwNC4yowK/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bgtJ41/hyZC334XKt/zUfVdOtx3WmUH4cOkRH99K/img.png?width=2956&amp;amp;height=1046&amp;amp;face=0_0_2956_1046');&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;[바로] DeadLock 범인 찾기 (Ft. 위험한 FK?)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Part 1. DeadLock 현상 발생단일 품목 주문 API 부하테스트를 진행하면서 아래와 같이 응답이 실패함을 확인할 수 있었다 실패의 원인을 찾는데는 APM 툴로 Pinpoint를 사용하고 있었기에 크게 어렵지 않&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 개선 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 3차 개선에서는 &lt;b&gt;FK 제약 조건 중 Products를 제거하여 DB 레벨의 참조 무결성 검증 오버헤드를 없애고&lt;/b&gt;, 대신 &lt;b&gt;애플리케이션 레벨에서 데이터 무결성을 관리&lt;/b&gt;하는 방식으로 전환하기로 결정했다. 이를 통해 트랜잭션 처리 시 &lt;b&gt;불필요한 락 전파를 방지&lt;/b&gt;하고 &lt;b&gt;전체적인 처리 성능을 향상시킬 수 있을 것으로 기대&lt;/b&gt;했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 3차 개선 후 부하테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K0K5t/btsP44zafYa/3Aq1XBJdC4OcLLv1hOVQOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K0K5t/btsP44zafYa/3Aq1XBJdC4OcLLv1hOVQOk/img.png&quot; data-alt=&quot;nGrinder 부하테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K0K5t/btsP44zafYa/3Aq1XBJdC4OcLLv1hOVQOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK0K5t%2FbtsP44zafYa%2F3Aq1XBJdC4OcLLv1hOVQOk%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;784&quot; height=&quot;145&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nGrinder 부하테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tKIui/btsP7AQnZyc/GCDlvR5YPPBS7v6HOqlja1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tKIui/btsP7AQnZyc/GCDlvR5YPPBS7v6HOqlja1/img.png&quot; data-alt=&quot;Prometheus + Grafana&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tKIui/btsP7AQnZyc/GCDlvR5YPPBS7v6HOqlja1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtKIui%2FbtsP7AQnZyc%2FGCDlvR5YPPBS7v6HOqlja1%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;442&quot; height=&quot;265&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Prometheus + Grafana&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS :&lt;/b&gt;&lt;span&gt; 187.4&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;p99 latency :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;1.44s&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;Average response time&lt;/b&gt;&amp;nbsp;:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;0.792s&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 2차 개선 대비 결과 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (2차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (3차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;179.5&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;187.4&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;4.4% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1.63s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1.44s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;11.66% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;0.939s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;0.792s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;15.66% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS &lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 179.5 &amp;rarr; 187.4로 약 &lt;b&gt;4.4% 증가&lt;/b&gt;하여 처리량이 소폭 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency &lt;/b&gt;: 응답 지연 상위 1% 구간이 1.63s &amp;rarr; 1.44s로 약 &lt;b&gt;11.66% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time &lt;/b&gt;: 0.939s &amp;rarr; 0.792s로 약 &lt;b&gt;15.66% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 3차 개선 결과 정리&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2차 개선이 저조했던 이유 = FK를 조심하자!&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3차 개선에서는 2차 개선과 달리 눈에 띄는 성능 향상을 보였다. &lt;b&gt;TPS는 4.4% 증가에 그쳤지만, p99 latency 11.66% 단축과 평균 응답 시간 15.66% 단축이라는 의미 있는 개선 효과&lt;/b&gt;를 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 개선의 핵심으로는 &lt;b&gt;Lock Contention 해소로 인한 대기 시간 감소&lt;/b&gt;에 있었다. 기존에는 order_items 삽입 시 products 테이블에 대한 참조 무결성 검증을 위해 &lt;b&gt;InnoDB가 자동으로 S-Lock을 걸면서 락이 여러 테이블로 전파되는 문제&lt;/b&gt;가 있었다. FK 제거를 통해 이러한 불필요한 &lt;b&gt;Lock 전파를 차단하고, 높은 동시성 환경에서 DeadLock을 발생시키지 않았다&lt;/b&gt;. Lock 전파로 인한 문제가 되는 부분은 아래 포스팅에서 자세히 정리해두었다(위 DeadLock 포스팅과 동일한 것이다ㅎㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/55&quot;&gt;https://chobo-backend.tistory.com/55&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756205300568&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;[바로] DeadLock 범인 찾기 (Ft. 위험한 FK?)&quot; data-og-description=&quot;Part 1. DeadLock 현상 발생단일 품목 주문 API 부하테스트를 진행하면서 아래와 같이 응답이 실패함을 확인할 수 있었다 실패의 원인을 찾는데는 APM 툴로 Pinpoint를 사용하고 있었기에 크게 어렵지 않&quot; data-og-host=&quot;chobo-backend.tistory.com&quot; data-og-source-url=&quot;https://chobo-backend.tistory.com/55&quot; data-og-url=&quot;https://chobo-backend.tistory.com/55&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3YOGx/hyZC8dgPjV/KgTQMKq3NkVgquyhOkuVG1/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bYM4zD/hyZC49KDVF/tk8y900s8eTVAWwNC4yowK/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bgtJ41/hyZC334XKt/zUfVdOtx3WmUH4cOkRH99K/img.png?width=2956&amp;amp;height=1046&amp;amp;face=0_0_2956_1046&quot;&gt;&lt;a href=&quot;https://chobo-backend.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chobo-backend.tistory.com/55&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3YOGx/hyZC8dgPjV/KgTQMKq3NkVgquyhOkuVG1/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bYM4zD/hyZC49KDVF/tk8y900s8eTVAWwNC4yowK/img.png?width=420&amp;amp;height=315&amp;amp;face=0_0_420_315,https://scrap.kakaocdn.net/dn/bgtJ41/hyZC334XKt/zUfVdOtx3WmUH4cOkRH99K/img.png?width=2956&amp;amp;height=1046&amp;amp;face=0_0_2956_1046');&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;[바로] DeadLock 범인 찾기 (Ft. 위험한 FK?)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Part 1. DeadLock 현상 발생단일 품목 주문 API 부하테스트를 진행하면서 아래와 같이 응답이 실패함을 확인할 수 있었다 실패의 원인을 찾는데는 APM 툴로 Pinpoint를 사용하고 있었기에 크게 어렵지 않&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chobo-backend.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 DB 레벨의 참조 무결성 검증 오버헤드가 제거되면서 트랜잭션 처리 효율성도 향상되었다. 매&lt;b&gt;번 부모-자식 테이블 간 데이터 존재 여부를 확인하는 추가적인 작업이 생략되어 전체적인 처리 시간이 단축&lt;/b&gt;되었고, 이는 특히 대량의 동시 요청이 발생하는 환경에서 시스템 안정성에 긍정적인 영향을 미쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이러한 성능 개선은 &lt;b&gt;데이터 무결성 관리에 대한 책임을 애플리케이션으로 이전시키는 트레이드오프&lt;/b&gt;를 수반한다. DB 레벨의 참조 무결성 보장 기능을 포기하는 대신 애플리케이션 레벨에서 데이터 무결성을 관리해야 하므로 개발 복잡도가 증가하고, &lt;b&gt;데이터 일관성을 위한 추가적인 검증 로직 구현이 필요&lt;/b&gt;하다. 그럼에도 불구하고 높은 동시성이 발생가능한 주문 시스템에서는 성능과 안정성을 우선시하는 것이 합리적인 선택이라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Part 5. 최종 결과와 한계점&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  개선 전 대비 결과 비교 (Before vs After)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.6278%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px; width: 26.2583%; text-align: center;&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.272%; text-align: center;&quot;&gt;&lt;b&gt;Before (개선 전)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 20.251%; text-align: center;&quot;&gt;&lt;b&gt;After (3차 개선)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;개선 효과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;TPS (Transactions/sec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;121&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;187.4&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;54.9% &amp;uarr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;p99 latency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;2.72 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;1.44s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;47.1% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 26.2583%; text-align: left;&quot;&gt;&lt;b&gt;Average response time&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.272%; text-align: center;&quot;&gt;1.21 s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.251%; text-align: center;&quot;&gt;0.792s&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.6946%; text-align: center;&quot;&gt;&lt;b&gt;34.6% &amp;darr;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TPS&lt;/b&gt;: 초당 처리 가능한 트랜잭션 수가 121 &amp;rarr; 187.4로 약 &lt;b&gt;54.9% 증가&lt;/b&gt;하여 처리량 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 latency&lt;/b&gt;: 응답 지연 상위 1% 구간이 2.72s &amp;rarr; 1.44s로 약 &lt;b&gt;47.1% 단축&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Average response time&lt;/b&gt;: 1.21s &amp;rarr; 0.792s로 약 &lt;b&gt;34.5% 단축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;목표 성능에는 턱없이 부족하다... t4g.micro의 한계인가?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  무엇을 더 해볼 수 있을까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 3가지 정도가 떠올랐다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. DB Connection Pool을 늘린다면 개선 될 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 서버나 DB를 Scale-up 한다면 개선 될 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Redis를 도입하면 개선 될 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비즈니스 로직이 훨씬 더 복잡해진다면?&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 주문 비즈니스 로직은 아주 간단하다. 재고 검증, 재고 처리, 주문 생성.&lt;/li&gt;
&lt;li&gt;그렇다면 비즈니스 로직이 지금보다 &lt;b&gt;더 복잡해지는 상황에서도 현재 방식이 가능한지 체크&lt;/b&gt;해야한다&lt;/li&gt;
&lt;li&gt;예를 들어, 쿠폰, 유저 등급에 따른 할인, 결제 등등 여러가지가 도입되었을 때이다&lt;/li&gt;
&lt;li&gt;UPDATE 단일 쿼리로 재고 관리가 가능할까에 대해서 고민해봐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;다음에 이어서..&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 방법들에 대한 고민한 내용을 담아 다음 포스팅에 정리하겠다!&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>DB</category>
      <category>lock</category>
      <category>MySQL</category>
      <author>chobo99</author>
      <guid isPermaLink="true">https://chobo-backend.tistory.com/54</guid>
      <comments>https://chobo-backend.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 20 Aug 2025 15:56:44 +0900</pubDate>
    </item>
  </channel>
</rss>