<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>l_cloud.log</title>
        <link>https://velog.io/</link>
        <description>내가 배운 것 정리</description>
        <lastBuildDate>Thu, 21 May 2026 03:00:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>l_cloud.log</title>
            <url>https://velog.velcdn.com/images/l_cloud/profile/f45a9b7f-166a-407f-b24d-3d72028b2c75/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. l_cloud.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/l_cloud" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[NVIDIA와 GPU]]></title>
            <link>https://velog.io/@l_cloud/NVIDIA%EB%8A%94-GPU%EB%A7%8C-%ED%8C%94%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</link>
            <guid>https://velog.io/@l_cloud/NVIDIA%EB%8A%94-GPU%EB%A7%8C-%ED%8C%94%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</guid>
            <pubDate>Thu, 21 May 2026 03:00:31 GMT</pubDate>
            <description><![CDATA[<h1 id="nvidia는-gpu만-팔지-않는다--cpu-백엔드-개발자가-본-ai-인프라">NVIDIA는 GPU만 팔지 않는다 — CPU 백엔드 개발자가 본 AI 인프라</h1>
<blockquote>
<p>본 글은 CPU 위주의 백엔드 서버를 만들어 본 경험은 있지만, GPU 프로그래밍이나 AI 인프라는 처음 접하는 독자를 대상으로 합니다. 복잡한 CUDA 코드는 다루지 않으며, NVIDIA가 파는 것이 하드웨어 관점에서 정확히 무엇인지 — 단일 칩에서 출발해 랙 단위 슈퍼컴퓨터, 그리고 실적표 해석까지 — 한 호흡에 정리합니다.</p>
</blockquote>
<h2 id="nvidia-그리고-gpu">NVIDIA, 그리고 GPU</h2>
<p>NVIDIA를 한 문장으로 정의하면 어떻게 될까요? &quot;GPU 만드는 회사&quot;라고 답하기 쉽습니다. 게임할 때 쓰는 그래픽카드, 그걸 데이터센터용으로 크게 만드는 회사 정도로요.</p>
<p>그런데 가장 최근 분기 실적표를 보면 어색한 부분이 있습니다.</p>
<pre><code>Data Center Compute     : $60.4B   YoY +77%
Data Center Networking  : $14.8B   YoY +199%
Edge Computing          : $6.4B    YoY +29%</code></pre><p>GPU 회사가 네트워킹으로만 한 분기 14.8B 달러를 벌고, 그게 1년 만에 세 배가 되었습니다. <strong>그래픽카드 회사라는 정의로는 설명이 안 되는 숫자</strong>입니다.</p>
<p>이 글은 이 위화감에서 출발합니다. 백엔드 개발자가 익숙한 CPU 멘탈 모델에서 출발해서, &quot;GPU 한 장 → CPU↔GPU 통신 → 랙 안 묶기 → 랙 사이 묶기 → 실적표 재해석&quot; 순서로 따라갑니다. 다 읽고 나면 NVIDIA가 왜 칩 회사가 아니라 <strong>랙 운영체제 + 인프라 회사</strong>가 되었는지가 보일 겁니다.</p>
<hr>
<h2 id="1장-gpu는-그래픽카드가-아니다">1장. GPU는 그래픽카드가 아니다</h2>
<h3 id="gpu의-실체-거대한-병렬-가속기">GPU의 실체: 거대한 병렬 가속기</h3>
<p>&quot;GPU&quot;라는 이름은 Graphics Processing Unit의 약자입니다. 90년대~2000년대 초까지는 진짜로 그랬습니다. 모니터에 픽셀을 그리려고 만든 칩이었죠. 하지만 지금 NVIDIA가 데이터센터에 파는 칩은 그래픽과는 사실상 무관합니다.</p>
<p>그렇다면 정확히 무엇일까요?</p>
<blockquote>
<p><strong>GPU는 수만 개의 단순한 연산기와, 그것을 먹일 만큼 미친 대역폭의 메모리를 한 패키지에 묶은 데이터 병렬 가속기다.</strong></p>
</blockquote>
<p>세 부분으로 나눠 보겠습니다. 수만 개의 단순한 연산기(SIMT), 행렬 곱 전용 회로(Tensor Core), 그리고 그걸 굶기지 않을 메모리(HBM)입니다.</p>
<h3 id="첫-번째-메커니즘-simt--한-명령으로-32명을-동시에-굴린다">첫 번째 메커니즘: SIMT — 한 명령으로 32명을 동시에 굴린다</h3>
<p>서버 개발자에게 &quot;병렬&quot;이라고 하면 보통 떠올리는 그림이 있습니다.</p>
<ul>
<li>멀티코어 CPU 16개에 스레드 16개를 띄운다</li>
<li>각 스레드는 <strong>서로 다른 함수</strong>를 실행할 수 있다</li>
<li>OS 스케줄러가 컨텍스트 스위칭으로 돌려가며 굴린다</li>
</ul>
<p>이걸 MIMD(Multiple Instruction, Multiple Data)라고 부릅니다. 각 코어가 독립적으로 다른 일을 합니다. CPU의 병렬은 이 모델입니다.</p>
<p>GPU는 다릅니다. 같은 명령어를 수십~수만 개의 데이터에 한꺼번에 적용합니다. 이 모델을 NVIDIA는 <strong>SIMT (Single Instruction, Multiple Threads)</strong> 라고 부릅니다.</p>
<ul>
<li>32개 스레드가 묶여 한 단위가 됩니다. 이 묶음을 <strong>Warp</strong>라고 부릅니다.</li>
<li>한 Warp 안의 32개 스레드는 <strong>항상 같은 명령어</strong>를 같은 사이클에 실행합니다.</li>
<li>단지 각 스레드가 보는 데이터(레지스터 값)는 다릅니다.</li>
</ul>
<p>비유하자면 이렇습니다. CPU는 회사원 16명이 각자 다른 보고서를 쓰는 사무실입니다. GPU는 군대 제식훈련에 가깝습니다. &quot;우향우!&quot;라는 한 마디 명령으로 분대원 32명이 동시에 같은 동작을 하는데, 발을 디디는 위치(데이터)는 각자 다릅니다.</p>
<p>왜 이렇게 만들었을까요? <strong>하나의 명령어 디코더로 32개 연산을 굴릴 수 있으면, 같은 트랜지스터 예산으로 훨씬 많은 연산을 욱여넣을 수 있기 때문</strong>입니다. CPU 코어 하나에 들어가는 분기 예측기·Out-of-Order 엔진·캐시 같은 &quot;똑똑한 회로&quot;의 비중을 다 빼버리고, 그 자리에 연산기를 더 박는 전략이죠.</p>
<p>대신 대가가 있습니다.</p>
<ul>
<li><code>if</code> 분기에서 32개 스레드가 갈라지면(divergence), 양쪽 분기를 직렬로 다 돌고 각자 마스킹합니다. GPU는 <code>if</code>를 싫어합니다.</li>
<li>코어 하나하나가 멍청해서, 한 스레드 단위로 보면 CPU보다 훨씬 느립니다.</li>
<li>어차피 데이터가 비슷한 일을 시킬 게 아니라면 GPU에 들고 갈 이유가 없습니다.</li>
</ul>
<p>행렬 곱, 컨볼루션, 어텐션 — 딥러닝 연산의 본체는 거의 다 &quot;같은 명령을 다른 데이터에 적용&quot;입니다. SIMT가 정확히 이걸 위해 만들어진 셈이고, 그래서 GPU와 딥러닝이 우연히 잘 맞아떨어진 게 아닙니다.</p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
Warp는 NVIDIA 용어이고, AMD에서는 Wavefront(64스레드)라고 부릅니다. SIMD와 SIMT의 차이도 알아두면 좋습니다. SIMD는 한 코어 내부 한 명령어가 벡터 레지스터 여러 lane에 적용되는 모델(AVX-512 등), SIMT는 그것을 스레드 추상화로 노출한 모델입니다.</p>
<ul>
<li>검색 키워드: <code>CUDA Warp scheduling</code>, <code>Warp divergence</code>, <code>SIMD vs SIMT</code>, <code>NVIDIA SM Streaming Multiprocessor</code></li>
</ul>
</blockquote>
<h3 id="두-번째-메커니즘-tensor-core--행렬-곱-전용-asic">두 번째 메커니즘: Tensor Core — 행렬 곱 전용 ASIC</h3>
<p>SIMT만으로도 GPU는 충분히 빠릅니다. 그런데 2017년부터 NVIDIA는 GPU 안에 한 가지를 더 박아 넣기 시작합니다. <strong>Tensor Core</strong>라는 회로입니다.</p>
<p>이름이 거창하지만 하는 일은 단순합니다. <strong>작은 행렬 두 개를 받아서 한 클럭 사이클 안에 곱한 다음 누적까지 해 줍니다.</strong> 보통 4×4 또는 8×8 단위입니다. CUDA Core(평범한 SIMT 연산기)로 같은 일을 시키려면 곱셈과 덧셈을 수십 번 돌려야 하는데, Tensor Core는 그 시퀀스 전체를 회로 한 번으로 끝냅니다.</p>
<p>이게 왜 중요할까요? 트랜스포머 모델의 본체 연산은 사실상 <strong>거대한 행렬 곱</strong>입니다. Attention의 Q×Kᵀ도, FFN의 W×x도, 임베딩 lookup도 모두 행렬 곱으로 환원됩니다. 모델 학습·추론 시간의 90% 이상이 행렬 곱에 들어갑니다.</p>
<p>그래서 NVIDIA가 매 세대마다 자랑하는 &quot;OOO exaFLOPS&quot; 같은 숫자는 거의 대부분 <strong>Tensor Core가 행렬 곱에서 내는 숫자</strong>입니다. CUDA Core가 내는 일반 부동소수점 성능과는 따로 노는 별개 지표입니다.</p>
<p>세대별로 Tensor Core가 다루는 정밀도가 다릅니다. 이 부분이 NVIDIA 칩 트렌드 중 가장 중요합니다.</p>
<table>
<thead>
<tr>
<th>세대</th>
<th>대표 칩</th>
<th>새로 추가된 정밀도</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Volta</td>
<td>V100 (2017)</td>
<td>FP16</td>
<td>학습용 16비트</td>
</tr>
<tr>
<td>Ampere</td>
<td>A100 (2020)</td>
<td>TF32, BF16</td>
<td>학습 안정성 + 속도</td>
</tr>
<tr>
<td>Hopper</td>
<td>H100 (2022)</td>
<td><strong>FP8</strong></td>
<td>학습/추론 모두 8비트</td>
</tr>
<tr>
<td>Blackwell</td>
<td>B200 (2024)</td>
<td><strong>FP4</strong></td>
<td>추론 4비트</td>
</tr>
<tr>
<td>Blackwell Ultra</td>
<td>B300 (2025)</td>
<td>FP4 강화 + 더 큰 HBM</td>
<td>추론 처리량 ↑</td>
</tr>
<tr>
<td>Rubin</td>
<td>R100 (2026 H2)</td>
<td>FP4 + 차세대 메모리</td>
<td>dual-die 패키지</td>
</tr>
</tbody></table>
<p>여기서 트렌드는 명확합니다. <strong>숫자를 점점 더 짧게 표현해서, 같은 시간에 더 많은 행렬 곱을 한다.</strong> FP32 → FP16 → FP8 → FP4로 내려갈 때마다 연산기당 트랜지스터가 줄고, 같은 칩에 더 많은 연산기를 박을 수 있고, 데이터를 옮기는 메모리 대역폭도 덜 듭니다. 모델 품질만 유지된다면 모두가 이득입니다. 딥러닝 연구자들이 매 세대마다 &quot;FP4로 학습되나?&quot;를 검증하는 이유가 이것입니다.</p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
Tensor Core는 단순히 &quot;행렬 곱이 빠른 회로&quot;가 아니라, <strong>데이터를 어떻게 메모리에서 가져와서 연산기에 먹이고 결과를 다시 메모리에 쓰는가</strong>까지 포함하는 설계 단위입니다. WMMA API, MMA instruction, cuBLAS·cuDNN이 Tensor Core를 어떻게 활용하는지 보면 GPU 컴파일러의 세계가 열립니다.</p>
<ul>
<li>검색 키워드: <code>Tensor Core WMMA</code>, <code>NVIDIA Transformer Engine</code>, <code>FP8 training</code>, <code>Microscaling FP4 MXFP4</code></li>
</ul>
</blockquote>
<h3 id="세-번째-메커니즘-hbm--미친-대역폭의-메모리">세 번째 메커니즘: HBM — 미친 대역폭의 메모리</h3>
<p>여기까지 보면 GPU는 그저 &quot;연산기가 많은 칩&quot;처럼 보일 수 있습니다. 그런데 진짜 비싼 부분, 그리고 NVIDIA가 SK하이닉스·삼성·마이크론에 매년 수십조 원을 갖다 바치는 부분은 따로 있습니다. 바로 <strong>메모리</strong>입니다.</p>
<p>CPU 백엔드 개발자가 알고 있는 DRAM(DDR5)은 한 채널 대역폭이 보통 50<del>70 GB/s 수준입니다. 듀얼 채널 / 쿼드 채널을 묶어도 200</del>400 GB/s 정도가 일반 서버의 한계입니다.</p>
<p>GPU 한 장이 쓰는 메모리는 단위가 다릅니다.</p>
<table>
<thead>
<tr>
<th>칩</th>
<th>메모리 종류</th>
<th>용량</th>
<th>대역폭</th>
</tr>
</thead>
<tbody><tr>
<td>H100</td>
<td>HBM3</td>
<td>80 GB</td>
<td>3.35 TB/s</td>
</tr>
<tr>
<td>H200</td>
<td>HBM3e</td>
<td>141 GB</td>
<td>4.8 TB/s</td>
</tr>
<tr>
<td>B200 (Blackwell)</td>
<td>HBM3e</td>
<td>192 GB</td>
<td>8 TB/s</td>
</tr>
<tr>
<td>B300 (Blackwell Ultra)</td>
<td>HBM3e</td>
<td>288 GB</td>
<td>8 TB/s</td>
</tr>
<tr>
<td>R100 (Rubin)</td>
<td>HBM4</td>
<td>~288 GB</td>
<td>13 TB/s 이상</td>
</tr>
</tbody></table>
<p>서버 DRAM 대비 <strong>20~40배</strong> 빠릅니다. 왜 이렇게 만들었을까요?</p>
<p>답은 첫 번째 메커니즘으로 돌아갑니다. <strong>수만 개의 연산기를 굶기지 않으려면 메모리가 그만큼 빨라야 합니다.</strong> 연산기는 1초에 페타플롭스 단위로 계산을 토해낼 수 있는데, 메모리가 데이터를 그 속도로 못 가져오면 연산기는 놀게 됩니다. 이걸 <strong>메모리 바운드(memory-bound)</strong> 라고 부르고, 트랜스포머 추론 같은 워크로드는 거의 항상 메모리 바운드입니다.</p>
<p>HBM(High Bandwidth Memory)이라는 이름은 그래서 붙었습니다. 평범한 DRAM 칩을 옆이 아니라 <strong>위로 8단~12단 쌓아서</strong>, GPU 다이 바로 옆에 인터포저(interposer)라는 실리콘 기판 위에 같이 박습니다. CPU↔DRAM처럼 메인보드 배선을 타고 신호를 보내는 게 아니라, 실리콘 위에서 수천 개 와이어로 직결합니다. 그래서 대역폭이 미친 수준으로 나옵니다.</p>
<p>대신 단점이 있습니다.</p>
<ul>
<li><strong>비쌉니다.</strong> Blackwell B200 한 장 가격의 상당 부분이 HBM 가격입니다. SK하이닉스가 갑자기 &quot;AI 수혜주&quot;가 된 이유입니다.</li>
<li><strong>공급이 제한됩니다.</strong> HBM 적층 공정 자체가 어려워서, 메모리 회사가 일정 이상 못 찍어냅니다. NVIDIA 출하량의 진짜 병목은 GPU 다이가 아니라 HBM이라는 분석이 많이 나옵니다.</li>
<li><strong>수리가 안 됩니다.</strong> GPU 다이와 HBM이 같은 패키지에 박혀 있어서, 하나가 죽으면 통째로 폐기됩니다.</li>
</ul>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
HBM은 표준이 JEDEC에서 정해집니다. HBM3 → HBM3e → HBM4로 가면서 채널 수, 적층 단수, 클럭이 계속 올라갑니다. CoWoS(Chip on Wafer on Substrate)라는 TSMC의 첨단 패키징 기술이 HBM과 GPU를 같은 인터포저에 붙이는 핵심 공정이고, 이 패키징 캐파가 NVIDIA 출하의 또 다른 병목입니다.</p>
<ul>
<li>검색 키워드: <code>HBM3e stack</code>, <code>CoWoS packaging</code>, <code>roofline model memory-bound</code>, <code>Arithmetic Intensity</code></li>
</ul>
</blockquote>
<h3 id="deep-dive-tensorcuda-한-줄의-내부">Deep Dive: <code>tensor.cuda()</code> 한 줄의 내부</h3>
<p>세 메커니즘을 이제 한 줄의 코드로 묶어 봅시다.</p>
<pre><code class="language-python">import torch

x = torch.randn(1024, 1024)   # CPU 위 텐서 (DDR5에 있음)
x = x.cuda()                  # ← 이 한 줄에서 무슨 일이 벌어지나?

y = x @ x                     # 행렬 곱</code></pre>
<p><code>x.cuda()</code>는 단순히 &quot;GPU 쓰겠습니다&quot; 정도로 보이지만, 실제로는 여러 컴포넌트가 분업해서 다음 일을 합니다.</p>
<ol>
<li><strong>PyTorch: 메모리 할당 요청</strong> — PyTorch가 CUDA 런타임에게 &quot;HBM 위에 1024×1024 float32 텐서 공간을 잡아 달라&quot;고 요청합니다. CUDA 런타임은 자체 메모리 풀(caching allocator)에서 적당한 블록을 떼어줍니다. 매번 <code>cudaMalloc</code>을 부르지 않고 풀링하는 이유는 시스템콜이 워낙 비싸기 때문입니다.</li>
<li><strong>CUDA Driver: 호스트 → 디바이스 복사</strong> — DDR5에 있던 4MB짜리 데이터를 PCIe Gen5(보통 64 GB/s)를 통해 HBM으로 옮깁니다. 이게 그 유명한 <code>cudaMemcpyHostToDevice</code>입니다. <strong>이 복사 자체가 비용</strong>이고, 그래서 한 번 GPU에 올린 데이터는 가능하면 거기서 다 처리하고 결과만 다시 받아오는 게 정석입니다.</li>
<li><strong>GPU 하드웨어: 데이터 도착, 끝</strong> — 복사가 끝나면 텐서는 HBM 위에 자리잡고, GPU 코어 입장에서는 &quot;거기에 데이터가 있다&quot;는 포인터만 알면 됩니다. 이 시점에서 CPU와 GPU의 메모리는 <strong>별개의 주소 공간</strong>입니다. 같은 변수 <code>x</code>라도 CPU <code>x.data_ptr()</code>과 GPU <code>x.cuda().data_ptr()</code>은 완전히 다른 주소를 가리킵니다.</li>
<li><strong>행렬 곱: SIMT + Tensor Core 동원</strong> — <code>y = x @ x</code>를 호출하면 PyTorch는 cuBLAS의 <code>cublasGemmEx</code> 같은 라이브러리 호출로 변환합니다. cuBLAS는 입력 크기에 맞춰 최적화된 <strong>CUDA 커널</strong>을 고르고, 그 커널은 행렬을 작은 타일로 쪼개 SM(Streaming Multiprocessor)에 할당하고, 각 SM은 Warp 단위로 Tensor Core를 호출해 16×16씩 곱한 뒤 결과를 HBM에 다시 씁니다.</li>
</ol>
<p>CPU는 이 4번 과정에 <strong>참여하지 않습니다</strong>. 명령어를 큐에 던지고 끝납니다. GPU가 알아서 다 합니다. 이 비동기성 자체가 2장의 주제입니다.</p>
<p>이 한 줄을 따라가 보면, GPU가 단순히 &quot;빠른 계산기&quot;가 아니라 <strong>별도의 메모리 공간 + 별도의 명령 시스템 + 별도의 실행 모델</strong>을 가진 컴퓨터라는 게 보입니다. 사실상 같은 케이스 안에 들어 있는 <strong>두 번째 컴퓨터</strong>에 가깝습니다.</p>
<hr>
<h2 id="2장-cpu는-gpu에게-어떻게-명령하는가">2장. CPU는 GPU에게 어떻게 명령하는가</h2>
<p>여기까지 GPU 한 장이 무엇인지 봤습니다. 그런데 그 GPU가 &quot;별개의 컴퓨터&quot;라면, CPU와 GPU는 어떤 인터페이스로 대화할까요? 함수 호출처럼 동기적으로 움직일까요, 아니면 백엔드끼리 메시지 큐로 통신하듯 비동기일까요?</p>
<h3 id="잘못된-멘탈-모델-부수기">잘못된 멘탈 모델 부수기</h3>
<p>GPU 처음 보는 개발자가 가장 흔히 갖는 잘못된 그림은 이렇습니다.</p>
<blockquote>
<p>&quot;CPU에서 <code>tensor @ tensor</code> 호출 → 시스템콜 → GPU에게 일을 시킴 → 결과 받음 → 다음 줄로 진행&quot;</p>
</blockquote>
<p>이 그림은 <strong>거의 모든 단계가 틀렸습니다</strong>. 실제로는 시스템콜을 매번 거치지 않고(너무 비쌈), CPU는 결과를 기다리지 않고(블로킹하면 GPU 사용률이 폭락), 그 줄이 끝났다고 GPU 일이 끝난 것도 아닙니다(큐에 던졌을 뿐).</p>
<p>정확한 그림은 <strong>메시지 큐 기반 비동기 워커 시스템</strong>에 훨씬 가깝습니다.</p>
<h3 id="첫-번째-메커니즘-커맨드-큐와-비동기-링버퍼">첫 번째 메커니즘: 커맨드 큐와 비동기 링버퍼</h3>
<p>CPU와 GPU 사이의 통신은 다음과 같이 일어납니다.</p>
<ol>
<li><strong>CPU(앱/PyTorch)는 GPU가 실행할 명령(CUDA Kernel)과 그 인자(VRAM 주소들)를 패키징합니다.</strong></li>
<li><strong>이 패키지를 메인 메모리에 자리잡은 커맨드 큐(Command Queue, 또는 CUDA Stream)에 비동기로 던집니다.</strong> CPU는 거기서 더 기다리지 않고 다음 줄로 갑니다.</li>
<li><strong>GPU 내부의 하드웨어 스케줄러가 큐에서 명령을 꺼내(Fetch) 실행합니다.</strong> CPU는 이 과정에 끼지 않습니다.</li>
<li><strong>CPU가 진짜로 GPU 결과를 필요로 하는 시점에만 동기화(<code>cudaStreamSynchronize</code>, <code>tensor.cpu()</code> 등)를 합니다.</strong> 이때 비로소 둘이 만납니다.</li>
</ol>
<p>이걸 백엔드 개발자 멘탈 모델로 옮기면 다음과 같습니다.</p>
<ul>
<li><strong>커맨드 큐 = Redis Queue / Kafka topic</strong></li>
<li><strong>CPU = Producer</strong> — 작업 메시지를 쏟아붓고 끝</li>
<li><strong>GPU = Consumer worker pool</strong> — 큐에서 자기 속도로 꺼내 처리</li>
<li><strong>Stream = 메시지 큐의 파티션</strong> — 같은 stream 안 명령은 순서 보장, 다른 stream끼리는 병렬 가능</li>
<li><strong>cudaStreamSynchronize = future.get() / blocking await</strong> — 결과가 필요할 때만 막힘</li>
</ul>
<p>이 구조의 핵심은 <strong>CPU의 호출과 GPU의 실행이 시간 축에서 떨어져 있다는 것</strong>입니다. PyTorch 코드 100줄을 1초 만에 다 던졌어도, GPU는 그것을 30초에 걸쳐 자기 속도로 실행할 수 있고, CPU는 그동안 다른 일(다음 batch 전처리, 데이터 로드 등)을 합니다. 이게 잘 굴러가야 GPU 사용률 90%대가 나옵니다. 안 굴러가면 GPU가 데이터 기다리며 놀고, 사용률 30%에서 박힙니다.</p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
CUDA Stream은 단일 GPU 안에서 작업을 병렬화하는 핵심 추상입니다. 한 stream은 FIFO지만, 여러 stream을 만들면 H↔D copy와 kernel 실행이 동시에 일어날 수 있습니다(overlap). <code>nsys</code> 같은 NVIDIA 프로파일러로 stream timeline을 보면 이 그림이 한눈에 들어옵니다.</p>
<ul>
<li>검색 키워드: <code>CUDA Stream overlap</code>, <code>cudaEventRecord</code>, <code>cudaStreamSynchronize</code>, <code>Nsight Systems timeline</code></li>
</ul>
</blockquote>
<h3 id="deep-dive-한-줄-더--ring-buffer와-doorbell">Deep Dive: 한 줄 더 — Ring Buffer와 Doorbell</h3>
<p>위에서 &quot;커맨드 큐에 명령을 던진다&quot;라고만 했는데, 그 큐가 어디 사는 것이고 GPU가 그걸 어떻게 알아채는지 한 계층 더 들어가 보면 멘탈 모델이 한 번 더 깔끔해집니다.</p>
<p>처음 GPU를 만지는 개발자가 가장 자주 헷갈리는 지점은 <strong>&quot;커널 코드 / 데이터 / 명령&quot;</strong> 세 가지를 한 덩어리로 보는 것입니다. 사실 셋은 서로 다른 곳에 살고, 다른 경로로 GPU에 닿습니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>정체</th>
<th>사는 곳</th>
<th>누가 옮기나</th>
</tr>
</thead>
<tbody><tr>
<td><strong>① 커널 코드</strong></td>
<td>&quot;행렬 곱하라&quot;는 GPU 실행 바이너리</td>
<td><strong>HBM</strong> (시작 시 한 번만 업로드)</td>
<td><code>import torch</code> 시점에 cuBLAS·cuDNN이</td>
</tr>
<tr>
<td><strong>② 데이터</strong></td>
<td>텐서, 가중치, 입력값</td>
<td><strong>HBM</strong> (<code>.cuda()</code>로 올림)</td>
<td><code>cudaMemcpyAsync</code> (PCIe DMA)</td>
</tr>
<tr>
<td><strong>③ 명령(descriptor)</strong></td>
<td>&quot;커널 #42를 주소 0x...로 실행해&quot; 같은 작은 패킷</td>
<td><strong>CPU DRAM의 ring buffer</strong></td>
<td>CPU가 직접 쓰기 + doorbell</td>
</tr>
</tbody></table>
<p><code>y = x @ x</code> 한 줄이 실행될 때, ①과 ②는 이미 HBM에 올라가 있고, 이 호출은 ③만 큐에 새로 적습니다. 그래서 매 호출의 핫 루프는 거의 비용이 없어요.</p>
<p><strong>Ring Buffer의 물리 위치 — HBM이 아니라 CPU DRAM</strong></p>
<p>커맨드 큐(ring buffer)는 보통 <strong>CPU DRAM의 한 페이지</strong>에 자리잡습니다. 단, OS가 그 페이지를 GPU도 PCIe로 읽을 수 있도록 매핑해 둡니다. CPU는 자기 메모리에 쓰는 것처럼 적고, GPU는 자기 메모리처럼 읽어가는 <strong>공유 메모리 영역</strong>입니다. HBM에 따로 복사하는 단계는 없습니다.</p>
<p><strong>Doorbell — GPU를 깨우는 종</strong></p>
<p>CPU가 ring buffer에 명령을 적은 뒤, 마지막에 GPU의 특수 레지스터(doorbell)에 한 바이트를 씁니다. 이 쓰기는 <strong>MMIO(Memory-Mapped I/O)</strong> — 일반 메모리 쓰기처럼 보이지만 사실은 PCIe를 타고 GPU 레지스터로 라우팅됩니다. <strong>syscall이 아니라 그냥 사용자 공간 메모리 쓰기</strong>라서 1마이크로초도 안 걸려요.</p>
<p>GPU 내부의 작은 컨트롤러(Command Processor)가 doorbell을 감지하고 ring buffer를 폴링해서 명령을 꺼냅니다. 한 가지 더 중요한 사실은 <strong>GPU에는 OS가 없다</strong>는 것. 그래서 &quot;GPU가 syscall 하나?&quot;라는 질문 자체가 성립하지 않습니다. GPU는 그냥 큐를 읽고 실행하는 거대한 기계입니다.</p>
<p>요약하면 핫 루프의 비용은 이 정도입니다.</p>
<ul>
<li>ring buffer에 100바이트짜리 descriptor 쓰기 (CPU DRAM)</li>
<li>doorbell 레지스터에 1바이트 쓰기 (MMIO)</li>
<li>끝. CPU는 다음 줄로</li>
</ul>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
이 doorbell + ring buffer 패턴은 NVIDIA 고유가 아니라 모든 고성능 PCIe 디바이스의 표준입니다. NVMe SSD가 디스크 I/O 명령을 받을 때, 고성능 NIC이 패킷을 받을 때 같은 방식을 씁니다. 백엔드 개발자가 NVMe 멘탈 모델을 갖고 있으면 GPU 통신도 같은 그림으로 이해할 수 있어요.</p>
<ul>
<li>검색 키워드: <code>MMIO doorbell</code>, <code>NVMe submission queue</code>, <code>CUDA user-mode driver (UMD)</code>, <code>GPU Command Processor</code></li>
</ul>
</blockquote>
<h3 id="두-번째-메커니즘-pcie를-통한-데이터-이동">두 번째 메커니즘: PCIe를 통한 데이터 이동</h3>
<p>CPU↔GPU 사이의 물리 통로는 전통적으로 <strong>PCIe(Peripheral Component Interconnect Express)</strong> 입니다. PC 메인보드의 그래픽카드 슬롯이 그것이고, 데이터센터 서버에서도 마찬가지입니다.</p>
<p>세대별 대역폭은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>세대</th>
<th>x16 슬롯 한 방향</th>
<th>양방향</th>
</tr>
</thead>
<tbody><tr>
<td>PCIe Gen3</td>
<td>16 GB/s</td>
<td>32 GB/s</td>
</tr>
<tr>
<td>PCIe Gen4</td>
<td>32 GB/s</td>
<td>64 GB/s</td>
</tr>
<tr>
<td>PCIe Gen5 (현행)</td>
<td>64 GB/s</td>
<td>128 GB/s</td>
</tr>
<tr>
<td>PCIe Gen6 (등장 중)</td>
<td>128 GB/s</td>
<td>256 GB/s</td>
</tr>
</tbody></table>
<p>이게 충분히 빨라 보이지만, <strong>GPU 내부 HBM 대역폭(8 TB/s)에 비하면 60~100배 느립니다</strong>. 즉 CPU에서 GPU로 데이터를 옮기는 과정은 항상 GPU 자체 연산보다 훨씬 느린 병목이고, 그래서 &quot;한 번 올린 데이터는 가능한 오래 GPU에 두라&quot;가 GPU 프로그래밍의 첫 번째 규칙이 됩니다.</p>
<p>이 병목이 워낙 거대해서, NVIDIA는 PCIe를 우회하는 자체 통로를 만들기 시작했습니다. 그게 NVLink 계열입니다.</p>
<h3 id="세-번째-메커니즘-nvlink-c2c--cpu와-gpu를-한-numa-노드처럼">세 번째 메커니즘: NVLink-C2C — CPU와 GPU를 한 NUMA 노드처럼</h3>
<p>2023년 Grace Hopper(GH200)부터, 그리고 현재 양산 중인 Grace Blackwell(GB200)에서, NVIDIA는 <strong>CPU와 GPU를 같은 패키지에 박고 둘 사이를 PCIe가 아닌 자체 인터커넥트로 직결</strong>하기 시작했습니다. 이게 <strong>NVLink-C2C(Chip-to-Chip)</strong> 입니다.</p>
<p>스펙은 PCIe Gen5의 7배입니다.</p>
<table>
<thead>
<tr>
<th>통로</th>
<th>양방향 대역폭</th>
</tr>
</thead>
<tbody><tr>
<td>PCIe Gen5 x16</td>
<td>128 GB/s</td>
</tr>
<tr>
<td><strong>NVLink-C2C</strong></td>
<td><strong>900 GB/s</strong></td>
</tr>
</tbody></table>
<p>이 차이는 단순한 속도 차이가 아니라 <strong>소프트웨어가 보는 메모리 모델 자체를 바꿉니다</strong>. 멀티소켓 서버 경험이 있다면 NUMA(Non-Uniform Memory Access)를 들어봤을 텐데, NUMA는 &quot;CPU 소켓이 두 개 있을 때 각자 자기 메모리를 갖고 있지만 서로의 메모리도 좀 느리게 접근할 수 있는 구조&quot;입니다. NVLink-C2C는 그 NUMA를 <strong>CPU와 GPU 사이에 만들어 줍니다</strong>.</p>
<p>즉 Grace Hopper/Blackwell 시스템에서 GPU는 자기 HBM뿐 아니라 옆에 붙은 Grace CPU의 LPDDR5X 메모리(480GB 이상)도 마치 &quot;조금 느린 자기 메모리&quot;처럼 접근합니다. 반대로 CPU도 GPU HBM에 직접 포인터로 접근 가능합니다. 메인보드 PCIe 슬롯 너머의 별개 컴퓨터가 아니라, <strong>같은 NUMA 도메인의 다른 소켓</strong>처럼 보이는 겁니다.</p>
<p>본인이 처음 본 GH200 슈퍼칩 다이어그램이 정확히 이 구조입니다.</p>
<p><img src="https://developer-blogs.nvidia.com/wp-content/uploads/2022/11/grace-hopper-overview.png" alt="NVIDIA Grace Hopper Superchip 다이어그램"></p>
<ul>
<li>CPU(Grace) ↔ LPDDR5X 546 GB/s</li>
<li>GPU(Hopper) ↔ HBM3 3 TB/s</li>
<li>CPU ↔ GPU(NVLink-C2C) 900 GB/s</li>
<li>CPU ↔ 외부 I/O(PCIe Gen5 x16 ×4) 512 GB/s</li>
<li>GPU ↔ 다른 GPU(NVLink 4) 900 GB/s ×18</li>
</ul>
<p>핵심은 NVLink-C2C 900 GB/s가 다른 모든 통로 중 가장 굵다는 것이 아니라, <strong>CPU와 GPU를 같은 메모리 공간으로 묶기에 충분히 빠르다</strong>는 것입니다.</p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
Grace CPU는 x86이 아니라 Arm Neoverse V2 기반 72코어 칩입니다. NVIDIA가 Arm을 인수하려다 실패했지만, 결국 자체 Arm CPU는 만들어 냈고, 그게 데이터센터 GPU 옆에 박혀 PCIe 종속에서 탈출한 핵심입니다. AMD는 비슷한 시도로 MI300A에서 Zen4 CPU + CDNA3 GPU를 한 패키지에 박았습니다.</p>
<ul>
<li>검색 키워드: <code>NVLink-C2C protocol</code>, <code>Grace CPU Neoverse V2</code>, <code>MI300A APU</code>, <code>Coherent memory CXL</code></li>
</ul>
</blockquote>
<h3 id="네-번째-메커니즘-cuda-unified-memory--그냥-포인터로-써라">네 번째 메커니즘: CUDA Unified Memory — &quot;그냥 포인터로 써라&quot;</h3>
<p>NVLink-C2C가 하드웨어 통로라면, <strong>CUDA Unified Memory</strong>는 그 위에 얹는 소프트웨어 추상입니다.</p>
<p>전통적으로 GPU 프로그래밍은 다음과 같습니다.</p>
<pre><code class="language-c">float *h_data = malloc(size);            // CPU 메모리
float *d_data;
cudaMalloc(&amp;d_data, size);               // GPU 메모리
cudaMemcpy(d_data, h_data, size, ...);   // 복사 (수동)
kernel&lt;&lt;&lt;...&gt;&gt;&gt;(d_data);                 // GPU 일 시킴
cudaMemcpy(h_data, d_data, size, ...);   // 결과 복사 (수동)</code></pre>
<p>CPU 포인터와 GPU 포인터를 별도로 관리하고, 명시적으로 복사를 호출해야 합니다. 백엔드 개발자가 분산 캐시 쓰는 느낌과 비슷합니다.</p>
<p>Unified Memory는 이걸 다음으로 줄입니다.</p>
<pre><code class="language-c">float *data;
cudaMallocManaged(&amp;data, size);          // CPU/GPU 공통 포인터
data[0] = 3.14;                          // CPU가 씀
kernel&lt;&lt;&lt;...&gt;&gt;&gt;(data);                   // GPU가 같은 포인터로 읽음
cudaDeviceSynchronize();
printf(&quot;%f\n&quot;, data[0]);                 // CPU가 결과 읽음</code></pre>
<p>같은 포인터가 CPU에서도 GPU에서도 유효합니다. 마치 하나의 메모리 공간을 공유하는 것처럼 보입니다.</p>
<p>내부적으로는 CUDA 드라이버가 <strong>페이지 단위로 데이터를 자동으로 옮겨다닙니다</strong>. CPU가 그 페이지에 접근하면 GPU에 있던 페이지가 CPU 쪽으로 마이그레이션되고, 반대도 마찬가지입니다. 우리가 익숙한 OS 가상 메모리 페이지 폴트와 비슷한 메커니즘을, CPU와 GPU 사이에서 굴리는 셈이죠.</p>
<p>다만 자동이라고 공짜는 아닙니다. 페이지를 잘못 이동시키면 핑퐁(ping-pong)이 일어나고, 그게 PCIe라면 성능이 폭망합니다. 그래서 <strong>NVLink-C2C가 들어온 Grace Hopper/Blackwell에서 Unified Memory가 진짜로 쓸 만해졌다</strong>고 평가받습니다. 통로가 7배 굵어졌으니 페이지 마이그레이션 비용이 그만큼 싸졌기 때문입니다.</p>
<h3 id="deep-dive-같은-pytorch-코드-세-가지-하드웨어">Deep Dive: 같은 PyTorch 코드, 세 가지 하드웨어</h3>
<pre><code class="language-python">x = torch.randn(N).cuda()</code></pre>
<p>같은 한 줄을 세 가지 하드웨어에서 돌릴 때 어떤 일이 일어나는지 비교해 봅시다.</p>
<p><strong>(A) 일반 데스크탑/서버 (PCIe로 연결된 GPU)</strong></p>
<p>CPU DRAM → PCIe(64 GB/s) → HBM. 큰 데이터일수록 PCIe가 병목. DataLoader에 <code>pin_memory=True</code> 옵션을 주는 이유는, 페이지가 swap out되지 않도록 pin해서 PCIe DMA 전송을 최대 속도로 끌어내기 위함입니다.</p>
<p><strong>(B) DGX H100 같은 PCIe 기반 데이터센터 시스템</strong></p>
<p>위와 동일하지만 CPU는 보통 듀얼 소켓 Xeon/EPYC이고, GPU도 8장. CPU↔GPU 통신은 여전히 PCIe Gen5. <strong>GPU↔GPU는 NVLink(아래 3장 참조)</strong> 라는 점이 다를 뿐, CPU↔GPU 경로는 동일합니다.</p>
<p><strong>(C) Grace Hopper / Grace Blackwell (NVLink-C2C 직결)</strong></p>
<p>CPU LPDDR5X ←NVLink-C2C(900 GB/s)→ GPU HBM. 같은 한 줄이지만 <strong>통로가 14배 굵습니다</strong>. 대용량 임베딩처럼 HBM에 다 안 들어가는 데이터를 CPU LPDDR5X(최대 480GB)에 두고 GPU가 직접 읽어쓰는 패턴이 비로소 실용적이 됩니다. 추천 시스템이나 매우 큰 임베딩 테이블을 가진 모델이 이 구조의 직접 수혜자입니다.</p>
<p>이 차이가 곧 NVIDIA가 단품 GPU에서 <strong>CPU+GPU 통합 슈퍼칩</strong>으로 사업 모델을 옮겨가는 이유 중 하나입니다. 같은 GPU라도 옆에 Grace CPU가 붙어 있으면 처리할 수 있는 워크로드의 폭이 다릅니다.</p>
<hr>
<h2 id="3장-한-대의-서버는-더-이상-한-대가-아니다--랙-단위-scale-up">3장. 한 대의 서버는 더 이상 한 대가 아니다 — 랙 단위 Scale-Up</h2>
<p>GPU 한 장의 구조와 그것이 CPU와 어떻게 통신하는지를 봤습니다. 이제 다음 의문이 남습니다. <strong>수십·수백·수천 장의 GPU를 어떻게 한 모델 학습/추론에 묶어 쓰는가?</strong></p>
<p>이 질문에 대한 답은 두 갈래입니다.</p>
<ul>
<li><strong>Scale-Up (랙 안)</strong>: GPU 여러 장을 같은 박스/같은 랙에 넣고, <strong>한 대처럼 동작</strong>하게 만든다.</li>
<li><strong>Scale-Out (랙 사이)</strong>: 여러 랙을 외부 네트워크로 묶어, 모델 학습을 분산한다.</li>
</ul>
<p>3장은 Scale-Up, 4장은 Scale-Out을 다룹니다. 이 구분이 NVIDIA 실적표를 이해하는 데도 결정적입니다.</p>
<h3 id="첫-번째-메커니즘-nvlink--gpu-간-직결-통로">첫 번째 메커니즘: NVLink — GPU 간 직결 통로</h3>
<p>CPU↔GPU에 NVLink-C2C가 있다면, <strong>GPU↔GPU에는 NVLink(그냥 NVLink)</strong> 가 있습니다. 한 패키지 안이 아니라 같은 박스 안 다른 GPU 카드 사이를 PCIe 우회로 직결합니다.</p>
<p>세대별 GPU 1장당 NVLink 양방향 대역폭은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>세대</th>
<th>GPU당 NVLink 대역폭 (양방향)</th>
</tr>
</thead>
<tbody><tr>
<td>NVLink 3 (Ampere, A100)</td>
<td>600 GB/s</td>
</tr>
<tr>
<td>NVLink 4 (Hopper, H100)</td>
<td>900 GB/s</td>
</tr>
<tr>
<td>NVLink 5 (Blackwell, B200)</td>
<td>1.8 TB/s</td>
</tr>
<tr>
<td>NVLink 6 (Rubin)</td>
<td>3.6 TB/s 예정</td>
</tr>
</tbody></table>
<p>같은 박스 안 GPU 8장을 묶을 때, NVLink는 평균 PCIe보다 14배(NVLink 5 기준) 빠릅니다. 그래서 모델 병렬화(Tensor Parallelism, Pipeline Parallelism)가 가능해집니다.</p>
<h3 id="두-번째-메커니즘-hgx-보드--gpu-8장이-한-메인보드처럼">두 번째 메커니즘: HGX 보드 — GPU 8장이 한 메인보드처럼</h3>
<p>NVIDIA가 데이터센터에 파는 GPU는 단품으로 잘 안 나옵니다. <strong>HGX 보드</strong>라는 형태로 묶어 팝니다.</p>
<ul>
<li>HGX H100: GPU 8장이 한 보드 위에 박혀 있고, 그 사이를 NVLink 4세대로 풀 메시 직결</li>
<li>HGX B200: GPU 8장이 NVLink 5세대로 풀 메시 직결</li>
</ul>
<p>이 보드 한 장이 곧 우리가 흔히 말하는 &quot;DGX/HGX 서버 1대&quot;의 본체 핵심이고, AWS·GCP·Azure에서 빌리는 8GPU 인스턴스의 실체이기도 합니다. 보드 한 장에서 GPU↔GPU 통신은 매우 빠르지만, 8장이 한계입니다.</p>
<p>문제는 LLM이 8장 GPU 메모리에 안 들어가기 시작하면서부터입니다. GPT-4 급, 그리고 그 이상의 모델은 한 모델을 80~200장의 GPU에 쪼개야 합니다. 그러면 보드를 넘어가는 통신이 필요해지고, 이 지점에서 NVLink Switch와 NVL 랙 시스템이 등장합니다.</p>
<h3 id="세-번째-메커니즘-nvlink-switch와-nvl72--한-랙이-곧-한-gpu-도메인">세 번째 메커니즘: NVLink Switch와 NVL72 — 한 랙이 곧 한 GPU 도메인</h3>
<p><strong>NVLink Switch</strong>는 백엔드 개발자가 익숙한 네트워크 스위치를 GPU 전용으로 만든 칩입니다. 여러 GPU의 NVLink 포트를 받아서 그들 사이를 풀 메시로 연결합니다.</p>
<p>현행 주력 시스템 <strong>GB200 NVL72</strong>는 다음과 같이 생겼습니다.</p>
<ul>
<li>18개의 1U 컴퓨트 트레이 (각 트레이당 Grace CPU 2개 + Blackwell GPU 4개)</li>
<li>9개의 NVLink Switch 트레이</li>
<li>한 랙 총합: <strong>Grace CPU 36개 + Blackwell GPU 72개</strong></li>
<li>총 메모리: HBM3e 13.4 TB (통합 GPU 메모리)</li>
<li>FP4 연산 성능: 1.44 exaFLOPS (희소성 포함)</li>
<li>랙 내 GPU↔GPU NVLink 총 대역폭: <strong>130 TB/s</strong></li>
<li>무게 1.36톤, 전력 120kW, 액체 냉각</li>
</ul>
<p>여기서 핵심은 &quot;72장의 GPU가 모두 NVLink Switch를 통해 한 도메인으로 묶인다&quot;는 것입니다. 어떤 GPU에서 어떤 GPU로도 1.8 TB/s 통로로 직접 통신 가능합니다. 소프트웨어 입장에서는 <strong>&quot;GPU 72장이 곧 한 거대한 가상 GPU&quot;</strong> 처럼 동작합니다.</p>
<p>CPU 백엔드 개발자 비유로 옮기면, 72개 머신을 RDMA 네트워크로 묶은 클러스터가 아니라 <strong>NUMA 노드 72개짜리 한 대의 거대한 SMP 서버</strong>에 가깝습니다.</p>
<p>랙 한 대 가격은 약 300만~400만 달러로 알려져 있고, 액체 냉각·전력 인프라까지 같이 설계해서 팝니다. 부동산처럼 &quot;랙 한 대&quot;가 거래 단위입니다.</p>
<p>NVIDIA 공식 페이지: <a href="https://www.nvidia.com/en-us/data-center/gb200-nvl72/">GB200 NVL72</a></p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
NVL72의 백플레인을 사진으로 보면 그야말로 <strong>구리 케이블이 5,000가닥</strong> 정도 깔린 광경입니다. 광케이블이 아니라 구리를 쓰는 이유는, 짧은 거리에서 광-전 변환 손실보다 구리 직결이 효율적이기 때문이고, NVIDIA가 직접 이 케이블 설계도 합니다. 즉 NVIDIA는 칩 뿐 아니라 케이블 어셈블리까지 파는 회사가 되었습니다.</p>
<ul>
<li>검색 키워드: <code>NVL72 copper backplane</code>, <code>NVLink Switch chip</code>, <code>Rack-scale GPU domain</code>, <code>NVSwitch fabric</code></li>
</ul>
</blockquote>
<h3 id="네-번째-메커니즘-한-도메인-크기가-왜-중요한가">네 번째 메커니즘: 한 도메인 크기가 왜 중요한가</h3>
<p>NVIDIA가 NVL72에 이렇게 돈을 들이는 이유는 무엇일까요? GPU 72장을 그냥 외부 네트워크로 묶으면 안 되나요?</p>
<p>답은 <strong>LLM 워크로드의 통신 패턴</strong>에 있습니다.</p>
<ul>
<li><strong>LLM 추론의 KV cache</strong>: 트랜스포머는 매 토큰마다 이전 토큰들의 Key/Value를 캐싱해 둡니다. 모델이 커지고 컨텍스트 길이가 길어질수록 이 KV cache가 거대해집니다(수십 GB~수백 GB). 이게 한 NVLink 도메인 안에 다 들어가야 토큰당 latency가 안 무너집니다.</li>
<li><strong>MoE(Mixture of Experts)</strong>: 최신 LLM의 절반 이상이 MoE 구조입니다. 토큰마다 256개 전문가 중 8개를 골라서 라우팅하는데, 이 라우팅 통신이 NVLink 도메인을 넘어가면 추론 throughput이 폭락합니다.</li>
<li><strong>Tensor Parallelism</strong>: 한 행렬 곱을 여러 GPU에 쪼개는 분산 학습 기법. 매 layer마다 GPU 사이에 결과를 교환해야 하므로, 통신 대역폭이 곧 학습 속도입니다.</li>
</ul>
<p>요약하면 <strong>&quot;한 NVLink 도메인 = 모델 한 덩어리가 빠르게 들어갈 수 있는 단일 메모리 풀&quot;</strong> 입니다. 도메인이 클수록 더 큰 모델을 빠르게 돌릴 수 있고, 도메인 경계를 넘는 순간 속도가 한 자릿수 떨어집니다.</p>
<p>H100 시대의 NVLink 도메인은 8장(HGX 보드 한 장)이었습니다. NVL72에서 그게 72장으로 9배 커진 것이 Blackwell 세대의 핵심 가치 명제입니다.</p>
<h3 id="다섯-번째-메커니즘-로드맵--nvl144-nvl576-그리고-그-이후">다섯 번째 메커니즘: 로드맵 — NVL144, NVL576, 그리고 그 이후</h3>
<p>NVIDIA가 발표한 차세대 로드맵은 NVLink 도메인을 계속 키우는 방향입니다.</p>
<table>
<thead>
<tr>
<th>세대</th>
<th>랙 시스템</th>
<th>NVLink 도메인 패키지</th>
<th>랙당 다이</th>
<th>출시</th>
</tr>
</thead>
<tbody><tr>
<td>Hopper</td>
<td>GH200 NVL32</td>
<td>32</td>
<td>32</td>
<td>2023~</td>
</tr>
<tr>
<td><strong>Blackwell</strong></td>
<td><strong>GB200 NVL72</strong></td>
<td><strong>72</strong></td>
<td><strong>144</strong> (dual-die)</td>
<td><strong>2024 양산</strong></td>
</tr>
<tr>
<td>Blackwell Ultra</td>
<td>GB300 NVL72</td>
<td>72</td>
<td>144 (dual-die)</td>
<td>2025</td>
</tr>
<tr>
<td><strong>Rubin</strong></td>
<td><strong>Vera Rubin NVL144</strong></td>
<td><strong>144</strong></td>
<td><strong>288</strong> (dual-die)</td>
<td><strong>2026 H2</strong></td>
</tr>
<tr>
<td>Rubin Ultra</td>
<td><strong>NVL576</strong></td>
<td><strong>576</strong></td>
<td>1,152 (dual-die)</td>
<td><strong>2027 H2</strong></td>
</tr>
<tr>
<td>Feynman</td>
<td>(미정)</td>
<td>TBD</td>
<td>TBD</td>
<td>2028+</td>
</tr>
</tbody></table>
<p>NVL 뒤의 숫자는 <strong>한 NVLink 도메인에 묶이는 GPU 패키지(소켓) 수</strong>를 가리킵니다. 다이 수가 아닙니다.</p>
<ul>
<li>Blackwell부터 GPU 한 패키지에 다이 2장이 붙는 <strong>dual-die 패키지</strong>입니다. 한 패키지 안에서 두 다이는 NV-HBI(약 10 TB/s)로 직결되어, 소프트웨어가 볼 땐 한 장의 GPU로 보입니다. 그래서 NVL72는 패키지 72개 = 다이 144개.</li>
<li><strong>NVL72 → NVL144 → NVL576</strong> 으로 가면서 NVIDIA는 한 NVLink 도메인에 묶는 <strong>물리 패키지 수 자체를 2~8배씩 다이렉트 스케일업</strong>하고 있습니다. 마케팅 숫자 트릭이 아니라 실제로 도메인 크기가 커지는 중입니다.</li>
<li>Rubin Ultra의 NVL576은 한 랙당 GPU 패키지 576개(다이 1,152개). 직관적으로 떠올렸던 &quot;랙 한 대에 칩 몇 백 개&quot;가 이 세대에서 맞아 들어갑니다.</li>
</ul>
<p>이 로드맵의 의미는 단순합니다. NVIDIA는 <strong>NVLink 도메인을 매 세대마다 2~8배씩 키우는 회사</strong>가 되었습니다. 그리고 그 키우는 작업의 절반은 GPU 자체가 아니라 <strong>NVLink Switch, 케이블, 전력·냉각 시스템</strong>입니다. 이게 바로 다음 4장의 주제, 그리고 실적표 Networking 폭증의 핵심입니다.</p>
<hr>
<h2 id="4장-네트워크가-곧-컴퓨터다--랙-사이-scale-out">4장. 네트워크가 곧 컴퓨터다 — 랙 사이 Scale-Out</h2>
<p>NVL72 한 랙은 거대하지만, GPT-5급 모델 학습에는 그것도 부족합니다. 실제 AI 데이터센터는 NVL72 랙을 <strong>수십~수백 대</strong> 묶어 운영합니다. 그 묶음을 NVIDIA는 <strong>DGX SuperPOD</strong>라고 부르고, 더 큰 단위는 그냥 &quot;AI Factory&quot;라고 부릅니다.</p>
<p>이 단계에서 등장하는 게 실적표에서 본 <strong>Networking</strong> 세그먼트의 본체입니다.</p>
<h3 id="첫-번째-메커니즘-대역폭의-절벽">첫 번째 메커니즘: 대역폭의 절벽</h3>
<p>먼저 숫자를 직관적으로 잡아 봅시다.</p>
<table>
<thead>
<tr>
<th>통신 구간</th>
<th>양방향 대역폭 (GPU당)</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td>GPU 내부 HBM ↔ 연산기</td>
<td>8 TB/s</td>
<td>CPU L1 캐시</td>
</tr>
<tr>
<td>같은 랙 안 GPU ↔ GPU (NVLink 5)</td>
<td>1.8 TB/s</td>
<td>CPU L2/L3 캐시</td>
</tr>
<tr>
<td><strong>랙 간 (InfiniBand NDR/Spectrum-X)</strong></td>
<td><strong>50~100 GB/s</strong></td>
<td><strong>메인 DRAM</strong></td>
</tr>
<tr>
<td>외부 인터넷 / 데이터센터 외</td>
<td>~1 GB/s</td>
<td>디스크</td>
</tr>
</tbody></table>
<p><strong>같은 랙 안과 랙 사이의 대역폭 차이가 18~36배</strong> 납니다. 통신 거리가 미터 단위로 길어지는 것만으로 한 자릿수 이상 떨어지는 거죠. 이게 &quot;Scale-Up 도메인 안에 워크로드를 가둬야 한다&quot;는 원칙이 나오는 이유입니다.</p>
<p>그렇다고 모델 학습을 한 랙에 가두기는 불가능합니다. 학습 워크로드는 어차피 여러 랙으로 펴야 하고, 그러면 이 50~100 GB/s 통로를 통한 <strong>분산 학습 통신</strong>이 학습 속도를 결정합니다.</p>
<h3 id="두-번째-메커니즘-rdma와-gpudirect--cpu-우회">두 번째 메커니즘: RDMA와 GPUDirect — CPU 우회</h3>
<p>랙 사이 통신의 첫 번째 핵심은 <strong>GPU 메모리에서 다른 랙의 GPU 메모리로 데이터를 옮길 때, CPU를 거치지 않는다</strong>는 것입니다.</p>
<p>전통적인 TCP/IP 통신은 다음 경로를 거칩니다.</p>
<pre><code>GPU A의 HBM → CPU A의 DRAM → NIC A → 네트워크 → NIC B → CPU B의 DRAM → GPU B의 HBM</code></pre><p>각 단계마다 메모리 복사가 일어나고, CPU 인터럽트 처리가 들어갑니다. 100GbE 네트워크라도 실효 대역폭은 절반 이하로 떨어지고, latency도 ms 단위가 됩니다.</p>
<p><strong>RDMA (Remote Direct Memory Access)</strong> 는 이 경로를 단축합니다.</p>
<pre><code>GPU A의 HBM → NIC A → 네트워크 → NIC B → GPU B의 HBM</code></pre><p>CPU와 메인메모리를 우회합니다. 이걸 GPU 메모리까지 적용한 것이 <strong>GPUDirect RDMA</strong>이고, 현대 AI 클러스터의 기본 통신 메커니즘입니다.</p>
<p>백엔드 개발자 비유로 옮기면, gRPC over TCP가 아니라 <strong>사용자 공간 zero-copy + DMA로 직결되는 메시지 패싱</strong>입니다. CPU는 통신 setup에만 끼고, 실제 데이터 이동은 NIC ASIC이 다 합니다.</p>
<p>이 RDMA를 지원하는 NIC 칩셋 시장의 1위가 <strong>Mellanox</strong>였고, NVIDIA가 2020년에 70억 달러에 인수했습니다. 그 결과가 지금 NVIDIA의 ConnectX·BlueField 시리즈 NIC, 그리고 InfiniBand·Spectrum-X 스위치입니다. <strong>Mellanox 인수가 NVIDIA의 진짜 게임체인저였다</strong>는 평가가 지금 와서 보면 명확합니다.</p>
<h3 id="세-번째-메커니즘-all-reduce--분산-학습의-본체-통신">세 번째 메커니즘: All-Reduce — 분산 학습의 본체 통신</h3>
<p>분산 학습에서 가장 자주 일어나는 통신은 <strong>All-Reduce</strong>입니다. 무엇인가 하면:</p>
<ul>
<li>GPU 각각이 자기 데이터로 gradient 계산을 합니다 (각 GPU 결과는 다 다름).</li>
<li>모든 GPU가 자기 gradient를 다른 모든 GPU에게 알리고, 합산한 결과를 다 같이 받아야 합니다.</li>
<li>그래야 다음 step에서 모든 GPU가 동일한 모델 파라미터 업데이트를 수행할 수 있습니다.</li>
</ul>
<p>이걸 N개 GPU에 대해 naive하게 하면 통신량이 O(N²)으로 폭발합니다. NCCL(NVIDIA Collective Communications Library)이 똑똑한 토폴로지(ring, tree, double binary tree)를 골라서 O(N) 수준으로 줄입니다.</p>
<p>PyTorch에서 <code>dist.all_reduce(tensor)</code> 한 줄을 호출하면, 그 아래에서 NCCL이 현재 클러스터 토폴로지를 분석(NVLink 도메인, IB 도메인, 다단 스위치)하고, 적합한 알고리즘을 선택하고, GPUDirect RDMA로 각 GPU 사이 데이터를 송수신하고, 완료 시 callback으로 통보합니다.</p>
<p>이 작업이 한 step당 수천 번 일어납니다. 그래서 NCCL이 사실상 분산 학습의 진짜 OS이고, NVIDIA가 NCCL 코드를 직접 관리하는 이유입니다.</p>
<h3 id="네-번째-메커니즘-infiniband-vs-spectrum-x--두-가지-무손실-네트워크">네 번째 메커니즘: InfiniBand vs Spectrum-X — 두 가지 무손실 네트워크</h3>
<p>NVIDIA는 데이터센터 GPU 간 네트워크로 두 가지를 제공합니다.</p>
<ul>
<li><strong>InfiniBand (Quantum 시리즈)</strong> — 슈퍼컴퓨터 전통의 무손실 네트워크. AI 학습 클러스터의 기본. 현행 NDR 세대는 400Gb/s, XDR 800Gb/s가 등장 중.</li>
<li><strong>Spectrum-X (이더넷)</strong> — 일반 데이터센터 표준 이더넷 위에 NVIDIA의 ASIC과 NIC을 얹어, AI 워크로드 전용으로 튜닝한 무손실 이더넷.</li>
</ul>
<p>두 가지 다 핵심 가치는 <strong>무손실(lossless)</strong> 입니다. 일반 이더넷은 폭주하면 패킷을 버리고, TCP가 재전송하는 모델인데, AI 학습에서는 한 패킷이 늦으면 GPU 수백 장이 그 동기화를 기다리며 같이 놀게 됩니다. 그래서 <strong>하드웨어 단에서 패킷이 절대 안 버려지는 네트워크가 필요</strong>합니다.</p>
<p>NVIDIA의 Spectrum-X가 이 무손실을 이더넷 위에서 달성하는 방식이 흥미롭습니다.</p>
<ul>
<li><strong>Adaptive Routing</strong> — 스위치 ASIC이 실시간으로 폭주 지점을 감지해 패킷 경로를 우회시킴.</li>
<li><strong>Congestion Control</strong> — NIC이 송신 속도를 자동 조절해서 스위치 큐가 꽉 차지 않게 함.</li>
<li><strong>Performance Isolation</strong> — 한 워크로드가 다른 워크로드의 통신을 방해하지 않도록 격리.</li>
</ul>
<p>이 모든 게 ASIC 하드웨어에서 실시간으로 일어납니다. 일반 이더넷 스위치라면 운영체제/펌웨어 소프트웨어에서 처리하는 일을, NVIDIA는 칩에 박은 것입니다.</p>
<h3 id="다섯-번째-메커니즘-sharp--네트워크가-연산까지-한다">다섯 번째 메커니즘: SHARP — 네트워크가 연산까지 한다</h3>
<p>여기서 한 발 더 나아간 게 <strong>SHARP (Scalable Hierarchical Aggregation and Reduction Protocol)</strong> 입니다.</p>
<p>위에서 본 All-Reduce를 다시 봅시다. N개 GPU가 각자 가진 텐서를 합산하는 작업입니다. 전통적으로는 GPU끼리 직접 데이터를 주고받으며 합산했습니다.</p>
<p>SHARP는 다음과 같이 바꿉니다.</p>
<ul>
<li>GPU들이 각자의 텐서를 InfiniBand 스위치에 보냅니다.</li>
<li><strong>스위치 ASIC 안에 있는 reduction 회로가 그 텐서들을 받으면서 실시간으로 합산합니다.</strong></li>
<li>합산 결과만 GPU에게 보냅니다.</li>
</ul>
<p>즉 <strong>네트워크 스위치가 곧 연산 가속기 역할</strong>을 합니다. GPU는 자기 텐서만 보내고 결과만 받으면 됩니다. 통신량이 절반 이하로 줄고, 동기화 시간이 짧아집니다.</p>
<p>이 기능을 가능하게 하는 SHARP는 InfiniBand 표준 위에서 NVIDIA가 정의한 확장이고, Spectrum-X에도 적용되고 있습니다. &quot;스위치가 단순 패킷 포워더가 아니라 연산기&quot;라는 발상이 NVIDIA의 네트워킹 사업이 단순 백본 회사가 아닌 이유입니다.</p>
<blockquote>
<p><strong>더 깊이 공부하고 싶다면?</strong>
SHARP는 reduction 외에도 broadcast, barrier 같은 다른 집합 통신 연산도 가속합니다. MPI(Message Passing Interface)를 알면 친숙한 개념이고, MPI를 GPU에 맞게 재해석한 게 NCCL + SHARP의 조합이라고 볼 수 있습니다.</p>
<ul>
<li>검색 키워드: <code>NCCL collective</code>, <code>SHARP In-Network Computing</code>, <code>Quantum InfiniBand switch</code>, <code>Spectrum-X Ethernet AI fabric</code></li>
</ul>
</blockquote>
<h3 id="deep-dive-왜-mellanox를-70억-달러에-샀나">Deep Dive: 왜 Mellanox를 70억 달러에 샀나</h3>
<p>2020년 NVIDIA의 Mellanox 인수는 당시에는 &quot;왜 GPU 회사가 NIC 회사를 사지?&quot;라는 평가가 많았습니다. 지금 와서 보면 명확합니다.</p>
<ul>
<li>GPU를 데이터센터 단위로 묶으려면 <strong>무손실 RDMA 네트워크</strong>가 필수.</li>
<li>그걸 만들 수 있는 회사가 사실상 Mellanox뿐이었음.</li>
<li>GPU와 네트워크 칩을 같은 회사가 같이 만들면, NCCL·NVLink·NIC·스위치를 <strong>수직 통합</strong>해서 경쟁사가 따라오기 어려운 성능을 낼 수 있음.</li>
<li>부가적으로 NIC에 ARM 코어를 박은 <strong>DPU(BlueField)</strong> 가 등장. DPU는 호스트 CPU 일을 일부 떠안아서 가상화·보안·스토리지 처리를 NIC에서 함. 이게 또 별도 사업이 됨.</li>
</ul>
<p>결과적으로 NVIDIA의 매출 구조가 이렇게 바뀝니다.</p>
<ul>
<li><strong>칩 한 장</strong> 팔던 회사 → <strong>랙 한 대</strong>를 파는 회사</li>
<li>랙 한 대에는 GPU만 들어가는 게 아니라 <strong>NVLink Switch 9개 + InfiniBand/Spectrum-X 스위치 + NIC 수십 장 + DPU + 케이블 5000가닥</strong>이 같이 들어감</li>
<li>모두 NVIDIA 마진 안에 있음</li>
</ul>
<p>이게 실적표의 &quot;Networking +199% YoY&quot;의 본질입니다. GPU 출하가 늘면 자동으로 네트워킹 매출이 따라오는 구조를 만들어 놓은 거죠.</p>
<hr>
<h2 id="5장-다시-실적표--세그먼트를-엔지니어-시선으로-재해석">5장. 다시 실적표 — 세그먼트를 엔지니어 시선으로 재해석</h2>
<p>여기까지 메커니즘을 이해하고 나면, 처음에 본 실적표가 완전히 다르게 읽힙니다.</p>
<pre><code>Data Center Compute     : $60.4B   YoY +77%
Data Center Networking  : $14.8B   YoY +199%
Edge Computing          : $6.4B    YoY +29%</code></pre><p>세그먼트별로 정리해 보겠습니다.</p>
<h3 id="data-center-compute--gpu--grace-cpu-칩-자체">Data Center Compute = &quot;GPU + Grace CPU 칩 자체&quot;</h3>
<ul>
<li>Blackwell B200/B300, 다음 세대 Rubin 등 GPU 다이의 칩값</li>
<li>Grace CPU의 칩값</li>
<li>HBM 비용 포함 (NVIDIA가 사서 패키지에 박아 파는 구조)</li>
<li>즉 우리가 흔히 &quot;엔비디아 칩 매출&quot;이라고 부르는 것</li>
</ul>
<p>가장 익숙한 부분이지만, 사실 이게 가장 단순한 부분입니다. 매 분기 새 칩을 더 많이 팔면 매출이 늘어납니다. +77% YoY는 Blackwell 양산이 본격화되면서 일어난 일이고, Rubin이 H2 2026에 나오면 또 한 번 점프가 예상됩니다.</p>
<h3 id="data-center-networking--랙-안과-사이를-잇는-모든-것">Data Center Networking = &quot;랙 안과 사이를 잇는 모든 것&quot;</h3>
<p>이게 본 글의 핵심 통찰입니다. Networking 매출의 구성:</p>
<ul>
<li><strong>NVLink Switch 칩</strong> (랙 안 GPU 간 연결) — NVL72 한 랙에 9개씩</li>
<li><strong>NVLink 케이블 어셈블리</strong> — 한 랙에 약 5000가닥의 구리 케이블</li>
<li><strong>InfiniBand Quantum 스위치</strong> (랙 간 연결, 무손실)</li>
<li><strong>Spectrum-X 이더넷 스위치</strong> (랙 간 연결, 이더넷 진영용 대안)</li>
<li><strong>ConnectX NIC, BlueField DPU</strong> (서버 측 네트워크 카드)</li>
</ul>
<p>+199% YoY는 이게 GPU 출하의 <strong>종속 변수</strong>이기 때문에 GPU보다 더 가파르게 따라옵니다. GB200 NVL72 한 랙이 팔리면:</p>
<ul>
<li>GPU 72장 → Compute 매출</li>
<li>NVLink Switch 9개 + 케이블 + InfiniBand 스위치/NIC → Networking 매출 (랙 한 대당 0.5~0.7M$ 추정)</li>
</ul>
<p>그리고 한 랙만 파는 게 아니라 <strong>DGX SuperPOD(8랙) 단위</strong>로 팔리면, 랙 사이를 잇는 InfiniBand 스위치가 추가됩니다. 즉 클라이언트가 큰 클러스터를 살수록 Networking 비중이 누적적으로 커집니다.</p>
<p>이게 NVIDIA가 단순 &quot;GPU 출하 사이클&quot;이 아니라 <strong>&quot;AI Factory 인프라 사이클&quot;</strong> 에 있는 이유입니다.</p>
<h3 id="edge-computing--데이터센터-바깥의-nvidia-칩">Edge Computing = &quot;데이터센터 바깥의 NVIDIA 칩&quot;</h3>
<p>NVIDIA가 <strong>FY2027 Q1(2026년 5월 발표)</strong> 부터 리포팅 구조를 개편하면서 새로 분리한 세그먼트로, 데이터센터가 아닌 곳에서 굴러가는 NVIDIA 칩들을 묶습니다.</p>
<ul>
<li><strong>Jetson 시리즈</strong> — 임베디드 GPU 모듈. 드론·로봇·산업 카메라·자율주행 prototype.</li>
<li><strong>DRIVE 시리즈</strong> — 자율주행 자동차용 SoC. Mercedes·BYD·Toyota 등이 채택.</li>
<li><strong>Isaac / GR00T</strong> — 로봇 학습/추론 플랫폼. Humanoid 로봇 기업들이 NVIDIA 칩 위에서 정책 학습.</li>
<li><strong>Omniverse 산업용 엣지</strong> — 공장·물류센터에서 디지털 트윈 + 실시간 추론.</li>
</ul>
<p>+29% YoY는 데이터센터에 비하면 작아 보이지만, 절대 규모 6.4B는 결코 작지 않고, 무엇보다 <strong>AI가 클라우드에서 물리 세계로 흘러내려가는 다음 단계의 출발점</strong>입니다. 본인이 본 그림이 GH200(데이터센터)이었다면, Edge Computing 쪽은 Jetson Thor(로봇), DRIVE Thor(자동차) 같은 별도 칩 라인업입니다.</p>
<h3 id="한눈에-정리">한눈에 정리</h3>
<pre><code class="language-mermaid">graph LR
    Customer[고객사&lt;br/&gt;OpenAI · Meta · Tesla · 자동차사] --&gt; AIFactory[AI Factory&lt;br/&gt;데이터센터]
    Customer --&gt; EdgeProducts[Edge 제품&lt;br/&gt;로봇 · 자동차 · 공장]

    AIFactory --&gt; Compute[Data Center Compute&lt;br/&gt;GPU + Grace CPU 칩]
    AIFactory --&gt; Networking[Data Center Networking&lt;br/&gt;NVLink Switch + IB/Spectrum-X&lt;br/&gt;NIC/DPU + 케이블]

    EdgeProducts --&gt; Edge[Edge Computing&lt;br/&gt;Jetson + DRIVE + Isaac/Omniverse]

    Compute -.-&gt;|랙 단위 판매로 자동 결합| Networking</code></pre>
<p>핵심은 <strong>Compute와 Networking이 강하게 결합</strong>되어 있다는 것입니다. 고객이 GB200 랙 한 대를 사면 두 세그먼트가 자동으로 같이 매출에 잡힙니다. 이게 NVIDIA의 진짜 비즈니스 모델이고, AMD·Intel이 단품 GPU만 팔아서 따라잡기 어려운 구조적 해자입니다.</p>
<hr>
<h2 id="결론-백엔드-개발자가-가져갈-4가지-멘탈-모델">결론: 백엔드 개발자가 가져갈 4가지 멘탈 모델</h2>
<p>긴 글이었습니다. 처음에 가졌던 의문 — &quot;GPU는 어떻게 돌아가나? CPU와는 어떻게 통신하나? 수많은 GPU를 어떻게 엮나? Networking은 왜 돈을 그렇게 버나?&quot; — 에 답하는 과정에서 본 메커니즘을, 멘탈 모델 네 개로 압축합니다.</p>
<p><strong>1. GPU = 별도의 메모리 공간을 가진 비동기 데이터 병렬 가속기</strong></p>
<p>CPU 프로그래밍에서 &quot;다른 코어&quot;는 같은 메모리를 공유합니다. GPU는 <strong>같은 메인보드의 다른 컴퓨터</strong>입니다. 별도 메모리, 별도 명령 시스템, 별도 실행 모델. CPU와 GPU 사이는 <strong>메시지 큐 기반 비동기 통신</strong>으로 움직입니다. PyTorch 한 줄이 GPU를 직접 실행시키지 않고, 큐에 명령을 던지고 끝납니다. 이 모델을 머릿속에 박아두면 GPU 디버깅과 최적화의 7할이 풀립니다.</p>
<p><strong>2. 모든 레이어에서 대역폭이 한 자릿수씩 떨어진다</strong></p>
<pre><code>GPU 내부 HBM:        8 TB/s
같은 랙 내 NVLink:   1.8 TB/s   (4.4배 ↓)
랙 사이 InfiniBand:  50-100 GB/s (18-36배 ↓)
외부 인터넷:         1 GB/s     (50-100배 ↓)</code></pre><p>CPU 캐시 계층(L1→L2→L3→DRAM)이 한 자릿수씩 느려지는 것과 같은 구조가, GPU 컴퓨팅에서는 <strong>칩→랙→랙 사이→데이터센터 사이</strong>로 확장된 형태로 존재합니다. 워크로드를 최대한 빠른 레이어에 가두는 것이 모든 분산 학습 최적화의 본질입니다.</p>
<p><strong>3. NVLink 도메인 = 가상의 거대한 단일 GPU</strong></p>
<p>같은 NVL72 랙 안 72장 GPU는 소프트웨어 입장에서 &quot;한 대의 거대 GPU&quot;처럼 보입니다. 메모리 13.4 TB, 연산 1.44 exaFLOPS짜리 GPU 한 장이 가상으로 존재하는 셈입니다. 이 도메인이 클수록 더 큰 모델을 더 빠르게 돌릴 수 있고, 매 세대마다 NVIDIA가 이 도메인을 키우는 게 칩 회사가 아닌 <strong>시스템 회사로의 진화</strong>입니다.</p>
<ul>
<li>Hopper 시대: 8장 (HGX 보드)</li>
<li>Blackwell 시대: 72장 (NVL72)</li>
<li>Rubin Ultra 시대 (2027): 576장 (NVL576)</li>
</ul>
<p>직관적으로 떠올린 &quot;랙 한 대에 칩 몇 백 개&quot;는 2027년부터 진짜가 됩니다.</p>
<p><strong>4. NVIDIA는 칩이 아니라 &quot;랙 운영체제 + 인프라&quot;를 판다</strong></p>
<p>처음에 본 실적표를 다시 봅시다. Compute 60.4B + Networking 14.8B + Edge 6.4B. <strong>이 세 숫자가 따로 노는 게 아니라, 랙이라는 하나의 상품 안에 묶여 있습니다.</strong> GB200 NVL72 한 랙을 팔면 Compute(GPU 72장)와 Networking(스위치·NIC·케이블)이 같이 매출로 잡힙니다. NVIDIA는 GPU 회사가 아니라 <strong>랙 단위로 데이터센터를 통째로 설계해 파는 회사</strong>가 되었고, 그래서 Mellanox(네트워크), Run:ai(스케줄러), 그리고 NCCL 같은 소프트웨어 스택을 다 수직 통합한 것입니다.</p>
<p>이 통찰이 투자 관점에서도, 엔지니어 관점에서도 NVIDIA를 정확히 이해하는 출발점입니다.</p>
<ul>
<li>투자 관점에서는 &quot;GPU 출하 사이클&quot;이 아니라 &quot;AI Factory 인프라 사이클&quot;이라는 더 큰 곡선을 봐야 합니다.</li>
<li>엔지니어 관점에서는 &quot;다음 모델이 어디까지 커질 수 있는가&quot;가 GPU 다이가 아니라 <strong>NVLink 도메인 크기·메모리 용량·랙 전력</strong>에 달려 있다는 점을 봐야 합니다.</li>
</ul>
<p>처음에 막혔던 &quot;엣지 컴퓨팅? 컴퓨팅 네트워크?&quot;라는 의문에 이제 한 줄로 답할 수 있습니다.</p>
<ul>
<li><strong>Compute</strong>: 칩 자체. GPU와 Grace CPU.</li>
<li><strong>Networking</strong>: 랙 안과 랙 사이를 잇는 모든 것. NVLink Switch, InfiniBand, Spectrum-X, NIC, DPU, 케이블. GPU 출하의 종속 변수라 더 가파르게 큼.</li>
<li><strong>Edge Computing</strong>: 데이터센터 밖. Jetson(임베디드), DRIVE(자동차), Isaac/GR00T(로봇).</li>
</ul>
<p>그리고 그 모든 게 NVLink-C2C·NVLink·InfiniBand·RDMA·SHARP라는 수직 통합된 통신 스택 위에서 굴러갑니다. NVIDIA가 칩 회사라는 정의로는 더 이상 설명되지 않는 이유입니다.</p>
<hr>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://developer.nvidia.com/blog/nvidia-grace-hopper-superchip-architecture-in-depth/">NVIDIA Grace Hopper Superchip Architecture</a> — Grace + Hopper + NVLink-C2C 백서</li>
<li><a href="https://www.nvidia.com/en-us/data-center/gb200-nvl72/">GB200 NVL72 | NVIDIA</a> — 현행 주력 랙 시스템</li>
<li><a href="https://developer.nvidia.com/blog/inside-the-nvidia-rubin-platform-six-new-chips-one-ai-supercomputer/">Inside the NVIDIA Vera Rubin Platform</a> — Rubin 세대 공식 소개</li>
<li><a href="https://introl.com/blog/nvidia-vera-rubin-gpu-600kw-racks-2027">NVIDIA Vera Rubin: 600kW Racks by 2027 — Introl Blog</a> — Rubin Ultra NVL576 분석</li>
<li><a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#simt-architecture">NVIDIA CUDA Programming Guide — SIMT Architecture</a></li>
<li><a href="https://developer.nvidia.com/nccl">NCCL: NVIDIA Collective Communications Library</a> — 분산 학습 통신 표준</li>
<li><a href="https://www.nvidia.com/en-us/networking/spectrumx/">NVIDIA Spectrum-X Platform</a> — 이더넷 진영 AI 네트워크</li>
<li><a href="https://docs.nvidia.com/networking/category/sharp">Mellanox SHARP In-Network Computing</a> — 스위치 내 reduction</li>
<li><a href="https://investor.nvidia.com/financial-info/sec-filings/">NVIDIA Investor Relations — SEC Filings</a> — FY2027 Q1 CFO Commentary (2026년 5월 발표) 포함. 이번 분기부터 Data Center + Edge Computing 체제로 세그먼트 개편</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 1부 - 컨테이너는 존재하지 않는다]]></title>
            <link>https://velog.io/@l_cloud/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-1%EB%B6%80</link>
            <guid>https://velog.io/@l_cloud/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-1%EB%B6%80</guid>
            <pubDate>Sat, 17 Jan 2026 05:31:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 <code>docker run</code> 명령어로 컨테이너를 띄워본 경험은 있지만, 쿠버네티스는 처음 접하는 독자를 대상으로 합니다. 복잡한 코드는 다루지 않으며, 쿠버네티스가 제어하는 기반 기술을 추상적으로 살펴봅니다. 본 내용은 시리즈로 연재될 예정이며, 컨테이너부터 쿠버네티스까지 차근차근 이야기합니다.</p>
