<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>동구름</title>
    <link>https://dcloud.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 07:23:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>동구름이</managingEditor>
    <item>
      <title>자주 바뀌는 데이터, 정적 테이블에 넣어도 괜찮을까</title>
      <link>https://dcloud.tistory.com/403</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 사용자의 위치 정보를 지속적으로 추적하고 관리해야 하는 요구사항이 주어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 고려해야할 조건은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;1. 위치 정보는 매우 자주 바뀐다. 서비스 특성상 5~30초 주기로 위치 갱신 발생&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;2. users 테이블은 비교적 정적이다 닉네임, 프로필 사진, 이메일 등은 변경이 거의 없음 &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;3. 서버에서 위치 정보를 관리해야하는 서비스다.&lt;/i&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;ERD를 설계하는데 문득 &quot;사용자의 위도/경도 위치 정보를 정적인 users 테이블에 넣는 게 맞을까?&quot; 생각이 들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2025-06-05 오후 7.58.26.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caRgAm/btsOsbZDSBz/eKWG9RMg10MTJY2rPBNOOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caRgAm/btsOsbZDSBz/eKWG9RMg10MTJY2rPBNOOK/img.png&quot; data-alt=&quot;한 테이블에 다 넣은 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caRgAm/btsOsbZDSBz/eKWG9RMg10MTJY2rPBNOOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaRgAm%2FbtsOsbZDSBz%2FeKWG9RMg10MTJY2rPBNOOK%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;383&quot; height=&quot;274&quot; data-filename=&quot;edited_스크린샷 2025-06-05 오후 7.58.26.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;한 테이블에 다 넣은 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 사용자 정보인데 한 테이블에 있는 게 편하지 않나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 조금만 고민해보면, 이 결정은 단순한 편의성 이상의 영향을 끼친다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고려 사항 : 구조적 병목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놓치기 쉬운 RDB의 저장 방식이 있다. 바로 &quot;&lt;i&gt;&lt;b&gt;디스크는 row가 아니라 블록(Page) 단위로 읽고 쓴다&lt;/b&gt;&lt;/i&gt;&quot;는 점이다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RDB의 물리 구조 이해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;대부분의 RDBMS(PostgreSQL, MySQL 등)는 데이터를 8KB 단위의 '페이지(Page)' 블록으로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 페이지는 아래처럼 여러 row를 묶어서 디스크에 배치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749125811499&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ Page 101 ]
