<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>kimong 님의 블로그</title>
    <link>https://think-yk.tistory.com/</link>
    <description>kimong 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 16 May 2026 12:09:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kimong</managingEditor>
    <item>
      <title>루퍼스 부트캠프 백엔드 3기를 수강 후기</title>
      <link>https://think-yk.tistory.com/7</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;a href=&quot;https://www.loopers.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.loopers.im/&lt;/a&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1777214492721&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Loop:PAK - 새로운 코딩 교육의 시작, 루퍼스에서 곧 만나요!&quot; data-og-description=&quot;루퍼스 부트캠프는 크루원들의 한단계 높은 성장을 이끄는 선순환을 목표로 현업 개발자와 협업하여 새롭게 탄생한 실전 개발 강의입니다. 열정있는 여러분에게, 진심을 다하는 루퍼스가, 진짜&quot; data-og-host=&quot;www.loopers.im&quot; data-og-source-url=&quot;https://www.loopers.im/&quot; data-og-url=&quot;https://loopers.im&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CvsUv/dJMb8QMeNun/Cb7vf3G9XlXy2g97uePZfK/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/BKOWA/dJMb9eTSqlF/JIT9rhFFGTWGATVYReteEk/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/bjvdIP/dJMb8WeCrwz/65detaCKvGOCvC4bSIlaQk/img.png?width=2560&amp;amp;height=1060&amp;amp;face=0_0_2560_1060&quot;&gt;&lt;a href=&quot;https://www.loopers.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.loopers.im/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CvsUv/dJMb8QMeNun/Cb7vf3G9XlXy2g97uePZfK/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/BKOWA/dJMb9eTSqlF/JIT9rhFFGTWGATVYReteEk/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/bjvdIP/dJMb8WeCrwz/65detaCKvGOCvC4bSIlaQk/img.png?width=2560&amp;amp;height=1060&amp;amp;face=0_0_2560_1060');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Loop:PAK - 새로운 코딩 교육의 시작, 루퍼스에서 곧 만나요!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;루퍼스 부트캠프는 크루원들의 한단계 높은 성장을 이끄는 선순환을 목표로 현업 개발자와 협업하여 새롭게 탄생한 실전 개발 강의입니다. 열정있는 여러분에게, 진심을 다하는 루퍼스가, 진짜&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.loopers.im&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;나의 루퍼스 부트캠프 백엔드 신청 계기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 재직 중인 회사는 팀 규모가 작고 서비스 트래픽이 안정적인 편이다. 개발자로서 평온한 일상도 좋지만, 마음 한편에는 늘 &amp;lsquo;이대로 괜찮을까?&amp;rsquo; 하는 막연한 불안감이 있었다. 실무에서 다룰 수 있는 기술적 범위가 제한적이다 보니, 대규모 트래픽 설계나 복잡한 장애 대응 같은 &amp;lsquo;진짜 백엔드&amp;rsquo;의 고민을 해볼 기회가 적었기 때문이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;이런 기술적 갈증을 느끼던 차에 루퍼스 부트캠프를 알게 되었다. 단순히 이론만 배우는 것이 아니라, 실무에서 마주할 법한 도전적인 시나리오를 던져준다는 점이 매력적이었다. 특히 3기부터 AI 활용을 적극 권장한다는 소식을 듣고, &lt;u&gt;&lt;b data-index-in-node=&quot;128&quot; data-path-to-node=&quot;4&quot;&gt;막연하게만 쓰고 있는 AI를 어떻게 하면 내 개발 업무에 제대로 녹여낼 수 있을지&lt;/b&gt;&lt;/u&gt; 그 해답을 찾고 싶어 신청하게 되었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;부트캠프 수료 후 달라진 나의 모습&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;'정답'보다 '근거'를 찾는 습관&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 절대적인 답을 찾으려던 습관이 있었다. 하지만 이제는 어떤 선택이든 트레이드오프가 존재하며, 중요한 것은 상황에 맞고 팀이 감당할 수 있는 최선의 선택을 하는 것임을 배웠다. 관성적으로 써온 패턴들에 대해 &quot;왜 이렇게 해야 하는가&quot;라는 근거를 스스로 묻고 답할 수 있게 된 것이 가장 큰 수확이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;AI를 활용하는 관점의 변화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전에는 AI를 단편적으로만 활용했다면, 이제는 테스트 코드 작성이나 문서화 등 번거로운 작업에 AI를 파트너로 활용한다. 덕분에 진입 장벽을 낮추고 본질적인 설계에 더 집중할 수 있는 워크플로우를 만들었고, 실제 회사 실무에서도 아주 유용하게 써먹고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;깊게 고민하며 공부하는 법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;당장 눈앞의 답을 찾아 적용하기 급급했던 계획 없는 공부법에서 벗어났다. 한 번 더 질문하고 스스로 생각을 확장해 보는 연습을 통해 앞으로 나아갈 공부 방향의 윤곽이 잡혔다. 장애가 터지기 전 어떻게 동작할지를 미리 설계하는 태도를 배운 것은 개발자로서 큰 자산이 될 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 내가 생각하는 루퍼스 &lt;b&gt;백엔드 부트캠프의 장점&lt;/b&gt;들이다.&amp;nbsp;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3&quot;&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;혹시나 고민하는 사람들이 있다면 아래글을 참고해봤으면 좋겠다.&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3&quot;&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; 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;u&gt;&lt;i&gt;&lt;b&gt;성장의 폭을 넓히고 싶은 사람에게 꼭 추천&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;하고 싶은 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X0TCE/dJMcacQqrWz/4mvYKbqGkXCyRgB9RHBoXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X0TCE/dJMcacQqrWz/4mvYKbqGkXCyRgB9RHBoXK/img.png&quot; data-alt=&quot;멘토링을 기다리는 멘티들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X0TCE/dJMcacQqrWz/4mvYKbqGkXCyRgB9RHBoXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX0TCE%2FdJMcacQqrWz%2F4mvYKbqGkXCyRgB9RHBoXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;479&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;멘토링을 기다리는 멘티들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; 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-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;루퍼스 부트캠프는 이런 나의 가려운 곳을 시원하게 긁어준 곳이다. 상세한 개발 문서 작성부터, 대량의 트래픽이 몰리는 상황을 어떻게 설계하고 대비해야 하는지 깊게 고민하며 직접 구현해 보는 미션들을 제공한다. 결코 쉽지 않은 과정이지만, 평소라면 접하기 힘든 가상의 극한 상황들을 해결해 나가는 과정은 개발자로서 정말 소중한 경험이 되었다. &lt;u&gt;&lt;b&gt;실무 환경의 제약 때문에 기술적 성장에 목말랐던 개발자라면, 이곳의 도전적인 미션들을 통해 확실한 해답&lt;/b&gt;&lt;/u&gt;을 찾을 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.15.59.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhaoI6/dJMcahxsajW/b9vYm3a7sUiAkZiOmPzml0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhaoI6/dJMcahxsajW/b9vYm3a7sUiAkZiOmPzml0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhaoI6/dJMcahxsajW/b9vYm3a7sUiAkZiOmPzml0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhaoI6%2FdJMcahxsajW%2Fb9vYm3a7sUiAkZiOmPzml0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;1192&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.15.59.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.16.05.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw9Rdu/dJMcaayku4N/luc9hHKkNBlxm3wChRwpK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw9Rdu/dJMcaayku4N/luc9hHKkNBlxm3wChRwpK0/img.png&quot; data-alt=&quot;주차별 미션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw9Rdu/dJMcaayku4N/luc9hHKkNBlxm3wChRwpK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw9Rdu%2FdJMcaayku4N%2Fluc9hHKkNBlxm3wChRwpK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;346&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.16.05.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주차별 미션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4&quot;&gt;건전한 경쟁 조성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매주 우수 멘티들에게 주는 'Rising Talent(RT)' 시스템은 학습 열정을 굉장히 좋은 시스템이었다&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(RT를 많이 받으면 나중에 좋은일이?)&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루퍼스는 단순히 줄을 세우는 경쟁이 아니라, 동료들의 좋은 코드를 보고 배우며 서로 긍정적인 자극을 주고받는 환경을 만들어 준다. &lt;u&gt;&lt;i&gt;&lt;b&gt;혼자 공부하며 동기부여가 안 됐던 사람들에게 이런 건강한 자극이 있는 문화를 특히 추천&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;하고 싶다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.19.39.png&quot; data-origin-width=&quot;3244&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAnI7z/dJMcafzDBRz/3ibXC1nclLJqKUmw4AO9S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAnI7z/dJMcafzDBRz/3ibXC1nclLJqKUmw4AO9S0/img.png&quot; data-alt=&quot;주차 미션 RT를 받은 사람들은 이렇게 전광판(?)에 올라간다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAnI7z/dJMcafzDBRz/3ibXC1nclLJqKUmw4AO9S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAnI7z%2FdJMcafzDBRz%2F3ibXC1nclLJqKUmw4AO9S0%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;3244&quot; height=&quot;956&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.19.39.png&quot; data-origin-width=&quot;3244&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주차 미션 RT를 받은 사람들은 이렇게 전광판(?)에 올라간다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3&quot;&gt;AI 활용 방법 및 팁 공유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;루퍼스 백엔드 3기부터는 AI 활용을 적극 권장한다. 단순히 써보라는 말에 그치지 않고, 똑똑하고 효율적으로 AI를 학습에 녹여내는 구체적인 가이드를 주는 점이 인상적이었다. 특히 빅테크 현업 멘토들의 AI 실무 노하우와 동료들의 다양한 활용법을 접하며 나는 평소에 AI를 너무 얕게 쓰고 있었다는 것을 깨달았다. 질문의 각도를 조금만 바꿔도 AI는 단순한 답안지가 아닌, 내 사고를 확장해 주는 강력한 파트너가 될 수 있었다. 거기서 얻은 실무적인 팁들은 학습 효율을 몇 배로 끌어올려 주었다. &lt;b&gt;&lt;i&gt;&lt;u&gt;AI를 똑똑하게 다루며 성장의 속도를 높이고 싶은 개발자&lt;/u&gt;&lt;/i&gt;&lt;/b&gt;라면, 루퍼스 부트캠프가 제시하는 가이드가 큰 도움이 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.24.24.png&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;1712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsCMoG/dJMcaad1GRs/9uJtFPK32K4pANipmYJvm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsCMoG/dJMcaad1GRs/9uJtFPK32K4pANipmYJvm1/img.png&quot; data-alt=&quot;멘토 + 멘티 모두가 자신만의 노하우를 공유하는 슬랙 채널도 있다 ㄷㄷ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsCMoG/dJMcaad1GRs/9uJtFPK32K4pANipmYJvm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsCMoG%2FdJMcaad1GRs%2F9uJtFPK32K4pANipmYJvm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;401&quot; data-filename=&quot;스크린샷 2026-04-26 오후 11.24.24.png&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;1712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;멘토 + 멘티 모두가 자신만의 노하우를 공유하는 슬랙 채널도 있다 ㄷㄷ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-path-to-node=&quot;3&quot; data-index-in-node=&quot;0&quot;&gt;함께하는 팀원, 동기들 그리고 스태프분들&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;10주라는 시간이 결코 짧지는 않다. 미션은 점점 어려워지고 체력적인 한계에 부딪힐 때도 있지만, 그때마다 함께 결의를 다지는 팀원들이 있었기에 포기하지 않고 달릴 수 있었다. 여기에 엔젤, 버디, 서포터 스태프분들이 옆에서 세심하게 챙겨주고 도와주신 덕분에 10주라는 &amp;lsquo;작은 마라톤&amp;rsquo;을 완주할 수 있었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;끝까지 포기하지 않고 함께 성장하는 경험&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;을 해보고 싶다면 이곳의 든든한 동료와 스태프 시스템이 큰 힘이 될 것 이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhH1Jm/dJMcacCUPkA/p4YBhPyWCdYYWvoeqlOXm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhH1Jm/dJMcacCUPkA/p4YBhPyWCdYYWvoeqlOXm1/img.png&quot; data-alt=&quot;격주 일요일마다 라디오처럼 사연 받고 상담(?)해주는 유일한 힐링 시간 루프톡!!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhH1Jm/dJMcacCUPkA/p4YBhPyWCdYYWvoeqlOXm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhH1Jm%2FdJMcacCUPkA%2Fp4YBhPyWCdYYWvoeqlOXm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;687&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;격주 일요일마다 라디오처럼 사연 받고 상담(?)해주는 유일한 힐링 시간 루프톡!!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;4&quot; data-ke-style=&quot;style2&quot;&gt;추신&lt;br /&gt;&lt;br /&gt;생각보다 시간 할애를 많이해야해요. 특히나 직장다니면서 하기에는 쪼~~금 벅찰 수는 있습니다(야근하면...ㅠ)&lt;br /&gt;AI를 활용해서 개발하니까 쉽게 쉽게 배우고 수료할 수 있을거란 생각은 접어두시는게 ㅠ.ㅠ&lt;br /&gt;&lt;br /&gt;물론 저희 기수 분들은 워낙 열심히 하시는 분들이라 96% 분들이 수료하셨습니다 :)&amp;nbsp;&lt;br /&gt;너무 겁먹지는 마시고, 그렇다고 너무 마음 놓치도 마시고 한번 도전해보세요!!!!&lt;br /&gt;루퍼스 부트캠프가 개발자 성장에 아주 큰 분기점이 될 수도....?&lt;br /&gt;&lt;br /&gt;화이팅 입니다!&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;아래 코드를 결제시 입력하시면 할인혜택을 받을 수 있습니다&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;4&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;MMA16&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;figure id=&quot;og_1777212054454&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Loop:PAK - 새로운 코딩 교육의 시작, 루퍼스에서 곧 만나요!&quot; data-og-description=&quot;루퍼스 부트캠프는 크루원들의 한단계 높은 성장을 이끄는 선순환을 목표로 현업 개발자와 협업하여 새롭게 탄생한 실전 개발 강의입니다. 열정있는 여러분에게, 진심을 다하는 루퍼스가, 진짜&quot; data-og-host=&quot;www.loopers.im&quot; data-og-source-url=&quot;https://www.loopers.im/&quot; data-og-url=&quot;https://loopers.im&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CvsUv/dJMb8QMeNun/Cb7vf3G9XlXy2g97uePZfK/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/BKOWA/dJMb9eTSqlF/JIT9rhFFGTWGATVYReteEk/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/bjvdIP/dJMb8WeCrwz/65detaCKvGOCvC4bSIlaQk/img.png?width=2560&amp;amp;height=1060&amp;amp;face=0_0_2560_1060&quot;&gt;&lt;a href=&quot;https://www.loopers.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.loopers.im/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CvsUv/dJMb8QMeNun/Cb7vf3G9XlXy2g97uePZfK/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/BKOWA/dJMb9eTSqlF/JIT9rhFFGTWGATVYReteEk/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/bjvdIP/dJMb8WeCrwz/65detaCKvGOCvC4bSIlaQk/img.png?width=2560&amp;amp;height=1060&amp;amp;face=0_0_2560_1060');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Loop:PAK - 새로운 코딩 교육의 시작, 루퍼스에서 곧 만나요!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;루퍼스 부트캠프는 크루원들의 한단계 높은 성장을 이끄는 선순환을 목표로 현업 개발자와 협업하여 새롭게 탄생한 실전 개발 강의입니다. 열정있는 여러분에게, 진심을 다하는 루퍼스가, 진짜&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.loopers.im&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>루퍼스</category>
      <category>백엔드</category>
      <category>부트캠프</category>
      <category>설계</category>
      <category>스프링</category>
      <category>자바</category>
      <category>코틀린</category>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/7</guid>
      <comments>https://think-yk.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 26 Apr 2026 23:29:50 +0900</pubDate>
    </item>
    <item>
      <title>10주간의 배운 과정을 돌아보며</title>
      <link>https://think-yk.tistory.com/6</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;1주차 &amp;mdash; 테스트 코드 &amp;amp; TDD&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD를 배우기 위해 돈까지 냈던 적이 있다. 그러면서도 막상 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;그런데 AI가 등장하면서 분위기가 조금 바뀐 것 같다. 반복적이고 귀찮은 테스트 코드 작성을 AI가 상당 부분 대신해주니, 진입 장벽이 낮아졌다. 덕분에 이번 주차는 예전보다 훨씬 편하게 테스트를 먼저 써볼 수 있었다. 그리고 막상 과제를 하면서 테스트를 먼저 쓰는 게 단순히 순서 문제가 아니라는 걸 다시 한번 느꼈다. 구현보다 먼저 어떻게 동작해야 하는가를 정의하다 보면, 설계가 얼마나 테스트하기 어렵게 짜여졌는지가 바로 드러난다. 의존성을 내부에서 직접 생성하거나, 한 함수에 책임이 너무 많으면 테스트 자체가 불가능했다. 좋은 테스트를 쓰려면 결국 좋은 구조를 만들 수밖에 없다는 걸, 머리가 아닌 손으로 느낀 주차였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2주차 &amp;mdash; 소프트웨어 디자인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 시작하기 전에 문서를 먼저 작성하는 것. 말로는 알고 있었지만 실천은 잘 못했다. 현재 회사도 문서 문화가 아쉽게도 조금 부족한 편이고, 나 역시 나름대로 작성하려고 했지만 돌아보면 부실하게 넘어간 부분들이 꽤 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차를 공부하면서 내가 무엇을 빠뜨리고 있었는지가 보이기 시작했다. 시퀀스 다이어그램, ERD, 유비쿼터스 언어. 이것들이 단순히 문서를 잘 쓰기 위한 도구가 아니라, 팀 전체가 같은 언어로 같은 개념을 이야기하기 위한 기반이라는 걸 알게 됐다. 나중에 &amp;nbsp;&quot;제가 요청한 것과 다른데요?&quot;라는 말을 듣지 않으려면, 코드 이전에 이 언어가 맞춰져 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후로 개발 업무를 시작하기 전에 문서를 먼저 작성하는 습관이 생겼다. 테스트 코드처럼 문서 작성도 AI의 도움으로 생각보다 수월해졌는데, 이것도 루퍼스 부트캠프에서 얻은 팁 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3주차 &amp;mdash; 도메인 모델링 &amp;amp; 아키텍쳐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델링도 중요했지만, 이번 주차에서 더 인상 깊었던 건 레이어드 아키텍처였다. Interfaces &amp;rarr; Application &amp;rarr; Domain &amp;rarr; Infrastructure. 계층을 나누는 건 알고 있었는데, 각 계층이 왜 그 방향으로만 의존해야 하는지는 제대로 생각해본 적이 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 DIP, 의존성 역전 원칙이었다. Domain이 Infrastructure를 직접 의존하면, DB나 외부 기술이 바뀔 때 비즈니스 로직까지 흔들린다. 반대로 Domain에 인터페이스를 두고 Infrastructure가 그것을 구현하도록 뒤집으면, 도메인은 외부 기술로부터 완전히 독립된다. 테스트할 때 FakeRepository 로 갈아끼울 수 있는 것도 이 구조 덕분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 구조는 Spring을 쓰면서 어느 정도 자연스럽게 따라가고 있었다. 그런데 &quot;왜 이렇게 해야 하는가&quot;를 명확히 설명할 수 없었다. 이번 주차 덕분에 내가 그동안 관성적으로 써온 패턴에 이유가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인을 설계하면서 한 가지 선택지가 생겼다. 도메인 모델을 @Entity로 쓸 것인가, 아니면 JPA에 의존하지 않는 순수 자바 객체로 쓸 것인가. 나는 후자를 선택했다. @Entity를 쓰면 편리하지만, 도메인 객체가 JPA 어노테이션에 종속되고 테스트도 그만큼 무거워진다. 순수 자바 객체로 도메인을 만들면 Entity, Value Object, Domain Service 각각의 책임이 더 명확해지고, Spring 없이도 도메인 로직을 테스트할 수 있다. 다만 코드 분량이 확실히 늘어난다는 트레이드오프가 있었다. 그래도 양쪽의 장단점을 직접 부딪히며 알게 된 게 수확이었다. 상황에 따라 선택할 수 있는 근거가 생겼으니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4주차 &amp;mdash; Transactional Operation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 예전엔 동시성 이슈가 언급되면 &quot;비관적 락이나 낙관적 락 쓰면 되는 거 아니야?&quot;라고 단순하게 생각했다. 해결책의 이름은 알았지만, 왜 그 문제가 발생하는지는 제대로 생각해본 적이 없었다. 이번 주차에서 그 근본 원인을 처음 제대로 짚었다. Read-Modify-Write 패턴. 값을 읽고, 수정하고, 다시 쓰는 이 세 단계가 원자적으로 처리되지 않을 때 동시성 문제가 생긴다. 원인을 알고 나니 락 전략이 왜 필요한지도 비로소 납득이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;낙관적 락과 비관적 락을 비교하면서 느낀 건, 둘 중 하나가 정답이 아니라 &quot;이 자원이 얼마나 자주 충돌하는가&quot;, &quot;실패를 허용할 수 있는가&quot;에 따라 선택이 달라진다는 것이었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;그리고 동시성을 해결하는 방법이 비관적 락, 낙관적 락만이 아니라는 것도 알게 됐다. 원자적 쿼리를 사용하면 락 없이도 많은 동시성 문제를 해결할 수 있고, 구현도 비교적 간단하다. 오히려 락보다 먼저 고려해볼 만한 선택지라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점이 있다면, 동시성 해결에 집중하다 보니 @Transactional 을 깊게 공부하지 못했다는 것이다. 매우 중요한 개념이라는 걸 알기에, 수료 후 잘 작성된 3기 라이팅 과제들을 읽으며 따로 채워볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5주차 &amp;mdash; Practical Read Optimization&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 성능을 가장 크게 좌지우지하는 건 결국 조회다. 쓰기는 아무리 많아도 읽기의 몇 분의 일에 불과하고, 사용자가 체감하는 속도도 대부분 조회에서 갈린다. 그만큼 중요한 파트인데, 개인적으로는 쉬운 것 같으면서도 막히는 부분이 많아 묘하게 어려운 주차였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 현재 회사 서비스는 트래픽이 많지 않아서 조회 성능이 문제가 되는 경우가 별로 없었다. 그러다 보니 문제가 생기면 &quot;인덱스 걸면 되겠지&quot;라고 너무 가볍게 생각하고 넘어간 적이 많았다. 이번 주차를 통해 그 안일함이 깨졌다. 인덱스는 어디에나 걸면 되는 게 아니라, 어느 컬럼에, 어떤 순서로, 어떤 조회 패턴에 맞게 설계해야 하는지가 핵심이었다. 잘못 걸면 오히려 쓰기 성능을 갉아먹고, 심지어 인덱스가 있어도 안 쓰이는 경우도 있다는 걸 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6주차 &amp;mdash; Failure-Ready Systems&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timeout, Retry, Circuit Breaker. 이름은 들어봤지만 솔직히 굉장히 낯선 개념들이었다. 회사 서비스에도 외부 시스템을 호출하는 곳이 꽤 있는데, 그 중 어디에도 이런 장치가 제대로 갖춰져 있었는지 자신 없다. 그게 다행이라고 느껴지는 것 자체가, 이번 주차를 배우기 전의 내 상태였다. 공부하면서 느낀 건, 이게 엄청난 장애를 막는 거창한 개념이라기보다는 만약의 사태에 미리 대비하는 방법이라는 것이었다. 외부 시스템이 느려지거나 죽었을 때 사용자가 당황하지 않도록 우회할 수 있는 구조를 사전에 만들어두는 것. 장애가 터진 뒤에 수습하는 게 아니라, 터지기 전에 어떻게 동작할지를 설계해두는 태도에 가깝다고 느꼈다.&lt;br /&gt;&lt;br /&gt;한 가지 어려웠던 건 Timeout이나 실패율 같은 수치를 어떻게 설정해야 하는가였다. 팀원분들과 같은 기수분들이 성능 테스트하는 방법을 공유해 주셔서 방향을 잡을 수 있었는데, 덕분에 &quot;일단 보수적으로 잡고, 실측 데이터를 보면서 조정한다&quot;는 감각을&lt;br /&gt;얻었다.&lt;br /&gt;&lt;br /&gt;앞으로 외부 시스템을 연동할 일이 생기면, 기능 구현보다 이 장치들을 먼저 떠올릴 것 같다. 코드를 짜기 전에 &quot;이 호출이 실패하면 어떻게 할 것인가&quot;를 먼저 생각하는 것. 그게 이번 주차에 배운 점이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7주차 &amp;mdash; Hello, Event! Welcome, Kafka!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주차는 Kafka를 처음 써봤다. Kafka 자체도 낯설었지만, 브로커, 파티션, 컨슈머 그룹 같은 내부 개념들이 한꺼번에 쏟아지니 하나하나 파고들기보다는 전체적인 맥락을 잡는 데 집중했다. 어떤 상황에서 Kafka를 써야 하는지, 왜 단순한 메시지 큐가 아닌지를 이해하는 것만으로도 충분히 의미 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Kafka는 규모가 크지 않은 회사에서는 쓸 일이 많지 않다. 그럼에도 루퍼스를 통해 EDA 구조를 간접적으로 경험할 수 있어서 재밌었다. 대규모 트래픽에서 트랜잭션을 어떻게 나누고, 서비스 간 결합도를 어떻게 낮추는지를 직접 구현해보는 경험은 쉽게 얻기 어려운 것이었다. 특히 이벤트를 분리하면 새로운 후속 처리가 필요할 때 기존 코드를 건드리지 않고 리스너만 추가하면 된다는 것, 그게 곧 확장성이라는 걸 이번 주차에서 처음 체감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 더 와닿았던 건 ApplicationEvent였다. 마침 회사 프로젝트에서도 회원 포인트 처리 쪽에 ApplicationEvent를 쓰는 부분이 있는데, 이번 주차를 배우고 나서 어떤 부분을 개선하면 좋을지가 보이기 시작했다. 곧 반영할 계획이다. Outbox Pattern도 처음 들은 개념이었는데, 이벤트 유실 없이 발행을 보장하는 방법이라는 점에서 중요하다는 게 느껴졌다. 당장 쓸 일이 없더라도 기억해두고 싶은 개념이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8주차 &amp;mdash; Please Wait. You&amp;rsquo;re In The Queue.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 가장 재밌게 공부했던 챕터다. 대기열은 티켓팅이나 한정 상품 구매처럼 일상에서 자주 마주치는 개념인데, 그걸 직접 구현해본다는 게 색달랐다. 사용자 입장에서는 그냥 &quot;기다리는 화면&quot;이지만, 그 안에 이렇게 많은 설계가 담겨 있다는 걸 간접적으로 경험한 것만으로도 기억에 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 몰릴 때 &quot;나중에 다시 시도하세요&quot;를 반환하면, 사용자는 오히려 더 자주 새로고침을 누른다. 대기열은 그 악순환을 끊는 구조였다. 순번을 보여주고 예상 대기 시간을 알려주는 것만으로 사람들이 기다린다는 것, 그게 단순한 기술이 아니라 사용자 경험에 대한 배려라는 걸 느꼈다. 처리 속도를 높이는 것보다, 흐름을 제어하는 것이 더 중요할 때가 있다는 것도 이번 주차에서 얻은 시각이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주차에서 Redis의 매력도 새롭게 와닿았다. 지금까지는 &quot;Redis는 캐시&quot;라는 개념으로만 알고 있었는데, Sorted Set으로 순번을 관리하고 TTL로 토큰을 자동 만료시키는 구조를 직접 만들어보니 Redis가 단순한 캐시 저장소가 아니라 상황에 따라 다양하게 활용할 수 있는 도구라는 게 실감났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9주차 &amp;mdash; Show Me The Ranking&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8주차에 이어 Redis의 막강함을 다시 한번 느낀 주차였다. Sorted Set 하나로 실시간 랭킹을 관리할 수 있다는 게 여전히 인상적이었고, Redis가 단순한 캐시를 넘어 얼마나 다양한 문제를 풀 수 있는 도구인지 점점 체감이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주차에서 멘토님이 하신 말씀 중에 가장 기억에 남는 말은 &quot;랭킹 시스템은 인기 많은 상품을 정렬하는 게 아니라, 우리 서비스에서 '인기'가 무엇인지를 먼저 정의하는 것이 시작이다&quot;라는 것이었다. 단순히 좋아요 수가 많은 게 인기인지, 판매량인지, 최근 조회수인지. 그 정의에 따라 가중치와 수식이 달라진다. 생각해보면 당연한 말인데, 막상 직접 수식을 산정하려니 쉽지 않았다. 어떤 지표에 얼마의 가중치를 줘야 하는지에 정답이 없다는 게 오히려 어렵게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜드 스타트 문제도 흥미로웠다. 랭킹 윈도우가 바뀌는 순간 모든 점수가 0으로 리셋되면, 아무 상품도 상위에 오를 수 없다. 전날 점수의 일부를 이월해서 자연스럽게 채워주는 방식이 단순하면서도 영리하다고 느꼈다. 랭킹 시스템만이 가진 고유한 문제를 해결하는 방법이라 재밌게 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10주차 &amp;mdash; Collect, Stack, Zip&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch도 Kafka처럼 처음 써보는 기술이었다. 솔직히 &quot;우리 회사에서 이걸 쓸 일이 있을까?&quot; 라는 생각이 먼저 들었다. 세부적으로 파고들면 어려운 개념들이 많아서, 이번에도 Job, Step, Chunk, Tasklet 같은 전체적인 컨셉을 이해하는 데 집중했다. 언젠가 쓸 일이 생겼을 때 &quot;아, 이런 구조였지&quot;라고 떠올릴 수 있을 만큼만.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 걸 실시간으로 처리하고 싶었지만, 수백만 건 데이터를 매 요청마다 집계하는 건 DB를 혹사시키는 일이다. 실시간과 배치를 잘 나누는 것도 설계 능력이라는 걸 알게 됐다. 지금 꼭 실시간이어야 하는지, 하루 한 번 새벽에 계산해도 충분한지. 그 판단 하나가 시스템의 복잡도와 비용을 크게 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 챕터에서도 언급됐던 Materialized View가 이번에도 다시 등장했다. 어려운 개념은 아니지만, 복잡한 집계 쿼리를 미리 계산해 저장해두는 이 방식이 적재적소에 잘 쓰이면 서비스 성능에 꽤 큰 도움이 될 것 같다는 생각이 들었다. 단순한 아이디어지만, 언제 어디에 쓸지를 판단하는 게 핵심이라는 점에서 다른 개념들과 닮아있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보너스 &amp;mdash; 클로드 코드, AI를 쓰는 법을 배우다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루퍼스 부트캠프를 수강하면서 Claude Code를 처음 써봤다. 솔직히 처음엔 그냥 &quot;AI 코딩 도구 하나 더 쓰는 거겠지&quot;라고 가볍게 생각했다. 그런데 멘토분들과 수강생분들이 각자의 팁을 아낌없이 공유해주면서, AI를 잘 쓴다는 게 단순히 도구를 아는 것이 아니라 어떻게 활용하느냐의 문제라는 걸 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 AI 사용법에 대한 시야가 넓어졌다. 틈틈이 &quot;어떻게 하면 AI를 더 잘 쓸 수 있을까&quot;를 찾아보게 됐고, 회사에서도 개인적으로 업무 효율을 높이기 위한 AI 워크플로우를 만들어보고 있다. 아직 완성된 건 아니지만, 그런 시도를 자연스럽게 하게 된 것 자체가 루퍼스가 만들어준 변화라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;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;&lt;br /&gt;그 태도를 함께 얻었다.&amp;nbsp;루퍼스에서&amp;nbsp;보낸&amp;nbsp;시간이&amp;nbsp;앞으로도&amp;nbsp;오래&amp;nbsp;남을&amp;nbsp;것&amp;nbsp;같다.&lt;br /&gt;&lt;a href=&quot;https://www.loopers.im/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;루퍼스&lt;/a&gt;, 진심으로 감사합니다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/6</guid>
      <comments>https://think-yk.tistory.com/6#entry6comment</comments>
      <pubDate>Fri, 17 Apr 2026 09:41:22 +0900</pubDate>
    </item>
    <item>
      <title>인기를 정의하면 공식이 보인다</title>
      <link>https://think-yk.tistory.com/5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제 요구사항에 &lt;u&gt;&lt;b&gt;인기 상품 랭킹 만들기&lt;/b&gt;&lt;/u&gt;&amp;nbsp;라는 요구사항이 있었다&lt;br&gt;&amp;nbsp;&lt;br&gt;처음에는 공식부터 고르면 되는 줄 알았다. 그래서 주문된 상품 갯수를 기준으로 사용할지, 매출을 사용할지 , 합쳐서 사용할지 정하기만 하면 끝이라고 생각했다.&lt;br&gt;&lt;br&gt;그런데 같은 데이터를 넣어보니 공식마다 1위가 달랐다. &lt;br&gt;볼펜이 1위가 되기도 했고, 명품백이 1위가 되기도 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;멘토링 시간에도 &lt;u&gt;&lt;b&gt;인기라는 정의를 우선하고 코드를 구현하는 것이다&lt;/b&gt;&lt;/u&gt;&amp;nbsp;라는 멘토님의 조언처럼&lt;br&gt;랭킹 공식은 먼저 고르는 것이 아니었다. 우리 서비스에서 &lt;u&gt;&lt;b&gt;인기를 무엇으로 볼지&lt;/b&gt;&lt;/u&gt; 정해야 공식도 정할 수 있었다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 세 가지 인기의 정의&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;공식을 고르기 전에 먼저 정의부터 나누고, 실험을 했다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실제 내 프로젝트에서는 세 가지 이벤트가 랭킹 점수에 기여하는데, 주문 이벤트의 weight는 0.7로 설정하였기 때문에 실험도 동일하게 0.7로 하였다&amp;nbsp; &amp;nbsp;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의 A) 많은 사람이 선택한 상품이 인기 상품이다&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;100명이 각자 1개씩 구매한 상품이, 1명이 100개를 한 번에 구매한 상품보다 더 인기 있다고 본다.&lt;br&gt;중요한 것은 구매 금액이 아니라 독립적인 선택의 수다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;double f1(int quantity) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return 0.7 * Math.log1p(quantity);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기서 log1p를 쓴 이유가 있다. 수량에 단순 선형 가중치를 쓰면 대량 구매 1건이 랭킹을 독점할 수 있다. log1p는 수량이 늘어날수록 점수 증가폭을 줄여준다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 110px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;수량&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;점수&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;직전 대비 증가율&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;0.49&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;10&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1.68&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;3.46x&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;100&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;3.23&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1.92.x&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1,000&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;4.84&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1.50x&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;100,000&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;8.06&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;1.25x&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;수량이 100,000배 늘어나도 점수는 16.6배 정도만 오른다. 대량 주문의 영향은 인정하되, 과도하게 반영되지는 않는다. 덕분에 F1은 &quot;다수의 독립적인 선택&quot;을 더 강한 신호로 보려는 정의 A의 의도를 유지할 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의 B) 매출이 높은 상품이 인기 상품이다&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;비즈니스 기여도가 곧 인기다.&lt;br&gt;1개에 200만원짜리나 1,000개에 2천원짜리나 총 매출이 같으면 같은 인기다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;double f2(int quantity, int price) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return 0.7 * Math.log1p((long) quantity * price);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의 C) 고가 상품도 공정하게 경쟁해야 한다&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;명품백 1개를 판 것과 볼펜 1,000개를 판 것은 다르다.&lt;br&gt;가격대가 높은 상품에도 별도의 존재감을 인정해줘야 한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;double f3(int quantity, int price) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return 0.7 * (Math.log1p(quantity) + Math.log1p(price));
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;세 정의 모두 논리적이다.&lt;br&gt;문제는 &lt;u&gt;&lt;b&gt;같은 데이터에서도 결과가 달라진다&lt;/b&gt;&lt;/u&gt;는 점이다.&lt;/p&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 실험&lt;/b&gt;&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 1 : 같은 매출인데 1위가 바뀐다&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;세 공식이 얼마나 다른 결과를 내는지 먼저 확인했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 &lt;u&gt;&lt;b&gt;총 매출이 같은 조건&lt;/b&gt;&lt;/u&gt;을 골랐다. 매출이 같으면 비즈니스 기여도가 동등한 상품들이다. 이 상품들을 세 공식에 넣었을 때 1위가 달라진다면, 그건 &quot;공식은 중립적이지 않다&quot;는 증거가 된다.&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;상품&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;수량&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;단가&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;총 매출&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;명품백&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;노트북&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2개&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;1,000,000원&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;운동화&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10개&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;200,000원&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;책&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;100개&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;20,000원&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;볼펜&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;1,000개&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000원&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;2,000,000원&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;상품&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;F1 (수량)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;F2 (매출)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;F3 (혼합)&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;명품백&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;0.49&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.64&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;노트북&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;0.77&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.44&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;운동화&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;1.68&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.22&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;책&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;3.23&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;볼펜&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;4.84&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;10.16&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;랭킹은 이렇게 갈렸다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;F1 &lt;b&gt;:&lt;/b&gt; 볼펜 &amp;gt; 책 &amp;gt; 운동화 &amp;gt; 노트북 &amp;gt; 명품백&lt;/li&gt;&lt;li&gt;F2 &lt;b&gt;:&lt;/b&gt; 전부 동점&lt;/li&gt;&lt;li&gt;F3&lt;b&gt; :&lt;/b&gt; 명품백 &amp;gt; 노트북 &amp;gt; 운동화 &amp;gt; 책 &amp;gt; 볼펜&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;같은 200만원 매출인데, 공식 하나 바꿨더니 꼴찌가 1위가 됐다.&lt;br&gt;F2가 전부 동점인 이유는 수식을 보면 바로 보인다.&lt;br&gt;log1p(quantity × price)는 quantity × price가 모두 200만으로 같으니 결과도 같다.&lt;br&gt;즉, F2는 &quot;총 매출이 같으면 같은 인기&quot;라는 정의를 그대로 구현한 것이고, 얼마짜리를 몇 개 팔았는지는 관심이 없다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;u&gt;&lt;b&gt;공식은 중립적이지 않다. 각 공식은 각기 다른 인기의 정의를 반영한다&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 2 : F1은 정의 A의 의도대로 동작하는가?&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;실험1에서 공식마다 결과가 다르다는 걸 확인했다. 그렇다면 우리가 선택하려는 F1이 실제로 정의 A의 의도대로 동작하는지 확인해야한다.&amp;nbsp;&lt;br&gt;정의 A는 &quot;많은 사람이 독립적으로 선택한 상품이 인기 상품&quot;이라 봤다. 총 판매량은 같지만 주문 패턴이 다른 세 상품을 F1에 넣어봤다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;상품 A: 1명이 100개를 한 번에 주문&lt;/li&gt;&lt;li&gt;상품 B: 10명이 10개씩 주문&lt;/li&gt;&lt;li&gt;상품 C: 100명이 1개씩 주문&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;총 판매량은 모두 100개로 같다. F1에 넣으면 점수는 이렇게 나온다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;double scoreA = f1(100);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 1건 × 100개
double scoreB = 10 * f1(10);&amp;nbsp;&amp;nbsp; // 10건 × 10개
double scoreC = 100 * f1(1);&amp;nbsp;&amp;nbsp; // 100건 × 1개&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;상품&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;점수&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 50%;&quot;&gt;A (1건 x 100개)&lt;/td&gt;&lt;td style=&quot;width: 50%;&quot;&gt;3.2점&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 50%;&quot;&gt;B (10건 x 10개)&lt;/td&gt;&lt;td style=&quot;width: 50%;&quot;&gt;16.8점&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 50%;&quot;&gt;C (100건 x 1개)&lt;/td&gt;&lt;td style=&quot;width: 50%;&quot;&gt;48.5점&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;같은 100개인데도,&amp;nbsp;&lt;u&gt;&lt;b&gt;C가 A보다 15배 높다.&lt;/b&gt;&lt;/u&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;100명이 각자 선택한 상품이 1명이 대량 구매한 상품보다 훨씬 높은 점수를 받는다. F1은 정의 A의 의도대로 동작한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;단, B2B처럼 대량 구매 자체가 핵심인 서비스라면 이야기가 달라진다. 그 경우에는 F1이 아니라 선형 수량 공식이 더 맞을 수도 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 3: 현실 데이터에서도 공식마다 순위가 갈리는가?&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 실험들은 &quot;매출만 같게&quot; 같은 통제된 조건을 썼다. 하지만 현실에서는 상품마다 주문 건수도 다르고, 건당 수량도 다르고, 가격도 다르다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;아래는 하루 주문 패턴을 가정해서 직접 구성한 가상 데이터다.&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 160px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;상품&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;주문건수&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;건당 수량&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;단가&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;F1&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;F2&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;&lt;b&gt;F3&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;명품시계&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;2건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;5,000,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;0.97&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;21.59&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;22.56&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;노트북&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;4건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1,500,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1.94&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;39.82&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;41.76&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;커피머신&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;6건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;500,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;2.91&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;55.11&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;58.03&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;운동화&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;8건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;150,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;3.88&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;66.74&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;70.62&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;사무용품박스&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;5건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;50개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;3,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;13.76&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;41.71&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;41.78&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;티셔츠&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;20건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;2개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;50,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;15.38&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;161.18&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;166.86&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;height: 20px;&quot;&gt;책&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;30건&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;1개&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;20,000원&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;14.55&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;208.17&lt;/td&gt;&lt;td style=&quot;height: 20px;&quot;&gt;222.53&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&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;F1&lt;b&gt; :&lt;/b&gt; 티셔츠 &amp;gt; 책 &amp;gt; 사무용품박스 &amp;gt; 운동화 &amp;gt; 커피머신 &amp;gt; 노트북 &amp;gt; 명품시계&lt;/li&gt;&lt;li&gt;F2 &lt;b&gt;:&lt;/b&gt; 책 &amp;gt; 티셔츠 &amp;gt; 운동화 &amp;gt; 커피머신 &amp;gt; 사무용품박스 &amp;gt; 노트북 &amp;gt; 명품시계&lt;/li&gt;&lt;li&gt;F3&lt;b&gt; :&lt;/b&gt; 책 &amp;gt; 티셔츠 &amp;gt; 운동화 &amp;gt; 커피머신 &amp;gt; 사무용품박스 &amp;gt; 노트북 &amp;gt; 명품시계&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 표에서 보인 건 세 가지다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 티셔츠와 책은 공식에 따라 1위가 바뀐다&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;책은 주문 30건, 티셔츠는 주문 20건이다. 이벤트 수만 보면 책이 더 강하다.&lt;br&gt;그런데 F1에서는 티셔츠가 1위다. 건당 2개라는 수량이 반영되기 때문이다.&lt;br&gt;반대로 F2와 F3에서는 책이 다시 1위가 된다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 사무용품박스는 수량 중심 공식의 성격을 보여준다&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;사무용품박스는 F1에서 3위지만, F2와 F3에서는 5위로 내려간다.&lt;br&gt;1건에 50개씩 팔리기 때문에 수량 기반 공식에서는 강하지만, 매출과 가격을 함께 보기 시작하면 힘이 빠진다.&lt;br&gt;즉, F1은 “많이 담긴 주문”에 반응하고, F2/F3는 “얼마의 가치가 팔렸는가”에 더 민감하다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 명품시계는 공식보다 비교 기준의 한계를 드러낸다&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;명품시계는 단가가 가장 높지만 세 공식 모두에서 꼴찌다.&lt;br&gt;이건 공식이 틀렸다기보다, 카테고리 맥락 없이 상품을 같은 기준으로 비교한 한계에 가깝다.&lt;br&gt;책 30건과 명품시계 2건은 숫자로는 비교되지만, 실제 비즈니스 의미는 같지 않을 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 그래서 어떤 정의를 골랐나&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;정의를 다시 표로 정리하면 이렇다.&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;공식&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;많은 사람이 선택한 상품&lt;/td&gt;&lt;td&gt;F1: log1p(quantity)&lt;/td&gt;&lt;td&gt;주문 횟수 누적 = 다수 고객 신호&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;매출 기여도 기준&lt;/td&gt;&lt;td&gt;F2: log1p(quantity × price)&lt;/td&gt;&lt;td&gt;총 매출이 같으면 동등하게 취급&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;가격대 다양성 보장&lt;/td&gt;&lt;td&gt;F3: log1p(q) + log1p(p)&lt;/td&gt;&lt;td&gt;고가 소량 상품도 경쟁 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;B2B 대량 구매&lt;/td&gt;&lt;td&gt;선형 quantity&lt;/td&gt;&lt;td&gt;단일 대량 주문에 충분한 가중치 필요&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;나는 F1을 골랐다.&lt;br&gt;이 서비스는 일반 소비자가 대상인 커머스라고 가정하고 개발 했기 때문이다.&lt;br&gt;여기서 인기 상품이란 &quot;많은 사람이 독립적으로 선택한 상품&quot; 이어야한다. 1명이 대량 구매한 상품보다, 여러 사람이 각자 골라간 상품이 더 많은 사람에게 의미있는 추천이 된다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;정의를 먼저 택했고, 공식은 그 정의를 구현한 결과였다.&lt;br&gt;그리고 log1p는 그 정의가 대량 구매에 의해 왜곡되지 않도록 지켜주는 장치였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 얻은 결론은 단순하다.&lt;br&gt;랭킹 공식은 수학 문제가 아니었다. 정의를 먼저 정하고, 공식은 그 정의를 구현하는 것이다. log1p 조차도 수학적 선택이 아니라, &quot;대량 구매가 랭킹을 왜곡하지 않아야한다&quot;는 정의를 지키기 위한 장치였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;공식 선택 전에 먼저 해야할 질문은 이것이다.&lt;/p&gt;&lt;blockquote data-end=&quot;5914&quot; data-start=&quot;5890&quot; data-ke-style=&quot;style1&quot;&gt; 
 &lt;p data-end=&quot;5914&quot; data-start=&quot;5892&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우리 서비스에서 인기란 무엇인가?&lt;/b&gt;&lt;/p&gt; 
