<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
    <link>https://soobarkbar.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 06:47:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>기내식은수박바</managingEditor>
    <image>
      <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
      <url>https://tistory1.daumcdn.net/tistory/3076242/attach/d9ec458535e6467b967018217189ee98</url>
      <link>https://soobarkbar.tistory.com</link>
    </image>
    <item>
      <title>대규모 분산 시스템에서 고유 ID를 생성하는 방법들</title>
      <link>https://soobarkbar.tistory.com/261</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 개발하다 보면 거의 모든 데이터에 ID가 필요하다.&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;회원 ID&lt;/li&gt;
&lt;li&gt;주문 ID&lt;/li&gt;
&lt;li&gt;게시글 ID&lt;/li&gt;
&lt;li&gt;댓글 ID&lt;/li&gt;
&lt;li&gt;결제 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버와 단일 데이터베이스만 사용하는 환경에서는 ID 생성이 비교적 단순하다. 데이터베이스의 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;나 &lt;code&gt;SEQUENCE&lt;/code&gt;를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;User Table

id | name
---|------
1  | Kim
2  | Lee
3  | Park&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시스템 규모가 커지면 상황이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 여러 대로 늘어나고, 데이터베이스가 샤딩되고, 여러 지역의 데이터센터에서 동시에 요청을 처리해야 한다면 단순히 숫자를 1씩 증가시키는 방식만으로는 충분하지 않을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; ID 생성
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 ID를 생성할 때 중요한 문제는 단순히 &amp;ldquo;중복되지 않는 값&amp;rdquo;을 만드는 것만이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID가 시간순으로 정렬될 수 있는지, 데이터베이스 인덱스에 유리한지, 중앙 서버 장애에 영향을 받는지, 외부에 노출해도 안전한지, 초당 얼마나 많은 ID를 생성할 수 있는지도 함께 고려해야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 ID는 어떤 조건을 만족해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 비교하기 전에 먼저 평가 기준을 정리할 필요가 있다.&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;좋은 ID 생성 방식을 판단할 때는 보통 다음 기준을 본다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기준&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;고유성&lt;/td&gt;
&lt;td&gt;서로 다른 데이터가 같은 ID를 가지면 안 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 성능&lt;/td&gt;
&lt;td&gt;많은 요청이 들어와도 빠르게 ID를 생성할 수 있어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분산 생성 가능 여부&lt;/td&gt;
&lt;td&gt;여러 서버가 동시에 ID를 만들어도 충돌이 없어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 가능성&lt;/td&gt;
&lt;td&gt;ID만 보고 생성 순서나 대략적인 시간을 알 수 있는지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장 공간&lt;/td&gt;
&lt;td&gt;ID가 차지하는 크기가 작을수록 저장 공간과 인덱스 크기에 유리하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스 효율&lt;/td&gt;
&lt;td&gt;데이터베이스 B-Tree 인덱스에서 삽입 성능이 좋은지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예측 가능성&lt;/td&gt;
&lt;td&gt;외부 사용자가 다음 ID를 쉽게 추측할 수 있는지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 복잡도&lt;/td&gt;
&lt;td&gt;별도의 중앙 서버, Worker ID 관리, 시간 동기화 등이 필요한지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;는 단순하고 인덱스 효율이 좋지만, 분산 환경에서는 확장성 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 UUID는 여러 서버에서 독립적으로 생성하기 쉽지만, UUID v4처럼 완전히 랜덤한 값은 데이터베이스 인덱스 관점에서 불리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Auto Increment / Sequence&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방식은 데이터베이스가 ID 생성을 담당하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;, PostgreSQL의 &lt;code&gt;SEQUENCE&lt;/code&gt;가 대표적이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 데이터가 insert될 때마다 데이터베이스가 자동으로 증가하는 숫자 ID를 부여한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1, 2, 3, 4, 5, ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Increment와 Sequence 방식의 장점은 명확하다.&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;ID가 숫자라 저장 공간이 작다.&lt;/li&gt;
&lt;li&gt;순차적으로 증가하므로 정렬이 쉽다.&lt;/li&gt;
&lt;li&gt;B-Tree 인덱스에 유리하다.&lt;/li&gt;
&lt;li&gt;운영 복잡도가 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS 기반의 일반적인 CRUD 서비스라면 이 방식만으로도 충분한 경우가 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 시스템이 커졌을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성을 데이터베이스가 담당하므로, 결국 데이터베이스가 ID 생성의 중심 지점이 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; Database Sequence
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 여러 대로 늘어나도 ID 생성은 데이터베이스에 의존한다. 트래픽이 커지면 데이터베이스가 병목이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 데이터베이스를 여러 개로 샤딩하면 더 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Shard 1: id = 1
Shard 2: id = 1
Shard 3: id = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 샤드가 독립적으로 Auto Increment를 사용하면 서로 같은 ID가 만들어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 피하기 위해 샤드별로 ID 범위를 나누거나, 증가 간격을 다르게 설정하거나, 별도의 ID 생성 서버를 두는 방법을 사용할 수 있다. 하지만 이 순간부터 단순했던 Auto Increment 방식도 운영 복잡도가 증가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Increment나 Sequence는 다음과 같은 상황에 적합하다.&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;li&gt;순차적인 숫자 ID가 필요한 경우&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 중앙 ID 생성 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방식은 ID 생성을 전담하는 중앙 서버를 두는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; ID Generator Server ──&amp;gt; 1000001
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버는 직접 ID를 만들지 않고, ID 생성 서버에 요청해서 새로운 ID를 받아온다.&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;Redis &lt;code&gt;INCR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;데이터베이스 Sequence 전용 서버&lt;/li&gt;
&lt;li&gt;ZooKeeper&lt;/li&gt;
&lt;li&gt;etcd&lt;/li&gt;
&lt;li&gt;별도 ID Generator API 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Auto Increment를 데이터베이스 테이블에 직접 묶어두는 대신, ID 생성 책임을 별도 컴포넌트로 분리한 형태라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 ID 생성 서버를 사용하면 여러 애플리케이션 서버가 같은 규칙으로 ID를 발급받을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역적으로 중복 없는 ID를 만들기 쉽다.&lt;/li&gt;
&lt;li&gt;순차 증가 ID를 만들기 쉽다.&lt;/li&gt;
&lt;li&gt;ID 생성 정책을 한 곳에서 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 서비스가 같은 ID 생성 규칙을 공유할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 단점은 중앙 장애점이 생긴다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ID Generator Server 장애
        &amp;darr;
새로운 데이터 생성 불가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 서버가 죽으면 새로운 주문, 게시글, 메시지를 만들 수 없게 된다.&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;또한 모든 ID 생성 요청이 네트워크 호출을 필요로 하므로, 애플리케이션 내부에서 직접 생성하는 방식보다 지연 시간이 늘어날 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 ID 생성 서버는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역적으로 순차적인 ID가 필요한 경우&lt;/li&gt;
&lt;li&gt;여러 서비스가 동일한 ID 발급 정책을 사용해야 하는 경우&lt;/li&gt;
&lt;li&gt;ID 생성 정책을 중앙에서 통제해야 하는 경우&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. UUID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 Universally Unique Identifier의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 128bit 식별자이며, 일반적으로 다음과 같은 문자열 형태로 표현된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;550e8400-e29b-41d4-a716-446655440000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID의 가장 큰 장점은 중앙 서버 없이 각 서버가 독립적으로 ID를 생성할 수 있다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ──&amp;gt; UUID 생성
Server B ──&amp;gt; UUID 생성
Server C ──&amp;gt; UUID 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버가 서로 통신하지 않아도 충돌 가능성이 매우 낮은 ID를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID에는 여러 버전이 있다. 이 글에서는 대표적으로 UUID v1, UUID v4, UUID v7을 간단히 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-1. UUID v1&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v1은 시간 정보와 노드 정보를 기반으로 ID를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화하면 다음과 같은 구조다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Timestamp + Clock Sequence + Node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Node 값에는 전통적으로 MAC Address가 사용될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 기반이기 때문에 어느 정도 정렬 가능하다는 장점이 있지만, MAC Address 같은 하드웨어 식별 정보가 포함될 수 있다는 점 때문에 개인정보나 보안 측면에서 부담이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 신규 시스템에서 UUID v1을 적극적으로 선택하는 경우는 많지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-2. UUID v4&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4는 랜덤 기반 UUID다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Random 122 bits + Version/Variant bits&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 개발자가 흔히 사용하는 UUID가 이 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 다음과 같이 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UUID id = UUID.randomUUID();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4의 장점은 단순하다.&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;li&gt;외부에 노출해도 다음 ID를 예측하기 어렵다.&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;UUID v4는 랜덤 값이기 때문에 생성 순서와 ID 정렬 순서가 일치하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;생성 순서:
A &amp;rarr; B &amp;rarr; C &amp;rarr; D

