<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>성장하는 성하 Blog</title>
    <link>https://ksh-coding.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 21:23:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BE_성하</managingEditor>
    <image>
      <title>성장하는 성하 Blog</title>
      <url>https://tistory1.daumcdn.net/tistory/5287205/attach/a4bc1183563c42bba239da8c6635ce22</url>
      <link>https://ksh-coding.tistory.com</link>
    </image>
    <item>
      <title>[Debezium] Debezium PostgreSQL Connector 동작 방식 &amp;amp; 간단한 구현 (With. Transactional Outbox Pattern)</title>
      <link>https://ksh-coding.tistory.com/165</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이전 포스팅에서 Transactional Outbox Pattern에 대해서 다뤄보았었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/164&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ksh-coding.tistory.com/164&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749196826627&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Transactional Outbox Pattern을 통해 Event Message 발행 보장하기&quot; data-og-description=&quot;0. 들어가기 전취준생 시절에 간간히 컨퍼런스나 여러 블로그를 보면서 'Transactional Outbox Pattern'을 접했었습니다.Transactional Outbox Pattern을 사용하면 순차적인 메시지 발행을 보장할 수 있다.&amp;nbsp;처음 &quot; data-og-host=&quot;ksh-coding.tistory.com&quot; data-og-source-url=&quot;https://ksh-coding.tistory.com/164&quot; data-og-url=&quot;https://ksh-coding.tistory.com/164&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c52JH4/hyY1e6NXuk/hZ27Eg3o8j5ZYIii4TQ1s1/img.png?width=800&amp;amp;height=249&amp;amp;face=0_0_800_249,https://scrap.kakaocdn.net/dn/m4jKN/hyY5fC0fKj/qts1XT8QuRlcOlEJAL5IU1/img.png?width=800&amp;amp;height=249&amp;amp;face=0_0_800_249,https://scrap.kakaocdn.net/dn/clVlBJ/hyY4czP04g/3slbi5xkkpCI2C5qFbMKD0/img.png?width=2180&amp;amp;height=1444&amp;amp;face=0_0_2180_1444&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/164&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ksh-coding.tistory.com/164&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c52JH4/hyY1e6NXuk/hZ27Eg3o8j5ZYIii4TQ1s1/img.png?width=800&amp;amp;height=249&amp;amp;face=0_0_800_249,https://scrap.kakaocdn.net/dn/m4jKN/hyY5fC0fKj/qts1XT8QuRlcOlEJAL5IU1/img.png?width=800&amp;amp;height=249&amp;amp;face=0_0_800_249,https://scrap.kakaocdn.net/dn/clVlBJ/hyY4czP04g/3slbi5xkkpCI2C5qFbMKD0/img.png?width=2180&amp;amp;height=1444&amp;amp;face=0_0_2180_1444');&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;Transactional Outbox Pattern을 통해 Event Message 발행 보장하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;0. 들어가기 전취준생 시절에 간간히 컨퍼런스나 여러 블로그를 보면서 'Transactional Outbox Pattern'을 접했었습니다.Transactional Outbox Pattern을 사용하면 순차적인 메시지 발행을 보장할 수 있다.&amp;nbsp;처음&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ksh-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 포스팅에서 Transactional Outbox Pattern에 대한 이론을 살펴봤었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Transactional Outbox Pattern의 Flow를 도식화하면 다음과 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKVEAi/btsOrzmLvhN/5aBXN6sc6ceiivYUFO8sdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKVEAi/btsOrzmLvhN/5aBXN6sc6ceiivYUFO8sdK/img.png&quot; data-alt=&quot;Transactional Outbox Flow&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKVEAi/btsOrzmLvhN/5aBXN6sc6ceiivYUFO8sdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKVEAi%2FbtsOrzmLvhN%2F5aBXN6sc6ceiivYUFO8sdK%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;1280&quot; height=&quot;399&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Transactional Outbox Flow&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 실제로 구현해야하는 부분은 크게 2가지로 나눌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;비즈니스 로직에서 발생하는 이벤트 메타데이터를 Outbox 테이블에 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Outbox 테이블의 이벤트 메타데이터를 기반으로 Kafka Message Relay (Send)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 Outbox 테이블에 저장하는 부분은 단순 DB Insert이므로 생략하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번 포스팅에서는 Debezium을 통해 Outbox 테이블의 데이터를 Kafka Broker로 Relay하는 부분을 간략하게 구현해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium은 여러 DBMS별 Kafka Connector를 제공하는데 프로젝트에서는 PostgreSQL을 사용했기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium PostgreSQL Connector를 사용하여 구현해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. Debezium?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium에 대해서 공식문서에서는 다음과 같이 소개하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium is a set of distributed services to capture changes in your databases so that your applications can see those changes and respond to them.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;애플리케이션에서 DB의 데이터 변경을 알 수 있도록 DB의 변경을 캡쳐하는 분산 서비스&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설명을 덧붙이면, &lt;b&gt;Debezium은 잘 알려진 CDC(Change Data Capture) 라이브러리입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CDC란, &lt;b&gt;DB에서 발생한 변경 내역을 추출해서 다른 시스템에 전달하는 기술입니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium은 Apache Kafka 기반으로 구축되어 DBMS별로 Kafka Connector와 호환되는 Connector들을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;해당 Connector가 DB의 변경되는 데이터를 감지하고 기록, 해당 데이터를 Kafka Topic의 Event로 Streaming합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 Transactional Outbox Pattern에 적용하면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Debezium이 Outbox Table에 쌓이는 이벤트 데이터를 감지하여 해당 데이터를 Kafka Topic의 Event로 Streaming&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. Debezium(CDC)과 관련한 PostgreSQL 요소&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium이 DB의 데이터 변경을 감지하는 만큼 DBMS별로 Connector가 별도로 존재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 DBMS로 PostgreSQL을 이용했기 때문에 PostgreSQL Connector에 대해 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 Debezium PostgreSQL Connector의 동작 방식을 알아보기 전에,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium PostgreSQL Connector의 동작 방식을 이해하기 위해서는 다음과 같은 PostgreSQL의 주요 개념을 이해해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;WAL (Write-Ahead Log)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;PostgreSQL Publication&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Logical Decoding Output Plug-in&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Logical Repliaction Slot&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;WAL Sender&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 개념에 대해 간략하게 설명해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2-1. WAL (Write-Ahead Log)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;PostgreSQL의 모든 변경 사항은 WAL(Write-Ahead Log)에 쌓이게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;WAL은 본질적으로는 PostgreSQL 데이터의 무결성을 보장하기 위한 요소이지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Logical Decoding 프로세스에서 WAL은 PostgreSQL의 데이터 변경 사항을 담는 저장소 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉, Debezium Kafka Connector에서는 WAL에 쌓인 원장 데이터 변경 사항을 가져가는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2-2. PostgreSQL Publication&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;PostgreSQL의 Publication은 PostgreSQL에서 WAL의 데이터 중 외부에 내보낼 대상을 지정하는 PostgreSQL Object&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium에서 어떤 대상의 데이터 변경만 감지할 것인지를 필터링하는 역할을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 PostgreSQL의 모든 데이터 변경이 WAL에 쌓이지만 Debezium Kafka Connector는 Publication에서 지정한 데이터만을 읽어서 처리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;즉, Debezium 입장에서 Publication은 데이터 변경을 감지할 대상을 필터링하는 역할을 수행합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2-3. Logical Decoding Output Plug-in&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Logical Decoding Output Plug-in은 PostgreSQL의 플러그인으로, 데이터 변경을 추출하는 역할을 수행합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일반적으로 PostgreSQL 10 이상부터 내장되어 있는 'pgoutput'을 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 pgoutput은 PostgreSQL의 WAL 데이터 스트리밍을 위한 Logical Streaming Replication Protocol로 변환하는 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium은 pgoutput을 통해 PostgreSQL 내부 포맷이 아닌 약속된 프로토콜로 변경 데이터를 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2-4. Logical Replication Slot&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Debezium Kafka Connector를 연결하기 위해 가장 중요한 요소는 Logical Replication Slot입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Logical Replication Slot은 다음과 같은 역할을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium에서 PostgreSQL의 변경 데이터를 받아가기 위한 연결 Channel&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마지막에 읽은 WAL의 LSN (Log Sequence Number)를 저장하여 어디까지 읽었는지 저장&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉, &lt;b&gt;Debezium은 PostgreSQL의 Logical Replication Slot을 지정하여 Subscribe Channel을 지정하고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;해당 Logical Replication Slot에서 마지막으로 읽은 데이터 이후의 데이터를 스트리밍하여 처리하게 됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2-5. WAL Sender&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;WAL Sender는 Debezium에서 데이터 스트리밍 시 PostgreSQL에서 생성되는 내부 백그라운드 프로세스입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 WAL Sender가 PG 내부의 데이터 스트리밍의 요청/응답을 처리하여 최종적으로 Debezium에게 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. Debezium connector for PostgreSQL&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서서 PostgreSQL의 Debezium과 관련한 요소들을 간략하게 살펴봤었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;해당 이해를 기반으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Debezium Kafka Connector의 동작 Flow&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;를 그림으로 나타내면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 16.38.06.png&quot; data-origin-width=&quot;3170&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I278Y/btsOLFzHSDe/r5l4HvzItHdcBR6GAnGAgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I278Y/btsOLFzHSDe/r5l4HvzItHdcBR6GAnGAgK/img.png&quot; data-alt=&quot;Debezium PostgreSQL Connector 동작 Flow&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I278Y/btsOLFzHSDe/r5l4HvzItHdcBR6GAnGAgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI278Y%2FbtsOLFzHSDe%2Fr5l4HvzItHdcBR6GAnGAgK%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;3170&quot; height=&quot;1456&quot; data-filename=&quot;스크린샷 2025-06-21 16.38.06.png&quot; data-origin-width=&quot;3170&quot; data-origin-height=&quot;1456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Debezium PostgreSQL Connector 동작 Flow&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 동작 순서로 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Debezium -&amp;gt; PostgreSQL 연결 (Access)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. Debezium Kafka Application -&amp;gt; PostgreSQL 연결&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. PostgreSQL 내 데이터 스트리밍 백그라운드 프로세스인 WAL Sender 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Change Data Capture (CDC)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. Debezium에서 지정한 Logical Replication Slot의 마지막 WAL의 LSN 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 마지막 WAL LSN 이후의 변경 데이터를 WAL에서 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. 조회한 WAL 변경 데이터 중 Publication에서 지정한 데이터만 필터링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6. 변경 데이터를 pgoutput에서 Debezium에서 처리하기 위한 Logical Streaming Replication Protocol으로 변환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;7. 최종 변경 데이터 Debezium Kafka Connector Application에 전달&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Data Processing &amp;amp; Send&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;8. Debezium Kafka Application 내부에서 전달받은 데이터를 Kafka Message Format으로 가공 (커스텀 가능)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;9. 가공된 Kafka Message를 Kafka Broker로 Send&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Update Logical Replication Slot Last LSN&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;10. Kafka Message 전송이 정상적으로 완료되면 Debezium Kafka Application -&amp;gt; WAL Sender에 완료 메시지 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;11. 완료 메시지를 받으면 Logical Replication Slot에 마지막 LSN Update&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기까지 Debezium PostgreSQL Connector의 동작 원리까지 살펴봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;간략하게 정리해봤지만 해당 원리를 이해하는 데까지 정말 많은 시간이 걸렸네요...  &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3. Debezium 연동을 위한 PostgreSQL 설정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;이제 PostgreSQL &amp;amp; Debezium PostgreSQL Connector &amp;amp; Kafka Broker를 연동해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;먼저, PostgreSQL 설정부터 살펴보면 앞서 주요 개념과 동작 Flow에서 봤듯이 다음과 같은 설정이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;wal_level 변경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Publication 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Replication Slot 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3-1. PostgreSQL wal_level 변경&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;해당 PostgreSQL wal_level은 가장 중요한 Logical Replication Slot과 WAL을 연동하기 위한 설정입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 언급하지는 않았지만 Logical Repliaction을 사용하려면 wal_level을 logical로 설정해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PostgreSQL 10 이상부터는 기본 wal_level이 replica이므로 변경해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로컬 환경에서는 도커를 사용해서 다음과 같이 커맨드를 지정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일반적으로는 postgresql.conf 내의 wal_level을 수정해주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749219442463&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  pgsql:
    image: docker.io/library/postgres:15.5
    
    ...
    
    command: [
      'postgres','-c','wal_level=logical'
    ]
    
    ...&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;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3-2. PostgreSQL Publication 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Publication 생성은 간단하게 다음과 같이 쿼리 1줄이면 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;더 자세한 상세 옵션들은 아래 PostgreSQL Publication 공식문서를 보면 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/sql-createpublication.html&quot;&gt;https://www.postgresql.org/docs/current/sql-createpublication.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749215821841&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE PUBLICATION outbox FOR TABLE pay.outbox_pay
    WITH (
        publish = 'insert',
        publish_via_partition_root = true
    );&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CREATE PUBLICATION outbox : 'outbox'이라는 이름으로 PUBLICATION 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;FOR TABLE pay.outbox_pay&lt;/b&gt; : PUBLICATION 대상 테이블을 'pay.outbox_pay'으로 지정&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 부분이 앞서 동작 방식에서 살펴본 WAL을 필터링하는 부분입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Publication에 지정되지 않은 테이블은 변경 데이터(WAL)가 외부(Debezium)으로 스트리밍되지 않게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;publish : 변경 이벤트를 발행할 DML 연산을 지정할 수 있습니다. (insert, update, delete, truncate)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Outbox 테이블의 Insert만 CDC 처리하기 때문에 insert로 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;publish_via_partition_root&lt;/b&gt; : 파티셔닝 테이블이 존재할 때, 해당 파티셔닝 테이블 데이터의 변경을 최상위 테이블 데이터 변경으로 발행&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 프로젝트에서 일자별 파티셔닝 테이블이 존재 (pay.outbox_pay_2025_06_05, pay.outbox_pay_2025_06_06, ...)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이후 Debezium 설정 시 발행되는 테이블 명으로 커스텀할 때 상위 테이블로 커스텀되도록 발행을 상위 테이블로 하도록 true&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3-3. PostgreSQL Logical Replication Slot 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Logical Replication Slot 생성도 간단하게 다음과 같이 PostgreSQL 함수를 사용하면 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749216748411&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT pg_create_logical_replication_slot(slot_name:='pay_local_outbox', plugin:='pgoutput');&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;slot_name : 생성할 Logical Repliaction Slot 이름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;plugin : Slot에 적용할 plugin으로, pgoutput 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 Publication과 Replication Slot을 생성해주면 PostgreSQL 설정은 끝납니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4. Debezium PostgreSQL Connector 설정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제 본격적인 Debezium PostgreSQL Connector 설정을 해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 설정들은 아래 Debezium 공식문서의 PostgreSQL Connector 챕터에서 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749219029478&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;Debezium connector for PostgreSQL :: Debezium Documentation&quot; data-og-description=&quot;Tombstone events When a row is deleted, the delete event value still works with log compaction, because Kafka can remove all earlier messages that have that same key. However, for Kafka to remove all messages that have that same key, the message value must&quot; data-og-host=&quot;debezium.io&quot; data-og-source-url=&quot;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&quot; data-og-url=&quot;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://debezium.io/documentation/reference/stable/connectors/postgresql.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Debezium connector for PostgreSQL :: Debezium Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Tombstone events When a row is deleted, the delete event value still works with log compaction, because Kafka can remove all earlier messages that have that same key. However, for Kafka to remove all messages that have that same key, the message value must&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;debezium.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;예시 설정들은 아주 간단한 학습용 설정으로 실제 프로덕션 환경에 적용해서 사용하기에는 무리가 있을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;더 자세한 설정은 위의 문서를 참고하여 추가해나가시면 될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&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;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4-1. 설정 JSON 전체 내용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설정이 너무 방대하기 때문에 설정 JSON 파일 전체를 먼저 올리고 이후에 부분적으로 설명하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;pay-outbox-source.json&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1750487337178&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; {
  &quot;name&quot;: &quot;outbox-source&quot;,
  &quot;config&quot;: {
    // 1. Connector Class 지정
    &quot;connector.class&quot;: &quot;io.debezium.connector.postgresql.PostgresConnector&quot;,

	// 2. PostgreSQL DB 정보 지정
    &quot;database.hostname&quot;: &quot;host.docker.internal&quot;,
    &quot;database.port&quot;: &quot;5432&quot;,
    &quot;database.user&quot;: &quot;postgres&quot;,
    &quot;database.password&quot;: &quot;seongha12!@&quot;,
    &quot;database.dbname&quot;: &quot;pay_local&quot;,

	// 3. PostgreSQL Logical Replication Slot 설정
    &quot;slot.name&quot;: &quot;pay_local_outbox&quot;,

	// 4. PostgreSQL Publication 설정
    &quot;publication.name&quot;: &quot;outbox&quot;,
    &quot;publication.autocreate.mode&quot;: &quot;disabled&quot;,
    
    // 5. Logical Decoding Plugin 지정
    &quot;plugin.name&quot;: &quot;pgoutput&quot;,

	// 6. 메시지 처리할 Table 필터링
    &quot;table.include.list&quot;: &quot;outbox.outbox_pay&quot;,

	// 7. Kafka Topic 설정
    &quot;topic.prefix&quot;: &quot;pay&quot;,
    &quot;topic.delimiter&quot;: &quot;.&quot;,
    &quot;topic.creation.enable&quot;: &quot;true&quot;,
    &quot;topic.creation.default.partitions&quot;: &quot;1&quot;,
    &quot;topic.creation.default.replication.factor&quot;: &quot;1&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. Connector Class 지정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750487727440&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;connector.class&quot;: &quot;io.debezium.connector.postgresql.PostgresConnector&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium Connector로 사용할 커넥터 Class를 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PostgreSQL Connector는 위와 같은 Class로 지정하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. PostgreSQL DB 정보 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750487761490&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;database.hostname&quot;: &quot;host.docker.internal&quot;,
    &quot;database.port&quot;: &quot;5432&quot;,
    &quot;database.user&quot;: &quot;postgres&quot;,
    &quot;database.password&quot;: &quot;seongha12!@&quot;,
    &quot;database.dbname&quot;: &quot;pay_local&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;변경되는 데이터를 읽을 DB의 정보를 지정하는 부분입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트 DB 정보를 입력해주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로, 저는 로컬 환경에서 Kafka Connect (Debezium)과 PostgreSQL을 Docker Container로 구성했기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서로 다른 Docker Container의 접근을 위해 hostname으로 `host.docker.internal`를 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. PostgreSQL Logical Replication Slot 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750488308332&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;slot.name&quot;: &quot;pay_local_outbox&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 생성한 Logical Repliaction Slot의 이름을 지정해주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. PostgreSQL Publication 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750488611856&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;publication.name&quot;: &quot;outbox&quot;,
&quot;publication.autocreate.mode&quot;: &quot;disabled&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 생성한 Publication 이름을 지정해주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 `publication.autocreate.mode`는 Publication을 자동 생성하는 모드를 결정하는 옵션입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;all_tables(default) : Publication이 있으면 해당 Publication 적용, 없다면 모든 테이블 대상으로 Publication 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;filtered : 커넥터 설정에 설정한 테이블(table.include.list, schema.include.list, ...)에 따라서 Publication 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;no_tables : Publication 생성 시 테이블을 지정하지 않고 생성 (모든 변경 데이터 패스)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;disabled : 해당 자동 생성 옵션 삭제, PG에서 직접 생성해야함.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위와 같은 여러 자동 생성 모드가 존재하지만, 저는 Publication은 PostgreSQL의 Object이기 때문에&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;생성 역할이 Debezium Connector로 넘어가는 것은 좋지 않다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 disabled를 사용하고 직접 위에서 생성한 것처럼 PostgreSQL 쿼리로 생성하는 방식을 택했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. PostgreSQL Logical Decoding Plug-in 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750489026952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;plugin.name&quot;: &quot;pgoutput&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PostgreSQL의 Logical Decoding Plug In을 지정하는 옵션입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;PostgreSQL 10+ 버전에서는 기본이 pgoutput이므로 pgoutput으로 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6. 메시지를 처리할 Table 필터링&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750489301404&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;table.include.list&quot;: &quot;outbox.outbox_pay&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 Publication에서 WAL을 필터링하여 다른 테이블의 변경 이벤트가 스트리밍 되지는 않지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;커넥터 설정에서도 필터링하는 테이블을 명확히 하기 위해 명시적으로 선언했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;7. Kafka Topic 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750489654210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;topic.prefix&quot;: &quot;pay&quot;,
    &quot;topic.delimiter&quot;: &quot;.&quot;,
    &quot;topic.creation.enable&quot;: &quot;true&quot;,
    &quot;topic.creation.default.partitions&quot;: &quot;1&quot;,
    &quot;topic.creation.default.replication.factor&quot;: &quot;1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kafka Broker에 Send할 Message의 Topic을 지정하는 부분입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Debezium Connector가 자동으로 생성하는 Kafka Topic은 다음과 같은 기준으로 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;topic.prefix + topic.delimiter + Schema Name + topic.delimiter + Table Name&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;topic.delimiter의 default : '.'&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;topic.prefix는 default X&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서, `pay.outbox_pay`의 경우에는 다음과 같이 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;pay.pay.outbox_pay&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4. Debezium PostgreSQL Connector 생성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞서 Connector 설정이 완료되었다면, 이제 해당 설정을 기반으로 Connector를 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kafka Connector는 Connector를 관리하는 REST API를 제공하여 REST API 호출으로 간단하게 커넥터를 관리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;관련 REST API는 다음과 같은 Confluent의 Kafka Connector 부분 공식문서에서 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750490181202&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;Kafka Connect REST Interface for Confluent Platform | Confluent Documentation&quot; data-og-description=&quot;Since Kafka Connect is intended to be run as a service, it also supports a REST API for managing connectors. By default, this service runs on port 8083. When executed in distributed mode, the REST API is the primary interface to the cluster. You can make r&quot; data-og-host=&quot;docs.confluent.io&quot; data-og-source-url=&quot;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&quot; data-og-url=&quot;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.confluent.io/platform/current/connect/references/restapi.html#connectors&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Kafka Connect REST Interface for Confluent Platform | Confluent Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Since Kafka Connect is intended to be run as a service, it also supports a REST API for managing connectors. By default, this service runs on port 8083. When executed in distributed mode, the REST API is the primary interface to the cluster. You can make r&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.confluent.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;REST API 중 Connector를 생성하는 API인 `POST /connectors`를 호출하여 생성하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 IntelliJ의 http 파일을 사용하여 해당 폴더에 JSON을 위치시키고 다음과 같이 실행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750490701289&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;### 샘플 커넥터 생성 (Kafka Connector -&amp;gt; localhost:8083)
POST localhost:8083/connectors
Content-Type: application/json

&amp;lt; ./pay-outbox-source.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 호출을 하게 되면 201 응답과 함께 커넥터가 생성된 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 Kafka UI를 Docker Container로 실행했기 때문에 다음과 같이 UI로도 확인할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 16.26.30.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebEN69/btsOMRlDgAx/KVxHd04YKApy2fTDqzuIpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebEN69/btsOMRlDgAx/KVxHd04YKApy2fTDqzuIpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebEN69/btsOMRlDgAx/KVxHd04YKApy2fTDqzuIpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebEN69%2FbtsOMRlDgAx%2FKVxHd04YKApy2fTDqzuIpk%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;3432&quot; height=&quot;686&quot; data-filename=&quot;스크린샷 2025-06-21 16.26.30.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;5. CDC 동작 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;이제 커넥터도 생성되었으므로 실제 Outbox 테이블에 쌓이는 변경 데이터를 Connector가 읽어가는지 확인해봅시다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;확인할 사항은 다음과 같습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지정한 토픽(자동 생성 토픽)이 생성되었는지, 해당 토픽에 Outbox 테이블에 해당하는 메시지가 쌓이는지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 위해 임의로 Outbox 테이블에 데이터를 다음과 같이 2건 Insert 하여 이벤트 발행 상황을 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750491073759&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into outbox.outbox
    (topic, event_id, created_at, event_type, aggregate_type, aggregate_id, metadata, payload)

values
    ('job', 'b77e5423-9ac0-43db-afb4-17c4b4675fb4', '2025-06-21 14:32:48.013000 +00:00', ...),
    ('job', 'bdf89548-cf3b-4e42-8092-caec5883f148', '2025-06-21 14:35:41.704000 +00:00', ...);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kafka UI에서 확인한 결과, 아래와 같이 메시지가 정상적으로 지정한 토픽에 쌓이는 것을 확인할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 16.31.42.png&quot; data-origin-width=&quot;3448&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKEDRU/btsOL5rlsPg/Z0VmkAcBQuBEPDykbjqz6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKEDRU/btsOL5rlsPg/Z0VmkAcBQuBEPDykbjqz6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKEDRU/btsOL5rlsPg/Z0VmkAcBQuBEPDykbjqz6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKEDRU%2FbtsOL5rlsPg%2FZ0VmkAcBQuBEPDykbjqz6k%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;3448&quot; height=&quot;784&quot; data-filename=&quot;스크린샷 2025-06-21 16.31.42.png&quot; data-origin-width=&quot;3448&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기까지 해서 Debezium PostgreSQL Connector의 동작 방식과 간단한 구현까지 알아보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 해당 구현만으로는 프로덕션에 Debezium Connector를 사용하기에는 무리가 있을 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로덕션에 사용하려면 쌓이는 메시지들을 프로젝트에 맞게 커스텀 Transformation을 하고 여러 추가 설정들이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 설정들까지 다루기에는 너무 글이 방대해져서 동작 방식과 간단한 구현으로 글을 마무리하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나머지 추가 설정들은 위에서 언급한 공식 문서들을 살펴보면 모두 나와 있으니 커스텀하여 사용하면 될 듯 합니다!  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>CDC</category>
      <category>Debezium</category>
      <category>kafkaconnector</category>
      <category>PostgreSQL</category>
      <category>publication</category>
      <category>replicationslot</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/165</guid>
      <comments>https://ksh-coding.tistory.com/165#entry165comment</comments>
      <pubDate>Sat, 21 Jun 2025 16:38:28 +0900</pubDate>
    </item>
    <item>
      <title>Transactional Outbox Pattern을 통해 Event Message 발행 보장하기</title>
      <link>https://ksh-coding.tistory.com/164</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취준생 시절에 간간히 컨퍼런스나 여러 블로그를 보면서 'Transactional Outbox Pattern'을 접했었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Transactional Outbox Pattern을 사용하면 순차적인 메시지 발행을 보장할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Transactional Outbox Pattern을 공부할 당시에는 메시지를 발행해보지도 않았고 크게 와닿지 않아서 넘어갔었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번에 서비스에 이벤트를 발행하는 Task를 맡아서 수행하면서 관련 이슈를 해결해야 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Transactional Outbox Pattern을 적용하고 해결하여 관련 글을 작성하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(기본적인 Event Driven Architecture에 대한 개념은 생략하도록 하겠습니다!)&lt;/p&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Transactional Outbox Pattern이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transactional Outbox Event란 무엇일까요? 관련 내용은 아래 문서에 간략하게 설명되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 문서에 소개된 내용을 좀 더 자세하게 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://microservices.io/patterns/data/transactional-outbox.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740814325198&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;Microservices Pattern: Pattern: Transactional outbox&quot; data-og-description=&quot;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&quot; data-og-host=&quot;microservices.io&quot; data-og-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-url=&quot;http://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nBEBp/hyYmZUWijz/rwKP3KSwHofTOcoDHGQekk/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/zpRU4/hyYmSIkfPJ/L9aymEwecjXiksokJU1oGk/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/bAtTSc/hyYjL49A0p/D1cEWfcVFg62k02NqAKxXK/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nBEBp/hyYmZUWijz/rwKP3KSwHofTOcoDHGQekk/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/zpRU4/hyYmSIkfPJ/L9aymEwecjXiksokJU1oGk/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/bAtTSc/hyYjL49A0p/D1cEWfcVFg62k02NqAKxXK/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445');&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;Microservices Pattern: Pattern: Transactional outbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;microservices.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. Transactional Outbox Pattern의 등장 배경 (Context)&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A service command typically needs to create/update/delete aggregates in the database&amp;nbsp;and send messages/events to a message broker. &lt;br /&gt;For example, a service that participates in a&amp;nbsp;saga&amp;nbsp;needs to update business entities and send messages/events. &lt;br /&gt;Similarly, a service that publishes a domain event must update an&amp;nbsp;aggregate&amp;nbsp;and publish an event&lt;b&gt;.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에서는 간략하게 다음과 같은 상황을 가정하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 서비스에서는 DB에 CUD하는 작업과 Message Broker로 Message/Event를 Send하는 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;Example)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Saga 패턴(분산 트랜잭션을 다루는 패턴, 각 분산 환경별로 작업 내용을 Event로 전달)에서 Entity Update와 Message/Event를 Send한다.&lt;/li&gt;
&lt;li&gt;도메인 Event Publish 시 Aggregate를 Update하고 Event를 Publish 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황은 Message System(Event)를 사용하는 아키텍쳐에서 아주 흔한 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 이벤트 Publish와 관련해서 이해하기 쉬운 예시를 들어보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요구 사항 : 결제 시 결제 알림을 전송한다.&lt;/li&gt;
&lt;li&gt;'결제'와 '알림'을 Message System을 통해 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결제 시 '결제 상태를 완료로 변경하는 DB Update' &amp;amp; '결제 완료 Event' Publish&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 해당 문서에서는 이러한 상황에 대해 다음과 같이 언급합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The command &lt;b&gt;must atomically update the database and send messages in order to avoid data inconsistencies and bugs.&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'반드시' 데이터 불일치와 버그를 피하기 위해 &lt;b&gt;DB Update와 Message Send를 '원자적으로' 수행해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  문제 상황 (일반적인 메시징 시스템 구성)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, 일반적인 메시징 시스템을 구성하면 위와 같이 DB Update와 Message Send의 원자성을 보장할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면, 아래 그림과 같이 Message Send 작업은 DB 작업이 아니기 때문에 DB 트랜잭션으로 묶을 수 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-01 16.57.05.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGToHu/btsMA7xfxfE/74s4rf6ANKFx3Cb43ZXYp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGToHu/btsMA7xfxfE/74s4rf6ANKFx3Cb43ZXYp0/img.png&quot; data-alt=&quot;not atomically command&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGToHu/btsMA7xfxfE/74s4rf6ANKFx3Cb43ZXYp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGToHu%2FbtsMA7xfxfE%2F74s4rf6ANKFx3Cb43ZXYp0%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;2180&quot; height=&quot;1444&quot; data-filename=&quot;스크린샷 2025-03-01 16.57.05.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1444&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;not atomically command&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 다음과 같은 문제점이 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 업데이트는 정상적으로 수행됐으나, Message 플랫폼의 이슈로 메시지 발행이 실패했을 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지가 유실되어 데이터 정합성을 지킬 수 없다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 Publish 처리 중 예외는 Dead-Letter Queue와 같은 방식으로 재발행을 할 수도 있겠지만, 커넥션이 끊어지는 등 예상치 못한 오류 시에는 메시지가 유실된다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 환경일 때 여러 대의 요청 중 일부 요청의 메시지만 발행이 실패했을 때 데이터 정합성이 깨진다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB의 데이터를 롤백하기도 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시로 설명하면, 결제 상태가 완료로 변경되었지만 완료 이벤트가 발송되지 않아 알림이 생성되지 않을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예시가 알림 도메인이라 비즈니스 영향이 크진 않을 수 있지만, 중요한 도메인 간의 메시지 Pub/Sub이라면 상당히 큰 오류일 것입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제 상황을 해결하기 위해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB Update와 Message Send를 원자적으로 수행하는 Transactional Outbox Pattern&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. Transactional Outbox Pattern&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Transactional Outbox Pattern은 어떻게 DB Update와 Message Send를 원자적으로 수행할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 가장 중요한 핵심은&lt;b&gt; 'Outbox' 테이블을 별도로 생성한다는 점&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에 'Outbox' 테이블 생성&lt;/li&gt;
&lt;li&gt;해당 Outbox 테이블에 발행할 이벤트 정보 row로 저장&lt;/li&gt;
&lt;li&gt;DB Update와 Outbox Insert를 1개의 DB 트랜잭션으로 묶어서 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 DB Update와 메시지 발행 정보를 1개의 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;&lt;b&gt;Transactional Outbox Pattern을 도입한 메시지 발행 Flow&lt;/b&gt;는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Application에서 DB Update와 메시지 발행 Outbox Table Insert 1개의 트랜잭션으로 수행&lt;/li&gt;
&lt;li&gt;Outbox 테이블에 쌓이는 메시지 row를 Read&lt;/li&gt;
&lt;li&gt;읽은 메시지를 Message Broker에 Publish&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 도식화하면 아래 그림과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpATNk/btsMz758Oae/mpUSq6cjJFoJZqVz3TQke1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpATNk/btsMz758Oae/mpUSq6cjJFoJZqVz3TQke1/img.png&quot; data-alt=&quot;Transactional Outbox Pattern (출처 : https://microservices.io/patterns/data/transactional-outbox.html)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpATNk/btsMz758Oae/mpUSq6cjJFoJZqVz3TQke1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpATNk%2FbtsMz758Oae%2FmpUSq6cjJFoJZqVz3TQke1%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;1298&quot; height=&quot;461&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Transactional Outbox Pattern (출처 : https://microservices.io/patterns/data/transactional-outbox.html)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Transactional Outbox Pattern을 사용하면 다음과 같은 이점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Outbox Table 자체가 Dead-Letter를 저장하는 Table이 되어 별도의 Dead-Letter Queue를 사용할 필요가 없다.&lt;/li&gt;
&lt;li&gt;DB Update와 메시지 발행(Outbox Insert)이 하나의 트랜잭션으로 묶이기 때문에 데이터 정합성 문제가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Transactional Outbox Pattern 고려할 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 간단하게 Transactional Outbox Pattern을 살펴봤을 때 간단하게 보일 수 있지만, 몇 가지 고려해야 할 점이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 고려할 점들을 간단하게 살펴보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Read한 Outbox Table Message(Row) 처리 방안&lt;/li&gt;
&lt;li&gt;Outbox Table의 Message(Row)를 Read하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. Read한 Outbox Table Message(Row) 처리 방안&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transactional Outbox Pattern Flow에서 Outbox Table에 쌓이는 메시지를 Read하여 발행하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 메시지 발행 후 쌓이는 메시지들을 어떻게 처리할지 다음과 같이 크게 2가지 방법이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쌓이는 메시지를 History로 남겨놓기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 소싱이나 이벤트를 추적하기 위한 목적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행 후 메시지를 지우기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 이벤트 추적이 필요 없을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우에는 발행한 이벤트의 기록을 남겨놓을 필요가 없었기 때문에 메시지를 지우는 방식을 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;b&gt;실시간으로 Read한 데이터를 지우는 것은 위험할 수 있다고 생각합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결국 DB Delete(Outbox Message Delete)와 Message Send가 동시에 이루어져야 함&lt;/li&gt;
&lt;li&gt;이는 결국, 또 다시 두 작업의 원자성을 보장하지 못할 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB Delete는 되었지만 Message Send가 되지 않았다면 똑같이 메시지 유실이 될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 저는 &lt;b&gt;하루 정도의 간격을 두고 쌓인 메시지를 DB에서 삭제하는 낙관적인 방법을 선택하여 메시지를 제거&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. Outbox Table의 Message(Row)를 Read하는 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox Table에 쌓인 메시지를 Read하는 방식도 크게 다음과 같이 2가지가 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Polling Publisher&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transaction Log Tailing&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Polling Publisher&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling Publisher 방식은 간단하게 Outbox Table를 주기적으로 Polling하여 메시지를 Read하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling할 주기를 설정하여 Outbox Table의 발행되지 않은 메시지를 읽어서 발행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Polling 시 발행되지 않은 메시지를 필터링하기 위해 메시지 발행 시 발행한 메시지 row를 삭제하거나 flag를 업데이트 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식은 &lt;b&gt;구현이 간단하지만 주기적으로 DB에 부하를 줄 수 있다는 점 때문에 사용하지 않게 되었습니다.&lt;/b&gt;&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Transaction Log Tailing&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transaction Log Tailing은 Polling Publisher보다는 러닝 커브가 있고 복잡한 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식은 Application 단에서 DB를 조회한 결과로 메시지를 Read하는 것이 아닌,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB단에서 생성되는 'Transaction Log'를 추적하고 데이터 변경을 감지하여 Read하는 CDC(Change Data Capture) 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blYH0o/btsMyBmMYz5/nVNGAlmGnZ8sFH4WsQnGW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blYH0o/btsMyBmMYz5/nVNGAlmGnZ8sFH4WsQnGW0/img.png&quot; data-alt=&quot;Transaction Log Tailing (출처 : https://microservices.io/patterns/data/transaction-log-tailing.html)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blYH0o/btsMyBmMYz5/nVNGAlmGnZ8sFH4WsQnGW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblYH0o%2FbtsMyBmMYz5%2FnVNGAlmGnZ8sFH4WsQnGW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;580&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Transaction Log Tailing (출처 : https://microservices.io/patterns/data/transaction-log-tailing.html)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 CDC를 구현하기가 힘들기 때문에 CDC를 구현한 Debezium과 같은 오픈 소스를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 과정에서 CDC의 개념과 Debezium 등 여러 관련 개념을 학습해야 하기 때문에 러닝 커브가 있는 편입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 CDC를 한번 구성하고 나면 관리 및 모니터링이 쉽고 DB 단에서 이루어지기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;직접 무거운 SQL 쿼리를 주기적으로 발생시키는 Polling 방식 보다는 DB 부하가 적을 것이라고 기대했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 사내에서 이미 CDC의 환경 구성이 되어 있었기 때문에 리소스가 적어서 Transaction Log Tailing 방식을 사용하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용한 CDC(Debezium)에 대해서는 이정도로만 간략히 소개하고, 시간이 나면 자세히 포스팅으로 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Transactional Outbox Pattern 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Transactional Outbox Pattern을 구현한 내용을 마지막으로 글을 마무리하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했듯이 저는 다음과 같이 Transactional Outbox Pattern을 구현했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 서비스 DB에 'Outbox' Table 구성&lt;/li&gt;
&lt;li&gt;Transaction Log Tailing을 위해 CDC 오픈소스인 Debezium 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 구현이라고 거창하게 말했지만, 어떻게 Outbox 테이블을 구성했는지에 대해서만 언급하게 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium을 적용한 설정들은 다루기에는 너무 딥하고 많기 때문에 시간이 나면 따로 포스팅으로 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. Outbox Table Schema&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox Table에는 Debezium(Message Relay)이 읽고 발행할 메시지의 정보를 담아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Event의 Spec과도 관련이 있는데, 공통된 Spec을 사용하기 위해 &lt;b&gt;'Cloud Events'라는 Event Spec&lt;/b&gt;을 기반으로 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cloudevents.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cloudevents.io/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740982088047&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;CloudEvents&quot; data-og-description=&quot; &quot; data-og-host=&quot;cloudevents.io&quot; data-og-source-url=&quot;https://cloudevents.io/&quot; data-og-url=&quot;https://cloudevents.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cd6Qmn/hyYncfWtnd/PQDfKdsJygfcp7ImukUSF1/img.png?width=1181&amp;amp;height=1196&amp;amp;face=0_0_1181_1196,https://scrap.kakaocdn.net/dn/zxUcp/hyYmYhH1ee/fdoqz9z0ErpjNkIJYTPJsk/img.png?width=501&amp;amp;height=501&amp;amp;face=0_0_501_501,https://scrap.kakaocdn.net/dn/jg7gO/hyYmIzbPQf/tRweBEzqCG0XPG3xL6pejk/img.jpg?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280&quot;&gt;&lt;a href=&quot;https://cloudevents.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloudevents.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cd6Qmn/hyYncfWtnd/PQDfKdsJygfcp7ImukUSF1/img.png?width=1181&amp;amp;height=1196&amp;amp;face=0_0_1181_1196,https://scrap.kakaocdn.net/dn/zxUcp/hyYmYhH1ee/fdoqz9z0ErpjNkIJYTPJsk/img.png?width=501&amp;amp;height=501&amp;amp;face=0_0_501_501,https://scrap.kakaocdn.net/dn/jg7gO/hyYmIzbPQf/tRweBEzqCG0XPG3xL6pejk/img.jpg?width=280&amp;amp;height=280&amp;amp;face=0_0_280_280');&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;CloudEvents&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloudevents.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Message의 Format은 크게 다음과 같이 3가지가 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key : Kafka Message를 식별하기 위한 키&lt;/li&gt;
&lt;li&gt;Value (Payload) : Message Payload&lt;/li&gt;
&lt;li&gt;Header : Message의 메타 데이터를 담는 헤더&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740982117199&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE outbox
(
    event_id       uuid PRIMARY KEY,
    created_at     timestamptz  NOT NULL,
    event_type     varchar(126) NOT NULL,
    aggregate_type varchar(126) NOT NULL,
    aggregate_id   jsonb        NOT NULL,
    metadata       jsonb        NOT NULL DEFAULT '{}'::jsonb,
    payload        jsonb        NOT NULL DEFAULT '{}'::jsonb
);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;event_id : 발행할 이벤트를 식별하기 위한 UUID, PK&lt;/li&gt;
&lt;li&gt;created_at : 이벤트 발생 일시&lt;/li&gt;
&lt;li&gt;aggregate 관련 : 발생한 도메인 type과 id 저장, 어느 Aggregate인지, 어떤 리소스인지 식별하기 위해 사용&lt;/li&gt;
&lt;li&gt;metadata : Kafka Message의 헤더에 담을 추가 정보(메타데이터)&lt;/li&gt;
&lt;li&gt;payload : Kafka Message의 Payload(Value)에 담을 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Outbox 테이블의 정보를 기반으로 Debezium에서 Message를 생성하여 발행하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 구성한 Transactional Outbox Pattern의 구성도는 다음과 같습니다.&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-03-03 15.25.14.png&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n2LKv/btsMAYHtHc5/jknaKViX8Z1K2EzpTchfzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n2LKv/btsMAYHtHc5/jknaKViX8Z1K2EzpTchfzK/img.png&quot; data-alt=&quot;Transactional Outbox Pattern 구성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n2LKv/btsMAYHtHc5/jknaKViX8Z1K2EzpTchfzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn2LKv%2FbtsMAYHtHc5%2FjknaKViX8Z1K2EzpTchfzK%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;2242&quot; height=&quot;700&quot; data-filename=&quot;스크린샷 2025-03-03 15.25.14.png&quot; data-origin-width=&quot;2242&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Transactional Outbox Pattern 구성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transactional Outbox Pattern을 사용하여 다음과 같은 문제를 해결할 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DB Update와 Message Send를 원자적으로 수행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB Update 이후 Message가 발행되지 않아도 Outbox 테이블에서 재발행이 가능하므로 데이터 정합성 보장(최종적 일관성)&lt;/li&gt;
&lt;li&gt;분산 환경에서도 공통 메시지 Table인 Outbox Table로 메시지 순서를 보장할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 Transactional Outbox Pattern을 사용해서 위와 같이 간단한 이점을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다소 간단해보이지만 이벤트로 비즈니스를 처리하는 EDA 관점에서 아주 중요한 이점이라고 생각이 드는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구현 과정에서 추가로 공부해야 할 부분은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CloudEvents : Event Spec 구성 시 사용한 well-known Event Specification (공통 Event Spec 구성을 위해)&lt;/li&gt;
&lt;li&gt;Debezium : DB의 Transaction Log를 기반으로 변경된 데이터를 감지하는 CDC 라이브러리 (Kafka Connector 기반)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련해서는 시간이 된다면 다음에 포스팅해보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://microservices.io/patterns/data/transactional-outbox.html&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740983761694&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;Microservices Pattern: Pattern: Transactional outbox&quot; data-og-description=&quot;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&quot; data-og-host=&quot;microservices.io&quot; data-og-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-url=&quot;http://microservices.io/patterns/data/transactional-outbox.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2GKxR/hyYmXC7tzr/EvZTsHle2DBkfpKrvuxXB1/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/bxI4SI/hyYmMIoxvI/qkGdanZCUX5kJxZvkd1mlK/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/ct09eJ/hyYmWKZI6y/PSdfzTZCkNQyikAkDmtcm1/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://microservices.io/patterns/data/transactional-outbox.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2GKxR/hyYmXC7tzr/EvZTsHle2DBkfpKrvuxXB1/img.png?width=1298&amp;amp;height=461&amp;amp;face=0_0_1298_461,https://scrap.kakaocdn.net/dn/bxI4SI/hyYmMIoxvI/qkGdanZCUX5kJxZvkd1mlK/img.jpg?width=720&amp;amp;height=903&amp;amp;face=0_0_720_903,https://scrap.kakaocdn.net/dn/ct09eJ/hyYmWKZI6y/PSdfzTZCkNQyikAkDmtcm1/img.png?width=1377&amp;amp;height=445&amp;amp;face=0_0_1377_445');&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;Microservices Pattern: Pattern: Transactional outbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;microservices.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740983764920&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Transactional Outbox 패턴으로 메시지 발행 보장하기 - 리디주식회사&quot; data-og-description=&quot;Event-Driven Architecture에서 메시지 발행의 신뢰성을 보장하는 방법은 무엇일까요? 리디 서비스에 Transactional Outbox 패턴을 도입한 배경과 그 과정에서 얻은 배움을 공유합니다.&quot; data-og-host=&quot;ridicorp.com&quot; data-og-source-url=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot; data-og-url=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5d40R/hyYmJZb6IP/IGhUGRcKIcpcURTwYBMyK1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/crVCvh/hyYmQxfcYm/f7kSPi73TvnURLvxGdX79K/img.png?width=940&amp;amp;height=688&amp;amp;face=0_0_940_688,https://scrap.kakaocdn.net/dn/cxdRxX/hyYnekwxUZ/ir4iJSP9zdFuqie7ZZUBn0/img.jpg?width=940&amp;amp;height=529&amp;amp;face=0_0_940_529&quot;&gt;&lt;a href=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5d40R/hyYmJZb6IP/IGhUGRcKIcpcURTwYBMyK1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/crVCvh/hyYmQxfcYm/f7kSPi73TvnURLvxGdX79K/img.png?width=940&amp;amp;height=688&amp;amp;face=0_0_940_688,https://scrap.kakaocdn.net/dn/cxdRxX/hyYnekwxUZ/ir4iJSP9zdFuqie7ZZUBn0/img.jpg?width=940&amp;amp;height=529&amp;amp;face=0_0_940_529');&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;Transactional Outbox 패턴으로 메시지 발행 보장하기 - 리디주식회사&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Event-Driven Architecture에서 메시지 발행의 신뢰성을 보장하는 방법은 무엇일까요? 리디 서비스에 Transactional Outbox 패턴을 도입한 배경과 그 과정에서 얻은 배움을 공유합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ridicorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Debezium</category>
      <category>Kafka</category>
      <category>transactional outbox pattern</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/164</guid>
      <comments>https://ksh-coding.tistory.com/164#entry164comment</comments>
      <pubDate>Mon, 3 Mar 2025 15:36:44 +0900</pubDate>
    </item>
    <item>
      <title>JPA에서 PostgreSQL Batch Insert 사용하기 (feat. Batch Size)</title>
      <link>https://ksh-coding.tistory.com/163</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용해서 개발 시 여러 건의 Entity를 INSERT를 해야하는 상황이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Spring Data JPA의 커스텀 메소드인 saveAll을 사용하면 Batch Insert 상태로 동작할 것이라고 예상했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 쿼리는 Batch Insert로 1건의 쿼리가 아닌 여러 건의 INSERT 쿼리가 발생하는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에 JPA, PostgreSQL에서 Batch Insert를 어떻게 할 수 있는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1738214762768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public List&amp;lt;Skill&amp;gt; createAll(Collection&amp;lt;Skill&amp;gt; skills, String jobId) {
    ...
 
    skillRepository.saveAll(entities);
    
    ...
}&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;위처럼 Spring Data JPA의 saveAll을 했을때, 예상과 달리 1건씩 쿼리가 나가는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738215093935&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* Hibernate 로그
 
insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 
insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
 
insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
 
* DB 로그
2025-01-22 04:18:15.836 UTC [511] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) values (...)
2025-01-22 04:18:15.836 UTC [511] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) values (...)
2025-01-22 04:18:15.837 UTC [511] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) values (...)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Batch Insert 설정 (PostgreSQL)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원했던 쿼리는 Multi-Value 쿼리로 여러 INSERT가 Batch Insert로 1개의 쿼리로 묶어서 나가는 것을 원했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738215572515&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* 기본 insert - 쿼리 N개
INSERT INTO skill (...) VALUES (...)
INSERT INTO skill (...) VALUES (...)
INSERT INTO skill (...) VALUES (...)
 