&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;정의가 바뀌면 공식도 바뀌어야 한다.&lt;br&gt;그리고 바꾸기 전에, 실제 데이터에 넣어보고 어떤 결과가 나오는지 먼저 확인해야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;끝&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/5</guid>
      <comments>https://think-yk.tistory.com/5#entry5comment</comments>
      <pubDate>Thu, 9 Apr 2026 23:58:46 +0900</pubDate>
    </item>
    <item>
      <title>대기열 순번 전달에서 SSE 대신 Polling을 선택한 이유</title>
      <link>https://think-yk.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 몰리는 순간 모든 사용자의 요청을 한 번에 처리하려고 하면, 애플리케이션과 DB가 순간 부하를 감당하지 못할 수 있다.&lt;br /&gt;이럴 때는 모든 요청을 즉시 처리하기보다, &lt;u&gt;&lt;b&gt;일정 수만 먼저 통과시키고 나머지는 대기시키는 방식&lt;/b&gt;&lt;/u&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-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후보는 크게 세 가지다. Polling, Long Polling, SSE.&lt;br /&gt;지연만 보면 Long Polling과 SSE가 더 좋아 보인다. 하지만 이 프로젝트에서는 &lt;u&gt;&lt;b&gt;Polling이 더 적절했다&lt;/b&gt;.&lt;/u&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;이 글에서는 세 가지 방식을 비교한 뒤, 왜 이 맥락에서 Polling을 선택했는지 정리한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;세가지 방식 비교&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 98px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 12.4419%; height: 18px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;Poliing&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.5814%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;Long Polling&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.4651%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;SSE&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 12.4419%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%; height: 20px; text-align: center;&quot;&gt;클라이언트가 주기적으로 질의&lt;/td&gt;
