<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>hyomee2</title>
    <link>https://hyomee2.tistory.com/</link>
    <description>안냥하세요 형바오 형마이 땡미입니다
많이 놀러오세요</description>
    <language>ko</language>
    <pubDate>Mon, 20 Jul 2026 22:44:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hyomee2</managingEditor>
    <image>
      <title>hyomee2</title>
      <url>https://tistory1.daumcdn.net/tistory/5452180/attach/7b81147b022d4862ab6749533cd2c34a</url>
      <link>https://hyomee2.tistory.com</link>
    </image>
    <item>
      <title>포트폴리오</title>
      <link>https://hyomee2.tistory.com/135</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 pdf 파일을 통해 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/lPkU1/dJMcagfb7sP/Q2cKZAxtyfBeB7eL8gRRIk/%E1%84%80%E1%85%AF%E1%86%AB%E1%84%92%E1%85%A7%E1%86%BC%E1%84%86%E1%85%B5_%E1%84%91%E1%85%A9%E1%84%90%E1%85%B3%E1%84%91%E1%85%A9%E1%86%AF%E1%84%85%E1%85%B5%E1%84%8B%E1%85%A9_1.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;권형미_포트폴리오_1.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;5.73MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;1688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csQiHQ/dJMcahLTtFC/uDfylGXMYNKxXRrnKJ9rb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csQiHQ/dJMcahLTtFC/uDfylGXMYNKxXRrnKJ9rb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csQiHQ/dJMcahLTtFC/uDfylGXMYNKxXRrnKJ9rb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsQiHQ%2FdJMcahLTtFC%2FuDfylGXMYNKxXRrnKJ9rb0%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;574&quot; height=&quot;820&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;1688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7bgPu/dJMcacDSjGG/eDdClkwfTmOTpTH5heK4g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7bgPu/dJMcacDSjGG/eDdClkwfTmOTpTH5heK4g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7bgPu/dJMcacDSjGG/eDdClkwfTmOTpTH5heK4g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7bgPu%2FdJMcacDSjGG%2FeDdClkwfTmOTpTH5heK4g1%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;574&quot; height=&quot;814&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;1676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzdVJ4/dJMb991qfhv/HPlhDx2fsA79t6RyUh19xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzdVJ4/dJMb991qfhv/HPlhDx2fsA79t6RyUh19xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzdVJ4/dJMb991qfhv/HPlhDx2fsA79t6RyUh19xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzdVJ4%2FdJMb991qfhv%2FHPlhDx2fsA79t6RyUh19xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;606&quot; height=&quot;858&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;1676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XlnkP/dJMcag0rRY3/pyKOe4A45LMD3AVHajxQB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XlnkP/dJMcag0rRY3/pyKOe4A45LMD3AVHajxQB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XlnkP/dJMcag0rRY3/pyKOe4A45LMD3AVHajxQB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXlnkP%2FdJMcag0rRY3%2FpyKOe4A45LMD3AVHajxQB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;879&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;1588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbWfPY/dJMcaiRAxrg/jcJd5eazewEzcszCK8liPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbWfPY/dJMcaiRAxrg/jcJd5eazewEzcszCK8liPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbWfPY/dJMcaiRAxrg/jcJd5eazewEzcszCK8liPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbWfPY%2FdJMcaiRAxrg%2FjcJd5eazewEzcszCK8liPK%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;594&quot; height=&quot;848&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;1588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpvcg/dJMcafAx1qI/BKmWFHv0igT377ZwP4HRcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpvcg/dJMcafAx1qI/BKmWFHv0igT377ZwP4HRcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpvcg/dJMcafAx1qI/BKmWFHv0igT377ZwP4HRcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqpvcg%2FdJMcafAx1qI%2FBKmWFHv0igT377ZwP4HRcK%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;596&quot; height=&quot;848&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;1678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;1680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AIQuP/dJMcajwbry3/N76RjDvvOOR48uhV6GELrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AIQuP/dJMcajwbry3/N76RjDvvOOR48uhV6GELrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AIQuP/dJMcajwbry3/N76RjDvvOOR48uhV6GELrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAIQuP%2FdJMcajwbry3%2FN76RjDvvOOR48uhV6GELrk%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;596&quot; height=&quot;847&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;1680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1LOAX/dJMcafHnrfJ/NUxB9604hxygj07NJgaPi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1LOAX/dJMcafHnrfJ/NUxB9604hxygj07NJgaPi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1LOAX/dJMcafHnrfJ/NUxB9604hxygj07NJgaPi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1LOAX%2FdJMcafHnrfJ%2FNUxB9604hxygj07NJgaPi1%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;593&quot; height=&quot;848&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/135</guid>
      <comments>https://hyomee2.tistory.com/135#entry135comment</comments>
      <pubDate>Mon, 13 Jul 2026 12:22:32 +0900</pubDate>
    </item>
    <item>
      <title>3. 알림 조회 성능 개선 - 동시성</title>
      <link>https://hyomee2.tistory.com/134</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hyomee2.tistory.com/133&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hyomee2.tistory.com/133&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783055932160&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2. 알림 조회 성능 개선 - 반정규화&quot; data-og-description=&quot;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다. https://hyomee2.tistory.com/132 1. 알림 조회 성능 개선 - 인덱스본 게시물은 트래블록스 프로젝트를 진행하&quot; data-og-host=&quot;hyomee2.tistory.com&quot; data-og-source-url=&quot;https://hyomee2.tistory.com/133&quot; data-og-url=&quot;https://hyomee2.tistory.com/133&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iucqY/dJMb83St8GZ/DjiKTGvjZDx9NpYiVVvQa0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bmzEKL/dJMb9efo3km/VE6czIRKlwigo63Bd6kPsk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A6rFv/dJMb87ghEBp/mefOkorcLXbkdaiTKugPwk/img.png?width=2076&amp;amp;height=1386&amp;amp;face=0_0_2076_1386&quot;&gt;&lt;a href=&quot;https://hyomee2.tistory.com/133&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hyomee2.tistory.com/133&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iucqY/dJMb83St8GZ/DjiKTGvjZDx9NpYiVVvQa0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bmzEKL/dJMb9efo3km/VE6czIRKlwigo63Bd6kPsk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A6rFv/dJMb87ghEBp/mefOkorcLXbkdaiTKugPwk/img.png?width=2076&amp;amp;height=1386&amp;amp;face=0_0_2076_1386');&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;2. 알림 조회 성능 개선 - 반정규화&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다. https://hyomee2.tistory.com/132 1. 알림 조회 성능 개선 - 인덱스본 게시물은 트래블록스 프로젝트를 진행하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hyomee2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 반정규화를 이용하여 병목이었던 COUNT 집계 쿼리를 제거하고 알림 목록 조회 API의 응답 시간을 개선했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 반정규화를 적용하며 또 다른 문제를 마주했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 동시성 문제다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 특정 사용자의 notification_count(알림 개수)가 10개라고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 동시에 두 알림이 생성된다면 아래와 같은 상황이 발생할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1783056540895&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread A: 10 조회 -&amp;gt; Thread A: 11 저장
