<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Tae._.goo</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 04 Oct 2026 06:07:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Tae._.goo</title>
            <url>https://velog.velcdn.com/images/taekyu_/profile/5ba3e490-7331-4209-b85d-8ff71a4e27c0/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Tae._.goo. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/taekyu_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DDIA (데이터 중심 애플리케이션 설계) Chapter 4]]></title>
            <link>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-4-8a3y84fs</link>
            <guid>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-4-8a3y84fs</guid>
            <pubDate>Sun, 04 Oct 2026 06:07:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는
데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
<a href="https://www.wikibook.co.kr/data-intensive/">https://www.wikibook.co.kr/data-intensive/</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** DDIA 4주차 - 03장. 저장소와 검색 (트랜잭션 처리와 분석) **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-트랜잭션이란span"><span style='background-color: MediumAquamarine'>1. 트랜잭션이란</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-일반적인-의미의-트랜잭션-span"><span style="color: SlateBlue">1-1. 일반적인 의미의 트랜잭션 </span></h3>
<p>트랜잭션(transaction)은 여러 개의 읽기와 쓰기를 하나로 묶은 논리적인 작업 단위다.</p>
<p>계좌 이체가 대표적인 예다. A 계좌에서 1만 원을 빼고 B 계좌에 1만 원을 더하는 두 쓰기는 반드시 함께 성공하거나 함께 실패해야 하며, 빼기만 성공하고 더하기가 실패하면 돈이 사라진다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/0bacbb2b-fb66-4a71-9927-0e413fcb42e6/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>커밋(commit):</strong></span> 트랜잭션 안의 모든 변경을 확정한다</p>
</li>
<li><p><span style="color: indianred"><strong>롤백(rollback):</strong></span> 중간에 실패하면 트랜잭션 안의 모든 변경을 취소하고 시작 전 상태로 되돌린다</p>
</li>
</ul>
<p>관계형 데이터베이스에서 트랜잭션이 보장하는 성질을 앞 글자를 따서 ACID라 부른다.</p>
<table>
<thead>
<tr>
<th>성질</th>
<th>의미</th>
<th>계좌 이체에서</th>
</tr>
</thead>
<tbody><tr>
<td>원자성(Atomicity)</td>
<td>전부 반영되거나 전혀 반영되지 않는다</td>
<td>빼기와 더하기 중 하나만 반영되는 일이 없다</td>
</tr>
<tr>
<td>일관성(Consistency)</td>
<td>트랜잭션 전후로 데이터가 정해진 규칙을 만족한다</td>
<td>두 계좌 잔액의 합이 이체 전후로 같다</td>
</tr>
<tr>
<td>격리성(Isolation)</td>
<td>동시에 실행되는 트랜잭션이 서로 간섭하지 않는다</td>
<td>이체 도중의 잔액을 다른 트랜잭션이 읽지 않는다</td>
</tr>
<tr>
<td>지속성(Durability)</td>
<td>커밋된 내용은 장애가 나도 사라지지 않는다</td>
<td>이체 완료 직후 서버가 죽어도 결과가 남아 있다</td>
</tr>
</tbody></table>
<br>

<h3 id="span-stylecolor-slateblue1-2-이-장에서-말하는-트랜잭션span"><span style="color: SlateBlue">1-2. 이 장에서 말하는 트랜잭션</span></h3>
<p>책은 트랜잭션을 조금 더 넓은 의미로 쓴다.</p>
<ul>
<li><p><span style="color: indianred"><strong>어원:</strong></span> 초창기 데이터베이스 쓰기는 판매, 공급 업체 발주, 직원 급여 지불 같은 상거래였고, 데이터베이스가 금전 거래가 아닌 영역으로 확장된 뒤에도 용어가 남아 논리 단위로 묶인 읽기와 쓰기 그룹을 뜻하게 됨.</p>
</li>
<li><p><span style="color: indianred"><strong>ACID와의 관계:</strong></span> 해당 장에서의 트랜잭션은 반드시 ACID 속성을 가질 필요가 없다</p>
</li>
<li><p><span style="color: indianred"><strong>트랜잭션 처리:</strong></span> 하루에 한 번처럼 주기적으로 수행하는 일괄 처리 작업과 달리, 클라이언트가 낮은 지연 시간으로 읽기와 쓰기를 할 수 있게 하는 것을 의미</p>
</li>
</ul>
<p><span style="color: indianred"><strong>즉, 해당 장에서 트랜잭션 처리와 분석을 나누는 기준은 ACID 여부가 아니라 접근 패턴이다.</strong></span> </p>
<p><strong>사용자의 요청을 그때그때 빠르게 읽고 쓰는 쪽이 트랜잭션 처리이고, 쌓인 데이터를 대량으로 훑어 통계를 내는 쪽이 분석이다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-트랜잭션-처리와-분석-oltp와-olapspan"><span style='background-color: MediumAquamarine'>2. 트랜잭션 처리와 분석 (OLTP와 OLAP)</span></h2>
<hr>

<p><strong>애플리케이션은 색인을 사용하여, 일부 키에 해당하는 적은 수의 레코드를 찾고, 사용자 입력에 따라 레코드를 삽입하거나 갱신한다.</strong></p>
<p><span style="color: indianred"><strong>이러한 애플리케이션은 대화식이기 때문에, 온라인 트랜잭션 처리(online transaction processing, OLTP)라 한다.</strong></span> </p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/46c5f54e-a06a-4626-8cc5-c9395681efdf/image.png" alt=""></p>
<p>반면 데이터 분석은 접근 패턴이 매우 다르다. </p>
<p><strong>분석 질의는 원시 데이터를 사용자에게 돌려주는 게 아니라, 많은 레코드를 스캔해 레코드당 일부 칼럼만 읽고 카운트, 합, 평균 같은 집계 통계를 계산한다.</strong></p>
<ul>
<li><p>1월에 각 매장의 총 수익은 얼마인가?</p>
</li>
<li><p>최근 프로모션 기간에 평소보다 바나나를 얼마나 더 팔았는가?</p>
</li>
<li><p>X 브랜드 기저귀와 함께 가장 자주 구매하는 유아식 브랜드는 무엇인가?</p>
</li>
</ul>
<p>이런 질의는 보통 비즈니스 분석가가 작성하며 경영진의 의사결정을 돕는 보고서인 비즈니스 인텔리전스를 만든다. </p>
<p><span style="color: indianred"><strong>트랜잭션 처리와 구별하기 위해 이 사용 패턴을 온라인 분석 처리(online analytic processing, OLAP)라 부른다.</strong></span> </p>
<table>
<thead>
<tr>
<th>특성</th>
<th>트랜잭션 처리 시스템 (OLTP)</th>
<th>온라인 분석 시스템 (OLAP)</th>
</tr>
</thead>
<tbody><tr>
<td>주요 읽기 패턴</td>
<td>질의당 적은 수의 레코드를 키 기준으로 가져온다</td>
<td>많은 레코드에 대한 집계</td>
</tr>
<tr>
<td>주요 쓰기 패턴</td>
<td>임의 접근, 사용자 입력을 낮은 지연 시간으로 기록</td>
<td>대규모 불러오기(bulk import, ETL) 또는 이벤트 스트림</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>웹 애플리케이션을 통한 최종 사용자/소비자</td>
<td>의사결정 지원을 위한 내부 분석가</td>
</tr>
<tr>
<td>데이터 표현</td>
<td>데이터의 최신 상태 (현재 시점)</td>
<td>시간이 지나며 일어난 이벤트 이력</td>
</tr>
<tr>
<td>데이터셋 크기</td>
<td>기가바이트에서 테라바이트</td>
<td>테라바이트에서 페타바이트</td>
</tr>
</tbody></table>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-데이터-웨어하우징span"><span style='background-color: MediumAquamarine'>3. 데이터 웨어하우징</span></h2>
<hr>

<p>처음에는 트랜잭션 처리와 분석 질의에 같은 데이터베이스를 썼고, SQL은 두 유형 모두에서 잘 동작하였다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/8dd28877-4df8-4c5a-9f1f-889c9d2bab65/image.png" alt=""></p>
<p><strong>그럼에도 1980년대 후반과 1990년대 초반부터 회사들은 분석을 별도의 데이터베이스에서 수행하기 시작했고, 이를 데이터 웨어하우스(data warehouse)라 부른다.</strong></p>
<h3 id="span-stylecolor-slateblue3-1-왜-따로-두는가span"><span style="color: SlateBlue">3-1. 왜 따로 두는가</span></h3>
<p><strong>기업은 고객 대면 웹 사이트, 매장 판매 관리, 창고 재고, 운송 경로 계획, 공급업체 관리, 직원 관리처럼 수십 가지의 OLTP 시스템을 갖추고 있고, 각 시스템은 보통 독자적으로 운영된다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>OLTP는 보호 대상이다:</strong></span> 사업 운영에 대단히 중요해 높은 가용성과 낮은 지연 시간을 기대하므로, DBA는 데이터셋의 많은 부분을 스캔해 동시에 실행되는 트랜잭션의 성능을 떨어뜨릴 수 있는 즉석 분석 질의인, ad hoc analytic query를 꺼린다</p>
</li>
<li><p><span style="color: indianred"><strong>데이터 웨어하우스는 분석 전용이다:</strong></span> 회사 내 모든 OLTP 시스템에 있는 데이터의 읽기 전용 복사본으로, 분석가가 OLTP 작업에 영향을 주지 않고 마음껏 질의할 수 있다</p>
</li>
</ul>
<p><strong>데이터는 OLTP 데이터베이스에서 주기적인 덤프나 지속적인 갱신 스트림으로 추출하고, 분석 친화적인 스키마로 변환해 정리한 다음, 데이터 웨어하우스에 적재한다.</strong></p>
<p><span style="color: indianred"><strong>이 과정을 ETL(Extract-Transform-Load)이라 한다.</strong> </span>
<img src="https://velog.velcdn.com/images/taekyu_/post/9f70126d-fa3e-423e-92af-221b86348ad5/image.png" width="60%" height="n%">
거의 모든 대기업에는 데이터 웨어하우스가 있지만, OLTP 시스템이 많지 않고 데이터가 적은 소규모 기업은 일반 SQL 데이터베이스나 스프레드시트로도 분석할 수 있어서 보통 없다.</p>
<p><span style="color: indianred"><strong>데이터 웨어하우스를 따로 두는 큰 장점은 분석 접근 패턴에 맞게 최적화할 수 있다는 점이다.</strong></span></p>
<p>해시 색인, LSM 트리, B 트리 같은 색인 알고리즘은 OLTP에서는 잘 동작하지만 분석 질의에는 별로 좋지 않다.</p>
<br>

<h3 id="span-stylecolor-slateblue3-2-oltp-데이터베이스와-데이터-웨어하우스의-차이점span"><span style="color: SlateBlue">3-2. OLTP 데이터베이스와 데이터 웨어하우스의 차이점</span></h3>
<p><strong>데이터 웨어하우스는 분석 질의에 적합한 SQL을 쓰기 위해 보통 관계형 모델을 사용하며, SQL 질의를 생성하고 결과를 시각화한다.</strong></p>
<p>또한, Drill-down, Slicing, Dicing으로 데이터를 탐색하게 해주는 그래픽 분석 도구가 많다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/8dd95c39-6f2a-4af0-9c8a-1c2495c7a034/image.png" alt=""></p>
<p>둘 다 SQL 인터페이스를 지원해서 겉으로는 비슷해 보이지만, 서로 매우 다른 질의 패턴에 맞게 최적화돼 내부는 완전히 다르다. 그래서 대부분의 벤더는 둘 중 하나에 집중한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>둘 다 지원:</strong></span> Microsoft SQL Server, SAP HANA는 한 제품에서 트랜잭션 처리와 데이터 웨어하우징을 모두 지원하지만, 공통 SQL 인터페이스 뒤에서 저장소와 질의 엔진은 점점 분리되고 있다</p>
</li>
<li><p><span style="color: indianred"><strong>상용 데이터 웨어하우스:</strong></span> Teradata, Vertica, SAP HANA, ParAccel이 보통 비싼 상용 라이선스로 판매되며, Amazon Redshift는 ParAccel의 호스팅 버전이다</p>
</li>
<li><p><span style="color: indianred"><strong>오픈소스 SQL-on-Hadoop:</strong></span> Apache Hive, Spark SQL, Cloudera Impala, Facebook Presto, Apache Tajo, Apache Drill이 상용 제품과의 경쟁을 목표로 하며, 일부는 구글 Dremel의 개념을 기반으로 한다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-분석용-스키마--별-모양-스키마와-눈꽃송이-모양-스키마span"><span style='background-color: MediumAquamarine'>4. 분석용 스키마 : 별 모양 스키마와 눈꽃송이 모양 스키마</span></h2>
<hr>

<p>트랜잭션 처리 영역에서는 애플리케이션에 따라 데이터 모델이 다양하지만, 분석에서는 다양성이 훨씬 적다. </p>
<p><span style="color: indianred"><strong>많은 데이터 웨어하우스가 별 모양 스키마(star schema), 다른 말로 차원 모델링(dimensional modeling)이라는 정형화된 방식을 쓴다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/62b5fad6-3d1d-4532-aac3-464b0a17a6fe/image.png" width="50%" height="n%"></p>
<ul>
<li><p><span style="color: indianred"><strong>사실 테이블(fact table):</strong></span> 스키마의 중심이며 각 로우가 특정 시각에 발생한 이벤트 하나.
식료품 소매업이라면 고객의 제품 구매 한 건이 한 로우(<code>fact_sales</code>)이고, 웹 트래픽 분석이라면 페이지 뷰나 클릭 한 번이 한 로우</p>
</li>
<li><p><span style="color: indianred"><strong>차원 테이블(dimension table):</strong></span> 사실 테이블의 일부 칼럼은 판매 가격, 원가 같은 속성이고 나머지는 차원 테이블을 가리키는 외래 키다. 차원은 이벤트의 <code>누가, 언제, 어디서, 무엇을, 어떻게, 왜</code>를 나타낸다</p>
</li>
</ul>
<p>예를 들어 <code>dim_product</code> 테이블의 각 로우는 SKU, 설명, 브랜드, 범주, 지방 함량, 상품 크기 같은 판매 제품의 한 유형을 나타낸다. </p>
<p>날짜와 시간도 보통 차원 테이블(<code>dim_date</code>)로 표현하는데, 이렇게 하면 공휴일 여부 같은 정보를 함께 부호화해 휴일과 평일의 판매 차이를 질의할 수 있다.</p>
<p>&quot;별 모양&quot;이라는 이름은 테이블 관계를 그리면 사실 테이블이 가운데에 있고 차원 테이블이 별의 광선처럼 둘러싸는 모양에서 왔다.</p>
<ul>
<li><span style="color: indianred"><strong>눈꽃송이 모양 스키마(snowflake schema):</strong></span> 차원을 하위 차원으로 더 세분화한 변형.
브랜드와 제품 범주를 별도 테이블로 분리하고 <code>dim_product</code>가 문자열 대신 외래 키로 참조하는 식이며, 더 정규화됐지만 분석가들은 작업하기 쉬운 별 모양 스키마를 선호한다</li>
</ul>
<p>*<em>데이터 웨어하우스의 테이블은 폭이 매우 넓다. *</em></p>
<p>사실 테이블은 보통 100개 이상의 칼럼을 가지며, Apple, Walmart, eBay 같은 대기업은 수십 페타바이트의 트랜잭션 내역을 보유하고 그중 대부분이 사실 테이블이다.</p>
<p><span style="color: indianred"><strong>차원 테이블은 보통 수백만 로우 정도로 훨씬 작아서, 효율적으로 저장하고 질의하기 어려운 쪽은 사실 테이블이다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-칼럼-지향-저장소span"><span style='background-color: MediumAquamarine'>5. 칼럼 지향 저장소</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue5-1-로우-지향-저장소의-문제span"><span style="color: SlateBlue">5-1. 로우 지향 저장소의 문제</span></h3>
<p>*<em>사실 테이블은 칼럼이 100개 이상이지만, 데이터 웨어하우스 질의는 보통 한 번에 4~5개 칼럼만 접근한다. *</em></p>
<p>분석용으로는 <code>SELECT *</code>가 거의 필요 없다.</p>
<pre><code class="language-sql">-- 사람들이 요일에 따라 신선 과일을 더 사는지 사탕을 더 사는지 분석을 위한 질의문
SELECT
  dim_date.weekday, dim_product.category,
  SUM(fact_sales.quantity) AS quantity_sold
FROM fact_sales
  JOIN dim_date    ON fact_sales.date_key   = dim_date.date_key
  JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
WHERE
  dim_date.year = 2013 AND
  dim_product.category IN (&#39;Fresh fruit&#39;, &#39;Candy&#39;)
GROUP BY
  dim_date.weekday, dim_product.category;</code></pre>
<p>해당 질의는 2013년의 과일·사탕 구매를 모두 훑으므로 많은 로우에 접근하지만, <code>fact_sales</code>에서는 <code>date_key</code>, <code>product_sk</code>, <code>quantity</code> 세 칼럼만 필요하다.</p>
<p>대부분의 OLTP 데이터베이스는 로우 지향으로 저장해서 한 로우의 모든 값이 인접해 있다. 문서 데이터베이스가 문서 전체를 하나의 연속된 바이트 열로 저장하는 것과 비슷하다.</p>
<p><span style="color: indianred"><strong><code>date_key</code>나 <code>product_sk</code>에 색인이 있어도, 로우 지향 저장소는 100개 이상의 속성을 포함한 로우 전체를 디스크에서 메모리로 적재한 뒤 구문을 해석하고 조건에 맞지 않는 로우를 걸러내야 해서 오래 걸린다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue5-2-칼럼-저장소-배치span"><span style="color: SlateBlue">5-2. 칼럼 저장소 배치</span></h3>
<p>칼럼 지향 저장소(column-oriented storage)의 개념은 간단하다. </p>
<p><strong>한 로우의 값을 함께 저장하지 않고 칼럼별로 모든 값을 함께 저장하며, 칼럼마다 파일을 따로 두면 질의에 쓰는 칼럼 파일만 읽고 해석하면 된다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/800305f1-fdcf-456a-b362-b6f83cbcc77b/image.png" width="50%" height="n%"></p>
<ul>
<li><p><span style="color: indianred"><strong>로우 재구성:</strong></span> 모든 칼럼 파일이 같은 순서로 로우를 담고 있어서, 각 칼럼 파일의 23번째 항목을 모으면 테이블의 23번째 로우가 된다</p>
</li>
<li><p><span style="color: indianred"><strong>비관계형 데이터에도 적용된다:</strong></span> Parquet는 구글 Dremel을 기반으로 한, 문서 데이터 모델을 지원하는 칼럼 저장소 형식이다</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue5-3-칼럼-압축span"><span style="color: SlateBlue">5-3. 칼럼 압축</span></h3>
<p>필요한 칼럼만 읽는 것에 더해 데이터를 압축하면 디스크 처리량을 더 줄일 수 있고, 칼럼 지향 저장소는 같은 값이 반복해서 나타나는 경우가 많아 압축에 잘 맞는다.</p>
<p><span style="color: indianred">*<em>데이터 웨어하우스에서 특히 효과적인 기법이 비트맵 부호화(bitmap encoding)이다. *</em></span></p>
<p><strong>칼럼의 고유 값 수는 로우 수에 비해 적으므로, 고유 값마다 비트맵 하나를 만들고 각 로우에 비트 하나를 할당해 그 값을 가지면 1, 아니면 0으로 표시한다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/fc746653-ea80-4b5f-b110-13db4993f1dd/image.png" width="50%" height="n%"></p>
<ul>
<li><p><span style="color: indianred"><strong>고유 값이 적을 때:</strong></span> country 칼럼처럼 고유 값이 200개 정도라면 비트맵을 로우당 비트 하나로 그대로 저장할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>고유 값이 많을 때:</strong></span> 대부분의 비트맵에 0이 훨씬 많아지는데(희소하다), 이때 런 렝스 부호화(run-length encoding)를 추가로 적용하면 칼럼을 현저히 작게 줄일 수 있다</p>
</li>
</ul>
<p>이런 비트맵 색인은 데이터 웨어하우스의 전형적인 질의에 잘 맞는다.</p>
<ul>
<li><p><span style="color: indianred"><strong><code>WHERE product_sk IN (30, 68, 69)</code>:</strong></span> 세 값의 비트맵을 적재해 비트 OR를 계산한다</p>
</li>
<li><p><span style="color: indianred"><strong><code>WHERE product_sk = 31 AND store_sk = 3</code>:</strong></span> 두 비트맵을 적재해 비트 AND를 계산한다. 모든 칼럼이 같은 순서로 로우를 담고 있어서 한 칼럼 비트맵의 k번째 비트와 다른 칼럼 비트맵의 k번째 비트가 같은 로우를 가리키기 때문에 가능하다</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue5-4-메모리-대역폭과-벡터화-처리span"><span style="color: SlateBlue">5-4. 메모리 대역폭과 벡터화 처리</span></h3>
<p>수백만 로우를 스캔하는 질의에서는 디스크에서 메모리로 데이터를 가져오는 대역폭이 큰 병목이지만 유일한 병목은 아니다. </p>
<p><strong>분석용 데이터베이스 개발자는 메인 메모리에서 CPU 캐시로 가는 대역폭을 효율적으로 쓰고, 분기 예측 실패와 파이프라인 버블을 피하며, SIMD(single-instruction-multi-data) 명령을 활용하도록 신경 써야 한다.</strong></p>
<p>칼럼 저장소 배치는 CPU 주기를 효율적으로 쓰는 데도 유리하다.</p>
<ul>
<li><p><span style="color: indianred"><strong>타이트 루프(tight loop):</strong></span> 질의 엔진이 압축된 칼럼 데이터를 CPU L1 캐시에 딱 맞는 덩어리로 가져와 함수 호출 없는 루프에서 반복 처리하며, CPU는 함수 호출이나 레코드마다 분기가 필요한 코드보다 이런 루프를 훨씬 빨리 실행한다</p>
</li>
<li><p><span style="color: indianred"><strong>압축의 효과:</strong></span> 칼럼을 압축하면 같은 크기의 L1 캐시에 더 많은 로우를 담을 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>벡터화 처리(vectorized processing):</strong></span> 비트 AND, OR 같은 연산자를 압축된 칼럼 데이터 덩어리에 바로 연산하도록 설계하는 기법이다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-칼럼-저장소의-정렬과-쓰기span"><span style='background-color: MediumAquamarine'>6. 칼럼 저장소의 정렬과 쓰기</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue6-1-칼럼-저장소의-순서-정렬span"><span style="color: SlateBlue">6-1. 칼럼 저장소의 순서 정렬</span></h3>
<p>로우를 삽입된 순서대로 저장하면 새 로우를 각 칼럼 파일에 덧붙이기만 하면 돼서 가장 쉽지만, SS테이블처럼 순서를 도입해 색인 메커니즘으로 쓸 수도 있다.</p>
<p>다만 각 칼럼을 독립적으로 정렬할 수는 없다. 그러면 어떤 항목이 같은 로우에 속하는지 알 수 없게 되므로, <span style="color: indianred"><strong>칼럼별로 저장하더라도 정렬은 로우 전체 단위로 해야 한다.</strong></span> </p>
<ul>
<li><p><span style="color: indianred"><strong>1차 정렬 키:</strong></span> DBA가 자주 쓰는 질의를 보고 고른다. &quot;지난 달&quot;처럼 시간 범위를 대상으로 하는 질의가 많다면 <code>date_key</code>를 1차 정렬 키로 두어, 질의 최적화기가 지난 달 로우만 스캔하게 할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>보조 정렬 키:</strong></span> 1차 정렬 키가 같은 로우들의 순서를 정한다. <code>product_sk</code>를 두 번째 키로 두면 같은 날짜에 판매된 같은 제품이 묶여, 특정 날짜 범위의 제품을 그룹화하거나 필터링하는 질의에 도움이 된다</p>
</li>
<li><p><span style="color: indianred"><strong>압축에도 유리하다:</strong></span> 1차 정렬 칼럼에 고유 값이 많지 않으면 정렬 후 같은 값이 길게 이어지고, 런 렝스 부호화로 수십억 로우의 칼럼도 수 킬로바이트로 압축할 수 있다</p>
</li>
</ul>
<p>압축 효과는 첫 번째 정렬 키에서 가장 강하고, 우선순위가 낮은 칼럼은 거의 임의 순서라 잘 압축되지 않는다. 그래도 앞쪽 몇 개 칼럼을 압축하는 것만으로 전체적으로 이득이다.</p>
<br>

<h3 id="span-stylecolor-slateblue6-2-다양한-순서-정렬span"><span style="color: SlateBlue">6-2. 다양한 순서 정렬</span></h3>
<p>*<em>질의마다 도움이 되는 정렬 순서가 다르다면 같은 데이터를 여러 방식으로 정렬해 저장하면 어떨까? *</em></p>
<p>어차피 장비 고장에 대비해 데이터를 여러 장비에 복제해야 하므로, 복제본마다 다르게 정렬해 두고 질의 패턴에 가장 맞는 버전을 쓰면 된다.</p>
<p>로우 지향 저장소에서 2차 색인을 여러 개 두는 것과 비슷하지만 차이가 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>로우 지향 + 2차 색인</th>
<th>칼럼 지향 + 여러 정렬 순서</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 위치</td>
<td>로우는 한 곳(힙 파일이나 클러스터드 색인)에만 있다</td>
<td>정렬 순서마다 데이터 자체가 따로 있다</td>
</tr>
<tr>
<td>색인의 내용</td>
<td>일치하는 로우를 가리키는 포인터만 담는다</td>
<td>데이터를 가리키는 포인터 없이 값을 담은 칼럼만 있다</td>
</tr>
</tbody></table>
<br>

<h3 id="span-stylecolor-slateblue6-3-칼럼-지향-저장소에-쓰기span"><span style="color: SlateBlue">6-3. 칼럼 지향 저장소에 쓰기</span></h3>
<p><strong>데이터 웨어하우스는 대부분 분석가의 대량 읽기 전용 질의라서 이런 최적화가 합리적이지만, 칼럼 지향 저장소, 압축, 정렬은 모두 읽기를 빠르게 하는 대신 쓰기를 어렵게 한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>제자리 갱신이 불가능하다:</strong></span> B 트리 같은 제자리 갱신 방식은 압축된 칼럼에서는 쓸 수 없다</p>
</li>
<li><p><span style="color: indianred"><strong>삽입 비용이 크다:</strong></span> 로우는 칼럼 안의 위치로 식별되므로, 정렬된 테이블 중간에 로우 하나를 삽입하려면 모든 칼럼 파일을 일관되게 다시 써야 한다</p>
</li>
</ul>
<p><span style="color: indianred">*<em>해결책은 LSM 트리 방식이다. *</em></span></p>
<p>모든 쓰기는 먼저 인메모리 저장소의 정렬된 구조에 추가하고, 충분히 모이면 디스크의 칼럼 파일과 병합해 새 파일로 대량 기록한다. </p>
<p>질의는 디스크의 칼럼 데이터와 메모리의 최근 쓰기를 모두 조사해 결합해야 하지만, 질의 최적화기가 이 구분을 사용자에게 드러내지 않으므로 분석가 입장에서는 삽입, 갱신, 삭제가 후속 질의에 즉시 반영된다.</p>
<p><span style="color: indianred"><strong>모든 데이터 웨어하우스가 칼럼 저장소를 쓰는 건 아니지만, 즉석 분석 질의에 상당히 빨라서 급속히 인기를 얻고 있다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine7-집계--데이터-큐브와-구체화-뷰span"><span style='background-color: MediumAquamarine'>7. 집계 : 데이터 큐브와 구체화 뷰</span></h2>
<hr>

<p>*<em>데이터 웨어하우스 질의는 보통 <code>COUNT</code>, <code>SUM</code>, <code>AVG</code>, <code>MIN</code>, <code>MAX</code> 같은 집계 함수를 쓰는데, 같은 집계를 여러 질의에서 쓴다면 매번 원시 데이터를 처리하는 건 낭비다. *</em>
<img src="https://velog.velcdn.com/images/taekyu_/post/265a71a6-a721-4ee9-9920-5481ac6d6a6a/image.png" alt=""></p>
<p><span style="color: indianred"><strong>자주 쓰는 집계 결과를 캐시해 두는 방법 중 하나가 구체화 뷰(materialized view)다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/90de9feb-611f-45b6-8e42-7f8105c764af/image.png" width="80%" height="n%"></p>
<table>
<thead>
<tr>
<th></th>
<th>가상 뷰 (virtual view)</th>
<th>구체화 뷰 (materialized view)</th>
</tr>
</thead>
<tbody><tr>
<td>정체</td>
<td>질의를 작성하는 단축키일 뿐이다</td>
<td>디스크에 기록된 질의 결과의 실제 복사본이다</td>
</tr>
<tr>
<td>읽을 때</td>
<td>SQL 엔진이 뷰를 원래 질의로 즉석에서 확장해 처리한다</td>
<td>저장된 결과를 바로 읽는다</td>
</tr>
<tr>
<td>원본이 바뀌면</td>
<td>신경 쓸 것이 없다</td>
<td>비정규화된 복사본이라 갱신해야 하며, DB가 자동으로 할 수 있다</td>
</tr>
</tbody></table>
<p><strong>구체화 뷰를 갱신하는 쓰기는 비싸서 OLTP 데이터베이스에서는 자주 쓰지 않지만, 읽기 비중이 큰 데이터 웨어하우스에서는 합리적인 전략이다.</strong></p>
<br>

<p><strong><span style="color: #12b886">데이터 큐브</span></strong></p>
<p>데이터 큐브(data cube) 또는 OLAP 큐브는 여러 차원으로 그룹화한 집계 테이블로, 구체화 뷰의 특별한 사례다.
<img src="https://velog.velcdn.com/images/taekyu_/post/04d07a4f-85b5-4c13-b9f4-c93599c14a63/image.png" width="80%" height="n%"></p>
<p>사실 테이블이 날짜와 제품 두 차원에만 외래 키를 가진다고 하면, 한 축은 날짜, 다른 축은 제품인 2차원 표를 그릴 수 있으며, 각 셀에는 그 날짜와 제품에 해당하는 모든 사실의 속성(<code>net_price</code>)을 집계(<code>SUM</code>)한 값이 들어간다.</p>
<table>
<thead>
<tr>
<th>알고 싶은 것</th>
<th>원시 데이터에 하는 질의</th>
<th>데이터 큐브에서는</th>
</tr>
</thead>
<tbody><tr>
<td>특정 날짜, 특정 제품의 판매액</td>
<td><code>SELECT SUM(net_price) FROM fact_sales WHERE date_key = 140101 AND product_sk = 32</code></td>
<td>해당 셀 하나를 읽는다</td>
</tr>
<tr>
<td>특정 날짜의 전체 판매액</td>
<td><code>SELECT SUM(net_price) FROM fact_sales WHERE date_key = 140101</code></td>
<td>그 날짜 줄의 합계를 읽는다</td>
</tr>
<tr>
<td>특정 제품의 전체 판매액</td>
<td><code>SELECT SUM(net_price) FROM fact_sales WHERE product_sk = 32</code></td>
<td>그 제품 줄의 합계를 읽는다</td>
</tr>
</tbody></table>
<p>*<em>실제로는 차원이 더 많다. *</em></p>
<p>별 모양 스키마 예시는 날짜, 제품, 매장, 프로모션, 고객의 5차원이라 5차원 하이퍼큐브가 되지만 원리는 같다.</p>
<p>각 셀이 특정 날짜, 제품, 매장, 프로모션, 고객 조합의 판매량을 담고 각 차원을 따라 반복해서 요약할 수 있다.</p>
<ul>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 특정 질의를 미리 계산해 둔 셈이라 매우 빠르다. 어제 매장별 총 판매량을 알고 싶으면 수백만 로우를 스캔하지 않고 해당 차원의 합계만 보면 된다</p>
</li>
<li><p><span style="color: indianred"><strong>단점:</strong></span> 원시 데이터에 질의하는 만큼 유연하지 않다. 가격은 차원이 아니므로 가격이 100달러 이상인 항목의 판매 비율 같은 것은 계산할 수 없다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>그래서 대부분의 데이터 웨어하우스는 원시 데이터를 최대한 많이 유지하고, 데이터 큐브 같은 집계는 특정 질의의 성능 향상에만 쓴다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine8-정리span"><span style='background-color: MediumAquamarine'>8. 정리</span></h2>
<hr>

<p><strong>고수준에서 저장소 엔진은 트랜잭션 처리에 최적화된 OLTP와 분석에 최적화된 OLAP 두 범주로 나뉘며, 두 사용 사례는 접근 패턴이 다르다.</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>OLTP</th>
<th>OLAP (데이터 웨어하우스)</th>
</tr>
</thead>
<tbody><tr>
<td>사용자</td>
<td>최종 사용자라 대량의 요청을 받는다</td>
<td>비즈니스 분석가라 질의 수는 훨씬 적다</td>
</tr>
<tr>
<td>질의당 데이터</td>
<td>키로 적은 수의 레코드만 다루고 색인으로 찾는다</td>
<td>짧은 시간에 수백만 개의 레코드를 스캔한다</td>
</tr>
<tr>
<td>병목</td>
<td>디스크 탐색</td>
<td>디스크 대역폭</td>
</tr>
</tbody></table>
<p>OLTP 쪽에는 두 가지 관점이 있다.</p>
<ul>
<li><p><span style="color: indianred"><strong>로그 구조화 관점:</strong></span> 파일에 추가하고 오래된 파일을 삭제할 뿐 한 번 쓴 파일은 갱신하지 않으며, Bitcask, SS테이블, LSM 트리, LevelDB, Cassandra, HBase, Lucene이 여기에 속한다. 임의 접근 쓰기를 순차 쓰기로 바꿔 쓰기 처리량을 높인다</p>
</li>
<li><p><span style="color: indianred"><strong>제자리 갱신 관점:</strong></span> 디스크를 덮어쓸 수 있는 고정 크기 페이지의 집합으로 다루며, 모든 주요 관계형 데이터베이스가 쓰는 B 트리가 대표적이다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>질의가 많은 로우를 순차적으로 스캔해야 하는 분석 작업부하에서는 색인보다, 디스크에서 읽는 데이터 양을 최소화하도록 데이터를 매우 작게 부호화하는 일이 중요하며, 칼럼 지향 저장소가 이 목표를 달성하는 방법이다.</strong></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDIA (데이터 중심 애플리케이션 설계) Chapter 3-2]]></title>
            <link>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-3-2-ewszlsea</link>
            <guid>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-3-2-ewszlsea</guid>
            <pubDate>Fri, 25 Sep 2026 13:31:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는
데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
<a href="https://www.wikibook.co.kr/data-intensive/">https://www.wikibook.co.kr/data-intensive/</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** DDIA 3주차 - 03장. 저장소와 검색 **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-저장소-엔진을-알아야-하는-이유span"><span style='background-color: MediumAquamarine'>1. 저장소 엔진을 알아야 하는 이유</span></h2>
<hr>

<p><strong>데이터베이스는 가장 기본적으로 데이터를 받으면 저장하고, 요청하면 다시 찾아 주는 두 가지 일을 한다.</strong></p>
<p>애플리케이션 개발자가 저장소 엔진을 직접 구현할 일은 드물지만, 여러 엔진 중 작업부하에 맞는 것을 고르고 성능을 조정하려면 엔진 내부에서 일어나는 일을 대략 알아야 한다.</p>
<p>이 장에서 다루는 저장소 엔진은 두 계열이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>로그 구조(log-structured) 계열:</strong></span> 해시 색인, SS테이블, LSM 트리</p>
</li>
<li><p><span style="color: indianred"><strong>페이지 지향(page-oriented) 계열:</strong></span> B 트리</p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">읽기 전에 알아두면 좋은 용어 (사전 지식)</span></strong></p>
<table>
<thead>
<tr>
<th>용어</th>
<th>뜻</th>
</tr>
</thead>
<tbody><tr>
<td>기본키(primary key)</td>
<td>테이블에서 로우 하나를 고유하게 식별하는 값이다. 중복되거나 비어 있을 수 없다</td>
</tr>
<tr>
<td>외래키(foreign key)</td>
<td>다른 테이블의 기본키를 가리키는 칼럼으로, 테이블 사이의 관계를 만든다</td>
</tr>
<tr>
<td>색인(index)</td>
<td>특정 칼럼의 값과, 그 값을 가진 데이터가 저장된 위치를 짝지어 따로 모아 둔 구조 (아래 예시 참고)</td>
</tr>
<tr>
<td>보조 색인</td>
<td>기본키가 아닌 칼럼에 만든 색인이며, 기본키와 달리 같은 값이 여러 로우에 있을 수 있다</td>
</tr>
<tr>
<td>키-값 저장소</td>
<td>키로 값을 저장하고 키로 다시 찾는 가장 단순한 형태의 데이터 저장소</td>
</tr>
<tr>
<td>범위 질의(range query)</td>
<td>값 하나가 아니라 특정 범위(예: 1월 1일~1월 31일)에 속하는 값을 모두 찾는 질의</td>
</tr>
<tr>
<td>순차 쓰기 / 무작위 쓰기</td>
<td>파일 끝에 이어서 쓰는 것과 여기저기 흩어진 위치에 쓰는 것으로, 디스크에서는 순차 쓰기가 훨씬 빠르다</td>
</tr>
</tbody></table>
<p>색인은 이 장 전체의 주제라서 조금 더 풀어 둔다.</p>
<p><code>users</code> 테이블에서 <code>email</code>로 사용자를 자주 찾는다고 가정할때, <span style="color: indianred"><strong><code>email</code> 색인은 email 값과 그 로우가 저장된 위치를 한 쌍으로 모아, 빨리 찾을 수 있는 형태로 원본과 따로 저장해 둔 것</strong></span>이다.</p>
<pre><code>users 테이블 (저장된 순서 그대로)          email 색인 (email 순으로 정렬한 예)
 위치   id      email                          email        위치
 #0     7    kim@a.com                      choi@a.com  →   #2
 #1     3    lee@a.com                      kim@a.com   →   #0
 #2     9    choi@a.com                     lee@a.com   →   #1
 #3     5    park@a.com                     park@a.com  →   #3</code></pre><ul>
<li><p><span style="color: indianred"><strong>색인이 없으면:</strong></span> <code>WHERE email = &#39;lee@a.com&#39;</code>을 처리하려고 테이블을 처음부터 끝까지 전부 읽어야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>색인이 있으면:</strong></span> <strong>색인에서 <a href="mailto:lee@a.com">lee@a.com</a>을 빠르게 찾은 뒤 적혀 있는 위치(#1)로 바로 간다</strong></p>
</li>
<li><p><span style="color: indianred"><strong>그 대가:</strong></span> 색인은 원본과 별도로 저장되므로 공간을 차지하고, 데이터를 쓸 때마다 색인도 함께 갱신해야 해서 쓰기가 느려진다. 따라서, 모든 칼럼에 만들지 않고, 개발자가 조건으로 자주 쓰는 칼럼을 골라 <code>CREATE INDEX</code>로 만든다.</p>
</li>
</ul>
<p><span style="color: indianred">*<em>결국 색인은 칼럼 값을 키로, 데이터의 위치를 값으로 하는 키-값 구조다. *</em></span></p>
<p>이 키-값 쌍을 해시로 둘지, 정렬된 파일로 둘지, 트리로 둘지가 이 장에서 다루는 저장소 엔진의 차이다.</p>
<p><strong>기본키와 외래키는 데이터가 지켜야 할 제약 조건이고, 색인은 찾는 속도를 높이기 위한 구조라는 점이 다르며, 대부분의 DB는 기본키를 선언하면 그 칼럼에 색인을 자동으로 만든다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-가장-간단한-데이터베이스와-색인span"><span style='background-color: MediumAquamarine'>2. 가장 간단한 데이터베이스와 색인</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-bash-함수-두-개로-만든-데이터베이스span"><span style="color: SlateBlue">2-1. bash 함수 두 개로 만든 데이터베이스</span></h3>
<pre><code class="language-bash">#!/bin/bash
db_set () {
  echo &quot;$1,$2&quot; &gt;&gt; database
}

db_get () {
  grep &quot;^$1,&quot; database | sed -e &quot;s/^$1,//&quot; | tail -n 1
}</code></pre>
<pre><code class="language-bash">$ db_set 42 &#39;{&quot;name&quot;:&quot;San Francisco&quot;,&quot;attractions&quot;:[&quot;Golden Gate Bridge&quot;]}&#39;
$ db_set 42 &#39;{&quot;name&quot;:&quot;San Francisco&quot;,&quot;attractions&quot;:[&quot;Exploratorium&quot;]}&#39;

$ db_get 42
{&quot;name&quot;:&quot;San Francisco&quot;,&quot;attractions&quot;:[&quot;Exploratorium&quot;]}

$ cat database
42,{&quot;name&quot;:&quot;San Francisco&quot;,&quot;attractions&quot;:[&quot;Golden Gate Bridge&quot;]}
42,{&quot;name&quot;:&quot;San Francisco&quot;,&quot;attractions&quot;:[&quot;Exploratorium&quot;]}</code></pre>
<ul>
<li><p><span style="color: indianred"><strong><code>db_set</code>:</strong></span> 파일 끝에 추가만 하므로 같은 키를 갱신해도 예전 값을 덮어쓰지 않으며, 파일 추가는 매우 효율적이라 쓰기가 빠르다</p>
</li>
<li><p><span style="color: indianred"><strong><code>db_get</code>:</strong></span> 최신 값을 찾으려고 파일 전체를 스캔한 뒤 마지막 항목을 취하므로(<code>tail -n 1</code>) 검색 비용이 O(n)이다</p>
</li>
</ul>
<p><span style="color: indianred">*<em>많은 데이터베이스가 내부적으로 이런 추가 전용(append-only) 데이터 파일, 즉 로그(log)를 쓴다. *</em></span> </p>
<p>실제 데이터베이스는 동시성 제어, 디스크 공간 회수, 오류 처리, 부분적으로 기록된 레코드 처리 같은 문제를 더 다뤄야 하지만 기본 원리는 같다. </p>
<p><span style="color: indianred"><strong>여기서 로그는 애플리케이션 로그가 아니라 연속된 추가 전용 레코드라는 일반적인 의미이며, 사람이 읽을 수 있는 형식일 필요도 없다.</strong></span> </p>
<br>

<h3 id="span-stylecolor-slateblue2-2-색인의-트레이드오프span"><span style="color: SlateBlue">2-2. 색인의 트레이드오프</span></h3>
<p><strong>특정 키의 값을 효율적으로 찾으려면 색인(index)이 필요하다.</strong> </p>
<p>색인은 기본 데이터에서 파생된 부가적인 메타데이터로, 원하는 데이터의 위치를 알려주는 이정표 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/f1301656-5178-4bf8-aef1-f3e0cc53cfdb/image.png" alt=""></p>
<p>색인을 추가하거나 삭제해도 데이터베이스 내용은 바뀌지 않고 질의 성능만 달라지지만, 데이터를 쓸 때마다 색인도 갱신해야 하므로 쓰기가 느려진다.</p>
<p><span style="color: indianred"><strong>잘 고른 색인은 읽기를 빠르게 하지만 모든 색인은 쓰기를 느리게 한다.</strong></span> </p>
<p><strong>그래서 데이터베이스는 모든 것을 자동으로 색인하지 않고, 개발자나 DBA가 애플리케이션의 질의 패턴을 보고 직접 골라야 한다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-해시-색인span"><span style='background-color: MediumAquamarine'>3. 해시 색인</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue3-1-인메모리-해시-맵으로-색인하기span"><span style="color: SlateBlue">3-1. 인메모리 해시 맵으로 색인하기</span></h3>
<p><strong>가장 간단한 색인 전략은 키를 데이터 파일의 바이트 오프셋에 매핑하는 인메모리 해시 맵을 유지하는 것이다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>쓰기:</strong></span> 파일에 키-값 쌍을 추가하고, 방금 기록한 위치의 오프셋을 해시 맵에 반영한다</p>
</li>
<li><p><span style="color: indianred"><strong>읽기:</strong></span> 해시 맵에서 오프셋을 찾아 그 위치의 값을 읽는다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/1db4ea7a-3b44-4471-9aef-f232298c8d9a/image.png" alt=""></p>
<p><strong>Riak의 기본 저장소 엔진인 Bitcask가 이 방식을 쓴다.</strong> </p>
<p>모든 키가 RAM에 들어간다는 조건만 만족하면 고성능 읽기와 쓰기를 보장하며, 값은 디스크 탐색 한 번으로 읽으므로 값 전체는 메모리보다 커도 된다. </p>
<p><span style="color: indianred"><strong>고양이 동영상 URL을 키로, 재생 횟수를 값으로 두는 것처럼 고유 키는 많지 않은데 키당 쓰기가 아주 많은 작업부하에 적합하다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue3-2-세그먼트와-컴팩션span"><span style="color: SlateBlue">3-2. 세그먼트와 컴팩션</span></h3>
<p><strong>파일에 계속 추가만 하면 결국 디스크가 부족해진다. 그래서 로그를 특정 크기의 Segment로 나누고, 크기에 도달하면 파일을 닫고 새 세그먼트에 쓴다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>Compaction:</strong></span> 세그먼트에서 중복된 키를 버리고 각 키의 최신 값만 남긴다</p>
</li>
<li><p><span style="color: indianred"><strong>병합:</strong></span> 컴팩션으로 작아진 여러 세그먼트를 하나로 합친다</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/986f17b5-a907-422b-9127-51fbad48a469/image.png" alt=""></p>
<p><span style="color: indianred">*<em>세그먼트는 한 번 쓰면 변경하지 않으므로 병합 결과는 새 파일로 만들고, 이 작업은 백그라운드 스레드에서 진행된다. *</em></span></p>
<p>그동안 읽기와 쓰기는 이전 세그먼트로 처리하다가, 병합이 끝나면 새 세그먼트로 전환하고 이전 파일을 지운다.</p>
<p>각 세그먼트는 자체 해시 맵을 가지며, 키를 조회할 때는 최신 세그먼트부터 차례로 확인한다. </p>
<p><span style="color: indianred"><strong>병합으로 세그먼트 수를 적게 유지하므로 확인할 해시 맵도 많지 않다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue3-3-실제-구현에서-고려할-것span"><span style="color: SlateBlue">3-3. 실제 구현에서 고려할 것</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>파일 형식:</strong></span> CSV보다 문자열 길이를 바이트로 부호화한 뒤 원시 문자열을 쓰는 바이너리 형식이 더 빠르고 간단하다</p>
</li>
<li><p><span style="color: indianred"><strong>레코드 삭제:</strong></span> 삭제 레코드인 툼스톤(tombstone)을 추가하면, 병합할 때 삭제된 키의 이전 값을 무시한다</p>
</li>
<li><p><span style="color: indianred"><strong>고장 복구:</strong></span> 재시작하면 인메모리 해시 맵이 사라지므로, Bitcask는 해시 맵의 스냅숏을 디스크에 저장해 세그먼트 전체를 다시 읽지 않고 빠르게 복원한다</p>
</li>
<li><p><span style="color: indianred"><strong>부분 레코드 쓰기:</strong></span> 레코드를 추가하는 도중 죽을 수 있으므로, 체크섬으로 손상된 부분을 찾아 무시한다</p>
</li>
<li><p><span style="color: indianred"><strong>동시성 제어:</strong></span> 쓰기는 스레드 하나로 순차 추가하고, 세그먼트는 불변이므로 읽기는 여러 스레드가 동시에 할 수 있다</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue3-4-추가-전용-설계의-장점과-해시-색인의-한계span"><span style="color: SlateBlue">3-4. 추가 전용 설계의 장점과 해시 색인의 한계</span></h3>
<p>덮어쓰기 대신 추가만 하는 방식은 낭비처럼 보이지만 장점이 크다.</p>
<ul>
<li><p><span style="color: indianred"><strong>순차 쓰기:</strong></span> 추가와 병합은 순차 쓰기라 무작위 쓰기보다 훨씬 빠르며, HDD에서 특히 그렇고 SSD도 순차 쓰기를 선호한다</p>
</li>
<li><p><span style="color: indianred"><strong>단순한 동시성과 고장 복구:</strong></span> 세그먼트가 불변이라 값을 덮어쓰는 도중 죽는 경우를 걱정할 필요가 없다</p>
</li>
<li><p><span style="color: indianred"><strong>조각화 방지:</strong></span> 오래된 세그먼트를 병합하므로 데이터 파일이 조각나지 않는다</p>
</li>
</ul>
<p><strong>반면 해시 색인에는 두 가지 한계가 있다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>메모리 제약:</strong></span> 해시 맵을 메모리에 둬야 해서 키가 너무 많으면 쓸 수 없다. 디스크에 두면 무작위 I/O가 많고 확장 비용이 비싸며 해시 충돌 처리가 번거롭다</p>
</li>
<li><p><span style="color: indianred"><strong>범위 질의:</strong></span> <code>kitty00000</code>부터 <code>kitty99999</code>까지 스캔하려면 키를 하나씩 모두 조회해야 한다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-ss테이블과-lsm-트리span"><span style='background-color: MediumAquamarine'>4. SS테이블과 LSM 트리</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue4-1-ss테이블span"><span style="color: SlateBlue">4-1. SS테이블</span></h3>
<p>*<em>세그먼트 파일의 키-값 쌍을 키로 정렬한 형식을 정렬된 문자열 테이블(Sorted String Table), 줄여서 SS테이블이라 한다. *</em></p>
<p>병합된 세그먼트 안에서 각 키는 한 번만 나타난다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/17ea0338-5986-44be-8282-b7a42eb88706/image.png" alt=""></p>
<ul>
<li><span style="color: indianred"><strong>병합이 효율적이다:</strong></span> 병합정렬처럼 여러 세그먼트의 첫 키를 비교해 가장 작은 키를 출력 파일로 옮기는 과정을 반복하므로 파일이 메모리보다 커도 되며, 같은 키가 여러 세그먼트에 있으면 가장 최근 세그먼트의 값을 남긴다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/be0a55c1-a65a-4717-8b1b-525dcf74bd3e/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>모든 키를 메모리에 둘 필요가 없다:</strong></span> <strong>정렬돼 있으므로 <code>handbag</code>과 <code>handsome</code>의 오프셋만 알면 그 사이의 <code>handiwork</code>는 <code>handbag</code>부터 스캔해 찾을 수 있다.</strong> 수 킬로바이트당 키 하나 정도의 희소 색인(sparse index)이면 충분하다</p>
</li>
<li><p><span style="color: indianred"><strong>블록 단위로 압축할 수 있다:</strong></span> 레코드들을 블록으로 묶어 압축하고 희소 색인이 각 블록의 시작을 가리키게 하면, 디스크 공간과 I/O 대역폭을 모두 아낀다</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue4-2-ss테이블-생성과-유지-lsm-트리span"><span style="color: SlateBlue">4-2. SS테이블 생성과 유지 (LSM 트리)</span></h3>
<p>*<em>쓰기는 임의 순서로 들어오는데 어떻게 키 순서로 정렬해 둘까? *</em></p>
<p><span style="color: indianred"><strong>디스크에서 정렬을 유지하기보다 레드 블랙 트리나 AVL 트리 같은 인메모리 균형 트리(Memtable)로 하는 편이 훨씬 쉽다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/73422b93-556c-44ef-8c2a-528b9955bf3c/image.png" width="60%" height="n%"></p>
<p><strong>그림의 WAL은 분리된 로그이고, Immutable MemTable은 디스크에 기록 중인 이전 멤테이블이다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>분리된 로그에 기록:</strong></span> <strong>데이터베이스가 고장 나면 아직 디스크로 기록되지 않고 멤테이블에 있는 가장 최신 쓰기가 손실되므로, 매번 쓰기를 즉시 추가할 수 있는 분리된 로그를 디스크 상에 유지한다.</strong> 해당 로그는 손상 후 멤테이블을 복원할 때만 필요하기 때문에 순서가 정렬되지 않아도 문제되지 않는다.</p>
</li>
<li><p><span style="color: indianred"><strong>멤테이블에 추가:</strong></span> 쓰기가 들어오면 인메모리 균형 트리 데이터 구조인 멤테이블에 추가한다</p>
</li>
<li><p><span style="color: indianred"><strong>임곗값 초과:</strong></span> 멤테이블이 보통 수 메가바이트 정도의 임곗값보다 커지면 SS테이블 파일로 디스크에 기록한다</p>
</li>
<li><p><span style="color: indianred"><strong>SS테이블 파일로 기록:</strong></span> 트리가 이미 키로 정렬된 키-값 쌍을 유지하고 있기 때문에 효율적으로 기록할 수 있고, 새로운 SS테이블 파일은 데이터베이스의 가장 최신 세그먼트가 된다. 멤테이블을 SS테이블로 기록하고 나면 해당 로그는 버릴 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>새로운 멤테이블 인스턴스:</strong></span> SS테이블을 디스크에 기록하는 동안 쓰기는 새로운 멤테이블 인스턴스에 기록한다</p>
</li>
</ol>
<p><strong>그림에는 쓰기 흐름만 있어서, 책의 읽기와 병합 과정은 따로 정리한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>읽기 요청:</strong></span> 먼저 멤테이블에서 키를 찾고, 그다음 디스크 상의 가장 최신 세그먼트, 두 번째로 오래된 세그먼트, 세 번째로 오래된 세그먼트 순으로 찾는다. 그림처럼 A가 이전 SS테이블에는 4, 새 멤테이블에는 5로 있다면 멤테이블을 먼저 찾으므로 최신 값인 5를 읽는다</p>
</li>
<li><p><span style="color: indianred"><strong>병합과 컴팩션:</strong></span> 가끔 세그먼트 파일을 합치고 덮어 쓰여지거나 삭제된 값을 버리는 병합과 컴팩션 과정을 백그라운드에서 수행한다. 그림의 새 멤테이블도 나중에 SS테이블로 기록되고, 두 세그먼트가 병합될 때 덮어 쓰여진 A → 4가 버려진다</p>
</li>
</ul>
<p>이 알고리즘을 LevelDB와 RocksDB가 쓰고, Bigtable 논문에서 영감을 얻은 Cassandra와 HBase도 비슷한 엔진을 쓴다. </p>
<p><span style="color: indianred"><strong>원래 로그 구조화 병합 트리(Log-Structured Merge-Tree), 줄여 LSM 트리라는 이름으로 발표된 구조이며, 정렬된 파일의 병합과 컴팩션에 기반한 엔진을 LSM 저장소 엔진이라 부른다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue4-3-성능-최적화span"><span style="color: SlateBlue">4-3. 성능 최적화</span></h3>
<p><strong>이러한 LSM 트리 알고리즘에서 성능을 최적화할 수 있는 다양한 방법들이 존재한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>블룸 필터(Bloom filter):</strong></span> LSM 트리는 존재하지 않는 키를 찾을 때 멤테이블부터 가장 오래된 세그먼트까지 모두 확인해야 해서 느린데, 블룸 필터가 키가 없다는 사실을 알려줘 불필요한 디스크 읽기를 크게 줄인다</p>
</li>
<li><p><span style="color: indianred"><strong>크기 계층 컴팩션(size-tiered):</strong></span> 새롭고 작은 SS테이블을 오래되고 큰 SS테이블에 연이어 병합한다.</p>
</li>
<li><p><span style="color: indianred"><strong>레벨 컴팩션(leveled compaction):</strong></span> 키 범위를 작은 SS테이블로 나누고 오래된 데이터를 개별 레벨로 옮겨, 컴팩션을 점진적으로 진행하고 디스크를 덜 쓴다. </p>
</li>
</ul>
<p><span style="color: indianred"><strong>LSM 트리의 핵심은 백그라운드에서 SS테이블을 연쇄적으로 병합하는 것이다. 데이터가 메모리보다 훨씬 커도 동작하고, 정렬돼 있어 범위 질의가 효율적이며, 디스크 쓰기가 순차적이라 쓰기 처리량이 매우 높다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-b-트리span"><span style='background-color: MediumAquamarine'>5. B 트리</span></h2>
<hr>

<p><strong>4번 섹션에서 설명한 로그 구조화 색인이 점점 보편화되어가고 있지만 가장 일반적인 색인 유형은 아니다.</strong></p>
<p><span style="color: indianred"><strong>1970년대에 등장해 10년도 안 돼 보편적인 색인 구조라 불린 B 트리는 가장 널리 쓰이는 색인 구조로, 거의 모든 관계형 DB의 표준 색인이며 많은 비관계형 DB도 쓴다.</strong></span></p>
<p>SS테이블처럼 키로 정렬된 키-값 쌍을 유지해 키 검색과 범위 질의가 효율적이지만, 설계 철학은 완전히 다르다.</p>
<table>
<thead>
<tr>
<th></th>
<th>로그 구조화 색인</th>
<th>B 트리</th>
</tr>
</thead>
<tbody><tr>
<td>저장 단위</td>
<td>수 MB 이상의 가변 크기 세그먼트</td>
<td>4KB 정도의 고정 크기 페이지</td>
</tr>
<tr>
<td>쓰기 방식</td>
<td>세그먼트를 항상 순차적으로 기록하고 제자리에서 갱신하지 않는다</td>
<td>한 번에 한 페이지씩 읽고 제자리에서 덮어쓴다</td>
</tr>
</tbody></table>
<p><strong>디스크도 고정 크기 블록으로 배열되므로 B 트리 설계는 하드웨어와 더 밀접하다.</strong></p>
<br>

<h3 id="span-stylecolor-slateblue5-1-검색과-갱신span"><span style="color: SlateBlue">5-1. 검색과 갱신</span></h3>
<p><strong>각 페이지는 주소로 식별되고, 페이지가 다른 페이지를 참조하면서(메모리 대신 디스크 위의 포인터) 페이지 트리를 이룬다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/63e5f5c7-7fa8-4879-b7ca-a372e13332ec/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>검색:</strong></span> 루트 페이지에서 시작하며, 페이지 안의 키들이 하위 페이지가 담당하는 범위의 경계를 나타낸다. 키 251을 찾으면 200과 300 사이의 참조를 따라 내려가고, 이를 반복해 개별 키를 담은 leaf page에 도달한다</p>
</li>
<li><p><span style="color: indianred"><strong>분기 계수:</strong></span> 한 페이지가 참조하는 하위 페이지 수로, 보통 수백 개다</p>
</li>
<li><p><span style="color: indianred"><strong>갱신:</strong></span> 키가 있는 리프 페이지를 찾아 값을 바꾸고 페이지를 다시 디스크에 쓴다</p>
</li>
<li><p><span style="color: indianred"><strong>삽입:</strong></span> 키가 들어갈 범위의 페이지에 추가하고, 공간이 부족하면 페이지를 반쯤 찬 두 페이지로 나눈 뒤 상위 페이지를 갱신한다</p>
</li>
</ul>
<!-- IMG: 그림 3-7 페이지 분리로 커진 B 트리 -->

<p><span style="color: indianred"><strong>이 알고리즘으로 트리는 항상 균형을 유지하고 깊이는 O(log n)이다. 대부분 3~4단계면 충분해서, 분기 계수 500에 4KB 페이지인 4단계 트리는 256TB까지 저장할 수 있다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue5-2-신뢰할-수-있는-b-트리-만들기span"><span style="color: SlateBlue">5-2. 신뢰할 수 있는 B 트리 만들기</span></h3>
<p>*<em>B 트리는 디스크의 페이지를 새 데이터로 덮어쓰며, 덮어써도 페이지 위치가 바뀌지 않아 참조가 유지된다고 가정한다. *</em></p>
<p>HDD라면 헤드를 옮기고 플래터가 돌아오길 기다려 섹터를 덮어쓰고, SSD는 저장소 칩의 큰 블록을 한 번에 지우고 다시 써야 해서 더 복잡하다.</p>
<ul>
<li><p><span style="color: indianred"><strong>고아 페이지 위험:</strong></span> 페이지 분할처럼 여러 페이지를 덮어써야 하는 동작 도중 죽으면, 어떤 부모와도 연결되지 않은 고아 페이지가 생겨 색인이 훼손된다</p>
</li>
<li><p><span style="color: indianred"><strong>쓰기 전 로그(write-ahead log, WAL):</strong></span> <strong>트리 페이지를 바꾸기 전에 모든 변경 사항을 추가 전용 파일에 먼저 기록해, 고장 후 일관된 상태로 복원한다. (재실행 로그라고도 한다)</strong></p>
</li>
<li><p><span style="color: indianred"><strong>래치(latch):</strong></span> 여러 스레드가 동시에 접근하면 일관성이 깨진 트리를 볼 수 있어 가벼운 잠금인 래치로 보호한다. 로그 구조화 방식은 백그라운드에서 병합하고 세그먼트를 원자적으로 교체하므로 이 부분이 훨씬 간단하다</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue5-3-b-트리-최적화span"><span style="color: SlateBlue">5-3. B 트리 최적화</span></h3>
<p>*<em>B트리는 오랫동안 사용되었기 떄문에 개발된 많은 최적화 기법들이 존재한다. *</em></p>
<ul>
<li><p><span style="color: indianred"><strong>쓰기 시 복사(copy-on-write):</strong></span> 변경된 페이지를 다른 위치에 쓰고 상위 페이지의 새 버전이 그곳을 가리키게 한다. 동시성 제어에도 유용하다</p>
</li>
<li><p><span style="color: indianred"><strong>키 축약:</strong></span> 페이지에 전체 키를 저장하는게 아니라 키를 축약해 저장하면, 한 페이지에 키를 더 넣어 분기 계수를 높이고 트리 깊이를 줄인다 (B+ 트리)</p>
</li>
<li><p><span style="color: indianred"><strong>리프 페이지 연속 배치:</strong></span> 범위 스캔 때 페이지마다 디스크를 찾지 않도록 리프 페이지를 디스크에 순서대로 배치하려 하지만, 트리가 커지면 유지하기 어렵다. LSM 트리는 병합 때 큰 세그먼트를 통째로 다시 쓰므로 이게 더 쉽다</p>
</li>
<li><p><span style="color: indianred"><strong>형제 포인터:</strong></span> 리프 페이지가 양쪽 형제 페이지를 참조하면 상위 페이지로 돌아가지 않고 순서대로 키를 스캔할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>프랙탈 트리(fractal tree):</strong></span> 디스크 찾기를 줄이기 위해 로그 구조화 개념을 일부 빌린 B 트리 변형이다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-b-트리와-lsm-트리-비교span"><span style='background-color: MediumAquamarine'>6. B 트리와 LSM 트리 비교</span></h2>
<hr>

<p>*<em>경험적으로 LSM 트리는 쓰기가, B 트리는 읽기가 빠르다고 여긴다. *</em></p>
<p>LSM 트리는 읽을 때 컴팩션 단계마다 있는 여러 데이터 구조와 SS테이블을 확인해야 하기 때문이다. 다만 벤치마크는 작업부하의 세부 사항에 민감하므로 실제 작업부하로 테스트해야 한다.</p>
<br>

<h3 id="span-stylecolor-slateblue6-1-쓰기-증폭span"><span style="color: SlateBlue">6-1. 쓰기 증폭</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>B 트리:</strong></span> 모든 데이터를 WAL에 한 번, 트리 페이지에 한 번 해서 최소 두 번 쓰고, 몇 바이트만 바뀌어도 페이지 전체를 쓴다. 일부 엔진은 전원 장애 때 일부만 갱신된 페이지가 남지 않도록 같은 페이지를 두 번 덮어쓰기도 한다</p>
</li>
<li><p><span style="color: indianred"><strong>LSM 트리:</strong></span> SS테이블의 반복된 컴팩션과 병합으로 데이터를 여러 번 다시 쓴다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>데이터베이스에 한 번 쓴 것이 디스크에 여러 번 쓰이는 효과를 쓰기 증폭이라 한다.</strong></span> </p>
<p>쓰기가 많은 애플리케이션에서는 이것이 곧 성능 비용이고, 덮어쓰기 횟수가 제한된 SSD에서는 수명 문제이기도 하다.</p>
<br>

<h3 id="span-stylecolor-slateblue6-2-두-구조의-장단점span"><span style="color: SlateBlue">6-2. 두 구조의 장단점</span></h3>
<table>
<thead>
<tr>
<th></th>
<th>LSM 트리</th>
<th>B 트리</th>
</tr>
</thead>
<tbody><tr>
<td>쓰기 처리량</td>
<td>높다. 쓰기 증폭이 상대적으로 낮고 SS테이블을 순차로 쓴다</td>
<td>여러 페이지를 제자리에서 덮어써야 한다</td>
</tr>
<tr>
<td>디스크 사용</td>
<td>압축률이 좋고, 주기적으로 다시 써서 파편화가 적다</td>
<td>페이지 분할 등으로 쓰지 않는 공간이 남는다</td>
</tr>
<tr>
<td>컴팩션 영향</td>
<td>컴팩션이 진행 중인 요청을 막아 상위 백분위 응답 시간이 길어질 수 있고, 데이터가 커질수록 컴팩션이 쓰기 대역폭을 더 가져간다</td>
<td>성능을 예측하기 쉽다</td>
</tr>
<tr>
<td>키 위치</td>
<td>여러 세그먼트에 같은 키의 복사본이 있을 수 있다</td>
<td>키가 색인의 한 곳에만 있어, 트리에 직접 잠금을 걸어 트랜잭션 격리를 구현하기 좋다</td>
</tr>
</tbody></table>
<p>대다수 SSD 펌웨어는 내부적으로 로그 구조화 알고리즘을 써서 엔진의 쓰기 패턴이 미치는 영향은 분명하지 않지만, 낮은 쓰기 증폭과 적은 파편화는 SSD에서도 유리하다.</p>
<p>쓰기 처리량이 높은데 컴팩션 설정이 부실하면 컴팩션이 유입 쓰기를 따라가지 못해, 병합되지 않은 세그먼트가 디스크가 찰 때까지 늘어나고 읽기도 느려진다. </p>
<p><strong>SS테이블 기반 엔진은 이때 쓰기 속도를 조절하지 않으므로 명시적으로 모니터링해야 한다.</strong></p>
<p><span style="color: indianred"><strong>B 트리는 데이터베이스 아키텍처에 깊이 뿌리내려 곧 사라지지 않을 것이고, 새로운 데이터 저장소에서는 로그 구조화 색인이 점점 인기를 얻고 있다. 어떤 엔진이 맞는지 정하는 쉬운 규칙은 없으니 테스트로 경험적으로 정하는 것도 나쁘지 않다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine7-기타-색인-구조span"><span style='background-color: MediumAquamarine'>7. 기타 색인 구조</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue7-1-기본키와-보조-색인span"><span style="color: SlateBlue">7-1. 기본키와 보조 색인</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>기본키(primary key) 색인:</strong></span> 관계형 DB의 로우, 문서 DB의 문서, 그래프 DB의 정점을 고유하게 식별하며, 다른 레코드는 이 기본키로 참조한다</p>
</li>
<li><p><span style="color: indianred"><strong>보조 색인(secondary index):</strong></span> <code>CREATE INDEX</code>로 만들며 효율적인 조인에 결정적이다. 기본키와 달리 키가 고유하지 않으므로, 키마다 로우 식별자 목록을 두거나 키에 로우 식별자를 덧붙여 고유하게 만든다</p>
</li>
</ul>
<p><strong>B 트리와 로그 구조화 색인 모두 보조 색인으로 쓸 수 있다.</strong></p>
<br>

<h3 id="span-stylecolor-slateblue7-2-색인-안에-값-저장하기span"><span style="color: SlateBlue">7-2. 색인 안에 값 저장하기</span></h3>
<p><strong>색인의 값은 실제 로우이거나, 다른 곳에 저장된 로우를 가리키는 참조다.</strong></p>
<table>
<thead>
<tr>
<th>방식</th>
<th>내용</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>힙 파일(heap file)</td>
<td>로우는 순서 없이 힙 파일에 두고 색인은 위치만 참조한다</td>
<td>보조 색인이 여러 개여도 데이터가 중복되지 않는다. 값이 커져 위치를 옮기면 모든 색인을 갱신하거나 이전 위치에 전방향 포인터를 남겨야 한다</td>
</tr>
<tr>
<td>클러스터드 색인(clustered index)</td>
<td>색인 안에 로우를 통째로 저장한다</td>
<td>힙 파일을 거치지 않아 읽기가 빠르다. MySQL InnoDB는 기본키가 항상 클러스터드 색인이고 보조 색인은 기본키를 참조하며, SQL Server는 테이블당 하나의 클러스터드 색인을 지정할 수 있다</td>
</tr>
<tr>
<td>커버링 색인(covering index)</td>
<td>색인 안에 일부 칼럼만 저장한다</td>
<td>색인만으로 일부 질의에 응답할 수 있다 (색인이 질의를 커버했다고 한다)</td>
</tr>
</tbody></table>
<p>클러스터드 색인과 커버링 색인은 읽기를 빠르게 하는 대신 저장 공간과 쓰기 오버헤드가 늘고, 중복된 데이터의 일관성을 지키기 위해 트랜잭션 보장에도 추가 노력이 든다.</p>
<br>

<h3 id="span-stylecolor-slateblue7-3-다중-칼럼-색인span"><span style="color: SlateBlue">7-3. 다중 칼럼 색인</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>결합 색인:</strong></span> 여러 필드를 정해진 순서로 이어 하나의 키로 만든다. (성, 이름)으로 정렬된 종이 전화번호부처럼 특정 성, 또는 성과 이름 조합으로는 찾을 수 있지만 이름만으로는 쓸 수 없다</p>
</li>
<li><p><span style="color: indianred"><strong>다차원 색인:</strong></span> 여러 칼럼에 동시에 질의하는 더 일반적인 방법으로, 지리 공간 데이터에서 특히 중요하다</p>
</li>
</ul>
<p>지도에 보이는 영역 안의 레스토랑을 찾으려면 이런 이차원 범위 질의가 필요하다.</p>
<pre><code class="language-sql">SELECT * FROM restaurants
WHERE latitude  &gt; 51.4946 AND latitude  &lt; 51.5079
  AND longitude &gt; -0.1162 AND longitude &lt; -0.1004;</code></pre>
<p>*<em>B 트리나 LSM 트리는 위도 범위와 경도 범위 중 하나로만 좁힐 수 있어 이 질의를 효율적으로 처리하지 못한다. *</em></p>
<p><span style="color: indianred"><strong>이차원 위치를 공간 채움 곡선으로 숫자 하나로 바꿔 B 트리를 쓰거나, 더 일반적으로 PostGIS처럼 R 트리 같은 전문 공간 색인을 쓴다.</strong></span></p>
<p>다차원 색인은 지리 정보에만 쓰이지 않는다. (빨강, 초록, 파랑) 3차원 색인으로 특정 색상 범위의 제품을 찾거나, (날짜, 기온) 2차원 색인으로 2013년에 기온이 25~30도인 관측을 한 번에 좁힐 수 있다. HyperDex가 이 기법을 쓴다.</p>
<br>

<h3 id="span-stylecolor-slateblue7-4-전문-검색과-퍼지-색인span"><span style="color: SlateBlue">7-4. 전문 검색과 퍼지 색인</span></h3>
<p>지금까지의 색인은 정확한 값이나 정렬된 범위로만 질의할 수 있어, 철자가 틀린 단어처럼 유사한 키는 찾지 못한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>전문 검색 엔진:</strong></span> 동의어로 질의를 확장하고, 단어의 문법적 활용을 무시하며, 인접해 나타난 단어를 찾는 등 언어학적 분석을 한다</p>
</li>
<li><p><span style="color: indianred"><strong>편집 거리(edit distance):</strong></span> Lucene은 한 글자의 추가, 삭제, 교체를 편집 거리 1로 보고, 특정 편집 거리 안의 단어를 검색해 오타에 대처한다</p>
</li>
<li><p><span style="color: indianred"><strong>오토마톤:</strong></span> Lucene의 인메모리 색인은 트라이(trie)와 비슷한 유한 상태 오토마톤이며, 이를 레벤슈타인 오토마톤으로 바꿔 편집 거리 검색을 효율적으로 한다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine8-모든-것을-메모리에-보관span"><span style='background-color: MediumAquamarine'>8. 모든 것을 메모리에 보관</span></h2>
<hr>

<p>지금까지의 데이터 구조는 모두 디스크의 한계를 다루기 위한 것이었다. </p>
<p><strong>그럼에도 디스크를 쓰는 이유는 전원이 꺼져도 남는 지속성과 기가바이트당 낮은 가격 때문인데, RAM이 저렴해지고 대부분의 데이터셋이 그리 크지 않아 메모리에 통째로 두는 인메모리 데이터베이스가 현실적인 선택이 됐다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>Memcached:</strong></span> 재시작하면 데이터를 잃어도 되는 캐시 용도</p>
</li>
<li><p><span style="color: indianred"><strong>VoltDB, MemSQL, Oracle TimesTen:</strong></span> 관계형 모델의 인메모리 DB</p>
</li>
<li><p><span style="color: indianred"><strong>RAMCloud:</strong></span> 지속성 있는 오픈소스 인메모리 키-값 저장소로, 메모리와 디스크 모두 로그 구조화 방식을 쓴다</p>
</li>
<li><p><span style="color: indianred"><strong>Redis, Couchbase:</strong></span> 비동기로 디스크에 기록해 약한 지속성을 제공한다</p>
</li>
</ul>
<p>*<em>RAM에서의 지속성은 배터리 전원 RAM 같은 특수 하드웨어, 디스크에 변경 로그 기록, 주기적 스냅숏, 다른 장비로의 복제로 확보한다. *</em></p>
<p>디스크에 기록하더라도 디스크는 지속성을 위한 추가 전용 로그로만 쓰고 읽기는 전부 메모리에서 하므로 여전히 인메모리 DB이며, 디스크 파일은 백업과 외부 분석에도 쓸 수 있다.</p>
<p>인메모리 DB가 빠른 이유는 디스크에서 읽지 않아서가 아니다. </p>
<p>디스크 기반 엔진도 메모리가 충분하면 OS가 최근 블록을 캐시해 디스크를 읽지 않으며, <span style="color: indianred"><strong>진짜 이유는 인메모리 데이터 구조를 디스크에 쓸 형태로 부호화하는 오버헤드를 피하기 때문이다.</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>새로운 데이터 모델:</strong></span> 디스크 기반 색인으로 구현하기 어려운 모델을 제공하며, Redis는 우선순위 큐와 셋 같은 자료 구조를 DB 인터페이스로 제공한다</p>
</li>
<li><p><span style="color: indianred"><strong>안티 캐싱(anti-caching):</strong></span> 메모리가 모자라면 가장 오래 쓰지 않은 레코드를 디스크로 내보내고 다시 접근할 때 적재한다. OS의 스왑과 비슷하지만 페이지가 아닌 레코드 단위라 더 효율적이며, 다만 색인은 여전히 전부 메모리에 있어야 한다</p>
</li>
<li><p><span style="color: indianred"><strong>비휘발성 메모리(NVM):</strong></span> 널리 채택되면 저장소 엔진 설계도 바뀌어야 할 것으로 보이는 연구 분야다</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine9-정리span"><span style='background-color: MediumAquamarine'>9. 정리</span></h2>
<hr>

<ul>
<li><p><span style="color: indianred"><strong>로그 구조화 계열:</strong></span> 파일에 추가만 하고 오래된 파일을 지우며 제자리에서 갱신하지 않는다. 해시 색인에서 SS테이블과 LSM 트리로 발전했고, 무작위 쓰기를 순차 쓰기로 바꿔 높은 쓰기 처리량을 얻는다</p>
</li>
<li><p><span style="color: indianred"><strong>페이지 지향 계열:</strong></span> 고정 크기 페이지를 제자리에서 덮어쓰며, B 트리가 대표적이고 거의 모든 관계형 DB의 표준 색인이다</p>
</li>
<li><p><span style="color: indianred"><strong>색인:</strong></span> 읽기를 빠르게 하는 대신 쓰기를 느리게 하므로 질의 패턴을 보고 골라야 하며, 클러스터드·커버링·다중 칼럼·퍼지 색인처럼 목적에 따라 형태가 다양하다</p>
</li>
<li><p><span style="color: indianred"><strong>인메모리 DB:</strong></span> RAM이 저렴해지며 현실적인 선택이 됐고, 빠른 이유는 디스크 부호화 오버헤드를 피하기 때문이다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>경험적으로 LSM 트리는 쓰기가, B 트리는 읽기가 빠르다고 여기지만, 사용 사례에 맞는 저장소 엔진을 정하는 쉬운 규칙은 없으므로 실제 작업부하로 테스트해 정해야 한다.</strong></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDIA (데이터 중심 애플리케이션 설계) Chapter 3-1]]></title>
            <link>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-3-1-sys2nj2n</link>
            <guid>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-3-1-sys2nj2n</guid>
            <pubDate>Fri, 25 Sep 2026 13:05:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는
데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
<a href="https://www.wikibook.co.kr/data-intensive/">https://www.wikibook.co.kr/data-intensive/</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** DDIA 3주차 - 02장. 데이터 모델과 질의 언어 (그래프형 데이터 모델) **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-그래프형-데이터-모델span"><span style='background-color: MediumAquamarine'>1. 그래프형 데이터 모델</span></h2>
<hr>

<p>애플리케이션이 주로 일대다 관계이거나 레코드 간 관계가 없다면 문서 모델이 적합하다.</p>
<p><span style="color: indianred"><strong>하지만 다대다 관계가 매우 일반적이고 데이터 간 연결이 복잡해지면 그래프로 모델링하는 편이 자연스럽다.</strong></span></p>
<p>그래프는 두 유형의 객체로 이뤄진다.</p>
<ul>
<li><p><span style="color: indianred"><strong>정점(vertex):</strong></span> 노드, 엔티티라고도 한다</p>
</li>
<li><p><span style="color: indianred"><strong>간선(edge):</strong></span> 관계, 호(arc)라고도 한다</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>그래프</th>
<th>정점</th>
<th>간선</th>
</tr>
</thead>
<tbody><tr>
<td>소셜 그래프</td>
<td>사람</td>
<td>사람들이 서로 알고 있음</td>
</tr>
<tr>
<td>웹 그래프</td>
<td>웹 페이지</td>
<td>다른 페이지에 대한 HTML 링크</td>
</tr>
<tr>
<td>도로/철도 네트워크</td>
<td>교차로</td>
<td>교차로 간 도로나 철로</td>
</tr>
</tbody></table>
<p>이런 그래프 위에서 동작하는 알고리즘도 잘 알려져 있어서, 자동차 내비게이션은 도로 네트워크에서 두 지점 간 최단 경로를 검색하고 페이지랭크는 웹 그래프로 웹 페이지의 인기와 검색 순위를 결정한다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/2fca1118-cf84-4dd0-bb33-5dd98e323f9d/image.png" alt=""></p>
<p><span style="color: indianred"><strong>그래프는 동종 데이터에 국한되지 않는다.</strong></span></p>
<p>페이스북은 사람, 장소, 이벤트, 체크인, 코멘트를 모두 정점으로 두고 친구 관계, 체크인 위치, 코멘트 작성자, 이벤트 참석 여부를 간선으로 표현하는 단일 그래프를 유지한다.</p>
<!-- IMG: 그림 2-5 그래프 구조 데이터 예제 (루시와 알랭) -->

<p>그래프를 구조화하고 질의하는 모델은 크게 두 가지다.</p>
<ul>
<li><p><span style="color: indianred"><strong>속성 그래프 모델:</strong></span> Neo4j, Titan, InfiniteGraph</p>
</li>
<li><p><span style="color: indianred"><strong>트리플 저장소 모델:</strong></span> Datomic, AllegroGraph</p>
</li>
</ul>
<p>질의 언어로는 선언형인 사이퍼, 스파클, 데이터로그를 다루며, 이 밖에 명령형 그래프 질의 언어인 Gremlin과 그래프 처리 프레임워크인 Pregel도 있다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-속성-그래프span"><span style='background-color: MediumAquamarine'>2. 속성 그래프</span></h2>
<hr>
<img src="https://velog.velcdn.com/images/taekyu_/post/e88819c6-0d94-4847-99f4-b6a66c63bb26/image.png" >

<ul>
<li><p><span style="color: indianred"><strong>정점:</strong></span> 고유한 식별자, Outgoing 간선 집합, Incoming 간선 집합, 속성 컬렉션(키-값 쌍)</p>
</li>
<li><p><span style="color: indianred"><strong>간선:</strong></span> 고유한 식별자, 간선이 시작하는 꼬리 정점, 간선이 끝나는 머리 정점, 관계 유형을 나타내는 레이블, 속성 컬렉션(키-값 쌍)</p>
</li>
</ul>
<p>관계형 테이블 두 개로 표현하면 이렇다.</p>
<pre><code class="language-sql">CREATE TABLE vertices (
  vertex_id  integer PRIMARY KEY,
  properties json
);

CREATE TABLE edges (
  edge_id     integer PRIMARY KEY,
  tail_vertex integer REFERENCES vertices (vertex_id),
  head_vertex integer REFERENCES vertices (vertex_id),
  label       text,
  properties  json
);

CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);</code></pre>
<p>정점과 간선의 속성은 PostgreSQL의 <code>json</code> 타입 칼럼에 담고, 머리와 꼬리 정점은 간선마다 저장한다. </p>
<p>어떤 정점의 유입 및 유출 간선 집합이 필요하면 <code>edges</code> 테이블을 <code>head_vertex</code>나 <code>tail_vertex</code>로 질의한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>연결 제약이 없다:</strong></span> 어떤 정점이든 다른 어떤 정점과 간선으로 연결될 수 있고, 이를 제한하는 스키마가 없다</p>
</li>
<li><p><span style="color: indianred"><strong>양방향으로 순회할 수 있다:</strong></span> 정점의 유입과 유출 간선을 효율적으로 찾아 그래프를 순회할 수 있으며, 마지막 두 <code>CREATE INDEX</code>가 이를 위한 것이다</p>
</li>
<li><p><span style="color: indianred"><strong>레이블로 관계를 구분한다:</strong></span> 관계마다 다른 레이블을 쓰면 단일 그래프에 서로 다른 유형의 정보를 저장하면서도 모델을 깔끔하게 유지할 수 있다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>이 유연성 덕분에 국가마다 다른 지역 구조나 제각각인 데이터 입도처럼 관계형 스키마로 표현하기 어려운 데이터도 담을 수 있다.</strong></span></p>
<p>*<em>또한, 그래프는 발전성도 좋아서 기능을 추가할 때 데이터 구조 변경을 쉽게 수용한다. *</em></p>
<p>예를 들어 알레르겐을 정점으로 두고 사람과 알레르겐을 간선으로 이어 음식 알레르기를 표현하고, 어떤 음식에 어떤 물질이 들어 있는지 보여주는 정점 집합과 알레르겐을 연결하면 각 사람이 먹을 수 있는 안전한 음식을 찾는 질의도 작성할 수 있다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-사이퍼-질의-언어span"><span style='background-color: MediumAquamarine'>3. 사이퍼 질의 언어</span></h2>
<hr>

<p><strong>사이퍼(Cypher)는 Neo4j를 위해 만들어진 속성 그래프용 선언형 질의 언어다.</strong></p>
<pre><code class="language-sql">CREATE
  (NAmerica:Location {name:&#39;North America&#39;, type:&#39;continent&#39;}),
  (USA:Location      {name:&#39;United States&#39;, type:&#39;country&#39;}),
  (Idaho:Location    {name:&#39;Idaho&#39;,         type:&#39;state&#39;}),
  (Lucy:Person       {name:&#39;Lucy&#39;}),
  (Idaho) -[:WITHIN]-&gt;  (USA) -[:WITHIN]-&gt; (NAmerica),
  (Lucy)  -[:BORN_IN]-&gt; (Idaho)</code></pre>
<p>화살표 표기로 간선을 만들며, <code>(Idaho) -[:WITHIN]-&gt; (USA)</code>는 꼬리 정점이 Idaho이고 머리 정점이 USA인 <code>WITHIN</code> 레이블 간선이다.</p>
<p>미국에서 태어나 유럽으로 이민 온 사람의 이름을 찾는 질의는 이렇게 쓴다.</p>
<pre><code class="language-sql">MATCH
  (person) -[:BORN_IN]-&gt;  () -[:WITHIN*0..]-&gt; (us:Location {name:&#39;United States&#39;}),
  (person) -[:LIVES_IN]-&gt; () -[:WITHIN*0..]-&gt; (eu:Location {name:&#39;Europe&#39;})
RETURN person.name</code></pre>
<ul>
<li><p><span style="color: indianred"><strong><code>BORN_IN</code> 조건:</strong></span> person의 출생지에서 <code>WITHIN</code> 간선을 따라 올라가면 &quot;United States&quot;에 도달해야 한다</p>
</li>
<li><p><span style="color: indianred"><strong><code>LIVES_IN</code> 조건:</strong></span> 같은 person의 거주지에서 <code>WITHIN</code> 간선을 따라 올라가면 &quot;Europe&quot;에 도달해야 한다</p>
</li>
<li><p><span style="color: indianred"><strong><code>*0..</code>:</strong></span> 0회 이상 따라가라는 의미로, 정규 표현식의 <code>*</code> 연산자와 같다</p>
</li>
</ul>
<p>실행 방법은 여러 가지다. 모든 사람을 훑으며 출생지와 거주지를 확인할 수도 있고, <code>name</code>에 색인이 있다면 미국과 유럽 정점에서 시작해 <code>WITHIN</code> 유입 간선을 거꾸로 따라가며 사람을 찾을 수도 있다.</p>
<p><span style="color: indianred"><strong>선언형 질의 언어라서 이런 수행 방식을 지정할 필요가 없고, 질의 최적화기가 가장 효율적이라고 예측한 전략을 자동으로 선택한다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-sql의-그래프-질의span"><span style='background-color: MediumAquamarine'>4. SQL의 그래프 질의</span></h2>
<hr>

<p><strong>그래프 데이터를 관계형 테이블에 넣어도 SQL로 질의할 수는 있지만 어렵다.</strong></p>
<p><span style="color: indianred">*<em>관계형 질의는 필요한 조인을 미리 알고 있는 반면, 그래프 질의는 원하는 정점을 찾기 전에 몇 개의 간선을 지나야 할지 모르기 때문에 조인 수를 미리 고정할 수 없다. *</em></span></p>
<p><code>LIVES_IN</code> 간선이 도시를 바로 가리킬 수도 있고, 거리를 가리켜서 <strong><code>거리 → 도시 → 군 → 주 → 국가</code></strong>로 여러 단계를 올라가야 할 수도 있다.</p>
<p>SQL:1999 이후로는 재귀 공통 테이블 식(<code>WITH RECURSIVE</code>)으로 이 가변 순회를 표현할 수 있다.</p>
<pre><code class="language-sql">WITH RECURSIVE
  -- 미국 내 모든 지역의 정점 ID
  in_usa(vertex_id) AS (
      SELECT vertex_id FROM vertices WHERE properties-&gt;&gt;&#39;name&#39; = &#39;United States&#39;
    UNION
      SELECT edges.tail_vertex FROM edges
        JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
        WHERE edges.label = &#39;within&#39;
  ),
  -- 유럽 내 모든 지역의 정점 ID
  in_europe(vertex_id) AS (
      SELECT vertex_id FROM vertices WHERE properties-&gt;&gt;&#39;name&#39; = &#39;Europe&#39;
    UNION
      SELECT edges.tail_vertex FROM edges
        JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
        WHERE edges.label = &#39;within&#39;
  ),
  -- 미국에서 태어난 모든 사람
  born_in_usa(vertex_id) AS (
    SELECT edges.tail_vertex FROM edges
      JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
      WHERE edges.label = &#39;born_in&#39;
  ),
  -- 유럽에 사는 모든 사람
  lives_in_europe(vertex_id) AS (
    SELECT edges.tail_vertex FROM edges
      JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
      WHERE edges.label = &#39;lives_in&#39;
  )
SELECT vertices.properties-&gt;&gt;&#39;name&#39;
FROM vertices
  JOIN born_in_usa     ON vertices.vertex_id = born_in_usa.vertex_id
  JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;</code></pre>
<ul>
<li><p><span style="color: indianred"><strong><code>in_usa</code>, <code>in_europe</code>:</strong></span> 이름이 &quot;United States&quot;, &quot;Europe&quot;인 정점에서 시작해 <code>within</code> 유입 간선을 재귀적으로 따라가며 하위 지역을 모두 모은다</p>
</li>
<li><p><span style="color: indianred"><strong><code>born_in_usa</code>, <code>lives_in_europe</code>:</strong></span> 그 지역들로 들어오는 <code>born_in</code>, <code>lives_in</code> 간선을 따라가 사람을 구한다</p>
</li>
<li><p><span style="color: indianred"><strong>마지막 <code>SELECT</code>:</strong></span> 두 집합을 조인해 교집합을 구한다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>해당 질의문으로 보았듯이, 같은 질의를 사이퍼는 4줄, SQL은 29줄로 작성해야 한다.</strong></span> </p>
<p><strong>따라서, 데이터 모델마다 만족시키려는 사용 사례가 다르고, 그래서 애플리케이션에 맞는 데이터 모델을 고르는 일이 중요하다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-트리플-저장소와-스파클span"><span style='background-color: MediumAquamarine'>5. 트리플 저장소와 스파클</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue5-1-트리플-저장소span"><span style="color: SlateBlue">5-1. 트리플 저장소</span></h3>
<p>트리플 저장소는 속성 그래프와 거의 동등한 모델을 다른 용어로 설명한다.</p>
<p>모든 정보를 (주어, 서술어, 목적어)의 세 부분 구문으로 저장하며, (짐, 좋아하다, 바나나)에서 짐이 주어, 좋아하다가 서술어, 바나나가 목적어다.</p>
<p><strong>주어는 그래프의 정점에 해당하고, 목적어가 무엇이냐에 따라 서술어를 읽는 방법이 달라진다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>목적어가 원시 데이터타입 값일 때:</strong></span> 문자열이나 숫자 같은 값이면 서술어는 주어 정점의 속성 키, 목적어는 그 속성 값이 된다</p>
</li>
<li><p><span style="color: indianred"><strong>목적어가 다른 정점일 때:</strong></span> 서술어는 두 정점을 잇는 간선의 레이블이 되고, 주어는 꼬리 정점, 목적어는 머리 정점이 된다</p>
</li>
</ul>
<pre><code>(루시, 나이, 33) → [lucy]  { age: 33 }
                  속성 키 :  속성 값

(루시, 결혼하다, 알랭) → [lucy] ───  결혼하다 ───▶ [alain]
                      꼬리 정점   간선 레이블     머리 정점</code></pre><br>

<p><strong><span style="color: #12b886">터틀(Turtle) 형식</span></strong></p>
<p><strong>터틀은 Notation3(N3)의 부분 집합으로, 한 줄이 곧 트리플 하나(<code>주어 서술어 목적어.</code>)다.</strong></p>
<pre><code class="language-turtle">@prefix : &lt;urn:example:&gt;.

_:lucy     a        :Person.
_:lucy     :name    &quot;Lucy&quot;.
_:lucy     :bornIn  _:idaho.
_:idaho    a        :Location.
_:idaho    :name    &quot;Idaho&quot;.
_:idaho    :type    &quot;state&quot;.
_:idaho    :within  _:usa.
_:usa      a        :Location.
_:usa      :name    &quot;United States&quot;.
_:usa      :type    &quot;country&quot;.
_:usa      :within  _:namerica.
_:namerica a        :Location.
_:namerica :name    &quot;North America&quot;.
_:namerica :type    &quot;continent&quot;.</code></pre>
<p>위의 두 경우는 목적어의 모양으로 구분해서 읽는다.</p>
<ul>
<li><p><span style="color: indianred"><strong>목적어가 문자열 리터럴이면 서술어는 속성이다:</strong></span> <code>_:usa :name &quot;United States&quot;.</code>는 정점 usa의 <code>name</code> 속성 값이 &quot;United States&quot;라는 뜻이다</p>
</li>
<li><p><span style="color: indianred"><strong>목적어가 정점이면 서술어는 간선이다:</strong></span> <code>_:idaho :within _:usa.</code>는 idaho에서 usa로 가는 <code>within</code> 간선이라는 뜻이다</p>
</li>
<li><p><span style="color: indianred"><strong><code>_:</code> 이름:</strong></span> 파일 외부의 무언가를 뜻하지 않으며, 여러 트리플이 같은 정점을 가리킨다는 것을 나타내기 위한 로컬 식별자다</p>
</li>
</ul>
<p>*<em>이때, 같은 주어를 매 줄 반복하는 건 번거로우므로, 세미콜론으로 같은 주어에 대한 여러 서술어를 묶어 쓸 수 있다. *</em></p>
<pre><code class="language-turtle">@prefix : &lt;urn:example:&gt;.

_:lucy     a :Person;   :name &quot;Lucy&quot;;          :bornIn _:idaho.
_:idaho    a :Location; :name &quot;Idaho&quot;;         :type &quot;state&quot;;   :within _:usa.
_:usa      a :Location; :name &quot;United States&quot;; :type &quot;country&quot;; :within _:namerica.
_:namerica a :Location; :name &quot;North America&quot;; :type &quot;continent&quot;.</code></pre>
<br>

<h3 id="span-stylecolor-slateblue5-2-시맨틱-웹과-rdfspan"><span style="color: SlateBlue">5-2. 시맨틱 웹과 RDF</span></h3>
<p>트리플 저장소는 시맨틱 웹과 독립적이지만 자주 함께 언급된다.</p>
<ul>
<li><p><span style="color: indianred"><strong>시맨틱 웹:</strong></span> 웹 사이트가 사람이 읽는 텍스트뿐 아니라 기계가 판독 가능한 데이터로도 정보를 게시하자는 개념이다.</p>
</li>
<li><p><span style="color: indianred"><strong>RDF(Resource Description Framework):</strong></span> 서로 다른 웹 사이트가 일관된 형식으로 데이터를 게시하는 방법으로, 다른 사람의 데이터와 결합할 때 이름이 충돌하지 않도록 주어, 서술어, 목적어에 <code>&lt;http://my-company.com/namespace#within&gt;</code> 같은 URI를 쓴다.</p>
</li>
</ul>
<p>시맨틱 웹 자체는 널리 실현되지 않았지만, 트리플은 애플리케이션의 내부 데이터 모델로도 충분히 쓸 만하다.</p>
<br>

<h3 id="span-stylecolor-slateblue5-3-스파클span"><span style="color: SlateBlue">5-3. 스파클</span></h3>
<p><strong>스파클(SPARQL)은 RDF 데이터 모델을 쓰는 트리플 저장소 질의 언어로, 사이퍼보다 먼저 나왔고 사이퍼가 스파클의 패턴 매칭을 차용해서 둘은 매우 비슷하다.</strong></p>
<pre><code class="language-sql">PREFIX : &lt;urn:example:&gt;

SELECT ?personName WHERE {
  ?person :name ?personName.
  ?person :bornIn  / :within* / :name &quot;United States&quot;.
  ?person :livesIn / :within* / :name &quot;Europe&quot;.
}</code></pre>
<p>변수는 <code>?</code>로 시작하며, RDF는 속성과 간선을 구분하지 않고 서술어만 쓰기 때문에, <code>:name &quot;United States&quot;</code> 같은 속성 매칭도 간선 순회와 같은 구문으로 쓴다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-초석--데이터로그span"><span style='background-color: MediumAquamarine'>6. 초석 : 데이터로그</span></h2>
<hr>

<p><strong>데이터로그(Datalog)는 스파클, 사이퍼보다 훨씬 오래된 언어로 1980년대 학계에서 광범위하게 연구됐고, 이후 질의 언어의 기반이 됐다.</strong></p>
<p><span style="color: indianred"><strong>데이터 모델은 트리플 저장소와 비슷하되 (주어, 서술어, 목적어) 대신 서술어(주어, 목적어)로 쓴다.</strong></span></p>
<pre><code class="language-datalog">name(namerica, &#39;North America&#39;).   
type(namerica, continent).

name(usa, &#39;United States&#39;).        
type(usa, country).    
within(usa, namerica).

name(idaho, &#39;Idaho&#39;).              
type(idaho, state).    
within(idaho, usa).

name(lucy, &#39;Lucy&#39;).                
born_in(lucy, idaho).

within_recursive(Location, Name) :- name(Location, Name).          /* 규칙 1 */
within_recursive(Location, Name) :- within(Location, Via),         /* 규칙 2 */
                                    within_recursive(Via, Name).

migrated(Name, BornIn, LivingIn) :- name(Person, Name),            /* 규칙 3 */
                                    born_in(Person, BornLoc),
                                    within_recursive(BornLoc, BornIn),
                                    lives_in(Person, LivingLoc),
                                    within_recursive(LivingLoc, LivingIn).

?- migrated(Who, &#39;United States&#39;, &#39;Europe&#39;).   /* Who = &#39;Lucy&#39; */</code></pre>
<ul>
<li><p><span style="color: indianred"><strong>규칙:</strong></span> <code>:-</code> 오른편의 서술어가 모두 대응되면 왼편이 DB에 추가된다. 규칙은 다른 규칙을 참조하거나 자기 자신을 재귀 호출할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>변수:</strong></span> 대문자로 시작하는 단어이며, <code>name(Location, Name)</code>은 <code>name(namerica, &#39;North America&#39;)</code>와 대응될 때 <code>Location = namerica</code>, <code>Name = &#39;North America&#39;</code>가 된다</p>
</li>
<li><p><span style="color: indianred"><strong>재귀 적용:</strong></span> 규칙 1이 <code>within_recursive(namerica, &#39;North America&#39;)</code>를 만들고, 규칙 2가 이를 <code>within(usa, namerica)</code>와 결합해 <code>within_recursive(usa, &#39;North America&#39;)</code>를, 다시 <code>within(idaho, usa)</code>와 결합해 <code>within_recursive(idaho, &#39;North America&#39;)</code>를 만든다</p>
</li>
</ul>
<!-- IMG: 그림 2-6 데이터로그 규칙을 사용해 아이다호가 북아메리카에 있는지 확인 -->

<p>사이퍼와 스파클이 <code>SELECT</code>로 바로 질의하는 것과 달리, 데이터로그는 규칙을 정의하며 한 번에 조금씩 나아간다.</p>
<p><span style="color: indianred"><strong>간단한 일회성 질의에는 불편하지만, 규칙을 결합하고 재사용할 수 있어 데이터가 복잡할수록 효과적이다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine7-정리span"><span style='background-color: MediumAquamarine'>7. 정리</span></h2>
<hr>

<p><strong>역사적으로 데이터를 하나의 큰 트리인 계층 모델로 표현하려 했지만 다대다 관계에 적합하지 않아 관계형 모델이 고안됐고, 관계형 모델에도 맞지 않는 애플리케이션을 위해 NoSQL이 두 갈래로 등장했다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>문서 데이터베이스:</strong></span> 데이터가 문서 자체에 포함돼 있고 문서 간 관계가 거의 없는 사용 사례</p>
</li>
<li><p><span style="color: indianred"><strong>그래프 데이터베이스:</strong></span> 모든 것이 잠재적으로 관련 있는 사용 사례</p>
</li>
</ul>
<p><span style="color: indianred"><strong>한 모델을 다른 모델로 흉내 낼 수는 있지만 결과는 대부분 엉망이며, 이러한 문제가 결국 단일 만능 솔루션이 아니라 목적에 맞는 시스템을 골라야 하는 이유다.</strong></span></p>
<p>문서 DB와 그래프 DB는 스키마를 강제하지 않아 요구사항 변화에 유연하지만, 애플리케이션은 여전히 데이터 구조를 가정하므로 결국 스키마가 명시적(쓰기 스키마)이냐 암시적(읽기 스키마)이냐의 차이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDIA (데이터 중심 애플리케이션 설계) Chapter 2]]></title>
            <link>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-2</link>
            <guid>https://velog.io/@taekyu_/DDIA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-2</guid>
            <pubDate>Wed, 16 Sep 2026 04:56:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는
데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
<a href="https://www.wikibook.co.kr/data-intensive/">https://www.wikibook.co.kr/data-intensive/</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** DDIA 2주차 - 02장. 데이터 모델과 질의 언어 **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-데이터-모델은-왜-중요한가span"><span style='background-color: MediumAquamarine'>1. 데이터 모델은 왜 중요한가</span></h2>
<hr>

<p><strong>데이터 모델은 소프트웨어가 어떻게 작성됐는지뿐만 아니라, 해결하려는 문제를 어떻게 생각해야 하는지에도 지대한 영향을 미친다.</strong></p>
<p>대부분의 애플리케이션은 하나의 데이터 모델 위에 다른 데이터 모델을 계층으로 쌓아 만든다.
<img src="https://velog.velcdn.com/images/taekyu_/post/b29157ee-a3b1-45aa-9276-92958500ba06/image.png" alt=""></p>
<ol>
<li><p><span style="color: indianred"><strong>애플리케이션 개발자:</strong></span> 현실(사람, 조직, 상품, 행동, 자금 흐름, 센서)을 객체나 데이터 구조로 모델링한다. 보통 애플리케이션에 특화돼 있다</p>
</li>
<li><p><span style="color: indianred"><strong>저장 계층:</strong></span> 그 데이터 구조를 JSON/XML 문서, 관계형 테이블, 그래프 같은 <strong>범용 데이터 모델</strong>로 표현한다</p>
</li>
<li><p><span style="color: indianred"><strong>DB 엔지니어:</strong></span> 그것을 메모리, 디스크, 네트워크 상의 바이트 단위로 표현하는 방법을 결정한다</p>
</li>
<li><p><span style="color: indianred"><strong>하드웨어 엔지니어:</strong></span> 전류, 빛의 파동, 자기장으로 바이트를 표현한다</p>
</li>
</ol>
<p><strong>각 계층은 명확한 데이터 모델을 제공해 하위 계층의 복잡성을 숨긴다.</strong> 이 추상화 덕분에 DB 벤더의 엔지니어와 애플리케이션 개발자처럼 서로 다른 그룹이 효율적으로 함께 일할 수 있다.</p>
<p><span style="color: indianred"><strong>그리고 각 데이터 모델은 사용 방법에 대한 가정을 담고 있다. 어떤 사용법은 쉽고 어떤 동작은 아예 지원하지 않으며, 어떤 연산은 빠르지만 어떤 연산은 매우 느리다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-관계형-모델과-문서-모델span"><span style='background-color: MediumAquamarine'>2. 관계형 모델과 문서 모델</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-관계형-모델의-역사와-nosqlspan"><span style="color: SlateBlue">2-1. 관계형 모델의 역사와 NoSQL</span></h3>
<ul>
<li><p>1970년 Edgar Codd가 관계형 모델을 제안했다. </p>
</li>
<li><p><strong>데이터는 관계(relation, SQL의 Table)로 구성되고, 관계는 순서 없는 튜플(tuple, SQL의 Row) 모음</strong>이다.</p>
</li>
<li><p>당시엔 효율적 구현이 가능하냐는 의문이 많았지만, 1980년대 중반 RDBMS와 SQL이 <span style="color: indianred"><strong>정규화된 구조로 데이터를 저장하고 질의할 필요가 있는 사람들 대부분이 선택하는 도구</strong></span>가 됐다</p>
</li>
<li><p><strong>관계형 데이터베이스의 우위는 약 25~30년간 지속됐다</strong></p>
</li>
</ul>
<p><strong>경쟁자들은 모두 밀려났다.</strong></p>
<table>
<thead>
<tr>
<th>시기</th>
<th>경쟁 모델</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>1970~80년대 초</td>
<td>네트워크 모델, 계층 모델</td>
<td>관계형 모델이 우위를 차지</td>
</tr>
<tr>
<td>1980년대 후반~90년대 초</td>
<td>객체 데이터베이스</td>
<td>나타났다가 사라짐</td>
</tr>
<tr>
<td>2000년대 초반</td>
<td>XML 데이터베이스</td>
<td>매우 적게 채택됨</td>
</tr>
</tbody></table>
<p><strong>관계형 DB는 본래 영역(비즈니스 데이터 처리)을 넘어 폭넓게 보편화됐다.</strong> </p>
<p>온라인 게시물, 토론, 소셜 네트워크, 전자 상거래, 게임, SaaS 생산성 애플리케이션 등 오늘날 웹에서 볼 수 있는 대부분의 서비스가 여전히 관계형 DB로 제공된다.</p>
<br>

<p><strong><span style="color: #12b886">NoSQL</span></strong></p>
<p>2010년대의 NoSQL은 관계형 모델의 우위를 뒤집으려는 가장 최신 시도다. </p>
<p><strong>원래 NoSQL은 특정 기술을 가리키는 이름이 아니었고, 이후 Not Only SQL로 재해석됐다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/f8b01da9-6a10-4018-b31b-3b2a714fb310/image.png" width="60%" height="n%"></p>
<ul>
<li><p><span style="color: indianred"><strong>확장성:</strong></span> 대규모 데이터셋이나 매우 높은 쓰기 처리량을 관계형 DB보다 쉽게 달성할 필요</p>
</li>
<li><p><span style="color: indianred"><strong>오픈소스 선호:</strong></span> 상용 DB 제품보다 무료 오픈소스 소프트웨어에 대한 선호도 확산</p>
</li>
<li><p><span style="color: indianred"><strong>특수 질의 동작:</strong></span> 관계형 모델에서 지원하지 않는 질의</p>
</li>
<li><p><span style="color: indianred"><strong>표현력:</strong></span> 관계형 스키마의 제한에 대한 불만과 더 동적이고 표현력 풍부한 데이터 모델에 대한 바람</p>
</li>
</ul>
<p><strong>어플리케이션은 저마다 요구사항이 다르므로, 각 상황에 맞는 최적의 기술을 선택하여야한다.</strong></p>
<p><span style="color: indianred"><strong>가까운 미래에 관계형 DB는 다양한 비관계형 데이터스토어와 함께 사용될 것이며, 이를 다중 저장소 지속성이라 한다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue2-2-객체-관계형-불일치span"><span style="color: SlateBlue">2-2. 객체 관계형 불일치</span></h3>
<p>오늘날 대부분의 애플리케이션은 객체지향 언어로 개발한다. 그런데 데이터를 관계형 테이블에 저장하려면 <strong>애플리케이션 코드와 DB 모델 객체(테이블, 로우, 칼럼) 사이에 거추장스러운 전환 계층</strong>이 필요하다.</p>
<p><span style="color: indianred"><strong>이 분리를 임피던스 불일치(impedance mismatch)라 부른다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/24b6d186-709c-4a48-a462-50f8d7efee55/image.png" width="60%" height="n%"></p>
<p><strong>ActiveRecord나 Hibernate 같은 ORM은 전환 계층에 필요한 상용구 코드의 양을 줄이지만, 두 모델 간의 차이를 완벽히 숨길 수 없다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/ffaea4ef-d0c5-4ebb-bacc-16f587239b2d/image.png" width="60%" height="n%"></p>
<p>책은 링크트인 이력서를 예로 든다. <code>first_name</code>, <code>last_name</code>은 사용자당 하나씩이라 <code>users</code> 테이블 칼럼으로 충분하다. 하지만 <strong>경력, 학력, 연락처는 사용자와 일대다 관계</strong>다.</p>
<br>

<p><strong><span style="color: #12b886">일대다를 표현하는 세 가지 방법</span></strong></p>
<ol>
<li><p><span style="color: indianred"><strong>정규화 (SQL:1999 이전의 일반적 방식):</strong></span> 직위, 학력, 연락처를 개별 테이블에 넣고 <strong>외래 키로 <code>users</code>를 참조</strong>한다</p>
</li>
<li><p><span style="color: indianred"><strong>구조화된 데이터타입 / XML, JSON 칼럼:</strong></span> SQL 표준의 마지막 버전이 추가한 방식. </p>
</li>
</ol>
<p><strong>단일 로우에 다중 값을 저장하고 문서 내 질의와 색인이 가능</strong>하다. </p>
<ol start="3">
<li><span style="color: indianred"><strong>텍스트 칼럼에 통째로 저장:</strong></span> JSON/XML로 부호화해 넣고 애플리케이션이 구조와 내용을 해석한다. <strong>해당 방식은 일반적으로 부호화된 칼럼의 값을 DB로 질의할 수 없다</strong></li>
</ol>
<br>

<h3 id="span-stylecolor-slateblue2-3-일대다-관계와-문서-모델의-지역성span"><span style="color: SlateBlue">2-3. 일대다 관계와 문서 모델의 지역성</span></h3>
<p><strong>이력서처럼 모든 내용을 갖추고 있는 데이터 구조는 JSON 표현에 매우 적합하다.</strong> 
MongoDB, RethinkDB, CouchDB, Espresso 같은 문서 지향 DB가 JSON 데이터 모델을 지원한다.</p>
<pre><code class="language-yaml">{
  &quot;user_id&quot;: 251,
  &quot;first_name&quot;: &quot;Bill&quot;,
  &quot;last_name&quot;: &quot;Gates&quot;,
  &quot;summary&quot;: &quot;Co-chair of the Bill &amp; Melinda Gates... Active blogger.&quot;,
  &quot;region_id&quot;: &quot;us:91&quot;,
  &quot;industry_id&quot;: 131,
  &quot;positions&quot;: [
    {&quot;job_title&quot;: &quot;Co-chair&quot;, &quot;organization&quot;: &quot;Bill &amp; Melinda Gates Foundation&quot;},
    {&quot;job_title&quot;: &quot;Co-founder, Chairman&quot;, &quot;organization&quot;: &quot;Microsoft&quot;}
  ],
  &quot;education&quot;: [
    {&quot;school_name&quot;: &quot;Harvard University&quot;, &quot;start&quot;: 1973, &quot;end&quot;: 1975},
    {&quot;school_name&quot;: &quot;Lakeside School, Seattle&quot;, &quot;start&quot;: null, &quot;end&quot;: null}
  ],
  &quot;contact_info&quot;: {
    &quot;blog&quot;: &quot;http://thegatesnotes.com&quot;,
    &quot;twitter&quot;: &quot;http://twitter.com/BillGates&quot;
  }
}</code></pre>
<p><span style="color: indianred"><strong>JSON 표현은 다중 테이블 스키마보다 더 나은 지역성을 갖는다.</strong></span></p>
<p>관계형 예제에서 프로필을 가져오려면 <strong>다중 질의</strong>(각 테이블에 <code>user_id</code>로 질의)를 하거나 <strong>난잡한 다중 조인</strong>을 수행해야 한다. <span style="color: indianred"><strong>하지만, JSON 표현에서는 모든 관련 정보가 한 곳에 있어 질의 하나로 충분하다</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue2-4-다대일과-다대다-관계span"><span style="color: SlateBlue">2-4. 다대일과 다대다 관계</span></h3>
<p>앞의 JSON에서 <code>region_id</code>와 <code>industry_id</code>가 평문 &quot;그레이터 시애틀 구역&quot;, &quot;자선활동&quot;이 아니라 <strong>ID로 주어진 점</strong>에 주목해야 한다.</p>
<p><strong>표준 목록에서 선택하게 하고 ID로 참조하면 얻는 것</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>모호함 회피:</strong></span> 이름이 같은 여러 도시가 있는 경우</p>
</li>
<li><p><span style="color: indianred"><strong>갱신의 편의성:</strong></span> 이름이 한 곳에만 저장되므로 전반적으로 갱신하기 쉽다</p>
</li>
<li><p><span style="color: indianred"><strong>현지화 지원:</strong></span> 표준 목록을 현지화해 사이트를 보는 사람의 언어로 표시할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>더 나은 검색:</strong></span> 지역 목록에 <strong>&quot;시애틀이 워싱턴에 있다&quot;는 사실을 부호화</strong>할 수 있다. 문자열 &quot;그레이터 시애틀 구역&quot;만으로는 &quot;워싱턴&quot;을 식별하지 못한다</p>
</li>
<li><p><span style="color: indianred"><strong>일관성:</strong></span> 프로필 간 일관된 스타일과 철자</p>
</li>
</ul>
<p><strong>ID의 핵심 장점은 ID 자체에 아무 의미가 없어서 변경할 필요가 없다는 점이다.</strong> 식별 정보가 바뀌어도 ID는 그대로 유지된다.</p>
<p>반대로 의미를 가지는 값이라면 언젠가 바꿔야 하고, <strong>정보가 중복돼 있으면 모든 중복 항목을 변경해야 한다.</strong> 여기엔 <strong>쓰기 오버헤드와 불일치 위험</strong>(일부만 갱신되는 상황)이 따른다.</p>
<p><span style="color: indianred"><strong>이런 중복을 제거하는 일이 데이터베이스 정규화 이면에 놓인 핵심 개념이다.</strong></span></p>
<br>

<p><strong><span style="color: #12b886">그런데 다대일 관계는 문서 모델에 적합하지 않다</span></strong></p>
<p>중복을 정규화하려면 <strong>다대일(many-to-one) 관계</strong>가 필요하다. 많은 사람이 한 지역에 살고, 많은 사람이 한 업계에서 일한다.</p>
<ul>
<li><p>관계형 DB에서는 <strong>조인이 쉽기 때문에</strong> ID로 다른 테이블의 로우를 참조하는 방식이 일반적이다</p>
</li>
<li><p><span style="color: indianred"><strong>문서 DB는 일대다 트리 구조에는 조인이 필요 없지만, 조인 지원 자체가 보통 약하다</strong></span></p>
</li>
<li><p>DB가 조인을 지원하지 않으면 <span style="color: indianred"><strong>애플리케이션 코드에서 다중 질의로 조인을 흉내 내야 한다.</strong></span> 조인을 만드는 작업이 DB에서 애플리케이션 코드로 옮겨간다. -&gt; 따라서, 개발자에게 그 몫이 넘어가게 된다.</p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">더 큰 문제는 데이터가 점점 상호 연결된다는 점이다</span></strong></p>
<p>초기 버전이 조인 없는 문서 모델에 적합하더라도, <strong>기능을 추가하면서 데이터는 점차 상호 연결되는 경향이 있다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/7ff8c2ce-0d6f-4e6f-9c06-0d90a754318f/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>조직과 학교를 엔티티로:</strong></span> <code>organization</code>, <code>school_name</code>이 문자열이 아니라 엔티티 참조라면, 각 이력서에서 조직 페이지로 링크하고 로고, 뉴스 피드 같은 추가 정보를 포함할 수 있다</p>
</li>
<li><p><span style="color: indianred"><strong>추천서 기능:</strong></span> 추천서에는 추천인의 이름과 사진이 함께 보여진다. 추천인이 사진을 갱신하면 그가 작성한 모든 추천서에 새 사진이 반영돼야 한다. 따라서 추천서는 작성자 프로필을 참조해야 한다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>이런 기능은 다대다(many-to-many) 관계를 필요로 하고, 질의할 때 조인이 필요해진다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-문서-데이터베이스는-역사를-반복하고-있나span"><span style='background-color: MediumAquamarine'>3. 문서 데이터베이스는 역사를 반복하고 있나</span></h2>
<hr>

<p><strong>다대다 관계를 어떻게 표현할 것인가 하는 논쟁은 NoSQL보다 훨씬 오래됐다.</strong> 사실상 가장 초기의 전산화 데이터베이스 시스템까지 거슬러 올라간다.</p>
<br>

<h3 id="span-stylecolor-slateblue3-1-계층-모델--ibm-imsspan"><span style="color: SlateBlue">3-1. 계층 모델 : IBM IMS</span></h3>
<ul>
<li><p>1970년대 비즈니스 데이터 처리에 가장 많이 쓰인 DB는 IBM의 <strong>IMS(Information Management System)</strong>다. 1968년 상업 출시됐고 오늘날에도 IBM 메인프레임에서 사용 및 유지보수되고 있다.</p>
</li>
<li><p>IMS는 계층 모델을 사용했다. 모든 데이터를 <strong>레코드 내에 중첩된 레코드 트리</strong>로 표현한다. 문서 DB의 JSON 모델과 비슷하다.</p>
</li>
</ul>
<p><span style="color: indianred"><strong>IMS도 일대다 관계에서는 잘 동작했지만, 다대다 관계 표현이 어려웠고 조인을 지원하지 않았다.</strong></span></p>
<p>개발자는 데이터를 비정규화해서 중복할지, 아니면 레코드 간 참조를 수동으로 해결할지 결정해야 했다.</p>
<p><strong>1960~70년대의 이 문제는 오늘날 문서 데이터베이스로 작업하는 개발자가 풀어야 할 문제와 매우 비슷하다.</strong></p>
<br>

<h3 id="span-stylecolor-slateblue3-2-네트워크-모델--코다실과-접근-경로span"><span style="color: SlateBlue">3-2. 네트워크 모델 : 코다실과 접근 경로</span></h3>
<p>계층 모델의 한계를 풀기 위해 두 해결책이 경쟁했다. </p>
<p><strong>관계형 모델과 네트워크 모델이며, 두 진영의 논쟁은 1970년대 내내 이어졌다.</strong></p>
<p>네트워크 모델은 코다실(CODASYL) 위원회가 표준화해 코다실 모델이라고도 부른다.</p>
<ul>
<li><p><strong>코다실은 계층 모델을 일반화한다.</strong> 계층 모델에서 모든 레코드는 부모가 정확히 하나지만, <span style="color: indianred"><strong>네트워크 모델에서 레코드는 다중 부모를 가질 수 있다.</strong></span> 그래서 다대일과 다대다를 모델링할 수 있다.</p>
</li>
<li><p>레코드 간 연결은 외래 키보다 프로그래밍 언어의 포인터에 가깝다 </p>
</li>
</ul>
<p><span style="color: indianred"><strong>레코드에 접근하는 유일한 방법은 최상위 레코드에서부터 연속된 연결 경로를 따르는 것이다. 
  이를 접근 경로라 한다.</strong></span></p>
<ul>
<li><p>하지만 <strong>다대다의 세계에서는 다양한 경로가 같은 레코드로 이어지고, 프로그래머는 이 경로들을 계속 추적해야 한다</strong></p>
</li>
<li><p><strong>코다실 위원회조차 이 방식이 n차원 데이터 공간을 항해하는 것과 같다고 인정했다</strong></p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">데이터가 어떻게 연결돼 있나</span></strong></p>
<p><strong>테이블과 외래 키도 없으며, 레코드가 포인터로 줄줄이 엮여 있는 형태이다.</strong></p>
<pre><code> [지역: 그레이터 시애틀 구역]          ← 최상위 레코드(root)
            │
            │ 첫 멤버 포인터
            ▼
    [사용자: Bill] ──────▶ [사용자: Alice] ──────▶ [사용자: Carol] ──▶ (끝)
            │                    다음 멤버 포인터
            │ 첫 직업 포인터
            ▼
    [직업: Microsoft] ──▶ [직업: Gates 재단] ──▶ (끝)</code></pre><br>

<p><strong><span style="color: #12b886">그래서 질의는 이렇게 생겼다</span></strong></p>
<p><strong>&quot;시애틀에 사는 사람 중 Microsoft에서 일한 적 있는 사람 찾기&quot;</strong>를 코다실에서 하면, 애플리케이션 코드가 커서를 직접 움직여야 한다.</p>
<pre><code>  커서 위치                         애플리케이션 코드가 해야 하는 일
  ────────────────────────────────────────────────────────────────
① [지역: 시애틀]                    진입점을 코드에 직접 지정한다
②  └▶ [사용자: Bill]                첫 멤버로 커서를 옮긴다
③      └▶ [직업: Microsoft]         그 사용자의 첫 직업으로 또 옮긴다
④          organization == ?        값을 직접 비교한다 → 일치, 수집
⑤      └▶ [직업: Gates 재단]        다음 직업으로 옮겨 ④를 반복
⑥          (직업 목록 끝)           사용자 레벨로 되돌아간다
⑦  └▶ [사용자: Alice]               다음 멤버로 옮겨 ③부터 반복
⑧      ...                          지역의 멤버가 끝날 때까지 계속</code></pre><p><span style="color: indianred"><strong>무엇을 원하는가가 아니라 어디로 어떻게 걸어갈 것인가를 전부 코드로 쓴 것이다.</strong></span> 오늘날의 <code>WHERE region = &#39;시애틀&#39; AND org = &#39;Microsoft&#39;</code> 한 줄이 위의 8단계 순회로 손에 들려 있었다.</p>
<p><strong>수동 접근 경로 선택은 1970년대의 제한된 하드웨어인 탐색이 매우 느린 테이프 드라이브를 가장 효율적으로 쓰는 방법이었다.</strong></p>
<p><span style="color: indianred"><strong>하지만 원하는 데이터에 대한 경로가 없으면 어려운 상황에 놓인다.</strong></span></p>
<p>접근 경로를 바꾸려면 아주 많은 수작업 질의 코드를 살펴보고 재작성해야 했으며, 애플리케이션의 데이터 모델을 바꾸는 작업이 매우 어려운 일이었다.</p>
<br>

<h3 id="span-stylecolor-slateblue3-3-관계형-모델--접근-경로를-자동으로-만든다span"><span style="color: SlateBlue">3-3. 관계형 모델 : 접근 경로를 자동으로 만든다</span></h3>
<p><strong>대조적으로 관계형 모델이 하는 일은 알려진 모든 데이터를 그냥 배치하는 것이다.</strong></p>
<p>관계(Table)는 단순히 튜플(Row)의 컬렉션이 전부다.</p>
<ul>
<li><p>얽히고설킨 중첩 구조가 없고, 따라가야 할 복잡한 접근 경로도 없다</p>
</li>
<li><p>임의 조건과 일치하는 로우를 선택해 읽을 수 있고, 일부 칼럼을 키로 지정해 특정 로우를 읽을 수 있다</p>
</li>
<li><p>외래 키 관계를 신경 쓰지 않고 임의 테이블에 새 로우를 삽입할 수 있다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>관계형 데이터베이스에서 질의 최적화기(query optimizer)는 질의의 어느 부분을 어떤 순서로 실행할지를 결정하고 사용할 색인을 자동으로 결정한다.</strong></span></p>
<p><strong>이 선택은 실제로 접근 경로이다. 큰 차이점은 접근 경로를 애플리케이션 개발자가 아니라 질의 최적화기가 자동으로 만든다는 점이다. 그래서 접근 경로를 따로 생각할 필요가 없다.</strong></p>
<ul>
<li><p>새로운 방식으로 질의하고 싶으면 <span style="color: indianred"><strong>새 색인을 선언하기만 하면 된다.</strong></span> 질의는 자동으로 가장 적합한 색인을 사용하며, 새 색인을 쓰려고 질의를 바꿀 필요가 없다.</p>
</li>
<li><p>관계형 DB의 질의 최적화기는 복잡해서 수년 간의 연구와 개발 노력을 필요로 한다</p>
</li>
<li><p><strong>질의 최적화기가 없다면 범용 최적화기를 만들기보다 특정 질의를 위한 접근 경로를 직접 코딩하는 게 쉽지만, 장기적으로는 범용 솔루션이 유리하다</strong></p>
</li>
</ul>
<p><span style="color: indianred"><strong>주목할 점은 질의 최적화기를 한번 만들면 그 DB를 사용하는 모든 애플리케이션이 혜택을 받는다는 것이다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue3-4-문서-데이터베이스와의-비교span"><span style="color: SlateBlue">3-4. 문서 데이터베이스와의 비교</span></h3>
<p><strong>문서 DB는 한 가지 측면에서 계층 모델로 되돌아갔다.</strong> </p>
<p>별도 테이블이 아닌 상위 레코드 내에 중첩된 레코드(<code>positions</code>, <code>education</code>, <code>contact_info</code> 같은 일대다 관계)를 저장한다.</p>
<p><span style="color: indianred"><strong>하지만 다대일과 다대다를 표현할 때 관계형 DB와 문서 DB는 근본적으로 다르지 않다.</strong></span></p>
<ul>
<li><p>둘 다 관련 항목을 고유한 식별자로 참조한다. </p>
</li>
<li><p>관계형에서는 외래 키, 문서 모델에서는 문서 참조라 부른다</p>
</li>
<li><p>이 식별자는 조인이나 후속 질의를 사용해 읽기 시점에 확인한다</p>
</li>
</ul>
<p><strong>즉, 현재까지는 문서 데이터베이스가 코다실의 전철을 밟지 않고 있다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-관계형과-문서-오늘날의-비교span"><span style='background-color: MediumAquamarine'>4. 관계형과 문서, 오늘날의 비교</span></h2>
<hr>

<p>두 모델을 비교할 때 고려할 차이는 많지만, 2장은 데이터 모델의 차이만 본다.</p>
<table>
<thead>
<tr>
<th></th>
<th>문서 모델</th>
<th>관계형 모델</th>
</tr>
</thead>
<tbody><tr>
<td>강점</td>
<td>스키마 유연성, 지역성, 애플리케이션 데이터 구조와의 근접성</td>
<td>조인, 다대일, 다대다 관계</td>
</tr>
<tr>
<td>적합한 데이터</td>
<td>일대다 트리를 한 번에 통째로 적재</td>
<td>상호 연결이 많음</td>
</tr>
</tbody></table>
<br>

<h3 id="span-stylecolor-slateblue4-1-어떤-모델이-코드를-더-간단하게-하는가span"><span style="color: SlateBlue">4-1. 어떤 모델이 코드를 더 간단하게 하는가</span></h3>
<p><strong>데이터가 문서처럼 생겼다면 문서 모델이 코드를 단순하게 만든다. 이걸 억지로 여러 테이블로 찢으면 다루기 힘든 스키마와 불필요하게 복잡한 코드가 나온다.</strong></p>
<p>문서 모델의 제한은 두 가지다.</p>
<ul>
<li><p>문서 내 중첩 항목을 직접 참조할 수 없다. &quot;사용자 251의 직위 목록의 두 번째 항목&quot;처럼 위치로 지정해야 한다. 코다실 모델의 접근 경로와 같은 방식이다</p>
</li>
<li><p>조인 지원이 미흡하다. 다만 이벤트 로그를 쌓는 분석 애플리케이션처럼 다대다가 아예 없는 경우라면 문제되지 않는다</p>
</li>
</ul>
<p><span style="color: indianred"><strong>다대다를 쓰는 순간부터 계산이 달라진다.</strong></span></p>
<ul>
<li><p>비정규화로 조인을 줄일 수는 있지만, 그 대신 애플리케이션이 일관성 유지를 떠안는다.</p>
</li>
<li><p>다중 요청으로 조인을 흉내 내면 복잡도가 애플리케이션으로 넘어올 뿐 아니라, DB 안에서 도는 조인보다 느리다</p>
</li>
</ul>
<p>결국 어떤 모델이 낫냐는 데이터 항목 사이에 어떤 관계가 있느냐에 달렸다. </p>
<p><span style="color: indianred"><strong>상호 연결이 많은 데이터라면 문서 모델은 곤란하고, 관계형은 무난하며, 그래프 모델은 매우 자연스럽다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue4-2-스키마-유연성-읽기-스키마와-쓰기-스키마span"><span style="color: SlateBlue">4-2. 스키마 유연성 (읽기 스키마와 쓰기 스키마)</span></h3>
<p>대부분의 문서 DB와 관계형 DB가 지원하는 JSON은 문서의 데이터에 어떤 스키마도 강요하지 않는다. </p>
<p><strong>임의의 키와 값을 문서에 추가할 수 있고, 읽을 때 클라이언트는 그 필드가 있는지 보장받지 못한다.</strong></p>
<p>그래서 문서 DB를 종종 Schemaless라 부르지만, 이 용어에는 오해의 소지가 있다. 데이터를 읽는 코드는 보통 구조의 유형을 어느 정도 가정하기 때문이다.</p>
<p><span style="color: indianred"><strong>즉 암묵적인 스키마는 있고, DB가 그것을 강요하지 않을 뿐이다. 조금 더 정확한 용어가 읽기 스키마와 쓰기 스키마다.</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>쓰기 스키마:</strong></span> 관계형 DB의 전통적인 접근 방식으로, 스키마가 명시적이고 쓰여진 모든 데이터가 스키마를 따르고 있음을 DB가 보장한다</p>
</li>
<li><p><span style="color: indianred"><strong>읽기 스키마:</strong></span> 데이터 구조가 암묵적이고, 데이터를 읽을 때만 해석되며, <strong>문서 DB가 여기에 해당한다.</strong></p>
</li>
</ul>
<p>프로그래밍 언어에 빗대면 읽기 스키마는 동적(런타임) 타입 확인과, 쓰기 스키마는 정적(컴파일 타임) 타입 확인과 유사하다. </p>
<p>정적 타입과 동적 타입 지지자들이 열띤 논쟁을 하는 것처럼, DB에서 스키마를 강제하는 문제도 일반적으로 옳고 그른 정답은 없다.</p>
<br>

<p><strong><span style="color: #12b886">차이는 필드를 바꿀 때 드러난다</span></strong></p>
<p>전체 이름 필드를 성과 이름으로 분리한다고 하자.</p>
<pre><code class="language-javascript">// 읽기 스키마 — 새 필드로 쓰기 시작하고, 읽는 쪽에서 예전 문서를 처리한다
if (user &amp;&amp; user.name &amp;&amp; !user.first_name) {
  // 2013년 12월 8일 이전에 쓴 문서는 first_name이 없음
  user.first_name = user.name.split(&quot; &quot;)[0];
}</code></pre>
<pre><code class="language-sql">-- 쓰기 스키마 — 마이그레이션을 수행한다
ALTER TABLE users ADD COLUMN first_name text;
UPDATE users SET first_name = split_part(name, &#39; &#39;, 1);      -- PostgreSQL
UPDATE users SET first_name = substring_index(name, &#39; &#39;, 1); -- MySQL</code></pre>
<p>마이그레이션이 느리고 위험하다는 평판은 절반만 맞다.</p>
<ul>
<li><p>대부분의 관계형 DB는 <code>ALTER TABLE</code>을 수 밀리초에 끝낸다</p>
</li>
<li><p>MySQL은 예외다. <code>ALTER TABLE</code> 시 전체 테이블을 복사하기 때문에 큰 테이블이면 수 분에서 수 시간이 걸린다 (회피 도구는 존재한다)</p>
</li>
<li><p>진짜 오래 걸리는 쪽은 <code>UPDATE</code>이며, 모든 로우를 재작성해야 한다. 감당이 안 되면 <code>first_name</code>을 NULL로 두고 읽는 시점에 채우면 된다. </p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">읽기 스키마가 유리한 상황</span></strong></p>
<p><strong>컬렉션 안의 항목이 애초에 같은 모양이 아닐 때다.</strong></p>
<ul>
<li>유형이 여러 가지인데 각각을 별도 테이블로 만드는 게 실용적이지 않을 때</li>
<li>데이터 구조를 외부 시스템이 정하고, 그쪽이 언제든 바꿀 수 있을 때</li>
</ul>
<p>반대로 모든 레코드가 같은 구조라면 스키마는 제약이 아니라 문서화 수단이다.</p>
<br>

<h3 id="span-stylecolor-slateblue4-3-질의를-위한-데이터-지역성span"><span style="color: SlateBlue">4-3. 질의를 위한 데이터 지역성</span></h3>
<p>문서는 보통 단일 연속 문자열로 저장된다. JSON, XML, 또는 MongoDB의 BSON 같은 이진 변형이다.</p>
<p><span style="color: indianred"><strong>한 덩어리로 붙어 있으니 통째로 읽을 때 빠르다.</strong></span> 다중 테이블이라면 같은 내용을 모으는 데 여러 번의 색인 검색과 디스크 탐색이 필요하다.</p>
<p><span style="color: indianred"><strong>문제는 이 이점에 조건이 붙는다는 것인데, 한 번에 문서의 많은 부분을 쓸 때만 이득이다.</strong></span></p>
<ul>
<li><p>필드 하나만 필요해도 DB는 문서 전체를 적재하므로, 문서가 크면 그만큼 낭비다.</p>
</li>
<li><p>갱신할 때도 보통 문서 전체를 다시 쓴다.</p>
</li>
<li><p>그래서 문서를 작게 유지하고 크기가 커지는 쓰기를 피하라고 권장하는데, 해당 제약 때문에 문서 DB가 유리한 상황이 생각보다 좁아진다</p>
</li>
</ul>
<p><strong>또한, 지역성은 문서 모델만의 것도 아니며, 관계형 쪽에도 같은 아이디어가 있다.</strong></p>
<table>
<thead>
<tr>
<th>시스템</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>구글 Spanner</td>
<td>부모 테이블 안에 자식 로우를 중첩 배치하도록 선언하는 스키마</td>
</tr>
<tr>
<td>Oracle</td>
<td>다중 테이블 색인 클러스터 테이블 (multi-table index cluster table)</td>
</tr>
<tr>
<td>Bigtable (Cassandra, HBase)</td>
<td>칼럼 패밀리 (column-family)</td>
</tr>
</tbody></table>
<br>

<h3 id="span-stylecolor-slateblue4-4-두-모델은-서로-닮아가고-있다span"><span style="color: SlateBlue">4-4. 두 모델은 서로 닮아가고 있다</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>관계형 DB는 문서를 받아들였다.</strong></span></p>
<ul>
<li>MySQL을 뺀 대부분이 2000년대 중반부터 XML을, PostgreSQL 9.3 / MySQL 5.7 / DB2 10.5부터 JSON을 지원한다. 문서 내부를 색인하고 질의할 수도 있다</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>문서 DB는 조인을 들여왔다.</strong></span></p>
<ul>
<li>RethinkDB는 질의 언어에서 관계형 조인을 지원하고, MongoDB 드라이버는 DB 참조를 자동으로 따라간다 </li>
</ul>
</li>
</ul>
<p><span style="color: indianred"><strong>각자 부족한 쪽을 채우는 중이고, 책은 이 혼합 모델이 앞으로 갈 길이라고 본다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-데이터를-위한-질의-언어span"><span style='background-color: MediumAquamarine'>5. 데이터를 위한 질의 언어</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue5-1-선언형과-명령형span"><span style="color: SlateBlue">5-1. 선언형과 명령형</span></h3>
<p>SQL은 선언형 질의 언어인 반면, IMS와 코다실은 명령형 코드로 데이터베이스에 질의했다.</p>
<pre><code class="language-javascript">// 명령형 — 어떻게 할지를 지시한다
function getSharks() {
  var sharks = [];
  for (var i = 0; i &lt; animals.length; i++) {
    if (animals[i].family === &quot;Sharks&quot;) {
      sharks.push(animals[i]);
    }
  }
  return sharks;
}</code></pre>
<pre><code class="language-sql">-- 선언형 — 무엇을 원하는지만 지정한다
SELECT * FROM animals WHERE family = &#39;Sharks&#39;;</code></pre>
<p>관계 대수로는 <code>sharks = σ(family=&quot;Sharks&quot;)(animals)</code>로 쓴다. σ(시그마)는 선택 연산자이고, SQL은 이 관계 대수의 구조를 상당히 유사하게 따랐다.</p>
<ul>
<li><p><span style="color: indianred"><strong>명령형 언어:</strong></span> 특정 순서로 특정 연산을 수행하게끔 컴퓨터에 지시한다. 한 줄씩 실행하고, 조건을 평가하고, 변수를 갱신하고, 루프를 더 돌지 결정한다</p>
</li>
<li><p><span style="color: indianred"><strong>선언형 언어:</strong></span> 목표 달성 방법이 아니라 결과가 충족해야 할 조건과 변환 방식(정렬, 그룹화, 집계)만 지정한다</p>
</li>
</ul>
<p>어떤 색인을 쓸지, 어떤 조인 함수를 쓸지, 질의의 어느 부분을 어떤 순서로 실행할지는 전부 질의 최적화기가 결정한다.</p>
<br>

<p><strong><span style="color: #12b886">선언형이 나은 이유 세 가지</span></strong></p>
<ol>
<li><p><span style="color: indianred"><strong>간결하다:</strong></span> 명령형 API보다 쓰고 읽기 쉽다</p>
</li>
<li><p><span style="color: indianred"><strong>구현이 숨겨져 있다:</strong></span> 질의를 고치지 않고도 DB가 빨라질 수 있다</p>
<ul>
<li><p>명령형 코드는 결과가 특정 순서로 나온다. DB가 디스크 공간을 회수하려고 레코드를 옮기면 그 순서가 바뀌는데, 코드가 순서에 의존하는지를 DB는 알 수 없다</p>
</li>
<li><p>SQL은 애초에 순서를 보장하지 않으므로 자유롭게 옮길 수 있다. 기능이 제한적이라는 점이 오히려 최적화 여지를 만든다</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>병렬 실행에 적합하다:</strong></span> 오늘날 CPU는 클록 속도보다 코어 수로 빨라지고 있다</p>
<ul>
<li><p>명령형은 명령어 순서를 못 박기 때문에 다중 코어로 쪼개기 어렵다</p>
</li>
<li><p>선언형은 알고리즘이 아니라 결과의 패턴만 지정하므로 DB가 알아서 병렬화할 수 있다</p>
</li>
</ul>
</li>
</ol>
<br>

<h3 id="span-stylecolor-slateblue5-2-웹에서의-선언형-질의span"><span style="color: SlateBlue">5-2. 웹에서의 선언형 질의</span></h3>
<p>선언형 질의 언어의 장점은 DB에만 국한되지 않는다. 책은 완전히 다른 환경인 웹 브라우저를 예로 든다.</p>
<p>바다 동물 사이트의 메뉴가 <code>&lt;li&gt;&lt;p&gt;Sharks&lt;/p&gt;...&lt;/li&gt;</code>, <code>&lt;li&gt;&lt;p&gt;Whales&lt;/p&gt;...&lt;/li&gt;</code> 구조이고, 현재 선택된 항목에만 <code>class=&quot;selected&quot;</code>가 붙어 있다고 하자. 선택된 항목의 제목을 파란 배경으로 강조하고 싶다.</p>
<pre><code class="language-css">/* 선언형 — CSS */
li.selected &gt; p {
  background-color: blue;
}</code></pre>
<p>선택자 <code>li.selected &gt; p</code>는 스타일을 적용할 엘리먼트의 패턴을 선언한다. <code>selected</code> 클래스를 가진 <code>&lt;li&gt;</code>가 <code>&lt;p&gt;</code>의 부모여야 한다는 뜻이다.</p>
<ul>
<li><p><span style="color: indianred"><strong><code>&lt;p&gt;Sharks&lt;/p&gt;</code>:</strong></span> 부모 <code>&lt;li&gt;</code>에 <code>class=&quot;selected&quot;</code>가 있으므로 일치</p>
</li>
<li><p><span style="color: indianred"><strong><code>&lt;p&gt;Whales&lt;/p&gt;</code>:</strong></span> 부모에 클래스가 없으므로 불일치</p>
</li>
</ul>
<p>같은 일을 명령형으로, 자바스크립트 DOM API를 써서 하면 이렇게 된다.</p>
<pre><code class="language-javascript">// 명령형 — DOM API
var liElements = document.getElementsByTagName(&quot;li&quot;);
for (var i = 0; i &lt; liElements.length; i++) {
  if (liElements[i].className === &quot;selected&quot;) {
    var children = liElements[i].childNodes;
    for (var j = 0; j &lt; children.length; j++) {
      var child = children[j];
      if (child.nodeType === Node.ELEMENT_NODE &amp;&amp; child.tagName === &quot;P&quot;) {
        child.setAttribute(&quot;style&quot;, &quot;background-color: blue&quot;);
      }
    }
  }
}</code></pre>
<p>코드량이 엄청날 뿐 아니라 심각한 문제가 두 가지 있다.</p>
<ul>
<li><p><span style="color: indianred"><strong>상태 변화를 따라가지 못한다:</strong></span> <code>selected</code> 클래스가 삭제돼도 코드를 재실행하면 파란색이 지워지지 않아 전체 페이지가 다시 로딩될 때까지 하이라이트가 남는 반면, CSS는 규칙이 더 이상 적용되지 않는 것을 브라우저가 자동으로 감지해 즉시 지운다.</p>
</li>
<li><p><span style="color: indianred"><strong>성능 개선을 누리려면 코드를 재작성해야 한다:</strong></span> <code>getElementsByClassName</code>이나 <code>document.evaluate()</code> 같은 새 API의 장점을 취하려면 코드를 고쳐야 하지만, 브라우저 벤더는 호환성을 깨뜨리지 않고 CSS와 XPath의 성능을 향상할 수 있다</p>
</li>
</ul>
<p>웹 브라우저에서 선언형 CSS를 쓰는 편이 자바스크립트로 스타일을 다루는 것보다 훨씬 낫듯, <span style="color: indianred"><strong>데이터베이스에서도 SQL 같은 선언형 질의 언어가 명령형 질의 API보다 훨씬 좋다고 나타났다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue5-3-맵리듀스-질의span"><span style="color: SlateBlue">5-3. 맵리듀스 질의</span></h3>
<p><strong>맵리듀스는 많은 컴퓨터에서 대량 데이터를 처리하기 위한 프로그래밍 모델로 구글이 널리 알렸다.</strong></p>
<p>MongoDB와 CouchDB 등 일부 NoSQL 저장소가 제한된 형태로 지원하고, 많은 문서를 대상으로 하는 읽기 전용 질의에 쓴다.</p>
<p>맵리듀스는 선언형 질의 언어도 완전한 명령형 질의 API도 아닌 그 중간에 있으며, 질의 로직을 처리 프레임워크가 반복적으로 호출하는 조각 코드로 표현한다. </p>
<p><span style="color: indianred"><strong>이름은 함수형 언어의 <code>map</code>(collect)과 <code>reduce</code>(fold, inject)에서 왔다.</strong></span></p>
<p>해양 생물학자가 되어 한 달에 상어를 얼마나 자주 발견하는지 보고서를 만든다고 하자.</p>
<pre><code class="language-sql">-- PostgreSQL
SELECT date_trunc(&#39;month&#39;, observation_timestamp) AS observation_month,
       sum(num_animals) AS total_animals
FROM observations
WHERE family = &#39;Sharks&#39;
GROUP BY observation_month;</code></pre>
<pre><code class="language-javascript">// MongoDB 맵리듀스
db.observations.mapReduce(
  function map() {
    var year  = this.observationTimestamp.getFullYear();
    var month = this.observationTimestamp.getMonth() + 1;
    emit(year + &quot;-&quot; + month, this.numAnimals);
  },
  function reduce(key, values) {
    return Array.sum(values);
  },
  {
    query: { family: &quot;Sharks&quot; },   // 상어만 거르는 필터 (MongoDB 특화 확장)
    out: &quot;monthlySharkReport&quot;      // 최종 출력 컬렉션
  }
);</code></pre>
<p>읽는 순서는 이렇다.</p>
<ol>
<li><p><span style="color: indianred"><strong><code>map</code>:</strong></span> <code>query</code>로 걸러진 모든 문서에 대해 한 번씩 호출된다. <code>this</code>가 문서 객체로 설정된다</p>
</li>
<li><p><span style="color: indianred"><strong><code>emit</code>:</strong></span> 키(&quot;2013-12&quot; 같은 연도와 월로 구성된 문자열)와 값(관측치의 동물 수)을 방출하며, 같은 키를 가진 쌍들이 모여 <code>reduce</code>를 한 번 호출한다</p>
</li>
<li><p><span style="color: indianred"><strong><code>reduce</code>:</strong></span> 그 달의 모든 관측치에서 동물 수를 합쳐 <code>out</code>이 가리키는 컬렉션에 기록한다</p>
</li>
</ol>
<p><code>map</code>과 <code>reduce</code>는 입력으로 전달된 데이터만 사용하고 추가 DB 질의를 수행할 수 없으며 부수 효과가 없는 순수 함수여야 한다. </p>
<p>제약처럼 보이지만, 덕분에 DB가 이 함수를 임의 순서로 어디서나 실행할 수 있고 장애가 발생해도 재실행할 수 있다.</p>
<p>맵리듀스를 두고 흔히 하는 오해도 정리해 둔다.</p>
<ul>
<li><p><span style="color: indianred"><strong>분산 질의의 독점권:</strong></span> SQL 같은 고수준 질의 언어도 맵리듀스 연산의 파이프라인으로 구현할 수 있지만, 맵리듀스를 사용하지 않은 분산 SQL 구현 역시 많다</p>
</li>
<li><p><span style="color: indianred"><strong>질의 중간의 자바스크립트:</strong></span> 고급 질의가 가능한 훌륭한 기능이지만 맵리듀스에만 해당하는 것은 아니며, 일부 SQL DB도 자바스크립트 함수로 확장될 수 있다</p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">그리고 MongoDB는 선언형으로 돌아왔다</span></strong></p>
<p>맵리듀스의 사용성 문제는 연계된 자바스크립트 함수 두 개를 신중하게 작성해야 한다는 점인데, 이는 종종 하나의 질의를 작성하는 것보다 어렵다. </p>
<p>더욱이 선언형 질의 언어라야 질의 최적화기가 성능을 높일 기회를 얻는다.</p>
<p><span style="color: indianred"><strong>이런 이유로 MongoDB 2.2는 집계 파이프라인(aggregation pipeline)이라는 선언형 질의 언어 지원을 추가했다.</strong></span></p>
<pre><code class="language-javascript">db.observations.aggregate([
  { $match: { family: &quot;Sharks&quot; } },
  { $group: {
      _id: {
        year:  { $year:  &quot;$observationTimestamp&quot; },
        month: { $month: &quot;$observationTimestamp&quot; }
      },
      totalAnimals: { $sum: &quot;$numAnimals&quot; }
  }}
]);</code></pre>
<p>문법은 JSON 기반이지만 표현력은 SQL의 부분 집합과 비슷하다. 책의 표현을 빌리면, NoSQL 시스템이 뜻하지 않게 SQL을 재발견하고 있는 셈이다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-정리span"><span style='background-color: MediumAquamarine'>6. 정리</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue6-1-다대다-관계를-어떻게-표현할-것인가span"><span style="color: SlateBlue">6-1. 다대다 관계를 어떻게 표현할 것인가</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>일대다:</strong></span> 문서 모델이 유리하다. 트리 구조가 그대로 드러나고 지역성 덕에 질의 한 번으로 끝난다.</p>
</li>
<li><p><span style="color: indianred"><strong>다대다:</strong></span> 중복을 없애려면 참조가, 참조를 따라가려면 조인이 필요한데 문서 DB는 조인이 약하다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span> 조인이 DB에서 애플리케이션 코드로 넘어온다.</p>
</li>
<li><p><span style="color: indianred"><strong>선례:</strong></span> 1960~70년대 IMS가 같은 문제를 먼저 겪었고, 비정규화해서 중복하거나 참조를 손으로 해결하거나 선택지까지 똑같았다.</p>
</li>
</ul>
<p>다만 다대일, 다대다를 표현하는 방식 자체는 관계형과 문서가 다르지 않다. 둘 다 고유 식별자로 참조하고(외래 키 / 문서 참조) 읽기 시점에 해석한다.</p>
<br>

<h3 id="span-stylecolor-slateblue6-2-접근-경로를-누가-만들-것인가span"><span style="color: SlateBlue">6-2. 접근 경로를 누가 만들 것인가</span></h3>
<table>
<thead>
<tr>
<th></th>
<th>접근 경로를 만드는 주체</th>
</tr>
</thead>
<tbody><tr>
<td>계층 모델(IMS), 네트워크 모델(코다실)</td>
<td>개발자가 손으로. 경로가 없으면 질의 코드를 전부 재작성</td>
</tr>
<tr>
<td>관계형 모델</td>
<td>질의 최적화기가 자동으로. 색인만 선언하면 질의는 그대로</td>
</tr>
</tbody></table>
<p>선언형 질의 언어가 명령형 API보다 나은 이유도 결국 여기로 모인다. 결과의 패턴만 지정하면 구현을 숨긴 채 성능을 개선할 수 있고 병렬 실행까지 열린다.</p>
<br>

<h3 id="span-stylecolor-slateblue6-3-그래서-무엇을-고를-것인가span"><span style="color: SlateBlue">6-3. 그래서 무엇을 고를 것인가</span></h3>
<p>어떤 모델이 나은지는 일반적으로 말할 수 없고, 데이터 항목 사이의 관계 유형에 따라 갈린다.</p>
<ul>
<li><p><span style="color: indianred"><strong>문서 모델:</strong></span> 일대다 트리를 통째로 읽고, 문서끼리 참조가 거의 없으며, 항목 구조가 제각각일 때</p>
</li>
<li><p><span style="color: indianred"><strong>관계형 모델:</strong></span> 다대다가 일상적이고, 레코드 구조가 예상 가능하며, 새로운 질의가 계속 추가될 때</p>
</li>
</ul>
<p>처음 판단이 맞았더라도 서비스가 자라면 전제가 바뀐다. 문자열이 엔티티가 되거나, 한 번의 갱신이 여러 문서로 번지거나, 애플리케이션에 조인 코드가 쌓이기 시작하면 다시 볼 때다.</p>
<p><span style="color: indianred"><strong>둘 중 하나만 골라야 하는 것도 아니다. 관계형 테이블에 JSON 칼럼을 두는 혼합 모델이 실용적인 답이 되는 경우가 많다.</strong></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDIA (데이터 중심 애플리케이션 설계) Chapter 1]]></title>
            <link>https://velog.io/@taekyu_/DDIA%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-1-a2v8ra9q</link>
            <guid>https://velog.io/@taekyu_/DDIA%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-Chapter-1-a2v8ra9q</guid>
            <pubDate>Wed, 09 Sep 2026 04:57:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 
데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
<a href="https://www.wikibook.co.kr/data-intensive/">https://www.wikibook.co.kr/data-intensive/</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** DDIA 1주차 - 01장. 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션 **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-데이터-중심-애플리케이션span"><span style='background-color: MediumAquamarine'>1. 데이터 중심 애플리케이션</span></h2>
<hr>

<p><strong>오늘날 많은 애플리케이션은 계산 중심(compute-intensive)이 아니라 데이터 중심(data-intensive)이다.</strong></p>
<p>CPU 성능이 애플리케이션을 제한하는 요소가 아니라, <strong>데이터의 양 / 데이터의 복잡도 / 데이터의 변화 속도</strong>가 더 큰 문제라는 뜻이다.</p>
<p>데이터 중심 애플리케이션은 보통 아래와 같은 표준 구성 요소를 조합해서 만든다.</p>
<ul>
<li><p><span style="color: indianred"><strong>데이터베이스:</strong></span> 나중에 다시 데이터를 찾을 수 있게 저장</p>
</li>
<li><p><span style="color: indianred"><strong>캐시:</strong></span> 읽기 속도 향상을 위해 값비싼 수행 결과를 기억</p>
</li>
<li><p><span style="color: indianred"><strong>검색 색인(search index):</strong></span> 키워드 검색이나 다양한 필터링을 제공</p>
</li>
<li><p><span style="color: indianred"><strong>스트림 처리(stream processing):</strong></span> 비동기 처리를 위해 다른 프로세스로 메시지 전달</p>
</li>
<li><p><span style="color: indianred"><strong>일괄 처리(batch processing):</strong></span> 주기적으로 대량의 누적 데이터를 분석</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue1-1-왜-전부-데이터-시스템으로-묶는가span"><span style="color: SlateBlue">1-1. 왜 전부 데이터 시스템으로 묶는가</span></h3>
<p>일반적으로 데이터베이스, 큐, 캐시는 서로 다른 범주의 도구로 생각한다. </p>
<p><span style="color: indianred"><strong>하지만 최근 도구들은 분류 간 경계가 흐려지고 있다.</strong></span></p>
<ul>
<li><p>메시지 큐로 사용하는 데이터스토어인 Redis</p>
</li>
<li><p>데이터베이스처럼 지속성을 보장하는 메시지 큐인 Apache Kafka</p>
</li>
</ul>
<p align=center><img src="https://velog.velcdn.com/images/taekyu_/post/28f5efc7-7f42-4686-8acc-cb14079e29eb/image.png" width="60%" height="n%"></p>

<p>또한 단일 도구로는 요구사항을 모두 만족시킬 수 없기 때문에, 작업을 태스크로 나누고 <strong>애플리케이션 코드로 여러 도구를 연결</strong>하게 된다.</p>
<p>메인 DB와 캐시 및 검색 색인의 동기화를 맞추는 책임은 결국 애플리케이션 코드에 있다.</p>
<p>이렇게 여러 구성 요소를 조합해 특수 목적의 시스템을 만든 것을 <strong>복합 데이터 시스템</strong>이라 부른다.</p>
<p><span style="color: indianred"><strong>즉, 이제 개발자는 애플리케이션 개발자일 뿐만 아니라 데이터 시스템 설계자이기도 하다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue1-2-이-책이-중점을-두는-세-가지-관심사span"><span style="color: SlateBlue">1-2. 이 책이 중점을 두는 세 가지 관심사</span></h3>
<ol>
<li><p><span style="color: indianred"><strong>신뢰성(Reliability):</strong></span> 하드웨어/소프트웨어 결함, 인적 오류에 직면하더라도 시스템은 지속적으로 올바르게 동작해야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>확장성(Scalability):</strong></span> 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 적절한 방법이 있어야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>유지보수성(Maintainability):</strong></span> 시간이 지나 다양한 사람들이 시스템을 다루더라도 모두가 생산적으로 작업할 수 있어야 한다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-신뢰성reliabilityspan"><span style='background-color: MediumAquamarine'>2. 신뢰성(Reliability)</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-신뢰성이란span"><span style="color: SlateBlue">2-1. 신뢰성이란</span></h3>
<p>소프트웨어에 대한 일반적인 기대치는 다음과 같다.</p>
<ul>
<li><p>애플리케이션은 사용자가 기대한 기능을 수행한다.</p>
</li>
<li><p>사용자가 범한 실수나 예상치 못한 사용법을 허용할 수 있다.</p>
</li>
<li><p>예상된 부하와 데이터 양에서 필수 사용 사례를 충분히 만족한다.</p>
</li>
<li><p>허가되지 않은 접근과 오남용을 방지한다.</p>
</li>
</ul>
<p><strong>이것을 한 문장으로 줄이면 &quot;무언가 잘못되더라도 지속적으로 올바르게 동작함&quot;이 신뢰성의 의미다.</strong></p>
<br>

<h3 id="span-stylecolor-slateblue2-2-결함과-장애는-다르다span"><span style="color: SlateBlue">2-2. 결함과 장애는 다르다</span></h3>
<p>이 장에서 가장 중요하게 짚고 넘어가야 할 용어 구분이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>결함(fault):</strong></span> 사양에서 벗어난 시스템의 한 구성 요소</p>
</li>
<li><p><span style="color: indianred"><strong>장애(failure):</strong></span> 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/964cec1b-40f7-42fc-bf44-f93d9bc71cea/image.png" alt=""></p>
<p>결함을 예측하고 대처할 수 있는 시스템을 내결함성 또는 탄력성을 지녔다고 말한다.</p>
<p><strong>다만 결함 확률을 0으로 줄이는 것은 불가능하다.</strong></p>
<p><span style="color: indianred"><strong>따라서 목표는 결함을 없애는 것이 아니라, 결함이 장애로 번지지 않게 설계하는 것이다.</strong></span></p>
<p>이때, 여기서 반직관적인 이야기가 나온다.</p>
<p><strong>고의적으로 결함을 일으켜 결함률을 증가시키는 방법이 납득할 만하다는 것이다.</strong></p>
<p>실제로 많은 중대한 버그는 미흡한 오류 처리에 기인하기 때문에, 일부러 결함을 유도해 지속적으로 훈련하고 테스트해야 한다.</p>
<p>Netflix의 카오스 몽키(Chaos Monkey)가 이런 접근 방식의 대표적인 예다.</p>
<br>

<h3 id="span-stylecolor-slateblue2-3-하드웨어-결함span"><span style="color: SlateBlue">2-3. 하드웨어 결함</span></h3>
<p>하드디스크의 평균 장애 시간(Mean time to failure, MTTF)은 약 10~50년으로 보고됐다.</p>
<p><strong>즉, 10,000개의 디스크로 구성된 저장 클러스터는 평균적으로 하루에 한 개의 디스크가 죽는다고 예상해야 한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>전통적인 대응:</strong></span> 각 하드웨어 구성 요소에 중복 추가 (RAID 구성, 이중 전원 장치, 핫 스왑 CPU, 예비 디젤 발전기 등)</p>
</li>
<li><p><span style="color: indianred"><strong>변화한 상황:</strong></span> 데이터 양과 계산 요구가 늘어나면서 사용하는 장비 수가 늘었고, 이에 비례해 하드웨어 결함율도 증가했다.</p>
</li>
<li><p><span style="color: indianred"><strong>클라우드 환경:</strong></span> AWS 같은 플랫폼은 가상 장비 인스턴스가 별도의 경고 없이 사용 불가가 되는 상황이 상당히 일반적이다. <strong>단일 장비 신뢰성보다 유연성과 탄력성을 우선하도록 설계됐기 때문이다.</strong></p>
</li>
</ul>
<p>그래서 하드웨어 중복만이 아니라 <span style="color: indianred"><strong>소프트웨어 내결함성 기술로 전체 장비 손실을 견디는 시스템으로 옮겨가고 있다.</strong></span></p>
<br>

<h3 id="span-stylecolor-slateblue2-4-소프트웨어-오류span"><span style="color: SlateBlue">2-4. 소프트웨어 오류</span></h3>
<p>하드웨어 결함은 보통 무작위적이고 서로 독립적이다. </p>
<p><strong>반면, 체계적 오류는 노드 간 상관관계가 있어 오히려 더 많은 시스템 오류를 유발한다.</strong></p>
<ul>
<li><p>잘못된 특정 입력에 모든 애플리케이션 서버 인스턴스가 죽는 버그</p>
</li>
<li><p>CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭 같은 공유 자원을 과도하게 사용하는 프로세스</p>
</li>
<li><p>속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 서비스</p>
</li>
<li><p>한 구성 요소의 작은 결함이 차례차례 번지는 연쇄 장애</p>
</li>
</ul>
<p><span style="color: indianred"><strong>이러한 버그들은 특정상황에 의해 발생되기 전까지 오랫동안 나타나지 않으며,  소프트웨어의 체계적 오류에는 신속한 해결책이 없다.</strong></span></p>
<p>따라서, 소프트웨어 오류에 대한 문제해결을 위해서는</p>
<ul>
<li><p>시스템의 가정과 상호작용에 대해 주의 깊게 생각하기</p>
</li>
<li><p>빈틈없는 테스트 </p>
</li>
<li><p>프로세스 격리</p>
</li>
<li><p>죽은 프로세스의 재시작 허용</p>
</li>
<li><p>프로덕션 환경에서의 측정, 모니터링, 분석</p>
</li>
</ul>
<p>이러한 방식들이 문제 해결에 도움을 준다.</p>
<br>

<h3 id="span-stylecolor-slateblue2-5-인적-오류span"><span style="color: SlateBlue">2-5. 인적 오류</span></h3>
<p>대규모 인터넷 서비스에 대한 한 연구 결과가 인상적이었다.</p>
<p><strong>운영자의 설정 오류가 중단의 주요 원인인 반면, 하드웨어(서버나 네트워크) 결함은 중단 원인의 10~25% 정도에 그친다.</strong></p>
<p>사람이 미덥지 않음에도 시스템을 신뢰성 있게 만들려면 다양한 접근 방식을 결합해야 한다.</p>
<ol>
<li><p><span style="color: indianred"><strong>오류 가능성을 최소화하는 설계:</strong></span> 잘 설계된 추상화와 API로 옳은 일은 쉽게, 잘못된 일은 막는다. 단, 인터페이스가 지나치게 제한적이면 사람들은 이를 피해서 작업하므로 균형이 어렵다.</p>
</li>
<li><p><span style="color: indianred"><strong>실수가 잦은 부분의 분리:</strong></span> 실제 데이터로 안전하게 실험할 수 있지만 실제 사용자에게는 영향이 없는 <strong>비 프로덕션 sandbox</strong>를 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>모든 수준에서의 철저한 테스트:</strong></span> 단위 테스트부터 통합 테스트, 수동 테스트까지. 특히 정상 동작에서는 거의 발생하지 않는 <strong>Corner Case</strong>를 다루는 데 유용하다.</p>
</li>
<li><p><span style="color: indianred"><strong>빠르고 쉬운 복구:</strong></span> 설정 변경 내역을 빠르게 롤백하고, 새로운 코드는 서서히 롤아웃하며, 이전 계산이 잘못된 경우를 대비해 데이터 재계산 도구를 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>상세하고 명확한 모니터링:</strong></span> 성능 지표와 오류율. 다른 엔지니어링 분야에서는 <strong>원격 측정(telemetry)</strong>이라 부른다.</p>
</li>
<li><p><span style="color: indianred"><strong>조작 교육과 실습:</strong></span> 까다롭지만 매우 중요한 측면 중 하나이다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-확장성scalabilityspan"><span style='background-color: MediumAquamarine'>3. 확장성(Scalability)</span></h2>
<hr>

<p><strong>시스템이 현재 안정적으로 동작한다고 해서 미래에도 안정적으로 동작한다는 보장은 없으며, 성능 저하를 유발하는 흔한 이유 중 하나는 부하 증가이다.</strong></p>
<p>확장성은 증가한 부하에 대처하는 시스템 능력을 설명하는 용어지만, 단순히 수치 하나로 말할 수 있는 성질은 아니다.</p>
<p>확장성을 논한다는 것은 <span style="color: indianred"><strong>&quot;시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?&quot;</strong>, <strong>&quot;추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?&quot;</strong></span>를 고려한다는 의미다.</p>
<br>

<h3 id="span-stylecolor-slateblue3-1-부하-기술하기-용어-정리span"><span style="color: SlateBlue">3-1. 부하 기술하기 (용어 정리)</span></h3>
<p>부하 성장에 대한 질문 (부하가 두 배가 되면 어떻게 될까?)을 논의하려면, 먼저 현재 부하를 간결하게 기술할 수 있어야 한다.</p>
<p>이때 사용하는 것이 <strong>부하 매개변수(load parameter)</strong>다.</p>
<ul>
<li><span style="color: indianred"><strong>부하 매개변수:</strong></span> 시스템의 부하를 나타내는 몇 개의 숫자. 웹 서버의 초당 요청 수, DB의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자 수, 캐시 적중률 등이 될 수 있다.</li>
</ul>
<p><strong>가장 적합한 부하 매개변수 선택은 시스템 설계에 따라 달라진다.</strong> </p>
<p>이 책은 트위터를 예로 들어 설명하는데, 사례를 따라가려면 용어 두 개를 먼저 정리하고 가는 편이 좋다.</p>
<ul>
<li><p><span style="color: indianred"><strong>홈 타임라인(home timeline):</strong></span> 트위터 메인 화면으로, 내가 팔로우하는 사람들이 작성한 트윗을 시간 역순으로 모아서 보는 피드다.</p>
</li>
<li><p><span style="color: indianred"><strong>팬 아웃(fan-out):</strong></span> 한 사용자가 트윗 하나를 올렸을 때, 그 글을 받아봐야 하는 <strong>모든 팔로워에게 퍼뜨려 전달하는 과정 또는 그 작업량</strong>을 말한다. </p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue3-2-트위터-사례---팬-아웃을-언제-할-것인가span"><span style="color: SlateBlue">3-2. 트위터 사례 - 팬 아웃을 언제 할 것인가</span></h3>
<p>트위터의 주요 두 가지 동작은 다음과 같다.</p>
<ul>
<li><p><span style="color: indianred"><strong>트윗 작성(쓰기):</strong></span> 평균 초당 4.6k 요청, 피크일 때 초당 12k 요청 이상</p>
</li>
<li><p><span style="color: indianred"><strong>홈 타임라인 조회(읽기):</strong></span> 초당 300k 요청</p>
</li>
</ul>
<p><strong>여기서 중요한 건 읽기가 쓰기보다 압도적으로 많다는 점이다. 초당 12,000건의 쓰기 처리 자체는 사실 상당히 쉽다.</strong></p>
<p><span style="color: indianred"><strong>트위터의 확장성 문제는 트윗 양이 아니라 팬 아웃 때문이다.</strong></span></p>
<p>개별 사용자는 많은 사람을 팔로우하고, 많은 사람이 개별 사용자를 팔로우하기 때문이다. 이 두 동작을 구현하는 방법은 크게 두 가지다.</p>
<br>

<p><span style="color: #12b886"><strong>접근 방식 1 - 읽는 시점에 합치기</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>트윗 작성:</strong></span> 트윗 테이블에 새 레코드를 한 줄 넣고 끝낸다.</p>
</li>
<li><p><span style="color: indianred"><strong>타임라인 조회:</strong></span> 사용자가 타임라인을 새로고침할 때마다 DB가 실시간으로 일한다. 
팔로우 테이블에서 내가 팔로우하는 사람을 찾고 → 그 사람들의 트윗을 트윗 테이블에서 모두 찾아오고 → 시간순으로 정렬해 합친다.</p>
</li>
</ul>
<pre><code class="language-sql">SELECT tweets.*, users.* FROM tweets
  JOIN users   ON tweets.sender_id    = users.id
  JOIN follows ON follows.followee_id = users.id
  WHERE follows.follower_id = current_user</code></pre>
<ul>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 쓰기 작업이 매우 가볍다. 트윗을 쓸 때 할 일이 거의 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>단점:</strong></span> 조회할 때마다 복잡한 조인과 정렬을 반복해야 한다. <strong>읽기가 쓰기보다 압도적으로 많은 트위터에서는 이 부하를 감당하지 못했다.</strong></p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">접근 방식 2 - 쓰는 시점에 미리 뿌리기</span></strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/55222870-e58c-47bc-928c-c92a63ceeb08/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>구조:</strong></span> 각 사용자마다 전용 우편함 역할을 하는 <strong>홈 타임라인 캐시</strong>를 둔다.</p>
</li>
<li><p><span style="color: indianred"><strong>트윗 작성:</strong></span> 누군가 트윗을 쓰면 그 사람의 팔로워 목록을 확인하고, <strong>모든 팔로워의 우편함에 해당 트윗을 미리 하나씩 복사해서 넣는다. (fan-out)</strong> </p>
</li>
<li><p><span style="color: indianred"><strong>타임라인 조회:</strong></span> 계산할 필요 없이 자기 우편함에 이미 모여 있는 목록만 꺼내 읽는다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 읽기 요청 시 결과가 이미 완성돼 있으므로 조회가 아주 빠르고 비용이 거의 들지 않는다.</p>
</li>
</ul>
<p><strong>트위터의 첫 번째 버전은 접근 방식 1을 사용했지만, 홈 타임라인 질의의 부하를 감당하지 못해 접근 방식 2로 전환했다.</strong></p>
<p>게시된 트윗의 평균 속도가 홈 타임라인 읽기 속도보다 100배 정도 낮기 때문에, 이 경우에는 <span style="color: indianred"><strong>쓰기 시점에 더 많은 일을 하고 읽기 시점에 적은 일을 하는 것이 바람직</strong></span>하다.</p>
<br>

<p><strong><span style="color: #12b886">그런데 접근 방식 2에는 유명인 문제가 있다</span></strong></p>
<p>읽기는 편해졌지만, 반대로 <strong>쓸 때 해야 할 일(팬 아웃)이 폭발적으로 늘어났다.</strong></p>
<ul>
<li><p>평균적으로 트윗이 약 75명의 팔로워에게 전달되므로, 초당 4.6k 트윗은 <strong>초당 345k건의 캐시 쓰기</strong>가 된다. </p>
</li>
<li><p>문제는 평균값이 사용자마다 팔로워 수가 매우 다르다는 사실을 가린다는 점이다. <span style="color: indianred"><strong>일부 사용자는 팔로워가 3천만 명이 넘는다.</strong></span></p>
</li>
<li><p>즉, 유명인의 <strong>단일 트윗 하나가 3천만 건 이상의 쓰기 요청</strong>이 될 수 있다.</p>
</li>
<li><p>트위터는 5초 이내에 팔로워에게 트윗을 전송하려고 하는데, 이 목표를 맞추는 것이 중요한 도전 과제가 됐다.</p>
</li>
</ul>
<br>

<p><strong><span style="color: #12b886">최종 해결책 - 두 방식을 섞은 혼합형</span></strong></p>
<p><strong>팔로워가 적은 대다수 사용자와 팔로워가 수백만 이상인 소수 사용자를 나눠서 처리한다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>일반 사용자가 트윗을 쓸 때:</strong></span> 접근 방식 2를 쓴다. 팔로워 수가 적으므로 팔로워들의 우편함에 바로 복사해 넣는다.</p>
</li>
<li><p><span style="color: indianred"><strong>유명인이 트윗을 쓸 때:</strong></span> <strong>팬 아웃에서 제외한다.</strong> 팔로워 우편함에 일일이 복사하지 않고 유명인의 트윗은 그대로 둔다.</p>
</li>
<li><p><span style="color: indianred"><strong>홈 타임라인을 조회할 때:</strong></span> 자기 우편함(미리 들어온 일반 사용자들의 트윗)을 꺼내고, 내가 팔로우하는 유명인의 트윗은 <strong>접근 방식 1처럼 읽는 시점에 별도로 가져와 시간순으로 합친다.</strong></p>
</li>
</ol>
<p><strong>이 혼합형 접근 방식으로 좋은 성능의 지속적인 전송이 가능해졌다.</strong></p>
<br>

<p><strong><span style="color: #12b886">이 사례가 말하고자 하는 핵심</span></strong></p>
<p><strong>트위터의 핵심 부하 매개변수는 초당 트윗 수가 아니라 사용자당 팔로워 수의 분포였다.</strong></p>
<p><span style="color: indianred"><strong>시스템을 설계할 때 평균값(사용자당 평균 팔로워 75명)만 보고 판단하면 안 된다는 이야기다.</strong></span></p>
<p>평균만 보면 접근 방식 2가 완벽해 보이지만, 실제로는 <strong>소수의 극단적인 이상치(팔로워 수천만 명의 유명인)가 시스템 전체를 마비시킬 수 있다.</strong></p>
<br>

<h3 id="span-stylecolor-slateblue3-2-성능-기술하기-평균이-아니라-백분위span"><span style="color: SlateBlue">3-2. 성능 기술하기 (평균이 아니라 백분위)</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>일괄 처리 시스템:</strong></span> 처리량(throughput)에 관심</p>
</li>
<li><p><span style="color: indianred"><strong>온라인 시스템:</strong></span> 응답 시간(response time)이 더 중요</p>
</li>
</ul>
<p>여기서 지연 시간과 응답 시간의 구분도 짚고 간다.
<img src="https://velog.velcdn.com/images/taekyu_/post/05699910-ee81-47b8-ac17-36ffd4ceed83/image.png" alt=""></p>
<ul>
<li><p><strong>응답 시간(response time):</strong> 클라이언트 관점의 시간. 실제 처리 시간(서비스 시간) + 네트워크 지연 + 큐 지연을 포함한다.</p>
</li>
<li><p><strong>지연 시간(latency):</strong> 요청이 처리되길 기다리는, 휴지 상태인 시간</p>
</li>
</ul>
<p><strong>응답 시간은 단일 숫자가 아니라 측정 가능한 값의 분포로 생각해야 한다.</strong></p>
<p><span style="color: indianred"><strong>전형적인 응답 시간을 알고 싶다면 평균은 그다지 좋은 지표가 아니다. 얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 때문이다.</strong></span></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/cc1da50a-b7ba-49d4-9e9e-32671823de92/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>중앙값(p50):</strong></span> 사용자가 보통 얼마나 기다리는지 알고 싶을 때 좋은 지표</p>
</li>
<li><p><span style="color: indianred"><strong>상위 백분위(p95, p99, p999):</strong></span> 특이 값(outlier)이 얼마나 나쁜지 확인. <strong>꼬리 지연 시간(tail latency)</strong>이라 부른다.</p>
</li>
</ul>
<p><strong>꼬리 지연 시간이 중요한 이유는 서비스의 사용자 경험에 직접 영향을 주기 때문이다.</strong></p>
<ul>
<li><p>아마존은 내부 서비스의 응답 시간 요구사항을 99.9분위로 기술한다. 요청 1,000개 중 1개만 영향이 있음에도 그렇게 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>응답 시간이 가장 느린 요청을 경험한 고객이 대개 데이터가 가장 많은, 즉 가장 소중한 고객이기 때문</strong></span>이다.</p>
</li>
<li><p>아마존은 응답 시간이 <strong>100밀리초 증가하면 판매량이 1% 줄어들고, 1초가 느려지면 고객 만족도 지표가 16% 줄어드는</strong> 현상을 관찰했다.</p>
</li>
<li><p>반면 99.99분위는 최적화 비용이 너무 커서 이익이 충분치 않다고 여긴다.</p>
<br>

</li>
</ul>
<p><span style="color: #12b886"><strong>함께 알아둘 개념 두 가지</strong></span></p>
<ul>
<li><span style="color: indianred"><strong>선두 차단(head-of-line blocking):</strong></span> 서버는 병렬로 소수의 작업만 처리할 수 있어, 느린 요청 몇 개가 후속 요청 처리를 지체시킨다. 이 때문에 <strong>클라이언트 쪽에서 응답 시간을 측정하는 것이 중요하다.</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/462defd6-550e-4877-bc27-06a9908d517e/image.png" alt=""></p>
<ul>
<li><span style="color: indianred"><strong>꼬리 지연 증폭(tail latency amplification):</strong></span> 하나의 최종 사용자 요청이 여러 백엔드를 호출하면, 가장 느린 호출이 끝날 때까지 기다려야 한다. 작은 비율의 백엔드 호출만 느려도 최종 사용자 요청의 상당 비율이 느려진다.</li>
</ul>
<blockquote>
<p><strong>따라서, 서비스의 지연과 응답 시간을 알고 싶다면, 평균을 사용하는것보다 백분위를 사용해서 분석하는 것이 좋다.</strong></p>
</blockquote>
<br>

<h3 id="span-stylecolor-slateblue3-3-부하-대응-접근-방식span"><span style="color: SlateBlue">3-3. 부하 대응 접근 방식</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>용량 확장(scaling up):</strong></span> 수직 확장. 좀 더 강력한 장비로 이동</p>
</li>
<li><p><span style="color: indianred"><strong>규모 확장(scaling out):</strong></span> 수평 확장. 다수의 낮은 사양 장비에 부하를 분산 → <strong>비공유 아키텍처</strong></p>
</li>
</ul>
<p><strong>현실적으로 좋은 아키텍처는 두 방식의 조합이다.</strong> 적절한 사양의 장비 몇 대가 다량의 낮은 사양 가상 장비보다 훨씬 간단하고 저렴한 경우도 많다.</p>
<ul>
<li><p><span style="color: indianred"><strong>탄력적 시스템:</strong></span> 부하 증가를 감지해 컴퓨팅 자원을 자동 추가. 부하를 예측할 수 없을 때 유용하다.</p>
</li>
<li><p><span style="color: indianred"><strong>수동 확장 시스템:</strong></span> 더 간단하고 <strong>운영상 예상치 못한 일이 더 적다.</strong></p>
</li>
</ul>
<p><strong>Stateless 서비스를 다수 장비에 배포하는 일은 간단하다. 하지만 Stateful 데이터 시스템을 분산하는 일은 아주 많은 복잡도를 추가한다.</strong></p>
<p><span style="color: indianred"><strong>그리고 결정적으로, 범용적이고 모든 상황에 맞는 확장 아키텍처는 없다.</strong></span></p>
<p><strong>각 크기가 1kB인 초당 100,000건의 요청을 처리하는 시스템과, 각 크기가 2GB인 분당 3건의 요청을 처리하는 시스템은 데이터 처리량이 같아도 완전히 다른 시스템이다.</strong></p>
<p>*<em>확장성 있는 아키텍처는 <span style="color: indianred">&#39;주요 동작이 무엇이고 잘 하지 않는 동작이 무엇인지&#39;</span>에 대한 가정 위에 세워진다. *</em></p>
<p>그래서 스타트업 초기 단계나 검증되지 않은 제품이라면, 미래를 가정한 부하에 대비하기보다 <strong>빠르게 반복해서 제품 기능을 개선하는 작업이 더 중요</strong>하다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-유지보수성maintainabilityspan"><span style='background-color: MediumAquamarine'>4. 유지보수성(Maintainability)</span></h2>
<hr>

<p><strong>소프트웨어 비용의 대부분은 초기 개발이 아니라 지속해서 이어지는 유지보수에 들어간다.</strong></p>
<p>버그 수정, 시스템 운영 유지, 장애 조사, 새 플랫폼 적응, 새 사용 사례를 위한 변경, 기술 채무 상환, 새 기능 추가 등이 모두 여기에 해당한다.</p>
<p><strong>희망적인 점은, 유지보수 중 고통을 최소화하고 레거시 소프트웨어를 직접 만들지 않게끔 설계할 수 있다는 것이다.</strong></p>
<p>이를 위한 세 가지 설계 원칙은 다음과 같다.</p>
<br>

<h3 id="span-stylecolor-slateblue4-1-운용성operability-운영의-편리함-만들기span"><span style="color: SlateBlue">4-1. 운용성(operability): 운영의 편리함 만들기</span></h3>
<p><span style="color: indianred"><strong>좋은 운영성이란 반복되는 태스크를 쉽게 만들어, 운영팀이 고부가가치 활동에 노력을 집중하게 하는 것이다.</strong></span></p>
<p>데이터 시스템이 할 수 있는 일은 다음과 같다.</p>
<ul>
<li><p>좋은 모니터링으로 runtime 동작과 시스템 내부에 대한 가시성 제공</p>
</li>
<li><p>표준 도구를 이용한 자동화와 통합 지원</p>
</li>
<li><p>개별 장비 의존성 회피. 유지보수로 장비를 내려도 시스템 전체는 계속 운영 가능해야 함</p>
</li>
<li><p>만족할 만한 기본 동작을 제공하되, 필요할 때 기본값을 재정의할 자유를 관리자에게 부여</p>
</li>
<li><p>적절한 자기 회복이 가능하면서, 필요 시 관리자가 수동으로 제어할 수 있게 함</p>
</li>
<li><p>예측 가능하게 동작하고 예기치 않은 상황을 최소화함</p>
</li>
<li><p>좋은 문서와 이해하기 쉬운 운영 모델 제공 (예: &quot;X를 하면 Y가 발생한다&quot;)</p>
</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue4-2-단순성simplicity-복잡도-관리span"><span style="color: SlateBlue">4-2. 단순성(simplicity): 복잡도 관리</span></h3>
<p>프로젝트가 커지면 시스템은 매우 복잡하고 이해하기 어려워지며, 복잡도는 다양한 증상으로 나타난다.</p>
<ul>
<li><p>상태 공간의 급증</p>
</li>
<li><p>모듈 간 강한 커플링(tight coupling)</p>
</li>
<li><p>복잡한 의존성</p>
</li>
<li><p>일관성 없는 명명과 용어</p>
</li>
<li><p>성능 문제 해결을 목표로 한 해킹</p>
</li>
<li><p>임시방편으로 문제를 해결한 특수 사례</p>
</li>
</ul>
<p><strong>복잡한 소프트웨어에서는 변경이 있을 때 버그가 생길 위험이 더 크다.</strong> 개발자가 시스템을 이해하기 어려워지면 숨겨진 가정과 의도치 않은 결과를 간과하기 쉽다.</p>
<p><span style="color: indianred"><strong>시스템을 단순하게 만드는 일이 반드시 기능을 줄인다는 의미는 아니다. 우발적 복잡도(accidental complexity)를 줄인다는 뜻일 수 있다.</strong></span></p>
<p>우발적 복잡도란 <strong>문제 자체에 내재하지 않고 구현에서만 발생하는 복잡도</strong>를 말한다.</p>
<p><span style="color: indianred"><strong>그리고 우발적 복잡도를 제거하기 위한 최상의 도구는 추상화다.</strong></span></p>
<ul>
<li><p>고수준 프로그래밍 언어는 기계 언어, CPU 레지스터, 시스템 호출을 숨긴 추상화</p>
</li>
<li><p>SQL은 디스크 기록, 메모리 자료 구조, 다른 클라이언트의 동시 요청, 고장 후 불일치를 숨긴 추상화</p>
</li>
</ul>
<p>다만 <strong>좋은 추상화를 찾기는 매우 어렵다.</strong> 분산 시스템 분야에는 좋은 알고리즘이 많지만, 이를 복잡도를 관리 가능한 수준으로 유지해주는 추상화로 묶는 방법은 아직 명확하지 않다.</p>
<br>

<h3 id="span-stylecolor-slateblue4-3-발전성evolvability-변화를-쉽게-만들기span"><span style="color: SlateBlue">4-3. 발전성(evolvability): 변화를 쉽게 만들기</span></h3>
<p><strong>시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다.</strong></p>
<p>새로운 사실을 배우고, 예기치 않은 사용 사례가 나타나고, 비즈니스 우선순위가 바뀌고, 법적/규제 요구사항이 변경된다.</p>
<p>조직 프로세스 측면에서는 agile 작업 패턴이 변화 적응 프레임워크를 제공한다. <span style="color: indianred"><strong>TDD, 리팩토링 같은 도구도 여기서 나왔다.</strong></span></p>
<p><strong>다만 애자일 기법에 대한 설명은 대부분 아주 작은 로컬 규모(같은 애플리케이션 안의 소스 파일 몇 개)에 초점을 맞추고 있다.</strong></p>
<p>이 책이 찾는 것은 <strong>여러 애플리케이션과 서비스로 구성된 대규모 데이터 시스템 수준에서 민첩성을 높이는 방법</strong>이다.</p>
<p><span style="color: indianred"><strong>예를 들어 트위터의 홈 타임라인 아키텍처를 &#39;접근 방식 1&#39;에서 &#39;접근 방식 2&#39;로 리팩토링하는 방법 같은 것이다.</strong></span></p>
<p>그래서 데이터 시스템 수준에서는 민첩성 대신 <strong>발전성</strong>이라는 단어를 사용한다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-정리span"><span style='background-color: MediumAquamarine'>5. 정리</span></h2>
<hr>

<p>애플리케이션이 유용하려면 </p>
<ul>
<li><p><strong>기능적 요구사항</strong>(데이터를 저장/조회/검색/처리하는 일)</p>
</li>
<li><p><strong>비기능적 요구사항</strong>(보안, 신뢰성, 법규 준수, 확장성, 호환성, 유지보수성)</p>
</li>
</ul>
<p>을 모두 충족시켜야 한다.
<br></p>
<p>1장은 그중 비기능적 요구사항의 세 축을 다뤘다.</p>
<ul>
<li><p><span style="color: indianred"><strong>신뢰성:</strong></span> 결함이 발생해도 시스템이 올바르게 동작하게 만든다는 의미다. 
결함은 <strong>하드웨어(대개 무작위)</strong>, <strong>소프트웨어(보통 체계적이고 다루기 어려움)</strong>, <strong>사람(가끔 불가피하게 실수함)</strong>에게서 온다. 내결함성 기술은 최종 사용자에게 특정 유형의 결함을 숨겨준다.</p>
</li>
<li><p><span style="color: indianred"><strong>확장성:</strong></span> 부하가 증가해도 좋은 성능을 유지하기 위한 전략이다. 이를 논하려면 부하와 성능(응답 시간 백분위을 양적으로 기술하는 방법이 먼저 필요하다.</p>
</li>
<li><p><span style="color: indianred"><strong>유지보수성:</strong></span> 본질은 시스템에서 작업하는 <strong>엔지니어와 운영팀의 삶을 개선하는 것</strong>이다. 좋은 추상화는 복잡도를 줄이고, 좋은 운용성은 시스템의 건강 상태를 잘 관찰하고 효율적으로 관리할 수 있게 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[K8s에서 GPU 쪼개기: Time Slicing에서 HAMi로]]></title>
            <link>https://velog.io/@taekyu_/K8s%EC%97%90%EC%84%9C-GPU-%EC%AA%BC%EA%B0%9C%EA%B8%B0-Time-Slicing%EC%97%90%EC%84%9C-HAMi%EB%A1%9C-hbbvh1nk</link>
            <guid>https://velog.io/@taekyu_/K8s%EC%97%90%EC%84%9C-GPU-%EC%AA%BC%EA%B0%9C%EA%B8%B0-Time-Slicing%EC%97%90%EC%84%9C-HAMi%EB%A1%9C-hbbvh1nk</guid>
            <pubDate>Wed, 19 Aug 2026 14:12:12 GMT</pubDate>
            <description><![CDATA[<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-기존-방식의-문제span"><span style='background-color: MediumAquamarine'>1. 기존 방식의 문제</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-기존-구성-time-slicing--소프트웨어-vram-제한-주입span"><span style="color: SlateBlue">1-1. 기존 구성: Time Slicing + 소프트웨어 VRAM 제한 주입</span></h3>
<p>학과 GPU 클러스터는 RTX 4070 Ti 4대 + RTX 4090 2대를 NVIDIA GPU Operator로 묶어, 학생들에게 JupyterHub 기반 딥러닝 실습 환경으로 서비스하고 있다.</p>
<p>GPU는 노드당 1장인데 학생은 그보다 많다. 그래서 GPU 하나를 여러 명이 나눠 쓰게 해야 하고, 지금까지는 Time Slicing으로 해결해왔다.</p>
<pre><code class="language-bash">$ kubectl get cm -n nvidia-gpu-operator time-slicing-config -o jsonpath=&#39;{.data}&#39;
{&quot;any&quot;:&quot;version: v1
flags:
  migStrategy: none
sharing:
  timeSlicing:
    resources:
    - name: nvidia.com/gpu
      replicas: 3&quot;}</code></pre>
<p><code>replicas: 3</code>  -&gt; Time Slicing을 사용하여, 물리 GPU 1장을 쿠버네티스에 3장으로 뻥튀기해서 보고한다.</p>
<pre><code class="language-bash">$ kubectl get nodes -o custom-columns=&quot;NODE:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu&quot;
NODE     GPU
gpu-01   3
gpu-02   3
gpu-03   3
gpu-04   3</code></pre>
<p>이렇게 하면 <code>nvidia.com/gpu: 1</code>을 요청하는 파드 3개가 같은 물리 GPU에 스케줄될 수 있다.
<br></p>
<p>그리고 여기에 내가 추가적으로 주입해놓은 소프트웨어 VRAM 제한이 한 겹 더 있다. 
JupyterHub 배포 레포의 학생 세션 프로필을 보면, </p>
<pre><code class="language-yaml"># jupyterhub/values.yaml — 학생 세션 프로필 (HAMi 전환 전)
- display_name: &quot;GPU Server (RTX 4070 Ti)&quot;
  description: &quot;딥러닝 GPU 서버 (GPU 메모리 4GB 제한)&quot;
  kubespawner_override:
    extra_resource_guarantees:
      nvidia.com/gpu: 1              # Time Slicing으로 뻥튀기된 GPU 1개
    node_selector:
      gpu-type: &quot;rtx4070ti&quot;
    environment:
      GPU_MEMORY_LIMIT_MB: &quot;3600&quot;    # 제한값은 환경변수로 전달
    lifecycle_hooks:
      postStart:                     # 컨테이너 시작 직후 제한 스크립트 실행
        exec:
          command:
            - /bin/bash
            - -c
            - |
              cp /mnt/gpu-memory-script/gpu_memory_limit.sh /tmp/gpu_memory_limit.sh
              chmod +x /tmp/gpu_memory_limit.sh
              /tmp/gpu_memory_limit.sh
    extra_container_config:
      livenessProbe:                 # 제한 파일이 지워지면 컨테이너 재시작
        exec:
          command:
            - /bin/bash
            - -c
            - test -f /home/jovyan/.ipython/profile_default/startup/00-gpu-memory-limit.py</code></pre>
<p>세션 파드가 뜨면 postStart 훅이 제한 스크립트를 실행하고, livenessProbe가 제한 파일이 살아있는지를 5분마다 감시한다. 학생이 파일을 지우면 컨테이너를 재시작시키는 것까지 해놨다.</p>
<p>그리고, 해당 제한 스크립트의 핵심부는 이렇다.</p>
<pre><code class="language-python"># jupyterhub/scripts/gpu_memory_limit.sh (핵심만 발췌)

# IPython startup 스크립트 생성 
cat &gt; /home/jovyan/.ipython/profile_default/startup/00-gpu-memory-limit.py &lt;&lt; EOF

# TensorFlow VRAM 하드 리밋 적용
tf.config.set_logical_device_configuration(
    gpu, [tf.config.LogicalDeviceConfiguration(memory_limit=GPU_MEMORY_LIMIT_MB)]
)
# Monkey Patch to prevent confusion/retries
tf.config.set_logical_device_configuration = locked_set_logical_device_configuration

# PyTorch VRAM 소프트 리밋 적용
fraction = min(GPU_MEMORY_LIMIT_MB / total_memory, 1.0)
torch.cuda.set_per_process_memory_fraction(fraction, 0)

# Prevent overriding the limit via Monkey Patching
def locked_set_memory_fraction(*args, **kwargs):
    print(f&quot;⚠️  GPU Memory Limit is strictly locked by administrator to {GPU_MEMORY_LIMIT_MB}MB.&quot;)
torch.cuda.set_per_process_memory_fraction = locked_set_memory_fraction
EOF</code></pre>
<p>ipython startup 디렉토리에 파이썬 스크립트를 심어서, 커널이 뜰 때마다 TensorFlow는 logical device 설정으로, PyTorch는 <strong><code>set_per_process_memory_fraction</code></strong>으로 상한을 걸고, 학생이 그 함수를 다시 호출해 풀지 못하도록 몽키패치까지 해두었다.</p>
<p>정리하면: <span style="color: indianred"><strong>스케줄링은 Time Slicing이 뻥튀기로, VRAM 제한은 컨테이너 안의 스크립트가 프레임워크 설정으로 1년 넘게 이 구조로 서비스했다.</strong></span></p>
<hr>
<h3 id="span-stylecolor-slateblue1-2-time-slicing의-한계-시간은-쪼개지만-격리가-없다span"><span style="color: SlateBlue">1-2. Time Slicing의 한계: 시간은 쪼개지만 격리가 없다</span></h3>
<p>Time Slicing은 이름 그대로 시간만 쪼갠다. GPU의 컨텍스트 스위칭으로 여러 프로세스가 번갈아 연산하는 것뿐이고, 격리라는 개념 자체가 없다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/cb471a25-f87e-4343-94b3-0106caef893b/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>VRAM 격리 없음</strong> </span></p>
<ul>
<li><p>파드 3개가 같은 GPU에 올라가도 각자에게 보이는 건 12GB 전부다. </p>
</li>
<li><p>한 명이 12GB를 다 잡으면 나머지 둘은 다음 할당에서 CUDA OOM을 맞는다.</p>
</li>
<li><p>누가 먼저 할당하느냐의 경쟁이다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>연산 격리 없음</strong> </span></p>
<ul>
<li><p>한 명이 무거운 학습을 돌리면 나머지의 연산이 그만큼 밀린다. </p>
</li>
<li><p>보장된 몫이라는 게 없다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>장애 전파</strong></span></p>
<ul>
<li><p>GPU 자체가 리셋되는 하드웨어 오류 (Xid 등)는 같은 카드 위 세션 전부에 영향을 준다.</p>
</li>
<li><p>해당 문제는 하드웨어를 물리적으로 나누는 MIG 말고는 어느 방식도 막지 못한다.</p>
<br>

</li>
</ul>
</li>
</ul>
<p>쿠버네티스 입장에서는 <code>nvidia.com/gpu: 1</code>이 할당된 정상 상태인데, 실제로는 동일한 GPU를 사용하고 있는 옆 사람 코드에 따라 내 세션이 죽는 환경이다. 실습 시간에 &quot;제 코드는 맞는데 CUDA OOM이 떠요&quot;가 나오면, 코드 문제인지 옆 세션 문제인지 구분할 방법이 없다.</p>
<p>*<em>이 중 제일 자주 터지는 VRAM 경쟁만이라도 막아보려고 넣은 게 1-1 섹션에서 제시한 스크립트로, 세션 시작 시 VRAM 제한 주입이다. *</em></p>
<p>하지만 이 방식에는 두 가지 문제가 있다.</p>
<blockquote>
<p><strong>1. 주입된 제한 자체가 강제가 아니라는 것 <br>
2. 연산 간섭과 장애 전파는 VRAM 제한과 무관하게 발생하므로, 컨테이너 안에서 뭘 주입하든 근본적으로 해결할 수 없다는 것</strong></p>
</blockquote>
<hr>
<h3 id="span-stylecolor-slateblue1-3-소프트웨어-제한의-한계-부탁이지-강제가-아니다span"><span style="color: SlateBlue">1-3. 소프트웨어 제한의 한계: 부탁이지 강제가 아니다</span></h3>
<p><strong>주입된 VRAM 제한은 GPU가 아니라 사용자 컨테이너 안에서, 프레임워크 설정으로 걸린다.</strong></p>
<ul>
<li><p>PyTorch의 <code>set_per_process_memory_fraction</code>은 PyTorch의 캐싱 할당자에만 적용된다.</p>
<ul>
<li>PyTorch를 안 거치는 할당인 직접 CUDA 호출, 다른 프레임워크, ipython startup을 안 타는 실행 경로은 그냥 통과되게 된다.</li>
</ul>
</li>
<li><p>몽키패치로 재설정을 막아놨지만, 이것도 결국 같은 컨테이너 안에서 파이썬 레벨로 막는 것이다. </p>
<ul>
<li>새 커널을 다른 방식으로 띄우면 그만이다.<br>


</li>
</ul>
</li>
</ul>
<p>*<em>즉, 지금 구조는 지켜주면 고마운 부탁이지 강제가 아니다. *</em></p>
<p>잘 돌아간 건 학생들이 굳이 우회하지 않았기 때문이지, 우회할 수 없었기 때문이 아니다.</p>
<p><span style="color: indianred"><strong>그리고 두 번째 문제는 연산 간섭과 장애 전파는 이 스크립트가 어떻게 진화하든 못 막는다.</strong></span></p>
<p>제한을 아무리 정교하게 주입해도, 같은 GPU 위의 세션들이 서로 간섭하는 건 컨테이너 바깥에 있는 GPU 레벨의 일이기 때문이다. </p>
<p><strong>결론적으로, 제한은 사용자 컨테이너 안이 아니라 밖에서 걸어야 한다.</strong></p>
<hr>
<h3 id="span-stylecolor-slateblue-1-4-대안-비교-mig--mps--time-slicing--hamispan"><span style="color: SlateBlue"> 1-4. 대안 비교: MIG / MPS / Time Slicing / HAMi</span></h3>
<p>그래서 GPU 공유 방식 자체를 갈아타기로 했다. 선택지를 놓고 비교하면 이렇다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Time Slicing</th>
<th>MPS</th>
<th>MIG</th>
<th>HAMi (vGPU)</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>시간 분할</td>
<td>CUDA 컨텍스트 공유</td>
<td>하드웨어 물리 분할</td>
<td>CUDA 호출 인터셉트</td>
</tr>
<tr>
<td>VRAM 격리</td>
<td>X</td>
<td>△ 제한적</td>
<td>O (하드웨어 수준)</td>
<td>O (하드 리밋)</td>
</tr>
<tr>
<td>연산 격리</td>
<td>X</td>
<td>△ 비율 지정 가능</td>
<td>O (하드웨어 수준)</td>
<td>△ (SW 비율 조절)</td>
</tr>
<tr>
<td>장애 격리</td>
<td>△ (프로세스 수준)</td>
<td>X 클라이언트 간 전파</td>
<td>O</td>
<td>△ (프로세스 수준)</td>
</tr>
<tr>
<td>지원 GPU</td>
<td>전부</td>
<td>전부</td>
<td><strong>A100/H100 등 데이터센터 전용</strong></td>
<td>전부</td>
</tr>
<tr>
<td>비용</td>
<td>0</td>
<td>0</td>
<td>GPU 가격</td>
<td>0 (오픈소스)</td>
</tr>
</tbody></table>
<ul>
<li><p><span style="color: indianred"><strong>MIG</strong></span>:  하드웨어를 물리적으로 쪼개기 때문에, 격리 수준은 제일 좋다. 하지만 A100/H100 같은 데이터센터 GPU 전용 기능이다. <strong>RTX 4070 Ti와 4090은 MIG를 지원하지 않는다.</strong> </p>
</li>
<li><p><span style="color: indianred"><strong>MPS</strong></span>: 여러 프로세스의 커널을 병렬로 섞어 돌릴 수 있고 연산 비율 지정도 되지만, 메모리 격리가 약하고 한 클라이언트의 비정상 종료가 같은 GPU의 다른 클라이언트에 전파된다. 신뢰할 수 있는 워크로드끼리 쓰는 물건이지, 학생들이 아무 코드나 돌리는 멀티테넌트 환경용이 아니다.</p>
</li>
<li><p><span style="color: indianred"><strong>Time Slicing</strong></span>: 지금까지 쓴 것. </p>
</li>
<li><p><span style="color: indianred"><strong>HAMi</strong></span>: 소프트웨어(CUDA 호출 인터셉트) 방식이라 컨슈머 GPU에서도 동작하고, VRAM과 연산 비율을 컨테이너 밖에서 강제한다. 사용자 컨테이너 안의 설정이 아니므로 프레임워크와 무관하고 우회도 안 된다.</p>
</li>
</ul>
<p>Consumer GPU라는 하드웨어 제약에서 MIG가 빠지고, 멀티테넌시 요구에서 MPS가 빠지면 남는 건 HAMi다. 소거법으로도 답이고, 요구사항(VRAM 하드 리밋 + 연산 비율 보장)으로도 답이다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-hami란span"><span style='background-color: MediumAquamarine'>2. HAMi란?</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-정의span"><span style="color: SlateBlue">2-1. 정의</span></h3>
<p><strong>HAMi(Heterogeneous AI Computing Virtualization Middleware)</strong>는 쿠버네티스에서 GPU를 비롯한 이기종 연산 장치를 가상화해서 나눠 쓰게 해주는 오픈소스 미들웨어다. </p>
<p>CNCF Incubating 프로젝트로, 예전 이름인 k8s-vGPU-scheduler로도 알려져 있다. </p>
<p><span style="color: indianred"><strong>HAMi는 물리 GPU 1장을 여러 개의 vGPU로 쪼개고, 각 vGPU에 VRAM 용량과 연산 비율을 하드 리밋으로 걸 수 있게 해준다.</strong> </span></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/5b2f4e4f-67c4-4b34-886f-7a077e345217/image.png" alt=""></p>
<p>MIG처럼 하드웨어 지원이 필요한 게 아니라 순수 소프트웨어 방식이라, 우리처럼 Consumer GPU(RTX 4070 Ti/4090)를 쓰는 클러스터에서도 동작한다.</p>
<p>파드 입장에서의 사용법은 기존과 거의 같고, 리소스 요청에 몇 줄 추가만 하면 된다.</p>
<pre><code class="language-yaml">resources:
  limits:
    nvidia.com/gpu: 1                # vGPU 1개
    nvidia.com/gpumem-percentage: 50 # 물리 VRAM의 50%
    nvidia.com/gpucores: 50          # 연산(SM)의 50%</code></pre>
<p>*<em>VRAM은 <code>gpumem</code>(절대값 MB)이나 <code>gpumem-percentage</code>(물리 VRAM 대비 비율) 중 하나로 지정할 수 있다. *</em></p>
<p>왜 우리가 percentage를 쓰게 됐는지는 트러블슈팅에서 다룬다.</p>
<hr>
<h3 id="span-stylecolor-slateblue2-2-동작-원리-libvgpuso로-cuda-호출을-가로챈다span"><span style="color: SlateBlue">2-2. 동작 원리: libvgpu.so로 CUDA 호출을 가로챈다</span></h3>
<p>HAMi의 핵심은 HAMi-core라고 부르는 <strong><code>libvgpu.so</code></strong> 라이브러리다. </p>
<p>애플리케이션이 GPU를 쓸 때의 호출 경로는 이렇다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/c2b6827b-8890-4555-998f-93a23bbcf360/image.png" alt=""></p>
<p>HAMi는 컨테이너가 시작될 때 <code>libvgpu.so</code>를 컨테이너 안에 마운트하고 <code>LD_PRELOAD</code>로 끼워넣는다. 이후 컨테이너 안에서 일어나는 모든 CUDA Driver API 호출과 NVML 호출은 <code>libvgpu.so</code>를 먼저 거친다.</p>
<p>이 인터셉트 레이어에서 두 가지 제한이 걸린다.</p>
<p><span style="color: indianred"><strong>1. VRAM 하드 리밋</strong> </span></p>
<ul>
<li><p><code>cuMemAlloc</code> 같은 메모리 할당 호출을 가로채서 컨테이너별 누적 사용량을 추적한다. </p>
</li>
<li><p>할당 요청이 한도를 넘으면 호출 자체를 CUDA OOM으로 실패시킨다. </p>
</li>
<li><p>어떤 프레임워크를 쓰든, 프레임워크 없이 CUDA를 직접 호출하든, 결국 이 경로를 지나므로 우회가 안 된다. </p>
</li>
<li><p>NVML 조회도 가로채기 때문에 컨테이너 안에서 <code>nvidia-smi</code>를 치면 물리 VRAM이 아니라 할당된 한도가 전체 용량으로 보인다.</p>
</li>
<li><p>결국에는, 사용자 입장에선 그냥 작은 GPU 1장이다.</p>
<br>

</li>
</ul>
<p><span style="color: indianred"><strong>2. 연산 비율 리밋</strong> </span></p>
<ul>
<li><p>GPU 사용률(SM utilization)을 주기적으로 모니터링하면서, 컨테이너가 지정된 비율(<code>gpucores</code>)을 초과해서 쓰고 있으면 커널 런치를 지연시켜 사용률을 한도 안으로 밀어넣는다. </p>
</li>
<li><p>Time Slicing처럼 알아서 나눠 갖길 기대하는 게 아니라, 비율을 초과하는 쪽을 실제로 억제한다.</p>
</li>
<li><p>다만 메모리와 달리 연산 제한은 사후 조절이다. GPU 자체는 여전히 프로세스 간 Time-Slicing으로 돌고, HAMi는 사용률을 주기적으로 재서 비율을 넘긴 쪽의 커널 런치를 늦추는 방식이라, 옆 세션이 놀고 있으면 지정 비율 이상을 쓰기도 한다. 할당 시점에 확정적으로 잘리는 메모리 제한보다는 약한 보장이다.</p>
<br>

</li>
</ul>
<p>따라서, 1장에서 내가 구축해서 사용했던 소프트웨어 제한과 비교하게 되면 이러하다.</p>
<table>
<thead>
<tr>
<th></th>
<th>기존 (스크립트 주입)</th>
<th>HAMi (libvgpu.so)</th>
</tr>
</thead>
<tbody><tr>
<td>제한이 걸리는 위치</td>
<td>프레임워크 설정 (파이썬 레벨)</td>
<td>CUDA Driver API (라이브러리 레벨)</td>
</tr>
<tr>
<td>프레임워크 의존</td>
<td>PyTorch/TF 각각 처리</td>
<td>무관 (모든 CUDA 호출이 지나감)</td>
</tr>
<tr>
<td>우회 가능성</td>
<td>ipython 안 타면 끝</td>
<td>LD_PRELOAD 아래로 내려가지 않는 한 불가</td>
</tr>
<tr>
<td>관리 주체</td>
<td>사용자 컨테이너 안</td>
<td>클러스터 인프라</td>
</tr>
</tbody></table>
<hr>
<h3 id="span-stylecolor-slateblue2-3-구성-컴포넌트span"><span style="color: SlateBlue">2-3. 구성 컴포넌트</span></h3>
<p>HAMi를 설치하면 크게 세 개의 컴포넌트가 클러스터에 추가되고, 이들이 파드 생성부터 실행까지 각 단계에 개입한다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/94efe866-4543-43e2-b5d5-a0e1f785bd2c/image.png" alt=""></p>
<p><span style="color: indianred"><strong>1. hami-device-plugin (DaemonSet, GPU 노드마다)</strong></span></p>
<ul>
<li><p>기존 NVIDIA device plugin을 대체한다.</p>
</li>
<li><p>노드의 물리 GPU를 <code>deviceSplitCount</code> 배수만큼의 vGPU로 등록하고, GPU의 UUID/VRAM/코어 정보를 노드 어노테이션(<code>hami.io/node-nvidia-register</code>)으로 보고한다.</p>
</li>
<li><p>스케줄러는 이 어노테이션을 보고 각 GPU에 남은 VRAM과 코어가 얼마인지 판단한다.</p>
</li>
</ul>
<p><span style="color: indianred"><strong>2. hami-scheduler (Deployment)</strong></span></p>
<ul>
<li><p>kube-scheduler의 extender로 동작한다. </p>
</li>
<li><p>HAMi 리소스를 요청한 파드의 스케줄링 때 Filter/Score 단계에 개입해서, <strong>노드 안의 어느 물리 GPU에 배치할지까지 결정한다.</strong> </p>
</li>
<li><p>기본 스케줄러는 노드에 GPU가 남았는가만 보지만, hami-scheduler는 이 GPU에 VRAM 50%를 더 얹을 수 있는가를 본다. 이로인해, <strong>GPU별 잔여량 기반 스케줄링이 가능해진다.</strong></p>
</li>
</ul>
<p><span style="color: indianred"><strong>3. mutating webhook</strong></span></p>
<ul>
<li><p>파드 생성 요청을 가로채는 admission webhook이다. </p>
</li>
<li><p>리소스에 <code>nvidia.com/gpu</code>가 있는 파드를 발견하면 <code>schedulerName</code>을 <code>hami-scheduler</code>로 바꿔치기한다. </p>
</li>
<li><p>사용자(JupyterHub 같은 상위 시스템)는 스케줄러를 지정할 필요 없이, 평소처럼 파드를 만들면 자동으로 HAMi 경로를 타게 된다.</p>
</li>
<li><p>해당 Webhook은 hami-scheduler 파드안에 속하게 된다.</p>
</li>
</ul>
<p><br>여기에 실행 단계의 <strong>HAMi-core(libvgpu.so)</strong>까지 합치면 전체 흐름은 이렇다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/ea0717b7-5124-4c9c-8139-1821c6186c30/image.png" alt=""></p>
<pre><code>파드 생성 요청
 → [webhook] HAMi 리소스 감지 → schedulerName을 hami-scheduler로 변경
 → [hami-scheduler] 노드 + 물리 GPU 선택 (GPU별 잔여 VRAM/코어 계산)
 → [device-plugin] 컨테이너에 GPU 할당 + libvgpu.so 마운트
   + 한도를 환경변수로 주입 (CUDA_DEVICE_MEMORY_LIMIT, CUDA_DEVICE_SM_LIMIT)
 → [컨테이너 실행] 모든 CUDA 호출이 libvgpu.so를 거침 → VRAM/연산 하드 리밋</code></pre><p>즉, 네 단계 (<code>파드 생성 → 스케줄링 → 할당 → 실행</code>) 전부에 HAMi가 끼어 있다. </p>
<hr>
<h3 id="span-stylecolor-slateblue2-4-이번-전환의-목표-설계span"><span style="color: SlateBlue">2-4. 이번 전환의 목표 설계</span></h3>
<p>우리 환경에 맞춘 설계는 이렇다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>GPU당 분할 수 (<code>deviceSplitCount</code>)</td>
<td>2</td>
<td>기존 Time Slicing은 3분할이었지만, 12GB급 GPU를 3분할하면 1인당 4GB로 실습용으로도 빠듯했다. 2분할 × 6GB가 현실적</td>
</tr>
<tr>
<td>VRAM (<code>gpumem-percentage</code>)</td>
<td>50</td>
<td>4070 Ti(12GB)는 1인당 약 6GB, 4090(24GB)은 약 12GB. 비율로 걸면 카드가 달라도 프로필 하나로 커버</td>
</tr>
<tr>
<td>연산 (<code>gpucores</code>)</td>
<td>50</td>
<td>2분할이니 연산도 절반씩 보장</td>
</tr>
<tr>
<td>노드 분리</td>
<td><code>gpu-type</code> 라벨</td>
<td>4070 Ti는 딥러닝 실습 학생용, 4090은 졸업 논문작성자/관리자 전용 (기존 정책 유지)</td>
</tr>
</tbody></table>
<p>기존 구조와 비교하면:</p>
<pre><code class="language-bash">[기존]
스케줄링: Time Slicing replicas=3 
VRAM:   postStart 스크립트 주입 (3600MB, 프레임워크 설정, 소프트)
연산:    없음

[전환 후]
스케줄링: hami-scheduler (GPU별 잔여량 계산, 2분할)
VRAM:   libvgpu.so 인터셉트 (물리 VRAM의 50%, CUDA 레벨, 하드)
연산:    libvgpu.so 인터셉트 (50%, 하드)</code></pre>
<p>따라서, 내가 구축한 약 200줄짜리 제한 스크립트, postStart 훅, livenessProbe 감시는 전부 삭제 대상이 된다. </p>
<p><span style="color: indianred"><strong>추가적으로, 한 가지 짚고 갈 것은 내 학과 클러스터는 GPU Operator의 컨테이너형 드라이버 (NVIDIA GPU Operator)를 쓴다.</strong></span></p>
<p>HAMi 문서의 기본 설치는 호스트에 드라이버가 깔려 있다고 가정하기 때문에, 이 조합에서는 손봐야 할 게 몇 가지 있다. </p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-전환-작업span"><span style='background-color: MediumAquamarine'>3. 전환 작업</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue3-1-time-slicing-걷어내기span"><span style="color: SlateBlue">3-1. Time Slicing 걷어내기</span></h3>
<p>새 체계를 올리기 전에 기존 체계부터 걷어냈다. </p>
<p>Time Slicing은 GPU Operator의 device plugin 설정으로 들어가 있으므로, ClusterPolicy에서 제거한다.</p>
<pre><code class="language-bash">$ kubectl patch clusterpolicy cluster-policy --type=json \
    -p=&#39;[{&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/spec/devicePlugin/config&quot;}]&#39;
clusterpolicy.nvidia.com/cluster-policy patched

$ kubectl -n nvidia-gpu-operator delete cm time-slicing-config
configmap &quot;time-slicing-config&quot; deleted from nvidia-gpu-operator namespace

$ kubectl patch clusterpolicy cluster-policy --type=merge \
    -p &#39;{&quot;spec&quot;:{&quot;devicePlugin&quot;:{&quot;enabled&quot;:false}}}&#39;
clusterpolicy.nvidia.com/cluster-policy patched</code></pre>
<p><strong>이때, Time Slicing 설정만 지우는 게 아니라 GPU Operator의 device plugin 자체를 꺼야 한다.</strong> </p>
<p>vGPU 등록은 이제 HAMi의 device plugin이 맡을 건데, 만약 둘 다 살아 있으면 같은 GPU를 두 플러그인이 서로 다른 개수로 등록하려 드는 충돌 상태가 된다.</p>
<pre><code class="language-bash">$ kubectl -n nvidia-gpu-operator get pods --no-headers | grep device-plugin
nvidia-device-plugin-daemonset-s2m6w    0/1   Terminating   0    30s

$ kubectl get nodes -o custom-columns=&quot;NODE:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu&quot; --no-headers | grep -v cp
gpu-01   0
gpu-02   0
gpu-03   0
gpu-04   0
gpu-05   0</code></pre>
<p>따라서, NVIDIA GPU Operator의 device plugin이 내려가면서 GPU allocatable이 전 노드 0이 됐다. </p>
<p>이 시점에 클러스터는 GPU가 한 장도 없는 상태이고, 이제 해당 역할을 HAMi가 하게 될 예정이다.</p>
<p><span style="color: indianred">*<em>참고로 GPU Operator에서 끄는 건 device plugin뿐이고, Driver(컨테이너형), Container Toolkit, DCGM 모니터링 등 나머지 컴포넌트는 그대로 GPU Operator가 관리한다. *</em></span></p>
<blockquote>
<p><strong>드라이버 공급은 GPU Operator, GPU 등록/분할/스케줄링은 HAMi</strong> </p>
</blockquote>
<hr>
<h3 id="span-stylecolor-slateblue-3-2-hami-설치span"><span style="color: SlateBlue"> 3-2. HAMi 설치</span></h3>
<p>HELM으로 설치한다.</p>
<pre><code class="language-bash">$ helm repo add hami-charts https://project-hami.github.io/HAMi/
&quot;hami-charts&quot; has been added to your repositories

$ helm search repo hami-charts/hami --versions | head -3
NAME             CHART VERSION  APP VERSION  DESCRIPTION
hami-charts/hami 2.9.0          2.9.0        Heterogeneous AI Computing Virtualization Middl...
hami-charts/hami 2.8.3          2.8.3        Heterogeneous AI Computing Virtualization Middl...</code></pre>
<p>이때, HAMi device plugin은 <code>gpu=on</code> 라벨이 붙은 노드에만 배포되므로, GPU 노드에 라벨부터 붙인다.</p>
<pre><code class="language-bash">$ kubectl label node gpu-01 gpu-02 gpu-03 gpu-04 gpu-05 gpu=on --overwrite
node/gpu-01 labeled
node/gpu-02 labeled
node/gpu-03 labeled
node/gpu-04 labeled
node/gpu-05 labeled</code></pre>
<p>설치 후, 최종적으로 안착한 값은 이러하다.</p>
<pre><code class="language-bash">$ helm install hami hami-charts/hami -n kube-system \
    --set scheduler.kubeScheduler.imageTag=v1.35.0 \
    --set devicePlugin.deviceSplitCount=2 \
    --set devicePlugin.runtimeClassName=nvidia</code></pre>
<p>옵션 세 개의 의미:</p>
<ul>
<li><p><code>scheduler.kubeScheduler.imageTag=v1.35.0</code></p>
<ul>
<li>hami-scheduler는 내부에 kube-scheduler 이미지를 품고 돌아간다. 클러스터 버전(v1.35)과 맞춰주지 않으면 스케줄러 동작이 어긋날 수 있어 명시적으로 고정했다.</li>
</ul>
</li>
<li><p><code>devicePlugin.deviceSplitCount=2</code> </p>
<ul>
<li>GPU당 vGPU 2개. 2-4 섹션에서 결정한 환경의 설계값이다. (설계자에 따라 다르게 설정 가능)</li>
</ul>
</li>
<li><p><code>devicePlugin.runtimeClassName=nvidia</code> </p>
<ul>
<li>컨테이너형 드라이버 환경에서 필요한 옵션 </li>
<li>사실 처음 설치할 때 이 옵션 없이 올렸다가 device plugin 전체가 CrashLoopBackOff에 빠졌다. 왜 그런지는 트러블슈팅(4-1 섹션)에서 다룬다.</li>
</ul>
</li>
</ul>
<p>설치 후 상태는 이러하다.</p>
<pre><code class="language-bash">$ kubectl -n kube-system get pods --no-headers | grep hami
hami-device-plugin-8nh6m          2/2   Running   0     54s
hami-device-plugin-jbz9f          2/2   Running   0     54s
hami-device-plugin-tg8xx          2/2   Running   0     54s
hami-device-plugin-vhn6f          2/2   Running   0     54s
hami-device-plugin-zx6hs          2/2   Running   0     54s
hami-scheduler-645b59d5cb-mdbls   2/2   Running   0     54s</code></pre>
<p><span style="color: indianred"><strong>GPU 노드 5대에 device plugin이 하나씩(각 파드는 device-plugin + vgpu-monitor 컨테이너 2개), hami-scheduler가 떴다. 참고로, HAMi mutating webhook은 hami-scheduler 파드에 함께 들어 있다.</strong></span></p>
<hr>
<h3 id="span-stylecolor-slateblue-3-3-등록-확인-클러스터가-gpu를-어떻게-보게-됐나span"><span style="color: SlateBlue"> 3-3. 등록 확인: 클러스터가 GPU를 어떻게 보게 됐나</span></h3>
<p>3-1 섹션에서 0이 됐던 GPU allocatable이 어떻게 바뀌었는지 확인한다.</p>
<pre><code class="language-bash">$ kubectl get nodes -o custom-columns=&quot;NODE:.metadata.name,vGPU:.status.allocatable.nvidia\.com/gpu&quot; --no-headers | grep -v cp
gpu-01   2
gpu-02   2
gpu-03   2
gpu-04   2
gpu-05   2</code></pre>
<p>물리 GPU 1장 → vGPU 2개. Time Slicing 때의 3이 아니라 2다. (<code>deviceSplitCount=2</code>)</p>
<p>숫자만 보면 Time Slicing의 뻥튀기와 다를 게 없어 보이지만, 노드 어노테이션을 보면 차이가 드러난다.</p>
<pre><code class="language-bash">$ kubectl get node gpu-01 gpu-05 -o jsonpath=&#39;{range .items[*]}{.metadata.name}: {.metadata.annotations.hami\.io/node-nvidia-register}{&quot;\n&quot;}{end}&#39;

gpu-01: [{&quot;id&quot;:&quot;GPU-550ccdd7-4f0e-99a8-17f1-87ec56026e91&quot;,&quot;count&quot;:2,&quot;devmem&quot;:12282,&quot;devcore&quot;:100,&quot;type&quot;:&quot;NVIDIA GeForce RTX 4070 Ti&quot;,&quot;mode&quot;:&quot;hami-core&quot;, ...
gpu-05: [{&quot;id&quot;:&quot;GPU-bdd1c304-e063-26b6-90f0-ae0f40b98f96&quot;,&quot;count&quot;:2,&quot;devmem&quot;:24564,&quot;devcore&quot;:100,&quot;type&quot;:&quot;NVIDIA GeForce RTX 4090&quot;,&quot;mode&quot;:&quot;hami-core&quot;, ...</code></pre>
<p>*<em>HAMi device plugin이 각 노드의 물리 GPU를 UUID 단위로, 실측 VRAM과 함께 등록해놨다. *</em></p>
<p>4070 Ti의 VRAM은 12282MB, 4090은 24564MB으로, hami-scheduler는 이 정보를 보고 &quot;이 GPU에 남은 VRAM이 얼마인가&quot;를 계산해서 파드를 특정 물리 GPU에 배치한다. </p>
<p><span style="color: indianred"><strong>즉, Time Slicing에는 없던, GPU별 잔여량 기반 스케줄링이 가능해진 것이다.</strong></span></p>
<p>여기서 눈여겨볼 값이 하나 있다. 4070 Ti의 실측 VRAM이 12288이 아니라 12282MB라는 것이다. 스펙상 12GB(12288MB)지만 실제 가용량은 6MB 모자란다. 이 6MB가 뒤에서 트러블슈팅 하나(4-3 섹션)를 만들어낸다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-트러블슈팅span"><span style='background-color: MediumAquamarine'>4. 트러블슈팅</span></h2>
<hr>



<h3 id="span-stylecolor-slateblue4-1-device-plugin-crashloopbackoff-nvml-error_library_not_foundspan"><span style="color: SlateBlue">4-1. device-plugin CrashLoopBackOff: NVML ERROR_LIBRARY_NOT_FOUND</span></h3>
<p>앞서 3-2 섹션에서 말한 대로, 처음엔 <code>runtimeClassName</code> 옵션 없이 설치했다. </p>
<pre><code class="language-bash">$ kubectl -n kube-system get pods -o wide --no-headers | grep hami
hami-device-plugin-2nb4d          0/2   CrashLoopBackOff     6 (49s ago)   112s   gpu-03
hami-device-plugin-gbbqc          0/2   CrashLoopBackOff     8 (8s ago)    112s   gpu-04
hami-device-plugin-r7rzh          0/2   CrashLoopBackOff     6 (45s ago)   112s   gpu-01
hami-device-plugin-swbs4          0/2   CrashLoopBackOff     6 (47s ago)   112s   gpu-02
hami-device-plugin-xm8bq          0/2   PostStartHookError   6 (49s ago)   112s   gpu-05
hami-scheduler-7b44c774dd-6fpx6   2/2   Running              0             112s   gpu-05</code></pre>
<p>스케줄러는 멀쩡한데 device plugin이 전 노드에서 CrashLoopBackOff 에러가 발생하였다.</p>
<p>로그를 보면,</p>
<pre><code class="language-bash">$ kubectl -n kube-system logs hami-device-plugin-2nb4d -c device-plugin --tail=8
E0819 08:10:19.293117  factory.go:135] Incompatible strategy detected auto
E0819 08:10:19.293126  factory.go:136] If this is a GPU node, did you configure the NVIDIA Container Toolkit?
E0819 08:10:19.297989  main.go:201] error starting plugins: ... invalid device discovery strategy

$ kubectl -n kube-system logs hami-device-plugin-2nb4d -c vgpu-monitor --tail=3
E0819 08:10:19.567548  main.go:129] Received error: failed to initialize NVML: ERROR_LIBRARY_NOT_FOUND</code></pre>
<p><strong><code>NVML: ERROR_LIBRARY_NOT_FOUND</code> 로그로, NVML 라이브러리(libnvidia-ml.so)를 찾지 못하고 있다.</strong></p>
<p>해당 에러는 우리 클러스터의 컨테이너형 드라이버 구조와 연결된다. </p>
<p>호스트에는 NVIDIA 드라이버가 설치되어 있지 않고, 드라이버 라이브러리는 전부 드라이버 파드가 관리하는 <code>/run/nvidia/driver</code> 아래에만 있다. </p>
<p>그런데 HAMi device plugin은 기본적으로 일반 런타임으로 뜨기 때문에, 컨테이너 안에서 이 라이브러리들이 보이지 않는다. </p>
<p><span style="color: indianred">*<em>해결은 device plugin 파드를 <code>nvidia</code> RuntimeClass로 띄우는 것이다. *</em></span></p>
<p>이 런타임으로 뜨면 NVIDIA Container Toolkit이 <code>/run/nvidia/driver</code>의 드라이버 라이브러리를 컨테이너에 주입해준다. </p>
<p>마침 GPU Operator가 만들어둔 RuntimeClass가 클러스터에 이미 있기에 적용하였다.</p>
<pre><code class="language-bash">$ kubectl get runtimeclass
NAME            HANDLER         AGE
nvidia          nvidia          197d
nvidia-cdi      nvidia-cdi      197d
nvidia-legacy   nvidia-legacy   197d</code></pre>
<p><code>nvidia</code> 런타임으로 뜨면 NVIDIA Container Toolkit이 <code>/run/nvidia/driver</code>의 드라이버 라이브러리를 컨테이너에 주입해준다.</p>
<pre><code class="language-bash">$ helm upgrade hami hami-charts/hami -n kube-system \
    --set scheduler.kubeScheduler.imageTag=v1.35.0 \
    --set devicePlugin.deviceSplitCount=2 \
    --set devicePlugin.runtimeClassName=nvidia
STATUS: deployed

$ kubectl -n kube-system get pods --no-headers | grep hami
hami-device-plugin-8nh6m          2/2   Running   0     54s
hami-device-plugin-jbz9f          2/2   Running   0     54s
hami-device-plugin-tg8xx          2/2   Running   0     54s
hami-device-plugin-vhn6f          2/2   Running   0     54s
hami-device-plugin-zx6hs          2/2   Running   0     54s
hami-scheduler-645b59d5cb-mdbls   2/2   Running   0     54s</code></pre>
<hr>
<h3 id="span-stylecolor-slateblue4-2-워크로드-starterror-unresolvable-cdi-devicesspan"><span style="color: SlateBlue">4-2. 워크로드 StartError: unresolvable CDI devices</span></h3>
<p>이제 vGPU가 등록됐으니 테스트 파드를 띄워보았다.</p>
<p>한 GPU에 여러 파드가 실제로 들어가는지 보는 게 목적이므로, nodeSelector로 스케줄 대상을 gpu-01 하나로 고정하고 같은 스펙의 파드 3개(<code>gpumem: 6144</code> + <code>gpucores: 50</code> )를 만들었다. </p>
<p>GPU 1장을 2분할했으니 2개는 돌고 1개는 Pending이 되어야 정상이다.</p>
<pre><code class="language-bash">$ kubectl get pods -o wide | grep hami-test
hami-test-1   0/1   StartError   0   60s   10.0.1.133   gpu-01
hami-test-2   0/1   StartError   0   60s   10.0.1.202   gpu-01
hami-test-3   0/1   StartError   0   60s   10.0.1.91    gpu-01</code></pre>
<p>3개의 Pod 모두다 StartError가 떠서 Describe 결과</p>
<pre><code class="language-bash">$ kubectl describe pod hami-test-1 | tail -8
  Normal   Scheduled         95s   hami-scheduler  Successfully assigned default/hami-test-1 to gpu-01
  Normal   FilteringSucceed  97s   hami-scheduler  find fit node(gpu-01), 0 nodes not fit, 1 nodes fit(gpu-01:0.00)
  Normal   BindingSucceed    96s   hami-scheduler  Successfully binding node [gpu-01] to default/hami-test-1
  Normal   Pulled            91s   kubelet         Successfully pulled image &quot;nvidia/cuda:12.2.0-base-ubuntu22.04&quot;
  Normal   Created           91s   kubelet         Container created
  Warning  Failed            90s   kubelet         Error: failed to create containerd task: failed to create
    shim task: OCI runtime create failed: could not apply required modification to OCI specification:
    error modifying OCI spec: failed to inject CDI devices: unresolvable CDI devices
    management.nvidia.com/gpu=GPU-550ccdd7-4f0e-99a8-17f1-87ec56026e91</code></pre>
<p>이벤트를 보면 스케줄링까지는 정상이다. </p>
<p>hami-scheduler가 gpu-01을 골라 바인딩까지 마쳤고, 실패는 컨테이너를 실제로 만드는 런타임 단계에서 났다. </p>
<p><code>unresolvable CDI devices</code>: CDI 방식으로 GPU 장치를 주입하려 했는데 해당 CDI 디바이스 정의를 찾지 못했다는 뜻이다.</p>
<p>CDI 얘기가 왜 나오는지 GPU Operator 설정을 확인해보니, CDI 모드가 켜져 있었다.</p>
<pre><code class="language-bash">$ kubectl get clusterpolicy cluster-policy -o jsonpath=&#39;{.spec.cdi}&#39;
{&quot;default&quot;:false,&quot;enabled&quot;:true}</code></pre>
<p>CDI(Container Device Interface)는 GPU 같은 장치를 컨테이너에 주입하는 표준 스펙이다. </p>
<p>문제는 GPU Operator의 CDI 모드와 HAMi의 장치 할당 방식이 서로 맞물리지 않는다는 점이다. </p>
<p>HAMi가 파드에 붙여준 장치 이름 (<code>management.nvidia.com/gpu=GPU-...</code>)을 런타임이 CDI 정의에서 찾으려다 실패한다. </p>
<p><span style="color: indianred"><strong>HAMi는 CDI가 아니라 환경변수 기반으로 장치를 주입하므로, 공존 구성에서는 CDI를 끄는 게 맞다.</strong></span></p>
<pre><code class="language-bash">$ kubectl patch clusterpolicy cluster-policy --type=merge \
    -p &#39;{&quot;spec&quot;:{&quot;cdi&quot;:{&quot;enabled&quot;:false,&quot;default&quot;:false}}}&#39;
clusterpolicy.nvidia.com/cluster-policy patched

$ kubectl -n nvidia-gpu-operator rollout status ds/nvidia-container-toolkit-daemonset
daemon set &quot;nvidia-container-toolkit-daemonset&quot; successfully rolled out</code></pre>
<p>container toolkit이 CDI 없는 설정으로 재배포되면서 이 에러는 사라졌다.</p>
<hr>
<h3 id="span-stylecolor-slateblue4-3-cardinsufficientmemory-6144-×-2--12288--12282span"><span style="color: SlateBlue">4-3. CardInsufficientMemory: 6144 × 2 = 12288 &gt; 12282?</span></h3>
<p>CDI를 고치고 테스트 파드를 다시 만들었다.</p>
<pre><code class="language-bash">$ kubectl get pods -o wide | grep hami-test
hami-test-1   1/1   Running   0   45s   10.0.1.21   gpu-01
hami-test-2   0/1   Pending   0   45s   &lt;none&gt;      &lt;none&gt;
hami-test-3   0/1   Pending   0   45s   &lt;none&gt;      &lt;none&gt;</code></pre>
<p>첫 파드는 Running인데,  두 번째가 Pending이다. </p>
<p>이론상 2분할이기때문에 2개의 Pod까지는 gpu-01에 들어가야한다.</p>
<pre><code class="language-bash">$ kubectl describe pod hami-test-2 | tail -2
  Warning  FilteringFailed   45s (x11 over 60s)  hami-scheduler  1 nodes CardInsufficientMemory(gpu-01)
  Warning  FilteringFailed   45s (x11 over 60s)  hami-scheduler  no available node, 1 nodes do not meet</code></pre>
<p>로그에서 확인해보니 <code>CardInsufficientMemory</code> 원인으로, GPU에 VRAM이 모자란게 문제였다.</p>
<p>12GB 카드에 6GB짜리 두 개가 왜 안 들어가나 싶지만, <strong>4070 Ti의 실측 등록값은 12288이 아니라 12282MB다.</strong></p>
<pre><code>요청: 6144MB × 2 = 12288MB
실측: 12282MB
→ 6MB 초과. 두 번째 파드는 영원히 못 들어간다.</code></pre><p>12GB는 마케팅 용량이고 실제 가용 VRAM은 그보다 약간(6MB)작다. 절대값(MB)으로 반반을 나누면 이 오차 때문에 딱 두 번째부터 막힌다.</p>
<p><span style="color: indianred"><strong>해결은 절대값을 버리고 비율로 가는 것이므로, <code>nvidia.com/gpumem-percentage</code>으로 바꾸었다.</strong></span></p>
<pre><code class="language-yaml"># 변경 전
nvidia.com/gpumem: 6144
# 변경 후
nvidia.com/gpumem-percentage: 50</code></pre>
<p>percentage는 실측 VRAM 기준으로 계산되므로 4070 Ti는 6141MB, 4090은 12282MB로 정확히 절반이 잡힌다. 카드별 하드코딩이 없어지니 나중에 다른 모델이 증설돼도 프로필을 수정할 필요가 없다는 부수 효과도 있다.</p>
<pre><code class="language-bash">$ kubectl get pods -o wide | grep hami-test     # percentage 50으로 재생성 후
hami-test-1   1/1   Running   0   40s   10.0.1.72    gpu-01
hami-test-2   1/1   Running   0   40s   10.0.1.250   gpu-01
hami-test-3   0/1   Pending   0   40s   &lt;none&gt;       &lt;none&gt;</code></pre>
<p>두 파드가 같은 노드에 들어갔고 세 번째는 Pending으로 2분할 초과가 정확히 차단된다. </p>
<hr>
<h3 id="span-stylecolor-slateblue4-4-jupyterhub-스폰-실패-user-scheduler가-hami-웹훅을-우회한다span"><span style="color: SlateBlue">4-4. JupyterHub 스폰 실패: user-scheduler가 HAMi 웹훅을 우회한다</span></h3>
<p>테스트 파드까지 확인했으니, 실제 서비스 중인 JupyterHub에 적용할 차례다.</p>
<p>프로필을 HAMi 리소스로 바꾸고(6장에서 정리) 세션을 스폰해봤는데, 스케줄 실패가 떴다.</p>
<pre><code class="language-bash">0/6 nodes are available: 1 node(s) didn&#39;t match Pod&#39;s node affinity/selector,
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
4 Insufficient nvidia.com/gpucores, 4 Insufficient nvidia.com/gpumem-percentage.</code></pre>
<p>GPU 노드 4대 전부에서 <code>Insufficient nvidia.com/gpucores</code> 로그가 떴고, HAMi 리소스가 부족하다고 한다. </p>
<p>방금 테스트 파드는 잘만 스케줄됐는데 세션 파드만 안 되는 상황이라, 세션 파드가 어떻게 만들어졌는지부터 확인했다.</p>
<pre><code class="language-bash">$ kubectl get pod jupyter-test -n jupyterhub -o jsonpath=&#39;schedulerName: {.spec.schedulerName}&#39;

schedulerName: jupyterhub-user-scheduler

limits: {&quot;cpu&quot;:&quot;6&quot;,&quot;memory&quot;:&quot;21474836480&quot;,&quot;nvidia.com/gpu&quot;:&quot;1&quot;,
         &quot;nvidia.com/gpucores&quot;:&quot;50&quot;,&quot;nvidia.com/gpumem-percentage&quot;:&quot;50&quot;}</code></pre>
<p>리소스 요청은 제대로 들어갔으나, schedulerName이 다른것을 확인하였다. hami-scheduler가 아니라 jupyterhub-user-scheduler로 되어 있다.</p>
<p><code>z2jh(JupyterHub 헬름 차트)</code>는 세션 파드를 효율적으로 몰아 배치하기 위한 자체 스케줄러인 user-scheduler를 기본으로 켜두고, hub가 세션 파드를 만들 때 schedulerName을 직접 지정한다. </p>
<pre><code class="language-bash">$ kubectl get cm hub -n jupyterhub -o jsonpath=&#39;{.data.jupyterhub_config\.py}&#39; | grep -B1 -A1 &quot;scheduler_name&quot;
# scheduling:
if get_config(&quot;scheduling.userScheduler.enabled&quot;):
    c.KubeSpawner.scheduler_name = get_name(&quot;user-scheduler&quot;)</code></pre>
<p>이때, HAMi 웹훅의 동작 방식과 충돌이 생긴다. </p>
<p>웹훅은 이미 커스텀 스케줄러가 지정된 파드는 건드리지 않는다. (다른 스케줄링 체계를 존중하는 설계) </p>
<p>그래서 세션 파드는 hami-scheduler에 도달하지 못한 채 user-scheduler로 갔고, user-scheduler는 HAMi 가상 리소스를 노드 allocatable에서 찾다가 Insufficient로 실패한 것이다.</p>
<p><span style="color: indianred"><strong>따라서, user-scheduler를 끄면 hub가 schedulerName을 지정하지 않게 되고, 그러면 웹훅이 정상적으로 개입할 수 있다.</strong></span> </p>
<p>values.yaml에 두 줄을 추가했다.</p>
<pre><code class="language-yaml">scheduling:
  userScheduler:
    enabled: false</code></pre>
<p>ArgoCD sync 후 확인</p>
<pre><code class="language-bash">$ kubectl -n jupyterhub get deploy user-scheduler
Error from server (NotFound): deployments.apps &quot;user-scheduler&quot; not found

$ kubectl -n jupyterhub get pods --no-headers | grep &quot;^hub&quot;
hub-79948b49c7-57psh   1/1   Running   0   48s</code></pre>
<p>user-scheduler가 제거되었고 hub가 새 설정으로 재기동되었다. 이후 스폰된 세션 파드는 웹훅을 거쳐 hami-scheduler로 간다.</p>
<p>참고로, user-scheduler의 원래 역할은 세션 파드를 한 노드에 몰아서 배치하는 역할이다. 
따라서, hami-scheduler의 GPU 잔여량 기반 배치가 사실상 대체하므로 기능적인 손실은 없다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-검증span"><span style='background-color: MediumAquamarine'>5. 검증</span></h2>
<hr>

<p>트러블슈팅을 마무리 짓고 검증을 진행하였으며, 검증은 총 5개 항목으로 진행하였다.</p>
<blockquote>
<p><strong>1. 같은 GPU를 두 파드가 나눠 쓰는가
2. 분할 수를 넘으면 차단되는가
3. 실전(JupyterHub 세션)에서 학습이 도는가 
4. 한도를 초과하면 정말 막히는가
5.  모니터링은 유지되는가</strong></p>
</blockquote>
<h3 id="span-stylecolor-slateblue5-1-같은-gpu를-두-파드가-나눠-쓰는가span"><span style="color: SlateBlue">5-1. 같은 GPU를 두 파드가 나눠 쓰는가</span></h3>
<p>gpu-01에 올라간 테스트 파드 두 개에서 각각 nvidia-smi를 쳐봤다.</p>
<pre><code class="language-bash">$ kubectl exec hami-test-1 -- nvidia-smi --query-gpu=name,uuid,memory.total --format=csv,noheader
NVIDIA GeForce RTX 4070 Ti, GPU-550ccdd7-4f0e-99a8-17f1-87ec56026e91, 6141 MiB

$ kubectl exec hami-test-2 -- nvidia-smi --query-gpu=name,uuid,memory.total --format=csv,noheader
NVIDIA GeForce RTX 4070 Ti, GPU-550ccdd7-4f0e-99a8-17f1-87ec56026e91, 6141 MiB</code></pre>
<p>이 출력 한 쌍이 HAMi의 동작을 통째로 보여준다.</p>
<ul>
<li><p><span style="color: indianred"><strong>UUID가 동일하다</strong></span></p>
<ul>
<li>두 파드는 진짜로 같은 물리 GPU를 공유하고 있다.</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>각자의 memory.total이 6141 MiB다</strong></span></p>
<ul>
<li><p>물리 VRAM은 12282MB인데, 컨테이너 안에서는 절반이 전체 용량으로 보인다. NVML 인터셉트가 실제로 동작하는 장면이다. </p>
</li>
<li><p>파드 입장에선 그냥 6GB짜리 GPU 1장이다.</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="span-stylecolor-slateblue5-2-분할-수를-넘으면-차단되는가span"><span style="color: SlateBlue">5-2. 분할 수를 넘으면 차단되는가</span></h3>
<p>같은 조건의 세 번째 파드는 어떻게 되는가.</p>
<pre><code>$ kubectl get pods -o wide | grep hami-test
hami-test-1   1/1   Running   0   40s   10.0.1.72    gpu-01
hami-test-2   1/1   Running   0   40s   10.0.1.250   gpu-01
hami-test-3   0/1   Pending   0   40s   &lt;none&gt;       &lt;none&gt;</code></pre><p>GPU 1장 = vGPU 2개이므로 두 개까지만 배치되고, 세 번째는 hami-scheduler가 잔여 VRAM 부족으로 Pending 상태가 된다. </p>
<p><strong>몇개의 세션까지 허용할지가 이제 스케줄러 레벨에서 강제된다.</strong></p>
<hr>
<h3 id="span-stylecolor-slateblue5-3-실전에서-학습이-도는가span"><span style="color: SlateBlue">5-3. 실전에서 학습이 도는가</span></h3>
<p>앞선 테스트 파드는 단순히 sleep만 수행하므로, 온전한 검증을 위해서는 실제 JupyterHub 세션 환경을 확인해 보아야 한다. </p>
<p>이에 실제 사용자 계정으로 로그인하여 4070 Ti 프로필로 세션을 스폰했다. 가장 먼저 세션 터미널에서 nvidia-smi를 실행해 보았다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/29bca355-c000-416d-82a3-757e0f99c4ab/image.png" alt=""></p>
<p>실제 물리 카드는 12GB의 VRAM을 가진 4070 Ti지만, 세션 내부에서는 전체 VRAM이 6141MiB로 인식된다. *<em>즉, 학생 입장에서는 처음부터 6GB 용량의 GPU를 단독으로 배정받은 것과 같은 환경이다. *</em></p>
<p>이 한도 안에서 학습이 정상적으로 도는지 확인하기 위해, 이 상태에서 PyTorch 기반의 CNN 모델 학습을 진행해 보았다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/6cf891e8-4a8e-4073-82e8-feb8e91f0fdc/image.png" alt=""></p>
<p>loss가 정상적으로 수렴한다. </p>
<hr>
<h3 id="span-stylecolor-slateblue5-4-한도를-초과하면-정말-막히는가span"><span style="color: SlateBlue">5-4. 한도를 초과하면 정말 막히는가</span></h3>
<p>가장 중요한 검증 단계는 할당된 VRAM 한도의 강제성이다. </p>
<p>기존 시스템의 PyTorch 제한은 한도를 초과해도 강제로 막지 않는 소프트 리밋이었으나, HAMi 환경에서는 실제 VRAM 초과 사용 시 시스템이 이를 어떻게 제어하는지 확인해 보았다.</p>
<p>같은 세션에서 6GB를 초과하는 대형 워크로드를 의도적으로 돌려보았다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/140a0c41-9f02-4cbe-a278-16bb5e73d1c1/image.png" alt=""></p>
<p><code>libvgpu.so</code>가 CUDA 할당 호출을 가로채서 리소스의 요청과 한도를 비교하고 할당을 거부한 것이다. PyTorch의 OutOfMemoryError는 거부를 전달받은 결과이다.</p>
<p>이때, 노트북 셸에서는 PyTorch의 OutOfMemoryError만 보이는데 이건 파이썬 레벨의 결과이며, 같은 코드를 터미널에서 돌려보면 그 앞단에서 실제로 할당을 거부한 쪽이 찍힌다.
<img src="https://velog.velcdn.com/images/taekyu_/post/bb986575-ed51-43a1-b87f-c6dbac6d51ba/image.png" alt=""></p>
<p><span style="color: indianred"><strong>이를 통해, 실제로 할당을 거부한 쪽은 HAMI-core쪽임을 확인할 수 있다.</strong></span></p>
<p>프레임워크 설정이 아니라 CUDA 레벨에서 막히므로, 학생이 무슨 프레임워크를쓰든, ipython을 타든 안 타든 이 한도는 우회되지 않는다. </p>
<p> <strong>그리고 한 명이 한도에 부딪혀도 같은 GPU의 옆 세션은 아무 영향이 없다.</strong> </p>
<hr>
<h3 id="span-stylecolor-slateblue5-5-모니터링은-유지되는가span"><span style="color: SlateBlue">5-5. 모니터링은 유지되는가</span></h3>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/40d66c51-3da7-4ec2-97a4-c60d87afc1d4/image.png" alt=""></p>
<p><strong>DCGM exporter는 HAMi와 무관하게 물리 GPU 단위로 메트릭을 계속 수집한다.</strong></p>
<p>HAMi가 아무리 vGPU로 쪼개서 보여줘도, 관리자 관점의 진실은 물리 GPU 5장이 각각 얼마나 바쁜가이고, 그건 DCGM이 그대로 보여준다.</p>
<p>이 구분이 운영에서 꽤 유용하다.</p>
<ul>
<li><p><span style="color: indianred"><strong>학생(컨테이너 안)</strong></span>: </p>
<ul>
<li><p>nvidia-smi를 치면 자기 몫(6141MiB)만 보인다</p>
</li>
<li><p>HAMi의 NVML 인터셉트</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>관리자(Grafana)</strong></span>: </p>
<ul>
<li><p>물리 GPU 전체의 사용률/VRAM/온도가 보인다</p>
</li>
<li><p>DCGM은 호스트 레벨에서 수집하므로 인터셉트의 영향을 받지 않는다</p>
</li>
</ul>
</li>
</ul>
<p>예를 들어 두 세션이 같은 GPU에서 각자 학습을 돌리면, 학생들 화면에는 각자의 사용량만 보이지만 Grafana에는 그 합이 물리 GPU 한 장의 사용률로 잡힌다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-gitops-반영span"><span style='background-color: MediumAquamarine'>6. GitOps 반영</span></h2>
<hr>

<p>클러스터에 직접 적용한 변경들은 아직 손으로 맞춘 상태이다. </p>
<p>해당 클러스터는 2개의 레포로 관리된다. JupyterHub 배포는 ArgoCD가 바라보는 helm values 레포, 클러스터구축은 Ansible 레포이며, 둘 다 현재 상태와 일치시켜야 재구축 시에도 같은 결과가 나온다.</p>
<h3 id="span-stylecolor-slateblue6-1-jupyterhub-helm-values-수정span"><span style="color: SlateBlue">6-1. JupyterHub helm values 수정</span></h3>
<p>프로필을 HAMi 리소스 기반으로 변경했다.</p>
<pre><code class="language-yaml"># jupyterhub/values.yaml — 변경 후 프로필 (핵심만)
- display_name: &quot;GPU Server (RTX 4070 Ti)&quot;
  slug: gpu-4070ti
  default: true
  kubespawner_override:
    extra_resource_guarantees:
      nvidia.com/gpu: 1
      nvidia.com/gpumem-percentage: 50
      nvidia.com/gpucores: 50
    extra_resource_limits:            # 확장 리소스는 limits에도 필수
      nvidia.com/gpu: 1
      nvidia.com/gpumem-percentage: 50
      nvidia.com/gpucores: 50
    node_selector:
      gpu-type: &quot;rtx4070ti&quot;           # 4090 프로필은 rtx4090</code></pre>
<p>기존 프로필에 있던 <code>GPU_MEMORY_LIMIT_MB</code> 환경변수, gpu-memory-script 볼륨/마운트, postStart 훅, livenessProbe 감시는 전부 삭제했다. 제한 스크립트를 배포하던 ConfigMap 템플릿도 함께 제거했다.</p>
<pre><code class="language-bash">$ git diff --stat
 jupyterhub/config.py                   |  12 --
 jupyterhub/scripts/gpu_memory_limit.sh | 202 -----------------------
 jupyterhub/templates/extra-config.yaml |  10 +-
 jupyterhub/values.yaml                 |  87 ++--------
 4 files changed, 12 insertions(+), 299 deletions(-)</code></pre>
<p>제한 스크립트와 주입 훅이 삭제되고 리소스 선언 몇 줄이 그 자리를 대신했다. 격리가 인프라 레이어로 내려가면서 애플리케이션 레이어에서 유지하던 장치들이 필요 없어진 결과다.</p>
<hr>
<h3 id="span-stylecolor-slateblue6-2-ansible-반영span"><span style="color: SlateBlue">6-2. Ansible 반영</span></h3>
<p>클러스터 구축 자동화 레포도 수정했다. 기존 코드는 Time Slicing 클러스터를 만들도록 되어 있어서, 이대로 재구축하면 전환 이전 상태로 돌아간다.</p>
<p><span style="color: indianred"><strong>1. GPU Operator 롤 수정</strong></span>
Time Slicing ConfigMap 생성 태스크를 삭제하고, 설치 옵션을 HAMi 공존 구성으로 변경했다.</p>
<pre><code class="language-yaml"># roles/11-nvidia-gpu-operator/tasks/main.yml (수정 후)
- name: Install NVIDIA GPU Operator (device-plugin/CDI disabled for HAMi)
  command: &gt;-
    helm upgrade --install gpu-operator nvidia/gpu-operator
    -n nvidia-gpu-operator
    --create-namespace
    --set driver.enabled=true
    --set toolkit.enabled=true
    --set devicePlugin.enabled=false
    --set cdi.enabled=false
    --set cdi.default=false
    --set dcgmExporter.enabled={{ enable_gpu_exporter | default(true) }}</code></pre>
<br>

<p><span style="color: indianred"><strong>2. HAMi 롤 신설</strong></span>
노드 라벨링부터 설치까지 roles에 추가하였다.</p>
<pre><code class="language-yaml"># roles/14-hami-vgpu/tasks/main.yml (요지)
- name: Label GPU nodes (gpu=on, gpu-type)
  # inventory의 gpu_workers_4070ti / gpu_workers_4090 그룹 기준으로
  # gpu=on + gpu-type=rtx4070ti|rtx4090 라벨 부여

- name: Install HAMi
  command: &gt;-
    helm upgrade --install hami hami-charts/hami -n kube-system
    --set scheduler.kubeScheduler.imageTag=v{{ k8s_version }}
    --set devicePlugin.deviceSplitCount={{ hami_device_split_count | default(2) }}
    --set devicePlugin.runtimeClassName=nvidia</code></pre>
<p><code>runtimeClassName=nvidia</code>도 포함했고, 분할 수는 변수로 뺐다.</p>
<pre><code class="language-yaml"># group_vars/gpu_cluster.yml
# gpu_time_slicing_replicas: 3   ← 삭제
hami_device_split_count: 2</code></pre>
<hr>
<h3 id="span-stylecolor-slateblue6-3-최종-아키텍처span"><span style="color: SlateBlue">6-3. 최종 아키텍처</span></h3>
<p>전환이 끝난 시점의 전체 그림이다. </p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/1358b21e-100a-4124-a779-3a973f676672/image.png" alt=""></p>
<ol>
<li><p>학생이 JupyterHub에 로그인해 프로필을 고른다. </p>
</li>
<li><p>Jupyterhub가 세션 파드를 생성하며, 프로필에 정의된 대로 파드에는 <code>nvidia.com/gpu: 1</code>, <code>gpumem-percentage: 50</code>, <code>gpucores: 50</code>이 붙는다. 
이때, userScheduler를 꺼뒀으므로 schedulerName은 비어 있는 상태다.</p>
</li>
<li><p><span style="color: indianred"><strong>파드 생성 요청이 API 서버에 도착하면 hami-webhook이 가로챈다. 
GPU 리소스가 있는 걸 확인하고 schedulerName을 hami-scheduler로 바꾼다.</strong></span></p>
</li>
<li><p>hami-scheduler가 노드 어노테이션(<code>hami.io/node-nvidia-register</code>)에 등록된 물리 GPU별 잔여 VRAM/코어를 계산해서, 여유가 있는 카드를 골라 파드를 바인딩한다. 모든 카드가 차 있으면 여기서 Pending으로 막힌다.</p>
</li>
<li><p>배정된 노드의 kubelet이 컨테이너를 만든다. nvidia RuntimeClass를 통해 container toolkit이 컨테이너형 드라이버(<code>/run/nvidia/driver</code>)의 라이브러리를 주입하고, <span style="color: indianred"><strong>hami-device-plugin이 libvgpu.so를 마운트하면서 VRAM/연산 한도를 환경변수로 넣는다.</strong></span></p>
</li>
<li><p>세션이 뜨게 되면서, 컨테이너 안에서 nvidia-smi를 치면 NVML 인터셉트 때문에 자기 몫이 전체 용량으로 보인다. <span style="color: indianred"><strong>이후 학생이 돌리는 모든 CUDA 호출은 libvgpu.so를 거친다.</strong></span></p>
</li>
<li><p>학습 중 메모리 할당이 한도를 넘으면 HAMI-core가 CUDA 레벨에서 할당을 거부한다. 이때, 같은 GPU의 옆 세션은 영향받지 않으며, 연산도 마찬가지로 사용률이 50%를 넘으면 커널 런치가 조절된다.</p>
</li>
<li><p>이 모든 과정과 무관하게 DCGM exporter는 물리 GPU 단위 메트릭을 계속 수집하고, 관리자는 Grafana에서 카드 5장의 전체 사용률을 본다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine7-마치며span"><span style='background-color: MediumAquamarine'>7. 마치며</span></h2>
<hr>

<blockquote>
<p>Time Slicing + 소프트웨어 VRAM 제한 주입으로 1년 넘게 버텨온 GPU 공유 체계를 HAMi로 전환하였다. <br>
격리가 컨테이너 안의 소프트웨어 리밋에서 CUDA 레벨의 하드 리밋으로 바뀌었고, 학생 세션은 이제 처음부터 6GB짜리 GPU 1장을 배정받게 되었으며, 한 명이 한도를 넘어도 옆 세션은 영향받지 않게 되었다.</p>
</blockquote>
<p>이번 작업을 진행하며 배운점은 2가지가 있다.</p>
<ul>
<li><p><span style="color: indianred"><strong>제한은 거는 위치가 중요하다.</strong></span></p>
<p>기존 방식은 몽키패치와 livenessProbe 등을 통해 우회를 막고자 했으나, 애플리케이션 레벨에서의 제한은 본질적인 한계가 있었다. </p>
<p>이를 인프라 레이어의 제어로 전환하자 수백 줄에 달하던 제어 스크립트가 단 몇 줄의 리소스 선언으로 간소화되었다. </p>
<p>제한 로직을 어느 레이어에 구현하느냐가 아키텍처의 복잡도와 안정성을 크게 좌우한다는 것을 깨닫게 되었다.</p>
</li>
<li><p><span style="color: indianred"><strong>문제는 도구가 아니라 접점에서 터진다.</strong></span></p>
<p>발생한 4건의 트러블슈팅 중 HAMi 자체의 결함은 없었다. </p>
<p>컨테이너형 Driver, GPU Operator의 CDI 모드, 실제 VRAM 오차, z2jh의 user-scheduler 등 모두 기존 환경과 새로운 도구가 맞물리는 시스템 접점에서 발생한 호환성 이슈였다. </p>
<p>결국 공식 문서의 기본 가이드보다, 현재 운용 중인 클러스터 환경의 특수성을 정확히 파악하는 것이 도입의 핵심이라는 것을 깨닫게 되었다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[죽은 GPU 노드 살리기: nouveau 드라이버 충돌 트러블슈팅]]></title>
            <link>https://velog.io/@taekyu_/%EC%A3%BD%EC%9D%80-GPU-%EB%85%B8%EB%93%9C-%EC%82%B4%EB%A6%AC%EA%B8%B0-nouveau-%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B2%84-%EC%B6%A9%EB%8F%8C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-gwe714yd</link>
            <guid>https://velog.io/@taekyu_/%EC%A3%BD%EC%9D%80-GPU-%EB%85%B8%EB%93%9C-%EC%82%B4%EB%A6%AC%EA%B8%B0-nouveau-%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B2%84-%EC%B6%A9%EB%8F%8C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-gwe714yd</guid>
            <pubDate>Wed, 19 Aug 2026 14:09:40 GMT</pubDate>
            <description><![CDATA[<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-무슨-일이-있었는가-span"><span style='background-color: MediumAquamarine'>1. 무슨 일이 있었는가 </span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-한-달간의-서버실-공사-그리고-재가동span"><span style="color: SlateBlue">1-1. 한 달간의 서버실 공사, 그리고 재가동</span></h3>
<p>서버실 공사와 전력 공사가 겹치면서 학과 GPU K8s 클러스터가 약 한 달간 내려가 있었다.</p>
<p>공사가 끝나고 서버를 다시 올렸는데, 노드 하나가 돌아오지 않았다.</p>
<pre><code class="language-bash">$ kubectl get nodes -o wide
NAME     STATUS                        ROLES          
gpu-01   Ready                         worker          
gpu-02   Ready                         worker          
gpu-03   NotReady                      worker          
gpu-04   Ready                         worker          
gpu-05   NotReady,SchedulingDisabled   worker          
gpu-cp   Ready                         control-plane   </code></pre>
<p>*<em>gpu-03이 NotReady다. *</em></p>
<p>(gpu-05도 NotReady지만 이건 2월부터 꺼져 있던 별개 건으로, 이날 OS를 새로 설치해 재편입했다)</p>
<p>노드 상태를 까보니 더 이상한 점이 있었다.</p>
<pre><code class="language-bash">$ kubectl describe node gpu-03
Conditions:
  Type                 Status    LastHeartbeatTime                 Reason              Message
  ----                 ------    -----------------                 ------              -------
  NetworkUnavailable   False     Tue, 03 Feb 2026 15:08:52 +0900   CiliumIsUp          Cilium is running on this node
  MemoryPressure       Unknown   Fri, 05 Jun 2026 19:26:45 +0900   NodeStatusUnknown   Kubelet stopped posting node status.
  DiskPressure         Unknown   Fri, 05 Jun 2026 19:26:45 +0900   NodeStatusUnknown   Kubelet stopped posting node status.
  PIDPressure          Unknown   Fri, 05 Jun 2026 19:26:45 +0900   NodeStatusUnknown   Kubelet stopped posting node status.
  Ready                Unknown   Fri, 05 Jun 2026 19:26:45 +0900   NodeStatusUnknown   Kubelet stopped posting node status.</code></pre>
<p>마지막 HeartBeat가 6월 5일</p>
<p>공사는 7월에 시작했는데, 이 노드는 공사 전부터 이미 죽어 있었다는 얘기다. 클러스터가 통째로 꺼져 있던 기간에 묻혀서 몰랐을 뿐, 실제로는 약 75일째 NotReady였다.</p>
<p>이 노드 위에 있던 파드 수십 개는 Terminating/Unknown 상태의 유령으로 남아 있었고, 이 노드의 디스크를 쓰는 Rook-Ceph OSD 두 개도 40일째 Pending이었다.</p>
<hr>
<h3 id="span-stylecolor-slateblue1-2-켜보니-죽어있는-gpu-노드span"><span style="color: SlateBlue">1-2. 켜보니 죽어있는 GPU 노드</span></h3>
<p>원격으로 할 수 있는 게 없었다. ping도 안 가고 ssh도 안 됐다. 결국 서버실에 직접 갔다.</p>
<p>서버실에서 확인해보니 전원은 들어와 있고 팬도 돈다. </p>
<p>즉, 노드 자체가 죽은 건 아니다. 모니터를 연결하고 재부팅해보니 증상이 특이했다.</p>
<ul>
<li>부팅 화면이 1초 정도 보였다가 블랙아웃. 화면은 안 나오는데 머신은 계속 돌아간다.</li>
<li>콘솔 로그인은 가능한데, NetworkManager를 보니 유선 연결이 Wired off 상태</li>
</ul>
<p>ping이 안 갔던 이유는 케이블도 꽂혀 있고 머신도 살아 있는데, 단순히 네트워크 연결 자체가 비활성 상태였던 것이다.</p>
<p>&quot;뭐야, 그럼 연결만 켜면 되는 거 아닌가?&quot; 하고 가볍게 연결을 올렸다.</p>
<hr>
<h3 id="span-stylecolor-slateblue1-3-현장에서-발견한-증상-네트워크를-열면-무한-재부팅span"><span style="color: SlateBlue">1-3. 현장에서 발견한 증상: 네트워크를 열면 무한 재부팅</span></h3>
<p>연결을 켜자 ssh도 들어와졌다. </p>
<p>&quot;이게 왜 죽어있었지?&quot; 싶던 그 순간 머신이 통째로 재부팅됐다. 네트워크 연결 후 정확히 10~20초 뒤였다.</p>
<p>그리고 부팅 후엔 다시 Wired off로 돌아와 있다. 다시 연결하면 또 10~20초 뒤 재부팅이 되는 현상이 지속되었다.</p>
<p>이때의 재부팅 기록이 그대로 남아 있다.</p>
<pre><code class="language-bash">$ last -x reboot | head -5
reboot   system boot  6.8.0-124-generi Wed Aug 19 13:42   still running
reboot   system boot  6.8.0-124-generi Wed Aug 19 13:41   still running
reboot   system boot  6.8.0-124-generi Wed Aug 19 13:27   still running
reboot   system boot  6.8.0-124-generi Wed Aug 19 13:26   still running
reboot   system boot  6.8.0-124-generi Wed Aug 19 13:17   still running</code></pre>
<p>13:26에 부팅한 머신이 13:27에 또 부팅되어 있다. 네트워크 연결 실험을 할 때마다 1~5분 간격으로 재부팅이 쌓였다. 이 노드는 죽어 있던 게 아니라, <span style="color: indianred"><strong>네트워크가 붙는 순간마다 크래시하는 무한 재부팅 루프에 갇혀 있었던 것이다.</strong></span>
<br></p>
<p>더 이상한 건 로그였다. 부팅 이력을 보면,</p>
<pre><code class="language-bash">$ journalctl --list-boots | tail -3
-2 64323b58... Wed 2026-08-19 13:37:12 KST—Wed 2026-08-19 13:37:12 KST
-1 6d3405a3... Wed 2026-08-19 13:42:11 KST—Wed 2026-08-19 13:42:11 KST
 0 831f0b6b... Wed 2026-08-19 13:47:09 KST—Wed 2026-08-19 13:47:40 KST</code></pre>
<p>크래시한 부팅들은 시작 시각과 마지막 로그 시각이 같다. 부팅 직후 한 번 flush된 뒤로 단 한 줄도 디스크에 기록되지 못하고 죽었다는 뜻이다. </p>
<p>커널 패닉 메시지도, 정상 종료 기록도 없다. 패닉 덤프가 남는 pstore도 확인했다.</p>
<pre><code class="language-bash">$ ls -la /sys/fs/pstore/
# (비어 있음)

$ journalctl -b -1 -k | grep -iE &quot;mce|panic|watchdog|thermal|nvrm|oops|hardware error&quot;
# (출력 없음)</code></pre>
<p>아무것도 없다. </p>
<p>로그가 없다는 건 보통 문제가 없었다는 뜻이지만, 이 경우는 커널이 로그 버퍼를 디스크에 flush할 틈조차 없이 즉사했다는 뜻이다.</p>
<pre><code class="language-bash">$ cat /proc/sys/kernel/panic
10</code></pre>
<p>그리고 이 머신에는 이런 해당 설정이 존재하였는데, 커널 패닉이 나면 10초 뒤 자동 재부팅을 한다는 의미이다. 
<br></p>
<p>정리하면 이렇다.</p>
<blockquote>
<p><strong>1. 네트워크만 안 켜면 멀쩡하다
2. 네트워크를 켜면 15~20초 뒤 죽는다
3. 죽는 순간의 로그는 어디에도 없다</strong></p>
</blockquote>
<p>네트워크가 원인처럼 보이지만, 네트워크 자체가 커널을 죽일 이유는 없다. </p>
<p>NIC 드라이버 문제였다면 링크가 붙는 순간 죽어야지 20초를 기다릴 이유가 없다.</p>
<p>이 20초는 뭔가가 준비되는 시간인것 같다는 생각이 들었고, 범인은 네트워크가 아니라, 네트워크가 연결된 뒤에 시작되는 무언가였다고 생각하였다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-nouveau와-nvidia-gpu-operator-span"><span style='background-color: MediumAquamarine'>2. nouveau와 NVIDIA GPU Operator </span></h2>
<hr>


<p>원인을 파악하기 위해서는 클러스터의 GPU 드라이버 관리 방식과 nouveau의 개념에 대한 이해가 필요하다.</p>
<h3 id="span-stylecolor-slateblue2-1-nvidia-gpu-operator란span"><span style="color: SlateBlue">2-1. NVIDIA GPU Operator란?</span></h3>
<p><strong>GPU Operator는 쿠버네티스에서 GPU를 쓰기 위해 필요한 컴포넌트들인 Driver, Container Toolkit, Device Plugin, Monitoring 등을 오퍼레이터 패턴으로 한 번에 배포와 관리해주는 NVIDIA의 공식 프로젝트다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/679f58a9-da3d-4f2e-94cb-77a81b1880e0/image.png" alt=""></p>
<p>여기서 중요한 건 <strong>컨테이너형 드라이버 (driver daemonset)</strong> 방식이다. 해당 클러스터는 호스트에 NVIDIA 드라이버를 직접 설치하지 않는다. </p>
<p>대신 <code>nvidia-driver-daemonset</code> 파드가 각 GPU 노드에 떠서, 부팅할 때마다 컨테이너 안에서 커널 모듈을 로드한다.
<img src="https://velog.velcdn.com/images/taekyu_/post/b3da1ec6-3020-4134-91fd-f2f7bf4f631d/image.png" alt=""></p>
<p>드라이버 파일은 호스트가 아니라 <code>/run/nvidia/driver</code> 아래에만 존재한다.</p>
<pre><code class="language-bash">$ kubectl get clusterpolicy -o jsonpath=&#39;{.items[0].spec.driver.version}&#39;
580.105.08

$ kubectl -n nvidia-gpu-operator get ds nvidia-driver-daemonset \
    -o jsonpath=&#39;{.spec.template.spec.containers[0].image}&#39;
nvcr.io/nvidia/driver:580.105.08-ubuntu22.04</code></pre>
<p>해당 방식은 노드 5대의 드라이버 버전을 apt로 하나하나 맞추는 게 아니라, ClusterPolicy 하나로 전 노드를 일괄 관리할 수 있다는 장점이 존재한다.</p>
<p>그런데 이 구조에는 전제가 하나 깔려 있다. </p>
<p><span style="color: indianred"><strong>드라이버 파드가 커널 모듈을 로드하는 시점에, GPU가 비어 있어야 한다.</strong></span></p>
<hr>
<h3 id="span-stylecolor-slateblue2-2-nouveau란span"><span style="color: SlateBlue">2-2. nouveau란?</span></h3>
<p><strong>nouveau는 리눅스 커뮤니티가 리버스 엔지니어링으로 만든 오픈소스 NVIDIA 그래픽 드라이버다.</strong> </p>
<p>NVIDIA가 드라이버 소스를 공개하지 않기 때문에 커뮤니티가 만든 대체품으로, 우분투를 포함한 대부분의 배포판에 기본 내장되어 있다.</p>
<ul>
<li>NVIDIA GPU가 장착된 채로 OS를 설치하면, 공식 드라이버가 없어도 화면은 나와야 하므로, 커널이 부팅 시 nouveau를 자동으로 로드한다</li>
<li>화면 출력용으로는 동작하지만 CUDA는 지원하지 않고, 성능도 공식 드라이버 대비 크게 떨어진다. GPU 연산 서버 입장에서는 사실상 방해물이다.</li>
</ul>
<p>솔직히 이번 일을 겪기 전까지, 리눅스를 설치하면 nouveau가 기본으로 들어간다는 사실 자체를 몰랐다. 공식 드라이버를 깔면 알아서 대체되는 줄 알았다.</p>
<hr>
<h3 id="span-stylecolor-slateblue2-3-왜-하나의-gpu에-두-드라이버가-공존할-수-없는가span"><span style="color: SlateBlue">2-3. 왜 하나의 GPU에 두 드라이버가 공존할 수 없는가</span></h3>
<p>*<em>하나의 PCI 디바이스는 하나의 커널 드라이버만 바인딩할 수 있다. *</em></p>
<p><span style="color: indianred"><strong>nouveau가 부팅 때 먼저 GPU를 점유하게 되면, 나중에 공식 nvidia 모듈이 로드되려면 nouveau를 먼저 언로드해야 한다.</strong></span></p>
<p>관건은 nouveau의 점유 해제 가능 여부다. </p>
<p>유휴 상태일 경우 정상적으로 언로드되지만, GNOME 같은 데스크톱이 nouveau 위에서 화면을 그리고 있으면, 사용 중인 드라이버를 강제로 뽑아내는 셈이 된다.</p>
<p><span style="color: indianred"><strong>화면 출력이 실시간으로 돌아가는 드라이버를 커널에서 제거하는 순간, 커널은 로그도 못 남기고 죽을 수 있다.</strong></span></p>
<p>그래서 NVIDIA 드라이버를 쓰는 시스템은 관례적으로 nouveau를 블랙리스트에 등록해서 부팅 단계부터 로드 자체를 막는다.</p>
<pre><code class="language-bash"># /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
install nouveau /bin/false</code></pre>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-원인-추적기-span"><span style='background-color: MediumAquamarine'>3. 원인 추적기 </span></h2>
<hr>

<h3 id="span-stylecolor-slateblue3-1-격리-테스트-kubelet과-containerd를-끄니-재부팅이-멈췄다span"><span style="color: SlateBlue">3-1. 격리 테스트: kubelet과 containerd를 끄니 재부팅이 멈췄다</span></h3>
<p>네트워크 연결 후에 시작되는 무언가를 찾기 위해 격리 테스트를 했다. </p>
<p>쿠버네티스 관련 데몬(kubelet, containerd)을 전부 내리고 네트워크를 연결해봤다.</p>
<pre><code class="language-bash">sudo systemctl stop kubelet containerd
# 이후 유선 연결 ON</code></pre>
<p>그렇게 해보니, 재부팅이 안 되면서 해당 상태로는 ssh로 접근이 가능하였다.</p>
<pre><code class="language-bash">$ ssh gpu-03 &#39;echo CONNECTED: $(hostname); uptime&#39;
CONNECTED: gpu-03
 13:47:09 up 4 min,  1 user,  load average: 0.24, 0.14, 0.06</code></pre>
<p>이는 네트워크 연결 직후 kubelet이 API 서버에 노드를 등록하면서 실행되는 파드 중 하나가 크래시를 유발함을 의미한다.</p>
<p>그리고, GPU 노드에서 커널 모듈을 직접 제어하여 시스템 레벨의 충돌을 일으킬 수 있는 파드는 <strong><code>nvidia-driver-daemonset</code></strong>뿐이다.</p>
<h3 id="span-stylecolor-slateblue-3-2-정상-노드와의-비교-gpu-03만-nouveau가-gpu를-점유하고-있었다span"><span style="color: SlateBlue"> 3-2. 정상 노드와의 비교: gpu-03만 nouveau가 GPU를 점유하고 있었다</span></h3>
<p>가설을 확인하기 위해 gpu-03의 드라이버 상태를 동일한 GPU를 사용하는 정상 노드와 비교했다.</p>
<pre><code class="language-bash"># --- gpu-03 (문제 노드) ---
$ lsmod | grep -E &quot;^nouveau|^nvidia&quot;; lspci -nnk -d 10de: | grep -E &quot;VGA|Kernel driver&quot;
nouveau              3100672  4
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2782] (rev a1)
      Kernel driver in use: nouveau

# --- gpu-04 (정상 노드) ---
$ lsmod | grep -E &quot;^nouveau|^nvidia&quot;; lspci -nnk -d 10de: | grep -E &quot;VGA|Kernel driver&quot;
nvidia_modeset       1814528  1
nvidia_uvm           2158592  8
nvidia              14385152  26 nvidia_uvm,nvidia_modeset
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2782] (rev a1)
      Kernel driver in use: nvidia</code></pre>
<p>같은 하드웨어인데 잡고 있는 드라이버가 다르다. </p>
<p><strong>gpu-03은 nouveau가 GPU를 잡고있다.</strong> 이는 GNOME 데스크톱이 nouveau 위에서 화면을 그리고 있다는 뜻이다. </p>
<p>부팅 화면이 1초 보이고 블랙아웃되던 증상도 nouveau의 커널모드 설정 핸드오버 문제로 같은 뿌리다.</p>
<p>왜 이런 차이가 나는지 <code>/etc/modprobe.d/</code>를 비교해보면:</p>
<pre><code class="language-bash">#--- gpu-03 ---
$ ls /etc/modprobe.d/ | grep -i -E &quot;nouveau|blacklist&quot;
amd64-microcode-blacklist.conf
blacklist-ath_pci.conf
blacklist.conf
blacklist-firewire.conf
blacklist-framebuffer.conf
blacklist-modem.conf
blacklist-oss.conf
blacklist-rare-network.conf
intel-microcode-blacklist.conf

#--- 정상 노드 ---
$ ls /etc/modprobe.d/ | grep -i -E &quot;nouveau|blacklist&quot;
amd64-microcode-blacklist.conf
blacklist-ath_pci.conf
blacklist.conf
blacklist-firewire.conf
blacklist-framebuffer.conf
blacklist-modem.conf
blacklist-nouveau.conf          #← 정상 노드들은 해당 blacklist가 존재
blacklist-oss.conf
blacklist-rare-network.conf
intel-microcode-blacklist.conf

$ cat /etc/modprobe.d/blacklist-nouveau.conf   # 정상 노드
blacklist nouveau
install nouveau /bin/false</code></pre>
<p>정상 노드들은 nouveau가 블랙리스트로 차단되어 있어 nvidia 드라이버가 GPU를 온전히 쓰고 있었고, gpu-03에는 해당 파일이 없었다.
<br></p>
<p>덤으로, 재부팅 후 네트워크가 매번 꺼져 있던 이유도 이때 확인됐다.</p>
<pre><code class="language-bash">$ nmcli -t -f NAME,DEVICE,AUTOCONNECT con show
Profile 1:enp7s0:no</code></pre>
<p>autoconnect값이 no이므로, 부팅해도 스스로 네트워크에 붙지 않았다.</p>
<hr>
<h3 id="span-stylecolor-slateblue-3-3-재구성한-크래시-시나리오span"><span style="color: SlateBlue"> 3-3. 재구성한 크래시 시나리오</span></h3>
<pre><code class="language-bash">부팅
 → nouveau가 GPU 선점, GNOME이 그 위에서 렌더링 (부팅 화면 1초 후 블랙아웃)
 → 유선 연결 ON
 → kubelet이 API 서버에 노드 등록
 → GPU Operator의 nvidia-driver-daemonset 파드 스케줄
 → 드라이버 컨테이너가 nvidia 모듈 로드 전에 nouveau 언로드 시도
 → 화면 출력 중인 드라이버를 강제 제거 → 커널 즉사 (로그 flush할 틈도 없음)
 → kernel.panic=10 설정으로 10초 후 자동 재부팅
 → NetworkManager autoconnect=no라 부팅 후 다시 &quot;Wired off&quot;
 → (수동으로 연결하면 처음부터 반복)</code></pre>
<p>네트워크 연결 후 10~20초는 kubelet 등록부터 드라이버 파드가 뜨기까지의 시간이었고, 로그가 없는 이유는 커널이 flush할 틈도 없이 리셋됐기 때문이었다.</p>
<p>추가적으로, 재부팅 후 Wired off는 autoconnect 설정 문제였다. </p>
<hr>
<h3 id="span-stylecolor-slateblue-3-4-왜-1년간-멀쩡했는가span"><span style="color: SlateBlue"> 3-4. 왜 1년간 멀쩡했는가?</span></h3>
<p>그렇다면 여기서 의문이 하나 남았다. 이 클러스터는 1년 넘게 GPU Operator로 JupyterHub를 서비스해왔다. 이런 시한폭탄이 있었는데 왜 이제야 터졌는지가 의문이었다.</p>
<p>답은 두 가지다.</p>
<ul>
<li><p><strong>첫째, 이 충돌은 부팅 시점에만 발생한다.</strong> 
서버는 재부팅할 일이 거의 없다. 한번 nvidia 드라이버가 GPU를 잡은 뒤에는 nouveau가 끼어들 틈이 없으니, 가동 중에는 아무 문제가 없다.</p>
</li>
<li><p><strong>둘째, headless 상태에서는 nouveau가 로드돼 있어도 조용히 언로드된다.</strong> 
모니터 없이 부팅하면 아무도 nouveau를 사용하지 않으므로, 드라이버 파드가 큰 충돌 없이 nouveau를 내리고 자기가 GPU를 잡을 수 있다. 즉 그동안은 구조적으로 안전했던 게 아니라 운이 좋았던 것이다.</p>
</li>
</ul>
<p>그러다 gpu-03에서 문제가 재현된 것이다. 설치된 커널 목록을 보면</p>
<pre><code class="language-bash">$ ls -l --time-style=long-iso /boot/vmlinuz-*
2026-01-28 /boot/vmlinuz-6.8.0-40-generic
2026-05-26 /boot/vmlinuz-6.8.0-124-generic

$ zgrep -hA1 &quot;Start-Date: 2026-06&quot; /var/log/apt/history.log.*.gz | head -4
Start-Date: 2026-06-03  06:35:30
Commandline: /usr/bin/unattended-upgrade
--
Start-Date: 2026-06-05  06:58:57
Commandline: /usr/bin/unattended-upgrade</code></pre>
<p>5월 26일에 unattended-upgrade가 새 커널(6.8.0-124)을 설치했고, 6월 5일에 재부팅되면서 새 커널로 올라왔다. </p>
<p>이때부터 nouveau이 GPU를 놓지 않는 상태가 되었고, 드라이버 파드의 언로드 시도가 커널 크래시로 이어지는 루프에 진입했다.</p>
<p>사람이 아무 짓도 안 했는데 노드가 죽은 이유가 이거였다. </p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-해결-방법-span"><span style='background-color: MediumAquamarine'>4. 해결 방법 </span></h2>
<hr>

<h3 id="span-stylecolor-slateblue4-1-nouveau-블랙리스트-등록--initramfs-재생성span"><span style="color: SlateBlue">4-1. nouveau 블랙리스트 등록 + initramfs 재생성</span></h3>
<p>gpu-03에서</p>
<pre><code class="language-bash">$ sudo nmcli con mod &quot;Profile 1&quot; connection.autoconnect yes &amp;&amp; echo &quot;1. autoconnect ON&quot;
1. autoconnect ON

$ printf &quot;blacklist nouveau\ninstall nouveau /bin/false\n&quot; | \
    sudo tee /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
install nouveau /bin/false

$ sudo update-initramfs -u
update-initramfs: Generating /boot/initrd.img-6.8.0-124-generic

$ sudo reboot</code></pre>
<p>블랙리스트 파일만 만들면 끝이 아니고 <code>update-initramfs -u</code>까지 해야 한다.</p>
<p>nouveau는 부팅 극초반 initramfs 단계에서 로드될 수 있어서, initramfs 안에 블랙리스트가 반영되어 있지 않으면 소용이 없다.</p>
<hr>
<h3 id="span-stylecolor-slateblue4-2-복구-확인span"><span style="color: SlateBlue">4-2. 복구 확인</span></h3>
<p>재부팅하고 10초 만에 네트워크가 스스로 돌아왔다. (autoconnect 효과)</p>
<pre><code class="language-bash">$ uptime -p; lsmod | grep nouveau &amp;&amp; echo &quot;0&quot; || echo &quot;1&quot;
up 1 minute
1</code></pre>
<p>nouveau가 사라졌다. 이제 드라이버 파드 배포를 다시 허용하고 지켜봤다.</p>
<pre><code class="language-bash">$ kubectl label node gpu-03 nvidia.com/gpu.deploy.driver=true --overwrite
node/gpu-03 labeled

$ kubectl get pods -n nvidia-gpu-operator -o wide --field-selector spec.nodeName=gpu-03
NAME                                               READY   STATUS     RESTARTS   AGE
gpu-feature-discovery-lrsvq                        0/2     Init:0/2   0          17s
nvidia-container-toolkit-daemonset-ddjkj           0/1     Init:0/1   0          17s
nvidia-dcgm-exporter-k82x8                         0/1     Init:0/1   0          17s
nvidia-device-plugin-daemonset-d2sq4               0/2     Init:0/2   0          17s
nvidia-driver-daemonset-c2794                      0/1     Running    0          20s
nvidia-operator-validator-wcqfc                    0/1     Init:0/4   0          17s</code></pre>
<p>예전 같으면 <code>nvidia-driver-daemonset</code>이 뜨는 순간 커널이 죽었어야 한다.</p>
<p>이번엔 죽지 않았다.</p>
<pre><code class="language-bash">$ kubectl -n nvidia-gpu-operator get pods --field-selector spec.nodeName=gpu-03 --no-headers
gpu-feature-discovery-lrsvq                        2/2   Running     0    2m6s
nvidia-container-toolkit-daemonset-ddjkj           1/1   Running     0    2m6s
nvidia-cuda-validator-kq28j                        0/1   Completed   0    35s
nvidia-dcgm-exporter-k82x8                         1/1   Running     0    2m6s
nvidia-device-plugin-daemonset-d2sq4               2/2   Running     0    2m6s
nvidia-driver-daemonset-c2794                      1/1   Running     0    2m9s
nvidia-operator-validator-wcqfc                    1/1   Running     0    2m6s

$ kubectl get node gpu-03
gpu-03   Ready   worker   196d   v1.35.0</code></pre>
<p>GPU 스택 전체가 Running이고 노드도 Ready 인것을 확인했다.</p>
<p>이후에, 드라이버가 실제로 GPU를 잡았는지도 확인하였다.</p>
<pre><code class="language-bash">$ kubectl -n nvidia-gpu-operator exec nvidia-driver-daemonset-c2794 -- \
    nvidia-smi --query-gpu=name,driver_version,temperature.gpu,power.draw --format=csv
name, driver_version, temperature.gpu, power.draw [W]
NVIDIA GeForce RTX 4070 Ti, 580.105.08, 36, 11.10 W</code></pre>
<p>이때 실시간으로 스트리밍해두고 있던 dmesg에서 로그를 확인해보니, nvidia 모듈이 올라오는 것을 확인할 수 있었다.</p>
<pre><code class="language-bash">[수  8월 19 14:10:03 2026] nvidia: loading out-of-tree module taints kernel.
[수  8월 19 14:10:03 2026] NVRM: loading NVIDIA UNIX Open Kernel Module for x86_64  580.105.08  Release Build
[수  8월 19 14:10:03 2026] nvidia-modeset: Loading NVIDIA UNIX Open Kernel Mode Setting Driver for x86_64  580.105.08</code></pre>
<p>이렇게 문제가 있는 노드는 네트워크를 연결해도 재부팅이 없고, 드라이버가 정상 로드되며 GPU allocatable이 복원되었다. </p>
<blockquote>
<p><strong>디버깅 팁</strong>: 이번처럼 크래시가 로그를 못 남기는 경우, 원격 머신에서
<code>ssh &lt;node&gt; &#39;sudo dmesg -wT&#39; &gt; crash.log</code>로 커널 로그를 실시간 스트리밍해두면
디스크에 기록되지 못한 마지막 메시지를 SSH 스트림으로 건질 수 있다.
디스크는 못 살려도 이미 전송된 패킷은 남는다.</p>
</blockquote>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-재발-방지-span"><span style='background-color: MediumAquamarine'>5. 재발 방지 </span></h2>
<hr>

<h3 id="span-stylecolor-slateblue5-1-ansible-롤로-신규-노드-자동-적용span"><span style="color: SlateBlue">5-1. Ansible 롤로 신규 노드 자동 적용</span></h3>
<p>구축할 때 손으로 해준 설정은 새 노드가 들어올 때 반드시 누락된다. </p>
<p>그래서 기존에 학과 클러스터를 관리하기 위해 구축해 놓은 클러스터 구축 자동화 레포(Using Ansible)에 GPU 노드 사전 준비 롤을 추가했다.</p>
<pre><code class="language-yaml"># roles/00-gpu-node-prep/tasks/main.yml 
- name: Blacklist nouveau
  copy:
    dest: /etc/modprobe.d/blacklist-nouveau.conf
    content: |
      blacklist nouveau
      install nouveau /bin/false
  register: blacklist_result

- name: Rebuild initramfs
  command: update-initramfs -u
  when: blacklist_result.changed

- name: Warn if nouveau is currently loaded
  ...</code></pre>
<p>블랙리스트 배포와 initramfs 재생성을 워커 조인 롤보다 앞 단계에 배치해서, 플레이북으로 노드를 추가하는 한 이 문제는 구조적으로 재발할 수 없게 했다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-마치며-span"><span style='background-color: MediumAquamarine'>6. 마치며 </span></h2>
<hr>

<blockquote>
<p><strong>GPU Operator의 컨테이너형 드라이버는 부팅 시 GPU가 비어 있다는 전제 위에서 동작하는데, 리눅스에 기본 내장된 nouveau가 그 전제를 깨뜨릴 수 있고, 그 결과가 로그 한 줄 없는 커널 크래시 무한 루프였다.</strong></p>
</blockquote>
<p>개인적으로 이번 트러블슈팅에서 얻은 게 두 가지다.</p>
<ul>
<li><p><span style="color: indianred"><strong>잘 돌아가는 시스템과 안전한 시스템이 다르다.</strong></span></p>
<ul>
<li><p>해당 클러스터는 1년간 멀쩡히 서비스되었지만, 그건 재부팅을 안 했기 때문이지 구성이 완전했기 때문이 아니었다. </p>
</li>
<li><p>평소에 안 터지는 문제일수록 부팅이나 재시작 같은 드문 이벤트에 숨어 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>로그가 없는 것도 단서다.</strong></span></p>
<ul>
<li>부팅 기록의 시작과 끝이 같은 시각이라는것, pstore가 비어 있다는 것 자체가 &quot;커널이 flush할 틈도 없이 죽었다&quot;는 정보였고, 그게 하드웨어 문제가 아닌 커널 레벨 충돌 쪽으로 방향을 잡게 해줬다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps을 정리하며]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%98%EB%A9%B0-d6gq3r8j</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%98%EB%A9%B0-d6gq3r8j</guid>
            <pubDate>Fri, 20 Feb 2026 14:37:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-90daysofdevops-스터디를-마무리하며span"><span style='background-color: MediumAquamarine'>1. 90DaysOfDevOps 스터디를 마무리하며</span></h2>
<hr>

<p>90DaysOfDevOps 스터디의 모든 강의 수강과 정리를 마쳤다. </p>
<p>방대한 DevOps 생태계와 클라우드 네이티브 기술들을 폭넓게 훑어볼 수 있는 시간이었다. 스터디를 리딩하며 함께 기술을 탐구하고, 실제 쿠버네티스 클러스터와 인프라를 운영하며 마주했던 여러 고민에 대한 해답을 엿볼 수 있었다.</p>
<p><strong>마지막 챕터인 만큼, 전체 강의 중에서도 내용이 특히 훌륭했거나 기술적으로 흥미로웠던 주제, 그리고 DevOps 엔지니어링 관점에서 앞으로 더 깊게 파고들어 공부해 볼 가치가 있는 강의들을 아래와 같이 추려보았다.</strong>
<br></p>
<p>📌 <span style="color: indianred"><strong>인상 깊었던 핵심 강의 리스트</strong></span></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Day 6.</strong></span> Kubernetes RBAC with Ansible: 쿠버네티스 RBAC 정책과 Ansible</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 8.</strong></span> Culinary Coding: Crafting Infrastructure Recipes with OpenTofu: Terraform과 OpenTofu</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 9.</strong></span> Why should developers care about container security?: 컨테이너 보안</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 10.</strong></span> Is Kubernetes Too Complicated?: 쿠버네티스란?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 14.</strong></span> An introduction to API Security in Kubernetes: 웹 애플리케이션 방화벽(WAF)</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 17.</strong></span> From Chaos to Resilience: Decoding the Secrets of Production Readiness: 서비스메시와 Istio</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 19.</strong></span> Building Efficient and Secure Docker Images with Multi-Stage Builds: 멀티스테이지 빌드 (Multi-Stage Build)</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 22.</strong></span> Test in Production with Kubernetes and Telepresence: 텔레프레즌스</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 24.</strong></span> DevSecOps - Defined, Explained &amp; Explored: DevSecOps란</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 25.</strong></span> Kube-Nation: Exploring the Land of Kubernetes: 쿠버네티스 아키텍처의 비유화 (국가)</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 27.</strong></span> From Automated to Automatic - Event-Driven Infrastructure Management with Ansible: Ansible이란?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 33.</strong></span> GitOps made simple with ArgoCD and GitHub Actions: ArgoCD와 Github Actions을 이용한 CI/CD</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 38.</strong></span> Open Standards: Empowering Cloud-Native Innovation: OCI (Open Container Initiative)와 K8s Interface</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 39.</strong></span> Is TLS in Kubernetes really that hard to understand?: 쿠버네티스 인증서</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 43.</strong></span> Let&#39;s go sidecarless in Ambient Mesh: Istio와 Ambient Mesh</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 44.</strong></span> Exploring Firecracker: AWS Lambda와 Firecracker</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 50.</strong></span> State of Cloud Native 2024: 주목받은 CNCF 생태계 툴</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 52.</strong></span> Creating a custom Dev Container for your GitHub Codespace to start with Terraform on Azure: Github Codespace와 Dev Container</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 55.</strong></span> Bringing Together IaC and CM with Terraform Provider for Ansible: Terraform과 Ansible</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 60.</strong></span> Migrating a monolith to Cloud-Native and the stumbling blocks that you don’t know about: 마이그레이션을 굳이 해야 하는가?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 63.</strong></span> Diving into Container Network Namespaces: 쿠버네티스 CNI를 리눅스 커널로 구현하기</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 68.</strong></span> Service Mesh for Kubernetes 101: The Secret Sauce to Effortless Microservices Management: Service Mesh (Istio)</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 69.</strong></span> Enhancing Kubernetes security, visibility, and networking control logic at the Linux kernel: eBPF와 Cilium (K8s CNI)이란?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 70.</strong></span> Simplified Cloud Adoption with Microsoft&#39;s Terraforms Azure Landing Zone Module: 랜딩존이란?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 72.</strong></span> Infrastructure as Code with Pulumi: Pulumi란?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 73.</strong></span> Introducing the Terraform Test Framework: IaC 테스트</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 76.</strong></span> All you need to know about AWS CDK: AWS CDK</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 80.</strong></span> Unlocking K8s Troubleshooting Best Practices with Botkube: Botkube</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 83.</strong></span> Saving Cloud Costs Using Existing Prometheus Metrics: KRR (Kubernetes Resource Recommender)</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 84.</strong></span> Hacking Kubernetes For Beginners: 쿠버네티스 해킹</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 87.</strong></span> Hands-on Performance Testing with k6: k6</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Day 89.</strong></span> Seeding Infrastructures: Merging Terraform with Generative AI for Effortless DevOps Gardens: 시딩 엔진 (Seeding Engine)</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-마치며span"><span style='background-color: MediumAquamarine'>2. 마치며</span></h2>
<hr>

<p>이번 스터디는 단순히 툴의 사용법을 익히는 것을 넘어, 왜 해당 기술이 등장했고 어떤 아키텍처적 이점을 주는지 깊이 고민해 볼 수 있는 계기가 되었다. </p>
<p><strong>특히 인프라 자동화(Ansible, Terraform)와 보안, 네트워크(eBPF, Istio) 영역은 단편적인 이해를 넘어 실질적인 구축 경험이 필수적임을 느꼈다.</strong></p>
<p>이 리스트에 포함된 주제들은 일회성 학습으로 끝내지 않고, 실제 인프라 환경이나 앞으로 진행할 프로젝트에 지속적으로 적용해 보며 완전히 내 것으로 만들어갈 계획이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 91)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-91-9ze5y1uv</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-91-9ze5y1uv</guid>
            <pubDate>Sun, 08 Feb 2026 07:45:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day91 - Team Topologies and Platform Engineering **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-빠른-흐름의-본질과-역-컨웨이-전략의-필요성span"><span style='background-color: MediumAquamarine'>1. 빠른 흐름의 본질과 역 컨웨이 전략의 필요성</span></h2>
<hr>

<p>데브옵스는 소규모 자율 팀이 피드백을 기반으로 빠르게 소프트웨어를 배포하는 것의 가치를 증명했다. </p>
<p><strong>하지만 진정한 의미의 빠른 흐름을 달성하기 위한 조직적, 문화적 변화는 여전히 미진한 상태다.</strong> </p>
<p><span style="color: indianred"><strong>우리는 기술보다 팀 구조와 상호작용에 주목해야 한다.</strong></span></p>
<h3 id="span-stylecolor-slateblue1-1-흐름과-사회-기술적-복잡성span"><span style="color: SlateBlue">1-1. 흐름과 사회-기술적 복잡성</span></h3>
<p>*<em>여기서 말하는 흐름이란 단순히 코드를 빨리 짜는 것이 아니라, 코드가 프로덕션 환경에 도달하기까지의 경로에 있는 모든 장애물을 제거하는 것을 의미한다. *</em></p>
<p>이는 서로 다른 팀과 도메인 간의 의존성을 해결하고, 팀 외부로 뻗어 있는 복잡한 기술의 덤불을 정리하는 과정이다.</p>
<p><span style="color: indianred"><strong>즉, 빠른 흐름은 주변의 사회-기술적 환경 전체를 단순화하여 가치를 더 빠르고 일관되게 전달하는 것이다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/91786c79-7bdf-46a1-b80b-2a340abe622e/image.png" width="50%" height="n%"></p>
<p>이러한 환경의 복잡성은 벤더의 제약, 예산 문제, 기술의 역사적 상태, 기존 팀 구조의 한계, 기술 부채 등 타당하고 역사적인 이유들로 인해 발생한다. </p>
<p><strong>따라서 모든 조직은 고유하며, 남들이 하는 방식을 그대로 따르는 것이 아니라 각 조직의 맥락에 맞는 독자적인 접근 방식이 필요하다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue1-2-컨웨이의-법칙과-역-컨웨이-전략span"><span style="color: SlateBlue">1-2. 컨웨이의 법칙과 역 컨웨이 전략</span></h3>
<p>*<em>조직 구조를 논할 때 컨웨이의 법칙(Conway&#39;s Law)은 매우 중요하다. *</em>
<img src="https://velog.velcdn.com/images/taekyu_/post/6e8c1bd1-8f92-48eb-99b6-6a72fa10e8f6/image.png" width="50%" height="n%"></p>
<p><span style="color: indianred"><strong>이 법칙은 조직은 그 조직의 의사소통 구조를 반영한 시스템을 설계하게 된다는 것을 의미한다. 쉽게 말해 여러분은 조직도를 소프트웨어로 배포한다는 것이다.</strong></span></p>
<p>문제는 실제 업무 현장에서는 사람들이 깔끔한 조직도에 구애받지 않고, 일을 처리하기 위해 필요한 사람과 무분별하게 소통한다는 점이다.</p>
<p>이는 조직도가 실제 소프트웨어 전달 흐름을 돕기는커녕, 서로 필요한 사람들을 멀리 떨어뜨려 놓음으로써 상황을 악화시킬 수 있음을 시사한다.
<br></p>
<p><span style="color: indianred"><strong>팀 토폴로지는 이를 뒤집는 역 컨웨이 전략(Reverse Conway Maneuver)을 제안한다.</strong> </span></p>
<p><strong>이는 만들고자 하는 소프트웨어 아키텍처와 시장에 전달하려는 가치에 맞춰 조직도를 역으로 설계하는 것이다.</strong></p>
<p>소프트웨어의 설계는 더 이상 팀의 설계와 분리될 수 없다. </p>
<p><strong>기술보다 팀을 우선순위에 두고, 팀 간의 불필요한 의존성을 줄여야 더 좋은 소프트웨어가 나온다는 것이 이 전략의 핵심이다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-인지-부하의-한계와-플랫폼-팀의-역할span"><span style='background-color: MediumAquamarine'>2. 인지 부하의 한계와 플랫폼 팀의 역할</span></h2>
<hr>

<p>*<em>팀 토폴로지에서 가장 중요한 개념 중 하나는 팀이 감당할 수 있는 인지 부하(Cognitive Load)를 관리하는 것이다. *</em></p>
<p>이는 플랫폼 엔지니어링이 등장하게 된 직접적인 배경이 된다.</p>
<h3 id="span-stylecolor-slateblue2-1-스트림-정렬-팀stream-aligned-teams의-딜레마span"><span style="color: SlateBlue">2-1. 스트림 정렬 팀(Stream-aligned Teams)의 딜레마</span></h3>
<p><strong>스트림 정렬 팀은 비즈니스 가치 흐름에 맞춰 구성된 팀으로, 회사의 수익 창출에 가장 가까이 위치한다.</strong></p>
<p>이들은 상류 의존성은 적고 하류 의존성은 많은 특징을 가진다.</p>
<p>하지만 이 팀들이 모든 것을 다 할 수는 없다. </p>
<p><span style="color: indianred"><strong>던바의 수(Dunbar&#39;s number)에 따르면 인간이 맺을 수 있는 사회적 관계에는 한계가 있으며, 이는 팀이 감당할 수 있는 인지 부하의 총량에도 한계가 있음을 의미한다.</strong></span></p>
<p>스트림 정렬 팀이 비즈니스 로직 개발뿐만 아니라 클라우드 인프라, 네트워킹, 보안, CI/CD 등 모든 기술적 세부 사항까지 신경 써야 한다면, 그들의 인지 부하는 한계치를 초과하게 되고 결국 비즈니스 가치 전달 속도는 느려질 수밖에 없다.
<br></p>
<h3 id="span-stylecolor-slateblue2-2플랫폼-팀platform-teams-인지-부하를-줄이는-해결사span"><span style="color: SlateBlue">2-2.플랫폼 팀(Platform Teams): 인지 부하를 줄이는 해결사</span></h3>
<p><span style="color: indianred"><strong>여기서 플랫폼 팀이 등장한다.</strong></span></p>
<p><strong>플랫폼 팀의 존재 목적은 하부 인프라(클라우드, K8s, IAM, DB 등)를 담당하여 스트림 정렬 팀이 비즈니스 문제 해결에만 집중할 수 있도록 돕는 것이다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>자동차와 타이어의 비유:</strong></span> 자동차 제조사(스트림 정렬 팀)는 타이어를 직접 만들지 않고 전문 타이어 제조사(플랫폼 팀)의 제품을 받아 조립한다. 제조사는 여전히 타이어를 차에 장착할 책임이 있지만, 고무의 배합 비율이나 트레드 패턴 제조법까지 알 필요는 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>DevOps의 유지:</strong></span> 이는 Dev과 Ops을 다시 분리하는 것이 아니다. &quot;직접 만든 사람이 직접 운영한다(You build it, you run it)&quot;는 원칙은 여전히 유효하다. 다만, 운영하기 쉬운 컴포넌트를 제공받아 운영의 난이도를 낮추는 것이다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue2-3-플랫폼-팀이-제공하는-가치와-상호작용-모드span"><span style="color: SlateBlue">2-3. 플랫폼 팀이 제공하는 가치와 상호작용 모드</span></h3>
<p><strong>플랫폼 팀은 복잡한 기술을 추상화하여 서비스로서(As-a-Service) 제공한다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>속도와 품질:</strong></span> 이미 검증된 상용 컴포넌트를 사용하므로 직접 구축할 필요가 없어 전달 속도가 빨라지고 품질이 보장된다.</p>
</li>
<li><p><span style="color: indianred"><strong>자율성:</strong></span> API와 문서 기반의 셀프 서비스로 제공되므로, 플랫폼 팀에 티켓을 보내거나 회의를 요청할 필요 없이 즉시 사용할 수 있다. (AWS를 쓸 때 아마존 직원에게 허락받지 않는 것과 같다.)</p>
</li>
<li><p><span style="color: indianred"><strong>조력(Facilitating):</strong></span> 서비스 제공 외에도, 플랫폼 팀의 전문가는 스트림 정렬 팀이 해당 서비스를 잘 활용할 수 있도록 코칭하고 베스트 프랙티스를 공유하는 &#39;조력자&#39; 역할을 수행한다. 이는 일시적인 상호작용이다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-슬로이피케이션slow-ification과-지속적-진화span"><span style='background-color: MediumAquamarine'>3. 슬로이피케이션(Slow-ification)과 지속적 진화</span></h2>
<hr>

<p><strong>플랫폼 엔지니어링을 조직에 성공적으로 도입하기 위해서는 단순한 기술적 구현을 넘어선 전략적 접근과 리더십의 지원이 필수적이다.</strong></p>
<h3 id="span-stylecolor-slateblue3-1시작점-찾기-공통된-과제span"><span style="color: SlateBlue">3-1.시작점 찾기: 공통된 과제</span></h3>
<p>시작은 여러 팀이 공통적으로 겪고 있는 고통스러운 지점(Pain Point)을 찾는 것이다. </p>
<p>보통 기술 스택의 하단부인 컨테이너, 가상 머신(VM), 데이터베이스, IAM 등이 좋은 후보가 된다.</p>
<p><strong>적은 수의 팀이 의존하는 작은 컴포넌트부터 시작하여 성공 사례를 만들고, 이를 조직 전체로 확산시키는전략이 유효하다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue3-2핵심-전략-슬로이피케이션slow-ificationspan"><span style="color: SlateBlue">3-2.핵심 전략: 슬로이피케이션(Slow-ification)</span></h3>
<p>*<em>발표자는 책 *『Wiring the Winning Organization』</em>에서 소개된 &#39;슬로이피케이션(Slow-ification)&#39; 개념을 강조한다. **
<img src="https://velog.velcdn.com/images/taekyu_/post/7df96508-b493-44c9-8d49-4e06e4d6a69b/image.png" width="50%" height="n%"></p>
<p><strong>이는 &quot;속도를 높이기 위해 일시적으로 속도를 늦추는 것&quot;을 의미한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>시스템 2 사고(System 2 Thinking):</strong></span> 일상적인 업무 처리(시스템 1 사고)를 멈추고, 문제의 근본적인 해결책을 만들기 위해 깊고 창의적이며 노력이 필요한 사고(시스템 2 사고)를 해야 한다. 이는 톱질을 멈추고 톱날을 가는 시간과 같다.</p>
</li>
<li><p><span style="color: indianred"><strong>리더십의 지원:</strong></span> 이 과정은 외부에서 볼 때 &quot;당장 필요한 일을 안 하고 있는 것&quot;처럼 보일 수 있다. 따라서 매니저의 강력한 하향식(Top-down) 지원과 보호가 없으면, 팀은 압박을 이기지 못하고 다시 예전의 비효율적인 방식으로 돌아가게 된다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue3-3진화하는-조직과-도메인-주도-설계dddspan"><span style="color: SlateBlue">3-3.진화하는 조직과 도메인 주도 설계(DDD)</span></h3>
<p>*<em>팀 구조와 책임은 한 번 정하면 끝나는 정적인 것이 아니다. *</em></p>
<p><strong>조직이 성숙하고 시장 상황이 변함에 따라 팀 토폴로지는 계속 진화해야 한다.</strong></p>
<p>완벽한 팀 구조는 존재하지 않으며, 현실은 언제나 복잡하고 중복과 비효율이 존재한다. 중요한 것은 완벽함이 아니라 시간이 지남에 따라 흐름이 개선되는 방향성이다.</p>
<p><span style="color: indianred"><strong>조직이 커질수록 올바른 팀 경계를 찾기 위해 도메인 주도 설계(DDD)를 활용해야 한다.</strong> </span>
<img src="https://velog.velcdn.com/images/taekyu_/post/9d4b01f3-7715-4e2a-ac05-bf6829f96773/image.png" alt=""></p>
<p>특히 <strong>&#39;독립 서비스 휴리스틱(Independent Service Heuristic)&#39;</strong>을 활용해 볼 수 있다. </p>
<p>이는 &quot;이 컴포넌트나 기능을 독립적인 SaaS 서비스로 판매할 수 있는가?&quot;라고 자문해 봄으로써 팀과 도메인의 경계를 명확히 설정하는 데 도움을 준다.</p>
<p>결론적으로, 팀 토폴로지와 플랫폼 엔지니어링은 인지 부하를 관리하고 빠른 흐름을 만들어내기 위해 조직 구조와 기술 아키텍처를 일치시키는 끊임없는 여정이다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-결론span"><span style='background-color: MediumAquamarine'>4. 결론</span></h2>
<hr>

<p><strong>팀 토폴로지와 플랫폼 엔지니어링은 단순한 조직 개편이 아니라, 인지 부하를 관리하여 지속 가능한 빠른 흐름을 만드는 전략이다.</strong></p>
<p>완벽한 구조는 없기에, 조직은 끊임없이 진화하며 상황에 맞는 유연한 팀 구조를 찾아야 한다.</p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 90)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-90-42nkvx25</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-90-42nkvx25</guid>
            <pubDate>Sun, 08 Feb 2026 06:41:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 90 - Fighting fire with fire: Why we cannot always prevent technical issues with more tech **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-무계획적-기술도입과-과잉-엔지니어링-span"><span style='background-color: MediumAquamarine'>1. 무계획적 기술도입과 과잉 엔지니어링 </span></h2>
<hr>

<p>새로운 기능을 개발하거나 버그를 수정해야 할 때, 우리는 종종 계획이나 문서화 단계를 건너뛰고 곧바로 코드를 짜거나 도구를 도입하려는 유혹에 빠진다. </p>
<p><strong>이러한 &#39;일단 만들자&#39; 식의 접근은 초기에는 빠르게 보일지 모르나, 장기적으로는 심각한 부작용을 초래한다.</strong></p>
<h3 id="span-stylecolor-slateblue1-1-이해할-수-없는-인프라와-전이적-의존성span"><span style="color: SlateBlue">1-1. 이해할 수 없는 인프라와 전이적 의존성</span></h3>
<p>문서화 없이 즉흥적으로(Ad-hoc) 문제를 해결하다 보면, 우리의 인프라는 마치 아이들의 장난감 블록처럼 변해버린다. </p>
<p><span style="color: indianred"><strong>여러 구성 요소가 연결은 되어 있지만, 도대체 어떻게, 왜 연결되어 있는지 아무도 명확히 설명할 수 없는 상태가 되는 것이다.</strong></span></p>
<p>이 과정에서 우리가 인지조차 하지 못하는 전이적 의존성 문제가 발생한다. 가장 대표적인 사례가 &#39;left-pad&#39; 사건이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>사건 개요:</strong></span> 2016년, 한 개발자가 npm 및 &#39;Kick&#39;이라는 회사와의 이름 분쟁 끝에 홧김에 자신이 만든 패키지들을 npm에서 모두 삭제해버렸다.</p>
</li>
<li><p><span style="color: indianred"><strong>영향:</strong></span> 그중 &#39;left-pad&#39;라는 패키지는 공백을 포함해 고작 11~12줄짜리 코드에 불과했지만, 수많은 대형 프로젝트들이 이 패키지를 간접적으로 의존하고 있었기에 전 세계 인터넷 서비스들이 줄줄이 마비되는 사태가 벌어졌다.</p>
</li>
<li><p><span style="color: indianred"><strong>교훈:</strong></span> 내가 직접 사용하지 않더라도 내 프로젝트가 의존하는 도구가 또 다른 도구에 의존하고 있을 수 있다. 문서화와 파악이 되지 않은 의존성은 시한폭탄과 같다.</p>
<br>


</li>
</ul>
<h3 id="span-stylecolor-slateblue1-2-과잉-엔지니어링의-늪span"><span style="color: SlateBlue">1-2. 과잉 엔지니어링의 늪</span></h3>
<p>소프트웨어 개발 초기에는 기능적/비기능적 요구사항을 완벽히 알 수 없다. </p>
<p>문제는 원래 해결하려던 문제가 무엇이었는지에 대한 원본 기록이 없을 때 발생한다.</p>
<p>초기의 목적을 잊어버린 채 개발을 진행하다 보면, <span style="color: indianred"><strong>단순한 해결책이 존재함에도 불구하고 불필요하게 복잡하고 정교한 솔루션을 만드는 과잉 엔지니어링(Over-engineering)에 빠지게 된다.</strong></span></p>
<p><strong>이는 시스템의 복잡도를 불필요하게 높여 유지보수를 어렵게 만든다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue1-3-나한테-물어봐-문화의-위험성span"><span style="color: SlateBlue">1-3. &quot;나한테 물어봐&quot; 문화의 위험성</span></h3>
<p>*<em>문서화가 부재한 조직에서 흔히 발생하는 또 다른 문제는 구전에 의존하는 온보딩이다. *</em></p>
<p>팀장이나 시니어 엔지니어는 선의로 &quot;문서는 내 머릿속에 다 있으니 모르는 게 있으면 언제든 물어봐&quot;라고 말한다.</p>
<p><span style="color: indianred">*<em>하지만 이는 친절해 보일지 몰라도, 실제로는 주니어 엔지니어나 신규 입사자의 자기 주도성을 뺏는 행위다. *</em></span></p>
<p>스스로 정보를 찾아 해결할 수 없게 만듦으로써, 사소한 것 하나까지 질문해야 하는 의존적인 환경을 조성하고, 결국 팀 전체의 생산성을 저하시킨다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-문서화-주도-개발의-가치와-전략-span"><span style='background-color: MediumAquamarine'>2. 문서화 주도 개발의 가치와 전략 </span></h2>
<hr>

<p>*<em>프레젠테이션에서는 기술적 문제 해결의 대안으로 문서화 주도 소프트웨어 개발을 제안한다. *</em></p>
<p>문서화는 단순한 기록이 아니라, 비용을 절감하고 비즈니스 연속성을 보장하는 핵심 전략이다.</p>
<h3 id="span-stylecolor-slateblue2-1-문서화의-비즈니스적-가치span"><span style="color: SlateBlue">2-1. 문서화의 비즈니스적 가치</span></h3>
<p>*<em>장애는 곧 돈이다. *</em></p>
<p>2017년 영국 항공은 시스템 장애로 인해 약 8,000만 파운드(한화 약 1,300억 원 이상)의 손실을 입었다.</p>
<p><span style="color: indianred"><strong>잘 정리된 런북(Runbook)과 장애 대응 문서는 온콜(On-call) 담당자의 스트레스를 줄여주고, 대응 시간을 단축시킨다.</strong> </span></p>
<p>특히 미들 레벨 엔지니어가 야간 장애 대응 시, 언제 시니어를 호출해야 할지 명확한 기준을 제공함으로써 심리적 안정감을 주고 불필요한 호출을 줄여준다.
<br></p>
<h3 id="span-stylecolor-slateblue2-2-문서의-유형과-작성-전략span"><span style="color: SlateBlue">2-2. 문서의 유형과 작성 전략</span></h3>
<p><strong>문서는 크게 개발자 문서(Developer Documentation)와 사용자 문서(User Documentation)</strong>로 나뉜다. </p>
<p>문서를 작성할 때는 독자의 경험 수준과 문서의 목적(설치, 업그레이드, 기능 활용 등)을 반드시 고려해야 한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>튜토리얼/하우투(How-to):</strong> </span> 특정 시나리오나 구현을 다루며, 수명이 짧을 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>공식 문서:</strong> </span> 여러 유지보수자에 의해 장기간 관리되며, 보다 범용적인 설정을 다룬다.</p>
<br>

</li>
</ul>
<p><span style="color: SlateBlue"><strong>[추천 프레임워크: Diátaxis]</strong></span></p>
<p><strong>발표자는 문서 작성의 체계를 잡기 위해 Diátaxis 프레임워크를 추천한다.</strong> </p>
<p>이는 문서를 4가지로 구분하여 작성하는 방식이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>튜토리얼(Tutorials):</strong> </span> 학습 지향 (직접 따라 하며 배우기)</p>
</li>
<li><p><span style="color: indianred"><strong>하우투 가이드(How-to Guides):</strong> </span> 문제 해결 지향 (특정 목표 달성)</p>
</li>
<li><p><span style="color: indianred"><strong>설명(Explanation):</strong> </span> 이해 지향 (배경 지식 및 개념 설명)</p>
</li>
<li><p><span style="color: indianred"><strong>레퍼런스(Reference):</strong> </span> 정보 지향 (API 명세 등)</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue2-3-기술을-활용한-문서화-k8sgptspan"><span style="color: SlateBlue">2-3. 기술을 활용한 문서화: K8sGPT</span></h3>
<p><strong>문서화에도 기술을 접목할 수 있다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/9280a6d0-db5c-4038-ba55-86c0ea08a33a/image.png" alt=""></p>
<p><strong>K8sGPT 같은 도구는 SRE의 지식을 코드로 내재화하고 AI를 활용하여 쿠버네티스 클러스터의 문제를 진단하고 설명해 준다.</strong></p>
<p>이는 마치 살아있는 문서처럼 작동하여, 주니어 엔지니어가 선배에게 묻지 않고도 에러의 원인과 해결책을 파악할 수 있게 돕는다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-카오스-엔지니어링-알-수-없는-것을-아는-것으로-span"><span style='background-color: MediumAquamarine'>3. 카오스 엔지니어링: 알 수 없는 것을 아는 것으로 </span></h2>
<hr>

<p>*<em>마지막으로, 문서화와 프로세스의 중요성은 카오스 엔지니어링(Chaos Engineering)에 적용될 때 더욱 빛을 발한다. *</em></p>
<p><span style="color: indianred"><strong>카오스 엔지니어링은 시스템에 의도적인 결함을 주입하여 회복 탄력성을 테스트하는 것이다.</strong></span></p>
<h3 id="span-stylecolor-slateblue3-1-지식의-4분면과-카오스-엔지니어링의-목표span"><span style="color: SlateBlue">3-1. 지식의 4분면과 카오스 엔지니어링의 목표</span></h3>
<p><strong>우리가 시스템에 대해 아는 지식은 네 가지 영역으로 나뉜다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/e42b2a2f-8fd0-4691-8448-115c5dfe0271/image.png" width="60%" height="n%"></p>
<ul>
<li><p><span style="color: indianred"><strong>Known Knowns (알고 있는 아는 것):</strong> </span> 우리가 인지하고 이해하는 영역. (문서화 대상)</p>
</li>
<li><p><span style="color: indianred"><strong>Known Unknowns (알고 있는 모르는 것):</strong> </span> 문제는 인지했지만 원인은 모르는 것. (예: 스토리지 레이어에 이슈가 있다는 건 알지만 정확한 원인은 모름)</p>
</li>
<li><p><span style="color: indianred"><strong>Unknown Knowns (모르고 있는 아는 것):</strong> </span> 이해는 하고 있지만 인지하지 못한 것. (직관이나 경험적으로 대처 가능하나 명시되지 않음)</p>
</li>
<li><p><span style="color: indianred"><strong>Unknown Unknowns (모르고 있는 모르는 것):</strong> </span> 가장 위험한 블랙박스 영역. 인지조차 못하고 있는 잠재적 위험.</p>
</li>
</ul>
<blockquote>
<p><strong>카오스 엔지니어링의 궁극적인 목표는 실험을 통해 Unknown Unknowns(블랙박스) 영역을 발견하고, 이를 실험을 통해 분석하여 Known Knowns(문서화된 지식) 영역으로 옮겨오는 것이다.</strong></p>
</blockquote>
<br>

<h3 id="span-stylecolor-slateblue3-2-실험의-프로세스와-사후-분석span"><span style="color: SlateBlue">3-2. 실험의 프로세스와 사후 분석</span></h3>
<p><strong>성공적인 카오스 엔지니어링을 위해서는 다음과 같은 체계적인 프로세스가 필요하다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>매핑(Mapping):</strong> </span> 현재 인프라의 상태와 서비스 간의 내/외부 의존성을 시각화하여 파악한다.</p>
</li>
<li><p><span style="color: indianred"><strong>가설 수립 및 계획:</strong> </span> &quot;포드를 삭제하면 레플리카셋이 자동으로 복구할 것이다&quot;와 같은 가설을 세우고, 폭발 반경(Blast Radius)을 최소화하여 실험을 설계한다.</p>
</li>
<li><p><span style="color: indianred"><strong>실험 실행:</strong> </span> <code>Chaos Mesh</code>, <code>LitmusChaos</code> 등의 도구를 활용한다.</p>
</li>
<li><p><span style="color: indianred"><strong>사후 분석(Post-mortem):</strong> </span> 실험 후 무엇이 발생했고, 어떻게 대응했는지를 반드시 기록해야 한다. 이는 팀 전체의 학습 기회이자, 미래의 장애를 예방하는 자산이 된다. 
(도구: <code>incident.io</code>, <code>Rootly</code> 등 활용 가능)</p>
<br>

</li>
</ol>
<h3 id="span-stylecolor-slateblue3-3-결론-일단-적어라span"><span style="color: SlateBlue">3-3. 결론: &quot;일단 적어라&quot;</span></h3>
<p><strong>기술적 해결책을 찾기 전에 기록하는 습관을 들이라는 것이다.</strong></p>
<p>잘못된 내용을 적는 것이 아무것도 적지 않는 것보다 낫다. </p>
<p>과거에 작동했던 명령어가 지금은 작동하지 않을지라도, 그것은 미래의 팀원에게 훌륭한 단서가 된다. </p>
<p>거창한 문서가 아니더라도, 홈랩(Home lab)이나 개인 학습 노트부터 시작하여 자신이 겪은 문제와 해결 과정을 기록하고 공유하는 것이야말로 진정한 엔지니어링의 시작이다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-정리-및-결론-span"><span style='background-color: MediumAquamarine'>4. 정리 및 결론 </span></h2>
<hr>

<p>기술적 부채와 복잡성은 더 많은 도구가 아닌, 명확한 문서화와 체계적인 프로세스로 해결해야한다.</p>
<p><strong>&#39;일단 만들자&#39;는 유혹을 뿌리치고, 우리가 아는 것과 모르는 것을 기록하며 Unknown 영역을 줄여나가는 것이 진정한 엔지니어링이다.</strong></p>
<p>완벽한 문서를 쓰려 하기보다, 사소한 트러블슈팅 과정이라도 기록하고 공유하는 습관을 들여보자.</p>
<p><span style="color: indianred"><strong>이러한 기록들이 모여 팀의 리스크를 줄이고, 더 견고하고 회복 탄력성 있는 인프라를 만드는 가장 강력한 무기가 될 것이다.</strong></span></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 89)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-89</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-89</guid>
            <pubDate>Sun, 08 Feb 2026 03:21:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 89 - Seeding Infrastructures: Merging Terraform with Generative AI for Effortless DevOps Gardens **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-테라폼과-생성형-ai-결합-span"><span style='background-color: MediumAquamarine'>1. 테라폼과 생성형 AI 결합 </span></h2>
<hr>

<p><strong>기존의 Terraform 사용 방식이 인간이 코드를 한 줄씩 작성하고 검증하는 방식이었다면, 생성형 AI의 도입은 이 프로세스를 근본적으로 변화시킨다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>코드 생성의 가속화:</strong></span> Amazon Bedrock의 Claude v2나 OpenAI의 GPT 모델들은 자연어 요구사항을 완벽한 테라폼 HCL(HashiCorp Configuration Language) 코드로 변환 가능</p>
</li>
<li><p><span style="color: indianred"><strong>단계별 추론:</strong></span> </p>
<ul>
<li><p>AI는 단순히 결과물만 내놓는 것이 아니라, &quot;먼저 VPC를 구성하고, 그 다음 서브넷을 나눈 뒤, EC2를 배치하라&quot;는 식의 순차적 사고가 가능함. </p>
</li>
<li><p>AutoGPT나 LangChain과 같은 프레임워크와 결합했을 때 더욱 강력해짐.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong><code>인간</code>-<code>AI</code>-<code>인프라</code>의 조화:</strong></span> AI가 초안을 빠르고 방대하게 작성하면, 인간은 이를 검수하고 전략적 의사결정을 내리는 구조로 업무가 재편</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-seeding-engine이란-span"><span style='background-color: MediumAquamarine'>2. Seeding Engine이란? </span></h2>
<hr>

<p>*<em>우리가 흔히 사용하는 ChatGPT나 베드락(Bedrock) 콘솔은 기본적으로 수동적이다. *</em></p>
<p>개발자가 &quot;EC2 인스턴스 하나 만들어줘&quot;라고 입력해야만 비로소 코드를 뱉어낸다.</p>
<p><span style="color: indianred"><strong>하지만 시딩 엔진은 능동적이고 지속적이다.</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>정의:</strong></span> 조직의 요구사항과 Best Practice를 입력받아, Terraform 코드를 생성(Seeding)할 뿐만 아니라, 배포된 인프라가 건강하게 유지되도록 지속적으로 관리하고 치유하는 통합 자동화 시스템</p>
</li>
<li><p><span style="color: indianred"><strong>비유:</strong></span> 마트에서 다 자란 화분을 사 오는 것이 아니라, 유능한 정원사(AI Engine)를 고용하여 씨앗을 심고, 물을 주고, 잡초를 뽑게 하는 것과 같다.</p>
</li>
</ul>
<p><strong>프레젠테이션에서는 해당 시스템이 단순한 질의응답 도구를 넘어선다고 강조한다.</strong></p>
<p>그 차별점은 다음 세 가지로 요약된다.</p>
<ol>
<li><p><span style="color: SlateBlue"><strong>Context을 이해하는 맞춤형 인프라 (RAG 활용)</strong></span></p>
<ul>
<li><p>일반적인 AI 모델은 인터넷에 있는 범용적인 지식으로 코드를 짠다. 하지만 시딩 엔진은 RAG(검색 증강 생성) 기술을 접목하여 조직의 특수성을 이해한다.</p>
</li>
<li><p><span style="color: indianred"><strong>일반 AI:</strong></span> &quot;보안 그룹 만들어줘&quot;라고 하면 0.0.0.0/0을 허용하는 등 범용적이지만 보안에 취약한 코드를 생성할 가능성이 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>시딩 엔진:</strong></span> *<em>기업 내부의 보안 가이드라인, 기존 아키텍처 문서, 레거시 코드를 참조한다. *</em>
따라서 &quot;우리 회사 보안 정책인 22번 포트 폐쇄 및 사내 VPN 대역만 허용&quot;하는 규칙을 준수하며 코드를 생성한다. 즉, 조직의 가드레일 안에서 안전하게 작동한다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>생성(Day 1)을 넘어선 운영(Day 2)의 자동화</strong></span></p>
<ul>
<li><p>코드를 짜고 배포하는 것에서 끝나는 것이 아니라, 인프라가 배포된 이후의 삶(Day 2 Operations)까지 관여한다.</p>
</li>
<li><p><span style="color: indianred"><strong>프로액티브 관리:</strong></span> 발표된 아키텍처에서 보았듯, 시딩 엔진은 CloudWatch 및 Lambda와 유기적으로 연결되어 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>작동 시나리오:</strong></span> 트래픽이 급증하여 사이트가 느려지는 상황을 가정해 보자.</p>
<ol>
<li><p>CloudWatch가 이상 징후를 감지한다.</p>
</li>
<li><p>시딩 엔진(Lambda)으로 신호를 보낸다.</p>
</li>
<li><p>엔진이 이를 분석하여 &quot;Auto Scaling Group의 Max Capacity를 늘려야 합니다&quot;라고 판단한다.</p>
</li>
<li><p>테라폼 코드를 스스로 수정하고 재배포를 자동화한다.</p>
<br>
</li>
</ol>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>인간과 AI의 협업 프로세스 내재화</strong></span></p>
<ul>
<li><p>시딩 엔진은 AI가 독단적으로 행동하지 않도록 설계된다.</p>
</li>
<li><p><span style="color: indianred"><strong>초안 작성:</strong></span> AI가 복잡한 인프라 구성의 80~90%를 순식간에 구축한다.</p>
</li>
<li><p><span style="color: indianred"><strong>인간의 검수:</strong></span> 엔지니어는 처음부터 코드를 짜는 것이 아니라, AI가 만든 구조가 전략적으로 맞는지, 보안상 치명적인 문제는 없는지 검토하는 역할로 전환된다.</p>
</li>
<li><p><span style="color: indianred"><strong>피드백 루프:</strong></span> 인간이 코드를 수정하면, 엔진은 이를 학습하여 다음 배포 때 더 정확한 코드를 생성한다.</p>
<br>


</li>
</ul>
</li>
</ol>
<p><strong>발표자는 이 개념을 실제 AWS 환경에 구현했다. 그 기술적 흐름은 다음과 같이 이루어진다.</strong>
<img src="https://velog.velcdn.com/images/taekyu_/post/e2591b15-7f89-40e6-bee2-e089cd5ddae4/image.png" width="70%" height="n%"></p>
<ol>
<li><p><span style="color: SlateBlue"><strong>입력:</strong></span> 엔지니어가 확장 가능한 웹 앱 인프라 필요라는 자연어 요구사항이나 설정 파일(JSON/YAML)을 입력한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>생성 (Generation via Bedrock):</strong></span> EC2에 호스팅된 엔진(Python 애플리케이션)이 Amazon Bedrock(Claude v2)을 호출한다. 이때 프롬프트 엔지니어링을 통해 테라폼 HCL 문법과 사내 규칙을 준수하도록 강제한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>검증 및 배포 :</strong></span> 생성된 코드는 S3에 저장되고, AWS CodePipeline을 통해 <code>terraform plan</code> -&gt; <code>validate</code> -&gt; <code>apply</code> 과정을 거친다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>감지 및 대응 :</strong></span> 인프라에 변경이 필요하거나 문제가 생기면, CloudWatch 알람이 다시 엔진(Lambda)을 깨워 코드를 수정하고 인프라를 치유한다.</p>
</li>
</ol>
<p><strong>발표에서 공개된 시딩 엔진의 아키텍처는 AWS의 관리형 서비스들을 유기적으로 결합하여 생성-배포-감지-대응의 완전한 파이프라인을 구축했다.</strong></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>아키텍처 구성 요소 상세</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>Amazon EC2 (Host):</strong></span> 시딩 엔진의 본체(Python 애플리케이션)가 구동되는 서버다. 전체 워크플로우를 조율하는 두뇌 역할을 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Amazon Bedrock (Generative AI):</strong></span> Anthropic Claude v2 모델을 사용하여, 보안성을 유지한 채 내부 네트워크로 AI 모델을 호출하고 테라폼 코드를 생성한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Amazon S3 (Storage &amp; Trigger):</strong></span> 생성된 테라폼 템플릿(<code>.tf</code>)이 저장된다. 파일이 업로드되거나 수정되는 이벤트가 발생하면, 이를 트리거로 후속 자동화 작업이 시작된다.</p>
</li>
<li><p><span style="color: indianred"><strong>AWS CodeBuild &amp; CodePipeline (CI/CD):</strong></span> S3의 변경 사항을 감지하여 실제로 <code>terraform plan</code>, <code>terraform apply</code>를 수행하는 배포 파이프라인이다.</p>
</li>
<li><p><span style="color: indianred"><strong>Amazon CloudWatch &amp; AWS Lambda:</strong></span></p>
<ul>
<li><p>CloudWatch는 인프라의 상태를 실시간으로 감시한다.</p>
</li>
<li><p>이상이 감지되면 Lambda가 트리거되어, EC2에 있는 시딩 엔진에게 &quot;코드를 수정하라&quot;는 신호를 보내거나 즉각적인 복구 스크립트를 실행한다. </p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>AWS Secrets Manager:</strong></span> Bedrock API 키나 DB 접근 정보 등 민감한 데이터를 코드에서 분리하여 안전하게 관리한다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-프롬프트-엔지니어링과-코드-구현-전략-span"><span style='background-color: MediumAquamarine'>3. 프롬프트 엔지니어링과 코드 구현 전략 </span></h2>
<hr>

<p><strong>시딩 엔진의 아키텍처를 갖췄다면, 이제는 이 엔진을 실제로 똑똑하게 움직이게 할 두뇌를 설계해야 한다.</strong></p>
<p><span style="color: indianred"><strong>프레젠테이션에서는 이 과정에서 가장 중요한 것은 AI에게 작업을 지시하는 프롬프트 엔지니어링과 결과를 검증하는 피드백 루프라고 강조한다.</strong></span></p>
<p>AI의 창의성을 기술적으로 통제하고, 결과물을 엄격하게 검증하는 과정이 없다면 자동화는 오히려 리스크가 될 수 있기 때문이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>구체적 프롬프트:</strong></span> &quot;웹 앱 만들어줘&quot; 같은 추상적 지시는 지양한다. 대신 &quot;EC2, Auto Scaling Group, ELB, RDS, S3, IAM Role, VPC 등을 포함한 아키텍처&quot;와 같이 필요한 구성 요소를 명확히 나열하여 청사진을 제시해야 AI가 정확한 코드를 작성한다.</p>
</li>
<li><p><span style="color: indianred"><strong>파라미터 튜닝:</strong></span> 인프라 코드는 소설이 아니므로 창의성보다는 팩트와 정확성이 필수다. 모델의 <code>Temperature</code>와 <code>Top_P</code> 값을 0에 가깝게 설정하여, 존재하지 않는 리소스를 만들어내는 Hallucination 현상을 차단하고 일관된 결과를 얻어야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>파이썬 구현:</strong></span> <code>boto3 라이브러리</code>로 <code>Bedrock(Claude v2)</code>을 호출하고, 응답받은 JSON에서 코드를 추출해 <code>.tf</code> 파일로 저장한다. 이후 <code>subprocess</code> 모듈로 <code>terraform init</code>과 <code>apply</code>를 연쇄 실행하여 코드 작성부터 배포까지를 하나의 함수로 자동화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>자동화된 테스트:</strong></span> 문법적으로 옳은 코드가 보안적으로도 안전하다는 보장은 없다. 배포 전 <code>Checkov</code>, <code>TFLint</code>, <code>Terratest</code> 같은 도구를 파이프라인에 심어 SSH 포트 개방이나 리소스 제한 같은 정책 위반을 기계적으로 걸러내야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>인간 개입:</strong></span> 기계적인 검증을 넘어 조직적인 피드백이 필요하다. 데이터 분석가나 아키텍트 등 다양한 직군의 요구사항을 반영해 프롬프트를 개선하고, 특히 민감한 보안 설정은 최종적으로 인간의 승인을 거치도록 프로세스를 설계해야 한다.</p>
</li>
</ul>
<p>이처럼 시딩 엔진은 단순히 코드를 생성하는 것을 넘어, <strong>&quot;믿어라, 하지만 검증하라(Trust, but Verify)&quot;</strong>는 원칙 아래 정교한 지시와 다층적인 검증 과정을 거쳐야 신뢰할 수 있는 시스템으로 완성된다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-케이스-스터디-및-결론-span"><span style='background-color: MediumAquamarine'>4. 케이스 스터디 및 결론 </span></h2>
<hr>

<p>프레젠에이션에서는 급성장하는 웹사이트 프로젝트에 이 시딩 엔진을 도입했다. </p>
<p>*<em>그 결과 트래픽 변동에 따라 인프라가 자동으로 확장되는 Adaptive Scaling 환경을 구축했으며, 다운타임과 유지보수 비용을 획기적으로 절감했다. *</em></p>
<p>엔지니어는 단순 코딩 시간은 줄이고 전략적 설계에 집중할 수 있게 되었다.</p>
<p><span style="color: SlateBlue"><strong>핵심 요약</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>나만의 엔진 구축:</strong></span> 베드락과 같은 파운데이션 모델을 그대로 쓰지 말고, 조직의 데이터와 가이드라인을 학습시켜 최적화해야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>협업 AI :</strong></span> 미래의 데브옵스는 AI가 인간을 대체하는 것이 아니라, 인간이 AI라는 강력한 파트너를 감독하고 협업하는 형태로 진화할 것이다.</p>
</li>
<li><p><span style="color: indianred"><strong>보안 타협 금지:</strong></span> 자동화의 편리함에 취해 보안을 놓치면 안 된다. 보안 검수는 AI와 인간의 이중 레이어로 철저히 수행되어야 한다.</p>
</li>
</ul>
<p><span style="color: indianred">*<em>결론적으로, 시딩 엔진은 인프라를 심고 스스로 자라나게 관리하는 새로운 데브옵스의 지평을 열고 있다. *</em></span></p>
<p><strong>이제 엔지니어는 Coder에서 AI 엔진의 설계자(Architect)로 역할을 확장해야 할 때다.</strong></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 88)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-88-8x1dn2jm</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-88-8x1dn2jm</guid>
            <pubDate>Sat, 07 Feb 2026 06:56:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 88 - What Developers Want from Internal Developer Portals **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-idp의-배경-span"><span style='background-color: MediumAquamarine'>1. IDP의 배경 </span></h2>
<hr>

<p><strong>지난 5년 동안 마이크로서비스, 쿠버네티스, 클라우드 등의 기술은 조직의 효율성을 높여주었지만, 동시에 그만큼의 부작용인 복잡성을 초래했다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>마이크로서비스와 쿠버네티스:</strong></span> 병렬 작업을 가능하게 하여 배포 속도를 높였지만, 너무 많은 서비스가 파편화되면서 소유권을 파악하고 전체 생태계를 이해하는 것이 불가능에 가까워졌다.</p>
</li>
<li><p><span style="color: indianred"><strong>클라우드 호스팅:</strong></span> 리소스 생성이 쉬워지고 비용이 저렴해졌지만, 이로 인해 정체를 알 수 없는 리소스가 방치되는 Sprawl 현상이 발생했다.</p>
</li>
<li><p><span style="color: indianred"><strong>도구의 홍수:</strong></span> 더 빠른 개발을 위해 수많은 도구를 도입했지만, 잦은 컨텍스트 스위칭과 알림 공해로 인해 오히려 개발자의 집중력을 저해하고 있다.</p>
</li>
</ul>
<br>

<p><span style="color: SlateBlue"><strong>기존 도구들의 한계: Context의 실종</strong></span></p>
<p>현재 시장의 도구들은 각자의 영역에서는 훌륭하지만, 전체를 꿰뚫는 통찰력을 제공하지 못한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>APM/모니터링 (Datadog 등):</strong></span> Quality를 측정하지만, 해당 서비스가 무엇이며 왜 존재하는지에 대한 맥락은 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>엔지니어링 메트릭 (DORA 등):</strong></span> 병목 현상은 알려주지만, 데이터가 정적이라 실제 Action으로 이어지기 어렵다.</p>
</li>
<li><p><span style="color: indianred"><strong>워크플로우 도구 (Git, Jira):</strong></span> 프로세스를 관리하지만, 우리가 올바른 방식으로 구축하고 있는지에 대한 가시성은 부족하다.</p>
</li>
</ul>
<p>결국 가장 큰 문제는 이 모든 파편화된 정보를 하나로 연결하여 <strong>&quot;이 서비스는 누가 만들었고, 어떤 리소스와 연결되며, 현재 상태는 어떠한가?&quot;</strong>를 보여주는 Entity Graph의 부재다. </p>
<p><span style="color: indianred"><strong>IDP는 바로 이 끊어진 연결고리를 잇는 역할을 해야 한다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-개발자가-idp에서-진정으로-원하는-3가지-span"><span style='background-color: MediumAquamarine'>2. 개발자가 IDP에서 진정으로 원하는 3가지 </span></h2>
<hr>

<p><strong>Cortex가 50명의 엔지니어를 대상으로 &quot;IDP에서 무엇을 원하는가?&quot;를 설문한 결과, 개발자들의 고통과 니즈는 다음 세 가지로 명확히 요약된다.</strong></p>
<ol>
<li><p><span style="color: SlateBlue"><strong>정보 탐색의 고통</strong></span></p>
<ul>
<li><p>개발자들은 서비스, 리소스, 인프라 정보가 수십 개의 도구에 흩어져 있어 필요한 정보를 찾는 데 엄청난 시간을 낭비한다.</p>
</li>
<li><p><span style="color: indianred"><strong>온보딩의 어려움:</strong></span> 신규 입사자가 전체 시스템 구조를 파악하는 데 오랜 시간이 걸린다.</p>
</li>
<li><p><span style="color: indianred"><strong>장애 대응 지연:</strong></span> 한밤중에 모르는 서비스의 알림을 받았을 때, 소유자가 누구인지, Runbook은 어디에 있는지 찾을 수 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>협업의 장벽:</strong></span> 내 기능이 다른 팀의 서비스와 상호작용할 때, 누구와 소통해야 하는지 파악하기 어렵다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue">*<em>우선순위의 불명확성 *</em></span></p>
<ul>
<li><p>보안(Snyk), 모니터링(Datadog), 기획(Jira), 코드 품질(SonarQube) 등 사방에서 알림과 요구사항이 쏟아진다.</p>
</li>
<li><p>이 소음 속에서 개발자는 <strong>&quot;지금 당장 내가 무엇을 먼저 처리해야 하는가?&quot;</strong>를 판단하기 어렵다. </p>
</li>
<li><p>개발자는 주관적인 판단이 아닌, 조직 차원의 명확한 우선순위 가이드를 원한다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue">*<em>템플릿과 셀프 서비스 *</em></span></p>
<ul>
<li><p><span style="color: indianred"><strong>개발자가 꼽은 IDP의 가장 필요한 기능 1위는 템플릿(Templates)이다.</strong></span></p>
</li>
<li><p>개발자는 새로운 프로젝트를 시작할 때마다 복잡한 초기 설정을 반복하고 싶어 하지 않는다.</p>
</li>
<li><p>조직의 Best Practice가 이미 적용된 골든 패스(Golden Path)를 통해, 버튼 클릭 몇 번으로 즉시 개발 가능한 환경을 구축하기를 원한다.</p>
</li>
</ul>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-cortex의-해법-실행-가능성-중심의-기능span"><span style='background-color: MediumAquamarine'>3. Cortex의 해법: 실행 가능성 중심의 기능</span></h2>
<hr>

<p><span style="color: indianred">*<em>개발자 포털이 실패하는 가장 큰 이유는 정보의 무덤이 되기 때문이다. *</em></span></p>
<p>Cortex는 정보 제공을 넘어 <strong>그래서 지금 무엇을 해야 하는가?</strong>에 대한 답을 주고, 개발자가 즉시 행동할 수 있도록 돕는 5가지 핵심 기능을 제공한다.</p>
<ol>
<li><p><span style="color: SlateBlue"><strong>기록 시스템 (System of Record)과 자동 발견</strong></span></p>
<ul>
<li><p>많은 조직이 엑셀이나 Wiki에 서비스 목록을 수동으로 정리하지만, 이는 금방 낡은 데이터가 된다. Cortex는 이를 자동화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>진실의 원천 (Source of Truth):</strong></span> Cortex는 기존 도구(Jira, GitHub, Snyk 등)를 대체하지 않는다. <strong>대신 50개 이상의 도구와 연동하여 현재 상태가 가장 정확한 데이터를 실시간으로 가져온다.</strong> 팀 정보는 HR 시스템(Workday)에서, 보안 정보는 보안 도구(Snyk)에서 가져와 하나로 합친다.</p>
</li>
<li><p><span style="color: indianred"><strong>자동 발견:</strong></span> 개발자가 일일이 &quot;이 서비스는 제가 만들었어요&quot;라고 입력할 필요가 없다. Cortex가 도구 간의 관계를 분석하여 <strong>&quot;이 GitHub 레포지토리는 저 쿠버네티스 클러스터에 배포되어 있고, 소유권은 A팀에게 있다&quot;</strong>는 의존성 그래프를 자동으로 생성하고 최신 상태로 유지한다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue">*<em>강력한 쿼리 빌더 *</em></span></p>
<ul>
<li><p>수집된 데이터가 단순히 쌓여만 있으면 의미가 없다. Cortex는 흩어진 데이터를 조합하여 복잡한 질문에 즉시 답할 수 있는 능력을 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>복합 질문 해결:</strong></span> 과거에는 보안팀, 인프라팀, 개발팀에게 각각 물어봐야 했던 질문을 한 번에 해결한다.</p>
</li>
<li><p><span style="color: indianred"><strong>실전 예시:</strong></span></p>
<ul>
<li><p>&quot;Log4j 취약점 버전이 포함된 라이브러리를 쓰면서(보안), 최근 24시간 동안 배포되지 않은(인프라) 서비스 목록을 줘.&quot;</p>
</li>
<li><p>&quot;개인정보(PII)가 포함된 DB를 사용하는데(데이터), 백업 설정이 없는(인프라) 위험한 서비스는 어디인가?&quot;</p>
<br>
</li>
</ul>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue">*<em>스코어카드(Scorecards)와 게임화 *</em></span></p>
<ul>
<li><p>조직의 표준을 지키라고 강요하는 대신, 게임처럼 레벨을 올리도록 유도하는 기능이다. 리더십에게는 현황판이 되고, 개발자에게는 명확한 가이드가 된다.</p>
</li>
<li><p><span style="color: indianred"><strong>표준의 자동화:</strong></span> 배포 전 체크리스트 같은 문서를 자동화된 규칙으로 만든다. 사람이 검사하는 것이 아니라, 시스템이 데이터(테스트 커버리지, 보안 점수 등)를 보고 자동으로 점수를 매긴다.</p>
</li>
<li><p><span style="color: indianred"><strong>단계별 성숙도:</strong></span> 등급을 나누어 동기를 부여한다.</p>
<ul>
<li><p>🥉 <strong>브론즈 (기본):</strong> 서비스 소유자가 등록되어 있고, Readme 파일이 있는가?</p>
</li>
<li><p>🥈 <strong>실버 (중간):</strong> 유닛 테스트가 자동화되어 있고, 당번(On-call) 로테이션이 설정되어 있는가?</p>
</li>
<li><p>🥇 <strong>골드 (최상):</strong> SLO(서비스 수준 목표)를 추적하고 있으며, 치명적인 취약점이 0개인가?</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>리더십 리포팅:</strong></span> 경영진은 &quot;우리 조직의 서비스 건전성이 지난달 대비 얼마나 좋아졌는가?&quot;, &quot;어떤 팀이 리스크가 가장 높은가?&quot;를 데이터로 한눈에 파악할 수 있다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Self-Service와 스캐폴더(Scaffolder)</strong></span></p>
<ul>
<li><p>*<em>개발자가 가장 원하는 기능 1위 *</em></p>
</li>
<li><p><strong>새로운 프로젝트를 시작할 때, 고민하지 않고 버튼 하나로 완벽한 환경을 만드는 것</strong></p>
</li>
<li><p><span style="color: indianred"><strong>스캐폴더 (Scaffolder):</strong></span> 발판이라는 뜻처럼, 프로젝트의 뼈대를 자동으로 만들어준다. 오픈 소스인 쿠키커터(Cookiecutter)를 기반으로 하여 보일러플레이트(기본 코드) 생성을 자동화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>피드백 루프와 골든 패스:</strong></span> 단순히 코드만 생성하는 게 아니다. Cortex의 템플릿을 사용하면 <strong><code>Git 저장소 생성 + 기본 코드 작성 + 카탈로그 등록</code></strong>이 한 번에 끝난다.</p>
</li>
<li><p><span style="color: indianred"><strong>동기 부여:</strong></span> 개발자에게 <strong>&quot;이 템플릿을 쓰면, 스코어카드에서 자동으로 실버등급을 달성합니다&quot;</strong>라고 안내한다. 개발자는 편해서 좋고, 조직은 자연스럽게 표준을 준수시킬 수 있다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>개발자 홈페이지와 플러그인</strong></span></p>
<ul>
<li><p>개발자에게 불필요한 알림을 줄이고, 나에게 중요한 정보만 보여주는 개인화된 공간이다.</p>
</li>
<li><p><span style="color: indianred"><strong>개인화된 대시보드:</strong></span> 로그인하자마자 &quot;내가 당번인가?&quot;, &quot;내가 리뷰해야 할 코드는 무엇인가?&quot;, &quot;내 서비스 점수는 몇 점인가?&quot;를 보여준다. 또한 Slack 등으로 <strong>&quot;지금 이것부터 처리하세요&quot;</strong>라고 넛지(Nudge, 부드러운 개입)를 보내 행동을 유도한다.</p>
</li>
<li><p><span style="color: indianred"><strong>플러그인 아키텍처:</strong></span> Cortex가 기본으로 제공하지 않는 기능도 문제없다. React/TypeScript를 사용해 조직 고유의 도구를 포털 안에 심을 수 있다. (예: 사내 전용 배포 UI나 K8s 제어판을 포털 메뉴로 통합).</p>
</li>
</ul>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-성공적인-idp-도입을-위한-4단계-로드맵span"><span style='background-color: MediumAquamarine'>4. 성공적인 IDP 도입을 위한 4단계 로드맵</span></h2>
<hr>

<p><span style="color: indianred">*<em>IDP 도입에 실패하는 가장 큰 이유는 데이터 없이 셀프 서비스부터 구축하려 하기 때문이다. *</em></span></p>
<p>Cortex는 다음의 순서로 도입할 것을 강력히 권장한다.</p>
<p><span style="color: SlateBlue"><strong>1단계: 정보 수집 및 집계 (Inventory &amp; Aggregation)</strong></span></p>
<ul>
<li><p>가장 먼저 <strong>무엇이 존재하는지를 파악</strong>해야 한다. </p>
</li>
<li><p>서비스, 인프라, 리소스, 소유권 정보를 긁어모아 카탈로그를 구축하라. </p>
</li>
<li><p>현재 상태를 모르면 개선할 수 없다.</p>
</li>
</ul>
<p><span style="color: SlateBlue"><strong>2단계: 평가 및 목표 설정 (Assess &amp; North Star)</strong></span></p>
<ul>
<li><p>데이터가 모였다면 <strong>우리는 어디에 있는가?를 평가</strong>해야 한다. </p>
</li>
<li><p>조직이 중요하게 생각하는 가치(신뢰성, 보안, 생산성 등)에 대한 Baseline을 설정하고, 목표를 정하라.</p>
</li>
</ul>
<p><span style="color: SlateBlue"><strong>3단계: 조치 처방 (Prescribe Action)</strong></span></p>
<ul>
<li><p>목표 대비 현재의 격차를 파악했다면, 개발자에게 무엇을 해야 하는지 알려주어야 한다. </p>
</li>
<li><p>스코어카드나 우선순위 알림을 통해 개발자가 집중해야 할 작업을 명확히 제시하라.</p>
</li>
</ul>
<p><span style="color: SlateBlue"><strong>4단계: 경험 최적화 (Optimize Experiences)</strong></span></p>
<ul>
<li><p><span style="color: indianred">*<em>마찰이 발생하는 지점이 명확해졌을 때 비로소 셀프 서비스를 도입하라. *</em></span></p>
</li>
<li><p>예를 들어, &quot;SLO 설정이 어렵다&quot;는 데이터가 나오면 그때 SLO 설정 자동화 템플릿을 제공하는 식이다.</p>
</li>
</ul>
<p><span style="color: SlateBlue"><strong>결론: 플라이휠(Flywheel) 효과</strong></span></p>
<ul>
<li><p>이 과정을 거치면 <code>측정 -&gt; 평가 -&gt; 조치 -&gt; 최적화</code>라는 피드백 루프가 형성된다. </p>
</li>
<li><p>이 루프가 반복될수록 IDP는 단순한 도구를 넘어 조직의 생산성을 지속적으로 가속화하는 플라이휠이 된다. 이것이 바로 성공적인 조직들이 IDP를 통해 가치를 창출하는 방식이다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-정리-및-결론span"><span style='background-color: MediumAquamarine'>5. 정리 및 결론</span></h2>
<hr>

<p><span style="color: indianred">*<em>성공적인 내부 개발자 포털(IDP)은 플랫폼 팀의 관점이 아닌, 실제 사용자인 개발자의 핵심 니즈(정보 탐색, 우선순위 명확화, 템플릿)에서 출발해야 한다. *</em></span></p>
<p>Cortex는 단순한 정보 통합을 넘어, 스코어카드와 스캐폴딩을 통해 개발자가 데이터를 보고 즉시 행동(Action)할 수 있는 실행 가능한 환경을 제공하는 데 초점을 맞춘다. </p>
<p>조직은 무작정 기능을 도입하기보다 <span style="color: indianred"><strong><code>정보 수집 → 평가 → 조치 처방 → 경험 최적화</code></strong></span>의 단계적 접근을 통해 선순환 피드백 루프를 형성해야 한다. </p>
<p><strong>결국 IDP는 단순한 관리 도구가 아니라, 조직의 엔지니어링 문화를 성숙시키고 생산성을 지속적으로 가속화하는 강력한 플라이휠(Flywheel)이 되어야 한다.</strong></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 87)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-87-sp3zkv76</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-87-sp3zkv76</guid>
            <pubDate>Sat, 07 Feb 2026 04:53:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 87 - Hands-on Performance Testing with k6 **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-성능-테스트performance-testing-개요span"><span style='background-color: MediumAquamarine'>1. 성능 테스트(Performance Testing) 개요</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-성능-테스트의-목적span"><span style="color: SlateBlue">1-1. 성능 테스트의 목적</span></h3>
<p><strong>성능 테스트의 궁극적인 목표는 테스트 대상 시스템의 실생활에서의 신뢰성을 검증하는 것이다.</strong></p>
<p><span style="color: indianred"><strong>단순히 기능이 작동하는지를 넘어, 실제 사용 환경과 유사한 부하 상황에서 시스템이 어떻게 반응하는지를 확인해야 한다.</strong> </span></p>
<p>이를 위해 주로 다음 세 가지 측면을 중점적으로 살펴본다.</p>
<ol>
<li><p><span style="color: SlateBlue"><strong>지연 시간 (응답 속도) :</strong></span> 시스템이 사용자 요청에 얼마나 빠르게 응답하는가? </p>
</li>
<li><p><span style="color: SlateBlue"><strong>오류 :</strong></span> 부하가 가해졌을 때 시스템이 에러를 반환하거나 내부적인 오류가 발생하지 않는가?</p>
</li>
<li><p><span style="color: SlateBlue"><strong>기능적 무결성 :</strong></span> 부하 상태에서도 통합 테스트나 End-to-End 사용자 흐름이 정상적으로 작동하는가?</p>
<ul>
<li><span style="color: indianred"><strong>중요:</strong></span> *<em>기능 테스트만으로는 부하 상황에서의 동작을 보장할 수 없다. *</em>
시스템은 사용자 10명일 때와 10,000명일 때 전혀 다르게 동작하기 때문이다.<br>

</li>
</ul>
</li>
</ol>
<h3 id="span-stylecolor-slateblue1-2-부하-테스트load-testing와-성능-테스트의-관계span"><span style="color: SlateBlue">1-2. 부하 테스트(Load Testing)와 성능 테스트의 관계</span></h3>
<p>성능 테스트와 부하 테스트는 매우 밀접한 관계가 있다. </p>
<p><strong>사실상 부하 테스트는 부하(Load)를 동반한 성능 테스트라고 정의할 수 있다.</strong></p>
<p><span style="color: indianred">*<em>부하 테스트는 크게 테스트 지속 시간(Duration)과 트래픽 부하량(Load)이라는 두 가지 변수에 따라 다음과 같이 세분화된다. *</em></span></p>
<img src="https://velog.velcdn.com/images/taekyu_/post/c381540e-52c9-47e4-b057-ba91d359e83f/image.png" width="70%" height="n%">

<ol>
<li><p><span style="color: SlateBlue"><strong>평균 부하 테스트 (Average Load Test):</strong></span> </p>
<ul>
<li><p>평소 운영 환경에서 발생하는 평균적인 트래픽 수준을 시뮬레이션</p>
</li>
<li><p>일상적인 성능 기준을 잡는 데 사용</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>스트레스 테스트 (Stress Test):</strong></span> </p>
<ul>
<li>시스템의 한계를 확인하기 위해 평소보다 높은 트래픽을 발생시킴.</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>스파이크 테스트 (Spike Test):</strong></span> </p>
<ul>
<li><p>특정 이벤트(예: 티켓 예매 오픈) 시점처럼, 트래픽이 매우 낮은 상태에서 갑자기 매우 높은 상태(Peak)로 급증하는 상황을 시뮬레이션</p>
</li>
<li><p>시스템의 급격한 확장에 대한 대응력을 테스트</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>스모크 테스트 (Smoke Test):</strong></span> </p>
<ul>
<li><p>최소한의 부하로 아주 짧은 시간 동안 수행</p>
</li>
<li><p>본격적인 테스트 전에 스크립트 오류 여부와 시스템의 기본 가용성을 빠르게 점검하는 것이 목적</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>소크 테스트 (Soak Test):</strong></span> </p>
<ul>
<li><p>장시간(2시간, 4시간, 혹은 그 이상) 동안 부하를 지속</p>
</li>
<li><p>메모리 누수(Memory Leak), 리소스 고갈, 디스크 공간 부족 등 장기 운영 시에만 드러나는 문제를 찾아내는 테스트</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>브레이크포인트 테스트 (Breakpoint Test):</strong></span> </p>
<ul>
<li><p>시스템이 과부하로 멈추거나 오류를 낼 때까지 부하를 지속적으로 증가시킴.</p>
</li>
<li><p>시스템의 물리적 한계점을 파악하여 용량 계획을 세우는 데 도움을 줌.</p>
<br>

</li>
</ul>
</li>
</ol>
<h3 id="span-stylecolor-slateblue1-3-부하를-사용하지-않는-성능-테스트span"><span style="color: SlateBlue">1-3. 부하를 사용하지 않는 성능 테스트</span></h3>
<p><strong>모든 성능 테스트가 동시 접속 부하를 필요로 하는 것은 아니다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>프로파일링 (Profiling):</strong></span> 단일 사용자/스레드로 특정 함수나 로직의 실행 시간을 분석한다.</p>
</li>
<li><p><span style="color: indianred"><strong>브라우저 성능 테스트:</strong></span> 백엔드 부하보다는 프론트엔드 최적화(렌더링 속도, 에셋 로딩 시간 등)에 집중한다. 이 경우 백엔드에 부하를 주입할 필요가 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>신세틱 모니터링 (Synthetic Monitoring):</strong></span> 운영 중인 시스템의 가용성을 확인하기 위해 주기적으로 가상의 트랜잭션을 발생시켜 모니터링한다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-성능-테스트-도구-생태계span"><span style='background-color: MediumAquamarine'>2. 성능 테스트 도구 생태계</span></h2>
<hr>

<p><strong>성능 테스트 도구는 매우 다양하며, 사용 방식과 목적에 따라 크게 4가지 카테고리로 분류할 수 있다.</strong></p>
<ol>
<li><p><span style="color: SlateBlue"><strong>벤치마킹 도구</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>대표 도구:</strong></span> <code>ab</code> (Apache Bench), <code>wrk</code></p>
</li>
<li><p><span style="color: indianred"><strong>특징:</strong></span> 단일 엔드포인트나 URL 리스트를 대상으로 단순한 부하를 발생시킨다.</p>
</li>
<li><p><span style="color: indianred"><strong>장단점:</strong></span> 사용이 매우 간편하고 결과가 터미널에 바로 출력되지만, 복잡한 시나리오 구성은 어렵다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>프로토콜 특화 도구</strong></span></p>
<ul>
<li><span style="color: indianred"><strong>특징:</strong></span> <code>gRPC</code>, <code>MQTT</code>, <code>Kafka</code> 등 특정 프로토콜 테스트에 최적화된 도구들이 존재한다.</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>GUI 기반 도구</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>대표 도구:</strong></span> JMeter</p>
</li>
<li><p><span style="color: indianred"><strong>특징:</strong></span> 가장 오래되고 널리 쓰이는 도구 중 하나다. GUI를 통해 테스트 단계를 클릭으로 구성할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>장단점:</strong></span> 코딩 없이 접근 가능하고 커뮤니티가 크지만, 스크립트 관리가 어렵고 리소스를 많이 차지할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>코드 기반(Scripting) 도구</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>대표 도구:</strong></span> k6 (JavaScript/Go), Locust (Python), Gatling (Scala/Java)</p>
</li>
<li><p><span style="color: indianred"><strong>특징:</strong></span> 개발자 친화적이며, 코드로 테스트 시나리오를 작성하므로 버전 관리(Git)가 용이하고 유연성이 매우 높다.</p>
</li>
<li><p><span style="color: indianred"><strong>참고:</strong></span> Artillery와 같이 YAML 설정을 사용하는 도구도 있지만, 스크립팅 언어만큼의 완벽한 유연성을 제공하지는 못할 수 있다.</p>
</li>
</ul>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-k6span"><span style='background-color: MediumAquamarine'>3. K6</span></h2>
<hr>

<p><strong>k6는 개발자 중심의 오픈 소스 부하 테스트 도구다.</strong></p>
<p>k6는 단순히 부하를 일으키는 도구를 넘어, 개발자 경험(DX, Developer Experience)을 최우선으로 설계된 오픈 소스 성능 테스트 프레임워크이며, Grafana Labs에 인수된 이후, 관측성(Observability) 생태계와 결합하며 더욱 강력해졌다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/e142e33f-1466-41d3-8287-1d22fd46d2f6/image.png" alt=""></p>
<p><span style="color: indianred"><strong>k6의 가장 큰 기술적 특징은 &quot;작성은 쉬운 JavaScript로, 실행은 강력한 Go로&quot; 수행한다는 점이다.</strong></span></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>스크립팅의 용이성:</strong></span> </p>
<ul>
<li><p>개발자에게 익숙한 ES6 자바스크립트 문법을 사용하여 테스트 시나리오를 작성한다. </p>
</li>
<li><p>덕분에 별도의 도구 사용법을 익힐 필요 없이, 복잡한 비즈니스 로직을 코드로 유연하게 구현할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>고성능 실행 엔진:</strong></span> </p>
<ul>
<li><p>*<em>작성된 스크립트는 내부적으로 내장된 Go 기반의 JS 인터프리터(goja)를 통해 실행된다. *</em></p>
</li>
<li><p>이는 Node.js나 브라우저 엔진 위에서 구동되는 것이 아니기 때문에 오버헤드가 극도로 낮다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>스레드 대신 가상 사용자(VU):</strong></span></p>
<ul>
<li><p>기존의 JMeter가 OS Thread를 사용하여 메모리 소비가 컸던 반면, k6는 Go의 Goroutine을 활용하여 가상 사용자(Virtual Users)를 구현한다. </p>
</li>
<li><p>덕분에 단일 인스턴스에서 수만 명의 동시 접속자를 시뮬레이션하더라도 메모리 사용량이 현저히 적다.</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/ef4e6712-1d64-4de7-aede-3eddd3eb54e7/image.png" alt=""></p>
<p><strong>첨부된 이미지가 보여주듯, k6는 소프트웨어 개발 수명 주기(SDLC) 전반에 걸쳐 성능을 관리할 수 있도록 설계되었다.</strong></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Pre-production (Proactive):</strong></span> 배포 전 단계에서 가상 사용자 트래픽(Virtual User Traffic)을 통해 잠재적인 병목 구간을 사전에 발견하고 해결한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Production (Reactive):</strong></span> 실제 운영 환경의 모니터링 데이터(Real User Traffic)와 테스트 결과를 비교 분석한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Grafana와의 결합:</strong></span> </p>
<ul>
<li>*<em>k6는 테스트 결과를 단순히 콘솔에 찍는 것에 그치지 않고, Grafana 플랫폼으로 전송하여 메트릭(Metrics), 로그(Logs), 트레이스(Traces)와 함께 통합 분석할 수 있게 해준다. *</em></li>
</ul>
</li>
</ul>
<br>


<p><span style="color: indianred"><strong>또한, k6의 가장 강력한 장점은 하나의 테스트 스크립트를 수정 없이 다양한 환경에서 실행할 수 있다는 점이다.</strong></span></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Local (로컬):</strong></span> 개발자 개인 PC나 단일 서버에서 실행한다. k6 run 명령어를 사용한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Distributed (분산):</strong></span> Kubernetes 클러스터 등을 활용하여 여러 파드(Pod)에 부하를 분산시켜 대규모 테스트를 수행한다.</p>
</li>
<li><p><span style="color: SlateBlue"><strong>Cloud (클라우드):</strong></span> k6의 상용 클라우드 서비스(Grafana Cloud k6)를 이용하여 인프라 관리 없이 대규모 테스트를 수행하고 결과를 저장/분석한다.</p>
</li>
</ul>
<p><strong>이는 로컬 개발부터 대규모 프로덕션 테스트까지 매끄러운 확장을 가능하게 한다.</strong>
<br></p>
<p><span style="color: indianred"><strong>추가적으로, k6를 사용할 때 반드시 이해해야 할 점은 k6는 브라우저가 아니라는 것이다.</strong></span></p>
<p>k6는 프로토콜 레벨(HTTP 등)에서 요청을 보내고 응답을 측정하는 도구다. </p>
<p><strong>웹 페이지의 HTML을 가져오지만, 브라우저처럼 화면을 렌더링하거나 페이지 내의 JavaScript를 실행하지는 않는다.</strong></p>
<p><span style="color: indianred">*<em>따라서 정확한 페이지 로딩 속도(렌더링 포함)를 측정하고 싶다면, 최근 k6에 실험적으로 추가된 k6 Browser 모듈(Chrome DevTools Protocol 기반)을 활용하거나 다른 도구를 병행해야 한다. *</em></span></p>
<blockquote>
<p><strong>하지만 백엔드 API의 성능을 극한으로 테스트하는 목적이라면 k6의 기본 모드가 가장 효율적이고 강력한 선택지가 된다.</strong></p>
</blockquote>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-k6-실전-데모span"><span style='background-color: MediumAquamarine'>4. K6 실전 데모</span></h2>
<hr>


<p>해당 프레젠테이션의 데모는 피자 주문 및 추천 서비스인 &#39;Quick Pizza&#39; 애플리케이션을 대상으로 진행된다. </p>
<p>Docker Compose를 사용하여 애플리케이션, Grafana, Prometheus를 로컬 환경에 구성했다.</p>
<h3 id="span-stylecolor-slateblue4-1-기본-테스트-스크립트-구조-span"><span style="color: SlateBlue">4-1. 기본 테스트 스크립트 구조 </span></h3>
<p><strong>k6 스크립트는 자바스크립트(ES6) 기반이며, 크게 Imports, Options, Default Function 세 부분으로 구성된다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>Imports:</strong></span> <code>k6/http</code>와 같은 내장 모듈이나 외부 라이브러리를 불러온다.</p>
</li>
<li><p><span style="color: indianred"><strong>Options:</strong></span> 가상 사용자 수(VUs), 지속 시간(Duration) 등 테스트 실행 설정을 정의한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Default Function:</strong></span> 가상 사용자가 반복해서 수행할 실제 비즈니스 로직을 작성한다.</p>
</li>
</ul>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { sleep, check } from &#39;k6&#39;;

// 1. 설정 (Options): 부하 수준 정의
export const options = {
  vus: 5,           // 가상 사용자 5명
  duration: &#39;10s&#39;,  // 10초 동안 실행
};

// 2. 메인 로직 (Default Function): VU가 반복 실행할 시나리오
export default function () {
  const res = http.post(&#39;http://localhost:3333/pizza&#39;); // HTTP 요청 (POST)

  // 응답 확인 및 로깅 (선택 사항)
  console.log(`Response status: ${res.status}`);

  sleep(1); // 1초 대기 (Think Time)
}</code></pre>
<br>

<h3 id="span-stylecolor-slateblue4-2-부하-모델링-심화-span"><span style="color: SlateBlue">4-2. 부하 모델링 심화 </span></h3>
<p><strong>k6는 매우 정교한 부하 제어 기능을 제공한다. 상황에 따라 다양한 옵션을 적용할 수 있다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>A. Iterations 기반 실행</strong></span></p>
<ul>
<li><p>기본적으로 시간(Duration) 기반으로 실행하지만, iterations 옵션을 사용하여 총 실행 횟수를 지정할 수도 있다. CLI에서 플래그로 덮어쓰는 것도 가능하다.</p>
<pre><code class="language-bash"># 5명의 VU가 협력하여 총 20번의 시나리오를 수행하고 종료
k6 run --iterations 20 --vus 5 script.js</code></pre>
<br>


</li>
</ul>
</li>
</ul>
<ul>
<li><p><span style="color: indianred"><strong>B. 스테이지(Stages)를 이용한 램핑(Ramping)</strong></span></p>
<ul>
<li><p>실제 트래픽처럼 부하를 서서히 올리고(Ramp-up) 내리는(Ramp-down) 패턴을 만든다. </p>
</li>
<li><p>options 객체 내 stages 배열을 사용한다.</p>
<pre><code class="language-javascript">export const options = {
 stages: [
   { duration: &#39;5s&#39;, target: 5 },  // 0명에서 5명으로 5초 동안 증가 (Ramp-up)
   { duration: &#39;10s&#39;, target: 5 }, // 5명 유지 (Steady State - 안정성 확인)
   { duration: &#39;5s&#39;, target: 0 },  // 5명에서 0명으로 감소 (Ramp-down)
 ],
};</code></pre>
<br>


</li>
</ul>
</li>
</ul>
<ul>
<li><p><span style="color: indianred"><strong>C. 초당 요청 수(RPS) 제어: Constant Arrival Rate</strong></span></p>
<ul>
<li><p>일반적인 방식으로는 함수 실행 시간(응답 속도)에 따라 반복 횟수가 달라지므로 정확한 RPS(Requests Per Second)를 맞추기 어렵다. </p>
</li>
<li><p><strong>이를 위해 <code>scenarios</code>의 <code>constant-arrival-rate 실행기</code>를 사용한다.</strong></p>
<pre><code class="language-javascript">export const options = {
scenarios: {
  constant_request_rate: {
    executor: &#39;constant-arrival-rate&#39;,
    rate: 30,             // 초당 30회 반복 목표
    timeUnit: &#39;1s&#39;,       // 시간 단위
    duration: &#39;30s&#39;,      // 30초 동안 실행
    preAllocatedVUs: 50,  // 미리 할당할 VU 수
    maxVUs: 100,          // 최대 허용 VU 수
  },
},
};</code></pre>
</li>
<li><p><span style="color: indianred"><strong>주의:</strong></span> <strong>k6는 엄밀히 말해 초당 반복(Iteration) 수를 제어한다.</strong> 
시나리오 내에 요청이 1개라면 반복 수 = RPS가 되지만, 요청이 여러 개라면 계산이 필요하다.</p>
<br>

</li>
</ul>
</li>
</ul>
<h3 id="span-stylecolor-slateblue4-3-메트릭의-이해-span"><span style="color: SlateBlue">4-3. 메트릭의 이해 </span></h3>
<p><strong>k6는 테스트 중 발생하는 데이터를 4가지 유형의 메트릭으로 수집한다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>Counter:</strong></span> 누적 합계 (예: <code>http_reqs</code>)</p>
</li>
<li><p><span style="color: indianred"><strong>Gauge:</strong></span> 특정 시점의 값 (예: <code>vus</code>)</p>
</li>
<li><p><span style="color: indianred"><strong>Rate:</strong></span> 성공/실패 비율 (예: <code>checks</code>, <code>http_req_failed</code>)</p>
</li>
<li><p><span style="color: indianred"><strong>Trend:</strong></span> 통계적 분포 (예: <code>http_req_duration</code> - p95, p99 등)</p>
</li>
</ol>
<p><span style="color: indianred"><strong>커스텀 메트릭 (Custom Metrics) 이란?</strong></span></p>
<p><strong>추가적으로, 기본 메트릭 외에 비즈니스 로직에 맞는 데이터를 수집할 수 있다.</strong></p>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { Trend, Counter } from &#39;k6/metrics&#39;;

// 커스텀 메트릭 정의
const ingredientsTrend = new Trend(&#39;quickpizza_ingredients&#39;);
const pizzaCounter = new Counter(&#39;quickpizza_number_of_pizzas&#39;);

export default function () {
  const res = http.get(&#39;http://localhost:3333/pizza&#39;);

  // 응답받은 JSON 데이터 파싱
  const pizza = res.json();

  // 메트릭에 데이터 추가
  ingredientsTrend.add(pizza.ingredients.length); // 재료 개수 통계
  pizzaCounter.add(1);                            // 피자 개수 카운트
}</code></pre>
<br>

<h3 id="span-stylecolor-slateblue4-4-결과-출력과-시각화-span"><span style="color: SlateBlue">4-4. 결과 출력과 시각화 </span></h3>
<ol>
<li><p><span style="color: indianred"><strong>터미널 요약 및 파일 내보내기</strong></span></p>
<ul>
<li><p>테스트 종료 후 터미널에 출력되는 요약 리포트는 집계된 데이터다.</p>
</li>
<li><p>상세 분석을 위해 <code>handleSummary()</code> 함수를 이용해 JSON 파일로 내보낼 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>실시간 데이터 스트리밍</strong></span></p>
<ul>
<li><p>가장 강력한 기능은 <code>--out</code> 플래그를 사용하여 Raw Data를 시계열 데이터베이스로 실시간 전송하는 것이다.</p>
<pre><code class="language-bash"># Prometheus로 데이터 실시간 전송 (Remote Write 기능 사용)
 k6 run --out experimental-prometheus-rw script.js</code></pre>
</li>
<li><p>*<em>위 명령어를 실행하면 k6가 데이터를 Prometheus로 전송하고, 이를 Grafana 대시보드에서 실시간 그래프로 확인할 수 있다. *</em></p>
</li>
<li><p>스파이크 발생 지점이나 지연 시간 변화 추이를 시각적으로 파악하는 데 필수적이다.</p>
<br>

</li>
</ul>
</li>
</ol>
<h3 id="span-stylecolor-slateblue4-5-검증-checks와-thresholds-span"><span style="color: SlateBlue">4-5. 검증: Checks와 Thresholds </span></h3>
<p><strong>k6는 테스트 성공 여부를 판단하고 검증하기 위해 두 가지 방법을 제공한다.</strong></p>
<p><span style="color: indianred">*<em>1. Checks *</em></span>
단순 확인용(Boolean) 검증으로, 실패해도 테스트가 중단되거나 실패로 기록되지 않는다.</p>
<pre><code class="language-javascript">import { check } from &#39;k6&#39;;

export default function (res) {
  // 응답 코드가 200인지 확인 (실패해도 테스트는 계속 진행됨)
  check(res, { &#39;is status 200&#39;: (r) =&gt; r.status === 200 });
}</code></pre>
<p><span style="color: indianred">*<em>2. Thresholds (임계값) *</em></span>
테스트의 성공/실패 기준을 정의하며, 이를 위반하면 k6는 실패(Non-zero) 종료 코드를 반환하므로 CI/CD 파이프라인에서 매우 중요하다.</p>
<pre><code class="language-javascript">export const options = {
  thresholds: {
    // 에러율(http_req_failed)이 1% 미만이어야 함
    &#39;http_req_failed&#39;: [&#39;rate&lt;0.01&#39;], 

    // 응답 시간(http_req_duration)의 95%가 500ms 이내여야 함
    &#39;http_req_duration&#39;: [&#39;p95&lt;500&#39;],

    // 커스텀 메트릭 평균이 2 미만이어야 함
    &#39;quickpizza_ingredients&#39;: [&#39;avg&lt;2&#39;], 
  },
};</code></pre>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-결론span"><span style='background-color: MediumAquamarine'>5. 결론</span></h2>
<hr>

<p>성능 테스트는 시스템의 신뢰성을 보장하기 위한 선택이 아닌 필수 과정이다. </p>
<p><strong>k6는 강력한 스크립팅 기능과 확장성, 그리고 개발자 친화적인 경험(DX)을 제공하여 이를 효율적으로 지원한다.</strong></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 86)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-86-78f3qstq</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-86-78f3qstq</guid>
            <pubDate>Sat, 07 Feb 2026 03:15:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 86 - Tools To Make Your Terminal DevOps and Kubernetes Friendly **</p>
<p><strong>프레젠테이션에서는 엔지니어에게 터미널은 하루의 대부분을 보내는 집과 같은 곳이기때문에,  이 공간을 더 효율적이고 예쁘게 만드는 것은 생산성과 직결된다고 한다.</strong></p>
<p><span style="color: indianred"><strong>따라서, Zsh, Terraform, Git, 그리고 Kubernetes 환경에서 사용할 수 있는 유용한 오픈 소스 및 상용 도구들을 제시한다.</strong></span></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-zsh-z-shell-관련-도구span"><span style='background-color: MediumAquamarine'>1. Zsh (Z Shell) 관련 도구</span></h2>
<hr>

<p><strong>Zsh는 유닉스 기반의 쉘로, Bash와 유사하지만 훨씬 강력한 기능과 생산성 향상 도구를 제공하여 최근 개발자들 사이에서 인기가 높다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/2a454b39-abba-4d3d-abab-7c91d1d0e0dc/image.png" alt=""></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Oh My Zsh</strong></span></p>
<ul>
<li><p><strong>Zsh 구성을 관리하기 위한 커뮤니티 기반 오픈 소스 프레임워크</strong></p>
</li>
<li><p><span style="color: indianred"><strong>특징:</strong></span> 수많은 테마와 플러그인을 제공하여 사용자 경험을 극대화</p>
</li>
<li><p><span style="color: indianred"><strong>설치:</strong></span> Zsh, curl, git이 사전에 설치되어 있어야 하며 <code>curl</code>, <code>wget</code>, <code>fetch</code> 등을 통해 설치 가능</p>
</li>
<li><p><span style="color: indianred"><strong>사용법:</strong></span> 홈 디렉토리의 <code>.zshrc</code> 파일을 수정하여 플러그인(예: git)을 추가하거나 테마를 변경할 수 있다. 다양한 테마의 스크린샷과 색상 정보를 확인하고 선택하는 것을 추천한다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>zsh-syntax-highlighting</strong></span></p>
<ul>
<li><p><strong>명령어를 입력하는 동안 실시간으로 구문을 강조해 주는 플러그인</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 명령어, 옵션, 인수, 변수 등을 시각적으로 구분해 준다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 오타나 문법 오류를 입력 단계에서 즉시 파악할 수 있어 명령어 구조를 이해하는 데 도움을 준다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>zsh-autosuggestions</strong></span></p>
<ul>
<li><p><strong>사용자의 명령어 히스토리를 기반으로 지능적인 자동 완성을 제안하는 플러그인</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 이전에 사용했던 명령어를 기억하여, 앞부분만 입력해도 회색 텍스트로 전체 명령어를 제안한다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span>  긴 명령어를 반복적으로 입력해야 할 때 Tab 키만 누르면 완성되므로 작업 속도가 빨라진다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-terraform-관련-도구span"><span style='background-color: MediumAquamarine'>2. Terraform 관련 도구</span></h2>
<hr>

<p><strong>Terraform은 HashiCorp의 IaC(Infrastructure as Code) 도구이며, Terragrunt는 Terraform 모듈을 DRY(Don&#39;t Repeat Yourself)하게 관리해 주는 래퍼 도구다.</strong></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>tfswitch &amp; tgswitch</strong></span></p>
<ul>
<li><p><strong>다양한 프로젝트나 환경에 따라 서로 다른 버전의 Terraform이나 Terragrunt를 사용해야 할 때 유용</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 시스템에 설치된 여러 버전 중 원하는 버전을 손쉽게 전환할 수 있게 해준다.</p>
<br>


</li>
</ul>
</li>
</ul>
<ul>
<li><p><span style="color: SlateBlue"><strong>Infracost</strong></span></p>
<ul>
<li><p><strong>IaC 코드의 비용을 추정하고 추적하는 도구</strong></p>
</li>
<li><img src="https://velog.velcdn.com/images/taekyu_/post/93cb17f7-360c-4d15-81b0-d523454d0e84/image.png" width="60%" height="n%">
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 구성 파일을 분석하여 실시간 클라우드 비용 견적을 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>활용:</strong></span> VS Code 확장 프로그램으로도 제공되며, 코드 작성 단계에서 예산을 고려한 의사결정을 돕는다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Trivy (구 tfsec 통합)</strong></span></p>
<ul>
<li><strong>보안 스캔 도구로, 기존의 tfsec이 Trivy로 통합</strong></li>
</ul>
</li>
</ul>
<ul>
<li><img src="https://velog.velcdn.com/images/taekyu_/post/6b6903c4-91cd-4d98-816c-f48d95ed4302/image.png" width="50%" height="n%">
</li>
<li><p><span style="color: indianred"><strong>변화:</strong></span> tfsec 제작사는 도구 통합으로 인해 Trivy로의 마이그레이션을 권장한다.</p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> OS 패키지, 소프트웨어 의존성, CVE(알려진 취약점), IaC(기존 tfsec 기능), 민감 정보(Secret), 라이선스 등을 포괄적으로 스캔한다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 코드의 잠재적 취약점과 모범 사례 위반을 식별하고 개선 방안을 제안한다.</p>
<br>

</li>
</ul>
<ul>
<li><p><span style="color: SlateBlue"><strong>terraform-docs</strong></span></p>
<ul>
<li><p><strong>Terraform 코드를 기반으로 문서 생성을 자동화해 주는 CLI 유틸리티</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 구성 파일에서 정보(<code>Requirements, Providers, Inputs, Outputs</code>)를 추출하여 마크다운 테이블 등 다양한 형식의 깔끔한 문서를 만들어준다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-git-관련-도구span"><span style='background-color: MediumAquamarine'>3. git 관련 도구</span></h2>
<hr>

<p><strong>버전 관리를 위한 Git 사용 시 생산성을 높여주는 도구들이다.</strong></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Git Aliases (별칭)</strong></span></p>
<ul>
<li><p><strong>자주 사용하는 긴 명령어를 짧은 단축어로 정의하여 사용</strong></p>
</li>
<li><p><span style="color: indianred"><strong>예시:</strong></span> git status 대신 gs를 입력하도록 설정하여 타이핑 수고를 덜고 속도를 높인다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>GitLens</strong></span></p>
<ul>
<li><p><strong>VS Code용 확장 프로그램으로 코드의 히스토리를 추적하는 데 최적화</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 코드 에디터 내에서 각 라인을 누가, 언제 수정했는지(Blame 기능) 바로 보여준다.</p>
</li>
<li><p><span style="color: indianred"><strong>상세:</strong></span> 커밋 메시지와 작성자 정보를 시각적으로 표시하며, 마우스 오버 시 해당 히스토리 링크로 연결되어 변경 이력을 쉽게 파악할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Git Graph</strong></span></p>
<ul>
<li><p><strong>VS Code용 확장 프로그램으로, Git 히스토리를 그래프 형태로 시각화</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 복잡한 브랜치 구조와 커밋 흐름을 시각적으로 표현해 준다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span> CLI만으로는 히스토리 파악이 어려운 초심자에게 현재 브랜치 위치와 흐름을 이해하는 데 큰 도움을 준다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-kubernetes-관련-도구span"><span style='background-color: MediumAquamarine'>4. Kubernetes 관련 도구</span></h2>
<hr>

<p><strong>복잡한 쿠버네티스 시스템을 효율적으로 관리하기 위해 개발된 다양한 도구들이다.</strong></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Kubernetes Aliases</strong></span></p>
<ul>
<li><p><strong>kubectl 명령어를 매번 입력하는 번거로움을 줄이기 위해 사용</strong></p>
</li>
<li><pre><code class="language-bash">alias k=&#39;kubectl&#39;
 alias kpod=&#39;kubectl get pods -A&#39;
 alias knode=&#39;kubectl get nodes&#39;
 alias kdesp=&#39;kubectl describe pod&#39;
 alias kdp=&#39;kubectl delete pod&#39;
 alias=&#39;kubectl get deployments&#39;</code></pre>
</li>
<li><p><span style="color: indianred"><strong>활용:</strong></span> kubectl을 k와 같은 한 글자 별칭으로 설정하여 명령어 입력 속도를 비약적으로 높인다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>kube-ps1</strong></span></p>
<ul>
<li><p><strong>현재 사용 중인 클러스터 컨텍스트와 네임스페이스를 프롬프트에 시각적으로 표시</strong></p>
</li>
<li><p><span style="color: indianred"><strong>필요성:</strong></span> 여러 클러스터를 관리할 때, 실수로 엉뚱한 클러스터(예: 프로덕션)에 명령을 내리는 치명적인 실수를 방지해 준다.</p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 터미널 프롬프트에 현재 컨텍스트와   - 네임스페이스 정보를 색상과 함께 보여주어 직관적인 확인이 가능하다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>kubecolor</strong></span></p>
<ul>
<li><p><strong>kubectl 명령어의 출력 결과에 색상을 입혀 가독성을 높인다.</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 밋밋한 텍스트 출력 대신 중요 정보를 색상으로 구분하여, 원하는 정보를 빠르게 식별할 수 있게 돕는다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>kubectx &amp; kubens</strong></span></p>
<ul>
<li><p><strong>컨텍스트와 네임스페이스 전환을 획기적으로 간편하게 만들어주는 도구</strong></p>
</li>
<li><p><span style="color: indianred"><strong>kubectx:</strong></span> 복잡한 명령어 없이 kubectx &lt;컨텍스트명&gt; 입력만으로 클러스터 간 전환이 가능하다.</p>
</li>
<li><p><span style="color: indianred"><strong>kubens:</strong></span> kubens &lt;네임스페이스명&gt; 입력만으로 네임스페이스를 즉시 변경할 수 있다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>K9s</strong></span></p>
<ul>
<li><p>터미널 기반의 UI(TUI)를 제공하는 쿠버네티스 관리 대시보드다.</p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> CLI 환경에서도 시각적인 인터페이스를 통해 클러스터 상태를 모니터링하고 리소스를 관리할 수 있다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Lens</strong></span></p>
<ul>
<li><p><strong>쿠버네티스 관리를 위한 데스크톱 GUI 애플리케이션</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 노드, 파드 등 모든 리소스를 그래픽 인터페이스로 보여주며, 마우스 클릭만으로 상세 정보를 확인하고 관리할 수 있는 강력한 도구다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Popeye</strong></span></p>
<ul>
<li><p><strong>쿠버네티스 클러스터 분석 및 살균(Sanitizer) 도구</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 클러스터 전체를 스캔하여 구성 오류, 리소스 비효율성, 보안 위협, 모범 사례 위반 사항 등을 찾아내고 보고한다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>kube-capacity</strong></span></p>
<ul>
<li><p><strong>클러스터 리소스 사용량과 용량을 분석하는 도구</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> CPU와 메모리의 요청(Requests) 및 제한(Limits) 설정값과 실제 사용량을 비교하여 보여주며, 노드 및 파드 단위의 용량 정보를 제공한다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>kube-shell</strong></span></p>
<ul>
<li><p><strong>쿠버네티스 CLI를 위한 통합 쉘</strong></p>
</li>
<li><p><span style="color: indianred"><strong>기능:</strong></span> 명령어 자동 완성 및 제안 기능을 제공하여, 사용 가능한 명령어를 쉽게 찾고 실행할 수 있도록 돕는다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-결론span"><span style='background-color: MediumAquamarine'>5. 결론</span></h2>
<hr>

<p>*<em>이미 시중에는 데브옵스 여정을 돕고 생산성을 높여줄 수 있는 수많은 도구가 존재한다. *</em></p>
<p>이러한 도구들을 적극적으로 활용하여 불필요한 반복 작업을 줄이고, 더 중요하고 가치 있는 일에 집중하며, 결과적으로 엔지니어로서의 삶을 더 편안하게 만드는 것이 중요하다.</p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 85)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-85-4srzfbxp</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-85-4srzfbxp</guid>
            <pubDate>Sun, 01 Feb 2026 05:59:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 85 - Reuse, Don&#39;t Repeat - Creating an Infrastructure as Code Module Library **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-왜-인프라-코드를-모듈화해야-하는가-span"><span style='background-color: MediumAquamarine'>1. 왜 인프라 코드를 모듈화해야 하는가 </span></h2>
<hr>

<p>IaC를 작성하다 보면 VM, 스토리지 계정 등 동일한 리소스를 반복적으로 정의하게 된다.</p>
<p><strong>매번 코드를 복사해서 붙여넣는 방식은 다음과 같은 문제를 야기한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>시간 낭비:</strong></span> 동일한 구성을 매번 새로 작성해야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>일관성 결여:</strong></span> 시간이 지남에 따라 초기에 작성한 코드와 나중에 작성한 코드가 달라져 관리 포인트가 늘어난다.</p>
</li>
</ul>
<hr>
<h3 id="span-stylecolor-slateblue1-1-모듈화의-4가지-핵심-이점span"><span style="color: SlateBlue"><strong>1-1. 모듈화의 4가지 핵심 이점</strong></span></h3>
<ul>
<li><p><span style="color: indianred"><strong>재사용성:</strong></span> 한 번 잘 만들어진 모듈을 여러 프로젝트에서 Parameter만 바꿔가며 반복 사용할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>복잡성 추상화:</strong></span> </p>
<ul>
<li><p>클라우드 리소스의 복잡한 설정(<strong><code>네트워크 피어링, 암호화 세팅 등</code></strong>)을 모듈 내부로 숨기고, 사용자에게는 이름과 위치 같은 단순한 인터페이스만 제공할 수 있다.</p>
</li>
<li><p>이를 통해 클라우드 전문 지식이 없는 팀원도 손쉽게 인프라를 배포할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>표준 준수:</strong></span> 회사의 보안 정책(예: HTTPS 필수, 퍼블릭 액세스 차단)을 모듈 내부에 하드코딩함으로써, 사용자가 모듈을 쓰기만 해도 자동으로 규정을 준수하도록 강제할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>지식 공유:</strong></span> 인프라 아키텍트의 노하우와 모범 사례를 모듈에 담아 조직 전체에 배포할 수 있다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-도구별-모듈-구현-방법span"><span style='background-color: MediumAquamarine'>2. 도구별 모듈 구현 방법</span></h2>
<hr>

<p><strong>대부분의 IaC 도구는 이미 모듈화 기능을 제공한다. 세션에서는 Bicep, Terraform, Pulumi를 예시로 들었다.</strong></p>
<h3 id="span-stylecolor-slateblue2-1-azure-bicepspan"><span style="color: SlateBlue">2-1. Azure Bicep</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>구현:</strong></span> 일반적인 <code>.bicep</code> 파일이 곧 모듈이 된다.</p>
</li>
<li><p><span style="color: indianred"><strong>인터페이스 설계:</strong></span> <code>param</code>으로 입력을 받고 output으로 결과를 내보낸다.</p>
</li>
<li><p><span style="color: indianred"><strong>내부 로직:</strong></span> 리소스 생성 시 <code>uniqueString()</code> 함수 등을 사용해 네이밍 규칙을 자동화하거나, 특정 속성(예: <code>supportsHttpsTrafficOnly: true</code>)을 강제하여 사용자에게 보이지 않게 처리한다.</p>
<br>


</li>
</ul>
<ul>
<li><span style="color: indianred"><strong>소비:</strong></span> <code>module</code> 키워드를 사용하여 해당 bicep 파일을 참조하고 파라미터를 전달한다.</li>
</ul>
<h3 id="span-stylecolor-slateblue2-2-terraformspan"><span style="color: SlateBlue">2-2. Terraform</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>구조:</strong></span> 일반적으로 <code>main.tf</code> (리소스 로직), <code>variables.tf</code> (입력 변수), <code>outputs.tf</code> (출력값)로 파일을 분리하여 관리한다.</p>
</li>
<li><p><span style="color: indianred"><strong>소비:</strong></span> <code>module</code> 블록을 선언하고 source 속성에 모듈이 위치한 경로(로컬 또는 원격)를 지정하여 사용한다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue2-3-pulumispan"><span style="color: SlateBlue">2-3. Pulumi</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>특징:</strong></span> Python, C# 등 범용 프로그래밍 언어를 사용하므로, 해당 언어의 클래스나 함수 구조를 그대로 활용한다.</p>
</li>
<li><p><span style="color: indianred"><strong>구현:</strong></span> <code>&#39;Component Resource&#39;</code> 클래스를 정의하여 리소스 생성 로직을 캡슐화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>소비:</strong></span> 메인 코드에서 해당 클래스의 인스턴스를 생성하는 방식으로 모듈을 호출한다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-좋은-모듈을-만드는-7원칙span"><span style='background-color: MediumAquamarine'>3. 좋은 모듈을 만드는 7원칙</span></h2>
<hr>

<p><strong>단순히 코드를 분리한다고 좋은 모듈이 되는 것은 아니다. 지속 가능한 라이브러리를 구축하기 위해 다음 원칙을 따라야 한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/4d3b8986-002f-4b04-92db-e0ea45a54ec6/image.png" alt=""></p>
<ol>
<li><p><span style="color: SlateBlue"><strong>단일 책임 원칙:</strong></span></p>
<ul>
<li><p><strong>슈퍼 모듈을 피해야 한다.</strong> 스토리지 모듈은 스토리지 생성에만 집중해야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>예외:</strong></span> Kubernetes 클러스터처럼 그 자체로 복합적인 리소스는 묶어서 배포하되, 네트워크처럼 외부 의존성이 있는 리소스는 분리하여 주입받는 것이 좋다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>단순한 인터페이스:</strong></span></p>
<ul>
<li><p>모든 속성을 파라미터로 노출하지 말아야 한다. <strong>모듈의 목적은 복잡성 은폐이다.</strong></p>
</li>
<li><p>사용자가 고민할 필요가 없는 설정(예: 보안 표준)은 내부적으로 고정하고, 꼭 필요한 값만 입력받는다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>가치 창출:</strong></span></p>
<ul>
<li><p>단순히 리소스 그룹처럼 옵션이 거의 없는 리소스를 감싸는 Wrapper 모듈은 의미가 없다. </p>
</li>
<li><p><strong>복잡성을 줄이거나 표준을 강제하는 가치가 있어야 한다.</strong></p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>버전 관리:</strong></span></p>
<ul>
<li><p>모듈 코드를 수정했을 때 기존 사용자의 배포가 깨지면 안 된다. </p>
</li>
<li><p>시맨틱 버저닝을 도입하여 사용자가 업그레이드 시점을 선택할 수 있게 해야 한다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>접근성:</strong></span></p>
<ul>
<li>사용자가 모듈을 쉽게 가져다 쓸 수 있어야 한다. </li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>유지보수:</strong></span></p>
<ul>
<li><p>클라우드 공급자의 신규 기능이나 변경 사항을 제때 반영해야 한다. </p>
</li>
<li><p>방치된 모듈은 결국 버려진다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>문서화 및 홍보:</strong></span></p>
<ul>
<li><p>사용법이 문서화되지 않은 모듈은 아무도 쓰지 않는다. </p>
</li>
<li><p>또한, 모듈의 존재와 사용 이점을 조직 내에 적극적으로 알려야 한다.</p>
</li>
</ul>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-모듈-배포-및-관리-전략span"><span style='background-color: MediumAquamarine'>4. 모듈 배포 및 관리 전략</span></h2>
<hr>

<p>*<em>로컬 경로(<code>./modules/storage</code>)를 참조하는 방식은 협업 환경에서 확장성이 없다. *</em></p>
<p><span style="color: indianred"><strong>각 도구별로 제공하는 중앙 저장소 기능을 활용하여 단일 진실 공급원을 구축해야 한다.</strong></span></p>
<ul>
<li><p><span style="color: SlateBlue"><strong>Azure (Bicep/ARM):</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>Template Specs:</strong></span> Azure 리소스로 템플릿을 저장하고 버전 관리할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>Bicep Registry:</strong></span> Azure Container Registry(ACR)를 OCI 호환 저장소로 활용하여 모듈을 배포할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Terraform:</strong></span></p>
<ul>
<li>Git 리포지토리를 직접 참조하거나, Terraform Registry (Terraform Cloud/Enterprise)를 사용한다.</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>Pulumi:</strong></span></p>
<ul>
<li>프로그래밍 언어 기반이므로 npm (Node.js), PyPI (Python), NuGet (.NET) 등 언어별 패키지 매니저를 통해 배포하고 설치한다. </li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>AWS CloudFormation:</strong></span></p>
<ul>
<li>CloudFormation Registry를 통해 모듈을 등록하고 관리한다.</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-정리span"><span style='background-color: MediumAquamarine'>5. 정리</span></h2>
<hr>

<p>*<em>인프라 코드 모듈화는 단순한 코드 정리를 넘어, 조직의 인프라 거버넌스를 기술적으로 구현하는 핵심 전략이다. *</em></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 84)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-84-ttzx8w7x</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-84-ttzx8w7x</guid>
            <pubDate>Sun, 01 Feb 2026 03:44:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 84 - Hacking Kubernetes For Beginners **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-쿠버네티스-해킹span"><span style='background-color: MediumAquamarine'>1. 쿠버네티스 해킹</span></h2>
<hr>

<p>*<em>해킹이란 마법이 아니라, 설계자가 의도하지 않았던 무언가를 활용하는 창의적 접근이다. *</em></p>
<p>해당 프레젠테이션은 쿠버네티스 클러스터를 공격하는 5가지 주요 시나리오를 통해 공격자의 관점과 방어자의 대응 전략을 살펴본다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/c06ee6c8-923d-4c75-a3f8-49ff1d003e8b/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>시나리오1:</strong></span> <strong>Kubelet 해킹</strong></p>
</li>
<li><p><span style="color: indianred"><strong>시나리오2:</strong></span> <strong>서비스 및 웹 애플리케이션 해킹</strong></p>
</li>
<li><p><span style="color: indianred"><strong>시나리오3:</strong></span> 컨테이너 탈출</p>
</li>
<li><p><span style="color: indianred"><strong>시나리오4:</strong></span> 이미지 레지스트리 오염</p>
</li>
<li><p><span style="color: indianred"><strong>시나리오5:</strong></span> 악의적인 관리자와 특권 파드</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-시나리오1-kubelet-해킹span"><span style='background-color: MediumAquamarine'>2. 시나리오1: Kubelet 해킹</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-정상적인-흐름span"><span style="color: SlateBlue">2-1. 정상적인 흐름</span></h3>
<p>관리자가 kubectl을 사용하여 파드에 접속하려 할 때, 요청은 
<span style="color: indianred"><strong><code>API Server -&gt; etcd(검증) -&gt; Kubelet(노드)</code></strong></span> 순으로 전달된다. </p>
<p><strong>Kubelet은 각 워커 노드의 문지기 역할을 하며 파드와 컨테이너를 관리한다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue2-2-red-team-공격-방법span"><span style="color: SlateBlue">2-2. Red Team: 공격 방법</span></h3>
<p>공격자는 API 서버를 거치지 않고, 워커 노드의 <strong>Kubelet 포트(기본 10250)</strong>로 직접 통신을 시도한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>공격 기법:</strong></span> <strong><code>curl -k https://&lt;node-ip&gt;:10250/pods</code></strong> 등과 같이 비인증 요청을 보낸다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span> Kubelet이 적절히 보호되지 않은 경우, 공격자는 인증 없이 파드 목록을 조회하거나 exec API를 통해 컨테이너 내부로 진입하여 명령어를 실행할 수 있다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue2-3-blue-team-방어-및-조치-전략span"><span style="color: SlateBlue">2-3. Blue Team: 방어 및 조치 전략</span></h3>
<p>강연에서는 보안 담당자가 파라미터를 설정했다고 언급했다. </p>
<p><strong>이를 구체적으로 구현하기 위해서는 Kubelet 설정 파일(<code>/var/lib/kubelet/config.yaml</code>)을 다음과 같이 수정해야 한다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>익명 인증 비활성화:</strong></span> 누구나 접근하지 못하도록 설정</p>
<ul>
<li><code>authentication.anonymous.enabled: false</code></li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>Kubelet 권한 부여 모드 설정:</strong></span> 모든 요청이 API 서버를 통해 권한 검사를 받도록 설정</p>
<ul>
<li><code>authorization.mode: Webhook</code></li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>인증서 기반 접근 제어:</strong></span> 클라이언트 인증서(x509)가 있는 요청만 수락하도록 설정한다.</p>
</li>
<li><p><span style="color: indianred"><strong>네트워크 정책 (NetworkPolicy):</strong></span> 워커 노드의 10250 포트에 대한 접근을 API 서버 등 신뢰할 수 있는 소스로만 제한하는 방화벽 규칙을 적용한다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-시나리오2-서비스-및-웹-애플리케이션-해킹span"><span style='background-color: MediumAquamarine'>3. 시나리오2: 서비스 및 웹 애플리케이션 해킹</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue3-1-정상적인-흐름span"><span style="color: SlateBlue">3-1. 정상적인 흐름</span></h3>
<p>사용자가 웹사이트에 접속하면 
<span style="color: indianred"><strong><code>Ingress -&gt; Service -&gt; Pod(Frontend)</code></strong></span> 순으로 트래픽이 전달된다. </p>
<p>프론트엔드에서 User ID를 입력하면 백엔드 DB와 통신하여 인증을 수행한다.
<br></p>
<h3 id="span-stylecolor-slateblue3-2-red-team-공격-방법span"><span style="color: SlateBlue">3-2. Red Team: 공격 방법</span></h3>
<p>공격자는 웹 애플리케이션의 입력 필드(User ID 창)에 정상적인 ID 대신 악의적인 명령어를 주입한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>공격 기법:</strong></span> SQL Injection 또는 Command Injection </p>
<ul>
<li><code>; cat /etc/passwd</code> 또는 MySQL 제어 명령</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span> 애플리케이션의 취약점을 통해 백엔드 데이터베이스를 탈취하거나, Reverse Shell을 실행하여 컨테이너 내부 쉘 권한을 획득한다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue3-3-blue-team-방어-및-조치-전략span"><span style="color: SlateBlue">3-3. Blue Team: 방어 및 조치 전략</span></h3>
<p>강연에서는 코드를 수정하고 출력 제어를 추가했다고 했다. </p>
<p><strong>이는 애플리케이션 레벨과 인프라 레벨의 보안이 모두 필요하다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>입력 값 검증:</strong></span> 애플리케이션 코드 단계에서 사용자 입력값에 대한 엄격한 타입 체크와 특수문자 필터링을 수행한다. (Secure Coding)</p>
</li>
<li><p><span style="color: indianred"><strong>WAF (Web Application Firewall):</strong></span> Ingress 단계에서 ModSecurity나 AWS WAF 등을 연동하여 SQL Injection 등의 잘 알려진 공격 패턴을 차단한다.</p>
</li>
<li><p><span style="color: indianred"><strong>최소 권한 원칙:</strong></span> 만약 뚫리더라도 피해를 최소화하기 위해 컨테이너를 Non-Root 사용자로 실행하고, 파일 시스템을 Read-Only로 설정한다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-시나리오3-컨테이너-탈출span"><span style='background-color: MediumAquamarine'>4. 시나리오3: 컨테이너 탈출</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue4-1-정상적인-흐름span"><span style="color: SlateBlue">4-1. 정상적인 흐름</span></h3>
<p><strong>공격자가 시나리오 2를 통해 컨테이너 내부 쉘을 획득했다고 가정한다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue4-2-red-team-공격-방법span"><span style="color: SlateBlue">4-2. Red Team: 공격 방법</span></h3>
<p><strong>공격자는 먼저 정찰을 수행한다. <code>id</code> 명령어로 루트 권한인지 확인하고, <code>ls /</code> 등을 통해 도커 환경임을 파악한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>공격 기법:</strong></span> 컨테이너가 과도한 권한을 가지고 있거나 커널 취약점이 있는 경우, 특수 제작된 스크립트를 실행하여 컨테이너 격리를 뚫고 호스트 또는 워커 노드의 쉘을 획득한다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span> 워커 노드의 루트 권한을 장악하여 해당 노드의 모든 컨테이너를 제어한다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue4-3-blue-team-방어-및-조치-전략span"><span style="color: SlateBlue">4-3. Blue Team: 방어 및 조치 전략</span></h3>
<p>강연에서는 컨테이너 보안 파라미터를 설정했다고 했다. <strong>이는 SecurityContext와 런타임 보안을 의미한다.</strong></p>
<ol>
<li><p><span style="color: indianred"><strong>SecurityContext 강화:</strong></span> 파드 배포 시 다음 설정을 강제한다.</p>
<ul>
<li><p><code>allowPrivilegeEscalation: false</code> (권한 상승 방지)</p>
</li>
<li><p><code>privileged: false</code> (특권 모드 금지)</p>
</li>
<li><p><code>runAsNonRoot: true</code> (루트 사용자 실행 금지)</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>Linux Capabilities 제한:</strong></span> 컨테이너에 불필요한 리눅스 커널 권한을 제거한다. (<code>drop: [&quot;ALL&quot;]</code>)</p>
</li>
<li><p><span style="color: indianred"><strong>Seccomp 및 AppArmor 프로파일 적용:</strong></span> 컨테이너가 호스트 커널에 요청할 수 있는 System Call을 제한하여 탈출 가능성을 차단한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Sandbox 컨테이너:</strong></span> gVisor나 Kata Containers와 같은 샌드박스 기술을 사용하여 커널 레벨의 격리를 강화한다.</p>
</li>
</ol>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-시나리오4-이미지-레지스트리-오염span"><span style='background-color: MediumAquamarine'>5. 시나리오4: 이미지 레지스트리 오염</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue5-1-정상적인-흐름span"><span style="color: SlateBlue">5-1. 정상적인 흐름</span></h3>
<p><strong>Kubelet은 파드를 생성할 때 지정된 레지스트리에서 이미지를 Pull하여 컨테이너를 실행한다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue5-2-red-team-공격-방법span"><span style="color: SlateBlue">5-2. Red Team: 공격 방법</span></h3>
<p><strong>공격자가 레지스트리 접근 권한을 탈취하여 정상 이미지를 악성 코드가 심어진 이미지로 교체한다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>공격 기법:</strong></span> 이미지 내부에 백도어나 리버스 쉘 코드를 삽입하여 기존 태그로 덮어쓰기한다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span> 관리자가 정상적인 절차로 파드를 생성하더라도, 실행되는 컨테이너는 이미 오염된 상태이므로 공격자가 즉시 접속할 수 있다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue5-3-blue-team-방어-및-조치-전략span"><span style="color: SlateBlue">5-3. Blue Team: 방어 및 조치 전략</span></h3>
<p>강연에서는 이미지 스캔 및 해시 키 비교를 언급했다. 이를 자동화된 파이프라인으로 구축해야 한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>이미지 서명 및 검증:</strong></span> </p>
<ul>
<li><p>Cosign이나 Notary를 사용하여 이미지를 서명한다. </p>
</li>
<li><p>배포 시 Admission Controller(Kyverno, OPA Gatekeeper)가 서명이 유효한 이미지만 실행되도록 강제한다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>이미지 취약점 스캔:</strong></span> CI/CD 파이프라인 및 레지스트리(Harbor 등) 단계에서 Trivy, Clair 등을 사용해 이미지를 주기적으로 스캔한다.</p>
</li>
<li><p><span style="color: indianred"><strong>불변 태그 사용:</strong></span> 레지스트리 설정에서 한 번 푸시된 태그(예: v1.0)를 덮어쓰지 못하도록 설정하거나, 이미지 태그 대신 고유한 해시값(sha256:abcd...)을 사용하여 배포한다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-시나리오5-악의적인-관리자와-특권-파드span"><span style='background-color: MediumAquamarine'>6. 시나리오5: 악의적인 관리자와 특권 파드</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue6-1-정상적인-흐름span"><span style="color: SlateBlue">6-1. 정상적인 흐름</span></h3>
<p><strong>쿠버네티스 관리자 권한을 가진 사용자(혹은 탈취된 계정)가 파드를 생성한다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue6-2-red-team-공격-방법span"><span style="color: SlateBlue">6-2. Red Team: 공격 방법</span></h3>
<p>관리자 계정이라도 호스트 서버(마스터/워커 노드)의 루트 파일 시스템에는 직접 접근할 수 없다. 하지만 관리자는 파드를 생성할 권한이 있다.</p>
<ul>
<li><span style="color: indianred"><strong>공격 기법:</strong></span> 호스트의 루트 디렉토리(<code>/</code>)를 파드 내부의 볼륨으로 마운트(hostPath 방식 이용)하는 특권 파드를 생성한다.</li>
<li><pre><code class="language-yaml">volumes:
- name: host-root
  hostPath:
    path: /</code></pre>
</li>
<li><span style="color: indianred"><strong>결과:</strong></span> 파드 내부로 진입(<code>exec</code>)하면 마운트된 볼륨을 통해 호스트의 모든 파일(Shadow 파일, SSH 키 등)을 읽고 수정할 수 있어 호스트를 완전히 장악한다.</li>
</ul>
<br>

<h3 id="span-stylecolor-slateblue6-2-red-team-공격-방법span-1"><span style="color: SlateBlue">6-2. Red Team: 공격 방법</span></h3>
<p>강연에서는 관리자 권한 제한 및 모니터링을 언급했다. </p>
<p><strong>이는 50:50의 싸움이라고 했지만, 기술적으로 통제 가능하다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>Pod Security Admission (PSA) 적용:</strong></span> 네임스페이스 수준에서 <code>Restricted</code> 또는 <code>Baseline</code> 정책을 적용하여 <strong>hostPath 사용이나 privileged 모드를 원천 차단한다.</strong></p>
</li>
<li><p><span style="color: indianred"><strong>정책 엔진 도입 (Policy-as-Code):</strong></span> OPA Gatekeeper나 Kyverno를 사용하여 &quot;특정 그룹을 제외하고는 hostPath 마운트 금지&quot;와 같은 세밀한 정책을 강제한다.</p>
</li>
<li><p><span style="color: indianred"><strong>RBAC 최소화:</strong></span> </p>
<ul>
<li><p>파드 생성 권한은 생각보다 강력한 권한이다. </p>
</li>
<li><p>개발자에게는 파드 생성 권한 대신, 제한된 범위의 권한만 부여하거나 ArgoCD와 같은 GitOps 도구를 통해서만 배포하도록 통제한다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>Audit Logging:</strong></span> 쿠버네티스 Audit Log를 활성화하고, SIEM 시스템(Falcosecurity 등)과 연동하여 특권 파드 생성이 시도될 때 즉시 알람을 발생시킨다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine7-정리-span"><span style='background-color: MediumAquamarine'>7. 정리 </span></h2>
<hr>

<p>*<em>방어자의 업무는 매우 어렵다. *</em></p>
<p><strong>공격자는 수많은 취약점 중 단 하나만 성공시키면 클러스터를 장악할 수 있지만, 방어자는 모든 구멍을 막아야 하기 때문이다.</strong></p>
<p>오늘 살펴본 5가지 시나리오는 빙산의 일각이며, 쿠버네티스 보안을 강화하기 위해서는 &#39;Hacking Kubernetes&#39;, &#39;Container Security&#39;와 같은 전문 서적을 참고하고, 지속적으로 클러스터의 보안 태세를 점검하는 것이 필수적이다.</p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 83)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-83-8dcir6g6</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-83-8dcir6g6</guid>
            <pubDate>Sun, 01 Feb 2026 02:46:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 83 - Saving Cloud Costs Using Existing Prometheus Metrics **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-krr-kubernetes-resource-recommender-란span"><span style='background-color: MediumAquamarine'>1. KRR (Kubernetes Resource Recommender) 란?</span></h2>
<hr>

<p>최근 1~2년 사이 많은 기업들이 클라우드 비용 절감을 위해 온프레미스로 회귀하거나 하이브리드 클라우드 전략을 채택하고 있다. </p>
<p><span style="color: indianred"><strong>하지만 온프레미스는 확장성과 유지보수 측면에서 명확한 한계가 있다. 클라우드의 이점을 유지하면서 비용을 줄이는 방법은 없을까?</strong></span></p>
<p>연구에 따르면, 쿠버네티스 클러스터 비용의 69% 이상이 리소스 과다 할당으로 인해 낭비되고 있다고 한다. </p>
<p><span style="color: indianred"><strong>즉, 애플리케이션에 적절한 리소스(Request/Limit)만 할당해도 막대한 비용을 절감할 수 있다는 것이다.</strong></span></p>
<p>따라서 프레젠테이션에서는 기존 Prometheus 데이터를 활용하여 데이터 기반의 리소스 최적화 권장 사항을 제공하는 오픈 소스 도구, <strong>KRR(Kubernetes Resource Recommender)</strong>에 대해 제시한다.</p>
<hr>
<p><strong>KRR (Kubernetes Resource Recommender)이란, Robusta에서 개발한 KRR은 쿠버네티스 리소스(CPU/Memory)의 Request과 Limit 값을 최적화할 수 있도록 도와주는 오픈 소스 CLI 도구이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/95f63414-1184-45d0-8af9-a60d36eb28cf/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>에이전트리스 (No Agent):</strong></span> 클러스터에 별도의 에이전트를 지속적으로 실행할 필요가 없다.</p>
</li>
<li><p><span style="color: indianred"><strong>Prometheus 통합:</strong></span> 이미 수집된 Prometheus의 히스토리 데이터를 활용한다. 
즉, 추측이 아닌 실제 내 애플리케이션의 데이터를 기반으로 분석한다.</p>
</li>
<li><p><span style="color: indianred"><strong>즉시 실행:</strong></span> 한 번의 CLI 실행으로 즉각적인 결과를 얻을 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>확장성:</strong></span> Python 기반으로 작성되어 있어 알고리즘을 사용자가 직접 커스터마이징할 수 있다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-설치-및-실행-방법span"><span style='background-color: MediumAquamarine'>2. 설치 및 실행 방법</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue2-1-사전-요구-사항span"><span style="color: SlateBlue">2-1. 사전 요구 사항</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>Prometheus:</strong></span> 클러스터 메트릭을 수집 중이어야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>kube-state-metrics:</strong></span> KRR이 분석에 필요한 메트릭을 제공한다.</p>
<ul>
<li><strong>참고:</strong> <code>kube-prometheus-stack</code>을 사용 중이라면 이미 포함되어 있어 별도 설정이 필요 없다.<br>


</li>
</ul>
</li>
</ul>
<h3 id="span-stylecolor-slateblue2-2-실행-및-결과-분석span"><span style="color: SlateBlue">2-2. 실행 및 결과 분석</span></h3>
<p>KRR을 실행하면 지난 14일간의 데이터를 분석하여 다음과 같은 권장 사항 테이블을 출력한다.
<img src="https://velog.velcdn.com/images/taekyu_/post/0b1d577b-e28b-4d57-9af2-df88d1a87497/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>Pods/Old Pods:</strong></span> 현재 실행 중인 파드와 과거 파드 수.</p>
</li>
<li><p><span style="color: indianred"><strong>Container:</strong></span> 파드 내 각 컨테이너별로 분석 결과를 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Recommended Request/Limit:</strong></span> 데이터 기반으로 산출된 최적의 CPU/Memory 값.</p>
</li>
<li><p><span style="color: indianred"><strong>Diff:</strong></span> 현재 설정값과 권장 값의 차이를 보여준다. (예: +251m은 추가 필요, -925Mi는 절감 가능)</p>
</li>
</ul>
<p><strong>이 결과를 통해 사용자는 어떤 서비스가 과다 할당되었는지, 혹은 리소스 부족 위험이 있는지 한눈에 파악할 수 있다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-krr에서는-왜-cpu-limit을-설정하지-말아야-하는가span"><span style='background-color: MediumAquamarine'>3. KRR에서는 왜 CPU Limit을 설정하지 말아야 하는가</span></h2>
<hr>

<p><strong>KRR을 실행해보면 대부분의 권장 사항이 CPU Limit을 설정 해제(Unset)하라는 내용임을 알게 된다.</strong> </p>
<p>이는 많은 엔지니어들이 의아해하는 부분이지만, 첨부된 이미지를 통해 그 이유를 명확히 이해할 수 있다.
<img src="https://velog.velcdn.com/images/taekyu_/post/06178703-3630-4b25-aa97-80c51d318761/image.png" width="50%" height="n%"></p>
<p><strong>해당 이미지는 CPU Request와 Limit 설정 조합에 따른 파드의 동작 방식을 4분면으로 나누어 설명한다.</strong></p>
<ol>
<li><p><span style="color: SlateBlue"><strong>CPU Requests + CPU Limits (주황색 - 좌상단)</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>상태:</strong></span> Request와 Limit을 모두 설정한 경우.</p>
</li>
<li><p><span style="color: indianred"><strong>동작:</strong></span> Request만큼의 CPU는 보장받는다. 하지만 Limit을 초과하는 CPU 사용은 불가능하다.</p>
</li>
<li><p><span style="color: indianred"><strong>문제점:</strong></span> 노드에 유휴 CPU 자원이 남더라도, 파드는 Limit에 막혀 이를 사용할 수 없다. 이는 CPU Throttling을 유발하여 애플리케이션의 성능 저하(Latency 증가)로 이어진다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>CPU Requests + No CPU Limits (초록색 - 우상단, 권장)</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>상태:</strong></span> Request는 설정하고, Limit은 설정하지 않은 경우.</p>
</li>
<li><p><span style="color: indianred"><strong>동작:</strong></span> Request만큼의 CPU 자원은 확실히 보장받는다.</p>
</li>
<li><p><span style="color: indianred"><strong>장점:</strong></span> 노드에 남는 CPU 자원이 있다면, 파드는 필요한 만큼 이를 가져다 쓸 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>결론:</strong></span> *<em>CPU는 압축 가능한 자원이다. *</em>
즉, 시스템이 바쁠 때는 조금 느려질 뿐 죽지는 않는다. 따라서 Limit을 없애 유휴 자원을 최대한 활용하여 성능을 높이는 것이 효율적이다. KRR이 이 방식을 권장하는 이유다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: SlateBlue"><strong>No CPU Requests (하단 영역)</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>No Requests + Limits (주황색):</strong></span> 쿠버네티스가 자동으로 Request를 Limit과 동일하게 설정한다. Limit만큼 보장되지만 그 이상은 사용 불가하다.</p>
</li>
<li><p><span style="color: indianred"><strong>No Requests + No Limits (빨간색 - 우하단):</strong></span> 아무런 자원 보장도 받지 못한다. 노드 자원이 부족하면 가장 먼저 성능이 저하되거나 죽을 수 있다. 절대 권장하지 않는다.</p>
<br>

</li>
</ul>
</li>
</ol>
<blockquote>
<ul>
<li><span style="color: SlateBlue"><strong>주의:</strong></span> <strong>위 내용은 CPU에만 해당된다. Memory는 압축 불가능한 자원이므로, Limit을 초과하면 OOM Kill이 발생한다. 따라서 메모리는 Limit 설정이 필수적이다.</strong></li>
</ul>
</blockquote>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-krr-vs-vpa-vertical-pod-autoscalerspan"><span style='background-color: MediumAquamarine'>4. KRR vs VPA (Vertical Pod Autoscaler)</span></h2>
<hr>

<p><strong>KRR과 유사한 기능을 하는 쿠버네티스 네이티브 도구인 VPA와의 차이점은 다음과 같다.</strong></p>
<table>
<thead>
<tr>
<th>특징</th>
<th align="left">KRR (Robusta)</th>
<th align="left">VPA (Kubernetes Native)</th>
</tr>
</thead>
<tbody><tr>
<td>실행 방식</td>
<td align="left">CLI 도구 (필요할 때 실행)</td>
<td align="left">클러스터 내 상시 실행 데몬</td>
</tr>
<tr>
<td>결과 도출</td>
<td align="left">즉시 (기존 Prometheus 데이터 활용)</td>
<td align="left">일정 기간 데이터 수집 필요 (Warm-up)</td>
</tr>
<tr>
<td>적용 방식</td>
<td align="left">권장 사항 리포트 제공 (사람이 판단)</td>
<td align="left">파드 설정을 직접/자동으로 수정</td>
</tr>
<tr>
<td>커스텀</td>
<td align="left">알고리즘 확장 및 수정 용이</td>
<td align="left">설정이 제한적</td>
</tr>
</tbody></table>
<p>KRR은 즉각적인 가시성을 제공하고, 변경 사항을 적용하기 전 엔지니어가 검토할 수 있다는 점에서 운영 안정성이 높다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-추가-기능-및-활용span"><span style='background-color: MediumAquamarine'>5. 추가 기능 및 활용</span></h2>
<hr>

<p>KRR은 단순 CLI를 넘어 다양한 방식으로 활용 가능하다.</p>
<ul>
<li><p><span style="color: indianred"><strong>Slack 통합:</strong></span> Robusta와 연동하여 주기적으로 리소스 최적화 리포트를 슬랙으로 받아볼 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>상세 분석:</strong></span> Robusta UI를 통해 특정 권장 사항이 도출된 근거(히스토리 그래프 등)를 시각적으로 확인할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>K9s 플러그인:</strong></span> 터미널 UI 도구인 K9s에 플러그인으로 설치하여, 실시간으로 파드 리스트 옆에서 권장 값을 확인할 수 있다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-정리span"><span style='background-color: MediumAquamarine'>6. 정리</span></h2>
<hr>

<ol>
<li><p><span style="color: indianred"><strong>리소스의 Request/Limit 최적화만으로도 상당한 비용을 아낄 수 있다.</strong></span></p>
</li>
<li><p><strong>새로운 데이터 수집 도구 필요 없이, 기존 Prometheus 데이터를 활용하면 된다.</strong></p>
</li>
<li><p><strong>KRR을 사용하면 별도의 에이전트 없이 즉각적인 최적화 권고안을 얻을 수 있다.</strong></p>
</li>
<li><p><strong>CPU Limit을 제거하고 Request를 적절히 설정하는 것이 리소스 효율성과 성능 면에서 가장 유리하다.</strong></p>
</li>
</ol>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 82)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-82-mgpnhtb6</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-82-mgpnhtb6</guid>
            <pubDate>Sat, 31 Jan 2026 17:21:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 82 - Dev Containers in VS Code **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-dev-containers의-본질과-도입-필요성-span"><span style='background-color: MediumAquamarine'>1. Dev Containers의 본질과 도입 필요성 </span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-단순한-docker가-아닌-환경-사양-specspan"><span style="color: SlateBlue">1-1. 단순한 Docker가 아닌 환경 사양 (Spec)</span></h3>
<p><strong>많은 개발자가 Dev Containers를 단순히 Docker 이미지를 띄우는 것으로 오해하곤 한다.</strong></p>
<p><span style="color: indianred"><strong>하지만 Dev Containers는 커뮤니티가 채택한 오픈 소스 사양(Specification)이다.</strong></span></p>
<ul>
<li><p><span style="color: indianred"><strong>정의:</strong></span> 단순한 OS 뿐만 아니라 개발에 필요한 도구, 런타임, Extensions, 설정, 파일 시스템 마운트 등을 코드인 devcontainer.json로 정의한 집합체다.
<img src="https://velog.velcdn.com/images/taekyu_/post/990fda0b-8eed-4b6e-8966-1b6a21b106c0/image.png" alt=""></p>
</li>
<li><p><span style="color: indianred"><strong>아키텍처:</strong></span> </p>
<ul>
<li><p>로컬 VS Code가 컨테이너 내부의 VS Code Server와 연결되는 구조다. </p>
</li>
<li><p>소스 코드는 마운트되어 격리된 환경에서 실행되지만, 개발자는 로컬 테마와 단축키를 그대로 사용하는 Inner Loop(내부 개발 루프)를 경험할 수 있다.</p>
</li>
</ul>
</li>
</ul>
<br>


<h3 id="span-stylecolor-slateblue1-2-왜-도입해야-하는가-span"><span style="color: SlateBlue">1-2. 왜 도입해야 하는가? </span></h3>
<p>개발 현장에서 겪는 세 가지 주요 고통을 해결한다.</p>
<ul>
<li><p><span style="color: indianred"><strong>고통스러운 온보딩:</strong></span> </p>
<ul>
<li>신규 프로젝트 투입 시 하드웨어 세팅, 라이선스 설정, 특정 경로 파일 복사 등 수작업으로 인해 며칠씩 소요되는 시간을 단축한다.</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>버전 호환성 문제:</strong></span> </p>
<ul>
<li><p>&quot;팀원 A는 Node 14를 쓰고 나는 Node 12를 써서 코드가 깨지는&quot; 상황을 방지한다. </p>
</li>
<li><p>컨설팅 업무 시 고객사마다 다른 언어 버전(.NET, Java 등)을 요구해도, 로컬 머신의 오염 없이 프로젝트별로 격리된 환경을 사용할 수 있다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>문서화와 실행의 괴리:</strong></span> </p>
<ul>
<li><p>README에 &quot;시작 전 1, 2, 3단계를 수행하시오&quot;라고 적어도 누락하기 쉽다. </p>
</li>
<li><p>Dev Containers는 <code>post-create-command</code> 등을 통해 초기화 작업을 자동화한다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-환경-구축-실습-span"><span style='background-color: MediumAquamarine'>2. 환경 구축 실습 </span></h2>
<hr>


<p><strong>Dev Containers의 가장 큰 장점은 로컬(Docker Desktop)과 클라우드(GitHub Codespaces)를 동일한 설정으로 오갈 수 있다는 유연성이다.</strong></p>
<h3 id="span-stylecolor-slateblue2-1-로컬-vs-code에서의-구축-span"><span style="color: SlateBlue">2-1. 로컬 VS Code에서의 구축 </span></h3>
<p><strong>프레젠테이션에서는 VS Code와 Docker Desktop(WSL 2)을 사용한 데모를 통해 다음 과정을 시연했다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>필수 도구:</strong></span> &#39;Dev Containers&#39; 확장 프로그램과 &#39;GitHub Codespaces&#39; 확장이 필요하다.</p>
</li>
<li><p><span style="color: indianred"><strong>설정 생성:</strong></span> 명령 팔레트(F1)에서 <code>Add Dev Container Configuration Files</code>를 실행하여 템플릿을 선택한다. Alpine, Debian 같은 OS 베이스나 Go, Rust 같은 언어 베이스 템플릿을 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>기능 활용:</strong></span> 복잡한 Dockerfile 스크립트 작성 없이, &#39;Features&#39; 메뉴에서 Azure CLI, Terraform, Kubectl 등을 체크박스 선택만으로 컨테이너에 주입할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과물:</strong></span> </p>
<ul>
<li><p><code>.devcontainer</code> 폴더 내에 <code>devcontainer.json(메인 설정)</code>, <code>Dockerfile(빌드 정의)</code>, <code>dependabot.yml(버전 관리)</code> 등이 생성된다.</p>
</li>
<li><p>이를 실행(&quot;Reopen in Container&quot;)하면 로컬에는 없는 도구들이 설치된 완벽한 개발 환경이 뚝딱 만들어진다.</p>
<br>


</li>
</ul>
</li>
</ul>
<h3 id="span-stylecolor-slateblue2-2-github-codespaces를-활용한-클라우드-개발span"><span style="color: SlateBlue">2-2. GitHub Codespaces를 활용한 클라우드 개발</span></h3>
<p>고사양 PC가 없거나 Docker 라이선스 문제가 있다면 GitHub Codespaces가 대안이 된다.</p>
<ul>
<li><p><span style="color: indianred"><strong>브라우저 모드:</strong></span> GitHub 리포지토리에서 버튼 하나로 실행되며, 별도 설치 없이 브라우저 내에서 VS Code 환경을 제공한다. .NET 8, GitHub CLI 등 설정된 도구가 즉시 가용 상태가 된다.</p>
</li>
<li><p><span style="color: indianred"><strong>하이브리드 모드 (Open in VS Code):</strong></span> 브라우저가 어색하다면, 로컬 VS Code를 인터페이스로 사용하고 컴퓨팅 자원만 클라우드(Codespace)를 사용할 수 있다. 로컬의 편안함과 클라우드의 성능을 동시에 누리는 방식이다.</p>
</li>
<li><p><span style="color: indianred"><strong>관리 기능:</strong></span> Dotfiles(개인 설정) 자동 적용, Secrets(API 키) 관리, 데이터 주권을 위한 리전 선택 등을 지원한다.</p>
</li>
<li><p><span style="color: indianred"><strong><code>containers.dev</code> 문서: **</span> **<a href="https://containers.dev/features">https://containers.dev/features</a></strong></p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-디버깅-및-네트워크-통합-span"><span style='background-color: MediumAquamarine'>3. 디버깅 및 네트워크 통합 </span></h2>
<hr>

<p><strong>Dev Containers는 환경만 격리할 뿐, 개발자가 누려야 할 생산성 도구(디버거, 네트워크 등)는 로컬 환경처럼 투명하게 연결해 준다.</strong></p>
<h3 id="span-stylecolor-slateblue3-1-로컬과-동일한-원격-디버깅span"><span style="color: SlateBlue">3-1. 로컬과 동일한 원격 디버깅</span></h3>
<p>프레젠테이션에서는 Rust와 Go 언어 샘플을 통해 디버깅 과정을 시연했다.</p>
<ul>
<li><p>코드는 컨테이너(또는 클라우드) 내부에서 돌지만, 개발자는 로컬 VS Code에서 중단점(Breakpoint)을 설정한다.</p>
</li>
<li><p>F5를 눌러 실행하면 정확히 중단점에서 멈추고, 변수 값 조사(Inspect)나 스택 추적을 로컬 개발과 똑같이 수행할 수 있다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue3-2-포트-포워딩span"><span style="color: SlateBlue">3-2. 포트 포워딩</span></h3>
<p>웹 애플리케이션 개발 시 네트워크 연결도 자동으로 처리된다.</p>
<ul>
<li><p><span style="color: indianred"><strong>시나리오:</strong></span> 클라우드 Codespace 내부에서 웹 서버를 9000번 포트로 실행했다.</p>
</li>
<li><p><span style="color: indianred"><strong>자동 연결:</strong></span> VS Code는 이를 감지하고 내 로컬 머신의 <code>localhost:9000</code>으로 자동 포트 포워딩을 수행한다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과:</strong></span>  개발자는 자신의 로컬 브라우저(Chrome, Edge 등)를 열어 localhost:9000에 접속하면, 클라우드에 있는 서버와 연결된다. 심지어 이 요청에 대해서도 실시간 디버깅이 작동한다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-정리-span"><span style='background-color: MediumAquamarine'>4. 정리 </span></h2>
<hr>

<p>*<em>Dev Containers는 단순한 도구가 아니라 개발 환경의 표준화를 위한 솔루션이다. *</em></p>
<p><span style="color: indianred">*<em>프로젝트에 <code>.devcontainer</code> 폴더를 추가하는 것만으로, 팀 전체가 버전 의존성에서 벗어나고 온보딩 시간을 획기적으로 단축할 수 있다. *</em></span></p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 81)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-81-yqv0c0pr</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-81-yqv0c0pr</guid>
            <pubDate>Fri, 30 Jan 2026 17:43:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 81 - Leveraging Kubernetes to build a better Cloud Native Development Experience **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-okteto에-대한-발전-배경-문제-제기span"><span style='background-color: MediumAquamarine'>1. Okteto에 대한 발전 배경 (문제 제기)</span></h2>
<hr>

<h3 id="span-stylecolor-slateblue1-1-배포에서-개발로의-관점-이동span"><span style="color: SlateBlue">1-1. 배포에서 개발로의 관점 이동</span></h3>
<p><strong>2014년 쿠버네티스 초기 시절, 업계의 주된 관심사는 워크로드를 어떻게 쿠버네티스에 배포할 것인가였다.</strong></p>
<p>당시에는 배포 자체가 난제였기에 &#39;Kubernetes the Hard Way&#39;와 같은 학습 자료나 Rancher Desktop 같은 솔루션들이 등장하며 배포의 복잡성을 해결하려 했다.</p>
<p>하지만 시간이 흐르며 상황은 변했다. </p>
<p><span style="color: indianred"><strong>개발자의 본질은 인프라 배포가 아닌 코드 작성에 있다.</strong></span> </p>
<p><strong>개발자는 불필요한 배포에 시간을 쏟기보다 비즈니스 로직 구현에 집중하기를 원한다.</strong>
<br></p>
<h3 id="span-stylecolor-slateblue1-2-전통적인-개발-루프의-고통-span"><span style="color: SlateBlue">1-2. 전통적인 개발 루프의 고통 </span></h3>
<p>일반적인 클라우드 네이티브 애플리케이션 개발 프로세스이다.</p>
<ol>
<li><p><span style="color: indianred"><strong>SRS(요구사항) 분석 및 코딩:</strong></span> 개발팀이 기능을 구현한다.</p>
</li>
<li><p><span style="color: indianred"><strong>컨테이너화:</strong></span> Dockerfile을 작성하고 이미지를 빌드한다.</p>
</li>
<li><p><span style="color: indianred"><strong>레지스트리 Push:</strong></span> 빌드된 이미지를 컨테이너 레지스트리에 업로드한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Pull 및 배포:</strong></span> 쿠버네티스 클러스터에서 이미지를 받아(Pull) 배포한다.</p>
<br>

</li>
</ol>
<p>문제는 기능 추가나 버그 수정이 필요할 때마다 이 과정을 무한 반복해야 한다는 점이다.</p>
<ul>
<li><p><span style="color: indianred"><strong>시나리오:</strong></span> 로컬에서 코드를 수정했다. 이 변경 사항이 운영 환경과 유사한 쿠버네티스 환경에서 어떻게 동작할지 확인해야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>비효율:</strong></span> 코드를 한 줄 고칠 때마다 <strong><code>이미지 Build -&gt; Push -&gt; Pull -&gt; 배포</code></strong> 과정을 거쳐야 한다.</p>
</li>
<li><p><span style="color: indianred"><strong>리스크:</strong></span> 로컬에서는 잘 작동하던 코드가 실제 클러스터에 배포되었을 때 의존성 문제 등으로 터질 수 있다. &quot;내 컴퓨터에서는 되는데?&quot;라는 상황이 발생한다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue1-3-제1원칙에-입각한-해결책span"><span style="color: SlateBlue">1-3. 제1원칙에 입각한 해결책</span></h3>
<p><strong>이 문제의 핵심은 피드백 루프가 너무 길고, 운영 환경과의 괴리가 크다는 것이다.</strong></p>
<p>이를 해결하기 위한 이상적인 솔루션은 다음과 같다.</p>
<p><span style="color: indianred"><strong>&quot;이미지를 매번 빌드하지 않고, 로컬 머신의 변경 사항이 쿠버네티스 클러스터 내부의 애플리케이션과 즉시 동기화되어 Preview 환경에서 바로 확인할 수 있다면 어떨까?&quot;</strong></span></p>
<p>이것이 바로 Okteto가 해결하고자 하는 지점이다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-솔루션-okteto-the-solutionspan"><span style='background-color: MediumAquamarine'>2. 솔루션: Okteto (The Solution)</span></h2>
<hr>

<p>*<em>Okteto의 핵심 기능은 로컬 개발 환경과 클러스터 환경의 실시간 동기화다. *</em></p>
<p><span style="color: indianred"><strong>이를 통해 개발자는 매번 이미지를 빌드/배포하는 과정 없이, 로컬에서 코드를 저장하는 즉시 클러스터에 반영된 결과를 확인할 수 있다.</strong></span></p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/4866145d-7af1-428f-8a19-4780e439edd7/image.png" alt=""></p>
<ul>
<li><p><span style="color: indianred"><strong>Okteto CLI &amp; Cloud:</strong></span> CLI와 관리형 클라우드 서비스를 모두 제공한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Preview 환경:</strong></span> PR 단계에서 변경 사항이 적용된 라이브 프리뷰를 자동으로 생성할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>동기화 메커니즘:</strong></span> 로컬 파일 시스템의 변경 사항을 컨테이너 내부로 즉시 전송한다.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue-21-핵심-아키텍처-및-작동-원리span"><span style="color: SlateBlue"> 2.1. 핵심 아키텍처 및 작동 원리</span></h3>
<p>Okteto은 단순한 파일 복사가 아니다. </p>
<p><strong>기존에 배포된 애플리케이션의 컨테이너를 개발용 컨테이너로 교체하는 기술이 핵심이다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>배포 교체:</strong></span></p>
<ul>
<li><p><code>okteto up</code>을 실행하면, Okteto는 기존의 운영용 Pod를 내리고 개발 도구(컴파일러, 디버거 등)가 포함된 Dev Container로 교체하여 재실행한다. </p>
</li>
<li><p>개발이 끝나면 원래 설정으로 자동 복구된다.</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>양방향 파일 동기화:</strong></span></p>
<ul>
<li><p>로컬 PC의 소스 코드 디렉토리와 클러스터 내 컨테이너를 네트워크로 연결한다. </p>
</li>
<li><p><span style="color: indianred"><strong>로컬에서 파일을 저장하는 순간, 변경된 파일만 즉시 컨테이너로 전송</strong></span>되며, 컨테이너 내부의 Hot Reload 도구가 이를 감지해 프로세스를 재시작한다. (소요 시간: 0.5초~2초)</p>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>자동 포트 포워딩:</strong></span></p>
<ul>
<li>별도의 Ingress 설정이나 kubectl port-forward 없이도, 로컬호스트(localhost:8080)로 요청을 보내면 터널링을 통해 클러스터 내부의 컨테이너로 안전하게 전달된다.<br>

</li>
</ul>
</li>
</ul>
<h3 id="span-stylecolor-slateblue-2-2-주요-기능-상세span"><span style="color: SlateBlue"> 2-2. 주요 기능 상세</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>Okteto Manifest (okteto.yml)</strong></span></p>
<ul>
<li><p>Okteto가 어떻게 동작해야 하는지 정의하는 설정 파일이다. </p>
</li>
<li><p>Dockerfile만 있어도 자동 추론하지만, 정교한 제어를 위해 작성한다.</p>
</li>
<li><pre><code class="language-yaml"># okteto.yml 예시
name: my-api
image: okteto/node:18  # 개발에 사용할 도구가 포함된 이미지
command: [&quot;bash&quot;]      # 컨테이너 시작 시 실행할 셸
sync:
  - .:/usr/src/app     # 로컬의 현재 폴더(.)를 컨테이너의 /usr/src/app과 동기화
forward:
  - 8080:8080          # 포트 포워딩 설정
  - 9229:9229          # 디버거 포트 연결 (원격 디버깅 가능)</code></pre>
</li>
<li><p>이 설정을 통해 어떤 이미지로 교체할지, 어떤 폴더를 동기화할지를 지정한다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>Preview 환경</strong></span></p>
<ul>
<li><p>GitHub/GitLab과 연동하여 PR가 생성될 때마다 격리된 네임스페이스에 해당 코드를 배포한다.</p>
</li>
<li><p><strong>Before:</strong> &quot;내 로컬에서 돌려봤는데 잘 돼. 머지해줘.&quot; (검증 불가)</p>
</li>
<li><p><strong>After:</strong> &quot;PR 34번 프리뷰 링크 생성됐어. 거기서 직접 눌러봐.&quot; (확실한 검증)</p>
</li>
<li><p>DB, Redis 등 연관된 모든 서비스를 포함한 전체 스택을 띄워주므로 통합 테스트에 유리하다.</p>
</li>
<li><p>PR이 닫히거나 머지되면 해당 환경(네임스페이스)은 자동으로 삭제되어 리소스 낭비를 막는다.</p>
<br>
</li>
</ul>
</li>
<li><p><span style="color: indianred"><strong>원격 디버거 연결</strong></span></p>
<ul>
<li><p>파일 동기화뿐만 아니라 디버깅 포트도 연결된다. </p>
</li>
<li><p>로컬 IDE에서 중단점을 찍으면, 쿠버네티스 클러스터에서 실행 중인 코드가 멈추고 변수 상태를 검사할 수 있다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-데모-시연-및-동작-원리span"><span style='background-color: MediumAquamarine'>3. 데모 시연 및 동작 원리</span></h2>
<hr>

<p><strong>발표에서는 &#39;Movies with Compose&#39;라는 React, Node.js, MongoDB, Nginx로 구성된 마이크로서비스 애플리케이션을 통해 Okteto의 장점을 어필했다.</strong></p>
<h3 id="span-stylecolor-slateblue3-1-초기-설정-및-배포-okteto-upspan"><span style="color: SlateBlue">3-1. 초기 설정 및 배포 (okteto up)</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>애플리케이션 준비:</strong></span> docker-compose.yml 또는 Dockerfile이 있는 프로젝트를 준비한다. 쿠버네티스 매니페스트가 없어도 된다.</p>
</li>
<li><p><span style="color: indianred"><strong>명령어 실행:</strong></span> <code>okteto context</code>로 클러스터를 지정하고 <code>okteto up</code>을 실행한다.</p>
</li>
<li><p><span style="color: indianred"><strong>자동화된 프로세스:</strong></span></p>
<ul>
<li><p>Okteto는 소스 코드를 분석하여 필요한 이미지를 빌드하고 Okteto Cloud 혹은 지정된 클러스터에 배포한다.</p>
</li>
<li><p>각 서비스(API, Frontend, DB 등)와 외부 접속을 위한 엔드포인트(URL)를 자동으로 생성한다.</p>
<br>

</li>
</ul>
</li>
</ul>
<h3 id="span-stylecolor-slateblue3-2-개발-컨테이너-활성화span"><span style="color: SlateBlue">3-2. 개발 컨테이너 활성화</span></h3>
<p>배포가 완료되면 Okteto는 사용자에게 <strong>&quot;어떤 서비스를 개발 모드로 전환할 것인가?&quot;</strong>를 묻는다.</p>
<ul>
<li><p>사용자가 특정 서비스(예: API)를 선택하면, Okteto는 해당 서비스의 기존 운영 컨테이너를 개발 컨테이너로 교체한다.</p>
</li>
<li><p>해당 개발 컨테이너는 로컬 머신의 소스 코드 디렉토리와 볼륨 동기화가 설정되어 있다.</p>
</li>
<li><p>터미널은 해당 컨테이너의 Shell로 바로 연결된다.</p>
<br>


</li>
</ul>
<h3 id="span-stylecolor-slateblue3-3-실시간-코드-수정-및-반영span"><span style="color: SlateBlue">3-3. 실시간 코드 수정 및 반영</span></h3>
<p>시연에서는 영화 목록이 중복해서 출력되는 버그를 수정하는 과정을 보여주었다.</p>
<ol>
<li><p><span style="color: indianred"><strong>버그 확인:</strong></span> 생성된 프리뷰 URL로 접속하여 버그를 확인한다.</p>
</li>
<li><p><span style="color: indianred"><strong>코드 수정:</strong></span> 로컬 IDE에서 server.js 파일을 열어 db.collection(&#39;movies&#39;)를 올바른 컬렉션으로 수정하고 저장한다.</p>
</li>
<li><p><span style="color: indianred"><strong>즉시 반영:</strong></span> 별도의 docker build나 kubectl apply 명령어가 필요 없다. 저장하는 순간 Okteto가 변경된 파일을 컨테이너로 동기화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>결과 확인:</strong></span> 브라우저를 새로고침하면 수정된 코드가 즉시 반영되어 버그가 해결된 것을 볼 수 있다.</p>
<br>


</li>
</ol>
<h3 id="span-stylecolor-slateblue3-4-dockerfile만으로-충분하다span"><span style="color: SlateBlue">3-4. Dockerfile만으로 충분하다</span></h3>
<p>가장 강력한 장점 중 하나는 복잡한 쿠버네티스 지식이 없어도 된다는 점이다.</p>
<p><img src="https://velog.velcdn.com/images/taekyu_/post/efe54292-91ac-49b0-af1a-95e8440ddfa5/image.png" alt=""></p>
<p><strong>Dockerfile이나 docker-compose.yml만 있다면, Okteto가 이를 기반으로 쿠버네티스 배포를 자동으로 처리하고 프리뷰 환경을 구축해준다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-결론span"><span style='background-color: MediumAquamarine'>4. 결론</span></h2>
<hr>

<p>*<em>과거에는 배포 자체가 목적이었다면, 지금은 얼마나 빠르고 효율적으로 개발하고 검증할 수 있는가가 핵심이다. *</em></p>
<p>따라서 프레젠테이션은 Okteto가 다음과 같은 가치를 제공한다고 주장한다. </p>
<ul>
<li><p><span style="color: indianred"><strong>Inner Loop 단축:</strong></span> 코드 수정부터 확인까지의 시간을 획기적으로 줄여준다.</p>
</li>
<li><p><span style="color: indianred"><strong>Prod-like Dev:</strong></span>  로컬(Docker Desktop 등)이 아닌 실제 쿠버네티스 클러스터 위에서 개발하므로, 배포 시 발생할 수 있는 환경 차이 문제를 사전에 방지한다.</p>
</li>
<li><p><span style="color: indianred"><strong>오픈 소스:</strong></span>  Okteto CLI는 오픈 소스이며 활발한 커뮤니티 지원을 받고 있다.</p>
</li>
</ul>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[90DaysOfDevOps (Day 80)]]></title>
            <link>https://velog.io/@taekyu_/90DaysOfDevOps-Day-80-eo2jwsv9</link>
            <guid>https://velog.io/@taekyu_/90DaysOfDevOps-Day-80-eo2jwsv9</guid>
            <pubDate>Sat, 24 Jan 2026 11:31:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>해당 스터디는 90DaysOfDevOps
<a href="https://github.com/MichaelCade/90DaysOfDevOps">https://github.com/MichaelCade/90DaysOfDevOps</a>
를 기반으로 진행한 내용입니다.</strong></p>
</blockquote>
<p>** Day 80 - Unlocking K8s Troubleshooting Best Practices with Botkube **</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine1-쿠버네티스-트러블슈팅span"><span style='background-color: MediumAquamarine'>1. 쿠버네티스 트러블슈팅</span></h2>
<hr>

<p>*<em>쿠버네티스 생태계는 거대하고 복잡하다. *</em></p>
<p>네트워킹, 컨테이너, 배포 전략 등 알아야 할 지식의 양이 방대하여 진입 장벽이 높을 뿐만 아니라, 수많은 CNCF 도구들과 함께 사용되기 때문에 이들을 통합 관리하는 것은 엔지니어에게 큰 부담이다.</p>
<p><span style="color: indianred"><strong>특히 팀이 지리적으로 분산된 하이브리드 근무 환경에서 장애가 발생했을 때, Slack이나 Teams 같은 메신저로 파편화된 정보를 공유하며 문제를 해결하는 것은 Context Switching 비용을 발생시키고 대응 속도를 늦춘다.</strong></span></p>
<hr>
<p><strong>쿠버네티스 트러블슈팅은 공식적으로 클러스터 내의 문제를 식별하고 해결하는 과정이다.</strong></p>
<p>하지만 이를 좀 더 실천적인 관점에서 정의하자면 <span style="color: indianred"><strong>&quot;모니터링 + 행동 또는 반응의 결합&quot;</strong></span>이라 할 수 있다.</p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine2-기존-트러블슈팅의-문제점span"><span style='background-color: MediumAquamarine'>2. 기존 트러블슈팅의 문제점</span></h2>
<hr>

<p>가장 흔한 에러 중 하나인 OOMKilled(Out Of Memory) 상황을 가정해보자. 전통적인 해결 방식은 다음 5단계를 거친다.</p>
<ol>
<li><p><span style="color: SlateBlue"><strong>종료된 Pod 식별</strong></span></p>
</li>
<li><p><span style="color: SlateBlue"><strong>메모리 사용량 확인</strong></span></p>
</li>
<li><p><span style="color: SlateBlue"><strong>로그 내 에러 확인</strong></span></p>
</li>
<li><p><span style="color: SlateBlue"><strong>이미지 업데이트</strong></span></p>
</li>
<li><p><span style="color: SlateBlue"><strong>메모리 Limit 증설</strong></span></p>
</li>
</ol>
<p>이 5단계는 간단해 보이지만, 20개 이상의 클러스터를 운영하거나 업무 시간 외에 발생할 경우, 해결까지 몇 분에서 며칠이 소요될 수 있다.
<br></p>
<p>또한, 일반적인 조직에서는 <span style="color: indianred"><strong><code>[개발자 장애 발견 → 티켓 생성 → DevOps 팀 알림 → DevOps 환경 진단 → SRE 팀 호출 → 트러블슈팅 → 상태 업데이트 → 팀 전파]</code></strong></span>라는 복잡한 과정을 거친다. 이 과정에서 너무 많은 사람과 단계가 개입된다.</p>
<ul>
<li><p><span style="color: indianred"><strong>DevOps 팀:</strong></span> 인력 부족과 업무 과다에 시달리며, 개발과 운영 사이에서 끊임없는 문맥 전환으로 인한 피로감을 느낀다.</p>
</li>
<li><p><span style="color: indianred"><strong>개발자:</strong></span> 앱의 상태만 확인하고 싶은데, 쿠버네티스 심층 지식까지 강요받는 부담을 느낀다.</p>
</li>
<li><p><span style="color: indianred"><strong>운영 팀:</strong></span> 보안 리스크와 비용 효율성 때문에 시스템 신뢰성 유지가 최우선 과제라 압박감이 크다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine3-쿠버네티스-트러블슈팅-모범-사례-5가지span"><span style='background-color: MediumAquamarine'>3. 쿠버네티스 트러블슈팅 모범 사례 5가지</span></h2>
<hr>

<p>이러한 문제를 해결하기 위한 5가지 핵심 전략은 다음과 같다.</p>
<ul>
<li><p><span style="color: indianred"><strong>모니터링과 관측성(Observability) 중앙화:</strong></span> 흩어진 정보를 한곳으로 모은다.</p>
</li>
<li><p><span style="color: indianred"><strong>인시던트 대응과 협업 강화:</strong></span> 팀 간 소통을 효율화한다.</p>
</li>
<li><p><span style="color: indianred"><strong>피드백 루프 구축:</strong></span> 과거 데이터를 통해 학습하고 개선한다.</p>
</li>
<li><p><span style="color: indianred"><strong>Command Execution 간소화:</strong></span> 복잡한 터미널 작업을 줄인다.</p>
</li>
<li><p><span style="color: indianred"><strong>관측성 및 전달 프로세스 자동화:</strong></span> 수동 작업을 최소화한다.</p>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine4-솔루션-botkube를-활용한-chatops-구현span"><span style='background-color: MediumAquamarine'>4. 솔루션: Botkube를 활용한 ChatOps 구현</span></h2>
<hr>

<p><strong>Botkube는 협업 중심의 쿠버네티스 트러블슈팅 도구로, 메신저(Slack, Teams, Discord, Mattermost)와 쿠버네티스 클러스터 사이를 연결하는 양방향 에이전트다.</strong> </p>
<p><span style="color: indianred"><strong>단순 알림 수신을 넘어, 메신저에서 직접 클러스터를 제어하는 것이 특징이다.</strong></span>
<img src="https://velog.velcdn.com/images/taekyu_/post/6c0e6031-e85f-4fb6-a181-d1607187b519/image.png" width="70%" height="n%"></p>
<h3 id="span-stylecolor-slateblue4-1-관측성-강화-observabilityspan"><span style="color: SlateBlue">4-1. 관측성 강화 (Observability)</span></h3>
<p><strong>모니터링은 단순한 데이터 수신이 아니라 문맥(Context)을 파악하는 것이다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>Watchdog 역할:</strong></span> 클러스터 변경 사항을 실시간으로 감지하여 알린다.</p>
</li>
<li><p><span style="color: indianred"><strong>채널별 격리:</strong></span> 개발팀은 개발 관련 알림만, 플랫폼 팀은 인프라 알림만 받도록 채널을 분리하여 피로도를 줄인다.</p>
</li>
<li><p><span style="color: indianred"><strong>직접 해결:</strong></span> 알림을 받고 터미널을 켜는 것이 아니라, 슬랙에서 버튼을 누르거나 명령어를 입력해 즉시 문제를 해결한다. 이는 문맥 전환 비용을 획기적으로 줄여준다.</p>
<img src="https://velog.velcdn.com/images/taekyu_/post/a42a5817-1e5d-4652-acd1-7205952ccb93/image.png" width="70%" height="n%">

</li>
</ul>
<br>


<h3 id="span-stylecolor-slateblue4-2-협업과-피드백-루프-collaboration--feedback-loopspan"><span style="color: SlateBlue">4-2. 협업과 피드백 루프 (Collaboration &amp; Feedback Loop)</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>정보 공유:</strong></span> 공유된 채널에서 트러블슈팅이 이루어지므로, 별도의 보고 절차 없이 팀 전체가 상황을 파악할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>Audit Logs:</strong></span> 누가 어떤 명령어를 실행했는지 기록된다. 예를 들어 주니어 엔지니어가 반복적인 실수를 한다면, 로그를 통해 근본 원인을 파악하고 교육하여 팀 역량을 높일 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>반응형 → 주도형:</strong></span> 과거 인시던트 이력을 바탕으로 장애의 전조 증상을 파악하고 사전 대응이 가능해진다.</p>
</li>
</ul>
<h3 id="span-stylecolor-slateblue4-3-개발자-셀프-서비스와-명령어-실행-간소화span"><span style="color: SlateBlue">4-3. 개발자 셀프 서비스와 명령어 실행 간소화</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>비전문가를 위한 접근성:</strong></span> 쿠버네티스 전문가가 아니더라도, Botkube가 제공하는 드래그 앤 드롭 UI나 간소화된 명령어를 통해 필요한 조치를 취할 수 있다.</p>
</li>
<li><p><span style="color: indianred"><strong>명령어 실행 속도 향상:</strong></span> 복잡한 kubectl 명령어를 일일이 기억할 필요 없이, 채팅창에서 빠르게 실행 가능하다.</p>
</li>
</ul>
<h3 id="span-stylecolor-slateblue4-4-자동화-automationspan"><span style="color: SlateBlue">4-4. 자동화 (Automation)</span></h3>
<p><strong>가장 강력한 기능은 트러블슈팅 단계의 자동화다. 앞서 언급한 OOMKilled 5단계를 Botkube는 2단계로 단축시킨다.</strong></p>
<ul>
<li><p><span style="color: indianred"><strong>기존 5단계:</strong></span> 식별 → 확인 → 로그 확인 → 업데이트 → 메모리 증설</p>
</li>
<li><p><span style="color: indianred"><strong>Botkube 자동화:</strong></span></p>
<ul>
<li><p>Botkube가 자동으로 파드를 식별하고, 로그와 메모리 사용량을 분석하여 에러 원인을 채팅창에 띄워준다 (1~3단계 자동 수행).</p>
</li>
<li><p><strong>엔지니어는 제공된 정보를 바탕으로 이미지 업데이트 및 메모리 증설(4~5단계)만 수행하면 된다.</strong></p>
</li>
</ul>
</li>
</ul>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine5-도입-효과-및-실제-사례span"><span style='background-color: MediumAquamarine'>5. 도입 효과 및 실제 사례</span></h2>
<hr>


<h3 id="span-stylecolor-slateblue5-1-정량적-기대-효과span"><span style="color: SlateBlue">5-1. 정량적 기대 효과</span></h3>
<ul>
<li><p><span style="color: indianred"><strong>MTTR(평균 복구 시간) 단축:</strong></span> 커뮤니케이션 지연을 제거하여 빠른 복구 가능.</p>
</li>
<li><p><span style="color: indianred"><strong>운영 효율성 증대:</strong></span> 단일 플랫폼에서 정보 확인 및 조치 가능.</p>
</li>
<li><p><span style="color: indianred"><strong>팀 번아웃 감소:</strong></span> 불필요한 문맥 전환과 전문가에 대한 과도한 의존 제거.</p>
</li>
<li><p><span style="color: indianred"><strong>장애 감소:</strong></span> 사전 대응을 통해 작은 이슈가 대형 장애로 번지는 것을 방지.</p>
<br>

</li>
</ul>
<h3 id="span-stylecolor-slateblue5-2-고객-사례-해운-회사span"><span style="color: SlateBlue">5-2. 고객 사례 (해운 회사)</span></h3>
<p>운영(Prod)과 스테이징(Staging) 클러스터를 운영하는 한 고객사는 Botkube의 RBAC(역할 기반 접근 제어) 기능을 활용했다. </p>
<p><strong>슬랙 채널별로 권한을 매핑하여, 특정 채널에서는 조회만 가능하고 비공개 채널에서는 명령 실행이 가능하도록 설정함으로써 보안과 신뢰성을 동시에 확보했다.</strong></p>
<hr>

<h2 id="span-stylebackground-color-mediumaquamarine6-정리span"><span style='background-color: MediumAquamarine'>6. 정리</span></h2>
<hr>

<p><strong>쿠버네티스 트러블슈팅에서 가장 중요한 것은 반응적(Reactive) 태도를 버리고 전략적(Strategic)으로 접근하는 것이다.</strong></p>
<p><span style="color: indianred"><strong>팀의 리소스와 히스토리를 바탕으로 트러블슈팅 과정을 중앙화하고 자동화해야 한다.</strong></span></p>
<p>Botkube와 같은 도구는 복잡한 하이브리드 환경에서 팀의 협업 능력을 극대화하고, 더욱 회복탄력성있는 조직을 만드는 데 핵심적인 역할을 한다.</p>
<p><span style="color: indianred"></span>
<span style="color: SlateBlue"></span></p>
]]></description>
        </item>
    </channel>
</rss>