<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>choi-hyk.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 28 Dec 2025 08:28:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>choi-hyk.log</title>
            <url>https://velog.velcdn.com/images/choi-hyk/profile/5800b0e9-9717-4248-9fe1-fa1bd8308def/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. choi-hyk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/choi-hyk" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[GitHub Pages] 디자인 개선 및 서버리스로 변경]]></title>
            <link>https://velog.io/@choi-hyk/GitHub-Pages-%EB%94%94%EC%9E%90%EC%9D%B8-%EA%B0%9C%EC%84%A0-%EB%B0%8F-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4%EB%A1%9C-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@choi-hyk/GitHub-Pages-%EB%94%94%EC%9E%90%EC%9D%B8-%EA%B0%9C%EC%84%A0-%EB%B0%8F-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4%EB%A1%9C-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Sun, 28 Dec 2025 08:28:39 GMT</pubDate>
            <description><![CDATA[<h1 id="🖥️-github-pages-리펙토링">🖥️ Github Pages 리펙토링</h1>
<p>아주 오랜만에 <strong>Github Pages</strong> 글을 작성하는 것 같다. 이번 글에는 최근에 내가 진행한 GitHub Pages 디자인 개선과 서버리스 구조로 변경한 작업을 정리해보려고 한다.</p>
<h2 id="🛠️-serverless">🛠️ Serverless...</h2>
<p>원래는 백엔드를 하나 두어서, 일정과, Velog 데이터 및 Github 데이터를 가져오도록 구성을 해두었다. 그런데  AWS 프리티어로 구동 중이다 보니 관리도 너무 힘들고, 서버가 먹통이 되거나 이런 현상이 많았다. 그래서 과감하게 일정 페이지를 삭제하고 단순히 렌더링 타임에 Velog 및 Github 데이터를 호출하도록 바꾸었다. 구매해둔 DNS는 다른 프로젝트에서 사용할 예정이다.</p>
<h2 id="📌-velog-데이터-가져오기">📌 Velog 데이터 가져오기</h2>
<p>GitHub 데이터는 단순히 내 계정의 정보와 public repo 정보를 가져오면 되므로, API를 호출하면 된다. 그런데 문제는 Velog 데이터를 가져오는 것이었다. Velog는 RSS로 가져오거나, GraphQL로 가져오는 방식 두개가 있는데, 상세한 정보를 가져오고 싶으면 GraphQL을 가져와야 한다. 나는 글의 태그들도 가져오기를 원해서 GraphQL 방식을 채택하였다. <strong>그런데 Velog의 GraphQL은 브라우저 호출을 허용하지 않기에 다른 방법을 찾아야 했다.</strong></p>
<p>나는 예전에 구성한 <a href="https://github.com/choi-hyk/velog_sync">VelogSync</a> 프로젝트로 내 Github repo에 Velog 글들을 자동 백업을 하고 있다.</p>
<p>관련글</p>
<ul>
<li><a href="https://velog.io/@choi-hyk/Mini-Project-Velog-Backup-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0">[Mini Project] Velog Backup 프로그램 만들기</a></li>
</ul>
<p>따라서 해당 repo의 정보를 가져오는 방식으로 구성을 하였다. 간단하게 해결이되어서 다행이지만, 일단 시간당 60회 호출 제한이 있긴 해서, 완전하지는 않다. 이 부분은 나중에 개선하기로 하겠다.</p>
<h2 id="디자인-개선">디자인 개선</h2>
<p>디자인 개선은 Codex CLI의 힘을 빌러서 진행했다. Codex CLI에 최근 트렌드에 맞춰서 디자인 개선을 진행하도록 프롬프팅을 하고 작업을 하였다. </p>
<h3 id="profile-page">Profile Page</h3>
<img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/0ae752ff-b615-45a4-8497-e3dfa2d3bdc4" />

<h3 id="github-page">Github Page</h3>
<img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/3610afc2-e17a-426e-bf2b-1898164f4509" />

<h3 id="velog-page">Velog Page</h3>
<img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/fe236b1f-be49-4568-a3c7-9c9670c965cd" />
<img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/17fa2bfb-e8e2-4326-9959-30cb2c141abc" />

<p><strong>Codex CLI</strong>를 써보니 나같은 디자인 잼병이나 프론트에 익숙하지 않은 주니어 개발자 혹은 백엔드 개발자한테 매우 좋은 툴인 것 같다. 백엔드를 구현할 때도 사용을 하지만, 프론트를 구현할 때 강력한 기능을 제공하는 것 같다. 로그인 페이지나 이번에 구성한 프로필 혹은 기타 카드로 이루어진 매우 심플한 페이지를 구성할 때, 시간 단축과 세련된 디자인을 제시해준다. <strong>Codex CLI</strong>에 대해서는 따로 글로 작성해서 MCP 도구 세팅과 유용한 사용법을 정리할 생각이다.</p>
<h2 id="😁-마무리">😁 마무리</h2>
<p>이렇게 서버리스로 바꾸고 디자인 개선을 진행해보니, 한결 마음이 편해진 것 같다. AWS도 빨리 정리해서 지금 진행중인 HippoBox 프로젝트에서 해당 DNS와 서버를 사용하도록 바꿔야 겠다.</p>
<p><strong>Github Page</strong>
<a href="https://choi-hyk.github.io./#/profile">choi-hyk.github.io</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HippoBox] HippoBox 시작하기]]></title>
            <link>https://velog.io/@choi-hyk/Project-HippoBox-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choi-hyk/Project-HippoBox-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 20 Dec 2025 09:00:59 GMT</pubDate>
            <description><![CDATA[<h1 id="최신-트렌드를-익히기-위한-프로젝트">최신 트렌드를 익히기 위한 프로젝트...</h1>
<p>너무 오랜만에 Velog 글을 작성하는 것 같다. 거의 2개월 만에 작성하는 것 같은데, 바빠서 작성을 못한 것은 아니고, 뭔가 열정이 없어져서 미루다 보니 이렇게 된 것 같다.</p>
<p>그래도 최근에 개인 프로젝트를 하나 시작해서 해당 프로젝트에 대해서 글을 작성할 생각이다. 시작한 프로젝트는 HippoBox라는 이름의 프로젝트이다. 프로젝트 내용은 <strong>여러 AI 서비스에서 사용 가능한 지식 베이스 서비스</strong>를 구성하는 것이다.</p>
<p>프로젝트를 시작한 이유는 최신 개발 트렌드를 익히고, AI 서비스와 접목시킬 수 있는 프로젝트가 뭐가 있을지 고민해 보았을 때, 지식 베이스를 통합시켜 서비스를 제공한다는 것이 괜찮은 아이디어 같아서 진행하게 되었다. 그리고 Github의 유명 오픈소스들을 토대로 워크플로우를 구성할 생각이다. 이 과정에서 영어 실력도 좀 늘리고, 실무에서 진행하는 패턴들을 익혀서 적용할 생각이다.</p>
<hr>
<h2 id="🛠️-stack">🛠️ Stack</h2>
<p>일단 <code>FastAPI</code>를 기반으로 빠르게 <strong>MCP 서버</strong>를 제공하는 것이 목표이다 (사실 이미 백엔드 환경은 얼추 구현함...). <code>FastAPI</code>에서 MCP 서버를 구성하도록 도와주는 라이브러리를 찾아보니 대표적으로 <code>FastMCP</code>와 <code>fastapi-mcp</code> 두 개가 있었다. <code>FastMCP</code>는 간단하게 도구 기능을 <code>FastAPI</code> 객체에 주입하여 MCP 엔드포인트를 만들어주는 라이브러리이다. <code>fastapi-mcp</code>는 라우터 자체를 도구로 호출할 수 있도록 해주는 라이브러리이다. 나는 <code>fastapi-mcp</code>를 선택하였으며, 이유는 단순히 MCP를 통해 서비스를 호출하는 것이 아닌, 프론트 서비스도 같이 제공하기 위해서이다. 그러기 위해서 API와 MCP 호출을 통합시키는 것이 개발에 편할 것이라고 판단하였다.</p>
<p>프론트 서비스는 아직 스택을 정하진 않았다. <strong>Svelte</strong>와 <strong>React</strong> 중에 고민 중인데, 아마 <strong>React</strong>로 진행할 것 같다.</p>
<hr>
<h2 id="🛠️-서비스-구조">🛠️ 서비스 구조</h2>
<p>서비스 구조는 매우 간단하다. 단순하게 지식 베이스 저장소를 제공하는데, 일단 데이터베이스와 벡터 데이터베이스 두 개를 제공하여, 자신의 지식 베이스에서 임베딩 검색이 가능하도록 하는 것이 주된 목적이다.</p>
<p>따라서 사용자는 ChatGPT, Claude, Cursor 등 여러 가지 AI 플랫폼에서 자신의 지식을 관리할 수 있게 하는 것이 최종적인 프로젝트 목표이다. 일단 개발자 위주의 서비스 제공이지만, 추후에 해당 AI 서비스들이 확장되어서, 일반 사용자들도 여러 가지 AI 서비스들에서 자신의 저장된 지식을 제한 없이 사용할 수 있게 하는 것이 최종적인 목적이다.</p>
<hr>
<h2 id="😘-마무리">😘 마무리</h2>
<p>앞으로 해당 프로젝트에 대해서 글을 작성할 생각이다. 현재는 일단 <code>User</code>와 <code>Knowledge</code> API는 구성이 완료되었고, MCP 호출 동작도 확인한 상태이다. 해당 내용은 다음 글에서 다룰 예정이다.</p>
<p><a href="https://github.com/HippoBox">Github 프로젝트 URL</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[FastAPI] sync/async 의 논리적 구조]]></title>
            <link>https://velog.io/@choi-hyk/Python-syncasync-%EC%9D%98-%EB%85%BC%EB%A6%AC%EC%A0%81-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@choi-hyk/Python-syncasync-%EC%9D%98-%EB%85%BC%EB%A6%AC%EC%A0%81-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 09 Oct 2025 08:58:43 GMT</pubDate>
            <description><![CDATA[<h1 id="🖥️-시작하기에-앞서">🖥️ 시작하기에 앞서...</h1>
<p>이제 회사에서 본격적으로 개발 일을 시작한지 1달 반이 다 되어간다. 현재 <code>Svelte</code>와 <code>FastAPI</code> 기반의 Monolothic 구조의 프로젝트를 유지보수 하고 있는데, 해당 프로젝트에서 이해가 안되는 부분이 매우 많이 있다. 특히 <code>FastAPI</code>의 <code>coroutine</code>을 통한 라우터 설정에 애를 먹고 있는데, 규모가 큰 오픈소스다 보니까, 어떤 기준으로 해당 함수는 <code>async</code>를 통해 코루틴 처리를 하였는지, 어떤 함수는 일반 함수 정의를 통해 <code>thread pool</code>로 관리하는지 이해가 안되고 있다. </p>
<p>아마도 나 같은 신입 개발자들은 이러한 동시성 관리가 익숙치 않을 것이다. 신입 개발자들은 다른 WAS 프레임워크들 또한 이러한 동시성 관리, 더 나아가 <code>Python</code> 기반이 아닌 <code>Spring Boot</code> 같은 다른 언어 진영의 병렬 처리 같은 물리적 구조를 고려한 프로그래밍을 경험할 기회가 없다. 본인이 개인 프로젝트나, 백엔드 서비스를 개발한 경험이 있어도 실무에서 요구하는 것 과는 분명히 차이가 있을 것이다. </p>
<p>물론 현재 나는 <code>FastAPI</code>를 사용 중이어서, 고 수준의 병렬 처리는 고려할 상황이 아니지만, 개발을 하면서, 성능 개선을 위해 동시성 강화같은 이슈를 처리를 하면 벽이 느껴진다... 심지어 이러한 동시성 관리와 성능 개선을 위해 <code>ThreadPool</code>을 사용해 블록킹 함수를 추가를 하면, 이러한 수정이 오히려 전체 프로젝트의 성능에 어떠한 영향을 미칠지 모르겠는 경우가 허다하다. 그래서 이번 시리즈에서는 <code>ASGI</code> 기반의 서버인 <code>FastAPI</code>가 어떤식으로 동시성을 관리하는지와 다른 서버 프레임워크들과 비교를 통해 어떤 식으로 요청을 받고, 처리하는지 정리를 해보려고 한다. </p>
<hr>
<h2 id="🛠️-blocking-vs-non-blocking">🛠️ Blocking vs Non-Blocking</h2>
<p>알아보기 전에 <code>Blocking</code>과 <code>Non-Blocking</code>에 대해서 자세히 알아볼 필요가 있다. 많은 사람들이 오해를 하는 것이 <code>Blocking</code> 함수와 <code>Non-Blocking</code> 함수의 구분 방법이다. 두 함수는 개발자가 저수준의 구현을 통해서 <code>Blocking</code>, <code>Non-Blocking</code>을 설정을 하는 것이 아닌, 기존에 존재하는 라이브러리나 구문을 통해서 설정된다. 또한 기본적인 함수들은 동기 실행을 가정한다. 여기서 가장 오해하는 부분이 동기와 <code>Blocking</code> 그리고 비동기와 <code>Non-Blocking</code>의 관계이다. 앞에서 이야기한 <strong>&quot;기본적인 함수들은 동기 실행을 가정한다&quot;</strong> 의 의미는 기본적인 함수들은 모두 동기로 처리된다는 의미이다. 당연한 이야기지만, 여기에 <code>Blocking</code>과 <code>Non-Blocking</code>을 고려해보자.</p>
<h3 id="cpu-bound-vs-io-bound">CPU bound vs I/O bound</h3>
<p><code>Blocking</code>은 <strong>I/O 바운드</strong> 를 통한 <strong>쓰레드의 대기 상태</strong>를 의미한다. 그러면 <strong>I/O 바운드</strong>가 아닌 <strong>CPU 바운드</strong>를 생각해보자 만약 개발자가 루프문을 $O(n^3)$ 의 시간복잡도 동안 실행한다고 해보자. 이 상황에서도 <code>Blocking</code> 이라고 할 수 있는가? 루프문을 실행하는 동안 같은 쓰레드 내의 다른 함수들은 실행되지 않지만, <strong>분명 해당 쓰레드는 실행 중</strong>이다. 이러한 쓰레드 내의 작업을 <strong>CPU 바운드</strong>라고 한다. 뭔가 거창하게 설명했지만, 그냥 일반적인 함수 실행이다... </p>
<h3 id="비동기와-non-blocking의-논리적-실행">비동기와 Non-Blocking의 논리적 실행</h3>
<p>그러면 다시 <code>Blocking</code> 함수로 돌아가서 <strong>I/O 바운드</strong>는 정확히 무엇을 의미를 할까? 우리가 서버 환경에서 클라이언트로부터 요청을 받고, 데이터베이스에서 사용자의 정보를 확인한다고 해보자.</p>
<pre><code class="language-python">def get_user_from_db(username: str):
    conn = sqlite3.connect(&quot;users.db&quot;)
    cursor = conn.cursor()
    cursor.execute(&quot;SELECT username, password FROM users WHERE username = ?&quot;, (username,))
    user = cursor.fetchone()
    conn.close()
    return user

@app.post(&quot;/login/blocking&quot;)
def login_blocking(username: str, password: str):
    user = get_user_from_db(username)
    if not user or user[1] != password:
        raise HTTPException(status_code=401, detail=&quot;Invalid credentials&quot;)
    return {&quot;message&quot;: f&quot;Welcome, {username}!&quot;}</code></pre>
<p>위의 함수는 동기 상태로 정의된 <code>FastAPI</code> 라우터이다. 위의 <code>get_user_from_db()</code>에서 내부 <strong>sqlite db</strong>에서 DML을 실행 중이다. 이때, <code>cursor.execute(&quot;SELECT username, password FROM users WHERE username = ?&quot;, (username,))</code> 는  CPU 바운드인가 I/O 바운드인가? 답은 <strong>I/O 바운드</strong>이다. 해당 함수를 실행을 하면, 현재 쓰레드는 추가적인 작업이 필요한지를 생각해보면, 전혀 아니다. <strong>현재 쓰레드에서는 단순히 해당 함수가 끝나길 기다릴 것이다. 즉 <code>Waiting</code> 상태가 된다.</strong> 그리고 <strong>CPU는 해당 DML을 수행하고 있는 DBMS가 점유</strong>할 것이다. 그리고 작업이 끝나면, 함수를 반환하고, 다시 현 쓰레드를 실행할 것이다. 다시 말해, <strong>서버의 스레드는 멈춰 있고, DBMS 프로세스가 디스크에서 데이터를 읽거나 쓰는 작업을 수행</strong>하는 것이다. 작업이 완료되면 DBMS는 결과를 반환하고, 커널은 대기 중이던 쓰레드를 깨워 이전의 함수 실행 지점부터 코드를 이어서 수행한다. 즉, 코드 상으로는 함수가 멈춰 있는 것처럼 보이지만, 실제로는 쓰레드가 CPU를 전혀 사용하지 않고, 외부 자원(디스크)의 응답을 기다리는 <strong>I/O Bound + Blocking</strong> 상황이 발생한 것이다. 그러면 생각해보자 여기서 어떻게 성능을 개선 할 수가 있을까?</p>
<p>위의 코드처럼 라우터에 1개의 요청 또는 1개의 <code>Blocking</code> 함수만 있으면 별로 상관이 없을 것이다. 이번에는 <code>Blocking</code> 함수가 여러 개가 있다고 생각해보자. </p>
<pre><code class="language-python">def get_user_from_db(username: str):
    conn = sqlite3.connect(&quot;users.db&quot;)
    cursor = conn.cursor()
    time.sleep(1)
    cursor.execute(&quot;SELECT username, password, info, history FROM users WHERE username = ?&quot;, (username,))
    user = cursor.fetchone()  
    conn.close()
    return user

@app.post(&quot;/login/blocking&quot;)
def login_blocking(username: str, password: str):
    user = get_user_from_db(username)
    if not user or user[1] != password:
        raise HTTPException(status_code=401, detail=&quot;Invalid credentials&quot;)
    return {&quot;message&quot;: f&quot;Welcome, {username}!&quot;}


@app.post(&quot;/info/blocking&quot;)
def get_user_info_blocking(username: str):
    user = get_user_from_db(username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
    return {&quot;username&quot;: user[0], &quot;info&quot;: user[2]}


@app.post(&quot;/history/blocking&quot;)
def get_user_history_blocking(username: str):
    user = get_user_from_db(username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
    return {&quot;username&quot;: user[0], &quot;history&quot;: user[3]}</code></pre>
<p>위의 함수에 사용자 3명이 동시다발적으로 3개의 요청을 각각 보낸다고 생각해보자. </p>
<blockquote>
<p><code>FastAPI</code> 에서는 일반 def 요청은 쓰레드 풀의 개별적인 쓰레드 워커에서 실행된다. 해당 부분은 다음 글에서 자세히 설명을 하고 지금은 단일 쓰레드에서 실행되는 것으로 가정 하겠다.</p>
</blockquote>
<p>8000포트에서 Listen 중인 상태로 프로세스를 실행하고, 사용자1이 <code>login_blocking()</code>, 사용자2가 <code>get_user_info_blocking()</code> 그리고 사용자3이 <code>get_user_history_blocking()</code>을 순서대로 요청을 보냈다고 가정하자. 또한 DML 실행시간은 1초라고 가정하자. 그리고 <strong>Task Queue</strong>에는 실행 프로세스의 쓰레드가 Task로 들어간다고 가정하자. 여기서 고려해야 되는 부분은,<strong>쓰레드가 하나이므로 만약 쓰레드가 Waiting 상태가 되면, 요청을 받지 못한다는 것이다.</strong> 간단히 생각해보면, 이미 실행 중인 프로세스에 추가적인 작업이 쌓이는 것을 생각하면 된다. 메인 워커 쓰레드를 $W$이라 하겠다. 또한 각 Task Queue에는 쓰레드 단위로 Task가 들어간다고 가정하겠다. </p>
<p>OS 수준에서는 1개의 프로세스와 1개의 단일 쓰레드를 <strong>Task Queue</strong>에서 실행 중이지만, 프로세스 관점에서는 프로세스에 추가적인 작업이 쌓이고 있다. 사용자1의 <code>login_blocking()</code>가 들어오는 순간 현재 쓰레드는 약 1초간 <code>Waiting</code> 상태가 될 것이다. <code>Waiting</code> 이 되는 1초 동안 나머지 2개의 요청을 받았다고 가정하자. 또한 2개의 코어로 병렬 처리가 된다고 가정하자.</p>
<img width="817" height="749" alt="Image" src="https://github.com/user-attachments/assets/ad66382d-5aaa-421e-a2ef-d9a71f8e1318" />

<p>위의 다이어그램은 Request를 받았을 때, 요청과 단일 쓰레드가 어떻게 처리되는지 보여준다. 현재 Blocking 함수는 <code>time.sleep()</code>, <code>cursor.execute()</code> 가 존재한다.  또한 $T_n$에서 $n$ 은 초 단위라고 가정을 하겠다. <code>accept queue</code>는 생소할텐데, listen 상태의 소켓을 지니고 있는 프로세스가 보유하는 큐로 아직 애플리케이션 레벨에서 <code>accept()</code> 호출로 가져가지 않은 연결들이 일시적으로 쌓여 있는 공간이다. 위의 그림을 보면, 당연하게도 $R_2$, $R_3$는 $R_1$이 처리되어야지 순서대로 처리될 것이다. 그러면 약 $T_4$에 모든 처리가 완료 될 것이다. 2개의 코어가 존재해도 $W$ 가 waiting이 되어 있으면, DB process가 실행 중일때 나머지 코어에 $W$를 실행하지 못할 것이다.</p>
<p>그러면 여기서 개선을 어떻게 할까? <code>FastAPI</code> 는 ASGI 기반이다. 이 말은 모든 요청을 async 인터페이스로 받는 웹 서버를 가정하는 프레임워크란 뜻이다. 위의 다이어그램을 봤을 때, 비동기로 처리를 한다고 하면, $R_1$ 처리 중에 다른 요청을 처리 할 수 있게 해야 한다. 여기서 사용하는 것이 바로 <code>async</code> 함수 내의 <code>await</code> 구문이다. 그리고 해당 구문으로 <code>Non-Blocking</code>의 진정한 의미를 알 수 있는데, 바로 <strong>실행 제어권을 반납하는 것</strong>이다. 매우 간단하다. <code>async</code> 함수 내에서 <code>await</code>를 만나면 해당 함수를 <code>Non-Blocking</code> 으로 실행하겠다는 의미이다. 그럼 여기서 드는 생각이, 결국에는 <code>FastAPI</code> 같은 ASGI 기반의 웹서버는 애플리케이션 수준에서 비동기를 지원하는 것이다. 즉 <strong>단일 쓰레드의 코루틴 내에서 비동기를 지원하여 동시성을 강화하는 것</strong>이다. 위의 다이어그램이 비동기로 처리될때, 차이점은 <strong>I/O 바운드 작업시에 $W$를 Waiting 상태에 빠지지 않도록하는 Non-Blocking 처리</strong>만 존재한다. 그러면 이게 어떻게 가능할까? 단일 쓰레드 내에서도 스케줄러같은 실행 처리를 도와주는 로직이 있는 것일까?</p>
<h3 id="coroutine">Coroutine</h3>
<p>Coroutine이 바로 이 비동기 처리를 구현하는 기법이다. OS는 Context switch같은 기법을 통해 동시성을 강화한다. 그럼 OS가 비동기를 처리한다고 할 수 있을까? 절대 아니다. 이유는 <strong>동시성 강화는 비동기 처리가 아니기 때문이다. 하지만 비동기 처리는 동시성을 강화하는 기법 중 한가지이다</strong>. OS가 동시성을 강화하는 이유는 여러가지의 작업을 효울적으로 처리하기 위해서다. 이를 통해 사용자는 작업이 동시에 이루어지는 환상을 만들어준다. <strong>하드웨어적인 관점으로 자원을 최소한으로 사용하여 가장 효율적인 스케줄을 통해 프로세스를 관리하는 것</strong> 이것이 목적이다. 그러면 Coroutine을 통한 비동기 처리는 무엇이 목적일까? 말한대로 동시성 강화가 목적이다. 하지만 하드웨어적인 관점에서 굳이 애플리케이션 수준에서 자원의 효율성같은 요소를 신경쓰지는 않을 것이다. 주요 목적은 위에서 말한 것 처럼, <strong>I/O 바운드 작업시에 코루틴 쓰레드를 Waiting 상태에 빠지지 않도록하는 Non-Blocking 처리를 하는 것이다.</strong> 이를 통해 Waiting이라는 요소를 제외하고 실행이 가능하다. 그리고 이것은 <strong>Event Loop를 통해 단일 쓰레드의 Call Stack의 Task switch로 이루어진다.</strong> 이는 OS 수준의 Context switch와 비슷한 기법이다. Event Loop 는 CPU 스케줄러 그리고 Task Switch는 Context switch로 비유할 수 있을 것이다. </p>
<blockquote>
<p>Coroutine은 OS처럼 물리적인 스케줄링을 수행하는 것이 아난, 단일 스레드 내부에서 실행 흐름을 논리적으로 전환(switch)하여 동시성을 달성하는 방식</p>
</blockquote>
<blockquote>
<p>OS의 Context switch가 커널이 직접 개입하여 CPU 레지스터, 프로그램 카운터, 스택 포인터 등 하드웨어 상태를 저장하고 복원하는 무거운 전환이라면, Coroutine의 Task switch는 단지 함수의 실행 위치와 로컬 상태를 저장하고 이벤트 루프가 다음 코루틴을 재개(resume)하는 가벼운 사용자 레벨 전환이라 할 수 있다.</p>
</blockquote>
<p>따라서 <strong>Coroutine은 커널이 아닌 애플리케이션 레벨에서 구현된 경량화된 동시성 메커니즘</strong>이며, Context switch의 하드웨어적 문맥 교환에 대응되는 소프트웨어적 제어 흐름 교환(Control-flow switching) 이라고 할 수 있다.</p>
<p>위의 다이어그램을 통해 비동기 처리가 구현된 다이어그램을 살펴보겠다. 이를 위해서 <code>async</code> 함수에서 <code>time.sleep()</code> 은  <code>await asyncio.sleep()</code> 로 바꾸고, DML 함수도 비동기 처리를 해야 한다. 이렇게 비동기 처리가 완료되면 다이어그램을 아래와 같을 것이다.</p>
<img width="1016" height="754" alt="Image" src="https://github.com/user-attachments/assets/c1df682b-aa75-42fe-be77-4b4bfe25b7f1" />

<p><code>asyncio sleep()</code> 과 <code>cursor.execute()</code> 같은 I/O 바운드가 실행되면, Event Loop는 등록된 다른 코루틴을 실행한다. 즉 위의 그림에서 coroutine1 이 <code>asyncio sleep()</code>을 통해 대기 상태에 들어가면  Event Loop는 Accept Queue에서 바로 $R_2$를 가져와서 coroutine2로 실행을 한다. 이때 오해를 하면 안되는 것이, coroutine은 <strong>병렬 실행이 절대 아니란 점</strong>이다. 위에 그림만 보면 오해를 할 수도 있지만, coroutine이 실행되는 로직은 기존에 실행 중이던 coroutine이 I/O 바운드로 인해 대기 상태에 들어갔을때만, 실행이 되는 <strong>동시성 강화</strong>이다.  따라서 위에서 OS 수준의 Context switch로 비유한 이유가 바로 이러한 Event Loop를 통한 coroutine 실행 관리 로직 때문이다. 이러한 동시성 강화를 통해 약 $T_{1.5}$ 에 모든 실행이 완료되는 것을 볼 수 있다. 기억하자 <strong>coroutine은 단일 쓰레드 내에서 이루어지는 비동기를 통한 동시성 강화 기법</strong>이라는 것을.</p>
<blockquote>
<p>비동기 코루틴은 단일 스레드 내에서 오직 하나의 Call Stack 위에서만 실행되며, 동시에 여러 coroutine이 CPU를 점유하는 일은 없다.</p>
</blockquote>
<p>OS 수준에서는 단순히 $W$를 실행하고 있으면 되고 이러한 비동기 처리는 애플리케이션 수준의 쓰레드 내부에서 전부 이루어지는 추상화가 ASGI 아키텍처의 철학이다. </p>
<hr>
<h2 id="😘-마무리">😘 마무리</h2>
<p><strong>비동기 모델에서의 동시성 강화는 OS 수준의 선점형 스케줄링(preemptive scheduling) 이 아닌, Event Loop를 중심으로 한 협력형(Cooperative) 스케줄링</strong> 에 의해 이루어지고 Event Loop는 OS의 CPU 스케줄러에 대응되는 역할을 수행하며, coroutine 간 전환(Task Switching)은 커널 수준의 Context Switch 대신 사용자 레벨에서 수행되는 <strong>가벼운 실행 흐름 전환(Control-flow switching) 으로 처리된다.</strong> 이번 글에서는 FastAPI를 비롯한 비동기 처리가 논리적으로 어떻게 이루어지는 지와 OS 수준과 애플리케이션 수준에서 헷갈리지 않도록 설명을 해보았다. 다음 글에서는 FastAPI의 비동기 처리 로직을 코드를 통해 알아보고 일반 def 선언은 어떻게 처리되는지 그리고 lifespan을 통한 coroutine 처리를 심도있게 다뤄보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] Pydantic 부시기]]></title>
            <link>https://velog.io/@choi-hyk/Python-Pydantic-%EB%B6%80%EC%85%94%EB%B2%84%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@choi-hyk/Python-Pydantic-%EB%B6%80%EC%85%94%EB%B2%84%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Wed, 10 Sep 2025 15:11:21 GMT</pubDate>
            <description><![CDATA[<h1 id="pydantic-✨">Pydantic ✨</h1>
<p>오늘은 <code>Pydantic</code>에 대해서 알아보려고 한다. 최근에 오픈소스로 이루어진 프로젝트들을 보면, 백엔드 서버를 <code>FastAPI</code>를 사용하는 경우가 많은데, Pydantic은 <code>FastAPI</code>에서 데이터 스키마를 정의하고 데이터 직렬화/역직렬화를 위해 많이 사용하는 라이브러리이다. 최근 회사에서 오픈소스를 활용한 프로젝트를 유지보수하는 업무를 하고 있는데, 해당 프로젝트가 나는 별로 사용해본 적이 없는 <code>Svelte</code>를 프론트로 사용 중이고 백엔드는 <code>FastAPI</code> 기반의 <strong>Monolithic</strong> 아키텍처를 구성하고 있다. 당연하게도 해당 프로젝트에서 백엔드는 <code>Pydnatic</code>으로 DTO를 구성하고 있다. <code>Pydantic</code>은 얼핏보면 간단해 보이지만, 수 많은 ASGI 코드들과 정의되어 있는 스키마를 보면 어지러워 질때가 있다. 그래서 해당 라이브러리에 익숙해질 필요를 느껴서 이렇게 정리를 해보려고 한다.</p>
<p>찾아보니 <code>Pydantic</code>은 처음에는 <strong>Samuel Colvin</strong> 이라는 사람이 2018년 쯤에 Python 환경에서 타입 힌트화를 통한 데이터 무결성 보장과 타입 직렬화/역직렬화를 지원하기 위해 만들었다고 한다. <del>GPT 피셜</del>. 나중에 FastAPI에서 공식적으로 Pydantic을 스키마 라이브러리로 채택하면서 널리 쓰이게 됐다고 한다. 특히, 데이터 스키마 정의를 통해 API 기반의 백엔드 서버의 라우터 문서화 <strong>(OpenAPI/Swagger)</strong> 를 자동화 하는 점이 큰 장점이다.  </p>
<hr>
<h2 id="사용법-🛠️">사용법 🛠️</h2>
<p>거두절미하고 바로 사용법을 알아보겠다. Pydantic을 써보면서 느낀점은 사용자 입맛대로 강력한 데이터 강제성을 주입 시킬 수 있다. 참고로 사용한 <code>Pydantic</code> 버전은 2.9.2 이다.</p>
<h3 id="basemodel">BaseModel</h3>
<pre><code class="language-python">class User(BaseModel):
    id: int = Field(
        default_factory=lambda: int(uuid.uuid4()),
        description=&quot;사용자의 고유 ID&quot;,
    )
    name: str = Field(
        ..., min_length=1, max_length=20, description=&quot;사용자의 이름 (1~20자)&quot;
    )
    email: str = Field(..., description=&quot;사용자의 이메일 주소&quot;)
    age: Optional[int] = Field(None, ge=0, description=&quot;사용자의 나이 (0 이상)&quot;)

    def __str__(self):
        return f&quot;User(id={self.id}, name=&#39;{self.name}&#39;, email=&#39;{self.email}&#39;, age={self.age})&quot;

    def to_model_dump(self):
        return self.model_dump()

    @classmethod
    def from_model_dump(cls, data):
        return cls.model_validate(data)

    @model_validator(mode=&quot;before&quot;)
    def check_email(cls, values):
        email = values.get(&quot;email&quot;)
        if email and &quot;@&quot; not in email:
            raise ValueError(&quot;Invalid email address&quot;)
        return values
</code></pre>
<p>이제 위의 코드를 보면 좀 어지러워 질텐데, 일단 <code>User</code> 스키마만 살펴보자.</p>
<pre><code class="language-python">class User(BaseModel):
    id: int 
    name: str
    email: str 
    age: int</code></pre>
<p>위의 스키마를 최대한 간단하게 정의하면 이렇게 작성할 수 있다. 먼저 <code>BaseModel</code> 은 <code>Pydantic</code>에서 해당 클래스가 스키마라는 것을 정의해주는 기본 클래스이다. 해당 클래스를 상속 함으로서 <code>User</code> 는 <code>Pydantic</code> 의 데이터 검증과 직렬화/역직렬화를 사용 가능하다. </p>
<pre><code class="language-python">user = User(id=&quot;123&quot;, name=&quot;Alice&quot;, email=&quot;user@example.com, age=&quot;25&quot;)
print(user) </code></pre>
<p>위 처럼 <code>User</code>를 정의했다고 생각해보자, 현재 <code>id</code> 와 <code>age</code> 는 <code>int</code> 형인데 <code>str</code> 형이 할당 되어있다. 마치 <code>JavaScript</code> 의 타입 캐스팅 처럼 <code>Pydantic</code> 은 바꿀 수 있는 타입은 알아서 바꿔 준다. 위의 경우에는 문제 없이 <code>int</code> 형으로 바뀔 것이다. 그러나 만약 &quot;one two three&quot; 같은 것이 할당되어 있으면, <code>ValidationError</code>를 발생 시킨다.</p>
<h3 id="field">Field</h3>
<pre><code class="language-python">class User(BaseModel):
    id: int = Field(
        default_factory=lambda: int(uuid.uuid4()),
        description=&quot;사용자의 고유 ID&quot;
    )
    name: str = Field(
        ..., min_length=1, max_length=20, description=&quot;사용자의 이름 (1~20자)&quot;
    )
    email: str = Field(
        ..., description=&quot;사용자의 이메일 주소&quot;
    )
    age: Optional[int] = Field(
        None, ge=0, le=150, description=&quot;사용자의 나이 (0~150)&quot;
    )</code></pre>
<p>이번에는 <code>Field</code>에 대해서 알아보자. <code>Field</code>는 일종의 데이터 명세서로 단순히 타입만 지정했을 때보다 훨씬 세밀하게 제약조건과 메타데이터를 설정할 수 있게 해준다. 위 코드를 보면, 모든 필드가 <code>description</code> 을 통해 필드 설명을 제공 중이다. 이 값은 문서화가 되었을 때, API 설명 부분에 자동을 할당된다.</p>
<p>각 필드를 살펴보면, <code>id</code>의 <code>default_factory</code> 를 볼 수 있는데, 해당 인자는 해당 필드를 동적으로 값을 생성한다는 의미이다. <code>default</code> 도 있는데, 해당 값은 동적이 아니라 정해진 값을 생성해주는 인자이다. 참고로 밑에 처럼 <code>Field</code>를 사용 안하고, <code>default</code> 선언도 가능하다.</p>
<pre><code class="language-python">class User(BaseModel):
    id: int = 10</code></pre>
<p>다음으로는 <code>...</code> 을 볼 수 있는데, 해당 값은 필수 인자라는 뜻이다. 따라서 해당 스키마를 정의할 때, 해당 값들을 할당하지 않고 정의하면 <code>ValidationError</code> 가 발생한다. </p>
<p>그 밖에도 여러가지 인자가 있는데, 밑에 표로 정리한 것을 살펴보면 이해가 편할 것이다.</p>
<h4 id="field-주요-인자-정리">Field 주요 인자 정리</h4>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>default</strong></td>
<td>기본값 지정</td>
<td><code>Field(0)</code></td>
</tr>
<tr>
<td><strong>default_factory</strong></td>
<td>동적으로 기본값 생성 (함수 실행 결과)</td>
<td><code>Field(default_factory=lambda: uuid.uuid4())</code></td>
</tr>
<tr>
<td><strong>... (Ellipsis)</strong></td>
<td>필수(required) 필드 지정</td>
<td><code>Field(...)</code></td>
</tr>
<tr>
<td><strong>title</strong></td>
<td>필드 제목 (문서화용)</td>
<td><code>Field(..., title=&quot;User ID&quot;)</code></td>
</tr>
<tr>
<td><strong>description</strong></td>
<td>필드 설명 (문서화용)</td>
<td><code>Field(..., description=&quot;사용자의 고유 ID&quot;)</code></td>
</tr>
<tr>
<td><strong>gt / ge</strong></td>
<td>숫자 크기 제한 (&gt;) / (≥)</td>
<td><code>Field(..., gt=0)</code></td>
</tr>
<tr>
<td><strong>lt / le</strong></td>
<td>숫자 크기 제한 (&lt;) / (≤)</td>
<td><code>Field(..., le=100)</code></td>
</tr>
<tr>
<td><strong>min_length / max_length</strong></td>
<td>문자열 길이 제한</td>
<td><code>Field(..., min_length=1, max_length=20)</code></td>
</tr>
<tr>
<td><strong>pattern</strong></td>
<td>정규식 패턴 검증</td>
<td><code>Field(..., pattern=r&quot;^[a-z0-9]+$&quot;)</code></td>
</tr>
<tr>
<td><strong>alias</strong></td>
<td>입력 받을 때 다른 키 이름 허용</td>
<td><code>Field(..., alias=&quot;user_id&quot;)</code></td>
</tr>
<tr>
<td><strong>deprecated</strong></td>
<td>필드가 더 이상 쓰이지 않음을 표시</td>
<td><code>Field(..., deprecated=True)</code></td>
</tr>
<tr>
<td><strong>examples</strong></td>
<td>API 문서에 예시 값 표시</td>
<td><code>Field(..., examples=[&quot;alice@example.com&quot;])</code></td>
</tr>
</tbody></table>
<h3 id="typing">Typing</h3>
<p><code>Pydantic</code> 은 <code>typing</code> 모듈의 정의 타입들을 사용하는데, 대표적으로 <code>Optional</code>이 있다.</p>
<pre><code class="language-python">class User(BaseModel):
    id: int = Field(..., description=&quot;사용자 ID (필수)&quot;)
    name: str = Field(..., min_length=1, max_length=20, description=&quot;사용자 이름&quot;)
    age: Optional[int] = Field(None, ge=0, le=150, description=&quot;나이 (없으면 None)&quot;)
    phone: Union[str, int, None] = Field(
        None, description=&quot;전화번호 (문자열 또는 숫자 허용, 없으면 None)&quot;
    )
    role: Literal[&quot;admin&quot;, &quot;user&quot;, &quot;guest&quot;] = Field(
        &quot;user&quot;, description=&quot;권한 (admin, user, guest 중 하나)&quot;
    )
    tags: List[str] = Field(default_factory=list, description=&quot;사용자 태그 목록&quot;)
    preferences: Dict[str, str] = Field(
        default_factory=dict, description=&quot;사용자 환경설정&quot;
    )</code></pre>
<p>이런식으로 타입 정의가 가능한데, 참고로 <code>Union</code> 보다는 간단하게 파이프 연산자를 사용하는 것을 추천한다. 다른 타입도 많은데, <code>FastAPI</code> 데이터 스키마에서는 이 정도면 사용하는 것 같다.</p>
<h3 id="method">Method</h3>
<p><code>Pydantic</code> 은 기본 <code>Method</code> 기능을 제공한다. 오픈소스 코드에서도 이러한 기본 함수를 적극적으로 활용하고 있어서, 반드시 알아둬야 된다.</p>
<h4 id="1-__str__">1. <strong><code>__str__</code></strong></h4>
<p>→ 객체를 print 했을 때 사람이 읽기 좋은 문자열 반환</p>
<pre><code class="language-python">from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

    def __str__(self):
        return f&quot;User(id={self.id}, name={self.name}, email={self.email})&quot;

u = User(id=1, name=&quot;Alice&quot;, email=&quot;alice@example.com&quot;)
print(u)  
# 출력: User(id=1, name=Alice, email=alice@example.com)</code></pre>
<h4 id="2-model_dump">2. <strong><code>model_dump()</code></strong></h4>
<p>→ 객체 → dict 직렬화</p>
<pre><code class="language-python">data = u.model_dump()
print(data)
# 출력: {&#39;id&#39;: 1, &#39;name&#39;: &#39;Alice&#39;, &#39;email&#39;: &#39;alice@example.com&#39;}</code></pre>
<h4 id="3-model_validate">3. <strong><code>model_validate()</code></strong></h4>
<p>→ dict → 객체 (검증 포함)</p>
<pre><code class="language-python">user_dict = {&quot;id&quot;: 2, &quot;name&quot;: &quot;Bob&quot;, &quot;email&quot;: &quot;bob@example.com&quot;}
u2 = User.model_validate(user_dict)
print(u2)
# 출력: User(id=2, name=Bob, email=bob@example.com)</code></pre>
<p>위의 함수들을 통해 <code>Pydantic</code>의 핵심 기능인, 데이터 검증과 <strong>직렬화/역직렬화</strong>를 간편하게 적용 가능하다.</p>
<h4 id="4-model_validator">4. <strong><code>@model_validator</code></strong></h4>
<p>→ 모델 생성 시 비즈니스 규칙 검증</p>
<pre><code class="language-python">from pydantic import model_validator

class User(BaseModel):
    id: int
    name: str
    email: str

    @model_validator(mode=&quot;before&quot;)
    def check_email(cls, values):
        email = values.get(&quot;email&quot;)
        if email and &quot;@&quot; not in email:
            raise ValueError(&quot;Invalid email address&quot;)
        return values

# 올바른 입력
User(id=3, name=&quot;Charlie&quot;, email=&quot;charlie@example.com&quot;)

# 잘못된 입력 → 예외 발생
User(id=4, name=&quot;Dave&quot;, email=&quot;invalid-email&quot;)
# ValueError: Invalid email address</code></pre>
<p>해당 함수는 <code>model_validate</code> 기능을 제공한다는 의미로 데코레이터로 정의 가능하다. 옆에 <code>(mode=&quot;before&quot;)</code> 는 스키마가 정의되기 전에 실행되는 함수라는 뜻이다. 따라서 이러한 검증 함수를 여러가지 만들 수가 있다.</p>
<h3 id="model-nested">Model Nested</h3>
<p>이제 <code>Pydantic</code>의 가장 강력한 기법이라 볼 수 있는 중첩을 알아보자.</p>
<pre><code class="language-python">class ProjectConfig(BaseModel):
    owner: User
    members: List[User]</code></pre>
<p>위의 방식 처럼 중첩을 사용해서 상위 스키마를 제공이 가능하다. 당연한 기능 같지만, <code>Pydantic</code>의 <code>BaseModel</code> 은 <code>Dict</code> 타입을 위에서 살펴본 모델 검증 과정을 통해 객체형으로 바꿔준다. </p>
<p>만약 위의 스키마대로 <code>Dict</code> 타입을 정의했다고 해보자</p>
<pre><code class="language-python">config = {
    &quot;owner&quot;: {
        &quot;name&quot;: &quot;Alice&quot;,
        &quot;email&quot;: &quot;alice@example.com&quot;
    },
    &quot;members&quot;: [
        {&quot;name&quot;: &quot;Bob&quot;, &quot;email&quot;: &quot;bob@example.com&quot;},
        {&quot;name&quot;: &quot;Charlie&quot;, &quot;email&quot;: &quot;charlie@example.com&quot;}
    ]
}</code></pre>
<p>위에서 정의된 <code>config</code> 에서 <code>members[0]</code> 를 살펴보려면 <code>config[&quot;members&quot;][0]</code> 으로 접근이 가능하다.</p>
<pre><code class="language-python">project = ProjectConfig.model_validate(config)</code></pre>
<p>이제 <code>BaseModel</code>의 <code>model_validate</code> 로 변환을 해보자. 그러면 <code>project</code>는 객체가 되어서 
<code>config.members[0]</code> 으로 접근이 가능하게 된다. 수 많은 <code>FastAPI</code>를 사용한 오픈소스 에서는 이러한 형태로 강력한 타입 설정을 하여, 관리를 하고있다. <code>FastAPI</code> 에서는 <code>resquest</code> 와 <code>response</code> 에 스키마를 설정하여, 자동으로 <code>JSON</code>이 직렬화 된 데이터를 객체 형태로 받게된다. </p>
<hr>
<h2 id="fastapi">FastAPI</h2>
<pre><code class="language-python">from typing import List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int 
    name: str 
    email: str 
    age: Optional[int]

class ProjectConfig(BaseModel):
    owner: User
    members: List[User]

class ProjectResponse(BaseModel):
    owner_name: str
    member_count: int

@app.post(&quot;/projects&quot;, response_model=ProjectResponse)
async def create_project(project: ProjectConfig):
    return ProjectResponse(
        owner_name=project.owner.name,
        member_count=len(project.members)
    )</code></pre>
<p>위의 코드를 보면, <code>create_project</code>에서 <code>project</code> 를 <code>ProjectConfig</code> 로 받고 있다. 만약에 클라이언트가 <code>Dict</code> 형태로 인자를 보내게 되면, 위의 포스트 라우터는 자동으로 <code>project</code>를 검증 및 변환하여 <code>ProjectConfig</code> 로 만들어 준다.</p>
<p>경로 옆 <code>response_model</code> 은 응답 타입도 정해주는 설정이다. 반환 값으로 <code>ProjectResponse</code> 스키마대로 반환을 하고 있다. 클라이언트는 해당 응답을 받으면, <code>Dict</code> 형태로 받게 된다. 이렇게도 쓸 수 있다.</p>
<pre><code class="language-python">@app.post(&quot;/projects&quot;, response_model=ProjectResponse)
async def create_project(project: ProjectConfig):
    return {
        &quot;owner_name&quot;: project.owner.name,
        &quot;member_count&quot;: len(project.members)
    }</code></pre>
<p><code>FastAPI</code> 에서 <code>Dict</code> 형으로 반환을 해도, 자동으로 스키마를 감지해준다. 컨벤션에 맞게 두가지를 조율해서 사용하면 될 것 같다. 보통의 API에서는 클라이언트는 항상 <code>JSON</code> 으로 직렬화 된 데이터를 보내므로, 이러한 데이터를 검증하고 좀 더 쉽게 관리가 가능하다. </p>
<p>참고로 상속 기능이 있긴 하지만, 상속 기능은 사용하면 너무 복잡해져서 많이 보진 못한 것 같다. 그래서 설명은 넘어가겠다.</p>
<hr>
<h2 id="마무리-😁">마무리 😁</h2>
<p>오늘은 <code>Pydantic</code>에 대해서 알아보았다. 처음에는 그냥 단순한 타입 정의 라이브러리라 생각하고, 찾아보지 않았다가, 실수나 타입 불일치 오류를 많이 보게 되었는데, 이번 기회에 제대로 알아보고 작업을 할 수 있을 것 같다. 기회가 되면, 회사에서 사용하는 스텍이나 라이브러리들을 하나씩 정리해서 학습을 해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Design Pattern] Bridge Pattern]]></title>
            <link>https://velog.io/@choi-hyk/Design-Pattern-Bridge-Pattern</link>
            <guid>https://velog.io/@choi-hyk/Design-Pattern-Bridge-Pattern</guid>
            <pubDate>Sun, 31 Aug 2025 07:51:56 GMT</pubDate>
            <description><![CDATA[<h1 id="bridge-pattern-🌉">Bridge Pattern 🌉</h1>
<p>이번에는 <code>Bridge Pattern</code> 에 대해서 알아보겠다. Bridge Pattern은 말 그대로 클래스와 클래스를 가교(Bridge)라는 관계로 정의하는 패턴이다. 한번 생각해보자, 우리가 어떠한 클래스를 상속을 통해 구현을 할때, 깊이 1에 있는 클래스들은 해당 클래스의 원형을 그대로 따라갈 것이다. 근데 만약에, 부모 클래스가(깊이 0) 새로운 개념의 서브 클래스를 생성한다 생각해보자. </p>
<img width="610" height="189" alt="Image" src="https://github.com/user-attachments/assets/efba71ae-dd25-430c-b038-8eecff148e32" />

<p>위의 이미지는 GOF 책에서 예시로 든 사용자 인터페이스 툴킷인 <code>Winodow</code> 클래스의 <strong>클래스 폭발</strong>을 보여준다. 툴킷인 <code>Window</code> 클래스를 사용해서 우리가 각 플랫폼의 특성이 반영된 <code>XWindow</code> 와 <code>PMWindow</code> 플랫폼을 구현했다고 해보자, 해당 구현만 존재하면 사용하는데는 문제가 없을 것이다. 그런데, <code>Window</code> 구현자가 새로운 기능을 담은 <code>Window</code> 인 <code>IconWindow</code> 를 출시 했다. 그러면 우리는 기존의 <code>XWindow</code> 와 <code>PMWindow</code> 를 다시 <code>IconWindow</code> 에 상속 받아서 해당 <code>Icon</code> 기능이 포함된 클래스들을 재정의 해야 한다. 매우 번거롭지 않은가?</p>
<p>그래서 사용되는 패턴이 <strong>Bridge Pattern</strong>이다.</p>
<blockquote>
<p>구현에서 추상을 분리하여, 이들이 독립적으로 다양성을 가질 수 있도록 합니다.</p>
</blockquote>
<p>구현에서 추상을 분리한다는 것은, 구현체와 추상으로 생성된 추가 클래스들을 분리한다는 것이다. 참고로 <strong>Bridge Pattern</strong>은 <strong>핸들/구현부(Handle/Body)</strong> 라는 이름으로도 불린다.</p>
<hr>
<h2 id="언제-사용하나-📌">언제 사용하나? 📌</h2>
<p>책에서는 위에서 말한 예시로 Bridge Pattern을 설명한다. </p>
<img width="576" height="359" alt="Image" src="https://github.com/user-attachments/assets/f9be37fd-6e35-4cd0-bf37-5c98a4402e4b" />

<p>이미지를 보면, <code>Window</code> 의 추상 클래스로 <code>IconWindow</code>, <code>TransientWindow</code> 가 설정되어 있고, <code>Window</code>는 <code>imp</code> 라는 구현체 인스턴스를 가지게 된다. 이 <code>imp</code> 는 <code>WindowImp</code> 를 참조하게 된다. <code>IconWindow</code>, <code>TransientWindow</code>는 기존의 <code>Winodw</code> 에서 제공하는 <code>DrawText()</code> 와 <code>DrawRect()</code> 로 자신들이 제공하는 기능을 구현하고 있다. 여기서 해당 패턴의 핵심이 나오는데, 바로 <code>WindowImp</code>는 <code>DrawRect()</code>를 4개의 <code>DevDrawLine()</code> 으로 구현 중이다. 이것이 <strong>Bridge Pattern</strong> 의 구현부의 역할이다. 구현부는 가장 <strong>저수준의 구현</strong>을 제공하고, 추상부는 해당 구현체들을 활용해서 실질적인 동작을 수행한다. 그리고 이러한 저수준의 구현을 하나의 클래스로 정의하면 해당 클래스의 서브 클래싱을 통해 여러가지 플랫폼에서 활용이 가능하다. </p>
<p>이렇게 함으로써 얻는 가장 큰 이점은, 기능(추상화 계층)과 플랫폼(구현 계층)을 각각 독립적으로 관리할 수 있다는 점이다. 기능이 늘어날 때마다 모든 플랫폼별 클래스를 다시 작성해야 하는 클래스 폭발 문제를 피할 수 있고, 새로운 플랫폼을 지원하는 것도 훨씬 수월하다.</p>
<hr>
<h2 id="구조-🏗️">구조 🏗️</h2>
<img width="600" height="246" alt="Image" src="https://github.com/user-attachments/assets/b7413f5c-919a-422a-a53b-64932b149974" />

<p>구조는 위의 예시를 이해했으면, 바로 파악이 될것이다. 정리하자면, <strong>Bridge Pattern</strong>은 상속으로 인해 <strong>기능 × 플랫폼 조합이 기하급수적으로 늘어나는 문제를 해결하기 위해, 추상 계층과 구현 계층을 분리하고, 이를 가교(imp)로 연결하는 방식</strong>이다. 이 덕분에 기능과 구현을 <strong>분리된 축(axis)</strong>으로 관리할 수 있어 확장성과 유지보수성이 크게 향상된다.</p>
<p>여기서 핵심 포인트는 <strong>추상은 고수준 동작을 정의, 구현은 저수준 세부사항을 담당, 그리고 둘은 런타임에 조합된다 라는 구조</strong>다.</p>
<hr>
<h2 id="구현-💻">구현 💻</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;memory&gt;
#include &lt;string&gt;
#include &lt;algorithm&gt;

// -------- Primitive --------
struct Point { int x{}, y{}; };

// -------- Implementor --------
class WindowImp {
public:
    virtual ~WindowImp() = default;
    virtual void DeviceRect(int x0, int y0, int x1, int y1) = 0;
    virtual void DeviceText(const char* s, int x, int y) = 0;
};

// -------- Concrete Implementors --------
class XWindowImp : public WindowImp {
public:
    void DeviceRect(int x0, int y0, int x1, int y1) override {
        int x = std::min(x0, x1);
        int y = std::min(y0, y1);
        int w = std::abs(x1 - x0);
        int h = std::abs(y1 - y0);
        std::cout &lt;&lt; &quot;[X] Rect (&quot; &lt;&lt; x &lt;&lt; &quot;,&quot; &lt;&lt; y &lt;&lt; &quot;) w=&quot; &lt;&lt; w &lt;&lt; &quot; h=&quot; &lt;&lt; h &lt;&lt; &quot;\n&quot;;
    }
    void DeviceText(const char* s, int x, int y) override {
        std::cout &lt;&lt; &quot;[X] Text \&quot;&quot; &lt;&lt; s &lt;&lt; &quot;\&quot; @(&quot; &lt;&lt; x &lt;&lt; &quot;,&quot; &lt;&lt; y &lt;&lt; &quot;)\n&quot;;
    }
};

class PMWindowImp : public WindowImp {
public:
    void DeviceRect(int x0, int y0, int x1, int y1) override {
        int left   = std::min(x0, x1);
        int right  = std::max(x0, x1);
        int bottom = std::min(y0, y1);
        int top    = std::max(y0, y1);
        std::cout &lt;&lt; &quot;[PM] Rect L=&quot; &lt;&lt; left &lt;&lt; &quot; R=&quot; &lt;&lt; right
                  &lt;&lt; &quot; B=&quot; &lt;&lt; bottom &lt;&lt; &quot; T=&quot; &lt;&lt; top &lt;&lt; &quot;\n&quot;;
    }
    void DeviceText(const char* s, int x, int y) override {
        std::cout &lt;&lt; &quot;[PM] Text \&quot;&quot; &lt;&lt; s &lt;&lt; &quot;\&quot; @(&quot; &lt;&lt; x &lt;&lt; &quot;,&quot; &lt;&lt; y &lt;&lt; &quot;)\n&quot;;
    }
};

// -------- Abstraction --------
class Window {
public:
    explicit Window(std::unique_ptr&lt;WindowImp&gt; imp) : imp_(std::move(imp)) {}
    virtual ~Window() = default;

    // 고수준 API
    virtual void DrawRect(const Point&amp; p1, const Point&amp; p2) {
        imp_-&gt;DeviceRect(p1.x, p1.y, p2.x, p2.y);
    }
    virtual void DrawText(const std::string&amp; s, const Point&amp; at) {
        imp_-&gt;DeviceText(s.c_str(), at.x, at.y);
    }
    virtual void DrawContents() = 0; 

protected:
    WindowImp* imp() { return imp_.get(); }

private:
    std::unique_ptr&lt;WindowImp&gt; imp_; 
};

// -------- Refined Abstractions --------
class IconWindow : public Window {
public:
    IconWindow(std::unique_ptr&lt;WindowImp&gt; imp, std::string iconName)
        : Window(std::move(imp)), icon_(std::move(iconName)) {}
    void DrawContents() override {
        DrawText((&quot;ICON:&quot; + icon_), {0, 0});
        DrawRect({0, 0}, {32, 32});
    }
private:
    std::string icon_;
};

class TransientWindow : public Window {
public:
    explicit TransientWindow(std::unique_ptr&lt;WindowImp&gt; imp)
        : Window(std::move(imp)) {}
    void DrawContents() override {
        DrawText(&quot;Transient&quot;, {8, 16});
        DrawRect({4, 4}, {128, 64});
    }
};

// -------- Client --------
int main() {
    // 런타임에 구현 선택 → 같은 추상도 다른 구현과 조합 가능
    IconWindow w1(std::make_unique&lt;XWindowImp&gt;(), &quot;app.png&quot;);
    TransientWindow w2(std::make_unique&lt;PMWindowImp&gt;());

    w1.DrawContents(); // X 구현으로 그리기
    w2.DrawContents(); // PM 구현으로 그리기
    return 0;
}</code></pre>
<p>전체 코드는 이렇게 되는데, 책에서 제시한 코드는 기능이 너무 많아서 간단하게 <code>DrawRect()</code>와 <code>DrawText()</code>만 구현을 했다. 그리고 Refined Abstraction으로 <code>IconWinodw</code> 만 구현을 했다.</p>
<h4 id="implementor">Implementor</h4>
<pre><code class="language-cpp">// -------- Implementor --------
class WindowImp {
public:
    virtual ~WindowImp() = default;
    virtual void DeviceRect(int x0, int y0, int x1, int y1) = 0;
    virtual void DeviceText(const char* s, int x, int y) = 0;
};

// -------- Concrete Implementors --------
class XWindowImp : public WindowImp {
public:
    void DeviceRect(int x0, int y0, int x1, int y1) override {
        std::cout &lt;&lt; &quot;[X] Rect (&quot; &lt;&lt; x0 &lt;&lt; &quot;,&quot; &lt;&lt; y0
                  &lt;&lt; &quot;)-(&quot; &lt;&lt; x1 &lt;&lt; &quot;,&quot; &lt;&lt; y1 &lt;&lt; &quot;)\n&quot;;
    }
    void DeviceText(const char* s, int x, int y) override {
        std::cout &lt;&lt; &quot;[X] Text \&quot;&quot; &lt;&lt; s &lt;&lt; &quot;\&quot; @(&quot; &lt;&lt; x &lt;&lt; &quot;,&quot; &lt;&lt; y &lt;&lt; &quot;)\n&quot;;
    }
};

class PMWindowImp : public WindowImp {
public:
    void DeviceRect(int x0, int y0, int x1, int y1) override {
        std::cout &lt;&lt; &quot;[PM] Rect (&quot; &lt;&lt; x0 &lt;&lt; &quot;,&quot; &lt;&lt; y0
                  &lt;&lt; &quot;)-(&quot; &lt;&lt; x1 &lt;&lt; &quot;,&quot; &lt;&lt; y1 &lt;&lt; &quot;)\n&quot;;
    }
    void DeviceText(const char* s, int x, int y) override {
        std::cout &lt;&lt; &quot;[PM] Text \&quot;&quot; &lt;&lt; s &lt;&lt; &quot;\&quot; @(&quot; &lt;&lt; x &lt;&lt; &quot;,&quot; &lt;&lt; y &lt;&lt; &quot;)\n&quot;;
    }
};</code></pre>
<p>Bridge Pattern의 <strong>Implementation(구현부)</strong> 는 <code>WindowImp</code>라는 인터페이스를 중심으로 구성된다. 이 클래스는 <code>DeviceRect</code>, <code>DeviceText</code>와 같이 플랫폼 의존적인 저수준 API(Application Programming Interface)를 정의한다. 그리고 실제 구현은 <code>XWindowImp</code>, <code>PMWindowImp</code>에서 이루어진다. 예를 들어 <code>XWindowImp</code>는 X 윈도우 시스템 호출을, <code>PMWindowImp</code>는 프레젠테이션 매니저 호출을 각각 캡슐화한다. 즉, <strong>어떻게 그릴 것인가</strong>라는 부분을 담당하는 것이 바로 구현부이며, 추상부와 독립적으로 교체하거나 확장할 수 있다.</p>
<h4 id="abstraction">Abstraction</h4>
<pre><code class="language-cpp">// -------- Abstraction --------
class Window {
public:
    explicit Window(std::unique_ptr&lt;WindowImp&gt; imp) : imp_(std::move(imp)) {}
    virtual ~Window() = default;

    virtual void DrawRect(const Point&amp; p1, const Point&amp; p2) {
        imp_-&gt;DeviceRect(p1.x, p1.y, p2.x, p2.y);
    }
    virtual void DrawText(const std::string&amp; s, const Point&amp; at) {
        imp_-&gt;DeviceText(s.c_str(), at.x, at.y);
    }
    virtual void DrawContents() = 0;

protected:
    WindowImp* imp() { return imp_.get(); }

private:
    std::unique_ptr&lt;WindowImp&gt; imp_;
};</code></pre>
<p><strong>Abstraction(추상부)</strong>는 <code>Window</code> 클래스가 담당한다. <code>Window</code>는 클라이언트에 노출되는 고수준 인터페이스를 정의하며, <code>DrawRect</code>, <code>DrawText</code> 같은 메서드를 통해 기능을 제공한다. 하지만 직접 그리기를 수행하지 않고, 내부에 <code>std::unique_ptr&lt;WindowImp&gt;</code>를 보관해 실제 동작을 구현부에 위임한다. 이렇게 하면 클라이언트는 <code>Window</code>의 API만 이용하면 되고, 저수준 동작은 구현부에서 알아서 처리된다.</p>
<h4 id="refined-abstraction">Refined Abstraction</h4>
<pre><code class="language-cpp">// -------- Refined Abstractions --------
class IconWindow : public Window {
public:
    IconWindow(std::unique_ptr&lt;WindowImp&gt; imp, std::string iconName)
        : Window(std::move(imp)), icon_(std::move(iconName)) {}
    void DrawContents() override {
        DrawText((&quot;ICON:&quot; + icon_), {0, 0});
        DrawRect({0, 0}, {32, 32});
    }
private:
    std::string icon_;
};

class TransientWindow : public Window {
public:
    explicit TransientWindow(std::unique_ptr&lt;WindowImp&gt; imp)
        : Window(std::move(imp)) {}
    void DrawContents() override {
        DrawText(&quot;Transient&quot;, {8, 16});
        DrawRect({4, 4}, {128, 64});
    }
};</code></pre>
<p><code>IconWindow</code>와 <code>TransientWindow</code> 같은 <strong>Refined Abstraction</strong>은 <code>Window</code>를 상속받아 고수준의 행위를 구체화한다. 예를 들어 <code>IconWindow</code>는 아이콘을 그리는 동작을 정의하고, <code>TransientWindow</code>는 임시 창을 그리는 방식을 정의한다. 하지만 이들도 직접 저수준 연산을 구현하지 않고, <code>imp()</code>를 통해 내부의 <code>WindowImp</code>에 작업을 위임한다. 이렇게 추상부는 “무엇을 할 것인지”를 정의하고, 구현부는 “어떻게 할 것인지”를 책임지게 되는 구조가 된다. 물론 나는 <code>IconWindow</code> 만 구현을 한 상태이다.</p>
<h4 id="client">Client</h4>
<pre><code class="language-cpp">// -------- Client --------
int main() {
    IconWindow w1(std::make_unique&lt;XWindowImp&gt;(), &quot;app.png&quot;);
    TransientWindow w2(std::make_unique&lt;PMWindowImp&gt;());

    w1.DrawContents(); // X 플랫폼 구현으로 동작
    w2.DrawContents(); // PM 플랫폼 구현으로 동작
    return 0;
}</code></pre>
<p>마지막으로 클라이언트는 실행 시점에 <code>IconWindow</code>나 <code>TransientWindow</code>를 생성하면서 원하는 구현체(<code>XWindowImp</code> 혹은 <code>PMWindowImp</code>)를 주입할 수 있다. 이렇게 <strong>런타임 조합(Runtime Composition)</strong> 을 활용하면, 기능 축(추상)과 플랫폼 축(구현)을 완전히 독립적으로 확장할 수 있으며, 기능 × 플랫폼 조합에 따라 모든 클래스를 미리 만들어야 하는 클래스 폭발 문제를 방지할 수 있다.</p>
<hr>
<h1 id="마무리-😘">마무리 😘</h1>
<p><strong>Bridge Pattern</strong>은 <strong>추상과 구현을 분리해서 독립적으로 확장할 수 있도록 만들어주는 구조적 패턴</strong>이다. 예시에서 보았듯이, <strong>추상화 계층과 구현 계층을 분리해두면 새로운 기능을 추가하더라도 클래스가 불필요하게 늘어나지 않고 훨씬 유연하게 확장할 수 있다</strong>. 즉, 어댑터 패턴이 기존 인터페이스의 불일치를 해결하기 위한 사후적 접근이었다면, <strong>Bridge Pattern은 처음부터 확장을 고려한 선제적 설계 방식</strong>이라고 볼 수 있다.</p>
<p>다음 글에서는 마찬가지로 구조 패턴 중 하나인 <code>Composite Pattern</code>을 다뤄볼 생각이다. <code>Composite Pattern</code>은 객체들을 <strong>트리 구조로 묶어서 부분-전체 계층을 표현</strong>하는 데 초점이 맞춰져 있다. 즉, <strong>개별 객체와 객체 집합을 동일한 방식으로 다룰 수 있게 해주는 패턴</strong>인데, 이를 통해 복잡한 계층 구조도 단순하게 다룰 수 있는 장점이 있다.</p>
<p><a href="https://www.cs.unc.edu/~stotts/GOF/hires/pat4bfso.htm">[참고] Bridge Pattern</a> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] LoRA (Low Rank Adaptation)]]></title>
            <link>https://velog.io/@choi-hyk/LLM-LoRA-Low-Rank-Adaptation</link>
            <guid>https://velog.io/@choi-hyk/LLM-LoRA-Low-Rank-Adaptation</guid>
            <pubDate>Sun, 31 Aug 2025 06:37:05 GMT</pubDate>
            <description><![CDATA[<h1 id="🪶-lora-low-rank-adaptation">🪶 LoRA (Low Rank Adaptation)</h1>
<p>이번에는 저번 글에서 말한 것 처럼<strong>LoRA(Low Rank Adaptation)</strong> 에 대해서 알아보겠다.</p>
<p><a href="https://arxiv.org/abs/2106.09685">「LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021)」</a></p>
<p><strong>LoRA</strong>는 위의 논문 <strong>「LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021)</strong> 에서 제시된 기존의 Fine-tuning을 개선한 방식이다. <strong>LoRA</strong>가 제시되기 이전에는 Fine-tuning은 전체의 파라미터를 조정하는 <strong>Full Fine-Tuning</strong>으로 이루어졌다. 이름만 들어도 전체 파라미터를 조정한다는 말에서 알 수 있듯, 비용이 어마어마하게 많이 들었다. 그래서 해당 논문에서는 LoRA를 사용해 저차원 부분행렬을 통해 일부 가중치만 조정하는 기법을 소개한다.</p>
<p>따라서 해당 논문을 바탕으로 LoRA에 대해서 정리를 해보겠다.</p>
<hr>
<h2 id="📘-introduction-of-lora">📘 Introduction of LoRA</h2>
<p>논문에서는 LoRA가 제시된 이유로 기존의 파인튜닝 방식인 Full Fine-Tuning의 단점을 먼저 이야기한다. 크게 두 가지 단점이 있었는데, <strong>첫 번째로 엄청난 양의 연산 비용을 요구한다</strong>는 점이다. 두 번째로 <strong>테스트를 위해 각 파라미터를 조정할 때 모델을 저장하여 성능 지표를 측정해야 한다</strong>는 점이다. 이는 <strong>GPT-2와 RoBERTa</strong>와 같은 LLM이 적은 기간 내에 계속 출시되면서 파라미터 수가 급격히 증가해 점점 어려워졌다. 이를 완화하기 위해 일부 파라미터만 저장하는 <strong>Adapting</strong>이라는 기법이 있었으나, 모델의 깊이가 증가하면 성능이 떨어지는 문제가 있었다.</p>
<img width="323" height="282" alt="Image" src="https://github.com/user-attachments/assets/9c74354d-1049-4606-a38f-dcc470aa8790" />  