* Batch Insert - 쿼리 1개
INSERT INTO skill (...)
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;해당 Batch Insert를 하기 위해서는 결론적으로 다음과 같은 2가지 설정을 하면 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Hibernate의 'hibernate.jdbc.batch_size' 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JDBC PostgreSQL Driver의 'reWriteBatchedInserts' 설정&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1738216323810&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* application.yml

spring:
  jpa:
    properties:
      hibernate.jdbc.batch_size: 50
      
  datasource:
    url: jdbc:postgresql://localhost:5432/batch_test?rewriteBatchedInserts=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;이렇게 설정하게 되면 다음과 같은 로그가 찍히게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738216660240&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* Hibernate 로그
 
insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 
 insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
 
insert
    into
        &quot;job&quot;.&quot;skill&quot; (...)
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  
 
 
* DB 로그
2025-01-22 07:43:20.092 UTC [1212] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) values (...),(...)
2025-01-22 07:43:20.093 UTC [1212] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) 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;로그를 보면 Hibernate 로그는 그대로고 DB 로그는 3건 중에 2건이 Multi-Value 쿼리가 되고 1건은 따로 나간 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3건의 INSERT 시)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과만 보면 조금 이상해보일 수 있지만 결론적으로는 제대로 Multi-Value 쿼리가 실행되어 Batch Insert가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서 2가지 설정의 원리를 살펴봅시다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Batch Insert 설정 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 Batch Insert로 처리하긴 했지만, 처음 문제를 마주쳤을 때 저는 자세한 원리를 이해하지 못했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 원리를 이해하기 위해서는 다음과 같은 3가지 개념의 역할을 이해해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JPA의 쓰기 지연 저장소&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hibernate의 batch_size 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JDBC PostgreSQL Driver의 reWriteBatchedInserts 설정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. JPA 쓰기지연 저장소&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음에 JPA를 사용한 Batch Insert를 생각했을 때, 쓰기지연 저장소가 flush 시에 모은 여러 쿼리를 날리는 역할을 하니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무런 설정 없이 JPA의 기본 동작으로만으로도 여러 쿼리가 1개의 쿼리로 Batch Insert가 동작할 줄 알았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, JPA의 쓰기지연 저장소의 역할은 &lt;b&gt;'flush 시점에 쿼리를 DB에 반영하기 위해 쿼리들을 저장소에 모아주는 역할'&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 모은 쿼리가 1번에 DB에 전송되는 것이 아니라 결국 1개씩 DB에 전송하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모은 쿼리가 10개라면 쿼리를 DB에 전송하고 응답받는 네트워크 왕복 비용이 10번 발생하게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&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: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. Hibernate의 batch_size 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Batch Size 설정은 &lt;b&gt;'여러 쿼리를 모아서 전송해주는 역할'&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 10개일 때 DB에 한번에 10개의 쿼리를 전송하기 때문에 네트워크 왕복 비용을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이는 전송하고 응답받는 네트워크 왕복 비용만 줄일 뿐, 실제로 발생하는 쿼리가 1개의 쿼리로 Multi-Value 쿼리가 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&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: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-3. JDBC PostgreSQL Driver의 reWriteBatchedInserts 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 reWriteBatchedInserts의 역할은 &lt;b&gt;'전송된 쿼리를 내부적으로 파싱 및 실행 최적화를 통해 Multi-Value 쿼리로 재작성 하는 역할'&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;그래서 여러 건의 쿼리가 모아져서 전송 된다면, &lt;b&gt;해당 쿼리들을 내부적으로 파싱하여 Multi-Value 쿼리, Batch Insert 쿼리를 완성&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;그러므로, Batch Size를 설정하지 않아서 1건의 쿼리씩만 온다면 해당 옵션을 설정하더라도 애초에 Batch Insert가 되지 않는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://jdbc.postgresql.org/documentation/use/&quot;&gt;https://jdbc.postgresql.org/documentation/use/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738217472032&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;Initializing the Driver | pgJDBC&quot; data-og-description=&quot;Initializing the Driver This section describes how to load and initialize the JDBC driver in your programs. Importing JDBC Any source file that uses JDBC needs to import the java.sql package, using: NOTE You should not import the org.postgresql package unl&quot; data-og-host=&quot;jdbc.postgresql.org&quot; data-og-source-url=&quot;https://jdbc.postgresql.org/documentation/use/&quot; data-og-url=&quot;https://jdbc.postgresql.org/documentation/use/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://jdbc.postgresql.org/documentation/use/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jdbc.postgresql.org/documentation/use/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Initializing the Driver | pgJDBC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Initializing the Driver This section describes how to load and initialize the JDBC driver in your programs. Importing JDBC Any source file that uses JDBC needs to import the java.sql package, using: NOTE You should not import the org.postgresql package unl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jdbc.postgresql.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4.  Batch Insert 주의할 점, 고려할 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 간단하게 Batch Insert 설정 방법과 원리들을 살펴봤지만, 추가적인 고려할 점이 몇 가지 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-1. 쿼리의 수가 Batch Size보다 작으면 무조건 1개의 쿼리로 묶어서 전송될까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 실제 동작 쿼리를 유심히 보면 알 수 있지만, 3건의 쿼리를 보낼 때 Batch Size를 3보다 크게 설정했음에도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multi-Value 쿼리가 2건, 1건으로 나가서 총 2개의 쿼리가 발생한 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738217643555&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* DB 로그
2025-01-22 07:43:20.092 UTC [1212] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) values (...),(...)
2025-01-22 07:43:20.093 UTC [1212] LOG: insert into &quot;job&quot;.&quot;skill&quot; (...) 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;해당 부분은 레퍼런스를 아무리 찾아봐도 제대로 된 문서를 찾을 수가 없어서 경험적으로 결과를 보고 추론하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는 &lt;b&gt;쿼리를 묶는 Batch 단위는 쿼리 개수에서 2의 제곱수 단위로 묶어진다는 것을 확인&lt;/b&gt;했습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EX) 쿼리 11개 -&amp;gt; value 8건 + value 2건 + value 1건 (쿼리 총 3개)&lt;/li&gt;
&lt;/ul&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-2. JPA ID 생성 전략에 따라 Batch Insert가 불가능하다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 ID 생성 전략을 기본적으로 @GeneratedValue 어노테이션의 속성을 통해 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 Enum으로 생성 전략을 지정하는데, 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738218025130&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum GenerationType { 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using an underlying 
     * database table to ensure uniqueness.
     */
    TABLE, 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using a database sequence.
     */
    SEQUENCE, 

    /**
     * Indicates that the persistence provider must assign 
     * primary keys for the entity using a database identity column.
     */
    IDENTITY,

    /**
     * Indicates that the persistence provider must assign
     * primary keys for the entity by generating an RFC 4122
     * Universally Unique IDentifier.
     */
    UUID,

    /**
     * Indicates that the persistence provider should pick an 
     * appropriate strategy for the particular database. The 
     * &amp;lt;code&amp;gt;AUTO&amp;lt;/code&amp;gt; generation strategy may expect a database 
     * resource to exist, or it may attempt to create one. A vendor 
     * may provide documentation on how to create such resources 
     * in the event that it does not support schema generation 
     * or cannot create the schema resource at runtime.
     */
    AUTO
}&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;이 중에서 IDENTITY 전략을 사용하게 되면 Batch Insert가 이루어지지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접적인 원인으로는&lt;b&gt; IDENTITY 전략을 사용하게 되면 쿼리가 쓰기지연 저장소를 거치지 않고 바로 DB로 전송되기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDENTITY 전략은 PK 생성을 DB에 위임하기 때문에 DB를 거쳐야만 PK가 지정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 영속성 컨텍스트의 1차 캐시에 해당 Entity를 저장하기 위해서는 DB를 거쳐 PK를 받아와야 저장이 가능하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기지연 저장소를 거치지 않고 바로 DB로 전송하여 쿼리를 전송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Batch Size를 지정하지 않은 것처럼 동작하여 reWriteBatchedInserts 옵션이 있더라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 내부적으로 1건의 쿼리씩 전송받기 때문에 파싱하여 Multi-Value 쿼리를 만들 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, JPA에서 Batch Insert를 사용하기 위해서는 IDENTITY 방식이 아닌 Sequence나 Table, UUID 방식을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;저는 Tsid를 사용하여 엔티티 생성 시 PK가 할당되어 문제가 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tsid에 관한 내용은 이전에 제가 작성한 ID 생성 전략 비교 글에서 조금 더 알아볼 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/157#5.%20TSID-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ksh-coding.tistory.com/157#5.%20TSID-1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738218899906&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[DB] DB PK 생성 전략 알아보기 (feat. Auto Increment, UUID, ULID, Snowflake ID, TSID)&quot; data-og-description=&quot;0. 들어가기 전이전까지 저는 모든 프로젝트에서 관성적으로 다음과 같이 Auto_Increment 전략을 사용해서 DB PK를 생성했습니다.public class XxxEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Lon&quot; data-og-host=&quot;ksh-coding.tistory.com&quot; data-og-source-url=&quot;https://ksh-coding.tistory.com/157#5.%20TSID-1&quot; data-og-url=&quot;https://ksh-coding.tistory.com/157&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/t6NA2/hyX74pa7sW/JzKt1yXHy5tja0zoib2Oo1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rCanh/hyX7XDAO7J/Q7O6NPd4K4tgqkAFXMuIK1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/DF5WQ/hyX7UfL1tD/megGSujHNYhH1sl6JmDPgK/img.png?width=3116&amp;amp;height=606&amp;amp;face=0_0_3116_606&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/157#5.%20TSID-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ksh-coding.tistory.com/157#5.%20TSID-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/t6NA2/hyX74pa7sW/JzKt1yXHy5tja0zoib2Oo1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rCanh/hyX7XDAO7J/Q7O6NPd4K4tgqkAFXMuIK1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/DF5WQ/hyX7UfL1tD/megGSujHNYhH1sl6JmDPgK/img.png?width=3116&amp;amp;height=606&amp;amp;face=0_0_3116_606');&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;[DB] DB PK 생성 전략 알아보기 (feat. Auto Increment, UUID, ULID, Snowflake ID, TSID)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;0. 들어가기 전이전까지 저는 모든 프로젝트에서 관성적으로 다음과 같이 Auto_Increment 전략을 사용해서 DB PK를 생성했습니다.public class XxxEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Lon&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ksh-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;</description>
      <category>Spring/JPA</category>
      <category>batchInsert</category>
      <category>PostgreSQL</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/163</guid>
      <comments>https://ksh-coding.tistory.com/163#entry163comment</comments>
      <pubDate>Thu, 30 Jan 2025 15:35:34 +0900</pubDate>
    </item>
    <item>
      <title>JPA에서 delete, insert 시 왜 insert 쿼리가 먼저 실행될까?</title>
      <link>https://ksh-coding.tistory.com/162</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용해서 개발하던 중에 예상과 다르게 동작하는 테스트를 발견하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Update 로직을 delete, insert로 구현했는데 delete 후 insert가 되는 것이 아니라, insert 후 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;왜 이러한 쿼리 순서로 실행되는지 상황부터 원인, 해결방법까지 살펴보도록 하겠습니다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기본 컨텍스트를 알려드리면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job - Skill 1:N이지만, 연관관계는 맺지 않고 JobId로 간접 참조&lt;/li&gt;
