<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jk-kim</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 26 Apr 2026 15:05:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jk-kim</title>
            <url>https://velog.velcdn.com/images/jw_kim/profile/f07c5a9f-5913-42b4-afd1-5b4776d3a185/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jk-kim. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jw_kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Claude Code + Spec Kit으로 스펙 주도 개발(SDD) 시작하기]]></title>
            <link>https://velog.io/@jw_kim/Claude-Code-Spec-Kit%EC%9C%BC%EB%A1%9C-%EC%8A%A4%ED%8E%99-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9CSDD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jw_kim/Claude-Code-Spec-Kit%EC%9C%BC%EB%A1%9C-%EC%8A%A4%ED%8E%99-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9CSDD-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 26 Apr 2026 15:05:38 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>요즘 AI 코딩 에이전트 뉘앙에 힙스되는 개발 스타일이 있습니다. 바로 &quot;블라인드하게 코드를 짜다(vibe coding)&quot;. AI가 코드를 품품 내줘주니 빠르게 만들 수는 있지만, 스펙 없이 만들어진 코드는 나중에 센센이 엽혀 다시 짜는 상황이 생길 수 있습니다.</p>
<p>GitHub이 만든 오픈소스 툴킷인 <strong>Spec Kit</strong>은 이 문제를 다릅니다. 코드를 짜기 전에 <strong>뜻(스펙)을 먼저 정의</strong>하고, AI가 그를 실현하도로 유도하는 <strong>Spec-Driven Development(SDD)</strong> 방식을 지향합니다. Claude Code와 조합하면 몇 가지 커맨드만으로 스펙 작성부터 구현까지 일관된 플로우를 만들어낼 수 있습니다.</p>
<h2 id="spec-kit이란">Spec Kit이란?</h2>
<p>Spec Kit은 GitHub이 2025년에 공개한 오픈소스 툴킷으로, <strong>스펙 주도 개발(Spec-Driven Development)</strong>을 실천하기 위한 도구입니다. <code>specify</code>라는 CLI 도구와 AI 코딩 에이전트에서 사용할 수 있는 슬래시 커맨드들을 제공합니다.</p>
<p>SDD의 핵심 아이디어는 단순합니다.</p>
<blockquote>
<p><strong>코드를 짜기 전에 무엇을(what) 만들지를 명확히 하라.</strong>
스펙이 코드를 영도하고, 코드는 스펙을 구현한다.</p>
</blockquote>
<p>Claude Code, Gemini CLI, Cursor, Copilot 등 30개 이상의 AI 코딩 에이전트와 호환되며, 이 글에서는 <strong>Claude Code</strong> 기준으로 설명합니다.</p>
<h2 id="설치-및-쓰이-환경-준비">설치 및 쓰이 환경 준비</h2>
<h3 id="1-claude-code-설치">1. Claude Code 설치</h3>
<p>Node.js 18+이 설치되어 있어야 합니다.</p>
<pre><code class="language-bash">npm install -g @anthropic-ai/claude-code
claude  # 실행 후 Anthropic API 키 입력</code></pre>
<h3 id="2-spec-kit-clispecify-설치">2. Spec Kit CLI(specify) 설치</h3>
<p>공식 설치는 PyPI가 아닌 GitHub 레포 직접에서 해야 합니다.</p>
<pre><code class="language-bash"># uv를 사용하는 경우 (권장)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git

# pipx를 사용하는 경우
pipx install git+https://github.com/github/spec-kit.git

# 설치 확인
specify version</code></pre>
<h2 id="sdd-워크플로우-5단계">SDD 워크플로우: 5단계</h2>
<p>Spec Kit은 다음 5단계 플로우로 개발을 진행합니다.</p>
<h3 id="step-0-프로젝트-초기화">STEP 0: 프로젝트 초기화</h3>
<p>프로젝트 디렉토리를 만들고 <code>specify init</code>으로 템플릿을 초기화합니다.</p>
<pre><code class="language-bash"># 신규 프로젝트
specify init MyApp
cd MyApp

# 기존 프로젝트에 적용
cd existing-project
specify init . --integration copilot
# Claude Code를 쓸 경우
specify init . --integration claude</code></pre>
<p>완료되면 <code>.specify/</code> 디렉토리와 <code>CLAUDE.md</code>가 생성됩니다. 또한 Claude Code에 <code>/speckit.*</code> 슬래시 커맨드들이 등록됩니다.</p>
<h3 id="step-1-원칙constitution-수립">STEP 1: 원칙(Constitution) 수립</h3>
<p>Claude Code를 실행한 뒤 <code>/speckit.constitution</code> 커맨드로 프로젝트의 개발 원칙을 수립합니다.</p>
<pre><code>claude

# Claude Code 안에서
/speckit.constitution 코드 품질, 테스트 커버리지, 성능 등을 중심으로 한 개발 원칙을 수립해줘</code></pre><p>이 커맨드는 <code>.specify/memory/constitution.md</code> 파일을 생성하며, 이후 모든 AI 응답의 기준이 됩니다.</p>
<h3 id="step-2-스펙spec-작성">STEP 2: 스펙(Spec) 작성</h3>
<p><code>/speckit.specify</code>로 만들 것에 대한 요구사항을 자연어로 작성합니다. 기술 스택은 여기서 언급하지 않는 게 포인트입니다.</p>
<pre><code>/speckit.specify 사용자가 할 일 목록을 관리하는 앱을 만들어줘.
사용자는 할 일을 추가/삭제/완료 체크할 수 있고,
우선순위를 지정할 수 있어야 해.
날짜 기준으로 정렬하는 기능도 있었으면 좋겠어.</code></pre><p>AI가 요구사항을 분석해 <code>.specify/specs/001-todo-app/spec.md</code> 파일을 생성합니다. User Story와 엑셍스 커버리지 충 등이 자동으로 정리됩니다.</p>
<h3 id="step-3-구현-계획plan-생성">STEP 3: 구현 계획(Plan) 생성</h3>
<p><code>/speckit.plan</code>에서 이제 기술 스택과 아키텐쳐를 알려줍니다.</p>
<pre><code>/speckit.plan React + TypeScript를 사용해서 만들어줘.
상태 관리는 Zustand, 데이터 저장은 localStorage 사용.
UI는 Tailwind CSS로 슬림하게 만들어줘.</code></pre><p>AI는 <code>plan.md</code>, <code>data-model.md</code> 등 세부 구현 문서를 생성합니다.</p>
<h3 id="step-4-태스크-분해">STEP 4: 태스크 분해</h3>
<p><code>/speckit.tasks</code>로 계획을 실행 가능한 태스크 단위로 취계합니다.</p>
<pre><code>/speckit.tasks</code></pre><p><code>tasks.md</code> 파일이 생성되며, 병렬 실행 가능한 태스크는 <code>[P]</code> 표시로 마크됩니다. 의존성 순서를 지켜서 태스크가 나열돬으며, 각 User Story별로 묵여서 정리됩니다.</p>
<h3 id="step-5-구현">STEP 5: 구현</h3>
<p><code>/speckit.implement</code>으로 AI가 tasks.md를 읽고 실제 코드를 작성합니다.</p>
<pre><code>/speckit.implement</code></pre><p>Claude Code는 constitution → spec → plan → tasks 순서로 컨텍스트를 확인하면서 코드를 작성합니다. 실행에 필요한 CLI 도구(npm, git 등)도 직접 호출합니다.</p>
<h2 id="유용한-추가-커맨드">유용한 추가 커맨드</h2>
<p>Spec Kit은 코어 5단계 외에도 성능/품질 강화를 위한 선택적 커맨드를 제공합니다.</p>
<table>
<thead>
<tr>
<th>커맨드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>/speckit.clarify</code></td>
<td>스펙 작성 후 더 명확하게 요구사항을 정리</td>
</tr>
<tr>
<td><code>/speckit.analyze</code></td>
<td>태스크 생성 후 컨시스턴시 검사</td>
</tr>
<tr>
<td><code>/speckit.checklist</code></td>
<td>요구사항 완성도 체크리스트 생성</td>
</tr>
</tbody></table>
<h2 id="커뮤니티-익스텐션-활용">커뮤니티 익스텐션 활용</h2>
<p>Spec Kit은 커뮤닄티가 만든 익스텐션을 <code>specify extension add</code>로 설치할 수 있습니다. 주목할 만한 것들로는:</p>
<ul>
<li><strong>spec-kit-review</strong>: 구현 후 코드 리뷰를 자동화</li>
<li><strong>spec-kit-cleanup</strong>: 구현 후 리팩토링과 소리 로직 수정</li>
<li><strong>spec-kit-jira</strong>: Jira 연동으로 태스크 자동 생성</li>
<li><strong>spec-kit-pr-bridge</strong>: PR 설명을 스펙에서 자동 생성</li>
<li><strong>spec-kit-security-review</strong>: 보안 삐넓 자동 검사</li>
</ul>
<pre><code class="language-bash"># 익스텐션 검색
specify extension search

# 설치
specify extension add spec-kit-review</code></pre>
<h2 id="정리">정리</h2>
<p>Spec Kit + Claude Code 조합은 AI 코딩의 장점은 살리면서, 스펙 없이 코드를 짜는 단점을 보완하는 접근법입니다. 특히 팀에서 AI 코딩 에이전트를 사용할 때, 스펙이 있으면 AI가 일관성 있는 코드를 생성하는 데 큰 도움이 됩니다.</p>
<ul>
<li>코드를 짜기 전에 <strong>뜻을 먼저</strong> 정의한다</li>
<li>AI는 스펙을 <strong>구현</strong>한다</li>
<li>스펙은 프로젝트 전체의 <strong>진실의 원천(single source of truth)</strong>이 된다</li>
</ul>
<p>이 세 원칙만 지켜도 AI 코딩의 편의성을 영리하지 않으면서도 훨씬 더 안정적인 개발을 할 수 있습니다.</p>
<hr>
<h2 id="실전-워크플로우-내가-실제로-사용하는-방식">실전 워크플로우: 내가 실제로 사용하는 방식</h2>
<p>Spec Kit 공식 가이드의 5단계 흐름에 더해, 실제로 운용하면서 효과적이라고 확인한 구체적인 방식을 공유합니다.</p>
<h3 id="1단계-constitution--tdd-강제--금지-원칙-중심으로-수립">1단계: Constitution — TDD 강제 + 금지 원칙 중심으로 수립</h3>
<p>Constitution을 작성할 때 가장 중요하게 생각하는 두 가지 원칙이 있습니다.</p>
<p><strong>TDD 강제</strong>: AI가 작성하는 모든 코드는 테스트 코드로 동작이 검증되고 명세될 수 있어야 합니다. 단순히 &quot;테스트를 작성해줘&quot;라고 하는 게 아니라, Constitution 수준에서 강제해야 AI가 구현 후 테스트를 붙이는 게 아니라 테스트를 명세로 먼저 정의하는 방향으로 작업합니다.</p>
<p><strong>허가 원칙보다 금지 원칙</strong>: AI는 모호한 상황에서 &quot;일단 해도 되겠지&quot;라고 판단하는 경향이 있습니다. &quot;이런 건 해도 된다&quot;는 허가 목록보다, &quot;이런 건 절대 하지 마라&quot;는 금지 목록이 훨씬 효과적으로 작동합니다. 예를 들어 &quot;도메인 로직을 UI 레이어에 작성하지 말 것&quot;, &quot;테스트 없이 구현 PR을 올리지 말 것&quot; 같은 식으로 구체적인 금지 지침을 명시합니다.</p>
<h3 id="2단계-specify--요구사항을-최대한-구체적으로">2단계: Specify — 요구사항을 최대한 구체적으로</h3>
<p><code>/speckit.specify</code>를 호출할 때 전달받은 요구사항이나 구현 목표를 최대한 상세하게 설명합니다. 명령 후 <code>.specify/specs/</code> 하위에 작업 디렉토리가 생성되는 것을 확인할 수 있습니다.</p>
<h3 id="3단계-clarify-반복--기획-구체화의-핵심">3단계: Clarify 반복 — 기획 구체화의 핵심</h3>
<p>이 단계가 전체 워크플로우에서 가장 공을 들이는 부분입니다. <code>/speckit.clarify</code>를 <strong>N번 반복 실행</strong>합니다.</p>
<p>실행할 때마다 두 가지 방식을 병행합니다.</p>
<ul>
<li><strong>능동적 구체화</strong>: 내가 직접 구체화가 필요한 지점을 명시해서 AI에게 인터뷰를 요청합니다.</li>
<li><strong>수동적 검토</strong>: 작성된 spec을 바탕으로 AI가 스스로 모호하거나 구체화가 필요한 지점을 찾아 인터뷰를 진행합니다.</li>
</ul>
<p>이 과정을 반복할수록 초기에는 보이지 않던 엣지 케이스, 정책 미결정 사항, 도메인 용어의 모호함 등이 드러납니다. clarify를 충분히 돌리고 나면 spec이 상당히 두꺼워지는데, 이게 나중에 plan과 tasks의 품질을 결정합니다.</p>
<h3 id="4단계-analyze--plan-전-일관성-검사">4단계: Analyze — Plan 전 일관성 검사</h3>
<p>plan을 수립하기 전에 <code>/speckit.analyze</code>를 실행해 spec 문서들 간의 일관성을 검사합니다. clarify를 여러 번 반복하다 보면 앞뒤가 맞지 않는 내용이 생길 수 있는데, 이 단계에서 미리 잡아두면 plan과 tasks 단계에서 AI가 엉뚱한 방향으로 작업하는 것을 막을 수 있습니다.</p>
<h3 id="5단계-plan--구현-계획-수립">5단계: Plan — 구현 계획 수립</h3>
<p><code>/speckit.plan</code>으로 기술 스택과 아키텍처를 명시하여 구현 계획을 수립합니다. 앞 단계에서 spec이 충분히 구체화되어 있으면 plan의 완성도도 자연히 높아집니다.</p>
<h3 id="6단계-tasks--원자적-단위로-분해">6단계: Tasks — 원자적 단위로 분해</h3>
<p><code>/speckit.tasks</code>로 plan을 태스크 단위로 분해하는 단계입니다. 이 단계의 품질이 구현 결과를 좌우합니다.</p>
<p>태스크를 나눌 때 핵심 기준은 다음과 같습니다.</p>
<ul>
<li><strong>원자적 단위</strong>: 하나의 태스크는 하나의 책임만 가집니다.</li>
<li><strong>도메인/계층 경계 존중</strong>: 도메인 경계나 계층 간 경계를 침범하지 않도록 분리합니다.</li>
<li><strong>독립 실행 가능</strong>: 독립적인 에이전트가 혼자서 작업할 수 있는 수준으로 분해합니다.</li>
</ul>
<p>이렇게 하면 두 가지 이점이 생깁니다. 개발자 입장에서 AI가 작업한 내용을 태스크 단위로 검토하기가 수월해지고, 기능 단위가 잘게 쪼개져 있어 추후 유지보수도 용이해집니다.</p>
<h3 id="7단계-implement--taskphase-단위-순차-검증">7단계: Implement — Task/Phase 단위 순차 검증</h3>
<p><code>/speckit.implement</code>로 구현을 진행할 때 전체를 한 번에 실행하기보다, <strong>태스크 단위로 실행하고 검증하는 사이클을 반복</strong>합니다. 경험적으로 phase 단위로 구현 작업을 나눠도 문제없이 구현됨을 확인했습니다.</p>
<p>이 방식의 장점은 AI가 작업 중 방향을 이탈했을 때 조기에 발견하고 수정할 수 있다는 점입니다. 태스크가 원자적으로 잘 분해되어 있을수록 검증 사이클도 빠르게 돌아갑니다.</p>
<hr>
<h3 id="전체-흐름-요약">전체 흐름 요약</h3>
<pre><code>constitution (TDD 강제 + 금지 원칙)
    ↓
specify (요구사항 상세 전달)
    ↓
clarify × N (기획 구체화 반복 인터뷰)
    ↓
analyze (spec 일관성 검사)
    ↓
plan (기술 스택 + 구현 계획)
    ↓
tasks (원자적 단위 분해)
    ↓
implement (task/phase 단위 순차 실행 + 검증 반복)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot + Redis: Lettuce 동작 방식과 Connection Pool]]></title>
            <link>https://velog.io/@jw_kim/Spring-Boot-Redis-Lettuce-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Connection-Pool</link>
            <guid>https://velog.io/@jw_kim/Spring-Boot-Redis-Lettuce-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Connection-Pool</guid>
            <pubDate>Sun, 26 Apr 2026 07:57:38 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Spring Boot 환경에서 Redis를 도입할 때, 단순히 의존성을 추가하고 <code>RedisTemplate</code>을 사용하는 것에서 그치는 경우가 많습니다. 하지만 Redis 클라이언트의 동작 방식을 제대로 이해하지 못하면, 운영 환경에서 예상치 못한 커넥션 폭증이나 성능 저하를 마주할 수 있습니다.</p>
<p>이 글에서는 Spring Boot에서 사용할 수 있는 Redis 클라이언트들을 비교하고, 기본 클라이언트인 <strong>Lettuce</strong>의 내부 동작 방식과 Connection Pool의 중요성에 대해 실제 트러블슈팅 경험을 바탕으로 설명합니다.</p>
<hr>
<h2 id="redis-클라이언트-비교-jedis-vs-lettuce-vs-redisson">Redis 클라이언트 비교: Jedis vs Lettuce vs Redisson</h2>
<p>Spring Boot에서 Redis를 사용할 때 선택할 수 있는 클라이언트는 크게 세 가지입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>Jedis</strong></th>
<th><strong>Lettuce</strong></th>
<th><strong>Redisson</strong></th>
</tr>
</thead>
<tbody><tr>
<td>동작 방식</td>
<td>동기, 블로킹</td>
<td>비동기, 논블로킹</td>
<td>비동기, 논블로킹</td>
</tr>
<tr>
<td>Thread-safe</td>
<td>❌ (Pool 필수)</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>Reactive 지원</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>Spring Data Redis</td>
<td>✅</td>
<td>✅</td>
<td>✅ (별도 연동)</td>
</tr>
<tr>
<td>분산락 / 고수준 기능</td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Spring Boot 기본값</td>
<td>❌</td>
<td>✅</td>
<td>❌</td>
</tr>
</tbody></table>
<p><strong>Spring Boot 2.0부터 기본 Redis 클라이언트는 Lettuce입니다.</strong> <code>spring-boot-starter-data-redis</code> 의존성만 추가하면 별도 설정 없이 Lettuce가 사용됩니다.</p>
<p>Jedis는 커넥션이 thread-safe하지 않아 멀티스레드 환경에서 반드시 Connection Pool을 사용해야 하고, 동기 블로킹 방식이라 고부하 환경에서 불리합니다. Redisson은 분산락, 분산 컬렉션 등 고수준 기능이 필요할 때 적합하며, 다음 편에서 자세히 다룰 예정입니다.</p>
<hr>
<h2 id="lettuce-아키텍처">Lettuce 아키텍처</h2>
<p>Lettuce는 <strong>Netty</strong> 기반의 비동기 논블로킹 Redis 클라이언트입니다.</p>
<h3 id="thread-safe한-이유">Thread-safe한 이유</h3>
<p>Lettuce는 내부적으로 단일 TCP 커넥션(<code>StatefulRedisConnection</code>)을 통해 모든 커맨드를 처리합니다. 커맨드는 Netty의 이벤트 루프에서 순차적으로 처리되며, 각 커맨드는 내부 큐에 적재되어 순서대로 Redis 서버에 전송됩니다.</p>
<p>이 구조 덕분에 <strong>여러 스레드가 하나의 커넥션을 공유해도 thread-safe</strong>합니다. 각 스레드가 커맨드를 큐에 넣으면, Netty 이벤트 루프가 순서대로 처리하기 때문입니다.</p>
<hr>
<h2 id="lettuce의-connection-사용-방식-두-가지">Lettuce의 Connection 사용 방식 두 가지</h2>
<p>Lettuce(+ Spring Data Redis)에서 커넥션을 사용하는 방식은 크게 두 가지로 나뉩니다.</p>
<h3 id="1-shared-connection-기본값">1. Shared Connection (기본값)</h3>
<p>별도 설정이 없으면 Lettuce는 <strong>하나의 커넥션을 모든 스레드가 공유</strong>합니다.</p>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(&quot;localhost&quot;, 6379);
        // 기본 LettuceConnectionFactory — Connection Pool 없음
        return new LettuceConnectionFactory(config);
    }
}</code></pre>
<p>일반적인 GET/SET 커맨드처럼 <strong>커넥션을 독점할 필요가 없는 작업</strong>에서는 이 방식으로 충분합니다. 커넥션 하나를 재사용하므로 리소스 효율이 높습니다.</p>
<h3 id="2-connection-pool-방식">2. Connection Pool 방식</h3>
<p><strong>커넥션을 독점적으로 점유해야 하는 작업</strong>에는 Connection Pool이 필요합니다. 대표적으로:</p>
<ul>
<li><strong>Pipeline</strong> (<code>executePipelined</code>)</li>
<li><strong>Transaction</strong> (<code>MULTI/EXEC</code>)</li>
<li><strong>Blocking 커맨드</strong> (<code>BLPOP</code> 등)</li>
</ul>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(&quot;localhost&quot;, 6379);

        LettucePoolingClientConfiguration poolingConfig = LettucePoolingClientConfiguration.builder()
            .poolConfig(connectionPoolConfig())
            .build();

        return new LettuceConnectionFactory(config, poolingConfig);
    }
}</code></pre>
<hr>
<h2 id="executepipelined와-openpipeline의-동작-원리">executePipelined()와 openPipeline()의 동작 원리</h2>
<h3 id="pipeline이란">Pipeline이란?</h3>
<p>일반적으로 Redis 커맨드는 <strong>요청 → 응답 → 요청 → 응답</strong> 형태로 round-trip마다 네트워크 지연이 발생합니다. Pipeline은 <strong>여러 커맨드를 한 번에 묶어서 전송</strong>하고 응답도 한 번에 받는 방식으로, 네트워크 왕복 비용을 크게 줄일 수 있습니다.</p>
<p>Spring Data Redis에서는 <code>RedisTemplate.executePipelined()</code>로 Pipeline을 사용합니다.</p>
<pre><code class="language-java">List&lt;Object&gt; results = redisTemplate.executePipelined((RedisCallback&lt;Object&gt;) connection -&gt; {
    for (String key : keyList) {
        connection.stringCommands().get(key.getBytes(StandardCharsets.UTF_8));
    }
    return null;
});</code></pre>
<h3 id="pipeline의-실제-동작-방식">Pipeline의 실제 동작 방식</h3>
<p>Pipeline을 사용한다고 해서 Redis 서버가 커맨드를 <strong>동시에 실행</strong>하는 것은 아닙니다.</p>
<p>Redis 서버는 <strong>싱글 스레드 이벤트 루프</strong> 기반으로 동작하므로, Pipeline으로 묶인 커맨드도 수신된 순서대로 <strong>하나씩 순차 실행</strong>됩니다.</p>
<p>Pipeline의 핵심은 <strong>서버의 실행 방식 변경이 아니라, 클라이언트 ↔ 서버 간의 네트워크 왕복(RTT) 횟수를 줄이는 것</strong>입니다.</p>
<pre><code>[ Without Pipeline ] — N번의 RTT

Client                        Redis Server
  │                                │
  │── GET key1 ──────────────────► │  ← 수신 즉시 실행
  │ ◄──────────────────── value1 ──│
  │                                │
  │── GET key2 ──────────────────► │  ← 수신 즉시 실행
  │ ◄──────────────────── value2 ──│
  │                                │
  │── GET key3 ──────────────────► │  ← 수신 즉시 실행
  │ ◄──────────────────── value3 ──│

총 RTT: 3회 (커맨드 수만큼)


[ With Pipeline ] — 1번의 RTT

Client                        Redis Server
  │                                │
  │── GET key1 ──────────────────► │ ┐
  │── GET key2 ──────────────────► │ │ 싱글 스레드가
  │── GET key3 ──────────────────► │ │ 순서대로 실행
  │                                │ │
  │ ◄────────────────── value1 ────│ │ 결과를 한번에
  │ ◄────────────────── value2 ────│ │ 응답
  │ ◄────────────────── value3 ────│ ┘

총 RTT: 1회</code></pre><blockquote>
<p>⚠️ <strong>Pipeline은 병렬 실행이 아닙니다.</strong>
서버는 커맨드를 <strong>순서대로 하나씩 실행</strong>하며, 절약되는 것은 <strong>네트워크 왕복 비용(RTT)</strong> 입니다.</p>
</blockquote>
<p>또한 RTT 외에도 <strong>시스템 콜(syscall) 비용</strong>도 줄어듭니다. Pipeline 없이는 커맨드마다 <code>read()</code> / <code>write()</code> 시스템 콜이 발생해 user space ↔ kernel space 간 컨텍스트 스위칭이 일어나지만, Pipeline을 사용하면 여러 커맨드를 단 한 번의 <code>read()</code> / <code>write()</code> 시스템 콜로 처리합니다.</p>
<p>이 두 가지 효과가 합쳐져 Redis 공식 문서 기준으로 Pipeline 적용 시 처리량이 <strong>최대 10배</strong>까지 향상될 수 있습니다.</p>
<p>단, 한 번에 너무 많은 커맨드를 묶으면 서버가 응답을 메모리에 큐잉해야 하므로 메모리 부하가 증가합니다. <strong>1,000 ~ 10,000개 단위</strong>로 나눠서 전송하는 것이 권장됩니다.</p>
<h3 id="왜-커넥션을-독점해야-하는가">왜 커넥션을 독점해야 하는가?</h3>
<p>Pipeline은 내부적으로 <code>openPipeline()</code>을 호출하여 커넥션을 <strong>파이프라인 모드</strong>로 전환합니다. 이 상태에서는 커맨드가 즉시 전송되지 않고 버퍼에 누적되다가 <code>flushCommands()</code> 시점에 한꺼번에 전송됩니다.</p>
<p>만약 이 커넥션을 다른 스레드와 공유한다면, <strong>다른 스레드의 커맨드가 파이프라인 버퍼에 섞여</strong> 의도치 않은 커맨드가 함께 전송되거나, 응답 순서가 뒤섞이는 문제가 발생합니다. 따라서 Pipeline은 반드시 <strong>커넥션을 독점</strong>해야 합니다.</p>
<hr>
<h2 id="실제-트러블슈팅-커넥션-폭증-문제">실제 트러블슈팅: 커넥션 폭증 문제</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>아래와 같이 여러 키에 대해 <code>SMEMBERS</code> 커맨드를 Pipeline으로 묶어 처리하는 코드가 있었습니다. 이 코드는 <strong>가장 많이 호출되는 API의 핵심 로직</strong>이었습니다.</p>
<pre><code class="language-java">public Map&lt;Long, Set&lt;Object&gt;&gt; getGroupIdAndItemIdSetMap(List&lt;Long&gt; groupIdList, String cacheKey) {
    Map&lt;Long, Set&lt;Object&gt;&gt; resultMap = new HashMap&lt;&gt;();

    List&lt;Object&gt; results = redisTemplate.executePipelined((RedisCallback&lt;Object&gt;) connection -&gt; {
        for (Long groupId : groupIdList) {
            String key = cacheKey + &quot;:&quot; + groupId;
            connection.setCommands().sMembers(key.getBytes(StandardCharsets.UTF_8));
        }
        return null;
    });

    for (int i = 0; i &lt; groupIdList.size(); i++) {
        Set&lt;Object&gt; items = (Set&lt;Object&gt;) results.get(i);
        if (items.contains(&quot;PLACEHOLDER&quot;)) {
            resultMap.put(groupIdList.get(i), Collections.emptySet());
        } else {
            resultMap.put(groupIdList.get(i), items);
        }
    }

    return resultMap;
}</code></pre>
<h3 id="왜-커넥션이-폭증했는가">왜 커넥션이 폭증했는가?</h3>
<p><code>executePipelined()</code>는 내부적으로 커넥션을 <strong>독점적으로 점유</strong>해야 합니다. Connection Pool이 설정되지 않은 상태에서는 Shared Connection을 파이프라인 전용으로 사용할 수 없기 때문에, <strong>매 <code>executePipelined()</code> 호출마다 새로운 커넥션을 생성</strong>하게 됩니다.</p>
<p>트래픽이 몰리는 피크타임에 이 API가 대량으로 호출되자, <strong>Redis 신규 커넥션 수립 지표가 1,000개 이상으로 폭증</strong>했습니다. 이는 Redis 서버의 커넥션 부하를 높이고 자원 고갈 위험을 초래하는 상황이었습니다.</p>
<pre><code>[Connection Pool 없는 경우]

스레드 A → executePipelined() → 새 커넥션 생성 → 사용 후 종료
스레드 B → executePipelined() → 새 커넥션 생성 → 사용 후 종료
스레드 C → executePipelined() → 새 커넥션 생성 → 사용 후 종료
...
(동시 요청 수만큼 커넥션 생성)</code></pre><h3 id="해결-connection-pool-설정">해결: Connection Pool 설정</h3>
<pre><code>[Connection Pool 있는 경우]