</blockquote>
<h2 id="1부-컨테이너는-존재하지-않는다">1부. 컨테이너는 존재하지 않는다</h2>
<h3 id="쿠버네티스-그리고-컨테이너">쿠버네티스, 그리고 컨테이너</h3>
<p>쿠버네티스를 한 문장으로 정의하면 <strong>&#39;컨테이너 오케스트레이션 도구&#39;</strong>입니다. 지휘자가 오케스트라를 지휘하듯, 수많은 컨테이너를 관리한다는 뜻이죠.</p>
<p>그런데 <strong>&#39;컨테이너&#39;</strong>란 정확히 무엇일까요?</p>
<p>도커를 사용해 봤다면 컨테이너라는 개념은 익숙할 것입니다. 
<code>docker run</code> 을 하면 독립된 환경인 컨테이너가 띄워진다고 많이들 이야기 합니다. 하지만 기술적인 관점에서 엄밀하게 말하면, <strong>리눅스 운영체제에 &#39;컨테이너&#39;라는 기술은 존재하지 않습니다.</strong></p>
<h3 id="컨테이너의-실체-격리된-프로세스">컨테이너의 실체: 격리된 프로세스</h3>
<p>크롬이 카카오톡의 메모리를 훔쳐볼 수 없듯, 기본적으로 프로세스 간 메모리는 철저히 분리되어 있습니다.</p>
<p>하지만 파일시스템과 네트워크는 사정이 다릅니다. 카카오톡 파일 전송 창을 열면 내 컴퓨터의 모든 폴더가 다 들여다보이고, 모든 프로그램이 하나의 IP 주소를 공유하기 때문에 서로 다른 <strong>포트</strong>를 써야만 충돌 없이 통신할 수 있습니다.</p>
<p>컨테이너는 여기에 <strong>Namespace</strong>와 <strong>Cgroup</strong>을 얹습니다. 파일시스템과 네트워크까지 격리하고, 자원 사용량도 제한하죠. 이를 통해 내부는 격리된 환경이지만, 호스트 입장에서 그 실체는 크롬이나 카카오톡과 다를 바 없는 프로세스 하나일 뿐입니다. </p>
<h3 id="첫-번째-메커니즘-namespace">첫 번째 메커니즘: Namespace</h3>
<p>리눅스 Namespace는 프로세스에게 <strong>&quot;시스템의 독립된 뷰&quot;</strong>를 제공합니다. 쉽게 말해 프로세스를 속이는 기술입니다.</p>
<ul>
<li><strong>mnt:</strong> 파일시스템을 분리합니다. 프로세스는 자신에게 할당된 루트 디렉토리(<code>/</code>)만 볼 수 있습니다.</li>
<li><strong>pid:</strong> 프로세스 ID를 분리합니다. 컨테이너 안에서 실행된 프로세스는 자신이 PID 1번이라고 인식하지만, 호스트에서 보면 15,342번 같은 일반 프로세스일 뿐입니다.</li>
<li><strong>net:</strong> 네트워크를 분리합니다. 자신만의 IP 주소, 포트, 라우팅 테이블을 갖습니다.</li>
<li><strong>uts:</strong> 호스트명을 분리합니다. 컨테이너마다 다른 hostname을 가질 수 있습니다.</li>
<li><strong>ipc:</strong> 프로세스 간 통신을 분리합니다. 공유 메모리나 파이프 같은 자원을 격리합니다.</li>
<li><strong>user:</strong> 사용자 권한을 분리합니다. 호스트에서는 일반 사용자여도 컨테이너 안에서는 root로 보이게 할 수 있습니다.</li>
</ul>
<blockquote>
<p>** 더 깊이 공부하고 싶다면?**</p>
</blockquote>
<p><strong>격리</strong>라고 해서 모든 Namespace를 분리해야 하는 건 아닙니다. 필요하다면 특정 Namespace만 공유할 수도 있습니다.</p>
<blockquote>
<p>실제로 쿠버네티스의 파드는 여러 컨테이너가 <strong>Network Namespace를 공유</strong>하는 구조입니다. 그래서 같은 파드 속 컨테이너들은 <code>localhost</code>로 통신이 가능한 것이죠.</p>
<ul>
<li>검색 키워드: <code>Linux Namespace Sharing</code>, <code>docker --net container</code>, <code>pause container</code></li>
</ul>
</blockquote>
<h3 id="두-번째-메커니즘-cgroup">두 번째 메커니즘: Cgroup</h3>
<p>격리만 한다고 끝이 아닙니다. 어떤 프로세스가 CPU를 혼자 다 써버리면 안 되니까요.</p>
<p><strong>Cgroup</strong>은 프로세스가 소비할 수 있는 리소스의 양을 제한합니다. CPU 코어 수, 메모리 상한, 네트워크 대역폭 등을 지정할 수 있고, 메모리 제한을 넘기면 강제 종료되기도 합니다.</p>
<p>이 제한은 계층 구조로 만들 수 있습니다. 상위 그룹에 10GB를 할당하고, 그 안에서 하위 프로세스들이 나눠 쓰게 할 수 있죠.</p>
<p>결국 컨테이너란 <strong>Namespace</strong>로 환경이 격리되고, <strong>Cgroup</strong>으로 자원이 제한된 리눅스 프로세스입니다.</p>
<blockquote>
<p>** 깊이 공부하고 싶다면?**
Cgroup은 단순히 제한만 거는 게 아니라, 자원 사용량을 측정하는 역할도 합니다. <code>docker stats</code> 명령어가 작동하는 이유이기도 합니다.</p>
<ul>
<li>검색 키워드: <code>Cgroup v1 vs v2</code>, <code>Systemd Cgroup driver</code></li>
</ul>
</blockquote>
<h3 id="deep-dive-docker-run의-내부">Deep Dive: <code>docker run</code>의 내부</h3>
<p>컨테이너의 핵심 원리를 알았으니 이제 <code>docker run</code> 명령어가 하는 일을 자세히 들여다봅시다.
<code>docker run -d nginx</code>를 실행하면 내부에서는 여러 컴포넌트가 역할을 나눠 순차적으로 동작합니다.</p>
<p><strong>1. Docker CLI: 요청의 시작</strong>
입력한 명령어는 REST API 형태로 변환되어 도커 데몬에게 전송됩니다.</p>
<p><strong>2. dockerd: API 게이트웨이</strong>
요청을 수신한 도커 데몬은 전체 흐름을 담당하지만, 직접 컨테이너를 생성하지는 않습니다. <strong>containerd</strong>에게 작업을 위임합니다.</p>
<p><strong>3. containerd: 라이프사이클 관리자</strong>
컨테이너의 생애 주기를 관리하는 핵심 계층입니다.</p>
<ul>
<li>이미지 확보: 로컬에 이미지가 없으면 레지스트리에서 다운로드합니다.</li>
<li>스냅샷 준비: 이미지를 압축 해제하고 실행 가능한 상태로 만듭니다.</li>
<li>실행 위임: 준비가 끝나면 저수준 런타임인 <strong>runc</strong>를 호출합니다.</li>
</ul>
<p><strong>4. runc: 커널 인터페이스</strong>
runc는 리눅스 커널의 기능을 직접 제어하는 실행기입니다. OCI 표준을 따르며, 아주 짧게 실행되고 사라집니다.</p>
<ul>
<li>Namespace 생성: 커널에게 요청해 격리된 공간을 만듭니다.</li>
<li>Cgroup 설정: CPU와 메모리 제한을 설정합니다.</li>
<li>프로세스 실행: 격리된 공간 안에서 nginx 프로세스를 실행합니다.</li>
<li>종료: 프로세스 실행이 완료되면 runc는 즉시 종료됩니다.</li>
</ul>
<p><strong>5. containerd-shim: 프로세스 관리</strong>
runc가 종료된 후에도 컨테이너는 계속 실행되어야 합니다. shim이 부모 프로세스 역할을 맡아 컨테이너를 관리합니다.</p>
<ul>
<li>stdout/stderr 같은 입출력을 관리합니다.</li>
<li>컨테이너가 종료되면 종료 코드를 상위 계층에 보고합니다.</li>
</ul>
<p>Gemini를 통해 구조를 그려보면 아래와 같습니다. </p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/09cc5b50-e258-4d21-96d8-64fdeba8740c/image.png" alt=""></p>
<h3 id="왜-이-계층-구조를-알아야-할까">왜 이 계층 구조를 알아야 할까?</h3>
<p><strong>&quot;도커가 알아서 해주는데 굳이?&quot;</strong> 라고 생각할 수 있습니다.  실제로 평소에는 이런 내부 구조를 몰라도 컨테이너는 잘 돌아가기 때문에 소홀해지기 쉽습니다.</p>
<p>하지만 문제가 생겼을 때 이 구조를 알고 있으면 디버깅이 훨씬 수월해집니다. 컨테이너가 안 뜨거나, 네트워크가 안 되거나, 자원 제한이 이상하게 동작할 때 <strong>&quot;어느 단계에서 막혔지?&quot;</strong>로 접근할 수 있습니다.</p>
<p>또한 이 구조는 쿠버네티스의 작동 원리와 직결됩니다. 쿠버네티스의 각 노드에는 <strong>Kubelet</strong>이 설치되어 있는데, 이 <strong>Kubelet</strong>이 <strong>dockerd</strong>의 역할을 대신해 <strong>containerd</strong>와 <strong>runc</strong>에게 명령을 내립니다. 결국 <strong>Pod</strong>를 만드는 과정도 우리가 살펴본 흐름과 동일합니다.
<strong>Kubelet</strong>과 <strong>Pod</strong>에 대해서는 이후 시리즈에서 자세히 다루겠습니다. 지금은 <strong>&quot;쿠버네티스도 결국 같은 리눅스 커널 기능을 쓴다&quot;</strong>는 점만 기억해 주세요.</p>
<h3 id="다음-이야기-이미지는-어디서-오는가">다음 이야기: 이미지는 어디서 오는가</h3>
<p>프로세스가 어떻게 격리되는지, 그리고 실제 실행이 어떻게 일어나는지 살펴봤습니다.
그런데 아직 풀리지 않은 의문이 하나 있습니다.
<code>docker run nginx</code>를 실행하면 순식간에 <code>nginx</code> 실행에 필요한 파일들이 준비됩니다. 격리된 프로세스일 뿐이라면서, 이 수많은 파일들은 대체 어디서 오는 걸까요?</p>
<p>다음 2부에서는 이 격리된 공간을 채우는 <code>이미지</code>와 <code>레이어</code>, 그리고 <code>OverlayFS</code>에 대해 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SideImpact 개선기 2편: 무거운 Router 덜어내기]]></title>
            <link>https://velog.io/@l_cloud/SideImpact-%EA%B0%9C%EC%84%A0%EA%B8%B0-2%ED%8E%B8-%EB%AC%B4%EA%B1%B0%EC%9A%B4-Router-%EB%8D%9C%EC%96%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@l_cloud/SideImpact-%EA%B0%9C%EC%84%A0%EA%B8%B0-2%ED%8E%B8-%EB%AC%B4%EA%B1%B0%EC%9A%B4-Router-%EB%8D%9C%EC%96%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Tue, 16 Sep 2025 12:17:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="">이전 글</a>에서는 개발 환경 배포 자동화를 공유했습니다. 이번 글은 코드 리팩토링 여정을 다룹니다.</p>