UUID 정렬 결과:
C &amp;rarr; A &amp;rarr; D &amp;rarr; B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성은 데이터베이스 인덱스에서 문제가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 정렬된 구조를 유지한다. 순차적으로 증가하는 ID는 인덱스의 끝에 계속 추가되기 때문에 비교적 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 UUID v4처럼 랜덤한 값은 인덱스의 여러 위치에 흩어져 삽입된다. 이로 인해 페이지 분할, 캐시 효율 저하, 인덱스 단편화가 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중앙 서버 없이 ID를 생성해야 하는 경우&lt;/li&gt;
&lt;li&gt;ID 예측 가능성을 낮춰야 하는 경우&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-3. UUID v7&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 시간 정렬성을 고려한 UUID다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RFC 9562에 정의된 UUID v7은 Unix Epoch 기준 millisecond timestamp를 기반으로 한다. 즉, ID 앞부분에 시간 정보가 들어가고 나머지 부분에 랜덤성이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Timestamp + Random&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 UUID v4의 장점인 분산 생성 가능성과 낮은 충돌 가능성을 유지하면서, 시간순 정렬에 더 유리하도록 설계되었다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;UUID v4: 랜덤 &amp;rarr; 정렬 불리
UUID v7: 시간 + 랜덤 &amp;rarr; 정렬 유리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 데이터베이스 Primary Key로 UUID를 사용해야 한다면 UUID v4보다 UUID v7이 더 나은 선택이 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID 형식을 유지하고 싶은 경우&lt;/li&gt;
&lt;li&gt;분산 환경에서 각 서버가 독립적으로 ID를 생성해야 하는 경우&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. ULID&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;이름 그대로 사전식 정렬이 가능한 고유 ID다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;01ARZ3NDEKTSV4RRFFQ69G5FAV&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 128bit 구조이며, 크게 두 부분으로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;48bit Timestamp + 80bit Randomness&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞부분에 timestamp가 들어가기 때문에 문자열을 정렬했을 때 생성 시간순으로 정렬할 수 있다.&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;UUID와 동일하게 128bit 수준의 식별자다.&lt;/li&gt;
&lt;li&gt;문자열 길이가 26자로 UUID 문자열 표현보다 짧다.&lt;/li&gt;
&lt;li&gt;시간순 정렬이 가능하다.&lt;/li&gt;
&lt;li&gt;URL-safe하다.&lt;/li&gt;
&lt;li&gt;사람이 읽기에는 UUID보다 조금 더 간결하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 로그, 이벤트, 메시지, 외부 공개 ID 등에 사용하기 좋다.&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;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Snowflake&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake는 Twitter에서 사용한 것으로 유명한 분산 ID 생성 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake 계열의 핵심은 64bit 정수 하나에 시간, 서버 식별자, 시퀀스 번호를 나누어 담는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;64bit

1bit  : Sign
41bit : Timestamp
10bit : Worker ID
12bit : Sequence&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 필드의 의미는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sign&lt;/td&gt;
&lt;td&gt;일반적으로 사용하지 않는 부호 비트다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp&lt;/td&gt;
&lt;td&gt;기준 시각 이후 흐른 시간을 millisecond 단위로 저장한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker ID&lt;/td&gt;
&lt;td&gt;ID를 생성한 서버나 프로세스를 구분한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequence&lt;/td&gt;
&lt;td&gt;같은 millisecond 안에서 여러 ID를 만들기 위한 증가값이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 시각에 여러 서버가 동시에 ID를 생성하더라도 Worker ID가 다르면 충돌하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A: Worker ID = 1
Server B: Worker ID = 2
Server C: Worker ID = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 같은 서버에서 같은 millisecond 안에 여러 개의 ID를 생성하면 Sequence 값을 증가시켜 구분한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;timestamp = 1000, worker = 1, sequence = 0
timestamp = 1000, worker = 1, sequence = 1
timestamp = 1000, worker = 1, sequence = 2&lt;/code&gt;&lt;/pre&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;64bit 숫자 ID를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;시간순 정렬이 가능하다.&lt;/li&gt;
&lt;li&gt;중앙 서버 없이 각 서버가 직접 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;UUID보다 저장 공간이 작다.&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스에 비교적 유리하다.&lt;/li&gt;
&lt;li&gt;초당 대량의 ID 생성이 가능하다.&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;첫 번째는 Worker ID 관리다. 두 서버가 같은 Worker ID를 사용하면 같은 시간에 같은 Sequence를 생성할 수 있으므로 ID 충돌이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 서버 시간 문제다. Snowflake는 timestamp를 사용하기 때문에 서버 시간이 뒤로 돌아가면 중복 ID가 생성될 위험이 있다. 이를 Clock Rollback 문제라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 같은 millisecond 안에서 만들 수 있는 ID 수에 제한이 있다는 점이다. 예를 들어 Sequence가 12bit라면 한 Worker가 같은 millisecond 안에서 만들 수 있는 ID는 최대 4096개다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&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;/li&gt;
&lt;li&gt;숫자형 ID가 필요한 경우&lt;/li&gt;
&lt;li&gt;시간순 정렬이 필요한 경우&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스 효율이 중요한 경우&lt;/li&gt;
&lt;li&gt;Worker ID와 시간 동기화를 운영할 수 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. MongoDB ObjectId&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB를 사용하면 기본적으로 ObjectId를 자주 접하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectId는 12byte 식별자이며, 일반적으로 다음과 같은 정보를 포함한다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Timestamp + Random Value + Counter&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectId는 생성 시간 정보를 포함하므로 대략적인 시간순 정렬이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB 환경에서는 애플리케이션이 데이터베이스에 저장하기 전에 ObjectId를 미리 생성할 수도 있다. 즉, 중앙 DB에 insert한 뒤에야 ID를 알 수 있는 구조가 아니라, 클라이언트나 애플리케이션 레벨에서 먼저 ID를 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB ObjectId는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MongoDB를 사용하는 경우&lt;/li&gt;
&lt;li&gt;문서 생성 시점에 애플리케이션에서 ID를 미리 알고 싶은 경우&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. KSUID / Sonyflake&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KSUID와 Sonyflake는 Snowflake나 ULID와 비슷한 문제를 해결하기 위해 등장한 ID 생성 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KSUID&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KSUID는 K-Sortable Unique ID의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 정보를 포함하고 있어 정렬 가능한 ID를 만들 수 있다. 주로 이벤트, 로그, 분산 시스템의 리소스 ID 등에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID와 비슷하게 &amp;ldquo;시간순 정렬 가능한 문자열 ID&amp;rdquo;라는 관점에서 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sonyflake&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sonyflake는 Snowflake의 변형이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake와 마찬가지로 timestamp, machine id, sequence를 조합해서 ID를 생성한다. 다만 bit 배치나 시간 단위 등이 Snowflake와 다르다.&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;중앙 서버 없이 ID 생성&lt;/li&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황별 선택 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 선택할 때는 &amp;ldquo;무엇이 가장 좋은가?&amp;rdquo;보다 &amp;ldquo;현재 시스템에서 어떤 특성이 중요한가?&amp;rdquo;를 기준으로 봐야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 DB 기반 서비스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 데이터베이스를 사용하고 있고, 트래픽이 크지 않다면 Auto Increment나 Sequence가 가장 단순하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단순함 &amp;gt; 분산 확장성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 굳이 UUID나 Snowflake를 사용할 필요가 없을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 서버에서 독립적으로 ID를 생성해야 하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버가 여러 대이고, 중앙 ID 생성 서버에 의존하고 싶지 않다면 UUID v4, UUID v7, ULID, Snowflake 계열을 고려할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;분산 생성 가능성 &amp;gt; 중앙 통제&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 Primary Key로 사용할 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary Key는 인덱스 성능에 직접적인 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 랜덤한 UUID v4는 B-Tree 인덱스에서 불리할 수 있다. 시간순 정렬이 가능한 UUID v7, ULID, Snowflake가 더 적합할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부에 노출되는 ID가 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 노출되는 URL이나 API 응답에 ID가 포함된다면 예측 가능성을 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/users/1
/users/2
/users/3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 순차 ID는 다음 리소스의 ID를 쉽게 추측할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 UUID v4, UUID v7, ULID처럼 예측하기 어려운 ID가 더 적합할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자형 ID가 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 시스템에서는 문자열 ID보다 숫자형 ID가 더 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터베이스 저장 공간, 인덱스 크기, 다른 시스템과의 호환성 때문에 &lt;code&gt;BIGINT&lt;/code&gt; ID를 선호할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Snowflake 계열이 좋은 선택지가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 분산 시스템에서 고유 ID를 생성하는 방식은 다양하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식은 해결하려는 문제가 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;핵심 관점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto Increment / Sequence&lt;/td&gt;
&lt;td&gt;단순하고 빠른 DB 중심 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중앙 ID 서버&lt;/td&gt;
&lt;td&gt;전역적으로 통제되는 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v4&lt;/td&gt;
&lt;td&gt;중앙 조정 없는 랜덤 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v7&lt;/td&gt;
&lt;td&gt;시간 정렬 가능한 UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULID&lt;/td&gt;
&lt;td&gt;문자열 정렬 가능한 시간 기반 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;64bit 숫자 기반 분산 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB ObjectId&lt;/td&gt;
&lt;td&gt;MongoDB 문서 중심 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KSUID / Sonyflake&lt;/td&gt;
&lt;td&gt;시간 정렬 가능한 Snowflake 계열 변형&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 완벽한 ID 생성 방식은 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 선택할 때는 다음 질문을 먼저 해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID가 반드시 숫자여야 하는가?&lt;/li&gt;
&lt;li&gt;시간순 정렬이 필요한가?&lt;/li&gt;
&lt;li&gt;여러 서버가 독립적으로 ID를 생성해야 하는가?&lt;/li&gt;
&lt;li&gt;중앙 ID 생성 서버를 둘 수 있는가?&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스 성능이 중요한가?&lt;/li&gt;
&lt;li&gt;외부 사용자가 ID를 예측하면 문제가 되는가?&lt;/li&gt;
&lt;li&gt;Worker ID나 서버 시간 동기화를 운영할 수 있는가?&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;간단한 서비스라면 Auto Increment만으로 충분할 수 있다. 반대로 대규모 분산 시스템에서는 UUID v7, ULID, Snowflake 같은 방식을 고려해야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RFC 9562 - Universally Unique IDentifiers(UUIDs): &lt;a href=&quot;https://datatracker.ietf.org/doc/rfc9562/&quot;&gt;https://datatracker.ietf.org/doc/rfc9562/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ULID Specification: &lt;a href=&quot;https://github.com/ulid/spec&quot;&gt;https://github.com/ulid/spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Twitter Snowflake 개념: &lt;a href=&quot;https://en.wikipedia.org/wiki/Snowflake_ID&quot;&gt;https://en.wikipedia.org/wiki/Snowflake_ID&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/261</guid>
      <comments>https://soobarkbar.tistory.com/261#entry261comment</comments>
      <pubDate>Sat, 27 Jun 2026 14:51:02 +0900</pubDate>
    </item>
    <item>
      <title>블룸 필터 (Bloom Filter)</title>
      <link>https://soobarkbar.tistory.com/260</link>
      <description>&lt;p&gt;어떤 값이 집합에 &amp;quot;있을 수도 있음 / 없음은 확실함&amp;quot;을 매우 빠르고 메모리 효율적으로 판단하기 위한 확률적 자료 구조이다.&lt;/p&gt;
