<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://jangjichang.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jangjichang.github.io/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-08-19T23:40:50+09:00</updated><id>https://jangjichang.github.io/feed.xml</id><title type="html">장지창</title><subtitle>분산 시스템, 데이터, 생성형 AI를 기록하는 백엔드 엔지니어 장지창의 블로그
</subtitle><author><name>장지창</name></author><entry><title type="html">k8s에서 Django Migration 실행 순서 보장하기</title><link href="https://jangjichang.github.io/posts/django-migration-deployment" rel="alternate" type="text/html" title="k8s에서 Django Migration 실행 순서 보장하기" /><published>2026-08-17T00:00:00+09:00</published><updated>2026-08-17T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/django-migration-deployment</id><content type="html" xml:base="https://jangjichang.github.io/posts/django-migration-deployment">&lt;p&gt;GitOps 환경에서 Migration Job은 단순히 한 번 실행하고 사라지는 리소스가 아니라 배포의 한 단계로 다뤄야 한다. 새 버전의 웹 애플리케이션이 변경된 데이터베이스 스키마를 전제로 한다면, Migration 성공은 Web Deployment의 선행 조건이어야 한다.&lt;/p&gt;

&lt;p&gt;처음에는 Web Pod가 시작될 때마다 실행하던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;를 Kubernetes Job으로 옮기면 문제가 끝난다고 생각했다. 하지만 Job으로 분리한 뒤에도 Web Deployment와의 실행 순서는 보장되지 않았다. 완료된 Job을 지우는 TTL은 Argo CD의 동기화 상태를 어긋나게 했다. 문제는 Migration의 실행 위치만이 아니라 배포 순서와 생명주기에 있었다. 이 글에서는 이 판단이 Argo CD &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt; Hook으로 이어진 과정을 정리한다.&lt;/p&gt;

&lt;h2 id=&quot;배포-구조-변화&quot;&gt;배포 구조 변화&lt;/h2&gt;

&lt;h3 id=&quot;1단계--web-pod마다-migration-실행&quot;&gt;1단계 — Web Pod마다 Migration 실행&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
    accTitle: 세 개의 Web Pod가 각각 Migration을 실행하는 구조
    accDescr: Deployment가 생성한 세 개의 Web Pod가 각각 migrate를 실행한 다음 gunicorn을 시작하며, 동일한 Database에 세 개의 migration 요청이 전달될 수 있다.

    A[&quot;Argo CD Sync&quot;]
    D[&quot;Web Deployment
    replicas: 3
    command: migrate → gunicorn&quot;]

    P1[&quot;Web Pod 1
    migrate → gunicorn&quot;]
    P2[&quot;Web Pod 2
    migrate → gunicorn&quot;]
    P3[&quot;Web Pod 3
    migrate → gunicorn&quot;]

    DB[(&quot;Database&quot;)]

    A --&amp;gt; D

    D --&amp;gt; P1
    D --&amp;gt; P2
    D --&amp;gt; P3

    P1 --&amp;gt;|&quot;migrate&quot;| DB
    P2 --&amp;gt;|&quot;migrate&quot;| DB
    P3 --&amp;gt;|&quot;migrate&quot;| DB

    DB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;처음에는 Web Deployment의 컨테이너 시작 명령에 Migration과 Gunicorn 실행을 함께 선언했다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;djangoServer&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicaCount&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/bin/bash&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-c&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;python manage.py migrate &amp;amp;&amp;amp; \&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;gunicorn &amp;lt;WSGI_MODULE&amp;gt;:application &amp;lt;GUNICORN_OPTIONS&amp;gt;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;migrateJob&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;migrate는-여러-번-실행해도-괜찮지-않을까&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;는 여러 번 실행해도 괜찮지 않을까?&lt;/h4&gt;

&lt;p&gt;Replica가 3개라면 세 Pod가 각각 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;를 실행한다. Pod가 재시작되거나 스케일 아웃될 때도 같은 명령을 다시 실행한다.&lt;/p&gt;

&lt;p&gt;Django는 적용이 끝난 Migration의 이력을 기록하므로, 나중에 순차적으로 실행한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;는 이미 적용된 항목을 건너뛴다. 그러나 이것이 Migration 작업 자체의 멱등성이나 동시 실행 안전성을 뜻하지는 않는다. 두 프로세스가 거의 같은 시점에 적용 대상을 계산하면 둘 다 같은 Migration을 미적용 상태로 판단할 수 있다. 이후 같은 DDL이나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RunPython&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RunSQL&lt;/code&gt;이 겹치면 데이터베이스와 작업의 종류에 따라 잠금 대기, 이미 존재하는 테이블·컬럼 오류, 데이터 중복 같은 문제가 발생할 수 있다.&lt;/p&gt;

&lt;p&gt;당시의 예외 종류와 발생 시각을 보여주는 기록은 남아 있지 않아 이 중 특정 오류가 실제로 발생했다고 단정할 수는 없다. 배포 설정으로 확인할 수 있는 사실은 여러 Pod가 같은 Migration을 동시에 시작할 수 있었다는 점이다. 이 구조 자체를 제거하는 것이 우선이었다.&lt;/p&gt;

&lt;p&gt;이 구조에는 세 가지 문제가 있었다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Migration이 Web Pod의 Replica 수만큼 동시에 시작될 수 있다.&lt;/li&gt;
  &lt;li&gt;Web Pod의 시작·재시작과 데이터베이스 스키마 변경의 생명주기가 결합된다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&amp;amp;&lt;/code&gt; 앞의 Migration이 실패하면 Gunicorn이 실행되지 않아 해당 Web Pod도 기동하지 못한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Migration 실패로 서버가 시작되지 않으면 결과적으로 잘못된 배포가 막히기도 한다. 하지만 Pod별로 Migration을 실행하는 방식은 실패 원인을 구분하기 어렵게 만든다. 데이터베이스 변경의 실행 횟수도 Web Deployment 상태에 맡긴다.&lt;/p&gt;

&lt;h3 id=&quot;2단계--migration을-job으로-분리&quot;&gt;2단계 — Migration을 Job으로 분리&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
    accTitle: Web Pod에서 Migration을 제거하고 별도 Job으로 분리한 구조
    accDescr: Argo CD Sync에서 Web Deployment와 Migration Job이 함께 적용된다. 세 개의 Web Pod는 gunicorn만 실행하고, ttlSecondsAfterFinished가 60으로 설정된 별도 Job이 한 번 migrate를 실행한다.

    A[&quot;Argo CD Sync&quot;]
    D[&quot;Web Deployment
    replicas: 3
    command: gunicorn&quot;]
    J[&quot;+ Migration Job
    ttlSecondsAfterFinished: 60&quot;]

    P1[&quot;Web Pod 1&amp;lt;br/&amp;gt;&amp;lt;s&amp;gt;migrate&amp;lt;/s&amp;gt;&amp;lt;br/&amp;gt;gunicorn&quot;]
    P2[&quot;Web Pod 2&amp;lt;br/&amp;gt;&amp;lt;s&amp;gt;migrate&amp;lt;/s&amp;gt;&amp;lt;br/&amp;gt;gunicorn&quot;]
    P3[&quot;Web Pod 3&amp;lt;br/&amp;gt;&amp;lt;s&amp;gt;migrate&amp;lt;/s&amp;gt;&amp;lt;br/&amp;gt;gunicorn&quot;]

    DB[(&quot;Database&quot;)]

    A --&amp;gt; D
    A --&amp;gt; J

    D --&amp;gt; P1
    D --&amp;gt; P2
    D --&amp;gt; P3

    J --&amp;gt;|&quot;migrate&quot;| DB

    classDef removed stroke:#d27b7b,stroke-width:2px
    classDef added stroke:#78b98c,stroke-width:2px
    class P1,P2,P3 removed
    class J added
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;끝이 있는 일회성 작업은 Kubernetes Job의 역할과 잘 맞는다. 그래서 Web Pod의 시작 명령에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;를 제거하고 별도 Job을 활성화했다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;djangoServer&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicaCount&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/bin/bash&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-c&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;gunicorn &amp;lt;WSGI_MODULE&amp;gt;:application &amp;lt;GUNICORN_OPTIONS&amp;gt;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;migrateJob&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;batch/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Job&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;RELEASE_NAME&amp;gt;-migrate&quot;&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ttlSecondsAfterFinished&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;60&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;migrate&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;APPLICATION_IMAGE&amp;gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;python&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;manage.py&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;migrate&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;envFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;configMapRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;CONFIG_MAP_NAME&amp;gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;restartPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Never&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backoffLimit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;job으로-옮기면-충분하지-않을까&quot;&gt;Job으로 옮기면 충분하지 않을까?&lt;/h4&gt;

&lt;p&gt;이제 Migration 실행 횟수는 Web Pod의 Replica 수와 분리됐다. 그러나 이 변경만으로는 Migration이 새 Web Deployment보다 먼저 끝난다는 보장이 없다. Argo CD의 같은 Sync 단계에 Job과 Deployment가 함께 들어가므로, 새 Web Pod가 Migration 완료 전에 기동할 수 있다.&lt;/p&gt;

&lt;h4 id=&quot;완료된-job은-ttl로-지우면-되지-않을까&quot;&gt;완료된 Job은 TTL로 지우면 되지 않을까?&lt;/h4&gt;

&lt;p&gt;완료된 Job을 정리하기 위해 추가한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ttlSecondsAfterFinished: 60&lt;/code&gt;도 GitOps에서는 다른 문제를 만들었다. TTL 컨트롤러는 Job이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Complete&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Failed&lt;/code&gt; 상태가 된 뒤 60초가 지나면 Job과 종속 Pod를 함께 삭제한다. 반면 Helm Chart를 렌더링한 목표 Manifest에는 여전히 Job이 존재한다. 그 결과 Argo CD는 클러스터에서 사라진 Job을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OutOfSync&lt;/code&gt;로 판단했다. Migration 로그도 Pod와 함께 사라져 원인 분석이 어려워졌다.&lt;/p&gt;

&lt;p&gt;그렇다고 TTL을 제거한 일반 Job이 매번 다시 실행되는 것은 아니다. 완료된 Job은 기본적으로 클러스터에 남는다. 고정된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metadata.name&lt;/code&gt;으로 다시 Sync해도 이미 완료된 같은 Job을 새로 생성하지 않는다. 이것은 Finalizer 때문이 아니라 완료된 Job을 보존하는 기본 생명주기 때문이다. 매번 이름을 바꾸면 새 Job을 만들 수 있다. 다만 이름 생성과 오래된 Job 정리까지 별도로 관리해야 한다.&lt;/p&gt;

&lt;p&gt;필요한 것은 일반 리소스의 목표 상태가 아니라 배포 절차였다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Sync를 시작할 때마다 새로운 Migration Job을 실행한다.&lt;/li&gt;
  &lt;li&gt;Migration이 성공해야 Web Deployment를 적용한다.&lt;/li&gt;
  &lt;li&gt;완료된 Job과 Pod는 다음 Sync 전까지 남겨 로그를 확인한다.&lt;/li&gt;
  &lt;li&gt;다음 Sync 직전에 이전 Job을 지우고 새 Job을 만든다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;3단계--argo-cd-presync로-배포-순서-보장&quot;&gt;3단계 — Argo CD PreSync로 배포 순서 보장&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;%%{init: {&quot;flowchart&quot;: {&quot;rankSpacing&quot;: 30, &quot;nodeSpacing&quot;: 30}}}%%
flowchart TB
    accTitle: PreSync Migration Job이 성공한 후 Web Deployment를 실행하는 구조
    accDescr: Argo CD Sync가 시작되면 PreSync Migration Job이 Database에 migrate를 실행한다. Migration이 성공하면 Sync 단계로 넘어가 Web Deployment와 세 개의 Web Pod가 적용된다. 2단계에 있던 ttlSecondsAfterFinished 설정은 제거된다.

    A[&quot;Argo CD Sync&quot;]
    J[&quot;PreSync Migration Job&amp;lt;br/&amp;gt;&amp;lt;s&amp;gt;ttlSecondsAfterFinished: 60&amp;lt;/s&amp;gt;&quot;]
    DB[(&quot;Database&quot;)]
    D[&quot;Web Deployment
    replicas: 3
    command: gunicorn&quot;]

    P1[&quot;Web Pod 1
    gunicorn&quot;]
    P2[&quot;Web Pod 2
    gunicorn&quot;]
    P3[&quot;Web Pod 3
    gunicorn&quot;]

    A --&amp;gt;|&quot;1. PreSync&quot;| J
    J --&amp;gt;|&quot;2. migrate&quot;| DB
    DB --&amp;gt;|&quot;3. Migration 성공 후 Sync&quot;| D

    D --&amp;gt; P1
    D --&amp;gt; P2
    D --&amp;gt; P3

    classDef changed stroke:#78b98c,stroke-width:2px
    class J changed
    linkStyle 0,1,2 stroke:#78b98c,stroke-width:2px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Argo CD Hook은 리소스에 배포 단계의 의미를 부여한다. Migration Job을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt; Hook으로 선언하면 Argo CD는 이 Job이 성공한 뒤에만 일반 Manifest를 적용하는 Sync 단계로 넘어간다. Hook이 실패하면 전체 Sync도 실패하고 Web Deployment는 적용되지 않는다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;batch/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Job&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;RELEASE_NAME&amp;gt;-migrate&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;argocd.argoproj.io/hook&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;PreSync&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;argocd.argoproj.io/hook-delete-policy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;BeforeHookCreation&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;activeDeadlineSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;180&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;migrate&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;APPLICATION_IMAGE&amp;gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;python&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;manage.py&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;migrate&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;envFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;configMapRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;CONFIG_MAP_NAME&amp;gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;restartPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Never&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backoffLimit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;각 설정은 이렇게 동작한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt;: Migration Job이 성공해야 Deployment를 포함한 Sync 단계를 진행한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BeforeHookCreation&lt;/code&gt;: 다음 Sync에서 새 Hook을 만들기 전에 기존의 같은 이름 Hook을 삭제한다. 완료된 Job과 Pod는 그때까지 남아 있어 로그를 확인할 수 있다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;activeDeadlineSeconds: 180&lt;/code&gt;: Job 전체 실행 시간을 제한한다. 제한 시간을 넘기면 실행 중인 Pod를 종료하고 Job을 실패 처리한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backoffLimit: 0&lt;/code&gt;: 실패한 Migration Pod를 Job 컨트롤러가 다시 시도하지 않게 한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ttlSecondsAfterFinished&lt;/code&gt; 제거: Kubernetes TTL 컨트롤러가 완료된 Job을 먼저 삭제해 Argo CD의 목표 상태와 어긋나는 상황을 없앤다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결과적으로 배포 순서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync Migration Job → Migration 성공 → Web Deployment&lt;/code&gt;가 됐다. Migration이 실패하면 새 Web Deployment는 시작하지 않는다. 실패한 Job과 Pod도 다음 Sync 전까지 남아 로그를 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;다만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt;가 보장하는 것은 Migration과 새 Web Deployment 사이의 순서다. Migration이 실행되는 동안 구버전 Web Pod는 계속 요청을 처리할 수 있다. 컬럼 삭제나 이름 변경처럼 구버전 코드와 호환되지 않는 변경은 한 번에 적용하지 않는다. 새 구조를 추가한 뒤 사용이 끝난 기존 구조를 나중에 제거하는 방식으로 나눠야 한다.&lt;/p&gt;

&lt;h2 id=&quot;단계별-비교&quot;&gt;단계별 비교&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;단계&lt;/th&gt;
      &lt;th&gt;실행 위치&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Replica와 분리&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Migration 선행&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;완료 Job 보존&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1단계&lt;/td&gt;
      &lt;td&gt;각 Web Pod&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2단계&lt;/td&gt;
      &lt;td&gt;일반 Job&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;3단계&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt; Job&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;Web Pod의 시작 명령에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;migrate&lt;/code&gt;를 실행하면 Migration이 Replica 수와 Pod 재시작에 영향을 받는다. 이를 Job으로 옮기면 실행 주체가 Web Pod에서 분리된다. 하지만 Web Deployment와의 순서까지 보장되지는 않는다. 완료된 일반 Job을 TTL로 삭제하는 방식도 Git에 선언된 목표 상태와 클러스터의 실제 상태를 어긋나게 했다.&lt;/p&gt;

&lt;p&gt;Migration Job을 Argo CD &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreSync&lt;/code&gt; Hook으로 전환하자 Migration이 성공한 뒤에만 Web Deployment가 적용되도록 순서가 명확해졌다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BeforeHookCreation&lt;/code&gt;은 이전 Job을 다음 Sync 직전까지 보존하며 매 Sync마다 새 Job을 실행한다.&lt;/p&gt;

&lt;p&gt;핵심은 Job을 만드는 것이 아니라 배포 순서를 선언하는 것이다.&lt;/p&gt;

&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/stable/topics/migrations/&quot;&gt;Django Migrations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/job/&quot;&gt;Kubernetes Job&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/&quot;&gt;Kubernetes TTL-after-finished Controller&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/&quot;&gt;Argo CD Sync Phases and Waves&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="django" /><category term="kubernetes" /><category term="argocd" /><summary type="html">GitOps 환경에서 Migration Job은 단순히 한 번 실행하고 사라지는 리소스가 아니라 배포의 한 단계로 다뤄야 한다. 새 버전의 웹 애플리케이션이 변경된 데이터베이스 스키마를 전제로 한다면, Migration 성공은 Web Deployment의 선행 조건이어야 한다.</summary></entry><entry><title type="html">Partitioning</title><link href="https://jangjichang.github.io/posts/partitioning" rel="alternate" type="text/html" title="Partitioning" /><published>2025-06-26T00:00:00+09:00</published><updated>2025-06-26T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/partitioning</id><content type="html" xml:base="https://jangjichang.github.io/posts/partitioning">&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#partitioning&quot;&gt;Partitioning&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#파티셔닝과-복제&quot;&gt;파티셔닝과 복제&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#키-값-데이터-파티셔닝&quot;&gt;키-값 데이터 파티셔닝&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#키-범위-기준-파티셔닝&quot;&gt;키 범위 기준 파티셔닝&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#키의-해시값-기준-파티셔닝&quot;&gt;키의 해시값 기준 파티셔닝&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#쏠린-작업부하와-핫스팟-완화&quot;&gt;쏠린 작업부하와 핫스팟 완화&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#파티셔닝과-보조-색인&quot;&gt;파티셔닝과 보조 색인&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#문서-기준-보조-색인-파티셔닝&quot;&gt;문서 기준 보조 색인 파티셔닝&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#용어-기준-보조-색인-파티셔닝&quot;&gt;용어 기준 보조 색인 파티셔닝&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#파티션-재균형화&quot;&gt;파티션 재균형화&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#재균형화-전략&quot;&gt;재균형화 전략&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#운영-자동-재균형화와-수동-재균형화&quot;&gt;운영: 자동 재균형화와 수동 재균형화&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#요청-라우팅&quot;&gt;요청 라우팅&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#병렬-질의-실행&quot;&gt;병렬 질의 실행&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터셋이 매우 크거나 질의 처리량이 매우 높다면 복제만으로는 부족하고 데이터를 &lt;strong&gt;파티션&lt;/strong&gt;으로
쪼갤 필요가 있다. 이 작업을 &lt;strong&gt;샤딩&lt;/strong&gt;이라고 한다.&lt;/p&gt;

&lt;p&gt;파티션을 나눌 때는 보통 각 데이터 단위(레코드, 로우, 문서)가 하나의 파티션에 속하게 한다.&lt;/p&gt;

&lt;p&gt;데이터 파티셔닝을 원하는 주된 이유는 &lt;strong&gt;확장성&lt;/strong&gt;이다.&lt;/p&gt;

&lt;h2 id=&quot;파티셔닝과-복제&quot;&gt;&lt;a href=&quot;#파티셔닝과-복제&quot;&gt;파티셔닝과 복제&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;보통 복제와 파티셔닝을 함께 적용해 각 파티션의 복사본을 여러 노드에 저장한다.&lt;/p&gt;

&lt;p&gt;각 레코드는 정확히 한 파티션에 속하더라도
이를 여러 다른 노드에 저장해서 내결함성을 보장할 수 있다는 의미다.&lt;/p&gt;

&lt;p&gt;한 노드에 여러 파티션을 저장할 수도 있다. 리더 팔로워 복제 모델을 사용한다면 파티셔닝과 복제의 조합은
다음 그림과 같다. 각 파티션의 리더는 하나의 노드에 할당되고 팔로워들은 다른 노드에 할당된다.
각 노드는 어떤 파티션에게는 리더이면서 다른 파티션에게는 팔로워가 될 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/replica_and_partitioning.png&quot; alt=&quot;replica_and_partitioning.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;키-값-데이터-파티셔닝&quot;&gt;&lt;a href=&quot;#키-값-데이터-파티셔닝&quot;&gt;키-값 데이터 파티셔닝&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;대량의 데이터를 파티셔닝할 때 어떤 레코드를 어느 노드에 저장할지 어떻게 결정할까?&lt;/p&gt;

&lt;p&gt;파티셔닝의 목적? 데이터의 질의 부하를 노드 사이에 고르게 분산하는 것.&lt;/p&gt;

&lt;p&gt;파티셔닝이 고르게 이뤄지지 않아 질의를 많이 받는 파티션이 있다면 &lt;strong&gt;쏠렸다(skewed)&lt;/strong&gt; 고 말한다.&lt;/p&gt;

&lt;p&gt;쏠림이 있으면 파티셔닝의 효과가 매우 떨어진다. 불균형하게 부하가 높은 파티션을 &lt;strong&gt;핫스팟&lt;/strong&gt;이라고 한다.&lt;/p&gt;

&lt;p&gt;핫스팟을 회피하는 가장 단순한 방법은 레코드를 할당할 노드를 무작위로 선택하는 것인데,
데이터가 노드들 사이에 고르게 분산되지만 어떤 레코드를 읽으려고 할 때 해당 레코드가
어느 노드에 있는지 알 수 없으므로 모든 노드에 질의를 보내야 한다.&lt;/p&gt;