</blockquote>
<h2 id="sideimpactio-도메인-알아보기">sideimpact.io 도메인 알아보기</h2>
<p>이야기를 시작하기 전에, 서비스를 간단히 소개해야 할 것 같습니다. <a href="http://sideimpact.io">sideimpact.io</a>는 <strong>커뮤니티 기반의 공모 사업 플랫폼</strong>입니다. </p>
<p>전체적인 흐름은 다음과 같습니다.</p>
<ol>
<li><strong>지원(Apply)</strong>: 지원가 자신의 아이디어를 프로젝트로 만들어 특정 라운드에 지원합니다.</li>
<li><strong>발표(Announce)</strong>: 지원 자격을 갖춘 프로젝트들이 커뮤니티에 공개됩니다.</li>
<li><strong>리뷰(Review)</strong>: 커뮤니티 멤버들이 발표된 프로젝트에 대한 리뷰를 작성합니다.</li>
<li><strong>투표(Vote)</strong>: 리뷰 기간이 끝나면, 멤버들은 마음에 드는 프로젝트에 투표합니다.</li>
<li><strong>완료(Complete)</strong>: 최종 결과가 발표되고 라운드가 종료됩니다.</li>
</ol>
<p>단순해 보이는 흐름이지만, 실제로는 사용자(User), 라운드(Round), 리뷰(Review), 투표(Vote) 등 다양한 도메인이 Stage별로 복잡하게 얽혀 있습니다. 프로젝트 하나가 완료되기까지, 이 모든 도메인이 서로를 참조하며 동작해야 합니다.</p>
<h2 id="간결한-구조와-간결하지-않은-router">간결한 구조와 간결하지 않은 Router</h2>
<p>그렇다면 이 흐름을 기존 코드는 어떻게 표현하고 있었을까요? 기존 코드는 각 도메인을 <strong>Model, Service, Repository가 1:1:1로 대응</strong>하는, 간결한 구조를 채택했습니다.</p>
<ul>
<li>Project 모델 → ProjectService → ProjectRepository</li>
<li>Review 모델 → ReviewService → ReviewRepository</li>
<li>Vote 모델 → VoteService → VoteRepository</li>
</ul>
<p>하지만 이 구조는 지저분한 코드를 양산 했습니다.
예를 들어, <strong>&#39;사용자가 프로젝트에 리뷰를 남기는&#39;</strong> 간단해 보이는 기능 하나를 처리하려면, 코드는 수많은 질문에 답해야 합니다.</p>
<ul>
<li>&quot;지금은 리뷰 기간이 맞나?&quot; (RoundService에 물어봐야 함)</li>
<li>&quot;이 사용자는 커뮤니티 멤버인가?&quot; (UserService에 물어봐야 함)</li>
<li>&quot;혹시 자기 자신의 프로젝트에 리뷰를 남기려는 건 아닌가?&quot; (ProjectService에 물어봐야 함) </li>
</ul>
<p>1:1:1 구조에서는 각 Service가 자신의 도메인에 대한 정보밖에 모릅니다. 결국 이 모든 질문에 답하고, 흐름을 지휘하고, 최종적으로 리뷰를 저장하는 책임은 전부 <strong>Router 계층에 떠넘겨졌습니다</strong>.</p>
<p>그 결과, &#39;리뷰 작성 API&#39;의 Router와 &#39;투표 진행 API&#39;의 Router는 서로 다른 기능임에도 불구하고, &#39;지금 라운드 단계가 맞는지?&#39;, &#39;사용자가 커뮤니티 멤버인지?&#39; 같은 <strong>비슷한 로직을 중복</strong>해서 가질 수밖에 없었습니다. 알림을 보내는 로직 또한 여러 Router에 흩어져 있었습니다.</p>
<h2 id="facade-service-비즈니스-흐름을-지휘하는-새로운-계층">Facade Service: 비즈니스 흐름을 지휘하는 새로운 계층</h2>
<p>물론 다시 처음부터 구조를 그릴 수도 있었겠지만, 몇 가지 현실적인 이유를 고려해야 했습니다. 
우선, 기존 코드가 이미 안정적으로 동작하고 있었기에 핵심 로직은 최대한 유지하며 변화를 최소화하고 싶었습니다. 또한, 모든 코드를 완벽히 이해하고 갈아엎기엔 시간과 리소스가 한정적이었습니다. 기존 구조의 핵심은 유지하되, 문제점만 개선할 방법을 찾고 싶어서 <a href="https://ko.wikipedia.org/wiki/%ED%8D%BC%EC%82%AC%EB%93%9C_%ED%8C%A8%ED%84%B4">Facade pattern</a> 도입을 하였습니다.</p>
<p>이를 적용해 <strong>Facade Service</strong>라는 새로운 계층을 Router와 기존 Domain Service 사이에 도입했습니다.</p>
<p>새로운 구조에서 각 계층의 책임은 재정의되었습니다.</p>
<ul>
<li><strong>Router</strong>: 오직 HTTP 요청과 응답이라는 창구 역할만 수행</li>
<li><strong>Facade Service</strong>: 여러 Domain Service를 지휘하여 실제 비즈니스 로직을 처리</li>
<li><strong>Domain Service</strong>: 1:1 원칙을 지키며 단일 도메인의 데이터 처리만 담당</li>
</ul>
<p>이 변화로, 여러 Router에 중복되어 있던 권한 확인, 상태 체크, 알림 발송 로직은 각각의 Facade Service라는 <strong>단 한 곳으로 응집</strong>되었습니다.</p>
<h2 id="테스트-가능한-구조로의-전환">테스트 가능한 구조로의 전환</h2>
<p>이 구조가 가져다준 하나의 큰 선물은 바로 <strong>&#39;테스트 용이성&#39;</strong>의 향상이었습니다.</p>
<p>이전 구조를 다시 떠올려 볼까요? 모든 비즈니스 로직의 조합이 Router에 있었습니다. 즉, &#39;리뷰 작성&#39;이라는 하나의 비즈니스 흐름을 검증하려면 반드시 API 요청부터 시작하는 통합 테스트를 수행해야만 했습니다.</p>
<p>하지만 Facade Service의 도입으로 아래와 같은 테스트 코드를 작성할 수 있게 되었습니다.</p>
<ul>
<li><p><strong>Domain Service</strong> : 여전히 자신의 도메인에만 집중하므로, 간단한 단위 테스트로 안정성을 확보할 수 있습니다.</p>
</li>
<li><p><strong>Facade Service</strong>: Router 계층에 흩어져 있던 비즈니스 로직을 품게 되면서, 테스트의 범위를 넓혔습니다. &quot;리뷰 기간이 아닐 때 리뷰를 작성하려 하면 막아내는가?&quot;, &quot;자신의 프로젝트에 리뷰/투표가 불가능한가?&quot; 와 같은 복잡한 시나리오를 API 호출 없이도 명확하고 빠르게 검증할 수 있게 되었고, 덕분에 서비스 레벨 테스트 커버리지를 대략 <strong>10%p</strong> 향상시켰습니다.</p>
</li>
</ul>
<h2 id="느낌점">느낌점</h2>
<p>이번 작업을 통해 도메인 이해도를 높일 수 있었고, 코드도 미약하나 개선할 수 있었습니다. 또한 다른 사람이 작성한 코드를 오랜 시간 들여다보며 수정한 경험은 처음이었는데, 코드에서 작성자의 성격과 고민을 느낄 수 있었습니다. 코드를 통해 이와 같은 것을 알 수 있다는 것이 참 재미있었습니다. </p>
<p>물론 제가 도입한 구조도 완벽하지는 않습니다. Facade Service가 너무 비대해질 위험도 있고, 계층이 하나 더 늘어난 만큼 복잡도가 증가한 측면도 있습니다. 앞으로 서비스를 운영하며 또 다른 개선점을 찾아 나아가야 할 것입니다.</p>
<h2 id="작지만-의미-있는-변화들">작지만 의미 있는 변화들</h2>
<p>1편과 2편에서 다루지 않은 소소한 변화도 있습니다. Docker 이미지 크기를 60% 이상 줄였고, Poetry에서 UV로 패키지 매니저를 전환했으며, 알림 발송 추적 시스템도 도입했습니다. 이런 이야기들도 기회가 되면 하나씩 공유해보겠습니다.</p>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SideImpact 개선기 1편: 번거로운 수동 배포 자동화]]></title>
            <link>https://velog.io/@l_cloud/SideImpact-%EA%B0%9C%EC%84%A0%EA%B8%B0-1%ED%8E%B8-%EB%B2%88%EA%B1%B0%EB%A1%9C%EC%9A%B4-%EC%88%98%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@l_cloud/SideImpact-%EA%B0%9C%EC%84%A0%EA%B8%B0-1%ED%8E%B8-%EB%B2%88%EA%B1%B0%EB%A1%9C%EC%9A%B4-%EC%88%98%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Tue, 16 Sep 2025 12:10:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 개발 환경의 배포 자동화에 대한 가벼운 경험을 공유하는 글입니다. 복잡한 롤백 전략이나 Blue/Green, 카나리 배포 등을 다루고 있지 않습니다.</p>