&lt;h2&gt;1. 목적&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;존재 여부 검사를 빠르게 하기 위함&lt;/li&gt;
&lt;li&gt;정확성보다 속도와 메모리 효율을 우선&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;대표적인 사용 목적&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;캐시 Penetration 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;존재 가능한 키만 Bloom Filter에 미리 등록&lt;/li&gt;
&lt;li&gt;요청 키가 Bloom Filter에서 &lt;code&gt;없음&lt;/code&gt;이면 Cache / DB 접근 자체를 차단&lt;/li&gt;
&lt;li&gt;존재하지 않는 ID에 대한 반복 DB 조회 방지 및 봇·악성 트래픽 방어&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;중복 요청 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;이미 처리된 요청 ID를 Bloom Filter에 기록하여 요청이 다시 들어올 경우 &amp;quot;이미 처리됨&amp;quot;으로 간주&lt;/li&gt;
&lt;li&gt;멱등성 보조 수단 및 결제, 메세지 발송 중복 방지 (정확성이 중요한 경우 단독 사용은 부적합) &lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;크롤러의 URL 중복 방문 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;방문한 URL을 Bloom Filter에 저장&lt;/li&gt;
&lt;li&gt;이미 방문했을 가능성이 있으면 스킵&lt;/li&gt;
&lt;li&gt;메모리 절약 및 대규모 URL 집합 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 핵심 특징&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;항목&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;False Positive&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;있다고 나올 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;False Negative&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;절대 발생하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;삭제&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;기본 Bloom Filter는 삭제 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;메모리&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;매우 적게 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;시간복잡도&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O(k) (k = 해시 함수 개수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;False Positive (거짓 양성) : 없는데 있다고 판단&lt;/p&gt;
&lt;p&gt;False Negative (거짓 음성) : 있는데 없다고 판단&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;3. 구조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비트 배열 (bit array): 크기 m&lt;/li&gt;
&lt;li&gt;해시 함수 k개: 서로 독립적인 해시 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 동작 방식&lt;/h2&gt;
&lt;h3&gt;값 추가 (insert)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;값을 k개의 해시 함수에 넣음&lt;/li&gt;
&lt;li&gt;각각의 해시 결과는 비트 배열의 인덱스를 의미&lt;/li&gt;
&lt;li&gt;해당 인덱스의 비트를 &lt;code&gt;1&lt;/code&gt;로 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;값 조회 (lookup)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;값을 동일하게 k개의 해시 함수에 넣음&lt;/li&gt;
&lt;li&gt;나온 인덱스들의 비트 확인&lt;/li&gt;
&lt;li&gt;결과&lt;ul&gt;
&lt;li&gt;하나라도 &lt;code&gt;0&lt;/code&gt; → 없음을 의미 (절대 없음)&lt;/li&gt;
&lt;li&gt;전부 &lt;code&gt;1&lt;/code&gt; → 있을 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 왜 False Positive가 생기는가&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;서로 다른 값들이 같은 비트 인덱스를 공유할 수 있음.&lt;/li&gt;
&lt;li&gt;비트 배열이 점점 &lt;code&gt;1&lt;/code&gt;로 채워질수록 새로운 값도 &amp;quot;있는 것처럼&amp;quot; 보일 확률 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번이라도 &lt;code&gt;0&lt;/code&gt;이면 바로 &amp;quot;없음&amp;quot; 판단 가능&lt;/li&gt;
&lt;li&gt;그래서 False Negative는 발생하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 장단점&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메모리 사용량이 매우 작음&lt;/li&gt;
&lt;li&gt;속도가 빠름 (CPU 캐시 친화적)&lt;/li&gt;
&lt;li&gt;대규모 트래픽에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100% 정확하지 않음&lt;/li&gt;
&lt;li&gt;삭제 불가 (→ Counting Bloom Filter로 보완, 비트를 카운터로 바꿔 삭제 가능)&lt;/li&gt;
&lt;li&gt;데이터가 많아질수록 정확도 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 무엇을 우선?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;n (들어갈 원소 수) 을 먼저 추정&lt;/li&gt;
&lt;li&gt;목표 false positive (p) 를 결정&lt;/li&gt;
&lt;li&gt;그에 맞춰 비트 배열 (m) 과 해시 함수 개수 (k) 를 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대략적으로:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최적 해시 개수는 k ≈ (m/n) * ln 2&lt;/li&gt;
&lt;li&gt;false positive는 대략 p ≈ (1 - e^{-kn/m})^k&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8. 다른 효율화 방안&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Counting Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;비트를 카운터로 바꾸므로 삭제 가능&lt;/li&gt;
&lt;li&gt;단점은 메모리를 더 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Scalable Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;n이 예상보다 커졌을 때 성능이 망가지는 문제 해결&lt;/li&gt;
&lt;li&gt;Bloom Filter를 하나 더 붙여가며 확장 (계층 구조)&lt;/li&gt;
&lt;li&gt;과소설계 리스트를 완화시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Partitioned Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;비트 배열을 k개의 구간으로 나누고 해시마다 구간을 고정&lt;/li&gt;
&lt;li&gt;비트 분포가 더 균등해져 성능이 안정적일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;해시 함수 최적화 (더블 해싱)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;해시 2개로 k개를 파싱&lt;/li&gt;
&lt;li&gt;빠르고 구현이 단순&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/Structure</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/260</guid>
      <comments>https://soobarkbar.tistory.com/260#entry260comment</comments>
      <pubDate>Thu, 18 Jun 2026 23:17:12 +0900</pubDate>
    </item>
    <item>
      <title>Redis가 정말 싱글 쓰레드로 작동할까?</title>
      <link>https://soobarkbar.tistory.com/259</link>
      <description>&lt;p&gt;Redis는 클라이언트 요청을 이벤트 루프 기반으로 처리하고, Redis 자료구조를 직접 읽고 쓰는 명령 실행은 메인 쓰레드 중심으로 동작한다. 하지만 파일 닫기, AOF fsync, 메모리 해제 같은 일부 느린 작업은 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;p&gt;그래서 Redis를 정확히 이해하려면 다음 두 가지를 구분해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Redis 명령 실행 경로
   - GET, SET, INCR, HSET 같은 명령 실행
   - 메인 쓰레드 중심

2. Redis 프로세스 전체
   - 메인 쓰레드 외에도 백그라운드 쓰레드 존재
   - 파일 닫기, AOF fsync, lazy free 등 처리&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;1. Redis가 싱글 쓰레드라는 말의 의미&lt;/h2&gt;
&lt;p&gt;Redis가 싱글 쓰레드라고 할 때 핵심은 &lt;strong&gt;데이터를 읽고 쓰는 명령 실행 경로가 기본적으로 하나의 메인 쓰레드에서 처리된다&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 명령이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
GET user:1
INCR view:post:100
LPUSH queue:order 1001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령들은 Redis 내부의 메인 이벤트 루프에서 순차적으로 실행된다.&lt;/p&gt;
&lt;p&gt;즉, 여러 클라이언트가 동시에 요청을 보내더라도 Redis는 내부적으로 명령을 하나씩 꺼내 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client A -&amp;gt; SET user:1
Client B -&amp;gt; GET user:1
Client C -&amp;gt; INCR view:post:100

Redis Main Thread
1. SET user:1 처리
2. GET user:1 처리
3. INCR view:post:100 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조 덕분에 Redis는 명령 실행 중에 여러 쓰레드가 같은 자료구조를 동시에 수정하는 문제를 피할 수 있다.&lt;/p&gt;
&lt;p&gt;멀티 쓰레드 구조라면 다음과 같은 고민이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thread 1: user:1 수정
Thread 2: user:1 조회
Thread 3: user:1 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 동시성 제어를 위해 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 처리하기 때문에, 내부 자료구조 접근에 대해 복잡한 lock 경합을 크게 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;이게 Redis가 단순하면서도 빠른 이유 중 하나다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 그럼 Redis 프로세스에는 쓰레드가 하나만 있을까?&lt;/h2&gt;
&lt;p&gt;그건 아니다.&lt;/p&gt;
&lt;p&gt;Redis에는 메인 쓰레드 외에도 백그라운드 작업을 처리하기 위한 쓰레드들이 존재한다.&lt;/p&gt;
&lt;p&gt;책에서 본 것처럼 Redis는 표면적으로는 싱글 쓰레드처럼 설명되지만, 실제로는 메인 쓰레드 외에 백그라운드 쓰레드가 함께 동작한다.&lt;/p&gt;
&lt;p&gt;대표적으로 Redis의 background I/O, 즉 BIO 쓰레드는 다음과 같은 작업을 담당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
- 클라이언트 연결 처리
- 명령 파싱
- 명령 실행
- 응답 생성

Background I/O Threads
- BIO_CLOSE_FILE
- BIO_AOF_FSYNC
- BIO_LAZY_FREE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 백그라운드 쓰레드가 Redis의 핵심 데이터 명령을 병렬로 실행하는 것이 아니라는 점이다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;HSET&lt;/code&gt; 같은 명령을 여러 쓰레드가 동시에 나눠 실행하는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;백그라운드 쓰레드는 주로 메인 쓰레드가 직접 처리하면 오래 걸릴 수 있는 작업을 뒤로 넘겨서 처리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Redis의 3가지 백그라운드 쓰레드&lt;/h2&gt;
&lt;p&gt;Redis의 백그라운드 I/O 쓰레드는 역할별로 나누어 볼 수 있다.&lt;/p&gt;
&lt;p&gt;대표적으로 다음 3가지가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. BIO_CLOSE_FILE
   - 파일 닫기 작업 처리

2. BIO_AOF_FSYNC
   - AOF 파일 fsync 처리

3. BIO_LAZY_FREE
   - 큰 객체의 메모리 해제 작업 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 쓰레드는 Redis 명령을 대신 실행하는 것이 아니라, &lt;strong&gt;메인 쓰레드가 오래 붙잡고 있으면 위험한 보조 작업을 비동기적으로 처리한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3-1. BIO_CLOSE_FILE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_CLOSE_FILE&lt;/code&gt;은 이름 그대로 파일을 닫는 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;일반적으로 파일을 닫는 &lt;code&gt;close()&lt;/code&gt; 작업은 매우 빠를 것처럼 보인다. 하지만 운영체제나 파일 시스템 상황에 따라 파일을 닫는 과정이 예상보다 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 Redis가 AOF 파일을 교체하거나, 임시 파일을 사용한 뒤 닫아야 하는 상황이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: 파일 작업 완료
2. Main Thread: 파일 닫기 작업을 BIO_CLOSE_FILE 큐에 등록
3. BIO_CLOSE_FILE Thread: 실제 close 작업 수행
4. Main Thread: 클라이언트 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 메인 쓰레드가 직접 파일 닫기를 처리하다가 지연되면, Redis는 그동안 다른 클라이언트 명령을 처리하지 못한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;나쁜 흐름

Main Thread
1. SET user:1 처리
2. 파일 close 처리 시작
3. close 작업 지연
4. GET user:2 대기
5. INCR view:1 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 Redis는 파일 닫기처럼 가끔 지연될 수 있는 작업을 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;개선된 흐름

Main Thread
1. SET user:1 처리
2. close 작업을 백그라운드 큐에 등록
3. GET user:2 처리
4. INCR view:1 처리

BIO_CLOSE_FILE Thread
1. 실제 파일 close 수행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 파일 닫기가 Redis 데이터 명령 실행과 직접 관련된 작업은 아니라는 점이다. 따라서 백그라운드로 넘겨도 Redis의 명령 처리 일관성을 해치지 않는다.&lt;/p&gt;
&lt;h3&gt;3-2. BIO_AOF_FSYNC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 AOF(Append Only File)의 &lt;code&gt;fsync&lt;/code&gt; 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;Redis에서 AOF를 사용하면 쓰기 명령이 AOF 파일에 기록된다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 명령을 실행했다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 켜두면 Redis는 이 쓰기 명령들을 파일에 append 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendonly.aof
SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 파일에 썼다는 것과 디스크에 안전하게 반영됐다는 것은 다르다.&lt;/p&gt;
&lt;p&gt;운영체제는 성능을 위해 파일 쓰기를 메모리 버퍼에 잠시 보관할 수 있다. 이때 &lt;code&gt;fsync&lt;/code&gt;는 “버퍼에 있는 내용을 실제 디스크에 반영해라”라고 요청하는 작업이다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;code&gt;fsync&lt;/code&gt;가 디스크 I/O 작업이기 때문에 느려질 수 있다는 점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread가 fsync까지 직접 처리하는 경우

1. SET user:1 처리
2. AOF 파일에 기록
3. fsync 수행
4. 디스크 응답 대기
5. 다음 명령 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 흐름에서는 디스크가 느려지는 순간 Redis 전체 응답 시간이 흔들릴 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;appendfsync everysec&lt;/code&gt; 설정에서는 보통 fsync를 매 명령마다 하지 않고, 약 1초 단위로 백그라운드에서 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
1. SET user:1 처리
2. AOF 버퍼 또는 파일에 append
3. 클라이언트에 응답
4. 다음 명령 처리

BIO_AOF_FSYNC Thread
1. 주기적으로 AOF 파일 fsync 수행
2. 디스크 반영 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 장점은 메인 쓰레드가 디스크 fsync 때문에 오래 멈추는 상황을 줄일 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;다만 트레이드오프도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 매 쓰기마다 fsync
- 내구성 강함
- 성능 저하 가능성 큼

appendfsync everysec
- 보통 1초 단위 fsync
- 성능과 내구성의 균형
- 장애 시 최근 약 1초 데이터 유실 가능

appendfsync no
- fsync를 운영체제에 맡김
- 성능은 좋을 수 있음
- 내구성 보장은 약함&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, &lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 Redis의 명령 실행을 빠르게 유지하면서도 AOF 내구성을 확보하기 위한 백그라운드 작업이라고 볼 수 있다.&lt;/p&gt;
&lt;h3&gt;3-3. BIO_LAZY_FREE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.&lt;/p&gt;
&lt;p&gt;Redis에서 key를 삭제할 때 실제로는 두 가지 일이 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. keyspace에서 key 제거
2. 해당 value가 사용하던 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;작은 문자열 key라면 이 작업은 매우 빠르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL user:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 value가 매우 큰 List, Set, Sorted Set, Hash라면 이야기가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 &lt;code&gt;big:list&lt;/code&gt;에 수백만 개의 요소가 들어 있다면, key를 제거하는 것보다 내부 요소들의 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL big:list

Main Thread
1. keyspace에서 big:list 찾기
2. key 제거
3. list 내부 요소 메모리 해제
4. 해제가 끝날 때까지 다른 명령 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt;를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 먼저 keyspace에서 제거한다. 그러면 이후 Redis 명령에서는 해당 key에 접근할 수 없다.&lt;/p&gt;
&lt;p&gt;그리고 실제 메모리 해제 작업은 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt; 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UNLINK big:list

Main Thread
1. keyspace에서 big:list 제거
2. 메모리 해제 작업을 BIO_LAZY_FREE 큐에 등록
3. 바로 다음 명령 처리

BIO_LAZY_FREE Thread
1. big:list 내부 요소 메모리 해제
2. 사용하던 메모리 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조가 안전한 이유는 메인 쓰레드가 먼저 keyspace에서 key를 제거하기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread가 keyspace에서 key 제거
2. 이후 어떤 Redis 명령도 그 key에 접근할 수 없음
3. Background Thread가 실제 value 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 백그라운드 쓰레드가 Redis 자료구조를 마음대로 수정하는 것이 아니라, 이미 접근 불가능해진 객체의 메모리를 정리하는 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 &lt;code&gt;UNLINK&lt;/code&gt;뿐 아니라 설정에 따라 만료, eviction, &lt;code&gt;FLUSHDB ASYNC&lt;/code&gt;, &lt;code&gt;FLUSHALL ASYNC&lt;/code&gt; 같은 작업에서도 사용될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key
FLUSHDB ASYNC
FLUSHALL ASYNC&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;관련 설정으로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정들은 eviction, expire, 서버 내부 삭제, replica flush 같은 상황에서 메모리 해제를 가능한 한 백그라운드로 넘길지 결정한다.&lt;/p&gt;
&lt;p&gt;정리하면 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 Redis의 싱글 쓰레드 구조에서 가장 체감하기 쉬운 백그라운드 쓰레드다. 큰 key 삭제나 전체 flush 작업이 메인 쓰레드를 오래 막지 않도록 도와주기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 왜 백그라운드 쓰레드가 필요할까?&lt;/h2&gt;
&lt;p&gt;Redis는 메인 쓰레드가 클라이언트 요청을 순차적으로 처리한다.&lt;/p&gt;
&lt;p&gt;이 구조에서는 하나의 작업이 오래 걸리면 뒤에 있는 요청들이 모두 기다려야 한다.&lt;/p&gt;
&lt;p&gt;예를 들어 아주 큰 key를 삭제한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 &lt;code&gt;big:list&lt;/code&gt;가 매우 큰 리스트라면, 삭제 과정에서 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;Redis 메인 쓰레드가 이 작업을 끝까지 직접 처리하면 그동안 다른 요청 처리가 지연될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. DEL big:list 처리 시작
2. 큰 메모리 해제 작업 수행
3. 작업이 끝날 때까지 다른 요청 대기
4. 이후 GET, SET 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt; 같은 명령을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 논리적으로 제거한 뒤, 실제 메모리 해제 작업은 백그라운드에서 처리할 수 있게 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: key를 keyspace에서 제거
2. BIO_LAZY_FREE Thread: 실제 메모리 해제
3. Main Thread: 다음 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis가 백그라운드 쓰레드를 사용하는 이유는 핵심 명령 실행을 병렬화하기 위해서라기보다, &lt;strong&gt;메인 이벤트 루프가 오래 막히지 않도록 느린 작업을 분리하기 위해서&lt;/strong&gt;라고 보는 편이 맞다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Redis 6부터는 I/O Thread도 있다&lt;/h2&gt;
&lt;p&gt;Redis 6부터는 I/O Thread 기능이 추가되었다.&lt;/p&gt;
&lt;p&gt;여기서 다시 헷갈릴 수 있다.&lt;/p&gt;
&lt;p&gt;“Redis 6부터 멀티 쓰레드라면 이제 명령도 병렬로 실행되는 건가?”&lt;/p&gt;
&lt;p&gt;그렇지는 않다.&lt;/p&gt;
&lt;p&gt;Redis 6의 I/O Thread는 주로 네트워크 I/O를 병렬화하기 위한 기능이다.&lt;/p&gt;
&lt;p&gt;Redis 요청 처리를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 클라이언트 요청 읽기
2. 명령 파싱
3. 명령 실행
4. 응답 쓰기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존에는 이 흐름 대부분을 메인 쓰레드가 처리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
Read -&amp;gt; Parse -&amp;gt; Execute -&amp;gt; Write&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 6 이후에는 설정에 따라 일부 네트워크 읽기/쓰기 작업을 I/O Thread가 도와줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I/O Thread
- socket read
- socket write
- command parsing 일부

Main Thread
- command execute
- Redis data structure access
- reply 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis 6 이후에도 핵심은 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;네트워크 I/O 일부는 멀티 쓰레드 가능
하지만 Redis 자료구조를 변경하는 명령 실행은 메인 쓰레드 중심&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 Redis의 I/O Thread와 앞에서 설명한 BIO 쓰레드는 역할이 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BIO Thread
- 파일 닫기
- AOF fsync
- lazy free
- 메인 쓰레드가 오래 막힐 수 있는 내부 작업 처리

I/O Thread
- 클라이언트 socket read/write 보조
- 네트워크 I/O 병목 완화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 둘 다 Redis 프로세스 내부의 추가 쓰레드지만 목적이 다르다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO Thread&lt;/code&gt;는 Redis 내부의 느린 작업을 백그라운드로 넘기기 위한 쓰레드이고, &lt;code&gt;I/O Thread&lt;/code&gt;는 클라이언트 네트워크 I/O 처리량을 높이기 위한 쓰레드다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Redis가 싱글 쓰레드인데도 빠른 이유&lt;/h2&gt;
&lt;p&gt;Redis가 빠른 이유를 단순히 “메모리를 사용하기 때문”이라고만 보면 부족하다.&lt;/p&gt;
&lt;p&gt;물론 Redis는 대부분의 데이터를 메모리에 두기 때문에 디스크 기반 DB보다 빠른 접근이 가능하다.&lt;/p&gt;
&lt;p&gt;하지만 그 외에도 몇 가지 이유가 있다.&lt;/p&gt;
&lt;h3&gt;1) 명령 실행이 단순하다&lt;/h3&gt;
&lt;p&gt;Redis 명령은 대부분 짧고 빠르게 끝난다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;GET user:1
SET token:abc &amp;quot;...&amp;quot;
INCR view:post:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 명령은 일반적인 RDBMS 쿼리처럼 복잡한 실행계획을 세우거나, 여러 테이블을 조인하거나, 디스크 블록을 많이 읽는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;대부분 메모리의 자료구조에 직접 접근한다.&lt;/p&gt;
&lt;h3&gt;2) I/O Multiplexing을 사용한다&lt;/h3&gt;
&lt;p&gt;Redis는 하나의 쓰레드로도 여러 클라이언트 연결을 다룰 수 있다.&lt;/p&gt;
&lt;p&gt;이때 사용하는 방식이 I/O Multiplexing이다.&lt;/p&gt;
&lt;p&gt;쉽게 말하면, 클라이언트마다 쓰레드를 하나씩 만드는 것이 아니라, 하나의 이벤트 루프가 여러 소켓을 감시하다가 준비된 요청만 처리하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client 1
Client 2
Client 3
Client 4
   ↓
