<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>oi_24.log</title>
        <link>https://velog.io/</link>
        <description>늦게나마 정신을 차리려고 하는 개발 뭐시기하는 사람</description>
        <lastBuildDate>Fri, 17 Apr 2026 01:30:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>oi_24.log</title>
            <url>https://velog.velcdn.com/images/oi_24/profile/3c124183-e926-4861-89c7-be556283f0cc/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. oi_24.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/oi_24" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Claude Code로 대형 오픈소스 분석하기 - 멀티에이전트 접근법]]></title>
            <link>https://velog.io/@oi_24/Claude-Code%EB%A1%9C-%EB%8C%80%ED%98%95-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0-%EB%A9%80%ED%8B%B0%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%A0%91%EA%B7%BC%EB%B2%95</link>
            <guid>https://velog.io/@oi_24/Claude-Code%EB%A1%9C-%EB%8C%80%ED%98%95-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0-%EB%A9%80%ED%8B%B0%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%A0%91%EA%B7%BC%EB%B2%95</guid>
            <pubDate>Fri, 17 Apr 2026 01:30:48 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-컨텍스트가-커지면-퀄리티가-떨어진다">문제: 컨텍스트가 커지면 퀄리티가 떨어진다</h2>
<p>OpenTelemetry Collector 같은 대형 프로젝트를 분석할 때 한 세션에서 전부 처리하려고 하면 문제가 생긴다.</p>
<p>LLM은 컨텍스트 윈도우 안에서 처리하는 정보가 많아질수록 집중도가 떨어지고, 분석 퀄리티도 함께 낮아진다. 소스가 클수록 &quot;전체를 다 보여주는&quot; 방식은 오히려 역효과다.</p>
<pre><code>[X] 한 세션에서 전부 처리
소스 전체 → [단일 에이전트] → 퀄리티 낮은 분석

[O] 멀티에이전트
소스 전체 → 파트 분할 → [스페셜리스트 A] → 검토/검증
                       → [스페셜리스트 B] → 검토/검증  → 최종 합성
                       → [스페셜리스트 C] → 검토/검증</code></pre><p>핵심 원칙은 단순하다. <strong>좁은 컨텍스트 = 깊은 분석</strong></p>
<hr>
<h2 id="접근법-오케스트레이터--스페셜리스트">접근법: 오케스트레이터 + 스페셜리스트</h2>
<h3 id="구조">구조</h3>
<pre><code>오케스트레이터 (메인 세션)
  ├── 전체 구조 파악 및 분석 단위 확정
  ├── 서브에이전트들에게 파트 위임
  ├── 결과 검토 및 cross-cutting 검증
  └── 최종 합성
       ↑
스페셜리스트 에이전트들 (병렬 실행)
  ├── Agent A: 핵심 인터페이스 담당
  ├── Agent B: 데이터 수집/전송 담당
  ├── Agent C: 데이터 처리/라우팅 담당
  ├── Agent D: 서비스/확장 담당
  └── Agent E: 설정/인프라 담당</code></pre><h3 id="역할-분리">역할 분리</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>담당</th>
<th>컨텍스트 범위</th>
</tr>
</thead>
<tbody><tr>
<td>오케스트레이터</td>
<td>구조 파악, 위임, 검증, 합성</td>
<td>전체 (얕게)</td>
</tr>
<tr>
<td>스페셜리스트</td>
<td>담당 파트 심층 분석</td>
<td>부분 (깊게)</td>
</tr>
</tbody></table>
<p>오케스트레이터는 깊이 파지 않는다. 스페셜리스트가 가져온 결과를 <strong>검토하고 검증하고 보완 요청</strong>하는 게 주 역할이다.</p>
<hr>
<h2 id="opentelemetry-collector에-적용">OpenTelemetry Collector에 적용</h2>
<h3 id="프로젝트-구조">프로젝트 구조</h3>
<p>OpenTelemetry Collector는 멀티모듈 Go 프로젝트로, 컴포넌트 경계가 명확하게 디렉토리로 나뉘어 있어 에이전트 분할에 적합하다.</p>
<pre><code>opentelemetry-collector/
├── component/      # 핵심 인터페이스
├── pdata/          # 텔레메트리 데이터 모델
├── pipeline/       # 파이프라인 시그널 타입
├── consumer/       # 컨슈머 인터페이스
├── receiver/       # 데이터 수집
├── exporter/       # 데이터 전송
├── processor/      # 데이터 처리
├── connector/      # 파이프라인 라우팅
├── extension/      # 확장 기능
├── service/        # 서비스 오케스트레이션
├── otelcol/        # 메인 바이너리
├── confmap/        # 설정 처리
├── featuregate/    # 피처 플래그
└── internal/       # 내부 유틸리티</code></pre><h3 id="에이전트-분할-5개">에이전트 분할 (5개)</h3>
<table>
<thead>
<tr>
<th>에이전트</th>
<th>담당 디렉토리</th>
<th>분석 포인트</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Core</strong></td>
<td><code>component</code>, <code>pdata</code>, <code>pipeline</code>, <code>consumer</code></td>
<td>핵심 인터페이스, 데이터 모델, 의존성 방향</td>
</tr>
<tr>
<td><strong>Receiver/Exporter</strong></td>
<td><code>receiver</code>, <code>exporter</code></td>
<td>수집/전송 인터페이스, 구현체 패턴, helper 구조</td>
</tr>
<tr>
<td><strong>Processor/Connector</strong></td>
<td><code>processor</code>, <code>connector</code></td>
<td>데이터 변환 로직, 라우팅 메커니즘</td>
</tr>
<tr>
<td><strong>Service</strong></td>
<td><code>extension</code>, <code>service</code>, <code>otelcol</code></td>
<td>라이프사이클 관리, 파이프라인 조립, 진입점</td>
</tr>
<tr>
<td><strong>Infra</strong></td>
<td><code>confmap</code>, <code>featuregate</code>, <code>filter</code>, <code>scraper</code>, <code>internal</code></td>
<td>설정 해석, 피처 플래그, 공통 유틸리티</td>
</tr>
</tbody></table>
<hr>
<h2 id="구현-claude-code-agent-툴">구현: Claude Code Agent 툴</h2>
<p>Claude Code에서 이 패턴은 별도 인프라 없이 내장 <code>Agent</code> 툴만으로 구현된다.</p>
<h3 id="병렬-실행">병렬 실행</h3>
<pre><code class="language-python"># 메인 세션에서 여러 서브에이전트를 한 번에 호출
Agent(subagent_type=&quot;Explore&quot;, prompt=&quot;Core 분석: component, pdata...&quot;)
Agent(subagent_type=&quot;Explore&quot;, prompt=&quot;Receiver/Exporter 분석: receiver, exporter...&quot;)
Agent(subagent_type=&quot;Explore&quot;, prompt=&quot;Processor/Connector 분석: processor, connector...&quot;)
# → 병렬로 실행, 결과가 메인 세션으로 반환</code></pre>
<h3 id="결과-흐름">결과 흐름</h3>
<pre><code>스페셜리스트 결과 반환
    ↓
오케스트레이터가 검토
    ↓
cross-cutting concerns 교차 검증
    ↓
누락 부분 보완 요청 (필요시 추가 에이전트 실행)
    ↓
최종 합성</code></pre><hr>
<h2 id="프롬프트-설계-원칙">프롬프트 설계 원칙</h2>
<p>각 스페셜리스트 프롬프트에 반드시 포함해야 할 것:</p>
<ol>
<li><strong>담당 범위 명시</strong> - 어떤 디렉토리/패키지만 볼 것인지</li>
<li><strong>분석 포인트</strong> - 인터페이스, 의존성, 핵심 로직 중 무엇에 집중할지</li>
<li><strong>결과 형식</strong> - 오케스트레이터가 검토하기 좋은 구조화된 포맷</li>
<li><strong>경계 명시</strong> - 다른 에이전트 담당 영역은 깊이 들어가지 않도록</li>
</ol>
<p>오케스트레이터 프롬프트에 반드시 포함해야 할 것:</p>
<ol>
<li><strong>cross-cutting 체크리스트</strong> - 각 에이전트 결과 간 연결 포인트</li>
<li><strong>검증 기준</strong> - 누락/모순 여부 판단 기준</li>
<li><strong>보완 요청 조건</strong> - 어떤 경우에 추가 분석을 요청할지</li>
</ol>
<hr>
<h2 id="마치며">마치며</h2>
<p>이 접근법의 핵심은 <strong>컨텍스트 관리</strong>다. LLM에게 많은 정보를 한꺼번에 주는 것보다, 적절히 쪼개서 각자 깊이 파게 하고 메인이 검증하는 구조가 실제로 더 나은 결과를 만든다.</p>
<p>대형 오픈소스 프로젝트 분석뿐 아니라, 복잡한 버그 디버깅이나 아키텍처 리뷰에도 같은 패턴을 적용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Grafana Alloy 개념 정리]]></title>
            <link>https://velog.io/@oi_24/Grafana-Alloy-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@oi_24/Grafana-Alloy-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 22 Mar 2026 12:41:56 GMT</pubDate>
            <description><![CDATA[<p><strong>데이터 파이프라인 에이전트</strong> — 어디서든 데이터를 받아서, 필요하면 가공하고, 어디든 보낼 수 있는 범용 수집기</p>
<hr>
<h2 id="핵심-개념">핵심 개념</h2>
<p>Alloy는 <strong>컴포넌트를 연결</strong>하는 방식으로 동작함.</p>
<pre><code>[소스 컴포넌트] → [처리 컴포넌트] → [목적지 컴포넌트]
     (input)          (process)          (output)</code></pre><p>설정 파일(<code>.alloy</code>)에서 컴포넌트를 선언하고 연결하면, Alloy가 그 흐름대로 데이터를 처리함.</p>
<hr>
<h2 id="역할-1-수집-producer">역할 1: 수집 (Producer)</h2>
<p>앱 서버에 <strong>에이전트로 설치</strong>되어 로그/메트릭을 읽어서 Kafka 등으로 전송.</p>
<pre><code>파일 로그, stdout, 메트릭
    → Alloy (읽기 + 변환)
        → Kafka</code></pre><pre><code class="language-hcl">loki.source.file &quot;app&quot; {
  targets    = [{ __path__ = &quot;/var/log/app.log&quot; }]
  forward_to = [otelcol.exporter.kafka.default.input]
}

otelcol.exporter.kafka &quot;default&quot; {
  brokers  = [&quot;kafka:9092&quot;]
  topic    = &quot;logs&quot;
  encoding = &quot;otlp_proto&quot;
}</code></pre>
<hr>
<h2 id="역할-2-소비-consumer">역할 2: 소비 (Consumer)</h2>
<p>Kafka 토픽의 메시지를 읽어서 <strong>ClickHouse / Loki 등 백엔드로 적재</strong>.</p>
<pre><code>Kafka 토픽
    → Alloy (소비 + 변환)
        → ClickHouse / Loki</code></pre><pre><code class="language-hcl">otelcol.receiver.kafka &quot;default&quot; {
  brokers  = [&quot;kafka:9092&quot;]
  topic    = &quot;logs&quot;
  encoding = &quot;otlp_proto&quot;

  output {
    logs = [otelcol.exporter.otlphttp.clickhouse.input]
  }
}</code></pre>
<hr>
<h2 id="지원-입출력">지원 입출력</h2>
<h3 id="입력-받을-수-있는-것">입력 (받을 수 있는 것)</h3>
<ul>
<li>파일 로그 (<code>/var/log/...</code>)</li>
<li>Docker / Kubernetes 컨테이너 로그</li>
<li>OpenTelemetry (traces, metrics, logs)</li>
<li>Prometheus metrics</li>
<li>Kafka 메시지</li>
</ul>
<h3 id="출력-보낼-수-있는-것">출력 (보낼 수 있는 것)</h3>
<ul>
<li>Kafka</li>
<li>Loki (로그)</li>
<li>Tempo (트레이스)</li>
<li>Prometheus / Mimir (메트릭)</li>
<li>ClickHouse (OTel exporter 경유)</li>
<li>OpenTelemetry 호환 백엔드 전반</li>
</ul>
<hr>
<h2 id="kafka--clickhouse-조합에서의-구조">Kafka + ClickHouse 조합에서의 구조</h2>
<h3 id="alloy-없이">Alloy 없이</h3>
<pre><code>앱 서버 ──(직접 produce)──→ Kafka ──→ ClickHouse</code></pre><ul>
<li>앱 코드에 Kafka producer 로직 직접 구현 필요</li>
<li>포맷 변경 시 앱 코드 수정 필요</li>
</ul>
<h3 id="alloy-추가-후">Alloy 추가 후</h3>
<pre><code>앱 서버
    → Alloy (에이전트, 각 서버에 설치)
        → Kafka
            → Alloy (게이트웨이, 중앙 서버)
                → ClickHouse</code></pre><p>앱은 로그만 쓰고, 수집/변환/적재는 Alloy가 담당.</p>
<hr>
<h2 id="장단점">장단점</h2>
<h3 id="장점">장점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>수집 분리</td>
<td>앱 코드에서 Kafka 의존성 제거</td>
</tr>
<tr>
<td>변환/정규화</td>
<td>포맷 통일, 불필요한 필드 제거를 파이프라인에서 처리</td>
</tr>
<tr>
<td>멀티 소스 통합</td>
<td>파일, stdout, OTel 등 다양한 소스를 하나로 수렴</td>
</tr>
<tr>
<td>라우팅</td>
<td>로그 종류에 따라 다른 토픽/백엔드로 분기 가능</td>
</tr>
<tr>
<td>표준화</td>
<td>모든 서버에 Alloy만 설치하면 수집 파이프라인 일원화</td>
</tr>
</tbody></table>
<h3 id="단점">단점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>추가 홉</td>
<td>앱 → Alloy → Kafka → ClickHouse로 레이턴시 소폭 증가</td>
</tr>
<tr>
<td>운영 복잡도</td>
<td>관리할 컴포넌트가 늘어남</td>
</tr>
<tr>
<td>Alloy 장애</td>
<td>Alloy가 죽으면 수집 중단 (로컬 버퍼로 일부 완화 가능)</td>
</tr>
<tr>
<td>오버엔지니어링</td>
<td>앱이 적고 구조가 단순하면 직접 produce가 더 나을 수 있음</td>
</tr>
</tbody></table>
<hr>
<h2 id="언제-쓸지-판단-기준">언제 쓸지 판단 기준</h2>
<pre><code>앱 코드를 건드리기 싫다          → Alloy 유용
여러 소스를 통합 수집해야 한다    → Alloy 유용
Kafka 전송 전에 변환이 필요하다   → Alloy 유용

앱이 1~2개이고 포맷이 단순하다    → 앱에서 직접 Kafka producer 써도 충분</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Claude Code Hooks 기본 정리]]></title>
            <link>https://velog.io/@oi_24/Claude-Code-Hooks-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@oi_24/Claude-Code-Hooks-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 22 Mar 2026 11:36:26 GMT</pubDate>
            <description><![CDATA[<p>Claude Code 실행 주기의 특정 시점에 자동으로 실행되는 사용자 정의 명령어.
LLM이 &quot;할 수도 있고 안 할 수도 있는&quot; 게 아니라, <strong>조건이 맞으면 무조건 실행</strong>됨.</p>
<hr>
<h2 id="hook-이벤트-종류">Hook 이벤트 종류</h2>
<table>
<thead>
<tr>
<th>이벤트</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>PreToolUse</code></td>
<td>도구 실행 <strong>전</strong> (차단 가능)</td>
</tr>
<tr>
<td><code>PostToolUse</code></td>
<td>도구 실행 <strong>후</strong></td>
</tr>
<tr>
<td><code>PostToolUseFailure</code></td>
<td>도구 실행 <strong>실패</strong> 후</td>
</tr>
<tr>
<td><code>PermissionRequest</code></td>
<td>권한 요청 시 (자동 승인/거부 가능)</td>
</tr>
<tr>
<td><code>UserPromptSubmit</code></td>
<td>프롬프트 제출 시</td>
</tr>
<tr>
<td><code>Notification</code></td>
<td>알림 발생 시</td>
</tr>
<tr>
<td><code>SessionStart</code></td>
<td>세션 시작/재개 시</td>
</tr>
<tr>
<td><code>SessionEnd</code></td>
<td>세션 종료 시</td>
</tr>
<tr>
<td><code>Stop</code></td>
<td>Claude 응답 완료 시</td>
</tr>
<tr>
<td><code>StopFailure</code></td>
<td>API 오류로 턴 종료 시</td>
</tr>
<tr>
<td><code>PreCompact</code></td>
<td>컨텍스트 압축 전</td>
</tr>
<tr>
<td><code>PostCompact</code></td>
<td>컨텍스트 압축 후</td>
</tr>
<tr>
<td><code>SubagentStart</code></td>
<td>Subagent 생성 시</td>
</tr>
<tr>
<td><code>SubagentStop</code></td>
<td>Subagent 완료 시</td>
</tr>
<tr>
<td><code>WorktreeCreate</code></td>
<td>Worktree 생성 시</td>
</tr>
<tr>
<td><code>WorktreeRemove</code></td>
<td>Worktree 삭제 시</td>
</tr>
<tr>
<td><code>ConfigChange</code></td>
<td>설정 파일 변경 시</td>
</tr>
</tbody></table>
<hr>
<h2 id="설정-위치">설정 위치</h2>
<pre><code>~/.claude/settings.json          # 전역 (모든 프로젝트)
.claude/settings.json            # 프로젝트 공유
.claude/settings.local.json      # 프로젝트 로컬 (gitignored)</code></pre><hr>
<h2 id="기본-구조">기본 구조</h2>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;EventName&quot;: [
      {
        &quot;matcher&quot;: &quot;regex_pattern&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;/path/to/script.sh&quot;,
            &quot;timeout&quot;: 30
          }
        ]
      }
    ]
  }
}</code></pre>
<hr>
<h2 id="hook-타입">Hook 타입</h2>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>command</code></td>
<td>셸 명령어 실행 (가장 일반적)</td>
</tr>
<tr>
<td><code>http</code></td>
<td>HTTP 엔드포인트 호출</td>
</tr>
<tr>
<td><code>prompt</code></td>
<td>LLM 프롬프트로 판단</td>
</tr>
<tr>
<td><code>agent</code></td>
<td>서브에이전트 실행</td>
</tr>
</tbody></table>
<hr>
<h2 id="exit-code-동작">Exit Code 동작</h2>
<table>
<thead>
<tr>
<th>Exit Code</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>0</code></td>
<td>성공, 액션 진행</td>
</tr>
<tr>
<td><code>2</code></td>
<td><strong>차단</strong> — 액션 실행 안 함, stderr를 Claude에게 피드백으로 전달</td>
</tr>
<tr>
<td><code>1</code>, <code>3+</code></td>
<td>오류 (비차단) — 액션은 진행</td>
</tr>
</tbody></table>
<hr>
<h2 id="hook-inputoutput">Hook Input/Output</h2>
<h3 id="input-stdin으로-전달되는-json">Input (stdin으로 전달되는 JSON)</h3>
<pre><code class="language-json">{
  &quot;session_id&quot;: &quot;abc123&quot;,
  &quot;cwd&quot;: &quot;/Users/me/myproject&quot;,
  &quot;hook_event_name&quot;: &quot;PreToolUse&quot;,
  &quot;tool_name&quot;: &quot;Bash&quot;,
  &quot;tool_input&quot;: {
    &quot;command&quot;: &quot;npm test&quot;
  },
  &quot;tool_use_id&quot;: &quot;toolu_...&quot;
}</code></pre>
<h3 id="output-exit-0--stdout-json">Output (exit 0 + stdout JSON)</h3>
<pre><code class="language-json">{
  &quot;continue&quot;: true,
  &quot;suppressOutput&quot;: false,
  &quot;systemMessage&quot;: &quot;optional message to Claude&quot;,
  &quot;hookSpecificOutput&quot;: {
    &quot;hookEventName&quot;: &quot;PreToolUse&quot;,
    &quot;permissionDecision&quot;: &quot;allow|deny|ask&quot;,
    &quot;permissionDecisionReason&quot;: &quot;reason&quot;
  }
}</code></pre>
<hr>
<h2 id="matcher-패턴">Matcher 패턴</h2>
<table>
<thead>
<tr>
<th>이벤트</th>
<th>매칭 대상</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>PreToolUse</code> / <code>PostToolUse</code></td>
<td>도구 이름</td>
<td><code>&quot;Bash&quot;</code>, <code>&quot;Edit|Write&quot;</code></td>
</tr>
<tr>
<td><code>SessionStart</code> / <code>SessionEnd</code></td>
<td>세션 이유</td>
<td><code>&quot;startup&quot;</code>, <code>&quot;resume&quot;</code>, <code>&quot;compact&quot;</code></td>
</tr>
<tr>
<td><code>ConfigChange</code></td>
<td>설정 소스</td>
<td><code>&quot;user_settings&quot;</code>, <code>&quot;project_settings&quot;</code></td>
</tr>
<tr>
<td><code>Notification</code></td>
<td>알림 타입</td>
<td><code>&quot;permission_prompt&quot;</code>, <code>&quot;idle_prompt&quot;</code></td>
</tr>
<tr>
<td>MCP 도구</td>
<td>도구 이름</td>
<td><code>&quot;mcp__github__.*&quot;</code>, <code>&quot;mcp__.*__write.*&quot;</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="실용-예시">실용 예시</h2>
<h3 id="1-민감-파일-보호">1. 민감 파일 보호</h3>
<pre><code class="language-bash">#!/bin/bash
# .claude/hooks/protect-files.sh