<p>위 그림은 LoRA를 간단히 설명하는 그림이다. 그림을 보면 $d$ 차원의 $x$ 입력이 각각 Pretrained로 설정된 가중치 $W$와 $A$에 input으로 들어간다. 여기서 LoRA의 핵심 개념을 알 수 있는데, 바로 기존의 $W$는 <strong>freeze</strong> 시키고 입력을 새로운 layer에 입력으로 넣어 저차원 공간으로 축소(Down Projection)하는 행렬 $A \in \mathbb{R}^{r \times d}$를 거친다는 점이다. 참고로 $d$는 출력 값의 차원이고, $r$은 Down Projection 했을 때의 차원이다. 이렇게 $d$ 차원의 입력을 $r$ 차원으로 줄여낸 뒤, 다시 $B \in \mathbb{R}^{d \times r}$ 행렬을 통해 원래 출력 차원 $d$로 확장(Up Projection)한다. 결국 전체 업데이트 행렬은 $\Delta W = BA$ 형태가 된다.</p>
<p>$$
h = W_0 x + BAx
$$</p>
<p>위의 식이 최종적으로 LoRA가 가중치를 구하는 방법이다. 기존의 방법인 Full Fine-Tuning은 다음과 같다.</p>
<p>$$
h = W_0 x
$$</p>
<p>위 식은 가중치 전체를 조정하는 <strong>Full Fine-Tuning</strong>을 나타낸다.</p>
<p>이 그림은 <strong>원래 가중치 $W_0$는 동결시키고, 작은 두 개의 행렬 $A, B$만 학습</strong>해서 기존 선형 변환 결과에 보정값을 더해주는 구조를 단순하게 보여준다. 이를 통해 큰 모델 전체를 건드리지 않고도 파라미터 효율적인 학습이 가능하다는 것이 LoRA의 핵심이다.</p>
<p>여기서 핵심은 바로 $r$인데, 논문에서는 파라미터가 175B이고 출력 차원 수가 12,288인 GPT-3에서도 $r = 1$ 또는 $r = 2$ 정도의 매우 작은 값으로도 성능이 유지된다고 한다. 사실 이렇게 들으면, 왜 성능이 유지되는지 의문이 될 정도로 터무니없게 차이가 크다.</p>
<p>논문에서는 이러한 원리를 LoRA를 고안할 때 영감을 받은 <strong>「Measuring the Intrinsic Dimension of Objective Landscapes (ICLR 2018)」</strong>, 그리고 <strong>「Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning (ACL 2021)」</strong>에서 언급한 <strong>LLM 모델의 Fine-Tuning은 저차원 공간에서 이루어진다</strong>는 이유로 설명한다. 두 논문에서는 <strong>거대한 네트워크라도 학습할 때는 저차원 공간(subspace)에서만 움직여도 충분하다는 발견</strong>, 그리고 <strong>대규모 언어모델 파인튜닝에서도 실제로 필요한 변화는 낮은 intrinsic dimension 안에서 발생한다는 것을 실험적으로 입증</strong>했다고 한다. 따라서 사실상 Fine-Tuning은 학습할 입력값에 대해 전체 차원을 사용할 필요가 없다는 것이다.</p>
<p>LoRA는 기본적으로 <strong>기존 파인튜닝보다 더 일반화된 방식</strong>이다. 풀 파인튜닝이 전체 파라미터를 학습하거나 일부만 선택해서 학습하는 것이라면, LoRA는 한 단계 더 나아가서 <strong>가중치 행렬 업데이트가 꼭 풀랭크(full-rank)일 필요가 없다</strong>는 점에 주목한다. 즉, <strong>모든 가중치 행렬에 LoRA를 적용하고 bias까지 학습하며, rank $r$을 원래 가중치 행렬의 랭크 수준으로 높게 잡아버리면 사실상 풀 파인튜닝과 같은 표현력을 회복할 수 있다.</strong> 다시 말해, <strong>학습 가능한 파라미터 수를 늘릴수록 LoRA는 풀 파인튜닝에 점점 가까워진다.</strong></p>
<hr>
<h2 id="📊-results">📊 Results</h2>
<img width="842" height="348" alt="Image" src="https://github.com/user-attachments/assets/14d1ef61-3963-4c20-a4e9-b4f9c6a1fed3" />  