Event Loop
   ↓
Ready 된 요청만 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조는 Node.js의 이벤트 루프와도 비슷하게 이해할 수 있다.&lt;/p&gt;
&lt;h3&gt;3) Lock 경합이 적다&lt;/h3&gt;
&lt;p&gt;멀티 쓰레드로 명령을 동시에 실행하면 공유 데이터에 대한 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 순차적으로 처리하기 때문에 lock 경합 비용이 작다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;멀티 쓰레드 방식
- 동시에 실행 가능
- lock 필요
- context switching 비용 발생
- 동시성 버그 가능성 증가

Redis 방식
- 명령 실행은 순차 처리
- lock 경합 감소
- 구현 단순
- 예측 가능한 실행 흐름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis는 이 단순한 구조를 선택한 대신, 오래 걸리는 작업을 피하거나 백그라운드로 넘기는 방식으로 성능을 유지한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 싱글 쓰레드 구조에서 조심해야 할 명령&lt;/h2&gt;
&lt;p&gt;Redis가 빠르다고 해서 모든 명령이 항상 안전한 것은 아니다.&lt;/p&gt;
&lt;p&gt;싱글 쓰레드 구조에서는 오래 걸리는 명령 하나가 전체 요청 처리에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;대표적인 예가 &lt;code&gt;KEYS&lt;/code&gt;다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;KEYS *&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령은 전체 keyspace를 순회한다.&lt;/p&gt;
&lt;p&gt;운영 환경에서 key가 많다면 메인 쓰레드가 오랫동안 이 작업을 수행하게 되고, 그동안 다른 요청이 밀릴 수 있다.&lt;/p&gt;
&lt;p&gt;개선된 방식은 &lt;code&gt;SCAN&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SCAN 0 MATCH user:* COUNT 100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt;은 전체를 한 번에 훑는 것이 아니라 cursor 기반으로 조금씩 순회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;KEYS
- 전체 key를 한 번에 조회
- 메인 쓰레드가 오래 점유될 수 있음
- 운영 환경에서 위험