&lt;td style=&quot;width: 30.5814%; height: 20px; text-align: center;&quot;&gt;클라이언트 요청 후 서버가 변경 시점까지 응답 보류&lt;/td&gt;
&lt;td style=&quot;width: 25.4651%; height: 20px; text-align: center;&quot;&gt;서버가 변경 시점에 Push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 12.4419%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;구현 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%; height: 20px; text-align: center;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 30.5814%; height: 20px; text-align: center;&quot;&gt;중간&lt;/td&gt;
&lt;td style=&quot;width: 25.4651%; height: 20px; text-align: center;&quot;&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 12.4419%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;서버 부하&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%; height: 20px; text-align: center;&quot;&gt;대기 인원 x 조회 주기&lt;/td&gt;
&lt;td style=&quot;width: 30.5814%; height: 20px; text-align: center;&quot;&gt;대기 인원 x 1 커넥션 유지&lt;/td&gt;
&lt;td style=&quot;width: 25.4651%; height: 20px; text-align: center;&quot;&gt;대기 인원 x 1 커넥션 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 12.4419%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;지연&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%; height: 20px; text-align: center;&quot;&gt;조회 주기만큼&lt;/td&gt;
&lt;td style=&quot;width: 30.5814%; height: 20px; text-align: center;&quot;&gt;거의 없음&lt;/td&gt;
&lt;td style=&quot;width: 25.4651%; height: 20px; text-align: center;&quot;&gt;거의 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 방식의 차이는 단순히 &amp;ldquo;누가 먼저 요청하느냐&amp;rdquo; 정도가 아니다.&lt;br /&gt;&lt;u&gt;&lt;b&gt;서버가 상태를 얼마나 오래 들고 있어야 하는지&lt;/b&gt;, &lt;b&gt;수평 확장에서 무엇이 추가로 필요한지&lt;/b&gt;&lt;/u&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;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;Polling&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 일정 주기로 서버에 요청을 보내는 방식이다&lt;/p&gt;
&lt;pre id=&quot;code_1775194999664&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; GET /queue/position  (1초마다)
서버       &amp;rarr; { position: 128, estimatedWaitSeconds: 43 }
클라이언트 &amp;rarr; GET /queue/position
서버       &amp;rarr; { position: 105, estimatedWaitSeconds: 35 }
...
클라이언트 &amp;rarr; GET /queue/position
서버       &amp;rarr; { token: &quot;abc123&quot; }  &amp;larr; 입장 토큰 발급됨, 폴링 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;구현이 단순하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라잉너트는 타이머를 돌리고, 서버는 요청이 오면 Redis에서 현재 순번을 읽어 반환하면 된다. 서버에 별도 상태를 유지할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 단점도 분명하다. &lt;u&gt;&lt;b&gt;순번이 바뀌지 않아도 요청은 계속 들어온다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;1304&quot; data-start=&quot;1240&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 대기 인원이 1,000명이고 1초마다 조회한다면, 순번 조회만으로도 초당 1,000건의 요청이 발생한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Long Polling&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Long Polling은 클라이언트가 요청을 보내면 서버가 즉시 응답하지 않고, &lt;u&gt;&lt;b&gt;순번이 바뀌는 시점까지 응답을 보류&lt;/b&gt;&lt;/u&gt;하는 방식이다.&lt;/p&gt;
&lt;pre id=&quot;code_1775195241142&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; GET /queue/position
서버: 아직 변경 없음 &amp;rarr; 응답 보류 (커넥션 유지)
서버: 순번 변경 감지 &amp;rarr; 응답 반환
클라이언트: 응답 받으면 즉시 다음 요청&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;Polling보다 불필요한 요청 수를 줄일 수 있고, 변경이 발생하면 거의 즉시 응답할 수 있다.&lt;/p&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;단순히 요청을 오래 붙잡고 있다고 Long Polling이 되는 것은 아니다. 순번 변경이 발생했을 때 어떤 요청을 깨워 응답할지 관리할 수 있어야 한다. 결국 내부적으로는 &lt;u&gt;&lt;b&gt;변경 이벤트를 감지하고 대기 중인 요청에 연결하는 구조&lt;/b&gt;&lt;/u&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;즉, Long Polling은 Polling보다 실시간성은 좋지만, 그만큼 &lt;u&gt;&lt;b&gt;대기 요청 관리 비용&lt;/b&gt;&lt;/u&gt;과 &lt;u&gt;&lt;b&gt;이벤트 연결 복잡도&lt;/b&gt;&lt;/u&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;SSE (Server-Sent Events)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 서버가 클라이언트로 이벤트를 push하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 한 번 연결을 맺고 나면, 서버가 순번 변경이나 토큰 발급 시점에 데이터를 내려보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1775195474774&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; GET /queue/stream  (EventSource 연결)
서버       &amp;larr; data: { position: 128 }
서버       &amp;larr; data: { position: 105 }
서버       &amp;larr; data: { token: &quot;abc123&quot; }
클라이언트: 커넥션 종료, 주문 API 호출&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;ldquo;변경이 생기면 바로 알려준다&amp;rdquo;는 요구와도 잘 맞는다.&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;문제는 SSE가 단순히 응답 형식 하나를 바꾸는 선택이 아니라는 점이다.&lt;br /&gt;SSE를 선택하는 순간, 순번 조회 문제는 &lt;u&gt;&lt;b&gt;장시간 커넥션 유지&lt;/b&gt;, &lt;b&gt;로드밸런서 설정&lt;/b&gt;, &lt;b&gt;인스턴스 간 이벤트 전파&lt;/b&gt;&lt;/u&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;SSE를 선택하지 않은 이유&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SSE가 더 실시간에 가까운데 왜 Polling을 선택했을까?&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;1. 로드밸런서와 idle timeout 문제&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 HTTP 요청은 요청과 응답이 끝나면 연결이 바로 닫힌다.&lt;/p&gt;
&lt;pre id=&quot;code_1775195828742&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; [로드밸런서] &amp;rarr; 서버
              요청 &amp;rarr; 응답 &amp;rarr; 종료 (수십 ms)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;반면 SSE는 연결을 오래 유지해야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775195881412&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; [로드밸런서] &amp;rarr; 서버
              연결 유지 중... (수분~수십분)
                    &amp;darr;
              로드밸런서: &quot;60초 지났는데 아무 응답도 없네? 죽은 연결이구나&quot; &amp;rarr; 강제 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 로드밸런서가 일정 시간 동안 트래픽이 없으면 해당 연결을 idle 상태로 보고 끊어버릴 수 있다는 점이다. 예를 들어 AWS ALB의 기본 idle timeout은 60초다. 대기열 유저는 수분 이상 기다릴 수 있는데, 그동안 아무 이벤트가 없으면 SSE 연결은 중간에 종료될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이를 막으려면 두 가지가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LB timeout 연장 : 로드밸런서의 idle timeout을 서비스에 맞게 늘린다 (예: 5분)&lt;/li&gt;