&lt;h3 id=&quot;키-범위-기준-파티셔닝&quot;&gt;&lt;a href=&quot;#키-범위-기준-파티셔닝&quot;&gt;키 범위 기준 파티셔닝&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;파티셔닝하는 방법 중 하나는 종이 백과사전처럼 각 파티션에 연속된 범위(어떤 최솟값
에서 최댓값까지)의 키를 할당하는 것이다. 각 범위들 사이의 경계를 알면 어떤 키가 어느 파티션에
속하는지 쉽게 찾을 수 있다. 또 어떤 파티션이 어느 노드에 할당됐는지 알면 적절한 노드로 요청을
직접 보낼 수 있다.&lt;/p&gt;

&lt;p&gt;키 범위 크기가 반드시 동일할 필요는 없다. 데이터가 고르게 분포하지 않을 수도 있기 때문이다.
예를 들어 그림의 1권은 A나 B로 시작하는 단어를 포함하지만 12권은 T, U, V, W, X, Y, Z로 시작하는 단어만 포함한다.&lt;/p&gt;

&lt;p&gt;알파벳 두 글자마다 한권씩 할당하면 다른 것들보다 훨씬 커지는 권이 생긴다. 데이터를 고르게
분산시키려면 파티션 경계를 데이터에 맞춰 조정해야 한다.&lt;/p&gt;

&lt;p&gt;파티션 경계는 관리자가 수동으로 선택하거나 데이터베이스에서 자동으로 선택되게 할 수 있다.
이런 식으로 파티셔닝하는 전략은 빅테이블, 빅테이블의 오픈소스 구현체인 HBase, 리싱크DB,
버전 2.4 이전의 몽고DB에서 사용된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/partitioned_by_key_range.png&quot; alt=&quot;partitioned_by_key_range.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;각 파티션 내에서는 키를 정렬된 순서로 저장할 수 있다. (ss테이블과 LSM 트리 참고)
이렇게 하면 범위 스캔이 쉬워지는 이점이 있고, 키를 연쇄된 색인으로 간주해서 질의 하나로 관련
레코드 여러개를 읽어오는 데 사용할 수 있다.&lt;/p&gt;

&lt;p&gt;그러나 키 범위 기준 파티셔닝은 특정 접근 패턴이 핫스팟을 유발하는 단점이 있다.
타임스탬프가 키라면 파티션은 시간 범위에 대응된다.&lt;/p&gt;

&lt;p&gt;그래서 1일치의 데이터를 파티션 하나가 담당하는 식이다. 특정 날짜에 대한 쓰기 및 읽기가
모두 동일한 파티션으로 전달되어 해당 파티션만 과부하가 걸리고 나머지 파티션은 유휴 상태로
남아 있을 수 있다.&lt;/p&gt;

&lt;p&gt;이 문제를 회피하는 방법?
키의 첫 번째 요소로 타임스탬프가 아닌 다른 것을 사용해야 함.
센서 이름을 붙여서 파티셔닝할 때 센서 이름을 먼저 사용한 후 시간을 사용하게 할 수 있다.
동시에 동작하는 센서가 많이 있다면 부하가 파티션 사이에 더 균등하게 퍼진다.
하나의 시간 범위 내에서 여러 센서의 값을 얻고 싶다면 센서 이름마다 별개의 범위 질의를
실행해야 한다.&lt;/p&gt;

&lt;h3 id=&quot;키의-해시값-기준-파티셔닝&quot;&gt;&lt;a href=&quot;#키의-해시값-기준-파티셔닝&quot;&gt;키의 해시값 기준 파티셔닝&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;쏠림과 핫스팟의 위험 때문에 많은 분산 데이터스토어는 키의 파티션을 정하는 데 해시 함수를 사용
한다.&lt;/p&gt;

&lt;p&gt;좋은 해시 함수는 쏠린 데이터를 입력으로 받아 균일하게 분산되게 한다. 문자열을 입력으로 받는
32비트 해시 함수는 0과 2^32-1 사이의 무작위 숫자를 반환한다.
입력 문자열이 거의 유사해도 해시값은 숫자 범위 내에서 균일하게 분산된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;파티셔닝용 해시 함수는 암호적으로 강력할 필요는 없다. 카산드라, 몽고DB는 MD5 해시 함수를 사용한다.&lt;/li&gt;
  &lt;li&gt;언어에 내장된 해시 함수는 적합하지 않을 수 있다. 자바의 Object.hashCode(), 루비의 Object#hash는
같은 키를 넣어도 다른 프로세스에서는 다른 해시값을 반환할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/partitioning_by_hash_of_key.png&quot; alt=&quot;partitioning_by_hash_of_key.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 기법은 키를 파티션 사이에 균일하게 분산시키는 데 좋다.
파티션 경계는 크기가 동일하도록 나눌 수도 있고 무작위에 가깝게 선택할 수도 있다.
이런 기법을 &lt;strong&gt;일관성 해싱&lt;/strong&gt;이라고 부르기도 한다.&lt;/p&gt;

&lt;p&gt;하지만 키의 해시값을 사용한 파티셔닝은 키 범위 파티셔닝의 좋은 속성을 잃어 버린다.
범위 질의를 효율적으로 실행할 수 있는 능력을 잃는다.
전에는 인접했던 키들이 이제는 모든 파티션에 흩어져서 정렬 순서가 유지되지 않는다.&lt;/p&gt;

&lt;p&gt;몽고DB에서는 해시 기반 샤딩 모드를 활성화하면 범위 질의가 모든 파티션에 전송돼야 한다.
리악, 카우치베이스, 볼드모트에서는 기본키에 대한 범위 질의가 지원되지 않는다.&lt;/p&gt;

&lt;p&gt;카산드라는 두 가지 파티셔닝 전략 사이에서 타협한다. 카산드라에서 테이블을 선언할 때
여러 칼럼을 포함하는 &lt;strong&gt;복합 기본키&lt;/strong&gt;를 지정할 수 있다.&lt;/p&gt;

&lt;p&gt;키의 첫 부분에만 해싱을 적용해 파티션 결정에 사용하고 남은 컬럼은 카산드라의 SS테이블에서
데이터를 정렬하는 연쇄된 색인으로 사용한다.&lt;/p&gt;

&lt;p&gt;따라서 복합 키의 첫 번째 칼럼에 대해서는 값 범위로 검색하는 질의를 쓸 수 없지만
첫 번째 칼럼에 고정된 값을 지정하면 키의 다른 컬럼에 대해서는 범위 스캔을 효율적으로 실행할 수 있다.&lt;/p&gt;

&lt;p&gt;예를 들어 소셜 미디어 사이트에서 사용자 한 명이 수정한 문서 여러 개를 올릴 수도 있다.
수정한 문서의 기본키를 (user_id, update_timestamp)로 선택하면 특정한 사용자가
어떤 시간 구간에서 수정한 모든 문서를 타임스탬프 순으로 정렬해서 읽어올 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;쏠린-작업부하와-핫스팟-완화&quot;&gt;&lt;a href=&quot;#쏠린-작업부하와-핫스팟-완화&quot;&gt;쏠린 작업부하와 핫스팟 완화&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;키를 해싱해서 파티션을 정하면 핫스팟을 줄이는 데 도움이 된다. 그렇지만 핫스팟을 완벽히 제거할 수는 없다.&lt;/p&gt;

&lt;p&gt;예를 들어 소셜 미디어 사이트에서 수백만 명의 팔로워를 가진 유명인이 뭔가를 하면 핫스팟이 생길 수 있다.
유명인의 글에 댓글을 작성하거나 조회를 하면 글 ID의 해시값은 동일하므로 해싱은 아무런 도움이 되지 않는다.&lt;/p&gt;

&lt;p&gt;이제 애플리케이션에서 쏠림을 완화해야 한다. 예를 들어 요청이 매우 많이 쏠리는 키를 발견하면
각 키의 시작이나 끝에 임의의 숫자를 붙이는 것이다.&lt;/p&gt;

&lt;p&gt;임의의 10진수 두 개만 붙이더라도 한 키에 대한 쓰기 작업이 100개의 다른 키로 균등하게 분산된다.&lt;/p&gt;

&lt;p&gt;그러나 다른 키에 쪼개서 쓰면 읽기에서 추가 작업이 필요하다. 100개의 키에 해당하는 데이터를 읽어서
조합해야 하기 때문이다. 추가적으로 저장해야 할 정보도 있다.&lt;/p&gt;

&lt;p&gt;이 기법은 요청이 몰리는 소수의 키에만 적용하는 게 타당하다. 쓰기 처리량이 낮은 대다수의 키에도
적용하면 불필요한 오버헤드가 생긴다. 따라서 어떤 키가 쪼개졌는지 추적할 방법도 있어야 한다.&lt;/p&gt;

&lt;h2 id=&quot;파티셔닝과-보조-색인&quot;&gt;&lt;a href=&quot;#파티셔닝과-보조-색인&quot;&gt;파티셔닝과 보조 색인&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;지금까지 설명한 파티셔닝 방식은 키-값 데이터 모델에 의존한다.
레코드를 기본키를 통해서만 접근한다면 키로부터 파티션을 결정하고 이를 사용해 키를 담당하는 파티션으로 읽기 쓰기 요청을 전달할 수 있다.&lt;/p&gt;

&lt;p&gt;보조 색인이 연관되면 복잡해진다.&lt;/p&gt;

&lt;h3 id=&quot;문서-기준-보조-색인-파티셔닝&quot;&gt;&lt;a href=&quot;#문서-기준-보조-색인-파티셔닝&quot;&gt;문서 기준 보조 색인 파티셔닝&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;중고차 판매 웹사이트 예시. 문서 ID 기준으로 파티셔닝한다. (0~499는 파티션 0, 500~999는 파티션 1 등)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/partitioning_secondary_indexes_by_document.png&quot; alt=&quot;partitioning_secondary_indexes_by_document.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자들이 색상과 제조사로 필터링할 수 있게 하려면 color와 make에 보조 색인을 만들어야 함&lt;/p&gt;

&lt;p&gt;이런 색인 방법을 사용하면 각 파티션이 완전히 독립적으로 동작한다.
각 파티션은 자신의 보조 색인을 유지하며 그 파티션에 속하는 문서만 담당한다.
그래서 문서 파티셔닝 색인은 &lt;strong&gt;지역 색인(local index)&lt;/strong&gt; 이라고도 한다.&lt;/p&gt;

&lt;p&gt;문서 ID에 특별한 작업을 하지 않는다면 보조 색인에 따라 데이터가 동일한 파티션에 저장된다는 보장이 없다.
그림에서 빨간색 자동차는 파티션 0에도 있고 파티션 1에도 있다.
따라서 빨간색 자동차를 찾고 싶다면 &lt;strong&gt;모든&lt;/strong&gt; 파티션으로 요청을 보내야 한다.&lt;/p&gt;

&lt;p&gt;이런 방법을 &lt;strong&gt;스캐터/개더(scatter/gather)&lt;/strong&gt; 라고 한다.&lt;/p&gt;

&lt;p&gt;여러 파티션에서 질의를 병렬 실행하더라도 스캐터/개더는 꼬리 지연 시간 증폭이 발생하기 쉽다.
그럼에도 보조 색인을 문서 기준으로 파티셔닝하는 경우가 많다.
몽고 DB, 리악, 카산드라, 엘라스틱서치, 솔라클라우드, 볼트DB는 모두 문서 기준으로 파티셔닝된
보조 색인을 사용한다.&lt;/p&gt;

&lt;h3 id=&quot;용어-기준-보조-색인-파티셔닝&quot;&gt;&lt;a href=&quot;#용어-기준-보조-색인-파티셔닝&quot;&gt;용어 기준 보조 색인 파티셔닝&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;각 파티션이 자신만의 보조 색인을 갖게 하는 대신, 모든 파티션의 데이터를 담당하는 &lt;strong&gt;전역 색인&lt;/strong&gt;을 만들 수도 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/partitioning_secondary_indexes_by_term.png&quot; alt=&quot;partitioning_secondary_indexes_by_term.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그러나 한 노드에만 색인을 저장할 수는 없다. 해당 노드가 병목이 되어 파티셔닝의 목적을 해치기 때문이다.
전역 색인도 파티셔닝해야 하지만 기본키 색인과는 다른 식으로 할 수 있다.&lt;/p&gt;

&lt;p&gt;모든 파티션에 있는 빨간색 자동차 정보는 색인에서 color:red 항목에 저장되지만
색깔 색인은 a부터 r까지의 글자로 시작하는 색깔은 파티션 0에, s부터 z까지의 글자로 시작하는 색깔은
파티션 1에 저장한다.&lt;/p&gt;

&lt;p&gt;찾고자 하는 용어에 따라 색인의 파티션이 결정되므로 이런 식의 색인을 &lt;strong&gt;용어 기준으로 파티셔닝됐다(term-partitioned)&lt;/strong&gt; 고 한다.&lt;/p&gt;

&lt;p&gt;문서 파티셔닝 색인에 비해 읽기가 효율적이다. 스캐터/개더를 실행할 필요 없이 원하는 용어를
포함하는 파티션으로만 요청을 보내면 된다.&lt;/p&gt;

&lt;p&gt;그러나 쓰기가 느리고 복잡하다는 단점이 있다. 단일 문서를 쓸 때 해당 색인의 여러 파티션에 영향을
줄 수 있기 때문이다(문서에 있는 모든 용어가 다른 노드에 있는 다른 파티션에 속할 수도 있다.)&lt;/p&gt;

&lt;p&gt;이상적인 세상이라면 색인은 항상 최신 상태에 있고 데이터베이스에 기록된 모든 문서는 바로 색인에
반영돼야 한다. 하지만 용어 파티셔닝 색인을 사용할 때 그렇게 하려면 쓰기에 영향받는
모든 파티션에 걸친 분산 트랜잭션을 실행해야 하는데,
모든 데이터베이스에서 분산 트랜잭션을 지원하지는 않는다.&lt;/p&gt;

&lt;h2 id=&quot;파티션-재균형화&quot;&gt;&lt;a href=&quot;#파티션-재균형화&quot;&gt;파티션 재균형화&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;시간이 지나면 데이터베이스에 변화가 생긴다&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;질의 처리량이 증가해서 늘어난 부하를 처리하기 위해 CPU를 더 추가하고 싶다.&lt;/li&gt;
  &lt;li&gt;데이터셋 크기가 증가해서 데이터셋 저장에 사용할 디스크와 램을 추가하고 싶다.&lt;/li&gt;
  &lt;li&gt;장비에 장애가 발생해서 그 장비가 담당하던 역할을 다른 장비가 넘겨받아야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 변화가 생기면 데이터와 요청이 한 노드에서 다른 노드로 옮겨져야 한다.
클러스터에서 한 노드가 담당하던 부하를 다른 노드로 옮기는 과정을 &lt;strong&gt;재균형화(rebalancing)&lt;/strong&gt; 라고 한다.&lt;/p&gt;

&lt;p&gt;어떤 파티셔닝 방식을 쓰는지에 무관하게 재균형화가 실행될 때 보통 만족시킬 것으로 기대되는
최소 요구사항이 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;재균형화 후, 부하(데이터 저장소, 읽기 쓰기 요청)가 클러스터 내에 있는 노드들 사이에 균등하게 분배돼야 한다.&lt;/li&gt;
  &lt;li&gt;재균형화 도중에도 데이터베이스는 읽기 쓰기 요청을 받아들여야 한다.&lt;/li&gt;
  &lt;li&gt;재균형화가 빨리 실행되고 네트워크와 디스크 I/O 부하를 최소화할 수 있도록 노드들 사이에
데이터가 필요 이상으로 옮겨 져서는 안 된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;재균형화-전략&quot;&gt;&lt;a href=&quot;#재균형화-전략&quot;&gt;재균형화 전략&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;쓰면 안되는 방법: 해시값에 모드 N 연산을 실행&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;앞서 말한 것 처럼, 키의 해시값 기준으로 파티셔닝할 때는 사용 가능한 해시값 범위를 나누고 각 범위를
한 파티션에 할당하는게 최선이다.&lt;/p&gt;

&lt;p&gt;모드 N 방식의 문제는 노드 개수 N이 바뀌면 대부분의 키가 노드 사이에 옮겨져야 한다.
이렇게 키가 자주 이동하면 재균형화 비용이 지나치게 커진다. 데이터를 필요 이상으로 이동하지 않는
방법이 필요하다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;파티션 개수 고정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;그 방법은 간단하다. 파티션을 노드 대수보다 많이 만들고 각 노드에 여러 파티션을 할당하는 것이다.&lt;/p&gt;

&lt;p&gt;노드 10대로 구성된 클러스터에서 실행되는 데이터베이스는 처음부터 파티션을 1,000개로 쪼개서 각 노드마다
약 100개의 파티션을 할당할 수 있다.&lt;/p&gt;

&lt;p&gt;클러스터에 노드가 추가되면 새 노드는 파티션이 다시 균일하게 분배될 때까지 기존 노드에서
파티션 몇 개를 &lt;strong&gt;뺏어올&lt;/strong&gt; 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/node.png&quot; alt=&quot;node.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;장점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;적은 데이터 이동으로 효율적 리밸런싱 가능&lt;/li&gt;
  &lt;li&gt;파티션 단위로 데이터 재배치하므로, 빠른 확장/축소 가능&lt;/li&gt;
  &lt;li&gt;더 좋은 로드 밸런싱 가능: 고성능 노드엔 더 많은 파티션 할당도 가능&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;주의점&lt;/p&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;p&gt;키 범위 파티셔닝을 사용하는 데이터베이스에서는 파티션 경계와 개수가 고정돼 있는게 매우 불편하다.
파티션 경계를 잘못 지정하면 모든 데이터가 한 파티션에 저장되고 나머지 파티션은 텅 빌 수도 있다.
파티션 경계를 수동으로 재설정한느 것은 매우 성가시다.&lt;/p&gt;

&lt;p&gt;이런 이유로 HBase나 리싱크DB처럼 키 범위 파티셔닝을 사용하는 데이터베이스에서는 파티션을
동적으로 만든다. 데이터가 많아지거나 적어지면 동적으로 리밸런싱한다.
B트리의 최상위 레벨에서 실행되는 작업과 유사하다.&lt;/p&gt;

&lt;p&gt;파티션 개수가 고정된 경우와 마찬가지로 각 파티션은 노드 하나에 할당되고 각 노드는 여러 파티션을
담당할 수 있다. 큰 파티션이 쪼개진 후 부하의 균형을 맞추기 위해 분할된 파티션 중 하나가
다른 노드로 이동될 수 있다.&lt;/p&gt;

&lt;p&gt;장점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;파티션 개수가 전체 데이터 용량에 맞춰 조정된다.&lt;/li&gt;
  &lt;li&gt;데이터 양이 작으면 파티션 개수가 적어도 되므로 오버헤드도 작다.&lt;/li&gt;
  &lt;li&gt;데이터 양이 거대하다면 개별 파티션의 크기는 설정된 최대치로 제한된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;특징&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;빈 데이터베이스는 파티션 경계를 어디로 정해야 할지 알 수 없으므로 시작할 때는 파티션이 하나다.&lt;/li&gt;
  &lt;li&gt;데이터셋이 작을 때는 모든 쓰기 요청이 하나의 노드에서 실행되고 다른 노드들은 유휴 상태에 머문다.&lt;/li&gt;
  &lt;li&gt;이 문제를 완화하기 위해 HBase, 몽고DB에서는 빈 데이터베이스에 초기 파티션 집합을 설정할
수 있게 한다. 이를 &lt;strong&gt;사전 분할(pre-splitting)&lt;/strong&gt; 이라고 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;노드 비례 파티셔닝&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;노드 수가 늘어나면 그에 비례해 파티션 수 자체를 늘리는 방법도 있다.&lt;/p&gt;

&lt;p&gt;장점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;유연한 확장성: 노드 수에 따라 파티션 수가 자동으로 조정된다.&lt;/li&gt;
  &lt;li&gt;균등한 데이터 분산: 노드당 파티션이 충분히 많으면 무작위 선택으로도 공정성 확보&lt;/li&gt;
  &lt;li&gt;파티션 크기 유지: 데이터 증가에 맞춰 노드 수를 늘리면 각 파티션의 데이터 크기는 일정함&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;주의사항&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;해시 기반 파티셔닝을 전제로 하므로, 키 순서나 범위 스캔이 어려움&lt;/li&gt;
  &lt;li&gt;초기 분할이나 리밸런싱 알고리즘이 비효율적이면 일시적 부하 불균형이 생길 수 있음&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;운영-자동-재균형화와-수동-재균형화&quot;&gt;&lt;a href=&quot;#운영-자동-재균형화와-수동-재균형화&quot;&gt;운영: 자동 재균형화와 수동 재균형화&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;완전 자동 재균형화는 일상적인 유지보수에 손이 덜 가므로 편리할 수 있다. 하지만 예측하기
어렵기도 하다. 재균형화는 요청 경로를 재설정해야 하고 대량의 데이터를 노드 사이에 이동해야
하므로 비용이 큰 연산이다. 자동화는 자동 장애 감지와 조합되면 위험해질 수 있다.&lt;/p&gt;

&lt;p&gt;이런 이유로 재균형화 과정에 사람이 개입하는 게 좋을 수도 있다. 완전 자동 처리보다는
느릴 수 있지만 운영상 예상치 못한 일을 방지하는 데 도움될 수 있다.&lt;/p&gt;

&lt;h2 id=&quot;요청-라우팅&quot;&gt;&lt;a href=&quot;#요청-라우팅&quot;&gt;요청 라우팅&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;데이터셋을 여러 장비에서 실행되는 여러 노드에 파티셔닝했는데, 클라이언트에서 요청을 보내려고 할 때
어느 노드로 접속해야 하는지 어떻게 알 수 있을까?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/request_routing.png&quot; alt=&quot;request_routing.png&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;클라이언트가 아무 노드에나 접속하게 한다(예를 들어 라운드로빈 로드 밸런서를 통해). 만약 해당 노드에 마침 요청을 적
용할 파티션이 있다면 거기서 요청을 직접 처리할 수 있다. 그렇지 않으면 요청을 올바른 노드로 전달해서 응답을 받고 클
라이언트에게 응답을 전달한다.&lt;/li&gt;
  &lt;li&gt;클라이언트의 모든 요청을 라우팅 계층으로 먼저 보낸다. 라우팅 계층에서는 각 요청을 처리할 노드를 알아내고 그에 따라