┌──────────────────────────────────────┐
│ User 1 (nickname, email, lat/lng...) │
│ User 2 (...)                         │
│ ...                                  │
└──────────────────────────────────────┘&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;DB는&amp;nbsp;특정&amp;nbsp;row&amp;nbsp;하나만&amp;nbsp;읽거나&amp;nbsp;수정하더라도,&amp;nbsp;해당&amp;nbsp;&lt;b&gt;row가&amp;nbsp;속한&amp;nbsp;페이지&amp;nbsp;전체&lt;/b&gt;를&amp;nbsp;로딩하고&amp;nbsp;수정한&amp;nbsp;뒤&amp;nbsp;다시&amp;nbsp;저장해야&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데&amp;nbsp;위치&amp;nbsp;정보는&amp;nbsp;자주&amp;nbsp;바뀐다&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 위치가 10초마다 업데이트된다고 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1749125562571&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE users SET latitude = ?, longitude = ? WHERE id = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 단순히 위치 필드만 바꾸는 것처럼 보이지만, 실제로는 다음과 비효율이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;디스크 I/O 병목&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치가 들어있는 row는 페이지 101에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; UPDATE가 발생하면 페이지 &lt;b&gt;101 전체&lt;/b&gt;가 디스크에서 읽혀지고, 다시 덮어써진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; nickname, email 같은 안 바뀌는 정보까지 포함된 페이지가 자주 교체된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&amp;nbsp;단&amp;nbsp;두&amp;nbsp;필드를&amp;nbsp;수정했을&amp;nbsp;뿐인데도,&amp;nbsp;&lt;b&gt;해당&amp;nbsp;페이지&amp;nbsp;전체&lt;/b&gt;가&amp;nbsp;계속&amp;nbsp;읽히고&amp;nbsp;다시&amp;nbsp;저장되는&amp;nbsp;반복&amp;nbsp;작업이&amp;nbsp;발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 페이지에는 &lt;b&gt;nickname, email처럼 바뀌지 않는 정보도 포함&lt;/b&gt;되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;b&gt;위치 정보 하나 바뀔 때마다, 정적 데이터가 들어있는 블록까지 강제로 I/O 연산 대상&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캐시 오염&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 자주 쓰는 페이지를 메모리에 캐시(Buffer Pool)해둔다.&lt;/p&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;1. 위치 정보가 갱신되면 해당 페이지는 &lt;b&gt;dirty&lt;/b&gt; 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 일정 시점이 되면 해당 페이지는 디스크에 &lt;b&gt;flush &lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 그 과정에서 페이지는 캐시에서 &lt;b&gt;evict &lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 다시 같은 사용자 정보를 조회하면 &amp;rarr; 디스크에서 페이지를 재로드해야 한다.&lt;/p&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;rarr; 즉, 자주 바뀌는 필드 하나 때문에 같은 페이지에 있는 정적 정보도 자주 캐시에서 밀려나고 다시 불려온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 nickname, email 같은 자주 쓰는 정적 필드의 &lt;b&gt;캐시 히트율&lt;/b&gt;이 급격히 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;락 충돌 증가 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 두 트랜잭션이 동시에 발생한다고 해보자&lt;/p&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;i&gt;A 트랜잭션: 닉네임 변경 &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;B 트랜잭션: 위치 정보 업데이트 (10초마다) &lt;/i&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;하지만 이 둘은 같은 row 혹은 같은 페이지에 있다. 그래서&amp;nbsp;DB는 락을 걸어야 하므로 &lt;b&gt;충돌이 발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결책: 테이블 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 정보만 따로 분리한다면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2025-06-05 오후 8.01.05.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDuoVh/btsOsePC7Vx/AwtRn62eZvUwXCOSlPDl8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDuoVh/btsOsePC7Vx/AwtRn62eZvUwXCOSlPDl8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDuoVh/btsOsePC7Vx/AwtRn62eZvUwXCOSlPDl8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDuoVh%2FbtsOsePC7Vx%2FAwtRn62eZvUwXCOSlPDl8K%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;378&quot; height=&quot;401&quot; data-filename=&quot;edited_스크린샷 2025-06-05 오후 8.01.05.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;740&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;size18&quot;&gt;&lt;b&gt;1.&amp;nbsp;users&amp;nbsp;테이블은&amp;nbsp;거의&amp;nbsp;읽기&amp;nbsp;전용&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;닉네임, 이메일 등은 자주 변경되지 않으니 캐시에 안정적으로 유지 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.&amp;nbsp;user_locations&amp;nbsp;테이블은&amp;nbsp;쓰기&amp;nbsp;전용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 정보만 빠르게 갱신한다. 디스크/메모리도 여기에 집중될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 두 테이블은 서로 다른 페이지 공간에 저장된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;users와 user_locations는 서로 다른 테이블이므로 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;디스크 상에서도 완전히 다른 블록(Page) 단위 공간&lt;/b&gt;을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 I/O, 캐시, 락이 모두 분리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/MySQL</category>
      <category>db</category>
      <category>디스크</category>
      <category>블록</category>
      <category>페이지</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/403</guid>
      <comments>https://dcloud.tistory.com/403#entry403comment</comments>
      <pubDate>Thu, 5 Jun 2025 21:58:05 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 / Java] Lv3. 표편집</title>
      <link>https://dcloud.tistory.com/393</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/81303&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/81303&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737290611142&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/81303&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3XLHc/hyX0w8sXIT/HyzWkHyLLBkrqLFPyf4Xk1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/WzU22/hyX0yFhu0i/THt7iKV1SgzPIwP6BVJnsk/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/81303&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/81303&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3XLHc/hyX0w8sXIT/HyzWkHyLLBkrqLFPyf4Xk1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/WzU22/hyX0yFhu0i/THt7iKV1SgzPIwP6BVJnsk/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;h3 data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 변수를 잘 관리하는 것이 포인트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 현재 컬럼의 위치를 나타내는 정수형 `cur`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 현재 표의 전체 크기를 나타내는 정수형 `size`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 삭제한 컬럼의 위치를 보관하는 스택 자료구조인 `deque` (자바에서는 stack이 레거시 자료구조라 deque로 구현함)&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. moveColumn()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향에 따라 현재 컬럼 위치(`cur`)를 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. deleteColumn()&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;표에서 행이 하나 없어지는 것이기 때문에, size도 하나 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제한 행의 위치는 복구를 위해 stack에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. recoverColumn() &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;stack은 LIFO이기 때문에 pop을 통해 마지막 값을 복구할 수 있다. 행이 복구되었기 때문에 size를 다시 늘린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 포인터의 위치와 비교해서 cur 값보다 아래의 값이 들어오면 cur를 ++한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 `size`, `stack` 자료구조를 이용해 결과 문자열을 만들 수 있다.&lt;/p&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. size 만큼 &quot;O&quot;으로 채운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. stack에서 pop을 하며 해당 인덱스를 &quot;X&quot;로 채운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 생각해, `삭제한 행들의 인덱스`만 기억하면 결과 문자열을 만들어낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1737290520772&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 ArrayDeque&amp;lt;Integer&amp;gt; deque = new ArrayDeque&amp;lt;&amp;gt;();
    static int cur;
    static int size;
    
    public String solution(int n, int k, String[] cmd) {
        String answer = &quot;&quot;;
        cur = k;
        size = n;
        
        for(String command:cmd){
            String[] str = command.split(&quot; &quot;);
            String control = str[0];
            switch(control){
                case &quot;U&quot;: 
                    moveColumn(str);
                    break;
                case &quot;D&quot;: 
                    moveColumn(str);
                    break;
                case &quot;C&quot;:
                    deleteColumn();
                    break;
                case &quot;Z&quot;:
                    recoverColumn();
                    break;
            }
        }
        
        answer = buildResult();
        return answer;
    }
    
    static void moveColumn(String[] str){
        String control = str[0];
        int X = Integer.valueOf(str[1]);
        if (control.equals(&quot;D&quot;)) {
            cur += X;
        }
        if (control.equals(&quot;U&quot;)) {
            cur -= X;
        }
    }
    
    static void deleteColumn(){
        deque.push(cur);
        size--;
        if(cur==size) cur--;
    }
    
    static void recoverColumn(){
        int value = deque.pop();
        if(value&amp;lt;=cur) cur++;
        size++;
    }
    
    static String buildResult(){
        StringBuilder sb = new StringBuilder();
        
        for(int i=0; i&amp;lt;size; i++) sb.append(&quot;O&quot;);
        while(!deque.isEmpty()){
            sb.insert(deque.pop(), &quot;X&quot;);
        }
        
        return sb.toString();
    }
    
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java/프로그래머스</category>
      <category>Java</category>
      <category>스택</category>
      <category>표편집</category>
      <category>프로그래머스</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/393</guid>
      <comments>https://dcloud.tistory.com/393#entry393comment</comments>
      <pubDate>Sun, 19 Jan 2025 21:51:58 +0900</pubDate>
    </item>
    <item>
      <title>libuv의 이벤트 루프를 파헤쳐보자 - libuv와 이벤트 루프 실행 흐름</title>
      <link>https://dcloud.tistory.com/337</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Node.js의 이벤트 루프는 libuv 라이브러리에 의해 관리된다.&lt;/p&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;libuv 를 학습하면서, 솔직히 정말 이해가 되지 않았다. 구체적으로 이벤트 루프가 무슨 동작을 하는 건지, microtask queue는 무엇인지, 이벤트 루프가 어떻게 논블로킹 I/O를 처리할 수 있는지 등 추상적으로만 이해할 뿐 어느 누구에게 제대로 설명하기가 쉽지 않았다.&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;그래서 node.js 공식 문서부터 여러 포스팅, git에 올라와있는 node.js, libuv 라이브러리 코드를 며칠간 파헤쳐보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;libuv란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-01 오후 9.27.25.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xR8lx/btsLBiOQU48/yH8yLTrvfIrHMrgu9MEYGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xR8lx/btsLBiOQU48/yH8yLTrvfIrHMrgu9MEYGk/img.png&quot; data-alt=&quot;https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xR8lx/btsLBiOQU48/yH8yLTrvfIrHMrgu9MEYGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxR8lx%2FbtsLBiOQU48%2FyH8yLTrvfIrHMrgu9MEYGk%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;379&quot; height=&quot;320&quot; data-filename=&quot;스크린샷 2025-01-01 오후 9.27.25.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Node.js의 이벤트 루프는 libuv라는 비동기 I/O 라이브러리에 의해 구현된다.&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;libuv는 C++로 작성된 라이브러리로, Node.js가 단일 스레드로 동작하면서도 비동기적으로 I/O 작업을 처리할 수 있게 하는 핵심 엔진이다.&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;libuv와 커널의 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;libuv는 단순한 라이브러리가 아니라,&amp;nbsp;&lt;b&gt;운영체제의&amp;nbsp;커널을&amp;nbsp;추상화한&amp;nbsp;Wrapping&amp;nbsp;라이브러리&lt;/b&gt;라는&amp;nbsp;점에서&amp;nbsp;특별하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-02 오후 9.13.16.png&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/01feq/btsLD1rIOgI/ILW1W3E8k7CdCLzbtUQCM0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/01feq/btsLD1rIOgI/ILW1W3E8k7CdCLzbtUQCM0/tfile.dat&quot; data-alt=&quot;https://docs.libuv.org/en/latest/design.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/01feq/btsLD1rIOgI/ILW1W3E8k7CdCLzbtUQCM0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F01feq%2FbtsLD1rIOgI%2FILW1W3E8k7CdCLzbtUQCM0%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;163&quot; data-filename=&quot;스크린샷 2025-01-02 오후 9.13.16.png&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.libuv.org/en/latest/design.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;운영체제는 비동기 처리를 위한 여러 API를 제공한다. 예를 들어, 리눅스의 epoll, macOS의 kqueue, 윈도우의 IOCP 같은 것들이다.&lt;/p&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;libuv는 이러한 &lt;b&gt;운영체제별 비동기 API를 추상화&lt;/b&gt;해, Node.js가 운영체제의 종류에 상관없이 동일한 방식으로 비동기 작업을 요청할 수 있도록 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;쉽게 말해, &lt;b&gt;libuv는 운영체제의 비동기 API가 무엇인지 이미 알고 있고&lt;/b&gt;, 이를 활용해 파일 읽기, 네트워크 작업 같은 논블로킹 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 libuv는 &lt;b&gt;운영체제의 비동기 API를 활용해 논블로킹 작업을 처리&lt;/b&gt;하거나, &lt;b&gt;커널이 이를 지원하지 않는 경우 자체적으로 스레드 풀을 활용&lt;/b&gt;해 작업을 처리한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, 아래와 같은 두 가지 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 커널이 비동기 작업을 지원하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.50.21.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tv6Vp/btsLGjmKcZn/HBPuz8mCdPv9LoLVxzSkNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tv6Vp/btsLGjmKcZn/HBPuz8mCdPv9LoLVxzSkNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tv6Vp/btsLGjmKcZn/HBPuz8mCdPv9LoLVxzSkNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTv6Vp%2FbtsLGjmKcZn%2FHBPuz8mCdPv9LoLVxzSkNk%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;490&quot; height=&quot;454&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.50.21.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #; text-align: start;&quot;&gt;&amp;nbsp;&lt;span&gt;만약&amp;nbsp;&lt;/span&gt;&lt;/span&gt;커널에서 해당 비동기 작업을 &lt;b&gt;지원할 경우&lt;/b&gt;,&amp;nbsp;libuv는&amp;nbsp;이를&amp;nbsp;커널에&amp;nbsp;위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 경우 libuv의 &lt;b&gt;uv_io_t&lt;/b&gt;가 사용된다. uv_io_t는 libuv의 내부 데이터 구조로, 커널의 비동기 작업을 추적하고, 작업 완료 시 이벤트 루프에서 이를 감지하고 처리하도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 커널이 비동기 작업을 지원하지 않는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.52.25.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBQbBw/btsLHSojN5m/ugOyiexUK9UmvIKM3UgKY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBQbBw/btsLHSojN5m/ugOyiexUK9UmvIKM3UgKY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBQbBw/btsLHSojN5m/ugOyiexUK9UmvIKM3UgKY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBQbBw%2FbtsLHSojN5m%2FugOyiexUK9UmvIKM3UgKY1%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;431&quot; height=&quot;314&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.52.25.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 커널에서 해당 비동기 작업을 &lt;b&gt;지원하지 않을 경우&lt;/b&gt;, libuv의 &lt;b&gt;스레드 풀&lt;/b&gt;이 직접 작업을 처리한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;조금 더 구체적으로는 아래의 두 가지 주요 상황에서 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;운영체제가&amp;nbsp;비동기&amp;nbsp;API를&amp;nbsp;지원하지&amp;nbsp;않는&amp;nbsp;작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를&amp;nbsp;들어,&amp;nbsp;유닉스&amp;nbsp;계열&amp;nbsp;운영체제에서는&amp;nbsp;대부분의&amp;nbsp;파일&amp;nbsp;시스템&amp;nbsp;작업(예:&amp;nbsp;read(),&amp;nbsp;write())이&amp;nbsp;&lt;b&gt;블로킹&amp;nbsp;호출&lt;/b&gt;로&amp;nbsp;제공된다.&amp;nbsp;이&amp;nbsp;경우&amp;nbsp;libuv는&amp;nbsp;스레드&amp;nbsp;풀을&amp;nbsp;활용해&amp;nbsp;이러한&amp;nbsp;블로킹&amp;nbsp;작업을&amp;nbsp;처리하고,&amp;nbsp;완료된&amp;nbsp;작업을&amp;nbsp;이벤트&amp;nbsp;루프에&amp;nbsp;전달한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스레드 풀이 &lt;b&gt;내부적으로 운영체제의 블로킹 API를 호출하지만, 이를 논블로킹처럼 동작&lt;/b&gt;하도록 처리하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;CPU&amp;nbsp;집약적인&amp;nbsp;작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해시&amp;nbsp;함수,&amp;nbsp;압축,&amp;nbsp;암호화&amp;nbsp;같은&amp;nbsp;CPU&amp;nbsp;연산&amp;nbsp;작업은&amp;nbsp;운영체제의&amp;nbsp;비동기&amp;nbsp;API로&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;이때도&amp;nbsp;libuv는&amp;nbsp;스레드&amp;nbsp;풀을&amp;nbsp;사용해&amp;nbsp;이러한&amp;nbsp;작업을&amp;nbsp;처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;libuv의 스레드 풀 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-03 오후 4.19.10.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wxyR4/btsLDlRLR90/t4AE4B1QrJIBAxjn8JLxX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wxyR4/btsLDlRLR90/t4AE4B1QrJIBAxjn8JLxX1/img.png&quot; data-alt=&quot;https://docs.libuv.org/en/latest/threadpool.html#threadpool&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wxyR4/btsLDlRLR90/t4AE4B1QrJIBAxjn8JLxX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwxyR4%2FbtsLDlRLR90%2Ft4AE4B1QrJIBAxjn8JLxX1%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;592&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2025-01-03 오후 4.19.10.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.libuv.org/en/latest/threadpool.html#threadpool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;공식 문서에 따르면, libuv는 기본적으로 &lt;b&gt;4개의 스레드&lt;/b&gt;를 가지는 스레드 풀을 생성한다. 그리고 UV_THREADPOOL_SIZE라는 환경 변수를 통해 최대 1024개의 스레드 생성이 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;이벤트 루프란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이제 libuv의 핵심 구성 요소인 이벤트 루프에 대해 알아보자.&lt;/p&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;이벤트 루프는 각 요청을 특성에 맞게 커널이나 Thread Pool에 위임하고, 실행 대기 중인 callback을 Event Queue에 모았다가 Main Thread에 의해 실행될 수 있도록 call stack으로 옮기는 역할을 한다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이벤트 루프를 공부하면서, 인터넷에 떠돌아다니는 이벤트 루프을 묘사한 그림에 많은 혼란을 겪었다. 실제로 대부분의 이미지는 이벤트 루프의 실제 동작을 표현하기엔 무리가 있다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=PNa9OMajw9w&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/mt2zO/hyWV3j58ss/kmZKjCCrXC942B9pgnbCyK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/PNa9OMajw9w&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;https://www.youtube.com/watch?v=PNa9OMajw9w&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Node.js의 핵심 개발자 중 한명인 Bert Belder의 강연이다. 위 영상의 1분 15초 부터 인터넷 상에 돌아다니는 많은 이벤트 루프 참고 그림은 잘못된 그림이라는 것을 지적한다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 루프의 페이즈&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.31.22.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GpFoa/btsLHv01FtQ/OQK4BCXz5swWJpXd2N5pSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GpFoa/btsLHv01FtQ/OQK4BCXz5swWJpXd2N5pSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GpFoa/btsLHv01FtQ/OQK4BCXz5swWJpXd2N5pSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGpFoa%2FbtsLHv01FtQ%2FOQK4BCXz5swWJpXd2N5pSk%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;509&quot; height=&quot;383&quot; data-filename=&quot;스크린샷 2025-01-08 오후 5.31.22.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서와 코드를 참고해 그린 이벤트 루프는 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;페이즈&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는 &lt;b&gt;6개의 페이즈&lt;/b&gt;로 구성되어있다. 위 그림에서 박스가 특정 작업을 수행하기 위한 페이즈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;각 페이즈는 &lt;b&gt;자신들이 관심있어 하는 작업들만 관리&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어 Timer Phase는 이름 그대로 타이머에 관한 비동기 작업들을 관리하고 Pending Callbacks는 이전 단계에서 완료되지 않은 I/O작업 콜백을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;페이즈 전환 순서&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이즈 전환 순서는 그림에 그린 것처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Timer Phase &amp;rarr; Pending Callbacks Phase &amp;rarr; Idle, Prepare Phase &amp;rarr; Poll Phase &amp;rarr; Check Phase &amp;rarr; Close Callbacks Phase&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;한 페이즈에서 다음 페이즈로 넘어가는 것&lt;/b&gt;을 &lt;b&gt;`Tick`&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;nextTickQueue, microTaskQueue&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위 그림 중, 점선으로 구성된 nextTickQueue와 microTaskQueue는 &lt;b&gt;이벤트 루프를 구성하는 요소는 아니다&lt;/b&gt;. 하지만 Node.js의 비동기 관리 작업을 도와주며, 이벤트 루프의 동작 방식에 영향을 미친다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;nextTickQueue&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서&amp;nbsp;process.nextTick으로&amp;nbsp;등록된&amp;nbsp;콜백을&amp;nbsp;저장하는&amp;nbsp;큐이다.&amp;nbsp;모든 이벤트 루프 페이즈보다 우선적으로 실행된다. 즉, 현재 페이즈에서 처리 중이던 작업이 완료된 직후 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;microTaskQueue&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Promise의 .then 또는 MutationObserver와 같은 미시 작업(microtask)을 저장하는 큐이다. nextTickQueue 다음으로 실행되며, 현재 이벤트 루프 페이즈가 끝나기 전에 실행된다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;1. 현재 이벤트 루프 페이즈에서 할 일을 처리한다.&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;2. nextTickQueue의 작업을 처리한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;3. microTaskQueue의 작업을 처리한다.&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;4. 다음 이벤트 루프 페이즈로 이동한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이즈에 대한 구체적인 설명은 다음 포스팅에서 다룰 예정이다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;페이즈의 큐와 시스템 실행 한도&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-08 오후 6.41.48.png&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tm5ez/btsLGdUrscH/FfXXeuHzDbPGwjWElV6121/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tm5ez/btsLGdUrscH/FfXXeuHzDbPGwjWElV6121/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tm5ez/btsLGdUrscH/FfXXeuHzDbPGwjWElV6121/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftm5ez%2FbtsLGdUrscH%2FFfXXeuHzDbPGwjWElV6121%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;540&quot; height=&quot;421&quot; data-filename=&quot;스크린샷 2025-01-08 오후 6.41.48.png&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이벤트 루프의 각 페이즈는 &lt;b&gt;자신만의 큐&lt;/b&gt;를 하나씩 가지고 있다. 이 큐에는 해당 페이즈에서 처리할 작업들이 순서대로 담긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어, Timer Phase에는 setTimeout과 setInterval의 콜백이, Check Phase에는 setImmediate의 콜백이 각각의 큐에 들어가고, 이벤트 루프는 이를 하나씩 꺼내어 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 여기서 중요한 점이 있다. 이벤트 루프가 &lt;b&gt;단순히 큐의 작업을 모두 처리하고 다음 페이즈로 넘어가는 것은 아니다&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;만약 새로운 작업이 계속해서 추가된다면?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;새로운 작업이 계속해서 추가된다면 어떻게 될까? 만약 이벤트 루프가 끝없이 하나의 페이즈에 머물며 큐의 작업만 처리한다면, 다른 페이즈로 넘어가지 못하고 특정 작업에 갇혀버릴 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 방지하기 위해 &lt;b&gt;시스템 실행 한도&lt;/b&gt;라는 개념이 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;시스템 실행 한도란, 각 페이즈에서 특정 조건에 따라 일정 시간이 경과하거나, 처리할 수 있는 작업량이 초과했을 때 강제로 다음 페이즈로 넘어가게 하는 제한이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;pre id=&quot;code_1736329999644&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;setTimeout(() =&amp;gt; {
  console.log('Callback 1');
  setTimeout(() =&amp;gt; {
    console.log('Callback 2');
    setTimeout(() =&amp;gt; {
      console.log('Callback 3');
      setTimeout(() =&amp;gt; {
        console.log('Callback 4');
        ...
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;스크린샷 2025-01-08 오후 6.58.39.png&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DoGAE/btsLGOgeESS/B0wZ1t2QuTrhCkGiku7nV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DoGAE/btsLGOgeESS/B0wZ1t2QuTrhCkGiku7nV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DoGAE/btsLGOgeESS/B0wZ1t2QuTrhCkGiku7nV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDoGAE%2FbtsLGOgeESS%2FB0wZ1t2QuTrhCkGiku7nV0%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;533&quot; height=&quot;307&quot; data-filename=&quot;스크린샷 2025-01-08 오후 6.58.39.png&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;1. Timer Phase에서 setTimeout 콜백 하나가 실행된다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;2. 콜백을 실행하는 도중 새로운 setTimeout 작업이 추가된다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;3. 이 새로운 작업이 다시 Timer Phase의 큐로 들어가면서 이벤트 루프가 계속해서 Timer Phase에 머무른다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이처럼 &lt;b&gt;특정 페이즈가 작업 추가로 인해 끝없이 실행되는 것을 방지&lt;/b&gt;하기 위해 시스템 실행 한도가 작동한다. 이를 통해 이벤트 루프는 적절한 시점에 다음 페이즈로 넘어가면서 다른 작업도 처리할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 시스템 실행 한도는 이벤트 루프가 &lt;b&gt;공정하게 각 페이즈를 순회하도록 만들어주는 안전 장치&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NodeJS 실행 시 이벤트 루프의 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeJS에서 어플리케이션을 실행하면 어떤 흐름으로 이벤트 루프가 실행될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.js 라는 어플리케이션을 실행한다고 가정했을 때, 전체 흐름은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-09 오후 5.25.32.png&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biig5U/btsLHTPgsfH/ct99f1PDGwNHCZRGOo0BkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biig5U/btsLHTPgsfH/ct99f1PDGwNHCZRGOo0BkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biig5U/btsLHTPgsfH/ct99f1PDGwNHCZRGOo0BkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbiig5U%2FbtsLHTPgsfH%2Fct99f1PDGwNHCZRGOo0BkK%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;733&quot; height=&quot;427&quot; data-filename=&quot;스크린샷 2025-01-09 오후 5.25.32.png&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;1. 이벤트 루프를 생성한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;2. 이벤트 루프에 진입하기 전, main.js를 처음부터 끝까지 실행한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;3. 이벤트 루프가 살아있는지 확인한다.(남아있는 작업이 있는지 확인)&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;4. 이벤트 루프가 살아있다면 이벤트 루프로 진입하고, 남은 작업이 없다면 이벤트 루프가 종료된다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NodeJS 소스 코드로 살펴보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;nodejs의 소스 코드&lt;/b&gt;를 보면서 다시 그림을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736410316542&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node main.js&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;NodeMainInstance::Run&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/nodejs/node/blob/main/src/node_main_instance.cc#L104&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/nodejs/node/blob/main/src/node_main_instance.cc#L104&lt;/a&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1736410679861&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {
  if (*exit_code == ExitCode::kNoFailure) {
    if (!sea::MaybeLoadSingleExecutableApplication(env)) {
      LoadEnvironment(env, StartExecutionCallback{});
    }

    *exit_code =
    	//이벤트 루프
        SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError);
  }

#if defined(LEAK_SANITIZER)
  __lsan_do_leak_check();
#endif
}&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;node main.js 명령을 통해 NodeJS가 구동되면, NodeMainInstance::Run 함수가 호출된다. 그리고 &lt;b&gt;SpinEventLoopInternal&lt;/b&gt; 함수가 호출되는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;SpinEventLoopInternal&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/nodejs/node/blob/main/src/api/embed_helpers.cc#L22C1-L91C1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/nodejs/node/blob/main/src/api/embed_helpers.cc#L22C1-L91C1&lt;/a&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1736411460178&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Maybe&amp;lt;ExitCode&amp;gt; SpinEventLoopInternal(Environment* env) {
  ...
    do {
      if (env-&amp;gt;is_stopping()) break;
      
      //이벤트 루프 생성
      uv_run(env-&amp;gt;event_loop(), UV_RUN_DEFAULT);
      
      if (env-&amp;gt;is_stopping()) break;
      platform-&amp;gt;DrainTasks(isolate);
      ...
      
      //이벤트 루프가 살아있는지 확인!
      more = uv_loop_alive(env-&amp;gt;event_loop());
    } 
      while (more == true &amp;amp;&amp;amp; !env-&amp;gt;is_stopping());
        env-&amp;gt;performance_state()-&amp;gt;Mark(
            node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
      }
  ...

}&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;그리고 SpinEventLoop 함수에서는 &lt;b&gt;uv_run &lt;/b&gt;호출을 통해&lt;b&gt; 이벤트 루프를 생성&lt;/b&gt;하고, &lt;b&gt;uv_loop_alive&lt;/b&gt; 함수를 통해 이벤트 루프가 살아있는지 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;uv_run()&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c#L425&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c#L425&lt;/a&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1736411919732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  if (mode == UV_RUN_DEFAULT &amp;amp;&amp;amp; r != 0 &amp;amp;&amp;amp; loop-&amp;gt;stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);       // 1. timer phase
  }

  while (r != 0 &amp;amp;&amp;amp; loop-&amp;gt;stop_flag == 0) {
    uv__run_pending(loop);      // 2. pending phase
    uv__run_idle(loop);         // 3. idle phase
    uv__run_prepare(loop);      // 4. prepare phase
    uv__io_poll(loop, timeout); // 5. poll phase
    ...
    uv__run_check(loop);        // 6. check phase
    uv__run_closing_handles(loop);
    ...
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }
  if (loop-&amp;gt;stop_flag != 0)
    loop-&amp;gt;stop_flag = 0;

  return r;
}&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;uv_run 함수는 앞서 소개한 것과 같다. while 문 안에서 6개의 페이즈가 순차적으로 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&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;h4 data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[Node.js의 이벤트 루프와 비동기] https://akasai.space/node-js/about_node_js_2/&lt;br /&gt;&lt;br /&gt;[libuv 공식 문서] https://docs.libuv.org/en/latest/index.html&lt;br /&gt;&lt;br /&gt;[NodeJs 공식 문서] https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick&lt;br /&gt;&lt;br /&gt;[libuv 소스 코드] https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c#L425&lt;br /&gt;&lt;br /&gt;[로우 레벨로 살펴보는 Node.js 이벤트 루프] https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/&lt;br /&gt;&lt;br /&gt;[Node.js 이벤트 루프(Event Loop) 샅샅이 분석하기] https://www.korecmblog.com/blog/node-js-event-loop&lt;br /&gt;&lt;br /&gt;[Tasks, microtasks, queues and schedules] &lt;br /&gt;https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/&lt;br /&gt;&lt;br /&gt;[node - 기본 동작 원리와 이벤트 루프, 브라우저를 벗어난 js 실행!] &lt;br /&gt;https://velog.io/@qlgks1/node-%EA%B8%B0%EB%B3%B8-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC&lt;br /&gt;&lt;br /&gt;[What you should know to really understand the Node.js Event Loop]&lt;br /&gt;https://medium.com/the-node-js-collection/what-you-should-know-to-really-understand-the-node-js-event-loop-and-its-metrics-c4907b19da4c&lt;br /&gt;&lt;br /&gt;[Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM]&lt;br /&gt;https://www.youtube.com/watch?v=PNa9OMajw9w&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JavaScript/JS를 파헤쳐보자</category>
      <category>Event loop</category>
      <category>libuv</category>
      <category>nodejs</category>
      <category>이벤트 루프</category>
      <category>페이즈</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/337</guid>
      <comments>https://dcloud.tistory.com/337#entry337comment</comments>
      <pubDate>Wed, 8 Jan 2025 19:10:22 +0900</pubDate>
    </item>
    <item>
      <title>Redis 메모리 초과로 서버 다운된 경험과 해결</title>
      <link>https://dcloud.tistory.com/383</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개발 중 예상치 못한 Redis 메모리 초과 문제로 서버가 다운되는 경험을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트용으로 임시 서버를 두고 작업을 해두었던 터라, 잠깐의 시간 동안 Redis Memory가 가득 찰 일은 없겠다는 안일한 생각을 했다. 그래서 Redis의 maxmemory와 maxmemory-policy 설정을 따로 해두지 않았었다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러다보니 무제한으로 데이터를 저장할 수 있는 상태가 되었다. 그리고 서버가 꽝 터졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기회를 통해 Redis 설정을 최적화하는 과정을 공유해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중이던 프로젝트에서, Redis를 사용해 방에 참가한 사용자 닉네임을 list 자료구조에 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 중, 클라이언트에서 소켓 이벤트를 잘못 처리해 엄청난 양의 데이터가 Redis로 흘러들어왔고, 순식간에 Redis 메모리가 초과되었다. 그리고 서버(Nest.js)도 다운되어버렸다. (&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/boostcampwm-2024/web14-betting-duck/wiki/%EC%86%8C%EC%BC%93-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EA%B0%80-%EB%AC%B4%ED%95%9C%EB%A6%AC%ED%95%84-%EB%90%98%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0&quot;&gt;프론트에서 소켓 이벤트 무한리필 문제&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버 로그&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;--- Last few GCs ---&amp;gt;

[74:0x7f5177d99000]   926795 ms: Scavenge (interleaved) 1952.0 (1990.5) -&amp;gt; 1951.6 (1995.5) MB, pooled: 0 MB, 21.36 / 0.00 ms  (average mu = 0.262, current mu = 0.196) allocation failure; 
[74:0x7f5177d99000]   928965 ms: Mark-Compact (reduce) 1954.8 (1995.5) -&amp;gt; 1952.7 (1990.5) MB, pooled: 0 MB, 1378.40 / 0.00 ms  (+ 418.4 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1851 ms) (average mu = 0.234

&amp;lt;--- JS stacktrace ---&amp;gt;

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Redis 컨테이너 로그&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 컨테이너도 종료되었는데, inspect로 확인해보니 아래의 OOM 에러 로그를 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-01 오전 1.41.17.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tzSfx/btsK2nJJdvJ/sT8ohSeGJ397evvgWE9hP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tzSfx/btsK2nJJdvJ/sT8ohSeGJ397evvgWE9hP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tzSfx/btsK2nJJdvJ/sT8ohSeGJ397evvgWE9hP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtzSfx%2FbtsK2nJJdvJ%2FsT8ohSeGJ397evvgWE9hP0%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;673&quot; height=&quot;349&quot; data-filename=&quot;스크린샷 2024-12-01 오전 1.41.17.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1733143180570&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExitCode 137
OOMKilled: true&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;이는 운영체제의 OOM(Out of Memory) Killer가 시스템 메모리를 보호하기 위해 프로세스를 종료한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법과 개선&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Max-memory : 시스템 메모리의 절반으로 제한하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;레디스를 사용할 때, max-memory-policy 설정은 필수적이다. 그리고 max-memory는 전체 시스템 메모리의 절반 정도로 설정하는 것이 권장된다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RDB 스냅샷 생성 중의 메모리 사용 : fork()&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;- RDB란?&lt;/b&gt;&lt;br /&gt;Redis는 인메모리 데이터 저장소다. 서버 재시작 시 모든 데이터가 유실된다. 그렇기에 적절한 데이터 백업이 필요하다.&lt;br /&gt;Redis는 이를 위해 AOF, RDB의 두 가지 Presistence Option을 제공한다.&lt;br /&gt;아주 간단히 비교하자면, RDB는 key1 : &quot;duck&quot; 이라는 정보의 스냅샷을 보관하는 것이고,&lt;br /&gt;AOF는 set key1 &quot;a&quot;, set key1 &quot;duck&quot; 이라는 명령어의 모음을 보관한다.&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;레디스에는 복구를 위한 RDB 스냅샷 기능을 제공한다. RDB 기능은 어떻게 레디스가 동작하면서 동시에 작업이 진행될 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이것은 Redis가 RDB 스냅샷을 생성할 때 OS의 &lt;b&gt;fork()를 호출해 새로운 자식 프로세스를 생성&lt;/b&gt;하기 때문이다. 백그라운드에서는 자식 프로세스로 RDB를 저장하고, 원래의 프로세스는 일반적인 요청을 처리할 수 있게된다.&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;스크린샷 2024-12-02 오후 10.07.36.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJnYIP/btsK43DFeaP/v4JQYFnQgViZFybIyjTkR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJnYIP/btsK43DFeaP/v4JQYFnQgViZFybIyjTkR0/img.png&quot; data-alt=&quot;http://redisgate.kr/redis/configuration/copy-on-write.php&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJnYIP/btsK43DFeaP/v4JQYFnQgViZFybIyjTkR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeJnYIP%2FbtsK43DFeaP%2Fv4JQYFnQgViZFybIyjTkR0%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;532&quot; height=&quot;331&quot; data-filename=&quot;스크린샷 2024-12-02 오후 10.07.36.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://redisgate.kr/redis/configuration/copy-on-write.php&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서 fork()는 프로세스를 복제하는데, 이 과정에서 Copy-On-Write 방식을 사용하여 부모 프로세스의 메모리를 자식 프로세스와 공유하게 된다. 이렇게 자식 프로세스는 해당 메모리를 복사하면서 추가적인 메모리 사용을 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 Redis가 이미 시스템 메모리를 거의 다 사용하고 있다면, fork() 호출로 인해 메모리 부족(OOM) 상태가 발생할 가능성이 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 방지하기 위해, Redis의 maxmemory는 전체 메모리의 절반 이하로 설정하는 것이 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Jemalloc과 메모리 단편화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 이유가 더 있다면, 레디스는 내부적으로 메모리 할당 시, jemalloc를 기본 할당자로 사용하기 때문이다.&lt;/p&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;jemalloc는 효율적인 메모리 관리를 위해 설계되었다. 하지만 그렇다 할지라도 여전히 메모리 단편화 문제가 발생할 수 있다.&lt;/p&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;단편화로 인해 실제 사용 중인 메모리보다 더 많은 물리적 메모리를 점유하게 되고, 시스템 메모리의 절반으로 maxmemory를 제한하면, 단편화나 추가 메모리 사용으로 인해 Redis가 과도한 메모리를 사용하는 상황을 방지할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Max-memory-policy 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;maxmemory가 설정되었다면, Redis는 메모리 제한에 도달했을 때의 동작을 정의하기 위해 maxmemory-policy 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리가 가득차면, 어떻게 키를 삭제할 것인가에 대한 알고리즘을 설정해주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 아래의 정책이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. noeviction: 메모리가 가득 차면 새 데이터를 받지 않고 에러를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. allkeys-lru: 모든 키에서 가장 적게 사용된 데이터(LRU)를 제거.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. volatile-lru: 만료 시간이 설정된 키에서 LRU 정책으로 제거.&lt;/p&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;기본 설정은 noeviction이다. (위에서 레디스가 터진 이유..) 내가 참여하는 서비스에서는 allkeys-lru를 설정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. ziplist 적용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ziplist란&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ziplist는 Redis에서 사용되는 압축된 연속 메모리 구조로, 작은 데이터를 효율적으로 저장하기 위한 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ziplist를 사용하면 메모리가 줄어드는 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그럼 ziplist를 사용하면, 기존의 방식과 어떻게 다르길래 메모리를 줄일 수 있을까?&lt;/p&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;이는 ziplist가 &lt;b&gt;연속된 메모리 공간에 데이터를 저장&lt;/b&gt;하여 오버헤드를 줄이고, 메모리를 최소화하도록 설계되었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;일반적으로 Redis의 표준 리스트나 해시는 포인터 기반 데이터 구조로, 각 요소를 저장할 때 포인터 오버헤드가 추가된다. 반면 ziplist는 데이터를 하나의 연속된 메모리 블록에 저장하므로 포인터와 관련된 추가적인 메모리 사용을 없앨 수 있다.&lt;/p&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;b&gt;일반&amp;nbsp;리스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1733146793617&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; [포인터1 | 데이터1] -&amp;gt; [포인터2 | 데이터2] -&amp;gt; ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소가 독립적으로 저장되고 포인터를 사용하여 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 여기서는 연속된 데이터로 저장되지 않기 때문에, 포인터 오버헤드가 포함된다.&lt;/p&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;ziplist&amp;nbsp;일반&amp;nbsp;리스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1733146827458&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[헤더 | 데이터1 길이 | 데이터1 | 데이터2 길이 | 데이터2 | ... | 테일]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요소가 연속된 메모리 공간에 저장되기 때문에, 포인터 오버헤드가 필요없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;추가로 ziplist는 요소의 데이터 타입과 길이, 값을 인코딩해서 저장한다. 예를 들어, 작은 정수는 최소한의 바이트(1바이트 또는 2바이트)로 저장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 단점도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;데이터 크기/수 제한이 있다. 특정 크기 이상(디폴트: 64바이트 또는 512개 요소)으로 커지면 더 이상 ziplist를 사용할 수 없게 되어서, 일반적인 자료 구조로 전환이 된다. 그래서대규모 데이터에서는 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;데이터가 연속적으로 저장되기 때문에, 중간 삽입/삭제 시 재배치가 필요하여 업데이트 시간이 더 걸리는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. Redis 5.0부터는 점차 Listpack이 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ziplist의 단점을 보완하기 위해 Redis 5.0부터 Ziplist 대신 Listpack이 도입되기 시작했다.&lt;/p&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;그러다 Redis 7.0는 Listpack이 대부분의 데이터 타입에 적용되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1733379542187&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[헤더 | 데이터1 길이 + 데이터1 | 데이터2 길이 + 데이터2 | ... | 테일]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 길이와 데이터를 함께 저장하여 데이터 접근 속도가 개선되었고, 동적으로 길이를 계산하지 않아도 되어 메모리 효율이 증가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;참고자료&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[우아한테크세미나] 191121 우아한레디스 by 강대명님&lt;br /&gt;https://www.youtube.com/watch?v=mPB2CZiAkKM&lt;br /&gt;&lt;br /&gt;[NHN FORWARD 2021] Redis 야무지게 사용하기&lt;br /&gt;https://www.youtube.com/watch?v=92NizoBL4uA&lt;br /&gt;&lt;br /&gt;Redis Copy-on-Write 분석&lt;br /&gt;http://redisgate.kr/redis/configuration/copy-on-write.php&lt;/blockquote&gt;</description>
      <category>Backend/Redis</category>
      <category>fork</category>
      <category>maxmemory</category>
      <category>ziplist</category>
      <category>레디스</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/383</guid>
      <comments>https://dcloud.tistory.com/383#entry383comment</comments>
      <pubDate>Sun, 1 Dec 2024 01:31:08 +0900</pubDate>
    </item>
    <item>
      <title>[삽질 기록과 해결] Cross-Origin WebSocket에서 쿠키 전송 문제</title>
      <link>https://dcloud.tistory.com/382</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 배포된 서버 Backend의 WebSocket 으로 접근하는 상황이었다.&lt;br /&gt;인증 정보를 쿠키로 전달하려 했지만, WebSocket 연결이 완료되지 못했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bet.gateway.ts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt; handleConnection(client: Socket) {
    try {
      const cookies = this.jwtUtils.parseCookies(
        client.handshake.headers.cookie,
      );
      const accessToken = cookies[&quot;access_token&quot;];
      if (!accessToken) {
        client.emit(&quot;error&quot;, {
          event: &quot;handleConnection&quot;,
          message: &quot;엑세스 토큰이 존재하지 않습니다.&quot;,
        });
        client.disconnect(true);
        return;
      }

     ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bet.gateway.ts에서는 위의 로직으로 accessToken을 검사한다. 그리고 accessToken이 없다면 &lt;code&gt;client.disconnect(true);&lt;/code&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;브라우저의 개발자 도구에서 응답의 헤더를 확인해보니 Cookie가 비어있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 이유로 서버에서 client.handshake.headers.cookie를 조회할 때에도 &lt;code&gt;undefined&lt;/code&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;h2 data-ke-size=&quot;size26&quot;&gt;삽질의 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 원인을 찾기 위해 Nginx 설정, CORS 설정, Socket.IO 설정 등을 모두 점검했지만 문제는 해결되지 않았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시도한 방법들&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Nginx CORS 설정 추가&lt;/h4&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Credentials' 'true';&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Socket.IO Gateway 설정&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@WebSocketGateway({
  namespace: &quot;api/betting&quot;,
  cors: {
    origin: [&quot;http://localhost:3000&quot;, &quot;http://175.45.205.245&quot;],
    credentials: true,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS, Nginx, Socket.IO 모두 올바르게 설정되었지만 쿠키는 여전히 포함되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 원인 파악&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿠키의 SameSite 속성&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE (user.controller.ts)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;    res.cookie(&quot;access_token&quot;, result.accessToken, {
      httpOnly: true,
      maxAge: 1000 * 60 * 60,
      secure: false, // HTTPS를 통해서만 전송되도록 설정
      // sameSite: &quot;strict&quot;, // 기본 값 LAX
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE에서 쿠키는 위와 같이 생성된다.&lt;/p&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;sameSite 설정을 따로 하지 않으면 기본값은 &lt;code&gt;SameSite=Lax&lt;/code&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;그런데 Lax 설정은 &lt;span style=&quot;color: #; text-align: start;&quot;&gt;예외 상황을 제외한&lt;/span&gt;&amp;nbsp;Cross-Origin 요청에서 쿠키를 포함하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;예외 상황: GET 요청 + 탑 레벨 네비게이션 (주소창 입력, 링크 클릭 등)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런 이유로 POST, PUT, WebSocket 연결 등 요청에서는 Lax 쿠키가 포함되지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 SameSite 설정 문제였다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 쿠키의 SameSite 설정 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SameSite=None&lt;/code&gt; + &lt;code&gt;Secure=true&lt;/code&gt;를 사용하여 쿠키가 Cross-Origin 요청에도 포함되도록 설정하는 방법이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE (user.controller.ts)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;res.cookie(&quot;access_token&quot;, token, {
  httpOnly: true,
  secure: true, // HTTPS 필수
  sameSite: &quot;None&quot;, // Cross-Origin 허용
});&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;code&gt;secure: true&lt;/code&gt;를 설정하면 HTTPS 환경이 필수적으로 요구되는데, 현재의 서버는 HTTPS로 구성되어 있지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) JWT 토큰 기반 인증으로 대체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키 대신 토큰을 사용하여 인증을 진행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { io } from &quot;socket.io-client&quot;;

const socket = io(SOCKET_URL + options.url, {
  auth: {
    token: `${access_token}`,
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;handleConnection(client: Socket) {
  const token = socket.handshake.auth.token;
  const payload = this.jwtUtils.verifyToken(token);
  client.data.userId = payload.id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 이용한 방식으로 에러를 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가로 참고하면 좋을 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 이용한 방식으로 처리할 때 가장 처음 custom header를 사용했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;const socket = io(SOCKET_URL + options.url, {
  transports: [&quot;websocket&quot;],
  extraHeaders: {
    Authorization: `Bearer ${accessToken}`, // 커스텀 헤더 만들기
  },
  withCredentials: true,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;  handleConnection(client: Socket) {
    try {
      const authorizationHeader = client.handshake.headers.authorization;
    ...&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;하지만 http&amp;rarr;websocket upgrade를 위한 handshake http 요청에는 custom header를 달 수 없다!&lt;/p&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;blockquote data-ke-style=&quot;style2&quot;&gt;https://socket.io/docs/v4/server-socket-instance/#sockethandshake&lt;br /&gt;https://github.com/whatwg/websockets/issues/16&lt;br /&gt;https://stackoverflow.com/questions/23406163/socket-io-client-how-to-set-request-header-when-making-connection&lt;/blockquote&gt;</description>
      <category>Backend/인증, 인가</category>
      <category>auth</category>
      <category>LAX</category>
      <category>웹소켓</category>
      <category>쿠키</category>
      <category>토큰</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/382</guid>
      <comments>https://dcloud.tistory.com/382#entry382comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:37:22 +0900</pubDate>
    </item>
    <item>
      <title>Redis HINCRBY로 동시성 문제 해결: 실시간 베팅</title>
      <link>https://dcloud.tistory.com/381</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ82Zi/btsLAShHoJO/jlKxiMnKONZkGhg0ELo4Jk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ82Zi/btsLAShHoJO/jlKxiMnKONZkGhg0ELo4Jk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ82Zi/btsLAShHoJO/jlKxiMnKONZkGhg0ELo4Jk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cZ82Zi/btsLAShHoJO/jlKxiMnKONZkGhg0ELo4Jk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;443&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실시간으로 베팅이 이루어지는 서비스이다. &quot;만약 많은 사용자가 동시에 베팅을 진행할 경우, 동시성 문제가 일어나 데이터가 부정확해지는 것은 아닐까?&quot; 고민을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 상황은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-28 오후 10.41.28.png&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SHhIF/btsLActZR9L/DUSEXwkZBxVlNG9KF30Aik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SHhIF/btsLActZR9L/DUSEXwkZBxVlNG9KF30Aik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SHhIF/btsLActZR9L/DUSEXwkZBxVlNG9KF30Aik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSHhIF%2FbtsLActZR9L%2FDUSEXwkZBxVlNG9KF30Aik%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;460&quot; height=&quot;396&quot; data-filename=&quot;스크린샷 2024-12-28 오후 10.41.28.png&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;1. 베팅을 하면 카운트가 올라간다. (베팅 참여자, 베팅한 금액)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;2. 카운트를 올리기 위해 데이터를 조회한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;3. 조회한 데이터에 증가 연산을 수행한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;4. 업데이트 된 데이터를 저장한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경우, 만약 두 명의 사용자가 데이터가 1인 시점에 접근해 각각 1씩 올려도 3이 아니라 &lt;b&gt;2가 되는 동시성 문제&lt;/b&gt;가 발생하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;레디스와 레디스의 HINCRBY 명령어&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;HINCRBY 명령어와 가상의 테스트 시나리오를 통해, 어떻게 동시성 문제를 해결했는지 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레디스의 특징과 Redis HINCRBY 명령&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 단일 스레드 기반으로 동작하여 모든 명령어를 순차적으로 처리한다. 즉 한번에 하나의 요청을 처리한다.&lt;br /&gt;이런 특성 덕분에 Redis는 많은 동시 요청을 처리할 때에도 원자성을 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HINCRBY&lt;/code&gt; 명령어는 그 자체로 원자성을 가진다. 아래의 메커니즘이 하나의 원자성으로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. 키 조회 및 변경
  지정된 해시 키(hash key)와 필드(field)를 조회한다.

2. 증가 연산
  조회한 값을 메모리에서 바로 증가 연산한다.

3. 변경사항 저장
  증가된 값을 다시 지정된 필드에 저장한다.

4. 응답 반환&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-28 오후 10.42.08.png&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd4OXz/btsLAUT8BZ8/hpmiHLz2jFQtKhu2TJvZv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd4OXz/btsLAUT8BZ8/hpmiHLz2jFQtKhu2TJvZv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd4OXz/btsLAUT8BZ8/hpmiHLz2jFQtKhu2TJvZv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd4OXz%2FbtsLAUT8BZ8%2FhpmiHLz2jFQtKhu2TJvZv1%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;507&quot; height=&quot;530&quot; data-filename=&quot;스크린샷 2024-12-28 오후 10.42.08.png&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;772&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;그림으로 쉽게 표현해보면 위와 같다. 하나의 저장소에 동시 접근이 불가능하고 HINCRBY 명령이 원자적으로 수행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동시성 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 확인하기 위해서 실제 레디스에 접근해 테스트를 진행했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베팅 옵션 업데이트&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;  async updateBetOption(roomId: string, option: string, betAmount: number) {
    await Promise.all([
      this.client.hincrby(`test:${roomId}:${option}`, 'currentBets', betAmount),
      this.client.hincrby(`test:${roomId}:${option}`, 'participants', 1),
    ]);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베팅을 업데이트하는 메서드는 위와 같다. 이것을 promise all을 통해 병렬에 가깝게 호출하여 테스트를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis.concurrency.ts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;async function run() {

  ... 레디스 초기화

  const option = &quot;option1&quot;
  const betAmount = 10;
  const updatePromises:Promise&amp;lt;void&amp;gt;[] = [];
  const updateCount = 1000000;

  // 100만 번
  for (let i = 0; i &amp;lt; updateCount; i++) {
    updatePromises.push(redisManager.updateBetOption(roomID, option, betAmount));
  }
  await Promise.all(updatePromises);

  const result = await redisManager.getChannelData(roomID);
  console.log('Fetching channel data:', result);

  const expectedBets = updateCount * betAmount;
  const expectedParticipants = updateCount;

  if (result &amp;amp;&amp;amp; result[option]) {
    const { currentBets, participants } = result[option];

    console.log(&quot;Test Result:&quot;, {
      expectedBets,
      actualBets: parseInt(currentBets, 10),
      expectedParticipants,
      actualParticipants: parseInt(participants, 10),
    });

    if (
      parseInt(currentBets, 10) === expectedBets &amp;amp;&amp;amp;
      parseInt(participants, 10) === expectedParticipants
    ) {
      console.log(&quot;테스트 통과&quot;);
      console.log(&quot;&quot;);
    } else {
      console.error(&quot;테스트 실패: 동시성 문제&quot;);
    }
  } 
  ... 레디스  삭제 및 종료
}

run().catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 번의 카운트를 통해 업데이트를 실행하고 결과를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대 값은 &lt;code&gt;expectBetAmount = 100,000*10&lt;/code&gt;, &lt;code&gt;expectParticipants = 100,000*1&lt;/code&gt; 이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzxqNq/btsL18kPWvq/f0DLTEWJH8KtHEybPCyJk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzxqNq/btsL18kPWvq/f0DLTEWJH8KtHEybPCyJk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzxqNq/btsL18kPWvq/f0DLTEWJH8KtHEybPCyJk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzxqNq%2FbtsL18kPWvq%2Ff0DLTEWJH8KtHEybPCyJk1%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;326&quot; height=&quot;218&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대값과 일치하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Redis</category>
      <category>동시성 문제</category>
      <category>레디스</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/381</guid>
      <comments>https://dcloud.tistory.com/381#entry381comment</comments>
      <pubDate>Tue, 19 Nov 2024 19:43:14 +0900</pubDate>
    </item>
    <item>
      <title>레디스에서 O(N) 관련 커맨드는 주의하기</title>
      <link>https://dcloud.tistory.com/380</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis는 단일 스레드(single-threaded) 기반으로 동작한다!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 단일 스레드를 기반으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 한번에 하나의 요청만 가능하다. 이는 하나의 명령을 처리하는 동안 다른 명령은 대기 상태로 머무르게 된다는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 명령어 처리 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 내부 처리 과정을 이해해보는 것도 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 클라이언트의 요청을 처리할 때 두 가지 주요 단계를 거친다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;processInputBuffer 단계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 클라이언트로부터 들어오는 데이터를 입력 버퍼(input buffer)에 저장하고, 클라이언트가 보낸 패킷이 하나의 완전한 명령어로 완성될 때까지 이 버퍼에 쌓이게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;processCommand 단계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 완전한 명령어로 완성되면, Redis는 processCommand를 호출하여 해당 명령을 실제로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, 명령이 완전히 실행될 때까지 패킷으로 대기하게 된다. 그런 이유로 이 단계에서 명령어의 시간 복잡도가 실행 시간에 직접적인 영향을 미치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 시간 복잡도가 O(N)인 명령어를 실행하면 데이터 크기에 비례하여 처리 시간이 길어지고, 이로 인해 Redis가 다른 요청을 처리하지 못하는 차단(blocking) 현상이 발생하게 된다.&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;자주 접하게 되는 O(N) 관련 명령어&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;code&gt;keys&lt;/code&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 패턴에 맞는 모든 키를 검색한다. 전체 키를 스캔하기 때문에 O(N)의 시간 복잡도를 가진다.&lt;br /&gt;데이터베이스 크기가 클수록 검색 시간이 길어지는 문제가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &lt;code&gt;DEL&lt;/code&gt; (대량 삭제 시)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 키를 한꺼번에 삭제할 때 데이터 크기에 따라 처리 시간이 길어진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. &lt;code&gt;FLUSHALL&lt;/code&gt; / &lt;code&gt;FLUSHDB&lt;/code&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 모든 데이터 또는 특정 DB의 데이터를 삭제한다.&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;keys 대신 &lt;code&gt;SCAN&lt;/code&gt; 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SCAN&lt;/code&gt; 명령어는 데이터베이스 전체를 한 번에 검색하지 않고, 일부씩 나누어 반환하는 비차단(non-blocking) 명령이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;let cursor = '0';
do {
  const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', 'prefix:*', 'COUNT', 10);
  cursor = nextCursor;
} while (cursor !== '0');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터를 한 번에 처리하지 않고, 포인터를 이용해 부분적으로 데이터를 나누어 반환한다.&lt;br /&gt;이렇게 분할 작업을 수행하기 때문에 검색 도중에도 다른 명령어를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DEL 대신 &lt;code&gt;UNLINK&lt;/code&gt; 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEL 명령어는 키 삭제 작업이 완료될 때까지 블로킹한다.&lt;br /&gt;반면에, UNLINK는 Redis 서버의 백그라운드 쓰레드에서 데이터를 삭제하기 때문에 비동기로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;await redis.unlink(...keys)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Redis</category>
      <category>O(n)</category>
      <category>레디스</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/380</guid>
      <comments>https://dcloud.tistory.com/380#entry380comment</comments>
      <pubDate>Tue, 19 Nov 2024 18:10:51 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] DROP 명령어는 어떻게 테이블을 통채로 날릴까?</title>
      <link>https://dcloud.tistory.com/377</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 명령어를 공부해본 사람이라면, DROP과 TRUNCATE, DELETE 명령어의 차이를 들어보았을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-12 오후 3.15.43.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buZxKC/btsKFfrWt2C/M3cvwE0lBZBigbrnQvfI6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buZxKC/btsKFfrWt2C/M3cvwE0lBZBigbrnQvfI6K/img.png&quot; data-alt=&quot;https://wikidocs.net/4021&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buZxKC/btsKFfrWt2C/M3cvwE0lBZBigbrnQvfI6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuZxKC%2FbtsKFfrWt2C%2FM3cvwE0lBZBigbrnQvfI6K%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;772&quot; height=&quot;201&quot; data-filename=&quot;스크린샷 2024-11-12 오후 3.15.43.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://wikidocs.net/4021&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 이야기를 하자면, DELETE 명령어는 원하는 데이터를 지울 수 있고, 삭제 후 잘못 삭제한 것을 되돌릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TRUNCATE 명령어는 테이블은 삭제하지는 않고, 데이터만 삭제한다. 삭제 후 절대 되돌릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DROP 명령어는 테이블 전체를 삭제한다. 삭제 후 절대 되돌릴 수 없다.&lt;/p&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;그럼 DROP 명령어는 어떻게 작동하길래, 테이블이 통채로 사라질까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-12 오후 3.19.07.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjK5KM/btsKGOzzxEv/c8uOjmOhW9nmEPNDTEkwgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjK5KM/btsKGOzzxEv/c8uOjmOhW9nmEPNDTEkwgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjK5KM/btsKGOzzxEv/c8uOjmOhW9nmEPNDTEkwgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjK5KM%2FbtsKGOzzxEv%2Fc8uOjmOhW9nmEPNDTEkwgK%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;654&quot; height=&quot;510&quot; data-filename=&quot;스크린샷 2024-11-12 오후 3.19.07.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 innoDB에서는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;테이블을 &lt;/span&gt;.ibd 파일로 저장한다.&lt;/p&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;DROP시 .ibd 파일이 어떻게 되는지 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1731392729649&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; CREATE DATABASE test_db;
Query OK, 1 row affected (0.01 sec)

mysql&amp;gt; INSERT INTO test_table (id, name) VALUES (1, 'example');
Query OK, 1 row affected (0.01 sec)&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;가상의 test_db를 만들고 테이블을 생성한 뒤 값을 삽입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 TRUNCATE 이후 .ibd 파일이 존재하는지 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1731392763651&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; TRUNCATE TABLE test_table;
Query OK, 0 rows affected (0.01 sec)

$ ls -l {---}/mysql/data/test_db/test_table.ibd
-rw-r----- 1 mysql mysql 98304 Nov 12 12:00 /usr/local/mysql/data/test_db/test_table.ibd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TRUNCATE 명령어는 .ibd 파일이 그대로 존재한다.&lt;/p&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;DROP 명령어 이후 .ibd 파일이 존재하는지 살펴보면,&lt;/p&gt;
&lt;pre id=&quot;code_1731392773771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; DROP TABLE test_table;
Query OK, 0 rows affected (0.01 sec)

$ ls -l {---}/mysql/data/test_db/test_table.ibd
ls: /usr/local/mysql/data/test_db/test_table.ibd: No such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.ibd 파일이 삭제된 것을 알 수 있다.&lt;/p&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;DROP 명령어는 .ibd 파일이 삭제해 테이블을 통채로 날리는 것을 알 수 있다.&lt;/p&gt;</description>
      <category>Backend/MySQL</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/377</guid>
      <comments>https://dcloud.tistory.com/377#entry377comment</comments>
      <pubDate>Tue, 12 Nov 2024 14:51:55 +0900</pubDate>
    </item>
    <item>
      <title>TS로 아주아주 간단한 MySQL 만들어보기</title>
      <link>https://dcloud.tistory.com/371</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 내부적으로 OS의 파일 I/O를 사용한다.&lt;/p&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;우선 MySQL의 아키텍처를 참고해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-05 오후 7.23.21.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mK5TE/btsKyVZSOGJ/0K6nF1Un5h1qmDyKqbpd6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mK5TE/btsKyVZSOGJ/0K6nF1Un5h1qmDyKqbpd6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mK5TE/btsKyVZSOGJ/0K6nF1Un5h1qmDyKqbpd6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmK5TE%2FbtsKyVZSOGJ%2F0K6nF1Un5h1qmDyKqbpd6k%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;612&quot; height=&quot;441&quot; data-filename=&quot;스크린샷 2024-11-05 오후 7.23.21.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 SQL 구문이 들어오면 Parser -&amp;gt; 전처리기 -&amp;gt; 옵티마이저 -&amp;gt; 엔진 실행기 -&amp;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;h2 data-ke-size=&quot;size26&quot;&gt;설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 옵티마이저와 엔진 실행기는 제거하고 설계했다. 옵티마이저의 실행 계획 알고리즘 CBO를 구현할 역량이 없다고 생각했다. 또 내부적으로 실행 계획에 따라 풀스캔이나 인덱스로 탐색하는 것을 구현해야하는데, 마찬가지로 구현할 역량이 없다고 판단했다.&lt;/p&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;스토리지 엔진은 InnoDB를 참고했다.&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-05 오후 7.21.11.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckQdOM/btsKyU0Zouf/fd6EivKHkvOxvwLQnwtvMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckQdOM/btsKyU0Zouf/fd6EivKHkvOxvwLQnwtvMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckQdOM/btsKyU0Zouf/fd6EivKHkvOxvwLQnwtvMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckQdOM%2FbtsKyU0Zouf%2Ffd6EivKHkvOxvwLQnwtvMK%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;716&quot; height=&quot;253&quot; data-filename=&quot;스크린샷 2024-11-05 오후 7.21.11.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 구현에서 초점을 둔 것은, &lt;b&gt;Disk I/O를 최대한 줄이는 것&lt;/b&gt;을 목표로 삼았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런 이유로 두 가지를 중점을 두고 다루었는데, &lt;b&gt;캐시 메모리&lt;/b&gt; 관리와 파일을 &lt;b&gt;Block 단위&lt;/b&gt;로 접근하는 것이다. 실제 MySQL도 위와 비슷하게 동작하기 때문에 많은 참고를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB Server 초기화&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;app.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731225599208&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import DBServer from &quot;./database/DBServer&quot;;

DBServer.start();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;DBServer.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731225599209&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import BufferPool from &quot;./BufferPool&quot;;
import Parser from &quot;./Parser&quot;;
import Preprocessor from &quot;./Preprocessor&quot;;
import FileSystem from &quot;./FileSystem&quot;;
import SQLHandler from &quot;./SQLHandler&quot;;

export default class DBServer{
  public static parser: Parser;
  public static preprocessor: Preprocessor;
  public static bufferPool: BufferPool;
  public static fileSystem: FileSystem;
  public static sqlHandler: SQLHandler;

  public static start(){
    this.parser = new Parser();
    this.preprocessor = new Preprocessor();
    this.bufferPool = new BufferPool();
    this.fileSystem = new FileSystem();
    this.preprocessor.loadMetaData();
    this.bufferPool.loadIndexData();
    this.sqlHandler = new SQLHandler(this.parser, this.preprocessor, this.bufferPool);
  }
}&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;app.ts가 실행될 때 DB 서버는 의존성과 preprocessor와 bufferPool의 메모리에 필요한 데이터를 로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;preprocessor는 디스크 접근과는 거리가 먼 역할이다. 적합한 테이블이 존재하는지, column이 존재하는지를 체크한다. 실제 DB에서는 권한이 있는지도 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그러기 위해서는 사실 디스크에 접근을 해야한다. 테이블과 컬럼이 디스크에 존재하기 때문이다. 그러나 매번 확인을 위해 디스크에 접근하면, 디스크 I/O가 생긴다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이것을 막기 위해서 로드 시에 캐시 메모리를 디스크에서 불러오도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Parser&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Parser.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731225850434&quot; class=&quot;processing&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export default class Parser {

  public parse(SQL: string){
    const sql = SQL.toLowerCase();
    const command = sql.split(&quot; &quot;)[0];
    if(command ==='select'){
      this.parserSelect(sql);
    }
    if(command ==='insert'){
      this.parserInsert(sql);
    }
  }

  public parserSelect(sql: string){
    const selectRegex = /select (.+) from (\w+)(?: where (.+))?/i;
    const match = sql.match(selectRegex);
    if (!match) {
      throw new Error('올바르지 않은 SELECT 구문');
    }
    const columns = match[1].split(',').map(col =&amp;gt; col.trim());
    const table = match[2];
    const condition = match[3] ? match[3].trim() : null;
    return {
      command: 'select',
      table,
      columns,
      condition
    };
  }

  public parserInsert(sql: string){
    const insertRegex = /insert into (\w+) \((.+)\) values \((.+)\)/i;
    const match = sql.match(insertRegex);
    if (!match) {
      throw new Error('올바르지 않은 INSERT 구문');
    }
    const table = match[1];
    const columns = match[2].split(',').map(col =&amp;gt; col.trim());
    const values = match[3].split(',').map(val =&amp;gt; val.trim());
    if (columns.length !== values.length) {
      throw new Error('컬럼 수가 값의 수와 일치하지 않습니다.');
    }

    return {
      command: 'insert',
      table,
      columns,
      values
    };
  }
}&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;아주 간단한 DB 구현이 목표였기 때문에, Parser는 insert, select 만을 구현했고 select의 조건도 id를 조회하는 것만으로 제한해 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;Preprocessor&lt;/h2&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;preprocessor는 DB에 적절한 테이블과 컬럼이 존재하는지를 체크한다. 위에서도 언급했지만, 그렇다면 여기서 파일 I/O가 이루어져야하는가에 대한 고민이 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실제 DB의 동작을 살펴보니 메타데이터를 미리 캐싱해둔다고 한다. DB 실행 시에, DB와 테이블 구조 정보를 메타데이터 파일에서 읽어와 메모리에 로드한다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;metadata.json&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731227072928&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;tables&quot;: {
    &quot;user&quot;: {
      &quot;columns&quot;: [&quot;id&quot;, &quot;name&quot;, &quot;email&quot;],
      &quot;indexes&quot;: [&quot;id&quot;],
      &quot;auto_increment&quot; : 1
    },
    &quot;board&quot;: {
      &quot;columns&quot;: [&quot;id&quot;, &quot;title&quot;, &quot;content&quot;, &quot;userId&quot;],
      &quot;indexes&quot;: [&quot;id&quot;, &quot;userId&quot;],
      &quot;auto_increment&quot; : 1
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대략적인 metadata 형식은 이처럼 지정했다. auto_increment 는 삽입 시에 PK를 어떻게 처리를 할까 고민을 해보니, 이것도 마찬가지로 실제 디스크에 접근하기에는 리스크가 크기 때문에 메타 데이터에 저장해두었다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Preprocessor.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731225962830&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import fs from 'fs';

export default class Preprocessor {
  
  private metadataMemory: any;
  
  public loadMetaData(){
    const filePath = `${process.cwd()}/src/database/files/metadata.json`;
    const rawData = fs.readFileSync(filePath, 'utf-8');
    this.metadataMemory = JSON.parse(rawData);
  }

  public preprocessInsert(parsedQuery: any){
    const { table, columns, values } = parsedQuery;
    if(!this.isTableExist(table)) throw new Error(`해당 테이블이 존재하지 않습니다. table: ${table}`);
    if(!this.isColumnExist(table, columns))  throw new Error(`컬럼이 테이블에 존재하지 않습니다. column: ${columns}, table: ${table}`);
  }

  public preprocessSelect(parsedQuery: any){
    const { table, columns, condition } = parsedQuery;
    if(!this.isTableExist(table)) throw new Error(`해당 테이블이 존재하지 않습니다. table: ${table}`);
    if(!this.isColumnExist(table, columns)) throw new Error(`컬럼이 테이블에 존재하지 않습니다. column: ${columns}, table: ${table}`);
    if(!this.validCondition(table, condition)) throw new Error(`적합하지 않은 조건입니다. condition: ${condition}, table: ${table}`);
  }

  public isTableExist(tableName: string){
    return !!(this.metadataMemory.tables[tableName]);
  }

  public isColumnExist(table: string, columns: string[]){
    columns.forEach((column: string) =&amp;gt; {
      if (column!=&quot;*&quot;&amp;amp;&amp;amp;!this.metadataMemory.tables[table].columns[column]) {
        return false;
      }
    });
    return true;
  }

  public validCondition(table: string, condition: string){
    const [column, value] = condition.split(&quot;=&quot;); //id=1
    if (!this.metadataMemory.tables[table].columns[column]) {
      return false;
    }
    return true;
  }
}&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;&amp;nbsp;아주 간단한 preprocessor 이다. 테이블과 컬럼, 조건이 일치하는지 정도로만 체크했다. 위에서 언급했던 것처럼 캐시된 메모리를 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 값이 변경되거나 테이블이 변경되면 캐시를 수정해야하는 로직이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BufferPool&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 파일을 따로 두어야하나 싶은 고민을 했다. 왜냐하면 인덱스 파일을 읽어와 다시 해당하는 데이터 파일로 접근하면 두 번의 I/O가 생기고, 그럴바에는 데이터 파일을 읽어버리는게 더 빠르지 않나 라는 고민을 했다.&lt;/p&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 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&amp;nbsp;실제&amp;nbsp;mysql의&amp;nbsp;동작을&amp;nbsp;확인해보니,&amp;nbsp;인덱스&amp;nbsp;파일도&amp;nbsp;innoDB의&amp;nbsp;버퍼&amp;nbsp;풀에서&amp;nbsp;캐시로&amp;nbsp;관리한다는&amp;nbsp;것을&amp;nbsp;알았다.&amp;nbsp;해당&amp;nbsp;부분을&amp;nbsp;고려해&amp;nbsp;버퍼풀&amp;nbsp;클래스에&amp;nbsp;메모리를&amp;nbsp;만들어두었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;BufferPool.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731226048602&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import FileSystem from './FileSystem';

export default class BufferPool {
  private indexMemory = new Map();
  private dataMemory = new Map();
  private fileSystem: FileSystem;

  constructor() {
    this.fileSystem = new FileSystem();
  }

  public loadIndexData(){
    const rawData = this.fileSystem.readIndexData();
    this.indexMemory = new Map(Object.entries(JSON.parse(rawData)).map(([key, value]) =&amp;gt; [Number(key), value]));
  }

  //id로 조회하는 경우만으로 한정
  public findInBufferPool(table:string, columns:string[], condition:string){
    if(condition){
      const [key, index] = condition.split(&quot;=&quot;);
      if(key==='id'){
        const data = this.dataMemory.get(`${table}-${index}`);
        if(data){
          if(columns[0]===&quot;*&quot;) return data;
          else{
            const filteredData:Record&amp;lt;string, any&amp;gt; = {};
            columns.forEach(col =&amp;gt; {
              filteredData[col] = data[col];
            });
            return filteredData;
          }
        }
      }
    }
    return null;
  }

  public loadData(table: string, id: number|null=null){
    if(!id){
      this.loadFullDataFromFile(table);
    }else{
      this.loadDataByIdFromFileAndCache(table, id);
    }
  }

  public loadDataByIdFromFileAndCache(table: string, id: number) {
    const blockInfo = this.indexMemory.get(`${table}-${id}`);
    if (!blockInfo) {
      throw new Error(`ID에 해당하는 블록을 찾을 수 없습니다: ${id}`);
    }
    const rawData = this.fileSystem.readBlock(table, blockInfo);
    const data = JSON.parse(rawData);
    this.dataMemory.set(`${table}-${id}`, data);
    return data;
  }

  public loadFullDataFromFile(table: string) {
    const rawData = this.fileSystem.readFullTable(table);
    const data = JSON.parse(rawData);
    return data;
  }

  public insertDataToFileAndBuffer(table: string, id: number, data: any) {
    const {blockStart, rawData} = this.fileSystem.writeBlock(table, data);
    this.indexMemory.set(`${table}-${id}`, { offset: blockStart, length: rawData.length });
    this.dataMemory.set(`${table}-${id}`, data);

    this.fileSystem.saveIndexData(this.indexMemory);
  }
}&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;버퍼풀은 innoDB의 버퍼풀을 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 데이터를 메모리에서 찾으면, 메모리에서 반환을 하고 만약 메모리에 없다면 디스크에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저장 방식에 대한 고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;목표는 파일 I/O를 최대한 줄이는 것이다. (물론 JSON 파일을 통채로 읽어와 캐시로 적재할 수는 있지만, 그렇게 하면 실제 DB의 동작과 거리가 있다고 생각했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실제 InnoDB에서도 Page라는 블록 단위로 데이터를 저장하고 읽어온다.&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;우선 JSON 으로 데이터를 저장을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그럼 만약 10만개의 데이터를 하나의 파일에 전부 몰아넣어야할까? 분리해서 저장한다면 어느정도로 분리를 해야하고, 파일명을 어떻게 지어야할까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) DB에서처럼 페이지 단위로 나누어서 저장 파일명을 페이지 번호로 두고, 인덱스 파일에 key, value 형태로 두어 해당하는 파일로 접근한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;(2)&amp;nbsp;한&amp;nbsp;파일에&amp;nbsp;다&amp;nbsp;저장하고&amp;nbsp;fileDescriptor.read()&amp;nbsp;메서드를&amp;nbsp;이용해&amp;nbsp;특정&amp;nbsp;부분만&amp;nbsp;읽어오기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;fileDescriptor.read는&amp;nbsp;파일의&amp;nbsp;특정&amp;nbsp;부분만&amp;nbsp;읽을&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;기능을&amp;nbsp;제공한다.&amp;nbsp;파일을&amp;nbsp;메모리에&amp;nbsp;다&amp;nbsp;올리지&amp;nbsp;않고,&amp;nbsp;지정한&amp;nbsp;바이트&amp;nbsp;범위만&amp;nbsp;읽어온다.&amp;nbsp;여기서도&amp;nbsp;인덱스&amp;nbsp;파일에는&amp;nbsp;offset을&amp;nbsp;저장하고&amp;nbsp;fileDescriptor에&amp;nbsp;해당&amp;nbsp;범위를&amp;nbsp;가져오게끔한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단 DB를 구현하며 흐름을 파악하는 것이 목표이기 때문에, 2번 방법을 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;FileSystem.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731225562258&quot; class=&quot;typescript&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 fs from 'fs';

export default class FileSystem {
  private readonly BLOCK_SIZE = 1024; //1KB

  public readFullTable(table: string){
    const filePath = `${process.cwd()}/src/database/files/table/${table}.data`;
    const rawData = fs.readFileSync(filePath, 'utf-8');
    return rawData;
  }
  
  public readBlock(table:string, blockInfo: any){
    const { offset, length } = blockInfo;
    const filePath = `${process.cwd()}/src/database/files/table/${table}.data`;
    const fd = fs.openSync(filePath, 'r'); 
    const buffer = Buffer.alloc(length);
    fs.readSync(fd, buffer, 0, length, offset);
    fs.closeSync(fd);
    const rawData = buffer.toString('utf-8').trim();
    return rawData;
  }

  public writeBlock(table: string, data: any){
    const filePath = `${process.cwd()}/src/database/files/table/${table}.data`;
    const rawData = JSON.stringify(data, null, 2);
    if (rawData.length &amp;gt; this.BLOCK_SIZE) {
      throw new Error(`데이터가 블록 크기(${this.BLOCK_SIZE}바이트)를 초과했습니다.`);
    }
    const fd = fs.openSync(filePath, 'a');
    const blockStart = fs.fstatSync(fd).size;
    const buffer = Buffer.alloc(this.BLOCK_SIZE, ' ');
    buffer.write(rawData);
    fs.writeSync(fd, buffer);
    fs.closeSync(fd);
    return {blockStart, rawData};
  }

  public saveIndexData(indexMemory:any) {
    const filePath = `${process.cwd()}/src/database/files/index.json`;
    fs.writeFileSync(filePath, JSON.stringify(Object.fromEntries(indexMemory), null, 2), 'utf-8');
  }

  public readIndexData() {
    const filePath = `${process.cwd()}/src/database/files/index.json`;
    const rawData = fs.readFileSync(filePath, 'utf-8');
    return rawData;
  }

  public readMetaData() {
    const filePath = `${process.cwd()}/src/database/files/metadata.json`;
    const rawData = fs.readFileSync(filePath, 'utf-8');
    return rawData;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파일 시스템에 접근하는 것은 이처럼 구현했다. 저장과 조회를 블럭 단위로 이루어지도록 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL Handler&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SQL Handler.ts&lt;/blockquote&gt;
&lt;pre id=&quot;code_1731227021389&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import BufferPool from &quot;./BufferPool&quot;;
import Parser from &quot;./Parser&quot;;
import Preprocessor from &quot;./Preprocessor&quot;;

export default class SQLHandler {
  private parser: Parser;
  private preprocessor: Preprocessor;
  private bufferPool: BufferPool;

  constructor(parser: Parser, preprocessor: Preprocessor, bufferPool: BufferPool) {
    this.parser=parser;
    this.preprocessor=preprocessor;
    this.bufferPool=bufferPool;
  }

  public query(sql: string) {
    const parsedQuery = this.parser.parse(sql);
    this.queryProcess(parsedQuery);
  }

  public queryProcess(parsedQuery: any){
    if (parsedQuery.command === 'select') {
      const { table, columns, condition } = parsedQuery;
      this.preprocessor.preprocessSelect(parsedQuery);
      const cachedData = this.bufferPool.findInBufferPool(table, columns, condition);
      if(cachedData){
        return cachedData;
      } else {
        const data = this.bufferPool.loadData(table, condition?condition.split('=')[0]:null);
        return data;
      }
    } else if (parsedQuery.command === 'insert') {
      const { table, columns, values } = parsedQuery;
      this.preprocessor.preprocessInsert(parsedQuery);


    } 
    //TODO: 다른 명령어 추가하기
    else {
      throw new Error(`SQL command 오류: ${parsedQuery.command}`);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 실행하는 핸들러는 이처럼 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731227240238&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.sqlHandler.query(
 'select A from table'
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 사용할 수 있게 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/MySQL</category>
      <category>MySQL</category>
      <category>간단</category>
      <category>디스크 I/O</category>
      <category>아키텍처</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/371</guid>
      <comments>https://dcloud.tistory.com/371#entry371comment</comments>
      <pubDate>Thu, 17 Oct 2024 10:14:09 +0900</pubDate>
    </item>
    <item>
      <title>100만개의 데이터, 경우에 따른 실행 계획 확인해보기</title>
      <link>https://dcloud.tistory.com/369</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;csv을 이용해 100만 개의 데이터를 DB에 넣은 상태이다.&amp;nbsp;explain 명령을 통해 &lt;span style=&quot;color: #; text-align: start;&quot;&gt;각 쿼리의&amp;nbsp;&lt;/span&gt;실행 계획이 어떻게 변화하는지를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Explain과 Analyze&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Explain&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, mysql에서 explain 키워드를 붙이면 아래와 같이 실행 계획을 조회할 수 있다. explain은 쿼리를 직접 실행하지는 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_스크린샷 2024-09-30 오후 2.54.01.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQJwR/btsKvaiKOB5/fkPitBpKN2qix6ZpHZlfpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQJwR/btsKvaiKOB5/fkPitBpKN2qix6ZpHZlfpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQJwR/btsKvaiKOB5/fkPitBpKN2qix6ZpHZlfpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQJwR%2FbtsKvaiKOB5%2FfkPitBpKN2qix6ZpHZlfpK%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;781&quot; height=&quot;207&quot; data-filename=&quot;edited_edited_스크린샷 2024-09-30 오후 2.54.01.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 나오는 각 필드의 개념을 표로 정리해보았다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 81.7442%; height: 591px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;실행계획의 순서. 이 순서대로 select 문이 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;select type&lt;/td&gt;
&lt;td&gt;SIMPLE : 단순 select문&lt;br /&gt;PRIMARY : 첫번째 쿼리&lt;br /&gt;DERIVED : select문으로 추출된 테이블 ( from 절에서의 서브쿼리 또는 inline view)&lt;br /&gt;SUBQUERY : sub query 중 첫번째 select문&lt;br /&gt;UNION : UNION쿼리에서 PRIMARY를 제외한 나머지 select문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;table&lt;/td&gt;
&lt;td&gt;대상이 되는 테이블 or alias명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;data access 타입&lt;br /&gt;mysql 서버가 각 테이블의 레코드를 어떤 방식으로 읽었는 지를 나타냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;possible_keys&lt;/td&gt;
&lt;td&gt;해당 테이블에서 데이터를 찾기 위해 선택한 인덱스 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;실제로 쿼리 실행에 사용한 인덱스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key_len&lt;/td&gt;
&lt;td&gt;쿼리를 처리하기 위해 단일, 다중 컬럼으로 구성된 인덱스의 각 레코드에서 몇 바이트까지 사용했는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ref&lt;/td&gt;
&lt;td&gt;행을 추출하는데 키와 함께 사용된 컬럼이나 상수 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows&lt;/td&gt;
&lt;td&gt;쿼리 수행에서 예상하는 검색해야 할 행 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extra&lt;/td&gt;
&lt;td&gt;쿼리에 관한 추가적인 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filtered&lt;/td&gt;
&lt;td&gt;where, on 절에 의해 필터링된 행의 백분율(필터링 되고 남은 레코드의 비율)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 참고해, 실행 계획을 살펴볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Analyze&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;analyze 명령어는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이건 실제로 실행 한 뒤의 결과를 통해 실행 계획을 보여주는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 9.44.15.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L4iAt/btsKuH2klHM/CMBVABKGTCHbDjGD72Gj41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L4iAt/btsKuH2klHM/CMBVABKGTCHbDjGD72Gj41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L4iAt/btsKuH2klHM/CMBVABKGTCHbDjGD72Gj41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL4iAt%2FbtsKuH2klHM%2FCMBVABKGTCHbDjGD72Gj41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;124&quot; data-filename=&quot;스크린샷 2024-11-02 오후 9.44.15.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡쳐한 사진을 보면, 실제 실행 시간인 actual time이라는게 보인다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;1. PK를 기준으로 탐색할 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 쿼리문을 날려보며 실행 계획을 살펴보자. 입력한 데이터에서 기본 PK는 id 필드인 것을 전제로 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 id를 기준으로 탐색한 쿼리이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.33.25.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OYFYU/btsKvawiLpa/SPd1y4FYXhsO3xypIIV8Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OYFYU/btsKvawiLpa/SPd1y4FYXhsO3xypIIV8Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OYFYU/btsKvawiLpa/SPd1y4FYXhsO3xypIIV8Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOYFYU%2FbtsKvawiLpa%2FSPd1y4FYXhsO3xypIIV8Q1%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;786&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.33.25.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1과 123456으로 쿼리를 탐색하니, type이 &lt;b&gt;range&lt;/b&gt;로 fullscan이&amp;nbsp;발생하지&amp;nbsp;않는&amp;nbsp;것을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;클러스터드 인덱스&lt;/b&gt;가 테이블의 데이터를 id 순서대로 정렬해 두었기 때문이다. 이 덕분에 id 범위 탐색 시 빠르게 조회가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;b&gt;클러스터드 인덱스&lt;/b&gt;가 무엇일까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터드 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터드 인덱스는 인덱스 자체가 데이터의 &lt;b&gt;물리적 순서&lt;/b&gt;대로 정렬되어 저장되는 방식이다. 즉, 테이블의 리프 노드가 PK 순서에 맞춰 정렬되고 연결되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 PK를 기준으로 정렬되어 있기 때문에, 쿼리가 id를 기준으로 탐색할 때 효율적으로 데이터를 조회할 수 있다.&amp;nbsp;MySQL에서는 PK 인덱스가 클러스터드 인덱스로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;커버링 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #; text-align: start;&quot;&gt;&amp;nbsp;그렇다면 이와 같은 인덱스가 id에만 해당하는 걸까?&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;쿼리에서 필요한 데이터가 모두 인덱스에 포함&lt;/b&gt;되어 있다면, 테이블 데이터에 접근하지 않고도 인덱스만으로 결과를 반환할 수 있다. 이를 커버링 인덱스라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;아래는 id를 통해 행을 불러오는 쿼리문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.34.08.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bviUlk/btsKuuCce1o/63p5NcVsCnvjER0K9FH1wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bviUlk/btsKuuCce1o/63p5NcVsCnvjER0K9FH1wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bviUlk/btsKuuCce1o/63p5NcVsCnvjER0K9FH1wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbviUlk%2FbtsKuuCce1o%2F63p5NcVsCnvjER0K9FH1wK%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;769&quot; height=&quot;102&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.34.08.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Extra를 보면 null인 것을 알 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 id를 통해 id를 가져오는 쿼리문을 날려보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.35.06.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDODkd/btsKtqVf9Oh/A4IR91uZ2cc52xiBN5s6Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDODkd/btsKtqVf9Oh/A4IR91uZ2cc52xiBN5s6Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDODkd/btsKtqVf9Oh/A4IR91uZ2cc52xiBN5s6Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDODkd%2FbtsKtqVf9Oh%2FA4IR91uZ2cc52xiBN5s6Mk%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;795&quot; height=&quot;171&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.35.06.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Extra를 보면 &lt;b&gt;Using Index&lt;/b&gt;인 것을 알 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여긴 왜 Using Index일까? 바로 &lt;b&gt;커버링 인덱스&lt;/b&gt; 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커버링&amp;nbsp;인덱스는&amp;nbsp;테이블의&amp;nbsp;데이터&amp;nbsp;파일에&amp;nbsp;접근하지&amp;nbsp;않고&amp;nbsp;인덱스만으로&amp;nbsp;쿼리&amp;nbsp;결과를&amp;nbsp;반환하는&amp;nbsp;방식이다.&amp;nbsp;이렇게&amp;nbsp;쿼리를&amp;nbsp;처리하면&amp;nbsp;불필요한&amp;nbsp;디스크&amp;nbsp;I/O를&amp;nbsp;줄이고,&amp;nbsp;쿼리&amp;nbsp;성능을&amp;nbsp;크게&amp;nbsp;향상시킬&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Extra에 Using Index가 나타난 이유는, id 컬럼만 조회하는 쿼리에서 id가 포함된 인덱스를 통해 모든 데이터를 커버할 수 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서 PK를 기준으로 데이터가 어떻게 정렬되어있는지, 커버링 인덱스는 무엇인지 알아볼 수 있었다&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. FK를 기준으로 탐색할 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 FK를 초점으로 탐색을 진행해보자. 현재 seller는 PK, price는 외래키인 상태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래는 각각 PK와 FK를 기준으로 쿼리를 날린 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.36.39.png&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nj2R4/btsKusxBMEq/y17IolVjt62N58UFPdydVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nj2R4/btsKusxBMEq/y17IolVjt62N58UFPdydVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nj2R4/btsKusxBMEq/y17IolVjt62N58UFPdydVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNj2R4%2FbtsKusxBMEq%2Fy17IolVjt62N58UFPdydVK%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;454&quot; height=&quot;562&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.36.39.png&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사진을 보면 FK로 탐색한 시간이 1.35sec로, PK에 비해 실행 시간이 오래 걸리는 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;edited_스크린샷 2024-10-16 오후 2.54.10.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uymBq/btsKtoJOXNe/FAQDDF9lIPKk85GkDnMtR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uymBq/btsKtoJOXNe/FAQDDF9lIPKk85GkDnMtR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uymBq/btsKtoJOXNe/FAQDDF9lIPKk85GkDnMtR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuymBq%2FbtsKtoJOXNe%2FFAQDDF9lIPKk85GkDnMtR1%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;827&quot; height=&quot;99&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 2.54.10.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;type 필드가 ALL로 풀스캔을  하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 FK의 탐색 시간을 빠르게 하기 위해서 어떻게 할 수 있을까? 인덱스를 생성해 스캔을 해보면 가능하다!&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 인덱스를 생성해서  조회 속도 향상해보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 인덱스&lt;/h3&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래의 명령어를 통해 단일 인덱스를 생성해서 스캔할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1730552244037&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX 인덱스명 ON 테이블명 (컬럼명)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 2.55.25.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QnkG5/btsKvdGwBB7/8oBIbkvGknECik8ISuwJw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QnkG5/btsKvdGwBB7/8oBIbkvGknECik8ISuwJw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QnkG5/btsKvdGwBB7/8oBIbkvGknECik8ISuwJw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQnkG5%2FbtsKvdGwBB7%2F8oBIbkvGknECik8ISuwJw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;101&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 2.55.25.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;taerae라는 인덱스 명으로 인덱스를 생성한 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;스크린샷 2024-11-02 오후 11.37.31.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tbsZ5/btsKtpB2QT5/kfIkNMOKjqG2S4DEjhtTH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tbsZ5/btsKtpB2QT5/kfIkNMOKjqG2S4DEjhtTH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tbsZ5/btsKtpB2QT5/kfIkNMOKjqG2S4DEjhtTH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtbsZ5%2FbtsKtpB2QT5%2FkfIkNMOKjqG2S4DEjhtTH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;101&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.37.31.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 다시 실행 계획을 보면 key가 지정한 걸로 변경된 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;edited_스크린샷 2024-10-16 오후 2.56.24.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgZoGF/btsKuGPU31w/ra41qUuNCjcFARvUfFtFD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgZoGF/btsKuGPU31w/ra41qUuNCjcFARvUfFtFD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgZoGF/btsKuGPU31w/ra41qUuNCjcFARvUfFtFD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgZoGF%2FbtsKuGPU31w%2Fra41qUuNCjcFARvUfFtFD0%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;535&quot; height=&quot;317&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 2.56.24.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1.35sec에서 0.00sec로 실행 속도가 매우 빨라진 것을 알 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;edited_스크린샷 2024-10-16 오후 2.57.15.png&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciToro/btsKtqHG2SG/YKgMajSysHgABR4djn3uxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciToro/btsKtqHG2SG/YKgMajSysHgABR4djn3uxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciToro/btsKtqHG2SG/YKgMajSysHgABR4djn3uxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciToro%2FbtsKtqHG2SG%2FYKgMajSysHgABR4djn3uxK%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;776&quot; height=&quot;305&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 2.57.15.png&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;sum(price)와 avg(price)로 조회하면 매우 빠른 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 인덱스에 포함된 price 정보만으로 연산이 가능하기 때문이다. 불필요한 데이터 파일 접근을 줄이면서 쿼리를 처리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;인덱스 처리를 했는데 FullScan을 하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 쿼리가 인덱스만으로 빠르게 처리되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.38.28.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NFjCY/btsKucBNrdy/m0wGKILuGfMDor5WOGK7zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NFjCY/btsKucBNrdy/m0wGKILuGfMDor5WOGK7zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NFjCY/btsKucBNrdy/m0wGKILuGfMDor5WOGK7zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNFjCY%2FbtsKucBNrdy%2Fm0wGKILuGfMDor5WOGK7zK%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;802&quot; height=&quot;275&quot; data-filename=&quot;스크린샷 2024-11-02 오후 11.38.28.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 보면 price를 했는데도 index가 아닌 풀스캔을 할 것을 볼 수 있다. (type 필드가 ALL)&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;edited_스크린샷 2024-10-16 오후 3.04.05.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1Vq1p/btsKuHH2XAr/yBV2JBwjqSeb8WsZ8lB9dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1Vq1p/btsKuHH2XAr/yBV2JBwjqSeb8WsZ8lB9dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1Vq1p/btsKuHH2XAr/yBV2JBwjqSeb8WsZ8lB9dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1Vq1p%2FbtsKuHH2XAr%2FyBV2JBwjqSeb8WsZ8lB9dk%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;802&quot; height=&quot;278&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.04.05.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서도 400의 범위를 넘어가니 풀스캔을 해버리는 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는&amp;nbsp;많은&amp;nbsp;데이터가&amp;nbsp;인덱스&amp;nbsp;범위를&amp;nbsp;벗어나기&amp;nbsp;때문이다. 데이터가 많은 상황에서는 세컨더리 인덱스가 전체 테이블을 Full Scan과 유사하게 수행해, 테이블의 개수에 비례하여 여러 번의 I/O를 발생시킨다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아주 쉬운 예를 생각해보자. 예를 들어, 전체 데이터의 80%를 조회하는 경우, 인덱스를 사용해 각 항목을 개별적으로 찾는 것보다 그냥 Full Scan으로 한 번에 읽는 게 더 효율적일 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; 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-filename=&quot;스크린샷 2024-10-16 오후 9.26.20.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o0l7e/btsJ8wUSEZU/RHYJpAqxKj4D7v9vm9vIsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o0l7e/btsJ8wUSEZU/RHYJpAqxKj4D7v9vm9vIsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o0l7e/btsJ8wUSEZU/RHYJpAqxKj4D7v9vm9vIsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0l7e%2FbtsJ8wUSEZU%2FRHYJpAqxKj4D7v9vm9vIsK%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;519&quot; height=&quot;295&quot; data-filename=&quot;스크린샷 2024-10-16 오후 9.26.20.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 세컨더리 인덱스의 작동 방식을 생각해볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;세컨더리 인덱스를 사용할 때, 인덱스의 정보를 통해 필요한 데이터의 PK를 찾고, 그 PK로 다시 테이블의 본 데이터를 찾아와야한다. 하지만 이렇게 많은 데이터를 조회하면 PK를 찾고 또 본 데이터를 읽어오는 과정이 너무 많아지면서 Full Scan처럼 느려질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;이런 경우, 인덱스 조회 대신 Full Scan이 더 효율적일 수 있다. 일반적으로 조회 대상 데이터가 전체의 15% 이상이라면 Full Scan이 오히려 더 빠른 선택이 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세컨더리 인덱스와 PK 인덱스 차이:&lt;/b&gt;&amp;nbsp;세컨더리 인덱스는 추가 비용이 발생할 수 있는 반면, PK는 기본적으로 클러스터드 인덱스로 정렬되어 있어 거의 항상 더 빠른 조회를 보장한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;커버링 인덱스의 디스크 I/O 발생:&lt;/b&gt;&amp;nbsp;일반적으로 커버링 인덱스는 테이블의 물리적 데이터 접근을 줄여주지만, 특정 상황에서는 디스크 I/O가 생길 수 있다.&lt;br /&gt;&amp;nbsp;보통 자주 쓰는 인덱스는 메모리에 저장되므로, 디스크 접근 없이 메모리에서 빠르게 처리할 수 있 다. 하지만 인덱스가 커지면 모든 인덱스를 메모리에 올릴 수 없게 돼서 디스크에 저장된 인덱스를 참조하게 되고, 이때 디스크 I/O가 발생할 수 있다.&lt;br /&gt;&amp;nbsp;커버링 인덱스가 테이블 데이터의 많은 부분을 포함하게 되면, 커버링 인덱스조차 메모리에 모두 올라가지 못할 수 있고, 이때 인덱스 데이터를 디스크에서 읽어 와야하기 때문에 디스크 I/O가 발생하면서 커버링 인덱스가 느려질 수 있다. &lt;br /&gt;&amp;nbsp;즉, 커버링 인덱스라고 해도 인덱스 크기가 너무 커지면 메모리 한계를 넘게 되고, 디스크에 접근하면서 속도가 느려질 수 있는 것!&lt;/blockquote&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;복합 인덱스&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.27.38.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mzn6I/btsKtRknT2u/uRGXRQgngeo0DkgFuoFYn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mzn6I/btsKtRknT2u/uRGXRQgngeo0DkgFuoFYn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mzn6I/btsKtRknT2u/uRGXRQgngeo0DkgFuoFYn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMzn6I%2FbtsKtRknT2u%2FuRGXRQgngeo0DkgFuoFYn0%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;834&quot; height=&quot;290&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.27.38.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;nickname이 유니크 키라서 인덱싱 되어 있는 상황이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 nickname과 money로도 인덱스처리하려면, 아래와 같은 명령어로 복합키를 만들어줄 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1730554658666&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX 인덱스명 ON 테이블명 (컬럼명1, 컬럼명2...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.29.12.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mpx1y/btsKvc1VKN2/4Ag5jrWJBODE8zKafCmRyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mpx1y/btsKvc1VKN2/4Ag5jrWJBODE8zKafCmRyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mpx1y/btsKvc1VKN2/4Ag5jrWJBODE8zKafCmRyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmpx1y%2FbtsKvc1VKN2%2F4Ag5jrWJBODE8zKafCmRyk%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;809&quot; height=&quot;224&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.29.12.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.29.44.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9tcIC/btsKvaJNSyk/2IkkHKiI0czx2fKdYLzxI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9tcIC/btsKvaJNSyk/2IkkHKiI0czx2fKdYLzxI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9tcIC/btsKvaJNSyk/2IkkHKiI0czx2fKdYLzxI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9tcIC%2FbtsKvaJNSyk%2F2IkkHKiI0czx2fKdYLzxI1%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;804&quot; height=&quot;171&quot; data-filename=&quot;edited_스크린샷 2024-10-16 오후 3.29.44.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;복합 키를 통해 만들어진 필드가&amp;nbsp;인덱스를 잘 타는 것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면&amp;nbsp;만약 nickname과 money를 바꿔서 복합키를&amp;nbsp; 설정하면 어떻게 될까?&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;순서가 다르기 때문에. 당연히 달라진다. 이때 고려해야할 것이 &lt;b&gt;카디널리티&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;카디널리티(Cardinality)와&amp;nbsp;인덱스&amp;nbsp;순서&lt;/h3&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;카디널리티는 해당 컬럼에 저장된 고유한 값의 수를 의미한다. 카디널리티가 높다는 것은 데이터가 더 다양하게 분포되어 있다는 뜻이고, 반대로 낮다면 데이터가 비슷하게 몰려 있다는 의미이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;인덱스 생성 시 카디널리티가 &lt;b&gt;높은 컬럼을 우선&lt;/b&gt;으로 두면, 인덱스를 검색할 때 검색 범위를 좁히는 데 효과적이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어, nickname이 더 고유한 값이 많고 money는 그보다 고유 값이 적다면, nickname, money 순으로 인덱스를 생성하는 게 더 효율적일 수 있다. 높은 카디널리티의 컬럼을 앞에 두면 검색 범위를 먼저 좁히기 때문에, 데이터 탐색 속도가 더 빨라지는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 일반적으로 카디널리티가 높은 컬럼을 먼저 두고, 그 다음으로 낮은 컬럼 순으로 인덱스를 생성하는 방식이 추천된다.&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; Explain plan을 시각적으로 보는법&lt;/h2&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 명령어를 통해 실행 계획을 살펴보았지만, Intellij와 MySQL에서 시각화로 보여주는 기능을 제공한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Intellij&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.39.37.png&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OQxuF/btsJ7yk8Wtb/1TRZffQSjiIBF5JWeEzE00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OQxuF/btsJ7yk8Wtb/1TRZffQSjiIBF5JWeEzE00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OQxuF/btsJ7yk8Wtb/1TRZffQSjiIBF5JWeEzE00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOQxuF%2FbtsJ7yk8Wtb%2F1TRZffQSjiIBF5JWeEzE00%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;488&quot; height=&quot;462&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.39.37.png&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이의 경우 오른쪽 마우스를 통해 Explain Plan 항목으로 들어가면 아래와 같은 화면을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.41.09.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IMN6E/btsJ8YW6Nto/TG4JMHI10pCWJKsaA1Sng1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IMN6E/btsJ8YW6Nto/TG4JMHI10pCWJKsaA1Sng1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IMN6E/btsJ8YW6Nto/TG4JMHI10pCWJKsaA1Sng1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIMN6E%2FbtsJ8YW6Nto%2FTG4JMHI10pCWJKsaA1Sng1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.41.09.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오전 12.49.59.png&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lb1P2/btsKTAQiZtk/r0Qf2P2M4aW2qPzQFMi9e0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lb1P2/btsKTAQiZtk/r0Qf2P2M4aW2qPzQFMi9e0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lb1P2/btsKTAQiZtk/r0Qf2P2M4aW2qPzQFMi9e0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLb1P2%2FbtsKTAQiZtk%2Fr0Qf2P2M4aW2qPzQFMi9e0%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;690&quot; height=&quot;398&quot; data-filename=&quot;스크린샷 2024-11-24 오전 12.49.59.png&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;mysql&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.42.46.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MDyXI/btsJ7MDJKmy/ilGKpl809crrKP1jppAAaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MDyXI/btsJ7MDJKmy/ilGKpl809crrKP1jppAAaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MDyXI/btsJ7MDJKmy/ilGKpl809crrKP1jppAAaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMDyXI%2FbtsJ7MDJKmy%2FilGKpl809crrKP1jppAAaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;211&quot; data-filename=&quot;스크린샷 2024-10-16 오후 3.42.46.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서도 Visual Explain을 클릭하면 유사한 화면을 볼 수 있다.&lt;/p&gt;</description>
      <category>Backend/MySQL</category>
      <author>동구름이</author>
      <guid isPermaLink="true">https://dcloud.tistory.com/369</guid>
      <comments>https://dcloud.tistory.com/369#entry369comment</comments>
      <pubDate>Wed, 16 Oct 2024 15:33:11 +0900</pubDate>
    </item>
  </channel>
</rss>