&lt;li&gt;Heartbeat : 서버가 주기적으로 빈 메시지(:\n\n)를 보내 &quot;아직 살아있어&quot;를 알린다. 로드밸런서가 트래픽이 흐른다고 인식해 커넥션을 유지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 주기적으로 빈 이벤트를 보내 &amp;ldquo;연결이 살아 있다&amp;rdquo;는 신호를 줘야 하고, 인프라도 그 연결을 충분히 오래 유지하도록 설정해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;3081&quot; data-start=&quot;3030&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3081&quot; data-start=&quot;3030&quot; data-ke-size=&quot;size16&quot;&gt;즉, SSE는 단순한 실시간 전송 방식이 아니라 &lt;u&gt;&lt;b&gt;연결 유지 전략까지 포함한 선택&lt;/b&gt;&lt;/u&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;2. 수평 확장에서 이벤트 전파 문제가 생긴다&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;서버 인스턴스가 하나일 때는 문제없다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;스케줄러가 토큰을 발급하면,&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;해당 서버에 연결된 SSE 커넥션으로 바로 이벤트를 보내면 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775196069796&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[인스턴스 A]
  ├─ 유저 1의 SSE 커넥션
  ├─ 유저 2의 SSE 커넥션
  └─ 스케줄러: 유저 1 토큰 발급 &amp;rarr; 유저 1 커넥션에 push ✓&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;인스턴스가 여러 개로 늘어나면 문제가 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775196085218&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[인스턴스 A]              [인스턴스 B]
  └─ 유저 1의 SSE 커넥션    └─ 스케줄러: 유저 1 토큰 발급&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 1의 연결은 인스턴스 A에 있는데, 토큰 발급 로직은 인스턴스 B에서 실행될 수 있다.&lt;br /&gt;이 경우 인스턴스 B는 유저 1의 SSE 연결에 직접 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3617&quot; data-start=&quot;3497&quot; data-ke-size=&quot;size16&quot;&gt;결국 인스턴스 간에 &lt;u&gt;&lt;b&gt;이벤트를 전달할 수 있는 별도 채널&lt;/b&gt;&lt;/u&gt;이 필요하다. 예를 들어 Redis Pub/Sub 같은 구조를 두고, 토큰 발급 이벤트를 발행한 뒤 각 인스턴스가 자신에게 연결된 유저에게 전달해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1775196118818&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[인스턴스 B] 토큰 발급 &amp;rarr; Redis Pub/Sub 채널에 발행
[인스턴스 A] 구독 중 &amp;rarr; 유저 1 토큰 수신 &amp;rarr; 유저 1 SSE 커넥션에 push ✓&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;즉, SSE를 도입하면 단순한 순번 전달이 아니라 &lt;u&gt;&lt;b&gt;이벤트 브로커 구성과 인스턴스 간 라우팅 문제&lt;/b&gt;&lt;/u&gt;까지 함께 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;다만 SSE는 서버에서 클라이언트로 이벤트를 지속적으로 전달해야 하는 경우에 매우 잘 맞는다. &lt;br /&gt;알림, 진행 상태 스트리밍, 실시간 모니터링처럼 단방향 Push가 핵심인 문제에서는 Polling보다 훨씬 자연스러운 모델이 된다.&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-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3808&quot; data-start=&quot;3781&quot; data-section-id=&quot;yjrefj&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Long Polling을 선택하지 않은 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Long Polling도 Polling보다 실시간성이 좋다.&lt;br /&gt;하지만 이 프로젝트에서는 그 이점이 결정적이지 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;3876&quot; data-start=&quot;3810&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4020&quot; data-start=&quot;3878&quot; data-ke-size=&quot;size16&quot;&gt;Long Polling은 요청 수를 줄일 수 있지만, 결국 서버가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;대기 요청을 붙잡고 있어야 하고&lt;/b&gt;&lt;/u&gt;, 순번 변경 시점에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;어떤 요청에 응답할지 관리&lt;/b&gt;&lt;/u&gt;해야 한다. 구현 난이도는 Polling보다 높고, 구조적으로는 이벤트 기반 감지가 필요해진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4020&quot; data-start=&quot;3878&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4087&quot; data-start=&quot;4022&quot; data-ke-size=&quot;size16&quot;&gt;반면 이 프로젝트의 순번 조회는 매우 단순했다.&lt;br /&gt;요청이 들어오면 Redis에서 현재 순번을 읽어 반환하면 끝이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4087&quot; data-start=&quot;4022&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4149&quot; data-start=&quot;4089&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;조회 비용이 충분히 낮은 상황에서 Long Polling의 복잡도를 감수할 이유가 크지 않았다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;4149&quot; data-start=&quot;4089&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;4149&quot; data-start=&quot;4089&quot; data-ke-style=&quot;style2&quot;&gt;다만 Long Polling은 변경이 자주 발생하지 않지만, 발생 시 즉시 반영이 필요한 경우에는 여전히 유효하다. &lt;br /&gt;SSE까지 도입할 정도는 아니지만, Polling의 불필요한 요청 수를 줄이고 싶을 때 좋은 절충안이 될 수 있다.&lt;/blockquote&gt;
&lt;p data-end=&quot;4470&quot; data-start=&quot;4431&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Polling으로도 충분했던 이유&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 순번 조회는 Redis ZRANK 한 번으로 끝난다.&lt;br /&gt;조회 비용은 매우 낮고, 응답도 빠르다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4246&quot; data-end=&quot;4262&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 남는 질문은 하나다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4264&quot; data-end=&quot;4295&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;굳이 더 복잡한 실시간 전송 구조가 필요한가?&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4264&quot; data-end=&quot;4295&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4297&quot; data-end=&quot;4429&quot; data-ke-size=&quot;size16&quot;&gt;이 시스템에서는 그렇지 않았다.&lt;br /&gt;대기열 순번은 주식 호가처럼 밀리초 단위 반응이 중요한 데이터가 아니다. 사용자는 &amp;ldquo;대략 지금 어느 정도 남았는지&amp;rdquo;를 확인하면 된다. 이 맥락에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;1~3초 수준의 지연은 충분히 허용 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4297&quot; data-end=&quot;4429&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4431&quot; data-end=&quot;4470&quot; data-ke-size=&quot;size16&quot;&gt;결국 이 문제는 실시간성 자체보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;복잡도 대비 이득&lt;/b&gt;&lt;/u&gt;의 문제였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4431&quot; data-end=&quot;4470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4431&quot; data-end=&quot;4470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Polling의 부하 문제와 적응형 폴링&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start; color: #333333;&quot;&gt;Polling의 단점은 대기 인원이 많을수록 요청이 많아진다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775196575773&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;대기 인원 1,000명 &amp;times; 1초 주기 = 초당 1,000건 (순번 조회만으로)
대기 인원 5,000명 &amp;times; 1초 주기 = 초당 5,000건&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;br /&gt;순번이 높을수록 입장까지 시간이 더 남아 있으므로, 조회 주기를 길게 잡아도 사용자 경험에는 큰 차이가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775196625111&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function pollPosition(userId) {
  const res = await fetch(`/api/v1/queue/position`, { headers: { 'X-User-Id': userId } });
  const { position, token } = await res.json();

  if (token) {
    startOrder(token); // 토큰 발급됨 &amp;rarr; 주문 진행
    return;
  }

  // 순번에 따라 다음 조회 주기 결정
  const delay = position &amp;lt;= 100 ? 1000
              : position &amp;lt;= 500 ? 3000
              : 5000;

  setTimeout(() =&amp;gt; pollPosition(userId), delay);
}&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-end=&quot;5245&quot; data-start=&quot;5222&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5245&quot; data-start=&quot;5222&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 대기 인원이 5,000명일 때:&lt;/p&gt;
&lt;pre id=&quot;code_1775196654684&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;대기 인원 5,000명
  순번 1~100   (100명) &amp;times; 1초  =  100건/s
  순번 101~500 (400명) &amp;times; 3초  =  133건/s
  순번 501~    (4500명) &amp;times; 5초 =  900건/s
  합계: 1,133건/s  (단일 주기 대비 77% 감소)&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;모든 유저가 1초마다 조회하는 경우의 5,000건/s와 비교하면, 요청 수를 크게 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling, Long Polling, SSE 중 무엇이 더 &amp;ldquo;좋은가&amp;rdquo;는 절대적인 문제가 아니다.&lt;br /&gt;중요한 것은 지금 시스템에서 무엇이 더 적절한가다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5681&quot; data-start=&quot;5535&quot; data-ke-size=&quot;size16&quot;&gt;Long Polling과 SSE는 더 실시간에 가깝지만, 그만큼 서버가 연결과 이벤트를 더 오래, 더 복잡하게 관리해야 한다. 특히 SSE는 로드밸런서 설정, heartbeat, 인스턴스 간 이벤트 전파까지 고려해야 하므로 단순한 전송 방식 이상의 선택이 된다.&lt;/p&gt;
