<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>아재 개발자의 메모장</title>
        <link>https://velog.io/</link>
        <description>안녕하세요. 아재 개발자입니다. 공부한 내용을 기록하고 잘못된 부분에 대해서 조언을 받기 위해 velog를 시작했습니다. :)</description>
        <lastBuildDate>Tue, 31 Mar 2026 00:12:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 아재 개발자의 메모장. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-lop" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[펜싱 토큰 (Fencing Token)]]></title>
            <link>https://velog.io/@dev-lop/%ED%8E%9C%EC%8B%B1-%ED%86%A0%ED%81%B0-Fencing-Token</link>
            <guid>https://velog.io/@dev-lop/%ED%8E%9C%EC%8B%B1-%ED%86%A0%ED%81%B0-Fencing-Token</guid>
            <pubDate>Tue, 31 Mar 2026 00:12:05 GMT</pubDate>
            <description><![CDATA[<p>분산 시스템에서 <strong>펜싱 토큰(Fencing Token)</strong>은 공유 자원에 대한 접근 권한이 이미 만료된 프로세스가 뒤늦게 자원을 수정하는 것을 방지하기 위해 사용하는 보안 및 일관성 유지 메커니즘입니다.</p>
<p>주로 <code>브레인 분할(Split-brain) 현상</code>이나 <code>STW(Stop-The-World)</code>, <code>네트워크 지연</code> 등으로 인해 발생하는 문제를 해결하기 위해 도입되었습니다.</p>
<h2 id="1-왜-필요한가요-문제-상황">1. 왜 필요한가요? (문제 상황)</h2>
<p>분산 시스템에서는 특정 노드가 죽었는지, 단순히 네트워크가 느린 것인지 완벽하게 구별하기 어렵습니다. 이때 다음과 같은 사고가 발생할 수 있습니다.</p>
<ol>
<li><p>노드 A가 잠금(Lock)을 획득하여 공유 파일(Storage)에 접근합니다.</p>
</li>
<li><p>노드 A에 갑자기 긴 GC(Garbage Collection)가 발생하여 일시 정지됩니다.</p>
</li>
<li><p>잠금 서비스(예: ZooKeeper)는 노드 A가 죽었다고 판단하고 잠금을 해제한 뒤, 노드 B에게 잠금을 부여합니다.</p>
</li>
<li><p>노드 B는 데이터를 쓰기 시작합니다.</p>
</li>
<li><p>이때 노드 A의 GC가 끝나고 깨어납니다. A는 자신이 여전히 잠금을 가지고 있다고 착각하고 데이터를 씁니다.</p>
</li>
</ol>
<p>결과적으로 노드 B의 데이터가 노드 A에 의해 덮어씌워져 데이터 오염이 발생합니다.</p>
<h2 id="2-펜싱-토큰의-작동-원리">2. 펜싱 토큰의 작동 원리</h2>
<p>펜싱 토큰은 잠금을 획득할 때마다 <strong>증가하는 숫자(Sequence Number)</strong>를 함께 부여함으로써 이 문제를 해결합니다.</p>
<h3 id="토큰-발행">토큰 발행</h3>
<p>클라이언트가 잠금을 획득할 때, 잠금 서버는 현재 버전보다 높은 숫자(예: 31)를 토큰으로 발행합니다.</p>
<h3 id="검증-요청">검증 요청</h3>
<p>클라이언트가 저장소(Storage)에 데이터를 쓸 때 이 토큰(31)을 함께 보냅니다.</p>
<h3 id="저장소의-거부">저장소의 거부</h3>
<p>저장소는 자신이 마지막으로 처리한 토큰 번호를 기억합니다. </p>
<p>만약 이전에 이미 번호 32를 처리했다면, 번호 31을 가진 요청은 &quot;과거의 요청&quot;으로 간주하여 거부합니다.</p>
<h2 id="3-주요-구성-요소">3. 주요 구성 요소</h2>
<ul>
<li><p><strong>Lock Service</strong>
잠금과 함께 단조 증가하는 토큰을 생성합니다.</p>
</li>
<li><p><strong>Resource (Storage)</strong>
가장 최근에 수용한 토큰 번호를 저장하고, 그보다 낮은 번호의 요청은 무시합니다.</p>
</li>
<li><p><strong>Client</strong>
잠금을 획득할 때 받은 토큰을 모든 작업 요청에 포함시킵니다.</p>
</li>
</ul>
<h2 id="4-요약">4. 요약</h2>
<p>펜싱 토큰은 &quot;<strong>늦게 온 요청이 시스템의 상태를 되돌리지 못하게 하는 안전장치</strong>&quot;입니다. </p>
<p>아무리 노드가 자신이 권한이 있다고 주장해도, 토큰 번호가 낮으면 저장소 수준에서 입구 컷(Fencing)을 당하게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[바이브 코딩과 클린 아키텍처]]></title>
            <link>https://velog.io/@dev-lop/%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9%EA%B3%BC-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@dev-lop/%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9%EA%B3%BC-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Fri, 13 Feb 2026 05:03:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글은 AI와 주고받은 내용을 정리한 메모성 글입니다.</p>
</blockquote>
<h1 id="바이브-코딩-vibe-coding">바이브 코딩 (Vibe Coding)</h1>
<p>바이브 코딩은 안드레이 카파시(Andrej Karpathy)가 언급하며 대중화한 개념으로, 개발자가 코드를 직접 한 땀 한 땀 타이핑하는 대신, <strong>LLM(대형 언어 모델)과 상호작용하며 의도와 흐름을 중심으로 소프트웨어를 구축하는 방식</strong>을 뜻합니다. </p>
<p>이제 개발자의 역량은 특정 언어의 문법을 외우고 구현하는 능력보다, 시스템을 추상화하여 AI에게 정확한 맥락을 제공하고 생성된 결과물을 검증하는 <strong>시스템 설계 및 검토 역량</strong>으로 변화하고 있습니다.</p>
<h2 id="본질">본질</h2>
<p>바이브 코딩은 단순히 AI에게 코딩을 시키는 것이 아닙니다. 개발자는 본인이 가지고 있는 도메인 지식과 아키텍처 설계 지식을 AI에게 명확히 전달하는 역량이 중요해졌습니다.</p>
<p>최근 <strong>프로덕트 엔지니어</strong>와 <strong>도메인 지식</strong>이 강조되는 이유도 이 때문입니다.</p>
<h2 id="ai의-한계">AI의 한계</h2>
<p>AI에게 아무리 명확하고 구체적인 요구사항을 전달하더라도 다음과 같은 치명적인 문제에 직면하게 됩니다.</p>
<h3 id="지능의-한계와-환각">지능의 한계와 환각</h3>
<p>LLM은 한 번에 처리할 수 있는 정보량인 <strong>컨텍스트 윈도우(Context Window)</strong> 범위 내에서만 추론할 수 있습니다. </p>
<p>만약 AI가 참조하고, 기억해야하는 컨텍스트의 크기가 커지고, 그 한계에 도달하게 되면 AI는 컨텍스트의 일부를 유실하는 망각 현상이 발생하게 됩니다.</p>
<p>망각 현상이 발생하게 되면 존재하지 않는 참조와 코드를 만들어내는 환각을 만들어내게 되는데, 이렇듯 구조화되지 않은 코드와 프로젝트 구조는 AI에게 과도한 노이즈를 제공하여 추론 능력을 저하시킵니다.</p>
<h3 id="경제적-효율성의-하락">경제적 효율성의 하락</h3>
<p>최근의 AI 에이전트(Claude의 Subagents 등)는 복잡한 문제를 풀기 위해 스스로 코드를 읽고 도구를 사용(Skills)하며 수십 번의 추론 과정을 거칩니다. </p>
<p>위에서 말했듯, 아키텍처가 부재한 프로젝트는 AI가 단 하나의 기능을 수정하기 위해 프로젝트 전체를 훑으며 관계없는 과도한 노이즈까지 컨텍스트로 참조하게 됩니다.</p>
<p>이는 <strong>기하급수적인 토큰 비용 상승을 야기하고 개발 생산성보다 AI 유지 비용이 더 커지는 역전 현상을 발생</strong>시키게 됩니다.</p>
<h3 id="비결정성non-determinism과-오염">비결정성(Non-determinism)과 오염</h3>
<p>바이브 코딩의 가장 치명적인 단점은 <strong>동일한 지시(Prompt)를 내려도 AI가 매번 다른 결과물을 내놓는다는 점</strong>입니다. </p>
<p>AI는 주어진 지시사항을 완수하기 위해 가장 확률적으로 높은 빠른 길을 선택합니다.</p>
<p>계층 구조가 모호한 프로젝트에서 AI는 비즈니스 로직(Service) 내부에 DB 스키마 의존성을 심거나 외부 API 호출 코드를 직접 삽입하는 등의 행위를 서슴지 않습니다.</p>
<p>이렇게 무작위적으로 생산되는 코드는 당장 동작하는데 문제가 없을지언정, 장기적으로는 인간도 AI도 이해할 수 없는 거대한 &#39;오염된 코드 덩어리&#39;를 만들어내며 기술적 부채를 통제 불가능한 수준으로 가속화시킵니다.</p>
<h1 id="클린-아키텍처">클린 아키텍처</h1>
<p>이러한 AI의 태생적인 한계를 극복할 수 있는 수단으로 Robert C. Martin(엉클 밥)이 제안한 클린 아키텍처가 떠오르고 있습니다.</p>
<p>클린 아키텍처의 핵심은 <strong>의존성 규칙(Dependency Rule)</strong>으로, 소스 코드의 의존성은 반드시 외부에서 내부로만 향해야 하며, 내부 원(Circle)에 있는 요소는 외부 원에 있는 어떤 것도 알지 못해야 한다는 원칙을 따릅니다.</p>
<p>위에서도 언급했듯이 바이브 코딩은 개발자가 세부 구현보다 <strong>의도(Vibe)</strong>에 집중하는 방식인데, 이때 클린 아키텍처는 AI에게 <strong>사고의 지도</strong>와 <strong>행동의 제약</strong>을 동시에 제공하며, 단순한 코드 정리를 넘어 비결정적인 AI를 통제하는 최적의 시스템이 됩니다.</p>
<h2 id="바이브-코딩과-클린-아키텍처의-시너지">바이브 코딩과 클린 아키텍처의 시너지</h2>
<h3 id="ai-최적화">AI 최적화</h3>
<p>클린 아키텍처는 UseCase라는 명확한 진입점을 제공하고 AI가 참조해야 할 파일의 범위를 좁혀주므로, 불필요한 토큰 소모를 줄이고 한정된 컨텍스트 내에서 AI의 추론 정확도를 극대화하는 효과를 얻을 수 있습니다.</p>
<h3 id="비결정성-제어">비결정성 제어</h3>
<p>AI는 동일한 지시에도 매번 다른 품질의 코드를 내놓는 비결정성 특징을 가집니다.</p>
<p>클린 아키텍처의 계층 구조는 AI가 편의를 위해 레이어를 침범하지 못하게 제한하기 때문에 결과적으로 코드의 오염과 기술적 부채가 쌓이는 것을 최소화할 수 있습니다.</p>
<h3 id="독립성">독립성</h3>
<p>클린 아키텍처는 엔티티, 유스케이스, 어댑터 등으로 계층화되어 있어 비즈니스 로직을 외부 도구(프레임워크, 개발 언어 등)로부터 완전히 격리시킬 수 있습니다.</p>
<p>이러한 독립성 때문에 개발자는 언어나 프레임워크에 종속되지 않은 채 프롬프트만으로 동일한 기능을 구현해낼 수 있습니다.</p>
<p>또한, 구조가 명확히 분리되어있어 AI가 생성한 코드가 어느 부분에 위치해야 할지 명확하게 예측할 수 있어 관리가 쉬워집니다.</p>
<hr>
<h1 id="가상-클린-아키텍처">가상 클린 아키텍처</h1>
<p>바이브 코딩으로 진행하는 모든 프로젝트를 당장 클린 아키텍처로 리팩토링할 수는 없습니다. 이때 활용할 수 있는 현실적인 대안이 바로 <strong>AI 전용 아키텍처 가이드라인(.md)</strong>을 활용한 &#39;가상 클린 아키텍처&#39; 전략입니다.</p>
<p>이는 물리적 폴더 구조를 바꾸지 않고도 AI의 오염을 막는 소프트웨어적 격벽 역할을 합니다.</p>
<h2 id="claude-code의-init-과-ai-전용-아키텍처-가이드라인의-차이">Claude Code의 /init 과 AI 전용 아키텍처 가이드라인의 차이</h2>
<p>Claude Code의 <code>/init</code> 명령어로 생성되는 CLAUDE.md와 혼동될 수 있지만, 두 문서의 목적은 명확히 다릅니다.</p>
<h3 id="claudemd">CLAUDE.md</h3>
<p>이 파일의 본질은 AI가 이 프로젝트에서 <strong>어떻게 행동해야 하는가</strong>에 대한 운영 매뉴얼에 가깝습니다.</p>
<p>빌드 명령어, 테스트 실행법, 네이밍 컨벤션, 린트 설정 등 작업 환경에 집중하는 내용을 갖고 있습니다.</p>
<h3 id="ai-전용-아키텍처-가이드라인-설계-원칙">AI 전용 아키텍처 가이드라인 (설계 원칙)</h3>
<p>AI 전용 아키텍처 가이드라인은 이 프로젝트가 어떤 철학으로 설계되었고, 설계해야하는가?에 대한 <strong>소프트웨어 설계 원칙</strong>을 박아넣는 것입니다. </p>
<p>AI가 편의를 위해 스파게티 코드를 짜려는 본능을 억제하고, 특정 아키텍처(예: 클린 아키텍처)의 설계 지능을 이식하는 역할을 수행합니다.</p>
<h2 id="가상-클린-아키텍처의-한계">가상 클린 아키텍처의 한계</h2>
<p>문서 하나로 모든 설계 문제를 해결할 수 있다면 좋겠지만, 가상 클린 아키텍처는 어디까지나 <strong>임시 방편</strong>입니다. </p>
<p>따라서 원본 코드의 상태에 따라 다음과 같은 한계가 발생합니다.</p>
<h3 id="context-한계와-환각의-상관관계">Context 한계와 환각의 상관관계</h3>
<p>위에서 말했듯, AI가 한 번에 읽을 수 있는 <strong>컨텍스트 윈도우(Context Window)</strong>는 한계가 정해져 있습니다. </p>
<p>하나의 파일에 수천 줄의 로직이 뒤섞여 있다면, AI는 가이드라인(.md)을 준수하려고 노력하는 동시에 거대한 원본 코드를 해석하느라 에너지를 소모합니다. </p>
<p>정보 밀도가 한계치에 다다르면 AI는 가이드라인을 무시하거나, 기존 코드의 스파게티 구조를 그대로 복제하는 환각 증상을 보이기 시작합니다.</p>
<h3 id="물리적-격벽-없는-규칙의-허망함">물리적 격벽 없는 규칙의 허망함</h3>
<p>아무리 &quot;도메인과 인프라를 분리하라&quot;고 명시해도, 실제 코드가 하나의 클래스 안에 모든 기능이 다 들어가 있다면 AI는 &#39;가상으로라도&#39; 분리해서 짤 공간을 찾지 못합니다. </p>
<p>결국 기존 코드를 따라가게 되며, 이는 논리적 규칙이 물리적 혼돈을 이기지 못하는 결과를 초래합니다.</p>
<h2 id="그래서">그래서?</h2>
<p>가상 클린 아키텍처는 <strong>시간을 벌어주는 도구</strong>이지 최종 목적지가 되어서는 안 됩니다. 가장 현실적인 로드맵은 다음과 같습니다.</p>
<ul>
<li><p><strong>점진적 격리</strong>
새로운 기능을 추가할 때만큼은 가상 아키텍처 가이드를 준수하며 별도의 패키지나 모듈로 분리합니다.</p>
</li>
<li><p><strong>보이스카우트 규칙</strong>
건드리는 코드 주변부터 조금씩 물리적 격벽을 세워 나갑니다.</p>
</li>
<li><p><strong>가이드의 구체화</strong>
AI에게 단순히 &quot;클린하게 짜줘&quot;라고 하기보다, &quot;새로운 기능은 반드시 src/features 하위에 UseCase 단위로 작성하라&quot;는 식의 물리적 위치를 지정해 주는 것이 효과적입니다.</p>
</li>
</ul>
<h1 id="결론">결론</h1>
<p>바이브 코딩 시대에 클린 아키텍처 구조는 AI의 환각과 오염 리스크를 줄일 수 있는 현 시점에서의 최선의 타협점이라고 생각합니다.</p>
<p>우리는 이제 클린 아키텍처를 단순히 &#39;코드 유지보수가 편한 디자인 패턴&#39;으로 봐서는 안 됩니다. 오히려 AI라는 강력하지만 통제하기 어려운 <strong>비결정적(Non-deterministic) 개발 도구</strong>를 안전하게 다루기 위한 핵심 방역 체계로 재해석해야 합니다.</p>
<p>클린 아키텍처는 AI에게 전달할 맥락을 도메인 단위로 격리하여 비용을 절감하고, 물리적 계층 분리를 통해 AI의 무분별한 코드 생성을 차단하는 &#39;가이드레일&#39; 역할을 합니다. </p>
<p>결국 바이브 코딩 시대의 경쟁력은 AI에게 얼마나 많은 코드를 맡기느냐가 아니라, AI가 사고를 쳐도 시스템의 핵심(Domain)만큼은 무너지지 않도록 얼마나 견고한 격벽(Architecture)을 설계하느냐에 달려 있습니다.</p>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">The Clean Architecture</a></li>
<li>Gemini</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Barrel Import]]></title>
            <link>https://velog.io/@dev-lop/Barrel-Import%EC%99%80-ClassValidator%EA%B0%80-%EC%B6%A9%EB%8F%8C%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@dev-lop/Barrel-Import%EC%99%80-ClassValidator%EA%B0%80-%EC%B6%A9%EB%8F%8C%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 10 Feb 2026 15:44:32 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<p>토이프로젝트를 진행하던 중 다른 Controller의 API는 정상인데, 특정 Controller의 API만 ClassValidator가 동작을 하지 않는 상황이 발생했습니다.</p>