</blockquote>
<h3 id="새로운-역할">새로운 역할</h3>
<p>2025년 2분기부터 <a href="http://sideimpact.io">sideimpact.io</a>의 백엔드 개발과 운영을 맡게 되었습니다. </p>
<p>기존에 잘 동작하던 서비스이지만, 서비스 기획과 비즈니스 요구 사항 <strong>변화</strong>에 더욱 잘 대응 하기 위해 내부적으로 많은 개선 작업을 시도하고 있습니다. 앞으로 몇 개의 글에 걸쳐 이 개선기들을 하나씩 공유해보고자 합니다.</p>
<p>그 첫 번째 이야기는 바로 <strong>개발(Dev) 환경의 CI/CD 개선기</strong>입니다.</p>
<p>개발 환경은 AWS EC2와 ALB를 사용하는, 비교적 간단한 구조입니다. 물론 운영(Production) 환경은 ECS, CodeBuild, CodeDeploy를 사용해 배포 중단이나 롤백이 가능한, 비교적 안정적인 CI/CD 파이프라인이 구축되어 있습니다.</p>
<p>그런데 개발 환경은 상황이 좀 달랐습니다.</p>
<h3 id="기존의-수동-배포-방식"><strong>기존의 수동 배포 방식</strong></h3>
<p>새로운 코드를 개발 환경에 배포하려면 아래와 같은 과정을 거쳐야 했습니다.</p>
<ol>
<li>내 로컬 컴퓨터에서 개발 서버 EC2로 <strong>SSH 접속</strong></li>
<li>서버에서 최신 코드를 받기 위해 <strong><code>git pull</code></strong></li>
<li><strong><code>tmux</code></strong> 세션에 들어가 기존에 돌고 있던 도커 컨테이너를 내리고, 다시 <code>docker build</code>와 <code>up</code>으로 실행</li>
</ol>
<p>어떠신가요? 과정이 복잡하진 않지만, 매우 <strong>번거롭습니다.</strong>
사소한 수정 사항을 반영할 때마다 매번 이 과정을 반복해야 했죠. 심지어 보안을 위해 SSH 접속 IP를 제한해 두었기 때문에, 외부에서 작업할 땐 IP를 등록해야 하는 관리 포인트까지 존재했습니다.</p>
<h2 id="개선-목표-최소한의-수정과-무비용"><strong>개선 목표: 최소한의 수정과 무비용</strong></h2>
<p>반복적인 수동 작업을 좋아하는 개발자는 없겠죠. 하지만 다른 기능 개발도 쌓여 있는 상황에서, 개발 환경 배포를 자동화하자고 거창한 시스템을 도입하는 건 너무 비효율적이라고 생각했습니다. 이번 작업에서 중요한 것은 <strong>정교함</strong>이 아니라 <strong>실용성</strong>이라고 생각하여 거창한 파이프라인 대신, 몇 가지 단순하고 실용적인 목표를 세웠습니다.</p>
<blockquote>
<ul>
<li><strong>첫째, 기존 <code>docker build up</code> 구조는 그대로 유지한다.</strong> </li>
<li><strong>둘째, 새로운 서비스를 도입해 관리 포인트를 늘리지 않는다.</strong> </li>
<li><strong>셋째, 비용을 쓰지 않는다.</strong></li>
</ul>
</blockquote>
<p>이 조건들을 만족시킬 조합을 고민하다, 두 가지 도구를 떠올렸습니다. 바로 <strong>GitHub Actions</strong>와 <strong>AWS Systems Manager(SSM)</strong>입니다.</p>
<blockquote>
<ul>
<li><strong>GitHub Actions</strong>: GitHub 저장소(repository)를 기반으로 테스트, 빌드, 배포 등 다양한 작업을 자동화하는 워크플로우 도구입니다. <strong>Free 플랜 사용자에게 매달 2,000분의 넉넉한 무료 사용 시간을 제공</strong>합니다.</li>
<li><strong>AWS SSM</strong>: SSH 접속 없이 EC2에 원격 명령을 내리게 해주는 AWS의 무료 기능입니다. 덕분에 보안 포트를 열거나 IP를 관리할 필요가 없어집니다.</li>
</ul>
</blockquote>
<p><code>Dev</code> 브랜치에 코드를 푸시하면, GitHub Actions가 이 SSM <code>Run Command</code>를 호출해서 EC2에 배포 스크립트를 실행시키는 그림이 그려지지 않나요?</p>
<p>하지만 여기서 중요한 질문이 하나 생깁니다. <strong>&quot;GitHub Actions가 어떻게 내 EC2에 명령을 내리도록 허락할 수 있을까?&quot;</strong></p>
<p>아무런 인증/인가 절차가 없다면, 누구나 내 서버에 마음대로 명령을 내릴 수 있겠죠. 가장 단순한 방법은 <code>AWS_ACCESS_KEY_ID</code>와 <code>AWS_SECRET_ACCESS_KEY</code>를 GitHub Actions의 Secret에 저장하는 것이지만 장기 자격 증명을 외부에 저장해야 한다는 점이 내키지 않았습니다.</p>
<h3 id="oidc-open-id-connect">OIDC (Open ID Connect)</h3>
<p>OIDC는 GitHub Actions가 AWS에 직접 키를 저장하지 않고도, 필요한 작업 수행을 위한 <strong>임시 자격 증명</strong>을 안전하게 발급받을 수 있게 해주는 표준 프로토콜입니다.</p>
<p>AWS IAM에서 특정 GitHub 저장소 및 브랜치만 신뢰하도록 역할을 설정하고, GitHub Actions 워크플로우에서는 그 역할을 사용하겠다고 선언만 해주면 됩니다. 덕분에 더 이상 민감한 키를 코드나 설정에 보관할 필요가 없어집니다.</p>
<blockquote>
<p>자세한 설정 방법은 AWS와 GitHub 양쪽에 약간의 설정이 필요한데, <a href="https://blog.outsider.ne.kr/1750">이 글</a>에 정말 잘 설명되어 있습니다.</p>
</blockquote>
<p>저는 아래 처럼 GitHub Actions 마켓플레이스에 있는 다양한 기능들을 자유롭게 조합하여 워크플로우를 구성했습니다.</p>
<pre><code class="language-yaml">
jobs:
  deploy-dev:
    # ... (생략) ...
    permissions: # OIDC 사용을 위해 GitHub에 토큰 발급 권한 부여
      id-token: write
      contents: read

    steps:
      - name: Configure AWS credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam:: ... # AWS OIDC 역할 ARN

      - name: Send Command to EC2
        id: send_command
        run: | # docker build, up 하는 스크립트 실행
          COMMAND_ID=$(aws ssm send-command ...)
          echo &quot;COMMAND_ID=$COMMAND_ID&quot; &gt;&gt; $GITHUB_OUTPUT

      - name: Wait for command to finish
        run: | # 위에서 보낸 명령이 끝날 때까지 대기
          aws ssm wait command-executed \
            --command-id ${{ steps.send_command.outputs.COMMAND_ID }} \
            --instance-id ${{ secrets.EC2_INSTANCE_ID_DEV }}

      # ... (이후 테스트 및 Slack 알림 로직 생략)</code></pre>
<ol>
<li><p>명령 전송: <code>aws ssm send-command</code>를 통해 EC2에 배포 스크립트(docker build, up)를 실행하라는 명령을 보냅니다.</p>
</li>
<li><p>실행 대기 및 테스트: <code>aws ssm send-command</code>가  완전히 끝날 때까지 기다린 후, 서비스가 정말 잘 실행되었는지 curl 같은 명령으로 간단한 <strong>테스트(ping)</strong>를 진행합니다.</p>
</li>
<li><p>결과 알림: 마지막으로, 이 테스트 결과를 바탕으로 배포의 성공 또는 실패 여부를 Slack으로 알림을 보냅니다.</p>
</li>
</ol>
<h3 id="결론">결론</h3>
<p>이 방식이 모든 상황에 맞는 완벽한 해결책은 당연히 아닙니다.</p>
<p>만약 배포에 실패한다면 어떻게 될까요? 네, 결국 <strong>AWS 콘솔에 접속해서 SSM의 명령 기록(Command history)을 보고 로그를 직접 확인</strong>해야 합니다. 상황에 따라서는 다시 EC2에 SSH로 접속해 수동으로 재배포를 해야 할 수도 있죠.</p>
<p>하지만 개발 환경에서는 이 정도의 단점은 충분히 감수할 만했습니다.</p>
<p>무엇보다 확실한 것은, <strong>이전보다 비교할 수 없이 편해졌다</strong>는 사실입니다.</p>
<blockquote>
<p>롤백 전략까지는 필요 없는 개발 환경에서, 비용 없이 간편하게 CI/CD를 구성해보고 싶다면 GitHub Actions와 SSM 조합을 고려해 볼 만합니다.</p>
</blockquote>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM을 추상적으로 이해하고 Bedrock에서 Fine-Tuning 실습하기 (2부)]]></title>
            <link>https://velog.io/@l_cloud/LLM%EC%9D%84-%EC%B6%94%EC%83%81%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-Bedrock%EC%97%90%EC%84%9C-Fine-Tuning-%EC%8B%A4%EC%8A%B5%ED%95%98%EA%B8%B0-2%EB%B6%80</link>
            <guid>https://velog.io/@l_cloud/LLM%EC%9D%84-%EC%B6%94%EC%83%81%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-Bedrock%EC%97%90%EC%84%9C-Fine-Tuning-%EC%8B%A4%EC%8A%B5%ED%95%98%EA%B8%B0-2%EB%B6%80</guid>
            <pubDate>Sun, 11 May 2025 10:17:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 LLM을 추상적으로 이해하고 Bedrock에서 Fine-Tuning 실습하기 (1부)에서 이어지는 글입니다. 실습 내용이 포함되기에 AWS, Python에 대한 기본 지식을 요구합니다.</p>
</blockquote>
<p>우리는 1부에서 자연어가 어떻게 숫자가 되는지, 그 숫자들이 어떻게 계산되고, LLM이 어떤 방식으로 의미를 학습하는지 차근차근 정리해봤습니다.</p>
<p>글 마지막엔 이미 거대한 데이터로 학습된 LLM을 내 목적에 맞게 조금만 바꾸는 방법, 즉 Fine-Tuning이라는 개념에 대해 살짝 언급햇습니다.</p>
<p>아래와 같은 궁금증이 들지 않나요?
<strong>“이걸… 직접 해보려면 뭐부터 알고 준비해야 하지?”</strong></p>
<p>Fine-Tuning 기법도 워낙 많고, 어떤 방식을 선택할지도 고민입니다.
<strong>LoRA?</strong> <strong>Adapter?</strong> <strong>Full Fine-Tuning?</strong>
GPU 같은 자원은 기본이고, 병렬 처리, 모델은 어디서 어떻게 불러와야 하는지 등 알아야 할 것이 정말 많죠.</p>
<p>다행히도 모델을 로드하고, 학습시키고, 튜닝된 모델을 평가까지 할 수 있는 완전한 실습 환경이 클라우드에 준비돼 있습니다.</p>
<p>바로, AWS의 <strong>Bedrock</strong>입니다.</p>
<h2 id="bedrock이란">Bedrock이란?</h2>
<p>Bedrock은 Amazon에서 제공하는 LLM 통합 플랫폼입니다.</p>
<p>여러 회사에서 만든 다양한 LLM(Claude, LLaMA, Titan 모델 등)을 불러올 수 있고,그 위에 내 데이터를 입혀 Fine-Tuning을 하거나, 튜닝된 모델을 평가하고 호출 할 수 있습니다.</p>
<p>또한 VPC 기반으로 네트워크를 격리할 수 있기 때문에,내부망에서만 배포하는 것도 가능합니다.</p>
<p>즉 Bedrock은 <strong>모델 호출 → 학습 → 평가 → 배포</strong>까지 한 플랫폼에서 이어서 해볼 수 있는 구조입니다.</p>
<h3 id="bedrock-요금-구조"><strong>Bedrock 요금 구조</strong></h3>
<p>서비스를 이용하기 전에 알아두어야 할 중요한 점은 <strong>&quot;얼마나 비용이 드는지&quot;</strong>와 <strong>&quot;어떤 부분에서 비용이 발생하는지&quot;</strong>입니다. 그래서 먼저 요금 구조에 대해 살펴보겠습니다.</p>
<p><strong>몇 억~몇 백 억개</strong>의 파라미터를 가진 LLM을 구동하고 연산을 수행하려면 상당한 컴퓨팅 자원이 필요합니다. Bedrock의 요금 체계는 이러한 특성을 반영하여 다음과 같이 구성되어 있습니다.</p>
<h3 id="1-학습-비용">1. 학습 비용</h3>
<p>Fine-Tuning은 1부에서 다룬 <strong>&quot;예측 → 오차(loss) 계산 → 내부 행렬 수정 → 다시 예측&quot;</strong>을 반복하는 과정입니다. <strong>몇 억 ~ 몇 백 억</strong>개의 파라미터에 대한 계산을 하려면 상당한 컴퓨팅 리소스가 필요하겠죠. 또한 학습이 완료된 모델을 어딘가에 저장해야할 필요도 있습니다. 요금 구조도 이에 맞춰져 있습니다.</p>
<ul>
<li><strong>학습(Training) 비용</strong><ul>
<li>학습 데이터의 총 토큰 수 × epoch 수만큼 연산이 발생합니다. 이 기준으로 요금이 부과됩니다.</li>
<li>ex) 10,000 토큰 × 3 epoch → 30,000 토큰 처리 비용</li>
</ul>
</li>
<li><strong>모델 저장 비용</strong><ul>
<li>학습이 끝난 모델은 Bedrock 전용 저장소에 저장됩니다. 이 저장소도 월 단위로 과금됩니다.</li>
<li>모델 별로 가격이 조금씩 다르며, <a href="https://aws.amazon.com/marketplace/pp/prodview-5iaka6xethmle">Haiku 모델</a> 기준으로는 월 $20입니다.</li>
</ul>
</li>
</ul>
<h3 id="2-추론-비용-모델-사용">2. 추론 비용 (모델 사용)</h3>
<p>학습시킨 모델을 실제로 사용할 때도 컴퓨터 리소스가 필요하기 때문에 요금이 발생합니다. Bedrock에서는 Fine-Tuned 모델을 <strong>Provisioned Throughput</strong> 방식으로만 사용할 수 있으며, 처리량과 사용 시간 기준으로 요금이 부과됩니다.</p>
<ul>
<li>처리 단위는 MU(Model Unit) 기준입니다. 분당 처리 가능한 토큰 수로 정해집니다.</li>
<li>약정 없이 시간당 과금되며, 1개월 또는 6개월 약정을 걸면 할인 가능합니다.</li>
</ul>
<h3 id="3-기타-비용">3. 기타 비용</h3>
<p>학습 데이터, 학습 결과, 로그 등은 S3에 저장됩니다. 이 역시 별도 과금이 됩니다.</p>
<ul>
<li>S3 스토리지: 학습에 필요한 모든 파일 저장 용도</li>
</ul>
<p>자세한 가격 정보: <a href="https://aws.amazon.com/ko/bedrock/pricing/">AWS Bedrock 공식 가격 페이지</a></p>
<h3 id="bedrock-fine-tuning-준비-체크리스트"><strong>Bedrock Fine-Tuning 준비 체크리스트</strong></h3>
<p>Fine-Tuning을 하려면 몇 가지 조건을 미리 확인해야 합니다. Bedrock이라는 플랫폼을 사용한다고 해서 모든 것이 가능한 것은 아닙니다.</p>
<h3 id="1-지역-제한">1. 지역 제한</h3>
<p>현재(25년 5월 기준) Fine-Tuning은 다음 두 리전에서만 가능합니다:</p>
<ul>
<li><strong>US East (N. Virginia)</strong></li>
<li><strong>US West (Oregon)</strong></li>
</ul>
<h3 id="2-모델-제한">2. 모델 제한</h3>
<p>우리가 알고 있는 모든 모델을 Fine-Tuning에 사용할 수 있는 것은 아닙니다. Bedrock에서 제공하는 모델 중에서도 Fine-Tuning이 가능한 모델은 제한적이며,  모델이 지원하는 커스터마이징 유형 도 다릅니다. 예를 들어 어떤 모델은 Text-to-Text만 가능하고, 어떤 모델은 Text-to-Image를 지원하는 등 사용 목적에 따라 선택이 필요합니다. 자세한 사항은 아래 공식 문서를 참고 해 주세요.</p>
<p><a href="https://docs.aws.amazon.com/bedrock/latest/userguide/custom-model-supported.html">AWS Bedrock 공식 문서: 지원 모델 목록</a></p>
<p><a href="https://docs.aws.amazon.com/ko_kr/bedrock/latest/userguide/model-customization-prepare.html">AWS Bedrock 공식 문서: 모델과 커스터마이징 유형</a></p>
<h2 id="실습하기">실습하기</h2>
<p>위에서 살펴본 제약 사항들을 염두에 두고, 이제 Bedrock에서 Fine-Tuning을 진행하며 이론적인 개념이 실제로 어떻게 적용되는지 확인해봅시다.</p>
<p>1부에서 설명한 Fine-Tuning의 핵심 과정인 <strong>&quot;예측 → 오차 계산 → 행렬 수정 → 다시 예측&quot;</strong>을 떠올려봅시다. Bedrock을 사용할 때 장점은 오차 계산과 행렬 수정이라는 복잡한 과정을 모두 자동으로 처리해준다는 것입니다. </p>
<p>그럼 우리가 해야할 일은 무엇일까요? 
바로 우리가 원하는 방향성이 담긴 데이터를 준비하는 것입니다. 즉, <strong>입력(Input) + 정답(Output)</strong> 쌍으로 구성된 학습 데이터가 있어야 합니다.</p>
<p>우리의 데이터를 모델에 반복해서 학습시키면, 모델은 &quot;이런 질문엔 이렇게 대답해야 하는구나&quot; 하고 패턴을 인식하게 됩니다. 모델마다 요구하는 데이터 포맷이 조금씩 다르므로 템플릿은 아래 문서를 참고해서 작성하고, S3에 업로드하면 됩니다:
<a href="https://docs.aws.amazon.com/ko_kr/bedrock/latest/userguide/model-customization-prepare.html">AWS Bedrock 모델 커스터마이징 데이터 가이드</a></p>
<p>이번 실습에서는 Haiku 모델을 사용할 예정입니다. Haiku는 <strong>single-turn</strong>(단일 대화) 및 <strong>multi-turn</strong>(다중 대화) 형식을 지원하며, 학습 데이터는 <code>.jsonl</code> 형식으로 저장해야 합니다.</p>
<blockquote>
<p><code>.jsonl</code>은 JSON 객체 하나를 한 줄에 하나씩 적는 방식입니다. 각 줄이 하나의 학습 예제가 되는 구조입니다.</p>
</blockquote>
<h3 id="haiku-데이터-셋-예시">Haiku 데이터 셋 예시</h3>
<pre><code class="language-json">// 단일 대화(single turn) 예시
{
  &quot;system&quot;: &quot;너는 친절한 쇼핑 도우미야.&quot;,
  &quot;messages&quot;: [
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;여름에 입기 좋은 옷은?&quot;},
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;통기성이 좋은 반팔티가 적당합니다.&quot;}
  ]
}

// 다중 대화(multi-turn) 예시
{
  &quot;system&quot;: &quot;너는 고객에게 친절하고 정확하게 제품 정보를 안내하는 쇼핑 도우미야.&quot;,
  &quot;messages&quot;: [
    { &quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;요즘 인기 있는 여름용 바지 추천해줘.&quot; },
    { &quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;네! 통기성이 좋고 얇은 린넨 소재의 와이드 팬츠가 요즘 인기 많아요. 특히 베이지나 카키 컬러가 많이 팔리고 있어요.&quot; },
    { &quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;가격대는 어느 정도야?&quot; },
    { &quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;브랜드에 따라 다르지만, 보통 3만 원대에서 7만 원대 사이 제품들이 많이 판매되고 있어요.&quot; }
  ]
}</code></pre>
<h3 id="실습-절차-정리">실습 절차 정리</h3>
<p>데이터 준비가 끝났으면 이제 콘솔에서 학습 Job을 만드는 일만 남았습니다.</p>
<ol>
<li>Bedrock 콘솔 접속 → 리전을 Virginia 또는 Oregon으로 설정</li>
<li>Custom Models → Create fine-tuning job 클릭
<img src="https://velog.velcdn.com/images/l_cloud/post/46f47607-1869-41c0-87ab-91e32a276fa6/image.jpg" alt=""></li>
</ol>
<ol start="3">
<li>모델 선택: Haiku</li>
</ol>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/d3317b25-3cc3-4af8-860d-a2abd43b2027/image.jpg" alt=""></p>
<ol start="4">
<li>S3 데이터 경로 입력 (.jsonl)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/5e493cff-17d4-4082-a5f0-217658462ecf/image.jpg" alt=""></p>
<ol start="5">
<li>Validation dataset (선택 사항으로 학습에 사용되지 않은 별도의 데이터)</li>
<li>Hyperparameters 설정:<ul>
<li>Epoch: 전체 데이터를 몇 번 반복 학습할지</li>
<li>Batch size: 한 번에 몇 개 예제를 학습할지</li>
<li>Learning rate: 가중치를 얼마나 빠르게 조정할지</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/a5b3b07c-d070-4cd4-b2a2-56ef4a582ee3/image.jpg" alt=""></p>
<p>Hyperparameters와 Validation dataset에 대해 잠깐 설명하자면…
1부에서는 따로 설명하지 않았지만, 실제 Fine-Tuning 단계에서는 이 값들이 모델의 학습 결과에 큰 영향을 미칩니다.</p>
<ul>
<li><strong>Epoch</strong>: 전체 데이터셋을 몇 번 반복해서 학습할지 결정합니다. 너무 많으면 과적합(overfitting)이 발생하고, 너무 적으면 학습이 부족할 수 있습니다.</li>
<li><strong>Batch size</strong>: 한 번에 처리할 데이터 샘플 수입니다. 너무 크면 메모리 사용량이 급증하고, 너무 작으면 학습 속도가 느려집니다.</li>
<li><strong>Learning rate</strong>: 모델이 각 학습 단계에서 가중치를 얼마나 크게 조정할지 결정합니다. 너무 크면 학습이 불안정해지고, 너무 작으면 학습이 제대로 진행되지 않을 수 있습니다.</li>
<li><strong>Validation dataset</strong></li>
</ul>
<blockquote>
<p>더 깊이 이해하고 싶다면 아래 키워드를 추가로 공부해보는 것을 추천드립니다.
<strong>Adam, SGD, Learning rate warmup, decay, Curriculum Learning, ...</strong></p>
</blockquote>
<p>지금 단계에서는 &quot;모델이 데이터의 패턴을 효과적으로 학습하며, 과도하게 외우지 않도록 균형을 맞추는 과정&quot;이라고 이해하면 충분합니다.</p>
<p>Hakiku model의 경우 <a href="https://aws.amazon.com/ko/blogs/machine-learning/best-practices-and-lessons-for-fine-tuning-anthropics-claude-3-haiku-on-amazon-bedrock/">Claude 3 Haiku 파인튜닝 모범 사례 및 교훈</a>이 있으니 이를 참고하여 자신의 데이터에 맞게 설정해주시면 됩니다.</p>
<ol start="7">
<li>IAM Role 설정<ul>
<li>S3에 접근하고 결과를 쓸 수 있는 권한을 가진 서비스 역할이 필요합니다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/b9ccd3ad-3965-4f97-8d5c-9049672c343f/image.jpg" alt=""></p>
<h2 id="모델-성능-평가하기">모델 성능 평가하기</h2>
<p>Fine-Tuning이 끝났습니다 🥳🥳
모델이 잘 작동하면 좋겠지만… 사실 막상 결과를 보면, 뭔가 조금씩 어긋날 때가 많습니다. 제대로 배운 줄 알았는데, 기대한 대답은 잘 안 나오는 느낌이죠.</p>
<p>그래서 여기서 중요한 게 하나 더 필요합니다. 바로, 평가입니다. 그럼 아래와 같은 의문이 듭니다.</p>
<p><strong>“무엇을 평가할까?”</strong>
<strong>“그리고 그걸 어떻게 평가하지?”</strong></p>
<p>다행히 유명한 벤치마크들이 있고, 손쉽게 자동화해주는 도구들이 존재합니다. Bedrock에서도 평가 기능을 제공하지만, 저의 경우 모델을 학습시길 때 Bedrock도 사용하고, Hugging Face로 Gemma도 직접 Fine-Tuning 했습니다. 이 두 모델에 대한 일관된 평가를 위해 자동화 도구인 lm-evaluation-harness를 사용했습니다.</p>
<h3 id="lm-evaluation-harness-이란"><a href="https://github.com/EleutherAI/lm-evaluation-harness">lm-evaluation-harness</a> 이란?</h3>
<p>lm-evaluation-harness는 다양한 언어 모델을 통일된 프레임워크에서 평가할 수 있게 해주는 도구입니다. 주요 특징은 아래와 같습니다.</p>
<ul>
<li>60개 이상의 학술 벤치마크를 지원하며, 수백 개의 세부 태스크와 변형이 구현되어 있음</li>
<li>Hugging Face, vLLM 등 다양한 모델 로딩 방식 지원</li>
<li>OpenAI API 등 상용 API 지원</li>
<li>커스텀 벤치마크 생성 가능: 템플릿만 맞추면 나만의 벤치마크도 쉽게 등록하고 코드로 테스트할 수 있음</li>
</ul>
<blockquote>
<p><a href="https://techblog.lycorp.co.jp/ko/automating-llm-application-evaluation-with-harness">LLM 평가와 lm-evaluation-harness 관련 추천 글1</a>
<a href="https://devocean.sk.com/blog/techBoardDetail.do?ID=166716&amp;boardType=techBlog&amp;searchData=&amp;searchText=&amp;id=&amp;techType=&amp;searchDataSub=&amp;searchDataMain=">추천 글 2</a></p>
</blockquote>
<h3 id="bedrock-haiku-평가-시-주의사항">Bedrock Haiku 평가 시 주의사항</h3>
<p>Bedrock의 Haiku 모델을 평가할 때는 몇 가지 주의사항이 있습니다.</p>
<ol>
<li><strong>loglikelihood 기반 평가는 지원하지 않습니다.</strong>
loglikelihood는 정답 토큰이 나올 확률을 계산해 점수화하는 방식인데, Haiku API에서는 이 기능을 제공하지 않습니다.</li>
<li><strong>generate_until 기반 평가만 가능합니다.</strong>
주어진 문맥에서 모델이 우리가 원하는 정답 문자열을 정확히 생성했는지를 기준으로 평가하는 방식입니다.</li>
<li><strong>lm-evaluation-harness에서 Bedrock API를 바로 호출할 수 있는 구현체는 기본으로 제공되지 않습니다.</strong>
직접 bedrock.py 모듈을 만들어 붙여야 합니다. 
참고로, 저는<a href="https://github.com/EleutherAI/lm-evaluation-harness/pull/1708">PR</a>과 <a href="https://github.com/EleutherAI/lm-evaluation-harness/blob/main/lm_eval/models/api_models.py">API Model</a>을 참고해서 bedrock.py 모듈을 직접 작성했습니다. 그리고 모델 평가를 위해 커스텀 벤치마크도 만들어 사용했습니다.</li>
</ol>
<p>그럼 이제 아래와 같은 Python 코드로 Bedrock 모델을 바로 평가해볼 수 있습니다. (물론 CLI 환경에서 Bash 명령어로도 실행할 수 있습니다)</p>
<pre><code class="language-python">
import lm_eval
from lm_eval.models.bedrock import BedrockChatLM

# 사용할 Bedrock 모델 ID 입력 (예: &#39;anthropic.claude-3-haiku-20240307-v1:0&#39;)
model_id = &quot;&quot;

# 평가할 태스크 리스트 지정 (예: [&quot;kmmlu&quot;, &quot;kobest&quot;] 등)
tasks = []

# Bedrock 모델 래퍼 객체 생성
lm_obj = BedrockChatLM(
    model=model_id,
    base_model=&quot;haiku&quot;,  
    max_gen_toks=2048,
    temperature=0,
)

# 평가 실행
results = lm_eval.simple_evaluate(
    model=lm_obj,
    tasks=tasks,
    num_fewshot=0,
)
...</code></pre>
<p>해당 평가를 실행 하하고 결과를 저장하면 아래와 같은 파일을 받을 수 있습니다!</p>
<pre><code class="language-json">
{
  &quot;results&quot;: {
    &quot;kmmlu_direct_criminal_law&quot;: {
      &quot;alias&quot;: &quot;kmmlu_direct_criminal_law&quot;,
      &quot;exact_match,none&quot;: 0.355,
      &quot;exact_match_stderr,none&quot;: 0.03392091008070854
    },
    &quot;kmmlu_direct_education&quot;: {
      &quot;alias&quot;: &quot;kmmlu_direct_education&quot;,
      &quot;exact_match,none&quot;: 0.57,
      &quot;exact_match_stderr,none&quot;: 0.04975698519562426
    },
  ....</code></pre>
<h2 id="결론">결론</h2>
<p>지금까지 LLM의 기본 원리부터 Bedrock을 활용한 Fine-Tuning, 그리고 실제 평가까지 차근차근 함께 살펴봤습니다.
물론 여기서 다룬 내용은 전체 그림을 이해하기 위한 추상적인 수준이라, 실제 LLM을 깊이 이해하려면 공부할 게 정말 많습니다.
하지만 큰 틀을 이해한다면, 앞으로 세부적인 기술을 배울 때 <strong>이게 왜 필요한지</strong>, <strong>어디에 쓰이는지</strong>를 파악하는 데 도움이 될 것이라 생각합니다.</p>
<p>LLM과 Fine-Tuning의 세계는 생각보다 훨씬 넓고, 깊습니다.
이 글이 그 첫발을 내딛는 데 작은 디딤돌이 되었으면 좋겠습니다.</p>
<p>긴 글 읽어주셔서 정말 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM을 추상적으로 이해하고 Bedrock에서 Fine-Tuning 실습하기 (1부)]]></title>
            <link>https://velog.io/@l_cloud/Temp-Title-psv2bdwh</link>
            <guid>https://velog.io/@l_cloud/Temp-Title-psv2bdwh</guid>
            <pubDate>Mon, 05 May 2025 14:55:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 행렬과 벡터의 기본 개념(벡터의 의미, 행렬 곱셈 등)이 있는 독자를 대상으로 합니다. 깊은 지식은 다루지 않으며 LLM에 대한 내용을 추상적으로 살펴봅니다. 총 2부로 AWS Bedrock 사용 경험을 공유하고자 합니다.</p>
</blockquote>
<h2 id="llm이란"><strong>LLM이란?</strong></h2>
<p>LLM은 Large Language Model, 말 그대로 <strong>거대한 언어 모델</strong>입니다.</p>
<p>여러 설명이 있지만, 수식도 구조도 다 빼고 완전히 추상화하면 <strong>행렬 계산기</strong>라고 할 수 있습니다.</p>
<p>요즘은 여러 기능을 제공하지만 아주 단순히 텍스트 입력을 받아 텍스트 출력을 생성하는 LLM이 동작하는 과정을 요약하면 
텍스트(우리의 input)가 벡터(혹은 행렬)로 바뀌고,
그걸 내부의 거대한 행렬과 곱해서 또 다른 벡터를 만들어냅니다.
그 결과는 다시 텍스트로 바뀌죠.</p>
<p>즉, LLM을 사용하는 과정을 이렇게 정리할 수 있습니다:</p>
<blockquote>
<p>텍스트 → 행렬 → 계산(내부 거대 행렬) → 행렬 → 텍스트</p>
</blockquote>
<p>그런데 이런 질문이 떠오르지 않나요? <strong>&quot;자연어가 어떻게 숫자가 되는거지..?&quot;</strong>
LLM은 입력도 숫자, 출력도 숫자, 내부도 전부 숫자 계산입니다.
그럼 도대체 텍스트는 <strong>어떻게 숫자로 바뀌는 걸까요?</strong></p>
<h3 id="rgb도-숫자입니다-하지만-색을-담고-있죠"><strong>RGB도 숫자입니다. 하지만 색을 담고 있죠.</strong></h3>
<p>RGB 색상 체계를 예로 들어보겠습니다. 빨강(R), 초록(G), 파랑(B) 값의 조합으로 색상을 표현하는 방식이죠.
(170, 33, 22)
이 숫자만 보면 우리는 &quot;아, 빨간색 계열이구나&quot;라고 떠올릴 수 있죠.
RGB처럼 <strong>숫자에도 정보가 담길 수 있다</strong>는 뜻입니다.</p>
<p>단어도 마찬가지입니다.
&#39;강아지&#39;와 &#39;고양이&#39;는 비슷한 맥락에서 등장하니, 숫자로 표현했을 때도 가까워야 자연스럽습니다.</p>
<p>이런 벡터를 만드는 방법에는 여러 가지가 있습니다.</p>
<h2 id="단어의-최소-단위는---토크나이저"><strong>단어의 최소 단위는? - 토크나이저</strong></h2>
<p>RGB에서 색을 RED, GREEN, BLUE라는 단위로 쪼갠 것처럼,<strong>텍스트도 적절한 단위로 나눠야 합니다.</strong></p>
<p>이 작업을 해주는 것이 바로 <strong>토크나이저(tokenizer)</strong>입니다.</p>
<p>하지만 텍스트는 색보다 훨씬 복잡하죠.</p>
<p>“어디서 끊을 것인가?”, “무엇을 의미 단위로 볼 것인가?”에는 여러 해석이 존재합니다.</p>
<p>예를 들어, 아래 문장을 보겠습니다:</p>
<blockquote>
<p>고양이는 귀엽다</p>
</blockquote>
<p>이 것을</p>
<ul>
<li>&quot;고양이&quot;, &quot;는&quot;, &quot;귀엽&quot;, &quot;다&quot;로 나눌 수도 있고</li>
<li>그냥 &quot;고양이는&quot;, &quot;귀엽다&quot;로 나눌 수도 있습니다.</li>
</ul>
<p>이처럼 <strong>단어를 나누는 방식</strong>은 언어마다, 목적마다 달라질 수 있습니다.</p>
<p>그래서 등판하는 것이 바로 <strong>형태소 분석기</strong>입니다.</p>
<h3 id="형태소란"><strong>형태소란?</strong></h3>
<p>형태소는 <strong>의미를 가지는 최소 단위</strong>입니다.</p>
<p>예시) “고양이는” → &quot;고양이&quot;(명사) + &quot;는&quot;(조사)</p>
<p>형태소 분석기는 텍스트를 이 단위로 쪼개주는 역할을 합니다. 한국어는 특히 조사, 어미 등이 단어에 붙기 때문에 이 작업이 중요합니다. (예: “보다”, “보았다”, “보이기도 했다” → 각각 다른 의미지만, 모두 ‘보’라는 공통 뿌리를 가짐)</p>
<p><strong>형태소 분석기 예시:</strong> <a href="https://github.com/kakao/khaiii">Khaiii github</a>, <a href="https://tech.kakao.com/posts/358">Khaiii 소개 글</a></p>
<h3 id="토크나이저tokenizer"><strong>토크나이저(tokenizer)</strong></h3>
<p>형태소 분석이라는 방법은 특히 한국어처럼 단어가 여러 조각으로 구성된 언어에서 유용하게 쓰일 수 있습니다. 하지만 LLM에서 사용하는 토크나이저(tokenizer) 는 꼭 형태소 분석을 거치지 않아도 됩니다.</p>
<p>실제로 많은 LLM은, 텍스트를 일정한 규칙에 따라 작은 단위로 나누는 방식을 사용합니다.
이렇게 텍스트를 계산 가능한 단위로 쪼개는 과정을 토크나이징(tokenizing) 이라고 하고, 이 일을 해주는 도구가 토크나이저(tokenizer) 입니다.</p>
<blockquote>
<p>토크나이징 방법에는 다양한 종류가 있습니다만...
지금은 “텍스트를 숫자로 바꾸기 전에, 적절한 단위로 잘 쪼갠다” 정도로만 이해해도 충분합니다.
다양한 방식이 궁금하다면 다음 키워드로 공부해보는 것을 추천합니다: Subword, BPE, SentencePiece, WordPiece, ... 
<a href="https://techblog.yogiyo.co.kr/%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84%EC%9D%98-analyzer-%ED%98%95%ED%83%9C%EC%86%8C%EB%B6%84%EC%84%9D%EA%B8%B0-%ED%86%A0%ED%81%AC%EB%82%98%EC%9D%B4%EC%A0%80-5878af195d14">추천 글</a> </p>
</blockquote>
<p>이제 텍스트를 어떤 단위로 나눌지까지는 정해졌습니다.
그렇다면 다음 질문은 이것입니다:</p>
<p>“그 조각에 의미를 어떻게 담을 것인가?”</p>
<p>그 의미를 벡터로 바꾸는 대표적인 방법부터 하나씩 살펴보겠습니다.</p>
<h2 id="시소러스-사람이-손으로-만든-의미-관계"><strong>시소러스: 사람이 손으로 만든 의미 관계</strong></h2>
<p>예전엔 사람이 단어 간의 의미를 직접 정의했습니다.
대표적으로 <a href="https://wordnet.princeton.edu/">WordNet</a> 같은 시소러스가 있죠.</p>
<p>예시)</p>
<ul>
<li>&#39;고양이&#39;는 &#39;동물&#39;이다</li>
<li>&#39;고양이&#39;는 &#39;냥이&#39;의 유의어다</li>
<li>&#39;탈 것&#39;은 &#39;자전거&#39;, &#39;기차&#39;의 상위어다</li>
</ul>
<p>이런 관계를 그래프로 표현하고, 0과 1로 연결 여부를 나타내는 <strong>인접 행렬</strong>도 만들 수 있습니다.
하지만 너무 손이 많이 들고, 새로운 단어나 미묘한 뉘앙스는 담기 어렵습니다.
그래서 지금은 대부분 <strong>통계 기반</strong>이나 <strong>추론 기반</strong> 기법을 사용합니다.</p>
<h2 id="통계-기반-방식-말뭉치에서-의미를-자동으로-뽑기"><strong>통계 기반 방식: 말뭉치에서 의미를 자동으로 뽑기</strong></h2>
<p><strong>말뭉치(corpus)</strong>란 사람이 쓴 텍스트 모음입니다.
뉴스, 블로그, 커뮤니티 글, 댓글 등 전부 말뭉치가 될 수 있죠.</p>
<p>말뭉치는 그냥 데이터 같지만, 그 안에는 자연어 사용에 대한 단어 선택, 문법, 뉘앙스 등과 같은 <strong>사람의 지식</strong>이 담겨 있습니다. 
이 말뭉치를 잘 관찰하면, 단어들이 어떤 맥락에서 자주 등장하는지를 파악할 수 있습니다.
예를 들어, ‘고양이’라는 단어가 자주 함께 나오는 단어들을 보면 그 단어가 어떤 의미를 가지는지 어렴풋이 짐작할 수 있겠죠.</p>
<blockquote>
<p>즉, 단어는 주변 단어를 보면 의미를 알 수 있다는 가설을 세울 수 있고
→ 이것을 <strong>분포 가설(distributional hypothesis)</strong>이라고 합니다</p>
</blockquote>
<p>이 가설을 바탕으로, 단어들이 어떤 단어들과 자주 함께 등장하는지를 숫자로 정리한 것이 바로 <a href="https://resultofeffort.tistory.com/122">동시발생 행렬</a>입니다.</p>
<h3 id="동시발생-행렬-맥락을-숫자로-바꾸기">동시발생 행렬: 맥락을 숫자로 바꾸기</h3>
<ul>
<li>중심 단어를 고르고</li>
<li>주변 단어들을 몇 칸(윈도우 크기) 기준으로 셉니다</li>
<li>그걸 반복하면 단어 간 빈도를 정리한 표가 나옵니다</li>
</ul>
<p>예시)</p>
<p>&quot;고양이는 귀엽다&quot;
&quot;고양이는 털이 많다&quot;
&quot;고양이와 강아지는 다르다&quot;</p>
<p>→ &#39;고양이&#39; 기준 동시발생 행렬:</p>
<pre><code>         고양이  귀엽다  털   많다  강아지  는   와   다르다
고양이     0     1     1     1     1     3    1     1
귀엽다     1     0     0     0     0     1    0     0
강아지     1     0     0     0     0     1    1     1
</code></pre><p>이런 식으로 단어를 <strong>벡터</strong>로 바꿔 표현할 수 있습니다.
벡터 간 유사도는 코사인 유사도 같은 걸로 계산합니다.</p>
<h3 id="그런데-말뭉치는-너무-크다"><strong>그런데 말뭉치는 너무 크다</strong></h3>
<p>단어 수가 많아질수록, 동시발생 행렬은 거대한 <strong><a href="https://ko.wikipedia.org/wiki/%EC%84%B1%EA%B8%B4_%ED%96%89%EB%A0%AC">희소 행렬(sparse matrix)</a></strong>이 됩니다.
즉, 대부분이 0이고 쓸모 있는 정보는 적죠.</p>
<p>지금 이 글에서만 뽑아도 &#39;이&#39;, &#39;은&#39;, &#39;는&#39; 같은 자주 등장하는 단어들이 행렬 전체를 덮어버릴 수 있습니다.
그래서 필요한 정보만 남기고 나머지는 줄입니다.</p>
<blockquote>
<p>여기서 PPMI, SVD 같은 기법들이 등장하지만...
지금은 그냥 &quot;쓸데없는 건 줄이고, 중요한 정보만 남긴다&quot; 정도로 기억해두면 충분합니다.
더 알고 싶다면 관련 키워드를 따로 공부해보는 것을 추천합니다 :)</p>
</blockquote>
<h2 id="추론-기반-방식-계산을-반복하며-의미-학습"><strong>추론 기반 방식: 계산을 반복하며 의미 학습</strong></h2>
<p>통계 기반은 한 번 계산해서 끝나는 방식이었습니다. 하지만 새로운 단어나 복잡한 문맥을 다루긴 어렵죠.
새로운 단어가 추가되면, 전체 데이터를 다시 계산해야 하기 때문에 유연하게 확장하기도 어렵습니다.</p>
<p>그래서 등장한 것이 <strong>추론 기반 방식</strong>입니다.</p>
<p>추론 기반 방식은 문장 속에 빈칸이 있을 때 어떤 단어가 들어갈지 <strong>예측하는 문제를 반복</strong>하면서 벡터를 조정합니다.</p>
<p>예시)
 &quot;고양이 __ 귀엽다&quot;
