<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발이야기</title>
    <link>https://seungjjun.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 20 May 2026 22:23:46 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>seungjjun</managingEditor>
    <item>
      <title>2025년 상반기 회고</title>
      <link>https://seungjjun.tistory.com/360</link>
      <description>&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;2025년 상반기 회고&lt;/b&gt;&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 정말 빠르게 흘러 벌써 2025년 상반기가 훌쩍 지나갔다.&lt;/p&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;b&gt;특히 이번 상반기에는 &lt;span data-token-index=&quot;1&quot;&gt;업무적으로 담당했던 기능이 많아 매우 바쁘게 보냈던 터라, 이 시점에서 한번 정리하는 시간을 갖는 것이 좋겠다 싶었다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바쁜 업무를 핑계로 개인적인 성장이 소홀했던 점은 없는지 돌아보고 반성하는 마음도 이번 회고글에 담으려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 6개월간의 &lt;span data-token-index=&quot;1&quot;&gt;업무적인 성장뿐만 아니라 개인적인 경험까지, 나의 상반기 전반을 솔직하게 돌아볼 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘했던 점과 아쉬웠던 점을 돌아보며 앞으로 더 나아갈 나를 위한 소중한 발판을 마련하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 회사&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 상반기에는 정말 다양한 업무들을 경험하며 기술적인 역량뿐만 아니라 문제 해결 능력까지 키울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상반기 초, 회사의 새로운 서비스 런칭에 기여하고 싶은 마음이 매우 커서 그야말로 열정적으로 보냈던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쳐내야 할 task들은 쌓여가는데, 그것들을 처리할 개발자의 수가 절대적으로 적었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이러한 업무를 내가 담당하여 빠르게 처리하고 쌓여있는 task들을 지우는 것에 재미를 느꼈고, 빠르게 일하는 것이 회사에 기여하는 길이라고 생각했다.&lt;/p&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;&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;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;특히 '빠르게 지우는것'에만 몰두하다 보니, &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;/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;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;style1&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 개인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상반기에는 개인적인 성장을 위한 공부를 &lt;span data-token-index=&quot;1&quot;&gt;상당히 소홀히 했던 것이 사실&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 업무가 워낙 많다는 핑계 아닌 핑계로, 퇴근 후 집에 오면 더 이상 손을 대기가 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 퇴근하고도 이것저것 찾아보고 시도해 보는 데 열정이 있었는데, 상반기에는 그 에너지가 고갈된 느낌이었다.&lt;/p&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 data-token-index=&quot;1&quot;&gt;한 달에 한 번은 업무를 통해 배운 점을 블로그에 포스팅하겠다는 작은 목표마저도 제대로 지키지 못했다는 것&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 배운 것도 많았고, 기록으로 남겨두면 나중에 큰 도움이 될 것을 알면서도 꾸준히 실천하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그를 꾸준히 운영하는 것이 얼마나 어려운 일인지 다시 한번 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기계발 서적이나 기술 관련 글을 읽는 시간도 현저히 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시 주춤했던 운동을 다시 시작하며 건강을 챙긴 것은 긍정적이지만, 개인적인 성장은 아쉬움이 많이 남는 상반기였다.&lt;/p&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;style1&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 하반기 목표&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상반기 회고를 통해 얻은 깨달음을 바탕으로, 하반기에는 더욱 균형 잡힌 성장을 이루기 위한 목표를 세워보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;
&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;이와 함께 업무 또는 개인적으로 공부를 통해 얻은 인사이트를 최소 월 1회 이상 블로그에 포스팅하는 것을 목표로 하자.&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;/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;최소 주 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;&amp;nbsp;&lt;/p&gt;</description>
      <category>성장이야기</category>
      <category>2025년 상반기 회고</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/360</guid>
      <comments>https://seungjjun.tistory.com/360#entry360comment</comments>
      <pubDate>Sun, 3 Aug 2025 12:05:51 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드 작성, 낭비가 아닌 투자였다.</title>
      <link>https://seungjjun.tistory.com/358</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&quot;또&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;작성하라고?&amp;nbsp;실제&amp;nbsp;기능&amp;nbsp;개발하기도&amp;nbsp;바쁜데...&quot;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 개발자, 특히 저와 같은 주니어 개발자들이 테스트 코드 작성에 대해 처음 가지는 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 역시 그랬습니다.&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;코드를&amp;nbsp;작성하면&amp;nbsp;개발&amp;nbsp;시간이&amp;nbsp;배로&amp;nbsp;늘어날&amp;nbsp;것이고,&amp;nbsp;코드를&amp;nbsp;리팩터링 할&amp;nbsp;때마다&amp;nbsp;테스트&amp;nbsp;코드까지&amp;nbsp;수정해야&amp;nbsp;한다면&amp;nbsp;유지보수는&amp;nbsp;더욱&amp;nbsp;힘들어질&amp;nbsp;거라고&amp;nbsp;생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;무엇보다&lt;b&gt;&amp;nbsp;&quot;도대체&amp;nbsp;어떤&amp;nbsp;것을&amp;nbsp;검증해야&amp;nbsp;옳은&amp;nbsp;테스트&amp;nbsp;코드인가?&quot;&lt;/b&gt;라는&amp;nbsp;근본적인&amp;nbsp;질문이&amp;nbsp;항상&amp;nbsp;머릿속을&amp;nbsp;맴돌았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 70개의 단위 테스트 코드를 작성하며 신규 기능을 개발한 지금, 제 인식은 완전히 바뀌었습니다. 테스트 코드 작성은 시간 낭비가 아니라 오히려 시간을 절약하기 위한 투자였습니다. 테스트 코드를 통해 안정적인 리팩토링이 가능해졌고, Postman 같은 도구로 수동 검증하던 불필요한 작업들이 자동화되었습니다. 게다가 BDD 방식으로 작성된 테스트 코드는 팀원들에게 기능의 의도를 명확히 전달하는 &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;이 글에서는 JUnit5, Mockito를 활용해 약 70개의 단위 테스트를 작성하며 배운 실전 노하우를 공유하려 합니다. 특히 주니어 개발자 관점에서 테스트 코드의 가치, BDD 스타일 테스트 코드의 구성 방법, 그리고 테스트 코드 작성 시 직면하게 될 문제점들과 그 해결책에 대해 이야기하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 코드가 개발 프로세스에 가져온 변화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;테스트&amp;nbsp;코드는&amp;nbsp;개발&amp;nbsp;속도를&amp;nbsp;늦춘다.&quot;&amp;nbsp;이것이&amp;nbsp;제가&amp;nbsp;가장&amp;nbsp;크게&amp;nbsp;오해했던&amp;nbsp;부분입니다.&amp;nbsp;하지만&amp;nbsp;실제로&amp;nbsp;프로젝트에&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;적용해 보니,&amp;nbsp;정반대의&amp;nbsp;결과를&amp;nbsp;가져왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트&amp;nbsp;코드&amp;nbsp;작성&amp;nbsp;전:&amp;nbsp;불안한&amp;nbsp;개발&amp;nbsp;사이클&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&amp;nbsp;코드를&amp;nbsp;작성하기&amp;nbsp;전에는&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;개발&amp;nbsp;사이클을&amp;nbsp;반복했습니다:&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;비즈니스&amp;nbsp;코드&amp;nbsp;작성&lt;br /&gt;2.&amp;nbsp;Postman으로&amp;nbsp;API&amp;nbsp;호출&amp;nbsp;및&amp;nbsp;응답&amp;nbsp;확인&lt;br /&gt;3.&amp;nbsp;문제&amp;nbsp;발견&amp;nbsp;시&amp;nbsp;코드&amp;nbsp;수정&lt;br /&gt;4.&amp;nbsp;다시&amp;nbsp;Postman으로&amp;nbsp;테스트&lt;br /&gt;5.&amp;nbsp;연관된&amp;nbsp;다른&amp;nbsp;기능들도&amp;nbsp;여전히&amp;nbsp;정상&amp;nbsp;작동하는지&amp;nbsp;일일이&amp;nbsp;확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링이&amp;nbsp;필요할&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;&quot;내가&amp;nbsp;놓친&amp;nbsp;케이스가&amp;nbsp;있지&amp;nbsp;않을까?&quot;라는&amp;nbsp;불안감은&amp;nbsp;항상&amp;nbsp;따라다녔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트&amp;nbsp;코드&amp;nbsp;작성&amp;nbsp;후:&amp;nbsp;개발&amp;nbsp;사이클&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&amp;nbsp;코드를&amp;nbsp;작성한&amp;nbsp;후,&amp;nbsp;개발&amp;nbsp;사이클이&amp;nbsp;완전히&amp;nbsp;바뀌었습니다&lt;/p&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.&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;작성&amp;nbsp;(또는&amp;nbsp;기존&amp;nbsp;테스트&amp;nbsp;수정)&lt;br /&gt;2.&amp;nbsp;기능&amp;nbsp;코드&amp;nbsp;작성&lt;br /&gt;3.&amp;nbsp;테스트&amp;nbsp;실행으로&amp;nbsp;기능&amp;nbsp;검증&lt;br /&gt;4.&amp;nbsp;모든&amp;nbsp;테스트가&amp;nbsp;통과하면&amp;nbsp;다음&amp;nbsp;기능으로&amp;nbsp;진행&lt;/p&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번의 순서는 바뀔 수 있으며 1번을 먼저하는 방식을 흔히 TDD라고 불립니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히&amp;nbsp;리팩토링&amp;nbsp;과정에서&amp;nbsp;테스트&amp;nbsp;코드의&amp;nbsp;가치가&amp;nbsp;확실히&amp;nbsp;드러났습니다.&amp;nbsp;코드&amp;nbsp;구조를&amp;nbsp;개선하거나&amp;nbsp;성능을&amp;nbsp;최적화할&amp;nbsp;때,&amp;nbsp;모든&amp;nbsp;테스트가&amp;nbsp;통과한다면&amp;nbsp;기존&amp;nbsp;기능을&amp;nbsp;깨뜨리지&amp;nbsp;않았다는&amp;nbsp;자신감을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;가장&amp;nbsp;인상적이었던&amp;nbsp;경험은&amp;nbsp;주가&amp;nbsp;데이터를&amp;nbsp;조회하는&amp;nbsp;서비스에서&amp;nbsp;발생했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장 전 / 장 중 / 장 종료 등 다양한 상황에 따라 다른 방식으로 데이터를 가져오는 조건부 로직이 있었습니다. 이를 리팩토링할 때, 각 시나리오에 대한 테스트 코드가 있었기 때문에 자신감 있게 코드를 수정할 수 있었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드 (BDD 스타일의 테스트 케이스 예시 (장 중 주가 데이터 조회)&lt;/p&gt;