&lt;p data-end=&quot;5681&quot; data-start=&quot;5535&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5825&quot; data-start=&quot;5683&quot; data-ke-size=&quot;size16&quot;&gt;반면 이 프로젝트에서는 순번 조회가 Redis 기반으로 충분히 빠르고, 수초의 지연도 허용 가능했다. 그렇다면 실시간성을 위해 구조를 복잡하게 만들기보다, &lt;u&gt;&lt;b&gt;단순한 Polling을 선택하고 필요하면 적응형 폴링으로 부하를 줄이는 쪽이 더 합리적&lt;/b&gt;&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;5884&quot; data-start=&quot;5827&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;5884&quot; data-start=&quot;5827&quot; data-ke-size=&quot;size16&quot;&gt;끝&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;5884&quot; data-start=&quot;5827&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/4</guid>
      <comments>https://think-yk.tistory.com/4#entry4comment</comments>
      <pubDate>Fri, 3 Apr 2026 16:24:16 +0900</pubDate>
    </item>
    <item>
      <title>외부 시스템이 포함된 흐름에서 성공을 어떻게 정의할까</title>
      <link>https://think-yk.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;118&quot; data-start=&quot;108&quot; data-section-id=&quot;2knlbm&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 개요&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;외부 시스템이 들어오면 성공의 기준부터 흔들린다.&lt;br /&gt;루퍼스에서 PG 연동 과제를 진행하면서 가장 먼저 부딪힌 것도 그 문제였다. 요청을 보냈다는 사실과 상태가 실제로 확정되었다는 사실은 다를 수 있기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;345&quot; data-ke-size=&quot;size16&quot;&gt;재밌게도 이 질문은, 실제로 이번 주 회사에서 회원 탈퇴 설계를 논의하면서 다시 돌아왔다. (아쉽게도 결론이 나진 않았다...)&lt;br /&gt;도메인은 다르지만 질문은 같았다. 우리 시스템 안의 작업이 끝났다는 사실만으로 정말 성공이라고 말할 수 있는가.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;▎&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;pg-simulator 연동에서 이 질문은 구체적인 형태로 나타났다. requestPayment()가 정상 응답을 받았다고 해서 결제가 완료된 것이 아니다. &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;가 콜백으로 최종 결과를 보내주기 전까지 결제 상태는 확정되지 않는다. 호출 성공과 상태 확정은 다른 시점에 일어난다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;회원 탈퇴도 마찬가지다.&lt;br /&gt;우리 시스템에서 회원 정보를 지우는 것만으로 탈퇴가 끝난 것인지, 아니면 애플&amp;middot;카카오 같은 외부 서비스의 revoke 또는 unlink까지 끝나야 탈퇴가 완료된 것인지 먼저 정해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;중요한 것은 delete 쿼리 자체가 아니다.&lt;/b&gt;&lt;/u&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt;내부 상태(내 서버)와 외부 상태(외부 서버)를 어디까지 함께 정리해야 성공으로 볼 것인지 정하는 일이다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;591&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 회원 탈퇴를 단순 삭제가 아니라, 외부 시스템까지 포함된 상태 정리 흐름으로 보고, 그 안에서 가능한 설계 선택지와 상태 모델을 정리해보려 한다.&lt;/p&gt;
&lt;h2 data-end=&quot;591&quot; data-start=&quot;492&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;78&quot; data-start=&quot;48&quot; data-section-id=&quot;1jdnep4&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 회원 탈퇴는 왜 단순한 delete가 아닌가&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1329&quot; data-ke-size=&quot;size16&quot;&gt;회원 탈퇴를 단순한 delete로 보면 문제는 쉬워 보인다.&lt;br /&gt;회원 데이터를 지우면 끝나는 일처럼 보이기 때문이다. 하지만 실제 서비스에서 탈퇴는 그렇게 끝나지 않는 경우가 많다.&lt;/p&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1329&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;874&quot; data-start=&quot;734&quot; data-ke-size=&quot;size16&quot;&gt;탈퇴에는 내부 데이터 삭제만 있는 것이 아니다.&lt;br /&gt;로그인 연동 해제, 외부 토큰 revoke, 제3자 서비스 unlink, 재가입 충돌 방지처럼 함께 정리해야 할 것들이 남는다. 이때부터 탈퇴는 단순 삭제가 아니라 여러 상태를 정리하는 흐름이 된다.&lt;/p&gt;
&lt;p data-end=&quot;874&quot; data-start=&quot;734&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;993&quot; data-start=&quot;876&quot; data-ke-size=&quot;size16&quot;&gt;문제는 이 흐름이 한 번의 요청으로 끝나지 않을 수 있다는 점이다.&lt;br /&gt;내부 데이터는 정리됐지만 외부 revoke가 실패할 수 있고, 외부 호출은 처리됐지만 우리 시스템은 아직 결과를 확정하지 못할 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;993&quot; data-start=&quot;876&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1130&quot; data-start=&quot;995&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서 탈퇴를 단순 delete로 취급하면 시스템은 실제 상태를 설명하지 못한다.&lt;br /&gt;사용자에게는 탈퇴 완료를 보여줬지만 외부 연동은 남아 있을 수 있고, 반대로 외부 연동은 끊겼지만 내부 상태는 아직 완료로 바뀌지 않았을 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;1130&quot; data-start=&quot;995&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1245&quot; data-start=&quot;1132&quot; data-ke-size=&quot;size16&quot;&gt;결국 회원 탈퇴에서 중요한 것은 데이터를 지웠는지가 아니다.&lt;br /&gt;내부와 외부의 정리 작업을 어디까지 완료해야 탈퇴가 끝났다고 볼 것인지, 그 기준을 시스템 안에 어떤 상태로 남길 것인지가 더 중요하다&lt;/p&gt;
&lt;h2 data-end=&quot;1245&quot; data-start=&quot;1132&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;78&quot; data-start=&quot;37&quot; data-section-id=&quot;4zhkdj&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 내부 삭제와 외부 revoke 중 어디까지를 성공으로 볼 것인가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1103&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;회원 탈퇴를 외부 시스템까지 포함된 흐름으로 본다면, 가장 먼저 정해야 하는 것은 구현 순서가 아니다.&lt;br /&gt;내부 데이터 정리와 외부 revoke 중 어디까지를 탈퇴 성공으로 볼 것인지부터 정해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1103&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1544&quot; data-start=&quot;1409&quot; data-ke-size=&quot;size16&quot;&gt;가장 단순한 기준은 둘 다 성공해야만 탈퇴 성공으로 보는 방식이다.&lt;br /&gt;우리 시스템 데이터도 정리되고 외부 서비스와의 연결도 끊겨야 탈퇴가 끝났다고 해석하는 것이다. 이 방식은 의미가 분명하다. 대신 외부 장애에 전체 탈퇴 흐름이 민감해진다.&lt;/p&gt;
&lt;p data-end=&quot;1544&quot; data-start=&quot;1409&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1704&quot; data-start=&quot;1546&quot; data-ke-size=&quot;size16&quot;&gt;반대로 내부 데이터 정리만 끝나면 우선 탈퇴 성공으로 보는 방식도 가능하다.&lt;br /&gt;이 경우 외부 revoke 실패는 후속 작업으로 넘긴다. 장점은 외부 시스템 문제 때문에 탈퇴 자체가 계속 막히지 않는다는 점이다. 대신 일정 시간 동안 내부 상태와 외부 상태의 불일치를 허용해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1704&quot; data-start=&quot;1546&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1804&quot; data-start=&quot;1706&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 문제는 어느 방식이 더 깔끔한지의 문제가 아니다.&lt;br /&gt;어떤 불일치를 허용할 것인지, 외부 장애를 어디까지 내부 기능의 실패로 받아들일 것인지 결정하는 문제에 가깝다.&lt;/p&gt;
&lt;p data-end=&quot;1804&quot; data-start=&quot;1706&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1831&quot; data-start=&quot;1806&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 질문이 먼저 정리되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1968&quot; data-start=&quot;1833&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1868&quot; data-start=&quot;1833&quot; data-section-id=&quot;1864xm2&quot;&gt;외부 revoke 실패 때문에 내부 탈퇴까지 막을 것인가&lt;/li&gt;
&lt;li data-end=&quot;1905&quot; data-start=&quot;1869&quot; data-section-id=&quot;r2rkml&quot;&gt;내부 탈퇴는 먼저 완료하고 외부 정리는 나중에 맞출 것인가&lt;/li&gt;
&lt;li data-end=&quot;1933&quot; data-start=&quot;1906&quot; data-section-id=&quot;ji215d&quot;&gt;일정 시간 상태 불일치를 허용할 수 있는가&lt;/li&gt;
&lt;li data-end=&quot;1968&quot; data-start=&quot;1934&quot; data-section-id=&quot;1hzqr6i&quot;&gt;사용자에게는 어느 시점을 기준으로 완료를 보여줄 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2116&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;이 기준이 정해져야 예외도 해석할 수 있다.&lt;br /&gt;외부 호출 실패를 곧바로 탈퇴 실패로 볼 수도 있고, 내부 탈퇴는 성공으로 두되 외부 정리만 재시도 대상으로 남길 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;2116&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;즉, 예외는 단순한 오류가 아니라 어떤 상태를 남기고 다음 처리를 어떻게 이어갈지 결정하게 만드는 신호에 가깝다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;2116&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;▎&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2116&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt; 연동에서 이 선택은 타임아웃 상황에서 명확하게 나타났다. &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;에 결제를 요청했는데 응답이 오지 않았을 때,&amp;nbsp;즉시 결제를 FAILED로 마킹할 것인지가 그 선택이었다. 타임아웃은 &quot;요청이 실패했다&quot;가 아니라 &quot;응답을 받지 못했다&quot;는 뜻이다. &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;가 이미 결제를 처리했을 수 있기 때문에, 즉시 실패로 확정하지 않고 PENDING으로 유지했다. 지금의 불일치를 허용하되 나중에 맞추겠다는 결정이었다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;127&quot; data-start=&quot;108&quot; data-section-id=&quot;1jnaobz&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 가능한 설계 선택지 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2045&quot; data-start=&quot;1968&quot; data-ke-size=&quot;size16&quot;&gt;내부 삭제와 외부 revoke를 어디까지 하나의 결과로 볼 것인지에 따라, 회원 탈퇴 흐름은 몇 가지 방식으로 나눌 수 있다. 중요한 것은 어떤 방식이 더 정답에 가깝냐가 아니라, 각 방식이 어떤 대가를 가지는지 아는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;2045&quot; data-start=&quot;1968&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2318&quot; data-start=&quot;2272&quot; data-section-id=&quot;xvpdwb&quot; data-ke-size=&quot;size20&quot;&gt;1) 내부 삭제와 외부 revoke가 모두 성공해야 탈퇴 성공으로 보는 방식&lt;/h4&gt;
&lt;p data-end=&quot;2363&quot; data-start=&quot;2320&quot; data-ke-size=&quot;size16&quot;&gt;가장 직관적이다.&lt;br /&gt;내부와 외부가 모두 정리되어야 탈퇴가 완료되었다고 본다.&lt;/p&gt;
&lt;p data-end=&quot;2363&quot; data-start=&quot;2320&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2424&quot; data-start=&quot;2365&quot; data-ke-size=&quot;size16&quot;&gt;장점은 상태 의미가 분명하다는 점이다.&lt;br /&gt;&amp;ldquo;탈퇴 완료&amp;rdquo;는 내부와 외부가 모두 정리된 상태라는 뜻이 된다.&lt;/p&gt;
&lt;p data-end=&quot;2424&quot; data-start=&quot;2365&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2532&quot; data-start=&quot;2426&quot; data-ke-size=&quot;size16&quot;&gt;단점은 외부 장애에 매우 민감하다는 점이다.&lt;br /&gt;내부 처리가 끝났어도 외부 revoke가 실패하면 탈퇴 전체를 완료로 볼 수 없다. 결국 외부 상태가 내부 기능의 완료 여부를 좌우하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;2532&quot; data-start=&quot;2426&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2591&quot; data-start=&quot;2534&quot; data-section-id=&quot;uz7sbg&quot; data-ke-size=&quot;size20&quot;&gt;2) 내부 삭제가 끝나면 우선 탈퇴 성공으로 보고, 외부 revoke는 후속 처리로 넘기는 방식&lt;/h4&gt;
&lt;p data-end=&quot;2685&quot; data-start=&quot;2593&quot; data-ke-size=&quot;size16&quot;&gt;우리 시스템 기준의 완료를 먼저 인정하는 방식이다.&lt;br /&gt;내부 데이터가 정리되면 사용자에게는 탈퇴 완료를 보여주고, 외부 revoke는 재시도나 후속 작업으로 넘긴다.&lt;/p&gt;
&lt;p data-end=&quot;2685&quot; data-start=&quot;2593&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2766&quot; data-start=&quot;2687&quot; data-ke-size=&quot;size16&quot;&gt;장점은 외부 시스템 문제 때문에 탈퇴가 계속 막히지 않는다는 점이다.&lt;br /&gt;반면 단점은 내부와 외부 상태가 일정 시간 어긋날 수 있다는 점이다.&lt;/p&gt;
&lt;p data-end=&quot;2766&quot; data-start=&quot;2687&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2857&quot; data-start=&quot;2768&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이 방식을 선택했다면, 외부 정리가 남아 있다는 사실을 시스템이 추적할 수 있어야 한다.&lt;br /&gt;그렇지 않으면 부분 성공 상태가 장기적으로 방치될 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;2857&quot; data-start=&quot;2768&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;▎&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2857&quot; data-start=&quot;2768&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt; 연동에서 이 방식을 선택했다. &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt; 호출이 타임아웃이나 서킷 브레이커로 막히더라도 결제를 즉시 FAILED로 확정하지 않고 PENDING으로 유지한다. 그리고 3분마다 실행되는 스케줄러가 PENDING 결제를 조회해 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;에 상태를 확인하고 동기화한다.&lt;br /&gt;&quot;지금의 불일치를 허용하되, 나중에 반드시 맞춘다&quot;는 구조다. 이 방식에서 중요한 것은 후속 처리 장치가 반드시 함께 있어야 한다는 점이다. 그렇지 않으면 PENDING 상태는 영원히 방치된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2894&quot; data-start=&quot;2859&quot; data-section-id=&quot;utefw4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2894&quot; data-start=&quot;2859&quot; data-section-id=&quot;utefw4&quot; data-ke-size=&quot;size20&quot;&gt;3) 중간 상태를 두고 최종 완료를 나중에 확정하는 방식&lt;/h4&gt;
&lt;p data-end=&quot;3033&quot; data-start=&quot;2896&quot; data-ke-size=&quot;size16&quot;&gt;성공과 실패를 바로 확정하지 않고 중간 상태를 두는 방식이다.&lt;br /&gt;예를 들어 탈퇴 요청이 들어오면 바로 DELETED로 바꾸지 않고, DELETING 같은 상태로 먼저 전이한 뒤 내부 정리와 외부 revoke가 모두 확인되면 최종 완료로 바꾼다.&lt;/p&gt;
&lt;p data-end=&quot;3033&quot; data-start=&quot;2896&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3120&quot; data-start=&quot;3035&quot; data-ke-size=&quot;size16&quot;&gt;장점은 시스템이 현재 상황을 더 정직하게 드러낼 수 있다는 점이다.&lt;br /&gt;아직 끝나지 않은 흐름을 억지로 성공이나 실패 중 하나로 밀어 넣지 않아도 된다.&lt;/p&gt;
&lt;p data-end=&quot;3120&quot; data-start=&quot;3035&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3183&quot; data-start=&quot;3122&quot; data-ke-size=&quot;size16&quot;&gt;대신 운영 복잡도는 올라간다.&lt;br /&gt;상태 전이 규칙, 재시도 정책, 운영 기준까지 함께 설계해야 하기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;3183&quot; data-start=&quot;3122&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3221&quot; data-start=&quot;3185&quot; data-ke-size=&quot;size16&quot;&gt;결국 세 방식의 차이는 구현 난이도보다 무엇을 우선하느냐에 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3290&quot; data-start=&quot;3223&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3242&quot; data-start=&quot;3223&quot; data-section-id=&quot;jqrv2b&quot;&gt;강한 정합성을 우선할 것인가&lt;/li&gt;
&lt;li data-end=&quot;3261&quot; data-start=&quot;3243&quot; data-section-id=&quot;1vudv5b&quot;&gt;빠른 완료를 우선할 것인가&lt;/li&gt;
&lt;li data-end=&quot;3290&quot; data-start=&quot;3262&quot; data-section-id=&quot;momrbq&quot;&gt;중간 상태를 드러내고, 나중에 최종 상태를 확정할 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 선택은 코드 스타일의 차이가 아니라 어떤 불일치를 허용하고 어떤 비용을 감당할 것인지 정하는 문제다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;138&quot; data-start=&quot;111&quot; data-section-id=&quot;1p3n4rv&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 상태 모델과 재시도는 어떻게 볼 것인가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1240&quot; data-start=&quot;1133&quot; data-ke-size=&quot;size16&quot;&gt;외부 시스템이 포함된 흐름에서는 성공과 실패를 한 번에 나누기 어렵다.&lt;br /&gt;내부 처리와 외부 revoke가 서로 다른 시점에 끝날 수 있다면, 중간 과정을 상태로 드러내는 편이 더 자연스럽다.&lt;/p&gt;
&lt;p data-end=&quot;1240&quot; data-start=&quot;1133&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3461&quot; data-start=&quot;3435&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 탈퇴를 아래처럼 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3637&quot; data-start=&quot;3463&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3484&quot; data-start=&quot;3463&quot; data-section-id=&quot;rch1kn&quot;&gt;&lt;b&gt;ACTIVE&lt;/b&gt;: 정상 상태&lt;/li&gt;
&lt;li data-end=&quot;3547&quot; data-start=&quot;3485&quot; data-section-id=&quot;db1clx&quot;&gt;&lt;b&gt;DELETING&lt;/b&gt;: 탈퇴 요청은 들어왔지만, 내부 정리나 외부 revoke가 아직 끝나지 않은 상태&lt;/li&gt;
&lt;li data-end=&quot;3584&quot; data-start=&quot;3548&quot; data-section-id=&quot;18ntrxa&quot;&gt;&lt;b&gt;DELETED&lt;/b&gt;: 내부와 외부 정리가 모두 끝난 상태&lt;/li&gt;
&lt;li data-end=&quot;3637&quot; data-start=&quot;3585&quot; data-section-id=&quot;wayv6u&quot;&gt;&lt;b&gt;DELETE_FAILED&lt;/b&gt;: 탈퇴 과정 중 일부가 실패해 추가 처리가 필요한 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;▎&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt; 연동에서 Payment의 PENDING 상태가 DELETING과 같은 역할을 한다. &quot;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;에 요청은 보냈지만 콜백이 오기 전까지 결과를 알 수 없다&quot;는 사실을 상태로 드러내는 것이다. 콜백이 도착하면 SUCCESS 또는 FAILED로 전이하고, 콜백이 오지 않으면 스케줄러가 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;에 직접 조회해서 상태를 확정한다. 억지로 즉시 성공이나 실패로 밀어 넣지 않아도 되는 이유가 이 중간 상태 덕분이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3710&quot; data-start=&quot;3639&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3710&quot; data-start=&quot;3639&quot; data-ke-size=&quot;size16&quot;&gt;이런 상태를 두는 이유는 단순하다.&lt;br /&gt;시스템이 지금 어디까지 끝났고, 무엇이 아직 남았는지를 설명할 수 있어야 하기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;3710&quot; data-start=&quot;3639&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3857&quot; data-start=&quot;3712&quot; data-ke-size=&quot;size16&quot;&gt;외부 revoke에서 예외가 발생했다고 해도 모든 경우를 같은 실패로 보면 안 된다.&lt;br /&gt;어떤 예외는 일시적이라 재시도할 가치가 있고, 어떤 예외는 이미 외부 상태가 반영되었을 가능성을 고려해야 하며, 어떤 예외는 요청 자체가 잘못되어 재시도해도 의미가 없다.&lt;/p&gt;
&lt;p data-end=&quot;3857&quot; data-start=&quot;3712&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3909&quot; data-start=&quot;3859&quot; data-ke-size=&quot;size16&quot;&gt;그래서 예외는 단순 오류가 아니라 &lt;u&gt;&lt;b&gt;다음 상태를 결정하게 만드는 신호&lt;/b&gt;&lt;/u&gt;로 다뤄야 한다.&lt;/p&gt;
&lt;p data-end=&quot;3909&quot; data-start=&quot;3859&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3917&quot; data-start=&quot;3911&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4087&quot; data-start=&quot;3919&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3974&quot; data-start=&quot;3919&quot; data-section-id=&quot;1q9yvk&quot;&gt;일시적인 외부 장애라면 &lt;b&gt;DELETING&lt;/b&gt; 상태를 유지하고 재시도 대상으로 남길 수 있다&lt;/li&gt;
&lt;li data-end=&quot;4031&quot; data-start=&quot;3975&quot; data-section-id=&quot;135eaqb&quot;&gt;재시도해도 의미가 없는 요청 오류라면 &lt;b&gt;DELETE_FAILED&lt;/b&gt;로 전이해 종료할 수 있다&lt;/li&gt;
&lt;li data-end=&quot;4087&quot; data-start=&quot;4032&quot; data-section-id=&quot;11cqx1m&quot;&gt;외부에서는 이미 revoke가 끝난 것으로 판단할 수 있다면 멱등하게 성공 처리할 수도 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;▎ &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(루퍼스 부트캠프 과제와 관련된 내용입니다, 읽지 않아도 무방)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt; 연동에서 예외를 이렇게 구분했다. ConnectException(연결 자체가 안 됨)과 HttpServerErrorException(PG 5xx)은 재시도 가능한 신호로 분류했고, SocketTimeoutException(타임아웃)은 재시도 불가로 구분했다. 타임아웃은 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;pg-simulator&lt;/span&gt;가 이미 처리했을 가능성이 있어 재시도하면 중복 결제가 발생할 수 있기 때문이다. 이를 위해 재시도 가능 여부를 담은 PgConnectionException이라는 커스텀 예외를 따로 만들었다. 예외를 잡는 것이 목적이 아니라, 이 예외가 &quot;다음에 무엇을&lt;br /&gt;해야 하는지&quot;를 결정하는 신호가 되도록 설계한 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4214&quot; data-start=&quot;4089&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4214&quot; data-start=&quot;4089&quot; data-ke-size=&quot;size16&quot;&gt;중요한 것은 상태 모델과 재시도 정책이 따로 떨어져 있지 않다는 점이다.&lt;br /&gt;어떤 상태를 둘 것인지 정해야 어떤 예외를 재시도 대상으로 남길지 결정할 수 있고, 어떤 예외를 허용할 것인지 정해야 상태 전이도 설계할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;4214&quot; data-start=&quot;4089&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4348&quot; data-start=&quot;4216&quot; data-ke-size=&quot;size16&quot;&gt;또 중간 상태를 두었다면, 그 상태를 최종 상태로 넘기는 장치도 함께 있어야 한다.&lt;br /&gt;DELETING 상태를 만들었다면, 재시도나 후속 확인을 통해 결국 최종 상태로 옮겨갈 수 있어야 한다. 중간 상태는 임시 보관이 아니라 관리 대상이어야 한다.&lt;/p&gt;
&lt;p data-end=&quot;4348&quot; data-start=&quot;4216&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4438&quot; data-start=&quot;4350&quot; data-ke-size=&quot;size16&quot;&gt;결국 상태 모델은 단순 분류가 아니다.&lt;br /&gt;지금 어디까지 끝났는지, 무엇이 남았는지, 다음에 무엇을 해야 하는지를 시스템이 일관되게 설명하게 만드는 기준이다.&lt;/p&gt;
&lt;h2 data-end=&quot;4438&quot; data-start=&quot;4350&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;58&quot; data-start=&quot;49&quot; data-section-id=&quot;106mjv1&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4955&quot; data-start=&quot;4798&quot; data-ke-size=&quot;size16&quot;&gt;회원 탈퇴는 겉으로 보면 단순한 delete처럼 보인다.&lt;br /&gt;하지만 외부 시스템이 함께 얽히는 순간, 문제는 데이터 삭제 하나로 닫히지 않는다. 어디까지를 성공으로 볼 것인지 먼저 정해야 하고, 그 기준에 따라 상태 모델과 예외 해석, 재시도 방식도 함께 달라진다.&lt;/p&gt;
&lt;p data-end=&quot;4955&quot; data-start=&quot;4798&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1538&quot; data-start=&quot;1470&quot; data-ke-size=&quot;size16&quot;&gt;결국 회원 탈퇴는 delete 한 번의 문제가 아니다.&lt;br /&gt;외부 시스템까지 포함된 흐름에서 성공의 경계를 설계하는 문제다.&lt;/p&gt;
&lt;p data-end=&quot;1538&quot; data-start=&quot;1470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1686&quot; data-start=&quot;1540&quot; data-ke-size=&quot;size16&quot;&gt;이 문제는 회원 탈퇴에만 있는 것도 아니다.&lt;br /&gt;외부 시스템이 포함되는 순간, 중요한 것은 호출 자체보다 성공의 기준과 상태를 어떻게 확정할 것인지다. &lt;u&gt;&lt;b&gt;결국 설계해야 하는 것은 API 호출 한 번이 아니라, 불일치를 어떻게 다루고 최종 상태를 어떻게 정리할 것인지다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;4955&quot; data-start=&quot;4798&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4955&quot; data-start=&quot;4798&quot; 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;&lt;/p&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/3</guid>
      <comments>https://think-yk.tistory.com/3#entry3comment</comments>
      <pubDate>Thu, 19 Mar 2026 23:55:26 +0900</pubDate>
    </item>
    <item>
      <title>&amp;ldquo;인덱스 걸면 빨라진다&amp;rdquo;는 말에 빠진 이야기들</title>
      <link>https://think-yk.tistory.com/2</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;0. 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트캠프에서 최근 상품 목록 API의 성능을 DB 인덱스로 최적화하는 과제가 있었다. 처음에는 인덱스를 그저 &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;nbsp;&lt;a href=&quot;https://velog.io/@seoki/%EC%BA%90%EC%8B%9C-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4%EC%9D%98-%EB%B0%98%EB%B3%B5&quot;&gt;캐시는 아이디어의 반복&lt;/a&gt;이라는 글을 읽게 됐다. CPU 캐시부터 Redis까지, 결국 느린 원본에 매번 직접 접근하지 않기 위해 더 가볍고 다루기 쉬운 구조를 따로 둔다는 내용이었다. 이 글을 읽고 나니 내가 테스트하던 인덱스도 비슷한 관점에서 볼 수 있겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;물론 인덱스는 캐시가 아니다. 캐시처럼 임시 저장소에 가깝지도 않고, 결과값 자체를 재사용하는 구조도 아니다. 다만 원본 전체를 반복해서 읽지 않기 위해 별도의 구조를 유지한다는 점에서는 분명 닮아 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1128&quot; data-start=&quot;1015&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 실험을 다시 해석해 보기로 했다. 단순히 &amp;ldquo;인덱스를 걸었더니 빨라졌다&amp;rdquo;가 아니라, &lt;u&gt;&lt;b&gt;인덱스는 정확히 어떤 비용을 줄이고, 그 성능은 어떤 대가 위에서 만들어지는가&lt;/b&gt;&lt;/u&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;style3&quot; /&gt;