<h2 id="원인">원인</h2>
<h3 id="barrel-import">Barrel Import</h3>
<blockquote>
<p><strong>Barrel Import</strong>
여러 모듈 또는 파일을 index 파일 하나에서 모두 export하고, 외부에서는 하나의 파일만 Import하여 사용하는 패턴</p>
</blockquote>
<p>결과적으로 <code>Controller</code> -&gt; <code>DTO</code> -&gt; <code>Barrel Import</code> -&gt; <code>Controller</code> 식으로 의존성이 꼬이면서 순환 참조가 발생했던 케이스였습니다.</p>
<p><strong>문제의 코드</strong></p>
<pre><code class="language-typescript">import { GenerateJwtTokenBody, GenerateJwtTokenResponse } from &#39;@app/controllers&#39;;</code></pre>
<p>위와 같이 잘못된 <code>Barrel Import</code>로 인해 순환 참조가 발생하게 되면, 자바스크립트 엔진은 모듈이 무한 루프에 빠지는 것을 방지하기 위해 <code>ReferenceError</code> 에러를 발생시키거나 <code>undefined</code> 객체를 응답하게 됩니다.</p>
<p>이렇게 정상적으로 Import가 되지 않은 상황에서 <code>reflect-metadata</code>가 호출되면 reflect는 메타데이터를 정상적으로 읽지 못하게 되고, class-validator, class-transformer, DI 의존성 주입, Swagger 문서 생성 등 다양한 기능들이 동작을 하지 않게 됩니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>해결 방법은 단순합니다.</p>
<p><code>Barrel Import</code>로 인해 순환 참조가 발생하게 되었으니, <code>Deep Import</code> 방식으로 파일을 Import하거나, 순환 참조가 발생하지 않게끔 상대 경로로 Import 하면 됩니다.</p>
<p><strong>수정한 코드</strong></p>
<pre><code class="language-typescript">import { GenerateJwtTokenBody, GenerateJwtTokenResponse } from &#39;./dto&#39;;</code></pre>
<h2 id="결론">결론</h2>
<p>이와 같이 논리적인 에러는 원인을 파악하기 힘듭니다... Syntactic Error가 최고야...</p>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://nodejs.org/api/modules.html#cycles">NodeJS 문서(Cycles)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#cyclic_imports">JavaScript MDN(Cyclic_imports)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 설치]]></title>
            <link>https://velog.io/@dev-lop/Kubernetes-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@dev-lop/Kubernetes-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sat, 29 Nov 2025 06:06:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글은 개인으로 사용하는 미니 PC에 Kubernetes를 설치하고, 기본적인 설정을 다룬 내용을 간략하게 정리한 글입니다.</p>