해당 노드로 요청을 전달한다. 라우팅 계층 자체에서는 아무 요청도 처리하지 않는다. 파티션 인지(partition-aware) 로드
밸런서로 동작할 뿐이다.&lt;/li&gt;
  &lt;li&gt;클라이언트가 파티셔닝 방법과 파티션이 어떤 노드에 할당됐는지를 알고 있게 한다. 이 경우 클라이언트는 중개자 없이 올
바른 노드로 직접 접속할 수 있다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;병렬-질의-실행&quot;&gt;&lt;a href=&quot;#병렬-질의-실행&quot;&gt;병렬 질의 실행&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;분석용으로 자주 사용되는 대규모 병렬 처리(massively parallel processing, MPP) 관
계형 데이터베이스 제품은 훨씬 더 복잡한 종류의 질의를 지원한다. 전형적인 데이터 웨어하우스 질
의는 조인(join), 필터링(filtering), 그룹화(grouping), 집계(aggregation) 연산을 몇 개 포함한
다.&lt;/p&gt;

&lt;p&gt;MPP 질의 최적화기는 복잡한 질의를 여러 실행 단계와 파티션으로 분해하며 이들 중 다수는 데
이터베이스 클러스터 내의 서로 다른 노드에서 병렬적으로 실행될 수 있다.&lt;/p&gt;

&lt;h2 id=&quot;정리&quot;&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;대용량 데이터셋을 더욱 작은 데이터셋으로 파티셔닝하는 다양한 방법을 살펴봤다.
저장하고 처리할 데이터가 너무 많아서 장비 한 대로 처리하는 게 불가능해지면 파티셔닝이 필요 하다.&lt;/p&gt;

&lt;p&gt;파티셔닝의 목적은 핫스팟(불균형적으로 높은 부하를 받는 노드)이 생기지 않게 하면서 데이터와 질의 부하를 여러 장비에 균일하게 분배하는 것이다.
그렇게 하려면 데이터에 적합한 파티셔닝 방식을 선택해야 하고 클러스터에 노드가 추가되거나 클러스터에서 노드가 제거될 때 파티션 재균형화를 실행해야 한다.&lt;/p&gt;

&lt;p&gt;두 가지 주요 파티셔닝 기법을 설명했다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;키 범위 파티셔닝: 키가 정렬돼 있고 개별 파티션은 어떤 최솟값과 최댓값 사이에 속하는 모든 키를 담당한다. 키가 정렬돼
있어 범위 질의가 효율적이라는 장점이 있지만 애플리케이션에서 정렬 순서가 서로 가까운 키에 자주 접근하면 핫스팟이
생길 위험이 있다. 이 방법에서는 보통 한 파티션이 너무 커지면 키 범위를 두 개로 쪼개 동적으로 재균형화를 실행한다.&lt;/li&gt;
  &lt;li&gt;해시 파티셔닝: 각 키에 해시 함수를 적용하고 개별 파티션은 특정 범위의 해시값을 담당한다. 이 방법을 쓰면 키 순서가 보장되지 않아 범위 질의가
비효율적이지만 부하를 더욱 균일하게 분산할 수 있다.
해시 파티셔닝을 사용할 때는 보통 고정된 개수의 파티션을 미리 만들어 각 노드에 몇 개씩의 파티션을 할당하며 노드가 추가되거나 제거되면 파티션을 통째로 노드 사이에서 이동한다. 동적 파티셔닝을 쓸 수도 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;두 가지 방법을 섞어 쓸 수도 있다. 이를테면 키의 일부분은 파티션 식별용으로, 나머지 부분은 정렬 순서용으로 만든 복합 키를 사용하는 것이다.
파티셔닝과 보조 색인 사이의 상호작용에 대해서도 얘기했다. 보조 색인도 파티셔닝이 필요한데 두가지 방법이 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;파티셔닝 색인(지역 색인): 보조 색인을 기본키와 값이 저장된 파티션에 저장한다. 쓸 때는 파티션 하나만 갱신하면 되
지만 보조 색인을 읽으려면 모든 파티션에 걸쳐서 스캐터/개더를 실행해야 한다.&lt;/li&gt;
  &lt;li&gt;용어 파티셔닝 색인(전역 색인): 색인된 값을 사용해서 보조 색인을 별도로 파티셔닝한다. 보조 색인 항목은 기본키의 모든
파티션에 있는 레코드를 포함할 수도 있다. 문서를 쓸 때는 보조 색인 여러 개를 갱신해야 하지만 읽기는 단일 파티션에서
실행될 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;끝으로 단순한 파티션 인지 로드 밸런서에서 복잡한 병렬 질의 처리 엔진까지 질의를 올바른 파티션으로 라우팅하는 기법도 다뤘다.
설계상 모든 파티션은 대부분 독립적으로 동작한다. 그렇기 때문에 파티셔닝된 데이터베이스는 여
러 장비로 확장될 수 있다. 그러나 여러 파티션에 기록해야 하는 연산은 따져 보기 어려울 수 있다.
예를 들어 한 파티션에는 쓰기 성공했지만 다른 파티션에서 실패하면 어떻게 될까? 이어지는 장에
서 이 의문을 다룬다.&lt;/p&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-engineering" /><summary type="html">Partitioning 파티셔닝과 복제 키-값 데이터 파티셔닝 키 범위 기준 파티셔닝 키의 해시값 기준 파티셔닝 쏠린 작업부하와 핫스팟 완화 파티셔닝과 보조 색인 문서 기준 보조 색인 파티셔닝 용어 기준 보조 색인 파티셔닝 파티션 재균형화 재균형화 전략 운영: 자동 재균형화와 수동 재균형화 요청 라우팅 병렬 질의 실행 정리</summary></entry><entry><title type="html">비트캐스크와 해시 색인의 동작 원리, 그리고 컴팩션 구조까지</title><link href="https://jangjichang.github.io/posts/understanding-bitcask-hash-indexing-and-the-compaction-process" rel="alternate" type="text/html" title="비트캐스크와 해시 색인의 동작 원리, 그리고 컴팩션 구조까지" /><published>2025-06-06T00:00:00+09:00</published><updated>2025-06-06T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/Understanding-Bitcask-Hash-Indexing-and-the-Compaction-Process</id><content type="html" xml:base="https://jangjichang.github.io/posts/understanding-bitcask-hash-indexing-and-the-compaction-process">&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#비트캐스크와-해시-색인의-동작-원리-그리고-컴팩션-구조까지&quot;&gt;비트캐스크와 해시 색인의 동작 원리, 그리고 컴팩션 구조까지&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#가장-간단한-데이터베이스로-시작&quot;&gt;가장 간단한 데이터베이스로 시작&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#색인&quot;&gt;색인&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#해시-색인&quot;&gt;해시 색인&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#비트캐스크&quot;&gt;비트캐스크&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#컴팩션&quot;&gt;컴팩션&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#해시-색인의-제한-사항&quot;&gt;해시 색인의 제한 사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#sstable-and-lsm-tree&quot;&gt;SSTable and LSM Tree&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#참고&quot;&gt;참고&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터애플리케이션 3장에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;저장소와 검색&lt;/code&gt;에 대해 다룬다.
파일 기반 저장소를 시작으로 SQL, NoSQL 데이터베이스에서 사용하는 데이터 저장 구조와 검색 방식에 대해 설명한다.
애플리케이션 개발자로서 상황에 맞는 데이터베이스 선택은 중요하다.
이를 위해 데이터베이스의 내부 구조와 검색 방식을 이해하는 것이 필요하기 때문에 3장에서 언급한 자료구조와 검색 방식을 따로 정리한다.&lt;/p&gt;

&lt;h2 id=&quot;가장-간단한-데이터베이스로-시작&quot;&gt;가장 간단한 데이터베이스로 시작&lt;/h2&gt;

&lt;p&gt;가장 간단한 데이터베이스 방식은 파일의 마지막에 추가하고, 파일을 읽어서 검색하는 것이다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

db_set&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; db.txt
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

db_get&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &quot;&lt;/span&gt; database | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,//&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;키, 값 저장소를 함수 두 개로 구현했다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db_set key value
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 키, 값을 저장하는 함수를 호출하면 데이터베이스에 key, value를 저장할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db_get key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 키를 입력하면 해당 키에 대한 값을 검색할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;이 방식은 몇가지 특징이 있다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;db_set을 호출할 때마다 파일의 끝에 추가하므로 키를 여러 번 갱신해도 예전 값을 덮어 쓰지 않는다.&lt;/li&gt;
  &lt;li&gt;최신 값을 찾기 위해서는 파일에서 키의 마지막 항목을 찾아야 한다. (그래서 db_get에서 tail -n 1을 사용했다.)&lt;/li&gt;
  &lt;li&gt;파일에 쓰기 작업은 매우 효율적이다.&lt;/li&gt;
  &lt;li&gt;읽기 작업은 파일을 처음부터 끝까지 읽어야 하므로 비효율적이다. 검색 비용이 O(n)이다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;색인&quot;&gt;색인&lt;/h2&gt;

&lt;p&gt;위에서 설명한 파일에 쓰는 데이터베이스에서 특정 키의 값을 효율적으로 찾기 위해 다른 데이터 구조가 필요하다. 바로 &lt;strong&gt;색인&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;색인의 일반적인 개념은 어떤 부가적인 메타데이터를 유지하는 것이다. 예를 들어, 파일의 각 줄에 키와 값이 저장되어 있다면,
색인은 키와 해당 키가 저장된 파일의 위치(오프셋)를 유지할 수 있다. 이 작업은 데이터베이스의 내용에는 영향을 미치지 않는다. 검색 성능에 영향을 준다.&lt;/p&gt;

&lt;h2 id=&quot;해시-색인&quot;&gt;해시 색인&lt;/h2&gt;

&lt;p&gt;디스크 상의 데이터를 색인하기 위해 인메모리 데이터 구조를 사용하는 것은 어떨까?&lt;/p&gt;

&lt;p&gt;앞의 예제처럼 단순히 파일에 추가하는 방식으로 데이터 저장소를 구성한다고 가정해보자. 그러면 가장 간단하게 가능한 색인 전략은 다음과 같다.&lt;/p&gt;

&lt;p&gt;키를 데이터 파일의 바이트 오프셋에 매핑해 인메모리 해시 맵을 유지하는 전략이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/byte-offset-hash.png&quot; alt=&quot;byte-offset-hash.png&quot; title=&quot;Byte Offset Hash&quot; /&gt;&lt;/p&gt;

&lt;p&gt;바이트 오프셋은 값을 바로 찾을 수 있는 위치다.&lt;/p&gt;

&lt;p&gt;key 123456은 파일의 시작 지점에서 0 byte 떨어진 곳에 있다. key 42는 64 byte 떨어진 곳에 있다. 파일에 새로운 키-값 쌍을 추가할 때마다
방금 기록한 데이터의 오프셋을 반영하기 위해 해시 맵도 갱신해야 한다.&lt;/p&gt;

&lt;p&gt;이 방식은 비트캐스크(Bitcask) (리악(Riak)의 기본 저장소 엔진)가 근본적으로 사용하는 방식이다. 비트캐스크는 해시 맵을 전부 메모리에 유지하기 때문에
사용 가능한 램에 모든 키가 저장된다는 조건을 전제로 고성능 읽기, 쓰기를 보장한다. 값은 한 번의 디스크 탐색으로 디스크에서 적재할 수 있기 때문에 사용
가능한 메모리보다 더 많은 공간을 사용할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;비트캐스크&quot;&gt;비트캐스크&lt;/h3&gt;

&lt;p&gt;비트캐스크 같은 저장소 엔진은 각 키의 값이 자주 갱신되는 상황에 매우 적합하다. 예를 들어 키는 고양이 동영상의 url이고 값은 비디오가 재생된 횟수인 경우다.
이런 유형의 작업부하에서는 쓰기가 아주 많지만 고유 키는 많지 않다. 즉, 키당 쓰기 수가 많지만 메모리에 모든 키를 보관할 수 있다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;data file segment&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;mew:1078&lt;/td&gt;
      &lt;td&gt;purr:2103&lt;/td&gt;
      &lt;td&gt;purr:2104&lt;/td&gt;
      &lt;td&gt;mew:1079&lt;/td&gt;
      &lt;td&gt;mew:1080&lt;/td&gt;
      &lt;td&gt;mew:1081&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;purr:2105&lt;/td&gt;
      &lt;td&gt;purr:2106&lt;/td&gt;
      &lt;td&gt;purr:2107&lt;/td&gt;
      &lt;td&gt;yawn:511&lt;/td&gt;
      &lt;td&gt;purr:2108&lt;/td&gt;
      &lt;td&gt;mew:1082&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;위의 테이블 처럼 왼쪽 위부터 오른쪽 아래로 순차적으로 키-값 쌍이 저장된다.&lt;/p&gt;

&lt;p&gt;하지만 파일에 항상 추가만 한다면 결국 디스크 공간이 부족해진다. 이 상황은 어떻게 피할 수 있을까? 특정 크기의 세그먼트로 로그를 나누는 방식으로 해결한다.
특정 크기에 도달하면 세그먼트 파일을 닫고 새로운 세그먼트 파일에 이후 쓰기를 수행한다. 그리고 컴팩션(compaction)을 수행할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;컴팩션&quot;&gt;컴팩션&lt;/h3&gt;

&lt;p&gt;컴팩션은 로그에서 중복된 키를 버리고 각 키의 최신 갱신 값만 유지하는 것을 위미한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;data file segment&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;mew:1078&lt;/td&gt;
      &lt;td&gt;purr:2103&lt;/td&gt;
      &lt;td&gt;purr:2104&lt;/td&gt;
      &lt;td&gt;mew:1079&lt;/td&gt;
      &lt;td&gt;mew:1080&lt;/td&gt;
      &lt;td&gt;mew:1081&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;purr:2105&lt;/td&gt;
      &lt;td&gt;purr:2106&lt;/td&gt;
      &lt;td&gt;purr:2107&lt;/td&gt;
      &lt;td&gt;yawn:511&lt;/td&gt;
      &lt;td&gt;purr:2108&lt;/td&gt;
      &lt;td&gt;mew:1082&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;-&amp;gt; 컴팩션 과정 후&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;컴팩션된 세그먼트&lt;/th&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;yawn:511&lt;/td&gt;
      &lt;td&gt;mew:1082&lt;/td&gt;
      &lt;td&gt;purr:2108&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;컴팩션 전에 yawn, mew, purr 키가 여러 번 갱신되었지만 컴팩션 후에는 각 키의 최신 값만 유지된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;컴팩션은 최신 값만 유지하기 때문에 보통 세그먼트를 더 작게 만든다.&lt;/li&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;p&gt;data file segment 1&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;data file segment 1&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;mew:1078&lt;/td&gt;
      &lt;td&gt;purr:2103&lt;/td&gt;
      &lt;td&gt;purr:2104&lt;/td&gt;
      &lt;td&gt;mew:1079&lt;/td&gt;
      &lt;td&gt;mew:1080&lt;/td&gt;
      &lt;td&gt;mew:1081&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;purr:2105&lt;/td&gt;
      &lt;td&gt;purr:2106&lt;/td&gt;
      &lt;td&gt;purr:2107&lt;/td&gt;
      &lt;td&gt;yawn:511&lt;/td&gt;
      &lt;td&gt;purr:2108&lt;/td&gt;
      &lt;td&gt;mew:1082&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;data file segment 2&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;data file segment 2&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;purr:2109&lt;/td&gt;
      &lt;td&gt;purr:2110&lt;/td&gt;
      &lt;td&gt;mew:1083&lt;/td&gt;
      &lt;td&gt;scratch:252&lt;/td&gt;
      &lt;td&gt;mew:1084&lt;/td&gt;
      &lt;td&gt;mew:1085&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;purr:2111&lt;/td&gt;
      &lt;td&gt;mew:1086&lt;/td&gt;
      &lt;td&gt;purr:2112&lt;/td&gt;
      &lt;td&gt;purr:2113&lt;/td&gt;
      &lt;td&gt;mew:1087&lt;/td&gt;
      &lt;td&gt;purr:2114&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;-&amp;gt; 컴팩션과 병합 과정&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;컴팩션된 세그먼트&lt;/th&gt;
      &lt;th&gt; &lt;/th&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;yawn:511&lt;/td&gt;
      &lt;td&gt;scratch:252&lt;/td&gt;
      &lt;td&gt;mew:1087&lt;/td&gt;
      &lt;td&gt;purr:2114&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;이제 각 세그먼트는 키를 파일 오프셋에 매핑한 자체 인메모리 해시 테이블을 갖는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;키의 값을 찾는 예시&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/scan-inmemory-hashtable.png&quot; alt=&quot;scan-inmemory-hashtable.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;데이터가 쓰여진 순서는 data file segment 1, data file segment 2 순서라고 가정한다.
그럼 각 세그먼트별로 인메모리 해시 테이블이 있고, 인메모리 해시 테이블을 순서대로 확인한다.
최신 세그먼트 해시 맵을 먼저 확인하고 만약 키가 없다면 두 번째 최신 세그먼트를 확인한다.&lt;/p&gt;

&lt;p&gt;책에서는 각 세그먼트별로 해시 테이블을 유지한다고 했고, Riak에서는 ‘keydir’&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;이라고 부르는 하나의 해시 테이블&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;로 유지한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/bitcask-keydir.png&quot; alt=&quot;bitcask-keydir.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;해시-색인의-제한-사항&quot;&gt;해시 색인의 제한 사항&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;너무 많은 메모리 공간 필요&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;해시 테이블은 메모리에 저장해야 하므로 키가 너무 많으면 문제가 된다. 원칙적으로는 디스크에 해시 맵을 유지할 수 있지만 그러면 좋은 성능을 기대하기 어렵다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;비효율적인 범위 질의(range query)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;예를들어 kitty000과 kitty999 사이의 모든 키를 쉽게 스캔할 수 없다. 해시 맵에서 모든 개별 키를 조회해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;sstable-and-lsm-tree&quot;&gt;SSTable and LSM Tree&lt;/h2&gt;

&lt;p&gt;이러한 제한 사항을 해결하기 위해 &lt;strong&gt;SSTable&lt;/strong&gt;과 &lt;strong&gt;LSM Tree&lt;/strong&gt;가 등장한다.&lt;/p&gt;

&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://riak.com/assets/bitcask-intro.pdf&quot;&gt;bitcask 소개 3페이지 참고&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://docs.riak.com/riak/kv/2.2.3/setup/planning/backend/bitcask/index.html#weaknesses&quot;&gt;riak bitcask backend weaknesses&lt;/a&gt; &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-structures" /><category term="sstable" /><category term="btree" /><category term="log-structured-merge-tree" /><category term="lsm-tree" /><summary type="html">비트캐스크와 해시 색인의 동작 원리, 그리고 컴팩션 구조까지 가장 간단한 데이터베이스로 시작 색인 해시 색인 비트캐스크 컴팩션 해시 색인의 제한 사항 SSTable and LSM Tree 참고</summary></entry><entry><title type="html">LSM Tree와 B-Tree</title><link href="https://jangjichang.github.io/posts/lsm-tree-and-sstable" rel="alternate" type="text/html" title="LSM Tree와 B-Tree" /><published>2025-06-06T00:00:00+09:00</published><updated>2025-06-06T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/lsm-tree-and-sstable</id><content type="html" xml:base="https://jangjichang.github.io/posts/lsm-tree-and-sstable">&lt;p&gt;해시 색인의 문제로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;너무 많은 메모리 공간 필요&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;비효율적인 범위 질의(range query)&lt;/code&gt; 등이 발생한다.&lt;/p&gt;

&lt;p&gt;이 문제를 해결하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSTable&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LSM Tree&lt;/code&gt;가 등장한다.&lt;/p&gt;

&lt;h2 id=&quot;ss테이블과-lsm-트리&quot;&gt;SS테이블과 LSM 트리&lt;/h2&gt;

&lt;p&gt;해시 색인에서 키-값 쌍을 키로 정렬하자. 이처럼 키로 정렬된 형식을 &lt;strong&gt;정렬된 문자열 테이블(Sorted String Table, SSTable)&lt;/strong&gt; 이라고 한다.
각 키는 각 병합된 세그먼트 파일 내에 한 번만 나타나야 한다(컴팩션 과정은 이를 이미 보장한다).&lt;/p&gt;

&lt;p&gt;SSTable은 해시 색인을 가진 로그 세그먼트보다 몇 가지 큰 장점이 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;세그먼트 병합은 파일이 사용 가능한 메모리보다 크더라도 간단하고 효율적이다. 병합정렬 알고리즘과 유사하다.
각 세그먼트의 첫번째 키를 보고 가장 낮은 키를 출력 파일로 복사한 뒤 이 과정을 반복한다. 이 과정에서 만들어진 파일도 정렬돼 있다.
각 세그먼트에 동일한 키가 있으면 가장 최근의 값을 출력 파일로 복사한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;data file segment 1&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;handbag:8786&lt;/td&gt;
      &lt;td&gt;handful:40308&lt;/td&gt;
      &lt;td&gt;handicap:65995&lt;/td&gt;
      &lt;td&gt;handkerchief:16324&lt;/td&gt;
      &lt;td&gt;handlebars:3869&lt;/td&gt;
      &lt;td&gt;handprinted:11150&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;data file segment 2&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;handcuffs:2729&lt;/td&gt;
      &lt;td&gt;handful:42307&lt;/td&gt;
      &lt;td&gt;handicap:67884&lt;/td&gt;
      &lt;td&gt;handiwork:16912&lt;/td&gt;
      &lt;td&gt;handkerchief:20952&lt;/td&gt;
      &lt;td&gt;handprinted:15725&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;data file segment 3&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;handful:44662&lt;/td&gt;
      &lt;td&gt;handicap:70836&lt;/td&gt;
      &lt;td&gt;handiwork:45521&lt;/td&gt;
      &lt;td&gt;handlebars:3869&lt;/td&gt;
      &lt;td&gt;handoff:5741&lt;/td&gt;
      &lt;td&gt;handprinted:33632&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;merged segment 1,2,3&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&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;handbag:8786&lt;/td&gt;
      &lt;td&gt;handcuffs:2729&lt;/td&gt;
      &lt;td&gt;handful:44662&lt;/td&gt;
      &lt;td&gt;handicap:70836&lt;/td&gt;
      &lt;td&gt;handiwork:45521&lt;/td&gt;
      &lt;td&gt;handkerchief:20952&lt;/td&gt;
      &lt;td&gt;handlebars:3869&lt;/td&gt;
      &lt;td&gt;handoff:5741&lt;/td&gt;
      &lt;td&gt;handprinted:33632&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;ul&gt;
  &lt;li&gt;파일에서 특정 키를 찾기 위해 더는 메모리에 모든 키의 색인을 유지할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그림에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handiwork&lt;/code&gt; 키를 찾으려 하지만 세그먼트 파일에서 키의 정확한 오프셋을 알지 못한다고 가정해보자. Sparse index in memory에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handiwork&lt;/code&gt;
