<?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>Sat, 17 Jan 2026 05:31:18 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[쿠버네티스 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>
        <item>
            <title><![CDATA[예제를 통해 어노테이션의 큰 그림을 이해하기]]></title>
            <link>https://velog.io/@l_cloud/%EC%98%88%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%9D%98-%ED%81%B0-%EA%B7%B8%EB%A6%BC%EC%9D%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@l_cloud/%EC%98%88%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%9D%98-%ED%81%B0-%EA%B7%B8%EB%A6%BC%EC%9D%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 Sep 2023 02:53:42 GMT</pubDate>
            <description><![CDATA[<p>Java를 학습하다 보면 <code>@Bean</code> , <code>@ComponentScan</code>, <code>@Override</code> 등 이런 어노테이션을 꽤 자주 마주칩니다. 대체 저 @가 무엇이기에 <code>@Bean</code>, <code>@Component</code> 과 같은 어노테이션이 붙은 클래스 혹은 매서드를 찾아낼 수 있고 컴파일 단계에서 오류를 검출할 수 있을까요? 어노테이션을 공부하면서 원리를 간략하게 알아봅시다! (파이썬의 데코레이터가 생각나셨다면 정상입니다! 데코레이터에 관련된 글은 <a href="https://velog.io/@l_cloud/%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%EC%9D%B4%ED%95%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%97%AC%EC%A0%95-feat-FastAPI">여기</a>서 확인해 보세요)</p>
<blockquote>
<p>본 글은 깊은 내용보다는 추상적으로 어노테이션이 무엇이고 어떻게 사용하고 동작하는지 이해하는데 초점을 두고 있습니다. 글을 다 읽고 무엇을 공부할지 대강 감을 잡을 수 있으면 좋겠습니다.</p>
<p>본문에서 알아볼 내용</p>
<ol>
<li>어노테이션 이란?</li>
<li>어노테이션의 종류</li>
<li>예시를 통해 이해하기</li>
<li>어노테이션과 리플랙션 및 어노테이션 프로세서</li>
</ol>
</blockquote>
<h3 id="어노테이션과-처리-과정">어노테이션과 처리 과정</h3>
<p>어노테이션이 무엇인지 우선 <a href="https://docs.oracle.com/javase/tutorial/java/annotations/">공식 문서</a>를 확인해 봅시다. </p>
<blockquote>
<p><em>Annotations</em>, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.</p>
</blockquote>
<p>코드에 영향을 주지 않는 메타 정보라고 합니다. </p>
<ul>
<li><strong>Information for the compiler</strong> — Annotations can be used by the compiler to detect errors or suppress warnings.</li>
<li><strong>Compile-time and deployment-time processing</strong> — Software tools can process annotation information to generate code, XML files, and so forth.</li>
<li><strong>Runtime processing</strong> — Some annotations are available to be examined at runtime.</li>
</ul>
<p>또한 컴파일러에 정보를 주거나 컴파일 및 배포 혹은 런타임 처리에 사용할 수 있다고 합니다. </p>
<p>어노테이션이 처리되는 과정을 간략히 살펴 봅시다. <a href="https://openjdk.org/groups/compiler/doc/compilation-overview/index.html">자세한 내용</a></p>
<p><img src="https://velog.velcdn.com/images/l_cloud/post/97c03964-61ac-4e13-bbc5-224398ea35cc/image.png" alt=""></p>
<p>어노테이션 프로세서는 기존 소스 파일을 수정하지 않고 새로운 파일을 <code>생성</code> 하기에 기존 코드에 영향을 주지 않습니다!</p>
<h4 id="어노테이션-종류">어노테이션 종류</h4>
<p>우선 어노테이션은 크게 세 가지로 구분이 됩니다.</p>
<ul>
<li><strong>표준 어노테이션</strong> → 자바에서 기본적으로 제공하는 어노테이션입니다. @Override, @Functionalinterface 등이 있습니다.</li>
<li><strong>메타 어노테이션</strong> → 어노테이션을 위한 어노테이션으로 어노테이션의 적용 대상이나 유지 기간 등을 지정하는 데 사용합니다.</li>
<li><strong>사용자 어노테이션</strong> → 사용자가 만든 어노테이션 입니다.</li>
</ul>
<p>사용자 어노테이션을 만들기 전에 메타 어노테이션 두 개만 알아봅시다.</p>
<p><code>@Reteantion</code>  : 어노테이션이 어느 시점까지 유지되어야 하는지를 지정합니다. 앞서 이야기한 컴파일 및 배포 혹은 런타임 중 어느 시점까지인지 정한다고 생각하면 됩니다. </p>
<ul>
<li><strong>@Retention(RetentionPolicy.SOURCE)</strong> -&gt; 소스 파일에만 존재. 클래스 파일에는 존재하지 않음 즉,컴파일 때 사라짐</li>
<li><strong>@Retention(RetentionPolicy.CLASS)</strong> -&gt; 클래스 파일에 존재. 런타임에 사용불가. 기본값.</li>
<li><strong>@Retention(RetentionPolicy.RUNTIME)</strong> -&gt; 클래스 파일에 존재. 실행시에 사용 가능</li>
</ul>
<p><code>@Target</code> : 어디에 적용할 수 있는지 (예: 메소드, 패키지, 필드, 생성자, 애너테이션 등)를 지정합니다.</p>
<p>더 자세한 정보는 <a href="https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html">여기</a>를 확인해 보세요. 이와 관련된 내용은 다른 블로그에서 쉽게 찾을 수 있기에 넘어가도록 하겠습니다.</p>
<h3 id="어노테이션-만들어-보기">어노테이션 만들어 보기</h3>
<p>그럼 이제 사용자 어노테이션을 만들어 봅시다.  문법은 인터페이스를 만드는 것과 유사합니다. <code>@interface</code>를 대신 작성해주면 됩니다. </p>
<pre><code class="language-java">import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME) // 런타임까지 어노테이션 유지
@Target(ElementType.TYPE) // Class, interface (including annotation interface), enum, or record
public @interface MyRuntimeAnnotation {
    String value() default &quot;default value&quot;;
}

// Retention, Target을 작성해주지 않으면 Default 값이 됩니다. </code></pre>
<blockquote>
<p>참고 : 너무 복잡하게 생각하지 마세요! 저는 한 번에 이해하려고 하다가 시간만 날리고 말았습니다. 우선 어노테이션을 코드에 영향을 주지 않는 메타 정보를 주입하는 문법으로 생각해 보세요. 처음부터 for 문을 배웠을 때를 떠올려 봅시다. 종료 조건을 컴퓨터가 어떻게 판별하는지, 어떻게 반복하는지를 공부하는 것이 아니라 그냥 for 문의 문법을 이해하는 데 초점을 맞추었습니다. 어노테이션도 이와 마찬가지입니다.</p>
</blockquote>
<p>이제 이를 적용해볼까요?</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.value()); // 여기!
    }
}