Thread B: 10조회 -&amp;gt; Thread B: 11 저장&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;실제로는 알림이 2개 생성되었으니 최종적으로 12가 되어야 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 저장한 값이 덮어쓰여져 11이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를&lt;b&gt; Lost Update&lt;/b&gt;라고 한다.&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-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방안 검토&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Optimistic Lock&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 자주 발생하지 않음을 가정하고 충돌을 감지하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티의 버전을 관리하여, 수정 시 조회했던 버전과 현재 버전이 일치하는지 확인하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 트랜잭션이 먼저 데이터를 수정했다면 OptimisticLockException을 발생시킨다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Lock을 잡지 않고 있기 때문에 다른 트랜잭션을 대기시키지 않아 높은 처리량을 기대할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 충돌이 자주 발생하지 않는 환경에서 우수한 성능을 갖는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 충돌 발생 시 OptimisticLockException이 발생하므로 재시도 로직을 구현해야 한다.&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;notification_count는 여러 알림이 동시에 생성될 경우 동일한 행을 빈번하게 갱신해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 optimistic lock을 적용하면 충돌이 자주 발생하고, 예외 처리와 재시도 로직도 반복될 가능성이 높기 때문에 적합하지 않다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Pessimistic Lock&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 같은 데이터를 수정하는 일이 빈번하게 발생할 것이라고 가정하는 동시성 제어 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 조회할 때 쓰기 락을 획득하여 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터를 수정하기 전에 행을 잠그므로 Lost Update를 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 충돌이 발생하더라도 예외나 재시도 없이 순차적으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 충돌 가능성이 높은 환경에서도 데이터 정합성을 안정적으로 보장할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 충돌이 발생할 경우 다른 트랜잭션들은 락이 해제될 때까지 대기해야 하므로 응답시간, 처리량이 감소할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터 정합성이 중요한 경우에 적합하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 notification_count를 단순히 1 증가시키는 연산에서 비관적 락을 적용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 회원에 대한 다른 수정 작업도 락이 해제될 때까지 대기해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 응답 시간 증가와 처리량 감소로 이어질 수 있어 현재 상황에 적합하지 않다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. synchronized&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 synchronized를 사용하면 단일 서버 환경에서 스레드 간 동시 접근을 제어할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 synchronized는 JVM 내부에서만 동작하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 객체에 대한 접근을 제어할 뿐 데이터베이스 자체의 상태를 관리하는 방식은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notification_count 값은 결국은 DB에 저장되는 데이터이기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 레벨에서 Lock을 관리하는 것보다는 DB 레벨에서 원자성을 보장하는 것이 더 적절하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. DB Atomic UPDATE&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재값을 조회한 후 애플리케이션에서 계산하는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스가 제공하는 원자적 UPDATE 연산을 이용하여 값을 직접 증가, 감소시키는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래와 같이 하나의 UPDATE문에서 현재값을 기준으로 연산을 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783087544391&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE members
SET notification_count = notification_count + 1
WHERE member_id = :memberId;&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;데이터베이스는 UPDATE를 수행하는 동안 해당 행에 대한 쓰기 lock을 획득하여 연산의 원자성을 보장하므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 여러 요청이 들어와도 연산이 유실되지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터베이스가 원자성을 방지하므로 Lost Update를 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SELECT -&amp;gt; 수정 -&amp;gt; UPDATE 과정없이 UPDATE 한번으로 처리되어 불필요한 조회를 제거할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 별도의 lock 관리나 재시도 로직이 필요없이 구현이 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 분산 환경에서도 동일한 방식으로 사용할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 단순한 증감 연산에는 적합하지만 여러 행을 함께 수정하거나 복잡한 비즈니스 로직에 적용은 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 결론적으로는 DB Atomic UPDATE를 통하여 Lost Update 문제를 해결했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;* 위의 네가지 방식에 대해서 간단하게 작성했지만, 다음 글에서 자세히 다뤄볼 예정이다.&lt;/span&gt;&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-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit을 기반으로 동시성 테스트를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 아래에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783330259339&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;travlocks-server/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java at develo&quot; data-og-description=&quot;트래블록스 백엔드 레포지토리입니다. Contribute to Travlocks/travlocks-server development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&quot; data-og-url=&quot;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZMA2V/dJMb8ZvMS62/YYks5zCorBIhINDUjHEJ8K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bkh88n/dJMb8RR3plz/CpL6wPGuwBApexxYK4Ar8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Travlocks/travlocks-server/blob/develop/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZMA2V/dJMb8ZvMS62/YYks5zCorBIhINDUjHEJ8K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bkh88n/dJMb8RR3plz/CpL6wPGuwBApexxYK4Ar8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;travlocks-server/src/test/java/org/umc/travlocksserver/domain/notification/service/NotificationCommandServiceTest.java at develo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;트래블록스 백엔드 레포지토리입니다. Contribute to Travlocks/travlocks-server development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 환경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 생성 시 개수와 관련한 동시성 문제를 검증하기 위해 JUnit 기반 동시성 테스트를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutorService를 사용하여 100개의 스레드가 동시에 알림을 생성하는 것을 가정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DB Atomic Update 적용 전&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100개의 동시 요청을 보낸 결과, 예상과 같이 모든 요청이 반영되지 않았으며, 평균적으로 약 12건만 정상적으로 반영되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 데이터를 여러 스레드가 동시에 수정하며 데이터 정합성이 보장되지 않은 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tsSIH/dJMcahdYExo/khbBPXG85uXoaLvnKrUJjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tsSIH/dJMcahdYExo/khbBPXG85uXoaLvnKrUJjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tsSIH/dJMcahdYExo/khbBPXG85uXoaLvnKrUJjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtsSIH%2FdJMcahdYExo%2FkhbBPXG85uXoaLvnKrUJjK%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;583&quot; height=&quot;99&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DB Atomic Update 적용 후&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;100개의 동시 요청을 보낸 결과, DB Atomic Update 적용 전과 달리, 모든 요청이 정상적으로 처리되어 기대한 값과 정확히 일치했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpYfRb/dJMb99Uu86J/qhLiPAVKKT4ODApjxwY4hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpYfRb/dJMb99Uu86J/qhLiPAVKKT4ODApjxwY4hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpYfRb/dJMb99Uu86J/qhLiPAVKKT4ODApjxwY4hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpYfRb%2FdJMb99Uu86J%2FqhLiPAVKKT4ODApjxwY4hK%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;236&quot; height=&quot;67&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 83.0233%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 26.9379%; text-align: center; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 19.3798%; text-align: center; height: 19px;&quot;&gt;동시 요청수&lt;/td&gt;
&lt;td style=&quot;width: 19.1086%; text-align: center; height: 19px;&quot;&gt;성공수&lt;/td&gt;
&lt;td style=&quot;width: 17.8826%; 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: 26.9379%; text-align: center; height: 19px;&quot;&gt;DB Atomic Update 적용 전&lt;/td&gt;
&lt;td style=&quot;width: 19.3798%; text-align: center; height: 19px;&quot;&gt;100개&lt;/td&gt;
&lt;td style=&quot;width: 19.1086%; text-align: center; height: 19px;&quot;&gt;12개&lt;/td&gt;
&lt;td style=&quot;width: 17.8826%; text-align: center; height: 19px;&quot;&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 26.9379%; text-align: center; height: 19px;&quot;&gt;DB Atomic Update 적용 후&lt;/td&gt;
&lt;td style=&quot;width: 19.3798%; text-align: center; height: 19px;&quot;&gt;100개&lt;/td&gt;
&lt;td style=&quot;width: 19.1086%; text-align: center; height: 19px;&quot;&gt;100개&lt;/td&gt;
&lt;td style=&quot;width: 17.8826%; text-align: center; height: 19px;&quot;&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 조회 API의 성능을 개선하던 중 찾은 COUNT을 해결하고 나니 또 다른 동시성 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;동시성 문제를 해결할 때 흔히 &quot;Lock&quot;이 떠오르곤 하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 동시성 문제 해결 방식을 찾고 고민해보며 이번 경우는 DB 레벨에서 원자성을 보장하는 것이 적절하다고 판단하고 적용할 수 있었다.&lt;br /&gt;이를 통해 현재 상황에 맞는 적절한 방식을 선택하는 것을 연습하고, 그 중요성을 깨달을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 단순히 어떤 방식을 적용했다는 것에서 끝내지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit으로 여러 스레드를 통해 동시 요청을 보내는 테스트 코드를 작성해보고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 수치적으로 적용 전과 후의 변화를 검증해볼 수 있어서 동시성 제어의 중요성을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>projects/travlocks</category>
      <category>Atomic Update</category>
      <category>db</category>
      <category>JUnit</category>
      <category>optimistic lock</category>
      <category>pessimistic lock</category>
      <category>synchronized</category>
      <category>낙관적 락</category>
      <category>동시성문제</category>
      <category>반정규화</category>
      <category>비관적 락</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/134</guid>
      <comments>https://hyomee2.tistory.com/134#entry134comment</comments>
      <pubDate>Mon, 6 Jul 2026 18:39:26 +0900</pubDate>
    </item>
    <item>
      <title>2. 알림 조회 성능 개선 - 반정규화</title>
      <link>https://hyomee2.tistory.com/133</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hyomee2.tistory.com/132&quot;&gt;https://hyomee2.tistory.com/132&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783800319765&quot; style=&quot;color: #333333; text-align: start;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;1. 알림 조회 성능 개선 - 인덱스&quot; data-og-description=&quot;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다. 배경초기 알림 테이블(notifications)에는 별도의 인덱스가 없이 설계되었지만, 대용량 환경을 대비해&quot; data-og-host=&quot;hyomee2.tistory.com&quot; data-og-source-url=&quot;https://hyomee2.tistory.com/132&quot; data-og-url=&quot;https://hyomee2.tistory.com/132&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/emSEjk/dJMb8U84MNm/eHRmJ53KtVpM4Mk0V69rhk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/oZMuc/dJMb8PGHh3l/hRMKHZsLDV8dvmgoj95eEK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/JItNM/dJMb8U84MNl/StYHGM1vUWmeLP91XvzCMk/img.png?width=2698&amp;amp;height=208&amp;amp;face=0_0_2698_208&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://hyomee2.tistory.com/132&quot; data-source-url=&quot;https://hyomee2.tistory.com/132&quot;&gt;
&lt;div style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/emSEjk/dJMb8U84MNm/eHRmJ53KtVpM4Mk0V69rhk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/oZMuc/dJMb8PGHh3l/hRMKHZsLDV8dvmgoj95eEK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/JItNM/dJMb8U84MNl/StYHGM1vUWmeLP91XvzCMk/img.png?width=2698&amp;amp;height=208&amp;amp;face=0_0_2698_208');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;1. 알림 조회 성능 개선 - 인덱스&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다. 배경초기 알림 테이블(notifications)에는 별도의 인덱스가 없이 설계되었지만, 대용량 환경을 대비해&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;hyomee2.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 인덱스를 이용해서 조회 쿼리 시간을 크게 단축시킬 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 사용자가 체감하는 것은 쿼리 실행 시간이 아니라 API 응답 시간이므로,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 서비스 관점에서 성능을 검증하고자 k6을 이용해 부하테스트를 진행했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부하테스트&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1783800319766&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';

const BASE_URL = __ENV.BASE_URL || 'http://3.27.246.45:8080';

export const notificationLatency = new Trend('notification_latency', true);

export const options = {
    scenarios: {
        notification_load_test: {
            executor: 'ramping-vus',  // 동시 사용자수 점진 증가
            startVUs: 0,
            stages: [
                { duration: '20s', target: 20 },   // 워밍업
                { duration: '1m', target: 50 },   // 실제 측정 구간
                { duration: '20s', target: 0 },    // 종료
            ],
            gracefulRampDown: '30s',
        },
    },
    // thresholds: {
    //     // 알림 API에 대해서만 p95 기준 적용 (로그인 제외)
    //     'http_req_duration{name:notifications}': ['p(95)&amp;lt;300'],  // p95가 300ms 미만이어야 성공
    //     'http_req_failed{name:notifications}': ['rate&amp;lt;0.01'],
    // },
};


// 로그인
export function setup() {
    const loginRes = http.post(
        `${BASE_URL}/api/v1/auth/login`,
        JSON.stringify({
            email: 's5763305@naver.com',
            password: 'hyoungmi1015',
        }),
        { headers: { 'Content-Type': 'application/json' } }
    );

    check(loginRes, {
        'login success': (r) =&amp;gt; r.status === 200,
    });

    const token = loginRes.json('data.accessToken');

    if (!token) {
        throw new Error('login fail');
    }

    return { token };
}