&lt;h2 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 조회는 &amp;lsquo;찾기&amp;rsquo;만의 문제가 아니었다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;상품 목록 조회는 겉보기에는 단순하다. 특정 브랜드의 상품을 찾고, 좋아요 순으로 정렬한 뒤, 상위 몇 개만 가져오면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773325067209&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM product
WHERE brand_id = 10
ORDER BY like_count DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 병목이 WHERE brand_id = 10 같은 조건 탐색에 있다고 생각했다. 그래서 자연스럽게 brand_id에 인덱스를 걸면 되겠다고 접근했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로는 그게 전부가 아니었다. &lt;u&gt;&lt;b&gt;조회는 단순히 데이터를 찾는 문제가 아니라, 찾은 뒤 어떻게 정렬할 것인가의 문제&lt;/b&gt;&lt;/u&gt;까지 포함하고 있었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;테이블 전체를 읽는 것도 비싸지만, 그 뒤에 원하는 순서로 다시 줄 세우는 비용도 작지 않았다. 즉 내가 처음 보고 있던 문제는 절반짜리였다. 인덱스가 필요한 이유는 &amp;ldquo;조건에 맞는 데이터를 빨리 찾기 위해서&amp;rdquo;만이 아니라, 경우에 따라서는 &lt;u&gt;&lt;b&gt;정렬 비용을 줄이기 위해서&lt;/b&gt;&lt;/u&gt;이기도 했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 인덱스라는 왜 캐시처럼 느껴졌을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 캐시라고 부르는 건 정확하지 않다. 그래도 이번 실험을 하면서 인덱스가 자꾸 캐시처럼 느껴졌던 이유는 분명했다. &lt;u&gt;&lt;b&gt;원본 전체를 직접 다루는 비용이 너무 크기 때문&lt;/b&gt;&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 건짜리 상품 테이블이 있다고 해보자. 여기서 특정 브랜드의 상품을 찾고 다시 좋아요 순으로 정렬해야 한다면, 인덱스가 없는 DB는 테이블 전체를 읽고, 조건에 맞는 데이터를 골라낸 뒤, 다시 정렬해야 한다. 이건 결국 원본을 그대로 상대하는 비용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 인덱스는 자주 찾는 기준을 중심으로 탐색하기 좋은 구조를 따로 유지한다. 결과를 저장해두는 캐시는 아니지만, 적어도 원본 전체를 매번 처음부터 끝까지 읽는 일은 피하게 해준다.&lt;/p&gt;
&lt;p data-end=&quot;2067&quot; data-start=&quot;2046&quot; data-ke-size=&quot;size16&quot;&gt;이 점에서 인덱스는 캐시와 닮아 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2154&quot; data-start=&quot;2069&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2096&quot; data-start=&quot;2069&quot; data-section-id=&quot;5ysz5b&quot;&gt;CPU 캐시는 RAM까지 가는 비용을 줄인다.&lt;/li&gt;
&lt;li data-end=&quot;2122&quot; data-start=&quot;2097&quot; data-section-id=&quot;jhwyt0&quot;&gt;Redis는 DB까지 가는 비용을 줄인다.&lt;/li&gt;
&lt;li data-end=&quot;2154&quot; data-start=&quot;2123&quot; data-section-id=&quot;udp5s6&quot;&gt;인덱스는 테이블 전체를 읽고 정렬하는 비용을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2227&quot; data-start=&quot;2156&quot; 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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-path-to-node=&quot;13&quot;&gt;&lt;b&gt;3. 그래서 직접 비교해 봤다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 생각이 맞는지 확인하기 위해 10만 건의 상품 데이터를 넣고 세 가지 경우를 비교했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2318&quot; data-start=&quot;2307&quot; data-section-id=&quot;1ajn2ej&quot;&gt;인덱스가 없을 때&lt;/li&gt;
&lt;li data-end=&quot;2344&quot; data-start=&quot;2319&quot; data-section-id=&quot;lfris4&quot;&gt;brand_id 단일 인덱스만 있을 때&lt;/li&gt;
&lt;li data-end=&quot;2384&quot; data-start=&quot;2345&quot; data-section-id=&quot;xr7g4q&quot;&gt;(brand_id, like_count) 복합 인덱스가 있을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2584&quot; data-start=&quot;2403&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2426&quot; data-start=&quot;2403&quot; data-section-id=&quot;abpat6&quot;&gt;MySQL 8.0.45 (InnoDB)&lt;/li&gt;
&lt;li data-end=&quot;2443&quot; data-start=&quot;2427&quot; data-section-id=&quot;mti44&quot;&gt;로컬 Docker 컨테이너&lt;/li&gt;
&lt;li data-end=&quot;2480&quot; data-start=&quot;2444&quot; data-section-id=&quot;1il37f5&quot;&gt;macOS 15.4.1 / Apple M4 / RAM 32GB&lt;/li&gt;
&lt;li data-end=&quot;2507&quot; data-start=&quot;2481&quot; data-section-id=&quot;1m0c8w0&quot;&gt;InnoDB Buffer Pool 128MB&lt;/li&gt;
&lt;li data-end=&quot;2525&quot; data-start=&quot;2508&quot; data-section-id=&quot;15dvlnw&quot;&gt;상품 데이터 100,000건&lt;/li&gt;
&lt;li data-end=&quot;2538&quot; data-start=&quot;2526&quot; data-section-id=&quot;1dej7yt&quot;&gt;브랜드 1,000개&lt;/li&gt;
&lt;li data-end=&quot;2557&quot; data-start=&quot;2539&quot; data-section-id=&quot;4of6vw&quot;&gt;브랜드당 상품 수 약 100개&lt;/li&gt;
&lt;li data-end=&quot;2584&quot; data-start=&quot;2558&quot; data-section-id=&quot;10sigc1&quot;&gt;측정 방식: EXPLAIN ANALYZE&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 동일하게 유지했다.&lt;/p&gt;
&lt;pre id=&quot;code_1773325232571&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM product
WHERE brand_id = 10
ORDER BY like_count DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&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;style3&quot; /&gt;
&lt;h2 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 인덱스가 없을 때: 결국 다 읽고 다시 정렬한다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;인덱스가 없는 상태에서 실행한 결과는 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2846&quot; data-start=&quot;2761&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2780&quot; data-start=&quot;2761&quot; data-section-id=&quot;1s5k8ha&quot;&gt;실행 시간: &lt;b&gt;29.2ms&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2846&quot; data-start=&quot;2781&quot; data-section-id=&quot;1w9iw4y&quot;&gt;실행 계획: Table scan on product, Sort: product.like_count DESC&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2934&quot; data-start=&quot;2848&quot; data-ke-size=&quot;size16&quot;&gt;DB는 브랜드 ID가 10인 상품을 찾기 위해 10만 건 전체를 읽어야 했다. 그리고 조건에 맞는 데이터를 찾은 뒤에는 다시 좋아요 순으로 정렬해야 했다.&lt;/p&gt;
&lt;p data-end=&quot;2934&quot; data-start=&quot;2848&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2998&quot; data-start=&quot;2936&quot; data-ke-size=&quot;size16&quot;&gt;이 케이스가 보여주는 건 단순하다.&lt;br /&gt;인덱스가 없으면 &lt;u&gt;&lt;b&gt;필터링 비용과 정렬 비용을 모두 쿼리 실행 시점에 감당&lt;/b&gt;&lt;/u&gt;해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;2998&quot; data-start=&quot;2936&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3120&quot; data-start=&quot;3000&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 브랜드 하나당 상품이 100개 정도밖에 안 되는데 그렇게 느릴까 싶었다. 하지만 중요한 건 최종 결과가 100개라는 점이 아니라, &lt;u&gt;&lt;b&gt;그 100개를 찾기 위해 10만 건 전체를 상대해야 한다는 점&lt;/b&gt;&lt;/u&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;3120&quot; data-start=&quot;3000&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-path-to-node=&quot;10&quot;&gt;&lt;b&gt;5. 단일 인덱스: 찾기는 빨라졌지만 정렬은 그대로 남는다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 brand_id에만 단일 인덱스를 걸었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3285&quot; data-start=&quot;3196&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3216&quot; data-start=&quot;3196&quot; data-section-id=&quot;tb6y5&quot;&gt;실행 시간: &lt;b&gt;약 5.2ms&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;3285&quot; data-start=&quot;3217&quot; data-section-id=&quot;1c0h681&quot;&gt;실행 계획: Index lookup on product using idx_brand, Using filesort&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3347&quot; data-start=&quot;3287&quot; data-ke-size=&quot;size16&quot;&gt;풀 스캔에 비해 속도는 크게 줄었다. 이제 DB는 브랜드 ID가 10인 상품만 빠르게 골라낼 수 있게 됐다.&lt;/p&gt;
&lt;p data-end=&quot;3347&quot; data-start=&quot;3287&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3347&quot; data-start=&quot;3287&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실행 계획에는 여전히 Using filesort가 남아 있었다. &lt;u&gt;&lt;b&gt;즉, 찾는 문제는 줄었지만 정렬 문제는 해결되지 않았다. &lt;/b&gt;&lt;/u&gt;이 결과를 보고 가장 크게 배운 점은 이것이었다.&lt;/p&gt;
&lt;blockquote data-end=&quot;3496&quot; data-start=&quot;3457&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3459&quot; data-ke-size=&quot;size16&quot;&gt;WHERE를 최적화했다고 해서 조회 전체가 최적화되는 것은 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;3595&quot; data-start=&quot;3498&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3595&quot; data-start=&quot;3498&quot; data-ke-size=&quot;size16&quot;&gt;단일 인덱스는 검색 범위를 줄여준다. 하지만 조건에 맞는 상품들을 다시 정렬해야 한다면, 그 비용은 여전히 남아 있다. 실제 서비스 쿼리에서는 이 정렬 비용이 생각보다 크다.&lt;/p&gt;
&lt;p data-end=&quot;3595&quot; data-start=&quot;3498&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6.&lt;span&gt; 복합 인덱스: 검색과 정렬을 함께 줄인다&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 (brand_id, like_count) 복합 인덱스를 생성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3761&quot; data-start=&quot;3678&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3697&quot; data-start=&quot;3678&quot; data-section-id=&quot;v1tep0&quot;&gt;실행 시간: &lt;b&gt;0.21ms&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;3761&quot; data-start=&quot;3698&quot; data-section-id=&quot;1wsb6sf&quot;&gt;실행 계획: Index scan on product using idx_brand_like (reverse)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3904&quot; data-start=&quot;3763&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 실행 시간이 크게 줄었다. brand_id로 먼저 범위를 좁힌 뒤, 그 안에서 like_count 순서까지 인덱스 구조에 반영되어 있었기 때문이다. DB는 별도의 정렬 작업 없이 인덱스를 역방향으로 스캔하면서 필요한 10개만 바로 가져올 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;3904&quot; data-start=&quot;3763&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3904&quot; data-start=&quot;3763&quot; data-ke-size=&quot;size16&quot;&gt;이 결과를 보며 인덱스를 보는 관점도 많이 달라졌다. 예전에는 인덱스를 단순히 &amp;ldquo;찾는 속도를 높이는 도구&amp;rdquo; 정도로 생각했다. 그런데 이번 실험에서는 오히려 &lt;u&gt;&lt;b&gt;정렬 비용까지 함께 줄여주는 설계&lt;/b&gt;&lt;/u&gt;에 가깝게 느껴졌다.&lt;/p&gt;
&lt;p data-end=&quot;3904&quot; data-start=&quot;3763&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;단일 인덱스가 범위를 좁혀주는 장치라면, 복합 인덱스는 범위를 좁힌 뒤 &lt;u&gt;&lt;b&gt;다시 정렬하는 비용까지 줄여주는 구조&lt;/b&gt;&lt;/u&gt;에 가깝다.&lt;/p&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;참고로 이번 실험에서는 SELECT *를 사용했기 때문에, 인덱스만으로 조회가 끝나는 &lt;b&gt;커버링 인덱스&lt;/b&gt; 상황은 아니었다. 필요한 컬럼만 조회 대상으로 제한하고, 그 컬럼들을 인덱스에 포함시킨다면 추가적인 테이블 접근 비용을 더 줄일 여지가 있다.&lt;/p&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 19px;&quot;&gt;테스트 케이스&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 19px;&quot;&gt;실행 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 19px;&quot;&gt;주요 특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;인덱스 없음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span data-path-to-node=&quot;5,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,1,1,0&quot;&gt;29.2ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;전체 스캔 + 정렬 부하(Filesort) 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;단일 인덱스 (brand_id)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span data-path-to-node=&quot;5,2,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,2,1,0&quot;&gt;5.2ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span data-path-to-node=&quot;5,2,2,0&quot;&gt;검색은 빠르나 정렬 부하 여전함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;복합 인덱스 (brand_id, like_count)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;0.21ms&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span data-path-to-node=&quot;5,3,1,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;5,3,2,0&quot;&gt;검색과 정렬을 동시에 해결 (140배 향상)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4018&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-path-to-node=&quot;10&quot;&gt;&lt;b&gt;7.&lt;span&gt;&lt;span&gt;&amp;nbsp;다만 이 성능은 공짜가 아니다&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 보면 복합 인덱스가 정답처럼 보인다. 실제로도 수치만 보면 그렇다. 29.2ms가 0.21ms가 되었으니 차이는 크다.&lt;/p&gt;
&lt;p data-end=&quot;4231&quot; data-start=&quot;4193&quot; data-ke-size=&quot;size16&quot;&gt;하지만 조회가 빨라진 만큼, 그 구조를 유지하는 비용도 함께 생긴다.&lt;/p&gt;
&lt;p data-end=&quot;4231&quot; data-start=&quot;4193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;4231&quot; data-start=&quot;4193&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 쓰기 성능&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 업데이트 쿼리도 같이 측정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1773325524595&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE product
SET like_count = like_count + 1
WHERE id = ?;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4405&quot; data-start=&quot;4357&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4379&quot; data-start=&quot;4357&quot; data-section-id=&quot;1vcd8mv&quot;&gt;인덱스 없을 때: &lt;b&gt;0.60ms&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;4405&quot; data-start=&quot;4380&quot; data-section-id=&quot;yesf9t&quot;&gt;복합 인덱스 추가 후: &lt;b&gt;0.75ms&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4503&quot; data-start=&quot;4407&quot; data-ke-size=&quot;size16&quot;&gt;이번 테스트에서는 약 25% 정도 느려졌다. like_count가 인덱스 구성 컬럼이기 때문에, 값이 바뀔 때마다 테이블뿐 아니라 인덱스도 함께 갱신해야 하기 때문이다. 인덱스가 많아질수록 쓰기 시 함께 갱신해야 할 구조도 늘어나므로, 전체 쓰기 비용은 더 커질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;4503&quot; data-start=&quot;4407&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4607&quot; data-start=&quot;4505&quot; data-ke-size=&quot;size16&quot;&gt;물론 이 수치를 일반화할 수는 없다. 실제 쓰기 비용은 데이터 분포, 갱신 패턴, 인덱스 구조에 따라 달라진다. 그래도 방향은 명확하다.&lt;br /&gt;&lt;u&gt;&lt;b&gt;조회 성능은 유지 비용을 동반한다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;4607&quot; data-start=&quot;4505&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;4193&quot; data-end=&quot;4231&quot;&gt;&lt;b&gt;7.2 저장 공간&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-end=&quot;4683&quot; data-start=&quot;4635&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4657&quot; data-start=&quot;4635&quot; data-section-id=&quot;7b6wd5&quot;&gt;테이블만 있을 때: 약 &lt;b&gt;9MB&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;4683&quot; data-start=&quot;4658&quot; data-section-id=&quot;1gcf6lh&quot;&gt;복합 인덱스 추가 후: 약 &lt;b&gt;15MB&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4795&quot; data-start=&quot;4685&quot; data-ke-size=&quot;size16&quot;&gt;10만 건 규모에서는 크게 느껴지지 않을 수 있다. 하지만 데이터가 수천만 건으로 커지면 이 차이는 무시하기 어렵다. 인덱스도 결국 디스크를 차지하고, 버퍼 풀에 올라와야 하며, 메모리를 소비한다.&lt;/p&gt;
&lt;p data-end=&quot;4879&quot; data-start=&quot;4797&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4879&quot; data-start=&quot;4797&quot; data-ke-size=&quot;size16&quot;&gt;그제야 인덱스가 왜 &amp;ldquo;그냥 걸면 좋은 옵션&amp;rdquo;이 아닌지 실감이 났다.&lt;br /&gt;&lt;u&gt;&lt;b&gt;읽기를 빠르게 만드는 대신, 쓰기와 공간과 메모리를 함께 요구하는 구조&lt;/b&gt;&lt;/u&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;4879&quot; data-start=&quot;4797&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8.&lt;span&gt;&lt;span&gt;&amp;nbsp;결국 중요한 건 컬럼이 아니라 쿼리 패턴이었다&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험을 통해 가장 크게 바뀐 건 인덱스를 보는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 인덱스를 컬럼 단위로 봤다. brand_id로 검색하니 brand_id에 인덱스를 건다. 이런 식이었다. 그런데 실제로 중요한 건 컬럼 하나가 아니라 &lt;u&gt;&lt;b&gt;쿼리 패턴 전체&lt;/b&gt;&lt;/u&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 data-end=&quot;5079&quot; data-start=&quot;5066&quot; data-section-id=&quot;1xc0uie&quot;&gt;어떤 조건으로 찾는가&lt;/li&gt;
&lt;li data-end=&quot;5094&quot; data-start=&quot;5080&quot; data-section-id=&quot;163rdg1&quot;&gt;어떤 순서로 정렬하는가&lt;/li&gt;
&lt;li data-end=&quot;5109&quot; data-start=&quot;5095&quot; data-section-id=&quot;hozsbt&quot;&gt;얼마나 자주 실행되는가&lt;/li&gt;
&lt;li data-end=&quot;5128&quot; data-start=&quot;5110&quot; data-section-id=&quot;1fu8vh4&quot;&gt;조회가 많은가, 쓰기가 많은가&lt;/li&gt;
&lt;li data-end=&quot;5143&quot; data-start=&quot;5129&quot; data-section-id=&quot;10vyffh&quot;&gt;결과를 어디까지 읽는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 (brand_id, like_count) 인덱스는 이번 쿼리에는 잘 맞는다. 하지만 like_count만으로 조회하거나 정렬하는 쿼리에는 큰 도움이 되지 않는다. 복합 인덱스는 왼쪽 컬럼부터 의미를 가지기 때문이다.&lt;/p&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;이 아니라, &lt;u&gt;&lt;b&gt;서비스에서 반복되는 조회 패턴을 읽어내는 일&lt;/b&gt;&lt;/u&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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-path-to-node=&quot;10&quot;&gt;&lt;b&gt;9.&lt;span&gt;&lt;span&gt; 인덱스가 있어도 해결되지 않는 문제도 있다&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 잘 설계했다고 해서 모든 조회 성능 문제가 사라지는 건 아니다. 대표적인 예가 깊은 페이지네이션이다.&lt;/p&gt;
&lt;p data-end=&quot;5470&quot; data-start=&quot;5454&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 쿼리를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1773325682820&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM product
WHERE brand_id = 10
ORDER BY like_count DESC
LIMIT 10 OFFSET 10000;&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;인덱스를 타더라도 앞의 10,000개를 읽고 버린 뒤 그다음 10개를 가져와야 한다. 페이지가 뒤로 갈수록 버려지는 비용이 계속 커진다.&lt;/p&gt;
&lt;p data-end=&quot;5737&quot; data-start=&quot;5652&quot; data-ke-size=&quot;size16&quot;&gt;이건 인덱스가 없어서 생기는 문제가 아니라, &lt;u&gt;&lt;b&gt;OFFSET 방식 자체의 한계&lt;/b&gt;&lt;/u&gt;에서 생기는 문제다. 이런 경우에는 보통 커서 기반 조회가 더 적합하다.&lt;/p&gt;
&lt;p data-end=&quot;5737&quot; data-start=&quot;5652&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5798&quot; data-start=&quot;5739&quot; data-ke-size=&quot;size16&quot;&gt;즉, 인덱스는 강력하지만 만능은 아니다. 어떤 문제는 인덱스보다 &lt;u&gt;&lt;b&gt;조회 방식 자체를 바꿔야&lt;/b&gt; &lt;/u&gt;해결된다.&lt;/p&gt;
&lt;p data-end=&quot;5798&quot; data-start=&quot;5739&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;결론&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험을 하면서 인덱스를 보는 시선이 꽤 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5976&quot; data-start=&quot;5848&quot; data-ke-size=&quot;size16&quot;&gt;전에는 인덱스를 &lt;u&gt;&lt;b&gt;조회 성능을 높이는 정답&lt;/b&gt;&lt;/u&gt;처럼 생각했다. 지금은 그렇게 보지 않는다. 인덱스는 빠른 조회를 위해 별도의 구조를 유지하는 선택이고, 그만큼 쓰기 성능과 저장 공간, 메모리 사용량을 대가로 지불하는 기술이라고 본다.&lt;/p&gt;
&lt;p data-end=&quot;5976&quot; data-start=&quot;5848&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6102&quot; data-start=&quot;5978&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 크게 느낀 점은, 조회 성능 문제를 단순히 &amp;ldquo;어떻게 빨리 찾을까&amp;rdquo;로만 보면 절반만 본다는 사실이었다. 실제 서비스 쿼리에서는 필터링 못지않게 정렬도 큰 비용이 된다. 그리고 복합 인덱스는 바로 그 지점을 건드린다.&lt;/p&gt;
&lt;p data-end=&quot;6102&quot; data-start=&quot;5978&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6102&quot; data-start=&quot;5978&quot; data-ke-size=&quot;size16&quot;&gt;이번 테스트를 통해 얻은 결론은 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6220&quot; data-start=&quot;6129&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6144&quot; data-start=&quot;6129&quot; data-section-id=&quot;17gfcf2&quot;&gt;인덱스는 마법이 아니다.&lt;/li&gt;
&lt;li data-end=&quot;6167&quot; data-start=&quot;6145&quot; data-section-id=&quot;igsa8y&quot;&gt;단일 인덱스만으로는 부족할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;6192&quot; data-start=&quot;6168&quot; data-section-id=&quot;13vuc7a&quot;&gt;복합 인덱스는 강력하지만 공짜가 아니다.&lt;/li&gt;
&lt;li data-end=&quot;6220&quot; data-start=&quot;6193&quot; data-section-id=&quot;sntxa&quot;&gt;결국 중요한 건 컬럼이 아니라 쿼리 패턴이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;6326&quot; data-start=&quot;6222&quot; data-ke-size=&quot;size16&quot;&gt;예전에는 인덱스를 &amp;ldquo;걸지 말지&amp;rdquo;의 문제로 봤다면, 지금은 &lt;u&gt;&lt;b&gt;어떤 비용을 줄이기 위해 어떤 구조를 유지할 것인가&lt;/b&gt;&lt;/u&gt;의 문제를 보고 파악하는 시야를 얻게 되었다. 끝!&lt;/p&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/2</guid>
      <comments>https://think-yk.tistory.com/2#entry2comment</comments>
      <pubDate>Thu, 12 Mar 2026 23:52:24 +0900</pubDate>
    </item>
    <item>
      <title>Read-Modify-Write 패턴의 위험성과 원자적 쿼리를 통한 동시성 해결</title>
      <link>https://think-yk.tistory.com/1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 동시성 이슈를 마주하면 단순히 &lt;u&gt;&lt;b&gt;낙관적&lt;/b&gt;&lt;b&gt;&lt;u&gt;락&lt;/u&gt;이나 비관적락을 활용해서 해결하면 되겠지?&lt;/b&gt;&lt;/u&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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 동시성, 다 똑같은 게 아니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 문제는 성격에 따라 해결 방법이 달라진다. 현재 프로젝트에서 발생할 수 있는 케이스는 크게 3가지 정도가 있었다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,0,1,0&quot;&gt;좋아요 (카운터)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,0,2,0&quot;&gt;쿠폰 (Exactly-once)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,0,3,0&quot;&gt;재고 (리소스 일관성)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;문제&amp;nbsp;성격&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;6,1,1,0&quot;&gt;수치의 정확성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;단 한 번만 실행&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;리소스 일관성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;비즈니스 규칙&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;여러번 증가해도 OK, 정확해야함&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,2,2,0&quot;&gt;한번만 사용되어야함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;재고가 음수가 되면 안됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;데이터 특성&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;누적 가능&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span data-path-to-node=&quot;6,2,2,0&quot;&gt;상태 변경&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;제한된 리소스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 가장 빈번하게 발생하면서도 놓치기 쉬운 &lt;u&gt;&lt;b data-index-in-node=&quot;30&quot; data-path-to-node=&quot;7&quot;&gt;좋아요(카운터 정확성)&lt;/b&gt;&lt;/u&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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 원인은 Read-Modify-Write 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 세 케이스는 모두 공통된 코드 패턴을 가지고 있다. 바로 데이터를 &lt;u&gt;&lt;b&gt;읽고(Read), 수정하고(Modify), 다시 쓰는(Write)&lt;/b&gt;&lt;/u&gt; 구조다.&lt;/p&gt;