&quot;__와 강아지는 다르다&quot;</p>
<p>빈칸에 어떤 단어가 가장 잘 어울리는지를 계속 맞춰보는 과정을 모델이 스스로 수행하는 겁니다.</p>
<blockquote>
<p>여기서 말하는 ‘모델’은 수 많은 행렬 계산이 연결된 구조입니다.
즉, 예측도 행렬 계산으로 이루어지고, 수정도 행렬 값을 바꾸는 식으로 이루어집니다.</p>
</blockquote>
<h3 id="어떻게-계산을-할-수-있을까요"><strong>어떻게 계산을 할 수 있을까요?</strong></h3>
<p>모델이 학습한다는 건 결국 <strong>&quot;예측이 틀렸을 때, 내부 행렬을 어떻게 바꿀까?&quot;</strong>라는 문제를 푸는 겁니다.</p>
<p>즉, <strong>예측 → 오차(loss) 계산 → 내부 행렬 수정 → 다시 예측</strong>의 과정입니다.</p>
<p>처음에는 모델의 가중치가 임의의 초기값으로 설정되어 있어 정확한 예측이 어렵습니다.
하지만 정답과 다르면, <strong>얼마나 차이가 나는지</strong>를 계산합니다. 이걸 <strong>오차(loss)</strong>라고 합니다.</p>
<p>오차가 생기면, 모델은 그걸 줄이기 위해 내부의 <strong>행렬(=가중치)</strong> 값을 조금씩 조정합니다.</p>
<p>가중치를 바꾸는 방식은 다양하지만, 대표적으로 <strong>경사하강법(Gradient Descent)</strong> 계열의 최적화 알고리즘을 많이 사용합니다.</p>
<blockquote>
<p><a href="https://youtu.be/sDv4f4s2SB8?feature=shared">추천 영상</a>
경사하강법을 간결히 설명하자면 오차가 줄어드는 방향을 계산해서, 그쪽으로 조금 이동시키는 방식입니다.</p>
</blockquote>
<p>하지만 내부 계산은 여러 층으로 연결돼 있어서,&quot;어디서 문제가 발생했는지&quot; 추적해야 합니다.
그걸 돕는 방법이 <strong>역전파(backpropagation)</strong>입니다.</p>
<blockquote>
<p><a href="https://youtu.be/DMCJ_GjBXwc?feature=shared">추천 영상</a> 
역전파를 간결히 설명하자면 오차를 출력에서부터 거꾸로 따라가며, 각 가중치가 얼마나 영향을 줬는지 계산하는 기법입니다. </p>
</blockquote>
<p>간단히 정리하면 모델 학습은 아래 사이클을 수백만 번 반복합니다.</p>
<blockquote>
<p>예측 → 오차(loss) 계산 → 영향 추적(역전파) → 가중치 조정</p>
</blockquote>
<p>그 결과로, 학습이 잘 된다면 &#39;고양이&#39;는 &#39;강아지&#39;와 가까운 벡터가 되고,&#39;자동차&#39;는 멀어진 벡터가 됩니다.</p>
<h3 id="학습이-끝나고-나면">학습이 끝나고 나면</h3>
<p>이제 모델은 우리가 입력한 문장에 대해 계산만 수행해서 결과를 뱉는 역할, 즉 추론(inference)을 하게 됩니다.
더 이상 오차를 계산하거나 가중치를 바꾸지 않고, 학습된 값 그대로 <strong>“예측만 하는 계산기”</strong>가 되는 거죠.</p>
<h2 id="드디어-llm-단어에서-문장으로"><strong>드디어 LLM! 단어에서 문장으로</strong></h2>
<p>지금까지 살펴본 추론 기반 방식은 단어 하나에 의미를 담는 과정을 반복 계산으로 학습하는 구조였습니다.
예측을 해보고, 틀리면 오차를 계산하고, 내부 가중치를 조정하는 이 과정은, 지금 우리가 사용하는 LLM의 핵심 원리와 사실상 동일합니다.</p>
<p>LLM은 이 구조를 <strong>더 크고 정교하게</strong> 확장한 모델입니다.
단어 하나하나의 의미만 학습하는 것이 아니라, <strong>문장 전체의 흐름과 의미, 단어들의 순서, 앞뒤 문맥의 연결 등과</strong>같은 정보까지 함께 고려하며 다음에 나올 단어를 예측할 수 있도록 훈련된 구조입니다.</p>
<p>즉, 작은 단위(단어 벡터)를 다루던 모델이,
더 많은 층과 더 복잡한 계산을 포함하면서 <strong>문맥을 이해하는 모델</strong>로 성장한 것이 바로 LLM입니다.</p>
<h2 id="gpt-우리가-가장-자주-마주치는-llm"><strong>GPT: 우리가 가장 자주 마주치는 LLM</strong></h2>
<p>GPT는 <strong>Generative Pre-trained Transformer</strong>의 약자입니다. 이름을 하나하나 풀어보면 아래와 같습니다.</p>
<ul>
<li><strong>Generative</strong>: 새로운 텍스트를 생성(예측)합니다.</li>
<li><strong>Pre-trained</strong>: 대량의 텍스트 데이터로 미리 학습되었습니다.</li>
<li><strong>Transformer</strong>: 앞서 설명한 계산 구조를 사용합니다.</li>
</ul>
<p>즉, GPT는 트랜스포머 구조를 사용하여 대규모 데이터로 사전 학습된 텍스트 생성 모델입니다.</p>
<p>지금까지 우리는 단어를 숫자로 바꾸고, 그 숫자를 가지고 계산을 반복해서 의미를 배우는 구조까지 살펴봤습니다.
이 모든 계산과 더 다양한 정보 계산을 효율적으로 설계한 구조가 바로 Transformer입니다.</p>
<p>즉 Transformer는</p>
<ul>
<li>단어 순서 같은 위치 정보는 어떻게 반영할까?</li>
<li>어떤 단어에 더 집중해야 할까?</li>
<li>계산을 병렬로 빨리하려면 어떻게 해야 할까?</li>
</ul>
<p>이런 고민을 반영해서 만든 <strong>계산의 설계도</strong>입니다.</p>
<p>GPT, BERT, LLaMA 등 대부분의 최신 LLM은 Transformer 구조를 기반으로 만들어져 있고, 현재도 발전된 변형들이 계속 나오고 있습니다.</p>
<blockquote>
<p>자세한 구조가 궁금하다면 다음 키워드로 공부해보는 걸 추천합니다:
Transformer, Self-Attention, Positional Encoding, Multi-Head Attention</p>
</blockquote>
<h2 id="결론-그리고-fine-tuning-이야기"><strong>결론 그리고 Fine-Tuning 이야기</strong></h2>
<p>지금까지</p>
<ul>
<li>LLM이란 무엇인가?</li>
<li>자연어를 어떻게 숫자로 바꾸는가?</li>
<li>학습은 어떻게 이루어 지는가?</li>
</ul>
<p>를 최대한 단순하고 추상적으로 풀어보았습니다.</p>
<p>그렇다면 이제 이런 생각이 들 수 있습니다. 
이미 거대한 데이터로 학습된 LLM, 조금만 내 목적에 맞게 바꿀 수 없을까? 
그게 바로 <strong>Fine-Tuning</strong>입니다. 그런데 LLM은 정말 큽니다. 예를 들어, 구 모델이 되어버린 LLaMA3.1 8B 모델은 대략 <strong>80억</strong> 개의 파라미터를 가집니다. 여기서 파라미터란 내부 계산을 담당하는 행렬 속 숫자 값들로, 이 숫자 하나하나가 모델의 의미를 결정합니다. (본 글에서 나온 행렬=가중치 라고 이해해도 무방합니다.)</p>
<p>그런데 이걸 전부 바꾸려면 계산 비용이 엄청나고 데이터도 많이 필요하고 시간도 오래 걸리겠죠.
아래와 같은 Fine-Tuning 기법들은 이 계산을 줄이고자 합니다. </p>
<ul>
<li>LoRA: 기존 계산기 옆에 작은 계산기를 붙입니다</li>
<li>Adapter: 중간층에 작은 신경망을 끼워 넣습니다</li>
<li>기타 등등...</li>
</ul>
<p>핵심은 간단합니다. 전체를 바꾸기엔 너무 크니까, 조금만 바꿔서 원하는 능력을 덧입히자.
하지만 모델 크기가 너무 크니 조금만 바꾸는 것도 손쉽지 않겠죠? 조금만 바꾼다 해도 GPU, 멀티 코어 등 자원이 필요하긴 마찬가지입니다.</p>
<p>하지만 이걸 클라우드에서 쉽게 해볼 수 있는 환경도 있습니다.</p>
<p>바로 <strong>AWS Bedrock</strong>입니다.</p>
<p>다음 글에서는</p>
<ul>
<li>Bedrock에서 모델을 불러오고</li>
<li>Fine-Tuning을 실습해보고</li>
<li>모델 평가까지</li>
</ul>
<p>차근차근 다뤄보겠습니다.</p>
<p>정말 긴 글 읽어주셔서 감사합니다 :)</p>
<h3 id="출처">출처</h3>
<p><a href="https://m.yes24.com/goods/detail/72173703">밑바닥부터 시작하는 딥러닝2</a>
<a href="https://www.youtube.com/@3blue1brown">3blue1brown</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔄 파이썬의 비동기: 코루틴에서 시스템콜까지]]></title>
            <link>https://velog.io/@l_cloud/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%97%90%EC%84%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%BD%9C%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@l_cloud/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%97%90%EC%84%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%BD%9C%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 24 Apr 2025 13:04:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 Python의 코루틴, Future, 이벤트 루프, system call 같은 비동기 프로그래밍의 개념과 구조를 이야기합니다. async, await, asyncio.run() 등을 한두 번 사용해 본 경험이 있는 독자를 대상으로 합니다.</p>
</blockquote>
<h3 id="비동기란">비동기란?</h3>
<p>&quot;비동기는 어떤 작업을 요청하고 끝날 때까지 기다리지 않고 다른 일을 하는 방식&quot;을 의미합니다. 이를 통해 동시성을 처리할 수 있으며 꼭 병렬성이 아니어도 됩니다. 보통 <strong>함수의 실행을 중단(suspend)하고 나중에 다시 재개(resume)할 수 있는 특수한 함수</strong>인 코루틴으로 이것을 구현하고는 합니다.</p>
<h3 id="코루틴이란">코루틴이란?</h3>
<p>일반적으로 코루틴은 &#39;함수의 실행을 중단(suspend)하고 나중에 다시 재개(resume)할 수 있는 특수한 함수&#39;를 의미합니다. 즉 일반 함수는 return되면 끝이지만, 코루틴은 중간에 멈췄다가 나중에 다시 실행할 수 있습니다.</p>
<p>asyncio와 await이 도입되기 전 파이썬은 제너레이터로 코루틴을 구현 하고는 했습니다. 아래는 아주 간단한 예시 코드입니다.</p>
<pre><code class="language-python">def my_coroutine():
   print(&quot;Start&quot;)
   x = yield &quot;Paused at yield&quot;
   print(f&quot;Resumed with x = {x}&quot;)

# 사용
coro = my_coroutine()
print(next(coro))        # &quot;Start&quot; → &quot;Paused at yield&quot;를 반환함... 다른 로직
print(coro.send(42))     # &quot;Resumed with x = 42&quot; send로 x에 42를 주입(?)
</code></pre>
<p>next(coro)는 실행을 시작하고 첫 yield에서 중단합니다. 이후 다른 로직을 실행하다 send(42)를 통해 코루틴에 값을 보내고 재개합니다.</p>
<p>위 예시처럼 yield 기반 코루틴은 중단과 재개가 가능하지만, 여러 개의 코루틴을 동시에 실행해야 한다면 문제가 복잡해집니다. 어떤 코루틴이 완료되었는지 확인하고, 중간에 예외가 발생했다면 이를 어떻게 처리할지 결정해야 하며, 각 코루틴을 어느 시점에 재개할지까지 모든 흐름을 개발자가 직접 제어해야 합니다.</p>
<p>이러한 로직을 수동으로 작성하려면 상태 관리, 에러 핸들링, 실행 순서 조정 등을 모두 고려해야 하며, 코루틴 개수가 많아질수록 유지보수도 어려워집니다. 생각만 해도 번거롭고 복잡하죠. 그래서 등장한 것이 바로 <strong>여러 코루틴의 실행과 중단, 완료 상태를 자동으로 관리해 주는 ‘이벤트 루프(event loop)’</strong>입니다.</p>
<p>파이썬에서는 이 개념을 표준 라이브러리 수준에서 제공하며, 그 구현체가 바로 asyncio입니다.</p>
<h3 id="asyncio-future-task">asyncio, Future, Task</h3>
<p>asyncio는 어떻게 작업의 완료 여부, 중단 시점, 재개할 타이밍 등을 모두 관리할까요? 여기서 Future와 Task 객체가 등장합니다.</p>
<pre><code class="language-python"># https://github.com/python/cpython/blob/main/Lib/asyncio/futures.py
class Future:
 &quot;&quot;&quot;
 Represents the result of an asynchronous computation.
 This class is *almost* compatible with concurrent.futures.Future.
 &quot;&quot;&quot;
</code></pre>
<p>Future 객체는 완료되었을 수도 있고 아닐 수도 있는 지연된 계산을 표현하는 데 사용되는 객체입니다. Future는 주로 asyncio가 내부적으로 사용하는 객체로, 우리가 직접 생성하거나 관리할 필요는 없습니다. 이는 asyncio가 어떤 작업이 언제 완료될지를 알고 있고, 그 흐름을 스스로 관리하기 때문입니다. Future 객체는 .result() 나 .add_done_callback() 같은 메서드를 통해 작업의 완료 여부를 확인하고 결과나 예외를 받아올 수 있도록 도와줍니다. 즉 지연된 작업을 캡슐화하는 객체라고 할 수 있습니다.</p>
<p>그렇다면 Future를 여러 개 생성해서 큐에 담아 관리하면 코루틴들을 관리할 수 있지 않을까요?</p>
<p>실제로 asyncio는 그런 방식으로 코루틴을 관리합니다. 예를 들어 asyncio.as_completed()는 내부적으로 큐와 Future 객체(정확히는 코루틴 실행에 특화된 Future을 상속받은 Task 객체)를 사용하여 코루틴을 관리하는 것을 확인할 수 있습니다.</p>
<pre><code class="language-python">def as_completed(fs, *, timeout=None):
 &quot;&quot;&quot;Return an iterator whose values are coroutines.

 When waiting for the yielded coroutines you&#39;ll get the results (or
 exceptions!) of the original Futures (or coroutines), in the order
 in which and as soon as they complete.

 This differs from PEP 3148; the proper way to use this is:

 for f in as_completed(fs):
 result = await f  # The &#39;await&#39; may raise.
 # Use result.

 If a timeout is specified, the &#39;await&#39; will raise
 TimeoutError when the timeout occurs before all Futures are done.

 Note: The futures &#39;f&#39; are not necessarily members of fs.
 &quot;&quot;&quot;
     if futures.isfuture(fs) or coroutines.iscoroutine(fs):
         raise TypeError(f&quot;expect an iterable of futures, not {type(fs).__name__}&quot;)

   from .queues import Queue  # Import here to avoid circular import problem.
   done = Queue()

   loop = events._get_event_loop()
   todo = {ensure_future(f, loop=loop) for f in set(fs)}
   timeout_handle = None

   def _on_timeout():
        for f in todo:
           f.remove_done_callback(_on_completion)
           done.put_nowait(None)  # Queue a dummy value for _wait_for_one().
           todo.clear()  # Can&#39;t do todo.remove(f) in the loop.

   def _on_completion(f):
        if not todo:
           return  # _on_timeout() was here first.
        todo.remove(f)
        done.put_nowait(f)
        if not todo and timeout_handle is not None:
            timeout_handle.cancel()

   async def _wait_for_one():
     f = await done.get()
     if f is None:
     # Dummy value from _on_timeout().
       raise exceptions.TimeoutError
       return f.result()  # May raise f.exception().

   for f in todo:
        f.add_done_callback(_on_completion)
        if todo and timeout is not None:
            timeout_handle = loop.call_later(timeout, _on_timeout)
            for _ in range(len(todo)):
               yield _wait_for_one()
</code></pre>
<p>asyncio는 내부적으로 Future와 Task를 기반으로 코루틴을 스케줄링하고 결과를 추적하는 기능을 제공하므로, 별도의 구현 없이도 편하게 사용할 수 있습니다 :)</p>
<h3 id="비동기-네트워크-io">비동기 네트워크 I/O</h3>
<p>그런데 함수를 중단하고 다시 실행하는 게 뭐가 좋을까요?</p>
<p>네트워크 I/O를 예로 들어봅시다. CPU는 일반적으로 1ns~10ns 단위로 연산을 수행하지만, 네트워크 왕복 시간은 보통 수 ms 단위로, 무려 수십만 배나 느립니다.
예를 들어 http.get() 같은 blocking 호출은 응답이 도착할 때까지 해당 쓰레드가 아무 일도 하지 못한 채 대기 상태로 머무르게 되며, 그 시간 동안 CPU 자원은 사실상 낭비됩니다.</p>
<p>이 작업을 비동기 방식으로 처리하면 어떨까요?
I/O 요청 이후 <strong>코루틴의 실행을 일시 중단(suspend)</strong>하고, 그사이에 다른 작업을 먼저 수행한 뒤, 응답이 도착하면 <strong>재개(resume)</strong>하도록 구성하면, CPU는 대기 시간 동안에도 유용한 작업을 계속 수행할 수 있게 됩니다.</p>
<h3 id="시스템콜과-dma">시스템콜과 DMA</h3>
<p>그렇다면 이런 중단과 재개가 어떻게 가능할까요?</p>
<p>운영체제 수업에서 배운 내용을 떠올려 보면, 바로 DMA 개념이 생각납니다.
네트워크 데이터를 송수신할 때는 일반적으로 NIC가 DMA를 통해 직접 메모리에 접근합니다. 이 과정에는 CPU의 개입이 거의 필요 없습니다.
NIC는 전송이 끝나면 인터럽트(interrupt)를 통해 CPU에 알리고, 이후 CPU는 해당 데이터를 처리하면 됩니다.
즉, CPU는 네트워크 I/O가 완료될 때까지 기다릴 필요 없이 다른 작업을 수행할 수 있게 됩니다.</p>
<h3 id="epoll과-시스템-콜의-역할">epoll과 시스템 콜의 역할</h3>
<p>그렇다면 CPU는 어떻게 &quot;데이터가 도착했다&quot;는 사실을 알게 될까요?
이때 등장하는 것이 바로 OS의 시스템 콜입니다.
운영체제는 select, poll, epoll 같은 시스템 콜을 통해 열어둔 소켓이나 파일 디스크립터에 데이터가 도착했는지를 감시할 수 있게 해줍니다. 이 시스템 콜들을 통해 &quot;읽을 준비가 된 소켓&quot;을 알 수 있습니다.</p>
<blockquote>
<p>select, poll, epoll 등이 어떻게 작동하는지 궁금하다면, 각 시스템 콜에 대한 man 페이지나 관련 문서를 참고해 보는 것을 추천합니다.</p>
</blockquote>
<h3 id="asyncio와-selector">asyncio와 Selector</h3>
<p>Python의 asyncio도 해당 시스템 콜을 사용하고 있습니다. 물론 이 시스템 콜을 직접 호출하지 않고, 내부적으로 Selector 객체를 통해 추상화하여 사용합니다.</p>
<pre><code class="language-python"># https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/asyncio/unix_events.py#L57
class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
 &quot;&quot;&quot;Unix event loop.

 Adds signal handling and UNIX Domain Socket support to SelectorEventLoop.
 &quot;&quot;&quot;</code></pre>