<p><img src="https://velog.velcdn.com/images/choi-hyk/post/0c0f6136-a4c5-40ab-82de-b90b66ea701a/image.png" alt=""></p>
<p>실험 결과를 보면 LoRA가 왜 이렇게 주목받는지 바로 알 수 있다. 먼저 평가 지표에 대해서 설명하겠다.</p>
<p><strong>WikiSQL은 자연어 질문을 SQL 쿼리로 바꾸는 데이터셋</strong>이다. 예를 들어 <strong>2010년에 개봉한 영화 제목 알려줘</strong>라는 문장이 들어오면 모델은 그걸 SQL 쿼리 형태로 바꿔야 한다. 그래서 단순히 언어 이해만 보는 게 아니라 <strong>데이터베이스 쿼리까지 연결하는 능력</strong>을 평가한다.</p>
<p><strong>MultiNLI는 두 문장의 의미적 관계를 따지는 데이터셋</strong>이다. <strong>나는 점심을 먹었다</strong>와 <strong>나는 밥을 안 먹었다</strong>는 모순, <strong>나는 점심을 먹었다</strong>와 <strong>나는 음식을 먹었다</strong>는 함의, 그리고 서로 관련 없는 문장은 중립으로 분류한다. 결국 모델이 <strong>문장 간 의미를 얼마나 정확히 파악하는지를 측정</strong>한다.</p>
<p><strong>SAMSum은 대화 요약 데이터셋</strong>이다. 메신저 대화처럼 짧은 대화가 주어지고, 모델은 그걸 요약해야 한다. 예를 들어 <strong>A: 오늘 뭐해? B: 영화 볼 건데. A: 같이 가자</strong>라는 대화가 있으면 <strong>A와 B가 같이 영화를 보기로 했다</strong>라고 요약하는 식이다. <strong>짧은 대화를 읽고 핵심만 뽑아내는 능력</strong>을 평가한다. 그리고 이런 요약 과제 성능을 볼 때 쓰는 게 R1, R2, RL이다. R1은 정답 요약과 단어 단위로 얼마나 겹치는지를 보는 지표이고, R2는 연속된 두 단어 bigram이 겹친 비율을 본다. RL은 최장 공통 부분 수열(Longest Common Subsequence)을 기반으로 해서 문장 구조 자체가 비슷한지를 평가한다. 결국 R1은 단어 겹침, R2는 구 겹침, RL은 문장 구조 겹침이라고 보면 된다.</p>
<p>우선 <strong>WikiSQL</strong> 결과부터 보면, Full Fine-Tuning은 가장 높은 성능을 보여주지만 파라미터 수가 엄청나다. 반면에 LoRA는 <strong>훨씬 적은 파라미터만 학습했음에도 불구하고 Full Fine-Tuning에 거의 근접한 정확도</strong>를 달성했다. Adapter(H)도 LoRA와 비슷하게 좋은 성능을 보이지만, Prefix 계열(PrefixEmbed, PrefixLayer)은 상대적으로 낮은 정확도를 보인다.</p>
<p><strong>MultiNLI-matched</strong> 결과는 더 극적이다. Full Fine-Tuning이 여전히 좋은 성능을 내지만, LoRA와 Adapter(H)는 보다 높은 정확도를 <strong>훨씬 더 효율적인 파라미터 사용</strong>으로 달성했다. 특히 LoRA는 실질적으로 Full Fine-Tuning 수준의 성능을 뛰어넘으면서도 필요한 파라미터 수는 압도적으로 적다.</p>
<p>그리고 <strong>SAMSum</strong>에서도 LoRA는 Full Fine-Tuning보다 더 높은 수준의 정확도를 보였다.</p>
<p>즉, LoRA는 단순히 파라미터를 줄이는 수준이 아니라, <strong>적은 자원으로도 풀 파인튜닝급 성능을 낼 수 있다</strong>는 걸 명확히 보여준다. 이런 점에서 실제 대규모 모델을 다룰 때 LoRA가 가지는 실용성은 엄청나다고 할 수 있다.</p>
<p>그리고 나는 FT가 항상 좋은 줄 알았는데, 찾아보니 과적합으로 인해 오히려 FT의 성능이 안 좋아질 수도 있다고 한다. 입력 데이터가 적을 경우 FT는 과적합이 일어날 가능성이 있지만 LoRA는 데이터셋이 적어도 적절한 학습이 가능하다.</p>
<hr>
<h1 id="📝-마무리">📝 마무리</h1>
<p>오늘은 LoRA에 대해 논문과 실험 결과를 중심으로 정리해 보았다. LoRA는 단순히 파라미터 효율성을 제공하는 수준을 넘어, 실제로 Full Fine-Tuning에 맞먹거나 그 이상의 성능을 적은 자원으로 달성할 수 있음을 보여준다. 특히, 데이터셋 크기가 제한적이거나 리소스가 부족한 상황에서 매우 강력한 대안이 될 수 있다.</p>
<p>다음 글에서는 오늘 정리한 내용을 바탕으로 <strong>실제 코드를 통해 LoRA를 활용한 파인튜닝 방법</strong>을 자세히 살펴보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Mini Project] Velog Backup 프로그램 만들기]]></title>
            <link>https://velog.io/@choi-hyk/Mini-Project-Velog-Backup-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@choi-hyk/Mini-Project-Velog-Backup-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 30 Aug 2025 14:36:55 GMT</pubDate>
            <description><![CDATA[<h1 id="📦-velog-backup-프로그램-만들기">📦 Velog Backup 프로그램 만들기</h1>
<p>오늘은 갑자기 미니 프로젝트를 하고 싶어져서, Velog 포스트들을 백업해주는 프로그램을 만들려고 한다. 갑자기 미니 프로젝트를 하는 이유는 딱히 없다. 그냥 해보고 싶어졌다. 프로그램 목적은 사용자의 이름을 환경변수로 주면 GrphQL로 Velog 정보를 가져와서 시리즈 별 디렉토리에 포스트들을 저장하는 방식이다.</p>
<p>그리고 <strong>Python Package Index</strong> 를 통해 배포를 해보고 <strong>GitHub Actions</strong> 로 자동화 까지 가능하도록 구현할 생각이다.</p>
<hr>
<h2 id="🔍-graphql">🔍 GraphQL</h2>
<p>내가 GitHub Pages를 만들 때, 그때도 Velog 포스트들을 가져와서 GitHub Pages에 출력 해주는 API를 만들었다. 그떄는 RSS를 사용해서 가져왔다. RSS는 근데 시리즈랑 프로필에 대한 상세한 정보가 없어서, 단순하게 내가 제목에 대괄호로 자체 태그를 만드는 걸 이용해서 포스트들의 시리즈를 구분했다. </p>
<p>이번 프로젝트는 다른 사람들이 전부 사용 가능하도록 GraphQL을 활용해서 시리즈를 추출할 생각이다. 참고로 GraphQL은 <strong>Facebook(현 Meta)</strong> 가 2012년 개발해서, 2015년 공개한 API 쿼리 언어라고 한다. 클라이언트가 필요한 데이터만 정확히 요청할 수 있도록 설계된 데이터 질의 언어인데, 서버와 클라이언트 간 데이터 통신을 더 유연하고 효율적으로 만들어 준다...</p>
<p>이번에 처음 써보는데, 클라이언트가 원하는 데이터만 응답해주는 것이 특징이다. 내가 느낀 건, GraphQL은 마치 &quot;필요한 만큼만 담아오는 주문표&quot; 같은 느낌이다. REST API에서는 <code>/posts</code> 요청하면 정해진 형식대로 모든 데이터가 쏟아지는데, GraphQL은 <code>title</code>이나 <code>tags</code>만 원하면 그것만 딱 주고, <code>series</code>까지 원하면 그것도 같이 준다. 그래서 불필요한 데이터 전송이 줄고, 필요한 관계형 데이터도 한 번에 가져올 수 있다. 대신 스키마랑 쿼리를 직접 설계해야 해서, 초반에는 좀 낯설고 복잡하게 느껴질 수도 있다. 하지만 익숙해지면 데이터 흐름이 훨씬 깔끔해지고, 특히 내가 이번에 시리즈 정보까지 정리해서 가져오려는 것처럼, RSS보다 훨씬 세밀하게 제어할 수 있는 게 장점이다.</p>
<pre><code class="language-python">import requests

ENDPOINT = &quot;https://v2.velog.io/graphql&quot;

def gql(query: str, variables: dict | None = None) -&gt; dict:
    &quot;&quot;&quot;
    GraphQL 쿼리를 실행하는 함수

    Args:
        query (str): GraphQL 쿼리 문자열
        variables (dict | None, optional): 쿼리 변수

    Returns:
        data[&quot;data&quot;] (dict): GraphQL 응답 데이터
    &quot;&quot;&quot;
    payload = {&quot;query&quot;: query, &quot;variables&quot;: variables or {}}
    res = requests.post(ENDPOINT, json=payload, timeout=15)
    res.raise_for_status()
    data = res.json()
    if &quot;errors&quot; in data:
        msgs = &quot;; &quot;.join(e.get(&quot;message&quot;, &quot;&quot;) for e in data[&quot;errors&quot;])
        raise RuntimeError(f&quot;GraphQL 오류: {msgs}&quot;)
    return data[&quot;data&quot;]</code></pre>
<p>위의 코드는 Velog의 정보를 가져오는 GraphQL 실행 함수이다. 저기 <code>payload</code> 에 내가 원하는 데이터의 정보를 넣게 되면, 해당 정보를 응답해 준다.</p>
<h3 id="query">QUERY</h3>
<pre><code class="language-python">PROFILE_QUERY = &quot;&quot;&quot;
query UserProfile($username: String!) {
    user(username: $username) {
        id
        username
        profile {
            display_name
            thumbnail
        }
    }
}
&quot;&quot;&quot;

LIST_QUERY = &quot;&quot;&quot;
query Posts($username: String!, $limit: Int!, $cursor: ID) {
    posts(username: $username, limit: $limit, cursor: $cursor) {
        id
        url_slug
    }
}
&quot;&quot;&quot;

DETAIL_QUERY = &quot;&quot;&quot;
query ReadPost($username: String!, $slug: String!) {
    post(username: $username, url_slug: $slug) {
        id
        url_slug
        title
        thumbnail
        tags
        series { name }
        released_at
        updated_at
        is_markdown
        body
        likes
    }
}
&quot;&quot;&quot;</code></pre>
<p>앞에서 말한 것 처럼 GraphQL에서는 Route가 없고, 클라이언트가 무슨 payload를 보내느냐에 따라 오는 응답이 달라진다. 나는 Velog 사용자의 프로필과, 모든 포스트 정보, 그리고 각 포스트의 컨텐츠 3개가 필요하다. 프로필은 <code>PROFILE_QUERY</code>를 통해 요청이 가능했다. 간단하게 Velog 유저 이름을 보내면 프로필 정보를 보내준다. 다음은 <code>LIST_QUERY</code>를 통해서 모든 포스트 정보를 가져왔다. 해당 쿼리가 제일 복잡한데, 이유는 GraphQL은 리스트를 요청할때 한번에 요청이 가능한 한도가 정해져 있어서 <code>cursor</code> 와 <code>limit</code>로 메세지 큐를 보내는 것처럼 잘라서 받아야 한다. 그래서 <code>cursor</code> 와 <code>limit</code> 가 <code>LIST_QUERY</code>를 보면 설정되어 있다. 마지막으로 <code>DETAIL_QUERY</code>는 <code>url_slug</code> 라는 <code>LIST_QUERY</code>에서 가져온 포스트들의 url로 해당 포스트의 컨텐츠를 가져온다. 이렇게 모든 정보를 가져오면 이제 간단하다. 각 시리즈들을 폴더로 만들고, 해당 폴더 안에 시리즈에 해당하는 포스트들을 md파일로 생성하면 된다. 매우 고맙게도 GraphQL은 응답을 md파일로 해줘서 매우 편했다. <del>RSS를 사용할때는 html형식을 md로 바꿔야해서 짜증이 났다.</del></p>
<hr>
<h2 id="⚙️-pyprojecttoml">⚙️ pyproject.toml</h2>
<p>이제 해당 프로젝트를 빌드를 하고, 빌드 파일을 배포해보겠다. 파이썬은 배포 환경이 매우 잘 되어 있는데 빌드를 <strong>PyPI</strong>에 업로드 하며 우리가 흔히 파이썬 패키지를 다운 받을 때 사용하는 <code>pip install</code>이 가능하다. </p>
<p>파이썬에서 패키지를 배포할 때는 <code>pyproject.toml</code> 파일을 작성해야 한다. 이게 일종의 <strong>패키지 설정서</strong> 역할을 하는데, 프로젝트 이름부터 버전, 의존성, 빌드 방식까지 전부 여기에 정의한다. 내가 작성한 항목들을 하나씩 보면 이렇다:</p>
<pre><code class="language-bash">[project]
name = &quot;velog_sync&quot;
version = &quot;0.1.0&quot;
description = &quot;Velog 글을 Markdown으로 백업 (시리즈별 폴더) — velog_sync PyPI 패키지 실행&quot;
readme = &quot;README.md&quot;
requires-python = &quot;&gt;=3.10&quot;
authors = [{ name = &quot;choi-hyk&quot;, email = &quot;blindlchoil@gmail.com&quot; }]
license = { file = &quot;LICENSE&quot; }   
classifiers = [
  &quot;License :: OSI Approved :: MIT License&quot;,
]
dependencies = [
  &quot;requests&gt;=2.32.0&quot;,
  &quot;tzdata&gt;=2024.1&quot;   
]

[project.scripts]
velog-sync = &quot;velog_sync:main&quot;

[tool.setuptools]
py-modules = [&quot;velog_sync&quot;]

[build-system]
requires = [&quot;setuptools&gt;=68&quot;, &quot;wheel&quot;]
build-backend = &quot;setuptools.build_meta&quot;</code></pre>
<hr>
<h3 id="project">[project]</h3>
<h4 id="name-velog_sync">name: <code>&quot;velog_sync&quot;</code></h4>
<ul>
<li>PyPI에 올라갈 패키지 이름. <code>pip install velog-sync</code> 할 때 쓰이는 이름이다.</li>
<li>참고로 언더바 ( _ ) 는 하이픈 ( - )으로 바뀐다<h4 id="version-010">version: <code>&quot;0.1.0&quot;</code></h4>
</li>
<li>패키지 버전. SemVer(주버전.부버전.패치버전) 규칙을 따른다.  </li>
</ul>
<h4 id="description-패키지-간단-설명">description: 패키지 간단 설명.</h4>
<h4 id="readme-readmemd">readme: <code>&quot;README.md&quot;</code></h4>
<ul>
<li>PyPI 페이지에 표시될 문서.  </li>
</ul>
<h4 id="requires-python-310">requires-python: <code>&quot;&gt;=3.10&quot;</code></h4>
<ul>
<li>파이썬 최소 버전. 여기서는 Python 3.10 이상만 지원하도록 했다.  </li>
</ul>
<h4 id="authors-작성자-정보-이름과-이메일을-적을-수-있다">authors: 작성자 정보. 이름과 이메일을 적을 수 있다.</h4>
<h4 id="license---file--license-">license = { file = &quot;LICENSE&quot; }</h4>
<ul>
<li>라이선스 파일을 명시해준다.</li>
</ul>
<h4 id="classifiers">classifiers</h4>
<ul>
<li>라이선스의 종류를 명시해준다.</li>
</ul>
<h4 id="dependencies">dependencies:</h4>
<ul>
<li><code>requests&gt;=2.32.0</code>: HTTP 요청용 라이브러리  </li>
<li><code>tzdata&gt;=2024.1</code>: 타임존 데이터용 라이브러리</li>
<li>패키지를 설치할 때 자동으로 같이 설치된다.</li>
</ul>
<hr>
<h3 id="projectscripts">[project.scripts]</h3>
<pre><code class="language-bash">velog-sync = &quot;velog_sync:main&quot;</code></pre>
<p>이 프로젝트가 단일 파이썬 파일(velog_sync.py)로 구성되어 있다는 걸 명시한다. 패키지 디렉토리 구조가 아니라 .py 모듈을 main 함수로 실행하면 위와 같이 적는다. 함수는 본인이 알아서 설정 가능하다.</p>
<hr>
<h3 id="build-system">[build-system]</h3>
<pre><code class="language-bash">requires = [&quot;setuptools&gt;=68&quot;, &quot;wheel&quot;]
build-backend = &quot;setuptools.build_meta&quot;</code></pre>
<p>빌드할 때 어떤 툴을 사용할지 지정한다. <code>setuptools</code>와 <code>wheel</code>이 필요하다고 정의했고, <code>setuptools.build_meta</code>를 빌드 백엔드로 사용한다고 명시했다. 이 설정 덕분에 <code>python -m build</code> 명령으로 <code>.tar.gz</code>와 <code>.whl</code> 빌드 파일을 만들 수 있다.</p>
<p>여기서 중요한 건, <code>build-system.requires</code>에 적은 패키지들이 실제 실행 환경에 필요한 건 아니라는 점이다. 이건 어디까지나 <strong>빌드 과정에서만 필요한 도구</strong>라서, 패키지를 설치하는 사람 입장에서는 신경 쓸 필요가 없다. 그리고 <code>setuptools.build_meta</code>는 일종의 빌드 엔진 역할을 하는데, <code>pip install .</code> 같은 명령을 실행했을 때 내부적으로 <code>build_wheel</code>, <code>build_sdist</code> 같은 함수를 호출해서 배포 파일을 만들어준다.</p>
<hr>
<h2 id="🚀-pypi-배포하기">🚀 PyPI 배포하기</h2>
<p>배포를 하려면 PyPI에 계정을 만들고, Token을 받아서 등록을 해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/choi-hyk/post/cc358957-8460-4e96-a68a-22a30a7ac5cd/image.png" alt=""></p>
<p>이제 해당 토큰을 자신의 로컬에 등록을 하면 된다.</p>
<pre><code>이 토큰을 사용하세요.
이 API 토큰을 사용하려면:

__token__에 사용자 이름을 설정합니다
pypi- 접두사를 포함하여 비밀번호를 토큰 값으로 설정하세요
예를 들어, 프로젝트를 PyPI에 업로드하기 위해 Twine을 사용하는 경우, $HOME/.pypirc 파일을 다음과 같이 설정하세요:

[pypi]
  username = __token__
  password = TOKEN</code></pre><p>위의 설정을 보고 로컬에 등록을 하면 로컬에서 배포가 가능하다. 먼저 빌드를 통해 코드를 배포 가능한 형태인 <code>.tar.gz</code>, <code>.whl</code>로 만들어야 한다.</p>
<h3 id="1-빌드">1. 빌드</h3>
<p><code>python -m build</code> 를 실행하면 dist/ 디렉토리에 아래와 같은 파일이 생긴다.</p>
<ul>
<li>velog_sync-0.1.0.tar.gz (소스 배포본)</li>
<li>velog_sync-0.1.0-py3-none-any.whl (휠 파일)</li>
</ul>
<hr>
<h3 id="2-업로드">2. 업로드</h3>
<p>이제 twine을 사용해서 PyPI에 업로드한다:</p>
<pre><code class="language-bash">twine upload dist/*</code></pre>
<p>여기서 <code>.pypirc</code> 파일에 등록해둔 토큰이 자동으로 사용된다. 업로드가 성공하면 PyPI 패키지 페이지에 바로 반영된다. </p>
<hr>
<h3 id="3-설치-확인">3. 설치 확인</h3>
<p>업로드가 끝나면 실제로 잘 올라갔는지 pip로 설치해본다:</p>
<pre><code class="language-bash">pip install velog-sync</code></pre>
<p>설치가 잘 되고, 내가 지정한 <code>velog-sync</code> 명령어까지 정상 실행되면 배포 완료다.</p>
<hr>
<h2 id="🤖-github-actions-배포">🤖 GitHub Actions 배포</h2>
<pre><code class="language-yaml">name: Publish to PyPI   # 워크플로우 이름 (GitHub Actions 탭에 표시됨)

on:
    push:
        tags: [&quot;v*&quot;]    # 태그가 v로 시작하는 커밋이 push될 때 실행됨 (예: v0.1.0, v1.0.0)

jobs:
    pypi-publish:
        name: Upload release to PyPI  # 잡 이름
        runs-on: ubuntu-latest        # 실행 환경: 최신 Ubuntu GitHub Runner 사용

        permissions:
            contents: read            # 리포지토리 컨텐츠 읽기 권한
            id-token: write           # OIDC(OpenID Connect) 토큰 발급 권한 → PyPI에 인증용

        steps:
            # 1. 코드 체크아웃
            - uses: actions/checkout@v4
              # GitHub Actions 런너에 현재 레포지토리 코드 가져오기

            # 2. Python 설치
            - uses: actions/setup-python@v5
              with:
                  python-version: &quot;3.12&quot;   # 파이썬 3.12 환경 구성

            # 3. 빌드 단계
            - name: Build
              run: |
                  python -m pip install --upgrade pip  # pip 최신화
                  pip install build                    # build 패키지 설치
                  python -m build                      # pyproject.toml 기반으로 dist/에 빌드 산출물 생성

            # 4. PyPI 업로드
            - name: Publish to PyPI
              uses: pypa/gh-action-pypi-publish@release/v1
              with:
                  skip-existing: true  # 이미 업로드된 파일이 있으면 스킵(중복 업로드 방지)</code></pre>
<p>위와 같이 구성이 가능한데, 살펴볼 점은 태그랑 인증 방법이다. GitHub Actions는 태그 설정을 통해 배포 자동화가 이루어진다.  예를 들어 <code>git tag v0.1.0</code> 을 하게 되면, 바뀐 버전이 해당 액션으로 자동 배포가 이루어진다.</p>
<p>다음은 PyPI의 인증 방식인데, 기존에 로컬에서는 Token을 발급받아서, 배포를 하였는데, PyPI는 GitHub Actions와 같이 자동화 툴들을 위해  <strong>PyPI Trusted Publisher</strong>라는 방법을 제공한다. 예전처럼 <code>.pypirc</code>에 비밀번호 저장하는 게 아니라, GitHub OIDC(OpenID Connect) 토큰을 이용해서 <strong>PyPI Trusted Publisher</strong>로 인증한다. 즉, GitHub 저장소와 PyPI 계정을 연결해두면 비밀번호/토큰 노출 없이 안전하게 배포 가능하다.  <strong>PyPI Trusted Publisher</strong> 를 사용하려면 자신의 PyPI 계정에 해당 GitHub repo를 등록하면 된다.</p>
<img width="838" height="217" alt="Image" src="https://github.com/user-attachments/assets/678f20aa-f48b-470c-a03c-3005dca06da4" />

<p>난 이렇게 등록을 하였다. </p>
<p>실행을 할때는 패치된 버전의 코드와 <code>pyproject.toml</code> 의 버전을 올리고 push와 push tag를 해줘야 한다. 참고로 <code>git tag</code> 명령어를 통해 tag를 등록하고 기존의 푸쉬 방법 처럼 <code>git push origin v0.1.0</code> 과 같은 방법으로 배포를 해줄 수 있다. 이때 주의할 점은 반드시 패치된 버전의 코드와 <code>pyproject.toml</code> 의 버전을 푸쉬해 놓은 상태여야 한다.</p>
<hr>
<h2 id="🔄-github-actions로-velog-sync-자동화-하기">🔄 GitHub Actions로 velog-sync 자동화 하기</h2>
<p>이제 로컬 배포와 GitHub Actions 배포도 구성을 하였으니, 실제로 사용자들이 쓸 수 있도록 GitHub Actions의 yml 파일을 제공하면 된다. 로컬에서 사용할 사람은 로컬에서 실행해서 백업을 진행하면 되고 나는 사용자들이 매일 03:00 시에 자동으로 Velog 포스트들을 GitHub repo에 업로드 되도록 yml 파일을 구성하였다.</p>
<pre><code class="language-yml">name: velog-sync (daily KST 03:00)

on:
    schedule:
        - cron: &quot;0 18 * * *&quot; # 매일 03:00 KST
    workflow_dispatch: {}

permissions:
    contents: write

jobs:
    sync:
        runs-on: ubuntu-latest
        environment: velog_sync
        steps:
            - name: Checkout
              uses: actions/checkout@v4

            - name: Set up Python
              uses: actions/setup-python@v5
              with:
                  python-version: &quot;3.11&quot;

            - name: Install velog-sync
              run: |
                  python -m pip install --upgrade pip
                  pip install velog-sync

            - name: Run velog-sync
              env:
                  VELOG_USERNAME: ${{ vars.VELOG_USERNAME }}
              run: velog-sync

            - name: Configure Git
              run: |
                  git config user.name &quot;github-actions[bot]&quot;
                  git config user.email &quot;41898282+github-actions[bot]@users.noreply.github.com&quot;

            - name: Rebase with remote main
              run: |
                  git pull --rebase --autostash origin main

            - name: Commit if changed
              env:
                  TZ: Asia/Seoul
              run: |
                  if [ -n &quot;$(git status --porcelain)&quot; ]; then
                    DATE_KST=&quot;$(date +&#39;%Y-%m-%d %H:%M:%S %Z&#39;)&quot;
                    git add -A
                    git commit -m &quot;chore: velog sync @ ${DATE_KST}&quot;
                    git push
                  else
                    echo &quot;No changes to commit.&quot;
                  fi</code></pre>
<p>yml 파일에서는 내가 만든 패키지인 <code>velog-sync</code>를 다운받고 해당 패키지를 사용해서 등록한 유저 환경변수를 통해 GitHub에 업로드 해준다.</p>
<img width="846" height="824" alt="Image" src="https://github.com/user-attachments/assets/3196d68c-5232-404d-806f-31739d6b9677" />

<p>배포가 완료된 모습이다. 아래 링크에서 확인 가능하다.
<a href="https://github.com/choi-hyk/Velog">https://github.com/choi-hyk/Velog</a></p>
<hr>
<h1 id="🏁-마무리">🏁 마무리</h1>
<p>오늘은 velog-sync라는 패키지를 만들고 배포까지 해보았는데, repo를 확인하고 이슈가 등록되면 개선해 나갈 생각이다. 그리고 지금은 Velog 가 조회수를 보여주는 API가 없지만, access_token을 통해 조회수를 확인 가능하다고 들었다. 그래서 해당 패키지에 access_token을 등록하여 조회수를 확인하는 기능을 넣고 싶다. 해당 패키지는 아래 링크에서 확인 가능하고, 이슈가 있으면 언제든지 등록을 해주길 바란다.</p>
<p><a href="https://pypi.org/project/velog-sync/">[PyPI] velog-sync</a>
<a href="https://github.com/choi-hyk/velog_sync">[GitHub] velog-sync</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Design Pattern] Adapter Pattern]]></title>
            <link>https://velog.io/@choi-hyk/Design-Pattern-Adapter-Pattern</link>
            <guid>https://velog.io/@choi-hyk/Design-Pattern-Adapter-Pattern</guid>
            <pubDate>Sun, 24 Aug 2025 08:29:55 GMT</pubDate>
            <description><![CDATA[<h1 id="adapter-pattern-🪛">Adapter Pattern 🪛</h1>
<p>이번에는 <code>Adapter Pattern</code>에 대해서 알아보겠다. GOF 디자인패턴 책에서는 구조패턴을 설명할 때 <strong>Adapter Pattern</strong> 을 제일 먼저 설명한다. <strong>Adapter Pattern</strong>은 말 그대로 기존의 클래스 인터페이스에 다른 라이브러리나 인터페이스를 결합하기 위해 사용하는 패턴이다. 그래서 구조는 매우 직관적이다. 기존에 우리가 사용할 인터페이스와 결합할 인터페이스를 다중 상속 받는 <strong>클래스 어댑터</strong>를 생각해 볼 수 있고, 다른 방법으로는 결합할 인터페이스를 인스턴스로 가지고 있는 <strong>객체 어댑터</strong>를 생각해 볼 수 있다</p>
<blockquote>
<p>클래스의 인터페이스를 사용자가 기대하는 인터페이스 형태로 적응(변한)시킵니다. 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시킵니다.</p>
</blockquote>
<hr>
<h2 id="언제-사용하나-📌">언제 사용하나? 📌</h2>
<p>책에서는 어댑터 패턴을 <code>Shape</code>라는 그래픽을 관리하는 클래스에 <code>TextView</code> 기능을 결합하는 예제로 설명을 한다.</p>
<img width="626" height="236" alt="Image" src="https://github.com/user-attachments/assets/a31f7502-6532-41d8-92a3-caadc49ebae5" />

<p>위의 그림은 <strong>객체 어댑터를</strong> 표현하고 있다. 그 이유는 <code>TextShape</code>가 <code>TextView</code>를 상속하지 않고 포함(Composition) 하고 있기 때문이다. 즉, TextShape 안에 <code>TextView</code> 인스턴스를 멤버 변수로 두고, <code>Shape</code>의 인터페이스를 구현하면서 내부적으로 <code>TextView</code>의 기능을 호출해주는 방식이다.</p>
<p>반면에 <strong>클래스 어댑터</strong> 방식이라면 <code>TextShape</code>가 <code>Shape</code>를 상속함과 동시에 <code>TextView</code>도 상속받아야 한다. 즉, 다중 상속을 이용해서 <code>TextView</code> 기능을 바로 가져오는 구조이다. 하지만 이렇게 하면 유연성이 떨어지고, 언어 제약(자바는 다중 상속 불가) 때문에 현실적으로 잘 안 쓰이는 경우가 많다.</p>
<hr>
<h2 id="구조-🏗️">구조 🏗️</h2>
<h4 id="클래스-어댑터">클래스 어댑터</h4>
<img width="543" height="197" alt="Image" src="https://github.com/user-attachments/assets/37a23640-e4f8-4e9b-9439-b43edd57c22c" />

<h4 id="객체-어댑터">객체 어댑터</h4>
<img width="524" height="197" alt="Image" src="https://github.com/user-attachments/assets/67f292f1-5c1f-495a-bccd-59f669cf7ad6" />

<p>구조는 매우 간단하다. 클래스 어댑터는 상속(Inheritance) 을 이용해서 구현하고, 객체 어댑터는 합성(Composition) 을 이용해서 구현한다. 즉, 클래스 어댑터는 이미 존재하는 클래스를 직접 상속받아 새로운 인터페이스를 맞추는 방식이고, 객체 어댑터는 기존 클래스를 멤버 변수로 두고 그 객체의 기능을 위임(delegate)하는 방식이다.</p>
<p>클래스 어댑터는 상속을 쓰는 만큼 <strong>기존 클래스의 세부 구현에 강하게 묶인다.</strong> 대신 <strong>성능상 조금 더 단순하고 직접적이다.</strong></p>
<p>객체 어댑터는 <strong>합성을 쓰기 때문에 더 유연하고, 다른 클래스와도 쉽게 조합할 수 있다. 다형성을 활용하기에도 적합하다.</strong></p>
<p>정리하면, <strong>&quot;빠르고 단순하게&quot;라면 클래스 어댑터</strong>, <strong>&quot;유연하고 확장성 있게&quot;라면 객체 어댑터</strong>를 쓰는 게 맞다. </p>
<hr>
<h2 id="구현-💻">구현 💻</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;memory&gt;

using namespace std;

struct Point { int x{}, y{}; };
struct Size  { int w{}, h{}; };
struct Rect  { int x1{}, y1{}, x2{}, y2{}; };

ostream&amp; operator&lt;&lt;(ostream&amp; os, const Rect&amp; r) {
    return os &lt;&lt; &quot;Rect{(&quot; &lt;&lt; r.x1 &lt;&lt; &quot;,&quot; &lt;&lt; r.y1 &lt;&lt; &quot;) ~ (&quot; &lt;&lt; r.x2 &lt;&lt; &quot;,&quot; &lt;&lt; r.y2 &lt;&lt; &quot;)}&quot;;
}

class Manipulate;
class TextManipulator;

class Shape{
    public:
        ~Shape()  = default;
        virtual void boundingBox() const = 0;
        virtual unique_ptr&lt;Manipulate&gt; createManipulate() const = 0;
};

class Manipulate {
    public:
        Manipulate() = default;               
        virtual ~Manipulate() = default;
        virtual void manipulate() const {
            std::cout &lt;&lt; &quot;Shape 조작\n&quot;;
        }
};

class TextManipulator : public Manipulate
{
    public:
        void manipulate() const override {
            std::cout &lt;&lt; &quot;TextShape 조작\n&quot;;
        }
};

class Line : public Shape
{
    public:
        Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}

        void boundingBox() const override {
            Rect r{
                min(p1_.x, p2_.x),
                min(p1_.y, p2_.y),
                max(p1_.x, p2_.x),
                max(p1_.y, p2_.y)
            };
            std::cout &lt;&lt; &quot;[Line] boundingBox = &quot; &lt;&lt; r &lt;&lt; &quot;\n&quot;;
        }

        unique_ptr&lt;Manipulate&gt; createManipulate() const override {
            return make_unique&lt;Manipulate&gt;();
        }

    private:
        Point p1_{}, p2_{};
};

class TextView{
    public: 
        virtual ~TextView() = default;
        Point getOrigin() const { return origin_; }
        Size  getExtent() const { return extent_; }

        virtual bool isEmpty() const = 0;

    protected:
        void setOrigin(Point p) { origin_ = p; }
        void setExtent(Size s)  { extent_ = s; }

    private:
        Point origin_{0, 0};
        Size  extent_{0, 0};
};

class TextShape : public Shape, private TextView
{
    public:
        TextShape(Point origin, Size extent, bool empty = false) : empty_(empty) {
            setOrigin(origin);
            setExtent(extent);
        }

    void boundingBox() const override {
        Point o = getOrigin();
        Size  s = getExtent();
        Rect r{o.x, o.y, o.x + s.w, o.y + s.h};
        cout &lt;&lt; &quot;[TextShape] origin=(&quot; &lt;&lt; o.x &lt;&lt; &quot;,&quot; &lt;&lt; o.y
            &lt;&lt; &quot;), extent=(&quot; &lt;&lt; s.w &lt;&lt; &quot;,&quot; &lt;&lt; s.h &lt;&lt; &quot;) -&gt; boundingBox = &quot;
            &lt;&lt; r &lt;&lt; &quot;\n&quot;;
        }

        unique_ptr&lt;Manipulate&gt; createManipulate() const override {
            return make_unique&lt;TextManipulator&gt;();
        }

        bool isEmpty() const override {
            return empty_;
        }

    private:
        bool empty_{false};
};

int main() {
    unique_ptr&lt;Shape&gt; s1 = make_unique&lt;Line&gt;(Point{10, 5}, Point{2, 20});
    s1-&gt;boundingBox();
    s1-&gt;createManipulate()-&gt;manipulate();

    unique_ptr&lt;Shape&gt; s2 = make_unique&lt;TextShape&gt;(Point{100, 200}, Size{50, 20});
    s2-&gt;boundingBox();
    s2-&gt;createManipulate()-&gt;manipulate();

    return 0;
}</code></pre>
<p>책에서 예제로 든 <code>Shape</code>에 <code>TextView</code>를 결합하는 <strong>클래스 어댑터</strong>이다. <code>Shape</code> 는 2개의 기능을 제공하는데 <code>Shape</code>를 생성하면 경계선 박스를 만드는 함수 <code>boundingBox()</code> 그리고 <code>Shape</code>를 이동시키거나 조작하는 조작기를 생성하는 <code>createManipulate()</code> 이 2가지의 기능을 제공한다. 이때 기존에 원래 존재하는 <code>Line</code>은 <code>Shape</code>의 기능을 그대로 상속받아 구현하고 있다. 우리는 <code>TextShape</code>라는 어댑터를 통해 <code>TextView</code>를 <code>Shape</code>에서 사용할 수 있도록 하는 것이 목표이다.</p>
<h4 id="adaptee">Adaptee</h4>
<pre><code class="language-cpp">class TextView{
    public: 
        virtual ~TextView() = default;
        Point getOrigin() const { return origin_; }
        Size  getExtent() const { return extent_; }

        virtual bool isEmpty() const = 0;

    protected:
        void setOrigin(Point p) { origin_ = p; }
        void setExtent(Size s)  { extent_ = s; }

    private:
        Point origin_{0, 0};
        Size  extent_{0, 0};
};</code></pre>
<p><code>TextView</code>는 3개의 기능이 존재하는데, 자신의 위치와 크기를 알려주는<code>getOrigin()</code>, <code>getExtent()</code> 두가지 기능과 텍스트가 채워져 있는지 아닌지를 알려주는 <code>isEmpty()</code>가 있다. 따라서 Target인 <code>Shape</code> 가 제공하는 두가지 기능인  <code>boundingBox()</code> 와 <code>createManipulator()</code>를 연동하기 위해서 기존의 <code>TextView</code>의 기능을 적절히 조합해서 만들거나 아예 새로운 코드를 넣어서 기능을 연동시켜야 한다.</p>
<h4 id="adapter">Adapter</h4>
<pre><code class="language-cpp">class TextShape : public Shape, private TextView
{
    public:
        TextShape(Point origin, Size extent, bool empty = false) : empty_(empty) {
            setOrigin(origin);
            setExtent(extent);
        }

    void boundingBox() const override {
        Point o = getOrigin();
        Size  s = getExtent();
        Rect r{o.x, o.y, o.x + s.w, o.y + s.h};
        cout &lt;&lt; &quot;[TextShape] origin=(&quot; &lt;&lt; o.x &lt;&lt; &quot;,&quot; &lt;&lt; o.y
            &lt;&lt; &quot;), extent=(&quot; &lt;&lt; s.w &lt;&lt; &quot;,&quot; &lt;&lt; s.h &lt;&lt; &quot;) -&gt; boundingBox = &quot;
            &lt;&lt; r &lt;&lt; &quot;\n&quot;;
        }

        unique_ptr&lt;Manipulate&gt; createManipulate() const override {
            return make_unique&lt;TextManipulator&gt;();
        }

        bool isEmpty() const override {
            return empty_;
        }

    private:
        bool empty_{false};
};</code></pre>
<p><code>TextShape</code>는 말한 것 처럼 다중상속을 통해 <code>Shape</code> 와 <code>TextView</code>를 받고 있다. 여기서 중요한 점이 Adaptee인 TextView는 Private로 해야 한다. 이유는 당연히 Target이 Adaptee를 Adater를 통해 사용할 때 내부의 구조를 알 필요가 없기 때문이다. <code>boundingBox()</code>를 보면 <code>TextView</code>의 <code>getOrigin()</code> 와 <code>getExtent()</code>를 사용해서 위치와 크기를 얻고 경계 박스를 구현하는 것으로 연동을 완료했다. 그런데 <code>createManipulator()</code>는 기존의 기능으로 연동이 불가능 하므로 새로운 <code>TextManipulator</code>를 생성해서 연동해야 한다.</p>
<h4 id="textmanipulator">TextManipulator</h4>
<pre><code class="language-cpp">class TextManipulator : public Manipulate
{
    public:
        void manipulate() const override {
            std::cout &lt;&lt; &quot;TextShape 조작\n&quot;;
        }
};</code></pre>
<p>이렇게 만든 <code>TextManipulator</code>를 통해 완벽히 연동이 되었다. 이제 클라이언트는 기존에 Shape를 이용하는 방식으로 TextView를 이용가능하다.</p>
<h4 id="client">Client</h4>
<pre><code class="language-cpp">int main() {
    unique_ptr&lt;Shape&gt; s1 = make_unique&lt;Line&gt;(Point{10, 5}, Point{2, 20});
    s1-&gt;boundingBox();
    s1-&gt;createManipulate()-&gt;manipulate();

    unique_ptr&lt;Shape&gt; s2 = make_unique&lt;TextShape&gt;(Point{100, 200}, Size{50, 20});
    s2-&gt;boundingBox();
    s2-&gt;createManipulate()-&gt;manipulate();

    return 0;
}</code></pre>
<p><code>s1</code>으로 <code>Line</code>을 만들고 <code>boundingBox()</code> 와 <code>createManipulate()</code>를 사용하고 있다. 그리고 <code>s2</code>로 <code>TextShape</code>를 만들고 똑같이 <code>boundingBox()</code> 와 <code>createManipulate()</code>를 사용하고 있다. 이렇게 완벽히 연동이 되었다. </p>
<p>이번에는 객체 어댑터는 어떻게 구현하는지 알아보자.</p>
<h4 id="adapter-1">Adapter</h4>
<pre><code class="language-cpp">class TextShape : public Shape{
public: 
    TextShape(shared_ptr&lt;TextView&gt; tv) : tv_(std::move(tv)) {}

    void boundingBox() const override {
        Point o = tv_-&gt;getOrigin();
        Size  s = tv_-&gt;getExtent();
        Rect r{o.x, o.y, o.x + s.w, o.y + s.h};
        cout &lt;&lt; &quot;[TextShape(ObjectAdapter)] origin=(&quot; &lt;&lt; o.x &lt;&lt; &quot;,&quot; &lt;&lt; o.y
            &lt;&lt; &quot;), extent=(&quot; &lt;&lt; s.w &lt;&lt; &quot;,&quot; &lt;&lt; s.h &lt;&lt; &quot;) -&gt; boundingBox = &quot;
            &lt;&lt; r &lt;&lt; &quot;\n&quot;;
    }

    unique_ptr&lt;Manipulate&gt; createManipulate() const override {
        return make_unique&lt;TextManipulator&gt;();
    }

    bool empty() const { return tv_-&gt;isEmpty(); }

private:
    shared_ptr&lt;TextView&gt; tv_;
};</code></pre>
<p><code>TextShape</code>는 <code>TextView</code>를 공유 포인터로 생성하면서 생성된다. 따라서 <code>TextView</code>를 합성하여 인스턴스로 가지고 있다.</p>
<h4 id="client-1">Client</h4>
<pre><code class="language-cpp">int main() {
    unique_ptr&lt;Shape&gt; s1 = make_unique&lt;Line&gt;(Point{10, 5}, Point{2, 20});
    s1-&gt;boundingBox();
    s1-&gt;createManipulate()-&gt;manipulate();

    auto tv = make_shared&lt;SimpleTextShape&gt;(Point{100, 200}, Size{50, 20});
    unique_ptr&lt;Shape&gt; s2 = make_unique&lt;TextShape&gt;(tv);
    s2-&gt;boundingBox();
    s2-&gt;createManipulate()-&gt;manipulate();

    return 0;
}</code></pre>
<p>따라서 먼저 <code>TextShape</code>를 생성한 다음 <code>Shape</code>에 주입을 해야 한다. 만약에 <code>TextView</code> 여러개의 서브클래스로 다양한 기능이 있다고 해보자. </p>
<pre><code class="language-cpp">class SimpleTextView : public TextView {
public:
    explicit SimpleTextView(Point origin, Size extent, bool empty = false)
        : empty_(empty)
    {
        setOrigin(origin);
        setExtent(extent);
    }

    bool isEmpty() const override { return empty_; }

private:
    bool empty_{false};
};</code></pre>
<p>이렇게 <code>SimpleTextView</code>라는 TextView의 기능을 확장해주는 서브클래스를 바로 주입이 가능하다. 그러면 우리는 객체 어댑터를 통해 TextShape를 여러가지 형태로 만들 수 있을 것이다. 이것이 클래스 어댑터에는 없는 객체 어댑터의 장점이다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>어댑터 패턴은 서로 다른 인터페이스를 가진 클래스들을 연결해주는 역할을 한다고 보면 된다. 클래스 어댑터는 상속으로, 객체 어댑터는 합성으로 풀어내는데, 결국 상황에 따라 어떤 방식을 선택할지가 달라진다. 내가 글에서 보여준 것처럼, <code>Shape</code>와 <code>TextView</code>를 연동할 때도 두 가지 방식 모두 동작은 되지만, 유연성과 확장성을 생각하면 <strong>객체 어댑터 쪽이 좀 더 현실적이</strong>라고 할 수 있다.</p>
<p>다음 글에서는 구조 패턴 중에서 <code>Bridge Pattern</code>을 소개할 생각이다. 브리지 패턴은 이름처럼 <strong>추상과 구현을 분리해서 독립적으로 확장할 수 있게</strong> 만들어주는 패턴인데, 어댑터 패턴과 비교하면 더 일반화된 구조를 갖는다. 즉, 인터페이스 불일치를 해결하는 게 목적이었던 어댑터와 달리, <strong>브리지는 애초에 확장 가능성을 열어두는 구조 설계</strong>에 초점이 맞춰져 있다</p>
<p><a href="https://www.cs.unc.edu/~stotts/GOF/hires/pat4afso.htm">[참고] Adapter Pattern</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] Fine-tuning]]></title>
            <link>https://velog.io/@choi-hyk/LLM-Fine-tuning</link>
            <guid>https://velog.io/@choi-hyk/LLM-Fine-tuning</guid>
            <pubDate>Sat, 23 Aug 2025 07:37:30 GMT</pubDate>
            <description><![CDATA[<h1 id="🖥️-fine-tuning">🖥️ Fine-tuning</h1>
<p>Transformer 구조를 이해했으니, 이제 자연스럽게 <strong>&quot;그럼 이렇게 만들어진 모델을 어떻게 내 태스크에 맞게 쓰는가?&quot;</strong> 라는 질문이 생긴다. 바로 여기서 <strong>Fine-tuning(파인튜닝)</strong> 이라는 개념이 나온다.</p>
<p>LLM이나 Transformer 모델은 처음에 <strong>Corpus</strong> 로 학습된다. 이 과정에서 모델은 언어의 전반적인 패턴, 문법, 의미 관계를 배우게 되는데, 이걸 <strong>사전학습(Pre-training)</strong> 이라고 한다. 앞의 글에서 살펴본 <strong>Transformer</strong> 과정이 사전학습을 진행하는 과정이다. 하지만 이렇게 학습된 모델을 바로 특정 태스크에 쓰기는 어렵다. 그 이유는 <strong>모든 도메인 지식</strong>을 아우를 수는 없기 때문이다. <strong>Corpus</strong>를 통해 LLM들은 방대한 양의 지식을 가지고 있지만 새로 생성된 지식이나, 특정 구조에 맞춰서 입력을 만들어야하는 모델이 필요하다면, 기존의 LLM 모델들을 자신의 목적에 맞게 업그레이드(?) 하고 싶을 것이다.</p>
<p>그래서 하는 게 바로 <strong>Fine-tuning</strong>이다. <del>물론 엄밀히 말하면 업그레이드는 아니다.</del> 원리를 간단히 말하면 이렇다.</p>
<p><strong>Transformer 내부는 기본적으로 Attention, FFN, Embedding 같은 블록으로 구성</strong>되어 있고, 이 블록들 안에는 수많은 <strong>가중치(Weight)</strong> 가 들어있다. Pre-training에서 이미 이 가중치들이 언어를 잘 다룰 수 있게 학습되어 있다. Fine-tuning에서는 이렇게 <strong>이미 구성된 LLM</strong> 에 <strong>내 태스크 데이터셋을 다시 넣고, 역전파(Backpropagation)</strong> 를 통해 가중치를 조금씩 조정한다. 여기서 역전파를 많이 들어봤을텐데. 사실 Pre-training 과정에서 이미 모델은 역전파를 통해 가중치를 조정한다. 파인튜닝은 이러한 역전파를 새로운 데이터셋으로 시도하는 것이라 보면 된다. <strong>Transformer</strong> 에서 모델을 완성하기 위해 <strong>최초의 Corpus로부터 각 layer의 가중치를 업데이트 하는 것을 반복하는 것이 Pre-training에서 진행되는 과정</strong> 이고, <strong>Fine-tuning은 완성된 모델을 다시 원하는 출력을 만드는 모델로 바꾸기 위해 새로운 입력을 넣고 가중치를 업데이트 하는 것을 반복하는 것</strong> 이다. 따라서 이전 글의 Transformer의 구조만 잘 이해하고 있으면, Fine-tuning은 이해하기 쉬울 것이다. 그러므로 이번 글에서는 Fine-tuning 자체 보다는 <strong>역전파(Backpropagation)</strong> 의 과정을 설명할 생각이다.</p>
<hr>
<h2 id="🖇️-역전파backpropagation">🖇️ 역전파(Backpropagation)</h2>
<p>사실 ** Fine-tuning이라는 개념은 Transformer가 등장하기 전부터 존재했다.** 예전에는 CNN 같은 컴퓨터 비전 모델을 학습할 때도, 대규모 데이터셋으로 학습된 모델의 가중치를 가져와 새로운 이미지 분류 작업에 맞게 일부 층만 조정하는 방식으로 활용했다. 즉, fine-tuning 자체는 오래된 개념이지만, 현대 LLM에서는 <strong>Transformer 아키텍처 위에서 이루어진다는 점이 다르다.</strong> 따라서 우리는 현대 모델들이 사용하는Transformer 구조를 기반으로 이해하면 된다. </p>
<p>Transformer를 기반으로 하는 Fine-tuning을 구체적으로 보면 <strong>Self-Attention, Multi-head Attention</strong> 같은 구조는 그대로 두고, 내부 가중치 행렬 $W^Q, W^K, W^V$, 그리고 Feed Forward Network(FFN)의 $W_1, W_2$ 같은 파라미터들이 <strong>다시 학습 대상</strong>이 된다. 이때 학습 과정은 Pre-training 때와 똑같이 <strong>순전파 → 손실 계산 → 역전파 → 가중치 업데이트</strong>로 돌아간다. 차이는 단지 <strong>데이터셋의 목적</strong>이다. Pre-training 때는 일반 텍스트 전체, Fine-tuning 때는 특정 태스크에 맞는 데이터라는 점이 다르다.</p>
<p>용어들에 대해서 헷갈릴 것 같은데, 정리를 해보겠다. </p>
<ul>
<li><p><strong>순전파(Forward Propagation)</strong>: 입력 데이터를 모델에 넣어서 예측값을 뽑아내는 과정. Transformer라면 입력 토큰이 Self-Attention, Multi-head Attention, FFN 등을 거쳐서 최종 출력 확률로 바뀌는 걸 의미.</p>
</li>
<li><p><strong>손실(Loss)</strong>: 모델의 예측과 정답 사이의 차이를 수치로 나타낸 값. <strong>Cross-Entropy</strong> 같은 걸 많이 쓰고, <strong>이 값이 클수록 모델이 정답과 멀리 있는 것.</strong></p>
</li>
<li><p><strong>역전파(Backpropagation)</strong>: 손실 값을 기준으로 <strong>어떤 가중치가 얼마나 잘못했는지</strong> 를 계산해서 뒤로 흘려보내는 과정. 각 층의 $W^Q, W^K, W^V, W_1, W_2$ 같은 파라미터가 손실에 얼마나 기여했는지 <strong>기울기를 구함.</strong></p>
</li>
<li><p><strong>가중치 업데이트(Weight Update)</strong>: 역전파로 구한 기울기를 바탕으로 실제 파라미터 값을 조금씩 수정하는 단계. 보통 <strong>SGD, Adam 같은 옵티마이저</strong> 가 이 역할을 담당.</p>
</li>
</ul>
<p>손실 계산부터 어떻게 파인튜닝이 진행되는지 살펴보자</p>
<h3 id="손실-계산">손실 계산</h3>
<p>예측 확률 $p$와 정답 레이블 $y$를 비교해서 손실 $\ell$을 구한다.</p>
<ul>
<li>Cross-Entropy 손실 기준으로는 $\ell = -\sum y \log p$.</li>
</ul>
<p>음의 로그 함수를 사용해서, 정답 클래스의 $p$를 음의 로그 스케일로 보면, 정답 확률이 0에 가까워 지면, 손실 $\ell$은 무한대로 증가하고 1에 가까워지면 $\ell$은 0에 가까워질 것이다.</p>
<hr>
<h3 id="오차-신호gradient-계산">오차 신호(Gradient) 계산</h3>
<p>손실을 로짓 $z$에 대해 미분하면</p>
<p>   $$
   \frac{\partial \ell}{\partial z} = p - y
   $$</p>
<p>이 값이 나온다 (값을 도출하는 것은 논문이나 다른 글에서 확인바람...). 이게 바로 역전파로 흘러가는 오차 신호다. 로짓 $z$ 는 <strong>예측 확률 $p$ 를 소프트맥스를 하여 확률 분포로 나타내기 직전의 상태</strong> 이다. 따라서 확률 데이터가 아닌, 단순히 <strong>점수(score)로서 정답과 얼마나 가까운지를 상대적으로 보여주는 값</strong> 이라 보면 된다. 따라서 <strong>&quot;손실률에 대해 로짓을 미분했다&quot;</strong> 는 것은, <strong>로짓 값이 바뀔 때 손실이 얼마나 영향을 받는지를 수치로 나타내는 것</strong> 이다.</p>
<p>클래스를 개(dog), 고양이(cat), 소(cow)로 두고 예시를 들어보자. 정답은 고양이라고 하자.</p>
<h4 id="1-모델이-낸-로짓-점수">1. 모델이 낸 로짓 (점수)</h4>
<p>$$
z = [2.0,; 1.0,; -0.5]
$$</p>
<ul>
<li>개 = 2.0</li>
<li>고양이 = 1.0</li>
<li>소 = -0.5</li>
</ul>
<p>로짓은 아직 확률이 아니라 “점수” 같은 거라 보면 된다. 여기서는 개가 가장 높은 점수를 가지고 있다. 따라서 오차가 있는 출력 값이다.</p>
<h4 id="2-소프트맥스-→-확률-분포">2. 소프트맥스 → 확률 분포</h4>
<p>$$
p = \text{softmax}(z) = [0.62,; 0.34,; 0.04]
$$</p>
<ul>
<li>개일 확률 = 62%</li>
<li>고양이일 확률 = 34%</li>
<li>소일 확률 = 4%</li>
</ul>
<p>정답은 고양이인데, 모델은 개가 더 맞다고 본 상황이므로 이를 통해서 오차 신호를 계산할 수 있다.</p>
<h4 id="3-오차-신호-계산">3. 오차 신호 계산</h4>
<p>$$
p-y = [0.62-0,; 0.34-1,; 0.04-0] = [0.62,; -0.66,; 0.04]
$$</p>
<h4 id="4-해석">4. 해석</h4>
<ul>
<li><strong>개(dog): $+0.62$</strong> → 정답이 아닌데 너무 높게 잡았다. 점수를 내려야 한다.</li>
<li><strong>고양이(cat): $-0.66$</strong> → 정답인데 확률이 낮다. 점수를 올려야 한다.</li>
<li><strong>소(cow): $+0.04$</strong> → 정답이 아닌데 살짝 점수를 줬다. 조금 줄여야 한다.</li>
</ul>
<p>결국 $p-y$는 단순한 차이가 아니라, <strong>가중치를 어느 방향으로 바꿔야 하는지 알려주는 오차 신호</strong>다.</p>
<ul>
<li>정답 클래스는 음수 → 점수를 올려야 한다.</li>
<li>오답 클래스는 양수 → 점수를 내려야 한다.</li>
</ul>
<hr>
<h3 id="역전파-진행">역전파 진행</h3>
<p>이 오차 신호가 FFN, Multi-head Attention, Embedding까지 거꾸로 내려가면서 각 파라미터의 기울기 $\frac{\partial \ell}{\partial W}$를 계산한다.</p>
<h4 id="경사하강법으로-가중치-업데이트">경사하강법으로 가중치 업데이트</h4>
<p>계산된 기울기를 바탕으로 옵티마이저가 가중치를 업데이트한다. 가장 기본적인 SGD는 다음과 같다.</p>
<p>   $$
   W \leftarrow W - \eta \cdot \frac{\partial \ell}{\partial W}
   $$</p>
<ul>
<li>$\eta$: 학습률(learning rate).</li>
<li>기울기가 양수면 가중치를 줄이고, 음수면 가중치를 늘려서 손실을 줄이는 방향으로 움직인다.</li>
</ul>
<p>위의 예시로 다시 살펴보면 이렇게 된다.</p>
<p>$$
g ;=; p-y ;=; [,0.62,; -0.66,; 0.04,]
$$</p>
<p>$$
\frac{\partial \ell}{\partial b} = g,\qquad
$$</p>
<p>$$
W \leftarrow W - \eta,\frac{\partial \ell}{\partial W}
\quad,\quad
b \leftarrow b - \eta,\frac{\partial \ell}{\partial b}
$$</p>
<p>예를 들어 $\eta=0.1$이면</p>
<p>$$
b&#39; = b - 0.1,[,0.62,; -0.66,; 0.04,]
= \big[b_1-0.062,; b_2+0.066,; b_3-0.004\big]
$$</p>
<ul>
<li><strong>개(dog)</strong>: $g_1=+0.62\Rightarrow b_1$ 감소(점수↓)</li>
<li><strong>고양이(cat)</strong>: $g_2=-0.66\Rightarrow b_2$ 증가(점수↑)</li>
<li><strong>소(cow)</strong>: $g_3=+0.04\Rightarrow b_3$ 소폭 감소(점수↓)</li>
</ul>
<h4 id="g_k0-오답을-과대평가-⇒-z_k-감소">$g_k&gt;0$ (오답을 과대평가) ⇒ $z_k$ <strong>감소</strong></h4>
<h4 id="g_k0-정답을-과소평가-⇒-z_k-증가">$g_k&lt;0$ (정답을 과소평가) ⇒ $z_k$ <strong>증가</strong></h4>
<p>이후 $z&#39;$에 softmax를 다시 적용하면, <strong>고양이 확률이 올라가고 개/소는 내려가는</strong> 방향으로 조정된다.</p>
<p>한마디로 요약하자면</p>
<p><strong>정답인데 확률을 높여야 된다!</strong></p>
<ul>
<li>로짓 $z$를 크게 만들어서 손실률을 줄여야 하므로 기울기 $g$가 음수 </li>
<li>기울기가 음수 이므로 경사하강법 $W -\eta{g}$ 적용 했을 때, <strong>가중치가 증가 함</strong> </li>
</ul>
<p><strong>오답인데 확률이 너무 높다!</strong></p>
<ul>
<li>로짓 $z$를 작게 만들어서 손실률을 줄여야 하므로 기울기 $g$가 양수</li>
<li>기울기가 양수이므로  경사하강법 $W -\eta{g}$ 적용 했을 때, <strong>가중치가 감소 함</strong> </li>
</ul>
<hr>
<h3 id="반복">반복</h3>
<p>이 과정을 수천~수억 번 반복하면서 모델은 점점 내 데이터셋에 맞게 적응한다. 결국 Fine-tuning은 <strong>손실을 계산하고, 그걸 역전파로 풀어내서, 경사하강법으로 가중치를 조금씩 고쳐나가는 과정</strong>이다. 새로운 모델을 처음부터 만드는 게 아니라, 이미 언어 감각을 익힌 Transformer를 내가 원하는 태스크에 맞게 조금씩 조율하는 작업이라고 보면 된다. 그리고 이러한 반복을 <strong>epoch</strong> 이라고 한다</p>
<hr>
<h1 id="😘-마무리">😘 마무리</h1>
<p>이번에는 파인튜닝의 역전파의 기본 원리에 대해 알아보았다. 다음글에서는 현대 모델에서 표준으로 자리잡은 파인튜닝 기법인 <strong>LoRA(Low-Rank Adaptation)</strong> 를 살펴보고, 실제 코드와 함께 파인튜닝 과정을 정리해보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] Transformer]]></title>
            <link>https://velog.io/@choi-hyk/LLM-Transformer</link>
            <guid>https://velog.io/@choi-hyk/LLM-Transformer</guid>
            <pubDate>Sun, 17 Aug 2025 09:03:08 GMT</pubDate>
            <description><![CDATA[<h1 id="🖱️-transformer">🖱️ Transformer</h1>
<p>오늘은 현대 LLM의 모델들이 활용중인 가장 중요한 요소인 <strong>Transformer</strong>에 대해서 알아보겠다. Transformer는 2017년 Google에서 발표한 <strong>「Attention is All you need」</strong> 논문에서 소개된 모델이다.</p>
<p><a href="https://arxiv.org/abs/1706.03762">Attention is All you need</a></p>
<p>해당 논문에서는 Transformer 아키텍처가 고안된 이유를 RNN의 단점을 서술하면서 설명하고, Transformer 아키텍처의 구성 방식을 각 layer를 기준으로 설명을 한다. 내용이 너무 어려워서 여러가지 영상이랑, 해석본도 찾아보면서 최대한 정리를 해보았다...</p>
<hr>
<h2 id="✏️-transformer가-고안된-이유">✏️ Transformer가 고안된 이유</h2>
<p><strong>「Attention is All you need」</strong> 에서는 먼저 RNN의 단점을 이야기하는데, RNN은 3가지의 주요 단점이 있다.</p>
<ul>
<li><p><strong>순차적 처리</strong>: RNN은 단어를 순서대로 처리하는 구조 때문에 병렬 처리가 불가능하다. 이는 대규모 데이터 학습에 많은 시간이 소요되는 원인이 된다.</p>
</li>
<li><p><strong>장기 의존성(Long-term Dependency) 문제</strong>: 문장이 길어질수록 초반부 단어의 정보가 점차 희미해진다. 이로 인해 문장의 앞부분에 있는 중요한 맥락 정보를 활용하기 어렵다.</p>
</li>
<li><p><strong>고정된 컨텍스트 벡터</strong>: RNN은 문장 전체의 정보를 하나의 고정된 크기 벡터에 압축하는데, 이 과정에서 정보 손실이 발생하여 복잡한 문장의 의미를 온전히 담기 어렵다.</p>
</li>
</ul>
<p>간단하게 RNN 문장이 길어질수록 성능이 떨어지고, 문장을 순서대로 처리를 해야되기 때문에, 병렬처리가 불가능하다. 또한 RNN과 더불어 <strong>CNN(Convolutional Neural Network)</strong> 이라는 신경망 기술에 대해서도 설명을 하였는데, 간단하게 CNN은 &quot;합성곱 신경망&quot;이라는 기술이다. CNN은 이미지 및 비디오와 같은 2차원 또는 3차원 데이터 처리에 특화된 딥러닝 모델인데, 데이터의 특징을 추출하여 분류나 탐지 같은 작업에 강점을 가진다. 하지만 CNN 또한 문장 내 단어 간의 <strong>장기적인 의존 관계</strong> 를 학습하는 데는 한계가 있었다.</p>
<p>즉, RNN은 순차 처리와 장기 의존성 문제, CNN은 문장의 전체 맥락을 포착하기 어렵다는 문제를 가지고 있었다.</p>
<p>따라서 Transformer는 에서 설명한 RNN과 CNN의 문제점인 병럴처리, 장기 의존성 그리고 다양한 문장의 표현을 고려한 모델이다. </p>
<hr>
<h2 id="🛠️-구조">🛠️ 구조</h2>
<img width="1520" height="2239" alt="Image" src="https://github.com/user-attachments/assets/d643741a-915e-4c5f-9aaf-a6fdeef848e3" />

<p>위의 구조를 보면 머리가 좀 아파올 것 같은데, layer 별로 나눠서 이해해보면 좀 괜찮을 것이다. 그림은 Transformer에서 사용하는 <strong>Encoder와 Decoder</strong>의 구조를 나타낸 그림이다. <strong>Encoder는 입력이 주어지면, 해당 입력을 기반으로 입력 데이터들 관의 관계를 파악</strong>하고, <strong>Decoder는 해당 입력과 Encoder에서 생성된 관계를 통해 다음 데이터를 예측</strong>한다. 이제 이 그림을 각 layer마다 살펴보겠다.</p>
<hr>
<h3 id="input-embedding">Input Embedding</h3>
<p>제일 먼저 Encoding에서 보이는 <strong>Input Embedding</strong> 은 자연어를 <strong>Tokenizer</strong> 해서 벡터화를 한 데이터이다.<br><strong>Tokenizer</strong> 와 Embedding은 추후에 다른 글로 알아보도록하고, </p>
<p>Embedding으로 벡터화를 하게 되면 각 토큰의 특징을 나타내는 차원이 생긴다. 예를 들어서 <code>바나나</code> 라는 토큰이 있으면, 해당 토큰은 여러개의 특징으로 나타낼 수 있다. </p>
<p>각 특징은 벡터의 한 요소로 표현되며, 예를 들어 바나나라는 단어가 4차원 벡터로 임베딩 되었다고 하면,</p>
<p>바나나 → [0.12, -0.87, 0.33, 0.55]</p>
<p>이런 식으로 수치화된다. 이 벡터의 각 값은 단순히 숫자가 아니라, 의미적인 특징을 압축한 값이다. 어떤 값은 과일과 관련된 의미를, 또 어떤 값은 음식이라는 카테고리적 의미를, 또 다른 값은 다른 단어들과의 관계 속에서 파생된 의미를 담고 있다.</p>
<p>이렇게 만들어진 Input Embedding은 이후 <strong>Positional Encoding</strong> 과 결합된다. Transformer는 RNN처럼 순차적으로 단어를 처리하지 않기 때문에, 단어의 순서 정보를 따로 제공해야 한다. 이때 사용하는 것이 Positional Encoding(위치 인코딩) 인데, 잠시 후에 알아보겠다.</p>
<hr>
<h3 id="attention">Attention</h3>
<p>Attention은 사실 Transformer에서 고안된 기법이 아니다. Attention은 벡터화 되어서, 각 특징들을 벡터로 나타내어진 자연어의 관계를 파악하는 기법이다. <strong>「Neural Machine Translation by Jointly Learning to Align and Translate」이라는 2014년 논문에서 RNN을 통한 기계번역을 개선하기 위해 고안된 기술</strong>이라고 한다.</p>
<p>그렇다면 드는 생각이, Transformer는 기존의 딥러닝 모델들과는 무엇이 다르냐는 것이다. Attention을 적용하는 기법에서 <strong>Transformer는 Self Attention과 Multi Head Attention 이라는 발전된 기법을 사용</strong>한다.</p>
<p>먼저 Attention의 기본 원리에 대해서 알아보자.</p>
<p>$$
Attention(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
$$</p>
<p>위의 식은 <strong>「Attention is All you need」</strong> 에서 설명하는 Attention을 구하는 식이다. </p>
<p><strong>Attention</strong>은 크게 세 단계로 진행된다. 먼저 입력 데이터는 <strong>Query(Q)</strong>, <strong>Key(K)</strong>, <strong>Value(V)</strong> 세 가지로 변환된다. 이때 <strong>Query는 현재 단어가 &quot;무엇을 찾고 있는가&quot;</strong> 를 나타내고, <strong>Key는 &quot;어떤 정보를 가지고 있는가&quot;</strong> 를, <strong>Value는 &quot;그 정보 자체&quot;</strong> 를 의미한다. </p>
<p>예를 들어서 검색창에 <strong>LLM과 관련된 논문</strong> 을 입력하면 해당 입력이 $Q$ 가 될 것이다. 그리고 검색 이후 나온 여러가지 웹 사이트와 논문들은 $K$ 가 되고 실제 논문 데이터는 $V$ 가 될 것이다. </p>
<p>이러한 <strong>$Query(Q)$</strong>, <strong>$Key(K)$</strong>, <strong>$Value(V)$</strong> 를 만드는 방식이 가중치 <strong>$W$</strong> 를 적용하는 것이다. 입력 임베딩 $X \in \mathbb{R}^{n \times d_{\text{model}}}$에 대해, 각각의 행렬은 다음과 같이 정의된다.</p>
<p>$$
Q = XW^Q, \quad K = XW^K, \quad V = XW^V
$$</p>
<p>여기서 $W^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}$, $W^K \in \mathbb{R}^{d_{\text{model}} \times d_k}$, $W^V \in \mathbb{R}^{d_{\text{model}} \times d_v}$ 는 모두 학습 가능한 파라미터이다.</p>
<p>즉, 하나의 입력 임베딩이 들어오더라도 서로 다른 가중치 행렬과 곱해지면서, <strong>질문을 하는 벡터(Q)</strong>, <strong>조건을 제공하는 벡터(K)</strong>, <strong>실제 정보를 전달하는 벡터(V)</strong> 로 투영된다.</p>
<p>그리고 $W^Q \in \mathbb{R}^{d_{\text{model}} \times d_k}$, $W^K \in \mathbb{R}^{d_{\text{model}} \times d_k}$, $W^V \in \mathbb{R}^{d_{\text{model}} \times d_v}$ 에서 보통 $d_k$, $d_v$는 $\frac{d_\text{model}}h$ 로 차원을 계산하게 되는데, 이와 관련해서 왜 가중치들의 집합의 크기가 $d_{\text{model}}\times d_k$, $d_{\text{model}}\times d_v$ 가 되는지 그리고 $h$가 무엇인지 궁금할 것이다. 일단 기억만 하고 있어라, 뒤에 <strong>Multi Head Attention</strong>에서 $h$ 가 무엇이고 집합의 크기가 왜 저렇게 나오는지, 그리고 차원을 맞추는 이유가 무엇인지 설명하겠다.</p>
<p>$Q$, $K$, $V를 구하는$ 과정은 단순한 선형 변환으로 보일 수 있지만, 학습을 통해 $W^Q, W^K, W^V$가 점차적으로 최적화되면서, <strong>Attention이 각 단어 간의 관계를 더 정교하게 파악할 수 있도록 만드는 핵심 장치</strong>가 된다. 결국 이 단계는 <strong>&quot;입력 임베딩을 서로 다른 관점에서 바라보는 방법&quot;을 모델이 스스로 학습하는 과정</strong> 이라고 이해할 수 있다.</p>
<p>그리고 이러한 가중치들은 최종적으로 <strong>디코더를 통해 생성된 확률 분포를 기반으로 손실 계산을 수행</strong>하고, 이 손실을 줄이기 위해 <strong>역전파와 가중치 조정 과정을 거쳐 업데이트된다.</strong> 이렇게 조정된 가중치는 모델의 성능을 향상시키며, 다음 번 예측의 정확도를 높이는 데 사용된다.</p>
<p><strong>새롭게 조정된 가중치로 모델은 다시 한번 입력 데이터를 받아 순전파를 시작</strong>한다. 참고로 순전파는 우리가 오늘 알아보는 Transformer의 과정이다. 인코더와 디코더는 업데이트된 가중치를 활용하여 입력 데이터의 문맥을 다시 파악하고, 디코더는 이를 기반으로 <strong>더 정확한 확률 분포를 생성</strong>한다. 이 반복적인 훈련 과정은 모델이 충분히 학습되어 <strong>손실값이 더 이상 줄어들지 않을 때까지 계속된다.</strong></p>
<p>이러한 가중치를 생성 및 조정하는 방법인 역전파는 다음글인 Fine-tuning에서 심도있게 정리해보겠다. 지금은 Attention에 집중해보도록 하자.</p>
<p>Attention은 위 세 가지 $Query(Q)$, $Key(K)$, $Value(V)$ 의 데이터들의 관계를 파악하는 기법이다. 다시 식으로 돌아가서, 각 데이터들은 임베딩 되어서 벡터로 표현된다고 했다. 그리고 각 벡터들은 차원(특징)을 가진다 했다. 여기서 하나의 $Q$는 여러 개의 $K$를 가질 것이다. 그리고 $Q$와 $K$의 개수는 $n{\text{ (토큰의 개수)}}\times d_k{\text{ (차원의 개수)}}$ 가 된다. 그리고 하나의 $Q$는 입력 값 $X$에서 생성된 같은 차원의 모든 $K$와 <strong>내적을 수행하여 유사도를 계산하고, 그 결과를 기반으로 각 $V$에 가중치를 부여하여 최종 Attention 출력을 만든다.</strong>  ${K^T}$는 전치 행렬을 의미한다. 전치 행렬로 만드는 이유는 아래의 식으로 설명하겠다.</p>
<p>$$
Q \in \mathbb{R}^{n \times d_k}, \quad K \in \mathbb{R}^{n \times d_k}
$$</p>
<p>위에서 말했다 싶이 $Q$와 $K$의 개수는 $n{\text{ (토큰의 개수)}}\times d_k{\text{ (차원의 개수)}}$ 이다. 이때 내적을 하기 위해서 행렬 곱을 하게 된다. 토큰의 행렬에서 행은 $n$을 열은 $d_k$를 나타낸다. 하지만 위의 크기로는 $Q$, $K$는 행렬곱을 하지 못한다. 따라서 전치를 통해 크기를 맞춰준다.</p>
<p>$$
K^T \in \mathbb{R}^{d_k \times n}
$$</p>
<p>$$
QK^T \in \mathbb{R}^{n \times n}
$$</p>
<p>이렇게 크기가 맞춰진 $Q$ 와 $K$는 $n\times n$ 크기의 어텐션 스코어(attention score)로 변환된다.</p>
<p>$$
\frac{QK^T}{\sqrt{d_k}}
$$</p>
<p>위의 식은 $Q$를 $K$와 내적을 한 값을 $\sqrt{d_k}$로 Scaling하는 것을 나타낸다. </p>
<p><strong>내적</strong> 은 $Query$가 $Key$와 얼마나 잘 맞는지를 나타내는 척도이며, 일종의 <strong>유사도(similarity) 점수</strong>라고 볼 수 있다. 고등학교때 배운 내적을 생각해보자</p>
<p>$Q = (1, 2, 3)$
$K_1 = (2, 0, 1)$
$K_2 = (-2, -1, 1)$
$K_3 = (0, 2, 2)$</p>
<p>이렇게 1개의 $Q$에 3개의 $K$가 있다고 해보자. 해당 벡터는 위에서 이야기한 가중치가 적용되어 3개의 차원으로 이루어진 값이다. 즉 ${d_k}$는 3이다.</p>
<p>$Q \cdot K_1 = 5$
$Q \cdot K_2 = -1$
$Q \cdot K_3 = 10$</p>
<p><em>행렬로 나타낸 경우</em></p>
<p>$$
QK^T = 
\begin{bmatrix}1 &amp; 2 &amp; 3\end{bmatrix}
\begin{bmatrix}
2 &amp; -2 &amp; 0 \
0 &amp; -1 &amp; 2 \
1 &amp; 1 &amp; 2
\end{bmatrix}
= \begin{bmatrix}5 &amp; -1 &amp; 10\end{bmatrix}
$$</p>
<p>위에서 내적의 결과를 보면, $K_3$가 10으로 $Q$ 와 가장 유사하다. 그리고 $K_2$가 -1로 가장 관련이 없다.</p>
<p>$\sqrt{d_k}$는 <strong>스케일링(scaling)</strong> 을 의미한다 앞에서 계산한 내적 결과는 $d_k$의 크기가 커질수록 값이 점점 커지게 된다 만약 차원이 수백 차원 이상으로 커진다면 내적 값은 지나치게 커지고 $softmax$ 함수에 넣었을 때 기울기가 매우 가팔라져 작은 차이에도 확률 분포가 한쪽으로 치우쳐 버린다</p>
<p>이를 방지하기 위해 내적 값을 차원의 제곱근으로 나누어 <strong>정규화(normalization)</strong> 를 해준다 예를 들어 위에서 $d_k = 3$이므로 $\sqrt{d_k} = \sqrt{3} \approx 1.73$ 이 된다</p>
<p>그럼 각각의 내적 값은 다음과 같이 스케일링된다</p>
<ul>
<li>$\frac{Q \cdot K_1}{\sqrt{3}} = \frac{5}{1.73} \approx 2.89$</li>
<li>$\frac{Q \cdot K_2}{\sqrt{3}} = \frac{-1}{1.73} \approx -0.58$</li>
<li>$\frac{Q \cdot K_3}{\sqrt{3}} = \frac{10}{1.73} \approx 5.77$</li>
</ul>
<p>이 과정을 거치면 값의 크기가 안정화되어 $Softmax$에 넣었을 때 적절한 확률 분포를 얻게 된다 즉 스케일링은 <strong>내적 값이 차원 수에 비례해 과도하게 커지는 문제를 제어하는 장치</strong>라고 이해하면 된다</p>
<p>그러면 이제 $Q$로부터 각 3개의 $K$의 관계를 알게 되었다. 그 다음으로 적용되는 것이 $softmax$이다. $softmax$는  $\frac{QK^T}{\sqrt{d_k}}$ 에서 나온 값 들을 전부 합 하였을 때, 1로 만들어주는 함수이다. 위의 경우에서는 $K_3$가 1에서 가장 많은 비율을 차지할 것이다. 그리고 $K_2$가 가장 적은 비율을 차지할 것이다. </p>
<p>마지막으로 가중합을 $V$에 적용하여 최종적인 정보의 관계를 생성하게 된다.</p>
<p>정리하자면, 특정 Query가 여러 Key들과 얼마나 관련성이 있는지를 Softmax를 통해 확률 값으로 바꾸게 되고, 이 확률 값이 바로 Attention에서 말하는 <strong>가중치(weight)</strong> 가 된다. 그리고 이 가중치는 Value $V$ 벡터에 곱해져 최종적으로 중요한 정보는 크게, 덜 중요한 정보는 작게 반영되도록 조절한다.</p>
<p>결과적으로 Attention 메커니즘은 “<strong>Query와 Key의 내적으로 구한 유사도를 스케일링 후 Softmax로 정규화하여, Value에 가중합을 적용하는 과정</strong>”이라고 정리할 수 있다.</p>
<h3 id="self-attention">Self Attention</h3>
<p>그렇다면 Self Attention은 무엇일까? 앞에서 예시는 검색엔진처럼 Query는 질문, Key는 문서의 제목, Value는 실제 내용으로 비유했다. 하지만 Self-Attention은 그 대상이 외부 데이터가 아니라, <strong>같은 문장 안의 토큰들끼리 서로를 참고하는 방식이다</strong>. 사실 앞에서 이야기한 &quot;하나의 $Q$는 입력 값 $X$에서 생성된 같은 차원의 모든 $K$와 <strong>내적을 수행하여 유사도를 계산한다&quot;</strong> 가 바로 Self Attention을 나타내는 말이었다. 그냥 Attention은 외부 데이터에서 $Q$, $K$, $V$를 각각 생성한다.</p>
<p>하나의 문장 <code>나는 학교에 간다</code>가 있다고 하면, 각 단어가 동시에 Q, K, V의 역할을 수행한다.</p>
<p>&quot;나는&quot; → Query를 만들고, Key와 Value도 만든다
&quot;학교에&quot; → Query, Key, Value를 모두 가진다
&quot;간다&quot; → 역시 Query, Key, Value를 가진다</p>
<p>이렇게 되면 문장 안에서 각 단어는 다른 단어와 자신 사이의 관련성을 계산할 수 있다. 이를 통해 처음에 말한 RNN에서 실현하지 못한 장기 의존성 그리고 다양한 문장의 표현을 해결이 가능하다. 그러면 마지막 목적인 병럴처리는 어떻게 실현이 가능할까? 바로 Multi Head Attention에 그 정답이 있다.</p>
<h3 id="multi-head-attention">Multi Head Attention</h3>
<img width="835" height="1282" alt="Image" src="https://github.com/user-attachments/assets/1991d59d-4881-4bba-8ac1-9e3b7c5e8be2" />

<p>먼저 Multi Head Attention에 대해서 알아보려면 head가 무엇인지 알아야 한다. 처음에 나는 차원이랑 헤드가 헷갈렸는데, 차원은 위에서 이야기 한 것처럼 각 토큰의 특징을 나타낸 것이다. </p>
<p>$d_{model} = 512$라면, 각 단어 토큰은 512개의 숫자로 표현된다.</p>
<p>&quot;바나나&quot;라는 토큰이 들어오면 벡터</p>
<p>$$
(0.12, -0.33, 0.98, \dots, 0.21) \in \mathbb{R}^{512}
$$</p>
<p>이런 식으로 512차원의 공간에서 하나의 점으로 표현된다. 즉 $\mathbb{R}^{512}$ 는 모델의 용량이 된다.</p>
<p>헤드는 <strong>Self-Attention을 여러 번 나눠서 병렬로 돌리는 단위</strong>이다.
예를 들어, $d_{model} = 512$이고 Head 수가 $h = 8$이라면,
각 Head는 차원을 나눠서</p>
<p>$$
d_{head} = \frac{d_{model}}{h} = 64
$$</p>
<p>의 크기로 Attention 연산을 한다.
전체 임베딩 512차원을 <strong>8개의 시각(Head)으로 쪼개서 동시에 바라본다</strong>고 이해하면 된다.</p>
<ul>
<li><strong>차원(dimension)</strong> → 토큰 벡터의 &quot;특징 공간&quot; 크기 (정보량)</li>
<li><strong>헤드(head)</strong> → 그 특징 공간을 &quot;여러 시각&quot;으로 분할해 병렬로 학습하는 방법</li>
</ul>
<p>즉 <strong>Multi Head Attention</strong>은 $h$번 만큼 Self Attention을 하는 것이다. 이를 통해 병렬처리를 실현 가능하다. </p>
<p>여기서 앞에서 이야기한 가중치의 크기와 $d_k, d_v$를 $\frac{d_{model}}{h}$로 맞추는 이유가 여기서 나온다.</p>
<p>$$
X \in \mathbb{R}^{n \times d_{\text{model}}}
$$</p>
<p>위의 식처럼 입력값 $X$의 크기는 $n$개의 토큰에서 차원 ($d_{\text{model}}$)을 곱한 값이다. 그러면 각 $Q$, $K$, $V$를 생성하기 위해 가중치와 곱하게 되는데 이때 $h$개 ($d_k$, $d_v$) head로 Self Attention을 하게 된다. 그 뜻은 원래 차원 각각에 $h$개의 특징 추가시키는 것이다. 위의 예시인 경우 512개의 차원에서 8개의 특징을 추가하여 512와 8을 곱한 98,304개가 된다.   </p>
<p>따라서 가중치들의 크기는 $d_{\text{model}}\times d_k$, $d_{\text{model}}\times d_v$  </p>
<p>$$
Q = XW^Q, \quad K = XW^K, \quad V = XW^V
$$</p>
<p>위에서 본 해당 식에서 각 행렬 곱을 하게 되면 
$$(n \times d_{\text{model}}) \times (d_{\text{model}} \times d_k) = (n \times d_k)$$
$$(n \times d_{\text{model}}) \times (d_{\text{model}} \times d_k) = (n \times d_v)$$</p>
<p>위의 예시에서 Self Attention의 각 차원은 64개 일것이다. 그리고 8번의 Self Attention을 하면 총 <strong>512개의 차원이 다시 완성되는 것이다</strong>. 이렇게 해야지 다음 과정인 Residual Connection(잔차 연결) 같은 구조에서 입력($d_{model}$)과 출력($d_{model}$)을 그대로 더할 수 있고, 다음 Feed-Forward Layer도 <strong>동일한 차원에서 작동할 수 있게 된다.</strong> 이렇게 $h$로 나누는 방법으로 차원을 맞추는 것이다.</p>
<p>$$
MultiHead(Q, K, V) = Concat(head_1, \dots, head_h)W^O
$$</p>
<p>$$
\text{where } head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
$$</p>
<p>위의 식은 Self Attention 이 $head$번 만큼 일어나고 각 결과를 더해서 <strong>Multi Head Attention</strong>을 구성하는 것을 나타낸다.</p>
<p><strong>Multi Head Attention</strong>까지 실행하면, 아마 입력값에 대해 모델이 충분히 이해했다고 생각할 것이다. 하지만 하나 빠트린것이 있는데, 바로 입력 값의 위치에 대한 정보이다.</p>
<hr>
<h3 id="positional-encoding">Positional Encoding</h3>
<p>자연어에서 입력값의 위치는 매우 중요하다. 그런데 위치를 고려하게 되면 RNN에서 장기 의존성(Long-term Dependency)와 같이 위치에 따른 정보 손실과 순차적 처리로 인해 병렬처리가 불가능하다. 이러한 위치에 따른 정보를 Transformer에서는 다른 기법으로 적용하였는데, 바로 <strong>Positional Encoding</strong>으로 적용을 한 것이다.</p>
<p>Transformer는 RNN처럼 순차적으로 단어를 처리하지 않기 때문에, 입력 토큰의 <strong>순서 정보</strong>가 사라지는 문제가 있다. 즉, &quot;나는 학교에 간다&quot;라는 문장이 들어와도, Transformer 입장에서는 단순히 4개의 벡터 집합일 뿐 &quot;나는 → 학교에 → 간다&quot;라는 순서 관계를 알 수 없다.</p>
<p>이를 해결하기 위해 <strong>Positional Encoding(위치 인코딩)</strong>을 추가한다. Positional Encoding은 각 토큰의 임베딩 벡터에 &quot;해당 토큰이 문장 내 몇 번째 위치에 있는지&quot;를 수학적으로 표현한 벡터를 더해주는 방식이다.</p>
<p>$$
Z = X + PE
$$</p>
<p>$X$는 원래의 단어 임베딩, $PE$는 위치 정보를 담은 인코딩 벡터이다.</p>
<p>논문에서 제안한 Positional Encoding은 <strong>사인(sin)과 코사인(cos)</strong> 함수를 이용한다.
특정 위치 $pos$와 차원 $i$에 대해 다음과 같이 정의된다.</p>
<p>$$
PE_{(pos, 2i)} = \sin \left( \frac{pos}{10000^{2i/d_{model}}} \right)
$$</p>
<p>$$
PE_{(pos, 2i+1)} = \cos \left( \frac{pos}{10000^{2i/d_{model}}} \right)
$$</p>
<p>여기서</p>
<ul>
<li>$pos$ : 단어의 위치 (0번, 1번, 2번 …)</li>
<li>$i$ : 임베딩 벡터의 차원 인덱스</li>
<li>$d_{model}$ : 임베딩 벡터의 총 차원 수</li>
</ul>
<p>즉, 짝수 차원은 사인 함수, 홀수 차원은 코사인 함수로 값을 넣어준다.
나도 정확한 원리는 모르지만 사인 코사인으로 파형을 생성해서 무한한 길이도 위치를 알아낼 수 있는 형태로 바꿔진다고 한다. <del>잘 모름</del></p>
<hr>
<h3 id="feed-forward-network-ffn">Feed Forward Network (FFN)</h3>
<p>마지막으로 Multi-Head Attention과 Positional Encoding을 거친 후의 출력은 그대로 다음 레이어로 전달되지 않고, 한 번 더 <strong>Feed Forward Network(포지션별 전결합 신경망)</strong>을 거치게 된다. FFN은 모든 위치(pos)에 대해 동일하게 적용되는 두 개의 선형 변환과 비선형 활성화 함수로 구성된다.</p>
<p>수식으로 표현하면 다음과 같다.</p>
<p>$$
FFN(x) = \max(0, xW_1 + b_1)W_2 + b_2
$$</p>
<p>여기서 $W_1, W_2$와 $b_1, b_2$는 학습 가능한 가중치와 편향이다. 중간에 들어가는 $\max(0, \cdot)$는 ReLU 함수로, 비선형성을 부여하여 모델이 더 복잡한 패턴을 학습할 수 있게 한다.</p>
<p>FFN의 특징은 <strong>각 토큰 위치마다 동일한 네트워크가 독립적으로 적용</strong>된다는 것이다. 즉, 입력이 10개의 토큰이든 20개의 토큰이든, 각 토큰 벡터는 똑같은 FFN 구조를 거쳐 변환된다. 이로 인해 모델은 위치에 무관하게 동일한 변환을 수행하면서도, Attention으로 이미 반영된 단어 간의 관계를 기반으로 비선형적인 특징을 학습할 수 있다.</p>
<p>간단히 말해 Attention이 <strong>단어들 사이의 관계</strong> 를 학습하는 단계라면, FFN은 그 관계로부터 <strong>복잡한 패턴을 추출하고 강화하는 단계</strong>라고 볼 수 있다.</p>
<p>이 과정을 거친 출력은 다시 Residual Connection(잔차 연결)과 Layer Normalization을 통해 안정화되고, 다음 Encoder Layer로 전달된다. Encoder는 이런 구조를 여러 층 쌓아 올려 강력한 표현 학습 능력을 얻게 된다.</p>
<h3 id="masking">Masking</h3>
<p>이제 <strong>Masking</strong>에 대해서 알아보겠다. 마스킹은 decoder에만 적용되는 기법이다. Decoder는 해당 입력과 Encoder에서 생성된 관계를 통해 다음 데이터를 예측한다... 라고 위에서 설명했다. 그런데 생각해보자 Encoder는 단순히 모든 입력에 관한 관계를 정의하는 것이라서, Encoder로 생성된 정보들은 전부 Self Attention 을 통해 조금이라도 각자의 정보를 가지고 있을 것이다.</p>
<p>하지만 Decoder는 다르다. Decoder는 <strong>순차적 예측(Autoregressive Generation)</strong> 을 수행해야 한다. 예를 들어, <code>나는 학교에 간다</code>라는 문장을 생성한다고 할 때, 첫 번째 단계에서는 <code>나는</code>만 알고 있어야 하며, 두 번째 단계에서는 <code>나는 학교에</code>까지만 알고 있어야 한다. 만약 Decoder가 앞으로 나올 단어 <code>간다</code>를 미리 참고해버린다면, 모델은 학습 과정에서 미래 정보를 엿보는 <strong>정보 누수(Information Leakage)</strong> 가 발생하게 된다. 이를 해결해 주는 것이 Masking 기법이다.</p>
<p>Masking은 미래 시점의 단어를 가려서 현재 시점 이전의 단어들만 보이도록 하는 역할을 한다. 수학적으로는 Attention의 Softmax 단계에서, 미래 단어 위치에 -∞ 값을 추가하여 확률이 0이 되도록 만든다. 이렇게 하면 Decoder는 항상 앞에서 생성된 단어까지만 참고해서 다음 단어를 예측하게 된다.</p>
<hr>
<h3 id="add--norm">Add &amp; Norm</h3>
<img width="1520" height="2239" alt="Image" src="https://github.com/user-attachments/assets/d643741a-915e-4c5f-9aaf-a6fdeef848e3" />

<p>그림을 다시 한번 봐보자 MultiHead Attention과 Feed Forward를 하고 나서 Add &amp; Norm이라는 과정이 있고, 화살표 하나는 MultiHead Attention과 Feed Forward를 하지 않고 Add &amp; Norm을 향하고 있다. </p>
<p>MultiHead Attention과 Feed Forward를 하고 나면 원래 입력 정보 $X$가 변질 되거나 학습지 되지 않아, 아예 다른 출력이 나올 수도 있다. 또한 각 layer를 지날때마다 데이터 분포 층이 뒤틀릴 수도 있다. 이를 해결 해 주는 것이 Add &amp; Norm이고, 따라서 각 layer를 지나고 나서 적용을 해준다.</p>
<p>즉, <strong>Add &amp; Norm은 &quot;입력 + 출력&quot;을 합쳐서 정규화하는 과정</strong>이다.Add &amp; Norm은 <strong>Residual Connection + Layer Normalization</strong>로 이루어진다. </p>
<p>먼저 Residual Connection은 레이어의 입력 $X$와 해당 레이어의 출력 $F(X)$를 더한다.</p>
<p>$$
Y = X + F(X)
$$</p>
<p>이렇게 더하면, 깊은 네트워크에서도 원래 입력 정보 $X$가 손실되지 않고 그대로 흘러갈 수 있다.</p>
<p>그 다음에 Layer Normalization을 적용한다. 평균과 분산을 구해서 정규화하는 거다.</p>
<p>$$
\text{LayerNorm}(Y) = \frac{Y - \mu}{\sigma} \cdot \gamma + \beta
$$</p>
<p>최종 출력은 이렇게 된다.</p>
<p>$$
\text{Output} = \text{LayerNorm}(X + F(X))
$$</p>
<hr>
<h3 id="backpropagation">BackPropagation</h3>
<p>이제 위에서 구한 값을 통해 역전파를 진행한다. 이러한 역전파는 수천~수억번을 반복하여 모델을 최적화 하게 된다. 앞에서 말한 것처럼 역전파는 다음 글에서 알아보겠다.</p>
<hr>
<h3 id="decoder">Decoder</h3>
<p>위의 그림을 보면 디코더에서는 두 가지 입력이 필요하다.
첫 번째는 <strong>아웃풋 임베딩(Output Embedding)</strong>, 두 번째는 <strong>인코더의 출력값</strong>이다.</p>
<p><strong>아웃풋 임베딩</strong>은 지금까지 생성한 단어들을 임베딩한 것이다. 예를 들어 번역 모델에서 이미 “I go”까지 만들었다면, 이게 디코더 입력으로 들어간다. 디코더는 이걸 바탕으로 다음 단어가 뭔지 예측한다.</p>
<p><strong>인코더 출력값</strong>은 원문 문장의 의미 표현이다. 디코더는 단순히 자기 자신(아웃풋 임베딩)만 보고는 번역을 할 수 없다. 원래 문장 정보도 같이 참고해야 한다. 그래서 중간에 <strong>Cross-Attention</strong> 이라는 중간단계에서 인코더 출력값을 Key, Value로 삼고, 디코더 쪽에서 만든 Query와 결합한다. 이렇게 해야 “출력 문장이 원문을 잘 반영하도록” 만들 수 있다.</p>
<h4 id="디코더만-사용하는-모델들">디코더만 사용하는 모델들</h4>
<p>GPT 같은 모델은 <strong>디코더만 사용하는 구조</strong>다. 인코더-디코더 구조가 아니기 때문에, <strong>아웃풋 임베딩만 가지고 학습한다.</strong> 방식은 다음과 같다. 디코더 모델은 입력 문장을 바로 디코더에 넣는다. 그리고 나서 마스크드 셀프 어텐션(Masked Self-Attention)으로, 앞으로 올 단어는 못 보고 과거 단어만 참고한다. 이렇게 해서 언어 모델링이 가능하다. 즉, 앞 단어가 주어졌을 때 다음 단어를 예측하는 방식이다. 따라서 디코더 생성형 모델이 사용하는 방식이다. 우리가 GPT를 통해 자연어를 입력하고 GPT가 응답을 생성하는 것이 이러한 토크나이저와 마스크드 셀프 어텐션으로 다음 단어의 확률을 통해 만드는 것이다.</p>
<hr>
<h2 id="😁-마무리">😁 마무리</h2>
<p>이번에는 Transformer에 대해서 알아보았는데, 사실 이 글을 5일에 걸쳐서 쓴것 같다. 중간에 틀린 내용도 고치고 논문 내용도 다시 살펴보느라 오래 걸렸는데, 이렇게 한번 정리하니 확실히 이해가 잘되는 것 같다. 다음에는 fine-tuning에 대해서 알아보겠다. 사실 우리가 LLM을 사용하면 이미 Transformer 가 적용된 LLM을 파인튜닝하거나 RAG, 프롬프팅을 적용하여 사용한다. 따라서 파인튜닝이야 말로 LLM을 실전에 사용할 수 있는 핵심적인 기술이다. 다음 글에서 파인튜닝에 대해서 심도있게 다뤄보겠다.</p>
<p><a href="https://www.youtube.com/watch?v=6s69XY025MU">[참고] Attention/Transformer 시각화로 설명, 임커밋 (YouTube)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] Overview]]></title>
            <link>https://velog.io/@choi-hyk/LLM-Overview</link>
            <guid>https://velog.io/@choi-hyk/LLM-Overview</guid>
            <pubDate>Sat, 16 Aug 2025 07:48:18 GMT</pubDate>
            <description><![CDATA[<h1 id="📖-overview">📖 Overview...</h1>
<p>이전 글에 <strong>Prompt Engineering</strong>과 <strong>Chunking</strong>에 대해서 정리를 했었는데, <strong>LLM의 기초</strong>부터 Velog에 정리를 해야 할 필요를 느꼈다. <strong>LLM의 개념</strong>에 대해서는 예전에 책으로 몇 번 보고, 영상이나 강의자료로 가볍게 본 기억이 있는데, 이번에 제대로 기초부터 다시 공부해서 정리해 보려고 한다.</p>
<p>이번 글에서는 간단한 <strong>LLM의 역사</strong>와 <strong>기본적인 원리</strong>를 간단하게 정리해 보고, 다음 글에서 <strong>Transformer</strong>에 대해서 심도 있게 다룰 생각이다.</p>
<hr>
<h2 id="📜-llm-history">📜 LLM History</h2>
<p><strong>LLM(Large Language Model)</strong> 은 처음 들어보면, 엄청나게 복잡한 알고리즘과 원리로 동작하는 것처럼 보인다. 하지만 <strong>기본적인 원리</strong>는 엄청 간단하다고 한다. <strong>뒷말 잇기</strong>를 생각해 보면 이해가 될 텐데, 만약 이러한 문장을 보았다고 하자.</p>
<p><code>나는 늦게 일어나서 학교까지 ~</code></p>
<p>나는 뒤에 <strong><code>뛰어갔다</code></strong> 를 넣으면 자연스러울 것 같다. 아마도 <strong><code>택시를 타고 갔다</code></strong> 도 괜찮을 것 같다. 그럼 <strong>LLM</strong>이 볼 때는 어떻게 생각을 할까? LLM은 수많은 예시를 가지고 있고, 각 예시는 <strong>확률</strong>을 가지고 있다.</p>
<table>
<thead>
<tr>
<th>후보 단어</th>
<th>확률(%)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>뛰어갔다</strong></td>
<td><strong>35%</strong></td>
</tr>
<tr>
<td><strong>택시를</strong></td>
<td><strong>25%</strong></td>
</tr>
<tr>
<td><strong>걸어갔다</strong></td>
<td><strong>15%</strong></td>
</tr>
<tr>
<td><strong>버스를</strong></td>
<td><strong>10%</strong></td>
</tr>
<tr>
<td><strong>지각했다</strong></td>
<td><strong>5%</strong></td>
</tr>
<tr>
<td><strong>기타</strong></td>
<td><strong>10%</strong></td>
</tr>
</tbody></table>
<p>만약 이러한 확률을 가지고 있다고 해보자. <strong>LLM</strong>은 아마도 이러한 확률 테이블에서 가장 적절한 후보 단어를 골라서 문장을 생성해 낼 것이다.</p>
<p>이것이 바로 <strong>기본적인 LLM의 동작</strong>이다. 이러한 원리는 <strong>1950년대</strong>부터 고안이 되었는데, 그 유명한 <strong>튜링 머신 테스트</strong>가 이러한 <strong>자연어 생성 메커니즘</strong>에 부합하는 기계를 찾는 테스트이다.</p>
<p>이후 <strong>1990년대</strong>에는 <strong>통계적 언어 모델</strong>이 등장했다. <strong>N-gram</strong>이라는 모델을 사용해 이전 단어들을 보고 다음 단어의 확률을 계산하는 방식이었다. 하지만 긴 문맥을 처리하지 못하고 데이터가 커질수록 <strong>희소성 문제</strong>가 발생했다.</p>
<p><strong>2010년대 초반</strong>, <strong>RNN(Recurrent Neural Network)</strong> 과 <strong>LSTM(Long Short-Term Memory)</strong> 같은 신경망 모델이 <strong>NLP</strong>에 도입되면서 조금 더 긴 문맥을 다룰 수 있게 되었지만, 여전히 <strong>학습 속도</strong>와 <strong>긴 시퀀스 처리</strong>에서 한계가 있었다.</p>
<blockquote>
<p>참고로 <strong>RNN</strong>은 <strong>순환 신경망</strong> 기술로 연쇄적인 데이터를 처리하기 위해 <strong>이전 상태</strong>를 입력으로 받아서 출력을 만들어 내는 <strong>뉴런 구조</strong>에서 착안한 기술이다.</p>
</blockquote>
<p>결정적인 전환점은 <strong>2017년 Transformer</strong>의 등장이다. <strong>「Attention is All You Need」</strong> 라는 논문에서 제안된 Transformer 구조는 <strong>병렬 처리</strong>가 가능하면서도 <strong>긴 문맥</strong>을 효과적으로 학습할 수 있게 했다. 그 이후 <strong>BERT, GPT, T5</strong> 같은 모델들이 등장하며 <strong>언어 모델의 패러다임</strong>을 완전히 바꾸었다.</p>
<hr>
<h2 id="🤖-bert--gpt--t5">🤖 BERT / GPT / T5</h2>
<p><strong>Transformer</strong>는 입력된 시퀀스의 모든 단어를 <strong>병렬로 처리</strong>할 수 있다. 이러한 병렬 처리를 가능하게 하는 것이 바로 <strong>멀티헤드 어텐션(MultiHead Attention)</strong>이다. <strong>멀티헤드 어텐션</strong>은 단어들의 <strong>관계</strong>를 파악하는 기법이다.</p>
<p><strong>Transformer</strong>는 크게 두 가지의 구조로 구성된다. <strong>인코더(Encoder)</strong>와 <strong>디코더(Decoder)</strong> 두 개의 절차로 구성되는데, 이를 어떻게 사용하느냐에 따라 <strong>BERT, GPT, T5</strong>와 같은 다양한 모델이 탄생했다.</p>
<h3 id="bert-bidirectional-encoder-representations-from-transformers">BERT (Bidirectional Encoder Representations from Transformers)</h3>
<h5 id="bidirectional-→-양방향">Bidirectional → 양방향</h5>
<p><strong>BERT</strong>는 <strong>구글</strong>에서 개발한 모델로, <strong>인코더만</strong>으로 구성되어 있다. <strong>양방향(Bidirectional)</strong> 으로 문맥을 학습하는 것이 특징이다. 예를 들어, <code>나는 늦게 일어나서 학교까지 뛰어갔다</code>라는 문장이 있을 때, <strong>BERT</strong>는 &quot;나는 늦게 일어나서&quot;와 &quot;학교까지&quot;라는 <strong>양쪽의 문맥</strong>을 모두 고려하여 <strong><code>뛰어갔다</code></strong>라는 단어를 이해한다. 이는 문장의 <strong>의미를 이해하고 분류하는 과제(NLU, Natural Language Understanding)</strong> 에 매우 효과적이다.</p>
<h3 id="gpt-generative-pre-trained-transformer">GPT (Generative Pre-trained Transformer)</h3>
<p><strong>GPT</strong>는 <strong>OpenAI</strong>에서 개발한 모델로, <strong>디코더만</strong>으로 구성되어 있다. <strong>GPT</strong>는 <strong>단방향(Unidirectional)</strong> 으로 학습하며, 문맥을 기반으로 <strong>다음 단어를 예측</strong>하는 방식이다. 위 예시에서, GPT는 <code>나는 늦게 일어나서 학교까지</code>라는 문장이 주어졌을 때, <strong>이전 단어들</strong>만을 참고하여 <strong><code>뛰어갔다</code></strong>를 예측한다. 이 구조는 새로운 문장을 <strong>생성하는 과제(NLG, Natural Language Generation)</strong> 에 뛰어나다.</p>
<h3 id="t5-text-to-text-transfer-transformer">T5 (Text-to-Text Transfer Transformer)</h3>
<p><strong>T5</strong>는 <strong>구글</strong>에서 개발한 모델로, <strong>인코더와 디코더</strong>를 모두 사용한다. <strong>T5</strong>의 가장 큰 특징은 모든 자연어 처리 문제를 <strong>&quot;텍스트를 텍스트로 바꾸는(text-to-text)&quot;</strong> 형식으로 통일했다는 점이다. 예를 들어, <strong>문장 분류, 요약, 번역</strong> 등 모든 과제를 <strong>질문과 답변 텍스트 쌍</strong>으로 변환하여 학습한다.</p>
<p>자세한 원리는 다음 글인 <strong><code>Transformer</code></strong>에서 심도 있게 다뤄보도록 하겠다.</p>
<hr>
<h2 id="🌟-emergence">🌟 Emergence</h2>
<p>그런데 우리가 궁금한 것은 이러한 소위 말하는 <strong>LLM 혁명</strong>이 어떻게 왔냐는 것이다. 아마 <strong>LLM</strong>에 관심이 많으면 <strong>창발적 능력</strong>이라는 말을 많이 들어봤을 텐데, 답은 여기에 있다.</p>
<p><strong>창발적 능력</strong>이란 <strong>작은 모델</strong>에서는 전혀 보이지 않던 능력이, <strong>모델의 규모</strong>가 일정 임계치를 넘었을 때 <strong>갑작스럽게 도약</strong>하듯 나타나는 현상을 의미한다.</p>
<img width="1129" height="833" alt="Image" src="https://github.com/user-attachments/assets/0f419745-167a-4a72-ad47-a56d8ed5341b" />  

<p>해당 그래프는 <strong>OpenAI</strong>가 <strong>2017년</strong>에 발표한 논문 <strong>「Learning to Generate Reviews and Discovering Sentiment Neurons」</strong> 에서 나온 결과인데, <strong>LSTM(Long Short-Term Memory)</strong> 기반의 언어 모델이 갑자기 <strong>긍정/부정 감정(sentiment)</strong>을 구분하는 능력을 갖추게 된 것이다. 이것이 초기 <strong>창발적 능력</strong>의 증거라고 보는 견해가 많다.</p>
<p>이러한 <strong>창발적 능력</strong>은 <strong>GPT-2</strong>에서도 관찰되었다는 견해가 있지만, 실제로 놀라운 성능을 보여준 것은 <strong>GPT-3</strong>부터였다. 바로 <strong><code>In Context Learning</code></strong>이라는 <strong>Prompt Engineering</strong>의 핵심이 되는 현상이 일어난 것이다.</p>
<p>이때부터 <strong>Microsoft</strong>가 <strong>OpenAI</strong>에 본격적으로 눈길을 돌렸다. 이미 <strong>2019년</strong>에 <strong>Azure 클라우드</strong>를 통해 일부 협력 관계를 맺고 있었지만, 이러한 <strong>창발적 능력</strong>으로 인한 <strong>LLM의 능력 극대화</strong>는 충격적으로 다가왔을 것이다.</p>
<p><strong>OpenAI</strong>의 CEO인 <strong>샘 알트먼</strong>과 연구진들은 이러한 <strong>창발적 능력</strong>을 <strong>2017년</strong>에 알게 되어 <strong>GPT-1부터 3까지 Zero-shot, Few-shot</strong> 등 여러 가지 현상을 관찰하고 개선하면서 지금에 이르렀다. 참고로 <strong>Zero-shot</strong>과 <strong>Few-shot</strong>은 각각 <strong>GPT-2</strong>와 <strong>GPT-3</strong>에서 처음 체계적으로 입증되었다고 한다.</p>
<hr>
<h2 id="📌-마무리">📌 마무리</h2>
<p>이번 글에서는 <strong>LLM의 역사</strong>와 <strong>기본 원리</strong>, 그리고 <strong>창발적 능력</strong>의 개념까지 정리해 보았다. 다음 글에서는 <strong>LLM의 핵심 구조 Transformer</strong>를 심도 있게 다룰 예정이다. 또한 <strong>LLM이 학습을 하는 방식</strong>도 다룰 생각이다.</p>
<p><a href="https://openai.com/index/unsupervised-sentiment-neuron/">[참고] Unsupervised sentiment neuron</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Design Pattern] Decorator Pattern]]></title>
            <link>https://velog.io/@choi-hyk/Design-Pattern-Decorator-Pattern</link>
            <guid>https://velog.io/@choi-hyk/Design-Pattern-Decorator-Pattern</guid>
            <pubDate>Fri, 15 Aug 2025 10:26:47 GMT</pubDate>
            <description><![CDATA[<h1 id="decorator-pattern-🎨">Decorator Pattern 🎨</h1>
<p>오랜만에 디자인패턴 글을 써보는데, 최근에 IPP로 회사에 가서 이것저것 하고 정신이 없어서 글 쓰는 것을 잊고 있었다.</p>
<p>앞으로는 일주일에 한번은 디자인패턴 글을 쓸 생각이다. 어쨌든 저번 프로토타입 패턴을 마지막으로 <strong>생성패턴은 전부 정리를 완료</strong>했고, 오늘부터는 <strong>장식자 패턴을 시작으로 구조 패턴을 차례대로 정리</strong>해보겠다.</p>
<p><strong>Decorator Pattern</strong>은 이름에서 알 수 있다시피, <strong>어떤 객체를 장식을 하는 패턴</strong>이다. 참고로 장식자 패턴을 포함한 <strong>Structure Patterns</strong>는 <strong>여러 개의 객체로 이루어진 구조를 정의</strong>해주는 패턴이다. 간단하게 <strong>보편적인 설계도를 정의한 것</strong>이라 보면 된다. 따라서 코드를 작성할 때, <strong>클래스나 함수를 정의하고 객체의 생명주기를 관리하는 방법에는 생성패턴</strong>이 사용된다면, <strong>전체적인 구조를 정의하고, 하나의 모듈로 동작하는 기능을 구현할 때는 구조패턴</strong>을 사용할 경우가 생길 것이다.</p>
<p>다시 돌아가서 장식자 패턴은 <strong>객체에 동적으로 새로운 책임(기능)을 추가하는 방식</strong>으로, <strong>상속의 대안</strong>으로 사용된다. 이 패턴은 <strong>기존 객체의 구조를 변경하지 않고 기능을 확장</strong>할 수 있다.</p>
<blockquote>
<p><strong>객체에 동적으로 새로운 책임을 추가할 수 있게 합니다. 기능을 추가하려면, 서브클래스를 생성하는 것보다 융통성 있는 방법을 제공합니다</strong></p>
</blockquote>
<p>GOF 책에서 보면 많은 패턴들이 <strong>서브클래스를 생성하는 것을 대체하고 효율적으로 기능을 추가하기 위해 고안</strong>된 것임을 알 수 있다.
그렇다면 여기서 <strong>동적</strong>은 무엇을 뜻하는 것일까?</p>
<p>여기서 동적은 실제로 컴퓨터공학에서 말하는 <strong><code>Dynamic</code></strong>을 의미한다.
만약 객체를 서브클래스로 기능과 책임을 만들 경우, <strong>컴파일 타임이나 빌드 타임에 &quot;정적&quot;으로 기능과 책임을 담당하는 클래스</strong>를 생성해야 한다. 하지만 장식자 패턴은 <strong>동적으로 실제 런타임 환경에서 이러한 추가 기능 클래스를 추가 가능</strong>하다. 또한 장식자 패턴은 <strong><code>Wrapper</code></strong>라고도 불리는데, <strong>객체를 감싸서 추가적인 기능이나 책임을 부여하는 구조</strong> 때문에 이렇게 불린다.</p>
<p><strong>책임을 부여하는 것</strong>이 장식자 패턴에서 가장 중요한 점인데, <strong>장식받는 객체는 자신의 기능만 신경 쓰면 되고</strong>, 나머지 장식을 하는 객체들의 구현과 기능은 신경 쓸 필요가 없다. 따라서 <strong><code>Decorator</code>가 사용되는 순간 장식받는 객체는 <code>Decorator</code>의 멤버 변수로 들어가서 기능 호출만 받으면 된다.</strong></p>
<hr>
<h2 id="언제-사용하나-📌">언제 사용하나? 📌</h2>
<p>책에서는 장식자 패턴을 <strong><code>TextView</code> 컴포넌트를 감싸서 기능을 <code>BorderDecorator</code>와 <code>ScrollDecorator</code>로 예시</strong>를 들었다.</p>
<img width="524" height="220" alt="Image" src="https://github.com/user-attachments/assets/cd5a7c25-28c9-46ef-84a9-6485f3defdc8" />  

<p>해당 이미지를 보면, 기존의 <strong><code>TextView</code>에 <code>BorderDecorator</code>와 <code>ScrollDecorator</code>로 감싸서 컴포넌트를 이루는 것</strong>을 나타낸다.</p>
<img width="578" height="312" alt="Image" src="https://github.com/user-attachments/assets/d910b2ca-ca2e-4e82-8739-85a9a61d5f73" />  

<p>위 사진을 보면 <strong>동적으로 어떻게 기능을 추가하는지</strong> 이해가 될 것이다. 바로 <strong><code>VisualComponent</code>라는 클래스가 <code>TextView</code>를 서브클래스로 가지고 있는데, 해당 클래스가 <code>Decorator</code>라는 클래스를 서브클래싱</strong>하여 관리를 한다.</p>
<p><strong><code>Decorator</code>는 <code>Draw()</code>로 원하는 컴포넌트를 그리면 된다.</strong> 여기서 중요한 점이 있는데, <strong><code>TextView</code>는 이러한 <code>Decorator</code>들을 알 필요가 없다.</strong> <code>TextView</code>를 정의하고, 만약 테두리를 그리고 싶으면 <strong><code>TextView</code>를 <code>Decorator</code>에 넘겨주고 해당 클래스에서 장식을 해준다.</strong></p>
<p>따라서 <strong><code>TextView</code>는 자신의 기능인 텍스트뷰 그리기만 신경</strong>을 쓰면 된다.</p>
<hr>
<h2 id="구조-🏗️">구조 🏗️</h2>
<img width="645" height="285" alt="Image" src="https://github.com/user-attachments/assets/b0938fb6-00ba-417f-9dfe-d8db2d85ff14" />  

<p><strong><code>ConcreteDecorator</code>들은 <code>Operation()</code>으로 자신만의 기능과 함께 <code>ConcreteComponent</code>의 <code>Operation()</code>을 호출</strong>할 것이다. 이렇게 <strong><code>ConcreteComponent</code>는 그저 자신의 기능만 호출하고 추가 기능 확장에 대해서는 신경 쓸 필요가 없다.</strong></p>
<hr>
<h2 id="구현-💻">구현 💻</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;memory&gt;

class VisualComponent {
public:
    virtual void Draw() = 0;
    virtual ~VisualComponent() {}
};

class TextView : public VisualComponent {
public:
    void Draw() override {
        std::cout &lt;&lt; &quot;기본 텍스트 뷰 그리기&quot; &lt;&lt; std::endl;
    }
};

class Decorator : public VisualComponent {
protected:
    std::unique_ptr&lt;VisualComponent&gt; _component;
public:
    Decorator(std::unique_ptr&lt;VisualComponent&gt; component)
        : _component(std::move(component)) {} 
    void Draw() override {
        if (_component) {
            _component-&gt;Draw();
        }
    }
};

class BorderDecorator : public Decorator {
private:
    int _width;
    void DrawBorder() {
        std::cout &lt;&lt; &quot;테두리 그리기&quot; &lt;&lt; std::endl;
    }
public:
    BorderDecorator(std::unique_ptr&lt;VisualComponent&gt; component, int width)
        : Decorator(std::move(component)), _width(width) {}

    void Draw() override {
        Decorator::Draw();
        DrawBorder();
    }
};

class ScrollDecorator : public Decorator {
private:
    void DrawScroll() {
        std::cout &lt;&lt; &quot;스크롤바 그리기&quot; &lt;&lt; std::endl;
    }
public:
    ScrollDecorator(std::unique_ptr&lt;VisualComponent&gt; component)
        : Decorator(std::move(component)) {}

    void Draw() override {
        Decorator::Draw();
        DrawScroll();
    }
};

int main() {
    auto textView = std::make_unique&lt;TextView&gt;();
    std::cout &lt;&lt; &quot;\n--- 기본 TextView ---&quot; &lt;&lt; std::endl;
    textView-&gt;Draw();

    auto textViewWithBorder = std::make_unique&lt;BorderDecorator&gt;(std::move(textView), 1);
    std::cout &lt;&lt; &quot;\n--- 테두리 추가된 TextView ---&quot; &lt;&lt; std::endl;
    textViewWithBorder-&gt;Draw();

    auto textViewWithBoth = std::make_unique&lt;ScrollDecorator&gt;(
        std::make_unique&lt;BorderDecorator&gt;(
            std::make_unique&lt;TextView&gt;(), 1));
    std::cout &lt;&lt; &quot;\n--- 테두리와 스크롤 모두 추가된 TextView ---&quot; &lt;&lt; std::endl;
    textViewWithBoth-&gt;Draw();

    return 0;
}</code></pre>
<p>위의 코드에서 중요하게 볼 점은, 바로 <strong><code>Decorator</code>들이 <code>VisualComponent* _component;</code>로 장식할 객체인 <code>TextView</code>를 멤버 변수로 받는 것</strong>이다. 이를 통해서 <strong><code>TextView</code>는 만약 <code>Decorator</code>를 사용하고 싶지 않으면, 해당 객체들을 만들 필요가 없다.</strong></p>
<p>다시 상기시키자면, <strong>장식자 패턴에서 가장 중요한 점은 바로 책임 전가</strong>이다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>Decorator 패턴은 기존 객체의 구조를 변경하지 않고, 런타임에 동적으로 새로운 기능과 책임을 부여할 수 있는 디자인 패턴이다. 상속 대신 객체를 감싸는 방식(Wrapper)을 사용하여 기능을 확장하므로, 필요할 때만 선택적으로 기능을 조합할 수 있고 클래스 폭발 문제를 피할 수 있다는 장점이 있다. 하지만 장식이 중첩될수록 구조가 복잡해지고, 디버깅이 어려워질 수 있으며, 너무 많은 데코레이터가 사용되면 유지보수 비용이 증가할 수 있다는 단점이 있다. 결국 이 패턴은 <strong>기능 확장이 빈번하고, 유연한 구조가 필요한 UI 컴포넌트나 모듈성 높은 시스템</strong>에서 특히 효과적으로 사용된다.</p>
<p>다음 글에서는 <strong>구조 패턴</strong> 중 <strong>Adapter Pattern</strong>에 대해 알아볼 예정이다. Adapter 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 연결하는 패턴으로, 기존 코드를 수정하지 않고 새로운 환경에 맞출 수 있다는 장점이 있다.</p>
<p><a href="https://www.cs.unc.edu/~stotts/GOF/hires/pat4dfso.htm">[참고] Decorator Pattern</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] Chunking]]></title>
            <link>https://velog.io/@choi-hyk/LLM-Chunking</link>
            <guid>https://velog.io/@choi-hyk/LLM-Chunking</guid>
            <pubDate>Thu, 14 Aug 2025 03:26:50 GMT</pubDate>
            <description><![CDATA[<h1 id="chunking-🔧">Chunking 🔧</h1>
<p>이번에는 저번 Prompt Engineering에 이어서 Chunking에 대해서 알아보겠다.</p>
<p>청킹은 자연어를 특정 크기로 나누는 것을 의미한다. 각 나누어진 단위를 <strong>청크(chunk)</strong>라고 하며, 이러한 기법을 적용하는 도구를 <strong>텍스트 분할기(Text splitters)</strong>라고 한다. 따라서 텍스트 분할을 어떻게 하느냐에 따라서 청킹이 구성되는 방식이 달라진다. 중요한 점은 이러한 텍스트 분할은 분할을 적용하려는 자연어의 종류에 따라 나뉜다는 것이다.</p>
<h2 id="텍스트-분할기-text-splitters-📑">텍스트 분할기 (Text Splitters) 📑</h2>
<p>텍스트 분할은 <strong>RAG</strong>(Retrieval-Augmented Generation, 검색 증강 생성)에서 사용되는 핵심 기법이다. 긴 문서를 모델이 처리하기 쉬운 작은 단위인 청크로 나누어, 효율적인 검색과 정확한 답변 생성을 가능하게 한다.</p>
<p>문서를 나누는 구체적인 이유는 다음과 같다.</p>
<ul>
<li><strong>다양한 문서 길이 처리:</strong> 실제 문서들은 길이가 제각각이다. 분할을 통해 모든 문서를 일관된 크기로 처리할 수 있다.</li>
<li><strong>모델 한계 극복:</strong> 대부분의 임베딩 모델과 언어 모델은 입력 크기 제한이 있다. 분할을 통해 이 제한을 초과하는 문서를 처리할 수 있다.</li>
<li><strong>표현 품질 향상:</strong> 긴 문서는 임베딩 품질이 저하될 수 있다. 분할을 통해 각 섹션에 더 집중된, 정확한 표현을 생성할 수 있다.</li>
<li><strong>검색 정확도 향상:</strong> 정보 검색 시스템에서 분할은 검색 결과의 세분성을 높여, 질의와 관련된 문서 섹션을 더 정확하게 찾아낼 수 있게 한다.</li>
<li><strong>연산 자원 최적화:</strong> 작은 텍스트 청크로 작업하면 메모리 효율이 높아지고, 처리 작업을 병렬화하기 쉬워진다.</li>
</ul>
<p>텍스트 분할을 구현하는 방법은 여러 가지가 있다.</p>
<h3 id="길이를-기반으로-분할-length-based-📝">길이를 기반으로 분할 (Length-based) 📝</h3>
<p>가장 직관적인 방법으로, 정해진 길이(문자 또는 토큰 수)를 기준으로 문서를 나눈다.</p>
<ul>
<li><strong>특징:</strong> 구현이 간단하고, 청크 크기가 일관적이며, 모델의 요구사항에 맞추기 쉽다.</li>
<li><strong>유형:</strong> <strong>토큰 기반</strong>은 언어 모델에 유용하도록 토큰 수를 기준으로 나누고, <strong>문자 기반</strong>은 텍스트 유형에 관계없이 일관된 문자를 기준으로 나눈다.</li>
</ul>
<h3 id="텍스트-구조를-기반으로-분할-text-structured-based-✨">텍스트 구조를 기반으로 분할 (Text-structured based) ✨</h3>
<p>문단, 문장, 단어 등 텍스트의 계층적 구조를 활용하여 자연스러운 언어 흐름을 유지하며 분할한다.</p>
<ul>
<li><strong>특징:</strong> 문맥적 일관성을 보존하고, 텍스트의 세분화 수준에 맞게 조절한다.</li>
</ul>
<h3 id="문서-구조를-기반으로-분할-document-structured-based-📚">문서 구조를 기반으로 분할 (Document-structured based) 📚</h3>
<p>HTML, Markdown, JSON 등 문서 자체의 내재된 구조를 활용하여 분할한다.</p>
<ul>
<li><strong>특징:</strong> 문서의 논리적 조직을 보존하고, 각 청크 내의 문맥을 유지하며, 검색이나 요약 같은 후속 작업에 더 효과적이다.</li>
<li><strong>유형:</strong> Markdown의 헤더(<code>#</code>, <code>##</code>), HTML의 태그, JSON의 객체나 배열 등을 기준으로 분할한다.</li>
</ul>
<h3 id="의미적-유사도를-기반으로-분할-semantic-meaning-based-🤔">의미적 유사도를 기반으로 분할 (Semantic meaning based) 🤔</h3>
<p>텍스트의 내용적 의미를 직접 분석하여 분할한다.</p>
<ul>
<li><strong>특징:</strong> 의미적 변화가 큰 지점을 찾아 분할한다. 이를 통해 의미적으로 더욱 응집된 청크를 만들고, 후속 작업의 품질을 향상시킨다.</li>
<li><strong>예시:</strong> <strong>슬라이딩 윈도우(Sliding Window)</strong> 방식을 사용하여 문장 그룹의 임베딩을 생성하고, 임베딩 간의 유의미한 차이를 비교하여 분할 지점을 찾는다.</li>
</ul>
<p>참고로, 슬라이딩 윈도우는 특정 데이터를 어떠한 단위(윈도우)로 나누고, 그 데이터 단위들을 순회(슬라이딩)하며 작업을 진행하는 기법이다. 의미적 유사도 기반 분할의 경우, 특정 단위의 문장(윈도우)의 의미를 분석하여 임베딩을 하고, 다음 문장으로 넘어간 뒤(슬라이딩), 다시 해당 문장을 임베딩하여 임베딩된 문장 사이의 차이점을 알아내 분할 지점을 찾는 방식이다.</p>
<hr>
<h2 id="청크-chunk-🧩">청크 (Chunk) 🧩</h2>
<p>텍스트 분할을 진행할 때 고려해야 하는 점은 자연어를 청크로 나누는 단위인 <strong>청크 크기(Chunk Size)</strong>와, 나누어진 청크 사이의 관계인 <strong>청크 중첩(Chunk Overlap)</strong>을 유지하는 것이다. 인접한 청크 사이에 청크 중첩을 적용하여 문맥적 연속성을 유지시켜주는 작업을 진행한다.</p>
<p>청크 크기와 청크 중첩, 이 두 변수를 적용하여 텍스트 분할을 진행하게 되는데, 해당 변수의 크기에 따라 자연어를 분석하는 정도가 달라진다. 중요한 점은, 해당 자연어가 어떠한 종류의 자연어인지가 중요한데, 각 종류에 따라 문장의 복잡도와 구조가 다르기 때문이다.</p>
<p>청크 크기에 따라 좋은 성능을 보이는 문서들을 표로 정리하면 다음과 같다. 이는 문서의 구조와 내용적 특성을 기반으로 한다.</p>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">작은 청크 크기(Small Chunk Size)</th>
<th align="left">큰 청크 크기(Large Chunk Size)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>적합한 문서 종류</strong></td>
<td align="left">질문-답변(FAQ) 문서, 기술 문서의 API 설명, 법률 조항, 단순 사실 목록</td>
<td align="left">학술 논문, 연구 보고서, 소설, 에세이, 복잡한 기술 매뉴얼, 심층 뉴스 기사</td>
</tr>
<tr>
<td align="left"><strong>효율성 근거</strong></td>
<td align="left">정확한 검색, 노이즈 감소, 컨텍스트 창 효율</td>
<td align="left">문맥 보존, 관계 추론 용이, 종합적 답변 가능</td>
</tr>
</tbody></table>
<p>청크 크기를 결정할 때는 문서의 내용이 얼마나 독립적인지와 문맥이 얼마나 중요한지를 기준으로 판단하는 것이 중요하다. 정형화되고 독립적인 내용이 많은 문서는 작은 청크가, 문맥적 흐름과 논리적 관계가 중요한 문서는 큰 청크가 더 효율적이다.</p>
<p>또한, 청크 오버랩은 보통 청크 크기의 <strong>10~20%</strong> 를 사용한다. 문서 구조와 내용에 따라 적절한 청크 크기를 찾아내고 10~20% 비율 안에서 최적의 오버랩 비율을 찾아내는 것이 좋은 텍스트 분할기를 만드는 방법이라 볼 수 있겠다.</p>
<p>다음 시간에는 <code>LangChain</code> 환경에서 간단한 청킹 실습을 진행해 보겠다.</p>
<p><a href="https://python.langchain.com/docs/concepts/text_splitters/">[참고] LangChain Text splitters</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM] Prompt Engineering]]></title>
            <link>https://velog.io/@choi-hyk/LLM-Promp-Engineering</link>
            <guid>https://velog.io/@choi-hyk/LLM-Promp-Engineering</guid>
            <pubDate>Wed, 13 Aug 2025 06:44:57 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-prompt-engineering">💻 Prompt Engineering</h2>
<p>저번에는 LangChain을 통해서 Prompt Engineering을 적용하는 법을 알아보았는데, 사실 Prompt Engineering 최근 LLM을 구성하는데 있어서 매우 중요한 기술이다. 따라서 
<a href="https://www.promptingguide.ai/kr">Prompt Engineering Guide</a>에서 학습 한 내용을 내 나름대로 정리해 보았다.</p>
<h1 id="프롬프팅-엔지니어링이란-🤖">프롬프팅 엔지니어링이란? 🤖</h1>
<p>프롬프팅 엔지니어링은 사람이 모델의 프롬프트를 개발하는 행위를 이야기한다. 따라서 비교적 최근에 활발히 연구되고 있는 분야다. 프롬프팅 엔지니어링은 <strong>LLM</strong>(Large Language Model, 거대 언어 모델)의 역량을 향상시키고, LLM 및 기타 도구와 인터페이스를 형성할 수 있다. </p>
<p>또한 중요한 점은 바로 하드웨어적이 변경 없이 오직 내부의 자연어로 처리된 프롬프팅을 통해 성능을 개선한다는 점이다. 사실 지금은 크게 와 닿지는 않는다...</p>
<hr>
<h2 id="매개변수-parameters-⚙️">매개변수 (Parameters) ⚙️</h2>
<p>매개변수는 프롬프팅 엔지니어링에서 사용하는 용어들과 더불어 성능을 조절할 수 있는 변수들이다. 프레임워크, 도구를 사용해서 프롬프팅을 할 때 이러한 변수들을 사용해서 성능과 출력 결과를 조절하게 된다.</p>
<h3 id="temperature">Temperature</h3>
<p>모델이 생성하는 텍스트의 <strong>무작위성</strong>을 조절하는 매개변수다.</p>
<ul>
<li>낮은 값을 설정하면 확률이 가장 높은 단어를 선택하여 <strong>결정적이고 사실적인</strong> 응답을 생성한다. 이는 질의응답과 같은 작업에 적합하다.</li>
<li>높은 값을 설정하면 다양한 단어의 선택 가중치를 높여 <strong>다양하고 창의적인</strong> 응답을 촉진한다. 이는 시 창작과 같은 작업에 유용하다.</li>
</ul>
<h3 id="top-p">Top-p</h3>
<p>온도와 유사하게 텍스트 생성의 <strong>결정성</strong>을 제어하는 매개변수다.</p>
<ul>
<li>낮은 값을 설정하면 정확하고 사실적인 답변을 생성한다.</li>
<li>높은 값을 설정하면 보다 다양한 응답을 유도한다.</li>
<li>일반적으로 온도(temperature)와 Top-p 중 <strong>하나만</strong> 조정하는 것이 권장된다.</li>
</ul>
<h3 id="최대-길이-max-length">최대 길이 (Max Length)</h3>
<p>모델이 생성할 수 있는 <strong>최대 토큰(단어) 수</strong>를 설정하는 매개변수다. 길고 불필요한 응답을 방지하고 비용을 관리하는 데 도움이 된다.</p>
<h3 id="정지-시퀀스-stop-sequences">정지 시퀀스 (Stop Sequences)</h3>
<p>모델의 텍스트 생성을 <strong>중단시키는 특정 문자열</strong>이다. 응답의 길이와 구조를 제어하는 데 사용된다.</p>
<h3 id="빈도-페널티-frequency-penalty">빈도 페널티 (Frequency Penalty)</h3>
<p>이미 생성된 단어가 다시 등장할 확률을 낮추는 매개변수다. 값이 높을수록 모델의 응답에서 단어의 반복을 방지한다.</p>
<h3 id="존재-페널티-presence-penalty">존재 페널티 (Presence Penalty)</h3>
<p>이미 한 번이라도 등장한 단어에 동일한 페널티를 적용하여 <strong>반복을 방지</strong>하는 매개변수다.</p>
<ul>
<li>값이 높을수록 다양한 텍스트를 생성하는 데 도움이 되고, 낮은 값은 사실 기반의 집중적인 응답에 적합하다.</li>
<li>빈도 페널티와 존재 페널티 중 <strong>하나만</strong> 조정하거나 둘 다 조정하지 않는 것이 일반적인 권장 사항이다.</li>
</ul>
<hr>
<h2 id="프롬프팅-기법-prompting-techniques-✨">프롬프팅 기법 (Prompting Techniques) ✨</h2>
<h3 id="zero-shot">Zero-shot</h3>
<p>Zero-shot은 모델에게 예시를 제공하지 않고, 질문과 지시만으로 답변을 유도하는 기법이다. 모델의 사전 학습된 지식을 활용하여 직접적인 답변을 생성한다.</p>
<pre><code class="language-prompt">텍스트를 중립, 부정 또는 긍정으로 분류합니다.
텍스트: 휴가는 괜찮을 것 같아요.
감정:</code></pre>
<pre><code class="language-output">중립</code></pre>
<p>이 프롬프트는 별도의 배경 정보나 예시 없이 모델에게 바로 질문을 던진다. 예제를 제시하지 않지만 모델의 사전 학습된 지식을 통해서 답변을 하게 된다. LLM은 뛰어난 제로샷 능력을 보여준다고 한다. 하지만 복잡한 작업에서는 개선이 필요하다.</p>
<h3 id="few-shot">Few-shot</h3>
<p>Few-shot은 원하는 답변의 패턴을 모델에게 학습시키기 위해 질문과 함께 몇 가지 예시를 제공하는 기법이다. 이를 통해 모델은 예시의 형식을 모방하여 답변을 생성한다.</p>
<pre><code class="language-prompt">정말 멋지네요! // 긍정
이건 나쁘다! // 부정
와우 그 영화 정말 멋졌어요! // 긍정
정말 끔찍한 쇼였어! // 부정</code></pre>
<p>이 프롬프트는 답변을 유도해서 정확한 출력을 하게 한다.</p>
<pre><code class="language-prompt">정말 멋지네요! // 부정
이건 나쁘다! // 긍정
와우 그 영화 정말 멋졌어요! // 긍정
정말 끔찍한 쇼였어! //</code></pre>
<p>현재 대규모 모델들은 이러한 Zero-shot, Few-shot 전부 가능하며, 위와 같이 무작위로 입력된 프롬프트에 대해서도 정확한 답변을 가져올 수 있다고 한다. 하지만 Few-shot 프롬프트도 복잡한 추론 작업에서는 완벽한 기술은 아니다.</p>
<pre><code class="language-prompt">이 집합 {15, 32, 5, 13, 82, 7, 1}에서 홀수를 모두 더하면 짝수이다.
A:</code></pre>
<pre><code class="language-output">네, 이 집합의 홀수를 모두 더하면 짝수인 107입니다.</code></pre>
<p>만약 질의 결과가 이렇게 나왔을 때는 다른 방식으로 프롬프팅을 진행해야 한다.</p>
<pre><code class="language-prompt">이 집합 {4, 8, 9, 15, 12, 2, 1}에서 홀수의 합은 짝수입니다.
A: 답은 거짓입니다.
이 집합 {17, 10, 19, 4, 8, 12, 24}에서 홀수의 합은 짝수입니다.
A: 정답은 참입니다.
이 집합 {16, 11, 14, 4, 8, 13, 24}에서 홀수의 합은 짝수입니다.
A: 답은 참입니다.
이 집합 {17, 9, 10, 12, 13, 4, 2}에서 홀수의 합은 짝수입니다.
A: 답은 거짓입니다.
이 집합 {15, 32, 5, 13, 82, 7, 1}에서 홀수의 합은 짝수입니다.
A:</code></pre>
<pre><code class="language-output">답은 참입니다.</code></pre>
<p>위의 결과로 프롬프팅을 해도 안 되는 경우가 존재할 것이다. 이럴 때 필요한 것이 <strong>Chain-of-Thought</strong>다.</p>
<h3 id="chain-of-thought-cot-🧠">Chain-of-Thought (CoT) 🧠</h3>
<p>CoT는 모델에게 &quot;단계별로 생각하라&quot;는 지시를 내려 복잡한 문제를 논리적인 사고 과정을 통해 해결하도록 유도하는 기법이다. 이를 통해 모델의 추론 오류를 줄이고 정확도를 높인다.</p>
<p>이 기법이 프롬프트 엔지니어링의 핵심이되는 추론 과정을 탑재한 기법이다.</p>
<p>앞으로 나오는 기법들은 해당 CoT를 활용해서 추론 과정을 바꾸고 여러가지 도구를 중간에 넣어서 성능을 향상시킨 것이다.</p>
<img width="940" height="473" alt="Image" src="https://github.com/user-attachments/assets/8dd72947-614d-42be-85ed-e93bb831263e" />

<pre><code class="language-prompt">집합 {4, 8, 9, 15, 12, 2, 1}에서 홀수를 모두 더하면 짝수야.
답변: 홀수(9, 15, 1)를 모두 더하면 25가 돼. 위의 명제는 거짓이야.
집합 {17, 10, 19, 4, 8, 12, 24}에서 홀수를 모두 더하면 짝수야.
답변: 홀수(17, 19)를 모두 더하면 36이 돼. 위의 명제는 참이야.
집합 {16, 11, 14, 4, 8, 13, 24}에서 홀수를 모두 더하면 짝수야.
답변: 홀수(11, 13)를 모두 더하면 24가 돼. 위의 명제는 참이야.
집합 {17, 9, 10, 12, 13, 4, 2}에서 홀수를 모두 더하면 짝수야.
답변: 홀수(17, 9, 13)를 모두 더하면 39가 돼. 위의 명제는 거짓이야.
집합 {15, 32, 5, 13, 82, 7, 1}에서 홀수를 모두 더하면 짝수야.
답변:</code></pre>
<pre><code class="language-output">홀수(15, 5, 13, 7, 1)를 모두 더하면 41이 돼. 위의 명제는 거짓이야.</code></pre>
<p>위의 프롬프팅과 같이 추론 과정을 입력하면 모델이 훨씬 정확한 답변을 하게 된다.</p>
<h3 id="zero-shot-cot-prompting-🚀">Zero-shot CoT Prompting 🚀</h3>
<p><img src="https://velog.velcdn.com/images/choi-hyk/post/ca29a440-a5fa-4486-ab82-c3ba713b3934/image.png" alt=""></p>
<p>모델의 성능이 향상되면서, <code>Let&#39;s think step by step</code>과 같은 질의 예시가 없는 Zero-shot이어도 모델이 추론 도출을 진행한다는 것이다.</p>
<p>현재의 LLM들은 이러한 제로샷 능력을 가지고 있다고 보면 된다.</p>
<pre><code class="language-prompt">나는 시장에 가서 사과 10개를 샀어. 사과 2개를 이웃에게 주고, 2개를 수리공에게 주었어. 그리고 사과 5개를 더 사서 1개는 내가 먹었어. 나는 몇 개의 사과를 가지고 있었니?</code></pre>
<pre><code class="language-output">사과 11개</code></pre>
<p>여기서 <strong>Zero-shot CoT Prompting</strong>을 적용하면 이렇게 구현할 수 있다.</p>
<pre><code class="language-prompt">나는 시장에 가서 사과 10개를 샀어. 나는 사과 2개를 이웃에게 주고, 2개를 수리공에게 주었어.
그리고 사과 5개를 더 사서 1개는 내가 먹었어.
사과가 몇 개나 남았니?
단계별로 생각해 보자.</code></pre>
<pre><code class="language-output">먼저 사과 10개로 시작했어.
이웃에게 2개, 수리공에게 2개를 나누어 주었으므로 사과가 6개가 남았어.
그런 다음 사과 5개를 더 사서 이제 사과가 11개가 되었어.
마지막으로 사과 1개를 먹었으므로 사과 10개가 남게 된다.</code></pre>
<h3 id="self-consistency-🔄">Self-Consistency 🔄</h3>
<p>Self-Consistency는 CoT를 확장한 기법으로, 같은 질문에 대해 여러 가지 다른 사고 경로를 탐색하여 다양한 답변을 생성한 뒤, 가장 일관된 답변을 최종 결과로 선택하는 방식이다.</p>
<p>참고로 디코딩은 LLM의 사고를 바탕으로 실제 텍스트를 선택해서 결과를 내놓는 과정이다. LLM에서 많이 쓰이는 디코딩 방식은 크게 세 가지로 요약할 수 있다. 이 방식들은 각각 속도, 품질, 다양성 측면에서 서로 다른 장단점을 가지고 있으며, 해결하고자 하는 문제의 성격에 따라 적절한 방식을 선택하여 사용한다.</p>
<p><strong>탐욕 알고리즘 (Greedy Decoding)</strong></p>
<p>가장 단순하고 빠른 방식으로, 매 단계에서 확률이 <strong>가장 높은 단어 하나만을</strong> 선택한다.</p>
<ul>
<li><strong>속도:</strong> 매우 빠르다.</li>
<li><strong>결과:</strong> 매번 동일한 결과를 생성하며, 다양성이 전혀 없다.</li>
<li><strong>장점:</strong> 계산 비용이 매우 낮아 리소스 소모가 적다.</li>
<li><strong>단점:</strong> 지역 최적해(local optimum)에 빠져 최적의 문맥을 놓칠 수 있다.</li>
</ul>
<p><strong>빔 서치 (Beam Search)</strong></p>
<p>매 단계에서 가장 확률이 높은 **K개의 단어(빔)**를 동시에 추적하며, 여러 가능한 경로를 탐색한다. K개의 경로 중 최종적으로 가장 높은 확률의 문장을 선택한다.</p>
<ul>
<li><strong>속도:</strong> 탐욕 알고리즘보다 느리다.</li>
<li><strong>결과:</strong> 탐욕 알고리즘보다 더 나은 품질의 결과를 생성할 가능성이 높다.</li>
<li><strong>장점:</strong> 탐욕 알고리즘의 한계를 보완하며, 전체적인 문맥을 고려한다.</li>
<li><strong>단점:</strong> 계산 비용이 높고, 생성된 문장이 다소 반복적이거나 일관성이 떨어질 수 있다.</li>
</ul>
<p><strong>샘플링 (Sampling)</strong></p>
<p>확률이 가장 높은 단어 하나만 선택하는 것이 아니라, 예측된 확률 분포에 기반하여 <strong>무작위로 단어를 선택하는 방식</strong>이다.</p>
<ul>
<li><strong>속도:</strong> 탐욕 알고리즘과 비슷하거나 조금 더 느리다.</li>
<li><strong>결과:</strong> 매번 다른 결과물을 생성하며, 다양성과 창의성이 높다.</li>
<li><strong>장점:</strong> 창의적인 글쓰기나 Self-Consistency와 같이 여러 다른 추론 경로를 생성하는 데 필수적이다.</li>
<li><strong>단점:</strong> 때때로 의미 없는 문장이나 논리적이지 않은 결과물을 생성할 수 있다.</li>
</ul>
<p>만약 탐욕 알고리즘 디코딩으로 자연어를 생성한다면, 확률이 높은 결과로 나아가는 특징이 있지만, 만약 지역 최적해에 빠지게 되면 최악의 결과를 내놓게 된다. 따라서 이를 보완한 것이 샘플링을 사용하는 Self-Consistency이다.</p>
<pre><code class="language-prompt">내가 6살이었을 때 내 여동생은 내 나이의 절반인 나이였어.
지금 나는 70살이라면 여동생은 몇 살일까?</code></pre>
<pre><code class="language-output">35</code></pre>
<p>위의 프롬프팅을 CoT를 적용할 수 있다.</p>
<pre><code class="language-prompt">Q: 과수원에는 15그루의 나무가 있어. 과수원의 일꾼들이 오늘 과수원에 나무를 심을 거야. 나무 심기가 끝나면 21그루의 나무가 있을 거야. 오늘 과수원 일꾼들은 몇 그루의 나무를 심었을까?
A: 15그루로 시작합니다. 나중에 나무가 21그루가 됩니다. 그 차이가 그들이 심은 나무의 수일 것입니다. 따라서 그들은 21 - 15 = 6그루의 나무를 심었어야 합니다. 정답은 6입니다.
Q: 주차장에 3대의 차량이 있고, 2대의 차량이 더 도착하면 주차장에 몇 대의 차량이 있을까?
A: 주차장에 이미 3대의 차량이 있습니다. 2대가 더 도착합니다. 이제 3 + 2 = 5대의 차량이 있습니다. 정답은 5입니다.
Q: 지호는 초콜릿을 32개, 여동생은 42개를 가지고 있었어. 둘이 35개를 먹었다면 총 몇 개가 남았을까?
A: 레아는 초콜릿 32개, 레아의 여동생은 42개를 가지고 있었습니다. 즉, 원래 32개 + 42개 = 74개의 초콜릿이 있었습니다. 35개를 먹었습니다. 따라서 총 74 - 35 = 39개의 초콜릿이 남아 있습니다. 정답은 39입니다.
Q: 선우는 막대 사탕을 20개 가지고 있었어. 그는 두리에게 막대 사탕을 몇 개 주었어. 이제 선우는 막대 사탕 12개를 가지고 있어. 선우는 두리에게 몇 개의 막대 사탕을 줬을까?
A: 선우는 막대 사탕 20개를 가지고 있었습니다. 이제 선우는 12개만 가지고 있으므로, 나머지는 두리에게 주었을 것입니다. 선우가 두리에게 준 막대 사탕의 수는 20 - 12 = 8개다. 정답은 8개다.
Q: 내가 6살이었을 때 내 여동생은 내 나이의 절반인 나이였어. 지금 나는 70살이고 내 여동생은 몇 살일까?
A:</code></pre>
<p>이렇게 CoT를 진행하면 샘플링으로 여러 개의 답이 나올 것이다.</p>
<pre><code class="language-output">내가 6살이었을 때 여동생은 내 나이의 절반이었으므로 3살이었다. 이제 나는 70살이므로 여동생은 70 - 3 = 67살이다. 정답은 67이다.</code></pre>
<pre><code class="language-output">화자가 6살이었을 때 여동생은 화자의 절반 나이인 3살이었다. 이제 화자가 70살이 되었으므로 여동생은 70 - 3 = 67살이 된다. 정답은 67이다.</code></pre>
<pre><code class="language-output">내가 6살이었을 때 여동생은 내 나이의 절반이었으므로 3살이고, 이제 나는 70살이므로 여동생은 70/2 = 35살이다. 정답은 35이다.</code></pre>
<p>디코딩으로 3개의 출력이 나왔고 이제 <strong>Self-Consistency</strong>는 다수결 합의를 통해 67과 35 중 더 많은 67을 선택하게 된다.</p>
<hr>
<h3 id="generative-knowledge-prompting-💡">Generative knowledge prompting 💡</h3>
<p>Generative knowledge prompting은 말 그대로 프롬프팅에 사용할 지식을 미리 생성하는 기법이다.</p>
<pre><code class="language-prompt">골프의 목적 중 하나는 다른 사람보다 더 높은 점수를 얻기 위해 노력하는 것이다.
예, 아니오?</code></pre>
<pre><code class="language-output">예.</code></pre>
<p>골프는 홀에 공을 넣어서, 타수가 최저가 되게 해야 한다. 이때 타수는 점수로 계산되므로, 점수가 낮도록 노력해야 된다. 따라서 해당 출력은 오답이다. 이를 개선하려면 위와 같은 형태의 질문이 들어왔을 때 해당 주제의 전반적인 지식을 지시하여 지식 수준을 높이는 프롬프팅이 가능하다.</p>
<p>만약 LLM에게 해당 질문을 하기 전에, <code>골프에 대한 지식을 알려줘</code>라고 입력하면 골프에 대한 지식을 출력할 것이다.</p>
<pre><code class="language-prompt">Input: 그리스는 멕시코보다 크다.
Knowledge: 그리스는 약 131,957 제곱 킬로미터이고, 멕시코는 약 1,964,375 제곱 킬로미터로 멕시코가 그리스보다 1389% 더 크다.
Input: 안경은 항상 김이 서린다.
Knowledge: 안경 렌즈에는 땀, 호흡 및 주변 습도에서 나오는 수증기가 차가운 표면에 닿아 식은 다음 작은 액체 방울로 변하여 안개처럼 보이는 막을 형성할 때 응결이 발생한다. 특히 외부 공기가 차가울 때는 호흡에 비해 렌즈가 상대적으로 차가워진다.
Input: 물고기는 생각할 수 있다.
Knowledge: 물고기는 보기보다 훨씬 더 똑똑하다. 기억력과 같은 많은 영역에서 물고기의 인지 능력은 인간이 아닌 영장류를 포함한 &#39;고등&#39; 척추동물과 비슷하거나 그 이상이다. 물고기의 장기 기억력은 복잡한 사회적 관계를 추적하는 데 도움이 된다.
Input: 평생 담배를 피우는 것의 일반적인 결과는 폐암에 걸릴 확률이 정상보다 높다는 것입니다.
Knowledge: 평생 동안 하루 평균 담배를 한 개비 미만으로 꾸준히 피운 사람은 비흡연자보다 폐암으로 사망할 위험이 9배 높았다. 하루에 한 개비에서 열 개비 사이의 담배를 피운 사람들은 폐암으로 사망할 위험이 비흡연자보다 거의 12배 높았다.
Input: 돌은 조약돌과 같은 크기다.
Knowledge: 조약돌은 퇴적학의 우든-웬트워스 척도에 따라 입자 크기가 4~64밀리미터인 암석 덩어리다. 조약돌은 일반적으로 과립(직경 2~4밀리미터)보다는 크고 자갈(직경 64~256밀리미터)보다는 작은 것으로 간주된다.
Input: 골프의 목적 중 하나는 다른 사람보다 더 높은 점수를 얻기 위해 노력하는 것이다.
Knowledge:</code></pre>
<p>위의 출력 마지막 질문에 답을 하기 전에 LLM은 골프에 대한 지식을 생성할 것이다.</p>
<pre><code class="language-knowledge"># Knowledge 1
골프의 목적은 최소의 스트로크로 한 세트의 홀을 플레이하는 것이다. 골프 라운드는 일반적으로 18홀로 구성된다.
각 홀은 표준 골프 코스에서 라운드 중 한 번씩 플레이된다. 각 스트로크는 1점으로 계산되며 총 스트로크 수를 사용하여 게임의 승자를 결정한다.

# Knowledge 2
골프는 경쟁하는 선수(또는 골퍼)가 여러 종류의 클럽을 사용하여 가장 적은 수의 스트로크로 코스에 있는 한 세트의 홀에 공을 치는 정밀한 클럽 앤 볼 스포츠다.
각 홀에서 기록한 총 타수를 합산하여 계산하는 점수가 최저가 되도록 코스를 완주하는 것이 목표다.
가장 낮은 점수를 기록한 플레이어가 게임에서 승리한다.</code></pre>
<p>이렇게 지식이 생성이 되면, LLM은 훨씬 쉽게 추론이 가능할 것이다.</p>
<hr>
<h3 id="prompt-chaining-⛓️">Prompt Chaining ⛓️</h3>
<p>프롬프트 체이닝 기법은 LLM의 작업을 하위 작업으로 나누는 것이다. 만약 하나의 작업에 대해 여러 개의 하위 작업으로 나누어지게 된다면, 각 응답을 서로 활용해서 보완이 가능하다. 이러한 연쇄적인 작용을 <strong>프롬프트 체이닝</strong>이라고 한다.</p>
<p>프롬프트 체이닝은 이러한 성능 개선뿐만 아니라 안전성과 작업의 직관성, 투명성을 높여줘서 사람 기준에서 디버깅과 분석을 하기에 용이하게 한다. 특히 문서 기반 자료를 분석할 때 유용한데, 아래 예시가 있다.</p>
<pre><code class="language-prompt"># Prompt 1
You are a helpful assistant. Your task is to help answer a question given a document. The first step is to extract quotes relevant to the question from the document, delimited by ####. Please output the list of quotes using &lt;quotes&gt;&lt;/quotes&gt;. Respond with &quot;No relevant quotes found!&quot; if no relevant quotes were found.
####
{{document}}
####</code></pre>
<p>이렇게 인용문을 <code>####</code> 기준으로 나누어서 생성해 달라고 지시할 수 있다.</p>
<pre><code class="language-prompt">Given a set of relevant quotes (delimited by &lt;quotes&gt;&lt;/quotes&gt;) extracted from a document and the original document (delimited by ####), please compose an answer to the question. Ensure that the answer is accurate, has a friendly tone, and sounds helpful.
####
{{document}}
####
&lt;quotes&gt;
- Chain-of-thought (CoT) prompting[27]
- Generated knowledge prompting[37]
- Least-to-most prompting[38]
- Self-consistency decoding[39]
- Complexity-based prompting[41]
- Self-refine[42]
- Tree-of-thought prompting[43]
- Maieutic prompting[45]
- Directional-stimulus prompting[46]
- Textual inversion and embeddings[59]
- Using gradient descent to search for prompts[61][62][63][64]
- Prompt injection[65][66][67]
&lt;/quotes&gt;</code></pre>
<p>그러면 이렇게 출력이 나오게 되면 다시 해당 인용문을 사용해서 프롬프팅할 수 있다.</p>
<pre><code class="language-output">The prompting techniques mentioned in the document include:
1. Chain-of-thought (CoT) prompting[27]
2. Generated knowledge prompting[37]
3. Least-to-most prompting[38]
4. Self-consistency decoding[39]
5. Complexity-based prompting[41]
6. Self-refine[42]
7. Tree-of-thought prompting[43]
8. Maieutic prompting[45]
9. Directional-stimulus prompting[46]
10. Textual inversion and embeddings[59]
11. Using gradient descent to search for prompts[61][62][63][64]
12. Prompt injection[65][66][67]
Each of these techniques employs unique strategies to enhance or specify the interactions with large language models to produce the desired outcomes.</code></pre>
<p>이제 모델은 인용문과 문서를 기반으로 응답하여 훨씬 정확한 답변을 할 것이다.</p>
<hr>
<h3 id="tree-of-thoughts-tot-🌳">Tree of Thoughts (ToT) 🌳</h3>
<p>해당 기법은 기존의 CoT에서 여러 가지의 의견을 가진 추론 과정을 생성하는 방법을 사용한다. 입력이 들어오면 위의 그림과 같이 여러 개의 추론 과정을 생성해낸다. 이때, 추론 과정에서 생성된 생각들을 평가하여 가장 높은 확률의 생각을 판단한다.</p>
<img width="1083" height="550" alt="Image" src="https://github.com/user-attachments/assets/20a63b8c-6f5b-4989-9412-615566e6e326" />

<img width="845" height="244" alt="Image" src="https://github.com/user-attachments/assets/76bfbc54-0121-40be-8997-174af2905d39" />

<p>위의 그림에서 <strong>Propose Prompt</strong>는 프롬프팅이 추론하여 생각을 생성하는 과정이다. <strong>Value prompt</strong>는 값을 평가하여 각 생각들의 정답 도달 확률을 구하게 된다.
<img width="803" height="307" alt="Image" src="https://github.com/user-attachments/assets/92b1c71e-8ca3-4919-9e02-5b817eb9b095" /></p>
<p>연구 결과를 보면 ToT가 다른 기법들보다 월등히 뛰어나다고 한다. 그러나 연산이 매우 오래 걸려서 고도화된 작업이나 퍼즐 문제 등 복잡한 연산 문제를 처리하는 데 적절하다.</p>
<p><a href="https://www.promptingguide.ai/kr">[참고] Prompt Engineering Guide</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangChain] Managing Conversation History]]></title>
            <link>https://velog.io/@choi-hyk/LangChain-Managing-Conversation-History</link>
            <guid>https://velog.io/@choi-hyk/LangChain-Managing-Conversation-History</guid>
            <pubDate>Tue, 12 Aug 2025 08:00:59 GMT</pubDate>
            <description><![CDATA[<h2 id="🗂️-managing-conversation-history">🗂️ Managing Conversation History</h2>
<p>이번에는 저번에 이어서 대화 맥락을 유지시켜 주는 trimmer 기능에 대해서 알아보겠다. Chatbot은 지금 하는 대화와 이전에 나눈 대화도 기억해서 사용자와 대화해야 한다. 이것을 가능하게 해주는 것이 trimmer 함수이다.</p>
<hr>
<h3 id="✂️-trim_messages">✂️ trim_messages</h3>
<pre><code class="language-python">trimmer = trim_messages(
    max_tokens=512,
    strategy=&quot;last&quot;,
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on=&quot;human&quot;,
)

messages = [
    SystemMessage(content=&quot;you&#39;re a good assistant&quot;),
    HumanMessage(content=&quot;hi! my name is HYK&quot;),
    AIMessage(content=&quot;hi! HYK!&quot;),
    HumanMessage(content=&quot;My favorite color is blue.&quot;),
    AIMessage(content=&quot;nice color!&quot;),
    HumanMessage(content=&quot;My favorite movie is DarkKnight.&quot;),
    AIMessage(content=&quot;nice movie!&quot;),
    HumanMessage(content=&quot;whats 2 + 2&quot;),
    AIMessage(content=&quot;4&quot;),
    HumanMessage(content=&quot;thanks&quot;),
    AIMessage(content=&quot;no problem!&quot;),
    HumanMessage(content=&quot;having fun?&quot;),
    AIMessage(content=&quot;yes!&quot;),
]

trimmer.invoke(messages)</code></pre>
<p>위의 코드는 메시지 트리머를 정의한 코드이다. 토큰을 충분히 크게 주어서 이전 대화를 최대한 많이 기억할 수 있도록 하였다. 만약 토큰을 적게 할당하면 이전에 나눈 많은 양의 대화를 잊을 것이다. <code>messages</code> 변수는 Chatbot에게 메시지를 주입하기 위해 설정한 배열이다.</p>
<p>이를 통해 Chatbot은 해당 대화 내용을 기억하고 있게 된다.</p>
<pre><code class="language-python">def call_model(state: State):
    trimmed_messages = trimmer.invoke(state[&quot;messages&quot;])
    prompt = prompt_template.invoke(
        {&quot;messages&quot;: trimmed_messages, &quot;language&quot;: state[&quot;language&quot;]}
    )
    response = model.invoke(prompt)
    return {&quot;messages&quot;: response}</code></pre>
<p>이렇게 모델을 정의할 때, <code>trimmed_messages = trimmer.invoke(state[&quot;messages&quot;])</code>를 삽입해 준다.</p>
<pre><code class="language-python">query = &quot;What&#39;s my name?.&quot;
language = &quot;Korean&quot;
input_messages = messages + [HumanMessage(query)]
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;What&#39;s my favorite color?&quot;
language = &quot;English&quot;
input_messages = messages + [HumanMessage(query)]
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;내가 가장 좋아하는 영화는?&quot;
input_messages = messages + [HumanMessage(query)]
language = &quot;Korean&quot;
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)</code></pre>
<p>마지막으로 <code>input_messages = messages + [HumanMessage(query)]</code>를 통해 정의한 메시지를 주입해 주면 된다.</p>
<h4 id="💬-answer">💬 answer</h4>
<pre><code>================================== Ai Message ==================================

당신의 이름은 HYK입니다.
================================== Ai Message ==================================

Your favorite color is blue.
================================== Ai Message ==================================

당신이 가장 좋아하는 영화는 다크 나이트입니다.</code></pre><p>이렇게 trimmer까지 구현을 완료했고, 다음 시간에는 RAG(Retrieval Augmented Generation)의 개념에 대해서 알아보겠다.</p>
<p><a href="https://python.langchain.com/docs/tutorials/chatbot/">[참고] LangChain Build a Chatbot</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangChain] Prompt Engineering]]></title>
            <link>https://velog.io/@choi-hyk/LangChain-Prompting-Engineering</link>
            <guid>https://velog.io/@choi-hyk/LangChain-Prompting-Engineering</guid>
            <pubDate>Tue, 12 Aug 2025 07:40:39 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-prompt-engineering">🎯 Prompt Engineering</h2>
<p>저번 시간에 이어서 이번에는 Chatbot을 프롬프팅해서 지시를 내리는 작업을 하겠다. 프롬프팅은 간단하다. 프롬프팅의 기능으로는 자신이 원하는 스타일의 모델을 생성 가능하도록 자연어 지시를 내리는 것이다. 또한 언어를 설정하는 기능도 존재한다.</p>
<hr>
<h3 id="📝-prompting-template">📝 Prompting Template</h3>
<pre><code class="language-python">import os
from typing import Sequence
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import Annotated, TypedDict
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

load_dotenv()
key = os.getenv(&quot;GOOGLE_API_KEY&quot;)
if not key:
    raise EnvironmentError(&quot;GOOGLE_API_KEY not found in .env&quot;)

model = init_chat_model(&quot;gemini-2.5-flash&quot;, model_provider=&quot;google_genai&quot;)

prompt_template = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;You are a happy assistant. Answer all questions with a smile.&quot;,
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
    ]
)


class State(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]


workflow = StateGraph(state_schema=State)


def call_model(state: State):
    prompt = prompt_template.invoke(state)
    response = model.invoke(prompt)
    return {&quot;messages&quot;: response}


workflow.add_edge(START, &quot;model&quot;)
workflow.add_node(&quot;model&quot;, call_model)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;abc123&quot;}}

query = &quot;Hi! I&#39;m HYK.&quot;
input_messages = [HumanMessage(query)]
output = app.invoke(
    {
        &quot;messages&quot;: input_messages,
    },
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;What&#39;s my name?&quot;
input_messages = [HumanMessage(query)]
output = app.invoke(
    {
        &quot;messages&quot;: input_messages,
    },
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;How are you today?&quot;
input_messages = [HumanMessage(query)]
output = app.invoke(
    {
        &quot;messages&quot;: input_messages,
    },
    config,
)
output[&quot;messages&quot;][-1].pretty_print()</code></pre>
<p>위의 코드를 살펴보자.</p>
<pre><code class="language-python">prompt_template = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;You are a happy assistant. Answer all questions with a smile.&quot;,
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
    ]
)</code></pre>
<p>일단 나는 행복한 느낌의 답변을 생성하는 프롬프팅을 하였다.</p>
<pre><code class="language-python">def call_model(state: State):
    prompt = prompt_template.invoke(state)
    response = model.invoke(prompt)
    return {&quot;messages&quot;: response}</code></pre>
<p><code>call_model()</code>에 prompt를 넣어서 구동을 시켜보자.</p>
<h4 id="💡-answer">💡 Answer</h4>
<pre><code>================================== Ai Message ==================================

Hello HYK! It&#39;s so lovely to meet you! 😊 I&#39;m thrilled to be your happy assistant today! How can I help you?
================================== Ai Message ==================================

Why, your name is HYK! 😄 It&#39;s a pleasure to remember! Is there anything else I can help you with, HYK?
================================== Ai Message ==================================

Oh, I&#39;m absolutely wonderful today, thank you for asking! 😊 I&#39;m bubbling with positive energy and ready to assist you with a big smile! How about you, HYK? I hope you&#39;re having a fantastic day too!
</code></pre><p>이렇게 억지로 이모티콘을 쓰면서 행복해하는 모델을 볼 수 있다.</p>
<hr>
<h3 id="🌐-prompting-language">🌐 Prompting Language</h3>
<p>이제 원하는 언어를 지시해 보겠다.</p>
<pre><code class="language-python">prompt_template = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;You are a happy assistant. Answer all questions with a smile and in {language}.&quot;,
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
    ]
)</code></pre>
<p>이렇게 마지막에 <code>{language}</code>로 말해 달라고 하면 모델을 정의할 때 들어간 언어 변수로 답변을 해주게 된다.</p>
<pre><code class="language-python">def call_model(state: State):
    prompt = prompt_template.invoke(state)
    response = model.invoke(prompt)
    return {&quot;messages&quot;: response}</code></pre>
<p>이제 언어를 각 메시지마다 설정해 보자.</p>
<pre><code class="language-python">query = &quot;Hi! I&#39;m HYK.&quot;
language = &quot;Korean&quot;
input_messages = [HumanMessage(query)]
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;What&#39;s my name?&quot;
language = &quot;Spanish&quot;
input_messages = [HumanMessage(query)]
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;How are you today?&quot;
input_messages = [HumanMessage(query)]
language = &quot;Japanese&quot;
output = app.invoke(
    {&quot;messages&quot;: input_messages, &quot;language&quot;: language},
    config,
)</code></pre>
<h4 id="💡-answer-1">💡 Answer</h4>
<pre><code>================================== Ai Message ==================================

안녕하세요, HYK님! 만나 뵙게 되어 정말 반갑습니다! 😊
================================== Ai Message ==================================

¡Claro que sí, HYK! ¡Tu nombre es HYK! 😊 ¡Es un placer conocerte!
================================== Ai Message ==================================

こんにちは！私はとても元気です、ありがとうございます！😊 HYKさんもお元気ですか？</code></pre><p>이렇게 여러 가지 언어로 답변을 해주는 것을 볼 수 있다. 다음 시간에는 trimming을 통해 성능을 향상시키는 방법을 알아보겠다.</p>
<p><a href="https://python.langchain.com/docs/tutorials/chatbot/">실습 출처: Build a Chatbot</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LangChain] Chatbot으로 LangChain 시작하기]]></title>
            <link>https://velog.io/@choi-hyk/LangChain-Chatbot%EC%9C%BC%EB%A1%9C-LangChain-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choi-hyk/LangChain-Chatbot%EC%9C%BC%EB%A1%9C-LangChain-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Aug 2025 03:36:42 GMT</pubDate>
            <description><![CDATA[<h2 id="🚀-langchain-시작하기">🚀 LangChain 시작하기</h2>
<p>LangChain은 요즘 핫한 AI 애플리케이션 개발을 도와주는 오픈소스 라이브러리이다. 간단하게 말해 LangChain은 <strong>LLM(Large Language Model)</strong> 앱을 빠르게 조립하는 파이프라인 프레임워크인데, 프롬프트 설계부터 도구 호출, 검색 연동까지 구성 요소를 작은 블록처럼 연결하는 것이 핵심이다.</p>
<img width="643" height="432" alt="Image" src="https://github.com/user-attachments/assets/0336d833-4f52-4a0e-a62d-ef1d7fcc6acc" />

<p>위 그림은 LangChain에서 사용하는 도구들을 나눈 것이라 생각하면 된다.
한번 역할을 간단하게 살펴보겠다.</p>
<p>코드 환경에서는 <strong>LangChain을 통해 개발</strong>을 진행하게 된다. 그리고 LangGraph를 통해 HIP(Human In the Loop)이라는 방법으로 고도화 작업이 가능하다고 하는데,
이 부분은 나중에 알아보도록 하자.</p>
<p>또한 <strong>LangSmith를 통해 품질 모니터링과 테스트</strong> 같은 활동이 가능하다.</p>
<p>마지막으로 <strong>LangGraph Platform은 실제 제품화를 위한 API 추출과 Assistant화</strong>를 도와준다고 한다.</p>
<p>나는 일단 LangChain을 통해 코드 환경에서 간단한 실습을 진행하고, 프로젝트화를 통해 기능을 넣을 생각이다.</p>
<hr>
<h3 id="💬-chatbot-만들기">💬 Chatbot 만들기</h3>
<p>이제 Chatbot 실습을 진행하겠다.</p>
<pre><code class="language-txt">langchain
langchain-core
langgraph&gt;0.2.27
dotenv</code></pre>
<p>먼저 필요한 패키지다.</p>
<pre><code class="language-bash">pip install -qU &quot;langchain[google-genai]&quot;</code></pre>
<p>이 명령은 google-genai를 사용하게 해주는 LangChain 키트를 설치하는 명령어다.
이 명령어는 꼭 별도로 설치해야 한다. 그렇지 않으면 의존성 오류가 발생한다.</p>
<pre><code class="language-python">import os
from dotenv import load_dotenv

load_dotenv()

key = os.getenv(&quot;GOOGLE_API_KEY&quot;)
if not key:
    raise EnvironmentError(&quot;GOOGLE_API_KEY not found in .env&quot;)

from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage

model = init_chat_model(&quot;gemini-2.5-flash&quot;, model_provider=&quot;google_genai&quot;)

resp = model.invoke(
    [
        HumanMessage(content=&quot;Hello, my name is choihyeok&quot;),
        AIMessage(content=&quot;Hello choihyeok! How can I assist you today?&quot;),
        HumanMessage(content=&quot;What&#39;s my name?&quot;),
    ]
)
print(resp.content)</code></pre>
<p>위 코드를 보면 <code>from langchain.chat_models import init_chat_model</code>을 통해 Chatbot 모델 기능을 사용 가능하다. 여기에 API 키를 넣어 사용하면 되며, 여기서는 Google-Gemini 2.25-flash 모델을 사용했다.</p>
<p><code>invoke</code>를 통해 모델에 메시지를 삽입하고, <code>HumanMessage</code>와 <code>AIMessage</code>로 사람과 AI의 대화를 구분한다. 마지막에 <code>resp.content</code>를 출력하면 텍스트 응답만 확인할 수 있다.</p>
<h4 id="📌-실행-예시">📌 실행 예시</h4>
<pre><code class="language-bash">Your name is **choihyeok**.</code></pre>
<hr>
<p>그런데 우리가 원하는 것은 <strong>프로세스 환경에서 실시간 대화</strong>다.</p>
<pre><code class="language-python">from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph

workflow = StateGraph(state_schema=MessagesState)

def call_model(state: MessagesState):
    response = model.invoke(state[&quot;messages&quot;])
    return {&quot;messages&quot;: response}

workflow.add_edge(START, &quot;model&quot;)
workflow.add_node(&quot;model&quot;, call_model)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;abc123&quot;}}

query = &quot;Hi! I&#39;m HYK.&quot;
input_messages = [HumanMessage(query)]
output = app.invoke({&quot;messages&quot;: input_messages}, config)
output[&quot;messages&quot;][-1].pretty_print()

query = &quot;What&#39;s my name?&quot;
input_messages = [HumanMessage(query)]
output = app.invoke({&quot;messages&quot;: input_messages}, config)
output[&quot;messages&quot;][-1].pretty_print()</code></pre>
<p><code>call_model</code> 메서드를 선언해 state를 설정하고, LangGraph의 State 기능을 통해 대화 상태를 기억하게 한다.</p>
<p>여기서 LangGraph의 강력한 기능을 알 수 있는데, 바로 State로 워크플로를 관리하는 것이다. LangGraph는 하나의 프로세스를 정의해서 해당 모델이 어떠한 상태를 가지고 있는지 정의한다.</p>
<pre><code class="language-python">workflow = StateGraph(state_schema=MessagesState)</code></pre>
<p>해당 코드가 핵심인데, <code>StateGraph</code>를 <code>MessagesState</code>로 정의하는 워크프로를 생성한다는 의미이다. 이제 모델은 해당 워크플로안에서 유지되면서 메세지를 받게된다.</p>
<pre><code class="language-python">workflow.add_edge(START, &quot;model&quot;)
workflow.add_node(&quot;model&quot;, call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)</code></pre>
<p>워크플로는 노드와 엣지 형태로 모델의 시작점과 진행 사항을 연결 및 유지 해준다.</p>
<p>위와 같이 구현하면 모델은 대화를 메모리에 저장하며 진행한다. 다만 현재는 프로세스를 종료하면 쓰레드가 정리되어 기록이 사라진다.</p>
<pre><code class="language-python">config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;abc123&quot;}}</code></pre>
<p>이 코드로 쓰레드를 설정해 대화 세션을 구분할 수 있다.</p>
<h4 id="📌-실행-예시-1">📌 실행 예시</h4>
<pre><code>================================== Ai Message ==================================
Hi HYK! It&#39;s nice to meet you.
How can I help you today?

================================== Ai Message ==================================
Your name is HYK! You told me that when you first introduced yourself.

================================== Ai Message ==================================
I don&#39;t know your name. As an AI, I don&#39;t have access to personal information about you or your identity.
How can I help you today?

================================== Ai Message ==================================
Your name is HYK! I remember you told me that when we first started chatting.</code></pre><p>위 출력에서 보듯, 첫 번째 메시지에서 이름을 알려줬지만 세번째 쓰레드에서는 이름을 모른다.</p>
<p>기본적인 구현은 위와 같으며, 다음에는 <strong>프롬프트 엔지니어링</strong>을 알아보겠다.</p>
<p><a href="https://python.langchain.com/docs/tutorials/chatbot/">[참고] LangChain Build a Chatbot</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Command] Linux 핵심 명령어 60개]]></title>
            <link>https://velog.io/@choi-hyk/Command-Linux-%ED%95%B5%EC%8B%AC-%EB%AA%85%EB%A0%B9%EC%96%B4-60%EA%B0%9C</link>
            <guid>https://velog.io/@choi-hyk/Command-Linux-%ED%95%B5%EC%8B%AC-%EB%AA%85%EB%A0%B9%EC%96%B4-60%EA%B0%9C</guid>
            <pubDate>Tue, 12 Aug 2025 00:31:08 GMT</pubDate>
            <description><![CDATA[<h2 id="1-ls--디렉터리-목록-표시-list">1. <strong>ls</strong> — 디렉터리 목록 표시 List</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">ls [옵션] [경로]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-l 상세 목록</p>
</li>
<li><p>-a 숨김 파일 포함</p>
</li>
<li><p>-R 하위 디렉터리 재귀</p>
</li>
<li><p>-h 사람이 읽기 쉬운 크기</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">ls -la</code></pre>
<ul>
<li>현재 디렉터리의 숨김 파일까지 상세 정보 출력</li>
</ul>
<pre><code class="language-bash">ls -lh /var/log</code></pre>
<ul>
<li>/var/log의 파일 크기를 사람이 읽기 쉬운 단위로 표시</li>
</ul>
<pre><code class="language-bash">ls -R ~/projects</code></pre>
<ul>
<li>projects 아래 하위 디렉터리까지 재귀적으로 나열</li>
</ul>
<hr>
<h2 id="2-pwd--현재-작업-디렉터리-경로-출력-print-working-directory">2. <strong>pwd</strong> — 현재 작업 디렉터리 경로 출력 Print Working Directory</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">pwd [옵션]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-P 실제 경로(심볼릭 링크 해제)</p>
</li>
<li><p>-L 논리 경로(기본값)</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">pwd</code></pre>
<ul>
<li>현재 작업 디렉터리의 논리 경로 표시</li>
</ul>
<pre><code class="language-bash">pwd -P</code></pre>
<ul>
<li>심볼릭 링크를 해제한 실제 경로 표시</li>
</ul>
<pre><code class="language-bash">cd /var &amp;&amp; pwd</code></pre>
<ul>
<li>/var로 이동 후 경로 출력</li>
</ul>
<hr>
<h2 id="3-cd--디렉터리-이동-change-directory">3. <strong>cd</strong> — 디렉터리 이동 Change Directory</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cd [경로]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>.. 상위 디렉터리</p>
</li>
<li><p>~ 홈 디렉터리</p>
</li>
<li><ul>
<li>이전 디렉터리로 전환</li>
</ul>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">cd ..</code></pre>
<ul>
<li>상위 디렉터리로 이동</li>
</ul>
<pre><code class="language-bash">cd ~/Downloads</code></pre>
<ul>
<li>홈의 Downloads 디렉터리로 이동</li>
</ul>
<pre><code class="language-bash">cd -</code></pre>
<ul>
<li>직전 작업 디렉터리로 이동</li>
</ul>
<hr>
<h2 id="4-mkdir--디렉터리-생성-make-directory">4. <strong>mkdir</strong> — 디렉터리 생성 Make Directory</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">mkdir [옵션] 디렉터리</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-p 하위 경로까지 일괄 생성</p>
</li>
<li><p>-m 권한 지정</p>
</li>
<li><p>-v 생성 과정 출력</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">mkdir project</code></pre>
<ul>
<li>현재 위치에 project 디렉터리 생성</li>
</ul>
<pre><code class="language-bash">mkdir -p a/b/c</code></pre>
<ul>
<li>중간 경로가 없어도 a/b/c까지 모두 생성</li>
</ul>
<pre><code class="language-bash">mkdir -m 755 web</code></pre>
<ul>
<li>권한 755로 web 디렉터리 생성</li>
</ul>
<hr>
<h2 id="5-rmdir--비어-있는-디렉터리-삭제-remove-directory">5. <strong>rmdir</strong> — 비어 있는 디렉터리 삭제 Remove Directory</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">rmdir [옵션] 디렉터리</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-p 상위 디렉터리 연쇄 삭제</p>
</li>
<li><p>-v 삭제 과정 출력</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">rmdir empty</code></pre>
<ul>
<li>empty 디렉터리를 삭제(비어 있어야 함)</li>
</ul>
<pre><code class="language-bash">rmdir -p a/b/c</code></pre>
<ul>
<li>c 삭제 후 비어 있으면 b, a 순서로 삭제</li>
</ul>
<pre><code class="language-bash">rmdir -v old</code></pre>
<ul>
<li>삭제 과정을 출력하며 비어 있는 old 삭제</li>
</ul>
<hr>
<h2 id="6-rm--파일디렉터리-삭제-remove">6. <strong>rm</strong> — 파일/디렉터리 삭제 Remove</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">rm [옵션] 파일/디렉터리</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-r 디렉터리 재귀 삭제</p>
</li>
<li><p>-f 강제 삭제(확인 없음)</p>
</li>
<li><p>-i 삭제 전 확인</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">rm file.txt</code></pre>
<ul>
<li>일반 파일 file.txt 삭제</li>
</ul>
<pre><code class="language-bash">rm -rf build</code></pre>
<ul>
<li>build 디렉터리와 내부 모든 항목 강제 삭제</li>
</ul>
<pre><code class="language-bash">rm -i data.csv</code></pre>
<ul>
<li>삭제 전 확인 프롬프트 표시</li>
</ul>
<hr>
<h2 id="7-cp--파일디렉터리-복사-copy">7. <strong>cp</strong> — 파일/디렉터리 복사 Copy</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cp [옵션] 원본 대상</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-R 디렉터리 재귀 복사</p>
</li>
<li><p>-p 권한/타임스탬프 보존</p>
</li>
<li><p>-i 덮어쓰기 전 확인</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">cp a.txt b.txt</code></pre>
<ul>
<li>a.txt를 b.txt로 복사</li>
</ul>
<pre><code class="language-bash">cp -R src dst</code></pre>
<ul>
<li>src 디렉터리 전체를 dst로 복사</li>
</ul>
<pre><code class="language-bash">cp -p config.ini /etc/app/</code></pre>
<ul>
<li>속성을 보존하며 복사</li>
</ul>
<hr>
<h2 id="8-mv--파일디렉터리-이동-또는-이름-변경-move">8. <strong>mv</strong> — 파일/디렉터리 이동 또는 이름 변경 Move</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">mv [옵션] 원본 대상</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-i 덮어쓰기 전 확인</p>
</li>
<li><p>-n 덮어쓰기 방지</p>
</li>
<li><p>-v 작업 로그 출력</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">mv a.txt b.txt</code></pre>
<ul>
<li>a.txt의 이름을 b.txt로 변경</li>
</ul>
<pre><code class="language-bash">mv /tmp/logs ./logs_old</code></pre>
<ul>
<li>/tmp/logs를 현재 위치로 이동하며 logs_old로 변경</li>
</ul>
<pre><code class="language-bash">mv *.log logs/</code></pre>
<ul>
<li>현재 디렉터리의 로그 파일을 logs로 이동</li>
</ul>
<hr>
<h2 id="9-touch--빈-파일-생성-또는-타임스탬프-변경">9. <strong>touch</strong> — 빈 파일 생성 또는 타임스탬프 변경</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">touch [옵션] 파일...</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-t 지정한 타임스탬프 설정</p>
</li>
<li><p>-a 접근 시간만 변경</p>
</li>
<li><p>-m 수정 시간만 변경</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">touch notes.md</code></pre>
<ul>
<li>빈 파일 생성(이미 존재 시 타임스탬프 갱신)</li>
</ul>
<pre><code class="language-bash">touch -t 202501010101 file</code></pre>
<ul>
<li>지정한 시각으로 타임스탬프 설정</li>
</ul>
<pre><code class="language-bash">touch -a -m data.bin</code></pre>
<ul>
<li>접근/수정 시간을 현재 시각으로 변경</li>
</ul>
<hr>
<h2 id="10-file--파일-타입-식별">10. <strong>file</strong> — 파일 타입 식별</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">file [옵션] 파일...</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-i MIME 타입 표시</p>
</li>
<li><p>-b 파일명 없이 결과만</p>
</li>
<li><p>-k 가능한 추가 정보 표시</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">file image.png</code></pre>
<ul>
<li>PNG 이미지 파일로 식별</li>
</ul>
<pre><code class="language-bash">file -i report.pdf</code></pre>
<ul>
<li>MIME 타입(application/pdf) 표시</li>
</ul>
<pre><code class="language-bash">file -b archive.tar.gz</code></pre>
<ul>
<li>파일명 없이 타입 정보만 출력</li>
</ul>
<hr>
<h2 id="11-zip--zip-압축-생성">11. <strong>zip</strong> — ZIP 압축 생성</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">zip [옵션] 아카이브.zip 파일들</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-r 디렉터리 재귀 압축</p>
</li>
<li><p>-9 최대 압축률</p>
</li>
<li><p>-q 조용한 모드</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">zip logs.zip *.log</code></pre>
<ul>
<li>현재 디렉터리의 .log 파일을 logs.zip으로 압축</li>
</ul>
<pre><code class="language-bash">zip -r site.zip ./site</code></pre>
<ul>
<li>site 디렉터리 전체를 압축</li>
</ul>
<pre><code class="language-bash">zip -9 backup.zip data/*</code></pre>
<ul>
<li>최대 압축률로 data 내용을 압축</li>
</ul>
<hr>
<h2 id="12-unzip--zip-압축-해제">12. <strong>unzip</strong> — ZIP 압축 해제</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">unzip [옵션] 아카이브.zip [-d 대상]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-d 대상 디렉터리 지정</p>
</li>
<li><p>-l 목록만 보기</p>
</li>
<li><p>-o 덮어쓰기 강제</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">unzip logs.zip</code></pre>
<ul>
<li>현재 디렉터리에 압축 해제</li>
</ul>
<pre><code class="language-bash">unzip -d out data.zip</code></pre>
<ul>
<li>out 디렉터리로 압축 해제</li>
</ul>
<pre><code class="language-bash">unzip -l site.zip</code></pre>
<ul>
<li>압축 내부 파일 목록만 출력</li>
</ul>
<hr>
<h2 id="13-tar--아카이브-생성해제-tape-archiver">13. <strong>tar</strong> — 아카이브 생성/해제 Tape ARchiver</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">tar [옵션] [아카이브] [파일/디렉터리...]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-c 생성</p>
</li>
<li><p>-x 해제</p>
</li>
<li><p>-t 목록</p>
</li>
<li><p>-f 파일 지정</p>
</li>
<li><p>-z gzip 사용</p>
</li>
<li><p>-v 진행 표시</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">tar -cf backup.tar folder</code></pre>
<ul>
<li>folder를 backup.tar로 묶기(압축 없음)</li>
</ul>
<pre><code class="language-bash">tar -czf backup.tar.gz folder</code></pre>
<ul>
<li>gzip으로 압축한 tarball 생성</li>
</ul>
<pre><code class="language-bash">tar -xzf backup.tar.gz -C /restore</code></pre>
<ul>
<li>/restore에 압축 해제</li>
</ul>
<hr>
<h2 id="14-nano--터미널-텍스트-편집기">14. <strong>nano</strong> — 터미널 텍스트 편집기</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">nano [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-l 줄 번호 표시</p>
</li>
<li><p>-B 백업 파일 생성</p>
</li>
<li><p>-m 마우스 지원</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">nano /etc/hosts</code></pre>
<ul>
<li>hosts 파일을 편집</li>
</ul>
<pre><code class="language-bash">nano -l notes.md</code></pre>
<ul>
<li>줄 번호가 표시된 상태로 편집</li>
</ul>
<pre><code class="language-bash">nano -B config.ini</code></pre>
<ul>
<li>편집 시 config.ini~ 백업 생성</li>
</ul>
<hr>
<h2 id="15-vi--터미널-텍스트-편집기vim-포함">15. <strong>vi</strong> — 터미널 텍스트 편집기(Vim 포함)</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">vi [+행] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>+N N번째 줄에서 시작</p>
</li>
<li><p>-R 읽기 전용 모드</p>
</li>
<li><p>-u NONE 기본 설정 없이</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">vi app.py</code></pre>
<ul>
<li>app.py 파일 편집</li>
</ul>
<pre><code class="language-bash">vi +10 main.c</code></pre>
<ul>
<li>10번째 줄에서 편집 시작</li>
</ul>
<pre><code class="language-bash">vi -R /etc/fstab</code></pre>
<ul>
<li>읽기 전용으로 열기</li>
</ul>
<hr>
<h2 id="16-cat--파일-내용-출력결합-concatenate">16. <strong>cat</strong> — 파일 내용 출력/결합 Concatenate</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cat [옵션] [파일...]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-n 줄 번호 출력</p>
</li>
<li><p>-A 제어문자 표시</p>
</li>
<li><p>-E 줄 끝 표시</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">cat file.txt</code></pre>
<ul>
<li>파일 내용을 표준출력으로 표시</li>
</ul>
<pre><code class="language-bash">cat a.txt b.txt &gt; all.txt</code></pre>
<ul>
<li>두 파일을 결합해 all.txt 생성</li>
</ul>
<pre><code class="language-bash">cat -n code.c | less</code></pre>
<ul>
<li>줄 번호를 붙여 페이지 단위로 보기</li>
</ul>
<hr>
<h2 id="17-tac--파일을-거꾸로-출력">17. <strong>tac</strong> — 파일을 거꾸로 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">tac [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-s 구분자 지정</p>
</li>
<li><p>-b 구분자 뒤에 출력</p>
</li>
<li><p>-r 정규식 구분자</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">tac file.txt</code></pre>
<ul>
<li>마지막 줄부터 첫 줄까지 역순 출력</li>
</ul>
<pre><code class="language-bash">tac -s &#39;---&#39; parts.txt</code></pre>
<ul>
<li>지정 구분자로 블록 단위 역순 출력</li>
</ul>
<pre><code class="language-bash">tac -b -s &quot;,&quot; csv_parts.txt</code></pre>
<ul>
<li>구분자 뒤에 이어 붙여 역순 출력</li>
</ul>
<hr>
<h2 id="18-grep--패턴-매칭-검색-global-regular-expression-print">18. <strong>grep</strong> — 패턴 매칭 검색 Global Regular Expression Print</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">grep [옵션] 패턴 [파일...]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-i 대소문자 무시</p>
</li>
<li><p>-n 줄 번호</p>
</li>
<li><p>-r 디렉터리 재귀</p>
</li>
<li><p>-E 확장 정규식</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">grep -n &#39;ERROR&#39; app.log</code></pre>
<ul>
<li>ERROR 포함 라인을 라인 번호와 함께 표시</li>
</ul>
<pre><code class="language-bash">dmesg | grep -i usb</code></pre>
<ul>
<li>커널 로그에서 usb 관련 메시지 필터링</li>
</ul>
<pre><code class="language-bash">grep -r &#39;TODO&#39; src/</code></pre>
<ul>
<li>src 디렉터리 전체에서 TODO 검색</li>
</ul>
<hr>
<h2 id="19-sed--스트림-편집기-stream-editor">19. <strong>sed</strong> — 스트림 편집기 Stream EDitor</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">sed [옵션] &#39;스크립트&#39; [파일]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-n 선택 출력</p>
</li>
<li><p>-E 확장 정규식</p>
</li>
<li><p>-i 제자리 수정(in-place)</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">sed -n &#39;1,10p&#39; file.txt</code></pre>
<ul>
<li>1~10행만 출력</li>
</ul>
<pre><code class="language-bash">sed &#39;s/red/blue/g&#39; colors.txt</code></pre>
<ul>
<li>red를 blue로 전역 치환 후 출력</li>
</ul>
<pre><code class="language-bash">sed -i &#39;s/DEBUG=false/DEBUG=true/&#39; .env</code></pre>
<ul>
<li>파일을 직접 수정하여 값 변경</li>
</ul>
<hr>
<h2 id="20-head--파일-앞부분-출력">20. <strong>head</strong> — 파일 앞부분 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">head [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-n N행 출력</p>
</li>
<li><p>-c N바이트 출력</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">head -n 20 access.log</code></pre>
<ul>
<li>앞 20줄 출력</li>
</ul>
<pre><code class="language-bash">head -c 1K big.bin</code></pre>
<ul>
<li>앞 1024바이트 출력</li>
</ul>
<pre><code class="language-bash">head README.md</code></pre>
<ul>
<li>기본 10줄 출력</li>
</ul>
<hr>
<h2 id="21-tail--파일-뒷부분-출력">21. <strong>tail</strong> — 파일 뒷부분 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">tail [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-n N행</p>
</li>
<li><p>-f 추가 내용 실시간 추적</p>
</li>
<li><p>-c N바이트</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">tail -n 50 access.log</code></pre>
<ul>
<li>마지막 50줄 출력</li>
</ul>
<pre><code class="language-bash">tail -f app.log</code></pre>
<ul>
<li>로그 파일을 실시간으로 추적</li>
</ul>
<pre><code class="language-bash">tail -c 512 data.bin</code></pre>
<ul>
<li>마지막 512바이트 출력</li>
</ul>
<hr>
<h2 id="22-awk--패턴-스캔과-처리-언어">22. <strong>awk</strong> — 패턴 스캔과 처리 언어</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">awk [옵션] &#39;패턴{동작}&#39; [파일]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-F 필드 구분자 지정</p>
</li>
<li><p>NR 현재 레코드 번호</p>
</li>
<li><p>NF 필드 개수</p>
</li>
<li><p>$1..$N 필드 참조</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">awk -F: &#39;{print $1}&#39; /etc/passwd</code></pre>
<ul>
<li>콜론 기준 1번째 필드(계정명) 출력</li>
</ul>
<pre><code class="language-bash">awk &#39;{sum+=$1} END{print sum}&#39; nums.txt</code></pre>
<ul>
<li>첫 필드 합계 계산</li>
</ul>
<pre><code class="language-bash">awk &#39;$3&gt;100 {print $0}&#39; data.tsv</code></pre>
<ul>
<li>3번째 필드가 100 초과인 행 출력</li>
</ul>
<hr>
<h2 id="23-sort--정렬">23. <strong>sort</strong> — 정렬</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">sort [옵션] [파일]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-r 내림차순</p>
</li>
<li><p>-n 숫자 정렬</p>
</li>
<li><p>-k N 정렬 키</p>
</li>
<li><p>-t 구분자</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">sort names.txt</code></pre>
<ul>
<li>기본 오름차순 정렬</li>
</ul>
<pre><code class="language-bash">sort -nr scores.txt</code></pre>
<ul>
<li>숫자 기준 내림차순 정렬</li>
</ul>
<pre><code class="language-bash">sort -t, -k2 data.csv</code></pre>
<ul>
<li>쉼표 구분, 2열 기준 정렬</li>
</ul>
<hr>
<h2 id="24-cut--필드문자-범위-추출">24. <strong>cut</strong> — 필드/문자 범위 추출</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cut [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-d 구분자</p>
</li>
<li><p>-f 필드 리스트</p>
</li>
<li><p>-c 문자 범위</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">cut -d, -f2,4 data.csv</code></pre>
<ul>
<li>2, 4열만 추출</li>
</ul>
<pre><code class="language-bash">cut -c1-8 ids.txt</code></pre>
<ul>
<li>각 행의 1~8번째 문자 추출</li>
</ul>
<pre><code class="language-bash">cut -d: -f1 /etc/passwd</code></pre>
<ul>
<li>계정명 필드만 출력</li>
</ul>
<hr>
<h2 id="25-diff--두-파일의-차이-비교">25. <strong>diff</strong> — 두 파일의 차이 비교</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">diff [옵션] 파일1 파일2</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-u 통합 형식</p>
</li>
<li><p>-r 디렉터리 재귀</p>
</li>
<li><p>-q 차이 유무만</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">diff a.txt b.txt</code></pre>
<ul>
<li>두 텍스트의 라인 단위 차이 표시</li>
</ul>
<pre><code class="language-bash">diff -u old.c new.c</code></pre>
<ul>
<li>패치에 적합한 통합 형식으로 표시</li>
</ul>
<pre><code class="language-bash">diff -r src_old src_new</code></pre>
<ul>
<li>디렉터리 간 차이를 재귀적으로 비교</li>
</ul>
<hr>
<h2 id="26-tee--출력을-화면과-파일에-동시에-기록">26. <strong>tee</strong> — 출력을 화면과 파일에 동시에 기록</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cmd | tee [옵션] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-a 파일에 이어쓰기</p>
</li>
<li><p>-i SIGINT 무시</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">echo hello | tee out.txt</code></pre>
<ul>
<li>hello를 화면과 out.txt에 동시에 기록</li>
</ul>
<pre><code class="language-bash">dmesg | tee -a kernel.log</code></pre>
<ul>
<li>커널 메시지를 화면+파일(추가)로 기록</li>
</ul>
<pre><code class="language-bash">ls -l | tee list.txt | wc -l</code></pre>
<ul>
<li>목록을 저장하고 행 수 계산</li>
</ul>
<hr>
<h2 id="27-tr--문자-변환삭제-translate">27. <strong>tr</strong> — 문자 변환/삭제 Translate</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">tr [옵션] 집합1 [집합2]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-d 삭제</p>
</li>
<li><p>-s 반복 문자 압축</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">echo &#39;Hello&#39; | tr &#39;[:upper:]&#39; &#39;[:lower:]&#39;</code></pre>
<ul>
<li>대문자를 소문자로 변환</li>
</ul>
<pre><code class="language-bash">echo &#39;a,,b,,,c&#39; | tr -s &#39;,&#39;</code></pre>
<ul>
<li>연속된 쉼표를 하나로 압축</li>
</ul>
<pre><code class="language-bash">echo &#39;abc123&#39; | tr -d &#39;0-9&#39;</code></pre>
<ul>
<li>숫자 문자 삭제</li>
</ul>
<hr>
<h2 id="28-chmod--파일-권한-변경-change-mode">28. <strong>chmod</strong> — 파일 권한 변경 Change Mode</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">chmod [옵션] 모드 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>u/g/o 사용자/그룹/기타</p>
</li>
<li><p>+/- 권한 추가/제거</p>
</li>
<li><p>숫자 표기 755 등</p>
</li>
<li><p>-R 재귀 적용</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">chmod 644 file.txt</code></pre>
<ul>
<li>소유자 읽기/쓰기, 그 외 읽기</li>
</ul>
<pre><code class="language-bash">chmod u+x script.sh</code></pre>
<ul>
<li>소유자에 실행 권한 추가</li>
</ul>
<pre><code class="language-bash">chmod -R 755 bin/</code></pre>
<ul>
<li>디렉터리 전체에 실행 권한 부여</li>
</ul>
<hr>
<h2 id="29-chown--파일-소유자그룹-변경-change-owner">29. <strong>chown</strong> — 파일 소유자/그룹 변경 Change Owner</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">chown [옵션] 소유자[:그룹] 파일</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-R 재귀 적용</p>
</li>
<li><p>--from 기존 소유자 조건</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">sudo chown user file.txt</code></pre>
<ul>
<li>file.txt의 소유자를 user로 변경</li>
</ul>
<pre><code class="language-bash">sudo chown user:staff -R www</code></pre>
<ul>
<li>www 디렉터리와 내부의 소유자/그룹 변경</li>
</ul>
<pre><code class="language-bash">sudo chown :www-data app.log</code></pre>
<ul>
<li>그룹만 www-data로 변경</li>
</ul>
<hr>
<h2 id="30-ln--하드심볼릭-링크-생성">30. <strong>ln</strong> — 하드/심볼릭 링크 생성</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">ln [옵션] 원본 링크명</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-s 심볼릭 링크</p>
</li>
<li><p>-f 기존 링크 덮어쓰기</p>
</li>
<li><p>-n 심볼릭 링크 대상 처리</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">ln file.txt file_hard</code></pre>
<ul>
<li>하드 링크 생성</li>
</ul>
<pre><code class="language-bash">ln -s /opt/app/bin/run ./run</code></pre>
<ul>
<li>실행 파일의 심볼릭 링크 생성</li>
</ul>
<pre><code class="language-bash">ln -sf new.conf current.conf</code></pre>
<ul>
<li>기존 링크를 새 대상에 강제로 갱신</li>
</ul>
<hr>
<h2 id="31-find--파일-검색">31. <strong>find</strong> — 파일 검색</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">find 경로 [조건] [동작]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-name 이름 패턴</p>
</li>
<li><p>-type 파일 타입</p>
</li>
<li><p>-size 크기 조건</p>
</li>
<li><p>-exec 명령 실행</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">find . -name &#39;*.log&#39;</code></pre>
<ul>
<li>현재 디렉터리에서 .log 파일 검색</li>
</ul>
<pre><code class="language-bash">find /var -type d -name &#39;nginx&#39;</code></pre>
<ul>
<li>/var에서 디렉터리 nginx 검색</li>
</ul>
<pre><code class="language-bash">find . -size +100M -exec rm -i {} \;</code></pre>
<ul>
<li>100MB 초과 파일을 찾아 삭제 확인</li>
</ul>
<hr>
<h2 id="32-locate--인덱스-기반-빠른-파일-검색">32. <strong>locate</strong> — 인덱스 기반 빠른 파일 검색</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">locate [패턴]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-i 대소문자 무시</p>
</li>
<li><p>-n N개 결과 제한</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">locate ssh_config</code></pre>
<ul>
<li>시스템 DB에서 ssh_config 경로 빠르게 검색</li>
</ul>
<pre><code class="language-bash">locate -i readme</code></pre>
<ul>
<li>대소문자 무시하고 README/Readme 등 검색</li>
</ul>
<pre><code class="language-bash">locate -n 5 nginx.conf</code></pre>
<ul>
<li>최대 5개 결과만 표시</li>
</ul>
<hr>
<h2 id="33-which--실행-파일의-경로-표시">33. <strong>which</strong> — 실행 파일의 경로 표시</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">which 프로그램</code></pre>
<p><strong>예시</strong></p>
<pre><code class="language-bash">which python</code></pre>
<ul>
<li>python 실행 파일의 절대 경로 출력</li>
</ul>
<pre><code class="language-bash">which ls</code></pre>
<ul>
<li>ls 명령의 실제 경로 확인</li>
</ul>
<pre><code class="language-bash">which node</code></pre>
<ul>
<li>node가 PATH에 있는지 확인</li>
</ul>
<hr>
<h2 id="34-whereis--명령의-바이너리소스매뉴얼-위치">34. <strong>whereis</strong> — 명령의 바이너리/소스/매뉴얼 위치</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">whereis 프로그램</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-b 바이너리만</p>
</li>
<li><p>-m 매뉴얼만</p>
</li>
<li><p>-s 소스만</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">whereis ls</code></pre>
<ul>
<li>ls의 바이너리와 매뉴얼 위치 표시</li>
</ul>
<pre><code class="language-bash">whereis -b gcc</code></pre>
<ul>
<li>gcc 바이너리 위치만 표시</li>
</ul>
<pre><code class="language-bash">whereis -m bash</code></pre>
<ul>
<li>bash의 man 페이지 위치 표시</li>
</ul>
<hr>
<h2 id="35-du--디스크-사용량-추정-disk-usage">35. <strong>du</strong> — 디스크 사용량 추정 Disk Usage</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">du [옵션] [경로]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-h 사람이 읽기 쉬운 단위</p>
</li>
<li><p>-s 총합 요약</p>
</li>
<li><p>-d 깊이 제한</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">du -h .</code></pre>
<ul>
<li>현재 디렉터리의 각 항목 사용량 표시</li>
</ul>
<pre><code class="language-bash">du -sh /var/log</code></pre>
<ul>
<li>/var/log의 총 사용량 요약</li>
</ul>
<pre><code class="language-bash">du -h -d1 /home/user</code></pre>
<ul>
<li>/home/user 하위 1단계까지 사용량 표시</li>
</ul>
<hr>
<h2 id="36-df--파일시스템별-여유전체-디스크-용량">36. <strong>df</strong> — 파일시스템별 여유/전체 디스크 용량</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">df [옵션] [경로]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-h 사람이 읽기 쉬운 단위</p>
</li>
<li><p>-T 파일시스템 타입 표시</p>
</li>
<li><p>-i inode 정보</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">df -h</code></pre>
<ul>
<li>모든 마운트의 용량/사용량을 사람이 읽기 쉬운 단위로 표시</li>
</ul>
<pre><code class="language-bash">df -T /</code></pre>
<ul>
<li>루트 파티션의 파일시스템 타입과 용량 표시</li>
</ul>
<pre><code class="language-bash">df -i</code></pre>
<ul>
<li>inode 사용 현황 표시</li>
</ul>
<hr>
<h2 id="37-free--메모리스왑-사용량">37. <strong>free</strong> — 메모리/스왑 사용량</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">free [옵션]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-h 사람이 읽기 쉬운 단위</p>
</li>
<li><p>-m MB 단위</p>
</li>
<li><p>-s N초마다 갱신</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">free -h</code></pre>
<ul>
<li>RAM/스왑 총량과 사용량을 보기 좋게 표시</li>
</ul>
<pre><code class="language-bash">free -m</code></pre>
<ul>
<li>메모리 정보를 MB 단위로 표시</li>
</ul>
<pre><code class="language-bash">free -h -s 5</code></pre>
<ul>
<li>5초마다 갱신하여 메모리 사용 추적</li>
</ul>
<hr>
<h2 id="38-top--실시간-프로세스리소스-모니터">38. <strong>top</strong> — 실시간 프로세스/리소스 모니터</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">top</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-p PID 특정 프로세스만</p>
</li>
<li><p>-u USER 사용자 필터</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">top</code></pre>
<ul>
<li>CPU/메모리 사용량 상위 프로세스 실시간 표시</li>
</ul>
<pre><code class="language-bash">top -u www-data</code></pre>
<ul>
<li>특정 사용자 프로세스만 모니터링</li>
</ul>
<pre><code class="language-bash">top -p 1234</code></pre>
<ul>
<li>PID 1234의 리소스 사용만 추적</li>
</ul>
<hr>
<h2 id="39-ps--프로세스-스냅샷">39. <strong>ps</strong> — 프로세스 스냅샷</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">ps [옵션]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>aux 모든 프로세스 상세</p>
</li>
<li><p>-ef 표준 포맷</p>
</li>
<li><p>-o 출력 형식 지정</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">ps aux | grep nginx</code></pre>
<ul>
<li>nginx 관련 프로세스 찾기</li>
</ul>
<pre><code class="language-bash">ps -ef --forest</code></pre>
<ul>
<li>트리 형태로 프로세스 관계 표시</li>
</ul>
<pre><code class="language-bash">ps -o pid,cmd -p 1234</code></pre>
<ul>
<li>특정 PID의 정보만 출력</li>
</ul>
<hr>
<h2 id="40-kill--프로세스-종료-신호-전송">40. <strong>kill</strong> — 프로세스 종료 신호 전송</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">kill [옵션] PID</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-SIGTERM 정상 종료 요청</p>
</li>
<li><p>-9 강제 종료 SIGKILL</p>
</li>
<li><p>-l 신호 목록</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">kill 1234</code></pre>
<ul>
<li>PID 1234에 종료 요청</li>
</ul>
<pre><code class="language-bash">kill -9 5678</code></pre>
<ul>
<li>PID 5678 강제 종료</li>
</ul>
<pre><code class="language-bash">kill -HUP 1111</code></pre>
<ul>
<li>PID 1111 설정 재로딩(HUP) 유도</li>
</ul>
<hr>
<h2 id="41-pkill--이름조건으로-프로세스에-신호-전송">41. <strong>pkill</strong> — 이름/조건으로 프로세스에 신호 전송</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">pkill [옵션] 패턴</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-f 전체 명령줄 매칭</p>
</li>
<li><p>-u 사용자 필터</p>
</li>
<li><p>-9 SIGKILL</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">pkill nginx</code></pre>
<ul>
<li>nginx라는 이름의 프로세스 종료 요청</li>
</ul>
<pre><code class="language-bash">pkill -f &#39;python app.py&#39;</code></pre>
<ul>
<li>명령줄에 패턴이 포함된 프로세스 종료</li>
</ul>
<pre><code class="language-bash">pkill -u www-data nginx</code></pre>
<ul>
<li>특정 사용자 소유 nginx만 종료</li>
</ul>
<hr>
<h2 id="42-xargs--표준입력을-인수로-변환해-명령-실행">42. <strong>xargs</strong> — 표준입력을 인수로 변환해 명령 실행</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">xargs [옵션] 명령</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-0 널 구분자 입력</p>
</li>
<li><p>-n N개씩 나눠 실행</p>
</li>
<li><p>-I{} 자리표시자 사용</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">printf &#39;a\nb\nc&#39; | xargs echo</code></pre>
<ul>
<li>a b c를 인수로 전달하여 한 줄 출력</li>
</ul>
<pre><code class="language-bash">find . -name &#39;*.log&#39; -print0 | xargs -0 rm -f</code></pre>
<ul>
<li>널 구분자로 안전하게 삭제</li>
</ul>
<pre><code class="language-bash">cat list.txt | xargs -n 1 wget -q</code></pre>
<ul>
<li>URL 목록을 한 줄씩 wget 실행</li>
</ul>
<hr>
<h2 id="43-man--매뉴얼-페이지-보기-manual">43. <strong>man</strong> — 매뉴얼 페이지 보기 Manual</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">man [섹션] 명령</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-k 키워드 검색(apropos)</p>
</li>
<li><p>-f 간단 설명(whatis)</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">man grep</code></pre>
<ul>
<li>grep의 공식 매뉴얼 페이지 열기</li>
</ul>
<pre><code class="language-bash">man 5 crontab</code></pre>
<ul>
<li>섹션 5 포맷 문서(crontab 파일 형식) 보기</li>
</ul>
<pre><code class="language-bash">man -k archive</code></pre>
<ul>
<li>아카이브 관련 명령 검색</li>
</ul>
<hr>
<h2 id="44-alias--명령-별칭-설정">44. <strong>alias</strong> — 명령 별칭 설정</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">alias 이름=&#39;명령&#39;</code></pre>
<p><strong>예시</strong></p>
<pre><code class="language-bash">alias ll=&#39;ls -alF&#39;</code></pre>
<ul>
<li>ll 입력만으로 상세/형식 표시 목록</li>
</ul>
<pre><code class="language-bash">alias gs=&#39;git status&#39;</code></pre>
<ul>
<li>git status를 gs로 단축</li>
</ul>
<pre><code class="language-bash">alias rm=&#39;rm -i&#39;</code></pre>
<ul>
<li>rm 사용 시 항상 확인 받기</li>
</ul>
<hr>
<h2 id="45-unalias--별칭-해제">45. <strong>unalias</strong> — 별칭 해제</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">unalias [옵션] 이름</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li>-a 모든 별칭 제거</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">unalias ll</code></pre>
<ul>
<li>ll 별칭 제거</li>
</ul>
<pre><code class="language-bash">unalias -a</code></pre>
<ul>
<li>현재 셸의 모든 별칭 제거</li>
</ul>
<pre><code class="language-bash">unalias gs || true</code></pre>
<ul>
<li>없어도 에러 무시하고 진행</li>
</ul>
<hr>
<h2 id="46-history--명령-기록-조회관리">46. <strong>history</strong> — 명령 기록 조회/관리</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">history [옵션]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-c 지우기</p>
</li>
<li><p>-d N 특정 항목 삭제</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">history | tail</code></pre>
<ul>
<li>최근 실행한 명령 몇 줄 확인</li>
</ul>
<pre><code class="language-bash">history -d 100</code></pre>
<ul>
<li>100번째 기록 삭제</li>
</ul>
<pre><code class="language-bash">history -c</code></pre>
<ul>
<li>전체 히스토리 초기화</li>
</ul>
<hr>
<h2 id="47-env--환경-변수-조회실행-환경-지정">47. <strong>env</strong> — 환경 변수 조회/실행 환경 지정</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">env [변수=값]... [명령]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li>-i 빈 환경으로 실행</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">env | sort</code></pre>
<ul>
<li>현재 환경 변수 목록 표시</li>
</ul>
<pre><code class="language-bash">env PATH=/custom/bin:$PATH mycmd</code></pre>
<ul>
<li>일시적으로 PATH를 바꿔 실행</li>
</ul>
<pre><code class="language-bash">env -i sh -c &#39;echo $PATH&#39;</code></pre>
<ul>
<li>빈 환경으로 셸을 실행</li>
</ul>
<hr>
<h2 id="48-export--환경-변수-내보내기자식-프로세스에-전달">48. <strong>export</strong> — 환경 변수 내보내기(자식 프로세스에 전달)</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">export 변수=값</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li>-p 현재 내보낸 변수 표시</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">export JAVA_HOME=/opt/jdk</code></pre>
<ul>
<li>JAVA_HOME 환경 변수 설정</li>
</ul>
<pre><code class="language-bash">export PATH=$HOME/bin:$PATH</code></pre>
<ul>
<li>사용자 bin을 PATH 앞에 추가</li>
</ul>
<pre><code class="language-bash">export -p | grep JAVA_HOME</code></pre>
<ul>
<li>내보낸 변수 목록에서 JAVA_HOME 확인</li>
</ul>
<hr>
<h2 id="49-echo--문자열-출력">49. <strong>echo</strong> — 문자열 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">echo [옵션] 문자열</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-n 끝의 개행 생략</p>
</li>
<li><p>-e 백슬래시 이스케이프 해석</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">echo Hello</code></pre>
<ul>
<li>Hello 출력 후 개행</li>
</ul>
<pre><code class="language-bash">echo -n &#39;No newline&#39;</code></pre>
<ul>
<li>개행 없이 출력</li>
</ul>
<pre><code class="language-bash">echo -e &#39;A\nB&#39;</code></pre>
<ul>
<li>이스케이프를 해석해 줄바꿈 포함 출력</li>
</ul>
<hr>
<h2 id="50-printf--포맷-지정-출력">50. <strong>printf</strong> — 포맷 지정 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">printf 포맷 [인수...]</code></pre>
<p><strong>예시</strong></p>
<pre><code class="language-bash">printf &#39;%s %d\n&#39; user 3</code></pre>
<ul>
<li>문자열과 정수를 형식에 맞게 출력</li>
</ul>
<pre><code class="language-bash">printf &#39;%.2f\n&#39; 3.14159</code></pre>
<ul>
<li>소수점 둘째 자리까지 반올림 출력</li>
</ul>
<pre><code class="language-bash">printf &#39;%-10s | %5d\n&#39; name 42</code></pre>
<ul>
<li>좌/우 정렬 폭 지정 출력</li>
</ul>
<hr>
<h2 id="51-date--시각-표시설정">51. <strong>date</strong> — 시각 표시/설정</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">date [옵션] [+포맷]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-u UTC 기준</p>
</li>
<li><p>-d 입력 시각 해석</p>
</li>
<li><p>-s 시스템 시각 설정</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">date &#39;+%Y-%m-%d %H:%M:%S&#39;</code></pre>
<ul>
<li>지정 형식으로 현재 시각 출력</li>
</ul>
<pre><code class="language-bash">date -u</code></pre>
<ul>
<li>UTC 기준 현재 시각 출력</li>
</ul>
<pre><code class="language-bash">date -d &#39;2025-01-01 12:00&#39; &#39;+%s&#39;</code></pre>
<ul>
<li>해당 시각의 epoch 초 계산</li>
</ul>
<hr>
<h2 id="52-cal--달력-출력">52. <strong>cal</strong> — 달력 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">cal [월] [년]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-y 전체 연도 달력</p>
</li>
<li><p>-3 이전/다음 달 포함</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">cal</code></pre>
<ul>
<li>현재 달의 달력 출력</li>
</ul>
<pre><code class="language-bash">cal 12 2025</code></pre>
<ul>
<li>2025년 12월 달력 출력</li>
</ul>
<pre><code class="language-bash">cal -y 2026</code></pre>
<ul>
<li>2026년 1~12월 달력 출력</li>
</ul>
<hr>
<h2 id="53-uname--시스템-정보-출력">53. <strong>uname</strong> — 시스템 정보 출력</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">uname [옵션]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-a 모든 정보</p>
</li>
<li><p>-r 커널 릴리스</p>
</li>
<li><p>-m 머신 하드웨어</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">uname -a</code></pre>
<ul>
<li>커널/호스트/아키텍처 등 전체 정보 표시</li>
</ul>
<pre><code class="language-bash">uname -r</code></pre>
<ul>
<li>커널 버전 표시</li>
</ul>
<pre><code class="language-bash">uname -m</code></pre>
<ul>
<li>머신 아키텍처(x86_64 등) 표시</li>
</ul>
<hr>
<h2 id="54-hostname--호스트명-조회설정">54. <strong>hostname</strong> — 호스트명 조회/설정</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">hostname [옵션] [이름]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-I IP 주소들</p>
</li>
<li><p>-f FQDN 전체 도메인명</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">hostname</code></pre>
<ul>
<li>현재 호스트명 출력</li>
</ul>
<pre><code class="language-bash">hostname -I</code></pre>
<ul>
<li>호스트에 할당된 IP 리스트 출력</li>
</ul>
<pre><code class="language-bash">sudo hostname new-host</code></pre>
<ul>
<li>호스트명을 일시적으로 변경</li>
</ul>
<hr>
<h2 id="55-ping--네트워크-연결-확인-icmp-echo">55. <strong>ping</strong> — 네트워크 연결 확인 ICMP Echo</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">ping [옵션] 대상</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-c 횟수 지정</p>
</li>
<li><p>-i 간격</p>
</li>
<li><p>-W 타임아웃</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">ping -c 4 8.8.8.8</code></pre>
<ul>
<li>구글 DNS에 4회 패킷 전송 테스트</li>
</ul>
<pre><code class="language-bash">ping -c 3 example.com</code></pre>
<ul>
<li>도메인 이름으로 연결 확인</li>
</ul>
<pre><code class="language-bash">ping -i 0.2 -c 5 1.1.1.1</code></pre>
<ul>
<li>간격 0.2초로 5회 빠르게 테스트</li>
</ul>
<hr>
<h2 id="56-curl--url로-데이터-전송다운로드-client-url">56. <strong>curl</strong> — URL로 데이터 전송/다운로드 Client URL</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">curl [옵션] URL</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-L 리다이렉트 따라가기</p>
</li>
<li><p>-o 파일로 저장</p>
</li>
<li><p>-I 헤더만 요청</p>
</li>
<li><p>-d 데이터 POST</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">curl https://example.com</code></pre>
<ul>
<li>HTML을 표준출력으로 가져오기</li>
</ul>
<pre><code class="language-bash">curl -L -o page.html http://example.com</code></pre>
<ul>
<li>리다이렉트 따라가 파일 저장</li>
</ul>
<pre><code class="language-bash">curl -X POST -d &#39;a=1&amp;b=2&#39; https://httpbin.org/post</code></pre>
<ul>
<li>폼 데이터를 POST</li>
</ul>
<hr>
<h2 id="57-wget--비대화식-네트워크-다운로드-world-get">57. <strong>wget</strong> — 비대화식 네트워크 다운로드 World GET</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">wget [옵션] URL</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-O 파일명 지정</p>
</li>
<li><p>-c 이어받기</p>
</li>
<li><p>-r 재귀 다운로드</p>
</li>
<li><p>--no-check-certificate 인증서 무시</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">wget https://example.com/file.zip</code></pre>
<ul>
<li>현재 디렉터리에 파일 저장</li>
</ul>
<pre><code class="language-bash">wget -O latest.html https://example.com</code></pre>
<ul>
<li>파일명을 지정해 저장</li>
</ul>
<pre><code class="language-bash">wget -c big.iso</code></pre>
<ul>
<li>중단된 다운로드를 이어서 받기</li>
</ul>
<hr>
<h2 id="58-ssh--원격-셸-접속-secure-shell">58. <strong>ssh</strong> — 원격 셸 접속 Secure Shell</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">ssh [옵션] 사용자@호스트 [명령]</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-p 포트 지정</p>
</li>
<li><p>-i 개인키 지정</p>
</li>
<li><p>-L 로컬 포워딩</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">ssh user@server</code></pre>
<ul>
<li>원격 서버에 셸 접속</li>
</ul>
<pre><code class="language-bash">ssh -i ~/.ssh/id_rsa user@server</code></pre>
<ul>
<li>특정 키로 인증하여 접속</li>
</ul>
<pre><code class="language-bash">ssh -L 8080:localhost:80 user@server</code></pre>
<ul>
<li>로컬 8080을 원격 80으로 포워딩</li>
</ul>
<hr>
<h2 id="59-scp--ssh-기반-파일-복사-secure-copy">59. <strong>scp</strong> — SSH 기반 파일 복사 Secure Copy</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">scp [옵션] 원본 대상</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-P 포트</p>
</li>
<li><p>-i 키 파일</p>
</li>
<li><p>-r 디렉터리 재귀</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">scp file.txt user@server:/tmp/</code></pre>
<ul>
<li>로컬 파일을 원격 /tmp로 업로드</li>
</ul>
<pre><code class="language-bash">scp -r site/ user@server:/var/www/</code></pre>
<ul>
<li>디렉터리를 재귀 업로드</li>
</ul>
<pre><code class="language-bash">scp -P 2222 user@server:/var/log/syslog .</code></pre>
<ul>
<li>특정 포트로 원격 파일 다운로드</li>
</ul>
<hr>
<h2 id="60-sudo--권한-상승하여-명령-실행-superuser-do">60. <strong>sudo</strong> — 권한 상승하여 명령 실행 Superuser Do</h2>
<p><strong>형식</strong></p>
<pre><code class="language-bash">sudo [옵션] 명령</code></pre>
<p><strong>주요 옵션</strong></p>
<ul>
<li><p>-v 자격 갱신</p>
</li>
<li><p>-k 자격 무효화</p>
</li>
<li><p>-u 사용자 지정</p>
</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-bash">sudo apt update</code></pre>
<ul>
<li>관리자 권한이 필요한 패키지 인덱스 갱신</li>
</ul>
<pre><code class="language-bash">sudo -u www-data ls /var/www</code></pre>
<ul>
<li>다른 사용자 권한으로 명령 실행</li>
</ul>
<pre><code class="language-bash">sudo -k &amp;&amp; sudo whoami</code></pre>
<ul>
<li>캐시 무효화 후 다시 인증 요구</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IPP] 팀 배정 & 자리 세팅]]></title>
            <link>https://velog.io/@choi-hyk/IPP-%ED%8C%80-%EB%B0%B0%EC%A0%95-%EC%9E%90%EB%A6%AC-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@choi-hyk/IPP-%ED%8C%80-%EB%B0%B0%EC%A0%95-%EC%9E%90%EB%A6%AC-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Sat, 09 Aug 2025 14:52:40 GMT</pubDate>
            <description><![CDATA[<h2 id="😁팀-배정">😁팀 배정</h2>
<p>일주일 동안 진행된 OJT 기간이 끝나고, 드디어 팀 선택을 통해 최종 팀 배정을 받는 시간이 찾아왔다. 팀 배정 방식은 본인이 원하는 팀과 각 팀장님이 원하는 인원을 서로 조율하여 결정하는 방식이었다. 즉, 내가 지원한 팀에서 나를 필요로 한다면, 높은 확률로 해당 팀에 배정되는 구조다.</p>
<p>다행히 나는 내가 가장 가고 싶었던 팀에 합류할 수 있었다. 내가 합류한 팀은 회사 제품에 새로운 기능을 더하기 위해 LLM을 파인튜닝하고 확장하는 업무를 주로 담당한다. 아직 나는 AI와 관련된 지식이 부족한 편이지만, 이번 기회를 계기로 열심히 학습하여 팀에 도움이 되고 싶다. 내가 원하는 팀에 합류했다는 점이 큰 동기부여가 되었고, 앞으로 최선을 다해 기여하겠다는 다짐을 하게 됐다.</p>
<hr>
<h2 id="🖥️자리-세팅">🖥️자리 세팅</h2>
<p>팀 배정이 끝난 후, 이제 본격적으로 자리를 세팅하는 시간이 왔다. 우연히도 내가 지난 1~2월 현장실습 때 배정받았던 자리 근처에 다시 자리를 배정받게 됐다. 그런데 한 가지 차이가 있었다. 그때는 Windows 운영체제를 사용했지만, 이번 팀에서는 Ubuntu 환경에서 작업한다고 했다.</p>
<p>그래서 우분투 USB 설치 파일을 준비해 직접 설치를 진행했다. 사실 리눅스를 완전히 처음 써보는 것은 아니었다. WSL(Windows Subsystem for Linux)이나 AWS EC2 환경에서 간단히 다뤄본 경험이 있고, 학교 실습 시간에 가상머신으로 우분투를 구동해본 적도 있었다. 하지만 그 외의 경험은 거의 없어서, 앞으로는 기본적인 명령어와 리눅스 환경에서의 작업 흐름을 좀 더 익혀야겠다고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GitHub Pages] GitHub Page 구현하기 with Cursor AI]]></title>
            <link>https://velog.io/@choi-hyk/GitHub-Pages-GitHub-Page-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Cursor-AI</link>
            <guid>https://velog.io/@choi-hyk/GitHub-Pages-GitHub-Page-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Cursor-AI</guid>
            <pubDate>Mon, 04 Aug 2025 13:11:10 GMT</pubDate>
            <description><![CDATA[<h2 id="🛠️-github-page-구현하기-with-cursor-ai">🛠️ GitHub Page 구현하기 with Cursor AI</h2>
<p>이번 시간에는 최종적으로 GitHub Page 구현을 마치려고 한다. 그런데 역시 끈기가 부족해서 구현하는 게 상당히 귀찮게 느껴졌다. 특히 CSS로 렌더링을 구현하는 게 짜증이 났다. 그래서 이번에는 온전히 Cursor AI를 통해 프로젝트를 분석하고 적절한 코드를 생성하려고 한다.</p>
<hr>
<h3 id="vibe-coding">Vibe Coding</h3>
<p>올해 가장 핫한 개발 트렌드는 <strong>Vibe Coding</strong>일 것이다. 학부생 수준의 개발자들은 AI를 적극적으로 활용해 개발하겠지만, 사용하면서 양심의 가책을 느낄 수도 있다(나만 그런가?). 현직 개발자들의 이야기를 들어보면, 이제 AI를 적극 활용해 업무 효율을 극대화하는 것이 매우 중요하다고 한다. 물론 나도 인턴십 경험이 겨우 2개월이라 실질적인 현업 경험은 부족하지만, 이번에 Cursor AI로 Vibe Coding과 유사한 작업물을 만들어 보려고 한다.</p>
<hr>
<h3 id="cursor-ai">Cursor AI</h3>
<p>나는 주로 <strong>VS Code IDE</strong>를 사용한다. Cursor AI는 완전히 VS Code 위에서 작동하는 AI 통합 개발 환경으로, LLM(대규모 언어 모델, Large Language Model)을 탑재했다. VS Code의 Copilot과 달리 전체 프로젝트를 분석해 더 수준 높은 코드를 생성할 수 있다.</p>
<p>Cursor AI는 다음과 같은 모델을 지원한다:</p>
<ul>
<li><p><strong>OpenAI</strong></p>
<ul>
<li>o3-pro (GPT-3.5 Pro)</li>
<li>GPT-4.1 (o4)</li>
<li>GPT-4 Turbo</li>
</ul>
</li>
<li><p><strong>Google</strong></p>
<ul>
<li>Gemini 2.5 Pro</li>
</ul>
</li>
<li><p><strong>Anthropic</strong></p>
<ul>
<li>Claude Sonnet 4</li>
<li>Claude Opus 4</li>
</ul>
</li>
<li><p><strong>xAI</strong></p>
<ul>
<li>Grok 3 Beta</li>
</ul>
</li>
<li><p><strong>DeepSeek</strong></p>
<ul>
<li>DeepSeek V3.1</li>
</ul>
</li>
<li><p><strong>Cursor 자체 모델</strong></p>
<ul>
<li>cursor-small (경량화 버전)</li>
</ul>
</li>
</ul>
<p>사용자는 자신이 보유한 API 키로 원하는 모델을 지정할 수 있다. 나는 무료 플랜에서 <strong>GPT-3.5</strong> 모델을 사용했다. Cursor AI의 강점은 전체 프로젝트 단위에서 자연어 프롬프트만으로 원하는 결과물을 얻을 수 있다는 점이다.</p>
<hr>
<h3 id="구현-과정">구현 과정</h3>
<p><img src="https://github.com/user-attachments/assets/1e40c8b7-6427-48db-abae-ba7a3e6d9adb" alt="Cursor AI 프롬프트 예시"></p>
<p>위와 같이 대략적인 프롬프트만 작성해도, 숙련된 사용자가 아니어도 매우 자연스럽고 정확한 코드를 생성해 준다. 나는 이미 구현해 둔 <code>Velog.tsx</code>의 스타일과 컴포넌트를 기준으로 요청하자, 거의 동일한 스타일로 코드를 받아낼 수 있었다.</p>
<hr>
<h3 id="최종-결과">최종 결과</h3>
<p><img src="https://github.com/user-attachments/assets/da280393-2c6f-4bed-8bb2-fb21b6ad0622" alt="최종 GitHub Page 화면"></p>
<p>프로젝트 내 전역 색상, 보더, 컴포넌트 스타일을 유사하게 구현한 결과를 확인할 수 있다. 앞으로는 이런 CSS 디자인 작업을 Cursor AI에 전적으로 맡길 생각이다.</p>
<p>다음 시간에는 GitHub Pages 중간 점검을 해 보고 잠시 쉬어갈 예정이다. 이후에는 <strong>GitHub Codespaces</strong> 서버와 개인 톡비서 같은 AI 챗봇 시스템을 구현할 계획이다. 그다음 프로젝트로는 AI와 <strong>Godot 엔진</strong>을 결합한 간단한 게임 개발을 생각 중이며, 틈틈이 AI 공부도 진행해 정리할 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>