FILE=$(cat | jq -r &#39;.tool_input.file_path // empty&#39;)

for pattern in &quot;.env&quot; &quot;package-lock.json&quot; &quot;.git/&quot;; do
  if [[ &quot;$FILE&quot; == *&quot;$pattern&quot;* ]]; then
    echo &quot;Blocked: $FILE matches protected pattern &#39;$pattern&#39;&quot; &gt;&amp;2
    exit 2
  fi
done

exit 0</code></pre>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PreToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;Edit|Write&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;\&quot;$CLAUDE_PROJECT_DIR\&quot;/.claude/hooks/protect-files.sh&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="2-파일-편집-후-자동-포맷팅">2. 파일 편집 후 자동 포맷팅</h3>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PostToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;Edit|Write&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;jq -r &#39;.tool_input.file_path&#39; | xargs npx prettier --write&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="3-권한-자동-승인">3. 권한 자동 승인</h3>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PermissionRequest&quot;: [
      {
        &quot;matcher&quot;: &quot;ExitPlanMode&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;echo &#39;{\&quot;hookSpecificOutput\&quot;: {\&quot;hookEventName\&quot;: \&quot;PermissionRequest\&quot;, \&quot;decision\&quot;: {\&quot;behavior\&quot;: \&quot;allow\&quot;}}}&#39;&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="4-작업-완료-시-macos-알림">4. 작업 완료 시 macOS 알림</h3>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;Notification&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;osascript -e &#39;display notification \&quot;Claude needs attention\&quot; with title \&quot;Claude Code\&quot;&#39;&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="5-linux-알림">5. Linux 알림</h3>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;Notification&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;notify-send &#39;Claude Code&#39; &#39;Claude needs your attention&#39;&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="6-mcp-도구-호출-로깅">6. MCP 도구 호출 로깅</h3>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;PostToolUse&quot;: [
      {
        &quot;matcher&quot;: &quot;mcp__github__.*&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;echo \&quot;GitHub tool called: $(jq -r &#39;.tool_name&#39;)\&quot; &gt;&gt; ~/.claude/tool-log.txt&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<hr>
<h2 id="http-hook">HTTP Hook</h2>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;http&quot;,
  &quot;url&quot;: &quot;http://localhost:8080/hooks&quot;,
  &quot;headers&quot;: {
    &quot;Authorization&quot;: &quot;Bearer $MY_TOKEN&quot;
  },
  &quot;allowedEnvVars&quot;: [&quot;MY_TOKEN&quot;],
  &quot;timeout&quot;: 30
}</code></pre>
<hr>
<h2 id="유용한-환경-변수">유용한 환경 변수</h2>
<table>
<thead>
<tr>
<th>변수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>$CLAUDE_PROJECT_DIR</code></td>
<td>프로젝트 루트 디렉토리</td>
</tr>
<tr>
<td><code>$CLAUDE_PLUGIN_ROOT</code></td>
<td>플러그인 디렉토리</td>
</tr>
<tr>
<td><code>$CLAUDE_PLUGIN_DATA</code></td>
<td>플러그인 지속 데이터 디렉토리</td>
</tr>
<tr>
<td><code>$CLAUDE_ENV_FILE</code></td>
<td>환경변수 저장 파일 (SessionStart 전용)</td>
</tr>
</tbody></table>
<hr>
<h2 id="주의사항">주의사항</h2>
<ul>
<li><strong>무한 루프 방지</strong>: <code>Stop</code> hook에서 <code>stop_hook_active</code> 값 확인 필요<pre><code class="language-bash">if [ &quot;$(echo &quot;$INPUT&quot; | jq -r &#39;.stop_hook_active&#39;)&quot; = &quot;true&quot; ]; then
  exit 0
fi</code></pre>
</li>
<li><strong>jq 설치 필요</strong>: JSON 파싱에 필수 (<code>brew install jq</code> / <code>apt-get install jq</code>)</li>
<li><strong>실행 권한 설정</strong>: <code>chmod +x .claude/hooks/my-hook.sh</code></li>
<li><strong>Shell 프로필 주의</strong>: <code>~/.zshrc</code>의 unconditional echo는 hook 오작동 유발 가능</li>
</ul>
<hr>
<h2 id="관리-명령어">관리 명령어</h2>
<pre><code class="language-bash">/hooks                  # 설정된 모든 hook 확인 (읽기 전용)</code></pre>
<p>모든 hook 비활성화:</p>
<pre><code class="language-json">{
  &quot;disableAllHooks&quot;: true
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ralph로 간단한 Todo를 구현했을때 나온 문제점 ]]></title>
            <link>https://velog.io/@oi_24/Ralph%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-Todo%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%96%88%EC%9D%84%EB%95%8C-%EB%82%98%EC%98%A8-%EB%AC%B8%EC%A0%9C%EC%A0%90</link>
            <guid>https://velog.io/@oi_24/Ralph%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-Todo%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%96%88%EC%9D%84%EB%95%8C-%EB%82%98%EC%98%A8-%EB%AC%B8%EC%A0%9C%EC%A0%90</guid>
            <pubDate>Fri, 20 Mar 2026 08:42:13 GMT</pubDate>
            <description><![CDATA[<h1 id="ralph-test-프로젝트">ralph-test 프로젝트</h1>
<h2 id="1-fix_planmd를-모호하게-적으면-모호하게-만든다">1. fix_plan.md를 모호하게 적으면 모호하게 만든다</h2>
<p><strong>상황:</strong> fix_plan.md에 &quot;Django API 연동&quot;이라고만 적었다.</p>
<p><strong>결과:</strong> Ralph가 <code>localhost</code>로 하드코딩해버렸다. WSL 환경에서 브라우저로 접근하면 동작하지 않는 코드였지만 지시에 환경 조건이 없었기 때문에 Ralph는 문제로 인식하지 못했다.</p>
<p><strong>교훈:</strong> 환경 조건, 접속 방식까지 구체적으로 적어야 한다.</p>
<hr>
<h2 id="2-테스트-통과-≠-실제-동작">2. 테스트 통과 ≠ 실제 동작</h2>
<p><strong>상황:</strong> Jest 테스트 11개가 전부 통과했다.</p>
<p><strong>결과:</strong> 테스트는 서버 내부에서 <code>supertest</code>로 직접 호출하는 방식이라 <code>localhost</code>가 당연히 동작했다. 실제 브라우저에서 외부 접근하는 케이스는 테스트하지 않았고 Ralph도 그걸 만들라는 지시가 없었으니 만들지 않았다.</p>
<p><strong>교훈:</strong> 통합 테스트, E2E 테스트가 필요하면 fix_plan.md에 명시해야 한다.</p>
<hr>
<h2 id="3-환경-차이를-알려줘야-한다">3. 환경 차이를 알려줘야 한다</h2>
<p><strong>상황:</strong> WSL 환경에서 Django를 <code>127.0.0.1:8000</code>으로 실행했다.</p>
<p><strong>결과:</strong> Windows 브라우저에서 접근이 불가능했다. <code>0.0.0.0:8000</code>으로 바인딩해야 한다는 걸 Ralph는 몰랐고 CLAUDE.md나 fix_plan.md에도 적혀있지 않았다.</p>
<p><strong>교훈:</strong> 실행 환경(OS, 네트워크 구성 등)을 CLAUDE.md에 명시해야 한다.</p>
<hr>
<h2 id="4-p3를-구체적으로-적지-않으면-껍데기만-나온다">4. P3를 구체적으로 적지 않으면 껍데기만 나온다</h2>
<p><strong>상황:</strong> fix_plan.md P3에 &quot;HTML 기본 구조&quot;, &quot;API 연동&quot; 수준으로만 적었다.</p>
<p><strong>결과:</strong> 화면은 만들어졌지만 실제 동작하는 기능이 없는 껍데기였다.</p>
<p><strong>교훈:</strong> &quot;회원가입 → 로그인 → JWT 저장 → Todo CRUD&quot; 처럼 흐름을 구체적으로 적어줬을 때 비로소 제대로 구현됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ralph의 한계]]></title>
            <link>https://velog.io/@oi_24/Ralph%EC%9D%98-%ED%95%9C%EA%B3%84</link>
            <guid>https://velog.io/@oi_24/Ralph%EC%9D%98-%ED%95%9C%EA%B3%84</guid>
            <pubDate>Fri, 20 Mar 2026 08:37:43 GMT</pubDate>
            <description><![CDATA[<h1 id="ralph의-한계">Ralph의 한계</h1>
<h2 id="1-지시한-것만-한다">1. 지시한 것만 한다</h2>
<p>fix_plan.md에 적힌 것만 구현한다.
모호하게 적으면 모호하게 만들고, 누락한 건 신경 쓰지 않는다.
결국 <strong>품질은 fix_plan.md를 얼마나 잘 쓰느냐</strong>에 달려있다.</p>
<h2 id="2-테스트-범위를-스스로-설계하지-못한다">2. 테스트 범위를 스스로 설계하지 못한다</h2>
<p>단위 테스트는 잘 작성하지만, 통합 테스트/E2E 테스트는 명시하지 않으면 안 만든다.
테스트가 통과해도 실제 환경에서 동작하지 않을 수 있다.</p>
<h2 id="3-환경-차이를-모른다">3. 환경 차이를 모른다</h2>
<p>코드가 실행되는 환경(OS, 네트워크, 인프라)에 대한 맥락이 없다.
개발환경과 운영환경의 차이, 팀마다 다른 인프라 구성은 사람이 알려줘야 한다.</p>
<h2 id="4-도메인-지식이-없다">4. 도메인 지식이 없다</h2>
<p>비즈니스 규칙, 팀 컨벤션, 히스토리는 CLAUDE.md나 fix_plan.md에 적어줘야 한다.
적지 않으면 일반적인 방식으로 구현한다.</p>
<h2 id="5-막히면-건너뛴다">5. 막히면 건너뛴다</h2>
<p>연속 3회 같은 에러가 발생하면 해당 항목을 포기하고 넘어간다.
근본 원인을 해결하지 못하는 문제는 사람이 직접 봐야 한다.</p>
<h2 id="6-코드-리뷰를-스스로-못-한다">6. 코드 리뷰를 스스로 못 한다</h2>
<p>테스트와 린트를 통과하면 완료로 판단한다.
보안 취약점, 성능 문제, 유지보수성 같은 건 사람이 리뷰해야 한다.</p>
<hr>
<h2 id="한마디로">한마디로</h2>
<blockquote>
<p><strong>Ralph는 실행력은 뛰어나지만 판단력은 없다.</strong></p>
<p>무엇을 만들지, 어떻게 검증할지, 어떤 환경에서 동작해야 하는지는 사람이 설계해줘야 한다. Ralph는 그 설계를 빠르게 구현하는 도구다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 보려는 Ralph 테스트 가이드]]></title>
            <link>https://velog.io/@oi_24/%EB%82%B4%EA%B0%80-%EB%B3%B4%EB%A0%A4%EB%8A%94-Ralph-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@oi_24/%EB%82%B4%EA%B0%80-%EB%B3%B4%EB%A0%A4%EB%8A%94-Ralph-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Fri, 20 Mar 2026 07:02:31 GMT</pubDate>
            <description><![CDATA[<h1 id="ralph-사용-가이드">Ralph 사용 가이드</h1>
<h2 id="프로젝트-구조">프로젝트 구조</h2>
<pre><code>ralph-test/
├── CLAUDE.md                  # Claude가 자동으로 읽는 프로젝트 설명서
├── PROJECT_STRUCTURE.md       # 구조 설명 문서
├── RALPH_GUIDE.md             # 이 파일 — Ralph 사용 가이드
├── .gitignore
│
├── .claude/
│   └── settings.json          # Claude 권한 + Stop Hook 설정
│
├── .ralph/
│   ├── fix_plan.md            # Ralph 할 일 목록
│   └── PROMPT.md              # Ralph 지시문
│
├── scripts/
│   ├── ralph-loop.sh          # Ralph 실행 스크립트
│   └── ralph-stop-hook.sh     # 완료 조건 검증 Hook
│
├── django-api/
│   ├── .venv/                 # Python 가상환경
│   └── requirements.txt       # Python 패키지 목록
│
└── node-api/
    ├── node_modules/          # Node 패키지
    ├── package.json
    └── eslint.config.js</code></pre><hr>
<h2 id="ralph를-쓰기-위해-필요한-것">Ralph를 쓰기 위해 필요한 것</h2>
<h3 id="1-claudemd">1. CLAUDE.md</h3>
<p>&quot;이 프로젝트가 뭔지&quot; Claude에게 알려주는 파일.
Claude는 세션 시작 시 이 파일을 자동으로 읽고 프로젝트 컨텍스트를 파악한다.</p>
<h3 id="2-ralphfix_planmd">2. .ralph/fix_plan.md</h3>
<p>&quot;뭘 만들어야 하는지&quot; 할 일 목록. Ralph가 이 파일을 보고 순서대로 구현한다.</p>
<pre><code class="language-markdown">## P1: 긴급 (반드시 완료)
- [ ] 미완료 항목  ← Claude가 구현
- [x] 완료 항목   ← 구현 + 테스트 통과 후 Claude가 직접 체크

## P2: 중요
- [ ] ...

## P3: 개선
- [ ] ...</code></pre>
<h3 id="3-ralphpromptmd">3. .ralph/PROMPT.md</h3>
<p>&quot;어떻게 일해야 하는지&quot; Claude에게 주는 지시문.
역할, 작업 순서, 완료 조건, 절대 금지 사항을 정의한다.</p>
<h3 id="4-claudesettingsjson">4. .claude/settings.json</h3>
<p>Claude가 실행할 수 있는 명령어(allow)와 없는 명령어(deny), 그리고 Stop Hook 연결을 정의한다.</p>
<pre><code class="language-json">{
  &quot;permissions&quot;: {
    &quot;allow&quot;: [&quot;pytest 실행&quot;, &quot;git commit&quot;, ...],
    &quot;deny&quot;: [&quot;rm -rf&quot;, &quot;git push force&quot;, ...]
  },
  &quot;hooks&quot;: {
    &quot;Stop&quot;: [{ &quot;command&quot;: &quot;bash scripts/ralph-stop-hook.sh&quot; }]
  }
}</code></pre>
<h3 id="5-scriptsralph-stop-hooksh">5. scripts/ralph-stop-hook.sh</h3>
<p>Claude가 멈추려 할 때마다 자동 실행되는 검증 스크립트.
모든 조건을 통과해야만 Claude가 종료할 수 있다.</p>
<pre><code>미완료 항목 있음?     → block (계속 작업하라)
Django 테스트 실패?  → block (고쳐라)
Django 린트 에러?    → block (고쳐라)
Node 테스트 실패?    → block (고쳐라)
Node 린트 에러?      → block (고쳐라)
전부 통과?           → allow  (종료 허용)
max-iterations 초과? → allow  (강제 종료)</code></pre><h3 id="6-환경-세팅">6. 환경 세팅</h3>
<p>Claude가 실제로 테스트/린트를 실행할 수 있는 환경.</p>
<table>
<thead>
<tr>
<th>환경</th>
<th>설정</th>
<th>도구</th>
</tr>
</thead>
<tbody><tr>
<td>Django</td>
<td><code>.venv/</code> 가상환경</td>
<td>pytest, ruff</td>
</tr>
<tr>
<td>Node.js</td>
<td><code>node_modules/</code></td>
<td>jest, eslint</td>
</tr>
</tbody></table>
<hr>
<h2 id="ralph-실행-방법">Ralph 실행 방법</h2>
<pre><code class="language-bash"># 기본 실행 (최대 30회)
bash scripts/ralph-loop.sh

# 반복 횟수 지정
bash scripts/ralph-loop.sh --max-iterations 10

# 오버나이트 (백그라운드)
nohup bash scripts/ralph-loop.sh --max-iterations 50 &gt; logs/ralph-session.log 2&gt;&amp;1 &amp;</code></pre>
<hr>
<h2 id="전체-실행-흐름">전체 실행 흐름</h2>
<pre><code>사람이 fix_plan.md 작성
        ↓
bash scripts/ralph-loop.sh 실행
        ↓
새 브랜치 생성 (ralph/run-YYYYMMDD-HHMM)
        ↓
PROMPT.md → Claude에게 전달
        ↓
┌─── Claude 루프 시작 ──────────────────────┐
│  fix_plan.md 읽기 → 미완료 항목 선택       │
│  테스트 먼저 작성 (TDD — Red)              │
│  구현 (Green)                             │
│  테스트 + 린트 실행                        │
│  통과 → fix_plan.md [x] 표시 + git commit │
│  완료 시도                                 │
│         ↓                                 │
│  ralph-stop-hook.sh 실행                  │
│  ├─ 미통과 → block → 루프 계속 ───────────┘
│  └─ 통과   → allow → 종료
└───────────────────────────────────────────</code></pre><hr>
<h2 id="max-iterations란">max-iterations란?</h2>
<p>Ralph가 최대 몇 번 반복할지 제한하는 숫자.
Claude가 작업 완료를 시도할 때마다 1회로 카운트된다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 값</th>
</tr>
</thead>
<tbody><tr>
<td>처음 테스트, 빠르게 확인</td>
<td><code>5~10</code></td>
</tr>
<tr>
<td>기능 몇 개 구현</td>
<td><code>20~30</code></td>
</tr>
<tr>
<td>오버나이트 풀 개발</td>
<td><code>50</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="결과-확인-방법">결과 확인 방법</h2>
<pre><code class="language-bash"># 어디까지 커밋됐는지
git log --oneline

# 항목별 완료 상태 확인
cat .ralph/fix_plan.md

# [x] 완료
# [!] 3회 연속 실패로 건너뜀 → 사람이 직접 확인 필요
# [ ] 미완료 → fix_plan.md 그대로 두고 다시 Ralph 실행</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Data Lakehouse 핵심 구성요소 정리]]></title>
            <link>https://velog.io/@oi_24/Data-Lakehouse-%ED%95%B5%EC%8B%AC-%EA%B5%AC%EC%84%B1%EC%9A%94%EC%86%8C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@oi_24/Data-Lakehouse-%ED%95%B5%EC%8B%AC-%EA%B5%AC%EC%84%B1%EC%9A%94%EC%86%8C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 10 Mar 2026 14:44:40 GMT</pubDate>
            <description><![CDATA[<h1 id="minio">MinIO</h1>
<p>S3 호환 오브젝트 스토리지다. AWS S3와 API가 동일해서 boto3 같은 S3 라이브러리를 그대로 쓸 수 있다.
로컬 또는 온프레미스 환경에서 S3처럼 파일을 저장하고 싶을 때 사용한다.</p>
<p>이 프로젝트에서는 Parquet 파일을 저장하는 스토리지로 사용한다.</p>
<hr>
<h1 id="apache-parquet">Apache Parquet</h1>
<p>데이터를 저장하는 파일 형식이다. 컬럼 기반으로 저장하기 때문에 특정 컬럼만 읽는 분석 쿼리에 유리하다.
JSON이나 CSV보다 압축률이 높고 읽기 성능이 좋다.</p>
<hr>
<h1 id="apache-iceberg">Apache Iceberg</h1>
<p><strong>테이블 포맷 스펙이다. 실행되는 서비스가 아니다.</strong></p>
<p>MinIO에 Parquet 파일이 쌓이면 그냥 파일 더미다. 어떤 파일이 어떤 테이블인지, 컬럼이 뭔지, 언제 추가됐는지 알 방법이 없다.</p>
<p>Iceberg는 Parquet 파일들을 테이블처럼 관리할 수 있게 해주는 규칙이다.</p>
<h2 id="iceberg가-관리하는-파일-구조">Iceberg가 관리하는 파일 구조</h2>
<pre><code>system_metrics/
├── data/
│   ├── 00000.parquet
│   └── 00001.parquet
└── metadata/
    ├── snap-001.avro        → 스냅샷 정보
    └── v1.metadata.json     → 테이블 메타데이터</code></pre><p><strong>스냅샷 정보</strong></p>
<p>특정 시점에 어떤 Parquet 파일이 테이블에 속했는지 기록한다. 이게 있어서 특정 시점의 데이터를 조회하는 Time Travel이 가능하다.</p>
<p><strong>테이블 메타데이터</strong></p>
<p>컬럼 이름/타입, 파티션 방식, 스키마 변경 이력, 현재 스냅샷 정보를 담고 있다.</p>
<h2 id="iceberg를-쓰는-이유">Iceberg를 쓰는 이유</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>경로 기반 관리</th>
<th>Iceberg</th>
</tr>
</thead>
<tbody><tr>
<td>스키마 변경</td>
<td>기존 파일 전부 재작성</td>
<td>자동 처리</td>
</tr>
<tr>
<td>Time Travel</td>
<td>불가</td>
<td>가능</td>
</tr>
<tr>
<td>파티션 등록</td>
<td>수동 (MSCK REPAIR TABLE)</td>
<td>자동</td>
</tr>
<tr>
<td>작은 파일 관리</td>
<td>수동</td>
<td>자동 compaction</td>
</tr>
</tbody></table>
<hr>
<h1 id="hive-metastore">Hive Metastore</h1>
<p>Iceberg 테이블의 메타데이터 위치를 저장하는 서비스다.</p>
<p>Trino가 쿼리를 실행할 때 다음 순서로 동작한다.</p>
<ol>
<li>Hive Metastore에 &quot;system_metrics 테이블 메타데이터 파일이 어디있어?&quot; 질문</li>
<li>Hive Metastore가 MinIO 경로 반환</li>
<li>Trino가 그 경로의 메타데이터 파일을 읽어서 어떤 Parquet 파일을 읽어야 하는지 파악</li>
<li>MinIO에서 실제 Parquet 파일을 읽어서 쿼리 실행</li>
</ol>
<p>Hive Metastore 자체의 메타데이터는 DB에 저장된다. 토이 프로젝트에서는 Derby(내장 DB), 프로덕션에서는 PostgreSQL 또는 MySQL을 사용한다.</p>
<hr>
<h1 id="trino">Trino</h1>
<p>분산 SQL 쿼리 엔진이다. Iceberg 포맷을 이해하고 MinIO의 Parquet 파일을 SQL로 조회할 수 있다.</p>
<pre><code class="language-sql">SELECT * FROM iceberg.windows_logs.system_metrics
WHERE timestamp &gt; &#39;2026-03-10&#39;
LIMIT 100;</code></pre>
<p>Trino 자체는 데이터를 저장하지 않는다. MinIO에 있는 파일을 읽어서 쿼리만 실행한다.</p>
<hr>
<h1 id="전체-흐름-요약">전체 흐름 요약</h1>
<p><strong>데이터 적재 시</strong></p>
<p>Consumer가 PyIceberg로 Parquet 파일을 MinIO에 저장하고, Iceberg 메타데이터를 업데이트한 뒤 Hive Metastore에 위치를 등록한다.</p>
<p><strong>쿼리 실행 시</strong></p>
<p>Trino가 Hive Metastore에서 메타데이터 위치를 확인하고, MinIO에서 메타데이터와 Parquet 파일을 읽어서 쿼리 결과를 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB vs 데이터 웨어하우스 vs 데이터 레이크]]></title>
            <link>https://velog.io/@oi_24/DB-vs-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9B%A8%EC%96%B4%ED%95%98%EC%9A%B0%EC%8A%A4-vs-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A0%88%EC%9D%B4%ED%81%AC</link>
            <guid>https://velog.io/@oi_24/DB-vs-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9B%A8%EC%96%B4%ED%95%98%EC%9A%B0%EC%8A%A4-vs-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A0%88%EC%9D%B4%ED%81%AC</guid>
            <pubDate>Mon, 09 Mar 2026 14:50:07 GMT</pubDate>
            <description><![CDATA[<table>
<thead>
<tr>
<th>저장소</th>
<th>한 줄 정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DB (데이터베이스)</strong></td>
<td>현재 운영에 필요한 데이터를 빠르게 읽고 쓰기 위한 저장소</td>
</tr>
<tr>
<td><strong>데이터 웨어하우스</strong></td>
<td>분석을 위해 정제·구조화된 데이터를 보관하는 저장소</td>
</tr>
<tr>
<td><strong>데이터 레이크</strong></td>
<td>원시 데이터를 형식에 상관없이 대량으로 쌓아두는 저장소</td>
</tr>
</tbody></table>
<hr>
<h2 id="쇼핑몰-데이터-예시">쇼핑몰 데이터 예시</h2>
<h3 id="db-일반-데이터베이스">DB (일반 데이터베이스)</h3>
<p>쇼핑몰 서비스가 실시간으로 읽고 쓰는 운영 데이터.</p>
<p><strong>users 테이블</strong></p>
<table>
<thead>
<tr>
<th>user_id</th>
<th>name</th>
<th>email</th>
<th>created_at</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김철수</td>
<td><a href="mailto:kim@example.com">kim@example.com</a></td>
<td>2025-01-03</td>
</tr>
<tr>
<td>2</td>
<td>이영희</td>
<td><a href="mailto:lee@example.com">lee@example.com</a></td>
<td>2025-01-10</td>
</tr>
</tbody></table>
<p><strong>orders 테이블</strong></p>
<table>
<thead>
<tr>
<th>order_id</th>
<th>user_id</th>
<th>total_amount</th>
<th>status</th>
<th>ordered_at</th>
</tr>
</thead>
<tbody><tr>
<td>1001</td>
<td>1</td>
<td>35000</td>
<td>배송완료</td>
<td>2025-03-01</td>
</tr>
<tr>
<td>1002</td>
<td>2</td>
<td>12000</td>
<td>결제완료</td>
<td>2025-03-05</td>
</tr>
</tbody></table>
<p><strong>products 테이블</strong></p>
<table>
<thead>
<tr>
<th>product_id</th>
<th>name</th>
<th>price</th>
<th>stock</th>
</tr>
</thead>
<tbody><tr>
<td>201</td>
<td>무선 마우스</td>
<td>25000</td>
<td>120</td>
</tr>
<tr>
<td>202</td>
<td>USB 허브</td>
<td>18000</td>
<td>45</td>
</tr>
</tbody></table>
<blockquote>
<p>목적: 주문 처리, 재고 확인, 회원 인증 등 서비스 운영에 필요한 CRUD 작업.</p>
</blockquote>
<hr>
<h3 id="데이터-웨어하우스">데이터 웨어하우스</h3>
<p>DB에서 추출한 데이터를 분석 목적에 맞게 변환·적재한 구조.
스타 스키마(Star Schema)를 주로 사용함.</p>
<p><strong>fact_orders 테이블</strong> (사실 테이블)</p>
<table>
<thead>
<tr>
<th>order_id</th>
<th>user_id</th>
<th>product_id</th>
<th>date_id</th>
<th>quantity</th>
<th>revenue</th>
</tr>
</thead>
<tbody><tr>
<td>1001</td>
<td>1</td>
<td>201</td>
<td>20250301</td>
<td>1</td>
<td>25000</td>
</tr>
<tr>
<td>1002</td>
<td>2</td>
<td>202</td>
<td>20250305</td>
<td>2</td>
<td>36000</td>
</tr>
</tbody></table>
<p><strong>dim_date 테이블</strong> (날짜 차원 테이블)</p>
<table>
<thead>
<tr>
<th>date_id</th>
<th>date</th>
<th>year</th>
<th>month</th>
<th>week</th>
<th>is_weekend</th>
</tr>
</thead>
<tbody><tr>
<td>20250301</td>
<td>2025-03-01</td>
<td>2025</td>
<td>3</td>
<td>9</td>
<td>false</td>
</tr>
<tr>
<td>20250305</td>
<td>2025-03-05</td>
<td>2025</td>
<td>3</td>
<td>10</td>
<td>false</td>
</tr>
</tbody></table>
<p><strong>분석 쿼리 예시</strong></p>
<pre><code class="language-sql">-- 월별 매출 합계
SELECT
    d.year,
    d.month,
    SUM(f.revenue) AS monthly_revenue
FROM fact_orders f
JOIN dim_date d ON f.date_id = d.date_id
GROUP BY d.year, d.month
ORDER BY d.year, d.month;</code></pre>
<pre><code class="language-sql">-- 상품별 판매량 순위
SELECT
    p.name AS product_name,
    SUM(f.quantity) AS total_quantity
FROM fact_orders f
JOIN dim_product p ON f.product_id = p.product_id
GROUP BY p.name
ORDER BY total_quantity DESC;</code></pre>
<blockquote>
<p>목적: 월별 매출 집계, 상품 판매 순위, 유저 구매 패턴 분석 등 BI/리포팅 작업.</p>
</blockquote>
<hr>
<h3 id="데이터-레이크">데이터 레이크</h3>
<p>원시 데이터(로그, 이미지, JSON 등)를 그대로 저장. MinIO 또는 S3 기반.</p>
<p><strong>디렉토리 구조 예시</strong></p>
<pre><code>s3://shopping-datalake/
├── raw/
│   ├── logs/
│   │   ├── 2025/03/01/access.log.gz      # 웹 서버 접근 로그
│   │   └── 2025/03/05/access.log.gz
│   ├── events/
│   │   ├── 2025/03/01/click_events.json  # 클릭 스트림 이벤트
│   │   └── 2025/03/05/click_events.json
│   └── db_snapshot/
│       └── 2025/03/01/orders.parquet     # DB 스냅샷
├── processed/
│   ├── user_behavior/
│   │   └── 2025/03/session_features.parquet
│   └── product_recommendation/
│       └── model_input_2025_03.csv
└── ml_models/
    └── recommendation/
        └── v1.2/model.pkl</code></pre><p><strong>활용 예시</strong></p>
<table>
<thead>
<tr>
<th>데이터</th>
<th>활용 목적</th>
</tr>
</thead>
<tbody><tr>
<td><code>access.log.gz</code></td>
<td>유입 경로 분석, 이상 트래픽 탐지</td>
</tr>
<tr>
<td><code>click_events.json</code></td>
<td>상품 클릭률, 전환율 분석</td>
</tr>
<tr>
<td><code>orders.parquet</code></td>
<td>장기 구매 패턴 분석, ML 학습 데이터</td>
</tr>
<tr>
<td><code>session_features.parquet</code></td>
<td>추천 모델 피처 생성</td>
</tr>
<tr>
<td><code>model.pkl</code></td>
<td>실시간 추천 API에서 로드하여 서빙</td>
</tr>
</tbody></table>
<blockquote>
<p>목적: 머신러닝 학습, 대규모 로그 분석, 장기 데이터 아카이빙.</p>
</blockquote>
<hr>
<h2 id="전체-데이터-흐름">전체 데이터 흐름</h2>
<pre><code>[쇼핑몰 서비스]
      |
      | 실시간 읽기/쓰기
      v
  [DB (MySQL/PostgreSQL)]
      |
      |-- ETL/CDC --&gt; [데이터 웨어하우스 (Redshift/BigQuery)]
      |                        |
      |                        v
      |                   BI 대시보드 / SQL 분석
      |
      |-- 로그/이벤트 --&gt; [데이터 레이크 (S3/MinIO)]
                                  |
                                  v
                         ML 학습 / 대용량 배치 분석</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>DB</th>
<th>데이터 웨어하우스</th>
<th>데이터 레이크</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 형태</td>
<td>정형</td>
<td>정형</td>
<td>정형 + 비정형</td>
</tr>
<tr>
<td>주요 작업</td>
<td>CRUD</td>
<td>SELECT (집계)</td>
<td>배치 처리, ML</td>
</tr>
<tr>
<td>응답 속도</td>
<td>밀리초 단위</td>
<td>초~분 단위</td>
<td>분~시간 단위</td>
</tr>
<tr>
<td>저장 비용</td>
<td>높음</td>
<td>중간</td>
<td>낮음</td>
</tr>
<tr>
<td>대표 기술</td>
<td>MySQL, PostgreSQL</td>
<td>BigQuery, Redshift</td>
<td>S3, MinIO, HDFS</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[DRF의 ListCreateAPIView는 내부에서 어떻게 동작하는가]]></title>
            <link>https://velog.io/@oi_24/DRF%EC%9D%98-ListCreateAPIView%EB%8A%94-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@oi_24/DRF%EC%9D%98-ListCreateAPIView%EB%8A%94-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Mon, 09 Mar 2026 14:32:26 GMT</pubDate>
            <description><![CDATA[<p>Django REST Framework(DRF)를 처음 쓰면 <code>generics.ListCreateAPIView</code> 한 줄로 GET/POST가 다 된다는 게 신기하면서도 불안하다. 뭔가 마법처럼 동작하는 것 같아서 문제가 생기면 어디서 봐야 할지 모른다.</p>
<p>이 글에서는 <code>ListCreateAPIView</code>가 요청을 받았을 때 내부에서 어떤 경로로 실행되는지 소스 코드를 따라가며 정리한다.</p>
<hr>
<h2 id="예시-코드">예시 코드</h2>
<pre><code class="language-python">class ScrapeTaskListCreateView(generics.ListCreateAPIView):
    queryset = ScrapeTask.objects.all().order_by(&#39;-created_at&#39;)
    serializer_class = ScrapeTaskSerializer</code></pre>
<p>이게 전부다. 근데 GET 요청을 보내면 목록이 나오고, POST를 보내면 생성이 된다. 어떻게?</p>
<hr>
<h2 id="상속-구조">상속 구조</h2>
<pre><code>View (Django 기본)
  └── APIView
        └── GenericAPIView
              └── ListModelMixin + CreateModelMixin
                    └── ListCreateAPIView</code></pre><p>DRF의 generic view는 여러 클래스를 조합해서 만들어진다.</p>
<pre><code class="language-python"># rest_framework/generics.py
class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)</code></pre>
<p><code>get()</code>은 <code>self.list()</code>로, <code>post()</code>는 <code>self.create()</code>로 위임한다. <code>list()</code>와 <code>create()</code>는 각각 <code>ListModelMixin</code>, <code>CreateModelMixin</code>에 정의되어 있다.</p>
<hr>
<h2 id="요청-흐름-추적">요청 흐름 추적</h2>
<h3 id="1-urlspy에서-as_view-호출">1. urls.py에서 <code>.as_view()</code> 호출</h3>
<pre><code class="language-python">path(&quot;tasks/&quot;, ScrapeTaskListCreateView.as_view())</code></pre>
<p><code>as_view()</code>는 <code>View</code>(Django 기본)에 정의된 클래스 메서드다. 호출하면 클래스를 함수처럼 쓸 수 있는 <code>view</code> 함수를 반환한다. Django 라우터는 함수만 받기 때문에 이 변환이 필요하다.</p>
<h3 id="2-요청이-들어오면-dispatch-실행">2. 요청이 들어오면 <code>dispatch()</code> 실행</h3>
<p>HTTP 요청이 들어오면 <code>APIView.dispatch()</code>가 호출된다.</p>
<pre><code class="language-python"># rest_framework/views.py
def dispatch(self, request, *args, **kwargs):
    request = self.initialize_request(request, *args, **kwargs)
    self.initial(request, *args, **kwargs)  # 인증, 권한, 쓰로틀 체크

    if request.method.lower() in self.http_method_names:
        handler = getattr(self, request.method.lower(), self.http_method_not_allowed)

    response = handler(request, *args, **kwargs)
    return self.finalize_response(request, response, *args, **kwargs)</code></pre>
<p><code>request.method.lower()</code>로 HTTP 메서드를 소문자로 변환한 뒤, 같은 이름의 메서드를 자기 자신에서 찾는다. GET이면 <code>self.get</code>, POST면 <code>self.post</code>.</p>
<h3 id="3-get-→-list-실행">3. <code>get()</code> → <code>list()</code> 실행</h3>
<pre><code class="language-python"># rest_framework/mixins.py
class ListModelMixin:
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)</code></pre>
<p><code>get_queryset()</code>은 클래스에 선언한 <code>queryset</code>을 반환한다. <code>get_serializer()</code>는 <code>serializer_class</code>로 시리얼라이저 인스턴스를 만든다. <code>serializer.data</code>는 Python 딕셔너리로 변환된 데이터고, <code>Response</code>가 이걸 JSON으로 렌더링한다.</p>
<h3 id="4-post-→-create-실행">4. <code>post()</code> → <code>create()</code> 실행</h3>
<pre><code class="language-python"># rest_framework/mixins.py
class CreateModelMixin:
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def perform_create(self, serializer):
        serializer.save()</code></pre>
<p><code>request.data</code>로 요청 바디를 받아서 유효성 검사 후 저장한다. <code>perform_create()</code>를 별도로 분리한 이유는 오버라이드하기 쉽게 하기 위해서다. 예를 들어 저장 후 Celery 태스크를 발행하고 싶다면:</p>
<pre><code class="language-python">def perform_create(self, serializer):
    task = serializer.save()
    scrape_url.delay(task.id)</code></pre>
<hr>
<h2 id="get_serializer_class는-언제-쓰나"><code>get_serializer_class()</code>는 언제 쓰나</h2>
<p><code>serializer_class</code>를 하나만 선언하면 GET/POST 모두 같은 시리얼라이저를 사용한다. GET 응답에는 전체 필드를 주고, POST 요청에는 일부 필드만 받고 싶다면 <code>get_serializer_class()</code>를 오버라이드한다.</p>
<pre><code class="language-python">class ScrapeTaskListCreateView(generics.ListCreateAPIView):
    queryset = ScrapeTask.objects.all()

    def get_serializer_class(self):
        if self.request.method == &quot;POST&quot;:
            return ScrapeTaskCreateSerializer
        return ScrapeTaskSerializer</code></pre>
<p><code>get_serializer()</code>가 내부에서 <code>get_serializer_class()</code>를 호출하기 때문에, 메서드에 따라 다른 시리얼라이저가 선택된다.</p>
<hr>
<h2 id="함수형-뷰와-비교">함수형 뷰와 비교</h2>
<p>같은 기능을 함수형 뷰로 작성하면 흐름이 훨씬 명확하다.</p>
<pre><code class="language-python">@api_view([&quot;GET&quot;, &quot;POST&quot;])
def task_list(request):
    if request.method == &quot;GET&quot;:
        tasks = ScrapeTask.objects.all().order_by(&quot;-created_at&quot;)
        serializer = ScrapeTaskSerializer(tasks, many=True)
        return Response(serializer.data)

    elif request.method == &quot;POST&quot;:
        serializer = ScrapeTaskCreateSerializer(data=request.data)
        if serializer.is_valid():
            task = serializer.save()
            scrape_url.delay(task.id)
            return Response(ScrapeTaskSerializer(task).data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)</code></pre>
<p>Generic View는 이 코드를 추상화한 것이다. 코드량은 줄지만 내부 동작을 모르면 커스터마이즈할 때 막힌다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>as_view()</code></td>
<td>클래스를 Django 라우터가 받을 수 있는 함수로 변환</td>
</tr>
<tr>
<td><code>dispatch()</code></td>
<td>HTTP 메서드에 맞는 핸들러(<code>get</code>, <code>post</code> 등)를 찾아 실행</td>
</tr>
<tr>
<td><code>list()</code></td>
<td>queryset 조회 → 시리얼라이즈 → Response 반환</td>
</tr>
<tr>
<td><code>create()</code></td>
<td>요청 데이터 유효성 검사 → 저장 → Response 반환</td>
</tr>
<tr>
<td><code>get_serializer_class()</code></td>
<td>GET/POST 별로 다른 시리얼라이저를 쓰고 싶을 때 오버라이드</td>
</tr>
<tr>
<td><code>perform_create()</code></td>
<td>저장 후 추가 작업(태스크 발행 등)을 끼워 넣을 때 오버라이드</td>
</tr>
</tbody></table>
<p>Generic View를 쓸 때 뭔가 안 되면 <code>dispatch</code> → <code>get</code>/<code>post</code> → <code>list</code>/<code>create</code> 순으로 소스 코드를 따라가면 대부분 원인을 찾을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Claude MCP 서버로 PostgreSQL 연동]]></title>
            <link>https://velog.io/@oi_24/Claude-MCP-%EC%84%9C%EB%B2%84%EB%A1%9C-PostgreSQL-%EC%97%B0%EB%8F%99%ED%95%98</link>
            <guid>https://velog.io/@oi_24/Claude-MCP-%EC%84%9C%EB%B2%84%EB%A1%9C-PostgreSQL-%EC%97%B0%EB%8F%99%ED%95%98</guid>
            <pubDate>Tue, 10 Feb 2026 02:54:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Claude에서 MCP 서버를 구축하고 PostgreSQL 데이터베이스를 연동하는 실전 가이드</p>
</blockquote>
<hr>
<h2 id="mcp란-무엇인가">MCP란 무엇인가</h2>
<p><strong>MCP(Model Context Protocol)</strong> 는 Anthropic에서 개발한 표준 프로토콜로, Claude와 외부 데이터 소스를 연결하는 인터페이스임.</p>
<h3 id="핵심-개념">핵심 개념</h3>
<p>MCP는 Claude가 외부 시스템과 통신할 수 있도록 하는 브릿지 역할을 함. 데이터베이스, API, 파일 시스템 등 다양한 데이터 소스에 접근 가능함.</p>
<pre><code>┌─────────────┐         ┌─────────────┐         ┌──────────────┐
│             │         │             │         │              │
│   Claude    │ ◄─────► │ MCP Server  │ ◄─────► │  PostgreSQL  │
│             │         │             │         │              │
└─────────────┘         └─────────────┘         └──────────────┘
    자연어 질의          프로토콜 변환            SQL 실행</code></pre><h3 id="mcp의-장점">MCP의 장점</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>표준화</strong></td>
<td>통일된 인터페이스로 다양한 데이터 소스 연결</td>
</tr>
<tr>
<td><strong>안전성</strong></td>
<td>읽기 전용 접근으로 데이터 무결성 보장</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>플러그인 방식으로 쉽게 확장 가능</td>
</tr>
<tr>
<td><strong>편의성</strong></td>
<td>자연어로 데이터 조회 및 분석 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="환경-구축하기">환경 구축하기</h2>
<h3 id="필수-요구사항">필수 요구사항</h3>
<p>MCP 서버 구축을 위해 다음 도구가 필요함:</p>
<ul>
<li><strong>Node.js</strong>: MCP 서버 실행 환경</li>
<li><strong>Claude Code CLI</strong>: Claude 명령줄 도구</li>
<li><strong>PostgreSQL</strong>: 연동할 데이터베이스 인스턴스</li>
</ul>
<h3 id="nodejs-설치-확인">Node.js 설치 확인</h3>
<pre><code class="language-bash">node --version
npm --version</code></pre>
<h3 id="postgresql-설치-확인">PostgreSQL 설치 확인</h3>
<pre><code class="language-bash">psql --version</code></pre>
<hr>
<h2 id="mcp-서버-설정">MCP 서버 설정</h2>
<h3 id="1-설정-파일-위치">1. 설정 파일 위치</h3>
<p>Claude의 MCP 설정 파일은 다음 위치에 있음:</p>
<pre><code>~/.claude/config.json</code></pre><h3 id="2-postgresql-mcp-서버-추가">2. PostgreSQL MCP 서버 추가</h3>
<p>설정 파일을 열고 다음 내용을 추가함:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://localhost/mydb&quot;
      ]
    }
  }
}</code></pre>
<p><strong>설정 항목 설명:</strong></p>
<ul>
<li><code>&quot;postgres&quot;</code>: MCP 서버의 이름 (임의로 지정 가능)</li>
<li><code>&quot;command&quot;</code>: 실행할 명령어 (<code>npx</code>는 npm 패키지를 즉시 실행)</li>
<li><code>&quot;args&quot;</code>: 명령어 인자<ul>
<li><code>-y</code>: 자동으로 yes 응답</li>
<li><code>@modelcontextprotocol/server-postgres</code>: PostgreSQL MCP 서버 패키지</li>
<li><code>postgresql://localhost/mydb</code>: PostgreSQL 연결 URL</li>
</ul>
</li>
</ul>
<h3 id="3-연결-문자열-설정">3. 연결 문자열 설정</h3>
<p>PostgreSQL 연결 문자열 형식:</p>
<pre><code>postgresql://[username[:password]@][host][:port][/database]</code></pre><p><strong>로컬 개발 환경:</strong></p>
<pre><code class="language-json">&quot;args&quot;: [
  &quot;-y&quot;,
  &quot;@modelcontextprotocol/server-postgres&quot;,
  &quot;postgresql://localhost/mydb&quot;
]</code></pre>
<p><strong>사용자 인증:</strong></p>
<pre><code class="language-json">&quot;args&quot;: [
  &quot;-y&quot;,
  &quot;@modelcontextprotocol/server-postgres&quot;,
  &quot;postgresql://user:password@localhost/mydb&quot;
]</code></pre>
<p><strong>원격 서버:</strong></p>
<pre><code class="language-json">&quot;args&quot;: [
  &quot;-y&quot;,
  &quot;@modelcontextprotocol/server-postgres&quot;,
  &quot;postgresql://user:password@remote-host:5432/mydb&quot;
]</code></pre>
<p><strong>SSL 연결:</strong></p>
<pre><code class="language-json">&quot;args&quot;: [
  &quot;-y&quot;,
  &quot;@modelcontextprotocol/server-postgres&quot;,
  &quot;postgresql://user:password@remote-host:5432/mydb?sslmode=require&quot;
]</code></pre>
<h3 id="4-여러-mcp-서버-설정">4. 여러 MCP 서버 설정</h3>
<p>여러 데이터베이스를 동시에 연결할 수 있음:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres-prod&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://user:pass@prod-server:5432/production&quot;
      ]
    },
    &quot;postgres-dev&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://localhost/development&quot;
      ]
    },
    &quot;postgres-test&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://localhost/test&quot;
      ]
    }
  }
}</code></pre>
<hr>
<h2 id="mcp-서버-시작-및-확인">MCP 서버 시작 및 확인</h2>
<h3 id="1-claude-재시작">1. Claude 재시작</h3>
<p>설정 파일을 수정한 후 Claude를 재시작해야 함:</p>
<pre><code class="language-bash"># Claude 프로세스 종료 후 다시 시작
claude</code></pre>
<h3 id="2-mcp-서버-연결-확인">2. MCP 서버 연결 확인</h3>
<p>Claude를 시작하면 MCP 서버가 자동으로 실행됨. 연결 상태 확인:</p>
<p><strong>사용 가능한 도구 확인:</strong></p>
<pre><code>Available MCP Tools:
- mcp__postgres__query: Execute read-only SQL queries</code></pre><h3 id="3-연결-테스트">3. 연결 테스트</h3>
<p>간단한 쿼리로 연결을 테스트함:</p>
<p><strong>자연어로 질의:</strong></p>
<pre><code>&quot;테이블 목록 보여줘&quot;</code></pre><p><strong>또는 직접 SQL 요청:</strong></p>
<pre><code>&quot;SELECT * FROM pg_tables WHERE schemaname = &#39;public&#39; 실행해줘&quot;</code></pre><p>정상적으로 테이블 목록이 출력되면 연결 성공임.</p>
<hr>
<h2 id="실전-사용-예제">실전 사용 예제</h2>
<h3 id="기본-데이터베이스-조회">기본 데이터베이스 조회</h3>
<p>MCP 서버가 연결되면 자연어로 데이터베이스 조회 가능:</p>
<p><strong>예제 1: 테이블 목록</strong></p>
<pre><code>사용자: &quot;데이터베이스에 어떤 테이블이 있어?&quot;
Claude: [자동으로 적절한 쿼리 실행 후 결과 제공]</code></pre><p><strong>예제 2: 테이블 구조 확인</strong></p>
<pre><code>사용자: &quot;users 테이블 구조 알려줘&quot;
Claude: [테이블 스키마 정보 제공]</code></pre><p><strong>예제 3: 데이터 조회</strong></p>
<pre><code>사용자: &quot;users 테이블에서 최근 가입한 사용자 10명 보여줘&quot;
Claude: [데이터 조회 및 분석 결과 제공]</code></pre><h3 id="자동-쿼리-생성">자동 쿼리 생성</h3>
<p>Claude가 자연어를 SQL로 자동 변환:</p>
<pre><code>사용자: &quot;이번 달 주문 건수가 몇 개야?&quot;
Claude: → SELECT COUNT(*) FROM orders
          WHERE created_at &gt;= DATE_TRUNC(&#39;month&#39;, CURRENT_DATE)</code></pre><pre><code>사용자: &quot;가장 많이 팔린 상품 5개 알려줘&quot;
Claude: → SELECT product_name, COUNT(*) as sales
          FROM orders
          GROUP BY product_name
          ORDER BY sales DESC
          LIMIT 5</code></pre><hr>
<h2 id="고급-설정">고급 설정</h2>
<h3 id="환경-변수-활용">환경 변수 활용</h3>
<p>민감한 정보는 환경 변수로 관리하는 것이 좋음:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;${DATABASE_URL}&quot;
      ],
      &quot;env&quot;: {
        &quot;DATABASE_URL&quot;: &quot;postgresql://user:password@localhost/mydb&quot;
      }
    }
  }
}</code></pre>
<p><strong>또는 시스템 환경 변수 사용:</strong></p>
<pre><code class="language-bash"># .bashrc 또는 .zshrc
export DATABASE_URL=&quot;postgresql://user:password@localhost/mydb&quot;</code></pre>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;${DATABASE_URL}&quot;
      ]
    }
  }
}</code></pre>
<h3 id="타임아웃-설정">타임아웃 설정</h3>
<p>장시간 실행되는 쿼리를 위해 타임아웃을 설정할 수 있음:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://localhost/mydb&quot;
      ],
      &quot;timeout&quot;: 30000
    }
  }
}</code></pre>
<h3 id="연결-풀-설정">연결 풀 설정</h3>
<p>PostgreSQL 연결 풀링 옵션:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://localhost/mydb?max_pool_size=10&amp;min_pool_size=2&quot;
      ]
    }
  }
}</code></pre>
<hr>
<h2 id="트러블슈팅">트러블슈팅</h2>
<h3 id="mcp-서버가-시작되지-않는-경우">MCP 서버가 시작되지 않는 경우</h3>
<p><strong>증상:</strong> Claude 시작 시 MCP 서버 연결 실패</p>
<p><strong>해결 방법:</strong></p>
<ol>
<li><p><strong>Node.js 설치 확인</strong></p>
<pre><code class="language-bash">node --version
npm --version</code></pre>
</li>
<li><p><strong>설정 파일 확인</strong></p>
<pre><code class="language-bash">cat ~/.claude/config.json</code></pre>
</li>
<li><p><strong>JSON 문법 오류 확인</strong></p>
</li>
</ol>
<ul>
<li>콤마 누락 확인</li>
<li>괄호 짝 맞는지 확인</li>
<li>따옴표 올바른지 확인</li>
</ul>
<ol start="4">
<li><strong>PostgreSQL 연결 확인</strong><pre><code class="language-bash">psql -h localhost -U user -d mydb</code></pre>
</li>
</ol>
<h3 id="인증-오류">인증 오류</h3>
<p><strong>증상:</strong> <code>password authentication failed</code> 에러</p>
<p><strong>해결 방법:</strong></p>
<ol>
<li><p><strong>사용자 계정 확인</strong></p>
<pre><code class="language-sql">SELECT usename FROM pg_user;</code></pre>
</li>
<li><p><strong>비밀번호 확인</strong></p>
<pre><code class="language-bash">psql -h localhost -U user -d mydb</code></pre>
</li>
<li><p><strong>pg_hba.conf 설정 확인</strong></p>
<pre><code class="language-bash"># PostgreSQL 설정 파일 위치 확인
psql -c &quot;SHOW hba_file&quot;</code></pre>
</li>
</ol>
<h3 id="연결-거부-오류">연결 거부 오류</h3>
<p><strong>증상:</strong> <code>Connection refused</code> 에러</p>
<p><strong>해결 방법:</strong></p>
<ol>
<li><p><strong>PostgreSQL 실행 확인</strong></p>
<pre><code class="language-bash">pg_isready
# 또는
sudo systemctl status postgresql</code></pre>
</li>
<li><p><strong>포트 확인</strong></p>
<pre><code class="language-bash">netstat -an | grep 5432</code></pre>
</li>
<li><p><strong>방화벽 설정 확인</strong></p>
<pre><code class="language-bash">sudo ufw status</code></pre>
</li>
</ol>
<h3 id="쿼리-권한-오류">쿼리 권한 오류</h3>
<p><strong>증상:</strong> <code>permission denied</code> 에러</p>
<p><strong>해결 방법:</strong></p>
<p>MCP 서버는 읽기 전용이므로 SELECT 권한만 필요:</p>
<pre><code class="language-sql">GRANT SELECT ON ALL TABLES IN SCHEMA public TO user;</code></pre>
<hr>
<h2 id="보안-고려사항">보안 고려사항</h2>
<h3 id="읽기-전용-접근">읽기 전용 접근</h3>
<p>MCP 서버는 기본적으로 읽기 전용 쿼리만 지원:</p>
<table>
<thead>
<tr>
<th>허용</th>
<th>차단</th>
</tr>
</thead>
<tbody><tr>
<td>SELECT</td>
<td>INSERT</td>
</tr>
<tr>
<td>SHOW</td>
<td>UPDATE</td>
</tr>
<tr>
<td>DESCRIBE</td>
<td>DELETE</td>
</tr>
<tr>
<td>EXPLAIN</td>
<td>DROP</td>
</tr>
</tbody></table>
<h3 id="전용-사용자-생성">전용 사용자 생성</h3>
<p>MCP 전용 읽기 전용 사용자 생성 권장:</p>
<pre><code class="language-sql">-- 읽기 전용 사용자 생성
CREATE USER mcp_readonly WITH PASSWORD &#39;secure_password&#39;;