<p>CPython/Lib/selectors.py를 보면 다음과 같이 현재 플랫폼에서 가장 효율적인 시스템 콜을 선택합니다.</p>
<pre><code class="language-python"># https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/selectors.py#L609
# Choose the best implementation, roughly:#    epoll|kqueue|devpoll &gt; poll &gt; select.
# select() also can&#39;t accept a FD &gt; FD_SETSIZE (usually around 1024)
if _can_use(&#39;kqueue&#39;):
 DefaultSelector = KqueueSelector
elif _can_use(&#39;epoll&#39;):
 DefaultSelector = EpollSelector
elif _can_use(&#39;devpoll&#39;):
 DefaultSelector = DevpollSelector
elif _can_use(&#39;poll&#39;):
 DefaultSelector = PollSelector
else:
 DefaultSelector = SelectSelector</code></pre>
<h3 id="실제-비교해-보기">실제 비교해 보기</h3>
<p>이제 동기와 비동기 방식의 네트워크 I/O의 속도 차이를 직접 비교해 봅시다.
아래는 간단한 HTTPS GET 요청의 예시입니다.</p>
<pre><code class="language-python">import asyncio, ssl
import socket


async def fetch_https_async(host: str):
 # 비동기 코드
 ssl_context = ssl.create_default_context()
 # 1. 비동기적으로 TCP 연결 + SSL 핸드셰이크까지 완료되기를 기다림
 reader, writer = await asyncio.open_connection(host, 443, ssl=ssl_context)
 request_header = (
 f&quot;GET / HTTP/1.1\r\n&quot; f&quot;Host: {host}\r\n&quot; f&quot;Connection: close\r\n\r\n&quot;
 )
 writer.write(request_header.encode(&quot;utf-8&quot;))
 # 2. write 버퍼가 비워질 때까지 대기 (소켓이 writable 상태가 될 때까지)
 await writer.drain() # 어째서 함수 이름이 drain.. ㅠ 파이이썬 참..    # 3. 서버 응답이 도착할 때까지 대기 (readable 상태를 기다림)
 response_data = await reader.read()
 response_data.decode(&quot;utf-8&quot;, errors=&quot;ignore&quot;)
 writer.close()
 # 4. 연결이 완전히 종료되기를 기다림
 await writer.wait_closed()


def fetch_https_sync(host: str):
 # 동기 코드
 ssl_context = ssl.create_default_context()

 sock = socket.create_connection((host, 443))
 ssock = ssl_context.wrap_socket(sock, server_hostname=host)
 request = f&quot;GET / HTTP/1.1\r\n&quot; f&quot;Host: {host}\r\n&quot; f&quot;Connection: close\r\n\r\n&quot;
 ssock.sendall(request.encode(&quot;utf-8&quot;))

 response = b&quot;&quot;
 while True:
 chunk = ssock.recv(4096)
 if not chunk:
 break
 response += chunk
 response.decode(&quot;utf-8&quot;, errors=&quot;ignore&quot;)
 ssock.close()
 sock.close()


import time

urls = [
 &quot;example.com&quot;,
 &quot;www.python.org&quot;,
 &quot;www.google.com&quot;,
 &quot;www.wikipedia.org&quot;,
 &quot;www.naver.com&quot;,
 &quot;www.daum.net&quot;,
 &quot;www.reddit.com&quot;,
]


def sync_get():
 print(&quot;=== SYNC 테스트 시작 ===&quot;)
 start = time.time()
 for host in urls:
 fetch_https_sync(host)
 elapsed = time.time() - start
 print(f&quot;동기 총소요 시간: {elapsed:.2f}초\n&quot;)


async def async_get():
 print(&quot;=== ASYNC 테스트 시작 ===&quot;)
 start = time.time()
 tasks = [fetch_https_async(host) for host in urls]
 results = await asyncio.gather(*tasks)
 elapsed = time.time() - start
 print(f&quot;비동기 총소요 시간: {elapsed:.2f}초\n&quot;)


if __name__ == &quot;__main__&quot;:
 sync_get()
 asyncio.run(async_get())

&#39;&#39;&#39;&#39;
필자의 컴퓨터 환경

=== SYNC 테스트 시작 ===
동기 총소요 시간: 2.81초

=== ASYNC 테스트 시작 ===
비동기 총소요 시간: 0.88초
&#39;&#39;&#39;</code></pre>
<p>확실히 비동기 방식이 더 빠릅니다.
동기 방식은 요청을 순차적으로 하나씩 처리해야 하므로, 각 요청이 완료될 때까지 무조건 기다려야 하죠.
반면 비동기 방식에서는 <strong>await</strong> 키워드를 만날 때마다 <strong>현재 실행을 일시 중단(suspend)</strong>하고, 제어권을 이벤트 루프에 넘깁니다.
이벤트 루프는 그동안 다른 준비된 코루틴을 실행하고, <strong>이전 코루틴이 다시 실행 가능한 상태가 되면 재개(resume)</strong>시킵니다.
이런 구조 덕분에 I/O처럼 느린 작업을 기다리는 동안 CPU는 다른 유용한 작업을 처리할 수 있게 됩니다.</p>
<h3 id="결론">결론</h3>
<p>왜 비동기 네트워크 처리처럼 보이는 <strong>open_connection()</strong>이 별도 네트워크 모듈로 빠지지 않고,
<strong>asyncio</strong> 안에 그대로 포함되어 있는지,
또 왜 익숙한 <strong>flush()</strong>가 아닌 <strong>drain()</strong>이라는 이름이 쓰였는지 정말 파이썬다운 알쏭달쏭한 의문과 함께, 비동기 프로그래밍의 개념과 동작 방식을 간단히 짚어보았습니다.</p>
<p>다음 글에서는 asyncio를 활용한 실전 비동기 패턴과 주의할 점들, 혹은 GIL이 존재하는 파이썬 환경에서 비동기와 멀티 프로세싱의 차이 또는 비동기를 정말 잘 쓸 수 있는 현실적인 사례 그리고 uvloop 톺아보기 중 하나를 살펴보려 합니다.</p>
<p>긴 글 읽어주셔서 감사합니다.</p>
<h3 id="출처">출처</h3>
<p><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B9617416545">전문가를 위한 파이썬 2판</a>
<a href="https://tenthousandmeters.com/blog/python-behind-the-scenes-12-how-asyncawait-works-in-python/">외국 블로그</a>
<a href="https://dev.to/uponthesky/python-a-journey-to-python-async-5-asyncio-library-kep#:~:text=if%20_can_use,poll%27%29%3A%20DefaultSelector%20%3D%20PollSelector%20else">외국 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전문가를 위한 파이썬 - 2장 ]]></title>
            <link>https://velog.io/@l_cloud/%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-2%EC%9E%A5</link>
            <guid>https://velog.io/@l_cloud/%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-2%EC%9E%A5</guid>
            <pubDate>Sat, 08 Feb 2025 09:25:47 GMT</pubDate>
            <description><![CDATA[<p>본 글은 전문가를 위한 파이썬 2판을 읽고 정리한 글입니다.</p>
<blockquote>
<p>PyObject와 같은 기본 개념과 C 코드를 대략 읽을 수 있는 독자를 대상으로 합니다.</p>
</blockquote>
<p>파이썬 표준 라이브러리는 C로 구현된 시퀀스형 제공합니다. 아래처럼 구분도 가능하고, 가변성에 따른 시퀀스 분류도 가능합니다.</p>
<ul>
<li>컨테이너 시퀀스 (서로 다른 자료형의 항목 담을 수 있음, list, tuple, collctions,deque)</li>
<li>균일 시퀀스 (단 하나의 자료형만 담을 수 있는 자료형, str, bytes, array, array.array)</li>
</ul>
<p>균일 시퀀스가 메모리를 더 적게 사용하지만 , 바이트, 정수, 실수 등 기본적인 자료형만 담을 수 있습니다.
왜 메모리를 적게 사용할까요?  균일 시퀀스는 미리 정해진 타입과 크기를 기반으로 데이터를 저장하며, 일부 타입은 C 스타일로 저장되어 PyObject 형태로 변환되지 않기 때문입니다. 물론 연산 시에는 PyObject로 변환하는 과정이 필요해 추가적인 오버헤드가 발생할 수 있습니다.</p>
<blockquote>
<p>PyObject를 잘 모른다면..
<a href="https://velog.io/@l_cloud/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EA%B3%BC-%EB%A9%94%EB%AA%A8%EB%A6%AC">https://velog.io/@l_cloud/파이썬과-메모리</a></p>
</blockquote>
<h3 id="list-comprehension">list comprehension</h3>
<p>set, dict, list comprehension은 for 문에 할당된 변수를 유지하기 위해 고유한 local scope를 할당받습니다. ex) <code>[i for i in range(4)]</code> ’i’ &lt;&lt; local scope에 할당</p>
<pre><code class="language-bash">&gt;&gt;&gt; import dis
&gt;&gt;&gt; code = &quot;&quot;&quot;
... i = 0
... [i for i in range(4)]
... &quot;&quot;&quot;
&gt;&gt;&gt;
&gt;&gt;&gt; dis.dis(code)
  0           0 RESUME                   0

  2           2 LOAD_CONST               0 (0)
              4 STORE_NAME               0 (i)

  3           6 LOAD_CONST               1 (&lt;code object &lt;listcomp&gt; at 0x104d363f0, file &quot;&lt;dis&gt;&quot;, line 3&gt;)
              8 MAKE_FUNCTION            0
             10 PUSH_NULL
             12 LOAD_NAME                1 (range)
             14 LOAD_CONST               2 (4)
             16 PRECALL                  1
             20 CALL                     1
             30 GET_ITER
             32 PRECALL                  0
             36 CALL                     0
             46 POP_TOP
             48 LOAD_CONST               3 (None)
             50 RETURN_VALUE
# list comprehension 내부
Disassembly of &lt;code object &lt;listcomp&gt; at 0x104d363f0, file &quot;&lt;dis&gt;&quot;, line 3&gt;:
  3           0 RESUME                   0
              2 BUILD_LIST               0
              4 LOAD_FAST                0 (.0)
        &gt;&gt;    6 FOR_ITER                 4 (to 16)
              8 STORE_FAST               1 (i) #외부 변수 i가 아닌 local scope i에 할당
             10 LOAD_FAST                1 (i)
             12 LIST_APPEND              2
             14 JUMP_BACKWARD            5 (to 6)
        &gt;&gt;   16 RETURN_VALUE</code></pre>
<p>대괄호 대신 소괄호 사용하면 튜플이 아닌 제너레이터 표현식이 됩니다.</p>
<h3 id="튜플">튜플</h3>
<p>여기서 튜플을 잠시 살펴봅시다. 튜플은 불변 시퀀스의 한 종류입니다. 크기는 고정이며 t가 튜플이고 l이 list일 때, tuple(t)를 하면 list(l) 과 다르게 t에 대한 참조를 반환할 뿐이며 값을 복사할 필요가 없어서 성능상 이점이 있습니다. 참고로 튜플 항목에 대한 참조는 튜플 구조체 배열에 저장되지만, 리스트는 다른 곳에 저장된 참조 배열에 대한 포인터 항목을 가집니다. </p>
<pre><code class="language-python">// Objects/tupleobject.c
typedef struct {
    PyObject_VAR_HEAD
    /* ob_item contains space for &#39;ob_size&#39; elements.
       Items must normally not be NULL, except during construction when
       the tuple is not yet visible outside the function that builds it. */
    PyObject *ob_item[1];
} PyTupleObject;

// Objects/PyListObject.c
typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for &#39;allocated&#39; elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 &lt;= ob_size &lt;= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;
</code></pre>
<p>tuple은 최적화 때문에 <strong>reverse</strong>() 메서드가 제공되지 않습니다. 하지만 reverse()함수 사용이 가능한데 이 이유는 아래와 같습니다. </p>
<ul>
<li><code>__reversed__</code>를 추가하는 것보다 이미 최적화된 <code>__len__</code>과 <code>__getitem__</code>을 사용하는 것이 메모리와 성능 면에서 더 효율적이기 때문입니다.</li>
</ul>
<p>개인적인 추측이지만 tuple의 경우 immutable 객체이기 때문에 크기가 고정 + 딱 크기만큼의 배열을 가지고 있어 굳이 <code>__reverse()__</code> 가 필요 없지만, list는 mutable 객체로, 실제 저장된 요소의 개수보다 더 큰 내부 배열 공간을 가지고 있어 len과 getitem만 사용하기에는 비효율적이어서 그렇다고 생각합니다. 자세한 것은 실제 구현 함수를 보면 되겠지요 :)</p>
<h2 id="시퀀스-반복형-객체의-언패킹-병렬-할당">시퀀스 반복형 객체의 언패킹 (병렬 할당)</h2>
<p>파이썬에서는 아래처럼 병렬 할당이 가능합니다.</p>
<pre><code class="language-python">x, y = 1, 2  # 튜플 언패킹 (콤마(,)로 구분된 여러 값들은 자동으로 튜플로 패킹)
a, b, c = [4, 5, 6]  # 리스트 언패킹
first, *rest = range(5)  # 확장 언패킹</code></pre>
<p>패킹은 여러 개의 값을 하나의 시퀀스로 묶는 것을 의미하고 언패킹은 시퀀스를 개별값들로 푸는 것을 의미합니다. 이는 다중값 반환을 가능하게 합니다.</p>
<pre><code class="language-python"># 여러 값들이 하나의 튜플로 패킹됨
packed = 1, 2, 3# (1, 2, 3)
# 튜플의 값들이 개별 변수로 언패킹됨
x, y, z = (1, 2, 3)

def get_point(): #다중값 반환 함수
 return 1, 2
</code></pre>
<p>Go와 Java를 개인적으로 공부한 적이 있는데 해당 언어들이 생각나는 파트였습니다. 단일 값 반환 언어와 다중값 반환 언어 모두 장단이 있지만 개인적으로 저는 다중값 반환을 잘 사용하지는 않습니다. 약타입 + 다중값 반환의 조합에서 유지보수를 잘할 수 있는 코드를 작성할 자신이.. 잘 없습니다 ㅎㅎ</p>
<p>또한 파이썬은 중첩 언패킹도 지원합니다.  언패킹할 표현식을 받는 튜플은 (a,b,(c,d))처럼 다른 튜플을 내포할 수 있고, 이 중첩 구조체에 일치하면 파이썬이 제대로 처리합니다.</p>
<p> ex) [(1,2,(3,4)] → for 문에서 name, _ , (c,d) 이렇게 처리 가능.</p>
<h2 id="26-시퀀스를-이용한-패턴-매칭--httpspepspythonorgpep-0634">[2.6 시퀀스를 이용한 패턴 매칭]  (<a href="https://peps.python.org/pep-0634/">https://peps.python.org/pep-0634/</a>)</h2>
<p>파이썬 3.10부터 사용 가능한 문법입니다.
파이썬에서 패턴 매칭이 있다는 것을 이 책을 통해 알게 되었습니다. 조금은 부끄럽네요. match/case 문은 구조 부해를 하고 첫 번째 match되는 case를 실행합니다. (match 이후 break이 없어도 일치하는 case를 실행하지 않습니다) <a href="https://github.com/gvanrossum/patma/blob/3ece6444ef70122876fd9f0099eb9490a2d630df/README.md">귀도 반 로섬의 예시</a>도 있습니다. </p>
<p>시퀀스 패턴은 튜플이나 리스트나 어떠한 형태로 중첩한 조합이더라도 차이가 없습니다. collections.abc.Sequence의 구상 혹은 가상 서브 클래스의 객체에 매칭될 수 있으나  str, bytes, bytearray 객체는 시퀀스로 처리되지 않습니다. </p>
<pre><code class="language-python"># match에서 [ ( &lt;&lt; 동일 취급. 리스트나 튜플이나.. (퀀스 패턴은 튜플이나 리스트나 어떠한 형태로 중첩한 조합이더라도 차이가 없음)
def check(value):
     match value:
          case [x, (y, z)]:
               print(f&quot;리스트-튜플: {x}, {y}, {z}&quot;)
          case [x, int(y)]:
               print(f&quot;리스트-정수: {x}, {y}&quot;)
          case (x,y):
               print(f&quot;튜플: {x}, {y}&quot;)
          case _:
               print(&quot;매칭 실패&quot;)

check([1, (2,3)])
check([1, 2])
check([1, [2,3,3]])   #case (x,y)에 걸림! 유의!</code></pre>
<p>case 안에서 캡처된 변수는 match 문 밖에서도 사용할 수 있습니다. 이는 바다코끼리 연산자(:=)와 같은 스코프 규칙을 따르기 때문입니다.</p>
<pre><code class="language-python">def check(value):
     match value:
          case [x,*_ ,[y, z]]:
               print(f&quot;매칭: {x}, {y}, {z}&quot;)

check([1,2,[45],[5],[3,5,6] ,[4,5]])

*_ ㅇ변수에 바인딩하지 않고 임의 개수의 항목에 매칭 0개 이상의 항목!

Summary: the name becomes a local variable in the closest containing function scope unless there’s an applicable nonlocal or global statement.</code></pre>
<p>“패턴 매칭은 선언적 프로그래밍의 예로서, 어떻게 매칭할지가 아니라 무엇을 매칭할지를 코딩한다.” 라고 저자는 이야기합니다. 개인적으로 강타입 처럼(?) 사용자들이 규칙을 잘 정해두고 사용하면 if elif 보다 훨씬 깔끔하고 명확하게 코딩할 수 있을 것 같습니다.</p>
<blockquote>
<p>파이썬 3.9에서 기존 LL(1) 기반 파서에서 PEG 기반 파서로 변경이 있으면서 탄생할 수 있었던 문법입니다. 파서에 관심이 있으시다면 <a href="https://velog.io/@kyeongmo31/Python3-%EC%9D%98-PEG-%ED%8C%8C%EC%84%9C">블로그</a>와 <a href="https://peps.python.org/pep-0617/">PEP 문서</a>를 추천합니다.</p>
</blockquote>
<hr>
<h3 id="27-슬라이싱">2.7 슬라이싱</h3>
<p>seq[start:stop:step] → seq.<strong>getitem</strong>(slice([start,stop,step])) 호출</p>
<p>memoryview(이후 등장)를 제외한 파이썬 내장 시퀀스형은 1차원 구조이므로, 단 하나의 인덱스나 슬라이스만 지원합니다. 파이썬의 list 객체 내부 구조를 떠올리며 [[1,2],[2,3]] 이런 리스트의 리스트 형태를 생각해 봅시다. 리스트에 리스트 포인터가 저장된 1차원 형태임 것을 상상할 수 있습니다. 즉 데이터가 연속된 메모리 블록에 저장되어 있지 않습니다. 이와 다르게 넘파이는 데이터가 하나의 연속된 메모리 블록에 저장됩니다. shape를 가지고 있어서 진짜 다차원 배열이며 다차원 슬라이싱이 가능합니다.</p>
<h3 id="210-메모리-뷰">2.10 메모리 뷰</h3>
<p><a href="https://docs.python.org/ko/3/library/stdtypes.html#memoryview"><code>memoryview</code></a> 객체는 파이썬 코드가 <a href="https://docs.python.org/ko/3/c-api/buffer.html#bufferobjects">버퍼 프로토콜</a> 을 지원하는 객체의 내부 데이터에 복사 없이 접근할 수 있게 합니다. (버퍼 프로토콜 == Certain objects available in Python wrap access to an underlying memory array or <em>buffer)</em></p>
<pre><code class="language-python">https://docs.python.org/3/c-api/buffer.html#buffer-protocol</code></pre>
<p>공유 메모리 시퀀스 형으로 bytes 형을 복사하지 않고 배열의 슬라이스를 다루게 해 줍니다. 즉 값 복사 없이 사용할 수 있습니다. </p>
<pre><code class="language-python"># 큰 바이트 시퀀스 생성
data = b&#39;Hello World&#39; * 1000  # 큰 데이터

# 일반적인 슬라이싱 - 새로운 메모리에 복사됨
slice1 = data[1:5]  

# memoryview 사용 - 메모리 복사 없이 참조
mv = memoryview(data)
slice2 = mv[1:5]  # 복사 없이 원본 메모리 참조

print(bytes(slice2))  # b&#39;ello&#39;</code></pre>
<p>memoryview.cast() 함수는 memoryview가 바라보는 메모리의 내용을 다른 데이터 타입으로 읽는  memoryview 객체를 반환합니다. 물론 언제나 동일한 메모리를 공유합니다.또한  shape() 함수도 있어 다른 형태(2 x 3  - &gt; 3 x 2 등)으로 볼 수 있습니다. 다만 numpy처럼 다차원 슬라이싱을 지원하지는 않습니다.</p>
<h3 id="기타-흥미로웠던-것들">기타 흥미로웠던 것들</h3>
<p>복합 할당은 원자적 연산이 아닙니다. 아래 예시가 상당히 흥미로웠습니다.
t[2]인 list에 + 연산을 하고 이후 immutable인 t에 = 할당을 하려고해서 에러가 발생하는 예시입니다.  <del>마치 버그인데 기능이라고 우기는 것 같은.. 상황</del></p>
<pre><code class="language-python">&gt;&gt;&gt; t = (1,2,[30,40])
&gt;&gt;&gt; t[2] += [50,60]
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
TypeError: &#39;tuple&#39; object does not support item assignment
&gt;&gt;&gt; t
(1, 2, [30, 40, 50, 60])
&gt;&gt;&gt;</code></pre>
<p><strong>리스트가 답이 아닐 때</strong>
리스트의 내부 구조를 보면 알 수 있지만 이중 포인터에 모든 요소는 PyObject 요소이며 메모리에 연속적으로 직접 저장되어 있지 않습니다. int만 저장하였다고 해도 PyObject_HEAD를 포함합니다. 이와 다르게 arrayobject는 데이터를 메모리에 연속적으로 직접 저장하며 PyObject형태가 아닌 C 데이터 타입을 저장합니다.</p>
<pre><code class="language-python">typedef struct arrayobject {
 PyObject_VAR_HEAD
 char *ob_item;
 Py_ssize_t allocated;
 const struct arraydescr *ob_descr;  // 배열의 타입 정보를 담고 있는 descriptor
 PyObject *weakreflist; /* List of weak references */
 Py_ssize_t ob_exports;  /* Number of exported buffers */
} arrayobject;
</code></pre>
<p>물론 여러 타입을 저장할 수 없고 연산시 PyObject로 타입 캐스팅이 필요하지만, 직렬화/역 직렬화 속도도 빠르고 메모리 사용량도 적기 때문에 적절한 상황에 사용하면 좋은 구조체입니다.</p>
<p><a href="https://docs.python.org/ko/3.13/library/array.html">지원하는 타입</a>과 <a href="https://hyperconnect.github.io/2023/05/30/Python-Performance-Tips.html">성능 비교</a></p>
<p><strong>기타 자료구조</strong>
Queue → thread-safe. SimpleQueue (task_done(), join() 이 없음)</p>
<p>주의! maxsize로 크기를 제한할 수 있지만 deque와 달리 공간이 꽉 찼을 때 항목을 안 버리고, 다른 스레드에서 큐 안의 항목을 제거해 공간을 확보해 줄 때까지 새로운 항목의 추가를 블로킹하며 기다립니다.</p>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전문가를 위한 파이썬 - 1장]]></title>
            <link>https://velog.io/@l_cloud/%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-1%EC%9E%A5</link>
            <guid>https://velog.io/@l_cloud/%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-1%EC%9E%A5</guid>
            <pubDate>Tue, 21 Jan 2025 13:47:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/l_cloud/post/d822f8ec-5325-43b9-a270-ca4ca0aaedb0/image.png" alt=""></p>
<p>본 글은 <a href="https://www.yes24.com/product/goods/139871125">전문가를 위한 파이썬 2판</a>을 읽고 정리한 글입니다.</p>
<blockquote>
<p>PyObject와 같은 기본 개념과 C 코드를 대략 읽을 수 있는 독자를 대상으로 합니다.</p>
</blockquote>
<p>1장은 파이썬의 일관성과 관련된 데이터 모델에 대해 이야기합니다.
데이터 모델이 제공하는 API를 잘 사용하기 위해  Magic Method 혹은 Dunder Method의 강력함을 소개합니다. 이를 구현하면 사용자 정의 객체도 파이썬 내장 객체처럼 작동하므로 파이썬 다운 표현력 있는 코딩 스타일을 사용할 수 있음을 강조합니다.</p>
<p>예를 들어 봅시다.</p>
<p>Java의 경우 아래처럼 길이를 호출하는 여러 함수가 있습니다.</p>
<pre><code class="language-java">// Java가 떠오른다...
int[] array = {1, 2, 3};
int arrayLength = array.length;

String str = &quot;hello&quot;;
int strLength = str.length();

ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();
int listSize = list.size();</code></pre>
<p>하지만 파이썬의 경우 아래와 같은 자료형이 모두 len 으로 길이를 알아낼 수 있죠. 그뿐만 아니라 사용자 정의 클래스도 <strong>len</strong> 을 구현했다면 len 메소드를 사용할 수 있습니다.</p>
<pre><code class="language-python">my_list = [1, 2, 3]
my_string = &quot;hello&quot;
my_tuple = (1, 2, 3)
my_dict = {&quot;a&quot;: 1, &quot;b&quot;: 2}
my_set = {1, 2, 3}
# len(my_list), len(my_string), len...</code></pre>
<p>len()은 abs()와 마찬가지로 파이썬 데이터 모델의 특별 대우를 받으므로 메서드라고 부르지 않는다는 점이 재미있었습니다.  len()은 CPython의 내장 객체에 대해서는 메서드를 호출하지 않고, 길이는 단지 C구조체의 필드를 읽어옵니다. <code>builtin_len</code> 의 코드입니다.</p>
<pre><code class="language-c">// Python/bltinmodule.c
static PyObject *
builtin_len(PyObject *module, PyObject *obj)
{
    Py_ssize_t res;

    res = PyObject_Size(obj);
    if (res &lt; 0) {
        assert(PyErr_Occurred());
        return NULL;
    }
    return PyLong_FromSsize_t(res);
}

---
// Objects/abstract.c
Py_ssize_t
PyObject_Size(PyObject *o)
{
    if (o == NULL) {
        null_error();
        return -1;
    }

    PySequenceMethods *m = Py_TYPE(o)-&gt;tp_as_sequence;
    if (m &amp;&amp; m-&gt;sq_length) { 
        Py_ssize_t len = m-&gt;sq_length(o); // sq_length 메서드 호출
        assert(_Py_CheckSlotResult(o, &quot;__len__&quot;, len &gt;= 0));
        return len;
    }

    return PyMapping_Size(o); #map 같은 객체들
}</code></pre>
<p>실제로 파이썬 내부 오브젝트의 sq_length는 어떤 식으로 구현되어 있나 궁금해서 찾아보았습니다. 아래는 list의 예시로 C 구조체의 ob_size 필드를 직접 읽고 있음을 확인할 수 있습니다.</p>
<pre><code class="language-c">// Objects/listobject.c
static PySequenceMethods list_as_sequence = {
    list_length,                                /* sq_length */
    list_concat,                                /* sq_concat */
    ...
};

static inline Py_ssize_t PyList_GET_SIZE(PyObject *op) {
    PyListObject *list = _PyList_CAST(op);
#ifdef Py_GIL_DISABLED
    return _Py_atomic_load_ssize_relaxed(&amp;(_PyVarObject_CAST(list)-&gt;ob_size));
#else
    return Py_SIZE(list);
#endif
}

---
//Include/cpython/listobject.h
static inline Py_ssize_t Py_SIZE(PyObject *ob) {
    assert(Py_TYPE(ob) != &amp;PyLong_Type);
    assert(Py_TYPE(ob) != &amp;PyBool_Type);
    return  _PyVarObject_CAST(ob)-&gt;ob_size;
}
</code></pre>
<p>사용자 정의 클래스는 그럼 어떠한 과정을 거치고 있을까요? <code>cpython/Objects/typeobject.c</code> 를 살펴보았습니다. 코드를 100% 정확히 이해한 것은 아니지만 <code>sq_length</code>의 구현체(?)를 보면 <strong>len</strong> 을 호출하는 부분을 확인 할 수 있습니다.</p>
<pre><code class="language-c">    ...
    SQSLOT(__len__, sq_length, slot_sq_length, wrap_lenfunc,
           &quot;__len__($self, /)\n--\n\nReturn len(self).&quot;),
    ...
    static Py_ssize_t
slot_sq_length(PyObject *self)
{
    PyObject* stack[1] = {self};
    PyObject *res = vectorcall_method(&amp;_Py_ID(__len__), stack, 1); // 호출
    Py_ssize_t len;

    if (res == NULL)
        return -1;

    Py_SETREF(res, _PyNumber_Index(res));
    if (res == NULL)
        return -1;

    assert(PyLong_Check(res));
    if (_PyLong_IsNegative((PyLongObject *)res)) {
        Py_DECREF(res);
        PyErr_SetString(PyExc_ValueError,
                        &quot;__len__() should return &gt;= 0&quot;);
        return -1;
    }

    len = PyNumber_AsSsize_t(res, PyExc_OverflowError);
    assert(len &gt;= 0 || PyErr_ExceptionMatches(PyExc_OverflowError));
    Py_DECREF(res);
    return len;
}</code></pre>
<p>본 책에서는 C 코드가 전혀 나오지 않지만.. 그냥 어떤 식으로 동작하는지 확인하고 싶어 필자가 혼자 살펴본 부분입니다.
다양한 Magic method는 <a href="https://rszalski.github.io/magicmethods/">여기</a>서 더 보실 수 있습니다.</p>
<p>이외로 <code>!r</code>  개념을 제대로 알고 있지 못 하여 정리하였습니다.
Python의 <code>!r</code> 변환 필드는 해당 객체의 <code>repr()</code> 형태로 변환하라는 의미입니다.</p>
<p><code>repr()</code>은 객체의 &quot;공식적인&quot; 문자열 표현을 반환하며, 가능하면 해당 객체를 다시 만들 수 있는 Python 코드 형태로 표현합니다. 즉 eval() 함수의 입력으로 사용하면 원본 객체와 동일한 값을 가진 객체를 생성할 수 있는 형태를 이야기합니다.</p>
<p>예시를 보면 이해가 쉽습니다.</p>
<pre><code class="language-python">text = &quot;Tab\\there&quot;
print(f&quot;{text}&quot;)      # Tab    here
print(f&quot;{text!r}&quot;)    # &#39;Tab\\there&#39;</code></pre>
<p>주로 디버깅이나 로깅할 때 객체의 정확한 값을 표시하고 싶을 때 유용합니다. 특히 문자열에 특수 문자가 포함되어 있을 때 실제 내용을 정확히 볼 수 있습니다. Langchain의 특정 클래스의 디버깅을 위해 <strong>repr()</strong>을 사용한 기억이 떠오르네요.</p>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[밑바닥부터 시작하는 딥러닝 - 딥러닝의 뼈대를 빠르게 알고 싶을 때]]></title>
            <link>https://velog.io/@l_cloud/%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-%EB%94%A5%EB%9F%AC%EB%8B%9D%EC%9D%98-%EB%BC%88%EB%8C%80%EB%A5%BC-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@l_cloud/%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-%EB%94%A5%EB%9F%AC%EB%8B%9D%EC%9D%98-%EB%BC%88%EB%8C%80%EB%A5%BC-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Tue, 24 Sep 2024 09:39:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=99518713">밑바닥부터 시작하는 딥러닝</a>를 읽게 된 이유와 감상에 대한 글입니다.</p>
</blockquote>
<h3 id="왜-읽었었을까">왜 읽었었을까?</h3>
<p>개발하며 서비스 측면에서나 개인적인 측면에서 AI를 많이 사용하고 있습니다. 발전 속도와 퍼지는 속도를 보며 AI(머신러닝? 딥러닝?)도 이제 CS의 한 축이 되지 않았나 생각하여 도커 책을 끝내고 학습을 시작하려 했습니다. 하지만 회사 내부 이슈로 Fine-tuning을 돕거나 직접 해야 하는 업무가 생겨 예정보다 빠르게 읽게 되었습니다. 애초에 책이 1~4권 시리즈가 있고, 목차를 보니 수학적으로 완전히 파고드는 스타일도 아니고 빠르게 딥러닝을 훑을 수 있을 듯하여 선택했습니다. <code>이 책을 보고 Fine-tuning을 해야지!</code>는 아니고 도대체 딥러닝이 그래서 뭔데? 에 대한 답을 기대하며 읽었습니다.</p>
<h3 id="이-책은-무엇을-이야기하는가">이 책은 무엇을 이야기하는가?</h3>
<p>퍼셉트론으로 시작해서 딥러닝까지 다루고 있습니다. Numpy를 가지고 퍼셉트론, Softmax, SGD, backpropagation, 신경망, CNN 등을 구현합니다. 범위를 보면 알 수 있듯, 수학적으로 엄밀하게 증명하거나 자세히 들어가지는 않습니다. 딥러닝에 필요한 뼈대를 잘 발라내서 이야기하고 있습니다. 김영한 강사님의 스프링 가본 편(무료 강의 혹은 1편)과 스타일이 유사합니다. 스프링이 왜 나오게 되었는지, 기존 방식과 무엇이 다르고 스프링의 핵심은 무엇인지를 강의에서 다루는데 이 책도 마찬가지입니다. 어떻게 딥러닝이 나오게 되었고 그 핵심 개념은 무엇인가를 빠르게 배울 수 있습니다.</p>
<h3 id="무엇을-배웠는가">무엇을 배웠는가?</h3>
<p>사실 학부 때 AI 수업을 들으며 신경망과 backpropagation까지는 배웠던 기억이 있습니다. 책을 읽으며 잊어버렸던 다시 기억을 되살리기도 했고, 다양한 매개변수 갱신 방법, 하이퍼 파라미터 초깃값, 드롭아웃 등을 새로 배웠습니다. CNN은 처음 접하는 내용이었기에 합성곱과 Pooling도 새로운 내용이었습니다. 다행히 신경망이라는 큰 틀은 변함없기에 생각보다는 쉽게 이해할 수 있었습니다. 코드와 설명을 보면서 이해는 했지만, 예제 없이 구현할 수 있을 정도로 학습하지는 않았습니다. 우선 빠르게 자연어 처리와 RNN까지는 학습하고 싶어서 큰 그림을 그리는 것에 초점을 두었습니다. 개인적으로 신경망이 어떻게 비선형성을 표현하는지가 정말 재미있었습니다. 이는 글 주제로 한 번 작성해 볼까 합니다.</p>
<h3 id="추천하는-독자는-누구인가">추천하는 독자는 누구인가?</h3>
<p>미적분과 선형 대수를 어느 정도 아는 독자가 좋을 것 같습니다. Numpy로 연산하는 것이 사실 다 행렬 연산이기에 선형 대수 개념이 없다면 이해하는데 다소 어려움을 겪을 듯합니다. 미적분은 고등학교 시절을 떠올리면 괜찮지 않을까 합니다. 개인적으로 이 책은 아주 기본서에 해당 한다고 생각하기에 완독 후&quot; 나는 딥러닝 알아!!&quot;라고 말하기는 어렵습니다. 마치 OS 공룡 책을 다 보고 &quot;나 OS 다 알아!&quot;라고 하는 것과 비슷합니다. 그래도 다른 딥러닝 기법을 배우기 위한 기초를 알려주는 책이니 AI를 살짝이라도 맛보고 싶은 사람들에게 적극 추천합니다! 1~2주 정도면 빠르게 다 읽을 수 있습니다!</p>
<p><a href="https://www.youtube.com/watch?v=XHfKCNkLfmg&amp;list=PLSN_PltQeOyjDGSghAf92VhdMBeaLZWR3">강력 추천 선형대수 강의</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Streamlit과 LangChain을 활용한 멀티 LLM 동시 스트리밍 - Python 동시성 실전 적용하기]]></title>
            <link>https://velog.io/@l_cloud/Streamlit%EC%9C%BC%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-stream%EC%B2%98%EB%A6%AC-%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@l_cloud/Streamlit%EC%9C%BC%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-stream%EC%B2%98%EB%A6%AC-%ED%95%98%EB%A9%B0</guid>
            <pubDate>Sun, 01 Sep 2024 06:29:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 Python의 동시성 개념과 이를 활용한 Streamlit에서의 멀티스레딩 구현에 대해 다룹니다. 동시성 개념, ThreadPoolExecutor의 사용법 등, Streamlit, Lanchain을 기본적으로 이해하고 있다는 전제로 작성되었습니다. 개념이 생소하다면 <a href="https://realpython.com/python-concurrency/">Python 동시성 개념 정리</a>, <a href="https://docs.streamlit.io/">Streamlit</a>, <a href="https://python.langchain.com/v0.2/docs/introduction/">Langchain</a>을 먼저 참고하시는 것을 권장합니다.</p>
</blockquote>
<h3 id="문제-상황">문제 상황</h3>
<p>같은 프롬프트에 대해 여러 LLM의 결과를 손쉽게 비교하고 싶었습니다. LLM API 호출은 Langhchain을 활용하여, 결과 비교는 GPT, Claude 등의 chat-UI와 유사한 UI를 손쉽게 만들어주는 Streamlit을 활용하기로 했습니다. 또한, 각 API를 순차적으로 처리하여 화면에 보여주는 것이 아니라 동시에 여러 API 호출하고 그 결과가 화면에 즉시 보이게 하고 싶었습니다. 그러나 아쉽게도 아쉽게도 Streamlit에서는 multi-write-stream을 제공하지 않습니다. 또한 Langchain에서도 batch stream 기능을 제공할 계획이 없다고 합니다. (<a href="https://github.com/langchain-ai/langchain/issues/19944">GitHub 이슈</a> 참고)</p>
<p>이를 해결하기 위해 Python의 동시성 처리 방법을 살펴보고, 이를 활용하여 문제를 해결해 보고자 합니다.</p>
<h3 id="python-동시성-살펴보기">Python 동시성 살펴보기</h3>
<p>동시성은 여러 작업을 동시에 처리하는 것처럼 보이게 하는 기법입니다. Python은 GIL(Global Interpreter Lock) 때문에 스레드를 통한 진정한 병렬 처리는 어렵지만, I/O 바운드 작업에서는 동시성을 통해 효율적인 처리가 가능합니다. Python은 multi-threading과 asyncio 등을 통해 동시성을 구현할 수 있으며, 본 글에서는 multi-threading을 중심으로 설명합니다.</p>
<p>우선, multi-threading의 기본 개념을 이해하기 위해 LangChain의 배치 작업 예시를 살펴보겠습니다. Langchain에서는 멀티스레딩을 사용해 여러 작업을 병렬로 처리합니다. 이 작업은 아래와 같이 <code>ThreadPoolExecutor</code>를 통해 이루어집니다.</p>
<pre><code class="language-python">with get_executor_for_config(configs[0]) as executor:
    # get_executor_for_config는 ThreadPoolExecutor을 상속 받아 구현한 객체입니다. 
    return cast(List[Output], list(executor.map(invoke, inputs, configs)))</code></pre>
<p>이 코드는 다소 복잡해 보일 수 있지만, 간단히 <code>submit</code>으로 바꿔보면 더 쉽게 이해할 수 있습니다.</p>
<pre><code class="language-python">with get_executor_for_config(configs[0]) as executor: 
    futures: List[Future] = [
        executor.submit(invoke, input, config)
        for input, config in zip(inputs, configs)
    ]


    results: List[Output] = []
    for future in futures:
        results.append(future.result())

    return cast(List[Output], results)</code></pre>
<p><code>submit</code> 함수는 스레드에 작업을 던져주고 현재 스레드는 결과를 기다리지 않고 다음 라인 코드로 넘어갑니다. 그렇다면 우리는 어떻게 해당 스레드의 작업 상태와 결과를 확인할 수 있을까요? 바로 <code>submit</code>이 반환하는 Future 객체를 통해 가능합니다.</p>
<p>Future 객체는 스레드의 상태, 결과 등을 기억하고 있는 객체입니다.</p>
<pre><code class="language-python">class Future(object):
    def __init__(self):
        self._condition = threading.Condition()
        self._state = PENDING
        self._result = None
        self._exception = None
        self._waiters = []
        self._done_callbacks = []</code></pre>
<p>이를 통해 비동기 작업의 상태, 완료 여부, 결과 등을 확인할 수 있으며, 콜백 등록도 가능합니다.</p>
<p>정리하자면, ThreadPoolExecutor는 작업마다 Future 객체를 생성해 <code>_worker</code> 함수를 통해 비동기로 작업을 처리해 여러 작업을 동시에 효율적으로 처리할 수 있습니다. (자세한 사항은 Python의 <a href="">ThreadPoolExecutor 코드</a>를 참고하세요. 또한 python thread는  <a href="https://github.com/python/cpython/blob/main/Python/thread.c">C 언어 네이티브 스레드 라이브러리</a>를 통해 생성이 됩니다. 또한 Thread를 직접 생성할 경우 Future객체는 생성되지 않습니다.)</p>
<h3 id="streamlit에서-여러-llm-동시-스트리밍하기">Streamlit에서 여러 LLM 동시 스트리밍하기</h3>
<p>이제 이 지식을 활용하여 Streamlit에서 여러 LLM을 동시에 호출하고 결과를 실시간으로 스트리밍해 보겠습니다. Thread를 직접 생성하고 관리하는 것은 복잡할 수 있으므로, Python의 <code>ThreadPoolExecutor</code>를 사용하였습니다. 그렇다면 아래 코드를 실행하면 여러 LLM 호출과 결과가 화면에 stream으로 표시되지 않을까요?</p>
<pre><code class="language-python">def v1(llm : BaseChatModel, message:List[BaseMessage]):
    with st.chat_message(&quot;assistant&quot;):
        response = st.write_stream(llm.stream(message))
        return response
with ThreadPoolExecutor(max_workers=2) as executor:
    results = [executor.submit(tempt, llm, message) for llm, message in zip(llms, messages)]</code></pre>
<p>하지만 위 코드를 실행하면 <code>&#39;ThreadPoolExecutor-2_0&#39;: missing ScriptRunContext</code>와 같은 경고와 화면에 아무 것도 출력되지 않는 것을 알 수 있습니다. 왜 그럴까요?
ScriptRunContext는 Streamlit에서는 화면의 어느 부분과 어느 위치에 출력해야 하는지를 지정해 주는 정보를 가지고 있는 객체입니다. 아래 코드를 보면 각 스레드는 자신만의 ScriptRunContext를 가지고 있어, 자신이 어느 화면, 어떤 위치에 데이터를 출력해야 하는지 알고 있습니다. </p>
<pre><code class="language-python">def get_script_run_ctx(suppress_warning: bool = False) -&gt; ScriptRunContext | None:
    &quot;&quot;&quot;
    Parameters
    ----------
    suppress_warning : bool
        If True, don&#39;t log a warning if there&#39;s no ScriptRunContext.
    Returns
    -------
    ScriptRunContext | None
        The current thread&#39;s ScriptRunContext, or None if it doesn&#39;t have one.

    &quot;&quot;&quot;
    thread = threading.current_thread()
    ctx: ScriptRunContext | None = getattr(thread, SCRIPT_RUN_CONTEXT_ATTR_NAME, None)</code></pre>
<p><code>v1</code>코드는 멀티 스레드를 사용하고, 각 스레드는 <code>ScriptRunContext</code>가 메인 스레드와 다르거나 <code>None</code>인 것을 추측할 수 있습니다. 그렇기 때문에 각 스레드는 어느 화면의 어떤 위치에 표시해야 하는지를 모르는 상태입니다. </p>
<p>따라서, 여러 스레드가 동시에 화면 정보를 공유하면서 작업을 수행하려면, 각 스레드에 메인 스레드의 <code>ScriptRunContext</code> 객체를 전달해 주어야 합니다. 이를 구현해 봅시다.</p>
<pre><code class="language-python">def v2(context : ScriptRunContext, llm : BaseChatModel, message:List[BaseMessage]):
    add_script_run_ctx(ctx=context)
    with st.chat_message(&quot;assistant&quot;):
        response = st.write_stream(llm.stream(message))
        return response

context = get_script_run_ctx()
with ThreadPoolExecutor(max_workers=2) as executor:
    results = [executor.submit(tempt, context, llm, message) for llm, message in zip(llms, messages)]</code></pre>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/b270b0c5-1414-4b12-b7f1-6e1ba73fddc0/image.gif" alt=""></p>