@MyRuntimeAnnotation
class A{}</code></pre>
<p>위 코드는 어떻게 될까요? <code>default value</code> 가 출력이 될까요? 아니요 컴파일 오류가 발생합니다!</p>
<p>왜요? 런타임에 사용할 수 있고 컴파일 시점에 남아 있다면서요? 다시 한번 더 이야기하지만 어노테이션은 <code>코드에 영향을 주지 않는 메타 정보</code> 입니다! <code>A</code> 클래스에서 <code>value()</code> 를 호출할 수 있다면 <code>A</code> 코드에 영향을 주는 것이겠죠? 그럼 메타 정보를 어디서 어떻게 확인할 수 있을까요?
<code>A.class</code> 파일을 조금 자세히 봅시다. <code>RuntimeVisibleAnnotations</code> 이 따로 추가된 것을 확인할 수 있습니다. (<code>RetentionPolicy</code> 에 따라 다를 수 있겠죠?)</p>
<pre><code class="language-java">javap -v A
...
  Compiled from &quot;Main.java&quot;
class A
 ...
  #10 = Utf8               RuntimeVisibleAnnotations
...
SourceFile: &quot;Main.java&quot;
RuntimeVisibleAnnotations:
  0: #11()
    MyRuntimeAnnotation</code></pre>