-- SELECT 권한 부여
GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;

-- 향후 생성될 테이블에도 자동 권한 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO mcp_readonly;</code></pre>
<h3 id="연결-정보-보호">연결 정보 보호</h3>
<p>민감한 연결 정보는 환경 변수나 별도 파일로 관리:</p>
<pre><code class="language-bash"># .env 파일 생성
DATABASE_URL=postgresql://mcp_readonly:password@localhost/mydb

# 권한 설정
chmod 600 .env</code></pre>
<h3 id="ssltls-암호화">SSL/TLS 암호화</h3>
<p>프로덕션 환경에서는 SSL 연결 필수:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;postgres&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-postgres&quot;,
        &quot;postgresql://user:pass@host:5432/db?sslmode=require&quot;
      ]
    }
  }
}</code></pre>
<p><strong>SSL 모드 옵션:</strong></p>
<ul>
<li><code>disable</code>: SSL 사용 안 함</li>
<li><code>require</code>: SSL 필수</li>
<li><code>verify-ca</code>: CA 인증서 검증</li>
<li><code>verify-full</code>: 호스트명까지 검증</li>
</ul>
<hr>
<h2 id="실전-활용-사례">실전 활용 사례</h2>
<h3 id="1-데이터-탐색">1. 데이터 탐색</h3>
<p>자연어로 데이터베이스 구조와 내용 탐색:</p>
<pre><code>&quot;어떤 테이블이 있어?&quot;
&quot;users 테이블 구조 알려줘&quot;
&quot;users 테이블 레코드 몇 개야?&quot;
&quot;최근 생성된 데이터 10개 보여줘&quot;</code></pre><h3 id="2-비즈니스-분석">2. 비즈니스 분석</h3>
<p>비즈니스 질문을 바로 데이터로 확인:</p>
<pre><code>&quot;이번 달 신규 가입자 수는?&quot;
&quot;어제 매출은 얼마야?&quot;
&quot;가장 인기 있는 카테고리는?&quot;
&quot;지역별 주문 분포 보여줘&quot;</code></pre><h3 id="3-데이터-검증">3. 데이터 검증</h3>
<p>데이터 품질 확인:</p>
<pre><code>&quot;NULL 값이 있는 레코드 있어?&quot;
&quot;중복된 이메일 있어?&quot;
&quot;유효하지 않은 날짜 데이터 있어?&quot;</code></pre><h3 id="4-트러블슈팅">4. 트러블슈팅</h3>
<p>문제 발생 시 빠른 확인:</p>
<pre><code>&quot;최근 1시간 에러 로그 보여줘&quot;
&quot;실패한 트랜잭션 조회해줘&quot;
&quot;응답 시간이 긴 API 요청 찾아줘&quot;</code></pre><hr>
<h2 id="mcp-vs-기존-방식-비교">MCP vs 기존 방식 비교</h2>
<h3 id="기존-방식">기존 방식</h3>
<pre><code class="language-bash"># 터미널에서 psql 접속
psql -h localhost -U user -d mydb