&lt;li&gt;Skill의 update 로직 메소드에서 상황 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738045307067&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * DELETE &amp;amp; INSERT
 */
@Transactional
public void replaceUpdateByJobId(Collection&amp;lt;Skill&amp;gt; skills, String jobId) {
    skillRepository.deleteAllByJobId(jobId);
 
    ...
 
    skillRepository.saveAll(skillEntities);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 컬럼인 JobId에 해당하는 모든 Skill 삭제 (DeleteAll)&lt;/li&gt;
&lt;li&gt;저장할 Skill들 INSERT (SaveAll)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 DELETE, INSERT로 여러 건의 업데이트를 처리하는 로직을 수행하는 메소드를 개발했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 제가 예상한 쿼리 순서는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT 1건 (영속성 컨텍스트에 처리하려는 Skill을 불러오기 위한 쿼리)&lt;/li&gt;
&lt;li&gt;DELETE 여러건 (jobId에 해당하는 Skill 개수만큼)&lt;/li&gt;
&lt;li&gt;INSERT 여러건 (INSERT할 Skill 개수만큼)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 실제 나가는 쿼리 로그를 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738045528982&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    select
        se1_0.&quot;id&quot;,
        se1_0.&quot;code&quot;,
        se1_0.&quot;job_id&quot;,
        se1_0.&quot;keyword&quot;,
        se1_0.&quot;type&quot;,
        se1_0.&quot;updated_at&quot;,
        se1_0.&quot;updated_by&quot;
    from
        &quot;job&quot;.&quot;skill&quot; se1_0
    where
        se1_0.&quot;job_id&quot;=?
 
---
    insert
    into
        &quot;job&quot;.&quot;skill&quot; (&quot;code&quot;, &quot;job_id&quot;, &quot;keyword&quot;, &quot;type&quot;, &quot;updated_at&quot;, &quot;updated_by&quot;, &quot;id&quot;)
    values
        (?, ?, ?, ?, ?, ?, ?)
 
... (개수 만큼)
---
    delete
    from
        &quot;job&quot;.&quot;skill&quot;
    where
        &quot;id&quot;=? 
 
... (개수 만큼)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT 1건&lt;/li&gt;
&lt;li&gt;INSERT 여러 건&lt;/li&gt;
&lt;li&gt;DELETE 여러 건&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 까지는 JPA의 동작대로 영속성 컨텍스트를 불러와야 하므로 잘 가져오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 왜 DELETE가 먼저 동작하지 않고 INSERT 이후에 DELETE가 동작하는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이유를 아래에서 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. JPA(Hibernate)의 쿼리 실행 순서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA(Hibernate)는 쿼리 실행 순서가 정해져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 쿼리가 실행되는 시점은 flush()를 하는 시점에 쓰기 지연 저장소에 저장했던 쿼리를 실행하게 됩니디.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 쿼리 실행 순서를 처리하는 로직도 flush()를 하는 로직에 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush()를 실행하는  로직은 따라가 보면&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;&lt;b&gt;AbstractFlushingEventListener의 performExecutions&lt;/b&gt;에서 처리하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;해당 메소드의 주석을 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;* AbstractFlushingEventListener - performExecutions(...)&amp;nbsp;&lt;br /&gt;Execute all SQL (and second-level cache updates) in&amp;nbsp;a special order so that foreign-key constraints cannot be violated:&amp;nbsp;&lt;br /&gt;&lt;br /&gt;1. Inserts,&amp;nbsp;in&amp;nbsp;the order they were performed&lt;br /&gt;2. Updates&lt;br /&gt;3. Deletion of collection elements&lt;br /&gt;4. Insertion of collection elements&lt;br /&gt;5. Deletes,&amp;nbsp;in&amp;nbsp;the order they were performed&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;모든 SQL을 FK 제약 조건을 위반하지 않게 하기 위해 다음과 같은 순서로 실행한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;1. INSERT&amp;nbsp;&lt;/li&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;2. UPDATE&lt;/li&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;3. Collection 요소들의 DELETE&lt;/li&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;4. Collection 요소들의 INSERT&lt;/li&gt;
&lt;li data-bidi-marker=&quot;true&quot;&gt;5. DELETE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명을 보면, &lt;b&gt;Hibernate에서 쿼리 실행 순서를 INSERT가 가장 먼저 실행되도록 처리&lt;/b&gt;하기 때문에 순서가 바뀐 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 설명에도 있듯이 FK 제약 조건을 위반하지 않게 하기 위함입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1:N 관계의 Job - Skill에서 Job Delete 쿼리와 Skill Insert 쿼리가 순서대로 실행된다면 FK 제약 조건을 위반하게 됩니다.&lt;/li&gt;
&lt;li&gt;이러한 상황을 막기 위해 순서를 고정하여 FK 제약조건을 위반하는 쿼리는 예외가 발생하도록 하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 원인은 비교적 간단하게 Hibernate의 처리 로직을 찾으면 알 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. DELETE 시 Where 절에 왜 일반 필드가 아닌 PK로 삭제할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 원인은 찾았지만 또 예상과 다른 쿼리가 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로 다음과 같은 DELETE 쿼리였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738047716060&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  delete
    from
        &quot;job&quot;.&quot;skill&quot;
    where
        &quot;id&quot;=?&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;deleteAllByJobId를 실행한 쿼리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK가 아닌 일반 필드인 JobId로 삭제하는 Spring Data JPA 커스텀 메소드를 정의하여 삭제했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 실제 실행되는 쿼리는 JobId가 아닌 PK로 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;전체 쿼리를 자세히 살펴보면, jobId를 사용하는 곳은 SELECT 쿼리입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738047881181&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    select
        se1_0.&quot;id&quot;,
        se1_0.&quot;code&quot;,
        se1_0.&quot;job_id&quot;,
        se1_0.&quot;keyword&quot;,
        se1_0.&quot;type&quot;,
        se1_0.&quot;updated_at&quot;,
        se1_0.&quot;updated_by&quot;
    from
        &quot;job&quot;.&quot;skill&quot; se1_0
    where
        se1_0.&quot;job_id&quot;=?
 
 
 ...
---
    delete
    from
        &quot;job&quot;.&quot;skill&quot;
    where
        &quot;id&quot;=? 
 
... (개수 만큼)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 쿼리 동작은 영속성 컨텍스트의 1차 캐시와 관련이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트의 1차 캐시에서는 가져온 Entity의 PK를 Key로 하고, Value를 Entity로 저장하게 됩니다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에서 Entity를 가져올 때 커스텀한 메소드의 필드인 JobId로 SELECT&lt;/li&gt;
&lt;li&gt;가져온 Entity를 1차 캐시에 저장 (Key : PK, value : Entity)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이후 작업은 영속성 컨텍스트 1차 캐시에 있는 Entity를 사용하기 때문에 PK로 쿼리를 실행&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 순서로 실행하기 때문에, 결론적으로 일반 컬럼으로 delete를 실행할 때 SELECT만 해당 컬럼이 사용되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 작업은 모두 PK로 처리되는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 그렇다면, Delete -&amp;gt; Insert로 처리하려면 어떻게 할 수 있을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 로직에서 Delete, Insert를 하더라도 FK 제약 조건에 위반되는 것이 아니기 때문에 해당 순서로 실행해도 문제가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Hibernate의 기본 동작이 Insert 후 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;아래와 같은 해결 방법을 통해 해결할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;강제 flush() 실행&lt;/li&gt;
&lt;li&gt;JPQL 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-1. 강제 Flush() 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 flush() 로직은 쓰기 지연 저장소에 쌓인 쿼리를 실행하는 순서를 조작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Delete -&amp;gt; Insert를 다 쌓고 flush()를 하지 않고 Delete가 쌓였을 때 flush()를 호출하게 된다면 정상적으로 동작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738048481386&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

private final EntityManager em;

/**
 * DELETE &amp;amp; INSERT
 */
@Transactional
public void replaceUpdateByJobId(Collection&amp;lt;Skill&amp;gt; skills, String jobId) {
    skillRepository.deleteAllByJobId(jobId);
    em.flush();
 
    ...
 
    skillRepository.saveAll(skillEntities);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 delete 로직 후 바로 flush()를 호출하면 가능합니다.&lt;/li&gt;
&lt;li&gt;하지만, 다음과 같은 2가지 이유로 사용하지 않았습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 Service와 Repository 계층이 나뉘어져 있는데, EntityManager를 Service 계층에서 사용하는 것이 책임에 맞지 않는 것 같다.&lt;/li&gt;
&lt;li&gt;이후에 나올 JPQL을 사용하는 방법보다 쿼리가 더 많이 실행된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT 쿼리 1건&lt;/li&gt;
&lt;li&gt;DELETE 쿼리 여러 건 (삭제할 Entity 개수만큼, PK로 삭제하기 때문에)&lt;/li&gt;
&lt;li&gt;INSERT 쿼리 여러 건&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-2. JPQL 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 영속성 컨텍스트를 사용하지 않고 직접 DB에 쿼리를 실행하는 JPQL을 사용하면 해결할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738048742400&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface SkillRepository extends JpaRepository&amp;lt;SkillEntity, String&amp;gt; {
     
    @Modifying
    @Query(&quot;DELETE FROM SkillEntity s WHERE s.jobId = :jobId&quot;)
    void deleteAllByJobId(String jobId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@Query와 @Modifying 어노테이션을 사용하여 JPQL을 작성하고 DB에 직접 쿼리를 실행할 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이렇게 동작하면 JPA에서 flush()를 호출하지 않고 직접 DB에 쿼리를 실행하기 때문에 Delete -&amp;gt; Insert가 가능합니다.&lt;/li&gt;
&lt;li&gt;또, &lt;b&gt;JPA의 영속성 컨텍스트를 사용하지 않기 때문에 쿼리 수가 강제 flush() 방법보다 적습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DELETE 쿼리 1건 (jobId에 해당하는 Skill을 모두 한번에 삭제)&lt;/li&gt;
&lt;li&gt;INSERT 쿼리 여러 건&lt;/li&gt;
&lt;li&gt;위와 같이, 영속성 컨텍스트를 사용하지 않기 때문에 가져오는 SELECT 쿼리가 필요 없고, 각 Skill의 PK로 삭제하는 것이 아니라 일반 필드 jobId로 삭제할 수 있기 때문에 쿼리 1건으로 여러 개의 Row를 삭제할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 구현 시에는 JPQL을 사용하여 위와 같은 문제를 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&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;※ @Query &amp;amp; @Modifying&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Query를 통해 Spring Data JPA에서 영속성 컨텍스트를 사용하지 않고 직접 DB에 쿼리를 실행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;@Modifying은 실행되는 JPQL이 SELECT가 아닌 DML(INSERT, UPDATE, DELETE)임을 명시하는 어노테이션입니다.&lt;/li&gt;
&lt;li&gt;Spring Data JPA에서 DML문을 직접 DB에 실행하기 위해서는 @Modifying과 @Query를 함께 선언해야 합니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Query만 선언하고 DML을 JPQL로 작성한 경우, &lt;b&gt;QueryExecutionRequestException가 발생합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;@Modifying만 선언한 경우 당연히, JPA 영속성 컨텍스트의 동작대로 실행되므로 예상과 다르게 동작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ @Modifying의 flushAutomatically(), clearAutomatically()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Modifying 어노테이션에는 2가지 속성이 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;flushAutomatically (boolean) default false&lt;/b&gt; : 해당 쿼리 실행 전에 영속성 컨텍스트의 변경 사항을 DB에 flush 할지 여부&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;clearAutomatically (boolean) default false&lt;/b&gt; : 해당 쿼리 실행 후 영속성 컨텍스트를 clear 할지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flushAutomatically 같은 경우에는 Hibernate의 &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;FlushModeType의 기본 값이 AUTO로 동작하기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;일반적으로는 false로 기본값이어도 flush가 수행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;해당 AUTO 동작은 JPQL로 실행 시에는 영향을 받는 Entity에 관해서만 flush가 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AUTO&amp;nbsp;flush&lt;br /&gt;&lt;br /&gt;By default, Hibernate uses the&amp;nbsp;AUTO&amp;nbsp;flush mode which triggers a flush in the following circumstances:&lt;br /&gt;&lt;br /&gt;* prior to committing a Transaction&lt;br /&gt;* prior to executing a JPQL/HQL query that overlaps with the queued entity actions&lt;br /&gt;* before executing any native SQL query that has no registered synchronization&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;따라서, 해당 JPQL 실행 시 다른 Entity의 쿼리까지 flush 하고 싶다면 true로 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clearAutomatically 같은 경우에는 마찬가지로 비즈니스 로직에 따라서 설정을 다르게 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, JPQL을 실행한 내용을 가져와서 이후 로직에 사용해야 한다면 true로 설정해야 할 것 입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://bin-repository.tistory.com/165&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bin-repository.tistory.com/165&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738051147132&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[SpringDataJPA] @Modifying 과 @Query 의 관계와 동작방식 (@Query 없이 @Modifying 만 사용한다면 어떻게 될까?&quot; data-og-description=&quot;1. @Modifying 이란? @Modifying 어노테이션은 @Query 어노테이션으로 작성된 수정, 삭제 쿼리 메소드를 사용할 때 필요하다. 즉, 조회 쿼리를 제외하고 데이터에 변경이 일어나는 INSERT, UPDATE, DELETE 쿼리&quot; data-og-host=&quot;bin-repository.tistory.com&quot; data-og-source-url=&quot;https://bin-repository.tistory.com/165&quot; data-og-url=&quot;https://bin-repository.tistory.com/165&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWtov/hyX4xztWbv/EQKSFwUwtYmTmrj0kASWhk/img.png?width=800&amp;amp;height=252&amp;amp;face=0_0_800_252,https://scrap.kakaocdn.net/dn/httga/hyX7WEnH8S/dycABcdgWu5mvpKwcwmt3K/img.png?width=800&amp;amp;height=252&amp;amp;face=0_0_800_252,https://scrap.kakaocdn.net/dn/bKK6Bt/hyX7WYGGs7/Mhr3d9WJ6fOREED4qvuqnk/img.png?width=1826&amp;amp;height=956&amp;amp;face=0_0_1826_956&quot;&gt;&lt;a href=&quot;https://bin-repository.tistory.com/165&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bin-repository.tistory.com/165&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWtov/hyX4xztWbv/EQKSFwUwtYmTmrj0kASWhk/img.png?width=800&amp;amp;height=252&amp;amp;face=0_0_800_252,https://scrap.kakaocdn.net/dn/httga/hyX7WEnH8S/dycABcdgWu5mvpKwcwmt3K/img.png?width=800&amp;amp;height=252&amp;amp;face=0_0_800_252,https://scrap.kakaocdn.net/dn/bKK6Bt/hyX7WYGGs7/Mhr3d9WJ6fOREED4qvuqnk/img.png?width=1826&amp;amp;height=956&amp;amp;face=0_0_1826_956');&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;[SpringDataJPA] @Modifying 과 @Query 의 관계와 동작방식 (@Query 없이 @Modifying 만 사용한다면 어떻게 될까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. @Modifying 이란? @Modifying 어노테이션은 @Query 어노테이션으로 작성된 수정, 삭제 쿼리 메소드를 사용할 때 필요하다. 즉, 조회 쿼리를 제외하고 데이터에 변경이 일어나는 INSERT, UPDATE, DELETE 쿼리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bin-repository.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/JPA</category>
      <category>@Modifying</category>
      <category>@Query</category>
      <category>flush</category>
      <category>hibernate</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/162</guid>
      <comments>https://ksh-coding.tistory.com/162#entry162comment</comments>
      <pubDate>Tue, 28 Jan 2025 16:59:32 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Apache Kafka 공식문서 살펴보기 (Design, 심화 이론)</title>
      <link>https://ksh-coding.tistory.com/161</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 전에 Kafka의 기본 이론에 대해서 알아봤었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/160&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ksh-coding.tistory.com/160&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733480477757&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kafka] Apache Kafka 알아보기 (기본 이론)&quot; data-og-description=&quot;0. 들어가기 전이전에 MSA 프로젝트를 진행할 때, Kafka를 사용해본 적이 있습니다.하지만 그때는 먼저 구현을 했어야 했기에 제대로 된 Kafka의 이론은 모른채 구현만 쫓아갔던 기억이 있습니다.&amp;nbsp;&quot; data-og-host=&quot;ksh-coding.tistory.com&quot; data-og-source-url=&quot;https://ksh-coding.tistory.com/160&quot; data-og-url=&quot;https://ksh-coding.tistory.com/160&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cgSjk5/hyXGEk27t9/OrrNj2YRJWNc19lqBPuK00/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/OVTlU/hyXGJ024TW/nWAFXhHLebtZCFFlHNCZ3k/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bnG6Cy/hyXKlYwYYU/m6i2InSTVPk4lq4TCVyZiK/img.png?width=2184&amp;amp;height=1182&amp;amp;face=0_0_2184_1182&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/160&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ksh-coding.tistory.com/160&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cgSjk5/hyXGEk27t9/OrrNj2YRJWNc19lqBPuK00/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/OVTlU/hyXGJ024TW/nWAFXhHLebtZCFFlHNCZ3k/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bnG6Cy/hyXKlYwYYU/m6i2InSTVPk4lq4TCVyZiK/img.png?width=2184&amp;amp;height=1182&amp;amp;face=0_0_2184_1182');&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;[Kafka] Apache Kafka 알아보기 (기본 이론)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;0. 들어가기 전이전에 MSA 프로젝트를 진행할 때, Kafka를 사용해본 적이 있습니다.하지만 그때는 먼저 구현을 했어야 했기에 제대로 된 Kafka의 이론은 모른채 구현만 쫓아갔던 기억이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ksh-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 공식문서에서 언급하는 좀 더 심화적인 내용들을 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 공식문서의 Design 챕터에서는 Kafka의 내부 구조, 원리를 다루고 어떤 장점이 있는지를 소개하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;1. Persistence&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 각 이벤트 메시지들은 어느 곳에, 어떻게 저장될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Kafka에서의 메시지 Read/Write 성능은 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 챕터에서는 이와 관련한 내용을 다룹니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Dont'fear the filesystem! : Kafka는 filesystem을 사용하지만, 성능이 좋다!&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka relies heavily on the filesystem for storing and caching messages. There is a general perception that &quot;disks are slow&quot; which makes people skeptical that a persistent structure can offer competitive performance.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 message를 저장하고 캐싱할 때 filesystem을 사용한다.&lt;/li&gt;
&lt;li&gt;보통 Disk는 느리지만, Disk 구조를 적절히 설계한다면 빠를 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 일반적인 Disk 구조 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 &lt;b&gt;Disk의 선형 Read/Write(순차 I/O) 속도는 빠르지만, 데이터를 탐색할 때의 Random I/O는 상당히 느리다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;순차 I/O의 쓰기 성능은 600MB/sec인 반면, Random I/O의 쓰기 성능은 100KB/sec으로 약 6000배나 차이가 난다.&lt;/li&gt;
&lt;li&gt;이러한 차이를 보완하기 위해서 현대 OS는 메모리 회수 시 메인 메모리의 free 메모리를 Disk 캐싱에 사용한다. (Random I/O 발생을 줄이기 위해)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서 이러한 메인 메모리를 Disk 캐싱에 사용한다면 문제가 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;Kafka는 JVM 기반으로 구축되는데, JVM 위에서 인메모리 캐시를 사용하면 객체 메모리 오버헤드가 많아지고 GC에 데이터가 커져서 더 느려지게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ Kafka Disk 캐싱&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서,&amp;nbsp;&lt;b&gt;Kafka에서는 메모리에 Disk를 캐싱하는 것이 아니라 OS 커널의 페이지 캐시에 Disk를 캐싱한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이로 인해 메인 메모리를 여유롭게 사용하면서 GC의 패널티 없이 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;또, 인메모리 캐싱은 서비스가 재시작될 때 다시 빌드해야하므로 성능이 좋지 않은데, 페이지 캐시에 Disk 캐싱을 하게되면 OS의 영역이기 때문에 재시작과 관련 없이 캐시를 유지한다.&lt;/li&gt;
&lt;li&gt;따라서, Kakfa의 모든 데이터는  filesystem에 'persistent log' 형태로 기록된다.  (페이지 캐시에 저장)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Constant Time Suffices : Kafka는 Read/Write 시간복잡도가 O(1)이다! (랜덤 I/O 대신 순차 I/O 사용)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 메시징 시스템에서는 B-Tree를 사용하여 메시지별로 메타 데이터를 유지합니다.&lt;/li&gt;
&lt;li&gt;하지만, B-Tree는 시간복잡도가 O(log N)으로 일반적으로 빠르지만 Disk 작업에서는 상당히 느릴 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Disk는 병렬 작업을 수행하지 못하고 초당 약 10ms로 탐색하므로 여러 건의 작업 시 오버헤드가 매우 높아집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka는 filesystem 기반 Queue로, 모든 연산을 로그 기반의 순차 I/O를 사용하여 O(1)으로 빠르게 디스크 탐색을 합니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 파일에 추가하는 방식으로 데이터 기록&lt;/li&gt;
&lt;li&gt;시간 복잡도가 O(1)이기 때문에 저장된 파일의 개수에 영향을 받지 않고 많이 적재되어 있더라도 성능을 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;따라서, 다른 메시징 시스템과 달리 사용자가 성능을 고려할 필요 없이&amp;nbsp;&lt;b&gt;메시지를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;원하는 기간동안 적재해놓고 재소비 가능&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. Efficiency&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 Kafka에서 어떤 식으로 메시지들을 효율적으로 처리하는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Message 그룹화 + 페이지 캐시 Read로 비효율성 제거&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 연산이 순차 I/O임에 따라 다음과 같은 2가지 문제가 발생합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;작은 크기의 I/O가 빈번하게 발생&lt;/b&gt; : 순차적으로 O(1)로 Write를 하기 때문에 작은 크기의 메시지가 버퍼에 쌓이지 않고 매번 I/O를 발생시켜 비효율적일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과도한 바이트 복사 발생&lt;/b&gt; : Kafka 자체 메모리 내부에 버퍼 및 캐시가 존재하는데, 메시지 처리 시마다 메시지를 복사해서 버퍼에 저장하고 캐싱하므로 과도한 바이트 복사가 일어나서 비효율적일 수 있다.&lt;/li&gt;
&lt;li&gt;EX) 작은 크기의 메시지들이 자주 처리되면 Network 비용이 처리 시마다 발생하고 바이트 복사가 매번 일어나서 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작은 크기의 I/O가 빈번하게 발생하는 비효율성을 제거하기 위해, Kafka는 메시지들을 그룹화해서 저장한다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Producer는 로그에 그룹화된 큰 메시지들을 저장하고 Consumer도 그룹화된 큰 메시지들을 소비하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과도한 바이트 복사를 피하기 위해 '페이지 캐시'에 한 번만 복사하고 이후에는 저장된 메시지들을 재사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   End-to-end Batch Compression : 여러 메시지들을 Batch 압축&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 메시지를 그룹화하는 것뿐만 아니라 Batch를 사용해서 메시지들을 압축한다.&lt;/li&gt;
&lt;li&gt;Batch 메시지들은 로그에 압축된 상태로 저장되고, Consumer가 해당 압축 데이터를 해제하여 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;3. Producer&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 챕터에는 메시지를 publish하는 Producer의  설계에 대해서 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Leader Broker &amp;amp; Follower Broker&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer를 보기 전에, &lt;b&gt;Event를 저장하는 Storage 역할을 하는 Broker에 대해서 조금 더 보충이 필요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka Cluster의 여러 Broker들은 파티션별로 Leader, Follower의 역할&lt;/b&gt;을 가집니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image213.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8wxfY/btsK9ySshb5/qIMCDg0wqY1LaldvJua7Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8wxfY/btsK9ySshb5/qIMCDg0wqY1LaldvJua7Gk/img.png&quot; data-alt=&quot;Leader &amp;amp;amp; Follwer Broker&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8wxfY/btsK9ySshb5/qIMCDg0wqY1LaldvJua7Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8wxfY%2FbtsK9ySshb5%2FqIMCDg0wqY1LaldvJua7Gk%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;706&quot; height=&quot;343&quot; data-filename=&quot;image213.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Leader &amp;amp; Follwer Broker&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Cluster의 Broker는 기본적으로 모든 Partition의 정보를 알고 있다.&lt;/li&gt;
&lt;li&gt;하지만, &lt;b&gt;각 Partition에서 메시지를 Read/Write하는 Broker&lt;/b&gt;는 단 1대이고, 이를 &lt;b&gt;Leader Broker&lt;/b&gt;라고 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Partition별로 Leader Broker가 아닌 Broker들은 해당 Partition의 Follower Broker가 된다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Follower Broker가 존재하는 이유는 Partition의 Leader Broker에 장애가 발생했을 때 빠르게 Leader로 승격되는 구조를 만들어서 HA를 보장할 수 있기 때문이다.&lt;/li&gt;
&lt;li&gt;따라서, 모든 Follower Broker들은 각 파티션의 Leader Broker로부터 지속적으로 새로운 메시지를 확인하여 복제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Load Balancing&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 메시지 Publish 시 Broker Load Balancing : 메시지 Publish 시 어떤 Broker가 메시지를 Read/Write할지 어떻게 결정할까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka의 Producer들은 Kafka Cluster의 모든 Kafka Node에게 어떤 Broker가 살아있는지, Partition의 Leader Broker들은 어떤 Broker인지 메타데이터를 요청하여 제공받는다.&lt;/li&gt;
&lt;li&gt;Producer가 메시지를 Publish 할 때, &lt;b&gt;모든 Broker에게 메시지를 publish 하지 않고 Publish할 Partition의 Leader Broker에게만 메시지를 보낸다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 메시지 Publish 시 Partition Load Balancing : 메시지 Publish 시 어떤 Partition에 메시지가 적재될 지 어떻게 결정할까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 기본적으로 라운드 로빈 방식을 사용하여 적재할 파티션을 결정하지만, 조건을 통해 적재할 파티션을 지정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적재할 파티션 지정 : &lt;b&gt;Event Key&lt;/b&gt;를 설정하여 지정&lt;/li&gt;
&lt;li&gt;Event Key가 설정된 Event는 해당 Key를 해싱하여 적재할 Partition을 정한다.&lt;/li&gt;
&lt;li&gt;따라서, 동일한 Event Key를 가진 Event는 모두 같은 Partition에 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   Asynchronous Send&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 Batch를 통해 Producer에서 메시지를 비동기로 Publish 할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 Send의 의미는 여러 건의 메시지를 1건씩 동기적으로 처리하지 않고 비동기로 처리한 후 Batch 처리한다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Producer에서 메시지를 바로 Publish하지 않고 메모리에 쌓아놨다가 하나의 Request에 모두 담아서 보낼 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리에 적재된 메시지의 양 (batch.size) or 최대 대기 시간 (linger.ms)을 설정하여 그 이상이 되면 Request를 보내도록 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;이를 통해 약간의 지연 시간(버퍼 시간)을 감수하고 처리량을 늘릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;4. Consumer&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이번 챕터에는 메시지를 consume하는 Consumer의 설계에 대해서 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   Kafka Consumer 동작 (feat. Offset)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Consumer는 메시지를 소비할 Partition의 Leader Broker에게&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;'fetch' 요청을 &lt;/span&gt;하는 방식으로 동작한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer는 각 'fetch' 요청마다 로그에 offset을 지정하고 해당 위치부터 끝 위치까지의 로그 Chunk를 다시 받는다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Offset이란, 파티션 내에서 데이터가 기록된 순서를 나타내는 고유 번호이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EX) Offset 1~10이 있을 때 fetch 요청 Offset이 4라면 Offset 4~10의 데이터를 받는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;Consumer는 해당 offset에 해당하는 로그를 재소비할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   Broker -&amp;gt; Consumer (Push) vs. Broker &amp;lt;- Consumer (Pull)&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;An initial question we considered is whether consumers should pull data from brokers or brokers should push data to the consumer.&amp;nbsp;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는 메시지 Consume 시 Broker에서 Conusmer에 Push할지, Consumer가 Broker에서 Pull할지를 고민했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Broker -&amp;gt; Consumer (Push)&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Broker가 Consumer에 Push하는 방식은 &lt;b&gt;Broker가 데이터 전송 속도를 제어하기 때문에 처리량이 다른 다양한 Consumer를 사용하기에 어려움이 있다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;EX) Push 방식을 사용하는 목적이 메시지가 Publish 될 때 바로 Consumer에서 처리하도록 하기 위함이지만,  Consumer가 많을 때 처리량이 다르다면 문제가 된다.&lt;/li&gt;
&lt;li&gt;또, 처리량이 같더라도 Publish 양이 엄청나게 많아지면 Consumer가 과부하가 걸릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Broker &amp;lt;- Consumer (Pull)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Push 방식의 단점을 Pull 방식을 사용하면 극복할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Publish 양이 많더라도 Consumer가 처리할 수 있을 때만 Pull 받아서 처리하기 때문에 과부하가 적다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또 다른 장점은 &lt;b&gt;Push 방식보다 효율적으로 Batch로 메시지를 소비할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Push 방식을 사용하면 Broker에서 메시지를 Batch로 보낼 수는 있지만, Consumer에서 소비할 수 있는지는 알지 못한채 보내게 되기 때문에 의미가 없게 된다.&lt;/li&gt;
&lt;li&gt;Pull 방식에서는 항상 Consumer의 처리 속도에 맞게 메시지를 가져올 수 있으므로 효율적으로 Batch를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pull 방식의 단점은, 반대로 Broker에 메시지가 없는 경우에도 메시지가 생성될 때까지 Polling 한다&lt;/b&gt;는 점이다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서는 이러한 단점을 방지하기 위해 Long Polling을 사용하여 응답이 올 때까지 연결을 끊지 않고 대기하여 효율을 조금 올린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;   Consumer Position&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Keeping track of what&amp;nbsp;has been consumed is, surprisingly, one of the key performance points of a messaging system.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시징 시스템에서 핵심 포인트 중 하나는 '소비된 메시지를 추적하는 것'입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ AS-IS 메시징 시스템 동작&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 메시징 시스템은 Broker에서 어떤 메시지가 소비되었는지에 대한 메타데이터를 보관합니다.&lt;/li&gt;
&lt;li&gt;이때, 보통 메시징 시스템들의 스토리지 구조는 확장성이 좋지 않기 때문에 메시지가 소비되면 보관한 메타데이터를 삭제합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 통해 데이터 크기를 작게 유지할 수 있기 때문에 확장을 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Broker에서 메시지가 소비되었다는 판단은 2단계에 거쳐서 발생합니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer가 메시지를 전달받았을 때 -&amp;gt; 'sent not consumed'의 의미로 마킹&lt;/li&gt;
&lt;li&gt;Consumer가 메시지 소비 후 Broker에게 메시지 소비 알림 -&amp;gt; 소비한 것으로 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동작은 다음과 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer가 Broker에 소비 알림을 보내지 못한 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2가지 선택 존재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Broker에서는 소비되지 않음으로 간주하고 메시지를 다른 Consumer에게 전달하여 재소비 &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer가 이미 처리한 메시지라면 &lt;b&gt;메시지 중복 재소비&lt;/b&gt;가 발생&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Broker에서 메시지를 삭제하지 않고 유지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미처리 상태의 메시지가 Broker에 지속적으로 쌓여 크기가 커질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 위와 같이 AS-IS의 메시징 시스템으로는 해결할 수 없는 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Kafka에서는 다음과 같은 방식으로 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ Kafka의 Offset 기반 Consume&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 &lt;b&gt;Broker가 아닌 Consumer가 메시지를 소비한 Position(Offset)을 관리&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;Topic 내의 여러 파티션은 정확히 각 1개의 Consumer에 의해 메시지가 소비됩니다.&lt;/li&gt;
&lt;li&gt;따라서, &lt;b&gt;Partition의 Offset은 할당된 Consumer가 마지막으로 메시지를 소비한 위치&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;이러한 &lt;b&gt;Partition의 Offset을 메시지 소비 시마다 주기적으로 저장하여 메시지 소비를 추적&lt;/b&gt;할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 하나의 정수에 불과하기 때문에 데이터 크기가 작고, 따라서 주기적으로 저장해도 성능에 무리가 없습니다.&lt;/li&gt;
&lt;li&gt;이를 통해 &lt;b&gt;이전에 소비했던 메시지도 필요한 경우 이전 Offset으로 돌아가서 메시지를 재소비&lt;/b&gt; 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;5. Message Delivery Semantics&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 Producer와 Consumer 동작에 대해서 살펴봤으니, 이제는 메시지 전달 방식에 대해서 살펴보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;* At most once&lt;/i&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&amp;mdash;Messages may be lost but are never redelivered.&lt;br /&gt;&lt;/span&gt;&lt;i&gt;* At least once&lt;/i&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&amp;mdash;Messages are never lost but may be redelivered.&lt;br /&gt;&lt;/span&gt;&lt;i&gt;* Exactly once&lt;/i&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&amp;mdash;this is what people actually want, each message is delivered once and only once.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;At most once : 메시지가 유실될 수 있지만 절대 다시 전달되지 않는다.&lt;/li&gt;
&lt;li&gt;At least once : 메시지가 절대 유실되지 않지만 다시 전달될 수 있다.&lt;/li&gt;
&lt;li&gt;Exactly once : 메시지가 정확히 1번만 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Kafka의 메시지 Commit에 대해서 알아봅시다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;When publishing a message we have a notion of the message being 'committed' to the log.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서 Message는 파티션 로그에 저장되었을 때 'committed' 되었다고 한다.&lt;/li&gt;
&lt;li&gt;Message가 커밋된 이후에는 활성 상태의 복제본이 최소 1대의 Broker에 존재하는 한 데이터가 유실되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka의 0.11.0.0 버전 이전/이후로 메시지 전달 보장 방식(semantic)이 추가되었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 : &lt;b&gt;Producer가 메시지 커밋 응답을 받지 못했을 때 메시지를 resend&lt;/b&gt; 할 수 밖에 없었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 동작은 실제로 메시지가 전달되었음에도 재전송이 발생할 경우 다시 로그에 기록되므로 'At least once'의 의미를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이후 : '&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;idempotent delivery option(멱등 전달)' &amp;amp; 메시지 트랜잭션 기능 &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;idempotent delivery (멱등 전달)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로그에 중복 커밋이 생기지 않도록 각 Producer에게 ID 할당하고, 메시지에 Sequence Number 할당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;해당 동작은 브로커에서 Producer ID + Sequence Number로 메시지를 식별하여, 같은 프로듀서에서 메시지를 중복으로 커밋되지 않게 방지한다.&lt;/li&gt;
&lt;li&gt;이는 'Exactly once'의 의미를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 트랜잭션&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer가 여러 토픽 파티션에 메시지를 send할 때 해당 메시지들을 트랜잭션처럼 묶어서 원자성 보장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 토픽 파티션에 메시지를 전달할 때도 원자성을 보장하여 'Exactly once'의 의미를 가진다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론적으로, Kafka는 기본적으로 'At least once'로 메시지를 받지 못하면 재전송하는 방식을 사용하지만,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Idempotent delivery &amp;amp; 트랜잭션 기능을 통해 메시지를 1번만 보내는 'At exactly once' 방식으로 동작할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;6. Replication&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka replicates the log for each topic's partitions across a configurable number of servers.&lt;br /&gt;This allows automatic failover to these replicas when a server in the cluster fails so messages remain available in the presence of failures.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 각 토픽 파티션들의 로그를 여러 개의 서버에 복제(replicate)한다.&lt;/li&gt;
&lt;li&gt;이러한 기능을 통해 클러스터 내의 서버에 장애가 발생하더라도 복제본으로 복구가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication의 단위는&lt;b&gt; 토픽 파티션&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;각 파티션별로 &lt;b&gt;1개의 Leader Broker와 나머지 Follower Broker 존재&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Produce/Consume(Write/Read) 작업은 모두 Leader Broker에서 처리&lt;/li&gt;
&lt;li&gt;이때 해당 파티션에 쌓이는 메시지의 로그는 Leader Broker, Follower Broker에 모두 적재된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ISR (In-Sync Replica)&lt;/b&gt; : 리더와 동일한 데이터 상태를 유지하며, 리더와 동기화되어 있는 복제본 Follower Broker들의 집합
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader Broker가 나머지 Follower Broker들의 로그 동기화 상태를 모니터링하여 ISR에 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Leader Broker 장애 발생 시 Failover 동작&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 정상적인 시나리오에서 메시지 적재 시 메시지 로그를 Follower Broker에 적재, 동기화된 Follower Broker를 ISR에 포함&lt;/li&gt;
&lt;li&gt;Leader Broker 장애 발생 시 클러스터 내의 'Controller Broker'가 ISR에서 1개의 Follower Broker를 Leader로 승격&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;7.  Log Compaction&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 부분은 저장 공간과 복원의 관점에서 어떻게 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Kafka에서 Log를&lt;span&gt; 저장하고 복원하는지에 관한 설명이 나와있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;일반적인 로그 Retention은 다음과 같이 동작합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Infinite Retension&amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영구적으로 모든 업데이트 로그를 저장하고 삭제하지 않는다.&lt;/li&gt;
&lt;li&gt;이 경우에 사용하지 않는 데이터들도 모두 저장되므로 저장 공간 문제가 생기고 실용적이지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Simple Retension (일정 기간동안만 저장)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기간을 설정하여 로그를 저장하고 기간이 지나면 해당 로그를 삭제&lt;/li&gt;
&lt;li&gt;일정 시간 후에는 로그가 삭제되므로 해당 시간 후 과거 로그를 재현하기 어려워진다.&lt;/li&gt;
&lt;li&gt;ex) 업데이트가 잦지 않은 유저 정보의 업데이트 내역이 Retension 이후에 사라져서, 해당 유저 정보가 사라진다면 재현이 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Kafka의 Log Compaction&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Kafka에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;위의 2가지 방식의 문제점을 절충한 Log Compaction 방식을 사용한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Log compaction is a mechanism to give finer-grained per-record retention, rather than the coarser-grained time-based retention. &lt;br /&gt;The idea is to selectively remove records where we have a more recent update with the same primary key. This way the log is guaranteed to have at least the last state for each key.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Log Compaction은 시간 기반 Retension이 아닌 Record별 보존이다.&lt;/li&gt;
&lt;li&gt;Log가 적재될 때 동일한 Primary Key로 최근 업데이트된 Log를 제거한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서, Log에는 해당 Primary Key의 가장 최근 상태가 영구적으로 보존된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이러한 방식을 통해 저장 공간도 효율적으로 사용하고, 업데이트가 잦지 않은 로그도 최근 상태를 보존하게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0asp7/btsLqEESwM8/g3r78T5x22imvPgD3htIh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0asp7/btsLqEESwM8/g3r78T5x22imvPgD3htIh0/img.png&quot; data-alt=&quot;Kafka Log Compaction&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0asp7/btsLqEESwM8/g3r78T5x22imvPgD3htIh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0asp7%2FbtsLqEESwM8%2Fg3r78T5x22imvPgD3htIh0%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;399&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Log Compaction&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Kafka 공식문서의 Design 챕터를 살펴봤습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 부분이고 공식문서를 번역하여 나열한 수준이지만 Kafka의 기본 Design을 살펴볼 수 있었습니다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/161</guid>
      <comments>https://ksh-coding.tistory.com/161#entry161comment</comments>
      <pubDate>Sat, 21 Dec 2024 16:19:46 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Apache Kafka 공식 문서 살펴보기 (기본 이론)</title>
      <link>https://ksh-coding.tistory.com/160</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 MSA 프로젝트를 진행할 때, Kafka를 사용해본 적이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그때는 먼저 구현을 했어야 했기에 제대로 된 Kafka의 이론은 모른채 구현만 쫓아갔던 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 실무에서도 Kafka를 사용하고 있는 시점이고 개인적으로도 어떤 기술이고 어떤 원리인지 궁금하기 때문에 자세히 알아보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 하나의 기술을 알아볼 때,  실전 -&amp;gt; 이론으로 배우는 것이 빠를 수 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;기술을 공부할 때 저는 무조건 기술의 공식문서가 1순위라고 생각하기 때문에 공식문서를 저만의 언어로 풀어서 포스팅해보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(거의 번역본일수도.. ㅎㅎ;;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Kafka 기술의 공식문서 링크는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kafka.apache.org/documentation/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731738251264&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;Apache Kafka&quot; data-og-description=&quot;Apache Kafka: A Distributed Streaming Platform.&quot; data-og-host=&quot;kafka.apache.org&quot; data-og-source-url=&quot;https://kafka.apache.org/documentation/&quot; data-og-url=&quot;https://kafka.apache.org/documentation/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ApLYh/hyXwjIyvqs/nbXVKeu2ldxtaI5WjLES11/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kafka.apache.org/documentation/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ApLYh/hyXwjIyvqs/nbXVKeu2ldxtaI5WjLES11/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Apache Kafka&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka: A Distributed Streaming Platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kafka.apache.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. What is Apache Kafka?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 설명하는 Apache Kafka는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Apache Kafka is an open-source distributed event streaming platform.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카프카는 &amp;lsquo;&lt;b&gt;분산 이벤트 스트리밍 플랫폼&lt;/b&gt;&amp;rsquo;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. What is event streaming?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 'event streaming'이란 뭘까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;event streaming is the practice of capturing data in real-time from event sources.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 스트리밍은 실시간으로 'event source'에서 발생하는 데이터를 캡쳐하는 것이다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;event source는 'event'가 발생하는 곳으로 DB, Software Application, mobile device 등이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Streaming이란, &lt;b&gt;실시간으로 발생하는 'event'를 'event stream' 형태로 저장하여 처리하고 필요한 곳에 전달하는 것을 말합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 다음과 같은 효과를 보장합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 데이터를 연속적으로 수집, 처리하는 데이터 흐름 보장&lt;/li&gt;