&lt;pre id=&quot;code_1749274723589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;장 중에는 실시간 주가 데이터를 조회한다&quot;)
void getRealTimePriceWhenMarketIsOpen() {
    // Given
    given(marketStatusChecker.isMarketOpen()).willReturn(true);
    given(stockPriceRepository.findRealTimePrice(STOCK_CODE))
        .willReturn(Optional.of(new StockPrice(STOCK_CODE, 50000, LocalDateTime.now())));
    
    // When
    StockPriceDto result = stockPriceService.getCurrentPrice(STOCK_CODE);
    
    // Then
    assertThat(result.isRealTimePrice()).isTrue();
    assertThat(result.getPrice()).isEqualTo(50000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;덕분에&amp;nbsp;복잡한&amp;nbsp;조건부&amp;nbsp;로직을&amp;nbsp;전략&amp;nbsp;패턴으로&amp;nbsp;리팩터링 하는&amp;nbsp;과정이&amp;nbsp;훨씬&amp;nbsp;수월했고,&amp;nbsp;모든&amp;nbsp;시나리오가&amp;nbsp;여전히&amp;nbsp;정상&amp;nbsp;작동한다는&amp;nbsp;확신을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 코드로 인한 버그 조기 발견&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&amp;nbsp;코드를&amp;nbsp;작성하면서&amp;nbsp;가장&amp;nbsp;놀라웠던&amp;nbsp;것은&amp;nbsp;기능&amp;nbsp;개발&amp;nbsp;중에&amp;nbsp;미처&amp;nbsp;생각하지&amp;nbsp;못했던&amp;nbsp;엣지&amp;nbsp;케이스나&amp;nbsp;버그를&amp;nbsp;사전에&amp;nbsp;발견할&amp;nbsp;수&amp;nbsp;있었다는&amp;nbsp;점입니다.&amp;nbsp;특히&amp;nbsp;&quot;주말에&amp;nbsp;조회&amp;nbsp;시&amp;nbsp;직전&amp;nbsp;거래일의&amp;nbsp;종가를&amp;nbsp;보여준다&quot;는&amp;nbsp;요구사항을&amp;nbsp;구현할&amp;nbsp;때,&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;작성하며&amp;nbsp;&quot;공휴일이&amp;nbsp;연속될&amp;nbsp;경우&quot;에&amp;nbsp;대한&amp;nbsp;처리가&amp;nbsp;빠져있다는&amp;nbsp;것을&amp;nbsp;발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&amp;nbsp;코드가&amp;nbsp;없었다면&amp;nbsp;이런&amp;nbsp;케이스는&amp;nbsp;프로덕션&amp;nbsp;환경에서&amp;nbsp;발견되었을&amp;nbsp;테고,&amp;nbsp;그만큼&amp;nbsp;대응&amp;nbsp;비용이&amp;nbsp;커졌을&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;결론적으로,&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;필수적인&amp;nbsp;'투자'였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>성장이야기</category>
      <category>BDD</category>
      <category>JUnit</category>
      <category>mockito</category>
      <category>TDD</category>
      <category>단위 테스트</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/358</guid>
      <comments>https://seungjjun.tistory.com/358#entry358comment</comments>
      <pubDate>Fri, 6 Jun 2025 00:48:19 +0900</pubDate>
    </item>
    <item>
      <title>Redis 캐싱했는데도 느렸던 이유, RTT가 숨은 범인이었다</title>
      <link>https://seungjjun.tistory.com/357</link>
      <description>&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;TL;DR&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;거래대금 상위 50개 기업 조회 API가 임시 테이블 정렬로 인해 10초 이상 걸리자 redis에 캐싱해 3초까지 줄였지만 실시간 주가 50회 개별 조회가 RTT 70 ms씩 추가돼 병목이 남았다. redis 파이프라인을 활용한 벌크 조회로 redis 요청을 50&amp;rarr;1회로 줄여 최종 응답을 200 ms 이하로 단축했고, 병목은 항상 &amp;lsquo;가장 느린 부분&amp;rsquo;을 찾아야 한다.&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;신규&amp;nbsp;서비스를&amp;nbsp;준비하며&amp;nbsp;신규&amp;nbsp;API를&amp;nbsp;개발하는&amp;nbsp;일이&amp;nbsp;많았는데,&amp;nbsp;그중&amp;nbsp;API&amp;nbsp;성능을&amp;nbsp;높이기&amp;nbsp;위해&amp;nbsp;Redis&amp;nbsp;캐시를&amp;nbsp;도입하여&amp;nbsp;성능&amp;nbsp;개선을&amp;nbsp;시도했으나&amp;nbsp;기대한&amp;nbsp;만큼&amp;nbsp;개선&amp;nbsp;효과를&amp;nbsp;보지&amp;nbsp;못한 일이&amp;nbsp;발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번&amp;nbsp;글에서는&amp;nbsp;해당&amp;nbsp;성능&amp;nbsp;이슈의&amp;nbsp;원인을&amp;nbsp;알아보고,&amp;nbsp;병목&amp;nbsp;지점을&amp;nbsp;분석하여&amp;nbsp;API&amp;nbsp;성능을&amp;nbsp;개선한&amp;nbsp;해결방안을&amp;nbsp;정리해보려고&amp;nbsp;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정&amp;nbsp;기간의&amp;nbsp;거래대금&amp;nbsp;상위&amp;nbsp;50개&amp;nbsp;기업을&amp;nbsp;조회하는&amp;nbsp;API의&amp;nbsp;응답&amp;nbsp;시간이&amp;nbsp;평균&amp;nbsp;10초&amp;nbsp;이상으로&amp;nbsp;매우&amp;nbsp;느린&amp;nbsp;문제가&amp;nbsp;있었습니다.&amp;nbsp;이는&amp;nbsp;당연히&amp;nbsp;서비스를&amp;nbsp;이용하는데&amp;nbsp;매우&amp;nbsp;큰&amp;nbsp;영향을&amp;nbsp;미치는&amp;nbsp;문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/h3&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;처음에는&amp;nbsp;조회&amp;nbsp;쿼리가&amp;nbsp;문제가&amp;nbsp;있을것이라&amp;nbsp;판단하고&amp;nbsp;거래대금&amp;nbsp;상위&amp;nbsp;50개&amp;nbsp;기업을&amp;nbsp;조회하는&amp;nbsp;쿼리의&amp;nbsp;실행계획을&amp;nbsp;분석하였습니다.&lt;br /&gt;특정 기간 동안 각 기업의 거래대금을 합산한 후, 거래대금 순으로 정렬하는 과정에서 성능 병목이 발생했는데 이 과정에서 데이터가 기업코드 기준으로 GROUP BY 된 후 임시 파일(Temporary table)에 기록되어 정렬되기 때문에, 인덱스를 활용하기 어렵다는 점이 원인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746252383777&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;거래대금을 기간별로 GROUP BY &amp;rarr; SUM() &amp;rarr; ORDER BY &amp;rarr; LIMIT 을 수행하는 쿼리에서 정렬 비용이 컸습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_임시.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bc70u/btsNJ4OgkqD/8XqLgVDVKzWdODtgP90Rp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bc70u/btsNJ4OgkqD/8XqLgVDVKzWdODtgP90Rp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bc70u/btsNJ4OgkqD/8XqLgVDVKzWdODtgP90Rp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBc70u%2FbtsNJ4OgkqD%2F8XqLgVDVKzWdODtgP90Rp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;293&quot; data-filename=&quot;edited_임시.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;798&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;사진을&amp;nbsp;보면&amp;nbsp;정렬이&amp;nbsp;임시&amp;nbsp;디스크를&amp;nbsp;사용하기&amp;nbsp;때문에&amp;nbsp;인덱스르&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;없어,&amp;nbsp;캐시를&amp;nbsp;적용해야겠다고&amp;nbsp;판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;redis 캐시 적용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;Redis에&amp;nbsp;캐싱하여&amp;nbsp;DB&amp;nbsp;부하&amp;nbsp;감소&amp;nbsp;및&amp;nbsp;API&amp;nbsp;조회&amp;nbsp;속도&amp;nbsp;개선을&amp;nbsp;진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 설계는 다음과 같이 진행했습니다&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경 &amp;rarr; 서비스 &amp;rarr; 버전 &amp;rarr; 메소드 파라미터 순으로, 변경 가능성이 낮은 순서로 키를 구성했습니다.&lt;/li&gt;
&lt;li&gt;예시: api-local:cache:top-trade-value::V1_KOSPI_1Y&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캐시 업데이트 전략&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거래대금 순위 데이터는 장 마감 후에 업데이트가 필요하므로, 거래 데이터를 담고 있는 테이블 업데이트 시점에 캐시도 갱신하도록 설정했습니다.&lt;/li&gt;
&lt;li&gt;특히,&amp;nbsp;당일&amp;nbsp;주가&amp;nbsp;데이터&amp;nbsp;업데이트가&amp;nbsp;완료된&amp;nbsp;이후에만&amp;nbsp;캐시가&amp;nbsp;업데이트되도록,&amp;nbsp;최근&amp;nbsp;거래&amp;nbsp;날짜와&amp;nbsp;현재&amp;nbsp;날짜를&amp;nbsp;비교하는&amp;nbsp;조건을&amp;nbsp;추가했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로, API 조회 시점에는 @Cacheable 어노테이션을 사용해 Redis에서 캐시 데이터를 먼저 조회하고, 주기적 갱신에는 @CachePut을 이용하여 데이터를 최신으로 유지했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;추가 병목 지점: 네트워크 왕복이 진짜 병목이었다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 캐시를 적용한 후에도 API 응답 시간이 여전히 평균 &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;b&gt;실시간 주가&lt;/b&gt;를 Redis에서 조회하는 부분에서 병목이 발생하는 것을 발견했습니다.&lt;br /&gt;장&amp;nbsp;중에&amp;nbsp;거래대금&amp;nbsp;상위&amp;nbsp;50개&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;redis에&amp;nbsp;접근하여&amp;nbsp;값을&amp;nbsp;조회해야&amp;nbsp;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;부분에서&amp;nbsp;병목이&amp;nbsp;발생하는&amp;nbsp;것을&amp;nbsp;발견했습니다.&lt;/p&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가 느렸을까?&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. 거래대금 상위 50개 기업의 실시간 주가를 Redis에서 개별적으로 조회하는 방식으로 구현되어 있었습니다. (50개 키를 순차적으로 GET)&lt;br /&gt;2. Redis에서 개별 조회 시 한 번 요청당 약 &lt;b&gt;70ms&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;50 &amp;times; 70ms = 3,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;size16&quot;&gt;일반적으로&amp;nbsp;redis는&amp;nbsp;단일&amp;nbsp;키&amp;nbsp;조회할&amp;nbsp;때&amp;nbsp;1ms&amp;nbsp;미만의&amp;nbsp;속도로&amp;nbsp;조회해오는데,&amp;nbsp;현재&amp;nbsp;한 개의&amp;nbsp;기업의&amp;nbsp;실시간&amp;nbsp;주가를&amp;nbsp;조회해 올&amp;nbsp;때&amp;nbsp;평균적으로&amp;nbsp;70ms&amp;nbsp;가&amp;nbsp;소요되는&amp;nbsp;문제가&amp;nbsp;있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;문제를&amp;nbsp;해결하면&amp;nbsp;개별&amp;nbsp;조회하여&amp;nbsp;50번&amp;nbsp;조회하여도&amp;nbsp;50ms&amp;nbsp;이기&amp;nbsp;때문에&amp;nbsp;1번은&amp;nbsp;큰&amp;nbsp;문제가&amp;nbsp;되지&amp;nbsp;않습니다.&lt;br /&gt;하지만&amp;nbsp;redis&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;10kb&amp;nbsp;이하)&lt;br /&gt;&lt;br /&gt;Redis&amp;nbsp;지연&amp;nbsp;현상을&amp;nbsp;분석하기&amp;nbsp;위해,&amp;nbsp;Redis&amp;nbsp;네트워크&amp;nbsp;응답&amp;nbsp;시간을&amp;nbsp;측정했습니다&lt;/p&gt;
&lt;pre id=&quot;code_1746252691742&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redis-cli --latency
min: 70ms, max: 102ms, avg: 70ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청-응답&amp;nbsp;간의&amp;nbsp;네트워크&amp;nbsp;왕복&amp;nbsp;(RTT)이&amp;nbsp;큰&amp;nbsp;병목&amp;nbsp;요인이라는&amp;nbsp;사실을&amp;nbsp;알게&amp;nbsp;되었습니다.&amp;nbsp;50번의&amp;nbsp;개별&amp;nbsp;요청이&amp;nbsp;이루어지며&amp;nbsp;매번&amp;nbsp;RTT가&amp;nbsp;발생했기&amp;nbsp;때문에&amp;nbsp;병목이&amp;nbsp;심화되었습니다.&lt;br /&gt;&lt;br /&gt;이를 해결하기 위해, 개별 조회 방식에서 Redis의 벌크 조회 방식(파이프라인 + HGETALL)으로 변경하여 &lt;b&gt;한 번의 요청으로 50개의 기업 실시간 주가 데이터를 조회하도록 개선&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러한 변경을 통해 Redis에 대한 네트워크 요청 횟수를 &lt;b&gt;50회에서 1회로 줄였고&lt;/b&gt;, 결과적으로 Redis 조회 성능을 개선할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis 벌크 조회 방식을 적용한 이후, API의 평균 응답 시간이 기존의 &lt;b&gt;10초 이상 &amp;rarr; 3초 &amp;rarr; 200ms 이하&lt;/b&gt;로 단계적으로 감소하여, 사용자 경험을 크게 개선할 수 있었습니다.&lt;br /&gt;&lt;br /&gt;이번&amp;nbsp;트러블&amp;nbsp;슈팅을&amp;nbsp;통해&amp;nbsp;성능&amp;nbsp;개선을&amp;nbsp;위해서는&amp;nbsp;단순히&amp;nbsp;캐시&amp;nbsp;적용뿐&amp;nbsp;아니라,&amp;nbsp;네트워크&amp;nbsp;비용도&amp;nbsp;고려하여&amp;nbsp;개선해야&amp;nbsp;한다는&amp;nbsp;점을&amp;nbsp;배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>redis</category>
      <category>Redis cache</category>
      <category>redis 캐시</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/357</guid>
      <comments>https://seungjjun.tistory.com/357#entry357comment</comments>
      <pubDate>Tue, 22 Apr 2025 23:34:29 +0900</pubDate>
    </item>
    <item>
      <title>아무리 스레드를 늘려도 성능 개선에 소용없던 이유 (MySQL CPU 사용률 99%)</title>
      <link>https://seungjjun.tistory.com/356</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TL;DR&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;단일 스레드에서 멀티스레드로 전환했음에도 배치 성능이 개선되지 못했던 이유&lt;br /&gt;MySQL CPU 사용률 99%가 병목의 원인이었으며, 이는 비즈니스 요구사항 때문에 불가피했던 LIKE 패턴 매칭 쿼리 때문이었습니다. 문제 해결을 위해 LIKE 쿼리를 IN 절로 변환하는 작업을 진행했고, 그 결과 CPU 사용률은 20% 이하로 떨어지고 배치 처리 시간은 10분에서 단 25초로 단축되어 사용자들이 최신 데이터에 즉시 접근할 수 있게 되었습니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&quot;배치&amp;nbsp;성능&amp;nbsp;개선?&amp;nbsp;그거&amp;nbsp;멀티스레드&amp;nbsp;쓰면&amp;nbsp;해결되는&amp;nbsp;거&amp;nbsp;아니었어?&quot;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 많은 개발자분들이 저와 같은 생각을 해보셨을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저는&amp;nbsp;사용자에게&amp;nbsp;최신&amp;nbsp;데이터를&amp;nbsp;제공하는&amp;nbsp;신규&amp;nbsp;기능을&amp;nbsp;개발하고&amp;nbsp;있었습니다.&amp;nbsp;&lt;br /&gt;이&amp;nbsp;신규&amp;nbsp;기능의&amp;nbsp;핵심은&amp;nbsp;사용자가&amp;nbsp;원하는&amp;nbsp;특정&amp;nbsp;기준으로&amp;nbsp;데이터를&amp;nbsp;커스텀하게&amp;nbsp;그룹화하고&amp;nbsp;집계하여&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;하는&amp;nbsp;것이었습니다.&lt;br /&gt;매월&amp;nbsp;한&amp;nbsp;번&amp;nbsp;업데이트되는&amp;nbsp;원본&amp;nbsp;데이터가&amp;nbsp;들어오면,&amp;nbsp;사용자들이&amp;nbsp;직접&amp;nbsp;만든&amp;nbsp;커스텀&amp;nbsp;그룹&amp;nbsp;데이터도&amp;nbsp;자동으로&amp;nbsp;업데이트되어야&amp;nbsp;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이 기능을 개발할 때는 단일 스레드로 시작했습니다. 하지만 곧 문제가 발생했습니다.&lt;br /&gt;원본 데이터가 업데이트되어도, 사용자들이 최신 커스텀 그룹 데이터를 확인하기까지는 무려 &lt;b&gt;10분&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;이라고 생각했습니다.&amp;nbsp;&lt;br /&gt;&quot;스레드 수를 늘리면, 동시에 작업할 수 있는 작업자가 많아지니 성능은 비례해서 빨라지겠지?&quot;라는 기대를 품고 10개의 스레드를 20개로 늘려보고, 커넥션 풀도 그에 맞춰 확장했습니다.&lt;br /&gt;하지만 결과는 저의 예상과 다르게 &lt;b&gt;10개 스레드일 때와 20개 스레드일 때 전혀 차이가 없었습니다.&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가 이미 &lt;b&gt;처리량이 한계치&lt;/b&gt;에 도달했거나, &lt;b&gt;네트워크, CPU, 디스크 I/O 같은 다른 자원에서 병목 현상&lt;/b&gt;이 발생하고 있다고 생각하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;멀티스레드의 발목을 잡던 진범을 찾아서&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레드를&amp;nbsp;적용했음에도&amp;nbsp;성능이&amp;nbsp;개선되지&amp;nbsp;않자,&amp;nbsp;저는&amp;nbsp;곧바로&amp;nbsp;병목&amp;nbsp;지점을&amp;nbsp;찾기&amp;nbsp;위한&amp;nbsp;분석에&amp;nbsp;돌입했습니다.&amp;nbsp;&lt;br /&gt;가장 먼저, 업데이트된 원본 수출 데이터를 'SELECT'해오는 부분에서 문제가 발생하는지, 아니면 최신 데이터를 사용자 커스텀 그룹에 'INSERT'하는 지점에서 문제가 발생하는지를 파악했습니다.&lt;br /&gt;그리고 병목 현상이 최신 데이터를 'SELECT' 해오는 지점에서 발생하고 있음을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 'SELECT' 쿼리의 실행 계획을 분석했을 때, 모든 조건에서 &lt;b&gt;인덱스를 잘 활용&lt;/b&gt;하고 있는 것으로 나타났습니다.&amp;nbsp;&lt;br /&gt;&quot;인덱스를 잘 쓰고 있는데 왜 느리지?&quot; 라는 의문이 들었습니다. 추가적으로 &lt;b&gt;Read IOPS&lt;/b&gt;를 확인했을 때도 낮은 수치로 측정되는 것을 보아, &lt;b&gt;디스크 I/O가 병목의 원인이 아닐 것&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;1414&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wYSxl/btsOseP7Rz3/CfEsoEkDanomh7g0ohMUx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wYSxl/btsOseP7Rz3/CfEsoEkDanomh7g0ohMUx1/img.png&quot; data-alt=&quot;ReadIOPS 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wYSxl/btsOseP7Rz3/CfEsoEkDanomh7g0ohMUx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwYSxl%2FbtsOseP7Rz3%2FCfEsoEkDanomh7g0ohMUx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;606&quot; height=&quot;177&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ReadIOPS 지표&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;하지만, CPU 사용률을 확인했을 때 사용률이 99%까지 치솟아 있는 것을 발견했습니다.&amp;nbsp;&lt;br /&gt;쿼리 성능 저하의 원인이 디스크 I/O 같은 비용이 아니라, &lt;b&gt;높은 연산 비용으로 인한 CPU 사용량&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;1418&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0556S/btsOrUYEuc4/B20X5miuNCryRzDxMTjHN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0556S/btsOrUYEuc4/B20X5miuNCryRzDxMTjHN0/img.png&quot; data-alt=&quot;CPU 사용률 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0556S/btsOrUYEuc4/B20X5miuNCryRzDxMTjHN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0556S%2FbtsOrUYEuc4%2FB20X5miuNCryRzDxMTjHN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;167&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;382&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;어떤&amp;nbsp;연산이&amp;nbsp;이토록&amp;nbsp;CPU를&amp;nbsp;잡아먹고&amp;nbsp;있을까요?&amp;nbsp;&lt;br /&gt;저는 'SELECT' 쿼리 내에서 연산 비용이 높을 것으로 추정되는 부분을 집중적으로 살펴보았습니다.&amp;nbsp; 그리고 마침내 'LIKE' 패턴 매칭 구문을 발견했습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;특히 'hscode LIKE CONCAT(hscode, '%')'와 같은 형태의 조건이 문제였습니다. 이 조건은 단순한 문자열 비교가 아닙니다. 각 행마다 'CONCAT' 함수를 통해 문자열을 결합해야 하고, 그 결과 문자열에 대해 다시 패턴 매칭을 수행해야 하므로, CPU 연산 비용이 높을 수밖에 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;바로 이 'LIKE' 패턴 매칭이 CPU를 99%까지 끌어올리며 멀티스레드의 발목을 잡고 있던 진범이었던 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기존 쿼리에서 `LIKE`를 사용할 수밖에 없었던 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 기능의 비즈니스 요구사항 때문에 'LIKE' 패턴 매칭이 &lt;b&gt;가장 직관적이고 유일한 해결책&lt;/b&gt;처럼 보였습니다.&lt;br /&gt;&lt;br /&gt;가장 큰 이유는 바로 &lt;b&gt;계층적 상품 분류 체계인 HS 코드&lt;/b&gt;에 있었습니다. HS 코드는 국제적으로 상품을 분류하는 코드로, 앞 4자리는 큰 상위 카테고리를, 8자리는 아주 세부적인 상품을 나타냅니다. 예를 들어, '5201'은 면화 전체를, '52010100'은 특정 면화 품목을 의미합니다.&lt;br /&gt;&lt;br /&gt;여기에 사용자들의 &lt;b&gt;유연한 데이터 등록 방식&lt;/b&gt;이라는 요구사항이 더해졌습니다. 사용자들은 관심 있는 상품을 등록할 때, '5201(면화 전체)'처럼 4자리만 입력할 수도 있고, '52010100(면화 특정 품목)'처럼 8자리 전체를 입력할 수도 있어야 했습니다. 이처럼 코드 길이가 &lt;b&gt;가변적&lt;/b&gt;이었기 때문에 정확히 일치하는 비교만으로는 데이터를 찾아낼 수 없었습니다.&lt;br /&gt;&lt;br /&gt;만약 어떤 사용자가 4자리 HS 코드 '5201'을 등록했다면, 시스템은 '5201'로 시작하는 '52010100', '52010200' 등 모든 하위 품목의 수출 데이터를 합산해서 보여줘야 했습니다. 이는 곧 &lt;b&gt;'PREFIX' 기반의 검색&lt;/b&gt;이 필수적이라는 의미였습니다.&lt;br /&gt;&lt;br /&gt;이러한 상황에서, `LIKE '5201%'`와 같은 패턴은 해당 HS 코드로 시작하는 모든 하위 코드를 직관적으로 검색하는 &lt;b&gt;가장 명확하고 단순한 방법&lt;/b&gt;이었습니다. 비록 나중에 CPU 병목의 주범으로 밝혀졌지만, 당시에는 비즈니스 로직을 가장 쉽게 구현할 수 있는 불가피한 선택이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;LIKE 패턴 매칭을 IN으로 바꿔 &lt;/b&gt;CPU 99% &amp;rarr; 20% 으로 사용률 감소&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 99% 병목의 주범이 LIKE 패턴 매칭이라는 것을 파악한 후, 저는 이를 어떻게 제거할 수 있을지에 대한 방법을 고민하기 시작했습니다. 핵심은 &lt;b&gt;최대한 연산을 줄이고 단순 문자열 비교를 활용하는 것&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 답은 &lt;b&gt;IN 절&lt;/b&gt;에 있다고 판단했습니다. IN 절은 특정 값들의 리스트 내에 데이터가 포함되는지 여부만 확인하므로, LIKE보다 훨씬 효율적인 비교 연산이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;51:1-51:122&quot; data-ke-size=&quot;size16&quot;&gt;문제는 IN 절을 사용하려면 &lt;b&gt;원본 상품의 8자리 HS 코드 전체가 필요하다는 것&lt;/b&gt;이었습니다. 사용자가 4자리 HS 코드를 선택했더라도, 실제 비교 대상은 그 하위에 속하는 모든 8자리 HS 코드여야 했습니다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;53:1-53:22&quot; data-ke-size=&quot;size16&quot;&gt;저는 다음과 같은 해결책을 구상했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-sourcepos=&quot;55:1-57:0&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-sourcepos=&quot;55:1-55:335&quot;&gt;&lt;b&gt;사용자 커스텀 그룹별 HS 코드 리스트 사전 확보&lt;/b&gt;: 원본 수출 데이터를 가져오기 전에, 먼저 사용자가 생성한 커스텀 그룹과 HS 코드가 매핑된 테이블에서 &lt;b&gt;각 그룹에 해당하는 모든 8자리 HS 코드 리스트를 미리 가져오는&lt;/b&gt; 작업을 수행했습니다. 이는 LIKE 패턴 매칭을 통해 이루어졌지만, 기존과는 큰 차이점이 있었습니다. 이전에는 원본 데이터 전체에 대해 LIKE 연산을 수행했지만, 이제는 &lt;b&gt;업데이트된 최신 데이터만 필터링한 후,&lt;/b&gt; 그 데이터 범위 내에서만 LIKE 패턴 매칭을 수행했습니다.&amp;nbsp;&lt;/li&gt;
&lt;li data-sourcepos=&quot;56:1-57:0&quot;&gt;&lt;b&gt;IN 절을 활용한 멀티스레드 처리&lt;/b&gt;: 각 커스텀 그룹별로 확보된 8자리 HS 코드 리스트는 이제 IN 절의 인자로 활용될 수 있었습니다. 이제 멀티스레드 환경에서 각 스레드가 특정 커스텀 그룹에 할당된 HS 코드 리스트를 가지고, 업데이트된 원본 데이터로부터 해당 HS 코드를 IN 절로 효율적으로 SELECT 해 올 수 있게 되었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-sourcepos=&quot;58:1-58:28&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 형태로 구현되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749282282210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 핵심 로직 개념 (실제 구현은 더 복잡할 수 있습니다)
// 1. 최신 거래일자 데이터 조회
LocalDate tradeDate = tradeMapper.selectLatestDataDate();

// 2. 사용자가 커스텀하게 정의한 모든 HS 코드와 그룹 매핑 정보 조회
// 이 과정에서 'LIKE 패턴 매칭'이 사용되지만, 대상 데이터가 훨씬 적고 (매핑 테이블),
// 이후 원본 데이터 조회 시에는 IN 절을 사용할 수 있도록 데이터를 미리 준비하는 역할을 합니다.
List&amp;lt;TradeGroupHscode&amp;gt; hscodes = tradeMapper.selectAllHscodesByTradeDate(tradeDate);
Map&amp;lt;Integer, List&amp;lt;String&amp;gt;&amp;gt; hscodeMap = hscodes.stream()
    .collect(Collectors.groupingBy(
        TradeGroupHscode::getGroupId, // 커스텀 그룹 ID
        Collectors.mapping(TradeGroupHscode::getHscode, Collectors.toList()) // 해당 그룹의 모든 8자리 HS 코드 리스트
    ));

// 3. 멀티스레드 환경에서 각 그룹별로 데이터 업데이트 실행
ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUMBER); // 20개 스레드 풀 (환경별로 개수는 다르게 설정)
List&amp;lt;CompletableFuture&amp;lt;Void&amp;gt;&amp;gt; futures = hscodeMap.keySet().stream().map(groupId -&amp;gt;
    CompletableFuture.runAsync(() -&amp;gt; {
        // 각 스레드에서 특정 커스텀 그룹 ID와 해당 그룹의 HS 코드 리스트를 가지고
        // 원본 데이터에서 IN 절 쿼리를 통해 효율적으로 데이터를 가져와 업데이트 수행
        customTradeUpdateService.getCustomDataByTradeDateAndGroupIdAndHscode(
            tradeDate, groupId, hscodeMap.get(groupId)
        );
    }, executor)
).toList();
// 모든 스레드 작업 완료 대기 로직 추가&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;이처럼 LIKE 패턴 매칭을 IN 절로 변환하고 멀티스레드를 활용하니 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;MySQL의 CPU 사용률은 &lt;/span&gt;&lt;b&gt;99%에서 20% 이하&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;/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;1416&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doZ1nX/btsOru0iIpD/4KjXSHC4HcQqrewDQ1ldmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doZ1nX/btsOru0iIpD/4KjXSHC4HcQqrewDQ1ldmk/img.png&quot; data-alt=&quot;개선 이후 CPU 사용률&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doZ1nX/btsOru0iIpD/4KjXSHC4HcQqrewDQ1ldmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoZ1nX%2FbtsOru0iIpD%2F4KjXSHC4HcQqrewDQ1ldmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;177&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 이후 CPU 사용률&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-sourcepos=&quot;95:1-95:111&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;95:1-95:111&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;95:1-95:111&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;배치 성능 10분 &amp;rarr; 25초, 사용자 경험 개선!&lt;/b&gt;&lt;/h3&gt;
&lt;p data-sourcepos=&quot;97:1-97:314&quot; 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;이러한 개선의 결과는 배치 처리 시간의 대폭 단축으로 이어졌습니다. 기존에 무려 &lt;/span&gt;&lt;b&gt;10분&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;b&gt;25초&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;/p&gt;
&lt;p data-sourcepos=&quot;97:1-97:314&quot; 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;최신 수출 데이터가 업데이트되면 이제는 빠르게 자신이 설정한 커스텀 그룹의 최신 데이터를 확인할&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;/p&gt;
&lt;p data-sourcepos=&quot;97:1-97:314&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;이 일련의 문제 해결 과정을 통해 저는 단순히 멀티스레드나 scale-up 으로 성능 개선이 무조건 이루어진다는 게 아니라는 점을 깨달았습니다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;성능 개선을 위해서는 시스템의 &lt;b&gt;병목 지점을 정확히 파악하고, 그 근본 원인을 해결해야 한다는&lt;/b&gt; 것을 깨닫게 되었습니다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;특히, 애플리케이션의 성능 병목을 진단할 때 &lt;b&gt;CPU, 디스크 I/O, 네트워크 등 다양한 지표를 단계적으로 확인하고 추론&lt;/b&gt;하는 능력을 기를 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 탐색 원리&lt;/b&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;101:1-101:324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://seungjjun.tistory.com/355&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://seungjjun.tistory.com/355&lt;/a&gt;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>like</category>
      <category>mysql cpu</category>
      <category>readiops</category>
      <category>멀티스레드</category>
      <category>패턴 매칭</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/356</guid>
      <comments>https://seungjjun.tistory.com/356#entry356comment</comments>
      <pubDate>Fri, 4 Apr 2025 18:12:13 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 인덱스 탐색 원리와 LIKE 검색의 인덱스 활용</title>
      <link>https://seungjjun.tistory.com/355</link>
      <description>&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;MySQL 인덱스 탐색 원리와 LIKE 검색의 인덱스 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 개발자라면 누구나 &amp;ldquo;인덱스를 걸면 검색속도가 빨라진다&amp;rdquo;는 사실은 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &amp;lsquo;왜 빨라지는지?&amp;rsquo; 그리고 특정 상황에서는 인덱스를 걸어도 속도가 빨라지지 않는데, 그 이유를 알기 위해는 인덱스가 어떤 구조로 데이터를 저장하고 탐색하는지 그 원리를 이해해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스가 없다면?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 인덱스가 없는 테이블에서 특정 값을 찾으려면 어떻게 해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무식하게 테이블의 모든 값을 처음부터 읽으면서 찾으려는 값과 맞는지 비교하면서 찾는 방법밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 &lt;b&gt;Full Table Scan&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;예를 들어 100만건 이상의 데이터가 저장되어 있는 유저 테이블에 &lt;b&gt;username = &amp;lsquo;seungjun&amp;rsquo;&lt;/b&gt; 인 행을 찾는다고 해보자. &lt;b&gt;Full Table Scan&lt;/b&gt; 으로 찾는 경우 최악의 경우 100만건 모두를 읽어야 하는 상황이 발생한다.&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;이러한 문제를 해결하기 위해 인덱스가 존재한다. 인덱스는 원본 데이터와 별도로, 검색에 최적화된 자료구조에 데이터를 정렬하여 저장한다. 이를 통해 Full Table Scan 없이도 원하는 데이터를 빠르게 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스의 자료구조 B+Tree&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL InnoDB는 인덱스 구현에 &lt;span data-token-index=&quot;1&quot;&gt;B+Tree&lt;/span&gt; 자료구조를 사용한다. B+Tree를 이해하는 것이 인덱스 동작 원리를 이해하는것의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree는 다음과 같은 특징을 가진 균형 트리이다.&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;649&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z1ysL/dJMcafE6ou9/STWLkE8yVyVwlQru5RszV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z1ysL/dJMcafE6ou9/STWLkE8yVyVwlQru5RszV0/img.png&quot; data-alt=&quot;https://blog.jcole.us/2013/01/10/btree-index-structures-in-innodb/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z1ysL/dJMcafE6ou9/STWLkE8yVyVwlQru5RszV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz1ysL%2FdJMcafE6ou9%2FSTWLkE8yVyVwlQru5RszV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;436&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://blog.jcole.us/2013/01/10/btree-index-structures-in-innodb/&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;/b&gt; 루트 노드와 내부 노드는 탐색을 위한 키 값만 가지고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리프 노드들은 서로 연결되어 있다(Linked List).&lt;/b&gt; 이 특성으로 Range Scan을 효율적으로 만든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 키는 정렬된 상태로 유지된다.&lt;/b&gt; 이것이 빠른 검색의 핵심이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트리의 모든 리프 노드는 같은 깊이에 있다.&lt;/b&gt; 어떤 데이터를 찾든 동일한 횟수의 노드 접근이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree의 가장 큰 특징은 &quot;모든 데이터가 정렬된 상태로 저장된다&quot;는 점이다.&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;B+Tree는 책의 목차와 같은 원리로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영영사전에서 Apple을 찾는다고 해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;맨 앞이 A인 페이지를 찾는다.&lt;/li&gt;