키를 찾지 못한 상황이다.&lt;/p&gt;

&lt;p&gt;그래도 handbag과 handsome 키의 오프셋을 알고 있고 정렬돼 있으므로 handiwork는 두 키 사이에 있다는 사실을 알 수 있다.
즉, handbag 오프셋으로 이동해 handiwork가 나올 때 까지 스캔하면 된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/inmemory-hash-sstable.png&quot; alt=&quot;inmemory-hash-sstable.png&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;ul&gt;
  &lt;li&gt;읽기 요청은 요청 범위 내에서 여러 키-값 쌍을 스캔해야 하기 때문에 해당 레코드들을 블록으로 그룹화하고 디스크에 쓰기 전에 압축한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;ss테이블-생성과-유지&quot;&gt;SS테이블 생성과 유지&lt;/h2&gt;

&lt;p&gt;레드 블랙 트리나 AVL 트리와 같이 잘 알려졌고 사용 가능한 트리 데이터 구조를 사용하면 임의 순서로 키를 삽입하고 정렬된 순서로 해당 키를 다시 읽을 수 있다.&lt;/p&gt;

&lt;p&gt;이제 저장소 엔진을 다음과 같이 만들 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;쓰기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;쓰기가 들어오면 인메모리 균형 트리 데이터 구조에 추가한다. 이 인메모리 트리는 &lt;strong&gt;멤테이블(MemTable)&lt;/strong&gt; 이라고도 한다.&lt;/li&gt;
  &lt;li&gt;멤테이블이 보통 수 메가바이트 정도의 임곗값보다 커지면 멤테이블을 디스크에 SSTable로 플러시한다. 트리가 이미 정렬돼 있으므로 플러시하는 과정은 간단하다.
SS 테이블을 디스크에 기록하는 동안 쓰기는 새로운 멤테이블 인스턴스에 기록한다.&lt;/li&gt;
&lt;/ul&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;가끔 세그먼트 파일을 합치고 덮어 쓰여지거나 삭제된 값을 버리는 병합과 컴팩션 과정을 수행한다. 이 과정은 백그라운드에서 수행된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;sstable에서-lsm-트리-만들기&quot;&gt;SSTable에서 LSM 트리 만들기&lt;/h2&gt;

&lt;p&gt;이 색인 구조는 로그 구조화 병합 트리(Log-Structured Merge Tree, LSM Tree)라고 한다. 이 색인 구조는 로그 구조화 파일 시스템의 초기 작업의 기반이 됐다.
정렬된 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진을 LSM 저장소 엔진이라 부른다.&lt;/p&gt;

&lt;p&gt;루씬(Lucene)은 엘라스틱서치나 솔라에서 사용하는 전문 검색 색인 엔진이다. 루씬은 &lt;strong&gt;용어 사전(term dictionary)&lt;/strong&gt;을 저장하기 위해 유사한 방법을 사용한다.
전문 색인은 키-값 색인보다 훨씬 더 복잡하지만 이와 유사한 개념을 기반으로 한다.&lt;/p&gt;

&lt;p&gt;검색 질의로 단어가 들어오면 단어가 언급된 모든 문서를 찾는다.
이 접근법은 키를 단어(용어)로, 값은 단어를 포함한 모든 문서의 ID 목록으로 하는 키-값 구조로 구현한다. 루씬에서 용어와 포스팅 목록의 매핑은 SSTable 같은
정렬 파일에 유지하고 필요에 따라 백그라운드에서 병합한다.&lt;/p&gt;

&lt;h2 id=&quot;성능-최적화&quot;&gt;성능 최적화&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;존재하지 않는 키를 찾는 경우&lt;/strong&gt;
데이터를 읽을 때 멤테이블에서 키를 찾고 없으면 순차적으로 최신의 세그먼트를 읽는다. 키를 찾을 때 까지 모든 세그먼트를 읽어야한다.
이런 종류의 접근을 최적화하기 위해 저장소 엔진은 보통 &lt;strong&gt;블룸 필터(Bloom filter)&lt;/strong&gt;를 추가적으로 사용한다.&lt;/p&gt;

&lt;h3 id=&quot;블룸-필터&quot;&gt;블룸 필터&lt;/h3&gt;

&lt;p&gt;블룸 필터는 공간 효율적인 확률적 자료구조(probabilistic data structure)로, 특정 키가 집합에 포함되지 않았는지 빠르게 확인할 수 있다.
단, 오류 확률을 허용하고 다음의 특징이 있습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;존재하지 않음을 확인할 때는 정확함 (true negative)이 보장됨&lt;/li&gt;
  &lt;li&gt;존재함을 확인할 때는 거짓 양성(false positive)이 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;구성 요소&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;고정 크기의 비트 배열&lt;/li&gt;
  &lt;li&gt;여러 개의 해시 함수&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터 삽입 과정&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;삽입할 키에 여러 개의 해시 함수를 적용합니다.&lt;/li&gt;
  &lt;li&gt;각 해시 함수의 결과 위치에 해당하는 비트를 1로 설정합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;조회 과정&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;조회할 키에 동일한 해시 함수를 적용합니다.&lt;/li&gt;
  &lt;li&gt;해시 결과 위치의 비트가 모두 1인지 확인한다.
    &lt;ul&gt;
      &lt;li&gt;하나라도 0이면: 해당 키는 존재하지 않음&lt;/li&gt;
      &lt;li&gt;모두 1이면: 해당 키가 존재할 수도 있음 (오류 가능). 실제 SSTable에서 확인해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;LSM Tree에서 각 SSTable에 대해 블룸 필터를 유지할 수 있다. 블룸 필터는 SSTable의 키가 존재하지 않는지 빠르게 확인할 수 있게 해준다.&lt;/p&gt;

&lt;p&gt;해시함수 1, 2, 3을 사용해 입력 키를 해싱하고, 해시값을 비트 배열의 인덱스로 사용한다. 해당 인덱스의 비트를 1로 설정한다.
mew, fin 키를 삽입한 예시다. 아래에 첫번째 행이 비트 배열이고 두번째 행이 해시 함수의 결과로 설정된 비트 인덱스다. 1로 설정하지만
예시를 위해 key 값을 넣어두었다. 키 값이 있는건 1로 설정되었다고 보면 된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/bloom_filter_1.png&quot; alt=&quot;bloom_filter_1.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mew&lt;/code&gt; 키를 입력하면 블룸 필터가 어떻게 변경되는지 살펴보자.
해시 함수에 대해 결과가 각 1, 3, 8이 나온다. 이 인덱스의 비트를 1로 설정한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/bloom_filter_2.png&quot; alt=&quot;bloom_filter_2.png&quot; /&gt;
이제 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fin&lt;/code&gt; 키를 입력해보자.
해시 함수에 대해 결과가 각 1, 5, 9가 나온다. 이 인덱스의 비트를 1로 설정한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/bloom_filter_3.png&quot; alt=&quot;bloom_filter_3.png&quot; /&gt;
이제 bengal 키를 조회해보자.
bengal 키를 해싱하면 1, 2, 3 해시 함수의 결과로 각각 2, 5, 8 인덱스가 나온다. 이 인덱스의 비트가 모두 1인지 확인한다.
이 경우 2는 0이므로 ‘하나라도 0이면 해당 키는 존재하지 않는다’는 블룸 필터의 성질에 따라 bengal 키는 존재하지 않는다고 판단할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/bloom_filter_4.png&quot; alt=&quot;bloom_filter_4.png&quot; /&gt;
거짓 양성(false positive)이 발생할 수 있다. 예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ragdoll&lt;/code&gt; 키를 조회해보자.
ragdoll 키를 해싱하면 1, 2, 3 해시 함수의 결과로 각각 3, 8, 9 인덱스가 나온다. 이 인덱스의 비트가 모두 1이므로 블룸 필터는 ragdoll 키가 존재한다고 판단한다.&lt;/p&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-structures" /><category term="sstable" /><category term="btree" /><category term="log-structured-merge-tree" /><category term="lsm-tree" /><summary type="html">해시 색인의 문제로 너무 많은 메모리 공간 필요, 비효율적인 범위 질의(range query) 등이 발생한다.</summary></entry><entry><title type="html">3장 저장소와 검색</title><link href="https://jangjichang.github.io/posts/storage-and-retrieval-chapter3" rel="alternate" type="text/html" title="3장 저장소와 검색" /><published>2025-05-28T00:00:00+09:00</published><updated>2025-05-28T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/storage-and-retrieval-chapter3</id><content type="html" xml:base="https://jangjichang.github.io/posts/storage-and-retrieval-chapter3">&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#3장-저장소와-검색&quot;&gt;3장 저장소와 검색&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#데이터베이스를-강력하게-만드는-데이터-구조&quot;&gt;데이터베이스를 강력하게 만드는 데이터 구조&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#해시-색인&quot;&gt;해시 색인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#ss테이블과-lsm-트리&quot;&gt;SS테이블과 LSM 트리&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#1단계-첫-번째-쓰기--sstable-a-생성&quot;&gt;1단계: 첫 번째 쓰기 &amp;amp; SSTable A 생성&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#쓰기-요청&quot;&gt;쓰기 요청&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#memtable-상태-flush-전&quot;&gt;MemTable 상태 (flush 전)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#sstable-상태&quot;&gt;SSTable 상태&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#이벤트-memtable이-꽉-참--sstable로-flush&quot;&gt;이벤트: MemTable이 꽉 참 → SSTable로 flush&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#sstable-a-생성&quot;&gt;SSTable A 생성&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#index-block-예시-희소-인덱스&quot;&gt;Index Block 예시 (희소 인덱스)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#결과-상태&quot;&gt;결과 상태&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#2단계-두-번째-쓰기--새로운-memtable-생성&quot;&gt;2단계: 두 번째 쓰기 &amp;amp; 새로운 MemTable 생성&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#쓰기-요청-1&quot;&gt;쓰기 요청&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#memtable-상태-현재&quot;&gt;MemTable 상태 (현재)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#sstable-상태-변함없음&quot;&gt;SSTable 상태 (변함없음)&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#3단계-병합compaction&quot;&gt;3단계: 병합(compaction)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#병합-로직&quot;&gt;병합 로직&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#sstable-b-생성&quot;&gt;SSTable B 생성&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#index-block-예시-희소-인덱스-1&quot;&gt;Index Block 예시 (희소 인덱스)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#병합-후-상태&quot;&gt;병합 후 상태&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#4단계-데이터-찾는-예시&quot;&gt;4단계: 데이터 찾는 예시&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#예시-a-date-찾기&quot;&gt;예시 A: “date” 찾기&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#예시-b-apple-찾기&quot;&gt;예시 B: “apple” 찾기&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#전체-구조-정리-병합-후&quot;&gt;전체 구조 정리 (병합 후)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#최종-요약&quot;&gt;최종 요약&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#b-트리&quot;&gt;B 트리&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#b-트리와-lsm-트리-비교&quot;&gt;B 트리와 LSM 트리 비교&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#advantages-of-lsm-trees&quot;&gt;Advantages of LSM-trees&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#downsides-of-lsm-trees&quot;&gt;Downsides of LSM-trees&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#트랜잭션-처리나-분석&quot;&gt;트랜잭션 처리나 분석?&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#칼럼-지향-저장소&quot;&gt;칼럼 지향 저장소&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터베이스의 가장 기본적인 기능은 데이터를 저장하고 나중에 그 데이터를 요청하면 다시 데이터를 제공하는 것이다.
데이터베이스가 데이터를 저장하는 방법과 데이터를 요청했을 때 다시 찾을 수 있는 방법을 알아보자.&lt;/p&gt;

&lt;p&gt;애플리케이션 개발자가 이를 알아야하는 이유가 무엇일까? 대부분 애플리케이션 개발자가 처음부터 저장소 엔진을 구현하기보다는
사용 가능한 여러 저장소 엔진 중에 애플리케이션에 적합한 엔진을 선택하는 작업이 필요하다.
특정 작업부하(workload) 유형에서 좋은 성능을 내게끔 저장소 엔진을 조정하려면 저장소 엔진이 내부에서 수행되는
작업에 대해 대략적인 개념을 이해할 필요가 있다.&lt;/p&gt;

&lt;p&gt;특히 트랜잭션 작업부하에 맞춰 최적화된 저장소 엔진과 분석을 위해 최적화된 엔진 간에는 큰 차이가 있다.&lt;/p&gt;

&lt;h2 id=&quot;데이터베이스를-강력하게-만드는-데이터-구조&quot;&gt;데이터베이스를 강력하게 만드는 데이터 구조&lt;/h2&gt;

&lt;p&gt;제일 간단한 데이터베이스&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

db_set&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; database
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

db_get&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &quot;&lt;/span&gt; database | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,//&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파일 저장의 특징&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;파일에 키-값 쌍을 저장한다. 키의 값을 가져온다.&lt;/li&gt;
  &lt;li&gt;일반적으로 파일 추가 작업은 매우 효율적이기 때문에 간단한 작업의 경우 실제로 꽤 좋은 성능을 보여준다.&lt;/li&gt;
  &lt;li&gt;많은 데이터베이스는 추가 전용(append-only) 데이터 파일인 로그(log)를 사용한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;파일 저장의 단점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;많은 레코드가 있으면 성능이 매우 좋지 않다. - 검색 비용이 O(n)이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터베이스에서 특정 키의 값을 효율적으로 찾기 위해서는 다른 데이터 구조가 필요하다. 바로 &lt;strong&gt;색인(index)&lt;/strong&gt; 이다.
색인의 일반적인 개념은 어떤 부가적인 메타데이터를 유지하는 것이다.&lt;/p&gt;

&lt;p&gt;색인의 특징&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;기본 데이터(primary data)에서 파생된 추가적인 구조다.&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;/li&gt;
  &lt;li&gt;따라서 필요 이상으로 오버헤드를 발생시키지 않으면서 애플리케이션에 가장 큰 이익을 안겨주는 색인을 선택할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;해시-색인&quot;&gt;해시 색인&lt;/h3&gt;

&lt;p&gt;해시 색인은 키-값 쌍을 저장하는 가장 간단한 방법 중 하나이다. 해시 색인은 키를 해싱하여 해시 테이블에 저장한다.&lt;/p&gt;

&lt;p&gt;앞의 예제처럼 단순히 파일에 추가하는 방식으로 데이터 저장소를 구성한다고 가정해보자. 그러면 가장 간단하게 가능한
색인 전략은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;키를 데이터 파일의 바이트 오프셋에 매핑해 인메모리 해시 맵을 유지하는 전략이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;../assets/img/indexed-with-an-inmemory-hash-map.png&quot; alt=&quot;indexed-with-an-inmemory-hash-map.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 방식은 비트캐스크(Bitcask)와 같은 시스템에서 사용된다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;비트캐스크(append-only 로그 구조 기반 저장 방식)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;특징&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;쓰기 연산은 항상 파일 끝에 추가
→ 새로운 데이터나 수정된 데이터는 기존 데이터를 덮어쓰지 않고 파일 끝에 추가됨.&lt;/li&gt;
  &lt;li&gt;파일은 세그먼트로 나뉨
→ 일정 크기가 되면 새로운 세그먼트 파일로 전환됨.&lt;/li&gt;
  &lt;li&gt;데이터는 중복 가능
→ 동일한 키가 여러 세그먼트에 존재할 수 있으며, 최신 값은 가장 마지막에 위치.&lt;/li&gt;
  &lt;li&gt;인메모리 인덱스 사용
→ 메모리 상에 key → (세그먼트 파일, 위치) 형태의 인덱스를 유지함.&lt;/li&gt;
  &lt;li&gt;병합(compaction) 과정 존재
→ 오래된 세그먼트 파일을 주기적으로 병합하여 공간을 절약하고, 최신 값만 남김.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;장점&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;순차적 디스크 접근으로 인한 성능 최적화
→ HDD와 같은 디스크에서 랜덤 접근보다 순차 접근이 빠르기 때문에, 성능 상 이점.&lt;/li&gt;
  &lt;li&gt;쓰기 지연(latency)가 낮음
→ 덮어쓰기 없이 파일 끝에 추가만 하므로, 동시성 처리도 간단하고 빠름.&lt;/li&gt;
  &lt;li&gt;크래시 복구가 용이함
→ append-only 구조이므로 중간에 실패하더라도 손상 위험이 낮고, 복구 가능성이 높음.&lt;/li&gt;
  &lt;li&gt;쓰기 경합이 적음
→ 같은 파일의 끝에 쓰기만 하므로 락이나 동기화 비용이 적음.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;단점&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;읽기 성능 저하 가능성
→ 동일한 키가 여러 세그먼트에 존재할 경우, 최신 값을 찾기 위해 여러 세그먼트를 탐색해야 할 수 있음. (따라서 인덱스가 필수)&lt;/li&gt;
  &lt;li&gt;디스크 공간 낭비 발생
→ 삭제되거나 덮어쓰인 데이터도 여전히 파일에 존재하므로, 주기적 compaction이 필요.&lt;/li&gt;
  &lt;li&gt;인메모리 인덱스 의존
→ 모든 key의 인덱스를 메모리에 유지해야 하므로, 키 수가 많아지면 메모리 부족 문제 발생 가능.&lt;/li&gt;
  &lt;li&gt;compaction 비용
→ 세그먼트 병합(compaction)은 디스크 I/O와 CPU 자원을 많이 소모하는 작업이므로, 적절한 스케줄링이 필요.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;해시 색인은 다음의 단점이 있고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SS테이블과 LSM 트리&lt;/code&gt;를 사용하여 이를 해결한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;메모리에 저장해야 하므로 키가 너무 많으면 메모리 부족이 발생할 수 있다.&lt;/li&gt;
  &lt;li&gt;해시 색인은 키의 순서를 유지하지 않는다. 따라서 범위 쿼리(range query)를 지원하지 않는다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ss테이블과-lsm-트리&quot;&gt;SS테이블과 LSM 트리&lt;/h3&gt;

&lt;p&gt;위의 세그먼트 파일의 형식에서 키-값 쌍을 키로 정렬해보자. 이처럼 키로 정렬된 형식을
&lt;strong&gt;정렬된 문자열 테이블(Sorted String Table, SSTable)&lt;/strong&gt; 이라고 한다.&lt;/p&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;동일한 키가 여러 번 등장 가능함 → 나중에 쓴 값이 최신으로 간주됨.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SSTable 방식&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;이제 파일 내부의 키-값 쌍을 정렬된 상태로 저장함 → 이를 SSTable(Sorted String Table)이라고 부름.&lt;/li&gt;
  &lt;li&gt;그리고 각 파일(세그먼트) 안에는 같은 키가 한 번만 등장해야 함 → 이건 병합(compaction) 과정에서 이미 해결된 상태로 가정.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SS테이블은 해시 색인을 가진 로그 세그먼트보다 몇 가지 큰 장점이 있다.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;병합(compaction)이 간단하고 메모리가 적게 듦.&lt;/strong&gt; 여러 SSTable 파일을 병합할 때 Merge Sort 방식을 사용함
    &lt;ol&gt;
      &lt;li&gt;두 정렬된 파일에서 각자의 첫 키를 비교&lt;/li&gt;
      &lt;li&gt;작은 키를 출력 파일로 복사&lt;/li&gt;
      &lt;li&gt;다음 키 비교 … 반복
        &lt;ul&gt;
          &lt;li&gt;이 방식은 파일이 메모리보다 커도 병합이 가능함 (조금씩 읽기 때문)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
  &lt;p&gt;예:&lt;/p&gt;

  &lt;p&gt;sst1: [apple, banana, handle]&lt;/p&gt;

  &lt;p&gt;sst2: [handiwork, harvest, igloo]&lt;/p&gt;

  &lt;p&gt;→ 병합 결과: [apple, banana, handle, handiwork, harvest, igloo]&lt;/p&gt;

  &lt;p&gt;만약 두 파일에 “handle”이라는 키가 모두 있다면,
→ 더 최신 세그먼트 파일의 값을 유지하고, 이전 값은 폐기.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;메모리 인덱스가 작아도 된다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SSTable은 정렬되어 있기 때문에, 모든 키의 위치를 인덱스로 알 필요가 없음.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;예: “handiwork”를 찾고 싶은데 정확한 위치는 모르는 경우. (인덱스가 없음)&lt;/p&gt;

  &lt;p&gt;다만, “handbag”은 offset 1000, “handsome”은 offset 2000에 있는 상황.&lt;/p&gt;

  &lt;p&gt;→ 그럼 “handiwork”는 1000~2000 사이에 있을 거라고 추정 가능&lt;/p&gt;

  &lt;p&gt;이 덕분에 인메모리 인덱스를 일부 키만 포함한 희소 인덱스(sparse index)로 줄일 수 있음&lt;/p&gt;

  &lt;p&gt;→ 메모리 사용량 감소&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;SSTable &amp;amp; MemTable 동작 예시 정리&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;1단계-첫-번째-쓰기--sstable-a-생성&quot;&gt;1단계: 첫 번째 쓰기 &amp;amp; SSTable A 생성&lt;/h3&gt;

&lt;h4 id=&quot;쓰기-요청&quot;&gt;쓰기 요청&lt;/h4&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;banana → 300
apple → 100
cherry → 200
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;memtable-상태-flush-전&quot;&gt;MemTable 상태 (flush 전)&lt;/h4&gt;