# SQL 작성
mydb=# SELECT COUNT(*) FROM users WHERE created_at &gt; NOW() - INTERVAL &#39;7 days&#39;;

# 결과 확인
 count
-------
   143</code></pre>
<h3 id="mcp-방식">MCP 방식</h3>
<pre><code>사용자: &quot;지난 7일간 가입한 사용자 수 알려줘&quot;
Claude: 지난 7일간 143명의 사용자가 가입했습니다.</code></pre><p><strong>장점:</strong></p>
<ul>
<li>SQL 문법 몰라도 됨</li>
<li>자연어로 질의</li>
<li>결과 자동 해석</li>
<li>연속 질문 가능</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>MCP를 활용하면 Claude가 PostgreSQL 데이터베이스에 직접 접근하여 자연어로 데이터를 조회할 수 있음.</p>
<p><strong>구축 요약:</strong></p>
<ol>
<li><code>~/.claude/config.json</code> 파일 생성</li>
<li>PostgreSQL MCP 서버 설정 추가</li>
<li>연결 문자열 입력</li>
<li>Claude 재시작</li>
<li>자연어로 데이터 조회</li>
</ol>
<p><strong>핵심 장점:</strong></p>
<ul>
<li><strong>쉬운 설정</strong>: 설정 파일 하나로 연동 완료</li>
<li><strong>자연어 지원</strong>: SQL 없이 데이터 조회</li>
<li><strong>안전한 접근</strong>: 읽기 전용으로 데이터 보호</li>
<li><strong>빠른 분석</strong>: 대화형으로 즉시 데이터 확인</li>
</ul>
<p>MCP 서버 구축은 간단하지만, 데이터 분석 워크플로우를 혁신적으로 개선할 수 있음.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<p><strong>공식 문서:</strong></p>
<ul>
<li><a href="https://modelcontextprotocol.io/">MCP 공식 문서</a></li>
<li><a href="https://github.com/anthropics/claude-code">Claude Code CLI</a></li>
<li><a href="https://www.postgresql.org/docs/">PostgreSQL 문서</a></li>
</ul>
<p><strong>MCP 서버:</strong></p>
<ul>
<li><a href="https://github.com/modelcontextprotocol/servers">MCP Server 저장소</a></li>
<li><a href="https://www.npmjs.com/package/@modelcontextprotocol/server-postgres">PostgreSQL MCP Server</a></li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[3가지 CAPTCHA 비교 (reCAPTCHA vs hCaptcha vs Turnstile)]]></title>
            <link>https://velog.io/@oi_24/3%EA%B0%80%EC%A7%80-CAPTCHA-%EB%B9%84%EA%B5%90-reCAPTCHA-vs-hCaptcha-vs-Turnstile</link>
            <guid>https://velog.io/@oi_24/3%EA%B0%80%EC%A7%80-CAPTCHA-%EB%B9%84%EA%B5%90-reCAPTCHA-vs-hCaptcha-vs-Turnstile</guid>
            <pubDate>Tue, 06 Jan 2026 01:34:11 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>  웹 폼에서 봇을 막기 위해 CAPTCHA를 많이 씀. Django에서 사용할 수 있는 대표적인 CAPTCHA 3가지를 직접 테스트해봄.</p>