&lt;li&gt;그 페이지에서 p로 시작하는 부분을 찾는다.&lt;/li&gt;
&lt;li&gt;그 다음 p ,l 순서로 좁혀가며 찾는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영영사전이 알파벳 순서로 정렬되어 있기 때문에 이렇게 찾을 수 있고 이와 동일하게 B+Tree도 키로 정렬되어 있기 때문에 동일한 원리로 검색이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Clustered Index와 Secondary Index&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB에서 인덱스는 크게 두 종류로 나뉜다.&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;Clustered Index&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 데이터를 담은 Clustered Index는 테이블당 단 하나만 존재한다. &lt;b&gt;실제 테이블 데이터 자체가 이 인덱스의 리프 노드에 저장된다.&lt;/b&gt; InnoDB에서는 Primary Key가 Clustered Index가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 테이블을 Full Scan 한다는 것은 곧 Clustered Index의 리프 노드를 순차적으로 읽는다는 의미이다.&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;Secondary Index&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary Key가 아닌 다른 컬럼에 생성하는 인덱스가 Secondary Index다. 예를 들어 username 컬럼에 인덱스를 생성하면 이것이 Secondary Index다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secondary Index의 리프 노드에는 &lt;b&gt;인덱스 키 값 + 해당 행의 Primary Key 값&lt;/b&gt;이 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스 탐색 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree 인덱스에서 데이터를 찾는 방식은 크게 세 가지로 분류할 수 있다.&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;span data-token-index=&quot;0&quot;&gt;Index Unique Scan&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary Key나 Unique Index로 단일 값을 검색할 때 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765086163437&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE id = 100;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree를 루트에서 리프까지 내려가며 정확히 하나의 행만 찾기 때문에 가장 효율적인 탐색 방식이다.&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;Index Range Scan&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 범위의 값을 검색할 때 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765086189413&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE username &amp;gt;= 'kim' AND username &amp;lt; 'lee';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 과정&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;B+Tree에서 범위의 시작점('kim')을 찾아 리프 노드까지 내려간다.&lt;/li&gt;
&lt;li&gt;리프 노드에서 조건을 만족하는 동안 &lt;b&gt;연결된 다음 리프 노드로 순차 이동&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;조건을 벗어나면('lee' 찾음) 검색 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree의 리프 노드가 연결 리스트로 이어져 있기 때문에 범위 탐색이 효율적이다.&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;Full Index Scan&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 전체 리프 노드를 읽는 경우다. Full Table Scan보다는 효율적일 수 있지만, 여전히 비효율적인 접근이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;LIKE 검색의 인덱스 활용 분석&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+Tree의 구조와 탐색 원리를 이해했으니, LIKE 검색에서 와일드카드 위치에 따라 인덱스 활용이 어떻게 달라지는지 이해할 수 있다.&lt;/p&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;username&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;[abraham] &amp;rarr; [anakim] &amp;rarr; [bob] &amp;rarr; [charlie] &amp;rarr; [kim] &amp;rarr; [kimchi] &amp;rarr; [park] &amp;rarr; [takim]&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;후위 와일드카드(전위 일치) LIKE 'kim%'&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1765086264238&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE username LIKE 'kim%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 'kim'으로 &lt;span data-token-index=&quot;1&quot;&gt;시작하는&lt;/span&gt; 모든 값을 찾기 때문에 인덱스를 &lt;span data-token-index=&quot;3&quot;&gt;완벽하게 활용&lt;/span&gt;한다.(Index Range Scan)&lt;/p&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;B+Tree는 문자열을 왼쪽부터 정렬한다.&lt;/li&gt;
&lt;li&gt;kim 로 시작하는 첫 번째 레코드는 정렬된 리스트의 특정 위치에 모여 있음이 보장된다.&lt;/li&gt;
&lt;li&gt;kim보다 크거나 같은 첫 번째 위치를 찾아, kim이 아닐 때까지 쭉 읽는다.&lt;/li&gt;
&lt;/ol&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;전위 와일드카드(후위 일치) LIKE '%kim'&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1765086298764&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE username LIKE '%kim';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 'kim'으로 &lt;span data-token-index=&quot;1&quot;&gt;끝나는&lt;/span&gt; 모든 값을 찾는다. 이 경우 인덱스를 활용할 수 없다. (Full Table Scan 또는 Full Index Scan)&lt;/p&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;b&gt;첫 글자부터&lt;/b&gt; 정렬되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 정렬 순서는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[abraham] &amp;rarr; [anakim] &amp;rarr; [bob] &amp;rarr; [charlie] &amp;rarr; [kim] &amp;rarr; [kimchi] &amp;rarr; [park] &amp;rarr; [takim]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'kim'으로 끝나는 값들을 살펴보자 &lt;b&gt;anakim&lt;/b&gt;, &lt;b&gt;kim&lt;/b&gt;, &lt;b&gt;takim&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값들은 인덱스에서 &lt;b&gt;연속&lt;/b&gt;되어 있지 않다. 첫 글자가 각각 &lt;b&gt;'a', 'k', 't'&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 data-ke-size=&quot;size16&quot;&gt;Index Range Scan은 정렬된 데이터에서 &lt;b&gt;연속된 구간&lt;/b&gt;을 읽는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'kim'으로 끝나는 값들은 인덱스에서 연속된 구간이 없기 때문에, 시작점과 종료점을 알수 없다.&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;b&gt;LIKE '%kim'&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;양쪽 와일드카드(양쪽 일치) LIKE '%kim%'&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1765086392480&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE username LIKE '%kim%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 'kim'을 &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;범위 검색이 가능하려면 &quot;인덱스 키의 prefix가 고정되어야&quot; 한다는 것이 핵심 원리다. &lt;b&gt;LIKE '%kim%'&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;에서는 prefix가 고정되어 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스를 아예 안 쓰는 걸까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후위 일치(또는 양쪽 일치) 검색을 할 때 Full Index Scan으로 검색하는지 Full Table Scan을 하는지는 상황에 따라 다르다.&lt;/p&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. 커버링 인덱스일 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리에서 요구하는 칼럼이 모두 인덱스에 포함되어 있다면, 데이터 파일까지 가지 않고 인덱스만 처음부터 끝까지 읽는다.(&lt;b&gt;Index Full Scan)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;테이블 전체를 읽는 것보단 빠르지만, 여전히 효율적인 &lt;b&gt;Range Scan&lt;/b&gt;은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 일반적인 경우&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;인덱스에 없는 칼럼까지 조회해야 한다면, 옵티마이저는 &quot;어차피 인덱스 다 뒤지고 다시 테이블도 가야 하니, 그냥 테이블을 처음부터 읽자&quot;고 판단하여 &lt;b&gt;Full Table Scan&lt;/b&gt;을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <category>B+Tree</category>
      <category>like</category>
      <category>mysql index</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/355</guid>
      <comments>https://seungjjun.tistory.com/355#entry355comment</comments>
      <pubDate>Sat, 1 Mar 2025 15:19:24 +0900</pubDate>
    </item>
    <item>
      <title>멀티 스레드로 병렬 처리하는것은 항상 성능에 이점이 있을까?</title>
      <link>https://seungjjun.tistory.com/354</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;멀티 스레드로 병렬 처리하는 것은 항상 성능에 이점이 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무를 진행하며 멀티 스레드로 성능 개선을 할 일이 있었는데, &lt;b&gt;&amp;ldquo;멀티 스레드로 병렬 처리하는 것이 항상 성능 향상에 이점이 있을까?&amp;rdquo;&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;br /&gt;저도 처음엔 &amp;ldquo;스레드가 많으면 빠르다, 따라서 스레드 수와 애플리케이션의 성능 속도는 정비례한다&amp;rdquo;고 단순하게 생각했습니다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 꼭 그렇지만은 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 단일 스레드로 동작한다고 알고 있는 Redis는 매우 빠른 처리 속도를 보여주는데, 이는 &amp;ldquo;단일 스레드 = 느리다&amp;rdquo;라는 가설이 항상 성립하는 것은 아니라는 점을 잘 보여줍니다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;556&quot; data-start=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 왜 멀티 스레드가 항상 빠른 것만은 아닌지, 간단한 Java 예제 코드를 통해 살펴보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;멀티 스레드로 병렬 처리&quot;가 항상 빠른 것은 아니다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 멀티 스레드로 병렬 처리가 항상 빠른 것이 아닌지 단순한 Java로 작성한 코드를 보면서 알아보곘습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 핵심은 sharedCounter라는 공유 자원을 doHeavyWork() 메서드에서만 수정하고, 이 메서드를 synchronized로 잠갔다는 점입니다. 그리고 10개의 스레드 풀을 사용하여 100개의 작업을 동시에 실행했을 때와, 단일 스레드에서 순차적으로 100개의 작업을 실행했을 때의 시간을 비교합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740906898736&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MultiThreadPerformanceApplication {

    // 임계 영역
    private int sharedCounter = 0;

    // synchronized 키워드를 사용해 메서드 락 (한번에 1개의 스레드만 접근 가능)
    private synchronized void doHeavyWork() {
        for (int i = 0; i &amp;lt; 1_000_000; i++) {
            sharedCounter++;
        }
    }

    private void runMultiThreadTask() throws InterruptedException {
        long startTime = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 100개의 작업을 멀티 스레드에서 실행
        for (int i = 0; i &amp;lt; 100; i++) {
            executorService.submit(this::doHeavyWork);
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.MINUTES);

        long endTime = System.currentTimeMillis();
        System.out.println(&quot;[Multi-thread] Total time: &quot; + (endTime - startTime) + &quot; ms&quot;);
        System.out.println(&quot;[Multi-thread] Final sharedCounter: &quot; + sharedCounter);
    }

    private void runSingleThreadTask() {
        long startTime = System.currentTimeMillis();

        // 100개의 작업을 단일 스레드(메인 스레드)에서 순차 실행
        for (int i = 0; i &amp;lt; 100; i++) {
            doHeavyWork();
        }

        long endTime = System.currentTimeMillis();
        System.out.println(&quot;[Single-thread] Total time: &quot; + (endTime - startTime) + &quot; ms&quot;);
        System.out.println(&quot;[Single-thread] Final sharedCounter: &quot; + sharedCounter);
    }

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

        System.out.println(&quot;=== Single Thread Test ===&quot;);
        application.runSingleThreadTask();

        // sharedCounter 값이 누적되지 않도록 새 객체 생성
        application = new MultiThreadPerformanceApplication();
        System.out.println(&quot;\n=== Multi Thread Test ===&quot;);
        application.runMultiThreadTask();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실행 결과&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1740907271480&quot; class=&quot;asciidoc&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;=== Single Thread Test ===