<p>짠! 이렇게 간단하게 멀티스레딩을 활용해 LLM 모델의 호출을 동시에 스트림으로 구현하였습니다. 물론 각 LLM 모델 응답을 저장하고, 다음 호출에 이전 대화 기록을 함께 보내야 하는 작업 등의 다양한 작업이 남아 있습니다.</p>
<h3 id="결론">결론</h3>
<p>본 글에서는 Python의 동시성을 활용해 Streamlit에서 여러 LLM을 동시에 호출하고 결과를 실시간으로 스트리밍하는 방법을 살펴보았습니다. 동시성 개념을 잘 이해하고 이를 응용하면 다양한 상황에서 더욱 효율적인 코드를 작성할 수 있습니다. 사실 Streamlit의 write_stream과 LangChain의 stream 기능은 Python의 Generator와 깊은 관련이 있습니다. 다음 글에서는 Generator의 개념을 확장한 Python의 asyncio를 활용한 비동기 프로그래밍에 대해 알아보겠습니다. 또한, 본 글에서 다루지 않았던 동시성을 구현 할 때의 &#39;주의점&#39;도 함께 살펴볼 예정입니다.
긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[처음 시작하는 FastAPI - FastApI를 빠르게 훑고 싶을 때 ]]></title>
            <link>https://velog.io/@l_cloud/%EC%B2%98%EC%9D%8C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-FastAPI-FastApI%EB%A5%BC-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%ED%9B%91%EA%B3%A0-%EC%8B%B6%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@l_cloud/%EC%B2%98%EC%9D%8C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-FastAPI-FastApI%EB%A5%BC-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%ED%9B%91%EA%B3%A0-%EC%8B%B6%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Tue, 23 Jul 2024 13:01:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.aladin.co.kr/m/mproduct.aspx?ItemId=341993289">처음 시작하는 FastAPI</a>를 읽게 된 이유와 감상에 대한 글입니다.</p>
</blockquote>
<h3 id="왜-읽었었을까">왜 읽었었을까?</h3>
<p>FastAPI는 인턴 시절 사용해 본 경험이 있었습니다. 하지만 이미 잘 구축된 API 서버이었기에 저는 주로 기존 코드를 참고하여 새로운 기능 추가, 테스트 코드 작성, 오류 수정을 하며 시스템을 처음부터 구축할 경험은 없었습니다. 회사에서 FastAPI를 사용하여 처음부터 시스템을 구축해야 할 일이 생겨 전체 기능을 훑기 위해 읽어보았습니다.</p>
<h3 id="이-책은-무엇을-이야기하는가">이 책은 무엇을 이야기하는가?</h3>
<p>책은 총 18장으로 이루어져 있습니다. 현대 웹과 계층을 시작으로 운영, 부하 테스트까지 넓은 범위를 다루고 있습니다. 상당히 넓은 범위를 얇은 책에서 이야기하므로 구체적인 내용을 다루지는 못합니다. 하지만 큰 흐름과 더 알아보면 좋을 것을 빠르게 파악할 수 있습니다. 예를 들어서 FastAPI와 떨어질 수 없는 Pydantic, 동기/비동기, WSGI, ASGI, Starlette 등이 무엇인지 추상적으로라도 알 수 있고, 파이썬 진영에서 사용하는 DB 드라이버, 지표 도구, 데이터 시각화 도구 등이 무엇이 있는지 소개해 줘서 검색을 용이하게 합니다.</p>
<h3 id="무엇을-배웠는가">무엇을 배웠는가?</h3>
<p>우선 FastAPI에서 어떻게 의존성을 관리하는지를 배웠습니다. 특히 <code>의존성을 함수의 인자로 정의할 수 있고, 정의된 의존성은 FastAPI에 의해 ‘자동’으로 호출되고, 호출 결과로 반환되는 ‘값’을 인자에 전달</code>하는 방식과 코드가 흥미로웠습니다. 자잘하게는 FastAPI의 응답 유형이나 보통 계층을 어떻게 나누는지 등을 배웠습니다. (<a href="https://faker.readthedocs.io/en/master/">Faker</a> 라는 가짜 데이터를 생성하는 모듈을 이 책에서 처음 접했는데 재미있었습니다.)</p>
<h3 id="추천하는-독자는-누구인가">추천하는 독자는 누구인가?</h3>
<p>FastAPI 처음 접하고, 이미 다른 웹 프레임워크를 가볍게 사용해 보았거나 웹에 대해 사전 지식이 있는 분에게 추천하고 싶습니다. 웹이나 파이썬을 이 책으로 배우기에는 너무 생략된 내용이 많아 온전한 이해가 힘들 수 있습니다. 또한 이 책으로 FastAPI 전체를 배우기는 힘듭니다. 일례로 Middle ware, Background Tasks 등을 다루지 않습니다. 물론 이는 공식 문서에 잘 나와 있습니다.</p>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth2.0 != Social login]]></title>
            <link>https://velog.io/@l_cloud/OAuth2.0-Social-login</link>
            <guid>https://velog.io/@l_cloud/OAuth2.0-Social-login</guid>
            <pubDate>Wed, 10 Jul 2024 14:24:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 OAuth2.0에 대해 설명하며, 인증, 인가, JWT를 배경지식으로 요구합니다. 또한 OAuth2.0을 들어 보았고 관련 글이나 영상을 본 사람을 대상으로 합니다. 실습이나 예제는 없습니다.</p>
</blockquote>
<p>인증, 인가, 로그인 등을 검색하면 소셜 로그인이라는 키워드와 함께 OAuth2.0을 자주 마주친다. 여태까지 <code>OAuth2.0 == 소셜 로그인</code>으로 알고 있었다. 하지만 소셜 로그인을 위해 OAuth2.0를 사용할 수 있는 것이지 <code>OAuth2.0 == 소셜 로그인</code>이 아니다. 소셜 로그인과 상관없이 OAuth2.0을 사용할 수 있다. </p>
<h2 id="oauth20이란">OAuth2.0이란?</h2>
<p><a href="https://auth0.com/intro-to-iam/what-is-oauth-2">Okta</a>에 의하면 OAuth2.0은 인가 프로토콜이며, 인증 프로토콜이 아니다.
인가를 위해 액세스 토큰을 사용하며, 그 중 JWT가 많이 사용된다.
즉 OAuth2.0은 인가를 위해 사용하는 것이며, 하나의 예시가 소셜 로그인일 뿐 다른 방식으로 다양하게 사용할 수 있다.</p>
<h2 id="oauth20의-구성-요소">OAuth2.0의 구성 요소</h2>
<p>OAuth2.0의 동작 방식을 알기 전에 알아야 할 사전 지식이 있다. 아래는 OAuth2.0 맥락 아래 사용하는 용어들이다.</p>
<ul>
<li><p>리소스 소유자(Resource Owner): 보호된 리소스를 소유하고 해당 리소스에 대한 접근 권한을 가지는 사용자 또는 시스템. 사용자라고 생각하면 편하다.</p>
</li>
<li><p>클라이언트(Client): OAuth 2.0을 사용하여 보호된 리소스에 접근하는 시스템. 리소스 소유자가 직접 상호작용을 하는 애플리케이션이다. 웹 애플리케이션, 모바일 앱 또는 백엔드 서버가 그 예시로 리소스 서버에 접근하기 위해 액세스 토큰을 사용한다.</p>
</li>
<li><p>인가 서버(Authorization Server): 클라이언트로부터 액세스 토큰 요청을 받고, 리소스 소유자의 동의를 거친 후 토큰을 발급하는 서버. 이 과정에서 리소스 소유자의 인증은 별도로 이루어지며, 인가 서버는 이미 인증된 사용자에 대해 인가를 수행한다. 예를 들어, 사용자가 카카오 로그인을 통해 다른 사이트에 회원 가입을 하거나 사용하는 경우, 인증은 카카오 로그인을 통해 이루어지고, 인가는 카카오 인가 서버에서 처리된다.</p>
</li>
<li><p>리소스 서버(Resource Server): 실제 리소스가 있고 클라이언트로부터 액세스 요청을 받는 서버이다. 소셜 로그인으로 생각해보면 구글 메일, 구글 캘린더 등 리소스가 있는 서버를 예시로 볼 수 있다.</p>
</li>
</ul>
<h2 id="oauth20-grant-type-인가-유형">OAuth2.0 Grant Type (인가 유형)</h2>
<p>OAuth2.0은 다양한 방식을 사용하여 리소스 서버에 대한 접근 권한을 부여하고 관리할 수 있다. 이 중 <code>Authorization Code Grant</code>와 <code>Client Credentials Grant</code> 그리고 <code>Resource owner password credentials Grant</code>를 알아보자. 더 다양한 인가 유형은 맨 마지막 출처를 보면 확인할 수 있다.</p>
<h3 id="authorization-code-flow">Authorization code flow</h3>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/9e677492-8f3d-45bc-bce6-9df58e6eded2/image.png" alt=""> </p>
<p>사진을 처음 보면 조금 복잡하지만, 구글이 아닌 사이트(Client 이하 A 서버)에서 구글 로그인을 하는 상황을 떠올리면서 보면 이해가 쉽다.</p>
<ol>
<li><p>Enter URL : 유저가 A에 구글 로그인을 요청한다.</p>
</li>
<li><p>Open URL : 브라우저가 해당 요청 URL을 서버로 보낸다.</p>
</li>
<li><p>Redirect to AuthZ Server : A 서버는 AuthZ URL로 redirect 응답을 한다. A 서버로 나의 구글 아디이와 비밀번호가 넘어가는 것은 너무 위험하니 A서버가 아닌 구글 서버에서 로그인 처리하려는 목적이다.</p>
</li>
<li><p>Opens redicrect URL : A 서버에게 받은 redirect URL로 다시 요청을 보낸다.</p>
</li>
<li><p>Present Authorization UI: 브라우저는 AuthorZ 서버에서 제공하는 로그인 UI를 사용자에게 보여준다.</p>
</li>
<li><p>Present credentials and authorize or deny: 사용자가 자격 증명(아이디와 비밀번호)을 입력하고 로그인 여부를 선택한다.</p>
</li>
<li><p>Present submitted data from user: 브라우저가 사용자가 입력한 데이터를 A 서버가 아닌 AuthZ 서버에 직접 제출한다.</p>
</li>
<li><p>Verify and create Authorization code: AuthZ 서버는 사용자의 자격 증명을 확인하고 증명이 되는 경우 Authorization code를 생성한다.</p>
</li>
<li><p>Redirect to Web Server with Authorization Code: AuthZ 서버는 Authorization Code와 redirect URL(A 서버의 URL)을 브라우저에 제공한다.</p>
</li>
<li><p>Follow redirect to Web Server: 브라우저는 Authorization Code를 포함한 URL로 A 서버에 다시 요청을 보낸다.</p>
</li>
<li><p>Present Authorization Code: A 서버는 받은 Authorization Code를 AuthZ 서버에 제출한다.</p>
</li>
<li><p>Return Access Token: AuthZ 서버는 Authorization Code를 확인하고 유효하면 Access Token을 A 서버에 반환한다.</p>
</li>
<li><p>Call protected resource with Access Token: A 서버는 받은 Access Token을 이용하여 Resource 서버에 보호된 자원을 요청한다.</p>
</li>
<li><p>Return protected resource: Resource 서버는 Access Token을 확인하고 유효하면 요청된 자원을 A 서버에 반환한다.</p>
</li>
</ol>
<p>브라우저가 7번 순서인 <code>Present submitted data from user</code> 을 처리하는 url 예시</p>
<pre><code>curl -X GET https://authorization-server.example.com/oauth/authorize
              ?response_type=code
              &amp;client_id=your_client_id
              &amp;redirect_uri=A_서버_redirect_uri
              &amp;state=your_state
              &amp;scope=your_scope</code></pre><p>A 서버가 12번 순서인 <code>Return Access Token</code>을 처리하는 url 예시</p>
<pre><code>curl -X POST &quot;https://authorization-server.example.com/token&quot; \
-d &quot;grant_type=authorization_code&quot; \
-d &quot;code=your_authorization_code&quot; \
-d &quot;client_id=your_client_id&quot; \
-d &quot;client_secret=your_client_secret&quot; \
-d &quot;redirect_uri=your_redirect_uri&quot;</code></pre><h3 id="client-credentials-grant-flow">Client credentials grant flow</h3>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/e8a5f16f-885d-4365-9e06-79f667ebd22f/image.png" alt=""></p>
<p>유저 개입 없이 클라이언트의 자격 증명을 사용하여 리소스에 접근할 하는 방식이다. 상황 예시로는 서버대 서버간 통신을 하거나 주기적인 작업 혹은 자동화 작업이 필요할 때 등이 있다. </p>
<ol>
<li><p>Client credentials: 클라이언트(예: 서버)는 AuthorZ 서버로 클라이언트 자격 증명을 전송한다. </p>
</li>
<li><p>Authenticate Client: AuthorZ 서버는 클라이언트 자격 증명을 확인하여 클라이언트를 인증하고 자격 증명이 올바르면 클라이언트에게 Access Token을 발급한다.</p>
</li>
<li><p>Access token with NO refresh token: AuthorZ 서버는 Access Token을 Refresh Token없이 클라이언트에게 반환한다. </p>
</li>
<li><p>Access protected resource with access token: 클라이언트는 받은 Access Token을 사용하여 Resource 서버에 자원을 요청한다.</p>
</li>
<li><p>Protected resource response: Resource 서버는 Access Token을 확인하고 유효하다면 보호된 자원을 클라이언트에게 반환한다.</p>
</li>
</ol>
<h3 id="resource-owner-password-credentials-flow">Resource owner password credentials flow</h3>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/75a4ba37-0918-4c45-b646-b3d43bea62c4/image.png" alt=""></p>
<p>유저의 자격 증명(아이디, 비밀번호 등)을 사용하여 클라이언트가 토큰을 발급 받고 리소스에 접근하는 방식이다. 로그인을 직접 구현하는 상황을 생각하면 된다. 크게 추천할 만한 방식은 아니지만 Client, AuthorZ 서버, Resource 서버를 구분 안 하고 하나의 백엔드에서 구현해도 되기는 한다.</p>
<ol>
<li><p>Resource Owner&#39;s credentials: 유저가 A 서버에 자신의 자격 증명(아이디와 비밀번호)을 전송한다. </p>
</li>
<li><p>Resource Owner&#39;s credentials: A 서버는 유저의 자격 증명을 AuthorZ 서버로 전송한다.</p>
</li>
<li><p>Authenticate Resource Owner: AuthorZ 서버는 유저의 자격 증명을 확인한다.</p>
</li>
<li><p>Authenticate Client: 3번 과정과 함께 AuthorZ 서버는 A 서버의 자격 증명도 확인한다. (보안상 A 서버가 아닌 제 3의 서버에서 요청을 막기 위해)</p>
</li>
<li><p>Access token with optional refresh token: 인증이 성공하면 AuthorZ 서버는 Access Token을 A 서버에 반환한다. 이때 Refresh Token도 선택적으로 포함될 수 있다.</p>
</li>
<li><p>Access protected resource with access token: A 서버는 받은 Access Token을 사용하여 Resource 서버에 자원을 요청한다.</p>
</li>
<li><p>Protected resource response: Resource 서버는 Access Token을 확인하고 유효하다면 자원을 A서버에 반환한다.</p>
</li>
</ol>
<p> Client, AuthorZ 서버, Resource 서버를 통합해 로그인을 구현한 추상적인 <code>Fastapi</code>예시.</p>
<pre><code class="language-python">

@app.post(&quot;/token&quot;, response_model=Token)
async def login_for_access_token(
    username: str = Form(), password: str = Form()
):
    try:
        user = authenticate_user(username, password) #인증
    except AuthorizationError:
        raise AuthorizationError
    login_token = make_login_token(user)
    return login_token #인가 토큰 발급

# get_current_user 인가 토큰 검증 함수

@app.get(&quot;/resource&quot;, response_model=Resource) #인가 토큰으로 리소스 접근
async def read_resource(current_user: User = Depends(get_current_user)): 
    if current_user is None:
        raise TokenError
    resource = get_resource(current_user)
    return resource #리소스 반납</code></pre>
<h3 id="마무리">마무리</h3>
<p>OAuth2.0을 직접 사용하거나 사용한 코드를 본 적이 없었다. <code>OAuth2.0에 관해 설명해 주세요</code>라는 기술 인터뷰 예상 질문을 어디선가 보고 유투브 영상과 블로그 글 몇 개를 읽으며 <code>OAuth2.0 == 소셜 로그인</code> 이며 <code>OAuth2.0</code>을 알고 있다고 착각했었다. 최근 프로젝트에서 <code>OAuth2.0</code>을 사용하는데 <code>OAuth2.0 == 소셜 로그인</code>이라는 관점에서만 코드를 보니 전혀 이해가 안 되어서 다시 공부하는 계기가 되었다. 처음부터 제대로 공부하는 사람이 되어야지...</p>
<p>긴 글 읽어주셔서 감사합니다 :)</p>
<p><a href="https://auth0.com/intro-to-iam/what-is-oauth-2">출처1</a>
<a href="https://guide.ncloud-docs.com/docs/b2bpls-oauth2">출처2</a>
<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/oauth_guide/content/oauth_flows.html">출처3</a>
<a href="https://stackoverflow.com/questions/38268175/is-it-possible-to-use-oauth-2-0-without-a-redirect-server">출처4</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 시스템 개발 스킬업 - 클라우드 도입이 필요할 때]]></title>
            <link>https://velog.io/@l_cloud/AWS-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C-%EC%8A%A4%ED%82%AC%EC%97%85-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%8F%84%EC%9E%85%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%A0-%EB%95%8C</link>
            <guid>https://velog.io/@l_cloud/AWS-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C-%EC%8A%A4%ED%82%AC%EC%97%85-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%8F%84%EC%9E%85%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%A0-%EB%95%8C</guid>
            <pubDate>Tue, 25 Jun 2024 13:13:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://m.yes24.com/Goods/Detail/126791190">AWS 시스템 개발 스킬업</a>를 읽게 된 이유와 감상에 대한 글입니다.</p>
</blockquote>
<h3 id="왜-읽었었을까">왜 읽었었을까?</h3>
<p>기존에 하던 업무는 로컬 혹은 EC2 Notebook 환경에서 데이터를 처리하여 다시 DB에 저장하는 일이었다. batch 작업으로 이루어지고, 아직은 정기적인 작업이 아니기에 운영 환경이 필요 없었다. 하지만 다른 프로젝트에서 운영과 배포 업무를 할당받아 필요성을 느꼈다. AWS를 사용하는 클라우드 환경에서의 운영이어서 해당 책을 선택했다. 너무 기초적이지도 않고, 특정 방법만 알려주는 책이 아니라는 느낌을 목차와 책 소개에서 받았다.</p>
<h3 id="이-책은-무엇을-이야기하는가">이 책은 무엇을 이야기하는가?</h3>
<p>일본인 저자들이 집필한 책 중 종종 <code>근본</code>부터 시작하는 것들이 있다. (RDB를 집합론 이야기를 시작하는 <a href="https://m.yes24.com/Goods/Detail/29343536">책</a>처럼) 이 책도 그렇게 시작한다. 클라우드 시스템에 관한 근본적인 사고방식으로 시작해 구체적인 구현을 풀어낸다. 온프레미스 환경과 비교하며 설명하며 비교되는 특징, 신경 써야 할 것, 관점 등을 설명한다. 클라우드가 항상 답이라는 결론에 도달하지 않아서 좋다.</p>
<h3 id="무엇을-배웠는가">무엇을 배웠는가?</h3>
<p>사실 이전에 AWS IAM 계정을 왜 사용하는지 정확히 이해를 못 했었다. 학부 프로젝트를 할 때 수업을 따라 하며 IAM 계정을 생성해서 팀원들과 분배하거나, 그냥 root 계정을 그대로 사용했었다. 단순히 <code>보안</code> 때문에 root 계정을 사용하지 않는 것으로 생각했었는데, 이 책에서 그 이유뿐만이 아님을 배웠다. 온프레미스에서도 계정 관리가 필요하고, 이것이 역할과 책임을 이야기한다는 점에서 객체지향이 떠올랐다. 이외에도 배포 방식, RTO/RPO에 따른 장애 대응과 설계 등을 배웠다.</p>
<h3 id="추천하는-독자는-누구인가">추천하는 독자는 누구인가?</h3>
<p> 어떠한 서비스를 EC2로 배포해 보는 이야기가 아니라 온프레미스와 클라우드의 차이점은 무엇이며, 계정과 예산, 시스템 등을 어떻게 관리할 것인가를 궁금해하는 초보 개발자가 추천 대상이다. 물론 EC2, CIDR, VPC 등 기초적인 AWS 용어는 알고 있어야 한다. 어려운 책은 아니기 때문에 주말에 시간을 내어 하루면 다 읽을 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오브젝트 - 요구 사항이 명확하지 않을 때]]></title>
            <link>https://velog.io/@l_cloud/%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%EC%9A%94%EA%B5%AC-%EC%82%AC%ED%95%AD%EC%9D%B4-%EB%AA%85%ED%99%95%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@l_cloud/%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%EC%9A%94%EA%B5%AC-%EC%82%AC%ED%95%AD%EC%9D%B4-%EB%AA%85%ED%99%95%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Wed, 22 May 2024 14:47:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://product.kyobobook.co.kr/detail/S000001766367">오브젝트</a>를 읽게 된 이유와 감상에 대한 글입니다. 책의 각 장을 요약해 둔 블로그가 많을 것 같기에, 내용보다는 제가 얻은 점을 중점으로 작성하였습니다.</p>
</blockquote>
<h3 id="왜-객체지향을-공부하게-되었나">왜 객체지향을 공부하게 되었나?</h3>
<p>코드 작성과 설계를 검색하다 보면 객체지향, 함수형과 같은 단어들을 마주친다. 모두 확장과 변경이 쉬운 코드를 작성하고 설계하라는 이야기를 하며 이는 너무 당연해 보인다. 마치 <code>착하게 살자</code>라는 표어 같다. 쓰레기를 줍는 것, 남을 배려하는 것 등 착하게 사는 방법을 배우는 것처럼, 확장과 변경이 쉬운 코드를 위해 SOLID 원칙, 추상화, 다형성 등을 배운다. 나 혼자만 사용하는 코드를 설계하거나, 작성된 코드에 기능을 추가하거나 수정하는 등의 업무만 할 때까지 나는 괜찮은 코드를 작성하는 줄 알았다.</p>
<p>현 회사는 설립이 3년이 안 되었다. 입사 초기 나에게 할당된 요구 사항이 인턴을 하던 회사에 비해 상당히 추상적이었다. 확고한 요구 사항이 없다는 것이 신입 입장에서는 꽤 어려웠고 정확히 어떤 기능을 개발해야 하는지 갈피를 못 잡았다. 진행 상황을 발표하던 자리에서 팀장님이 요구가 명확하지 않을 때 개발자는 어떠한 코드를 작성해야 하는지를 약간 설명해 주셨다. 조금은 정석으로 객체지향을 공부하고 싶어 여러 책을 구매했고, 유명한 <a href="https://product.kyobobook.co.kr/detail/S000001766367">오브젝트</a>로 첫 시작을 하였다.</p>
<p>크게 이 책은 객체지향에서의 설계 원칙, 상속과 합성, 역할과 책임, 협력에 대한 개념과 접근 방식을 다룬다.
책에서 적절한 사례와 코드로 위 내용을 설명한다. 사례도 좋았고 읽으며 내 경험 두 가지가 떠올랐다.</p>
<ol>
<li><p>책 초반 <code>요구 사항은 항상 변하며 개발을 시작하는 시점에 구현에 필요한 모든 요구사항을 수집하는 것은 불가능에 가깝다</code>라는 내용이 나온다. 이를 읽으며 입사 초기 나에게 할당된 일이 왜 추상적으로 느껴지고 나를 힘들게 했는지 깨달았다. 나는 변하지 않는 요구 사항을 원하고 있었다! 수정과 확장성이 필요 없는 코드만 작성해 왔기 때문에 유연한 코드를 작성해야 하는 것이 어렵게 느껴졌다고 생각한다. (당시에는 유연한 코드를 작성해야 하는 것 자체도 사실 생각을 못 했다)</p>
</li>
<li><p>&#39;책임&#39;에 대한 이야기는 반복해서 나온다. 읽으며 문득 LLM Ouput이 Json schema에 일치하지 않으면 schema에 맞게 output을 수정하거나 LLM이 고치는 로직을 개발한 것이 떠올랐다. 처음 개발하였을 때는 prompt, model call, parsing, retry를 하나의 클래스에서 담당하고 있었다. 지금 생각하면 어이가 없지만 당시 재시작을 담당하는 책임만 지고 있다고 생각했다. 당연히 사용성과 확장성이 좋지 않았고 Langchain의 LCEL 문법과도 맞지 않아서 다시 개발했다. 두 번째로 개발할 때는 Langchain의 코드를 조금 더 자세히 살펴보며 클래스를 어떻게, 왜 나누었는지 생각해 보았다. 그들이 생각한 책임의 범위를 최대한 따르며 개발하려고 했다. 방법으로는 적절한 상속과 합성을 둘 다 사용했다. 완벽하다고 할 수는 없지만, 책임도 나름 분리되었고 LCEL 문법과도 호환이 되어 훨씬 사용성이 높아졌다.</p>
</li>
</ol>
<h3 id="느낀-점">느낀 점</h3>
<p>현실 세상은 상당히 복잡하다. 코드 세상도 마찬가지이다. 거짓말이 항상 악이 아닌 것처럼, 의존성도 항상 악이 아니다. 오브젝트를 통해 언제 거짓말이 악이 될 수 있는지, 어떻게 하면 거짓말하는 상황을 피할 수 있는지 혹은 언제 거짓말을 해도 되는지와 같은 것을 배울 수 있다. 당연히 모든 것을 관통하는 법칙이나 방법은 없다. 객체지향에 입문하기 좋은 책이고 다른 책과 다양한 관점도 꾸준히 공부 해야겠다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ptyhon 3.11 - New Feature : ExceptionGroup & TaskGroup]]></title>
            <link>https://velog.io/@l_cloud/Ptyhon-3.11-New-Feature-ExceptionGroup-TaskGroup</link>
            <guid>https://velog.io/@l_cloud/Ptyhon-3.11-New-Feature-ExceptionGroup-TaskGroup</guid>
            <pubDate>Fri, 01 Mar 2024 14:27:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 Python 3.11에 새로 소개된 ExceptionGroup &amp; TaskGroup에 대해 소개합니다. 
Exception, asyncio에 사전 지식이 있는 독자를 대상으로 합니다.</p>
</blockquote>
<h2 id="exceptiongroup">ExceptionGroup</h2>
<p>Python 3.11에서는 여러 예외들을 하나의 그룹으로 묶어서 동시에 처리할 수 있게 해주는 &#39;ExceptionGroup&#39;이라는 새로운 기능을 도입하였습니다. 이를 어떻게, 언제 사용할 수 있는지 살펴봅시다.</p>
<p>Python은 원칙적으로 한 번에 하나의 예외만을 처리할 수 있습니다. 그러나 발생한 여러 오류를 한꺼번에 처리하는 것이 더 유리한 경우도 있습니다. 멀티 프로세싱이나 asyncio 그룹 작업이 대표적인 예입니다. <a href="https://peps.python.org/pep-0654/">PEP-654</a>에서는 이와 관련하여 더 다양한 사례를 찾아볼 수 있습니다.</p>
<p>예외 그룹은 Exception을 상속받는 클래스이기 때문에, 아래와 같이 사용할 수 있습니다. </p>
<pre><code class="language-python">try:
    raise ExceptionGroup(&quot;group&quot;, [ValueError(654)])
except ExceptionGroup:
    print(&quot;Handling ExceptionGroup&quot;)</code></pre>
<p>여러 에러를 명확히 구분하여 처리하는 것이 좋습니다. 아래 예제처럼 여러 에러를 ExceptionGroup으로 묶어 명확하게 처리할 수 있습니다.</p>
<pre><code class="language-python">try:
    raise ExceptionGroup(
        &quot;group&quot;, [TypeError(&quot;str&quot;), ValueError(654), TypeError(&quot;int&quot;)]
    )
except* ValueError as eg:
    print(f&quot;Handling ValueError: {eg.exceptions}&quot;)
except* TypeError as eg:
    print(f&quot;Handling TypeError: {eg.exceptions}&quot;)</code></pre>
<p>다만, <code>except*</code> 구문을 사용하여 모든 오류를 처리해야 합니다. 아래 예제에서는 ValueError만 처리하고 나머지 TypeError는 처리되지 않아 예외가 발생합니다.</p>
<pre><code class="language-python">try:
    raise ExceptionGroup(
        &quot;group&quot;, [TypeError(&quot;str&quot;), ValueError(654), TypeError(&quot;int&quot;)] 
        # 개인적으로 위 코드가 너무 인위적이라서 실제로 이런 에러가 발생할 예시가 와닿지 않았는데
        # 다음 문단에서 예시가 나옵니다!
    )
except* ValueError as eg:
    print(f&quot;Handling ValueError: {eg.exceptions}&quot;)</code></pre>
<pre><code class="language-python"> | ExceptionGroup: group (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | TypeError: str
    +---------------- 2 ----------------
    | TypeError: int
    +------------------------------------</code></pre>
<p>ExceptionGroup은 기존의 예외 처리를 대체하는 것이 아니라, 여러 예외를 동시에 처리할 필요가 있을 때 유용합니다.</p>
<p>실제로 Python 사용자가 ExceptionGroup을 사용할 일은 많지 않을 수 있습니다. 그러나 Python 3.11이 널리 사용됨에 따라, 의존하는 패키지에서 예외 그룹을 발생시킬 수 있으므로, 애플리케이션에서 이를 처리할 필요가 있을 수 있기에 알아두면 좋을 듯 합니다 :)</p>
<h2 id="taskgroup">TaskGroup</h2>
<p>TaskGroup은비동기 작업을 동시에 실행하고 관리하는 데 사용되는 새로운 기능입니다. 물론 이전에도 asyncio.gather이라는 기능이 있었죠. asyncio.gather과의 주요 차이점을 살펴봅시다.</p>
<ul>
<li><p><strong>에러 핸들링</strong> : <strong><code>TaskGroup</code></strong>은 작업 중 하나라도 실패하면 즉시 다른 모든 작업을 취소합니다. 이는 <strong><code>asyncio.gather</code></strong>와 비교하여 에러를 더 일찍 포착하고, 불필요한 작업 실행을 방지할 수 있게 해줍니다. <strong><code>gather</code></strong>를 사용할 때는 <strong><code>return_exceptions</code></strong> 파라미터를 <strong><code>True</code></strong>로 설정하지 않는 이상, 모든 작업이 완료된 후에야 예외를 던집니다. 또한 TaskGroup은 Exeption Group으로 에러를 관리합니다. </p>
</li>
<li><p><strong>자동 취소</strong> : <strong><code>TaskGroup</code></strong> 내에서 실행되는 모든 작업은 <strong><code>TaskGroup</code></strong>이 종료될 때 자동으로 취소됩니다. 이는 추가적인 취소 로직을 작성할 필요없이, 코드를 더 간결하고 안전하게 만듭니다.</p>
</li>
</ul>
<p>-
<strong>동적 작업 추가</strong> : <strong><code>TaskGroup</code></strong>을 사용하면 그룹 실행 중에도 새로운 작업을 동적으로 추가할 수 있습니다. 이는 <strong><code>asyncio.gather</code></strong>에서는 불가능한데, <strong><code>gather</code></strong>는 호출 시점에 모든 작업을 알고 있어야 합니다.</p>
<ul>
<li><strong>손쉬운 관리</strong> : <strong>TaskGroup</strong>을 사용하면 with 문 내에서 발생하는 모든 작업의 예외를 처리하고 적절히 집계하고, 작업 그룹의 생명 주기를 명확하게 관리할 수 있습니다. </li>
</ul>
<p>gather은 비동기 작업을 개별 작업을 묶어서 처리하고 각각 영향을 안 받지만 TaskGroup은 여러 작업이 묶여서 하나의 작업이 되는 느낌입니다. <del>(연대 책임)</del> 여기서 그럼 ExceptionGroup의 힘이 발휘되지 않을까요? </p>
<p>우선 asyncio.gather를 사용하는 코드를 살펴봅시다.</p>
<pre><code class="language-python">
import asyncio
import sys

import colorama
from colorama import Cursor

colorama.init()

async def print_at(row, text):
    print(Cursor.POS(1, 1 + row) + str(text))
    await asyncio.sleep(0.03)

async def count_lines_in_file(file_num, file_name):
    counter_text = f&quot;{file_name[:20]:&lt;20} &quot;
    with open(file_name, mode=&quot;rt&quot;, encoding=&quot;utf-8&quot;) as file:
        for line_num, _ in enumerate(file, start=1):
            counter_text += &quot;□&quot;
            await print_at(file_num, counter_text)
        await print_at(file_num, f&quot;{counter_text} ({line_num})&quot;)

async def count_all_files(file_names):
    tasks = [
        asyncio.create_task(count_lines_in_file(file_num, file_name))
        for file_num, file_name in enumerate(file_names, start=1)
    ]
    await asyncio.gather(*tasks)

if __name__ == &quot;__main__&quot;:
    asyncio.run(count_all_files(sys.argv[1:]))

</code></pre>
<p>아래와 같은 명령어를 실행하면 <code>not_utf8.txt</code>, <code>empty_file.txt</code> 이 두 파일에러 에러가 발생하리라는 것을 예상 할 수 있습니다.</p>
<pre><code class="language-bash">python count_taskgroup.py not_utf8.txt empty_file.txt</code></pre>
<p>하지만 실제 에러는 1개만 출력됩니다.</p>
<pre><code class="language-python">UnicodeDecodeError: &#39;utf-8&#39; codec can&#39;t decode byte 0xe5 in position 2: invalid continuation byte</code></pre>
<p>이제 TaskGroup으로 바꿔 본 다음 동일한 명령어를 입력해 봅시다. </p>
<pre><code class="language-python">async def count_all_files(file_names):
    async with asyncio.TaskGroup() as tg:
        for file_num, file_name in enumerate(file_names, start=1):
            tg.create_task(count_lines_in_file(file_num, file_name))