<table>
<thead>
<tr>
<th>CAPTCHA</th>
<th>제공사</th>
<th>Django 패키지</th>
</tr>
</thead>
<tbody><tr>
<td>reCAPTCHA v2</td>
<td>Google</td>
<td><code>django-recaptcha</code></td>
</tr>
<tr>
<td>hCaptcha</td>
<td>Intuition Machines</td>
<td><code>django-hcaptcha</code></td>
</tr>
<tr>
<td>Turnstile</td>
<td>Cloudflare</td>
<td><code>django-turnstile</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="1-google-recaptcha-v2">1. Google reCAPTCHA v2</h2>
<p>  Google에서 제공하는 가장 널리 사용되는 CAPTCHA 서비스임. &quot;I&#39;m not a robot&quot; 체크박스 방식으로, 행동 분석 기반으로 봇을 탐지함. 의심스러운 경우 이미지 선택 챌린지가 나옴.</p>
<p>  가장 높은 인지도와 풍부한 레퍼런스, 안정적인 서비스가 장점임. 다만 Google에 데이터가 전송되기 때문에 프라이버시 이슈가 있음.</p>
<pre><code class="language-python">  from django_recaptcha.fields import ReCaptchaField
  from django_recaptcha.widgets import ReCaptchaV2Checkbox

  class MyForm(forms.Form):
      captcha = ReCaptchaField(widget=ReCaptchaV2Checkbox())</code></pre>
<hr>
<h2 id="2-hcaptcha">2. hCaptcha</h2>
<p>  reCAPTCHA의 프라이버시 친화적 대안으로 떠오른 서비스임. GDPR 준수가 잘 되어 있고, Cloudflare가 한때 이걸 기본 CAPTCHA 로 채택했었음. 무료 티어가 제공됨.</p>
<p>  프라이버시 보호와 GDPR 친화적인 점이 장점임. 다만 reCAPTCHA보다 인지도가 낮고, 간혹 챌린지가 어려울 때가 있음.</p>
<pre><code class="language-python">  from hcaptcha.fields import hCaptchaField

  class MyForm(forms.Form):
      captcha = hCaptchaField()</code></pre>
<hr>
<h2 id="3-cloudflare-turnstile">3. Cloudflare Turnstile</h2>
<p>  Cloudflare에서 2022년에 출시한 최신 CAPTCHA임. 대부분의 경우 사용자 상호작용이 필요 없고, 완전 무료로 제공됨. 프라이버시 보호도 잘 되어 있음.</p>
<p>  사용자 경험이 가장 좋고 완전 무료인 게 장점임. 다만 상대적으로 신생 서비스라 레퍼런스가 적음.</p>
<pre><code class="language-python">  from turnstile.fields import TurnstileField

  class MyForm(forms.Form):
      captcha = TurnstileField()</code></pre>
<hr>
<h2 id="테스트-키-정보">테스트 키 정보</h2>
<p>  개발 환경에서 테스트할 때 각 서비스에서 제공하는 테스트 키를 사용하면 됨. 테스트 키는 항상 검증을 통과함.</p>
<table>
<thead>
<tr>
<th>서비스</th>
<th>Site Key</th>
<th>Secret Key</th>
</tr>
</thead>
<tbody><tr>
<td>reCAPTCHA</td>
<td><code>6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI</code></td>
<td><code>6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe</code></td>
</tr>
<tr>
<td>hCaptcha</td>
<td><code>10000000-ffff-ffff-ffff-000000000001</code></td>
<td><code>0x0000000000000000000000000000000000000000</code></td>
</tr>
<tr>
<td>Turnstile</td>
<td><code>1x00000000000000000000AA</code></td>
<td><code>1x0000000000000000000000000000000AA</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="총평">총평</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>reCAPTCHA</th>
<th>hCaptcha</th>
<th>Turnstile</th>
</tr>
</thead>
<tbody><tr>
<td>UX</td>
<td>보통</td>
<td>보통</td>
<td>최고</td>
</tr>
<tr>
<td>프라이버시</td>
<td>낮음</td>
<td>높음</td>
<td>높음</td>
</tr>
<tr>
<td>가격</td>
<td>무료~유료</td>
<td>무료~유료</td>
<td>완전 무료</td>
</tr>
<tr>
<td>레퍼런스</td>
<td>많음</td>
<td>보통</td>
<td>적음</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django + Redis Cluster 연동 시 Pipeline 제약 문제(이게 맞는 방법인가??)]]></title>
            <link>https://velog.io/@oi_24/Django-Redis-Cluster-%EC%97%B0%EB%8F%99-%EC%8B%9C-Pipeline-%EC%A0%9C%EC%95%BD-%EB%AC%B8%EC%A0%9C%EC%9D%B4%EA%B2%8C-%EB%A7%9E%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@oi_24/Django-Redis-Cluster-%EC%97%B0%EB%8F%99-%EC%8B%9C-Pipeline-%EC%A0%9C%EC%95%BD-%EB%AC%B8%EC%A0%9C%EC%9D%B4%EA%B2%8C-%EB%A7%9E%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 30 Dec 2025 03:14:12 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>  Django에서 세션 저장소로 Redis Cluster를 사용하려고 <code>django-redis</code>를 설정했는데 아래 에러가 발생함.</p>