스레드 A → executePipelined() → Pool에서 커넥션 대여 → 반납
스레드 B → executePipelined() → Pool에서 커넥션 대여 → 반납
스레드 C → executePipelined() → Pool이 고갈되면 maxWait 동안 대기 → 대여
...
(최대 maxTotal 수의 커넥션만 유지)</code></pre><hr>
<h2 id="connection-pool-설정-코드">Connection Pool 설정 코드</h2>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration serverConfig =
            new RedisStandaloneConfiguration(&quot;localhost&quot;, 6379);

        LettucePoolingClientConfiguration poolingConfig = LettucePoolingClientConfiguration.builder()
            .poolConfig(connectionPoolConfig())
            .build();

        return new LettuceConnectionFactory(serverConfig, poolingConfig);
    }

    private GenericObjectPoolConfig&lt;StatefulConnection&lt;?, ?&gt;&gt; connectionPoolConfig() {
        GenericObjectPoolConfig&lt;StatefulConnection&lt;?, ?&gt;&gt; poolConfig = new GenericObjectPoolConfig&lt;&gt;();

        // 피크 기준 전체 엔드포인트 동시 Redis 작업 수의 2~3배 여유를 확보한 값
        poolConfig.setMaxTotal(16);
        // maxTotal의 절반: 트래픽 스파이크 시 신규 커넥션 생성 없이 즉시 사용 가능한 커넥션 예비
        poolConfig.setMaxIdle(8);
        // 저트래픽 시간대에도 최소 2개 상시 유지하여 첫 요청 시 cold start 지연 방지
        poolConfig.setMinIdle(2);
        // 풀 고갈 시 최대 대기 시간: commandTimeout보다 짧게 설정하여 풀 대기 중 타임아웃 선행 방지
        poolConfig.setMaxWait(Duration.ofSeconds(2));
        // eviction 실행 시 유휴 커넥션 유효성 검증: 커넥션 끊김 문제를 선제적으로 감지
        poolConfig.setTestWhileIdle(true);
        // 30초 주기로 eviction 스레드를 실행하여 유휴 커넥션 정리 및 유효성 검증
        poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));
        // 60초 초과 유휴 커넥션 회수: minIdle 아래로 풀 크기를 줄여 Redis 서버 측 커넥션 부하 최소화
        poolConfig.setMinEvictableIdleDuration(Duration.ofSeconds(60));

        return poolConfig;
    }
}</code></pre>
<h3 id="각-설정값의-의미">각 설정값의 의미</h3>
<ul>
<li><strong>maxTotal(16)</strong>: Pool이 유지할 수 있는 최대 커넥션 수. 피크 트래픽 기준 동시 Redis 작업 수의 2~3배로 설정하여 여유를 확보합니다.</li>
<li><strong>maxIdle(8)</strong>: 유휴 상태로 유지할 최대 커넥션 수. 트래픽 스파이크 시 즉시 사용 가능한 커넥션을 예비해 둡니다.</li>
<li><strong>minIdle(2)</strong>: 항상 유지할 최소 커넥션 수. 저트래픽 시간대에도 최소한의 커넥션을 상시 유지하여 첫 요청의 cold start 지연을 방지합니다.</li>
<li><strong>maxWait(2s)</strong>: Pool이 고갈되었을 때 커넥션을 기다리는 최대 시간. commandTimeout보다 짧게 설정하여 풀 대기 중 타임아웃이 먼저 발생하는 상황을 방지합니다.</li>
<li><strong>testWhileIdle(true)</strong>: eviction 실행 시 유휴 커넥션의 유효성을 검증합니다. 네트워크 장애 등으로 끊어진 커넥션을 선제적으로 감지합니다.</li>
<li><strong>timeBetweenEvictionRuns(30s)</strong>: eviction 스레드 실행 주기. 30초마다 유휴 커넥션을 정리하고 유효성을 검증합니다.</li>
<li><strong>minEvictableIdleDuration(60s)</strong>: 60초 이상 유휴 상태인 커넥션을 회수합니다. minIdle 이하로는 줄이지 않으며, Redis 서버 측 커넥션 부하를 최소화합니다.</li>
</ul>
<h3 id="이렇게-설정하면-실제로-어떻게-달라지는가">이렇게 설정하면 실제로 어떻게 달라지는가?</h3>
<p><strong>설정 전 (기본값 사용 시)</strong></p>
<p><code>GenericObjectPoolConfig</code>를 별도로 지정하지 않으면 Commons Pool2의 기본값이 적용됩니다. 기본값은 <code>maxTotal=8</code>, <code>maxIdle=8</code>, <code>minIdle=0</code>, <code>maxWait=-1(무제한)</code> 입니다.</p>
<p>이 상태에서 <code>executePipelined()</code>를 사용하면:</p>
<ul>
<li><code>minIdle=0</code>이므로 저트래픽 구간에 유휴 커넥션이 모두 반납되고, 다음 요청 시 매번 새 커넥션을 생성하는 <strong>cold start 지연</strong>이 발생합니다.</li>
<li><code>maxWait=-1(무제한)</code>이므로 Pool이 고갈되면 스레드가 무한정 대기하여 <strong>요청 처리 지연 및 장애</strong>로 이어질 수 있습니다.</li>
<li>Connection Pool이 없는 경우(기본 <code>LettuceConnectionFactory</code> 사용 시) 피크타임에 <strong>동시 요청 수만큼 신규 커넥션이 생성</strong>되어 Redis 서버 측 커넥션 지표가 폭증합니다.</li>
</ul>
<p><strong>설정 후 (위 Pool Config 적용 시)</strong></p>
<ul>
<li><strong>maxTotal(16)</strong> 덕분에 최대 16개의 커넥션만 생성되어, 피크타임에도 <strong>Redis 서버의 신규 커넥션 수립 지표가 안정적으로 유지</strong>됩니다. 실제로 피크타임 1,000개 이상이던 신규 커넥션 수립 지표가 Pool 적용 후 <strong>maxTotal 범위 내로 수렴</strong>하여 안정화되었습니다.</li>
<li><strong>minIdle(2)</strong> 덕분에 저트래픽 시간대에도 최소 2개의 커넥션이 상시 유지되어 <strong>첫 요청의 커넥션 생성 지연(cold start)이 제거</strong>됩니다.</li>
<li><strong>maxWait(2s)</strong> 덕분에 Pool이 고갈되더라도 최대 2초 내로 대기가 끊기므로 <strong>무한 대기에 의한 스레드 블로킹 장애를 방지</strong>합니다. commandTimeout보다 짧게 설정하여 Pool 대기 중 타임아웃 예외가 먼저 발생하는 상황도 방지됩니다.</li>
<li><strong>testWhileIdle + timeBetweenEvictionRuns(30s)</strong> 조합으로 30초마다 유휴 커넥션의 유효성을 검증하여, 네트워크 장애나 Redis 서버 재시작으로 인해 <strong>끊어진 커넥션이 Pool에 남아 있다가 사용되는 문제를 선제적으로 방지</strong>합니다.</li>
<li><strong>minEvictableIdleDuration(60s)</strong> 덕분에 트래픽이 줄어드는 야간 시간대에 60초 이상 유휴 상태인 커넥션이 자동 회수되어 <strong>Redis 서버의 불필요한 커넥션 유지 부하를 줄일 수 있습니다</strong>.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>권장 방식</th>
</tr>
</thead>
<tbody><tr>
<td>일반 GET/SET 등 단순 커맨드</td>
<td>Shared Connection (기본값)</td>
</tr>
<tr>
<td>Pipeline, Transaction, Blocking 커맨드</td>
<td>Connection Pool 필수</td>
</tr>
</tbody></table>
<p>Lettuce는 기본적으로 훌륭한 thread-safe 클라이언트이지만, <strong>Pipeline처럼 커넥션을 독점해야 하는 작업</strong>을 Connection Pool 없이 사용하면 운영 환경에서 커넥션 폭증이라는 심각한 문제로 이어질 수 있습니다.</p>
<p><code>executePipelined()</code>를 사용하고 있다면, 반드시 <code>LettucePoolingClientConfiguration</code>을 통해 Connection Pool을 설정하세요.</p>
<hr>
<h2 id="다음-편-예고">다음 편 예고</h2>
<p>다음 편에서는 Lettuce와 자주 비교되는 <strong>Redisson</strong> 클라이언트를 다룹니다. 분산락, 분산 컬렉션 등 고수준 기능이 필요한 경우 Redisson이 어떤 이점을 제공하는지 살펴볼 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring framework 란?]]></title>
            <link>https://velog.io/@jw_kim/Spring-Spring-framework-%EB%9E%80</link>
            <guid>https://velog.io/@jw_kim/Spring-Spring-framework-%EB%9E%80</guid>
            <pubDate>Fri, 16 Jan 2026 05:37:44 GMT</pubDate>
            <description><![CDATA[<p>스프링 프레임워크에 대해 알아보기에 앞서 프레임워크란 무엇인지 알아보겠습니다.</p>
<h3 id="framwork와-spring-framework">Framwork와 Spring Framework</h3>
<p>프레임워크는 프로그래밍 환경에서 발생할 수 있는 다양한 문제를 쉽고 빠르게 해결할 수 있도록 도와주는 미리 정의된 기능의 집합입니다.
스프링 프레임워크란(정확히는 자바 기반 애플리케이션 전반에서) 개발자가 구현해야하는 다양하고 복잡한 문제들을 이미 구현해두었고, 개발자는 이를 활용해서 웹을 포함한 애플리케이션을 쉽고 빠르게 구축할 수 있도록 도와주는 프레임워크 입니다.</p>
<h3 id="spring-frmaework의-특징">Spring Frmaework의 특징</h3>
<p>그렇다면 스프링 프레임워크는 어떻게 애플리케이션을 쉽고 빠르게 구축할 수 있도록 도와줄까요? 이는 스프링 프레임워크의 특징을 살펴보면 이해할 수 있습니다.
일반적으로 스프링 프레임워크의 특징을 이야기 할 때, IoC container, DI, AOP 등을 이야기합니다.</p>
<h3 id="ioc-container와-di">IoC Container와 DI</h3>
<p>IoC는 Inversion Of Control의 축약어로 ‘제어의 역전’을 의미합니다. 일반적인 프로그래밍 환경에서 개발자는 직접 객체를 생성하고 객체간의 의존 관계를 정의하며 기능을 구현해냅니다. 하지만 스프링을 사용하면 객체 생성과 의존 관계 연결 같은 공통 작업을 프레임워크가 대신 관리해주기 때문에, 개발자 입장에서는 대부분의 경우 객체의 생명주기 자체를 일일이 관리하기보다 비지니스 로직 작성에 더 집중할 수 있게 됩니다.</p>
<p>이런 ‘제어의 역전’을 구현하는 대표적인 방식이 IoC Container와 DI(의존성 주입) 입니다. 개발자가 비지니스 로직 작성에 필요한 컴포넌트들을 등록해두면, Spring은 이를 Bean으로 관리하고 자체 컨테이너 안에 보관합니다. 이 컨테이너를 IoC Container 라고 합니다.
개발자는 컴포넌트 사용을 위해 매번 새로운 객체를 생성할 필요 없이, IoC 컨테이너가 관리하는 Bean을 주입(DI) 받는 형태로 간단하게 가져와서 사용할 수 있게됩니다.</p>
<h3 id="aop">AOP</h3>
<p>다음으로 AOP(관점 지향 프로그래밍)에 대해 알아보겠습니다. 프로그램을 구현하다 보면 핵심 기능에 해당하는 비지니스 로직 뿐만 아니라 사용자 요청과 응답의 기록(Logging), 권한 검증 등 다양한 부가 로직들도 함께 작성이 됩니다.
만약 하나의 메소드에 핵심 비지니스 로직 이외의 부가적인 역할을 하는 부가 로직이 같이 작성된다면, 단일 책임 원칙을 위반할 뿐만 아니라 코드 가독성이 저해되고 유지보수가 힘들어지는 상황을 초래할 수 있습니다.</p>
<p>AOP는 이런 부가 로직을 핵심 로직과 분리해서 적용할 수 있게 도와줍니다. 보통 애노테이션 등을 활용해서 “어디에 적용할지”를 선언할 수 있고, 스프링은 그 지점에 부가 기능이 실행되도록 연결해줍니다.
결국 비지니스 로직은 핵심 책임에 더 집중할 수 있고, 부가 로직은 공통 기능으로 관리할 수 있어서 유지보수가 쉬워지며 기능 확장도 더 수월해집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[주니어 백엔드 실무지식] 5장. 비동기 연동, 언제 어떻게 써야 할까]]></title>
            <link>https://velog.io/@jw_kim/%EC%A3%BC%EB%8B%88%EC%96%B4-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%A7%80%EC%8B%9D-5%EC%9E%A5.-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%97%B0%EB%8F%99-%EC%96%B8%EC%A0%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jw_kim/%EC%A3%BC%EB%8B%88%EC%96%B4-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%A7%80%EC%8B%9D-5%EC%9E%A5.-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%97%B0%EB%8F%99-%EC%96%B8%EC%A0%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 15 Jan 2026 15:44:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>동기(synchronous)방식은 순차적으로 실행된다. 동기 방식은 한 작업이 끝날 때까지 다음 작업이 진행되지 않는다. 동기 방식은 코드 순서가 곧 실행 방식이 된다. 