&lt;table&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;apple&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;banana&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;sstable-상태&quot;&gt;SSTable 상태&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;없음 (아직 디스크에 저장되지 않음)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;이벤트-memtable이-꽉-참--sstable로-flush&quot;&gt;이벤트: MemTable이 꽉 참 → SSTable로 flush&lt;/h4&gt;

&lt;h4 id=&quot;sstable-a-생성&quot;&gt;SSTable A 생성&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;키&lt;/th&gt;
      &lt;th&gt;값&lt;/th&gt;
      &lt;th&gt;Offset&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;apple&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;banana&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;index-block-예시-희소-인덱스&quot;&gt;Index Block 예시 (희소 인덱스)&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;키&lt;/th&gt;
      &lt;th&gt;Offset&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;apple&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;결과-상태&quot;&gt;결과 상태&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;MemTable: 비어 있음&lt;/li&gt;
  &lt;li&gt;SSTable A: [apple, banana, cherry]&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;2단계-두-번째-쓰기--새로운-memtable-생성&quot;&gt;2단계: 두 번째 쓰기 &amp;amp; 새로운 MemTable 생성&lt;/h3&gt;

&lt;h4 id=&quot;쓰기-요청-1&quot;&gt;쓰기 요청&lt;/h4&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;banana → 350   (업데이트)
date → 400     (새로운 키)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;memtable-상태-현재&quot;&gt;MemTable 상태 (현재)&lt;/h4&gt;

&lt;table&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;banana&lt;/td&gt;
      &lt;td&gt;350&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;date&lt;/td&gt;
      &lt;td&gt;400&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;sstable-상태-변함없음&quot;&gt;SSTable 상태 (변함없음)&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;SSTable A: [apple, banana, cherry]&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;3단계-병합compaction&quot;&gt;3단계: 병합(compaction)&lt;/h3&gt;

&lt;h4 id=&quot;병합-로직&quot;&gt;병합 로직&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;키&lt;/th&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;apple&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
      &lt;td&gt;SSTable A 유일 값&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;banana&lt;/td&gt;
      &lt;td&gt;350&lt;/td&gt;
      &lt;td&gt;MemTable 값이 최신&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;SSTable A 유일 값&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;date&lt;/td&gt;
      &lt;td&gt;400&lt;/td&gt;
      &lt;td&gt;MemTable 유일 값&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;sstable-b-생성&quot;&gt;SSTable B 생성&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;키&lt;/th&gt;
      &lt;th&gt;값&lt;/th&gt;
      &lt;th&gt;Offset&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;apple&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;banana&lt;/td&gt;
      &lt;td&gt;350&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;date&lt;/td&gt;
      &lt;td&gt;400&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;index-block-예시-희소-인덱스-1&quot;&gt;Index Block 예시 (희소 인덱스)&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;키&lt;/th&gt;
      &lt;th&gt;Offset&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;apple&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cherry&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;병합-후-상태&quot;&gt;병합 후 상태&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;SSTable A: 삭제됨&lt;/li&gt;
  &lt;li&gt;MemTable: 비어 있음&lt;/li&gt;
  &lt;li&gt;SSTable B: [apple, banana, cherry, date]&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4단계-데이터-찾는-예시&quot;&gt;4단계: 데이터 찾는 예시&lt;/h3&gt;

&lt;h4 id=&quot;예시-a-date-찾기&quot;&gt;예시 A: “date” 찾기&lt;/h4&gt;
&lt;ol&gt;
  &lt;li&gt;MemTable → 없음&lt;/li&gt;
  &lt;li&gt;SSTable B 확인
    &lt;ul&gt;
      &lt;li&gt;Index block에서 “date”는 “cherry” 이후일 것으로 판단됨&lt;/li&gt;
      &lt;li&gt;Index block에는 “cherry”가 offset 200에 있음 → offset 200부터 순차적으로 스캔&lt;/li&gt;
      &lt;li&gt;“date” 발견&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;결과: “date” → 400&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;예시-b-apple-찾기&quot;&gt;예시 B: “apple” 찾기&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;MemTable → 없음&lt;/li&gt;
  &lt;li&gt;SSTable B 확인
    &lt;ul&gt;
      &lt;li&gt;Index block에 정확히 있음 → offset 0&lt;/li&gt;
      &lt;li&gt;즉시 접근 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;결과: “apple” → 100&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;전체-구조-정리-병합-후&quot;&gt;전체 구조 정리 (병합 후)&lt;/h4&gt;

&lt;table&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;MemTable&lt;/td&gt;
      &lt;td&gt;없음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SSTable B&lt;/td&gt;
      &lt;td&gt;apple, banana, cherry, date&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;최종-요약&quot;&gt;최종 요약&lt;/h4&gt;

&lt;table&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;1단계&lt;/td&gt;
      &lt;td&gt;MemTable에 데이터 쓰기 → SSTable A로 flush&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2단계&lt;/td&gt;
      &lt;td&gt;새로운 데이터 쓰기 → 새로운 MemTable 생성&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3단계&lt;/td&gt;
      &lt;td&gt;SSTable A와 MemTable 병합 → SSTable B 생성&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4단계&lt;/td&gt;
      &lt;td&gt;SSTable B에서 인덱스 기반으로 효율적으로 탐색&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;b-트리&quot;&gt;B 트리&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;균형 잡힌 트리 구조&lt;/strong&gt;로, 디스크 기반 시스템에서 널리 사용되는 인덱스 구조&lt;/li&gt;
  &lt;li&gt;각 노드는 여러 개의 키와 자식 포인터를 가짐&lt;/li&gt;
  &lt;li&gt;트리의 높이를 작게 유지하여 디스크 접근 횟수를 최소화함&lt;/li&gt;
  &lt;li&gt;데이터는 &lt;strong&gt;정렬된 순서&lt;/strong&gt;로 저장되어 범위 검색이 빠름&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;B-Tree의 구성 요소&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;내부 노드 (Internal nodes)&lt;/strong&gt;: 키와 자식 노드 포인터를 포함&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;리프 노드 (Leaf nodes)&lt;/strong&gt;: 실제 키-값 쌍 저장, 보통 디스크 페이지와 1:1 대응&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;디스크 페이지 단위로 구성되며&lt;/strong&gt;, 한 페이지는 보통 4KB~8KB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;디스크 I/O와 관련된 최적화&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;디스크 접근은 느리므로, &lt;strong&gt;트리의 높이를 최소화&lt;/strong&gt;해야 함&lt;/li&gt;
  &lt;li&gt;이를 위해 각 노드(페이지)는 &lt;strong&gt;수백 개의 키를 가질 수 있음&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;분기 계수(branching factor)&lt;/strong&gt;: 한 페이지에서 하위 페이지를 참조하는 수&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;이로 인해 B-Tree는 &lt;strong&gt;log(N)&lt;/strong&gt;의 시간 복잡도로 검색 가능하며, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt;이 매우 큼&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;쓰기 동작&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;새로운 키를 삽입할 때, 정렬 순서에 따라 리프 노드에 삽입&lt;/li&gt;
  &lt;li&gt;노드가 가득 차면 &lt;strong&gt;분할(split)&lt;/strong&gt; 발생 → 상위 노드에 키 승격&lt;/li&gt;
  &lt;li&gt;삽입은 트리 구조에 따라 재귀적으로 진행됨&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;장점&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;읽기 효율이 매우 뛰어남&lt;/strong&gt;: 정렬 + 낮은 트리 높이&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;범위 쿼리(RANGE QUERY)&lt;/strong&gt;에 매우 적합&lt;/li&gt;
  &lt;li&gt;단일 키 조회도 빠름&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;단점&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;쓰기 작업 시 디스크 임의 접근(random write)&lt;/strong&gt;이 발생&lt;/li&gt;
  &lt;li&gt;많은 업데이트는 &lt;strong&gt;페이지 분할(split), 병합(merge)&lt;/strong&gt; 등을 유발하여 성능 저하 가능&lt;/li&gt;
  &lt;li&gt;즉, &lt;strong&gt;쓰기 성능은 LSM-Tree에 비해 낮음&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;b-트리와-lsm-트리-비교&quot;&gt;B 트리와 LSM 트리 비교&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;B-Tree&lt;/th&gt;
      &lt;th&gt;LSM-Tree&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;데이터 정렬&lt;/td&gt;
      &lt;td&gt;노드 간 정렬 유지&lt;/td&gt;
      &lt;td&gt;SSTable별 정렬 + 병합 시 정렬 유지&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;디스크 접근&lt;/td&gt;
      &lt;td&gt;랜덤 읽기/쓰기 포함&lt;/td&gt;
      &lt;td&gt;대부분 순차 쓰기&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;쓰기 성능&lt;/td&gt;
      &lt;td&gt;낮음 (분할/병합 오버헤드 있음)&lt;/td&gt;
      &lt;td&gt;높음 (append-only 구조)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;읽기 성능&lt;/td&gt;
      &lt;td&gt;빠름 (낮은 트리 높이 덕분)&lt;/td&gt;
      &lt;td&gt;상대적으로 느림 (다중 SSTable 조회 필요)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;범위 쿼리&lt;/td&gt;
      &lt;td&gt;매우 효율적&lt;/td&gt;
      &lt;td&gt;효율적이지만 병합 상태에 따라 다름&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;advantages-of-lsm-trees&quot;&gt;Advantages of LSM-trees&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;High Write Throughput&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;LSM-트리는 디스크에 &lt;strong&gt;순차적으로 데이터 쓰기&lt;/strong&gt;가 가능하다.&lt;/li&gt;
      &lt;li&gt;이는 &lt;strong&gt;랜덤 쓰기 비용이 큰 디스크 환경에서 매우 효과적&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Efficient Use of Disk Bandwidth&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;LSM-트리는 쓰기 연산을 메모리에 저장한 뒤, 배치(batch)로 디스크에 기록한다.&lt;/li&gt;
      &lt;li&gt;디스크 I/O를 &lt;strong&gt;큰 블록 단위로 사용할 수 있어 효율적&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compaction Enables Better Compression&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터는 정렬된 상태로 저장되므로, &lt;strong&gt;압축률이 높아지고 저장 공간도 절약&lt;/strong&gt;된다.&lt;/li&gt;
      &lt;li&gt;특히 범위가 비슷한 값들이 함께 저장되기 때문에 &lt;strong&gt;block-level compression에 적합&lt;/strong&gt;하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;downsides-of-lsm-trees&quot;&gt;Downsides of LSM-trees&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;High Write Amplification&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;컴팩션(compaction) 과정에서 같은 데이터를 여러 번 복사하게 되므로,&lt;br /&gt;
&lt;strong&gt;쓰기 증폭(write amplification)&lt;/strong&gt;이 발생할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;High Read Amplification&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 키를 찾기 위해 여러 SSTable을 조회해야 할 수 있다.&lt;/li&gt;
      &lt;li&gt;이로 인해 &lt;strong&gt;읽기 성능이 떨어질 수 있으며&lt;/strong&gt;, 이는 특히 존재하지 않는 키를 조회할 때 문제가 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compaction Overhead&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;컴팩션은 시스템 자원을 많이 소모하며,&lt;br /&gt;
&lt;strong&gt;읽기 및 쓰기 성능을 일시적으로 저하시킬 수 있다&lt;/strong&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stale Data May Persist Longer&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;삭제된 데이터는 즉시 사라지지 않고 tombstone으로 남는다.&lt;/li&gt;
      &lt;li&gt;이 데이터는 나중에야 제거되므로, &lt;strong&gt;공간이 즉시 회수되지 않는다&lt;/strong&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;트랜잭션-처리나-분석&quot;&gt;트랜잭션 처리나 분석?&lt;/h2&gt;

&lt;p&gt;트랜잭션 처리(transaction processing, OLTP)와 분석 쿼리(analytics, OLAP)의 구분&lt;/p&gt;

&lt;p&gt;현대의 데이터 시스템은 크게 두 가지 목적 중 하나를 중심으로 설계된다:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;온라인 트랜잭션 처리(online transaction processing, OLTP)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주로 사용자의 상호작용에 반응하는 시스템&lt;/li&gt;
      &lt;li&gt;예: 전자상거래, 뱅킹, SNS의 상태 업데이트&lt;/li&gt;
      &lt;li&gt;데이터베이스는 &lt;strong&gt;짧고 빈번한 쓰기 작업&lt;/strong&gt;을 효율적으로 처리해야 한다&lt;/li&gt;
      &lt;li&gt;일반적으로 데이터는 &lt;strong&gt;행(row) 단위&lt;/strong&gt;로 조직되며, 많은 작은 쓰기가 발생&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;온라인 분석 처리(online analytical processing, OLAP)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주로 관리자나 데이터 분석가의 질문(query)에 응답하는 용도&lt;/li&gt;
      &lt;li&gt;예: “이번 달에 가장 많이 팔린 상품은 무엇인가?”&lt;/li&gt;
      &lt;li&gt;시스템은 &lt;strong&gt;대량의 데이터를 스캔하고 집계&lt;/strong&gt;하는 데 최적화되어야 함&lt;/li&gt;
      &lt;li&gt;데이터는 &lt;strong&gt;열(column) 단위&lt;/strong&gt;로 저장될 때가 많고, &lt;strong&gt;읽기 최적화&lt;/strong&gt;가 중요함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;설계 목표의 차이점&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;OLTP (트랜잭션 처리)&lt;/th&gt;
      &lt;th&gt;OLAP (분석 쿼리)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;주요 작업 유형&lt;/td&gt;
      &lt;td&gt;쓰기 중심, 짧은 읽기&lt;/td&gt;
      &lt;td&gt;대규모 읽기, 복잡한 집계&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;데이터 액세스&lt;/td&gt;
      &lt;td&gt;행 단위(row-oriented)&lt;/td&gt;
      &lt;td&gt;열 단위(column-oriented)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;사용 예&lt;/td&gt;
      &lt;td&gt;주문, 계좌 이체&lt;/td&gt;
      &lt;td&gt;매출 보고서, 사용자 행동 분석&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;최적화 방향&lt;/td&gt;
      &lt;td&gt;빠른 삽입/갱신&lt;/td&gt;
      &lt;td&gt;빠른 읽기/스캔&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;통합 처리 시 고려사항&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;OLTP와 OLAP은 &lt;strong&gt;상반된 성능 특성&lt;/strong&gt;을 요구하기 때문에,&lt;br /&gt;
하나의 시스템에서 두 요구사항을 모두 만족시키기는 어렵다.&lt;/li&gt;
  &lt;li&gt;따라서 많은 시스템은 &lt;strong&gt;서로 다른 저장소를 구성&lt;/strong&gt;하거나,&lt;br /&gt;
ETL(extract-transform-load) 과정을 통해 &lt;strong&gt;OLTP 데이터를 OLAP 시스템으로 복사&lt;/strong&gt;하는 방식을 채택함.&lt;/li&gt;
  &lt;li&gt;최근에는 &lt;strong&gt;HTAP(hybrid transactional/analytical processing)&lt;/strong&gt; 시스템도 등장하여,&lt;br /&gt;
OLTP와 OLAP의 경계를 줄이려는 시도가 이루어지고 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;칼럼-지향-저장소&quot;&gt;칼럼 지향 저장소&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;일반적인 데이터베이스는 &lt;strong&gt;행(row)-지향 저장(row-oriented storage)&lt;/strong&gt;을 사용한다.
    &lt;ul&gt;
      &lt;li&gt;즉, 한 행의 모든 필드를 함께 디스크에 저장함&lt;/li&gt;
      &lt;li&gt;예: 이름, 이메일, 생년월일이 같은 위치에 저장됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;반면, &lt;strong&gt;컬럼 지향 저장(column-oriented storage)&lt;/strong&gt;은 각 컬럼을 &lt;strong&gt;별도의 파일이나 블록&lt;/strong&gt;에 저장함
    &lt;ul&gt;
      &lt;li&gt;예: 이름만 따로, 이메일만 따로, 생년월일도 따로 저장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OLAP(온라인 분석 처리, analytics)&lt;/strong&gt;에서는 주로 &lt;strong&gt;몇 개의 컬럼만 조회&lt;/strong&gt;하는 경우가 많음
    &lt;ul&gt;
      &lt;li&gt;예: “부서별 평균 급여” → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;department&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;salary&lt;/code&gt; 컬럼만 필요&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;이때 행 기반 저장은 필요 없는 데이터까지 함께 읽게 되어 &lt;strong&gt;디스크 I/O 낭비&lt;/strong&gt; 발생&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;컬럼 저장 방식은:
    &lt;ul&gt;
      &lt;li&gt;필요한 컬럼만 읽음 → &lt;strong&gt;I/O 최소화&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;같은 컬럼 값들이 연속적으로 저장 → &lt;strong&gt;압축 효율 향상&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;예시 비교&lt;/p&gt;

&lt;table&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;name, email, birth / name, email, birth / …&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;열 지향&lt;/td&gt;
      &lt;td&gt;name, name, name / email, email, email / birth, birth, birth&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;→ 열 지향 방식에서는 &lt;strong&gt;동일한 컬럼이 반복되므로 압축률이 높고, 벡터 연산도 최적화 가능&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;컬럼 저장의 장점&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;읽기 성능 최적화&lt;/strong&gt;: 필요한 컬럼만 로드 가능&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;압축 효율 우수&lt;/strong&gt;: 같은 유형의 값들이 연속 저장&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;벡터화 처리&lt;/strong&gt;에 유리: CPU 캐시에 잘 맞으며 연산 성능 향상&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;단점&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;쓰기 작업이 느릴 수 있음&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;행 전체를 삽입해야 할 때, 각 컬럼 파일을 따로 갱신해야 하므로 비효율적&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OLTP(트랜잭션 처리)에는 부적합&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;빠른 삽입/갱신에는 부적절 → 주로 읽기 중심의 분석 시스템에 적합&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;비교&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;행 지향(row-oriented)&lt;/th&gt;
      &lt;th&gt;열 지향(column-oriented)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;용도&lt;/td&gt;
      &lt;td&gt;OLTP (쓰기 중심)&lt;/td&gt;
      &lt;td&gt;OLAP (읽기 중심)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;저장 방식&lt;/td&gt;
      &lt;td&gt;한 행 전체를 함께 저장&lt;/td&gt;
      &lt;td&gt;컬럼 단위로 따로 저장&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;I/O 효율&lt;/td&gt;
      &lt;td&gt;낮음 (불필요한 필드까지 읽음)&lt;/td&gt;
      &lt;td&gt;높음 (필요한 컬럼만 읽음)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;압축률&lt;/td&gt;
      &lt;td&gt;낮음&lt;/td&gt;
      &lt;td&gt;높음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;쓰기 성능&lt;/td&gt;
      &lt;td&gt;빠름&lt;/td&gt;
      &lt;td&gt;느릴 수 있음&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-engineering" /><summary type="html">3장 저장소와 검색 데이터베이스를 강력하게 만드는 데이터 구조 해시 색인 SS테이블과 LSM 트리 1단계: 첫 번째 쓰기 &amp;amp; SSTable A 생성 쓰기 요청 MemTable 상태 (flush 전) SSTable 상태 이벤트: MemTable이 꽉 참 → SSTable로 flush SSTable A 생성 Index Block 예시 (희소 인덱스) 결과 상태 2단계: 두 번째 쓰기 &amp;amp; 새로운 MemTable 생성 쓰기 요청 MemTable 상태 (현재) SSTable 상태 (변함없음) 3단계: 병합(compaction) 병합 로직 SSTable B 생성 Index Block 예시 (희소 인덱스) 병합 후 상태 4단계: 데이터 찾는 예시 예시 A: “date” 찾기 예시 B: “apple” 찾기 전체 구조 정리 (병합 후) 최종 요약 B 트리 B 트리와 LSM 트리 비교 Advantages of LSM-trees Downsides of LSM-trees 트랜잭션 처리나 분석? 칼럼 지향 저장소</summary></entry><entry><title type="html">2장 데이터 모델과 질의 언어</title><link href="https://jangjichang.github.io/posts/data-models-and-query-languages-chapter2" rel="alternate" type="text/html" title="2장 데이터 모델과 질의 언어" /><published>2025-05-22T00:00:00+09:00</published><updated>2025-05-22T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/Data-Models-and-Query-Languages-chapter2</id><content type="html" xml:base="https://jangjichang.github.io/posts/data-models-and-query-languages-chapter2">&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#2장-데이터-모델과-질의-언어&quot;&gt;2장 데이터 모델과 질의 언어&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#관계형-모델과-문서-모델&quot;&gt;관계형 모델과 문서 모델&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#nosql의-탄생&quot;&gt;NoSQL의 탄생&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#객체-관계형-불일치&quot;&gt;객체 관계형 불일치&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#다대일과-다대다-관계&quot;&gt;다대일과 다대다 관계&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#문서-데이터베이스는-역사를-반복하고-있나&quot;&gt;문서 데이터베이스는 역사를 반복하고 있나?&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#네트워크-모델&quot;&gt;네트워크 모델&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#관계형-모델&quot;&gt;관계형 모델&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#문서-데이터베이스와의-비교&quot;&gt;문서 데이터베이스와의 비교&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#관계형-데이터베이스와-오늘날의-문서-데이터베이스&quot;&gt;관계형 데이터베이스와 오늘날의 문서 데이터베이스&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#어떤-데이터-모델이-애플리케이션-코드를-더-간단하게-할까&quot;&gt;어떤 데이터 모델이 애플리케이션 코드를 더 간단하게 할까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#문서-모델에서의-스키마-유연성&quot;&gt;문서 모델에서의 스키마 유연성&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#질의를-위한-데이터-지역성&quot;&gt;질의를 위한 데이터 지역성&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#문서-데이터베이스와-관계형-데이터베이스의-통합&quot;&gt;문서 데이터베이스와 관계형 데이터베이스의 통합&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#데이터를-위한-질의-언어&quot;&gt;데이터를 위한 질의 언어&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#웹에서의-선언형-질의&quot;&gt;웹에서의 선언형 질의&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#맵리듀스-질의&quot;&gt;맵리듀스 질의&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#그래프형-데이터-모델&quot;&gt;그래프형 데이터 모델&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#속성-그래프&quot;&gt;속성 그래프&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#사이퍼-질의-언어&quot;&gt;사이퍼 질의 언어&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#sql의-그래프-질의&quot;&gt;SQL의 그래프 질의&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#트리플-저장소와-스파클&quot;&gt;트리플 저장소와 스파클&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#시맨틱-웹&quot;&gt;시맨틱 웹&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#rdf-데이터-모델&quot;&gt;RDF 데이터 모델&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#스파클-질의-언어&quot;&gt;스파클 질의 언어&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#초석-데이터로그&quot;&gt;초석: 데이터로그&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터 모델은 중요하다. 왜냐하면 소프트웨어가 어떻게 작성됐는지 뿐만 아니라 해결하려는 &lt;strong&gt;문제를 어떻게 생각해야 하는지&lt;/strong&gt;에