&lt;pre id=&quot;code_1772719056643&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 좋아요 기능
public void increaseLikeCount(Long productId) {
    Product product = getById(productId);         // 1. READ
    product.increaseLikeCount();                  // 2. MODIFY
    productRepository.update(product);            // 3. WRITE
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1772719130765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 쿠폰 사용
public CouponApplyResult applyToOrder(Long couponId, Long userId, Money orderAmount) {
    Coupon coupon = getById(couponId);          // 1. READ
    coupon.validateUsable(userId, orderAmount, ZonedDateTime.now()); // 2. MODIFY(검증)
    Money discountAmount = coupon.calculateDiscount(orderAmount);
    use(coupon);                                 // 2. MODIFY(상태 변경)
    return new CouponApplyResult(coupon.getId(), discountAmount); // 3. WRITE
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1772719144933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 재고 차감
public void decreaseStock(Long productId, Integer decreaseStock) {
    Product product = getById(productId);       // 1. READ
    product.decreaseStock(decreaseStock);       // 2. MODIFY
    productRepository.update(product);          // 3. WRITE
}&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;table style=&quot;border-collapse: collapse; width: 100%; height: 121px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;&lt;span data-path-to-node=&quot;12,0,1,0&quot;&gt;트랜잭션 A&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;&lt;span data-path-to-node=&quot;12,0,2,0&quot;&gt;트랜잭션 B&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;b&gt;&lt;span data-path-to-node=&quot;12,0,3,0&quot;&gt;결과&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T1&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;12,1,1,0&quot;&gt;Read (값: 0)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T2&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;12,2,2,0&quot;&gt;Read (값: 0)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T3&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Modify (0&amp;rarr;1)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T4&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Modify (0&amp;rarr;1)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T5&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;12,5,1,0&quot;&gt;Write (값: 1)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;T6&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Write (값: 1)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;b&gt;Lost Update 발생!&lt;br /&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 트랜잭션 입장에서는 다음처럼 생각한다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A: &amp;ldquo;0에서 1로 올렸어.&amp;rdquo;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;B: &amp;ldquo;0에서 1로 올렸어.&amp;rdquo;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 DB 입장에서는 최종 결과가 0 &amp;rarr; 1 &amp;rarr; 1두 번 올렸는데, 최종 값은 1번만 오른 셈이다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Write 를 2번했지만, 최종적으론 1번만 진행됐다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(Write(=Update)가 1개가 없어졌기때문에 이를&amp;nbsp;Lost Update라고 표현하기도 한다)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 동시에 100번 좋아요를 클릭해도, 최종 좋아요 갯수는 100이 나올 수 없는 구조다&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 원인을 좀 더 정확히 정의하자&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 나는 동시성 이슈의 원인을 단순히 이렇게 생각했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 사용자가 DB 데이터에 동시에 접근하니까 발생하는 거구나&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;틀린 말은 아니지만, 현상을 설명하기엔 너무 단편적인 대답이다. 조금 더 구조적인 이유를 들어 설명하고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;&lt;b&gt;읽고/수정하고/다시 쓰는 구조(RMW)&lt;/b&gt;&lt;/u&gt; 가 원인이다. READ와 WRITE 사이에 시간 간격이 있고, 그 사이에 다른 트랜잭션이 접근하기 때문이다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;여기서 한걸음 더 나아간 대답을 하자면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;&lt;b&gt;애플리케이션 레벨에서 읽고/수정하고/다시 쓰는 구조(&lt;u&gt;&lt;b&gt;RMW&lt;/b&gt;&lt;/u&gt;)&lt;/b&gt;&lt;/u&gt; 이기 때문에 발생한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;READ와 WRITE 사이에 시간 간격이 있고, 그 사이에 다른 트랜잭션이 접근하기 때문이다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동시성 이슈가 발생하는 위 코드들을 다시보면, DB에서 데이터를 가져오고(READ), 자바 코드에서 계산하고(MODIFY), 다시 DB에 넣고있다(WRITE). &lt;u&gt;&lt;b&gt;즉, 데이터가 DB를 떠나 자바 메모리에 머무는 그 짧은 순간, 데이터는 DB의 통제권을 벗어난 방치된 상태가 된다.&lt;/b&gt;&lt;/u&gt; 이것이 동시성 이슈의 시작점이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 해결전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1 원자적 쿼리 (Atomic Query)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 레벨에서 RMW는 구조를 해결하는 가장 쉽고 확실한 방법은 &lt;u&gt;&lt;b&gt;RMW 과정을 모두 DB에서 처리&lt;/b&gt;&lt;/u&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 원자적 쿼리 (Atomic Query)라고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772720712358&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE product SET like_count = like_count + 1 WHERE id = :productId;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리는 의미 그대로 DB에서 직접 읽어서 처리하고 저장까지 한다. 전 처럼 수정을 애플리케이션 레벨에서 진행하지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 드는 의문이 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위치만 달라졌지 READ-MODIFY-WRITE는 이전과 같은데, 만약 여러 트랜잭션들이 DB에 동시에 접근하면 다시 동시성 이슈가 생기는거 아닌가?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론만 말하면 원자적 쿼리도 DB Lock을 사용하기 때문에, 사용 중에는 다른 트랜잭션이 접근할 수 없다&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1.1 원자적 쿼리는 어떻게 작동하나?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB(InnoDB)는 &lt;u&gt;&lt;b&gt;UPDATE 실행 시&lt;/b&gt;&lt;/u&gt; 해당 로우에 &lt;u&gt;&lt;b&gt;잠금(Lock)&lt;/b&gt;&lt;/u&gt;을 건다. 그렇기 때문에 트랜잭션은 접근할 수 없다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Lock 획득&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt; :&lt;/b&gt; &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;해당 ID를 가진 행(Row)에 lock을 를 걸어 다른 트랜잭션이 접근할 수 없게 한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;원자적 RMW&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt; :&lt;/b&gt; DB 내부에서 최신 값을 읽어 계산하여 즉시 기록한다(RMW) 과정은 쪼개질 수 없는 하나의 단위다&lt;/li&gt;
&lt;li&gt;Unlock&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,2,0&quot;&gt; :&lt;/b&gt; 트랜잭션 종료 시 자물쇠를 푼다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1.2 원자적 쿼리 주의사항 : 트랜잭션 범위&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock은 쿼리가 종료 될때 해제되는 것이 아니라 &lt;u&gt;&lt;b&gt;트랜잭션이 종료 될 때 해제&lt;/b&gt;&lt;/u&gt; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 원자적 쿼리 뒤에 무거운 외부 API 호출(3초 소요)이 있다면, 다른 트랜잭션들은 3초 동안 대기해야한다. 따라서 업데이트 쿼리는 트랜잭션의 &lt;u&gt;&lt;b&gt;최대한 마지막 순서에 배치&lt;/b&gt;&lt;/u&gt;해야 성능 병목을 막을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bad Case : &lt;span style=&quot;color: #000000;&quot;&gt;원자적 쿼리(락 획득)&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;-&amp;gt;&amp;nbsp;&lt;/span&gt;무거운 로직&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;-&amp;gt;&amp;nbsp;&lt;/span&gt;커밋(락 해제)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Good Case : &lt;span style=&quot;color: #000000;&quot;&gt;무거운 로직&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;-&amp;gt;&amp;nbsp;&lt;/span&gt;원자적 쿼리(락 획득)&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;-&amp;gt;&amp;nbsp;&lt;/span&gt;커밋(락 해제)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1.3 설계와의 트레이드 오프 (개인적인 생각)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원자적 쿼리는 강력하지만, &lt;u&gt;&lt;b data-index-in-node=&quot;15&quot; data-path-to-node=&quot;23&quot;&gt;비즈니스 로직이 도메인 객체에서 쿼리로 넘어간다&lt;/b&gt;&lt;/u&gt;는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 단위 테스트를 통한 순수한 비즈니스 로직 검증이 어려워지고, DB가 포함된 통합 테스트 의존도가 높아진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 설계와 성능 사이의 피할 수 없는 선택이 아닐까 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 어떤 성격의 동시성 문제를 해결할 것인지 정확히 파악하고, 트레이드 오프를 저울질 하며 해결방법을 선택해야할 것 같다.&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;as-is&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772722411908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // ProductService.java
    public void increaseLikeCount(Long productId) {
        Product product = getById(productId);
        product.increaseLikeCount();
        productRepository.update(product);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;to-be&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772722588125&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // ProductService.java
    public void increaseLikeCount(Long productId) {
        productRepository.incrementLikeCount(productId);
    }
    
    // ProductJpaRepository.java
    public interface ProductJpaRepository extends JpaRepository&amp;lt;ProductEntity, Long&amp;gt; {
    
    	@Modifying
	    @Query(&quot;UPDATE ProductEntity p SET p.likeCount = p.likeCount + 1 WHERE p.id = :id&quot;)
    	void incrementLikeCount(@Param(&quot;id&quot;) Long id);

	    @Modifying
    	@Query(&quot;UPDATE ProductEntity p SET p.likeCount = p.likeCount - 1 WHERE p.id = :id AND p.likeCount &amp;gt; 0&quot;)
	    void decrementLikeCount(@Param(&quot;id&quot;) Long id);
	}&lt;/code&gt;&lt;/pre&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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 다른 대안들과의 비교&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.1 비관적 락 (Pessimistic Lock) VS 원자적 쿼리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-path-to-node=&quot;10,0&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;동시성 이슈가 발생할 확률이 높아. 충돌하기 전부터 확실히 잠가둘게&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;락의 점유 범위:&lt;/b&gt; 비관적 락은 조회를 시작하는 시점(SELECT ... FOR UPDATE)부터 트랜잭션이 끝날 때까지 전체 구간을 점유한다. 반면, 원자적 쿼리는 업데이트가 발생하는 찰나의 순간만 점유한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;성능 차이:&lt;/b&gt; 초당 100건의 요청이 올 때, 비관적 락은 앞사람이 조회를 시작해 비즈니스 로직을 다 마칠 때까지 뒤의 99명이 대기해야하고,&amp;nbsp; 원자적 쿼리는 DB 내부 연산 속도로 자물쇠(lock)를 빠르게 넘겨주며 처리하므로 처리량이 훨씬 높다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;적합성:&lt;/b&gt; 조회 시점의 데이터가 비즈니스 로직 내내 변하지 않아야 하는 '결제' 등에는 적합하지만, 단순 합산인 '좋아요'에는 배보다 배꼽이 더 큰 비용을 지불하는 셈이라 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-path-to-node=&quot;25&quot;&gt;&lt;b&gt;4.2 낙관적 락 (Optimistic Lock) VS 원자적 쿼리&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;충돌이 별로 안 날 것 같아. 일단 진행하고 충돌나면 그때 다시 시도할게&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;작동 방식:&lt;/b&gt; DB 락을 거는 대신 &lt;u&gt;&lt;b&gt;version 컬럼을 이용해 애플리케이션 레벨&lt;/b&gt;&lt;/u&gt;에서 충돌을 감지한다. 충돌이 없으면 락 비용이 들지 않지만, 충돌 시 재시도(Retry) 로직을 개발자가 직접 관리해야한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;충돌 비용:&lt;/b&gt; &quot;낙관적 락의 &lt;b&gt;&lt;u&gt;구조적 한계는 동시성 이슈가 발생했을 때 생기는 비용&lt;/u&gt;&lt;/b&gt;이다. 100명이 동시에 누르면 1명만 성공하고 99명은 실패하는데, 이때 이 99명은 성공할 때까지 계속 재시도 쿼리를 날린다면 DB 부하가 커질 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0&quot;&gt;적합성:&lt;/b&gt; 수정이 가끔 일어나는 곳에는&amp;nbsp; 유리하지만, 1초에도 수십 번 충돌이 발생하는 '좋아요'에서는 재시도 비용이 부담이 될 수 있다.&lt;/li&gt;
&lt;/ul&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;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 결론: 정답은 없지만 근거는 있어야 한다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 좋아요 동시성 이슈 해결을 위해 최종적으로 &lt;u&gt;&lt;b&gt;원자적 쿼리&lt;/b&gt;&lt;/u&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 data-index-in-node=&quot;0&quot; data-path-to-node=&quot;31,0,0&quot;&gt;가성비:&lt;/b&gt; 단순 증감 연산에 비관적/낙관적 락을 도입하는 것은 오버엔지니어링이다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;31,1,0&quot;&gt;효율성:&lt;/b&gt; 락 점유 시간을 최소화하여 대량 요청을 가장 빠르게 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현실적 타협: &lt;/b&gt;좋아요 증가처럼 단순 증감만 존재하는 경우에는 비즈니스 로직이 DB에 존재해도 된다고 생각한다.&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;낙관적락 or 비관적락이면 다 된다&lt;/b&gt;가 아니라, &lt;b data-index-in-node=&quot;17&quot; data-path-to-node=&quot;32&quot;&gt;문제의 성격(카운터)과 발생 원인을 명확히 이해해서 근거 있는 선택&lt;/b&gt;을 내린 것이 이번 학습의 가장 큰 수확이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kimong</author>
      <guid isPermaLink="true">https://think-yk.tistory.com/1</guid>
      <comments>https://think-yk.tistory.com/1#entry1comment</comments>
      <pubDate>Fri, 6 Mar 2026 00:39:11 +0900</pubDate>
    </item>
  </channel>
</rss>