비동기(asynchronous)방식은 한 작업이 끝날 때까지 기다리지 않고 바로 다음 작업을 처리한다. 비동기 방식을 사용하면 외부 연동이 끝날 때까지 기다리지 않고 바로 다음 작업을 수행한다.</p>
</blockquote>
<h2 id="동기-연동과-비동기-연동">동기 연동과 비동기 연동</h2>
<p>동기 방식은 프로그램의 흐름을 직관적으로 이해할 수 있고, 디버깅이 용이하다. 하지만 동기 방식으로 외부 연동을 처리할 경우 외부 연동 실패가 전체 기능의 실패인지 확인이 필요하다.</p>
<p>비동기 방식은 한 작업이 끝날 때까지 기다리지 않고 다음 작업을 실행한다. 비동기 연동은 다양한 방식으로 구현할 수 있다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>동기/비동기의 차이를 “실행 흐름” 말고 “결과 전달(성공/실패), 자원 점유, 장애 전파” 관점에서 설명해보세요.</strong></li>
<li><strong>외부 연동을 동기로 둘지 비동기로 둘지 결정 기준 3가지는?</strong> (예: 사용자 응답 요구, 정합성 요구, 장애/지연 전파 허용 여부)</li>
<li><strong>동기 외부 호출이 늘어질 때(지연/타임아웃) 서버에서는 어떤 병목이 연쇄적으로 생기나요?</strong> (스레드 점유 → 커넥션풀/큐 대기 → 타임아웃 확산 등)</li>
<li><strong>동기 연동 실패 시 재시도는 어디에서 어떻게 해야 하나요?</strong> (클라이언트/서버/메시지 레벨, 백오프/서킷브레이커)</li>
<li><strong>비동기로 바꾸면 ‘사용자에게 성공을 언제 알려야’ 하나요?</strong> (즉시 성공 vs 접수 성공, 상태 조회/콜백/웹훅)</li>
</ul>
<hr>
<h2 id="별도-스레드로-실행하기">별도 스레드로 실행하기</h2>
<p>비동기 연동을 실행하는 가장 쉬운 방법은 별도의 스레드로 실행하는 것이다.</p>
<pre><code class="language-kotlin">fun placeOrder(request: OrderRequest): OrderResult {
    ... // 주문 생성 처리

    // 별도 스레드를 이용해 푸시 발송
    Thread { pushClient.sendPush(pushData) }.start()

    // 스레드 풀을 이용
    // executor.submit(() -&gt; pushClient.sendPush(pushData)).start();

    return successResult(....); // 푸시 발송 요청 응답을 기다리지 않고 리턴

}</code></pre>
<p>별도 스레드로 실행하면 연동 과정에서 발생한 오류 처리에 더 신경 써야 한다. 별도 스레드로 실행되는 코드는 익셉션이 전파되지 않기 때문에 실행되는 코드 내부에서 연동 과정에서 발생한 오류를 직접 처리해야한다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>새 스레드로 던져버리는 방식의 운영 리스크는 뭐고, 어떻게 완화하나요?</strong> (스레드 폭증, 컨텍스트 전파, 종료/유실, 관측성)</li>
<li><strong>별도 스레드에서는 예외가 전파되지 않는데, 실패 처리는 어떻게 설계하나요?</strong> (재시도, DLQ 성격의 저장, 알림/모니터링)</li>
</ul>
<hr>
<h2 id="메세징">메세징</h2>
<p>서로 다른 시스템 간에 비동기로 연동할 때 주로 사용하는 방식은 메세징 시스템을 사용하는 방식이다.</p>
<p>메세징 시스템을 사용하면 전체적인 구조가 복잡해지는 대신 다른 이점을 얻을 수 있다.</p>
<p><strong>두 시스템이 서로 영향을 주지 않는다.</strong></p>
<p>시스템 A와 연동되어있는 시스템 B로 처리량 이상의 트래픽이 발생하는 상황을 가정하면, 직접 연동이 되어있을 경우 시스템 B에 성능 저하가 발생하고 이는 전체적인 시스템 성능 저하로 이어질 수 있다. 메세징 시스템은 시스템 A가 보낸 메세지를 일단 저장하고, 시스템 B가 처리 가능한 만큼만 처리할 수 있다.</p>
<p><strong>확장이 용이하다.</strong></p>
<p>시스템 A와 새로운 시스템 C의 연동이 필요해질 때, 직접 연동의 경우 시스템 A와 시스템 C 모두 연동 로직을 추가해야하지만, 메시지 시스템을 도입 할 경우 시스템 A 수정 없이 시스템 C에서 메세지를 수신하여 처리하는 로직만 작성하면 된다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>메시징을 도입하면 얻는 이점(디커플링)과 동시에 생기는 비용(복잡도)은 무엇이고, 언제 도입이 정당화되나요?</strong></li>
</ul>
<h3 id="메세지-생성-측-고려-사항">메세지 생성 측 고려 사항</h3>
<p>메세지를 생성할 때 고려할 점은 메세지 유실에 대한 것이다. 메세지가 유실되었을 경우</p>
<ul>
<li>무시하거나</li>
<li>재시도하거나</li>
<li>실패 기록을 남기거나</li>
</ul>
<p>오류 처리를 위해 위 세가지 방식 중 하나를 선택하여 처리해야한다. 이는 메세지의 특징을 파악하여 적절한 처리 방식을 선택할 필요가 있다.</p>
<p>메세지 생성자는 DB 트랜잭션과의 연동도 고려해야한다. DB 트랜잭션의 커밋과 롤백 시점과 메세지 발생 시점을 고려해야한다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>메시지 유실을 허용/재시도/실패 기록 중 무엇을 고르는 기준은?</strong> (업무 중요도, 부작용 크기, 사용자 영향)</li>
<li><strong>DB 트랜잭션 커밋과 메시지 발행의 원자성을 어떻게 맞추나요?</strong> (커밋 전에 발행/커밋 후 발행 각각의 문제점)</li>
<li><strong>‘정확히 한 번 발행(exactly-once)’이 어렵다면 현실적으로 어떤 보장을 목표로 하나요?</strong> (at-least-once + 멱등 처리)</li>
</ul>
<h3 id="메세지-소비-측-고려-사항">메세지 소비 측 고려 사항</h3>
<p>메세지 소비 측은 </p>
<ul>
<li>메세지 생산자가 동일한 내용의 메세지를 메세징 시스템에 중복 발행</li>
<li>소비자가 메세지 처리 과정에서 발생한 오류로 인해 메세지 재수신</li>
</ul>
<p>이러한 이유로 동일한 메세지를 중복해서 처리할 수 있다. 중복 처리가 문제가 되지 않는 경우도 있지만, 데이터 정합성이 틀어지는 상황이 대부분이다.</p>
<p>중복 소비를 방지하기 위해  멱등성을 보장하는 API를 구현하게 되면 동일 요청을 여러번 하더라도 결과가 바뀌지 않는다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>왜 중복 소비가 발생하고, 중복을 “없애는 것”과 “허용하고 멱등으로 흡수” 중 무엇을 택하나요?</strong></li>
<li><strong>멱등성을 구현하는 대표 패턴을 설명해보세요.</strong> (idempotency key 저장, unique constraint, 처리 로그, 상태 전이)</li>
<li><strong>소비 실패 시 재처리 설계는 어떻게 하나요?</strong> (재시도 횟수/백오프, DLQ, poison message, 수동 복구)</li>
</ul>
<h3 id="메세지-종류-이벤트와-커멘드">메세지 종류: 이벤트와 커멘드</h3>
<p><strong>이벤트</strong></p>
<p>어떤 일이 발생했음을 알려주는 메세지 (예: 주문함, 로그인에 실패함, 상품 정보를 조회함)</p>
<p>정해진 수신자가 없다. 발생한 사건에 관심이 있는 소비자가 메세지를 수신한다.</p>
<p><strong>커멘드</strong></p>
<p>무언가를 요청하는 메세지 (예: 포인트 지급하기, 배송 완료 문자 발송하기)</p>
<p>메세지를 수신할 측의 기능 실행에 초점이 맞춰져있다. 수신자가 정해져있다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>이벤트와 커맨드를 어떻게 구분하고, 잘못 설계하면 어떤 문제가 생기나요?</strong> (결합도 상승, 책임 혼선, 확장성 저하)</li>
<li><strong>이벤트에 “정해진 수신자 없음”을 유지하려면 무엇을 조심해야 하나요?</strong> (특정 소비자 요구사항을 이벤트 스키마에 박아넣는 문제)</li>
<li><strong>같은 주제(예: 포인트 지급)를 이벤트로 할지 커맨드로 할지 결정 기준은?</strong> (요청/책임/보상 필요 여부)</li>
</ul>
<hr>
<h2 id="트랜잭션-아웃박스-패턴">트랜잭션 아웃박스 패턴</h2>
<p>메세지 생성자는 잘못된 메세지의 발생을 막기 위해 DB 트랜잭션이 완료된 이후 메세지를 전송하는 방식을 고려할 수 있다. 메세지에 해당하는 데이터를 DB에 저장하고 저장된 메세지를 읽어 메세지 시스템에 전송하는 방식을 <strong>트랜잭션 아웃박스 패턴</strong>이라고한다.</p>
<ul>
<li>실제 업무 로직에 필요한 DB 변경 작업을 수행한다.</li>
<li>메세지 데이터를 아웃박스 테이블에 추가한다.</li>
</ul>
<p>위 두 가지 로직이 하나의 트랜잭션으로 묶인다면 원자성을 보장하며 데이터 변경과 메세지 발행 요청을 처리할 수 있다. DB에 저장된 메세지 데이터는 별도의메세지 중계 프로세스가 주기적으로 읽어 메세징 시스템에 전송한다.</p>
<h3 id="아웃박스-테이블-구조">아웃박스 테이블 구조</h3>
<p>아웃박스 테이블은 각자 상황에 맞게 구성된다. 아웃박스 테이블은 메세지 종류와 페이로드, 처리 상태등을 저장하고있어야한다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>아웃박스 패턴이 해결하는 문제와, 정확히 어떤 실패 시나리오를 막는지 설명해보세요.</strong> (DB 커밋은 됐는데 메시지 발행 실패 등)</li>
<li><strong>아웃박스 릴레이(중계)가 중복 발행/재시작/지연될 때 정합성은 어떻게 보장하나요?</strong> (상태 컬럼, 락/폴링, 멱등 소비)</li>
<li><strong>아웃박스 테이블을 운영하면 생기는 추가 이슈는?</strong> (테이블 증가/정리, 인덱스, 폴링 부하, 지연 SLA, 모니터링)</li>
</ul>
<hr>
<h2 id="배치-전송">배치 전송</h2>
<p>데이터를 연동하는 가장 전통적인 방법. 일정 간격으로 데이터를 전송하는 방식</p>
<ol>
<li>DB에서 전송할 데이터를 조회</li>
<li>조회한 결과를 파일로 기록</li>
<li>파일을 연동 시스템에 전달</li>
</ol>
<p><strong>질문</strong></p>
<ul>
<li><strong>배치 방식이 여전히 유효한 상황은 언제고, 실시간 메시징 대비 트레이드오프는?</strong> (지연 허용, 단순성, 비용)</li>
<li><strong>배치에서 ‘부분 실패’가 나면 재처리를 어떻게 설계하나요?</strong> (체크포인트, 재실행 범위, 중복 처리/멱등)</li>
<li><strong>대용량 배치에서 성능 병목은 보통 어디서 생기고 어떻게 튜닝하나요?</strong> (조회/정렬/인덱스, 파일 I/O, 네트워크, 청크 처리)</li>
</ul>
<h3 id="재처리-기능-만들기">재처리 기능 만들기</h3>
<hr>
<h2 id="cdc">CDC</h2>
<p>변경된 데이터를 추적하고 판별해서 변경된 데이터로 작업을 수행할 수 있도록 하는 소프트웨어 설계 패턴</p>
<p>다양한 DBMS 시스템이 데이터가 변경되면 그 내용을 통지하는 기능을 제공한다.</p>
<p>DB는 <strong>커밋된 데이터만 변경된 순서에</strong> 맞게 CDC 처리기로 전달한다. 롤백된 데이터가 전달되거나 잘못된 순서로 데이터가 전달되지 않는다.</p>
<h3 id="cdc와-데이터-위치">CDC와 데이터 위치</h3>
<p>CDC 처리기는 변경된 데이터를 어디까지 처리했는지 기록해야한다. 이를 기록해야 CDC 처리기를 재시작 할 때 마지막으로 조회한 로그부터 읽어올 수 있다.</p>
<h3 id="cdc가-유용할-때">CDC가 유용할 때</h3>
<p>신규 서비스의 도입으로 레거시 시스템에 연동 코드를 추가해야할 때, 이를 위한 작업 공수가 필요 이상으로 발생하거나 불가능한 상황이 있다. 이런 경우 시스템 코드를 수정하지 않고 CDC를 활용하여 데이터를 전파할 수 있다.</p>
<p><strong>질문</strong></p>
<ul>
<li><strong>CDC가 유리한 케이스와 불리한 케이스를 비교해보세요.</strong> (레거시 수정 불가 vs 운영 복잡도/스키마 변화 대응)</li>
<li><strong>CDC는 “커밋된 변경”을 순서대로 준다고 했는데, 그럼에도 고려해야 할 정합성 이슈는 뭐가 있나요?</strong> (스냅샷/초기 적재, 스키마 진화, 지연/재처리)</li>
<li><strong>CDC 처리기의 오프셋/체크포인트 관리는 어떻게 설계하고, 장애 복구 시 어떤 전략을 쓰나요?</strong> (at-least-once, 재처리 범위, idempotency)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[주니어 백엔드 실무 지식]  4. 부 연동이 문제일 때 살펴봐야 할 것들]]></title>
            <link>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%98%EB%B0%B1%EC%8B%A4-4.-%EB%B6%80-%EC%97%B0%EB%8F%99%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%9D%BC-%EB%95%8C-%EC%82%B4%ED%8E%B4%EB%B4%90%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%98%EB%B0%B1%EC%8B%A4-4.-%EB%B6%80-%EC%97%B0%EB%8F%99%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%9D%BC-%EB%95%8C-%EC%82%B4%ED%8E%B4%EB%B4%90%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Tue, 06 Jan 2026 07:40:32 GMT</pubDate>
            <description><![CDATA[<h1 id="4장-외부-연동이-문제일-때-살펴봐야-할-것들">4장. 외부 연동이 문제일 때 살펴봐야 할 것들</h1>
<blockquote>
<p>💡 외부 연동은 서버 개발에서 없어서는 안될 요소가 되었다. 마이크로서비스를 도입하는 기업이 늘어나면서 내부 서비스 간 연동도 복잡해지고있다. 연동하는 서비스에 장애가 발생하면 우리 서비스도 영향을 받는다. 서비스 간 연동이 많아질수록 연동 시스템의 품질도 함께 신경써야한다.
연동 서비스의 문제를 완전히 차단하기는 어렵다. 하지만 그 영향도를 줄일 수 있다.</p>
</blockquote>
<h2 id="타임아웃">타임아웃</h2>
<blockquote>
<p>외부 연동에서 가장 중요한 설정 중 하나가 타임아웃이다. 연동 서비스를 호출할 때 타임아웃을 적절하게 설정하지 않으면, 연동 서비스에 장애가 발생했을 때 전체 서비스의 품질이 급격하게 나빠질 수 있다.</p>
</blockquote>
<h3 id="연결-타임아웃과-읽기-타임아웃">연결 타임아웃과 읽기 타임아웃</h3>
<p>API 연동 과정을 단순화하면 연결 → 요청 → 응답 → 종료 4단계로 나눌 수 있다.</p>
<p><strong>연결</strong></p>
<p>네트워크을 통한 연결에는 시간이 소요된다. 네트워크 상황이나 연결할 서버의 상태에 따라 연결에 오랜 시간이 소요될 수도 있다. 연결에 시간이 오래 걸리면 사용자의 대기 시간도 함께 증가하게된다. 대기 시간이 무한정 길어지면 성능에 문제가 발생하므로, <strong>연결 타임아웃</strong>을 설정해 연결 대기 시간을 제한해야한다.</p>
<p>일단 연결이 되면 요청을 하고 응답을 기다린다. 이때 응답을 받기까지 시간이 오래 걸리면 대기 시간 문제가 다시 발생한다. 따라서 <strong>읽기 타임아웃</strong>을 설정하여 응답 대기 시간을 제한해야한다.</p>
<p>❓연결 타임아웃(connect timeout)과 읽기 타임아웃(read timeout)의 차이를 “연동 단계(연결/요청/응답)” 기준으로 설명해보세요.</p>
<p>❓읽기 타임아웃이 발생했을 때 “서버가 실제로 처리를 완료했을 가능성”은 왜 존재하나요?</p>
<p>❓서버 응답 지연이 늘었을 때 “타임아웃으로 실패를 빠르게 만들면” 시스템 안정성은 왜 좋아질 수 있나요?</p>
<hr>
<h2 id="재시도">재시도</h2>
<blockquote>
<p>외부 연동에 실패했을 때 처리 방법 중 하나는 재시도를 하는 것</p>
</blockquote>
<h3 id="재시도-가능-조건">재시도 가능 조건</h3>
<p>재시도를 통해 연동 실패를 줄일 수 있지만, 항상 재시도를 할 수 있는 것은 아니다. 연동 API가 다시 호출해도 되는 조건인지 확인해야 한다. 포인트 차감 기능과 같은 외부 API에 재시도를 적절하지 않게 설정하면 중복 차감 등 심각한 문제가 발생할 수 있다.</p>
<p><strong>재시도 가능 조건</strong></p>
<ul>
<li>단순 조회 기능: 단순 조회 기능은 재시도하여도 원본 데이터를 변경하지 않는다.</li>
<li>연결 타임아웃: 연결 타임아웃이 발생할 경우 외부 API 연결에 실패하였으므로 재시도로 인해 발생할 수 있는 중복 결제 등의 문제가 없다.</li>
<li><strong>멱등성</strong>을 가진 변경 기능: 멱등성을 보장하는 API의 경우 동일 요청에 항상 동일 응답을 반환하기 때문에 재시도로 인해 중복 처리 문제가 발생하지 않는다.</li>
</ul>
<p>❓어떤 조건에서 재시도가 안전한가요? (조회/연결 실패/멱등 보장)</p>
<p>❓“멱등성”이 있다고 판단하려면 어떤 전제가 필요하나요? (요청 키, 서버 구현, 중복 처리 방지)</p>
<p>❓결제/포인트 차감 같은 “변경 API”는 왜 재시도가 위험한가요?</p>
<p>❓읽기 타임아웃은 왜 재시도 위험도가 큰 편인가요? 그럼에도 재시도가 필요하면 뭘 추가해야 하나요?</p>
<h3 id="재시도-횟수와-간격">재시도 횟수와 간격</h3>
<p>재시도 전략을 수립할 때 <strong>횟수와 간격 2가지</strong>를 고려해야한다.</p>
<p><strong>재시도 횟수</strong></p>
<p>재시도를 무한정 할 수는 없다. 재시도 횟수 만큼 응답 시간도 함께 증가하며, 외부 서버에 가해지는 부하도 가중되기 때문에, 적당한 (보통 1-2회) 횟수로 재시도를 제한해야한다. </p>
<p><strong>재시도 간격</strong></p>
<p>네트워크 연결 상태가 6초간 좋지 않은 상황에서, API 연결 타임 아웃을 3초로 설정할 경우 최초 호출이 실패하게된다. 이 경우 즉시 재시도 요청을 할 경우 두 번째 요청도 실패하게된다. 때문에 일시적 네트워크 문제등이 해소되면 연결에 성공할 수 있도록 재시도 간격을 적절하게 설정하는것이 중요하다.</p>
<p>❓재시도 횟수를 1~2회로 제한하는 이유를 <strong>사용자 응답 시간</strong>과 <strong>상대 시스템 부하</strong> 관점에서 설명해보세요.</p>
<p>❓즉시 재시도 vs 지연 재시도는 어떤 상황에서 유리한가요?</p>
<p>❓재시도 정책을 “전 구간 동일”하게 두면 어떤 문제가 생기나요? (엔드포인트별/오류별 분리 필요성)</p>
<h3 id="재시도-폭풍retry-storm-안티-패턴">재시도 폭풍(retry storm) 안티 패턴</h3>
<p>재시도를 통해 성공 가능성을 높일 수 있지만, 반대로 연동 서비스에 더 큰 부하를 줄 수 있다. 따라서 재시도를 검토할 때는 연동 시브스의 성능 상황도 함께 고려해야한다.</p>
<p>❓retry storm이 발생하는 전형적인 시나리오를 설명해보세요.</p>
<p>❓retry storm을 막기 위해 어떤 기법을 조합하나요? (지수 백오프, 지터, 서킷브레이커, 동시요청 제한)</p>
<p>❓재시도는 “클라이언트”와 “서버/게이트웨이” 중 어디에서 해야 하나요? 각각의 장단점은?</p>
<hr>
<h2 id="동시-요청-제한">동시 요청 제한</h2>
<blockquote>
<p>연동 서비스의 동시 요청 수가 100건일 때, 연동 서비스로 동시 요청이 300개 들어오면, 연동 서비스 최대 처리량을 초과하여 응답 시간 지연이 발생한다.</p>
</blockquote>
<p>연동 서비스에 임계치 이상의 요청을 보내면 발생하는 성능 저하 문제를 완화하는 방법은, 연동 서비스에 요청을 일정 수준 이상으로 보내지 않는 것</p>
<p>❓동시 요청 제한이 필요한 이유를 “상대 서비스 처리량 한계”와 “큐잉 지연”으로 설명해보세요.</p>
<p>❓제한 초과 시 동작은 어떻게 해야 하나요? (즉시 실패/대기/큐잉) 사용자 경험 관점에서 답해보세요.</p>
<p>❓동시성 제한과 rate limit(QPS 제한)은 무엇이 다르고, 각각 어떤 장애를 막나요?</p>
<hr>
<h2 id="서킷-브레이커">서킷 브레이커</h2>
<blockquote>
<p>연동 서비스에 과부하가 발생해 응답을 제대로 주지 못하고있는 상황이라면, 연동 서비스가 정상화되기 까지 요청을 보내도 계속해서 에러 응답만 발생한다.
외부 서비스에 장애가 발생하였을 때, 내부 서비스에서 장애 상황을 인지하고 사용자에게 빠른 응답을 주는 것이 필요</p>
</blockquote>
<p>서킷브레이커는 누전차단기와 같이 동작한다. 외부 서비스 연동 과정에 과도한 오류가 발생하면 연동을 중지시키서고 즉시 에러를 응답한다.</p>
<p>서킷브레이커가 열린 상태에서는 외부 연동 요청을 수행하지 않고 빠르게 에러 응답을 사용자에게 전달한다. 서킷브레이커는 닫힘-반열림-열림 상태를 변경하며 외부 서비스 장애에 대응한다.</p>
<p>❓서킷 브레이커의 목적을 “장애 전파 차단” 관점에서 설명해보세요.</p>
<p>❓어떤 지표로 open을 판단하나요? (error rate, slow call rate, timeouts)</p>
<hr>
<h2 id="외부-연동과-db-연동">외부 연동과 DB 연동</h2>
<h3 id="외부-연동과-트랜잭션-처리">외부 연동과 트랜잭션 처리</h3>
<p>DB연동과 외부 연동을 하나의 트랜잭션으로 실행할 경우, 오류 발생 시 DB 트랜잭션을 어떻게 처리할지 판단해야한다.</p>
<p><strong>외부 연동이 실패했을 경우 전체 트랜잭션 롤백</strong></p>
<p>트랜잭션 범위 안에서 외부 연동에 실패한경우, 트랜잭션을 롤백할 수 있다. 이 경우 변경한 데이터가 DB에 반영되지 않기 때문에 DB 데이터에 이상이 생기는 것을 방지할 수있다.</p>
<p>하지만 외부 서비스 호출 과정에서 <strong>읽기 타임아웃</strong>이 발생하였을 경우, 실제로 외부 서비스에서 요청을 성공적으로 처리했을 가능성을 염두해두어야한다. 이런 경우 아래와 같은 두 가지 방법을 검토해야한다.</p>
<ul>
<li>일정 주기로 두 시스템간 데이터 일치 여부를 확인하고 보정</li>
<li>성공 확인 API 호출로 성공 여부 확인</li>
</ul>
<p><strong>외부 연동은 성공했지만 DB 트랜잭션이 실패해 트랜잭션을 롤백</strong></p>
<p>외부 연동을 성공했지만 DB 트랜잭션이 실패하는 경우는 취소 API를 추가적으로 호출하여 외부 연동 이전 상태로 데이터를 복원해야한다. 취소 API가 없는 경우 일정 주기로 데이터를 보정하는 프로세스가 추가적으로 필요하게된다.</p>
<p>❓ “DB 트랜잭션 안에서 외부 API 호출”이 위험한 이유를 설명해보세요. (락/커넥션 점유/지연 전파)</p>
<p>❓ 외부 연동 실패 시 롤백 전략을 어떻게 결정하나요? (비즈니스 규칙, 정합성 요구 수준)</p>
<p>❓ 읽기 타임아웃이 떴을 때 “외부는 성공했을 수 있음”을 어떻게 다루나요?</p>
<p>❓외부는 성공했는데 DB가 실패하면 왜 보상 트랜잭션(취소 API)이 필요하나요?</p>
<h3 id="외부-연동이-느려질-때-db-커넥션-풀-문제">외부 연동이 느려질 때 DB 커넥션 풀 문제</h3>
<p>외부 연동이 포함되어있는 트랜잭션에서 외부 연동 시간 증가로 인해 커넥션 풀이 고갈되는 상황이 발생할 수 있다. 이 경우 DB 처리 시간은 동일하지만 외부 연동 문제로 인해 응답 시간이 증가하는 상황이 발생한다.</p>
<p>DB 연동과 무관하게 외부 연동을 실행하는 방안도 고려해볼 필요가 있다.</p>
<p>❓ 외부 연동 지연이 DB 커넥션 풀 고갈로 이어지는 메커니즘을 설명해보세요.</p>
<p>❓ “DB 처리 시간은 동일한데 응답 시간이 늘어나는” 상황에서 어떤 지표로 원인을 특정하나요?</p>
<p>❓ 외부 연동을 트랜잭션 밖으로 빼는 설계는 어떤 경우에 가능한가요? 불가능한 경우는?</p>
<p>❓ 비동기 처리(큐/이벤트)로 전환하면 정합성·사용자 경험은 어떻게 설계해야 하나요</p>
<hr>
<h2 id="연동-서비스-이중화">연동 서비스 이중화</h2>
<p>이커머스 서비스에서 외부 연동을 통해 결제를 처리할 경우, A 결제 서비스의 장애로 인해 결제가 불가능한 상황이 발생할 수 있다. 만약 결제 서비스가 이중화되어있지 않을 경우 A 결제 서비스의 장애가 복구될 때 까지 우리 서비스의 결제가 불가능한 상황이 발생한다.</p>
<p>이러한 상황은 추가적인 결제 서비스를 도입 이중화를 통해 해소할 수 있다. A 결제 서비스의 장애가 발생할 경우 B 결제 서비스를 통해 결제를 진행하는 방식이다.</p>
<p>❓ 결제 서비스 이중화의 필요성을 “단일 장애점(SPOF)” 관점에서 설명해보세요.</p>
<p>❓ A 장애 시 B로 failover 할 때 라우팅은 어디에서 결정하나요? (앱, 게이트웨이, 서비스 디스커버리)</p>
<p>❓ 이중화 시 “정산/취소/중복 결제” 같은 정합성 이슈를 어떻게 방지하나요?</p>
<p>❓ 서로 다른 결제사의 API 스펙 차이를 어떻게 흡수하나요? (어댑터/포트-어댑터, 표준화 모델)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[주니어 백엔드 실무 지식]  3. 성능을 좌우하는 DB 설계와 쿼리]]></title>
            <link>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%98%EB%B0%B1%EC%8B%A4-3.-%EC%84%B1%EB%8A%A5%EC%9D%84-%EC%A2%8C%EC%9A%B0%ED%95%98%EB%8A%94-DB-%EC%84%A4%EA%B3%84%EC%99%80-%EC%BF%BC%EB%A6%AC</link>
            <guid>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%98%EB%B0%B1%EC%8B%A4-3.-%EC%84%B1%EB%8A%A5%EC%9D%84-%EC%A2%8C%EC%9A%B0%ED%95%98%EB%8A%94-DB-%EC%84%A4%EA%B3%84%EC%99%80-%EC%BF%BC%EB%A6%AC</guid>
            <pubDate>Sat, 03 Jan 2026 16:05:05 GMT</pubDate>
            <description><![CDATA[<h1 id="3장-성능을-좌우하는-db-설계와-쿼리">3장. 성능을 좌우하는 DB 설계와 쿼리</h1>
<blockquote>
<p>💡 <strong>DB 성능은 연동하는 모든 서버 성능에 영향</strong>을 준다. DB 성능 문제로 전체 서비스가 먹통이 되는 상황도 빈번하게 발생한다. 쿼리 실행 시간이 길어지면 전체 서비스가 느려지는 성능 문제는 흔히 발생하는 문제이다. 하지만 DB 자체가 문제인 상황은 많지 않다. 오히려 DB를 잘못 사용해서 발생한 문제가 더 많다. DB를 전문가 수준으로 깊이 이해할 수 있다면 이상적이겠지만, <strong>서버 개발자 입장에서 조금만 신경써도 DB 성능 문제를 충분히 줄이거나 없앨 수 있다.</strong></p>
</blockquote>
<h2 id="조회-성능을-고려한-인덱스-설계">조회 성능을 고려한 인덱스 설계</h2>
<h3 id="인덱스-설계">인덱스 설계.</h3>
<p>DB 테이블을 설계할 때는 조회 기능과 트래픽 규모를 고려해야 한다. 전체 게시글이 1000만건 이상 적제된 테이블을 대상으로 사용자가 특정 카테고리의 게시글 조회 요청을 할 경우, (카테고리 컬럼에) 인덱스가 없다면 1000만건의 데이터를 비교해야한다. </p>
<p>많은 사용자가 동시에 게시글 조회를 하게되면 다수의 풀 스캔이 발생하고 DB의 CPU 사용률이 100%에 도달할 경우 DB가 제 기능을 하지 못하게 된다.</p>
<ul>
<li>풀 스캔이 발생하지 않도록 하려면 <strong>조회 패턴을 기준으로 인덱스를 설계</strong>해야 한다.</li>
</ul>
<h3 id="단일-인덱스와-복합-인덱스">단일 인덱스와 복합 인덱스</h3>
<ul>
<li>단일 인덱스: userId와 같이 단일한 컬럼을 인덱스로 사용</li>
<li>복합 인덱스: (userId, activityDate)와 같이 여러 컬럼은 묶어서 인덱스로 사용</li>
</ul>
<p>데이터의 특징을 파악하면 단일 인덱스/복합 인덱스 중 더욱 적절한 형태의 인덱스가 적절한지 판단하는데 도움이 된다.</p>
<h3 id="선택도를-고려한-인덱스-컬럼-선택">선택도를 고려한 인덱스 컬럼 선택</h3>
<p>인덱스를 생성할 때는 <strong>선택도</strong>가 높은 컬럼을 골라야 한다.</p>
<ul>
<li>선택도: 특정 칼럼의 고유한 값 비율 (카디널리티)</li>
</ul>
<p>선택도가 높으면 해당 칼럼에 고유값이 많다는 뜻, 선택도가 높을 수록 인덱스를 이용한 조회 효율이 높아진다.</p>
<blockquote>
<p>작업 상태 컬럼과 같이 특정 배치 작업에서 일정 상태값을 조회하는 경우 선택도가 낮더라도 반복 작업의 효율을 높히기 위해 선택도가 낮은 컬럼을 인덱스로 대상으로 선정하여 효율을 높일 수도 있다.</p>
</blockquote>
<h3 id="커버링-인덱스">커버링 인덱스</h3>
<p>커버링 인덱스는 특정 쿼리를 실행하는 데 필요한 컬럼을 모두 포함하는 인덱스를 말한다.</p>
<pre><code class="language-sql">select activityDate, activityType
from activityLog
where activityDate = &#39;2025-01-01&#39; and activityType = &#39;VISIT&#39;;</code></pre>
<p>위와 같은 쿼리를 실행할 때 (activityDate, activityType)을 사용하는 커버링 인덱스가 생성되어 있다면, 실제 데이터를 읽어오는 과정을 생략하고 인덱스에서 필터링과 조회가 가능하기 때문에 더욱 빠른 응답이 가능하다.</p>
<h3 id="인덱스는-필요한-만큼만-만들기">인덱스는 필요한 만큼만 만들기</h3>
<p>효과가 적은 인덱스를 추가할 경우 오히려 성능에 악영향을 줄 수 있음. 인덱스는 조회 속도를 빠르게 하지만 데이터 추가, 변경, 삭제 시에는 인덱스 관리에 따른 추가 비용이 발생하기 때문. 또한 인덱스 자체도 데이터이기 때문에 인덱스가 많아질수록 메모리와 디스크 사용량도 함께 증가</p>
<hr>
<h2 id="조회-성능-개선-방법">조회 성능 개선 방법</h2>
<p>인덱스를 적용하지 않고 조회 성능을 개선하는 몇가지 방법</p>
<h3 id="미리-집계">미리 집계</h3>
<p>게시글에 대한 댓글, 좋아요 와 같은 정보가 서로 다른 테이블에 저장되어있을 경우 1개의 (집계 쿼리가 포함된)게시물 조회 요청에 대하여 N개의 댓글 + M개의 좋아요로 인해 과도한 쿼리 수행 시간이 소요될 수 있다.</p>
<p>이 경우 반정규화(비정규화)를 통해 게시글 테이블에 좋아요 수를 관리할 컬럼을 추가하는 등의 방식으로 추가적인 연산에 소요되는 시간을 줄일 수 있다.</p>
<h3 id="페이지네이션-정보를-기준으로-목록-조회하는-대신-id고유번호를-기준으로-조회">페이지네이션 정보를 기준으로 목록 조회하는 대신 ID(고유번호)를 기준으로 조회</h3>
<h3 id="조회-범위를-시간-기준으로-제한">조회 범위를 시간 기준으로 제한</h3>
<p>조회 범위를 제한하여 최신 데이터만 조회할 수 있도록 설계할 경우 전체 데이터를 조회하지 않기 때문에 조회 성능을 개선할 수 있음</p>
<h3 id="전체-개수-세지-않기">전체 개수 세지 않기</h3>
<p>대량의 데이터를 보관하고있는 테이블을 대상으로 전체 개수를 세지 않도록 기능 협의 혹은 반정규화 등으로 해결</p>
<h3 id="오래된-데이터-삭제-및-분리-보관">오래된 데이터 삭제 및 분리 보관</h3>
<p>데이터 개수가 늘어날 수록 쿼리 수행 시간은 증가한다. 대부분의 조회는 최신 데이터를 기준으로 발생하고 오래된 데이터를 사용하는 빈도는 줄어들기 때문에 일정 크기의 데이터 사이즈를 유지하며 오래된 데이터는 삭제(혹은 별도 테이블,데이터베이스 이동)하는 방식으로 조회 성능을 개선(일정 수준 유지)할 수 있다.</p>
<h3 id="그-외">그 외</h3>
<p>그 외 조회 성능을 개선하기 위한 방법은 DB 장비 확장(스케일), 별도 캐시 서버 구성 등이 있다.</p>
<hr>
<h2 id="몇-가지-주의-사항">몇 가지 주의 사항</h2>
<h3 id="쿼리-타임-아웃">쿼리 타임 아웃</h3>
<p>응답 시간은 처리량에 큰 영향을 준다. 동시 사용자가 증가할 때 응답 시간이 길어지면 처리량은 감소한다.</p>
<p>앞선 요청이 처리되지 않은 상태에서의 사용자 요청은 서버 부하를 더욱 가중시킨다. 사용자의 재시도가 반복되면 서버 부하는 폭증하게 된다.</p>
<p>이런 상황을 방지하는 방법 중 하나는 쿼리 실행 시간(쿼리 타임아웃)을 설정하는 것. 예를 들어 쿼리 실행 시간을 5초로 제한 할 경우 5초를 초과하는 쿼리에 대하여 타임아웃 에러를 발생시켜 사용자에게 빠르게 오류 응답을 전달할 수 있다.</p>
<p>결제와 같은 데이터 정합성이 중요한 요청의 경우 타임아웃 에러가 발생하면 후속 처리와 데이터 정합성에 대해 고려해야한다.</p>
<ul>
<li>적절한 타임아웃 길이를 결정해야한다.</li>
</ul>
<h3 id="상태-변경-기능은-복제-db에서-조회하지-않기">상태 변경 기능은 복제 DB에서 조회하지 않기</h3>
<p>primary DB - replica DB 구조를 사용하는 경우 데이터 변경은 primary, 데이터 조회는 replica를 사용한다.</p>
<p>하지만 데이터 변경을 위한 select 실행을 replica DB대상으로 실행하였을 경우 문제가 발생할 수 있다.</p>
<ul>
<li>변경 데이터의 전파 과정에서 일시적인 데이터 불일치 발생으로 인한</li>
</ul>
<p>때문에 데이터 변경 시 데이터 변경 대상 데이터(primary)를 조회하여 처리해야한다.</p>
<h3 id="배치-쿼리-실행-시간-증가">배치 쿼리 실행 시간 증가</h3>
<p>배치 프로그램은 데이터를 일괄로 조회, 집계, 생성하는 등의 작업을 수행한다. 데이터가 많아질 수록 일괄 처리 쿼리를 수행하는 시간도 함께 증가한다. 이런 문제를 예방하려면 배치에서 사용하는 쿼리 실행 시간을 지속적으로 추적해야할 필요가 있다. 만약 배치 쿼리에서 문제가 발생한다면 아래와 같은 방식으로 성능 최적화를 진행할 수 있다.</p>
<ul>
<li>커버링 인덱스 활용</li>
<li>데이터를 일정한 크기로 나눠서(chunk) 처리</li>
</ul>
<h3 id="타입이-다른-컬럼의-조인">타입이 다른 컬럼의 조인</h3>
<p>만약 join에 활용되는 컬럼의 타입이 테이블별로 상이하다면 데이터베이스는 두 테이블을 조인하는 과정에서 타입 변환을 추가적으로 수행한다.</p>
<p>만약 서로 다른 타입의 값을 조인에 활용해야한다면 <code>cast()</code> 등을 활용하여 불필요한 타입 변환을 줄일 수 있다.</p>
<h3 id="테이블-변경">테이블 변경</h3>
<p>데이터가 많은 테이블에 새로운 컬럼을 추가하거나, 기존 열거 타입을 변경할 때 데이터베이스는 변경된 새 테이블을 생성하고, 원본 테이블의 데이터를 복사하여 최종 대체하는 방식으로 테이블 변경 작업을 진행한다. 테이블 복사 과정에서 DML의 실행은 허용하지 않기 때문에 복사 시간만큼 서비스가 멈춘다.</p>
<p>만약 복사 시간이 길어진다면, 복사 시간만큼 서비스가 먹통이 되는 위험한 상황이 발생할 수 있어 테이블 변경은 항상 신중하게 진행해야한다.</p>
<h3 id="db-최대-연결-개수">DB 최대 연결 개수</h3>
<hr>
<h2 id="실패와-트랜잭션-고려">실패와 트랜잭션 고려</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[주니어 백엔드 실무 지식] 2장.느려진 서비스, 어디서부터 봐야 할까 ]]></title>
            <link>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%B1%EB%B0%98%EC%8B%A4-2%EC%9E%A5.%EB%8A%90%EB%A0%A4%EC%A7%84-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%96%B4%EB%94%94%EC%84%9C%EB%B6%80%ED%84%B0-%EB%B4%90%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jw_kim/%EC%A3%BC%EB%B0%B1%EB%B0%98%EC%8B%A4-2%EC%9E%A5.%EB%8A%90%EB%A0%A4%EC%A7%84-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%96%B4%EB%94%94%EC%84%9C%EB%B6%80%ED%84%B0-%EB%B4%90%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 01 Jan 2026 12:15:26 GMT</pubDate>
            <description><![CDATA[<h1 id="2장-느려진-서비스-어디서부터-봐야-할까">2장. 느려진 서비스, 어디서부터 봐야 할까</h1>
<blockquote>
<p>💡 사용자는 무언가를 실행했을 때 동작하기까지 걸리는 시간으로 성능을 판단하지만, 실제로는 다양한 지표가 성능과 관련되어있다.
이러한 다양한 지표 중에서 서버 성능과 관련있는 중요한 지표들에 대하여 알아보고 성능 개선을 위한 방안을 확인해볼 수 있어야한다.</p>
</blockquote>
<h3 id="응답-시간">응답 시간</h3>
<p><strong>‘응답 시간’</strong>이란 <strong>사용자의 요청을 처리하는 데 걸리는 시간</strong>을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/39815ec6-6d23-4abe-936a-20e1b8fa6763/image.svg" alt=""></p>
<p><strong>응답 시간</strong>은 크게 <strong>API 요청 전송 시간, 서버의 처리 시간, API 응답 시간</strong>으로 나눌 수 있다. 이중 개발자가 주로 확인하게 되는<strong>서버의 처리 시간</strong>은 아래와 같이 구성되어있다.</p>
<ul>
<li><strong>로직 수행</strong></li>
<li><strong>DB 연동</strong></li>
<li><strong>외부 API 연동</strong></li>
<li><strong>응답 데이터 생성</strong></li>
</ul>
<p>등으로 구성되고 이중 가장 높은 수행 시간 비중을 차지하는 것은 일반적으로
<strong>외부 API 연동 &gt; DB 연동 &gt; 로직 수행 &gt; 응답 데이터 생성</strong> 순이다.</p>
<p>❓<strong>DB 시간과 애플리케이션 로직 시간을 구분해서 측정하려면 어떤 방식(로그/트레이싱/DB 슬로우쿼리)을 쓰나요?</strong></p>
<h3 id="처리량">처리량</h3>
<p>처리량은 단위 시간당 시스템이 처리하는 작업량을 의미, 흔히 TPS, RPS로 처리량을 나타낸다</p>
<ul>
<li><strong>TPS</strong>: 초당 트랜잭션 수</li>
<li><strong>RPS</strong>: 초당 요청 수</li>
</ul>
<p>최대 TPS는 시스템이 처리할 수 있는 최대 요청의 수를 의미한다. 최대 TPS를 초과하는 요청이 들어왔을 때, 서버는 초과한 요청을 나중에 처리한다. 이 과정에서 응답 시간이 증가하게 되고 사용자의 이탈로 이어질 수 있다.</p>
<p>처리량을 늘릴 수 있는 방법</p>
<ul>
<li>서버가 동시에 처리할 수 있는 요청 수를 늘려 대기 시간 줄이기</li>
<li>처리 시간 자체를 줄여 대기 시간 줄이기</li>
</ul>
<p>⭐ 성능 개선을 위해서는 우선적으로 현재 서버의 TPS와 응답 시간을 파악해야한다. 막연히 성능이 좋지 않다, 느리다라는 것 보다 </p>
<ul>
<li>트래픽이 많은 시간대의 TPS와 응답 시간 측정 결과</li>
</ul>
<p>등을 바탕으로 목표 TPS와 응답 시간을 설정하고 효과적인 개선안을 도출해야한다.</p>
<h3 id="병목-지점">병목 지점</h3>
<p>문제 지점을 찾는 간단한 방법은 처리 시간이 오래 걸리는 작업을 식별 하는것</p>
<ul>
<li>모니터링 도구를 이용</li>
<li>경험에 의한 추측</li>
<li>의심되는 코드에 로그를 기록</li>
</ul>
<p><strong>❓ 성능 저하가 발생했을 때 “가장 먼저 확인할 3가지”를 정해 순서대로 설명해보세요.</strong></p>
<p><strong>❓ CPU/메모리/디스크/네트워크/DB 중 어디가 병목인지 어떻게 판별하나요?</strong></p>
<p><strong>❓ APM 없이도 병목을 찾기 위해 어떤 로그(구간 측정, correlation id 등)를 설계하나요?</strong></p>
<p><strong>❓ “경험에 의한 추측”을 줄이기 위해 어떤 관측 가능성(Observability) 구성을 해두면 좋나요?</strong></p>
<h3 id="수직-확장과-수평-확장">수직 확장과 수평 확장</h3>
<p>성능 문제의 원인을 찾았다면 우선적으로 사용자가 서비스를 정상적으로 이용 가능한 상황을 만드는 것이 가장 중요. 이 때 수직 확장을 고려할 수 있음</p>
<p>‘수직 확장’이란 CPU, 메모리, 디스크 등 자원을 증가시키는 방법</p>
<ul>
<li>더 빠른 CPU 혹은 더 많은 코어수의 CPU로 교체</li>
<li>메모리 확장</li>
<li>HDD → SDD 변경</li>
</ul>
<p>이러한 작업만으로도 서버 자체 처리량이 증가하기 때문에 일시적으로 문제를 해결할 수 있음 (클라우드 환경에서는 비교적 빠르게 적용 가능) 
수직 확장은 즉각적인 효과를 얻을 수 있지만, 수직확장을 무한정 반복할 수 없음 (비용과 기술적인 한계)</p>
<p><strong>❓서버를 수평 확장했는데 TPS가 늘지 않는 다면 어떤 이유일지?</strong></p>
<p><strong>❓커넥션 풀을 키우면 무조건 성능이 좋아지나요? 오히려 나빠지는 케이스는?</strong></p>
<p>‘수평 확장’이란 자원을 추가하는 것이 아닌 서버 자체를 추가로 투입하는 방법</p>
<ul>
<li>인스턴스의 물리적인 수를 증가시켜 사용자 트래픽을 분산하여 TPS를 높이는 방식</li>
</ul>
<p>단, 이 경우 병목 지점을 파악하는 것이 중요. DB 수준에서 성능 문제가 발생할 경우 서바 인스턴스 수평 확장은 DB로의 부하를 더욱 가중시켜 TPS가 향상되지 않을 뿐더러 추가적인 성능 저하가 발생할 수 있음.</p>
<p>또한, 물리적 인스턴스의 수를 증가시키기 때문에 트래픽을 적절하게 분산시킬 수 있는 방법(로드밸런스 등)을 추가적으로 고려해야함</p>
<h3 id="db-커넥션-풀">DB 커넥션 풀</h3>
<p>서버는 DB와 통신을 위해 네트워크 연결을 생성하고 반납하는 작업을 수행한다. 이 작업에는 0.5ms 이상 시간이 소요되기 때문에 매 요청마다 연결을 새로 생성하여 사용하면 응답 시간이 길어지고 성능에 영향을 줄 수 있다.</p>
<p>때문에 애플리케이션 수준에서 DB 커넥션을 미리 생성하여 보관하고 요청이 필요할 때 이 커넥션을 사용하여 빠르게 DB 로직을 수행할 수 있다. 이를 <strong>‘커넥션풀’</strong>이라 한다.</p>
<ul>
<li>커넥션 풀의 크기(최소, 최대)</li>
<li>커넥션을 구할 때까지 대기할 시간</li>
<li>커넥션 유지 시간</li>
</ul>
<p>등을 관리하여 DB 질의를 보다 효과적으로 처리할 수 있다.</p>
<p><strong>❓ 커넥션 풀을 왜 쓰는지, “매 요청마다 커넥션 생성”이 왜 비싼지 설명해보세요.</strong></p>
<p><strong>❓ 커넥션 풀이 부족할 때 애플리케이션에서 어떤 증상(스레드 대기, 타임아웃)이 나타나나요?</strong></p>
<p><strong>❓커넥션 풀을 키우면 무조건 성능이 좋아지나요? 오히려 나빠지는 케이스는?</strong></p>
<h3 id="커넥션-풀의-크기">커넥션 풀의 크기</h3>
<p>전체 응답 시간과 TPS를 고려하여 적절한 커넥션 풀의 크기를 설정하는 것이 중요</p>
<p>커넥션풀에 유효 커넥션이 존재하지 않을 경우, 이후 사용자의 요청은 커넥션 풀이 반납될 때까지 대기하게된다.</p>
<h3 id="커넥션-대기-시간">커넥션 대기 시간</h3>
<p>유효한 커넥션이 존재하지 않을 경우, 커넥션 획득을 위한 대기 시간을 설정할 수 있다.(Hikari 기본 대기시간 30초). 하지만 커넥션 획득을 위해 장시간 대기할 경우 사용자 입장에서 응답 없는 상황이라 받아들일 수 있으므로 적절한 대기 시간을 설정하고, 커넥션 획득이 어려울 경우 빠른 오류 응답이 더 적절할 수도있음.</p>
<p><strong>❓성능 저하 API를 개선하기 위해서는 어떤 지표들의 확인이 필요한가</strong></p>
<p><strong>❓평소 잘 동작하던 API 서버에 성능 저하가 발생하였을 경우 어떤 순서로 성능 저하 원인을 파악할것인가</strong></p>
<hr>
<h3 id="서버-캐시">서버 캐시</h3>
<ul>
<li>서버의 확장은 많은 비용이 발생한다.</li>
<li>DB 서버 수평적 확장은 (처리량은 늘어나지만) 실행 시간이 획기적으로 줄어들지 않을 수 있다.</li>
</ul>
<p>만약 비용적인 한계가 있는 상황에서 응답 시간과 처리량을 개선하기 위해서는 <strong>‘캐시(cache)’</strong> 도입을 고려해볼 수있다.</p>
<p>캐시는 (키, 값) 쌍을 저장하는 Map 형태의 데이터 저장소이다. 스토리지에서 데이터를 읽어오는 DB와는 달리 캐시 저장소는 메모리에 적재되어있는 데이터를 key 기반으로 읽어오기 때문에 읽는 속도가 DB에 비해 빠르다.</p>
<p><strong>❓ 비용적인 제한으로 서버 혹은 DB 인스턴스의 스케일 확장이 불가능 할 경우 성능 개선을 위한 방법이 있는지</strong> </p>
<p><strong>❓ 캐시 도입이 “응답 시간”과 “처리량”에 각각 어떤 영향을 주나요?</strong></p>
<p><strong>❓캐시를 도입했는데도 효과가 없는 대표적인 원인(적중률 낮음, 키 계 문제 등)은?</strong></p>
<p><strong>❓캐시가 오히려 장애를 유발하는 케이스(캐시 스탬피드, 핫키)는 무엇이고 어떻게 막나요?</strong></p>
<h3 id="적중률과-삭제-규칙">적중률과 삭제 규칙</h3>
<p>캐시의 효율성을 판단하기 위해 적중률(Hit rate)을 참고할 수 있다.</p>
<ul>
<li>적중률 = 캐시에 존재한 건수 / 캐시에서 조회를 시도한 건수</li>
</ul>
<p>캐시 적중률이 높을 수록 사용자 요청이 DB에서 조회되지 않고 캐시 데이터로 응답을 많이 했다는 의미. 이는 곧 DB 부하 감소</p>
<p>캐시는 메모리 공간을 사용하기 때문에 데이터를 무한정 저장할 수 없다. 때문에 조회 빈도수가 높거나, 빠른 응답 성능이 필요한 적절한 대상 API를 선정하는 것이 중요</p>
<p><strong>❓ 적중률을 어떤 기준으로 “좋다/나쁘다” 판단하나요? 시스템/도메인에 따라 다른가요?</strong></p>
<p><strong>❓ 캐시 용량이 제한될 때 “무엇을 캐시해야 하는지” 우선순위를 정하는 기준은?</strong></p>
<h3 id="로컬-캐시와-리모트-캐시">로컬 캐시와 리모트 캐시</h3>
<p>서버가 사용하는 캐시에는 두 종류가 있다.</p>
<ul>
<li>로컬 캐시: 서버 프로세스와 동일한 메모리 저장소를 공유하며, 캐시 저장소로 사용</li>
<li>리모트 캐시: 서버 프로세스와는 다른 별도의 프로세스를 캐시 저장소로 사용</li>
</ul>
<p>로컬 캐시는 구현이 간단하고 외부 연동이 필요하지 않다는 장점이 있으나, 서버 프로세스와 자원을 공유하기 때문에 저장할 수 있는 데이터에 한계가 있음, 또한 서버 프로세스를 재시작 할 경우 캐시 데이터가 초기화되는 문제</p>
<p>리모트 캐시의 경우 캐시 크기를 유연하게 확장할 수 있으며, 서버 프로세스가 재시작되더라도 캐시 데이터는 유지된다. 다만 주가적인 인프라 도입이 필요하고 서버와 연동을 위한 기능을 추가적으로 작성해야한다.</p>
<ul>
<li>현재 상황에서 캐시 대상 데이터들의 성질을 파악하고 적절한 형태의 캐시 저장소를 도입해야한다.</li>
</ul>
<p>트래픽이 순간적으로 급증하는 패턴에 대하여 캐시 데이터를 사전에 적재하는 전략도 고려할 수 있음.</p>
<p><strong>❓ 로컬 캐시와 리모트 캐시를 각각 선택해야 하는 상황을 예시로 설명해보세요.</strong></p>
<p><strong>❓ 수평 확장 환경에서 로컬 캐시를 쓸 때 일관성 문제를 어떻게 다루나요?</strong></p>
<p><strong>❓ 리모트 캐시(예: Redis)를 도입했을 때 새로 생기는 운영/장애 포인트는?</strong></p>
<p><strong>❓로컬 캐시 도입을 결정하기 위해 고려한 사항들은 무엇인지, 리모트 캐시를 도입하지 않은 이유는?</strong></p>
<h3 id="캐시-무효화">캐시 무효화</h3>
<p>캐시를 사용할 경우 유효하지 않은 데이터에 대한 캐시 무효화(삭제) 전략을 반드시 고려해야한다.</p>
<p>데이터의 원본이 수정된 상황에서 캐시 데이터가 갱신되거나 삭제되지 않을 경우 사용자 입장에서는 변경되기 이전의 데이터를 확인하기 때문에 심각한 문제가 발생할 수 있다.</p>
<p><strong>❓ 캐시 무효화 전략 3가지(Write-through/Write-around/Cache-aside 등)를 비교해보세요.</strong></p>
<p><strong>❓데이터 변경이 잦은 도메인에서 캐시 일관성을 보장하려면 어떤 패턴(이벤트 기반 무효화 등)을 쓰나요?</strong></p>
<h3 id="그-외-서버-성능-향상-방법">그 외 서버 성능 향상 방법</h3>
<p>그 외 서버의 성능을 향상시킬 수 있는 방법으로는 </p>
<ul>
<li>가비지 컬렉터 튜닝</li>
<li>대용량 처리를 위한 스트림 활용(메모리 활용)</li>
<li>응답 데이터를 압축하여 서빙</li>
<li>브라우져 캐시 활용 또는 CDN 도입</li>
<li>대기열 도입</li>
</ul>
<p>등이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud로 구현하는 MSA 인프라 아키텍처 - (1) Service Registry & Discovery]]></title>
            <link>https://velog.io/@jw_kim/Spring-Cloud%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-MSA-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-Service-Registry-Discovery</link>
            <guid>https://velog.io/@jw_kim/Spring-Cloud%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-MSA-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1-Service-Registry-Discovery</guid>
            <pubDate>Wed, 03 Dec 2025 18:13:21 GMT</pubDate>
            <description><![CDATA[<h3 id="1-왜-service-registry가-필요한가">1. 왜 Service Registry가 필요한가?</h3>
<p>모놀리식 아키텍처에서는 하나의 애플리케이션이 단일 배포 단위로 묶여 있다.
애플리케이션 프로세스가 하나고, 보통 고정된 포트에서 동작하기 때문에 “서비스 위치”를 관리하는 것은 어렵지 않다.</p>
<p>[단일 애플리케이션]  →  <a href="http://app.example.com:8080">http://app.example.com:8080</a></p>
<p>반면 <strong>MSA(Microservices Architecture)</strong>에서는 도메인 중심으로 여러 개의 독립적인 서비스가 분리되고, 각 서비스가 각각의 배포 단위를 가진다. 여기에 클라우드 컴푸팅 기술이 더해지면 다음과 같은 특징이 생긴다.</p>
<ul>
<li><p>서비스 인스턴스 수가 동적으로 증가하거나 감소</p>
</li>
<li><p>각 인스턴스의 IP / Port가 고정되어 있지 않음</p>
</li>
<li><p>배포·롤링 업데이트마다 인스턴스 구성이 지속적으로 변경</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/bdcf06a3-f567-4cd0-9316-668e73f5630f/image.png" alt=""></p>
<p>규모가 작은 환경에서는 환경 변수나 설정 파일에 서비스 URL을 직접 적어 두는 방식으로 어느 정도 버틸 수 있다.
하지만 실제 아마존이나 넷플릭스처럼 수백~수천 개의 마이크로서비스를 운영하는 환경에서는</p>
<p><code>“모든 서비스 인스턴스의 IP/Port를 사람이 직접 관리한다”</code> 는 것은 사실상 불가능하다.</p>
<p>이런 배경 때문에 등장한 것이 Service Registry / Service Discovery 패턴이다.</p>
<hr>
<h3 id="2-service-registry--service-discovery-패턴-정리">2. Service Registry &amp; Service Discovery 패턴 정리</h3>
<h4 id="21-service-registry-서버-레지스트리">2.1 Service Registry (서버 레지스트리)</h4>
<p><strong>Service Registry(서비스 레지스트리)</strong>는 마이크로서비스 인스턴스의 </p>
<ul>
<li>IP / Port</li>
<li>서비스 이름</li>
<li>메타데이터(환경, 버전, zone 등) </li>
</ul>
<p>를 중앙에서 저장하고, 각 인스턴스를 서비스 이름으로 식별할 수 있도록 매핑 정보를 관리하는 컴포넌트다.</p>
<p>간단히 말해:</p>
<pre><code>“catalog-service라는 이름으로 어떤 인스턴스들이 어디에서 떠 있는지”
를 알고 있는 전화번호부 역할을 한다.</code></pre><h4 id="22-service-discovery-서버-디스커버리">2.2 Service Discovery (서버 디스커버리)</h4>
<p><strong>Service Discovery(서비스 디스커버리)</strong>는 클라이언트가</p>
<pre><code>“order-service야, catalog-service로 요청 하나 보내줘.”</code></pre><p>라고 서비스 이름만으로 요청을 보냈을 때,</p>
<ul>
<li><p>Service Registry에서 실제 인스턴스 목록을 조회하고</p>
</li>
<li><p>헬스 체크 / 로드밸런싱 정책을 적용해</p>
</li>
<li><p>어느 인스턴스로 보낼지 결정하는 메커니즘을 의미한다.</p>
</li>
</ul>
<p>즉, 클라이언트 입장에서는 더 이상 <code>http://10.0.1.23:8080</code> 같은 구체적인 주소를 알 필요가 없고,  <code>“catalog-service에게 보내라”</code> 는 논리적인 이름만 알고 있으면 된다.</p>
<hr>
<h3 id="3-spring-eureka-소개">3. Spring Eureka 소개</h3>
<p>넷플릭스는 자사 인프라에서 이 문제를 해결하기 위해 Eureka라는 이름의 Service Registry &amp; Service Discovery 서버를 개발해 사용해 왔다.</p>
<p>이후 Spring 진영에서는 이를 손쉽게 사용할 수 있도록 Spring Cloud Netflix 프로젝트에 통합(2015.03)했고, Spring Boot 애플리케이션에서도 Eureka 기반의 서비스 등록·조회 기능을 쉽게 사용할 수 있게 되었다.</p>
<p>Eureka는 크게 두 가지 역할로 나뉜다.</p>
<ul>
<li><p><strong>Eureka Server</strong></p>
<ul>
<li><p>Service Registry 역할</p>
</li>
<li><p>서비스 인스턴스 정보(IP, Port, 상태 등)를 저장하고 관리</p>
</li>
</ul>
</li>
<li><p><strong>Eureka Client</strong></p>
<ul>
<li><p>실제 비즈니스 로직을 수행하는 각 마이크로서비스</p>
</li>
<li><p>기동 시 자신을 Eureka Server에 등록하고,  다른 서비스 위치가 필요할 때 Eureka로부터 조회</p>
</li>
</ul>
</li>
</ul>
<p>Spring Boot에서의 의존성은 대략 다음과 같이 추가한다.</p>
<pre><code>// Eureka Server
implementation &#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-server&#39;

// Eureka Client
implementation &#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-client&#39;</code></pre><hr>
<h3 id="4-eureka-server-기본-구성">4. Eureka Server 기본 구성</h3>
<h4 id="41-서버-애플리케이션-만들기">4.1 서버 애플리케이션 만들기</h4>
<p>먼저 Eureka Server로 동작할 Spring Boot 애플리케이션을 하나 만든다.
여기에 @EnableEurekaServer 애노테이션을 추가하면 된다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}</code></pre>
<p>이제 설정 파일(application.yml)에서 서버 포트와 기본 설정을 잡아준다.</p>
<pre><code class="language-yaml">server:
  port: 8761

spring:
  application:
    name: eureka-server</code></pre>
<h4 id="42-단일-eureka-server로-동작시키기">4.2 단일 Eureka Server로 동작시키기</h4>
<p>Eureka Server는 내부적으로도 Eureka Client 역할을 수행한다. 즉, 다른 Eureka Server에 자신을 등록하거나, 레지스트리를 조회할 수 있다.</p>
<p>하지만 단일 레지스트리 서버로만 사용할 때는 자기 자신을 다시 Eureka에 등록할 필요가 없기 때문에 Client 기능을 끄는 것이 일반적이다.</p>
<pre><code class="language-yaml">eureka:
  client:
    # 서버 자신을 Eureka에 등록하지 않음
    register-with-eureka: false
    # 다른 레지스트리로부터 레지스트리를 가져오지 않음
    fetch-registry: false</code></pre>
<p><code>register-with-eureka: false</code></p>
<ul>
<li>“나는 다른 Eureka Server에 등록하지 않겠다”</li>
</ul>
<p><code>fetch-registry: false</code></p>
<ul>
<li>“다른 Eureka Server로부터 레지스트리를 가져오지 않겠다”</li>
</ul>
<hr>
<h3 id="5-eureka-server가-client-역할을-하는-이유">5. Eureka Server가 Client 역할을 하는 이유</h3>
<p>그렇다면 왜 Eureka Server도 Client 역할을 할 수 있도록 설계되어 있을까?</p>
<p>고가용성(HA)이 필요한 환경에서는 Eureka Server 역시 다중 인스턴스로 구성하고, 각 서버가 서로를 peer로 인식해서 레지스트리를 복제한다.</p>
<p>서버가 1대만 있으면 장애 시 전체 시스템에 큰 영향을 미친다. 
서버를 2대 이상 두고 서로 레지스트리를 동기화하면,</p>
<p>한 대가 장애가 나더라도 나머지 서버가 계속 Service Registry 역할을 수행할 수 있다.</p>
<p>이를 위해 Eureka Server도 Eureka Client로서 동작하면서 서로를 service-url.defaultZone에 등록하는 구조를 사용한다.</p>
<hr>
<h3 id="6-자주-사용하는-eureka-server-설정">6. 자주 사용하는 Eureka Server 설정</h3>
<h4 id="61-인스턴스헬스-체크-관련-설정-eurekainstance">6.1 인스턴스/헬스 체크 관련 설정 (eureka.instance)</h4>
<p>Eureka Server 자체도 “하나의 서비스 인스턴스”이기 때문에,
다른 서비스들과 마찬가지로 eureka.instance 설정을 사용할 수 있다.</p>
<pre><code class="language-yaml">eureka:
  instance:
    hostname: eureka-server-1           # 실제 환경에 맞는 호스트/DNS
    # prefer-ip-address: true           # 필요 시 IP 기반으로 등록
    health-check-url-path: /actuator/health
    status-page-url-path: /actuator/info
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90</code></pre>
<ul>
<li><p><code>hostname</code></p>
<ul>
<li><p>Eureka에 등록될 때 사용할 호스트 이름</p>
</li>
<li><p>실제로는 보통 클라이언트 서비스에서 도메인 기반 등록을 할 때 더 많이 활용</p>
</li>
</ul>
</li>
<li><p><code>health-check-url-path</code></p>
<ul>
<li><p>Eureka가 인스턴스 상태를 확인하기 위해 호출하는 URL</p>
</li>
<li><p>Spring Boot Actuator를 사용하면 /actuator/health로 지정하는 것이 일반적</p>
</li>
</ul>
</li>
<li><p><code>status-page-url-path</code></p>
<ul>
<li>상태 페이지 URL (Eureka 대시보드에서 링크로 노출)</li>
</ul>
</li>
<li><p><code>lease-renewal-interval-in-seconds</code></p>
<ul>
<li><p>클라이언트가 Eureka에 <strong>하트비트(heartbeat)</strong>를 보내는 주기</p>
</li>
<li><p>기본 30초 수준. 너무 짧게 줄이면 트래픽 부담 증가</p>
</li>
</ul>
</li>
<li><p><code>lease-expiration-duration-in-seconds</code></p>
<ul>
<li>이 시간 동안 하트비트가 오지 않으면, Eureka가 해당 인스턴스를 죽었다고 판단하는 시간</li>
</ul>
</li>
</ul>
<h4 id="62-자기-보호-모드-설정-eurekaserver">6.2 자기 보호 모드 설정 (eureka.server)</h4>
<p>Eureka Server는 네트워크 이슈 등으로 인해 하트비트가 갑자기 줄어드는 경우에도,
정상 인스턴스를 함부로 레지스트리에서 제거하지 않도록 자기 보호 모드(Self-Preservation)를 제공한다.</p>
<pre><code class="language-yaml">eureka:
  server:
    enable-self-preservation: true
    renewal-percent-threshold: 0.85
    eviction-interval-timer-in-ms: 60000</code></pre>
<ul>
<li><p><code>enable-self-preservation</code></p>
<ul>
<li><p>기본값은 true이며, 자기 보호 모드를 활성화한다.</p>
</li>
<li><p>네트워크 장애 등으로 하트비트가 일시적으로 급감하더라도 일정 기준까지는 인스턴스를 바로 제거하지 않는다.</p>
</li>
<li><p>운영 환경에서는 보통 true 유지가 일반적이고, 개발 환경에서는 디버깅을 위해 false로 두기도 한다.</p>
</li>
</ul>
</li>
<li><p><code>renewal-percent-threshold</code></p>
<ul>
<li>예상되는 정상 하트비트 대비, 어느 정도 비율 이상이 유지되어야 “정상”으로 볼 것인지 기준</li>
</ul>
</li>
<li><p><code>eviction-interval-timer-in-ms</code></p>
<ul>
<li>만료된 인스턴스를 레지스트리에서 실제로 제거하는 주기 (기본 60초 수준)</li>
</ul>
</li>
</ul>
<p>개발 단계에서 인스턴스를 자주 재기동하면, “이미 죽은 인스턴스가 대시보드에서 한동안 남아 있는” 현상을 볼 수 있는데, 대부분 이 <code>self-preservation</code>과 <code>eviction</code> 주기 설정 때문에 발생한다.</p>
<hr>
<h3 id="7-eureka-server-다중-인스턴스peer-구성-예시">7. Eureka Server 다중 인스턴스(peer) 구성 예시</h3>
<p>Eureka Server를 2대로 구성하는 예시를 보자.
각 인스턴스는 서로를 peer로 등록해 레지스트리를 복제한다.</p>
<h4 id="71-eureka-server-1-설정">7.1 eureka-server-1 설정</h4>
<pre><code class="language-yaml">spring:
  application:
    name: eureka-server

server:
  port: 8761

eureka:
  instance:
    hostname: eureka-server-1
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

  client:
    # peer 구성이므로 두 옵션 모두 true
    register-with-eureka: true
    fetch-registry: true
    service-url:
      # &quot;내가 등록/조회할 대상&quot; → 반대편 서버 주소
      defaultZone: http://eureka-server-2:8762/eureka/

  server:
    enable-self-preservation: true
    eviction-interval-timer-in-ms: 60000</code></pre>
<h4 id="72-eureka-server-2-설정">7.2 eureka-server-2 설정</h4>
<pre><code class="language-yaml">spring:
  application:
    name: eureka-server

server:
  port: 8762  # 1번과 다른 포트 (LB 앞에 두면 동일 포트도 가능)

eureka:
  instance:
    hostname: eureka-server-2
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      # 이 서버는 1번 서버를 peer로 본다
      defaultZone: http://eureka-server-1:8761/eureka/

  server:
    enable-self-preservation: true
    eviction-interval-timer-in-ms: 60000</code></pre>
<p><strong>두 설정의 핵심은 다음 한 줄이다.</strong></p>
<pre><code class="language-yaml">eureka.client.service-url.defaultZone</code></pre>
<ul>
<li><p>“이 인스턴스가 등록/조회할 Eureka Server는 어디인가?” 를 지정한다.</p>
</li>
<li><p>peer 구성에서는 서로의 주소를 defaultZone에 등록하여 레지스트리 정보를 양방향으로 동기화한다.</p>
</li>
</ul>
<hr>
<h3 id="8-정리">8. 정리</h3>
<p>여기까지 정리한 내용을 다시 한 번 요약해보면:</p>
<ul>
<li><p>MSA &amp; 클라우드 환경에서는 서비스 인스턴스의 IP/Port가 동적으로 변하기 때문에 사람이나 설정 파일만으로는 서비스 위치 관리가 불가능에 가깝다.</p>
</li>
<li><p>이를 해결하기 위해 Service Registry / Service Discovery 패턴이 등장했고, 넷플릭스는 이를 구현한 Eureka를 운영해 왔다.</p>
</li>
<li><p>Spring에서는 Spring Cloud Netflix Eureka를 통해 Spring Boot 애플리케이션에서도 손쉽게 Eureka 기반 서비스 등록/조회 기능을 제공한다.</p>
</li>
<li><p>Eureka는 </p>
<ul>
<li>Eureka Server: 레지스트리 관리, 서비스 등록/조회</li>
<li>Eureka Client: 실제 비즈니스 서비스, 자신 등록 + 다른 서비스 조회
로 역할이 나뉜다.</li>
</ul>
</li>
<li><p>Eureka Server는</p>
<ul>
<li>단일 서버 모드에서는 register-with-eureka=false, fetch-registry=false</li>
<li>다중 서버(HA) 구성에서는 서로를 peer로 등록하여 레지스트리를 복제한다.</li>
</ul>
</li>
<li><p>실제 운영 환경에서는</p>
<ul>
<li>health-check-url-path, lease-interval, self-preservation 등
몇 가지 핵심 옵션을 이해하고 적절히 튜닝하는 것이 중요하다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] mysqldump를 활용한 MySQL DB 백업과 복원 방법 01]]></title>
            <link>https://velog.io/@jw_kim/MySQL-mysqldump%EB%A1%9C-DB-%EB%B0%B1%EC%97%85%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@jw_kim/MySQL-mysqldump%EB%A1%9C-DB-%EB%B0%B1%EC%97%85%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Tue, 08 Jul 2025 04:04:40 GMT</pubDate>
            <description><![CDATA[<h2 id="💡-왜-데이터베이스-백업이-중요한가">💡 왜 데이터베이스 백업이 중요한가?</h2>
<p>데이터베이스는 대부분의 서비스에서 핵심 자산이며, 사용자 정보, 주문 내역, 결제 기록 등 손실 시 복구가 불가능하거나 심각한 비즈니스 피해를 야기할 수 있는 정보를 담고 있습니다. 하드웨어 장애, 실수로 인한 데이터 삭제, 랜섬웨어 공격, 소프트웨어 버그 등은 언제든 발생할 수 있으며, 이때 정기적인 백업 없이는 서비스를 정상 상태로 복구하기 어렵습니다. 백업은 단순한 보안책이 아니라, 서비스의 신뢰성과 연속성을 보장하는 가장 기본적인 안전망입니다.</p>
<p>이번 포스트에서는 MySQL에서 제공하는 <code>mysqldump</code>를 활용하여 MySQL DB를 백업하고 복원하는 방법에 대해 알아보겠습니다.</p>
<h2 id="📌-데이터베이스-백업의-종류">📌 데이터베이스 백업의 종류</h2>
<p>데이터베이스 백업은 목적과 방식에 따라 다음과 같이 나뉩니다:</p>
<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>SQL 쿼리 형식으로 덤프 (<code>mysqldump</code>, <code>pg_dump</code> 등). 이식성과 가독성이 좋음.</td>
</tr>
<tr>
<td><strong>물리 백업</strong></td>
<td>실제 데이터 파일을 복사 (<code>xtrabackup</code>, <code>LVM snapshot</code>, 디스크 복사 등). 대용량에 적합하며 성능 손실이 적음.</td>
</tr>
</tbody></table>
<hr>
<h2 id="1mysqldump로-백업하기">1.<code>mysqldump</code>로 백업하기</h2>
<h3 id="1-전체-db-백업">(1) 전체 DB 백업</h3>
<pre><code class="language-bash">mysqldump -u [사용자명] -p --all-databases &gt; all-databases.sql
</code></pre>
<h3 id="2-특정-db만-백업">(2) 특정 DB만 백업</h3>
<pre><code class="language-bash">mysqldump -u [사용자명] -p [데이터베이스명] &gt; database.sql
</code></pre>
<h3 id="3-특정-테이블만-백업">(3) 특정 테이블만 백업</h3>
<pre><code class="language-bash">mysqldump -u [사용자명] -p [데이터베이스명] table1 table2 &gt; partial.sql
</code></pre>
<h3 id="4-자주-사용하는-옵션">(4) 자주 사용하는 옵션</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>--single-transaction</code></td>
<td>InnoDB 사용 시 전체 일관된 상태로 백업</td>
</tr>
<tr>
<td><code>--quick</code></td>
<td>메모리 절약을 위해 row 단위로 읽기</td>
</tr>
<tr>
<td><code>--routines</code></td>
<td>저장 프로시저 포함</td>
</tr>
<tr>
<td><code>--triggers</code></td>
<td>트리거 포함 (기본값: 포함됨)</td>
</tr>
<tr>
<td><code>--set-gtid-purged=OFF</code></td>
<td>GTID 복제 환경에서 권장</td>
</tr>
</tbody></table>
<h3 id="예시">예시</h3>
<pre><code class="language-bash">mysqldump -u root -p --single-transaction --quick --routines mydb &gt; mydb.sql
</code></pre>
<hr>
<h2 id="2-백업-파일-압축하기">2. 백업 파일 압축하기</h2>
<p>백업 파일은 용량이 크기 때문에 압축하는 것이 일반적입니다.</p>
<pre><code class="language-bash">gzip mydb.sql
# 결과: mydb.sql.gz
</code></pre>
<p>복원 전에는 압축을 해제해야 합니다.</p>
<pre><code class="language-bash">gunzip mydb.sql.gz
</code></pre>
<hr>
<h2 id="3-mysqldump로-복원하기">3. <code>mysqldump</code>로 복원하기</h2>
<p>복원은 단순히 <code>mysql</code> 클라이언트를 통해 백업 SQL 파일을 실행하면 됩니다.</p>
<pre><code class="language-bash">mysql -u [사용자명] -p [대상DB명] &lt; mydb.sql
</code></pre>
<p><strong>주의:</strong> 복원 전에 DB가 생성되어 있어야 합니다. 없다면 먼저 생성합니다.</p>
<pre><code class="language-sql">CREATE DATABASE mydb;
</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>mysqldump의 강력한 기능 덕분에 데이터베이스의 백업과 복원을 쉽고 빠르게 할 수 있게되었습니다. 다만, mysqldump로 생성된 백업 파일들이 서비스와 동일한 서버에 보관될 경우, 서버에 문제가 발생 시 복원에 필요한 파일에도 접근이 불가능한 문제가 발생할 수 있습니다. 때문에 NAS나 S3같은 별도의 저장소에 백업 파일을 보관하는 것이 일반적입니다.
다음 포스트에서는 AWS CLI를 활용하여 S3(NCP Object Storage) 저장소에 백업 파일을 전송하는 방법을 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] 불변 객체와 가변객체: 불변 객체를 선호해야하는 이유]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EC%99%80-%EA%B0%80%EB%B3%80%EA%B0%9D%EC%B2%B4-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%84%A0%ED%98%B8%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EC%99%80-%EA%B0%80%EB%B3%80%EA%B0%9D%EC%B2%B4-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%84%A0%ED%98%B8%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 29 Jun 2025 12:49:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>💡 많은 고수 개발자 분들이 불변 객체 사용의 중요성에 대해 이야기하고있습니다. 소프트웨어 개발에서 매우 중요하지만 때로는 간과될 수 있는 개념인 <strong>불변 객체(Immutable Object)</strong>와 <strong>가변 객체(Mutable Object)</strong>에 대해 조금 더 깊이 알아보고 왜 가변 객체의 사용보다 불변 객체 사용을 지향해야 하는지 그 이유를 알아보겠습니다.</p>
</blockquote>
<h3 id="1-불변-객체와-가변-객체-그-정의부터">1. 불변 객체와 가변 객체, 그 정의부터!</h3>
<p>우선 두 개념을 명확히 정의해보곘습니다.</p>
<ul>
<li><strong>가변 객체(Mutable Object)</strong>: 이름 그대로 값이 변할 수 있는 객체를 의미합니다. 객체가 생성된 후에도 외부에서 해당 객체의 상태(필드 값)를 변경할 수 있습니다. 대부분의 언어에서 기본적으로 제공하는 List, Map 같은 컬렉션 타입이 대표적인 가변 객체입니다.</li>
</ul>
<pre><code class="language-java">// Java 예시
List&lt;String&gt; mutableList = new ArrayList&lt;&gt;();
mutableList.add(&quot;Apple&quot;); // 객체의 상태 변경
mutableList.add(&quot;Banana&quot;);</code></pre>
<ul>
<li><strong>불변 객체(Immutable Object)</strong>: 가변 객체와 달리 한번 생성되면 그 상태를 절대 변경할 수 없는 객체를 의미합니다. 객체가 생성될 때 모든 값이 할당되며, 이후에는 어떤 메서드를 통해서도 내부 상태를 수정할 수 없습니다. 값을 변경해야 할 때는 기존 객체의 값을 복사한 새로운 불변 객체를 생성하여 반환하는 방식으로 동작합니다. 자바의 String 클래스나 Integer 같은 래퍼 클래스들이 대표적인 불변 객체입니다.</li>
</ul>
<pre><code class="language-java">// Java 예시
String originalString = &quot;Hello&quot;;
String newString = originalString.concat(&quot; World&quot;); // 새로운 String 객체 생성
// originalString은 여전히 &quot;Hello&quot; 입니다.</code></pre>
<hr>
<h3 id="2-왜-가변-객체보다-불변-객체를-선호해야-할까">2. 왜 가변 객체보다 불변 객체를 선호해야 할까?</h3>
<p>이제 핵심 질문입니다. 왜 많은 전문가들이 불변 객체 사용을 권장할까요? 특히 현대의 복잡한 소프트웨어 환경에서는 불변 객체의 장점이 더욱 두드러집니다.</p>
<h4 id="1-예측-가능성-및-이해-용이성-코드를-읽는-즐거움">1) 예측 가능성 및 이해 용이성: 코드를 읽는 즐거움</h4>
<p>가변 객체는 언제든 상태가 변할 수 있으므로, 해당 객체를 사용하는 코드 전체에서 그 상태 변화를 예측하고 추적하기 어렵습니다. 특히 함수나 메서드를 호출할 때 원본 객체가 변경될 수 있다면, 개발자는 항상 <strong>부수 효과(Side Effect)</strong>를 염두에 두어야 합니다.</p>
<p>반면 불변 객체는 한 번 생성되면 상태가 변하지 않으므로, 객체의 동작을 예측하기 매우 쉽습니다. <strong>특정 시점에 객체가 어떤 상태를 가지고 있는지 명확</strong>하게 알 수 있어, 코드를 이해하고 디버깅하는 데 드는 많은 공수를 크게 줄여줍니다. 이는 클린 아키텍처에서 강조하는 <strong>계층 간의 명확한 역할 분리</strong>와도 맞닿아 있습니다.</p>
<h4 id="2-⭐️-스레드-안정성thread-safety-멀티스레드-환경의-구세주">2) ⭐️ 스레드 안정성(Thread Safety): 멀티스레드 환경의 구세주</h4>
<p>멀티스레드 환경은 현대 소프트웨어 개발의 필수 요소입니다. Spring Applicaion의 경우 톰캣 기반으로 구동하기 때문에 기본적으로는 멀티 스레드 환경에서 구동되는 소프트웨어라고 할 수 있습니다. <strong>여러 활성 상태의 스레드가 동일한 가변 객체에 동시에 접근하여 상태를 변경</strong>하는 경우를 생각해봅시다. 이런 상황에서 너무나 간단하게 <strong>동시성 문제가 발생</strong>할 수 있습니다. 이는 데이터 불일치, 값이 오염되는 문제, 심지어 애플리케이션 충돌로 이어질 수 있으며, 복잡한 동기화 메커니즘(락, 세마포어 등)을 필요로 합니다. 이러한 동기화 로직은 데드락이나 라이브락과 같은 더 큰 문제를 야기하기도 합니다.</p>
<p>하지만 불변 객체는 상태가 변경되지 않으므로, <strong>여러 스레드가 동시에 접근하더라도 데이터를 안전하게 공유</strong>할 수 있습니다. 별도의 동기화 메커니즘이 필요 없어 멀티스레드 프로그래밍의 복잡성을 크게 줄여주며, <strong>도메인 주도 설계(DDD)</strong>에서 도메인 모델의 일관성을 유지하는 데 결정적인 이점을 제공합니다.</p>
<h4 id="3-부수-효과-최소화-버그-없는-코드의-지름길">3) 부수 효과 최소화: 버그 없는 코드의 지름길</h4>
<p>가변 객체를 사용하는 메서드는 종종 객체의 내부 상태를 변경하는 &#39;부수 효과&#39;를 가질 수 있습니다. 이러한 부수 효과는 예기치 않은 동작을 유발하고 코드의 흐름을 파악하기 어렵게 만들어 버그의 온상이 됩니다.</p>
<p>불<strong>변 객체는 상태를 변경하는 메서드를 가질 수 없습니다</strong>. 대신, <strong>상태가 변경된 새로운 객체를 반환</strong>하는 방식으로 동작합니다. 이는 함수형 프로그래밍 패러다임과도 잘 맞으며, <strong>부수 효과를 최소화하여 코드의 순수성을 높이고 버그 발생 가능성을 현저히 줄여줍니다.</strong></p>
<h4 id="4-캐싱-및-컬렉션-사용-용이성-성능과-편의성-모두-잡기">4) 캐싱 및 컬렉션 사용 용이성: 성능과 편의성 모두 잡기</h4>
<p>가변 객체는 상태가 변할 수 있으므로 캐싱된 데이터의 유효성을 관리하기 어렵습니다. 캐시된 객체가 변경되면 캐시된 데이터와 실제 데이터 간의 불일치가 발생할 수 있습니다.</p>
<p>반면 불변 객체는 상태가 변하지 않으므로 한 번 계산된 값을 안전하게 캐시할 수 있습니다. 이는 성능 최적화에 유리하며, HashMap이나 HashSet 같은 컬렉션의 키로 사용될 때도 안정적인 동작을 보장합니다. DDD에서 복잡한 계산 결과나 <strong>값 객체(Value Object)</strong>를 효율적으로 재사용할 수 있게 합니다.</p>
<h4 id="5-테스트-용이성-믿을-수-있는-코드">5) 테스트 용이성: 믿을 수 있는 코드</h4>
<p>가변 객체를 테스트할 때는 객체의 초기 상태를 설정하고, 테스트 도중 발생할 수 있는 모든 상태 변화를 고려해야 하므로 테스트 코드가 복잡해질 수 있습니다.</p>
<p><strong>불변 객체는 상태가 고정되어 있으므로 특정 상태를 재현하기 쉽고, 테스트 간의 독립성이 보장</strong>됩니다. 이는 클린 아키텍처에서 각 <strong>계층의 단일 책임 원칙(SRP)</strong>을 지키고, 유닛 테스트를 용이하게 하는 데 크게 기여합니다.</p>
<hr>
<h3 id="3-ddddomain-driven-design-관점에서의-불변-객체">3. DDD(Domain-Driven Design) 관점에서의 불변 객체</h3>
<p>DDD는 복잡한 비즈니스 도메인을 모델링하는 데 중점을 둡니다. 불변 객체는 DDD의 여러 핵심 개념과 시너지를 발휘합니다.</p>
<ul>
<li><p><strong>값 객체(Value Object)</strong>: DDD의 가장 중요한 개념 중 하나인 값 객체는 &quot;측정하거나 서술하는 속성들을 모아놓은 객체&quot;로 정의됩니다. 예를 들어, <code>Money</code>, <code>Address</code>, <code>DateRange</code> 등이 있습니다. 값 객체는 그 본질상 불변이어야 합니다. $5는 항상 $5이지, $10으로 변하지 않습니다. 만약 금액이 변하면 새로운 Money 객체가 생성되는 것이 자연스럽습니다. 불변 값 객체는 공유하기 쉽고 부수 효과가 없으므로 도메인 모델의 견고성을 높입니다.</p>
</li>
<li><p><strong>도메인 이벤트(Domain Event)</strong>: 도메인 이벤트는 과거에 발생한 사실을 나타내므로 불변해야 합니다. 이벤트가 발생한 후에는 그 내용이 변경될 수 없습니다.</p>
</li>
<li><p><strong>스냅샷(Snapshot)</strong>: 특정 시점의 도메인 모델 상태를 저장하는 스냅샷도 불변 객체로 생성되어야 합니다.</p>
</li>
</ul>
<hr>
<h3 id="4-불변-객체-만능은-아니다-주의할-점">4. 불변 객체, 만능은 아니다! (주의할 점)</h3>
<p>불변 객체의 장점이 많지만, 무분별한 사용이 항상 최선은 아닙니다.</p>
<ul>
<li><p><strong>메모리 및 GC(Garbage Collection) 오버헤드</strong>: 불변 객체는 상태 변경 시 항상 새로운 객체를 생성하므로, 매우 빈번하게 객체를 변경해야 하는 상황에서는 많은 객체가 생성되고 소멸됩니다. 이는 가비지 컬렉션의 부담을 늘려 애플리케이션의 응답 시간에 미세한 영향을 줄 수 있습니다.</p>
</li>
<li><p><strong>성능 최적화는 나중에</strong>: 하지만 현대의 JVM과 GC 기술은 매우 발전하여 <strong>대부분의 비즈니스 애플리케이션에서는 이러한 오버헤드가 큰 문제가 되지 않습니다</strong>. <strong>불변 객체 사용으로 인한 성능 이슈는 대부분의 경우 코드의 안정성, 가독성, 유지보수성보다 후순위</strong>에 있습니다. 프로파일링을 통해 명확한 성능 병목 현상이 발견될 때만 가변 객체로의 전환이나 객체 풀링 같은 최적화 기법을 고려해야 합니다. 섣부른 최적화는 오히려 복잡성과 버그를 유발할 수 있습니다.</p>
</li>
</ul>
<hr>
<h3 id="마무리하며">마무리하며</h3>
<p>불변 객체는 현대 소프트웨어 개발에서 강력한 이점을 제공하며, 여러분의 코드를 더욱 예측 가능하고, 안전하며, 유지보수하기 쉽게 만들어 줄 것입니다. 특히 멀티스레드 환경과 복잡한 도메인 로직을 다룰 때 그 가치는 더욱 빛을 발합니다.</p>
<p>이제 여러분의 코드베이스에서 불변 객체를 적극적으로 활용해보는 것은 어떨까요? 작은 변화가 큰 차이를 만들 것입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] 인덱스. 왜 빠르고, 어떻게 동작할까?]]></title>
            <link>https://velog.io/@jw_kim/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EB%8D%B1%EC%8A%A4.-%EC%99%9C-%EB%B9%A0%EB%A5%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jw_kim/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EB%8D%B1%EC%8A%A4.-%EC%99%9C-%EB%B9%A0%EB%A5%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Fri, 27 Jun 2025 03:42:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>💡며칠 전부터 <a href="https://www.maeil-mail.kr/">매일매일</a>을 구독하면서 하루에 하나씩 기술 관련 이론을 공부하며 하루를 시작하고있습니다. 최근 데이터베이스 인덱스에 대한 질문에 대하여 공부를 하던 중 흥미로운 내용을 발견해서 함께 정리해보려 합니다.</p>