<p>  ClusterError: Command # 1 (EXISTS ...) of pipeline caused error: TTL exhausted.</p>
<p>  분명히 Redis Cluster 구성도 잘 됐고, <code>redis-py</code>로 직접 테스트하면 잘 됨. 근데 Django에서만 안 됨.</p>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<h3 id="redis-cluster의-데이터-분산-구조">Redis Cluster의 데이터 분산 구조</h3>
<p>  Redis Cluster는 <strong>16384개의 해시 슬롯</strong>으로 데이터를 분산 저장함.</p>
<p>  키 → CRC16(키) % 16384 → 슬롯 번호 → 담당 노드</p>
<p>  ┌─────────────────┬─────────────┬─────────────┐
  │    Master 1     │   Master 2  │   Master 3  │
  │  슬롯 0-5460    │ 슬롯 5461-  │ 슬롯 10923- │
  │                 │    10922    │    16383    │
  ├─────────────────┼─────────────┼─────────────┤
  │ &quot;user:1&quot;        │ &quot;user:2&quot;    │ &quot;session:a&quot; │
  │  → 슬롯 1234    │  → 슬롯 7890│  → 슬롯     │
  │                 │             │    12345    │
  └─────────────────┴─────────────┴─────────────┘</p>
<p>  키의 해시값에 따라 서로 다른 노드에 저장됨.</p>
<hr>
<h3 id="pipeline이란">Pipeline이란?</h3>
<p>  일반적인 Redis 요청은 <strong>요청 → 응답 → 요청 → 응답</strong> 순서로 동작함.</p>
<p>  일반 방식 (느림):
  Client          Redis
    │── GET a ──────▶│
    │◀───── &quot;1&quot; ─────│
    │── GET b ──────▶│
    │◀───── &quot;2&quot; ─────│</p>
<p>  네트워크 왕복: 2번</p>
<p>  <strong>Pipeline</strong>은 여러 명령을 한 번에 보내고 응답도 한 번에 받음. 네트워크 왕복을 줄여서 성능이 좋음.</p>
<p>  Pipeline 방식 (빠름):
  Client          Redis
    │── GET a ──────▶│
    │── GET b ──────▶│
    │◀───── &quot;1&quot; ─────│
    │◀───── &quot;2&quot; ─────│</p>
<p>  네트워크 왕복: 1번</p>
<hr>
<h3 id="redis-cluster--pipeline--문제">Redis Cluster + Pipeline = 문제</h3>
<p>  단일 Redis에서는 모든 키가 같은 서버에 있으니까 pipeline 문제 없음.</p>
<p>  근데 <strong>Redis Cluster</strong>에서는 키마다 담당 노드가 다름.</p>
<p>  Client가 Master 1에 Pipeline 전송:</p>
<pre><code>│── EXISTS session:xyz ──▶│ Master 1
│── GET session:xyz ─────▶│</code></pre><p>  문제:</p>
<ul>
<li><p>session:xyz → 슬롯 12345 → Master 3 담당인데?</p>
</li>
<li><p>Master 1한테 물어봤으니까 → MOVED 응답 반환</p>
<p>Master 1 입장에서는 &quot;나한테 왜 물어봄? Master 3 가서 물어봐&quot;라고 응답하는 것임.</p>
<p>MOVED 12345 172.28.0.23:7003</p>
<p>클라이언트가 이걸 받고 Master 3으로 재시도 → 또 문제 발생 → 반복 → <strong>TTL exhausted</strong></p>
</li>
</ul>
<hr>
<h3 id="django-redis가-문제인-이유">django-redis가 문제인 이유</h3>
<p>  <code>django-redis</code>의 <code>DefaultClient</code>는 내부적으로 pipeline을 사용함.</p>
<h1 id="django-redis-내부-코드-간략화">django-redis 내부 코드 (간략화)</h1>
<pre><code class="language-python">  def get(self, key):
      with self.client.pipeline() as pipe:
          pipe.exists(key)   # ← 이 키가 다른 노드에 있으면 MOVED
          pipe.get(key)      # ← 마찬가지
          return pipe.execute()  # ← 여기서 TTL exhausted 터짐

  django-redis는 원래 단일 Redis용으로 만들어진 거라서 Cluster 환경을 고려 안 함.</code></pre>
<h3 id="해결-방법">해결 방법</h3>
<h4 id="커스텀-connection-factory-작성">커스텀 Connection Factory 작성</h4>
<pre><code class="language-python">  # redis-py의 RedisCluster를 직접 사용하는 Connection Factory를 만들어서 주입함.

  app/config/redis_cluster_factory.py

  from django_redis.pool import ConnectionFactory
  from redis.cluster import RedisCluster, ClusterNode

  class RedisClusterConnectionFactory(ConnectionFactory):
      def __init__(self, options):
          self._pool = None
          self._client = None
          self._options = options

      def connect(self, url):
          if self._client is None:
              startup_nodes = [
                  ClusterNode(&#39;172.28.0.21&#39;, 7001),
                  ClusterNode(&#39;172.28.0.22&#39;, 7002),
                  ClusterNode(&#39;172.28.0.23&#39;, 7003),
              ]
              self._client = RedisCluster(
                  startup_nodes=startup_nodes,
                  decode_responses=False,
                  skip_full_coverage_check=True,
              )
          return self._client

      def disconnect(self, connection):
          if self._client:
              self._client.close()
              self._client = None

      def get_connection(self, params):
          return self.connect(None)

  # settings.py

  SESSION_ENGINE = &#39;django.contrib.sessions.backends.cache&#39;
  SESSION_CACHE_ALIAS = &#39;default&#39;

  CACHES = {
      &#39;default&#39;: {
          &#39;BACKEND&#39;: &#39;django_redis.cache.RedisCache&#39;,
          &#39;LOCATION&#39;: &#39;redis://172.28.0.21:7001/0&#39;,
          &#39;OPTIONS&#39;: {
              &#39;CLIENT_CLASS&#39;: &#39;django_redis.client.DefaultClient&#39;,
              &#39;CONNECTION_FACTORY&#39;: &#39;config.redis_cluster_factory.RedisClusterConnectionFactory&#39;,
          },
      }
  }</code></pre>
<hr>
<h1 id="왜-이게-해결이-되는가">왜 이게 해결이 되는가?</h1>
<pre><code>  RedisCluster 객체는 내부적으로 키의 슬롯을 계산해서 올바른 노드에 직접 요청함.

  rc = RedisCluster(...)

  rc.get(&quot;user:1&quot;)    # → 슬롯 계산 → Master 1로 요청
  rc.get(&quot;user:2&quot;)    # → 슬롯 계산 → Master 2로 요청
  rc.get(&quot;session:a&quot;) # → 슬롯 계산 → Master 3로 요청

  MOVED 응답이 와도 자동으로 올바른 노드로 리다이렉트 처리함.</code></pre><hr>
<h4 id="요약">요약</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>단일 Redis</th>
<th>Redis Cluster</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 위치</td>
<td>한 서버에 모두 저장</td>
<td>슬롯별로 분산</td>
</tr>
<tr>
<td>Pipeline</td>
<td>문제 없음</td>
<td>다른 슬롯 키 혼합 시 MOVED 발생</td>
</tr>
<tr>
<td>django-redis</td>
<td>정상 동작</td>
<td>호환 문제 (TTL exhausted)</td>
</tr>
<tr>
<td>해결책</td>
<td>-</td>
<td>커스텀 Connection Factory로 RedisCluster 직접 사용</td>
</tr>
</tbody></table>
<hr>
<h4 id="참고">참고</h4>
<blockquote>
<p>  Docker 환경에서 Redis Cluster 구성 시 고정 IP + cluster-announce-ip 설정도 필수임. 안 하면 클라이언트가 노드 IP에 접근 못해서 똑같이 TTL exhausted 에러 남.</p>
</blockquote>
<h4 id="docker-composeyml">docker-compose.yml</h4>
<p>  redis-node1:
    command: &gt;
      redis-server
      --cluster-announce-ip 172.28.0.21
      --cluster-announce-port 7001
    networks:
      app-network:
        ipv4_address: 172.28.0.21</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 장애 복구(Failover) 매커니즘 분석: Sentinel vs Cluster]]></title>
            <link>https://velog.io/@oi_24/Redis-%EC%9E%A5%EC%95%A0-%EB%B3%B5%EA%B5%ACFailover-%EB%A7%A4%EC%BB%A4%EB%8B%88%EC%A6%98-%EB%B6%84%EC%84%9D-Sentinel-vs-Cluster</link>
            <guid>https://velog.io/@oi_24/Redis-%EC%9E%A5%EC%95%A0-%EB%B3%B5%EA%B5%ACFailover-%EB%A7%A4%EC%BB%A4%EB%8B%88%EC%A6%98-%EB%B6%84%EC%84%9D-Sentinel-vs-Cluster</guid>
            <pubDate>Tue, 30 Dec 2025 02:51:11 GMT</pubDate>
            <description><![CDATA[<p>Redis 운영의 핵심은 <strong>&quot;Master 노드가 죽었을 때 서비스가 얼마나 빨리, 자동으로 복구되느냐&quot;</strong>임. 구성 방식에 따라 장애를 감지하고 복구하는 주체와 프로세스가 완전히 다름.</p>
<p>각 방식별 Failover 메커니즘을 상세히 정리함.</p>
<hr>
<h2 id="1-standalone-replication-only">1. Standalone (Replication Only)</h2>
<p>가장 기초적인 Master-Replica 구조임. <strong>자동 복구가 불가능</strong>하다는 것이 핵심.</p>
<h3 id="장애-복구-프로세스-수동">장애 복구 프로세스 (수동)</h3>
<ol>
<li><strong>장애 발생:</strong> Master 노드 다운.</li>
<li><strong>서비스 중단:</strong> Application에서 쓰기(Write) 작업 실패 발생.</li>
<li><strong>관리자 개입:</strong><ul>
<li>개발자/엔지니어가 알람을 보고 접속.</li>
<li>Replica 노드에 직접 접속하여 승격 명령어 실행.<pre><code class="language-bash"># Replica 노드에서 실행하여 Master로 승격
REPLICAOF NO ONE</code></pre>
</li>
</ul>
</li>
<li><strong>설정 변경:</strong> Application 서버의 Redis 연결 설정을 새로운 Master IP로 변경 후 배포/재시작.</li>
</ol>
<h3 id="특징">특징</h3>
<ul>
<li><strong>Down Time이 긺:</strong> 사람이 직접 대응해야 하므로 야간이나 휴일 장애 시 서비스 중단 시간이 매우 길어질 수 있음.</li>
<li><strong>감시 주체 없음:</strong> 별도의 헬스 체크 시스템이 없으면 장애 인지가 늦음.</li>
</ul>
<hr>
<h2 id="2-redis-sentinel-센티널">2. Redis Sentinel (센티널)</h2>
<p><strong>별도의 감시 프로세스(Sentinel)</strong>가 관리자를 대신해 장애를 감지하고 복구함.</p>
<h3 id="장애-복구-프로세스-자동">장애 복구 프로세스 (자동)</h3>
<ol>
<li><strong>장애 감지 (SDOWN -&gt; ODOWN):</strong><ul>
<li>Sentinel 인스턴스가 Master에게 주기적으로 PING을 날림.</li>
<li>응답이 없으면 해당 Sentinel은 <strong>SDOWN(Subjective Down, 주관적 다운)</strong>으로 인지.</li>
<li>설정된 Quorum(정족수) 이상의 Sentinel들이 &quot;얘 죽은 거 맞다&quot;고 동의하면 <strong>ODOWN(Objective Down, 객관적 다운)</strong>으로 확정.</li>
</ul>
</li>
<li><strong>리더 선출:</strong> Sentinel들끼리 투표하여 Failover를 진행할 &#39;리더 Sentinel&#39;을 선출함.</li>
<li><strong>Failover 실행:</strong><ul>
<li>리더 Sentinel이 건강한 Replica 중 하나를 선택.</li>
<li>해당 Replica에게 <code>REPLICAOF NO ONE</code> 명령어를 전송해 Master로 승격시킴.</li>
<li>나머지 Replica들이 새 Master를 바라보도록 설정 변경 (<code>REPLICAOF new-master-ip port</code>).</li>
</ul>
</li>
<li><strong>Client 전파:</strong><ul>
<li>Client는 Sentinel에게 현재 Master 주소를 질의하다가 변경된 주소를 받게 됨 (Pub/Sub 메커니즘 활용).</li>
</ul>
</li>
</ol>
<h3 id="특징-1">특징</h3>
<ul>
<li><strong>감시 주체:</strong> 별도의 <code>redis-sentinel</code> 프로세스.</li>
<li><strong>클라이언트 지원 필수:</strong> Application 코드(라이브러리)가 Sentinel 기능을 지원해야 함. (직접 Redis IP를 박는 게 아니라 Sentinel IP 리스트를 설정함).</li>
</ul>
<hr>
<h2 id="3-redis-cluster-클러스터">3. Redis Cluster (클러스터)</h2>
<p>Sentinel 없이 <strong>노드들끼리 서로 감시(P2P)</strong>하며 집단지성으로 복구함.</p>
<h3 id="장애-복구-프로세스-자동---gossip-protocol">장애 복구 프로세스 (자동 - Gossip Protocol)</h3>
<ol>
<li><strong>상호 감시:</strong> 클러스터 내 모든 Master 노드는 서로 mesh 구조로 연결되어 <code>PING/PONG</code>을 주고받음 (Gossip Protocol).</li>
<li><strong>장애 감지 (PFAIL -&gt; FAIL):</strong><ul>
<li>Node A가 Node B에게 응답을 못 받으면 <strong>PFAIL(Possible Fail)</strong>로 마킹.</li>
<li>다른 Master 노드들에게도 Gossip 메시지로 Node B 상태를 물어봄.</li>
<li>과반수 이상의 Master가 PFAIL이라 판단하면 <strong>FAIL</strong> 상태로 확정하고 클러스터 전체에 브로드캐스팅.</li>
</ul>
</li>
<li><strong>승격 투표:</strong><ul>
<li>죽은 Master의 Replica가 이를 감지하고 승격 선거를 시작. (&quot;나 Master 할게 투표 좀 해줘&quot;)</li>
<li>살아있는 다른 Master 노드들이 투표(Vote)함.</li>
</ul>
</li>
<li><strong>Failover 실행:</strong><ul>
<li>과반수 표를 얻은 Replica가 Master로 승격.</li>
<li>자신이 담당할 Hash Slot 정보를 갱신하고 클러스터 설정(Epoch)을 업데이트.</li>
</ul>
</li>
</ol>
<h3 id="특징-2">특징</h3>
<ul>
<li><strong>감시 주체:</strong> Redis Master 노드 자신들 (Sentinel 불필요).</li>
<li><strong>리다이렉션:</strong> 클라이언트는 아무 노드나 접속했다가, 해당 Key가 다른 노드에 있으면 <code>MOVED</code> 에러와 함께 올바른 주소로 리다이렉트됨.</li>
</ul>
<hr>
<h2 id="4-요약-비교">4. 요약 비교</h2>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Standalone</th>
<th align="left">Sentinel</th>
<th align="left">Cluster</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>자동 복구</strong></td>
<td align="left"><strong>불가능 (수동)</strong></td>
<td align="left"><strong>가능</strong></td>
<td align="left"><strong>가능</strong></td>
</tr>
<tr>
<td align="left"><strong>감시 주체</strong></td>
<td align="left">(없음)</td>
<td align="left"><strong>Sentinel 프로세스</strong></td>
<td align="left"><strong>Redis 노드 간 (Gossip)</strong></td>
</tr>
<tr>
<td align="left"><strong>장애 판단</strong></td>
<td align="left">사람</td>
<td align="left">Sentinel 간 투표 (Quorum)</td>
<td align="left">Master 간 투표 (과반수)</td>
</tr>
<tr>
<td align="left"><strong>클라이언트</strong></td>
<td align="left">IP 변경 후 재배포 필요</td>
<td align="left">Sentinel 라이브러리 지원 필요</td>
<td align="left">Smart Client (리다이렉트 처리) 필요</td>
</tr>
<tr>
<td align="left"><strong>복잡도</strong></td>
<td align="left">낮음</td>
<td align="left">중간</td>
<td align="left">높음</td>
</tr>
</tbody></table>
<h3 id="결론">결론</h3>
<ul>
<li><strong>Sentinel:</strong> 별도의 감시반(보디가드)을 고용해서 지키게 하는 방식.</li>
<li><strong>Cluster:</strong> 구성원들끼리 서로 생존 신고하며 빈자리를 채우는 자치적인 방식.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 구성 방식 (Replication vs Sentinel vs Cluster)]]></title>
            <link>https://velog.io/@oi_24/Redis-%EA%B5%AC%EC%84%B1-%EB%B0%A9%EC%8B%9D-Replication-vs-Sentinel-vs-Cluster</link>
            <guid>https://velog.io/@oi_24/Redis-%EA%B5%AC%EC%84%B1-%EB%B0%A9%EC%8B%9D-Replication-vs-Sentinel-vs-Cluster</guid>
            <pubDate>Tue, 30 Dec 2025 02:08:01 GMT</pubDate>
            <description><![CDATA[<p>Redis 도입 시 가장 먼저 하는 고민은 <strong>&quot;어떤 아키텍처로 구성할 것인가?&quot;</strong>임. 서비스 규모, 가용성(HA), 데이터 분산 필요성에 따라 크게 세 가지 방식(Replication, Sentinel, Cluster)으로 나뉨.</p>
<p>각 방식의 특징과 장단점을 정리함.</p>
<hr>
<h2 id="1-standalone-replication--기본-복제-구성">1. Standalone (Replication) : 기본 복제 구성</h2>
<p>가장 기본적인 형태. Master 노드 1개와 Replica(Slave) 노드 1개 이상으로 구성됨.</p>
<h3 id="동작-원리">동작 원리</h3>
<ul>
<li><strong>Master:</strong> 데이터 쓰기(Write)와 읽기(Read) 모두 수행.</li>
<li><strong>Replica:</strong> Master 데이터를 <strong>비동기(Asynchronous)</strong>로 복제해 유지. 주로 읽기 전용(Read-Only)으로 설정해 <strong>Read 트래픽 분산</strong> 용도로 사용.</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>구성이 가장 간단함.</li>
<li>Replica를 늘려 Read 성능 확보 가능.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><strong>SPOF (Single Point of Failure):</strong> Master 장애 시 쓰기 불가.</li>
<li><strong>수동 복구:</strong> 장애 시 관리자가 직접 Replica를 Master로 승격시켜야 함.</li>
<li><strong>Scale-up 의존:</strong> 데이터 증가 시 장비 스펙 업(Scale-up) 외엔 방법 없음.</li>
</ul>
<hr>
<h2 id="2-redis-sentinel--고가용성ha-확보">2. Redis Sentinel : 고가용성(HA) 확보</h2>
<p>Replication의 &#39;수동 복구&#39; 한계를 해결하기 위한 구조. 별도의 <strong>Sentinel(감시자)</strong> 프로세스가 Redis를 모니터링함.</p>
<h3 id="동작-원리-1">동작 원리</h3>
<ol>
<li><strong>Monitoring:</strong> Sentinel이 Master, Replica 상태 주기적 감시.</li>
<li><strong>Notification:</strong> 장애 감지 시 관리자에게 알림 발송.</li>
<li><strong>Automatic Failover:</strong> Master 다운 시, Sentinel 투표로 Replica 중 하나를 Master로 승격.</li>
<li><strong>Configuration Provider:</strong> 클라이언트는 Sentinel에 접속해 현재 Master 주소를 받아옴.</li>
</ol>
<h3 id="특징">특징</h3>
<ul>
<li><strong>홀수 구성:</strong> 과반수 투표(Quorum) 위해 Sentinel은 최소 3개 이상 홀수로 구성 필요.</li>
<li><strong>데이터 샤딩 불가:</strong> 모든 데이터가 하나의 Master에 저장되므로 대용량 처리에 한계 존재.</li>
</ul>
<hr>
<h2 id="3-redis-cluster--샤딩--고가용성">3. Redis Cluster : 샤딩 + 고가용성</h2>
<p>단일 서버 메모리 초과나 쓰기 트래픽 분산이 필요할 때 쓰는 <strong>수평 확장(Scale-out)</strong> 구조.</p>
<h3 id="동작-원리-hash-slot">동작 원리 (Hash Slot)</h3>
<ul>
<li><strong>Sharding:</strong> 전체 데이터를 <strong>16,384개 Hash Slot</strong>으로 분할.</li>
<li><strong>분산 저장:</strong> Key를 CRC16 해시 함수로 돌려 저장할 슬롯(노드) 결정.<blockquote>
<p><code>SLOT = CRC16(key) mod 16384</code></p>
</blockquote>
</li>
<li><strong>Failover:</strong> Master별로 Replica를 가지며, 장애 시 해당 Replica가 자동 승격 (Sentinel 없이 자체 수행).</li>
</ul>
<h3 id="장점-1">장점</h3>
<ul>
<li><strong>무한한 확장성:</strong> 노드 추가로 용량과 처리량 증대 가능.</li>
<li><strong>HA:</strong> 일부 노드 장애에도 전체 서비스 중단 없음.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>설정과 관리가 복잡함.</li>
<li><strong>Multi-key 연산 제한:</strong> 서로 다른 노드에 있는 Key 간 트랜잭션이나 <code>MGET</code> 사용이 어려움.</li>
</ul>
<hr>
<h2 id="4-비교">4. 비교</h2>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">Standalone (Replication)</th>
<th align="left">Sentinel</th>
<th align="left">Cluster</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>목적</strong></td>
<td align="left">단순 복제, 읽기 분산</td>
<td align="left"><strong>고가용성 (HA)</strong></td>
<td align="left"><strong>확장성 (Sharding)</strong> + HA</td>
</tr>
<tr>
<td align="left"><strong>데이터 분산</strong></td>
<td align="left">불가</td>
<td align="left">불가</td>
<td align="left"><strong>가능 (노드별 분할)</strong></td>
</tr>
<tr>
<td align="left"><strong>장애 복구</strong></td>
<td align="left">수동</td>
<td align="left"><strong>자동 (Sentinel 개입)</strong></td>
<td align="left"><strong>자동 (자체 수행)</strong></td>
</tr>
<tr>
<td align="left"><strong>쓰기 성능</strong></td>
<td align="left">Master 1대 한계</td>
<td align="left">Master 1대 한계</td>
<td align="left"><strong>노드 추가 시 증가</strong></td>
</tr>
<tr>
<td align="left"><strong>구현 난이도</strong></td>
<td align="left">하</td>
<td align="left">중</td>
<td align="left">상</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-결론-선택-가이드">5. 결론: 선택 가이드</h2>
<ul>
<li><strong>소규모 / 단순 캐시:</strong> <code>Standalone (Replication)</code>으로 충분함.</li>
<li><strong>다운타임 치명적 / 데이터 적음:</strong> <code>Sentinel</code> 도입해 자동 장애 복구 체계 구축.</li>
<li><strong>대규모 데이터 / 쓰기 트래픽 많음:</strong> <code>Cluster</code>로 데이터 분산 처리 필요.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CrateDB 클러스터링 아키텍처와 고가용성(HA) 메커니즘 관련 정리]]></title>
            <link>https://velog.io/@oi_24/CrateDB-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-%EA%B3%A0%EA%B0%80%EC%9A%A9%EC%84%B1HA-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98-%EA%B4%80%EB%A0%A8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@oi_24/CrateDB-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-%EA%B3%A0%EA%B0%80%EC%9A%A9%EC%84%B1HA-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98-%EA%B4%80%EB%A0%A8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 29 Dec 2025 05:42:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Docker Compose 기반 3-Node CrateDB 클러스터 구축 시 필수적인 핵심 개념(Node, Shard, Replica)과 장애 발생 시 자동 복구(Failover) 프로세스 정리.</p>