// 실제 부하테스트
export default function (data) {
    const url = `${BASE_URL}/api/v1/notifications?size=10`;

    const res = http.get(url, {
        headers: {
            Authorization: `Bearer ${data.token}`,
        },
        tags: {
            name: 'notifications',
        },
    });

    notificationLatency.add(res.timings.duration);

    check(res, {
        'status is 200': (r) =&amp;gt; r.status === 200
    });

    if (res.status !== 200) {
        console.log(`status: ${res.status}`);
        console.log(`body: ${res.body}`);
    }

    sleep(0.2); // 실제 사용자 행동 간격
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 시나리오는 아래와 같이 구성했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 점진적으로 동시 사용자 증가&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1783800319769&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;            stages: [
                { duration: '20s', target: 20 },   // 워밍업
                { duration: '1m', target: 50 },   // 실제 측정 구간
                { duration: '20s', target: 0 },    // 종료
            ],&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음부터 100명의 사용자를 동시에 생성하지 않고,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 서비스 환경과 최대한 유사하게 진행해보고자 사용자가 점점 증가하는 상황을 가정했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 20초 동안 20명까지 증가 (워밍업)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 1분 동안 50명 유지하며 실제 성능 측정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 20초 동안 점진적으로 종료&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JVM 초기화, 커넥션 풀 생성 등의 영향이 있을 수 있기 때문에 이를 최소화하고 안정적인 상태에서 성능을 측정하고자 워밍업 구간을 두었다.&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 로그인은 한 번만 수행&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인의 경우는 우리가 테스트하고자 하는 API와 별도이지만 선행되어야 하므로 setup()에서 수행한 후,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모두 동일한 토큰을 이용하도록 구성하여 로그인 API의 성능이 결과에 영향을 주지 않도록 했다.&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;테스트 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 인덱스 적용 전,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/11Kj9/dJMcaglRQE3/kgFcxSBzoxYsKCpTk9x8Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/11Kj9/dJMcaglRQE3/kgFcxSBzoxYsKCpTk9x8Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/11Kj9/dJMcaglRQE3/kgFcxSBzoxYsKCpTk9x8Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F11Kj9%2FdJMcaglRQE3%2FkgFcxSBzoxYsKCpTk9x8Y0%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;2032&quot; height=&quot;1382&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 인덱스(Covering Index + PK Lookup)을 적용한 후의 부하테스트 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ju4TZ/dJMcabZfvyT/CwA1lkNkaJbrCB0mnQF3lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ju4TZ/dJMcabZfvyT/CwA1lkNkaJbrCB0mnQF3lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ju4TZ/dJMcabZfvyT/CwA1lkNkaJbrCB0mnQF3lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJu4TZ%2FdJMcabZfvyT%2FCwA1lkNkaJbrCB0mnQF3lK%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;2026&quot; height=&quot;1382&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과를 정리해보면 아래와 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 96.6279%; height: 70px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%; text-align: center;&quot;&gt;&lt;b&gt;지표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.9999%; text-align: center;&quot;&gt;&lt;b&gt;인덱스 적용 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7093%; text-align: center;&quot;&gt;&lt;b&gt;인덱스 적용 후&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.0814%; 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: 23.721%; text-align: center;&quot;&gt;&lt;b&gt;Average&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.9999%; text-align: center;&quot;&gt;9,670ms&lt;/td&gt;
&lt;td style=&quot;width: 24.7093%; text-align: center;&quot;&gt;1,980ms&lt;/td&gt;
&lt;td style=&quot;width: 23.0814%; text-align: center;&quot;&gt;&lt;b&gt;79.5%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%; text-align: center;&quot;&gt;&lt;b&gt;p95&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.9999%; text-align: center;&quot;&gt;15,530ms&lt;/td&gt;
&lt;td style=&quot;width: 24.7093%; text-align: center;&quot;&gt;3,330ms&lt;/td&gt;
&lt;td style=&quot;width: 23.0814%; text-align: center;&quot;&gt;&lt;b&gt;78.6%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%; text-align: center;&quot;&gt;&lt;b&gt;RPS(http_reqs/s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.9999%; text-align: center;&quot;&gt;3.0req/s&lt;/td&gt;
&lt;td style=&quot;width: 24.7093%; text-align: center;&quot;&gt;12.8req/s&lt;/td&gt;
&lt;td style=&quot;width: 23.0814%; text-align: center;&quot;&gt;&lt;b&gt;76.6%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;표를 확인하면 인덱스 적용 전에 비해 적용 후가 수치적으로 훨씬 개선된 것은 맞다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 알림 자체를 조회하는 쿼리는 지난 측정에 따르면 0.537ms에 불과한데, 평균 기준 나머지 약 1,980ms는 어디에서 발생했는지 의문이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 StopWatch를 이용하여 또 다른 병목 지점이 어디인지 파악해보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(이 부분은 %만 확인하면 된다고 생각해서 로컬에서 로그로 찍어보았다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1016&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLbrjO/dJMcabkxpMg/ulYQG16uwo75Q8iL21cSS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLbrjO/dJMcabkxpMg/ulYQG16uwo75Q8iL21cSS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLbrjO/dJMcabkxpMg/ulYQG16uwo75Q8iL21cSS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLbrjO%2FdJMcabkxpMg%2FulYQG16uwo75Q8iL21cSS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;412&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1016&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과는 확실했다.바로 count 쿼리가 대부분의 시간을 차지했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IQugd/dJMcabLDm3X/5hCWutFkUuDBEiv0X3Rid1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IQugd/dJMcabLDm3X/5hCWutFkUuDBEiv0X3Rid1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQugd/dJMcabLDm3X/5hCWutFkUuDBEiv0X3Rid1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIQugd%2FdJMcabLDm3X%2F5hCWutFkUuDBEiv0X3Rid1%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;321&quot; height=&quot;161&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 count 병목을 제거하고자 했다.&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;COUNT 집계 쿼리 병목을 어떻게 개선할 것인가?&lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;COUNT 연산을 제거하기 위해 두가지 방식을 고민했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 반정규화&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫번째로 생각한 방식은 members 테이블에 알림 개수를 저장하여 반정규화 하는 방식이었다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- DB 트랜잭션 내에서 관리 가능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 간단하게 조회 가능&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Redis을 이용한 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로는 Redis에 사용자별 알림 개수를 저장하고, 조회 시 DB가 아닌 Redis에서 바로 값을 가져오는 방식을 생각했다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 메모리 기반 저장소이므로 매우 빠르게 조회 가능&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Redis와 DB 간 데이터 정합성 추가적으로 관리 필요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 장애 발생 시 Redis와 DB의 값이 달라질 수 있어 복구 로직 필요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 COUNT 병목을 해결하는데 있어서는 &quot;반정규화&quot; 방식을 사용하기로 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 사용자별로 카운트 -&amp;gt; 실시간으로 급격히 증가하는 값이 아님&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- PK 조회이므로 반정규화로도 충분히 빠를 것으로 판단&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Redis를 도입할 만큼의 트래픽은 아님&lt;/p&gt;
&lt;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;COUNT 집계 쿼리 개선 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2078&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5e2oA/dJMcacDRKvM/KOR6qet0ALTF5veLowXzx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5e2oA/dJMcacDRKvM/KOR6qet0ALTF5veLowXzx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5e2oA/dJMcacDRKvM/KOR6qet0ALTF5veLowXzx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5e2oA%2FdJMcacDRKvM%2FKOR6qet0ALTF5veLowXzx0%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;2078&quot; height=&quot;1388&quot; data-origin-width=&quot;2078&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 96.6279%; height: 70px;&quot; border=&quot;1&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;COUNT 제거 후&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 style=&quot;text-align: center;&quot;&gt;&lt;b&gt;Average&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;1,980ms&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;157.0ms&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;97.4%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;p95&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3,330ms&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;179.6ms&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;97.1%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;TPS(http_reqs/s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;12.8req/s&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;76.2req/s&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;66.2%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 95.8148%; height: 70px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 19.6295%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;지표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6371%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;인덱스 적용 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7202%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;인덱스 적용 후&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.1738%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;COUNT 제거 후&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.5086%; height: 19px; text-align: center;&quot;&gt;&lt;b&gt;개선율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 19.6295%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;Average&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6371%; height: 17px; text-align: center;&quot;&gt;9,670ms&lt;/td&gt;
&lt;td style=&quot;width: 19.7202%; height: 17px; text-align: center;&quot;&gt;1,980ms&lt;/td&gt;
&lt;td style=&quot;width: 19.1738%; height: 17px; text-align: center;&quot;&gt;157.0ms&lt;/td&gt;
&lt;td style=&quot;width: 18.5086%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;98.4%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 19.6295%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;p95&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6371%; height: 17px; text-align: center;&quot;&gt;15,530ms&lt;/td&gt;
&lt;td style=&quot;width: 19.7202%; height: 17px; text-align: center;&quot;&gt;3,330ms&lt;/td&gt;
&lt;td style=&quot;width: 19.1738%; height: 17px; text-align: center;&quot;&gt;179.6ms&lt;/td&gt;
&lt;td style=&quot;width: 18.5086%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;98.8%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 19.6295%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;RPS(http_reqs/s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6371%; height: 17px; text-align: center;&quot;&gt;3.0req/s&lt;/td&gt;
&lt;td style=&quot;width: 19.7202%; height: 17px; text-align: center;&quot;&gt;12.8req/s&lt;/td&gt;
&lt;td style=&quot;width: 19.1738%; height: 17px; text-align: center;&quot;&gt;76.2req/s&lt;/td&gt;
&lt;td style=&quot;width: 18.5086%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;96.1%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 이전에는 데이터베이스에서 정규화에 대한 내용을 배우며 &quot;정규화할 수 있으면 항상 하는게 좋은거 아닌가?&quot; 라는 생각을 했었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이론적으로는 상황에 따라 반정규화가 더 적합할 수도 있다고 했지만, 직접 와닿지는 않은 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이번 기회를 통해 쿼리 시간은 크게 단축했는데 API 응답 속도는 단축되지 않은 것을 보면서 의아함을 느꼈다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 사용자가 실제로 체감하는 것은 API 응답 시간일 것이므로,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 쿼리 성능이 좋아졌다는 결과에 만족하는 것이 아니라&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 병목이 어디에 있는지 파악하고 실제 API 응답 시간을 최적화하는데에 초점을 맞춰야겠다는 깨달음을 얻었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 StopWatch을 이용해 API 내부적으로 구간별 실행 시간을 측정하며 병목 지점을 직접 파악하고 개선하는 것도 의미있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;어디가 병목인지 어떻게 파악하지?&quot; 라는 생각으로 &quot;집계 쿼리가 문제인가?&quot; 라는 감을 가지고 시작했지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;측정을 기반으로 근거를 가지고 병목을 찾아 개선하는 과정의 중요성도 배울 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로도 최적화가 필요한 상황에서는 병목 지점이 어디인지 파악하고, 찾은 병목에 대해 가장 적합한 개선 방안을 선택하고자 한다.&lt;/p&gt;</description>
      <category>projects/travlocks</category>
      <category>K6</category>
      <category>redis</category>
      <category>반정규화</category>
      <category>병목</category>
      <category>인덱스</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/133</guid>
      <comments>https://hyomee2.tistory.com/133#entry133comment</comments>
      <pubDate>Fri, 3 Jul 2026 14:08:45 +0900</pubDate>
    </item>
    <item>
      <title>1. 알림 조회 성능 개선 - 인덱스</title>
      <link>https://hyomee2.tistory.com/132</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 문제 및 개선 사항을 정리한 글입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기 알림 테이블(notifications)에는 별도의 인덱스가 없이 설계되었지만,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;대용량 환경을 대비해 안정적인 성능을 유지할 수 있도록 최적화가 필요하다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1차적으로는 복합 인덱스&lt;/b&gt;를 통해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2차적으로는 Covering Index 전략&lt;/b&gt;을 이용해 조회 성능을 최적화하려 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;커서 기반 페이징을 이용했기에 사용한 SQL문은 아래와 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783794731175&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT notification_id, created_at, actor_id, actor_nickname_snapshot, receiver_id, template_id, type
FROM notifications
WHERE receiver_id = 1
ORDER BY created_at DESC, notification_id DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;대용량 데이터 삽입&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 운영 환경을 가정하여 40만 건의 더미 데이터를 아래 SQL문을 통해 삽입해주었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783794700903&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET autocommit = 0;

INSERT INTO notifications (
    created_at, actor_id, actor_nickname_snapshot, receiver_id, template_id, type
)
SELECT
    NOW(6) - INTERVAL FLOOR(RAND() * 365) DAY
        - INTERVAL FLOOR(RAND() * 86400) SECOND,
    FLOOR(1 + RAND() * 3),
    ELT(FLOOR(1 + RAND() * 3), '판당이', '효미', '조아'),
    CASE
        WHEN RAND() &amp;lt; 0.7 THEN 1
        WHEN RAND() &amp;lt; 0.9 THEN 2
        ELSE 3
        END,
    FLOOR(1 + RAND() * 100000),
    ELT(FLOOR(1 + RAND() * 2), 'TEMPLATE_FAVORITED', 'TEMPLATE_REMIXED')
FROM information_schema.columns a
         CROSS JOIN information_schema.columns b
LIMIT 400000;

COMMIT;
SET autocommit = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;데이터 수 확인하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL문을 활용하여 40만개의 데이터 수를 확인했다.&lt;/p&gt;
&lt;pre id=&quot;code_1783794700905&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT COUNT(*) AS cnt FROM notifications;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6NYRy/dJMb99SXO5q/KlKDV3F2KnkewOiutycUgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6NYRy/dJMb99SXO5q/KlKDV3F2KnkewOiutycUgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6NYRy/dJMb99SXO5q/KlKDV3F2KnkewOiutycUgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6NYRy%2FdJMb99SXO5q%2FKlKDV3F2KnkewOiutycUgK%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;160&quot; height=&quot;125&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;조회 성능 비교&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[기존] 인덱스 미적용 상태의 실행&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1783794745824&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT notification_id, created_at, actor_id, actor_nickname_snapshot, receiver_id, template_id, type
FROM notifications
WHERE receiver_id = 1
ORDER BY created_at DESC, notification_id DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;EXPLAIN ANALYZE를 확인해 실제 측정값을 확인해보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;평균값을 내기 위해 총 5번 실행을 해주었으며, 첫 시도는 제외했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qkwAc/dJMcaaTvTgK/r9fOrY5xIPH6sDKwNuQHW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qkwAc/dJMcaaTvTgK/r9fOrY5xIPH6sDKwNuQHW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qkwAc/dJMcaaTvTgK/r9fOrY5xIPH6sDKwNuQHW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqkwAc%2FdJMcaaTvTgK%2Fr9fOrY5xIPH6sDKwNuQHW1%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;2524&quot; height=&quot;220&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2534&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCrhZZ/dJMcah6e5eD/IcxRWKur4X2a8X0saukZ41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCrhZZ/dJMcah6e5eD/IcxRWKur4X2a8X0saukZ41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCrhZZ/dJMcah6e5eD/IcxRWKur4X2a8X0saukZ41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCrhZZ%2FdJMcah6e5eD%2FIcxRWKur4X2a8X0saukZ41%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;2534&quot; height=&quot;218&quot; data-origin-width=&quot;2534&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vudMu/dJMcajbLQqH/hvGr7nx9qmG1fKdcxHxlGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vudMu/dJMcajbLQqH/hvGr7nx9qmG1fKdcxHxlGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vudMu/dJMcajbLQqH/hvGr7nx9qmG1fKdcxHxlGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvudMu%2FdJMcajbLQqH%2FhvGr7nx9qmG1fKdcxHxlGK%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;2524&quot; height=&quot;216&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSTei8/dJMcagM2pcC/ZXJeU00qBCsDbeiSr3Xcl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSTei8/dJMcagM2pcC/ZXJeU00qBCsDbeiSr3Xcl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSTei8/dJMcagM2pcC/ZXJeU00qBCsDbeiSr3Xcl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSTei8%2FdJMcagM2pcC%2FZXJeU00qBCsDbeiSr3Xcl1%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;2540&quot; height=&quot;222&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKrz9O/dJMcacX6ecw/slcQPkg4gVe0OYSIleG0VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKrz9O/dJMcacX6ecw/slcQPkg4gVe0OYSIleG0VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKrz9O/dJMcacX6ecw/slcQPkg4gVe0OYSIleG0VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKrz9O%2FdJMcacX6ecw%2FslcQPkg4gVe0OYSIleG0VK%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;2548&quot; height=&quot;210&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;해석&lt;/b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래에서부터 위로 진행된다고 생각하면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Table scan on n&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 테이블 n 전체를 읽는다. (Full Table Scan)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Filter&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 조건 필터링&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Sort&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 필터링된 데이터를 정렬 규칙에 따라 정렬&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Limit: 10 row(s)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; Limit 10이 적용되어 최종적으로 10개의 데이터 반환한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Full Table Scan 발생 (약 40만 건 전체 탐색)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 필터링 후 정렬 수행 (매우 큰 정렬 비용, 실행 시간의 대부분이 정렬 비용)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 평균 DB 내부 실행 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;242ms&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- WHERE과 ORDER BY를 동시에 만족하는 복합 인덱스의 부재&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 대량의 정렬 비용 발생&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- LIMIT가 있지만 전부 수행되는 정렬&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;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[1차 개선] 복합 인덱스 설계 -&amp;gt; '정렬 제거'에 집중&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 WHERE과 ORDER BY를 동시에 만족하도록 복합 인덱스를 설계해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1783794700907&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE INDEX idx_notifications_receiver_created_id
ON notifications (receiver_id, created_at DESC, notification_id DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3OvWq/dJMb99L2J28/GfU63ishz7PXbl25dsIIqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3OvWq/dJMb99L2J28/GfU63ishz7PXbl25dsIIqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3OvWq/dJMb99L2J28/GfU63ishz7PXbl25dsIIqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3OvWq%2FdJMb99L2J28%2FGfU63ishz7PXbl25dsIIqk%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;717&quot; height=&quot;70&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SELECT문은 위에서와 같이 실행해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1783794759009&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT notification_id, created_at, actor_id, actor_nickname_snapshot, receiver_id, template_id, type
FROM notifications
WHERE receiver_id = 1
ORDER BY created_at DESC, notification_id DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마찬가지로 EXPLAIN ANALYZE를 확인해 실제 측정값을 확인해보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;평균값을 내기 위해 총 5번 실행을 해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2544&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPUhL/dJMcagzpGFr/oNkbz1xPUN7ZppMpYqYR70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPUhL/dJMcagzpGFr/oNkbz1xPUN7ZppMpYqYR70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPUhL/dJMcagzpGFr/oNkbz1xPUN7ZppMpYqYR70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPUhL%2FdJMcagzpGFr%2FoNkbz1xPUN7ZppMpYqYR70%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;2544&quot; height=&quot;154&quot; data-origin-width=&quot;2544&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0BBlJ/dJMcaaeWQeV/M5tMKlMfIiInh3VLyoimUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0BBlJ/dJMcaaeWQeV/M5tMKlMfIiInh3VLyoimUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0BBlJ/dJMcaaeWQeV/M5tMKlMfIiInh3VLyoimUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0BBlJ%2FdJMcaaeWQeV%2FM5tMKlMfIiInh3VLyoimUK%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;2546&quot; height=&quot;148&quot; data-origin-width=&quot;2546&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2534&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lEZ0i/dJMcacqjyGE/HrR8hGvRDu5dA1uviOwslk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lEZ0i/dJMcacqjyGE/HrR8hGvRDu5dA1uviOwslk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lEZ0i/dJMcacqjyGE/HrR8hGvRDu5dA1uviOwslk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlEZ0i%2FdJMcacqjyGE%2FHrR8hGvRDu5dA1uviOwslk%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;2534&quot; height=&quot;144&quot; data-origin-width=&quot;2534&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbyHAh/dJMcadW0aSn/9IwRASGixKEEBw1KTYKUa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbyHAh/dJMcadW0aSn/9IwRASGixKEEBw1KTYKUa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbyHAh/dJMcadW0aSn/9IwRASGixKEEBw1KTYKUa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbyHAh%2FdJMcadW0aSn%2F9IwRASGixKEEBw1KTYKUa1%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;2540&quot; height=&quot;148&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2536&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clzVeb/dJMcabrmG13/EKiEPqrXi0L8bv8pYDQpAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clzVeb/dJMcabrmG13/EKiEPqrXi0L8bv8pYDQpAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clzVeb/dJMcabrmG13/EKiEPqrXi0L8bv8pYDQpAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclzVeb%2FdJMcabrmG13%2FEKiEPqrXi0L8bv8pYDQpAK%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;2536&quot; height=&quot;154&quot; data-origin-width=&quot;2536&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Full Scan 제거&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Filesort 제거&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Index range scan 적용&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 평균 DB 내부 실행 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;5.5ms&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[2차 개선] Covering Index + PK Lookup -&amp;gt; '접근 구조'에 집중&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1차 개선을 통해서도 약 97.7%의 성능 향상이 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1차 개선에서는 쿼리에 필요한 모든 컬럼을 인덱스가 포함하는 것은 아니어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1) Index 탐색 2) PK 조회 3) 테이블 접근(랜덤 I/O)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 단계를 거치는데, Covering Index를 통해 랜덤 I/O를 제거하고, 디스크 접근을 최소화하고자 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1783794775976&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT n.notification_id, n.created_at, n.actor_id,
       n.actor_nickname_snapshot, n.receiver_id,
       n.template_id, n.type
FROM notifications n
    JOIN (
        SELECT notification_id
        FROM notifications
        WHERE receiver_id = 1
        ORDER BY created_at DESC, notification_id DESC
        LIMIT 10
    ) as temp ON n.notification_id = temp.notification_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 쿼리를 개선함에 따라&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서브쿼리에서는 인덱스만 사용해서 notification_id만 추출하고 테이블 접근이 없고(Covering Index)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 PK lookup을 10회 수행하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Covering Index에 대해 조금 더 설명하자면,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리에 필요한 모든 컬럼을 인덱스가 포함하고 있어 테이블에 다시 접근하지 않아도 되는 인덱스를 의미한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QxuUJ/dJMcadvPY5J/aOPhPpDcAWQakYgBWx6Jf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QxuUJ/dJMcadvPY5J/aOPhPpDcAWQakYgBWx6Jf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QxuUJ/dJMcadvPY5J/aOPhPpDcAWQakYgBWx6Jf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQxuUJ%2FdJMcadvPY5J%2FaOPhPpDcAWQakYgBWx6Jf1%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;2596&quot; height=&quot;382&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GSxuT/dJMcahLRvcL/rcwujAYUdqkSD52FRePYik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GSxuT/dJMcahLRvcL/rcwujAYUdqkSD52FRePYik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GSxuT/dJMcahLRvcL/rcwujAYUdqkSD52FRePYik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGSxuT%2FdJMcahLRvcL%2FrcwujAYUdqkSD52FRePYik%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;2590&quot; height=&quot;388&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSK2nv/dJMcaccIzyN/y1EudXx6J5D6oWX9OuZi80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSK2nv/dJMcaccIzyN/y1EudXx6J5D6oWX9OuZi80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSK2nv/dJMcaccIzyN/y1EudXx6J5D6oWX9OuZi80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSK2nv%2FdJMcaccIzyN%2Fy1EudXx6J5D6oWX9OuZi80%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;2580&quot; height=&quot;388&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmBsGz/dJMcagTKJ7v/TuLwnCxvjyreWdrLOht3IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmBsGz/dJMcagTKJ7v/TuLwnCxvjyreWdrLOht3IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmBsGz/dJMcagTKJ7v/TuLwnCxvjyreWdrLOht3IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmBsGz%2FdJMcagTKJ7v%2FTuLwnCxvjyreWdrLOht3IK%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;2592&quot; height=&quot;392&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2582&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mwm1B/dJMcaazfCea/V68K572KJCjLIEQmyOIkHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mwm1B/dJMcaazfCea/V68K572KJCjLIEQmyOIkHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mwm1B/dJMcaazfCea/V68K572KJCjLIEQmyOIkHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMwm1B%2FdJMcaazfCea%2FV68K572KJCjLIEQmyOIkHk%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;2582&quot; height=&quot;376&quot; data-origin-width=&quot;2582&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해석&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Covering index range scan&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 커버링 인덱스 이용. 최신순 조회를 하므로 인덱스를 뒤에서부터 읽어서 reverse가 붙어있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Filter&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 조건 필터링&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Limit: 10 row(s)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 10개의 데이터 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Materialize&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 찾은 데이터를 메모리에 temp 테이블로 저장 (임시 테이블)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Table Scan on temp&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; temp 테이블의 10개 데이터 스캔&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Nested loop inner join&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; temp의 각 notification_id마다 PK 조회&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Single-row index lookup&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; PK로 한 건씩 조회&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 테이블 접근 최소화&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 평균 DB 내부 실행 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0.537ms&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1차 개선 VS 2차 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB에서 Secondary Index 리프에는 '인덱스 컬럼 + PK'만 저장되기 때문에 실제 row 데이터는 PK를 통해 다시 읽어야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차 개선&lt;/b&gt;에서는 인덱스에 있는 컬럼 이외에도 조회해야하는 데이터가 있기 때문에&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;PK로 row마다 테이블에 접근해야 해서 I/O가 생기게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2차 개선&lt;/b&gt;의 서브 쿼리에는 인덱스 안에 필요한 컬럼이 있기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서브 쿼리 내에서는 테이블 접근이 없고&lt;/b&gt;, 외&lt;b&gt;부 JOIN에서만 PK로 테이블에 10번 접근&lt;/b&gt;하기 때문에 더 좋은 성능을 갖게 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 29px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 9.88373%; text-align: center; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;개선 전&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.8837%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;1차 개선&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.1279%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;2차 개선&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.2907%; text-align: center; height: 19px;&quot;&gt;&lt;b&gt;실행시간 개선율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 9.88373%; text-align: center; height: 10px;&quot;&gt;&lt;b&gt;평균&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; text-align: center; height: 10px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;242&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.8837%; text-align: center; height: 10px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;5.5&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.1279%; text-align: center; height: 10px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;0.537&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.2907%; text-align: center; height: 10px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;99.8%&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(단위: ms)&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;삽입 성능 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조회 최적화를 위해 보조 인덱스를 도입했고, 조회 성능은 약 99.8%가 향상되는 효과가 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만, 인덱스를 하나 추가 하게 되면, row를 하나 삽입할 때 인덱스 개수만큼 B+Tree에 삽입이 발생하게 된다. (O(log N))&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 O(log N)보다 체감이 더 클 수도 있는데,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 B+Tree 리프 페이지가 16KB이기 때문에 새 데이터가 들어왔을 때 해당 페이지에 공간이 없으면 페이지 분할이 발생하기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 보조인덱스를 1개 추가해주었는데, 삽입 성능도 측정해볼 필요성을 느껴 측정해보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 40만개의 데이터에 추가로 20만개의 데이터를 넣어주었다.&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;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[기존] 인덱스 미적용 상태의 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 인덱스가 없을 때의 실행 결과로, 평균값을 내기 위해 총 5번 실행을 해주었으며, 첫 시도는 제외했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QUmda/dJMcacPHte8/kHuw8xRGzGKCbo5hAHkkX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QUmda/dJMcacPHte8/kHuw8xRGzGKCbo5hAHkkX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QUmda/dJMcacPHte8/kHuw8xRGzGKCbo5hAHkkX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQUmda%2FdJMcacPHte8%2FkHuw8xRGzGKCbo5hAHkkX1%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;1856&quot; height=&quot;920&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qSaRQ/dJMcac3cojq/1JBuM7fp39n00n2KEv25Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qSaRQ/dJMcac3cojq/1JBuM7fp39n00n2KEv25Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qSaRQ/dJMcac3cojq/1JBuM7fp39n00n2KEv25Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqSaRQ%2FdJMcac3cojq%2F1JBuM7fp39n00n2KEv25Wk%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;1812&quot; height=&quot;926&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BLDYr/dJMcad1473B/3jYdaj6PSVFQ8QYF55wbF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BLDYr/dJMcad1473B/3jYdaj6PSVFQ8QYF55wbF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BLDYr/dJMcad1473B/3jYdaj6PSVFQ8QYF55wbF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBLDYr%2FdJMcad1473B%2F3jYdaj6PSVFQ8QYF55wbF0%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;1844&quot; height=&quot;938&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GEO62/dJMcafMrtEE/PDlZ6MrcgoitDkhltH7P71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GEO62/dJMcafMrtEE/PDlZ6MrcgoitDkhltH7P71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GEO62/dJMcafMrtEE/PDlZ6MrcgoitDkhltH7P71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGEO62%2FdJMcafMrtEE%2FPDlZ6MrcgoitDkhltH7P71%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;1834&quot; height=&quot;926&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCbvX/dJMcabws9j1/UETCxWifbzOYTKKiNk5iW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCbvX/dJMcabws9j1/UETCxWifbzOYTKKiNk5iW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCbvX/dJMcabws9j1/UETCxWifbzOYTKKiNk5iW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCbvX%2FdJMcabws9j1%2FUETCxWifbzOYTKKiNk5iW1%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;1822&quot; height=&quot;916&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 평균 DB 내부 실행 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2826.4ms&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[1차 개선] 복합 인덱스 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 인덱스가 있을 때의 실행 결과로, 평균값을 내기 위해 총 5번 실행을 해주었으며, 첫 시도는 제외했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpjY3l/dJMcahcnX6B/3mpxKLFGszL8lDoMvePin1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpjY3l/dJMcahcnX6B/3mpxKLFGszL8lDoMvePin1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpjY3l/dJMcahcnX6B/3mpxKLFGszL8lDoMvePin1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpjY3l%2FdJMcahcnX6B%2F3mpxKLFGszL8lDoMvePin1%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;1864&quot; height=&quot;920&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1850&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/omzI9/dJMcagxLOsr/dNXKbJKbQdHGP7kijhJDk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/omzI9/dJMcagxLOsr/dNXKbJKbQdHGP7kijhJDk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/omzI9/dJMcagxLOsr/dNXKbJKbQdHGP7kijhJDk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FomzI9%2FdJMcagxLOsr%2FdNXKbJKbQdHGP7kijhJDk0%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;1850&quot; height=&quot;918&quot; data-origin-width=&quot;1850&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfc58f/dJMcadVj8nV/eEarWpOzc9s1F8zls4BZ51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfc58f/dJMcadVj8nV/eEarWpOzc9s1F8zls4BZ51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfc58f/dJMcadVj8nV/eEarWpOzc9s1F8zls4BZ51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfc58f%2FdJMcadVj8nV%2FeEarWpOzc9s1F8zls4BZ51%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;1824&quot; height=&quot;930&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cd06U/dJMcaaqOviE/J7fiLkxy33gCjqfkEd3NIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cd06U/dJMcaaqOviE/J7fiLkxy33gCjqfkEd3NIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cd06U/dJMcaaqOviE/J7fiLkxy33gCjqfkEd3NIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCd06U%2FdJMcaaqOviE%2FJ7fiLkxy33gCjqfkEd3NIk%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;1812&quot; height=&quot;908&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IgrHs/dJMcabpJ4u6/0qrpY1EYF6l69VCWEucU70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IgrHs/dJMcabpJ4u6/0qrpY1EYF6l69VCWEucU70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IgrHs/dJMcabpJ4u6/0qrpY1EYF6l69VCWEucU70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIgrHs%2FdJMcabpJ4u6%2F0qrpY1EYF6l69VCWEucU70%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;1830&quot; height=&quot;922&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 평균 DB 내부 실행 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;3720.8ms&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 27px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 11.8604%; height: 27px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 15.4651%; height: 27px;&quot;&gt;&lt;b&gt;인덱스 X&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 20.814%; height: 27px;&quot;&gt;&lt;b&gt;인덱스 O&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 51.7441%; height: 27px;&quot;&gt;&lt;b&gt;INSERT 수행 시간 증가율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 30px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 11.8604%; height: 30px;&quot;&gt;&lt;b&gt;평균&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 15.4651%; height: 30px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;2826.4&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 20.814%; height: 30px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;3720.8&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 51.7441%; height: 30px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;31.7%&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(단위: ms)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 결과를 통해 보조 인덱스를 추가함에 따라 조회 성능은 증가하였지만, 쓰기 성능은 감소한 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로,&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;알림은 현재 읽기 빈도가 쓰기 빈도가 훨씬 크다고 판단했고, 읽기 성능이 전체 시스템 성능에 더 중요하다고 판단해 인덱스를 도입하기로 결정했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 쓰기 성능이 훨씬 중요한 기능에서는 아무리 조회 성능이 좋아졌다고 해서 그에 따른 쓰기 성능이 감소할 것이기 때문에 다른 결정을 내려야 할 수도 있다.&amp;nbsp;읽기 성능, 쓰기 성능의 트레이드 오프를 고려하여 기능에 적절한 최적화 방식을 선택해야함을 다시금 깨달았다.&lt;/p&gt;</description>
      <category>projects/travlocks</category>
      <category>covering index</category>
      <category>db</category>
      <category>explain analyze</category>
      <category>PK Lookup</category>
      <category>데이터베이스</category>
      <category>성능개선</category>
      <category>인덱스</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/132</guid>
      <comments>https://hyomee2.tistory.com/132#entry132comment</comments>
      <pubDate>Fri, 3 Jul 2026 00:02:54 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] #42897 도둑질, 원형 DP</title>
      <link>https://hyomee2.tistory.com/131</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42897&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42897&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1781102901434&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42897&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dqKS5R/dJMb8PGEpj1/t42X8LSNsgmuFCJdUpexT1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/W9nhK/dJMb8UHXtg8/U7f26mmlwmhKccGMd4IqQK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42897&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42897&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dqKS5R/dJMb8PGEpj1/t42X8LSNsgmuFCJdUpexT1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/W9nhK/dJMb8UHXtg8/U7f26mmlwmhKccGMd4IqQK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제의 핵심은 &lt;b&gt;&quot;원형 구조를 어떻게 처리할 것인가&quot;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집들이 원형으로 배치되어 있으므로, 0번 집을 털고 마지막 집도 터는 경우는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 경우를 나눠줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 0번 집을 터는 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 마지막 집은 선택할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 따라서 탐색 범위는 0 ~ &lt;b&gt;(length -2)&lt;/b&gt;가 되고&lt;/p&gt;
&lt;pre id=&quot;code_1781103119241&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 2; i &amp;lt; money.length - 1; i++)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 훔칠 수 있는 돈의 최댓값은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1781103167759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dp[money.length - 2]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 0번 집을 털지 않는 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 마지막 집을 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이때는 0번째집은 무시하고 탐색 범위는 1 ~ (length - 1)로 진행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1781103298760&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dp[1] = money[1];

for (int i = 2; i &amp;lt; money.length; i++)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 훔칠 수 있는 돈의 최댓값은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1781103324712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dp[money.length - 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1781103389613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int[] money) {
        int answer = 0;
        int[] dp = new int[money.length];
        
        // 첫번째 집 방문하는 경우
        dp[0] = money[0];
        dp[1] = dp[0];
        
        for (int i = 2; i &amp;lt; money.length - 1; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + money[i]);
        }
        
        answer = dp[money.length - 2];
        
        // 첫번째 집 방문 안하는 경우
        dp = new int[money.length];
        dp[1] = money[1];
        for (int i = 2; i &amp;lt; money.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + money[i]);
        }

        return (answer &amp;lt; dp[money.length - 1]) ? dp[money.length - 1] : answer;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>코딩테스트/프로그래머스</category>
      <category>DP</category>
      <category>도둑질</category>
      <category>원형 DP</category>
      <category>프로그래머스</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/131</guid>
      <comments>https://hyomee2.tistory.com/131#entry131comment</comments>
      <pubDate>Wed, 10 Jun 2026 23:56:38 +0900</pubDate>
    </item>
    <item>
      <title>[백준] #9205 맥주 마시면서 걸어가기, BFS</title>
      <link>https://hyomee2.tistory.com/130</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/9205&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/9205&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시도 1&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 제출한 코드는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 읽고 생각해보면서, 좌표와 좌표 사이의 거리가 1000 이하면 되는 문제네? 라고 생각하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 알고리즘 없이 구현하는 문제라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 잘못 생각한 점은 주어진 편의점을 &lt;b&gt;&quot;순서대로&quot;&lt;/b&gt; 방문하는 것이 아니라 방문 순서를 정할 수 있는데, 그 점을 놓쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 아래 코드를 보니 굳이 시작, 편의점, 페스티벌 좌표를 나눌 필요없이 for문을 이용하면 되는데 불필요한 코드 분리가 있었던 게 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1774874967411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine()); // 테스트 케이스 수
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        // 배열로 상황 만들기
        for (int i = 0; i &amp;lt; t; i++) {
            int n = Integer.parseInt(br.readLine()); // 맥주 파는 편의점 수
            st = new StringTokenizer(br.readLine(), &quot; &quot;);
            int[][] loc = new int[n + 2][2];
            loc[0] = new int[]{Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken())};

            for (int j = 0; j &amp;lt; n; j++) {
                st = new StringTokenizer(br.readLine(), &quot; &quot;);
                loc[j + 1] = new int[]{Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken())};
            }

            st = new StringTokenizer(br.readLine(), &quot; &quot;);
            loc[n + 1] = new int[]{Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken())};

            for (int j = 0; j &amp;lt; n + 1; j++) {
                int distance = (Math.abs(loc[j + 1][0] - loc[j][0]) + Math.abs(loc[j + 1][1] - loc[j][1]));
                if (distance &amp;gt; 1000) {
                    sb.append(&quot;sad&quot;).append(&quot;\n&quot;);
                    break;
                }

                if (j == n) {
                    sb.append(&quot;happy&quot;).append(&quot;\n&quot;);
                }
            }
        }

        System.out.println(sb);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시도 2&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아~ 편의점 방문 순서가 입력 순서가 아닌 것을 깨닫고, BFS를 생각하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 어떻게든 락 페스티벌에 도착하면 되는 것이기 때문에, 너비 우선 탐색을 이용하면 초반에 거를 수 있는 경로가 있을 것이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 불필요하게 시작점, 편의점, 페스티벌 장소 코드를 분리했던 것을 한 번의 for문으로 입력 받았고, bfs을 이용해서 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표로 visited 같은 배열을 만드는 것은 불필요하다고 생각해서 인덱스를 이용해 흐름을 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 중요한 점은 거리니까 절댓값을 해줘야 한다는 점..!&lt;/p&gt;