</blockquote>
<h3 id="인덱스">인덱스?</h3>
<p><strong>인덱스</strong>는 데이터베이스를 사용하는 개발자라면 반드시 알아야 할 핵심 개념 중 하나입니다. 인덱스를 설명할 때 흔히 백과사전의 <strong>색인</strong>을 예로 들곤 하는데요. 수많은 페이지 중에서 특정 내용을 빠르게 찾고 싶을 때, 색인 페이지를 보면 해당 키워드가 몇 페이지에 있는지 바로 알 수 있습니다.</p>
<p>데이터베이스 인덱스도 이와 비슷합니다. 추가적인 저장 공간(페이지)을 사용해서 자료 검색에 특화된 새로운 자료 구조를 만들고, 이를 통해 <strong>조회 성능을 크게 향상시키는 것</strong>이 바로 인덱스의 목적입니다.</p>
<p>그렇다면 이 인덱스의 데이터는 어떤 형태로 저장되어 있길래 조회 성능을 이렇게까지 높일 수 있는 걸까요? 인덱스의 자료 구조를 검색해보면 <strong>B-Tree</strong>라는 단어를 자주 접할 수 있습니다. 실제로 현대 대부분의 관계형 데이터베이스(RDB)는 인덱스 자료 구조로 B-Tree (혹은 B-Tree의 변형인 B+Tree)를 채택하고 있습니다.</p>
<table>
<thead>
<tr>
<th>DBMS</th>
<th>기본 인덱스 구조</th>
<th>특수 인덱스 유형</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>MySQL (InnoDB)</strong></td>
<td>B+Tree</td>
<td>Full-text, Spatial, Hash (MEMORY 엔진)</td>
<td>InnoDB는 클러스터형 인덱스를 사용해요.</td>
</tr>
<tr>
<td><strong>PostgreSQL</strong></td>
<td>B-Tree</td>
<td>Hash, GiST, GIN, BRIN, SP-GiST</td>
<td>다양한 데이터 타입에 최적화된 인덱스를 제공해요.</td>
</tr>
<tr>
<td><strong>Oracle</strong></td>
<td>B-Tree</td>
<td>Bitmap, Hash, Clustered, Function-based</td>
<td>병렬 처리와 옵티마이저 힌트와의 연동이 강력해요.</td>
</tr>
<tr>
<td><strong>SQL Server</strong></td>
<td>B+Tree</td>
<td>Full-text, XML, Spatial, Filtered</td>
<td>클러스터형/비클러스터형 인덱스를 선택할 수 있어요.</td>
</tr>
<tr>
<td><strong>MongoDB</strong></td>
<td>B-Tree</td>
<td>Geospatial, Text, Hashed</td>
<td>복합 인덱스와 다중 필드 인덱스를 지원해요.</td>
</tr>
<tr>
<td><strong>Redis</strong></td>
<td>없음 (자료구조 기반)</td>
<td>ZSET(Range), HASH, SET, BITMAP</td>
<td>검색용 인덱스가 아니라 데이터 구조 자체가 인덱스 역할을 해요.</td>
</tr>
<tr>
<td><strong>Elasticsearch</strong></td>
<td>Inverted Index</td>
<td>BKD Tree (for range), Term dictionary</td>
<td>검색 엔진에 특화되어 있고, 텍스트 분석 기반이에요.</td>
</tr>
</tbody></table>
<p>하지만 사실 인덱스에는 B-Tree 형태 외에도 여러 가지 자료 구조가 사용될 수 있습니다.</p>
<table>
<thead>
<tr>
<th>자료구조</th>
<th>특징</th>
<th>사용 목적</th>
</tr>
</thead>
<tbody><tr>
<td><strong>B-Tree / B+Tree</strong></td>
<td>정렬된 키를 저장하고, 범위 검색에 효율적이에요.</td>
<td>대부분의 조건( <code>=</code>, <code>&lt;</code>, <code>&gt;</code>, <code>BETWEEN</code>, <code>ORDER BY</code>)에 사용되는 <strong>범용 인덱스</strong>입니다.</td>
</tr>
<tr>
<td><strong>Hash Table</strong></td>
<td><code>=</code> (등가) 검색에 매우 빠르지만, 정렬/범위 검색은 불가능해요.</td>
<td><code>WHERE key = value</code>와 같이 정확히 일치하는 값을 찾을 때만 빠르게 처리해요.</td>
</tr>
<tr>
<td><strong>Bitmap</strong></td>
<td>공간 효율이 좋고, 카디널리티(중복도)가 낮은 데이터에 적합해요.</td>
<td><code>성별 = 남</code> / <code>국가코드 = KR</code>처럼 중복되는 값이 많은 경우에 유용해요.</td>
</tr>
<tr>
<td><strong>GIN (Postgres)</strong></td>
<td>다중 키워드 검색에 특화되어 있어요.</td>
<td>배열, JSONB, 전문 검색(Full-text search) 등에 사용돼요.</td>
</tr>
<tr>
<td><strong>GiST (Postgres)</strong></td>
<td>범용 인덱스 프레임워크로, 다양한 데이터 타입과 연산자를 지원해요.</td>
<td>위치 정보, 유사도 검색 등에 활용돼요.</td>
</tr>
<tr>
<td><strong>ZSET (Redis)</strong></td>
<td>정렬된 Set 형태로, 스코어(score) 기반으로 정렬돼요.</td>
<td>실시간 랭킹, 우선순위 큐 등에 사용돼요.</td>
</tr>
<tr>
<td><strong>BKD Tree (Lucene)</strong></td>
<td>고차원 숫자 및 벡터 데이터 처리에 강해요.</td>
<td>Elasticsearch에서 범위/필터 검색에 사용돼요.</td>
</tr>
</tbody></table>
<p>오늘 우리는 이 중에서 인덱스 자료 구조의 핵심이라고 할 수 있는 <strong>B-Tree</strong>와 <strong>Hash Table</strong>의 개념을 자세히 알아보고, 왜 인덱스에 다른 자료 구조가 아닌 B-Tree가 주로 사용되는지(사용되어야 하는지)에 대해 이야기해보겠습니다.</p>
<hr>
<h3 id="인덱스-자료-구조-자세히-보기">인덱스 자료 구조 자세히 보기</h3>
<p>앞서 살펴본 것처럼 인덱스에는 다양한 자료 구조가 사용될 수 있습니다. 이 중에서 Hash Table과 B-Tree 자료 구조에 대해 조금 더 자세히 알아볼겠습니다.</p>
<h3 id="hash-table-해시-테이블">Hash Table (해시 테이블)</h3>
<p>Hash Table은 <strong>(Key, Value) 쌍</strong>의 형태로 데이터를 저장하는 자료 구조입니다. Key 값으로 사용될 데이터를 <strong>해시 함수(Hash Function)</strong>를 통해 고유한 인덱스(주소)를 생성하고, 그 주소에 데이터를 저장합니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/3fd8cf69-aad9-4151-9bf0-1c74bbceda67/image.png" alt=""></p>
<p>이러한 특성 때문에 Hash Table이 적용된 인덱스에서는 <code>WHERE key = value</code>와 같이 <strong>정확히 일치하는 값을 찾는 등가(Equal) 검색 시</strong> 데이터의 위치를 한 번에 계산할 수 있어 <strong>O(1)이라는 매우 빠른 시간 복잡도</strong>로 검색을 지원합니다. 말 그대로 찰나의 순간에 데이터를 찾아낼 수 있다는 뜻이죠.</p>
<p>하지만 인덱스 자료구조로 Hash Table을 사용하지 못하는 데에는 이유가 있습니다. <code>where</code> 조건으로 고유 번호에 해당하는 1:1 대응 데이터를 조회하는 것 외에 <strong>대부분 데이터 조회는 특정 범위에 해당하는 범위 연산</strong>을 필요로 합니다. 하지만 Hash Table은 <strong>부등호</strong> (<code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>, <code>&gt;=</code>)나 <strong><code>BETWEEN</code> 등을 사용한 범위 연산을 지원하지 않습니다.</strong> 데이터가 해시 함수에 의해 무작위로 흩어져 저장되기 때문에, &quot;A부터 Z까지&quot;와 같은 <strong>범위의 데이터를 찾으려면 결국 전체 테이블을 스캔(Table Full Scan)</strong>해야 합니다. 또한, 데이터를 정렬하여 저장하지 않기 때문에 클러스터링(Clustering)을 지원하지 않는다는 단점도 있습니다. 그럼에도 불구하고 동등 비교 시 O(1)이라는 매력적인 성능 향상 효과 때문에 인덱스에 제한적으로 사용되기도 합니다.</p>
<blockquote>
<p>📘 <strong>잠깐! 스캔 방식에 따른 성능 차이</strong></p>
<p>데이터베이스는 데이터를 조회할 때 여러 방식의 <strong>스캔 전략</strong>을 사용합니다.<br>아래는 스캔 방식에 따른 <strong>성능 효율</strong>을 간단히 정리한 표입니다.</p>
<table>
<thead>
<tr>
<th>스캔 방식</th>
<th>설명</th>
<th>성능 효율</th>
</tr>
</thead>
<tbody><tr>
<td>Full Table Scan</td>
<td>인덱스 없이 테이블 전체를 읽음</td>
<td>❌ 매우 낮음</td>
</tr>
<tr>
<td>Index Full Scan</td>
<td>인덱스 전체를 순차적으로 읽음</td>
<td>⚠️ 중간</td>
</tr>
<tr>
<td>Index Range Scan</td>
<td>인덱스 범위 조건에 해당하는 부분만 탐색</td>
<td>✅ 효율적</td>
</tr>
<tr>
<td>Index Unique Scan</td>
<td>PK 또는 Unique 조건으로 1건만 조회</td>
<td>✅✅ 매우 효율적</td>
</tr>
<tr>
<td>Index Only Scan</td>
<td>인덱스만으로 모든 데이터 조회</td>
<td>✅✅ 매우 효율적</td>
</tr>
</tbody></table>
<p>Hash Table 자료구조를 사용하는 인덱스를 기준으로 범위 조회를 실행하게 된다면 가장 좋지 않은 성능의 <strong>Full Table Scan</strong>이 수행되는 것입니다.</p>
</blockquote>
<hr>
<h3 id="b-tree-비-트리">B-Tree (비-트리)</h3>
<p>B-Tree는 <strong>&quot;Balanced Tree(균형 트리)&quot;</strong>의 약자로, 이름처럼 균형이 잘 잡혀 있는 트리(Tree) 자료 구조를 의미합니다. 우리가 흔히 아는 이진 트리(Binary Tree)와 비슷하지만, B-Tree는 하나의 노드가 여러 개의 자식 노드를 가질 수 있는 <strong>다원 탐색 트리</strong>의 일종입니다.</p>
<p>B-Tree는 크게 <strong>루트 노드(Root Node), 내부 노드(Internal Node), 리프 노드(Leaf Node)</strong>로 구성되어 있으며, 각 노드에 데이터를 저장하고 있습니다.</p>
<h3 id="b-tree의-구조와-특징">B-Tree의 구조와 특징</h3>
<p>B-Tree는 데이터베이스 인덱스의 <strong>표준</strong>이라고 할 수 있습니다. 그 이유는 바로 B-Tree가 인덱스의 핵심 목표인 <strong>빠른 검색</strong>과 더불어 <strong>범위 검색, 데이터 삽입/삭제 시의 효율성</strong>을 모두 만족시키기 때문입니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/35866e4b-9088-48be-ae5c-d7d5094e0822/image.png" alt=""></p>
<h3 id="b-tree의-구성-요소">B-Tree의 구성 요소:</h3>
<ul>
<li><strong>루트 노드 (Root Node):</strong> 트리의 가장 상위에 있는 노드입니다. 모든 검색은 루트 노드에서 시작됩니다.</li>
<li><strong>내부 노드 (Internal Node):</strong> 루트 노드와 리프 노드 사이에 있는 노드들입니다. 내부 노드에는 다음 레벨의 자식 노드로 이동하기 위한 <strong>키(Key)</strong> 값과 해당 자식 노드의 <strong>주소(포인터)</strong>가 저장됩니다. 실제 데이터는 저장하지 않고, 검색 경로를 안내하는 역할을 합니다.</li>
<li><strong>리프 노드 (Leaf Node):</strong> 트리의 가장 하위에 있는 노드들입니다. 리프 노드에는 실제 데이터 레코드의 <strong>키 값</strong>과 해당 레코드가 저장된 테이블의 <strong>주소(ROWID 또는 Primary Key)</strong>가 함께 저장됩니다. 리프 노드는 항상 같은 레벨에 위치하여 트리가 <strong>균형을 유지</strong>하도록 돕습니다.</li>
</ul>
<h3 id="b-tree의-작동-원리-검색">B-Tree의 작동 원리 (검색):</h3>
<p>B-Tree에서 데이터를 검색하는 과정은 매우 효율적입니다. 예를 들어, 특정 <code>ID</code>를 가진 레코드를 찾는다고 가정해봅시다.</p>
<ol>
<li><strong>루트 노드</strong>에서 시작하여 찾으려는 <code>ID</code>와 노드에 저장된 키 값들을 비교합니다.</li>
<li>비교 결과에 따라 다음 레벨의 <strong>적절한 자식 노드</strong>로 이동합니다. (예: 찾으려는 ID가 노드의 키 값보다 작으면 왼쪽 자식, 크면 오른쪽 자식 등)</li>
<li>이 과정을 <strong>리프 노드</strong>에 도달할 때까지 반복합니다.</li>
<li>리프 노드에 도달하면 해당 키 값이 있는지 확인하고, 있다면 연결된 실제 데이터 레코드의 주소를 통해 데이터를 가져옵니다.</li>
</ol>
<p>B-Tree는 노드에 저장되는 키의 개수가 많을수록 트리의 깊이가 얕아지므로, 디스크 I/O (데이터를 읽고 쓰는 작업) 횟수를 줄여 검색 속도를 향상시킵니다. 트리의 깊이가 얕다는 것은 데이터를 찾기 위해 거쳐야 하는 노드의 수가 적다는 의미이고, 이는 곧 디스크에서 데이터를 읽어와야 하는 횟수가 적다는 것을 뜻합니다.</p>
<hr>
<h3 id="btree-비플러스-트리와-b-tree의-차이점">B+Tree (비플러스-트리)와 B-Tree의 차이점</h3>
<p>대부분의 현대 관계형 데이터베이스는 B-Tree보다는 <strong>B+Tree</strong>를 인덱스 자료 구조로 채택하고 있습니다. B+Tree는 B-Tree의 장점을 계승하면서, 특히 <strong>범위 검색 성능을 극대화</strong>하기 위해 고안된 자료 구조입니다.</p>
<h3 id="btree의-주요-특징-b-tree와의-차이점">B+Tree의 주요 특징 (B-Tree와의 차이점):</h3>
<ul>
<li><strong>내부 노드에는 데이터가 저장되지 않는다:</strong> B-Tree는 모든 노드에 데이터를 저장할 수 있지만, B+Tree는 <strong>오직 리프 노드에만 데이터(또는 데이터 레코드의 포인터)를 저장</strong>합니다. 내부 노드는 오로지 자식 노드를 가리키는 <strong>키 값과 포인터만</strong> 가지고 있습니다.<ul>
<li><strong>장점:</strong> 내부 노드의 크기가 작아지므로, 한 블록(페이지)에 더 많은 키 값을 저장할 수 있습니다. 이는 트리의 높이를 더욱 낮춰 디스크 I/O를 줄이는 데 유리합니다.</li>
</ul>
</li>
<li><strong>리프 노드는 연결 리스트(Linked List)로 연결되어 있다:</strong> B+Tree의 모든 리프 노드는 <strong>좌우로 연결 리스트</strong>처럼 서로 연결되어 있습니다.<ul>
<li><strong>장점:</strong> 이 연결 덕분에 <strong>범위 검색(Range Search)</strong>이 매우 효율적입니다. 특정 범위의 데이터를 찾을 때, 시작 값을 가진 리프 노드를 찾은 후 연결된 리스트를 따라가면서 필요한 범위의 데이터를 순차적으로 읽어낼 수 있습니다. B-Tree에서는 범위 검색 시 여러 노드를 오가며 찾아야 할 수도 있어 비효율적입니다.</li>
</ul>
</li>
<li><strong>모든 키 값은 리프 노드에 존재한다:</strong> B+Tree에서는 모든 키 값이 리프 노드에 중복 저장됩니다. 내부 노드는 단지 검색 경로를 위한 키 값을 가지고 있을 뿐입니다.<ul>
<li><strong>장점:</strong> 어떤 종류의 검색(단일 값 검색이든 범위 검색이든)이든 항상 리프 노드까지 도달해야 하므로, 검색 시간이 예측 가능하고 일관적입니다.</li>
</ul>
</li>
</ul>
<h3 id="왜-b-tree-대신-btree를-사용할까">왜 B-Tree 대신 B+Tree를 사용할까?</h3>
<p>데이터베이스 인덱스의 주된 목적 중 하나는 <strong>범위 검색</strong>입니다. 사용자는 특정 값을 찾는 것 외에도 &quot;나이가 20세에서 30세 사이인 사람들&quot;이나 &quot;날짜가 2025년 1월부터 3월까지인 데이터&quot;와 같은 범위 조건을 자주 사용합니다. B+Tree는 리프 노드가 연결 리스트로 이어져 있어 이러한 범위 검색에 압도적으로 효율적입니다.</p>
<p>또한, 내부 노드에 데이터를 저장하지 않음으로써 한 노드에 더 많은 키를 담을 수 있어 트리의 높이가 더 낮아지고, 이는 디스크에서 데이터를 읽어오는 횟수를 줄여 전반적인 성능을 향상시킵니다. 따라서 B+Tree는 대용량 데이터를 처리하는 관계형 데이터베이스의 인덱스에 가장 적합한 자료 구조로 널리 사용됩니다.</p>
<hr>
<h3 id="결론-왜-인덱스는-b-tree-btree일까">결론: 왜 인덱스는 B-Tree (B+Tree)일까?</h3>
<p>이제 왜 데이터베이스 인덱스에 B-Tree (정확히는 B+Tree)가 주로 사용되는지 명확해졌을 것입니다.</p>
<ul>
<li><strong>빠른 탐색 속도:</strong> 균형 잡힌 트리 구조로 어떤 데이터를 찾든 최악의 경우에도 트리의 깊이만큼만 탐색하면 되므로, 일관되고 빠른 검색 성능을 보장합니다. 이는 디스크 I/O를 최소화하는 데 핵심적입니다.</li>
<li><strong>효율적인 범위 검색:</strong> 특히 B+Tree의 리프 노드 연결은 <code>WHERE</code> 절에 부등호나 <code>BETWEEN</code>을 사용하는 범위 검색을 매우 효율적으로 처리할 수 있게 합니다.</li>
<li><strong>삽입/삭제 시 성능 유지:</strong> 새로운 데이터가 삽입되거나 삭제될 때도 트리의 균형을 자동으로 조절하여 검색 성능 저하를 방지합니다. Hash Table과 달리 데이터의 추가/삭제가 빈번해도 충돌 문제를 걱정할 필요가 적습니다.</li>
<li><strong>다양한 연산 지원:</strong> <code>=</code> 뿐만 아니라 <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>, <code>&gt;=</code>, <code>BETWEEN</code>, <code>ORDER BY</code> 등 다양한 조건절에 인덱스를 활용할 수 있어 활용도가 높습니다.</li>
</ul>
<p>결론적으로 B-Tree와 B+Tree는 <strong>빠른 단일 검색, 효율적인 범위 검색, 그리고 데이터 변경에 강하다는 인덱스의 요구사항을 모두 만족</strong>시키기 때문에 현대 데이터베이스 인덱스의 핵심 자료 구조로 자리 잡았습니다.</p>
<p>인덱스는 단순히 빠른 조회를 위한 도구가 아니라, 데이터베이스의 성능을 좌우하는 중요한 요소입니다. 오늘 다룬 B-Tree와 Hash Table의 개념을 잘 이해하고 활용하여 더욱 효율적인 데이터베이스 설계와 쿼리 작성을 할 수 있기를 바랍니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] 분산락, 분산 환경에서 동시성 제어하기]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%B6%84%EC%82%B0%EB%9D%BD-%EB%B6%84%EC%82%B0-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%B6%84%EC%82%B0%EB%9D%BD-%EB%B6%84%EC%82%B0-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 09 May 2025 15:41:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>ℹ️ 이 글은 항해99 백엔드 플러스 과정 6주차 회고를 위해 작성되었습니다.
분산 락의 개념과 도입 시 고려할 점들을 실제 예제와 함께 정리해보았습니다. <br>
<strong>6주차 목표</strong></p>
</blockquote>
<ul>
<li>분산 락을 도입하는 이유에 대해 알아보고 E-Commerce 서비스에서 적절한 적용 지점을 찾아봅니다.</li>
<li>Redis를 활용하여 데이터 캐시를 적용하고 성능을 분석해봅니다.</li>
</ul>
<h3 id="경쟁-상태race-condition와-동시성-제어">경쟁 상태(Race condition)와 동시성 제어</h3>
<p>대부분의 웹 서비스는 여러 사용자가 동시에 이용하게 됩니다. 예를 들어, 이커머스 서비스에서는 하나의 상품을 여러 명이 동시에 주문하고, 콘서트 예매 서비스에서는 하나의 좌석을 여러 명이 동시에 예매하려고 시도합니다. 이러한 요청은 모두 하나의 데이터베이스에서 동일한 데이터를 읽거나 수정하려고 하기 때문에 충돌이 발생할 수 있습니다.</p>
<p>두 명의 사용자가 재고가 1개 남은 상품을 동시에 주문하는 상황을 가정해봅시다. 재고가 1개뿐이라면 두 명 중 한 명은 주문에 실패해야 하지만, 동시 요청에 대한 처리가 없다면 두 사용자 모두 주문 성공 처리가 되는 상황이 발생할 수 있습니다. 이처럼 동시에 같은 자원에 대한 접근으로 인해 발생하는 상황을 <strong>경쟁 상태(Race Condition)</strong>라고 합니다.</p>
<p>이러한 문제를 방지하기 위해서는 동시 요청에 대해 순서를 정해 차례대로 처리하는 장치, 즉 <strong>동시성 제어</strong>가 필요합니다.</p>
<h3 id="동시성-제어-기법">동시성 제어 기법</h3>
<p>경쟁 상태를 막기 위해 사용되는 것이 바로 <strong>동시성 제어 기법</strong>입니다. 대표적으로 사용되는 기법들은 다음과 같습니다:</p>
<ul>
<li><strong>비관적 락(Pessimistic Lock)</strong>: 충돌이 발생할 가능성이 높다고 판단하여 데이터를 사용하는 시점부터 락을 걸어 다른 접근을 차단하는 방식입니다. 주로 <code>SELECT ... FOR UPDATE</code>와 같은 데이터베이스의 락 기능을 사용합니다.</li>
<li><strong>낙관적 락(Optimistic Lock)</strong>: 충돌이 드물다고 가정하고 작업을 먼저 수행한 뒤, 최종 저장 시점에 데이터가 변경되지 않았는지 확인합니다. 변경 감지는 버전 정보나 타임스탬프 등을 활용합니다.</li>
<li><strong>큐 기반 직렬 처리</strong>: 요청을 큐에 저장하고 하나씩 순차적으로 처리하여 순서를 보장하는 방식입니다. 실시간성이 크게 요구되지 않으면서도 동시 처리가 위험한 상황에서 자주 사용됩니다.</li>
<li><strong>분산 락(Distributed Lock)</strong>: 여러 서버나 인스턴스에서 동시에 자원에 접근할 수 있는 환경에서는 Redis, ZooKeeper 등의 외부 시스템을 활용한 분산 락이 필요합니다. 단일 서버 환경에서는 고려하지 않아도 되지만, 마이크로서비스나 클라우드 환경에서는 필수적입니다.</li>
<li><strong>이벤트 기반 비동기 처리</strong>: 요청을 즉시 처리하지 않고 이벤트를 큐에 넣어 비동기로 처리함으로써 자원 접근 시점을 조율하는 방식입니다. 주문 처리나 알림 발송 같은 후처리 작업에서 자주 사용됩니다.</li>
</ul>
<p>동시성 제어는 시스템의 안정성과 직결되는 요소이며, 서비스의 특성, 처리량, 인프라 구조에 따라 적절한 전략을 선택해야 합니다. <strong>각 기법의 특성과 한계를 이해하고 이를 설계에 반영하는 것</strong>이 중요합니다.</p>
<h3 id="분산-락의-개념">분산 락의 개념</h3>
<p>앞서 학습한 비관적 락과 낙관적 락은 데이터베이스를 기반으로 한 동시성 제어 기법입니다. 이들은 단일 데이터베이스 환경에서 매우 효과적이지만, 시스템의 규모가 커지면서 서버와 데이터베이스가 여러 대로 구성되는 분산 환경에서는 한계가 발생합니다.</p>
<p>이때 고려할 수 있는 대안이 바로 <strong>분산 락(Distributed Lock)</strong>입니다.</p>
<p><strong>분산 락</strong>은 여러 서버나 인스턴스가 동시에 하나의 자원에 접근하지 못하도록 제어하는 락입니다. 단일 서버에서는 synchronized 블록이나 데이터베이스 락으로 충분할 수 있지만, 다중 인스턴스 환경에서는 서로 다른 서버가 같은 데이터를 동시에 변경할 수 있기 때문에, 이들 사이에서 <strong>공통으로 사용할 수 있는 락의 기준점</strong>이 필요합니다.</p>
<p>이런 기준점 역할을 하는 도구로는 <strong>Redis, ZooKeeper, Etcd</strong> 등이 있으며, 특히 Redis는 Redisson 라이브러리를 통해 분산 락 구현을 간편하게 할 수 있습니다. 예를 들어, 하나의 재고 데이터를 여러 서버가 동시에 처리할 수 있는 구조라면, 해당 재고에 대한 락을 Redis에 설정하여 하나의 인스턴스만 작업을 수행하도록 만들 수 있습니다.</p>
<p>다음과 같은 상황에서는 분산 락의 도입을 고려할 수 있습니다:</p>
<ul>
<li>여러 서버가 동일한 데이터를 동시에 갱신할 수 있는 구조일 때</li>
<li>중복 처리가 절대 발생해서는 안 되는 중요한 작업이 있을 때 (예: 쿠폰 발급, 재고 차감 등)</li>
<li>이벤트 기반 또는 비동기 처리 환경에서 여러 컨슈머 간 작업 순서를 제어할 필요가 있을 때</li>
</ul>
<p>단, 분산 락은 일반적인 락보다 구현이 복잡하며, 잘못 사용할 경우 병목이나 데드락을 유발할 수 있으므로 사용 여부를 신중하게 판단해야 합니다.</p>
<h3 id="💡-왜-redisson인가">💡 왜 Redisson인가?</h3>
<p>Redisson은 Redis를 기반으로 동작하는 Java 클라이언트 라이브러리로, 단순한 키-값 저장소를 넘어서 분산 락, 세마포어, 큐, 캐시 등 다양한 동기화 도구를 제공합니다. 특히 여러 서버에서 동시에 같은 자원에 접근할 수 있는 분산 환경에서 Redisson은 락의 획득과 해제를 안정적으로 처리할 수 있도록 고수준 API를 제공하여 개발자가 복잡한 동시성 제어 로직을 직접 구현하지 않아도 되도록 도와줍니다.</p>
<h3 id="💡-예제-시나리오">💡 예제 시나리오</h3>
<p><strong>재고가 1개 남은 상품</strong>에 대해 여러 사용자가 동시에 주문할 때, 중복 주문이 발생하지 않도록 분산 락을 활용하여 <strong>동시에 하나의 서버만 재고 차감 작업을 진행</strong>하도록 합니다.</p>
<pre><code class="language-java">@Service
public class OrderService {