[Single-thread] Total time: 10 ms
[Single-thread] Final sharedCounter: 100000000

=== Multi Thread Test ===
[Multi-thread] Total time: 26 ms
[Multi-thread] Final sharedCounter: 100000000&lt;/code&gt;&lt;/pre&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 data-end=&quot;3016&quot; data-start=&quot;2958&quot;&gt;싱글 스레드로 처리했을 때가 약 10ms, 멀티 스레드로 처리했을 때가 약 26ms가 걸렸습니다.&lt;/li&gt;
&lt;li data-end=&quot;3053&quot; data-start=&quot;3017&quot;&gt;결과적으로 싱글 스레드가 약 16ms 정도 더 빨랐습니다.&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;b&gt;1. 락 경쟁 (Lock Contention)&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;synchronized 메서드인 doHeavyWork()는 한 번에 오직 1개의 스레드만 진입할 수 있습니다.&lt;/li&gt;
&lt;li&gt;10개의 스레드가 동시에 실행된다고 해도, 결국 이 메서드를 사용할 때는 차례차례 대기해야 하므로 사실상 싱글 스레드처럼 처리되는 구간이 생깁니다.&lt;/li&gt;
&lt;li&gt;스레드가 늘어날수록 락을 얻기 위한 &quot;대기&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 컨텍스트 스위칭(Context Switching) 비용&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 data-end=&quot;3423&quot; data-start=&quot;3370&quot;&gt;여러 스레드가 실행되는 도중, 운영체제는 각각의 스레드를 번갈아가며 CPU에 할당합니다.&lt;/li&gt;
&lt;li data-end=&quot;3500&quot; data-start=&quot;3427&quot;&gt;이때 스레드 레지스터, 스택 등을 저장하고 복원해야 하는데, 이를 &lt;b&gt;&quot;컨텍스트 스위칭&quot;&lt;/b&gt;이라 하며 상당한 오버헤드가 발생합니다.&lt;/li&gt;
&lt;li data-end=&quot;3561&quot; data-start=&quot;3504&quot;&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;3. CPU 캐시 활용 문제&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 data-end=&quot;3696&quot; data-start=&quot;3610&quot;&gt;단일 스레드로 작업할 때는 CPU 캐시가 일관성 있게 활용되지만, 멀티 스레드일 때는 여러 스레드 간 캐시 동기화(Coherency)가 발생합니다.&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;4. 임계 영역(critical section)이 커서 실제 병렬성이 거의 없는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임계 영역은 &lt;span style=&quot;text-align: left;&quot;&gt;여러 스레드가&amp;nbsp;&lt;/span&gt;&lt;b&gt;동시에 접근하면 안 되는 공유 자원(변수, 메모리 영역 등)을 사용하거나 수정하는 부분&lt;/b&gt;을 의미합니다.&lt;/li&gt;
&lt;li data-end=&quot;800&quot; data-start=&quot;706&quot;&gt;예를 들어, 여러 스레드가 동시에 특정 변수의 값을 변경할 때, 해당 연산 전체가 &quot;원자적(Atomic)&quot;으로 처리되지 않으면 데이터 무결성이 깨질 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;908&quot; data-start=&quot;801&quot;&gt;이를 방지하기 위해 임계 영역에 들어가는 스레드는 &lt;b&gt;락(lock)&lt;/b&gt;이나 &lt;b&gt;뮤텍스(mutex)&lt;/b&gt; 같은 동기화 기법을 사용해 &quot;한 번에 오직 1개의 스레드만 접근&quot;하게 해야 합니다.&lt;/li&gt;
&lt;li data-end=&quot;992&quot; data-start=&quot;909&quot;&gt;즉, 임계 영역이 많은 로직(특히 락으로 보호되는 부분)이 복잡해질수록, 멀티 스레드의 이점이 줄어들고 오히려 성능에 악영향을 줄 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;o1&quot; data-message-id=&quot;644f1b5f-5f45-4a0e-9a9c-1ca62411a676&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5507&quot; data-start=&quot;5051&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5088&quot; data-start=&quot;5051&quot;&gt;&amp;ldquo;멀티 스레드 = 항상 빠르다&amp;rdquo;는 공식은 성립하지 않습니다.&lt;/li&gt;
&lt;li data-end=&quot;5270&quot; data-start=&quot;5089&quot;&gt;&lt;b&gt;임계 영역&lt;/b&gt;(공유 자원을 보호하는 구역)이 많거나 락 경쟁이 심한 경우, 멀티 스레드가 오히려 싱글 스레드보다 느릴 수 있습니다. 실제 예제에서처럼 한 번에 하나의 스레드만 doHeavyWork()를 실행할 수 있다면, 멀티 스레드의 이점을 살리지 못하고 오히려 대기와 컨텍스트 스위칭 오버헤드만 늘어납니다.&lt;/li&gt;
&lt;li data-end=&quot;5373&quot; data-start=&quot;5271&quot;&gt;단순히 스레드의 개수가 아니라 &lt;b&gt;작업 특성, 공유 자원 사용 여부, 시스템 구조&lt;/b&gt; 등이 성능에 큰 영향을 미칩니다.&lt;/li&gt;
&lt;li data-end=&quot;5507&quot; data-start=&quot;5374&quot;&gt;병렬화 효과가 큰 CPU 바운드 작업이나, I/O가 많은 작업에 적절한 스레드 풀을 구성한다면 멀티 스레딩이 유리할 수 있습니다. 하지만 임계 영역에서의 동시 접근이 잦거나, 락이 많이 걸리면 멀티 스레딩의 이점이 빠르게 희미해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;5703&quot; data-start=&quot;5509&quot; 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;가 관건이라고 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>멀티 스레드</category>
      <category>멀티 스레드 성능</category>
      <category>싱글 스레드</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/354</guid>
      <comments>https://seungjjun.tistory.com/354#entry354comment</comments>
      <pubDate>Wed, 29 Jan 2025 18:20:45 +0900</pubDate>
    </item>
    <item>
      <title>MySQL Optimizer에 대하여</title>
      <link>https://seungjjun.tistory.com/353</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서&amp;nbsp;쿼리&amp;nbsp;최적화(Query&amp;nbsp;Optimization)는&amp;nbsp;성능을&amp;nbsp;결정짓는&amp;nbsp;핵심&amp;nbsp;요소&amp;nbsp;중&amp;nbsp;하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히&amp;nbsp;Optimizer는&amp;nbsp;사용자가&amp;nbsp;작성한&amp;nbsp;SQL&amp;nbsp;쿼리를&amp;nbsp;분석하여&amp;nbsp;실행&amp;nbsp;비용이&amp;nbsp;가장&amp;nbsp;낮은(또는&amp;nbsp;상대적으로&amp;nbsp;효율적인)&amp;nbsp;실행&amp;nbsp;계획을&amp;nbsp;선택하는&amp;nbsp;역할을&amp;nbsp;맡는다.&lt;br /&gt;&lt;br /&gt;MySQL Optimizer의 내부 구조와 동작 원리를 알아보고, 쿼리가 어떻게 최적화되어 실행 계획으로 이어지는지, 그리고 어떤 데이터나 통계를 활용하는지에 대한 이해 하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Optimizer의 기본 개념&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&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;1338&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDXKAp/btsMGBFOvat/eWX6heVReMwKR6uSa521r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDXKAp/btsMGBFOvat/eWX6heVReMwKR6uSa521r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDXKAp/btsMGBFOvat/eWX6heVReMwKR6uSa521r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDXKAp%2FbtsMGBFOvat%2FeWX6heVReMwKR6uSa521r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;502&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;1066&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;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;SQL 문이 서버로 들어오면, MySQL SQL Parser는 쿼리 문법을 검사하고 내부 표현 구조(Parse Tree)로 변환한다. &lt;br /&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;쿼리&amp;nbsp;전처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 실행 계획으로 넘기기 전, MySQL은 &lt;b&gt;View&lt;/b&gt;, &lt;b&gt;Subquery&lt;/b&gt;, &lt;b&gt;Referential Integrity&lt;/b&gt; 검사 등 다양한 전처리 작업을 수행한다. &lt;br /&gt;이때 MySQL은 Parse Tree를 규격화하고, 쿼리에 따른 스키마 정보를 로드하며, 필요한 경우 쿼리 변환 및 단순화 작업을 병행한다.&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;p data-ke-size=&quot;size16&quot;&gt;전처리된 쿼리가 Optimizer로 전달되면, Optimizer는 여러 가지 가능한 실행 경로를 검토하여&amp;nbsp;&lt;b&gt;예상 비용&lt;/b&gt;이 가장 낮은 경로를 선택한다.&lt;br /&gt;이를 위해 다양한 통계 정보와 규칙 기반 + 비용 기반 알고리즘을 혼합해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MySQL&amp;nbsp;Optimizer의&amp;nbsp;구성&amp;nbsp;요소&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;비용&amp;nbsp;기반&amp;nbsp;최적화(Cost-based&amp;nbsp;Optimization)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL Optimizer는 데이터베이스 내부에 저장된 테이블 및 인덱스의&amp;nbsp;&lt;b&gt;Cardinality&lt;/b&gt;, &lt;b&gt;통계&lt;/b&gt;&amp;nbsp;&lt;b&gt;정보&lt;/b&gt;&amp;nbsp;등을&amp;nbsp;활용하여&amp;nbsp;각&amp;nbsp;선택지에&amp;nbsp;대한&amp;nbsp;비용을&amp;nbsp;계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;Cardinality&lt;/b&gt;: 테이블의 행 수, 인덱스 내에서 특정 값을 가지는 로우 수 등에 대한 추정치&lt;br /&gt;- &lt;b&gt;Statistics&lt;/b&gt;:&amp;nbsp;인덱스&amp;nbsp;분포,&amp;nbsp;데이터&amp;nbsp;분포,&amp;nbsp;테이블&amp;nbsp;크기,&amp;nbsp;평균&amp;nbsp;행&amp;nbsp;길이&amp;nbsp;등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optimizer는&amp;nbsp;이러한&amp;nbsp;통계를&amp;nbsp;참고하여&amp;nbsp;&amp;lsquo;어떤&amp;nbsp;인덱스를&amp;nbsp;사용할&amp;nbsp;것인가&amp;rsquo;,&amp;nbsp;&amp;lsquo;조인&amp;nbsp;순서를&amp;nbsp;어떻게&amp;nbsp;할&amp;nbsp;것인가&amp;rsquo;,&amp;nbsp;&amp;lsquo;서브쿼리를&amp;nbsp;어떻게&amp;nbsp;풀어낼&amp;nbsp;것인가&amp;rsquo;를&amp;nbsp;결정하게&amp;nbsp;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;통계&amp;nbsp;정보의&amp;nbsp;중요성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL Optimizer는 비용 기반으로 동작하기 위해 내부 통계 정보를 적극적으로 활용한다. 이때 통계 정보가 오래되면 부정확한 실행 계획이 세워질 수 있으므로, &lt;b&gt;ANALYZE TABLE&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;MRR(Multi-Range&amp;nbsp;Read),&amp;nbsp;ICP(Index&amp;nbsp;Condition&amp;nbsp;Pushdown)&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;MRR(Multi-Range Read): 범위를 읽을 때 발생하는 Random Access를 최소화하기 위한 최적화 기술이다.&lt;br /&gt;인덱스를 통해서 특정 범위의 레코드 위치를 한 번에 수집해 두고, 저장 엔진(MyISAM, InnoDB 등)이 연속된 디스크 I/O를 수행할 수 있게끔 정렬하는 기법이다.&lt;/li&gt;