<p>그럼 어떻게 사용하라는 거야?! 하는 의문이 들 수 있습니다. 이때 나오는 것이 바로 <code>리플렉션</code> 입니다!</p>
<h3 id="리플랙션">리플랙션</h3>
<p>리플랙션이란 런타임에 동적으로 특정 클래스의 정보를 추출할 수 있는 기법입니다.  이제 리플랙션을 이용해서 어노테이션 정보를 가져와 봅시다!</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        A a = new A();

        Annotation[] annotations = a.getClass().getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof MyRuntimeAnnotation) {
                MyRuntimeAnnotation myAnnotation = (MyRuntimeAnnotation) annotation;
                System.out.println(&quot;Annotation: &quot; + annotation);  // Annotation: @MyRuntimeAnnotation(value=default value)
                System.out.println(&quot;Value: &quot; + myAnnotation.value()); // Value: default value
            }
        }
    }
}</code></pre>
<p>객체 생성 없이 클래스 정보를 가져오거나 특정 패키지 내의 클래스 모두 가져오는 것도 가능합니다.  물론 해당 클래스 정보가 JVM에 없으면 클래스 로더에 의해서 로딩이 됩니다.</p>
<p>그럼 스프링에서 어떻게 bean을 등록하는지 큰 그림이 그려지시죠? 구체적인 것은 코드를 봐야겠지만 패키지 내의 클래스 파일들을 읽어보고 <code>@Component</code> , <code>@Bean</code> 어노테이션이 붙은 클래스를 식별할 것이라 봅니다. <code>ClassPathScanningCandidateComponentProvider</code> 키워드로 검색하면 뭔가 나올 것 같네요.</p>
<h3 id="어노테이션-프로세서">어노테이션 프로세서</h3>
<p>그럼 <code>@Retention(RetentionPolicy.RUNTIME)</code> 이 아닌 경우는 어떻게 될까요? 리플랙션을 사용하지 못하는데? 이때는 어노테이션 프로세서를 생성 해서 사용합니다. 어노테이션 프로세서는 컴파일 단계에서 Annotation에 정의된 로직을 동작하게 해줍니다. 개발자는 이 로직을 직접 작성 할 수 있습니다. </p>
<pre><code class="language-java">import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes(&quot;com.package.MyAnnotation&quot;) //적용할 어노테이션 위치
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set&lt;? extends TypeElement&gt; annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            // 여기에서 규칙을 확인합니다. 예를 들어, 메소드 이름이 &quot;myMethod&quot;인지 확인합니다.
            if (!element.getSimpleName().toString().equals(&quot;myMethod&quot;)) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, &quot;Method should be named &#39;myMethod&#39;&quot;, element);
            }
        }
        return true;
    }
}
</code></pre>
<p>추가로 <code>META-INF/services/</code>에 <code>javax.annotation.processing.Processor</code> 파일을 생성하고 어노테이션 프로세서를 등록해야합니다. 그럼 특정 조건을 만족하지 않으면 컴파일 타임에 오류가 발생하는 어노테이션을 만들 수 있겠죠. </p>
<h3 id="마무리">마무리</h3>
<p>개인적으로 어노테이션을 공부할 때 마법처럼 느껴져 이해하기 어려웠습니다. 대부분의 글과 책에서는 어노테이션의 정의와 미리 정의된 어노테이션에 대한 설명, 그리고 어노테이션을 만드는 방법에 대해서만 다루고 있었기 때문이었습니다. 너무 구체적인 내용만 있고 도대체 저 어노테이션이 부여한 메타 정보를 어떻게 사용하는지는 찾기 어려웠습니다. 우선 자세한 내용보다는 큰 틀에서 어노테이션을 살펴보니 이후 어떤 것을 공부하면 좋을지에 대한 방향이 잡히기 시작했고 이전보다 빠르게 이해가 갔습니다.</p>
<p>지적은 언제나 환영입니다. 긴 글 읽어 주셔서 감사합니다!</p>
<p>자세한 내용을 공부하기 좋은 곳들</p>
<p><strong>리플렉션</strong></p>
<ul>
<li><p><a href="https://jeong-pro.tistory.com/234">Link</a></p>
</li>
<li><p><a href="https://hudi.blog/java-reflection/">Link</a></p>
</li>
<li><p><a href="https://kmongcom.wordpress.com/2014/03/15/%EC%9E%90%EB%B0%94-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%A4%ED%95%B4%EC%99%80-%EC%A7%84%EC%8B%A4/%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98">Link</a></p>
</li>
</ul>
<p><strong>어노테이션</strong></p>
<ul>
<li><p><a href="https://www.nextree.co.kr/p5864/">Link</a></p>
</li>
<li><p><a href="https://jake-seo-dev.tistory.com/67">Link</a></p>
</li>
</ul>
<p><strong>어노테이션 프로세서</strong></p>
<ul>
<li><p><a href="https://www.baeldung.com/java-annotation-processing-builder">Link</a></p>
</li>
<li><p><a href="https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html">Link</a></p>
</li>
<li><p><a href="https://medium.com/@jason_kim/annotation-processing-101-%EB%B2%88%EC%97%AD-be333c7b913">Link</a></p>
</li>
</ul>
<p>더 공부하면 좋을 것들</p>
<ul>
<li>Retension의 CLASS 정책은 왜 필요한가? </li>
<li>리플랙션 </li>
<li>어노테이션 프로세서</li>
<li>어노테이션 처리 과정</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>