&lt;pre id=&quot;code_1774874889530&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    static Deque&amp;lt;Integer&amp;gt; queue;
    static boolean[] visited;
    static int n;
    static int[][] loc;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine()); // 테스트 케이스 수
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        // 배열로 상황 만들기
        for (int i = 0; i &amp;lt; t; i++) {
            n = Integer.parseInt(br.readLine()); // 맥주 파는 편의점 수
            visited = new boolean[n + 2];
            loc = new int[n + 2][2];

            // loc 만들기
            for (int j = 0; j &amp;lt; n + 2; j++) {
                st = new StringTokenizer(br.readLine(), &quot; &quot;);
                loc[j] = new int[]{Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken())};
            }

            String answer = bfs() ? &quot;happy&quot; : &quot;sad&quot;;
            sb.append(answer).append(&quot;\n&quot;);
        }

        System.out.println(sb);
    }

    public static boolean bfs() {
        queue = new ArrayDeque&amp;lt;&amp;gt;();
        visited[0] = true;
        queue.offer(0);

        while (!queue.isEmpty()) {
            int newIndex = queue.poll();

            if (newIndex == n + 1) {
                return true;
            }

            for (int i = 1; i &amp;lt; n + 2; i++) {
                if (!visited[i]) {
                    int distance = Math.abs(loc[newIndex][0] - loc[i][0]) + Math.abs(loc[newIndex][1] - loc[i][1]);

                    if (distance &amp;lt;= 1000) {
                        visited[i] = true;
                        queue.offer(i);
                    }
                }
            }
        }

        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>코딩테스트/백준</category>
      <category>BFS</category>
      <category>맥주 마시면서 걸어가기</category>
      <category>백준</category>
      <category>백준 9205</category>
      <category>자바</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/130</guid>
      <comments>https://hyomee2.tistory.com/130#entry130comment</comments>
      <pubDate>Mon, 30 Mar 2026 21:55:59 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] #87694 아이템 줍기</title>
      <link>https://hyomee2.tistory.com/129</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/87694&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/87694&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773665023932&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/87694&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ex8Z0/dJMb8Z3p40A/svECoIQStvmkkfVKYq0UFk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/mKe6Q/dJMb8TB8nZI/gQlQdkjdE1vUxXVoodEGDk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/87694&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/87694&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ex8Z0/dJMb8Z3p40A/svECoIQStvmkkfVKYq0UFk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/mKe6Q/dJMb8TB8nZI/gQlQdkjdE1vUxXVoodEGDk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;흔히 마주하는 BFS/DFS 문제(맵, 미로찾기 등)와 비슷해 보이지만 해당 문제는 각 칸에 대해 0/1(벽인지 길인지)로 주어져있지는 않다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 어떻게 해야하는지 고민했는데, 그럼 내가 직접 0/1 구조(이동할 수 있는 길은 1, 안되는 길은 0)로 map을 만들어주면 되겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 그렇게 하면 문제가 발생하는데, 좌표가 붙어있는데 길은 없을 경우 1과 1이 인접하여 길이 있다고 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이러한 문제를 해결하는 방법 중 하나는 모든 좌표를 2배로 늘리는 것이다. 그럼 좌표들 사이에 길이 없을 경우 0이 되어 길이 없음을 코드 상으로 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 우선 map을 만들어줄건데, 모두 1로 넣어준 뒤, 내부는 0으로 바꿔준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;-&amp;gt; 이 과정을 거치면 이제는 흔히 마주하던 미로 탐색 문제와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 가장 짧은 거리를 구해줘야 하므로 BFS를 수행하고 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;-&amp;gt; 이때 모든 좌표에 2배 해주었으므로 다시 2배를 나누어 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1773665456645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    static int[][] map;
    public int solution(int[][] rectangle, int characterX, int characterY, int itemX, int itemY) {
        map = new int[101][101];
        
        // map 만들기 - 1로 채움
        for (int[] rect : rectangle) {
            for (int i = rect[0] * 2; i &amp;lt;= rect[2] * 2; i++) {
                for (int j = rect[1] * 2; j &amp;lt;= rect[3] * 2; j++) {
                    map[i][j] = 1;
                }
            }
        }
        
        // map 만들기 - 내부는 0으로 빈칸 표시
        for (int[] rect : rectangle) {
            for (int i = rect[0] * 2 + 1; i &amp;lt; rect[2] * 2; i++) {
                for (int j = rect[1] * 2 + 1; j &amp;lt; rect[3] * 2; j++) {
                    map[i][j] = 0;
                }
            }
        }
        
        return bfs(characterX * 2, characterY * 2, itemX * 2, itemY * 2);
    }
    
    private int bfs(int startX, int startY, int itemX, int itemY) {
        Deque&amp;lt;int[]&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
        boolean[][] visited = new boolean[101][101];
        int[] dx = {1, 0, -1, 0};
        int[] dy = {0, -1, 0, 1};
        
        queue.offer(new int[]{startX, startY, 0});
        visited[startX][startY] = true;
        
        while(!queue.isEmpty()) {
            int[] location = queue.poll();
            
            if (location[0] == itemX &amp;amp;&amp;amp; location[1] == itemY) {
                return location[2] / 2;
            }
            
            for (int i = 0; i &amp;lt; 4; i++) {
                int nx = location[0] + dx[i];
                int ny = location[1] + dy[i];
                
                if (nx &amp;gt;= 0 &amp;amp;&amp;amp; nx &amp;lt;= 100 &amp;amp;&amp;amp; ny &amp;gt;= 0 &amp;amp;&amp;amp; ny &amp;lt;= 100 &amp;amp;&amp;amp; map[nx][ny] == 1 &amp;amp;&amp;amp; !visited[nx][ny]) {
                    visited[nx][ny] = true;
                    queue.offer(new int[] {nx, ny, location[2] + 1});
                }
            }
        }
        
        return -1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩테스트/프로그래머스</category>
      <category>BFS</category>
      <category>아이템 줍기</category>
      <category>자바</category>
      <category>프로그래머스</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/129</guid>
      <comments>https://hyomee2.tistory.com/129#entry129comment</comments>
      <pubDate>Mon, 16 Mar 2026 21:53:27 +0900</pubDate>
    </item>
    <item>
      <title>[백준] #1783 병든 나이트, 그리디</title>
      <link>https://hyomee2.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1783&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1783&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나이트가 이동할 수 있는 규칙은 아래 4가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 2칸 위, 1칸 오른쪽&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 1칸 위, 2칸 오른쪽&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 1칸 아래, 2칸 오른쪽&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 2칸 아래, 1칸 오른쪽&lt;/p&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;처음에는 응? 그냥 이동하면 모든 칸을 방문할 수 있는거 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각했는데, 움직일 수 있는 규칙을 잘 보면 모두 &lt;b&gt;&quot;오른쪽&quot;&lt;/b&gt;으로 움직일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 조금 파악해보고자 아래와 같이 그림을 그려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGAxE/dJMcaiCvHFC/1yc0DvQmcbI5VLuvYEp3Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGAxE/dJMcaiCvHFC/1yc0DvQmcbI5VLuvYEp3Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGAxE/dJMcaiCvHFC/1yc0DvQmcbI5VLuvYEp3Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGAxE%2FdJMcaiCvHFC%2F1yc0DvQmcbI5VLuvYEp3Kk%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;413&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 세로(N)가 1일 때&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 위아래로 움직일 수 없으니 1칸만 방문할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 세로(N)가 2일 때&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1칸 위아래로만 이동할 수 있으므로 규칙 2, 3번만 이용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이때 2, 3번은 가로로 2칸씩 가야하므로, 방문 칸수는 (M + 1) / 2인데 최대 4칸까지 갈 수 있다. (4가지 규칙 다 못쓰므로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 세로(N)가 3 이상일 때&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 가로(M)가 7미만이라면 4가지 규칙을 다 쓸 수 없으므로 최대 4칸까지 갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 가로(M)가 7 이상이면 모든 규칙을 한 번씩 다 쓸 수 있는데, 4가지 규칙을 다 쓰는데 5칸을 방문해야하고, 남은 가로들로는 가장 효율적으로 오른쪽으로 1칸 이동 규칙(1, 4번)을 이용하면 된다. 따라서 식으로 표현하면 5 + (M - 7) = M - 2 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식을 정리하면 아래와 같이 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSN6TQ/dJMcafeMfoq/iAbWopvObksXxhICLe1bf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSN6TQ/dJMcafeMfoq/iAbWopvObksXxhICLe1bf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSN6TQ/dJMcafeMfoq/iAbWopvObksXxhICLe1bf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSN6TQ%2FdJMcafeMfoq%2FiAbWopvObksXxhICLe1bf0%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;332&quot; height=&quot;101&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 코드로 아래와 같이 표현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773292555847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());

        if (N == 1) {
            System.out.println(1);
        } else if (N == 2) {
            System.out.println(Math.min((M + 1) / 2, 4));
        } else {
            if (M &amp;lt; 7) {
                System.out.println(Math.min(M, 4));
            } else {
                System.out.println(M - 2);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>코딩테스트/백준</category>
      <category>백준</category>
      <category>백준1783</category>
      <category>병든 나이트</category>
      <category>자바</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/128</guid>
      <comments>https://hyomee2.tistory.com/128#entry128comment</comments>
      <pubDate>Thu, 12 Mar 2026 14:16:37 +0900</pubDate>
    </item>
    <item>
      <title>[백준] #11497 통나무 건너뛰기, 그리디</title>
      <link>https://hyomee2.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11497&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11497&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;문제 요약&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;통나무들을 원형으로 배치했을 때, 인접한 통나무의 높이 차이의 최댓값을 최소화해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉,&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;max(|a[i] - a[i+1]|)&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;font-family: 'Nanum Gothic';&quot;&gt;의 최솟값을 구해야 한다. (원형이므로 첫번째와 마지막도 인접)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;아이디어&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리는 차이가 작아지게 해야하는데, 차이가 커지는 경우는 큰값과 작은값이 함께 붙어있을 때이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서 우리는 큰 값과 작은 값이 붙지 않게 배치해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이럴 경우, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;a0 a1 a2 a3 ... aN-1 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이렇게 오름차순이라고 하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래와 같이 배치할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;a0 a2 a4 ... a5 a3 a1&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 우선 문제에서는 정렬되지 않은 통나무의 높이가 주어지므로 정렬을 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. Deque을 이용해 '&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #666666; text-align: left;&quot;&gt;a0 a2 a4 ... a5 a3 a1' &lt;span style=&quot;color: #000000;&quot;&gt;이런 꼴로 배치해준다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; text-align: left;&quot;&gt;3. 배열을 순회하며 최댓값을 담아준다. (원형이므로 처음과 마지막 비교를 잊지 않는다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773079780801&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    static StringBuilder sb = new StringBuilder();
    static StringTokenizer st;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine());

        for (int i = 0; i &amp;lt; T; i++) {
            int N = Integer.parseInt(br.readLine());
            int[] temp = new int[N];;
            st = new StringTokenizer(br.readLine());

            for (int j = 0; j &amp;lt; N; j++) {
                temp[j] = Integer.parseInt(st.nextToken());
            }

            Arrays.sort(temp);  // 시간복잡도: O(N log N)

            Deque&amp;lt;Integer&amp;gt; deque = new LinkedList&amp;lt;&amp;gt;();

            for (int j = 0; j &amp;lt; N; j++) {  // 시간복잡도: O(N) (deque 연산 O(1)이 N번)
                if (j % 2 == 0) {
                    deque.addFirst(temp[j]);
                } else {
                    deque.addLast(temp[j]);
                }
            }

            Integer[] result = deque.toArray(Integer[]::new);  // 시간복잡도: O(N) (모든 원소 복사)

            int level = 0;

            for (int j = 0; j &amp;lt; N - 1; j++) {  // 시간복잡도: O(N)
                level = Math.max(level, Math.abs(result[j + 1] - result[j]));
            }

            // 처음, 마지막 것 검사(원형)
            level = Math.max(level, Math.abs(result[N - 1] - result[0]));
            sb.append(level).append(&quot;\n&quot;);
        }

        System.out.println(sb);
    }
}