&lt;li&gt;ICP(Index Condition Pushdown): 인덱스로 필터링 가능한 조건을 최대한 먼저 수행하여 디스크 I/O를 줄이는 최적화 기술이다.&lt;br /&gt;MySQL 5.6 이후 도입된 기능으로, 인덱스 스캔 과정에서 추가 조건을 적용해 불필요한 레코드 접근을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 최적화 기능들도 Optimizer가 실행 계획을 세울 때 고려하는 요소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿼리&amp;nbsp;최적화&amp;nbsp;과정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단일&amp;nbsp;테이블&amp;nbsp;최적화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 인덱스&amp;nbsp;선택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일&amp;nbsp;테이블에&amp;nbsp;대한&amp;nbsp;SELECT&amp;nbsp;쿼리의&amp;nbsp;경우,&amp;nbsp;Optimizer는&amp;nbsp;사용&amp;nbsp;가능한&amp;nbsp;인덱스들을&amp;nbsp;확인하여&amp;nbsp;가장&amp;nbsp;효율적으로&amp;nbsp;검색할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;인덱스를&amp;nbsp;선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Range scan&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Index scan&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Full table scan&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Index only scan (Covering Index)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 검색&amp;nbsp;조건의&amp;nbsp;단순화&amp;nbsp;및&amp;nbsp;인덱스&amp;nbsp;활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절에 있는 조건을 가능한 한 인덱스 조건으로 변환할 수 있는지 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 `WHERE column BETWEEN 10 AND 100` 같은 경우 Range Scan으로 처리할 수 있으며, 만약 `column`이 인덱스로 설정되어 있다면 빠른 검색이 가능하다 -&amp;gt; Covering Index&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. 프로젝션 최적화&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 컬럼이 인덱스에 모두 포함되어 있다면, 실제 테이블 데이터(클러스터드 인덱스나 데이터 페이지)를 읽지 않고도 결과를 반환할 수 있는 &lt;b&gt;Covering Index&lt;/b&gt;&amp;nbsp;전략이 사용된다. &lt;br /&gt;이를 통해 불필요한 I/O를 방지하여 DB에 대한 부하를 최소화할 수 있다.&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;테이블&amp;nbsp;최적화&lt;/b&gt;&lt;/h4&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;MySQL Optimizer는 여러 테이블을 조인할 때, 각각의 테이블에 대하여 접근 방법을 결정하고, 테이블의 조인 순서를 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 가능한 조인 순서가 많을 경우, 모든 경우의 수를 완전 탐색할 수도 있지만, 테이블이 많아지면 매우 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Nested&amp;nbsp;Loop&amp;nbsp;Join,&amp;nbsp;Block&amp;nbsp;Nested&amp;nbsp;Loop&amp;nbsp;Join&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 대체로&amp;nbsp;&lt;b&gt;Nested Loop Join&lt;/b&gt; 기법을 사용한다. 내부적으로는&amp;nbsp;MRR과&amp;nbsp;같은&amp;nbsp;기법을&amp;nbsp;활용하여&amp;nbsp;조인&amp;nbsp;시&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;무작위&amp;nbsp;접근을&amp;nbsp;줄이도록&amp;nbsp;한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Index Nested Loop Join&lt;/b&gt;: 조인 컬럼에 인덱스가 있으면 outer loop에서 건네준 값으로 빠르게 내부 테이블을 찾아볼 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Block Nested Loop&lt;/b&gt;: 내부 테이블의 데이터를 일시적으로 블록 단위로 가져와서, 외부 테이블의 각 로우와 매칭할 때의 비용을 감소시키는 기법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Semi-Join 및 Subquery 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리를&amp;nbsp;사용할&amp;nbsp;때,&amp;nbsp;MySQL&amp;nbsp;5.6부터는&amp;nbsp;Semi-Join&amp;nbsp;전략을&amp;nbsp;통해&amp;nbsp;서브쿼리를&amp;nbsp;최적화한다.&amp;nbsp;`EXISTS`,&amp;nbsp;`IN`&amp;nbsp;서브쿼리&amp;nbsp;등을&amp;nbsp;외부&amp;nbsp;조인으로&amp;nbsp;변환하거나,&amp;nbsp;FirstMatch&amp;nbsp;전략&amp;nbsp;등을&amp;nbsp;사용하여&amp;nbsp;전체&amp;nbsp;결과를&amp;nbsp;다&amp;nbsp;읽지&amp;nbsp;않고도&amp;nbsp;조건&amp;nbsp;충족&amp;nbsp;여부를&amp;nbsp;빠르게&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;조인&amp;nbsp;버퍼&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 없는 조인 칼럼으로 조인할 때, MySQL은&amp;nbsp;&lt;b&gt;조인 버퍼&lt;/b&gt;를 사용하여 임시로 데이터를 저장하고, 그 버퍼를 통해 상대 테이블을 스캔하는 식으로 조인 비용을 최소화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿼리&amp;nbsp;재작성&amp;nbsp;및&amp;nbsp;기타&amp;nbsp;최적화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;서브쿼리&amp;nbsp;Unnesting&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 종종&amp;nbsp;&lt;b&gt;서브쿼리 Unnesting&lt;/b&gt;&amp;nbsp;기법을&amp;nbsp;사용하여&amp;nbsp;쿼리를&amp;nbsp;재작성한다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;`WHERE&amp;nbsp;column&amp;nbsp;IN&amp;nbsp;(SELECT...)`처럼&amp;nbsp;작성된&amp;nbsp;쿼리는&amp;nbsp;내부적으로&amp;nbsp;조인&amp;nbsp;형태로&amp;nbsp;변환될&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;이때&amp;nbsp;성능이&amp;nbsp;개선될&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;size18&quot;&gt;&lt;b&gt;Distinct Optimization&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`SELECT DISTINCT` 구문에서, 인덱스를 이용해 미리 중복 제거를 수행할 수 있는지(Loose Index Scan) 확인하고 최적화한다.&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;Group By 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`GROUP BY` 구문이 있을 경우, 인덱스를 통한 그룹화가 가능한지 판단한다. &lt;br /&gt;인덱스가&amp;nbsp;적절히&amp;nbsp;구성되어&amp;nbsp;있으면&amp;nbsp;MySQL은&amp;nbsp;임시&amp;nbsp;테이블&amp;nbsp;사용&amp;nbsp;없이&amp;nbsp;바로&amp;nbsp;그룹화&amp;nbsp;작업을&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <category>mysql optimizer</category>
      <category>mysql query optimization</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/353</guid>
      <comments>https://seungjjun.tistory.com/353#entry353comment</comments>
      <pubDate>Wed, 15 Jan 2025 23:15:06 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고</title>
      <link>https://seungjjun.tistory.com/352</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년의&amp;nbsp;키워드는&amp;nbsp;&lt;b&gt;'경험'&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2023년&amp;nbsp;4월에&amp;nbsp;개발자로&amp;nbsp;커리어를&amp;nbsp;시작하며&amp;nbsp;2023년은&amp;nbsp;회사에&amp;nbsp;적응하며&amp;nbsp;보냈었고,&amp;nbsp;차츰&amp;nbsp;적응이&amp;nbsp;되기&amp;nbsp;시작했던&amp;nbsp;2024년은&amp;nbsp;다양한&amp;nbsp;경험을&amp;nbsp;하기&amp;nbsp;위해&amp;nbsp;이것저것&amp;nbsp;시도를&amp;nbsp;많이&amp;nbsp;해보았다.&lt;br /&gt;회고를 통해 이번 연도에 경험했던 것들을 다시 돌아보며 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;우당탕탕 사이드 프로젝트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선&amp;nbsp;2024년&amp;nbsp;1월에는&amp;nbsp;사이드&amp;nbsp;프로젝트를&amp;nbsp;&quot;비사이드&quot;라는&amp;nbsp;플랫폼을&amp;nbsp;통해&amp;nbsp;시작했다.&lt;br /&gt;&quot;비사이드&quot;는 사이드 프로젝트 모임 플랫폼인데, 14주간 개발자, 디자이너, 기획자들과 함께 한 팀을 이루어 프로젝트를 출시하는 프로그램에 참여했었다. (다시 찾아보니 지금은 사라지고 10일 만에 끝내는 포텐데이라는 프로그램만 존재한다.)&lt;br /&gt;&lt;br /&gt;결과부터&amp;nbsp;말하면&amp;nbsp;프로젝트는&amp;nbsp;출시조차&amp;nbsp;하지&amp;nbsp;못하게&amp;nbsp;되었다.&lt;br /&gt;기획, 디자인, 개발까지 어느 정도 완성하여 무난히 출시하여 운영하는 단계까지 갈 수 있을 줄 알았는데, 막바지에 프론트 개발자분들이 하차를 하게 되어 더 이상 진행하기가 어려웠다.&lt;br /&gt;그때&amp;nbsp;당시에는&amp;nbsp;좀&amp;nbsp;아쉬웠었는데&amp;nbsp;다시&amp;nbsp;생각해 보면&amp;nbsp;얻는 것도&amp;nbsp;상당히&amp;nbsp;많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선&amp;nbsp;기획자분들이&amp;nbsp;기획하는&amp;nbsp;것을&amp;nbsp;어깨너머로&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;경험을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있었다.&lt;br /&gt;개발 외 다른 분야는 무지했던 나는 기획이라는 영역이 생각보다 고려해야 하는 게 많다는 것을 알게 되어 기획이라는 분야에 대한 관점이 완전히 바뀌게 되었던 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획자분들에게 죄송하지만 원래 기획을 해본 적도 배워본 적도 없어서 대충 해도 어느 정도 가닥이 잡힐 줄 알았다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPV2S/btsLxkZPf5B/B9Qq3eckcDRKuD6I3ZeBVk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPV2S/btsLxkZPf5B/B9Qq3eckcDRKuD6I3ZeBVk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPV2S/btsLxkZPf5B/B9Qq3eckcDRKuD6I3ZeBVk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPV2S%2FbtsLxkZPf5B%2FB9Qq3eckcDRKuD6I3ZeBVk%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;273&quot; height=&quot;204&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;194&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 data-ke-size=&quot;size16&quot;&gt;그리고 이번 사이드 프로젝트를 하면서 가장 크게 배웠다고 생각되는 부분은 &quot;커뮤니케이션&quot; 영역이었다.&lt;br /&gt;8명이서&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;br /&gt;그러다 보니 다양한 아이디어가 나오지 못하고 회의를 주도하는 PM은 점점 지쳐가는 게 눈에 보였다.&lt;br /&gt;초반에는&amp;nbsp;나도&amp;nbsp;&quot;8명이니&amp;nbsp;누군가&amp;nbsp;좋은&amp;nbsp;아이디어&amp;nbsp;내주겠지&quot;라는&amp;nbsp;아주&amp;nbsp;안일한&amp;nbsp;생각을&amp;nbsp;갖고&amp;nbsp;있어&amp;nbsp;소극적이었던&amp;nbsp;장면이&amp;nbsp;떠오르기도&amp;nbsp;한다.&lt;br /&gt;그런&amp;nbsp;모습을&amp;nbsp;뒤늦게&amp;nbsp;발견하고&amp;nbsp;적극적으로&amp;nbsp;참여하려고&amp;nbsp;노력했지만&amp;nbsp;다시&amp;nbsp;생각해 보면&amp;nbsp;그&amp;nbsp;조차&amp;nbsp;부족했었던 것&amp;nbsp;같다.&amp;nbsp;(죄송합니다&amp;nbsp;ㅠㅠ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고&amp;nbsp;아이디어에&amp;nbsp;대한&amp;nbsp;&quot;&lt;b&gt;주인의식&lt;/b&gt;&quot;이&amp;nbsp;필요하다는&amp;nbsp;것도&amp;nbsp;알게 되었다.&lt;br /&gt;프로젝트&amp;nbsp;주제&amp;nbsp;선정&amp;nbsp;시&amp;nbsp;각자&amp;nbsp;아이디어를&amp;nbsp;제시하지만&amp;nbsp;결국에는&amp;nbsp;하나의&amp;nbsp;아이디어로&amp;nbsp;선정되기&amp;nbsp;때문에&amp;nbsp;아쉬워도&amp;nbsp;누군가의&amp;nbsp;아이디어는&amp;nbsp;철회될&amp;nbsp;수밖에&amp;nbsp;없다.&amp;nbsp;(그렇게&amp;nbsp;내&amp;nbsp;아이디어도&amp;nbsp;노션&amp;nbsp;어딘가에..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8명이&amp;nbsp;모두&amp;nbsp;투표를&amp;nbsp;통해&amp;nbsp;선정된&amp;nbsp;아이디어이기&amp;nbsp;때문에&amp;nbsp;MVP&amp;nbsp;출시를&amp;nbsp;위해&amp;nbsp;기능&amp;nbsp;산정을&amp;nbsp;하는데,&amp;nbsp;아주&amp;nbsp;안일하게도&amp;nbsp;내가&amp;nbsp;낸&amp;nbsp;아이디어가&amp;nbsp;아니니&amp;nbsp;적극적으로&amp;nbsp;기능에&amp;nbsp;대한&amp;nbsp;의견&amp;nbsp;제시를&amp;nbsp;하지&amp;nbsp;않았던&amp;nbsp;부끄러운&amp;nbsp;과거가&amp;nbsp;떠오릅니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;결국&amp;nbsp;이러한&amp;nbsp;마인드&amp;nbsp;때문인지&amp;nbsp;사이드&amp;nbsp;프로젝트를&amp;nbsp;시작했던&amp;nbsp;본연의&amp;nbsp;목적인&amp;nbsp;&quot;재미&quot;와&amp;nbsp;&quot;배움&quot;&amp;nbsp;둘&amp;nbsp;다&amp;nbsp;놓쳤던 것&amp;nbsp;같다.&lt;br /&gt;주인의식이&amp;nbsp;없으니&amp;nbsp;개발을&amp;nbsp;하면서도&amp;nbsp;재미가&amp;nbsp;없고(하라는 것만&amp;nbsp;하는&amp;nbsp;느낌),&amp;nbsp;재미가&amp;nbsp;없으니&amp;nbsp;추가적으로&amp;nbsp;공부를&amp;nbsp;하지&amp;nbsp;않게&amp;nbsp;되었던&amp;nbsp;것&amp;nbsp;같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트가 출시되지 못한 이유로 프론트 개발자가 나간 이유라고 앞부분에 서술하였지만 결국에는 이러한 나의 마인드 때문에 출시할 수 있었어도 하지 못했던 것 같기도 하다.&lt;br /&gt;(주인의식이&amp;nbsp;있었더라면&amp;nbsp;어떻게든&amp;nbsp;프론트쪽을&amp;nbsp;공부하여&amp;nbsp;부족한&amp;nbsp;부분만&amp;nbsp;이어서&amp;nbsp;개발할&amp;nbsp;수도&amp;nbsp;있었을&amp;nbsp;것&amp;nbsp;같다는&amp;nbsp;생각이&amp;nbsp;든다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cphqZC/btsLwqfufF4/CVB5mKhRUu0oJiZHiShxS0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cphqZC/btsLwqfufF4/CVB5mKhRUu0oJiZHiShxS0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cphqZC/btsLwqfufF4/CVB5mKhRUu0oJiZHiShxS0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcphqZC%2FbtsLwqfufF4%2FCVB5mKhRUu0oJiZHiShxS0%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;539&quot; height=&quot;303&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;단순히&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;br /&gt;아무튼 이러한 경험들을 통해 회사 업무에서나, 다른 사이드 프로젝트에서 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 실패를 통해서도 배우는 것이 많으니 다들 사이드 프로젝트를 진행하는 것을 추천하는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;항해&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 연도 중 가장 값진 경험은 &lt;b&gt;&quot;항해 플러스&quot;라는&lt;/b&gt; 교육 프로그램을 진행한 것이 아닐까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 초였나 한창 내가 하고 있는 개발에 대한 의구심과 주어진 단순한 업무만 하며 회사를 다니다 보니 정체되어 있는 느낌을 받고 있던 찰나에 &quot;항해플러스&quot;라는 주니어 개발자를 대상으로 멘토링을 하는 프로그램이 있다는 것을 알게 되었고 다행히 그때 당시 다음 기수인 4기를 모집하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 바로 신청을 하였고 3월에 그 과정(10주)이 시작되었다.&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;이&amp;nbsp;과정을&amp;nbsp;통해&amp;nbsp;얻고&amp;nbsp;싶었던 건&amp;nbsp;크게&amp;nbsp;2가지였다.&lt;br /&gt;우선&amp;nbsp;회사&amp;nbsp;내&amp;nbsp;나와&amp;nbsp;비슷한&amp;nbsp;연차의&amp;nbsp;백엔드&amp;nbsp;개발자가&amp;nbsp;없었기&amp;nbsp;때문에,&amp;nbsp;비슷한&amp;nbsp;연차의&amp;nbsp;주니어&amp;nbsp;개발자는&amp;nbsp;어떤&amp;nbsp;생각을&amp;nbsp;하며&amp;nbsp;어떻게&amp;nbsp;개발을&amp;nbsp;하고&amp;nbsp;있는지가&amp;nbsp;너무&amp;nbsp;궁금했었다.&lt;br /&gt;그리고 흔히 알려져 있는 빅테크 기업에 다니는 시니어 개발자는 어떻게 개발을 하며 그분들의 생각을 엿보고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 만을 바라보고 갔지만 생각보다 많은 것들을 얻을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발의 영역에서는 실무에서 경험할 수 없는 동시성 처리, 대용량 트래픽 처리를 위한 설계, 의미 있는 코드 작성 등등 많은 경험들을 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 교육을 통해 얻은 성과로는 실무에서 개발하면서 기존에는 고려하지 않았던 부분들을 고려(e.g 동시성 발생 가능성, 인덱스 설계 등등)하며 개발을 하기 시작했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 해당 과정에서 매주 진행되는 과제 평가를 잘 통과하여 블랙 배지라는 인증과 항해를 진행하며 한 주간 작성했던 회고글을 통해 수료식에도 회고상을 받을 수 있었다! (나름 뿌듯)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;회고상.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biQXC5/btsLw4XloOo/yAf9sopsafsRBKLjHkDVA0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biQXC5/btsLw4XloOo/yAf9sopsafsRBKLjHkDVA0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biQXC5/btsLw4XloOo/yAf9sopsafsRBKLjHkDVA0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiQXC5%2FbtsLw4XloOo%2FyAf9sopsafsRBKLjHkDVA0%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;309&quot; height=&quot;412&quot; data-filename=&quot;회고상.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;회사&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해&amp;nbsp;과정이&amp;nbsp;끝나고&amp;nbsp;나서부터는&amp;nbsp;새로운&amp;nbsp;서비스&amp;nbsp;준비 때문에&amp;nbsp;지금까지&amp;nbsp;바쁘게&amp;nbsp;업무만&amp;nbsp;하며&amp;nbsp;보내왔다.&amp;nbsp;&lt;br /&gt;조금&amp;nbsp;더&amp;nbsp;잘하고&amp;nbsp;싶은&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;br /&gt;작년에 비해 회사에서도 개발자로서 어느 정도 자리가 잡힌 느낌이 들기도 하고 어렵게만 느껴졌던 금융이라는 도메인에 대한 이해도도 작년보다 늘었다는 것을 느낄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 업무에 대해 조금 이야기해 보자면 단순 CRUD 기능을 개발하고 유지보수 하는 것뿐만 아니라 작년 말부터 시작했던 카프카 스트림즈를 활용한 실시간 데이터 처리나 LLM을 활용한 기능 개발 등등 새롭게 경험할 수 있는 영역이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무를 통해 배운 LLM 활용 기술을 통해 최근에 LLM을 활용하여 혼자 사이드 프로젝트를 진행하여 배포까지 한 상황이다.&lt;/p&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 title=&quot;TechHive&quot; href=&quot;https://www.tech-hive.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tech-hive&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;아무튼 내년에 현재 회사에서 개발 중인 서비스 론칭을 하는데, 이번 서비스는 개발 초기부터 참여하였기 때문에 담당한 기능이 많아 서비스 론칭이 기대가 되기도 하고 실 서비스 환경에서 발생할 버그를 생각하면 걱정이 되기도 한다. (개발 환경에서도 종종 발생하는 버그를 처리하며..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 외적으로는 짧게 회고하면 아주 작게 투자를 하기 시작했고(약간의 경제 공부), 운동을 올 초부터 8월까지 열심히 하다 업무로 바쁘다는 핑계 삼아 잠시(?) 멈추게 되었다.&lt;/p&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;2024년은 경험하며 무언가 배우는 것에 집중했던 1년을 보냈던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년은 배운 것들을 활용하여 나의 가치를 올리고 성장하는데 집중하는 한 해를 보낼 예정이다!&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;700&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oyl1C/btsLx5VhVOh/st6CtAyAtVwtjXQXOtY7Qk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oyl1C/btsLx5VhVOh/st6CtAyAtVwtjXQXOtY7Qk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oyl1C/btsLx5VhVOh/st6CtAyAtVwtjXQXOtY7Qk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyl1C%2FbtsLx5VhVOh%2Fst6CtAyAtVwtjXQXOtY7Qk%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;482&quot; height=&quot;301&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>성장이야기/주간회고</category>
      <category>2024년 회고</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/352</guid>
      <comments>https://seungjjun.tistory.com/352#entry352comment</comments>
      <pubDate>Mon, 23 Dec 2024 20:47:07 +0900</pubDate>
    </item>
    <item>
      <title>LLM 기반 보고서 자동 요약 프롬프트 최적화 전략 (내용 누락 트러블 슈팅)</title>
      <link>https://seungjjun.tistory.com/349</link>
      <description>&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;TL;DR&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PPT 보고서를 LLM을 통해 특정 형식(JSON)으로 요약할 때, 키워드 누락 문제가 발생하여 명시적 지시문 추가, Chain-of-Thought(생각의 사슬), 역할 기반 프롬프팅, 퓨샷(Few-Shot) 프롬프팅 등 다양한 전략을 시도함.&lt;br /&gt;결과적으로 원샷 대신 퓨샷 프롬프팅으로 변경하니 입력 토큰은 약 22% 증가했지만, 키워드 누락 없이 정확도가 크게 개선됨.&lt;br /&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;PPT로 작성된 보고서를 텍스트만 전달하여 LLM 기반으로 보고서를 특정 형식에 맞게 요약하는 시스템을 개발하고 있었습니다.&lt;/p&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.g. 삼성전자, 불닭 등등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 상황&lt;br /&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;구글 키워드&amp;rdquo;라는 제목의 슬라이드에 작성된 키워드가 &amp;ldquo;농심&amp;rdquo;, &amp;ldquo;엔씨소프트&amp;rdquo;, &amp;ldquo;하이브&amp;rdquo;이고 &amp;ldquo;유튜브 키워드&amp;rdquo;라는 제목의 슬라이드에 작성된 키워드는 &amp;ldquo;삼양식품&amp;rdquo;, &amp;ldquo;롯데칠성&amp;rdquo; 으로 작성되어 있다고 가정하겠습니다.&lt;/p&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;ldquo;농심&amp;rdquo;, &amp;ldquo;엔씨소프트&amp;rdquo;, &amp;ldquo;하이브&amp;rdquo;, &amp;ldquo;삼양식품&amp;rdquo;, &amp;ldquo;롯데칠성&amp;rdquo; 으로 전부 추출이 되어야 하는데, 실제로 요약 내용에는 &amp;ldquo;하이브&amp;rdquo;, &amp;ldquo;롯데칠성&amp;rdquo; 키워드가 누락되는 문제가 반복적으로 발생했습니다. (&amp;ldquo;농심&amp;rdquo;, &amp;ldquo;엔씨소프트&amp;rdquo;, &amp;ldquo;삼양식품&amp;rdquo; 만 요약됨.)&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;현재 프롬프트 작성 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 작성했던 프롬프트는 보고서 내용에서 작성된 날짜, 제목, 키워드 등등 에 대한 요약 프롬프트를 작성하고 JSON 형식으로 응답받기 위해 JSON 구조를 같이 전달하였습니다.&lt;/p&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;b&gt;&amp;ldquo;원샷&amp;rdquo;&lt;/b&gt; (단 하나의 예시만 제공함으로써 추론 하는 것) 프롬프트 전략을 적용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 낮은 Temperature를 설정하여 모델이(GPT-4o mini 기준) 단어를 불필요하게 변형 없이 생성하도록 하였습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;lsquo;Temperature' 값을 낮게 설정하면 일반적인 문장의 답변을, 높게 설정하면 창의적인 답변을 생성하게 됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프롬프트 최적화 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 내용 요약시 키워드가 누락되는 문제가 있어 아래와 같은 프롬프트 최적화를 시도해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 명시적 지시문 추가&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;/b&gt; 프롬프트 내에 &lt;b&gt;&amp;ldquo;특정 플랫폼에 나타나는 키워드는 반드시 포함해 주세요&amp;rdquo;와&lt;/b&gt; 같이 직접 명시함으로써, 모델이 해당 키워드들을 요약에 반드시 포함하도록 유도하였습니다. &lt;br /&gt;추가로 키워드들의 개수가 너무 많아지면 모델이 자동으로 키워드들을 누락하는 건지 싶어, 키워드의 개수 제한을 두지 말라고 명시적으로 작성해 주었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시 포함:&lt;/b&gt; 실제로 누락되지 않은 올바른 요약 예시를 제공하여, 모델이 어떤 결과를 기대하는지 학습할 수 있도록 하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 프롬프트&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;Summarize the PPT content and return a concise JSON response. 