</blockquote>
<hr>
<h2 id="1-핵심-아키텍처-정의">1. 핵심 아키텍처 정의</h2>
<h3 id="🔹-노드-node">🔹 노드 (Node)</h3>
<ul>
<li><strong>정의:</strong> 클러스터를 구성하는 하나의 서버 인스턴스(컨테이너).</li>
<li><strong>역할:</strong> 데이터 저장, 클라이언트 요청 처리 및 노드 간 통신을 통한 클러스터 형성.</li>
<li><strong>구성:</strong> 현재 3개의 컨테이너(<code>node1</code>, <code>node2</code>, <code>node3</code>)가 하나의 논리적 클러스터(<code>crate-cluster</code>)로 결합.</li>
</ul>
<h3 id="🔹-샤드-shard">🔹 샤드 (Shard)</h3>
<ul>
<li><strong>정의:</strong> 대용량 데이터의 분산 저장을 위해 테이블을 논리적으로 분할한 단위.</li>
<li><strong>목적:</strong> 데이터 수평적 확장(Scale-out) 및 병렬 처리 지원.</li>
<li><strong>설정:</strong> 별도 설정 부재 시 테이블당 기본 <strong>4개의 샤드</strong>로 분할 및 노드 간 균등 배포.</li>
</ul>
<h3 id="🔹-레플리카-replica">🔹 레플리카 (Replica)</h3>
<ul>
<li><strong>정의:</strong> 데이터 유실 방지를 위한 원본 샤드(Primary Shard)의 복제본.</li>
<li><strong>규칙:</strong> 가용성 보장을 위해 <strong>반드시 원본 샤드와 서로 다른 노드에 배치.</strong></li>
<li><strong>설정:</strong> 3-Node 구성 시 <code>replicas=1</code> (원본 1 + 사본 1) 자동 적용.</li>
</ul>
<hr>
<h2 id="2-데이터-저장-프로세스-normal-state">2. 데이터 저장 프로세스 (Normal State)</h2>
<p>데이터 쓰기(<code>INSERT</code>) 요청 시의 내부 처리 흐름.</p>
<ol>
<li><strong>라우팅 (Routing):</strong> 데이터 ID의 해시(Hash) 연산을 통해 저장될 샤드 위치 결정.</li>
<li><strong>동기 복제 (Synchronous Replication):</strong><ul>
<li><strong>Primary Shard(원본) 기록:</strong> 라우팅된 노드의 원본 샤드에 데이터 기록.</li>
<li><strong>Replica Shard(복제본) 전파:</strong> 즉시 다른 노드의 복제본 샤드로 데이터 전송 및 기록 요청.</li>
<li><strong>응답(Ack):</strong> 원본과 복제본 <strong>모두 저장 완료 확인 후</strong> 클라이언트에 성공 응답 반환.</li>
</ul>
</li>
<li><strong>특징:</strong> 강력한 데이터 정합성(Consistency) 보장 및 단일 노드 장애 시 데이터 유실 방지.</li>
</ol>
<hr>
<h2 id="3-장애-대응-및-자동-복구-failover-scenario">3. 장애 대응 및 자동 복구 (Failover Scenario)</h2>
<p><strong>시나리오:</strong> 정상 운영(<code>Green</code>) 중 <strong>Node 3 다운</strong> 발생 시 클러스터의 대응 메커니즘.</p>
<h3 id="단계-1-장애-감지-및-승격-failover">단계 1: 장애 감지 및 승격 (Failover)</h3>
<p>Node 3에 위치하던 Primary Shard 소실 발생.</p>
<ul>
<li><strong>승격 (Promotion):</strong> 잔존 노드(Node 1 또는 2)에 위치한 <strong>Replica Shard를 즉시 Primary Shard로 승격.</strong></li>
<li><strong>가용성 유지:</strong> 새로운 Primary를 통해 읽기/쓰기 서비스 지속.</li>
<li><strong>상태 변경:</strong> 클러스터 상태 <code>Green</code> ➜ <code>Yellow</code> 전환 (서비스 정상, 복제본 부족 상태).</li>
</ul>
<h3 id="단계-2-재복제-re-replication--self-healing">단계 2: 재복제 (Re-replication / Self-Healing)</h3>
<p>승격 후 <code>replicas=1</code> 정책 미달(Replica 부재) 상태 해소 과정.</p>
<ul>
<li><strong>복제 수행:</strong> 잔존 노드 중 데이터가 없는 노드에 <strong>새로운 Replica Shard 생성 및 데이터 복사.</strong></li>
<li><strong>복구 완료:</strong> 원본 1 + 사본 1 구조 재확립.</li>
<li><strong>상태 변경:</strong> 클러스터 상태 <code>Yellow</code> ➜ <code>Green</code> 복귀.</li>
</ul>
<hr>
<h2 id="4-노드-복구와-동기화-node-recovery">4. 노드 복구와 동기화 (Node Recovery)</h2>
<p>다운되었던 Node 3 재기동 및 클러스터 재합류 시 동작.</p>
<h3 id="peer-recovery-델타-동기화">Peer Recovery (델타 동기화)</h3>
<p>전체 데이터 복사가 아닌 효율적인 복구 수행.</p>
<ol>
<li><strong>비교 (Check):</strong> Node 3의 데이터 상태와 현재 Primary Shard의 상태 비교.</li>
<li><strong>동기화 (Sync):</strong> 다운타임 동안 발생한 <strong>변경분(Delta) 트랜잭션만 전송</strong> 및 반영.</li>
<li><strong>리밸런싱 (Rebalancing):</strong> 데이터 균형을 위해 특정 노드에 편중된 샤드를 Node 3로 재배치.</li>
</ol>
<hr>
<h2 id="5-요약-추상화와-투명성">5. 요약: 추상화와 투명성</h2>
<p>애플리케이션(Client) 관점에서의 이점.</p>
<ul>
<li><strong>투명성 (Transparency):</strong> 샤딩, 복제, 승격 등 내부 복잡성은 DBMS 엔진이 전담. 개발자는 물리적 노드 상태와 무관하게 논리적 엔드포인트 사용.</li>
<li><strong>코디네이터 노드 (Coordinator Node):</strong> 클라이언트 요청을 수신한 노드가 최신 클러스터 메타데이터를 기반으로 정확한 데이터 위치(Primary Shard)로 쿼리 라우팅 수행.</li>
<li><strong>결론:</strong> 인프라 장애 발생 시에도 <strong>지속적인 서비스 제공(High Availability)</strong> 및 <strong>데이터 무결성</strong> 보장.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker Compose로 CrateDB 클러스터 구성하기]]></title>
            <link>https://velog.io/@oi_24/Docker-Compose%EB%A1%9C-CrateDB-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@oi_24/Docker-Compose%EB%A1%9C-CrateDB-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 29 Dec 2025 02:34:04 GMT</pubDate>
            <description><![CDATA[<h2 id="docker-compose로-cratedb-클러스터-구성하기-핵심-설정-옵션분석">Docker Compose로 CrateDB 클러스터 구성하기: 핵심 설정 옵션분석</h2>
<p>Docker Compose를 사용하여 <strong>3-Node CrateDB 클러스터</strong>를 구축할 때, 가장 중요한 것은 <code>command</code> 섹션의 설정값.</p>
<p>CrateDB는 Elasticsearch를 기반으로 하기 때문에 설정 방식이 유사하며
<strong>스플릿 브레인(Split-Brain)</strong> 방지와 
<strong>데이터 무결성(Data Integrity)</strong>을 위해 아래 옵션들을 정확히 이해하고 설정해야함.</p>
<h3 id="docker-composeyml-설정-예시">docker-compose.yml 설정 예시</h3>
<pre><code class="language-yaml">  # CrateDB Node 1 (Seed / Master)
  crate-node1:
    image: crate:latest
    container_name: crate-node1
    ports:
      - &quot;4200:4200&quot;
      - &quot;5432:5432&quot;
    command: &gt;
      crate
      -Cnetwork.host=_site_
      -Ccluster.name=crate-cluster
      -Cnode.name=crate-node1
      -Cdiscovery.seed_hosts=crate-node2,crate-node3
      -Ccluster.initial_master_nodes=crate-node1,crate-node2,crate-node3
      -Cgateway.expected_data_nodes=3
      -Cgateway.recover_after_data_nodes=2
    restart: always

  # CrateDB Node 2
  crate-node2:
    image: crate:latest
    container_name: crate-node2
    command: &gt;
      crate
      -Cnetwork.host=_site_
      -Ccluster.name=crate-cluster
      -Cnode.name=crate-node2
      -Cdiscovery.seed_hosts=crate-node1,crate-node3
      -Ccluster.initial_master_nodes=crate-node1,crate-node2,crate-node3
      -Cgateway.expected_data_nodes=3
      -Cgateway.recover_after_data_nodes=2
    restart: always

  # CrateDB Node 3
  crate-node3:
    image: crate:latest
    container_name: crate-node3
    command: &gt;
      crate
      -Cnetwork.host=_site_
      -Ccluster.name=crate-cluster
      -Cnode.name=crate-node3
      -Cdiscovery.seed_hosts=crate-node1,crate-node2
      -Ccluster.initial_master_nodes=crate-node1,crate-node2,crate-node3
      -Cgateway.expected_data_nodes=3
      -Cgateway.recover_after_data_nodes=2
    restart: always</code></pre>
<h3 id="1-설정-주입-방식--c">1. 설정 주입 방식: <code>-C</code></h3>
<ul>
<li><strong>문법:</strong> <code>-C[키]=[값]</code> (예: <code>-Ccluster.name=my-cluster</code>)</li>
<li><strong>설명:</strong> CrateDB 설정 파일(<code>crate.yml</code>) 수정 없이 실행 시점에 설정 주입</li>
<li><strong>비유:</strong> Java 애플리케이션의 <code>-D</code> 시스템 프로퍼티 주입과 동일 원리</li>
</ul>
<h3 id="2-네트워크-및-식별-network--identity">2. 네트워크 및 식별 (Network &amp; Identity)</h3>
<h4 id="-cnetworkhost_site_"><code>-Cnetwork.host=_site_</code></h4>
<ul>
<li><strong>설명:</strong> CrateDB 특수 변수 <code>_site_</code> 사용. 컨테이너의 <strong>사설 IP(Private IP)</strong> 자동 감지 및 바인딩</li>
<li><strong>목적:</strong> 동일 네트워크 내 다른 노드 간 통신 허용 (기본값 <code>localhost</code>는 외부 접근 불가)</li>
</ul>
<h4 id="-cclusternamecrate-cluster"><code>-Ccluster.name=crate-cluster</code></h4>
<ul>
<li><strong>설명:</strong> 클러스터 고유 식별자 (팀 이름)</li>
<li><strong>주의:</strong> 클러스터 내 모든 노드의 값이 <strong>완벽하게 동일</strong>해야 함</li>
</ul>
<h4 id="-cnodenamecrate-node1"><code>-Cnode.name=crate-node1</code></h4>
<ul>
<li><strong>설명:</strong> 각 노드를 구분하는 고유 식별자</li>
<li><strong>목적:</strong> 장애 발생 시 로그 분석 및 문제 노드 식별</li>
</ul>
<h3 id="3-디스커버리-및-마스터-선출-discovery">3. 디스커버리 및 마스터 선출 (Discovery)</h3>
<h4 id="-cdiscoveryseed_hostscrate-node2crate-node3"><code>-Cdiscovery.seed_hosts=crate-node2,crate-node3</code></h4>
<ul>
<li><strong>의미:</strong> 초기 연결 대상 목록 (친구 전화번호부)</li>
<li><strong>설명:</strong> 클러스터 시작 시 서로를 탐색(Discovery)하여 그룹을 형성하기 위한 피어(Peer) 주소</li>
</ul>
<h4 id="-cclusterinitial_master_nodesnode1node2node3"><code>-Ccluster.initial_master_nodes=node1,node2,node3</code></h4>
<ul>
<li><strong>의미:</strong> 초기 마스터 선출 자격 노드 목록</li>
<li><strong>목적:</strong> <strong>스플릿 브레인(Split-Brain)</strong> 방지 (네트워크 단절 시 마스터가 여러 개 생기는 현상 차단)</li>
</ul>
<h3 id="4-데이터-안정성-및-복구-gateway--recovery">4. 데이터 안정성 및 복구 (Gateway &amp; Recovery)</h3>
<h4 id="-cgatewayexpected_data_nodes3"><code>-Cgateway.expected_data_nodes=3</code></h4>
<ul>
<li><strong>의미:</strong> 데이터 복구 시작을 위한 최소 노드 수 (정원 확인)</li>
<li><strong>동작:</strong> 3개 노드가 모두 감지될 때까지 데이터 복구 대기</li>
<li><strong>목적:</strong> 성급한 데이터 이동 방지 및 <strong>데이터 정합성</strong> 보장</li>
</ul>
<h4 id="-cgatewayrecover_after_data_nodes2"><code>-Cgateway.recover_after_data_nodes=2</code></h4>
<ul>
<li><strong>의미:</strong> 서비스 활성화를 위한 최소 노드 수 (과반수)</li>
<li><strong>동작:</strong> 과반수(2개) 이상 연결 시 클러스터 활성화 및 서비스 시작</li>
<li><strong>효과:</strong> 일부 노드 장애 시에도 전체 서비스 중단 방지 (<strong>고가용성/HA</strong>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[파사드 패턴을 사용해야 하는 경우]]></title>
            <link>https://velog.io/@oi_24/%ED%8C%8C%EC%82%AC%EB%93%9C-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</link>
            <guid>https://velog.io/@oi_24/%ED%8C%8C%EC%82%AC%EB%93%9C-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</guid>
            <pubDate>Thu, 27 Nov 2025 08:52:27 GMT</pubDate>
            <description><![CDATA[<h1 id="아키텍처-파사드-패턴facade-pattern-써야-하는-경우">[아키텍처] 파사드 패턴(Facade Pattern) 써야 하는 경우</h1>
<p>백엔드 개발하다 보면 Controller, Service, Repository 계층 구조에 익숙해짐.
근데 비즈니스 로직이 복잡해질수록 <strong>Service가 비대해지거나</strong>, <strong>Controller가 너무 많은 Service를 호출하는 문제</strong>가 발생함.</p>
<p>이럴 경우 사용해야 하는 것이 <strong>파사드(Facade) 레이어</strong>.
실무에서 파사드 패턴을 도입해야 하는 <strong>확실한 타이밍 3가지</strong>를 정리해 봄.</p>
<hr>
<h2 id="1-파사드facade란">1. 파사드(Facade)란?</h2>
<p>건물의 정면(출입구)을 의미하는 단어처럼, <strong>복잡한 내부 로직을 감추고 외부(Controller)에는 깔끔한 인터페이스만 보여주는 역할</strong>을 함.</p>
<blockquote>
<p><strong>핵심 역할:</strong> Controller와 여러 Service 사이의 <strong>중간 조율자 (Orchestrator)</strong></p>
</blockquote>
<hr>
<h2 id="2-언제-써야-할까-도입-기준-3가지">2. 언제 써야 할까? (도입 기준 3가지)</h2>
<h3 id="case-1-컨트롤러가-너무-많은-서비스를-의존할-때">Case 1. 컨트롤러가 너무 많은 서비스를 의존할 때</h3>
<p>하나의 API 요청을 처리하기 위해 3~4개의 Service를 호출해야 한다면, Controller가 과도한 책임을 지고 있다는 신호임.</p>
<ul>
<li><strong>Before (Controller가 바쁨):</strong>
  Controller가 <code>OrderService</code>, <code>PaymentService</code>, <code>DeliveryService</code>를 다 주입받아서 순서대로 호출함. 로직이 Controller에 노출됨.</li>
<li><strong>After (Facade 도입):</strong>
  Controller는 <code>OrderFacade</code> 하나만 알고 있음. &quot;주문해줘&quot;라고 요청하면 끝.</li>
</ul>
<h3 id="case-2-서비스-간의-순환-참조circular-dependency를-끊을-때">Case 2. 서비스 간의 순환 참조(Circular Dependency)를 끊을 때</h3>
<p>개발하다 보면 <code>UserService</code>가 <code>PointService</code>를 참조하고, 반대로 <code>PointService</code>가 <code>UserService</code>를 참조해야 하는 상황이 옴. 이때 서로 <code>import</code> 하면 <strong>순환 참조 에러</strong>가 발생함.</p>
<p>이때 <strong>파사드</strong>가 두 서비스를 위에서 내려다보며 조율하면, 서비스끼리는 서로 몰라도 되므로 순환 참조가 깔끔하게 해결됨.</p>
<h3 id="case-3-트랜잭션-단위가-여러-서비스에-걸쳐-있을-때">Case 3. 트랜잭션 단위가 여러 서비스에 걸쳐 있을 때</h3>
<p>여러 서비스의 로직이 <strong>&#39;전부 성공하거나, 전부 실패해야 하는(Atomic)&#39;</strong> 경우임.
개별 Service에 트랜잭션을 거는 것만으로는 부족할 때, 파사드 메서드에 <code>@Transactional</code>을 걸어 전체 흐름을 하나의 트랜잭션으로 묶어주기 좋음.</p>
<hr>
<h2 id="3-코드로-보는-before--after-python">3. 코드로 보는 Before &amp; After (Python)</h2>
<p>사용자가 &#39;상품 구매&#39;를 요청했을 때의 흐름 비교.</p>
<h3 id="before-controller가-모든-로직을-제어">Before: Controller가 모든 로직을 제어</h3>
<p>Controller가 비즈니스 흐름(재고 확인 -&gt; 결제 -&gt; 알림)을 다 알고 있음. 코드가 지저분하고 재사용이 어려움.</p>
<pre><code class="language-python"># controller.py

class OrderController:
    def __init__(self, inventory_svc, payment_svc, noti_svc):
        self.inventory_svc = inventory_svc
        self.payment_svc = payment_svc
        self.noti_svc = noti_svc

    def order(self, request):
        # 1. 재고 감소
        self.inventory_svc.decrease(request.product_id)

        # 2. 결제 시도
        self.payment_svc.pay(request.user_id, request.amount)

        # 3. 알림 발송
        self.noti_svc.send(request.user_id, &quot;주문 완료&quot;)

        return &quot;Success&quot;</code></pre>
<h3 id="after-facade-레이어-도입">After: Facade 레이어 도입</h3>
<p>Controller는 단순해지고, 비즈니스 흐름의 조합은 Facade가 전담함.</p>
<pre><code class="language-python"># facades/order_facade.py

class OrderFacade:
    def __init__(self, inventory_svc, payment_svc, noti_svc):
        self.inventory_svc = inventory_svc
        self.payment_svc = payment_svc
        self.noti_svc = noti_svc

    # 여러 서비스의 흐름을 하나의 트랜잭션으로 관리하기 용이함
    def process_order(self, user_id, product_id, amount):
        self.inventory_svc.decrease(product_id)
        self.payment_svc.pay(user_id, amount)
        self.noti_svc.send(user_id, &quot;주문 완료&quot;)

# controller.py

class OrderController:
    def __init__(self, order_facade):
        self.order_facade = order_facade

    def order(self, request):
        # 깔끔해진 컨트롤러
        self.order_facade.process_order(
            request.user_id, 
            request.product_id, 
            request.amount
        )
        return &quot;Success&quot;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL로 머신러닝을 구축: MindsDB(1)]]></title>
            <link>https://velog.io/@oi_24/SQL%EB%A1%9C-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D%EC%9D%84-%EA%B5%AC%EC%B6%95-MindsDB1</link>
            <guid>https://velog.io/@oi_24/SQL%EB%A1%9C-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D%EC%9D%84-%EA%B5%AC%EC%B6%95-MindsDB1</guid>
            <pubDate>Mon, 10 Nov 2025 05:03:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/oi_24/post/5470fcea-f7b5-401a-8637-acd8a37f3147/image.png" alt=""></p>
<h3 id="0-사용한-계기">0. 사용한 계기</h3>
<pre><code>회사에서 대규모 로그 데이터에 대한 활용방안을 논의하다가
로그 데이터를 학습 데이터로 전환하여 활용하고
해당 데이터로 예측을 해보는 것에 대하여</code></pre><hr>
<h3 id="1-mindsdb란-">1. MindsDB란 ?</h3>
<blockquote>
<ul>
<li>SQL 인터페이스를 통해 데이터베이스 내/외부 데이터로 예측 모델을 구축하고 쿼리할 수 있게 해주는 오픈 소스 플랫폼</li>
</ul>
</blockquote>
<hr>
<h3 id="2-환경-설정-및-시작">2. 환경 설정 및 시작</h3>
<h4 id="1-git-저장소-복제">1. Git 저장소 복제</h4>
<pre><code>git clone https://github.com/mindsdb/mindsdb.git</code></pre><h4 id="2-docker-이미지-빌드-실행">2. Docker 이미지 빌드 실행</h4>
<pre><code>sudo docker build -t mindsdb:ltscpu .</code></pre><h4 id="3-컨테이너-실행">3. 컨테이너 실행</h4>
<pre><code>docker run -p [외부_포트]:47334 
-e MINDSDB_API_PORT=47334 -d mindsdb:ltscpu</code></pre><p><img src="https://velog.velcdn.com/images/oi_24/post/188b664a-46c1-441b-9e2e-56a5d8c3e8fa/image.png" alt=""></p>
<h4 id="4-mindsdb-studio">4. MindsDB Studio</h4>
<p><img src="https://velog.velcdn.com/images/oi_24/post/c3a24002-494a-4ef6-91c8-6d466d908c9d/image.png" alt=""></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Logstash JDBC Paging]]></title>
            <link>https://velog.io/@oi_24/Logstash-JDBC-Paging</link>
            <guid>https://velog.io/@oi_24/Logstash-JDBC-Paging</guid>
            <pubDate>Wed, 09 Apr 2025 04:34:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Logstash를 사용하여 ElasticSearch에 2억건의 데이터를 indexing 과정에 OutOfMemoryError가 나옴