    private final RedissonClient redissonClient;
    private final ProductRepository productRepository;

    public OrderService(RedissonClient redissonClient, ProductRepository productRepository) {
        this.redissonClient = redissonClient;
        this.productRepository = productRepository;
    }

    public void placeOrder(Long productId) {
        String lockKey = &quot;lock:product:&quot; + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 락 획득 시도: 최대 3초 대기, 락을 잡으면 5초 후 자동 해제
            if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                Product product = productRepository.findById(productId)
                        .orElseThrow(() -&gt; new IllegalArgumentException(&quot;상품이 존재하지 않습니다.&quot;));

                if (product.getStock() &lt;= 0) {
                    throw new IllegalStateException(&quot;상품이 품절되었습니다.&quot;);
                }

                product.decreaseStock(1); // 재고 차감
                productRepository.save(product);
            } else {
                throw new IllegalStateException(&quot;다른 사용자가 주문을 처리 중입니다. 잠시 후 다시 시도해주세요.&quot;);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(&quot;락 획득 중 인터럽트 발생&quot;, e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // 반드시 락 해제
            }
        }
    }
}</code></pre>
<h3 id="📘-코드-설명">📘 코드 설명</h3>
<ul>
<li><strong><code>lockKey</code></strong>: 상품 ID 기반으로 고유한 락 키를 설정하여, 상품 단위로 락을 분리합니다.</li>
<li><strong><code>tryLock(대기시간, 자동해제시간, 단위)</code></strong>: 락이 해제될 때까지 최대 3초간 기다리고, 락을 획득하면 5초 뒤 자동으로 해제됩니다. 첫 번째 인자는 대기 시간, 두 번째는 락 유지 시간입니다.</li>
<li><strong><code>finally</code> 블록에서의 락 해제</strong>는 필수입니다. 락을 잡은 스레드만 해제할 수 있으므로 조건을 확인해야 합니다.</li>
</ul>
<h3 id="✅-마무리하며">✅ 마무리하며</h3>
<p>Race Condition과 동시성 제어는 모든 웹 서비스에서 한 번쯤은 마주하게 되는 주제입니다. 이번 글에서는 그 개념부터 분산 환경에서의 해결책인 분산 락까지, 실무에서 자주 사용되는 방식들을 중심으로 정리해보았습니다. 특히 Redisson을 활용한 분산 락 구현은 마이크로서비스 환경에서의 실질적인 대안이 될 수 있습니다.</p>
<p>서비스가 커지면 동시성 제어는 선택이 아닌 필수가 됩니다. 상황에 맞는 적절한 방법을 이해하고 활용할 수 있다면, 안정성과 확장성을 모두 갖춘 구조를 만들 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] NCP Object Storage + Global CDN 으로 안전하게 미디어파일 제공하기]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-NCP-Object-Storage-Global-CDN-%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%95%EC%A0%81%ED%8C%8C%EC%9D%BC-%EC%A0%9C%EA%B3%B5%ED%95%98%EA%B8%B0-qntdwg7v</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-NCP-Object-Storage-Global-CDN-%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%95%EC%A0%81%ED%8C%8C%EC%9D%BC-%EC%A0%9C%EA%B3%B5%ED%95%98%EA%B8%B0-qntdwg7v</guid>
            <pubDate>Tue, 21 Jan 2025 00:41:07 GMT</pubDate>
            <description><![CDATA[<h2 id="🎧-정적-파일-보안을-위한-실전-접근법--ncp-object-storage--global-cdn-secure-token-활용기">🎧 정적 파일 보안을 위한 실전 접근법 – NCP Object Storage + Global CDN Secure Token 활용기</h2>
<blockquote>
<p>회사에서 운영 중인 오디오북 서비스에서 <strong>음원 파일의 무분별한 접근을 방지</strong>하고자 한 실전 적용 사례입니다.</p>
</blockquote>
<hr>
<h3 id="✅-왜-원본-파일-보안이-필요할까">✅ 왜 원본 파일 보안이 필요할까?</h3>
<p>정적 파일(이미지, 음원, 영상 등)을 인터넷에 공개할 경우, 사용자 누구나 직접 다운로드하거나 재사용이 가능해집니다. DRM 솔루션을 도입하면 복제와 재생을 제어할 수 있지만, 다음과 같은 단점이 있습니다.</p>
<ul>
<li>💸 <strong>비용</strong>: 상용 DRM 솔루션은 라이선스 비용이 발생</li>
<li>🔧 <strong>복잡성</strong>: 기술 구현 및 플랫폼 연동 난이도가 높음</li>
</ul>
<p>저희 팀은 <strong>오디오북 음원 파일의 무단 접근 문제</strong>를 해결하고자, 비교적 간단하고 효과적인 방법으로 **Naver Cloud Platform(NCP)**의 Object Storage와 Global CDN의 Secure Token 기능을 도입했습니다.</p>
<hr>
<h3 id="🗃️-object-storage를-활용한-원본-파일-저장">🗃️ Object Storage를 활용한 원본 파일 저장</h3>
<p>원본 파일을 안전하게 저장하고 외부 접근을 제어하기 위해, NCP의 Object Storage를 선택했습니다.</p>
<h4 id="📁-버킷-생성-및-권한-설정">📁 버킷 생성 및 권한 설정</h4>
<ul>
<li><p>버킷 생성 시 기본 설정 유지</p>
</li>
<li><p><strong>권한 관리: “공개” 설정</strong></p>
<ul>
<li>버킷 전체는 비공개라도, 개별 파일을 공개하면 접근 가능</li>
<li>그러나 “공개 안함”으로 설정하면 개별 파일이 공개되어도 접근 불가</li>
</ul>
</li>
</ul>
<h4 id="🔍-파일-업로드-및-접근-테스트">🔍 파일 업로드 및 접근 테스트</h4>
<ul>
<li><code>sample-audio.mp3</code> 파일 업로드 후 공개 상태에서 접근 시, 링크만 알면 누구나 다운로드 가능</li>
<li>버킷/파일 권한을 &quot;공개 안함&quot;으로 설정하면 접근 차단됨</li>
</ul>
<blockquote>
<p>✔️ 기본적으로는 &quot;공개 안함&quot; 상태로 파일을 업로드하고,
필요 시 <strong>Secure Token</strong>이나 <strong>Presigned URL</strong>을 통해 제한적 접근을 허용하는 방식이 적절합니다.</p>
</blockquote>
<hr>
<h3 id="🌍-global-cdn--secure-token-적용하기">🌍 Global CDN + Secure Token 적용하기</h3>
<p>NCP에서는 기존 <code>CDN+</code> 대신 <code>Global CDN</code>을 새롭게 제공하고 있으며, 보안 기능으로 <strong>Secure Token</strong> 발급을 지원합니다.</p>
<h4 id="1-cdn-생성하기">1. CDN 생성하기</h4>
<ul>
<li><p><strong>서비스 설정</strong>: 이름 설정, 프로토콜은 <code>ALL</code> 선택</p>
</li>
<li><p><strong>원본 설정</strong>: 앞서 생성한 Object Storage 버킷을 원본으로 지정</p>
</li>
<li><p><strong>캐싱 설정</strong>: 기본값 사용</p>
</li>
<li><p><strong>Viewer 전송 설정</strong>:</p>
<ul>
<li>Secure Token → <code>사용</code></li>
<li>방식 → <code>Query String</code> 선택</li>
</ul>
</li>
</ul>
<h4 id="2-서비스-도메인-확인">2. 서비스 도메인 확인</h4>
<p>CDN이 생성되면 약 10분 내 활성화됩니다. 이후 <code>https://{CDN 도메인}/sample-audio.mp3</code> 형식으로 접근할 수 있습니다.
단, 버킷이 “공개 안함” 상태이므로 기본적으로 접근은 차단됩니다.</p>
<hr>
<h3 id="🔐-secure-token-생성하기">🔐 Secure Token 생성하기</h3>
<p>Secure Token은 다음 조건을 포함한 암호화된 문자열로, 일정 시간 동안만 특정 파일에 접근할 수 있도록 제한합니다.</p>
<ul>
<li><strong>start_time</strong>: 토큰 유효 시작 시간</li>
<li><strong>window_seconds</strong>: 토큰 유지 시간(초)</li>
<li><strong>acl</strong>: 접근 허용 경로</li>
<li><strong>key</strong>: CDN에서 제공하는 보안 키</li>
</ul>
<h4 id="🐍-python으로-토큰-발급하기">🐍 Python으로 토큰 발급하기</h4>
<p>네이버에서 제공하는 Python 예제를 기반으로 토큰을 생성할 수 있습니다.
코드를 실행하면 아래와 같이 토큰이 발급됩니다.</p>
<pre><code class="language-bash">$ python test.py -k [보안키] -n token -s now -w 3600 -a &#39;/sample-audio.mp3*&#39;

# 결과 예시
token=st=1738757016~exp=1738760616~acl=/sample-audio.mp3*~hmac=...</code></pre>
<h4 id="🔗-파일-요청-예시">🔗 파일 요청 예시</h4>
<pre><code class="language-http">https://{CDN도메인}/sample-audio.mp3?token=st=...~exp=...~hmac=...</code></pre>
<p>이 URL로 접근하면, 지정한 시간 동안만 해당 파일을 안전하게 요청할 수 있습니다.</p>
<hr>
<h3 id="🧾-마무리-정리">🧾 마무리 정리</h3>
<ul>
<li><p>정적 파일은 공개 설정만으로는 충분한 보안이 어렵습니다.</p>
</li>
<li><p><strong>Object Storage + Secure Token CDN 방식</strong>은 비교적 간단하면서도 실효적인 파일 보호 수단입니다.</p>
</li>
<li><p>보안을 강화할 필요가 있는 경우,</p>
<ul>
<li><code>Presigned URL</code>, <code>Secure Token</code>, <code>암호화된 파일 업로드</code> 등을 상황에 따라 선택적으로 사용해야 합니다.</li>
</ul>
</li>
<li><p>추가적으로 Java SDK나 AWS S3 호환 기능도 함께 검토해볼 수 있습니다.</p>
</li>
</ul>
<blockquote>
<p>📚 더 자세한 사용 방법은 <a href="https://guide.ncloud-docs.com/docs/ko/home">NCP 공식 가이드</a>를 참고하세요.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] 데이터 조회 성능 향상시키기 3편]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-3%ED%8E%B8</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-3%ED%8E%B8</guid>
            <pubDate>Fri, 20 Dec 2024 04:45:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-2%ED%8E%B8-c96cg854">이전 포스트</a>에서 Redis를 설치하고 Spring boot 프로젝트와 연동하는 방법을 알아보았습니다. 이번 포스트에서는 실제로 Redis를 적용하는 방법과 성능의 차이를 비교해보도록 하겠습니다.</p>