SCAN
- cursor 기반으로 나눠 조회
- 한 번에 처리하는 양을 조절 가능
- 운영 환경에서 상대적으로 안전&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 key 삭제도 마찬가지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:key&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 객체를 삭제할 때는 다음처럼 &lt;code&gt;UNLINK&lt;/code&gt;를 고려할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL
- key 삭제와 메모리 해제를 동기적으로 처리할 수 있음
- 큰 key에서는 지연 발생 가능

UNLINK
- keyspace에서는 빠르게 제거
- 실제 메모리 해제는 BIO_LAZY_FREE가 백그라운드 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 사용하는 환경에서는 &lt;code&gt;appendfsync&lt;/code&gt; 설정도 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;appendonly yes
appendfsync everysec&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보통은 &lt;code&gt;everysec&lt;/code&gt;가 성능과 내구성의 균형점으로 많이 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 쓰기마다 디스크 동기화
- 안전하지만 느려질 수 있음

appendfsync everysec
- 약 1초 단위로 fsync
- BIO_AOF_FSYNC가 백그라운드에서 처리
- 일반적인 운영 환경에서 많이 사용

appendfsync no
- 운영체제 정책에 맡김
- 빠를 수 있지만 장애 시 유실 범위가 커질 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;8. Redis를 “싱글 쓰레드”라고만 말하면 부족한 이유&lt;/h2&gt;
&lt;p&gt;Redis를 이해할 때는 관점을 나눠야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 명령 실행 관점
   - 대부분 싱글 쓰레드
   - Redis 자료구조 접근은 메인 쓰레드 중심

2. Redis 프로세스 관점
   - 메인 쓰레드 외 백그라운드 쓰레드 존재
   - BIO_CLOSE_FILE, BIO_AOF_FSYNC, BIO_LAZY_FREE 등 처리

3. Redis 6 이후 네트워크 I/O 관점
   - I/O Thread 사용 가능
   - socket read/write 일부를 병렬 처리 가능

4. Persistence 관점
   - AOF fsync는 백그라운드 쓰레드가 도울 수 있음
   - RDB snapshot, AOF rewrite는 별도 프로세스가 관여할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 정확히 표현하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Redis는 클라이언트 명령 실행과 데이터 접근은 주로 싱글 쓰레드로 처리하지만, 백그라운드 I/O 작업과 네트워크 I/O 최적화를 위해 여러 쓰레드 또는 별도 프로세스를 사용할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;9. 실제 운영에서는 어떻게 봐야 할까?&lt;/h2&gt;
&lt;p&gt;Redis를 운영할 때는 “싱글 쓰레드니까 CPU 코어 하나만 보면 된다”라고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;물론 명령 실행 자체는 메인 쓰레드 병목이 될 수 있다.&lt;br&gt;그래서 Redis는 보통 하나의 인스턴스가 하나의 CPU 코어를 강하게 사용하는 형태로 나타날 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 다음 요소들도 함께 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;- 메인 쓰레드 CPU 사용률
- 네트워크 I/O 병목
- 큰 key 삭제 여부
- AOF fsync 지연
- RDB snapshot 또는 AOF rewrite 시점
- slowlog
- latency monitor
- lazy free pending object 증가 여부&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 Redis가 느려졌을 때는 단순히 CPU만 볼 게 아니라 다음 명령들을 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SLOWLOG GET 10&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO stats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO commandstats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO persistence&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO memory&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;INFO memory&lt;/code&gt;에서는 lazy free와 관련된 지표를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lazyfree_pending_objects&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 값이 계속 증가한다면 백그라운드에서 해제해야 할 객체가 많이 쌓이고 있다는 의미로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;또 AOF를 사용하는 경우에는 &lt;code&gt;INFO persistence&lt;/code&gt;를 통해 AOF rewrite, fsync, persistence 관련 상태를 함께 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 마무리&lt;/h2&gt;
&lt;p&gt;Redis는 흔히 싱글 쓰레드라고 설명된다.&lt;/p&gt;
&lt;p&gt;이 말은 틀린 말은 아니지만, 정확히는 &lt;strong&gt;Redis의 핵심 명령 실행 경로가 싱글 쓰레드에 가깝다&lt;/strong&gt;는 의미로 이해해야 한다.&lt;/p&gt;
&lt;p&gt;Redis 프로세스 전체를 보면 메인 쓰레드 외에도 백그라운드 쓰레드가 존재하고, Redis 6 이후에는 네트워크 I/O를 위한 I/O Thread도 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Redis는 완전한 의미의 단일 쓰레드 프로그램은 아니다.

하지만 Redis의 핵심 데이터 명령 실행은 여전히 메인 쓰레드 중심으로 동작한다.

BIO_CLOSE_FILE은 파일 닫기 작업을 백그라운드에서 처리한다.

BIO_AOF_FSYNC는 AOF fsync 작업을 백그라운드에서 처리한다.

BIO_LAZY_FREE는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.