CrateDB는 memory.breaker.limit 설정으로 각 쿼리에서 사용할 수 있는 메모리 상한선을 두고 있는데그 허용한도를 넘어버림</p>
</blockquote>
<pre><code>ERROR: [query] Data too large, data for [mergeOnHandler: 1] 
would be [1288635950/1.2gb], 
which is larger than the limit of [1288490188/1.1gb]
</code></pre><h4 id="해결-jdbc-쿼리를-페이징-처리">해결: JDBC 쿼리를 페이징 처리</h4>
<pre><code># logstash.conf 파일에 아래와 같은 페이징 처리를 함
  jdbc_paging_enabled =&gt; true
  jdbc_page_size =&gt; 10000</code></pre><h4 id="정리">정리</h4>
<pre><code>Logstash가 내부적으로 페이징 쿼리를 자동 생성하여 
한 번에 너무 많은 데이터를 가져오지 않도록 하여 처리

해당 방법은 데이터 양이 많을수록 OFFSET이 커져서 쿼리 성능이 급격히 나빠질 수 있음

이번 indexing 작업은 인스턴스 성격임으로 해당 방식으로 해결</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[ElasticSearch 기초 개념 정리]]></title>
            <link>https://velog.io/@oi_24/ElasticSearch-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@oi_24/ElasticSearch-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 07 Apr 2025 08:31:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기존의 회사 솔루션에 golang으로 만들어진 검색엔진이 처리하는 데이터가 많으면 느리다 라는 사용자의 요구사항으로 인해 ElasticSearch를 도입해보려고 함 </p>
</blockquote>
<h3 id="1-엘라스틱서치-기본-개념">1. 엘라스틱서치 기본 개념</h3>
<pre><code>1. 오픈소스 기반의 분산형 검색 및 분석 엔진

2. 텍스트 기반의 데이터를 빠르게 검색, 필터, 집계

3. 기본적으로 JSON 기반의 RESTful API로 동작

4. 내부적으로는 Apache Lucene을 사용
 - Apache Lucene이란? 
     자바(Java)로 작성된 풀텍스트 검색 엔진 라이브러리</code></pre><h3 id="2-왜-사용하는가">2. 왜 사용하는가</h3>
<pre><code> 역색인(inverted index) 이라는 구조로 검색어 위치를 기억해서 매우 빠름</code></pre><h3 id="3-사용-사례">3. 사용 사례</h3>
<pre><code>1. 검색창 자동완성
2. 로그 분석
3. 추천 시스템</code></pre><h3 id="4-기본-용어-정리">4. 기본 용어 정리</h3>
<pre><code>1. Index (인덱스): 
    데이터가 저장되는 공간. RDBMS의 데이터베이스에 해당

2. Document (문서): 
    실제로 저장되는 JSON 형태의 단일 데이터 (RDB의 row)

3. Field (필드):
    Document 안의 key (RDB의 column)

4. Mapping:
    필드에 대한 스키마 정의

5. Inverted Index:
    검색을 빠르게 하기 위한 구조. 단어 -&gt; 문서 ID 매핑 구조

6. Cluster:
    Elasticsearch 전체 시스템 단위. 하나 이상의 노드로 구성

7. Node:
    Elasticsearch 인스턴스 1개 (보통 컨테이너 1개)

8. Shard:
    인덱스를 물리적으로 분할한 단위. 실제 데이터를 저장

9. Primary Shard:
    실제 데이터를 저장하는 주 샤드

10. Replica Shard:
    Primary의 복제본. 장애 대비 및 읽기 처리

11. Routing:
    어떤 문서를 어떤 샤드에 넣을지 결정하는 해싱 로직

12. text:
    분석기를 거쳐 토큰화되어 저장됨. 자연어 검색에 적합

13. keyword:
    전체 문자열을 그대로 저장. 정렬/집계/필터에 적합

14. fields:
    한 필드를 text + keyword 같이 여러 타입으로 저장할 때 사용

15. doc_values:
    keyword 등 집계/정렬용 필드의 내부 디스크 구조


[Cluster]
  └─ [Node]
        └─ [Shard]   -&gt; Primary / Replica
              └─ [Index Data (partial)]
                    └─ [Document]
</code></pre><h3 id="5-간단하게-흐름-정리">5. 간단하게 흐름 정리</h3>
<pre><code>1. JSON 데이터를 Elasticsearch에 보내면 분석 -&gt; 역색인 생성 -&gt; 저장
    -&gt; Indexing

    * 역색인(Inverted Index): &quot;단어-&gt;문서 번호 목록&quot; 으로 연결된 자료구조

2. 검색 요청 시 단어 분석 -&gt; 색인에서 문서 찾기 -&gt; 점수 계산 -&gt; 결과 반환
    -&gt; Search</code></pre><pre><code>ex)
&quot;삼성전자는 반도체를 개발한다.&quot;
    1. 분석기(analyzer)가 단어 추출: -&gt; [&quot;삼성전자&quot;, &quot;반도체&quot;, &quot;개발&quot;]

    2. 역색인에 저장:
      {
        &quot;삼성전자&quot;: [문서1],
        &quot;반도체&quot;: [문서1],
        &quot;개발&quot;: [문서1]
      }

    3. 사용자가 &quot;반도체 개발&quot; 검색

    4. 검색어 분석 -&gt; [&quot;반도체&quot;, &quot;개발&quot;]

    5. 기존에 만들어진 역색인에서 찾음:
      반도체 -&gt; 문서1, 문서5
      개발 -&gt; 문서1, 문서2

    6. 공통 문서인 문서1을 우선적으로 반환

</code></pre><h3 id="6-개인적인-의문사항">6. 개인적인 의문사항</h3>
<pre><code>1. Elastic Search 는 RDBMS의 테이블 개념이 없는가?
 - Elasticsearch는 스키마가 없는 문서 기반 저장소이기 때문에
  RDB의 Table처럼 명확히 정의된 구조가 없고, Index 안에 바로 Document들이 있음.

  ex)
    PUT /blog_posts
    {
      &quot;mappings&quot;: {
        &quot;properties&quot;: {
          &quot;title&quot;:   { &quot;type&quot;: &quot;text&quot; },
          &quot;content&quot;: { &quot;type&quot;: &quot;text&quot; },
          &quot;author&quot;:  { &quot;type&quot;: &quot;keyword&quot; }
        }
      }
    }
    -&gt; 이런식으로 각각의 JSON 문서가 RDB의 한 &quot;row&quot;에 해당
</code></pre>]]></description>
        </item>
    </channel>
</rss>