</blockquote>
<h3 id="redis-cache-설정하기">Redis cache 설정하기</h3>
<p>Redis는 key:value형식으로 데이터를 다룹니다. value로 입력되는 값은 직렬화된 형태로 저장됩니다. Redis Config 파일에 직렬화/역직렬화를 위한 cacheManager 설정 부분을 추가하도록 하겠습니다.</p>
<h4 id="redisconfigclass">RedisConfig.class</h4>
<pre><code class="language-java">    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder =
                RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());

        RedisCacheConfiguration configuration =
                RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues()
                .entryTtl(Duration.ofMinutes(30L));

        builder.cacheDefaults(configuration);

        return builder.build();
    }</code></pre>
<p>Java에서는 직렬화/역직렬화를 위해 다양한 <strong>직렬화 규칙</strong>을 적용 할 수 있습니다. (StringRedisSerializer, Jackson2JsonRedisSerializer, <strong>GenericJackson2JsonRedisSerializer</strong> 등)</p>
<p>저는 이중 GenericJackson2JsonRedisSerializer를 사용해보겠습니다. 
GenericJackson2JsonRedisSerializer의 이점은 다른 직/역직렬화 규칙과는 다르게 <strong>@class정보를 value값과 함께 등록하여 Object mapping</strong>하여 사용할 수 있다는 점입니다.</p>
<hr>
<h3 id="cache-annotations">Cache Annotations</h3>
<p>Cache적용에 앞서 캐싱하기 위한 데이터의 형태와 변경이 발생하였을 경우 대응 방안 등 캐싱 전략을 우선적으로 수립하여야합니다.(<a href="https://wnsgml972.github.io/database/2020/12/13/Caching/">캐싱 전략에 대하여 개발자 KimJunHee님의 블로그 글</a>) </p>
<p>이 포스팅은 캐싱으로 인한 조회 성능 개선 비교를 중심으로 작성하기 때문에 캐싱 전략에 대한 설명은 생략하겠습니다. Spring framework을 사용하신다면 <strong>org.springframework.cache.annotation</strong>에서 제공하는 <strong>@Cacheable, @CachePut, @CacheEvict</strong> annotation을 사용하여 간단하게 데이터를 저장하거나 수정, 조회할 수 있습니다.</p>
<h4 id="cacheable">@Cacheable</h4>
<p>메서드의 반환값을 <strong>캐시 저장소에 저장하고 조회</strong>를 위해 사용합니다. <strong>@Cacheable</strong>이 붙어있는 메소드가 실행되면 데이터를 DB에서 조회하는 것이 아닌 <strong>우선 Cache 저장소</strong>(이 경우 Redis)<strong>에 명시된 key값</strong>으로 데이터 존재 유무를 확인합니다.</p>
<ul>
<li><p>만약 <strong>데이터가 존재</strong>할 경우 DB조회 단계는 생략되고 <strong>Cache 저장소의 내용을 반환</strong>하게됩니다.</p>
</li>
<li><p>** 데이터가 존재하지 않을 경우** DB조회 로직을 수행하게되고 반환하는 결과값에 대하여 Cache 저장소에 저장 후 결과값을 반환하게됩니다.</p>
</li>
</ul>
<p>캐싱되어있지 않은 데이터를 조회할 경우 최초 요청에 대해서는 Redis 저장소에 데이터를 등록하는 추가적인 작업이 필요하기 때문에 단순 조회에 비해 성능에서 희생을 필요로하게되지만, 이후 요청에 대해서는 단순 조회보다 월등히 뛰어난 성능을 보여주게 됩니다. </p>
<p>다음은 체육관 전체 목록을 조회하는 로직입니다.</p>
<pre><code class="language-java">@Cacheable(cacheNames = &quot;gyms&quot;, key = &quot;&#39;all&#39;&quot;)
public Gyms findAllGymsWithCache(){
    List&lt;Gym&gt; gyms = gymRepository.findAll();
     return new Gyms(gyms);
}</code></pre>
<p>체육관의 전체 목록에 대한 조회 요청이 있을 경우 우선 Redis 저장소의 gyms::all으로 저장되어있는 데이터를 조회합니다. 
만약 데이터가 존재하지 않을 경우 <code>gymRepository.findAll();</code>이 실행되고 결과값에 대하여 Redis 저장소에 등록, 데이터 반환 과정이 차례대로 진행됩니다.</p>
<p>Test 코드를 통해 위 로직을 실행하고 최초 등록 시 응답에 걸리는 시간과 Redis 저장소에 등록 내용을 확인해보겠습니다.</p>
<pre><code class="language-java">    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class 체육관_조회_테스트{

        @Test
        @Rollback(value = false)
        @DisplayName(&quot;00_벌크 데이터 등록&quot;)
        public void test_a(){
            int gymCount = 30000;
            for (int i = 1; i &lt;= gymCount; i++) {
                Gym gym = new Gym(&quot;test gym&quot; + i);
                repository.save(gym);
                for(int j = 0; j &lt; 2; j++){
                    Member member = new Member(&quot;test member&quot;, gym.getName(), TestValue.IPSUM);
                    memberRepository.save(member);
                }
            }
        }


        @Test
        @DisplayName(&quot;01_체육관 정보 조회&quot;)
        public void test_b(){
            long before = System.currentTimeMillis();

            List&lt;Gym&gt; gyms = service.findAllGyms();

            long after = System.currentTimeMillis();
            long diff = after - before;

            log.debug(&quot;전체 데이터 크기: {}&quot;, gyms.size());
            log.debug(&quot;전체 조회 실행 시간: {}&quot;, diff);
        }

        @Test
        @DisplayName(&quot;02_체육관 정보 조회 캐시적용&quot;)
        public void test_c(){
            long before = System.currentTimeMillis();

            Gyms gyms = service.findAllGymsWithCache();

            long after = System.currentTimeMillis();
            long diff = after - before;
            log.debug(&quot;전체 데이터 수: {}&quot;, gyms.getGyms().size());
            log.debug(&quot;전체 조회 캐시 최초 실행 시간: {}&quot;, diff);
        }

        @Test
        @DisplayName(&quot;03_체육관 전체 정보 캐시조회&quot;)
        public void test_d(){
            long before = System.currentTimeMillis();

            Gyms gyms = service.findAllGymsWithCache();

            long after = System.currentTimeMillis();
            long diff = after - before;
            log.debug(&quot;전체 데이터 수: {}&quot;, gyms.getGyms().size());
            log.debug(&quot;전체 조회 캐시 적용 실행 시간: {}&quot;, diff);
        }

        @Test
        @DisplayName(&quot;04_체육관 전체 정보 캐시조회&quot;)
        public void test_e(){
            long before = System.currentTimeMillis();

            Gyms gyms = service.findAllGymsWithCache();

            long after = System.currentTimeMillis();
            long diff = after - before;
            log.debug(&quot;전체 데이터 수: {}&quot;, gyms.getGyms().size());
            log.debug(&quot;전체 조회 캐시 적용 실행 시간: {}&quot;, diff);
        }
    }</code></pre>
<p>30,000건의 bulk data를 등록 후 <code>일반 조회 -&gt; 캐시 조회 (최초) -&gt; 캐시 조회 2회</code> 순서로 테스트 코드를 실행하고 각 단계별로 조회에 소요된 시간을 기록하여 비교해보도록 하겠습니다.</p>
<h3 id="실행-결과-비교">실행 결과 비교</h3>
<h4 id="일반-조회">일반 조회</h4>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/264d1104-df0f-483c-b376-050423c3380d/image.png" alt=""> redis cache가 적용되지 않은 일반 조회 로직을 실행하였을 때, <strong>30,000개의 데이터</strong>를 조회하는데 <strong>311ms</strong>가 소요되었습니다. </p>
<hr>
<h4 id="redis-cache-조회-최초-실행">Redis cache 조회 최초 실행</h4>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/7f350bb0-cd0f-4038-aeba-2fd52c967089/image.png" alt=""> 다음으로 redis cache가 적용된 로직을 실행한 경우 중 최초 실행하였을때입니다. 실행 시간은 493ms로 앞서 일반 조회 성능보다 180ms이상 성능이 악화되었습니다. 
실행된 hibernate 로그를 확인해보면 데이터 조회하는 SQL이 실행된 것을 볼 수 있습니다.</p>
<pre><code>2025-01-02T13:26:57.107+09:00 DEBUG 43420 --- [board-for-workers] [           main] org.hibernate.SQL                        : select g1_0.gym_id,g1_0.address,g1_0.close_time,g1_0.is_open,g1_0.location,g1_0.name,g1_0.open_time,g1_0.phone_number from gym g1_0
Hibernate: select g1_0.gym_id,g1_0.address,g1_0.close_time,g1_0.is_open,g1_0.location,g1_0.name,g1_0.open_time,g1_0.phone_number from gym g1_0</code></pre><p><strong>cache가 적용된 로직</strong>의 경우 최초 실행 시 명시된 <strong>cache 저장소</strong>에서 요청된 <strong>key::value 형태로 저장되어있는 값이 존재하는지 우선적으로 확인</strong>하게됩니다. <strong>만약 존재하지않다면 SQL을 실행하여 데이터베이스 조회</strong>를 실행하고 사용자에게 응답 전 <strong>cache 저장소에 해당 내용을 기록</strong>하게됩니다. 최초 실행 시 일반 실행보다 더 많은 시간이 소요되는 것은 이 cache 저장소에 최초로 기록하는 작업이 발생하기 때문입니다.
다시 말해 cache는 <strong>최초 요청자에 대한 응답을 희생</strong>하여 <strong>이후 동일한 요청이 있을 때 성능 향상</strong>을 기대하는 것입니다.</p>
<hr>
<h4 id="redis-cache-조회-실행-2회">Redis cache 조회 실행 (2회)</h4>
<p>앞서 이야기한대로라면 이번 조회 실행부터는 일반 조회 혹은 cache 최초 실행에 비해 더 빠른 응답 시간을 기대할 수 있습니다.</p>
<ul>
<li>1회 실행
<img src="https://velog.velcdn.com/images/jw_kim/post/516ec3c2-734c-40c8-9b4a-1a297fa3aa20/image.png" alt=""></li>
<li>2회 실행
<img src="https://velog.velcdn.com/images/jw_kim/post/a76ca531-c437-4a63-bd9c-53ca12eb8da0/image.png" alt=""></li>
</ul>
<p>위와같이 cache 저장소에 값이 입력되어있을 경우 일<strong>반 조회에 비하여 절반 정도의 성능이 향상</strong>된 것을 확인할 수 있습니다. 실제로 데이터 조회를 위해 SQL을 실행하지 않고 Redis cache 저장소의 값을 return하기때문에 hibernate 로그도 찍히지 않았습니다. </p>
<h3 id="마무리-주의-사항">마무리 (주의 사항)</h3>
<p>실제 테스트를 진행한 환경은 단순 30,000건의 데이터를 조회하는 테스트기때문에 폭발적인 성능의 향상까지는 확인할 수 없었습니다. 하지만 실제로 join연산이 동반되거나 수십만건의 데이터를 조회할 경우 cache의 적용만으로도 엄청난 성능 향상이 되는 것을 확인할 수 있습니다.
하지만 이런 <strong>cache 저장소의 적용에 앞서 반드시 주의해야할 내용</strong>이 한가지 있습니다. 바로 c<strong>ache저장소에 저장된 내용에 대하여는 SQL을 실행하지 않기 때문에 발생</strong>하는 문제입니다. </p>
<p>아래의 경우를 생각해보겠습니다.
<code>Gym 전체 목록 조회 요청 (cache/최초) -&gt; SQL 실행 -&gt; 데이터 cache 저장소 등록 -&gt; 사용자에게 응답 -&gt; Gym 목록에서 2개의 데이터 삭제</code>
기존 데이터에 수정이나 삭제 등 변경이 있을 경우 cache 저장소의 데이터에 별도 처리가 없을 경우, 사용자가 동일한 요청을 보냈을 때 SQL이 실행되지 않고 cache 저장소의 값을 응답하기 때문에 사용자는 수정 이전에 데이터를 조회하는 문제가 발생합니다.
때문에 cache 저장소의 적용에 앞서 데이터 동기화 처리를 위한 cache 설계 전략을 수립해야합니다. (예: 데이터 등록/삭제/수정 시 관련 cache 데이터 clear)</p>
<p>이번 포스팅은 이것으로 마무리를 하고 다음 포스트에서는 실제 실무에서 사용하였을 때 어느정도의 성능 차이가 있는지 확인해보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] 데이터 조회 성능 향상시키기 2편]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-2%ED%8E%B8-c96cg854</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-2%ED%8E%B8-c96cg854</guid>
            <pubDate>Wed, 18 Dec 2024 00:43:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-1%ED%8E%B8">이전 포스트</a>에서 Index를 적용하여 데이터 조회 성능을 향상시키는 방법에 대하여 알아보았습니다. 이번 포스트에서는 Redis in-memory cache를 적용하여 조회 성능을 향상시키는 방법에 대해 알아보겠습니다.</p>