대해서도 지대한 영향을 미치기 때문이다.&lt;/p&gt;

&lt;p&gt;대부분의 애플리케이션은 하나의 데이터 모델을 다른 데이터 모델 위에 계층을 둬서 만든다. 각 계층의 핵심적인 문제는 다음 하위 계층 관점에서
데이터 모델을 &lt;strong&gt;표현&lt;/strong&gt;하는 방법이다. 예를 들어보자.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;애플리케이션 개발자는 현실(사람, 조직, 상품 등)을 보고 객체나 데이터 구조, 그리고 이러한 데이터 구조를 다루는 API를 모델링한다.
이런 구조는 보통 애플리케이션에 특화돼 있다.&lt;/li&gt;
  &lt;li&gt;데이터 구조를 저장할 때는 JSON이나 XML 문서, 관계형 데이터베이스 테이블이나 그래프 모델 같은 범용 데이터 모델로 표현한다.&lt;/li&gt;
  &lt;li&gt;데이터베이스 소프트웨어를 개발하는 엔지니어는 JSON / XML / 관계형 / 그래프 데이터를 메모리나 디스크 또는
네트워크 상의 바이트 단위로 표현하는 방법을 결정한다. 이 표현은 다양한 방법으로 데이터를 질의, 탐색, 조작, 처리할 수 있게 한다.&lt;/li&gt;
  &lt;li&gt;더 낮은 수준에서 하드웨어 엔지니어는 전류, 빛의 파동, 자기장 등의 관점에서 바이트를 표현하는 방법을 알아냈다.&lt;/li&gt;
&lt;/ul&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;계층&lt;/th&gt;
      &lt;th&gt;주체&lt;/th&gt;
      &lt;th&gt;관찰대상&lt;/th&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;사용자 / 기획자&lt;/td&gt;
      &lt;td&gt;현실(사람, 조직, 상품 등)&lt;/td&gt;
      &lt;td&gt;현실 개념&lt;/td&gt;
      &lt;td&gt;실제 현상을 추상화하고 시스템화&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;애플리케이션&lt;/td&gt;
      &lt;td&gt;애플리케이션 개발자&lt;/td&gt;
      &lt;td&gt;객체, 데이터 구조&lt;/td&gt;
      &lt;td&gt;API&lt;/td&gt;
      &lt;td&gt;기능 구현을 위한 추상화된 데이터 표현&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;저장 계층&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;서버 개발자&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;구조화된 데이터&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;JSON, XML 문서, 관계형 데이터베이스 테이블, 그래프 모델&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;범용 데이터 모델로 표현&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;DB 내부 표현 계층&lt;/td&gt;
      &lt;td&gt;DBMS 개발자&lt;/td&gt;
      &lt;td&gt;JSON, XML, 관계형, 그래프 데이터&lt;/td&gt;
      &lt;td&gt;메모리, 디스크 또는 네트워크 상의 바이트 단위로 표현&lt;/td&gt;
      &lt;td&gt;다양한 방법으로 데이터를 질의, 탐색, 조작 처리 가능하도록 설계 해야함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;하드웨어&lt;/td&gt;
      &lt;td&gt;하드웨어 엔지니어&lt;/td&gt;
      &lt;td&gt;바이트&lt;/td&gt;
      &lt;td&gt;전류, 빛의 파동, 자기장&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;이번 장에서는 데이터 저장과 질의(표의 &lt;strong&gt;저장 계층&lt;/strong&gt; 항목)를 위한 다양한 범용 데이터 모델을 살펴본다.
특히 relational model과 document model, 그리고 몇 가지 graph-based data model을 비교한다.
또한 다양한 질의 언어를 살펴보고 사용 사례도 비교한다.&lt;/p&gt;

&lt;h2 id=&quot;관계형-모델과-문서-모델&quot;&gt;관계형 모델과 문서 모델&lt;/h2&gt;

&lt;p&gt;가장 잘 알려진 모델은 관계형 모델을 기반으로 한 SQL이다. 데이터는 &lt;strong&gt;관계(relation (sql에서 테이블))&lt;/strong&gt; 로 구성되고
각 관계는 순서 없는 &lt;strong&gt;튜플(tuple) (sql에서 row))&lt;/strong&gt; 모음이다.&lt;/p&gt;

&lt;p&gt;관계형 모델은 이론적 제안이었음. 이론대로 구현할 수 있는지 의문이었지만 1980년대 중반에 RDBMS와 SQL은
정규화된 구조로 데이터를 저장하고 질의할 필요가 있는 사람들 대부분이 선택하는 도구가 되었음.&lt;/p&gt;

&lt;p&gt;관계형 데이터베이스의 근원은 1960년대와 70년대에 메인프레임 컴퓨터에서 수행된 &lt;strong&gt;비즈니스 데이터 처리&lt;/strong&gt;에 있다.
이 사용 사례는 보통 &lt;strong&gt;트랜잭션 처리&lt;/strong&gt;(영업이나 은행 거래, 항공 예약, 창고에 재고 보관)와 &lt;strong&gt;일괄 처리&lt;/strong&gt;
(고객 송장 작성, 급여 지불, 보고)로 오늘날의 관점에서는 일상적으로 수행되는 일이다.&lt;/p&gt;

&lt;h3 id=&quot;nosql의-탄생&quot;&gt;NoSQL의 탄생&lt;/h3&gt;

&lt;p&gt;2010년대에 &lt;strong&gt;NoSQL&lt;/strong&gt;은 관계형 모델의 우위를 뒤집으려는 가장 최신 시도다.
NOSQL 데이터베이스 채택한 데는 다음과 같은 다양한 원동력이 있다.&lt;/p&gt;
&lt;ul&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;/ul&gt;

&lt;p&gt;가까운 미래에는 관계형 데이터베이스가 폭넓은 다양함을 가진 비관계형 데이터스토어와 함께 사용될 것이다.
이런 개념을 종종 &lt;strong&gt;다중 저장소 지속성(polyglot persistence)&lt;/strong&gt; 이라고 부른다.&lt;/p&gt;

&lt;h3 id=&quot;객체-관계형-불일치&quot;&gt;객체 관계형 불일치&lt;/h3&gt;

&lt;p&gt;오늘날 대부분의 애플리케이션은 객체지향 프로그래밍 언어로 개발한다. 데이터를 관계형 테이블에 저장하려면
애플리케이션 코드와 데이터 베이스 모델 객체(테이블, 로우, 칼럼) 사이에 거추장스러운 전환 계층이 필요하다.
이런 모델 사이의 분리를 종종 &lt;strong&gt;임피던스 불일치(impedance mismatch)&lt;/strong&gt; 라고 부른다.&lt;/p&gt;

&lt;p&gt;액티브레코드나 하이버네이트 같은 객체 관계형 매핑(ORM) 라이브러리는 전환 계층에 필요한 상용구 코드의 양을
줄이지만 두 모델 간의 차이를 완벽히 숨길 수 없다.&lt;/p&gt;

&lt;p&gt;링크드인 이력서 저장을 위한 데이터 모델을 생각해보자. 경력에 넣을 직업이 하나 이상이며 학력 기간과 연락처 정보도
다양하다. 사용자와 이들 항목은 일대다(one-to-many) 관계를 가진다.&lt;/p&gt;

&lt;p&gt;이력서 같은 데이터 구조는 모든 내용을 갖추고 있는 문서라서 JSON 표현에 매우 적합하다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;예제 2-1&lt;/strong&gt;. JSON 문서로 표현한 링크드인 프로필&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;251&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;first_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bill&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;last_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Gates&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;summary&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Co-chair of the Bill &amp;amp; Melinda Gates Foundation&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;region_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;us:91&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;industry_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;131&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;photo_url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/p/7/000/253/05b/308dd6e.jpg&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;positions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;job_title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Co-chair&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;organization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bill &amp;amp; Melinda Gates Foundation&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;job_title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Co-founder, Chairman&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;organization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Microsoft&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;education&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;school_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Harvard University&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1973&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1975&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;school_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Lakeside School, Seattle&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contact_info&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;blog&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://thegatesnotes.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;twitter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://twitter.com/BillGates&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; 
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;JSON 표현은 다중 테이블 스키마보다 더 나은 &lt;strong&gt;지역성(locality)&lt;/strong&gt; 을 갖는다. 관계형 예제에서 프로필을
가져오려면 다중 질의(각 테이블에 user_id로 질의)를 수행하거나 users 테이블과 그 하위 테이블 간에 난잡한
다중 조인을 수행해야 한다.&lt;/p&gt;

&lt;p&gt;하지만 JSON 표현에서는 모든 관련 정보가 한 곳에 있어 질의 하나로 충분하다.&lt;/p&gt;

&lt;h3 id=&quot;다대일과-다대다-관계&quot;&gt;다대일과 다대다 관계&lt;/h3&gt;

&lt;p&gt;예제 2-1에서 regin_id, industry_id는 평문인 “그레이터 시애틀 구역”과 “자선활동”이 아니라 ID로 주어졌다.
자유 텍스트 필드가 있다면 평문으로 저장하는 편이 합리적이지만
지리적 지역과 업계의 표준 목록으로 드롭다운 리스트나 자동 완성 기능을 만들어 사용자가 선택하게 하는 데는 다음과 같은 장점이 있다.&lt;/p&gt;

&lt;ul&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;li&gt;더 나은 검색. 예를 들어 워싱턴 주에 있는 자선가를 검색하려 할 때 지역 목록에 시애틀이 워싱턴에 있다는 사실을
부호화(“그레이터 시애틀 구역” 문자열로는 “워싱턴”을 식별하지 못함) 할 수 있기 때문에 원하는 프로필을 찾을 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;중복된 데이터를 정규화하려면 다대일(many-to-one) 관계가 필요하다.
다대일: 많은 사람들은 한 특정 지역에 살고 많은 사람들은 한 특정 업계에서 일한다.
관계형 데이터베이스에서는 조인이 쉽기 때문에 ID로 다른 테이블의 로우를 참조하는 방식은 일반적이다.
하지만 문서 데이터베이스에서는 일대다 트리 구조를 위해 조인이 필요하지 않지만 조인에 대한 지원이 보통 약하다.&lt;/p&gt;

&lt;ul&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;h3 id=&quot;문서-데이터베이스는-역사를-반복하고-있나&quot;&gt;문서 데이터베이스는 역사를 반복하고 있나?&lt;/h3&gt;

&lt;h4 id=&quot;네트워크-모델&quot;&gt;네트워크 모델&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;1960~70년대의 IMS 시스템과 CODASYL 모델은 데이터를 중첩된 트리 형태로 저장했으며, 포인터 기반의 명시적 탐색 경로가 필요했음.&lt;/li&gt;
  &lt;li&gt;다대다 관계를 표현하기 어렵고, 경로를 바꾸면 전체 코드 수정이 필요했기 때문에 유연하지 못했음.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;관계형-모델&quot;&gt;관계형 모델&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;관계형 모델은 테이블 기반으로 데이터를 납작하게(flat) 표현하며, 쿼리 최적화기를 통해 다양한 질의가 가능하게 설계됨.&lt;/li&gt;
  &lt;li&gt;접근 경로를 명시할 필요 없이 자유롭게 데이터를 참조할 수 있음.&lt;/li&gt;
  &lt;li&gt;쿼리 최적화기 하나만 잘 만들면 여러 응용이 이를 활용 가능하다는 점에서 큰 장점이 있음.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;문서-데이터베이스와의-비교&quot;&gt;문서 데이터베이스와의 비교&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;문서 모델은 1:N 트리 구조에 유리하고, 전체 문서를 한 번에 읽는 데 최적화되어 있음.&lt;/li&gt;
  &lt;li&gt;다대다 관계는 문서 모델에서 비효율적이며, 중복 저장, 데이터 불일치 위험, 애플리케이션 로직 복잡화가 발생함.&lt;/li&gt;
  &lt;li&gt;참조 ID로 관계를 표현할 경우, 문서 간 조인 지원이 부족해 별도 쿼리 또는 코드에서 처리해야 함.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;관계형-데이터베이스와-오늘날의-문서-데이터베이스&quot;&gt;관계형 데이터베이스와 오늘날의 문서 데이터베이스&lt;/h3&gt;

&lt;h4 id=&quot;어떤-데이터-모델이-애플리케이션-코드를-더-간단하게-할까&quot;&gt;어떤 데이터 모델이 애플리케이션 코드를 더 간단하게 할까?&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;트리 구조로 데이터가 구성되어 있고 한 번에 로딩되는 경우 → 문서 모델이 단순하고 효과적.&lt;/li&gt;
  &lt;li&gt;문서를 잘게 나누어 여러 테이블로 저장하고 조인하는 관계형 모델은 유지보수 비용이 증가할 수 있음.&lt;/li&gt;
  &lt;li&gt;다대다 관계가 존재하거나, 동일한 개체를 참조하는 경우 → 관계형 모델이 더 적합.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;문서-모델에서의-스키마-유연성&quot;&gt;문서 모델에서의 스키마 유연성&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;문서 DB는 스키마를 강제하지 않음 → 스키마 온 리드(schema-on-read)&lt;/li&gt;
  &lt;li&gt;명시적 스키마가 없더라도 대부분의 코드에서 암묵적으로 구조를 가정하므로, 실질적 구조는 존재함&lt;/li&gt;
  &lt;li&gt;유연성은 높지만, 일관성 검사나 데이터 검증은 애플리케이션 수준에서 처리해야 함&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;질의를-위한-데이터-지역성&quot;&gt;질의를 위한 데이터 지역성&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;문서는 JSON 또는 바이너리로 연속 저장됨 → 전체 문서 접근 시 빠름&lt;/li&gt;
  &lt;li&gt;반면 문서 크기가 클 경우, 부분만 접근하더라도 전체 문서를 로드해야 해 비효율적&lt;/li&gt;
  &lt;li&gt;문서의 크기를 작게 유지해야 성능상 이점이 있음&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;문서-데이터베이스와-관계형-데이터베이스의-통합&quot;&gt;문서 데이터베이스와 관계형 데이터베이스의 통합&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;PostgreSQL, MySQL, DB2 등은 JSON, XML 데이터 타입과 쿼리 기능을 지원함&lt;/li&gt;
  &lt;li&gt;MongoDB 등은 일부 조인 기능(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lookup&lt;/code&gt;)을 도입함&lt;/li&gt;
  &lt;li&gt;양쪽 모두 서로의 장점을 받아들이는 형태로 진화 중 → 하이브리드 모델 가능성 확대&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;데이터를-위한-질의-언어&quot;&gt;데이터를 위한 질의 언어&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;이 절에서는 데이터를 질의하는 다양한 방식, 특히 선언형 언어와 명령형 언어의 차이, 그리고 MapReduce 방식의 질의 처리 방식에 대해 설명합니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;웹에서의-선언형-질의&quot;&gt;웹에서의 선언형 질의&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SQL&lt;/strong&gt;은 대표적인 &lt;strong&gt;선언형(Declarative) 질의 언어&lt;/strong&gt;입니다.
    &lt;ul&gt;
      &lt;li&gt;사용자는 “무엇을 원하는지”만 명시합니다.&lt;/li&gt;
      &lt;li&gt;“어떻게 얻을 것인지”는 데이터베이스 쿼리 최적화기가 결정합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;이는 &lt;strong&gt;CSS, XPath, XSL&lt;/strong&gt; 등 웹 기술의 선언형 스타일과 유사합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;예시-css-vs-javascript&quot;&gt;예시: CSS vs JavaScript&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;선언형(CSS):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.selected&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;blue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;명령형(JavaScript):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;liElements&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementsByTagName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;liElements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;liElements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;className&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;selected&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;children&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;liElements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;childNodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;child&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;child&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nodeType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ELEMENT_NODE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;child&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tagName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;child&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;background-color: blue&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;선언형 스타일은 간결하며, 구조적 변화에도 자동 적용되어 유지보수가 쉬움.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;선언형 언어의 이점&lt;/strong&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;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;맵리듀스-질의&quot;&gt;맵리듀스 질의&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;MapReduce&lt;/strong&gt;는 Google이 제안한 대규모 분산 데이터 처리 모델입니다.&lt;/li&gt;
  &lt;li&gt;MongoDB, CouchDB 등의 NoSQL 시스템은 제한된 형태의 MapReduce 기능을 제공함.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;기본-개념&quot;&gt;기본 개념&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map&lt;/code&gt;: 문서 하나하나를 처리하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(key, value)&lt;/code&gt; 쌍을 생성&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reduce&lt;/code&gt;: 같은 key를 가진 value들을 집계&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;예시-mongodb&quot;&gt;예시 (MongoDB):&lt;/h4&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mapReduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observationTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getFullYear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observationTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getMonth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;year&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;numAnimals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;family&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Sharks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;monthlySharkReport&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;관찰된 상어의 수를 월별로 집계하는 예시&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map()&lt;/code&gt; 함수에서 key는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;1995-12&quot;&lt;/code&gt; 형식, value는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numAnimals&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reduce()&lt;/code&gt;는 같은 달의 value들을 합산&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;특징-및-한계&quot;&gt;특징 및 한계&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;순수 함수&lt;/strong&gt; 요구: 외부 상태에 의존하지 않아야 함 → 병렬 처리 가능&lt;/li&gt;
  &lt;li&gt;쿼리 최적화 어려움 → SQL보다 사용자 코드 의존도가 높음&lt;/li&gt;
  &lt;li&gt;MongoDB는 이를 보완하기 위해 &lt;strong&gt;Aggregation Pipeline&lt;/strong&gt; 도입&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;aggregate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;$match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;family&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Sharks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;$group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;$year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$observationTimestamp&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;$month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$observationTimestamp&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;totalAnimals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;$sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;$numAnimals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;SQL 유사한 JSON 스타일의 선언형 질의 구조&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
  &lt;p&gt;선언형 언어는 간결성과 유지보수성, 최적화 가능성에서 강점을 가지며,&lt;/p&gt;

  &lt;p&gt;MapReduce는 고급 쿼리의 분산 처리에 유리하나, 사용 난이도와 최적화 측면에서 한계를 가짐.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;그래프형-데이터-모델&quot;&gt;그래프형 데이터 모델&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;그래프 데이터 모델은 복잡한 다대다 관계나 연결 중심의 데이터를 다루기에 적합한 구조로, 다양한 종류의 객체와 관계를 유연하게 표현할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;속성-그래프-property-graph&quot;&gt;속성 그래프 (Property Graph)&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;구성 요소:
    &lt;ul&gt;
      &lt;li&gt;정점(Vertex): 개체 (예: 사람, 장소)&lt;/li&gt;
      &lt;li&gt;간선(Edge): 관계 (예: 친구, 방문함)&lt;/li&gt;
      &lt;li&gt;각 정점과 간선은 고유 ID, 레이블, 속성(key-value pair)을 가질 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Neo4j, Titan 등에서 사용됨&lt;/li&gt;
  &lt;li&gt;SQL 테이블처럼 정점/간선을 테이블로 저장 가능&lt;/li&gt;
  &lt;li&gt;예시:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;vertex_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;properties&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JSON&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;edge_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tail_vertex&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;head_vertex&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;properties&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JSON&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;사이퍼-질의-언어-cypher&quot;&gt;사이퍼 질의 언어 (Cypher)&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Neo4j용 선언형 쿼리 언어&lt;/li&gt;
  &lt;li&gt;그래프 패턴을 시각적으로 표현 (→ 방향 간선)&lt;/li&gt;
  &lt;li&gt;예: 미국에서 태어나 유럽에 사는 사람 찾기&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-cypher highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;MATCH&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;:BORN_IN&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;:WITHIN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;us:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Location&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;name:&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;United States&apos;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;}),&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;:LIVES_IN&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;:WITHIN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;eu:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Location&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;name:&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Europe&apos;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RETURN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person.name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[:WITHIN*0..]&lt;/code&gt;: WITHIN 관계를 0번 이상 반복하여 추적&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;sql의-그래프-질의&quot;&gt;SQL의 그래프 질의&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;SQL:1999 이후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WITH RECURSIVE&lt;/code&gt;를 통해 재귀적 질의 가능&lt;/li&gt;
  &lt;li&gt;Cypher와 동일한 쿼리:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RECURSIVE&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;in_usa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertex_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;United States&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;UNION&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tail_vertex&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in_usa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head_vertex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in_usa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertex_id&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;within&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- 생략&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;born_in_usa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;복잡하고 가독성 떨어짐 → Cypher에 비해 불리함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;트리플-저장소와-스파클&quot;&gt;트리플 저장소와 스파클&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;RDF 기반의 그래프 표현 방식으로, 시맨틱 웹 및 연결된 데이터를 위한 표준&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;시맨틱-웹&quot;&gt;시맨틱 웹&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;웹 상의 문서 외에 &lt;strong&gt;기계가 이해 가능한 데이터&lt;/strong&gt; 공유를 목표로 함&lt;/li&gt;
  &lt;li&gt;RDF는 (주어, 술어, 목적어) 트리플로 정보를 표현&lt;/li&gt;
  &lt;li&gt;예: (Lucy, bornIn, Idaho)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;rdf-데이터-모델&quot;&gt;RDF 데이터 모델&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Turtle 문법 예시:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-turtle highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;@prefix&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&amp;lt;urn:example:&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lucy&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Lucy&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bornIn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idaho&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idaho&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
       &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Idaho&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
       &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;within&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;usa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;URI를 통해 전 세계적으로 고유 식별자 사용&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;스파클-질의-언어-sparql&quot;&gt;스파클 질의 언어 (SPARQL)&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;RDF 트리플을 질의하기 위한 W3C 표준&lt;/li&gt;
  &lt;li&gt;Cypher와 매우 유사한 패턴 매칭 방식&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-sparql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;PREFIX&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;&amp;lt;urn:example:&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;?personName&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;?person&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;?personName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;?person&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;bornIn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;within&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;United States&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;?person&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;livesIn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;within&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Europe&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;초석-데이터로그-datalog&quot;&gt;초석: 데이터로그 (Datalog)&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Prolog 계열의 &lt;strong&gt;논리 기반 질의 언어&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;SPARQL과 Cypher의 이론적 기반&lt;/li&gt;
  &lt;li&gt;예시:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-prolog highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;ss&quot;&gt;within_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:-&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;