// 총 시간 복잡도: O(N log N)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 '&lt;span style=&quot;font-family: 'Nanum Gothic'; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;a0 a2 a4 ... a5 a3 a1&lt;/span&gt;' 이 꼴을 잘보면 결국 이웃한 것끼리는 인덱스 2칸 차이나는 것을 볼 수 있고,&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000; text-align: left;&quot;&gt;따라서 아래와 같이 코드를 간단하게 작성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773080758457&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;            Arrays.sort(a);
            
            for(int k = 0; k &amp;lt; N-2; k++) {
                max = Math.max(max, a[k+2]-a[k]);
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, a4와 a5는 1 차이인데 저 코드로 이 두값을 비교하지 못하는거 아닌가? 하는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞다. 하지만 이 둘이 비교할 필요가 없다!&lt;br /&gt;a는 정렬된 오름차순이니, 항상 a5과 a3의 차이가 a5와 a4의 차이보다 크기 때문이다.&lt;/p&gt;</description>
      <category>코딩테스트/백준</category>
      <category>그리디</category>
      <category>백준</category>
      <category>백준 11497</category>
      <category>통나무 건너뛰기</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/127</guid>
      <comments>https://hyomee2.tistory.com/127#entry127comment</comments>
      <pubDate>Tue, 10 Mar 2026 03:53:24 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] Redis 캐시 조회 시 Invalid UTF-32 character 발생 문제 해결</title>
      <link>https://hyomee2.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;본 게시물은 트래블록스 프로젝트를 진행하며 발생한 트러블슈팅을 정리한 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhKzyV/dJMcafyFKDT/zWY9hx1D2kGwkQ4k5pryX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhKzyV/dJMcafyFKDT/zWY9hx1D2kGwkQ4k5pryX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhKzyV/dJMcafyFKDT/zWY9hx1D2kGwkQ4k5pryX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhKzyV%2FdJMcafyFKDT%2FzWY9hx1D2kGwkQ4k5pryX1%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;385&quot; height=&quot;131&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이번 프로젝트를 진행하면서 Redis를 처음 직접 사용해보게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis를 사용하게 되는 구간은 템플릿을 개인별로 추천하게 되면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그 추천 내역을 redis에 15분 간 저장하여 15분 동안은 같은 추천 내용을 불러오는 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;어느정도 코드를 다 구현하고 swagger로 테스트를 하는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음(캐싱않고 DB를 통해 추천 결과 조회)에는 조회가 되는데 그 다음(캐싱O)에는 아래와 같은 에러가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZX8gz/dJMb99LXAjt/fdKHKKe4a7zclgQitbKB50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZX8gz/dJMb99LXAjt/fdKHKKe4a7zclgQitbKB50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZX8gz/dJMb99LXAjt/fdKHKKe4a7zclgQitbKB50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZX8gz%2FdJMb99LXAjt%2FfdKHKKe4a7zclgQitbKB50%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;754&quot; height=&quot;425&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;1250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예외를 요약하면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769623586140&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.data.redis.serializer.SerializationException:
Could not read JSON: Invalid UTF-32 character ... (above 0x0010ffff)&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;font-family: 'Nanum Gothic';&quot;&gt;찾아보니,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisTemplate.opsForVallue().get() 호출 시&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;GenericJackson2JsonRedisSerializer가 Redis value를 JSON으로 역직렬화하는 과정에서 실패해서 발생하는 오류라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉, &lt;b&gt;Redis에 JSON으로 저장된 값을 읽어오다 JSON 파싱이 깨졌다&lt;/b&gt;는 의미였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;문제 원인 찾아보기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;접근 1. Redis에 value가 어떻게 저장되었는지 확인해보기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나는 아래와 같이 value를 JSON 문자열로 저장하도록 RedisConfig에 설정을 해주었어서 '뭐지?' 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769623841009&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        StringRedisSerializer string = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer json = new GenericJackson2JsonRedisSerializer(mapper);

        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);

        template.setKeySerializer(string);  // Redis key를 사람이 읽는 문자열로 저장
        template.setValueSerializer(json);  // Redis value를 JSON 문자열로 저장

        template.setHashKeySerializer(string);
        template.setHashValueSerializer(json);

        template.setDefaultSerializer(json);&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;font-family: 'Nanum Gothic';&quot;&gt;그래도 우선 어떻게 저장되었는지 확인하기 위해 터미널에서 아래 명령어를 통해 Redis 컨테이너의 key에 저장된 value를 출력해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769624100079&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; docker exec -i travlocks-redis-local redis-cli --raw GET &quot;recommendation:templates:v1:member:3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그랬더니 내가 예상했던 JSON 형태로 나와서 value가 JSON이 아닌 Binary(JDK 직렬화)로 저장돼서 발생한 문제는 아니었다.&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;접근 2. Redis MONITOR로 실제 어떤 명령이 나가는지 확인해보기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;애플리케이션이 Redis에 어떤 명령을 치는지 확인해보기 위해 아래 명령어를 통해 Redis 컨테이너에서 MONITOR를 실행했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769624890420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it travlocks-redis-local redis-cli MONITOR&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;font-family: 'Nanum Gothic';&quot;&gt;그리고 API를 호출해주었더니 아래와 같이 로그가 찍혔다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769625031387&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1769621375.194768 [0 127.0.0.1:37976] &quot;DEL&quot; &quot;recommendation:templates:v1:member:3&quot;