Focus especially on any text matching the pattern &quot;keyword:text&quot;
and include all such instances without removing or changing them. 
Do not limit the quantity of keywords.

Return your answer in the following JSON structure (in Korean):

{
  &quot;title&quot;: &quot;&amp;lt;보고서 제목&amp;gt;&quot;,
  &quot;keywords&quot;: [
    &quot;keyword:text&quot;,
    &quot;keyword:text&quot;,
    &quot;... 추가 키워드 ...&quot;
  ],
  &quot;summary&quot;: &quot;&amp;lt;보고서 요약&amp;gt;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 생각의 사슬 (Chain of Thought)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각의 사슬 기법은 모델에게 각 단계를 차례로 생각하며 문제를 해결하도록 요청함으로써, 최종 답변에 이르기까지의 과정을 명확하게 하는 기법입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단계 분리:&lt;/b&gt; 첫 번째 단계에서 &amp;ldquo;구글 (유튜브, 네이버) 키워드&amp;rdquo; 슬라이드 내 키워드들을 추출하도록 요청한 후, 두 번째 단계에서 해당 키워드들을 원하는 변형 하여 응답하도록 요청하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 프롬프트&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;[지시사항]
1) 최종 요약에 필요한 모든 텍스트를 PPT 내용에서 발췌하되, 'keyword:'로 시작하는 항목을 절대 누락하지 마세요.
2) 'keyword:'라는 접두어와 뒤에 오는 텍스트(회사명, 상품명, 브랜드명 등)는 원본 그대로 유지해 주세요.
3) 보고서 전체 맥락에 대한 요약은 'summary' 필드에 작성해 주시고, 'title' 필드는 PPT의 첫 슬라이드 또는 주제 문구를 활용해 주세요.
4) 출력은 반드시 JSON 형태의 텍스트여야 하며, 올바른 JSON 구문을 준수해야 합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 역할 기반 프롬프트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 기반 프롬프트는 명확한 직업(또는 역할)을 명시해 주어 도메인별 맞춤형 응답을 제공받도록 유도할 수 있습니다. 해당 기법을 통해 모델이 올바른 어휘를 선택하도록 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할 부여:&lt;/b&gt; 요청에 맞는 명확한 역할을 특정인물이나 직업을 넣어 페르소나를 부여하였습니다. (e.g. 당신은 한국 시장의 전문 데이터 애널리스트입니다. 주어진 보고서를 형식에 맞게 요약하는 것이 과제입니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 프롬프트&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;You are an expert data summarizer specializing in analyzing PPT slides. 
Your primary objective is to ensure that all keywords appearing in the slides 
are captured accurately without any omission. 
Please provide all answers in Korean.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 원샷 프롬프트에서 퓨샷 프롬프트로 변경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 원샷 프롬프팅 전략으로 1개의 예시를 제공했던 방식에서 &lt;b&gt;퓨샷(few-shot) 프롬프팅 전략&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;ldquo;원샷 프롬프팅&amp;rdquo; 전략을 선택했던 이유는 보고서 내용이 길어서 작성해야 하는 프롬프트 내용도 길었고, 여러 예시를 던져주게 되면 추가 비용(입력 토큰이 많아짐에 따라)이 발생하기 때문에 &amp;ldquo;원샷&amp;rdquo;을 통해 문제를 해결하려고 하였습니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;ldquo;요약 정확도 vs 모델 호출 비용&amp;rdquo; 트레이드오프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보고서 요약 정확도를 더 높이기 위해 GPT-4o mini 모델 사용과 출력 토큰에 비해 입력 토큰의 비용이 적은 점, 보고서 요약은 1달에 최대 7번인 점을 고려해 비용을 감수해야 한다는 것을 말씀드렸습니다.&lt;/p&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;ldquo;퓨샷 프롬프팅&amp;rdquo; 전략을 선택하였고 결론적으로 기존 원샷 대비 퓨샷 프롬프팅 전략 사용 시 입력 토큰이 약 22% 정도 증가 하였지만 정확도 측면에서 큰 이점을 얻었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(일반적으로 입력 토큰보다 출력 토큰에 더 많은 연산 리소스가 소모되기 때문에 출력 토큰의 비용이 더 높다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;자세한 pricing 정책은 &lt;a href=&quot;https://openai.com/api/pricing/&quot;&gt;https://openai.com/api/pricing/&lt;/a&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;LLM 기반으로 보고서를 요약하는 과정에서 프롬프팅 최적화 전략을 알아보았고, 더 다양한 상황에서 LLM을 통해 가치 창출을 할 수 있을 것 같다는 생각을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>llm</category>
      <category>Prompt Engineering</category>
      <category>프롬프트 최적화</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/349</guid>
      <comments>https://seungjjun.tistory.com/349#entry349comment</comments>
      <pubDate>Wed, 27 Nov 2024 09:37:04 +0900</pubDate>
    </item>
    <item>
      <title>자바에서의 CRTP(Curiously Recurring Template Pattern)</title>
      <link>https://seungjjun.tistory.com/347</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 빌더 패턴을 구현하다 CRTP(Curiously Recurring Template Pattern)&amp;nbsp;패턴을 알게 되었는데, 처음 접하는 패턴이라는 점과 제네릭을 통해 구현된 다소 복잡해 보이는 패턴이라 공부하게 되었습니다. 이번 글을 통해 CRTP 패턴이 무엇인지와 장단점에 대해 소개드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;CRTP란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&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;CRTP는 원래 C++에서 주로 사용되는 패턴으로 클래스가 자신의 서브클래스 타입을 제네릭 매개변수로 사용하는 디자인 패턴입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 주로 빌더 패턴에서 자식 클래스의 타입을 부모 클래스에 전달하여 메서드 체이닝 시 자식 클래스의 메서드를 사용할 수 있게 합니다. (제네릭을 이용한 재귀적 타입 제한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 빌더 패턴에서 주로 이루어지는 메서드 체이닝에서 자식 클래스 타입을 유지함으로 컴파일 시점에 타입 오류를 방지할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 CRTP 패턴이 무엇인지 알아보았는데 조금 더 자세히 어떤 장점과 단점을 갖고 있는지 예제 코드와 함께 소개드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VehicleBuilder 코드&lt;/p&gt;
&lt;pre id=&quot;code_1734848850625&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class VehicleBuilder&amp;lt;T extends VehicleBuilder&amp;lt;T&amp;gt;&amp;gt; {

    protected String color;
    protected int speed;

    public T color(String color) {
        this.color = color;
        return self();
    }

    public T speed(int speed) {
        this.speed = speed;
        return self();
    }

    // 자식클래스에서 자신의 인스턴스를 반환하도록 추상 메서드 정의
    protected abstract T self();

    public Vehicle build() {
        return new Vehicle(color, speed);
    }
}&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;위 코드와 같이 VehicleBuilder 클래스를 상속받고 있는 자식 클래스를 T(제네릭 매개변수)로 사용하고 있는 형태를 CRTP 패턴이라 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 클래스를 제네릭 매개변수로 사용함으로써 다음과 같은 이점을 갖게 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;타입 안전성&lt;/li&gt;
&lt;li&gt;확장성(코드 재사용성)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드를 통해 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1. 타입 안정성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VehicleBuilder 클래스를 상속받고 있는 CarBuilder 클래스를 통해 Car 객체를 만든다고 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734848850627&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CarBuilder extends VehicleBuilder&amp;lt;CarBuilder&amp;gt; {

    private int numberOfWheel;

    public CarBuilder numberOfWheel(int numberOfWheel) {
        this.numberOfWheel = numberOfWheel;
        return this;
    }

    @Override
    protected CarBuilder self() {
        return this;
    }

    @Override
    public Car build() {
        return new Car(color, speed, numberOfWheel);
    }
}

public class Vehicle {
    private final String color;
    private final int speed;

    public Vehicle(String color, int speed) {
        this.color = color;
        this.speed = speed;
    }
}


public class Car extends Vehicle {

    private final int numberOfWheel;

    public Car(String color, int speed, int numberOfWheel) {
        super(color, speed);
        this.numberOfWheel = numberOfWheel;
    }
}&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;이전에 보여준 VehicleBuilder 클래스는 color(), speed() 메서드가 정확히 CarBuilder 클래스를 반환하도록 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 CarBuilder 클래스가 VehicleBuilder 클래스를 상속받고 자신의 타입을 제네릭 매개변수로 사용(CarBuilder extends VehicleBuilder )하였기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734848850629&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Car car = new CarBuilder()
      .color(&quot;Red&quot;)
      .speed(180)
      .numberOfWheel(4)
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위 CarBuilder를 통해 Car 객체를 만들 때 color(), speed() 메서드를 사용하더라도 정확한 타입(여기서 CarBuilder)이 반환되므로 타입 변환이 필요 없어집니다.&lt;/p&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;2. 확장성(코드 재사용성)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Car 객체를 만드는 CarBuilder 뿐만 아니라 Bike 객체를 만드는 BikeBuilder를 만든다고 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1734848850629&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BikeBuilder extends VehicleBuilder&amp;lt;BikeBuilder&amp;gt;{

    private boolean hasHelmet;

    public BikeBuilder hasHelmet(boolean hasHelmet) {
        this.hasHelmet = hasHelmet;
        return this;
    }

    @Override
    protected BikeBuilder self() {
        return this;
    }

    @Override
    public Vehicle build() {
        return new Bike(color, speed, hasHelmet);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탈것을 만드는 공통 로직(색상, 스피드)은 추상 클래스에 두어 재사용하고, 자식 클래스에서는 개별적인 설정을 추가로 가능하도록 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 각각의 빌더 클래스에서 공통적인 코드를 불필요하게 작성하지 않고 추상 클래스에 작성함으로써 코드를 재사용할 수 있고 자식 클래스에서는 추가적으로 확장을 통한 기능 구현이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 특징 덕분에 CRTP는 복잡한 객체 생성이나 추가 로직을 유연하고 타입 안전하게 구현하는 데 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRTP 을 구현했을 때 제가 생가한 단점은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;복잡성 증가&lt;/li&gt;
&lt;li&gt;디버깅 난이도 증가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1. 복잡성 증가&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 느꼈던 그대로 CRTP는 제네릭과 상속을 결합한 패턴이라, 구조가 다소 복잡해 보일 수 있고 특히 제네릭에 대한 이해도가 떨어진다면 이해하기 어려운 패턴이라고 생각하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 제네릭 타입 매개변수와 추상 메서드의 조합으로 인해 코드가 복잡해지고, 가독성이 떨어지는 느낌을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡성이 증가함에 따라 유지보수도 어려워질 수 있다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스를 상속받는 자식 클래스가 증가하게 되었을 때, 제네릭과 상속을 결합한 복잡한 패턴이 오히려 유지보수를 어렵게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;2. 디버깅 난이도 증가&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡성이 증가함에 따라 디버깅 난이도도 함께 증가하는 것을 체감할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 제네릭 타입을 런타임 시점에 소거하기 때문에, 복잡한 제네릭 구조에서 발생하는 오류는 컴파일 타임에만 확인 가능하며, 디버깅 과정에서 타입 정보를 확인하기가 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>성장이야기/TIL</category>
      <author>seungjjun</author>
      <guid isPermaLink="true">https://seungjjun.tistory.com/347</guid>
      <comments>https://seungjjun.tistory.com/347#entry347comment</comments>
      <pubDate>Tue, 6 Aug 2024 09:41:44 +0900</pubDate>
    </item>
  </channel>
</rss>