</blockquote>
<h3 id="index-적용의-한계">Index 적용의 한계</h3>
<p>데이터베이스의 Index는 데이터 조회 성능을 향상시키기 위해 추가적인 색인 자료구조를 생성하는 방식입니다. 때문에 읽기의 성능을 향상될 수 있으나 쓰기 작업에 대하여는 데이터 이외 색인 쓰기 작업이 추가로 발생하기 때문에 성능 저하가 발생할 수 있습니다. 또한 컴퓨팅 자원 증설의 한계로 일정 시점 이상으로 성능 향상을 기대할 수 없다는 단점이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/03e06aa5-0191-4670-80be-550aa4c173e6/image.png" alt="">(<em>출처:데이터베이스 배움터</em>)</p>
<h3 id="in-memory-cache">in-memory cache</h3>
<p>위와 같이 Index로는 더이상 효과를 보기 어려운 상황이거나 추가적으로 조회 성능에 개선이 필요한 시점이 오게 될 경우, <strong>in-memory cache</strong>의 도입을 검토해 볼 수 있겠습니다. 컴퓨터의 연산 작업은 일반적으로 아래와 같은 절차를 거치게됩니다.</p>
<p><code>비지니스 로직 실행 -&gt; 연산 장치(CPU)의 데이터 요청 -&gt; 주 기억장치 (memory) -&gt; 보조 기억장치(SSD, HDD 등) -&gt; 연산 장치 데이터 전송 -&gt; 결과 반환</code></p>
<p>이 과정에서 연산 장치가 필요로 하는 데이터가 주기억장치에 존재한다면 보조 기억장치로의 데이터 요청 작업이 생략되고 연산이 수행되며 결과를 반환하게 됩니다. 하지만 <strong>주기억장치에 데이터가 존재하지 않을 경우</strong> 이를 <strong>보조기억장치에 요청</strong>하게되고 주기억장치와 보조기억장치간의 <strong>데이터 교환 비용</strong>이 발생하게 됩니다.(연산 과정에서 가장 많은 비용 발생)</p>
<p>in-memory cache는 연산에 필요한 데이터 혹은 연산의 결과값을 주기억장치에 저장하여 보조 기억장치로의 데이터 요청 작업을 생략할 수 있게 도와줍니다.</p>
<p>주로 사용되는 in-memory cache에는 Ehcache, Redis, Memcached등이 있습니다.</p>
<hr>
<h3 id="redis">Redis</h3>
<h4 id="in-memory-cache의-종류">in-memory cache의 종류</h4>
<p>Spring framework을 사용하고 있다면 간단하게 적용 가능한 Ehcache가 유효한 선택지가 될 수 있습니다. (<a href="https://jojoldu.tistory.com/57">이동욱 개발자님의 ehcache 예제 포스팅</a>) 다만 프레임워크 라이브러리로 제공되는 기능이다보니 cli를 활용한 데이터 조회나 외부 서비스 연동에 어려움이 있습니다. 하여 이번 포스팅에서는 범용적으로 사용 가능한 <strong>Redis</strong>를 예로 들어 작성해보겠습니다.</p>
<h4 id="redis-설치-macos">Redis 설치 (macOS)</h4>
<p>맥 PC의 경우 <a href="https://brew.sh/ko/">brew 패키지 관리자</a>를 사용하면 간단하게 Redis를 설치할 수 있습니다.
터미널을 실행 후 <code>brew install redis</code> 명령어를 입력하면 Redis가 설치됩니다. (설치 완료 후 <code>redis-server --version</code>로 확인할 수 있습니다.)</p>
<p>설치가 완료되었다면 redis를 실행해보겠습니다.
<img src="https://velog.velcdn.com/images/jw_kim/post/dfd6a02b-9713-421a-95e7-592f733d7324/image.png" alt="">현재는 <strong>foregraund로</strong> 실행된 상태이기때문에 터미널을 종료하면 redis도 함께 종료가 됩니다. brew의 프로세스 관리 명령어를 사용하여 <strong>background로 실행</strong> 하는 방법을 알아보겠습니다.
<img src="https://velog.velcdn.com/images/jw_kim/post/0494c757-141f-4239-ac66-45bd39da8bbe/image.png" alt=""></p>
<p>redis가 background모드로 실행되고있는 것을 확인할 수 있습니다. (프로세스 종료 명령어 <code>brew services stop redis</code>) Redis가 실행중이라면 cli를 활용하여 접속할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/7bfd0b74-711f-49e5-844b-173b40d6e5ef/image.png" alt=""> redis는 기본적으로 6379번 port에서 실행됩니다.</p>
<hr>
<h3 id="spring-project-연동">Spring project 연동</h3>
<p>앞서 Redis를 설치하고 실행하는 방법에 대해 알아보았습니다. 이제 실행된 Redis를 Spring boot 프로젝트와 연동하여 데이터를 등록하거나 조회, 수정하는 방법을 알아보겠습니다.</p>
<blockquote>
<p>프로젝트 환경</p>
</blockquote>
<ul>
<li>OS: macOS</li>
<li>JDK: openJDK 17</li>
<li>Spring version: spring boot 3.x</li>
<li>Database: H2 database</li>
</ul>
<h4 id="spring-boot-redis-환경-설정">spring-boot redis 환경 설정</h4>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/4b31bd5b-6d0e-4d4b-a5fe-f68a7d8e0120/image.png" alt="">spring boot와 redis를 연동하기 위해 build.gradle &gt; dependencies에
<code>implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code> 를 추가하였습니다.</p>
<p>다음으로 application.yml(application.properties)에 연결할 redis의 port와 host를 명시해주겠습니다.
<img src="https://velog.velcdn.com/images/jw_kim/post/a46d37d0-629a-4803-a525-55af8565dc40/image.png" alt=""> 로컬 환경에서의 연동 테스를 위한 프로젝트이기 때문에 접속 password는 설정하지않았습니다.(상용 환경에서이 도입을 원할 경우 반드시 password를 설정해주세요.)</p>
<h4 id="redisconfigclass">RedisConfig.class</h4>
<p>다음으로 redis bean 생성을 위한 class 파일을 추가해보겠습니다.</p>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Value(&quot;${spring.data.redis.host}&quot;)
    private String host;

    @Value(&quot;${spring.data.redis.port}&quot;)
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate() {
        RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        return redisTemplate;
    }

}</code></pre>
<p>java에서 제공하는 Redis client에는 Jedis와 Lettuce 두 종류가 있습니다. 저는 Lettuce를 선택하여 사용하도록 하겠습니다.(<a href="https://jojoldu.tistory.com/418">이동욱 개발자님의 Jedis 보다 Lettuce 를 쓰자</a>)</p>
<h4 id="redis-test">Redis test</h4>
<p>Redis client를 사용하여 데이터를 입력 / 조회하는 테스트 코드를 작성해보겠습니다.</p>
<pre><code class="language-java">@SpringBootTest
public class RedisServiceTest {

    @Autowired
    private StringRedisTemplate redisTemplate;


    @Test
    @Transactional
    public void redis_key_insert_test(){
        String key = &quot;test-redis-key-01&quot;;
        String value = &quot;test-redis-value-01&quot;;

        final ValueOperations&lt;String, String&gt; stringStringValueOperations = redisTemplate.opsForValue();
        stringStringValueOperations.set(key, value);

        String redisValue = redisTemplate.opsForValue().get(key);

        Assertions.assertEquals(value, redisValue);
    }
}</code></pre>
<p>&quot;test-redis-key-01&quot;:&quot;test-redis-value-01&quot;과 같이 key:value를 입력하고 입력된 데이터를 key값으로 조회하여 일치 여부를 검증하는 테스트 코드입니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/2cc8fb2a-1896-471b-9f3b-ecfdf181f66d/image.png" alt="">테스트가 정상적으로 실행되었습니다. 이제 redis-cli로 실제 입력된 데이터를 확인해보겠습니다.
<img src="https://velog.velcdn.com/images/jw_kim/post/ab16d35a-8aee-4e7e-a6cb-105f9a897574/image.png" alt="">
테스트 코드를 통하여 입력하였던 key:value가 등록되어있습니다.</p>
<hr>
<h3 id="마무리">마무리</h3>
<p>실제 조회 로직에 redis를 적용하기 위해서는 @CachePut, @Cacheable, @CacheEvict을 사용하여 캐싱 전략을 결정하여야합니다. 이번 포스팅에서는 기본적인 Redis의 설치와 사용법을 알아보았다면, 다음 포스팅에서는 실제 데이터를 조회하고 변경된 데이터에 대하여 캐시를 갱신하는 등의 작업과 redis적용 이전과 이후의 성능 차이를 비교해보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] 데이터 조회 성능 향상시키기 1편]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-1%ED%8E%B8</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0-1%ED%8E%B8</guid>
            <pubDate>Thu, 12 Dec 2024 11:29:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>데이터의 조회 성능을 향상시키는 방법은 데이터베이스의 종류에 따라 또 데이터의 규모에 따라 다양합니다. 이번 포스트로 알아볼 것은 데이터 조회 성능을 향상시킬 수 있는 가장 기본적인 index 적용에 대하여 알아보고 실제 벌크 데이터를 활용하여 성능을 비교해보겠습니다.</p>
</blockquote>
<h3 id="인덱스란">인덱스란?</h3>
<p>흔히 index라는 단어는 사전이나 두꺼운 책의 맨 뒷부분에서 찾아볼 수 있습니다. 읽는이로 하여금 찾아보고싶은 키워드가 몇 페이지에 있는지 빠르게 찾아볼 수 있도록 만들어둔 별도의 페이지를 인덱스라합니다.</p>
<blockquote>
</blockquote>
<p><em>데이터베이스 분야에 있어서 <strong>테이블에 대한 동작의 속도를 높여주는 자료 구조</strong>를 일컫는다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작뿐만 아니라 레코드 접근과 관련 효율적인 순서 매김 동작에 대한 기초를 제공한다. 인덱스를 저장하는 데 필요한 디스크 공간은 보통 테이블을 저장하는 데 필요한 디스크 공간보다 작다. (왜냐하면 보통 인덱스는 키-필드만 갖고 있고, 테이블의 다른 세부 항목들은 갖고 있지 않기 때문이다.) 관계형 데이터베이스에서는 인덱스는 테이블 부분에 대한 하나의 사본이다.</em> 
<a href="https://ko.wikipedia.org/wiki/%EC%9D%B8%EB%8D%B1%EC%8A%A4_(%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4)">위키피디아(인덱스)</a></p>
<p>대부분의 관계형 데이터베이스는 인덱스 기능을 제공합니다. Mysql, Oracle, Postgresql은 물론 이번 포스팅에서 사용할 H2 데이터베이스 역시 인덱스 기능을 지원합니다.</p>
<pre><code class="language-bash">[Mysql]
SHOW INDEX FROM tablename; ## Table 내 인덱스 조회
ALTER TABLE tablename ADD INDEX indexname (column1, column2 ...); ## Table에 새로운 인덱스 추가</code></pre>
<h3 id="성능-조회-테스트">성능 조회 테스트</h3>
<p>성능 조회는 테스트를 위하여 Gym과 Member 테이블을 만들었습니다.</p>
<pre><code class="language-sql">
create table Gym (
    gym_id            bigint primary key,      ## 8byte
    name             varchar(255),            ## 255byte
    location        varchar(255),            ## 255byte
    address            varchar(255),            ## 255byte
    phone_number    varchar(255),            ## 255byte
    is_open            tinyint,                ## 1byte
    open_time        timestemp,                ## 4byte
    close_time        timestemp                ## 4byte
); ## 총 1037byte