1769621380.193901 [0 172.20.0.1:63344] &quot;GET&quot; &quot;recommendation:templates:v1:member:3&quot;
1769621380.280639 [0 172.20.0.1:63344] &quot;SETRANGE&quot; &quot;recommendation:templates:v1:member:3&quot; &quot;15&quot; &quot;{\&quot;recommendationId\&quot;: ... ,\&quot;totalScore\&quot;:36.928031367991565}]}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 로그를 보면&lt;b&gt; SET이 아닌 SETRANGE&lt;/b&gt;가 나간 것을 확인할 수 있었다. 어느정도 가닥이 잡혔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;SETRANGE key offsest value&lt;/b&gt;는 Redis String의 특정 위치(offset)부터 덮어쓰는 커맨드다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 로그를 해석해보면&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 value의 15번째 위치부터 우리의 데이터를 넣고, 그 이전(0~14번째 바이트)은 자동으로 채우라는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;자동으로 채우면 데이터 앞에는 널 문자(\x00)가 15개 붙고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;우리가 Redis에서 볼 때는 JSON으로 데이터가 잘 저장된 것처럼 보이지만 실제로 맨 앞에는 보이지 않는 바이너리가 있었던 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Jackson은 value를 맨 앞 바이트부터 읽으며 첫 글자가 { 여야 JSON이라고 판단&lt;/b&gt;하는데,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;실제 첫 바이트가 \x00이니 UTF-32로 오판&lt;/b&gt;했던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;그래서 set()에서 잘못된 것을 찾기 시작했다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;캐시 저장 코드는 아래와 같았다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769625915908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Value(&quot;${cache.recommendation.templates.cache-ttl-minutes}&quot;)
private long ttlMinutes;
    
public void set(Long memberId, CachedTemplateRecommendations value) {
    redis.opsForValue().set(key(memberId), value, ttlMinutes);
}&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;font-family: 'Nanum Gothic';&quot;&gt;여기서 set 함수에 hover를 해보았더니&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ValueOperations의 set의 마지막 매개변수의 타입이 Duration이 돼야하는 것을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5uj4o/dJMcahpINO3/FYYeNc1NZ5bXALaqA9iQfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5uj4o/dJMcahpINO3/FYYeNc1NZ5bXALaqA9iQfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5uj4o/dJMcahpINO3/FYYeNc1NZ5bXALaqA9iQfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5uj4o%2FdJMcahpINO3%2FFYYeNc1NZ5bXALaqA9iQfK%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;471&quot; height=&quot;224&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 long인 ttlMinutes를 매개변수로 넣어주었는데, 아래와 같이 Duration 타입으로 넣어주었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769626478320&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void set(Long memberId, CachedTemplateRecommendations value) {
	redis.opsForValue().set(key(memberId), value, Duration.ofMinutes(ttlMinutes));
}&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;font-family: 'Nanum Gothic';&quot;&gt;그리고 다시 API 요청을 날린 뒤 MONITOR을 확인해보니 의도한대로 SET이 발생하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;캐싱된 데이터가 정상적으로 조회되는 것을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2kJC1/dJMcabJPe7G/QK84D6f5ufKohFNqKr3wM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2kJC1/dJMcabJPe7G/QK84D6f5ufKohFNqKr3wM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2kJC1/dJMcabJPe7G/QK84D6f5ufKohFNqKr3wM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2kJC1%2FdJMcabJPe7G%2FQK84D6f5ufKohFNqKr3wM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;57&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HETGf/dJMcaivlza8/vUoC65AsJdUxcfVpsNQFr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HETGf/dJMcaivlza8/vUoC65AsJdUxcfVpsNQFr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HETGf/dJMcaivlza8/vUoC65AsJdUxcfVpsNQFr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHETGf%2FdJMcaivlza8%2FvUoC65AsJdUxcfVpsNQFr0%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;386&quot; height=&quot;255&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;왜 이런 일이 발생했던 것일까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제는 해결됐는데... 왜 SETRANGE가 MONITOR에 찍혔는지 궁금했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서 JavaDoc를 살펴보다보니 아래와 같은 set 함수를 찾아볼 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시그니처가 (K, V, long)으로 내가 썻던 매개변수 형태와 완전 일치했고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;해당 함수에 오버로딩되어 SETRANGE가 찍혔구나라고 추측할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉 내가 매개변수로 넣어준 Long ttlMinutes인 15가 offset으로서 들어간거였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpK8YQ/dJMcabQysD4/Q52ayph50ef1lQcwKJB5kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpK8YQ/dJMcabQysD4/Q52ayph50ef1lQcwKJB5kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpK8YQ/dJMcabQysD4/Q52ayph50ef1lQcwKJB5kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpK8YQ%2FdJMcabQysD4%2FQ52ayph50ef1lQcwKJB5kk%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;593&quot; height=&quot;247&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>projects/travlocks</category>
      <category>Invalid UTF-32 character</category>
      <category>redis</category>
      <category>Set</category>
      <category>SETRANGE</category>
      <category>트러블슈팅</category>
      <author>hyomee2</author>
      <guid isPermaLink="true">https://hyomee2.tistory.com/125</guid>
      <comments>https://hyomee2.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 29 Jan 2026 04:11:56 +0900</pubDate>
    </item>
  </channel>
</rss>