&lt;li&gt;단순히 데이터를 전달 뿐만이 아닌 데이터를 분석하고 해석하는 구조 제공 (데이터 처리)&lt;/li&gt;
&lt;li&gt;처리 지연 없이 적절한 시점에 적절한 위치로 데이터 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. What is event stream? (event? stream?)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 'event stream'이란 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'event stream'은 말 그대로, 'event'와 'stream'의 개념을 합친 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;event : 어떤 일이 발생했다는 사실 (ex : 회원 탈퇴, 주문 취소, 로그인 성공, ...)&lt;/li&gt;
&lt;li&gt;stream : 시간에 따라 연속적으로 발행하는 데이터의 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 'event stream'은 &lt;b&gt;연속적으로 발생하는 Event의 흐름&lt;/b&gt;을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &lt;b&gt;Event를 연속적으로 발생하는 stream으로 저장하여 이를 가공 및 처리할 수 있도록 합니다.&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;&lt;b&gt;1-3. What is Kafka 'event streaming platform' meaning?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Kafka는 어떠한 원리로 'Event Streaming Platform'을 구성할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 다음과 같은 3가지 Core Concept을 사용해서 'Event Streaming Platform'을 구성합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. To publish (write) and subscribe to (read) streams of events.&lt;br /&gt;2. To store streams of events durably and reliably for as long as you want.&lt;br /&gt;3. To process streams of events as they occur or retrospectively.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event Stream을 Publish/Subscribe한다. (Pub/Sub 구조)&lt;/li&gt;
&lt;li&gt;원하는 시간만큼 Event Stream을 안전하게 저장한다.&lt;/li&gt;
&lt;li&gt;실시간 Event Stream, 과거의 Event Stream을 처리할 수 있다. (실시간 처리, 재처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. How does Kafka Work? (Internals, Server-Client)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 Kafka는 어떻게 동작하는지 내부 구조(Internal)를 살펴봅시다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka is a distributed system consisting of servers and&amp;nbsp;clients that communicate via a high-performance&amp;nbsp;TCP network protocol.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka는 높은 성능의 TCP Protocol로 통신하는 여러 Server, Client로 구성된 분산 시스템&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. Servers&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka is run as a cluster of one or more servers that can span multiple datacenters or cloud regions.&amp;nbsp;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka는 확장 가능한 하나 이상의 서버로 이루어진 'Cluster' 형태로 동작한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 Kafka Cluster 내의 서버는 Broker를 의미한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Broker&lt;/b&gt; : Event Stream을 저장하는 Storage Layer 역할의 Server&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 서버로는,&lt;b&gt; Kafka Connect Server&lt;/b&gt;가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka Connect Server&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Kafka Connector를 실행시키는 Server
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Connect란,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지속적으로 Event Stream을 Import/Export해서 외부 시스템(DB)나 다른 Kafka Cluster를 통합하는 것&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;a Kafka cluster is highly scalable and fault-tolerant.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Cluster는 높은 확장성과 실패 시에도 정상적으로 동작할 수 있다.&lt;/li&gt;
&lt;li&gt;1개의 서버에 장애가 발생하더라도 다른 서버에서 작업을 이어받아서 데이터의 손실 없이 작업을 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. Clients&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Client를 통해 Kafka Cluster와 상호작용하면서 Event를 Pub/Sub 할 수 있다.&lt;/li&gt;
&lt;li&gt;이를 통해 Event Stream을 Pub/Sub하고 가공 및 처리할 수 있는 분산 애플리케이션과 마이크로서비스를 구축할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 Server-Client 구조를 간략하게 표현해보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Kafka Connect는 생략)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2511&quot; data-origin-height=&quot;933&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TNObL/btsKMmQ09OT/C9WsnST8jRRK7ClGmsPgEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TNObL/btsKMmQ09OT/C9WsnST8jRRK7ClGmsPgEk/img.png&quot; data-alt=&quot;Kafka Server-Client&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TNObL/btsKMmQ09OT/C9WsnST8jRRK7ClGmsPgEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTNObL%2FbtsKMmQ09OT%2FC9WsnST8jRRK7ClGmsPgEk%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;2511&quot; height=&quot;933&quot; data-origin-width=&quot;2511&quot; data-origin-height=&quot;933&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Server-Client&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Components &amp;amp; Terms&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 본격적으로 Kafka에서 사용되는 구성 요소 및 사용되는 용어에 대해서 간략하게 알아보도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-1. Event&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event는 앞서 잠깐 살펴봤지만, 좀 더 구체적인 예시로 설명해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;An event&amp;nbsp;records the fact that &quot;something happened&quot; in the world or in your business.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event는 서비스에서 '어떤 일이 발생했다는 사실'을 나타낸다.&lt;/li&gt;
&lt;li&gt;Event는 다음과 같은 요소로 구성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key&lt;/li&gt;
&lt;li&gt;Value&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Optional Metadata Headers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731741766333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* Bank Application
* Event : 'Alice가 Bob에게 200$를 송금했다.' (2020년 6월 25일 오후 2시 6분)