create table Member (
    member_id        bigint primary key,        ## 8byte
    real_name        varchar(255),            ## 255byte
    gym_name        varchar(255),            ## 255byte
    desc            varchar(512),            ## 512byte
    test_str1        varchar(255),            ## 255byte
    test_str2        varchar(255),            ## 255byte
    test_str3        varchar(255),            ## 255byte
    append_date        timestemp,                ## 4byte
    update_date        timestemp                ## 4byte

); ## 총 1803 byte</code></pre>
<p>Gym 테이블에는 15000개의 데이터가, Member 테이블에는 30000개의 데이터가 들어가있습니다. 
데이터 조회를 위한 Spring 프로젝트를 생성하여 전체 조회에 소요되는 시간을 계산해보았습니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/ec88f45b-26b1-4da8-8eb8-94412386f278/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/cab22028-42f7-4d40-9cd2-b6df8588e0cc/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>(단위:ms)</td>
<td></td>
</tr>
</tbody></table>
<p>특정 컬럼에 인덱스를 추가하여 where 연산으로 성능을 측정할수도 있지만, join인 연산을 활용하여 member 테이블과 gym 테이블이 조인된 30000개의 데이터를 조회하는 것으로 성능 테스트를 진행해보겠습니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/95ceae07-56ea-4835-acbd-bf89c305dfaf/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/fa14872c-1ce4-461b-a2d1-e7b8685b0a88/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>조인된 30000개의 데이터를 조회하는데 무려 <strong>19221ms(19sec)</strong>의 시간이 소요되었습니다. 어떤 웹 페이지에서 데이터 목록을 조회하는데 19초동안이나 응답이 없다는 것은 말도 안되는 응답 시간입니다.</p>
<h4 id="인덱스-추가-후-성능-테스트">인덱스 추가 후 성능 테스트</h4>
<p>이제 join 조건이 되는 Gym 테이블의 name 컬럼에 인덱스를 추가해주겠습니다.</p>
<pre><code class="language-sql">[H2]
create index gym_name on gym(name);
</code></pre>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/80f2a559-020e-4e8b-9334-e502ccd1923f/image.png" alt=""></p>
<p>이제 앞서 테스트했던 join 연산과 동일한 로직을 실행하여 실행 시간을 비교해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/84aaba25-e132-4fe6-83b9-e5ac7a77edca/image.png" alt=""></p>
<p>그 결과 연산 수행 시간이 <strong>19221ms -&gt; 1792ms</strong>로 10배 이상의 성능 향상이 된 결과를 볼 수 있습니다.</p>
<p>이처럼 데이터베이스에 적절한 인덱스를 추가하는 간단한 작업만으로도 성능을 폭발적으로 향상 시킬 수 있습니다. 실제 제가 있는 회사에서는 방대한 데이터로 인하여 join 연산 수행 중에 결과값을 얻지 못하고 쿼리가 죽어버리는 경우가 있었습니다. 문제의 테이블에는 join 연산에 사용되는 컬럼에 어떠한 인덱스도 추가되어있지 않아 간단하게 인덱스만을 추가하는 작업으로 수행 시간을 평균 900ms까지 줄일 수 있었습니다.</p>
<p>이처럼 만약 실무에서 쿼리가 실행되는 데 과도하게 시간이 많이 걸린다면 연관된 테이블을 확인하여 인덱스를 추가하는 방안을 고려해 볼 수 있겠습니다.</p>
<p>다음 포스트로는 인덱스만으로는 더 이상 조회 성능의 향상을 기대하기 어려울 때 고려해 볼 수 있는 캐시 메모리의 적용을 redis적용 예제를 진행하며 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 🚀 우리 조직의 Docker 전환기: 개발 환경을 컨테이너로 갈아탄 이유와 기록-3]]></title>
            <link>https://velog.io/@jw_kim/docker-docker%EB%A1%9C-%ED%9A%8C%EC%82%AC-%EA%B0%9C%EB%B0%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-03-Dockerfile-Image-build</link>
            <guid>https://velog.io/@jw_kim/docker-docker%EB%A1%9C-%ED%9A%8C%EC%82%AC-%EA%B0%9C%EB%B0%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-03-Dockerfile-Image-build</guid>
            <pubDate>Wed, 13 Nov 2024 06:52:02 GMT</pubDate>
            <description><![CDATA[<h2 id="🧱-나만의-docker-이미지-만들기-dockerfile로-컨테이너-자동화하기">🧱 나만의 Docker 이미지 만들기: Dockerfile로 컨테이너 자동화하기</h2>
<p>앞선 포스트에서는 Docker의 기본 사용법과 컨테이너 실행 과정을 살펴봤습니다.<br>그 과정에서 필요한 컨테이너는 <code>docker pull</code> 명령어를 통해 Docker Hub에서 이미지로 내려받아 사용했죠.  </p>
<p>그렇다면, 만약 <strong>우리 회사에서 직접 운영 중인 서비스를 이미지로 만들 수 있다면?</strong><br>트래픽 급증, 인프라 이전, 테스트 환경 분리 등 다양한 상황에서 이 이미지를 통해 빠르게 새로운 컨테이너를 띄울 수 있을 겁니다.  </p>
<p>이를 가능하게 해주는 도구가 바로 <code>Dockerfile</code>입니다.</p>
<hr>
<h3 id="📄-dockerfile이란">📄 Dockerfile이란?</h3>
<p><code>Dockerfile</code>은 Docker 이미지를 만들기 위한 <strong>스크립트 파일</strong>입니다.<br>보통 프로젝트 루트 디렉토리에 위치하며, 여기에 작성한 명령어들을 기반으로 Docker는 자동으로 이미지를 생성합니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/67f53b42-4241-4035-aef2-7c14bd4e82a5/image.png" alt=""></p>
</blockquote>
<p>이미지를 만들 때는 <code>docker build</code> 명령어를 사용하며, 이렇게 만들어진 이미지로 원하는 컨테이너를 자유롭게 생성할 수 있습니다.</p>
<hr>
<h3 id="✍-dockerfile-작성-예시">✍ Dockerfile 작성 예시</h3>
<p>앞서 실습했던 Ubuntu 22.04 + Nginx 환경을 Dockerfile로 정의해보겠습니다.</p>
<pre><code class="language-dockerfile"># 베이스 이미지
FROM ubuntu:22.04
MAINTAINER jw.kim &lt;jongbell4@gmail.com&gt;

# 비대화 모드 설정 (패키지 설치 시 사용자 입력 방지)
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Seoul

# 패키지 설치
RUN apt-get update &amp;&amp; apt-get install -y tzdata
RUN apt-get install -y nginx

# HTML 파일 복사
COPY index.html /var/www/html/index.html

# 외부 포트 지정 (명시적 선언)
EXPOSE 80/tcp

# 컨테이너 시작 시 실행될 명령어
CMD service nginx start &amp;&amp; tail -f /dev/null</code></pre>
<hr>
<h3 id="🔍-주요-dockerfile-명령어-정리">🔍 주요 Dockerfile 명령어 정리</h3>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>FROM</code></td>
<td>사용할 베이스 이미지 지정 (ex. <code>ubuntu:22.04</code>)</td>
</tr>
<tr>
<td><code>MAINTAINER</code></td>
<td>이미지 작성자 정보 (선택)</td>
</tr>
<tr>
<td><code>ARG</code></td>
<td>이미지 <strong>빌드 시점</strong>에 사용되는 변수 정의</td>
</tr>
<tr>
<td><code>ENV</code></td>
<td>이미지/컨테이너 내에서 사용할 환경변수 설정</td>
</tr>
<tr>
<td><code>RUN</code></td>
<td>컨테이너 생성 시 실행할 명령어</td>
</tr>
<tr>
<td><code>COPY</code>, <code>ADD</code></td>
<td>호스트의 파일/디렉토리를 이미지로 복사</td>
</tr>
<tr>
<td><code>EXPOSE</code></td>
<td>컨테이너가 사용하는 포트를 명시 (실제 포트 열림은 아님)</td>
</tr>
<tr>
<td><code>CMD</code></td>
<td>컨테이너 실행 시 기본적으로 실행할 명령어</td>
</tr>
</tbody></table>
<p>💡 참고:</p>
<ul>
<li><code>ARG</code>는 build 시점에만 유효하고,</li>
<li><code>ENV</code>는 실행 중에도 적용되며 <code>ARG</code>보다 우선 적용됩니다.</li>
</ul>
<hr>
<h3 id="🔨-이미지-빌드--컨테이너-실행">🔨 이미지 빌드 &amp; 컨테이너 실행</h3>
<pre><code class="language-bash"># 프로젝트 디렉토리 구조 (예시)
$ tree ubuntu-image
ubuntu-image
├── Dockerfile
└── index.html

# 이미지 목록 확인
$ docker images

# Dockerfile 기반 이미지 빌드
$ docker build -t ubuntu-nginx .

# 빌드 후 이미지 확인
$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
ubuntu-nginx   latest    4fb8xxxxxxx     3 minutes ago   ...</code></pre>
<hr>
<h3 id="🚀-컨테이너-실행--테스트">🚀 컨테이너 실행 &amp; 테스트</h3>
<p>이제 새로 빌드한 이미지를 기반으로 컨테이너를 실행하고, 포트 포워딩 설정을 추가해 웹 요청을 테스트해보겠습니다.</p>
<pre><code class="language-bash"># 컨테이너 실행 (호스트 80포트를 컨테이너 80포트에 연결)
$ docker run -itd --name container-unginx -p 80:80 ubuntu-nginx

# 실행 중인 컨테이너 확인
$ docker ps

# 컨테이너 IP 확인
$ docker inspect container-unginx | grep IPAddress
&quot;IPAddress&quot;: &quot;172.17.0.10&quot;,

# 컨테이너에 HTTP 요청
$ curl http://172.17.0.10
&lt;h1&gt; hello this is ubuntu-nginx image container-unginx&lt;/h1&gt;</code></pre>
<p>🎉 Dockerfile 기반으로 만든 이미지가 정상적으로 실행되고, 웹 요청도 잘 처리되고 있습니다!</p>
<hr>
<h3 id="✅-마무리">✅ 마무리</h3>
<p>이번 글에서는 <code>Dockerfile</code>을 작성하고 직접 이미지를 빌드한 뒤, 이를 기반으로 컨테이너를 실행해보는 과정을 다뤘습니다.</p>
<p>사실 이 예시는 매우 단순한 형태이고, 실무에서는 <code>컨테이너 하나에 하나의 프로세스만 구동하는 방식</code>이 권장됩니다.
예:</p>
<ul>
<li>Nginx 컨테이너</li>
<li>API 컨테이너</li>
<li>Jenkins 컨테이너 등</li>
</ul>
<p>저희 회사에서도 실제로는 Ubuntu, Rocky Linux 일부를 제외하면 대부분의 컨테이너는 하나의 역할만 수행하도록 구성하고 있습니다.</p>
<p>다음 시리즈에서는 네트워크 설정, 스토리리 설정 등 Docker를 조금 더 효율적으로 사용하는 심화 기능들에 대해 추가적으로 알아보도록하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기타] Nginx로 로드밸런싱 설정하기]]></title>
            <link>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-Nginx%EB%A1%9C-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-lcxmjp0a</link>
            <guid>https://velog.io/@jw_kim/%EA%B8%B0%ED%83%80-Nginx%EB%A1%9C-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-lcxmjp0a</guid>
            <pubDate>Wed, 23 Oct 2024 06:59:19 GMT</pubDate>
            <description><![CDATA[<p> Nginx에서 제공하는 upstream 설정을 활용하여 웹 서버에 가해지는 부하를 분산하는 방법을 알아보겠습니다.</p>
<h2 id="로드-밸런싱">로드 밸런싱</h2>
<blockquote>
<p><strong>로드 밸런싱</strong>은 애플리케이션을 지원하는 <strong>리소스 풀 전체에 네트워크 트래픽을 균등하게 배포</strong>하는 방법입니다. - <a href="https://aws.amazon.com/ko/what-is/load-balancing/">AWS(로드 밸런싱이란 무엇인가요?)</a></p>
</blockquote>
<h3 id="단일-서버-구성-문제점">단일 서버 구성 문제점</h3>
<p>웹 서비스를 이용하는 사용자가 늘어날 수록 서버에 가해지는 트래픽이 증가하게됩니다. 서버가 감당할 수 없는 과도한 트래픽이 몰리게 된다면 장애가 발생하여  서비스 이용이 불가능하게 될 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/20080433-cabb-428f-a59a-adfaf5f6d1d1/image.png" alt=""></p>
<p>서버를 재부팅하게 된다면 인입되던 트래픽이 초기화되어 잠시나마 정상적으로 서비스가 이용 가능한 것처럼 보이겠지만, 이내 트래픽이 증가하면 동일한 장애가 발생하게 됩니다. 결국 장애를 해결하기 위해서는 인입되는 트래픽이 수용 가능한 정도의 <strong>PC자원을 증설</strong>하는 방식을 고려해보아야 합니다.</p>
<h3 id="scale-out과-scale-up">scale-out과 scale-up</h3>
<p> 사용 가능한 PC자원을 증설하는 방법은 <strong>Scale-out</strong> 과 <strong>Scale-up</strong> 두 가지 방식을 고려해 볼 수 있습니다. 그림과 함께 두 방식이 어떻게 차이가 나는지 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/7c19fba0-49b2-4731-9523-36e3e9e2606d/image.png" alt=""></p>
<ul>
<li><p><strong>Scale-out</strong>은 기존 서버와 동일한 스펙의 <strong>새로운 컴퓨팅 자원을 추가</strong>하는 방식입니다. 클라우드 컴퓨팅 서비스를 이용할 경우 사용 가능한 비용 안에서 무한대로 자원을 증설 할 수 있다는 장점이 있습니다. 하지만 새로운 PC 자원이 추가되는 방식인 만큼 서비스로 인입되는 트래픽을 어떤 방식으로 분산시켜줄 지에 대한 추가적인 작업이 필요합니다.</p>
</li>
<li><p><strong>Scale-up</strong>은 기존 서버의 스펙을 상향시켜 적용하는 방식입니다. 이 방식의 경우 기존 단일 서버로 설계되었을 때와 구조상 차이는 존재하지 않기 때문에 자원 운영 측면에서는 out방식보다 간단하지만 컴퓨팅 자원을 무한대로 추가할 수 없는 한계점이 존재하며, 이후 트래픽이 줄어든 상황이 올 경우 필요 이상의 자원 사용료를 지불해야 한다는 문제점이 있습니다.</p>
</li>
</ul>
<p>때문에 PC자원을 증설할 때에는 트래픽의 변화에 유동적으로 대응할 필요가 있다면 scale-out방식이 더욱 적절한 선택지가 될 수 있습니다. 그렇다면 scale-out으로 자원이 증설하였을 때 고려해야하는 사항에 대해 추가적으로 알아보겠습니다.</p>
<h3 id="scale-out과-트래픽-관리">scale-out과 트래픽 관리</h3>
<p> 물리적으로 서버 자원을 추가하였기 때문에 기존과 동일한 트래픽에 대하여 처리 능력이 향상되었습니다. 하지만 서버 자원이 추가되며 각각의 서버는 고유의 네트워크 인터페이스를 갖게됩니다. </p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/c49ca4e9-1f22-450f-9438-c1795b38904d/image.png" alt=""></p>
<p>이 경우 사용자 단말과 서버 사이에서 요청된 트래픽을 적절한 서버로 분배시켜줄 필요가 있습니다.(만약 트래픽이 분배되지 않으면, 기존 구조와 동일하게 단일 서버로 트래픽이 몰리기 때문에 scale-out의 효과를 볼 수 없게됩니다.)
이러한 트래픽 분산의 역할을 하는 하드웨어 혹은 소프트웨어를 LB(Load Balancer, 로드 밸런서)라고 합니다.</p>
<h2 id="로드-밸런서">로드 밸런서</h2>
<p> 네트워크 계층을 이야기할 때 거론되는 대표적인 모델인 OSI 7계층 모델이 있다. 로드벨런서는 L2(데이터링크), L3(네트워크), <strong>L4(전송), L7(애플리케이션)</strong> 계층에 적용할 수 있습니다. 이중 가장 범용으로 적용되는 L4계층과 L7계층의 로드밸런서에 대하여 알아보겠습니다.</p>
<h3 id="l4--l7-로드-밸런서">L4 / L7 로드 밸런서</h3>
<blockquote>
<p> <strong>L4 Layer (Transport Layer, 전송 계층)</strong>는 TCP / UDP 프로토콜을 사용하여 종단간 통신을 제어합니다. 
TCP 프로토콜의 IP 주소와 Header의 Port 정보를 활용하여 목적지를 확정하여 로드 밸런싱 역할을 수행할 수 있습니다.<br>
예) </p>
</blockquote>
<ul>
<li>192.168.10.201:8080 --&gt; 192.168.17.221</li>
<li>192.168.10.201:9090 --&gt; 192.168.17.222</li>
</ul>
<blockquote>
<p> <strong>L7 Layer (Application Layer, 응용 계층)</strong>는 HTTP / HTTPS 프로토콜을 사용하여 통신을 제아할 수 있습니다.
L4 Layer에서 참조 가능한 정보에 비하여 보다 세밀한 로드밸런싱이 가능합니다.<br>
예)</p>
</blockquote>
<ul>
<li>192.168.10.201/service-a --&gt; 192.168.17.221</li>
<li>192.168.10.201/service-b --&gt; 192.168.17.222</li>
</ul>
<h3 id="nginx를-활용한-로드-밸런싱-예제">nginx를 활용한 로드 밸런싱 예제</h3>
<p>앞서 알아본 바와 같이 TCP/IP 정보만을 활용하여 nginx L4 로드 밸런서를 설정해보도록 하겠습니다. 실습 환경은 다음과 같이 virtualBox를 활용하여 구성하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/70bf7940-ad9e-409f-bafd-80ffe05a3c49/image.png" alt=""></p>
<p>Host PC는 Load Balancer 역할을 하는 192.168.137.143으로만 요청을 전송하게됩니다. 요청을 받은 LB는 upstream으로 설정된 server 01과 server 02에 RR(Round Robin, 라운드로빈) 방식으로 응답을 받아 보여줍니다.</p>
<h4 id="실습-환경-구축">실습 환경 구축</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/4971762c-f7e7-4bdf-94cf-05007acbf62e/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/jw_kim/post/f3105a5b-c10a-4a2e-8724-5ab83b53e2e0/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>virtualBox로 로드밸런싱의 대상이 되는 두 개의 인스턴스를 생성하였습니다. 각자 고유의 내부 IP (192.168.137.141~142)를 할당 받았습니다. 
또한 각각 nginx를 설치/실행하였고 index page 응답 시 각각 서버의 정보를 응답하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/986b97f8-a2cc-4ab5-8678-b3ce6b6764cf/image.png" alt=""> load balancer(192.168.137.143) 역할을 하게 될 서버에서 각각의 웹 서버로 curl request에 대한 결과입니다. 응답 내용을 통하여 VM간 내부 IP를 통하여 정상적으로 통신하는 것을 확인하였습니다.</p>
<h4 id="load-balancer-설정">Load Balancer 설정</h4>
<p>이제 앞서 137.143주소의 Load balancer 서버에 nginx를 설치하여 들어오는 요청을 RR방식으로 각각 137.141, 137.142서버로 트래픽이 분배되도록 하여봅시다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/91d0e334-442b-44e1-8430-58b9aca34253/image.png" alt="">virtual BOX의 포트포워딩 설정으로 <strong>127.0.0.1:11080으로 접속 시</strong> <strong>load balancer서버의 80포트로 포워딩</strong>되도록 설정하였습니다.</p>
<p>nginx는 간단한 설정 파일 수정으로 로드밸런싱 설정을 할 수 있습니다.
/etc/nginx/conf.d 디렉토리 하위에 default.conf 파일을 새로 만들어(기존에 존재하는 경우 텍스트 추가) 아래와 같이 작성하였습니다.
<img src="https://velog.velcdn.com/images/jw_kim/post/e3858024-7dbc-4b5a-aa21-6fd22bb282f9/image.png" alt=""></p>
<ul>
<li><p><strong>upstream:</strong> 로드 밸런싱 대상이 될 서버의 목록을 명시합니다. 아래와 같은 형식으로 작성할 수 있습니다.</p>
<pre><code class="language-bash">  upstream &lt;alias&gt; {
      server {target IP or Domain}:{port};
              ....
  }</code></pre>
</li>
<li><p><strong>server:</strong> 실제로 요청을 받을 포트와 서버 이름, url을 식별하여 proxy_pass 설정등을 명시할 수 있습니다.</p>
</li>
</ul>
<h4 id="동작-테스트">동작 테스트</h4>
<p>이제 모든 VM을 실행하고 Host PC의 브라우저를 통해 localhost(127.0.0.1):11080/ 으로 요청을 보내겠습니다. 로드 밸런싱 방식은 default(Round Robin 방식)으로 설정되어있기 때문에 매 요청바다 서로 다른 서버의 응답을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/59a8936a-30de-4455-872c-20fac519ebcc/image.gif" alt=""></p>
<p>정상적으로 동작하는 것을 확인할 수 있었습니다.</p>
<h3 id="마무리">마무리</h3>
<p>로드 밸런싱을 적용하기 위해서는 타겟이 되는 서버들의 <strong>health-check</strong>가 필수적입니다. 고가용성과 성능을 얻기 위해 적용하는 방식인 만큼, 타겟 서버가 응답하지 않아 서비스 이용이 어려워지는 사태를 방지해야하기 때문입니다.</p>
<p>다만 기본 nginx에서 제공하는 로드 밸런싱 기능에서는 타겟 서버의 health-check기능을 지원하지 않습니다.(유료 기능)
때문에 nginx 무료 버젼을 사용하여 로드 밸런싱 설정을 생각하고 있을 경우 <a href="https://github.com/yaoweibin/nginx_upstream_check_module.git">오픈 소스 nginx health check 모듈</a>의 적용 혹은 health-check기능을 제공하는 다른 오픈소스 로드 밸런서를 고려할 필요가 있습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 🚀 우리 조직의 Docker 전환기: 개발 환경을 컨테이너로 갈아탄 이유와 기록-2]]></title>
            <link>https://velog.io/@jw_kim/docker-docker%EB%A1%9C-%ED%9A%8C%EC%82%AC-%EA%B0%9C%EB%B0%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-02-docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jw_kim/docker-docker%EB%A1%9C-%ED%9A%8C%EC%82%AC-%EA%B0%9C%EB%B0%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-02-docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 Sep 2024 01:49:56 GMT</pubDate>
            <description><![CDATA[<h2 id="🧪-docker-실습-시작하기-우분투-컨테이너-만들고-접속해보기">🧪 Docker 실습 시작하기: 우분투 컨테이너 만들고 접속해보기</h2>
<p>이전 포스트에서는 Docker Host로 사용할 서버를 구성하고, Docker 엔진을 설치해 실행해보았습니다.<br>이번 글에서는 그 위에 <strong>새로운 Ubuntu 컨테이너를 생성하고</strong>, <strong>명령어 실행 및 SSH 접속 실습까지</strong> 단계별로 정리해보겠습니다.</p>
<hr>
<h3 id="📦-docker-hub--이미지란">📦 Docker Hub &amp; 이미지란?</h3>
<p>Docker 컨테이너는 사전에 빌드된 <strong>이미지(image)</strong>를 바탕으로 생성됩니다.<br>이 이미지들은 <a href="https://hub.docker.com/_/ubuntu"><strong>Docker Hub</strong></a>라는 공개 저장소에서 관리되며, 누구나 다운로드하거나 자신이 만든 이미지를 공유할 수 있습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/66a8a368-2bd4-4c89-aafb-fe2d646090d7/image.png" alt=""></p>
</blockquote>
<p>이미지마다 Ubuntu 버전이 명시되어 있는 것을 볼 수 있습니다.<br>사용 가능한 이미지 목록은 다음 명령어로 확인할 수 있습니다.</p>
<pre><code class="language-bash">$ docker images</code></pre>
<p>처음 설치한 Docker 환경에서는 아무 이미지도 없기 때문에 빈 결과가 출력될 것입니다.</p>
<hr>
<h3 id="📥-docker-이미지-다운로드-docker-pull">📥 Docker 이미지 다운로드: <code>docker pull</code></h3>
<p>이제 <code>docker pull</code> 명령어를 사용해 Ubuntu 22.04 이미지를 받아보겠습니다.</p>
<pre><code class="language-bash">$ docker pull ubuntu:22.04</code></pre>
<p>태그를 생략하면 기본적으로 <code>latest</code> 버전을 다운로드합니다.</p>
<pre><code class="language-bash">$ docker pull ubuntu</code></pre>
<p>다시 이미지 목록을 확인해봅니다.</p>
<pre><code class="language-bash">$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
ubuntu       latest    b1e9cef3f297   4 weeks ago   78.1MB
ubuntu       22.04     97271d29cb79   2 weeks ago   77.9MB</code></pre>
<hr>
<h3 id="🚀-컨테이너-생성-docker-run">🚀 컨테이너 생성: <code>docker run</code></h3>
<p>이미지를 받았다면 이제 컨테이너를 생성해보겠습니다.</p>
<pre><code class="language-bash">$ docker run ubuntu:22.04</code></pre>
<p>이후 상태를 확인하면 다음과 같이 컨테이너가 바로 종료된 것을 볼 수 있습니다.</p>
<pre><code class="language-bash">$ docker ps -a</code></pre>
<p>이는 기본적으로 컨테이너가 실행된 후 <strong>모든 입력이 종료되면 자동으로 꺼지기 때문</strong>입니다.
컨테이너를 상시 실행 상태로 유지하려면 몇 가지 옵션을 추가해줘야 합니다.</p>
<hr>
<h3 id="🛠️-자주-쓰는-docker-run-옵션">🛠️ 자주 쓰는 <code>docker run</code> 옵션</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>--name</code></td>
<td>컨테이너 이름 지정</td>
</tr>
<tr>
<td><code>-i</code></td>
<td>표준 입력 유지</td>
</tr>
<tr>
<td><code>-t</code></td>
<td>TTY(터미널) 할당</td>
</tr>
<tr>
<td><code>-d</code></td>
<td>백그라운드 실행</td>
</tr>
<tr>
<td><code>-p</code></td>
<td>포트 포워딩 설정</td>
</tr>
<tr>
<td><code>-v</code></td>
<td>볼륨 마운트</td>
</tr>
<tr>
<td><code>-e</code></td>
<td>환경 변수 지정</td>
</tr>
</tbody></table>
<p>컨테이너를 실행 상태로 유지하고 bash에 접속할 수 있도록 다음과 같이 실행합니다:</p>
<pre><code class="language-bash">$ docker run -itd --name container-ubuntu ubuntu:22.04</code></pre>
<p>컨테이너가 정상적으로 실행되고 있는지 확인합니다.</p>
<pre><code class="language-bash">$ docker ps</code></pre>
<hr>
<h3 id="🧑💻-실행-중인-컨테이너에-접속하기">🧑‍💻 실행 중인 컨테이너에 접속하기</h3>
<p>단발성 명령어를 실행하려면 <code>docker exec</code>을 사용합니다.</p>
<pre><code class="language-bash">$ docker exec container-ubuntu echo &quot;hello&quot;</code></pre>
<p>컨테이너 내부 쉘에 직접 접속하려면 <code>-it</code> 옵션을 추가합니다.</p>
<pre><code class="language-bash">$ docker exec -it container-ubuntu /bin/bash
root@e39c9c5d19c4:/#</code></pre>
<hr>
<h2 id="🌐-nginx-설치하고-포트-포워딩-테스트하기">🌐 Nginx 설치하고 포트 포워딩 테스트하기</h2>
<p>이번엔 <strong>호스트 PC의 80번 포트를 컨테이너의 80번 포트로 연결</strong>한 뒤, Nginx를 설치해 웹 요청을 테스트해보겠습니다.</p>
<pre><code class="language-bash">$ docker run -itd -p 80:80 --name ubuntu-nginx ubuntu:22.04
$ docker ps</code></pre>
<p>컨테이너에 접속한 후 Nginx를 설치합니다.</p>
<pre><code class="language-bash">$ docker exec -it ubuntu-nginx /bin/bash
# apt update
# apt-get install -y nginx
# service nginx start
# service nginx status</code></pre>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/930818f1-9680-4a53-8536-4301f6c38e33/image.png" alt=""></p>
</blockquote>
<p>이제 웹 브라우저에서 <code>http://&lt;Docker Host IP&gt;</code> 로 접속하면 아래와 같이 Nginx 기본 페이지가 보입니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jw_kim/post/94aeef10-f361-4495-b67b-0d0cad039561/image.png" alt=""></p>
</blockquote>
<hr>
<h3 id="✅-마무리">✅ 마무리</h3>
<p>이번 포스트에서는 컨테이너 생성부터 접속, Nginx 설치까지 <strong>Docker의 기본 사용법</strong>을 실습해보았습니다.</p>
<p>다만, 예시처럼 하나의 Ubuntu 컨테이너에 Nginx, API 등 여러 프로세스를 올리는 방식은 <strong>Docker의 권장 사용법과는 다릅니다</strong>.
컨테이너 하나에는 **하나의 역할(프로세스)**만 수행하는 것이 바람직합니다.</p>
<blockquote>
<p>현재 저희 조직에서도 하나의 Ubuntu, 하나의 Rocky Linux를 제외하면 대부분 컨테이너당 하나의 프로세스만 실행하는 구조를 사용하고 있습니다.</p>
</blockquote>
<p>다음 글에서는 Docker의 네트워크 구성, 볼륨 설정, 이미지 커스터마이징과 같은 <strong>조금 더 응용적인 활용법</strong>을 다뤄보겠습니다.
Kubernetes와의 연동 이야기도 곧 시작됩니다 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 🚀 우리 조직의 Docker 전환기: 개발 환경을 컨테이너로 갈아탄 이유와 기록-1]]></title>
            <link>https://velog.io/@jw_kim/docker-%EC%82%AC%EB%82%B4-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-docker%EB%A1%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-docker-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-ujvnfvsi</link>
            <guid>https://velog.io/@jw_kim/docker-%EC%82%AC%EB%82%B4-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-docker%EB%A1%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-docker-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-ujvnfvsi</guid>
            <pubDate>Thu, 12 Sep 2024 07:04:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>저는 회사 개발 조직이 온프레미스 환경에서 클라우드 플랫폼(Naver Cloud Platform, 이하 NCP)으로 전환을 시작하던 시점에 입사했습니다. 클라우드 마이그레이션이 완료된 이후, 기존 온프레미스 자원은 자연스럽게 개발 환경으로 활용되기 시작했죠. 그리고 최근, 이 개발 환경을 Docker 기반 컨테이너 환경으로 전면 전환하게 되었습니다.
이번 시리즈에서는 Docker 도입을 결정하게 된 과정부터 실제 전환까지의 여정을 공유해보려 합니다.</p>
</blockquote>
<h3 id="🐳-여러-대의-개발-서버-대신-왜-docker였을까">🐳 여러 대의 개발 서버 대신, 왜 Docker였을까?</h3>
<p>한동안 우리는 사내에 남아있던 여러 대의 물리 서버를 활용해 개발 환경을 운영해왔습니다. Windows Server, CentOS, VirtualBox 등 다양한 운영체제를 직접 설치하고 구성하며 프로젝트마다 환경을 맞추는 데 많은 시간이 소요됐습니다.</p>
<p>하지만 점점 서버가 복잡해지고, 환경이 꼬이기 시작했습니다. 누가 어떤 설정을 했는지 모르는 서버, 숨겨진 백업 데이터, 프로젝트마다 다른 OS 요구사항...</p>
<p>이러한 문제들을 겪으며 자연스럽게 <strong><code>“개발 환경을 간단하고 효율적으로 구성하는 방법은 없을까?”</code></strong> 라는 고민을 하게되었습니다. </p>
<p>Docker를 도입하기에 앞서, 내부적으로는 <strong>두 가지 선택지</strong>를 두고 고민했습니다. </p>
<ul>
<li>하나는 Windows의 Hyper-V를 기반으로 한 전통적인 가상 머신 방식이고, </li>
<li>다른 하나는 Docker를 활용한 컨테이너 기반 환경이었습니다.</li>
</ul>
<hr>
<h4 id="hyper-v">Hyper-V?</h4>
<p>Hyper-V는 각 개발 환경을 완전히 분리된 가상 머신으로 구성할 수 있어 운영체제를 자유롭게 설정할 수 있고 보안적으로도 안정적이라는 장점이 있었습니다. 그러나 VM 하나를 구성하려면 운영체제 설치부터 소프트웨어 세팅까지 시간이 오래 걸리고, 리소스 소모도 상당하다는 단점이 있었습니다. 특히 여러 명이 동시에 다양한 프로젝트를 운영해야 하는 우리 조직 구조에서는 비효율적인 선택처럼 느껴졌습니다.</p>
<h4 id="docker">Docker?</h4>
<p>반면 Docker는 운영체제 수준에서 격리된 컨테이너를 활용하기 때문에, 훨씬 빠르게 환경을 구성할 수 있고 자원 소모도 훨씬 적었습니다. 운영체제에 관계없이 컨테이너만 있으면 누구든 동일한 환경에서 개발할 수 있고, 이미지로 관리되기 때문에 배포나 복구도 용이하다는 점이 큰 장점이었습니다.</p>
<p>결국 우리는 보다 빠르고 유연하며, 협업에 강한 개발 환경을 구축할 수 있는 <strong>Docker를 선</strong>택하게 되었습니다. 그리고 실제로 전환을 진행하면서 예상했던 이점 이상으로 효율을 체감하고 있습니다.</p>
<hr>
<h3 id="🛠️-docker-일단-설치부터-해봅시다">🛠️ Docker, 일단 설치부터 해봅시다</h3>
<p>앞선 글에 이어 실제로 Docker를 설치하고 사용해보기 위한 준비 과정을 소개하려고 합니다.</p>
<p>도입을 결심하긴 했지만, 사내 개발 인력 중 Docker에 익숙한 사람이 없었기 때문에 먼저 기본적인 사용법부터 익힐 필요가 있었습니다. 그래서 내부적으로 도커, 컨테이너 빌드업! (이현룡 지음)이라는 책을 구매해 2주 정도 학습을 진행했습니다. 이 포스트는 그 학습 내용을 토대로 Docker 설치 과정을 간단히 정리한 내용입니다.</p>
<h4 id="🐧-설치-환경">🐧 설치 환경</h4>
<p>Docker는 리눅스 컨테이너 기술(LXC)을 기반으로 동작하기 때문에, Linux 기반의 호스트 OS가 필요합니다. 이번 설치는 기본서와 동일하게 Ubuntu 18.04 환경에서 진행했습니다.</p>
<h4 id="🔧-설치-전-준비-단계">🔧 설치 전 준비 단계</h4>
<pre><code class="language-bash"># 1. 패키지 목록 업데이트
$ sudo apt-get update

# 2. 의존성 패키지 설치
$ sudo apt-get install -y \
  apt-transport-https \
  ca-certificates \
  curl \
  software-properties-common

# 3. Docker 공식 GPG 키 추가
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# (선택) GPG 키 확인
$ sudo apt-key fingerprint

# 4. Docker 리포지토리 추가
$ sudo add-apt-repository \
  &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot;

# 5. 패키지 목록 업데이트 (리포지토리 추가 후)
$ sudo apt-get update</code></pre>
<h4 id="📝-각-단계-설명">📝 각 단계 설명</h4>
<p><strong>1. 패키지 목록 업데이트</strong>
<code>apt-get update</code>로 시스템에 등록된 패키지 목록을 최신 상태로 유지합니다.</p>
<p><strong>2. Docker 의존성 패키지 설치</strong></p>
<ul>
<li>apt-transport-https: HTTPS를 통한 패키지 다운로드 지원</li>
<li>ca-certificates: 보안 인증서 검증용</li>
<li>curl: URL 기반 다운로드 도구</li>
<li>software-properties-common: PPA 추가/관리용 도구</li>
</ul>
<p><strong>3. GPG 키 등록</strong>
Docker에서 제공하는 GPG 키를 등록해 설치 패키지의 신뢰성을 검증합니다.</p>
<p><strong>4. Docker 리포지토리 추가</strong>
Ubuntu 기본 저장소에는 Docker의 최신 버전이 없을 수 있으므로, 공식 리포지토리를 따로 등록합니다.</p>
<p><strong>5. 패키지 목록 재업데이트</strong>
리포지토리가 변경되었으니 다시 업데이트해줍니다.</p>
<hr>
<h4 id="📦-docker-설치-및-권한-설정">📦 Docker 설치 및 권한 설정</h4>
<pre><code class="language-bash"># Docker 설치
sudo apt-get -y install docker-ce

# (선택) 특정 버전 설치
# sudo apt-get install docker-ce=20.10.10

# 설치 확인
docker version

# 현재 사용자에게 Docker 권한 부여
sudo usermod -aG docker $(whoami)

# Docker 재시작
sudo service docker restart</code></pre>
<p>설치 후에는 다시 로그인하거나 터미널을 재시작하면 sudo 없이도 docker 명령어를 사용할 수 있습니다.</p>
<h3 id="✅-마무리">✅ 마무리</h3>
<p>이제 Docker를 사용할 수 있는 기본 환경이 갖춰졌습니다.
다음 글에서는 간단한 컨테이너를 실행해보며 Docker의 동작 방식을 테스트해볼 예정입니다.</p>
]]></description>
        </item>
    </channel>
</rss>