<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>nuung</title>
        <link>https://velog.io/</link>
        <description>🔥 [ AI RPA 의 시대가 기대되는, software/product 개발자 정현우 입니다. ] 🔥</description>
        <lastBuildDate>Mon, 09 Mar 2026 04:59:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>nuung</title>
            <url>https://images.velog.io/images/qlgks1/profile/627dac89-ffc3-449a-8f6e-09d2f8052004/githubIcon.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. nuung. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/qlgks1" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[LLM - 모델 경쟁이 끝나고, “하네스 경쟁”의 시작?!, opencode 와 oh-my-opencode]]></title>
            <link>https://velog.io/@qlgks1/opencode-oh-my-opencode-harness</link>
            <guid>https://velog.io/@qlgks1/opencode-oh-my-opencode-harness</guid>
            <pubDate>Mon, 09 Mar 2026 04:59:10 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: LLM의 오케스트레이션과 하네스, opencode 와 OMO 의 탄생 배경과 how to use 정리, 이미지 빼고 AI가 안썼어요.. 사람이 썼어요..🥹 ]</p>
<h1 id="모델-경쟁이-끝나고-하네스-경쟁의-시작">모델 경쟁이 끝나고, “하네스 경쟁”의 시작?!</h1>
<blockquote>
<p>LLM 평가 축은 빠르게 변하고 있습니다. 이제 “모델이 얼마나 똑똑한가”만으로는 체감 품질을 설명하기 어렵습니다!! 실제 현업에서의 생산성은 긴 작업을 얼마나 안정적으로 이어갈 수 있는지, 그리고 도구·컨텍스트·검증 루프를 얼마나 잘 운영하는지에서 갈립니다.</p>
</blockquote>
<p>LLM 의 스케일링 법칙(Scaling Laws)은 이제 한계라는 의견이 많습니다. (<a href="https://velog.io/@qlgks1/LLM-Intro-to-Large-Language-Models#8-%EC%84%B1%EB%8A%A5%EC%9D%84-%EB%86%92%EC%9D%B4%EB%8A%94-%EC%97%B4%EC%87%A0-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81-%EB%B2%95%EC%B9%99scaling-laws">참조 링크</a>). 더욱이 SLM 을 특정 작업에 맞춰 최적화하고, 고품질 데이터로 재학습시키고, 나아가 MoE 구조로도 사용합니다. </p>
<p>특히 신규 LLM 모델은 진짜 &quot;체감할 수 있는 차이&quot; 에 있어서 <strong>*&quot;구체적으로, 그리고 정량적으로 우리의 작업이 얼마나 나아졌는가&quot;*</strong> 를 설명하기 매우 어려워졌습니다.</p>
<p>모델 경쟁이 끝난 것은 아닙니다. 하지만 차별화의 중심축은 이미 위 레이어로 올라가고 있습니다. 특히 코딩, 리서치, 마이그레이션, 대규모 리팩토링처럼 길고 복잡한 작업에서는 “한 번 잘 답하는가”보다 “끝까지 안정적으로 완주하는가”가 훨씬 중요합니다. 그리고 그 완주 능력을 만들어내는 것이 바로 <strong>에이전트 하네스(Agent Harness)</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4d22962a-483e-4d1d-88d3-e171b88d878a/image.png" alt=""></p>
<h3 id="하네스">하네스?!</h3>
<ul>
<li>에이전트 하네스는 AI 모델을 감싸고, 장기적이거나 복잡한 작업을 안정적으로 수행하도록 관리하는 운영 인프라에 가깝습니다. </li>
<li>모델이 문장을 생성하는 엔진이라면, 하네스는 그 엔진 위에 올라가는 차량의 프레임, 조향 장치, 브레이크, 계기판 처럼 사람 승인 지점, 파일 시스템 접근 제어, 도구 호출 순서, 하위 에이전트 협업, 프롬프트 프리셋, 실패 복구까지 묶어 실제 “작동하는 시스템”으로 만드는 layer 입니다. (참조: <a href="https://aakashgupta.medium.com/2025-was-agents-2026-is-agent-harnesses-heres-why-that-changes-everything-073e9877655e">2025 Was Agents. 2026 Is Agent Harnesses. Here’s Why That Changes Everything.</a>)</li>
</ul>
<hr>
<h2 id="1-opencode-">1. Opencode ?!</h2>
<h3 id="1-정의-및-배경">1) 정의 및 배경</h3>
<p>OpenCode는 스스로를 <strong><em>오픈소스 AI 코딩 에이전트로</em></strong> 소개하며, 터미널 기반 인터페이스, 데스크톱 앱, IDE 확장 형태로 사용할 수 있습니다. LSP 지원, 멀티 세션, 세션 공유 링크, 다양한 모델 프로바이더 연결을 핵심 특성으로 내세웁니다. 즉 OpenCode는 단순한 “채팅형 코딩 도구”라기보다, 에이전트를 실행하는 런타임에 더 가깝습니다.</p>
<ul>
<li>LSP 기반 코드 이해(심볼/정의/진단 등)</li>
<li>멀티 세션(작업별 세션 관리)</li>
<li>플러그인 지원(동작 확장)</li>
<li>결과 공유(링크 공유) 및 팀 사용 시나리오</li>
</ul>
<p>즉, OpenCode 자체가 이미 <strong>*<span style="color: rgb(99 148 255);">“에이전트 실행기 + 도구 런타임 + 세션 관리자”</span>*</strong> 성격을 갖고 있고, 그 위에 플러그인을 얹어 하네스 역량을 키우는 구조입니다. </p>
<h3 id="2-작동-원리">2) 작동 원리</h3>
<p>아주 아주 기본적인 개념은 &quot;다양한 LLM 들을 하나의 tool 에서 모아서 사용&quot; 이라는 점에서 크게 다르지 않습니다. 세션 단위로 작업 컨텍스트를 유지하며, 사용자는 TUI에서 입력/전환/실행을 수행합니다. <strong><em>(실행 시 TUI와 HTTP 서버가 함께 뜨는 client/server architecture)</em></strong></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4fb91142-f6b4-4bf5-94bb-0dda67627a0b/image.png" alt=""></p>
<p>최근에는 <code>mode</code> 보다 <code>agent</code> 구성이 중심이지만, 사용 경험상 여전히 많은 분들이 <code>Build</code> 와 <code>Plan</code> 을 “모드”처럼 이해하고 있습니다. <del>(물론 저도요 ㅎ)</del></p>
<p>내장 <strong>primary agent</strong>는 <code>Build</code> 와 <code>Plan</code> 이고, <strong>subagent</strong>로는 <code>General</code> 과 <code>Explore</code> 가 제공됩니다. (근데 다들 각자 플러그인을 많이 사용하다보니 순정을 보기 힘들어진 기분)</p>
<ul>
<li><code>Build</code> 는 일반 개발 작업용으로 모든 도구 접근이 열려 있고, 실제 편집, 패치, 명령 실행, 검증에 적합합니다.</li>
<li><code>Plan</code> 은 파일 수정과 bash 실행을 제한하거나 승인 기반으로 다뤄 분석과 설계 중심으로 쓰이도록 설계되어 있고, 분석, 설계, 변경안 검토, 위험 식별에 적합합니다. (기본적으로 파일 수정과 bash 실행이<code>ask</code> 로 제한됨)</li>
<li><code>General</code> 은 범용적이게 사용하고,<code>Explore</code> 는 읽기 &amp; 탐색 중심!</li>
<li>또 내부적으로는 <code>compaction</code>, <code>title</code>, <code>summary</code> 같은 숨겨진 시스템 에이전트가 자동 실행된다고 합니다. </li>
</ul>
<p>사실 이런 접근은 이제 아주 일반화가 된 것 같습니다. 저 역시 바이브 코딩과 LLM에 대한 대규모 서베이, Augmented Coding 글과 같이 &quot;plan&quot; 의 중요성을 매우 체감하고 있네요.</p>
<h4 id="사실-권한-제어와-lsp-control-이-핵심인-듯-한">사실 권한 제어와 LSP control 이 핵심인 듯 한?!</h4>
<p><strong>*<span style="color: rgb(99 148 255);">[ 권한 제어와 도구들의 디테일 ]</span>*</strong></p>
<ul>
<li>권한은 <code>allow</code>, <code>ask</code>, <code>deny</code>로 제어되고</li>
<li><code>read</code>, <code>edit</code>, <code>bash</code>, <code>task</code>, <code>lsp</code>, <code>webfetch</code>, <code>websearch</code>, <code>codesearch</code> 같은 항목별로 통제한다고 합니다. </li>
<li><code>grep</code>, <code>glob</code>, <code>list</code> 는 내부적으로 <code>ripgrep</code> 을 사용하며, <code>.gitignore</code> 를 따른다고 하며 (디테일), 사실 LLM 기반으로 마치 Re-ACT agent, tool calling 처럼, 도구 호출 중심 실행 프레임워크로 만들어졌다고 보는게 맞는 것 같네요.</li>
</ul>
<p><strong>*<span style="color: rgb(99 148 255);">[ LSP(Language Server Protocol) ]</span>*</strong></p>
<ul>
<li>LLM이 코드베이스와 상호작용할 때 진단 정보(diagnostics) 를 활용합니다. 또한 여러 언어에 대해 내장/자동 설치 LSP를 제공한다고 합니다. </li>
<li>그래서 OpenCode는 단순히 파일 내용을 긁어서 모델에 던지는 수준이 아니라, 정적 분석 계층의 피드백까지 모델 루프 안으로 넣는 구조입니다. <del>(그래서 토큰이 녹아내릴 수 있죠.)</del></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4ec6abd6-e5ed-4dfd-9eb7-c8d618713e2e/image.png" alt=""></p>
<h3 id="3-플러그인-시스템-하네스가-붙는-자리">3) 플러그인 시스템: 하네스가 붙는 자리</h3>
<p>플러그인은 JavaScript/TypeScript &quot;모듈 형태로 훅&quot;을 내보내며, 로컬 플러그인 디렉터리 또는 <code>npm</code> 패키지 방식으로 로드할 수 있습니다.</p>
<ol>
<li>로컬 파일: 프로젝트/전역 플러그인 디렉터리에 JS/TS 배치</li>
<li>npm 패키지: opencode.json의 plugin 배열에 패키지명을 등록</li>
</ol>
<p>npm 플러그인은 시작 시 Bun으로 자동 설치되며, 캐시는 <code>~/.cache/opencode/node_modules/</code> 에 default로 저장됩니다. </p>
<p>로드 순서는 <strong>[ 전역 config → 프로젝트 config → 전역 dir → 프로젝트 dir ]</strong> 입니다. 에이전트 하네스 쪽은 보통 설정 충돌, 훅 순서, 컨텍스트 주입 순서 때문에 디버깅이 어려운데, OpenCode는 적어도 플러그인이 어디서, 어떤 순서로 붙는지를 문서화해 둔 편입니다. 그래서 플러그인 생태계가 잘 만들어질 수 있었던 것 같네요. </p>
<h3 id="4-설치--세팅">4) 설치 &amp; 세팅</h3>
<p>(작성일 기준이라 공식 홈페이지 한 번 참고하시는게 좋습니다. - <a href="https://opencode.ai/ko">https://opencode.ai/ko</a>), 최근에는 데스크톱 앱이 나온 것 같네요!</p>
<p><code>curl -fsSL https://opencode.ai/install | bash</code></p>
<p>역시 인기있는 tool 들은 설치와 세팅이 아주 간단합니다. 설치 후 <code>opencode</code> 로 실행이 끝입니다. 이후 <code>/connect</code> 커맨드 활용해서 &quot;프로바이더 (상용 LLM 포함 외부 LLM auth 세팅)&quot; 세팅까지 이어가면 바로 사용 가능합니다!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/cc6a6dc3-0f33-4f18-bcdc-a40786eeb0c7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/40f3eac2-3994-401a-9faa-58609f3dfaa8/image.png" alt=""></p>
<hr>
<h2 id="2-omo-oh-my-opencode-">2. OMO: Oh-my-opencode ?!</h2>
<h3 id="1-정의-및-배경-1">1) 정의 및 배경</h3>
<blockquote>
<p>Oh My OpenCode는 공식 사이트에서 스스로를 <strong>“OpenCode 위에 올라가는 specialized orchestration layer”</strong> 라고 설명합니다. OMO는 OpenCode를 대체하는 별도 제품이 아니라, OpenCode 위에 에이전트, 훅, MCP, LSP, 설정값을 묶어 더 강한 운영 구조를 제공하는 플러그인 입니다!</p>
</blockquote>
<p>그냥 “코드를 잘 써주는 도구”가 아니라</p>
<ul>
<li>복잡한 빌드 파이프라인 이해</li>
<li>다수 에이전트 병렬 실행</li>
<li>컨텍스트 관리</li>
<li>작업 지속성</li>
<li>세션 복구</li>
<li>문서 검색과 코드 탐색 자동화</li>
</ul>
<p>를 기반으로 &quot;잘 굴러가게 만드는 하네스&quot; 를 지향합니다. &quot;AI 팀&quot; 에 비유 하며 다수의 전문 에이전트(역할 분업), 스킬(워크플로 템플릿), 커맨드(/refactor 등), 훅(키워드 감지/복구/알림/컨텍스트 주입)을 묶어서 제공한다고 설명합니다. (<a href="https://github.com/code-yeongyu/oh-my-opencode">플러그인 공식 깃헙 레포</a>, <a href="https://news.hada.io/topic?id=24978">관련 긱뉴스</a>, <a href="https://youtu.be/o-rE93-nLpY?si=y4calJcK_Bf4vcq7">제작자 유튜브 인터뷰(팟캐스트)</a>)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/488c8818-b759-4b1d-99bc-72a857984e63/image.png" alt=""></p>
<h3 id="2-oh-my-opencode의-각-모드는-무엇이며-언제-쓰는가">2) Oh-My-OpenCode의 “각 모드”는 무엇이며, 언제 쓰는가</h3>
<p>OMO는 기본적으로 <code>Planner-Sisyphus</code>, <code>Librarian</code>, <code>Explore</code>, <code>Oracle</code> 같은 전문 에이전트를 제공합니다. <code>Sisyphus</code> 를 기본 오케스트레이터로 설명하고, <code>Prometheus</code>, <code>Metis</code> 같은 계획 보조 에이전트, 그리고 <code>frontend-ui-ux-engineer</code>, <code>document-writer</code>, <code>multimodal-looker</code> 같은 역할 특화 에이전트가 있습니다. </p>
<h4 id="다수-에이전트를-ai-팀으로-제공">다수 에이전트를 “AI 팀”으로 제공</h4>
<ol>
<li>Sisyphus: 기본 오케스트레이터(계획·위임·실행)</li>
<li>Prometheus / Metis / Momus: 계획 수립·사전 점검·계획 리뷰</li>
<li>Oracle / Librarian / Explore: 설계·문서/OSS 리서치·코드베이스 탐색(쓰기 제한)</li>
<li>document-writer: README, API 문서, 가이드 작성</li>
<li>multimodal-looker: PDF/이미지/다이어그램 분석</li>
</ol>
<p><code>@</code> 를 통해서 에이전트를 타겟할 수 도 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ef128e69-769f-4a49-8170-2831b2d789bf/image.png" alt=""></p>
<h3 id="3-ultrawork-search-analyze-">3) ultrawork, search, analyze ?!</h3>
<p>이게 지금의 OMO를 만들어준, 하이라이팅될 수 있던 feature들이 아닐까 하네요, OMO는 &quot;키워드 기반 감지&quot; 로 &quot;하네스&quot; 가 작동이 됩니다. <code>ultrawork</code> 또는 <code>ulw</code> 는 최대 성능 모드, <code>search</code> 또는 <code>find</code> 는 병렬 탐색 모드, <code>analyze</code> 또는 <code>investigate</code> 는 심층 분석 모드로 안내가 됩니다. 또 <code>think deeply</code>, <code>ultrathink</code> 같은 표현은 <code>think mode</code> 훅이 감지해 추론 설정을 조정한다고 합니다. </p>
<p>사용자가 “이번 작업의 성격”만 잘 말해도 하네스가 행동 방식을 바꿔주기 때문입니다. 즉 프롬프트가 단순 지시문이 아니라, 오케스트레이션 정책을 바꾸는 신호가 됩니다.</p>
<h4 id="span-stylecolor-rgb99-148-255ultraworkulw--최대-성능-모드span"><strong>*<span style="color: rgb(99 148 255);">ultrawork/ulw = 최대 성능 모드</span>*</strong></h4>
<p>ultrawork, 줄여서 ulw는 OMO README에서 사실상 “마법의 단어”처럼 소개됩니다. 공식 README 표현을 정리하면, 병렬 에이전트 실행, 백그라운드 작업, 적극적 탐색, 완료까지 밀어붙이는 성격을 가진 최대 성능 모드입니다. 대규모 리팩토링, 복잡한 마이그레이션, 여러 파일과 여러 축의 검증이 동시에 필요한 작업에 잘 맞습니다.</p>
<ul>
<li>작업 범위가 크고(리팩토링/마이그레이션/대규모 기능 추가)</li>
<li>실패 비용이 높고(프로덕션/핵심 모듈)</li>
<li>여러 축(리서치·코드·테스트·문서)을 병렬로 돌려야 할 때</li>
</ul>
<p>기본 오케스트레이터가 작업을 분해하고, 전문 에이전트를 공격적으로 병렬 실행하는 성격으로 설계돼 있다고 설명합니다. 즉 자체적으로 &quot;각 업무 전문가에게 일을 할당하고, 평가하고, 리팩토링하고 등&quot; 이 모두 묶여있습니다. (다음 섹션에서 저는 어떻게 ulw 를 사용하는지 정리해 뒀습니다!)</p>
<h4 id="searchfind--병렬-탐색-모드">search/find = 병렬 탐색 모드</h4>
<p>이 모드는 빠른 코드베이스 탐색이 핵심입니다. OpenCode의 내장 <code>Explore</code> 가 원래 읽기 전용 탐색 성격을 갖고 있는데, OMO는 이런 탐색 성격을 더 공격적으로 활용합니다. 레거시 프로젝트 진입점 찾기, 설정 파일 찾기, 실제 호출 경로 파악, 특정 동작이 어디서 시작되는지 확인할 때 특히 유용합니다.</p>
<ul>
<li>레거시 프로젝트 온보딩</li>
<li>특정 동작의 진짜 진입점/호출 경로를 찾아야 할 때</li>
<li>설정 파일/핵심 클래스/핫스팟 파일을 빠르게 식별해야 할 때</li>
</ul>
<h4 id="analyzeinvestigate--심층-분석-모드">analyze/investigate = 심층 분석 모드</h4>
<p>이 모드는 구현보다 해석과 판단에 가깝습니다. 장애 원인 분석, 설계 리뷰, 트레이드오프 비교, “왜 이런 구조가 되었는가”를 증거 기반으로 정리할 때 잘 맞습니다. 공식 사이트의 <code>Oracle</code> 소개와 README 설명을 합치면, 코드 설명과 문제 진단, 아키텍처 판단을 보조하는 방향으로 이해할 수 있습니다.</p>
<ul>
<li>장애 재현이 어렵거나, 원인이 여러 후보로 갈릴 때</li>
<li>설계 결정을 내려야 하는데 트레이드오프가 복잡할 때</li>
<li>“왜 이 로직이 이렇게 됐는지”를 증거 기반으로 정리해야 할 때</li>
</ul>
<p>문서는 oracle을 “아키텍처 결정/코드 리뷰/디버깅(읽기 전용)” 상담역으로 설명합니다.</p>
<h4 id="think-mode">think mode</h4>
<p>Think mode는 구현 이전 사고 비용을 늘리는 장치입니다. “바로 고치지 말고 먼저 깊게 생각해라”라는 의도를 하네스 차원에서 반영합니다. 문제 정의가 불분명하거나, 정책과 SDK 제한을 함께 검토해야 하거나, 근거를 모아 판단해야 할 때 특히 유효합니다.</p>
<ul>
<li>구현 이전에 문제 정의/요구사항 정리/리스크 식별이 필요할 때</li>
<li>장단점/대안 비교가 본질일 때</li>
<li>문서·정책·SDK 제한 때문에 “확실한 판단”이 필요한데 근거를 모아야 할 때</li>
</ul>
<p>think-mode 훅이 관련 키워드를 감지해 모델 설정(extended thinking 등)을 조정한다고 명시합니다. (사견으로는 생각 자체에 회귀를 할 때가 있어서 적절한 결론에 대한 신호 체계를 만드는게 좋습니다!)</p>
<h3 id="4-설치--세팅-1">4) 설치 &amp; 세팅</h3>
<p>물론 <code>OpenCode</code> 설치가 무조건 선행되어야 합니다. 하지만, 역시 간단합니다. (<code>bun</code> 런타임이 필요합니다.)</p>
<p><code>bunx oh-my-opencode install</code></p>
<p>그러면 세팅 가이드가 자동으로 안내를 해줍니다!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/425eb39a-dc24-424a-89dc-8dbb89bf7177/image.png" alt=""></p>
<hr>
<h2 id="3-실제-사용-예시">3. 실제 사용 예시</h2>
<h4 id="일단-하나의-예시를-보면">일단 하나의 예시를 보면,,</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ba0f044e-9d1d-4583-a379-bd613c65f26f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/81bb3cb9-3161-4278-9031-b79e1ac67911/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ebc7d35d-088b-412a-9c73-1e7be44971fd/image.gif" alt=""></p>
<p>OMO의 <code>ulw</code> 는 진짜 한 줄만 줘도 진정한 의미의 &quot;바이브코딩&quot; 을 합니다. &quot;딸각&quot; 이 가능하죠. 근데 <del>제가 여전히 구시대적인 사람인지 몰라도</del> 저는 이게 너무 와닿지 않습니다. &quot;통제 가능성&quot; 과 &quot;일관성&quot;, &quot;규칙&quot; 이 저에게는 너무 중요해서..</p>
<h3 id="1-일단-플젝-세팅-부터-제대로">1) 일단 플젝 세팅 부터 제대로</h3>
<h4 id="1-무엇을-만들어야-하는지-결정된다면-stack-부터-정합니다">1. 무엇을 만들어야 하는지 결정된다면 <code>stack</code> 부터 정합니다.</h4>
<ul>
<li>이땐 리서치와 대화형 LLM만 사용합니다. 사실 &quot;통제가능성&quot; 이 중요하기에 제가 익히 잘 아는 stack에서 잘 벗어나지 않습니다. </li>
<li>대부분 ts, react + nextjs, nestjs, python, django + drf or ninja ... 가끔 rust 섞음</li>
<li>아직까진 모노레포를 선호하지는 않습니다. 최근에 모노레포로 한 번 했다가 AI markdown 세팅이나 AI rule 세팅이 더 복잡해져서 바로 버렸습니다.</li>
</ul>
<h4 id="2-agentsmd-claudemd-등-포함-부터-출발합니다">2. <code>AGENTS.md</code> (CLAUDE.md 등 포함) 부터 출발합니다.</h4>
<ul>
<li>요즘은 <code>/init</code> 으로 직접 이 마크다운을 만드는 경우가 많지만, 저는 여전히, <a href="https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding">Augmented Coding</a> 에서 차용한 TDD와 Tidy code를 아주 적극적으로 사용합니다.</li>
<li>그래서 <code>AGENTS.md</code> 에는 &quot;논리적인 방법론&quot; 들을 정리합니다. 기본적인 역할, TDD, Tidy First, Quality 등에 대해서요.</li>
<li>디렉토리나 스택, 언어 등을 언급하지 않고, 대신 &quot;SYSTEM_DESIGN.md 꼭 참조해라!&quot; 라는 인디케이터만 넣습니다. </li>
</ul>
<h4 id="3-system_designmd-를-만듭니다">3. <code>SYSTEM_DESIGN.md</code> 를 만듭니다.</h4>
<ul>
<li>일례로 아래와 같습니다. 제가 <code>python</code> 으로 작업할땐 꼭 아래 시스템 디자인으로 출발합니다. 포인트는 <strong><em><code>Do not over-apply design patterns</code></em></strong></li>
</ul>
<pre><code># SYSTEM_DESIGN

This document defines the core system design rules for this project.

---

## 1. Python Version &amp; Typing

- We use **Python 3.13 or higher**.
- Do **not** use the `typing` module for type hints.
- Always use **built-in types** for annotations (e.g., `int`, `str`, `list`, `dict`, etc.).

---

## 2. Code Style

- Follow the **Google Python Style Guide**:
  - &lt;https://google.github.io/styleguide/pyguide.html&gt;
- Follow **PEP 8** (Python’s official style guide):
  - &lt;https://peps.python.org/pep-0008/&gt;

If there is any conflict between local conventions and these guides, prefer clarity and consistency within this project.

---

## 3. Object-Oriented Design

We favor **object-oriented programming (OOP)** and its core principles:

- **Encapsulation** – Group related data and behavior inside classes and hide internal details.
- **Abstraction** – Expose clear interfaces and hide unnecessary implementation details.
- **Inheritance** – Reuse behavior via well-designed base classes and subclasses when appropriate.
- **Polymorphism** – Design interfaces so that different implementations can be used interchangeably.

OOP should improve readability and maintainability, not add unnecessary complexity.

---

## 4. Architectural Pattern

- We **aim for a layered architecture pattern** (e.g., presentation, application/service, domain, infrastructure).
- Each layer should have a clear responsibility and minimal knowledge of other layers.

At the same time:

- Do **not** over-apply design patterns or split the codebase into too many tiny files.
- Avoid “architecture for architecture’s sake.”
- Aim for:
  - **Reasonable maintainability**
  - **Reasonable separation of concerns**
  - **Always pragmatic, balanced design**

In short, we prefer a **practical, moderately layered architecture** that is easy to understand, extend, and maintain, rather than a theoretically &quot;perfect&quot; but over-engineered structure.</code></pre><ul>
<li>이 파일에서 저는 &quot;물리적인 방법론&quot; 과 실제 구현 방향에 대해 정확하고 구체적으로 정리합니다. </li>
<li>특히 python 은 &quot;빠르게, 저렴하게&quot; 라는 측면에서 바이브코딩과 아주 맞닿아 있지만, 언어 특성때문에 볼륨이 조금만 커져도 무너져 내리더라구요. 1부터 10개 작업하면, 2번과 9번의 코드 스타일이 완전하게 달라지는 이슈도 빈번했구요. </li>
<li>그래서 type 을 꽤나 엄격하게 다루는데, <code>mypy</code> 보다는 <code>pyright</code> 가 좀 더 유연한 측면에서 맞는 것 같네요. </li>
</ul>
<h4 id="4-pre-commit-ruffeslint--prettier-testpytest-jest-세팅을-바로-합니다">4. <code>pre-commit</code>, <code>ruff(eslint &amp; prettier)</code>, <code>test(pytest, jest)</code> 세팅을 바로 합니다.</h4>
<ul>
<li>저는 <code>uv + ruff</code> 으로 아래 기본 세팅은 하고 갑니다. 스크롤이 너무 길어져서 뺄까 했는데, 혹시나 다른 분들을 위한 설정 값 공유!</li>
</ul>
<p>(아래 pre-commit)</p>
<pre><code class="language-yaml">ci:
  autoupdate_schedule: monthly

default_language_version:
  python: python3.13

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
      - id: check-merge-conflict
      - id: end-of-file-fixer
      - id: trailing-whitespace

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.0
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/astral-sh/uv-pre-commit
    rev: 0.10.2
    hooks:
      - id: uv-export
        args:
          - --frozen
          - --no-dev
          - --no-hashes
          - --output-file=requirements.txt
          - --quiet</code></pre>
<p>(아래 toml 에서 기본 설정들)</p>
<pre><code class="language-toml"># ================================================================
# Ruff (Linter &amp; Formatter) Settings
# ================================================================
[tool.ruff]
# 수정에서 제외할 파일 및 디렉토리 목록
exclude = [
    &quot;.bzr&quot;,
    &quot;.direnv&quot;,
    &quot;.eggs&quot;,
    &quot;.git&quot;,
    &quot;.hg&quot;,
    &quot;.mypy_cache&quot;,
    &quot;.nox&quot;,
    &quot;.pants.d&quot;,
    &quot;.pytype&quot;,
    &quot;.ruff_cache&quot;,
    &quot;.svn&quot;,
    &quot;.tox&quot;,
    &quot;.venv&quot;,
    &quot;__pypackages__&quot;,
    &quot;_build&quot;,
    &quot;buck-out&quot;,
    &quot;build&quot;,
    &quot;dist&quot;,
    &quot;node_modules&quot;,
    &quot;venv&quot;,
    &quot;*/migrations/*.py&quot;,
]
# 한 줄의 최대 글자 수
line-length = 100

# --- Linter (코드 분석기) 설정 ---
[tool.ruff.lint]
# 활성화할 규칙 선택:
# E, W: pycodestyle (에러, 경고)
# F: Pyflakes (논리적 오류)
# I: isort (import 정렬)
select = [&quot;E&quot;, &quot;F&quot;, &quot;W&quot;, &quot;I&quot;]
ignore = []

# 모든 수정 가능한 규칙을 자동으로 고치도록 설정
fixable = [&quot;ALL&quot;]
unfixable = []

# --- Formatter (코드 포맷터) 설정 ---
[tool.ruff.format]
# Black과 유사한 포맷팅 스타일을 따릅니다.
quote-style = &quot;double&quot;
indent-style = &quot;space&quot;
skip-magic-trailing-comma = false
line-ending = &quot;auto&quot;

# --- 플러그인별 상세 설정 ---
[tool.ruff.lint.isort]
# 프로젝트에서 사용하는 서드파티 라이브러리 목록
# known-third-party = [&quot;django&quot;, &quot;graphene_django&quot;]

# --- 파일/디렉토리별 규칙 무시 설정 ---
[tool.ruff.lint.per-file-ignores]
# settings 파일: 와일드카드 import 허용
# &quot;config/settings/*&quot; = [&quot;F403&quot;, &quot;F405&quot;]

# __init__.py 파일: 하위 모듈 노출을 위한 미사용/와일드카드 import 허용
&quot;**/__init__.py&quot; = [&quot;F401&quot;, &quot;F403&quot;]

# 테스트 파일: 가독성을 위해 긴 줄 허용
&quot;**/test_*.py&quot; = [&quot;E501&quot;]</code></pre>
<ul>
<li>js&amp;ts 쪽 파일도 붙여넣을까 했는데 진짜 너무 과하게 길어질 것 같아서 생략.. </li>
<li>맥락은 같습니다. 어차피 AI 가 또는 next(or nest) 를 쓰든 프로젝트 init 하면 린터와 포매터 세팅은 따라 나옵니다. 이걸 입맛에 맞는 커스텀 템플릿으로 바꾸는 정도!</li>
<li>그리고 ts 경우 type 을 얼마나 strict 하게 할지, 미리 세팅해두면 아주 도움이 되는 것 같네요. </li>
</ul>
<h4 id="5-github-action-ci-부터-역시-바로-세팅합니다">5. github action CI 부터 역시 바로 세팅합니다.</h4>
<ul>
<li>내용이 너무 길어서 스킵,, 일부분만 사진으로 대체</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/077b9a63-bda1-4dda-a2e1-5995acc7a2ba/image.png" alt=""></p>
<ul>
<li>그리고 <code>github cli</code> 인 <code>gh</code> 를 기가막히게 잘 쓰더라구요. 그래서 더욱이 CI 부터 세팅하려고 합니다. 위는 python, uv 기반 CI 파이프라인이고, 처음부터 멀티 버전 (매트릭스) 대상으로 하지는 않습니다. </li>
</ul>
<h3 id="2-그리고-작업-시작-planmd-부터">2) 그리고 작업 시작, plan.md 부터!</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a6f246a9-0ad0-40ee-9f8d-6997dd235bb7/image.png" alt=""></p>
<ul>
<li>저는 무조건 <code>plan.md</code> 부터 작성합니다. 이게 <code>AGENTS.md</code> 에서 명시한 것들과 일맥상통하기도 하고, &quot;통제 가능성&quot; 이 여전히 중요하기도 하구요. <strong>*<span style="color: rgb(99 148 255);">그리고 여전히 저는 무조건 테스트 코드 부터 작성합니다.</span>*</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c237cde5-d29e-4fc5-9c1d-1ccf2c11316c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/7c8c5fdd-1c73-40bf-8c0f-da05e584e676/image.png" alt=""></p>
<ul>
<li>이를 병목으로 보는 시선도 많아졌더라구요. 되돌아보면 과거에 비해 plan 을 좀 더 넓은 범위로 작성합니다. 예전에는 딱 한 작업, 한 commit 단위 대상으로 plan 을 작성했었습니다. (A 라는 API 만 바꾸고 싶다던지 등)</li>
<li>지금은 조금 더 범위가 큽니다. &quot;알림 관련 기능을 만들건데 이러이러해~ 이러이러한 것을 위한 ORM 모델 부터 핵심 API 만 만들자~&quot; 이런 느낌으로요.</li>
<li>더욱이 plan.md 는 이제 하나의 &quot;체크포인트&quot; 이기도 합니다. 그래서 저는 <code>plans</code> 디렉토리에 따로 모아둬요!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/5e381999-2163-4690-9fc7-b1728e9e6a0d/image.png" alt=""></p>
<ul>
<li>근데 opencode &amp; omo 를 쓰시면 <code>.sisyphus</code> 경로에 자동으로 저장이 되긴 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/62afe1b8-454a-4a72-bd7c-826215e6937c/image.png" alt=""></p>
<h4 id="planmd-를-작성했으면-이제-이를-기반으로-작업을-합니다">plan.md 를 작성했으면 이제 이를 기반으로 작업을 합니다!</h4>
<ul>
<li>opencode + omo 에서는 원래 <code>/start-work</code> 라는 사전 명령어를 사용해서 시작합니다. 그러면 <code>.sisyphus</code> 경로에 자동 저장된 plan 찾아서 바로 작업을 이어가고, agent 에게 일을 할당해주기 시작합니다!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/368a5d4e-691d-410f-93f9-334f6ae3c7c8/image.png" alt=""></p>
<ul>
<li>제가 쓰는 <code>AGENTS.md</code> 에 따르면 <code>go</code> 라는 시그널만 주는데, 이는 좀 때에 따라 다르게 하긴 합니다. <code>ulw</code> 가 필요하다 싶으면 Sisyphus 를 사용합니다!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/0add6d91-5bc7-432a-85f6-172de4bb4d77/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/72925fe5-d153-46a0-97fc-7449881c2eab/image.gif" alt=""></p>
<ul>
<li>여러 에이전트에게 자동 분할 및 위임되고, 병렬로 작업되는 것을 확인할 수 있습니다. 가장 좋은건, opencode 가 &quot;에이전트의 실행 상태&quot; 에 대해 개괄적으로 모니터링하는 GUI(TUI)가 타 native cli (claude, codex 등) 보다는 좋다는 점!</li>
</ul>
<h4 id="plan-규모가-좀-있다면-무조건-검토를-시킵니다">plan 규모가 좀 있다면 무조건 검토를 시킵니다.</h4>
<pre><code>plan.md 기반으로 모든 사항이 적용되었는지 A to Z 를 검토해야 해.
아래 사항에 따라 검토하되, 체크 박스도 모두 제대로 처리 해.

1. AGENTS.md 와 SYSTEM_DESIGN.md 를 1순위로 따르고 있는지 체크.
2. 변경에 따른 하위호환성과 영향 범위를 절대 잊지말고 더블 체크.
3. 관련된 테스트 코드 역시 업데이트 되어야 해.
4. 관련된 테스트가 과하거나, 중복되거나, 이미 자명한데 쓸데없는 테스트를 하거나 하지 않는지 체크
5. 2025년, 2026년 외부 공식 자료와 외부 best example 을 참고해서 개선해줘. 최대한 비판적으로 수용하고 지금 코드를 업데이트 해야 해.

이를 위해 다양한 모델과 서브에이전트를 적극적으로 활용해.</code></pre><h3 id="3-다음-턴-부터는-상황에-따라">3) 다음 턴 부터는 상황에 따라</h3>
<ul>
<li>저는 Plan 을 한 번 하면 검토를 하고, 검토 output 이 최종이라고 생각하며 자체 H-I-L(human in the loop), E2E 를 합니다. </li>
<li>그 과정에서 리팩토링 &amp; 세부 피쳐를 작업할땐 바로 plan 을 짜지 않고 이때부턴 기본적인 AI md (AGENTS.md 등) 만 활용하고 <strong>*<span style="color: rgb(99 148 255);">skill 을 적극적으로 사용</span>*</strong> 합니다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/0b9fcb47-b4fc-4a0d-b46c-89c44e4f3b48/image.png" alt=""></p>
<ul>
<li>지금은 거의 3가지 중 하나를 쓰게되더라구요. 특히 제일 많이 쓰는건 <code>ui-ux-pro-max</code> </li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f9ffbe21-832a-4bb4-a152-701ee5905279/image.png" alt=""></p>
<ul>
<li>이런식으로 <code>skill</code> 과 하네스를 섞어서 쓰기도 합니다. 이땐 작업 범위가 좁을수록 output 이 좋았습니다. </li>
</ul>
<h3 id="4-절대-agentsmd-system_designmd-등을-그대로-두지-않습니다">4) 절대 AGENTS.md, SYSTEM_DESIGN.md 등을 그대로 두지 않습니다.</h3>
<ul>
<li><p>진행할 수 록 layer 는 많아지고 무조건 프로젝트는 복잡성이 올라갑니다. 그래서 무조건 이 AI를 위한 마크다운을 초기설정 그대로 두지 않습니다!</p>
</li>
<li><p><strong><em>무조건 프로젝트 현 상태에 따라 맞춰 업데이트를 합니다!</em></strong> 일례로, 동시성에 대한 경고, web이 아닌 OS응용 프로그램, GUI를 위한 룰, 또는 FE 작업할땐 &quot;<code>DESIGN_SYSTEM.md</code>&quot; 도 만드는데, 이 디자인 룰 역시도요. </p>
</li>
</ul>
<h4 id="최대한-영어로-최대한-핵심만-짧고-요약해서">최대한 영어로, 최대한 핵심만 짧고 요약해서</h4>
<ul>
<li>AI를 위한 마크다운은 무조건 토큰에 영향을 주고, 이는 비용과 퍼포먼스에 영향을 줄 수 밖에 없습니다. 그래서 저는 무조건 영어로, 최대한 짧고 굵게 작성하려고 합니다.</li>
<li>특히 <code>AGENTS.md</code> 와 같이 기본적으로 에이전트가 물고가는 마크다운 파일은 더욱더요!</li>
</ul>
<h3 id="5-한-세션이-너무-길어진다면">5) 한 세션이 너무 길어진다면?</h3>
<ul>
<li>멀티 에이전트는 보통 아래 흐름입니다. (실제로 위에서 사용한 흐름이 모두 아래와 같죠.)</li>
</ul>
<pre><code>Research Agent
   ↓
Planner Agent
   ↓
Implementation Agent
   ↓
Reviewer Agent
   ↓
Documentation Agent</code></pre><p><img src="https://velog.velcdn.com/images/qlgks1/post/3b21dac8-18b9-4516-837a-e093dcaba1aa/image.png" alt=""></p>
<ul>
<li>한 세션에서 너무 볼륨 큰 작업을 하거나, 계속 한 세션에서 작업을 길게 이어간다면, 초기 지시 사항을 잊을 수 도 있습니다! 대표적으로 아래와 같은 허들, 이슈가 있죠.</li>
</ul>
<ol>
<li>긴 세션에서 컨텍스트가 사라짐</li>
<li>다른 에이전트가 작업을 이어받을 때 맥락 손실</li>
<li>작업 상태 / 결정 / 다음 단계가 사라짐</li>
</ol>
<ul>
<li>그럴때마다 <code>ctrl + c</code> 로 취소하거나 <code>/new</code> 로 바로 새로운 세션을 시작할 필요가 없습니다!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1be74b04-8f29-4e15-be55-ab9f4edbd80b/image.png" alt=""></p>
<ul>
<li><strong><em><code>/handoff</code></em></strong> 를 쓰면 됩니다! </li>
<li><code>/handoff</code> 는 현재 에이전트나 세션의 작업 상태(context)를 다른 에이전트 또는 다음 단계의 작업으로 넘기는 작업 전달 메커니즘입니다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/370ac63a-f280-4f70-bbe8-06e785e1909c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a6c92357-4914-4cba-8602-6d0c3aa0e3e1/image.png" alt=""></p>
<ul>
<li>자동으로 위 프롬프트가 세팅되고, 바로 그 다음 사진과 같이 핵심 작업이 자동으로 요약이 됩니다. (현재 작업 목표, 지금까지의 결정, 구현된 내용, 남은 작업, 다음 담당자)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/8cf52686-2195-4f43-bfa6-970852c3edd0/image.png" alt=""></p>
<ul>
<li>그냥 심플하게 새로운 세션으로 가서 해당 내용을 그대로 복사해서 사용하면 끝!</li>
</ul>
<h3 id="6-정리">6) 정리</h3>
<p>정리해 보면 opencode + OMO 세팅에서 아래 4~5가지 흐름으로만 사용하는 것 같습니다.</p>
<ol>
<li><p>프로젝트 세팅부터 AI 마크다운들 포함해서 제대로.</p>
</li>
<li><p>불확실한 요구사항/큰 작업</p>
<ul>
<li><code>Prometheus (Plan Builder)</code> 로 plan</li>
<li>때에 따라 <code>Atlas (Plan Executor)</code> 또는 <code>Sisyphus (Ultraworker)</code> 를 통해 <code>ulw</code> 를 붙여 병렬 실행을 유도(하네스 최대 가동). </li>
</ul>
</li>
<li><p>레거시 코드 파악이 먼저인 작업</p>
<ul>
<li>프롬프트에 <code>find</code> 또는 <code>search</code> 를 포함해 탐색 모드로 시작</li>
<li><code>@explore</code> 에게 “진입점/핵심 모듈/핫스팟 파일”을 먼저 뽑게 하고(쓰기 제한이 있어 안전), 이후 Build로.</li>
</ul>
</li>
<li><p>디버깅/근본원인 분석</p>
<ul>
<li>프롬프트에 <code>investigate</code>를 넣어 분석 모드로 전환</li>
<li>설계/논리 검토는 <code>@oracle</code> 을 “읽기 전용”으로 붙여 객관화</li>
</ul>
</li>
<li><p>볼륨이 크지는 않지만 특정 부분 (또는 역할) 만 업그레이드 할 경우</p>
<ul>
<li>스킬과 하네스 섞어서 사용</li>
<li>적극적으로 &quot;외부 공식 자료와 외부 best example&quot; 을 search 하게 유도</li>
</ul>
</li>
</ol>
<p>특히 3번 4번은 오픈소스 코드들 또는 이미 볼륨이 있고 커진 프로젝트에서 특정 부분만 집중할때 꽤나 좋았었습니다. 
요즘처럼 AI 관련 stack들이 생명주기가 반년도 안되는 시대에 이런 접근이나 방법들이 또 언젠가 레거시 처럼 여겨질지도 모르겠네요..!</p>
<h4 id="ps">PS...</h4>
<ul>
<li><p>아! 가끔 자기전에, 외출전에 랄프(Ralph Loop)를 가동하긴 합니다. 근데 개인적으로 랄프보단 <code>ulw</code> 를 프롬프트로 자동회귀하게 세팅하는게 체감 성능이 좋더라구요..</p>
</li>
<li><p>plan 을 가득 가득 만들어두고 한 호흡으로 진행하라는, 즉 무조건 끝날때까지 진행하는 프롬프트와 <code>ulw</code> 돌리시면 토큰 사용 다 할때까지 돌아가는 마법을 보실 수 도 있습니다..</p>
</li>
<li><p><code>opencode stats</code> 를 한 번 입력해보세요! - <a href="https://opencode.ai/docs/cli/">https://opencode.ai/docs/cli/</a></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9e7bc71b-48c7-4361-8c8a-cb49f63c4e1e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1ba303b3-0f9d-43de-8834-9d551babfd2a/image.png" alt=""></p>
<ul>
<li>저는 <code>Cache Read</code> 에 24억 7,120만 토큰 정도 사용했네요! <del>캐시라서 정말 다행</del> 약 30일간 <strong><em>총 27억</em></strong> 토큰 정도 사용했습니다. (그 이상의 기간은 비밀입니다.. 저도 알고싶지 않았습니다..)</li>
</ul>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://aakashgupta.medium.com/2025-was-agents-2026-is-agent-harnesses-heres-why-that-changes-everything-073e9877655e">2025 Was Agents. 2026 Is Agent Harnesses. Here’s Why That Changes Everything.</a></li>
<li><a href="https://opencode.ai/">오픈코드 공식 홈페이지</a></li>
<li><a href="https://github.com/code-yeongyu/oh-my-opencode">OMO 플러그인 공식 깃헙 레포</a></li>
<li><a href="https://news.hada.io/topic?id=24978">OMO 관련 긱뉴스</a></li>
<li><a href="https://youtu.be/o-rE93-nLpY?si=y4calJcK_Bf4vcq7">OMO 제작자 유튜브 인터뷰(팟캐스트)</a></li>
<li><a href="https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding">Augmented Coding</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/3590/">위시캣, 한 달 만에 20만 다운로드, 전 세계 홀린 한국 개발자</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] 우리, 프로그래머들 - 로버트 C. 마틴]]></title>
            <link>https://velog.io/@qlgks1/%EC%9A%B0%EB%A6%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EB%93%A4-%EB%A1%9C%EB%B2%84%ED%8A%B8-C-%EB%A7%88%ED%8B%B4</link>
            <guid>https://velog.io/@qlgks1/%EC%9A%B0%EB%A6%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EB%93%A4-%EB%A1%9C%EB%B2%84%ED%8A%B8-C-%EB%A7%88%ED%8B%B4</guid>
            <pubDate>Sun, 08 Feb 2026 17:26:52 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;길벗 출판사에서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="우리-프로그래머들">우리, 프로그래머들</h1>
<blockquote>
<p>로버트 C. 마틴: 로버트 C. 마틴(엉클 밥)은 1970년부터 프로그래머로 일해 왔다. 그는 엉클 밥 컨설팅(Uncle Bob Consulting, LLC)의 설립자이며, 아들 미카 마틴과 함께 클린 코더스(Clean Coders, LLC)를 공동 설립했다. 각종 업계 저널에 글을 수십 편 기고했고, 국제 콘퍼런스와 전시회에서 정기적으로 강연하고 있다. </p>
</blockquote>
<blockquote>
<p>저서 및 편저로는 <code>『Designing Object-Oriented C++ Applications Using the Booch Method』</code>, <code>『Pattern Languages of Program Design 3』</code>, <code>『More C++ Gems』</code>, <code>『Extreme Programming in Practice』</code>, <code>『Agile Software Development: Principles, Patterns, and Practices』</code>, <code>『UML for Java Programmers』</code>, <code>『Clean Code』</code>, <code>『The Clean Coder』</code>, <code>『Functional Design: Principles, Patterns, and Practices』</code> 등이 있다. 소프트웨어 개발 업계의 선도적 인물로 3년간 ‘The C++ Report’의 편집장을 지냈고, 애자일 얼라이언스(Agile Alliance)의 초대 의장을 맡았다. <strong><em>개발자에겐 너무 익숙한 우리 엉클밥 형님..</em></strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/cbc69a28-4c93-4430-8b65-4242cff43e27/image.png" alt=""></p>
<p><strong><em>🔥 길벗 책 링크 - <a href="https://gilbut.co/c/26015205VE">https://gilbut.co/c/26015205VE</a></em></strong></p>
<h2 id="리뷰">리뷰</h2>
<p>요새 개발자의 미래에 대한 글과 견해가 쏟아져 나옵니다. 더구나 온라인 세션, 멘토링을 하다 보면 “AI가 코딩 다 하는데 왜 배워요?”, “개발자는 이제 뭐가 남아요?” 같은 질문을 정말 자주 받습니다. 하꼬인 저도 그러는데 로버트 C. 마틴(엉클 밥)은 이런 질문을 얼마나 오래, 얼마나 많이 받아왔을까 싶어 웃음이 나기도 했습니다.</p>
<p>그 쏟아지는 담론 중 제가 가장 오래 붙잡게 된 키워드는 “대체”가 아니라 업의 재정의였습니다. 개발자는 사라지느냐/남느냐의 문제가 아니라, ‘개발자’라는 직업이 어떤 핵을 중심으로 다시 정의되느냐의 문제라는 관점입니다. 웹 퍼블리셔, 웹 디자이너가 “증발”하지 않았듯이, 다들 역할과 책임이 재배치되며 타이틀과 경계가 다시 그려지는 중이니까요.</p>
<p>최근에 꽤 재미있게 본 <strong>*&quot;AI가 코딩 다 하는데 왜 배워요?&quot; 에 대한 하버드 세션을 공유합니다. &gt;&gt; <a href="https://youtu.be/7ZJ4oo4h8vI?si=0bDPwk9GBJbv472b">1편</a>, <a href="https://youtu.be/3uwdBZBpO8E?si=YNJpAZki4vITI5da">2편</a>*</strong> 
거시적으로는 이 책이 말하는 바와 꽤 닮아 있다고 느꼈습니다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fd9aba62-cf3d-462d-a068-aa1e5ee6cc53/image.png" alt=""></p>
<p>추가로 Velog 에서도 아주 감명깊게 읽었던 테오님의 <strong><em><a href="https://velog.io/@teo/ai-and-developer">AI 시대의 개발자: 현업 개발자의 솔직한 이야기</a></em></strong> 도 같이 붙여봅니다!</p>
<p><strong>*<span style="color: rgb(99 148 255);">이 책은 “미래 예언서”가 아니라 “정체성의 정의서”에 가깝다!</span>*</strong></p>
<p>책 자체는 자서전을 포함한 에세이에 매우 가깝습니다. 처음에는 솔직히 “내가 말이야~ 수십 년을 했는데~ 미래는 이래~” 같은 류의 에세이라고 짐작했습니다. 그런데 실제로는 반대에 가까웠습니다. 이 책은 프로그래밍/컴퓨터 영역에서 ‘사실’과 ‘계보’를 쌓는 데 유난히 집요합니다.</p>
<p>그리고 그 사실들의 누적이 결국 1장(그리고 후반부 20장)에서 나오는 “미래에 대한 태도”를 떠받치는 근거가 됩니다. 저는 이 구조가 매우 설득력 있게 다가왔습니다. “내가 이렇게 생각한다”가 아니라, “내가 이런 사실들을 지나왔고, 그래서 이런 결론에 닿았는데, 당신은 이 사실들을 보고 무엇을 그리겠는가” 쪽에 더 가깝게 읽혔습니다.</p>
<p><strong>*<span style="color: rgb(99 148 255);">근데 좀 의아한 건 AI에 대해 생각보다 많이 냉소적으로 평가했다는 점</span>*</strong></p>
<p>좀 더 찾아보니 책의 초안이 2023년 말 ~ 2024년 사이에 작성되었다고 하네요. 놀랍게 여기에 대해 리서치를 좀 해보니 <strong><em><a href="https://gist.github.com/spilist/ae120f5d7b2f4e17c8b959e1f0ccab26">같은 의문을 가진 사람이 리서치 내용을 요약한게 있습니다! (클릭)</a></em></strong></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4402d2d0-5c39-4184-88d0-d2bb0d567419/image.png" alt=""></p>
<p><strong>*<span style="color: rgb(99 148 255);">책 소개에서는 각 부를 아래와 같이 설명합니다.</span>*</strong></p>
<p>1부에서 우리의 정체성을 되짚고,
2부에서 거장들의 길을 따라가며,
3부에서 저자의 경험을 통해 전환점을 마주하고,
4부에서 우리가 맞이할 미래를 바라본다.</p>
<p>저도 이 흐름에 거의 동의합니다. 특히 1부/2부가 책의 절반 이상을 차지하는 이유는, ‘미래’보다 ‘뿌리’를 보여주기 위해서라고 느꼈습니다. 개발자라는 역할의 시발점, 존재 가치, 정체성. 전쟁과 알고리즘, 루틴과 서브루틴, 추상화의 탄생 같은 이야기들이 여기서 살아납니다.</p>
<p>읽는 내내 대학 1학년 때 배웠던 개론 수업의 기억도 자꾸 떠올랐습니다. 저는 오히려 그때보다 한참 뒤에 개론을 더 깊게 찾아보고 공부했습니다. 그리고 C를 제대로 배울 때(재수강했을 때), 포인터와 주소, 메모리, 컴퓨터 구조, 운영체제의 가상 메모리가 하나로 맞물리며 머릿속에서 “번쩍”하던 순간—마냥 어렵기만 했던 공학적 지식이 실제 코딩 경험과 함께 퍼즐처럼 맞춰지던 순간을 아직도 잊기 어렵습니다. 비록 그 코드가 단순 출력 함수 호출이었더라도요.</p>
<p>그리고 지금은, 언어를 “학습해서 사용”하는 건 이미 너무 쉬워진 세상입니다. 그래서 모두가 학습의 재정의, 일의 재정의를 이야기합니다. 저 역시 1년 전과 비교하면 일을 처리하는 방식이 하늘과 땅 차이입니다.</p>
<p>그럼 순수 작업 시간이 줄었는가? 솔직히 꼭 그렇진 않습니다. 다만 단위 시간에 처리할 수 있는 일의 양이 체감상 5배 이상 늘었고, 퀄리티가 떨어졌다고 느끼지도 않습니다. 저는 여전히 ‘바이브 코딩’을 하진 않습니다. (솔직히 한 끗 차이긴 합니다. 그런데 그 한 끗이 진짜 큰 차이를 만든다고 믿습니다.)</p>
<p><a href="https://velog.io/@qlgks1/a-survey-of-vibe-coding-with-llm">바이브 코딩과 LLM에 대한 대규모 서베이, 논문 리뷰</a> 와 <a href="https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding">켄트 벡(Kent Beck) 형님과 함께하는 Augmented Coding, &quot;증강 코딩&quot;</a> 글 참조</p>
<p>체감적으로, 예전에는 <strong><em>[설계 &amp; 문서화 &amp; 구현(테스팅/코딩)/리팩터링]</em></strong> 의 비중이 대략 <code>3, 3, 4</code> 정도였다면, 지금은 <code>6, 3, 1</code> 정도로 바뀌었습니다. 그리고 “코딩 &amp; 리팩터링”은 <strong>무조건 diff(view diff)로 달라진 부분 위주로 끝까지 눈으로 최종 검토</strong> 합니다. 저는 이 변화가 “AI가 코딩을 대신해 준다”의 결론이라기보다, <strong>사람이 책임져야 하는 지점이 더 선명해지는 방향</strong> 이라고 느낍니다.</p>
<p>2부의 거장 파트는 디지털 논리 회로와 컴퓨터 구조에 대한 기본적인 이해—적어도 진공관, 레지스터, 플립플롭에 대한 최소한의 감각—이 없으면 솔직히 지루할 수도 있습니다. 그런데 저자가 풀어내는 방식은, 이해가 부족해도 몰입할 수 있을 만큼 ‘이야기’로 설계되어 있다고 느꼈습니다. (그리고 그 몰입이 결국, “지금 우리가 쓰는 추상화들이 어디서 왔는지”를 체감하게 만듭니다.)</p>
<p>3부는 엉클 밥이 살아온 환경에서 컴퓨터와 프로그래밍이 얼마나 폭발적으로 성장했는지를 보여줍니다. 자연스럽게 개론과 저자의 배경 위로 올라타게 되고, 정말 거시적인 관점에서 “미래가 어떻게 될지”(정확히는 저자가 어떤 미래를 상정하는지)가 어렴풋이 보이기 시작합니다.</p>
<p>저도 변화의 흐름이 파도처럼 거대하다고 느끼지만, 90년대 태생인 제가 체감하는 변화조차 엉클 밥 같은 형님이 눈앞에서 겪었던 변화의 결에 비하면 “생각보다 빠른 게 아닐 수도 있겠다”는 상대화가 되기도 했습니다.</p>
<p>그리고 4부에서 책은 덤덤하게 마무리합니다. 과장된 예언 대신, “프로그래밍의 핵은 생각보다 잘 안 바뀐다”는 태도에 가깝게요. 다만 그 덤덤함이 체념은 아니었습니다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c4b6feae-a902-4d19-b996-54678d6178f3/image.png" alt=""></p>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<h3 id="1부-서막을-열며">1부. 서막을 열며</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/36fc2ea7-7936-41af-ac69-59965125235f/image.jpg" alt=""></p>
<h4 id="1장-우리는-누구인가">1장. 우리는 누구인가?</h4>
<p>1장의 기능은 “정의”다. 프로그래머를 ‘문제를 해결하는 사람’으로만 두지 않고, <strong>디테일을 사랑하고 조합하여 의미를 만드는 사람</strong>으로 다시 규정한다. 이 규정은 기술 스택의 변화(언어/프레임워크/플랫폼)를 뛰어넘어, 직업적 정체성의 코어를 고정시키는 문장처럼 작동한다.</p>
<p>“디테일”이라는 단어가 AI 시대에 더 도발적으로 들리는 이유는, 생성형 AI가 바로 그 디테일(코드/문장/구현)을 대량 생산하기 시작했기 때문이다. 그러나 여러 서평이 공통으로 붙드는 축은 &quot;산출물&quot;이 아니라 &quot;책임&quot;이다.</p>
<p>내가 느낀 1장의 정의는 “AI가 대체할 것/못할 것”을 나누는 얄팍한 직업 전망이 아니라, <strong>AI와 함께 일할 때 무엇을 끝까지 붙들 것인가</strong>를 묻는 선언에 가까웠다. </p>
<blockquote>
<p><em>PS. 사실 1장이 책에서 하고 싶은 말을 다 담고 있다는 걸, 다 읽고 다시 깨닫게 되었다.</em></p>
</blockquote>
<h3 id="2부-거장">2부. 거장</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f7301fae-3ed1-4945-bbb1-8ed09123ed09/image.jpg" alt=""></p>
<h4 id="2장-배비지-최초의-컴퓨터-엔지니어">2장. 배비지, 최초의 컴퓨터 엔지니어</h4>
<p>“명령어(카드)와 데이터(숫자계기)의 분리”라는 감각은, 오늘날 관점에서 보면 너무 당연해 보이지만, 당시에는 “계산을 하는 장치”를 “계산을 <strong>기술하는</strong> 장치”로 끌어올리는 발상이었다.</p>
<p>배비지 장에서 인상적인 것은, 천재성보다도 <strong>실패의 형태</strong>다. 차분기관이 “실패했다”는 사실보다, 실패가 남긴 유산(레지스터적 사고, 반복 가능한 연산 단위, 표준화된 절차)이 더 중요하다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/cf378237-4439-4719-8ebe-4d913cd7361b/image.png" alt=""></p>
<h4 id="3장-힐베르트-튜링-그리고-폰-노이만-최초의-컴퓨터-아키텍트">3장. 힐베르트, 튜링, 그리고 폰 노이만: 최초의 컴퓨터 아키텍트</h4>
<p>3장에선 힐베르트의 아래 3가지가 주요 내용이었다.</p>
<ul>
<li>힐베르트의 공리화 시도 → (괴델) 불완전성의 충격  </li>
<li>그 흐름이 “증명=알고리즘(절차)”로 이어진다는 통찰  </li>
<li>그 다음, 계산 장치의 설계 철학(내장식 컴퓨터)로 연결</li>
</ul>
<p>1) <strong>수학의 충격(형식체계의 한계)</strong></p>
<ul>
<li>“수학 전체를 공리화”하려는 열망과 그 좌절은, ‘인간이 완전한 체계를 만들 수 있다’는 낙관의 붕괴로 읽힌다.  </li>
<li>하지만 “폰 노이만이 자연수는 러셀의 역설에 해당하지 않는다” 는 서포팅도 있었다.</li>
</ul>
<p>2) <strong>계산의 정의(Computability)</strong></p>
<ul>
<li>배비지 해석기관이 명령어(나무카드 구멍), 데이터(회전식 숫자계기) 완전 분리된 방식이 포인트였고, 이를 튜링이 해당 사고를 확장했다. 튜링머신이 제공한 것은, ‘컴퓨터’가 아니라 “무엇이 계산 가능한가”에 대한 정의였다.  </li>
<li>계산 가능성을 엄밀히 정의할수록, “계산 불가능한 것(혹은 계산 비용이 과도한 것)”이 더 선명하게 남는다.</li>
</ul>
<p><em>PS. 독일에서 폰(von) 귀족 칭호라고 한다.</em></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ab7d32d6-00a5-4f22-8486-5c4b16398fc9/image.png" alt=""></p>
<ul>
<li>위 그림은 튜링 머신의 계산 방식을 도식화 한 그림이다. 책에서는 전혀 다른 표를 활용한다.</li>
<li>서브루틴(재사용) 개념과 상태 전이의 압축(표를 다 들고 있지 않아도 되는 방식)  </li>
<li>SD(표준 기술)로 “프로그램을 숫자/기호로 표현”  </li>
<li>그 SD를 테이프에 담아 실행하는 U(범용 기계)</li>
</ul>
<p>이 흐름은, 오늘날 관점에서 “해석기/컴파일러/VM/런타임”으로 계속 환생한다.<br>다익스트라 장에서 언급한 P-코드와 런타임(가상 머신과 닮음)까지 연결하면, 2부의 거장들은 서로 떨어져 있지 않다. 같은 아이디어가 다른 언어로 재등장한다.</p>
<p>더욱이 &quot;존 폰 노이만이 작성&quot; 이 작성한 EDVAC(에드박) 설계 초안, [ 입력, 출력, 연산, 제어, 기억 장치 ] 구성 요소 다섯개, 오늘날까지 사용하는 내장식 컴퓨터 기초가 된다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4ff488a4-7db4-4aa7-9bd7-de8ca63a8c57/image.png" alt=""></p>
<p><em>PS. 여기서 전쟁에 대한 얘기, 탄도연구소를 넘어 맨해튼 프로젝트 (오펜하이머 영화 ㅎㅎ), 마크 I 와 ENIAC(에니악)을 경험. 트리니티 프로젝트에 대한 얘기가 나온다.</em></p>
<h4 id="4장-그레이스-호퍼-최초의-소프트웨어-엔지니어">4장. 그레이스 호퍼: 최초의 소프트웨어 엔지니어</h4>
<p><a href="https://maily.so/almostfamous/posts/l8mo5l9nr9p">개발자라면 누구나 겪어본 그것! &#39;버그&#39;를 발견한 최초의 프로그래머, 그레이스호퍼</a></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/97042b92-4043-416e-b5a2-08e3cb2f16f2/image.png" alt=""></p>
<p>주석, 서브루틴, 멀티프로세싱, 개발 방법론, 디버깅, 컴파일러, 오픈 소스, 사용자 계정 및 그룹, 주소, 이진수, 비트, 어셈블러, 브레이크포인트, 문자, 코드, 디버그, 편집... 이 모든 개념에 그레이스 호퍼의 노력이 묻어있다고 한다. </p>
<p>호퍼가 밀어붙인 것은 “추상적 언어 → 자동 변환(컴파일)”이었다. 여기서 핵심은 기술이 아니라 <strong>사회적 합의</strong>다. 당시에도 “천공판 구멍 뚫는 사람 다 어디감?” 같은 공포가 등장했고, 그 공포는 단순히 무지의 산물이 아니라 생계의 문제였다.</p>
<p>그런데도 호퍼의 방향은 분명하다.</p>
<ul>
<li>더 읽기 쉽고 쓰기 쉬운 표현  </li>
<li>더 많은 사람이 협업 가능한 언어  </li>
<li>기계어로 내려가는 과정을 자동화</li>
</ul>
<p>AI가 코드를 만든다는 말이 결국 “더 높은 추상화로 올라간다”는 말이라면, 호퍼의 자동 프로그래밍은 전례라고 볼 수 도 있다. 개인적인 견해로 그레이스 호퍼 얘기를 굳이 현 시대의 AI 확장에 대한 해석을 붙이자면, 나는 아래 2개가 중요하다 이해되었다.</p>
<ul>
<li>(일부 역할은 줄거나 바뀌지만) <strong>전체 산업의 총량은 오히려 커진다.</strong></li>
<li>문제는 기술이 아니라, <strong>전환을 관리하는 사회적/교육적 장치</strong>다.</li>
</ul>
<p>그녀는 해군에 입대했으며, 그 경험이 그녀를 ‘진정한 소프트웨어 엔지니어’의 길로 이끌었다. 처음에는 암호 해독 업무를 하게 될 것이라 예상했지만, 결과적으로 세계에서 두 번째 컴퓨터로 알려진 자동 순차 제어 계산기(ASCC, Mark I의 다른 이름)에서 부책임자이자 세 번째 프로그래머로 일하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4690b5a5-8e98-445a-bcb1-4b3425586928/image.png" alt=""></p>
<p><a href="https://www.thoughtco.com/howard-aiken-and-grace-hopper-4078389">Who Invented the Mark I Computer?</a></p>
<p>베티 스나이더의 사례도 흥미롭다. 정렬은 ‘매개변수’에 따라 동작했다. 여러 레코드를 정렬하려면 정렬 기준이 되는 필드의 위치, 크기, 정렬 방향 같은 정보가 필요하고, 이 매개변수를 입력받아 레코드를 정렬하는 방식으로 일종의 “프로그램 생성”이 가능했던 셈이다.</p>
<p>그리고 핵심은, 호퍼가 그 무렵 ACM 논문을 통해 “자동 프로그래밍”을 언급했다는 점이다. 그러자 곧바로 “천공판 구멍 뚫는 사람 다 어디감?” 같은 두려움이 따라붙는다. 다만 이 변화는 대체(replace)라기보다 완화(relieve)에 가까웠다는 맥락으로 읽힌다. A형 컴파일러가 등장하고, 프로그래머의 일자리를 빼앗을지 모른다는 공포가 생기면서도 A-1, A-2 개발이 가속화되고, 이어 A-3(일명 B형 컴파일러)로 이어진다. MATH-MATIC 언어도 그 흐름 안에 있다.</p>
<p>또 한 가지 인상적인 지점은 표현 방식의 선택이다. 호퍼는 <code>A × B = C</code> 같은 수학적 표기 대신, 비즈니스와 더 일반적인 대중을 위해서는 아래처럼 문장형 표현이 선호될 수 있다고 판단한 것으로 정리된다.</p>
<p><code>MULTIPLY BASE-PRICE AND DISCOUNT-PERCENT GIVING DISCOUNT-PRICE</code></p>
<p>이러한 맥락에서 영어 기반 언어인 B-0 개발로 이어지고, “추상적인 언어가 프로그래머들이 훨씬 자유롭게 협업할 수 있게 해준다”는 관점이 강조된다. 그리고 COmmon Business-Oriented Language, 즉 코볼(COBOL)의 탄생으로 연결된다. (놀랍게 아직도 살아 있다. 전 세계 금융 트랜잭션의 약 70~80%가 코볼로 작성된 시스템을 거치는 것으로 추정)</p>
<h4 id="5장-존-배커스-첫-번째-고수준-언어">5장. 존 배커스: 첫 번째 고수준 언어</h4>
<p>하이파이 오디오에 빠져 있던 시절, 훌륭한 스승을 만나 수학으로 방향을 틀었다. 이후 IBM 시설을 구경하던 중 SSEC 프로그래머로 취업했고, 이 일을 계기로 본격적으로 프로그래밍의 세계로 들어간다.</p>
<p>한국 전쟁 시기, IBM의 군수 컴퓨팅 맥락에서 IBM 701이 등장한다. 배커스가 SSEC 다음으로 마주한 대상이 바로 이것이었다. 완전한 전자식 컴퓨터였고, 진공관이 약 4천 개 들어갔다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/dc8e2182-32a0-4fd5-802f-f3f5a9c57384/image.png" alt=""></p>
<p>현대와 달리 IBM 701에서는 반복문이나 배열 순회를 구현하려면 명령어 자체를 바꾸는 기법이 필요했다. 입력 처리까지 포함해 전반적으로 번거로운 작업이 많았는데, 배커스는 이를 줄이기 위해 “스피드코딩(Speedcoding)”이라는 프로그램을 만든다. </p>
<p>본인 표현으로는 “게으름 때문”이었다고 한다. 부동소수점 연산을 지원하는, 일종의 인터프리터에 가까운 도구였고, <strong><em>인덱스 레지스터 증가 또는 감소 작업 수행</em></strong>, 간접 주소 지정 같은 기능도 제공했다. 한때 A-0 컴파일러를 폄하하기도 했고, 이후 FORmula TRANslation—포트란(Fortran)이라는 이름의 “예비 보고서”로 이어진다.</p>
<h4 id="6장-에츠허르-다익스트라-첫-번째-컴퓨터-과학자">6장. 에츠허르 다익스트라: 첫 번째 컴퓨터 과학자</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/762ba746-ba42-4cb1-9218-17295f5eeb25/image.png" alt=""></p>
<p>GOTO문을 쓰지 말자고 강조한 인물 중 하나다. 세마포어 개념을 처음 고안했고, 여러 유명 알고리즘을 만들었으며, ALGOL 60 컴파일러 최초 버전 공동 개발 등으로도 알려져 있다. 네덜란드 로테르담 출신. 초반에는 프로그래밍 자체보다 이론 물리학자로 성장하려 했지만, 빈가르덴의 영향이 인생의 전환점이 된다.</p>
<p>다익스트라는 “호출 가능한 서브루틴”의 장점을 분명히 본 사람이다. 제어 흐름이 동일한 명령어 집합을 한 번만 정의해 두고 필요할 때 실행할 수 있다면 얼마나 좋은가를 언급했고, 호퍼식의 “명령어 중복” 방식은 비효율적이며 메모리 낭비라고 봤다.</p>
<p>다익스트라 알고리즘은 최단 경로 알고리즘이다. (요즘은 모르겠지만, 나의 시대에는 꽤 단골 문제였다.) 한 지점에서 다른 특정 지점(혹은 모든 다른 지점)까지의 최단 경로를 구한다. “로테르담에서 흐로닝언까지 가장 짧은 길을 어떻게 찾지?” 같은 질문에서 출발해 설계한 알고리즘으로, 20분 만에 아이디어를 떠올렸고 논문 완성까지는 3년이 걸렸다고 한다.</p>
<p>너무 TMI지만, 나는 이 내용이 유독 기억에 남는다. 네덜란드에 교환학생 경험이 있었고, 사실 나는 네덜란드를 굉장히 IT 친화적으로 생각했다.(python..) 그리고 헤이그에 있으면서 로테르담, 흐로닝언까지 자전거를 타고 다닌 적이 있기 때문이다 ㅎ</p>
<p>하여튼, 이 알고리즘을 기반으로 ARMAC의 성능을 보여주려고 했었다. 그런데 이 코드를 포인터 없이, 재귀 없이, 심지어 서브루틴 호출 명령어조차 없이 작성해야 했다. 각 명령어의 내부 주소를 직접 수정해야 했고, 드럼 메모리와 코어 버퍼 사이에서 데이터가 밀리거나 덮어쓰이지 않도록 최적화도 직접 해야 했다. 아찔한 제약이다.</p>
<p>심지어 차기 컴퓨터 X1의 뒷판 구리 회로가 비싸서, 구리 배선을 최적화하기 위한 “MST(최소 신장 트리)” 알고리즘까지 활용했다.</p>
<p><strong><em>다익스트라와 존테벨트는 언어와 기계 사이에 추상화 경계(abstraction boundary)를 만들어,</em></strong> 당시 다른 팀보다 앞서나갈 수 있었다. 컴파일러가 ALGOL을 P코드(포터블 코드)로 내리고, 별도의 런타임이 이를 해석해 실행하는 구조였는데, 이 런타임은 오늘날의 가상 머신(VM)과도 닮아 있다.</p>
<p>이후 멀티프로그래밍 시스템 프로젝트를 시작한다. 당시로서는 매우 독창적이었다. 이를 위해 HW level에서 상위 계층이 하위 계층의 복잡한 내부 동작을 알거나 의존하지 못하도록 만들었다. 그리고 여기서 세마포어가 등장한다. 동시 실행되는 프로세스들이 공유 자원을 업데이트할 때 생기는 race condition을 다루는 방법이다. 이 과정에서 “임계 구역(critical section)”과 “불가분 연산(indivisible action)” 같은 용어도 도입된다.</p>
<p>나아가 다익스트라 팀은 해당 시스템의 정확성을 “수학적으로 증명했다”고 믿었다. 「프로그램의 신뢰성에 대하여」 같은 글에서 “프로그래밍은 점점 더 수학적인 활동이 될 것이다”라고 주장하기도 했는데, 저자 관점에서는 이것이 오판이라고 본다.
<img src="https://velog.velcdn.com/images/qlgks1/post/acc458de-6ca0-4a37-8bdd-71fe60814bde/image.png" alt=""></p>
<p>본질적으로 수학 OK, 두 분야의 접점도 당연히 OK다. 그런데 문제는 “증명”으로 접근했다는 점이다. 그리고 소프트웨어가 그 증명으로 이루어진 수학적 시스템이 되리라 생각했던 것 같다. 지금 보면, 누구도 위와 같은 수학적 증명을 실제 개발에서 하지는 않는다. 소프트웨어 세계의 ‘기하학 원론’은 없고, 앞으로도 없을 것이다. 저자는 소프트웨어가 수학이라기보다 과학에 가깝다고 보기 때문이다. 과학은 어떤 이론이 “옳다”를 증명하는 접근이 아니라, 그 이론이 틀렸다는 것을 관찰할 수 있는가에 가깝다. 소프트웨어를 만들면 “잘 설계된 테스트를 통해 프로그램이 잘못되었는지 관찰하고 오류를 찾아낸다”는 방식이다.</p>
<p>그리고 다익스트라의 “구조적 프로그래밍”이 이어진다. GOTO문에 반대하는 논거를 발표했고, 시장에 파장을 일으켰다. (네, Verilog 하시는 분들은 여전히 GOTO를 볼 수 있습니다.) 순차, 선택, 반복 세 가지만으로 프로그램을 구성하자고 제안한다. 왜 이런 구조인가? → 수학적 증명을 하지 않아도, 코드 자체가 “증명 가능”하도록 만들기 위해서다. 단순히 GOTO를 없앤 것 이상으로, 만들어낸 가치는 크다고 본다. 아키텍처 관점까지도.</p>
<h4 id="7장-니가드와-달-첫-번째-oopl--8장-존-케메니-모두를-위한-첫-번째-언어-basic">7장. 니가드와 달: 첫 번째 OOPL &amp;&amp; 8장. 존 케메니: 모두를 위한 첫 번째 언어, BASIC</h4>
<p>니가드와 달은 &quot;객체 지향의 발전&quot; 얘기다. <code>SIMULA 67</code> 와 같은 최초 객체 지향 프로그래밍(Object-Oriented Programming Language)이 등장했고 이는 <code>C++</code> 창시자에게도 영향을 줬다. </p>
<p>더욱이 클래스와 서브클래스 선언 논문 발표하며 &quot;처음으로 객체 용어&quot; 사용 했다. </p>
<p>존 케메니 얘기는 누구나 프로그래밍 영역 접근 할 수 있어야 한다고 믿었던 프로그래머들의 여정에 대한 얘기다. 하지만 자기 천재성에 스스로 눈과 귀 닫아버린 이들의 이야기이기도 하다.</p>
<p>그리고 폰 노이만의 <code>First Draft of a Report on the EDVAC</code> 비공식 문서 보고 엄청 큰 깨달음 얻었다고 한다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/57ce3822-7b0e-4728-b437-e59f02d31b2c/image.png" alt=""></p>
<p>&quot;타임셰어링 혁명&quot; 을 만들었지만 시장에서 고집으로 인해 역사속으로 사라졌다. </p>
<h4 id="9장-주디스-앨런--10장-톰프슨-리치-커니핸">9장. 주디스 앨런 &amp;&amp; 10장. 톰프슨, 리치, 커니핸</h4>
<p>위 2장은 직접 책에서 보길 바란다. 특히 10장의 C언어 얘기, 독자들이 꽤 시대적 흐름으로 더 많은 동감을 할 수 있지 않을까 생각한다. </p>
<h3 id="3부-급격한-전환점">3부. 급격한 전환점</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/572b512e-8002-4b4f-8724-d2d451955815/image.jpg" alt=""></p>
<p>1970년대부터 2020년까지 얼마나 폭발적으로 성장했는지!</p>
<h4 id="11장-1960년대">11장. 1960년대</h4>
<p>&quot;반(反)문화의 시기&quot; 라고 정의한다. 어릴 때 Digi-Comp I 기계를 가지고 놀며 플립플롭을 “장난감”처럼 다뤘는데, 그걸 제대로 쓰려면 결국 매뉴얼을 공부해야 했다. 그 경험을 바탕으로 릴레이 장치로 더 다양한 시도를 해보고, 트랜지스터·저항기·커패시터 같은 부품을 직접 배우고 만들고 조립해 본 기억이 이어진다.</p>
<p>고등학교에서는 ECP-18을 통해 전자식 컴퓨터 세계를 처음 제대로 경험했고, 이때 첫 프로그래밍 시도가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/bfc47767-aee4-4ac9-9ad5-5ec86ab2c7b6/image.png" alt=""></p>
<h4 id="12장-1970년대">12장. 1970년대</h4>
<p>16세에 프로그래머로 일을 시작했다. <code>EASYCODER</code> 책과 함께 허니웰 H200 시리즈용 어셈블리 언어를 다루며, 펀치 카드 더미를 실제로 겪는다. 이후에는 프로그래머 애널리스트 경험도 하고, 대형 미니컴퓨터 프로젝트에 참여했으며, 컴퓨터로 제어되는 레이저 절삭 시스템까지 경험한다.</p>
<p>특히 이 시기에는 여유 시간이 있어 팀과 “방식”에 대한 깊은 논의가 가능했다고 한다.
정렬 알고리즘, 탐색 알고리즘, 인덱싱 방식, 큐잉 방식 등</p>
<p>이직 후에는 System/7을 위한 어셈블리 경험을 하고, 그러던 중 “구조적 프로그래밍 (다익스트라)”의 <strong>*&quot;<code>GOTO</code> 가 해롭다는&quot;*</strong> 글을 보고 충격을 받는다. System/7에 곧바로 적용하면서 받아들였다고 한다 ㅎ.</p>
<p>&quot;서브루틴은 좋은 것이지만, 모듈 사이를 마구 뛰어다니는 점프는 나쁘다&quot;
이 구분에 완전히 매료된다. 그리고 이에 대한 글을 쓰고, 인생 첫 출장 비행을 하고, 프로그래머 그룹을 처음으로 교육하는 경험도 한다.</p>
<p>그러다 해고를 당하고 TAS로 돌아간다. 일종의 스타트업 같은 곳이었다. 개인을 갈아 넣으며 돌아가는 시스템. 많은 프로젝트, 많은 경험, 압도적으로 많은 코드 경험이 쌓인다. 후반부로 갈수록 컴퓨팅 능력 자체가 커지면서 시장도 더 빠르고 크게 성장한다.</p>
<h4 id="13장-1980년대">13장. 1980년대</h4>
<p>벨 텔레콤과 미팅 기회가 생겨서 “MLT 엔지니어는 어떤 언어를 쓰냐?”라는 질문에 돌아온 답은 &quot;당연히 C였다.&quot; 거기에 꽂혔다. 바로 책을 사서 읽었고, 빠져들어 “C 컴파일러를 반드시 구해야 한다”는 생각까지 하게 된다.</p>
<p>회사에서는 시스템 관리자가 된다. 80년대 전화 회사들의 디지털 혁명 배경에는 구리 가격 상승이 있었다. 엄청난 양의 구리선을 회수하고 싶었고, 오래된 릴레이식 스위치를 디지털 스위치로 교체하는 대대적인 아키텍처 변화가 진행된다. 이 과정에서 C와 BOSS를 적극적으로 사용한다.</p>
<p>수년간 몰두하게 만드는 거대한 프로젝트도 이어진다. (E.R. 특허/프로젝트 등)</p>
<p>그리고 애플 II가 등장한다. 회사 사무실에 개인용 컴퓨터가 들어오고, 스프레드시트가 실제 업무 도구가 된다. 이어지는 매킨토시까지. 흥미로운 점은, 이 시기에 폭포수 프로세스가 아니라 특별한 제약 없이 칸반을 이용해 개발했다는 대목이다.</p>
<p>E.R 프로젝트는 H/W, N/W까지 포함해 밑바닥부터 만들었지만 시대 흐름과 맞지 않았던 것 같다고 한다. 프로젝트와 특허는 모두 무산. 개인용 컴퓨터 보급이 본격화된다. 이후 GUI와 개인용 서비스 몇 가지(wator 등) 개발, 그리고 객체 지향에도 도전한다.</p>
<h4 id="14장-1990년대">14장. 1990년대</h4>
<p>시장의 낙관적인 전망과 빠른 성장이 이어지며 “성장”이 눈에 보이던 시대. 동시에 세계 전쟁, 테러 사건 같은 균열도 함께 있었다. 닷컴 버블로 절정까지 치닫는다.</p>
<p>스타트업 경험, 인터넷 경험, 온라인 세상의 체감이 깊어진다. 선 마이크로시스템스에서 C++ 컴파일러가 등장하고, 유즈넷/뉴스그룹에 가입해 폭발적인 성장의 공기를 직접 겪는다. 저자가 감명 깊게 본 그래디 부치의 <code>&lt;Object Oriented Design with Applications&gt;</code>도 나온다. (불확실한 정보지만 아직 대학교 강의 교재처럼 쓰는 곳이 있는 것 같기도 하다.)</p>
<p>회사는 사업적으로 어려웠지만 경력은 오히려 상승 곡선을 탄다. 뉴스그룹 활동이 주목을 받으며 글 투고를 시작한다. 그러다 헤드헌터에게 전화가 온다(래셔널 와라!). 인하우스로 가는 대신 역으로 컨설턴트를 제안했고, 그 제안이 받아들여진다. 이후 이것이 싹이 되어 프로그래밍 컨설팅 역량을 펼친다. 그중 비네트 프로젝트를 통해 “재사용 가능한 프레임워크”의 성공과 실패를 모두 경험한다.</p>
<p>또한 GOF(Gang of Four)의 <code>&lt;Design Patterns&gt;</code> 책 온라인 리뷰어 경험도 있다. (이 책은 언제 대체재가 나오려나ㅎㅎ.) </p>
<p>현 시대의 거장들 얘기가 갑자기 확확 와닿았다. 그들도 르네상스처럼 일종의 그들만의 &quot;장&quot; 이 있었고 그 &quot;장&quot;을 만들고 이어온게, 이게 정말 중요한게 아닐까 싶기도 하다.</p>
<p>또 저자는 켄트 벡의 XP에 큰 감명을 받아서 직접 만나 이야기한다. 이를 교육과 컨설팅으로 확장한다. TDD를 처음 경험하고 보고 느끼며 충격을 먹는다. 완전히 새로운 코딩 방법이었다고 한다. </p>
<p><em>PS. TDD는 나에게도 너무 혁명이었다. 물론 저자와 상황과 시대도 달랐었지만, 진짜로 TDD를 프로젝트에 적용했을때 그 새로움, 특히 신규 feature를 추가할때 그 쾌락을 잊을 수 가 없다.</em> </p>
<h4 id="15장-밀레니엄">15장. 밀레니엄</h4>
<p>불확실성과 침체가 함께 온 시기. 9.11, 전쟁, 2008년 글로벌 금융 위기 등.</p>
<p>그럼에도 XP 교육과 컨설팅은 한동안 호황이었다고 한다. 소프트웨어 전문가 17명이 모여 애자일 선언문(아직 살아있다!! - <a href="https://agilemanifesto.org/iso/ko/manifesto.html)%EC%9D%84">https://agilemanifesto.org/iso/ko/manifesto.html)을</a> 작성했고, 이것이 큰 파도를 일으켰다고 한다. 그러는 와중에 9.11, 그리고 2001년부터 닷컴 버블 붕괴가 시작된다.</p>
<p>이후 2년간 수요 감소와 침체가 이어지고, 오히려 책을 집필할 수 있었다고 한다. 이때 나온 것이 <code>&lt;Agile Software Development, Principles, Patterns, and Practices&gt;</code> 출간. 그리고 이후 <code>&lt;Clean Code&gt;</code> 집필로 이어진다.</p>
<p><em>PS. 사견으로 이 두 권이야말로 아직까지 전 세계 개발자에게 로버트 C. 마틴 “엉클밥”을 알리게 만든 결정적 계기가 아니었나 싶다.</em></p>
<p>그리고 이후에는 SICP를 기반으로 함수형 프로그래밍의 매력에 빠졌다고 한다. 생각보다 현 시대, 동시대의 거장들이 함수형 프로그래밍을 “매우 맛있게” 표현하는 경우가 있는데, 내가 느끼기엔 이게 약간 예술의 경지가 아닐까 한다.</p>
<h3 id="4부-미래">4부. 미래</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/331eb4e3-333e-40a5-a0b1-ba0684832ecb/image.jpg" alt=""></p>
<h4 id="16장-프로그래밍-언어">16장. 프로그래밍 언어</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f1b972c6-2c3f-466d-8457-12c9ee36c282/image.png" alt=""></p>
<p>요즘 프로그래밍 언어 진짜 개많다. 왜 그렇게 많을까? 정말 그렇게 다양할 필요가 있을까? Java, C#, 거시적으로 객관적으로 보면 사실상 거의 동일해 보이기도 한다. 사실 우리가 “모든 것을 지배할 단 하나의 언어”, “성배 같은 언어”를 끊임없이 찾아서 그런 거 아닐까?  </p>
<p>저자는 과거에 비해 새언어가 주는 depth 있는 메커니즘 차이가 거의 없는 것처럼 느껴진다고 한다. 그래서 이 상황을 성경 전도서의 <strong><em>“이미 있던 것이 다시 있을 뿐. 태양 아래 새로운 것이 없도다”에 비유하게 된다.</em></strong></p>
<p>사실 컴퓨터 분야에서만 그런 게 아니라 다른 산업 분야에서도 비슷한 일이 반복되어 왔다. 단 하나의 언어만 남기자고 사회적 협의가 될 수도 있지 않을까? 이러면 모두가 편해질 텐데…  </p>
<p><em>PS. ‘새로움’이 사라졌다기보다 <strong>새로움의 무대가 런타임/생태계/운영 비용으로 옮겨 간 것</strong> 같기도 하다.</em> 물론 이 새로움이 저자가 원하는 니즈를 충족시키진 못할 것 같다.</p>
<p><em>PS. 근데 이제 실제로 LLM이라는 것을 “언어” 관점으로 본다면, 하나로 합일화 되고 있는 것 같기도 하다. 다만 전혀 다른 방식이긴 하지만… 이 경우의 합일화는 “프로그래밍 언어가 하나로 통일된다”기보다, <strong>입력 방식(자연어 프롬프트)</strong> 이 통일되는 쪽에 가까워 보인다.</em></p>
<p>타입에 대한 것도, 컴파일에 타입을 검사할까, 런타임에 검사할까? 수십 년째 줄다리기. 정적 언어는 퍼즐처럼 제자리에 다 끼워야 해서 귀찮고 어려운 언어, 동적 언어는 이 단점을 상쇄하지만 그만큼 잘못 끼울 위험이 있는 것이다. </p>
<p>저자는 두 가지를 절충해서 <strong>&quot;타입 검사를 형식적으로 엄격하게 하되, 런타임으로 미루자고 제안함. 그리고 언제 적용할지 프로그래머가 선택할 수 있게&quot;</strong> 하자고 제안한다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f5e41739-281c-4a01-a2ee-9984dc853e48/image.png" alt=""></p>
<p>저자는 언어를 합일화 한다고 하면 리스프 계열 언어가 남는다고 생각한다. (매우 의외)</p>
<p>1) 대부분 극도로 단순함. 요즘 유행하는 언어들이 좀 과하게 문법이 방대하다고 생각함. 그래서 트릭과 기교까지 배워야 한다는 점.
2) 단순하기 때문에 생기는 표현력. 언어 제약에서 벗어나 내가 표현하고 싶은 것을 표현할 수 있게 됨.
3) 리스프는 프로그래밍 언어보다는 데이터를 표현하는 언어에 가까움. 여기에 데이터가 프로그램처럼 해석될 수 있도록 하는 런타임이 결합되어 있는 것.</p>
<p>이 리스프 얘기의 핵심은 결국 “단순함” 자체보다, <strong>코드=데이터(code-as-data)</strong> 감각에 있는 것 같다. <code>Clojure</code> 가 리스프를 설명할 때도, 코드-데이터와 매크로 시스템이 리스프 계열을 구분 짓는 특징이라고 정면으로 말한다.</p>
<p>그리고 “폰 노이만 아키텍처 같다”는 비유는, 저장 프로그램 개념(명령과 데이터가 같은 저장소에 있고, 프로그램이 데이터처럼 취급될 수 있음)을 떠올리게 한다는 점에서 의미가 있다. 실제로 Britannica도 폰 노이만 머신의 원리 중 하나로 “데이터와 명령을 한 저장소에 두고, 프로그램을 데이터로 취급할 수 있다”는 점을 강조해왔다.</p>
<h4 id="17장-ai">17장. AI</h4>
<p>미래학자들이 우리 모두가 AI 혁명 벼랑 끝에 서 있다고 말한다. 하지만 저자는 이 기술들이 초자연적인 무언가이거나, 인간의 지능이나 창의력에 조금이라도 근접하게 될 것이라고는 보지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/25467c41-c75b-41ec-8a6d-52cc545652a6/image.png" alt=""></p>
<p>그 근거로 저자는 인간의 뇌 구조와 뉴런을 꺼내 든다. 뉴런이 얼마나 강력한 계산 단위이며, 뉴런 간 연결(시냅스)의 규모가 얼마나 압도적인지 이야기하면서, “스카이넷” 같은 도약은 쉽게 오지 않을 것이라고 선을 긋는다. </p>
<p>실제로 인간 뇌의 뉴런 수와 시냅스 연결 수에 대한 대표적 추정치만 보아도, 규모 면에서 아직은 우리가 ‘비슷한 것’을 만든다고 말하기 어려운 구간이 있다는 느낌을 준다(뉴런 약 860억, 시냅스 약 100조 수준의 추정 등).</p>
<p>이어서 “신경망(Neural Nets)”으로 넘어간다. 아마 LLM을 포함한 현대 AI의 핵심을 설명하기 위한 길목이다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/75cafeb7-47a0-48f7-908d-966e88450d5a/image.png" alt=""></p>
<p>여기서 포인트는 분명하다. <strong>여전히 블랙박스</strong>이며, <strong>가중치는 미리 예측할 수 없고</strong>, 결과값이 “어떤 공식”으로 딱 떨어지게 정해지는 것도 아니라는 것. 즉, 입력을 넣고 출력에서 원하는 것이 나오기를 “기도하듯” 바라는 상태가 생길 수 있다는 말이다. 이 문제의식은 오늘날 XAI(Explainable AI) 논의가 왜 반복되는지와도 맞닿아 있다. 딥러닝 모델은 성능이 뛰어나더라도 내부 작동을 설명하기 어렵다는 이유로 “블랙박스”로 불려 왔고, 그래서 ‘설명 가능성’이 별도의 연구 축이 되어 왔다.</p>
<p>저자는 “학습을 다시 시켜서 더 나아지기를 바라는 것”을 운에 맡기는 전략과 비슷하다고 본다. 그리고 신경망을 만드는 일을 소프트웨어 엔지니어링보다는 “현수교를 만드는 것에 가깝다”고 평가한다. </p>
<p>그런데 저자는 여기서 한 번 더 꺾는다. <strong>신경망은 SW 없이 존재할 수 없고</strong>, 이를 둘러싼 도구와 인프라는 소프트웨어가 깊이 관여할 수밖에 없다. 다만, 그 소프트웨어는 운에 맡기는 게 아니다. 프로그래머가 다루는 본질은 여전히 <strong>결정론적</strong> 결과물이며, 애매한 희망이 아니라 확실한 이진적 사실을 다룬다는 것. </p>
<p><strong><em>이 주장이 장 전체를 관통하는 “태도”처럼 남는다. AI가 불확실하다는 말이 곧 프로그래밍 자체가 불확실해진다는 뜻은 아니다. 오히려 불확실한 블록을 끼워 넣을수록, 주변의 결정론적 장치(검증·테스트·제약·관측)가 더 중요해진다는 뉘앙스로 읽혔다.</em></strong></p>
<p>이 장에서 저자가 특히 강조하는 또 하나는, LLM이 현시대 “신경망의 걸작”처럼 보이더라도 지능적·창의적으로 보이는 순간들이 곧장 ‘창의성’의 증거는 아니라는 점이다. </p>
<p>그리고 아주 단적으로 <strong>GIGO(garbage in, garbage out)</strong> 를 떠올리게 하는 예시(학습 데이터가 범죄적이면 출력도 범죄적으로 기울 수 있음)를 통해, 결국 모델이 무엇을 반사(reflect)하는지 다시 보게 한다.</p>
<p><em>PS. 출력 품질은 곧 입력 품질(데이터·프롬프트·컨텍스트·정책)의 그림자다. 그러니 책임도 결국 ‘모델 바깥’에 남는다.</em></p>
<p>그리고 저자는 LLM을 활용해 얻어낸 코드를 직접 리뷰하면서 그 출력물을 신랄하게 비판한다. 여기서는 웃음이 나왔다ㅋㅎㅋㅎㅋㅎ. “코드는 나왔지만, 설계가 없다”는 느낌. 즉, 그럴듯한 구현은 쉽게 생성되지만, 요구사항의 모서리·예외·장기 유지보수·실패 모드·테스트 전략 같은 디테일은 여전히 사람이 붙들어야 한다는 주장으로 이어진다. <strong><em>(이 부분이 가장 의아했는데, 위에서 언급했듯, 최근의 의견은 조금 달라진 듯 하다.)</em></strong></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4c6534aa-5a92-4d0b-b86e-043c0c466822/image.png" alt=""></p>
<p>LXM(X 자리에 Music, Art, Code 등). <strong>LXM은 결국 도구</strong>이며, C도 도구, IDE도 도구, 결국 사람이 이용해야 한다는 것. 이때 책은 “도구가 사람을 대체한다”는 공포가 역사적으로 반복되어 왔음을 상기시킨다. </p>
<p>초기 프로그래머들—바이너리 코드로 직접 프로그램을 짜던 사람들—이 그레이스 호퍼의 A-0 같은 컴파일러를 두려워했지만, 결과는 정반대였다는 이야기. 실제로 호퍼의 A-0 시스템은 1952년 무렵의 초기 컴파일러(혹은 컴파일러에 준하는 시스템)로 종종 언급되며, 이후 고수준 언어와 도구 생태계의 확장을 여는 흐름에 놓인다. 요지는 “대체”가 아니라 “확장”이다.</p>
<p>왜 지난 세월간 전 세계 프로그래머 수가 폭발적으로 증가했는가? 저자의 답은 간결하다. 컴퓨터를 활용할 수 있는 분야가 아직도 무궁무진하기 때문이다. 그리고 “그렇다면 인간의 어떤 점 때문에 LXM이 프로그래머들을 대체할 수 없을까?”라는 질문으로 다시 돌아온다. 저자는 결국 1부 1장 “우리는 누구인가?”에서의 답, 즉 <strong>디테일</strong>을 다시 꺼낸다. 불확실한 결과를 ‘쓸 수 있는 결과’로 바꾸는 일, 맥락을 정의하고 책임을 지는 일, 실패를 설계하고 복구를 설계하는 일—이 디테일이 있는 한, 도구는 사람의 자리를 빼앗기보다 사람의 능력을 증폭시키는 방향으로 진화한다는 것이다.</p>
<p>장 끝에서 남는 감정은 묘하게 담백하다. AI는 불가해하고, 그래서 더 조심스럽게 다뤄야 한다. 그러나 그 불가해함이 곧바로 “프로그래머의 종말”을 뜻하지는 않는다. 오히려 프로그래머의 일은 더 명확해진다. <strong>불확실한 것을 시스템 안에 안전하게 배치하는 사람</strong>, 그 디테일을 책임지는 사람. 저자가 말하는 미래는 그 점에서 과장되지 않는다. 다만 그래서 더 현실적이다.</p>
<h4 id="18장-하드웨어--19장-월드-와이드-웹--20장-프로그래밍">18장. 하드웨어 &amp;&amp; 19장. 월드 와이드 웹 &amp;&amp; 20장. 프로그래밍</h4>
<p>저자는 월드 와이드 웹의 미래를 말하면서 “브라우저 같은 것이 사라진다”가 아니라, <strong>브라우저가 ‘인식에서 사라질 것’</strong> 이라고 본다. 이게 가장 인상 깊었다. 마치 지금 우리가 전화번호를 외우지 않고, 통신 인프라를 의식하지 않듯이, 웹도 특정한 형태(브라우저, 주소창, 탭)로 ‘보이는 것’이 아니라 서로 소통하는 프로그램들만 남고, 사용자는 그것을 웹이라고조차 부르지 않게 될 거라는 전망이다.</p>
<p><strong><em>그렇다면 앞으로의 프로그래밍은 어떻게 바뀔까?</em></strong></p>
<p>저자는 지난 50년을 되돌아보며 50년 후를 상상했을 때, 자신은 여전히 if, while, 대입문 같은 기본 구문을 쓰고, 컴파일하고, 테스트하고 있을 것 같다고 말한다. 이건 보수적인 선언이라기보다 “핵심은 생각보다 잘 안 바뀐다”는 쪽에 가깝다.</p>
<p>더 흥미로운 건, “근래의 세월에 소프트웨어 역사상 소프트웨어 원칙에 대한 눈에 띄는 발전은 거의 없었다”는 진단이다.</p>
<p>구조적/함수형/객체지향이라는 세 주요 패러다임은 이미 70년대에 등장했고, SOLID 같은 것이 이후에 널리 퍼졌지만, 그 밑바탕 자체는 이미 오래전에 확립되어 있었다는 말이다. 그래서 앞으로 50년 동안 무언가가 극적으로 뒤집힐 거라는 기대를 크게 하지 않는다. 이름이 바뀌고 포장이 달라질 수는 있어도, 본질은 비슷하게 남을 거라는 태도다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/084513d7-42ac-4cd3-84c0-e69372b4916d/image.png" alt=""></p>
<p>대신 저자가 “진짜로” 기대하는 건 기술의 급진적 변화가 아니라, 프로그래밍 직군에 전문 윤리와 기준, 기법이 자리 잡는 것이다. 도구나 방법론이 늘어나는 것 못지않게, “직업 윤리 의식”이 필요하다는 주장인데, 나는 이 부분이 이 책 전체의 결론처럼 들렸다. 결국 프로그래머의 미래는 더 강력한 도구가 아니라, 더 강한 책임감과 기준에서 갈린다는 식으로.</p>
<p>개인적으로는 이후 집필 후기와 톰 길브의 소감이 정말 재미있었다. 조금 자극적인 농담도 섞여 있는데, 예컨대 “노인은 누군가 들어주는 사람이 있으면 자신의 이야기를 무척 좋아한다” 같은 문장이나, “이 책에 나오는 사람들은 이제 ‘원로’들이다”라는 식의 표현이 그렇다.</p>
<p>그 ‘원로’들의 지혜가 닿길 바라는 마음에서 이 책을 만진다는 말이, 어떤 면에서는 이 책의 가장 솔직한 동기처럼 느껴지기도 했다. 다익스트라가 자기 자신을 거만한 컨설턴트라고 적었다는 이야기나, 그레이스 호퍼 강연에서 감명 깊었던 포인트를 풀어놓는 대목도 그 연장선에서 읽히면서, 본문과는 다른 결로 웃게 만든다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[velog dashboard v2 - MAU 1k+, 아직도 통계를 안보셨나요?!]]></title>
            <link>https://velog.io/@qlgks1/velog-dashboard-v2-MAU-1k</link>
            <guid>https://velog.io/@qlgks1/velog-dashboard-v2-MAU-1k</guid>
            <pubDate>Tue, 20 Jan 2026 16:15:48 GMT</pubDate>
            <description><![CDATA[<h1 id="velog-dashboard-v2">Velog Dashboard v2</h1>
<blockquote>
<p>오랜만에 찾아왔습니다!!
<strong>빨리 접속하기:</strong> <a href="https://velog-dashboard.kro.kr/">https://velog-dashboard.kro.kr/</a><br><strong>Github repo:</strong> <a href="https://github.com/check-Data-Out/velog-dashboard-v2">https://github.com/check-Data-Out/velog-dashboard-v2</a></p>
</blockquote>
<p>이 서비스가 velog에 귀속된(?) 구조다 보니 알릴 수 있는 채널이 사실상 velog 밖에 없습니다. 하지만! 뉴스레터 이후로 유입도 계속 올라가고 <strong>*<span style="color: rgb(99 148 255);">어느 순간부터 MAU 1K+ </span>*</strong> 를 찍고 있더라구요...!</p>
<p>그리고 애초에 “우리나 편하게 쓰자”로 시작한 프로젝트라 막 공격적으로 알리자! 이런 마음은 애초에 크지 않았고요. <strong>어떻게 알고 찾아오시나요?..!?</strong>  </p>
<p>간간이 LinkedIn 이나 velog 글에서 통계 지표를 캡쳐해서 활용하시는 분들도 보이고, 그럴 때마다 저희 모두 조용히 뿌듯해하고 있었습니다ㅎㅎㅎ</p>
<p>사실 이 프로젝트는 같이 시작했던 취준생 분들을 위한 (나름의) 취업 장려 프로젝트이기도 했는데, 멤버 절반 이상이 취업을 했습니다! <del>(하거나, 다시 돌아온..!! 상태)</del> 프로젝트의 비공개 KPI는… 달성했습니다. 😎 </p>
<p>혹시 이 글을 읽고 계시는 취준생 분이 계시다면! 개인적으로 나 혼자 쓰는 product 이라도 0 to 1 으로 세상에 공개하는 경험과 관리해보는 경험이 정말 큰 도움이 될 것이라고 생각합니다!</p>
<p>아무튼, 살아있다는 근황 겸 소소한 업데이트 공유해봅니다. 🙏🔥</p>
<p>① 직접 통계 새로고침!
② 완전 싱크 맞는 &quot;마크다운 통계 뱃지&quot; (사실 TBD이긴 한..)
③ 토큰 공격 방어!! <code>Fail-fast JWT + Redis</code> 기반 인증 실패 추적/차단 (rate limit)</p>
<hr>
<h2 id="1-직접-통계-새로고침-딸각">1. 직접 통계 새로고침 (딸각)</h2>
<p>그동안 통계를 “유저가 직접 업데이트” 하지 못하게 막아뒀습니다. 이유는 단 하나... 서버 비용이 0원이라 리소스가 절망적이기 때문 입니다 ㅎㅎ</p>
<p>그래서 통계를 어그리게이션 하는 가장 무거운 배치는 철저하게 GitHub Actions 로만 돌립니다. (무려 20개 배치를 30분마다 돌림 ㅋㅎㅋㅎㅋㅎ)</p>
<p>근데 문제가 있죠.</p>
<ul>
<li>방금 가입했는데, 배치가 돌 때까지 기다려야 하는 케이스</li>
<li>한 달만에 들어오면 토큰 만료로 인해 레거시/과거 데이터를 보게 되는 케이스
(저도 그렇고.. 배치 도는 순간까지 절대 못 참습니다.)</li>
</ul>
<p>그래서 이번에 “어느 정도 cap” 을 두고, 유저가 직접 통계를 새로고침 할 수 있는 창구를 만들었습니다.</p>
<h3 id="핵심-흐름">핵심 흐름</h3>
<ul>
<li>직접 새로고침 버튼 → API 호출 → <code>Redis</code> 큐 적재 → <code>Consumer</code> 구독/처리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a05e8b6d-bc8a-4265-bf8e-5878ac9ba03f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4eb185bc-3de8-4aac-8006-98a900a8189d/image.png" alt=""></p>
<p><code>Consumer</code> 는 가장 리소스가 널널한 백오피스 서버에서 가동하기로 했습니다! </p>
<h3 id="무한-새로고침-방지-중복-호출-방지">무한 새로고침 방지 (중복 호출 방지)</h3>
<p>15분 리미트가 걸려있습니다. 기준은 버튼 누른 시간이 아니라, “나의 가장 마지막 업데이트 시간”으로부터 15분 입니다 ㅎㅎ. <strong><em>고로 신규로 오신분은 바로 클릭이 가능하실거에요!</em></strong></p>
<h3 id="consumer-가-배보다-배꼽이-커진-이유">Consumer 가 배보다 배꼽이 커진 이유...!</h3>
<p>첫 설계시 &quot;비동기 task를 범용적으로 처리하는 consumer&quot; 는 아니었는데, 만들다 보니 범용성을 목표로 하게 되었습니다.. (일 부분 만큼은 오히려 창업한 product 보다 완성도가 높은 기분) 그래서 핵심 아래 3가지 경우를 중요하게 다뤘습니다.</p>
<p>1) 셧다운 때 / 에러 발생 시
2) 메시지 수신 전용 큐 / 처리 전용 큐 / 실패 전용 큐 분리 + 비동기 작업 처리<br>3) 실패 시 <code>retry</code> + 완전 실패 시 일정 사이즈만큼 보관 (디버깅/추적)</p>
<p>플로우 차트는 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/956d9487-f08a-499e-811d-06a1a3359fbc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/6ad7c389-39f3-42f1-a179-2acf0980802e/image.png" alt=""></p>
<p>코드가 궁금하다면 여기서 바로 보실 수 있습니다!! 
<a href="https://github.com/Check-Data-Out/velog-dashboard-v2-back-office/tree/main/consumer">https://github.com/Check-Data-Out/velog-dashboard-v2-back-office/tree/main/consumer</a></p>
<hr>
<h2 id="2-완벽한-마크다운-통계-뱃지-tbd">2. 완벽한 &quot;마크다운 통계 뱃지&quot; TBD</h2>
<blockquote>
<p><strong>*<span style="color: rgb(99 148 255);">최적화 작업으로 인해, <br /> 사실, 아쉽게  26.01.23 이후에야 제대로된 기능으로 사용하실 수 있습니다.. 🙏🙇🏻‍♂️🙏🙇🏻‍♂️🙏🙇🏻‍♂️</span>*</strong></p>
</blockquote>
<p>통계 뱃지를 만들어 주신 분들이 이미 많이 있습니다. 근데 이미 기존 뱃지들은 어쩔 수 없이 제대로 된 통계 데이터를 한 번에 불러올 수 없는 구조 입니다.ㅠㅠ</p>
<ul>
<li>직접 통계를 저장하지 않는 구조, 실시간으로 모든 게시글의 누적 통계를 가져올 수 없음!</li>
<li>과거 이력들은 결국 “코딩된 값”으로 들어갈 수 밖에 없고, “지금 이 순간 내 통계”를 완전하게 동기화 하기가 어려움!</li>
</ul>
<p>근데 Velog Dashboard 에 등록한 분들은, 완벽한 마크다운 뱃지를 “직접 생성해서” 쓸 수 있습니다. 우측 상단 프로필에 &quot;뱃지 생성기&quot; 가 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4a314178-87ad-4387-87dc-7254d6ea8448/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a4f4d2a7-4e3f-49d9-970d-61d27f12710f/image.png" alt=""></p>
<h3 id="어떤-형태로-제공하냐면">어떤 형태로 제공하냐면</h3>
<ul>
<li>이미지 URL 을 바로 주고, 마크다운에서 쓸 수 있는 HTML 코드를 같이 줍니다. 그리고 그 이미지 URL 은 아래와 같은 png 를 바로 줍니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/b044edb1-09c4-4fc3-b809-5060f36fa6d7/image.png" alt=""></p>
<p>그래서 GitHub <code>README.</code>md / velog 소개 페이지 / 기타 <strong><em>마크다운/html 지원하는 곳 어디든 다 박을 수 있어요.</em></strong> 짜잔.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/534e98dd-6580-4aa0-b803-83d0f152f872/image.png" alt=""></p>
<h3 id="뱃지-처리-user-flow">뱃지 처리 user flow</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/8841646f-0d94-4eaa-879f-5f2473eabf2a/image.png" alt=""></p>
<p>1) <code>/badge?username=xxx</code> 로 요청이 들어오면 파라미터 검증을 하고
2) Next(server-side)에서 백엔드 API 호출로 통계/최근글 조회 (클라에 노출 안 됨)
3) React 컴포넌트 → <code>Satori</code> → PNG 로 이미지 생성합니다. 
4) img 태그 하나로 끝! 입니다.
5) 최적화 작업으로 이제 캐싱 layer 가 추가되어서 릴리즈 될거에요!</p>
<hr>
<h2 id="3-토큰-공격-방어-fail-fast-jwt--redis-기반-인증-실패-추적차단">3. 토큰 공격 방어 (Fail-fast JWT + Redis 기반 인증 실패 추적/차단)</h2>
<p>이 업데이트는 기능 추가라기보다, 보안을 위해 결국 해야만 했던 방어 작업이다..! 어느 날부터 “가끔” 서버가 트래픽이 피크를 찍는 구간이 있었는데, 역시 해외에서의 무작위 요청! 근데 이게 어떻게 특정 포인트를 알아버린건지 DB connection 을 써야만 하는 API 을 타겟을 제대로 했더라구요..</p>
<h3 id="문제-쓰레기-토큰-대량-유입-→-연결-풀-고갈">문제: 쓰레기 토큰 대량 유입 → 연결 풀 고갈</h3>
<p><strong><em>쓰레기 토큰이 대량으로 들어옴 → 인증 미들웨어가 매번 <code>payload</code> 파싱 시도 → 예외가 계속 남 → 방어 없이 계속 들어옴 → 연결 풀이 먼저 말라버림 → &quot;<code>Max client connections reached</code>&quot;</em></strong></p>
<p>즉, 어차피 의미 없는 토큰인데도 미들웨어가 열심히 달려버리면서 (그리고 중간중간 DB/연결 리소스를 건드리면서) 커넥션 풀이 빠르게 고갈되는 구조였습니다. 사실 이게 구조적인 문제가 좀 있었죠. (애초에 지금 서비스가 직접 회원을 관리하는 구조가 아니기에...)</p>
<p>그래서 전략을 바꿨습니다.</p>
<ol>
<li><strong><em><code>Fail-fast</code></em></strong>: JWT “형식부터” 먼저 보고, 아니면 바로 컷 (DB까지 갈 자격 없음)</li>
<li><strong><em><code>Rate limit</code></em></strong>: 계속 던지는 IP는 <code>Redis</code> 에 기록해서, 임계치 넘으면 잠깐 차단.</li>
<li><strong><em><code>Fail-open</code></em></strong>: <code>Redis</code> 가 죽어도 서비스는 죽지 않게 (차단 기능만 비활성)</li>
</ol>
<h3 id="물리적인-시큐어코딩-업데이트">물리적인 시큐어코딩 업데이트</h3>
<h4 id="1-jwt-형식-검증-유틸-추가">1. JWT 형식 검증 유틸 추가</h4>
<ul>
<li>점(.) 2개인지, 빈 파트 없는지, Base64URL 문자셋인지 같은 기본 형식부터 체크  </li>
<li>형식이 아니면 payload 디코딩/파싱 자체를 시도하지 않음</li>
</ul>
<h4 id="2-payload-추출을-안전하게-safeextractpayload">2. Payload 추출을 안전하게 (safeExtractPayload)</h4>
<ul>
<li>깨진 Base64 / 깨진 JSON 이면 null 처리 → InvalidTokenError  </li>
<li>예외가 “서비스를 죽이지 않게” 컨트롤</li>
<li>Redis 기반 인증 실패 추적/차단  <ul>
<li>IP별 인증 실패 횟수 누적 (윈도우/TTL 적용)</li>
<li>임계치 넘으면 429 + retryAfter</li>
<li>캐시 에러 시에는 fail-open (서비스 우선)</li>
</ul>
</li>
</ul>
<h4 id="3-error-handling-개선">3. Error handling 개선</h4>
<ul>
<li><code>InvalidTokenError</code> 발생 시 실패 추적(trackAuthFailure) 연결</li>
<li>500 에러만 Sentry 로 캡쳐 (4xx 커스텀 에러는 제외)</li>
</ul>
<h3 id="토큰-밸리데이션--정상-처리-플로우">토큰 밸리데이션 &amp; 정상 처리 플로우</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/b10eaf38-5604-430e-9d5f-71d9699f843a/image.png" alt=""></p>
<h3 id="fail-fast--redis-block-플로우">Fail-fast + Redis block 플로우</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9cc13309-f2d1-499c-89cb-9c1fbe9f919d/image.png" alt=""></p>
<hr>
<h2 id="vd2-는-누구나-pr-올릴-수-있게-열려있습니다">VD2 는 누구나 PR 올릴 수 있게 열려있습니다!</h2>
<p>템플릿도 있고, 이슈도 열려있고, 진짜로 “어? 이거 고치고 싶은데?” 하면 바로 들어오실 수 있게 해놨습니다!! <a href="https://github.com/Check-Data-Out/velog-dashboard-v2">https://github.com/Check-Data-Out/velog-dashboard-v2</a> 여기서 관련 레포를 한 눈에 볼 수 있어요.</p>
<ul>
<li>FE - <a href="https://github.com/Check-Data-Out/velog-dashboard-v2-fe">https://github.com/Check-Data-Out/velog-dashboard-v2-fe</a></li>
<li>BE(API) - <a href="https://github.com/Check-Data-Out/velog-dashboard-v2-api">https://github.com/Check-Data-Out/velog-dashboard-v2-api</a></li>
<li>Back Office - <a href="https://github.com/Check-Data-Out/velog-dashboard-v2-back-office">https://github.com/Check-Data-Out/velog-dashboard-v2-back-office</a></li>
</ul>
<p>놀랍게도 대시보드 프로젝트는 벌써 1년이 넘었더라구요. <del>시간 진짜 너무 빠르네요.</del> 이 프로젝트는 앞으로도 그냥 이런 “적당한 기술 놀이터” 같은 느낌으로 계속 이어갈 것 같습니다.</p>
<p>통계 데이터 개수만 벌써 곧 1천만개 입니다...! (무료로, 어떻게든, 살아남는 중)</p>
<h3 id="마지막으로">마지막으로</h3>
<ul>
<li>아직 안 써보신 velog 유저분들은 한 번만 딸각 해보세요 :) <a href="https://velog-dashboard.kro.kr/">https://velog-dashboard.kro.kr/</a></li>
<li><strong><em>PR/이슈/피드백 모두 환영입니다!!</em></strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM - 바이브 코딩과 LLM에 대한 대규모 서베이, 논문 리뷰]]></title>
            <link>https://velog.io/@qlgks1/a-survey-of-vibe-coding-with-llm</link>
            <guid>https://velog.io/@qlgks1/a-survey-of-vibe-coding-with-llm</guid>
            <pubDate>Sun, 21 Dec 2025 05:41:45 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: LLM을 활용한 Vibe Coding (Preprint 상태)에 대한 세계 최초의 포괄적이고 체계적인 리서치 &amp; 논문 리뷰잉, 단순 번역이 아니라 요약, 순서 재정리, 주관이 첨가되어 있음. <strong><em>스크롤 압박 주의</em></strong> ]</p>
<h1 id="a-survey-of-vibe-coding-with-large-language-models">A Survey of Vibe Coding with Large Language Models</h1>
<blockquote>
<p><a href="https://arxiv.org/abs/2510.12399">LLM 기반 Vibe coding의 세계 최초 서베이 논문</a> 에 대한 정리 및 분석 글이다. 초점은 &quot;코딩을 위한 LLM, LLM 기반 코딩 에이전트, 코딩 에이전트를 위한 개발 환경, 피드백 메커니즘&quot; 에 맞춰져 있다. 정말 놀라운건, &quot;바이브코딩&quot; 이라는 단어가 등장한지 1년도 안 됐다! OpenAI의 공동 설립자이자 전 테슬라 AI 디렉터(Director of AI)인 안드레이 카페시(Andrej Karpathy)가 자신의 X(구 트위터) 에 내용을 올리면서 시작되었다. 이는 처음에 밈처럼 번지다가 지금은 문화 이상, 도구 그 이상의 가치가 되어버렸다. </p>
</blockquote>
<blockquote>
<p>이 서베이가 일단 목차부터 재미있다. 그리고 바이브코딩에 대한 전반적인 동향에 대한 리서치 뿐 아니라 진짜 진지하게 학문적으로 접근했다.  <del>사실 나에게는 여전히 바이브코딩이라는 단어가 하나의 밈처럼 느껴진다</del></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/089b7259-cfbf-4e17-8abb-8c5369304074/image.png" alt=""></p>
<p><a href="https://x.com/karpathy/status/1886192184808149383">출처: https://x.com/karpathy/status/1886192184808149383</a></p>
<h2 id="1-vibe-coding">1. Vibe Coding</h2>
<h3 id="1-들어가며">1) 들어가며...</h3>
<p>해당 논문에서는 바이브코딩을 *&quot;대규모 소프트웨어 개발을 위한 엔지니어링 방법론&quot;* 으로 정의한다. (we define Vibe Coding as an engineering methodology for software development grounded in large language models)</p>
<ul>
<li><p>새로운 개발 방법론인 <strong>“Vibe Coding”</strong> 에서는 개발자가 코드를 한 줄 한 줄 확인하기보다는 실행 결과를 관찰하여 AI가 생성한 구현을 검증한다. </p>
</li>
<li><p>이러한 혁신적인 패러다임은 큰 잠재력을 지니지만, 그 효과가 아직 충분히 검증되지 않았고 인간-AI 협업 측면에서 예상치 못한 생산성 저하와 근본적인 과제들이 보고되고 있다. </p>
</li>
<li><p>실제로, 한 연구에서는 Cursor 에디터와 Claude 모델을 활용한 숙련 개발자들의 작업 완료 시간이 기대보다 19% 증가한 것으로 나타나, Vibe Coding의 효용성에 대한 의문을 제기하기도 했다. </p>
</li>
<li><p>논문에서는 자연어로 이루어지는 비구조적 지시만으로는 세밀한 요구사항이나 아키텍처 제약을 전달하기 어려우며, 인간 개발자와 AI 에이전트 간의 효율적인 협업을 위해서는 체계적인 프롬프트 엔지니어링 및 컨텍스트 엔지니어링, 구조화된 지침 제공, 그리고 상호작용 유형별로 균형 잡힌 주체성 분배 등이 필수적임을 보여준다. </p>
</li>
</ul>
<blockquote>
<p>Vibe Coding이란 대규모 언어 모델에 기반한 새로운 소프트웨어 개발 방법론으로, 인간 개발자, 소프트웨어 프로젝트, 코딩 에이전트 간의 삼자 상호작용을 중심으로 한다.</p>
</blockquote>
<ul>
<li><p>Vibe Coding 패러다임에서 인간은 더 이상 직접 코드를 작성하는 주체가 아니라, 의도 전달자이자 맥락 제공자, 그리고 최종 품질 평가자의 역할이 되었다. </p>
</li>
<li><p>프로젝트는 단순한 정적 코드 저장소를 넘어, 코드베이스와 데이터베이스, 도메인 지식까지 아우르는 다층적 정보 공간으로 확장되었다. </p>
</li>
</ul>
<h3 id="2-바이브코딩의-3자-협업-triadic-collaboration">2) 바이브코딩의 3자 협업 (Triadic Collaboration)</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c97a8ca6-f4d7-4c5b-8f7c-62085f803bd6/image.png" alt=""></p>
<ol>
<li>인간 개발자 (Human Developer, $$H$$)<ul>
<li>직접 코드를 짜는 작성자에서 의도 표명자(Intent Articulator) 및 <strong>품질 판정자(Quality Arbiter)</strong>로 역할</li>
<li>요구사항을 지시하고, 결과를 검토하여 수용할지 거부할지 결정한다. </li>
</ul>
</li>
<li>소프트웨어 프로젝트 (Software Project, $$P$$)<ul>
<li>단순한 코드 저장소를 넘어, 코드베이스, 데이터베이스, 도메인 지식을 포함하는 다면적인 정보 공간(Multifaceted Information Space).</li>
</ul>
</li>
<li>코딩 에이전트 (Coding Agent, $$Aθ$$)<ul>
<li>인간의 의도와 프로젝트의 제약 조건 하에서 코드 생성, 수정, 디버깅을 수행하는 <strong>지능형 실행자(Intelligent Executor)</strong></li>
</ul>
</li>
</ol>
<p>이들의 관계는 <strong>&quot;반복적인 지시-피드백 루프(Iterative Instruction-Feedback Loop)&quot;</strong> 를 통해 작동하며, 개발자는 코드의 세부 사항보다는 <strong>결과(Outcome)</strong> 를 중심으로 검증한다. </p>
<blockquote>
<p><strong><em>이 3자 관계는 수학적으로 제약된 <a href="https://en.wikipedia.org/wiki/Markov_decision_process">마르코프 결정 과정(Constrained MDP)</a> 으로 정식화 할 수 있다.</em></strong></p>
</blockquote>
<h4 id="1-시스템-정의-v--langle-h-p-a_theta-rangle"><strong><em>(1) 시스템 정의: $$V = \langle H, P, A_{\theta} \rangle$$</em></strong></h4>
<p>Vibe Coding 시스템 $$V$$는 다음 세 요소로 구성된다.</p>
<ul>
<li><strong>$$H$$ (인간):</strong><ul>
<li><strong>요구사항 인지 능력</strong> ($$H_{req}: D \rightarrow I$$): 도메인 요구사항($$D$$)을 자연어 지시($$I$$, Instruction)로 변환한다.</li>
<li><strong>품질 판별 능력</strong> ($$H_{eval}: O \rightarrow {0, 1} \times F$$): 에이전트의 산출물($$O$$)을 보고 수락(1)/거부(0)를 결정하고 피드백($$F$$)을 준다.</li>
</ul>
</li>
<li><strong>$$P$$ (프로젝트):</strong> 프로젝트의 컨텍스트 공간은 $$P = \langle C_{code}, C_{data}, C_{know} \rangle$$로 정의된다.</li>
<li><strong>$$A_{\theta}$$ (에이전트):</strong> 매개변수 $$\theta$$를 가진 LLM으로, 조건부 생성 함수 $$A_{\theta}: I \times P \times E \rightarrow O$$를 수행한다 ($$E$$는 실행 환경).</li>
</ul>
<h4 id="2-constrained-mdp-공식-v_mdp"><strong><em>(2) Constrained MDP 공식: $$V_{MDP}$$</em></strong></h4>
<p>이 협업 과정은 다음과 같은 5개의 튜플로 정의된다.</p>
<p>$$
V_{MDP} = \langle S_P, A_{H \rightarrow A_{\theta}}, T_{A_{\theta}|P}, R_H, \gamma \rangle
$$</p>
<ul>
<li><strong>$$S_P$$ (상태 공간):</strong> 프로젝트의 현재 상태 (코드 및 데이터의 상태)에 의해 정의된다. </li>
<li><strong>$$A_{H \rightarrow A_{\theta}}$$ (행동 공간):</strong> 인간의 지시가 에이전트의 행동을 촉발한다.</li>
<li><strong>$$T_{A_{\theta}|P}$$ (전이 함수):</strong> 에이전트가 코드를 수정하여 프로젝트 상태를 변화시키는 과정이며, 프로젝트 사양에 의해 제약된다.</li>
<li><strong>$$R_H$$ (보상 함수):</strong> 인간의 평가(수락/거부 및 피드백)에 의해 결정되는 보상이다.</li>
<li><strong>$$\gamma$$ (감가율):</strong> 미래 보상의 가치를 조정하는 할인 계수다.</li>
</ul>
<h4 id="3-에이전트의-생성-과정-conditional-generation-process"><strong><em>(3) 에이전트의 생성 과정 (Conditional Generation Process)</em></strong></h4>
<p>에이전트가 코드를 생성하는 과정은 다음 확률 분포로 표현된다.</p>
<p>$$
P_{\theta}(Y | I, K, E) = \prod_{t=1}^{T} P_{\theta}(y_t | y_{&lt;t}, C_t)
$$</p>
<ul>
<li><strong>$$I$$</strong>: 인간의 의도 (지시).</li>
<li><strong>$$K$$</strong>: 프로젝트 컨텍스트의 부분집합 ($$K \subseteq P$$).</li>
<li><strong>$$E$$</strong>: 실행 환경.</li>
<li><strong>$$C_t$$</strong>: 생성 단계 $$t$$에서의 <strong>동적 컨텍스트(Dynamic Context)</strong> ($$C_t = A(I, K, E, y_{&lt;t})$$). 이는 다음 세 레이어의 정보를 조합한 것:<ul>
<li><em>Human Layer:</em> 지시사항 ($$c_{instr}$$).</li>
<li><em>Project Layer:</em> 코드, 데이터, 지식 ($$c_{code}, c_{data}, c_{know}$$).</li>
<li><em>Agent Layer:</em> 도구 정의, 메모리, 현재 작업 ($$c_{tool}, c_{mem}, c_{tasks}$$).</li>
</ul>
</li>
</ul>
<h4 id="4-최적화-목표-optimization-objective"><strong><em>(4) 최적화 목표 (Optimization Objective)</em></strong></h4>
<p>Vibe Coding의 핵심 목표는 제한된 컨텍스트 윈도우($$L_{max}$$) 내에서 <strong>최적의 컨텍스트 전략($$F^*$$)</strong> 을 찾아 보상을 최대화하는 것이다!</p>
<p>$$
F^* = \mathop{\arg\max}<em>{F} \mathbb{E}</em>{\tau \sim T} [R(P_{\theta}(Y | C_F(\tau)), Y_{\tau}^*)] \quad \text{s.t.} \quad |C_F(\tau)| \le L_{max}
$$</p>
<ul>
<li>즉, 프로젝트의 방대한 정보 중에서 <strong>어떤 정보($$C_F(\tau)$$)를 에이전트에게 제공해야</strong> 인간이 원하는 이상적인 결과($$Y_{\tau}^*$$)와 가장 유사한 결과를 낼지 결정하는 최적화 문제다!</li>
</ul>
<p><strong><em>(5) 반복적 진화 및 피드백 (Iterative Evolution)</em></strong></p>
<p>인간의 피드백을 통해 요구사항이 점진적으로 구체화되는 과정은 다음과 같이 표현된다:</p>
<p>$$
(o_{k+1}, I_{k+1}) = \begin{cases} (o_k, I_k) &amp; \text{if } A_k = o_k \text{ (완전 수락, 종료)} \ (A_{\theta}(o_k \setminus A_k; \delta_k, I_k, K), I_k) &amp; \text{if } \delta_k \in F \text{ (부분 수정)} \ (A_{\theta}(I_k \cup {\delta_k}, K), I_k \cup {\delta_k}) &amp; \text{if } \delta_k \in I_{new} \text{ (요구사항 확장)} \end{cases}
$$</p>
<ul>
<li><strong>설명:</strong> 인간은 에이전트의 산출물($$o_k$$)을 보고 일부만 수락($$A_k$$)하고 수정 사항($$\delta_k$$)을 지시하거나, 아예 새로운 요구사항을 추가하여 지시 집합($$I$$)을 확장($$I_{k+1}$$)한다. 이는 <strong>&quot;점진적 요구사항 명료화(Progressive Requirement Clarification)&quot;</strong>를 수학적으로 표현한 것이다.</li>
</ul>
<h3 id="3-뭐라는거에요">3) 뭐라는거에요..?</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/5439fe65-fae6-485c-b0b4-72dbb2a06c76/image.png" alt=""></p>
<p>처음에 논문의 위 설명을 보고 굉장히 당황스러웠다ㅎㅎ;; 논문에서는 바이브코딩의 이 3자 협업이 &quot;새로운 공학&quot; 이며 &quot;수학적으로 계산 가능한 시스템&quot; 이라는 걸 증명하기 위해서 정리했다. </p>
<p>쉽게 좀 바꿔서 이해해보자면 아래와 같다.</p>
<ul>
<li>인간을 &quot;사장님/팀장님&quot; 으로, <strong>&quot;무엇(What)&quot;</strong> 을 만들지 지시하고, 결과물이 나오면 <strong>&quot;왜 합격/불합격인지(Why)&quot;</strong> 을 판단하며</li>
<li>에이전트를 &quot;손이 엄청 빠른 인턴&quot; 으로, 사장님이 시키는 대로, 그리고 회사의 규칙대로 실제로 <strong>&quot;어떻게(How)&quot;</strong> 일을 처리할지 고민하고 실행하며</li>
<li>프로젝트를 &quot;회사 창고/규정집&quot; 으로, 인턴이 일을 할 때 참고해야 하는 <strong>&quot;어디서(Where)&quot;</strong> 에 해당하는 정보를 의미한다.</li>
</ul>
<p>그리고 &quot;수학적 접근&quot;으로 결국 논문에서 중요하다고 강조하는 것들은 아래 3가지라 이해된다.</p>
<ul>
<li>말을 잘해야 한다 (<strong><em>Context Engineering</em></strong>)</li>
<li>보는 눈을 길러야 한다 (<strong><em>Evaluation</em></strong>)</li>
<li>반복해야 한다 (<strong><em>Iterative Loop</em></strong>)</li>
</ul>
<p>그니까.. 바이브코딩은 결국 최적화 문제라는거다..!! 제한된 컨텍스트 윈도우($$L_{max}$$) 내에서 <strong>최적의 컨텍스트 전략($$F^*$$)</strong> 을 찾아 보상을 최대화하는 것을 목표로 해야 한다..! 이를 Markov decision process로 접근한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/88bfc87d-3550-4e3c-9103-cfc50a9c5ee3/image.png" alt=""></p>
<p><em>진짜 얼마나 LLM쪽 AI, model 이 대격변인지 보여주는 사진이다..</em></p>
<h3 id="4-코딩을-위한-대규모-언어-모델들">4) 코딩을 위한 대규모 언어 모델들</h3>
<p>논문에서는 코딩을 위한 LLM(Code LLMs)을 Vibe Coding 생태계를 지탱하는 핵심 인프라로 보고 있다. 어떻게 Code 전용 LLM을 구성했는지 개괄적인 내용을 다룬다.</p>
<h4 id="1-데이터-기초data-foundation">(1) 데이터 기초(Data Foundation)</h4>
<ul>
<li><p><strong>코드 말뭉치 (Code Corpora)</strong>: 주로 GitHub나 Stack Overflow 같은 오픈 플랫폼에서 데이터를 수집한다. &#39;The Stack&#39;과 같은 데이터셋은 허가된 라이선스가 있는 코드만을 선별하여 법적 리스크를 줄였다. </p>
</li>
<li><p>데이터 구성 전략은 인기 있는 언어에 집중하는 &#39;깊이 중심(depth-focused)&#39; 전략과 다양한 언어를 포괄하는 &#39;너비 중심(breadth-focused)&#39; 전략으로 나뉜다. </p>
<ul>
<li>깊이 중심 The Stack (v1): 라이선스 문제가 없는(permissively licensed) 소스 코드 3.1TB를 사용하며, 단 30개의 프로그래밍 언어에만 집중했다. 특히 라이선스와 데이터 출처(provenance) 관리에 엄격했다.</li>
<li>너비 중심 The Stack v2: 깊이 중심이었던 v1과 달리 커버리지를 대폭 확장했습니다. 무려 619개의 언어를 포함하며 데이터 양도 67.5TB로 폭증했다.</li>
<li>GPT-Neo: &quot;The Pile&quot;이라는 다양한 혼합 말뭉치(mixed corpora)를 사용했고, </li>
<li>CodeLlama: SlimPajama 데이터셋의 6,270억(627 billion) 토큰과 The Stack의 코드를 결합하여 광범위한 학습을 수행했다.</li>
<li>Arctic-SnowCoder: 필터링된 데이터 조합을 통해 4,000억(400 billion) 토큰을 사용했다. </li>
</ul>
</li>
<li><p><strong>지침 및 선호도 데이터 (Instruction &amp; Preference Datasets)</strong>: 단순히 코드를 완성하는 것을 넘어 사용자의 의도를 따르게 하기 위해, 커밋 메시지나 자연어 지침이 포함된 데이터셋(CommitPack, OpenCodeInstruct 등)을 사용한다. 비용 절감을 위해 &#39;Self-Instruct&#39;나 &#39;Evol-Instruct&#39;와 같이 AI가 합성 데이터를 생성하여 학습에 활용하는 방식이 주류가 되고 있다고 한다. </p>
</li>
</ul>
<p>생각보다 라이선스와 출처에 엄격했던 모델이 꽤 있다는 점에 놀랐다. 조금 덧붙이면, 9월에 Claude Code Meetup Seoul 가서 클로드 코드 개발 관계자가 바이브코딩엔 Typed Language(정적 타입 언어) 를 더 추천한다고 했다. Rust, TypeScript(TS) 이 2가지를 가장 먼저 말했던게 기억에 남는다 ㅎㅎ (<del>제발 python + mypy 조합으로 다시 학습시키고 싶다..</del>)</p>
<h4 id="2-사전-학습-기술-pre-training-techniques">(2) 사전 학습 기술 (Pre-training Techniques)</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/d2f80be7-8bdc-44b2-b4cd-6ecb4216497e/image.png" alt=""></p>
<p><em>얼마나 방대한양을 사전 조사했는지 알 수 있는 &quot;대표 코딩 데이터셋 개요&quot; table,, (중간에 짤렸다. 논문 본문 참조.)</em></p>
<ul>
<li><p><strong>마스크 언어 모델링 (MLM)</strong>: 문맥을 양방향으로 이해하는 데 유리하며 주로 코드 이해(Code Understanding) 모델(CodeBERT 등)에 사용된다.</p>
</li>
<li><p><strong>자동 회귀 모델링 (Autoregressive)</strong>: 이전 문맥을 바탕으로 다음 토큰을 예측하는 방식으로, 코드 생성(Code Generation) 모델(CodeGPT, CodeLlama 등)의 기초가 된다.</p>
</li>
<li><p><strong>중간 채우기 (Fill-in-the-Middle)</strong>: 코드의 앞부분(<code>prefix</code>)과 뒷부분(<code>suffix</code>)을 보고 중간을 채우는 능력으로, 코드 완성 기능에 필수적이다!</p>
</li>
<li><p><strong>지속적 사전 학습 (Continual Pre-training)</strong>: Llama 2와 같은 일반 목적의 LLM을 가져와서 코드 데이터로 추가 학습시켜 코딩 능력을 강화하는 전략이다.(예: CodeLlama, DeepSeek-Coder-V2). 이는 기존 지식을 잊어버리는 &#39;재앙적 망각(catastrophic forgetting)&#39;을 방지하면서 전문성을 높이는 것이 핵심 과제이다.</p>
</li>
</ul>
<p>이 이상은 논문에서 해당 섹션을 참고하는 것을 추천한다. &quot;Fill-in-the-Middle&quot; 과 같은 pre-training을 할까 했었는데 실제로 적극적으로 하는 것에 놀랐다. <del>근데 왜 내 코드에는 syntax 안지키는 코드만 fill in 할까?</del></p>
<h4 id="3-사후-학습-기술-post-training-techniques">(3) 사후 학습 기술 (Post-training Techniques)</h4>
<ul>
<li><p><strong>지도 미세 조정 (SFT)</strong>: 모델이 지침을 잘 따르도록(Instruction Following) 가르친다. 최근에는 매개변수의 일부만 업데이트하는 <strong><code>LoRA(Low-Rank Adaptation)</code></strong> 같은 효율적인 튜닝 기법이 널리 쓰인다. 데이터의 양보다는 &#39;질&#39;이 중요하다는 것이 입증되어, 적지만 고품질의 데이터를 선별해 학습시키는 추세다. </p>
<ul>
<li>이게 &quot;바이브코딩&quot; 을 발전시키는 중추로 보인다. 인간이 작성한 문제-해결 예시를 반복적으로 변형하여 대규모의 학습 데이터를 생성하는 Instruction Evolution 기법과 같이 말이다. </li>
<li>보안 강화를 위한 특수 튜닝으로 취약점 없는 코드를 생성하도록 모델을 훈련시켜 보안 수준을 크게 향상시킨 사례도 보고되었고, 성능 최적화나 코드 수정 및 디버깅 작업에 특화된 튜닝 전략들도 연구되고 있다 한다. </li>
</ul>
</li>
<li><p><strong>강화 학습 (Reinforcement Learning)</strong>: 모델을 인간의 선호도나 객관적인 정답에 맞게 정렬(Alignment) 한다. </p>
<ul>
<li><strong><code>RLHF/DPO</code></strong>: 인간의 피드백이나 선호 데이터를 이용해 모델을 최적화</li>
<li><strong>실행 피드백 활용</strong>: 코딩 도메인의 특수성을 활용하여, 컴파일러의 성공 여부나 유닛 테스트 통과 여부를 보상(Reward) 신호로 사용하여 강화 학습을 수행한다(CodeRL, PPOCoder). 이는 수학이나 코딩처럼 정답 검증이 가능한 영역에서 특히 효과적이다.</li>
</ul>
</li>
</ul>
<h3 id="5-코딩-에이전트">5) 코딩 에이전트</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c80056bb-fa63-4d8f-95dd-480910b6bd2d/image.png" alt=""></p>
<p>코딩 에이전트는 LLM 기반으로 동작하며, 코드 자동 완성 수준을 넘어 자율적으로 프로그래밍 작업을 수행하는 인공지능 요원을 의미한다.</p>
<p>이러한 에이전트들은 코드 생성 기능과 함께, <strong>문제 해결을 위한 계획 수립 능력(planning)</strong>, 현재까지의 진행 상황이나 중요한 정보를 기억하는 <strong>메모리 메커니즘(memory)</strong>, 그리고 외부 도구나 환경과 상호작용할 수 있는 <strong>툴 통합 능력(tool integration)</strong> 등을 갖추고 있다.</p>
<p>이 챕터는 코딩 에이전트를 <strong>인지 시스템(Cognitive System)</strong> 으로 보고, 인간 개발자와 유사하게 사고하고 행동하도록 만드는 5가지 핵심 구성 요소를 소개한다. </p>
<h4 id="1-분해-및-계획-능력-decomposition-and-planning-capability">(1) 분해 및 계획 능력 (Decomposition and Planning Capability)</h4>
<ul>
<li><p><strong>작업 분해 (Task Decomposition)</strong>: &#39;생각의 사슬(Chain-of-Thought, CoT)&#39;이나 &#39;생각의 나무(Tree-of-Thoughts, ToT)&#39;와 같은 기법을 사용하여 복잡한 요구사항을 단계별로 추론하고 분할한다.</p>
</li>
<li><p><strong>계획 수립 (Plan Formulation)</strong>: 에이전트는 PDDL(Planning Domain Definition Language) 같은 형식을 사용하거나, ReAct(Reasoning + Acting) 패턴을 통해 &#39;생각-행동-관찰&#39;의 루프를 반복하며 동적으로 계획을 수정한다. (PS. 개인적으로 <strong><em><code>ReAct 패턴</code></em></strong> 이 정말 나에게 &quot;에이전틱한 것이 뭔가&quot; 에 대해 진지하게 고민할 수 있는 포인트를 만들어 줬다.)</p>
</li>
</ul>
<h4 id="2-메모리-메커니즘-memory-mechanism">(2) 메모리 메커니즘 (Memory Mechanism)</h4>
<ul>
<li><p><strong>단기 및 장기 기억</strong>: 인간의 인지 구조를 모방하여, 현재 추론 중인 내용은 단기 기억(Short-term)에, 방대한 프로젝트 지식이나 과거 디버깅 이력은 벡터 데이터베이스(Vector DB) 등을 활용한 장기 기억(Long-term)에 저장한다. </p>
</li>
<li><p><strong>메모리 관리</strong>: 정보를 단순히 저장하는 것을 넘어 통합(consolidation), 인덱싱(indexing), 망각(forgetting, 에빙하우스 망각 곡선 적용) 등의 관리 작업을 통해 효율성을 극대화한다. </p>
</li>
</ul>
<h4 id="3-행동-실행-action-execution">(3) 행동 실행 (Action Execution)</h4>
<ul>
<li><p><strong>도구 호출 (Tool Invocation)</strong>: 터미널, 파일 시스템, 컴파일러, 웹 검색 등 외부 도구를 API 형태로 호출합니다. 최근에는 <strong>MCP (Model Context Protocol)</strong> 와 같은 표준화된 인터페이스가 등장하여 에이전트와 도구 간의 상호운용성을 높이고 있다. <strong><em>(PS. 최근엔 <a href="https://code.claude.com/docs/ko/skills">claude 가 &quot;SKILL&quot; 이라는 공을 던졌다.</a>)</em></strong></p>
</li>
<li><p><strong>코드 기반 행동 (Code-based Action)</strong>: JSON 형식의 정적인 행동 정의보다, 실행 가능한 파이썬 코드 자체를 행동(Action)으로 생성하여 실행하는 방식이 더 강력한 유연성과 수정 능력을 보여주는 추세다. </p>
</li>
</ul>
<h4 id="4-성찰-반복-검증-및-디버깅-reflection-iteration-validation-and-debugging-vibe-coding의-핵심인-결과-중심-검증을-가능하게-하는-것">(4) 성찰: 반복, 검증 및 디버깅 (Reflection: Iteration, Validation, and Debugging): Vibe Coding의 핵심인 &#39;결과 중심 검증&#39;을 가능하게 하는 것.</h4>
<ul>
<li><p><strong>반복적 개선 (Iterative Refinement)</strong>: 초기 생성된 코드에 대해 스스로 비평(Self-Critique)하거나 컴파일러/테스트 결과를 반영하여 코드를 점진적으로 개선한다.</p>
</li>
<li><p><strong>지능형 디버깅 (Intelligent Debugging)</strong>: 실행 중 발생하는 에러나 중간 변수 상태를 분석(Self-Debugging)하여 논리적 오류를 찾아낸다. 예를 들어, Reflexion 프레임워크는 <em>실패 기록을 장기 기억에 저장하여</em> 다음 시도 때 같은 실수를 반복하지 않도록 한다.</p>
</li>
<li><p><strong>자가 개선(self-improving) 에이전트는 17~53%</strong> 의 추가 성능 향상을 입증했다. 과거 단순한 코드 생성 보조 도구(Assistant) 수준이었던 AI가 이제는 스스로 환경을 설정하고, 코드를 실행하며, 오류를 수정하는 자율적 에이전트(Autonomous Agent) 단계로 진화했음을 보여주고 있다.</p>
</li>
</ul>
<h4 id="5-에이전트-협업-agent-collaboration">(5) 에이전트 협업 (Agent Collaboration)</h4>
<ul>
<li><p><strong>역할 분담 (Role-Based Collaboration)</strong>: 기획자, 프로그래머, 테스터, 리뷰어 등 에이전트에게 특정 페르소나와 역할을 부여한다. </p>
</li>
<li><p><strong>협업 프레임워크</strong>: MetaGPT나 ChatDev와 같이 가상의 소프트웨어 회사를 모델링하여, 에이전트들이 표준 운영 절차(SOP)에 따라 대화하며 소프트웨어를 개발하는 방식이 대표적이다. </p>
</li>
</ul>
<p>사견으로 이 &quot;에이전트 협업&quot; 관점이 요즘 개발자들이 가장 활발하게 시도하는 섹터라고 보인다. 다양한 mcp 또는 병렬 호출, 최근의 skills 에 이르기까지, &quot;동시 다발적으로 다양한 형태로 에이전트에게 일을 던지고 A to Z 를 하게 하는 행위&quot; 에 다들 뭔가 하나씩 시도하는게 보인다. </p>
<h3 id="6-코딩-에이전트-개발-환경">6) 코딩 에이전트 개발 환경</h3>
<h4 id="1-에이전트가-생성한-코드를-실제로-실행하기-위해서는-격리된-실행-환경isolated-execution-environment-이-필요하다">(1) 에이전트가 생성한 코드를 실제로 실행하기 위해서는 <strong>격리된 실행 환경(isolated execution environment)</strong> 이 필요하다.</h4>
<p><strong><em><a href="https://old.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/">Claude CLI가 홈 디렉터리를 삭제해 Mac이 초기화되었어요</a></em></strong>. <del>긱뉴스와 레딧에서 아주 인상깊었던 일이다.</del></p>
<ul>
<li><p><strong>컨테이너화 기술 (Containerization)</strong>: Docker와 같은 기술을 사용하여 운영체제 수준에서 가상화를 수행한다. 이는 에이전트에게 일관된 실행 환경을 제공하고, 호스트 시스템과 분리된 독립적인 파일 시스템 및 네트워크 공간을 보장한다.</p>
</li>
<li><p><strong>보안 격리 메커니즘 (Security Isolation)</strong>: 샌드박스(Sandbox)는 1차 방어선 역할을 합니다. <code>gVisor</code> 와 같은 도구나 <code>WebAssembly</code> 기반 엔진, 그리고 <code>Intel SGX/PKU</code> 와 같은 하드웨어 기반 격리 기술을 사용하여 에이전트가 시스템의 민감한 자원에 접근하지 못하도록 권한을 엄격히 제한한다. </p>
</li>
<li><p><strong>클라우드 기반 실행 플랫폼</strong>: <code>Kubernetes</code> 와 같은 오케스트레이션 도구를 통해 에이전트의 작업을 격리된 포드(pod)에 할당하고, 수천 개의 CPU 코어에서 대규모로 코드를 실행하고 평가할 수 있는 확장성을 제공한다. </p>
</li>
</ul>
<h4 id="2-대화형-개발-인터페이스-환경-interactive-development-interface-environment">(2) 대화형 개발 인터페이스 환경 (Interactive Development Interface Environment)</h4>
<p>개발자가 에이전트와 협업하는 접점(IDE)이 어떻게 진화하고 있는지 설명 하는 섹션이다.</p>
<ul>
<li><p>AI 네이티브 개발 인터페이스: 기존의 코드 편집기를 넘어, 개발자가 자연어로 의도를 말하면 에이전트가 코드를 생성하고 수정하는 대화형 방식이 주류가 되고 있다. 커서(<code>Cursor</code>)와 같은 도구는 인라인 제안(<code>Inline Suggestion</code>)과 대화형 상호작용(<code>Conversational Interaction</code>)을 결합하여 문맥을 이해하는 지원을 제공한다. </p>
</li>
<li><p>원격 개발 (<code>Remote Development</code>): GitHub Codespaces와 같이 클라우드 상에 미리 구성된 표준화된 개발 환경을 제공하여, 에이전트가 로컬 환경 설정 문제없이 즉시 작업을 수행할 수 있도록 한다. 비슷하게 클로드가 또 <a href="https://claude.ai/code">https://claude.ai/code</a> 를 통해 웹에서 바이브코딩이 가능하게 만들어버렸다.</p>
</li>
<li><p>도구 통합 프로토콜 표준: 에이전트가 다양한 도구와 원활하게 소통하기 위한 표준이 만들어졌다. (PS. 클로드가 MCP 공을 던진 뒤로 약간 멱살끌고 가는 느낌)</p>
<ul>
<li><strong><code>MCP (Model Context Protocol)</code></strong>: 소스 코드나 문서 같은 문맥 정보를 교환하는 범용 인터페이스.</li>
<li><strong><code>LSP (Language Server Protocol)</code></strong>: 코드 자동 완성이나 진단 기능을 언어에 상관없이 제공.</li>
<li><strong><code>DAP (Debug Adapter Protocol)</code></strong>: 디버깅 상호작용을 표준화.</li>
</ul>
</li>
</ul>
<h4 id="3-분산-오케스트레이션-플랫폼-환경-distributed-orchestration-platform-environment">(3) 분산 오케스트레이션 플랫폼 환경 (Distributed Orchestration Platform Environment)</h4>
<ul>
<li><p>CI/CD 파이프라인 통합: 에이전트가 생성한 코드가 실제 소프트웨어 제품에 통합되기 전에 자동으로 빌드, 테스트, 배포되는 파이프라인(Jenkins, GitHub Actions 등)과 연동된다. (&#39;Vibe Coding&#39;에서도 품질 보증을 위한 필수적인 관문으로 판단된다. 최후의 보루 느낌)</p>
</li>
<li><p>클라우드 컴퓨팅 오케스트레이션: TOSCA와 같은 명세나 Kubernetes를 사용하여 컴퓨팅 리소스를 동적으로 할당하고 관리한다. 이를 통해 에이전트의 워크로드를 효율적으로 처리한다.</p>
</li>
<li><p>다중 에이전트 협업 프레임워크: 복잡한 개발 작업을 수행하기 위해 AutoGen, CrewAI, MetaGPT와 같은 프레임워크를 사용한다. 이들은 기획자, 개발자, 테스터 등 서로 다른 역할을 가진 에이전트들이 협력하여 문제를 해결하도록 조직화하며, 단일 에이전트보다 높은 신뢰성과 확장성을 제공한다. <strong><em>나아가 LLMOps라 불리는 기법을 통해 에이전트 자체의 성능 모니터링과 로그 피드백을 자동화하려는 시도를 하고 있다.</em></strong></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/94f239a1-4242-4297-9d4f-e02f6102b85f/image.png" alt=""></p>
<ul>
<li>그리고 이를 발판으로 더 이상 &quot;Single Agent&quot; 로만 접근하지 않는다. 위 논문에서와 같이, <code>AutoGen</code>, <code>CrewAI</code>, <code>MetaGPT</code>, <code>LangGraph</code> 와 같은 프레임워크는 이러한 패러다임을 잘 보여주고 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/236e9467-5172-48e8-b436-fdb83cd73321/image.png" alt=""></p>
<h3 id="7-피드백-메커니즘">7) 피드백 메커니즘</h3>
<h4 id="1-컴파일러-피드백compiler-feedback">(1) 컴파일러 피드백(Compiler Feedback)</h4>
<ul>
<li>에이전트가 작성한 코드를 컴파일하거나 정적 분석함으로써 얻는 피드백</li>
<li>컴파일 에러, 타입 체크 오류, 린트 경고 등이 이에 해당하며, 이러한 피드백을 통해 명백한 문법 오류나 타입 불일치 등을 에이전트가 즉각 수정할 수 있다.</li>
<li>예를 들어 &quot;세미콜론 누락&quot;과 같은 컴파일 오류 메시지를 에이전트에게 전달하면, 에이전트는 해당 문제를 해결하도록 코드를 변경한다.</li>
</ul>
<h4 id="2-실행-피드백execution-feedback">(2) 실행 피드백(Execution Feedback)</h4>
<ul>
<li>생성된 코드를 실행하여 얻는 런타임 결과나 테스트 결과를 말한다.</li>
<li>프로그램을 실제로 돌려봄으로써 발생하는 예외(exception) 메시지, 실행 로그, 또는 미리 작성된 테스트케이스의 통과/실패 여부 등이 이에 포함된다.</li>
<li>에이전트는 예를 들어 &quot;테스트 X 실패: 입력 5에 대해 8이 아닌 10을 반환함&quot;과 같은 피드백을 받아, 논리 오류를 찾아내고 수정하는 과정을 거친다. </li>
</ul>
<h4 id="3-인간-피드백human-feedback">(3) 인간 피드백(Human Feedback)</h4>
<ul>
<li>개발자 또는 도메인 전문가가 에이전트의 출력에 대해 주는 직접적인 피드백이다.</li>
<li>예컨대, 에이전트가 생성한 코드에 대해 개발자가 &quot;이 부분의 알고리즘 복잡도가 높습니다. 더 효율적으로 개선하세요.&quot;와 같이 조언하거나, &quot;보안상 이 함수에서는 사용자 입력을 검증해야 합니다.&quot;와 같은 지침을 추가로 제공할 수 있다.</li>
<li>인간 피드백은 일반적으로 자연어 형태로 제공되며, 에이전트는 이를 해석하여 코드를 개선한다. 이때 <strong><em><code>RLHF(Reinforcement Learning from Human Feedback)</code></em></strong> 등 학습 기법을 통해 향후 더 나은 출력을 내도록 에이전트를 조정하는 경우도 있지만, Vibe Coding 맥락에서는 실시간 협업 측면의 피드백 제공이 주를 이룬다. </li>
</ul>
<h4 id="4-자체-개선-피드백self-refinement">(4) 자체 개선 피드백(Self-refinement)</h4>
<ul>
<li>에이전트 스스로가 자신의 출력을 검토 및 개선하는 메커니즘이다.</li>
<li>예를 들면, 에이전트가 코드를 생성한 후 곧바로 그 코드를 다시 한번 검토하여 잠재적 버그나 개선점을 찾아내는 것이다.</li>
<li>최근 제안된 <code>Reflexion</code> 기법 등은 에이전트에게 <strong><em>비판적 리뷰어의 프롬프트를 추가로 주어</em></strong>, 에이전트가 자기 코드의 결함을 찾아내고 수정하도록 유도한다.</li>
<li>이러한 자기 피드백 루프를 통해 추가 학습 없이도 출력 품질을 향상시키는 효과가 보고되었다. <code>Self-refinement</code> 는 내부 피드백으로 분류되지만, 충분히 체계화된다면 강력한 자동 디버깅 수단으로 활용될 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c4ee7111-5680-46ce-b93c-08312440d09c/image.png" alt=""></p>
<p><em>바이브 코딩 개발 모델 프레임워크와 소프트웨어 개발 모델 비교 그래프</em></p>
<p><a href="https://arxiv.org/abs/2501.07811">CodeCoR: An LLM-Based Self-Reflective Multi-Agent Framework for Code Generation</a> 논문을 보면 <code>CodeCoR</code> 는 <code>Prompt Agent</code>, <code>Coding Agent</code>, <code>Test Agent</code>, <code>Repair Agent</code> 가 밀접하게 협력하는 &quot;자기 성찰(Self-Reflective)&quot; 기반 멀티 에이전트 프레임워크이다. </p>
<hr>
<h2 id="2-vibe-coding-development-models">2. Vibe Coding Development Models</h2>
<blockquote>
<p>논문은 다음 세 가지 축을 기준으로 모델을 나눈다. 
<em>1. 인간의 품질 통제 (Human Quality Control)</em>: 개발자가 코드를 얼마나 꼼꼼히 리뷰하고 이해하는가?
<em>2. 구조적 제약 메커니즘 (Structured Constraint Mechanisms)</em>: 사전 기획이나 자동화된 테스트가 포함되는가?
<em>3. 문맥 관리 능력 (Context Management Capability)</em>: 프로젝트의 기존 코드나 문서를 얼마나 잘 참조하는가?</p>
</blockquote>
<p>논문은 이 부분을 &quot;현상, 리서치&quot; 정도로 접근한 것 같다. 오히려 논문의 이 분류와 현 상태덕분에 프로젝트 성격에 따른 &#39;트레이드오프(Trade-off)&#39; 와 &#39;다른 사람은 어떻게 하는지?&#39; 에 대해 더 자세하게 알 수 있었다. </p>
<h3 id="1-다섯-가지-개발-모델">1) 다섯 가지 개발 모델</h3>
<h4 id="1-무제약-자동화-모델-uam-unconstrained-automation-model">(1) 무제약 자동화 모델 (UAM, Unconstrained Automation Model)</h4>
<ul>
<li><p>핵심: AI를 전적으로 신뢰하고, 코드를 줄 단위로 검사하지 않으며 기능 동작 여부만 확인한다. (&quot;Vibe Coding&quot;의 원래 정의에 가장 가까움)</p>
</li>
<li><p>특징: 에이전트에게 광범위한 자율성이 부여되며, 개발 속도가 매우 빠르고 진입 장벽이 낮아 비전문가도 프로토타입을 빠르게 만들 수 있다. 전통적인 <strong>RAD(Rapid Application Development)</strong> 와 유사하다.</p>
</li>
<li><p>단점: 인간의 면밀한 감독 없이 진행되므로 코드 품질이나 아키텍처 일관성이 떨어질 위험이 있다. 동시에 보안 취약점이나 기술 부채가 쌓이기 쉽고, 유지보수가 어렵다. (이로 인한 사건 사고 이슈가 아주 쏟아지고 있다..)</p>
<ul>
<li><a href="https://byline.network/2025/08/4-322/">바이브코딩의 배신, AI 에이전트 때문에 DB 날린 개발자</a></li>
<li><a href="https://www.skshieldus.com/blog-security/security-trend-idx-69">하루 만에 AI로 만든 앱, 보안은 안전할까? 바이브 코딩의 양면성</a></li>
</ul>
</li>
<li><p>추천: UAM은 일회용 프로토타입이나 개인용 도구처럼 실패 위험이 낮은 시나리오에 적합, 일회성 프로토타입, 개인용 도구 개발.</p>
</li>
</ul>
<h4 id="2-반복적-대화-협업-모델-iccm-iterative-conversational-collaboration-model">(2) 반복적 대화 협업 모델 (ICCM, Iterative Conversational Collaboration Model)</h4>
<ul>
<li><p>핵심: AI는 &#39;드라이버(Driver)&#39;, 인간은 &#39;내비게이터(Navigator)&#39;가 되어 짝 프로그래밍(Pair Programming) 하듯 지속적인 대화로 개발한다. <code>AI가 코드 생성 → 사람이 검토 및 이해 → 테스트로 검증 → 결과에 따라 수정 or 확정</code> 의 반복.</p>
</li>
<li><p>특징: 인간이 AI가 짠 코드를 이해하고 검토한 뒤 수락한다. 속도와 품질의 균형을 맞출 수 있으며, 팀 단위 프로젝트에 적합하다. </p>
<ul>
<li>애자일 개발의 페어 프로그래밍(pair programming) 관행과 유사하다고 평가된다.</li>
<li>IDE에 붙은(내재된) agent 를 활용할때, cursor 에서 자동완성을 수동적으로 사용할때 가장 흔하게 볼 수 있지 않을까 한다.</li>
</ul>
</li>
<li><p>단점: 개발자의 높은 역량이 요구되며, 잦은 리뷰로 피로도가 높을 수 있다. </p>
</li>
</ul>
<h4 id="3-계획-주도-모델-pdm-planning-driven-model">(3) 계획 주도 모델 (PDM, Planning-Driven Model)</h4>
<ul>
<li><p>핵심: 전통 소프트웨어 공학의 아키텍처 우선 원칙을 Vibe Coding에 적용한 모델이다. 코딩 전에 인간이 기술 사양서, 코딩 규칙 등 <strong>&#39;청사진(blueprint)&#39;</strong> 을 완벽히 설계한 뒤 AI에게 구현을 맡긴다. </p>
</li>
<li><p>특징: 전통적인 워터폴(Waterfall) 방식의 현대적 적용으로 보인다. 구조가 탄탄하고 모듈화가 잘 된 코드를 얻을 수 있다. </p>
</li>
<li><p>단점: 초기 기획 문서 작성에 시간이 많이 걸린다. 하지만 이러한 선투자는 이후 유지보수 비용과 재작업 부담을 줄여주며, 팀 협업 시 프로젝트의 이해도를 높이는 효과가 있다. </p>
</li>
<li><p>추천: 복잡한 풀스택 애플리케이션, 아키텍처가 중요한 프로젝트에 추천된다. </p>
</li>
</ul>
<h4 id="4-테스트-주도-모델-tdm-test-driven-model">(4) 테스트 주도 모델 (TDM, Test-Driven Model)</h4>
<ul>
<li><p>핵심: <strong>TDD(테스트 주도 개발)</strong> 를 적용하여, 인간이 테스트 케이스를 먼저 작성하고 AI가 이를 통과하는 코드를 작성하게 한다. 여기서 조금 더 AI의 손을 빌리면 <strong>*&quot;<a href="https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding">켄트 벡(Kent Beck) 형님과 함께하는 Augmented Coding, &quot;증강 코딩&quot; 잘해보기</a>&quot;*</strong> 형태가 되는 것 같다.</p>
</li>
<li><p>특징: <strong>테스트 케이스 자체를 명세(specification)</strong> 로 사용한다는 점이 특징이다. 인간의 주관적 리뷰 대신 기계적인 검증(테스트 통과 여부)으로 품질을 보증한다. 리팩토링 안정성이 매우 높다. </p>
</li>
<li><p>단점: 테스트 코드를 짜는 초기 비용이 든다. </p>
</li>
<li><p>추천: 핵심 알고리즘, 프로덕션 레벨의 애플리케이션에 추천된다. </p>
</li>
</ul>
<h4 id="5-컨텍스트-강화-모델-cem-context-enhanced-model">(5) 컨텍스트 강화 모델 (CEM, Context-Enhanced Model)</h4>
<ul>
<li><p>핵심: 이는 독립적인 워크플로우라기보다는, <strong>위의 4가지 모델에 결합할 수 있는 &#39;수평적 강화 능력&#39;(보강 기법)</strong> 이라고 명시한다. </p>
</li>
<li><p>특징: <strong><code>RAG</code>(검색 증강 생성)</strong> 기술 등을 활용해 AI에게 프로젝트 전체의 코드, 문서, 스타일 가이드를 주입한다. 그 결과, 에이전트는 코드를 생성할 때 기존 프로젝트 코드와 일관된 명명법과 스타일을 따르고, 기존 함수나 API를 정확히 호출하며, 프로젝트의 아키텍처적 제약을 준수하는 코드를 만들어낼 수 있다. </p>
<ul>
<li>CEM은 앞서 언급한 네 모델과 자유롭게 조합될 수 있는데, 예를 들어 <code>UAM + CEM</code> 조합은 신속하면서도 일정 수준 통제가 가미된 프로토타이핑을 가능하게 하고, <code>ICCM + CEM</code> 은 방대한 코드베이스 협업에, <code>PDM + CEM은</code>  설계 명세 준수에, <code>TDM + CEM</code> 은 최고 수준의 코드 품질 확보에 각각 적합한 접근을 제공한다. </li>
<li>구현 측면에서, CEM은 프로젝트 초기화 시 벡터 DB 인덱스를 생성하고 대화 흐름마다 관련 정보를 검색하여 맥락으로 주입하는 자동 검색 전략이나, 개발자가 참조할 파일을 직접 지정하는 수동 참조 전략 등으로 실현된다. </li>
</ul>
</li>
<li><p>효과: AI가 기존 프로젝트의 스타일을 따르고 API를 정확히 호출하도록 하여 환각을 줄이고 일관성을 높인다. 대규모 레거시 코드 유지보수에 필수적이다. (컨텍스트 윈도우를 생각해보자)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4e1c0c03-e0c7-4f6d-bc4e-013402207720/image.png" alt=""></p>
<p>위 5가지 분류에 따르면 나는 <strong>*<span style="color: rgb(99 148 255);">PDM + CEM 으로 셋업하고 -&gt; TDM 으로 확장하는 형태에</span>*</strong> 가장 가깝다. <strong><em><a href="https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding">켄트 벡(Kent Beck) 형님과 함께하는 Augmented Coding, &quot;증강 코딩&quot; 잘해보기</a></em></strong> 에서도 언급했듯, AI 를 위한 markdown 이 정말 중요해졌고, 이는 RAG와 매우 유사하게 작동된다. </p>
<h3 id="2-미래-전망-및-과제-future-impact-and-open-challenges">2) 미래 전망 및 과제 (Future Impact and Open Challenges)</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ccd97fad-b083-4ace-bb1b-222d35c438a2/image.png" alt=""></p>
<p>위 사진은 원래 논문에서 가장 먼저 등장하는 &quot;Figure 1&quot; 이다. 다 보고 다시 이 그림을 볼때 더 개괄적으로 거시적인 분류에 대해 좀 더 와닿았다. </p>
<blockquote>
<p> The taxonomy of Vibe Coding is categorized into large language model foundations, coding agent architectures, development environments, and feedback mechanisms. Each area encompasses specific techniques and frameworks that collectively advance the systematic integration of LLMs and agents into intelligent and collaborative software development workflows.</p>
</blockquote>
<h4 id="1-개발-프로세스의-재설계-reengineering-of-development-process">(1) 개발 프로세스의 재설계 (Reengineering of Development Process)</h4>
<ul>
<li><p>단계적 주기에서 연속적 마이크로 반복으로: 전통적인 &#39;기획-구현-테스트(SDLC)&#39;의 긴 주기가 <strong>&quot;프롬프트-생성-검증(Prompt-Generate-Validate)&quot;</strong> 이라는 초고속 반복 주기(수 분~수 초)로 대체된다. 설계와 구현의 경계가 흐려지며, 개발자는 실행 결과(Vibe)를 보며 실시간으로 요구사항을 수정한다. (끊임없이 대화하며 점진적으로 목표를 만족시켜가는 <code>iterative goal satisfaction</code> 방식)</p>
</li>
<li><p>개발자 역할의 재정의: 개발자는 코드를 직접 짜는 &#39;저자(Author)&#39;에서, AI에게 맥락을 제공하고 결과를 조율하는 <strong>&#39;문맥 엔지니어(Context Engineer)&#39;</strong> 이자 <strong>&#39;아키텍트(Architect)&#39;</strong> 로 역할이 바뀌고 있다. </p>
</li>
<li><p>프로젝트 관리의 난관: 코드 생성 속도가 예측 불가능(어떤 기능은 1분, 어떤 건 수 시간)해져 공수 산정이 어려워지며, 기존의 코드 리뷰 방식(라인 단위 검토)이 작동하지 않게 된다. </p>
</li>
</ul>
<h4 id="2-코드-신뢰성과-보안-code-reliability-and-security">(2) 코드 신뢰성과 보안 (Code Reliability and Security)</h4>
<ul>
<li><p>수동 리뷰의 한계: AI가 짠 코드를 인간이 일일이 검토하는 것은 Vibe Coding의 장점(속도)을 깎아먹는 모순이다. 또한, 개발자가 AI가 생성한 복잡한 로직을 완전히 이해하지 못할 수도 있다. (개발 속도와 확실성 사이의 트레이드오프 딜레마)</p>
</li>
<li><p>통합된 보안 피드백 루프: 따라서 인간의 개입 없이도 AI가 코드를 생성하는 즉시 보안 취약점을 잡는 자동화된 가드레일이 필수적이다. 전통적인 정적/동적 분석 도구(SAST/DAST)를 프롬프트-생성-검증 사이클에 내장하여, 에이전트가 코드를 내놓을 때마다 자동으로 보안 스캐닝과 품질 분석을 수행하고 그 피드백을 즉각 에이전트에게 제공하는 통합 피드백 루프가 이상적</p>
<ul>
<li>생성 전(Pre-Generation): 프롬프트 단계에서 보안 요구사항 주입.</li>
<li>생성 중(In-flight): 코드가 생성되는 즉시 정적 분석(SAST) 수행</li>
<li>샌드박스 동적 분석: 실행 시점에 동적 분석(DAST) 및 퍼징(Fuzzing)을 통해 런타임 취약점 탐지</li>
</ul>
</li>
<li><p>AWS의 AI 보조 코드 생성기인 <code>Amazon CodeWhisperer(현 Amazon Q Developer)</code> 는 코드 완성 시 보안 취약점 스캔 결과를 함께 제시하는 초기 기능을 선보였는데, 향후에는 이보다 더욱 지능적이고 상황 인지적인 보안 검사 에이전트가 개발되어, Vibe Coding 과정 전반에 걸쳐 인간을 대신해 지속적인 경계 역할을 수행해야 할 것이다.</p>
</li>
<li><p>마지막 방어선으로서 인간 개발자가 최종 감수를 하는 방안도 고려된다. 예컨대 Vibe Coding을 적용하더라도, 안전이 중요한 코드나 윤리적으로 민감한 영역에서는 최종 배포 전에 보안 전문가 또는 개발 리드가 결과물을 점검하여 승인하는 절차를 유지함으로써, 속도와 안전성 사이의 균형을 찾는 것이다. Vibe Coding의 보편화를 위해서는 이러한 다층적인 보안/신뢰성 확보 체계에 대한 연구 개발이 시급하다.</p>
</li>
</ul>
<h4 id="3-에이전트-감독의-확장성-scalable-oversight">(3) 에이전트 감독의 확장성 (Scalable Oversight)</h4>
<ul>
<li><p>새로운 위험 요소: 자율 에이전트는 <strong>연쇄 오류(Cascading Errors)</strong> 를 일으키거나, 불필요한 라이브러리를 마구 설치하는 의존성 증식(Dependency Proliferation) 문제를 야기할 수 있다. </p>
</li>
<li><p>확장 가능한 감독 아키텍처: 인간이 모든 걸 감시할 수 없으므로, <strong>&quot;약한 감독자가 강한 에이전트를 통제(Weak-to-Strong Generalization)&quot;</strong> 하는 기술이 필요합니다.</p>
<ul>
<li>계층적 감독: 작은 AI 모델이 큰 AI 모델을 1차로 감시.</li>
<li>다중 에이전트 토론(Multi-Agent Debate): 여러 에이전트가 서로의 코드를 비평하며 오류를 찾아내는 기술 필요.</li>
<li>자동화된 감시견(Watchdog): 에이전트가 권한을 넘어서는지 감시하는 별도의 AI 도입.</li>
</ul>
</li>
</ul>
<h4 id="4-인간적-요소-human-factors">(4) 인간적 요소 (Human Factors)</h4>
<ul>
<li><p>새로운 협업 방식에 대한 거부감이나 신뢰 부족은 Vibe Coding 채택의 큰 장애가 될 수 있다. 따라서 에이전트의 의사결정 근거를 설명하는 설명가능성(XAI) 제공, 개발자가 에이전트의 진행 상황을 쉽게 이해하고 개입할 수 있는 UI/UX 디자인, 그리고 개발자의 심리적 안정감을 높일 수 있는 컨트롤 옵션 등이 중요하다. </p>
</li>
<li><p>멘탈 모델의 전환: 개발자는 &#39;알고리즘 구현&#39;보다는 <strong>&#39;의도 표현(Intent Articulation)&#39;</strong> 과 <strong>&#39;문맥 구성(Context Engineering)&#39;</strong> 에 집중해야 한다.</p>
</li>
<li><p>새로운 필요 역량: 프롬프트 엔지니어링, 작업 분해 능력(Task Decomposition), 그리고 AI가 만든 결과물을 검증하는 품질 감독 능력이 코딩 능력보다 중요해진다.</p>
</li>
<li><p>책임과 신뢰의 문제: AI가 버그를 만들었을 때 누구의 책임인가? 개발자가 AI를 맹신(Over-reliance)하거나 지나치게 불신하는 문제를 어떻게 해결할 것인가에 대한 조직적 논의가 필요하다.</p>
</li>
<li><p>주니어들에게 더 이상 간단한 코드 작성 작업보다는 요구사항 명세 작성이나 테스트 시나리오 설계에 집중하게 될 수 있다. 이때 숙련된 개발자와 초급 개발자 간의 업무 분담을 어떻게 새롭게 정의할지, 조직 내 평가 체계나 커리어 패스를 어떻게 조정할지도 인적 측면의 과제다.</p>
</li>
<li><p>인간이 최종 결정권자로 남아있는 구조를 어떻게 구현할지도 고민해야 합니다. 자동화된 에이전트에게 과업을 위임하더라도, 중요한 제품이나 시스템에 대해서는 인간이 결과를 최종 승인하도록 하는 <code>Human-In-The-Loop</code> 메커니즘을 설계함으로써 예측하지 못한 실수를 방지해야 한다.</p>
</li>
</ul>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li>논문 원본 - <a href="https://arxiv.org/abs/2510.12399">https://arxiv.org/abs/2510.12399</a></li>
<li>바이브 코딩 어원 트윗 - <a href="https://x.com/karpathy/status/1886192184808149383">https://x.com/karpathy/status/1886192184808149383</a></li>
<li><a href="https://en.wikipedia.org/wiki/Markov_decision_process">마르코프 결정 과정(Constrained MDP)</a></li>
<li><a href="https://arxiv.org/abs/2501.07811">CodeCoR: An LLM-Based Self-Reflective Multi-Agent Framework for Code Generation</a></li>
<li><a href="https://old.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/">Claude CLI가 홈 디렉터리를 삭제해 Mac이 초기화되었어요</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[python - 기본적인 파이썬 스타일 가이드와 import, init 그리고 모듈화]]></title>
            <link>https://velog.io/@qlgks1/python-basic-style-guide-and-import-tips</link>
            <guid>https://velog.io/@qlgks1/python-basic-style-guide-and-import-tips</guid>
            <pubDate>Wed, 12 Nov 2025 16:52:58 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: 파이썬 스타일 가이드와 import &amp; init, 기본적인 &quot;모듈화&quot; 팁 기록, <del>사실은 교육자료로 쓰려고 작성함</del> ]</p>
<h1 id="파이썬-스타일-가이드와-import-init-그리고-모듈화">파이썬 스타일 가이드와 import, init 그리고 모듈화</h1>
<blockquote>
<p>python 의 자유도 덕분에 진입 장벽이 낮기에 초기엔 코드 스타일이 굉장히 다이나믹하다.. (정말 막 써도 실행이 되니까..!) 하지만 코드는 작성하는 시간보다 읽는 시간이 길다. 그리고 우리는 항상 AI를 포함한 &#39;누군가&#39; 와 같이 일한다. 일관된 스타일은 가독성을 높이고 유지보수 비용을 낮춘다. 특히 LLM의 시대에는 이 규칙, 일관성이 더 중요해지고 있고 Zen of Python 에서도 <strong><em>“Readability counts”</em></strong> 라고 강조한다.</p>
</blockquote>
<blockquote>
<p>이 글에서는 <strong><code>PEP 8</code></strong> (Python 공식 스타일 가이드)과 <strong><code>Google Python Style Guide</code></strong> (대규모 협업을 위한 권장 규칙), <strong><code>PEP 484</code></strong> (타입 힌트 표준), 그리고 <strong><code>Python import 시스템</code></strong> (정규 패키지 vs 네임스페이스 패키지, 절대 vs 상대 import)에 기반하여 글을 작성해보고자 한다.</p>
</blockquote>
<h2 id="1-스타일-가이드">1. 스타일 가이드</h2>
<h3 id="1-pep8-핵심">1) PEP8 핵심</h3>
<ul>
<li><a href="https://velog.io/@qlgks1/python-all-about-basic#3-peppython-enhance-proposal">PEP 를 처음듣는다면 잠깐 이 글을 추천</a>. </li>
</ul>
<ol>
<li><p><strong><em>네이밍(Naming)</em></strong>: 함수·변수는 소문자 스네이크케이스(snake_case), 클래스는 단어마다 대문자(CapWords/PascalCase) 사용, 상수는 대문자+snake_case로 작성한다. (예: MAX_COUNT)</p>
</li>
<li><p><strong><em>들여쓰기(Indentation)</em></strong>: 스페이스 4칸을 사용한다. 탭 혼용은 금지되고, 기존 코드가 탭을 쓰지 않는 한 새 코드에는 탭을 사용하지 않는다.</p>
</li>
<li><p><strong><em>라인 길이(Line Length)</em></strong>: 한 줄에 79자 제한을 권장한다. Docstring이나 주석은 72자 이내로 줄 바꿈한다. (팀 합의로 88자나 100자 등으로 늘릴 수도 있으나, 일단 팀 내부에서는 일관성 있게 유지해야 한다. PEP 8에 따르면 팀 프로젝트의 경우 99자까지 허용할 수 있지만 docstring/comment는 여전히 72자 권장이다.)</p>
<ul>
<li>정말 왈가왈부가 많은 부분이다 ㅎㅎ. 개인적으로 88자~100자 사이를 더 선호한다. 특히 <code>logging</code> 처리나 문자열처리에 이 길이제한이 너무 과하다고 많이 느낀다.</li>
</ul>
</li>
<li><p><strong><em>공백/여백(Whitespace)</em></strong>: 불필요한 공백을 지양한다. 예를 들어 쉼표나 콜론 앞에는 공백을 넣지 않고 뒤에만 넣는다. 키워드 인자 기본값 설정 시에도 <code>=</code> 양옆에 공백을 넣지 않는 것이 규칙이다. </p>
<ul>
<li>타입 힌트가 있을 때는 예외: <code>def func(arg: int = 0) -&gt; None:</code> 처럼 기본값 설정의 <code>=</code> 앞뒤에 공백을 넣는 것이 권장된다.</li>
<li>추가로 주석은 스페이스 2칸을 띄우고 <code># 주석</code> 을 붙인다.</li>
</ul>
</li>
<li><p><strong><em>import 순서</em></strong>: PEP 8 권장 순서는 ① 표준 라이브러리 ② 서드파티 ③ 로컬 모듈 순으로, 각 블록마다 알파벳 순서를 유지한다. 각 그룹 사이에는 빈 줄을 넣어 구분한다.</p>
<ul>
<li>완전 사견이고 취향일 수 있는데 여기서 python에 얼마나 익숙한지 차이가 보인다고 느껴진다. 왜냐면 애초에 처음 python을 사용하면 표준 라이브러리, 서드파티를 잘 구분못하기에...</li>
</ul>
</li>
<li><p><strong><em>Docstring</em></strong>: 공개 API에는 Docstring을 작성하고, 세부 규칙은 PEP 257을 따른다. 요약은 한 줄로, 이어서 공백 줄, 그리고 자세한 설명 순으로 적는다. 따옴표 3개(&quot;&quot;&quot;)로 감싸고, 한 줄 요약은 마침표로 끝내도록 한다.</p>
</li>
<li><p>사람 대신 <strong>자동 포매터(formatter)</strong> 와 <strong>린터(linter)</strong> 가 스타일을 강제하도록 해보자! <strong><em><a href="https://velog.io/@qlgks1/python-uv-ruff-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%EC%99%9C-%EC%A3%BC%EB%AA%A9-%EB%B0%9B%EB%8A%94%EA%B0%80">uv &amp; ruff 설치부터 project initializing</a> 참조!</em></strong></p>
</li>
</ol>
<p>개인적으로 어떤 언어든 익숙해지기전부터 save할때 <code>auto-formatting</code> 은 절대 비추천한다. 린팅과 포멧팅을 수기로 돌리는게 이런 rule에 대해 익숙해지기 쉽다고 생각한다.</p>
<h3 id="2-google-python-style-guide-포인트">2) Google Python Style Guide 포인트</h3>
<blockquote>
<p><a href="https://google.github.io/styleguide/pyguide.html">https://google.github.io/styleguide/pyguide.html</a> 참조. 많아보이지만 생각보다 복잡하지 않아서 한 10분이면 한 숨에 다 읽을 수 있다. </p>
</blockquote>
<p>구글 스타일가이드에서 가장 핵심포인트는 <strong><em>“이렇게 해도 된다”보다 “이렇게 하지 말라”는 금지를 분명하게 한다는 점이다.</em></strong></p>
<ol>
<li><p>PEP 8 기반 + 명확한 가이드: Google 스타일 가이드는 기본적으로 PEP 8을 따르면서, 각 항목마다 팀 컨벤션으로서 “Yes/No”와 권장/비권장/결론을 명시해준다. 이를 통해 스타일 논쟁을 줄이고 모두가 동일한 결정을 따르도록 유도한다.</p>
</li>
<li><p>Docstring과 타입: Google 스타일 가이드는 Docstring에 Args/Returns/Raises 섹션을 명확히 적는 형식을 제시하며, 함수와 메소드에 타입 힌트 사용을 적극 권장한다. 예를 들어, 인자와 리턴 타입을 명시하고, 예외 발생 가능성을 Docstring에 기술하도록 권고한다. 또한 새로운 공개 API를 작성할 때는 반드시 타입 어노테이션을 포함하고, <strong><em>pytype 등의 정적 검사기를 CI에 적용할 것을 요구한다.</em></strong> (PEP 484의 선택적 타입 체크 개념을 적극 도입하는 추세와 맥을 같이 한다.) </p>
</li>
</ol>
<pre><code class="language-python">def analyze_sales_data(
    transactions: list[dict[str, str | int | float]],
    min_amount: float = 0.0,
    category_filter: str | None = None
) -&gt; dict[str, float]:
    &quot;&quot;&quot;거래 데이터를 분석하여 카테고리별 총액을 계산합니다.

    Args:
        transactions: 거래 정보를 담은 딕셔너리 리스트.
            각 딕셔너리는 &#39;category&#39;, &#39;amount&#39;, &#39;date&#39; 키를 포함해야 함
        min_amount: 집계에 포함할 최소 거래 금액 (기본값: 0.0)
        category_filter: 특정 카테고리만 필터링 (None이면 모든 카테고리 포함)

    Returns:
        카테고리명을 키로, 총 거래액을 값으로 하는 딕셔너리

    Raises:
        ValueError: min_amount가 음수인 경우
        KeyError: 필수 키(&#39;category&#39;, &#39;amount&#39;)가 누락된 경우
    &quot;&quot;&quot;

    if min_amount &lt; 0:
        raise ValueError(&quot;min_amount는 0 이상이어야 합니다&quot;)

    result: dict[str, float] = {}

    for transaction in transactions:
        # 필수 키 검증
        if &#39;category&#39; not in transaction or &#39;amount&#39; not in transaction:
            raise KeyError(&quot;거래 데이터에 &#39;category&#39;와 &#39;amount&#39; 키가 필요합니다&quot;)

        category = transaction[&#39;category&#39;]
        amount = transaction[&#39;amount&#39;]

        # 타입 검증 및 변환
        if not isinstance(category, str):
            continue
        if not isinstance(amount, (int, float)):
            continue

        # 필터링 조건 확인
        if category_filter and category != category_filter:
            continue
        if float(amount) &lt; min_amount:
            continue

        # 집계
        result[category] = result.get(category, 0.0) + float(amount)

    return result</code></pre>
<ol start="3">
<li><strong><em>명확한 금지 규정</em></strong>: 예를 들어 <code>mutable</code> 한 type 기본 인자 사용 금지, 예외 사용 가이드, <code>global</code> 변수 사용 지양 등 실무에서 발생하는 문제 상황에 대한 규칙이 제시되어 있어, 팀원들이 의도를 파악하기 쉽다.</li>
</ol>
<p>그러니 당장 인하우스에서 직접 A to Z 코드 스타일 정의하기 어렵다면, 그냥 PEP8 + Google Python Style Guide 를 그대로 녹이는 건 어떨까?  </p>
<h3 id="3-mypy-같은-static-type-checker-없어도-시그니처-힌트-습관화">3) “mypy 같은 static type checker 없어도” 시그니처 힌트 습관화!</h3>
<ol>
<li><p><code>PEP 484</code>(Type Hints)은 파이썬에 표준화된 타입 표기법을 도입한 제안으로, 3.5 버전부터 적용되었다. 이는 타입 주석을 활용해 정적 분석과 리팩토링, IDE 지원을 개선하려는 목적이었다. 예를 들어 함수 인자와 반환값에 타입을 써두면, IDE가 자동완성이나 오류 검출을 더 잘해줄 수 있다.</p>
</li>
<li><p>동적 타입 언어에 선택적 적용: 타입 힌트는 강제 사항이 아니며, 런타임에 아무 검사도 하지 않는다. <strong><em>파이썬 인터프리터는 타입 어노테이션을 무시하고 (<code>__annotations__</code> 에 저장만 함) 실행에 영향을 주지 않는다.</em></strong> 대신 mypy 같은 별도 오프라인 타입 체크 도구로 검사하도록 설계되었다. 다시 말해 파이썬은 여전히 동적 타이핑 언어이고, 타입 힌트는 권장사항일 뿐이다. (PEP 484에도 “타입 힌트를 절대 의무화하지 않을 것”이라고 명시되어 있다.)</p>
</li>
<li><p>그럼에도 불구하고, 타입 힌트는 점진적으로 많은 프로젝트에서 채택되고 있다. 표준 라이브러리의 <code>typing</code> 모듈이 다양한 타입 힌트 표현을 제공하고, Python 3.9+에서는 <code>list[int]</code> 같이 내장 컬렉션에 제네릭 표기를 지원하며, 3.10부터는 <code>X | Y</code> 형태의 유니언 표기법도 추가되는 등 타입 힌트 문법이 꾸준히 발전 중이다.</p>
<ul>
<li>그러니 from typing 과 같은 <code>import</code> 는 이제 사실 굳이..? 필자는 그렇기에 최소 <code>3.10</code> 이상, 가능하다면 <code>3.12</code> 이상 사용을 권한다.</li>
<li><code>dataclass</code>, <code>Pydantic</code> 에서도 이 제네릭 표기를 지원한다!</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c4c840b8-1d3e-432c-a98b-d4e8e5c782bb/image.png" alt=""></p>
<ol start="4">
<li><p>새 코드엔 타입 힌트 권장: 팀 차원에서 새로운 함수나 메소드를 작성할 때는 파라미터 타입과 리턴 타입을 꼭 명시하도록 한다. 기존 코드를 리팩토링할 때도 함수 시그니처에 타입을 추가하는 것을 권장한다. (Google Style Guide도 신규 public API에 타입 어노테이션을 반드시 추가하도록 요구한다.)</p>
</li>
<li><p>컨테이너 타입 명시: <code>list[int]</code>, <code>dict[str, Any]</code> 처럼 내부 요소 타입을 제네릭으로 명시한다. Python 3.9부터 <code>list[int]</code> 구문을 지원하므로 별도 <code>from typing import List</code>를 하지 않고도 표기가 가능하다. 불특정 타입은 <code>Any</code>를 사용하고, 가능하면 구체적인 <code>Protocol</code>이나 <code>TypeVar</code>로 대체를 고민한다.</p>
</li>
<li><p>유니언과 옵션: <code>X | Y</code> 표기 (typing.Union[X, Y]의 축약)로 여러 타입을 허용할 수 있다. <code>Optional[X]</code>도 <code>X | None</code>으로 쓸 수 있다 (<code>|</code> 연산자 지원은 Python 3.10+).</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/6cc913c6-b807-4bd9-824e-d8c86f2b3852/image.png" alt=""></p>
<ol start="7">
<li><code>TypedDict/Protocol</code> 활용: 복잡한 구조는 <code>TypedDict</code>(PEP 589)로 키-값 타입을 정의하거나, <code>Protocol</code>(PEP 544)로 인터페이스를 정의해두면 코드 이해에 도움이 된다. 이러한 고급 타입은 팀원들이 충분히 숙지한 경우에 단계적으로 도입한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/3d2c060c-e13f-4026-80d9-95654449cee6/image.png" alt=""></p>
<p>아래는 타입 힌트를 포함한 간단한 함수 예시이다. 타입 힌트로 함수의 입력과 출력이 명확히 드러나므로, 읽는 사람이 함수 의도를 쉽게 파악할 수 있다. 또한 잘못된 타입을 넣으려고 하면 IDE나 린터가 경고해줄 수 있다.</p>
<pre><code class="language-python">from typing import Iterable, Sequence

def topk(items: Sequence[int], k: int) -&gt; list[int]:
    &quot;&quot;&quot;Return top-k largest integers from the sequence.&quot;&quot;&quot;
    if k &lt; 0:
        raise ValueError(&quot;k must be non-negative&quot;)
    return sorted(items, reverse=True)[:k]

def join_csv(tokens: Iterable[str]) -&gt; str:
    &quot;&quot;&quot;Join iterable of strings into one CSV string.&quot;&quot;&quot;
    return &quot;,&quot;.join(tokens)</code></pre>
<p>위 코드에서 <code>topk</code> 함수는 정수 시퀀스를 받아 상위 k개 정수를 리스트로 반환하며, <code>join_csv</code>는 문자열 이터러블을 받아 콤마로 연결한 문자열을 반환한다. 각각 타입 어노테이션(<code>Sequence[int]</code>, <code>list[int]</code>, <code>Iterable[str]</code>, <code>str</code>)을 달아두었기 때문에 함수 사용법이 명확해진다.</p>
<h4 id="이제-대부분의-dto-도-dataclass-선에서-모두-정리가-가능하다-python---파이써닉한-dataclass-와-enum">이제 대부분의 DTO 도 dataclass 선에서 모두 정리가 가능하다. <a href="https://velog.io/@qlgks1/python-%ED%8C%8C%EC%9D%B4%EC%8D%A8%EB%8B%89%ED%95%9C-dataclass-%EC%99%80-ENUM">python - 파이써닉한 dataclass 와 ENUM</a></h4>
<hr>
<h2 id="2-모듈화-import-init">2. 모듈화, import, init</h2>
<h3 id="1-python-import-시스템-개요">1) Python import 시스템 개요</h3>
<ul>
<li><p>모듈과 패키지: 파이썬에서 파일 하나(.py)는 곧 모듈(module)이고, 폴더는 패키지(package)로 취급된다. <strong><em>모든 패키지는 모듈의 한 종류일 뿐이며</em></strong>, 단지 <code>__path__</code> 라는 속성을 지닌 모듈이 패키지가 된다. 서브패키지는 점으로 상위 패키지와 연결된 &quot;네임스페이스&quot; 로 구성된다! (예: email.mime.text는 email 패키지 아래 mime 패키지의 text 모듈).</p>
</li>
<li><p>&quot;네임스페이스&quot; 라는 단어가 아직 익숙하지 않다면, 다음 2개의 링크 및 글을 추천한다. <a href="https://en.wikipedia.org/wiki/Namespace">(1) 네임스페이스 설명 위키</a>, <a href="https://realpython.com/python-namespace/">(2) Namespaces in Python</a></p>
</li>
<li><p>정규 패키지 vs 네임스페이스 패키지: 파이썬 3.2까지의 전통적인 패키지는 <strong>정규 패키지(regular package)</strong> 로, 해당 디렉터리에 <code>__init__.py</code> 파일이 있어야 패키지로 인식되고 첫 <code>import</code> 시 <code>__init__.py</code> 가 실행되어 패키지 네임스페이스가 구성된다. 반면 파이썬 3.3부터 도입된 <strong>네임스페이스 패키지(namespace package)</strong> 는 <code>__init__.py</code> 없이도 패키지로 인식되며, 하나의 논리 패키지를 여러 디렉터리에 걸쳐 분산시킬 수 있다. <strong><em>즉, 동일한 패키지 이름을 가진 폴더가 여러 경로에 있어도 하나의 패키지 네임스페이스로 합쳐진다 (이 기능은 <code>PEP 420</code>에 정의됨).</em></strong></p>
</li>
<li><p>사실 <code>__init__.py</code> 은 구버전의 잔재로 보이지만 여전히 활용도가 높다. 파이썬 3.4부터 <code>import</code> 동작을 표현하는 <strong><em><code>ModuleSpec</code></em></strong> 객체 개념이 도입되었다. <code>importlib.machinery.ModuleSpec</code> 은 모듈을 찾고 적재할 때 필요한 메타데이터를 담고 있어, import 시스템을 더 일관되고 투명하게 만들었다. <code>PEP 451</code> 덕분에 <code>finders/loaders</code> 등의 내부 API가 단순화되고 향후 개선의 발판이 마련되었다. (이 부분은 import 훅을 직접 구현하는 경우를 제외하면 파이썬 사용자에게 내부 동작이 투명해진 정도의 의미가 있다.)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/7e07bcaa-08a4-4a52-bbcd-043540926bf3/image.png" alt=""></p>
<p><strong><em><a href="https://www.youtube.com/watch?v=QCSz0j8tGmI">Python&#39;s Import System - Module object|Regular/Namespace Packages|Finders &amp; Loaders|Relative imports</a></em></strong> 영상을 아주 적극적으로 추천한다.</p>
<h3 id="2-initpy의-탄생-배경과-현재-위치">2) init.py의 탄생 배경과 현재 위치</h3>
<ul>
<li><p>과거의 역할: 과거 파이썬에서는 디렉터리가 패키지로 인식되기 위해 반드시 <code>__init__.py</code> 파일이 필요했다. 이 파일이 존재하면 해당 폴더를 패키지로 간주하여 <code>import</code> 할 수 있었고, <code>import</code> 시 파일 내부의 코드가 실행되면서 패키지 초기화가 이루어졌다. 또한 패키지 수준에서 사용될 변수나 함수를 정의하거나, 하위 모듈을 <code>import</code> 해서 네임스페이스에 미리 올려주는 등의 초기화 용도로 활용됐다.</p>
</li>
<li><p>PEP 420 이후: 파이썬 3.3부터는 <code>__init__.py</code> 가 없어도 패키지를 만들 수 있게 되었는데(Implicit Namespace Packages, PEP 420), 여러 분산된 패키지를 하나로 취급할 수 있는 유연성이 생겼다. 따라서 지금은 꼭 필요하지 않은 경우라면 패키지 초기화 파일을 생략할 수 있다. 예컨대 단순히 여러 작은 패키지를 모아 네임스페이스만 공유하고 싶은 경우(예: <code>company.plugin.alpha</code>, <code>company.plugin.beta</code> 등 여러 배포 패키지를 하나의 논리 패키지로 묶을 때) <code>__init__.py</code> 없이도 동작하게 할 수 있다.</p>
</li>
</ul>
<h3 id="3-그럼에도-실무에서는-initpy가-다음-상황에서-여전히-유용하거나-필요하다">3) 그럼에도 실무에서는 <strong>init</strong>.py가 다음 상황에서 여전히 유용하거나 필요하다.</h3>
<ol>
<li><strong><em>초기화 코드가 필요한 경우</em></strong>: 패키지 <code>import</code> 시 한 번 실행되어야 하는 설정이나 검증 코드가 있다면 <code>__init__.py</code> 에 넣어둘 수 있다. 네임스페이스 패키지는 이러한 코드를 쓸 곳이 없다.</li>
</ol>
<pre><code class="language-python"># mypackage/__init__.py
import sys
import logging

# 패키지 버전 정의
__version__ = &quot;1.0.0&quot;

# 최소 Python 버전 검증
MIN_PYTHON = (3, 8)
if sys.version_info &lt; MIN_PYTHON:
    raise RuntimeError(
        f&quot;This package requires Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or higher. &quot;
        f&quot;You are using Python {sys.version_info.major}.{sys.version_info.minor}.&quot;
    )

# 패키지 레벨 로거 설정 (한 번만 실행)
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

print(f&quot;Initialized {__name__} v{__version__}&quot;)</code></pre>
<pre><code class="language-python"># main.py
import mypackage  # &quot;Initialized mypackage v1.0.0&quot; 출력 + 버전 검증 실행

print(mypackage.__version__)  # &quot;1.0.0&quot;
print(mypackage.logger)  # &lt;Logger mypackage (WARNING)&gt;</code></pre>
<ul>
<li>정규 패키지의 경우, 최초 <code>import package</code> 시에 <code>package/__init__.py</code> 가 실행되어 <strong><em>패키지의 <code>__dict__</code> (네임스페이스)이 채워진다.</em></strong> 이때 필요한 변수를 설정하거나, 로그를 설정하는 등 초기화 작업을 수행할 수 있다. 다만, 복잡한 초기화는 지양하고 가능하면 최소한의 작업만 하는 것이 좋다 (<code>import</code> 시간 지연을 피하기 위해).</li>
</ul>
<ol start="2">
<li><strong><em>공개 API 재노출</em></strong>: 패키지 내부 구현 경로를 숨기고, 일관된 상위 API만 노출하고 싶을 때다. <code>__init__.py</code> 에서 하위 모듈의 클래스나 함수를 <code>import</code> 해서 상위 패키지 네임스페이스에 올려주면, 사용자는 깊은 경로를 신경쓰지 않고도 일관된 방법으로 사용할 수 있다.</li>
</ol>
<pre><code>*** 가령 디렉토리 구조가 아래와 같을 때 *** 

mylib/
  __init__.py
  internal/
    __init__.py
    database.py  -&gt; class DatabaseConnection
    auth.py      -&gt; class Authenticator
  utils/
    __init__.py
    validators.py -&gt; validate_email()</code></pre><pre><code class="language-python"># mylib/__init__.py
&quot;&quot;&quot;공개 API 정의 - 사용자는 내부 구조를 몰라도 됨&quot;&quot;&quot;

# 내부 모듈에서 핵심 클래스/함수만 가져와서 패키지 최상위 네임스페이스에 노출
from .internal.database import DatabaseConnection
from .internal.auth import Authenticator
from .utils.validators import validate_email

# from mylib import * 시 노출할 항목 명시
__all__ = [&quot;DatabaseConnection&quot;, &quot;Authenticator&quot;, &quot;validate_email&quot;]</code></pre>
<ul>
<li><p><code>__all__</code> 에 명시된 이름들은 <code>from package import *</code> 할 때 가져올 대상이고, 일반 import에는 영향 주지 않는다.</p>
</li>
<li><p>이 방법은 서로 import 하는 것에 대한 이해도가 없다면! <strong><em>순환 참조를 일으킬 수 있으므로 유의해야 한다.</em></strong> 특히 패키지 간 의존성이 복잡한 경우 <code>__init__.py</code> 에서 너무 많은 것을 가져오면 import 순환 문제가 생길 수 있다. 따라서 상호 의존성이 없는 경량 객체 위주로 활용하는 것이 좋다.</p>
</li>
</ul>
<pre><code class="language-python"># 실제 사용 예시
# 사용자 코드 - 간결한 import (내부 구조 은닉)
from mylib import DatabaseConnection, Authenticator, validate_email

# 내부 경로를 알 필요 없음!
db = DatabaseConnection()
auth = Authenticator()
is_valid = validate_email(&quot;user@example.com&quot;)</code></pre>
<ol start="3">
<li><strong><em>패키지 리소스 접근</em></strong>: <code>importlib.resources</code> 등을 활용할 때, 패키지를 정규 패키지로 두는 편이 리소스 경로 관리가 명시적이다. 네임스페이스 패키지는 물리적 디렉터리가 여러 개일 수 있어 리소스 파일 관리가 복잡해질 수 있다. 예를 들어 <code>files()</code> 함수를 통해 패키지 리소스를 읽을 수 있다.</li>
</ol>
<pre><code class="language-python">from importlib.resources import files
config_text = files(&quot;my_pkg.data&quot;).joinpath(&quot;config.toml&quot;).read_text(encoding=&quot;utf-8&quot;)</code></pre>
<ul>
<li>위 코드는 <code>my_pkg/data/config.toml</code> 파일을 읽는 예시다. 과거에는 <code>pkg_resources</code> 등을 사용했지만, 이제 표준 라이브러리에서 안전하고 일관된 방법을 제공하므로 이를 쓰는 것이 권장된다. 이처럼 패키지 구조와 코드가 분리되어 있을 경우(data 폴더 등), 정규 패키지의 <code>__init__.py</code> 에 해당 서브패키지를 명시적으로 <code>import</code> 해두면 (필요 시) 리소스 접근에도 용이하다.</li>
</ul>
<h3 id="4-절대-vs-상대-import-pep-328">4) 절대 vs 상대 import (PEP 328)</h3>
<ul>
<li><p>절대 경로 <code>import</code> 우선: 기본 원칙은 절대 경로 import를 사용하라는 것이다. 절대 import란 <code>import my_project.utils.parser</code> 또는 <code>from my_project.utils import parser</code> 처럼 <strong><em>최상위 패키지명부터 명시하는 방식이다.</em></strong> </p>
</li>
<li><p>이는 모듈 출처를 명확히 보여주며, 리팩토링(모듈 이동/이름 변경) 시에도 영향 범위를 좁게 만든다. PEP 8에서도 <strong><em>“모든 import는 기본적으로 절대 import로 해라. 내부 모듈과 표준 라이브러리가 이름 충돌할 경우 절대 import가 모호성을 줄인다”라고</em></strong> 권고한다.</p>
</li>
<li><p>상대 <code>import</code> 사용 시기: <code>from . import submodule</code> 같은 명시적 상대 import는 &quot;동일 패키지 내 모듈을 참조할 때&quot; 사용할 수 있다. 다만, PEP 8에 따르면 상대 import는 패키지 레이아웃이 복잡해서 절대 경로가 너무 장황해질 때 등 제한적인 경우에만 쓰고, 일반적으로는 지양한다. 특히 최상위 패키지 경로가 바뀔 수 있는 대형 프로젝트가 아니라면 절대 import로 충분하다. 상대 import를 남용하면 다른 개발자가 코드의 의존 관계를 파악하기 어려워질 수 있다.</p>
</li>
<li><p>Python 2와의 관계: <code>PEP 328</code> 이전엔 (Python 2 시절) 현재 패키지 기준의 암시적 import가 가능했지만, Python 3에서는 명시적 상대 import만 허용하고 기본은 절대 import로 동작한다. 요즘 코드는 모두 Python 3이므로 특별히 <code>from __future__ import absolute_import</code> 등을 신경 쓸 필요는 없지만, 과거 코드 일부를 가져올 경우 이런 맥락을 알아두면 도움이 된다.</p>
</li>
</ul>
<h4 id="absolute_import-왜-씀"><code>absolute_import</code> 왜 씀?</h4>
<ul>
<li>Python 2.4 이하에서는 패키지 내부에서 import string을 실행하면, Python이 먼저 패키지 디렉토리 내부에서 상대 import를 시도한다. </li>
</ul>
<pre><code># 디렉토리 구조
pkg/
  __init__.py
  main.py
  string.py      # 사용자가 만든 모듈</code></pre><pre><code class="language-python"># pkg/main.py (Python 2.4 이하)
import string  # 어떤 string을 import할까?

print(string)</code></pre>
<ul>
<li>Python 2.4 이하에서는 표준 라이브러리의 string 모듈 대신 같은 패키지 내의 <code>pkg/string.py</code> 를 import한다. 이것이 <strong><em>암묵적 상대 import(implicit relative import) 문제</em></strong> 다. 그래서 최상단에 <code>from __future__ import absolute_import</code> 를 사용해 이를 해결했다.</li>
</ul>
<pre><code class="language-python"># pkg/main.py (Python 2.5+)
from __future__ import absolute_import

import string  # 이제 표준 라이브러리의 string을 import!

# 같은 패키지의 string.py를 import하려면 명시적으로:
from . import string as pkg_string  # 명시적 상대 import
# 또는
from pkg import string as pkg_string  # 절대 import</code></pre>
<p>만약 여러분들이 매우 과거 python 과 씨름중이라면,, 레거시와 싸우고 있다면,, 이를 꼭 알아야 한다..!</p>
<h3 id="5-모듈화-원칙과-import패키지-전략-팁">5) 모듈화 원칙과 import/패키지 전략 팁!</h3>
<blockquote>
<p>아래 내용은 디자인패턴을 차치하고 관점을 SWE의 &quot;모듈화&quot; 에 맞춘 최소한의 가이드에 가깝다.</p>
</blockquote>
<ol>
<li><p><strong><em>단일 책임 원칙(<code>Single Responsibility Principle</code>)</em></strong>: 모듈이나 패키지는 하나의 역할에 집중하도록 설계한다. 예를 들어 <code>api/</code>, <code>core/</code>, <code>utils/</code> 처럼 기능별로 디렉터리를 나누고, 각 디렉터리에는 그 책임에 맞는 모듈만 포함시킨다.</p>
</li>
<li><p><strong><em>관심사 분리(<code>Separation of Concerns</code>)</em></strong>: 계층 구조를 명확히 분리하여, 예컨대 <code>api</code> 패키지는 입출력(플라스크 엔드포인트 등)만, <code>core</code> 는 비즈니스 로직만, <code>adapters</code> 는 DB나 외부 API 연동만 담당하도록 한다. 이렇게 나누면 <code>import</code> 방향도 한쪽으로 흐르게 되어(상위 계층 -&gt; 하위 계층) 순환 의존을 피할 수 있다.</p>
</li>
<li><p><strong><em>안정된 API 노출</em></strong>: 내부 구현 세부 사항은 숨기고, 각 계층의 공개 API만 <code>init.py</code> 등을 통해 노출한다. 예를 들어 <code>core/__init__.py</code> 에서 핵심 서비스 함수만 import하여 외부에 공개하면, 바깥에서는 <code>core.run()</code> 처럼 사용하고 내부 구조가 바뀌어도 인터페이스는 유지할 수 있다. (다만 public, common API는 변경 시 호환성에 주의해야 하므로, 팀 합의하에 버전 관리나 deprecation 절차를 둔다.)</p>
</li>
<li><p><strong><em>배포 관점</em></strong>: 실제 패키징(배포)할 때는 폴더 구성과 <code>pyproject.toml/setup.cfg</code> 메타데이터 등이 일관되어야 한다. 이는 <code>PyPA(Python Packaging Authority)</code> 에서 제공하는 가이드에 따라 설계한다. 프로젝트 루트에 <code>pyproject.toml</code> 설정을 두고, 패키지 폴더들은 <code>src/</code> 디렉터리를 활용하는 등의 표준 패키징 레이아웃을 따르면 배포 시 문제가 적다. 또한 <code>namespace</code> 패키지를 쓸 때는 관련된 모든 배포 패키지에서 <code>__init__.py</code> 를 빼야 한다는 점도 유의한다. (한 군데라도 넣으면 네임스페이스 패키지가 제대로 동작하지 않는다.) <em>PS. <a href="https://www.pypa.io/en/latest/">참고로 PyPA는 &quot;파이썬 패키징 관리 그룹&quot; 이다.</a></em></p>
</li>
</ol>
<pre><code class="language-python">my_project/
    api/          # 외부 요청/응답 처리 (입출력 계층)
        __init__.py   (공개 API 재노출)
        ...  
    core/         # 핵심 도메인 로직 (엔진/서비스 계층)
        __init__.py   (공개 API 재노출)
        ...
    adapters/     # DB, 외부 API 등 인프라 연동
        __init__.py   (필요한 경우 설정)
        ...
    utils/        # 여러 곳에서 쓰이는 공용 유틸리티
        __init__.py   (특별한 초기화는 없음)
        ...</code></pre>
<ul>
<li><p>각 패키지의 <code>__init__.py</code> 에는 해당 계층에서 외부에 공개할 필요한 심볼들만 임포트하여 노출한다. 예를 들어 <code>core/__init__.py</code> 에는 <code>from .service import run</code> 정도만 넣고 나머지 내부 구현은 숨긴다. </p>
</li>
<li><p>이렇게 하면 상위 레벨에서 <code>from my_project.core import run</code> 처럼 명확하게 쓸 수 있고, 내부 구조 변경이 있어도 run의 인터페이스만 유지하면 된다. 반면 <code>utils</code> 처럼 단순 헬퍼 모음이라면 굳이 <code>__init__</code> 에 재노출을 하지 않고 각 모듈을 필요한 곳에서 직접 <code>import</code> 해 써도 무방하다.</p>
</li>
</ul>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://velog.io/@qlgks1/python-all-about-basic#3-peppython-enhance-proposal">python - 역사와 특징, 장단점, 기본 정보와 데이터 타입</a></li>
<li><a href="https://velog.io/@qlgks1/python-uv-ruff-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%EC%99%9C-%EC%A3%BC%EB%AA%A9-%EB%B0%9B%EB%8A%94%EA%B0%80">uv &amp; ruff 설치부터 project initializing, 왜 주목 받는가?</a></li>
<li><a href="https://peps.python.org/pep-0008/">PEP 8 – Style Guide for Python Code</a></li>
<li><a href="https://google.github.io/styleguide/pyguide.html">Google Python Style Guide</a></li>
<li><a href="https://peps.python.org/pep-0257/">PEP 257 – Docstring Conventions</a></li>
<li><a href="https://peps.python.org/pep-0484/">PEP 484 – Type Hints</a></li>
<li><a href="https://peps.python.org/pep-0585/">PEP 585 – Type Hinting Generics In Standard Collections</a></li>
<li><a href="https://peps.python.org/pep-0604/">PEP 604 – Allow writing union types as X | Y</a></li>
<li><a href="https://peps.python.org/pep-0420/">PEP 420 – Implicit Namespace Packages</a></li>
<li><a href="https://peps.python.org/pep-0451/">PEP 451 – A ModuleSpec Type for the Import System</a></li>
<li><a href="https://peps.python.org/pep-0328/">PEP 328 – Imports: Multi-Line and Absolute/Relative</a></li>
<li><a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/">Packaging Python Projects – Python Packaging User Guide</a></li>
<li><a href="https://docs.python.org/3/library/importlib.resources.html">importlib.resources – Python Documentation</a></li>
<li><a href="https://black.readthedocs.io/">Black Formatter Documentation</a></li>
<li><a href="https://docs.astral.sh/ruff/">Ruff – An Extremely Fast Python Linter</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[python - uv & ruff 설치부터 project initializing, 왜 주목 받는가? ]]></title>
            <link>https://velog.io/@qlgks1/python-uv-ruff-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%EC%99%9C-%EC%A3%BC%EB%AA%A9-%EB%B0%9B%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@qlgks1/python-uv-ruff-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%EC%99%9C-%EC%A3%BC%EB%AA%A9-%EB%B0%9B%EB%8A%94%EA%B0%80</guid>
            <pubDate>Tue, 30 Sep 2025 17:44:02 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: 의존성 관리와 패키징을 위한 uv 세팅 부터 활용 기록, 린팅과 포맷팅을 동시에 지원하는 ruff 까지 같이하는 세팅 기록 / <strong>*<span style="color: rgb(99 148 255);">최종 업데이트 25.10.16</span>*</strong> ]</p>
<h1 id="uv--ruff-설치부터-project-initializing">uv &amp; ruff 설치부터 project initializing</h1>
<blockquote>
<p>Uv, Ruff 모두 Astral(아스트랄) 회사의 오픈소스, 라이브러리이다. <code>python</code> 생태계는 정말 &quot;파이써닉&quot; 한 방향으로 계속 진화하고 있다. <code>poetry</code> 만 해도 엇그제 같은데, 아스트랄의 제품들이 정말 무서울 정도로의 속도로 관련 오픈소스들을 압살하고 있다..! <strong><em>제일 대표작 Uv, Ruff 오픈소스를 개괄적으로 살펴보자!</em></strong></p>
</blockquote>
<blockquote>
<p>실제 세팅만 보고 싶다면 <a href="#2-uv--ruff-%EC%84%A4%EC%B9%98%EC%99%80-%EC%84%B8%ED%8C%85">2. uv &amp; ruff 설치와 세팅</a> 을 바로 보면 됩니다~</p>
</blockquote>
<h2 id="1-왜-지금-uv와-ruff인가">1. 왜 지금 uv와 ruff인가?</h2>
<h3 id="0-astral아스트랄-을-아시나요">0) Astral(아스트랄) 을 아시나요?!</h3>
<p><strong>*&quot;파이썬 생태계의 생산성을 높이는 고성능 개발 도구를 만든다&quot;*</strong> 라는 일념하에 세워진 아스트랄은 창업자 <code>Charlie Marsh</code> 가 <strong><em>“파이썬 툴링은 훨씬 더 빨라질 수 있다”</em></strong> 는 가설을 <code>Ruff</code> 로 스스로 검증하며 쏘아올려졌다. <del>(상남자다..)</del> - <a href="https://astral.sh/blog/announcing-astral-the-company-behind-ruff">Announcing Astral, the company behind Ruff</a> </p>
<p>23년 4월, &quot;We’ve raised $4m in seed funding led by Accel&quot; 라고 언급되어 있듯, &quot;400만 달러 - 한화 약 57억&quot; 을 시드받고 날개를 활짝 핀 것 같다. 도구는 오픈소스(무료)로 유지하고, 그 위에 유료 hosted service를 만든다는 유료화 목표가 있는 것 같다. 아마 클라우드를 도입할 것 같은데, 어떻게 BM을 만들지 미래가 정말 궁금하다.</p>
<p>대표적으로 아래 3개에 집중하고 있다. 출시 순서도 <code>ruff -&gt; uv -&gt; pyx -&gt; ty</code> 이다. 이 글에서는 <code>ruff</code> 와 <code>uv</code> 만 다뤄보고자 한다. </p>
<ol>
<li><code>Ruff</code>: 린팅·포매팅(편집기 통합 포함)</li>
<li><code>uv</code>: 패키지/프로젝트/스크립트/도구/파이썬 버전까지 원스톱 관리</li>
<li><code>pyx</code>: &quot;Python-native package registry&quot; -&gt; 얘가 아스트랄의 첫번째 유료화 서비스이다. 아직 상용화는 안한 듯 하다. 클로즈 베타인지, 위시리스트를 받는 것 같다. - <a href="https://astral.sh/blog/introducing-pyx">pyx: a Python-native package registry, now in Beta</a>. 꽤 신박하니 소개 글 추천!</li>
<li><code>ty</code>: 타입 체커, 얘는 진짜 핫하다. 2025-09-19 공개된 문서 - <a href="https://docs.astral.sh/ty/">https://docs.astral.sh/ty/</a></li>
</ol>
<h3 id="1-인기성장-지표">1) 인기/성장 지표</h3>
<h4 id="●-uv">● uv</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/3db7f5a2-a0e5-4bee-a85c-99f6d9ff8467/image.png" alt=""></p>
<p><code>Poetry</code> 는 수년간 꾸준히 발전하며 약 <code>33.9k</code> 개의 스타를 얻었지만, 2024년에 등장한 <code>uv</code> 는 <strong><em>불과 1년 만에 스타 개수가 36k를 넘어섰다.</em></strong> 나중에 다시 언급하겠지만, 나는 <code>poetry</code> 가 가상환경을 더 불편하게 업데이트 한 것에 좀 불편함이 있다. </p>
<p>2024년 10월 기준으로 <code>PyPI</code> 에서 전체 패키지 다운로드의 약 <strong><code>13.3%</code> 가 uv를 통해 이루어졌고, uv 자체도 월 2천8백만 건 이상의 다운로드 수를 기록</strong> 하며 거대한 성장세를 증명하고 있다. 도대체 어떻게 한건가.. </p>
<h4 id="●-ruff">● ruff</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c7390771-47f5-4ccc-bf85-830314e97a71/image.png" alt=""></p>
<p>이 정도면 주작아닌가 싶을정도로 ruff 의 성장 기울기는 미쳤다. 불과 2년여 만에 ruff는 GitHub 스타 <strong>27k+</strong> 를 돌파해 <code>Black</code> 을 포함한 기존 도구를 넘어섰다. 실제로 ruff는 주당 약 270개의 스타를 얻으며 핫한 프로젝트 1위에 올랐는데, 이는 Black의 스타 증가 속도(주당 약 107개)보다 두 배 이상 빠른 수치라고 한다. - <a href="https://www.awesomepython.org/hot/#:~:text=Hot%20repositories%20,31">AwesomePython</a></p>
<p>현재 Flake8, Black 등의 역할을 하나로 통합하면서도 수백만 건의 주간 다운로드를 보여주고 있다. 사실 이미 나도 린팅은 <code>black</code> &amp; <code>flake8</code> 버리고 ruff 로 갈아타버렸다. 사실 python 에서 린팅을 위해 사전 세팅이 꽤 많다. 그리고 이를 위한 써드파티나 플러그인도 필요하니, <code>toml</code> 에 종속적인 것을 위해 <code>ruff</code> 로 갈아타고, 자연스럽게 <code>uv</code> 로 스며들고 있다. </p>
<h3 id="2-uv-는-뭘-해결하고-왜-빠를까">2) uv 는 뭘 해결하고, 왜 빠를까?</h3>
<p>아스트랄의 미션과 비전에 얼라인하는 &quot;초고속 패키지/프로젝트 매니저&quot; 오픈소스를 만든 것이다. <code>pip</code>, <code>virtualenv</code>, <code>pipx</code>, <code>pyenv</code>, <code>poetry</code> 등과 같은 도구를 &quot;일원화&quot; 하는게 가장큰 목표이다. 그리고 첫 탄생과 다르게 이제 <code>uv</code> 만으로 라이브러리를 배포하고 버저닝 할 수 있다. (나에게는 이게 가장 큰 요인이었다.) <del>언급된 도구들 진짜 다 써본 것 같은데 개인적으로 아나콘다가 최악이다..</del></p>
<p><a href="https://zaloog.github.io/2025/01/19/uv.html">uv ill like uv</a> 글에 따르면 &quot;Initially Armin Ronacher started rye as a “cargo for python” to unify the fragmented python tooling landscape (for a great overview check out this talk by Anna-Lena Popkes).&quot; 라고 언급되어 있다. </p>
<p>Armin Ronacher가 시작한 실험적 패키징 툴 <code>Rye</code> 가 있는데, Astral의 창업자 Charlie Marsh가 이 아이디어를 이어받아 uv를 통합 후속 프로젝트로 출시한 것 이다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/d7871f79-4830-4008-8f2f-e5801204c778/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/8495bee2-db90-4d5b-a0e5-d3b9d25a2e6a/image.png" alt=""></p>
<p>현재 빠르다고 하는 애들보다 더 빠르다고 한다. (pip 대비 10~100배). 근본적인 빠른 이유는 <code>Rust</code> 기반 컴파일 바이너리파일이기 때문인데, 여러 최적화 스킬이 있다. - <a href="https://astral.sh/blog/uv">uv: Python packaging in Rust</a></p>
<ol>
<li>글로벌 패키지 캐시를 도입하여 동일한 패키지를 반복 설치할 때 다시 다운로드하거나 빌드하지 않는다.</li>
<li>또한 파일 복사 시 Copy-on-Write와 하드링크를 활용하여 가상환경을 만들 때 디스크 복사 비용을 최소화하고</li>
<li>이어서 바로 종속성 해석을 같은 프로세스에 처리해서, python 을 중복 실행하는 오버헤드를 없앴다고 한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/49efafe5-7979-433d-a096-bea44edcf48d/image.png" alt=""></p>
<p><a href="https://deepwiki.com/astral-sh/uv">딥 위키에서 uv</a> 를 보면 더 이해하기 쉽다. 시간있을때 <a href="https://deepwiki.com/search/-uv-python-installer-lets-thin_90419ea2-54e1-4a0d-81bd-8894df0eedb7">https://deepwiki.com/search/-uv-python-installer-lets-thin_90419ea2-54e1-4a0d-81bd-8894df0eedb7</a> 를 따라가면서 한 번 봐보자!</p>
<h4 id="●-pip은">● pip은?</h4>
<p>일단 <code>pip</code> 은 기본적으로 &quot;순차적인 백트래킹&quot; 방식으로 의존성 체크를 한다. 예로 1버전 시도 -&gt; 충돌 -&gt; 1.1버전 시도 이런식으로 말이다. 그렇기에 순환 참조를 포함한 복잡한 의존성 그래프에서는 최적의 조합을 찾지 못하거나 해결에 실패하는 &#39;의존성 지옥(dependency hell)&#39;에 빠질 수 있다. 게다가 이 과정에서 종종 전체 패키지 파일(wheel)을 다운로드하여 메타데이터를 확인해야 하므로 I/O 비용이 크다!</p>
<h4 id="●-uv는">● uv는?</h4>
<p>언급한대로 &quot;SAT 해결기 기반 접근법&quot; 을 사용한다. (<a href="https://en.wikipedia.org/wiki/Boolean_satisfiability_problem">SAT: Boolean Satisfiability Problem, 부울 충족 가능성 문제</a>) 유사한 문제로 취급하는 PubGrub 알고리즘에서 영감을 받았다고 한다. - <a href="https://docs.astral.sh/uv/reference/internals/resolver/">Resolver internals
</a></p>
<h3 id="3-uv-장단점-기존-것과-비교">3) uv 장단점, 기존 것과 비교</h3>
<h4 id="●-uv-장점">● uv 장점</h4>
<ol>
<li><p>rust 바이너리기반 훨등한 속도. <a href="https://fmind.medium.com/poetry-was-good-uv-is-better-an-mlops-migration-story-f52bf0c6c703">Poetry Was Good, Uv Is Better: An MLOps Migration Story</a> 에서 <code>poetry</code> 대비 CI/CD 속도가 기하급수적으로 빨라졌다는 (약 10배) 후기도 있다.</p>
</li>
<li><p>&quot;찐 통합 툴&quot;. <code>installer</code>, <code>pyenv</code>, <code>virtualenv</code> 심플하게 할 수 있으며 (이상한) <code>pipx</code> 대신 <code>uvx, uv tool</code> 로 대신할 수 있다. <code>uv publish</code> 로 <code>twine</code> 없이 배포할 수 있다. </p>
</li>
<li><p>&quot;표준 준수, 심플한 세팅&quot;. <strong><code>PEP 621/631</code></strong> 에 맞는 <code>toml</code> 하나로 패키지 메타데이터 + 의존성 관리가능! 특히 2024년 말 도입된 <strong><code>PEP 735(dependency groups 표준)</code></strong> 을 빠르게 지원. </p>
</li>
<li><p><code>uv sync, uv run</code> 이 의존성 동기화를 &quot;알아서 보장&quot; 해줘서 workflow 의 몇 단계를 하나로 함축시킬 수 있음.</p>
</li>
</ol>
<h4 id="●-사견으로">● 사견으로</h4>
<p><code>poetry</code> 대신 <code>uv</code> 로 무조건 모든 프로젝트를 넘어가지는 않았다. 위 4가지 장점은 사실 <code>poetry</code> 도 매우 잘 해준다. 다만 <code>poetry</code> 가 내게 불편 포인트를 줬던건 <code>1) 다중 python 버저닝을 따로 다루는 것, 2) 가상환경을 즈그들 방식대로 한다는 점.</code> 이다. </p>
<p>사실 2번이 매우 열받는 포인트인데, <code>source .venv ...</code> 로 vscode 에게 가상환경 먹여줘야 할 때, 그리고 중앙관리식 가상환경을 쓰면 오히려 사용자인 내가 가끔 까먹는다. 이게 관성에 벗어나는 방식이라...</p>
<p>또, 프로덕션에서는 나는 굳이 <code>uv</code> 또는 <code>poetry</code> 를 사용해서 서버를 운영하지는 않는다. 추가 써드파티나 의존성을 전혀 하고 싶지 않아서 인데, <code>uv</code> 는 오히려 production 에서도 사용해볼까 한다. </p>
<h4 id="●-uv-단점">● uv 단점</h4>
<ol>
<li><p>CLI 커맨드가 꽤 많다. 그리고 생각보다 마냥 간단하지는 않다. <code>uv lock --upgrade</code> 과 같이 아니 왜 <code>update</code> 가 아닌건데? ㅎㅎ. </p>
</li>
<li><p><code>uv pip install</code> 과 <code>uv add</code> 는 다르다. <code>uv add</code> 를 하고 <code>uv sync</code> 로 설치하는 흐름. 애매한 경계선에 있는 명령어들 때문에 헷갈릴 수 있다.</p>
</li>
<li><p><code>poetry</code> 는 <code>python</code> 으로 만들어졌다. 근데 <code>uv</code> 는 <code>rust</code> 로 만들어졌다. 사실 <code>python</code> 생태계에서는 이게 꽤나 중요할지 모른다. <code>rust</code> 로 만들어진 <code>python</code> 을 위한 생태계 툴이라니,, 제 3의 기여자를 만들기 어렵다고 판단된다. </p>
</li>
</ol>
<p>근데 아스트랄이 소통을 많이 하는 듯 하다. 다양한 장단점을 흡수하고 개선하며 정말 지속적으로 빠르게 발전하고 있다.</p>
<h3 id="4-ruff-는-뭘-해결하고-왜-빠를까">4) ruff 는 뭘 해결하고, 왜 빠를까?</h3>
<p>아스트랄의 시작이자, 이름을 알린 태초의 라이브러리, ruff는 기존에 Python 코드 품질 관리를 위해 Flake8, isort, pylint, Black 등 여러 도구를 조합해야 했던 불편함을 해결하고자 했다. 위에 언급한대로, 아스트랄은 &quot;파이썬 툴링은 훨씬 더 빨라질 수 있다&quot; 는 가설을 Ruff 로 (Charlie Marsh) 스스로 검증하며 시작됐다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/88948d35-9b4e-4aed-969e-38a8905e1c57/image.png" alt=""></p>
<p>PEP8 을 따라가기위해 pycodestyle 검사 (사내룰, 구글 스타일 가이드 등) -&gt; flake8 -&gt; isort (import 정렬) -&gt; 포맷팅엔 black 등의 순서로 진행했고, 각 세팅마저 달랐다. 나의 과거글 <a href="https://velog.io/@qlgks1/Python-flake8-Black-%EB%8F%84%EC%9E%85-clean-code-%EC%8B%A4%EC%B2%9C%ED%95%98%EA%B8%B0">python - flake8, Black 도입, pre-commit &amp; clean code-style 실천하기</a> 에서도 정말 간단한 세팅에 노고가 좀 필요했다. </p>
<p>근데 <code>ruff</code> 요놈은 한 번 딸각에 해결한다. 더욱이 <code>uv</code> 랑 같이쓰면 세팅이 매우매우 깔끔하고 편해진다. <code>poetry</code> 쓰는 사람들도 <code>ruff</code> 는 바로 써보면 좋겠다. <code>poetry</code> 랑도 합이 잘 맞는다.</p>
<h4 id="●-왜-빠른가">● 왜 빠른가?</h4>
<p>단순하게 <code>Rust</code> 기반이라서 빠른 것 보다, 핵심적으로 아래 3가지가 꼽힌다. </p>
<ol>
<li>ruff는 파일 단위 병렬처리를 통해 다중 코어를 적극 활용.<ul>
<li>수백 개 파일을 검사할 때 파일별 토큰화, 파싱을 스레드로 병렬 실행한다.</li>
<li>토큰화 과정 등에서 CPU의 SIMD 명령어를 활용하여 문자열 검색 같은 작업을 한 번에 묶어서 처리한다. </li>
<li><a href="https://medium.com/@chimuichimu/ruff-extremely-fast-python-linter-heres-why-2355ee039c34">Ruff: Extremely Fast Python Linter — Here’s Why</a></li>
</ul>
</li>
<li>한번 소스 코드를 파싱하면 그 결과를 기반으로 여러 규칙을 한꺼번에 적용. <ul>
<li>토큰화/구문분석을 단 1회만 수행하고 다양한 체크를 실행한다. 즉, 다중 검사일땐 사실 모두 토큰화가 필요했는데, 이를 뛰어넘을 수 있는 것이다!</li>
</ul>
</li>
<li>저수준 구현에서도 세세한 최적화. <ul>
<li>예를 들어 Python의 BigInt 처리에 대응하기 위해 Rust에서는 작은 정수는 64비트로, 매우 큰 정수만 힙 할당하도록 변경하여 정수 토큰화 성능을 약 8% 개선했고</li>
<li>토큰 위치를 내부적으로 “행-열” 대신 바이트 오프셋으로 저장하여 위치 계산을 빠르게 하는 기법으로 전체 처리 시간을 10% 감소시켰다고 한다. </li>
</ul>
</li>
</ol>
<p>이러한 세심한 튜닝 덕분에 ruff는 Flake8 등의 10~100배 속도로 코드베이스를 토큰화 하고 검사하고 포맷팅할 수 있다!</p>
<h3 id="5-ruff-장단점-기존-것과-비교">5) ruff 장단점, 기존 것과 비교</h3>
<h4 id="●-ruff-장점">● ruff 장점</h4>
<ol>
<li><p>ruff는 하나의 툴로 <strong><em>800개 이상의 린트 규칙(기존 Flake8 플러그인 다수 포함)을</em></strong> 제공하고, 코드 자동포맷팅 기능도 지원한다.</p>
</li>
<li><p>린팅, 포맷팅 속도가 거의 실시간이다. 대규모 코드베이스에서도 ruff를 돌리는 데 걸리는 시간은 블링크 수준이라, 실시간 피드백을 받기에 충분하다. </p>
</li>
<li><p>더 이상 프로젝트에 flake8, black, isort 등을 모두 설치하고 설정할 필요 없이 [tool.ruff] 하나의 설정으로 일관성 있게 관리할 수 있다. </p>
<ul>
<li>특히 ruff의 포매터는 Black과 &gt;99.9% 동일한 결과를 내도록 설계되어, 기존 Black 사용 프로젝트도 쉽게 ruff로 전환할 수 있다. </li>
<li>예를 들어 Django 전체 코드 포맷 비교에서 ruff와 Black의 결과 차이는 극히 일부분(2772개 파일 중 34파일)뿐이었다고 한다. </li>
<li>ruff가 기본적으로 Black의 스타일 가이드를 따르고, 라인 길이 등 기본값도 Black과 동일하게 설정되어 있어서 기존 코드에 최소한의 변경만 주기 때문. </li>
</ul>
</li>
<li><p>ruff는 IDE/에디터 통합과 CI 연동이 잘 되어 있다! (탄생때부터 이걸 노린 듯하다.)</p>
<ul>
<li>VS Code 확장이나 PyCharm 플러그인 등이 이미 나와 있어 빠른 피드백을 얻을 수 있고</li>
<li>GitHub Actions용 액션이나 <code>pre-commit</code> 훅도 공식 지원하여 쉽게 품질 검증 파이프라인에 넣을 수 있다. </li>
</ul>
</li>
<li><p><strong><em>ruff는 지금까지도 빠르게 진화하고 있다.</em></strong> 규칙 커버리지도 계속 늘어나고 있고, 향후 <code>static type checker(ty)</code> 까지 개발 중일 정도로 툴체인 확장을 모색하고 있다.</p>
</li>
</ol>
<h4 id="●-ruff-단점">● ruff 단점</h4>
<ol>
<li><p>ruff는 기본적으로 모든 검사를 내장하고 있어, Flake8처럼 사용자가 간편하게 플러그인을 추가하는 방식은 지원하지 않는다.</p>
<ul>
<li>따라서 혹시 기존 프로젝트에서 매우 특수한 Flake8 플러그인을 쓰고 있었다면, 또는 사내 전용 flake8 룰세팅이 있다면, 직접 개발하지 않는 이상 이를 도입할 수 &quot;없다&quot;.</li>
<li>ruff에 해당 규칙이 구현되길 기다리거나 직접 기여해야 한다. (그래도 인기 있는 검사들은 거의 다 포함되어 있고 부족한 부분도 빠르게 채워지고 있다고 한다.)</li>
</ul>
</li>
<li><p><code>Pylint</code> 처럼 &quot;정적 분석 수준에서 코드의 논리적 버그&quot; 까지 잡아주는 기능은 ruff에는 없다. (ruff는 타입체커가 아니고 주로 스타일, 버그 패턴 위주 검사임). </p>
<ul>
<li>타입 안정성 검사 등을 위해서는 mypy 같은 도구를 별도로 운용해야 할 수도 있다. (이는 앞서 언급한 장점대로, <code>ty</code> 등과 캐미가 어떻게 되냐에 따라 굉장히 달라질 듯 하다.)</li>
<li>그 외에 ruff 역시 Rust로 만들어졌다는 점에서, Python으로 작성된 flake8/pylint보다 사용자가 룰을 커스터마이징하거나 기여하기 어렵다는 의견이 있다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-uv--ruff-설치와-세팅">2. uv &amp; ruff 설치와 세팅</h2>
<blockquote>
<p><strong><code>MacOS</code></strong> 기준</p>
</blockquote>
<h3 id="1-uv-install--init">1) uv install &amp; init</h3>
<pre><code class="language-bash">$ curl -LsSf https://astral.sh/uv/install.sh | sh</code></pre>
<p>다른 공식자료들, 외부 자료들 때문과 <code>PATH</code> 때문이라도 로 brew 로 하지 않았으면 한다.. 
<code>Rust</code>나 <code>Python</code>이 없어도 <code>uv</code> 바이너리를 시스템에 설치해준다.</p>
<pre><code class="language-bash">$ mkdir myproject &amp;&amp; cd myproject
$ uv init .</code></pre>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/22ceda4b-22dd-4e4c-8f11-f282b539de6c/image.png" alt=""></p>
<p>타 써드파티와 매우 유사하다. <code>init</code> 을 하면 <code>pyproject.toml</code> 이 만들어지며 <code>poetry</code> 와 유사하게 <code>[project]</code> 메타데이터 및 의존성 섹션을 활용한다. <code>README.md</code>, <code>.python-version</code> 과 <code>git</code> 까지 세팅해준다. </p>
<pre><code class="language-bash">uv init --lib mylib       # 라이브러리 템플릿
uv init --package mypkg   # 패키징 가능한 프로젝트(기본 빌드 시스템 부여)</code></pre>
<p>재미있는 포인트는, 라이브러리 템플릿, 패키징 가능한 플젝 템플릿도 바로 제공해준다. </p>
<h3 id="2-uv-파이썬-버저닝과-가상환경">2) uv 파이썬 버저닝과 가상환경</h3>
<h4 id="●-python-버전-변경">● python 버전 변경</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/144b8d0a-22d9-4898-87af-f006fddc6107/image.png" alt=""></p>
<p><code>uv python pin 3.13</code> 과 같이 하며, 자동으로 관련 값들을 업데이트 해준다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/49270749-45d3-4874-8624-02ed55af3ec1/image.png" alt=""></p>
<p><code>uv python list</code> 로 내가 지금 설치한 python version 과 path 를 모두 한 눈에 확인할 수 있다. 좋다! 물론 이는 위 사진에서 cpython-3.13.0-... 을 보면 알겠지만 <code>alias</code> 를 포함한 것이다. (정확하게는 심볼릭 링크)</p>
<h4 id="●-가상환경은-어떻게-함">● 가상환경은 어떻게 함?</h4>
<p><strong><em>오피셜하게는 <code>uv venv</code> 이다.</em></strong> 근데 <code>uv run</code> 을 하면 가상환경과 프로젝트 세팅 &amp; 인터프리터 세팅을 바로 할 수 있다. 즉, 뭔가를 지금 플젝에 세팅한대로 <code>python</code> 을 실행할때, 마치 <code>poetry run ...</code> 처럼, <code>uv run ...</code> 을 하면 된다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9b942ed5-f0a5-4c99-aefa-424a0226ee69/image.png" alt=""></p>
<p><code>uv add fastapi</code> 와 같이 패키지를 설치하며, 기존 가상환경이 없으면 <code>add</code> 만으로 바로 가상환경을 만든다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/b9936d1c-de36-4760-90a7-14b71bdfbe3b/image.png" alt=""></p>
<p>결론적으로 &quot;가상환경 activation은 <code>source .venv/bin/activate</code> 와 같이, 개인적으로 나에게 아주아주 익숙하고 편한 방식으로 관리할 수 있다. 진짜 이름마저 찰떡.</p>
<h4 id="●-lock-file-다루기">● lock file 다루기</h4>
<p>만약 패키지매니저가 처음이라면, <code>lock</code> 이 익숙하지 않을 수 있다. 간단하다. &quot;모든 개발자·머신·배포 환경에서 동일한 설치 결과를 재현하기 위해&quot; <code>uv.lock</code> 을 사용한다. 그래서 플랫폼(운영체제/아키텍처/파이썬 버전) 차이까지 고려한 보편(크로스플랫폼)적인 패키지 정보들을 제공한다. </p>
<p><code>uv lock</code> 으로 lock file 을 만들고 기존 lock 과 <code>uv lock --check</code> 로 최신성만 빠르게 검사할 수 있다. <code>uv run --locked</code> 와 <code>uv sync --locked</code> 으로 엄격한 모드로 실행하고 싱크를 맞추게 할 수 있다. </p>
<p>그리고 <code>add</code> 와 <code>pip install</code> 핵심 차이는 아래와 같다. (lock file 갱신 여부 차이)</p>
<pre><code class="language-bash">- `uv add`: pyproject/lockfile **갱신** + .venv 설치
- `uv pip install`: lockfile **미갱신**(실험/임시 설치에 유용)</code></pre>
<h3 id="3-uv-적용하기-not-uv-to-uv--uv-sync">3) uv 적용하기 (not uv to uv || uv sync)</h3>
<h4 id="●-다른-uv-기반-플젝에서-시작하기">● 다른 uv 기반 플젝에서 시작하기</h4>
<p><strong><em><code>uv.lock</code> 이 있다면 <code>uv sync</code> 한 번 으로 가능하다.</em></strong> (사실 이건 <code>poetry</code> 도 그럼). 락파일이 있다면 그 버전 그대로 설치하고, 락파일이 없고 <code>pyproject.toml</code> 만 있다면 <code>uv sync</code> 하기 전에 <code>uv lock</code> 으로 명시적 생성 후 실행하는 것을 추천한다. </p>
<p>그 외 <a href="https://docs.astral.sh/uv/getting-started/features/#features">아스트랄 uv 공식 docs의 Features</a> 에서 확인하길 바란다. <code>uv tree</code>, <code>uv build</code>, <code>uv publish</code> 사실 <code>poetry</code> 랑 거의 동일하다고 느껴진다.</p>
<h4 id="●-pip-또는-타-패키지매니저에서-시작하기">● pip 또는 타 패키지매니저에서 시작하기</h4>
<ol>
<li><p>일단 버저닝을 진짜 100% 완벽하게 하고 싶다면, &quot;준비된 <code>requirements.txt</code>&quot; 로 시작하는게 가장 깔끔하다.</p>
</li>
<li><p>해시와 python 지원 버전이 같이 명시된 require 파일이면 베스트지만, 버전까지만 명시되어도 괜찮다. 어짜피 uv 가 나머지를 판단해준다. <code>uv init</code> 을 하자!</p>
</li>
<li><p>그리고 <code>python version</code> 맞추는게 중요할텐데, <code>uv python pin 3.13</code> 를 하거나 <code>uv init --python 3.13</code> 로 시작한다. 만약 local 에 해당하는 python version이 없다면? <code>uv python install 3.13</code> 를 하면 된다.</p>
</li>
<li><p>이제 <code>uv add -r requirements.txt</code> 로 설치하면 끝이다. 관성적으로 흐름이 정말 기존 tool과 크게 다르지 않다.</p>
</li>
<li><p>또는 기존 txt의 고정 버전을 <strong>제약(constraints)</strong> 으로 적용해 uv.lock에 반영하려면, <code>uv add -r requirements.in -c requirements.txt</code> 를 하자. - <a href="https://docs.astral.sh/uv/guides/migration/pip-to-project/#migrating-from-pip-to-a-uv-project">Migrating from pip to a uv project</a></p>
</li>
<li><p>혹시 모르니 <code>uv lock</code> -&gt; <code>uv sync</code> 로 완벽하게 lock file 도 싱크를 맞춰주자.</p>
</li>
</ol>
<h3 id="4-ruff-install--init">4) ruff install &amp; init</h3>
<p><code>uv</code> 를 쓴다고 가정하고, <code>ruff</code> 를 설치할땐 아래와 같이 한다. &quot;--group&quot; 이 앞서 언급한 <code>PEP 735(dependency groups 표준)</code> 이다. </p>
<pre><code class="language-bash">$ uv add --group dev ruff</code></pre>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/679f35ca-b8a6-4a99-92f9-0cd55664d68a/image.png" alt=""></p>
<p>위 사진과 같이 <code>dependencies.dev</code> 섹션에 추가 된다. <code>poetry add -D ruff</code> 와 동일한 맥락이다. 근데 설치없이 <code>uv</code> 의 <code>uvx</code> 를 통해 바로 실행할 수 있다! (이는 비추천)</p>
<pre><code class="language-bash">uvx ruff check
uvx ruff format</code></pre>
<h4 id="●-기본-사용법">● 기본 사용법</h4>
<p>린팅</p>
<pre><code class="language-bash">ruff check .                 # 재귀적으로 모든 파이썬 파일 검사
ruff check path/to/file.py   # 특정 파일
ruff check . --fix           # 자동 수정 가능한 항목만 적용
ruff check --unsafe-fixes        # 비적용이지만 제안 표시
ruff check --fix --unsafe-fixes  # 실제 적용</code></pre>
<p>포맷팅</p>
<pre><code class="language-bash">ruff format                  # 코드 포맷
ruff format --check --diff   # CI에서 포맷 불일치 검출</code></pre>
<h4 id="●-기존것들이랑-섞어서-쓸-수-있음">● 기존것들이랑 섞어서 쓸 수 있음?</h4>
<p><a href="https://daftdev.blog/2024/03/25/migrating-to-ruff-from-black-and-flake8">Migrating to ruff from black and flake8</a> 글과 같이 <strong><em>ruff의 목표는 Black, Flake8, isort 등을 모두 대체하는 것</em></strong> 이므로 굳이 기존 린터/포매터를 함께 쓸 필요는 없다.</p>
<ol>
<li><p>Black 대체: 만약 기존에 Black으로 포맷팅해오던 프로젝트라면, <code>ruff</code> 설치 후 <code>ruff check --diff</code> 실행과 <code>black</code> 실행의 차이점 파악. 상호 세팅 차이가 있는 것을 업데이트 하고 (포메팅 규칙) <code>black</code> 을 삭제하면 된다.</p>
</li>
<li><p>Flake8/기타 린터 대체: 맥락은 거의 위와 동일하다. Flake8 설정(.flake8이나 setup.cfg의 [flake8] 섹션 등, 와 이거 진짜 개인적으로 싫다.)</p>
</li>
<li><p>그 외는 아래 표를 살펴보자!</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>도구 / 플러그인</th>
<th>ruff로 대체 가능?</th>
<th>ruff 규칙 접두사</th>
<th>참고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Flake8</strong></td>
<td>예</td>
<td>F (Pyflakes), E/W (pycodestyle)</td>
<td>대부분의 조건에서 완벽하게 대체 가능합니다.</td>
</tr>
<tr>
<td><strong>Black</strong></td>
<td>예 (포맷터)</td>
<td>ruff format</td>
<td>99.9% 이상 호환되지만 일부 의도된 차이점이 있습니다.</td>
</tr>
<tr>
<td><strong>isort</strong></td>
<td>예</td>
<td>I</td>
<td>임포트 정렬 기능이 내장되어 있습니다.</td>
</tr>
<tr>
<td><strong>pyupgrade</strong></td>
<td>예</td>
<td>UP</td>
<td>최신 파이썬 문법을 제안합니다.</td>
</tr>
<tr>
<td><strong>flake8-bugbear</strong></td>
<td>예</td>
<td>B</td>
<td>잠재적인 버그와 설계 문제를 찾아냅니다.</td>
</tr>
<tr>
<td><strong>flake8-comprehensions</strong></td>
<td>예</td>
<td>C4</td>
<td>더 관용적인 컴프리헨션 작성을 돕습니다.</td>
</tr>
<tr>
<td><strong>pydocstyle</strong></td>
<td>예</td>
<td>D</td>
<td>독스트링 컨벤션을 강제합니다.</td>
</tr>
<tr>
<td><strong>flake8-bandit</strong></td>
<td>예</td>
<td>S</td>
<td>일반적인 보안 문제를 확인합니다.</td>
</tr>
<tr>
<td><strong>Pylint</strong></td>
<td>부분적으로</td>
<td>PL</td>
<td>ruff는 완벽한 대체재가 아니며, 규칙 집합이 다릅니다.</td>
</tr>
<tr>
<td><strong>사용자 정의 플러그인</strong></td>
<td>아니요</td>
<td>해당 없음</td>
<td>ruff는 서드파티 플러그인을 지원하지 않으며, 규칙은 핵심 프로젝트에 기여해야 합니다.</td>
</tr>
</tbody></table>
<h3 id="5-uv--ruff-default-conf-with-pre-commit-hook">5) uv + ruff default conf (with pre-commit hook)</h3>
<blockquote>
<p>pre-commit hook 이 뭔가요?! 한다면 -&gt; <strong><em><a href="https://velog.io/@qlgks1/Python-flake8-Black-%EB%8F%84%EC%9E%85-clean-code-%EC%8B%A4%EC%B2%9C%ED%95%98%EA%B8%B0">python - flake8, Black 도입, pre-commit &amp; clean code-style 실천하기</a></em></strong> 글을 참조해 주세요!</p>
</blockquote>
<h4 id="●-toml-에-세팅하기">● <code>.toml</code> 에 세팅하기</h4>
<pre><code class="language-toml"># ================================================================
# Ruff (Linter &amp; Formatter) Settings
# ================================================================
[tool.ruff]
# 수정에서 제외할 파일 및 디렉토리 목록
exclude = [
    &quot;.bzr&quot;,
    &quot;.direnv&quot;,
    &quot;.eggs&quot;,
    &quot;.git&quot;,
    &quot;.hg&quot;,
    &quot;.mypy_cache&quot;,
    &quot;.nox&quot;,
    &quot;.pants.d&quot;,
    &quot;.pytype&quot;,
    &quot;.ruff_cache&quot;,
    &quot;.svn&quot;,
    &quot;.tox&quot;,
    &quot;.venv&quot;,
    &quot;__pypackages__&quot;,
    &quot;_build&quot;,
    &quot;buck-out&quot;,
    &quot;build&quot;,
    &quot;dist&quot;,
    &quot;node_modules&quot;,
    &quot;venv&quot;,
    &quot;*/migrations/*.py&quot;,
]
# 한 줄의 최대 글자 수
line-length = 120

# --- Linter (코드 분석기) 설정 ---
[tool.ruff.lint]
# 활성화할 규칙 선택:
# E, W: pycodestyle (에러, 경고)
# F: Pyflakes (논리적 오류)
# I: isort (import 정렬)
select = [&quot;E&quot;, &quot;F&quot;, &quot;W&quot;, &quot;I&quot;]
ignore = []

# 모든 수정 가능한 규칙을 자동으로 고치도록 설정
fixable = [&quot;ALL&quot;]
unfixable = []

# --- Formatter (코드 포맷터) 설정 ---
[tool.ruff.format]
# Black과 유사한 포맷팅 스타일을 따릅니다.
quote-style = &quot;double&quot;
indent-style = &quot;space&quot;
skip-magic-trailing-comma = false
line-ending = &quot;auto&quot;

# --- 플러그인별 상세 설정 ---
[tool.ruff.lint.isort]
# 프로젝트에서 사용하는 서드파티 라이브러리 목록
known-third-party = [&quot;django&quot;, &quot;rest_framework&quot;, &quot;graphene_django&quot;]

# --- 파일/디렉토리별 규칙 무시 설정 ---
[tool.ruff.lint.per-file-ignores]
# settings 파일: 와일드카드 import 허용
&quot;config/settings/*&quot; = [&quot;F403&quot;, &quot;F405&quot;]

# __init__.py 파일: 하위 모듈 노출을 위한 미사용/와일드카드 import 허용
&quot;**/__init__.py&quot; = [&quot;F401&quot;, &quot;F403&quot;]

# 테스트 파일: 가독성을 위해 긴 줄 허용
&quot;**/test_*.py&quot; = [&quot;E501&quot;]

# 긴 문자열이 많은 파일들, 줄 길이(E501) 등 규칙 무시
&quot;utils/(모자이크)/*&quot; = [&quot;E501&quot;, &quot;W291&quot;, &quot;W293&quot;]
&quot;batch/*&quot; = [&quot;E501&quot;, &quot;W291&quot;, &quot;W293&quot;]
&quot;artifacts/*&quot; = [&quot;E501&quot;, &quot;W291&quot;, &quot;W293&quot;]

# 기타 특정 파일들의 줄 길이(E501) 규칙 무시
&quot;utils/(모자이크).py&quot; = [&quot;E501&quot;]
&quot;config/(모자이크)/(모자이크).py&quot; = [&quot;E501&quot;]
&quot;config/(모자이크).py&quot; = [&quot;E501&quot;]
&quot;apps/(모자이크).py&quot; = [&quot;E501&quot;, &quot;W291&quot;]
</code></pre>
<p>위는 실제 내가 프로젝트에 적용해서 사용하는 <code>ruff</code> 세팅이다. 통으로 수정을 제외하는 디렉토리, 한 줄 최대 글자 수, 활성화 린팅 그룹 세팅과 포맷팅 세팅, 자체 플러그인, 자체 써드파티, 특정 파일의 특정 규칙만 무시하기! </p>
<p><code>[tool.ruff.lint]</code> 에서 <strong><code>I</code></strong> 를 추가했기에 <code>import</code> 역시 <code>isort</code> 대로 린팅&amp;포맷팅을 한다. </p>
<h4 id="●-uv-pre-commit">● uv pre-commit</h4>
<p><a href="https://github.com/astral-sh/uv-pre-commit">https://github.com/astral-sh/uv-pre-commit</a> 에서 uv 관련된 pre-commit 들을 볼 수 있다. 특히 배포환경에서는 uv 등의 패키지매니저를 굳이 사용하지 않을때 중요한건 <code>requirements.txt</code> 을 <code>lock</code> file 에 준하게 strict 한 버전으로 만들어야 한다는 것! 그래서 아래와 같은 pre-commit 을 사용할 수 있다!</p>
<pre><code class="language-yaml">- repo: https://github.com/astral-sh/uv-pre-commit
  # uv version.
  rev: 0.8.22
  hooks:
    - id: uv-export</code></pre>
<p><code>uv export --format requirements.txt --all-groups --output-file requirements.txt</code> 로 dev 그룹을 포함한, dev를 제외하려면 <code>--no-dev</code>, lock file 기준의 requirements file을 얻을 수 있다. </p>
<p><code>uv export --format requirements-txt --all-groups &gt; requirements.txt</code> 로도 가능하다.</p>
<h4 id="●-ruff-pre-commit">● ruff pre-commit</h4>
<p>이 정도 pre-commit 은 세팅하자. </p>
<pre><code class="language-yaml">repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: &quot;v0.13.2&quot;  # 사용하고자 하는 ruff 버전 태그
  hooks:
    - id: ruff-check    # 린트 검사 (--fix 없을 경우 검사만)
      args: [--fix]      # 자동수정 활성화 - 권장
    - id: ruff-format   # 포매팅 (Black 대체)</code></pre>
<h4 id="●-cicd-github-actions-에서-workflow">● CI/CD, github actions 에서 workflow</h4>
<pre><code class="language-yaml">- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3</code></pre>
<p>CI/CD 파이프라인에 추가할때 위와 같이 넣으면 된다!</p>
<h4 id="●-vscode-에서도-쓰려면">● <code>.vscode</code> 에서도 쓰려면</h4>
<p><code>.vscode/settings.json</code> 에 아래 설정값을 추가해보자!</p>
<pre><code class="language-json">{
  &quot;[python]&quot;: {
    // 1. 기본 포맷터를 &#39;ruff&#39;로 지정합니다.
    &quot;editor.defaultFormatter&quot;: &quot;charliermarsh.ruff&quot;,

    // 2. 파일을 저장할 때마다 자동으로 포맷팅을 실행합니다.
    &quot;editor.formatOnSave&quot;: true,

    // 3. 저장 시 추가적인 코드 액션을 실행합니다.
    &quot;editor.codeActionsOnSave&quot;: {
      // 3a. 수정 가능한 모든 린트 오류를 자동으로 수정합니다.
      &quot;source.fixAll.ruff&quot;: &quot;always&quot;,
      // 3b. isort 규칙에 따라 임포트 구문을 자동으로 정렬합니다.
      &quot;source.organizeImports.ruff&quot;: &quot;always&quot;
    }
  },

  // 4. ruff가 프로젝트의 가상 환경을 올바르게 인식하도록 인터프리터 경로를 지정합니다.
  //    이렇게 하면 ruff가 설치된 패키지를 정확히 인지하고 &#39;import&#39; 관련 오류를 줄일 수 있습니다.
  &quot;ruff.interpreter&quot;: [&quot;${workspaceFolder}/.venv/bin/python&quot;],

  // 5. (선택 사항) ruff 실행 시 추가 인자를 전달할 수 있습니다.
  //    예: 특정 설정 파일을 강제하거나, preview 기능을 활성화할 때 유용합니다.
  &quot;ruff.lint.args&quot;: [&quot;--config=pyproject.toml&quot;, &quot;--preview&quot;],
  &quot;ruff.format.args&quot;: [&quot;--config=pyproject.toml&quot;, &quot;--preview&quot;]
}</code></pre>
<hr>
<h2 id="3-바쁜분들을-위한-올인원-세팅-커멘드">3. 바쁜분들을 위한 올인원 세팅 커멘드</h2>
<ol>
<li>uv 이니셜라이징하고, ruff 랑 pre-commit 인스톨 후 lock 파일 싱크맞추기</li>
</ol>
<pre><code class="language-shell">uv init .
uv add --group dev ruff pre-commit
uv lock
uv sync --all-groups

# requirements.txt 만들기
uv export --format requirements-txt --all-groups &gt; requirements.txt</code></pre>
<ol start="2">
<li>ruff 린팅 포맷팅 세팅과 러닝</li>
</ol>
<pre><code class="language-.toml"># .toml 에 최소한의 ruff 세팅
[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = [&quot;E&quot;, &quot;F&quot;, &quot;I&quot;]
ignore = []

[tool.ruff.format]
# Black 호환 기본값 권장. 필요 시만 조정.</code></pre>
<pre><code class="language-bash">uv run ruff check  # 린팅

# 자동 수정, 포맷팅
uv run ruff check . --fix
uv run ruff check . --fix --unsafe-fixes   # 위험도가 높은 자동수정까지 허용</code></pre>
<ol start="3">
<li>빠르게 <code>pre-commit</code> 에 등록해서 사용하기</li>
</ol>
<pre><code class="language-yaml"># .pre-commit-config.yaml (예: ruff 전용 리포지토리 사용)
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: &quot;v0.13.2&quot;
  hooks:
    - id: ruff-check
      args: [--fix]
    - id: ruff-format</code></pre>
<pre><code class="language-bash">uv run pre-commit run --all-files</code></pre>
<ol start="4">
<li>github ci/cd 퀵 구성 (workflow)</li>
</ol>
<pre><code class="language-yaml">name: ci
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # uv 설치 + 캐시
      - uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true  # 선택사항이지만 명시 권장

      # 프로젝트 환경을 락 기준으로 정확히 동기화 (dev 포함)
      - run: uv sync --locked --all-groups

      # 포맷 확인(자동 수정 금지). 방금 sync 했으므로 run은 no-sync로 빠르게
      - run: uv run --no-sync ruff format . --check

      # 린트(gh 어노테이션). 마찬가지로 no-sync
      - run: uv run --no-sync ruff check . --output-format=github</code></pre>
<p>PS. github CI/CD 에서 testing 때문에 uv 를 사용하지, 진짜 ruff만 쓸꺼면 uv 필요 없다!
PS. <code>.vscode/settings.json</code> 를 쓰는 것은 비추천한다. 무지성 날코드 후 오토세이브로 구원받는 습관을 들이게 된다. <del>내 얘기가 맞다.</del></p>
<hr>
<h2 id="출처">출처</h2>
<ol>
<li><a href="docs.astral.sh">uv 공식 문서 – Astral 제공 (uv 소개 및 사용법)</a></li>
<li><a href="astral.sh">Astral 블로그: uv (2024) – Charlie Marsh, uv 탄생 배경 및 벤치마크</a></li>
<li><a href="https://www.loopwerk.io/articles/2024/python-poetry-vs-uv">Poetry vs uv – Kevin Renskers, uv와 Poetry 비교 후기</a></li>
<li><a href="https://medium.com/@chimuichimu/ruff-extremely-fast-python-linter-heres-why-2355ee039c34">“Ruff: Extremely Fast Python Linter — Here’s Why”, ruff 성능 기법 분석</a></li>
<li><a href="https://daftdev.blog/2024/03/25/migrating-to-ruff-from-black-and-flake8">“Migrating to ruff from black and flake8”, ruff 마이그레이션 실례</a></li>
<li><a href="https://zaloog.github.io/2025/01/19/uv.html">“uv ill like uv” (2025), uv 최신 기능과 통계 정리</a></li>
<li><a href="https://www.awesomepython.org/">Python GitHub 저장소 – python-poetry/poetry (스타 수 등 프로젝트 현황)</a></li>
<li><a href="https://github.com/astral-sh">아스트랄 공식 깃헙 레포</a></li>
<li><a href="https://velog.io/@qlgks1/Python-flake8-Black-%EB%8F%84%EC%9E%85-clean-code-%EC%8B%A4%EC%B2%9C%ED%95%98%EA%B8%B0">python - flake8, Black 도입, pre-commit &amp; clean code-style 실천하기</a></li>
<li><a href="https://peps.python.org/">Python Enhancement Proposal</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[켄트 벡(Kent Beck) 형님과 함께하는 Augmented Coding, "증강 코딩" 잘해보기]]></title>
            <link>https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding</link>
            <guid>https://velog.io/@qlgks1/Kent-Beck-Augmented-Coding</guid>
            <pubDate>Mon, 15 Sep 2025 17:53:54 GMT</pubDate>
            <description><![CDATA[<h1 id="augmented-coding">Augmented Coding</h1>
<blockquote>
<p>바이브 코딩과 증강 코딩은 다르다. <a href="https://www.linkedin.com/posts/midov_we-used-ai-to-code-2x-faster-then-spent-activity-7342619721445376001-Tv2G">We used AI to code 2x faster. Then spent 3x more time fixing bugs.</a> 여전히 뜨거운 감자인 바이브 코딩, 그리고 이제 사이 중간 어느메에서 &quot;AI와 협업&quot; 으로서 &quot;잘&quot; 사용하려는 움직임이 많이 있다. 지금의 나에게는 &quot;Augmented Coding&quot; 이 정답에 가깝게 느껴진다. <del>언젠간 오답이 될 수 있지만</del></p>
</blockquote>
<h2 id="1-kent-becks-augmented-coding">1. Kent Beck&#39;s &quot;Augmented Coding&quot;</h2>
<blockquote>
<p>Farmers in olden times had a saying, “Don’t eat the seed corn.” Better to go hungry in the spring, plant the corn, &amp; eat later. My coding genie, unfortunately, doesn’t know this saying.</p>
</blockquote>
<blockquote>
<p>농부가 다음 시즌 파종을 위해 남겨둬야 할 씨앗까지 먹어버리면 미래 수확을 포기하게 된다. Kent Beck 은 &quot;Vibe Coding&quot; 의 가장 큰 폐해는 <strong>*<span style="color: rgb(99 148 255);">&quot;지금 편하자고 코드 구조를 망가뜨리면 미래의 개발 옵션(Optionality)을 잃게 되는 것&quot;</span>*</strong> 이라고 지적한다. </p>
</blockquote>
<p><strong><em><code>Features</code></em></strong> 는 새로운 테스트를 작성하고 실행되게 하는 것과 같은 &quot;기능 구현&quot; 을 의미하고, 이 과정에서 결합도가 올라가고 디자인이 저하되기도 한다. </p>
<p><strong><em><code>Options</code></em></strong> 는 구조를 개선하고 결합도를 줄이고, 응집도를 높여 &quot;optionality&quot; 를 높이는 것을 의미한다. Kent Beck 은 아래 그림과 같이, 들숨&amp;날숨으로 기능 개발하고, 구조 개선하는 것을 비유한다. 그리고 아래 그림의 우측과 같이 AI는 한 호흡에 우다다 해버린다. (Genie Just Inhales)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fad2cebe-57e1-46d2-86bc-f2efb9ecf4d6/image.png" alt=""></p>
<h3 id="1-vibe-coding-vs-augmented-coding">1) <em>Vibe Coding</em> VS <em>Augmented Coding</em></h3>
<ol>
<li><p>바이브 코딩 (<code>Vibe Coding</code>): 최종적으로 시스템이 어떻게 동작하는지에만 관심을 두는 방식. 코드의 품질이나 구조보다는 &quot;일단 돌아가게만 해줘&quot;라는 식의 접근법에 가깝다. 에러가 발생하면 다시 AI에게 에러 메시지를 주고 해결하고의 사이클 반복한다.</p>
</li>
<li><p>증강 코딩 (<code>Augmented Coding</code>): <strong>작동하는 깔끔한 코드(Tidy Code That Works)</strong> 를 목표로 한다. 코드의 품질, 복잡도, 테스트 커버리지 등을 중요하게 생각하며, 개발자가 주도권을 가지고 AI를 활용하는 방식. 과거와 같이 좋은 코드를 추구하되, AI의 도움으로 타이핑의 양을 줄이고 생산성을 높이는 것.</p>
</li>
</ol>
<p>켄트 벡은 바이브 코딩과 증강 코딩의 차이를 위와 같이 정의한다. 그리고 <strong><code>B+ 트리</code></strong> 프로젝트를 &quot;Augmented Coding&quot; 방식으로 한 것에 대해 설명한다. 그가 프로젝트를 리뷰한 내용에서 많은 인사이트를 얻을 수 있다.</p>
<h3 id="2-ai가-길을-잃고-있다는-3가지-신호">2) AI가 길을 잃고 있다는 3가지 신호</h3>
<p>켄트 벡은 AI와 협업할 때, AI가 잘못된 방향으로 가고 있음을 알려주는 3가지 신호를 경계해야 한다고 말한다.</p>
<ol>
<li><p><strong><em><code>Loops</code></em></strong>: AI가 비슷한 코드를 계속 생성하거나, 해결되지 않는 문제에 갇혀 무한 루프처럼 행동하는 경우.</p>
</li>
<li><p><strong><em><code>Functionality I hadn&#39;t asked for</code></em></strong> (even if it was a reasonable next step): 요청하지 않은 기능을 구현할 때. 논리적인 다음 단계로 보여도, 명시적으로 요청하지 않은 기능을 AI가 스스로 구현하기 시작하면 주의해야 한다!</p>
</li>
<li><p><strong><em><code>Any indication that the genie was cheating, for example by disabling or deleting tests</code></em></strong>: 테스트를 삭제하거나 비활성화하는 등, 문제 해결을 위해 &quot;cheating&quot; 할 때 주의해야 한다!</p>
</li>
</ol>
<p>이러한 신호가 보일 때 개발자가 적극적으로 개입하여 방향을 바로잡아주는 것이 &#39;증강 코딩&#39;의 핵심이라고 볼 수 있다. </p>
<h3 id="3-증강-코딩의-핵심-원칙">3) 증강 코딩의 핵심 원칙</h3>
<p>(원문뿐 아니라, Kent Beck 의 다른글과 해외 관련 글을 참고했다.) </p>
<ol>
<li><p><strong><em><code>Constrain Context (맥락 한정하기)</code></em></strong>: AI에게는 당장 다음 단계에 필요한 정보만 제공하기. 요구 사항이나 코드베이스의 일부 등 꼭 필요한 맥락만 주어 AI가 한 번에 너무 많은 것을 고려하지 않게 하기. 결국 context window 를 생각해서라도 이게 꽤 중요하다고 생각한다. 하지만 아이러니하게도 이 &#39;context sizing&#39;을 잘하려면, 기존 코드베이스가 이미 &#39;낮은 결합도와 높은 응집도&#39;를 가져야 한다고 생각한다.</p>
</li>
<li><p><strong><em><code>Preserve Optionality (옵셔널리티 유지하기)</code></em></strong>: &quot;씨앗 옥수수&quot; 비유처럼, AI의 나쁜 설계 제안을 그대로 받아들이지 않고 미래의 선택지를 남겨두기. 구조를 해치는 코드나 과도한 결합도 상승을 경계하여, 향후 변경과 확장이 쉽도록 설계상의 여지를 유지해야 한다. 이런 핀트가 보이면 개발자가 적극적으로 개입해야 한다!</p>
</li>
<li><p><strong><em><code>Balance Expansion &amp; Contraction (확장과 수축의 균형)</code></em></strong>: 기능 추가(확장)와 리팩터링(수축)을 번갈아가며 조화시키기! 새로운 기능을 추가하여 복잡성이 늘어났다면, 곧바로 리팩터링을 통해 복잡성을 줄이는 주기를 유지하기. </p>
</li>
<li><p><strong><em><code>Maintain Human Judgment (인간의 판단 유지)</code></em></strong>: 최종 결정은 인간이 내리는 원칙. AI가 생성한 코드나 제안을 주기적으로 <strong>&quot;검토(review)&quot;</strong> 하고, 아키텍처나 설계상의 중요한 결정은 개발자가 가이드해야 한다는 것이다. </p>
</li>
</ol>
<p>이를 바탕으로 켄트 벡은 &quot;AI와 페어프로그래밍&quot; 을 위해 &quot;AI와 함께하는 개발 워크플로우&quot; 를 소개했다. 그 팁들을 좀 더 살펴보자!</p>
<hr>
<h2 id="2-ai와-페어프로그래밍-증강-코딩-팁">2. AI와 페어프로그래밍, &quot;증강 코딩&quot; 팁</h2>
<h3 id="1-tdd와-tidy-code를-위한-시스템-프롬프트">1) TDD와 Tidy code를 위한 시스템 프롬프트</h3>
<p>켄트 벡은 AI가 TDD 사이클(Red-Green-Refactor)을 잘 따르도록 유도하는 시스템 프롬프트를 공유했다. - <a href="https://github.com/KentBeck/BPlusTree3/blob/ca80e4d85a99cd0af2effe717f709d43e80403bc/rust/docs/CLAUDE.md">https://github.com/KentBeck/BPlusTree3/blob/ca80e4d85a99cd0af2effe717f709d43e80403bc/rust/docs/CLAUDE.md</a> / (프롬프트 덩어리들 양이 진짜 살벌하다 ㅋㅎㅋㅎ)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fdf1d456-457e-41a8-a958-ba3d3fb06e38/image.png" alt=""></p>
<blockquote>
<p>이를 한글로 번역하자면, 아래와 같다. <strong><em>켄트 벡 답게 TDD 와 Tidy First 를 아주 강조한 시스템 프롬프트</em></strong> 다. 즉, <code>테스트 주도 개발(TDD)</code> 사이클을 녹여내고, <code>Tidy First 원칙 (구조 개선을 우선하는 원칙)</code> 을 적용하여 기능 추가와 코드 정돈을 엄격히 분리하고 있다. </p>
</blockquote>
<pre><code>항상 plan.md의 지시사항을 따른다. 내가 &quot;go&quot;라고 하면, plan.md에서 표시되지 않은 다음 테스트를 찾고 그 테스트를 구현한 뒤, 그 테스트를 통과시키는 데 필요한 최소한의 코드만 구현한다.

# 역할과 전문성

당신은 켄트 벡(Kent Beck)의 테스트 주도 개발(TDD)과 Tidy First 원칙을 따르는 시니어 소프트웨어 엔지니어다. 당신의 목적은 이러한 방법론을 정확히 준수하도록 개발을 이끄는 것이다.

# 핵심 개발 원칙

- 항상 TDD 사이클(레드 → 그린 → 리팩터)을 따른다.
- 가장 단순한 실패하는 테스트부터 작성한다.
- 테스트를 통과시키는 데 필요한 최소한의 코드만 구현한다.
- 테스트가 모두 통과한 이후에만 리팩터링한다.
- 캔트 벡의 “Tidy First” 접근을 따라 구조적 변경과 행위 변경을 분리한다.
- 개발 전 과정에서 높은 코드 품질을 유지한다.

# TDD 방법론 가이드

- 기능을 작은 증가분으로 정의하는 실패하는 테스트부터 시작한다.
- 행위를 설명하는 의미 있는 테스트 이름을 사용한다(예: `shouldSumTwoPositiveNumbers`).
- 테스트 실패는 명확하고 정보가 풍부하도록 만든다.
- 테스트를 통과시키는 데 충분한 코드만 작성한다—그 이상은 하지 않는다.
- 모든 테스트가 통과하면, 리팩터링 필요성을 검토한다.
- 새로운 기능에 대해 이 사이클을 반복한다.
- 결함을 수정할 때는, 먼저 API 레벨의 실패하는 테스트를 작성하고, 문제를 재현하는 가능한 가장 작은 테스트를 추가한 다음, 두 테스트 모두 통과시키도록 구현한다.

# Tidy First 접근

- 모든 변경을 두 가지 유형으로 명확히 구분한다:
  1. **구조적 변경(Structural Changes)**: 행위를 바꾸지 않는 코드 재배치(이름 변경, 메서드 추출, 코드 이동 등)
  2. **행위 변경(Behavioral Changes)**: 실제 기능을 추가하거나 수정하는 변경
- 같은 커밋에 구조적 변경과 행위 변경을 절대 섞지 않는다.
- 둘 다 필요하다면 항상 구조적 변경을 먼저 수행한다.
- 구조적 변경 전후로 테스트를 실행해 행위가 바뀌지 않았음을 검증한다.

# 커밋 규율

- 다음 조건을 모두 만족할 때만 커밋한다:
  1. **모든** 테스트가 통과한다.
  2. **모든** 컴파일러/린터 경고가 해소되었다.
  3. 변경이 단일한 논리 단위를 이룬다.
  4. 커밋 메시지에 구조적 변경인지, 행위 변경인지 명확히 표기한다.
- 크고 드문 커밋보다 작고 빈번한 커밋을 선호한다.

# 코드 품질 기준

- 중복을 가차 없이 제거한다.
- 이름과 구조로 의도를 명확히 표현한다.
- 의존성을 명시적으로 만든다.
- 메서드는 작게 유지하고 단일 책임에 집중한다.
- 상태와 부작용을 최소화한다.
- “작동할 수 있는 가장 단순한 해법”을 사용한다.

# 리팩터링 가이드라인

- 리팩터링은 테스트가 통과(그린 단계)할 때만 수행한다.
- 표준 리팩터링 패턴을 올바른 명칭과 함께 사용한다.
- 한 번에 하나의 리팩터링만 적용한다.
- 각 리팩터링 단계 후에 테스트를 실행한다.
- 중복을 제거하거나 가독성을 높이는 리팩터링을 우선한다.

# 예시 워크플로우

새로운 기능을 구현할 때:

1. 기능의 작은 부분을 위한 단순한 실패하는 테스트를 작성한다.
2. 테스트를 통과시키는 데 필요한 최소한만 구현한다.
3. 테스트를 실행해 통과(그린)함을 확인한다.
4. 필요한 구조적 정리를 수행한다(Tidy First). 각 변경 후 테스트를 실행한다.
5. 구조적 변경을 별도의 커밋으로 기록한다.
6. 다음 작은 증가분을 위한 또 다른 테스트를 추가한다.
7. 기능이 완성될 때까지 이를 반복하되, 행위 변경과 구조적 변경의 커밋을 분리한다.

이 과정을 한 치의 오차도 없이 따르며, 빠른 구현보다 깔끔하고 잘 테스트된 코드를 항상 우선한다.

항상 한 번에 하나의 테스트를 작성하고, 그 테스트를 실행 가능하게 만든 다음, 구조를 개선한다. 매번 모든 테스트(장시간 테스트는 제외)를 실행한다.</code></pre><h3 id="2-그래서-우리의-실전은">2) 그래서 우리의 실전은?</h3>
<p><del>여기서부터는 사견이 많이 담겨 있다.</del> </p>
<p>바로 결론을 보자면, 나에게는 크게 2가지다. <strong>*<span style="color: rgb(99 148 255);">(1) AI markdown 과 시스템 프롬프트, (2) 단계별 진행</span>*</strong> 이 개괄적으로 증강 코딩을 위해 매우 중요한 것 같았다. 여기선 하나 하나를 다 뜯어보기보단 개괄적으로 내가 접근하는 방법을 적어보고자 한다. </p>
<h4 id="1-ai-markdown">(1) AI markdown</h4>
<p>요즘엔 &quot;AI markdown 문서&quot; 라고 해서, <code>AGENT.md</code>, <code>CLAUDE.md</code>, <code>GEMINI.md</code>, <code>MODEL_CARD.md</code> ... 등 종류가 정말 많다. 그나마 널리 인식되는게 이 3가지 정도? (그 외 아주 춘추전국 시대다.) </p>
<ol>
<li><p><code>AGENT.md</code> 적극적으로 활용하자. </p>
<ul>
<li><a href="https://news.hada.io/topic?id=22635">https://news.hada.io/topic?id=22635</a></li>
<li><a href="https://agents.md/">https://agents.md/</a></li>
<li>저장소 루트에 두고 프로젝트의 전체적이고 개괄적인 내용이 들어가면 좋다. 위 링크들 참조</li>
</ul>
</li>
<li><p><code>CLAUDE.md</code>, <code>GEMINI.md</code> 등과 같은 LLM 전용 markdown 적극적으로 활용하기</p>
<ul>
<li><p><strong><em>이게 정말 정말 중요하다.</em></strong> 켄트 벡의 시스템 프롬프트를 그대로 활용해도 괜찮으니 꼭 전용 markdown 과 LLM CLI (gemini, claude ... cli) 를 사용해봤으면 한다. </p>
</li>
<li><p><a href="https://yozm.wishket.com/magazine/detail/3339/">https://yozm.wishket.com/magazine/detail/3339/</a> 에서와 같이 &quot;계층적 구조&quot; 도 정말 효과적이다. </p>
</li>
<li><p><a href="https://www.anthropic.com/engineering/claude-code-best-practices">Claude Code: Best practices for agentic coding</a> 글에서 소개하는 가이드와 <a href="https://docs.anthropic.com/en/docs/claude-code/common-workflows">common-workflows</a> 에서 다루는 workflow 를 꼭 참조했으면 한다. </p>
</li>
<li><p>여기서 &quot;시스템 프롬프트 디테일의 차이&quot; 가 나는데, <code>Vibe Coding</code> 이 되어버리거나 <code>Augmented Coding</code> 로 리드하느냐의 &#39;한 끗&#39; 차이가 여기서 나온다.</p>
</li>
</ul>
</li>
<li><p>LLM CLI 를 안써도, <strong><em>Claude 에서 github repo 로 바로 연결하거나, gemini 에서 source code repo 를 바로 연결해서 사용하더라도,</em></strong> 이 AI markdown 지침서는 꼭 repo 에 포함시켜 보자. 이 경우에도 굉장히 많은 도움이 된다. (다만 context를 더 주의해야 한다.)</p>
</li>
<li><p>가능하다면, 영어로 작성하자. 토큰의 양으로 보나 LLM 의 이해도와 물리적 퍼포먼스(학습 원천 데이터에 따른 중간 단계들의 생략) 관점에서 영어는 언제나 AI에게 유리하다.</p>
</li>
</ol>
<p><em>PS) <code>README.md</code> 파일을 AI를 위한 제물처럼 쓰는 경우가 있는데, <code>README.md</code> 는 인간을 위해 좀 남겨두었으면 한다..</em></p>
<h4 id="2-단계별-진행">(2) 단계별 진행</h4>
<p>켄트 벡은 <code>plan.md</code> 라는 step by step 지침서를 매번 만들어서 code cycle 을 돌리는 것 같다. 이게 <code>Vibe Coding</code> 과 가장 물리적인 차이가 아닐까? 이 <code>plan.md</code> 가 곧 TODO Lists &amp; Checklists 이기도 하고, <a href="https://www.reddit.com/r/ClaudeAI/comments/1mpeefp/my_claude_code_tips_for_newer_users/">이 TODO를 모아서 <code>backlog.md</code> 로 만들어서 관리하기도 한다고 한다.</a></p>
<p>여기서 위에서 언급한 <code>3) 증강 코딩의 핵심 원칙</code> 이 중요하다고 생각한다. 켄트 벡이 언급한 바와 같이 <code>plan.md</code> 등을 활용해 <code>Features</code> -&gt; <code>Options</code> 로 진행하고, 한 cycle 의 주도권을 잡고 있어야 한다고 생각한다.</p>
<p>좀 더 구체적으로는 <code>plan.md</code> 로 <strong><em>Red → Green → Refactor</em></strong> 사이클을 한 스텝씩 진행하고, 행위 변경과 구조 변경을 &quot;따로 진행&quot; 하며 커밋 단위에서 절대 섞지 않는 것이다. </p>
<p>그리고 이 &quot;단계별 진행&quot; 은 위에서 언급한 <code>2) AI가 길을 잃고 있다는 3가지 신호</code> 에서 &quot;즉시 개입&quot; 해서 제어해야 한다. (그래야 돈도 아낀다..)</p>
<p>근데 처음엔 <code>Stop</code> 을 한 뒤에 context 를 이어가는 게 굉장히 난해했다. <strong><em>그때 계층적 구조, 계층적 AI markdown 들이 도움이 되었고, 그 계층적 구조는 다시 context window 의 sizing 에서도 유리했다.</em></strong> </p>
<hr>
<h2 id="3-앞으로-코딩은">3. 앞으로 코딩은?</h2>
<p>켄트 벡은 AI와 함께하는 프로그래밍이 기존의 프로그래밍과 본질적으로 다르지 않고, 오히려 더 나은 프로그래밍 경험이 될 수 있다고도 언급한다. AI라는 새로운 &#39;지니(genie)&#39;와 함께 더 즐겁게 코딩할 수 있는 시대를 맞이하는게 좀 더 맞지 않을까. </p>
<p><a href="https://link.springer.com/article/10.1007/s10796-025-10591-5">Understanding Human-AI Augmentation in the Workplace: A Review and a Future Research Agenda</a> 에서도 자동화(automatic)에서는 역할 대체가 중심이지만, 보조(augmentation)에서는 인간이 판단·결정·감독(directing, decision-making, oversight)하는 역할이 필수임을 강조한다. </p>
<p><a href="https://builtin.com/articles/ai-transformed-role-software-developers">How AI Has Transformed the Role of Software Developers</a> 에서는 AI 도구들이 루틴한 코드 작성, 테스트 실행 등을 자동화하면서, 개발자들이 비즈니스 요구 사항, 아키텍처 결정, 보안 및 리뷰, 성능 최적화 등에 더 많이 관여하게 됨. 즉, 단순한 코드 작성자의 역할은 줄고, 코드 리뷰/감독/전략적 사고 역할이 늘고 있다고 언급한다. </p>
<p>결국 개발자의 역할이 AI를 감독하고, 더 나은 설계와 아키텍처에 집중할 수 있게 해주는 파트너로서 개발자의 본질적 가치를 더욱 부각시키는 역할을 하게 될 것이라고 기대된다. 그리고 아주 솔직하게 그 역할만큼은 안 빼앗기려고 하지 않을까?</p>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li>B+ Tree 프로젝트 - <a href="https://github.com/KentBeck/BPlusTree3">https://github.com/KentBeck/BPlusTree3</a></li>
<li>원본 글 - <a href="https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes">https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes</a> &amp; <a href="https://tidyfirst.substack.com/p/augmented-coding-and-design">https://tidyfirst.substack.com/p/augmented-coding-and-design</a></li>
<li>한빛 글 - <a href="https://www.hanbit.co.kr/channel/view.html?cmscode=CMS7386143238">https://www.hanbit.co.kr/channel/view.html?cmscode=CMS7386143238</a></li>
<li>긱 뉴스 - <a href="https://news.hada.io/topic?id=21733">https://news.hada.io/topic?id=21733</a> &amp; <a href="https://news.hada.io/topic?id=22635">https://news.hada.io/topic?id=22635</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/3339/">CLAUDE.md 이렇게 쓰면 정말 편합니다.</a></li>
<li><a href="https://www.anthropic.com/engineering/claude-code-best-practices">Claude Code: Best practices for agentic coding</a> &amp; <a href="https://docs.anthropic.com/en/docs/claude-code/common-workflows">common-workflows</a></li>
<li><a href="https://www.reddit.com/r/ClaudeAI/comments/1mpeefp/my_claude_code_tips_for_newer_users/">https://www.reddit.com/r/ClaudeAI/comments/1mpeefp/my_claude_code_tips_for_newer_users/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM - OpenAI 가 알려주는 할루시네이션의 이유?, Why Language Models Hallucinate 따끈한 논문 리뷰]]></title>
            <link>https://velog.io/@qlgks1/why-language-models-hallucinate</link>
            <guid>https://velog.io/@qlgks1/why-language-models-hallucinate</guid>
            <pubDate>Mon, 08 Sep 2025 16:55:58 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: OpenAI 가 말아주는 할루시네이션 이야기, 내가 생각하는 핵심 위주로 요약 및 정리 ]</p>
<h1 id="why-language-models-hallucinate">Why Language Models Hallucinate?!</h1>
<p><em>[ 논문 페이지: <a href="https://arxiv.org/abs/2509.04664">arXiv</a> / 원문: <a href="https://openai.com/index/why-language-models-hallucinate/">OpenAI 블로그</a> / PDF: <a href="https://cdn.openai.com/pdf/d04913be-3f6f-4d2b-b283-ff432ef4aaa5/why-language-models-hallucinate.pdf">OpenAI PDF</a> ]</em></p>
<blockquote>
<p>25년 9월 5일 공식 홈페이지에 올라온 따근따근한 글, &quot;할루시네이션은 왜 생기는가!&quot;. 해당 논문은 할루시네이션의 &quot;기술적, 통계적&quot; 이유와 이를 야기하는 <strong>&quot;훈련 및 평가 방식(기존 벤치마크)&quot;</strong> 을 지적하는게 핵심 주제다. </p>
</blockquote>
<ul>
<li>해당 글의 썸네일인 &quot;세종대왕 맥북 프로 던짐&quot; 사건</li>
<li>구글 오버뷰의 &quot;하루에 돌 1개 섭취&quot; 사건</li>
<li>2023년 뉴욕에서, <em><a href="https://www.reuters.com/legal/new-york-lawyers-sanctioned-using-fake-chatgpt-cases-legal-brief-2023-06-22/">Schwartz 변호사가 변론서(legal brief)에 6건의 판례 인용을 했는데 모두 &quot;가짜&quot; 였던 사건</a></em> 등</li>
</ul>
<p>이 사건들, 특히 마지막은 LLM의 할루시네이션이 실무에 끼칠 수 있는 리스크를 명확히 보여준 대표적인 사례로 남아 있다. </p>
<p>OpenAI는 할루시네이션이 단순한 버그가 아니라, <strong>*<span style="color: rgb(99 148 255);">훈련 및 평가 인센티브 구조 자체가 환각을 조장한다</span>*</strong> 는 근본적 원인을 제시한다. 어떤 얘기인지 좀 더 깊게 살펴보자. </p>
<h2 id="1-할루시네이션이-왜-발생하는가">1. 할루시네이션이 왜 발생하는가?</h2>
<p>Large Language Model 은 &quot;확률적 언어 모델&quot; 이다. 이 내용이 너무 질려버린 사람이 있을 수 있기에 LLM자체에 대한 설명은 <a href="https://velog.io/@qlgks1/LLM-Intro-to-Large-Language-Models">Intro to Large Language Models 글</a> 로 대체한다. </p>
<p>결국 주어진 context 에서 &quot;가장 가능성 높은 다음 단어 예측&quot; 하도록 훈련된다. 문법·철자와 같이 <strong>일관적 패턴</strong> 은 학습으로 정복할 수 있지만, <strong>단발성 사실(singletons)</strong> 처럼 데이터에 거의 등장하지 않는 정보는 통계적으로 일반화가 불가능하다.  </p>
<p>즉, 모델은 “이게 진짜 사실인가?”를 배우는 것이 아니라, “이 문장이 훈련 데이터에 나왔는가?”를 학습하기 때문에, 결국 <strong>사실에 근거하지 않은 추측</strong> 을 내놓게 된다. <em>(아래, 논문 저자 Adam 의 생일을 물어보는 질문)</em></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/6f80a07c-21ca-4317-99f1-8af5aa1da6ac/image.png" alt=""></p>
<h3 id="1-문제는-학생이-아니라-시스템이다">1) 문제는 학생이 아니라 시스템이다!!</h3>
<p>(논문 초록, Abstract) 논문은 마치 &quot;어려운 시험 문제에 직면한 학생&quot;처럼, LLM이 불확실성을 인정하기보다는 추측하도록 보상받는 환경에 놓여있다고 비유한다. 해결책 역시 새로운 모델 아키텍처가 아닌, 기존 벤치마크의 점수 체계를 수정하는 &#39;사회-기술적 완화&#39; 방안을 제안한다.</p>
<p>PS) 일각에서는 OpenAI가 자기에게 유리한 평가로 프레임 전환을 하고 있다는 비판도 있다...!</p>
<blockquote>
<p>Like students facing hard exam questions, large language models sometimes guess when uncertain, producing plausible yet incorrect statements instead of admitting uncertainty.</p>
</blockquote>
<p>이진 0-1 채점 방식 하에서, 정답은 +1점, 오답은 0점, 그리고 &quot;모르겠습니다(I don&#39;t know, IDK)&quot;와 같은 불확실성의 표현 역시 0점을 받는다. 이 구조에서 불확실한 문제에 대해 추측하는 행위는 오답일 경우 0점으로 본전이지만, 정답일 경우 +1점을 얻을 수 있는 비대칭적 이득을 제공한다. <strong><em>따라서 점수 극대화를 목표로 최적화된 모델에게 추측은 변칙적인 행동이 아니라 합리적이고 학습된 행동이 된다.</em></strong></p>
<p>AI 모델이 누군가의 생일을 모를 때, &quot;모르겠다&quot;고 답하면 무조건 0점을 받지만, &quot;9월 10일&quot;이라고 추측하면 365분의 1 확률로 점수를 얻을 수 있다. 이러한 시스템은 모델이 정직한 소통가보다는 영리한 시험 응시자가 되도록 유도한다. 이 얘기는 뒤에 더 상세하게 다룬다. </p>
<h3 id="2-인간의-지각-경험에서-오는-환각과는-근본적인-차이가-있다">2) 인간의 지각 경험에서 오는 환각과는 근본적인 차이가 있다.</h3>
<p>(서론에서) 언어 모델의 환각은 정보 생성 과정에서 발생하는 통계적 오류와 평가 시스템에 의해 강화되는 학습된 행동에 가깝다고 한다. 그래서 인간이 경험하는 할루시네이션(환각)과 LLM에서의 할루시네이션은 <strong><em>근본적인 차이가 있음을 분명히 한다.</em></strong> </p>
<hr>
<h2 id="2-환각은-이진-분류-오류와-밀접하게-연관되어-있다">2. 환각은 이진 분류 오류와 밀접하게 연관되어 있다.</h2>
<blockquote>
<p>Hallucinations need not be mysterious — they originate simply as errors in binary classification. If incorrect statements cannot be distinguished from facts, then hallucinations in pretrained language models will arise through natural statistical pressures. We then argue that language models hallucinate because the training and evaluation procedures reward guessing over acknowledging uncertainty, and we analyze the statistical causes of hallucinations in the modern training pipeline.</p>
</blockquote>
<p>논문은 할루시네이션 현상을 현대 LLM 파이프라인의 두 가지 주요 단계에 걸쳐 분석한다. <strong>첫 번째는 오류가 처음 발생하는 &#39;사전 훈련(Pre-training)&#39; 단계</strong> 이고, <strong>두 번째는 오류가 지속되는 &#39;사후 훈련(Post-training)&#39; 단계</strong> 이다. </p>
<p>논문은 복잡한 텍스트 생성 문제를 <strong>Is-It-Valid (IIV)</strong> 라고 불리는 더 간단한 지도 학습 기반 이진 분류 문제로 환원한다. 이는 &quot;이것이 유효한 언어 모델 출력인가?&quot;라는 질문에 답하는 분류기를 가정한다. (즉, 주어진 텍스트 출력(response)이 유효한지(+) 또는 오류인지(-)를 분류)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/e75a620b-38ec-443c-93c5-b6ce763ae671/image.png" alt=""></p>
<p>생성 모델이 유효한 출력을 생성하는 것은 이러한 <strong><em>예/아니오 질문에 답하는 것보다 어떤 면에서 더 어렵다.</em></strong> 왜냐하면 유효한 출력을 생성하려면 암묵적으로 각 후보 응답에 대해 &quot;이것이 유효한가&quot;라는 질문에 답해야 하기 때문이다.</p>
<ul>
<li>이러한 환원을 통해 논문은 생성 오류율과 IIV 오분류율 사이에 다음과 같은 핵심적인 수학적 관계를 정립한다. </li>
<li>$(generative error rate)≳2⋅(IIV misclassification rate)$</li>
<li>생성 오류율이 IIV 오분류율의 두 배 이상이 될 수 있다는 수학적 관계를 제시한다. </li>
</ul>
<h3 id="1-사전-훈련pre-training-단계에서의-오류">1) 사전 훈련(Pre-training) 단계에서의 오류</h3>
<h4 id="arbitrary-fact-hallucinations">Arbitrary-fact hallucinations</h4>
<ul>
<li>데이터에 간결하게 설명할 수 있는 패턴이 없을 때 발생하는 <strong>인식론적 불확실성(epistemic uncertainty)</strong> 으로 인해 발생</li>
<li><strong><em>싱글톤 비율(Singleton Rate):</em></strong> 훈련 데이터에 단 한 번 등장하는 사실의 비율이 환각 빈도를 결정.</li>
<li>사전 훈련 데이터에 한 번만 나타나는 특정 사실의 비율(singleton rate)이 환각률의 하한이 된다는 것을 보여준다. (ex - 위 언급된 Adam 의 생일!)</li>
</ul>
<h4 id="poor-model-families">Poor Model Families</h4>
<p>모델의 아키텍처나 표현 능력이 특정 개념을 잘 나타내지 못할 때 발생한다. 예를 들어, 옛날의 트라이그램 언어 모델은(n-gram 모델) 제한된 문맥으로 인해 문법적으로 틀린 문장을 자주 생성했고 DeepSeek-V3 모델이 글자 세기 작업에서 오류를 내는 것도 모델의 한계 때문으로 분석된다. </p>
<h4 id="그-외">그 외</h4>
<ul>
<li>Calibration (오류가 없는 언어모델은 &quot;보정 상태&quot;가 될 수 없다(<code>δ</code>가 0이 될 수 없음), 세부 내용은 논문 참조!)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ce37a92e-f621-4e5d-84d4-ebd51ff88641/image.png" alt=""></p>
<ul>
<li>계산적 난이도(Computational Hardness) - 암호 해독과 같은 문제</li>
<li>분포 변화(Distribution Shift) - 훈련 데이터 분포와 크게 다른 OOD(Out-of-Distribution) 프롬프트</li>
<li>GIGO(Garbage In, Garbage Out), 대규모 훈련 코퍼스에 포함된 수많은 사실적 오류가 기본 모델에 의해 복제될 수 있음이 언급된다. </li>
</ul>
<h3 id="2-사후-훈련post-training-단계에서의-오류">2) 사후 훈련(Post-training) 단계에서의 오류</h3>
<blockquote>
<p>사후 훈련은 인간 피드백 기반 강화 학습(RLHF), 직접 선호 최적화(DPO), AI 피드백 기반 강화 학습(RLAIF)과 같은 기법을 통해 사전 훈련된 기본 모델의 오류를 줄이고 인간의 선호도에 맞게 조정하는 것을 목표로 한다. 하지만 논문은 이러한 방법들이 할루시네이션을 제거하는 데 실패한다고 주장한다. 그 이유는 이 기법들이 최적화하려는 목표, 즉 주류 평가 벤치마크 자체가 근본적으로 잘못 정렬되어 있기 때문이라 한다. </p>
</blockquote>
<p>논문은 GPQA, MMLU-Pro, SWE-bench 등 10개의 널리 사용되는 벤치마크에 대한 메타 분석을 수행했으며, 그 결과 거의 모든 벤치마크가 이진 채점 방식을 사용한다는 것을 언급한다. </p>
<ul>
<li>정답 = 1점, 오답 = 0점, “모르겠다” = 0점</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1778231d-52af-46b1-9fd1-aa0702b9612e/image.png" alt=""></p>
<p><code>WildBench</code> 만이 불확실성 표시에 대해 최소한의 점수를 부여하지만, 그마저도 IDK 응답이 사실 오류나 환각이 있는 &quot;공정한(fair) 할루시네이션&quot; 응답보다 낮은 점수를 받을 수 있어 여전히 추측을 장려할 수 있다고 지적한다.</p>
<p>따라서 모델은 기대값 관점에서 <strong>“찍는 것이 항상 유리”</strong> 하다. “모르겠습니다”보다는 <strong>그럴듯하게라도 답하는 쪽이 보상 구조상 이득</strong>이 된다. 논문에서는 이를 어려운 시험 문제에 직면했을 때 불확실하면 추측하는 학생들의 행동에 비유한다. </p>
<p>이로 인해 언어 모델은 항상 &quot;시험을 치르는 모드(test-taking mode)&quot;에 있게 되며, 자신의 지식 부족을 인정하기보다는 과신에 찬 답변을 생성하도록 유도된다고 한다. </p>
<hr>
<h2 id="3-그래서-어떻게-할루시네이션을-줄일-수-있는가">3. 그래서 어떻게 할루시네이션을 줄일 수 있는가?</h2>
<h3 id="1-환각-문제-해결을-위한-사회-기술적-완화-socio-technical-mitigation">1) 환각 문제 해결을 위한 사회-기술적 완화 (Socio-technical Mitigation)</h3>
<p>기존 리더보드와 벤치마크의 채점 체계를 바꿔야 한다고 주장한다. “불확실성 표명” 자체에 보상을 주어, 모델이 <strong>정직하게 모른다고 답할 수 있는 환경</strong>을 조성해야 한다!</p>
<p>기존 주류 평가 벤치마크에 매몰되어 환각이 심화되거나 조장할 수 있으니, 불확실성 캘리브레이션을 반영한 새로운 지표를 만들자!</p>
<h3 id="2-explicit-confidence-targets">2) Explicit Confidence Targets</h3>
<p>모델이 <strong>확률적 진실성</strong> 대신 <strong>선택적 진실성</strong> 을 따르도록 유도해야 한다. - <code>behavioral calibration</code></p>
<ul>
<li>&quot;t보다 높은 확신이 있는 경우에만 답변하고, 오류는 t/(1-t)점 감점, 정답은 1점, &#39;모르겠습니다&#39;는 0점&quot;과 같이 명확한 지침을 제공하는 것</li>
<li>예: 확신도 70% 이상일 때만 답변, 그 이하는 “모르겠다(IDK)” 처리.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/45f136b8-64f1-48a4-aeea-76bfee018098/image.png" alt=""></p>
<h3 id="3-search-and-reasoning-are-not-panaceas">3) Search (and reasoning) are not panaceas</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f2638df5-4387-4f15-8bc8-1c2ec6094d28/image.png" alt=""></p>
<p><del>이는 사실 논문에서 줄이기 위한것이 아니라 &quot;한계점에 대한 언급&quot;</del></p>
<p>검색 증강 생성(RAG)과 같은 기술이 할루시네이션을 줄이는 데 효과적임이 입증되었지만 이러한 기술이 만병통치약이 아님을 강조한다. 이진 채점 시스템은 검색이 확실한 답변을 제공하지 못할 때에도 여전히 추측에 보상하기 때문. 또한, 검색은 글자 세기 예시와 같은 계산 오류나 다른 내재적 환각에는 도움이 되지 않을 수 있다. </p>
<p>논점에서 살짝 벗어나지만 RAG 를 위해 Vector DBMS를 선택하는 것도 필수가 아니라는 점이다. 사견으로, 이론상 RAG 가 할루시네이션을 더 줄이려면 Vector 보다는 <strong>*&quot;유사한게 아니라 명확하고 확실하게 검색되는 것&quot;*</strong> 이 더 도움을 줄 수 있다. </p>
<p>결국 RAG를 위한 output 도 prompt 의 일부분이고, 결국 RAG 도 context window 에 제한적이다. 가끔 현업에서 모든 정보를 vector 에 담아서 &quot;어떻게든 증강하겠지<del>&quot; 라는 기도메타를 보는데.. ~</del>사실 어느정도 내 얘기기도 하다..~~ 오히려 100% 확실한 정보만, 짧고 굵게 매핑할 수 있는 라벨링이 훨씬 유의미하지 않을까 생각한다.</p>
<h4 id="그-외는-사실-결론과-이어지는-것이다">그 외는 사실 결론과 이어지는 것이다.</h4>
<hr>
<h2 id="4-결론-더-똑똑한-ai가-아닌-더-정직한-ai-">4. 결론: &#39;더 똑똑한&#39; AI가 아닌 &#39;더 정직한&#39; AI !!</h2>
<p>할루시네이션은 지도 학습(supervised learning)에서의 오분류(misclassifications)와 유사하게 생성 오류(generative errors)로 발생하며, 이는 교차 엔트로피 손실(cross-entropy loss) 최소화의 자연스러운 결과로 나타난다. </p>
<p>논문은 생성 문제를 “이 출력이 유효한가?”라는 <strong>IIV(Is-It-Valid)</strong> 이진 분류로 환원하고, 두 오류 사이의 하한 관계를 명시한다. <strong><code>생성 오류율 ≥ 2 × IIV 오분류율.</code></strong>  </p>
<p>이 관계는 사전훈련만 놓고 보더라도 모델이 피할 수 없는 통계적 제약(예: <strong>싱글턴 비율</strong>처럼 데이터에 한 번만 등장하는 사실의 비중)이 환각으로 이어짐을 보여준다.</p>
<p>그러나 환각은 <strong>사후훈련 단계</strong>에서 “없어지지 않는다.” 오늘의 주류 벤치마크는 <code>정답=1, 오답=0, IDK=0</code>인 <strong><code>0-1 채점</code></strong> 을 널리 쓰며, 이 구조는 <strong>모를 때 ‘찍는’ 편이 기대값 상 유리</strong> 하도록 모델을 길들인다.  </p>
<p>그 결과 모델은 항상 <strong>시험 응시 모드(test-taking mode)</strong> 로 작동하며, 불확실성 표명보다 <strong>자신감 있는 오답!!</strong> 을 택하게 된다. 이는 환각을 “미스터리한 버그”가 아닌, <strong>평가 인센티브가 낳은 학습된 행동</strong> 으로 설명해 준다.</p>
<ul>
<li><p>핵심 처방은 <strong>모델 구조가 아니라 평가 시스템을 &#39;간단히 수정(simple modification)&#39;</strong> 하는 것이라고 언급한다.</p>
</li>
<li><p>기존 리더보드의 주 지표를 <strong>정확도 일변도에서 “불확실성 인식/자제”를 보상하는 형태</strong>로 수정하면 (예: <strong>확신이 낮을 땐 답변을 유보하게 만드는</strong> 명시적 confidence 타깃, <strong>행동적 캘리브레이션</strong>) “찍기”의 기대이익을 제거하고 <strong>정직한 불확실성 표현</strong> 을 유도할 수 있다.  </p>
</li>
<li><p>오답(특히 <strong>자신감 높은 오답</strong>)에는 <strong>유의미한 페널티</strong>, 적절한 <strong>IDK/유보에는 부분 크레딧</strong>을 부여하는 채점으로 <strong>주요 벤치마크 전체</strong>를 업데이트해야 한다.  </p>
</li>
</ul>
<p>PS) 엄청나게 큰 대형 모델이 있으면 덜할까? 논문에서는 사실 거의 아니다라고 보는 것 같다. 오히려 &quot;작은 모델이 한계를 아는 태도, 대답을 유보하는 모습&quot; 을 쉽게 보일 수 있다고 언급한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] Continuous Deployment - 발렌티나 세르빌]]></title>
            <link>https://velog.io/@qlgks1/%EC%B1%85-%EB%A6%AC%EB%B7%B0-Continuous-Deployment</link>
            <guid>https://velog.io/@qlgks1/%EC%B1%85-%EB%A6%AC%EB%B7%B0-Continuous-Deployment</guid>
            <pubDate>Sun, 31 Aug 2025 12:03:47 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="continuous-deployment-지속적-배포-oreilly">Continuous Deployment, 지속적 배포 (oreilly)</h1>
<blockquote>
<p>발렌티나 세르빌(Valentina Servile): 방콕에 본사를 둔 소트웍스의 수석 소프트웨어 개발자로, 분산 시스템의 지속적 배포 분야에서 수많은 고객과 협업하며 컨설팅을 해왔다. 여러 다기능 팀에서 근무하며 대규모 분산 시스템과 마이크로서비스, 지속적 배포 프랙티스, 진화하는 아키텍처 등 다양한 기술 스택을 쌓아왔다. 평소 코드 작성은 물론, 다른 동료를 멘토링하는 일을 즐긴다. 소트웍스의 고객사에서 소프트웨어 배포 프랙티스를 개선하고 안정적인 릴리스를 더 자주 수행함으로써 비즈니스 환경 변화에 신속하게 대응할 수 있도록 지원하는 일에 보람을 느낀다.</p>
</blockquote>
<ul>
<li><a href="https://www.thoughtworks.com/en-us/insights/books/continuous-deployment">thoughtworks 링크에서 저자에 대한 내용과 책 자체에 대한 소개를 좀 더 많이 볼 수 있다.</a> -&gt; 초반 챕터들의 맛보기와 팟캐스트를 이용할 수 있다!</li>
<li><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B4987981105">🔥 한빛 책 링크</a></li>
</ul>
<h2 id="리뷰">리뷰</h2>
<p>이제 github action 의 test code runing 하는 yaml 파일은 꼭 먼저 만들고 시작하게 된다. (물론 린팅 -&gt; 테스팅 단계로) 하지만 이는 어디까지나 <strong>“최소한의 규칙 점검”</strong> 에 가깝다.  </p>
<p>이 책이 묻는 본질은 한층 직설적이다. <strong>우리는 왜 CI/CD를 구성하는가?</strong> 『지속적 배포』는 도구 사용법보다 <strong>팀이 공동으로 가져야 할 관점과 습관</strong>에 집중한다. O’Reilly 특유의 교과서적 정밀함과, 현장에 닿아 있는 실무성의 균형이 돋보인다.</p>
<p>책은 아래와 같은 5개의 파트로 이뤄져 있다. 개념을 모두 뿌려두고, S/W 생명 주기에 맞춰 이를 어떻게 적용하는지의 흐름이다. </p>
<ul>
<li><strong>Part 1</strong>: CD 개념·배경 정리(Dev/Ops·XP·CI/CD의 계보 포함)  </li>
<li><strong>Part 2</strong>: <em>개발 이전</em> — 작업 슬라이싱·가치 중심 설계·팀 규칙  </li>
<li><strong>Part 3</strong>: <em>개발 단계</em> — 트렁크 기반 개발, 기능 토글, 파이프라인  </li>
<li><strong>Part 4</strong>: <em>개발 이후</em> — 프로덕션 중심 검증·릴리스 전략(카나리 등)  </li>
<li><strong>Part 5</strong>: 사례 연구 — N26·TravelPerk·AutoScout24 등 도입기</li>
</ul>
<p>책의 성격은  <strong>*<span style="color: rgb(99 148 255);">“사용법”이라기보다는 &quot;팀 차원의 ‘계몽’과 행동 변화를 유도&quot;</span>*</strong> 하는 책에 가깝다.</p>
<h3 id="주관적인-하이라이트">주관적인 하이라이트</h3>
<h4 id="트렁크-기반-개발tbd---trunk-based-development">트렁크 기반 개발(TBD) - Trunk-Based Development</h4>
<p>책의 기본 원리는 명확하다. <strong>“더 작게, 더 자주, 더 안전하게.”</strong> 배포는 릴리스와 다르며, 코드는 항상 운영 환경에 들어갈 수 있어야 한다. 사용자는 <strong>기능 토글(Feature Toggles)</strong> 과 <strong>카나리(Canary)</strong> 등으로 <strong>노출을 통제</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/148659d9-00c3-43c4-bd4e-70c6b3f030b8/image.png" alt=""></p>
<p>핵심은 <strong>실패를 전제로 설계</strong>하는 것이다. “잘못된 커밋의 비율”보다 <strong>실패 감지 속도와 우아한 복구(낮은 MTTR)</strong> 가 더 중요하다. 긴 브랜치와 늦은 병합이 만들어내는 충돌 비용을 <strong>짧은 수명 브랜치 + 잦은 통합</strong>으로 끊고, 파이프라인 신뢰도를 높이며 <strong>커밋 단위의 책임</strong>을 선명하게 만든다.  </p>
<ul>
<li>브랜치 수명 제한(예: 1~2일), “Definition of Merge” 합의(필수 체크·리뷰 SLA)  </li>
<li>메인 브랜치의 <strong>항상 배포 가능 상태</strong> 보장(빨간 파이프라인 금지 원칙)  </li>
<li>롤백 대신 <strong>롤포워드</strong>를 표준화(토글/리버전 플랜 사전 준비)</li>
</ul>
<h4 id="기능-토글feature-toggles">기능 토글(Feature Toggles)</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/954185ec-f389-46e1-8695-3a1f1203c1f3/image.png" alt=""></p>
<p>기능 토글은 미완성 기능도 <strong>배포 가능</strong>하게 만든다. <strong>최상위/중첩 토글</strong>과 <strong>확장·축소 패턴</strong>으로 노출을 세밀하게 제어하며 <strong>배포와 릴리스의 결합을 해체</strong>한다. 운영 관점에서 중요한 것은 <strong>토글의 수명 관리</strong>다. 토글은 채무가 되기 쉽다.</p>
<ul>
<li><strong>토글 분류</strong>: 릴리스 토글(기능 노출), 운영 토글(긴급 차단), 실험 토글(A/B), 권한 토글(세그먼트)  </li>
<li><strong>거버넌스</strong>: 토글 생성 시 만료 기준·제거 시점 명시, 대시보드화, “좀비 토글” 주기적 청소  </li>
<li><strong>프로그레시브 딜리버리</strong>: 카나리 → 점진적 확대 → 자동 롤백 조건(에러율/지연/전환율)</li>
</ul>
<h4 id="작업-슬라이싱-전략">작업 슬라이싱 전략</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1c40a878-bf51-4913-bcf2-d0162a3af4cb/image.png" alt=""></p>
<p><strong>수평 분할</strong>(레이어 중심)보다 <strong>수직 분할</strong>(사용자 가치 단위)을 우선한다. <strong><em>INVEST 원칙(<code>Independent</code>·<code>Negotiable</code>·<code>Valuable</code>·<code>Estimable</code>·<code>Small</code>·<code>Testable</code>)</em></strong> 을 티켓 템플릿에 내장하고, 각 항목을 <strong>즉시 쪼갤 수 있는 기준</strong>으로 삼는다.</p>
<ul>
<li>“완성 기능”이 아니라 <strong>가치 흐름의 얇은 조각</strong>을 목표로 설계  </li>
<li>각 슬라이스에 <strong>명확한 수용 기준(AC)</strong> 과 관측 지표(로그·메트릭·이벤트) 부여  </li>
<li>인터페이스 변경이 필요한 경우, <strong>백워드 호환</strong>과 계약 테스트로 <strong>무중단 전환</strong></li>
</ul>
<h4 id="프로덕션-중심-품질-보증">프로덕션 중심 품질 보증</h4>
<p>“승인 절차가 안전을 보장한다”는 착시에서 벗어나야 한다. <strong>운영 환경에서만 검증 가능한 사실</strong>(데이터 볼륨/형상, 실제 트래픽 패턴, 네트워크·인프라 구성)을 근거로 <strong>프로덕션 테스트를 표준화</strong>한다.  </p>
<ul>
<li><strong>관측 가능성(Observability)</strong>: 트레이스·로그·메트릭 일체화, 기능별 SLI/SLO  </li>
<li><strong>셰도우 트래픽</strong>·리플레이, 실사용 세그먼트 기반 실험, <strong>오토 롤백 규칙</strong> 사전 정의  </li>
<li>품질의 단위를 “테스트 통과 여부”에서 <strong>“운영 지표의 안정성”</strong> 으로 전환</li>
</ul>
<h4 id="전체-스택-예제와-현실-이슈">전체 스택 예제와 현실 이슈</h4>
<p>React·Spring Boot·SQL로 이어지는 예제는 <strong>세션/상태, 클라이언트 캐시 무효화, DB 마이그레이션</strong> 같은 <strong>현업 난제</strong>를 직접 다룬다. 필요하면 <strong>파이프라인 일시 중지 → 로컬 검증</strong> 같은 <strong>우회 전략</strong>도 제시한다.</p>
<p>DB는 <strong>Expand → Migrate → Contract</strong>(전개·이행·축소) 3단계로 안전하게 바꾸고, 배치·백필 작업은 <strong>멱등성</strong>과 <strong>재시도 정책</strong>으로 보호한다. 배포 전략은 서비스 성격에 따라 <strong>롤링/블루-그린/카나리</strong>를 선택한다.</p>
<h4 id="사례-연구에서">사례 연구에서</h4>
<p>핀테크·이동 서비스·이커머스 등 서로 다른 도메인(N26, TravelPerk, AutoScout24 등)의 사례를 통해 <strong>CD → 조직 문화 혁신 → 배포 빈도 증가 → 비즈니스 민첩성</strong>으로 이어지는 경로가 드러난다. 이 파트는 <strong>기술 채택기</strong>를 넘어서 <strong>조직 변화기</strong>에 가깝다.  </p>
<ul>
<li>“우리의 제약(규제/리스크/조직도)을 토글·카나리·슬라이싱으로 어떻게 흡수할 것인가?” 에 대한 고찰을 준다. </li>
<li>또한 규모가 조금 있는 조직에서 “변경 리드타임·배포 빈도·MTTR을 어떤 OKR로 연결할 것인가?” 에 대한 고민포인트가 될 것 같다.</li>
</ul>
<h4 id="결론">결론</h4>
<p>Part 1의 “왜 CD인가” 논증은 독자에 따라 다소 길게 느껴질 수 있다. 또한 관료성이 <strong>항상 악</strong>은 아니다. 어떤 조직은 git-flow·배치 릴리스를 유지해도 만족할 수 있다. 반대로 문화 성숙 없이 TBD만 밀어붙이면 <strong>“배포 담당자”로 책임이 쏠리는 역효과</strong> 가 생길 수 있다고 생각한다. </p>
<p>그럼에도 <strong>트렁크 기반 개발·기능 토글·프로덕션 검증을 하나의 체계로 묶어, ‘배포 공포증’을 조직의 학습 사이클로 전환</strong>한다는 점과 <strong>즉시 적용 가능한 패턴</strong>과 <strong>현실적 우회 전략</strong>을 모두 담았기에, <strong>당장 배포 흐름을 개선하려는 팀</strong>, <strong>장기적으로 전달 문화를 재정립하려는 조직</strong>, 그리고 <strong>개인(팀)</strong> 모두에게 추천할 만하다.</p>
<p>개인적으로 <strong>[PART 3 개발 단계]</strong> 가 특히 인상적이었다. JVM(Spring) 환경에서 <strong>기능 토글을 코드 레벨로 구현</strong>하는 실습이 포함되어 있어, 추상적인 개념이 채워지는 느낌이 있다. <del>사실 이런 느낌의 코드를 처음봐서 그런것 같다.</del></p>
<hr>
<h2 id="목차">목차</h2>
<h3 id="part-01-지속적-배포">[PART 01 지속적 배포]</h3>
<p>CHAPTER 01 지속적 배포
_1.1 수개월, 수년마다 한 번 배포
_1.2 며칠마다 한 번 배포<br>_1.3 지속적 배포
_1.4 익스트림 프로그래밍
_1.5 데브옵스
_1.6 지속적 통합
_1.7 지속적 전달
_1.8 최종 프로덕션 게이트
_1.9 시사점
_1.10 지속적 배포는 위험한가?
_1.11 정리하기</p>
<p>CHAPTER 02 이점
_2.1 원피스 플로와 린 생산
_2.2 DORA 메트릭
_2.3 품질 시프트 레프트
_2.4 정리하기</p>
<p>CHAPTER 03 사고방식의 전환
_3.1 변경사항을 정의하는 것과 적용하는 것
_3.2 진행 중인 작업 숨기기
_3.3 분산 시스템
_3.4 프로덕션 경로 간의 계약
_3.5 배포는 릴리스가 아니다
_3.6 엔드투엔드 전달 라이프 사이클
_3.7 정리하기</p>
<p>CHAPTER 04 최소 요건
_4.1 자율적 다기능 팀
_4.2 이해관계자의 신뢰
_4.3 정리하기</p>
<p>CHAPTER 05 도전 과제
_5.1 배포에 민감한 시스템
_5.2 유저 설치 소프트웨어
_5.3 규제 대상 산업
_5.4 인지 부하
_5.5 정리하기</p>
<h3 id="part-02-개발-이전">[PART 02 개발 이전]</h3>
<p>CHAPTER 06 예정된 작업 나누기
_6.1 수평 분할 vs 수직 분할
_6.2 지속적 배포를 하면<br>_6.3 효과적인 수직 분할
_6.4 예제: 그로서루
_6.5 정리하기</p>
<p>CHAPTER 07 프로덕션 빌드
_7.1 배포성 요건
_7.2 테스트성 요건<br>_7.3 관찰 가능성 요건
_7.4 보안 요건
_7.5 성능 요건
_7.6 (좀 더) 완전한 유저 스토리 템플릿
_7.7 예제: 그로서루 유저 스토리에 CFR 추가
_7.8 정리하기</p>
<h3 id="part-03-개발-단계">[PART 03 개발 단계]</h3>
<p>CHAPTER 08 플랫폼 아키텍처 재구축
_8.1 유저 스토리
_8.2 그로서루 애플리케이션
_8.3 정리하기</p>
<p>CHAPTER 09 라이브 기능 리팩터링
_9.1 해야 할 일
_9.2 상품 식별 체계
_9.3 현재 상태
_9.4 목표 상태
_9.5 어떻게 목표를 달성할까?
_9.6 확장/축소 구현
_9.7 정리하기</p>
<p>CHAPTER 10 데이터와 데이터 손실
_10.1 해야 할 일
_10.2 현재 상태
_10.3 목표 상태
_10.4 어떻게 목표를 달성할까?
_10.5 이중 쓰기 구현 전략
_10.6 이중 읽기 구현 전략
_10.7 NoSQL<br>_10.8 정리하기</p>
<h3 id="part-04-개발-이후">[PART 04 개발 이후]</h3>
<p>CHAPTER 11 프로덕션에서 테스트
_11.1 왜 프로덕션에서 테스트를 해야 하나?
_11.2 어떻게 프로덕션에서 테스트를 할까?<br>_11.3 스테이징 이후의 스토리
_11.4 정리하기</p>
<p>CHAPTER 12 릴리스
_12.1 안티패턴: 빅뱅 릴리스
_12.2 안티패턴: 부분 배포로 일부만 릴리스
_12.3 릴리스에 기능 토글 응용
_12.4 카나리 릴리스
_12.5 A/B 테스트
_12.6 정리하기</p>
<h3 id="part-05-사례-연구">[PART 05 사례 연구]</h3>
<p>CASE STUDY A 오토스카우트24
_A.1 오토스카우트24의 당시 상황
_A.2 오토스카우트24의 지속적 배포 도입
_A.3 오토스카우트24의 지속적 배포 구현</p>
<p>CASE STUDY B 오토
_B.1 오토의 당시 상황
_B.2 오토의 지속적 배포 도입
_B.3 오토의 지속적 배포 구현
_B.4 참고 자료</p>
<p>CASE STUDY C N26
_C.1 N26의 당시 상황
_C.2 N26의 지속적 배포 도입
_C.3 N26의 지속적 배포 구현
_C.4 참고 자료</p>
<p>CASE STUDY D 클라이밋파트너
_D.1 클라이밋파트너의 당시 상황
_D.2 클라이밋파트너의 지속적 배포 도입
_D.3 클라이밋파트너의 지속적 배포 구현</p>
<p>CASE STUDY E 모타빌리티 오퍼레이션즈
_E.1 모타빌리티 오퍼레이션즈의 당시 상황<br>_E.2 모타빌리티 오퍼레이션즈의 지속적 배포 도입
_E.3 모타빌리티 오퍼레이션즈의 지속적 배포 구현</p>
<p>CASE STUDY F 레아 그룹
_F.1 레아 그룹의 당시 상황
_F.2 레아 그룹의 지속적 배포 도입<br>_F.3 레아 그룹의 지속적 배포 구현    </p>
<p>CASE STUDY G 메이즈
_G.1 메이즈의 당시 상황<br>_G.2 메이즈의 지속적 배포 도입
_G.3 메이즈의 지속적 배포 구현</p>
<p>CASE STUDY H 메이즈
_H.1 트래블퍼크의 당시 상황
_H.2 트래블퍼크의 지속적 배포 도입
_H.3 트래블퍼크의 지속적 배포 구현</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[velog dashboard v2 - 트랜드 글 메일로 받아보세요~~]]></title>
            <link>https://velog.io/@qlgks1/velog-dashboard-v2-trend</link>
            <guid>https://velog.io/@qlgks1/velog-dashboard-v2-trend</guid>
            <pubDate>Wed, 20 Aug 2025 16:33:27 GMT</pubDate>
            <description><![CDATA[<h1 id="velog-dashboard-v2">Velog Dashboard v2</h1>
<blockquote>
<p>Velog Dashboard, <strong><em>신규 feature update 공유!</em></strong>
<strong><em>빨리 접속하기: <a href="https://velog-dashboard.kro.kr/">https://velog-dashboard.kro.kr/</a></em></strong>
<strong><em>Github repo: <a href="https://github.com/check-Data-Out/velog-dashboard-v2">https://github.com/check-Data-Out/velog-dashboard-v2</a></em></strong></p>
</blockquote>
<p>Velog 통계를 한눈에 확인할 수 있는 서비스를 100% 무료로 제공하고 있습니다! Velog 생태계가 살아 있는 한 저희도 함께 성장하며, 오히려 선순환 구조에 기여할 수 있기를 바라고 있습니다. <strong><em>(요즘 광고글 트랜딩에 너무 많아요<del>~ AI 필터링 도입해주세요</del> 쩨발,, 과해 ㅠㅠ)</em></strong></p>
<ul>
<li><p>저희 팀은 주 1회 이상 정기 회의를 진행하며, 그 과정에서 작지만 잦은 업데이트들이 꾸준히 이루어지고 있습니다!! 현재 코드 커버리지는 90% 이상을 달성했고, <strong>프론트엔드에서는 E2E 테스트까지 도입했습니다!</strong></p>
</li>
<li><p>또한 지금까지 총 9대의 서버를 무료로 확보하여 운영해 왔는데, 이를 안정적으로 유지하기 위해 많은 노력을 기울였습니다. 무료 서버를 여기저기 모아 쓰는 게 결코 쉽지 않다는 걸 몸소 체감하고 있습니다. 아래 소개할 인프라 개괄도를 보시면 그 과정이 더 재미있게 다가올지도 모르겠습니다.</p>
</li>
<li><p>이번 글은 서비스의 생존 소식을 전하고, 동시에 그간의 업데이트 현황을 공유드려보고자 합니다~~ 🎉🫡</p>
</li>
</ul>
<p><strong>*<span style="color: rgb(99 148 255);">업데이트 요약본</span>*</strong> </p>
<p>① LLM 기반 <strong>주간 메일링 분석</strong>(트렌드·작성 글 분석) - <strong><em>매주 월요일 오전에 발송!</em></strong>
② <strong>리더보드 고도화</strong>(기간별 조회/좋아요 증감, 사용자·게시글 단일 클릭 이동)
③ <strong>Velog API 의존 최소화</strong>(내부 탐색·네비게이션 시 자체 저장 정보 활용)
④ <strong>30분 캐시 레이어</strong> 도입으로 응답 체감 속도 개선
⑤ <strong>샤딩·풀링 아키텍처 재정비</strong>
⑥ <strong>DevOps 도입</strong></p>
<hr>
<h2 id="1-메일링-서비스">1. 메일링 서비스</h2>
<blockquote>
<p>Velog 의 주간 트랜드! + 글을 안쓰면 재촉까지!!
그리고 여러분들의 누적 조회 변동량 + 작성한 글에 대한 분석!!</p>
</blockquote>
<ul>
<li>글을 쓰면 어떤지 분석을 해드려요, LLM 을 활용한 서비스랍니다 :) 어떤 분석인지 궁금하면 빨리 등록해서 메일을 받아보셔요! &gt;&gt; <strong><em><a href="https://velog-dashboard.kro.kr/">빨리 접속하기</a></em></strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/94843933-92db-4a94-8512-55467cff09a0/image.png" alt=""></p>
<h3 id="세부-구현">세부 구현</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c7a04418-2e93-45bc-8854-bb39f0ad10fc/image.png" alt=""></p>
<p>이번에 구현한 <strong>“벨로그 글 트렌드 분석” 배치 작업</strong>은 <strong>추상 클래스 기반 템플릿 메서드 패턴</strong>으로 설계했습니다. 내부적으로는 <strong>3가지 배치 작업</strong>으로 나누어져 있으며, 이를 뒷받침하는 <strong>3개의 독립 외부 모듈</strong>이 존재합니다. </p>
<pre><code class="language-python">@dataclass
class AnalysisContext:
    &quot;&quot;&quot;분석 컨텍스트 정보&quot;&quot;&quot;

    week_start: datetime
    week_end: datetime
    velog_client: VelogClient


# ==================================== #
# LLM 호출 부분 ...
# ==================================== #

    async def _analyze_data(
        self, raw_data: list[TrendingPostData], context: AnalysisContext
    ) -&gt; list[WeeklyTrendInsight]:
        &quot;&quot;&quot;LLM을 사용한 트렌드 분석&quot;&quot;&quot;
        try:
            # LLM 입력 데이터 준비
            llm_input = [post_data.to_llm_format() for post_data in raw_data]

            # LLM 분석 실행
            llm_result = analyze_trending_posts(
                llm_input, settings.OPENAI_API_KEY
            )
            ...

# ==================================== #
# AWS SES 호출 부분 ...
# ==================================== #

class WeeklyNewsletterBatch:
    def __init__(
        self,
        ses_client: SESClient,
        ...


        # 최대 max_retry_count 만큼 메일 발송
        while failed_count &lt; self.max_retry_count and not success:
            try:
                self.ses_client.send_email(newsletter.email_message)
                ...</code></pre>
<ul>
<li><p>이번 배치는 단순히 <em>“그냥 만들자!”</em> 보다는 <em>“잘 만들어 두자!”</em>에 가까웠습니다.<br>특히 처음으로 <strong>유저에게 직접 발송되는 메일링</strong>을 다루다 보니, 유지보수에 열려 있고 변경하기 쉬운 구조가 무엇보다 중요했습니다.  </p>
</li>
<li><p>외부 모듈은 총 <strong>3개</strong>로, <code>llm</code>, <code>velog</code>, <code>SES email</code>입니다. (각자 조금씩 다른 디자인 형태로 설계 되었습니다! <a href="https://github.com/check-Data-Out/velog-dashboard-v2-back-office/">실제 코드 보러가기</a>)</p>
</li>
<li><p>이 모듈들은 <strong>퍼사드(Facade) 패턴</strong>과 <strong>Lazy Init 싱글톤 패턴</strong>으로 구현하여, 내부 비즈니스 로직과 <strong>철저히 분리</strong>해 두었고, 개별 유닛테스트로 동작을 보장하게 되어있습니다.</p>
</li>
<li><p>즉, 내부에서는 해당 모듈의 클라이언트에 접근해 필요한 핵심 비즈니스 로직을 호출하는 것 외에는 어떤 의존성도 가지지 않도록 설계했습니다. 그리고 호출시 주입하도록 했구요! <del>물론 구현 과정에서 배보다 배꼽이 더 커졌던 것 같긴 하지만요…</del>  </p>
</li>
</ul>
<ol>
<li>주간 트렌드 분석  </li>
<li>주간 사용자 트렌드 분석  </li>
<li>트렌드 메일 발송  </li>
</ol>
<ul>
<li>배치를 <strong>3가지 유형으로 분리한 것</strong>도 같은 맥락입니다. 각각의 역할을 명확히 나누어 <strong>유연성과 확장성</strong>을 확보할 수 있도록 했습니다. 약간의 아쉬움은 존재하지만 과한 집착은 오히려 안티패턴을 만들어~~</li>
</ul>
<h2 id="2-리더보드">2. 리더보드</h2>
<blockquote>
<p><strong>Velog Dashboard</strong> 사용자들은 이제 <strong>기간별 조회수 증가량</strong>과 <strong>좋아요 증가량 리더보드</strong>를 더 쉽고 빠르게 확인할 수 있습니다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/3be989a6-b3a1-4a69-bd3d-e0f29fdd7d45/image.png" alt=""></p>
<p>또한 새로운 기능이 추가되었습니다!</p>
<ol>
<li><strong>사용자 클릭 시 → 해당 사용자 Velog 페이지로 이동</strong>  </li>
<li><strong>게시글 기준 정렬 시 → 해당 게시글로 바로 이동</strong>  </li>
</ol>
<ul>
<li><p>이를 구현하기 위해 기존에는 저장하지 않던 <code>velog profile</code> 관련 정보(<code>username</code> 등)를 불가피하게 저장하게 되었습니다.  </p>
</li>
<li><p>그래도 긍정적인 것은 그 결과, 서비스 자체에서는 <strong>더 이상 Velog API를 직접 호출하지 않아도</strong> 됩니다!! (물론 <strong>통계 집계에는 여전히 Velog API</strong>를 사용합니다.) -&gt; 간헐적으로 velog api 가 뻗을때 저희도 Timeout 이 되는 이슈가 있었는데 사실 이제 없다는 의미 ㅎㅎ (첫 로그인 외...)</p>
</li>
</ul>
<h3 id="추가로-cache-layer를-도입했습니다">추가로 <strong>Cache Layer</strong>를 도입했습니다!</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/7f02e55a-8696-4623-aa9b-7de3e57d1b58/image.png" alt=""></p>
<p><a href="https://github.com/Check-Data-Out/velog-dashboard-v2-cache/blob/main/docker-compose.yaml">깃허브 docker compose 보러가기</a></p>
<ul>
<li>stand-alone 형태로 독립시켜서 가동했고, 여러가지 업데이트가 있을 것 같아 레포를 파서 버저닝을 하게 되었습니다.</li>
<li>그래서 리더보드는 <strong>30분 단위 캐싱</strong>으로 훨씬 쾌적해졌습니다!! 앞으로 이 캐싱의 적용 범위는 계속 늘어날 것 같습니다!</li>
<li>현재는 <strong>쿼리 최적화</strong>도 병행하고 있지만, DBMS 구조적 한계가 있어 <strong>우선 캐싱부터 적용</strong>했습니다.  </li>
</ul>
<hr>
<h2 id="3-dbms-샤딩과-캐시-layer-추가">3. DBMS 샤딩과 캐시 layer 추가</h2>
<h3 id="1-처음엔-버티컬-샤딩을-하려고-했었다">1) 처음엔 버티컬 샤딩을 하려고 했었다...</h3>
<p>초기에는 Supabase 기반으로 운영했습니다! 다만 다음 이슈들이 겹치며 이소 준비를...</p>
<ol>
<li><strong>Postgres 17 전환과 TimescaleDB 지원 변경</strong></li>
</ol>
<ul>
<li><p>Supabase가 Postgres 17 번들을 예고하면서 <code>timescaledb</code>가 번들에서 제외(Deprecated)되어, 업그레이드 전에 드롭이 필요해졌습니다. -&gt; 시계열 워크로드를 계속 운영하기엔 마이그레이션·대안 검토 비용이 커졌습니다.</p>
</li>
<li><p><strong>데이터 egress 비용 압박!!</strong>, batch 에서 bulk 로 데이터를 많이 밀어넣다보니 egress 가 유독 한계치를 계속 찍었습니다.. 이 탓에 일단 구독을 해버렸었죠.. (월 2.5만) 근데 supabase 를 no-code tool로 사용할 거라면 돈내고도라도 쓰는데, 우리는 이미 가용할 수 있는 무료 서버도 많았죠...</p>
</li>
</ul>
<h3 id="2-pgbouncer로-풀링--간이-분산을-시도">2) PgBouncer로 풀링 + 간이 분산을 시도</h3>
<p>설계 초안</p>
<ul>
<li><strong>토폴로지</strong>: 1대 Primary, 2대 Replica(Read-only), 앞단에 <strong>PgBouncer</strong></li>
<li><strong>의도</strong>: 커넥션 폭주 완화(풀링) + 간이 라운드로빈</li>
</ul>
<p>근데 <code>PgBouncer</code> 는 <strong>경량 커넥션 풀러</strong>입니다. 자체적으로 <strong>다중 호스트 라우팅/샤딩을 하지 않았죠...</strong></p>
<ul>
<li>다만 <strong>DNS 라운드로빈(또는 host 리스트 뒤 LB)</strong> 을 물려두면, PgBouncer의 <code>server_round_robin</code> 옵션으로 <strong>서버 커넥션 재사용 방식을 라운드로빈</strong>에 가깝게 바꿀 수 있었습니다.</li>
<li>뭐,, 결론적으로 PgBouncer는 <strong>샤딩 도구가 아니라 풀러</strong>이므로, “읽기/쓰기 분리”나 “키 기반 샤딩 라우팅”은 <strong>별도 계층</strong>이 필요했죠,, <strong><em>탈락!</em></strong></li>
</ul>
<h3 id="3-진짜로-원했던-건-유저-그룹-기반-수평-샤딩">3) 진짜로 원했던 건 ‘유저 그룹’ 기반 <strong>수평 샤딩</strong></h3>
<p>도메인 특성상 핵심 테이블이 <strong>유저 중심</strong>이고, 모든 유저가 <strong>group 키</strong>를 갖도록 최초부터 설계했습니다. 따라서 <strong>횡적 분할(샤딩)</strong> 을 통해!</p>
<ul>
<li>단일 노드 I/O 병목을 회피</li>
<li>그룹 단위의 <strong>데이터 지역성(locality)</strong> 확보</li>
<li>장애/스케일 전략의 독립성 제고</li>
</ul>
<p>라는 뻔한(X, 큰꿈) 목표를 노렸고 <code>pgcat</code> 으로 시도 했었죠.. <del>하지만.. SQL 기반으로 분산 처리 가능하다는데 제대로 작동을 안함</del></p>
<p>그니까 사실 <code>pgcat</code> 은 <code>sqlparser</code> 기반의 쿼리 파서를 실행해서 <strong><em>SELECT → Replica / 그 외(트랜잭션·DML 포함) → Primary 로 자동 라우팅하는</em></strong> 기능은 명백하게 가지고 있었습니다.. (<a href="https://github.com/postgresml/pgcat">pgcat github</a>)</p>
<p><strong>*<span style="color: rgb(99 148 255);">근데 더 depth 있게 SQL 의 특정 key 값만 판단해서 라우팅을 하려면 모듈을 오버라이딩 해야 했죠..</span>*</strong> </p>
<ul>
<li><p>사실 더 정확하겐 <code>SET SHARDING KEY</code> 를 통해 샤드 라우팅을 할 수 있는데, 이는 결국 백오피스든, API에서든, 이를 위한 서드파티 구성이 필요하다는 의미..</p>
</li>
<li><p>일단 본능적으로 한 보 후퇴,, 그래서 일단 P.D.D (Primary + 2 Dup) 세팅으로 하되, 다음을 노리는 것으로 방향을 잡았습니다. (근데 사실 아직까지 못끝낸거 실화?...)</p>
</li>
</ul>
<h3 id="4-redis-기반-cache-layer-이를-위한-모듈">4) redis 기반 cache layer, 이를 위한 모듈</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/e0dcd21a-57e8-4f6b-be1c-dbe2e2c9ba71/image.png" alt=""></p>
<p>캐시에서 조금 그럴듯 한 얘기를 붙이자면 <code>Interface Segregation Principle</code> 지향했고<code>CacheConfig</code>, <code>Redis</code> 구현체를 위한 <code>ICache</code> <strong>&quot;<code>Strategy Pattern</code>&quot;</strong> 과 <code>RedisCache</code> 를 하나의 <strong>&quot;<code>Adapter</code>&quot;</strong> 로 사용하고자 했습니다.</p>
<pre><code class="language-typescript">// cache.config.ts
const cacheInstance: ICache = new RedisCache(cacheConfig);
export const cache = cacheInstance; // 전역에서 하나의 인스턴스 사용</code></pre>
<ul>
<li>위와 같이 <code>Singleton</code> 으로 사용했고, 필요할때만 전역에서 불러와 사용합니다.</li>
<li><code>ICache</code> 덕에 test code 에서 주입하기도 좋습니다!</li>
<li>캐싱 전략은 우선적으로 TTL 밖에 없습니다! </li>
</ul>
<h3 id="5-그래서-현-최종-인프라-개괄도">5) 그래서 현 최종 인프라 개괄도</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ca5ab3e8-c959-44e6-ace8-a12e5151a2f7/image.png" alt=""></p>
<p><del>개발새발인 것 같지만 이게 최대치</del></p>
<h2 id="4-devops">4. DevOps</h2>
<p><code>Prometheus</code> + <code>PLG</code> stack 을 기본 베이스로 깔고, <code>(Prometheus)node-exporter</code> 들로 서버 자체 매트릭을 가져오는 정도를 목표로 세팅했습니다. (사실 아직 완료는 못했고 중에 있습니다.)</p>
<ul>
<li>근데 웬걸, <a href="https://grafana.com/docs/alloy/latest/">alloy 라는 걸로 바뀌어 버렸네?</a></li>
<li>굳이 왜..? 를 좀 더 찾아보니 &quot;Grafana는 Prometheus, Promtail, Grafana Agent 등 서로 다른 수집 도구들을 하나로 통합&quot; 하고 싶었다고 하네요.. - 
<a href="https://grafana.com/blog/2024/04/09/grafana-agent-to-grafana-alloy-opentelemetry-collector-faq/?utm_source=chatgpt.com">https://grafana.com/blog/2024/04/09/grafana-agent-to-grafana-alloy-opentelemetry-collector-faq/?utm_source=chatgpt.com</a> <del>근데 사실 오히려 좋아</del> </li>
</ul>
<table>
<thead>
<tr>
<th>단계</th>
<th>일정 (2025년 기준)</th>
<th>주요 내용</th>
</tr>
</thead>
<tbody><tr>
<td>Promtail LTS 시작</td>
<td><strong>2025-02-13</strong></td>
<td>기능 업데이트 종료, 보안·버그 수정만 지원 (<a href="https://grafana.com/blog/2025/02/13/grafana-loki-3.4-standardized-storage-config-sizing-guidance-and-promtail-merging-into-alloy/?utm_source=chatgpt.com" title="Grafana Loki 3.4">Grafana Labs</a>, <a href="https://grafana.com/docs/loki/latest/send-data/promtail/?utm_source=chatgpt.com" title="Promtail agent">Grafana Labs</a>)</td>
</tr>
<tr>
<td>Alloy 기능 통합</td>
<td><strong>Loki 3.4 이후</strong></td>
<td>Promtail 기능 및 구성 변환 도구 Alloy에 통합 (<a href="https://grafana.com/docs/loki/latest/release-notes/v3-5/?utm_source=chatgpt.com" title="Grafana Loki 3.5">Grafana Labs</a>, <a href="https://grafana.com/docs/alloy/latest/set-up/migrate/from-promtail/?utm_source=chatgpt.com" title="Migrate from Promtail to Grafana Alloy">Grafana Labs</a>)</td>
</tr>
<tr>
<td>Promtail EOL</td>
<td><strong>2026-03-02</strong></td>
<td>공식 지원 및 업데이트 완전 종료 (<a href="https://grafana.com/blog/2025/02/13/grafana-loki-3.4-standardized-storage-config-sizing-guidance-and-promtail-merging-into-alloy/?utm_source=chatgpt.com" title="Grafana Loki 3.4">Grafana Labs</a>, <a href="https://grafana.com/docs/loki/latest/send-data/promtail/?utm_source=chatgpt.com" title="Promtail agent">Grafana Labs</a>)</td>
</tr>
</tbody></table>
<ol>
<li><strong>메트릭, 로그, 트레이스 통합 수집 → Alloy</strong>, </li>
<li><strong>시각화/알람 → Grafana</strong>, </li>
<li><strong>스토리지 → Loki/Prometheus</strong></li>
</ol>
<p>이 3강 체제로 &quot;Observability&quot; 를 끌어올리려고 합니다! </p>
<hr>
<h2 id="마무리와-tobe">마무리와 TOBE</h2>
<ul>
<li><p>(혹시나 궁금하신 분들 위해..) DAU 는 약 50+- a 선으로 나오고 있습니다. 꾸준히 보는 사람만 보는? 그런 좀비 서비스랄까요ㅎㅎ 일단 제가 가장 열심히 DAU에 일조하고 있는 듯</p>
</li>
<li><p>통계 데이터는 약 300만개에 달하게 되었습니다. P.D.D 세팅이 시급합니다.. 어서 제발 끝내자..</p>
</li>
</ul>
<blockquote>
<p>이제 다음 feature 는 진짜 다른 플랫폼 통계 데이터 aggregation 으로의 확장일 것 같습니다. 아마 미디엄이나 티스토리 둘 중 하나가 될 것 같네요!  </p>
</blockquote>
<p><strong><em>여기까지가 Velog Dashboard v2의 생존기(?)와 업데이트 소식입니다!</em></strong>
<del>매번 글을 너무 과하게 쓰는 것 같아서 많이 줄였습니다 ㅋㅎㅋㅎ</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] 객체 지향 시스템 디자인 원칙 - 마우리시오 아니체]]></title>
            <link>https://velog.io/@qlgks1/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@qlgks1/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Sun, 20 Jul 2025 10:09:43 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;길벗 출판사에서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="객체지향-시스템-디자인-원칙">객체지향 시스템 디자인 원칙</h1>
<blockquote>
<p>Maurício Aniche(마우리시오 아니체): 아디옌(Adyen, 네덜란드 기술 기업)에서 테크 리드로 근무(근데 최근 우버로 이직하신 듯), 아디옌의 테크 아카데미를 포함해 엔지니어를 위한 추가 교육과 훈련에 중점을 둔 엔지니어링 지원 이니셔티브 팀을 이끌고 있다. 또한 네덜란드 델프트 공과대학교(Delft University of Technology)에서 소프트웨어 공학 조교수로 재직 중. 2021년 올해의 컴퓨터 과학 교사 상을 받았고, 혁신적인 강사에게 수여되는 명예로운 TU 델프트 교육 펠로우십(Delft Education Fellowship)을 받았다. - <em><a href="https://www.linkedin.com/in/mauricioaniche/">링크드인</a> &amp; <a href="https://mauricioaniche.com/">저자 블로그</a></em></p>
</blockquote>
<p><del>요새 저자들 링띤을 어떻게든 찾아내서 책 잘 읽었다고 슬쩍 메시지를 보내본다. 모르는 부분 물어보는 재미가 아주 쏠쏠하다.</del></p>
<p>🔥 길벗 책 링크 - <a href="https://www.gilbut.co.kr/book/view?bookcode=BN004492">https://www.gilbut.co.kr/book/view?bookcode=BN004492</a>
🔥 코드 참조 깃허브 레포 - <a href="https://github.com/enshahar/SimpleObjectOrientedDesignCode">https://github.com/enshahar/SimpleObjectOrientedDesignCode</a> (옮긴이, 오현석님께서 모두 한글로 재구성 해주셨다.)
🔥 <strong><em>그리고 이를 <code>python</code> 으로 직접 포팅한 레포</em></strong> - <a href="https://github.com/Nuung/SimpleObjectOrientedDesignCode">https://github.com/Nuung/SimpleObjectOrientedDesignCode</a></p>
<h2 id="리뷰">리뷰</h2>
<p>시장에는 객체지향 설계(OOP)와 디자인 패턴 관련 책들이 정말 넘쳐난다. 그럼에도 이 책이 매력적이었던 포인트는, 저자의 머릿말에서부터 “<strong>이 정도면 충분한 디자인을 이루는 방법</strong>” 에 대해 이야기하고 있기 때문이다. 이 책은 실제로 아래 그림 한 장으로 표현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/c67acd4e-be39-4c9c-804d-2beeaef07cd2/image.png" alt=""></p>
<p>이상하게 가장 오래 기억에 남는 문장은 <strong>6장에서 나오는 &quot;프레임워크와 싸우지 마라&quot;</strong> 이다.
도메인 주도 설계(DDD)에 깊이 몰입하다 보면, ‘프레임워크는 거들 뿐’이라는 말에 너무 진심이 되어버릴 때가 있다. 실제로 나도 과거에 프레임워크 의존성을 싹 걷어낸 뒤, REST API 하나 만들기 위해 어답터 패턴을 굳이 도입하고, 퍼사드를 하나 더 추가하면서 “프레임워크랑 싸운” 경험이 있다. (나름 프레임워크 의존성 없는 독립 모듈에 프레임워크를 얹어 보겠다는 일념으로)</p>
<p>물론 그 시도가 틀렸다고 말할 순 없지만, 책에서 말하듯 <strong>어디에서는 프레임워크에 의존하지 말아야 하고, 어디에서는 과감하게 활용해야 하는지의 경계</strong>를 짚어주는 부분이 특히 와닿았다. (해당 장에서는 &quot;인프라 계층&quot; 에 대한 것을 다루는데, 아주 좋음!)</p>
<p>이 책은 전체적으로 짧다. <strong>그렇기에 실습 없이 읽으면 아무런 의미가 없다.</strong> 꼭 깃허브에 공개된 예제 코드를 따라가는 것을 추천한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/496be073-1468-4c51-a201-2a8d412a8d27/image.png" alt=""></p>
<ul>
<li><strong><em>2장부터 5장은</em></strong> 코드작게, 일관성, 의존성, 추상화까지 SOLID 원칙을 하나씩 top-down 하는 흐름이고, </li>
<li><strong><em>6장과 7장이</em></strong> 인프라를 포함한 외부 의존성 (모듈)에 대해 다루며, </li>
<li><strong><em>8장</em></strong> 이상과 현실에 대한 얘기를 하며 끝난다. </li>
</ul>
<p>솔직히 말하면, 많은 설계/아키텍처 책들은 읽다 보면,, 마치 &quot;성공하는 100가지 법칙!&quot; 같은 책을 읽는 느낌이 든다. 책 내용이 안좋다는게 아니라, 유니콘에 대한 해설책 느낌이랄까.</p>
<p>물론 이 책도 모든 걸 해결해주지는 않지만, <strong>점진적인 고도화 전략</strong>과 <strong>각 장별로 전달하려는 핵심을 예제 기반으로 잘 녹여낸다는 점</strong>에서 실전성 높은 책이라고 생각이 든다. (각 장에서 핵심을 설명하고, “피플그로우!” 라는 실전 예제가 항상 느낌표와 함께 따라온다. 이 반복도 의외로 인상 깊다.)</p>
<p>무엇보다도 좋았던 건, 처음부터 완벽하지 않아도 괜찮다고 말해준다는 점이다. <strong>처음에는 모든 책임을 명확히 구분하거나, 도메인을 제대로 정의하지 못하는 게 당연하며</strong>, 그것을 점진적으로 개선해가는 방향이 더 현실적인 접근이라는 메시지가 담겨 있다. &quot;진짜 좋은 디자인은 세 번쯤 다시 작성한 후에야 얻어진다!&quot;</p>
<p>나는 이 책을 처음에는 한숨에 읽고, 그 뒤 코드를 하나하나 Python으로 포팅하면서 다시 읽었는데, 생각보다 시간이 정말 오래 걸렸다. <del>사실 Python으로 다시 짜는 데 대부분의 시간이 들었다는 건 비밀이다.</del> 디자인 책은 아무리 라이트하다고 해도, <strong>주제 자체가 결코 라이트하지 않다는 점</strong>을 다시금 느꼈다.</p>
<p>이 책은 마틴 파울러, DDD, 전통적인 디자인 패턴 등 다양한 고전과 참고 문헌을 바탕으로 하고 있다. 하지만 <strong>그 모든 걸 당장 직접 찾아가며 읽을 필요는 없다.</strong> </p>
<p>오히려 이 책이 그런 책들에 들어가기 전, 특히 DDD를 공부하기 전에 읽으면 딱 좋은, 전초전 같은 느낌의 책이다. “왜 디자인 패턴?” “왜 객체지향?” “좋은 코드란 대체 뭘까?”라는 질문에 대해 가볍고도 넓게 훑어준다. 이 책을 읽은 다음 <strong>‘클린 아키텍처’와 DDD를 읽으면, 훨씬 더 잘 이해할 수 있을 것</strong>이다.</p>
<hr>
<p>이제 실제 코드 구현은 AI 가 대부분이 한다. 하지만 사견으로 여전히 &quot;적절한 디자인&quot; 은 답답한 구석이 너무나 많다. 그리고 &quot;적절한 디자인&quot; 이라 함은 S/W 구준, 서비스의 현실적인 상황, 팀 내부의 상황 등을 포함해 내&amp;외부에 따라 trade-off 정도가 천차만별이다.</p>
<p>나는 여전히 이 부분에서 필연적으로 AI 를 control 할 인력이 필요하다고 본다. 당장의 <code>cache layer</code> 를 구현하는데, TTL 전략 외 LRU, LFU 까지 강제 구현해버려서 짜증이 났다.</p>
<p>철저하게 <code>TTL</code> 만 사용할꺼고 필요로 하는데, 이 때문에 <code>storage</code> 와 <code>strategy</code> 인터페이스(추상화) 까지 만들어 버렸다. 물론 이게 틀린게 아니라 내가 원한 것 보다 너무 과했기 때문이다. 이걸 다시 리펙토링 하며 가다듬다보면 &#39;이럴꺼면 내가 처음부터 하지&#39;,,, 라는 생각도 많이 든다.</p>
<p>이 부분은 더 고도화되겠지만 본질적으로 거시적인 관점에서 trade-off 에 맞는 output 을 평가하는 주체는 아직까지 &quot;인간&quot; 의 영역이고, 이 영역은 더 소중해 질 것으로 보인다. 그렇기 때문에 이런류의 책이 &quot;더 이상 필요 없다 / 이제 AI 가 다 해준다&quot; 라는 평가에는 강력하게 반대한다. </p>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<h3 id="1장-모든-게-복잡도-관리다">1장 모든 게 복잡도 관리다</h3>
<p>이 책은 기존의 객체지향 베스트 프랙티스를 단순히 반복하지 않는다. 원제인 <em>Simple Object Oriented Design</em> 이 암시하듯이, <strong>복잡도를 줄이는 것</strong>, 즉 <strong>단순한 구조와 유지보수 가능한 시스템을 어떻게 설계할 수 있는가</strong>에 초점을 맞춘다. </p>
<p> 얘기 시작. 복잡성을 줄이거나 그대로 유지하기 위한 작업을 수행하지 않으면 S/W 시스템은 시간이 지남에 따라 복잡성이 증가한다.</p>
<p>저자는 <strong>매니 레만(Lehman)</strong>의 논문, <a href="https://gwern.net/doc/cs/1979-lehman.pdf">&quot;대규모 프로그램의 생명 주기에서 법칙, 진화, 보존에 대한 이해&quot; 라는 논문에서</a> 인용하며 논의를 시작한다. 요지는 단순하다. <strong>*<span style="color: rgb(99 148 255);">복잡성은 줄이거나 유지하지 않으면 반드시 증가한다.</span>*</strong> 즉, 이를 방치하면 시스템은 유지보수하기 점점 어려워지고, 결과적으로 개발 속도는 느려지고 품질은 낮아진다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/5c5b2738-27ba-444c-9458-37331f23ffcb/image.png" alt=""></p>
<h4 id="-단순한-객체지향-디자인을-위한-여섯-가지-핵심-원칙-">[ 단순한 객체지향 디자인을 위한 여섯 가지 핵심 원칙 ]</h4>
<p>책에서는 단순함을 지향하는 객체지향 디자인을 위해 다음 여섯 가지 원칙을 제시한다.</p>
<ol>
<li><p><strong>단순한 코드</strong><br>메서드와 클래스는 작고 단순하게 유지해야 한다. 코드의 &quot;크기&quot; 자체가 복잡성을 야기할 수 있다.<br>→ 즉, <strong>짧고 명료한 함수</strong>, <strong>작은 클래스 단위</strong>를 유지해야 함.</p>
</li>
<li><p><strong>일관성 있는 객체</strong><br>객체는 항상 유효한 상태를 유지해야 한다.<br>외부에서 <code>Basket</code> 클래스를 마음대로 수정할 수 있다면 이는 곧 <strong>무결성 침해</strong>로 이어진다.<br>이를 위해선 다음 키워드 정도를 떠올릴 수 있다.</p>
<ul>
<li>불변성(immutable), 적절한 getter/setter 제공, 접근 제어자 활용 등</li>
</ul>
</li>
<li><p><strong>적절한 의존성 관리</strong><br><strong>높은 응집도(high cohesion)</strong>와 <strong>낮은 결합도(low coupling)</strong>를 동시에 지향해야 한다.<br>자식 클래스가 바뀔 때마다 부모 클래스도 바뀐다면, 이는 <strong>변경 전파의 악순환</strong>이다.<br>→ 이러한 상황을 방지하기 위한 핵심은 <strong>디커플링(decoupling)</strong>이다.</p>
</li>
<li><p><strong>좋은 추상화</strong><br>추상화는 단순성과 확장성 사이의 균형을 제공한다.<br>단순함을 추구하다 보면 어느 순간 클래스의 메서드가 지나치게 많아지며 혼란을 유발할 수 있다.<br><strong>의미 있는 추상화 계층을 도입</strong>하는 것이 해결책이다.</p>
</li>
<li><p><strong>외부 의존성과 인프라를 적절히 다루기</strong><br>비즈니스 로직과 외부 시스템(DBMS, API 등)과의 의존성은 분리되어야 한다.<br>예: DB 연결 로직이 여기저기 흩어져 있다면, 추후 캐시 도입이나 DB 교체 시 유지보수 지옥이 펼쳐진다.</p>
<p><em>※ 참고: DDD에서는 이러한 외부 시스템과 자원을 “인프라”로 분류한다.</em></p>
</li>
<li><p><strong>좋은 모듈화</strong><br>시스템을 <strong>작고 명확한 컴포넌트 단위로 나누는 것</strong>이 중요하다.<br>이는 이해하기 쉽고, 변경하기 쉬운 코드를 만드는 데 핵심이다.</p>
</li>
</ol>
<h4 id="-일상적인-활동으로서의-단순한-디자인-">[ 일상적인 활동으로서의 단순한 디자인 ]</h4>
<p>복잡도를 낮추는 행위는 일회성 이벤트가 아니라, <strong>일상의 루틴처럼 지속적으로 수행되어야 한다.</strong></p>
<ul>
<li><p><strong>복잡성 줄이기는 개인 위생과 같다</strong><br>매일 조금씩 관리하지 않으면, 쌓이고 쌓여 나중에는 감당이 안 되는 수준이 된다.</p>
</li>
<li><p><strong>복잡성이 필요할 수도 있지만 영구적이어서는 안 된다</strong><br>예외적 상황에서 복잡한 구조가 불가피하더라도, 이를 영구 구조로 유지하면 안 된다. (하지만 소 잡는 칼로 닭 잡지 마라! 점진적 접근!)</p>
</li>
<li><p><strong>지속적으로 복잡성을 해결하는 것이 비용 효율적이다</strong><br>미뤄놓으면 나중에 더 큰 비용을 치른다. 리팩터링과 점진적 개선을 게을리하지 말아야 한다.</p>
</li>
<li><p><strong>고품질 코드는 좋은 실무 프랙티스를 촉진한다</strong><br>→ 이른바 &quot;깨진 유리창 이론&quot;처럼, 더 나은 코드가 더 좋은 문화를 만든다.</p>
</li>
<li><p><strong>복잡성을 통제하는 것은 생각보다 어렵지 않다</strong><br>꾸준한 주의와 실천이 있다면 충분히 관리 가능한 수준이다.</p>
</li>
<li><p><strong>디자인을 단순하게 유지하는 것은 개발자의 책임이다</strong><br>아무도 해주지 않는다. 개발자 스스로가 구조를 정리하고 유지할 책임이 있다.</p>
</li>
<li><p><strong>“이 정도면 충분히 좋은 디자인이다”라는 마음가짐</strong><br>완벽한 디자인을 고집하기보다, 지금 당장 필요한 수준에서 가장 깔끔한 해법을 추구해야 한다.<br>『A Philosophy of Software Design』에선, <strong>*<span style="color: rgb(99 148 255);">&quot;진짜 좋은 디자인은 세 번쯤 다시 작성한 후에야 얻어진다&quot;</span>*</strong> 라는 말이 등장한다. 전적으로 동의하는 대목이다.</p>
</li>
</ul>
<h4 id="-정보-시스템-아키텍처-구성-요소-">[ 정보 시스템 아키텍처 구성 요소 ]</h4>
<p>이 책에서는 복잡도를 줄이기 위해 <strong>객체지향 아키텍처를 어떻게 구성할 것인가</strong>에 대해서도 간략히 소개한다:</p>
<ol>
<li><p><strong>엔터티(Entity)</strong><br>비즈니스 개념을 표현하는 핵심 객체.<br>예: <code>Invoice</code> 클래스는 청구서라는 개념을 속성과 메서드로 표현한다.</p>
</li>
<li><p><strong>서비스(Service)</strong><br>복잡한 비즈니스 로직을 캡슐화.<br>예: <code>GenerateInvoice</code>는 장바구니의 품목을 종합해 최종 청구서를 생성한다.</p>
</li>
<li><p><strong>리포지터리(Repository)</strong><br>데이터 저장 및 조회 로직을 담당.<br>DB와의 통신은 이 계층에서 처리.</p>
</li>
<li><p><strong>DTO (Data Transfer Object)</strong><br>계층 간 정보 전달에 사용되는 단순한 데이터 구조체.</p>
</li>
<li><p><strong>유틸리티 클래스</strong><br>언어나 프레임워크가 제공하지 않는 범용 기능을 모은 클래스.<br>→ 다만 너무 남용하면 도메인 의미를 흐릴 수 있어 주의 필요.</p>
</li>
</ol>
<h3 id="2장-코드를-작게-유지하기">2장 코드를 작게 유지하기</h3>
<p>(개인적인 해석으로) 이 장의 핵심 주제는 단 하나다.</p>
<blockquote>
<p><strong>“코드를 작게 나누는 것이 복잡도 제어의 출발점이다.”</strong></p>
</blockquote>
<p>크고 복잡한 코드일수록 변경에 취약하며, 버그가 발생하기 쉽고, 테스트와 협업이 어렵다.<br>반대로 작고 응집력 있는 코드 단위는 더 안전하고 유지보수도 용이하다.</p>
<p><strong>클래스와 메서드는 작아야 한다.</strong> 긴 메서드는 언제든 버그의 온상이 될 수 있다. 그래서 기본 원칙은 단순하다. “작은 단위는 항상 큰 단위보다 낫다.”</p>
<h4 id="-복잡한-메서드를-비공개-메서드로-나눠라-">[ 복잡한 메서드를 비공개 메서드로 나눠라 ]</h4>
<p>응집력 있는 컴포넌트는 <strong>단일한 책임</strong>을 가진다. 즉, 한 가지 일만 하는 메서드로 쪼개야 한다. 이를 위한 체크리스트는 다음과 같다:</p>
<ol>
<li>새 메서드의 <strong>목적을 설명하는 명확한 이름</strong>을 붙일 수 있는가?</li>
<li>새 메서드가 <strong>응집력 있고, 작으며, 외부 공개 메서드에서 쉽게 호출</strong>될 수 있는가?</li>
<li>많은 파라미터나 클래스에 의존하지 않고 <strong>간결하고 명확</strong>한가?</li>
<li><strong>정적 메서드</strong>로 만들 수 있을 정도로 독립적인가? -&gt; 정적 메서드로 만들 수 있다는 건, 해당 코드가 객체 상태에 의존하지 않는다는 뜻이기도 하다.</li>
</ol>
<h4 id="-복잡한-코드-단위를-다른-클래스로-옮겨라-">[ 복잡한 코드 단위를 다른 클래스로 옮겨라 ]</h4>
<p>특정 코드가 현재 클래스의 <strong>주요 책임과 관련이 없다면</strong>, 그 코드는 다른 클래스로 옮겨야 한다. 아래 질문들을 스스로 던져보자:</p>
<ul>
<li>이 코드는 클래스의 나머지 코드와 <strong>다른 작업</strong>을 하는가?</li>
<li>이 코드에 <strong>별도의 이름</strong>과 클래스가 필요할 정도로 <strong>독립적인 의미</strong>가 있는가?</li>
<li><strong>독립적인 테스트</strong>가 필요한가?</li>
<li>클래스 전체가 너무 <strong>비대해지고 있진 않은가?</strong>
→ 이럴 땐 <strong>분리하라.</strong></li>
</ul>
<h4 id="-코드를-작은-단위로-나누지-말아야-할-때-">[ 코드를 작은 단위로 나누지 말아야 할 때 ]</h4>
<p>항상 쪼개는 게 답은 아니다. 오히려 쪼개면 복잡해지는 경우도 있다. 아래는 <strong>쪼개지 말아야 할 때의 기준</strong>이다:</p>
<ol>
<li><strong>퍼즐 조각들이 독립적으로 존재할 수 없을 때</strong> 억지로 분리하면 메서드 시그니처가 복잡해짐</li>
<li><strong>해당 로직이 교체될 가능성이 낮을 때</strong></li>
<li><strong>별도로 테스트할 만한 가치가 없을 때</strong></li>
</ol>
<p>PS. 여기서 <em>“클래스병”</em> 조심하자 — 존 오스터하우트의 <em>Software Design Philosophy</em> 에서 말하듯, 불필요하게 클래스를 쪼개면 오히려 관리가 더 힘들어진다.</p>
<h4 id="-리팩터링하기-전에-전체적으로-살펴보라-">[ 리팩터링하기 전에 전체적으로 살펴보라 ]</h4>
<p>무작정 리팩터링하지 말고 먼저 <strong>최종 구조를 머릿속에 그려보라.</strong> → 미래를 고려한 변화인지 반드시 확인하고 실행할 것.</p>
<ul>
<li>리팩터링 후 모습은 어떤가?</li>
<li>그 모습이 <strong>지향하는 설계 철학</strong>에 부합하는가?</li>
<li>지금의 디자인에 <strong>문제가 있는 건가?</strong></li>
</ul>
<h4 id="-예제-직원-데이터-임포트하기-">[ 예제: 직원 데이터 임포트하기 ]</h4>
<p>이 책은 &quot;피플그로우!&quot; 프로젝트를 계속 고도화 해간다. 상단 깃허브 레포, <a href="https://github.com/Nuung/SimpleObjectOrientedDesignCode">https://github.com/Nuung/SimpleObjectOrientedDesignCode</a> 에서 실제 코드를 볼 수 있다. (근데 <code>python</code> 으로 포팅해봄)</p>
<p>여기서 <code>import_employee_service</code>라는 <strong>덩치 큰 서비스</strong>를 점진적으로 리팩터링하고 쪼개나간다.</p>
<pre><code class="language-shell">└── ch2
    ├── v1
    │   ├── __init__.py
    │   ├── csv_parser_library.py
    │   ├── employee_repository.py
    │   ├── employee.py
    │   ├── import_employee_service.py
    │   └── import_result.py
    └── v2
        ├── __init__.py
        └── import_employee_service.py</code></pre>
<h4 id="-코드를-읽기-쉽게-만들고-문서화하라-">[ 코드를 읽기 쉽게 만들고 문서화하라 ]</h4>
<p>클린코드에선 코드 작성 vs 읽기 시간의 비율은 1:10 이라는 언급이 있었고, 개발자는 <strong>전체 시간의 60%를 코드 읽기에 소비</strong>한다는 논문도 있다. 이 사실만으로도 <strong>“읽기 쉬운 코드”의 중요성</strong>은 충분히 강조된다.</p>
<h4 id="1-좋은-이름을-계속-찾아라">1. 좋은 이름을 계속 찾아라</h4>
<p><strong>유비쿼터스 언어(Ubiquitous Language, 개발 팀원 모두가 도메인 개념을 이해하고 의사소통하기 위해 사용하는 일관성 있는 공통 언어)</strong> 를 기반으로 하라. 팀 전체가 도메인을 공유하는 언어로 <strong>일관성 있게 표현</strong>해야 한다. → 좋은 네이밍은 단순한 기교가 아니라, <strong>팀 커뮤니케이션의 기반</strong>이다.</p>
<h4 id="2-의사결정을-문서화하라">2. 의사결정을 문서화하라</h4>
<p>조건 분기나 로직이 복잡할수록 “왜 이런 결정을 했는가”를 <strong>문서화해야</strong> 한다. → 조건문은 “결과”이고, 주석(설명, 외부 docs등)은 그 “배경”이다.</p>
<h4 id="3-코드에-주석을-추가하라">3. 코드에 주석을 추가하라</h4>
<p>너무 많은 주석은 오히려 혼란을 주지만, 주석이 전혀 없는 코드도 읽기 힘들다. <strong>잘 정제된 핵심 주석</strong>은 코드 품질의 일부다.</p>
<p>주석에 대한 의견은 다양하지만, 개인적으로 임베디드 시스템이나 byte 한 개에 목숨거는게 아닌 이상, 무슨 TCP 의 14byte 못지키면 죽는게 아닌 이상, 개인적으로 제발 주석을 좀 썻으면 한다. 그리고 책에서는 &quot;왜 했는가?&quot; 도 주석에 남길 필요가 있는 경우가 있다고 언급한다! <del>매우 동감! 쩨발!</del></p>
<h4 id="-새로운-복잡성을-기존-클래스에서-분리하라-">[ 새로운 복잡성을 기존 클래스에서 분리하라 ]</h4>
<p>비즈니스 복잡성 증가함에 따라 코드는 성장할 수 밖에 없지만, 성장은 통제돼야 한다. 많은 경우 <strong><em>클래스가 무한정 성장하는 이유는 개발자가 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있게 해주는 적절한 추상화나 확장 지점이 부족하기 때문.</em></strong> <del>(뼈를 너무 맞아서 좀 아픈 대목)</del></p>
<h4 id="1-복잡한-비즈니스-로직을-자체-클래스로-분리하라">1. 복잡한 비즈니스 로직을 자체 클래스로 분리하라</h4>
<p>새로운 복잡한 로직은 <strong>전용 클래스로 독립</strong>시켜야 한다. 단, <strong>해당 로직이 작용하는 대상 클래스와 논리적으로 가까운 곳</strong>에 위치시키는 것이 좋다.</p>
<h4 id="2-큰-비즈니스-흐름을-분해하라">2. 큰 비즈니스 흐름을 분해하라</h4>
<p>여러 단계가 복합적으로 얽힌 비즈니스 흐름은 다음과 같은 방식으로 나눠보자! 핵심은, 하나의 흐름을 <strong>여러 개의 작고 응집력 있는 단위</strong>로 나누는 것이다.</p>
<ul>
<li><p>GOF 패턴 예시:</p>
<ul>
<li><strong>책임 연쇄 패턴(Chain of Responsibility)</strong></li>
<li><strong>데코레이터 패턴</strong></li>
<li><strong>옵저버 패턴</strong></li>
</ul>
</li>
<li><p>더 복잡한 경우:</p>
<ul>
<li><strong>도메인 이벤트 기반 시스템</strong>을 고려하자</li>
<li><strong>이벤트 기반 아키텍처</strong>로 재설계할 수도 있다</li>
</ul>
</li>
</ul>
<h4 id="srpsingle-responsibility-principle는-하나의-변경-이유만-있어야-한다는-원칙이다">SRP(Single Responsibility Principle)는 <strong>“하나의 변경 이유만 있어야 한다”</strong>는 원칙이다.</h4>
<p>이 장에서 강조하는 <strong>“작은 단위로 쪼개기”</strong> 는 SRP와 결을 같이하지만, SRP가 “책임”에 초점을 둔다면, 이 장은 “크기와 응집력”에 초점을 둔다. 초기 설계 단계에서는 &quot;책임&quot;을 명확히 정의하기 어렵기 때문에, 그보다 더 간단한 접근은 <strong>“일단 작게 쪼개기”</strong> 라고 한다. </p>
<h3 id="3장-객체의-일관성-유지하기">3장 객체의 일관성 유지하기</h3>
<p>이 장에서는 객체가 스스로 자신의 상태를 &quot;일관성 있게 유지하도록&quot; 설계하는 것이 얼마나 중요한지를 중심으로 다룬다.
특히 도메인 모델에서의 일관성 유지란 단순히 값 검증을 넘어서, 객체가 책임져야 할 행위와 상태에 대한 깊은 이해를 요구한다.</p>
<p>PS. 일관성(consistency)은 객체가 정확하고 신뢰할 수 있는 정보를 가지고 있음을 나타냄
PS. 반면 DBMS 에서는 무결정(integrity)이 &quot;정보를 정확하게 유지하는 것&quot;을 의미 (일관성은 주로 데이터의 가용성과 관련 있음)</p>
<h4 id="-항상-일관성을-유지하라-">[ 항상 일관성을 유지하라 ]</h4>
<h4 id="1-클래스가-스스로-일관성을-책임지게-하라">1. 클래스가 스스로 일관성을 책임지게 하라</h4>
<p>데이터의 일관성을 보장하는 책임은 데이터가 속한 클래스 내부에 있어야 한다. 예를 들어, 교육 과정 등록 기능에서 <code>Offering</code> 클래스는 최대 참가자 수를 넘기지 않도록 직접 <code>add_employee()</code> 메서드 내에서 유효성 검사를 수행하고, 등록 시 자동으로 빈 자리를 줄인다. 외부에서 이를 처리하면 중복된 로직이 생기고, 시스템 전반에서 예기치 못한 불일치 상태가 생길 수 있다.</p>
<h4 id="2-전체-작업과-복잡한-일관성-검사를-캡슐화하라">2. 전체 작업과 복잡한 일관성 검사를 캡슐화하라</h4>
<p>단일 클래스가 책임지기 어려운 경우, 서비스와 엔티티가 함께 협력하여 일관성을 보장할 수 있도록 설계해야 한다. 서비스는 흐름을 조율하고, 엔티티는 그 안에서 내부 상태를 보장하는 식이다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ded49755-ebda-42bb-a6db-352f0dfb5b9e/image.png" alt=""></p>
<h4 id="3-예제-employee-엔터티-업데이트">3. 예제, <code>Employee</code> 엔터티 업데이트!</h4>
<p>클래스 내부 일관성 보장하도록</p>
<ul>
<li>오퍼링 직언 추가 되면 잔여 허용 인원 자동으로 하나 줄이고</li>
<li>자리가 꽉 찬 경우라면 새로운 직원이 추가되지 않도록</li>
</ul>
<p>동시성과 디자인</p>
<ul>
<li>비기능적 요구 사항이 디자인 결정에 영향을 미칠 수 있다. 오퍼링에 직원 추가하는 요청이 동시에 일어난다면, 지금 디자인은 작동하지 않을 수 있음.</li>
</ul>
<pre><code class="language-python">class Offering:
    &quot;&quot;&quot;Training offering with enhanced enrollment management&quot;&quot;&quot;

    def __init__(self, training: Training, date_: date, maximum_number_of_attendees: int):
        self._id: int | None = None
        self._training = training
        self._date = date_
        self._employees: Set[Employee] = set()
        self._maximum_number_of_attendees = maximum_number_of_attendees
        self._available_spots = maximum_number_of_attendees

    @property
    def employees(self) -&gt; Set[Employee]:
        &quot;&quot;&quot;Get immutable copy of enrolled employees&quot;&quot;&quot;
        return frozenset(self._employees)  # Return immutable set

    def add_employee(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Add employee to offering with validation and prevent duplicates&quot;&quot;&quot;
        if self._available_spots == 0:
            raise OfferingIsFullException()
        if employee in self._employees:
            # 이미 등록된 직원이면 spots 차감하지 않고 무시
            return
        self._employees.add(employee)
        self._available_spots -= 1

    def has_available_spots(self) -&gt; bool:
        &quot;&quot;&quot;Check if offering has available spots&quot;&quot;&quot;
        return self._available_spots &gt; 0

    @property
    def available_spots(self) -&gt; int:
        &quot;&quot;&quot;Get number of available spots&quot;&quot;&quot;
        return self._available_spots

    @property
    def training(self) -&gt; Training:
        &quot;&quot;&quot;Get training for this offering&quot;&quot;&quot;
        return self._training

    def is_employee_registered(self, employee: Employee) -&gt; bool:
        &quot;&quot;&quot;Check if employee is registered for this offering&quot;&quot;&quot;
        return employee in self._employees</code></pre>
<h4 id="-효과적인-데이터-유효성-검사-메커니즘을-디자인하라-">[ 효과적인 데이터 유효성 검사 메커니즘을 디자인하라 ]</h4>
<h4 id="1-사전-조건을-명시적으로-정의하라">1. 사전 조건을 명시적으로 정의하라</h4>
<p>메서드는 자신이 기대하는 입력 조건을 분명하게 설정해야 한다. 예를 들어, <code>addEmployee(employee)</code> 메서드에 <code>None</code> 값이 들어오면 단순히 무시하는 것이 아니라, 아예 <code>None</code> 이 들어올 수 없음을 전제로 코드 설계를 해보는 것. 그렇게 함으로써, 숨은 오류 발생을 방지할 수 있고 유지보수가 쉬워진다.</p>
<p>근데 모든 경우에 대해 다 처리하면, 내부 개발 코드만 더 늘어난다. 이때 중요한 것이 <strong>*&quot;존재하지 않는 오류 정의&quot;*</strong> 하는 것이다.</p>
<h4 id="2-유효성-검증-컴포넌트를-만들라">2. 유효성 검증 컴포넌트를 만들라</h4>
<p>비즈니스 룰이 복잡해질수록 검증 로직은 분리되어야 재사용성과 가독성이 높아진다. 이 때 <code>Specification Pattern</code> 과 같은 명세 패턴을 도입해, 특정 조건을 만족하는지를 명확히 정의할 수 있다.
다만 모든 유효성 검사를 엔티티 내부에 구현하는 것은 피하고, 서비스 계층에서 조율하는 방향이 좋다.</p>
<h4 id="3-null은-신중하게-사용하고-피할-수-있다면-피하라">3. null은 신중하게 사용하고, 피할 수 있다면 피하라</h4>
<p>null은 호출자에게 모든 책임을 전가하기 때문에 위험하다. 가능하면 빈 객체, 옵션 값, 또는 에러 객체로 대체하는 것이 바람직하다.
(<em>특히 파이썬과 같은 언어에서는 None 검사 분산이 코드 복잡도를 높이는 주범이 될 수 있다ㅠㅠ</em>)</p>
<p>근데 빈 값이 가능하다면?</p>
<ul>
<li>값 없음을 나타내는 객체를 만들어 보는 것은 어떨지? (빈 리스트 등)</li>
<li>문제 발생시 null 리턴이 아니라, 문제가 무엇인지 설명하는 클래스 리턴은?</li>
</ul>
<h4 id="4-예제-교육-과정에-직원-추가하기">4. 예제: 교육 과정에 직원 추가하기</h4>
<pre><code class="language-python">class AddEmployeeToOfferingService:
    &quot;&quot;&quot;Service for adding an employee to a training offering with validation&quot;&quot;&quot;

    def __init__(
        self,
        offerings: OfferingRepository,
        employees: EmployeeRepository,
        validator: AddEmployeeToOfferingValidator,
    ):
        self._offerings = offerings
        self._employees = employees
        self._validator = validator

    def add_employee(self, offering_id: int, employee_email: str) -&gt; None:
        &quot;&quot;&quot;Add an employee to an offering after validation&quot;&quot;&quot;
        offering_opt = self._offerings.find_by_id(offering_id)
        employee_opt = self._employees.find_by_email(employee_email)

        if not offering_opt or not employee_opt:  # 1
            raise InvalidRequestException(&quot;Offering and employee IDs should be valid&quot;)

        offering = offering_opt
        employee = employee_opt

        validation = self._validator.validate(offering, employee)  # 2
        if validation.has_errors():  # 3
            raise ValidationException(validation)

        offering.add_employee(employee)  # 4


class ValidationResult:
    &quot;&quot;&quot;Result of a validation check, containing errors if any&quot;&quot;&quot;

    def __init__(self):
        self.errors: List[str] = []

    def has_errors(self) -&gt; bool:
        &quot;&quot;&quot;Check if there are any validation errors&quot;&quot;&quot;
        return bool(self.errors)

    def add_error(self, error: str) -&gt; None:
        &quot;&quot;&quot;Add a validation error message&quot;&quot;&quot;
        self.errors.append(error)


class ValidationException(Exception):
    &quot;&quot;&quot;Exception raised for validation failures&quot;&quot;&quot;

    def __init__(self, validation_result: ValidationResult):
        self.validation_result = validation_result
        super().__init__(f&quot;Validation failed with errors: {validation_result.errors}&quot;)


class AddEmployeeToOfferingValidator:
    &quot;&quot;&quot;Validator for adding an employee to a training offering&quot;&quot;&quot;

    def __init__(self, trainings: TrainingRepository):
        self._trainings = trainings

    def validate(self, offering: Offering, employee: Employee) -&gt; ValidationResult:
        &quot;&quot;&quot;Validate if an employee can be added to an offering&quot;&quot;&quot;
        validation = ValidationResult()

        if not offering.has_available_spots():  # 1
            validation.add_error(&quot;Offering has no available spots.&quot;)

        times_participant_took_the_training = self._trainings.count_participations(
            employee, offering.training
        )

        if times_participant_took_the_training &gt;= 3:  # 2
            validation.add_error(&quot;Participant can&#39;t take the training again.&quot;)

        if offering.is_employee_registered(employee):
            validation.add_error(&quot;Participant already in this offering.&quot;)

        return validation</code></pre>
<p>DDD(Domain-Driven Design)와 클린 아키텍처에서는 <strong>도메인 서비스</strong>와 <strong>애플리케이션 서비스</strong>를 명확히 구분할 것을 권장한다.</p>
<ul>
<li><strong>애플리케이션 서비스</strong>는 <em>작업을 조정하는 책임</em>만을 가지며, 비즈니스 규칙을 포함하지 않는다. 즉, 외부 요청을 받아 도메인 객체를 호출하고 그 결과를 반환하거나 후속 작업을 조정하는 역할에 집중한다.</li>
<li>반면, <strong>도메인 서비스</strong>는 <em>도메인 내의 중요한 비즈니스 규칙</em>을 담는 위치다. 특히 도메인 엔티티만으로 표현하기 어렵거나 여러 엔티티가 협력해야만 정의 가능한 로직은 도메인 서비스에 위치시킨다.</li>
</ul>
<p>이렇게 제어 흐름과 비즈니스 로직을 명확히 분리하면 도메인 모델의 순수성과 코드 유지 보수성이 향상된다.
그러나 필자는 처음부터 이 분리를 강박적으로 지키기보다는, <strong>단순한 서비스 코드</strong> 안에 제어와 로직을 함께 담아 빠르게 구현하는 방식을 선택한다.</p>
<p>즉, <strong>처음엔 하나의 서비스가 애플리케이션과 도메인 역할을 모두 수행하게 한 뒤</strong>, 코드가 점차 커지고 복잡성이 증가하면 그때서야 역할을 분리하는 점진적 리팩터링을 제안한다. 이는 실용주의적인 접근으로, 지나치게 이른 추상화와 과도한 레이어 분리를 피하고, 현실적인 개발 흐름을 존중한 방식이다.</p>
<h4 id="-상태-확인을-캡슐화하라-">[ 상태 확인을 캡슐화하라 ]</h4>
<p>객체의 상태 확인은 그 자체로도 캡슐화되어야 한다. 단순히 속성 값을 외부로 노출하기보다는, 객체 내부에서 판단 가능한 책임을 맡기고, 클라이언트는 그 판단 결과만 신뢰하도록 해야 한다.</p>
<p>복잡한 구조일수록 상태 확인 로직이 분산되기 쉽고, 이로 인해 <strong>&quot;샷건 수술(Shotgun Surgery)&quot;</strong> 현상이 발생할 수 있다.
이는 어떤 기능 변경이 시스템 여러 곳의 코드를 동시에 수정하게 만드는 대표적인 안티패턴이다 (<a href="https://en.wikipedia.org/wiki/Shotgun_surgery">Wikipedia - Shotgun Surgery</a>).</p>
<h4 id="1-명령하라-질문하지-마라">1. 명령하라, 질문하지 마라!</h4>
<p>OOP의 고전적인 원칙인 <strong>Tell, Don’t Ask</strong>는 마틴 파울러가 강조한 철학으로, 객체에게 데이터를 꺼내와서 외부에서 판단하게 하지 말고, 객체에게 <em>무엇을 해야 할지</em>를 명령하라는 뜻이다.<br>즉, <code>if (obj.canDoX()) { obj.doX(); }</code>와 같은 패턴은 <code>obj.tryToDoX()</code>처럼 하나의 메시지로 통합하는 것이 좋다.</p>
<p>이 원칙은 객체의 내부 구현을 숨기고, 역할 중심의 인터페이스를 설계하는 데 핵심이 된다.</p>
<h4 id="-필요한-게터와-세터만-제공하라-">[ 필요한 게터와 세터만 제공하라 ]</h4>
<p>객체의 캡슐화를 유지하려면, 게터(getter)와 세터(setter)를 남발해서는 안 된다.
게터를 통해 내부 상태를 지나치게 노출하거나, 세터를 통해 객체 외부에서 무분별하게 값을 변경하게 되면 객체가 스스로의 일관성을 책임지기 어려워진다.</p>
<p>(특히나 python 에서도 이걸 좀 잘 생각해야 한다. 중요한 것들을 마냥 attribute 로 접근해서 휘젓고 다니지 못하게 해야 한다!)</p>
<h4 id="1-상태를-변경하지-않고-클라이언트에-너무-많은-정보를-노출하지-않는-게터">1. 상태를 변경하지 않고 클라이언트에 너무 많은 정보를 노출하지 않는 게터</h4>
<p><strong>CQS(Command-Query Separation)</strong> 원칙에 따르면, 메서드는 <em>명령(command)</em> 또는 <em>조회(query)</em> 중 하나만 수행해야 한다.  </p>
<ul>
<li>명령은 상태를 변경하지만 값을 반환하지 않아야 하고,  </li>
<li>쿼리는 값을 반환하지만 상태를 변경하지 않아야 한다.</li>
</ul>
<p>이 원칙을 지키면, 예측 가능한 인터페이스 설계가 가능해지고, 사이드 이펙트를 방지할 수 있다.<br>객체의 상태를 외부에서 해석하게 하지 말고, 그 의미를 추상화한 메서드를 제공하는 것이 바람직하다.
예를 들어 <code>hasAvailableSpots()</code>는 <code>availableSpots &gt; 0</code>이라는 내부 판단을 외부에 감추면서도 의미를 정확히 전달한다.</p>
<h4 id="2-세터는-객체를-설명하는-속성에만-사용한다">2. 세터는 객체를 설명하는 속성에만 사용한다</h4>
<p>객체의 핵심 상태나 일관성이 중요한 필드에는 세터를 제공해서는 안 된다.<br>대신, 의미 있는 메서드를 통해 <em>의도를 표현하고</em>, 내부에서 상태 변경과 유효성 검사를 함께 수행하는 방식이 좋다.<br>예를 들어, <code>setAvailableSpots(int)</code> 같은 메서드보다는 <code>addEmployee(Employee)</code>처럼 도메인 맥락에 맞는 행위 중심 메서드가 더 안전하고 명확하다.</p>
<h4 id="-객체-집단의-불변-조건을-보장하도록-애그리게이트를-모델링하라-">[ 객체 집단의 불변 조건을 보장하도록 애그리게이트를 모델링하라 ]</h4>
<p>객체 집단의 일관성을 보장하기 위해서는 <strong>애그리게이트</strong>라는 구조적 단위를 설계해야 한다. DDD에서 애그리게이트는 <strong>관련된 객체들을 하나로 묶고, 그 집단의 일관성을 유지하는 단위</strong>이며, 이 집단의 대표로서 <strong>애그리게이트 루트(aggregate root)</strong> 가 존재한다.</p>
<p>애그리게이트 루트는 다음과 같은 역할을 수행한다:</p>
<ul>
<li>클라이언트는 루트에만 접근할 수 있다.</li>
<li>루트 객체를 통해서만 내부 객체의 상태가 변경된다.</li>
<li>루트의 허가 없이는 하위 객체를 직접 변경할 수 없다.</li>
<li>모든 클라이언트는 애그리게이트 루트에 대한 참조만 유지해야 하며, 하위 객체에 대한 참조는 허용되지 않는다.</li>
</ul>
<p>이것은 단순한 <strong>캡슐화(encapsulation)</strong> 가 아니라, 도메인 모델에서 <strong>일관성(consistency)</strong> 을 강제하기 위한 핵심 원칙이다. </p>
<p>또한, 객체를 데이터베이스에 저장할 때도 <strong>루트 단위로 저장 및 조회</strong>해야 하며, 루트 객체마다 하나의 리포지터리(Repository) 또는 DAO(Data Access Object)가 있어야 한다.<br>내부 구성 객체가 아니라 루트를 중심으로 트랜잭션과 상태 변경이 관리되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fd53b5dd-8751-4795-93a1-5b5f1e88e318/image.png" alt=""></p>
<h4 id="1-애그리게이트-루트의-규칙을-깨지-마라">1. 애그리게이트 루트의 규칙을 깨지 마라</h4>
<p>실제 개발 과정에서는 하위 객체 중 하나만 빠르게 수정하고 싶어지는 유혹이 발생할 수 있다.
예를 들어:</p>
<ul>
<li>성능상의 이유로 루트를 통하지 않고 하위 객체에 직접 접근하고 싶을 때</li>
<li>사용하는 프레임워크나 라이브러리가 내부 객체에 직접 접근하게 만들 때</li>
<li>깊은 객체 트리를 구성한 경우, 루트를 통해 접근하면 보일러플레이트 코드가 많아지는 경우 등</li>
</ul>
<p>이런 경우에도 <strong>루트를 우회하지 말고, 설계 규칙을 우선 유지하는 것이 이상적</strong>이다. 그러나 저자는 여기서 질문을 던진다:</p>
<blockquote>
<p>&quot;이 규칙이 과연 불변의 진리인가?&quot;</p>
</blockquote>
<p>상황에 따라서는 <strong>성능, 단순성, 유지보수성 등을 이유로 트레이드오프가 필요할 수 있음</strong>을 인정하고, <strong>현실적인 타협점</strong>을 만드는 유연함도 중요하다고 지적한다.</p>
<h4 id="2-예제-offering-애그리게이트">2. 예제: Offering 애그리게이트</h4>
<pre><code class="language-python">class Offering:
    &quot;&quot;&quot;Represents a training offering with enrollment management&quot;&quot;&quot;

    def __init__(
        self,
        training: Training,
        date_: date,
        maximum_number_of_attendees: int,
    ):
        self._id: int = None
        self._training = training
        self._date = date_
        self._enrollments: List[Enrollment] = []  # 1
        self._maximum_number_of_attendees = maximum_number_of_attendees
        self._available_spots = maximum_number_of_attendees

    def enroll(self, employee: Employee) -&gt; None:  # 2
        &quot;&quot;&quot;Enroll an employee in the offering&quot;&quot;&quot;
        if not self.has_available_spots():
            raise OfferingIsFullException()

        now = date.today()
        self._enrollments.append(Enrollment(employee, now))
        self._available_spots -= 1

    def cancel(self, employee: Employee) -&gt; None:  # 3
        &quot;&quot;&quot;Cancel an employee&#39;s enrollment in the offering&quot;&quot;&quot;
        enrollment_to_cancel = self._find_enrollment_of(employee)
        if enrollment_to_cancel is None:
            raise EmployeeNotEnrolledException()

        now = date.today()
        enrollment_to_cancel.cancel(now)

        self._available_spots += 1

    def _find_enrollment_of(self, employee: Employee) -&gt; Optional[Enrollment]:  # 4
        &quot;&quot;&quot;Find the enrollment for a specific employee&quot;&quot;&quot;
        for enrollment in self._enrollments:
            if enrollment.employee == employee:
                return enrollment
        return None

    def has_available_spots(self) -&gt; bool:
        &quot;&quot;&quot;Check if there are available spots in the offering&quot;&quot;&quot;
        return self._available_spots &gt; 0

    @property
    def training(self) -&gt; Training:
        &quot;&quot;&quot;Get the training for this offering&quot;&quot;&quot;
        return self._training

    def is_employee_registered(self, employee: Employee) -&gt; bool:  # 5
        &quot;&quot;&quot;Check if an employee is registered for this offering&quot;&quot;&quot;
        return any(enrollment.employee == employee for enrollment in self._enrollments)</code></pre>
<p>이 Offering 클래스는 애그리게이트 루트로서 다음과 같은 역할을 수행하고 있다</p>
<ul>
<li>1) <code>enroll()</code>과 <code>cancel()</code>은 외부에서 호출되는 메서드이며, 내부 상태(<code>_enrollments</code>, <code>_available_spots</code>)를 직접 변경하지 못하게 하고 모든 로직을 루트 내부에서 처리한다.</li>
<li>2) 내부 데이터 구조(<code>List[Enrollment]</code>)에 직접 접근하지 못하게 <code>private</code> 속성으로 두고, 검색 및 조회는 루트 메서드를 통해 캡슐화한다.</li>
</ul>
<p>이처럼 애그리게이트 루트가 모든 상태 변경의 관문 역할을 하며, 전체 집합의 일관성을 책임진다.
그러나 이 구현 방식은 등록 목록을 순회하여 특정 직원의 등록 정보를 찾는 구조이기 때문에, 참가자 수가 많아질수록 성능 저하의 우려가 있다. (6장에서 다시 다룬다.)</p>
<h3 id="4장-의존성-관리하기">4장 의존성 관리하기</h3>
<p>소프트웨어에서 <strong>의존성(Dependency)</strong> 은 피할 수 없는 개념이다. 예컨대 서비스 클래스는 여러 레포지토리(repository)와 엔터티(entity)에 의존해서 동작한다. 이 말은 곧 다른 클래스와 “결합”된다는 의미다. </p>
<p>우리는 지금까지 설계 상 큰 클래스 대신 작은 클래스를 사용하고, <strong>하나의 클래스가 모든 일을 하지 않도록</strong> 신경 써 왔다. 이는 “단일 책임 원칙(SRP)”과도 연결된다. 따라서 어떤 클래스가 <strong>다른 클래스에 의존하는 것 자체는 바람직할 수 있다.</strong> 실제로 혼자 모든 걸 처리하려 하기보다 역할을 나누고 협력하는 것이 객체지향의 핵심이다.</p>
<blockquote>
<p>그러나 문제는 <strong>의존이 늘어날수록, 그 중 하나의 문제가 전체로 전파될 가능성도 함께 커진다</strong>는 점이다. 그래서 의존을 무분별하게 설정하면 안 된다. </p>
</blockquote>
<p>중요한 건, &quot;이 클래스가 어떤 클래스에 의존하는가?&quot;, &quot;그 의존이 정말 좋은 의존인가?&quot;, 이 두 가지 질문에서 의존성 관리는 출발한다.</p>
<h4 id="-고수준-코드와-저수준-코드를-분리하라-">[ 고수준 코드와 저수준 코드를 분리하라 ]</h4>
<p>의존성 관리에서 가장 먼저 떠올려야 할 개념은 바로 <strong>고수준(high-level)과 저수준(low-level)의 구분</strong>이다.</p>
<ul>
<li><strong>고수준 코드</strong>는 “무엇을 해야 하는가(what)”에 대해 설명한다.</li>
<li><strong>저수준 코드</strong>는 “어떻게 수행할 것인가(how)”에 대해 설명한다.</li>
<li><strong>고수준 코드를 먼저 읽으면 전체 기능이 어떤 역할을 하는지 빠르게 이해할 수 있으며</strong>, 저수준의 변경이 고수준에 영향을 주지 않도록 구조화할 수 있다.  </li>
<li>이는 <strong>추상화 수준의 차이</strong>로 설명된다. 고수준은 더 추상적이고, 따라서 더 안정적이다. 고수준이 다른 고수준에 의존한다면 변경으로 인한 영향이 적다.</li>
</ul>
<h4 id="-의존성-역전-원칙-dip-dependency-inversion-principle-">[ 의존성 역전 원칙 (DIP, Dependency Inversion Principle) ]</h4>
<p><strong>*&quot;세부사항에 의존하지 말고, 추상화에 의존하라.&quot;*</strong> 이 원칙은 단순히 “의존성을 뒤집어라”는 말이 아니다. 진짜 의미는 다음과 같다. </p>
<ul>
<li><strong>고수준 모듈은 저수준 모듈에 의존하지 말고, 공통된 추상화(인터페이스)에 의존하라.</strong></li>
<li><strong>저수준 모듈도 동일한 추상화에 의존하게 만들라.</strong></li>
</ul>
<p>이 원칙은 <a href="https://en.wikipedia.org/wiki/SOLID">Robert C. Martin의 SOLID 원칙</a> 중 하나이며, <strong><em>유지보수성</em></strong>과 <strong><em>확장성</em></strong>을 크게 높여준다.</p>
<p>이 책에서도 강조되지만, 스티브 프리먼과 냇 프라이스의 『테스트 주도 개발로 배우는 객체 지향 설계와 실천』에서는 <strong>인터페이스가 구조를 얼마나 유연하게 만들어주는지</strong> 아주 명확히 보여준다. 테스트가 용이한 구조로 잘 설계하고 싶다면, DIP와 인터페이스 활용을 잘하면 좋다.</p>
<h4 id="-고수준-코드와-저수준-코드를-분리하지-않아도-되는-경우-">[ 고수준 코드와 저수준 코드를 분리하지 않아도 되는 경우 ]</h4>
<p>모든 상황에서 고수준과 저수준을 무조건 분리할 필요는 없다.<br>예를 들어 고수준의 구현 세부 사항을 <strong>비공개 메서드(private method)</strong> 로 캡슐화할 수 있다면, 필요할 때 쉽게 내부로 이동 가능하다. <strong>리팩터링의 기회를 엿보면서 점진적으로 개선</strong>하면 된다.</p>
<p>근데 <strong><em>절대 섞지 말아야 할 것은 인프라 코드와 비즈니스 코드</em></strong> 이다. 예컨대 SQL 쿼리, HTTP 호출, 메시지 전송과 같은 인프라스트럭처 세부 구현은 비즈니스 코드와 <strong>절대 한데 섞이면 안 된다.</strong>  </p>
<p>비즈니스 로직이 외부 시스템에 직접 의존하게 되면 <strong>테스트도 어렵고, 재사용도 어렵고, 변경에도 취약</strong>해진다.</p>
<h4 id="-예제-메시지-처리-작업-">[ 예제: 메시지 처리 작업 ]</h4>
<p><em>(나의 깃허브 레포 기준, <code>python/ch4/v1/message_sender.py</code> 위치, <code>MessageSender</code>)</em></p>
<pre><code class="language-python">class MessageSender:
    &quot;&quot;&quot;Service for sending messages&quot;&quot;&quot;

    def __init__(
        self,
        bot: Bot,
        user_directory: UserDirectory,
        repository: MessageRepository,
    ):
        self._bot = bot
        self._user_directory = user_directory
        self._repository = repository

    def send_messages(self) -&gt; None:
        &quot;&quot;&quot;Send all messages that need to be sent&quot;&quot;&quot;
        messages_to_be_sent = self._repository.get_messages_to_be_sent()
        for message_to_be_sent in messages_to_be_sent:  # 1
            user_id = self._user_directory.get_account(message_to_be_sent.email)  # 2
            self._bot.send_private_message(
                user_id,
                message_to_be_sent.body_in_markdown,
            )  # 3
            message_to_be_sent.mark_as_sent()  # 4</code></pre>
<p>이 <code>MessageSender</code> 클래스는 <strong>충분히 고수준</strong>이다.</p>
<ul>
<li><code>Bot</code>, <code>UserDirectory</code>, <code>MessageRepository</code>라는 <strong>인터페이스에만 의존</strong>하고 있다.</li>
<li>이 인터페이스의 <strong>구체 구현체(저수준 클래스)</strong> 는 다른 곳에 있다.</li>
<li>이 구조 덕분에, 세부 구현이 바뀌더라도 <code>MessageSender</code>의 로직은 그대로 유지될 수 있다.</li>
<li><code>MessageSender</code>는 자신의 <strong>“관심사”에 집중</strong>할 수 있고, 각 협력자들이 “어떻게” 일하는지는 알 필요조차 없다.</li>
</ul>
<h4 id="-불필요한-세부-사항이나-요소에-의존하는-것을-피하라-">[ 불필요한 세부 사항이나 요소에 의존하는 것을 피하라 ]</h4>
<p>복잡한 시스템일수록 <em>정보 은닉(information hiding)</em> 의 중요성이 커진다. </p>
<p>여기서 말하는 정보 은닉이란 단지 “숨기자”는 차원을 넘어, <strong><em>변화 가능성이 높은 요소와 그렇지 않은 요소를 구분해 의존 구조를 설계하는 것이다.</em></strong> 핵심은 어떤 요소가 바뀌더라도 다른 구성 요소에 파급효과를 일으키지 않도록 만드는 것. 이를 위해선 &#39;의존성을 최소화하고, 꼭 필요한 것에만 의존하도록&#39; 제한하는 설계가 필수적이다.</p>
<h4 id="1-여러분이-소유한-클래스만-요구하거나-반환하라">1. 여러분이 소유한 클래스만 요구하거나 반환하라</h4>
<ul>
<li><p>여기서 ‘여러분이 소유한 클래스’란 <strong>도메인 모델에 속하며, 여러분이 직접 통제하고 수정할 수 있는 클래스</strong>를 의미한다. 반대로, 외부 라이브러리나 SDK 등은 여러분의 코드 바깥에 있으며, 변경을 예측하기 어렵고 주기적으로 업데이트되는 요소들이다.</p>
</li>
<li><p>예를 들어 채팅 도구 SDK를 도입한다고 할 때, 이 SDK가 제공하는 클래스를 그대로 코드 전반에 퍼뜨리면, SDK의 버전이 바뀔 때마다 전방위적으로 코드를 수정해야 할 수도 있다. 즉, <strong>SDK에 정의된 클래스를 직접적으로 전달하거나 반환하는 순간, 강결합이 시작되는 것</strong>이다.<br>이러한 강결합을 피하기 위해선 <strong>외부 의존성과 도메인 모델 사이에 적절한 추상화 계층을 두는 것</strong>이 중요하다.</p>
</li>
</ul>
<h4 id="2-클라이언트에게-필요한-것-이상을-제공하지-마라">2. 클라이언트에게 필요한 것 이상을 제공하지 마라</h4>
<ul>
<li><p>하나의 도메인 엔티티를 여러 곳에서 재사용하는 것은 자연스럽고 흔한 일이다. 하지만 문제는 <strong>엔티티 전체를 노출할 때 발생한다.</strong>  </p>
</li>
<li><p>예컨대, 어떤 클라이언트는 단지 이름과 이메일만 필요로 하지만, 전체 엔티티를 전달받게 되면, 그 엔티티의 어떤 속성이든 변경될 가능성을 갖는다. 이로 인해 의도치 않은 변경 전파와 데이터 노출 문제가 발생할 수 있다.</p>
</li>
<li><p>따라서, <strong>엔티티 전체가 아닌 필요한 정보만 제공하는 것이 중요하며</strong>, 이를 실현하기 위한 대표적인 방법이 바로 <strong>클라이언트 요청과 엔티티를 분리하고, 정보를 추상화하는 것</strong>이다. DTO(Data Transfer Object)나 응답 전용 뷰 모델 같은 것들이 이에 해당한다.</p>
</li>
</ul>
<h4 id="-너무-많은-클래스에-의존하는-클래스를-분리하라-">[ 너무 많은 클래스에 의존하는 클래스를 분리하라 ]</h4>
<p>예제: MessageSender 서비스 분리하기, <em>(나의 깃허브 레포 기준, <code>python/ch4/v2/message_sender.py</code> 위치, <code>MessageSender</code>)</em></p>
<pre><code class="language-python">class MessageSender:
    &quot;&quot;&quot;Service for sending messages via multiple channels&quot;&quot;&quot;

    def __init__(
        self,
        bot: Bot,
        user_directory: UserDirectory,
        repository: MessageRepository,
        email_sender: EmailSender,  # 1
        user_prefs: UserPreferences,  # 1
    ):
        self._bot = bot
        self._user_directory = user_directory
        self._repository = repository
        self._email_sender = email_sender
        self._user_prefs = user_prefs

    def send_messages(self) -&gt; None:
        &quot;&quot;&quot;Send all messages that need to be sent&quot;&quot;&quot;
        messages_to_be_sent = self._repository.get_messages_to_be_sent()
        for message_to_be_sent in messages_to_be_sent:
            user_id = self._user_directory.get_account(message_to_be_sent.email)
            self._bot.send_private_message(user_id, message_to_be_sent.body_in_markdown)
            if self._user_prefs.send_via_email(message_to_be_sent.email):  # 2
                self._email_sender.send_message(message_to_be_sent)
            # 메시지를 보낸 것으로 표시한다
            message_to_be_sent.mark_as_sent()</code></pre>
<ul>
<li>이제 추가로, 이메일을 보내고싶어 <code>EmailSender</code> 추가했고, 사용자가 수신 가능한지 체크하기 위해 <code>UserPreferences</code> 추가했다.</li>
<li>이메일 발송 관련 로직이나 사용자 선호 설정이 변경되면 <code>MessageSender</code>도 변경해야 하며, 이는 <strong>SRP(Single Responsibility Principle)</strong>를 위반한 셈이다.</li>
</ul>
<pre><code class="language-python">class MessageBot:
    &quot;&quot;&quot;Handles sending messages through a bot interface&quot;&quot;&quot;

    def __init__(
        self,
        bot: Bot,
        user_directory: UserDirectory,
    ):
        self._bot = bot
        self._user_directory = user_directory

    def send(self, msg: Message) -&gt; None:  # 2
        &quot;&quot;&quot;Send a message to a user via the bot&quot;&quot;&quot;
        user_id = self._user_directory.get_account(msg.email)
        self._bot.send_private_message(user_id, msg.body_in_markdown)</code></pre>
<ul>
<li><code>Bot</code>과 <code>UserDirectory</code>를 묶어 새로운 클래스로 만들어서 분리한다면?!</li>
<li>이렇게 설계를 변경하면, <code>MessageSender</code>는 더 이상 <em>모든 세부 구현</em>에 직접 관여하지 않고, <strong>간접 결합된 협력자(MessageBot)</strong>를 통해 메시지를 발송하게 된다. 최종적으로 리팩터링된 <code>MessageSender</code>는 다음과 같다:</li>
</ul>
<pre><code class="language-python">class MessageSender:
    &quot;&quot;&quot;
    Service for sending messages via multiple channels, utilizing a MessageBot.
    &quot;&quot;&quot;

    def __init__(
        self,
        message_bot: MessageBot,
        repository: MessageRepository,
        email_sender: EmailSender,  # 1
        user_prefs: UserPreferences,  # 1
    ):
        self._message_bot = message_bot
        self._repository = repository
        self._email_sender = email_sender
        self._user_prefs = user_prefs

    def send_messages(self) -&gt; None:
        &quot;&quot;&quot;Send all messages that need to be sent.&quot;&quot;&quot;
        messages_to_be_sent = self._repository.get_messages_to_be_sent()
        for message_to_be_sent in messages_to_be_sent:
            self._message_bot.send(message_to_be_sent)
            if self._user_prefs.send_via_email(message_to_be_sent.email):  # 2
                self._email_sender.send_message(message_to_be_sent)
            # 메시지를 보낸 것으로 표시한다
            message_to_be_sent.mark_as_sent()</code></pre>
<ul>
<li>의존성이 간결해지고, 각 클래스는 자신의 책임에 충실해진다. 이 구조는 테스트 가능성과 변경 대응력을 높여주는 좋은 예시다.</li>
</ul>
<h4 id="-의존성을-주입하라의존성-주입을-사용하라-">[ 의존성을 주입하라(의존성 주입을 사용하라) ]</h4>
<p><strong>의존성 주입(DI)</strong>은 객체가 협력자(다른 객체)를 직접 생성하지 않고, 외부로부터 주입받는 설계 기법이다. 이를 통해 <em>디자인의 유연성</em>과 <em>테스트의 용이성</em>을 동시에 얻을 수 있다.</p>
<p>과거에는 객체 생성 비용이나 성능 문제로 인해 DI를 꺼리는 경우도 있었지만, 이제는 런타임 객체 관리 및 메모리 관리 기술이 발전했기 때문에, <strong><em>주입 가능한 구조를 채택하는 것이 더 나은 선택이 되었다.</em></strong></p>
<h4 id="1-상태를-변경하는-작업에-정적-메서드를-사용하지-마라">1. 상태를 변경하는 작업에 정적 메서드를 사용하지 마라</h4>
<ul>
<li>정적 메서드는 <em>런타임 시점에 대체하거나 모킹(mocking)</em>하기가 어렵기 때문에 테스트가 불가능한 구조가 된다. 상태 변경을 수반하는 로직일수록 정적 메서드를 피하고, 명시적인 객체와 메서드를 통해 의존성을 표현해야 한다.</li>
</ul>
<h4 id="2-항상-협력자를-주입하라-그-외에는-원하는-대로-하라">2. 항상 협력자를 주입하라: 그 외에는 원하는 대로 하라</h4>
<ul>
<li>위 예시에서 <code>MessageSender</code>는 <code>Bot</code>, <code>UserDirectory</code>, <code>MessageRepository</code>를 사용한다. 이들을 직접 생성하지 않고 외부에서 주입받으면, 다양한 구현체(Mock, Stub, Spy 등)로 교체가 가능해진다.  </li>
<li>즉, <strong>협력자를 명시적으로 받고, 사용자는 인터페이스나 추상 타입에 의존하도록 설계하라.</strong></li>
</ul>
<h4 id="3-클래스와-의존성을-함께-생성하는-전략">3. 클래스와 의존성을 함께 생성하는 전략</h4>
<ul>
<li>복잡한 의존 그래프를 가진 시스템에서는 객체 생성 시점에 많은 고민이 필요하다. 이를 효율적으로 처리하기 위해선 <strong>DI 컨테이너나 프레임워크(SPRING, Google Guice 등)</strong>를 사용하는 것이 유리하다.  </li>
<li>현대적인 프레임워크는 대부분 DI 설계를 기본 전제로 두고 있기 때문에, 이런 흐름을 따르는 것이 유지보수와 확장성 면에서 안정적인 선택이다.</li>
</ul>
<h3 id="5장-추상화-잘-디자인하기">5장 추상화 잘 디자인하기</h3>
<p><a href="https://ko.wikipedia.org/wiki/%EC%97%90%EC%B8%A0%ED%97%88%EB%A5%B4_%EB%8D%B0%EC%9D%B4%ED%81%AC%EC%8A%A4%ED%8A%B8%EB%9D%BC">에츠허르 데이크스트라 왈</a>, <strong>*<span style="color: rgb(99 148 255);">&quot;추상화는 모호하다는 것과 본질적으로 다르다. 추상화의 목적은 모호해지는 것이 아니라, 절대적으로 정확한 새로운 의미 수준을 만드는 것&quot;</span>*</strong> (The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise)</p>
<p>이 말은 이 장 전체의 핵심을 가장 명확히 설명해준다. 추상화란 복잡한 구현을 감추는 것이 아니라, <strong>의미를 더 정확히 드러내기 위해 불필요한 세부사항을 제거하는 것</strong>이다. 즉, <strong>본질에 집중하고, 비본질을 과감히 덜어내는 행위</strong>가 추상화의 본질이다.</p>
<ul>
<li>추상화는 개념, 기능, 프로세스를 클라이언트가 내부 메커니즘을 몰라도 이해 가능한 방식으로 설명한다.</li>
<li>본질적인 특성에 집중하고, 구현은 신경 쓰지 않으며 신경 쓸 필요도 없다.</li>
</ul>
<h4 id="-추상화와-확장-지점을-디자인하라-">[ 추상화와 확장 지점을 디자인하라 ]</h4>
<p>추상화는 단순히 복잡성을 숨기는 도구가 아니라, <strong>변화에 유연하게 대응할 수 있도록 구조를 여유 있게 설계하는 기반</strong>이 된다. 즉, 새로운 기능을 쉽게 추가하거나 기존 기능을 확장하거나 변경할 수 있도록 만든다.</p>
<h4 id="1-추상화의-필요성-식별하기">1. 추상화의 필요성 식별하기</h4>
<p>추상화는 멋있어 보인다고 도입해서는 안 된다. <strong>불필요한 추상화는 오히려 복잡도를 증가시킬 뿐</strong>이다. 추상화를 적용하기 전에는 반드시 ‘그럴만한 이유’가 있어야 한다.</p>
<blockquote>
<p><strong>✔ 대표적 원칙: 추상화는 변화의 방향성이 명확할 때만 도입하라.</strong><br>참고: <a href="https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/">Robert C. Martin (Clean Architecture)</a>에서는 “변경의 이유가 둘 이상일 때 분리하라”는 단일 책임 원칙(SRP) 아래, <strong>변경 가능성이 존재하는 지점에 추상화를 도입해야 한다</strong>고 강조한다. 이처럼 추상화는 목적 있는 복잡성이다.</p>
</blockquote>
<h4 id="2-확장-지점-디자인하기">2. 확장 지점 디자인하기</h4>
<p>확장 지점을 설계할 때는 단순히 인터페이스를 나누는 것 이상을 고려해야 한다.  </p>
<ul>
<li>향후 어떤 부분이 가장 자주 변경될 것인가?</li>
<li>어떤 방식으로 확장될 가능성이 높은가?</li>
</ul>
<p>이런 질문에 대한 답을 바탕으로 <strong>유연한 구조를 미리 설계</strong>해야 한다.<br>잘 설계된 확장 지점은 이후 변화에 따라 <strong>조건문이 아니라 클래스 추가</strong>로 대응할 수 있게 만들어준다.</p>
<h4 id="3-좋은-추상화의-속성">3. 좋은 추상화의 속성</h4>
<p>좋은 추상화는 <strong>무엇(What)과 어떻게(How)를 분리</strong> 한다.  </p>
<ul>
<li>클라이언트는 “무엇을 할 수 있는가”만 알면 된다.  </li>
<li>“어떻게 수행되는가”는 감춰져도 무방하다.</li>
</ul>
<h4 id="4-추상화에서-배워라">4. 추상화에서 배워라</h4>
<p>추상화는 한 번에 완벽히 만드는 것이 아니다. <strong>점진적으로 발전시키며 리팩터링을 반복해야 한다.</strong>  꾸준함이 결국 좋은 추상화로 이어진다.</p>
<h4 id="5-추상화에-대해-배워라">5. 추상화에 대해 배워라</h4>
<p>추상화를 더 잘 다루고 싶다면, 디자인 패턴은 훌륭한 교과서다.  </p>
<ul>
<li><strong>GoF 디자인 패턴</strong>은 다양한 추상화 사례를 담고 있다.<br>특히 전략, 상태, 책임 연쇄, 데코레이터, 템플릿 메서드, 커맨드 패턴 등은 <strong>행위의 변화와 확장</strong>을 위한 추상화 관점에서 매우 유용하다.</li>
<li>또한, <strong>오픈 소스 프레임워크</strong>는 실전에서 적용된 추상화의 정수다. 그 패턴과 모듈화를 분석하며 배우는 것이 가장 효과적이다.</li>
</ul>
<h4 id="6-추상화와-결합">6. 추상화와 결합</h4>
<p>추상화와 결합도는 함께 고려되어야 한다.  </p>
<ul>
<li><strong>추상화는 결합도를 낮추고 유연성을 높이는 도구</strong>이지만, 잘못하면 <strong>불필요한 의존성과 인터페이스 남용으로 결합이 오히려 증가</strong>할 수도 있다.</li>
<li>즉, <strong>&quot;적절한 추상화 + 낮은 결합도&quot;</strong>가 목표다.</li>
</ul>
<h4 id="-예제-직원에게-배지-수여하기-">[ 예제: 직원에게 배지 수여하기 ]</h4>
<p>(<code>python/ch5/v1/badge_giver.py</code> 의 <code>BadgeGiver</code>)</p>
<pre><code class="language-python">class BadgeGiver:
    &quot;&quot;&quot;Assigns badges to employees based on their training history&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; None:  # 1
        &quot;&quot;&quot;Give badges to the employee&quot;&quot;&quot;
        self._per_training(employee)
        self._per_quantity(employee)

    def _per_training(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Assigns badges based on specific training completions&quot;&quot;&quot;
        trainings_taken: TrainingsTaken = employee.trainings_taken
        # 품질 관련 교육을 받은 경우 배지를 받는다     # 2
        if trainings_taken.has(&quot;TESTING&quot;) and trainings_taken.has(&quot;CODE QUALITY&quot;):
            self._assign(employee, Badge.QUALITY_HERO)
        # 보안 관련 교육을 모두 들으면 배지를 받는다
        if trainings_taken.has(&quot;SECURITY 101&quot;) and trainings_taken.has(&quot;SECURITY FOR MOBILE DEVS&quot;):
            self._assign(employee, Badge.SECURITY_COP)
        # ... 다른 배치 수여 규칙들

    def _per_quantity(self, employee: Employee) -&gt; None:  # 3
        &quot;&quot;&quot;Assigns badges based on the quantity of trainings completed&quot;&quot;&quot;
        trainings_taken: TrainingsTaken = employee.trainings_taken
        if trainings_taken.total_trainings() &gt;= 5:
            self._assign(employee, Badge.FIVE_TRAININGS)
        if trainings_taken.total_trainings() &gt;= 10:
            self._assign(employee, Badge.TEN_TRAININGS)
        if trainings_taken.trainings_in_past_3_months() &gt;= 3:
            self._assign(employee, Badge.ON_FIRE)

    def _assign(self, employee: Employee, badge: Badge) -&gt; None:
        &quot;&quot;&quot;Assign a badge to the employee&quot;&quot;&quot;
        employee.win_badge(badge)</code></pre>
<ul>
<li><code>Employee</code> 에게 다양한 규칙에 따라 뱃지를 수여하는 <code>class</code></li>
<li>로직이 명확히 나뉘지 않아 변경에 취약하며, 로직 복잡성이 커지면 메서드 내부가 조건문으로 난잡해질 가능성 존재. 일단 class 단위를 쪼개보자.</li>
</ul>
<p>(<code>python/ch5/v2/badge_giver.py</code> 파일 내부 class 들)</p>
<pre><code class="language-python">class BadgeGiver:
    &quot;&quot;&quot;Assigns badges to employees based on their training history&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; None:  # 1
        &quot;&quot;&quot;Give badges to the employee by applying different badge rules&quot;&quot;&quot;
        BadgesForTraining().give(employee)
        BadgesForQuantity().give(employee)


class BadgesForTraining:
    &quot;&quot;&quot;Applies badge rules related to specific training completions&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Assigns badges based on specific training completions&quot;&quot;&quot;
        trainings_taken: TrainingsTaken = employee.trainings_taken
        ... 생략 ...


class BadgesForQuantity:
    &quot;&quot;&quot;Applies badge rules related to the quantity of trainings completed&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; None:  # 3
        ... 생략 ...</code></pre>
<ul>
<li>로직이 명확하게 나뉘며, 각 책임 클래스에서의 단위 변경이 쉬움</li>
<li>하지만 여전히 로직이 많아지고 규칙이 다양해지면 복잡도는 상승</li>
</ul>
<pre><code class="language-python">class Badge(Enum):
    &quot;&quot;&quot;Enum representing different types of badges&quot;&quot;&quot;

    SECURITY_COP = &quot;SECURITY_COP&quot;
    FIVE_TRAININGS = &quot;FIVE_TRAININGS&quot;
    TEN_TRAININGS = &quot;TEN_TRAININGS&quot;
    ON_FIRE = &quot;ON_FIRE&quot;
    QUALITY_HERO = &quot;QUALITY_HERO&quot;

class BadgeRule:
    &quot;&quot;&quot;Interface for a badge rule&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        &quot;&quot;&quot;Determines if the badge should be given to the employee&quot;&quot;&quot;
        pass

    def badge_to_give(self) -&gt; Badge:
        &quot;&quot;&quot;Returns the badge associated with this rule&quot;&quot;&quot;
        pass


class QualityHero(BadgeRule):
    &quot;&quot;&quot;Badge rule for Quality Hero badge&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        trainings_taken: TrainingsTaken = employee.trainings_taken
        return trainings_taken.has(&quot;TESTING&quot;) and trainings_taken.has(&quot;CODE QUALITY&quot;)

    def badge_to_give(self) -&gt; Badge:
        return Badge.QUALITY_HERO


class SecurityCop(BadgeRule):
    &quot;&quot;&quot;Badge rule for Security Cop badge&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        trainings_taken: TrainingsTaken = employee.trainings_taken
        return trainings_taken.has(&quot;SECURITY 101&quot;) and trainings_taken.has(&quot;SECURITY FOR MOBILE DEVS&quot;)

    def badge_to_give(self) -&gt; Badge:
        return Badge.SECURITY_COP


class FiveTrainings(BadgeRule):
    &quot;&quot;&quot;Badge rule for Five Trainings badge&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        trainings_taken: TrainingsTaken = employee.trainings_taken
        return trainings_taken.total_trainings() &gt;= 5

    def badge_to_give(self) -&gt; Badge:
        return Badge.FIVE_TRAININGS


class TenTrainings(BadgeRule):
    &quot;&quot;&quot;Badge rule for Ten Trainings badge&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        trainings_taken: TrainingsTaken = employee.trainings_taken
        return trainings_taken.total_trainings() &gt;= 10

    def badge_to_give(self) -&gt; Badge:
        return Badge.TEN_TRAININGS


class OnFire(BadgeRule):
    &quot;&quot;&quot;Badge rule for On Fire badge&quot;&quot;&quot;

    def give(self, employee: Employee) -&gt; bool:
        trainings_taken: TrainingsTaken = employee.trainings_taken
        return trainings_taken.trainings_in_past_3_months() &gt;= 3

    def badge_to_give(self) -&gt; Badge:
        return Badge.ON_FIRE</code></pre>
<ul>
<li><code>BadgeRule</code> 을 추상화 해서 (python interface 없기에 ABC 활용)</li>
<li>ENUM 도 활용했고, 결국 <code>BadgeGiver</code> 자체는 <code>BadgeRule</code> 에 따라 <code>give</code> 가 <code>true</code> 라면 <code>badge_to_give</code> 로 수여만 하면 된다. </li>
</ul>
<pre><code class="language-python">
class BadgeGiver:
    &quot;&quot;&quot;Assigns badges to employees based on a list of rules&quot;&quot;&quot;

    def __init__(self, rules: List[BadgeRule]):  # 1
        self._rules = rules

    def give(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Applies each rule to the employee and assigns badges accordingly&quot;&quot;&quot;
        for rule in self._rules:  # 2
            if rule.give(employee):
                employee.win_badge(rule.badge_to_give())</code></pre>
<ul>
<li><code>BadgeGiver</code> 는 오직 룰을 적용하는 컨트롤러 역할만 수행하며 <code>OCP (Open-Closed Principle)</code> 를 만족시킴!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f39f7833-2126-450f-b532-b720d445355f/image.png" alt=""></p>
<ul>
<li>이 다음 <code>BadgesForTraining</code> 자체를 팩토리 패턴과 <code>BadgeRule</code> 인터페이스 상속을 구현한 예제가 이어짐. 이는 책에서 확인하는 것을 추천.</li>
</ul>
<h4 id="-단순한-추상화를-선호하라-">[ 단순한 추상화를 선호하라 ]</h4>
<h4 id="1-경험적-규칙">1. 경험적 규칙</h4>
<ul>
<li>추상화가 꼭 필요한가? → 단순한 것이 항상 낫다.</li>
<li>추상화의 필요성이 명백하게 드러나는가? → 이쯤 되면 고려해야 한다.</li>
<li>좋은 추상화가 필요하게 되리라는 걸 안다면? → 처음부터 추상화를 두려워 말라.</li>
</ul>
<h4 id="2-단순한-것이-항상-더-낫다">2. 단순한 것이 항상 더 낫다</h4>
<ul>
<li>추상화는 도구이지 목표가 아니다.</li>
<li>단순함을 추구하다 보면 불필요한 추상화를 피하고, 꼭 필요한 순간에만 의미 있는 추상화를 만들 수 있다.</li>
<li>if 구문으로 가득찬 코드, 클리스 등. 더욱이 &quot;잘못된 추상화 보다 중복이 더 저렴하다&quot; 라고 말한 사람도 있다(루비 개발자, 샌디 메츠). 하지만 중복은 잘된 추상화 보다 비용이 더 많이 든다는 점도 아주 중요한 시사점이다.</li>
</ul>
<h4 id="3-이쯤-되면-추상화를-고려해야-한다">3. 이쯤 되면 추상화를 고려해야 한다</h4>
<ul>
<li>같은 클래스를 계속 반복적으로 수정하는가?</li>
<li>클래스가 계속 커지는 가?</li>
<li>변화를 구현하기 위해 if 조건문이 추가 되는가?</li>
<li>기존 비즈니스 규칙을 시스템의 다른 부분에 결합시키는 과정이 어거지로 이어붙이는 것 같은가?</li>
</ul>
<h4 id="4-처음부터-추상화를-모델링하는-것을-두려워하지-마라">4. 처음부터 추상화를 모델링하는 것을 두려워하지 마라</h4>
<ul>
<li>추상화를 사후에 도입하려 하면 더 큰 비용이 든다. 처음부터 확장성을 고려한 모델링은 오히려 단순함을 보장해준다.</li>
<li>단, 미리 설계하되, 지금 필요한 만큼만 구현하라. 추상화를 위한 추상화는 독이다.</li>
</ul>
<h3 id="6장-외부-의존성과-인프라-다루기">6장 외부 의존성과 인프라 다루기</h3>
<h4 id="-도메인-코드와-인프라를-분리하라-">[ 도메인 코드와 인프라를 분리하라 ]</h4>
<p>인프라와 도메인 코드를 분리하는 이유는 단순히 아키텍처적 우아함을 위한 것이 아니다. 실제로 <strong>외부의 세부 사항이 시스템 전반에 영향을 미치지 않도록 하기 위해</strong>, 그리고 <strong>테스트 가능성을 높이고 유지보수성을 확보하기 위해</strong> 반드시 필요하다.</p>
<ol>
<li><p><strong>외부 세부 사항은 테스트를 어렵게 한다.</strong><br>예컨대 AWS SDK, 타사 데이터베이스 클라이언트처럼 외부와 직접 통신하는 코드가 프로젝트 곳곳에 퍼져 있다면, 테스트 시에는 이를 일일이 mocking 해야 하며, 환경이 조금만 바뀌어도 문제가 생길 수 있다.</p>
</li>
<li><p><strong>캡슐화 없이는 외부 라이브러리의 변경에도 영향을 받는다.</strong><br>외부 API는 버전이 올라가며 추상화가 바뀌고, 리턴 구조가 달라질 수 있다. 이 변화가 전파되지 않도록 하려면 중간에 캡슐화 계층이 필요하다.</p>
</li>
<li><p><strong>인프라 코드는 저수준이다.</strong><br>DB, 메시지 큐, 외부 API 등은 시스템 아키텍처의 하부를 구성하며, 이들이 변경되었을 때 영향도를 최소화하려면 추상화와 분리가 선행되어야 한다.</p>
</li>
</ol>
<blockquote>
<p>잘 된 추상화는 인프라 세부 사항을 감추고, 시스템의 다른 부분이 변경 없이 그대로 동작할 수 있도록 해준다. 그러나 DBMS → SQS 로 바뀌는 수준의 변화까지 무리 없이 감추기란 어렵다.   </p>
</blockquote>
<blockquote>
<p>결국 <strong>어느 수준까지 분리할 것인가, 어떤 방식으로 분리할 것인가에 따라 트레이드오프</strong>가 필연적으로 존재한다.</p>
</blockquote>
<h4 id="1-인터페이스가-필요한가">1. 인터페이스가 필요한가?</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/744e0232-bcd8-4d99-a1fb-e32b83d718a3/image.png" alt=""></p>
<p>다음 조건 중 하나라도 해당된다면 인터페이스를 도입하자!</p>
<ul>
<li><p><strong>동일 인프라에 대해 여러 구현이 예상될 경우</strong><br>예: <code>EmployeeRepository</code>를 RDB, NoSQL, Mock 등 다양한 방식으로 구현</p>
</li>
<li><p><strong>인프라 구조에 대한 지식이 아직 부족할 경우</strong><br>추상적인 인터페이스를 만들고 구체 구현은 나중에 교체 가능</p>
</li>
<li><p><strong>여러 군데에서 동일 인프라를 사용하는데, 공통화되지 않은 경우</strong><br>인터페이스를 도입하면 무거운 클래스 대신 가벼운 추상 계층으로 관리 가능</p>
</li>
</ul>
<p>하지만 주의할 점은, <strong>기저 인프라가 복잡할수록 세부사항이 누출되기 쉽고, 이를 막는 것도 쉽지 않다는 점</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4dc16e4d-c993-419b-8f2a-9ee521f78fc3/image.png" alt=""></p>
<h4 id="2-코드에서는-세부-사항을-숨기고-개발자에게는-숨기지-마라">2. 코드에서는 세부 사항을 숨기고, 개발자에게는 숨기지 마라</h4>
<p>외부에 공개되는 코드 수준에서는 인프라의 디테일을 철저히 숨겨야 한다. 하지만 <strong>팀 내부나 문서에서는 그 디테일을 명확히 공유하고, 숨기지 않아야 한다.</strong> 그래야 변경 시 충돌과 오해가 줄어든다.</p>
<p>즉 개발자에게 까지 제발 숨기지 말라는 것이다. 이게 문서화를 통해서든, 가이드를 통해서든 말이다. </p>
<h4 id="3-인프라-변경하기-괜한-걱정일까-실제로-일어날까">3. 인프라 변경하기: 괜한 걱정일까, 실제로 일어날까?</h4>
<p>아주 솔직하게 DBMS 종류가 바뀌는 사건들은 마냥 흔한 일은 아니다. 하지만 인프라 변경은 현실이다. 단일 서버에서 RDS로, MySQL에서 Postgres로, 자체 이메일 시스템에서 SES로… 등의 변화는 &quot;생각 보다 매우 빈번&quot; 하다. </p>
<p>더욱이 서비스가 성장한다면 말이다. <strong>&quot;변경은 없을 것이다&quot;는 단순한 낙관주의</strong>다. 따라서 변경 가능성을 가정하고 구조를 짜야 한다.</p>
<h4 id="4-예제-데이터베이스-접근과-메시지-봇">4. 예제: 데이터베이스 접근과 메시지 봇</h4>
<pre><code class="language-python">class EmployeeRepository(ABC):
    &quot;&quot;&quot;Abstract repository interface for Employee operations&quot;&quot;&quot;

    @abstractmethod
    def find_by_email(self, email: str) -&gt; Optional[Employee]:
        &quot;&quot;&quot;Find employee by email address&quot;&quot;&quot;
        pass

    @abstractmethod
    def save(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Save employee to repository&quot;&quot;&quot;
        pass

    @abstractmethod
    def update(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Update existing employee in repository&quot;&quot;&quot;
        pass


class HibernateEmployeeRepository(EmployeeRepository):
    &quot;&quot;&quot;Employee repository implementation simulating Hibernate interaction&quot;&quot;&quot;

    def __init__(self, session: MockSession):
        self._cache: Cache[Employee, str] = Cache()
        self._session = session

    def find_by_id(self, id_: int) -&gt; Optional[Employee]:
        &quot;&quot;&quot;Find an employee by their ID&quot;&quot;&quot;
        return self._session.find(Employee, id_)

    def find_by_last_name(self, last_name: str) -&gt; Set[Employee]:  # 2
        &quot;&quot;&quot;Find employees by their last name, using cache&quot;&quot;&quot;
        if not self._cache.contains(last_name):
            # Simulate database query
            result_list = self._session.create_query(
                &quot;from Employee e where e.lastName = :lastName&quot;, Employee
            ).set_parameter(&quot;:lastName&quot;, last_name).get_result_list()
            self._cache.add_all(last_name, set(result_list))
        return self._cache.get(last_name)

    def find_by_email(self, email: str) -&gt; Optional[Employee]:
        &quot;&quot;&quot;Find an employee by their email address&quot;&quot;&quot;
        return self._session.create_query(
            &quot;from Employee e where e.email = :email&quot;, Employee
        ).set_parameter(&quot;:email&quot;, email).get_single_result_or_null()

    def save(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Save a new employee&quot;&quot;&quot;
        self._session.persist(employee)

    def update(self, employee: Employee) -&gt; None:
        &quot;&quot;&quot;Update an existing employee&quot;&quot;&quot;
        self._session.merge(employee)</code></pre>
<ul>
<li>이 예제는 <code>EmployeeRepository</code> 라는 추상 계층을 통해 도메인과 인프라를 분리하고 있다.</li>
<li>특정 로직에만 <code>cache</code> 를 추가하고 싶은 경우, <code>HibernateEmployeeRepository</code> 내에서 <code>_cache</code> 필드만 추가(활용)하면 된다.</li>
<li>사실 요즘 완벽한 도메인 분리는 못해도 무조건 <code>layered pattern</code> 까지는 지키려고 노력하는데, 이도 비슷한 궤</li>
</ul>
<h4 id="-인프라를-최대한-활용하라-">[ 인프라를 최대한 활용하라 ]</h4>
<p>요즘 당연하게 데이터 영속성을 위해 DBMS 를 사용하고, API를 효율적으로 만들기 위해 웹F/W 에 의존한다.
그리고 최근 몇십 년 동안 폭발적으로 발전해왔다. <strong>*<span style="color: rgb(99 148 255);">근데 class 디자인에 맞지 않는다는 이유로 기능 무시하는 것은 아까운 일</span>*</strong> 이다. </p>
<h4 id="1-디자인을-망가뜨리지-않도록-최선을-다하라">1. 디자인을 망가뜨리지 않도록 최선을 다하라</h4>
<ul>
<li><p>디자인에서 모든 선택에 트레이드오프 관계가 있다. 해당 예제 등록 취소하기는 에그리게이트를 우회하려는 것에 대해 다룬다.</p>
</li>
<li><p>특히 <code>cancel</code> 이라는 method 는 오퍼링 내의 모든 등록 내용 확인하는 로직이 있었기 때문
순회를 하지말고, 여기서 DBMS 를 활용해 직접 등록 정보를 다 가져와서 cancel 처리하게 하면 됨. <strong><em>(이를 리펙토링 하는 과정을 직접 책 보면서 따라가는 것 추천.)</em></strong></p>
</li>
</ul>
<h4 id="-자신이-소유한-것에만-의존하라-">[ 자신이 소유한 것에만 의존하라 ]</h4>
<p>외부 라이브러리, SDK 등을 직접 사용하는 대신, 반드시 내부 래퍼 또는 어댑터를 두자. 이를 통해 다음과 같은 장점이 생긴다.</p>
<ul>
<li>외부 변경에 대한 완충 작용</li>
<li>도메인 코드에서 의미 있는 인터페이스 제공</li>
<li>테스트 용이성</li>
</ul>
<p>결과적으로 서드파티 의존성이 코드 전반에 퍼지는 것을 방지 한다. 통제하기 어려운 것이 내 손안에 들어온다!</p>
<h4 id="1-프레임워크와-싸우지-마라">1. 프레임워크와 싸우지 마라</h4>
<p>(개인적으로 매우 와닿았음 ㅋㅋ) </p>
<ul>
<li><strong><em>모든 의존성과 완전한 분리는 실현 불가능한 목표</em></strong> 이다. 오히려 완전한 분리가 <strong><em>코드 복잡도의 폭발적 증가</em></strong> 를 낳을 수 있다.</li>
<li>즉, 프레임워크를 무시하거나 거스르는 것이 곧 좋은 아키텍처는 아니다.</li>
<li>도메인과 프레임워크는 분리하되, 프레임워크의 강점을 적절히 활용하는 타협이 필요하다.</li>
</ul>
<h4 id="2-간접-누출에-주의하라">2. 간접 누출에 주의하라</h4>
<ul>
<li>ORM 생각해보자. 다들 object &amp; orm method 의 모든 부분이 raw query 로 어떻게 사용되는지 완벽하게 파악하고 사용하는가?</li>
<li>하지만 ORM의 기능을 무비판적으로 사용하면 성능이나 트랜잭션 이슈가 발생할 수 있다.</li>
<li>추상화의 깊이를 항상 점검하고, 불필요한 간접 누출을 경계하자.</li>
</ul>
<h4 id="-저수준-인프라-오류를-고수준-도메인-오류로-캡슐화하라-">[ 저수준 인프라 오류를 고수준 도메인 오류로 캡슐화하라 ]</h4>
<p>저수준 인프라 라이브러리는 다음과 같은 문제를 가진다.</p>
<ul>
<li>고유한 error code 체계</li>
<li>raw exception 노출</li>
<li>메시지 구조가 도메인에 맞지 않음</li>
</ul>
<p>따라서 다음과 같이 바꿔야 한다</p>
<ul>
<li>외부 오류 → 의미 있는 도메인 오류 (<code>EmailAlreadyRegisteredError</code>, <code>DataConsistencyError</code> 등)</li>
<li>이 과정에서 도메인 계층은 외부 시스템의 오류를 전혀 알 필요가 없어진다.</li>
</ul>
<p><strong><em>애플리케이션은 이를 기반으로 UX 수준에서 적절한 피드백을 구성 가능해진다!</em></strong></p>
<h3 id="7장-모듈화-달성하기">7장 모듈화 달성하기</h3>
<p>6장까지 다뤘던 <strong><em><code>단순성</code>, <code>일관성 유지</code>, <code>좋은 추상화</code>, <code>확장 지점</code>, <code>인프라 세부 사항의 캡슐화와 격리</code></em></strong> 는 사실상 모두 <strong>모듈화</strong>라는 큰 주제 아래 자연스럽게 수렴된다. 이 장은 그러한 원칙들을 <strong>모듈 수준에서 구현하고 지켜내는 방법</strong>에 대한 이야기다.</p>
<ul>
<li><p><strong>캡슐화의 확장</strong>
우리가 데이터를 클래스 내부에 감추는 것처럼, 모듈 또한 연관된 기능들을 내부에 감추고 외부에는 필요한 것만 드러내야 한다. 이 때의 &#39;모듈&#39;은 단순한 코드 묶음이 아니라, <strong>구조적 경계</strong>이자, 소프트웨어의 관리 단위다.</p>
</li>
<li><p><strong>복잡한 기능 위에 단순한 인터페이스 제공</strong>
좋은 모듈은 클라이언트 입장에서 쓰기 쉬워야 한다. 복잡한 로직은 내부에 숨기고, 이를 감싸는 단순하고 명확한 API만 노출해야 한다. 이는 단순한 함수 수준이 아니라, 모듈 전체 수준에서도 동일하게 적용된다.</p>
</li>
<li><p><strong>내부 변경이 외부에 영향을 주지 않도록</strong>
좋은 모듈은 내부 세부 사항이 변경되더라도, 이를 사용하는 클라이언트 코드가 변경되지 않아도 되도록 설계되어야 한다. 즉, <strong>모듈은 클라이언트를 보호하는 방패</strong>가 되어야 한다.</p>
</li>
<li><p><strong>안정적이고 하위 호환 가능한 인터페이스</strong>
모듈의 가장 핵심은 일관된 인터페이스 설계다. 이 인터페이스가 깨질 경우, 모듈을 사용하는 수많은 클라이언트가 일제히 영향을 받는다. 따라서 <strong>명확하고 지속 가능한 통신 방식</strong>이 필수적이다.</p>
</li>
<li><p><strong>확장 지점의 설계</strong>
시스템이 커질수록 기능 추가와 변형이 요구된다. 이때 모듈은 유연한 확장을 허용하되, 그 방식은 일관되고 예측 가능해야 한다. 즉, <strong>확장 지점은 있어야 하지만, 아무나 아무 데나 꽂을 수 있게 해서는 안 된다</strong>.</p>
</li>
<li><p><strong>모듈 간 세부 사항 비공개화</strong>
모듈은 자신의 내부를 감추고, 다른 모듈의 내부를 알지 않아야 한다. 그래야만 <strong>모듈 간 결합도를 줄일 수 있으며, 독립적으로 변경과 진화를 꾀할 수 있다</strong>.</p>
</li>
<li><p><strong>클라이언트는 누출된 세부 사항에 의존하지 않도록 주의</strong>
만약 어떤 모듈이 내부 구현을 의도치 않게 노출한다면, 이를 사용하는 측에서 그 부분에 의존하게 될 수 있다. 이 의존성은 이후 변경 시 <strong>‘모듈의 구현 변경이 전파되는 리스크’를 낳는다</strong>.</p>
</li>
<li><p><strong>모듈의 명확한 소유권과 규칙</strong>
여러 팀이 함께 시스템을 만들 때, 모듈이 분리되어 있지 않으면 갈등이 생기기 쉽다. 어떤 코드에 누가 책임을 질지 모호해지기 때문이다. 명확한 소유권과 규칙이 있는 모듈은 <strong>조직적 커뮤니케이션 비용까지 줄여준다</strong>.</p>
</li>
</ul>
<p>사견을 덧붙이자면, 모듈화는 어떤 기법이라기보다는 앞선 장들의 내용을 충분히 소화한 결과물에 가깝다. 단순히 &quot;모듈을 설계하자&quot;는 의도를 넘어서, 좋은 코드와 아키텍처를 고민하다 보면 <strong>결과적으로 자연스럽게 형성되는 단위</strong>가 모듈이라는 생각이다.</p>
<p>또한, 나의 경우 모듈화를 할 때 마치 <em>내가 오픈소스를 만들고 있다</em>는 상상을 해보는 것도 꽤 도움이 되었다. 일종의 라이브러리나 SDK를 만든다고 생각하고, 사용자 입장에서 인터페이스를 설계하다 보면 오히려 더 집중하게 되고, 실용적인 결과물을 만들 수 있게 된다. </p>
<h3 id="8장-실용적인-접근법">8장 실용적인 접근법</h3>
<h4 id="✔-실용적으로-접근하되-딱-필요한-만큼만">✔ 실용적으로 접근하되, 딱 필요한 만큼만</h4>
<p>완벽한 설계보다는 <strong>지금 필요한 만큼의 단순한 해결책</strong>이 우선이다. 미래를 대비한 과도한 설계는 실제로 쓰이지도 않을 가능성이 높고, 오히려 현재를 더 복잡하게 만들 수 있다. 실용성은 포기하지 않는 선에서 <strong>절제된 추상화와 설계</strong>가 요구된다.</p>
<h4 id="✔-과감하게-리팩터링하되-단-작은-단위로-나눠서">✔ 과감하게 리팩터링하되, 단 작은 단위로 나눠서</h4>
<p>리팩터링은 주저해서는 안 되는 작업이지만, 동시에 무작정 갈아엎는 식이어서는 안 된다. 이 장에서는 리팩터링을 &#39;과감하게&#39; 하되, <strong>안전하게 작은 단위로 쪼개서 실행</strong>할 것을 강조한다. 시스템 전체를 한 번에 뒤엎는 방식이 아니라, 변화 가능성을 감지하고 <strong>작은 확신들로부터 개선을 시작하는 태도</strong>가 중요하다.</p>
<h4 id="✔-코드가-완벽하지-않다는-사실을-받아들여라">✔ 코드가 완벽하지 않다는 사실을 받아들여라</h4>
<p>모든 코드가 완벽할 수는 없으며, <strong>완벽을 추구하다가 실제 문제 해결이 늦어지는 일</strong>은 피해야 한다. 때로는 어설픈 코드라도 현재 문제를 해결하는 것이 더 가치 있을 수 있다. 물론 그 임시방편이 &#39;영원한 기술 부채&#39;가 되지 않도록, <strong>지속적으로 개선할 수 있는 여지를 남겨두는 태도</strong>가 필요하다.</p>
<h4 id="✔-재디자인을-고려하라">✔ 재디자인을 고려하라</h4>
<p>지속적인 기능 추가와 요구사항 변화 속에서, 기존 구조가 더 이상 적절하지 않게 되는 시점이 온다. 그럴 때는 <strong>과감하게 재디자인을 검토해야 한다</strong>. 이는 리팩터링보다 큰 결정이지만, 때로는 시스템의 생명력을 연장시키는 유일한 선택이 되기도 한다. 단, 재디자인은 개인의 열정이 아니라, <strong>정량적이고 조직적인 판단</strong>에 기반해야 한다.</p>
<h4 id="✔-여러분은-주니어-개발자들에-대한-책임이-있다">✔ 여러분은 주니어 개발자들에 대한 책임이 있다</h4>
<p>시스템의 구조와 코드는 단지 기계가 해석할 수 있도록 짜는 것이 아니라, <strong>함께 일하는 사람들—특히 주니어 개발자—가 이해할 수 있도록 짜야 한다.</strong> 좋은 설계는 곧 <strong>좋은 학습 자료이자 멘토링 수단</strong>이 된다. 코드가 혼란스럽고 복잡할수록, 팀 전체의 성장 가능성은 낮아진다. 경험 많은 개발자일수록, 이 책임을 자각해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[python - 동시성 처리, with 구문과 context manager]]></title>
            <link>https://velog.io/@qlgks1/python-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC-with-%EA%B5%AC%EB%AC%B8%EA%B3%BC-context-manager</link>
            <guid>https://velog.io/@qlgks1/python-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC-with-%EA%B5%AC%EB%AC%B8%EA%B3%BC-context-manager</guid>
            <pubDate>Fri, 11 Jul 2025 08:38:33 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: python 에서 context manager 가 필요한 상황과 원리, with 에 대한 deep dive, <strong><em>python 3.13 기준!!</em></strong> ]</p>
<h1 id="python-context-manager">Python context manager</h1>
<blockquote>
<p>python 에는 with 구문이 있다. (사실 2005년 <a href="https://peps.python.org/pep-0343/?utm_source=chatgpt.com">PEP 343</a> 부터 있었던 고인물). 동시성 얘기와 context manager 얘기하는데 왜 with 냐? 사실 이 질문은.. 고민이 역전된 질문이다. <strong><em>정확히 말하면 with는 context manager를 사용하기 위한 문법이기때문!</em></strong></p>
</blockquote>
<blockquote>
<p>사실 batch 중심 서비스에서 celery 쓰긴 과해서, 기존 운영 DBMS 에 추상화된 task model 로 단일 python runtime process 띄워서 처리하는데, 이 &quot;task&quot; 객체 모델링하다가 삘받아서 쓰는 정리글인건 비밀이다.</p>
</blockquote>
<h2 id="1-동시성-처리">1. 동시성 처리</h2>
<p>이제는 멀티코어 프로세서가 일반화되고 I/O 집약적인 작업을 효율적으로 처리하기 위해 동시성 프로그래밍이 거의 기본 개념이 되어버렸다. (왜 동시성 처리가 필요한지는 이제 너무 식상한 얘기가 되어버린 것 같은..)</p>
<p>이제 멀티스레딩, 멀티프로세싱뿐만 아니라, 단일 스레드 내에서도 이벤트 루프와 코루틴을 활용해 동시성을 극대화할 수 있다. (<a href="https://velog.io/@qlgks1/python-%EC%BD%94%EB%A3%A8%ED%8B%B4coroutine-%EB%8F%99%EC%8B%9C%EC%84%B1%EA%B3%BC-%EB%B3%91%EB%A0%AC%EC%84%B1-%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85-blocking%EA%B3%BC-non-blocking-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BD%94%EB%A3%A8%ED%8B%B4">python 코루틴(coroutine) - 동시성과 병렬성, 동기와 비동기 작업, blocking과 non-blocking 그리고 코루틴</a> / <a href="https://velog.io/@qlgks1/javascript-node-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC">javascript - 기본 동작 원리와 v8 js 엔진</a> 참조)</p>
<p>근데 <code>with</code> 랑 <code>context manager</code> 얘기하면서 왜 동시성 얘기 부터 하느냐? 사실 동시성 프로그래밍에서 <strong>*<span style="color: rgb(99 148 255);">가장 까다로운 문제는 바로 &quot;공유 리소스 관리(Shared Resource Management)&quot;</span>*</strong> 이기 때문이다. </p>
<p>즉 여러 스레드나 코루틴이 파일, 데이터베이스 커넥션, 네트워크 소켓과 같은 공유 리소스에 동시에 접근할 때, 접근 순서를 제어하고 사용 후 리소스를 안정적으로 해제하지 않으면 경쟁 상태(Race Condition), 데드락(Deadlock), 리소스 누수(Resource Leak) 등 심각한 문제로 이어질 수 있기 때문이다. </p>
<pre><code class="language-python">import threading

lock = threading.Lock()
shared_resource = 0

def worker():
    global shared_resource
    # with 구문이 lock의 획득(acquire)과 해제(release)를 보장합니다.
    with lock:
        # 이 블록은 한 번에 하나의 스레드만 실행할 수 있는 임계 영역(Critical Section)입니다.
        data = shared_resource
        data += 1
        # 다른 스레드가 끼어들 수 있는 잠재적 위험 구간
        time.sleep(0.1) 
        shared_resource = data

# ... 스레드 생성 및 실행 ...


# 또는 장고에서 
from django.db import transaction

def my_view():
    with transaction.atomic():
        # 이 블록 안의 작업이 하나의 트랜잭션으로 묶임
        do_something()
        do_something_else()</code></pre>
<p>python 하면 위 코드는 식상할 정도로 뭔가 많이 본 형태다. 누군가 &quot;왜 <code>with</code> 를 쓰나요?&quot; 라고 한다면, &quot;<code>lock</code> 을 저 block 에서만 사용하고 빠빠이 하려고요!&quot; 라고만 답하게 된다면, 조금 더 아래를 내려보자!</p>
<h3 id="1-동시성-처리에서-context-manager가-필수적인-이유">1) 동시성 처리에서 Context Manager가 필수적인 이유</h3>
<ul>
<li>결론부터 말하면, <strong><em>안전한 리소스의 &#39;획득&#39;과 &#39;해제&#39;를 보장하기 때문</em></strong> 이다.</li>
</ul>
<p>동시성 환경에서는 여러 실행 흐름이 언제든지 CPU를 점유하고 리소스에 접근할 수 있다. 이때 리소스 접근을 제어하는 &#39;락(Lock)&#39;을 획득하고 사용 후 반드시 &#39;해제&#39;해야 한다. </p>
<p>만약 락을 해제하는 코드가 실행되기 전에 예외가 발생한다면? 해당 락은 영원히 해제되지 않아 다른 스레드들은 무한정 대기하는 데드락 상태에 빠지게 된다.</p>
<p><code>Context Manager</code> 는 <code>with</code> 블록에 진입할 때 리소스를 획득하고, 블록을 빠져나올 때 예외 발생 여부와 관계없이 반드시 리소스를 해제하는 작업을 수행하도록 보장한다. 이것이 동시성 프로그래밍에서 Context Manager가 필수적인 이유다.</p>
<h4 id="with-없는-lock-획득-예시">with 없는 lock 획득 예시</h4>
<pre><code class="language-python">import threading

lock = threading.Lock()

def unsafe_operation():
    lock.acquire()  # 락 획득
    # ... 공유 리소스 작업 ...
    if some_error_condition:
        raise ValueError(&quot;오류 발생!&quot;)
    lock.release()  # 예외 발생 시 이 코드는 절대 실행되지 않음!</code></pre>
<h4 id="위-문제는-try--finally-로-해결-가능">위 문제는 try ... finally 로 해결 가능</h4>
<pre><code class="language-python"># 조금 나아진 예시: try...finally 사용
def slightly_better_operation():
    lock.acquire()
    try:
        # ... 공유 리소스 작업 ...
        if some_error_condition:
            raise ValueError(&quot;오류 발생!&quot;)
    finally:
        lock.release() # finally 블록으로 해제를 보장</code></pre>
<ul>
<li>이 code 형태는 java 에서도 매우 유사하다.</li>
<li>하지만 위 예시는 lock 이 필요한 부분에서 코드 자체가 겁나 지저분해진다는 한계가 있다. 그렇다고 매번 <code>try ... finally</code> 구문을 추상화 해서 사용할 수 도 없는 것.. (자주 바뀌게 되어있음...)</li>
</ul>
<h4 id="이걸-with-로-해결한-파이써닉한-처음-예제">이걸 with 로 해결한 파이써닉한 처음 예제!</h4>
<pre><code class="language-python">import threading

lock = threading.Lock()

def safe_operation():
    with lock: # 진입 시 lock.acquire(), 탈출 시 lock.release() 자동 호출
        # ... 공유 리소스 작업 ...
        if some_error_condition:
            raise ValueError(&quot;오류 발생!&quot;)
    # with 블록이 끝나면 예외가 발생해도 락은 안전하게 해제됨</code></pre>
<ul>
<li>with 를 쓰는게 확실하게 깔끔하다. (물론 depth 가 깊어진다는 trade-off도 있다.)</li>
<li>그러면 이 <code>with</code> 가 이걸 어떻게 해결한다는 건가? 어떻게 진입 시 <code>lock.acquire()</code>, 탈출 시 <code>lock.release()</code> 자동 호출을 한다는 것이가!?!?</li>
</ul>
<h3 id="2-아니-그래서-with-가-뭔-상관인데유">2) 아니 그래서 with 가 뭔 상관인데유</h3>
<h4 id="with-걍-resource-leak-방지-하려고-한거-아녀유">with, 걍 resource leak 방지 하려고 한거 아녀유?</h4>
<ul>
<li>매우 맞다. 근데 이는 &quot;with&quot; 의 특성때문에 이 목적이 가능한거다!</li>
</ul>
<pre><code class="language-python"># 리소스 누수 예시
def leak_file_descriptors():
    # 이 함수를 반복 호출하면 결국 에러 발생
    f = open(&#39;temp.txt&#39;, &#39;w&#39;)
    f.write(&#39;leak&#39;)
    # f.close()를 의도적으로 누락</code></pre>
<ul>
<li>(위 코드) 파일을 열고 <code>close()</code> 를 호출하지 않는 코드가 반복 실행되면 운영체제가 프로세스에 할당한 <strong><em>파일 디스크립터(File Descriptor) 개수 제한에 도달</em></strong> 해 &quot;Too many open files&quot; 오류가 발생한다. <code>with</code> 는 이걸 원천적으로 막아준다.</li>
</ul>
<pre><code class="language-python"># 리소스 누수 방지
def no_leak_example():
    with open(&#39;temp.txt&#39;, &#39;w&#39;) as f:
        f.write(&#39;safe&#39;)
    # with 블록이 끝나면 f.close()가 자동으로 호출됨</code></pre>
<h4 id="with-는-컨텍스트-관리-프로토콜context-management-protocol-을-따르는-객체와-함께-동작한다">with 는 컨텍스트 관리 프로토콜(Context Management Protocol) 을 따르는 객체와 함께 동작한다.</h4>
<ul>
<li><p><code>with</code> 는 &quot;Context Manager&quot;를 위한 &quot;Syntactic Sugar&quot; 이다. <a href="https://docs.python.org/ko/3/library/contextlib.html">contextlib — with 문 컨텍스트를 위한 유틸리티</a></p>
</li>
<li><p>쉽게 말하면 <code>__enter__()</code> 와 <code>__exit__()</code> 메소드를 구현한 객체로 <strong>*<span style="color: rgb(99 148 255);"><code>with</code> 문 사용 시 자동으로 호출되는 메서드들이다.</span>*</strong> <del>(만약 던더메서드, 매직메서드를 모른다면 이 글은 도움이 못된다.)</del></p>
</li>
</ul>
<ol>
<li><p><code>__enter__(self)</code> : <code>with</code> 블록에 진입할 때 호출된다. 리소스를 획득하고 설정하는 역할을 하며, <code>as</code> 키워드로 변수에 할당할 값을 반환한다.</p>
</li>
<li><p><code>__exit__(self, exc_type, exc_value, traceback)</code> : <code>with</code> 블록을 &quot;빠져나올 때&quot; 반드시 호출된다. 리소스를 해제하는 역할을 한다. <strong><em>만약 블록이 예외 없이 정상 종료되었다면 세 인자(exc_type, exc_value, traceback)는 모두 None이 된다.</em></strong> 예외가 발생했다면 해당 예외 정보가 전달된다. <code>__exit__</code> 메서드가 <code>True</code> 를 반환하면 예외가 전파되지 않고 억제된다. </p>
</li>
</ol>
<h4 id="with-의-바이트-코드">with 의 바이트 코드</h4>
<pre><code class="language-python">import dis

def my_func():
    with open(&#39;file.txt&#39;, &#39;w&#39;) as f:
        f.write(&#39;hello&#39;)

dis.dis(my_func)</code></pre>
<pre><code class="language-shell">   3           RESUME                   0

   4           LOAD_GLOBAL              1 (open + NULL)
               LOAD_CONST               1 (&#39;file.txt&#39;)
               LOAD_CONST               2 (&#39;w&#39;)
               CALL                     2
               BEFORE_WITH
       L1:     STORE_FAST               0 (f)

               ...생략...

   4   L2:     LOAD_CONST               0 (None)
               LOAD_CONST               0 (None)
               LOAD_CONST               0 (None)
               CALL                     2
               POP_TOP
               RETURN_CONST             0 (None)
       L3:     PUSH_EXC_INFO
               WITH_EXCEPT_START
               ...생략...</code></pre>
<ul>
<li><p>왜 갑자기 바이트코드냐면,, 지금 cpython 3.14 이상인 main인 깃헙 레포에는 <code>with</code> 에 대한 코드를 찾기 어렵다.. <a href="https://github.com/python/cpython/blob/f1b8d01c802165b9807172965b7097b6aaf6fcd3/Misc/NEWS.d/3.14.0a1.rst#L5097">&quot;Remove the BEFORE_WITH and BEFORE_ASYNC_WITH instructions. Add the new :opcode:<code>LOAD_SPECIAL</code> instruction&quot; 때문 ㅠ</a></p>
</li>
<li><p>덧붙이자면 <code>with</code> 문은 <code>Python 3.11</code> 부터 도입된 <strong><em>특화 적응형 인터프리터(Specializing Adaptive Interpreter)</em></strong> 덕분에 고도로 최적화된 바이트코드를 사용한다. - <a href="https://peps.python.org/pep-0659/">https://peps.python.org/pep-0659/</a> &amp; <a href="https://www.youtube.com/watch?v=shQtrn1v7sQ">https://www.youtube.com/watch?v=shQtrn1v7sQ</a> <strong><em>(아니 그니까 좀 python 최소한 3.11 이상은 써라 제발 좀)</em></strong></p>
</li>
<li><p>여튼 <code>BEFORE_WITH</code> 가 <code>__enter__</code> 이며 <code>WITH_EXCEPT_START</code> 가 <code>with</code> 에서 예외 발생 시 <code>__exit__(...)</code> 호출 하기 위한 세팅이다. <del>파이썬이 이렇게나 상위 문법을 지원해줘서 감사할따름</del></p>
</li>
</ul>
<hr>
<h2 id="2-contextlib-모듈과-제너레이터">2. contextlib 모듈과 제너레이터</h2>
<ul>
<li><p>사실 <code>__enter__</code> 와 <code>__exit__</code> 를 가진 클래스를 매번 작성하는 것은 &quot;귀찮다&quot;. 이럴때마다 파이써닉이 와닿는데, <code>contextlib</code> 내장 라이브러리가 이 과정을 훨씬 쉽게 만들어주는 <code>@contextmanager</code> 데코레이터를 제공한다. </p>
</li>
<li><p><a href="https://github.com/python/cpython/blob/main/Lib/contextlib.py">https://github.com/python/cpython/blob/main/Lib/contextlib.py</a> 에서 실제 해당 내장 라이브러리의 코드 참조!</p>
</li>
</ul>
<h3 id="1-contextmanager-데코레이터의-내부-구현">1) <code>@contextmanager</code> 데코레이터의 내부 구현</h3>
<p><code>@contextmanager</code> 데코레이터는 제너레이터(Generator) 함수를 손쉽게 Context Manager로 변환해 준다. 이 데코레이터는 내부적으로 <code>_GeneratorContextManager</code> 라는 헬퍼 클래스를 사용하여 제너레이터를 컨텍스트 관리 프로토콜에 맞게 래핑한다.</p>
<pre><code class="language-python">def contextmanager(func):
    &quot;&quot;&quot;@contextmanager decorator.

    Typical usage:

        @contextmanager
        def some_generator(&lt;arguments&gt;):
            &lt;setup&gt;
            try:
                yield &lt;value&gt;
            finally:
                &lt;cleanup&gt;

    This makes this:

        with some_generator(&lt;arguments&gt;) as &lt;variable&gt;:
            &lt;body&gt;

    equivalent to this:

        &lt;setup&gt;
        try:
            &lt;variable&gt; = &lt;value&gt;
            &lt;body&gt;
        finally:
            &lt;cleanup&gt;
    &quot;&quot;&quot;
    @wraps(func)
    def helper(*args, **kwds):
        return _GeneratorContextManager(func, args, kwds)
    return helper</code></pre>
<h4 id="_generatorcontextmanager">_GeneratorContextManager</h4>
<pre><code class="language-python">class _GeneratorContextManager(
    _GeneratorContextManagerBase,
    AbstractContextManager,
    ContextDecorator,
):
    &quot;&quot;&quot;Helper for @contextmanager decorator.&quot;&quot;&quot;

    def __enter__(self):
        # do not keep args and kwds alive unnecessarily
        # they are only needed for recreation, which is not possible anymore
        del self.args, self.kwds, self.func
        try:
            return next(self.gen)
        except StopIteration:
            raise RuntimeError(&quot;generator didn&#39;t yield&quot;) from None

    def __exit__(self, typ, value, traceback):
        if typ is None:
            try:
                next(self.gen)
            except StopIteration:
                return False
            else:
                try:
                    raise RuntimeError(&quot;generator didn&#39;t stop&quot;)
                finally:
                    self.gen.close()
        else:
            if value is None:
                # Need to force instantiation so we can reliably
                # tell if we get the same exception back
                value = typ()
            try:
                self.gen.throw(value)
            except StopIteration as exc:
                # Suppress StopIteration *unless* it&#39;s the same exception that
                # was passed to throw().  This prevents a StopIteration
                # raised inside the &quot;with&quot; statement from being suppressed.
                return exc is not value
            except RuntimeError as exc:
                # Don&#39;t re-raise the passed in exception. (issue27122)
                if exc is value:
                    exc.__traceback__ = traceback
                    return False
                # Avoid suppressing if a StopIteration exception
                # was passed to throw() and later wrapped into a RuntimeError
                # (see PEP 479 for sync generators; async generators also
                # have this behavior). But do this only if the exception wrapped
                # by the RuntimeError is actually Stop(Async)Iteration (see
                # issue29692).
                if (
                    isinstance(value, StopIteration)
                    and exc.__cause__ is value
                ):
                    value.__traceback__ = traceback
                    return False
                raise
            except BaseException as exc:
                # only re-raise if it&#39;s *not* the exception that was
                # passed to throw(), because __exit__() must not raise
                # an exception unless __exit__() itself failed.  But throw()
                # has to raise the exception to signal propagation, so this
                # fixes the impedance mismatch between the throw() protocol
                # and the __exit__() protocol.
                if exc is not value:
                    raise
                exc.__traceback__ = traceback
                return False
            try:
                raise RuntimeError(&quot;generator didn&#39;t stop after throw()&quot;)
            finally:
                self.gen.close()</code></pre>
<p><code>_GeneratorContextManager</code> 가 이미 매우, 충분히 잘 만들어져 있기때문에, <strong>(제발 다시 처음부터 만들지 말고)</strong> <code>@contextmanager</code> 이거 부터 사용할지 고민해봐야 한다.  </p>
<ol>
<li><code>__enter__</code> 메서드</li>
</ol>
<ul>
<li>context manager에 진입할 때 호출</li>
<li>제너레이터의 첫 번째 yield까지 실행</li>
<li>yield된 값을 반환 (보통 with 문의 as 변수에 할당)</li>
</ul>
<ol start="2">
<li><code>__exit__</code> 메서드</li>
</ol>
<ul>
<li>context manager에서 나갈 때 호출</li>
<li>예외가 없으면: 제너레이터가 정상 종료되는지 확인</li>
<li>예외가 있으면: 제너레이터에 예외를 전달하여 처리 기회 제공</li>
<li>예외 억제 여부를 결정하여 반환</li>
</ul>
<p>간단한 해당 class 의 flow chart 는 아래와 같다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/127efb9e-0146-457f-ad80-3dfd400d5a82/image.png" alt=""></p>
<h4 id="예외-발생시의-genthrowvalue-조금만-더-보자">예외 발생시의 <code>gen.throw(value)</code> 조금만 더 보자!</h4>
<ul>
<li><p>예외가 있으면, <code>__exit__</code> 메서드는 전달받은 예외 정보(typ, value, traceback)를 사용하여 제너레이터의 <code>throw()</code> 메서드를 호출한다. </p>
</li>
<li><p>즉, <code>gen.throw(value)</code> 를 통해 제너레이터가 <code>yield</code> 에서 멈춰있던 지점으로 예외를 &#39;주입&#39; 한다. 그러면 제너레이터 함수 안의 <code>try...except</code> 블록에 의해 잡히게 된다. </p>
</li>
<li><p>그러니까 <code>@contextmanager</code> 를 사용한 함수에서 <code>try...except</code> 에서 잡히게 된다는 거고, <code>try...except</code> 를 써야 좀 더 depth 있는 디버깅을 할 수 있다는 것!</p>
</li>
</ul>
<h4 id="수동-클래스-구현-vs-contextmanager-데코레이터">수동 클래스 구현 vs. @contextmanager 데코레이터</h4>
<pre><code class="language-python"># 방법 1: 클래스로 직접 구현
class Timer:
    def __enter__(self):
        self.start = time.time()
        print(&quot;타이머 시작&quot;)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        end = time.time()
        print(f&quot;소요 시간: {end - self.start:.2f}초&quot;)
        return False # 예외를 억제하지 않음

# 방법 2: @contextmanager 데코레이터 사용
from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    print(&quot;타이머 시작&quot;)
    try:
        yield # __enter__의 반환값 (여기선 None), 이 지점에서 with 블록 코드가 실행됨
    finally:
        # with 블록을 빠져나오면 이 코드가 실행됨
        end = time.time()
        print(f&quot;소요 시간: {end - start:.2f}초&quot;)

# 사용법은 동일
with timer():
    time.sleep(1)</code></pre>
<p><code>@contextmanager</code> 를 사용하면 <code>try...yield...finally</code> 패턴으로 <code>__enter__</code> 와 <code>__exit__</code> 의 로직을 훨씬 직관적으로 표현할 수 있다. </p>
<ol>
<li><strong><em>yield 이전까지의 코드</em></strong>: <code>__enter__</code> 에 해당</li>
<li><strong><em>yield 이후 finally 블록 안의 코드</em></strong>: <code>__exit__</code> 에 해당</li>
</ol>
<p><code>_GeneratorContextManager</code>는 <code>__enter__</code>가 호출되면 제너레이터를 <code>yield</code> 지점까지 실행하고, <code>__exit__</code> 가 호출되면 예외 정보를 제너레이터의 <code>throw()</code> 메서드로 주입하거나 <code>next()</code> 를 호출하여 <code>finally</code> 블록이 실행되도록 한다. </p>
<p>더욱이 이제 &quot;바이트코드&quot; 로 구현되어 있어서 <strong><em>최소한의 오버헤드를 추가</em></strong> 한다고 한다. </p>
<h3 id="2-얘랑-제네레이터랑-무슨-연관이-있음">2) 얘랑 제네레이터랑 무슨 연관이 있음?</h3>
<p>사실 제네레이터를 모르면 위 예제들이나 설명이 와닿지가 않는다. (<a href="https://velog.io/@qlgks1/python-%EC%BD%94%EB%A3%A8%ED%8B%B4coroutine-iterator-generator-asyncio-async-await-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BD%94%EB%A3%A8%ED%8B%B4-2">python 코루틴(coroutine) - iterator, generator, asyncio, async, await 그리고 코루틴 (2)</a> 참조) 단순한 정의를 보면 python 에서 제네레이터는 &quot;이터레이터를 생성하는 함수&quot; 이다. <code>yield</code> 표현식을 사용하고 다음 호출 시 <strong><em>마지막으로 실행된 <code>yield</code> 표현식 이후부터 실행을 재개한다.</em></strong></p>
<ol>
<li><p>실행 흐름의 일시 중단 및 재개: yield 키워드는 함수의 실행을 잠시 멈추고 제어권을 호출자에게 넘겨준다. <code>with</code> 블록의 코드가 실행되는 동안 제너레이터는 <code>yield</code> 지점에서 대기하게 된다.</p>
</li>
<li><p>상태 유지: 제너레이터 함수 내의 &quot;지역 변수&quot;는 <code>yield</code> 를 통해 중단되었다가 다시 재개될 때까지 그 상태를 그대로 유지한다. (start 변수처럼)</p>
</li>
<li><p>예외 주입: 제너레이터의 <code>throw()</code> 메서드를 사용하면 제너레이터가 멈춰있는 <code>yield</code> 지점 <strong><em>외부에서 예외를 발생시킬 수 있다.</em></strong> <code>@contextmanager</code> 는 이 기능을 활용해 <code>with</code> 블록의 예외를 제너레이터 내부로 전달한다. 이 얘기가 바로 위에서 본 &quot;예외 발생시의 gen.throw(value)&quot; 얘기다. </p>
</li>
</ol>
<p>그리고 사실 대용량 처리일 수 록, &quot;메모리 관점의 이점이 상당하다.&quot; 이건 당연히 제네레이터의 특성이자 이를 기반으로 할 수 있는 <code>with</code> 의 결과론적인 이점이다. </p>
<ul>
<li>리소스 누수 방지 &amp; 리소스 자동 정리 &amp; 제네레이터 &amp; 캐시 지역성</li>
</ul>
<h3 id="3-실제-사용하면-좋은-케이스">3) 실제 사용하면 좋은 케이스</h3>
<h4 id="데이터베이스-연결-관리">데이터베이스 연결 관리</h4>
<ul>
<li>데이터베이스 연결은 열고 닫는 것뿐만 아니라, 작업 성공 시 <code>commit</code>, 실패 시 <code>rollback</code>을 수행해야한다. </li>
</ul>
<pre><code class="language-python">from contextlib import contextmanager
import sqlite3

@contextmanager
def db_transaction(db_path):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    print(&quot;DB 커넥션 및 트랜잭션 시작&quot;)
    try:
        yield cursor # with 블록에서 사용할 커서 객체를 반환
        print(&quot;트랜잭션 커밋&quot;)
        conn.commit()
    except Exception as e:
        print(f&quot;예외 발생: {e}, 트랜잭션 롤백&quot;)
        conn.rollback()
        raise # 예외를 다시 발생시켜 호출자에게 알림
    finally:
        print(&quot;DB 커넥션 종료&quot;)
        conn.close()

# 사용 예시
with db_transaction(&#39;app.db&#39;) as cursor:
    cursor.execute(&quot;INSERT INTO users (name) VALUES (&#39;Alice&#39;)&quot;)
    # 만약 여기서 예외가 발생하면 자동으로 롤백됩니다.</code></pre>
<h4 id="근데-사실-sqlalchemy-같은-대부분의-최신-db-라이브러리는-이미-자체적으로-뛰어난-context-manager를-내장하고-있어-직접-만들-필요는-거의-없다">근데 사실 SQLAlchemy 같은 대부분의 최신 DB 라이브러리는 이미 자체적으로 뛰어난 Context Manager를 내장하고 있어 직접 만들 필요는 거의 없다.</h4>
<pre><code class="language-python"># SQLAlchemy의 내장 Context Manager 예시
from sqlalchemy.orm import Session

with Session(engine) as session:
    session.add(User(name=&quot;Bob&quot;))
    # 예외 발생 시 자동으로 롤백, 정상 종료 시 커밋 (설정에 따라 다름)
    session.commit()
# 세션은 자동으로 닫힘</code></pre>
<ul>
<li>그리고 본문 가장 초기에 언급한 django transaction 예제도 내부적으로 context manager protocol 에 따른 형태로 class가 구현된 것을 볼 수 있다. ㅎㅎ</li>
</ul>
<h4 id="분산-락과-동기화">분산 락과 동기화</h4>
<p>여러 서버에 걸쳐 리소스 접근을 동기화해야 할 때 Redis 같은 외부 저장소를 이용해 분산 락을 구현할 수 있다. 이때도 Context Manager는 매우 유용하다.</p>
<p><strong><em>(나쁜예)</em></strong></p>
<pre><code class="language-python">import redis

# 락을 획득했지만, 프로세스가 갑자기 죽으면 락이 해제되지 않을 수 있음
def bad_distributed_lock(r, lock_key):
    is_acquired = r.set(lock_key, &quot;locked&quot;, nx=True, ex=30)
    if is_acquired:
        # ... 임계 영역 작업 ...
        r.delete(lock_key) # 작업 중 오류가 나면 실행되지 않음
    else:
        print(&quot;락 획득 실패&quot;)</code></pre>
<p><strong><em>(context manager 예시)</em></strong></p>
<pre><code class="language-python">from contextlib import contextmanager
import redis
import uuid

@contextmanager
def distributed_lock(r: redis.Redis, lock_key: str, timeout: int = 30):
    lock_id = str(uuid.uuid4())
    # 락 획득 시도 (nx=True는 키가 없을 때만 set)
    if not r.set(lock_key, lock_id, nx=True, ex=timeout):
        raise TimeoutError(&quot;분산 락을 획득할 수 없습니다.&quot;)

    try:
        yield lock_id # 락을 획득했음을 알림
    finally:
        # 내가 획득한 락이 맞는지 확인하고 안전하게 삭제 (Lua 스크립트 사용)
        release_script = &quot;&quot;&quot;
        if redis.call(&quot;get&quot;, KEYS[1]) == ARGV[1] then
            return redis.call(&quot;del&quot;, KEYS[1])
        else
            return 0
        end
        &quot;&quot;&quot;
        r.eval(release_script, 1, lock_key, lock_id)

# 사용 예시
r = redis.Redis()
try:
    with distributed_lock(r, &quot;my-distributed-lock&quot;) as lock_id:
        print(f&quot;락 획득 성공 (ID: {lock_id})&quot;)
        # 여러 서버에서 공유하는 중요한 작업 수행
except TimeoutError as e:
    print(e)</code></pre>
<h4 id="api-비율-제한과-리소스-조절">API 비율 제한과 리소스 조절</h4>
<p>외부 API 호출 시 <strong>*&quot;비율 제한(Rate Limiting)&quot;*</strong> 이 필요하거나, 스레드 풀 같은 리소스를 사용하고 안전하게 종료(shutdown)하는 데도 Context Manager가 이상적이다.</p>
<p><strong><em>(나쁜예)</em></strong></p>
<pre><code class="language-python">import time
import requests
from concurrent.futures import ThreadPoolExecutor

# 스레드에 안전하지 않은(non-thread-safe) API 클라이언트
class UnsafeAPIClient:
    def __init__(self, requests_per_second=1):
        self.interval = 1.0 / requests_per_second
        self.last_request_time = 0

    def make_request(self, url):
        # 여러 스레드가 이 부분을 동시에 통과할 수 있어 비율 제한이 깨짐 (Race Condition)
        time_since_last = time.time() - self.last_request_time
        if time_since_last &lt; self.interval:
            time.sleep(self.interval - time_since_last)

        self.last_request_time = time.time()
        print(f&quot;{threading.current_thread().name}: Requesting {url} at {self.last_request_time:.2f}&quot;)
        return requests.get(url)

# 스레드 풀을 생성하고 제대로 종료하지 않음
def bad_api_usage():
    client = UnsafeAPIClient(requests_per_second=2) # 초당 2회 제한
    executor = ThreadPoolExecutor(max_workers=5)

    urls = [&#39;https://api.example.com/data&#39;] * 5

    # 여러 스레드에서 안전하지 않은 클라이언트를 공유하며 작업 제출
    for url in urls:
        executor.submit(client.make_request, url)

    # executor.shutdown(wait=True) 호출을 잊어버림!
    # 프로그램이 즉시 종료되지 않거나, 스레드 리소스가 누수될 수 있음.
    print(&quot;모든 작업을 제출했지만, 스레드 풀을 종료하지 않았습니다.&quot;)</code></pre>
<p>위와 같이 스레드 풀을 수동으로 관리하고, 비율 제한이 스레드에 안전하지 않은 경우는</p>
<ol>
<li><p><strong>경쟁 상태 (Race Condition)</strong>: <code>UnsafeAPIClient</code>의 비율 제한 로직은 여러 스레드가 동시에 접근하면 바로 꺠진다. 여러 스레드가 거의 동시에 <code>time_since_last</code> 를 확인하고 <code>sleep</code> 없이 바로 요청을 보내기 때문에 의도한 비율 제한을 바아로 초과하게 된다.</p>
</li>
<li><p><strong>리소스 누수 (Resource Leak)</strong>: <code>ThreadPoolExecutor</code> 를 생성한 후 <code>shutdown()</code> 메서드가 호출되지 않았다! -&gt; 백그라운드 스레드가 정리되지 않아 프로그램이 비정상적으로 대기하거나 리소스가 계속 점유되는 문제가 발생한다. (이거 생각보다 그냥 놓치면 나중에 디버깅하기 어렵다. 평소 습관이 중요...)</p>
</li>
</ol>
<p><strong><em>(context manager 예시)</em></strong></p>
<pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
import threading

# 스레드 풀을 안전하게 관리하는 Context Manager
@contextmanager
def thread_pool(max_workers):
    executor = ThreadPoolExecutor(max_workers=max_workers)
    try:
        yield executor
    finally:
        # with 블록이 끝나면 반드시 shutdown이 호출됨
        executor.shutdown(wait=True)

# 스레드에 안전한 비율 제한 로직을 제공하는 Context Manager
@contextmanager
def rate_limiter(client_instance):
    with client_instance.lock: # 스레드 락으로 임계 영역 보호
        time_since_last = time.time() - client_instance.last_request_time
        if time_since_last &lt; client_instance.interval:
            time.sleep(client_instance.interval - time_since_last)
        client_instance.last_request_time = time.time()
    yield

class SafeAPIClient:
    def __init__(self, requests_per_second=1):
        self.interval = 1.0 / requests_per_second
        self.last_request_time = 0
        self.lock = threading.Lock() # 스레드 동기화를 위한 락

    def make_request(self, url):
        with rate_limiter(self): # 컨텍스트 매니저로 비율 제한
             print(f&quot;{threading.current_thread().name}: Requesting {url} at {time.time():.2f}&quot;)
             return requests.get(url)

# Context Manager를 활용한 안전한 병렬 API 요청
def good_api_usage():
    client = SafeAPIClient(requests_per_second=2)
    urls = [&#39;https://api.example.com/data&#39;] * 5

    with thread_pool(max_workers=5) as executor:
        futures = [executor.submit(client.make_request, url) for url in urls]
        # 결과 처리...

    print(&quot;모든 작업이 완료되고 스레드 풀이 안전하게 종료되었습니다.&quot;)</code></pre>
<ul>
<li>사실, 눈치챗듯 위 <code>thread_pool</code> 은 좀 과하다 ㅎㅎ; </li>
<li><code>ThreadPoolExecutor</code> 가 이미 <code>__enter__</code> 와 <code>__exit__</code> 메서드를 구현하고 있기 때문에 불필요한 중복이다 ㅎ. 하나의 예시를 위해 가져와서 사용했다.</li>
<li><code>with ThreadPoolExecutor(max_workers=5) as executor</code> 로 처리 가능하다.</li>
</ul>
<pre><code class="language-python"># ThreadPoolExecutor를 직접 Context Manager로 활용
def good_api_usage_simplified():
    client = SafeAPIClient(requests_per_second=2)
    urls = [&#39;https://api.example.com/data&#39;] * 5

    # ThreadPoolExecutor 자체가 Context Manager이므로 별도 래퍼 함수 불필요
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [executor.submit(client.make_request, url) for url in urls]
        # 결과 처리...
        # concurrent.futures.as_completed(futures) 등을 사용하여 완료된 순서대로 결과 처리 가능

    print(&quot;모든 작업이 완료되고 스레드 풀이 안전하게 종료되었습니다.&quot;)</code></pre>
<hr>
<h2 id="핵심-요약">핵심 요약</h2>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a55fb68d-a55e-4730-9900-cee54854c60b/image.png" alt=""></p>
<ul>
<li><p>결국 Context Manager는 <strong>*&quot;리소스의 안전한 획득과 해제를 보장하는 파이썬의 핵심 메커니즘&quot;*</strong> 이다. 그리고 이걸 <code>with</code> 구문으로 사용할 수 있다. (<code>__enter__</code> 와 <code>__exit__</code> 자동 호출)</p>
</li>
<li><p>결국 <code>with</code> 문은 <strong><em>동시성 환경에서 발생할 수 있는 경쟁 상태(Race Condition), 데드락(Deadlock), 리소스 누수(Resource Leak) 등의 심각한 문제들을 원천적으로 방지</em></strong> 하는 짱편한 문법이다.</p>
</li>
<li><p>특히 <code>@contextmanager</code> 데코레이터와 제너레이터의 조합은 복잡한 클래스 구현 없이도 직관적인 <code>try...yield...finally</code> 패턴으로 리소스 관리 로직을 표현할 수 있게 해준다.</p>
</li>
<li><p>특히 <strong>*<span style="color: rgb(99 148 255);"> 3.11 이상의 특화 적응형 인터프리터 덕분에 성능 오버헤드도 최소화되었으니</span>*</strong>, 파일 I/O, 데이터베이스 트랜잭션, 스레드 동기화, 분산 락 등 리소스가 관련된 모든 곳에서 Context Manager를 적극 활용해보자!</p>
</li>
</ul>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://peps.python.org/pep-0343/">https://peps.python.org/pep-0343/</a></li>
<li><a href="https://docs.python.org/3/library/contextlib.html">https://docs.python.org/3/library/contextlib.html</a></li>
<li><a href="https://docs.python.org/3/reference/compound_stmts.html#the-with-statement">https://docs.python.org/3/reference/compound_stmts.html#the-with-statement</a></li>
<li><a href="https://github.com/python/cpython/blob/main/Lib/contextlib.py">https://github.com/python/cpython/blob/main/Lib/contextlib.py</a></li>
<li><a href="https://realpython.com/python-with-statement/">https://realpython.com/python-with-statement/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] LLM 엔지니어링 - 폴 이우수틴 , 막심 라본]]></title>
            <link>https://velog.io/@qlgks1/%EC%B1%85-%EB%A6%AC%EB%B7%B0-LLM-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81</link>
            <guid>https://velog.io/@qlgks1/%EC%B1%85-%EB%A6%AC%EB%B7%B0-LLM-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81</guid>
            <pubDate>Sun, 29 Jun 2025 14:26:09 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="llm-엔지니어링">LLM 엔지니어링</h1>
<blockquote>
<p>Paul Iusztin(폴 이우수틴): 7년 이상 생성형 AI, 컴퓨터 비전 및 MLOps 설루션을 구축한 시니어 ML/MLOps 엔지니어. 최근에는 Metaphysic에서 대규모 신경망을 프로덕션에 적용하는 핵심 엔지니어로 근무했다. 또한 프로덕션급 ML 교육 채널인 Decoding ML을 설립해 사람들이 ML 시스템을 구축할 수 있도록 IT 기사와 오픈 소스 강좌를 제공하고 있다. - <em><a href="https://www.linkedin.com/in/pauliusztin/">링크드인</a></em> / PS) 실제 <a href="https://medium.com/@pauliusztin">폴 이우스틴의 미디엄</a> 에서 해당 책의 원본, 원문 내용을 많이 찾아볼 수 있다.</p>
</blockquote>
<blockquote>
<p>Maxime Labonne(막심 라본): Liquid AI의 모델 최적화 총괄 책임자. 파리 폴리테크닉 연구소에서 ML 박사 학위를 취득했으며, AI/ML 분야의 구글 개발자로 일하고 있다. LLM 과정과 LLM AutoEval 등의 도구, NeuralDaredevil과 같은 SOTA 모델을 포함해 오픈 소스 커뮤니티에 활발히 기여하고 있으며, 기술 블로그도 꾸준히 운영하고 있다. 저서로는 『핸즈온 그래프 인공신경망 with Python』(홍릉, 2024)이 있다. - <em><a href="https://www.linkedin.com/in/maxime-labonne/">링크드인</a></em></p>
</blockquote>
<p>🔥 한빛 책 링크 - <a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B8130648672">https://www.hanbit.co.kr/store/books/look.php?p_code=B8130648672</a>
🔥 코드 참조 깃허브 레포 - <a href="http://github.com/inrap8206/LLM-Engineers-Handbook">http://github.com/inrap8206/LLM-Engineers-Handbook</a></p>
<h2 id="리뷰">리뷰</h2>
<p><strong>실제 MVP의 핵심 기능을 직접 정하고, ML &amp; MLOps 관점에서 아키텍처 설계부터 실습까지 점진적으로 나아가는 구성</strong>은 이 책의 가장 큰 미덕이다. 각 장을 넘길수록 기능이 하나씩 추가되고, 아키텍처가 세밀하게 확장되어 가는 방식은 단순한 튜토리얼을 넘어선다. 프로젝트가 완성되는 시점에서 &quot;이 책은 처음부터 꽤나 <strong>치밀하게 설계</strong>되었다는 것&quot;을 깨달았고, 그 흐름 속에 독자를 몰입하게 만든다는 점에서 확실히 &#39;밀도&#39; 있는 구성이다.</p>
<p>무엇보다도 <strong>토이 프로젝트의 완성도가 기대 이상으로 재밌다.</strong> 단순히 ‘구현했다’ 수준을 넘어 실제 운영 가능한 수준의 기획과 구조, 그리고 실험 기반 접근이 더해져 있어서 진짜 ‘프로젝트 하나 만든다’는 느낌이 강하다. 단순한 예제 코드가 아닌, 실전 감각이 묻어난 프로젝트라는 점에서 분명 큰 만족감을 준다. <strong>*<span style="color: rgb(99 148 255);">입문자용이 아니라서 아주아주 마음에 들었던 책..</span>*</strong> FTI 아키텍쳐 하나 제대로 배운 것 만으로도 만족!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9d582d46-3c9b-45bc-906a-a3fcfbf5e636/image.png" alt=""></p>
<p><em>[아쉬웠던 포인트 들]</em></p>
<p>책의 설명은 굉장히 상세하지만, 그만큼 진입장벽도 있다. 실습 환경을 세팅하거나 파이프라인 흐름을 따라가는 과정에서, 시각적으로 빠르게 이해하는 데 한계가 있다. <strong>영상이나 인터랙티브한 문서가 병행되었다면</strong> 더 많은 독자들이 이 책의 가치를 누릴 수 있었을 것이다. (근데 나중에 발견한 링크, <a href="https://decodingml.substack.com/p/build-your-second-brain-ai-assistant">https://decodingml.substack.com/p/build-your-second-brain-ai-assistant</a> 참조하면 도움이 꽤 될 듯 하다.)</p>
<p>그리고 한국 독자 기준으로는 데이터셋이 꽤 낯설 수 있다. 필자의 경우 GitHub, Notion, LinkedIn, Velog 데이터를 기반으로 커스터마이징하여 실습을 진행했는데, <strong>국내 데이터셋에 맞춘 튜닝 사례나 팁이 조금이라도 언급되었으면 훨씬 현실감 있는 접근이 가능했을 것이다.</strong></p>
<p>매우 주관적으로 <strong>&quot;범위와 구성이 약간 과했다&quot;</strong> 는 생각이 든다. 특히 도메인 주도 설계(DDD) 파트는 실제로 직접 <strong>카테고리를 확장하거나 데이터 파이프라인을 커스터마이징할 때 예상보다 많은 구조 변경이 필요했다.</strong> <del>(물론 내가 못해서...)</del> 가벼운 레이어를 분리정도로 했어도 괜찮은 프로젝트 사이즈가 아닐까!? 했다. </p>
<p><strong>&quot;LLM 평가 파트의 아쉬움&quot;</strong> 이 있다. 아주 개인적으로는 도메인 특화 모델의 평가에 더 깊이 들어가주기를 바랐다. 파인튜닝 이후 모델을 어떻게 평가해야 하는지, 특히 정량적 지표가 부정확하거나 불충분할 때 어떻게 QA 해야 하는지가 궁금했으나, <strong>방법론적인 틀 정도만 제시</strong>되고 넘어간 점은 아쉬웠다. 물론, 특화 도메인의 평가가 어렵다는 점은 현실적으로 공감되지만 말이다.</p>
<p>이 책은 ‘한 번 읽고 덮을 책’이 아니다. 처음부터 끝까지 따라하며 실습하는 것도 좋지만, <strong>직접 파이프라인을 추가하고, 특성을 바꾸고, 평가 방식을 고민하며 R&amp;D하는 데 활용하면 훨씬 큰 가치를 지닌다.</strong> 단지 구현에 그치지 않고, <strong>업계에서 자주 마주치는 안티패턴을 경계하면서 고응집 / 저결합 아키텍처를 지향한다는 점</strong>도 매우 인상 깊다.</p>
<p><em>(요즘 읽은 책 중에 가장 읽기 어려웠던 책이다. 바쁜 일상 속에서도 쉽게 놓치고 싶지 않은 내용이 너무 많았기에, 욕심내며 읽게 된 책이었다. 그런 만큼 다시 두 번, 세 번 반복해서 보는 가치가 있는 책이기도 하다.)</em></p>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<h3 id="ch-1-llm-twin-개념과-아키텍처-이해">CH 1. LLM Twin 개념과 아키텍처 이해</h3>
<p>이 장에서는 LLM Twin을 만들기 위한 전략적 접근과 아키텍처 설계 개념을 다룬다. 먼저, LLM Twin을 현실화하기 위한 <strong>MVP(Minimum Viable Product)</strong> 전략으로 출발하여, 실제로 어떤 기능이 핵심인지 정의한다. 이 때 단순히 모델 개발이 아닌, <strong>제품 관점에서의 기능 정의와 프로세스 구성</strong>에 포인트를 두고 있는게 인상 깊다.</p>
<h4 id="ml-시스템은-기본적으로-다음과-같은-전체-흐름을-가진다">&lt; ML 시스템은 기본적으로 다음과 같은 전체 흐름을 가진다. &gt;</h4>
<ul>
<li>새로운 데이터 수집, 정제, 검증</li>
<li>학습 환경과 추론 환경의 분리</li>
<li>비용 효율적 모델 서빙</li>
<li>데이터셋 및 모델의 버전 관리와 추적</li>
<li>인프라 및 모델 모니터링</li>
<li>확장 가능한 인프라 기반 배포</li>
<li>학습과 배포 자동화</li>
</ul>
<p>이러한 이상적인 시스템 구성은 구글 클라우드 팀이 제시한 아래 그림에서도 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/d2726e2b-0995-4ebf-9e21-997f433a5822/image.png" alt="책 그림 1-1, ML시스템의 일반적인 구성 요소"></p>
<h4 id="이를-위한-가장-단순한-아키텍처는-모놀리식-배치-아키텍처다">&lt; 이를 위한 가장 단순한 아키텍처는 <code>모놀리식 배치 아키텍처</code>다. &gt;</h4>
<p>이 구조는 학습과 서빙 사이의 왜곡(training-serving skew)을 피할 수 있다는 장점이 있지만, 다음과 같은 단점을 가진다.</p>
<ul>
<li>특성 재사용성이 낮음</li>
<li>데이터 양이 증가하면 PySpark, Ray 등으로 리팩터링 필요</li>
<li>팀 간 협업 어려움</li>
<li>실시간 학습 전환의 어려움</li>
</ul>
<h4 id="단점을-극복하기-위한-대안으로-무상태-실시간-아키텍처">&lt; 단점을 극복하기 위한 대안으로 <code>무상태 실시간 아키텍처</code> &gt;</h4>
<p>하지만 <strong>실시간으로 대용량 데이터를 처리하며 지속 학습을 수행하는 것은 여전히 도전적</strong>이다. 예컨대 영화 추천 시스템처럼, 사용자 상태와 맥락에 따라 전혀 다른 예측이 필요한 경우, 이러한 시스템 설계는 복잡해진다.</p>
<h4 id="이러한-문제의-해결을-위해-구글은-다음과-같은-아키텍처를-제시했다">&lt; 이러한 문제의 해결을 위해, 구글은 다음과 같은 아키텍처를 제시했다. &gt;</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/cbbacbfc-a61f-4861-80c0-89932d697c97/image.png" alt=""></p>
<p>(실제 <a href="https://cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?hl=ko">MLOps: 머신러닝의 지속적 배포 및 자동화 파이프라인</a> 의 구글 문서에서도 따온 이미지로 설명함)</p>
<h4 id="fti-아키텍처---ml-시스템의-본질">&lt; FTI 아키텍처 - ML 시스템의 본질 &gt;</h4>
<p><code>Feature - Training - Inference</code> 로 구성된 FTI 아키텍쳐가 논리적인 ML 구조에 가장 부합하고, 구글 그림을 3개로 단순화 할 수 있다고 한다. </p>
<p>전통적인 웹 시스템에서의 <code>DB - Business Logic - UI</code> 계층처럼, <strong>각각의 독립성과 연결성을 모두 갖춘 구조</strong>로 설명된다. 그렇기 때문에 &quot;다양한 팀에서 관리하고, 재사용할 수 있는 형태&quot; 라고 한다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9de5ccb3-744e-47f9-8c14-12485b191d6a/image.png" alt=""></p>
<h4 id="1-특성-파이프라인-feature-pipeline">&lt; 1. 특성 파이프라인 (Feature Pipeline) &gt;</h4>
<ul>
<li>원시 데이터를 수집하고 필요한 특성과 레이블로 가공</li>
<li>이 결과는 특성 저장소(Feature Store)에 저장됨</li>
<li>다양한 팀에서 재사용할 수 있는 형태로 구성됨</li>
</ul>
<h4 id="2-학습-파이프라인-training-pipeline">&lt; 2. 학습 파이프라인 (Training Pipeline) &gt;</h4>
<ul>
<li>Feature Store에서 특성과 레이블을 불러와 모델 학습을 진행</li>
<li>하나 이상의 모델을 생성하고, 이를 모델 레지스트리(Model Registry)에 저장</li>
</ul>
<h4 id="3-추론-파이프라인-inference-pipeline">&lt; 3. 추론 파이프라인 (Inference Pipeline) &gt;</h4>
<ul>
<li>입력: 특성, 레이블, 학습된 모델</li>
<li>출력: 예측값</li>
<li>사용 방식: 배치 또는 실시간 형태</li>
</ul>
<p>각 파이프라인은 <strong>독립적으로 실행 가능한 모듈</strong>이며, 각각 다른 팀이 운영하거나 독립적으로 확장/교체할 수 있다.</p>
<p>그리고 저자는 ML 시스템이 아무리 복잡해져도 이 기본 구조는 변하지 않을 것이라고 한다. (저자는 각 파이프라인이 현업에서 보통 어떤 팀이 담당할지도 언급을 한다.)</p>
<h3 id="ch-2-도구-및-설치">CH 2. 도구 및 설치</h3>
<p>이 장은 개념적 설명보다는 <strong>LLM Twin 구축에 필요한 도구 소개와 사전 환경 세팅 가이드</strong>에 초점이 맞춰져 있다. 개발 환경을 제대로 구축하는 것은 이 책의 실습을 따라가기에 필수이며, 특히 Python에 익숙하지 않은 독자라면 진입장벽이 다소 높을 수 있다. (사실 이 책 자체가 익숙하지 않는 독자 대상이 아닌 듯 하다.) </p>
<p>참고로 <a href="https://github.com/inrap8206/LLM-Engineers-Handbook">https://github.com/inrap8206/LLM-Engineers-Handbook</a> 에 레포 세팅을 따라가려면 대부분의 API 키 세팅이 필요하니, 해당 장에서 미리 clone 하고 .env.sample 에 맞춰 키세팅을 하는 것을 추천한다.</p>
<h4 id="python-환경-세팅">&lt; Python 환경 세팅 &gt;</h4>
<ul>
<li><strong><code>pyenv</code></strong>: 다양한 버전의 Python을 손쉽게 설치하고 관리할 수 있는 도구.</li>
<li><strong><code>poetry</code></strong>: 패키지와 의존성 관리를 위한 현대적인 도구. <code>pyproject.toml</code> 중심의 구성 파일을 사용하여 환경을 선언적으로 관리한다. <a href="https://velog.io/@qlgks1/python-poetry-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0">python - poetry 설치부터 project initializing, 활용하기</a> 참조</li>
<li><strong><code>poe the poet</code></strong>: poetry의 스크립트 실행 기능을 더 직관적으로 만들어주는 도구. CLI 명령어를 간편하게 정의할 수 있으며, 공식 사이트는 <a href="https://poethepoet.natn.io/index.html">poethepoet.natn.io</a>.<ul>
<li>참고로 프로젝트 세팅할때 <code>poetry self add &#39;poethepoet[poetry_plugin]&#39;</code> 해야 함!</li>
</ul>
</li>
</ul>
<h4 id="huggingface">&lt; HuggingFace &gt;</h4>
<ul>
<li>모델과 토크나이저를 쉽게 불러올 수 있는 <strong>모델 레지스트리 기능</strong>을 제공.</li>
<li>여러 프로젝트에서 동일한 모델을 재사용하거나 커스터마이징할 때 유용.</li>
</ul>
<h4 id="zenml">&lt; ZenML &gt;</h4>
<ul>
<li><strong>오케스트레이터</strong>, <strong>아티팩트</strong>, <strong>메타데이터 관리</strong> 기능을 갖춘 프레임워크.</li>
<li>ML 워크플로우를 재현 가능하고 구조적으로 설계할 수 있도록 도와준다.</li>
<li>핵심은 <code>DAG(Directed Acyclic Graph)</code> 기반의 처리.</li>
<li>비슷한 도구로는 <strong>Airflow</strong>, <strong>Prefect</strong>, <strong>Metaflow</strong>, <strong>Dagster</strong> 등이 있다.</li>
<li>ZenML은 특히 <strong>실습과 실전 프로젝트 모두에 적합하도록 구성된 모던한 워크플로우 엔진</strong>이라는 점에서 강점이 있다.</li>
<li><code>poetry run poe local-zenml-server-up</code> 로 local-server zenml 을 바로 띄울 수 있다.</li>
</ul>
<pre><code class="language-shell">❯ poetry run poe local-zenml-server-up

Poe &lt;= sys.platform
Poe =&gt; poetry run zenml up --blocking
The local ZenML dashboard is about to deploy in a blocking process. You can connect to it using the 
&#39;default&#39; username and an empty password.
Deploying a local ZenML server with name &#39;local&#39;.
Initializing the ZenML global configuration version to 0.67.0
Starting ZenML Server as blocking process... press CTRL+C once to stop it.
INFO:     Started server process [97367]
INFO:     Waiting for application startup.
Not writing the global configuration to disk in a ZenML server environment.
Not writing the global configuration to disk in a ZenML server environment.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8237 (Press CTRL+C to quit)
INFO:     127.0.0.1:65079 - &quot;GET / HTTP/1.1&quot; 200 OK</code></pre>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/7ca59f1f-44a4-4c18-a758-5188338f39d3/image.png" alt=""></p>
<h4 id="comet-ml">&lt; Comet ML &gt;</h4>
<ul>
<li>실험 추적을 위한 대표적 SaaS 도구.</li>
<li>반복적인 모델 실험을 효율적으로 추적 가능.</li>
<li>다음과 같은 지표를 시각화 및 기록:<ul>
<li>학습 및 평가 손실</li>
<li>gradient norm (손실 함수에 대한 기울기의 크기)</li>
<li>모델 예측 결과 등</li>
</ul>
</li>
</ul>
<h4 id="opik">&lt; Opik &gt;</h4>
<ul>
<li><strong>프롬프트 모니터링 도구</strong>로 소개됨.</li>
<li>생성형 AI의 성능을 추적하고 품질을 정량화하기 위한 도구로 보임 (자세한 내용은 다소 제한적이지만, 프롬프트 기반 실험을 추적하는 데 활용 가능).</li>
</ul>
<h4 id="데이터-저장-및-처리-도구">&lt; 데이터 저장 및 처리 도구 &gt;</h4>
<ul>
<li><strong>MongoDB</strong>: 문서 기반의 대표적인 NoSQL 데이터베이스. 비정형 데이터를 유연하게 다룰 수 있음.</li>
<li><strong>Qdrant</strong>: 고성능 <strong>벡터 검색 DB</strong>. 대규모 임베딩 데이터를 저장하고 빠르게 검색할 수 있는 백터 DB로, RAG 시스템과 궁합이 좋음. (PS. 저자의 말을 빌리자면 요즘 대부분의 성능에서 모난 곳 없이 가장 안정적이라고 한다.)</li>
</ul>
<h4 id="aws-세팅-및-sagemaker-소개">&lt; AWS 세팅 및 SageMaker 소개 &gt;</h4>
<ul>
<li>SageMaker는 <strong>학습과 추론을 위한 클라우드 기반 ML 플랫폼</strong>.</li>
<li>GPU 클러스터에서 모델을 학습하거나 파인튜닝할 수 있으며, REST API 형태로 배포 가능.</li>
<li>LLM Twin을 배포할 때, 전 세계 사용자들이 실시간으로 접근 가능하도록 만드는 <strong>핵심 인프라</strong> 역할을 수행.</li>
</ul>
<p>PS. 이 장은 진짜 재미있는게 저자의 짬바가 느껴지는 SaaS &amp; PaaS 를 소개해줘서 오히려 좋았다. 그 목적과 이유도 명백해서 더 와닿았고 ㅎㅎ.</p>
<h3 id="ch-3-데이터-엔지니어링">CH 3. 데이터 엔지니어링</h3>
<p>이 장에서는 LLM Twin 프로젝트의 데이터 수집과 저장 구조, 즉 ETL 파이프라인을 어떻게 설계하고 구현하는지에 대한 전체 그림을 다룬다. MongoDB를 데이터 웨어하우스로 상정하고, 크롤러 → 파이프라인 → 저장의 흐름으로 진행되며, 핵심은 &#39;카테고리 중심 수집 체계&#39;와 &#39;ZenML 기반 자동화&#39;에 있다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/aab8fa09-7407-418f-8e81-393d9af1a0b7/image.png" alt=""></p>
<h4 id="카테고리-기반-수집-구조">&quot;카테고리&quot; 기반 수집 구조</h4>
<p>수집 데이터는 플랫폼 단위로 나누지 않고, <strong><em>데이터 카테고리에 귀속</em></strong> 시켜 구성한다.
예를 들어 Medium은 ‘아티클’, GitHub은 ‘레포지토리’, LinkedIn 포스트는 ‘게시물’처럼 정의하여 확장성 있는 구조를 만든다.</p>
<ul>
<li>PoC에서는 수백 개 단위로 충분할 수 있지만</li>
<li>실 운영환경에서는 수천 개 이상의 데이터가 필요하다</li>
<li>따라서 &#39;완성도 높은 설계&#39;보다는 &#39;점진적 진화&#39;를 강조한다</li>
</ul>
<p>이게 특성 파이프라인과 어떻게 연결되는가? -&gt; <code>MongoDB</code> 에 저장된 원시 데이터를 가져와 <strong>*&quot;특성으로 변환&quot;*</strong> 하는 후속작업을 통해 <code>Qdrant Vector DB</code> 에 저장할 것이다.</p>
<p>참고로 여기서부턴 <a href="http://github.com/inrap8206/LLM-Engineers-Handbook">http://github.com/inrap8206/LLM-Engineers-Handbook</a> 레포를 보면서 따라오는게 좋다. (clone 을 먼저 하는 것을 추천)</p>
<h4 id="전체적인-구조">전체적인 구조</h4>
<svg viewBox="0 0 1400 900" xmlns="http://www.w3.org/2000/svg">
  <!-- Background -->
  <rect width="1400" height="900" fill="#f8f9fa"/>

  <!-- Title -->
<p>  <text x="700" y="30" font-size="24" font-weight="bold" text-anchor="middle" fill="#1a1a1a">데이터 수집 파이프라인 상세 분석</text></p>
  <!-- ZenML Pipeline Structure -->
  <g transform="translate(50, 70)">
    <rect x="0" y="0" width="600" height="200" fill="#e8eaf6" stroke="#3f51b5" stroke-width="2" rx="10"/>
    <text x="300" y="30" font-size="18" font-weight="bold" text-anchor="middle">ZenML Pipeline: digital_data_etl</text>
    <line x1="20" y1="40" x2="580" y2="40" stroke="#3f51b5" stroke-width="1"/>

<pre><code>&lt;!-- Step 1 --&gt;
&lt;rect x=&quot;30&quot; y=&quot;60&quot; width=&quot;250&quot; height=&quot;60&quot; fill=&quot;#fff&quot; stroke=&quot;#3f51b5&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;155&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;Step 1: get_or_create_user&lt;/text&gt;
&lt;text x=&quot;155&quot; y=&quot;105&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;#666&quot;&gt;User 생성/조회&lt;/text&gt;

&lt;!-- Step 2 --&gt;
&lt;rect x=&quot;320&quot; y=&quot;60&quot; width=&quot;250&quot; height=&quot;60&quot; fill=&quot;#fff&quot; stroke=&quot;#3f51b5&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;445&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;Step 2: crawl_links&lt;/text&gt;
&lt;text x=&quot;445&quot; y=&quot;105&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;#666&quot;&gt;링크별 크롤링 실행&lt;/text&gt;

&lt;!-- Flow --&gt;
&lt;path d=&quot;M 280 90 L 320 90&quot; stroke=&quot;#3f51b5&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow)&quot;/&gt;

&lt;!-- Inputs/Outputs --&gt;
&lt;text x=&quot;30&quot; y=&quot;150&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;Inputs:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;170&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• user_full_name: str&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;185&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• links: list[str]&lt;/text&gt;

&lt;text x=&quot;320&quot; y=&quot;150&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;Outputs:&lt;/text&gt;
&lt;text x=&quot;330&quot; y=&quot;170&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• UserDocument → MongoDB&lt;/text&gt;
&lt;text x=&quot;330&quot; y=&quot;185&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Article/Post/Repo Documents → MongoDB&lt;/text&gt;</code></pre>  </g>

  <!-- Crawler Dispatcher Pattern -->
  <g transform="translate(700, 70)">
    <rect x="0" y="0" width="650" height="200" fill="#f3e5f5" stroke="#7b1fa2" stroke-width="2" rx="10"/>
    <text x="325" y="30" font-size="18" font-weight="bold" text-anchor="middle">Crawler Dispatcher 패턴</text>
    <line x1="20" y1="40" x2="630" y2="40" stroke="#7b1fa2" stroke-width="1"/>

<pre><code>&lt;text x=&quot;30&quot; y=&quot;65&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;URL 패턴 매칭:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;85&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• https://medium.com/* → MediumCrawler&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;100&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• https://linkedin.com/* → LinkedInCrawler&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;115&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• https://github.com/* → GithubCrawler&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;130&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• 기타 → CustomArticleCrawler (기본값)&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;160&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;Registry 패턴:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;180&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;_crawlers = {&quot;regex_pattern&quot;: CrawlerClass}&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;195&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;동적 크롤러 선택 및 실행&lt;/text&gt;</code></pre>  </g>

  <!-- Crawler Details -->
  <g transform="translate(50, 300)">
    <rect x="0" y="0" width="400" height="250" fill="#e3f2fd" stroke="#1976d2" stroke-width="2" rx="10"/>
    <text x="200" y="30" font-size="16" font-weight="bold" text-anchor="middle">Medium Crawler 상세</text>
    <line x1="20" y1="40" x2="380" y2="40" stroke="#1976d2" stroke-width="1"/>

<pre><code>&lt;text x=&quot;30&quot; y=&quot;65&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;기술 스택:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;85&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Selenium WebDriver (헤드리스 Chrome)&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;100&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• BeautifulSoup4 (HTML 파싱)&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;115&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• chromedriver-autoinstaller&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;140&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;크롤링 프로세스:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;160&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;1. driver.get(link) - 페이지 로드&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;175&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;2. scroll_page() - 동적 콘텐츠 로딩&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;190&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;3. BeautifulSoup으로 HTML 파싱&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;205&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;4. 제목, 부제목, 본문 추출&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;230&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;추출 데이터:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;245&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;{&quot;Title&quot;: str, &quot;Subtitle&quot;: str, &quot;Content&quot;: str}&lt;/text&gt;</code></pre>  </g>

  <g transform="translate(480, 300)">
    <rect x="0" y="0" width="400" height="250" fill="#e8f5e9" stroke="#388e3c" stroke-width="2" rx="10"/>
    <text x="200" y="30" font-size="16" font-weight="bold" text-anchor="middle">GitHub Crawler 상세</text>
    <line x1="20" y1="40" x2="380" y2="40" stroke="#388e3c" stroke-width="1"/>

<pre><code>&lt;text x=&quot;30&quot; y=&quot;65&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;기술 스택:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;85&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• subprocess (git clone)&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;100&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• os.walk (파일 탐색)&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;115&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• tempfile (임시 디렉토리)&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;140&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;크롤링 프로세스:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;160&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;1. tempfile.mkdtemp() - 임시 디렉토리&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;175&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;2. git clone [repo_url]&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;190&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;3. 파일 트리 순회 및 읽기&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;205&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;4. ignore 패턴 필터링&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;230&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;추출 데이터:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;245&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;{file_path: file_content} 딕셔너리&lt;/text&gt;</code></pre>  </g>

  <g transform="translate(910, 300)">
    <rect x="0" y="0" width="440" height="250" fill="#fff3e0" stroke="#f57c00" stroke-width="2" rx="10"/>
    <text x="220" y="30" font-size="16" font-weight="bold" text-anchor="middle">LinkedIn Crawler (Deprecated)</text>
    <line x1="20" y1="40" x2="420" y2="40" stroke="#f57c00" stroke-width="1"/>

<pre><code>&lt;text x=&quot;30&quot; y=&quot;65&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;기술적 이슈:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;85&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• LinkedIn의 보안 강화로 로그인 차단&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;100&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• reCAPTCHA 및 봇 감지 시스템&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;115&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• 현재 _is_deprecated=True 상태&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;140&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot;&gt;원래 구현 내용:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;160&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• 프로필 정보 수집 (About, Experience)&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;175&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• 포스트 및 이미지 수집&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;190&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• 무한 스크롤 처리&lt;/text&gt;

&lt;text x=&quot;30&quot; y=&quot;215&quot; font-size=&quot;12&quot; font-weight=&quot;bold&quot; fill=&quot;#d32f2f&quot;&gt;⚠️ 대안:&lt;/text&gt;
&lt;text x=&quot;40&quot; y=&quot;235&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;LinkedIn API 또는 수동 데이터 수집 필요&lt;/text&gt;</code></pre>  </g>

  <!-- Data Flow to MongoDB -->
  <g transform="translate(50, 580)">
    <rect x="0" y="0" width="1300" height="280" fill="#e0f2f1" stroke="#00695c" stroke-width="2" rx="10"/>
    <text x="650" y="30" font-size="18" font-weight="bold" text-anchor="middle">MongoDB 저장 프로세스</text>
    <line x1="20" y1="40" x2="1280" y2="40" stroke="#00695c" stroke-width="1"/>

<pre><code>&lt;!-- Document Models --&gt;
&lt;rect x=&quot;40&quot; y=&quot;60&quot; width=&quot;280&quot; height=&quot;120&quot; fill=&quot;#fff&quot; stroke=&quot;#00695c&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;180&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;ArticleDocument&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;105&quot; font-size=&quot;11&quot;&gt;content: dict&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;120&quot; font-size=&quot;11&quot;&gt;link: str&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;135&quot; font-size=&quot;11&quot;&gt;platform: str&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;150&quot; font-size=&quot;11&quot;&gt;author_id: UUID&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;165&quot; font-size=&quot;11&quot;&gt;author_full_name: str&lt;/text&gt;

&lt;rect x=&quot;350&quot; y=&quot;60&quot; width=&quot;280&quot; height=&quot;120&quot; fill=&quot;#fff&quot; stroke=&quot;#00695c&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;490&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;PostDocument&lt;/text&gt;
&lt;text x=&quot;360&quot; y=&quot;105&quot; font-size=&quot;11&quot;&gt;content: dict&lt;/text&gt;
&lt;text x=&quot;360&quot; y=&quot;120&quot; font-size=&quot;11&quot;&gt;image: Optional[str]&lt;/text&gt;
&lt;text x=&quot;360&quot; y=&quot;135&quot; font-size=&quot;11&quot;&gt;platform: str&lt;/text&gt;
&lt;text x=&quot;360&quot; y=&quot;150&quot; font-size=&quot;11&quot;&gt;author_id: UUID&lt;/text&gt;
&lt;text x=&quot;360&quot; y=&quot;165&quot; font-size=&quot;11&quot;&gt;author_full_name: str&lt;/text&gt;

&lt;rect x=&quot;660&quot; y=&quot;60&quot; width=&quot;280&quot; height=&quot;120&quot; fill=&quot;#fff&quot; stroke=&quot;#00695c&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;800&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;RepositoryDocument&lt;/text&gt;
&lt;text x=&quot;670&quot; y=&quot;105&quot; font-size=&quot;11&quot;&gt;content: dict (file tree)&lt;/text&gt;
&lt;text x=&quot;670&quot; y=&quot;120&quot; font-size=&quot;11&quot;&gt;name: str&lt;/text&gt;
&lt;text x=&quot;670&quot; y=&quot;135&quot; font-size=&quot;11&quot;&gt;link: str&lt;/text&gt;
&lt;text x=&quot;670&quot; y=&quot;150&quot; font-size=&quot;11&quot;&gt;platform: str = &quot;github&quot;&lt;/text&gt;
&lt;text x=&quot;670&quot; y=&quot;165&quot; font-size=&quot;11&quot;&gt;author_id: UUID&lt;/text&gt;

&lt;rect x=&quot;970&quot; y=&quot;60&quot; width=&quot;280&quot; height=&quot;120&quot; fill=&quot;#fff&quot; stroke=&quot;#00695c&quot; stroke-width=&quot;2&quot; rx=&quot;5&quot;/&gt;
&lt;text x=&quot;1110&quot; y=&quot;85&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot; text-anchor=&quot;middle&quot;&gt;UserDocument&lt;/text&gt;
&lt;text x=&quot;980&quot; y=&quot;105&quot; font-size=&quot;11&quot;&gt;first_name: str&lt;/text&gt;
&lt;text x=&quot;980&quot; y=&quot;120&quot; font-size=&quot;11&quot;&gt;last_name: str&lt;/text&gt;
&lt;text x=&quot;980&quot; y=&quot;135&quot; font-size=&quot;11&quot;&gt;full_name: property&lt;/text&gt;
&lt;text x=&quot;980&quot; y=&quot;150&quot; font-size=&quot;11&quot;&gt;id: UUID (auto)&lt;/text&gt;

&lt;!-- Save Methods --&gt;
&lt;text x=&quot;40&quot; y=&quot;210&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot;&gt;저장 메서드:&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;230&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• instance.save() - 단일 문서 저장&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;245&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Model.bulk_insert(documents) - 배치 저장&lt;/text&gt;
&lt;text x=&quot;50&quot; y=&quot;260&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Model.get_or_create(**kwargs) - 중복 방지&lt;/text&gt;

&lt;!-- MongoDB Structure --&gt;
&lt;text x=&quot;500&quot; y=&quot;210&quot; font-size=&quot;14&quot; font-weight=&quot;bold&quot;&gt;MongoDB 컬렉션:&lt;/text&gt;
&lt;text x=&quot;510&quot; y=&quot;230&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Database: twin&lt;/text&gt;
&lt;text x=&quot;510&quot; y=&quot;245&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Collections: users, articles, posts, repositories&lt;/text&gt;
&lt;text x=&quot;510&quot; y=&quot;260&quot; font-size=&quot;11&quot; fill=&quot;#666&quot;&gt;• Connection: mongodb://llm_engineering:llm_engineering@127.0.0.1:27017&lt;/text&gt;</code></pre>  </g>

  <!-- Arrows -->
  <defs>
    <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#666" />
    </marker>
  </defs>
</svg>

<h4 id="zenml-기반-pipeline-구조">ZenML 기반 Pipeline 구조</h4>
<p>ZenML 파이프라인 digital_data_etl은 다음과 같은 구조다.</p>
<ul>
<li><code>get_or_create_user</code><ul>
<li>사용자 정보를 생성하거나 조회</li>
<li>user_full_name, UUID 기준으로 처리</li>
</ul>
</li>
<li><code>crawl_links</code><ul>
<li>전달받은 링크들을 기준으로 크롤링 수행</li>
<li>링크 리스트는 ZenML step에 입력으로 들어감</li>
</ul>
</li>
</ul>
<p>이 파이프라인은 완전히 모듈화되어 있고, 입력/출력에 따라 자동으로 <code>MongoDB</code> 에 저장된다. <code>poetry poe run-digital-data-etl</code> 로 실행하면 된다.</p>
<pre><code class="language-bash">poetry run python -m tools.run --run-etl --no-cache --etl-config-filename digital_data_etl_maxime_labonne.yaml
poetry run python -m tools.run --run-etl --no-cache --etl-config-filename digital_data_etl_paul_iusztin.yaml</code></pre>
<p><code>digital_data_etl_maxime_labonne.yaml</code> 과 <code>digital_data_etl_paul_iusztin.yaml</code> 에 설정에 따라 실행되며 아래와 같이 zenml 이 추적된다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1034c629-c9a6-43bf-8f7d-dcca5e6c985b/image.png" alt=""></p>
<h4 id="dispatcher--registry-패턴">Dispatcher &amp; Registry 패턴</h4>
<p>크롤러 로직은 Dispatcher로 관리되며, URL 패턴에 따라 자동으로 적절한 크롤러가 선택된다.</p>
<ul>
<li>예를 들어 아래와 같다.<ul>
<li><a href="https://medium.com/">https://medium.com/</a>* → MediumCrawler</li>
<li><a href="https://github.com/">https://github.com/</a>* → GithubCrawler</li>
<li>기타 → CustomArticleCrawler</li>
</ul>
</li>
</ul>
<p>이는 내부적으로 <code>_crawlers = {regex_pattern: CrawlerClass}</code> 형태의 Registry를 유지하며, 각 Crawler는 템플릿 메서드 패턴으로 공통 구조를 따르면서도 플랫폼에 특화된 로직을 담는다. </p>
<blockquote>
<p>개인적으로 해당 파이프라인은 독자마다 철저하게 커스텀해서 세팅하는 것을 강력 추천한다. 솔직히 &quot;카테고리&quot; 와 &quot;MongoDB&quot; 적재 세팅만 맞추면 파이프라인은 알아서 구성하는 것이 나을 것 같다. <strong><em>나의 경우 <code>github</code>, <code>velog</code>, 그리고 <code>notion</code> 을 사용했다.</em></strong> (참고로 처음 읽을땐 그냥 한 번 처음부터 끝까지 따라가보고 난 뒤에 바꿨다.)</p>
</blockquote>
<p>(PS. 다른 원천 데이터 셋을 구성하려면, 스크래핑과 크롤링에 익숙하지 않은 사람이라면 해당 장에서 꾀나 애먹을 수 도 있다.)</p>
<h4 id="mongodb-저장-구조-및-document-구성">MongoDB 저장 구조 및 Document 구성</h4>
<p>데이터 저장은 twin 이라는 데이터베이스 내 아래 컬렉션들로 구성된다.</p>
<ul>
<li><code>users</code><ul>
<li>UserDocument (full_name, id 등)</li>
</ul>
</li>
<li><code>articles</code><ul>
<li>ArticleDocument (content, link, platform, author_id 등)</li>
</ul>
</li>
<li><code>posts</code><ul>
<li>PostDocument (content, image, platform 등)</li>
</ul>
</li>
<li><code>repositories</code><ul>
<li>RepositoryDocument (file tree, name, link 등)</li>
</ul>
</li>
</ul>
<p>저장 방식은 다음과 같다.</p>
<ul>
<li><code>instance.save()</code> → 단일 문서 저장</li>
<li><code>Model.bulk_insert(docs)</code> → 다수 문서 저장</li>
<li><code>Model.get_or_create(**kwargs)</code> → 중복 방지 저장</li>
</ul>
<p>PS. 깃허브 레포 기준 <code>llm_engineering/domain/base/nosql.py</code> 에 기본 ODM class 를 구현했다. 사실 이 부분에서 이 책은 확실히 진입장벽이 높다고 많이 느겼다. (근데 코드와 다르게 설명은 이제 막 NoSQL 이 뭔지 배운 사람에게 설명하는 듯하다 ㅋㅋㅋ)</p>
<p>이후  <code>poe the poet</code> 로 사전에 세팅된 <code>poe command</code> 로 바로 시작할 수 있다. </p>
<h3 id="ch-4-rag-특성-파이프라인">CH 4. RAG 특성 파이프라인</h3>
<p>개인적으로 이 책의 정수는 4장부터라고 생각된다. 본격적으로 Retrieval-Augmented Generation(RAG)에 대한 구조와 개념이 실제 시스템에 어떻게 구현되는지를 보여주는 장이며, 단순 이론에 그치지 않고 LLM Twin 프로젝트의 구체적인 사례로 연결되는 부분이 핵심이다.</p>
<h4 id="rag의-개요와-목적">RAG의 개요와 목적</h4>
<p>RAG는 이름 그대로 <strong>Retrieval(검색)</strong>, <strong>Augmented(증강)</strong>, <strong>Generation(생성)</strong>의 세 단계를 조합한 구조다. 모든 LLM은 기본적으로 <strong><em>매개변수화된 지식(parameterized knowledge)</em></strong>에 의존한다. 즉, 사전 학습된 데이터에 기반한 지식을 제공하므로 최신 정보가 반영되지 않거나 사전에 포함되지 않은 정보를 요청하면 할루시네이션(환각) 문제가 발생할 수 있다.</p>
<p>이를 해결하기 위해 외부 정보에 접근할 수 있는 구조가 필요하며, 대표적인 해결책이 RAG다. 특히 금융 비서, 실시간 뉴스 기반 어시스턴트처럼 <strong>외부 정보의 실시간 접근이 필수적인</strong> 도메인에서는 RAG 구조가 매우 효과적이다.</p>
<h4 id="1-수집-파이프라이닝">1. 수집 파이프라이닝</h4>
<ul>
<li><strong>데이터 추출 모듈</strong>: 다양한 소스(Crawling, DW 등)에서 원시 데이터 수집</li>
<li><strong>정제 모듈</strong>: 수집된 데이터를 표준화, 정규화, 정제</li>
<li><strong>청킹 모듈</strong>: 모델 처리 효율성을 위해 작은 단위로 문서 분할</li>
<li><strong>임베딩 모듈</strong>: 청킹된 문서를 벡터화</li>
<li><strong>로딩 모듈</strong>: 임베딩 결과와 메타데이터를 함께 Vector DB에 저장</li>
</ul>
<h4 id="2-검색-파이프라이닝">2. 검색 파이프라이닝</h4>
<ul>
<li>사용자 입력(텍스트, 이미지 등)을 임베딩</li>
<li>Vector DB에서 유사한 벡터를 K개 검색 (코사인 거리 등 활용)</li>
<li>검색 결과를 LLM 프롬프트에 보강 정보로 삽입</li>
</ul>
<p>여기서 사용하는 거리 계산법 중 가장 일반적인 것은 <strong>코사인 거리</strong>이며, 수식은 다음과 같다:</p>
<p>$$
\text{Cosine Similarity}(A, B) = \frac{A \cdot B}{||A|| \times ||B||}
$$</p>
<p>$$
\text{Cosine Distance} = 1 - \text{Cosine Similarity}
$$</p>
<p>두 벡터 사이 각도의 코사인 값을 1에서 뺀 값이며, -1 에서 1 사이 값을 가진다.(벡터 서로 반대일 때 -1, 수직일 때 0, 같은 방향 1)</p>
<h4 id="3-생성-파이프라이닝">3. 생성 파이프라이닝</h4>
<ul>
<li>검색된 문서 + 사용자 입력 → 프롬프트로 구성</li>
<li>LLM에 전달하여 최종 답변 생성</li>
<li>프롬프트도 버전 관리가 필요하면 <code>LangFuse</code> 같은 도구로 모니터링 및 관리 가능</li>
</ul>
<h4 id="임베딩">임베딩</h4>
<p>임베딩은 단어, 이미지, 추천 시스템의 항목 등 객체를 연속적인 벡터 공간에 벡터로 인코딩한 밀집된 숫자 표현이다. 의미론적 의미(semantic meaning) 와 의미론적 관계(semantic relationship)를 포착하는데 도움 준다. (ML은 오직 숫자만 처리 가능하다. 왜? 임베딩 필요? 는 skip)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a4f9978c-7ff6-4b3f-840a-f819ebbcb9e2/image.png" alt=""></p>
<p>(출처: <a href="https://uracle.blog/2025/03/14/embedding/">https://uracle.blog/2025/03/14/embedding/</a>)</p>
<ul>
<li>일반적인 차원: <strong>64 ~ 2048차원</strong></li>
<li>시각화 도구: <strong>UMAP</strong> (사람이 인지하는 2-3차원 보다 훨씬 높기 때문에 시각화 못함. 근데 강제로 3차원으로 축소해서 시각화)</li>
<li>초기 기법: <strong>Word2Vec, GloVe</strong></li>
<li>현대 기법: <strong>BERT, RoBERTa</strong>, <code>sentence-transformers</code> (Python)</li>
<li>벤치마크: <strong>HuggingFace의 MTEB</strong></li>
</ul>
<p>이미지 임베딩은 CNN, 특히 ResNet을 주로 활용하며, 이런 임베딩된 값들을 저장하기 위해 기존 스칼라 기반 DB는 이를 다루기 어려워 Vector DB가 부상하게 됐다. vecotr dbms 대신 <code>FAISS</code> 같은 독립형 벡터 인덱스도 유사도 검색에 효과적이긴 하지만 포괄적인 관리 기능 부족하다. </p>
<p>원-핫 인코딩과 같은 간단한 방법은 왜 안하는가? -&gt; <a href="https://velog.io/@jh_ds/%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%9E%84%EB%B2%A0%EB%94%A9-one-hot-encoding%EC%9D%98-%ED%95%9C%EA%B3%84%EC%A0%90%EA%B3%BC-word2vec-omfblsx4">&quot;차원의 저주 문제&quot;가 있다.</a></p>
<p>텍스트 임베딩 기법으로 <code>Word2Vec</code>, <code>GloVe</code> 등이 초기에 등장했고, 오늘날엔 <code>BERT</code>, <code>RoBERTa</code> 트랜스포머 모델이 유명하다. 더욱이 요즘에 쉽게 임베딩 모델 바로 사용가며, python <code>Sentence Transformers</code> 패키지 같은거 사용해보면 좋다! </p>
<p>PS) 허깅 페이스 MTEB(Massive Text Embedding Benchmark) 통해 임베딩 모델 성능 비교 가능.</p>
<h4 id="vector-db의-작동-방식-요약">Vector DB의 작동 방식 요약</h4>
<ul>
<li>임베딩된 입력 쿼리 → Vector DB로 전달</li>
<li><strong>ANN(Approximate Nearest Neighbor)</strong> 방식으로 근접 벡터 검색</li>
<li>후처리로 정렬, 필터링 등의 단계 존재</li>
</ul>
<h4 id="rag-최적화의-세-단계">RAG 최적화의 세 단계</h4>
<ol>
<li><strong>검색 전처리 (Pre-retrieval)</strong><ul>
<li>데이터 인섹싱과 쿼리 최적화</li>
<li>인덱싱 개선, 쿼리 전처리, 슬라이딩 윈도우, 데이터 세분화 개선 (enhancing data granularity), 메타데이터 구조화, small-to-big, 쿼리 라우팅, 쿼리 재작성, 쿼리 확장 기법 등</li>
</ul>
</li>
<li><strong>검색 최적화</strong><ul>
<li>임베딩 모델 개선 혹은 instructor 모델 활용</li>
<li>하이브리드 검색 (필터링 + 벡터 검색)</li>
</ul>
</li>
<li><strong>검색 후처리</strong><ul>
<li>리랭킹, 프롬프트 압축, 필터링 등</li>
</ul>
</li>
</ol>
<p>정리하자면 RAG는 <strong><em>검색 전처리, 검색, 검색 후처리</em></strong> 세 가지 핵심 단계 개선하는게 중요</p>
<h4 id="llm-twin에서의-특성-파이프라인-구현">LLM Twin에서의 특성 파이프라인 구현</h4>
<p>LLM Twin은 DDD(Domain-Driven Design) 원칙을 기반으로, 데이터 흐름과 상태를 명확히 나눈다. </p>
<ul>
<li><strong>데이터 범주</strong>: 게시물, 기사, 레포지터리</li>
<li><strong>데이터 상태</strong>: 정제됨 / 청킹됨 / 임베딩됨</li>
<li>Pydantic 도메인 엔티티, OVM (VecotrBaseDocument class 참조), 디스패처 계층(핸들러 적용) 와 같이 잘 짜여진 구조얘기도 같이 나온다! <strong><em>이 부분은 정말 직접 구축할 때 많은 도움이 될 것 같다.</em></strong></li>
</ul>
<p>텍스트는 단순히 문자열로 저장됐다고 해서 특성(feature)이 아니며, 모델 입력을 위한 구조화, 즉 <strong>토큰화</strong> 과정이 반드시 필요하다.</p>
<p>LLM Twin은 배치 기반 파이프라인을 선택하며, 스트리밍 기반은 복잡도 및 실시간성을 요구하는 도메인에 한정해 사용한다. 대표적으로 아래와 같은 구조로 구성된다.</p>
<h4 id="llm-twin-rag-특성-파이프라인-5단계">LLM Twin RAG 특성 파이프라인 5단계</h4>
<ol>
<li>데이터 추출</li>
<li>데이터 정제</li>
<li>문서 청킹</li>
<li>임베딩</li>
<li>Vector DB 적재 (Qdrant 활용)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/3947b08c-830e-42df-802b-96fa36d5861c/image.png" alt=""></p>
<h4 id="배치-vs-스트리밍-파이프라인">배치 vs 스트리밍 파이프라인</h4>
<ul>
<li><strong>배치(Batch)</strong> 방식은 데이터를 일정 단위로 모아서 처리하는 형태로, 상대적으로 구현이 단순하고 안정적임.</li>
<li><strong>스트리밍(Streaming)</strong> 방식은 실시간으로 데이터를 처리해야 하므로 구현 복잡성이 높음. <ul>
<li>메시징 큐(MQ), 이벤트 기반 아키텍처, 낮은 지연 시간 설계 필요</li>
<li>실시간 업데이트가 중요한 <strong>틱톡</strong>, <strong>소셜 미디어 추천 시스템</strong>, <strong>사기 탐지 시스템</strong> 등에서 필수적</li>
</ul>
</li>
<li><strong>LLM Twin</strong>에서는 시스템 복잡도를 고려해 <strong>배치 방식</strong>을 채택함. <strong><em>즉 배치 방식으로 Qdrant Vecotr DB Upsert 되는 형태.</em></strong></li>
</ul>
<h4 id="cdcchange-data-capture-활용">CDC(Change Data Capture) 활용</h4>
<ul>
<li>배치 기반 데이터 처리 후, <strong>Qdrant Vector DB에 Upsert</strong> 방식으로 적재함.</li>
<li>데이터 동기화를 위해 <strong>CDC(Change Data Capture)</strong> 방식 활용</li>
<li>CDC 활용 (<a href="https://velog.io/@qlgks1/Elasticsearch-ELK-stack-Postgresql-Logstash-query-based-CDC-%EB%A7%8C%EB%93%A4%EA%B8%B0-by-docker-compose">Elasticsearch - ELK stack &amp; Postgresql &amp; Logstash, query based CDC 만들기 by docker compose</a> / <a href="https://velog.io/@qlgks1/%EC%B9%B4%ED%94%84%EC%B9%B4-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%99%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC-2-Debezium-Postgresql-Django-log-based-CDC-%EB%A7%8C%EB%93%A4%EA%B8%B0-source-sink-connector">카프카 클러스터와 파이썬 (2) - Debezium &amp; Postgresql &amp; Django, log based CDC 만들기 (source &amp; sink connector)</a> 참조!)</li>
<li><strong>업계에서는 로그 기반 CDC</strong>를 더 선호하는 추세 (더 안정적이고 누락 없이 데이터 반영 가능)</li>
</ul>
<h4 id="두-번의-스냅샷-저장-이유">두 번의 스냅샷 저장 이유</h4>
<ol>
<li><strong>데이터 정제 후 저장</strong> → LLM 파인튜닝에 사용  </li>
<li><strong>문서 청킹 + 임베딩 후 저장</strong> → RAG 검색용으로 활용  </li>
</ol>
<h4 id="실제-특성-파이프라인-메인-코드">실제 특성 파이프라인 메인 코드</h4>
<pre><code class="language-python">@pipeline
def feature_engineering(author_full_names: list[str], wait_for: str | list[str] | None = None) -&gt; list[str]:
    raw_documents = fe_steps.query_data_warehouse(author_full_names, after=wait_for)

    cleaned_documents = fe_steps.clean_documents(raw_documents)
    last_step_1 = fe_steps.load_to_vector_db(cleaned_documents)

    embedded_documents = fe_steps.chunk_and_embed(cleaned_documents)
    last_step_2 = fe_steps.load_to_vector_db(embedded_documents)

    return [last_step_1.invocation_id, last_step_2.invocation_id]</code></pre>
<p>위 코드에서는 청크 전/후 데이터를 구분하여 각각 Vector DB에 적재한다. 병렬성 확보를 위해 GIL 제약을 피하고, 각 fetch 함수는 ThreadPoolExecutor 기반으로 실한다.</p>
<p>전체 파이프라인은 <code>poetry poe run-feature-engineering-pipeline</code> 명령어로 실행 가능 하다.</p>
<h3 id="ch-5-지도-학습-파인튜닝">CH 5. 지도 학습 파인튜닝</h3>
<p>LLM을 실제 응용 프로그램에 효과적으로 적용하기 위한 핵심 과정이 바로 <strong>지도 학습 기반 파인튜닝(SFT)</strong> 이다. 사전학습된 모델이 언어 일반 능력을 갖추고 있다면, SFT는 여기에 실전 적합성을 더해주는 역할을 한다. 즉, &quot;일반적인 언어 이해&quot;와 &quot;실전 문제 해결 능력&quot;의 간극을 메우는 것이 SFT의 목적이다.</p>
<p>이 장은 크게 세 부분으로 나뉜다. 1) <strong>지시문 데이터셋 생성</strong>, 2) <strong>SFT 기법</strong>, 3) <strong>파인튜닝 구현</strong>이다.</p>
<h4 id="지시문-데이터셋-파인튜닝에서-가장-어려운-부분">지시문 데이터셋: 파인튜닝에서 가장 어려운 부분</h4>
<p>지도 학습 파인튜닝은 기본적으로 자연스러운 <strong>지시문-응답(instruction-response)</strong> 쌍이 필요하다. 하지만 원시 텍스트를 이러한 구조로 전환하는 것은 쉽지 않다.<br>이 과정은 거의 <strong><em>노가다 수준의 수작업이 필요하고, 무엇보다 데이터 품질이 매우 중요하다.</em></strong></p>
<ul>
<li><strong>Open-Orca/SlimOrca</strong> (<a href="https://huggingface.co/datasets/Open-Orca/SlimOrca">https://huggingface.co/datasets/Open-Orca/SlimOrca</a>) 와 같은 오픈 데이터셋은 좋은 참고 예시이다.</li>
<li><strong>LIMA (Less Is More for Alignment)</strong> 논문에 따르면, 700억 파라미터 모델도 고품질 데이터 샘플 1,000개만으로도 효과적으로 튜닝 가능하다.</li>
</ul>
<h4 id="sft-목적에-따른-모델-유형">SFT 목적에 따른 모델 유형</h4>
<p>파인튜닝 주요 목적은 &quot;작업 특화 모델&quot; 과 &quot;도메인 특화 모델&quot; 을 개발하는 것이다. </p>
<ul>
<li><p><strong>작업 특화 모델(Task-specialized Model)</strong><br>번역, 요약, 감정 분석 등 특정 작업에 최적화<br>→ 작은 모델(8B 이하)도 효율적</p>
</li>
<li><p><strong>도메인 특화 모델(Domain-specialized Model)</strong><br>의료, 법률, 금융, 엔지니어링 등 특정 분야 용어와 언어 패턴 학습<br>→ 도메인 복잡성에 따라 난이도 매우 상이</p>
</li>
</ul>
<h4 id="규칙-기반-필터링-기법">규칙 기반 필터링 기법</h4>
<p><strong>데이터 품질 관리</strong>를 위한 체계적인 방식</p>
<ol>
<li><strong>길이 필터링</strong>: 너무 짧거나 긴 응답 제외</li>
<li><strong>키워드 필터링</strong>: 저품질 키워드 포함 여부로 제거</li>
<li><strong>형식 검사</strong>: JSON, 코드 예제 등 구조 일관성 유지</li>
<li><strong>중복 제거</strong>:<ul>
<li>정확 중복</li>
<li>퍼지(Fuzzy) 중복 (MinHash 등)</li>
<li>의미론적 유사도 기반 중복 제거 (밀집 벡터 - Dense Vector 기반 등)</li>
</ul>
</li>
</ol>
<p>과적합을 방지하기 위해 <strong>유사 샘플 제거</strong>는 반드시 필요하다.</p>
<h4 id="데이터-품질-평가와-자동화-방안">데이터 품질 평가와 자동화 방안</h4>
<p>수작업은 시간과 비용이 많이 들기 때문에, 최근에는 LLM을 평가자로 삼거나, <strong>보상 모델</strong>, <strong>분류기 기반 예측 모델</strong> 등을 사용하는 방식이 시도되고 있다.</p>
<ul>
<li>참고: <a href="https://huggingface.co/RLHFlow/ArmoRM-Llama3-8B-v0.1">ArmoRM-Llama3-8B-v0.1</a>의 보상 모델 기반 아키텍처<br><img src="https://velog.velcdn.com/images/qlgks1/post/4016fa18-5121-40bd-8c3e-8ddec3aaec25/image.png" alt=""></li>
</ul>
<h4 id="lm-twin에서의-실제-적용">LM Twin에서의 실제 적용</h4>
<p>3장에서는 크롤링한 데이터를 기반으로 지시문 데이터셋을 <strong>자체 생성</strong>한다. → 여기엔 두 가지 큰 난제가 존재</p>
<ul>
<li>크롤링 데이터의 <strong>비정형성</strong></li>
<li>크롤링 가능한 <strong>기사 수의 한계</strong></li>
</ul>
<p>이를 해결하기 위해 <strong>합성 데이터 생성 파이프라인</strong>을 구성한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/cb8e3014-4ef2-48ee-bde0-fc4b03efaf03/image.png" alt=""></p>
<p>위 그림과 같이 원시 텍스트에서 지시문 데이터셋으로 합성 데이터 생성 파이프라인을 만들 수 있다. (실제 python 코드 예시가 이어진다.)</p>
<h4 id="지시문-템플릿-구조화">지시문 템플릿 구조화</h4>
<p>지시문-응답 쌍은 모델별로 템플릿이 조금씩 다르다. 예를 들어 OpenAI GPT 계열과 HuggingFace 기반 모델은 프롬프트 구조가 다를 수 있다.</p>
<pre><code>&lt;|im_start|&gt;system
당신은 유용한 AI 도우미입니다.
&lt;|im_end|&gt;
&lt;|im_start|&gt;user
토끼와 거북이 이야기를 요약해줘.
&lt;|im_end|&gt;
&lt;|im_start|&gt;assistant
거북이가 느리지만 꾸준히 가서 결국 토끼를 이깁니다.
&lt;|im_end|&gt;</code></pre><h4 id="sft-기법-세-가지">SFT 기법 세 가지</h4>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/2a37b482-0040-497b-8cb0-1235f67e69f9/image.png" alt=""></p>
<p>(출처: <a href="https://huggingface.co/blog/mlabonne/sft-llama3">https://huggingface.co/blog/mlabonne/sft-llama3</a>)</p>
<ol>
<li><strong>전체 파인튜닝</strong>: 모든 파라미터를 다시 학습</li>
<li><strong>LoRA (Low-Rank Adaptation)</strong>:<ul>
<li>학습 가능한 저랭크 행렬 도입</li>
<li>메모리 사용량 감소 / 빠른 학습 / 파괴 없는 튜닝</li>
<li>작업 간 빠른 전환 가능</li>
</ul>
</li>
<li><strong>QLoRA</strong>:<ul>
<li>LoRA + 양자화(quantization) 기법</li>
<li><strong>NF4 (4비트 커스텀 데이터 타입)</strong> 활용 → 소형 GPU에서도 학습 가능</li>
</ul>
</li>
</ol>
<h4 id="학습-하이퍼파라미터-구성">학습 하이퍼파라미터 구성</h4>
<p>학습 품질을 좌우하는 핵심 요소들</p>
<ol>
<li><strong>학습률 &amp; 스케줄러</strong></li>
<li><strong>배치 크기 (Batch Size)</strong></li>
<li><strong>최대 시퀀스 길이 &amp; 패킹 전략</strong></li>
<li><strong>에포크 수 (Epochs)</strong></li>
<li><strong>옵티마이저 선택 (AdamW 등)</strong></li>
<li><strong>가중치 감소(Weight Decay)</strong></li>
<li><strong>그레디언트 체크포인팅 (Gradient Checkpointing)</strong></li>
</ol>
<p>이후 장에서는 실제로 <strong>SFT 학습 코드 예시</strong>와 함께 실습이 이어진다. 이 장은 실제 SFT의 이론부터 실전 구현까지를 아우르는, 모델을 <strong>현실의 문제 해결 도구로 전환하는 과정</strong>의 핵심이다.</p>
<h3 id="이후-ch-6--11-">이후 CH 6 ~ 11 ...</h3>
<blockquote>
<p>책이 양이 엄청 방대하기에 이후 장은 핵심 주제에 대해서만 요약하고자 한다. 총 평에서 언급했듯 LLM 에 관심이 있다면, 해당 책 정도는 꼭 한 번 찍먹이라도 하면 도움이 많이 될 것 같다.</p>
</blockquote>
<h4 id="ch-6-선호도-정렬을-활용한-파인튜닝">CH 6 선호도 정렬을 활용한 파인튜닝</h4>
<ul>
<li>단순한 SFT(Supervised Fine-Tuning)를 넘어서, <strong>DPO(Direct Preference Optimization)</strong> 같은 선호도 기반 학습 기법에 대한 얘기.</li>
</ul>
<h4 id="ch-7-llm-평가-모델-평가-rag-평가-twinllama-31-8b-평가">CH 7 LLM 평가, 모델 평가, RAG 평가, TwinLlama-3.1-8B 평가</h4>
<ul>
<li>모델의 성능은 <strong>정량 지표만으로는 절대 충분하지 않다</strong>는 것을 강조한다. 특히 RAG 시스템이나 커스텀 모델에서는, 단순 BLEU, ROUGE와 같은 전통적 자연어 지표보다 <strong>&#39;실제 문제 해결 능력&#39;</strong>이 훨씬 중요해진다.</li>
<li>이 챕터에서는 <strong>모델 평가, RAG 평가, TwinLlama-3.1-8B 모델 평가</strong>까지 다루며, 성능 지표와 주관적 평가의 균형을 어떻게 잡을 것인지에 대해 말한다. 개인적으로 기대했던 도메인 특화 파인튜닝에 대한 구체적 평가 방안은 부족했지만, <strong>지표 설계 자체를 실험적으로 풀어내는 의도가 담겨 있었다.</strong></li>
</ul>
<h4 id="ch-8-추론-최적화-모델-최적화-전략-병렬-처리-양자화">CH 8 추론 최적화, 모델 최적화 전략, 병렬 처리, 양자화</h4>
<ul>
<li><strong>모델 최적화 전략:</strong> 레이턴시 감소, 메모리 효율성 증대 / key value 캐싱</li>
<li><strong>병렬 처리:</strong> multi-GPU 환경과 tensor 병렬, pipeline 병렬 등 다양한 방법론 소개</li>
<li><strong>양자화:</strong> 특히 최근 논의가 활발한 int4, int8 양자화를 활용한 경량화 접근이 구체적으로 설명된다.</li>
</ul>
<h4 id="ch-9-rag-추론-파이프라인-llm-twin의-rag-추론-파이프라인과-rag-기법-구현">CH 9 RAG 추론 파이프라인, LLM Twin의 RAG 추론 파이프라인과 RAG 기법, 구현</h4>
<h4 id="ch-10-추론-파이프라인-배포-모놀리식--msa-오토스케일링">CH 10 추론 파이프라인 배포, 모놀리식 &amp; MSA, 오토스케일링</h4>
<h4 id="chapter-11-devops-mlops-llmops-llm-twin-파이프라인을-클라우드에-배포와-llmops-적용기">CHAPTER 11 DevOps, MLOps, LLMOps, LLM Twin 파이프라인을 클라우드에 배포와 LLMOps 적용기</h4>
<ul>
<li><strong>DevOps:</strong> CI/CD 기반 배포 자동화</li>
<li><strong>MLOps:</strong> 실험 추적, 모델 버전 관리, 재현성 보장</li>
<li><strong>LLMOps:</strong> 프롬프트 버전 관리, 벡터 인덱스 동기화, 쿼리 추적 등 LLM 기반 시스템에 특화된 운영 전략</li>
<li><strong>&quot;운영 가능한 시스템&quot;을 만들기 위한 최소한의 규칙을 지켜야 한다</strong>. 특히 클라우드에 올릴 때 발생하는 문제들, 실험 결과를 롤백하거나 재현해야 할 때의 장애 포인트들을 미리 체크해볼 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM - Diffusion LLM vs Autoregressive LLM (근데 이제 논문을 곁들인...)]]></title>
            <link>https://velog.io/@qlgks1/LLM-Diffusion-LLM-vs-Autoregressive-LLM-%EA%B7%BC%EB%8D%B0-%EC%9D%B4%EC%A0%9C-%EB%85%BC%EB%AC%B8%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8</link>
            <guid>https://velog.io/@qlgks1/LLM-Diffusion-LLM-vs-Autoregressive-LLM-%EA%B7%BC%EB%8D%B0-%EC%9D%B4%EC%A0%9C-%EB%85%BC%EB%AC%B8%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8</guid>
            <pubDate>Sun, 15 Jun 2025 09:04:32 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: 250615 기준, 현재 Diffusion LLM 의 상태와 최대한 쉽게 매커니즘 정리, 기존(Autoregressive) 과 비교 ]</p>
<h1 id="diffusion-llm">Diffusion LLM</h1>
<blockquote>
<p>GPT 3.5 의 전율이 엊그제 같은데 벌써 2년이 넘었다니.. 올해 3월 <a href="https://aimatters.co.kr/news-report/ai-news/16652/">인셉션(Inception Labs) Mercury</a> 이 공개되면서 더 많은 대중의 관심을 끌게된 &quot;dLLM&quot;. 기존 autoregressive 방식과 어디가 어떻게 얼마나 차이가 나는 걸까? 이제 &quot;차세대 언어모델&quot; 로 주목받으며 (또는 과장이라 비난 받기도 하고) 커져가는 diffusion llm 의 거시적인 흐름을 살펴보자!</p>
</blockquote>
<h2 id="1-기존-llm의-한계와-diffusion의-등장-배경-현-상태">1. 기존 LLM의 한계와 Diffusion의 등장 배경, 현 상태</h2>
<p>기존 autoregressive LLM(<strong>ARM</strong>)들은 <strong>왼쪽에서 오른쪽으로 순차적으로 토큰을 생성</strong>하는 방식으로 작동한다. GPT 시리즈가 대표적인 예로, 각 토큰은 이전에 생성된 모든 토큰들을 조건으로 하여 다음 토큰의 확률 분포를 계산한다. (<strong><em><a href="https://velog.io/@qlgks1/LLM-Intro-to-Large-Language-Models">LLM - Intro to Large Language Models</a></em></strong> 참조)</p>
<pre><code class="language-python"># Autoregressive 생성 방식 (순차적)
P(x) = ∏ᵢ P(xᵢ | x₁, x₂, ..., xᵢ₋₁)

# 실제 생성 과정
x₁ = predict_next_token(prompt)
x₂ = predict_next_token(prompt + x₁)
x₃ = predict_next_token(prompt + x₁ + x₂)
# ... 순차적으로 계속</code></pre>
<h3 id="1-autoregressive-단점">1) autoregressive 단점?!</h3>
<p>하지만 이 방식은 태생적인 <strong>순차 병목현상</strong>을 가지고 있다. 각 토큰은 앞선 토큰들이 &#39;모두 생성된 후&#39; 에야 만들어질 수 있어서, (일반적인) <strong>병렬화가 불가능</strong>하고 <strong>긴 시퀀스 생성 시 속도가 크게 저하</strong> 된다. (&#39;추론모델&#39; 도 사실 스스로 만들어낸 토큰을 스스로 참조하면서 다시 depth 있게 접근하는 방식이다.)</p>
<p>그래서 아래와 같은 한계가 있다.</p>
<ol>
<li><strong><em>속도 제약</em></strong>: 각 토큰은 앞선 토큰들이 모두 생성된 후에야 만들어질 수 있어 병렬화가 불가능(어려움)</li>
<li><strong><em>누적 오류</em></strong>: 앞서 잘못 생성된 토큰이 뒤의 모든 토큰에 영향을 미침</li>
<li><strong><em>유연성 부족</em></strong>: 중간 부분을 수정하거나 특정 부분만 재생성하기 어려움</li>
</ol>
<pre><code class="language-python">prompt = &quot;파이썬은&quot;
x₁ = &quot;어려운&quot;  # 잘못된 생성
x₂ = &quot;프로그래밍&quot;  # x₁에 영향받아 계속 잘못된 방향
x₃ = &quot;언어다&quot;  # 전체적으로 부정확한 결과
# 중간 수정이 불가능 → 처음부터 다시 생성해야 함</code></pre>
<p>이런 한계를 극복하고, ARM 자체의 통념을 깨기 위해 처음부터 Diffusion 형태로 학습한 LLaDA 와 같은 모델이 나왔다. 아래 <a href="https://velog.io/@qlgks1/LLM-Diffusion-LLM-vs-Autoregressive-LLM-%EA%B7%BC%EB%8D%B0-%EC%9D%B4%EC%A0%9C-%EB%85%BC%EB%AC%B8%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8#2-diffusion-llm%EC%9D%98-%EA%B0%84%EB%8B%A8-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98">&quot;2. Diffusion LLM의 간단 메커니즘&quot;</a> 에서 다시 자세하게 살펴보자. 우선 당장의 <strong>&quot;Diffusion LLM 의 성능은 어떤가?&quot;</strong> 부터 살펴보자.</p>
<h3 id="2-diffusion-llm-의-요즘-성능-벤치마크">2) diffusion llm 의 요즘 성능 벤치마크</h3>
<p>최근 재미있는 포인트는 Diffusion과 autoregressive 언어 모델 간의 성능 격차가 극적으로 줄어들었다고 한다. <strong><a href="https://arxiv.org/abs/2502.09992">LLaDA 8B는 15개 벤치마크에서 LLaMA3 8B와 경쟁력 있는 성능을 달성하면서도 LLaMA3의 15T 토큰 대비 단 2.3T 훈련 토큰만 사용했다.</a></strong> </p>
<p>ARC-C 추론 작업에서 <code>LLaMA3</code> 8B의 <strong><em><code>82.4</code></em></strong>, <code>LLaDA</code> 8B의 <strong><em><code>88.5</code></em></strong> 로 우수한 성능을 보였으며, <a href="https://www.inceptionlabs.ai/introducing-mercury">Mercury Coder는 HumanEval에서 88.0%를 달성하면서 초당 1,109 토큰을 생성</a>했다. 이는 GPT-4o Mini의 59 토큰/초에 비해 압도적이다. (아래 사진)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/42b1ad58-e8a7-468d-9e87-0ec2b2f3488c/image.png" alt=""></p>
<p>PS) LLaDA의 88.5 점과 LLaMA3 82.4 얘기는 <a href="https://arxiv.org/html/2502.09992v1">Supervised Fine‑Tuning(SFT) 이후를 말한다.</a></p>
<p><a href="https://arxiv.org/abs/2310.16834">SEDD(Score Entropy Discrete Diffusion)는 기존 diffusion 모델보다 25-75% 개선된 perplexity를 제공</a>하며, <a href="https://m-arriola.com/bd3lms/">BD3-LMs는 LM1B 데이터셋에서 28.23 perplexity로 기존 방법 대비 13% 향상을 달성</a>했다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/71d8eb52-106c-4b0c-bd4d-a7ea8b443bde/image.png" alt=""></p>
<p><strong><a href="https://www.maginative.com/article/inception-labs-launches-mercury-the-first-commercial-diffusion-based-language-model/">Mercury 시리즈는 NVIDIA H100 GPU에서 초당 1000토큰 이상을 생성</a></strong> 했다고 한다. - 이게 사실이라면 GPT-4o Mini보다 약 19배 빠르면서도 비교 가능한 코딩 성능을 유지한다. (아직 100% fact 인지는 판단 안 된 듯)</p>
<p>속도 우위는 단순한 처리량을 넘어선다. Diffusion 모델은 병렬 토큰 생성, 양방향 추론, 환각을 줄이는 반복적 개선을 지원한다. 특히 autoregressive 모델이 어려워하는 역순 작업에서 뛰어나며, <a href="https://ml-gsai.github.io/LLaDA-demo/">중국어 시 완성 벤치마크에서 LLaDA가 GPT-4o를 데모에서는 앞섰다고도 한다</a>. 속도, 품질, 제어 가능성의 조합은 지연에 민감하고 높은 처리량이 필요한 애플리케이션에서 diffusion LLM을 최적의 선택으로 만든다.</p>
<p>PS) 논외지만, 최근 중국 논문의 피인용수가 폭발적으로 증가하고 있다고 한다 ㅎㅎ..</p>
<h4 id="요약">요약</h4>
<ol>
<li>LLaDA 8B: 단 2.3T 토큰으로 훈련했음에도, <strong>LLaMA3 8B (15T)</strong> 와 유사하거나 더 나은 성능(예: ARC-C에서 88.5 vs 82.4)을 보여줬다고 한다.</li>
<li>Mercury Coder 는 HumanEval 정확도 88.0%, 초당 1,109 토큰 생성을 한다고 &quot;주장&quot; 하고 있고, 이게 fact 라면 GPT-4o Mini (59 tokens/s) 대비 19배 이상 빠른 추론 속도가 된다.</li>
<li>병렬 토큰 생성, 양방향 추론, 반복적 개선 가능이 가능하고, 역순 생성 등 autoregressive 모델이 어려워하는 작업에서 유리하다. </li>
<li><strong>*<span style="color: rgb(99 148 255);">빠른 속도와 낮은 지연 시간 덕분에 고성능·실시간 처리 요구 앱에서 diffusion LLM이 유리할 수 있다.</span>*</strong></li>
</ol>
<hr>
<h2 id="2-diffusion-llm의-간단-메커니즘">2. Diffusion LLM의 간단 메커니즘</h2>
<p>사실 &quot;Diffusion&quot; 이라는 개념 자체가 이제와서 엄청 핫해진건 아니다. 15년대 부터 논문에 등장했다고 하는데, 진짜 &quot;대중적으로 유명&quot;해진건 &quot;Stable Diffusion, DALL-E 2&quot; 라고 생각된다. (근데 사실 학문적 논문은 20년이 전환점이라고 보인다.)</p>
<p>같은 맥락에서 Diffusion LLM은 이미지 생성에서 검증된 <strong>점진적 디노이징(denoising) 프로세스</strong>를 텍스트에 적용한 접근법이다. 완전히 마스크된 텍스트에서 시작하여 <strong>여러 단계에 걸쳐 동시에 여러 토큰을 정제</strong>해나가는 방식으로 작동한다.</p>
<h3 id="1-핵심-동작-원리">1) 핵심 동작 원리</h3>
<p><strong><em>일단 기존의 diffusion model</em></strong> 을 조금 알아야 한다. <code>Diffusion model</code> 은 데이터를 만들어내는 <strong>deep generative model</strong> 중 하나로, data로부터 noise를 조금씩 더해가면서 data를 완전한 noise로 만드는 <code>forward process(diffusion process)</code> 와 이와 반대로 noise로부터 조금씩 복원해가면서 data를 만들어내는 <code>reverse process</code> 를 활용한다. <strong><em>(<a href="https://lilianweng.github.io/posts/2021-07-11-diffusion-models/">What are Diffusion Models?</a>)</em></strong></p>
<p>그림에서 예시로, &quot;대략적인 윤곽(스케치) → 스케치 채우기 → 세부사항 → 마무리&quot; 비유를 많이 한다. 이를 자연어에서 비유해 보자면 아래와 같은 흐름이다. </p>
<pre><code class="language-python"># 예시: &quot;AI가 미래를 바꿀 것이다&quot; 문장 생성하기

# 1단계: 모든 곳이 빈칸인 상태로 시작
&quot;[____] [____] [____] [____] [____]&quot;

# 2단계: 가장 확실한 단어들부터 채우기
&quot;[____] [미래를] [____] [것이다] [____]&quot;

# 3단계: 남은 빈칸들 채우기  
&quot;[AI가] [미래를] [____] [것이다] [____]&quot;

# 4단계: 마지막 빈칸 완성
&quot;[AI가] [미래를] [바꿀] [것이다] [____]&quot;

# 최종: 완성된 문장
&quot;AI가 미래를 바꿀 것이다&quot;

PS) 예시를 클로드한테 만들어 달라고 했다. 이제 클로드한테 선생님라고 해야할 듯...</code></pre>
<p>이렇게 접근하면 아래와 같은 이점이 생긴다. </p>
<ol>
<li><strong><em>병렬 처리 가능</em></strong>: 기존 방식(첫 번째→두 번째→세 번째)과 달리 모든 위치를 동시에 처리 가능 하다.</li>
<li><strong><em>전체 맥락 고려</em></strong>: 문장 전체의 흐름과 의미를 처음부터 고려할 수 있다.</li>
<li><strong><em>오류 수정 기회</em></strong>: 여러 단계를 거치면서 잘못된 예측을 바로잡을 수 있다.</li>
</ol>
<pre><code class="language-python"># Diffusion 생성 방식: 모든 위치를 동시에 고려
def generate_text_diffusion():
    # 1. 완전히 빈 상태로 시작
    text = [&quot;[MASK]&quot;] * 문장길이

    # 2. 여러 단계에 걸쳐 점진적으로 완성
    for 단계 in range(총_단계수, 0, -1):
        # 현재 상태에서 각 위치의 단어 예측
        예측결과 = model.predict(text, 현재_단계=단계)

        # 가장 확실한 예측부터 빈칸 채우기
        text = 확실한_예측만_반영(text, 예측결과)

    return text</code></pre>
<h3 id="2-텍스트의-이산적-특성">2) 텍스트의 이산적 특성</h3>
<p>텍스트의 <strong><em>이산적 특성</em></strong> 때문에 연속적인 이미지 확산과는 다른 접근이 필요하다. 텍스트는 다음과 같은 개별 단위들로 구성된다.</p>
<ul>
<li>문자 단위: &#39;ㄱ&#39;, &#39;ㄴ&#39;, &#39;ㄷ&#39; 또는 &#39;a&#39;, &#39;b&#39;, &#39;c&#39;</li>
<li>단어 단위: &quot;사과&quot;, &quot;바나나&quot;, &quot;오렌지&quot;</li>
<li>토큰 단위: &quot;안녕&quot;, &quot;하세요&quot;, &quot;!&quot;</li>
</ul>
<p>각 단위는 명확히 구분되고, 그 사이에 &quot;중간 상태&quot;가 없다. 예를 들어 &quot;사과&quot;와 &quot;바나나&quot; 사이에 &quot;사바나나&quot; 같은 중간 단어는 의미가 없듯이 말이다. 이 특성 때문에 텍스트 처리에서 &quot;특별한 접근이&quot; 필요하다. 왜냐면 아래와 같인 이유때문이다!!</p>
<ol>
<li>&quot;딥러닝에서&quot; 연속적인 숫자로 변환(임베딩)해야 한다.</li>
<li>&quot;확률 모델링에서&quot; 각 토큰별로 확률을 계산해야 한다.</li>
<li>&quot;생성 모델에서&quot; 한 번에 하나씩 토큰을 선택해야 한다.</li>
</ol>
<h3 id="3-absorbing-state">3) Absorbing State</h3>
<h4 id="그래서-absorbing-state-와-같은-특별한-접근법이-등장했다">그래서 &quot;Absorbing State&quot; 와 같은 특별한 접근법이 등장했다.</h4>
<blockquote>
<p><strong><em>사실 더 정확하게는 D3PM(2021)에서 absorbing state를 포함한 이산 확산 모델이 제안되었고, 여전히 성능 문제가 있었다.</em></strong> 하지만 SEDD(2023)에서 score entropy라는 새로운 손실 함수로 이를 크게 개선했다.</p>
</blockquote>
<p>Google Research 등에서 발표한 D3PM, <strong><em><code>D3PM(Discrete Denoising Diffusion Probabilistic Models)</code></em></strong> 논문에서 Absorbing State는 <strong><em><code>&quot;모든 토큰이 최종적으로 [MASK]라는 특별한 상태로 흡수되는 과정&quot;</code></em></strong> 으로 정의한다. <strong><em>(<a href="https://arxiv.org/abs/2107.03006">Structured Denoising Diffusion Models in Discrete State-Spaces</a>)</em></strong> 이 과정이 아래와 같다. </p>
<pre><code class="language-python"># 원본 문장
&quot;파이썬은 배우기 쉬운 언어다&quot;

# Forward Process: 점진적으로 단어들이 [MASK]로 변함
# t=1: &quot;파이썬은 배우기 쉬운 [MASK]&quot;  
# t=2: &quot;파이썬은 [MASK] 쉬운 [MASK]&quot;
# t=3: &quot;[MASK] [MASK] [MASK] [MASK]&quot;  ← 모든 단어가 흡수됨

# Reverse Process: 거꾸로 [MASK]에서 원래 단어 복원
# t=3→2: &quot;[MASK] [MASK] [MASK] 언어다&quot;
# t=2→1: &quot;파이썬은 [MASK] 쉬운 언어다&quot; 
# t=1→0: &quot;파이썬은 배우기 쉬운 언어다&quot;  ← 완전 복원</code></pre>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ea0437ab-0d7f-4297-9a53-d2a3f2f71f90/image.gif" alt=""></p>
<pre><code class="language-python"># https://pmc.ncbi.nlm.nih.gov/articles/PMC10909201/
# 공식 논문의 수학적 표현을 쉽게 번역하면!

# Forward Process (원본 → 마스크)
# αt = 시간 t에서의 원본 토큰 유지 확률
q(z_t | x) = αt * 원본토큰 + (1-αt) * [MASK]

# 예시: α₃ = 0.7이면
# 70% 확률로 원본 유지, 30% 확률로 [MASK]로 변환

# Reverse Process (마스크 → 원본)
# 신경망이 마스크된 토큰의 원본을 예측
p_θ(z_{t-1} | z_t) = 모델이_예측한_토큰_분포(z_t, t)</code></pre>
<p>더 자세한 정보는 <a href="https://s-sahoo.com/mdlm/">Simple and Effective Masked Diffusion Language Models</a> 를 추천한다. </p>
<p>하지만, <strong><em>D3PM</em></strong> 에서는 <code>Mean Prediction</code> 방식의 한계, <code>Concrete Score Matching</code> 의 문제 등이 있었고, 이후 <code>Stanford</code> 와 <code>Pika Labs</code> 가 공동 개발한 <strong><em>Score Entropy Discrete Diffusion (SEDD)</em></strong> 은 이산 공간에서의 score matching 이론적 기반을 마련했다. 특히 음수값 문제, 확장성 문제, 연속시간 근사 문제를 해결하는 새로운 score entropy 손실 함수를 도입하여, 이산 데이터(특히 자연어)에 대한 Diffusion 모델의 이론적 토대를 확립했다.</p>
<p>이후 현재 <strong><em><code>LLaDA</code></em></strong> 까지 발전되어 왔다. </p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1a60ca77-f43c-4cc3-a140-cef4e3a1449e/image.png" alt=""></p>
<p>LLaDA의 학습 및 추론 방식을 도식화 하면 위와 같다. 학습 과정에서는 입력에 점진적으로 잡음(Noise)을 추가하여 마스킹 토큰을 생성고 입력 전체가 마스킹 되면, 다시 원본 문장을 점진적으로 복원한다. 추론 과정에서는 프롬프트가 아닌 응답 부분만 마스킹 처리한 후, 이 부분을 예측하는 방식을 통해 응답을 생성한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fcac0eae-3883-413c-8d27-55625a35f393/image.png" alt=""></p>
<p><strong><em>색이 어두울수록 나중에 예측된 토큰이며, 밝을수록 일찍 예측된 토큰이다.</em></strong></p>
<h4 id="google-deepmind의-실제-구현">Google DeepMind의 실제 구현</h4>
<p>Google DeepMind의 Gemini Diffusion 모델은 이 매커니즘을 바탕으로 구현됐다. &quot;전통적인 autoregressive 모델이 한 번에 하나씩 토큰을 생성하는 반면, diffusion 모델은 노이즈를 단계적으로 정제하여 출력을 생성한다&quot;고 설명한다. - <a href="https://deepmind.google/models/gemini-diffusion/">https://deepmind.google/models/gemini-diffusion/</a></p>
<h3 id="4-diffusion-이-만능인가여">4) Diffusion 이 만능인가여?!</h3>
<ul>
<li>결론 부터 보자면 아직은 시기 상조라고 보여진다. 하지만 핵심은 &quot;특정 섹터에서는 기존 방식의 가격대비 성능을 뛰어넘고&quot; 있다.</li>
</ul>
<h4 id="autoregressive의-여전한-강점">Autoregressive의 여전한 강점</h4>
<ol>
<li><p>생태계 성숙도</p>
<ul>
<li>HuggingFace Transformers, vLLM, TensorRT-LLM 등 완성된 최적화 도구들
수천 개의 사전훈련된 모델과 광범위한 커뮤니티 지원</li>
<li>프로덕션 검증된 배포 인프라 (Kubernetes operators, monitoring tools)</li>
</ul>
</li>
<li><p>확장성과 품질</p>
<ul>
<li>GPT-4, Claude, Gemini 등 100B+ 매개변수의 (엄청 라지스케일의) 대규모 모델들</li>
<li><strong><em>복잡한 추론, 긴 맥락 이해에서 여전히 우수한 성능</em></strong></li>
<li>Chain-of-thought, few-shot learning 등에서 자연스러운 강점.</li>
</ul>
</li>
<li><p>예측 가능성</p>
<ul>
<li>토큰별 순차 생성으로 디버깅과 해석이 용이</li>
<li>확률 분포가 명확해서 불확실성 정량화가 쉬움 <strong><em>(물론 이게 어떻게 black box 확률을 계산했냐를 알 수 있다는 뜻은 아님)</em></strong></li>
<li>길이 제어가 자연스러움 (EOS 토큰까지 생성)</li>
</ul>
</li>
<li><p>특정 작업에서의 우위</p>
<ul>
<li>대화형 AI에서 자연스러운 turn-taking</li>
<li>코드 생성에서 논리적 흐름 유지</li>
<li>창작 글쓰기에서 일관된 스타일 유지</li>
</ul>
</li>
</ol>
<h4 id="diffusion의-현실적-한계">Diffusion의 현실적 한계</h4>
<ol>
<li><p>훈련 복잡성</p>
<ul>
<li>노이즈 스케줄 튜닝, 마스킹 전략 등 하이퍼파라미터 민감도 높음</li>
<li>Autoregressive 대비 수렴 안정성 낮음</li>
<li>디버깅이 어려운 확률적 과정</li>
</ul>
</li>
<li><p>제한된 규모</p>
<ul>
<li>현재 대부분 8B 이하, 100B+ 규모 모델 부재</li>
<li>스케일링 법칙이 아직 완전히 검증되지 않음</li>
</ul>
</li>
<li><p>특정 작업에서의 약점</p>
<ul>
<li>매우 긴 시퀀스 생성에서는 여전히 느림</li>
<li>순차적 추론이 중요한 수학 문제 해결에서 한계</li>
<li>실시간 대화에서 응답 품질 vs 속도 트레이드오프</li>
</ul>
</li>
</ol>
<h4 id="요약-1">요약</h4>
<ol>
<li>고속 대량 처리: Diffusion 이 유리할 수 있음</li>
<li>복잡한 추론: 아직 Autoregressive 여전히 강세로 보임</li>
<li>안정적 프로덕션: (시장 성숙도를 포함하면) Autoregressive가 더 안전</li>
<li>비용 효율성: Diffusion이 유리 (동일 성능 대비를 의미)</li>
</ol>
<hr>
<h2 id="3-모델-중심-요약-및-정리">3. 모델 중심 요약 및 정리</h2>
<blockquote>
<p>주관을 100% 담은 Diffusion LLM 을 위한 논문/모델 주의 시계열 정리</p>
</blockquote>
<ol>
<li><strong><code>[2021.07]</code></strong> Google Research + MIT = D3PM: Discrete Denoising Diffusion Probabilistic Models 발표</li>
<li><strong><code>[2023.10]</code></strong> SEDD (Score Entropy Discrete Diffusion)</li>
<li><strong><code>[2023.12]</code></strong> Apple, PLANNER, Latent Language Diffusion Model 발표 (NeurIPS 2023)</li>
<li><strong><code>[2024.06]</code></strong> Cornell Tech의 Volodymyr Kuleshov 그룹, <code>MDLM</code> - Masked discrete Diffusion Language Model</li>
<li><strong><code>[2025.02]</code></strong> LLaDA 8B 공개, <strong>*<span style="color: rgb(99 148 255);">단 2.3T 토큰으로 학습했음에도 LLaMA3 8B (15T) 에 밴치마크 비교 우위</span>*</strong></li>
<li><strong><code>[2025.02]</code></strong> Inception Labs: Mercury Coder 시리즈</li>
<li><strong><code>[2025.03]</code></strong> BD3-LMs: Block Discrete Diffusion 기반의 language model</li>
<li><strong><code>[2025.05]</code></strong> Google DeepMind: Gemini Diffusion 모델 구조 공개 (Google I/O)</li>
</ol>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://lilianweng.github.io/posts/2021-07-11-diffusion-models/">Lilian Weng의 “What are Diffusion Models?”</a></li>
<li><a href="https://arxiv.org/abs/2502.09992">LLaDA: Language Models as Diffusion Agents. 2024. arXiv</a></li>
<li><strong><em><a href="https://ml-gsai.github.io/LLaDA-demo/">LLaDA 데모 페이지</a></em></strong></li>
<li><a href="https://kimjy99.github.io/%EB%85%BC%EB%AC%B8%EB%A6%AC%EB%B7%B0/llada/">[논문리뷰] Large Language Diffusion Models (by 김준영)</a></li>
<li><a href="https://aimatters.co.kr/news-report/ai-news/16652/">인셉션랩 Mercury 공개 뉴스 (AI Matters)</a></li>
<li><a href="https://www.inceptionlabs.ai/introducing-mercury">Mercury Coder 소개 페이지 (Inception Labs)</a></li>
<li><a href="https://www.maginative.com/article/inception-labs-launches-mercury-the-first-commercial-diffusion-based-language-model/">Mercury 기반 상업용 diffusion 모델 소개 (Maginative)</a></li>
<li><a href="https://arxiv.org/html/2502.09992v1">Supervised Fine-Tuning 이후 성능 비교 (arXiv HTML 버전)</a></li>
<li><a href="https://m-arriola.com/bd3lms/">BD3-LMs 공식 블로그 (by Manuel Arriola)</a></li>
<li><a href="https://arxiv.org/abs/2310.16834">SEDD 논문: Score Entropy Discrete Diffusion</a></li>
<li><a href="https://arxiv.org/abs/2107.03006">Structured Denoising Diffusion Models in Discrete State-Spaces (D3PM)</a></li>
<li><a href="https://s-sahoo.com/mdlm/">Simple and Effective Masked Diffusion Language Models (MDLM)</a></li>
<li><a href="https://deepmind.google/models/gemini-diffusion/">Google DeepMind Gemini Diffusion 모델 소개</a></li>
<li><a href="https://machinelearning.apple.com/research/latent-language-diffusion-model">Apple Machine Learning Research: PLANNER 논문</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 - 최범균]]></title>
            <link>https://velog.io/@qlgks1/%EC%A3%BC%EB%8B%88%EC%96%B4-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EC%8B%A4%EB%AC%B4-%EC%A7%80%EC%8B%9D-%EC%B5%9C%EB%B2%94%EA%B7%A0</link>
            <guid>https://velog.io/@qlgks1/%EC%A3%BC%EB%8B%88%EC%96%B4-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EC%8B%A4%EB%AC%B4-%EC%A7%80%EC%8B%9D-%EC%B5%9C%EB%B2%94%EA%B7%A0</guid>
            <pubDate>Thu, 29 May 2025 16:03:22 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="주니어-백엔드-개발자가-반드시-알아야-할-실무-지식">주니어 백엔드 개발자가 반드시 알아야 할 실무 지식</h1>
<blockquote>
<p>최범균: 나이를 먹어서도 백발에 개발을 하고 싶은 코딩을 좋아하는 개발자다. 좋은 책을 쓰는 것을 꿈꾸고 있고, 꾸준히 블로그와 브런치에 글을 쓰고 있다. ‘스프링4 프로그래밍 입문’, ‘JSP 2.3 웹 프로그래밍’, ‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴’ 등의 책을 집필했다. <a href="https://javacan.tistory.com/">https://javacan.tistory.com/</a> 블로그와 <a href="https://www.youtube.com/@madvirus">https://www.youtube.com/@madvirus</a> 유투브 열심히 지금까지도 지식공유를 해주신다!</p>
</blockquote>
<ul>
<li><a href="https://www.hanbit.co.kr/channel/view.html?cmscode=CMS7603127967">코딩을 할 수 있게 된 것일 뿐: 실무에서 마주한 주니어 백엔드 개발자의 첫 위기</a></li>
</ul>
<h2 id="리뷰">리뷰</h2>
<p>책 소개에 나오는 사례가 완벽하게 학부생때 경험한 시나리오였다. 커넥션풀, DBMS 에 대한 낮은 이해도, 사실 &quot;연결&quot;이 왜 메모리를 먹는지도 잘 몰았던 시절, 연결에 왜 timeout 을 줘야하는지도 몰랐던 그 때가 바로 떠올랐다.</p>
<p>개인적으로 무엇보다 개발은 실수를 하며 심연을 해맬때 결국 가장 큰 성과를 얻었다. <del>근데 지금도 심연에 있는 기분이 들때가 많다..</del> 결국 오답노트가 공부에 가장 큰 도움이 되었다. 이 책은 <strong>*<span style="color: rgb(99 148 255);">그 좋은 오답노트 수첩을 훔쳐보는 기분이다.</span>*</strong> 시니어 분들에게는 향수와 가이드할 목차를, 주니어 분들에게는 겪어 보지 못한 실수도 같이 해보며 따라갈 수 있는 실마리를 줄 것 같다. </p>
<p>인스턴트가 넘쳐나는 LLM 시대에 (어쩌면 벌써) Classic 을 읽는 기분이다. </p>
<pre><code class="language-text">- 성능의 근본 개념과 접근법을 다룬 2장,
- 실질적이고 깊이 있는 DB 이야기로 가득 찬 3장,
- 프레임워크를 떠나 API 서버와 인프라 전반을 다루는 4장 (외부 API 연동, 동시 요청 제한, 서킷 브레이커, HTTP 커넥션 풀, 연동 서비스 이중화 등),
- 이어서 코드 수준에서 동기/비동기, 스레드 처리, 메시징, 트랜잭션 아웃박스, CDC까지 짚어주는 5장,
- 레이스 컨디션, 락과 세마포어, DB 동시성 처리 등을 다룬 6장,
- IO 병목과 네트워크 관점에서의 자원 효율화 방식을 설명한 7장,
- 실무에서 반드시 필요한 보안 기초(시큐어 코딩 포함)를 정리한 8장,
- 리눅스 기반의 OS 및 서버에 대한 기본 지식(계정 권한, 디스크 용량, 크론 등)을 다룬 9장,
- 네트워크 기본 개념을 복습하는 10장,
- 그리고 마지막으로 자주 사용되는 서버 구조 및 설계 패턴(MVC, 계층형 아키텍처, DDD, 마이크로서비스, CQRS 등)을 다룬 11장까지.</code></pre>
<p>각 장은 위와 같은 구성으로, 2장을 시작으로 실무에서 꼭 짚고 넘어가야 할 체크포인트들을 순차적으로 다루며, 각 주제에 필요한 핵심 사전 지식을 자연스럽게 덧붙이는 방식으로 전개된다. 요즘 주니어 개발자를 위한 가이드는 확실히 잘 나오는 듯하다... <del>그런데 왜 중니어, 시니어를 위한 책은 없는 걸까 ㅠㅠ</del> </p>
<p>물론 이 책이 모든 부분을 CS/OS 레벨이나 네트워크의 HW나 DB의 스토리지 레벨까지 깊게 파고들지는 않지만, 그 대신 얕고 넓게, 무엇보다 “실제로 겪은 실무 사례”와 함께 폭넓게 정리되어 있다는 점이 강점이다. 사실 이런 책은 실마리만 잘 잡아주면 된다. 이후의 depth는 각자의 몫이니까.</p>
<p>더욱이 조금 더 주관적인 의견을 덧붙이자면, <strong>DBMS(특히 인덱싱과 쿼리 최적화)</strong>, <strong>네트워크</strong>, <strong>리눅스 기반 OS 활용</strong>만 제대로 마스터해도 대부분의 서비스 이슈는 어떻게든 해결 가능하다고 믿는다. 하나 덧붙이자면 서비스로직 처리할때 비동기, 동시성, lock 개념 정도..?</p>
<p>마무리하자면, 이 책은 마치 옆자리 팀장님이 “나는 그럴 땐 이렇게 했었어~” 하며 빠르게 구두로 알려주는 느낌이다. 그리고 그 말 한마디에 문득 실마리가 풀리던, 그 익숙한 순간처럼 읽힌다. </p>
<p><del>근데 아주 솔직히 내가 진짜 0-1년차 였다면 이 책이 한 숨에 읽히진 않았을 것 같다. 그리고 마냥 웃으면서 볼 수도 없었을 것 같고 ㅎㅎ</del></p>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<h3 id="2장-느려진-서비스-어디부터-봐야-할까">2장 느려진 서비스, 어디부터 봐야 할까</h3>
<h4 id="1-처리량과-응답-시간">1) 처리량과 응답 시간</h4>
<p>API 호출이라는 단순한 흐름(application → server → DB)을 바탕으로 성능을 바라보는 시각은 명쾌하다. 응답 시간이라는 것은 단순히 서버 코드만의 문제가 아니며, <strong><em><code>TTFB(Time to First Byte)</code></em></strong>, <strong><em><code>TTLB(Time to Last Byte)</code></em></strong> 처럼 전체 체인을 기준으로 측정되어야 한다. 특히 파일 다운로드의 경우, 두 시간 차이가 클 수 있다. <a href="https://web.dev/articles/ttfb?hl=ko">아래 글 한번 읽어보길 추천</a></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/e4f84c40-af30-4c6c-955c-3f9d59152e8d/image.svg" alt=""></p>
<p>구글이 &quot;100ms의 지연이 검색 횟수를 0.2% 감소, 400ms 지연은 0.4% 감소시킨다&quot;는 발표를 한 적 있다. (응답 시간)퍼포먼스는 사용자 경험(UX)의 핵심이다.</p>
<p>실제 서비스에서의 서버 처리 시간은 단순히 비즈니스 로직을 수행하는 시간이 아니라, DB 커넥션, 외부 API 연동, 응답 데이터 생성까지를 포함한다. 결국 이 모든 항목의 병목을 파악하려면 구간별 실행 시간을 측정하고 분리 분석하는 능력이 필요하다.</p>
<p>특히 TPS(Transaction Per Second)를 기준으로 병목을 판단하는 관점은 실무에서 유용하다. 단순히 요청 수를 보는 것이 아니라, <strong>&quot;얼마나 처리 완료됐는가?&quot;</strong> 를 보는 기준은 진짜 서비스를 운영하는 입장에서 중요하다.</p>
<p>시간당 시스템 처리하는 작업량, TPS(transaction per second) or RPS(request per second) - 여기서는 주로 TPS (&quot;처리 완료&quot; 가 중요)</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/8149f129-7825-4676-ab67-80d7da86f953/image.png" alt=""></p>
<p>동시에 처리할 수 있는 요청을 증가 시키던지, 한 개 요청을 처리하는 시간을 단축 시키던지의 방향으로 TPS를 높여야 하고, 사실 측정을 위해서는 모니터링이 중요하다. </p>
<h4 id="2-서버-성능-개선-기초">2) 서버 성능 개선 기초</h4>
<p>서버의 성능 저하 증상은 다음과 같이 나타난다. </p>
<ul>
<li>전체 응답 시간이 10초 이상으로 증가</li>
<li>연결 시간 초과</li>
<li>서버 재시작 후 일시 회복되나 반복 발생</li>
</ul>
<p>이 경우 단순히 서버를 재시작하는 것으로는 근본적인 해결이 안 된다. <strong>정확한 병목 지점을 찾아내야 하고</strong>, 이를 위해선 <strong>구간별 실행 시간을 측정</strong> 할 수 있어야 한다.</p>
<ul>
<li><strong>수직 확장(Scale-up)</strong>: CPU, RAM 등을 늘려 한 서버의 스펙을 강화  </li>
<li><strong>수평 확장(Scale-out)</strong>: 서버 대수를 늘리고, 로드밸런서를 통해 분산 처리</li>
</ul>
<p>긴급한 상황에서는 수직 확장이 효과적일 수 있지만, 지속 가능한 확장은 수평 확장이며, 특히 다수의 요청을 안정적으로 처리하기 위해 필수적이다.</p>
<h4 id="3-db-커넥션-풀">3) DB 커넥션 풀</h4>
<p>DBMS도 결국 서버다. 요청이 들어올 때마다 DB에 새로 접속하면 비효율적이다. 그래서 <strong>DB 커넥션 풀</strong>을 이용해 일정 수의 커넥션을 유지하고 재활용한다. 커넥션 풀을 구성할 때 고려해야 할 주요 요소는 다음과 같다.</p>
<ul>
<li>최대/최소 커넥션 수</li>
<li>커넥션 유지 시간 (타임아웃)</li>
<li>커넥션 대기 시간</li>
</ul>
<p>(관련해서 <a href="https://velog.io/@qlgks1/Django-5.1-%EC%97%90%EB%8A%94-connection-pool-%EC%9D%B4-%EC%9E%88%EC%96%B4%EC%9A%94">Django 5.1의 커넥션 풀 적용기</a>를 참고하면 실무 적용 힌트를 얻을 수 있다.) 
예시로, 커넥션 풀 크기: 5, 쿼리 처리 시간: 0.1초 → 초당 최대 50쿼리 처리 가능하다.</p>
<p>대기 시간이 3초를 초과한다면, 요청을 거절하거나 <strong>&quot;잠시 후 다시 시도&quot;</strong> 메시지를 보내는 것이 더 나은 사용자 경험일 수 있다.</p>
<h4 id="4-캐시-전략">4) 캐시 전략</h4>
<p>DB 조회 자체를 줄이기 위해 <strong>캐시를 도입</strong>하는 것도 병목 개선의 중요한 방법이다. 책에서 캐시의 크게 두 유형을 소개한다. 있다</p>
<ul>
<li><strong>로컬 캐시</strong> (프로세스 내부 메모리 기반, 빠르지만 스케일링 어려움)</li>
<li><strong>리모트 캐시</strong> (redis 등 외부 서버, 느리지만 공유 가능)</li>
</ul>
<p>각각 장단점이 정반대이므로 상황에 맞게 선택해야 한다. 예를 들어 <strong>트래픽이 순간적으로 튀는 경우</strong>에는, 사전에 데이터를 캐시에 미리 올려두는 <strong>&quot;워밍(warming)&quot;</strong> 전략이 유효하다.</p>
<p>단, <strong>캐시 무효화(invalidation)</strong> 가 매우 중요하다. 원본 데이터가 변경되었을 때 캐시도 함께 갱신되거나 삭제되지 않으면 오히려 더 큰 문제가 된다.</p>
<p>대용량 API 응답 시에는 <strong>GC(Garbage Collection)</strong> 및 <strong>메모리 사용량</strong>도 주의해야 한다. Java에서는 스트림 방식, Go나 Rust로 전환하는 등 GC 부담을 줄이기 위한 다양한 접근이 있다. (디스코드 사례)</p>
<h4 id="5-응답-데이터-압축">5) 응답 데이터 압축</h4>
<p>응답 시간의 상당 부분은 <strong>데이터 전송 시간</strong>이 차지한다. 네트워크 속도는 제어하기 어렵지만, <strong>전송할 데이터의 크기</strong>는 줄일 수 있다.</p>
<ul>
<li>HTML, CSS, JS, JSON과 같은 정적 텍스트는 <strong>gzip 압축</strong> 을 통해 최대 70%까지 줄일 수 있다.</li>
<li>HTTP의 <code>Accept-Encoding</code> 헤더를 통해 브라우저와 서버 간 압축 여부 협상이 가능하다.</li>
</ul>
<p>이는 <strong>사용자 응답 속도 개선</strong>뿐 아니라 <strong>클라우드 전송 비용 절감</strong>에도 매우 효과적이다. 정적 파일은 가능하면 CDN을 활용해 가까운 곳에서 받아오게 하고, <strong>브라우저 캐시</strong>를 최대한 활용해야 한다. 이 또한 응답 시간과 비용 모두를 줄이는 데 도움이 된다. </p>
<h4 id="6-대기-처리">6) 대기 처리</h4>
<p>일부 서비스는 특정 시간에 엄청난 트래픽이 몰린다. (ex. 콘서트 티켓 예매, 이벤트 시작 직후 등) 이때 단순한 스케일링(up or out)으로는 대응이 어렵고, 오히려 단순 비용만 증가할 수 있다. 따라서 &quot;현재 수용 가능한 요청만 받고 나머지는 대기시키는&quot; 방식이 실용적이다.  </p>
<p>이런 방식은 마치 <strong>은행의 창구 번호표 시스템</strong>과 같다. 대표적인 방식은 <strong>유량 제어(rate limiting)</strong> 가 있고, 이는 보통 미들웨어나 클라이언트 사이드에서 구현한다.</p>
<h3 id="3장-성능을-좌우하는-db-설계와-쿼리">3장 성능을 좌우하는 DB 설계와 쿼리</h3>
<p>이 장은 신입이라면! 쿼리 실행 계획을 직접 비교해보며 따라가 보는 것을 추천한다! 예시도 실무에 맞게 잘 준비되어 있어 Follow-up 하며 익히기에 적절하다. <del>PS) 개인적으로 너무 귀에 딱지가 앉은 내용들이라 많이 함축했다.</del></p>
<p>데이터 양이 조금만 많아져도 <strong>Full Table Scan</strong> 은 쿼리 성능에 큰 영향을 미친다. 이는 DB가 조건 없이 테이블의 모든 데이터를 순차적으로 읽는 방식인데, 단일 인덱스 또는 <strong>복합 인덱스</strong>를 적극적으로 활용해 피해야 한다.</p>
<ul>
<li><strong>단일 인덱스</strong>: 하나의 컬럼만을 기준으로 한 인덱스</li>
<li><strong>복합 인덱스</strong>: 두 개 이상의 컬럼을 묶어 조건절에 활용</li>
<li><strong>선택도가 높은 컬럼</strong>: 중복 값이 적은 컬럼일수록 인덱스 효과가 크다.</li>
<li><strong>커버링 인덱스 (Covering Index)</strong>: 실행하는 쿼리에 필요한 모든 컬럼을 포함한 인덱스.<ul>
<li>예: <code>SELECT name, age FROM users WHERE email=&#39;abc@xyz.com&#39;</code>의 경우 <code>(email, name, age)</code>로 인덱스를 구성하면 쿼리 수행 시 테이블을 접근하지 않아도 된다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>다만, 인덱스는 추가/변경/삭제 연산 시마다 <strong>쓰기 성능에 오버헤드</strong>가 발생한다. 항상 트레이드오프를 고려해야 한다.</p>
</blockquote>
<p>자주 호출되는 <strong>Aggregation 쿼리</strong>(합계, 카운트 등)는 <strong>실시간 계산</strong>보다 <strong>사전 집계</strong>해서 저장하는 방식이 효과적이다.</p>
<ul>
<li>예: 사용자의 총 좋아요 수, 누적 조회수 등을 별도 필드에 미리 저장</li>
<li>단, 집계 값이 변경되는 트리거가 있는 경우, 동기화 로직이 필요하다.</li>
</ul>
<p>서비스가 장기 운영되면 <strong>데이터 양이 계속 누적</strong>된다. 이 중 자주 조회되지 않는 오래된 데이터는 별도 테이블로 분리하거나, 이관/보관 처리하는 것이 효율적이다.</p>
<h4 id="1-db-인프라-확장---primary--replica-구조">1) DB 인프라 확장 - Primary / Replica 구조</h4>
<ul>
<li><strong>읽기와 쓰기 분리</strong>: <code>Primary</code> 는 쓰기 전용, <code>Replica</code> 는 읽기 전용으로 나눠 부하를 분산</li>
<li>단, 아래와 같은 주의점이 존재한다:</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/dae94f12-e14a-4a52-a229-8ff22871211c/image.png" alt="Primary / Replica 구조"></p>
<ul>
<li>Replica는 Primary의 변경 사항을 <strong>비동기적으로 복제</strong>하기 때문에, <strong>지연</strong>이 발생한다.</li>
<li>따라서 인증/인가, 사용자 세션 등 <strong>실시간성이 중요한 SELECT</strong>는 반드시 Primary에서 수행해야 한다.</li>
</ul>
<p>쿼리 타임아웃 &amp; 배치 작업 시 청킹(Chunking)</p>
<ul>
<li>장시간 실행되는 쿼리는 <strong>타임아웃</strong>을 유발할 수 있으므로, 대용량 데이터 처리 시에는 <strong>청크 단위로 나눠서 처리</strong>한다.</li>
<li>특히 배치 작업에서는 병렬 처리 또는 페이지 단위로 나눠서 로직을 구성하는 것이 필수적이다.</li>
</ul>
<p>DBMS는 동시에 허용할 수 있는 최대 연결 수가 정해져 있다.</p>
<ul>
<li>연결 수 초과는 전체 서비스에 장애를 유발할 수 있으므로, 커넥션 풀 또는 연결 수 제한 설정을 잘 조정해야 한다.</li>
</ul>
<p>여러 데이터 수정이 <strong>정합성</strong>에 민감한 작업이라면 반드시 하나의 트랜잭션으로 묶어야 한다.</p>
<ul>
<li><code>BEGIN → 작업들 → COMMIT</code> or <code>ROLLBACK</code></li>
<li>트랜잭션을 적절히 활용하면 장애 복구 시에도 <strong>데이터 무결성</strong>을 보장할 수 있다.</li>
</ul>
<h4 id="2-서비스-중인-db-테이블-수정-시-특히-주의할-점">2) 서비스 중인 DB 테이블 수정 시 특히 주의할 점</h4>
<p><code>MySQL</code> 은 테이블 구조를 변경할 때, 실제로는 <strong>새로운 테이블을 생성한 뒤</strong>, 기존 데이터를 복사하고, 이후 기존 테이블을 <strong>새 테이블로 교체</strong>하는 방식으로 작동한다.</p>
<ul>
<li>이 과정에서 <code>UPDATE</code>, <code>INSERT</code>, <code>DELETE</code> 등의 DML이 일시적으로 <strong>허용되지 않을 수 있다</strong>.</li>
<li>운영 서비스에서 테이블 변경이 병목이나 서비스 중단으로 이어질 위험이 크다.</li>
</ul>
<p><code>PostgreSQL</code> 은 다행히 대부분의 구조 변경(<code>ADD COLUMN</code>, <code>ALTER TYPE</code>, <code>RENAME</code> 등)이 내부 메타데이터 변경으로 처리되어, <strong>Non-blocking(비차단)</strong> 방식으로 작동하는 경우가 많다. (PS. 이 책의 DBMS 기준은 대부분 <code>MySQL</code> 을 말한다.)</p>
<ul>
<li>그러나 <strong>컬럼 타입 변경</strong> 등 일부 연산은 여전히 테이블 전체를 리라이트(재작성)할 수 있으므로 사전 확인 필요.</li>
<li>운영 중인 DB 테이블에 변경을 가할 경우, <strong>DBMS마다 동작 방식과 영향 범위가 다르므로 반드시 사전 검증</strong>이 필요하다. 무중단 배포를 위해서는 <strong>마이그레이션 절차의 자동화</strong>와 <strong>사전 롤백 시나리오</strong>도 함께 준비하는 것이 바람직하다.</li>
</ul>
<h3 id="4장-외부-연동이-문제일-때-살펴봐야-할-것들">4장 외부 연동이 문제일 때 살펴봐야 할 것들</h3>
<p>외부 시스템이 우리보다 성능이 떨어지거나 트래픽을 감당하지 못할 경우가 많다. 이때 우리는 &#39;서버&#39;가 아니라 &#39;클라이언트&#39;가 되는 셈이다. 이 입장에서 체크 할 것을 알려주는 장이다.</p>
<h4 id="1-외부-api-호출시-타임아웃-설정은-필수">1) 외부 API 호출시 타임아웃 설정은 필수</h4>
<p>타임아웃이 설정되어 있지 않으면, 외부 API가 응답을 주지 않아도 무한정 기다리게 된다. 그 결과  </p>
<ul>
<li>전체 시스템 처리량이 급격히 하락하고  </li>
<li>커넥션 풀 고갈 → 요청 처리 지연  </li>
<li>최악의 경우 서비스 다운으로 이어진다  </li>
</ul>
<p>따라서 반드시 타임아웃을 명확히 설정해야 하며, 실무에서는 기본적으로 짧게(수 초 내외) 잡고, 예외적인 경우만 늘려야 한다.</p>
<h4 id="2-재시도retry-전략">2) 재시도(Retry) 전략</h4>
<p>재시도는 단순히 &#39;다시 해보자&#39;가 아니라 조건과 설계가 중요한 전략이다.
책에서는 재시도 가능한 조건은 다음 세 가지를 제시한다.</p>
<ol>
<li>단순 조회(읽기) 기능일 때  </li>
<li>연결 타임아웃이 발생했을 때  </li>
<li>멱등성(idempotent)을 보장하는 변경 기능일 때  </li>
</ol>
<p>읽기 타임아웃(read timeout)은 실제로 API 요청이 두 번 발생하게 되므로, 외부 비용(예: 과금)이 발생할 수 있으면 신중하게 판단해야 한다.</p>
<p>재시도는 ‘몇 번’, ‘얼마 간격으로’가 핵심이다. 무한 재시도는 곧 Retry Storm이라는 안티패턴으로 이어지며, 외부 시스템을 더 위험하게 만들 수 있다.</p>
<p>외부 API 호출에 제한이 있거나 트래픽 보호가 필요할 경우</p>
<ul>
<li>초과 요청에 대해 503(Service Unavailable)을 즉시 응답  </li>
<li>벌크헤드(Bulkhead) 패턴을 적용하여, 문제가 생긴 외부 API와 나머지 시스템이 영향을 분리하여 받도록 설계  </li>
</ul>
<h4 id="3-서킷-브레이커circuit-breaker">3) 서킷 브레이커(Circuit Breaker)</h4>
<p>전기 누전차단기처럼, 외부 시스템이 일정 횟수 이상 실패하면 즉시 실패(Fail Fast)로 전환해 전체 시스템을 보호하는 방식이다.</p>
<ul>
<li>초기 상태: 닫힘(Closed) → 정상 호출  </li>
<li>일정 실패율 이상: 열림(Open) → 호출 금지  </li>
<li>일정 시간 후: 반열림(Half-Open) → 일부 호출로 상태 확인  </li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/b05308d7-4bf9-452b-9629-3563f0318cf0/image.png" alt=""></p>
<h4 id="4-외부-api-응답-지연과-dbms-커넥션-고갈">4) 외부 API 응답 지연과 DBMS 커넥션 고갈</h4>
<p>외부 API의 응답이 늦어질 경우, 그 응답을 기다리는 동안 DB 커넥션을 점유하게 되면  </p>
<ol>
<li>DBMS 커넥션 풀이 고갈되고  </li>
<li>후속 요청들은 대기 → 응답 시간 증가  </li>
<li>결국 타임아웃 및 서비스 에러로 이어짐  </li>
</ol>
<p>커넥션은 최대한 빨리 반납하는 게 핵심이다. 외부 연동과 DB 작업은 명확히 분리하고, 가능하면 응답 대기 전에 커넥션부터 반납해야 한다.</p>
<h4 id="5-http-커넥션-풀과-웹서버-고려">5) HTTP 커넥션 풀과 웹서버 고려</h4>
<p>웹서버 레벨에서의 커넥션 풀 관리도 중요하다. 예를 들어, keep-alive 설정이 적절치 않으면 서버 리소스를 불필요하게 점유하게 된다. 클라이언트와의 커넥션도 ‘적절한 수명과 갯수 제한’이 필수다.</p>
<h3 id="5장-비동기-연동-언제-어떻게-써야-할까">5장 비동기 연동, 언제 어떻게 써야 할까</h3>
<p><strong>*<span style="color: rgb(99 148 255);">개인적으로 5장부터 7장은 처음 개발을 배우던 나에게 항상 &quot;벽&quot; 처럼 느껴졌던 부분이다.</span>*</strong> 동기와 비동기, &quot;왜 비동기 처리를 하지?&quot; 부터 비동기 처리하면서 고민해야 할 동시성 이슈, 또한 동시 실행에 대한 제어와 제한 이 모든게 따라온다. </p>
<p>참고로 대부분의 예시는 <code>java</code> 이다. <code>python</code> 에서도 사실 코루틴, 경량 스레드 (또는 직접 thread를 만드는) 측면에서 아주 동일하게 접근 가능하며, 더욱이 <code>node</code> 는 Asynchronous (이벤트 루프) 자체에 집중하고 따라가면 될 것 같다. </p>
<p>5장은 아래 사진 한 장이 시작이자 끝이다 ㅎ</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/2bd0f843-fb3d-40a2-aa65-fabaafbcd75a/image.png" alt=""></p>
<ul>
<li>쇼핑몰에서 주문이 들어오면 판매자에게 푸시 보내기</li>
<li>학습을 완료하면 학생에게 포인트 지급하기</li>
<li>컨텐츠 등록할 때 검색 서비스에도 등록하기</li>
<li>인증 번호를 요청하면 SMS로 인증 메시지 발송하기</li>
</ul>
<p>등 위 케이스에서 API 가 &quot;동기&quot; 로 응답 또는 외부 서비스 (API) 응답까지 기다릴 필요가 있을까? 이 부분 부터 시작이다. 대게 아래와 같은 특징을 가진다. </p>
<ol>
<li>연동의 시차가 생겨도 문제가 크지 않다.</li>
<li>실패했을 때 재시도가 가능하다.</li>
<li>나중에 수동으로 처리해도 크리티컬하지 않다.</li>
<li>실패해도 무시해도 된다. (단순 알림)</li>
</ol>
<p>이러한 전제를 바탕으로, 이 책은 여러 실전적 비동기 처리 방식들을 소개한다.</p>
<h4 id="1-별도-스레드로-실행하기">1) 별도 스레드로 실행하기</h4>
<p>대표적인 예시로는 Spring Framework의 <code>@Async</code> 어노테이션을 통해 특정 메서드를 비동기 실행하는 방법이 있다. 다만 주의할 점은 다음과 같다:</p>
<ul>
<li><code>try-catch</code> 구문 사이에 비동기 코드가 껴 있으면 <strong>에러 전파가 안된다</strong></li>
<li>즉, <strong>트랜잭션 롤백이 되지 않음</strong> → 호출부가 아닌 <strong>정의부에서 에러처리 필요</strong></li>
<li>그리고 중요한 사실: <strong>스레드는 메모리를 많이 사용한다</strong></li>
</ul>
<p>이러한 점을 간과하면 비동기 처리가 오히려 시스템 리소스를 잡아먹는 병목이 될 수 있다.</p>
<h4 id="2-메시징">2) 메시징</h4>
<p>아주 주관적으로 나의 경우 &quot;메시징 시스템, 이벤트 기반 아키텍쳐&quot; 를 배우고 이해하는 순간 진짜 새로운 세계가 열렸던 기분이 들었었다. (<a href="https://velog.io/@qlgks1/series/Kafka-Basic">메시징 관련 시리즈 보러가기</a>)</p>
<p><strong><em>Producer</em></strong>: 메시지를 발행
<strong><em>Consumer</em></strong>: 메시지를 구독하고 처리</p>
<ul>
<li>Producer는 <strong>유실에 대비해야</strong> 하며, 타임아웃과 실패에 대한 <strong>에러 처리 로직</strong>을 마련해야 한다.</li>
<li>예를 들어, 재시도할 수도 있지만, <strong>중복 처리를 피하기 위한 고유 식별자</strong> 부여가 필수적이다.</li>
<li><strong>재시도 → 중복 → 소비자 쪽에서 idempotent하게 처리</strong></li>
<li>로그를 남긴다면 <strong>후처리 가능한 형태의 데이터</strong>로 남겨야 한다</li>
</ul>
<p><strong><em>메시지는 일반적으로 아래 2가지로 개념적 분리를 한다</em></strong></p>
<ul>
<li><strong>이벤트(Event)</strong>: &quot;무엇이 발생했다&quot;를 알리는 것 (ex: 주문됨, 배송완료됨)</li>
<li><strong>커맨드(Command)</strong>: &quot;무엇을 해달라&quot;는 요청 (ex: 포인트 지급)</li>
</ul>
<h4 id="3-글로벌-트랜잭션이-필요한-이유">3) 글로벌 트랜잭션이 필요한 이유</h4>
<p>단일 서비스에서 여러 리소스를 조작할 때, 또는 분산 환경에서 <strong>여러 DB나 시스템을 하나의 트랜잭션처럼 다뤄야 할 때</strong> 등장하는 개념이 바로 <strong>글로벌 트랜잭션</strong>이다.</p>
<ul>
<li>사용자가 상품을 주문하면,<ul>
<li>주문 DB에 저장하고</li>
<li>동시에 푸시 알림을 메시지 큐에 넣어야 함</li>
</ul>
</li>
<li>그런데 푸시 메시지를 먼저 보냈는데 주문이 실패했다면?<ul>
<li><strong>알림은 갔지만 실제 주문은 존재하지 않는 비정상 상태</strong>가 된다. → 이를 방지하려면 <strong>글로벌 트랜잭션(2PC 또는 Outbox 패턴)</strong> 같은 처리가 필요하다.</li>
</ul>
</li>
</ul>
<p>위 상황을 아래와 같이 접근할 수 있다.</p>
<ul>
<li><strong>주문 처리 트랜잭션이 끝난 후</strong>, 메시지를 메시지 큐에 전파</li>
<li>메시지 큐에 넣는 로직은 트랜잭션 외부에서 처리</li>
<li>Outbox 테이블에 먼저 메시지를 적재한 후, 커밋 후에 큐로 전달</li>
</ul>
<h4 id="4-궁극적-일관성">4) 궁극적 일관성</h4>
<p>분산 시스템의 핵심 개념 중 하나다.</p>
<ul>
<li>데이터 복제가 실시간은 아니지만, <strong>결국 언젠가는 일치하는 상태에 도달한다</strong></li>
<li>이 과정에서 일시적 불일치가 발생할 수 있음 → 비동기 메시징도 이와 유사</li>
</ul>
<h4 id="5-cdcchange-data-capture">5) CDC(Change Data Capture)</h4>
<ul>
<li>DB의 변경사항을 감지하여, 외부 시스템과 연동하는 비동기 처리 기술</li>
<li>예시: Postgres → Kafka → ELK Stack으로 전파</li>
<li>실제로 아래 두 글에서 Logstash 기반의 CDC 와 Debezium 기반 CDC, 아래 2가지 글을 작성한 적이 있는데 해당 책 내용과 참조하면 많은(?) 도움이 되지 않을까 한다 ㅎㅎ</li>
</ul>
<ol>
<li><a href="https://velog.io/@qlgks1/Elasticsearch-ELK-stack-Postgresql-Logstash-query-based-CDC-%EB%A7%8C%EB%93%A4%EA%B8%B0-by-docker-compose">Elasticsearch - ELK stack &amp; Postgresql &amp; Logstash, query based CDC 만들기 by docker compose</a></li>
<li><a href="https://velog.io/@qlgks1/%EC%B9%B4%ED%94%84%EC%B9%B4-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%99%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC-2-Debezium-Postgresql-Django-log-based-CDC-%EB%A7%8C%EB%93%A4%EA%B8%B0-source-sink-connector">카프카 클러스터와 파이썬 (2) - Debezium &amp; Postgresql &amp; Django, log based CDC 만들기 (source &amp; sink connector)</a></li>
</ol>
<h3 id="6장-동시성-데이터가-꼬이기-전에-잡아야-한다">6장 동시성, 데이터가 꼬이기 전에 잡아야 한다</h3>
<p>서버가 동시에 들어오는 수많은 요청을 어떻게 처리할 것인가는 백엔드 실무에서 매우 중요한 주제다. 특히 하나의 공유 자원에 여러 요청이 동시에 접근하게 되면, &quot;동시성(concurrency)&quot; 문제가 발생하고, 이는 쉽게 <strong>데이터 꼬임</strong> 또는 <strong>경쟁 상태(race condition)</strong> 로 이어질 수 있다.</p>
<p>대표적인 예로는 <strong>투표 시스템</strong>이 있다. 특정 API에 요청이 몰릴 경우, 실제로는 투표 수가 100번 증가했어야 하는데, 중간에 요청이 섞이며 50번만 반영되는 식의 문제다. 이런 문제를 피하기 위한 동시성 제어는 시스템 전반에서 이루어져야 한다.</p>
<h4 id="1-프로세스-수준에서의-동시-접근-제어">1) 프로세스 수준에서의 동시 접근 제어</h4>
<p>서버는 일반적으로 다음 두 가지 방식 중 하나로 동시 요청을 처리한다:</p>
<ul>
<li><strong>스레드 기반 처리</strong>: 각 요청마다 독립된 스레드를 생성하거나 할당하여 처리</li>
<li><strong>비동기 IO 기반 처리</strong>: 이벤트 루프 기반으로, IO 작업이 완료될 때 콜백으로 응답을 처리</li>
</ul>
<p>공유 자원에 대한 접근은 반드시 <strong>임계 영역(critical section)</strong>을 정의하고, 해당 영역에 접근하는 코드에 대해서는 <strong>락(lock)</strong>을 걸어야 한다. 이 흐름은 다음과 같다:</p>
<blockquote>
<p>잠금 획득 → 임계 영역 접근 → 잠금 해제</p>
</blockquote>
<p>Java에서는 <code>synchronized</code>, <code>ReentrantLock</code> 등이 활용되고, 이를 일반화하면 <strong>mutex(mutual exclusion)</strong>라 한다.</p>
<p>또한 <strong>세마포어(semaphore)</strong>를 활용하면, 특정 자원에 <strong>동시 접근 가능한 스레드 수를 제한</strong>할 수 있다. 예를 들어, <strong><em><a href="https://github.com/Check-Data-Out/velog-dashboard-v2-back-office/blob/main/scraping/main.py#L29">Velog Dashboard V2의 비동기 배치 시스템</a></em></strong> 에서는 세마포어 형태로 최대 요청 수를 제한하고 있다.</p>
<p>또 하나의 좋은 전략은 아예 공유 자원을 사용하지 않는 것, 즉 <strong>불변(immutable) 객체를 사용하는 방식</strong>이다. 불변 객체는 상태 변경 자체가 없으므로 동시 접근 제어가 필요 없다.</p>
<h4 id="2-db와-관련된-동시성-제어">2) DB와 관련된 동시성 제어</h4>
<p>동시성 문제는 DB에서도 동일하게 나타난다. 이에 대한 대표적인 접근법은 아래 두 가지로 나뉜다:</p>
<ul>
<li><p><strong>비관적 잠금(Pessimistic Locking)</strong>  </p>
<ul>
<li>&quot;실패할 가능성이 높다&quot;는 가정 하에, 미리 다른 접근을 막는 방식<br>→ 데이터에 <strong>베타적 잠금(exclusive lock)</strong>을 걸어, 한 번에 하나의 클라이언트만 접근 가능</li>
</ul>
</li>
<li><p><strong>낙관적 잠금(Optimistic Locking)</strong>  </p>
<ul>
<li>&quot;성공할 가능성이 높다&quot;는 가정 하에, 변경 전후 값을 비교해 충돌 여부를 판단<br>→ 실제 락 없이 <code>version</code> 필드나 <code>updated_at</code> 비교로 처리</li>
</ul>
</li>
</ul>
<p>특히 <strong>트랜잭션 범위 내에서 외부 시스템(예: PG사, 결제 API)과 연동</strong>해야 하는 경우에는, <strong>비선점 낙관적 방식보다는 선점 비관적 방식이 더 안전하다.</strong> 예컨대 결제 취소까지 함께 고려되어야 한다면, 먼저 확실하게 잠금을 걸어야 한다.</p>
<p>또한 <strong>증분 쿼리 방식(예: <code>UPDATE SET count = count + 1</code>)</strong>은 DBMS가 원자적으로 처리해주는지를 반드시 확인해야 한다. 일부 DB는 이조차도 race condition을 일으킬 수 있다.</p>
<h4 id="3-잠금-사용-시-주의-사항">3) 잠금 사용 시 주의 사항</h4>
<p>잠금을 사용할 때 반드시 고려해야 할 중요한 이슈는 다음과 같다!</p>
<ul>
<li><p><strong>잠금은 반드시 해제되어야 한다.</strong><br>잠금을 놓치면, 해당 자원은 영구적으로 접근 불가해지며 시스템이 멈춘다.</p>
</li>
<li><p><strong>대기 시간 설정을 명확히 해야 한다.</strong><br>무한 대기 대신 <code>timeout</code>을 명시해줘야 대기하는 스레드가 적절히 포기할 수 있다.</p>
</li>
<li><p><strong>교착 상태(deadlock)를 피하자.</strong><br>예: 스레드 A는 잠금 X를 가지고 Y를 기다리고, 스레드 B는 Y를 가지고 X를 기다리는 상황<br>→ 이를 피하기 위해 <strong>잠금 순서를 일관되게 정하거나</strong>, <strong>타임아웃 및 실패 처리 로직</strong>을 함께 두는 것이 중요하다.</p>
</li>
</ul>
<h4 id="4-동시성-회피-전략-단일-스레드-처리">4) 동시성 회피 전략: 단일 스레드 처리</h4>
<p>사실상 가장 간단하고 강력한 전략은 바로 <strong>단일 스레드 처리(single-threaded processing)</strong> 이다.<br>예를 들어, Redis 기반 큐, Kafka consumer group, Node.js의 이벤트 루프 모델 등은 모두 기본적으로 단일 스레드 방식으로 설계되어 <strong>동시성 문제 자체를 회피</strong>하는 전략이다.</p>
<hr>
<p>7장 IO 병목, 어떻게 해결하지에서는</p>
<ul>
<li>네트워크 IO 에 대한 이해와 자원 효율화, 특히 가상 스레드 얘기, </li>
<li>논블로킹 IO 로 성능 올리는 방법과 언제 무엇을 트레이드 오프하냐에 대한 얘기를 다룬다.</li>
</ul>
<p>8장 실무에서 꼭 필요한 보안 지식에서는</p>
<ul>
<li>인증과 인가부터 토근 보안, RBAC(Role Based Access Control) 구조, 데이터 암호화,</li>
<li>Hash-based Message Authentication Code (무결성 인증 보장), 방화벽, 감사 로그, 비정상 접근 처리를 다룬다.</li>
</ul>
<p>9장 최소한 알고 있어야 할 서버 지식에서는</p>
<ul>
<li>대부분 OS 와 shell 기반 인프라 관리에 대한 얘기이며, <strong><em>진짜 이 정도 기본은 해야한다고 매우 동감하는 장이었다.</em></strong></li>
<li>OS 계정과 권한, 프로세스 체크, 디스크 관리, <strong><em>File Descriptor</em></strong>, 서버 timezone, cron(tab),</li>
<li>alias, 네트워크 정보 체크 (ifconfig, netstat 등) 를 다룬다.</li>
</ul>
<p>10장 모르면 답답해지는 네트워크 기초, 11장 자주 쓰는 서버 구조와 설계 패턴를 넘어 부록까지 너무 알차다. 진짜 딱 신입오면 너무 주고싶은 책이다.</p>
<p><del>사실 이제 사무실에 이 책을 두고 새로 오시는 분에게 꼭 찔러주고 싶다. 이걸로 온보딩 과제를 만들어 볼까도 싶다 ㅎ</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[velog dashboard v2 - 벌써 120만개가 모인, 리뷰 & 0.6 version (feat. 서버 비용 0원)]]></title>
            <link>https://velog.io/@qlgks1/velog-dashboard-v2-retrospective</link>
            <guid>https://velog.io/@qlgks1/velog-dashboard-v2-retrospective</guid>
            <pubDate>Wed, 21 May 2025 07:18:21 GMT</pubDate>
            <description><![CDATA[<h1 id="velog-dashboard-v2">Velog Dashboard v2</h1>
<blockquote>
<p>Velog Dashboard 정식 릴리즈 이후 약 2.5개월간의 &quot;결과&quot; 와 <strong><em><code>0.6 version</code></em></strong>
<strong><em>빨리 접속하기: <a href="https://velog-dashboard.kro.kr/">https://velog-dashboard.kro.kr/</a></em></strong>
<strong><em>Github repo: <a href="https://github.com/check-Data-Out/velog-dashboard-v2">https://github.com/check-Data-Out/velog-dashboard-v2</a></em></strong></p>
</blockquote>
<h2 id="누구세요">누구세요?</h2>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/25cda120-66b9-4923-924a-5bb2eba0845a/image.png" alt=""></p>
<ul>
<li><p><code>velog dashboard</code> 라는 프로젝트는 <strong>*&quot;딸각&quot; 으로 velog 전체 통계를 보여주는*</strong> web-application project 입니다! </p>
<ul>
<li><a href="https://velog.io/@qlgks1/velog-dashboard-v2-%EB%B2%A0%ED%83%80-%EC%98%A4%ED%94%88-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EA%B5%AC%EC%9D%B8">velog dashboard v2 - 베타 오픈!!</a></li>
<li><a href="https://velog.io/@qlgks1/velog-dashboard-v2-%EB%94%B8%EA%B9%8D%EC%9C%BC%EB%A1%9C-%EC%A0%84%EC%B2%B4-%ED%86%B5%EA%B3%84-%EB%B3%B4%EA%B8%B0">velog dashboard v2 - 딸깍으로 전체 통계 보기</a></li>
</ul>
</li>
<li><p>오픈 1주일만에 100명이상 빠르게 등록해주시며, (🙏🙇🏻‍♂️) 확실하게 velog 의 통계 쉽고 한 눈에 보고 싶은 마음이 있는 것을 증명해주셨습니다.ㅎㅎ 사실 꽤 예전에도 이런 시도가 많았는데요, 다 사양되고, 개개인에 의존하고 있는 형태였었습니다. </p>
</li>
<li><p>사실 <a href="https://velog.io/@qlgks1/velog-dashboard-%EC%A0%9C%EC%9E%91%EA%B8%B0-%EB%B2%A8%EB%A1%9C%EA%B7%B8-%ED%86%B5%EA%B3%84%EB%A5%BC-%ED%8E%B8%ED%95%98%EA%B2%8C-%EB%B3%B4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94-%E3%85%A0">&quot;velog dashboard 제작기 (1) - 벨로그 통계를 편하게 보고 싶어요 ㅠ&quot;</a> 라는 것으로 23년도 11월 즈음 한 번 등장한 적이 있었으나, <strong><em>제가 만든 해당 플젝이 velog 쪽 DBMS 를 터뜨려버렸습니다...</em></strong></p>
</li>
<li><p>그래서 머리부터 발끝까지 완전 재정비해서 <code>0 to 1</code>, 지금은 <code>1 to 10</code> 을 향해 나가고 있는 project 입니다!! ㅎㅎ </p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/a176319c-4037-4aa1-8e1f-ad454e32a2da/image.png" alt=""></p>
<blockquote>
<p><strong><em>아직 확인 안해보신 velog 유저분들 가볍게 &quot;딸각&quot; 으로 전체 통계 한 번 보고가셔요!!~</em></strong></p>
</blockquote>
<h2 id="성과-공유">성과 공유</h2>
<p><strong><em>일단 daily 통계 개수가 벌써 120만개가 넘었습니다..!🥳🥳🎉</em></strong> 그래서요.. supabase 가 더 이상 무료 티어 못쓴대요.. (사실 <code>egress</code> 때문) 좋은 소식인지 슬픈 소식인지 애매하네요 🥹</p>
<p>하지만 &quot;수평 분할!!&quot;, 샤딩을 세팅 중이며, 받지 못했던 신규 사용자 분들을 더 적극적으로 받을 수 있게 되었습니다!! 이 때문에 어디서 사용해보라고 적극적으로 말을 못했었어요 ㅎㅎ..</p>
<p><em>이제 더 편하게 확인해주세요!! 🔥🔥</em></p>
<h3 id="1-게시글-수">1) 게시글 수</h3>
<ul>
<li><strong><code>17,248 개</code></strong></li>
<li>저는 이 부분에서 놀랐는데, &quot;전체 통계 궁금하신 분들은 velog 헤비유저였습니다.&quot; <del>(당연한 것인가?!)</del></li>
<li>사실 처음 가설로는 velog 사용하시는 분들 대부분 본능적으로 전체 통계가 궁금할 것 이라고 생각했는데, 압도적인 헤비 유저분들의 리텐션차이가 있었습니다!!</li>
</ul>
<h3 id="2-사용자-별-평균--중앙값-게시글-수">2) 사용자 별 평균 &amp; 중앙값 게시글 수</h3>
<ul>
<li><strong><code>Average</code></strong>: 약 87 개</li>
<li><strong><code>Median</code></strong>: 약 56 개</li>
<li>평군과 중앙값 에서 알 수 있듯, &quot;<code>long tail</code>&quot;,, 소수 상위 유저가 견인하고 있는 게시글 수 형태 입니다 ㅎㅎ</li>
<li>가볍게 <em>파레토 법칙 기반의 상위 10%</em> 사용자 게시글이 몇 % 차지하는지 보면 아래와 같더라구요?!</li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>해석</th>
</tr>
</thead>
<tbody><tr>
<td>상위 10% 유저 수</td>
<td>20명</td>
<td>-</td>
</tr>
<tr>
<td>상위 10% 게시글 수</td>
<td>7,320개</td>
<td><strong><em>전체의 42.44%</em></strong></td>
</tr>
<tr>
<td>하위 50% (6~10분위) 유저 수</td>
<td>99명</td>
<td>-</td>
</tr>
<tr>
<td>하위 50% 게시글 수</td>
<td>2,377개</td>
<td><strong><em>전체의 13.78%</em></strong></td>
</tr>
</tbody></table>
<h3 id="3-active-user">3) Active User</h3>
<p><em>(PS. 철저하게 GA 랑 web-server log 에 의존한 분석.. 오차 분명 존재 가능성 농후 :&#39;))</em></p>
<h4 id="dau--mau">DAU &amp; MAU</h4>
<ul>
<li><strong><code>DAU</code></strong>: 평균 약 30-50 선</li>
<li><strong><code>MAU</code></strong> : 평균 약 1000-2000 사이 선</li>
<li>사실 생각보다 많아서 놀랬습니다,, <del>왜 많아요..?</del></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/571182e5-e0fb-4651-8431-641372b06a81/image.png" alt=""></p>
<ul>
<li><p>역시 위에서 살펴본 바와 같이 우리 헤비 유저 분들이 견인하고 있는 수치입니다. 물론 여기엔 제가 정말 매일 아주 잘 사용하고, 만족하고 있습니다 ㅎ <del>나 쓸라고 만든 product</del></p>
</li>
<li><p>유입은 Direct 가 가장 많았는데, 긍정적으로 해석하자면 &quot;대부분의 daily 활성 사용자 분들은 <strong><em><code>즐겨찾기</code></em></strong> 로 들어오고 계셨다~&quot; 라고 볼 수 있습니다! 왜냐면 저흰 마케팅이란걸 전혀 안하기때문.. 유입 채널은 velog 의 &quot;제 게시글 뿐&quot; ...</p>
</li>
<li><p>혹시나 코호트를 포함한 이상을 바라신다면,, pass ㅎㅎ 사실 추적을 위한 세부 세팅을 안해둬서 (애초에 구해볼 생각을 안했어서..) <del>귀찮아서 패스입니다..</del></p>
</li>
</ul>
<h2 id="update">Update!!</h2>
<ul>
<li>사실 안보이는 부분 업데이트가 정말 많았습니다.. 매우 슬프게 다들 투여할 수 있는 시간이 제한적이라 ㅠㅜ 특히 <strong><em><code>&quot;최적화(메모리, 네트워크 사용량, 레이턴시, 랜더링 퍼포먼스)&quot;</code></em></strong> 많은 노력을 했습니다 ㅎㅎ)</li>
</ul>
<h3 id="1-타임존-관련-이슈-해결">1) 타임존 관련 이슈 해결!</h3>
<blockquote>
<p><em>그렇게 당하고 또 당해버린 타임존 이슈..</em> 이 때문에 00시 부터 09시 까지 저희 통계는 stop the world 가 되었죠.. KST 기준 새벽에 쓴 글이 어제 쓴글이 되어버리기도 하구요</p>
</blockquote>
<ol>
<li>&quot;통계값을 가져오는 배치&quot; 는 KST 기준으로 데이터를 동기화 하고 있었습니다. 그리고 &quot;날짜 값만&quot; 저장했죠!</li>
<li>하지만 API 서버에서 이를 간과하고 <code>SQL</code> 들이 모두 UTC 기준으로 &quot;하루&quot;를 책정하고 있었죠.. </li>
<li>그 덕분에 FE 에서받는 시간관련값이 어떤 곳은 KST 로 변환되서 주고, 어떤 곳은 UTC 그대로 주고 있었죠.. (FE 에서 아주 아찔)</li>
<li>더욱이 <code>api server, node runtime</code> 과 <code>supabase</code> 모두 UTC 기준으로 모두 셋업되어 있었습니다! ㅎㅎ</li>
<li>처음에 급한대로 FE 에서 일단 시간 보정을 했는데, 다들 아시다시피 client-side, 즉 브라우저에서 실행되는 Date 는 대부분의 method 가 브라우저 시간을 따라가더라구요! 결국 아래와 같은 <strong><em>강제 KST 보정 함수</em></strong> 를 사용하고 있습니다!</li>
</ol>
<pre><code class="language-ts">const KST_DIFF = 9 * 60 * 60 * 1000;

/**
 * KST로 변환된 날짜 정보를 담는 인터페이스
 */
export interface KSTDateFormat {
  /** &quot;YYYY-MM-DD&quot; 형식의 날짜 문자열 */
  short: string;

  /** ISO 8601 형식 + KST 오프셋 포함 문자열 */
  iso: string;

  /** KST로 보정된 Date 객체 */
  full: Date;
}

/**
 * 주어진 날짜 문자열을 KST(한국 표준시) 기준으로 변환함.
 *
 * @param {string} [date] - 변환할 날짜 문자열 (예: &quot;2025-05-15T08:00:00Z&quot;)
 * @returns {KSTDateFormat | undefined} 날짜가 없으면 undefined 반환
 */

export const convertDateToKST = (date?: string): KSTDateFormat | undefined =&gt; {
  if (!date) return;

  // UTC 날짜 파싱
  const utcDate = new Date(date);

  // UTC+9 (KST) 시간으로 변환
  const kstTimestamp = utcDate.getTime() + KST_DIFF;
  const kstDate = new Date(kstTimestamp);

  // UTC 메서드를 사용하여 KST 시간을 추출
  // (UTC 메서드에 KST 시간을 넣으면 원하는 결과를 얻을 수 있음)
  const year = kstDate.getUTCFullYear();
  const month = (kstDate.getUTCMonth() + 1).toString().padStart(2, &#39;0&#39;);
  const day = kstDate.getUTCDate().toString().padStart(2, &#39;0&#39;);
  const hours = kstDate.getUTCHours().toString().padStart(2, &#39;0&#39;);
  const minutes = kstDate.getUTCMinutes().toString().padStart(2, &#39;0&#39;);
  const seconds = kstDate.getUTCSeconds().toString().padStart(2, &#39;0&#39;);

  return {
    short: `${year}-${month}-${day}`,
    iso: `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+09:00`,
    full: kstDate,
  };
};</code></pre>
<ul>
<li><p>왜? 라면, &quot;하루&quot; 라는 기준이 client side 브라우저 시간대에 따라 달라지지만, 저희 서비스는 &quot;하루&quot; 를 배치에서 무조건 KST 기준으로 하기 때문에 only KST 라는 기준을 세워야 했습니다!! (철저한 Korea 로컬라이제이션)</p>
</li>
<li><p>사실 실제 root cause 를 찾는 과정과 debugging 은 위 시나리오처럼 간단하지 않았습니다.. 시작은 리더보드와 Query Explain 이 던진 공이었죠 ㅎㅎㅎㅎㅎ 🥹 그리고 모든 SQL 을 갈아 엎었구요. ㅎㅎㅎㅎ</p>
</li>
</ul>
<h3 id="2-공지사항-상단-navbar-에-공지사항이-생겼습니다">2) 공지사항: 상단 navbar 에 공지사항이 생겼습니다!!</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/433d7def-eef4-4b24-8a6a-cdddaf38d443/image.png" alt=""></p>
<ul>
<li>아직 확인을 안하셨다면? 아래와 같이 &quot;상단 상태바&quot; 형태로 인디케이터가 있습니다~~</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fb9b96a7-d00f-47f7-a536-f6381db50cc5/image.png" alt=""></p>
<h3 id="3-드디어-리더보드-리더보드-기능이-생겼습니다">3) 드디어, 리더보드: 리더보드 기능이 생겼습니다!!</h3>
<ul>
<li><strong><code>[ 사용자 또는 게시글 기준 / 조회수 증가 또는 좋아요 증가 / 10위 또는 30위 / 30일 또는 7일 ]</code></strong> 필터로 리더보드를 볼 수 있습니다!</li>
<li>저희가 <code>username</code> 은 저장하지 않고 있어요! <strong><em>email 없으신 분들은 리더보드에서 제외됩니다!!</em></strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/fc93626f-506e-46a6-8542-db0a1ab7ac69/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/75d17364-c82c-4483-96d1-279c70e5c889/image.png" alt=""></p>
<h3 id="4-qr-코드-로그인">4) QR 코드 로그인!</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f0108236-72e7-4aa9-86f5-6ba5ede5ae85/image.png" alt=""></p>
<ul>
<li>다중 로그인에 불편함이 있으셨을거에요!! 이제 QR 코드 한 방으로 자동 로그인이 가능합니다!! PC to PC 를 위해서 QR 밑에 url 로 추가하려고 합니다!!</li>
</ul>
<h3 id="5-드디어-공식-extension">5) 드디어, 공식 &#39;Extension&#39;</h3>
<ul>
<li>이는 저희쪽 API를 활용한 익스텐션이며, 벨로그 페이지에서만 보여주는 전체 요약 통계 입니다!!</li>
<li>이를 바탕으로 readme 의 카드, 특정 곳에 임베딩 등과 같이 아주 다양한 곳에서 활용 가능성을 기대하고 있습니다~~</li>
<li><del>아직 익스텐션 앱스토어에 릴리즈는 못했습니다 / 계속 허가를 안해줘..</del></li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/0fbfd27a-6675-4217-917c-db74ef04b68a/image.png" alt=""></p>
<ul>
<li><strong><em><a href="https://drive.google.com/file/d/1Ccn7WnYFQ6dJP29LZW9BwUxqhqZAhS8D/view?usp=sharing">다운로드는 여기서!!</a></em></strong><ul>
<li><a href="https://drive.google.com/file/d/1Ccn7WnYFQ6dJP29LZW9BwUxqhqZAhS8D/view?usp=sharing">https://drive.google.com/file/d/1Ccn7WnYFQ6dJP29LZW9BwUxqhqZAhS8D/view?usp=sharing</a></li>
<li>추가하는 방법이 궁금하시다면?! - <a href="https://support.google.com/chrome_webstore/answer/2664769?hl=ko">https://support.google.com/chrome_webstore/answer/2664769?hl=ko</a></li>
</ul>
</li>
</ul>
<h3 id="6-마무리">6) 마무리</h3>
<ul>
<li>센트리를 붙여서 live-time issue 트래킹이 가능해요!! nextjs 의 build output, source map 때문에 애를 먹었습니다.. </li>
<li>전체적으로 FE 사이즈 조절, 반응형 고도화를 했어요!!</li>
<li><strong><em>과거의 daily 통계값은 아마 이제 5~6개월까지만 보관하려고 합니다!!</em></strong><ul>
<li>이 때문에 게시글 하나 하나의 전체 범위 통계 변화 조회가 아니라, 최대 6개월 내외로 조회가 가능하게 될 것입니다. 🙏🙇🏻‍♂️</li>
</ul>
</li>
</ul>
<hr>
<h2 id="서버-비용은-0-원">서버 비용은 0 원!!!</h2>
<h3 id="어떻게">어떻게?</h3>
<ul>
<li><p>사실 너무 별거 없어서 공유 드릴 말이 없는데, 많이 문의 주셔서 다시 남겨보자면! <code>Oracle</code> + <code>Supabase</code> + <code>git action</code> 조합이라 그렇습니다. </p>
</li>
<li><p>특히 엄청난 transaction 들이 필요한 API 도 딱히 없고 (최근에 추가된 리더보드 빼고) 엄청난 DAU 가 있는 것 도 아니라 완전 버틸만 합니다. </p>
</li>
<li><p>stand-by 상태 oracle instance 가 총 8대 정도라 (api + fe 만 6개 가용, 팀원들 인스턴스 ㅎㅎㅎ) 추산치 &amp; 예상치로 1~5만까지는 거뜬할 것 같네요. (로드밸런서만 잘 버텨준다면)</p>
</li>
<li><p>결국 저희한테 제일 중요한게 *&quot;통계 batch&quot;* 인데, 지금 <a href="https://github.com/Check-Data-Out/velog-dashboard-v2-back-office/actions">https://github.com/Check-Data-Out/velog-dashboard-v2-back-office/actions</a> 보시면 <strong><em>20개 aggregation (scraping) 배치가 열일중!!</em></strong></p>
</li>
</ul>
<h3 id="무료-티어-근데-더-못쓴대요">무료 티어 근데 더 못쓴대요..</h3>
<ul>
<li><p>그래서 <code>supabase</code> 무료 티어를 &quot;샤딩&quot; 하려고 합니다 흐흐흐 😈😈. duplication 을 하려는 것은 아니고 완벽한 물리적 분할, <strong><code>Horizontal Partitioning</code></strong> 을 하려고 합니다!</p>
</li>
<li><p>덕분에 더 많은 velog 사용자분들의 daily 통계를 보여드릴 수 있습니다!!</p>
</li>
</ul>
<hr>
<h2 id="to-be">TO BE</h2>
<h3 id="1-llm-을-활용-해보려고-합니다">1) LLM 을 활용 해보려고 합니다!</h3>
<h4 id="왜">왜?</h4>
<ul>
<li><p>근본적으로 &quot;벨로그 통계&quot; 는 어떻게 보면 마케팅에서 콘텐츠 성과 측정과 같은 목적이죠! 성과 측정은 콘텐츠의 퀄리티 또는 방향에 대해서 고민하게 되고, 결국 더 좋은 방향으로 가게 됩니다!!</p>
</li>
<li><p>하지만 이는 너무 마케팅 관점이라, 진정으로 &quot;내가 가고 싶은 방향&quot; 이라는 다를 수 있다고 생각했어요! &quot;동기부여&quot; 관점과 &quot;내가 가고 싶은 방향&quot; 을 align 하기 위해서는 &quot;동반자&quot; 가 될 수 있는 어떤 것이 있으면 훨씬 좋다고 생각했고, 단순한 aggregation 이 아니라, 자연어와 함께 개개인의 &quot;콘텐츠 동반자가 되는&quot; LLM 을 붙이고 싶었습니다!!</p>
</li>
</ul>
<h4 id="내용">내용</h4>
<ul>
<li><p>&quot;주간 트랜드&quot; 와 &quot;사용자의 주간 게시글&quot; 을 LLM 활용해 &quot;내용을 포함한 글의 트랜드 &amp; 통계 변화 분석&quot; 해서 &quot;주간 메일링&quot; 을 하려고 합니다!!</p>
</li>
<li><p>계속 프로프팅 하고, mail template 과 내용을 바꿔가며 테스트해보고 있습니다! 아마 아래와 같은 형태가 아닐까? 합니다! (ps. 특정 데이터는 모킹된 가짜)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4c25bd7c-34de-47fa-8654-6dba64b5373b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/dc74e7b9-71e2-45ae-ab25-32d45a861397/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/d57a9072-9a1c-405f-8a91-2b1eacc4e529/image.png" alt=""></p>
<h3 id="2-다중-플랫폼의-통계를-가져와-보려고-합니다">2) 다중 플랫폼의 통계를 가져와 보려고 합니다!!</h3>
<p><strong><em>바로 다음 타켓은 <code>티스토리</code> &amp; <code>Medium</code> 입니다!</em></strong></p>
<ul>
<li>둘 다 좋은 통계를 제공하는데요, 더욱이 티스토리는 &quot;유입 분석&quot; 역시 제공합니다. (과거 다음블로그 시절, 완전하게 콘텐츠 광고로 사용되었기 때문에...) </li>
<li>하지만 velog-dashboard v2 는 궁극적으로 &quot;콘텐츠 통계 aggregation&quot; 으로 나아가고 있고, 이를 발판 삼아 <strong>*&quot;기술 블로그들의 aggregation &amp; contents hub&quot; 가 되고자 합니다!*</strong></li>
</ul>
<hr>
<blockquote>
<p>사실 가끔 velog 통계에 왜 그렇게 집착하냐고 듣는데 ㅋㅋ 여기가 &quot;시작인 것&quot; 뿐 입니다. ㅎㅎ 저희 25년도 North Start는 <strong><em>velog를 쓰는 모든 사람이 전체 통계를 아주 편하고 빠르게 보게하는 것</em></strong> 입니다!! <del>26년, 27년 North Start는 다음 기회에 ㅎㅎ</del>
여전히 갈 길이 멀었지만, 지금까지 그래왔던 것 처럼 step by step 으로 소소하게 업드레이드 되는 모습 계속 보여드리겠습니다 :) 🙇🏻‍♂️🫡🫡🔥🔥🔥</p>
</blockquote>
<blockquote>
<p><strong><em>빨리 접속하기: <a href="https://velog-dashboard.kro.kr/">https://velog-dashboard.kro.kr/</a></em></strong>
<strong><em>Github repo: <a href="https://github.com/check-Data-Out/velog-dashboard-v2">https://github.com/check-Data-Out/velog-dashboard-v2</a></em></strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[python - 파이써닉한 dataclass 와 ENUM ]]></title>
            <link>https://velog.io/@qlgks1/python-%ED%8C%8C%EC%9D%B4%EC%8D%A8%EB%8B%89%ED%95%9C-dataclass-%EC%99%80-ENUM</link>
            <guid>https://velog.io/@qlgks1/python-%ED%8C%8C%EC%9D%B4%EC%8D%A8%EB%8B%89%ED%95%9C-dataclass-%EC%99%80-ENUM</guid>
            <pubDate>Thu, 01 May 2025 16:23:49 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: python 에서 dataclass 와 ENUM 을 쓰면서 생긴 팁 기록 &amp; 공유 ]</p>
<h1 id="pythonic한-dataclass-와-enum">Pythonic한 dataclass 와 ENUM</h1>
<blockquote>
<p>3.7 부터 등장한 dataclass (<strong><em><a href="https://peps.python.org/pep-0557/">PEP-557</a></em></strong>), 3.4 부터 등장한 ENUM(<strong><em><a href="https://peps.python.org/pep-0435/">PEP-435</a></em></strong>), 사실 필자는 3.8 부터 해당 기능들을 제대로 쓰기 시작했다! 쓰면서 생긴 나름의 사용 방법을 공유하고자 한다 ㅎ (기본적인 사용법은 거의 skip, 만약 처음이라면 해당 글을 비추천!)</p>
</blockquote>
<h2 id="1-dataclass">1. dataclass</h2>
<h3 id="1-정의--기본-사용법">1) 정의 &amp; 기본 사용법</h3>
<ul>
<li><p>얘의 본질적인 등장 핵심은 <strong>&quot;클래스 정의를 간결하게 만들어주는 것&quot;</strong> 이며, <strong><em>기본적으로 생기는 dunder method 에 <code>__init__()</code>, <code>__repr__()</code>, <code>__eq__()</code> 를 알아서 정의해주는 것</em></strong> 이다. (물론 비교 연산자, <code>__hash__</code> 등도 정의를 하게 할 수 있다.)</p>
</li>
<li><p>가장 와닿는 예시를 보면, server-side 에서 특정 layer 마다(또는 외부/내부, 도메인 등) &quot;데이터(Object)&quot; 를 주고 받을때 &quot;너 어떻게 생겼니&quot; 를 가르쳐 줘야 한다. (물론 상남자라면 dict 만 쓰면 된다. <del>평생 혼자 개발하면 된다.</del>) <em>특히 만약 사내에서 python 으로 &quot;DDD&quot; 를 해보자!</em> 한다면 이제 난리나는 부분들이 이 지점이 아닐까 한다 ㅎ</p>
</li>
</ul>
<pre><code class="language-python">class ProductDTO:
    def __init__(self, id, name, price, in_stock=True):
        self.id = id
        self.name = name
        self.price = price
        self.in_stock = in_stock

    def __repr__(self):
        return f&quot;ProductDTO(id={self.id!r}, name={self.name!r}, price={self.price!r}, in_stock={self.in_stock!r})&quot;

    def __eq__(self, other):
        if not isinstance(other, ProductDTO):
            return NotImplemented
        return (self.id, self.name, self.price, self.in_stock) == (other.id, other.name, other.price, other.in_stock)</code></pre>
<ul>
<li><p>아주 심플한 상품 DTO 를 위해 <code>__init__</code> 만 있어도 된다. 하지만 기본적으로 출력이나 equal 연산 정도는 해줘야 의미가 있다. (== 연산) 이게 쌓이고 쌓이다 보면 꽤나 아찔한 상황이 있다. </p>
</li>
<li><p>이때 원래 고민의 포인트는 자연스럽게 <strong>*&quot;<code>Pydantic</code> 를 쓰냐 마냐&quot;*</strong> 였지만, 사견으로 대부분의 경우 <code>dataclass</code> 선에서 정리가 가능한 것 같다. 시리얼라이징/디시리얼라이징 필요 없고, 외부 의존성도 필요없고, (상대적) 훨씬 유연하고, 오버헤드가 없어서 상대적으로 빠르다. </p>
</li>
</ul>
<pre><code class="language-python">@dataclass
class ProductDTO:
    id: int
    name: str
    price: float
    in_stock: bool = True</code></pre>
<ul>
<li><p><code>ProductDTO</code> 를 위와 같이 아주 심플하게 세팅 가능하다. 물론 &quot;API 입출력, 사용자 입력 validation, 데이터 보정이 필요한 경우&quot; 는 무조건 <code>dataclass</code> 를 사용하는 것은 비추천 한다. 오히려 <code>pydantic.dataclasses.dataclass</code> 를 쓰는 것도 좋은 수단이라고 생각한다. - <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/">https://docs.pydantic.dev/latest/concepts/dataclasses/</a></p>
</li>
<li><p>아래 부터는 &quot;사견이 가득 담긴&quot; 기록이다. 모든 활용을 쓰는 것 보다 실제 필자가 많이 쓴 것 정도, 실무에서 많이 사용해 본 것들 위주로 리스트 해봤다.</p>
</li>
</ul>
<h3 id="2-mutable--immutable">2) mutable / immutable</h3>
<ul>
<li><p><code>dataclass</code> 는 기본적으로 &quot;mutable&quot; 한 객체이다. 하지만 외부 API 응답에 대한 단순 정의 같이 &quot;굳이 바뀔필요 없고, 바뀌어서도 안되는 것&quot; 들은 실수를 방지하게 위해 &quot;immutable&quot; 한게 좋다. (서버 설정 값, 배치 시작 설정 값 등)</p>
</li>
<li><p>그럴때 <code>@dataclass(frozen=True)</code> 를 쓰면 바로 read-only 객체, &quot;immutable&quot; 해진다. </p>
</li>
</ul>
<pre><code class="language-python">@dataclass(frozen=True)
class DeliveryStatus:
    order_id: str
    status: str
    updated_at: str

# 외부 API 응답 파싱
resp = DeliveryStatus(order_id=&quot;ORD123&quot;, status=&quot;in_transit&quot;, updated_at=&quot;2025-05-01T12:00:00&quot;)

# 값을 바꾸려 하면?
resp.status = &quot;delivered&quot;
# ❌ FrozenInstanceError: cannot assign to field &#39;status&#39;</code></pre>
<ul>
<li>이게 또 좋은 포인트는 <strong><em>hashable해져서 dict의 키나 set 요소로도 사용 가능</em></strong> 해지는 것이다. good example 은 아니지만 위 예시를 그대로 이어가보면, 아래와 같이 메모리 활용으로 <code>O(1)</code> 속도로 같은 응답 체크가 가능해진다! ㅎㅎ</li>
</ul>
<pre><code class="language-python">cache = {resp: &quot;cached&quot;}
if resp in cache:
    print(&quot;이미 받은 응답입니다.&quot;)</code></pre>
<ul>
<li>참고로 무조건 hashable 해지는게 아니다. 필드에 mutable 객체가 있을 경우 TypeError 발생한다! 이는 밑을 쭉 따라가면 대응 가능한 포인트가 있다 ㅎ</li>
</ul>
<h3 id="3-default_factory">3) <code>default_factory</code></h3>
<ul>
<li><p>사실 팁이라기 보단 생각보다 놓칠 수 있는 <code>default_factory</code>, 그리고 사실 python 3.9 이상이라면 무조건 사용했을 값이다. 즉 <strong>*&quot;기본값이 리스트, 딕셔너리, set 등 <code>mutable</code> 한 타입일 때는 무조건 써야 하는 값이다.&quot;*</strong></p>
</li>
<li><p>어떤 문제가 있었을까? &quot;mutable&quot; 한 object 대상으로 &quot;값을 공유하는&quot; 이슈가 있었다. 사실 이슈라기 보다는 python 자체가 기본값은 &quot;정의 시점&quot; 에 평가되는데, <code>dataclass</code> 최초 객체가 생길때 list 하나 만들었으면, 다음번에 만들때 계속 해당 list object 를 공유하기 때문!</p>
</li>
</ul>
<pre><code class="language-python">@dataclass
class Basket:
    items: list[str] = []

a = Basket()
a.items.append(&quot;apple&quot;)

b = Basket()
print(b.items)  # [&#39;apple&#39;] 😱 ???</code></pre>
<ul>
<li>그래서 &quot;default_factory&quot; 를 쓰며, 3.9 이상에서는 안쓰면 <code>ValueError</code> 가 발생한다. -&gt; <code>ValueError: mutable default &lt;class &#39;list&#39;&gt; for field items is not allowed: use default_factory</code></li>
</ul>
<pre><code class="language-python">from dataclasses import dataclass, field

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)

a = Basket()
a.items.append(&quot;apple&quot;)

b = Basket()
print(b.items)  # [] → 완전히 독립된 객체</code></pre>
<h3 id="4-__post_init__">4) <code>__post_init__</code></h3>
<ul>
<li><code>dataclass</code> 를 만들때 벨리데이션이 필요한 경우가 있다. 여기서 <code>pydantic</code> 에 대한 고민이 들지만, 정말 특정 필드에 대한 검증만 하면 된다면, <code>__post_init__()</code> 를 사용해 볼 수 있다.</li>
</ul>
<pre><code class="language-python">@dataclass
class User:
    name: str
    age: int
    email: str

    def __post_init__(self):
        if self.age &lt; 0:
            raise ValueError(f&quot;❌ 나이(age)는 음수가 될 수 없습니다: {self.age}&quot;)
        if &quot;@&quot; not in self.email:
            raise ValueError(f&quot;❌ 이메일 형식이 잘못되었습니다: {self.email}&quot;)</code></pre>
<ul>
<li><p>절대 음수가 올 수 없는 값에 대해 <code>ValueError</code> 를 만들어 줄 수 있다. 위의 <code>DeliveryStatus</code> 라는 값 예시에서도, 외부에서 주는 값이라면, 그리고 <code>status</code> 가 절대 될 수 없는 값이 있다면, 이 방법이 아주 유용하다. </p>
</li>
<li><p>외부 API 응답은 성공했는데 응답이 기대한 것과 다를때에 대한 검증이 <code>dataclass</code> 만으로 바로 가능하기 때문이다. </p>
</li>
</ul>
<pre><code class="language-python">from datetime import datetime
from dataclasses import dataclass

@dataclass
class Event:
    title: str
    date: str  # 입력은 str으로 오지만…

    def __post_init__(self):
        parsed_date = datetime.fromisoformat(self.date)
        object.__setattr__(self, &#39;date&#39;, parsed_date)
        # 참고로 self.date ... 로도 접근 가능</code></pre>
<ul>
<li><p>더욱이 &quot;값 보정&quot; 을 할 수도 있다. 근데 개인적으로 값 보정 자체를 <code>__post_init__</code> 에서 하는 것은 비추천, &quot;공백 제거 정도&quot; 와 위 예시에서 <code>date</code> 가 어쩔 수 없이 문자열로 받지만, <code>datetime</code> 으로 casting 할 때 유의미 했다. </p>
</li>
<li><p>위 예시는 <code>self.date</code> 를 안썻는데, 이유는 <strong><em><code>frozen=True</code> 경우는 불가능 하기 때문</em></strong> 이다! </p>
</li>
</ul>
<h3 id="5-asdict--astuple">5) <code>asdict</code> / <code>astuple</code></h3>
<ul>
<li><code>DTO</code> 나 아주 간단한 객체에 대한 데이터 명시로 사용할때, &quot;직렬화&quot; 가 필요한 경우 사용했다. 특히 <strong>중첩된 dataclass</strong>, 또는 <code>Kafka (or other message queue)</code> 를 위해서 사용할 때!</li>
</ul>
<pre><code class="language-python">from dataclasses import dataclass, asdict

@dataclass
class User:
    name: str
    age: int

@dataclass
class Post:
    title: str
    author: User

p = Post(title=&quot;Hello&quot;, author=User(name=&quot;Alice&quot;, age=30))
print(asdict(p))</code></pre>
<pre><code class="language-json">{
    &#39;title&#39;: &#39;Hello&#39;,
    &#39;author&#39;: {
        &#39;name&#39;: &#39;Alice&#39;,
        &#39;age&#39;: 30
    }
}</code></pre>
<ul>
<li><p>자동으로 <strong><em>중첩 구조까지 재귀적으로 dict 변환이 된다.</em></strong> 이걸 이제 <code>json.dumps()</code> 할 수 있다! 외부 API / 서버 / 이기종 시스템을 위한 데이터 전송 세팅 준비가 (시리얼라이징) 딸깍 완료! 된다! - 참고로 100% 안전하지는 않는다... <code>datetime</code> 은 직렬화 불가..</p>
</li>
<li><p>그에 반해 사실 <code>astuple</code> 을 많이 쓰지는 않았는데 딱 한 번 매우 동감하며 쓴 기억이 있다. (LLM function call 을 위한) 임시 배치 프로세스를 만들었는데 <code>SQL</code> 을 만들때 &quot;파라미터&quot; 들을 <code>dataclass</code> 로 부터 출발해서 만들때!! 예시로 보면 아래와 같다!</p>
</li>
</ul>
<pre><code class="language-python">from dataclasses import dataclass, astuple

@dataclass
class User:
    name: str
    age: int

u = User(name=&quot;Alice&quot;, age=30)
print(astuple(u))  # (&#39;Alice&#39;, 30)</code></pre>
<ul>
<li><p><code>INSERT INTO</code> 에 아주 이쁘게 <code>(&#39;Alice&#39;, 30)</code> 를 넣을 수 있다. 즉, <strong><em>순서형 데이터가 필요한 경우</em></strong> 꽤나 유용하게 쓸 수 있었다. </p>
</li>
<li><p>그리고 다른 일례로, <strong><em>dict는 mutable이라 hashable하지 않지만, tuple은 가능 (단, 내부에 mutable 요소가 없을 경우)</em></strong> 하기 때문에 <code>if astuple(obj1) == astuple(obj2): ...</code> 이런 해쉬 기반으로 아주 빠른 비교가 가능하다. </p>
</li>
</ul>
<h3 id="6-order-옵션을-통해-data간-대소비교">6) <code>order</code> 옵션을 통해, data간 대소비교!</h3>
<ul>
<li>dataclass는 기본적으로 <code>__eq__()</code> 만 만들어주지만, <code>order=True</code> 를 설정하면
<code>__lt__</code>, <code>__le__</code>, <code>__gt__</code>, <code>__ge__</code> 까지 자동으로 추가된다!! 즉, 아래처럼 객체끼리 정렬, 크기 비교가 가능해진다. </li>
</ul>
<pre><code class="language-python">from dataclasses import dataclass

@dataclass(order=True)
class User:
    score: int
    name: str

users = [
    User(score=50, name=&quot;Alice&quot;),
    User(score=90, name=&quot;Bob&quot;),
    User(score=75, name=&quot;Charlie&quot;),
]

sorted_users = sorted(users)  # score 기준으로 정렬됨</code></pre>
<ul>
<li>만약 여러분들이 <code>heapq</code> (우선순위 큐) 를 쓴다면? 최소/최대값을 찾아야 한다면?! 대소 비교 (&lt;, &gt; 비교) 가 필요하다면?! 쓰는 것을 추천!</li>
</ul>
<h4 id="참고로-선언된-순서로-소팅-우선순위가-된다">참고로 선언된 순서로 소팅 우선순위가 된다!</h4>
<pre><code class="language-python">@dataclass(order=True)
class Person:
    sort_order: int  # 이걸 기준으로 정렬됨
    name: str</code></pre>
<h3 id="7-slotstrue-로-메모리-최적화">7) <code>slots=True</code> 로 메모리 최적화?!</h3>
<ul>
<li>참고로 이건 <code>Python 3.10+</code> 전용이다. 최근에 배치 프로세스 만들때, 사용자 데이터 분석 할 때 (bluk 로 가져올때) 사용했다. 목적은 단순하다. <code>dataclass</code> 만들때 <code>__dict__</code> 속성도 만들어진다. 근데 이걸 못하게 하는거다! (ps. python class 만들때 <code>__dict__</code> 가 만들어짐)</li>
</ul>
<pre><code class="language-python">from dataclasses import dataclass

@dataclass
class MyData:
    x: int
    y: int

d = MyData(1, 2)
d.z = 3  # 동적으로 필드 추가
print(d.__dict__)  # {&#39;x&#39;: 1, &#39;y&#39;: 2, &#39;z&#39;: 3}</code></pre>
<ul>
<li>위 예시에서 <code>z</code> attribute 가 만들어지는 것을 막을 수 있다. 참고로 원래 python 에서는 <code>class</code> 에서 이걸 막으려고 전통적으로 <code>__slots__</code> 을 사용했고, 아래와 같은 방식으로 사용했다. (아마 python 2 부터 가능한 것으로 알고 있다.)</li>
</ul>
<pre><code class="language-python">@dataclass
class A:
    __slots__ = [&#39;x&#39;, &#39;y&#39;]
    x: int
    y: int
    z: int  # ⚠️ 이 필드는 누락되었기 때문에 AttributeError 발생 가능</code></pre>
<ul>
<li>하지만 이제 데코레이터에 <code>slots=True</code> 를 추가하면 바로 가능!</li>
</ul>
<pre><code class="language-python">@dataclass(slots=True)
class B:
    x: int
    y: int
    z: int  # 걱정 없음, 자동 처리됨</code></pre>
<ul>
<li>특히 여기에 대한 아주 강력한 추천 글이 있다! - <strong><em><a href="https://stackoverflow.com/questions/472000/usage-of-slots">Usage of <strong>slots</strong>?</a></em></strong></li>
</ul>
<p>ps) 이 외에 분명 엄청 많지만, 최대한 사견을 담은 것 위주로 리스팅했다. 더욱이 <code>unsafe_hash</code> 같은 것은 비추천.. 한다..!</p>
<hr>
<h2 id="2-enum">2. ENUM</h2>
<ul>
<li>&quot;왜?&quot; 와 &quot;정의&quot; 에 대해서는 최대한 간략하게만 살펴보자</li>
<li><code>Enumeration</code>, 열거형은 개인적으로 <strong>*&quot;휴먼 에러 최소화&quot;*</strong> 에 초점이 가득 담긴 친구라고 생각한다.</li>
<li>&quot;정해진 것&quot; 만 값으로 넣을 수 있게 강제화 하니 IDE 에서 체크하기도 쉽고, 가독성 올라가고, 타입 안정성 올라가고, 더불어 &quot;상수&quot; 니까 자연스럽게 &quot;유지보수성&quot; 을 증가시킨다. </li>
</ul>
<blockquote>
<p>여기서는 enum 을 쓰면서 기억에 남았던 것들, 좋은 사례를 모아보고자 한다.</p>
</blockquote>
<h3 id="1-python-에서-literal-type-과-enum">1) python 에서 Literal type 과 ENUM</h3>
<ul>
<li><p><code>python</code> 은 보기드문 &quot;순수 객체 지향 언어&quot; 이다. (java 등과는 다르게 원시 타입이 없다.) <code>str</code>, <code>int</code> type 들도 &quot;class&quot; 이고, 객체의 인스턴스가 된다. </p>
</li>
<li><p>과한 자유로움은 장점이자 단점이 되고, 단점을 보강하기 위해 type 을 사용한다. (사실 &quot;협업&quot; 과 &quot;유지보수&quot; 가 최고 목표 아닐까?) python 은 본격적으로 이 부분을 지원한게 <a href="https://peps.python.org/pep-0484/">PEP 484 – Type Hints</a> 로 시작된다. (물론 이전에 다양한 접근 방식이 존재했지만, official 부분만 놓고 보자면!)</p>
</li>
<li><p>이 흐름속에서 등장한게 <strong><code>Literal</code></strong> 이다. 이유는 단순하다. <strong><em>정적 타입 힌팅이 강화되면서 <code>str</code>, <code>int</code> 등 기본 타입은 제한할 수 있었지만, 특정 값만 허용하는 타입 제한은 불가능했기 때문</em></strong> 이다. - <a href="https://peps.python.org/pep-0586/">PEP 586 – Literal Types</a> </p>
</li>
</ul>
<h4 id="enum-과-차이는-아래와-같다">enum 과 차이는 아래와 같다.</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>enum.Enum</code></th>
<th><code>typing.Literal</code></th>
</tr>
</thead>
<tbody><tr>
<td><strong>정의 목적</strong></td>
<td>상수 집합을 클래스로 정의하고, 값과 의미를 묶기 위함</td>
<td>함수 인자나 변수 값이 특정 값 중 하나임을 명시적으로 제한</td>
</tr>
<tr>
<td><strong>런타임 존재 여부</strong></td>
<td>런타임에도 존재하며 객체처럼 동작함</td>
<td>타입 힌트 전용, <strong>런타임에는 아무 기능도 없음</strong></td>
</tr>
<tr>
<td><strong>값의 의미 표현</strong></td>
<td>각 멤버는 이름과 값을 가지며, 독립된 의미 부여 가능</td>
<td>단순한 리터럴 값 (<code>&quot;A&quot;</code>, <code>1</code> 등)을 나열하는 것</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>메서드 추가 등 확장 가능 (<code>Color.RED.hex()</code>)</td>
<td>불가능, 단순한 값 고정</td>
</tr>
<tr>
<td><strong>타입 검사 도구 활용</strong></td>
<td>mypy, pyright 등에서도 동작 가능</td>
<td>타입 힌트로만 작동 (정적 분석 도구에만 의미 있음)</td>
</tr>
<tr>
<td><strong>용례</strong></td>
<td>상태, 종류, 옵션 등의 구분 (<code>UserType.ADMIN</code>)</td>
<td>함수 인자의 값 제한 (<code>Literal[&quot;asc&quot;, &quot;desc&quot;]</code>)</td>
</tr>
</tbody></table>
<ul>
<li>외부 API 호출 응답값 제한, pydantic &amp; dataclass 에서 자유롭게 쓸 수 있고, 함수 파라미터에서 특정 값을 가장 쉽고 빠르게 정의 할 수 있어서 개인적으로 <code>Literal</code> 은 여전히 잘 쓰고 있다. </li>
</ul>
<p><em>ps) Python 3.9~3.11에 걸쳐 typing 라이브러리 일부 기능은 built-in 이 되었으며, 필자는 거의 더 이상 typing 를 사용하지 않고 있다.</em></p>
<h3 id="2-str-class-상속-같이-받기">2) <code>str</code> class 상속 같이 받기</h3>
<ul>
<li><code>.value</code> attribute 접근 필요가 없어진다.</li>
</ul>
<pre><code class="language-python">class Reliability(Enum):
    HIGH = &quot;high&quot;

reliability = Reliability.HIGH
print(type(reliability))    # &lt;enum &#39;Reliability&#39;&gt;
print(reliability)          # Reliability.HIGH 출력
print(reliability.value)    # &quot;high&quot; 출력
print(f&quot;신뢰도: {reliability.value}&quot;)  # &quot;신뢰도: high&quot; 출력</code></pre>
<ul>
<li>위 enum 을 아래와 같이 사용할 수 있다. </li>
</ul>
<pre><code class="language-python">class Reliability(str, Enum):
    HIGH = &quot;high&quot;

reliability = Reliability.HIGH
print(reliability)          # &quot;high&quot; 출력
print(f&quot;신뢰도: {reliability}&quot;)  # &quot;신뢰도: high&quot; 출력</code></pre>
<ul>
<li>python 의 다중상속의 MRO(Method Resolution Order), &quot;왼쪽에서 오른쪽으로&quot; 우선적으로 적용한다. 즉, <code>str</code> 의 문자열 표현과 관련된 메서드(<code>__str__</code>, <code>__repr__</code>, <code>__add__</code> 등)는 <code>str</code> 의 구현이 사용되기 때문이다. </li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li><p><em>type 제한 하려고 enum 썼는데 아니 type 을 다중으로 해버리네?</em> 라고 생각할 수 있다. 위 같은 방식이면 <code>isinstance</code> 와 같은 빡센 타입 체킹에서 예상치 못한 동작이 있을 수 있다!! </p>
<ul>
<li>(ex - <code>isinstance(reliability, str)</code> 가 <code>True</code> 가 되기 때문. 근데 이걸 유도할 수 도 있다. 문자열 비교가 필요하면 말이다.)</li>
</ul>
</li>
<li><p>그래서 한 가지 대안은 아래와 같다! 직접 <code>__str__</code> 을 오버라이딩 하는 것!</p>
</li>
</ul>
<pre><code class="language-python">    def __str__(self):
        return self.value</code></pre>
<p><em>ps) str 상속해서 메모리 오버헤드 걱정이 된다면.. python 을 안쓰는 것을 추천.. 한다..</em></p>
<h3 id="3-비교-가능한-enum-만들기-intenum-사용">3) 비교 가능한 Enum 만들기 (<code>IntEnum</code> 사용)</h3>
<ul>
<li>모든 언어에서 &quot;문자열 비교&quot; 는 비싼 연산 중 하나다. <code>Enum</code> 값들을 문자열 형태로 만들다 보면 &quot;비교 연산&quot; 에서 계속 문자열 비교를 하는 경우가 있다. </li>
</ul>
<pre><code class="language-python">class Priority(Enum):
    LOW = &quot;low&quot;
    MEDIUM = &quot;mi&quot;
    HIGH = &quot;high&quot;</code></pre>
<ul>
<li>이렇게 선언해두면 <code>if current_status == Priority.LOW</code> 와 같은 동등 비교 중심으로만 연산하게 된다. 더욱이 동등 연산자의 전자가 문자열이라면, 자연스럽게 문자열로 바꿔 &quot;문자열 비교 연산&quot; 을 하게 된다. </li>
</ul>
<pre><code class="language-python">class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3</code></pre>
<ul>
<li><p>이제 비교 연산이 가능해진다. <code>if current_status.value &gt; Priority.HIGH.value</code> 와 같이, <strong><em><code>HIGH</code> 가 아닌 모든 것들에 대해 일관적인 조건 체크를 빠르게 할 수 있다.</em></strong></p>
</li>
<li><p>이 방식이 가장 두드러지는 것은 &quot;로깅 레벨&quot; 에서 볼 수 있다. </p>
</li>
</ul>
<h3 id="4-auto-함수와-flag-enum으로-비트-플래그-만들기">4) <code>auto()</code> 함수와 Flag Enum으로 비트 플래그 만들기</h3>
<ul>
<li><p>나는 &quot;값&quot; 이 중요한게 아니라 &quot;키&quot; 가 중요해! 일 경우 <code>auto</code> 를 쓸 수 있다. 사실 위에 예시와 같이 &quot;상태&quot; 는 &quot;값&quot; 보다는 &quot;의미, 키&quot; 가 중요하다. </p>
</li>
<li><p>auto()는 기본적으로 1부터 시작해 1씩 증가하는 값을 제공한다. 실수로 같은 값을 넣어서 이슈가 있을 가능성 자체가 없어진다. (<code>_generate_next_value_</code> 메서드를 내부적으로 활용한다. 이를 오버라이딩하면 전혀 다른 값으로 증가하게 할 수 있다.)</p>
</li>
</ul>
<pre><code class="language-python">from enum import Enum, auto

class Status(Enum):
    PENDING = auto()    # 1
    ACTIVE = auto()     # 2
    INACTIVE = auto()   # 3</code></pre>
<ul>
<li>새로운 &quot;키&quot; 가 추가되어도 &quot;값&quot; 에 신경 안쓰고 바로 늘릴 수 있다. 하지만 <strong>순서에 따라 할당 받는 값</strong> 이 달라지며 이 경우 <strong><em>value 비교연산은 안하는게 좋다.</em></strong> 그리고 이 <code>auto()</code> 를 활용해 <code>Flag</code> enum 을 좀 더 편하게 사용이 가능하다.</li>
</ul>
<h4 id="그-전에-비트-연산-을-알아야-이해가-쉽다-비트마스킹-등">그 전에 &quot;비트 연산&quot; 을 알아야 이해가 쉽다. (비트마스킹 등)</h4>
<ol>
<li><code>| (OR)</code>: 두 비트 중 하나라도 1이면 1</li>
<li><code>&amp; (AND)</code>: 두 비트가 모두 1이면 1</li>
<li><code>^ (XOR)</code>: 두 비트가 서로 다르면 1</li>
<li><code>~ (NOT)</code>: 비트 반전 (1→0, 0→1)</li>
<li><code>&lt;&lt;, &gt;&gt; (SHIFT)</code>: 비트를 좌/우로 이동</li>
</ol>
<ul>
<li>이걸 통해 뭘 할 수 있을까? 아래와 같은 예시처럼, 한 바이트로 8가지 상태 (비트당 하나의 상태값 표시 가능) 에 표현이 가능하다. (사실 8가지가 아니라 경우의 수로 보면 총 2^8 = 256가지의 다른 조합 표현이 가능하다.)</li>
</ul>
<pre><code class="language-python"># 이렇게 8개 변수 대신
is_read = True
is_write = False
is_execute = True
# ...5개 더

# 이렇게 하나의 변수로 표현
permissions = 0b10100101  # 한 바이트로 8가지 상태 표현</code></pre>
<ul>
<li>물론 보는 사람이 &quot;사람&quot; 이라서 가독성이랑 유지보수성이 부족하다. H/W 에 가까운 개발을 할수록 사실 이런 비트연산은 &quot;기본&quot; 에 가까워진다. 근데 이걸 <strong><em>Enum 개념을 조금 차용해서 Flag 를 활용하면?!</em></strong> 아래와 같이 쓸 수 있다.</li>
</ul>
<pre><code class="language-python"># 여러 개의 if문 대신
if user.can_read and (user.is_admin or user.is_owner) and not user.is_banned:
    # 작업 수행...

# 비트 연산으로 한 번에 처리
required_perm = READ | (ADMIN | OWNER) &amp; ~BANNED
if user.permissions &amp; required_perm == required_perm:
    # 작업 수행...</code></pre>
<ul>
<li>실제 <code>Flag Enum</code> 은 아래와 같이 <code>Flag</code> 클래스를 상속받아 비트 플래그 Enum을 만들 수 있다.</li>
</ul>
<pre><code class="language-python">from enum import Flag, auto

class Permission(Flag):
    READ = auto()      # 1 (binary: 001)
    WRITE = auto()     # 2 (binary: 010)
    EXECUTE = auto()   # 4 (binary: 100)

    # 조합된 값, 복합 상태 값! -&gt; 이게 진짜 강력하다!
    READ_WRITE = READ | WRITE          # 3 (binary: 011)
    ALL = READ | WRITE | EXECUTE       # 7 (binary: 111)

# 사용 예시
user_perm = Permission.READ | Permission.WRITE  # 3

# 비트 연산으로 권한 확인
if Permission.READ in user_perm:
    print(&quot;사용자는 읽기 권한이 있습니다&quot;)</code></pre>
<ul>
<li><p>가장 편리한 지점은, 어짜피 비트 연산을 눈으로 매번 따라가면 실수하기 쉽다. 그러니까 자연스럽게 <code>auto</code> 랑 같이 섞어서 쓸 수 있고, 더욱이 <strong>*&quot;복합 상태 값&quot; 이 진짜 강력한 것이다.*</strong></p>
</li>
<li><p>만약 통신에 이 개념을 사용하면, <strong><em>데이터 전송 효용성</em></strong> 과 그에 따른 <strong><em>비용 절약</em></strong> 을 기본으로 깔고 갈 수 있다. (사실 실제로 최적화 관점에서 많이들 사용한다.)</p>
</li>
</ul>
<h3 id="5-enum-값의-유일성-강제하기-unique-데코레이터">5) Enum 값의 유일성 강제하기 (unique 데코레이터)</h3>
<ul>
<li><code>Enum</code> 은 기본적으로 &quot;값&quot; 의 중복을 허용하며 이를 &quot;alias(별칭)&quot; 으로 받아들인다. </li>
</ul>
<pre><code class="language-python">from enum import Enum

class Color(Enum):
    RED = 1
    CRIMSON = 1  # RED의 별칭
    GREEN = 2</code></pre>
<ul>
<li>근데 &quot;값&quot; 자체의 유일함이 중요하다면, <code>@unique</code> 데코레이터를 사용하면 된다!</li>
</ul>
<pre><code class="language-python">from enum import Enum, unique

@unique
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3
    # DUPLICATE = 1  # 이렇게 하면 ValueError 발생</code></pre>
<ul>
<li>이게 빛을 발하는 경우는, <strong><em><code>auto()</code> 랑 커스텀을 같이 할 때</em></strong> 인 것 같다. 즉, <code>auto</code> 에 <code>unique</code> 를 달아줘서 의도치 않은 실수 방지를 강제할 수 있다!</li>
</ul>
<pre><code class="language-python">from enum import Enum, auto, unique

@unique
class Status(Enum):
    START = auto()
    RUNNING = auto()
    STOP = 1  # ValueError 발생 가능성!</code></pre>
<h3 id="6-복합-값을-가진-enum-만들기">6) 복합 값을 가진 Enum 만들기</h3>
<ul>
<li><code>Enum</code> 값으로 튜플이나 복잡한 객체를 사용할 수 있다! 사실 개인적으로 협업할때 가장 선호하는 형태이긴 하다. 특히 단순한 Enum 이 아니라 <strong>*&quot;상태 값을 나타내는&quot; 경우*</strong>, &quot;사람&quot;을 위해서 코드의 가독성을 올려보자!</li>
</ul>
<pre><code class="language-python">class HttpStatus(Enum):
    OK = (200, &quot;Success&quot;)
    NOT_FOUND = (404, &quot;Not Found&quot;)
    SERVER_ERROR = (500, &quot;Server Error&quot;)

    def __init__(self, code, message):
        self.code = code
        self.message = message

status = HttpStatus.NOT_FOUND
print(f&quot;Status: {status.code}, Message: {status.message}&quot;)</code></pre>
<h4 id="여기에-이어서-문서화에-활용-할-수-있다">여기에 이어서 &quot;문서화에 활용&quot; 할 수 있다!!</h4>
<pre><code class="language-python">class LogLevel(Enum):
    &quot;&quot;&quot;로깅 레벨을 정의하는 열거형 클래스&quot;&quot;&quot;

    DEBUG = 10
    &quot;&quot;&quot;디버깅 목적의 상세 정보&quot;&quot;&quot;

    INFO = 20
    &quot;&quot;&quot;일반적인 정보 메시지&quot;&quot;&quot;

    WARNING = 30
    &quot;&quot;&quot;잠재적 문제 상황에 대한 경고&quot;&quot;&quot;

    ERROR = 40
    &quot;&quot;&quot;프로그램 실행은 가능하나 오류가 발생함&quot;&quot;&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/09f06f70-44af-4b67-bada-71a89ddd74c0/image.png" alt=""></p>
<ul>
<li>이 얼마나 아름다운 &quot;사람&quot; 과 &quot;협업&quot; 을 위한 <code>Enum</code> 인 것인가!! 더 나아가 Enum 의 <code>__doc__</code> 을 살펴보면 좋다. - <a href="https://tech.isyncbrain.com/python/enum/alias/sqlalchemy/2022/05/15/annotated-enum.html">https://tech.isyncbrain.com/python/enum/alias/sqlalchemy/2022/05/15/annotated-enum.html</a> (Enum <code>__doc__</code> 을 SQLAlchemy 와 함께 활용한 예제!)</li>
</ul>
<h3 id="7-_missing_-메서드로-커스텀-룩업-구현하기">7) <code>_missing_</code> 메서드로 커스텀 룩업 구현하기</h3>
<pre><code class="language-python">from enum import Enum

class Color(Enum):
    RED = &quot;red&quot;
    GREEN = &quot;green&quot;
    BLUE = &quot;blue&quot;

Color(&quot;RED&quot;)  # ValueError: &#39;RED&#39; is not a valid Color</code></pre>
<ul>
<li>사실 &quot;RED&quot; 는 존재하는 것 (&quot;red&quot;) 이다. 이런 경우, <code>Enum</code> 에 정의되지 않은 값이 주어졌을 때의 처리 방식을 커스터마이징 할 수 있다. 바로 <code>_missing_</code> 를 통해서!</li>
</ul>
<pre><code class="language-python">class CaseInsensitiveEnum(str, Enum):
    @classmethod
    def _missing_(cls, value):
        if isinstance(value, str):
            # 대소문자 구분 없이 찾기
            for member in cls:
                if member.value.lower() == value.lower():
                    return member
        raise ValueError(f&quot;{value!r}는 {cls.__name__}의 유효한 값이 아닙니다&quot;)

class Color(CaseInsensitiveEnum):
    RED = &quot;red&quot;
    GREEN = &quot;green&quot;
    BLUE = &quot;blue&quot;

# 대소문자 구분 없이 값 찾기
assert Color(&quot;RED&quot;) == Color(&quot;red&quot;) == Color.RED</code></pre>
<ul>
<li><p>이렇게 하면 <code>&quot;RED&quot;, &quot;Red&quot;, &quot;red&quot;</code> 등 다양한 형태의 문자열을 동일하게 인식하여 Color.RED로 매핑할 수 있다.</p>
</li>
<li><p>참고로 <code>_missing_</code> 에서 <code>return None</code> 을 하는 등의 행위 말고, <code>raise ValueError</code> 가 더 올바른 Enum 접근 방법이다!</p>
</li>
</ul>
<h3 id="8-__str__--__repr__-커스터마이징">8) <code>__str__</code> &amp; <code>__repr__</code> 커스터마이징</h3>
<ul>
<li>Enum의 출력 형식을 커스터마이징을 하는 경우가 꽤 있는데, 미리 아래와 같은 base enum 을 하나 만들어 두고, 이걸 상속 받는 방식도 나쁘지 않다!</li>
</ul>
<pre><code class="language-python">class FormattedEnum(Enum):
    def __str__(self):
        return f&quot;{self.name} ({self.value})&quot;

    def __repr__(self):
        return f&quot;{self.__class__.__name__}.{self.name}&quot;

class HttpStatus(FormattedEnum):
    OK = 200
    NOT_FOUND = 404
    ERROR = 500

print(HttpStatus.OK)        # &quot;OK (200)&quot;
print(repr(HttpStatus.OK))  # &quot;HttpStatus.OK&quot;</code></pre>
<ul>
<li>로깅에 많은 도움이 된다고 생각한다. 그러니 디버깅에서도 활용도가 높았다. </li>
</ul>
<h4 id="etc">ETC</h4>
<p>ps) 그 외 Enum 상속에 대한 것 (사실 예시에서 바로 보임), <code>Enum.__members__</code> 로 속성 값 딕셔너리로 다 가져오는 것, 함수형으로 Enum 동적 생성하기 등이 있다. </p>
<p>ps) 더욱이 <code>@property</code> 는 &quot;불변 상수 / 메타데이터&quot; 가 목적인 <code>Enum</code> 에 잘 안맞는다고 생각한다. 잘못 사용하면, &quot;로직&quot; 이 추가되는 side effect 가 다분히 존재한다고 생각한다!</p>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://peps.python.org/pep-0435/">https://peps.python.org/pep-0435/</a></li>
<li><a href="https://peps.python.org/pep-0484/">https://peps.python.org/pep-0484/</a></li>
<li><a href="https://peps.python.org/pep-0557/">https://peps.python.org/pep-0557/</a></li>
<li><a href="https://peps.python.org/pep-0586/">https://peps.python.org/pep-0586/</a></li>
<li><a href="https://docs.python.org/3/library/dataclasses.html#dataclasses.field">https://docs.python.org/3/library/dataclasses.html#dataclasses.field</a></li>
<li><a href="https://docs.pydantic.dev/latest/concepts/dataclasses/">https://docs.pydantic.dev/latest/concepts/dataclasses/</a></li>
<li><a href="https://stackoverflow.com/questions/472000/usage-of-slots">https://stackoverflow.com/questions/472000/usage-of-slots</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] 그림으로 배우는 도커 - 스즈키 료]]></title>
            <link>https://velog.io/@qlgks1/%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4</link>
            <guid>https://velog.io/@qlgks1/%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4</guid>
            <pubDate>Fri, 25 Apr 2025 04:31:37 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="그림으로-배우는-도커">그림으로 배우는 도커</h1>
<blockquote>
<p>스즈키 료: 별명은 호게 상. 어쩌다 들어간 대학의 정보통신 계열 학과에서 프로그래밍을 접한 후 정보통신 분야에 빠져들었다. 2012년 모 전자 메이커 대기업에 취직해서 백엔드 엔지니어로서 ISP 서비스 개발에 종사했다. 2021년 미라이토디자인으로 이직, 현재는 Zenn(엔지니어 정보 공유 커뮤니티)에 투고하거나 회사 유튜브 채널에 진지한 동영상이나 그렇지 못한 동영상을 공개하고 있다(?)고 한다. (자료를 이 이상으로 찾지 못했다..)</p>
</blockquote>
<p><strong><em><a href="http://hanbit.co.kr/src/11347">🔥 책 예제 소스</a></em></strong> // <a href="https://books.google.co.kr/books/about/%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C_%EB%B0%B0%EC%9A%B0%EB%8A%94_%EB%8F%84%EC%BB%A4.html?id=qWVMEQAAQBAJ&amp;redir_esc=y">구글 도서, 책 미리 보기</a> // <a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B6249658359">한빛 책 링크</a></p>
<h2 id="리뷰">리뷰</h2>
<p><strong><code>『그림으로 배우는 도커』</code></strong> 는 총 7부로 구성되어 있으며, 1부부터 5부까지는 도커의 기본 개념과 명령어, Dockerfile 작성법까지 점진적으로 설명하고, 6부에서는 실무에 가까운 컨테이너 활용 및 복합 이미지 구축을 다루며, 7부에서는 실제 운영 환경에서 마주치는 디버깅, 트러블슈팅, 그리고 환경 구성 노하우까지 전반적으로 포괄하고 있다.</p>
<p><strong>*<span style="color: rgb(99 148 255);">이 책의 가장 큰 장점은 단연 &quot;그림&quot; 이다!!(책 이름 값이 쩔어요~)</span>*</strong></p>
<p>단순한 시각 보조 수준을 넘어, 도커의 내부 동작과 추상적 개념들을 직관적으로 이해하게 해주는 데 압도적으로 효과적이다. 도커를 처음 접했던 시절, <a href="https://www.youtube.com/watch?v=3Kn6_b-1mK4">유튜브 영상 하나</a> 와 여기저기 흩어진 문서들로 간신히 image 하나를 만들어보고 감탄했던 기억이 있다. 외장하드에 우분투를 넣고 들고 다니며 겨우 컨테이너 하나 띄워봤던 그 어설픈 시작과 비교하면, 이 책은 입문자에게 너무나 친절하고, 중급자에게는 뼈대가 정리되는 경험을 선사한다.</p>
<p>특히 이 책은 <strong>&#39;당장 도커를 다뤄야 하는 사람&#39; 에게 실질적인 도움이 되는 책</strong> 이다. 시스템의 깊은 구조나 리눅스 커널 수준에서 컨테이너의 작동 원리를 파고들기보다는, 도커를 어떻게 사용할 수 있는지를 실습 중심으로 풀어낸다. <code>container</code>, <code>image</code>, <code>Dockerfile</code>, 그리고 <code>docker compose</code> 까지, &quot;순차적&quot; 으로 기능을 확장해 나가며 자연스럽게 독자가 불편함을 느끼고 그 불편함을 해결하는 흐름으로 구성되어 있다.</p>
<p><strong><em>5부까지의 실습</em></strong> 은 각각의 도커 명령어를 하나하나 직접 실행하면서 기본기를 다지고, 이후 <code>Dockerfile</code> 을 중심으로 필요한 설정을 직접 추가해가며 컨테이너를 구성한다. 이 과정을 통해 단순 실행에서 끝나는 것이 아니라 <strong>&#39;왜 이렇게 구성해야 하는가&#39;</strong> 를 되짚게 되고, 최종적으로는 웹 메일함 프로젝트를 compose 파일 하나로 통합 실행하면서 도커를 실무에 도입할 수 있는 감각을 얻게 된다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/d770b9f4-188b-4484-a079-2ee5dc1fd610/image.png" alt=""></p>
<p>(출처: <a href="https://gngsn.tistory.com/129">https://gngsn.tistory.com/129</a>)</p>
<p>다만 이 책은 <strong>도커의 작동 원리 자체를 깊이 있게 이해하고자 하는 독자</strong>에게는 분명 아쉬움이 남는다. 예를 들어 리눅스 커널의 <code>cgroup</code>과 <code>namespace</code>가 어떻게 자원 격리와 프로세스 분리를 구현하는지, <code>overlay filesystem</code>이 어떻게 이미지 레이어를 효율적으로 구성하고 병합하는지에 대한 구조적 설명은 거의 등장하지 않는다. </p>
<p>또한 컨테이너의 PID 네임스페이스와 유저 네임스페이스, 네트워크 브리징 방식 등 <strong>도커의 겉모습이 아닌 &#39;안쪽 구조&#39;를 궁금해하는 독자</strong>라면, 이 책의 실습 위주 구성은 다소 단조롭게 느껴질 수 있다. 실습 중심의 빠른 흐름은 입문자에게는 유익하지만, 개념과 구조를 충분히 곱씹으며 이해하고 싶은 독자에게는 <strong>설명이 생략되거나, 추상적인 레이어에서 멈춘다는 점</strong>이 아쉽다.</p>
<p>개념적 깊이보다는 실용적 완성도가 돋보이는 책이며, 현업 개발자라면 책장 한켠에 두고 필요할 때마다 꺼내보게 될, 그런 실전형 책인 것 같다.</p>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<h3 id="1부-가상화와-도커-기본-지식">1부 가상화와 도커 기본 지식</h3>
<p>(1장 ~ 4장)</p>
<p>가상화란, 소프트웨어로 하드웨어 자원을 추상화하여 마치 물리적인 머신처럼 보이도록 하는 기술이다. 이를 통해 하나의 물리 머신에서 여러 개의 가상 머신(VM)을 운용할 수 있고, 각각의 VM은 자체 OS 및 서비스를 운영하므로 충돌 없이 다중 서비스를 구축하는 데에 유리하다.</p>
<p>가상화의 방식은 크게 세 가지로 나뉜다.</p>
<ol>
<li><p><strong>호스트형 가상화</strong>: 호스트 OS 위에 가상화 소프트웨어(VirtualBox 등)를 설치해 게스트 OS를 가동한다. 이 구조는 하드웨어 접근 시 반드시 호스트 OS를 경유해야 하므로 성능 저하가 발생할 수 있다.</p>
</li>
<li><p><strong>하이퍼바이저형 가상화</strong>: 하드웨어 위에 직접 하이퍼바이저를 설치하고 그 위에 게스트 OS들이 가동되는 구조이다. 별도의 호스트 OS가 존재하지 않아 성능 면에서 유리하다.</p>
</li>
<li><p><strong>컨테이너형 가상화</strong>: 도커와 같은 도구로 애플리케이션을 컨테이너 단위로 격리한다. 게스트 OS가 없으며, 커널은 호스트와 공유한다. 즉, 커널 수준의 격리를 통해 VM보다 가볍고 빠른 환경 제공이 가능하다.</p>
</li>
</ol>
<p>컨테이너형은 리눅스 커널에 의존하기 때문에, 리눅스 기반이 아닌 환경에서는 리눅스 커널을 별도로 사용해야 한다. 또한 호스트 머신의 CPU 아키텍처(예: amd64, arm64)는 컨테이너의 실행에도 영향을 미친다. 예를 들어 애플 실리콘(macOS/arm64)에서는 해당 아키텍처를 지원하는 이미지를 사용해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f1eeefae-41a8-4cff-9b36-50c4cfbc76fa/image.png" alt=""></p>
<h4 id="도커-구성-요소">도커 구성 요소</h4>
<p>도커의 핵심 구성은 다음 세 가지이다.</p>
<ol>
<li><strong>도커 데몬(dockerd)</strong>: 백그라운드에서 동작하며 이미지 빌드, 컨테이너 생성 및 관리를 담당. (실제 도커 핵심 엔진 로직)</li>
<li><strong>도커 CLI</strong>: 사용자가 명령어를 입력하는 인터페이스</li>
<li><strong>도커 API</strong>: CLI나 다른 도구가 데몬과 통신할 수 있도록 하는 API</li>
</ol>
<p>이러한 구조 덕분에 CLI나 GUI(도커 데스크탑 등)는 결국 API를 호출하는 방식으로 동일한 기능을 수행한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/71f958be-4966-4778-b1da-d890ddd3f842/image.png" alt=""></p>
<p>CLI 클라이언트를 위한 명령어 모음세트가 결국 <code>docker compose</code> (<code>.yaml</code>) 과 같다고 이해하면 된다. </p>
<p>도커 허브(Docker Hub)는 도커 이미지의 저장소로, GitHub처럼 버전 관리가 가능하고, 동시에 패키지 매니저와 같은 역할도 수행한다. 사용자는 이를 통해 공식 이미지나 개인 이미지 저장 및 배포가 가능하다.</p>
<p>컨테이너 및 이미지 규격은 <strong>OCI(Open Container Initiative)</strong> 라는 비영리 단체에서 정의하고 있으며, 이는 도커 외에도 Podman 등 다양한 도구들이 이 표준을 따르게 해준다.</p>
<p>도커는 리눅스의 핵심 기능을 조합하여 컨테이너를 구현하고 있으며, 그 핵심은 다음 세 가지이다.</p>
<ol>
<li><strong>namespace</strong>: PID, mount, network 등 시스템 자원을 격리</li>
<li><strong>cgroups</strong>: 자원(CPU, memory 등) 사용 제한</li>
<li><strong>chroot</strong>: 루트 디렉토리 변경으로 파일 시스템 격리</li>
</ol>
<p>이로 인해 각 컨테이너는 독립된 PID 1(최상위 프로세스)을 가지며, 이는 호스트와 충돌하지 않는다. 컨테이너 내부에서 <code>ps</code> 명령어를 입력하면 자신의 PID 1만 보이는 것도 이 때문이다.</p>
<p>더 depth 있는 커널에 대한 정보는 <a href="https://amsekharkernel.blogspot.com/2016/11/what-is-linux-namespace-cgroups.html">https://amsekharkernel.blogspot.com/2016/11/what-is-linux-namespace-cgroups.html</a> 와 Linux Kernel Documentation 의 <a href="https://docs.kernel.org/admin-guide/namespaces/index.html">https://docs.kernel.org/admin-guide/namespaces/index.html</a>, <a href="https://docs.kernel.org/admin-guide/cgroup-v2.html">https://docs.kernel.org/admin-guide/cgroup-v2.html</a> 를 추천한다. </p>
<h4 id="이미지">이미지</h4>
<p>이미지는 컨테이너 실행에 필요한 실행 파일, 라이브러리, 설정 등을 여러 개의 <strong>레이어(layer)</strong> 로 구성한다. 각 레이어는 <strong>tar 아카이브 파일</strong>이며, 불변 속성을 갖는다. 여러 이미지가 공통 레이어를 공유할 수 있어 저장 공간 및 네트워크 사용을 최적화할 수 있다.</p>
<p><code>Dockerfile</code> 은 이미지를 만들기 위한 설정 파일로, 새로운 레이어를 추가하는 역할을 한다. </p>
<h4 id="도커-명령어-기초-지식">도커 명령어 기초 지식</h4>
<p>초기에는 모든 명령어가 <code>docker</code> 접두사를 사용했지만, 명령어가 너무 많아지면서 v1.13 이후부터 <code>docker container</code>, <code>docker image</code>, <code>docker network</code> 등으로 분기되었다. 이로 인해 명령어 체계가 좀 더 직관적으로 구성되었다.</p>
<p><code>docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]</code></p>
<p>특히 4.6장 명령어 치트 시트, 정리가 진짜 잘 되어 있다. sub 명령어 묶음과 <code>container</code> 의 <code>status</code> 에 따라서 flow chart 처럼 정리된 것을 보고 해당 장표 찢어서 어디 벽에 붙여 놓을까 했다. ㅎ</p>
<h3 id="2부-도커-컨테이너-활용법">2부 도커 컨테이너 활용법</h3>
<p>(5장 ~ 11장)</p>
<h4 id="컨테이너는-pid1과-함께-탄생하고-죽는다">컨테이너는 PID1과 함께 탄생하고 죽는다</h4>
<p><strong><em>도커 컨테이너 내부에서 실행되는 첫 번째 프로세스는 Linux 시스템 상의 PID1에 해당하며, 이 프로세스가 종료되면 컨테이너 자체가 종료된다.</em></strong> </p>
<p>이는 컨테이너가 본질적으로 하나의 단일 프로세스를 중심으로 동작하는 경량 VM이라는 점을 극명하게 보여주는 설계다. 그래서 컨테이너 정지는 곧 PID1 프로세스의 종료를 의미하고, 그 이후 컨테이너는 <code>stop</code> 명령으로 정지시키거나 <code>rm</code> 명령으로 삭제하는 방식으로 관리된다. (필자는 stop 상태 그대로 둔 적이 딱히 없다고 한다! 물론 나도...)</p>
<p>앞으로 명령어의 상세한 내용은 <a href="https://docs.docker.com/reference/">도커레퍼런스</a> 를 적극 참조하자! </p>
<h4 id="실행run과-제거rm는-컨테이너-조작의-기본">실행(run)과 제거(rm)는 컨테이너 조작의 기본</h4>
<p><code>docker container run ubuntu whoami</code> 처럼 이미지를 기반으로 한 번만 실행하고 끝내는 방식도 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/106385b3-286b-4fdd-96f5-30483d17b3b2/image.png" alt=""></p>
<p><del>(그림 설명이 진짜.. 너무 친절하다!!!)</del></p>
<p>이때 <code>whoami</code> 는 명령어 이후 인자는 모두 <code>ARG...</code>로 처리된다. 실행 후 자동 삭제는 <code>--rm</code> 옵션으로 가능하다. (<strong>--name 과 --rm 이 조합이 좋음</strong>)</p>
<p><code>--name</code> 으로 컨테이너 이름을 지정하면 관리가 쉬워진다. <code>docker run --rm --name test ubuntu echo hello</code> 같은 조합은 일회성 테스트에 유용하다.</p>
<h4 id="대화형-셸-실행을-위한-옵션">대화형 셸 실행을 위한 옵션</h4>
<p><code>--interactive --tty</code>, 줄여서 <code>-it</code></p>
<ul>
<li><code>--interactive</code> 는 컨테이너의 표준 입력(stdin) 을 열어둔 상태로 유지하여, 사용자가 입력을 계속 보낼 수 있도록 하는 것</li>
<li><code>--tty</code> 는 가상 터미널(TTY) 을 할당한다. 즉, 컨테이너에서 실행되는 프로세스가 터미널인 것처럼 동작할 수 있게 하는 것이다!</li>
</ul>
<p><code>docker container run -it --rm python python3</code> 형태로 Python REPL 을 바로 열 수 있다. bash 셸을 실행할 경우 <code>docker run -it ubuntu bash</code> 와 같이 활용이 가능하다.</p>
<h4 id="포트-매핑으로-웹서버-외부-공개">포트 매핑으로 웹서버 외부 공개</h4>
<ul>
<li><code>docker container run --publish 8080:80 nginx</code> 와 같이 <code>--publish</code> 또는 <code>-p</code> 옵션으로 호스트와 컨테이너 간의 포트 바인딩이 가능하다.</li>
<li>이는 웹 개발이나 테스트 시 로컬에서 접속할 수 있는 환경을 만들 때 기본적인 패턴이다!</li>
</ul>
<h4 id="db-서버는-환경-변수-세팅이-필수">DB 서버는 환경 변수 세팅이 필수</h4>
<p><code>MySQL</code> 을 예로 들면 아래와 같이 &quot;필수 환경 변수 값&quot; 들이 있다. </p>
<pre><code class="language-bash">docker container run \
--name db \
--rm \
--env MYSQL_ROOT_PASSWORD=secret \
--publish 3306:3306 \
mysql</code></pre>
<p>대부분의 공식 이미지들은 필수 환경 변수들이 명시되어 있으며, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD 등 추가적인 설정도 가능하다.</p>
<p>도커 허브에 공식 이미지마다 환경 변수 문서가 잘 정리되어 있다!</p>
<h4 id="백그라운드-실행은-detach">백그라운드 실행은 detach</h4>
<p><code>--detach</code> 또는 <code>-d</code> 옵션으로 실행하면 컨테이너는 백그라운드에서 실행되고, 사용자 셸은 즉시 반환된다. (사실 이거 detach 가 아니라 daemon 인 줄 알았다.. attach 가 있고, 이 행위와 반대의 detach 개념이라는 점..!) -&gt; 정확하게는 &quot;표준 입출력을 분리&quot; 하는 옵션이다. </p>
<p><code>docker run -d ...</code> 는 nginx, mysql 같이 지속적으로 동작해야 하는 서버성 컨테이너에 필수적인 실행 방식이다. </p>
<h4 id="컨테이너-출력-확인-logs">컨테이너 출력 확인 logs</h4>
<p><code>docker container logs [OPTIONS] CONTAINER</code>, <code>--follow (-f)</code> 옵션과 함께 사용하면 실시간 로그 확인이 가능하다.</p>
<h4 id="실행-중인-컨테이너에서-명령어-실행">실행 중인 컨테이너에서 명령어 실행</h4>
<ol>
<li><code>docker container exec [OPTIONS] CONTAINER COMMAND [ARG...]</code></li>
<li><code>docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]</code></li>
</ol>
<p><code>docker container exec -it db bash</code>, <code>exec</code> 는 기존에 가동 중인 컨테이너의 내부에서 별도의 프로세스를 실행하는 방식이다. </p>
<p>이미 실행 중인 컨테이너에 접속해서 새로운 명령어를 실행할 수 있는 방법이 <code>exec</code> 이다. <code>run</code> 은 새로운 컨테이너를 만드는 것이고, <code>exec</code> 은 기존에 살아 있는 컨테이너에 명령을 추가로 집어넣는 것이다. </p>
<p>예를 들어 <code>docker container exec -it db bash</code> 와 같은 명령어는 마치 SSH 를 사용하는 것처럼 컨테이너 내부에 진입하는 효과를 준다. 하지만 이는 프로세스 단위의 실행일 뿐, 실제 SSH 가 아니며, VM 과는 다르게 컨테이너는 전체 운영체제를 제공하지 않기 때문에 혼동하면 안 된다! (이는 &quot;호스팅형 가상화&quot; 와 &quot;컨테이너형 가상화&quot; 차이를 알아야 한다는 것이다!)</p>
<h3 id="3부-도커-이미지-활용법">3부 도커 이미지 활용법</h3>
<p>(12장 ~ 15장) </p>
<h4 id="이미지의-기본-내용">이미지의 기본 내용</h4>
<p>도커 이미지는 여러 레이어(layer)로 구성되어 있으며, 최상단의 <strong>쓰기 가능 레이어(writable layer)</strong> 외에는 모두 <strong>읽기 전용(read-only layer)</strong>이다. 이미지를 구성하는 각 레이어는 설치나 설정 등 시스템 상태의 변화를 담고 있으며, 최종적으로 컨테이너를 만들 때 이 레이어들 위에 쓰기 가능한 레이어가 덧붙여진다.</p>
<ul>
<li><code>메타데이터(metadata)</code>는 이미지 전체의 속성으로 환경 변수, 기본 실행 명령 등을 포함한다.</li>
<li>이미지 명명 방식은 <code>[HOST[:PORT_NUMBER]/][NAMESPACE/]REPOSITORY[:TAG]</code> 형태다.<ul>
<li><code>HOST</code> 생략 시 기본값은 <code>docker.io</code></li>
<li><code>NAMESPACE</code> 생략 시 <code>library</code> 사용 (공식 이미지)</li>
<li><code>TAG</code> 생략 시 <code>latest</code>가 기본값이나, 이는 이미지의 최신 상태가 예기치 않게 변경될 수 있으므로 주의가 필요하다.</li>
</ul>
</li>
</ul>
<p>도커 이미지 관련 주요 명령어는 <code>docker image --help</code>를 통해 확인 가능하며, <code>--help</code> 옵션을 적극적으로 사용하는게 많은 도움이 된다!</p>
<pre><code class="language-shell">❯ docker image --help
Usage:  docker image COMMAND

Manage images

Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Download an image from a registry
  push        Upload an image to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run &#39;docker image COMMAND --help&#39; for more information on a command.</code></pre>
<h4 id="이미지-기본-조작">이미지 기본 조작</h4>
<ol>
<li><code>docker image ls [OPTIONS] [REPOSITORY[:TAG]]</code> : 호스트 머신에 존재하는 이미지 목록을 확인</li>
<li><code>docker image pull [OPTIONS] NAME[:TAG|@DIGEST]</code> : 외부 레지스트리에서 이미지 다운로드, <code>docker container run</code> 명령어에서도 자동 수행된다!</li>
<li><code>docker image inspect [OPTIONS] IMAGE [IMAGE...]</code> : 이미지의 상세 메타데이터를 JSON 형식으로 출력<ul>
<li>특히 <code>RepoTags</code>, <code>Config.Env</code>, <code>Config.Cmd</code> 항목을 확인하면 유용하다!</li>
</ul>
</li>
</ol>
<p>이미지 조작 명령어는 디버깅과 환경 구성 시의 핵심 도구이며, JSON으로 나오는 상세 정보를 통해 이미지 구성과 환경 변수, 실행 커맨드를 사전에 파악할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/789f336a-ed76-4683-be50-46727fd64e4e/image.png" alt=""></p>
<h4 id="vi를-설치한-우분투-이미지를-작성하고-공유하기">vi를 설치한 우분투 이미지를 작성하고 공유하기</h4>
<ol>
<li><code>docker container run --name myubuntu --interactive --tty ubuntu:22.04 bash</code></li>
<li><code>apt update &amp; apt install vim &amp; which vi</code> -&gt; 그냥 ubuntu 이미지에서 which vi 하면 존재하지 않는다!, 이 순서로 myubuntu 에 vim 을 설치하는 것!</li>
<li>컨테이너에서 이미지 작성, <code>docker container commit [OPTIONS] CONTAINER [REPOSITORY:[TAG]]</code>  을 통해 이미지를 새로 만들어 보자!</li>
</ol>
<pre><code class="language-shell">❯ docker container commit myubuntu vi-ubuntu:commit
sha256:2204542f690c950e74aef2c8d2af737b1f9edb06d3edbb6eb1294a92e718ff62

❯ docker image ls vi-ubuntu
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
vi-ubuntu    commit    2204542f690c   10 seconds ago   185MB

❯ docker container run --rm vi-ubuntu:commit which vi
/usr/bin/vi</code></pre>
<p><code>commit</code> 은 컨테이너에서 이미지를 만들지만, 만들어진 이미지는 git 관리 또는 파일 저장소 업로드 불가능하다. 그래서 <code>export</code> 를 활용한다.</p>
<p>이러면 <code>tar</code> 아카이브 파일로 추출이 가능하며, 이후 <code>tar</code> 를 기반으로 image 처럼 읽어 올 수 있다! 그럼 두 명령어는 어떤 차이가 있을까?!</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/38cf9a24-7e53-4ab0-b88b-ca7647310bf2/image.png" alt=""></p>
<p>container 중심 말고 당연히 &quot;IMAGE&quot; 중심으로 <code>tar</code> 아카이브파일을 만들고 불러오는 것도 가능하다. </p>
<p><code>docker image save [OPTIONS] IMAGE [IMAGE...] (다수 이미지 지정 가능하다는 의미)</code> 로 이미지를 tar 아카이브 파일로 작성 가능하며, <code>load</code> 명령어로 다시 이미지화 가능하다! <strong><em>왜씀? 이미지 백업, 이관 &amp; 이동에 활용!</em></strong></p>
<h3 id="4부-도커파일-활용법">4부 도커파일 활용법</h3>
<p>(16장 ~ 19장)</p>
<p>위에서 살펴본 &quot;tar&quot; 파일 중심으로는 이미지 내용을 알 수 없다. 그렇다고 모르는 tar 를 합쳐서, 하나 하나의 layer 로 활용해서 쓰기에도 쉽지 않고 버전관리도 어렵다. 하지만 도커의 &quot;이미지&quot; 자체를 만들일은 굉장히 많다. 그때 도커파일(Dockerfile)을 사용해야 한다. </p>
<p>Dockerfile 은 컨테이너 환경을 코드로 명확하게 정의하고, 반복 가능한 이미지를 만들 수 있게 해주는 강력한 도구다. 기존엔 컨테이너를 만들고 설정하고 저장하는 흐름이 <code>container run → exec → commit</code> 으로 다소 수동적이었다면, 도커파일은 그 과정을 완전히 코드화할 수 있게 해준다. </p>
<p>특히 이미지를 tar로 만들면 내부 내용이 추상화되어 파악하기 어렵기 때문에, 어떤 이미지든 그 빌드 과정을 명시적으로 남길 수 있는 도커파일은 매우 중요하다.</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>FROM</td>
<td>베이스 이미지 지정</td>
<td>FROM ubuntu:22.04</td>
</tr>
<tr>
<td>ARG</td>
<td>빌드 시점 변수</td>
<td>ARG VERSION=1.0</td>
</tr>
<tr>
<td>ENV</td>
<td>환경 변수 설정</td>
<td>ENV NODE_ENV=production</td>
</tr>
<tr>
<td>LABEL</td>
<td>이미지 메타데이터</td>
<td>LABEL maintainer=&quot;<a href="mailto:you@example.com">you@example.com</a>&quot;</td>
</tr>
<tr>
<td>WORKDIR</td>
<td>작업 디렉토리 설정</td>
<td>WORKDIR /app</td>
</tr>
<tr>
<td>COPY</td>
<td>로컬 파일 복사</td>
<td>COPY . /app</td>
</tr>
<tr>
<td>ADD</td>
<td>파일 복사, URL 다운로드, 압축 해제</td>
<td>ADD <a href="https://example.com/file.tar.gz">https://example.com/file.tar.gz</a> /app/</td>
</tr>
<tr>
<td>RUN</td>
<td>빌드 시 명령어 실행</td>
<td>RUN apt-get update &amp;&amp; apt-get install -y curl</td>
</tr>
<tr>
<td>CMD</td>
<td>기본 실행 명령어</td>
<td>CMD [&quot;npm&quot;, &quot;start&quot;]</td>
</tr>
<tr>
<td>ENTRYPOINT</td>
<td>컨테이너 실행 명령어</td>
<td>ENTRYPOINT [&quot;python3&quot;, &quot;app.py&quot;]</td>
</tr>
<tr>
<td>EXPOSE</td>
<td>수신 대기 포트</td>
<td>EXPOSE 8080</td>
</tr>
<tr>
<td>VOLUME</td>
<td>볼륨 마운트 디렉토리</td>
<td>VOLUME [&quot;/data&quot;]</td>
</tr>
<tr>
<td>USER</td>
<td>실행 사용자/UID 설정</td>
<td>USER appuser</td>
</tr>
<tr>
<td>HEALTHCHECK</td>
<td>상태 확인 명령어</td>
<td>HEALTHCHECK CMD curl --fail <a href="http://localhost:8080">http://localhost:8080</a></td>
</tr>
<tr>
<td>SHELL</td>
<td>기본 셸 지정</td>
<td>SHELL [&quot;/bin/bash&quot;, &quot;-c&quot;]</td>
</tr>
<tr>
<td>ONBUILD</td>
<td>파생 이미지용 명령어</td>
<td>ONBUILD COPY . /app/src</td>
</tr>
</tbody></table>
<p>이 도커파일 명령어들은 이미지가 만들어지는 과정을 &#39;계층별로&#39; 기록하는 역할을 한다. <code>RUN</code> 같은 명령은 실행 결과가 새로운 이미지 레이어로 저장되고, <code>ENV</code>, <code>LABEL</code>, <code>EXPOSE</code> 같은 명령은 이미지의 메타데이터로 저장된다. 도커 이미지가 결국 읽기 전용 레이어들의 조합이고, 도커파일은 그 조합의 &#39;조리법&#39;인 셈이다.</p>
<p>특히 <code>FROM</code>은 도커 이미지의 연쇄 구조를 만들어낸다. 하나의 이미지가 다른 이미지의 기반이 되고, 그 위에 또 다른 이미지가 올라가는 구조인데, 이건 마치 <code>Git</code>의 커밋 히스토리를 타고 올라가듯이 도커 이미지의 뿌리까지 따라갈 수 있다는 뜻이다. 도커 허브에서 이미지를 봤을 때, 어떤 베이스 이미지에서 파생되었는지 확인하는 것도 가능하다.</p>
<h4 id="실습-예시-요약">실습 예시 요약</h4>
<ol>
<li><p><strong>vi가 가능한 우분투 이미지</strong></p>
<ul>
<li><code>FROM ubuntu</code></li>
<li><code>RUN apt-get update &amp;&amp; apt-get install -y vim</code></li>
<li>단순하지만, 이미지 생성 과정을 명확히 기록할 수 있어 재현성과 이식성이 높아짐</li>
</ul>
</li>
<li><p><strong>시간대 설정 및 로그 출력이 설정된 MySQL 이미지</strong></p>
<ul>
<li><code>FROM mysql:latest</code></li>
<li><code>ENV TZ=Asia/Seoul</code></li>
<li><code>COPY ./my.cnf /etc/mysql/conf.d/</code></li>
<li><code>RUN echo &quot;설정된 시간대와 로그 출력을 위한 설정 포함&quot;</code></li>
<li>운영 환경에 필요한 세밀한 설정을 담을 수 있음</li>
</ul>
</li>
<li><p><strong>간단한 파이썬 웹 서버 이미지</strong></p>
<ul>
<li><code>FROM python:3.11</code></li>
<li><code>COPY ./app.py /app/app.py</code></li>
<li><code>CMD [&quot;python&quot;, &quot;/app/app.py&quot;]</code></li>
<li>단일 파일 기반의 웹 서비스도 도커를 통해 바로 배포 가능한 형태로 구성</li>
</ul>
</li>
</ol>
<h3 id="5부--7부-직접-보면-더-도움이-될-부분들">5부 ~ 7부, 직접 보면 더 도움이 될 부분들</h3>
<h4 id="5부-고급-도커-컨테이너-활용법">5부 고급 도커 컨테이너 활용법</h4>
<p>5부에서는 도커를 단순한 실행 도구가 아니라, <strong>서비스 환경을 제어하는 방법</strong>으로 확장하는 과정을 보여준다. 핵심은 두 가지다: <strong>볼륨</strong>과 <strong>네트워크</strong>.</p>
<ol>
<li><strong>볼륨</strong>: 컨테이너는 기본적으로 휘발성인데, <code>volume</code> 기능을 통해 <strong>데이터를 유지</strong>할 수 있다. <code>volume create</code>, <code>--mount</code> 옵션을 사용해 데이터를 보존하고, 컨테이너 재시작 시에도 동일한 상태를 유지하게 된다.<ul>
<li>바인드 마운트를 활용하면 <strong>호스트 머신의 디렉토리와 컨테이너 내부를 동기화</strong>할 수 있어, 로컬에서 코드나 설정 파일을 수정하면 바로 컨테이너에 반영된다. 실습에서는 루비 컨테이너에 로컬 스크립트를 실행하거나, MySQL 데이터의 지속성을 확보하는 방식으로 이를 체득하게 된다.</li>
<li><code>--volume</code> 과 <code>--mount</code> 의 커멘트 차이, 특히 볼륨 마운트와 바인드 마운트를 차이에 집중하는게 아주 좋았다. <del>개인적으로 mount 는 거의 써본적이 전무하다.</del></li>
</ul>
</li>
</ol>
<ol start="2">
<li><strong>네트워크</strong>: <code>docker network create</code>, <code>--network</code> 옵션을 사용해 여러 컨테이너 간의 통신을 설정할 수 있다. 기본 브릿지 네트워크 외에 독립 네트워크를 정의하고, <strong>PHP 컨테이너와 MySQL 컨테이너가 서로 통신</strong>할 수 있게 연결하는 구조는 실제 서비스 구성에서 매우 자주 활용된다.</li>
</ol>
<p>전체적으로 실습 위주이며, 단순한 실행 단계를 넘어서, <strong>컨테이너 간의 협업</strong>을 이해하게 해준다. 이 과정은 일종의 &quot;도커로 시스템 아키텍처를 설계하는 감각&quot;을 키우는 데 도움된다.</p>
<blockquote>
<p>아래는 개인적인 사견 및 정리</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/ddf478d1-064b-4240-9146-4621b9bfb205/image.png" alt=""></p>
<p>각 컨테이너는 <code>network namespace</code> 안에 있고(서로 격리된 네트워크 공간) 컨테이너마다 <strong>veth pair</strong> (<code>virtual ethernet pair</code>) 를 생성해서, 하나는 컨테이너 내부에, 다른 하나는 도커 브릿지 네트워크(<code>docker0</code>) 에 연결한다. 그래서 컨테이너 내부 통신 흐름은 아래와 같은 느낌이다.</p>
<pre><code>[컨테이너1] veth0 &lt;--&gt; [브릿지 docker0] &lt;--&gt; veth1 [컨테이너2]</code></pre><p>볼륨은 VFS 구조 하에 컨테이너의 mount namespace를 활용해 호스트 디렉토리 또는 독립된 볼륨을 연결하며, 핵심은 mount() syscall을 통해 실제 경로가 연결된다는 점이다.</p>
<ul>
<li><strong><a href="https://fuchsia.dev/fuchsia-src/concepts/starnix/architecture-of-the-starnix-vfs">Architecture of the Starnix VFS</a></strong></li>
<li><strong><a href="https://blockchain.dcwebmakers.com/2023/12/22/linux-os-file-system-architecture-and-management/">Linux File System Architecture and Management</a></strong></li>
</ul>
<h4 id="6부-웹서비스-개발-환경-구축">6부 웹서비스 개발 환경 구축</h4>
<p>6부는 개발자가 자주 마주치는 스택 구성 (PHP, MySQL, Mailpit 등)을 <strong>도커파일 기반으로 직접 만들어보고</strong>, <code>docker compose</code> 로 통합하는 구조를 다룬다. 핵심은 다음과 같다.</p>
<ul>
<li><strong>Dockerfile을 단계적으로 구성</strong>: 각 서비스별로 Dockerfile을 만들고, <code>COPY</code>, <code>RUN</code>, <code>ENV</code> 등을 활용해 설정값과 초기 상태를 명확히 한다.</li>
<li><strong>개별 컨테이너로 실습한 내용을 Compose로 통합</strong>: 
<code>compose.yaml</code>을 통해 의존성을 명시하고, 이름, 네트워크, 볼륨까지 정리한 하나의 선언적 환경 정의 파일로 완성된다.</li>
</ul>
<p>이 과정에서 중요한 포인트는 <strong>&quot;각 컨테이너가 배타적으로 동작하는 게 아니라, 서로 보완하며 작동한다&quot;</strong>는 개념이다. 웹 서버가 DB를 참조하고, 메일 서버가 로그를 수신하는 방식은 <strong>서비스 간의 연결성과 설정의 유연함</strong>을 도커로 어떻게 구성할 수 있는지를 체감할 수 있다.</p>
<p>특히 compose.yaml로 정리할 때의 구조화된 쾌감은 단순한 실습을 넘어선 실전 감각으로 이어진다.</p>
<h4 id="7부-운영-시-주의할-점과-트러블슈팅">7부 운영 시 주의할 점과 트러블슈팅</h4>
<p>7부는 도커를 현업에서 <strong>운영단에서 활용하는 시선</strong>을 제공한다.</p>
<ul>
<li>유료 플랜, 도커 계정과 같은 정책적 변화</li>
<li>애플 실리콘 맥에서 ISA 호환 이슈 대응</li>
<li>환경 변수와 <code>.dockerignore</code>, 여러 compose.yaml 파일 결합 활용</li>
<li>디버깅: <code>docker inspect</code>, <code>logs</code>, <code>exec</code> 등을 통해 문제 상황을 좁혀가는 방식</li>
</ul>
<p>이 파트는 기술적인 팁 외에도, <strong>운영 중 마주칠 수 있는 현실적인 문제들을 예방하고 해결하는 방법론</strong>이 담겨 있다. 도커를 쓰면서 언젠가 반드시 맞닥뜨릴 상황들 — 퍼포먼스 문제, 아키텍처 호환성, 계정/정책 변화 등 — 에 대한 대비가 된다.</p>
<p>7부는 실습보다도, <strong>읽으면서 ‘현실 감각’을 얻는 장</strong>이다. 개인적으로 이 책에서 가장 유익했던 챕터이기도 하며, 도커를 단순히 실행 도구가 아닌 <strong>서비스 운영 플랫폼</strong>으로 인식하게 되는 계기였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[책 리뷰] NLP 와 LLM 실전 가이드 - 리오르 가지트, 메이삼 가파리]]></title>
            <link>https://velog.io/@qlgks1/NLP-%EC%99%80-LLM-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@qlgks1/NLP-%EC%99%80-LLM-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sat, 29 Mar 2025 14:22:30 GMT</pubDate>
            <description><![CDATA[<p><strong><em>[ &quot;한빛미디어 서평단 &lt;나는리뷰어다&gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot; ]</em></strong></p>
<h1 id="nlp-와-llm-실전-가이드">NLP 와 LLM 실전 가이드</h1>
<blockquote>
<p>리오르 가지트(Lior Gazit): 금융 분야 머신러닝 그룹의 수석 디렉터이자 신생 스타트업의 수석 머신러닝 자문위원으로서 업계에서 풍부한 지식과 경험을 바탕으로 존경받는 리더이다. <em><a href="https://www.linkedin.com/in/liorgazit/">링크드인</a></em></p>
</blockquote>
<blockquote>
<p>메이삼 가파리(Meysam Ghaffari): 현재 MSKCC에서 근무하며 의료 문제를 해결하기 위한 머신러닝과 자연어 처리 모델의 개발과 최적화에 전념하고 있다. 머신러닝 분야에서 9년 이상, 자연어 처리와 딥러닝 분야에서 4년 이상의 실무 경험이 있다. <em><a href="https://www.linkedin.com/in/meysam-ghaffari-ph-d-a2553088/">링크드인</a></em></p>
</blockquote>
<p><strong><em><a href="https://www.youtube.com/watch?v=xoJQ1Ce-QQ8&amp;list=PLaTc2c6yEwmrNTO7vWp50vRMvtwjrZ-Lr&amp;index=1">🔥 8 ~ 9 장을 위한 박조은(옮긴이) 님의 youtube 영상</a></em></strong></p>
<p><strong><em><a href="https://github.com/corazzon/Mastering-NLP-from-Foundations-to-LLMs?tab=readme-ov-file">🔥 실습 코드, 실습 레포</a></em></strong></p>
<h2 id="리뷰">리뷰</h2>
<p>진짜 공부하고 싶게 만드는 책이다. 흩어져있는 자연어의 A to Z 를 정석같이 일목요연하게 정리되어 있어서, 모든 챕터 하나 하나가 이정표 역할을 한다. 특히 NLP 입문자에 가까운 나에게 길 잃지말라고 계속 가이드를 준다. </p>
<p>각 챕터가 배경 지식을 하나씩 쌓아가는 형태고, 서술하는 방식이 &quot;장 소개&quot;, &quot;목차와 전체 내용&quot; 을 짚고 세부 내용으로 하나씩 진행하는게 &quot;자연어 처리&quot; 라는 분야의 official docs 를 읽는 것 같다. </p>
<h4 id="책은-주관적으로-크게-3개-섹터로-이뤄지는-것-같다">책은 (주관적으로) 크게 3개 섹터로 이뤄지는 것 같다.</h4>
<ol>
<li>ML을 위한 기초 지식, ML을 학습하기 위한 기존의 방법들 (자연어 처리 중심)</li>
<li>현 시점 SOTA, LLM 에 대한 얘기, 활용과 실습 중심</li>
<li>그리고 미래에 대한 전망</li>
</ol>
<p>초반부에서 다루는 선형대수, 확률, 통계는 실제로 머신러닝 실습을 하는 사람이라면 반드시 알고 있어야 하는 내용들이다. 다만 이 책은 자세한 설명을 일일이 파고드는 스타일이 아니라, 중요한 핵심 개념을 &quot;놓치지 않게&quot; 딱딱 짚어준다. 개인적으로는 이 부분이 정말 좋았다. 너무 깊게 들어가면 NLP 입문자 입장에서 부담스러울 수 있는데, 딱 적절한 수준에서 정리해준다.</p>
<p>CH3부터 CH6까지는 <strong>기존 머신러닝 기반 자연어 처리</strong>의 전통적인 구조를 설명한다. 전처리, 피처 엔지니어링, 분류, 모델 평가로 이어지는 이 흐름이 아주 교과서적이다.</p>
<p>특히 좋았던 점은 많은 책들이 대충 넘기는 <strong>텍스트 전처리</strong>를 이 책은 굉장히 다양한 방법론과 함께 자세히 짚고 간다는 것이다. 소문자화, 특수문자 제거, 정규표현식, 불용어 제거, 표제어 추출, 개체명 인식(NER), 품사 태깅(POS tagging) 등이 각각 어떤 맥락에서 중요하고, 어떤 선택지를 고려할 수 있는지 다룬다. 단순한 예제 코드가 아니라 실무적인 감각으로 설명한다.</p>
<p>그리고 CH6부터는 트랜스포머 기반 딥러닝 언어 모델로 넘어가는데, 여기서부터는 NLP 입장에서의 최신 기술 스택에 대한 안내서처럼 구성된다. RNN, CNN을 넘어서 왜 트랜스포머가 등장했는지, BERT와 같은 모델이 어떻게 문맥을 이해하고 분류 성능을 높일 수 있는지 서술한다.</p>
<p>개인적으로는 여기까지 읽고 &quot;실전 가이드 맞네&quot; 라는 생각이 들었다. 설명은 교과서 같지만, 적용은 실전에 가깝게 잘 정돈되어 있다.</p>
<p>CH7부터 CH9까지는 GPT, BERT, LLaMA, PaLM 같은 대규모 언어 모델의 구조와 트렌드, 활용 사례를 다룬다. 특히 OpenAI의 <strong>RLHF</strong> 접근 방법을 풀어낸 부분은 실전에서 LLM을 접했던 입장에서 더 인상 깊었다. (<del>최근에 읽었던 <strong>&quot;GPT API를 활용한 인공지능 앱 개발&quot;</strong> 책의 내용을 응축해둔 듯한 느낌</del>)</p>
<p>그리고 10장부터는 자연어 처리와 LLM의 <strong>현재와 미래</strong>에 대한 이야기다. 이 부분은 책의 인문학적인 성격도 살짝 느껴졌다. 무어의 법칙부터 시작해 GPU, TPU, 양자컴퓨팅, LLMOps, 임베딩 구조, 백터DB, RAG의 중요성까지 기술적인 내용과 산업 트렌드를 폭넓게 다룬다. 여기서 인상 깊었던 건 단순히 기술의 발전만을 다룬 것이 아니라, 아래 흐름으로 으로 알려줘서, 지금 그 흐름 위에 있다는 걸 다시 인지 할 수 있었다.</p>
<ul>
<li>산업 전반에 어떤 변화가 일어났는지</li>
<li>왜 CAIO(Chief AI Officer) 같은 포지션이 등장했는지</li>
<li>다중 에이전트, AutoGen, LFM(대규모 기초모델) 같은 흐름이 왜 주목받는지</li>
</ul>
<p><strong>딥러닝 기반 자연어 처리</strong>의 전통적인 접근부터 LLM, RAG, 그리고 미래까지 이어지는 흐름 속에서 <strong>지금 어디에 서 있는지, 앞으로 뭘 알아야 할지</strong>를 알려주는 책이다.</p>
<blockquote>
<p>현대는 (어떤 형태의) 비즈니스 이전에 테크가 존재할 수는 없다고 생각하며 기술 그 자체로는 결코 목적이 될 수 없다고 생각한다.  우리는 언제나 &#39;무엇을 이루고자 하는가&#39;라는 목적에서 출발해야 한다. 그리고 그 목적을 이루기 위해 가장 날카롭고 적절한 수단과 도구를 선택하는 것이다. 이 책은 바로 그런 관점에서, 다양한 도구와 방법론들을 마치 잘 정리된 도구 상점처럼 펼쳐 보인다.</p>
</blockquote>
<hr>
<h2 id="목차별-리뷰">목차별 리뷰</h2>
<blockquote>
<p>모든 목차를 다루기엔 책에서 다루는 범위와 영역이 너무 거대해서 할 수 없다..! 상대적으로 유연한 초반부만 좀 세부적으로 정리하고, 후반부는 철저하게 리뷰 중심이다.
<em>(PS. 참고로 저작권을 위해 모든 내용은 당연히 있으면 안된다.)</em></p>
</blockquote>
<h3 id="ch1-자연어-처리-개요-살펴보기">CH1. 자연어 처리 개요 살펴보기</h3>
<p>이 장에서는 자연어 처리의 역사와 접근 방식을 간결하게 조망한다. 초창기 튜링 테스트를 시작으로, 50~60년대 룰 기반 처리 방식과 <strong><em><a href="https://m.blog.naver.com/newheater/221763031420">조지타운 실험 사례</a></em></strong> 가 소개된다. 이후 70년대까지는 구조화된 접근과 개념 기반 온톨로지 도입을 통해 시스템이 조금씩 발전했다.</p>
<p>80년대 후반부터는 대규모 말뭉치를 활용한 통계적 접근법이 등장하면서 자연어 처리 분야가 실질적인 전환점을 맞이했다. 이 시기를 기점으로 머신러닝 기반의 자연어 처리가 본격화되고, 21세기 이후 인터넷과 함께 폭발적으로 증가한 데이터는 비지도, 준지도 학습 알고리즘의 발전을 촉진시켰다. 2010년대 이후 신경망 기반 딥러닝 기술의 등장은 자연어 처리의 패러다임을 또 한 번 바꾸었다.</p>
<p>자연어 처리가 단순 규칙 기반의 기술에서 데이터 기반 학습 기술로 진화해왔음을 시대별 사례를 통해 자연스럽게 보여준다. 최신 기술을 이해하는 데 앞서, 지금 우리가 딥러닝을 자연스럽게 사용하는 배경이 어떤 역사와 전환을 거쳤는지 알 수 있는 장이었다.</p>
<h4 id="자연어-처리-기본-접근-전략">자연어 처리 기본 접근 전략</h4>
<ol>
<li><p>불용어 제거
문장의 전반적인 의미에 큰 영향을 주지 않는 단어들을 제거하는 과정이다. 그러나 상황에 따라 불용어도 중요한 의미를 가질 수 있어 무조건 제거하는 것은 위험할 수 있다. 문맥을 고려한 판단이 필요하다는 점이 강조된다.</p>
</li>
<li><p>어간 추출과 표제어 추출
단어의 시제나 복수형, 파생 접사 등을 제거해 어근 형태로 축소하는 방법이다. 어간 추출은 규칙 기반으로 잘라내는 반면, 표제어 추출은 문맥에 따라 사전 기반으로 판단한다. 두 방식 모두 단어 간 유사성을 높이는 데 쓰이지만, 역시 맥락에 따라 선택이 달라질 수 있다.</p>
</li>
<li><p>데이터 정규화와 정제
단어 단위의 전처리 외에도 텍스트 전반의 구조를 정리하는 과정이다. 특수문자 제거, 소문자화, 중복 공백 제거 같은 기본적인 작업부터, 실제 적용 시에는 파이프라인 형태로 여러 단계의 처리가 연결된다.</p>
</li>
<li><p>전처리 파이프라이닝
여러 전처리 단계를 순차적으로 자동 처리할 수 있도록 구성하는 방식이다. 정제, 정규화, 필터링, 토큰화 등을 하나의 흐름으로 묶는 설계 방식으로, 이후 모델 학습에 사용될 데이터를 일관성 있게 다듬을 수 있다.</p>
</li>
<li><p>사전 학습 모델과 트랜스포머의 등장
BERT, 트랜스포머 같은 모델이 자연어 처리에 끼친 영향이 간단히 소개된다. 기존의 전처리나 피처 엔지니어링에만 의존하던 방식에서 벗어나, 모델 자체가 문맥을 이해하는 구조로 바뀌었음을 시사한다.</p>
</li>
</ol>
<h3 id="ch2-머신러닝과-자연어-처리를-위한-선형대수-확률-통계-마스터하기">CH2. 머신러닝과 자연어 처리를 위한 선형대수, 확률, 통계 마스터하기</h3>
<p>거의 7<del>9년 전 기억이 나서 굉장히 슬펐다(?). ~</del>그때 진짜 싫었는데..<del>~ 그리고 솔직히 스스로 depth 있게 modeling 을 안하니, 이 개념을 놓치고 살았고, ~</del>앞으로도 놓치고 살고 싶다는 생각이 들었다 ㅎㅎ.~~ 만약 이 개념이 생소하면 <a href="https://youtu.be/k_yto_vDRF0?si=m2oNb5lfz68rcHcs">https://youtu.be/k_yto_vDRF0?si=m2oNb5lfz68rcHcs</a> 과 같은 영상이라도 한 번 보는 것을 추천한다. 엄청 자세하게 정리하려고 하지는 않았는데, 스스로 정리차원에서 하나씩 곱씹어봤다. </p>
<h3 id="ml-을-위한-기본적인-선형대수학">ML 을 위한 기본적인 선형대수학</h3>
<h4 id="1-스칼라-벡터-행렬">[1] 스칼라, 벡터, 행렬</h4>
<ul>
<li><strong>스칼라 (Scalar)</strong>: 단일 수 (예: 3, -5, 0.2).</li>
<li><strong>벡터 (Vector)</strong>: 숫자의 나열로, 행 벡터와 열 벡터로 구분됨.</li>
<li><strong>행렬 (Matrix)</strong>: 2차원 숫자 배열로, 머신러닝에서 데이터를 표현하는 기본 구조.</li>
<li><strong>텐서 (Tensor)</strong>: 다차원 배열을 의미하며, 벡터와 행렬을 일반화한 개념.</li>
</ul>
<h4 id="2-벡터-연산">[2] 벡터 연산</h4>
<ul>
<li><strong>벡터 덧셈</strong>: 동일한 차원의 벡터끼리 요소별 덧셈 수행.</li>
<li><strong>벡터 내적 (Dot Product)</strong>: 두 벡터의 대응 원소 곱의 합.  <ul>
<li>내적 결과는 스칼라 값. 내적은 다른 벡터에 얼마나 투영되는지 측정하는데 사용.</li>
<li><strong><em>내적은 교환법칙 성립, 벡터 순서가 결과에 영향 X</em></strong></li>
<li>내적이 0이면 두 벡터는 서로 직교(orthogonal).</li>
<li>내적 공식:  </li>
</ul>
</li>
</ul>
<p>$$
\mathbf{a} \cdot \mathbf{b} = a_1b_1 + a_2b_2 + \dots + a_nb_n
$$</p>
<ul>
<li><p><strong>벡터 외적 (Cross Product)</strong>: 3차원 벡터에서 정의되며, 두 벡터에 모두 수직(orthogonal)인 벡터를 생성하며, 방향은 오른손 법칙을 따른다. 외적 결과는 벡터 값.</p>
</li>
<li><p><strong>벡터의 노름 (Norm)</strong></p>
<ul>
<li>벡터의 크기를 나타내는 값. 벡터 자신과 내적은 제곱 노름.</li>
<li>가장 많이 사용되는 노름: <strong>유클리드 노름 (Euclidean Norm)</strong></li>
<li>유클리드 노름 공식:</li>
</ul>
</li>
</ul>
<p>$$
|\mathbf{a}|_2 = \sqrt{a_1^2 + a_2^2 + \dots + a_n^2}
$$</p>
<h4 id="3-행렬-연산">[3] 행렬 연산</h4>
<ul>
<li><strong>행렬 전치 (Transpose)</strong>: 행과 열을 바꾸는 연산.  <ul>
<li>행 벡터 ↔ 열 벡터 변환.</li>
<li>전치 연산 예시:</li>
</ul>
</li>
</ul>
<p>$$
A^T_{m \times n} = A_{n \times m}
$$</p>
<ul>
<li><strong>행렬과 벡터의 곱</strong>: 결과는 벡터가 됨.</li>
<li><strong>행렬의 행렬곱 (Matrix Multiplication)</strong>:  <ul>
<li>$$A (m × n)$$ 과 $$B (n × p)$$ 가 있을 때, 결과는 $$(m × p)$$ 행렬.</li>
<li>교환법칙 성립하지 않음: $$(AB \neq BA)$$</li>
</ul>
</li>
</ul>
<h4 id="4-고윳값과-고유-벡터-eigenvalues-and-eigenvectors">[4] 고윳값과 고유 벡터 (Eigenvalues and Eigenvectors)</h4>
<ul>
<li><strong>정의</strong>:  <ul>
<li>행렬 $$(A)$$ 에 대해 다음을 만족하는 벡터 $$(\mathbf{v})$$ 와 상수 $$(\lambda)$$ 가 존재할 때:</li>
</ul>
</li>
</ul>
<p>$$
A\mathbf{v} = \lambda \mathbf{v}
$$</p>
<ul>
<li>$$(\lambda)$$는 고윳값(Eigenvalue), $$(\mathbf{v})$$ 는 고유 벡터(Eigenvector).<ul>
<li><strong>고윳값 분해 (Eigendecomposition)</strong>:  </li>
</ul>
</li>
<li>대각화 가능한 행렬 $$(A)$$ 는 다음과 같이 표현 가능:<ul>
<li>$$(D)$$: 대각행렬 (고윳값이 대각 원소로 위치)</li>
<li>$$(P)$$: 고유 벡터를 열 벡터로 가지는 행렬</li>
</ul>
</li>
</ul>
<p>$$
A = PDP^{-1}
$$</p>
<ul>
<li><strong>ML에서의 활용</strong>:<ul>
<li><strong>주성분 분석(PCA)</strong>: 데이터 차원 축소를 위해 고윳값 분해 활용.</li>
<li><strong>특잇값 분해(SVD)</strong>: 비정방 행렬의 차원 축소에 활용.</li>
</ul>
</li>
</ul>
<h3 id="ml-을-위한-기본적인-확률">ML 을 위한 기본적인 확률</h3>
<h4 id="1-확률-개념">[1] 확률 개념</h4>
<ul>
<li><strong>시행 (Trial)</strong>: 한 번의 실험.</li>
<li><strong>실험 (Experiment)</strong>: 확률적 결과를 가지는 과정.</li>
<li><strong>표본 공간 (Sample Space)</strong>: 가능한 모든 결과의 집합.</li>
<li><strong>사건 (Event)</strong>: 표본 공간의 부분집합.</li>
</ul>
<h4 id="2-확률-변수-random-variable">[2] 확률 변수 (Random Variable)</h4>
<ul>
<li><strong>이산 확률 변수 (Discrete Random Variable)</strong>:  <ul>
<li>가능한 값이 유한 개 또는 셀 수 있는 경우.</li>
<li>확률 질량 함수(PMF, Probability Mass Function)로 표현.</li>
</ul>
</li>
<li><strong>연속 확률 변수 (Continuous Random Variable)</strong>:  <ul>
<li>값이 연속적인 경우.</li>
<li>확률 밀도 함수(PDF, Probability Density Function)로 표현.</li>
</ul>
</li>
</ul>
<h4 id="3-조건부-확률과-독립성">[3] 조건부 확률과 독립성</h4>
<ul>
<li><strong>조건부 확률 (Conditional Probability)</strong>:<ul>
<li>사건 B가 발생한 상태에서 사건 A가 발생할 확률:</li>
</ul>
</li>
</ul>
<p>$$
P(A|B) = \frac{P(A \cap B)}{P(B)}
$$</p>
<ul>
<li><strong>독립 사건 (Independent Events)</strong>:<ul>
<li>사건 A와 B가 서로 독립이면:</li>
</ul>
</li>
</ul>
<p>$$
P(A \cap B) = P(A)P(B)
$$</p>
<h4 id="4-최대-우도-추정법-maximum-likelihood-estimation-mle">[4] 최대 우도 추정법 (Maximum Likelihood Estimation, MLE)</h4>
<ul>
<li><strong>MLE 개념</strong>:<ul>
<li>확률 분포의 매개변수를 추정하는 방법.</li>
<li>가능도 함수 (Likelihood Function)를 최대화하는 매개변수 선택.</li>
</ul>
</li>
<li><strong>로그 우도 (Log-Likelihood)</strong>:<ul>
<li>곱셈 연산보다 덧셈이 계산적으로 더 유리하여 로그 우도 사용:</li>
</ul>
</li>
</ul>
<p>$$
\log L(\theta) = \sum_{i=1}^{n} \log P(x_i | \theta)
$$</p>
<ul>
<li><strong>자연어 처리에서 활용</strong>:<ul>
<li>다음 단어 예측에서 가장 높은 확률을 갖는 단어를 선택하는 방식으로 사용.</li>
<li>즉, 주어진 문맥 𝑤 1 , 𝑤 2 , . . . , 𝑤 𝑡 − 1 가 있을 때, 다음 단어 𝑤 𝑡 의 확률을 최대화하는 모델을 찾는 것. <strong><em>가능도 함수(Likelihood Function)</em></strong> 활용.</li>
</ul>
</li>
</ul>
<h4 id="5-베이지안-추정-bayesian-estimation">[5] 베이지안 추정 (Bayesian Estimation)</h4>
<ul>
<li><strong>베이즈 정리 (Bayes&#39; Theorem)</strong>:<ul>
<li>사전 확률과 데이터에 기반한 사후 확률 계산:</li>
</ul>
</li>
</ul>
<p>$$
P(\theta | D) = \frac{P(D | \theta) P(\theta)}{P(D)}
$$</p>
<ul>
<li><code>MLE</code> 와 달리 사전 확률을 고려하는 방식.</li>
</ul>
<h4 id="6-추가-개념">[6] 추가 개념</h4>
<ul>
<li><strong>하우스홀더 반사 행렬 (Householder Reflection Matrix)</strong>:<ul>
<li>벡터를 특정 차원에서만 0이 아닌 성분을 갖도록 변환하는 행렬.</li>
</ul>
</li>
<li><strong>대각화 가능성 (Diagonalizability)</strong>:<ul>
<li>행렬이 대각화 가능하면 계산이 용이함.</li>
</ul>
</li>
<li><strong>가역 행렬 (Invertible Matrix)</strong>:<ul>
<li>역행렬이 존재하는 행렬.</li>
</ul>
</li>
<li><strong>가우스 소거법 (Gaussian Elimination)</strong>:<ul>
<li>연립 방정식 풀이를 위한 행렬 변형 기법.</li>
</ul>
</li>
<li><strong>행렬의 대각합 (Trace of a Matrix)</strong>:<ul>
<li>대각 원소의 합.</li>
<li>선형 변환의 고윳값 합과 동일.</li>
</ul>
</li>
</ul>
<h3 id="ch3-자연어-처리에서-머신러닝-잠재력-발휘하기">CH3. 자연어 처리에서 머신러닝 잠재력 발휘하기</h3>
<h4 id="데이터-탐색">데이터 탐색</h4>
<p>자연어 처리 모델의 성능은 입력 데이터 품질에 크게 좌우된다. 이 장에서는 데이터를 탐색하고 정제하는 과정에서 고려해야 할 핵심 요소들을 설명한다. 먼저 결측치는 전체 분석의 왜곡을 야기할 수 있으므로 반드시 처리해야 하며, 중복 데이터 역시 불필요한 중복 학습을 유발할 수 있으므로 제거 대상이다. </p>
<p>데이터 표준화에서는 각 특성(feature)의 평균을 0, 표준편차를 1로 맞추는 Z-score 정규화가 자주 사용된다. 반면 최소-최대 스케일링은 데이터를 0과 1 사이로 정규화하는 방법으로, 각 값의 상대적인 크기를 유지하며 모델이 특정 특성에 과도하게 민감하지 않게 도와준다.</p>
<ul>
<li><p><strong>Z-score 정규화 (Standardization)</strong> $$x&#39; = (x - μ) / σ$$</p>
<ul>
<li>μ는 특성의 평균 (mean)</li>
</ul>
</li>
<li><p><strong>Min-Max 정규화 (Min-Max Scaling)</strong> </p>
</li>
</ul>
<p>$$
x&#39; = \frac{x - x_{\text{min}}}{x_{\text{max}} - x_{\text{min}}}
$$</p>
<p>이상치 처리는 모델 안정성에 중요하다. 윈저화는 일정 범위 밖의 값들을 경계 값으로 조정해 이상치의 영향을 줄이는 기법이다. 또는 강건한 통계 기법을 활용해 이상치를 억제할 수도 있다. 중요한 건 이상치를 제거하는 것이 아니라, 그것이 분석 결과에 어떤 영향을 미치는지 파악하고 도메인 지식과 함께 판단해야 한다는 점이다.</p>
<p>데이터 오류 수정은 통계적 이상 탐지나 수동 검사 외에도 머신러닝 기반 접근법이 가능하다. 도메인 지식이 병행되면 신뢰도는 더욱 높아진다. 이후 가장 중요한 과정 중 하나는 <strong>특성 선택(feature selection)</strong> 이다. 정보량 기반 방법, 카이제곱 검정, 상관계수 분석, 라쏘 회귀(L1 패널티) 등을 통해 유의미한 특성을 골라야 한다. 또한, 고차원 데이터에서는 차원 축소 기법인 PCA(주성분 분석), LDA(선형 판별 분석)도 유효하다.</p>
<h4 id="일반적인-머신러닝-모델-인프런-파이썬-머신러닝-완벽-가이드-추천">일반적인 머신러닝 모델 <a href="https://www.inflearn.com/course/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EC%99%84%EB%B2%BD%EA%B0%80%EC%9D%B4%EB%93%9C">인프런, 파이썬 머신러닝 완벽 가이드</a> (추천...)</h4>
<ul>
<li><strong>선형 회귀</strong>는 연속적인 수치 예측에 적합하고,</li>
<li><strong>로지스틱 회귀</strong>는 이진 분류에 활용된다.</li>
<li><strong>결정 트리</strong>는 해석이 쉬운 구조로 분류와 회귀 모두 가능하지만 과적합되기 쉬우며, </li>
<li><strong>랜덤 포레스트</strong>는 여러 결정 트리를 앙상블하여 안정성과 성능을 향상시킨다.</li>
<li><strong>SVM(서포트 벡터 머신)</strong> 은 고차원에서의 마진 최대화를 통해 분류 문제에 강력하며,</li>
<li><strong>인공 신경망</strong>은 복잡한 비선형 관계를 학습할 수 있어 텍스트, 음성, 이미지 등 다양한 도메인에서 활용된다.</li>
</ul>
<p>특히, 트랜스포머는 자연어 처리에서 혁신적 성능을 보여준 구조로, 셀프 어텐션을 기반으로 문맥을 효과적으로 반영하는 구조다. 이는 후속 챕터에서 더 깊이 다루어진다.</p>
<h4 id="모델-과소적합과-과대적합">모델 과소적합과 과대적합</h4>
<p>모델이 너무 단순해서 학습 데이터조차 설명하지 못하는 경우는 <strong>과소적합(underfitting)</strong>, 반대로 지나치게 복잡해 학습 데이터에만 특화된 경우는 <strong>과대적합(overfitting)</strong>이다. 이 둘의 균형을 맞추는 것이 <strong>편향-분산 트레이드오프</strong>이다.</p>
<p>이를 해결하기 위한 전략으로는 </p>
<ul>
<li><strong>정규화 기법</strong>(L1 라쏘, L2 릿지, 또는 두 방법을 혼합한 엘라스틱넷),</li>
<li><strong>교차 검증</strong>,</li>
<li><strong>조기 종료</strong>(validation loss가 더 이상 줄어들지 않을 때 학습 중단),</li>
<li><strong>드롭아웃</strong>(신경망 일부 노드를 임의로 비활성화),</li>
<li><strong>데이터 증강</strong>,</li>
<li><strong>앙상블</strong> 등이 있다.</li>
</ul>
<h4 id="데이터-분할">데이터 분할</h4>
<p>모델 학습 과정에서 데이터를 <strong>훈련(train)</strong>, <strong>검증(validation)</strong>, <strong>테스트(test)</strong> 세트로 나누는 것은 일반적인 접근이다. 기본적으로 8:2로 훈련-테스트를 나누거나, 더 정교하게는 <strong>K-폴드 교차 검증</strong>으로 평가의 일관성을 확보한다. <strong>계층화 K-폴드</strong>는 클래스 불균형을 다루는 데 유용하며, 시계열 데이터의 경우 시간 순서를 고려하여 분할해야 한다.</p>
<h4 id="하이퍼파라미터-튜닝">하이퍼파라미터 튜닝</h4>
<p>모델 학습 전에 설정하는 값들인 <strong>하이퍼파라미터(hyperparameter)</strong>는 모델 성능에 큰 영향을 미친다. 예를 들어, 학습률, 정규화 강도, 은닉층 수 등이다. </p>
<p>이를 튜닝하기 위한 방법에는</p>
<ul>
<li><strong>그리드 탐색(grid search)</strong>: 조합을 전수조사</li>
<li><strong>랜덤 탐색(random search)</strong>: 무작위 조합을 샘플링</li>
<li><strong>베이지안 최적화</strong>: 이전 결과 기반으로 다음 탐색 지점 예측</li>
</ul>
<p>특히 <strong>SMBO(Sequential Model-Based Optimization)</strong>는 성능 예측 모델을 통해 탐색 효율을 높인다. 하이퍼파라미터 튜닝은 탐색 공간이 크고 평가 비용이 높기 때문에 샘플링 전략과 병렬화 등이 고려되어야 한다.</p>
<h4 id="앙상블-모델">앙상블 모델</h4>
<p><strong>앙상블 학습</strong>은 여러 모델의 예측을 결합하여 전체 성능을 향상시키는 방법이다.</p>
<ol>
<li><strong><em>배깅(Bagging - Bootstrap Aggregating)</em></strong></li>
</ol>
<ul>
<li>작동 원리: 원본 데이터셋에서 부트스트랩 샘플링(복원 추출)을 통해 여러 서브셋을 생성하고, 각 서브셋에서 동일한 유형의 모델을 학습시킨 후 결과를 통합한다. </li>
<li>통합 방식: 분류 문제에서는 투표(voting), 회귀 문제에서는 평균(averaging)을 사용한다.</li>
<li>장점: 분산(variance)을 감소시켜 과적합을 줄이고 모델의 안정성을 높인다.</li>
<li>주요 알고리즘:<ul>
<li>랜덤 포레스트: 부트스트랩 샘플링으로 얻은 각기 다른 훈련 데이터를 사용하고, 특성 또한 매번 랜덤하게 선택하여 다양성을 높이는 방법이다.</li>
<li>배깅 결정 트리: 동일한 알고리즘을 사용하지만 다른 훈련 데이터로 여러 모델을 훈련시키는 방법이다.</li>
</ul>
</li>
</ul>
<ol start="2">
<li><strong><em>부스팅(Boosting)</em></strong></li>
</ol>
<ul>
<li>작동 원리: 약한 학습기(weak learner)를 순차적으로 학습시키며, 이전 모델이 잘못 예측한 샘플에 더 높은 가중치를 부여한다.</li>
<li>특징: 이전 모델의 오류에 집중하여 점진적으로 예측 성능을 향상시킵니다.</li>
<li>주요 알고리즘:<ul>
<li>AdaBoost(Adaptive Boosting): 오분류된 샘플에 더 높은 가중치를 할당하며, 각 모델의 정확도에 따라 최종 예측에 다른 가중치를 부여한다.</li>
<li>Gradient Boosting: 이전 모델의 잔차(residual)나 오차에 대해 다음 모델을 학습시킵니다. 이 방식은 손실 함수의 기울기(gradient)를 최소화하는 방향으로 진행된다.</li>
<li>XGBoost, LightGBM, CatBoost: Gradient Boosting의 최적화된 구현으로, 속도와 성능 측면에서 개선되었다. </li>
</ul>
</li>
</ul>
<ol start="3">
<li><strong><em>스태킹(Stacking)</em></strong></li>
</ol>
<ul>
<li>작동 원리: 여러 기본 모델(base model)의 예측 결과를 새로운 특성으로 사용하여 메타 모델(meta-model)을 학습시킨다. </li>
<li>구현 방식: 다양한 알고리즘으로 여러 기본 모델을 학습시킵니다 (예: 결정 트리, SVM, 로지스틱 회귀 등).<ul>
<li>각 기본 모델의 예측값을 새로운 특성으로 변환한다.</li>
<li>이 새로운 특성들을 입력으로 사용하는 메타 모델을 훈련시킨다.</li>
</ul>
</li>
<li>장점: 각 모델의 강점을 활용하고 약점을 상쇄하여 전체적인 예측 성능을 향상시킨다.</li>
<li>구현 시 고려사항: 기본 모델 결과의 과적합을 방지하기 위해 교차 검증을 통한 예측값 생성이 중요하다. </li>
</ul>
<h3 id="ch4-자연어-처리-성능을-위한-텍스트-전처리-과정-최적화">CH4. 자연어 처리 성능을 위한 텍스트 전처리 과정 최적화</h3>
<p>전처리는 중요한 초기 단계이다. &quot;원시 상태&quot; 의 자연어를 &quot;머신러닝 알고리즘이 쉽게 이해할 수 있는 형식&quot; 으로 변환하는 과정 포함한다. </p>
<p>자연어 처리에서의 소문자 변환, 특수 문자와 구두점 제거, 불용어 제거, 개체명 인식 (NER), 품사 태깅(POS 태깅), 전처리 파이프라인 을 다룬다. NLTK, spaCy, 사이킷런 라이브러리 활용한다. (with 쥬피터노트북)</p>
<p><strong><em>1. 자연어 처리에서의 소문자 변환</em></strong></p>
<ul>
<li><p>소문자 변환은 텍스트 전처리에서 가장 먼저 수행되는 작업 중 하나로, 전체 어휘 집합의 복잡도를 줄이고 통일성을 확보하는 데 도움이 된다. 예를 들어 &quot;Apple&quot;과 &quot;apple&quot;을 동일하게 처리함으로써 분류기나 모델이 더 안정적으로 학습할 수 있다.</p>
</li>
<li><p>하지만 모든 상황에서 유용한 것은 아니다. 특히 개체명 인식(NER)에서는 대문자 정보가 중요한 신호가 되기 때문에, 무작정 소문자로 바꾸는 것이 오히려 성능 저하로 이어질 수 있다. </p>
</li>
<li><p>책에서 소개된 정규 표현식 예시인 <code>re.sub(r&#39;[^A-Za-z0-9]+&#39;, &#39;&#39;, text)</code>는 공백까지 제거하는 잘못된 패턴으로 보인다. <code>re.sub(r&#39;[^A-Za-z0-9\s]+&#39;, &#39;&#39;, text)</code> 처럼 공백은 살리고 특수 문자만 제거하는 패턴을 말하고자 한 것 같다. </p>
</li>
</ul>
<p><strong><em>2. 특문과 구두점 제거</em></strong></p>
<ul>
<li>텍스트에서 특수 문자나 구두점은 대부분 의미가 약하거나 노이즈로 작용하는 경우가 많다. 따라서 이들을 제거함으로써 모델의 학습을 보다 효과적으로 만들 수 있다. 그러나, 예외적인 경우(예: 감정 분석에서 느낌표)에는 정보 손실로 이어질 수 있어 주의가 필요하다.</li>
</ul>
<p><strong><em>3. 불용어 제거</em></strong></p>
<ul>
<li>불용어(stopwords)는 텍스트 의미에 큰 영향을 주지 않는 단어들로, 예: a, an, the, and, in, at 등이 있다. 불용어 제거를 통해 어휘 크기와 특성 공간의 차원을 줄여 효율성을 높일 수 있다.</li>
<li><a href="https://wikidocs.net/217239">한글 불용어 제거</a></li>
</ul>
<p><strong><em>4. 맞춤법 검사와 교정</em></strong></p>
<ul>
<li>텍스트 내 오타나 철자 오류는 전처리 단계에서 수정해야 한다. 사소한 오탈자 하나가 모델의 처리 단위를 바꿔 성능을 저하시킬 수 있기 때문이다.</li>
</ul>
<p><strong><em>5. 표제어 추출</em></strong></p>
<ul>
<li>단어를 기본 형태 또는 사전 형태인 표제어로 단순화하는 텍스트 정규화 방법이다.</li>
<li>예: &quot;cats&quot; → &quot;cat&quot;, &quot;mice&quot; → &quot;mouse&quot;. 단어의 품사 정보까지 고려하므로 정교한 처리가 가능하다.</li>
</ul>
<p><strong><em>6. 어간 추출</em></strong></p>
<ul>
<li>단어를 기본적이거나 뿌리 형태로 축소하는 과정이다. 이때 뿌리를 어간(stem) 이라 한다.</li>
<li>품사나 문맥은 고려하지 않기 때문에 다소 과도한 변형이 발생할 수 있음을 유의해야 한다.</li>
<li>KoNLPy 의 okt 형태소 분석기가 어간 추출 기능 제공한다. </li>
</ul>
<h4 id="개체명-인식">개체명 인식</h4>
<ul>
<li>NER은 텍스트 내에서 사람, 조직, 장소와 같은 고유명사를 탐지하고 분류하는 작업이다.  </li>
<li>주로 조건부 무작위장(CRF), 순환 신경망(RNN) 기반 모델이 사용된다.  </li>
<li>트랜스포머 기반의 BERT 역시 최근 NER 성능을 크게 향상시킨 대표적 모델이다.  </li>
<li>이 챕터에서는 관련 구현 예제를 깃허브 코드로 제공한다.</li>
</ul>
<h4 id="품사-태깅">품사 태깅</h4>
<ul>
<li>단어에 명사, 동사, 형용사 등의 품사 태그를 부여하는 과정으로, 다음 세 가지 접근이 있다.</li>
</ul>
<ol>
<li><p><strong>규칙 기반 방법</strong><br>예: 단어가 -ing로 끝나면 동명사일 가능성, 앞에 관사가 있으면 명사일 가능성</p>
</li>
<li><p><strong>통계 기반 방법</strong><br>은닉 마르코프 모델(HMM), 조건부 랜덤 필드(CRF)를 활용한 확률적 추론 기반<br>문맥에 따라 가장 가능성 높은 품사를 선택한다.</p>
</li>
<li><p><strong>딥러닝 기반 방법</strong><br>RNN, 특히 LSTM 셀을 사용한 시퀀스 모델 기반의 접근<br>입력 레이어에 워드 임베딩을 넣고, LSTM 레이어를 통해 문맥 정보를 처리한 뒤, 출력 레이어에서 품사 태그를 예측한다.<br>최근에는 BERT 같은 트랜스포머 기반 모델이 더 높은 정확도로 POS 태깅을 수행한다.<br>관련 코드도 깃허브 레포에 포함되어 있다.</p>
</li>
</ol>
<h4 id="정규-표현식">정규 표현식</h4>
<ul>
<li>regex, regexp, 쉽고 빠른 유효성 검증 가능한 접근 법이다.</li>
<li>검색 및 교체, 특정 패턴에 따른 데이터 추출에 유리하다!</li>
</ul>
<h4 id="토큰화">토큰화</h4>
<ul>
<li>tokenization, 텍스트를 토큰 단위로 분리하는 과정이다.</li>
<li><strong>단어 토큰화</strong>: 공백이나 구두점을 기준으로 분리</li>
<li><strong>문장 토큰화</strong>: 마침표, 느낌표 등을 기준으로 분리</li>
<li><strong>서브워드 토큰화</strong>: WordPiece(BERT), Byte Pair Encoding(BPE) 등을 활용해 희귀어(OOV)를 처리<ul>
<li>BERT에서는 WordPiece 방식이 사용되며, 드물거나 긴 단어도 부분적으로 분할해 처리할 수 있는 장점이 있다.</li>
</ul>
</li>
</ul>
<h4 id="전처리-파이프라인-예시와-개체명-인식-및-품사-태깅은-깃허브-레포-코드로-상세하게-볼-수-있다">전처리 파이프라인 예시와 개체명 인식 및 품사 태깅은 깃허브 레포 코드로 상세하게 볼 수 있다.</h4>
<h3 id="chapter-5-텍스트-분류-강화-전통적인-머신러닝-기법-활용하기">CHAPTER 5 텍스트 분류 강화: 전통적인 머신러닝 기법 활용하기</h3>
<h4 id="텍스트-분류의-유형">텍스트 분류의 유형</h4>
<p>텍스트 분류는 말 그대로 텍스트 데이터를 어떤 카테고리에 속하는지 판단하는 작업이다. 이때 사용할 수 있는 학습 방식은 크게 지도 학습, 비지도 학습, 준지도 학습으로 나뉜다.</p>
<ul>
<li><p>지도 학습은 레이블이 부여된 데이터를 통해 학습하고, 이후에는 새로운 텍스트에 대해 자동으로 레이블을 예측할 수 있다. 주요 알고리즘으로는 나이브 베이즈, 로지스틱 회귀, 서포트 벡터 머신(SVM)이 있다. 특히 SVM은 초평면을 이용한 분류로 유명하다.</p>
</li>
<li><p>비지도 학습은 레이블 없이 데이터 내 숨겨진 구조나 패턴을 발견하는 접근이다. 군집화(Clustering), LDA(Latent Dirichlet Allocation, 잠재 디리클레 할당), Word2Vec, GloVe와 같은 임베딩 기반 학습 등이 대표적이다. 특히 Word2Vec과 GloVe는 단어를 밀집 벡터로 표현하여 의미론적 유사성을 잘 반영할 수 있도록 한다.</p>
</li>
<li><p>준지도 학습은 지도와 비지도의 중간 형태로, 레이블이 일부만 부여된 상황에서 지도학습과 비지도학습을 혼합하여 모델을 훈련시킨다. 대표적인 방법으로 레이블 전파(Label Propagation), 공동 훈련(Co-training) 등이 있다.</p>
</li>
</ul>
<h4 id="tf-idf를-활용한-텍스트-분류">TF-IDF를 활용한 텍스트 분류</h4>
<p>가장 전통적인 텍스트 분류 접근은 원-핫 인코딩(One-hot Encoding)이나 단어의 등장 빈도를 바탕으로 특징을 추출하는 방법이다. 흔히 사용하는 방법이 단어의 출현 빈도를 기준으로 문서를 벡터화하는 단어 가방(Bag of Words, BoW) 모델이다.</p>
<p>BoW의 단점은 문서마다 단어의 중요도를 반영하지 못하는 것인데, 이를 보완하기 위해 등장한 것이 TF-IDF(Term Frequency - Inverse Document Frequency)이다.</p>
<ul>
<li><code>TF</code> 는 특정 문서에서의 단어 빈도</li>
<li><code>IDF</code> 는 그 단어가 다른 문서에서 얼마나 드물게 나타나는지를 수치화</li>
</ul>
<p>이를 곱한 TF-IDF 값이 클수록 특정 문서에서 해당 단어가 중요하다고 판단한다.</p>
<p><code>TF-IDF</code> 는 단어의 중요도를 반영한 특징 벡터를 만들어 <code>SVM</code>, 로지스틱 회귀 등의 분류기에 적용할 수 있으며, 실무에서도 여전히 많이 사용되는 전통적인 접근이다.</p>
<h4 id="word2vec을-활용한-텍스트-분류">Word2Vec을 활용한 텍스트 분류</h4>
<p><code>Word2Vec</code> 은 단어를 저차원의 실수 벡터로 임베딩하는 모델이다. 두 가지 주요 학습 방법이 존재한다.</p>
<ul>
<li><code>CBOW(Continuous Bag of Words)</code>: 주변 단어들을 보고 중심 단어를 예측</li>
<li><code>Skip-gram</code>: 중심 단어를 보고 주변 단어들을 예측</li>
</ul>
<p>Word2Vec은 학습 과정에서 확률적 경사 하강법(SGD)과 역전파(backpropagation)를 사용하여 모델 파라미터를 최적화한다. 결과적으로 학습된 임베딩은 유사한 의미를 가진 단어들이 벡터 공간에서 가까운 위치를 갖도록 표현된다.</p>
<p>Word2Vec을 활용하면 문서 전체를 단어 임베딩의 평균이나 합으로 표현하여 분류 문제에 활용할 수 있다.</p>
<h4 id="토픽-모델링-비지도-텍스트-분류의-특정-사례">토픽 모델링: 비지도 텍스트 분류의 특정 사례</h4>
<p>토픽 모델링은 비지도 학습의 대표적인 사례로, 문서가 어떤 주제를 다루는지 자동으로 파악하는 방법이다. 가장 대표적인 알고리즘은 LDA(Latent Dirichlet Allocation)이다.</p>
<p>LDA는 각 문서가 여러 토픽의 혼합으로 구성되었고, 각 토픽은 단어들의 확률분포로 표현된다고 가정한다. 이를 통해 문서에 숨겨진 주제 분포를 추정한다.</p>
<p>LDA는 클러스터링이나 추천 시스템 등 다양한 NLP 태스크에 활용되며, 레이블이 없는 상황에서 유용하게 쓰인다.</p>
<h4 id="머신러닝-시스템-설계">머신러닝 시스템 설계</h4>
<p>실제 분류 모델을 만들기 위해서는 학습 데이터를 분할하고(훈련/검증/테스트), 모델을 훈련한 후 적절한 평가 지표로 성능을 검증해야 한다. 또한 하이퍼파라미터 튜닝은 반드시 필요한 과정이다.</p>
<ul>
<li>하이퍼파라미터는 학습 전에 설정하는 파라미터로, 학습 중에는 변경되지 않는다. 예를 들어 나이브 베이즈의 알파값, SVM의 C 값, 로지스틱 회귀의 정규화 계수 등이 이에 해당한다.</li>
</ul>
<p>하이퍼파라미터는 모델의 성능에 큰 영향을 미치기 때문에 그리드 서치나 랜덤 서치 같은 방법으로 적절히 탐색해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/240feb8e-78df-4fb0-98c2-dbe11e4edcd0/image.png" alt=""></p>
<h3 id="ch6-텍스트-분류의-재해석-딥러닝-언어-모델-깊게-탐구하기">CH6. 텍스트 분류의 재해석: 딥러닝 언어 모델 깊게 탐구하기</h3>
<p>딥러닝은 텍스트 데이터를 다루는 데 있어 기존의 RNN, CNN을 넘어서는 혁신적인 전환점을 마련해왔다. 이 장에서는 딥러닝과 자연어 처리(NLP)의 결합을 어떻게 실현할 수 있는지, 그리고 최근 가장 각광받는 트랜스포머 기반 언어 모델들이 어떤 구조와 원리로 작동하는지를 다루고 있다.</p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/6fbdc6f2-3465-40bd-ba37-b26bbf66145a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f89751d9-c96f-4d4c-9f8a-5203e1fa79e8/image.png" alt=""></p>
<p>(이미지 출처: <a href="https://heung-bae-lee.github.io/2019/12/08/deep_learning_03/">https://heung-bae-lee.github.io/2019/12/08/deep_learning_03/</a>)</p>
<p>신경망의 기본 구성은 입력층, 은닉층, 출력층으로 나뉜다. 특히 은닉층은 입력과 출력 사이에서 정보를 가공하며, 가중치의 곱과 편향을 합산한 뒤 <strong>활성화 함수</strong>를 통해 비선형성을 도입한다. 주요 활성화 함수로는 Sigmoid, Tanh, ReLU, ELU 등이 있으며, 각각의 특성에 따라 네트워크의 성능이나 수렴 속도에 영향을 준다.</p>
<p>또한 학습의 반복을 나타내는 <strong>Epoch</strong>, 한 번의 학습에서 사용하는 데이터 크기를 의미하는 <strong>배치 크기(Batch size)</strong> 등의 하이퍼파라미터는 과대적합과 과소적합을 피하면서 최적 성능을 달성하는 데 매우 중요하다.</p>
<h4 id="다양한-신경망-아키텍처">다양한 신경망 아키텍처</h4>
<ul>
<li><strong>FNN (Feedforward Neural Network)</strong>: 기본적인 전방향 신경망</li>
<li><strong>MLP (Multilayer Perceptron)</strong>: 은닉층이 1개 이상인 구조</li>
<li><strong>CNN (Convolutional Neural Network)</strong>: 이미지 처리에 적합한 구조로, NLP에서도 특징 추출 목적으로 사용됨</li>
<li><strong>RNN (Recurrent Neural Network)</strong>: 시퀀스 데이터를 다룰 수 있는 구조로, 이전 상태의 정보를 메모리 셀에 저장</li>
<li><strong>AE (Autoencoder)</strong>: 입력을 압축하고 다시 복원하는 방식의 비지도 학습 모델</li>
<li><strong>GAN (Generative Adversarial Network)</strong>: 생성자와 판별자가 경쟁하는 구조</li>
</ul>
<h4 id="트랜스포머-이해하기">트랜스포머 이해하기</h4>
<p>트랜스포머는 논문 <em>Attention is All You Need</em>에서 처음 소개된 모델로, 순차적 연산이 필요했던 RNN과 달리 <strong>병렬 처리</strong>가 가능하다는 점에서 큰 혁신을 일으켰다. 핵심은 <strong>Self-Attention</strong> 메커니즘이다.</p>
<p><strong>Self-Attention (셀프 어텐션)</strong>은 입력 시퀀스 내에서 각 단어가 다른 단어들과 얼마나 연관이 있는지를 파악하고, 그에 따라 가중치를 부여하는 방식이다. 이를 위해 각 단어 임베딩은 선형 변환을 거쳐 <strong>쿼리(Q)</strong>, <strong>키(K)</strong>, <strong>값(V)</strong>로 변환된다. 그 후, 다음 수식을 통해 어텐션 점수가 계산된다:</p>
<p>$$
\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right)V
$$</p>
<p>이 점곱 어텐션은 <strong>Scaled Dot-Product Attention</strong>이라 불리며, 계산된 값이 너무 크거나 작아지는 것을 방지하기 위해 $$\sqrt{d_k}$$ 로 나눈다.</p>
<p>한편, 트랜스포머는 순차 정보를 고려하지 않기 때문에 입력 임베딩에 <strong>위치 인코딩</strong>을 더해 각 단어의 순서를 보완한다. 위치 인코딩은 사인/코사인 함수를 활용하여 각 차원별로 주기적인 위치 정보를 부여한다.</p>
<p>가장 좋았던 부분은 &quot;BERT를 텍스트 분류에 미세 조정을 하는 방법&quot; 이 있다는 것이다. 물론 이론적인 부분이 과반이지만 앞서 설명한 얘기를 기반으로 진행하기 때문에 설명이 친절한 편이라고 생각된다. 실제로 해당 장의 마지막엔 실습이 코드와 함께 있음 (물론 실제 하려면 사전 세팅이나 준비 사항이 꽤 많이 필요함)</p>
<h3 id="ch7-대규모-언어-모델-이해하기">CH7. 대규모 언어 모델 이해하기</h3>
<p>이번 장에서는 대규모 언어 모델(LLM, Large Language Model)의 탄생 배경과 발전 과정을 중심으로 설명하고 있다. 본격적으로 LLM이 주목받기 시작한 2018~2019년부터, 2023년에 이르기까지 NLP 분야에서 일어난 변화들을 연대기적으로 서술한다.</p>
<p>OpenAI가 ChatGPT로 널리 알린 <strong>RLHF</strong>는 LLM 발전의 가장 중요한 전환점 중 하나였다.</p>
<p>기존의 언어 모델은 단순히 대량의 데이터를 이용한 사전 학습(Pretraining)과, 정답이 있는 문제에 대한 지도 학습(Supervised Fine-Tuning)만으로는 사용자의 기대에 부합하는 응답을 생성하기 어려웠다. 이를 해결하기 위해 도입된 것이 RLHF다.</p>
<p>기본적인 흐름은 다음과 같다.</p>
<ol>
<li><p><strong>Supervised Fine-Tuning (SFT)</strong><br>우선 사람이 직접 작성한 고품질 응답 데이터를 바탕으로 언어 모델을 지도 학습한다.</p>
</li>
<li><p><strong>Reward Model (RM) 학습</strong><br>사람이 두 응답 중 더 좋은 것을 선택하여 쌍 비교 데이터(pairwise preference)를 제공하면, 이를 통해 모델이 무엇이 더 좋은 응답인지를 학습하는 보상 모델(Reward Model)을 구축한다.</p>
</li>
<li><p><strong>PPO (Proximal Policy Optimization)</strong><br>이후 모델의 출력을 RM을 통해 평가하면서 강화학습을 진행한다. PPO는 강화학습에서 흔히 사용되는 기법으로, 모델의 정책이 너무 급격하게 변화하지 않도록 안정성을 확보하는 목적을 가진다. 이 과정을 통해 모델은 사람의 피드백을 반영한 보다 유용하고 안전한 응답을 생성할 수 있게 된다.</p>
</li>
</ol>
<p>실제로 OpenAI는 RLHF를 통해 GPT-3를 ChatGPT로 발전시켰고, Google은 PaLM을, Meta는 LLaMA 시리즈를, Anthropic은 Constitutional AI를 적용한 Claude를 발표했다. 특히 RLHF 기반 모델은 사용자의 피드백을 받아 들이며, 이전보다 훨씬 정교한 대화와 문장 생성을 보여주었다.</p>
<h3 id="ch8-대규모-언어-모델의-잠재력을-끌어내는-rag-활용-방법">CH8. 대규모 언어 모델의 잠재력을 끌어내는 RAG 활용 방법</h3>
<p>이 장에서는 최근 LLM 응용에서 핵심 키워드가 된 <strong>RAG (Retrieval-Augmented Generation)</strong>의 개념과 활용법, 그리고 이를 실제로 구현하기 위한 기술 스택으로 <strong>LangChain</strong>을 소개한다. 또한, OpenAI API 기반 접근법과 로컬에서 LLaMA, GPT-J 같은 오픈소스 모델을 세팅하는 실전적인 예제도 포함되어 있다.</p>
<h4 id="langchain-파이프라인">LangChain 파이프라인</h4>
<p>RAG를 제대로 활용하기 위해서는 LLM 단독으로는 부족하고, 외부에서 정보를 검색하고, 이를 활용하여 답변을 생성하는 파이프라인이 필요하다. 이를 위해 LangChain 프레임워크가 소개된다.</p>
<p>LangChain은 다양한 LLM과 데이터 소스를 연결하고, 체계적으로 응답을 생성할 수 있게 도와주는 파이썬 기반 오픈소스 라이브러리이다.</p>
<ul>
<li><strong>Component (컴포넌트)</strong>: LangChain의 최소 단위로, 프롬프트 템플릿, LLM, 메모리, 툴 등 다양한 유형이 존재</li>
<li><strong>Chain (체인)</strong>: 여러 컴포넌트를 순차적으로 연결하여 하나의 일관된 흐름을 만드는 구조</li>
<li><strong>Agent (에이전트)</strong>: 체인과 달리, 스스로 의사결정을 하고, 필요한 툴을 골라 사용하며 상황에 맞게 동작을 수행</li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/1b7c0b00-4449-4e1c-b7db-597b5aeab393/image.png" alt=""></p>
<p>RAG 뿐만 아니라, LangChain에서는 <strong>메모리</strong>라는 개념도 중요한데, 이는 이전 대화나 정보를 저장하여 이후 대화에 활용하는 기능이다. 메모리를 활용하면 대화형 에이전트가 앞선 대화 내용을 기억하고 이를 기반으로 더 자연스럽고 일관성 있는 대화를 이어갈 수 있다. 특히 LLM이 가진 한계 중 하나인 컨텍스트 윈도우 문제를 보완할 수 있는 중요한 장치이다.</p>
<h3 id="ch9-대규모-언어-모델이-주도하는-고급-응용-프로그램-및-혁신의-최전선">CH9. 대규모 언어 모델이 주도하는 고급 응용 프로그램 및 혁신의 최전선</h3>
<p>이번 장에서는 대규모 언어 모델(LLM)을 활용한 고급 시스템 설계와 최근 주목받고 있는 다양한 응용 기술들을 실습 중심으로 소개한다. 특히 LangChain을 활용한 <strong>고급 RAG</strong>, <strong>프롬프트 최적화</strong>, <strong>다중 에이전트</strong> 구성까지 이어지는 내용이 흥미롭다.</p>
<p>Retriever, Memory, Chain, Agent를 단순히 나열하는 수준이 아니라, 여러 개의 컴포넌트를 유기적으로 연결하여 더욱 정교한 시스템을 구축한다.</p>
<p>예를 들어, 사용자의 질문이 들어왔을 때 적절한 <strong>Retriever를 조건부로 선택</strong>하거나, 검색된 여러 정보를 <strong>우선순위로 정렬</strong>한 후 LLM에 전달하는 등 보다 실용적인 로직을 실습 형태로 설명하고 있다.</p>
<h4 id="프롬프트-압축-llm-lingua">프롬프트 압축 (LLM Lingua)</h4>
<p>LLM을 사용할 때 가장 큰 문제 중 하나는 프롬프트 토큰의 한계와 비용이다. 특히 RAG 구조에서는 외부 검색 결과를 LLM 입력으로 넣을 때, 프롬프트가 지나치게 커질 수 있다.</p>
<p>이를 해결하기 위한 방법으로 <strong>LLMLingua</strong>가 소개된다. LLMLingua는 대규모 언어 모델이 처리할 프롬프트를 축약(compression)하는 도구로, 입력 문서에서 의미를 최대한 유지하면서도 토큰 수를 줄여준다.</p>
<p>책에서는 실제로 LLMLingua를 활용하여 동일한 정보를 담고 있지만 압축된 프롬프트를 LLM에 넣고, 성능 및 비용을 개선하는 방법을 코드로 보여준다.</p>
<h4 id="autogen-microsoft">AutoGen (Microsoft)</h4>
<p>Microsoft가 개발한 <strong>AutoGen</strong>은 다중 에이전트 프레임워크의 대표적인 사례로, LangChain과는 다른 접근 방식을 제공한다.</p>
<p>AutoGen의 핵심은 에이전트가 사전에 정해진 프로세스를 따르는 것이 아니라, <strong>대화를 통해 문제를 협의하고 해결</strong>할 수 있도록 설계된 점이다. 에이전트들은 스스로 토론하며 어떤 에이전트가 어떤 작업을 수행할지 결정하고, 순차적 또는 병렬적으로 문제를 해결한다.</p>
<p>책에서는 간단한 AutoGen 사용법과 함께, AutoGen이 어떻게 에이전트 간의 자연스러운 협업을 지원하는지를 설명하고, 이를 활용한 RAG + Multi-Agent 시스템의 예시도 소개한다.</p>
<h3 id="ch10-대규모-언어-모델과-인공지능이-주도하는-과거-현재-미래-트렌드-분석--ch11-세계적-전문가들이-바라본-산업의-현재와-미래">CH10. 대규모 언어 모델과 인공지능이 주도하는 과거, 현재, 미래 트렌드 분석 &amp; CH11. 세계적 전문가들이 바라본 산업의 현재와 미래</h3>
<p>대규모 언어 모델의 발전을 논할 때 컴퓨팅 파워는 빠질 수 없는 이야기이다. 기존에는 <strong>무어의 법칙</strong>(2년마다 트랜지스터 수가 2배 증가)을 바탕으로 꾸준한 하드웨어 성능 향상을 기대했지만, 2020년대를 지나며 물리적 한계와 비용 문제로 의문이 제기되었다. 그럼에도 무어의 법칙은 여전히 산업계에서 중요한 기준점으로 작용하고 있다.</p>
<p>이와 동시에 <strong>Tensor Processing Unit(TPU)</strong>, GPU의 발전, 그리고 딥러닝 특화 하드웨어의 등장으로 LLM 훈련과 추론 환경은 크게 개선되었다. 특히 TPU와 GPU 클러스터를 활용한 <strong>클라우드 컴퓨팅</strong>의 확산으로, 개인이나 중소기업도 대규모 언어 모델을 실험하고 서비스할 수 있는 환경이 마련되었다.</p>
<h4 id="프롬프트-엔지니어링과-rag의-재조명">프롬프트 엔지니어링과 RAG의 재조명</h4>
<p>프롬프트 엔지니어링은 LLM을 활용한 애플리케이션 개발의 첫 번째 단계로 자리 잡았다. 그 뒤를 이어 <strong>RAG (Retrieval-Augmented Generation)</strong>이 재조명되고 있는데, 이는 검색된 정보와 언어 모델을 결합하여 보다 정확하고 신뢰성 있는 결과를 제공하기 위함이다.</p>
<p>RAG의 성능은 단순히 모델의 크기나 파라미터 수에 의존하는 것이 아니라, </p>
<ul>
<li><strong>검색된 데이터의 구조 설계</strong> </li>
<li><strong>Vector DBMS의 선택</strong></li>
<li><strong>임베딩 품질</strong></li>
</ul>
<p>등에 의해 크게 좌우된다.<br>여기서 임베딩은 텍스트 데이터를 벡터로 변환하는 과정으로, 손실 압축 메커니즘이기 때문에 어느 정도 정보 손실이 발생한다. 따라서, 효율적인 임베딩 설계가 RAG의 성능을 좌우한다.</p>
<h4 id="llm을-활용한-응용은-다음과-같은-복잡도-단계를-가진다">LLM을 활용한 응용은 다음과 같은 복잡도 단계를 가진다.</h4>
<ol>
<li><strong>프롬프트 엔지니어링</strong></li>
<li><strong>RAG</strong></li>
<li><strong>미세 조정(Fine-Tuning)</strong></li>
<li><strong>모델 재학습</strong></li>
<li><strong>사전학습(Pretraining)</strong></li>
</ol>
<p>각 단계로 갈수록 비용과 기술적 복잡성은 급격히 증가하지만, 그만큼 얻을 수 있는 커스터마이징의 폭도 커진다.</p>
<p>최근에는 이러한 워크플로우를 체계화한 <strong>LLMOps</strong>가 등장했다. LLMOps는 LLM의 개발, 배포, 모니터링, 버전 관리, 평가를 포함하는 LLM 기반 MLOps의 확장된 개념이다.</p>
<p>책의 마지막은 글로벌 기업의 AI 담당자들과의 인터뷰로 구성되어 있다. 그 중에서도 인상 깊었던 것은 이베이의 CAIO <strong>니잔 메켈-보브로브 박사</strong>의 발언이었다.</p>
<p>니잔 박사는 앞으로의 트렌드로 <strong><em>대규모 기초 모델 (LFM, Large Foundation Model)</em></strong> 로의 전환을 강조했다. 이는 특정 용도에만 국한된 모델이 아니라, 다수의 태스크를 포괄할 수 있는 범용적인 모델의 필요성을 뜻한다. 실제로 OpenAI, Google, Meta 역시 모두 LFM 중심으로 연구를 이어가고 있으며, 다중 언어 지원, 복합적인 reasoning, 멀티모달 학습 등으로 확장되고 있다.</p>
<p>팔란티어 CTO는 <strong>K-LLMs</strong> 즉, 특정 도메인 또는 조직에 맞게 커스텀된 LLM들의 활용 가능성을 강조했다. 앞으로는 단일 대규모 모델보다는 다양한 크기의 특화된 LLM들을 유기적으로 조합하는 전략이 중요해질 것으로 보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리눅스 - SIGTERM 과 SIGKILL ?!, 근데 Signal 을 곁들인 (ps. kill -9 만 쓰시나요?)]]></title>
            <link>https://velog.io/@qlgks1/%EB%A6%AC%EB%88%85%EC%8A%A4-SIGTERM-%EA%B3%BC-SIGKILL</link>
            <guid>https://velog.io/@qlgks1/%EB%A6%AC%EB%88%85%EC%8A%A4-SIGTERM-%EA%B3%BC-SIGKILL</guid>
            <pubDate>Sun, 23 Mar 2025 17:29:01 GMT</pubDate>
            <description><![CDATA[<p>[ 글의 목적: 리눅스 OS 에서 process 를 kill (IPC 중 signal 방식) 할때 SIGTERM, SIGKILL 동작에 대한 기록 ]</p>
<h1 id="sigterm과-sigkill에-대한-심층-분석">SIGTERM과 SIGKILL에 대한 심층 분석</h1>
<blockquote>
<p>습관적으로 <code>pkill -9 ...</code> 을 때리다가 어떻게 OS 는 process를 &quot;safe&quot; 한 방식으로 죽일 수 있을까 부터 시작해 UNIX/Linux 의 Signal IPC 까지 올라가서 러프하게 정리한 글이다. </p>
</blockquote>
<blockquote>
<p>한 번 상상해보자. python 으로 작성한 <code>a.py</code> 에 <code>while True</code> 가 있어도, <code>Ctrl + C</code> 한 방이면 <code>KeyboardInterrupt</code> 발생하면서 stop 되고 결국 프로세스는 죽는다. 이 흐름을 정말 A to Z 까지 알고 있는가? 이게 왜 중요한가? 우린 OS 의 &quot;프로세스&quot; 와 &quot;쓰레드&quot; 의 생명주기와 IPC 는 기억하지만, 이 흐름은 놓치고 있는 경우가 많다. <del>(근데 나도 몰랐다.)</del></p>
</blockquote>
<ul>
<li>썸네일은 gpt 와 claude 의 콜라보다. 우측 하단의 &quot;쾅&quot; 이 클로드인데, 역시 튜닝의 끝은 순정인가,, 잼민이 감성..</li>
</ul>
<h3 id="python-의-keyboardinterrupt-이어서">Python 의 <code>KeyboardInterrupt</code> 이어서..</h3>
<ol>
<li><p>위에 얘기한 흐름을 다시 정리해보자면, 우리는 shell 에서 python 을 실행한다. <code>python a.py</code> 와 같이. </p>
</li>
<li><p>해당 <code>a.py</code> 는 <code>python</code> 이라는 S/W 를 기반으로 runtime 이 구성되고, <code>python interpreter</code> 가 실행되며 <strong><em>foreground 프로세스로 실행</em></strong> 한다. </p>
</li>
<li><p>이 상태에서 Ctrl + C를 누르면, 터미널은 이를 특수 제어 문자(<strong><em><code>ETX - End of Text, 0x03</code></em></strong>)로 인식한다. (이는 python 에서 인식하는게 아니라 OS 에서 인식한다!!)</p>
</li>
<li><p>터미널은 이 키 입력(0x03)을 감지하면 foreground에서 실행 중인 프로세스(예: Python)에 <code>SIGINT</code> 시그널을 보낸다. 이건 <strong><em>POSIX 표준이자 리눅스/유닉스/macOS에서 동일하게 작동하는 규칙</em></strong> 이다. </p>
</li>
<li><p>이때 Python 인터프리터는 자체 등록한 <code>SIGINT</code> 핸들러를 통해 <code>KeyboardInterrupt</code> 예외를 발생시킨다. </p>
</li>
<li><p>Python은 인터프리터(ex - CPython 구현체)가 시작될 때 <code>signal</code> 모듈을 통해 자체적으로 <code>SIGINT</code> 에 대한 핸들러를 기본으로 등록해 두고 있다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f5af6536-250e-46a3-b4bf-04336c2ae03a/image.svg" alt=""></p>
<ul>
<li><p>이 예시외에도 <code>nginx</code> 같은 S/W level 의 web-server process 를 죽일때, <code>gunicorn</code> 이나 <code>apache &amp; tomcat</code> 으로 돌아가는 spring server 를 죽일때도 비슷한 흐름이다. </p>
</li>
<li><p>이제부터 <code>Signal</code> 에 대해서 좀 더 자세하게 알아보자. 가장 먼저 종료 signal 의 대표적인 예시인 <code>SIGTERM</code> 과 <code>SIGKILL</code> 차이점은 아래와 같다.</p>
</li>
</ul>
<h3 id="sigint-sigterm-sigkill-비교표">SIGINT, SIGTERM, SIGKILL 비교표</h3>
<ul>
<li>셋 다 프로세스를 멈추는 신호(Signal)이며, <code>SIGTERM, SIGKILL</code> 은 &quot;프로세스 종료&quot; 에 더 가깝다. 여기서는 사실 <code>SIGTERM</code>, <code>SIGKILL</code> 에 대해 더 자세히 알아보고자 한다. </li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th><strong>SIGINT (2)</strong></th>
<th><strong>SIGTERM (15)</strong></th>
<th><strong>SIGKILL (9)</strong></th>
</tr>
</thead>
<tbody><tr>
<td>의미</td>
<td>사용자 중단 요청 (Interrupt)</td>
<td>정상 종료 요청 (Termination Request)</td>
<td>강제 종료 (Forced Kill)</td>
</tr>
<tr>
<td>발생 시점</td>
<td>주로 사용자가 Ctrl+C 입력</td>
<td>일반적으로 <code>kill</code> 명령 사용 시 기본값</td>
<td><code>kill -9</code> 또는 시스템 강제 종료 시</td>
</tr>
<tr>
<td>기본 동작</td>
<td>프로세스에 인터럽트 요청 → 정상적인 종료 처리 실행</td>
<td>종료 핸들러 실행 기회 제공</td>
<td>즉시 강제 종료</td>
</tr>
<tr>
<td>프로세스 대응</td>
<td>무시 가능, 시그널 핸들러로 처리 가능</td>
<td>무시 가능, 시그널 핸들러로 처리 가능</td>
<td>무시 불가능 (non-maskable, non-catchable)</td>
</tr>
<tr>
<td>우선순위</td>
<td>사용자 개입 수준</td>
<td>일반 종료 시퀀스</td>
<td>최우선, 무조건 종료</td>
</tr>
<tr>
<td>리소스 정리</td>
<td>가능 (시그널 핸들러에서 정리 가능)</td>
<td>가능 (핸들러 내부에서 리소스 정리 수행 가능)</td>
<td>불가능 (리소스 누수 위험 있음)</td>
</tr>
<tr>
<td>커널 개입</td>
<td>커널이 시그널 전달 → 프로세스 핸들링</td>
<td>커널이 시그널 전달 → 프로세스 핸들링</td>
<td>커널이 직접 종료 (컨텍스트 무시하고 즉시 종료)</td>
</tr>
<tr>
<td>사용 예시</td>
<td>사용자가 <code>Ctrl+C</code> 입력</td>
<td><code>kill PID</code> (기본값)</td>
<td><code>kill -9 PID</code></td>
</tr>
<tr>
<td>시스템 콜</td>
<td><code>kill(pid, SIGINT)</code></td>
<td><code>kill(pid, SIGTERM)</code></td>
<td><code>kill(pid, SIGKILL)</code></td>
</tr>
<tr>
<td>프로세스 상태 전이</td>
<td>Running → Signal Handling → (계속/종료)</td>
<td>Running/Waiting → Terminating → Zombie → Removed</td>
<td>Running/Waiting → Zombie → Removed</td>
</tr>
<tr>
<td>자식 프로세스 처리</td>
<td>일반적으로 전파되지 않음</td>
<td>애플리케이션 설정에 따라 자식에게도 전파 가능</td>
<td>자식 프로세스 포함 전체 강제 종료 (kill -9 트리 구조 시)</td>
</tr>
<tr>
<td>비동기성</td>
<td>처리 핸들링 가능 (시그널 핸들러 등록)</td>
<td>비동기적으로 처리 가능</td>
<td>즉시 처리됨, 핸들링 불가</td>
</tr>
</tbody></table>
<ul>
<li><code>SIGTERM</code> 은 &quot;정중한 종료 요청&quot;으로, 프로세스에게 &quot;작업을 마무리하고 종료해달라&quot;는 의미이며, <code>SIGKILL</code> 은 &quot;즉각적인 강제 종료 명령&quot;으로 프로세스에게 어떤 기회도 주지 않고 즉시 종료시킨다.</li>
</ul>
<hr>
<h2 id="1-프로세스-간-통신ipc과-시그널">1. 프로세스 간 통신(IPC)과 시그널</h2>
<ul>
<li>심플하게 IPC 와 종류, 특히 &quot;시그널&quot; 에 대해 살펴보자!</li>
<li><a href="https://en.wikipedia.org/wiki/Signal_(IPC)">위키피디아 - Signal(IPC)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/signal.7.html">리눅스 man - signal(7) — Linux manual page</a></li>
</ul>
<h3 id="1-ipc의-개념과-종류">1) IPC의 개념과 종류</h3>
<ul>
<li>프로세스 간 통신(Inter-Process Communication, IPC)은 서로 다른 프로세스가 데이터를 교환하고 동기화하는 메커니즘이다. UNIX/Linux 시스템에서 IPC의 주요 방식은 다음과 같다.</li>
</ul>
<ol>
<li><p><strong>파이프(Pipe)와 명명된 파이프(Named Pipe)</strong></p>
<ul>
<li>단방향 데이터 흐름을 제공</li>
<li>예: <code>|</code> 연산자를 사용한 파이프라인</li>
</ul>
</li>
<li><p><strong>메시지 큐(Message Queue)</strong></p>
<ul>
<li>비동기적 메시지 전달 시스템</li>
<li><code>msgget()</code>, <code>msgsnd()</code>, <code>msgrcv()</code> 시스템 콜 사용</li>
</ul>
</li>
<li><p><strong>공유 메모리(Shared Memory)</strong></p>
<ul>
<li>여러 프로세스가 동일한 메모리 영역에 접근</li>
<li>가장 빠른 IPC 방식</li>
</ul>
</li>
<li><p><strong>세마포어(Semaphore)</strong></p>
<ul>
<li>공유 자원에 대한 접근 제어</li>
<li>상호 배제(mutual exclusion)를 보장</li>
</ul>
</li>
<li><p><strong>소켓(Socket)</strong></p>
<ul>
<li>네트워크를 통한 프로세스 간 통신</li>
<li>로컬 및 원격 통신 모두 지원</li>
</ul>
</li>
<li><p><strong>시그널(Signal)</strong></p>
<ul>
<li>비동기적인 이벤트 알림</li>
<li>프로세스 제어 및 예외 상황 처리에 사용</li>
</ul>
</li>
</ol>
<h3 id="2-시그널의-특징과-역할">2) 시그널의 특징과 역할</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/4699e4e8-eccc-47cc-9b3e-fb8913677f44/image.png" alt=""></p>
<ul>
<li>시그널은 UNIX/Linux 시스템에서 가장 오래된 IPC 메커니즘 중 하나로, 다음과 같은 특징이 있다.<ul>
<li><strong>비동기성</strong>: 프로세스가 어떤 작업을 하고 있든 상관없이 전달 가능</li>
<li><strong>경량성</strong>: 시그널은 단순한 정수 값으로, 오버헤드가 적음</li>
<li><strong>이벤트 드리븐</strong>: 특정 이벤트 발생 시 프로세스에 알림을 제공</li>
<li><strong>제한된 정보</strong>: 시그널 번호 외에 추가 데이터를 전달하기 어려움</li>
</ul>
</li>
</ul>
<h3 id="3-주요-시그널-종류">3) 주요 시그널 종류</h3>
<table>
<thead>
<tr>
<th>신호 이름</th>
<th>번호</th>
<th>설명</th>
<th>기본 동작</th>
<th>원인</th>
</tr>
</thead>
<tbody><tr>
<td>SIGHUP</td>
<td>1</td>
<td>행업(Hangup)</td>
<td>종료</td>
<td>제어 터미널이 종료될 때</td>
</tr>
<tr>
<td>SIGINT</td>
<td>2</td>
<td>인터럽트</td>
<td>종료</td>
<td>Ctrl+C 키 입력</td>
</tr>
<tr>
<td>SIGQUIT</td>
<td>3</td>
<td>종료 및 코어 덤프</td>
<td>코어 덤프와 함께 종료</td>
<td>Ctrl+\ 키 입력</td>
</tr>
<tr>
<td>SIGILL</td>
<td>4</td>
<td>잘못된 명령어</td>
<td>코어 덤프와 함께 종료</td>
<td>잘못된 CPU 명령 실행</td>
</tr>
<tr>
<td>SIGTRAP</td>
<td>5</td>
<td>트랩</td>
<td>코어 덤프와 함께 종료</td>
<td>디버깅용 트랩</td>
</tr>
<tr>
<td>SIGABRT</td>
<td>6</td>
<td>중단</td>
<td>코어 덤프와 함께 종료</td>
<td>abort() 함수 호출</td>
</tr>
<tr>
<td>SIGFPE</td>
<td>8</td>
<td>부동 소수점 예외</td>
<td>코어 덤프와 함께 종료</td>
<td>0으로 나누기 등 연산 오류</td>
</tr>
<tr>
<td>SIGKILL</td>
<td>9</td>
<td>강제 종료</td>
<td>종료 (무시 불가)</td>
<td>관리자 권한으로 강제 종료</td>
</tr>
<tr>
<td>SIGUSR1</td>
<td>10</td>
<td>사용자 정의 1</td>
<td>종료</td>
<td>사용자/애플리케이션 정의</td>
</tr>
<tr>
<td>SIGSEGV</td>
<td>11</td>
<td>세그멘테이션 오류</td>
<td>코어 덤프와 함께 종료</td>
<td>잘못된 메모리 참조</td>
</tr>
<tr>
<td>SIGUSR2</td>
<td>12</td>
<td>사용자 정의 2</td>
<td>종료</td>
<td>사용자/애플리케이션 정의</td>
</tr>
<tr>
<td>SIGPIPE</td>
<td>13</td>
<td>파이프 깨짐</td>
<td>종료</td>
<td>닫힌 파이프에 쓰기 시도</td>
</tr>
<tr>
<td>SIGALRM</td>
<td>14</td>
<td>알람</td>
<td>종료</td>
<td>alarm() 함수로 설정된 타이머 만료</td>
</tr>
<tr>
<td>SIGTERM</td>
<td>15</td>
<td>종료</td>
<td>종료</td>
<td>kill 명령의 기본값</td>
</tr>
<tr>
<td>SIGCHLD</td>
<td>17</td>
<td>자식 상태 변경</td>
<td>무시</td>
<td>자식 프로세스가 종료되거나 중지될 때</td>
</tr>
<tr>
<td>SIGCONT</td>
<td>18</td>
<td>계속 실행</td>
<td>중단된 프로세스 재개</td>
<td>중단된 프로세스를 계속 실행</td>
</tr>
<tr>
<td>SIGSTOP</td>
<td>19</td>
<td>중지</td>
<td>프로세스 중지 (무시 불가)</td>
<td>프로세스 중지</td>
</tr>
<tr>
<td>SIGTSTP</td>
<td>20</td>
<td>터미널 중지</td>
<td>프로세스 중지</td>
<td>Ctrl+Z 키 입력</td>
</tr>
<tr>
<td>SIGTTIN</td>
<td>21</td>
<td>터미널 입력</td>
<td>프로세스 중지</td>
<td>백그라운드 프로세스가 터미널에서 읽기 시도</td>
</tr>
<tr>
<td>SIGTTOU</td>
<td>22</td>
<td>터미널 출력</td>
<td>프로세스 중지</td>
<td>백그라운드 프로세스가 터미널에 쓰기 시도</td>
</tr>
</tbody></table>
<ul>
<li><code>SIGUSR1</code> 로 재미있는 걸 해볼 수 있지 않을까?</li>
</ul>
<hr>
<h2 id="2-os-관점에서-sigterm-과-sigkill-의-차이">2. OS 관점에서 <code>SIGTERM</code> 과 <code>SIGKILL</code> 의 차이</h2>
<blockquote>
<p>정확하겐 POSIX 표준에 정의된 시그널 관련 시스템 콜과 라이브러리 함수를 해당 레포에서 직접 확인할 수 있다. - <a href="https://github.com/torvalds/linux?tab=readme-ov-file">https://github.com/torvalds/linux?tab=readme-ov-file</a></p>
</blockquote>
<h3 id="1-sigterm-15---정상적인-종료-요청">1) SIGTERM (15) - 정상적인 종료 요청</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/f4e669d8-2802-456c-9e82-82a6583f7b7c/image.svg" alt=""></p>
<ol>
<li><p><strong>시그널 전송 과정</strong></p>
<ul>
<li>사용자가 <code>kill PID</code>를 실행하면, 커널의 <code>kill()</code> 시스템 콜이 호출된다.</li>
<li>시스템 콜 인터페이스를 통해 커널 공간으로 전환된다.</li>
<li>커널은 프로세스 테이블에서 PID에 해당하는 프로세스를 찾는다.</li>
</ul>
</li>
<li><p><strong>시그널 전달 및 큐잉</strong></p>
<ul>
<li>커널은 해당 프로세스의 <code>task_struct</code> 내의 시그널 마스크와 핸들러 정보를 확인한다.</li>
<li><code>SIGTERM</code> 시그널이 해당 프로세스의 시그널 큐(sigqueue)에 추가된다.</li>
<li>프로세스의 시그널 큐는 보류 중인 시그널(pending signals)을 관리한다.</li>
</ul>
</li>
<li><p><strong>컨텍스트 스위칭과 시그널 처리</strong></p>
<ul>
<li>다음 스케줄링 시점에 프로세스가 CPU를 획득하면, 커널은 해당 프로세스의 시그널 큐를 검사한다.</li>
<li><code>SIGTERM</code> 이 발견되면, 커널은 다음을 확인한다:<ul>
<li>시그널이 블록되었는지 (프로세스가 <code>sigprocmask()</code>로 블록했는지)</li>
<li>사용자 정의 핸들러가 등록되었는지 (<code>sigaction()</code>으로 설정)</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>핸들러 실행</strong></p>
<ul>
<li>만약 <code>SIGTERM</code> 핸들러가 등록되어 있다면<ul>
<li>커널은 사용자 공간의 스택에 시그널 프레임을 추가한다.</li>
<li>프로세스의 실행 컨텍스트를 시그널 핸들러로 변경한다.</li>
<li>프로세스는 핸들러 내에서 정리 작업을 수행한다:<ul>
<li>열린 파일 디스크립터 닫기 (<code>close()</code>)</li>
<li>데이터베이스 커넥션 종료</li>
<li>임시 파일 삭제</li>
<li>로그 파일에 종료 메시지 기록</li>
<li>자식 프로세스에게 종료 시그널 전파</li>
</ul>
</li>
</ul>
</li>
<li>시그널 핸들러가 완료된 후, 프로세스는 인터럽트되었던 지점(또는 시그널 핸들러 반환 후의 지점)으로 돌아간다!</li>
</ul>
</li>
<li><p><strong>프로세스 종료</strong></p>
<ul>
<li>핸들러 실행 후, 애플리케이션은 일반적으로 <code>exit()</code> 시스템 콜을 호출하여 정상 종료한다.</li>
<li>만약 핸들러가 없거나 시그널이 무시되지 않았다면, 기본 동작(default action)인 종료가 수행된다.</li>
<li>프로세스 종료 시 다음 과정이 진행된다:<ul>
<li>모든 스레드 종료</li>
<li>프로세스 리소스 정리 (메모리, 파일 핸들 등)</li>
<li>부모 프로세스에 SIGCHLD 시그널 전송</li>
<li>프로세스 상태가 좀비(zombie) 상태로 전환</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>좀비 프로세스 정리</strong></p>
<ul>
<li>부모 프로세스가 <code>wait()</code> 또는 <code>waitpid()</code>를 호출하면, 자식의 종료 상태를 회수하고 프로세스 테이블 엔트리가 완전히 제거된다.</li>
<li>만약 부모가 <code>wait()</code>를 호출하지 않으면, 좀비 프로세스가 남게 된다.</li>
</ul>
</li>
</ol>
<h3 id="2-sigkill-9---강제-종료">2) SIGKILL (9) - 강제 종료</h3>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9da684c1-9456-4b5f-8b6f-bd68d1180dd2/image.svg" alt=""></p>
<ol>
<li><p><strong>시그널 전송 과정</strong>:</p>
<ul>
<li>사용자가 <code>kill -9 PID</code>를 실행하면, 역시 <code>kill()</code> 시스템 콜이 호출된다.</li>
<li>커널 공간으로 전환되어 프로세스 테이블을 검색한다.</li>
</ul>
</li>
<li><p><strong>특수 처리</strong></p>
<ul>
<li><strong><em><code>SIGKILL</code> 은 특별한 시그널로</em></strong> 커널은 이를 프로세스의 시그널 큐에 넣지 않는다.</li>
<li>대신, 즉시 프로세스 종료 절차를 시작한다! 프로세스의 시그널 마스크나 핸들러 설정을 확인하지 않는다! (무시할 수 없는 시그널)</li>
</ul>
</li>
<li><p><strong>강제 종료 프로세스</strong>:</p>
<ul>
<li>커널은 프로세스의 모든 스레드에 대해 즉시 실행을 중단시킨다.</li>
<li><code>do_exit()</code> 커널 함수가 호출되어 프로세스 종료 절차를 진행한다:<ul>
<li>프로세스의 가상 메모리 공간을 해제한다.</li>
<li>열린 파일 디스크립터를 강제로 닫는다.</li>
<li>시스템 V IPC 리소스를 해제한다.</li>
<li>프로세스가 소유한 세마포어를 해제한다.</li>
<li>프로세스 상태를 <code>EXIT_ZOMBIE</code> 로 변경한다.</li>
<li>부모 프로세스에 <code>SIGCHLD</code> 시그널을 보낸다.</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>사용자 공간 코드 실행 없음</strong></p>
<ul>
<li><code>SIGKILL</code> 은 프로세스의 사용자 공간 코드가 실행될 기회를 전혀 주지 않는다.</li>
<li>이는 애플리케이션이 자신의 리소스를 정리할 수 없음을 의미한다.</li>
<li>커널만이 프로세스의 커널 리소스(파일 핸들, 메모리 등)를 정리한다.</li>
<li>PS) 그러니까 <code>SIGTERM</code> call 이 더 안전한 종료 접근법이다!!</li>
</ul>
</li>
<li><p><strong>좀비 프로세스와 후속 처리</strong>:</p>
<ul>
<li><code>SIGTERM</code> 과 마찬가지로, 프로세스는 부모가 <code>wait()</code>를 호출할 때까지 좀비 상태로 남는다.</li>
<li>부모 프로세스가 이미 종료된 경우, init 프로세스(PID 1)가 고아 프로세스를 자식으로 대려와 좀비를 정리한다.</li>
</ul>
</li>
</ol>
<h3 id="3-두-시그널의-핵심-차이점">3) 두 시그널의 핵심 차이점</h3>
<ol>
<li><p><strong>실행 컨텍스트</strong></p>
<ul>
<li><code>SIGTERM</code>: 프로세스의 사용자 공간 코드(시그널 핸들러)가 실행된다.</li>
<li><code>SIGKILL</code>: 전적으로 커널 공간에서 처리되며, 사용자 코드는 실행되지 않는다.</li>
</ul>
</li>
<li><p><strong>리소스 정리</strong></p>
<ul>
<li><code>SIGTERM</code>: 애플리케이션이 자신의 리소스(임시 파일, 네트워크 연결, 데이터베이스 트랜잭션 등)를 정리할 수 있다.</li>
<li><code>SIGKILL</code>: 애플리케이션 수준의 리소스 정리가 불가능하며, 커널 수준의 리소스만 정리된다.</li>
</ul>
</li>
<li><p><strong>실행 시간</strong></p>
<ul>
<li><code>SIGTERM</code>: 핸들러 실행과 정리 과정으로 인해 종료에 시간이 걸릴 수 있다.</li>
<li><code>SIGKILL</code>: 즉시 종료되어 지연이 최소화된다.</li>
</ul>
</li>
<li><p><strong>안전성</strong></p>
<ul>
<li><code>SIGTERM</code>: 정상적인 종료 절차를 통해 데이터 무결성을 보장할 가능성이 높다.</li>
<li><code>SIGKILL</code>: 데이터 손실이나 불일치, 네트워크 연결 문제 등을 야기할 수 있다.</li>
</ul>
</li>
</ol>
<ul>
<li>사실 이 때문에 두 시그널을 두고 다양한 밈들이 있다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/9b42fa95-a6a2-48af-ac6b-502d4cf7b4c5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/qlgks1/post/74fccf37-943e-4bf2-8c3c-55777575dbd7/image.png" alt=""></p>
<h3 id="4-우아한-종료graceful-shutdown의-중요성">4) 우아한 종료(Graceful Shutdown)의 중요성</h3>
<ol>
<li><p><strong>데이터 무결성 보장</strong></p>
<ul>
<li>불완전한 작업이 없도록 보장</li>
<li>트랜잭션 완료 또는 롤백</li>
<li>디스크 버퍼 플러시</li>
</ul>
</li>
<li><p><strong>클라이언트 영향 최소화</strong></p>
<ul>
<li>기존 연결의 적절한 종료</li>
<li>오류 메시지 대신 정상 응답 제공</li>
<li>세션 데이터 보존</li>
</ul>
</li>
<li><p><strong>분산 시스템 일관성</strong></p>
<ul>
<li>클러스터 노드 간 상태 동기화</li>
<li>리더 선출 또는 장애 조치 트리거</li>
<li>로드 밸런서에서 노드 제거</li>
</ul>
</li>
</ol>
<ul>
<li>그러니 우리가 만약 응용 프로그램을 직접 만들게 된다면, 이 &quot;Graceful Shutdown&quot; 을 위한 <code>SIGTERM</code> 핸들러를 직접 추가해서 처리하는 방향으로 접근해보자!</li>
</ul>
<h3 id="5-종료-시간-제한timeout의-설정">5) 종료 시간 제한(Timeout)의 설정</h3>
<ul>
<li>대부분의 시스템에서는 우아한 종료에 시간 제한을 두는 것이 중요하다.</li>
</ul>
<ol>
<li><p><strong>Kubernetes의 종료 프로세스</strong></p>
<ul>
<li>Pod가 <code>SIGTERM</code> 을 받은 후 <code>terminationGracePeriodSeconds</code> 동안 기다린다 (기본 30초).</li>
<li>시간이 초과되면 <code>SIGKILL</code> 을 보내 강제 종료한다.</li>
</ul>
</li>
<li><p><strong>Systemd의 종료 프로세스</strong></p>
<ul>
<li><code>TimeoutStopSec</code> 설정으로 종료 타임아웃을 지정 (기본 90초).</li>
<li>타임아웃 후 <code>SIGKILL</code> 을 보낸다.</li>
</ul>
</li>
<li><p><strong>Docker의 종료 프로세스</strong></p>
<ul>
<li><code>docker stop</code> 명령은 컨테이너에 SIGTERM을 보낸다.</li>
<li>기본적으로 10초 후 <code>SIGKILL</code> 을 보낸다.</li>
<li><code>docker stop --time=&lt;seconds&gt;</code> 옵션으로 타임아웃 조정 가능.</li>
</ul>
</li>
<li><p><strong>적절한 타임아웃 설정 전략</strong></p>
<ul>
<li>애플리케이션의 일반적인 정리 시간을 측정</li>
<li>최악의 경우 시나리오(많은 연결, 큰 트랜잭션 등) 고려</li>
<li>마진을 추가하되, 너무 길지 않게 설정</li>
<li>배포 중단 시 전체 시스템 영향 고려</li>
</ul>
</li>
</ol>
<blockquote>
<p>그러니 이제 <code>SIGTERM</code> 을 위해 <strong><em><code>kill PID</code></em></strong> 을 기본으로 사용하되, 경우에 따라 <strong><em><code>kill -9 PID</code></em></strong> 를 사용하는게 어떨까? <del>to. 스스로에게...</del></p>
</blockquote>
<hr>
<h2 id="4-shell-script로-실습">4. shell script로 실습!</h2>
<h3 id="1-sigterm을-먼저-사용하기">1) SIGTERM을 먼저 사용하기</h3>
<ul>
<li>효과적인 프로세스 종료를 위한 최선의 방법!</li>
</ul>
<ol>
<li><strong>단계적 접근</strong></li>
</ol>
<pre><code class="language-bash"># 1. 먼저 SIGTERM으로 정상 종료 시도
kill &lt;PID&gt;

# 2. 일정 시간 대기 (5-10초)
sleep 10

# 3. 프로세스가 여전히 실행 중인지 확인
if ps -p &lt;PID&gt; &gt; /dev/null; then
    echo &quot;Process still running, sending SIGKILL...&quot;
    kill -9 &lt;PID&gt;
else
    echo &quot;Process terminated gracefully.&quot;
fi</code></pre>
<ol start="2">
<li><strong>스크립트 자동화</strong></li>
</ol>
<pre><code class="language-bash">#!/bin/bash

terminate_with_timeout() {
    local pid=$1
    local timeout=${2:-30}  # 기본 30초 타임아웃

    # 프로세스가 존재하는지 확인
    if ! ps -p $pid &gt; /dev/null; then
    echo &quot;Process $pid does not exist.&quot;
    return 0
    fi

    # SIGTERM 전송
    echo &quot;Sending SIGTERM to process $pid...&quot;
    kill $pid

    # 타임아웃 내에 종료되는지 확인
    local count=0
    while ps -p $pid &gt; /dev/null &amp;&amp; [ $count -lt $timeout ]; do
    sleep 1
    count=$((count + 1))
    echo &quot;Waiting for process to terminate: $count/$timeout seconds&quot;
    done

    # 여전히 실행 중이면 SIGKILL 전송
    if ps -p $pid &gt; /dev/null; then
    echo &quot;Process still running after $timeout seconds, sending SIGKILL...&quot;
    kill -9 $pid
    return 1
    else
    echo &quot;Process terminated gracefully.&quot;
    return 0
    fi
}

# 사용 예: terminate_with_timeout &lt;PID&gt; [timeout_in_seconds]</code></pre>
<h3 id="2-그러니-다시-정리하는-sigkill을-남용하지-말아야-하는-이유">2) 그러니, 다시 정리하는 SIGKILL을 남용하지 말아야 하는 이유!</h3>
<ul>
<li>여러분들의 응용프로그램에게 <code>SIGKILL</code> 의 남용이 초래할 수 있는 문제들!</li>
</ul>
<ol>
<li><p><strong>데이터 일관성 문제</strong></p>
<ul>
<li>데이터베이스 트랜잭션 중단으로 인한 데이터 불일치</li>
<li>파일 쓰기 작업 중 강제 종료로 인한 파일 손상</li>
<li>캐시와 영구 저장소 간의 불일치 발생</li>
</ul>
</li>
<li><p><strong>리소스 누수</strong></p>
<ul>
<li>임시 파일이 제거되지 않음</li>
<li>공유 메모리 세그먼트가 정리되지 않음</li>
<li>네트워크 포트가 적절히 해제되지 않음</li>
<li>외부 리소스 락(lock)이 해제되지 않음</li>
</ul>
</li>
<li><p><strong>분산 시스템 문제</strong></p>
<ul>
<li>클러스터에 종료 통지가 전송되지 않음</li>
<li>리더 선출 프로세스가 비정상적으로 트리거됨</li>
<li>다른 노드와의 통신이 정상적으로 종료되지 않음</li>
</ul>
</li>
<li><p><strong>실제 사례 분석</strong></p>
<ul>
<li>데이터베이스 서버의 SIGKILL 종료는 WAL(Write-Ahead Log) 손상을 초래할 수 있음</li>
<li>메시지 큐의 SIGKILL 종료는 메시지 중복 처리나 유실을 초래할 수 있음</li>
<li>분산 락 관리자의 SIGKILL 종료는 &quot;뇌 분할(split-brain)&quot; 현상을 초래할 수 있음</li>
</ul>
</li>
</ol>
<h3 id="3-복잡한-애플리케이션의-종료-전략">3) 복잡한 애플리케이션의 종료 전략</h3>
<ul>
<li>다중 노드 등의 복잡한 형태, 조금 더 큰 규모의 시스템에서의 안전한 종료 전략은 자식 부터 부모로 올라오거나 치명적인 것에서 덜 치명적인 순서로 접근하는게 필요하다!</li>
</ul>
<ol>
<li><p><strong>다중 계층 애플리케이션</strong></p>
<ul>
<li>종료 순서가 중요함: 클라이언트 계층 → 애플리케이션 계층 → 데이터 계층</li>
<li>각 계층은 다른 종료 유예 시간이 필요할 수 있음</li>
</ul>
</li>
<li><p><strong>리더-팔로워 시스템</strong></p>
<ul>
<li>팔로워 노드 먼저 종료</li>
<li>리더 노드는 새로운 리더 선출 후 종료</li>
<li>리더 변경 사항이 모든 노드에 전파된 후 완전 종료</li>
</ul>
</li>
<li><p><strong>장기 실행 작업이 있는 시스템</strong></p>
<ul>
<li>진행 중인 작업의 체크포인트 생성</li>
<li>작업 큐의 적절한 상태 저장</li>
<li>재시작 시 중단된 지점부터 계속할 수 있는 메커니즘 구현</li>
</ul>
</li>
<li><p><strong>데이터베이스 종료</strong></p>
<ul>
<li>트랜잭션 커밋 또는 롤백</li>
<li>버퍼된 데이터 디스크에 쓰기</li>
<li>체크포인트 생성</li>
<li>메타데이터 업데이트</li>
</ul>
</li>
</ol>
<hr>
<h2 id="출처">출처</h2>
<h3 id="1-리눅스-시그널-관련-문서">1) 리눅스 시그널 관련 문서</h3>
<ul>
<li>리눅스 매뉴얼 페이지<ul>
<li><code>man 7 signal</code></li>
<li><code>man 2 kill</code></li>
<li><code>man 2 sigaction</code></li>
<li><code>man 3 signal</code></li>
</ul>
</li>
<li>리눅스 커널 소스 코드<ul>
<li><code>kernel/signal.c</code></li>
<li><code>include/linux/signal.h</code></li>
</ul>
</li>
<li><a href="https://en.wikipedia.org/wiki/Signal_(IPC)">위키피디아 - Signal(IPC)</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/signal.7.html">리눅스 man - signal(7) — Linux manual page</a></li>
<li><a href="https://linuxhandbook.com/sigterm-vs-sigkill/">리눅스 핸드북 sigterm vs sigkill</a></li>
</ul>
<h3 id="2-관련-표준-및-사양">2) 관련 표준 및 사양</h3>
<ul>
<li>POSIX.1-2017 시그널 사양 - <a href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html">https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html</a></li>
<li>시그널 안전 함수 목록 - <a href="https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03">https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>