<p>관련해서 왜 이렇게 설치를 했는지에 대해서는 지속적으로 추가 업데이트 할 예정입니다.</p>
</blockquote>
<h1 id="초기-설정">초기 설정</h1>
<h2 id="os-설정">OS 설정</h2>
<h3 id="selinux-설정">SELinux 설정</h3>
<p>쿠버네티스는 기본적으로 SELinux Mode가 <code>permissive</code>로 설정되어 있어야 합니다.
( 관련 문서: <a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl">installing-kubeadm-kubelet-and-kubectl</a> )</p>
<p>따라서 아래의 명령어를 통해 SELinux 설정을 변경해줍니다.</p>
<pre><code># Set SELinux in permissive mode (effectively disabling it)
sudo setenforce 0
sudo sed -i &#39;s/^SELINUX=enforcing$/SELINUX=permissive/&#39; /etc/selinux/config</code></pre><h3 id="swapconfig">swapconfig</h3>
<p>그리고 kubelet은 스왑 메모리가 감지되면 정상적으로 동작하지 않습니다. 
( 관련 문서: <a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#swap-configuration">install-kubeadm</a> )</p>
<p>따라서 아래 명령어를 통해 스왑 메모리 설정을 비활성화시켜 줍니다.</p>
<pre><code>sudo swapoff -a</code></pre><h3 id="ip_forward-설정">ip_forward 설정</h3>
<p><code>net.ipv4.ip_forward</code> 옵션은 리눅스 커널이 IPv4 패킷을 다른 인터페이스로 라우팅할 지에 대한 여부를 설정하는 옵션입니다.</p>
<p>쿠버네티스는 기본적으로 CNI를 통해 Pod / Node간 통신을 처리하는 구조로 되어있기 때문에, 라우팅/포워딩을 할 수 있도록 1로 설정해줍니다.</p>
<pre><code>sudo sysctl -w net.ipv4.ip_forward=1</code></pre><p>만약 이 설정을 하지 않을 경우 컨트롤 플레인 초기화 시 아래와 같은 에러가 발생할 수 있습니다.</p>
<pre><code>[ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1</code></pre><h3 id="firewalld-disabled">firewalld disabled</h3>
<p>쿠버네티스는 기본적으로 <a href="https://kubernetes.io/docs/reference/networking/ports-and-protocols">사용되는 포트</a>가 존재합니다.</p>
<p>이 중 Kubernetes API server 포트와 Kubelet API 포트는 필수적으로 사용되는 포트이기 때문에 OS 방화벽에서 해당 포트들을 허용처리 해주어야 합니다.</p>
<p>쿠버네티스에서 기본적으로 사용되는 포트와 MySQL, Redis 등을 사용할 때 마다 방화벽을 설정하는 것도 좋지만, 이번 글에서는 <del>귀찮으므로</del> OS 방화벽 설정을 해제하는 방법으로 진행하고자 합니다.</p>
<pre><code>systemctl stop firewalld
systemctl disable firewalld</code></pre><p>만약 이러한 설정을 하지 않을 경우 컨트롤 플레인 초기화 시 아래와 같은 에러가 발생할 수 있습니다.</p>
<pre><code>[WARNING Firewalld]: firewalld is active, please ensure ports [6443 10250] are open or your cluster may not function correctly</code></pre><h2 id="containerd-설치">containerd 설치</h2>
<p>쿠버네티스는 기본적으로 Pod에서 컨테이너를 실행시키기 위해 <a href="https://kubernetes.io/docs/setup/production-environment/container-runtimes/">container runtime</a>을 사용합니다.</p>
<blockquote>
<p><strong>사용할 수 있는 Container Runtime 목록</strong></p>
</blockquote>
<ul>
<li>containerd</li>
<li>CRI-O</li>
<li>Docker Engine</li>
<li>Mirantis Container Runtime</li>
</ul>
<p>그리고 이 글에서는 <a href="https://github.com/containerd/containerd/blob/main/docs/getting-started.md">containerd</a>를 사용하는 방법으로 진행합니다.</p>
<h4 id="dnf-repository-추가">dnf Repository 추가</h4>
<pre><code>sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo</code></pre><h4 id="containerdio-설치">containerd.io 설치</h4>
<pre><code>sudo dnf install containerd.io</code></pre><h4 id="containerd-설정-변경">containerd 설정 변경</h4>
<p>containerd는 기본적으로 cri 플러그인이 비활성화된 상태로 설치가 됩니다.</p>
<p>cri는 containerd가 쿠버네티스와 통신하기 위한 규약인데, /etc/containerd/config.toml 설정파일을 열어서 cri를 활성화시켜줍니다.</p>
<pre><code>disabled_plugins = [] # disabled_plugins = [&quot;cri&quot;] 에서 cri를 지워주세요.</code></pre><p>그리고 재시작을 해줍니다.</p>
<pre><code>service containerd restart</code></pre><p>만약 이러한 설정을 하지 않을 경우 컨트롤 플레인 초기화 시 아래와 같은 에러가 발생할 수 있습니다.</p>
<pre><code>[preflight] WARNING: Couldn&#39;t create the interface used for talking to the container runtime: failed to create new CRI runtime service: validate service connection: validate CRI v1 runtime API for endpoint &quot;unix:///var/run/containerd/containerd.sock&quot;: rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService</code></pre><h1 id="kubernetes-설치">Kubernetes 설치</h1>
<h2 id="kubernetes-저장소-추가">kubernetes 저장소 추가</h2>
<pre><code>cat &lt;&lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key
EOF</code></pre><h2 id="kubelet-kubeadm-kubectl-설치">kubelet, kubeadm, kubectl 설치</h2>
<pre><code>dnf install kubelet kubeadm kubectl</code></pre><p><img src="https://velog.velcdn.com/images/dev-lop/post/bcf6065b-2524-439f-8a1a-ab0ceb31a9b9/image.png" alt=""></p>
<h2 id="컨트롤-플레인-초기화">컨트롤 플레인 초기화</h2>
<pre><code>kubeadm init --apiserver-advertise-address={IP_ADDRESS}</code></pre><p>아래와 같이 메세지가 응답되었다면 Kubernetes control-plane 초기 설정에 성공한 것입니다.</p>
<pre><code>Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run &quot;kubectl apply -f [podnetwork].yaml&quot; with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join {IP Address:Port} --token {token값} --discovery-token-ca-cert-hash {hash값}</code></pre><p>그러면 안내를 따라서 아래의 명령을 실행해줍니다.</p>
<pre><code>mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config</code></pre><h3 id="컨트롤-플레인-노드에서-pod를-실행할-수-있게-taint-설정">컨트롤 플레인 노드에서 Pod를 실행할 수 있게 taint 설정</h3>
<p>쿠버네티스는 기본적으로 컨트롤 플레인과 워커 노드로 구성됩니다.</p>
<p>기본적으로 컨트롤 플레인 노드는 클러스터의 운영, 관리의 역할을 수행하기 때문에 Pod를 띄울 수 없으며, <code>kubeadm init</code>을 통해 컨트롤 플레인을 구성하고, <code>kubeadm join</code>을 통해 워커 노드를 추가하여 Pod를 띄우는 것이 일반적인 방법입니다.</p>
<p>하지만 지금처럼 쿠버네티스를 단일 서버로 구성하는 경우 컨트롤 플레인 노드에서 Pod를 실행할 수 있게끔 taint를 변경해주어야 합니다.</p>
<pre><code>kubectl taint nodes --all node-role.kubernetes.io/control-plane-</code></pre><p><img src="https://velog.velcdn.com/images/dev-lop/post/18a84114-7c0d-42ae-be38-f72bd2557878/image.png" alt=""></p>
<h2 id="calico-설치">Calico 설치</h2>
<p>참고 문서: <a href="https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/calico-network-policy/">https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/calico-network-policy/</a></p>
<pre><code>kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml</code></pre><h2 id="중간-과정-확인">중간 과정 확인</h2>
<pre><code>kubectl get pods --all-namespaces</code></pre><pre><code>[root@localhost tmp]# kubectl get pods --all-namespaces
NAMESPACE              NAME                                                    READY   STATUS    RESTARTS   AGE
kube-system            calico-kube-controllers-b45f49df6-zq85q                 1/1     Running   0          35m
kube-system            calico-node-6qr5n                                       1/1     Running   0          35m
kube-system            coredns-66bc5c9577-mdvrd                                1/1     Running   0          42m
kube-system            coredns-66bc5c9577-pvwdq                                1/1     Running   0          42m
kube-system            etcd-localhost.localdomain                              1/1     Running   0          42m
kube-system            kube-apiserver-localhost.localdomain                    1/1     Running   0          42m
kube-system            kube-controller-manager-localhost.localdomain           1/1     Running   0          42m
kube-system            kube-proxy-lw4z6                                        1/1     Running   0          42m
kube-system            kube-scheduler-localhost.localdomain                    1/1     Running   0          42m</code></pre><pre><code>kubectl get services --all-namespaces</code></pre><pre><code>[root@localhost tmp]# kubectl get services --all-namespaces
NAMESPACE              NAME                                   TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
default                kubernetes                             ClusterIP      10.96.0.1        &lt;none&gt;        443/TCP                      42m
kube-system            kube-dns                               ClusterIP      10.96.0.10       &lt;none&gt;        53/UDP,53/TCP,9153/TCP       42m</code></pre><h2 id="helm-설치">helm 설치</h2>
<pre><code>sudo dnf install helm</code></pre><h3 id="helm-repository-추가">helm Repository 추가</h3>
<pre><code>helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/</code></pre><h2 id="ingress-nginx-설치">ingress-nginx 설치</h2>
<pre><code>helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace</code></pre><h2 id="kubernetes-dashboard-설치">Kubernetes Dashboard 설치</h2>
<p><a href="https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/">https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/</a></p>
<pre><code>helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard</code></pre><p>아래와 같이 나오면 성공입니다.</p>
<pre><code>Release &quot;kubernetes-dashboard&quot; does not exist. Installing it now.
NAME: kubernetes-dashboard
LAST DEPLOYED: Sat Nov 29 00:13:33 2025
NAMESPACE: kubernetes-dashboard
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
*************************************************************************************************
*** PLEASE BE PATIENT: Kubernetes Dashboard may need a few minutes to get up and become ready ***
*************************************************************************************************

Congratulations! You have just installed Kubernetes Dashboard in your cluster.

To access Dashboard run:
  kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443

NOTE: In case port-forward command does not work, make sure that kong service name is correct.
      Check the services in Kubernetes Dashboard namespace using:
        kubectl -n kubernetes-dashboard get svc

Dashboard will be available at:
  https://localhost:8443</code></pre><h3 id="네트워크-설정">네트워크 설정</h3>
<h4 id="대시보드-ingress-추가">대시보드 ingress 추가</h4>
<pre><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: HTTPS
spec:
  ingressClassName: nginx
  rules:
  - host: {dashboard-host}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard-kong-proxy
            port:
              number: 443</code></pre><h4 id="공유기-포트포워딩-설정">공유기 포트포워딩 설정</h4>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/a24b6bb8-b6fa-489d-83b1-a0445f1087c3/image.png" alt=""></p>
<h4 id="접속-테스트">접속 테스트</h4>
<p>ingress에서 설정한 {dashboard-host} 도메인으로 접속을 했을 때 아래와 같이 나오면 성공입니다.</p>
<pre><code>https://{dashboard-host}</code></pre><p><img src="https://velog.velcdn.com/images/dev-lop/post/b123934c-29f6-4c62-b7df-af290ae71720/image.png" alt=""></p>
<h3 id="bearer-token-발급">Bearer token 발급</h3>
<p>Kubernetes Dashboard에 접속할 때 사용할 수 있는 인증 전략은 여러가지가 있습니다.</p>
<p>그 중 아주 기본적이고 간단하게 접속할 수 있는 Bearer token을 수동으로 발급받아 접속하는 방법으로 설명하려고 합니다.</p>
<h4 id="serviceaccount-생성">ServiceAccount 생성</h4>
<pre><code>apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kubernetes-dashboard

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kubernetes-dashboard</code></pre><h4 id="bearer-token-발급-1">Bearer Token 발급</h4>
<pre><code>kubectl -n kubernetes-dashboard create token admin-user</code></pre><pre><code>kubectl -n kubernetes-dashboard create token admin-user
{발급된 Bearer Token 어쩌구}</code></pre><h3 id="접속">접속</h3>
<p>발급된 Bearer Token을 입력 후 접속을 했을 때 아래와 같이 나온다면 성공입니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/b7063be1-f4e2-4545-8e61-06da2fa2f7d5/image.png" alt=""></p>
<h2 id="서비스-등록하기">서비스 등록하기</h2>
<p>아래처럼 서비스를 활성화 및 시작하여 OS가 재부팅될 때 자동으로 실행되게끔 처리합니다.</p>
<pre><code>sudo systemctl enable containerd
sudo systemctl start containerd

sudo systemctl enable kubelet
sudo systemctl start kubelet</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Scheduler]]></title>
            <link>https://velog.io/@dev-lop/Kubernetes-Scheduling</link>
            <guid>https://velog.io/@dev-lop/Kubernetes-Scheduling</guid>
            <pubDate>Tue, 28 Oct 2025 14:48:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번에 Kubernetes 스케줄링 관련 공부를 하면서 알게된 내용을 정리하는 글로서, 간략하게만 정리한 내용을 담고 있습니다.</p>
</blockquote>
<blockquote>
<h3 id="용어-정리"><strong>용어 정리</strong></h3>
</blockquote>
<ul>
<li><strong>ActiveQ</strong>
스케줄링을 할 수 있는 Pod들이 대기하는 큐
새로 생성된 Pod 또는 BackoffQ 또는 unschedulablePods로 분류되었다가 다시 스케줄링을 할 수 있는 상태가 된 Pod가 이에 해당합니다.</li>
<li><strong>BackoffQ</strong>
이전에 스케줄링을 시도했으나 일시적인 이유로 실패하고, 일정 시간동안 스케줄링을 기다리는 Pod들이 대기하는 큐</li>
<li><strong>unschedulablePods</strong>
스케줄링을 시도했으나, taint 등의 이유로 그 어떤 Node에도 배치할 수 없는 Pod들</li>
</ul>
<h1 id="kube-scheduler">kube-scheduler</h1>
<p>kube-scheduler는 Pod를 노드에 할당하는 제어 플레인 프로세스입니다.</p>
<p>새로 생성되었지만 Worker Node에 할당되지 않은 Pod들을 감시하고, 이러한 Pod들을 적합한 Worker Node에 배정하는 역할을 수행합니다.</p>
<h2 id="적합한-worker-node를-찾는-방법-scheduling">적합한 Worker Node를 찾는 방법 (Scheduling)</h2>
<h3 id="watch">Watch</h3>
<p>kube-scheduler는 API 서버를 통해 nodeName 필드가 비어있는 Pod가 존재하는지 주기적으로 확인합니다.</p>
<p>이 때 새로 생성된 Pod를 감지하는 경우, 해당 Pod를 <code>ActiveQ</code>에 등록합니다.</p>
<h3 id="filtering">Filtering</h3>
<p><code>ActiveQ</code>에 등록된 Pod들의 조건(예: CPU/Memory 요청량, Label, Taint 등)을 충족하는 Worker Node를 선별하는 작업입니다.</p>
<p>Pod가 요구하는 Resource를 충족하는 Worker Node가 존재하지 않는 등의 일시적인 이유로 필터링에 실패한 경우, 해당 Pod는 <code>backoffQ</code>에 등록되고 일정 시간(backoff period) 이후 <code>ActiveQ</code>에 재등록되어 스케줄링을 다시 시도합니다.</p>
<p>만약 taint가 불일치하거나, Affinity 조건 불충족 등의 이유로 실패한 경우, 해당 Pod는 <code>unschedulablePods</code>로 분류되고, 이후 Node가 추가되거나 taint가 변경되는 등의 클러스터 이벤트가 발생했을 때 <code>ActiveQ</code>에 등록되며, 스케줄링을 다시 시도합니다.</p>
<h3 id="scoring">Scoring</h3>
<p>Filtering을 통과한 Node들을 대상으로 점수를 부여합니다.</p>
<p>Node의 Resuorce 여유량, 해당 노드에 할당된 Pod 갯수 등 다양한 기준으로 점수를 부여합니다.</p>
<h3 id="binding">Binding</h3>
<p>kube-scheduler는 가장 높은 점수를 가진 Node에 Pod를 배치하며, 만약 동일한 점수를 가진 Node가 2개 이상인 경우 랜덤으로 Node를 선택하여 Pod를 배치하게 됩니다.</p>
<p>Pod의 nodeName 필드에 선택된 Worker Node의 이름을 기록합니다.</p>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://kubernetes.io/ko/docs/concepts/scheduling-eviction/kube-scheduler">Kubernetes Scheduler</a></li>
<li><a href="https://kubernetes.io/docs/concepts/scheduling-eviction">Scheduling, Preemption and Eviction</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[431 Request Header Fields Too Large]]></title>
            <link>https://velog.io/@dev-lop/431-Request-Header-Fields-Too-Large</link>
            <guid>https://velog.io/@dev-lop/431-Request-Header-Fields-Too-Large</guid>
            <pubDate>Sat, 10 Aug 2024 05:30:30 GMT</pubDate>
            <description><![CDATA[<p><code>[431] Request Header Fields Too Large</code> 에러는 요청된 HTTP Header의 크기가 웹 서버가 정의한 크기보다 클 경우 응답되는 Http Response Status Code 중 하나입니다.</p>
<blockquote>
<p><strong>참고!</strong>
비슷한 에러로는 <code>[413] Content Too Large</code>가 있습니다.
다만 <code>NodeJS</code> 기준으로 431 에러와 413 에러를 처리하는 레이어가 다릅니다.</p>
</blockquote>
<h1 id="웹-서버는-왜-http-header-size를-제한하는걸까">웹 서버는 왜 HTTP Header Size를 제한하는걸까?</h1>
<p>기본적으로 HTTP Header 명세에서는 Header 크기에 대해 명시가 되어있지 않습니다. 그럼에도 불구하고 웹 서버가 Header 크기를 제한하는 이유는 무엇일까요?</p>
<p>기본적으로 Client와 웹 서버의 연결은 TCP 방식으로 통신이 이루어집니다. Client가 웹 서버로 HTTP 요청 패킷을 보내면 웹 서버는 해당 패킷을 파싱하여 처리를 하게 됩니다.</p>
<p>이 과정에서 파싱된 패킷 데이터는 웹 서버가 처리할 수 있는 구조로 변환되고, 이 요청에 대해 응답을 할 때 까지 파싱된 패킷 데이터는 웹 서버 메모리에 올라가게 되는데요. 이 때 Client가 요청한 데이터의 크기에 제한이 없다면, 매우 큰 헤더의 요청을 받았을 때 서버의 리소스가 고갈되어 처리 성능이 저하되거나 서비스가 불가능한 상태로 확장될 수 있습니다.</p>
<p>웹 서버는 이러한 문제를 방지하고자 기본적으로 적절한 크기로 HTTP Header의 크기를 제한하고 있습니다.</p>
<blockquote>
<p><strong>참고!</strong>
웹 서버마다 기본으로 설정된 HTTP Header Size가 다릅니다. :)</p>
</blockquote>
<h1 id="일반적으로-431-에러가-발생하는-케이스">일반적으로 431 에러가 발생하는 케이스</h1>
<h2 id="http1x">HTTP/1.x</h2>
<blockquote>
<p><code>HTTP/1.x</code>에서 정의된 Header Field 목록은 <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-14">rfc2616</a>에서 확인할 수 있습니다.</p>
</blockquote>
<p>일반적으로 <code>HTTP/1.x</code>에서 웹 사이트를 이용할 때 영향을 주는 대표적인 헤더 필드는 아래와 같습니다.</p>
<ol>
<li><code>Cookie</code> 크기 증가</li>
<li><code>Referer</code> 길이 증가</li>
<li><code>UserAgent</code> 길이 증가</li>
<li><code>Authorization</code> 헤더의 길이 증가</li>
<li><code>X-Forwarded-For</code> 길이 증가</li>
</ol>
<h2 id="http2">HTTP/2</h2>
<blockquote>
<p><code>HTTP/2</code>로 버전이 올라가면서 변경된 부분은 <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-8.1">rfc7540 - HTTP Request/Response Exchange</a>에서 자세히 확인할 수 있습니다.</p>
</blockquote>
<p>일반적으로 <code>HTTP/2</code>에서 웹 사이트를 이용할 때 영향을 주는 대표적인 헤더 필드는 아래와 같습니다.</p>
<ul>
<li><code>HTTP/1.x</code> 필드 길이 증가</li>
<li><code>Pseudo-Header</code> 길이 증가 (<code>:authority</code>, <code>:path</code>)</li>
</ul>
<h3 id="pseudo-header">Pseudo-Header</h3>
<blockquote>
<p>Pseudo-Header Fields의 자세한 내용은 아래의 링크에서 확인해주세요.
<a href="https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3">https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3</a></p>
</blockquote>
<p><code>HTTP/2</code>에서 Pseudo-Header가 신규로 추가되었는데, 이 헤더는 일반 헤더 블록의 앞에 위치하기 때문에 HTTP Header 크기에 영향을 줍니다.</p>
<blockquote>
<p><strong><a href="https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.1">8.1.2.1.  Pseudo-Header Fields</a></strong>
All pseudo-header fields MUST appear in the header block before regular header fields.</p>
</blockquote>
<hr>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431">https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc2616">https://datatracker.ietf.org/doc/html/rfc2616</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7540">https://datatracker.ietf.org/doc/html/rfc7540</a></li>
<li><a href="https://github.com/nodejs/node/blob/main/src/node_http_parser.cc">https://github.com/nodejs/node/blob/main/src/node_http_parser.cc</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토이프로젝트] 네이버 부동산 크롤러]]></title>
            <link>https://velog.io/@dev-lop/%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EB%8F%99%EC%82%B0-%ED%81%AC%EB%A1%A4%EB%9F%AC</link>
            <guid>https://velog.io/@dev-lop/%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EB%8F%99%EC%82%B0-%ED%81%AC%EB%A1%A4%EB%9F%AC</guid>
            <pubDate>Mon, 15 Jul 2024 04:33:04 GMT</pubDate>
            <description><![CDATA[<p>저는 2023년 10월쯤, 토이프로젝트로 네이버 부동산 크롤러를 만들었습니다.</p>
<p>지금도 제가 원하는 지역의 아파트 시세나 매물 등을 확인하는 용도로 사용을 하고 있는데요.</p>
<p>관련해서 작업했던 기록을 정리해보려고 합니다.</p>
<p>이 글에서는 네이버 부동산 매물 크롤러를 만들 때 설계한 내용을 정리하는 글입니다.</p>
<blockquote>
<p>참고!
네이버는 부동산 매물 관련해서 공식 API를 지원하지 않습니다.</p>
</blockquote>
<p>그래서 네이버 부동산 웹 사이트에서 호출하는 API를 기반으로 매물을 조회해서 가지고 와야 합니다.</p>
<blockquote>
</blockquote>
<p>네이버 부동산 API는 짧은 시간에 대량으로 요청하면 일시적으로 호출이 차단될 수 있으므로 호출량을 적절히 조절하여 호출하는 과정이 필요합니다.</p>
<blockquote>
</blockquote>
<p>관련해서 발생할 수 있는 법적 문제는 본인 책임입니다.</p>
<hr>
<h1 id="github-repository">Github Repository</h1>
<p>Repository: <a href="https://github.com/jissp/naver-land-crawler">링크</a></p>
<p>프로젝트에 대한 간단한 설명은 레포지토리 내 readme.md 파일에 기록해두었습니다.</p>
<hr>
<h1 id="토이-프로젝트의-목적">토이 프로젝트의 목적</h1>
<ol>
<li>NestJS 프레임워크에 익숙해지기</li>
<li>관심이 생긴 부동산 (언제가 될지는 모르겠지만 내집마련의 꿈)</li>
<li>심심풀이 땅콩</li>
</ol>
<hr>
<h1 id="개발-환경">개발 환경</h1>
<ul>
<li>Node.js (with Typescript, NestJS)</li>
<li>MySQL (with TypeOrm)</li>
<li>Redis (with BullJS)</li>
<li>Docker</li>
</ul>
<hr>
<h1 id="네이버-부동산-api-정리">네이버 부동산 API 정리</h1>
<h2 id="cluster-api">Cluster API</h2>
<h3 id="clusterajaxarticlelist">/cluster/ajax/articleList</h3>
<p>부동산 지도 화면에서 매물 목록을 누르면 호출되는 API입니다.</p>
<p>지정한 위도, 경도 좌표 범위 내에 존재하는 부동산 매물의 목록을 조회할 수 있습니다.</p>
<p>그 외 Filter 값을 통해 조회할 매물을 필터할 수 있습니다.</p>
<p><strong>호출 예시:</strong>
<a href="https://m.land.naver.com/cluster/ajax/articleList?rletTpCd=APT%3AOPST&amp;tradTpCd=A1%3AB2&amp;z=19&amp;lat=37.5236987&amp;lon=126.8992539&amp;btm=37.522901&amp;lft=126.8958207&amp;top=37.5244964&amp;rgt=126.9026871&amp;spcMin=33&amp;spcMax=900000000&amp;dprcMax=40000&amp;wprcMax=10000&amp;showR0=&amp;totCnt=41">https://m.land.naver.com/cluster/ajax/articleList?rletTpCd=APT%3AOPST&amp;tradTpCd=A1%3AB2&amp;z=19&amp;lat=37.5236987&amp;lon=126.8992539&amp;btm=37.522901&amp;lft=126.8958207&amp;top=37.5244964&amp;rgt=126.9026871&amp;spcMin=33&amp;spcMax=900000000&amp;dprcMax=40000&amp;wprcMax=10000&amp;showR0=&amp;totCnt=41</a></p>
<h3 id="clusterajaxcomplexlist">/cluster/ajax/complexList</h3>
<p>부동산 지도 화면에서 단지 목록을 누르면 호출되는 API입니다.</p>
<p>지정한 위도, 경도 좌표 범위 내에 존재하는 단지 목록을 조회할 수 있습니다.</p>
<p>호출 시 필요한 QueryString은 <code>/cluster/ajax/articleList</code>와 동일합니다.</p>
<p><strong>호출 예시:</strong>
<a href="https://m.land.naver.com/cluster/ajax/complexList?rletTpCd=APT%3AOPST&amp;tradTpCd=A1%3AB2&amp;z=19&amp;lat=37.5236987&amp;lon=126.8992539&amp;btm=37.522901&amp;lft=126.8958207&amp;top=37.5244964&amp;rgt=126.9026871&amp;spcMin=33&amp;spcMax=900000000&amp;dprcMax=40000&amp;wprcMax=10000&amp;showR0=&amp;totCnt=22&amp;isOnlyIsale=false">https://m.land.naver.com/cluster/ajax/complexList?rletTpCd=APT%3AOPST&amp;tradTpCd=A1%3AB2&amp;z=19&amp;lat=37.5236987&amp;lon=126.8992539&amp;btm=37.522901&amp;lft=126.8958207&amp;top=37.5244964&amp;rgt=126.9026871&amp;spcMin=33&amp;spcMax=900000000&amp;dprcMax=40000&amp;wprcMax=10000&amp;showR0=&amp;totCnt=22&amp;isOnlyIsale=false</a></p>
<h3 id="위도-경도-좌표-범위에-대해서">위도, 경도 좌표 범위에 대해서</h3>
<p>lat는 <code>latitude</code>, lon은 <code>longitude</code>, btm은 <code>bottom</code>, lft는 <code>left</code>, top은 <code>top</code>, rgt는 <code>right</code>의 약자로 위도, 경도의 좌표값을 가지고 있습니다.</p>
<p>위도에 해당하는 필드: <code>lat</code>, <code>top</code>, <code>btm</code>
경도에 해당하는 필드: <code>lon</code>, <code>lft</code>, <code>rgt</code></p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/b75fd680-cc3c-4ba5-a328-107543473e36/image.png" alt=""></p>
<p>위 지도를 기준으로 매물 목록 버튼을 누르면 아래의 QueryString으로 <code>/cluster/ajax/articleList</code> API가 호출됩니다.</p>
<p><strong>QueryString</strong></p>
<pre><code>lat=37.5100191&amp;lon=126.891867&amp;btm=37.5056275&amp;lft=126.8754734&amp;top=37.5144105&amp;rgt=126.9082607</code></pre><p>이 때 각 필드의 의미는 아래와 같습니다.</p>
<p>보여지는 지도의 중앙 좌표: <code>lat</code>, <code>lon</code>
보여지는 지도의 상하좌우 끝단의 좌표: <code>top</code>, <code>lft</code>, <code>rgt</code>, <code>btm</code></p>
<p>따라서 위 지도 기준으로 <code>lat</code>, <code>lon</code> 좌표의 위치는 <code>신도림역</code>이고, <code>lft</code>와 <code>btm</code> 좌표의 위치는 왼쪽 하단의 <code>월드메르디앙 아파트</code>의 위치가 됩니다.</p>
<hr>
<h2 id="front-api">Front API</h2>
<p>네이버 부동산 소스 코드를 뜯어보면 아래와 같이 front-api로 명시된 API 목록을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/e591cab9-fd21-4625-be86-690e5f4298a6/image.png" alt=""></p>
<p>프론트 화면을 구성하기 위해 매물에 대한 추가 정보를 조회하는 API 모음일 것으로 예상하고 있으며, 이 중 몇가지 중요 API를 설명하도록 하겠습니다.</p>
<h3 id="front-apiv1articlekey">/front-api/v1/article/key</h3>
<p>매물과 연관된 Key 정보를 조회할 수 있는 API 입니다.</p>
<p>이 API를 통해서 확인한 데이터는 다른 front-api를 호출할 때 사용됩니다.</p>
<p><strong>요청 예시:</strong>
<a href="https://fin.land.naver.com/front-api/v1/article/key?articleId=2429861377">https://fin.land.naver.com/front-api/v1/article/key?articleId=2429861377</a></p>
<h3 id="front-apiv1articlebasicinfo">/front-api/v1/article/basicInfo</h3>
<p>매물의 상세 정보를 조회할 수 있습니다.</p>
<p>매물 목록 조회 API를 통해서 알 수 있는 정보보다 더 자세한 매물 정보를 조회할 수 있습니다.</p>
<p><strong>요청 예시:</strong>
<a href="https://fin.land.naver.com/front-api/v1/article/basicInfo?articleId=2429861377&amp;realEstateType=A02&amp;tradeType=A1">https://fin.land.naver.com/front-api/v1/article/basicInfo?articleId=2429861377&amp;realEstateType=A02&amp;tradeType=A1</a></p>
<h3 id="front-apiv1complex">/front-api/v1/complex</h3>
<p>매물의 단지 정보를 조회할 수 있습니다.</p>
<p><strong>요청 예시:</strong>
<a href="https://fin.land.naver.com/front-api/v1/complex?complexNumber=127997">https://fin.land.naver.com/front-api/v1/complex?complexNumber=127997</a></p>
<h3 id="front-apiv1complexevstaion-네이버에서-오타낸듯">/front-api/v1/complex/evStaion (네이버에서 오타..낸듯..?)</h3>
<p>단지 내 전기차 충전 시설 정보를 조회할 수 있습니다.</p>
<p><strong>요청 예시:</strong>
<a href="https://fin.land.naver.com/front-api/v1/complex/evStaion?complexNumber=3210">https://fin.land.naver.com/front-api/v1/complex/evStaion?complexNumber=3210</a></p>
<h3 id="front-apiv1articletransport">/front-api/v1/article/transport</h3>
<p>매물 주변의 대중교통 정보를 조회할 수 있습니다.</p>
<p><strong>요청 예시:</strong>
<a href="https://fin.land.naver.com/front-api/v1/article/transport?itemId=2429861377&amp;itemType=article">https://fin.land.naver.com/front-api/v1/article/transport?itemId=2429861377&amp;itemType=article</a></p>
<hr>
<h2 id="rate-limit-">Rate Limit ?</h2>
<p>네이버 부동산 API는 짧은 시간에 많은 요청을 보내면 일정 시간동안 접속이 차단되니 주의하셔야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL Character Set과 Collation]]></title>
            <link>https://velog.io/@dev-lop/MySQL-Character-Set%EA%B3%BC-Collation</link>
            <guid>https://velog.io/@dev-lop/MySQL-Character-Set%EA%B3%BC-Collation</guid>
            <pubDate>Thu, 20 Jun 2024 00:01:57 GMT</pubDate>
            <description><![CDATA[<p>MySQL을 다루면서 알게된 지식을 간단하게 정리하는 글입니다.</p>
<hr>
<p>Character Set과 Collation은 MySQL이 Text 데이터를 관리하는 데 있어서 가장 기본적이고 중요한 요소 중 하나입니다. </p>
<p>하지만 많은 사람들이 무심코 넘어가는 이 녀석들. MySQL의 성능과 데이터 저장 방식, 검색 결과 등에 영향을 끼치는 아주아주 중요한 역할을 한다는 것을 알고 계셨나요?</p>
<p>이 글에서는 Character Set과 Collation에 대해서 간단하게 정리를 해보려고 합니다.</p>
<hr>
<h1 id="character-set">Character Set</h1>
<p>Character Set은 MySQL이 문자열 데이터를 저장하고 처리할 때 사용하는 문자 집합을 의미합니다.</p>
<table>
<thead>
<tr>
<th>Character Set</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>ASCII</td>
<td>기본 영어 알파벳과 숫자, 몇몇 제어 문자로 구성된 <strong>7비트</strong> 문자 집합.<br />최대 128개의 문자를 표현할 수 있습니다.</td>
</tr>
<tr>
<td>latin1 (ISO-8859-1)</td>
<td>대부분의 서유럽 언어를 지원하는 <strong>1 Byte</strong> 문자 집합.<br />최대 256개의 문자를 표현할 수 있습니다.</td>
</tr>
<tr>
<td>utf8 (utf8mb3)</td>
<td><strong>최대 3Byte</strong>의 유니코드 문자를 인코딩할 수 있는 문자 집합.<br />BMP(Basic Multilingual Plane) 내의 문자만 표현할 수 있습니다.</td>
</tr>
<tr>
<td>utf8mb4</td>
<td><strong>최대 4Byte</strong>의 유니코드의 모든 문자를 인코딩할 수 있는 문자 집합. <br />BMP 외의 문자와 이모지까지 표현할 수 있습니다.</td>
</tr>
</tbody></table>
<h2 id="mysql에서-지원하는-character-set-확인하기">MySQL에서 지원하는 Character Set 확인하기</h2>
<p>아래의 쿼리를 통해서 MySQL에서 사용 가능한 Character Set 목록을 확인할 수 있습니다.</p>
<pre><code class="language-sql">SHOW CHARACTER SET;</code></pre>
<h2 id="character-set-관련-변수">Character Set 관련 변수</h2>
<p>이 글에서는 MySQL에서 데이터를 액세스하고, Client와 통신할 때 영향을 받는 변수들만 정리합니다.</p>
<h3 id="character_set_database">character_set_database</h3>
<blockquote>
<p><strong>참고!</strong>
여기에서 말 하는 Database는 <strong>MySQL의 Schema를 의미</strong>합니다.</p>
</blockquote>
<p>Database의 기본 Character Set을 정의합니다.</p>
<p>Database를 생성할 때 Character Set을 명시하지 않으면 이 변수에 설정된 Character Set으로 자동 생성됩니다.</p>
<p>만약 이 변수의 값이 설정되지 않았다면 <code>character_set_server</code> 변수 값을 따릅니다.</p>
<p>해당 변수는 추후 <code>deprecated</code> 될 예정이라고 하니 참고하시기 바랍니다.</p>
<blockquote>
<p><strong>공식 문서 내용</strong>
The global character_set_database and collation_database system variables are deprecated; expect them to be removed in a future version of MySQL.</p>
</blockquote>
<h3 id="character_set_client">character_set_client</h3>
<p>Client가 MySQL 서버로 데이터(SQL 쿼리, Text 데이터 등)를 전송할 때 사용한 Character Set을 지정할 때 사용합니다.</p>
<p>기본적으로 MySQL 서버는 Client가 전송한 데이터의 Character Set을 알 수가 없습니다. 그렇기 때문에 MySQL 서버는 요청이 들어온 Client의 Session 변수(<code>character_set_client</code>)를 참고하여 Character Set을 유추할 수 있습니다.</p>
<p>그렇기 때문에 Client는 MySQL 서버로 데이터를 전송할 때 <code>character_set_client</code> 세션 변수 값을 변경하여 MySQL 서버에게 &#39;나는 이런 Character Set을 사용하여 데이터를 전송했어&#39;라고 알려주는 과정이 중요합니다.</p>
<h3 id="character_set_connection">character_set_connection</h3>
<p>Client가 전송한 데이터를 처리할 때 사용할 Character Set을 설정합니다. </p>
<p>Client가 요청한 데이터의 인코딩을 <code>character_set_client</code> → <code>character_set_connection</code>으로 재인코딩하여 처리를 수행합니다.</p>
<p>기본적으로 이 값은 MySQL 서버의 Character Set과 동일하게 맞춰 사용을 합니다.</p>
<h3 id="character_set_results">character_set_results</h3>
<p>MySQL 서버가 Client로 결과를 반환할 때 사용하는 Character Set을 설정합니다.</p>
<p><code>character_set_client</code> 변수가 Client에서 MySQL 서버로 전송할 때 라면, 이 변수는 MySQL 서버에서 Client로 데이터를 전송할 때 사용하는 Character Set을 의미합니다.</p>
<h2 id="문자열-데이터-type의-길이-측정-방식">문자열 데이터 Type의 길이 측정 방식</h2>
<p>문자열 데이터 Type은 최대로 저장할 수 있는 문자열 길이를 Byte로 관리를 합니다. </p>
<p>아래는 주로 사용하는 Type에 대해 정리한 표 입니다.</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>최대 Byte</th>
<th>문자열 길이 필드</th>
</tr>
</thead>
<tbody><tr>
<td>varchar</td>
<td>255 Byte</td>
<td>1 Byte</td>
</tr>
<tr>
<td>varchar</td>
<td>256 Byte ~ 65,535 Byte</td>
<td>2 Byte</td>
</tr>
<tr>
<td>text</td>
<td>65,535 Byte</td>
<td>2 Byte</td>
</tr>
<tr>
<td>mediumtext</td>
<td>16,777,215 Byte</td>
<td>3 Byte</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody></table>
<p>기본적으로 MySQL은 문자열 데이터를 관리할 때 문자열 길이와 문자열을 조합하여 저장합니다. </p>
<p>예를 들어 <code>Eat</code>라는 문자열을 저장하면 <code>3Eat</code>와 같이 문자열 길이와 데이터가 조합된 형식으로 저장이 되기 때문에 문자열 길이 사이즈를 제외한 Byte만큼 문자열을 저장할 수 있습니다.</p>
<p>그리고 이 제한은 Character Set이 지원하는 문자당 최대 Byte 수에도 영향을 받습니다.</p>
<p>아래는 Character Set이 지원하는 문자당 최대 Byte 수와 <code>한글</code> 문자열을 저장했을 때 실제로 계산되는 길이 Byte를 정리한 표 입니다.</p>
<table>
<thead>
<tr>
<th>Character Set</th>
<th>지원 Byte(최대)</th>
<th>디스크 용량(Byte)</th>
<th>길이 용량(Byte)</th>
</tr>
</thead>
<tbody><tr>
<td>latin1 (ISO-8859-1)</td>
<td>1Byte</td>
<td>4Byte (3Eat 각 1Byte)</td>
<td>3Byte (Eat 각 1Byte)</td>
</tr>
<tr>
<td>utf8 (utf8mb3)</td>
<td>3Byte</td>
<td>4Byte (3Eat 각 1Byte)</td>
<td>9Byte  (Eat 각 3Byte)</td>
</tr>
<tr>
<td>utf8mb4</td>
<td>4Byte</td>
<td>4Byte (3Eat 각 1Byte)</td>
<td>12Byte (Eat 각 4Byte)</td>
</tr>
</tbody></table>
<p>따라서 <code>utf8mb4</code> 기준으로 <code>varchar</code> Type에 저장할 수 있는 문자열은 최대 16,383 글자입니다.</p>
<p>그 근거로 컬럼을 <code>varchar(65535)</code> Type으로 생성하려고 했을 때 <code>utf8mb3</code>와 <code>utf8mb4</code>에서 발생하는 에러 메세지를 출력한 내용입니다.</p>
<ul>
<li><p>** utf8mb3 **</p>
<pre><code>[42000][1074] Column length too big for column &#39;name&#39; (max = 21845); use BLOB or TEXT instead.</code></pre><p>21,845는 <code>varchar</code> Type으로 선언할 수 있는 최대 길이인 65,535에서 3Byte를 나눈 수 입니다.</p>
</li>
<li><p>** utf8mb4 **</p>
<pre><code>[42000][1074] Column length too big for column &#39;name&#39; (max = 16383); use BLOB or TEXT instead.</code></pre><p>16,383은 <code>varchar</code> Type으로 선언할 수 있는 최대 길이인 65,535에서 4Byte를 나눈 수 입니다.</p>
</li>
</ul>
<p>이는 MySQL이 각 Character Set에서 지원하는 최대 Byte에 해당하는 문자열을 가지고 데이터를 저장하는 최악의 시나리오로 가정을 하고 문자열 길이를 계산하기 때문입니다.</p>
<h2 id="인덱스에도-영향을-끼친다">인덱스에도 영향을 끼친다.</h2>
<p>길이를 계산할 때 해당 Character Set에서 지원하는 문자당 최대 Byte를 가지고 계산한다고 말씀드렸는데, 이러한 관리 방법은 인덱스 Key 에서도 동일하게 적용이 됩니다.</p>
<blockquote>
<p><strong>참고!</strong>
The index key prefix length limit is <code>3072 bytes</code> for InnoDB tables that use <code>DYNAMIC</code> or <code>COMPRESSED</code> row format.</p>
</blockquote>
<p>The index key prefix length limit is <code>767 bytes</code> for InnoDB tables that use the <code>REDUNDANT</code> or <code>COMPACT</code> row format. </p>
<blockquote>
</blockquote>
<p>For example, you might hit this limit with a column prefix index of more than 191 characters on a TEXT or VARCHAR column, assuming a utf8mb4 character set and the maximum of 4 bytes for each character.</p>
<p>Character Set을 <code>utf8mb4</code>로 설정하는 경우 실제 Byte보다 더 긴 길이를 사용하는 것으로 판단하기 때문에 하나의 인덱스 페이지에서 관리할 수 있는 인덱스 엔트리 수가 줄어들게 됩니다.</p>
<p>결국 데이터를 조회할 때 더 많은 인덱스 페이지를 탐색해야하기 때문에 Disk I/O가 증가할 수 있습니다.</p>
<h2 id="character-set-casting">Character Set Casting</h2>
<p>SubQuery 또는 Join 조건으로 지정된 컬럼들이 서로 다른 Character Set으로 설정이 되어있다면 MySQL은 데이터 비교를 위해 Character Set 변환이 발생할 수 있습니다.</p>
<p>따라서 <strong>관계를 맺는 컬럼은 동일한 Character Set을 사용하는 것</strong>이 좋습니다.</p>
<blockquote>
<p><strong>예시)</strong>
A 테이블의 code와 B 테이블의 ref_code가 다른 Character Set으로 지정된 상태에서 
<code>B.ref_code IN (SELECT code FROM A ...)</code> 와 같이 사용할 경우 Character Set 캐스팅이 발생하면서 조회 비용이 증가할 수 있습니다.</p>
</blockquote>
<hr>
<h1 id="collation">Collation</h1>
<p>Collation은 Character Set에 대해서 비교 값이나 정렬 순서를 정의한 규칙을 의미합니다.</p>
<p>지금 이 글을 읽고 계신 분에게 &quot;<strong>숫자 1은 숫자 5보다 작은게 맞아?</strong>&quot;라고 물어본다면, 여러분은 &quot;<strong>숫자 1은 숫자 5보다 작은게 맞지!</strong>&quot; 라고 바로 대답을 할 수 있을겁니다. </p>
<p>하지만 &quot;<strong>문자열 A는 문자열 B보다 작은게 맞아?</strong>&quot;라고 물어본다면 여러분은 바로 대답을 하실 수 있으신가요?</p>
<p>개발을 모르는 일반 사람들은 쉽게 대답을 하지 못할 것이고, Ascii 코드를 아는 사람이라면 A와 B를 Ascii 코드로 치환해서 비교를 할 수도 있고, 이 내용을 알고 계신 분들이라면 &quot;Collation 설정에 따라 다르겠지&quot; 라고 생각하실 수 있을겁니다.</p>
<p>이렇게 문자열에 대한 비교는 주체가 누구냐에 따라 기준이 다르기 때문에 쉽게 비교를 할 수 없는 상황이 펼쳐집니다.</p>
<p>MySQL은 이러한 상황을 방지하기 위해 Collation을 기준으로 문자열을 정렬하거나 비교하게 됩니다.</p>
<h2 id="collation-이름-규칙">Collation 이름 규칙</h2>
<p>자세한 내용은 <a href="https://dev.mysql.com/doc/refman/8.0/en/charset-collation-names.html">MySQL 문서</a>를 참고해주세요.</p>
<h3 id="bin">Bin</h3>
<p>바이너리 비교를 수행하는 Collation 입니다.</p>
<p>대소문자를 구분하고, 각 문자의 바이너리 값으로 비교, 정렬을 수행합니다.</p>
<p><code>utf8mb4_bin</code> 과 같은 이름을 가진 Collation을 의미합니다.</p>
<h3 id="ai-accent-insensitive">AI (Accent Insensitive)</h3>
<p>악센트를 무시하고 비교하는 Collation 입니다.</p>
<p>악센트(예: é, è, ê 등)에 대해서 같은 비교 값을 가지기 때문에 <code>eat</code>와 <code>êat</code>는 같은 문자열로 취급합니다.</p>
<p>한글의 경우 <code>가</code>와 <code>ㄱㅏ</code>를 같은 문자열로 취급합니다.</p>
<h3 id="as-accent-sensitive">AS (Accent Sensitive)</h3>
<p>악센트를 구분하여 비교하는 Collation 입니다.</p>
<p>악센트(예: é, è, ê 등)에 대해서 다른 비교 값을 가지기 때문에 <code>eat</code>와 <code>êat</code>는 다른 문자열로 취급합니다.</p>
<p>한글의 경우 <code>가</code>와 <code>ㄱㅏ</code>를 다른 문자열로 취급합니다.</p>
<h3 id="ci-case-insensitive">CI (Case Insensitive)</h3>
<p>대소문자를 구분하지 않고 비교하는 Collation 입니다.</p>
<p>대소문자에 대해서 같은 비교 값을 가지기 때문에 <code>eat</code>와 <code>EAt</code>는 같은 문자열로 취급됩니다.</p>
<h3 id="cs-case-sensitive">CS (Case Sensitive)</h3>
<p>대소문자를 구분하여 비교하는 Collation 입니다.
대소문자에 대해서 다른 비교 값을 가지기 때문에 <code>eat</code>와 <code>EAt</code>는 다른 문자열로 취급됩니다.</p>
<hr>
<h1 id="mysql-기본-character-set--collation">MySQL 기본 Character Set / Collation</h1>
<p>MySQL은 버전별로 Default Character Set과 Collation이 다릅니다.</p>
<table>
<thead>
<tr>
<th>버전</th>
<th>Character Set</th>
<th>Collation</th>
</tr>
</thead>
<tbody><tr>
<td>5.7</td>
<td>latin1</td>
<td>latin1_swedish_ci</td>
</tr>
<tr>
<td>8.0</td>
<td>utf8mb4</td>
<td>utf8mb4_0900_ai_ci</td>
</tr>
<tr>
<td>8.4</td>
<td>utf8mb4</td>
<td>utf8mb4_0900_ai_ci</td>
</tr>
</tbody></table>
<p>보시다시피 MySQL은 기본적으로 대소문자를 구분하지 않는 Collation을 기본 값으로 사용하고 있습니다.</p>
<p>따라서 대소문자를 구분해야하는 환경에서는 Collation을 꼭! 확인하셔야 합니다.</p>
<h2 id="mysql에서-지원하는-collation">MySQL에서 지원하는 Collation</h2>
<pre><code class="language-sql"># Collation 목록 조회
SHOW COLLATION;</code></pre>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/charset.html">https://dev.mysql.com/doc/refman/8.0/en/charset.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/charset-collation-names.html">https://dev.mysql.com/doc/refman/8.0/en/charset-collation-names.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html">https://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/string-type-syntax.html">https://dev.mysql.com/doc/refman/8.0/en/string-type-syntax.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html">https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_character_set_database">https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_character_set_database</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 통계 정보]]></title>
            <link>https://velog.io/@dev-lop/MySQL-%ED%86%B5%EA%B3%84-%EC%A0%95%EB%B3%B4</link>
            <guid>https://velog.io/@dev-lop/MySQL-%ED%86%B5%EA%B3%84-%EC%A0%95%EB%B3%B4</guid>
            <pubDate>Sat, 08 Jun 2024 07:16:14 GMT</pubDate>
            <description><![CDATA[<p>해당 글은 제가 직접 DB 최적화 작업을 진행하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p>MySQL 옵티마이저에 대한 내용은 <a href="https://velog.io/@dev-lop/MySQL-Optimizer">이 글</a>을 확인해주세요.</p>
<hr>
<h1 id="통계-정보">통계 정보</h1>
<p>통계 정보는 MySQL 옵티마이저가 데이터 접근 비용을 정확하게 평가하고 최적의 실행 계획을 선택하는 데 도움을 주는 역할을 합니다.</p>
<h2 id="테이블-통계-정보">테이블 통계 정보</h2>
<h3 id="mysqlinnodb_table_stats">mysql.innodb_table_stats</h3>
<p>InnoDB 테이블에 대해 <strong>테이블 레벨</strong>의 통계 정보를 가지고 있습니다.</p>
<p>MySQL 옵티마이저는 Row 수, 테이블의 크기 등을 참고하여 실행 계획을 세울 수 있습니다.</p>
<h4 id="테이블-구조">테이블 구조</h4>
<table>
<thead>
<tr>
<th>Column name</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>database_name</td>
<td>Database name</td>
</tr>
<tr>
<td>table_name</td>
<td>Table name, partition name, or subpartition name</td>
</tr>
<tr>
<td>last_update</td>
<td>A timestamp indicating the last time that InnoDB updated this row</td>
</tr>
<tr>
<td>n_rows</td>
<td>The number of rows in the table</td>
</tr>
<tr>
<td>clustered_index_size</td>
<td>The size of the primary index, in pages</td>
</tr>
<tr>
<td>sum_of_other_index_sizes</td>
<td>The total size of other (non-primary) indexes, in pages</td>
</tr>
</tbody></table>
<h3 id="mysqlinnodb_index_stats">mysql.innodb_index_stats</h3>
<p>InnoDB 테이블에 대해 <strong>인덱스 레벨</strong>의 통계 정보를 가지고 있습니다.</p>
<p>MySQL 옵티마이저는 인덱스의 크기, 리프 페이지 수 등을 참고하여 실행 계획을 세울 수 있습니다.</p>
<h4 id="테이블-구조-1">테이블 구조</h4>
<table>
<thead>
<tr>
<th>Column name</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>database_name</td>
<td>Database name</td>
</tr>
<tr>
<td>table_name</td>
<td>Table name, partition name, or subpartition name</td>
</tr>
<tr>
<td>index_name</td>
<td>Index name</td>
</tr>
<tr>
<td>last_update</td>
<td>A timestamp indicating the last time the row was updated</td>
</tr>
<tr>
<td>stat_name</td>
<td>The name of the statistic, whose value is reported in the stat_value column</td>
</tr>
<tr>
<td>stat_value</td>
<td>The value of the statistic that is named in stat_name column</td>
</tr>
<tr>
<td>sample_size</td>
<td>The number of pages sampled for the estimate provided in the stat_value column</td>
</tr>
<tr>
<td>stat_description</td>
<td>Description of the statistic that is named in the stat_name column</td>
</tr>
</tbody></table>
<h3 id="통계-정보-갱신">통계 정보 갱신</h3>
<p>통계 정보는 <code>ANALYZE TABLE Statement</code>를 통해 갱신할 수 있으며, 관련 옵션에 따라 자동 갱신되는 방식이 달라집니다.</p>
<table>
<thead>
<tr>
<th>변수</th>
<th>설명</th>
<th>기본 값</th>
</tr>
</thead>
<tbody><tr>
<td><strong>innodb_stats_persistent</strong></td>
<td>통계 정보를 디스크에 유지할지 여부를 지정합니다. OFF인 경우 통계 데이터가 메모리에만 상주합니다.</td>
<td>ON</td>
</tr>
<tr>
<td><strong>innodb_stats_auto_recalc</strong></td>
<td><code>innodb_stats_persistent</code> 옵션이 활성화된 경우, 통계 정보를 자동으로 갱신할지 여부를 지정합니다.</td>
<td>ON</td>
</tr>
</tbody></table>
<h4 id="innodb_stats_persistent이-on-일-때">innodb_stats_persistent이 <code>ON</code> 일 때</h4>
<ul>
<li><strong>테이블에 컬럼이 추가되거나 삭제된 경우</strong></li>
<li><strong>테이블에 인덱스가 추가되거나 삭제된 경우</strong></li>
<li><strong>테이블의 레코드가 대량으로 변경(추가, 수정, 삭제)되는 경우</strong>
  일반적으로 전체 레코드의 10% 정도가 수정되었을 때 갱신되며, <code>innodb_stats_auto_recalc</code> 옵션이 활성화 되어있을 때만 해당됩니다.</li>
</ul>
<blockquote>
<p><strong>참고!</strong>
SQL 실행 계획이 변경되는 것을 방지하기 위해 <code>innodb_stats_auto_recalc</code> 옵션을 <code>OFF</code>하고, DB 점검하는 시간에 수동으로 <code>Analyze Table</code> 명령으로 관리하는 방법으로 관리하는 곳도 많습니다.</p>
</blockquote>
<h4 id="innodb_stats_persistent이-off-일-때">innodb_stats_persistent이 <code>OFF</code> 일 때</h4>
<ul>
<li><code>SHOW TABLE STATUS</code>, <code>SHOW INDEX</code> 명령문을 실행했을 경우</li>
<li>테이블이 처음 Open 된 경우 (<a href="https://velog.io/@dev-lop/MySQL-Connection">참고 페이지</a>)</li>
<li>마지막 통계 업데이트 이후 1/16 정도가 수정됐음이 감지되었을 경우</li>
</ul>
<hr>
<h2 id="히스토그램">히스토그램</h2>
<p>히스토그램은 MySQL 8.0에서 추가된 기능으로, 특정 테이블 컬럼에 대해서 데이터 분포 정보를 정밀하게 관리를 합니다.</p>
<p>MySQL 옵티마이저는 히스토그램을 참조하여 보다 더 정확한 실행 계획을 세울 수 있습니다.</p>
<blockquote>
<p><strong>참고!</strong>
MySQL 옵티마이저는 Index Dives를 통해 Index로 지정된 컬럼의 데이터 분포를 대략적으로 예측할 수 있습니다.</p>
</blockquote>
<p>Index가 걸려있지 않은 컬럼에 히스토그램을 생성하면 정확한 데이터 분포를 알 수 있게 되므로, MySQL 옵티마이저는 효율적으로 실행 계획을 세울 수 있습니다.</p>
<h3 id="히스토그램-확인">히스토그램 확인</h3>
<p>생성된 히스토그램은 아래의 SQL 쿼리로 확인 가능합니다.</p>
<pre><code class="language-sql">SELECT * FROM information_schema.COLUMN_STATISTICS;</code></pre>
<h3 id="히스토그램의-단점">히스토그램의 단점</h3>
<p>히스토그램은 데이터 분포 파악을 위해 테이블의 모든 행을 분석하기 때문에 <code>Full Scan</code>이 발생할 수 밖에 없습니다. </p>
<p>분석 과정에서 CPU 사용량 증가, 디스크 I/O 부하, 쿼리 성능 저하 등이 발생할 수 있기 때문에 사용자가 적은 시간에 <code>수동</code>으로 관리를 해줘야 합니다.</p>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li>Real MySQL 8.0 1권: 개발자와 DBA를 위한 MySQL 실전 가이드</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-optimizer-statistics.html">https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-optimizer-statistics.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-persistent-stats.html">https://dev.mysql.com/doc/refman/8.0/en/innodb-persistent-stats.html</a></li>
<li><a href="https://dev.mysql.com/blog-archive/histogram-statistics-in-mysql">https://dev.mysql.com/blog-archive/histogram-statistics-in-mysql</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL EXPLAIN Statement]]></title>
            <link>https://velog.io/@dev-lop/MySQL-EXPLAIN-Statement</link>
            <guid>https://velog.io/@dev-lop/MySQL-EXPLAIN-Statement</guid>
            <pubDate>Thu, 06 Jun 2024 05:47:15 GMT</pubDate>
            <description><![CDATA[<p>MySQL을 다루면서 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p>MySQL 옵티마이저의 실행 계획에 대한 내용은 <a href="https://velog.io/@dev-lop/MySQL-Optimizer">이 글</a>을 참고하시면 도움이 됩니다.</p>
<hr>
<h1 id="explain-statement">EXPLAIN Statement</h1>
<p>EXPLAIN Statement는 옵티마이저가 결정한 실행 계획에 대한 정보를 제공합니다. </p>
<p>EXPLAIN Statement를 통해서 사용자의 요구 사항(SQL 명령)을 어떤식으로 처리할지(테이블 조인 순서, 인덱스 사용 유무 등)에 대한 정보를 알 수 있으므로 SQL 최적화를 진행할 때 없어서는 안될 중요한 명령문 중 하나입니다.</p>
<p>MySQL 문서에서 설명하는 EXPLAIN Statement의 사용 방법은 아래와 같습니다.</p>
<pre><code class="language-sql">{EXPLAIN | DESCRIBE | DESC}
    tbl_name [col_name | wild]

{EXPLAIN | DESCRIBE | DESC}
    [explain_type]
    {explainable_stmt | FOR CONNECTION connection_id}

{EXPLAIN | DESCRIBE | DESC} ANALYZE [FORMAT = TREE] select_statement</code></pre>
<p><strong>explain_type</strong></p>
<pre><code class="language-sql">explain_type: {
    FORMAT = format_name
}

format_name: {
    TRADITIONAL
  | JSON
  | TREE
}</code></pre>
<p><strong>explainable_stmt</strong></p>
<pre><code class="language-sql">explainable_stmt: {
    SELECT statement
  | TABLE statement
  | DELETE statement
  | INSERT statement
  | REPLACE statement
  | UPDATE statement
}</code></pre>
<hr>
<h2 id="explain">EXPLAIN</h2>
<p>EXPLAIN은 SQL 명령문을 <strong>실제로 실행하지 않고</strong> 실행 계획을 확인하는 방법입니다.</p>
<h3 id="explain-explainable_stmt">EXPLAIN [explainable_stmt]</h3>
<p>미리 작성한 SQL 명령의 실행 계획을 확인하는 방법으로, EXPLAIN을 사용할 때 가장 많이 사용하는 방식입니다.</p>
<p>사용 예시는 아래와 같습니다.</p>
<pre><code class="language-sql">EXPLAIN SELECT * FROM [table] WHERE [where]...;</code></pre>
<h3 id="explain-for-connection-connection_id">EXPLAIN [FOR CONNECTION connection_id]</h3>
<p>현재 연결된 ConnectionID를 전달하여 실행 계획을 확인하는 방법으로, 해당 ConnectionID에서 현재 실행중인 SQL 명령에 대한 실행 계획을 확인할 수 있습니다.</p>
<p>특정 Connection에서 지연이 발생하는 경우 간편하게 실행 계획을 확인할 수 있는 방법 중 하나입니다.</p>
<p>사용 예시는 아래와 같습니다.</p>
<pre><code class="language-sql">EXPLAIN FOR CONNECTION 223;</code></pre>
<h3 id="format">Format</h3>
<p>EXPLAIN 문에서는 <code>TRADITIONAL</code>과 <code>JSON</code> Format만 사용이 가능합니다.</p>
<h4 id="traditional">TRADITIONAL</h4>
<p>explain_type을 명시하지 않았을 때 사용하는 기본 Format 형식입니다.</p>
<p><strong>응답 구조</strong></p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: 1,
    &quot;select_type&quot;: &quot;SIMPLE&quot;,
    &quot;table&quot;: &quot;naver_land_articles&quot;,
    &quot;type&quot;: &quot;ref&quot;,
    &quot;possible_keys&quot;: &quot;naver_land_articles_trad_tp_cd_created_at_index&quot;,
    &quot;key&quot;: &quot;naver_land_articles_trad_tp_cd_created_at_index&quot;,
    &quot;key_len&quot;: &quot;1&quot;,
    &quot;ref&quot;: &quot;const&quot;,
    &quot;rows&quot;: 33109,
    &quot;Extra&quot;: &quot;Using where&quot;
  }
]</code></pre>
<h4 id="json">JSON</h4>
<p><strong>응답 구조</strong></p>
<pre><code class="language-json">{
  &quot;query_block&quot;: {
    &quot;select_id&quot;: 1,
    &quot;table&quot;: {
      &quot;table_name&quot;: &quot;naver_land_articles&quot;,
      &quot;access_type&quot;: &quot;ref&quot;,
      &quot;possible_keys&quot;: [&quot;naver_land_articles_trad_tp_cd_created_at_index&quot;],
      &quot;key&quot;: &quot;naver_land_articles_trad_tp_cd_created_at_index&quot;,
      &quot;key_length&quot;: &quot;1&quot;,
      &quot;used_key_parts&quot;: [&quot;trad_tp_cd&quot;],
      &quot;ref&quot;: [&quot;const&quot;],
      &quot;rows&quot;: 33106,
      &quot;filtered&quot;: 100,
      &quot;attached_condition&quot;: &quot;crawler.naver_land_articles.trad_tp_cd &lt;=&gt; &#39;A1&#39; and crawler.naver_land_articles.trad_tp_cd = &#39;A1&#39; and crawler.naver_land_articles.region1 in (&#39;경기도&#39;,&#39;서울&#39;)&quot;
    }
  }
}</code></pre>
<h3 id="응답-필드">응답 필드</h3>
<p>자세한 응답 필드에 대한 설명은 <a href="https://dev.mysql.com/doc/refman/8.0/en/explain-output.html">해당 문서</a>를 참고해주세요.</p>
<hr>
<h2 id="explain-analyze-select_statement">EXPLAIN ANALYZE [select_statement]</h2>
<p>EXPLAIN ANALYZE는 MySQL 8.0.18부터 도입된 기능으로, <strong>쿼리를 실제로 실행하면서</strong> 실행 계획과 실행 결과를 확인하는 방법입니다.</p>
<p>EXPLAIN 과는 다르게 구체적인 성능 문제를 진단하는데 사용을 합니다.</p>
<p><strong>요청 SQL문</strong></p>
<pre><code class="language-sql">EXPLAIN ANALYZE SELECT * FROM [table] WHERE [where]...;</code></pre>
<h3 id="format-1">Format</h3>
<p>EXPLAIN ANALYZE에서는 <code>TREE</code> Format만 사용이 가능합니다.</p>
<h4 id="tree">TREE</h4>
<p><strong>응답 구조</strong></p>
<pre><code>-&gt; Sort: orders.created_at DESC  (cost=16.14 rows=43) (actual time=5.157..5.157 rows=0 loops=1)
    -&gt; Filter: (orders.market_code = &#39;smartstore&#39;)  (actual time=5.149..5.149 rows=0 loops=1)
        -&gt; Index lookup on orders using searchRule1 (store_uuid=&#39;어쩌구&#39;)  (actual time=4.957..5.137 rows=43 loops=1)</code></pre><hr>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/explain.html">https://dev.mysql.com/doc/refman/8.0/en/explain.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/explain-output.html">https://dev.mysql.com/doc/refman/8.0/en/explain-output.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL Optimizer]]></title>
            <link>https://velog.io/@dev-lop/MySQL-Optimizer</link>
            <guid>https://velog.io/@dev-lop/MySQL-Optimizer</guid>
            <pubDate>Tue, 04 Jun 2024 05:01:22 GMT</pubDate>
            <description><![CDATA[<p>해당 글은 제가 직접 DB 최적화 작업을 진행하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p><strong>통계 정보</strong>에 대해서는 <a href="https://velog.io/@dev-lop/MySQL-%ED%86%B5%EA%B3%84-%EC%A0%95%EB%B3%B4">이 글</a>을 확인해주세요.</p>
<hr>
<h1 id="mysql의-optimizer">MySQL의 Optimizer</h1>
<p>MySQL 옵티마이저는 DB의 성능을 결정짓는 중요한 요소 중 하나로, 사용자가 요청한 SQL 명령을 가장 효율적으로 실행하기 위한 최적의 방법을 찾기 위해 다양한 실행 계획을 세우고 평가합니다.</p>
<p>최적의 실행 계획이 수립되면 쿼리 실행 시간을 단축시키고 시스템 리소스를 절약하는 데 크게 기여합니다. </p>
<p>옵티마이저는 최적의 실행 계획을 평가하기 위해 <code>규칙 기반</code>과 <code>비용 기반</code> 최적화 방식을 사용합니다. 다만 <code>규칙 기반</code>의 최적화는 요즘에는 잘 사용되지 않는 방식이라 해당 글에서 다루지 않습니다.</p>
<blockquote>
<p><strong>참고!</strong>
MySQL 옵티마이저는 모든 상황을 계획하지 않습니다. </p>
</blockquote>
<p>그 이유는 실행 계획을 세우는 시간이 길어질 수록 SQL 쿼리 수행 시간에 영향을 끼치기 때문입니다.</p>
<blockquote>
</blockquote>
<p>MySQL 옵티마이저가 SQL 명령문을 잘 이해하고 최적의 실행 계획을 세울 수 있게 <strong>✨SQL 명령문을 효율적으로 작성하는 것✨</strong>이 중요합니다.</p>
<h2 id="optimizer-동작-제어">Optimizer 동작 제어</h2>
<p>다양한 방법을 통해 MySQL 옵티마이저의 동작 방식을 제어할 수 있습니다. 동작 방식 중 자주 사용되는 동작 제어 방식 중 하나인 INDEX 힌트에 대해서</p>
<p>자세한 내용은 <a href="https://dev.mysql.com/doc/refman/8.0/en/controlling-optimizer.html">문서</a>를 통해 확인할 수 있습니다.</p>
<h3 id="index-힌트">Index 힌트</h3>
<p>MySQL 옵티마이저에게 사용하거나 제외할 인덱스 목록을 전달할 수 있습니다.</p>
<pre><code class="language-sql">tbl_name [[AS] alias] [index_hint_list]

index_hint_list:
    index_hint [index_hint] ...

index_hint:
    USE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list])
  | {IGNORE|FORCE} {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

index_list:
    index_name [, index_name] ...</code></pre>
<h4 id="use-index">USE INDEX</h4>
<p>명명된 인덱스 중 하나를 사용하도록 &#39;<strong>제안</strong>&#39;합니다.</p>
<p>옵티마이저가 명명된 인덱스가 아닌 다른 인덱스가 더 효율적이라고 판단을 하면, 다른 인덱스를 사용하는 방법으로 실행 계획을 수립합니다.</p>
<h4 id="force-index">FORCE INDEX</h4>
<p>명명된 인덱스 중 하나를 사용하도록 &#39;<strong>강제</strong>&#39;합니다. </p>
<p>옵티마이저가 명명된 인덱스가 아닌 다른 인덱스가 더 효율적이라 판단을 하더라도 명명된 인덱스 중 하나를 사용하여 실행 계획을 수립합니다.</p>
<h4 id="ignore-index">IGNORE INDEX</h4>
<p>명명된 인덱스를 사용하지 않도록 &#39;<strong>강제</strong>&#39;합니다.</p>
<hr>
<h1 id="비용-기반-최적화">비용 기반 최적화</h1>
<p>비용 기반 최적화는 각 실행 계획에 대해 예상 비용을 계산하고, 비용이 적게드는 실행 계획을 선택하는 방식입니다. </p>
<p>옵티마이저는 예상 비용을 계산할 때 <a href="https://velog.io/@dev-lop/MySQL-%ED%86%B5%EA%B3%84-%EC%A0%95%EB%B3%B4">통계 정보</a>를 활용하여 실행 계획을 평가합니다.</p>
<blockquote>
<p><strong>참고!</strong>
통계 정보가 정확하지 않으면 옵티마이저는 실행 계획을 효율적으로 평가할 수 없습니다.</p>
</blockquote>
<p>예) 테이블에 100,000건의 데이터가 존재하는데 통계 정보는 갱신되지 않아 10건의 데이터만 있다고 한다면, MySQL 옵티마이저는 <code>Full Scan</code>을 이용하는 방향으로 실행 계획을 세울 수 있습니다.</p>
<h2 id="코스트-모델cost-model">코스트 모델(Cost Model)</h2>
<p>MySQL 옵티마이저가 실행 계획에 대해 예상 비용을 계산할 때 필요한 단위 작업들의 비용을 의미합니다.</p>
<p>MySQL 8.0 기준으로 코스트 모델에서 지원하는 단위 작업은 아래와 같습니다.</p>
<h3 id="engine_cost"><strong>engine_cost</strong></h3>
<p>레코드를 가진 데이터 페이지를 가져오는데 필요한 비용을 관리합니다.</p>
<table>
<thead>
<tr>
<th>cost</th>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>engine_cost</td>
<td>io_block_read_cost</td>
<td>디스크 데이터 페이지 읽기</td>
</tr>
<tr>
<td>engine_cost</td>
<td>memory_block_read_cost</td>
<td>메모리 데이터 페이지 읽기</td>
</tr>
</tbody></table>
<h3 id="server_cost"><strong>server_cost</strong></h3>
<p>인덱스를 찾고, 레코드를 비교하고, 임시 테이블 처리에 대한 비용을 관리합니다.</p>
<table>
<thead>
<tr>
<th>cost</th>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>server_cost</td>
<td>disk_temptable_create_cos</td>
<td>디스크 임시 테이블 생성</td>
</tr>
<tr>
<td>server_cost</td>
<td>disk_temptable_row_cost</td>
<td>디스크 임시 테이블의 레코드 읽기</td>
</tr>
<tr>
<td>server_cost</td>
<td>key_compare_cost</td>
<td>인덱스 키 비교</td>
</tr>
<tr>
<td>server_cost</td>
<td>memory_temptable_crate_cost</td>
<td>메모리 임시 테이블 생성</td>
</tr>
<tr>
<td>server_cost</td>
<td>memory_temptable_row_cost</td>
<td>메모리 임시 테이블의 레코드 읽기</td>
</tr>
<tr>
<td>server_cost</td>
<td>row_evaluate_cost</td>
<td>레코드 비교</td>
</tr>
</tbody></table>
<hr>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li>Real MySQL 8.0 1권: 개발자와 DBA를 위한 MySQL 실전 가이드</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-optimizer-statistics.html">https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-optimizer-statistics.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/cost-model.html">https://dev.mysql.com/doc/refman/8.0/en/cost-model.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/switchable-optimizations.html">https://dev.mysql.com/doc/refman/8.0/en/switchable-optimizations.html</a></li>
<li><a href="https://dev.mysql.com/blog-archive/optimization-to-skip-index-dives-with-force-index/">https://dev.mysql.com/blog-archive/optimization-to-skip-index-dives-with-force-index/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 동시성 제어]]></title>
            <link>https://velog.io/@dev-lop/MySQL-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@dev-lop/MySQL-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Mon, 03 Jun 2024 11:23:40 GMT</pubDate>
            <description><![CDATA[<p>제가 2018년 1월부터 2021년 9월까지, 총 3년 9개월을 다녔던 회사에서 근무하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p>최종적으로 낙관적 동시성 제어 방식을 활용하여 <strong>재고 수량을 초과하여 주문이 되는 문제</strong>를 해결하였습니다.</p>
<p>동시성 제어는 순수 MySQL만을 이용해서 제어하는 방법과 Redis 등 Application의 도움을 받아 제어하는 등 다양한 방법과 상황이 존재합니다.</p>
<p>이 글은 MySQL만을 이용해서 동시성을 제어하는 방법에 대해서 정리합니다.</p>
<blockquote>
<p><strong>참고!</strong>
아래의 예시 플로우차트는... 2018년도에 대충 PPT로 그린 플로우차트입니다.
즉, 레거시한 이미지 입니다...</p>
</blockquote>
<hr>
<h1 id="동시성-제어의-필요성">동시성 제어의 필요성</h1>
<p>개발을 하다보면 동시성을 제어해야만 하는 상황이 생깁니다. 대표적으로 상품을 구매했을 때 재고를 차감하는 로직이 그러합니다.</p>
<p>일반적으로 구매자가 상품을 구매하면 아래와 같은 순서로 처리가 이루어집니다.</p>
<ol>
<li>상품을 구매할 수 있는 상태인지 확인합니다.</li>
<li>구매할 수 있는 상품이라면, 구매자를 주문서 작성 페이지로 이동시킵니다.</li>
<li>구매자가 주문 정보(구매자 정보, 배송지 정보, 할인 수단 등)를 입력 후 결제를 시도합니다.</li>
<li>시스템은 결제 페이지로 넘기기 전에 구매자가 입력한 주문서 정보를 DB에 저장하고, 상품의 재고가 유효한지 확인합니다.</li>
<li>상품의 재고가 유효하면 구매자가 구매한 수량만큼 재고를 차감합니다.</li>
<li>재고 차감이 성공하면 구매자를 결제 페이지(예: PG 화면 등)로 이동시킵니다.</li>
<li>결제가 성공하면 주문이 완료됩니다.</li>
</ol>
<p>위 과정만 본다면 로직에는 별 문제가 없어보입니다. 하지만 같은 시간에 여러 사용자가 같은 상품을 구매하면 어떻게 될까요?</p>
<p>아래는 재고를 차감하는 로직에 대해서 A, B 사용자가 동시에 요청을 했을 때 발생하는 상황을 나타냅니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/a0b787f6-1ed1-4329-aa56-7cb16dac9748/image.png" alt=""></p>
<p>상품의 재고가 5개만 남아있는 상황에서 A 사용자는 5개, B 사용자는 3개를 구매하기를 원합니다.</p>
<p>이 두 사용자가 같은 시간에 구매를 시도할 경우 A 사용자의 트랜잭션이 재고를 반영하기 전에 B 사용자의 트랜잭션에서 상품의 재고를 조회하기 때문에 A와 B 둘 다 재고가 유효하다고 판단을 하게 됩니다. </p>
<p>따라서 상품의 재고는 실제로 5개밖에 존재하지 않지만 Application 로직의 동시성 문제로 두 사용자 모두 정상적으로 구매가 이루어지게 되며, 상품의 재고는 결과적으로 -3개가 되는 상황이 발생하게 됩니다.</p>
<p>이런식으로 동시성을 제어하지 않은 로직은 의도하지 않은 결과를 초래할 수 있습니다.</p>
<h1 id="동시성-제어-방식">동시성 제어 방식</h1>
<h2 id="비관적-동시성-제어-locking-reads-이용">비관적 동시성 제어 (Locking Reads 이용)</h2>
<p>비관적 동시성 제어 방식은 MySQL의 잠금을 이용해서 동시성 문제를 해결하는 방법입니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/3dc44d85-5ceb-4ba5-896f-a93251c98029/image.png" alt=""></p>
<p>잠금이 발생하면 다른 트랜잭션은 잠금이 해제될 때 까지 데이터에 접근할 수가 없으므로 처리량(성능)이 떨어진다는 단점이 있지만, 처리 과정 중 실패나 에러가 발생할 경우 데이터를 Rollback하기가 편하다는 장점이 있습니다.</p>
<h3 id="select--for-share"><strong>SELECT ... FOR SHARE</strong></h3>
<p>읽기는 허용하되 수정은 불가능하게 하는 잠금 방식입니다.</p>
<h3 id="select--for-update"><strong>SELECT ... FOR UPDATE</strong></h3>
<p>읽기와 수정 둘 다 불가능하게 하는 잠금 방식입니다.</p>
<p>해당 방식은 UPDATE 쿼리를 날렸을 때와 동일한 잠금 방식입니다.</p>
<hr>
<h2 id="낙관적-동시성-제어">낙관적 동시성 제어</h2>
<blockquote>
<p><strong>참고!</strong>
일반적으로 MySQL은 트랜잭션과 격리 수준, 그리고 잠금 매커니즘을 사용하여 데이터의 순차성과 일관성을 보장하기 때문에, 동시 다발적으로 UPDATE 쿼리가 실행되어도 문제가 발생하지 않습니다</p>
</blockquote>
<p>낙관적 동시성 제어는 MySQL의 잠금을 이용하지 않고, UPDATE 쿼리에 조건을 지정하여 쿼리 성공 유무와 적용된 행의 개수로 동시성을 제어하는 방식으로, 비관적 동시성 제어와 다르게 처리량(성능) 저하가 발생하지 않습니다. </p>
<p>대신 처리 과정에서 실패나 에러가 발생할 경우 데이터를 Rollback 하기가 어렵다는 단점이 있습니다.</p>
<p>아래는 낙관적 동시성 제어에 대한 예시입니다.</p>
<ol>
<li>Update 쿼리 요청을 보냅니다.<pre><code class="language-sql"> UPDATE TABLE SET stock=stock-3 WHERE stock &gt;= 3; -- 재고가 3개 이상일 때만 차감</code></pre>
</li>
<li>Application에서 MySQL 서버가 응답한 데이터(쿼리 성공 여부와 적용된 행)를 확인합니다.</li>
<li>만약 쿼리가 성공됐고 적용된 행이 존재할 경우 성공에 관련된 처리를 진행합니다.</li>
<li>만약 쿼리가 실패됐거나, 성공했지만 적용된 행이 존재하지 않을 경우 실패로 간주하고 처리를 합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slow Read Attack]]></title>
            <link>https://velog.io/@dev-lop/Slow-Read-Attack-And-PHP-FPM</link>
            <guid>https://velog.io/@dev-lop/Slow-Read-Attack-And-PHP-FPM</guid>
            <pubDate>Mon, 03 Jun 2024 04:49:42 GMT</pubDate>
            <description><![CDATA[<p>제가 2018년 1월부터 2021년 9월까지, 총 3년 9개월을 다녔던 회사에서 근무하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p>아래의 글을 참고하시면 이해하시는데 도움이 됩니다.</p>
<ul>
<li><a href="https://velog.io/@dev-lop/Linux-Socket%EA%B3%BC-LocalPort">Linux에서 Socket과 LocalPort</a></li>
<li><a href="https://velog.io/@dev-lop/PHP-FPM%EA%B3%BC-LocalPort">PHP-FPM과 LocalPort 문제</a></li>
</ul>
<hr>
<h1 id="slow-read-attack">Slow Read Attack</h1>
<blockquote>
<p><strong>참고!</strong>
<strong>TCP Window Size</strong>는 패킷을 전송하는 사람이 보낼 수 있는 최대 Size를 의미합니다.</p>
</blockquote>
<p>Slow Read 공격은 HTTP 공격의 일종으로 <code>TCP Window Size</code> 등을 조작하여 서버에게 데이터를 천천히 보내고, 응답을 천천히 읽어 TCP 연결을 지연시키는 공격 기법입니다.</p>
<p>공격자는 <code>TCP Window Size</code> 를 작게 조작하고, 데이터 처리율을 감소시킨 상태에서 HTTP 패킷을 송신하여, 웹 서버가 정상적으로 응답하지 못하게 함으로써 Dos 상태를 유발시킵니다.</p>
<p>공격 대상이 된 서버는 공격자와의 데이터 전송이 완료될 때 까지 Connection 자원을 점유하게 되기 때문에, 대량 발생할 경우 Connection 자원이 고갈되어 장애가 발생합니다.</p>
<hr>
<h1 id="php-fpm과-slow-read-attack">PHP-FPM과 Slow Read Attack</h1>
<p>PHP-FPM 특성 상 Slow Read Attack 공격을 받으면 아래의 문제가 발생하게 됩니다.</p>
<ol>
<li>모든 Worker 프로세스가 해당 공격을 처리하기 위해 사용되면서 정상적인 요청을 처리할 수 없는 상태로 만듭니다.</li>
<li>그리고 외부 서비스와 연결하기 위해 생성된 TCP 소켓으로 인해 LocalPort가 고갈되기 쉽습니다.
 이 과정에서 외부 서비스의 Connection도 점유하여 장애를 전파시킬 수 있습니다.</li>
</ol>
<hr>
<h1 id="실제-공격이-들어온-사례">실제 공격이 들어온 사례</h1>
<p>```
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 99.557, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 163.487, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/82.0&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 78.148, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/82.0&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 91.221, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/82.0&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 76.058, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPhone; CPU iPhone OS 10_15_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/29.0 Mobile/15E148 Safari/605.1.15&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 85.225, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPod; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 86.266, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.185 Mobile Safari/537.36&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 124.046, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 163.626, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPad; CPU OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 163.414, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 126.826, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPod touch; CPU iPhone 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1&quot;}
{&quot;time&quot;: &quot;2020-11-08T13:31:48+09:00&quot;, &quot;request_host&quot;: &quot;http://도메인은비공개.com&quot;, &quot;request_uri&quot;: &quot;http://도메인은비공개.com/&quot;, &quot;body_bytes_sent&quot;: 0, &quot;request_time&quot;: 163.380, &quot;status&quot;: 408, &quot;request&quot;: &quot;GET / HTTP/1.1&quot;, &quot;request_method&quot;: &quot;GET&quot;, &quot;referrer&quot;: &quot;-&quot;, &quot;user_agent&quot;: &quot;Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1&quot;}
...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PHP-FPM과 LocalPort 문제]]></title>
            <link>https://velog.io/@dev-lop/PHP-FPM%EA%B3%BC-LocalPort</link>
            <guid>https://velog.io/@dev-lop/PHP-FPM%EA%B3%BC-LocalPort</guid>
            <pubDate>Sun, 02 Jun 2024 06:48:56 GMT</pubDate>
            <description><![CDATA[<p>제가 2018년 1월부터 2021년 9월까지, 총 3년 9개월을 다녔던 회사에서 근무하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<p>TCP 통신과 Linux Socket 관련 내용은 <a href="https://velog.io/@dev-lop/Linux-Socket%EA%B3%BC-LocalPort">여기</a>에서 확인해주세요.</p>
<hr>
<h1 id="php-fpm-php-fastcgi-process-manager">PHP-FPM (PHP FastCGI Process Manager)</h1>
<h3 id="cgi-common-gateway-interface">CGI (Common Gateway Interface)</h3>
<p>웹서버와 외부 프로토콜을 연결시켜주는 표준 프로토콜로서, 기본적으로 Request 요청별로 프로세스를 생성하여 처리하는 방식을 의미합니다.</p>
<h3 id="fastcgi">FastCGI</h3>
<p>Request가 발생할 때 마다 프로세스를 생성하여 처리하고, 프로세스를 종료하는 CGI의 단점을 개선한 방식으로, Request 요청을 처리할 프로세스를 미리 만들어두고 재사용하는 방식으로 처리를 합니다.</p>
<h3 id="php-fpm">PHP-FPM</h3>
<p>php-fpm은 PHP FastCGI Process Manager의 약자로서, PHP 스크립트를 실행하는 FastCGI 서버로 동작합니다.</p>
<p>php-fpm은 Worker 프로세스를 관리하는 Master 프로세스와 PHP 스크립트를 실행하는 Worker 프로세스로 구분되며, Master 프로세스는 웹 서버(예: Nginx, Apache 등)로부터 FastCGI 요청을 수신받아 Worker 프로세스로 전달하는 역할도 합니다.</p>
<blockquote>
<p><strong>참고!</strong>
php-fpm은 <code>max_children</code> 옵션을 통해 미리 만들어둘 프로세스의 수량을 지정할 수 있습니다.
메모리 누수 등의 문제를 최소화하기 위해 <code>max_requests</code> 옵션도 제공합니다.</p>
</blockquote>
<p>그 외 <a href="https://www.php.net/manual/en/install.fpm.configuration.php">FastCGI 동작에 관련된 많은 설정</a>을 제공하고 있습니다.</p>
<hr>
<h1 id="php-fpm과-localport의-문제">PHP-FPM과 LocalPort의 문제</h1>
<p>php-fpm은 일반적으로 Server의 역할을 수행하기 때문에 LocalPort를 고갈시키지 않습니다. </p>
<p>하지만 소스 코드 내에 MySQL, Redis 등 외부 서비스로 연결하는 로직이 포함되면 이야기가 달라집니다. </p>
<p>PHP는 MySQL, Redis 등 외부 서비스로 연결하는 과정에서 TCP 소켓을 생성하게 되는데, 이 과정에서 PHP는 커널에게 LocalPort을 요청하게 되고, 이후 외부 서비스와의 연결을 Disconnect하는 과정에서 소켓의 상태를 <code>TIME_WAIT</code>로 변경합니다. </p>
<blockquote>
<p><strong>참고!</strong>
<code>TIME_WAIT</code> 상태를 갖는 소켓은 패킷 유실 등의 처리를 위해 약 1분동안 대기한 다음 연결을 종료하게 됩니다.</p>
</blockquote>
<p>여기서 중요한 점은 php-fpm은 Request 단위로 처리를 한다는 점입니다. </p>
<p>만약 1분도 안되는 시간에 트래픽(사용자의 요청)이 급격하게 증가하면 <code>TIME_WAIT</code> 상태를 갖는 소켓으로 인해 리눅스의 LocalPort가 고갈되어 서비스에 장애가 발생할 수 있습니다.</p>
<blockquote>
<p><strong>참고!</strong>
소켓은 TCP 통신을 위해 생성된 File Descriptor를 의미합니다. 따라서 Process별로 가질 수 있는 File Descriptor 설정에도 영향을 받습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리눅스에서 TCP 소켓과 LocalPort]]></title>
            <link>https://velog.io/@dev-lop/Linux-Socket%EA%B3%BC-LocalPort</link>
            <guid>https://velog.io/@dev-lop/Linux-Socket%EA%B3%BC-LocalPort</guid>
            <pubDate>Sat, 01 Jun 2024 14:48:45 GMT</pubDate>
            <description><![CDATA[<p>제가 2018년 1월부터 2021년 9월까지, 총 3년 9개월을 다녔던 회사에서 근무하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 간단하게 정리하는 글입니다.</p>
<hr>
<h1 id="사전-지식">사전 지식</h1>
<h3 id="tcpip의-4-tuple">TCP/IP의 4-Tuple</h3>
<p>TCP 통신을 위해 사용되는 소켓은 <code>Source IP</code>, <code>Source Port</code>, <code>Destination IP</code>, <code>Destination Port</code> 4개의 Tuple을 기준으로 생성이 됩니다.</p>
<p>예를 들어 MySQL 서버에 연결을 한다고 가정하면, TCP 통신을 위한 소켓의 Header는 아래와 같이 구성됩니다.</p>
<pre><code>Source IP: Client IP
Source Port: Client Port
Destination IP: MySQL 서버 IP
Destination Port: 3306</code></pre><h3 id="localport">LocalPort</h3>
<p>TCP/IP 프로토콜 스택에서 Port 필드는 16비트의 크기를 가집니다. 하지만 0번 포트는 실제로 사용되지 않기 때문에 1~65535개의 범위를 갖습니다.</p>
<p>따라서 LocalPort는 우리가 일반적으로 알고있는 1~65535까지의 포트를 의미한다고 보시면 됩니다.</p>
<h3 id="socket">Socket</h3>
<p>Unix 혹은 Unix 계열의 운영체제(예: 리눅스)에서 Socket은 TCP/UDP 또는 unix 방식 등의 통신을 위해 생성된 File Descriptor를 의미합니다.</p>
<p>그렇기 때문에 File Descriptor 제한에 영향을 받습니다.</p>
<hr>
<h1 id="tcp-소켓과-localport의-관계">TCP 소켓과 LocalPort의 관계</h1>
<h3 id="server">Server</h3>
<p>Server는 Client와 통신을 하기 위해 아래의 절차를 거칩니다.</p>
<ol>
<li><strong>socket 생성</strong></li>
<li><strong>socket에 IP 주소와 Port를 설정 (bind)</strong>
 이 단계에서 Server는 생성된 socket에 INADDR_ANY(0.0.0.0)와 지정된 LocalPort 정보를 설정합니다.
 <img src="https://velog.velcdn.com/images/dev-lop/post/cdb53e2b-b198-4845-b569-3e81e774dcdf/image.png" alt="">
 만약 bind 하려는 Port가 이미 사용중이라면 에러가 발생합니다.</li>
<li><strong>수신 대기 (listen)</strong></li>
<li><strong>연결 수락 (accept)</strong>
 이 단계에서 Server는 동시성, 병렬성, 독립성 등의 이유로 해당 Client와 1:1 통신을 위한 Socket을 새로 생성합니다.</li>
</ol>
<p>Server는 TCP 통신을 위한 소켓 정보(Client 정보와 bind 된 LocalPort)를 이미 알기 때문에 bind 시점에서만 LocalPort를 소비하고, 연결을 수락(accept) 과정에서 생성되는 Socket은 LocalPort를 고갈시키지 않습니다.</p>
<h3 id="client">Client</h3>
<p>Client는 Server와 통신을 하기 위해 아래의 절차를 거칩니다.</p>
<ol>
<li><strong>socket 생성</strong></li>
<li><strong>커널에게 LocalPort를 요청합니다.</strong>
 socket을 생성하는 시점의 Client는 <code>Source Port</code>을 알지 못합니다. 그렇기 때문에 Client는 커널에게 사용 가능한 LocalPort를 요청하여 배정받아야 합니다.
 이 단계에서 Client는 LocalPort를 소비하게 됩니다.</li>
<li><strong>socket에 Source IP, Port와 Destination IP, Port를 설정합니다.</strong></li>
<li><strong>TCP 통신을 시작합니다. (connect)</strong></li>
</ol>
<p>Client는 Server와의 통신을 위해 고유한 LocalPort를 사용해야 하기 때문에 LocalPort와 Socket은 1:1 관계를 가집니다. 따라서 동시에 통신하는 TCP 소켓이 많아질 수록 소비하는 LocalPort도 많아집니다.</p>
<h3 id="time_wait">TIME_WAIT</h3>
<blockquote>
<p>Socket이 <code>TIME_WAIT</code> 상태를 가지면 일정 시간동안 LocalPort를 반환하지 않고 점유하기 때문에 TCP 소켓 생성이 잦은 Client는 LocalPort 고갈 또는 리소스 부족으로 장애가 발생할 수 있습니다.</p>
</blockquote>
<p>TCP 통신에서 연결 종료 절차는 아래와 같습니다.</p>
<ol>
<li><strong>송신자가 수신자에게 FIN 메세지를 보냅니다.</strong></li>
<li><strong>수신자는 송신자에게 ACK 메세지를 보냅니다.</strong></li>
<li><strong>수신자는 송신자에게 FIN 메세지를 보냅니다.</strong></li>
<li><strong>송신자는 수신자에게 ACK 메세지를 보냅니다.</strong></li>
</ol>
<p>위 4번 단계에서 수신자는 송신자가 보낸 ACK 메세지를 받지 못하는 상황이 생길 수 있습니다.</p>
<p>이러한 문제를 해결하기 위해 송신자의 Socket은 <code>TIME_WAIT</code> 상태를 가지고 일정시간을 대기한 다음에 연결을 종료합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL Connection 정리]]></title>
            <link>https://velog.io/@dev-lop/MySQL-Connection</link>
            <guid>https://velog.io/@dev-lop/MySQL-Connection</guid>
            <pubDate>Fri, 31 May 2024 15:11:18 GMT</pubDate>
            <description><![CDATA[<p>Too many connection 에러는 서비스를 운영하면서 자주 겪게되는 오류 중 하나입니다.</p>
<p>지금까지 개발을 하면서 직접 경험한 내용을 바탕으로 공부하고 알게된 지식을 정리하는 글입니다.</p>
<hr>
<h1 id="foreground-thread">Foreground Thread</h1>
<p>MySQL은 프로세스 기반이 아닌 스레드 기반으로 작동하며, 일반적으로 Foreground Thread와 Background Thread로 구분을 합니다.
(Background Thread는 이 글에서 다루지 않습니다.)</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/5228028e-447b-49a8-92da-59de5015697a/image.png" alt=""></p>
<p>Client가 MySQL 서버에 연결을 요청하면 MySQL 서버는 해당 요청을 Connection Requests 대기열에 추가를 합니다. 그리고 Receiver Thread는 이러한 요청을 순차적으로 소비하면서 각 Client의 요청을 처리해줄 Thread를 생성합니다.</p>
<blockquote>
<p><strong>참고!</strong>
과거에는 Thread를 생성하고 삭제할 때 발생하는 비용이 컸기 때문에 이러한 오버헤드를 줄이고 성능을 향상시키기 위해 Thread Cache를 이용하여 Thread를 관리했습니다.</p>
</blockquote>
<p>그러나 요즘에는 Thread를 생성하고 삭제할 때 발생하는 비용이 저렴해져 Thread Cache를 레거시로 보고 있습니다.</p>
<p>이렇게 생성된 Thread는 MySQL 앞단에서 Client와 통신을 하기 때문에 Foreground Thread라고 불립니다. 그렇기 때문에 일반적으로 Foreground Thread는 MySQL 서버에 연결된 Client 수 만큼 존재하게 됩니다.</p>
<h1 id="mysql-connection">MySQL Connection</h1>
<p>MySQL에서 Connection은 Client가 MySQL 서버에 접속해서 데이터를 주고 받는 통신 세션을 의미합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/2518473c-6e2b-46f2-b1af-568de9cdb8b7/image.png" alt=""></p>
<h3 id="threads_connected">Threads_connected</h3>
<p>MySQL 서버에서 현재 연결중인 Connection의 수를 확인하는 방법은 현재 활성화된 Foreground Thread의 갯수를 확인하는 방법입니다.</p>
<p>따라서 아래와 같이 현재 활정화된 Thread의 수를 확인하여 현재 연결중인 Connection의 수를 확인할 수 있습니다.</p>
<pre><code class="language-sql">SHOW STATUS LIKE &#39;Threads_connected&#39;;</code></pre>
<h3 id="max_connections">max_connections</h3>
<p><code>max_connections</code>는 MySQL 서버에서 동시에 연결할 수 있는 Client의 수를 제한하는 옵션입니다.</p>
<blockquote>
<p><strong>참고!</strong>
<code>max_connections</code>의 최대값은 아래의 요인에 따라 달라질 수 있습니다.</p>
</blockquote>
<ul>
<li>특정 플랫폼의 스레드 라이브러리 품질</li>
<li>사용 가능한 RAM의 양</li>
<li>각 연결에 사용된 RAM의 양</li>
<li>OS에서 사용가능한 File Descriptor의 Limit <blockquote>
</blockquote>
그리고 AWS RDS의 경우 DBInstanceClassMemory에 비례하여 <code>max_connections</code>의 기본값이 설정됩니다.</li>
</ul>
<p>MySQL 서버는 위에서 언급한 Receiver Thread에서 연결 요청을 처리하는 과정에서 현재 연결된 connection 수와 <code>max_connections</code> 설정의 값을 참조하여 Thread를 생성할지, 아니면 Too many connections 에러를 응답할 것인지 결정하게 됩니다.</p>
<h4 id="max_connections와-db-관리자와의-관계">max_connections와 DB 관리자와의 관계</h4>
<p>MySQL 서버는 모니터링 및 장애 조치를 위해 <code>CONNECTION_ADMIN</code> 권한을 가진 유저가 접속할 수 있도록 <code>max_connections</code> 값에서 +1 만큼의 여유 Connection을 미리 확보합니다.</p>
<p>그렇기 때문에 Too many connection 에러가 발생하는 상황에서도 <code>CONNECTION_ADMIN</code> 권한을 가진 계정으로 MySQL 서버에 접속을 할 수 있습니다.</p>
<p>만약 Too many connection 에러가 발생하는 상황일 때 MySQL 서버에 접속이 불가능하다면 <code>CONNECTION_ADMIN</code> 권한이 없거나 누군가가 이미 접속을 하고 있다고 볼 수 있습니다.</p>
<h4 id="복제를-위한-내부-세션connection과-max_connections의-관계">복제를 위한 내부 세션(Connection)과 max_connections의 관계</h4>
<p>MySQL <code>8.0.18</code> 버전까지는 복제를 위한 내부 세션(Connection)도 <code>max_connections</code> 제한에 포함이 되었습니다. 그렇기 때문에 Too many connections 에러가 발생하는 상황에서 복제 작업이 동작하면, 해당 작업이 실패되는 상황이 발생했습니다.</p>
<p>다행히도 MySQL <code>8.0.19</code> 버전부터는 복제를 위한 내부 세션(Connection)은 <code>max_connections</code> 제한에 포함되지 않도록 변경되었습니다.</p>
<h4 id="max_connections와-os-file-descriptor">max_connections와 OS File Descriptor</h4>
<blockquote>
<p><strong>파일 디스크립터(File Descriptor)</strong>
유닉스 및 유닉스 계열 운영체제(예: 리눅스)에서 파일이나 입출력 장치를 식별하는데 사용되는 정수(Integer) 값으로, 프로세스가 파일이나 파이프, 소켓 등 다양한 입출력 자원과 상호작용할 수 있게 해주는 역할을 가지고 있습니다. <br />
아래에서 말하는 Open은 운영체제(커널)의 open 함수를 통해 ibd 파일을 열어서 File Descriptor를 반환받는 행위를 말합니다.</p>
</blockquote>
<p>MySQL은 세션(Connection)별로 각기 다른 상태(트랜잭션이나 상태, 락, 캐시, user 권한 등)를 가짐으로써 발생되는 문제를 최소화하기 위해서 테이블에 접근할 때 세션(Connection)별로 테이블을 열게끔 설계가 되어있습니다.</p>
<p>Client가 MySQL로 CRUD SQL을 요청하면 Foreground Thread는 해당 요청을 처리하기 위해 관련이 있는 테이블의 ibd 파일을 Open하게 됩니다.</p>
<p>즉, N개의 세션(Connection)들이 동일한 테이블로 CRUD 요청에 해당하는 SQL 명령을 요청하게되면 동일한 ibd 파일을 N번 열게되면서 N개의 <strong>File Descriptor</strong>가 생성됩니다.</p>
<p>이 과정에서 MySQL 프로세스가 가질 수 있는 <strong>File Descriptor</strong>의 제한을 초과하게 되면 장애가 발생할 수 있습니다.</p>
<p>MySQL은 이러한 문제를 방지하기 위해서 아래의 설정을 제공하고 있습니다.</p>
<ul>
<li><strong>open_files_limit</strong>
  테이블, 로그 등을 포함하여 MySQL이 동시에 가질 수 있는 File Descriptor의 수를 지정할 수 있습니다.</li>
<li><strong>table_open_cache</strong>
  한 번 Open된 테이블의 파일 핸들러를 캐시 레이어에 저장하여, 이후 동일한 테이블에 접근할 때 발생되는 오버헤드(디스크 I/O 등)를 줄임으로써 성능을 향상시킬 수 있습니다.
  이 변수는 캐시 레이어에 캐싱할 파일 핸들러의 개수를 설정할 수 있습니다.</li>
</ul>
<hr>
<h1 id="참고-문서">참고 문서</h1>
<ul>
<li><a href="https://dev.mysql.com/blog-archive/mysql-connection-handling-and-scaling/">https://dev.mysql.com/blog-archive/mysql-connection-handling-and-scaling/</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/connection-management.html">https://dev.mysql.com/doc/refman/8.0/en/connection-management.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/table-cache.html">https://dev.mysql.com/doc/refman/8.0/en/table-cache.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_open_files_limit">https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_open_files_limit</a></li>
<li><a href="https://man7.org/linux/man-pages/man2/open.2.html">https://man7.org/linux/man-pages/man2/open.2.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Aurora MySQL 장애 대응]]></title>
            <link>https://velog.io/@dev-lop/AWS-Aurora-MySQL-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91</link>
            <guid>https://velog.io/@dev-lop/AWS-Aurora-MySQL-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91</guid>
            <pubDate>Fri, 31 May 2024 11:24:59 GMT</pubDate>
            <description><![CDATA[<p>2024년 5월 15일, AWS Aurora MySQL의 8.0.mysql_aurora.3.04.1 버전에서 발생한 오류로 장애를 조치하기위해 진행했던 작업과 공부했던 내용을 간단하게 정리한 글입니다.</p>
<hr>
<h1 id="장애-리포팅">장애 리포팅</h1>
<p>2024년 5월 17일 16시 46분. CS팀으로부터 장애 리포팅이 들어옵니다.</p>
<p>&quot;지금 API 호출이 안 되고 있습니다.&quot;
&quot;504 Gateway Timeout이 발생하고 있어요&quot;</p>
<p>17시 15분까지 미팅이 있었던 저는 30분이 지난 17시 16분에 해당 내용을 식별하게 되었고, TL분께서 저한테 현재 상황을 공유해 주셨습니다.</p>
<p><strong>&quot;현재 모든 고객사의 API 호출이 불가능한 상태이고, Argo에서 각 서비스 Pod의 로그를 확인했을 때 별다른 문제가 확인되지 않습니다. 관련해서 디깅 좀 해주시겠어요?&quot;</strong></p>
<p>해당 내용을 전달받은 저는 AWS 서비스의 상태를 확인하기 시작합니다. 그리고 AWS Aurora MySQL 인스턴스 중 하나의 CPU가 99%까지 올라가 있는 것을 확인하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/90549be5-ce9d-456c-b682-89bc3e453888/image.png" alt=""></p>
<p>그리고 저는 아래의 내용을 채널에 공유하게 됩니다.</p>
<p><strong>&quot;저희 운영 DB가 죽은 것 같습니다.&quot;</strong></p>
<hr>
<h1 id="원인-분석">원인 분석</h1>
<p>운영 DB 인스턴스의 장애를 공유하자마자 바로 MySQL에 접속을 시도했습니다. CPU 수치가 최대치에 도달한 걸로 봐서는 Slow Query가 존재할 것이라 생각했기 때문입니다.</p>
<p>하지만 다급했던 저의 마음을 모르는 것인지 야속하게도 MySQL 인스턴스는 Too Many Connections 에러만 응답하기 시작합니다.
( MySQL Connection에 대한 내용은 <a href="https://velog.io/@dev-lop/MySQL-Connection">여기</a>에서 확인해주세요. )</p>
<blockquote>
<p><strong>참고!</strong>
이 때 발생한 Too many connection 에러는 해당 장애와는 무관했습니다.</p>
</blockquote>
<p>서비스 초기 단계여서 사양이 낮은 DB 인스턴스를 사용하게 되었는데, MSA 방식을 &quot;일부&quot; 도입하게 되면서 개발되는 앱의 갯수가 늘어났고, 앱 마다 일정 수의 Connection Pool을 확보하게 되면서 Connection 수치가 max_connections 에 도달해서 발생하던 문제였습니다.</p>
<p>이때 저는 아래의 고민을 하기 시작합니다.</p>
<ol>
<li>MySQL CPU가 최대치로 도달했으니 재부팅을 요청한다.</li>
<li>max_connections 값에 대해서 상향 조정을 요청한다.</li>
</ol>
<p>잠깐의 고민을 마치고 인프라팀 담당자분께 2번 내용으로 요청을 했습니다. 그 이유는 아래와 같습니다.</p>
<ol>
<li>빠른 서비스 정상화를 위해 재부팅을 하는 것이 맞으나, 재부팅을 한다 하더라도 동일한 증상이 재현될 것이라 생각했습니다.</li>
<li>MySQL 서버가 Too many connections 에러를 응답하는 걸로 봐서는 서버가 완전히 다운되지는 않았다는 것 입니다.</li>
<li>장애가 발생하는 상황에서 MySQL의 서버의 상태와 실행중인 쿼리를 살펴보는 것이 원인을 파악하는데 가장 최적의 상황이라고 생각을 했습니다.</li>
<li>실행 중인 RDS 인스턴스에 연결된 DB 파라미터 그룹에서 동적 파라미터로 지정된 값의 변경은 재부팅 없이 적용됩니다.</li>
</ol>
<p>위 내용들을 근거로 인프라팀 담당자분에게 max_connections 값을 상향해 달라고 요청을 했습니다. 그 후 상향 조정된 값이 적용되자마자 MySQL 인스턴스에 접속하여 ProcessList를 확인했습니다.</p>
<pre><code>SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != &#39;Sleep&#39; ORDER BY TIME;</code></pre><p>하지만 예상과는 다르게 조회되는 Slow Query는 존재하지 않았습니다. 당연하게도 RDS 로그 및 이벤트 탭의 slowquery.log 파일에도 기록된 Slow Query는 존재하지 않았습니다.</p>
<p>예상했던 문제가 장애의 원인이 아니라는 것을 확인하자마자 더이상 지연시키지 않고 서비스 정상화를 위해 인프라팀 담당자분에게 인스턴스 재부팅을 요청하게 됩니다.
(재부팅 후 DB 인스턴스가 정상화되었으나, 예상했던대로 원인을 해결할 때까지 장애가 지속적으로 발생했습니다.)</p>
<hr>
<h1 id="원인-분석2">원인 분석2</h1>
<p>인프라팀에게 DB 인스턴스 재부팅을 요청드린 다음 저는 여러가지 로그 파일들을 확인하기 시작했고, 다음과 같이 이상한 부분을 발견할 수 있었습니다.</p>
<p><strong>1. audit.log의 비정상적인 패턴</strong>
<img src="https://velog.velcdn.com/images/dev-lop/post/4c7a908f-8497-4443-a965-d1181455f226/image.png" alt="">
audit.log 파일의 파일명은 보통 .1로 끝나는데 1000번대까지 생성이 되어있습니다.</p>
<p><strong>2. general.log.spillover 파일 기록</strong>
<img src="https://velog.velcdn.com/images/dev-lop/post/53bb0475-9d01-48fe-be7d-8261ac30f65b/image.png" alt=""></p>
<p><strong>3. mysql-general.log 파일이 no space left on device 에러로 적재되지 않고 있다는 점</strong>
<img src="https://velog.velcdn.com/images/dev-lop/post/a7b67ae6-1279-4332-92b9-fb70b3765478/image.png" alt=""></p>
<blockquote>
<p><strong>참고!</strong>
no space left on device 에러는 인스턴스에 저장 공간이 부족할 때 발생하는 에러입니다.</p>
</blockquote>
<p>이러한 에러가 발생하고 있는 것이 의심스러워 모니터링 지표를 살펴보기 시작합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/8be1ab92-24d9-4d94-9f65-829f3f1b8240/image.png" alt=""><img src="https://velog.velcdn.com/images/dev-lop/post/0aa35c77-87f2-40c6-854e-407bc1162a25/image.png" alt=""></p>
<p><a href="https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraMonitoring.Metrics.html">Amazon Aurora CloudWatch 지표 설명</a>에서 FreeLocalStorage 항목을 아래와 같이 설명하고 있습니다.</p>
<blockquote>
<p>사용 가능한 로컬 스토리지 양입니다.</p>
<p>다른 DB 엔진과 달리 Aurora DB 인스턴스의 경우, 이 지표는 각 DB 인스턴스에 사용 가능한 스토리지 크기를 보고합니다. 이 값은 DB 인스턴스 클래스에 좌우됩니다(요금에 대한 자세한 내용은 Amazon RDS 요금 페이지 참조). 
DB 인스턴스 클래스를 큰 것으로 선택하면 인스턴스의 여유 스토리지 공간을 늘릴 수 있습니다.
(Aurora Serverless v2에는 적용되지 않습니다.)</p>
</blockquote>
<p>정리를 하면 어떠한 이유로 DB 인스턴스의 LocalStorage 용량을 고갈시키면서 장애가 발생했고, AWS는 이러한 인스턴스를 FailOver 처리 하면서 다시 정상화되고, 다시 LocalStorage 용량을 고갈시키면서 장애가 발생, 다시 재부팅... 이러한 현상이 며칠동안 지속되고 있었습니다.</p>
<p>제일 먼저 해당 패턴이 언제부터 시작됐는지 식별하기 위해 FreeLocalStorage 지표의 범위를 더욱 늘려서 확인을 합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/2182a1e6-84b9-4d35-bc1b-18750a0bda1c/image.png" alt=""></p>
<p>지표 상으로 해당 장애는 5월 15일 새벽 2시부터 발생하기 시작했다는 것을 알 수 있습니다. 그리고 이를 기반으로 서비스에 영향이 갈만한 부분들을 검토하기 시작합니다.</p>
<ol>
<li><p><strong>MySQL이 LocalStorage 용량을 어떠한 상황에서 사용하는지.</strong>
 → 각종 Logging과 가상 테이블 및 임시 테이블을 위해 사용됩니다.</p>
</li>
<li><p><strong>5월 13일(월요일)부터 문제가 발생한 시점인 5월 17일(목요일) 사이에 배포된 기능이 있는지.</strong>
 → Jenkins 히스토리와 Argo에서 발견된 배포 기록은 찾을 수 없었기 때문에 서비스 배포의 문제는 아니었습니다. </p>
</li>
<li><p><strong>데이터가 쌓이면서 SQL 실행 동작이 변경되면서 가상 테이블을 사용하는 케이스가 늘었는지.</strong>
 MySQL에 설정된 메모리 용량을 over 하면 디스크에 기록하게 되면서 LocalStorage 용량을 소비할 수 있습니다.
 그러나 이 부분에 대해서도 별다른 이상함을 찾지 못했습니다. (이 부분을 확인하기 위해 리서치하고 공부했던 내용은 추후 게시글로 작성할 예정입니다.)</p>
</li>
<li><p><strong>그 외 우리가 모르는 문제가 생겼는지. (AWS 문제 등)</strong></p>
</li>
</ol>
<hr>
<h1 id="처음으로-돌아가서">처음으로 돌아가서...</h1>
<p>여러가지의 가능성을 열고 살펴봤음에도 별다른 문제를 찾지 못했습니다. 그래서 AWS의 문제일 수 있다는 가정을 가지고 처음으로 되돌아가서 확인하기 시작합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/68bfcb2a-5402-44b9-834b-9df29374b416/image.png" alt=""></p>
<p>RDS 로그 및 이벤트 탭의 audit.log 파일의 패턴을 보면 파일이 비정상적으로 대량 생성되고 있었습니다.</p>
<p>이 부분에 대해서 AWS Case를 열어볼까 고민하며 리서치를 하던 중 아래의 문서를 발견하게 됩니다.
( <a href="https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.3042.html">Aurora MySQL 데이터베이스 엔진 업데이트 2024-03-15 (버전 3.04.2, MySQL 8.0.28과 호환)</a> )</p>
<blockquote>
</blockquote>
<p><strong>가용성 향상:</strong></p>
<ul>
<li>Aurora 스토리지와 통신하는 구성 요소의 결함으로 인해 Aurora MySQL 작성기 DB 인스턴스가 장애 조치될 수 있는 문제를 수정했습니다. 이 결함은 소프트웨어 업데이트 이후 DB 인스턴스와 기본 스토리지 간의 통신이 중단되어 발생합니다.</li>
<li>감사 로깅 스레드로 인해 발생하는 잠금 경합에 따라 CPU 사용률이 높아지고 클라이언트 애플리케이션 제한 시간이 초과될 수 있는 문제를 해결했습니다.    <blockquote>
</blockquote>
</li>
<li><em>일반적인 개선 사항:*</em></li>
<li>다운로드 또는 로테이션 시 로그 파일에 액세스 할 수 없고 경우에 따라 CPU 사용량이 증가할 수 있는 감사 로그 파일 관리 관련 문제를 수정했습니다.</li>
</ul>
<p>현재 저희가 사용 중인 인스턴스의 엔진 버전은 <code>8.0.mysql_aurora.3.04.1</code>으로 위 문제가 발생할 수 있는 버전이었습니다.</p>
<p>해당 문서의 내용을 토대로 MySQL의 Logging을 전부 비활성화하니 관련 이슈가 더 이상 발생하지 않았습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-lop/post/4aff893b-05d0-42bb-bfe2-0d693ec7836e/image.png" alt=""></p>
<p>그리고 이 내용을 공유하면서 장애 관련 조치는 일단락되었습니다. 추후 인프라팀에 Aurora MySQL 엔진 업그레이드를 요청한 다음 로그를 다시 활성화시켜 볼 예정입니다.</p>
<p>읽어주셔서 감사합니다.</p>
]]></description>
        </item>
    </channel>
</rss>