&lt;span class=&quot;ss&quot;&gt;within_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:-&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;within&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Via&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;within_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Via&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;

&lt;span class=&quot;ss&quot;&gt;migrated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;BornIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;LivingIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:-&lt;/span&gt; 
  &lt;span class=&quot;ss&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;born_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;BornLoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;within_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;BornLoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;BornIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;lives_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;LivingLoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;within_recursive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LivingLoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;LivingIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;규칙 기반으로 재사용성과 조합성 뛰어남&lt;/li&gt;
  &lt;li&gt;Datomic, Cascalog 등에서 사용됨&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
  &lt;p&gt;그래프 데이터 모델은 다대다 및 복잡한 관계형 데이터를 효율적으로 표현합니다. Cypher나 SPARQL처럼 패턴 기반의 선언형 질의 언어는 이러한 구조에서 뛰어난 질의 표현력을 제공합니다.&lt;/p&gt;
&lt;/blockquote&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-engineering" /><summary type="html">2장 데이터 모델과 질의 언어 관계형 모델과 문서 모델 NoSQL의 탄생 객체 관계형 불일치 다대일과 다대다 관계 문서 데이터베이스는 역사를 반복하고 있나? 네트워크 모델 관계형 모델 문서 데이터베이스와의 비교 관계형 데이터베이스와 오늘날의 문서 데이터베이스 어떤 데이터 모델이 애플리케이션 코드를 더 간단하게 할까? 문서 모델에서의 스키마 유연성 질의를 위한 데이터 지역성 문서 데이터베이스와 관계형 데이터베이스의 통합 데이터를 위한 질의 언어 웹에서의 선언형 질의 맵리듀스 질의 그래프형 데이터 모델 속성 그래프 사이퍼 질의 언어 SQL의 그래프 질의 트리플 저장소와 스파클 시맨틱 웹 RDF 데이터 모델 스파클 질의 언어 초석: 데이터로그 정리</summary></entry><entry><title type="html">1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션</title><link href="https://jangjichang.github.io/posts/reliable-scalable-and-maintainable-applications-chapter1" rel="alternate" type="text/html" title="1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션" /><published>2025-05-16T00:00:00+09:00</published><updated>2025-05-16T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/Reliable-Scalable-and-Maintainable-Applications-chapter1</id><content type="html" xml:base="https://jangjichang.github.io/posts/reliable-scalable-and-maintainable-applications-chapter1">&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1장-신뢰할-수-있고-확장-가능하며-유지보수하기-쉬운-애플리케이션&quot;&gt;1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#신뢰성&quot;&gt;신뢰성&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#하드웨어-결함&quot;&gt;하드웨어 결함&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#소프트웨어-오류&quot;&gt;소프트웨어 오류&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#인적-오류&quot;&gt;인적 오류&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#신뢰성은-얼마나-중요할까&quot;&gt;신뢰성은 얼마나 중요할까&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#확장성&quot;&gt;확장성&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#부하-기술하기&quot;&gt;부하 기술하기&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#성능-기술하기&quot;&gt;성능 기술하기&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#부하-대응-접근-방식&quot;&gt;부하 대응 접근 방식&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#유지보수성&quot;&gt;유지보수성&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#운용성--운영의-편리함-만들기&quot;&gt;운용성: 운영의 편리함 만들기&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#단순성--복잡도-관리&quot;&gt;단순성: 복잡도 관리&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#발전성--변화를-쉽게-만들기&quot;&gt;발전성: 변화를 쉽게 만들기&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;오늘날 많은 애플리케이션은 계산 중심(compute-intensive)과는 다르게 데이터 중심(data-intensive)적이다. 
이제부터 개발자는 애플리케이션 개발자뿐만 아니라 데이터 시스템 설계자이기도 하다.&lt;/p&gt;

&lt;p&gt;점점 더 많은 애플리케이션이 단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시킬 수 없는
과도하고 광범위한 요구사항을 갖고 있기 때문이다.&lt;/p&gt;

&lt;p&gt;사용자 요청에 따라&lt;/p&gt;
&lt;ol&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&gt;데이터 시스템이나 서비스를 설계할 떄 까다로운 문제가 많이 생긴다. 이 책에서는 대부분의 소프트웨어
시스템에서 중요하게 여기는 세 가지 관심사에 중점을 둔다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;신뢰성 (Reliability)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;하드웨어나 소프트웨어 결함, 인적 오류 같은 문제가 발생해도 시스템은 지속적으로 올바르게 동작해 야한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;확장성 (Scalability)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;유지보수성 (Maintainability)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;모든 사용자가 시스템 상에서 &lt;strong&gt;생산적으로&lt;/strong&gt; 작업할 수 있게 해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;신뢰성&quot;&gt;&lt;a href=&quot;#신뢰성&quot;&gt;신뢰성&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;“무언가 잘못되더라도 지속적으로 올바르게 동작함”을 신뢰성의 의미로 이해할 수 있다.
잘못될 수 있는 일을 &lt;strong&gt;결함(fault)&lt;/strong&gt;이라 함.
결함을 예측하고 대처할 수 있는 시스템을 내결함성 또는 탄력성이라고 함&lt;/p&gt;

&lt;p&gt;내결함성은 특정 유형의 결함 내성에 대해서만 이야기 하는 것이 타당함.
예를 들어, 블랙홀이 지구와 지구 상의 모든 서버를 삼켜버려도 웹 호스팅이 가능한 내결함성을 지닐 순 없다.&lt;/p&gt;

&lt;p&gt;결함은 장애(failure)와 다름.
결함은 사양에서 벗어난 시스템의 한 구성 요소로 정의함.
장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 것을 의미함.&lt;/p&gt;

&lt;h3 id=&quot;하드웨어-결함&quot;&gt;&lt;a href=&quot;#하드웨어-결함&quot;&gt;하드웨어 결함&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;하드디스크 고장. 램 결함. 대규모 정전. 네트워크 케이블을 잘못 뽑는 것. 등
규모가 큰 데이터센터에서 일하는 사람은 많은 장비를 다룰 경우 이 같은 일은 늘상 일어난다.&lt;/p&gt;

&lt;p&gt;장애율을 줄이기 위해 하드웨어 구성 요소에 중복을 추가하는 방법이 일반적이다.
디스크는 RAID 구성으로 설치 가능. 서버는 이중 전원 디바이스와 핫 스왑 가능한 CPU 구성.
데이터센터는 예비 전원용 발전기 갖추기.&lt;/p&gt;

&lt;p&gt;AWS 같은 일부 클라우드 플랫폼은 단일 장비 신뢰성보다 유연성과 탄력성을 우선적으로 처리하게끔
설계됐기 때문에 가상 장비 인스턴스가 별도의 경고 없이 사용할 수 없게 되는 상황이 상당히 일반적임.&lt;/p&gt;

&lt;h3 id=&quot;소프트웨어-오류&quot;&gt;&lt;a href=&quot;#소프트웨어-오류&quot;&gt;소프트웨어 오류&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;소프트웨어 오류는 하드웨어 결함보다 더 일반적이고 다양한 원인으로 발생함.
소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다. 시스템의 가정과 상호작용에 대해 주의 깊게
생각하기. 빈틈없는 테스트. 프로세스 격리. 재시작 허용. 시스템 동작 측정, 모니터링. 분석하기 등&lt;/p&gt;

&lt;h3 id=&quot;인적-오류&quot;&gt;&lt;a href=&quot;#인적-오류&quot;&gt;인적 오류&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;대규모 인터넷 서비스에 대한 한 연구에 따르면 운영자의 설정 오류가 중단의 주요 원인임.
반면 하드웨어 결함은 중단 원인의 10~25%에 불과함.&lt;/p&gt;

&lt;p&gt;어떻게 인적 오류를 줄일까? 최고의 시스템은 다양한 접근 방식을 결합한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;오류의 가능성을 최소화하는 방향으로 설계. 잘 설계된 추상화를 제공. 하지만 지나치게 제한적인 추상화면
이 인터페이스를 피해 작업한다. 그래서 균형을 맞추기 어려움.&lt;/li&gt;
  &lt;li&gt;샌드박스를 제공하라. 실제 데이터를 사용해 안전하게 살펴보고 실험할 수 있도록 해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;신뢰성은-얼마나-중요할까&quot;&gt;&lt;a href=&quot;#신뢰성은-얼마나-중요할까&quot;&gt;신뢰성은 얼마나 중요할까&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;비즈니스 애플리케이션에서 버그는 생산성 저하의 원인이고 매출에 손실이 발생하고 명성에 타격을 준다는
면에서 많은 비용이 든다.&lt;/p&gt;

&lt;p&gt;‘중요하지 않은’ 애플리케이션도 사용자에 대한 책임이 있다. 사진 앱에 아이들의 사진과 동영상을 모두 보관한 부모.
에러가 발생했을 때 부모의 심정은?&lt;/p&gt;

&lt;p&gt;증명되지 않은 시장을 위해 시제품을 개발하는 비용이나 매우 작은 이익률의 서비스를 운영하는 비용을
줄이려 신뢰성을 희생해야 하는 상황이 있다. 하지만 이 경우에는 비용을 줄여야 하는 시점을 매우 잘 알고 있어야 함.&lt;/p&gt;

&lt;h2 id=&quot;확장성&quot;&gt;&lt;a href=&quot;#확장성&quot;&gt;확장성&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;시스템이 현재 안정적으로 동작한다고 해서 미래에도 그럴 것이라는 보장은 없다.
성능 저하를 유발하는 흔한 이유 중 하나는 부하 증가다.&lt;/p&gt;

&lt;h3 id=&quot;부하-기술하기&quot;&gt;&lt;a href=&quot;#부하-기술하기&quot;&gt;부하 기술하기&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;시스템의 현재 부하를 간결하게 기술헤야 한다. 그래야 부하 성장 질문(부하가 두 배로 되면 어떻게 될까?)을
논의 할 수 있다.&lt;/p&gt;

&lt;p&gt;부하는 부하 매개변수(load parameter)로 기술할 수 있다. 가장 적합한 부하 매개변수 선택은 시스템
설계에 따라 달라진다. 웹 서버의 초당 요청 수, 데이터베이스 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자 수,
캐시 적중률 등이 될 수 있다.&lt;/p&gt;

&lt;p&gt;트윗 예시&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;트윗 작성&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사용자는 팔로워에게 새로운 메시지를 게시할 수 있다(평균 초당 4.6k 요청, 피그일 때 초당 12k 요청 이상).&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;홈 타임라인&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사용자는 팔로우한 사람이 작성한 트윗을 볼 수 있다(초당 300k 요청).&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;홈 타임라인 기능을 설계할 때 팔로우하는 모든 사람의 트윗을 시간순으로 정렬해서 합치는 방식은 질의 부하를
버텨내기 어렵다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweets&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sender_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;follows&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;follows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;followee_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;follows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;follower_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;current_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 Fanout on write 방식을 사용함. 트윗을 작성할 때마다 모든 팔로워의 홈 타임라인에 트윗을 추가함.
평균적으로 트윗이 약 75명의 팔로워에게 전달되므로 초당 4.6k 트윗은 홈 타임라인 캐시에 초당 345k건의 쓰기가 된다.&lt;/p&gt;

&lt;p&gt;그러나 팔로워가 3천만 명이 넘는 경우 단일 트윗이 홈 타임라인에 3천만 건 이상의 쓰기 요청이 된다.&lt;/p&gt;

&lt;p&gt;팔로워의 분포는 팬아웃 부하를 결정하기 때문에 확장성을 논의할 때 핵심 부하 매개변수가 된다.&lt;/p&gt;

&lt;p&gt;트위터는 최종적으로 Fanout on write, Fanout on read를 조합하여 사용함.
유저가 홈 타임라인을 요청할 때 팔로우 목록을 조회 후 DB에서 트윗 가져옴.&lt;/p&gt;

&lt;p&gt;대부분의 사용자의 트윗은 계속해서 사람들이 작성할 때 홈 타임라인에 펼쳐지지만 팔로워 수가 매우 많은
소수 사용자는 팬 아웃에서 제외된다. 유명인의 트윗은 별도로 가져와 읽는 시점에 사용자의 홈 타임라인에 합친다.&lt;/p&gt;

&lt;h3 id=&quot;성능-기술하기&quot;&gt;&lt;a href=&quot;#성능-기술하기&quot;&gt;성능 기술하기&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;시스템 부하를 기술하면 부하가 증가할 때 어떤 일이 일어나는지 조사할 수 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;부하 매개변수를 증가시키고 시스템 자원(CPU, 메모리, 네트워크 대역퐁 등)은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까?&lt;/li&gt;
  &lt;li&gt;부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;실제로 다양한 요청을 다루는 시스템에서 응답 시간은 많이 변한다.
응답 시간은 단일 숫자가 아니라 측정 가능한 값의 분포로 생각해야 한다.&lt;/p&gt;

&lt;p&gt;평균 응답 시간을 살피는 일은 일반적이다. 하지만 이는 좋은 지표가 아님.
얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 때문이다.&lt;/p&gt;

&lt;p&gt;백분위를 사용하는 편이 좋다. 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지
정렬하면 중간 지점이 중앙값이 된다. 중간 응답 시간이 200밀리초면 요청의 반은 200밀리초 미만으로
반환되고 나머지 반은 그보다 오래 걸린다는 뜻이다.&lt;/p&gt;

&lt;p&gt;특이 값이 얼마나 좋지 않은지 알아보려면 상위 백분의를 살펴본다. p95, p99, p99.9가 일반적이다.
p95는 95%의 요청이 이 응답 시간보다 빠르다는 뜻이다.&lt;/p&gt;

&lt;p&gt;예를 들어, p95 응답 시간이 1.5초라면 100개 중 95개는 1.5초 미만이고 5개는 1.5초 이상 걸린다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;꼬리 지연 시간(tail latency): 상위 백분위 응답 시간은 서비스의 사용자 경험에 직접 영향을
주기 떄문에 중요하다.&lt;/p&gt;

&lt;p&gt;예를 들어 아마존은 내부 서비스의 응답 시간 요구사항을 99.9분위로 기술한다.
1000개 중 1개만 영향이 있음에도 말이다.&lt;/p&gt;

&lt;p&gt;보통 응답 시간이 가장 느린 요청을 경험한 고객들은 많은 구매를 해서 고객 중에서 계정에 가장 많은
데이터를 갖고 있어서다.(즉, 이 고객들은 가장 소중한 고객이다)&lt;/p&gt;

&lt;p&gt;아마존은 응답 시간이 100밀리초 증가하면 판매량이 1% 줄어들고 1초가 느려지면 만족도 지표는
16% 줄어드는 현상을 관찰했다.&lt;/p&gt;

&lt;p&gt;하지만 99.9분위 최적화 작업은 비용이 너무 들지만 아마존이 추구하는 목표에 충분히 이익을
가져다주지 못한다고 여겨진다. 최상위 백분위는 통제할 수 없는 임의 이벤트에 쉽게 영향을 받기 때문에
응답 시간을 줄이기가 매우 어려워 이점은 더욱 줄어든다.&lt;/p&gt;

&lt;h3 id=&quot;부하-대응-접근-방식&quot;&gt;&lt;a href=&quot;#부하-대응-접근-방식&quot;&gt;부하 대응 접근 방식&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;용량 확장(scaling up): 수직 확장, 좀 더 강력한 장비로 이동
규모 확장(scaling out): 수평 확장, 다수의 낮은 사양 장비에 부하를 분산&lt;/p&gt;

&lt;p&gt;다수의 장비에 부하를 분산하는 아키텍처를 비공유(shared-nothing) 아키텍처라 부른다.&lt;/p&gt;

&lt;p&gt;다수의 장비에 상태 비저장 서비스를 배포하는 일은 상당히 간단하다. 하지만 단일 노드에 상태 유지
데이터 시스템을 분산 설치하는 일은 아주 많은 복잡도가 추가적으로 발생한다.
이런 이유로 확장 비용이나 데이터베이스를 분산으로 만들어야 하는 고가용성 요구가 있을 때까지
단일 노드에 데이터베이스를 유지하는 것(용량 확장)이 최근까지의 통념이다.&lt;/p&gt;

&lt;h2 id=&quot;유지보수성&quot;&gt;&lt;a href=&quot;#유지보수성&quot;&gt;유지보수성&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;고통을 최소화하고 레거시 소프트웨어를 만들지 않게끔 설계하는 방법.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;운용성(operability)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들어라.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;단순성(simplicity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;발전성(evolvabilty)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라. 그래야 요구사항 변경 같은 예기치 않은
사용 사례를 적용하기가 쉽다. 이 속성은 유연성, 수정 가능성, 적응성으로 알려져 있다.&lt;/p&gt;

&lt;p&gt;신뢰성, 확장성을 달성하기 위한 쉬운 해결책은 없다. 그보다 운용성, 단순성, 발전성을 염두에 두고
시스템을 생각하려 노력해야 한다.&lt;/p&gt;

&lt;h3 id=&quot;운용성-운영의-편리함-만들기&quot;&gt;&lt;a href=&quot;#운용성-운영의-편리함-만들기&quot;&gt;운용성: 운영의 편리함 만들기&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;“좋은 운영은 종조 나쁜 소프트웨어의 제약을 피하는 대안이 될 수 있다. 하지만 좋은 소프트웨어라도
나쁘게 운영할 경우 작동을 신뢰할 수 없다.”&lt;/p&gt;

&lt;p&gt;좋은 운영성이란 동일하게 반복되는 태스크를 쉽게 수행하게끔 만들어 운영팀이 고부가가치 활동에 노력을
집중한다는 의미다.&lt;/p&gt;

&lt;h3 id=&quot;단순성-복잡도-관리&quot;&gt;&lt;a href=&quot;#단순성-복잡도-관리&quot;&gt;단순성: 복잡도 관리&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;복잡도는 같은 시스템에서 작업해야 하는 모든 사람의 진행을 느리게 하고 나아가 유지보수 비용이 증가한다.
복잡도는 다양한 증상으로 나타난다. 상태 공간의 급증, 모듈 간 강한 커플링, 복잡한 의존성, 일관성 없는 명명과 용어,
성능 문제 해결을 목표로 한 해킹, 임시방편으로 문제를 해결한 특수 사례 등.&lt;/p&gt;

&lt;p&gt;추상화는 복잡도를 제거하기 위한 최상의 도구다.&lt;/p&gt;

&lt;h3 id=&quot;발전성-변화를-쉽게-만들기&quot;&gt;&lt;a href=&quot;#발전성-변화를-쉽게-만들기&quot;&gt;발전성: 변화를 쉽게 만들기&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다.&lt;/p&gt;

&lt;p&gt;데이터 시스템 변경을 쉽게 하고 변화된 요구사항에 시스템을 맞추는 방법은 시스템의 간단함과 추상화와 밀접한
관련이 있다. 간단하고 이해하기 쉬운 시스템은 대개 복잡한 시스템보다 수정하기 쉽다.&lt;/p&gt;</content><author><name>장지창</name></author><category term="engineering" /><category term="designing-data-intensive-applications" /><category term="data-engineering" /><summary type="html">1장 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션 신뢰성 하드웨어 결함 소프트웨어 오류 인적 오류 신뢰성은 얼마나 중요할까 확장성 부하 기술하기 성능 기술하기 부하 대응 접근 방식 유지보수성 운용성: 운영의 편리함 만들기 단순성: 복잡도 관리 발전성: 변화를 쉽게 만들기</summary></entry><entry><title type="html">답이 없지만 답을 찾으려고 노력하는 게 바로 바둑이다</title><link href="https://jangjichang.github.io/posts/the-match-movie-review" rel="alternate" type="text/html" title="답이 없지만 답을 찾으려고 노력하는 게 바로 바둑이다" /><published>2025-03-29T00:00:00+09:00</published><updated>2025-03-29T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/the-match-movie-review</id><content type="html" xml:base="https://jangjichang.github.io/posts/the-match-movie-review">&lt;p&gt;영화 &lt;strong&gt;승부&lt;/strong&gt;는 세계 최고 대회에서 우승해 국민 영웅이 된 조훈현이, 재능 있는 제자 이창호와 함께 수년간 수련한 끝에 첫 사제 대결에서 충격적인 패배를 겪는 과정을 그립니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;제자의 길 찾기:&lt;/strong&gt;&lt;br /&gt;
이창호는 스승의 화려하고 공격적인 스타일과는 달리 자신만의 독자적인 바둑 철학을 구축해 나갑니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스승의 깨달음:&lt;/strong&gt;&lt;br /&gt;
오랫동안 자신의 방식이 정답이라고 믿었던 조훈현은, 제자의 수를 인정하며 정답이 없다는 사실과 그로 인한 성장을 깨닫게 됩니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;두 인물은 바둑판 위에서 서로의 강점을 인정하며 끊임없는 도전과 자기 반성을 통해 각자의 길을 찾아 나갑니다.&lt;/p&gt;

&lt;p&gt;이 영화를 보며 최근 읽은 &lt;strong&gt;이펙티브 엔지니어&lt;/strong&gt;의 8장 ‘품질과 실용주의 사이에서 균형을 유지하라’가 주는 메시지와 닮아 있다고 생각했습니다.&lt;/p&gt;

&lt;h2 id=&quot;옳고-그름보다는-효과를-우선시하라&quot;&gt;&lt;a href=&quot;#옳고-그름보다는-효과를-우선시하라&quot;&gt;옳고 그름보다는 효과를 우선시하라&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;책에서 “옳고 그름 대신에 효과가 있느냐 없느냐의 관점에서 대상을 보는 것을 선호한다”라는 말을 통해,
소프트웨어 개발에 절대적인 정답을 찾기보다 상황에 맞는 최적의 해결책을 찾아야 한다고 강조합니다.&lt;/p&gt;