Redis 6 이후의 I/O Thread도 명령 실행을 병렬화하는 것이 아니라 네트워크 I/O 병목을 줄이기 위한 기능이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 Redis의 싱글 쓰레드 모델은 단순한 제약이 아니라, 빠른 메모리 접근과 이벤트 루프, lock 없는 명령 실행을 조합한 설계 선택이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;Redis를 사용할 때 중요한 것은 “싱글 쓰레드냐, 멀티 쓰레드냐”를 외우는 것이 아니라, &lt;strong&gt;어떤 작업이 메인 쓰레드를 막고 어떤 작업이 백그라운드로 분리될 수 있는지 이해하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Redis 공식 문서 - Latency optimization&lt;/li&gt;
&lt;li&gt;Redis 공식 문서 - INFO command&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - The little-known feature of Redis 4.0 that will speed up your applications&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - Redis 8.0-M03 is out. Even more performance &amp;amp; new features&lt;/li&gt;
&lt;li&gt;Redis GitHub source - &lt;code&gt;src/bio.c&lt;/code&gt;, &lt;code&gt;src/bio.h&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/259</guid>
      <comments>https://soobarkbar.tistory.com/259#entry259comment</comments>
      <pubDate>Sun, 7 Jun 2026 17:44:48 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 풀스캔(Index Full Scan) 훑어보기</title>
      <link>https://soobarkbar.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 보다 보면 &lt;code&gt;range&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt; 같은 접근 방식과 함께&lt;br /&gt;DBMS에 따라 &lt;b&gt;인덱스 풀스캔&lt;/b&gt;에 가까운 동작이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 보면 비효율적으로 느껴질 수 있지만, 실제로는 상황에 따라 꽤 합리적인 선택이 된다.&lt;br /&gt;중요한 건 &lt;b&gt;&amp;ldquo;인덱스를 일부만 읽는 스캔이 아니라, 인덱스 전체를 읽는 스캔&amp;rdquo;&lt;/b&gt; 이라는 점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인덱스 풀스캔이 무엇이며, 어떻게 동작하는가&lt;/h2&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;br /&gt;예를 들어 &lt;code&gt;where id = 10&lt;/code&gt; 같은 조건에서는 인덱스의 일부만 읽으면 된다.&lt;br /&gt;그런데 어떤 쿼리는 조건 검색보다도 &lt;b&gt;정렬된 순서&lt;/b&gt;나 &lt;b&gt;인덱스에 담긴 컬럼 자체&lt;/b&gt;가 더 중요하다.&lt;br /&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;h3 data-ke-size=&quot;size23&quot;&gt;1) 왜 인덱스 전체를 읽는가&lt;/h3&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;한 건을 저장하는 데이터 크기가 더 작아서 같은 양을 읽을 때 필요한 블록 수가 더 적을 수 있다. 용량을 많이 차지하는 데이터 타입 (varchar(1000), blob, ...) 이 있다면 하나의 블록에 들어갈 수 있는 로우 수가 적어지기 때문에 테이블의 폭이 더 넓을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B-Tree 인덱스는 키 순서대로 정렬되어 있다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스 안에 있으면 테이블까지 안 가도 되는 경우가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;전체를 읽더라도 테이블보다 인덱스가 비용이 더 싸다&amp;rdquo;라고 판단되면 인덱스 풀스캔이 나올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 동작 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스 루트/브랜치 블록을 따라 첫 리프 블록으로 이동한다&lt;/li&gt;
&lt;li&gt;리프 블록을 처음부터 끝까지 읽는다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스에 다 있으면 결과를 바로 반환한다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스에 없으면 테이블을 추가 조회한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE employees (
    emp_id INT,
    dept_id INT,
    emp_name VARCHAR(100),
    hire_date DATE,
    salary INT
);

CREATE INDEX idx_emp_hire_date ON employees(hire_date);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 hire_date만 필요하고, 정렬도 hire_date 기준이다.&lt;br /&gt;이 경우 테이블을 전부 읽고 정렬하는 것보다, idx_emp_hire_date 인덱스를 처음부터 끝까지 읽는 편이 더 유리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인덱스 풀스캔이 유리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 &amp;ldquo;풀스캔&amp;rdquo;이라는 이름 때문에 무조건 나쁜 방식처럼 보이지만, 실제로는 꽤 유용한 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 필요한 컬럼이 인덱스에 다 들어 있는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 가장 유리하다.&lt;br /&gt;인덱스만 읽고 결과를 만들 수 있으니 테이블 접근을 줄일 수 있다. MySQL에서는 이런 경우 Using index가 붙는 covering index 형태가 될 수 있고, Oracle에서도 인덱스만으로 결과를 만들 수 있을 때 빠른 경로를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary);

SELECT dept_id, salary
FROM employees
ORDER BY dept_id, salary;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 컬럼도 인덱스에 있고, 정렬 순서도 인덱스 순서와 맞는다.&lt;br /&gt;이런 경우 인덱스를 전체로 읽는 방식이 꽤 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 결과를 이미 정렬된 상태로 가져오고 싶은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 키 순서대로 저장된다.&lt;br /&gt;그래서 ORDER BY가 인덱스 순서와 맞아떨어지면 별도의 정렬 작업을 줄일 수 있다. MySQL은 인덱스를 이용해 정렬을 피할 수 있고, Oracle의 일반적인 index full scan도 인덱스 키 순서를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전체를 읽고 sort 하는 것보다, hire_date 인덱스를 순서대로 읽는 편이 더 나을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 테이블보다 인덱스가 훨씬 가벼운 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 컬럼이 많고 행 크기가 큰 반면, 인덱스는 상대적으로 얇다.&lt;br /&gt;그래서 &amp;ldquo;어차피 많이 읽어야 한다면 차라리 인덱스를 읽자&amp;rdquo;라는 판단이 가능하다. MySQL 문서도 covering index의 경우 인덱스만 스캔하는 것이 보통 테이블 전체를 읽는 것보다 빠를 수 있다고 설명한다.&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;employees 테이블에 큰 TEXT, JSON, BLOB 컬럼이 많다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT dept_id
FROM employees
ORDER BY dept_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 dept_id 인덱스만 훑는 편이 테이블 전체를 읽는 것보다 부담이 적을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인덱스 풀스캔이 불리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 인덱스 풀스캔이 비효율적인 경우도 분명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 대부분 행에 대해 다시 테이블 접근이 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에는 키 값만 있고 실제 필요한 컬럼은 테이블에 있는 경우가 많다.&lt;br /&gt;이때 인덱스를 전체로 읽은 뒤, 각 행마다 다시 테이블을 찾아가면 랜덤 I/O가 많이 생길 수 있다.&lt;br /&gt;이 경우는 차라리 테이블 풀스캔이 더 나을 수 있다. Oracle도 다른 접근 경로보다 비용이 높으면 full table scan을 선택한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hire_date 인덱스는 정렬에는 도움이 되지만, SELECT * 이므로 결국 대부분 행을 다시 테이블에서 읽어야 한다.&lt;br /&gt;데이터 양이 많으면 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 조건으로 일부만 읽어도 되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상이 일부 구간에 불과하다면 인덱스 전체를 읽는 것보다 레인지 스캔이 더 낫다.&lt;br /&gt;MySQL 문서도 range access는 인덱스 값의 일부 구간만 읽는 방식이라고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT emp_id, hire_date
FROM employees
WHERE hire_date BETWEEN '2025-01-01' AND '2025-01-31';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 전체 인덱스를 다 볼 필요가 없다.&lt;br /&gt;조건에 맞는 구간만 읽는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 인덱스 정렬 순서가 쿼리와 맞지 않는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 전체로 읽더라도 원하는 정렬이나 필터링 조건과 맞지 않으면 이점이 줄어든다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_id ON employees(dept_id);

SELECT salary
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인덱스는 dept_id 기준인데, 쿼리는 hire_date 기준으로 정렬한다.&lt;br /&gt;이런 경우 idx_emp_dept_id를 풀스캔해도 정렬 이점을 얻기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL과 Oracle에서 인덱스 풀스캔이 어떻게 다르게 동작하는지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &amp;ldquo;인덱스를 많이 읽는다&amp;rdquo;는 점은 비슷하지만, 실행 계획에서의 의미와 세부 동작은 다르게 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 인덱스 전체를 읽는 방식도 두 가지로 나눠서 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FULL SCAN&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;ORDER BY를 만족시키는 데 유리하다&lt;/li&gt;
&lt;li&gt;보통 단일 블록 I/O 성격으로 이해한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FAST FULL SCAN&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;멀티블록 I/O, 병렬 처리 활용이 가능하다&lt;/li&gt;
&lt;li&gt;물리적으로 디스크에 저장된 순서대로 블록을 읽기 때문에 결과가 인덱스 순서로 나오지 않을 수 있다&lt;/li&gt;
&lt;li&gt;인덱스에 필요한 컬럼이 모두 있을 때 full table scan의 대안이 될 수 있다&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;2) MySQL은 보통 type=index와 Using index를 같이 해석해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 실행 계획에서는 Oracle처럼 INDEX FULL SCAN, INDEX FAST FULL SCAN이라는 이름으로 세분화해서 보여주지 않는다.&lt;br /&gt;대신 EXPLAIN 결과에서 type=index가 나오면 인덱스를 처음부터 끝까지 읽는 스캔에 가깝게 해석하는 경우가 많다.&lt;br /&gt;그리고 Extra에 Using index가 있으면 인덱스만으로 결과를 처리하는 covering index 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Oracle은 &amp;ldquo;정렬 보장 여부&amp;rdquo;까지 구분해서 보고, MySQL은 실행 계획 컬럼 조합으로 해석하는 편이다&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;표현 방식&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN, INDEX FAST FULL SCAN 등으로 구분&lt;/td&gt;
&lt;td&gt;보통 type=index, Extra=Using index 등 조합으로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 순서 활용&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN은 키 순서 활용 가능&lt;/td&gt;
&lt;td&gt;인덱스 순서를 활용할 수 있지만 Oracle처럼 명시적으로 scan 이름이 나뉘진 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스만 읽는 빠른 전체 스캔&lt;/td&gt;
&lt;td&gt;INDEX FAST FULL SCAN 존재&lt;/td&gt;
&lt;td&gt;covering index scan 형태로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 단순히 &amp;ldquo;인덱스를 썼으니 좋은 것&amp;rdquo;, 또는 &amp;ldquo;풀스캔이니 무조건 나쁜 것&amp;rdquo;으로 보면 안 된다.&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;정렬 이점이나 covering index가 있으면 유리할 수 있다&lt;/li&gt;
&lt;li&gt;반대로 테이블 재접근이 많아지면 비효율적일 수 있다&lt;/li&gt;
&lt;li&gt;Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분해서 봐야 하고,&lt;br /&gt;MySQL은 type=index, Using index 등을 조합해서 해석해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 볼 때는 &amp;ldquo;인덱스를 탔는가&amp;rdquo;만 보지 말고, 인덱스를 어떻게 탔는가까지 같이 봐야 한다.&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/258</guid>
      <comments>https://soobarkbar.tistory.com/258#entry258comment</comments>
      <pubDate>Sat, 18 Apr 2026 16:57:51 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] MacOS에서 카프카 매니저 설치</title>
      <link>https://soobarkbar.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. github에서 tar.gz 파일 다운로드 (&lt;a href=&quot;https://github.com/yahoo/CMAK&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yahoo/CMAK&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZKBf%2FbtsGrW9BOEb%2FzHJGyiMHUMJgQNSevDX750%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;2312&quot; height=&quot;1114&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 압축 해제 후 설치한 폴더 내에서 ./sbt clean dist 명령어 실행&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Cannot use JVMCI compiler: No JVMCI compiler found&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;명령어 실행 시 위 에러가 발생한 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/927&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&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;size18&quot;&gt;3. target/universal 경로에 생성된 .zip 파일을 원하는 위치에 압축 해제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. conf/application.conf 파일 수정 (로컬에 주키퍼를 띄웠기 때문에 아래와 같이 수정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnqPxL%2FbtsGpKXqiXR%2FuNZJ1NEpLhVKhSsL3cRH70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;113&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5.  ./bin/cmak 명령어 실행 후, localhost:9000 접속&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAkO6m%2FbtsGprqkxz5%2F5wl7mrfkvQC1Mwcp7hH4Bk%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;3340&quot; height=&quot;604&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&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;!&amp;nbsp;@7jn3njc8f&amp;nbsp;-&amp;nbsp;Internal&amp;nbsp;server&amp;nbsp;error,&amp;nbsp;for&amp;nbsp;(GET)&amp;nbsp;[/assets/dataTables/javascripts/dataTables.bootstrap4.js]&lt;/li&gt;