---

 python count_taskgroup.py not_utf8.txt empty_file.txt
  + Exception Group Traceback (most recent call last):
  |   ...
  | ExceptionGroup: unhandled errors in a TaskGroup (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File &quot;count_taskgroup.py&quot;, line 18, in count_lines_in_file
    |     for line_num, _ in enumerate(file, start=1):
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | UnicodeDecodeError: &#39;utf-8&#39; codec can&#39;t decode byte 0xe5 in position 2:
    |                     invalid continuation byte
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File &quot;count_taskgroup.py&quot;, line 21, in count_lines_in_file
    |     await print_at(file_num, f&quot;{counter_text} ({line_num})&quot;)
    |                                                 ^^^^^^^^
    | UnboundLocalError: cannot access local variable &#39;line_num&#39; where it is
    |                    not associated with a value
    +------------------------------------</code></pre>
<p>두 개의 에러가 모두 출력 되고 있습니다.</p>
<p>조금 더 쉬운 예제를 살펴봅시다.</p>
<pre><code class="language-python">from asyncio import TaskGroup
import asyncio
async def task(n):
    if n % 2 == 0:
        await asyncio.sleep(0.1)
        raise ValueError(f&quot;Value error in task {n}&quot;)
    return f&quot;Task {n} completed successfully&quot;

async def main():
    try:
        async with TaskGroup() as tg:
            for i in range(4): 
                tg.create_task(task(i))
    except* ValueError as e:
        print(e.exceptions)
        # (ValueError(&#39;Value error in task 0&#39;), ValueError(&#39;Value error in task 2&#39;))

asyncio.run(main())</code></pre>
<pre><code class="language-python">async def main():
    tasks = [task(i) for i in range(4)]
    result = await asyncio.gather(*tasks, return_exceptions=True)
    print(result)
    # [ValueError(&#39;Value error in task 0&#39;), &#39;Task 1 completed successfully&#39;, ValueError(&#39;Value error in task 2&#39;), &#39;Task 3 completed successfully&#39;]
asyncio.run(main())</code></pre>
<h3 id="결론">결론</h3>
<p>확실히 <code>asyncio.gather</code>는 독립적인 코루틴을 단순히 모아둔다는 느낌을 줍니다. 반면, <code>TaskGroup</code>은 여러 작업을 묶어 하나의 큰 작업 단위로 만듭니다. 그룹이 하나의 작업 단위가 되었을 때, <code>TaskGroup</code>을 사용하면 에러 핸들링 로직이나 생명 주기를 보다 편리하게 관리할 수 있겠습니다.</p>
<p>다음 글은 Python의 Asyncio를 주제로 해보고자 합니다. 긴 글 읽어주셔서 감사합니다 :)</p>
<hr>
<p><a href="https://realpython.com/python311-exception-groups/">출처</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Langchain 에 contribute 하기]]></title>
            <link>https://velog.io/@l_cloud/%EC%B2%AB-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EA%B2%BD%ED%97%98%EB%8B%B4-Langchain</link>
            <guid>https://velog.io/@l_cloud/%EC%B2%AB-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EA%B2%BD%ED%97%98%EB%8B%B4-Langchain</guid>
            <pubDate>Sun, 28 Jan 2024 13:53:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 처음 Opensource 에 contribute 한 경험을 서술하였습니다.
어떻게 contribute 하였는지를 이야기합니다. Contribute를 걱정하는 분들에게 도움이 되었으면 좋겠습니다.</p>
</blockquote>
<p>총 세 번의 PR이 모두 merge가 되며 Langchain contributor가 되었습니다 🥳🥳🥳
<img src="https://velog.velcdn.com/images/l_cloud/post/0fce7e78-3be6-4860-89a6-93ecdedd2d47/image.png" alt=""></p>
<h3 id="관심의-시작">관심의 시작</h3>
<p>현재 재직 중인 회사에서 <a href="https://github.com/langchain-ai/langchain">Langchain</a>을 사용하여 개발을 진행하고 있어 학습할 수밖에 없는 환경이었습니다. 또한 업데이트도 상당히 빠른 속도로 이루어져서 <a href="https://blog.langchain.dev/">blog</a>와 메일 구독 해 놓은 상태였습니다. 메일로 정식 stable version <a href="https://blog.langchain.dev/langchain-v0-1-0/">Langchain 0.1.0</a>이 나온다는 소식과 <a href="https://github.com/langchain-ai/langchain/issues/15664">New Contributor</a>를 모집한다는 소식을 받았습니다. New Contributor를 위한 작업은 <code>Update Integration Documentation : Your contribution will make it easier for users to use integrations with the newest LangChain syntax</code> 이었습니다. stable version이 되며 정식 문법을 정착시키고 그것에 맞게 공식 문서 작업이 필요해진 상황이었습니다. 이미 사용하고 있는 Tool이었고, 최대한 최신 버전을 가져가려고 했기 때문에 학습한 내용을 정리하기도 좋아 주말에 요청하고 작업을 진행했습니다.</p>
<h3 id="작업-시작">작업 시작</h3>
<p>오픈 소스에 기여하는 것은 처음이었기에 이전 사람들의 양식을 많이 참고했습니다. 어떤 식으로 문서를 변경했고, PR은 어떻게 올렸고, 어떻게 요청하였고, PR 양식은 어떻게 했는지 등을 많이 참고 했습니다. 보통 Repo에 내용이 잘 적혀있기는 합니다. 그래도 긴가민가한 부분은 Merge 된 PR 들을 참고 했습니다. 이후에는 아래 방식으로 작업을 진행했습니다.</p>
<ol>
<li>Repo Fork</li>
<li>update</li>
<li>PR</li>
</ol>
<p>대략 4시간 정도 걸려서 문서 업데이트를 진행하고 PR을 했습니다.</p>
<h3 id="merge">Merge</h3>
<p>Document의 퀄리티, 방향성이 맞지 않게 수정한 것은 아닐지 하는 걱정을 좀 했습니다. 우려와 다르게 3일 정도 후 약간의 수정 사항이 들어와서 이를 수정하니 Merge가 되었습니다 :)</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/5da7388c-849c-421e-8567-971bd59270ac/image.png" alt=""></p>
<p>제가 수정한 <a href="https://python.langchain.com/docs/integrations/chat/anthropic">Document</a>입니다.</p>
<h3 id="bug-발견">Bug 발견</h3>
<p>Merge가 되고 나서 하루 후 회사에서 작업을 하다 code level에서 bug를 발견합니다. 작업을 하다 오류가 발생하여 원인을 추적하다 보니 Langchain 쪽 코드가 문제가 있음을 발견하여 이를 [issue](<a href="https://github.com/langchain-ai/langchain/issues">https://github.com/langchain-ai/langchain/issues</a> 생성하고 코드를 수정해서 <a href="https://github.com/langchain-ai/langchain/pull/16563">PR</a>을 남겨두었습니다. 다음날 바로 Merge가 되며 두 번째로 contribute를 했습니다.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/97b27818-9f3b-413d-85ff-6bd5b43bd4d7/image.png" alt=""></p>
<h3 id="후기">후기</h3>
<p>opensource 에 contribute를 하고 싶은 마음은 항상 있었지만, 어떻게 시작해야 할지 막막했습니다. 그리고 내가 사용하지도 않는 opensource 에 contribute를 위한 contribute는 딱히 하고 싶지 않았습니다. 실제 opensource를 사용하고 학습하다 불편한 부분이나 개선 사항을 정리하여 contribute하고 싶었는데 좋은 기회였습니다. 특히 신생 project이고 update도 상당히 빠르게 이루어졌기 때문에 merge가 되지 않았나 싶습니다. 첫 시작은 코드의 변경도 아니고 상당히 작은 내용의 추가이기는 하지만 내가 사용하는 Tool에 contribute를 해서 상당히 재미있고 뿌듯했습니다. issue를 작성하고 PR을 하는 방법도 알았으니 또 상황이 맞으면 계속 기여를 해보려고 합니다 :)</p>
<p><del>P.S 아직 <a href="https://github.com/langchain-ai/langchain/issues/15664">Document issue</a>는 계속 열려있으니 contribute는 가능합니다!!</del></p>
<hr>
<p>24년 설에 새로운 <a href="https://github.com/langchain-ai/langchain/pull/17162">Contribute</a>도 하였습니다. 이번에는 Parser의 로직을 변경하는 것이라 테스트 코드도 추가하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python3.12 - subinterpreter와 GIL]]></title>
            <link>https://velog.io/@l_cloud/Python3.12-subinterpreter%EC%99%80-GIL</link>
            <guid>https://velog.io/@l_cloud/Python3.12-subinterpreter%EC%99%80-GIL</guid>
            <pubDate>Sat, 30 Dec 2023 15:57:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 GIL, Thread와 Process의 차이, IPC, interpreter에 대해 사전 설명을 하지 않습니다. subinterpreter와 interpreter라는 용어가 혼재되어 있습니다. 본 글에서는 글을 같은 의미로 이해하여도 괜찮습니다.</p>
</blockquote>
<blockquote>
<p>Python3.12가 업데이트되었습니다. new features가 여러 개 공개되었는데 개인적으로 가장 흥미로운 부분은  “<a href="https://docs.python.org/3.12/whatsnew/3.12.html#pep-684-a-per-interpreter-gil">Support for isolated subinterpreters</a> with separate Global Interpreter Locks (<a href="https://peps.python.org/pep-0684">PEP 684</a>)” 이었습니다. 이를 살펴보면서 subinterpreter에 대해 간략하게 공부한 내용을 정리 해보고자 합니다.</p>
</blockquote>
<h3 id="subinterpreters">subinterpreters</h3>
<p>subinterpreter는 하나의 파이썬 프로세스에서 main interpreter와 <strong>독립적</strong>인 실행 환경을 가지고 병렬적으로 실행할 수 있는 인터프리터를 의미합니다. (Python 1.5 이후로, 여러 인터프리터를 가질 수 있는 C-API가 있습니다.) 그런데 무엇이 독립적일까요? 스레드도 독립적인 stack 영역이 있는데 이와 유사한 것일까요? 아니면 프로세스처럼 메모리 영역과 네임 스페이스 등이 독립적인 것일까요? 이해도를 높이기 위해 아래 사진을 봅시다.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/871ec241-7995-49b7-977e-e6e34f55b8b6/image.png" alt="">
<a href="https://tonybaloney.github.io/posts/sub-interpreter-web-workers.html">출처</a></p>
<p>파이썬에는 모든 인터프리터가 공유하는 Runtime State(global state of Cpython 정도로 이해하면 편합니다.)이 있습니다. 그리고 Interpreter마다 interpreter state가 있고, Thread마다 각자의 stack 영역이 있죠. subinterpreter의 경우 독립적인 공간을 가진 스레드 집합체라고 이해하시면 됩니다. interpreter에서 thread를 생성하면 다른 interpreter에서는 보이지 않습니다. 또한 global scope name table, import 한 module 등은 독립적입니다. 하지만 OS가 프로세스에 할당한 것(메모리, 파일 핸들러 등)은 독립적이지 않습니다. Thread가 같은 global scope name table을 가지고 heap 영역을 공유하고, Process는 서로 다른 memory sapce를 가진다는 것과 다르죠.</p>
<blockquote>
<p>subinterprter의 메모리가 독립적이지 않다는 의미는 OS에 할당받은 같은 memory space를 interpreter끼리 공유하여 사용한다는 의미이고, heap 영역이나 stack 영역은 서로 독립적입니다.</p>
</blockquote>
<h3 id="thread-process와-비교했을-때-어떠한-장점이-있을까요">thread, process와 비교했을 때 어떠한 장점이 있을까요?</h3>
<p>untrusted code를 실행해야 하는 상황을 생각해 봅시다. 현재 interprter와 격리된 레벨에서 실행하므로 thread보다 상대적으로 보안 측면에서 더 뛰어나겠죠. global scope name table을 다르게 하고 싶거나 다른 모듈을 사용하고 싶을 때도, thread가 아닌 subinterprter가 적절하겠죠. 또한 같은 프로세스 내에서 실행되기 때문에 multi-processing보다 communication 비용이 더 적지 않을까요? (물론 interpreter 간의 data sharing은 직접적으로는 불가능하며 OS.pipe()를 사용해야 합니다. 당연히 직렬화와 역 직렬화 비용이 들어갑니다. 추가로 multi-processing 때 고려 해야할 문제도 고민 해야하고요.)</p>
<h3 id="multi-threading-multi-processing의-장점만-모아둔-것-아니야">multi-threading, multi-processing의 장점만 모아둔 것 아니야?</h3>
<p>우리는 앞서 Runtime State를 전체 interprter가 공유한다고 배웠습니다. GIL도 여기에 포함됩니다. 즉 true parallelism을 달성할 수 없었죠. subinterpreter 생성이 process를 하나 더 실행하는 것보다 빠르기는 하지만 thread를 생성하는 것보다는 당연히 느립니다. 이점이 많이 상쇄되기도 하고 python의 stdlib내에 포함되어 있지도 않아서 사용이 번거롭습니다.</p>
<h3 id="312의-변화---a-per-interpreter-gil">3.12의 변화 - <strong>A Per-Interpreter GIL</strong></h3>
<p><a href="https://peps.python.org/pep-0684/">PEP 684</a>에서 자세히 보실 수 있습니다. 3.12에서는 Runtime State에서 GIL을 interpreter state로 옮겼습니다. 정말 간단해 보이지만 이 과정은 7년 이상이 소요 되었습니다. GIL은 여러 thread가 동시에 Runtime State에 접근하는 것을 방지해 race condition을 방지하고 있었기 때문입니다. 이를 위해 많은 변경 사항이 있었습니다. <a href="https://github.com/python/cpython/pull/101660">&quot;obmalloc&quot; 을 runtime state에서 interpreter state로 변경한 것</a>도 하나의 예시입니다.  더 자세한 이야기는 <a href="https://realpython.com/python312-subinterpreters/#changes-to-extension-modules">여기</a>를 통해 확인해 보세요! 와 그러면 이제 Cpython에서도 true parallelism을 기대해 봐도 좋을까요? 필자가 3.12 버전을 통해 여러 방면으로 실험해 보려 했으나 실험 코드 작성이 쉽지도 않고 누군가 저보다 훨씬 잘 정리를 해놓았기에 <a href="https://tonybaloney.github.io/posts/sub-interpreter-web-workers.html">링크</a>를 남깁니다. 성능 비교도 보기 좋게 해두었습니다.</p>
<h3 id="subinterpreter의-stdlib-편입-gil-제거">subinterpreter의 stdlib 편입, GIL 제거</h3>
<p> PEP 554, PEP 734는 현재 프로세스에서 interpreter를 생성하고, 분석하고, 실행시키는 새로운 모듈인 <code>interpreters</code>를 표준 라이브러리에 포함하는 것을 제안합니다. 아직 accept 된 것은 아니라 어떻게 될지는 모르지만 잘 만들어져서 편하게 사용했으면 좋겠네요 :) 이외에도 GIL을 Optional로 변경하려는 <a href="https://peps.python.org/pep-0703/">PEP 703</a> 논의도 있습니다. 3.13에서 반영이 될 예정이라고 합니다. </p>
<h3 id="출처-및-후기">출처 및 후기</h3>
<p> GIL과 관련하여 큼직한 변화들이 나오고 있어 흥미로웠습니다. 현재 회사에서 Python을 사용 중이기에 계속 변화를 따라가고자 합니다. 설계에 대한 공부가 많이 필요함을 느끼기도 했습니다. 현재 글과 큰 관련은 없지만
 <a href="https://hyperconnect.github.io/2023/05/30/Python-Performance-Tips.html">하이퍼 컨넥트의 파이썬 성능 최적화</a> 글을 읽으며 Python의 multi-processing에 대해 상당히 많이 배웠습니다.</p>
<p><a href="https://tonybaloney.github.io/posts/sub-interpreter-web-workers.html">출처1</a></p>
<p><a href="https://realpython.com/python312-subinterpreters/">출처2</a></p>
<p><a href="https://thinhdanggroup.github.io/subinterpreter/">출처3</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[가상 메모리는 왜 등장하게 되었을까?]]></title>
            <link>https://velog.io/@l_cloud/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC%EB%8A%94-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%95%98%EA%B2%8C-%EB%90%98%EC%97%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@l_cloud/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC%EB%8A%94-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%95%98%EA%B2%8C-%EB%90%98%EC%97%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sun, 05 Nov 2023 13:34:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 가상 메모리, 메모리 파편화, 페이징, 스왑 등을 들어본 독자를 대상으로 합니다.
프로세스가 직접 메모리를 조작하지 않고 OS를 거쳐 간접적으로 조작하게 한 이유를 알아봅니다.</p>
</blockquote>
<p>메모리 할당은 보통 두 가지 타이밍에서 발생합니다.</p>
<ol>
<li>프로세스를 생성할 때</li>
<li>프로세스를 생성한 뒤 추가로 동적 메모리를 할당할 때!</li>
</ol>
<p>가상 메모리를 사용하지 않고 단순히 개별 프로세스가 직접 메모리에 접근한다면 문제가 발생할까요?</p>
<ol>
<li><p><strong>다른 용도의 메모리에 접근 가능</strong>
메모리 주소를 통해 직접 접근할 수 있으니 커널이나 다른 프로세스가 사용 중인 곳에 접근이 가능해집니다.</p>
</li>
<li><p><strong>여러 프로세스를 다루기 곤란</strong>
동일한 프로그램을 1개 더 가동해 메모리에 매핑할 때 어떻게 해야 할까요? 파일 헤더에는 코드와 데이터 영역의 파일 상 오프셋, 사이즈, 메모리 맵 시작 주소 등이 적혀있는데 그럼 동일한 프로그램은 코드 영역이 겹치니 동시에 실행 못 하지 않을까요? 그리고 다른 프로그램을 만들 때 메모리 주소를 직접 정하면 기존에 있는 프로그램의 메모리 주소를 다 피해서 지정해야 하지 않을까요?</p>
</li>
<li><p><strong>메모리 단편화</strong>
메모리 획득 해제를 반복하면 메모리 파편화 문제가 발생합니다. 남아있는 영역은 300kb인데 100kb씩 나뉘어 있으면 어떻게 해야 할까요? 3개의 영역을 하나로 묶어서 다루면 될까요? 그렇다면 매번 프로세스를 실행할 때마다 몇 개의 영역으로 나뉘어 있는지 확확인해야  않을까요? 상당히 불편하겠죠?</p>
</li>
</ol>
<h3 id="가상-메모리의-등장">가상 메모리의 등장!</h3>
<p>가상 메모리의 핵심 개념은 <code>가상 주소를 가지고 물리 메모리에 간접적으로 접근한다.</code> 입니다. 프로세스가 실제 물리 메모리 영역에 <code>직접</code> 접근할 방법은 없습니다. 그럼 어떻게 간접적으로 접근을 할까요?
바로 OS가 개별 프로세스에 <code>페이지 테이블</code>을 제공하여 OS를 통해서만 물리 메모리에 접근할 수 있게 합니다. 이렇게 하면 프로세스에 허용되지 않은 메모리 접근을 막을 수 있어서 1번 문제를 해결할 수 있겠죠.</p>
<p>직접 메모리에 접근하는 코드로 실험을 해봅시다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main()
{
    int *p = NULL;
    puts(&quot;Before invalid access&quot;);
    *p = 0;
    puts(&quot;After invalid access&quot;);
}

root@c89f1455e7c0:/test# ./segv
Before invalid access
Segmentation fault</code></pre>
<p>프로그램이 허용되지 않은 메모리 영역에 접근을 시도하거나, 허용되지 않은 방법으로 메모리 영역에 접근을 시도할 경우 발생하는 Segmentaion fault가 발생하는 것을 확인할 수 있습니다.</p>
<p>또한 단편화된 물리 메모리 주소를 페이지 테이블에서 적절하게 가상 주소로 매핑하여 3번 문제도 해결하고, 개별 프로세스마다 페이지 테이블이 제공되니 2번 문제도 해결이 됩니다.</p>
<blockquote>
<p>아래는 참고 내용
처음에 프로세스에 약간의 메모리를 할당하고 추가로 더 할당할 때는 프로세스에 페이지 테이블을 추가로 OS가 작성한 다음 프로세스에 넘겨준다. 그럼 페이지 테이블에 해당 정보가 계속 추가 된다.
c언어 <code>mmap()</code> 은 페이지 단위, <code>malloc()</code>은 바이트 단위로 메모리를 확보한다. 그래서 <code>malloc()</code> 대비하기 위해서 glibc에서 사전에 메모리 풀로 미리 메모리를 확보하고 있다가 malloc() 호출하면 그 풀에서 메모리를 주고.. 그래서 리눅스에서 메모리 사용량 체크하는 거랑 프로세스 내에서 메모리 체크하는 거랑 다를 수 있음! 메모리 풀을 포함하느냐 안 하느냐에 따라.. (이는 가상 메모리 응용을 이해하는 데 도움이 됨)</p>
</blockquote>
<h2 id="가상-메모리의-응용">가상 메모리의 응용</h2>
<ul>
<li>파일 맵</li>
<li>디맨드 페이징</li>
<li>Copy on Write 방식의 고속 프로세스 생성</li>
<li>스왑</li>
<li>계층형 페이지 테이블</li>
<li>Huge page</li>
</ul>
<p><strong>파일 맵(MMF)</strong></p>
<p>전통적인 파일 입출력은 데이터를 읽거나 쓸 때 시스템 콜 사용합니다. 그리고 데이터는 버퍼에 일시적으로 저장되며 여기에서 읽기, 쓰기 등이 이루어집니다. 하지만 메모리 맵 파일은 파일 전체나 일부를 가상 메모리의 주소 공간에 <strong>직접</strong> 매핑함. 메모리에 있는 일반 변수를 읽고 쓰는 것과 유사하게 동작함.
따라서 <code>write</code>와 같은 시스템 콜 호출 없이 메모리 영역대로 내용을 복사해서 실제 파일에 내용을 저장할 수 있습니다. <a href="https://github.com/caniro/linux-structure-practice/blob/main/chapter05/src/filemap.c">예시 코드</a></p>
<p><strong>디맨드 페이징</strong></p>
<p>프로세스의 모든 영역이 메모리에 올라올 필요는 없습니다. 즉 현재 필요한 페이지만 메모리에 올리면 됩니다.
그럼 아직 할당이 안 된 영역에 접근하면 어떻게 될까요? 페이지 폴트가 발생한 다음 커널 모드에서 메모리를 할당해 줍니다. 이렇게 동적으로 할당하면 훨씬 메모리를 아낄 수 있겠죠?</p>
<p>실제로 <code>mmap()</code> 함수는 <code>메모리 영역 확보는 일단 가상 메모리를 확보했음</code>을 의미하고 실제 물리 메모리 확보를 하지는 않습니다. 실제 그 메모리에 접근할 때 물리 메모리에 할당이 됩니다. 이 <a href="https://github.com/caniro/linux-structure-practice/blob/main/chapter05/src/mmap.c">코드</a>와 함께 페이지 폴트 여부와 메모리 상황을 모니터링 해보면 해당 내용을 눈으로 확인할 수 있습니다. </p>
<p><strong>Copy on Write</strong> </p>
<p><code>fork()</code>  시스템 콜을 사용하면 부모 프로세스의 메모리를 자식 프로세스에 전부 복사하는 것이 아니라 그냥 페이지 테이블만 복사하기에 상당히 빠릅니다. 그럼 데이터의 <code>write</code>는 어떻게 할까요? 일단 물리 메모리 영역을 공유하고 있기 때문에 쓰기 호출이 들어오는 경우</p>
<ol>
<li>페이지에 쓰기는 허용하지 않기 때문에 일단 페이지 폴트 발생</li>
<li>CPU가 커널 모드로 변경되어서 페이지 폴트 핸들러 동작</li>
<li>페이지 폴트 핸들러는 <code>write</code> 하려고 하는 페이지를 다른 장소에 복사하고, <code>write</code> 요청을 보낸 프로세스에 해당 영역을 할당한 후 내용을 작성함</li>
<li>부모, 자식 프로세스 모두 각각 공유가 해제된 페이지에 대응하는 페이지 테이블 엔트리를 업데이트!!</li>
</ol>
<p>그림으로 나타내면 아래와 같습니다.
<img src="https://velog.velcdn.com/images/l_cloud/post/d4569011-873c-425f-9e20-f68314cb6216/image.png" alt=""></p>
<p><strong>스왑</strong></p>
<p>저장 장치의 일부를 일시적으로 메모리 대신 사용하는 방식입니다.</p>
<p>스왑 아웃과 스왑 인을 합쳐서 스와핑이라고 합니다.</p>
<p>메모리가 부족해서 메모리에 접근할 때마다 스와프 인, 스왑 아웃 발생하면 <a href="https://blog.skby.net/%EC%8A%A4%EB%A0%88%EC%8B%B1-thrashing/">스래싱 상태</a>가 됩니다.</p>
<p>sar -W 1 명령어로 스와핑 발생 유무도 확인할 수 있습니다.</p>
<p><code>Major Fault</code> → 저장 장치에 대한 접근이 발생하는 페이지 폴트. (SSD, HDD 등)</p>
<h3 id="페이지-테이블-크기-문제-해결-방법">페이지 테이블 크기 문제 해결 방법</h3>
<p><strong>계층형 페이지 테이블</strong></p>
<p>x86_64 아키텍쳐의 가상 주소는 128테라 바이트입니다. 1페이지의 크기는 4kb, 테이지 테이블 엔트리 사이즈는 8byte. 그럼 프로세스 1개당 (8 바이트 * 128 테라 바이트 / 4kb)의 용량이 필요할까요? NO!!</p>
<p>계층 구조로 이것을 표현해서 용량을 줄이거나 해시 페이지 테이블, 역 페이지 테이블 등을 사용합니다.
<a href="https://charles098.tistory.com/108">자세한 내용</a></p>
<p><strong>Huge Page</strong></p>
<p>프로세스의 가상 메모리 사용 사이즈가 증가하면 페이지 테이블에 사용하는 물리 메모리양도 증가합니다.</p>
<p>이를 해결하기 위해 Huge Page를 사용해서 페이지 테이블에 필요한 메모리양을 줄입니다.</p>
<hr>
<h3 id="출처">출처</h3>
<p><a href="https://code-lab1.tistory.com/58">COW</a>
<a href="https://github.com/caniro/linux-structure-practice/tree/main">소스코드</a>
<a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=181554153">책</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WS, WAS, Spring의 관계]]></title>
            <link>https://velog.io/@l_cloud/WS-WAS-Spring%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@l_cloud/WS-WAS-Spring%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Wed, 04 Oct 2023 02:54:29 GMT</pubDate>
            <description><![CDATA[<p><strong>브라우저에 <a href="http://www.sample.com%EC%9D%84">www.sample.com을</a> 입력하면 어떤 일이 발생하나요?</strong> 
면접 단골 질문이자 인터넷에서 많은 내용을 찾을 수 있는 질문입니다. 하지만 대부분 클라이언트 측면에서 발생하는 내용을 담고 있고 web server(이하 WS), web application server(이하 WAS), Spring, servlet 등의 관계를 정확히 이해하지 못하는 분들이 있어 해당 글을 통해 알아보고자 합니다.</p>
<blockquote>
<p>본 글을 웹 프레임워크를 사용해 본 독자를 대상으로 합니다. WS, HTTP 등에 대한 사전 학습이 필요합니다.
WAS, MVC, Spring에 대한 깊이 있는 설명보다는 그들의 연결에 중점을 맞추고 있습니다.</p>
</blockquote>
<p>태초의 웹 서비스로 돌아가 봅시다. HTTP 요청을 받으면 미리 저장된 데이터를 return 해주었습니다. 사용자가 데이터에 개입할 여지는 없었죠.(<a href="http://info.cern.ch">체험하기</a>) 하지만 요즘 웹 서비스는 어떤가요? 가장 쉬운 예시로 로그인을 생각해 봅시다. 사용자가 어떤 정보를 서버에 보내면 서버는 로그인 실패 혹은 로그인이라는 응답을 보내줍니다. 즉 사용자와 서버가 상호작용하며 동적으로 데이터를 만들고 있습니다. 동적으로 데이터를 만들어 낸다는 것은 어떠한 가공 처리가 이루어졌다는 이야기입니다. 이렇게 동적으로 데이터를 처리하면 스크롤에 따른 새로운 피드, 사용자 맞춤 광고 등이 가능하며 보다 나은 서비스를 제공해 줄 수 있습니다.</p>
<p>간단히 사진으로 보면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/ec570e5b-9be5-420e-9a51-8ed5f183ed71/image.png" alt=""></p>
<blockquote>
<p>[이미지 출처] (<a href="https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/">https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/</a>)</p>
</blockquote>
<p>“가공 처리”는 결국 어떠한 코드를 실행했다는 의미겠죠? 결국 <strong>어떻게 코드를 실행할 것인가</strong> 이 질문이 오늘 주제의 핵심입니다. 그 발자취를 따라가 봅시다.</p>
<h3 id="cgi의-등장">CGI의 등장</h3>
<p>CGI는 HTTP 서버와 web content를 생성하는 프로그램과의 Common Gateway Interface를 의미합니다. CGI 규약만 지킨다면 Python, C++, C, Java 등 어떠한 프로그래밍 언어로 작성해도 상관없습니다.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/5d7fad0e-505d-4a58-90fc-46186deeef00/image.png" alt=""></p>
<blockquote>
<p><a href="%5Bhttps://www.elprocus.com/what-is-common-gateway-interface-working-and-its-applications/%5D(https://support.novell.com/techcenter/articles/dnd20000302.html)">이미지 출처</a></p>
</blockquote>
<p>하지만 CGI 프로그램은 요청마다 새로운 프로세스를 생성합니다. 모두 독립적이기에 무겁고 느리고 중복된 코드가 많이 발생할 수밖에 없습니다!</p>
<blockquote>
<p>왜 Interface가 필요한지를 한번 생각해 보시면 Servlet의 존재 의의도 이해하실 수 있습니다. 웹 통신이 HTTP 프로토콜을 사용한다는 점을 한 번 떠올려 보세요.</p>
</blockquote>
<h3 id="was와-servlet의-등장">WAS와 Servlet의 등장</h3>
<p>우선 Servlet은 HTTP 요청에 대한 파싱 그리고 그에 대한 응답을 만들어 주는 Java 클래스라고 생각하시면 됩니다. CGI의 주요 단점은 개별 프로세스를 요청마다 생성하고 종료시켜야 하는 것이었습니다. Servlet은 WAS을 Servlet Container에 등록하면 WAS가 이를 관리하고 스레드 단위로 요청이 실행됩니다. 조금 복잡하죠? 사진을 통해 조금 더 자세히 알아봅시다.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/5b2e16c9-1af6-494e-9c4b-63e33338e8aa/image.png" alt=""></p>
<blockquote>
<p>[이미지 출처] (<a href="https://e-una.tistory.com/73">https://e-una.tistory.com/73</a>)</p>
</blockquote>
<p>가공이 필요한 요청이 WS로 들어온 경우를 생각해 보면, WAS로 가공을 부탁합니다. WAS는 해당 요청에 맞는 Servlet을 찾아 실행한 후 결과를 WS로 보내줍니다. 그림을 보면 알 수 있듯, Servlet은 생명 주기를 가집니다.</p>
<p>생명 주기는 아래와 같습니다.</p>
<ol>
<li>요청이 오면, Servlet 클래스가 로딩되어 Servlet 객체가 생성된다.</li>
<li>init()을 통해 초기화 한다.</li>
<li>service() 로직을 실행하고 service()는 특정 HTTP 요청을 처리하는 메서드(doGet(), doPost() 등)을 호출한다.</li>
<li>destroy()를 호출해 Servlet을 제거한다.</li>
</ol>
<p>WAS의 한 종류인 Tomcat은 한 번 생성된 Servlet 객체를 메모리에 두어서 init() 메서드가 한 번만 실행하도록 관리를 합니다. 또 종료되기 전이나 reload 전에 destroy()를 호출하여 매번 객체가 생성되는 것을 방지합니다.</p>
<p>하지만 이러한 방식은 아래 그림처럼 어떤 공통 로직을 처리해야 한다면 코드가 반복되는 문제점을 가지고 있습니다. 만약 어떤 서블릿에서 공통 로직을 잊어버렸다면 상당히 곤란하겠죠?</p>
<p>(Servlet을 가지고 코딩을 해보셨다면 구체적인 코드가 떠오르시지 않을까 합니다.)</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/feb93f7c-c80e-42ac-ab35-e1b43eaf4339/image.png" alt=""></p>
<h3 id="spring-구조">Spring 구조</h3>
<p>그럼 아래와 같은 구성을 하면 좋지 않을까요? 입구를 하나로 만들어서 공통 로직을 모두 처리하고  요청에 맞는 컨트롤러를 찾아서 호출하도록 하는 것이죠.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/12a8a038-f814-40c6-aba1-6841fc298a8f/image.png" alt=""></p>
<p>그렇다면 다른 컨트롤러는 굳이 Servlet이 아니어도 괜찮겠죠? 그럼 Servlet을 WAS에 하나만 등록하고 나머지 컨트롤러는 Servlet 객체 상속을 안 받아도 되니 구현도 훨씬 단순해지겠죠! 
Spring 프레임워크는 Front Controller를 제공해주고 이를 Dispather Servlet 이라 부릅니다. 그렇다면 최종 모습은 아래와 같겠죠.</p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/9ee65547-6405-4dbd-bb64-e8765d6bd87a/image.png" alt=""></p>
<h3 id="마무리">마무리</h3>
<p>전체적인 구조 이해를 위해 Tomcat에 어떻게 Servlet을 등록하는지, MVC, Front Controller 등에 대한 내용과 코드를 모두 생략했습니다. 이는 다음에 다루어 보도록 하겠습니다. 개인적으로 Tomcat에서 어노테이션을 통해 Servlet을 등록하는 것을 보고 Spring과 상당히 유사하다고 느꼈습니다. 아마 Spring이 이를 참고하지 않았을까 합니다. (Bean 관리도 그렇고요) MVC도 왜 HTML, CSS, JS로 파일을 나눈 이유와 유사한 부분을 많이 느꼈습니다.</p>
<p>참고 링크</p>
<p><a href="https://techdifferences.com/difference-between-cgi-and-servlet.html">CGI &amp; Servlet</a>
<a href="https://jongminlee0.github.io/2020/10/10/cgivsservlet/">CGI &amp; Servlet</a>
<a href="https://www.youtube.com/watch?v=h0rX720VWCg">테크톡</a>
<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1">인프런 강의</a>
<a href="https://velog.io/@devjooj/Server-Ngnix-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C">Nginx</a></p>
]]></description>
        </item>
    </channel>
</rss>