Event key: &quot;Alice&quot;
Event value: &quot;Made a payment of $200 to Bob&quot;
Event timestamp: &quot;Jun. 25, 2020 at 2:06 p.m.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼, Event는 서비스의 비즈니스에 따라 다양한 내용으로 나타날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-2. Producer &amp;amp; Consumers&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Producers&lt;/b&gt; are those client applications that publish (write) events to Kafka, &lt;br /&gt;and &lt;b&gt;consumers&lt;/b&gt;&amp;nbsp;are those that subscribe to (read and process) these events.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer와 Consumer는 모두 &lt;b&gt;Kafka의 Client Application&lt;/b&gt;이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer&lt;/b&gt; : Event를 Publish(Write)하는 Client Application&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer&lt;/b&gt; : Event를 Subscribe(Read)하는 Client Application&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kafka는 높은 확장성을 위해&lt;b&gt; Producer와 Consumer가 분리되어서 결합을 가지지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;따라서, Producer는 Consumer 컨디션과 상관없이 Event를 Publish 할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-3. Topics&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Events are organized and durably stored in &lt;b&gt;topics.&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Event는 Topic 내에 저장된다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event : File System의 &lt;b&gt;file&lt;/b&gt;과 유사&lt;/li&gt;
&lt;li&gt;Topic : File System의 &lt;b&gt;Folder&lt;/b&gt;와 유사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Topic들은 항상 여러 Producer와 여러 Consumer를 가질 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 토픽은 Producer, Consumer가 없을 수도 있고 여러 개일 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Topic 내에 저장된 Event들은 전통적인 Message System과 달리, 사용자가 원하는 만큼 재소비 할 수 있다. (이벤트를 소비 후 버리지 않을 수도 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3-4. Partitions&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Topics are &lt;b&gt;partitioned&lt;/b&gt;, meaning a topic is spread over a number of &quot;buckets&quot; located on different Kafka brokers.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Topic은  파티셔닝되어 각 파티션에 각각 다른 Broker가 할당된다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 1개당 1개의 Kafka Broker 할당 (정확히는 Leader Broker 1개 할당, 뒤에 챕터에서 설명)&lt;/li&gt;
&lt;li&gt;따라서, 같은 Topic이라도 다른 Partition이라면 서로 다른 Kafka Broker를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 Topic 내에 여러 Partition이 존재&lt;/b&gt;한다고 이해하면 이해하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;새로운 Event가 하나의 Topic에 Publish 되면 Topic 내의 하나의 Partition에 적재되는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;Topic을 파티셔닝하는 'Partition' 개념을 도입한 이유는 이러한 '데이터 분산 적재'가 확장성에 매우 중요하기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; Topic 내의 여러 파티션이 존재하고, 파티션별로 Broker가 여러 개 할당되어 있다.&lt;/li&gt;
&lt;li&gt;이를 통해 &lt;b&gt;Client Application은 하나의 Topic 내의 Event를 사용하고자 할 때, 여러 Broker를 사용하여 동시에 여러 데이터를 읽고 쓸 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;만약, 하나의 Topic 내에 파티션 없이 하나의 Broker가 할당되었다면 병렬 처리가 힘들고 하나의 Broker에 부하가 집중될 수 있다.&lt;/li&gt;
&lt;li&gt;Kafka는&amp;nbsp;&lt;b&gt;여러 파티션에 Broker를 1개씩 할당하여 데이터를 분산하면서 병렬 처리 및 부하 분산, 높은 확장성을 가진다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event가 Publish되어 Kafka 내부에 저장되는 구조를 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;1182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkBpcN/btsKL704Xtj/877SewI4OEsYewcaJ0ktD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkBpcN/btsKL704Xtj/877SewI4OEsYewcaJ0ktD1/img.png&quot; data-alt=&quot;Kafka Topic, Partition&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkBpcN/btsKL704Xtj/877SewI4OEsYewcaJ0ktD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkBpcN%2FbtsKL704Xtj%2F877SewI4OEsYewcaJ0ktD1%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;2184&quot; height=&quot;1182&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;1182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Topic, Partition&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Event는 Topic 내부 Partition에 저장됩니다.&lt;/li&gt;
&lt;li&gt;Partition에 저장되는 순서는 기본적으로 라운드로빈 방식으로 저장됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 1 -&amp;gt; 2 -&amp;gt; 3 -&amp;gt; 4 순서&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Event Consume 순서 불일치 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 하나의 Topic 내의 Event가 여러 Partition에 라운드로빈 방식으로 저장됨에 따라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client Application에서 해당 Event를 Consume 시 &lt;b&gt;저장된 Event 순서와 Consume한 Event 순서가 불일치하는 문제가 발생할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해, 예시 상황을 가정해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1개의 토픽에 3개의 파티션 존재 (Partition 0, Partition 1, Partition 2)&lt;/li&gt;
&lt;li&gt;Event 1 ~ Event 6 순서로 6개가 들어왔다고 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 경우에 각 파티션에 적재되는 Event는 다음과 같을 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 0 : Event 1, Event 4&lt;/li&gt;
&lt;li&gt;Partition 1 : Event 2, Event 5&lt;/li&gt;
&lt;li&gt;Partition 2 : Event 3, Event 6&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Event를 소비하는 Consumer는 여러 개로 구성하여 Event 처리 성능을 높입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;b&gt;여러 Consumer가 Event를 처리하는 속도나 네트워크 지연과 같은 변수 상황에 의해 Event 소비 순서가 달라질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition 1을 담당하는 Consumer에 문제가 생겨 지연이 발생했다고 가정해보면 다음과 같이 소비됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event 적재 순서 : Event 1 -&amp;gt; Event 2 -&amp;gt; Event 3 -&amp;gt; Event 4 -&amp;gt; Event 5 -&amp;gt; Event 6&lt;/li&gt;
&lt;li&gt;실제 Event 소비 순서 : Event 1 -&amp;gt; Event 3 -&amp;gt; Event 4 -&amp;gt; Event 6 -&amp;gt; Event 2 -&amp;gt; Event 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 상황은 비즈니스에 따라 치명적일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 간단하게 주문 상태 관련 Topic에서 사용자가 주문 후에 주문 취소를 했다고 가정해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event 적재 순서 : '주문 완료' -&amp;gt; '주문 취소'&lt;/li&gt;
&lt;li&gt;실제 Event 소비 순서 : '주문 취소' -&amp;gt; '주문 완료'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 Event가 소비되면, 실제 주문이 이루어지기 때문에 서비스에 심각한 문제를 초래할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Event Consume 순서 불일치 문제 해결 - Event Key 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 Event를 Produce할 때 Event Key를 설정해서 보내면, 해당 Event는 정해진 Partition에만 적재됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Key를 해싱하여 적재할 Partition을 정한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 해당 Event는 Partition 1개에만 적재되어 1개의 Consumer에서만 소비되기 때문에 이벤트 순서가 보장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현은 이론을 다루는 글이다보니 생략하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Main Concepts&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 간략하게 각 구성 요소들의 Main Concept에 대해서 소개해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&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: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-1. Event를 필요한 만큼 소비 가능&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Events in a topic can be read as often as needed&amp;mdash;unlike traditional messaging systems, events are not deleted after consumption.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Topic 내의 Event들은 전통적인 다른 메시징 시스템과 달리, &lt;b&gt;소비 후에 사라지지 않기 때문에 필요한 만큼 여러 번 읽을 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt; Topic당 Event 유지 기간을 설정하여 그 기간만큼 Event를 보관할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-2. 동일 Event Key는 동일 Partition 저장 보장 &amp;amp; 파티션 내 이벤트 Consume 순서 보장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 하나의 Topic 내의 Event가 여러 Partition에 라운드로빈 방식으로 저장됨에 따라,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Client Application에서 해당 Event를 Consume 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장된 Event 순서와 Consume한 Event 순서가 불일치하는 문제가 발생할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해, 예시 상황을 가정해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1개의 토픽에 3개의 파티션 존재 (Partition 0, Partition 1, Partition 2)&lt;/li&gt;
&lt;li&gt;Event 1 ~ Event 6 순서로 6개가 들어왔다고 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 경우에 각 파티션에 적재되는 Event는 다음과 같을 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 0 : Event 1, Event 4&lt;/li&gt;
&lt;li&gt;Partition 1 : Event 2, Event 5&lt;/li&gt;
&lt;li&gt;Partition 2 : Event 3, Event 6&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 Event를 소비하는 Consumer는 여러 개로 구성하여 Event 처리 성능을 높입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여러 Consumer가 Event를 처리하는 속도나 네트워크 지연과 같은 변수 상황에 의해 Event 소비 순서가 달라질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Partition 1을 담당하는 Consumer에 문제가 생겨 지연이 발생했다고 가정해보면 다음과 같이 소비됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event 적재 순서 : Event 1 -&amp;gt; Event 2 -&amp;gt; Event 3 -&amp;gt; Event 4 -&amp;gt; Event 5 -&amp;gt; Event 6&lt;/li&gt;
&lt;li&gt;실제 Event 소비 순서 : Event 1 -&amp;gt; Event 3 -&amp;gt; Event 4 -&amp;gt; Event 6 -&amp;gt; Event 2 -&amp;gt; Event 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 상황은 비즈니스에 따라 치명적일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 간단하게 주문 상태 관련 Topic에서 사용자가 주문 후에 주문 취소를 했다고 가정해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event 적재 순서 : '주문 완료' -&amp;gt; '주문 취소'&lt;/li&gt;
&lt;li&gt;실제 Event 소비 순서 : '주문 취소' -&amp;gt; '주문 완료'&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 Event가 소비되면, 실제 주문이 이루어지기 때문에 서비스에 심각한 문제를 초래할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이러한 문제를 Kafka는 'Event Key'를 설정하여 해결합니다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Events with the same event key (e.g., a customer or vehicle ID) are written to the same partition, and Kafka&amp;nbsp;&lt;br /&gt;guarantees that any consumer of a given topic-partition will always read that partition's events in exactly the same order as they were written.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 Event Key를 가진 Event는 동일한 Partition에 적재된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, Event Key는 이벤트를 특정 Partition에 할당하는 데 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kafka는 Partition 내 Event가 기록된 순서를 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4-3. Topic 복제 기능&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;To make your data fault-tolerant and highly-available, every topic can be replicated.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애 시 안정성과 높은 가용성을 위해서 모든 토픽은 복제될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 Kafka 공식문서에 나와 있는 기본적인 Kafka의 개념들을 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 좀 더 심화적인 내부 원리, 설계에 관해서 다뤄보도록 하겠습니다! :)&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Apache kafka</category>
      <category>Consumer</category>
      <category>Kafka</category>
      <category>Producer</category>
      <category>topic</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/160</guid>
      <comments>https://ksh-coding.tistory.com/160#entry160comment</comments>
      <pubDate>Sat, 30 Nov 2024 17:49:11 +0900</pubDate>
    </item>
    <item>
      <title>[PostgreSQL] 2. PostgreSQL Index 알아보기 (Index Type, Index Scan)</title>
      <link>https://ksh-coding.tistory.com/159</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 챕터에서 PostgreSQL의 Internal, 내부 구조에 대해서 간략하게 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 PostgreSQL에는 &lt;b&gt;어떤 Index Type이 있는지, Index Scan은 어떤 것이 있는지&lt;/b&gt; 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index는 깊게 들어가면 너무나 어려운 내용이라서 간략하게만 다뤄보도록 하겠습니다 :(&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;('PostgreSQL'의 Index에 대해서 알아보는 것이기 때문에, 기본 Index에 관한 개념은 알고 있다고 가정하고 다루지 않겠습니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 Index를 공부하면서 목표로 한 점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 PostgreSQL Index Type의 동작방식, 내부 원리를 설명할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 PostgreSQL 타입, 상황에 대해 어떤 Index를 적용할지 떠올릴 수 있다.&lt;/li&gt;
&lt;li&gt;쿼리 실행 계획에서 마주하는 Scan 방식에 대해 왜 해당 Scan이 적용되었는지 떠올릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 PostgreSQL의 공식문서를 통해서 공부했지만, 다소 내용이 적고 어려운 느낌이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 PostgreSQL의 주요 Contributor로 이루어진 PostgresPro 팀에서 출판한 `PostgreSQL 14 Internals`를 읽고 공부한 부분을 작성해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문서는 아래 링크에서 무료 PDF로 받을 수 있고, 책을 구매할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://postgrespro.com/community/books/internals&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://postgrespro.com/community/books/internals&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731126225122&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;PostgreSQL 14 Internals&quot; data-og-description=&quot;Postgres Professional is a PostgreSQL company delivering Postgres Pro DBMS and all kinds of PostgreSQL professional services worldwide&quot; data-og-host=&quot;postgrespro.com&quot; data-og-source-url=&quot;https://postgrespro.com/community/books/internals&quot; data-og-url=&quot;https://postgrespro.com/community/books/internals&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://postgrespro.com/community/books/internals&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://postgrespro.com/community/books/internals&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;PostgreSQL 14 Internals&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Postgres Professional is a PostgreSQL company delivering Postgres Pro DBMS and all kinds of PostgreSQL professional services worldwide&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;postgrespro.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. PostgreSQL Index Types&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 PostgreSQL의 다양한 Index들에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는 다양한 Index 타입이 존재하는데, 인덱스를 생성할 때 타입을 지정해서 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다음과 같이 Index를 생성할 때, 타입을 지정하지 않으면 B-Tree 인덱스 타입이 지정되어 생성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731126755091&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX member_name_idx ON member (name);&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;타입을 지정하기 위해서는 'USING' 키워드를 사용하여 타입을 명시하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731126899819&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX member_name_idx ON member USING HASH(name);&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;b&gt;각 Index Type은 모두 다른 알고리즘을 사용해서 인덱싱하므로 인덱스 타입에 따라 저장되는 형태가 달라질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;또, Index Type에 따라 지원하는 연산자가 다르기 때문에 데이터, 상황에 맞는 인덱스 타입을 사용하는 것이 중요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 각 타입의 인덱싱 구조와 지원하는 연산자를 알아보면서 어떤 상황, 데이터에 적합한지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. Hash Index&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash Index는 Key-Value 기반의 Hash Table 형태로 디스크에 데이터를 인덱싱합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 Hash Index의 구조를 도식화하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2399&quot; data-origin-height=&quot;1876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhO2G2/btsKDRiYHhf/9hL08oq6KIbVtN6zYEHzSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhO2G2/btsKDRiYHhf/9hL08oq6KIbVtN6zYEHzSk/img.png&quot; data-alt=&quot;Hash Index 내부 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhO2G2/btsKDRiYHhf/9hL08oq6KIbVtN6zYEHzSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhO2G2%2FbtsKDRiYHhf%2F9hL08oq6KIbVtN6zYEHzSk%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;2399&quot; height=&quot;1876&quot; data-origin-width=&quot;2399&quot; data-origin-height=&quot;1876&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hash Index 내부 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 데이터 값들은 Hash Function에 의해 해싱되어 Disk 내의 Bucket(Hash Table) 형태로 저장됩니다.&lt;/li&gt;
&lt;li&gt;Key-Value 형태로, Key는 해싱된 데이터 값을 의미하고 Value로는 해당 Tuple이 있는 TID를 가집니다.&lt;/li&gt;
&lt;li&gt;정렬된 순서를 보장하지 않기 때문에, &lt;b&gt;범위 연산(&amp;lt;, &amp;gt;, &amp;gt;=, &amp;lt;=)을 지원하지 않고 동등 연산(=)만 지원합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash Index는 상대적으로 간단한 구조이고 인덱스 크기도 작다는 장점이 있지만, 일반적으로 자주 사용하지 않습니다.&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;※ Hash Index 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;범위 연산 없이 동등 연산만을 사용하는 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2.  B-Tree Index&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree Index는 PostgreSQL 뿐만 아니라 다른 DB 벤더도 기본 Index Type으로 할만큼 자주 사용되는 Index Type입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 B-Tree Index의 구조를 도식화하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4126&quot; data-origin-height=&quot;1347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWOrZ9/btsKDFC5tr3/Sq3rSxn67g6yorjT3m8B21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWOrZ9/btsKDFC5tr3/Sq3rSxn67g6yorjT3m8B21/img.png&quot; data-alt=&quot;B-Tree 내부 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWOrZ9/btsKDFC5tr3/Sq3rSxn67g6yorjT3m8B21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWOrZ9%2FbtsKDFC5tr3%2FSq3rSxn67g6yorjT3m8B21%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;4126&quot; height=&quot;1347&quot; data-origin-width=&quot;4126&quot; data-origin-height=&quot;1347&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;B-Tree 내부 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Balanced Tree의 구조로 이루어져 있습니다.&lt;/li&gt;
&lt;li&gt;B Tree는 하나의 노드에 여러 값을 가질 수 있고, 2개 이상의 자식 노드를 가질 수 있습니다. (그림은 이해를 돕기 위해 이진 트리처럼 1개의 값, 2개의 자식 노드로 가정했습니다.)&lt;/li&gt;
&lt;li&gt;노드는 3종류로 나뉘며, 모두 다른 역할을 수행합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Root Node&lt;/b&gt; : 처음의 검색 범위를 구분하기 위한 Index Key, 다음 자식 Node로 이동하기 위한 Pointer 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Branch Node&lt;/b&gt; : 다음 Node를 선택하기 위한 Index Key, 다음 자식 Node로 이동하기 위한 Pointer 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Leaf Node&lt;/b&gt; : 검색 Key에 해당하는지를 나타내는 Index Key, 실제 데이터가 위치한 TID 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Root -&amp;gt; Branch -&amp;gt; Leaf Node를 거쳐 찾고자 하는 데이터의 TID를 찾는 구조입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 특성은 위와 같고, &lt;b&gt;Index에서 중요한 특성은 다음과 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Root -&amp;gt; Branch -&amp;gt; Leaf로의 빠른 탐색을 위해 모든 요소는 정렬되어 있습니다.&lt;/li&gt;
&lt;li&gt;균형 트리이기 때문에 모든 Leaf Node는 같은 Depth를 가지고, 따라서 모든 값의 검색 시간은 같습니다.&lt;/li&gt;
&lt;li&gt;각 노드에 저장할 수 있는 데이터 크기가 크고, 여러 개의 값을 저장할 수 있기 때문에 데이터가 많더라도 Depth는 항상 적습니다.&lt;/li&gt;
&lt;li&gt;Leaf Node는 정렬된 양방향 리스트로 설계되어 있기 때문에 리프 노드를 한 방향으로 탐색하면 정렬된 데이터 셋을 얻을 수 있습니다.&lt;/li&gt;
&lt;li&gt;동등 연산자와 범위 비교 연산자를 지원합니다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=&lt;/li&gt;
&lt;li&gt;&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;gt;=&lt;/li&gt;
&lt;li&gt;&amp;lt;&lt;/li&gt;
&lt;li&gt;&amp;lt;=&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 데이터가 균형 분포 및 정렬되어 있어 성능이 준수하고 동등 연산, 범위 비교 연산을 지원한다는 점에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash보다 B Tree를 기본 Index Type으로 지정한 것이 아닐까 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ B Tree 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 B Tree는 다양한 상황, 데이터에 적절하기 때문에 다른 Type Index가 적절하지 않을 때 기본적으로 적용하면 좋을 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-3. GiST Index&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GiST는 Generalized Search Tree로,&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;해당 Index는 앞서 소개된 PDF, PostgrePro의 GiST를 소개한 내용의 이미지를 인용하여 설명해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://postgrespro.com/blog/pgsql/4175817&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://postgrespro.com/blog/pgsql/4175817&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731132146972&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Indexes in PostgreSQL &amp;mdash; 5 (GiST)&quot; data-og-description=&quot;In the previous articles, we discussed PostgreSQL indexing engine , the interface of access methods , and two access methods: hash index and B-tree . In this article, we will describe GiST indexes. GiST GiST is an abbreviation of &amp;quot;generalized search tree&amp;quot;.&quot; data-og-host=&quot;postgrespro.com&quot; data-og-source-url=&quot;https://postgrespro.com/blog/pgsql/4175817&quot; data-og-url=&quot;https://postgrespro.com/blog/pgsql/4175817&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sHh1E/hyXwvmYJ6o/1jYvQWgEP4bTJTf3NC6471/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450&quot;&gt;&lt;a href=&quot;https://postgrespro.com/blog/pgsql/4175817&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://postgrespro.com/blog/pgsql/4175817&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sHh1E/hyXwvmYJ6o/1jYvQWgEP4bTJTf3NC6471/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450');&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;Indexes in PostgreSQL &amp;mdash; 5 (GiST)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In the previous articles, we discussed PostgreSQL indexing engine , the interface of access methods , and two access methods: hash index and B-tree . In this article, we will describe GiST indexes. GiST GiST is an abbreviation of &quot;generalized search tree&quot;.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;postgrespro.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 GiST도 Tree 구조이기 때문에 각 Root, Branch, Leaf 노드를 동일하게 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신, 각 노드에서 가지는 정보가 다음과 같이 다릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;B Tree : Index Key &amp;amp; Next Node Pointer&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GiST&lt;/b&gt; : &lt;b&gt;Predicate&lt;/b&gt; &amp;amp; Next Node Pointer&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Predicate가 무엇일까요? 바로 &lt;b&gt;Predicate는 조건을 나타내는 개념&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는 각 데이터에 따라서 정해진 Predicate 조건을 사용합니다. (Predicate 커스텀도 가능한 것으로 알고 있습니다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Geometric Type (point, box, polygon, ...) : &lt;b&gt;각 공간 데이터가 나타내는 최소-최대 사각형 영역(MBR)을 Predicate 조건으로 사용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Predicate 조건을 통해 주어진 값들이 영역에 포함되는지, 교차하는지 등을 판단&lt;/li&gt;
&lt;li&gt;EX) 특정 노드가 (1, 1), (5, 3), (2, 8) 등의 포인트를 포함한다면, Predicate : (1, 1)부터 (5, 8)까지의 범위&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Range Type (int4range, daterange, tsrange, ...) : &lt;b&gt;저장되는 최소-최대 범위를 Predicate 조건으로 사용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Predicate 조건을 통해 주어진 값들이 범위에 포함되는지를 판단&lt;/li&gt;
&lt;li&gt;EX) 노드에 [1, 5), [3, 7), [2, 10)와 같은 범위가 포함되어 있다면, 노드의 Predicate 범위는 [1, 10)으로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Text Search Type (tsvector, tsquery) : &lt;b&gt;해당 데이터의 토큰 집합을 Predicate 조건으로 사용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Predicate 조건을 통해 Text 일치, 포함, 유사도 검색 사용&lt;/li&gt;
&lt;li&gt;EX) 특정 노드에 &quot;cat&quot;, &quot;dog&quot;, &quot;animal&quot; 등의 토큰이 포함된 여러 tsvector가 존재한다면, 노드의 Predicate는 {cat, dog, animal}과 같은 토큰 집합으로 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 Geometric Type을 예시로 들면, 다음과 같이 인덱싱된다고 표현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVjjN/btsKDQK8T51/luPcpVG3X7mwGbronsKNa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVjjN/btsKDQK8T51/luPcpVG3X7mwGbronsKNa0/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4175817&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVjjN/btsKDQK8T51/luPcpVG3X7mwGbronsKNa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVjjN%2FbtsKDQK8T51%2FluPcpVG3X7mwGbronsKNa0%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;900&quot; height=&quot;450&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4175817&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;187&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9fL2/btsKCUufkm4/PfjXGFiKfk5LKSRgvXAFF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9fL2/btsKCUufkm4/PfjXGFiKfk5LKSRgvXAFF0/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4175817&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9fL2/btsKCUufkm4/PfjXGFiKfk5LKSRgvXAFF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9fL2%2FbtsKCUufkm4%2FPfjXGFiKfk5LKSRgvXAFF0%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;187&quot; height=&quot;195&quot; data-origin-width=&quot;187&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4175817&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;해당 데이터는 PostgreSQL의 point 타입으로 나타낼 수 있고, 내부 GiST 구조는 B Tree와 같이 균형 트리로 다음과 같이 표현됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dG3eoA/btsKCxfhioF/K9nI4AN5uF34YQHe7pEXQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dG3eoA/btsKCxfhioF/K9nI4AN5uF34YQHe7pEXQ0/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4175817&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dG3eoA/btsKCxfhioF/K9nI4AN5uF34YQHe7pEXQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdG3eoA%2FbtsKCxfhioF%2FK9nI4AN5uF34YQHe7pEXQ0%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;314&quot; height=&quot;146&quot; data-origin-width=&quot;314&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4175817&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다양한 비정형 데이터를 지원하는 만큼, 데이터 타입에 따라 다양한 연산자들을 지원합니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;&amp;lt;&lt;/li&gt;
&lt;li&gt;&amp;amp;&amp;lt;&lt;/li&gt;
&lt;li&gt;&amp;amp;&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;gt;&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;&amp;lt;|&lt;/li&gt;
&lt;li&gt;&amp;amp;&amp;lt;|&lt;/li&gt;
&lt;li&gt;|&amp;amp;&amp;gt;&lt;/li&gt;
&lt;li&gt;|&amp;gt;&amp;gt;&lt;/li&gt;
&lt;li&gt;@&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;@&lt;/li&gt;
&lt;li&gt;~=&lt;/li&gt;
&lt;li&gt;&amp;amp;&amp;amp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 타입에 따라 사용 예시도 다르고 많기 때문에 아래 문서를 참고하고 연산별 설명은 생략하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(간략하게 요약하면, 데이터가 범위, 영역에 포함되는지 어느 방향에 위치하는지(위, 아래, 오른쪽, 왼쪽)에 대해 판단하는 연산자들 입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/6.3/c09.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/6.3/c09.htm&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731134847465&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Operators&quot; data-og-description=&quot;PostgreSQL Prev Next Postgres provides a large number of built-in operators on system types. These operators are declared in the system catalog pg_operator. Every entry in pg_operator includes the name of the procedure that implements the operator and the &quot; data-og-host=&quot;www.postgresql.org&quot; data-og-source-url=&quot;https://www.postgresql.org/docs/6.3/c09.htm&quot; data-og-url=&quot;https://www.postgresql.org/docs/6.3/c09.htm&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hYrbS/hyXwj09ltm/z37nZhTsAAJkF4j2GxYGk0/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/6.3/c09.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.postgresql.org/docs/6.3/c09.htm&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hYrbS/hyXwj09ltm/z37nZhTsAAJkF4j2GxYGk0/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557');&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;Operators&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL Prev Next Postgres provides a large number of built-in operators on system types. These operators are declared in the system catalog pg_operator. Every entry in pg_operator includes the name of the procedure that implements the operator and the&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.postgresql.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ GiST 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비정형 데이터 (공간 데이터, 범위 데이터, 텍스트 데이터 등)&lt;/li&gt;
&lt;li&gt;포함 요소 검색, 근접 요소 검색과 같은 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-4. SP-GiST&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SP-GiST는 기본적으로 GiST를 기반으로 'SP'라는 이름이 추가된 Index입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SP는 Space Patitioning으로 공간 파티셔닝에 특화된 Index임을 알 수 있습니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 GiST와 달리 공간을 겹치지 않게 나누면서 non-balanced Tree를 형성합니다.&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;SP-GiST에서는 다음과 같은 개념이 GiST의 개념에 대응됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하위 노드의 정보 Pointer : &lt;b&gt;lables&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하위 노드가 만족해야하는 조건 Predicate : &lt;b&gt;prefix&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 개념은 GiST와 동일하고 '공간 파티셔닝', 'non-balanced Tree'가 다르기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 점이 다른지 GiST와 동일한 예시로 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlHbiE/btsKD2Lorbi/WEzZbZdkjK4ZXGR8xUkAkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlHbiE/btsKD2Lorbi/WEzZbZdkjK4ZXGR8xUkAkk/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4220639&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlHbiE/btsKD2Lorbi/WEzZbZdkjK4ZXGR8xUkAkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlHbiE%2FbtsKD2Lorbi%2FWEzZbZdkjK4ZXGR8xUkAkk%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;900&quot; height=&quot;450&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4220639&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PtoIx/btsKCWS6fm1/8Ede7V9AI4XRdEAdXpUHKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PtoIx/btsKCWS6fm1/8Ede7V9AI4XRdEAdXpUHKk/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4220639&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PtoIx/btsKCWS6fm1/8Ede7V9AI4XRdEAdXpUHKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPtoIx%2FbtsKCWS6fm1%2F8Ede7V9AI4XRdEAdXpUHKk%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;395&quot; height=&quot;195&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4220639&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;겹치는 영역으로 분리되었던 GiST와 달리 SP-GiST는 겹치는 영역없이 파티션 단위로 위의 그림처럼 공간을 분리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 구조를 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFArWU/btsKDVTdq5y/iLUbDQd4K0g9iP5NR6U9iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFArWU/btsKDVTdq5y/iLUbDQd4K0g9iP5NR6U9iK/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/4220639&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFArWU/btsKDVTdq5y/iLUbDQd4K0g9iP5NR6U9iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFArWU%2FbtsKDVTdq5y%2FiLUbDQd4K0g9iP5NR6U9iK%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;306&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/4220639&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;(4,4)가 파티셔닝의 중심인 Root Node가 되고 4번 영역에는 데이터가 없을 수 있기 때문에 non-balanced Tree가 형성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속해서 파티션 단위로 분할하여 파티셔닝을 하기 때문에 SP-GiST는 하위 노드 수(가지 수)가 적고 깊은 Depth를 가지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ SP-GiST 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;non-balanced Tree인 만큼, 비대칭인 데이터 ( 큰 범위의 좌표 중 특정 지역에 밀집된 좌표 데이터 등)&lt;/li&gt;
&lt;li&gt;공간 분할이 필요한 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ GiST / SP-GiST 둘 중 어떤 것을 사용?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GiST, SP-GiST 두 개의 인덱스를 살펴보다보면 비슷하다보니 사용 기준을 구분하는게 어려울 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SP-GiST의 '공간 파티셔닝' 특성과 'non-balanced Tree' 특성에 초점을 맞춰 살펴보면 다음과 같이 구분할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포괄적이고 균형적인 데이터 검색 : GiST&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기준 텍스트와 유사한 모든 텍스트 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;계층이 존재하고 (분할이 되는) 비대칭적인 데이터 검색 : SP-GiST
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트의 '접두어'만 일치하는 텍스트 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-5. GIN&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GIN은 Generalized Inverted Index의 약자로, &lt;b&gt;하나의 데이터가 여러 개별 요소로 구성된 데이터 타입을 인덱싱하기 위한 인덱스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 요소를 포함하는 Array Type&lt;/li&gt;
&lt;li&gt;Json의 여러 Key/Value를 포함하는 Json Type(json, jsonb)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GIN의 내부 구조를 이해하기 위해서는 GIN의 약자 중에서 &lt;b&gt;'Inverted'&lt;/b&gt;에 주목해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 하나의 요소를 가지는 데이터를 인덱싱할 때는 다음과 같은 구조로 인덱싱을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 데이터로 Index Key를 생성하고, 해당 데이터가 존재하는 Tuple의 ID (TID)를 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조를 여러 요소가 존재하는 데이터 타입에 사용하면 어떻게 될지 예를 들어서 설명해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 언어 데이터를 Array 타입으로 가지는 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731137500896&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ID | Array
----------------
1  | ['KOREAN', 'ENGLISH', 'JAPANESE']
2  | ['KOREAN']
3  | ['ENGLISH', 'JAPANESE']
4  | ['KOREAN', 'JAPANESE']&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;이때, 각 데이터로 Index Key를 생성하고 TID를 저장하는 기본 구조(B-Tree)를 따른다면 배열 요소 데이터들로 Key가 생성될 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제로는 PostgreSQL에서는 Array 타입에서 B Tree 인덱스를 지원하지 않습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 여러 요소를 포함한 데이터에서의 쿼리는 대부분 '특정 요소가 포함되어 있는지'에 관한 쿼리일 것입니다.&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;b&gt;'특정 요소가 포함되어 있는지'에 관한 연산에 사용하기 위해 GIN은 다음과 같이 인덱싱을 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 컬럼에 존재하는 모든 요소를 Index Key로 사용하고, 해당 요소가 포함된 TID를 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 GIN을 사용했을 때 '특정 요소가 포함되어 있는지'에 관한 연산은 어떻게 처리될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱싱된 해당 요소에 포함된 TID들을 한번에 가져와서 Scan하면 빠르게 데이터 셋을 가져올 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ GIN 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 요소가 포함된 데이터 타입 (Array, Json)&lt;/li&gt;
&lt;li&gt;tsvector에서도 포함된 키워드 확인 같은 상황에서 유리 (고객 리뷰에 특정 키워드가 포함된 리뷰를 빠르게 찾고자 할 때)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GIN 사용 시 키워드별로 인덱싱 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-6. BRIN&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BRIN은 필요한 행들을 빠르게 가져오는 것에 초점을 맞춘 다른 Index들과 달리 다른 목적에 초점을 맞춘 인덱스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BRIN은 &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;BRIN의 내부 구조에서는 '범위'가 핵심 키워드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BRIN은&lt;b&gt; 인덱싱 될 컬럼의 데이터를 여러 개의 '범위' 블록으로 나누어서 인덱싱하는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckiLfL/btsKDRJ7ADJ/BKcxTKQTLD4skZJ1bQBKfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckiLfL/btsKDRJ7ADJ/BKcxTKQTLD4skZJ1bQBKfK/img.png&quot; data-alt=&quot;origin : https://postgrespro.com/blog/pgsql/5967830&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckiLfL/btsKDRJ7ADJ/BKcxTKQTLD4skZJ1bQBKfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckiLfL%2FbtsKDRJ7ADJ%2FBKcxTKQTLD4skZJ1bQBKfK%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;214&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;origin : https://postgrespro.com/blog/pgsql/5967830&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 점은 &lt;b&gt;해당 블록에 데이터들의 TID를 저장하지 않고 블록 범위에 대한 MIN, MAX 요약 정보만 저장&lt;/b&gt;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나이를 나타내는 age 컬럼에 BRIN을 적용하면 나이대별로 블록이 생성됩니다.&lt;/li&gt;
&lt;li&gt;해당 블록은 범위에 속하는 실제 행의 TID가 아닌, 범위의 Min / Max 값만 저장합니다.&lt;/li&gt;
&lt;li&gt;EX) age 20~30 블록 : MIN 20 / MAX 30 정보만 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식을 통해 가지는 이점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 인덱스 타입에 비해 요약 정보만 저장하기 때문에 &lt;b&gt;인덱스 크기가 매우 작다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;약 3천만 건의 데이터가 있는 테이블에서 B-Tree Index는 210MB, BRIN은 180KB의 크기를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 실제로 TID를 가지고 있지 않다면 어떻게 찾고자 하는 데이터를 가져오는지 궁금할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BRIN을 사용했을 때는 다음과 같이 생각하면 이해에 도움이 될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BRIN은 찾고자 하는 데이터 셋을 가져오는 데 사용되지 않고, 찾고자 하는 데이터 셋의 범위를 가져오는데 사용된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, &lt;b&gt;BRIN을 사용한 컬럼에서 조건 검색이 들어오면 해당 조건에 해당하는 블록을 도출한 후 해당 범위를 Scan합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 해당 범위에 검색 조건과 맞지 않는 데이터들이 존재할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EX) 검색조건이 age &amp;le; 25일 때, BRIN의 범위 정보가 20~30일 수 있다.&lt;/li&gt;
&lt;li&gt;이때는 age가 25보다 큰 데이터를 제외하는 Sequential Scan을 통해 필터링하여 20~25의 데이터들만 추출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ BRIN 적절한 상황, 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연속적이고 규칙적인 데이터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BRIN의 범위 블록은 데이터가 저장된 순서에 따라 그대로 파티셔닝하는 느낌으로 나뉩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;따라서, 정렬되지 않은 불연속적인 데이터가 하나의 범위 블록으로 묶인다면 데이터 오차가 커질 것입니다.&lt;/li&gt;
&lt;li&gt;그에 따라 Sequential Scan이 많이 발생하여 성능이 더 느릴 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. PostgreSQL Index  Scan&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 PostgreSQL의 다양한 Index Type에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Index Type에서는 데이터가 어떻게 인덱싱이 되는지 타입별로 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱싱된 데이터는 실제로 어떻게 Scan이 되는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 Index를 사용하면 무조건 Index Scan이 발생하는 줄 알고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 테이블에 인덱스를 생성하고 실행 계획을 분석한 순간 다음과 같이 예상치 못한 Scan이 나와 당황한 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(테스트 용이라 테이블도 가장 간단하게 생성했었습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1731142309370&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 테스트용 member 테이블 생성
CREATE TABLE member (
    id    BIGINT PRIMARY KEY,
    name  VARCHAR
);

-- member name 인덱스 생성
CREATE INDEX member_name_idx ON member (name);

-- seongha1, seognha2, seongha3 순으로 name을 가지는 member 100000만건 생성
... 생략

-- 'seongha1'인 이름 찾기
SELECT * FROM member WHERE name = 'seongha1'&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-origin-width=&quot;1130&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnWFfz/btsKDd1pGLw/FxP7dbOehlIwoPYgql8hL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnWFfz/btsKDd1pGLw/FxP7dbOehlIwoPYgql8hL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnWFfz/btsKDd1pGLw/FxP7dbOehlIwoPYgql8hL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnWFfz%2FbtsKDd1pGLw%2FFxP7dbOehlIwoPYgql8hL0%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;1130&quot; height=&quot;410&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index 키워드가 Bitmap Index Scan으로 있긴 한데, 다른 형태의 Index Scan이 존재한다는 것을 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 이번에는 PostgreSQL에서 데이터가 Scan되는 다양한 방식에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. Index Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 가장 기본적인 Index Scan을 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 기본적인 Index Scan은 다음과 같이 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index가 설정된 데이터를 조회할 때 조건과 일치하는 데이터의 TID를 반환&lt;/li&gt;
&lt;li&gt;TID를 기반으로 실제 데이터를 찾아서 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반적으로는&lt;/b&gt; Index가 설정된 데이터를 조회할 때 Index Scan을 통해 데이터를 읽어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터를 Full Scan하는 대신  인덱싱된대로 데이터를 가져올 수 있기 때문에 Disk 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;Index Scan은 가장 기본적인 인덱스를 사용했을 때의 Scan이므로 별다른 설명없이 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. Bitmap Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 일반적이지 않은 앞서 든 예시에서 나왔던 Bitmap Scan은 어떤 Scan일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bitmap Scan을 이해하기 위해서는 먼저 Index Scan이 비효율적인 상황을 이해해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 상황은 &lt;b&gt;'인덱스 저장 순서와 실제 데이터의 저장 순서 유사도'인 Correlation&lt;/b&gt;과 관련이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 B Tree Index를 사용한다고 했을 때 &lt;b&gt;Index는 정렬된 상태를 유지하지만, 실제 데이터는 아닐 수 있습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱싱된 데이터가 정렬된 상태를 유지하더라도, 빠르게 인덱스를 찾을 수 있어서 인덱스를 찾는 Disk I/O를 줄일 뿐입니다.&lt;/li&gt;
&lt;li&gt;실제 데이터가 흩어져 있다면 &lt;b&gt;실제 데이터를 가져오기 위한 페이지 이동이 잦아져 Disk I/O가 많아질 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이때, 이미 접근한 페이지도 다시 재조회하는 비용이 발생하여 Disk I/O가 많아질 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위의 이유로 Correlation이 작다면, 실제 데이터 저장 순서가 정렬되어 있지 않다면 Index Scan은 비효율적일 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 실제 데이터가 흩어져 있는, &lt;b&gt;Correlation이 작은 상황일 때 잦은 페이지 이동으로 인한 Disk I/O를 줄이는 것이 Bitmap Scan입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 실제 데이터가 흩어져 있을 때 Disk 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;Bitmap Scan은 다음과 같은 2단계로 나뉩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bitmap Index Scan&lt;/li&gt;
&lt;li&gt;Bitmap Heap Scan&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Bitmap Index Scan&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bitmap Index Scan에서는 일반적인 Index Scan처럼 다음과 같은 동작을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index를 통해 조건과 일치하는 데이터의 TID 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Index Scan과 다른 점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바로 실제 데이터를 조회하지 않고, TID를 Bitmap에 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Bitmap은 데이터를 효율적으로 관리하기 위해 생성되는 PostgreSQL의 메모리 내 자료 구조로, 해당 Bitmap에 인덱스에 해당하는 TID들을 저장하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Bitmap Heap Scan&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져올 실제 데이터 TID들이 있는 Bitmap이 Bitmap Index Scan을 통해 생성되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bitmap Heap Scan 단계에서 Bitmap 기반으로 가져올 데이터의 페이지들을 읽고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 페이지에서 가져올 데이터들을 모두 읽어서 가져옵니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동작방식은 다음과 같은 이점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;한번 페이지를 접근했을 때, 해당 페이지에 존재하는 조건에 맞는 데이터를 모두 가져오기 때문에 페이지 재조회가 일어나지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Index Scan의 페이지 재조회 Disk I/O를 줄여서 더 최적의 성능을 제공하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 위의 예시에서 Optimizer는 Index Scan 대신 Bitmap Scan을 선택했던 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Index Scan VS Bitmap Scan&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 일반적으로 Bitmap Scan이 항상 더 Index Scan보다 성능이 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bitmap Scan도 결국 Bitmap을 생성하는 비용을 생각해야 합니다.&lt;/p&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;Bitmap Scan은 Bitmap을 통해 Index Scan의 페이지 재조회 Disk I/O를 줄여서 특정 상황에 유리할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반대로, Index Scan에서 페이지 재조회가 일어나지 않는다면 Bitmap 생성 비용이 더 커져 Bitmap Scan이 비효율적일 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 간단하게 각 Scan이 유리한 상황을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index Scan
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 데이터가 정렬되어 있어서 이미 조회한 페이지를 재조회하지 않는 경우 (Correlation이 큰 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bitmap Scan
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 데이터가 정렬되어 있지 않고 Index Scan 시 여러 번 페이지를 재조회해야 하는 경우 (Correlation이 작은 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. Sequential Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequential Scan은 PostgreSQL에서 수행하는 Table Full Scan이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로, 테이블의 모든 Tuple을 순차적으로 Scan하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Index가 존재하지 않을 때 사용되는 Scan입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, Index가 존재하더라도 인덱스 페이지에 접근하는 비용이 테이블을 Full Scan하는 Sequntial Scan 비용보다 클 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 Sequential Scan이 적용되게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-4. Index-Only Scan&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index-Only Scan은 조금 특별한 Scan입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로, Covering Index가 적용될 때 사용되는 Scan 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Covering Index란, &lt;b&gt;쿼리에 사용되는 정보를 모두 가지고 있는 Index&lt;/b&gt;를 말합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731737000171&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 테스트용 member 테이블 생성
CREATE TABLE member (
    id    BIGINT PRIMARY KEY,
    name  VARCHAR
);

-- member name 인덱스 생성
CREATE INDEX member_name_idx ON member (name);

-- seongha1, seognha2, seongha3 순으로 name을 가지는 member 100000만건 생성
... 생략

-- 'seongha1'인 이름 찾기
SELECT * FROM member WHERE name = 'seongha1'&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;위의 예시에서 'name' 컬럼에 Index를 생성했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 생성될 때 name 컬럼의 값이 Index Key가 되어 인덱스에 저장될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 쿼리에 집중해보면 'SELECT *'로 name 이외의 해당 테이블의 모든 컬럼을 select 해야하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 인덱스 Key로 name의 값이 있다고 하더라도 TID로 해당하는 Tuple을 찾아서 다른 컬럼의 데이터도 가져와야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 'SELECT name'으로 name만 가져와도 된다면 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TID로 Tuple을 찾을 필요 없이 인덱스의 Key로만 결과를 반환하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 테이블에 접근하는 Disk I/O를 줄일 수 있는 것이 Covering Index, Index-Only Scan입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 PostgreSQL의 다양한 Index Type과 Index Scan에 대해서 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 시작할 때 가졌던 목표는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 PostgreSQL Index Type의 동작방식, 내부 원리를 설명할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 PostgreSQL 타입, 상황에 대해 어떤 Index를 적용할지 떠올릴 수 있다.&lt;/li&gt;
&lt;li&gt;쿼리 실행 계획에서 마주하는 Scan 방식에 대해 왜 해당 Scan이 적용되었는지 떠올릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 목표를 달성하기 위해서 Index Type에 대해서 간략하게 내부 동작과 적절한 상황들을 알아봤었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, Scan 관련해서도 내부 원리를 살펴보며 쿼리 실행 계획에서 Scan 방식을 마주쳤을 때 추론할 수 있게 될 것 같습니다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://postgrespro.com/community/books/internals&quot;&gt;https://postgrespro.com/community/books/internals&lt;/a&gt;&lt;/p&gt;</description>
      <category>DB</category>
      <category>index</category>
      <category>index scan</category>
      <category>index type</category>
      <category>PostgreSQL</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/159</guid>
      <comments>https://ksh-coding.tistory.com/159#entry159comment</comments>
      <pubDate>Sat, 16 Nov 2024 15:17:46 +0900</pubDate>
    </item>
    <item>
      <title>[PostgreSQL] 1. PostgreSQL 내부 구조 알아보기 (feat. 쿼리 처리 과정)</title>
      <link>https://ksh-coding.tistory.com/158</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이전까지 DB 벤더 중에서 학습용으로 MySQL만 사용해왔었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이번 실무에서 PostgreSQL을 사용하게 되면서 PostgreSQL에 대해서 알아보고자 글을 작성하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 모든 내용을 다루기에는 너무나 방대한 양이기에 제가 궁금한 부분, 중요한 부분들만 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 주제를 잡아서 주제별로 PostgreSQL의 공식문서를 보고 글을 작성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음은 PostgreSQL에서 커넥션을 맺고 쿼리를 어떻게 처리하는지 PostgreSQL의 내부 구조 를 간략하게 추상적으로 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/17/overview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/17/overview.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729322980397&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Chapter&amp;nbsp;50.&amp;nbsp;Overview of PostgreSQL Internals&quot; data-og-description=&quot;Chapter&amp;nbsp;50.&amp;nbsp;Overview of PostgreSQL Internals Table of Contents 50.1. The Path of a Query 50.2. How Connections Are Established 50.3. The &amp;hellip;&quot; data-og-host=&quot;www.postgresql.org&quot; data-og-source-url=&quot;https://www.postgresql.org/docs/17/overview.html&quot; data-og-url=&quot;https://www.postgresql.org/docs/17/overview.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bn5kNQ/hyXlOmCTbV/4LavWJVo2LGnNSfSkTNUtK/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/17/overview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.postgresql.org/docs/17/overview.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bn5kNQ/hyXlOmCTbV/4LavWJVo2LGnNSfSkTNUtK/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557');&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;Chapter&amp;nbsp;50.&amp;nbsp;Overview of PostgreSQL Internals&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Chapter&amp;nbsp;50.&amp;nbsp;Overview of PostgreSQL Internals Table of Contents 50.1. The Path of a Query 50.2. How Connections Are Established 50.3. The &amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.postgresql.org&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. How Connections Are Established : 커넥션 맺는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 &amp;lsquo;process per user&amp;rsquo; &lt;b&gt;client/server model&lt;/b&gt;을 사용합니다. &lt;b&gt;(Client당 1개의 Server 프로세스 할당)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Server의 프로세스는 다음과 같은 2개로 구분할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Supervisor Process (postmaster)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client와 직접적으로 커넥션을 맺고 Backend Process를 생성하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Backend Process&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client와 연결되어 Client에서 요청한 쿼리를 처리하여 반환하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client의 쿼리 요청이 들어왔을 때의 과정을 자세히 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Supervisor Process가 커넥션을 맺는 특정 TCP/IP Port를 listen&lt;/li&gt;
&lt;li&gt;커넥션 요청이 감지될 때마다 새로운 Backend Process 생성&lt;/li&gt;
&lt;li&gt;Client는 생성된 Backend Process에게 쿼리 요청&lt;/li&gt;
&lt;li&gt;Backend Process는 쿼리를 처리하여 결과를 Client에게 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 간단하게 도식화하면 다음과 같은 구조로 커넥션을 맺고 요청을 주고 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2466&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6Q8OT/btsKb8l9v9Z/9GP2kjvp1YN1fBMoV3DKAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6Q8OT/btsKb8l9v9Z/9GP2kjvp1YN1fBMoV3DKAK/img.png&quot; data-alt=&quot;Client/Server Model 도식화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6Q8OT/btsKb8l9v9Z/9GP2kjvp1YN1fBMoV3DKAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6Q8OT%2FbtsKb8l9v9Z%2F9GP2kjvp1YN1fBMoV3DKAK%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;2466&quot; height=&quot;1052&quot; data-origin-width=&quot;2466&quot; data-origin-height=&quot;1052&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Client/Server Model 도식화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. The Parser Stage : 전달받은 Query 파싱 &amp;amp; 변환&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 단계에서 Client는 커넥션을 맺고 요청할 Query를 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 이해하기 위해서 간단한 Query를 예시로 이후 과정을 설명해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729325228885&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT id, name FROM member WHERE age &amp;gt; 30&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;위의 쿼리를 Client가 요청했을 때 이후 과정을 살펴봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 해당 쿼리는 PostgreSQL Server에서 이해할 수 있는 형태로 구조화된 것이 아니라 단순 Plain Text 형태로 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Application으로 비유를 들면 다음과 같은 단순 String 변수로 전달되는 것입니다. (Java 예시)&lt;/p&gt;
&lt;pre id=&quot;code_1729325369089&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String query = &quot;SELECT id, name FROM member WHERE age &amp;gt; 30&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 PostgreSQL에서는 해당 Plain Text를 여러 Tree를 생성하며 구조화하는 여정을 거치게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 이 단계에서는 &lt;b&gt;전달받은 Query Text를 파싱하고 변환하는 과정&lt;/b&gt;을 거칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Parser : 파싱 단계&lt;/li&gt;
&lt;li&gt;Transformation Process : 변환 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1 . Parser : 파싱 단계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파싱 단계에서는 간단하게 전달받은 Query의 문법에 오류가 없는지 체크하고, 없다면 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Parse Tree&lt;/b&gt;&lt;/span&gt;를 생성하여 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT를 통해 간단히 Parse Tree를 다음과 같이 표현해봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729325692758&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                   SELECT
                 /    |    \
            target_list   FROM   WHERE
           /     |        |        |
         id    name    member   age &amp;gt; 30&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;이처럼 단순한 Plain Text를 파싱하여 다음과 같은 구조를 형성하여 다음 단계에 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&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: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. Transformation Process : 변환 단계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환 단계에서는 파싱 단계에서 전달받은 Parse Tree를 기반으로 여러 조건을 확인하는 단계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 Parse Tree를 전달받았다고 하면, 다음과 같은 검증을 거칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Table &amp;amp; Column 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;member 테이블 존재 확인&lt;/li&gt;
&lt;li&gt;id, name, nickname, age 컬럼이 member 테이블에 존재하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;연산자 및 데이터 타입 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건인 age 컬럼이 숫자형 데이터 타입인지 확인하고, 적용 가능한 연산인지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 중요한 점은, &lt;b&gt;실제 DB 객체를 확인하여 검증하는 것이기 때문에 객체 정보가 있는 system catalog를 조회한다는 점입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 검증이 끝나고 나면, 해당 해석을 담는 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Query Tree&lt;/b&gt;&lt;/span&gt;를 생성하여 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT를 통해 간단히 Query Tree를 다음과 같이 표현해봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729326393515&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                   SELECT
                 /    |    \
            target_list   FROM   WHERE
           /     |        |        |
         id    name    member   age &amp;gt; 30
           |     |        |
        column  column   table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ Parser Stage가 2단계(파싱 - 변환)로 나뉘는 이유&amp;nbsp;&lt;/b&gt;&lt;/h4&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;이전에 변환 단계에서 실제 DB 객체와 비교하여 검증을 수행하기 때문에 system catalog를 조회한다고 언급했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 작업은 트랜잭션 위에서 수행되어야 하기 때문에 변환 단계에서는 트랜잭션이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Parser Stage가 2단계가 아니라 파싱과 변환이 함께 묶였다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 필요하지 않은 파싱 단계에서도 트랜잭션을 가지기 때문에 트랜잭션이 길어지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Parser Stage를 2단계로 나누어 변환 단계에서만 트랜잭션 위에서 작업이 수행되도록 한 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. The PostgreSQL Rule System : The Rewrite System&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, 변환 단계를 거쳐 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Query Tree&lt;/b&gt;&lt;/span&gt;를 전달받은 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 쿼리를 '재작성'하는 Rewrite System이 동작하여 적용된 Rule이 있는지 확인하고 Query Tree를 재작성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 PostgreSQL의 Rule은 CUD 쿼리에 적용하는 Rule을 생성할 수 있고 View를 생성하게 되면 자동으로 Rule이 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 Rule System 관련해서는 내용이 깊기 때문에 추가적인 내용은 아래 공식문서에서 확인해보시면 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/17/rules.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/17/rules.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729327180449&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Chapter&amp;nbsp;39.&amp;nbsp;The Rule System&quot; data-og-description=&quot;Chapter&amp;nbsp;39.&amp;nbsp;The Rule System Table of Contents 39.1. The Query Tree 39.2. Views and the Rule System 39.2.1. How SELECT Rules &amp;hellip;&quot; data-og-host=&quot;www.postgresql.org&quot; data-og-source-url=&quot;https://www.postgresql.org/docs/17/rules.html&quot; data-og-url=&quot;https://www.postgresql.org/docs/17/rules.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wNXwN/hyXlWrqf4v/azGWLevAg68q8rWLyLlay0/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/17/rules.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.postgresql.org/docs/17/rules.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wNXwN/hyXlWrqf4v/azGWLevAg68q8rWLyLlay0/img.png?width=540&amp;amp;height=557&amp;amp;face=0_0_540_557');&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;Chapter&amp;nbsp;39.&amp;nbsp;The Rule System&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Chapter&amp;nbsp;39.&amp;nbsp;The Rule System Table of Contents 39.1. The Query Tree 39.2. Views and the Rule System 39.2.1. How SELECT Rules &amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.postgresql.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 예제에는 따로 적용된 Rule이 없기 때문에 그대로 Query Tree가 재작성 되지 않고 전달되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 알아보기 위해, 아래의 View를 조회한다고 가정해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1729327258276&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* View
CREATE VIEW active_users AS
SELECT * FROM users WHERE is_active = true;

---
SELECT * FROM active_users;&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;이때, Parser Stage를 거쳐 Query Tree가 전달되었을 때 `active_users`는 테이블이 아니라 View이므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 데이터에서 조회할 수가 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Rewrite System에서 해당 쿼리를 Rule을 통해 실제 데이터를 조회하는 쿼리로 분석하여 Query Tree를 재작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(View를 생성하면 자동으로 관련 Rule이 생성됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 특정 Rule이 존재한다면, Rewrite System에서 해당 Rule을 적용한 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Query Tree&lt;/b&gt;&lt;/span&gt;로 재작성 후 전달합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt; Planner / Optimizer : 최적의 Execution Plan 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 단계에서는 전달받은 Query Tree를 기반으로 실제 데이터를 어떻게 읽고 처리할지에 관한 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Plan Tree&lt;/b&gt;&lt;/span&gt;를 생성하여 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달받은 Query Tree로 Plan을 생성했을 때, 여러가지 방법들이 존재할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 조회하려는 컬럼에 인덱스가 존재한다면 다음과 같은 방법이 있을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Seq Scan : 순차적으로 모두 스캔&lt;/li&gt;
&lt;li&gt;Index Scan : 인덱스 스캔&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 다양한 Plan을 모두 생성하는 역할을 Planner가 수행하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Plan을 받아서 Optimizer가 해당 Plan들을 모두 검증하고 가장 빠르게 실행될 것으로 예측되는 Plan을 선정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후에 해당 Plan을 기반으로 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Plan Tree&lt;/b&gt;&lt;/span&gt;를 생성하여 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT를 통해 간단히 Plan Tree를 다음과 같이 표현해봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729327802212&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                   Seq Scan
                 /          \
            Output         Filter
          /    |    \       |
       id    name   age   age &amp;gt; 30
                 |
               member (table)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4.  Executor : Plan 실행&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Executor는 마지막 단계로, 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달받은 Plan Tree의 노드들을 재귀적으로 하위노드 -&amp;gt; 상위노드로 처리하면서 실제 DB에서 필요한 row들을 추출하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 최적의 Plan을 실행하고 필요한 row를 추출해서 Client에게 전달하게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. The Path Of a Query : 쿼리 처리 과정 요약&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 알아본 PostgreSQL에서 쿼리가 어떻게 처리되는지에 관한 과정들을 요약해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client(Application)에서 PostgreSQL 서버와 커넥션을 맺고, 쿼리 명령을 전달하고 결과를 기다린다.&lt;/li&gt;
&lt;li&gt;Parser Stage 에서 Client가 전달한 쿼리의 문법 오류가 있는지 체크하고 query tree 를 생성&lt;/li&gt;
&lt;li&gt;rewrite system에서 query tree에 적용될 rules를 찾고 query tree에 적용한 후, 재작성&lt;/li&gt;
&lt;li&gt;planner/optimizer 는 query tree 를 가져와서 executor에 입력될  plan tree를 생성한다.&lt;/li&gt;
&lt;li&gt;executor 는 plan tree 를 재귀적으로 살펴보고 plan 에 따라 행들을 탐색한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로는 이렇게 요약이 되고, 조금 딱딱한 것 같아서 Flow를 알아보기 쉽게 그림으로 다음과 같이 표현해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4738&quot; data-origin-height=&quot;3122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEtcdU/btsKbRyacaW/VJaUkEe1cSQuzn4dHtEJQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEtcdU/btsKbRyacaW/VJaUkEe1cSQuzn4dHtEJQ1/img.png&quot; data-alt=&quot;The Path Of Query&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEtcdU/btsKbRyacaW/VJaUkEe1cSQuzn4dHtEJQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEtcdU%2FbtsKbRyacaW%2FVJaUkEe1cSQuzn4dHtEJQ1%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;4738&quot; height=&quot;3122&quot; data-origin-width=&quot;4738&quot; data-origin-height=&quot;3122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;The Path Of Query&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;</description>
      <category>DB</category>
      <category>DB</category>
      <category>iNTERNAL</category>
      <category>PostgreSQL</category>
      <category>내부 구조</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/158</guid>
      <comments>https://ksh-coding.tistory.com/158#entry158comment</comments>
      <pubDate>Sat, 19 Oct 2024 18:22:59 +0900</pubDate>
    </item>
    <item>
      <title>[DB] DB PK 생성 전략 알아보기 (feat. Auto Increment, UUID, ULID, Snowflake ID, TSID)</title>
      <link>https://ksh-coding.tistory.com/157</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 들어가기 전&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 저는 모든 프로젝트에서 관성적으로 다음과 같이 Auto_Increment 전략을 사용해서 DB PK를 생성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726906630693&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class XxxEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long 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;이번에 프로젝트 코드 분석을 하는 도중에 JPA Entity에 다음과 같이 PK 생성을 하는 것을 보게 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726906070691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class XxxEntity {

    @Id @Tsid
    private String 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;관련해서 검색해보니, TSID라는 생성 전략이 있는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 TSID 외에도 다른 PK 생성 전략에 대해 무지했기 때문에 다른 여러 PK 생성 전략도 알아보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(현재 포스팅에서는 여러 방법들을 PK에 국한되어 설명하지만, PK 외에 고유 ID를 생성할 때도 사용할 수 있습니다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. PK(고유 ID) 생성 전략 종류&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 PK를 생성하는 방법들에는 어떤 것들이 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 방법들이 있지만, 일반적으로 사용하는 방법은 크게 다음과 같이 5가지 방법들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Auto_Increment&lt;/li&gt;
&lt;li&gt;UUID&lt;/li&gt;
&lt;li&gt;ULID&lt;/li&gt;
&lt;li&gt;Snowflake ID&lt;/li&gt;
&lt;li&gt;TSID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 말하면, 프로젝트에서는 해당 5가지 방법 중 TSID를 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 5가지 방법들을 하나씩 살펴보고, 왜 TSID를 적용했는지도 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Auto_Increment&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto_Increment 전략은 저도 그랬고, 일반적으로&lt;b&gt; 개발을 처음 시작하고 나서 PK를 생성할 때 가장 많이 적용하는 방법&lt;/b&gt;일 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto_Increment는 일반적으로 사용하는 PK 생성 방법이기 때문에 생성 결과만 간략하게 소개하고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF3btv/btsJHp9INPZ/IX5UKfLKXX7OQe1F0L9mV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF3btv/btsJHp9INPZ/IX5UKfLKXX7OQe1F0L9mV1/img.png&quot; data-alt=&quot;Auto_Increment&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF3btv/btsJHp9INPZ/IX5UKfLKXX7OQe1F0L9mV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF3btv%2FbtsJHp9INPZ%2FIX5UKfLKXX7OQe1F0L9mV1%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;200&quot; height=&quot;304&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Auto_Increment&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방식으로 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;DB 자체에서 1부터 자동증가로 채번하여 PK를 생성하는 방법&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. Auto_Increment에 어떤 문제가 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto_Increment는 위처럼 간단하게 PK를 생성하는 방식임을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 많은 사람들이 Auto_Increment 전략을 통해 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;하지만, 레퍼런스들을 찾아보면 뒤에서 다룰 UUID, ULID, Snowflake, TSID를 사용하는 프로젝트도 있는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 왜 Auto_Increment는 사용하지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Auto_Increment를 선택했을 때 생기는 문제들이 존재하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 문제들이 있는지 Auto_Increment의 문제들을 살펴보도록 하겠습니다.&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;Problem 1 - 다른 데이터들을 쉽게 추적할 수 있다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자뿐만 아니라, 일반 사용자들도 페이지의 URL을 들여다보면 Auto_Increment된 식별자가 있는 페이지들을 자주 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, 제가 사용하는 블로그 플랫폼인 Tistory의 예시를 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image 복사본.png&quot; data-origin-width=&quot;3126&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4ChLJ/btsJHXSnTGn/KZ6EPgL8Wfv1oeGidtnh31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4ChLJ/btsJHXSnTGn/KZ6EPgL8Wfv1oeGidtnh31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4ChLJ/btsJHXSnTGn/KZ6EPgL8Wfv1oeGidtnh31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4ChLJ%2FbtsJHXSnTGn%2FKZ6EPgL8Wfv1oeGidtnh31%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;3126&quot; height=&quot;602&quot; data-filename=&quot;Image 복사본.png&quot; data-origin-width=&quot;3126&quot; data-origin-height=&quot;602&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;빨간 박스가 쳐진 URL을 살펴보면, tistory 도메인 뒤에 '135'인 글의 식별자를 발견할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 식별자를 보고 다른 데이터들을 어떻게 추적할 수 있을까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;135니까 136을 넣어서 접근해볼까?&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 위와 같이 접근하면 다음과 같이 다른 데이터(포스트)로 접근되는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Image 복사본2.png&quot; data-origin-width=&quot;3116&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4klEk/btsJIVsKJ3e/lL2EuviNeHtqU6H8rwppc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4klEk/btsJIVsKJ3e/lL2EuviNeHtqU6H8rwppc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4klEk/btsJIVsKJ3e/lL2EuviNeHtqU6H8rwppc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4klEk%2FbtsJIVsKJ3e%2FlL2EuviNeHtqU6H8rwppc0%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;3116&quot; height=&quot;606&quot; data-filename=&quot;Image 복사본2.png&quot; data-origin-width=&quot;3116&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 추적은 티스토리와 같이 정보에 민감하지 않은 도메인들은 상관이 없겠지만, 정보가 민감한 도메인에는 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 데이터들을 추적하는 것이 쉽기 때문에 &lt;b&gt;크롤링, 해킹에 아주 취약할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롤링 : 자사의 정보들을 크롤링하기 쉬워지므로 타사 및 경쟁사에서 정보들을 가져갈 수 있다.&lt;/li&gt;
&lt;li&gt;해킹 : 자사의 고객 개인 정보들과 같은 민감한 정보들을 해커가 해킹하기 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Problem 2 - 분산 시스템에서 ID가 고유하지 않을 수 있다.&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;그 중에서 비즈니스를 고려하여 DB를 다음과 같이 분산할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글로벌 서비스에서 서비스가 성장하여 다양한 국가에서 데이터들이 하나의 DB로 접근하는 상황&lt;/li&gt;
&lt;li&gt;이 상황에서 국가별로 DB를 분산시켜서 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;DB 부하를 줄일 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이렇게 국가별로 DB가 나뉜 상황에서 '회원' 도메인을 예시로 들어보겠습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yObJM/btsJGv395eW/ca9aF0JDXPOhRlhrszCsSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yObJM/btsJGv395eW/ca9aF0JDXPOhRlhrszCsSk/img.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;456&quot; data-is-animation=&quot;false&quot; width=&quot;200&quot; height=&quot;268&quot; style=&quot;width: 32.2237%; margin-right: 10px;&quot; data-widthpercent=&quot;32.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yObJM/btsJGv395eW/ca9aF0JDXPOhRlhrszCsSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyObJM%2FbtsJGv395eW%2Fca9aF0JDXPOhRlhrszCsSk%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;340&quot; height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pV83T/btsJIgc4YqF/c0Uqf289kREkQkWc8EPeT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pV83T/btsJIgc4YqF/c0Uqf289kREkQkWc8EPeT0/img.png&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;450&quot; data-is-animation=&quot;false&quot; width=&quot;200&quot; height=&quot;262&quot; style=&quot;width: 33.0375%; margin-right: 10px;&quot; data-widthpercent=&quot;33.82&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pV83T/btsJIgc4YqF/c0Uqf289kREkQkWc8EPeT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpV83T%2FbtsJIgc4YqF%2Fc0Uqf289kREkQkWc8EPeT0%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;344&quot; height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMktgg/btsJIooskve/4vKZNG0ZmH7G2uAQkvHzy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMktgg/btsJIooskve/4vKZNG0ZmH7G2uAQkvHzy1/img.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;448&quot; data-is-animation=&quot;false&quot; width=&quot;200&quot; height=&quot;267&quot; style=&quot;width: 32.4132%;&quot; data-widthpercent=&quot;33.19&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMktgg/btsJIooskve/4vKZNG0ZmH7G2uAQkvHzy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMktgg%2FbtsJIooskve%2F4vKZNG0ZmH7G2uAQkvHzy1%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;336&quot; height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;국가별 회원 DB PK&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 모두 다른 DB를 사용하기 때문에 DB 별로 1부터 증가하는 Auto_Increment는 중복이 발생할 것입니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통계를 위해 국가별 회원 데이터를 하나의 통계 테이블로 구성해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 하나의 테이블에 데이터를 모으려고 할 때 각각의 PK가 중복되므로 고유한 데이터를 식별하는 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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Problem 3 - 정렬 컬럼에 INDEX를 생성해야 하는 경우, 혼란을 야기할 수 있다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 앞선 2개의 문제와는 다르게, Auto_Increment 전략을 사용해서 문제가 발생하는 케이스는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 말하면, 다른 전략인 ULID, Snowflake, TSID를 사용하면 해당 문제를 해결할 수 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대적으로 Auto_Increment를 사용하면 해당 문제를 해결할 수 없기 때문에 문제로 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(말이 어렵네요  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨말인지 풀어서 설명해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 조건적으로 발생하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제가 발생하려면, 다음과 같은 조건이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블에 created_at 같은 정렬 조건으로 사용하는 컬럼이 존재한다.&lt;/li&gt;
&lt;li&gt;정렬 속도 향상을 위해 해당 컬럼에 인덱스를 생성했다.&lt;/li&gt;
&lt;li&gt;또한, 해당 상황에서 조건으로 많이 사용하는 컬럼에 인덱스를 생성했다.&lt;/li&gt;
&lt;li&gt;쿼리 시 where과 order by에서 index로 설정한 컬럼을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 조건이 갖춰졌을 때, Index Scan의 성능이 나빠질 수 있습니다.&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;※ Member 테이블 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로만 쓰면 상황이 잘 와닿지 않는데, PostgreSQL을 사용한 구체적인 테이블로 예시를 들어보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726920046614&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* postgreSQL
CREATE TABLE member (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

* name 인덱스 생성
CREATE INDEX idx_member_name ON member(name);&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;위와 같은 DDL을 가지는 member 테이블이 있다고 해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 생성 이후, 조건에 자주 사용되는 &lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;name에 먼저 INDEX를 생성&lt;/span&gt;&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한&lt;b&gt; 초기 데이터로 'seongha1', 'seongha2', 'seongha3' name을 가진 Member 데이터를 각각 백만 건 INSERT&lt;/b&gt; 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, 다음과 같은 쿼리를 EXPLAIN 하면 다음과 같은 결과가 나옵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726985894724&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
select * 
from member
where name = 'seongha3'&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-origin-width=&quot;624&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sVVkW/btsJItwBuMB/hcfkohjroqKVVvYdO0W6z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sVVkW/btsJItwBuMB/hcfkohjroqKVVvYdO0W6z1/img.png&quot; data-alt=&quot;EXPLAIN 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sVVkW/btsJItwBuMB/hcfkohjroqKVVvYdO0W6z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsVVkW%2FbtsJItwBuMB%2FhcfkohjroqKVVvYdO0W6z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;291&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EXPLAIN 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bitmap Index Scan&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;name의 INDEX를 사용하여 Index Scan&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;사진에는 없지만, 약 0.67s 소요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 정렬이 많이 사용되어 정렬 속도 향상을 위해 created_at에도 INDEX를 생성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726986276742&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* created_at 인덱스 생성
CREATE INDEX idx_member_created_at ON member(created_at);&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;그리고 다음과 같이 조건 + 정렬의 쿼리를 EXPLAIN 했을 때 쿼리는 어떤 INDEX를 통해 Scan 할까요?&lt;/p&gt;
&lt;pre id=&quot;code_1726986226351&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN
select * 
from member
where name = 'seongha3'
order by created_at desc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg3EeV/btsJGXlAdlI/Mk51bokxDbp8orNrgrV0tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg3EeV/btsJGXlAdlI/Mk51bokxDbp8orNrgrV0tK/img.png&quot; data-alt=&quot;EXPLAIN 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg3EeV/btsJGXlAdlI/Mk51bokxDbp8orNrgrV0tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg3EeV%2FbtsJGXlAdlI%2FMk51bokxDbp8orNrgrV0tK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;398&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EXPLAIN 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index Scan&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;cretaed_at의 INDEX를 사용하여 Index Scan&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;사진에는 없지만, 약 1.3s 소요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 'name' 조건 컬럼의 INDEX가 아닌 'created_at' 정렬 컬럼의 INDEX를 통해 Scan을 하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index가 여러 개일 때 어떤 Index를 사용해서 Scan할 것인지는 Optimizer의 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마도, 'name'의 Cardinality가 'created_at'의 Cardinality보다 낮아서 'created_at' INDEX를 선택한 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼, 이렇게 되면 &lt;b&gt;조건 컬럼인 'name'에 INDEX를 생성한 의미가 없어지고 개발자의 의도와 다르게 동작할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 설명이 길었는데, 이러한 부분을 문제로 설정하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Problem 4 - DB 리소스를 소모해서 PK를 생성한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto_Increment는 PK 생성 책임을 DB에 위임해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 자동 증가되는 숫자를 저장하고 있다가,&amp;nbsp;데이터가 들어오면 1씩 증가시켜서 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;이때, 동시에 많은 데이터 저장 쿼리가 DB에 요청되면 해당 숫자들은 어떻게 정합성을 지키면서 증가할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 이러한 구현은 DB 벤더사별로 다르게 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL : Auto_Increment Lock이 존재하여, 데이터가 INSERT 될 때마다 PK의 정합성을 유지하기 위해 Lock을 걸고 내부 Increment 숫자를 증가시킨 후에 Lock을 해제합니다.&lt;/li&gt;
&lt;li&gt;PostgreSQL : 트랜잭션과 별도로 동작하는 'Sequence'를 만들어서 원자성을 보장합니다.&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 DB별로 별도의 자원을 소모하여 Auto Increment의 데이터 정합성을 지키게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 다른 PK 생성 전략을 사용하는 것 대비 DB 리소스를 사용하기 때문에 DB에 부하가 간다는 점이 문제라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Auto_Increment를 선택했을 때 발생하는 문제 4가지를 알아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto_Increment가 아닌 UUID, ULID, Snowflake, TSID를 사용하면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이러한 문제들을&lt;span&gt; 부분적으로&amp;nbsp;&lt;/span&gt;&lt;/span&gt;해결하거나 전부 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 설명할 &lt;b&gt;UUID, ULID, Snowflake, TSID는 해당 문제들을 어떻게 해결하는지, 어디까지 해결하는지 살펴보도록 하겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. UUID&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 &lt;b&gt;Universally Unique Identifier의 약자로, 말그대로 고유한 식별자를 의미&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;128비트의 고유한 식별자 36자 문자열을 만들어내는 표준&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 여러 버전이 있고, 버전에 따라 고유 식별자를 만드는 방법들이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 중에서 일반적으로 버전 4가 완전한 랜덤값을 통해 고유 식별자를 사용하기 때문에 버전 4를 많이 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 사실 고유한 식별자를 생성하는 표준이지만, 중복 가능성이 0%는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RFC 4122 (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4122&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://datatracker.ietf.org/doc/html/rfc4122&lt;/a&gt;) 문서에 정의된 바에 따르면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1조개의 UUID 중에 중복이 일어날 확률은 10억 분의 1이라고 서술하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고, 간단하게 거의 고유한 식별자를 생성할 수 있고 작은 크기를 가지기 때문에 고유 ID를 생성할 때 많이 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;UUID로 생성되는 문자열의 예시는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;b00f1718-540e-4ac0-aaa2-68cc65e41d5d&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;UUID로 PK를 생성하게 되면 다음과 같이 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH6uKG/btsJIfSMXyU/oHgztwfcCaMFkKMRgJQI41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH6uKG/btsJIfSMXyU/oHgztwfcCaMFkKMRgJQI41/img.png&quot; data-alt=&quot;UUID PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH6uKG/btsJIfSMXyU/oHgztwfcCaMFkKMRgJQI41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH6uKG%2FbtsJIfSMXyU%2FoHgztwfcCaMFkKMRgJQI41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;188&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UUID PK&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;이렇게 저장되면 Auto_Increment 사용 시 발생했던 크롤링, 해킹 문제를 부분적으로 해결할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;크롤링, 해킹 위험이 Auto_Increment보다 적다.&lt;/b&gt; (다른 데이터 추적 어려움)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PK 생성에 DB 리소스를 소모하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 여전히 다음 2가지 문제는 해결할 수 없습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분산 시스템에서 ID가 고유하지 않을 수 있다.&lt;/b&gt; (사실 분산 시스템이 아니더라도 UUID는 중복 가능성이 0%가 아니기 때문에 UUID 자체의 100% 고유 식별자가 아닌 문제가 있다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정렬 Index 사용으로 인해 Index Scan 성능이 나빠질 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID 버전 1을 사용하면 시간을 기반으로 생성되기 때문에 PK를 통해 정렬해서 정렬 Index를 사용하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;하지만, 일반적으로 자바에서도 UUID 버전 3, 4를 제공하고 버전 1은 외부 라이브러리를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;이러한 이유때문에 일반적인 경우(버전 4)를 고려해서 정렬 Index를 사용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 문제가 존재하기 때문에 사실 UUID도 PK 생성 전략으로 많이 사용하지는 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. ULID&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 Universally Unique Lexicographically Sortable Identifier의 약자입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(근데 UULSI가 아니라 ULID네요,,, ㅎㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID와 비슷해보이지만 다음과 같은 키워드가 추가되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lexicographically : 사전적으로&lt;/li&gt;
&lt;li&gt;Sortable : 정렬 가능한&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 UUID에서 '정렬' 기능이 추가되었다고 생각하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, ULID는 '&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;128비트의 고유한  식별자 문자열을 만드는 표준&lt;/span&gt;'이라는 점은 UUID와 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID와 다른 점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Timestamp 기반으로 생성하기 때문에 ULID로 시간순 정렬이 가능하다.&lt;/li&gt;
&lt;li&gt;36자가 아닌 26자로 생성되기 때문에 문자열 크기가 더 작다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID를 나타내면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1726923696868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; 01AN4Z07BY      79KA1307SR9X4MV3

|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞 10자 : Timestamp 기반으로 대소문자를 구분하지 않고 시간을 나타냄&lt;/li&gt;
&lt;li&gt;뒤 16자 : 랜덤 값으로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID로 PK를 생성하게 되면 다음과 같이 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아주 약간의 일정한 간격을 두고 5개를 생성했습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; data-alt=&quot;ULID PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh94xi%2FbtsJGNJ0FIP%2FIXd4YxKs8c368fgowLNEtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;234&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ULID PK&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;아주 약간의 일정한 간격을 두고 생성했기 때문에 맨 앞 10자가 '01J8AA75M0'이나 '01J8AA75M0'으로 비슷한 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 ULID는 기존 문제 중 다음과 같은 문제를 해결합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;크롤링, 해킹 위험이 Auto_Increment보다 적다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(다른 데이터 추적 어려움)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PK 생성에 DB 리소스를 소모하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 여전히 다음 2가지 문제는 해결할 수 없습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분산 시스템에서 ID가 고유하지 않을 수 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(사실 분산 시스템이 아니더라도 ULID는 중복 가능성이 0%가 아니기 때문에 ULID 자체의 100% 고유 식별자가 아닌 문제가 있다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정렬 Index 사용으로 인해 Index Scan 성능이 나빠질 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, 고유하지 않은 ID 문제는 UUID와 같기 때문에 이해가 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 UUID에서 Timestamp 기반으로 시간 정렬 기능이 추가된 ULID인데 왜 정렬 문제가 그대로 남아있는지 궁금할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 ULID PK 예시를 봐봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; data-alt=&quot;ULID PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh94xi/btsJGNJ0FIP/IXd4YxKs8c368fgowLNEtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh94xi%2FbtsJGNJ0FIP%2FIXd4YxKs8c368fgowLNEtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;234&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ULID PK&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3, 4, 5번을 살펴보면 거의 동일한 시간 간격으로 생성했기 때문에 Timestamp 기반의 앞 10자리가 '01J8AA75M1'로 동일합니다.&lt;/p&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로 정렬을 수행하게 되면 &lt;b&gt;맨 앞 10자리는 동일하기 때문에 뒤의 랜덤 16자리에 의해 정렬&lt;/b&gt;되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위의 경우 11번째 자리가 각각 '7 / 3 / C'이므로 정렬은 '3 -&amp;gt; 7 -&amp;gt; C' 순으로 정렬됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 생성 순서 3, 4, 5를 정렬했는데 4, 3, 5로 정렬되게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유때문에 결국 ULID도 생성한 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;그래서 ULID로 생성한 ID도 PK로 사용하기 적절하지 않아보입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. Snowflake ID&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Twitter Snowflake ID는 2010년에 Twitter에서 만든 분산환경에서 사용할 수 있는 고유 ID 생성 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Twitter Snowflake 공식 문서(현재는 X 공식문서) 링크는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.x.com/engineering/en_us/a/2010/announcing-snowflake&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.x.com/engineering/en_us/a/2010/announcing-snowflake&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 Snowflake ID 방법을 도입한 이유를 다음과 같이 설명하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 MySQL 인스턴스에 데이터를 저장했다가, NoSQL인 Cassandra와 MySQL을 샤딩하여 나눠서 데이터 저장&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #242b34; text-align: start;&quot;&gt;이 상황에서, 고유한 ID를 만드는 적절한 방법이 없었다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242b34; text-align: start;&quot;&gt;고가용성 방식으로 분산 시스템에서 초당 수만개의 ID를 생성해야한다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242b34; text-align: start;&quot;&gt;ID로 정렬이 가능해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242b34; text-align: start;&quot;&gt;리팩토링을 위해 새로운 ID는 기존 ID 비트 수인 64비트를 유지해야한다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 해당 문제들을 해결하기 위해 Snowflake ID는 고유한 ID를 다음과 같이 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmuK6v/btsJG2Ah0Y5/CoO4Zx6Lb1gUbrWUcMMpFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmuK6v/btsJG2Ah0Y5/CoO4Zx6Lb1gUbrWUcMMpFk/img.png&quot; data-alt=&quot;Snowflake 구성 요소 (출처 : https://keeplearning.dev/twitter-snowflake-approach-is-cool-3156f78017cb)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmuK6v/btsJG2Ah0Y5/CoO4Zx6Lb1gUbrWUcMMpFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmuK6v%2FbtsJG2Ah0Y5%2FCoO4Zx6Lb1gUbrWUcMMpFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;304&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Snowflake 구성 요소 (출처 : https://keeplearning.dev/twitter-snowflake-approach-is-cool-3156f78017cb)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;총 64비트의 ID&lt;/li&gt;
&lt;li&gt;1 bit sign : 1비트를 할당해서 양수 / 음수를 결정, 일반적으로 양수를 나타내기위해 항상 0으로 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;41 bit timestamp&lt;/b&gt; : 특정 기준 시간인 Epoch를 정해놓고 그 시점부터 경과한 시간을 ms 단위로 저장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;41비트는 약 2의 41승 개의 다른 값을 가질 수 있는데, 이를 기간으로 환산하면 약 &lt;b&gt;69년&lt;/b&gt;까지 고유한 값을 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;해당 timestamp bit를 통해 ID로 시간순 정렬이 가능하고, 언제 생성되었는지 추적할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;10 bit worker ID&lt;/b&gt; : Snowflake ID를 생성하는 인스턴스마다 고유한 ID를 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;총 10비트로, 2의 10승 개인 1024개의 분산 인스턴스까지 사용 가능하다.&lt;/li&gt;
&lt;li&gt;해당 10 bit worker ID를 통해 분산 시스템 간의 Snowflake ID 중복 충돌이 발생하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;12 bit sequence&lt;/b&gt; : 동일한 ms 내에서 여러 ID 생성 시에 사용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본은 0으로, 만약 동일한 ms 내에 여러 ID를 생성해야 할 때 1씩 증가하면서 2의 12승인 4096까지 나타낼 수 있다.&lt;/li&gt;
&lt;li&gt;즉, 동일한 ms 내에서 최대 4096개의 고유한 ID를 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;만약 4096개 이상의 동일한 요청이 들어오면 대기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake ID로 PK를 생성하게 되면 다음과 같이 저장됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(아주 약간의 일정한 간격을 두고 5개를 생성했습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cddoC2/btsJIe0OWve/ALKhotGFkKvQBAjj27EI01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cddoC2/btsJIe0OWve/ALKhotGFkKvQBAjj27EI01/img.png&quot; data-alt=&quot;Snowflake ID PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cddoC2/btsJIe0OWve/ALKhotGFkKvQBAjj27EI01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcddoC2%2FbtsJIe0OWve%2FALKhotGFkKvQBAjj27EI01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;454&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Snowflake ID PK&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;해당 Snowflake는 기존 문제 중 다음과 같은 문제를 해결합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;크롤링, 해킹 위험이 Auto_Increment보다 적다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(다른 데이터 추적 어려움)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PK 생성에 DB 리소스를 소모하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;분산 시스템에서 ID가 고유하다. (약 69년까지만.)&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Snowflake ID로 정렬이 가능하기 때문에 정렬 컬럼을 따로 만들지 않고 PK로 정렬이 가능하다.&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Snowflake를 사용하면 기존 4가지 문제를 모두 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 고유 ID를 만들 수 있는 기간이 최대 69년이라는 사실이 조금 걸리긴 하지만, 이전 방법들 중에서는 가장 좋은 방법 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Discord나 Instagram 같은 유명한 서비스에서도 고유 ID를 Snowflake ID를 사용하여 생성한다고 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. TSID&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID는 Time-Sorted Unique Identifier의 약자로, UUID를 대체할 목적으로 나온 방법이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID는 앞서 살펴본 ULID와 Snowflake ID를 합쳐서 ID를 구성한 오픈소스 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/f4b6a3/tsid-creator?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/f4b6a3/tsid-creator?tab=readme-ov-file&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726992310527&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - f4b6a3/tsid-creator: A Java library for generating Time-Sorted Unique Identifiers (TSID).&quot; data-og-description=&quot;A Java library for generating Time-Sorted Unique Identifiers (TSID). - f4b6a3/tsid-creator&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/f4b6a3/tsid-creator?tab=readme-ov-file&quot; data-og-url=&quot;https://github.com/f4b6a3/tsid-creator&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/smQAX/hyW6zKlhgn/Zakj7AR418UsRbEnIZWYWk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/f4b6a3/tsid-creator?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/f4b6a3/tsid-creator?tab=readme-ov-file&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/smQAX/hyW6zKlhgn/Zakj7AR418UsRbEnIZWYWk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - f4b6a3/tsid-creator: A Java library for generating Time-Sorted Unique Identifiers (TSID).&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Java library for generating Time-Sorted Unique Identifiers (TSID). - f4b6a3/tsid-creator&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID는 간단하게 다음과 같은 2가지 요소로 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Time 요소 (42 bits)&lt;/li&gt;
&lt;li&gt;Random 요소 (22 bits)&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;node (0 ~ 20 bits)&lt;/li&gt;
&lt;li&gt;counter (2 ~ 22 bits)&lt;/li&gt;
&lt;li&gt;여기서 node, counter는 bit 수가 정해져있지 않고 유연하게 node, counter 수를 조정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;ex) 현재 인스턴스 수에 맞게 적절하게 node, counter를 조절하여 동시성을 고려할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 그림으로 도식화하면 다음과 같습니다. (공식문서의 구조 코드블럭)&lt;/p&gt;
&lt;pre id=&quot;code_1726993424771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                                            adjustable
                                           &amp;lt;----------&amp;gt;
|------------------------------------------|----------|------------|
       time (msecs since 2020-01-01)           node       counter
                42 bits                       10 bits     12 bits

- time:    2^42 = ~69 years or ~139 years (with adjustable epoch)
- node:    2^10 = 1,024 (with adjustable bits)
- counter: 2^12 = 4,096 (initially random)&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;기본적으로 구조는 앞서 살펴봤던 Snowflake ID와 거의 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 점은 ULID에 있었던 &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;node가 생성될 때 이전의 Snowflake ID에서는 나머지 22 bit를 다음과 같이 처리했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10bit의 worker ID : 각 worker(node)의 ID를 지정하여 worker 마다 동일한 ID 지정&lt;/li&gt;
&lt;li&gt;12 bit의 seqence : 순차적으로 증가하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID는 해당 방식에서 랜덤성을 추가하여 다음과 같이 구현합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;node : node마다 ID를 랜덤으로 지정&lt;/b&gt; (동일한 node라도 매 요청마다 ID 달라짐, 하지만 설정으로 고유하게 지정도 가능)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;counter : 무조건 랜덤으로 설정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;랜덤성과 정렬의 Trade-Off&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID에도 ULID의 랜덤성이 추가되면서, ULID의 한계도 같이 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랜덤성을 추가하다보니, 동일한 Timestamp일 경우에 뒤의 22 bit 요소가 중복될 확률이 0%가 아니게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 반대로 인스턴스의 ID가 동일할 때도 랜덤하게 생성하고 counter가 랜덤하게 바뀌면서 보안성은 훨씬 좋아진 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, TSID와 Snowflake ID를 결정하는 기준 중 하나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'보안성'과 '중복성'&lt;/b&gt;일 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 중복성에서 중복 확률이 0%가 아니지만, 거의 0%에 수렴하므로 무시해도 되지 않을까 싶긴 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&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;TSID as long / TSID as String&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, TSID를 사용해서 ID를 생성하면 결과가 어떻게 나올까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSID는 'long', 'String' 2가지 타입으로 상황에 맞게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2FMOr/btsJIihT6Nx/KyNwXaBYyPLuKKTQky9i91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2FMOr/btsJIihT6Nx/KyNwXaBYyPLuKKTQky9i91/img.png&quot; data-alt=&quot;TSID as long PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2FMOr/btsJIihT6Nx/KyNwXaBYyPLuKKTQky9i91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2FMOr%2FbtsJIihT6Nx%2FKyNwXaBYyPLuKKTQky9i91%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;222&quot; height=&quot;229&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;229&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TSID as long PK&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MqixJ/btsJGVBoG8d/xguttM5lVilXDa1pLIDfLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MqixJ/btsJGVBoG8d/xguttM5lVilXDa1pLIDfLk/img.png&quot; data-alt=&quot;TSID as String PK&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MqixJ/btsJGVBoG8d/xguttM5lVilXDa1pLIDfLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMqixJ%2FbtsJGVBoG8d%2FxguttM5lVilXDa1pLIDfLk%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;221&quot; height=&quot;229&quot; data-origin-width=&quot;221&quot; data-origin-height=&quot;229&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TSID as String PK&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;이때, String 타입을 사용한다면 Crockford's base32 인코딩을 사용해서 인코딩한 값을 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;해당  TSID는 기존 문제 중 다음과 같이 모든 문제를 해결하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;크롤링, 해킹 위험이 Auto_Increment보다 적다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(다른 데이터 추적 어려움)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PK 생성에 DB 리소스를 소모하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;분산 시스템에서 ID가 고유하다. (TSID도 Snowflake와 timestamp 부분 bit 수가 같기 때문에 약 69년까지만.)&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;TSID 자체로 시간 순 정렬이 가능하기 때문에 정렬 컬럼을 따로 만들지 않고 PK로 정렬이 가능하다.&lt;/b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;※ Snowflake ID vs TSID&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 구조만 본다면 거의 Snowflake와 다른 점이 없어보이는데 Snowflake ID와 비교해서 TSID는 어떤 점이 다를까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유연한 확장성&lt;/b&gt; : Snowflake ID는 확장 시마다 고유 ID를 부여해줘야하지만, TSID는 랜덤 방식을 선택하면 고유 ID를 새롭게 지정할 필요없이 랜덤하게 생성됩니다. 따라서, 고유 ID를 관리해야 할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 저장 공간 절약&lt;/b&gt; : String으로 저장하게 되면 13자로 더 짧은 문자열을 생성하기 때문에, 저장되는 크기가 더 적습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 가독성&lt;/b&gt; : 상대적으로 19자의 숫자인 Snowflake 보다 Base32로 인코딩된 TSID가 개발자 입장에서 가독성이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 다음과 같이 다양한 PK(고유 ID) 생성 전략들을 알아봤습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Auto_Increment&lt;/li&gt;
&lt;li&gt;UUID&lt;/li&gt;
&lt;li&gt;ULID&lt;/li&gt;
&lt;li&gt;Snowflake ID&lt;/li&gt;
&lt;li&gt;TSID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 어떤 상황에 어떤 전략들을 사용할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 결론부터 말하자면, 저는 Auto_Increment / Snowflake ID / TSID 중 상황에 맞게 사용할 것 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규모가 적고, 확장 가능성이 적고 정보 노출에 민감하지 않은 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;서비스&lt;span&gt; : Auto Increment&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;DB에서 PK를 생성하므 Application 단계에서  UUID, ULID에 비해 간단한 구현&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt; UUID, ULID에 비해 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;상대적으로 좋은 코드 가독성&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;상황상 발생하지 않는 Side Effect&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;반대로 규모가 크거나 확장 가능성이 있거나 정보 노출에 민감한 서비스 : Snowflake ID / TSID 중 선택&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;상황상 Auto Increment, UUID, ULID는 Side Effect가 발생할 수 있기 때문에 제외&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;또, 시간순 정렬이 가능하게 설계하면 대규모 데이터 정렬 시 더 빠른 성능으로 조회가 가능하므로 Snowflake ID / TSID 중 선택&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;둘 중에서는 일반적으로 TSID를 사용하고, 랜덤성으로 인한 데이터 중복이 1건이라도 있으면 안되는 경우 Snowflake ID 선택&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;TSID가 Snowflake ID 대비 확장에 유리하고, 데이터 공간 절약, 좋은 가독성을 가지기 때문에 일반적으로 TSID 선택&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;단, 엄청 희박한 확률이지만 TSID의 랜덤성으로 인해서 데이터 중복이 되었을 때 손해가 큰 도메인(돈 관련 도메인)에서는 중복이 없는 Snowflake ID를 선택&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.chulgil.me/tsid/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.chulgil.me/tsid/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726921857855&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[TSID] 분산환경 기본키 채번 전략&quot; data-og-description=&quot;분산환경, 기본키 채번 전략, TSID UUID, ULID, Auto Increment&quot; data-og-host=&quot;blog.chulgil.me&quot; data-og-source-url=&quot;https://blog.chulgil.me/tsid/&quot; data-og-url=&quot;https://blog.chulgil.me/tsid/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8FXaO/hyW2SYXc9H/erV04kqIpTPZmW3yXKl6Ak/img.png?width=1376&amp;amp;height=990&amp;amp;face=0_0_1376_990,https://scrap.kakaocdn.net/dn/c9LsbP/hyW2SYXcov/UxGSJJPHpVaofKXItF2Jyk/img.png?width=1376&amp;amp;height=990&amp;amp;face=0_0_1376_990&quot;&gt;&lt;a href=&quot;https://blog.chulgil.me/tsid/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.chulgil.me/tsid/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8FXaO/hyW2SYXc9H/erV04kqIpTPZmW3yXKl6Ak/img.png?width=1376&amp;amp;height=990&amp;amp;face=0_0_1376_990,https://scrap.kakaocdn.net/dn/c9LsbP/hyW2SYXcov/UxGSJJPHpVaofKXItF2Jyk/img.png?width=1376&amp;amp;height=990&amp;amp;face=0_0_1376_990');&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;[TSID] 분산환경 기본키 채번 전략&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;분산환경, 기본키 채번 전략, TSID UUID, ULID, Auto Increment&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.chulgil.me&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/resources/glossary/uuid&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.tosspayments.com/resources/glossary/uuid&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726921861552&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;UUID(Universally Unique Identifier) | 토스페이먼츠 개발자센터&quot; data-og-description=&quot;UUID는 128-bit의 고유 식별자에요. UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 빠르고 간단하게 만들 수 있어요.&quot; data-og-host=&quot;docs.tosspayments.com&quot; data-og-source-url=&quot;https://docs.tosspayments.com/resources/glossary/uuid&quot; data-og-url=&quot;https://docs.tosspayments.com/resources/glossary/uuid&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bewGNT/hyW6ItEhX9/SIfE51I3CqUVaMyQHRRRBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bHgVtb/hyW2YLFH2Z/2JM8jdq5AEl2aPSICFS1V1/img.png?width=2760&amp;amp;height=794&amp;amp;face=0_0_2760_794&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/resources/glossary/uuid&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.tosspayments.com/resources/glossary/uuid&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bewGNT/hyW6ItEhX9/SIfE51I3CqUVaMyQHRRRBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bHgVtb/hyW2YLFH2Z/2JM8jdq5AEl2aPSICFS1V1/img.png?width=2760&amp;amp;height=794&amp;amp;face=0_0_2760_794');&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;UUID(Universally Unique Identifier) | 토스페이먼츠 개발자센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;UUID는 128-bit의 고유 식별자에요. UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 빠르고 간단하게 만들 수 있어요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.tosspayments.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>DB</category>
      <category>autoincrement</category>
      <category>PK</category>
      <category>snowflakeid</category>
      <category>tsid</category>
      <category>ULID</category>
      <category>UUID</category>
      <category>고유id</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/157</guid>
      <comments>https://ksh-coding.tistory.com/157#entry157comment</comments>
      <pubDate>Sun, 22 Sep 2024 22:12:41 +0900</pubDate>
    </item>
    <item>
      <title>2022~2024 머글이 신입 백엔드 개발자로 전직하기까지의 회고</title>
      <link>https://ksh-coding.tistory.com/156</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;0. 서론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 Big Event가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 나의 취업.... &lt;b&gt;직장인으로 전직&lt;/b&gt;했다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 입사한지는 이제 거의 한달 차가 다 되어 가는데, 입사하고 나서는 정신없어서 이런 회고 글조차 작성할 겨를이 없었다.&lt;/p&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;개발자의 길을 걷기로 결심한 2022년부터 어찌저찌 신입 개발자가 되어버린 나의 회고를 연도별로 정리해보려고 한다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 2021년 말, 진로 마시다가 진로 고민...  &lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 성적따라 건축공학과에 진학해서 2021년 말 전까지 미래 고민 없이 충분히 현재를 즐기며 살았었다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나 뭐하지?&lt;/span&gt; &lt;br /&gt;건축쪽은 흥미도 없고 적성도 없는 거 같은데??&lt;br /&gt;그럼 어렸을 때 컴공 준비했었는데, 개발자 준비해보자?? &lt;br /&gt;OK~&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도 그때 가졌던 의식의 흐름이다,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진로 고민이라고 거창하게 써놨지만 사실 하루만에 고민을 끝내고 바로 행동으로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 도움을 받았던 강의는 인프런 한정수님의 '비전공자를 위한 개발자 취업 올인원 가이드 [통합편]'이다. (내돈내산)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 강의를 통해 전반적인 개발 생태계를 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;한정수 - '비전공자를 위한 개발자 취업 올인원 가이드 [통합편]&quot; href=&quot;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%ED%86%B5%ED%95%A9%ED%8E%B8/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%ED%86%B5%ED%95%A9%ED%8E%B8/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726582544250&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;한정수 | 체육을 전공하고 29살에 개발 공부를 시작해서, 30살에 연봉 4천만원, 31살에 연봉 6천만원, 32살에 연봉 x천만원 이상 받는 탑티어 회사 개발자가 된 노하우를 모두 담았습니다!, 개발자 &quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%ED%86%B5%ED%95%A9%ED%8E%B8/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/개발자-취업-통합편&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bdLuGq/hyW6zppuCd/hsWtGDX7BWorHwXCc2rvf0/img.jpg?width=768&amp;amp;height=500&amp;amp;face=351_120_378_151,https://scrap.kakaocdn.net/dn/bzf8cb/hyW6HOvcPr/GYbUFn0WWmfdmFa1yV9eHk/img.jpg?width=768&amp;amp;height=500&amp;amp;face=351_120_378_151,https://scrap.kakaocdn.net/dn/ths4j/hyW6H12eI3/dzS5hNeQHWaTMrH3RDGkak/img.png?width=1270&amp;amp;height=1120&amp;amp;face=0_0_1270_1120&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%ED%86%B5%ED%95%A9%ED%8E%B8/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%ED%86%B5%ED%95%A9%ED%8E%B8/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bdLuGq/hyW6zppuCd/hsWtGDX7BWorHwXCc2rvf0/img.jpg?width=768&amp;amp;height=500&amp;amp;face=351_120_378_151,https://scrap.kakaocdn.net/dn/bzf8cb/hyW6HOvcPr/GYbUFn0WWmfdmFa1yV9eHk/img.jpg?width=768&amp;amp;height=500&amp;amp;face=351_120_378_151,https://scrap.kakaocdn.net/dn/ths4j/hyW6H12eI3/dzS5hNeQHWaTMrH3RDGkak/img.png?width=1270&amp;amp;height=1120&amp;amp;face=0_0_1270_1120');&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;한정수 | 체육을 전공하고 29살에 개발 공부를 시작해서, 30살에 연봉 4천만원, 31살에 연봉 6천만원, 32살에 연봉 x천만원 이상 받는 탑티어 회사 개발자가 된 노하우를 모두 담았습니다!, 개발자&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의를 들으면서 느꼈던 점은, 진로 고민 때 '개발자' 분야라고 퉁쳤지만 다양한 분야가 존재한다는 점이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트엔드 개발자&lt;/li&gt;
&lt;li&gt;백엔드 개발자&lt;/li&gt;
&lt;li&gt;Data Engineer&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 밥먹을 때도 메뉴 못 정하는 결정장애인 나의 성향에 따라 당연히도 처음부터 분야를 정하진 못했다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결국 2021년 말의 나는 하나씩 경험해보기로 결심한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 2022년 초, 다양한 분야 경험하면서 진로 결정하기  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 2022년 초부터 개발자의 다양한 분야들을 경험해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지금와서 생각해보면 아무런 계획없이 경험할 분야 순서를 끌리는 대로 선택했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는 다음과 같이 경험해보고 결국 &lt;b&gt;'백엔드 개발자'&lt;/b&gt;로 결정하게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트엔드 개발자&lt;/li&gt;
&lt;li&gt;Data Engineer&lt;/li&gt;
&lt;li&gt;백엔드 개발자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. 프론트엔드 - 클론코딩? 재밌다! 끝  &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 시작하고 처음 마주한 건 html, css였다.&lt;/p&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;소름돋게도, 그 당시에는 뭐하는 기술인지도 모르는 Spring 프레임워크의 사이트를 화면만 클론코딩했었다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(클론코딩한 Spring이 회사에 입사한 지금까지도 내가 가장 많이 쓰는 기술이 될지 꿈에도 몰랐다;;;  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 당시에 진짜 아무런 스킬이 없어서 크롬 F12로 나오는 raw한 html을 그대로 분석하면서 따라 쳤던 기억이 아직도 선명하다!&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFyGxl/btsJFJTuflt/0K1R6gLlmc551AXQTHV4N1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFyGxl/btsJFJTuflt/0K1R6gLlmc551AXQTHV4N1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFyGxl/btsJFJTuflt/0K1R6gLlmc551AXQTHV4N1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bFyGxl/btsJFJTuflt/0K1R6gLlmc551AXQTHV4N1/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;600&quot; height=&quot;324&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;324&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;그러나, 클론코딩이 아닌 본격적으로 JavaScript, React를 사용해서 화면을 렌더링할 때 그다지 큰 재미를 느끼지 못했다.&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-2. 데이터 엔지니어 - 오, 해볼까? 박사?...  &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 처음 보는 분야 중에서 데이터 엔지니어가 가장 흥미로웠었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터들을 분석해서 차트로 표현하고, 머신러닝을 통해 데이터들을 정제하는게 흥미로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에 큰 규모의 AI 사이트인 Kaggle의 한 예제 원문을 한국어로 옮기면서 이해해보는 작업들도 수행했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 이 당시에 컴공을 전공하는 친구와 밥을 먹는 자리를 가졌었는데 친구가 AI는 대학원생 아니면 하기 힘들다고 말렸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 친구의 말을 듣고 그 당시에 정말 고민을 많이 했었는데 결국 현실적인 문제로 데이터 엔지니어도 보류하기로 했다.&lt;/p&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-3. 백엔드 - 남은게 이거네? 찍먹하려 했는데 푹먹이네?  &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자로 취업한 지금 생각해보면 백엔드 공부의 시작은 자의가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드는 흥미, 적성이 없고 데이터 엔지니어는 현실적인 벽에 부딪혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 순서인 백엔드 분야를 경험해볼 차례였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 말하면, 백엔드 개발자를 선택한 이유는 백엔드는 &lt;b&gt;찍먹에 실패했기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 공부 전반적인 의식의 흐름을 살펴보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;백엔드? -&amp;gt; Java 시작&lt;br /&gt;Java -&amp;gt; Spring 시작&lt;br /&gt;Spring -&amp;gt;  데이터 담아야하는데?&lt;br /&gt;데이터 -&amp;gt; DB 시작&lt;br /&gt;서버 어떻게 띄우지? -&amp;gt; 인프라 시작&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 프론트엔드, 데이터 엔지니어와 조금 찍먹 결이 달랐던 것 같다.&lt;/p&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;데이터 엔지니어도 마찬가지로  Kaggle의 예제를 이해하면서 빠르게 찍먹할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 백엔드는 아무래도 이름처럼 뒷단에 위치하기 때문에&amp;nbsp;결과를 눈으로 보기까지가 상당히 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 영역을 공부할 때마다 독립적이지 않고 유기적으로 연결되어서 결과가 나오기 때문에 찍먹이 오래걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 결과를 눈으로 보는 것까지를 찍먹으로 기준을 생각했는데, 찍먹까지가 상당히 오래걸렸다,, ㅎㅎㅎㅎ&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 : Java &amp;amp; Spring&lt;/li&gt;
&lt;li&gt;데이터 저장 : DB&lt;/li&gt;
&lt;li&gt;기능 테스트를 위한 서버 구동 : 인프라&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 여러 영역들을 찍먹하고 나서야 백엔드 분야를 찍먹할 수 있었다. (푹먹인가? ㅎㅎ;;;  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시기도 적절했던 게, 여러 대외 프로그램에서 프로젝트를 진행했었는데 마침 백엔드 찍먹 시기여서 백엔드로 프로젝트들을 참여했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결국 &lt;b&gt;백엔드가 푹먹이 되어버려서 이후에 쭉 백엔드로 진로를 결정했다!&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 2022년 말 ~ 2023년 말, 우아한테크코스 교육 수료  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 초에 백엔드 개발자로 진로를 결정하고 난 이후에 여러 프로젝트를 진행하며 백엔드 지식을 쌓아가고 있었다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_인재상.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2IIHL/btsJDgFX3FZ/oYLX1clCjmnOR70zml0tF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2IIHL/btsJDgFX3FZ/oYLX1clCjmnOR70zml0tF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2IIHL/btsJDgFX3FZ/oYLX1clCjmnOR70zml0tF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2IIHL%2FbtsJDgFX3FZ%2FoYLX1clCjmnOR70zml0tF1%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;745&quot; height=&quot;412&quot; data-filename=&quot;edited_인재상.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 설명회 영상을 보면서 위의 캡쳐를 할 정도로 우테코는 상당히 가고 싶어서 준비를 많이 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 프리코스도 잘 마치고 교육에 참여할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjA010/btsJD6vBI8k/JuHpDX6kOetQ6EqRGuapf0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjA010/btsJD6vBI8k/JuHpDX6kOetQ6EqRGuapf0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjA010/btsJD6vBI8k/JuHpDX6kOetQ6EqRGuapf0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjA010%2FbtsJD6vBI8k%2FJuHpDX6kOetQ6EqRGuapf0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;2월부터 11월까지 너무 긴 경험이어서 얻었던 소중한 경험들을 간단하게 요약하자면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본적인 백엔드 지식 학습&lt;/b&gt; : 앞서 말한 Java &amp;amp; Spring / DB / 인프라 모든 부분의 지식을 학습할 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자기주도적 학습&lt;/b&gt; : 대부분의 기술을 자기주도적으로 학습하면서 현재 회사 입사 후에도 처음 보는 기술을 자기주도적으로 잘 학습할 수 있게 되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최고의 동료들&lt;/b&gt; : 캠퍼스에 가서 주변을 둘러보면 항상 열심히 하고 잘하는 동료들이 있었다. 현재까지 연락하는 동료들도 있어서 비전공자인 나에게 한줄기 개발 인맥이 되어주었다!  &lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트 한 사이클 경험&lt;/b&gt; : 기획-개발-운영으로 이어지는 프로젝트의 한 사이클을 찐하게 경험할 수 있었다. 이전 사이드 프로젝트들과는 양과 질 모두 다른 아주아주 소중한 경험이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 내에서 종종 우테코를 '샌드박스'라고 비유하는 것을 들었었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 생활은 나에게 정말 '샌드박스'와도 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 짓을 해도 안전한 모래가 나를 지탱해줬고, 그 속에서 최고의 동료들과 함께 학습 환경을 만들며 꾸준히 성장할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발 인생이 짧긴 하지만 이후에 개발자의 길을 쭉 걸어도 우테코 경험이 기억의 남는 순간 중 하나이지 않을까 싶다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 2023년 말 ~ 2024년 8월, 기나긴 취준 암흑기  &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코를 다니는 중 취업에 대한 얘기들이 슬금슬금 나올 때 다음과 같은 말을 자주 들었었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;올해 취업시장 더 안 좋아진다던데?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 성향은 완전 MBTI 'S', 경험중심적인 사람이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 내가 경험해보지 못한 것에는 잘 체감이 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 말은 지금까지도 체감이 잘 되지는 않지만, 그 당시에는 더더욱 경험해보지 못했기에 체감되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 개발자로 취준을 처음 시작하는 것이기 때문에 이전 개발자 채용 시장을 경험해보지 못했기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(취업한 지금 생각해보면, 뭔가 절대적으로도 채용 공고가 적은 느낌이 있기는 했던 것 같다 ㅎㅎ;;;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우테코를 수료하고 난 직후인 11월 말에는 근자감이 하늘을 찔렀던 것 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래도 우테코 수료했는데 금방 취업하지 않을까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 생각이 내가 2023년 말에 가졌던 오만한 생각이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면 우테코를 막 수료했을 때는 적어도 2024년 상반기에는 취업해서 회사에 다니고 있을 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 취준 기간이 2024년 상반기를 넘겼을 때 조금 조급해지기 시작했었다.&lt;/p&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하다보면 늘겠지  &lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 취준 기간 동안 Github 프로필 소개에도 해당 멘트로 변경했었다! (지금까지도 유지!)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;329&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zph4y/btsJEiiiySC/hBYEpHH8ztW87x6bRgfBd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zph4y/btsJEiiiySC/hBYEpHH8ztW87x6bRgfBd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zph4y/btsJEiiiySC/hBYEpHH8ztW87x6bRgfBd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzph4y%2FbtsJEiiiySC%2FhBYEpHH8ztW87x6bRgfBd1%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;329&quot; height=&quot;437&quot; data-origin-width=&quot;329&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 잦은 서류 탈락에도 다른 사람들보다는 멘탈이 흔들리지 않고 그냥 하던 거 했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 취준을 하면서 만족하는 부분은 다음과 같은 2가지다. (그냥 의식의 흐름으로 회고중 ㅎㅎㅎ)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모르거나 궁금한 것 위주로 학습하기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'하다보면 늘겠지'하는 꾸준함을 증명하기&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 학습을 내가 경험했던 것 중에 경험하지 못해서 궁금하거나 모르는 것 위주로 학습을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방향이 물론 '신입 개발자'를 준비하는 입장에서는  과도하거나 불필요한 방향일 수 있었지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 궁금한 것을 하다보니 하루하루 재밌게 개발했고, 효율이 잘나왔던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 궁금한 것을 해결하지 않았다면 지금쯤 후회했을 것 같다 ㅎㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 나는 블로그 활동을 통해서 꾸준함을 증명한 것이 만족스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'하다보면 늘겠지'하는 긍정적인 꾸준함도 사실 면접관 입장에서 보여지는 것이 없이 마인드만 있으면 효과가 없을 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힘들긴 했지만 새로운 것을 학습할 때마다 꾸준히 블로그에 작성한 것이 꾸준함을 증명하는 것 같아서 만족스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 요즘은 기술 블로그 작성이 Default인 것 같지만.... 그래도 꾸준함을 증명하는 건 맞지 않을까?... 나름 질도 좋다고 생각한다 ㅎㅎ,,)&lt;/p&gt;
&lt;p data-ke-size=&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;취준 생활 동안 서류를 통과하고 면접을 볼 수 있었던 이유 중에 해당 2가지가 가장 컸다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 가장 기술 면접이 어려웠던 TOSS 면접에서도 궁금해서 파고 든 MSA 구현 포스팅들을 칭찬해주신 기억이 선명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(칭찬 전에 이미 기술 질문 폭격으로 멘탈 바닥났던 것도 기억남 ㅎㅎ;;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 결과 두둥!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는 약 &lt;b&gt;75개&lt;/b&gt;의 기업에 서류를 넣고 한 곳에 합격해서 8월부터 다니게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대적으로 뭔가 넣은 서류가 적어보이지만 나름대로는 맞춰서 다 넣었다고 생각하는데, 아닌가? ㅎㅎ;;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 75개 중에서도 신입만 뽑는 공고를 필터링하면 정말 적을 것이다. 그만큼 채용 시장이 힘들다는 증거인가?&lt;/p&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 2024년 8월, 직장인 전직  &amp;zwj; &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'하다보면 늘겠지' 마인드로 하루하루 살아가던 도중, 회사 한 곳에서 서류를 붙고 면접을 보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 면접 중 몇 개의 질문들에 답변을 못했다고 생각했는데 운 좋게 붙게 되어서 너무 기뻤다  &lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자로서 동기부여가 생길 것 같은 회사인지?&lt;/li&gt;
&lt;li&gt;개발자로서 성장할 수 있을 것 같은 회사인지?&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;입사한 회사는 해당 2가지 기준을 모두 만족한다고 개인적으로는 생각이 들어서 기분 좋게 입사할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 후에는 최종 면접 제외하고 다 면접 보셨던 분들과 함께 일을 하게 되어서 입사하고 처음 뵐 때 너무 신기했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 면접 대기했던 회사 앞 카페에 회사 점심을 먹고 커피를 사러 간다는 게 새삼 기분이 새로웠다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 있으면 입사 1달이 되어서 온보딩을 진행하는데, 기대된다 ㅎㅎㅎㅎ (별거 없겠지만)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 회사 생활에 대한 회고가 아니기 때문에! 회사 생활 회고는 이후 포스팅으로 미루고 이번 챕터를 마무리하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후.. 뭔가 회고를 해보자는 짧은 생각에서 회고를 시작했는데, 생각보다 길어진 것 같아서 당황스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그만큼 개발 진로 전환 후에 걸어온 길이 깊은 건가? ㅋㅎㅎㅎ (사실 경험한 것 중 새발의 피도 안 쓴 거 같은데)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지도 취준을 하고 있었으면 이런 회고 글 따위 시간낭비라고 생각했을 텐데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사를 하게 되어 이런 회고 글을 쓸 수 있다는 게 감회가 새롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>심심한 회고</category>
      <category>백엔드개발자회고</category>
      <category>신입</category>
      <category>회고</category>
      <author>BE_성하</author>
      <guid isPermaLink="true">https://ksh-coding.tistory.com/156</guid>
      <comments>https://ksh-coding.tistory.com/156#entry156comment</comments>
      <pubDate>Wed, 18 Sep 2024 02:38:02 +0900</pubDate>
    </item>
  </channel>
</rss>