&lt;li&gt;카프카 매니저 접속 시에 JS, CSS를 불러오지 못하는 위 에러가 발생하는 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/844&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/256</guid>
      <comments>https://soobarkbar.tistory.com/256#entry256comment</comments>
      <pubDate>Sat, 6 Apr 2024 14:54:32 +0900</pubDate>
    </item>
    <item>
      <title>기수 정렬 (Radix Sort)</title>
      <link>https://soobarkbar.tistory.com/156</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기수 정렬 (Radix Sort) ?&lt;/h2&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: #ee2323;&quot;&gt;&lt;b&gt;각 자리 위치를 하나씩 증가시키면서&lt;/b&gt;&lt;/span&gt; &lt;b&gt;숫자들을 정렬&lt;/b&gt;하는 방법이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정 정렬&lt;/b&gt;에 속하며, &lt;a href=&quot;https://soobarkbar.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카운팅 정렬&lt;/a&gt;과 마찬가지로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;값의 비교연산 없이&lt;/span&gt; 정렬&lt;/b&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;제자리 정렬이 아니기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;추가적인 메모리가 필요&lt;/b&gt;&lt;/span&gt;하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;동일한 길이를 가진&lt;/span&gt; 숫자나 문자열&lt;/b&gt;이여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정렬하는 숫자 자릿수 \(k\) 에 따라 \(O(kn)\) 의 시간 복잡도를 가진다.&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;1의 자리 숫자를 비교해서 오름차순 또는 내림차순으로 정렬한 뒤, 큐 버킷에 삽입한 뒤 순서대로 뽑아 정렬한다.&lt;/li&gt;