&lt;p&gt;저는 지금까지 코드 리뷰는 항상 코드를 머지하기 전에 선행되어야 하고, 작성한 코드에 적어도 하나의 테스트 코드는 작성해야 한다고
믿어왔습니다. 하지만 정답은 없고 팀의 문화와 상황에 따라 달라집니다.&lt;/p&gt;

&lt;p&gt;예를 들어, 코드 리뷰와 테스트 작성 방식은 다음과 같이 달라질 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;코드 리뷰:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;구글&lt;/strong&gt;은 모든 변경 사항에 대해 철저한 리뷰를 진행해 코드 품질을 유지합니다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;인스타그램&lt;/strong&gt;은 오버 더 숄더 리뷰를 통해 자연스러운 코드 검토 문화를 형성합니다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;스퀘어와 트위터&lt;/strong&gt;는 페어 프로그래밍을 통해 팀 내 협업과 실시간 피드백을 중시합니다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;우얄라와 쿼라&lt;/strong&gt;는 핵심 기능이나 비즈니스 로직에 초점을 맞춘 리뷰 방식을 채택해 효율적인 작업 환경을 마련합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;테스트 작성:&lt;/strong&gt;&lt;br /&gt;
모든 기능에 대해 테스트를 작성하기보다는, 비즈니스 로직처럼 중요한 부분에 집중하여 시간과 비용을 절약하는 전략이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;영화 &lt;strong&gt;승부&lt;/strong&gt;는 “정답은 없지만, 끊임없이 답을 찾아가는 과정”이라는 큰 메시지를 담고 있습니다.&lt;/p&gt;

&lt;p&gt;머리로는 여러 방법이 있다고 생각하면서도, 종종 익숙한 방식에 안주하곤 합니다. 혹은 내 방법이 정답이라고 착각하기도 하죠.
영화 속에서 세계 최고 대회 우승자조차 제자의 새로운 가능성을 인정하며 성장하는 모습을 보니,
정말 열린 마음으로 세상을 바라봐야겠습니다.&lt;/p&gt;

&lt;p&gt;결국 개발자에게도 하나의 정답만 존재하는 것은 아닙니다.
여러 방법을 시도하고, 다양한 관점을 받아들이는 자세야말로 우리를 더 나은 개발자로 이끄는 길임을 느끼게 됩니다.&lt;/p&gt;</content><author><name>장지창</name></author><category term="essay" /><category term="movie" /><summary type="html">영화 승부는 세계 최고 대회에서 우승해 국민 영웅이 된 조훈현이, 재능 있는 제자 이창호와 함께 수년간 수련한 끝에 첫 사제 대결에서 충격적인 패배를 겪는 과정을 그립니다. 제자의 길 찾기: 이창호는 스승의 화려하고 공격적인 스타일과는 달리 자신만의 독자적인 바둑 철학을 구축해 나갑니다. 스승의 깨달음: 오랫동안 자신의 방식이 정답이라고 믿었던 조훈현은, 제자의 수를 인정하며 정답이 없다는 사실과 그로 인한 성장을 깨닫게 됩니다.</summary></entry><entry><title type="html">품질과 실용주의 사이에서 균형을 유지하라</title><link href="https://jangjichang.github.io/posts/effective-engineer-08-balance-quality-with-pragmatism" rel="alternate" type="text/html" title="품질과 실용주의 사이에서 균형을 유지하라" /><published>2025-03-25T00:00:00+09:00</published><updated>2025-03-25T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/effective-engineer-08-Balance-Quality-With-Pragmatism</id><content type="html" xml:base="https://jangjichang.github.io/posts/effective-engineer-08-balance-quality-with-pragmatism">&lt;h2 id=&quot;실천-사항&quot;&gt;&lt;a href=&quot;#실천-사항&quot;&gt;실천 사항&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;이 책의 모든 장이 신선하게 다가왔지만 특히나 이 장은 사고를 넓히게 해줬다.&lt;/p&gt;

&lt;p&gt;내가 항상 지키고 따르려고 했던 두가지가 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;코드리뷰는 항상 Pull Request를 머지하기 전에 해야한다.&lt;/li&gt;
  &lt;li&gt;테스트는 반드시 작성해야한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;하지만 책에서 말하듯, 모든 것엔 트레이드오프가 있고 옳고 그름 대신에 효과가 있느냐 없느냐의 관점에서 대상을 바라보아야한다.&lt;/p&gt;

&lt;p&gt;코드 리뷰도 ‘코드 머지 전에’, ‘반드시 해야한다’는 생각에 벗어날 필요가 있다.
코드 리뷰가 효과를 내는 적절한 균형을 찾는 것이 중요하다.&lt;/p&gt;

&lt;h2 id=&quot;책-내용&quot;&gt;&lt;a href=&quot;#책-내용&quot;&gt;책 내용&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;소프트웨어 품질은 균형의 문제다. 어떻게 해야 한다고 규정된 하나의 보편적인 원칙은 존재하지 않는다. 
따라서 옳고 그름 대신에 효과가 있느냐 없느냐의 관점에서 대상을 보는 것이 좋다.&lt;/p&gt;

&lt;p&gt;좋은 소프트웨어를 만들어야 한다. 그렇지 않으면 소프트웨어를 대충 만들며 아낀 시간보다
이를 다루는 데 더 많은 시간을 낭비하게 된다.&lt;/p&gt;

&lt;p&gt;고품질의 코드베이스를 만드는 몇 가지 전략과 여기에 수반하는 몇 가지 손익을 살펴보자.&lt;/p&gt;

&lt;h2 id=&quot;지속-가능한-코드-리뷰-프로세스를-만들어라&quot;&gt;&lt;a href=&quot;#지속-가능한-코드-리뷰-프로세스를-만들어라&quot;&gt;지속 가능한 코드 리뷰 프로세스를 만들어라&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;코드 리뷰가 개발자에게 제공하는 명확한 혜택은 다음과 같다.&lt;/p&gt;
&lt;ul&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;li&gt;장기적인 작업 속도가 향상된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;코드를 리뷰하지 않는 개발자는 코드 리뷰가 코드 품질 개선에 도움이 된다는 것을 인정하면서도,
종종 리뷰가 개발 주기 반복 속도에 미치는 영향에 대해 우려를 표명한다. 코드 리뷰에 드는 시간과 노력을
제품 개발의 다른 측면에 쓰는 것이 더 낫다고 주장한다.&lt;/p&gt;

&lt;p&gt;코드 리뷰를 하는 것이 합리적일까?
해야한다 하지말아야한다의 이분법적인 사고 방식은 벗어나자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;구글&lt;/strong&gt;은 코드의 모든 변경사항을 반드시 리뷰하게 한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인스타그램&lt;/strong&gt;은 초창기에 모니터를 공유해서 다른 사람의 코드를 살펴보게 하는 오버 더 숄더 리뷰 방식을 활용했다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스퀘어와 트위터&lt;/strong&gt;는 코드 리뷰 대신에 페어 프로그래밍을 많이 활용했다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;우얄라&lt;/strong&gt;에서는 코드 리뷰를 도입한 당시, 팀을 참조한 이메일에 댓글을 달고 핵심 기능의 까다로운 부분만 리뷰했다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;쿼라&lt;/strong&gt;에서는 비즈니스 로직의 모델과 컨트롤러 코드에 대한 리뷰만 요청했다. 대부분의 코드를 프로덕션에 푸시한 후에 리뷰했다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;코드 리뷰가 효과를 내는 적절한 균형을 실험을 통해 찾아보라. 우얄라는 초기에 코드 리뷰 없이 운영했다.
하지만 품질이 떨어지는 코드가 제품 개발을 방해하는 것을 깨닫고 결국은 품질을 높이는 하나의 방법으로 리뷰를 도입했다.&lt;/p&gt;

&lt;h2 id=&quot;추상화를-통해-복잡성을-관리하라&quot;&gt;&lt;a href=&quot;#추상화를-통해-복잡성을-관리하라&quot;&gt;추상화를 통해 복잡성을 관리하라&lt;/a&gt;&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;올바른 추상화를 선택하면 프로그래밍이 설계부터 자연스럽게 흘러간다. 모듈 인터페이스는 작고 간단할 것이고
광범위한 개편이 없어도 새로운 기능이 잘 안착할 것이다. 잘못된 추상화를 선택하면 프로그래밍하는 동안 예상 밖의 문제가
줄줄이 일어난다. 인터페이스는 예상치 못한 인터랙션을 억지로 수용하기 위해 찌그러지거나 투박해질 것이고 아주 간단한 변경조차
하기 어려워진다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;위 인용문은 올바른 추상화가 엔지니어링 생산성을 어떻게 높이는지 보여준다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;원래 문제의 복잡성을 이해하기 쉬운 원시 형태로 분해해준다.&lt;/li&gt;
  &lt;li&gt;애플리케이션 유지 보수에 드는 수고가 줄고 개선사항을 적용하기 쉬워진다.&lt;/li&gt;
  &lt;li&gt;어려운 문제를 한 번 해결하면 그 해결책을 여러 번 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;좋은 추상화는 다음과 같은 속성이 있다.&lt;/p&gt;
&lt;ul&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;li&gt;확장하기 쉽다.&lt;/li&gt;
  &lt;li&gt;대상 사용자에게 적합하다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;좋은 추상화를 설계하려면 수고가 필요하다. 추상화를 처음 설계할 때 도움이 되는 몇 가지 아이디어를 소개하면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;회사의 코드베이스나 깃허브 저장소에서 인기 있는 추상화를 찾아라. 관련 문서를 읽고 소스 코드를 살펴보고 이를 확장해보라.&lt;/li&gt;
  &lt;li&gt;구글, 페이스북, 링크드인, 트위터 같은 IT 기업의 오픈 소스 프로젝트를 살펴보라.&lt;/li&gt;
  &lt;li&gt;인기 있는 API 인터페이스를 연구하라. 드롭박스, 페이스북, 아마존 웹 서비스 등에서 개발한 것이 좋은 예다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;테스트를-자동화하라&quot;&gt;&lt;a href=&quot;#테스트를-자동화하라&quot;&gt;테스트를 자동화하라&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;코드의 상당 부분이 출시된 제품에 포함되지 않는 상황에서 테스트에 대한 투자를 정당화하기 어렵다.&lt;/p&gt;

&lt;p&gt;보통 첫 번째 테스트를 작성하는 게 가장 어려운데, 결국 한 개발자가 기본적인 자동 테스트를 작성하면,
시간이 절약된다는 것을 확인하자 사람들은 개발 속도를 높이는 데 도움이 될 만한 다른 테스트를 찾는다.&lt;/p&gt;

&lt;p&gt;대규모 코드베이스를 작업할 때 테스트를 작성하는 습관을 들일 효과적인 방법은 레버리지가 높은 테스에 집중하는 것이다.
즉, 작성하는 데 든 시간에 비해 많은 시간을 절약해주는 테스트에 집중하는 것이다. 그러니 가장 가치 있는 테스트부터 시작하여
한 걸음씩 전진하라.&lt;/p&gt;

&lt;h2 id=&quot;기술-부채를-상환하라&quot;&gt;&lt;a href=&quot;#기술-부채를-상환하라&quot;&gt;기술 부채를 상환하라&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;가끔 단기로는 타당하나 장기로는 비용이 많이 드는 방식으로 작업할 떄가 있다. 어느 순간을 지나 부채가 너무 늘어나면
발전을 방해한다. 더 효과적인 개발자가 되려면 기한에 맞춰 업무를 완료해야 할 때는 어쩔 수 없이 기술 부채를 만들지만,
대신 주기적으로 그 부채를 상환해야 한다.&lt;/p&gt;

&lt;p&gt;팀의 실행 능력이 눈에 띄게 떨어질 정도로 개발 속도가 느려져서 기술 부채가 너무 늘어났다는 것이 확인되면 명시적으로 재작성
프로젝트 일정을 잡고 여기에 수반되는 위험을 감수하는 회사도 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;구글&lt;/strong&gt;은 개발자들이 특정 주제 관련 문제를 처리하도록 권장하는 픽스잇 데이(Fixit day)를 개최해 기술 부채를 상환하는
가벼운 장치로 활용한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;링크드인&lt;/strong&gt;은 회사가 상장된 후 2개월 동안 신규 기능 개발을 잠시 멈췄다. 그리고 망가진 프로세스를 수정하는 시간으로 삼았다.
그 결과 새로운 기능을 배초하는 데 한 달이나 걸렸는데 휴지기 뒤에는 개발 속도가 훨씬 더 빨라졌다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;모든 기술 부채가 상환할 가치가 있는 것은 아니다. 코드베이스에서 더 자주 읽고 호출하고 수정하는 부분일수록
기술 부채 이자가 높아진다. 제품의 주변부에 있는 코드나, 읽고 수정하는 일이 거의 없는 코드는 설사 기술 부채가 잔뜩
쌓여 있더라도 전체 개발 속도에 영향을 미치지 않는다.&lt;/p&gt;

&lt;h2 id=&quot;핵심-요약&quot;&gt;&lt;a href=&quot;#핵심-요약&quot;&gt;핵심 요약&lt;/a&gt;&lt;/h2&gt;
&lt;ul&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;/ul&gt;</content><author><name>장지창</name></author><category term="reading-notes" /><category term="effective-engineer" /><summary type="html">실천 사항</summary></entry><entry><title type="html">프로젝트 추정 기술을 향상시켜라</title><link href="https://jangjichang.github.io/posts/effective-engineer-07-improve-your-project-estimation-skills" rel="alternate" type="text/html" title="프로젝트 추정 기술을 향상시켜라" /><published>2025-03-11T00:00:00+09:00</published><updated>2025-03-11T00:00:00+09:00</updated><id>https://jangjichang.github.io/posts/effective-engineer-07-improve-your-project-estimation-skills</id><content type="html" xml:base="https://jangjichang.github.io/posts/effective-engineer-07-improve-your-project-estimation-skills">&lt;h2 id=&quot;실천-사항&quot;&gt;&lt;a href=&quot;#실천-사항&quot;&gt;실천 사항&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;일을 쪼개서 하라는 말은 많이 들었지만 얼마나 쪼개야할지 기준을 정하진 못했다.&lt;/p&gt;

&lt;p&gt;앞으로 이틀 이상 걸리는 작업은 더 쪼개려고 한다. 기준은 이틀로 정했다.&lt;/p&gt;

&lt;p&gt;업무별 목표를 정해서 진행하는데, 목표를 구체적으로 정해야한다.&lt;/p&gt;

&lt;p&gt;왜냐하면 불필요한 작업을 덜어주기 때문이다. 개발자로서 욕심이 생겨서
‘X 작업할 때 평소 신경쓰였던 Y도 같이 고쳐야 겠다.’라고 생각하는 경우가 많다.&lt;/p&gt;

&lt;p&gt;일을 시작할 때 아래 세가지를 정해야겠다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;작게 쪼개기&lt;/li&gt;
  &lt;li&gt;구체적인 목표 정하기&lt;/li&gt;
  &lt;li&gt;측정 가능한 마일스톤 정의하기&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;책-내용&quot;&gt;&lt;a href=&quot;#책-내용&quot;&gt;책 내용&lt;/a&gt;&lt;/h2&gt;

&lt;h2 id=&quot;정확한-추정치를-활용하여-프로젝트-계획을-추진하라&quot;&gt;&lt;a href=&quot;#정확한-추정치를-활용하여-프로젝트-계획을-추진하라.&quot;&gt;정확한 추정치를 활용하여 프로젝트 계획을 추진하라.&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;“이 프로젝트를 마치는 데 시간이 얼마나 걸릴까요?” 소프트웨어 프로젝트를 진행할 때 자주 듣는 질문이다.
우리가 한 추정이 설사 부정확하더라도 다른 사업적 결정에 반영된다. 추정이 형편없으면 큰 대가를 치러야 할 수도 있다.&lt;/p&gt;

&lt;p&gt;유연성을 보장하는 정확한 추정치를 도출하는데 도움되는 전략 몇가지&lt;/p&gt;
&lt;ul&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;li&gt;기준점 편향에 주의하라.&lt;/li&gt;
  &lt;li&gt;하나의 업무를 여러 방법을 사용해 추정하라.&lt;/li&gt;
  &lt;li&gt;이상적인 인월(person-month)을 조심하라.&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&gt;추정을 반복해서 수정하면 프로젝트 결과물이 나아질 수 있다. 프로젝트 초반에는 추정에 불확실한 부분이 많지만,
세부사항이 구체화될수록 변화의 여지가 줄어든다.&lt;/p&gt;

&lt;h2 id=&quot;미지의-변수를-고려하라&quot;&gt;&lt;a href=&quot;#미지의-변수를-고려하라.&quot;&gt;미지의 변수를 고려하라.&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;일정을 크게 어긋나게 한 원인은 아예 추정하거나 계산할 수 없었던 온갖 미지의 프로젝트와 문제에 있다.&lt;/p&gt;
&lt;ul&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;li&gt;고객 데이터가 많아지면서 발생하는 제품의 확장성 문제 해결하기&lt;/li&gt;
  &lt;li&gt;프로젝트 중간에 초창기 개발자를 다른 회사에 빼앗기기: 많은 지식을 이어받고 작업을 재분배해야 했다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;‘이 프로젝트 엔지니어링 작업을 완료하기까지 1개월이 걸릴 것이다.’라는 말은 달력상 1개월 이상이 소요된다는 말이다.&lt;/p&gt;

&lt;p&gt;평소 개발자는 눈에 띄는 버그 수정, 면접 진행, 팀 회의 참석, 관리자와 1:1 대화 진행, 비상 당번 근무,
신입 개발자 교육, 이메일 회신 등 많은 엔지니어링 외적 업무를 반복해서 처리해야 한다. 이런 사항을 고려하면
하루 8시간을 근무한다고 해서 프로젝트에 8시간을 쓴다고 볼 수는 없다.&lt;/p&gt;

&lt;p&gt;일회성 방해 요소도 발생한다.&lt;/p&gt;
&lt;ul&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;li&gt;핵심 사업 지표가 갑자기 떨어져서 조사가 필요할 때도 있다.&lt;/li&gt;
  &lt;li&gt;팀원이 아프거나 휴가를 갈 때도 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;구체적인-프로젝트-목표와-측정-가능한-마일스톤을-정의하라&quot;&gt;&lt;a href=&quot;#구체적인-프로젝트-목표와-측정-가능한-마일스톤을-정의하라.&quot;&gt;구체적인 프로젝트 목표와 측정 가능한 마일스톤을 정의하라.&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;프로젝트 목표를 설정하는 간단한 작업을 하면 두 가지 구체적인 혜택이 뒤따른다.&lt;/p&gt;

&lt;p&gt;첫째, 잘 정의해둔 목표는 작업 목록에서 꼭 해야 할 일과 하면 좋은 일을 구분하는 중요한 필터가 된다.
목표가 구체적일수록 기능을 구별하는 데 도움이 된다. 구체적인 프로젝트 목표의 예를 들면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;홈페이지 사용자 지연 시간의 p95를 0.5초 이하로 감소시키기&lt;/li&gt;
  &lt;li&gt;사용자가 콘텐츠 유형에 따라 결과를 필터링할 수 있는 새로운 검색 기능 배포하기&lt;/li&gt;
  &lt;li&gt;서비스를 루비에서 C++로 포팅하여 성능 개선하기&lt;/li&gt;
  &lt;li&gt;서버에서 설정값을 요청할 수 있게 웹 애플리케이션 재설계하기&lt;/li&gt;
  &lt;li&gt;통신망 접속이 없을 때도 콘텐츠에 접근할 수 있게 오프라인에서도 모바일 애플리케이션 지원하기&lt;/li&gt;
  &lt;li&gt;고객당 매출을 증가시키기 위해 제품 결제 흐름 A/B 테스트하기&lt;/li&gt;
  &lt;li&gt;국가별로 핵심 지표를 세분화하는 새로운 분석 보고서 개발하기&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;두번째는 핵심 이해 관계자들 사이에 명확성과 공통의 이해가 형성된다는 것이다.&lt;/p&gt;

&lt;h2 id=&quot;재작성-프로젝트는-매우-조심스럽게-접근하라&quot;&gt;&lt;a href=&quot;#재작성-프로젝트는-매우-조심스럽게-접근하라.&quot;&gt;재작성 프로젝트는 매우 조심스럽게 접근하라.&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;무언가를 바닥부터 재작성하려는 욕구는 소프트웨어 개발자의 아주 일반적인 특징이다.&lt;/p&gt;

&lt;p&gt;안타깝게도 재작성 프로젝트는 매우 위험한 편이다. 재작성 프로젝트가 특히 골머리를 썩이는 데는 몇 가지 이유가 있다.&lt;/p&gt;
&lt;ul&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;/ul&gt;

&lt;h2 id=&quot;마라톤-중간에-전력-질주하지-마라&quot;&gt;&lt;a href=&quot;#마라톤-중간에-전력-질주하지-마라&quot;&gt;마라톤 중간에 전력 질주하지 마라.&lt;/a&gt;&lt;/h2&gt;
&lt;ul&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;li&gt;마감 기한이 다가올수록 의사소통 간접 비용이 늘어난다.&lt;/li&gt;
  &lt;li&gt;기한을 향한 전력 질주 때문에 기술 부책 유발된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;초과 근무할 필요가 있다고 생각한다면 팀원의 동의를 구해라.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;지금까지 타임라인이 지연된 주요 원인을 모두가 이해하고 공유하라.&lt;/li&gt;
  &lt;li&gt;프로젝트 계획과 타임라인을 현실적으로 수정하라.&lt;/li&gt;
  &lt;li&gt;프로젝트가 수정한 타임라인보다 더 지연된다면 전력 질주를 포기하라.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;핵심-요약&quot;&gt;&lt;a href=&quot;#핵심-요약&quot;&gt;핵심 요약&lt;/a&gt;&lt;/h2&gt;

&lt;ul&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;li&gt;초과 근무의 한계를 이해하라.&lt;/li&gt;
&lt;/ul&gt;</content><author><name>장지창</name></author><category term="reading-notes" /><category term="effective-engineer" /><summary type="html">실천 사항</summary></entry></feed>