&lt;li&gt;그 다음, 10의 자리 숫자를 비교해서 동일하게 수행한다.&lt;/li&gt;
&lt;li&gt;가장 큰 숫자의 자릿 수만큼 반복해서 수행한다.&lt;/li&gt;
&lt;/ul&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;1057&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF5fiB%2FbtqCgSllX0Z%2FkECRdI1FQquPfMjBbkoZEK%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;1057&quot; height=&quot;419&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code&lt;/h3&gt;
&lt;pre id=&quot;code_1696321370732&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void radixSort(int[] arr, int maxSize) {
    int jarisu = 1, size = 0;
    int len = arr.length;
    int[] output = new int[len];

    while (size &amp;lt; maxSize) {
        int[] bucket = new int[10];

        for (int i = 0; i &amp;lt; len; ++i) {
            ++bucket[arr[i] / jarisu % 10];
        }

        for (int i = 1; i &amp;lt; 10; ++i) {
            bucket[i] += bucket[i - 1];
        }

        for (int i = len - 1; i &amp;gt;= 0; --i) {
            int idx = arr[i] / jarisu % 10;

            output[bucket[idx] - 1] = arr[i];
            --bucket[idx];
        }

        for (int i = 0; i &amp;lt; len; ++i) {
            arr[i] = output[i];
        }

        jarisu *= 10;
        ++size;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;
&lt;table class=&quot;colorscripter-code-table&quot; style=&quot;margin: 0; padding: 0; border: none; background-color: #272727; border-radius: 4px;&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px; border-right: 2px solid #4f4f4f;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; word-break: normal; text-align: right; color: #aaa; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;1&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;2&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;3&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;4&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 6px 0; text-align: left;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;1&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[82,&amp;nbsp;43,&amp;nbsp;3,&amp;nbsp;15,&amp;nbsp;35,&amp;nbsp;27,&amp;nbsp;7,&amp;nbsp;38,&amp;nbsp;18,&amp;nbsp;9]&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;2&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[3,&amp;nbsp;7,&amp;nbsp;9,&amp;nbsp;15,&amp;nbsp;18,&amp;nbsp;27,&amp;nbsp;35,&amp;nbsp;38,&amp;nbsp;43,&amp;nbsp;82]&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;vertical-align: bottom; padding: 0 2px 4px 0;&quot;&gt;&lt;a style=&quot;text-decoration: none; color: white;&quot; href=&quot;http://colorscripter.com/info#e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-size: 9px; word-break: normal; background-color: #4f4f4f; color: white; border-radius: 10px; padding: 1px;&quot;&gt;cs&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://lktprogrammer.tistory.com/48&quot;&gt;https://lktprogrammer.tistory.com/48&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&quot;&gt;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/Sort</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/156</guid>
      <comments>https://soobarkbar.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 3 Oct 2023 17:23:20 +0900</pubDate>
    </item>
    <item>
      <title>Forward / Reverse Proxy</title>
      <link>https://soobarkbar.tistory.com/248</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Proxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;프록시&amp;nbsp;서버를&amp;nbsp;통해서&amp;nbsp;다른&amp;nbsp;네트워크&amp;nbsp;서비스에&amp;nbsp;간접적으로&amp;nbsp;접속할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;해주는&amp;nbsp;컴퓨터&amp;nbsp;시스템&amp;nbsp;또는&amp;nbsp;응용&amp;nbsp;프로그램&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 (Proxy) : 서버와 클라이언트 사이에서 대리로 통신을 수행하는 것&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프록시 서버 (Proxy Server) : 중계 기능을 수행하는 서버&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;br /&gt;1. Forward Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;접근하는&amp;nbsp;것이&amp;nbsp;아니라&amp;nbsp;프록시&amp;nbsp;서버가&amp;nbsp;요청을&amp;nbsp;받고&amp;nbsp;인터넷에&amp;nbsp;연결하여&amp;nbsp;결과를&amp;nbsp;클라이언트에&amp;nbsp;전달&amp;nbsp;(Forward)&amp;nbsp;해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;보안 : 프록시 서버에서 In / Out Bound 패킷에 대한 보안 정책 (Content Filtering 등) 을 적용할 수 있다.&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;성능 : 프록시 서버 내부에 캐시를 유지하며 한 번 통신한 외부 서버의 이미지, 파일 등을 저장할 수 있다. 캐시에 데이터가 있으면 프록시 서버가 데이터를 바로 제공할 수 있어 빠른 통신을 지원한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkfkRx%2FbtrA0XzX0Or%2F3MGQUCeH1LIiZaJ2G4KrZ0%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;780&quot; height=&quot;283&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;2. Reverse Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;데이터를&amp;nbsp;요청하면&amp;nbsp;리버스&amp;nbsp;프록시가&amp;nbsp;이&amp;nbsp;요청을&amp;nbsp;받아&amp;nbsp;내부&amp;nbsp;서버에서&amp;nbsp;데이터를&amp;nbsp;받은&amp;nbsp;후&amp;nbsp;클라이언트에&amp;nbsp;전달한다.&lt;br /&gt;&lt;br /&gt;클라이언트는&amp;nbsp;내부&amp;nbsp;서버에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;알&amp;nbsp;필요&amp;nbsp;없이&amp;nbsp;리버스&amp;nbsp;프록싱만&amp;nbsp;요청하면&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;내부&amp;nbsp;서버&amp;nbsp;(WAS)&amp;nbsp;에&amp;nbsp;직접&amp;nbsp;접근할&amp;nbsp;경우,&amp;nbsp;DB에&amp;nbsp;접근이&amp;nbsp;가능하기&amp;nbsp;때문에&amp;nbsp;중간에&amp;nbsp;리버스&amp;nbsp;프록시를&amp;nbsp;두고&amp;nbsp;클라이언트와&amp;nbsp;내부&amp;nbsp;서버&amp;nbsp;사이의&amp;nbsp;통신을&amp;nbsp;담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;보안 : 모든 접속은 리버스 프록시 서버에게 들어오고 요청에 매핑되는 내부 서버에 요청 정보를 넘겨주기 때문에 외부 사용자는 실제 내부망에 있는 서버의 존재를 모른다.&lt;/li&gt;
&lt;li&gt;로드밸런싱 : 프록시 서버가 내부 서버의 정보를 알고 있기 때문에 로드 밸런싱을 통해 부하 여부에 따라 요청을 분배할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZDP0i%2FbtrA33Gzlfe%2F79jDfwoNKKzYWL7qkR63Hk%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;810&quot; height=&quot;287&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 요청하는 End Point&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;실제 서버 도메인&lt;/b&gt;&lt;br /&gt;Reverse&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;프록시&amp;nbsp;서버&amp;nbsp;도메인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;감춰지는 대상&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward Proxy : &lt;b&gt;클라이언트 &lt;/b&gt;(요청 받는 서버는 포워드 프록시 서버를 통해서 요청을 받기 때문에 클라이언트의 정보를 알 수 없다.)&lt;br /&gt;Reverse Proxy : &lt;b&gt;서버&amp;nbsp;&lt;/b&gt;(클라이언트는 리버스 프록시 서버에게 요청하기 때문에 실제 서버의 정보를 알 수 없다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://bcp0109.tistory.com/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bcp0109.tistory.com/194&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/248</guid>
      <comments>https://soobarkbar.tistory.com/248#entry248comment</comments>
      <pubDate>Mon, 2 May 2022 12:19:13 +0900</pubDate>
    </item>
    <item>
      <title>count(*) / count(1) / count(컬럼) 차이</title>
      <link>https://soobarkbar.tistory.com/246</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;count(*) VS count(1)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; data-alt=&quot;출처 : mysql 홈페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYl5XM%2FbtrztXQDJVV%2F28BViKfccMQ62OF9xbvlYK%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;1672&quot; height=&quot;66&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : mysql 홈페이지&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;차이가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;count(*) VS count(컬럼)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;count(*) : null을 포함하여 모든 Row 수를 카운트한다.&lt;/li&gt;
&lt;li&gt;count(컬럼명) : null을 제외한 Row 수를 카운트한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://asktom.oracle.com/pls/apex/f?p=100:11:0::NO::P11_QUESTION_ID:1156159920245&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://asktom.oracle.com/pls/apex/f?p=100:11:0::NO::P11_QUESTION_ID:1156159920245&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/246</guid>
      <comments>https://soobarkbar.tistory.com/246#entry246comment</comments>
      <pubDate>Fri, 15 Apr 2022 17:59:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring MVC (Model, View, Controller)</title>
      <link>https://soobarkbar.tistory.com/244</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model / View / Controller로 역할을 나누는 패턴이다.&lt;/li&gt;
&lt;li&gt;MVC 패턴의 목적은 Business Logic과 Presentation Logic을 분리하는 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Business Logic : 어떤 특정한 값을 얻기 위해 데이터 처리를 수행하는 응용프로그램의 일부 (즉, 원하는 값을 얻기 위해 백엔드에서 일어나는 각종 처리)&lt;/li&gt;
&lt;li&gt;Presentation Logic : 화면상의 디자인 구성을 위한 처리 (즉, 화면을 보여주기 위한 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;역할 별로 코드를 분리하여 하나의 파일에 코드가 모이는 것을 방지하기 때문에, 가독성과 코드 재사용이 증가한다.&lt;/li&gt;
&lt;li&gt;단점으로 View와 Model 사이의 높은 의존성 때문에 애플리케이션이 커질수록 복잡해지고 유지보수가 어렵다.&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;320&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRO5QC%2FbtrzraH9Fvs%2FKqAFxev7ivroOcuH7OG4vK%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;320&quot; height=&quot;350&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Model&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller에서 View로 전달되는 데이터 객체이다.&lt;/li&gt;
&lt;li&gt;Key-Value 형태를 가지며, 하나의 (Key-Value) 객체를 Model Attribute라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjSSQr%2FbtrzuznobGr%2FKuk3d79NjzBajWFiNvOKF1%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;119&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Controller&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View와 Model 사이의 인터페이스 역할을 수행한다.&lt;/li&gt;
&lt;li&gt;Request에 따라 적절한 결과를 Model에 담아 View에 전달한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, View Name과 View에 출력할 Model을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model 데이터의 렌더링을 담당하며, HTML Output을 생성한다.&lt;/li&gt;
&lt;li&gt;여러가지 템플릿 엔진이 존재한다. (ex. JSP, Thymleaf, Freemarker, ...)&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;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC의 흐름&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8Msx%2FbtrzoN1JQ6B%2FYmQKp1nnXmOiHlRb4znswK%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;1000&quot; height=&quot;588&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Client 요청 (Request) 이 들어오면 Filter를 거치고, Dispatcher Servlet으로 온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Dispatcher Servlet은 받은 요청을 Handler Mapping에게 전달하고, Handler Mapping은 요청 URL을 분석하여 적절한 Controller 정보를 찾아 Dispatcher Servlet에게 반환한다.&lt;/li&gt;
&lt;li&gt;적절한 Controller 정보를 받은 Dispatcher Servlet은 Handler Adapter를 호출하고, 전달 받은 Controller 중 요청한 URL에 맞는 적절한 Method를 찾아준다.&lt;/li&gt;
&lt;li&gt;호출된 Controller는 Business Logic을 처리하고 나온 결과를 View에 전달할 수 있도록 Model 객체에 저장한다.&lt;/li&gt;
&lt;li&gt;그 후, 호출된 Controller는 Dispatcher Servlet에게 View Name을 반환한다.&lt;/li&gt;
&lt;li&gt;Dispatcher Servlet은 View Resolver를 호출하여 View Name을 전달하고, View Resolver는 View Name을 바탕으로 적절한 View 객체를 결정한다.&lt;/li&gt;
&lt;li&gt;View 객체는 적절한 View (ex. JSP, Thymeleaf) 를 호출하고, View는 화면 표시에 필요한 데이터를 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Model 객체에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;가져와 화면 표시를 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;렌더링된 View 화면을 Client에게 Response 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 요소&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Dispatcher Servlet&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: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Spring Framework가 제공하는 Servlet 클래스로, Front Controller를 담당하고 있어&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;모든 HTTP 요청을 받는다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Handler Mapping&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request URL에 해당하는 Controller 정보를 저장하는 테이블을 가진다.&lt;/li&gt;
&lt;li&gt;테이블에 저장된 정보에 따라 Request를 처리할 Method에 매핑한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ViewResolver&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: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Controller가 반환한 View Name을 바탕으로 적절한 View 객체 (Physical View Files) 를 반환한다.&lt;/span&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;View Name: home&lt;/li&gt;
&lt;li&gt;Prefix: /WEB-INF/views/&lt;/li&gt;
&lt;li&gt;Suffix: .jsp&lt;/li&gt;
&lt;li&gt;즉, &quot;/WEB-INF/views/home.jsp&quot; 위치의 View(JSP) 에 Controller에서 받은 Model을 전달하고, 해당 View에서 Model 데이터를 이용하여 적절한 페이지를 만들고 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;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aridom.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://aridom.tistory.com/61&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&amp;amp;blogId=semi7623&amp;amp;logNo=100005637337&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&amp;amp;blogId=semi7623&amp;amp;logNo=100005637337&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joont92.github.io/spring/DispatcherServlet-Flow/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://joont92.github.io/spring/DispatcherServlet-Flow/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gmlwjd9405.github.io/2018/12/20/spring-mvc-framework.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gmlwjd9405.github.io/2018/12/20/spring-mvc-framework.html&lt;/a&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;</description>
      <category>Spring</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/244</guid>
      <comments>https://soobarkbar.tistory.com/244#entry244comment</comments>
      <pubDate>Fri, 15 Apr 2022 15:01:25 +0900</pubDate>
    </item>
    <item>
      <title>(MAC) HomeBrew + JDK 8버전 설치</title>
      <link>https://soobarkbar.tistory.com/243</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HomeBrew 설치&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M1 칩 있는 맥북&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782265452&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://gist.githubusercontent.com/nrubin29/bea5aa83e8dfa91370fe83b62dad6dfa/raw/48f48f7fef21abb308e129a80b3214c2538fc611/homebrew_m1.sh)&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;M1 칩 없는 맥북&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782318568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&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;size18&quot;&gt;그런데 만약 M1 칩 있는 맥북에서 M1 칩 없는 맥북 설치 방법으로 한다면 아래와 같이 인식을 못한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648782372812&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;zsh: command not found: brew&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러면 아래 명령어를 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648782400210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eval $(/opt/homebrew/bin/brew shellenv)&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;터미널을 켤 때마다 brew가 실행되도록 하는 방법&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;m1 칩 맥북
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vi ~/.zshrc의 마지막 줄에 아래 명령어 입력 후 저장 (:wq)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782723284&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eval $(/opt/homebrew/bin/brew shellenv)&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;m1 칩 없는 맥북
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vi ~/.zshrc에서 환경변수 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782800492&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export PATH=/usr/local/bin:/usr/local/sbin:$PATH&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;brew 설치 확인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;brew -v&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;h2 data-ke-size=&quot;size26&quot;&gt;JDK 8 설치&lt;/h2&gt;
&lt;pre id=&quot;code_1648782969971&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew tap AdoptOpenJDK/openjdk
 
brew install --cask adoptopenjdk8
 
/usr/libexec/java_home -v 1.8&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;/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: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;java -version&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco6nXv%2FbtryaG8FxlW%2FVj1lvfq8JD9Kg75z2QEzA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;68&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;javac&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZ06c%2Fbtrx9AgLMdA%2FYDwCIwbvlsUFxD5I2rgAhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;403&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1164&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;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tlo-developer.tistory.com/267&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tlo-developer.tistory.com/267&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://designdepot.tistory.com/209&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://designdepot.tistory.com/209&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/243</guid>
      <comments>https://soobarkbar.tistory.com/243#entry243comment</comments>
      <pubDate>Fri, 1 Apr 2022 12:19:27 +0900</pubDate>
    </item>
  </channel>
</rss>