<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>twonezero_log</title>
        <link>https://velog.io/</link>
        <description>I Enjoy Learn-and-Run Vibe😊</description>
        <lastBuildDate>Mon, 23 Feb 2026 05:20:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>twonezero_log</title>
            <url>https://velog.velcdn.com/images/twonezero_98/profile/3e46ad92-ccd1-4031-b9ea-29b05abda8e7/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. twonezero_log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/twonezero_98" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[langchain 문서 요약 정리]]></title>
            <link>https://velog.io/@twonezero_98/langchain-%EB%AC%B8%EC%84%9C-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@twonezero_98/langchain-%EB%AC%B8%EC%84%9C-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 23 Feb 2026 05:20:26 GMT</pubDate>
            <description><![CDATA[<h2 id="textsplitter">TextSplitter</h2>
<ol>
<li><strong>RecursiveCharacterTextSplitter</strong></li>
<li><strong>TokenTextSplitter</strong></li>
</ol>
<h3 id="📊-recursivecharactertextsplitter-vs-tokentextsplitter">📊 <code>RecursiveCharacterTextSplitter</code> vs <code>TokenTextSplitter</code></h3>
<table>
<thead>
<tr>
<th>특징 / 동작</th>
<th><code>RecursiveCharacterTextSplitter</code> (일반)</th>
<th><code>TokenTextSplitter</code></th>
</tr>
</thead>
<tbody><tr>
<td><strong>측정 단위</strong></td>
<td>문자(Character)</td>
<td>토큰(Token)</td>
</tr>
<tr>
<td><strong>의미적 분할</strong></td>
<td>✅ 예 – 단락, 문장, 단어 순으로 재귀적으로 시도</td>
<td>❌ 아니오 – 토큰 수에 기반한 강제 분할</td>
</tr>
<tr>
<td><strong>토크나이저 인식</strong></td>
<td>❌ 아니오 – 토큰 제한을 고려하지 않음</td>
<td>✅ 예 – 토큰 수 기반 분할 (<code>tiktoken</code> 통해)</td>
</tr>
<tr>
<td><strong>문맥 보존 (중첩)</strong></td>
<td>✅ 지원됨</td>
<td>✅ 지원됨</td>
</tr>
<tr>
<td><strong>분할 대체 전략</strong></td>
<td>시도: <code>\\n\\n</code>, <code>\\n</code>, <code>&quot; &quot;</code>, 그 다음 문자</td>
<td>없음 – N토큰마다 고정 분할</td>
</tr>
<tr>
<td><strong>일반적인 사용 사례</strong></td>
<td>토큰 이전 LLM 입력 준비, 작은 모델용 청킹</td>
<td>토큰 제한이 엄격한 LLM (예: OpenAI, Claude)</td>
</tr>
<tr>
<td><strong>크기 제어 정확도</strong></td>
<td>❌ 근사치 (문자 수만)</td>
<td>✅ 정확함 (토크나이저 기반)</td>
</tr>
<tr>
<td><strong>토큰 중간 분할 위험</strong></td>
<td>N/A – 토큰 사용하지 않음</td>
<td>❌ 가능 – 분할이 문장 논리를 깨뜨릴 수 있음</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>⚡ 빠름 (토큰화 단계 없음)</td>
<td>토큰화로 인해 약간 느림</td>
</tr>
<tr>
<td><strong>청크 크기 일관성</strong></td>
<td>✅ 문자 길이 기반 일관적</td>
<td>✅ 토큰 길이 기반 일관적</td>
</tr>
<tr>
<td><strong>최적 용도</strong></td>
<td>대략적 분할, 짧은 텍스트, 프로토타이핑</td>
<td>토큰 정확 분할, 프로덕션 LLM 워크플로우</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-요약-권장사항">✅ 요약 권장사항</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>사용할 Splitter</th>
</tr>
</thead>
<tbody><tr>
<td><strong>자연어 경계</strong>가 토큰보다 중요한 경우</td>
<td><code>RecursiveCharacterTextSplitter</code></td>
</tr>
<tr>
<td><strong>GPT 같은 토큰 제한 모델</strong>로 작업하는 경우</td>
<td><code>TokenTextSplitter</code></td>
</tr>
<tr>
<td><strong>정밀한 토큰 제어</strong>가 필요한 경우 (예: 청크당 최대 500토큰)</td>
<td><code>TokenTextSplitter</code></td>
</tr>
<tr>
<td><strong>LLM 토큰 제약 없는 일반 문서</strong>를 처리하는 경우</td>
<td><code>RecursiveCharacterTextSplitter</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="pypdfloader">PyPDFLoader</h2>
<ul>
<li>각 줄 끝에 <code>/n</code>을 읽어들임 (문장 중간이라도)</li>
<li>원본 문서의 포맷을 완전히 캡처하지 못함 (<code>/n/n</code> 없음). PDF의 경우 분할이 덜 정확함.</li>
<li>대부분의 PDF 리더는 시각적 줄바꿈을 실제 <code>\\n</code> 줄바꿈으로 읽어들이며, 이는 단순한 줄 래핑 때문일지라도 실제 문장 경계가 아닐 수 있음.</li>
<li>→ 더 큰 청크 크기와 중첩으로 이러한 영향을 완화할 수 있음</li>
</ul>
<hr>
<h2 id="map-reduce-chain-배치-처리">Map-Reduce-Chain (배치 처리)</h2>
<pre><code class="language-python">
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 1. PDF 문서 로드
pdf_path = &quot;파일 이름&quot;
loader = PyPDFLoader(pdf_path, mode = &quot;single&quot;)
doc = loader.load()
#full_text = doc[0].page_content

# 2. 의미 있는 청크로 분할
splitter = RecursiveCharacterTextSplitter(
    chunk_size=10000, # 문자 단위! ~ 2000-2500 토큰
    chunk_overlap=2000, # 10-20%
    separators=[&quot;\\n\\n&quot;, &quot;\\n&quot;, &quot;.&quot;, &quot;!&quot;, &quot;?&quot;, &quot; &quot;],  # 광범위 -&gt; 좁게
)
chunks = splitter.split_documents(doc)

# 3. 배치 API용 입력 준비
inputs = [{&quot;text&quot;: chunk.page_content} for chunk in chunks]

# 4. 청크 요약용 프롬프트 (MAP)
map_prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;You are a business anylyst and helpful assistant.&quot;),
    (&quot;human&quot;, &quot;Summarize the following text briefly in 3 bullet points:\\n\\n{text}\\n\\nSummary:&quot;)
])

# 5. 최종 요약용 프롬프트 (REDUCE)
reduce_prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;You are a business anylyst and helpful assistant.&quot;),
    (&quot;human&quot;, &quot;Summarize the following texts in a consistent final summary {reduce_style}:\\n\\n{text}\\n\\nFinal Summary:&quot;)
])

# 6. LLM
llm = ChatOpenAI(
    model=&quot;gpt-4o-mini&quot;,
    temperature=0
)

# 7. 파서
parser = StrOutputParser()

# 8. 두 개의 별도 체인
map_chain = map_prompt | llm | parser
reduce_chain = reduce_prompt | llm | parser

# 9. MAP 단계: 각 청크를 *병렬로* 요약
summaries = map_chain.batch(
    inputs, # 청크 목록
    config={&quot;max_concurrency&quot;: 4},   # AI API Rate limits 고려 필요
)

# 10. REDUCE 단계: 청크 요약들로부터 최종 요약
final_summary = reduce_chain.invoke({&quot;reduce_style&quot;:&quot;in a very detailed, structured and comprehensive manner while dropping duplicated info&quot;,
                                     &quot;text&quot;: &quot;\\n\\n&quot;.join(summaries)})

# 11. 최종 출력
print(&quot;\\n📄 Summary:\\n&quot;, final_summary)
</code></pre>
<hr>
<h2 id="refine-chain-반복적-업데이트">Refine-Chain (반복적 업데이트)</h2>
<pre><code class="language-python">
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 1. PDF 문서 로드
pdf_path = &quot;파일 이름&quot;
loader = PyPDFLoader(pdf_path, mode = &quot;single&quot;)
doc = loader.load()

# 2. 의미 있는 청크로 분할
splitter = RecursiveCharacterTextSplitter(
    chunk_size=20000, # 문자 단위! ~ 4000-5000 토큰
    chunk_overlap=2000, # 10-20%
    separators=[&quot;\\n\\n&quot;, &quot;\\n&quot;, &quot;.&quot;, &quot;!&quot;, &quot;?&quot;, &quot; &quot;],  # 광범위 -&gt; 좁게
)
chunks = splitter.split_documents(doc)

# 3. 첫 번째 청크 프롬프트
question_prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;You are a business anylyst and helpful assistant. Do not speculate or invent facts; rely only on the provided text.&quot;),
    (&quot;human&quot;,
     &quot;Create an initial brief summary of the following passage. Only include information supported by the text.&quot;
     &quot;The Summary should contain 3-5 bullet points. \\n\\n&quot;
     &quot;Text:\\n{text}\\n\\n&quot;
     &quot;Initial summary:&quot;)
])

# 4. 개선 프롬프트 (REFINE)
refine_prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;You are a business anylyst and helpful assistant. Do not speculate or invent facts; rely only on the provided text.&quot;),
    (&quot;human&quot;,
     &quot;We have an existing summary that must remain consistent and high-quality:\\n&quot;
     &quot;{existing_answer}\\n\\n&quot;
     &quot;Incorporate the following new passage:\\n&quot;
     &quot;{text}\\n\\n&quot;
     &quot;Update rules:\\n&quot;
     &quot;If this passage is irrelevant to the current summary or fully redundant, leave the summary unchanged. Otherwise:\\n&quot;
     &quot;1) Crtically review the entire summary and fully rewrite the summary.\\n&quot;
     &quot;2) Use 5-12 Topic Headers (main topics!) and keep the structure consistent.\\n&quot;
     &quot;3) Add only new, material information; remove duplicates and redundancy.\\n&quot;
     &quot;4) Remove info from the previous summary that becomes more and more niche or irrelevant in the updated total context.\\n&quot;
     &quot;5) If the new text contradicts or clarifies earlier statements, correct the summary.\\n&quot;
     &quot;6) The summary length should not execeed the length of a typical executive summary. No irrelevant details.\\n&quot;
     &quot;7) Prefer concise wording; replace rather than append where possible.\\n\\n&quot;
     &quot;Return only the refined summary (no commentary):&quot;)
])

# 5. LLM
llm = ChatOpenAI(
    model=&quot;gpt-4o-mini&quot;,
    temperature=0
)

# 6. 파서
parser = StrOutputParser()

# 7. 2개 체인 구성
question_chain = question_prompt | llm | parser   # 첫 번째 청크
refine_chain    = refine_prompt   | llm | parser  # 이후 청크들

# 8. 첫 번째 청크에 대한 초기 요약
current_summary = question_chain.invoke({&quot;text&quot;: chunks[0].page_content})
intermediate_summaries = [] # 중간 요약 저장
intermediate_summaries.append(current_summary)

# 9. 반복적 개선
for chunk in chunks[1:]:
    current_summary = refine_chain.invoke({
        &quot;existing_answer&quot;: current_summary,
        &quot;text&quot;:            chunk.page_content
    })
    intermediate_summaries.append(current_summary)

# 10. 최종 요약 출력
print(current_summary)
</code></pre>
<hr>
<h2 id="요약-전략-정리">요약 전략 정리</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>동작 방식</th>
<th>사용 시기</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stuff</strong></td>
<td>모든 텍스트를 연결해서 하나의 프롬프트로 전송</td>
<td>입력이 컨텍스트에 맞고 전체적 일관성이 필요할 때</td>
<td>간단, 빠름, 최고의 전체 인식</td>
<td>컨텍스트 제한 엄격; 병렬 처리 불가</td>
</tr>
<tr>
<td><strong>Map-Reduce</strong></td>
<td>청크들을 요약(&quot;map&quot;), 그 다음 요약들을 병합(&quot;reduce&quot;)</td>
<td>입력이 길거나 많을 때; 청크들이 독립적으로 이해될 때</td>
<td>대규모 말뭉치로 확장 가능; 병렬 map; 프롬프트 튜닝 가능</td>
<td>호출/비용 증가; 일부 청크 간 뉘앙스 손실</td>
</tr>
<tr>
<td><strong>Refine</strong></td>
<td>초기 요약을 만든 다음, 각 청크로 반복적으로 업데이트</td>
<td>순차적 의존성이 중요할 때; 업데이트가 문맥/수정사항을 전달해야 할 때</td>
<td>청크 간 강력한 일관성; 결정적 범위</td>
<td>순차적 (map 병렬화 불가); 더 느림; 순서 민감</td>
</tr>
</tbody></table>
<h2 id="실용-휴리스틱스">실용 휴리스틱스</h2>
<ul>
<li><strong>청킹:</strong> 1~2k 토큰에 10–15% 중첩이 좋은 시작점</li>
<li><strong>일관성:</strong> 초기 및 refine/reduce 단계에서 <strong>동일한 출력 형식</strong> 유지 (예: 고정 제목이나 고정 불렛 개수)</li>
<li><strong>가드레일:</strong> <strong>간결한 표현</strong> 요청, &quot;<strong>추가보다는 교체</strong>&quot;, 그리고 &quot;<strong>지원되는 사실만 포함</strong>&quot; 요청. 엄격한 구조가 필요할 때 JSON + 파서 고려</li>
<li><strong>병렬화:</strong> <strong>map</strong> 단계만 병렬화. Rate limit에 맞는 보수적인 <code>max_concurrency</code> 사용</li>
<li><strong>비용/시간:</strong> <code>stuff</code> &lt; <code>map_reduce</code> &lt; <code>refine</code> (일반적으로). 청크 크기 최적화, map 출력을 짧게 유지, 불필요한 재작성 피하기</li>
</ul>
<h2 id="경험적인-전략-시도">경험적인 전략 시도</h2>
<blockquote>
</blockquote>
<p>확실하지 않다면, 먼저 <strong>Map-Reduce</strong> 시도 (크기 + 속도에 좋은 기본값).
중요한 교차 섹션 의존성이 놓치고 있다면 <strong>Refine</strong>로 전환.
문서가 작다면 <strong>Stuff</strong>로 간단하게 유지.</p>
<blockquote>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAG 기본 정리]]></title>
            <link>https://velog.io/@twonezero_98/RAG</link>
            <guid>https://velog.io/@twonezero_98/RAG</guid>
            <pubDate>Mon, 23 Feb 2026 05:06:44 GMT</pubDate>
            <description><![CDATA[<h1 id="rag">RAG</h1>
<h2 id="기본적인-rag-코드">기본적인 RAG 코드</h2>
<pre><code class="language-python">
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS # NEW
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough


# 1. Loading a PDF doc
pdf_path = &quot;파일 이름&quot; 
loader = PyPDFLoader(pdf_path, mode = &quot;single&quot;)
doc = loader.load()                    

# 2. split into coherent chunks
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  
    chunk_overlap=100, 
    separators=[&quot;\n\n&quot;, &quot;\n&quot;, &quot;.&quot;, &quot;!&quot;, &quot;?&quot;, &quot; &quot;],  
)
chunks = splitter.split_documents(doc)

# 3. Embeddings + Vector‑Store
embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-small&quot;)
vectordb = FAISS.from_documents(chunks, embeddings)
retriever = vectordb.as_retriever(search_kwargs={&quot;k&quot;: 4})

# 4. LLM
llm = ChatOpenAI(
    model=&quot;gpt-4o-mini&quot;,
    temperature=0,
)


# 5. Prompt‑Template
qa_prompt = ChatPromptTemplate.from_messages([
    (&quot;system&quot;, &quot;You are a business analyst and helpful assistant.&quot;),
    (&quot;human&quot;, &quot;Answer the following question as accurate and diligent as possible.&quot;
              &quot;Do not speculate or invent facts, rely only on the provided text.&quot;
              &quot;\n\nContext:\n{context}\n\nQuestion: {question}\nAnswer:&quot;)
])



def format_docs(docs):
    return &quot;\n\n---\n\n&quot;.join(doc.page_content for doc in docs)


# LCEL
rag_chain = (
    {&quot;context&quot;: retriever | format_docs, &quot;question&quot;: RunnablePassthrough()}
    | qa_prompt
    | llm
    | StrOutputParser()
)


question = input(&quot;❓ Your Question for the Bot: &quot;)
result = rag_chain.invoke(question)

print(&quot;\nResponse:\n&quot;, result)
</code></pre>
<hr>
<p><strong>RAG가 필요한 이유</strong></p>
<ul>
<li><strong>Input 크기에 따라 확장 가능:</strong> 관련된 몇 개의 문단만 검색하므로, 위키/드라이브가 커지더라도 컨텍스트 윈도우 제한에 도달하지 않습니다.</li>
<li><strong>더 낮은 비용 및 지연 시간:</strong> 전체 문서를 매번 프롬프트에 붙여넣는 것보다 훨씬 적은 토큰을 사용합니다.</li>
<li><strong>더 높은 신호 대 잡음 비율:</strong> 모델이 짧고 관련성 높은 발췌문에 집중합니다. 답변을 인용과 함께 더 쉽게 근거를 제시할 수 있습니다.</li>
<li><strong>최신 정보 + 권한 인식:</strong> 문서가 변경되면 재인덱싱하고 사용자 권한으로 필터링할 수 있으며, 모델을 재학습할 필요가 없습니다.</li>
</ul>
<p><strong>RAG의 장단점</strong></p>
<ul>
<li><strong>더 많은 구성 요소:</strong> 수집, 청킹, 임베딩, 벡터 저장소/리랭커, 모니터링이 필요합니다.</li>
<li><strong>”검색 실패” 위험:</strong> 청킹이 부족하거나 검색기가 약하면 올바른 문단을 찾지 못할 수 있습니다 (튜닝/평가 필요).</li>
<li><strong>운영 작업:</strong> 접근 제어 매핑, 개인정보 처리, 품질 테스트가 지속적인 과제가 됩니다.</li>
</ul>
<p><strong>”모든 문서를 프롬프트에 넣는 방식”의 장점</strong></p>
<ul>
<li><strong>구현이 간단:</strong> 파이프라인이나 인프라가 필요 없습니다.</li>
<li><strong>검색 실패 없음:</strong> 프롬프트 컨텍스트에 있는 내용은 모델이 볼 수 있습니다.</li>
<li><strong>작고 고정된 컨텍스트에 적합:</strong> 예: 짧은 핸드북 또는 단일 정책 문서.</li>
</ul>
<p><strong>전체 문서를 프롬프트에 넣는 방식의 단점</strong></p>
<ul>
<li><strong>확장성 부족:</strong> 토큰 제한에 빠르게 도달하며 문서 크기에 비례해 비용이 증가합니다.</li>
<li><strong>주의력 희석:</strong> 크고 노이즈가 많은 컨텍스트는 답변 품질을 저하시킵니다.</li>
<li><strong>최소 권한 적용이 어려움:</strong> 모델/API에 과도한 콘텐츠를 공유할 수 있습니다.</li>
<li><strong>출처 추적이 약함:</strong> 명확한 소스 발췌문을 보여주기 어렵고, 감사가 복잡합니다.</li>
</ul>
<p><strong>일반적인 지침</strong></p>
<ul>
<li>Input 컨텍스트가 <strong>약 20~50페이지</strong>이고, 정기적으로 변경되며, <strong>사용자별 권한</strong>이 있는 경우 <strong>RAG</strong>를 사용하세요 (선택적으로 리랭킹 및 캐싱과 함께).</li>
<li><strong>작고 고정</strong>되어 있으며 <strong>신뢰 경계가 단순</strong>한 경우, <strong>컨텍스트 스터핑</strong>이 적합하고 시작하기에 가장 저렴합니다.</li>
</ul>
<hr>
<h3 id="chunk_size">chunk_size</h3>
<table>
<thead>
<tr>
<th><strong>chunk_size (토큰 ≈ 문자/4)</strong></th>
<th><strong>장점</strong></th>
<th><strong>단점</strong></th>
<th><strong>적합한 용도</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>&lt; 1,000 토큰</strong> (≤ ~4,000 문자)</td>
<td>매우 세분화됨; 특정 용어에 대한 높은 재현율</td>
<td>청크가 많음 → 더 많은 임베딩, 더 큰 인덱스, 더 높은 지연 시간</td>
<td>FAQ, 채팅 로그</td>
</tr>
<tr>
<td><strong>1,000–2,500 토큰</strong> (~4,000–10,000 문자)</td>
<td>견고한 균형: 충분한 컨텍스트, 과도하지 않음</td>
<td>여전히 긴 섹션을 분할할 수 있음</td>
<td>기술 문서, 블로그 게시물</td>
</tr>
<tr>
<td><strong>2,500–4,000 토큰</strong> (~10,000–16,000 문자)</td>
<td>섹션을 온전하게 유지 (더 적은 “고립된 정보”)</td>
<td>키워드가 가장자리에 있을 경우 재현율이 낮아질 수 있음</td>
<td>연간 보고서, 백서</td>
</tr>
<tr>
<td><strong>&gt; 4,000 토큰</strong> (&gt;~16,000 문자)</td>
<td>최소한의 분할; 더 적은 검색 호출</td>
<td>컨텍스트 예산을 초과할 위험; 청크당 더 많은 노이즈</td>
<td>매우 큰 컨텍스트 모델과 드문 검색의 경우에만</td>
</tr>
</tbody></table>
<p><strong>10K 컨텍스트(긴 문서, 포멀하고 섹션이 구분된 구조)</strong>: chunk 크기의 시작점으로는 약 1,800–2,400 tokens가 적절함</p>
<h3 id="chunk_overlap">chunk_overlap</h3>
<table>
<thead>
<tr>
<th><strong>overlap</strong></th>
<th><strong>사용 이유</strong></th>
<th><strong>장단점</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>0–5%</strong></td>
<td>문단이 깔끔하게 구분될 때 빠르고 저렴함</td>
<td>문장/테이블이 경계를 넘어 잘릴 위험</td>
</tr>
<tr>
<td><strong>10–20%</strong> (일반적)</td>
<td>제목, 각주, 테이블 간의 흐름을 유지</td>
<td>더 많은 저장소 및 임베딩 비용</td>
</tr>
<tr>
<td><strong>&gt; 25%</strong></td>
<td>상호 참조가 많은 계약서/코드</td>
<td>중복이 많음; 더 느리고 비쌈</td>
</tr>
</tbody></table>
<p><strong>긴 보고서의 경우:</strong> 약 <strong>15% 중복</strong> (또는 약 <strong>200 토큰</strong>)을 목표로 합니다.
<strong>20%</strong>도 괜찮지만, 일반적으로 컨텍스트를 잃지 않고 <strong>약 15%</strong>로 낮출 수 있습니다.</p>
<hr>
<h3 id="큰-청크-vs-작은-청크의-장단점">큰 청크 vs 작은 청크의 장단점</h3>
<table>
<thead>
<tr>
<th></th>
<th><strong>더 큰 <code>chunk_size</code>, 더 작은 중복</strong></th>
<th><strong>더 작은 <code>chunk_size</code>, 더 큰 중복</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점</strong></td>
<td>더 적은 검색 호출; 더 자체 포함적인 컨텍스트; LLM이 더 적은 연결 작업 필요</td>
<td>좁은 쿼리에서 더 높은 재현율; 더 정확한 인용</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>청크당 더 많은 “노이즈”; 중요한 세부 정보가 묻힐 수 있음</td>
<td>더 높은 비용/지연 시간; 모델이 여러 청크를 함께 엮어야 함 (일관성 위험)</td>
</tr>
</tbody></table>
<p><strong>핵심 요약:</strong> 10K의 경우, 기준으로 청크당 약 <strong>8,000~10,000 문자</strong>에 약 <strong>15% 중복</strong>으로 시도한 다음, 평가 점수와 프롬프트 비용에 따라 조정하세요.</p>
<hr>
<h2 id="retrieving-여러-방식">Retrieving 여러 방식</h2>
<ol>
<li>기본적인 유사도 검색<blockquote>
<p><code>top_docs = vectordb.similarity_search(query, k=4)</code>
or</p>
<pre><code class="language-python">retriever = vectordb.as_retriever(
   search_type=&quot;similarity&quot;,          
   search_kwargs={&quot;k&quot;: 4}             
)
top_docs = retriever.invoke(query)</code></pre>
</blockquote>
</li>
</ol>
<ol start="2">
<li><p>유사도 점수 임계값 포함</p>
<blockquote>
<pre><code class="language-python">
retriever = vectordb.as_retriever(
   search_type=&quot;similarity_score_threshold&quot;,
   search_kwargs={
       &quot;score_threshold&quot;: 0.4,
       &quot;k&quot;: 4 
   }
)</code></pre>
</blockquote>
</li>
<li><p>MMR(Max Marginal Relevance) Search</p>
<blockquote>
<ol>
<li>쿼리와 <strong>유사도가 높은 상위 z개 후보 문서(청크)</strong>를 넓게 가져온&gt;다.</li>
<li>이후 유사도(similarity)와 다양성(diversity)를 함께 고려해, 중&gt;복을 줄이면서 최종 k개를 재선정한다.</li>
<li>lambda_mult (0~1) 값으로 <strong>유사도 중심(1에 가까움) ↔ 다양성 &gt;중심(0에 가까움)</strong>의 균형을 조절한다.</li>
</ol>
<pre><code class="language-python">
retriever = vectordb.as_retriever(
   search_type=&quot;mmr&quot;,                   
   search_kwargs={&quot;k&quot;: 4,
                  &quot;fetch_k&quot;: 20,
                  &quot;lambda_mult&quot;: 0.5}  
)</code></pre>
</blockquote>
</li>
</ol>
<hr>
<h3 id="요약">요약</h3>
<table>
<thead>
<tr>
<th>검색 유형</th>
<th>작동 방식</th>
<th>반환 결과</th>
<th>후보 풀</th>
<th>주요 파라미터 (일반적)</th>
<th>장점 / 사용 시기</th>
<th>주의 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>similarity</strong></td>
<td>벡터 유사도에 따른 상위 <em>k</em>개의 최근접 이웃.</td>
<td>정확히 <em>k</em>개의 <code>Document</code> (리트리버를 통한 점수 없음).</td>
<td>일반적으로 <em>k</em>개 (추가 풀 없음).</td>
<td><code>k</code> (예: 4), 선택적 <code>filter</code>.</td>
<td>간단하고 빠른 기준선; 가장 유사한 청크가 필요한 직접 QA에 적합.</td>
<td>입력 컨텍스트에 노이즈가 많으면 저품질 일치 항목을 반환할 수 있음; 다양성 제어 없음.</td>
</tr>
<tr>
<td><strong>similarity_score_threshold</strong></td>
<td>후보를 가져와 <strong>관련성</strong>(≈[0,1])으로 변환하고, <code>score_threshold</code> 미만인 항목을 <strong>제거</strong>한 다음 최대 <em>k</em>개를 반환.</td>
<td>임계값을 충족하는 ≤ <em>k</em>개의 <code>Document</code>.</td>
<td>임계값 적용에 충분한 후보를 확보하기 위해 일반적으로 <em>k</em>보다 큼 (내부/<code>fetch_k</code> 통해).</td>
<td><code>score_threshold</code> (예: 0.3–0.7), <code>k</code>, 선택적 <code>filter</code>.</td>
<td>강력한 일치 항목만 유지; 환각이나 “주제 외” 컨텍스트를 피하는 데 유용.</td>
<td>임계값이 너무 높으면 <strong>k개 미만</strong> (또는 0개)을 반환할 수 있음; 점수는 원시 거리가 아니라 <strong>정규화된 관련성</strong>임을 기억할 것.</td>
</tr>
<tr>
<td><strong>mmr (Max Marginal Relevance)</strong></td>
<td>쿼리 유사도와 <strong>다양성</strong>의 균형을 맞추기 위해 <strong>더 큰 후보 세트</strong>를 재순위화; 관련성이 있고 중복되지 않는 항목을 반복적으로 선택.</td>
<td>정확히 <em>k</em>개의 <code>Document</code>, 더 다양함.</td>
<td>적절한 풀을 위해 <code>fetch_k</code> &gt; <em>k</em> (예: 20)를 사용한 다음 <em>k</em>개를 선택.</td>
<td><code>k</code>, <code>fetch_k</code> (예: 4–5×k), <code>lambda_mult</code> (약 0.3–0.7; 높을수록 더 많은 관련성, 낮을수록 더 많은 다양성), 선택적 <code>filter</code>.</td>
<td>요약, 긴 답변, 또는 상위 k개가 거의 중복일 때 훌륭함; 하위 주제의 적용 범위 개선.</td>
<td><code>fetch_k</code>가 너무 작으면 다양성에 해로움; 극단적인 <code>lambda_mult</code>는 과도할 수 있음: 1.0에 가까우면 일반 유사도와 동일, 0.0에 가까우면 다양하지만 주제에서 벗어날 수 있음.</td>
</tr>
</tbody></table>
<hr>
<h3 id="작업별-빠른-추천">작업별 빠른 추천</h3>
<table>
<thead>
<tr>
<th>작업</th>
<th>검색 유형</th>
<th align="right">k (필터링 후 반환)</th>
<th align="right">fetch_k (후보 풀)</th>
<th align="right">score_threshold (관련성 0–1)</th>
<th align="right">lambda_mult (MMR)</th>
<th>이유 / 참고 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>집중 QA</strong> (정확한 사실 조회)</td>
<td><code>similarity_score_threshold</code></td>
<td align="right"><strong>4–6</strong></td>
<td align="right"><strong>20–40</strong></td>
<td align="right"><strong>0.55–0.70</strong> (<strong>0.60</strong>부터 시작)</td>
<td align="right">—</td>
<td>최대 정밀도; 임계값이 약한 일치 항목을 제거; 작은 k는 컨텍스트를 좁게 유지.</td>
</tr>
<tr>
<td><strong>탐색적 QA / 다측면 답변</strong></td>
<td><code>mmr</code></td>
<td align="right"><strong>6–10</strong></td>
<td align="right"><strong>5×k</strong> (≈ <strong>40–80</strong>)</td>
<td align="right"><em>(선택적 후 필터)</em> 0.40–0.55</td>
<td align="right"><strong>0.4–0.6</strong> (<strong>0.5</strong>부터 시작)</td>
<td>관련성과 다양성의 균형을 맞춰 10개의 거의 중복된 결과를 방지.</td>
</tr>
<tr>
<td><strong>요약</strong> (map-reduce / 개요)</td>
<td><code>mmr</code></td>
<td align="right"><strong>8–12</strong></td>
<td align="right"><strong>5×k</strong> (≈ <strong>50–100</strong>)</td>
<td align="right">0.30–0.45</td>
<td align="right"><strong>0.3–0.5</strong></td>
<td>더 넓은 범위를 탐색; 주변부지만 중요한 섹션을 포함하기 위해 낮은 임계값.</td>
</tr>
<tr>
<td><strong>의미론적 검색/찾아보기</strong> (사용자에게 결과 표시)</td>
<td><code>similarity</code> <strong>또는</strong> <code>mmr</code></td>
<td align="right"><strong>10–20</strong></td>
<td align="right"><strong>3×k</strong></td>
<td align="right">0.40–0.60 (필요하면 제거)</td>
<td align="right"><strong>0.4–0.6</strong> (MMR인 경우)</td>
<td>사용자가 결과를 읽는 경우 다양성이 도움이 됨; 다양성을 위해 MMR 고려.</td>
</tr>
</tbody></table>
<hr>
<h2 id="문서-필터링-where_document-옵션">문서 필터링: <code>where_document</code> 옵션</h2>
<p><code>similarity_search</code> 등의 검색 메서드에서 <code>where_document</code> 파라미터를 사용하여 <strong>문서 내용</strong>으로 필터링할 수 있습니다.</p>
<h3 id="1-문자열-포함-연산자">1. 문자열 포함 연산자</h3>
<pre><code class="language-python"># 특정 문자열 포함 (대소문자 구분)
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={&quot;$contains&quot;: &quot;search_string&quot;}
)

# 특정 문자열 미포함
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={&quot;$not_contains&quot;: &quot;unwanted_string&quot;}
)</code></pre>
<h3 id="2-정규표현식">2. 정규표현식</h3>
<pre><code class="language-python"># 정규표현식 패턴 매칭
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={&quot;$regex&quot;: r&quot;\bAPI\b&quot;}
)

# 이메일 패턴 예시
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={&quot;$regex&quot;: r&quot;^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$&quot;}
)</code></pre>
<h3 id="3-논리-연산자-and-or-조합">3. 논리 연산자 ($and, $or 조합)</h3>
<pre><code class="language-python"># AND 조건: 두 조건을 모두 만족하는 문서
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={
        &quot;$and&quot;: [
            {&quot;$contains&quot;: &quot;machine learning&quot;},
            {&quot;$regex&quot;: &quot;[0-9]+&quot;}
        ]
    }
)

# OR 조건: 두 조건 중 하나라도 만족하는 문서
results = vectordb.similarity_search(
    &quot;query&quot;,
    where_document={
        &quot;$or&quot;: [
            {&quot;$contains&quot;: &quot;Python&quot;},
            {&quot;$contains&quot;: &quot;JavaScript&quot;}
        ]
    }
)</code></pre>
<h3 id="where-vs-where_document-비교"><code>where</code> vs <code>where_document</code> 비교</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>용도</th>
<th>지원 연산자</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>where</code></strong></td>
<td><strong>메타데이터</strong> 필터링</td>
<td><code>$eq</code>, <code>$ne</code>, <code>$gt</code>, <code>$gte</code>, <code>$lt</code>, <code>$lte</code>, <code>$in</code>, <code>$nin</code></td>
</tr>
<tr>
<td><strong><code>where_document</code></strong></td>
<td><strong>문서 내용</strong> 필터링</td>
<td><code>$contains</code>, <code>$not_contains</code>, <code>$regex</code></td>
</tr>
</tbody></table>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-python"># 메타데이터와 문서 내용을 동시에 필터링
results = vectordb.similarity_search(
    &quot;AI 기술 질문&quot;,
    k=5,
    where={&quot;source&quot;: &quot;technical_doc.pdf&quot;},  # 메타데이터 필터
    where_document={&quot;$contains&quot;: &quot;neural network&quot;}  # 문서 내용 필터
)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[LangGraph 대화 기록 관리]]></title>
            <link>https://velog.io/@twonezero_98/LangGraph-%EB%8C%80%ED%99%94-%EA%B8%B0%EB%A1%9D-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@twonezero_98/LangGraph-%EB%8C%80%ED%99%94-%EA%B8%B0%EB%A1%9D-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Tue, 17 Feb 2026 10:39:59 GMT</pubDate>
            <description><![CDATA[<h2 id="1-메시지-대화-수집하기-add_messages-reducer">1. 메시지 대화 수집하기 (<code>add_messages</code> reducer)</h2>
<p>LangGraph에서는 <code>add_messages</code> reducer를 사용해 메시지 목록을 효율적으로 관리할 수 있습니다.</p>
<pre><code class="language-python">from langgraph.graph.message import add_messages
from langchain_core.messages import AIMessage, HumanMessage

# 빈 메시지 리스트 생성
msgs = []

# 메시지 생성 (ID 없음)
human = HumanMessage(content=&quot;Hi&quot;, type=&quot;human&quot;, name=&quot;Alex&quot;)
ai = AIMessage(content=&quot;Hi. How can I help you?&quot;, name=&quot;MyBot&quot;)

# 리스트와 단일 메시지 병합 → reducer가 자동으로 고유 ID 추가
msgs = add_messages(msgs, human)

# 두 메시지 리스트 병합
msgs = add_messages(msgs, [ai])

msgs</code></pre>
<p><strong>결과 예시:</strong></p>
<pre><code>[
    HumanMessage(
        content=&#39;Hi&#39;,
        additional_kwargs={},
        response_metadata={},
        name=&#39;Alex&#39;,
        id=&#39;0434f672-f473-46a1-8ea2-dd3e89def10d&#39;
    ),
    AIMessage(
        content=&#39;Hi. How can I help you?&#39;,
        additional_kwargs={},
        response_metadata={},
        id=&#39;def7339f-294b-4954-be75-aefea1878c85&#39;,
        name=&#39;MyBot&#39;,
        tool_calls=[],
        invalid_tool_calls=[]
    )
]</code></pre><h3 id="메시지-id가-필요한-이유">메시지 ID가 필요한 이유</h3>
<ul>
<li>✅ <strong>메시지 수정 가능</strong>: ID를 통해 특정 메시지를 식별하고 수정</li>
<li>✅ <strong>중복 방지</strong>: 동일한 ID의 메시지가 여러 번 추가되는 것을 방지</li>
<li>✅ <strong>메시지 삭제</strong>: ID를 기반으로 특정 메시지 삭제</li>
</ul>
<hr>
<h2 id="2-왜-대화-기록-관리가-필수인가">2. 왜 대화 기록 관리가 필수인가?</h2>
<p>긴 대화에서 &quot;모든 것을 영원히 저장&quot;하는 것은 부작용이 따릅니다.</p>
<h3 id="대화-기록-관리가-필요한-이유">대화 기록 관리가 필요한 이유</h3>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>컨텍스트 제한 &amp; 비용</strong></td>
<td>토큰은 한정적입니다. 기록이 길어질수록 지연 시간과 비용이 증가합니다.</td>
</tr>
<tr>
<td><strong>품질 저하</strong></td>
<td>LLM은 &quot;중간 망각&quot; 현상을 겪습니다. 긴 대화 기록을 그대로 넣으면 중요한 정보의 리콜률이 오히려 떨어질 수 있습니다.</td>
</tr>
<tr>
<td><strong>보안 &amp; 프라이버시</strong></td>
<td>민감한 정보를 영원히 저장하면 안 됩니다. 삭제/TTL 설정으로 노출을 줄여야 합니다.</td>
</tr>
<tr>
<td><strong>드리프트</strong></td>
<td>필터링되지 않은 과거 응답이 미래 답변에 편향을 주거나 환각을 전파할 수 있습니다.</td>
</tr>
<tr>
<td><strong>툴 노이즈</strong></td>
<td>원시 툴 호출 페이로드(JSON, 로그)가 프롬프트를 불필요하게 부풀립니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-대화-기록-관리-핵심-전략">3. 대화 기록 관리 핵심 전략</h2>
<p>단순한 전략부터 복잡한 전략까지 순서대로 소개합니다.</p>
<h3 id="1-슬라이딩-윈도우-잘라내기">1) 슬라이딩 윈도우 (잘라내기)</h3>
<p>마지막 <strong>K개 메시지</strong> 또는 <strong>T토큰</strong>만 유지합니다.</p>
<ul>
<li><strong>장점</strong>: 구현이 쉽고 비용이 저렴</li>
<li><strong>단점</strong>: 초기에 나왔지만 중요한 사실이 컨텍스트에서 제외될 수 있음</li>
</ul>
<h3 id="2-롤링-요약-대화-개요">2) 롤링 요약 (대화 개요)</h3>
<p>&quot;지금까지 중요한 것&quot;에 대한 짧고 지속적으로 업데이트되는 요약을 유지하고, 원시 메시지는 최근 일부만 보관합니다.</p>
<ul>
<li><strong>장점</strong>: 오래된 사실을 압축된 형태로 유지, 토큰 크기 예측 가능</li>
<li><strong>단점</strong>: 요약이 뉘앙스를 놓칠 수 있음, 추가 LLM 호출 필요</li>
</ul>
<h3 id="3-계층적에피소드-요약">3) 계층적/에피소드 요약</h3>
<p>주제/세그먼트별로 요약하고, 세그먼트가 끝나면 요약들을 다시 요약(&quot;챕터&quot; 방식)합니다.</p>
<ul>
<li><strong>장점</strong>: 매우 긴 세션으로 확장 가능, 주제 전환 지원</li>
<li><strong>단점</strong>: 관리할 부분이 많음</li>
</ul>
<h3 id="4-엔티티슬롯-메모리">4) 엔티티/슬롯 메모리</h3>
<p>안정적인 사실(이름, 선호도, 제약사항)을 구조화된 키→값 또는 작은 지식 그래프로 추출/저장하고, 시스템 또는 컨텍스트 메시지로 재주입합니다.</p>
<ul>
<li><strong>장점</strong>: 지속 가능하고 매우 작은 용량</li>
<li><strong>단점</strong>: 추출 및 충돌 해결 필요 (예: &quot;저는 바보입니다.&quot; → 나중에 &quot;저는 천재입니다.&quot;)</li>
</ul>
<h3 id="5-검색-메모리-벡터-저장소">5) 검색 메모리 (벡터 저장소)</h3>
<p>대화 기록(또는 증류된 핵심)을 인덱싱합니다. 각 턴마다 가장 관련성 높은 <strong>k개</strong> 조각을 검색해 주입합니다.</p>
<ul>
<li><strong>장점</strong>: 관련 있는 것만 가져옴, 확장성 우수</li>
<li><strong>단점</strong>: 인프라 추가(인덱스), 청킹 및 중복 제거 필요</li>
</ul>
<h3 id="6-툴-트레이스-압축">6) 툴 트레이스 압축</h3>
<p>툴 입출력을 제거하거나 요약하고 결과 또는 간결한 근거만 유지합니다.</p>
<ul>
<li><strong>장점</strong>: 큰 JSON을 반환하는 툴에서 큰 절감 효과</li>
</ul>
<h3 id="7-프롬프트-위생--고정">7) 프롬프트 위생 &amp; 고정</h3>
<p>핵심 규칙을 재설명하는 안정적인 시스템 메시지를 유지하고, 스크롤되어 사라지지 않게 합니다.</p>
<ul>
<li><strong>장점</strong>: 추가 토큰 없이 드리프트 감소 (시스템 메시지는 어차피 각 호출의 일부)</li>
</ul>
<h3 id="8-ttl-편집-사용자-컨트롤">8) TTL, 편집, 사용자 컨트롤</h3>
<p>메시지의 수명(TTL), PII 편집, &quot;잊어&quot; 명령, 스레드별 삭제를 제공합니다.</p>
<ul>
<li><strong>장점</strong>: 모델 성능을 저하시키지 않으면서 프라이버시 및 규정 준수</li>
</ul>
<hr>
<h2 id="4-실용적인-구성">4. 실용적인 구성</h2>
<ul>
<li><strong>작은 슬라이딩 윈도우</strong> 유지 (예: 마지막 6-12턴)</li>
<li>지속적인 사실과 결정을 캡처하는 <strong>롤링 요약</strong> 유지</li>
<li><strong>벡터 저장소</strong>에 핵심 저장; 각 턴마다 3-5개 관련 항목 검색</li>
</ul>
<p>→ 훌륭한 품질/비용 균형을 제공하고 확장 가능합니다.</p>
<hr>
<h2 id="5-상황별-전략-선택-가이드">5. 상황별 전략 선택 가이드</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>사용 상황</th>
<th>참고사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>슬라이딩 윈도우</strong></td>
<td>가장 저렴하고 단순한 해결책 원할 때</td>
<td>메시지 개수 기반이 아닌 <strong>토큰 인식</strong> 방식으로 만들기</td>
</tr>
<tr>
<td><strong>롤링 요약</strong></td>
<td>대화가 수십 턴을 넘고 초기에 중요한 사실이 나올 때</td>
<td>요약을 짧게 유지</td>
</tr>
<tr>
<td><strong>검색 메모리</strong></td>
<td>매우 긴 세션 또는 여러 주제에서 선택적 리콜이 필요할 때</td>
<td>원시 덤프가 아니라 증류된 핵심을 저장</td>
</tr>
<tr>
<td><strong>엔티티 메모리</strong></td>
<td>안정적인 사용자 프로필/선호도가 중요할 때</td>
<td>충돌 병합 필요</td>
</tr>
<tr>
<td><strong>툴 압축</strong></td>
<td>큰 JSON 페이로드를 반환하는 툴을 호출할 때</td>
<td>미래 추론에 영향을 주는 필드만 유지</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[LangGraph - Tool Binding과 Super-Step 이해하기]]></title>
            <link>https://velog.io/@twonezero_98/LangGraph-Tool-Binding%EA%B3%BC-Super-Step-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/LangGraph-Tool-Binding%EA%B3%BC-Super-Step-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 04 Feb 2026 10:31:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/d423ff7a-380f-4862-b591-f8ed9013d392/image.png" alt=""></p>
<h2 id="📚-시작하며">📚 시작하며</h2>
<blockquote>
<p>LangGraph를 학습하면서 가장 중요하게 이해해야 할 개념들을 정리했습니다. 특히 Tool Binding된 LLM 노드를 추가하는 방법과 Super-Step의 개념, 그리고 이것이 메모리 관리와 어떻게 연결되는지를 중심으로 기록했습니다.</p>
</blockquote>
<hr>
<h2 id="🔧-tool-binding---llm에-도구를-연결하기">🔧 Tool Binding - LLM에 도구를 연결하기</h2>
<h3 id="tool의-기본-구조">Tool의 기본 구조</h3>
<p>LangChain은 일반 함수를 Tool로 변환할 수 있는 래퍼 클래스를 제공합니다. 예를 들어 검색 기능을 Tool로 만드는 과정은 다음과 같습니다.</p>
<pre><code class="language-python">from langchain.agents import Tool
from langchain_community.utilities import GoogleSerperAPIWrapper

serper = GoogleSerperAPIWrapper()
tool_search = Tool(
    name=&quot;search&quot;,
    func=serper.run,
    description=&quot;Useful for when you need more information from an online search&quot;
)</code></pre>
<p>이렇게 생성된 Tool은 독립적으로 호출할 수 있습니다.</p>
<pre><code class="language-python">tool_search.invoke(&quot;What is the capital of France?&quot;)</code></pre>
<h3 id="llm에-tool-바인딩하기">LLM에 Tool 바인딩하기</h3>
<p>Tool을 구현할 때는 항상 2가지 변경사항이 필요합니다:</p>
<ol>
<li><strong>OpenAI 호출 시 Tool을 JSON 형식으로 제공</strong></li>
<li><strong>응답 처리: <code>finish_reason==&quot;tool_calls&quot;</code>를 확인하고 함수를 실행한 후 결과 제공</strong></li>
</ol>
<p>LangGraph에서는 <code>bind_tools()</code> 메서드를 사용하여 LLM에 Tool을 바인딩합니다.</p>
<pre><code class="language-python">from langchain_openai import ChatOpenAI

tools = [tool_search]

llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)
llm_with_tools = llm.bind_tools(tools)</code></pre>
<blockquote>
<p>💡
<code>llm.bind_tools()</code>는 LLM이 필요할 때 특정 도구를 호출할 수 있도록 연결해주는 핵심 메서드입니다. 이를 통해 LLM은 단순히 텍스트를 생성하는 것을 넘어 실제 기능을 수행할 수 있게 됩니다.</p>
</blockquote>
<hr>
<h2 id="🔄-graph-구조에서-tool-node-추가하기">🔄 Graph 구조에서 Tool Node 추가하기</h2>
<h3 id="state-정의">State 정의</h3>
<pre><code class="language-python">from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]</code></pre>
<p>TypedDict를 사용하여 State 객체를 정의했습니다. <code>add_messages</code>는 메시지 리스트를 관리하는 reducer입니다.</p>
<h3 id="graph-builder와-node-추가">Graph Builder와 Node 추가</h3>
<pre><code class="language-python">from langgraph.graph import StateGraph, START
from langgraph.prebuilt import ToolNode, tools_condition

# Step 1-2: Graph Builder 초기화
graph_builder = StateGraph(State)

# Step 3: Chatbot Node 추가
def chatbot(state: State):
    return {&quot;messages&quot;: [llm_with_tools.invoke(state[&quot;messages&quot;])]}

graph_builder.add_node(&quot;chatbot&quot;, chatbot)
graph_builder.add_node(&quot;tools&quot;, ToolNode(tools=tools))

# Step 4: Edge 연결
graph_builder.add_conditional_edges(&quot;chatbot&quot;, tools_condition, &quot;tools&quot;)
graph_builder.add_edge(&quot;tools&quot;, &quot;chatbot&quot;)
graph_builder.add_edge(START, &quot;chatbot&quot;)

# Step 5: Compile
graph = graph_builder.compile()</code></pre>
<blockquote>
<p>💡
<code>ToolNode</code>는 LangGraph에서 제공하는 prebuilt 노드로, Tool 실행을 자동으로 처리합니다. <code>tools_condition</code>은 LLM이 Tool을 호출해야 하는지 판단하는 조건부 분기 입니다.</p>
</blockquote>
<hr>
<h2 id="⚡-super-step---langgraph의-핵심-개념">⚡ Super-Step - LangGraph의 핵심 개념</h2>
<h3 id="왜-state가-메모리를-자동으로-처리하지-않을까">왜 State가 메모리를 자동으로 처리하지 않을까?</h3>
<p>Graph가 State를 유지하고 append하고 있는데, 왜 이를 통해 메모리를 자동으로 관리하지 않을까요? 이것이 <strong>LangGraph를 이해하는 핵심 포인트</strong>입니다.</p>
<h3 id="super-step의-정의">Super-Step의 정의</h3>
<blockquote>
<p>💡
<strong>Super-Step은 그래프 노드들의 단일 반복(iteration)으로 간주됩니다.</strong><br>병렬로 실행되는 노드들은 같은 Super-Step에 속하고, 순차적으로 실행되는 노드들은 서로 다른 Super-Step에 속합니다.</p>
</blockquote>
<h3 id="관용적인-langgraph-사용법">관용적인 LangGraph 사용법</h3>
<p><strong>관용적인 LangGraph에서는 각 Super-Step마다, 즉 각 상호작용마다 <code>invoke</code>를 호출합니다.</strong></p>
<ul>
<li><strong>Reducer는 하나의 Super-Step 내에서 자동으로 State 업데이트를 처리합니다.</strong></li>
<li><strong>하지만 Super-Step 사이에서는 자동으로 처리하지 않습니다.</strong></li>
</ul>
<p>바로 이것이 <strong>체크포인팅(Checkpointing)</strong>이 달성하는 목표입니다.</p>
<blockquote>
<p>💡
Super-Step의 개념을 이해하지 못하면 LangGraph의 메모리 관리와 State 전파 메커니즘을 제대로 활용할 수 없습니다. 각 <code>invoke</code> 호출이 하나의 Super-Step을 의미한다는 점을 명심해야 합니다.</p>
</blockquote>
<hr>
<h2 id="💾-checkpointing으로-메모리-구현하기">💾 Checkpointing으로 메모리 구현하기</h2>
<h3 id="memorysaver를-사용한-기본-메모리">MemorySaver를 사용한 기본 메모리</h3>
<pre><code class="language-python">from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()

# Graph 컴파일 시 checkpointer 추가
graph = graph_builder.compile(checkpointer=memory)</code></pre>
<h3 id="config를-통한-thread-관리">Config를 통한 Thread 관리</h3>
<pre><code class="language-python">config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;1&quot;}}

def chat(user_input: str, history):
    result = graph.invoke(
        {&quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input}]}, 
        config=config
    )
    return result[&quot;messages&quot;][-1].content</code></pre>
<h3 id="state-history-조회">State History 조회</h3>
<pre><code class="language-python"># 현재 State 확인
graph.get_state(config)

# State 히스토리 조회 (최신순)
list(graph.get_state_history(config))</code></pre>
<blockquote>
<p>💡
LangGraph는 특정 시점으로 되돌아가거나 분기할 수 있는 도구를 제공합니다:</p>
<pre><code class="language-python">config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;1&quot;, &quot;checkpoint_id&quot;: ...}}
graph.invoke(None, config=config)</code></pre>
<p>이를 통해 이전 체크포인트에서 복구하고 재실행할 수 있는 안정적인 시스템을 구축할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="🗄️-sql-기반-영구-메모리">🗄️ SQL 기반 영구 메모리</h2>
<h3 id="sqlite를-이용해-기억하기">SQLite를 이용해 기억하기</h3>
<p>인메모리 저장소 대신 SQLite 데이터베이스를 사용하면 영구적인 메모리 관리가 가능합니다.</p>
<pre><code class="language-python">import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver

db_path = &quot;memory.db&quot;
conn = sqlite3.connect(db_path, check_same_thread=False)
sql_memory = SqliteSaver(conn)

# Graph에 SQL 메모리 적용
graph = graph_builder.compile(checkpointer=sql_memory)</code></pre>
<pre><code class="language-python">config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;3&quot;}}

def chat(user_input: str, history):
    result = graph.invoke(
        {&quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input}]}, 
        config=config
    )
    return result[&quot;messages&quot;][-1].content</code></pre>
<blockquote>
<p>💡
단순한 인메모리 상태 관리를 넘어, 데이터베이스 기반의 영구적이고 반복 가능하며 견고한 시스템을 구축할 수 있습니다. 이를 통해 에이전트 시스템에 자연스러운 기억력을 심어줄 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="📝-핵심-요약">📝 핵심 요약</h2>
<h3 id="langgraph의-주요-구성-요소">LangGraph의 주요 구성 요소</h3>
<ol>
<li><p><strong>State</strong> - 현재 앱의 스냅샷을 표현하는 불변 객체</p>
<ul>
<li>Reducer를 통해 State 업데이트</li>
<li>여러 노드의 동시 실행 가능</li>
</ul>
</li>
<li><p><strong>Nodes</strong> - 실제 작업을 수행하는 함수 단위</p>
<ul>
<li>Chatbot Node: LLM 호출 담당</li>
<li>Tool Node: 도구 실행 담당</li>
</ul>
</li>
<li><p><strong>Edges</strong> - 노드 간 흐름 제어</p>
<ul>
<li>Conditional Edges: 조건부 분기</li>
<li>Normal Edges: 고정된 전환</li>
</ul>
</li>
</ol>
<h3 id="tool-binding-핵심-포인트">Tool Binding 핵심 포인트</h3>
<ul>
<li><code>llm.bind_tools(tools)</code>로 LLM에 도구 연결</li>
<li><code>ToolNode</code>와 <code>tools_condition</code>으로 자동 처리</li>
<li>LLM이 필요시 적절한 도구를 선택하여 실행</li>
</ul>
<h3 id="super-step-이해">Super-Step 이해</h3>
<ul>
<li>하나의 Super-Step = 하나의 <code>invoke</code> 호출</li>
<li>Reducer는 Super-Step 내에서만 자동 작동</li>
<li>Super-Step 간 상태 유지는 Checkpointing이 담당</li>
</ul>
<h3 id="checkpointing의-중요성">Checkpointing의 중요성</h3>
<ul>
<li><strong>MemorySaver</strong>: 인메모리 임시 저장</li>
<li><strong>SqliteSaver</strong>: 영구 데이터베이스 저장</li>
<li>Thread ID로 대화 세션 관리</li>
<li>특정 체크포인트로 복구 및 분기 가능</li>
</ul>
<hr>
<h2 id="🎯-마치며">🎯 마치며</h2>
<blockquote>
</blockquote>
<p>LangGraph는 단순한 상태 관리 라이브러리가 아니라, 복잡한 에이전트 시스템을 구축하기 위한 강력한 프레임워크입니다. Tool Binding을 통해 LLM에 실제 기능을 부여하고, Super-Step 개념으로 상태 전파를 제어하며, Checkpointing으로 견고하고 반복 가능한 시스템을 만들 수 있습니다.</p>
<blockquote>
</blockquote>
<p>특히 Super-Step과 Checkpointing의 관계를 정확히 이해하는 것이 LangGraph를 제대로 활용하는 핵심입니다. Reducer가 자동으로 모든 것을 처리해줄 것이라는 착각에서 벗어나, 명시적인 체크포인팅을 통해 상태를 관리해야 합니다.</p>
<blockquote>
</blockquote>
<p>이러한 개념들을 바탕으로 실제 프로덕션 환경에서 사용 가능한 에이전트 시스템을 구축할 수 있을 것입니다.</p>
<blockquote>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔍 OpenAI SDK를 활용한 Deep Research 패턴 구현]]></title>
            <link>https://velog.io/@twonezero_98/OpenAI-SDK%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Deep-Research-%ED%8C%A8%ED%84%B4-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@twonezero_98/OpenAI-SDK%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Deep-Research-%ED%8C%A8%ED%84%B4-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 02 Feb 2026 07:40:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/fc1cd54b-b7d5-4a4c-81ae-373134131a02/image.png" alt=""></p>
<h2 id="📝-서론">📝 서론</h2>
<blockquote>
<p>비동기 프로그래밍과 AI 에이전트를 결합하여 심층 검색(Deep Research) 시스템을 구현하는 방법을 정리했습니다. 이 글에서는 OpenAI SDK의 Agent 프레임워크를 활용하여 <strong>Plan → Search → Report</strong> 3단계 파이프라인을 구성하는 실제 구현 방법을 다룹니다.</p>
</blockquote>
<p>핵심은 Python의 <code>asyncio</code>를 통한 병렬 처리와 Pydantic 기반 Structured Output을 통한 응답 제어입니다.</p>
<blockquote>
</blockquote>
<hr>
<h2 id="🧩-asyncio와-코루틴의-이해">🧩 AsyncIO와 코루틴의 이해</h2>
<h3 id="asyncio-기본-개념">AsyncIO 기본 개념</h3>
<p><code>async def</code>로 정의된 함수는 비동기 함수로, <code>await</code> 키워드를 사용하여 비동기 작업을 수행할 수 있습니다. 이러한 함수는 <strong>코루틴(Coroutine)</strong>을 생성합니다.</p>
<pre><code class="language-python">async def example_async_function():
    result = await some_async_operation()
    return result</code></pre>
<h3 id="코루틴coroutine이란">코루틴(Coroutine)이란?</h3>
<ul>
<li>일반 함수와 비슷하지만, <code>await</code> 키워드를 사용하여 실행을 일시 중지하고 나중에 다시 시작할 수 있습니다.</li>
<li>호출 시 coroutine object를 반환합니다.</li>
<li>Event Loop가 코루틴을 실행합니다.</li>
</ul>
<h3 id="event-loop의-역할">Event Loop의 역할</h3>
<p>Event Loop는 비동기 작업을 스케줄링하고 실행하는 역할을 합니다. 특정 coroutine이 waiting 상태라면 다른 coroutine을 실행하여 효율적인 동시성(concurrency)을 제공합니다.</p>
<blockquote>
<p>💡
Event Loop는 단일 스레드 내에서 여러 작업을 동시에 처리할 수 있게 해주는 핵심 메커니즘입니다. I/O bound 작업에서 특히 효과적입니다.</p>
</blockquote>
<hr>
<h2 id="🤖-openai-sdk-agent-프레임워크">🤖 OpenAI SDK Agent 프레임워크</h2>
<h3 id="agent와-tool-등록">Agent와 Tool 등록</h3>
<p>OpenAI SDK는 AI 에이전트를 쉽게 구축할 수 있는 라이브러리를 제공합니다. <code>@function_tool</code> 데코레이터를 통해 Python 함수를 tool로 등록할 수 있습니다.</p>
<pre><code class="language-python">from agents import Agent, function_tool

@function_tool
def my_tool_function(param: str) -&gt; str:
    &quot;&quot;&quot;도구 설명&quot;&quot;&quot;
    return f&quot;결과: {param}&quot;

agent = Agent(
    name=&quot;MyAgent&quot;,
    instructions=&quot;당신의 역할은...&quot;,
    tools=[my_tool_function],
    model=&quot;gpt-4o-mini&quot;
)</code></pre>
<h3 id="agent를-tool로-등록">Agent를 Tool로 등록</h3>
<p>Agent 자체도 다른 Agent의 tool로 등록할 수 있습니다. 이는 <code>as_tool()</code> 메서드를 사용합니다.</p>
<pre><code class="language-python">agent1.as_tool(
    tool_name=&quot;sales_agent1&quot;, 
    tool_description=&quot;영업 관련 질문에 답변하는 에이전트&quot;
)</code></pre>
<h3 id="handoffs-agent-간-대화-관리">Handoffs: Agent 간 대화 관리</h3>
<p>Handoffs는 Agent 간의 작업 전달을 관리하는 기능입니다. 특정 역할을 가진 Agent에게 작업을 전달하고, 해당 Agent의 작업이 끝나면 다른 Agent에게 작업을 전달할 수 있습니다.</p>
<p><code>handoffs_description</code> 매개변수에 에이전트의 역할 또는 임무를 설명합니다.</p>
<hr>
<h2 id="🛡️-guardrails-패턴">🛡️ Guardrails 패턴</h2>
<h3 id="guardrail을-agent로-구현">Guardrail을 Agent로 구현</h3>
<p>가드레일 자체를 Agent로 구현하여 입력 검증 로직을 구조화할 수 있습니다.</p>
<pre><code class="language-python">from pydantic import BaseModel
from agents import Agent, input_guardrail, Runner, GuardrailFunctionOutput

class NameCheckOutput(BaseModel):
    is_name_in_message: bool
    name: str

guardrail_agent = Agent( 
    name=&quot;Name check&quot;,
    instructions=&quot;Check if the user is including someone&#39;s personal name in what they want you to do.&quot;,
    output_type=NameCheckOutput,  # Structured output
    model=&quot;gpt-4o-mini&quot;
)

@input_guardrail
async def guardrail_against_name(ctx, agent, message):
    result = await Runner.run(guardrail_agent, message, context=ctx.context)
    is_name_in_message = result.final_output.is_name_in_message
    return GuardrailFunctionOutput(
        output_info={&quot;found_name&quot;: result.final_output},
        tripwire_triggered=is_name_in_message
    )</code></pre>
<blockquote>
<p>💡
Pydantic 모델을 <code>output_type</code>으로 지정하면, Agent가 자유 형식이 아닌 정해진 구조로만 응답하도록 강제할 수 있습니다. 이는 강력한 가드레일 역할을 합니다.</p>
</blockquote>
<hr>
<h2 id="🔬-deep-research-구현-패턴">🔬 Deep Research 구현 패턴</h2>
<p>Deep Research는 OpenAI SDK의 내장 <code>WebSearchTool</code>과 Agent의 <strong>Structured Output</strong>을 활용하여 심층 검색 기능을 구현하는 패턴입니다.</p>
<h3 id="전체-아키텍처-plan-→-search-→-report">전체 아키텍처: Plan → Search → Report</h3>
<pre><code class="language-mermaid">graph LR
    A[사용자 쿼리] --&gt; B[Planner Agent]
    B --&gt;|검색 계획| C[Search Agent]
    C --&gt;|병렬 검색| D[검색 결과들]
    D --&gt; E[Writer Agent]
    E --&gt; F[최종 리포트]</code></pre>
<hr>
<h2 id="📋-1단계-plan---structured-outputs를-통한-검색-계획">📋 1단계: Plan - Structured Outputs를 통한 검색 계획</h2>
<h3 id="pydantic-모델-정의">Pydantic 모델 정의</h3>
<p>검색 계획을 구조화하기 위해 Pydantic 모델을 정의합니다.</p>
<pre><code class="language-python">from pydantic import BaseModel, Field

class WebSearchItem(BaseModel):
    reason: str = Field(description=&quot;이 검색이 필요한 이유&quot;)
    query: str = Field(description=&quot;검색어&quot;)

class WebSearchPlan(BaseModel):
    searches: list[WebSearchItem] = Field(description=&quot;수행할 웹 검색 목록&quot;)</code></pre>
<h3 id="planner-agent-구성">Planner Agent 구성</h3>
<pre><code class="language-python">from agents import Agent

HOW_MANY_SEARCHES = 5

planner_agent = Agent(
    name=&quot;PlannerAgent&quot;,
    instructions=f&quot;You are a helpful research assistant. Given a query, come up with a set of web searches to perform to best answer the query. Output {HOW_MANY_SEARCHES} terms to query for.&quot;,
    model=&quot;gpt-4o-mini&quot;,
    output_type=WebSearchPlan,  # ← Structured Output (Format Guardrail)
)</code></pre>
<blockquote>
<p>💡
<code>output_type</code>을 지정하면 LLM이 반드시 해당 스키마에 맞는 JSON만 반환합니다. 이는 파싱 오류를 원천 차단하는 강력한 방법입니다.</p>
</blockquote>
<h3 id="실제-사용-예시">실제 사용 예시</h3>
<pre><code class="language-python">result = await Runner.run(planner_agent, f&quot;Query: {query}&quot;)
search_plan = result.final_output_as(WebSearchPlan)
print(f&quot;Will perform {len(search_plan.searches)} searches&quot;)</code></pre>
<hr>
<h2 id="🌐-2단계-search---병렬-검색-실행">🌐 2단계: Search - 병렬 검색 실행</h2>
<h3 id="openai-websearchtool-활용">OpenAI WebSearchTool 활용</h3>
<p>Search Agent는 OpenAI의 내장 <code>WebSearchTool</code>을 사용하여 실제 웹 검색을 수행합니다.</p>
<pre><code class="language-python">from agents import Agent, WebSearchTool

search_agent = Agent(
    name=&quot;Search agent&quot;,
    instructions=&quot;검색어에 대해 2-3문단으로 요약하세요.&quot;,
    tools=[WebSearchTool(search_context_size=&quot;low&quot;)],  # OpenAI 내장 검색 툴
    model=&quot;gpt-4o-mini&quot;,
)</code></pre>
<h3 id="asyncio를-통한-병렬-검색-구현">AsyncIO를 통한 병렬 검색 구현</h3>
<p>여러 검색어를 <strong>병렬(Parallel)</strong>로 처리하는 것이 Deep Research의 핵심입니다. <code>asyncio.create_task</code>와 <code>asyncio.gather</code>를 사용합니다.</p>
<pre><code class="language-python">import asyncio

async def perform_searches(search_plan: WebSearchPlan) -&gt; list[str]:
    &quot;&quot;&quot;검색 계획의 모든 검색을 병렬로 실행&quot;&quot;&quot;
    # 각 검색 아이템을 Task로 생성
    tasks = [
        asyncio.create_task(search(item)) 
        for item in search_plan.searches
    ]
    # 병렬로 실행 및 결과 수집
    results = await asyncio.gather(*tasks)
    return results

async def search(item: WebSearchItem) -&gt; str:
    &quot;&quot;&quot;개별 검색 수행&quot;&quot;&quot;
    input_text = f&quot;Search term: {item.query}\nReason for searching: {item.reason}&quot;
    result = await Runner.run(search_agent, input_text)
    return str(result.final_output)</code></pre>
<blockquote>
<p>💡
<code>asyncio.gather(*tasks)</code>는 모든 Task를 병렬로 실행하고, 모든 결과가 완료될 때까지 기다립니다. 5개의 검색을 순차적으로 하면 5배의 시간이 걸리지만, 병렬 처리로 큰 성능 향상을 얻을 수 있습니다.</p>
</blockquote>
<h3 id="실제-구현-진행-상황-추적">실제 구현: 진행 상황 추적</h3>
<p>실제 프로젝트에서는 <code>asyncio.as_completed</code>를 사용하여 완료되는 검색부터 처리하고 진행 상황을 추적할 수 있습니다.</p>
<pre><code class="language-python">async def perform_searches(self, search_plan: WebSearchPlan) -&gt; list[str]:
    &quot;&quot;&quot;검색을 병렬로 수행하고 진행 상황을 추적&quot;&quot;&quot;
    num_completed = 0
    tasks = [
        asyncio.create_task(self.search(item)) 
        for item in search_plan.searches
    ]

    results = []
    for task in asyncio.as_completed(tasks):
        result = await task
        if result is not None:
            results.append(result)
        num_completed += 1
        print(f&quot;Searching... {num_completed}/{len(tasks)} completed&quot;)

    return results</code></pre>
<hr>
<h2 id="✍️-3단계-report---최종-보고서-작성">✍️ 3단계: Report - 최종 보고서 작성</h2>
<h3 id="writer-agent-구성">Writer Agent 구성</h3>
<p>검색 결과를 종합하여 최종 리포트를 작성하는 Agent입니다.</p>
<pre><code class="language-python">from pydantic import BaseModel, Field

class ReportData(BaseModel):
    short_summary: str = Field(
        description=&quot;A short 2-3 sentence summary of the findings.&quot;
    )
    markdown_report: str = Field(description=&quot;The final report&quot;)
    follow_up_questions: list[str] = Field(
        description=&quot;Suggested topics to research further&quot;
    )

writer_agent = Agent(
    name=&quot;WriterAgent&quot;,
    instructions=(
        &quot;You are a senior researcher tasked with writing a cohesive report for a research query. &quot;
        &quot;You will be provided with the original query, and some initial research done by a research assistant.\n&quot;
        &quot;You should first come up with an outline for the report that describes the structure and &quot;
        &quot;flow of the report. Then, generate the report and return that as your final output.\n&quot;
        &quot;The final output should be in markdown format, and it should be lengthy and detailed. Aim &quot;
        &quot;for 5-10 pages of content, at least 1000 words.\n&quot;
        &quot;무조건 한국어로 작성해줘.&quot;
    ),
    model=&quot;gpt-4o-mini&quot;,
    output_type=ReportData,
)</code></pre>
<h3 id="보고서-생성">보고서 생성</h3>
<pre><code class="language-python">async def write_report(query: str, search_results: list[str]) -&gt; ReportData:
    &quot;&quot;&quot;검색 결과를 바탕으로 최종 리포트 작성&quot;&quot;&quot;
    input_text = f&quot;Original query: {query}\nSummarized search results: {search_results}&quot;
    result = await Runner.run(writer_agent, input_text)
    return result.final_output_as(ReportData)</code></pre>
<hr>
<h2 id="🏗️-전체-시스템-통합-researchmanager">🏗️ 전체 시스템 통합: ResearchManager</h2>
<p>모든 단계를 통합하는 <code>ResearchManager</code> 클래스입니다.</p>
<pre><code class="language-python">class ResearchManager:
    def __init__(self, api_key: str):
        &quot;&quot;&quot;Initialize the ResearchManager with an OpenAI API key&quot;&quot;&quot;
        self.api_key = api_key

    async def run(self, query: str):
        &quot;&quot;&quot;Run the deep research process, yielding the status updates and the final report&quot;&quot;&quot;
        # 1. Planning phase
        search_plan = await self.plan_searches(query)

        # 2. Searching phase
        search_results = await self.perform_searches(search_plan)

        # 3. Writing phase
        report = await self.write_report(query, search_results)

        return report</code></pre>
<h3 id="api-key-관리-패턴">API Key 관리 패턴</h3>
<p>Gradio app 을 허깅페이스에 업로드하기 위해 동적으로 API Key를 관리하기 위해 환경 변수를 임시로 설정하는 패턴을 사용합니다.</p>
<pre><code class="language-python">async def plan_searches(self, query: str) -&gt; WebSearchPlan:
    &quot;&quot;&quot;Plan the searches to perform for the query&quot;&quot;&quot;
    original_key = os.environ.get(&quot;OPENAI_API_KEY&quot;)
    os.environ[&quot;OPENAI_API_KEY&quot;] = self.api_key
    try:
        result = await Runner.run(planner_agent, f&quot;Query: {query}&quot;)
        return result.final_output_as(WebSearchPlan)
    finally:
        # Restore original key
        if original_key:
            os.environ[&quot;OPENAI_API_KEY&quot;] = original_key
        else:
            os.environ.pop(&quot;OPENAI_API_KEY&quot;, None)</code></pre>
<blockquote>
<p>💡
API Key를 다룰 때는 반드시 try-finally 블록을 사용하여 원래 환경 변수를 복원해야 합니다. 그렇지 않으면 다른 코드에서 잘못된 API Key를 사용할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="🖥️-gradio-ui-통합">🖥️ Gradio UI 통합</h2>
<p>실시간 상태 업데이트를 제공하는 Gradio UI를 구축했습니다.</p>
<pre><code class="language-python">import gradio as gr

async def run(query: str, api_key: str):
    &quot;&quot;&quot;Run research and yield status updates&quot;&quot;&quot;
    # Validate API key
    if not api_key or not api_key.strip():
        yield (&quot;&quot;, &quot;❌ **Error: Please provide a valid OpenAI API key**&quot;, &quot;&quot;)
        return

    # Clear query box and show initial status
    yield (&quot;&quot;, &quot;🚀 **Starting research...**&quot;, &quot;&quot;)

    status_text = &quot;&quot;
    final_report = &quot;&quot;

    async for chunk in ResearchManager(api_key).run(query):
        # Check if this chunk contains the final report
        if &quot;---&quot; in chunk:
            parts = chunk.split(&quot;## 📊 Research Report&quot;)
            if len(parts) == 2:
                status_text = parts[0]
                final_report = &quot;## 📊 Research Report&quot; + parts[1]
                yield (&quot;&quot;, status_text, final_report)
        else:
            status_text = chunk
            yield (&quot;&quot;, status_text, final_report)</code></pre>
<h3 id="css-애니메이션">CSS 애니메이션</h3>
<pre><code class="language-css">.status-container {
    animation: fadeBlur 1.5s ease-in-out infinite;
}

@keyframes fadeBlur {
    0%, 100% {
        opacity: 1;
        filter: blur(0px);
    }
    50% {
        opacity: 0.6;
        filter: blur(0.5px);
    }
}</code></pre>
<hr>
<h2 id="🎯-핵심-설계-원칙">🎯 핵심 설계 원칙</h2>
<h3 id="1-separation-of-concerns">1. Separation of Concerns</h3>
<ul>
<li>Plan, Search, Report 각 단계를 독립적인 Agent로 분리하여 관심사를 명확히 구분했습니다.</li>
</ul>
<h3 id="2-structured-output을-통한-신뢰성-확보">2. Structured Output을 통한 신뢰성 확보</h3>
<ul>
<li>Pydantic 모델을 사용하여 Agent의 출력 형식을 강제함으로써, 파싱 오류와 예측 불가능한 응답을 방지했습니다.</li>
</ul>
<h3 id="3-asyncio를-통한-성능-최적화">3. AsyncIO를 통한 성능 최적화</h3>
<ul>
<li>병렬 검색 처리로 전체 실행 시간을 크게 단축했습니다. 순차 처리 대비 N배의 성능 향상을 달성했습니다.</li>
</ul>
<h3 id="4-에러-처리와-복원력">4. 에러 처리와 복원력</h3>
<ul>
<li>API Key 관리, 예외 처리, 환경 변수 복원 등을 통해 시스템의 안정성을 확보했습니다.</li>
</ul>
<hr>
<h2 id="💡-결론">💡 결론</h2>
<blockquote>
</blockquote>
<p>OpenAI SDK의 Agent 프레임워크와 Python AsyncIO를 결합하여 효율적인 Deep Research 시스템을 구현했습니다. 
핵심은 다음 세 가지입니다:</p>
<blockquote>
</blockquote>
<ol>
<li><strong>Structured Output을 통한 제어 가능성</strong>: Pydantic 모델로 LLM 응답을 구조화</li>
<li><strong>병렬 처리를 통한 성능</strong>: AsyncIO로 여러 검색을 동시에 실행</li>
<li><strong>명확한 단계 분리</strong>: Plan → Search → Report의 3단계 파이프라인<blockquote>
</blockquote>
이 패턴은 단순한 Q&amp;A를 넘어서, 논리적이고 체계적인 심층 리서치 결과를 제공하는 AI 시스템을 구축하는 데 효과적입니다.<blockquote>
</blockquote>
</li>
</ol>
<hr>
<h2 id="📚-참고-사항">📚 참고 사항</h2>
<p><a href="https://openai.github.io/openai-agents-python/ko/">OpenAI SDK 설명</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 Next.js 16 캐싱 적용해보기]]></title>
            <link>https://velog.io/@twonezero_98/Next.js-16-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/Next.js-16-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 30 Jan 2026 08:58:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/addddcd8-46dd-4688-8d42-be7d93f7f6c9/image.png" alt=""></p>
<h2 id="📌-개요">📌 개요</h2>
<blockquote>
<p>Next.js 16의 새로운 캐싱 기능을 프로젝트에 적용하여 성능을 개선함. 기존의 fetch 기반 데이터 로딩 방식에서 서버 액션과 <code>&#39;use cache&#39;</code> 디렉티브를 활용한 최적화된 구조로 전환했음.</p>
</blockquote>
<hr>
<h2 id="🎯-주요-변경-사항">🎯 주요 변경 사항</h2>
<h3 id="1-캐시된-서버-액션-추가">1. 캐시된 서버 액션 추가</h3>
<p>기존에는 클라이언트에서 직접 API를 호출하는 방식을 사용했으나, 이를 서버 액션으로 전환하고 캐싱을 적용함.</p>
<p><strong>변경 전 (app/page.tsx):</strong></p>
<pre><code class="language-tsx">const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;

const Page = async () =&gt; {
  if (!BASE_URL) return null;
  const res = await fetch(`${BASE_URL}/api/events`);
  const { events } = await res.json();
  // ...
}</code></pre>
<p><strong>변경 후 (app/page.tsx):</strong></p>
<pre><code class="language-tsx">import { getAllEventsCached } from &quot;@/lib/actions/event.actions&quot;;

const Page = async () =&gt; {
  const events = await getAllEventsCached();
  // ...
}</code></pre>
<blockquote>
<p><strong>💡 주요 개선점</strong></p>
<ul>
<li>환경 변수 의존성 제거</li>
<li>불필요한 네트워크 오버헤드 감소</li>
<li>서버 사이드에서 직접 데이터베이스 접근으로 응답 속도 향상</li>
</ul>
</blockquote>
<hr>
<h3 id="2-getalleventscached-구현">2. getAllEventsCached 구현</h3>
<p><code>lib/actions/event.actions.ts</code>에 새로운 캐시된 서버 액션을 추가함:</p>
<pre><code class="language-typescript">export const getAllEventsCached = async () =&gt; {
    &#39;use cache&#39;;

    try {
        await connectDB();

        const events = await Event.find().sort({ createdAt: -1 }).lean();

        return JSON.parse(JSON.stringify(events));
    } catch (error) {
        console.error(&quot;Error fetching all events:&quot;, error);
        throw error;
    }
};</code></pre>
<blockquote>
<p><strong>🔥 핵심 포인트</strong></p>
<ul>
<li><strong><code>&#39;use cache&#39;</code> 디렉티브</strong>: Next.js 16의 새로운 기능으로, 함수 실행 결과를 자동으로 캐싱 (<em><code>next.config.ts</code> 에서 <strong>cacheComponents</strong> 를 true 로 해야함.</em>)</li>
<li><strong><code>.lean()</code></strong>: Mongoose의 lean() 메서드로 순수 JavaScript 객체 반환, 메모리 사용량 감소</li>
<li><strong><code>JSON.parse(JSON.stringify())</code></strong>: MongoDB ObjectId와 Date 객체를 직렬화하여 클라이언트 컴포넌트에서도 안전하게 사용 가능하도록 처리</li>
</ul>
</blockquote>
<hr>
<h3 id="3-eventlist-컴포넌트-분리">3. EventList 컴포넌트 분리</h3>
<p>코드 구조 개선을 위해 이벤트 목록 렌더링 로직을 별도 컴포넌트로 추출함.</p>
<p><strong>components/EventList.tsx:</strong></p>
<pre><code class="language-tsx">import EventCard from &quot;@/components/EventCard&quot;;
import { IEvent } from &quot;@/database&quot;;

export default async function EventList({ events }: { events: IEvent[] }) {

  return (
    &lt;ul className=&quot;events&quot;&gt;
      {events &amp;&amp; events.length &gt; 0 ? (
        events.map((event: IEvent) =&gt; (
          &lt;li key={event.title} className=&quot;list-none&quot;&gt;
            &lt;EventCard {...event} /&gt;
          &lt;/li&gt;
        ))
      ) : (
        &lt;p className=&quot;text-center text-gray-500&quot;&gt;No events found at the moment.&lt;/p&gt;
      )}
    &lt;/ul&gt;
  );
}</code></pre>
<p>이를 통해 다음과 같은 이점을 얻음:</p>
<blockquote>
</blockquote>
<ul>
<li>관심사의 분리</li>
<li>재사용 가능한 컴포넌트 구조</li>
<li>더 나은 코드 가독성</li>
<li>빈 상태 처리 개선<blockquote>
</blockquote>
</li>
</ul>
<hr>
<h3 id="4-nextjs-설정-업데이트">4. Next.js 설정 업데이트</h3>
<p><code>next.config.ts</code>에 캐싱 관련 설정을 추가함:</p>
<pre><code class="language-typescript">const nextConfig: NextConfig = {
  cacheComponents: true,

  images: {
    remotePatterns: [
      // ...
    ]
  }
};</code></pre>
<blockquote>
<p><strong>⚙️ cacheComponents 옵션</strong></p>
<p>Next.js 16에서 기능으로, 컴포넌트 레벨의 캐싱을 활성화함. 이를 통해 서버 컴포넌트의 렌더링 결과도 캐시할 수 있음.</p>
</blockquote>
<hr>
<h2 id="📊-성능-개선-효과">📊 성능 개선 효과</h2>
<h3 id="before">Before</h3>
<ol>
<li>클라이언트 → API Route → MongoDB</li>
<li>매 요청마다 전체 네트워크 라운드트립 발생</li>
<li>환경 변수 검증 오버헤드</li>
</ol>
<h3 id="after">After</h3>
<ol>
<li>서버 컴포넌트 → 캐시된 서버 액션 → MongoDB</li>
<li>캐시 히트 시 데이터베이스 쿼리 생략</li>
<li>직접적인 데이터베이스 접근으로 응답 시간 단축</li>
</ol>
<hr>
<h2 id="🔍-기술적-인사이트">🔍 기술적 인사이트</h2>
<h3 id="nextjs-16의-use-cache-디렉티브">Next.js 16의 &#39;use cache&#39; 디렉티브</h3>
<p><code>&#39;use cache&#39;</code>는 Next.js 16에서 새롭게 도입된 기능으로, 다음과 같은 특징이 있음:</p>
<ul>
<li><strong>함수 레벨 캐싱</strong>: 특정 함수의 실행 결과를 캐시</li>
<li><strong>자동 무효화</strong>: 배포 시 또는 revalidate 설정에 따라 자동으로 캐시 무효화</li>
<li><strong>타입 안전성</strong>: TypeScript와 완벽하게 호환</li>
<li><strong>서버 액션과의 통합</strong>: 서버 액션에서 바로 사용 가능</li>
</ul>
<h3 id="mongoose-lean-최적화">Mongoose .lean() 최적화</h3>
<pre><code class="language-typescript">const events = await Event.find().sort({ createdAt: -1 }).lean();</code></pre>
<p><code>.lean()</code> 메서드는:</p>
<blockquote>
</blockquote>
<ul>
<li>Mongoose Document 대신 순수 JavaScript 객체 반환</li>
<li>메모리 사용량 약 30-40% 감소</li>
<li>직렬화 성능 향상</li>
<li>읽기 전용 데이터에 최적화<blockquote>
<p><strong>⚠️ 주의사항</strong></p>
<p><code>.lean()</code>을 사용하면 Mongoose의 virtuals, getter/setter, 메서드 등을 사용할 수 없음. 순수하게 데이터만 읽어올 때 사용해야 함.</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="🎨-코드-구조-개선">🎨 코드 구조 개선</h2>
<h3 id="컴포넌트-분리의-이점">컴포넌트 분리의 이점</h3>
<p>기존에는 <code>app/page.tsx</code>에서 직접 이벤트 목록을 렌더링했으나, <code>EventList</code> 컴포넌트로 분리함으로써:</p>
<ol>
<li><strong>단일 책임 원칙 준수</strong>: 각 컴포넌트가 하나의 명확한 역할 수행</li>
<li><strong>테스트 용이성</strong>: 독립적인 컴포넌트 테스트 가능</li>
<li><strong>재사용성</strong>: 다른 페이지에서도 동일한 이벤트 리스트 UI 재사용 가능</li>
<li><strong>유지보수성</strong>: 이벤트 목록 관련 변경사항을 한 곳에서 관리</li>
</ol>
<hr>
<h2 id="🚦-다음-단계">🚦 다음 단계</h2>
<p>현재 구현된 캐싱은 기본적인 수준이며, 추가로 고려할 수 있는 개선사항:</p>
<ol>
<li><p><strong>Revalidation 전략 수립</strong></p>
<ul>
<li>이벤트 생성/수정 시 캐시 무효화</li>
<li>시간 기반 revalidation 설정</li>
</ul>
</li>
<li><p><strong>세분화된 캐싱</strong></p>
<ul>
<li>개별 이벤트 단위 캐싱</li>
<li>태그 기반 캐시 무효화</li>
</ul>
</li>
<li><p><strong>모니터링</strong></p>
<ul>
<li>캐시 히트율 측정</li>
<li>성능 메트릭 추적</li>
</ul>
</li>
</ol>
<hr>
<h2 id="📝-결론">📝 결론</h2>
<p>Next.js 16의 새로운 캐싱 기능을 활용하여 애플리케이션의 성능을 크게 개선함. <code>&#39;use cache&#39;</code> 디렉티브와 서버 액션의 조합은 매우 강력하며, 기존의 복잡한 캐싱 전략을 단순화할 수 있음.</p>
<p>특히 다음과 같은 점에서 의미가 있음:</p>
<ul>
<li>✅ 개발자 경험(DX) 향상: <strong>선언적인 캐싱 방식</strong></li>
<li>✅ 성능 개선: 불필요한 네트워크 요청 제거</li>
<li>✅ 코드 품질: 더 나은 구조와 관심사의 분리</li>
<li>✅ 확장성: 향후 추가 최적화를 위한 기반 마련</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tool & Agent Loop 학습]]></title>
            <link>https://velog.io/@twonezero_98/Tool-Agent-Loop-%ED%95%99%EC%8A%B5</link>
            <guid>https://velog.io/@twonezero_98/Tool-Agent-Loop-%ED%95%99%EC%8A%B5</guid>
            <pubDate>Wed, 28 Jan 2026 06:13:09 GMT</pubDate>
            <description><![CDATA[<h1 id="자율적으로-todo를-수행하는-에이전트-만들어보기">자율적으로 Todo를 수행하는 에이전트 만들어보기</h1>
<h2 id="서론">서론</h2>
<blockquote>
<p>AI 에이전트를 라이브러리 없이 순수하게 구현해보면서 에이전트의 핵심 작동 원리를 이해해봅시다. 이 글에서는 OpenAI의 Function Calling 기능과 반복 루프(Agent Loop)만으로 목표를 달성하기 위한 Todo 리스트를 자동으로 생성하고, 각 항목을 순차적으로 실행하는 완전한 에이전트 시스템을 구축하는 방법을 다룹니다.</p>
</blockquote>
<h2 id="개념-소개">개념 소개</h2>
<h3 id="agent란-무엇인가">Agent란 무엇인가?</h3>
<p>에이전트에는 몇 가지 논의된 정의가 있습니다:</p>
<ol>
<li><strong>Sam Altman의 정의</strong>: 독립적으로 작업을 수행할 수 있는 AI 시스템</li>
<li><strong>Anthropic의 정의</strong>: LLM이 워크플로우를 제어하는 시스템</li>
<li><strong>새로운 표준 정의</strong>: LLM이 목표를 달성하기 위해 도구를 반복적으로 실행하는 시스템</li>
</ol>
<p>이 글에서는 세 번째 정의, 즉 <strong>&quot;도구를 반복 실행하며 목표를 달성하는 에이전트&quot;</strong>를 실제로 구현해봅니다.</p>
<hr>
<h2 id="todo-관리-도구-만들기">Todo 관리 도구 만들기</h2>
<blockquote>
<p>에이전트가 사용할 도구를 먼저 만들어야 합니다. 여기서는 간단한 Todo 관리 시스템을 구현합니다.</p>
</blockquote>
<pre><code class="language-python"># 필요한 라이브러리 import 및 환경변수 로드
from rich.console import Console #콘솔 print 를 이쁘게 꾸미기 위함
from dotenv import load_dotenv
from openai import OpenAI
import json
load_dotenv(override=True)

def show(text):
    try:
        Console().print(text)
    except Exception:
        print(text)

openai = OpenAI()</code></pre>
<pre><code class="language-python"># 기본 데이터 구조
todos = []        # Todo 항목들을 저장하는 리스트
completed = []    # 각 Todo의 완료 여부를 저장하는 리스트

def create_todos(descriptions: list[str]) -&gt; str:
    &quot;&quot;&quot;
    새로운 Todo 항목들을 생성하는 도구

    Args:
        descriptions: Todo 항목들의 설명 리스트
    Returns:
        현재 Todo 리스트 상태를 문자열로 반환
    &quot;&quot;&quot;
    todos.extend(descriptions)
    completed.extend([False] * len(descriptions))
    return get_todo_report()

def mark_complete(index: int, completion_notes: str) -&gt; str:
    &quot;&quot;&quot;
    특정 Todo를 완료 처리하는 도구

    Args:
        index: 완료할 Todo의 인덱스 (1부터 시작)
        completion_notes: 완료 과정에 대한 설명
    Returns:
        업데이트된 Todo 리스트 상태
    &quot;&quot;&quot;
    if 1 &lt;= index &lt;= len(todos):
        completed[index - 1] = True
    else:
        return &quot;No todo at this index.&quot;
    Console().print(completion_notes)  # 완료 노트를 출력
    return get_todo_report()

def get_todo_report() -&gt; str:
    &quot;&quot;&quot;
    현재 Todo 리스트 상태를 시각적으로 표시
    완료된 항목은 취소선과 녹색으로 표시
    &quot;&quot;&quot;
    result = &quot;&quot;
    for index, todo in enumerate(todos):
        if completed[index]:
            # Rich 라이브러리의 마크업을 사용한 시각적 표현
            result += f&quot;Todo #{index + 1}: [green][strike]{todo}[/strike][/green]\n&quot;
        else:
            result += f&quot;Todo #{index + 1}: {todo}\n&quot;
    show(result)
    return result</code></pre>
<p>이 도구들은 에이전트가 계획을 세우고(create_todos), 실행하고(mark_complete), 진행 상황을 확인(get_todo_report)할 수 있게 해줍니다.</p>
<hr>
<h2 id="function-calling을-위한-json-schema">Function Calling을 위한 JSON Schema</h2>
<p>OpenAI의 Function Calling 기능을 사용하려면 각 도구를 JSON Schema 형식으로 정의해야 합니다.</p>
<pre><code class="language-python"># 🔧 Tool 정의: create_todos
create_todos_json = {
    &quot;name&quot;: &quot;create_todos&quot;,
    &quot;description&quot;: &quot;Add new todos from a list of descriptions and return the full list&quot;,
    &quot;parameters&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
            &quot;descriptions&quot;: {
                &#39;type&#39;: &#39;array&#39;,           # 배열 타입
                &#39;items&#39;: {&#39;type&#39;: &#39;string&#39;},  # 문자열 항목들
                &#39;title&#39;: &#39;Descriptions&#39;
            }
        },
        &quot;required&quot;: [&quot;descriptions&quot;],      # 필수 파라미터
        &quot;additionalProperties&quot;: False
    }
}

# 🔧 Tool 정의: mark_complete
mark_complete_json = {
    &quot;name&quot;: &quot;mark_complete&quot;,
    &quot;description&quot;: &quot;Mark complete the todo at the given position (starting from 1) and return the full list&quot;,
    &quot;parameters&quot;: {
        &#39;properties&#39;: {
            &#39;index&#39;: {
                &#39;description&#39;: &#39;The 1-based index of the todo to mark as complete&#39;,
                &#39;title&#39;: &#39;Index&#39;,
                &#39;type&#39;: &#39;integer&#39;
            },
            &#39;completion_notes&#39;: {
                &#39;description&#39;: &#39;Notes about how you completed the todo in rich console markup&#39;,
                &#39;title&#39;: &#39;Completion Notes&#39;,
                &#39;type&#39;: &#39;string&#39;
            }
        },
        &#39;required&#39;: [&#39;index&#39;, &#39;completion_notes&#39;],
        &#39;type&#39;: &#39;object&#39;,
        &#39;additionalProperties&#39;: False
    }
}

# OpenAI API에 전달할 tools 리스트
tools = [
    {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: create_todos_json},
    {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: mark_complete_json}
]</code></pre>
<blockquote>
<p>💡 JSON Schema는 LLM에게 도구의 사용법을 알려주는 명세서입니다. LLM은 이 스키마를 보고 언제, 어떻게 도구를 호출해야 하는지 결정합니다.</p>
</blockquote>
<hr>
<h2 id="agent-loop-구현하기">Agent Loop 구현하기</h2>
<p>이제 핵심인 <strong>Agent Loop</strong>를 구현합니다.</p>
<pre><code class="language-python"># 🚀 Tool Call 처리 함수
def handle_tool_calls(tool_calls):
    &quot;&quot;&quot;
    LLM이 요청한 도구 호출들을 실제로 실행

    Args:
        tool_calls: LLM이 요청한 tool call 객체들
    Returns:
        실행 결과를 담은 메시지 리스트
    &quot;&quot;&quot;
    results = []
    for tool_call in tool_calls:
        tool_name = tool_call.function.name  # 호출할 함수 이름
        arguments = json.loads(tool_call.function.arguments)  # 함수 인자를 파싱

        # globals()에서 함수 찾기 - 실제 Python 함수 참조
        tool = globals().get(tool_name)

        # 함수 실행 및 결과 반환
        result = tool(**arguments) if tool else {}

        # OpenAI API 형식에 맞는 결과 메시지 생성
        results.append({
            &quot;role&quot;: &quot;tool&quot;,
            &quot;content&quot;: json.dumps(result),
            &quot;tool_call_id&quot;: tool_call.id
        })
    return results

# 🔄 핵심 Agent Loop
def loop(messages):
    &quot;&quot;&quot;
    에이전트의 핵심 반복 루프
    LLM이 도구를 호출할 때마다 실행하고, 
    더 이상 도구 호출이 없을 때까지 반복
    &quot;&quot;&quot;
    done = False
    while not done:
        # 1. LLM에게 현재 상황 전달 및 응답 받기
        response = openai.chat.completions.create(
            model=&quot;gpt-4&quot;, 
            messages=messages, 
            tools=tools
        )

        finish_reason = response.choices[0].finish_reason

        # 2. 응답 유형에 따라 분기
        if finish_reason == &quot;tool_calls&quot;:
            # LLM이 도구를 호출하려고 함
            message = response.choices[0].message
            tool_calls = message.tool_calls

            # 3. 도구 실행
            results = handle_tool_calls(tool_calls)

            # 4. 대화 기록에 추가 (LLM의 요청 + 도구 실행 결과)
            messages.append(message)
            messages.extend(results)
            # 루프 계속 - LLM이 다시 판단하도록
        else:
            # LLM이 최종 답변을 제공함 - 루프 종료
            done = True

    # 최종 결과 출력
    show(response.choices[0].message.content)</code></pre>
<blockquote>
<p>💡<strong>Agent Loop의 핵심 원리</strong></p>
<ol>
<li>LLM에게 현재 상태와 가용 도구를 제공</li>
<li>LLM이 도구 호출을 요청하면 실행</li>
<li>실행 결과를 대화 기록에 추가</li>
<li>LLM이 &quot;완료&quot;라고 판단할 때까지 1-3 반복</li>
</ol>
<p>이 간단한 패턴으로 복잡한 여러 단계의 문제도 해결할 수 있습니다!</p>
</blockquote>
<hr>
<h2 id="실제-사용-예시">실제 사용 예시</h2>
<p>목표 지향적인 시스템 프롬프트와 함께 실행해봅시다:</p>
<pre><code class="language-python"># 📋 시스템 프롬프트 - 에이전트의 역할 정의
system_message = &quot;&quot;&quot;
당신은 주어진 문제를 해결하기 위해 &#39;할 일 목록(todo)&#39; 도구를 사용하여 계획을 세우고, 
각 단계를 순차적으로 실행하는 해결사입니다.
계획을 세우고, 실행하고, 최종 해결책을 응답하세요.
수치가 명확하지 않다면 합리적인 추정치를 포함하세요.
코드 블록 없이 Rich 콘솔 마크업으로 답변하고, 사용자에게 질문하지 마세요.
&quot;&quot;&quot;

# 💬 실제 문제 제시
user_message = &quot;&quot;&quot;
서울역에서 오후 2시에 출발하는 시속 250km KTX가 있습니다.
오후 3시에 부산역에서 서울을 향해 출발하는 시속 300km 열차가 있습니다.
두 열차는 언제 만날까요? (서울-부산 거리는 약 400km로 가정합니다)
&quot;&quot;&quot;

messages = [
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_message}, 
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_message}
]

# 🎯 에이전트 실행
todos, completed = [], []
loop(messages)</code></pre>
<h3 id="에이전트가-수행하는-과정">에이전트가 수행하는 과정</h3>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/a5c141ba-fa61-407a-9e95-61ddf91524ab/image.gif" alt=""></p>
<ol>
<li><strong>계획 수립</strong>: <code>create_todos</code> 호출</li>
<li><strong>순차 실행</strong>: 각 Todo를 <code>mark_complete</code>로 처리</li>
<li><strong>최종 답변</strong>: 모든 Todo 완료 후 결과 제시</li>
</ol>
<ul>
<li>결과 예시
<img src="https://velog.velcdn.com/images/twonezero_98/post/6f140cec-1853-4da2-856f-a42080b42463/image.png" alt=""></li>
</ul>
<blockquote>
<p>💡 저는 단순히 질문을 했을 뿐이지만, AI 가 직접 문제를 해결하기 위한 조건을 생성하고 차례대로 해결해 나가는 것을 볼 수 있습니다. 이 패턴은 단순한 계산뿐만 아니라 웹 검색, 파일 조작, API 호출 등 다양한 도구와 결합하여 복잡한 작업을 자동화할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="주의사항">주의사항</h2>
<h3 id="1-무한-루프-방지">1. 무한 루프 방지</h3>
<ul>
<li>실제 프로덕션에서는 최대 반복 횟수 제한 필요</li>
<li>예: <code>max_iterations = 10</code></li>
</ul>
<h3 id="2-에러-핸들링-필요">2. 에러 핸들링 필요</h3>
<ul>
<li>에러 내용에 대한 로그를 확인할 수 있는 시스템 구축 필요</li>
</ul>
<h3 id="3-비용-관리">3. 비용 관리</h3>
<ul>
<li>각 루프마다 API 호출이 발생하므로 토큰 사용량 모니터링 필요</li>
<li><code>response.usage</code>로 토큰 사용량 추적</li>
</ul>
<h3 id="4-도구-보안">4. 도구 보안</h3>
<ul>
<li><code>globals().get(tool_name)</code>은 편리하지만 보안 위험 존재</li>
<li>프로덕션에서는 허용된 도구 목록을 명시적으로 관리:<pre><code class="language-python">ALLOWED_TOOLS = {
  &quot;create_todos&quot;: create_todos,
  &quot;mark_complete&quot;: mark_complete
}
tool = ALLOWED_TOOLS.get(tool_name)</code></pre>
<h3 id="5-평가-피드백-루프-추가">5. 평가-피드백 루프 추가</h3>
</li>
<li><strong>Evaluator Agent</strong> 를 추가하여 각 Agent 의 답변이 올바른지 평가하고 피드백 Loop 를 추가하여 답변의 퀄리티를 높일 수 있습니다.</li>
</ul>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li><strong>기본적인 Agent Loop 패턴</strong>: LLM이 도구 호출과 결과 확인을 반복하며 목표를 달성하는 패턴</li>
<li><strong>자율성</strong>: 도구 실행 결과를 대화 기록에 추가하여 LLM이 다음 행동을 스스로 결정</li>
<li><strong>구현의 단순함</strong>: OpenAI Function Calling + while 루프만으로 강력한 에이전트 구현 가능</li>
<li><strong>확장성</strong>: Todo 관리 도구를 웹 검색, 데이터베이스 조작, 코드 실행 등으로 대체하면 실용적인 에이전트로 발전</li>
</ul>
<h2 id="next-step">Next Step:</h2>
<ol>
<li><strong>멀티 에이전트 시스템</strong>: 여러 에이전트가 협력하여 작업 수행</li>
<li><strong>메모리 관리</strong>: 대화 기록이 너무 길어질 때 요약 기법 적용</li>
<li><strong>고급 도구</strong>: 파일 시스템 접근, API 호출, 데이터베이스 쿼리 등</li>
<li><strong>에이전트 프레임워크</strong>: LangChain, AutoGen 등의 프레임워크가 이 패턴을 어떻게 추상화하는지 학습</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 16 개념 정리: Routing, Data Fetching, Caching 등]]></title>
            <link>https://velog.io/@twonezero_98/Next.js-16-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-Routing-Data-Fetching-%EB%93%B1</link>
            <guid>https://velog.io/@twonezero_98/Next.js-16-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-Routing-Data-Fetching-%EB%93%B1</guid>
            <pubDate>Sun, 25 Jan 2026 08:07:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/2eeb61a4-37b3-4e6b-94e4-8960792f262d/image.png" alt=""></p>
<blockquote>
<p>AI agent를 통한 개발이 활발하게 이루어지고 있고, 그에 따라 사용하는 프레임워크나 Tool 선택이 중요해졌습니다. 그 중 Next.js 가 16 버전으로 업데이트 되면서, ai와 함께 하는 풀스택 개발이 더욱 용이해진 것 같습니다.
<strong>개요 :</strong> 기존의 파일 기반 라우팅 시스템을 계승하면서도, <strong>성능 최적화</strong>, <strong>비동기 데이터 처리</strong>, 그리고 <strong>명시적 캐싱 모델</strong>에서 혁신적인 변화를 도입했습니다. 본 포스트에서는 이러한 핵심 개념들을 체계적으로 정리하고, 프로젝트에서 바로 활용할 수 있는 예제와 함께 정리해보겠습니다.</p>
</blockquote>
<hr>
<h2 id="1-파일-기반-라우팅file-based-routing-기초">1. 파일 기반 라우팅(File-based Routing) 기초</h2>
<p>Next.js의 라우팅 시스템은 <strong>&quot;Convention over Configuration&quot;</strong> 철학을 따릅니다. 별도의 라우팅 라이브러리나 복잡한 설정 없이, 파일 시스템 구조 자체가 곧 URL 경로가 됩니다.</p>
<h3 id="1-1-기본-원리-app-routing-or-page-routing">1-1. 기본 원리 (App routing or Page routing)</h3>
<p><code>app</code> 디렉터리가 라우팅의 루트가 되며, 폴더 구조가 URL 경로와 1:1로 매핑됩니다.</p>
<pre><code>app/
├── page.tsx           → /
├── about/
│   └── page.tsx       → /about
├── blog/
│   ├── page.tsx       → /blog
│   └── [slug]/
│       └── page.tsx   → /blog/:slug
└── dashboard/
    ├── page.tsx       → /dashboard
    └── settings/
        └── page.tsx   → /dashboard/settings</code></pre><h3 id="1-2-pagetsx의-역할">1-2. <code>page.tsx</code>의 역할</h3>
<p>각 폴더 내의 <code>page.tsx</code> 파일이 해당 경로의 <strong>Entry Point</strong>가 됩니다. 이 파일이 없으면 해당 경로는 접근 불가능합니다.</p>
<pre><code class="language-typescript">// app/about/page.tsx
export default function AboutPage() {
  return (
    &lt;main&gt;
      &lt;h1&gt;About Us&lt;/h1&gt;
      &lt;p&gt;Welcome to our about page!&lt;/p&gt;
    &lt;/main&gt;
  );
}</code></pre>
<h3 id="1-3-중첩-라우팅의-장점">1-3. 중첩 라우팅의 장점</h3>
<p>폴더 안에 폴더를 구성하여 계층 구조를 자연스럽게 형성할 수 있습니다. 이는 다음과 같은 이점을 제공합니다:</p>
<table>
<thead>
<tr>
<th>장점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>직관적인 구조</strong></td>
<td>URL 경로와 파일 구조가 일치하여 코드 내비게이션이 쉬움</td>
</tr>
<tr>
<td><strong>레이아웃 공유</strong></td>
<td>상위 폴더의 레이아웃이 하위 경로에 자동 적용</td>
</tr>
<tr>
<td><strong>코드 분할</strong></td>
<td>각 경로별로 자동 코드 스플리팅이 적용됨</td>
</tr>
<tr>
<td><strong>유지보수성</strong></td>
<td>관련 코드가 물리적으로 가까이 위치</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-동적-라우팅과-비동기-params-처리">2. 동적 라우팅과 비동기 Params 처리</h2>
<p>URL의 가변적인 세그먼트를 처리하는 동적 라우팅은 Next.js의 핵심 기능 중 하나입니다.</p>
<h3 id="2-1-동적-세그먼트-종류">2-1. 동적 세그먼트 종류</h3>
<table>
<thead>
<tr>
<th>패턴</th>
<th>예시</th>
<th>매칭 경로</th>
<th>params 결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>[id]</code></td>
<td><code>app/posts/[id]/page.tsx</code></td>
<td><code>/posts/1</code>, <code>/posts/abc</code></td>
<td><code>{ id: &#39;1&#39; }</code></td>
</tr>
<tr>
<td><code>[...slug]</code></td>
<td><code>app/docs/[...slug]/page.tsx</code></td>
<td><code>/docs/a/b/c</code></td>
<td><code>{ slug: [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;] }</code></td>
</tr>
<tr>
<td><code>[[...slug]]</code></td>
<td><code>app/shop/[[...slug]]/page.tsx</code></td>
<td><code>/shop</code>, <code>/shop/a/b</code></td>
<td><code>{ slug: undefined }</code> 또는 <code>{ slug: [&#39;a&#39;, &#39;b&#39;] }</code></td>
</tr>
</tbody></table>
<h3 id="2-2-비동기적-params-접근-🆕-핵심-변경사항">2-2. 비동기적 Params 접근 (🆕 핵심 변경사항)</h3>
<p><strong>Next.js 15/16 버전부터 <code>params</code>와 <code>searchParams</code>는 Promise 객체로 전달됩니다.</strong> 이는 프레임워크의 렌더링 최적화를 위한 중요한 변화입니다.</p>
<pre><code class="language-typescript">// ❌ 이전 방식 (Next.js 14 이하) - 더 이상 동작하지 않음
export default function Page({ params }: { params: { id: string } }) {
  return &lt;div&gt;User ID: {params.id}&lt;/div&gt;; // 런타임 에러!
}

// ✅ 현재 방식 (Next.js 15/16) - 비동기 처리 필수
export default async function Page({
  params,
}: {
  params: Promise&lt;{ id: string }&gt;;
}) {
  const { id } = await params;
  return &lt;div&gt;User ID: {id}&lt;/div&gt;;
}</code></pre>
<h3 id="2-3-클라이언트-컴포넌트에서의-처리">2-3. 클라이언트 컴포넌트에서의 처리</h3>
<p>클라이언트 컴포넌트에서는 <code>async/await</code>를 직접 사용할 수 없으므로, React 19의 <code>use()</code> 훅을 활용합니다.</p>
<pre><code class="language-typescript">&#39;use client&#39;;
import { use } from &#39;react&#39;;

export default function UserProfile({
  params,
}: {
  params: Promise&lt;{ id: string }&gt;;
}) {
  const { id } = use(params);

  return (
    &lt;div className=&quot;profile-card&quot;&gt;
      &lt;h2&gt;User Profile&lt;/h2&gt;
      &lt;p&gt;ID: {id}&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<blockquote>
<p><strong>❗마이그레이션 체크리스트</strong></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 모든 <code>params</code> 접근을 <code>await</code> 또는 <code>use()</code>로 변경</li>
<li><input checked="" disabled="" type="checkbox"> <code>searchParams</code> 역시 동일하게 비동기 처리</li>
<li><input checked="" disabled="" type="checkbox"> 타입 정의를 <code>Promise&lt;T&gt;</code>로 업데이트</li>
<li><input checked="" disabled="" type="checkbox"> 기존 동기 접근 로직 제거</li>
</ul>
</blockquote>
<hr>
<h2 id="3-레이아웃layouts-및-성능-최적화">3. 레이아웃(Layouts) 및 성능 최적화</h2>
<p>레이아웃은 여러 페이지 간에 <strong>공통 UI를 공유</strong>하고 <strong>상태를 유지</strong>하는 핵심 메커니즘입니다.</p>
<h3 id="3-1-레이아웃-계층-구조">3-1. 레이아웃 계층 구조</h3>
<pre><code>app/
├── layout.tsx          ← 모든 페이지에 적용 (필수)
├── page.tsx
└── dashboard/
    ├── layout.tsx      ← /dashboard/* 경로에만 적용
    ├── page.tsx
    └── analytics/
        └── page.tsx    ← 상위 두 레이아웃 모두 적용</code></pre><pre><code class="language-typescript">// app/layout.tsx (루트 레이아웃 - 필수)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang=&quot;ko&quot;&gt;
      &lt;body&gt;
        &lt;header&gt;
          &lt;nav&gt;메인 네비게이션&lt;/nav&gt;
        &lt;/header&gt;
        &lt;main&gt;{children}&lt;/main&gt;
        &lt;footer&gt;© 2026 My App&lt;/footer&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

// app/dashboard/layout.tsx (대시보드 전용 레이아웃)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;div className=&quot;dashboard-wrapper&quot;&gt;
      &lt;aside className=&quot;sidebar&quot;&gt;
        &lt;ul&gt;
          &lt;li&gt;개요&lt;/li&gt;
          &lt;li&gt;분석&lt;/li&gt;
          &lt;li&gt;설정&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/aside&gt;
      &lt;section className=&quot;content&quot;&gt;{children}&lt;/section&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="3-2-layout-deduplication-🆕-nextjs-16-핵심-최적화">3-2. Layout Deduplication (🆕 Next.js 16 핵심 최적화)</h3>
<p><strong>Next.js 16에서 도입된 Layout Deduplication</strong>은 동일한 레이아웃을 사용하는 경로들 사이를 탐색할 때, 해당 레이아웃 컴포넌트를 <strong>재다운로드하지 않고 재사용</strong>합니다.</p>
<pre><code>[기존 방식]
/dashboard/overview → /dashboard/analytics
레이아웃 번들 다운로드 (중복 발생!)

[Next.js 16 Deduplication]
/dashboard/overview → /dashboard/analytics
레이아웃 캐시 활용 (네트워크 요청 없음!)</code></pre><p><strong>성능 개선 효과:</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>개선 전</th>
<th>개선 후</th>
<th>향상률</th>
</tr>
</thead>
<tbody><tr>
<td>네트워크 요청</td>
<td>레이아웃마다 재요청</td>
<td>최초 1회만 요청</td>
<td>~80% 감소</td>
</tr>
<tr>
<td>페이지 전환 속도</td>
<td>200-500ms</td>
<td>50-100ms</td>
<td>~70% 단축</td>
</tr>
<tr>
<td>프리페칭 효율</td>
<td>중복 다운로드</td>
<td>스마트 캐싱</td>
<td>대역폭 절약</td>
</tr>
</tbody></table>
<h3 id="3-3-레이아웃-vs-템플릿">3-3. 레이아웃 vs 템플릿</h3>
<table>
<thead>
<tr>
<th>특성</th>
<th>Layout</th>
<th>Template</th>
</tr>
</thead>
<tbody><tr>
<td><strong>리렌더링</strong></td>
<td>페이지 전환 시 유지</td>
<td>매 전환마다 새로 마운트</td>
</tr>
<tr>
<td><strong>상태 유지</strong></td>
<td>O</td>
<td>X</td>
</tr>
<tr>
<td><strong>useEffect 재실행</strong></td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td><strong>사용 케이스</strong></td>
<td>네비게이션, 사이드바</td>
<td>진입 애니메이션, 페이지 뷰 로깅</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-라우트-그룹route-groups">4. 라우트 그룹(Route Groups)</h2>
<p>URL 구조에 영향을 주지 않으면서 프로젝트 구조를 <strong>논리적으로 조직화</strong>할 수 있는 강력한 기능입니다.</p>
<h3 id="4-1-기본-사용법">4-1. 기본 사용법</h3>
<p>폴더명을 소괄호 <code>()</code>로 감싸면 해당 폴더는 URL 경로에서 제외됩니다.</p>
<pre><code>app/
├── (marketing)/
│   ├── layout.tsx      ← 마케팅 전용 레이아웃
│   ├── page.tsx        → /
│   ├── about/
│   │   └── page.tsx    → /about
│   └── pricing/
│       └── page.tsx    → /pricing
├── (dashboard)/
│   ├── layout.tsx      ← 대시보드 전용 레이아웃
│   ├── overview/
│   │   └── page.tsx    → /overview
│   └── settings/
│       └── page.tsx    → /settings
└── (auth)/
    ├── layout.tsx      ← 인증 전용 레이아웃 (최소 UI)
    ├── login/
    │   └── page.tsx    → /login
    └── register/
        └── page.tsx    → /register</code></pre><h3 id="4-2-활용-시나리오">4-2. 활용 시나리오</h3>
<p><strong>시나리오 1: 다중 루트 레이아웃</strong>
동일한 루트 레벨에서 완전히 다른 레이아웃을 적용해야 할 때 유용합니다.</p>
<pre><code class="language-typescript">// app/(marketing)/layout.tsx - 화려한 랜딩 페이지 레이아웃
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
  return (
    &lt;div className=&quot;marketing-theme&quot;&gt;
      &lt;header className=&quot;hero-header&quot;&gt;...&lt;/header&gt;
      {children}
      &lt;footer className=&quot;full-footer&quot;&gt;...&lt;/footer&gt;
    &lt;/div&gt;
  );
}

// app/(dashboard)/layout.tsx - 미니멀한 앱 레이아웃
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    &lt;div className=&quot;app-theme&quot;&gt;
      &lt;Sidebar /&gt;
      &lt;main&gt;{children}&lt;/main&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><strong>시나리오 2: 팀/도메인별 코드 분리</strong></p>
<pre><code>app/
├── (shop)/              ← 이커머스 팀 담당
│   ├── products/
│   └── cart/
├── (blog)/              ← 콘텐츠 팀 담당
│   ├── posts/
│   └── categories/
└── (admin)/             ← 관리자 기능
    └── users/</code></pre><hr>
<h2 id="5-특수-파일과-ui-상태-처리">5. 특수 파일과 UI 상태 처리</h2>
<p>Next.js는 특정 상태에 대응하기 위한 <strong>예약된 파일들</strong>을 제공하여 선언적인 UI 관리를 가능하게 합니다.</p>
<h3 id="5-1-특수-파일-종류">5-1. 특수 파일 종류</h3>
<table>
<thead>
<tr>
<th>파일명</th>
<th>용도</th>
<th>React 기반</th>
<th>필수 지시어</th>
<th>버전</th>
</tr>
</thead>
<tbody><tr>
<td><code>layout.tsx</code></td>
<td>공유 UI 래퍼</td>
<td>-</td>
<td>-</td>
<td>13+</td>
</tr>
<tr>
<td><code>page.tsx</code></td>
<td>고유 페이지 UI</td>
<td>-</td>
<td>-</td>
<td>13+</td>
</tr>
<tr>
<td><code>loading.tsx</code></td>
<td>로딩 중 스켈레톤 UI</td>
<td>Suspense</td>
<td>-</td>
<td>13+</td>
</tr>
<tr>
<td><code>error.tsx</code></td>
<td>런타임 에러 폴백</td>
<td>Error Boundary</td>
<td><code>&#39;use client&#39;</code></td>
<td>13+</td>
</tr>
<tr>
<td><code>not-found.tsx</code></td>
<td>404 에러 페이지</td>
<td>-</td>
<td>-</td>
<td>13+</td>
</tr>
<tr>
<td><code>template.tsx</code></td>
<td>리마운트 레이아웃</td>
<td>-</td>
<td>-</td>
<td>13+</td>
</tr>
<tr>
<td><code>forbidden.tsx</code></td>
<td>403 권한 없음</td>
<td>-</td>
<td>-</td>
<td><strong>16 🆕</strong></td>
</tr>
<tr>
<td><code>unauthorized.tsx</code></td>
<td>401 인증 필요</td>
<td>-</td>
<td>-</td>
<td><strong>16 🆕</strong></td>
</tr>
</tbody></table>
<h3 id="5-2-로딩-상태-처리-loadingtsx">5-2. 로딩 상태 처리 (<code>loading.tsx</code>)</h3>
<pre><code class="language-typescript">// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    &lt;div className=&quot;loading-container&quot;&gt;
      &lt;div className=&quot;skeleton-header&quot; /&gt;
      &lt;div className=&quot;skeleton-grid&quot;&gt;
        {[1, 2, 3, 4].map((i) =&gt; (
          &lt;div key={i} className=&quot;skeleton-card&quot; /&gt;
        ))}
      &lt;/div&gt;
      &lt;p className=&quot;loading-text&quot;&gt;대시보드 로딩 중...&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="5-3-에러-처리-errortsx">5-3. 에러 처리 (<code>error.tsx</code>)</h3>
<pre><code class="language-typescript">// app/dashboard/error.tsx
&#39;use client&#39;; // ⚠️ 필수!

import { useEffect } from &#39;react&#39;;

export default function DashboardError({
  error,
  reset,
}: {
  error: Error &amp; { digest?: string };
  reset: () =&gt; void;
}) {
  useEffect(() =&gt; {
    // 에러 로깅 서비스로 전송
    console.error(&#39;Dashboard Error:&#39;, error);
  }, [error]);

  return (
    &lt;div className=&quot;error-container&quot;&gt;
      &lt;h2&gt;⚠️ 문제가 발생했습니다&lt;/h2&gt;
      &lt;p&gt;{error.message}&lt;/p&gt;
      &lt;button onClick={reset} className=&quot;retry-btn&quot;&gt;
        다시 시도
      &lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h3 id="5-4-인증권한-에러-처리-🆕-nextjs-16">5-4. 인증/권한 에러 처리 (🆕 Next.js 16)</h3>
<pre><code class="language-typescript">// app/admin/forbidden.tsx - 403 Forbidden
export default function AdminForbidden() {
  return (
    &lt;div className=&quot;forbidden-page&quot;&gt;
      &lt;h1&gt;🚫 접근 권한이 없습니다&lt;/h1&gt;
      &lt;p&gt;이 페이지를 보려면 관리자 권한이 필요합니다.&lt;/p&gt;
      &lt;a href=&quot;/contact&quot;&gt;권한 요청하기&lt;/a&gt;
    &lt;/div&gt;
  );
}

// app/dashboard/unauthorized.tsx - 401 Unauthorized
export default function DashboardUnauthorized() {
  return (
    &lt;div className=&quot;unauthorized-page&quot;&gt;
      &lt;h1&gt;🔐 로그인이 필요합니다&lt;/h1&gt;
      &lt;p&gt;이 페이지를 보려면 먼저 로그인해주세요.&lt;/p&gt;
      &lt;a href=&quot;/login&quot;&gt;로그인 페이지로 이동&lt;/a&gt;
    &lt;/div&gt;
  );
}</code></pre>
<blockquote>
<p>[!WARNING]
<strong>Error Component 주의점</strong>
<code>error.tsx</code>는 클라이언트 사이드에서 에러를 캡처해야 하므로 반드시 파일 최상단에 <code>&#39;use client&#39;</code> 지시어를 포함해야 합니다. 이를 누락하면 에러 바운더리가 제대로 동작하지 않습니다.</p>
</blockquote>
<hr>
<h2 id="6-api-라우트-핸들러route-handlers">6. API 라우트 핸들러(Route Handlers)</h2>
<p>프론트엔드 라우팅과 <strong>동일한 파일 시스템 기반</strong>으로 백엔드 API 엔드포인트를 구축합니다.</p>
<h3 id="6-1-기본-구조">6-1. 기본 구조</h3>
<pre><code>app/
└── api/
    ├── users/
    │   ├── route.ts          → GET/POST /api/users
    │   └── [id]/
    │       └── route.ts      → GET/PUT/DELETE /api/users/:id
    └── events/
        └── route.ts          → /api/events</code></pre><h3 id="6-2-http-메서드-핸들러">6-2. HTTP 메서드 핸들러</h3>
<pre><code class="language-typescript">// app/api/users/route.ts
import { NextRequest, NextResponse } from &#39;next/server&#39;;

// GET /api/users - 사용자 목록 조회
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get(&#39;page&#39;) || &#39;1&#39;);
  const limit = parseInt(searchParams.get(&#39;limit&#39;) || &#39;10&#39;);

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({
    data: users,
    pagination: { page, limit },
  });
}

// POST /api/users - 사용자 생성
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { name, email } = body;

    const newUser = await db.user.create({
      data: { name, email },
    });

    return NextResponse.json(newUser, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: &#39;Failed to create user&#39; },
      { status: 500 }
    );
  }
}</code></pre>
<h3 id="6-3-동적-api-라우트-비동기-params-적용">6-3. 동적 API 라우트 (비동기 params 적용)</h3>
<pre><code class="language-typescript">// app/api/events/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  const { id } = await params; // ⚠️ 비동기 처리 필수!

  const event = await db.event.findUnique({
    where: { id },
    include: { attendees: true },
  });

  if (!event) {
    return Response.json(
      { error: &#39;Event not found&#39; },
      { status: 404 }
    );
  }

  return Response.json(event);
}

export async function PUT(
  request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  const { id } = await params;
  const body = await request.json();

  const updatedEvent = await db.event.update({
    where: { id },
    data: body,
  });

  return Response.json(updatedEvent);
}

export async function DELETE(
  request: Request,
  { params }: { params: Promise&lt;{ id: string }&gt; }
) {
  const { id } = await params;

  await db.event.delete({ where: { id } });

  return new Response(null, { status: 204 });
}</code></pre>
<hr>
<h2 id="7-캐싱-변화-cache-components">7. 캐싱 변화: Cache Components</h2>
<p>Next.js 16 캐싱의 핵심은 <strong>&quot;기본 동적 렌더링, 선택적 캐싱(Opt-in)&quot;</strong>으로의 패러다임 전환입니다.</p>
<blockquote>
</blockquote>
<ul>
<li><strong>SSG</strong>(Static Site Generation)나 <strong>ISR</strong>(Incremental Static Regeneration)은 이제 별도의 설정이 아니라, 개발자가 Cache Boundaries를 어떻게 정의하느냐에 따른 결과물로 통합되었습니다.<blockquote>
</blockquote>
</li>
</ul>
<h3 id="7-1-패러다임의-전환-default-dynamic">7-1. 패러다임의 전환: Default Dynamic</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>변경 전 (Next.js 14 이하)</th>
<th>변경 후 (Next.js 15/16)</th>
</tr>
</thead>
<tbody><tr>
<td><code>fetch</code> 기본값</td>
<td><code>force-cache</code> (정적)</td>
<td><code>no-store</code> (동적)</td>
</tr>
<tr>
<td>캐싱 결정</td>
<td>프레임워크가 자동 판단</td>
<td>개발자가 명시적 선언</td>
</tr>
<tr>
<td>Stale Data 가능성</td>
<td>높음</td>
<td>낮음</td>
</tr>
</tbody></table>
<h3 id="7-2-use-cache-지시어">7-2. &quot;use cache&quot; 지시어</h3>
<pre><code class="language-typescript">// next.config.ts에서 활성화 필요
const nextConfig = {
  experimental: {
    cacheComponents: true,
  },
};

// 파일 단위 캐싱
&#39;use cache&#39;;

export default async function CachedPage() {
  const data = await fetchExpensiveData();
  return &lt;div&gt;{data}&lt;/div&gt;;
}

// 함수 단위 캐싱
async function getCachedProducts(category: string) {
  &#39;use cache&#39;;
  const products = await db.product.findMany({
    where: { category },
  });
  return products;
}

// 컴포넌트 단위 캐싱
async function CachedSidebar() {
  &#39;use cache&#39;;
  const categories = await getCategories();
  return (
    &lt;aside&gt;
      {categories.map((cat) =&gt; (
        &lt;a key={cat.id} href={`/products/${cat.slug}`}&gt;{cat.name}&lt;/a&gt;
      ))}
    &lt;/aside&gt;
  );
}</code></pre>
<h3 id="7-3-세밀한-캐시-수명-제어">7-3. 세밀한 캐시 수명 제어</h3>
<table>
<thead>
<tr>
<th>API</th>
<th>설명</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>cacheLife(&#39;max&#39;)</code></td>
<td>최대한 오래 캐싱</td>
<td>정적 콘텐츠</td>
</tr>
<tr>
<td><code>cacheLife(&#39;hours&#39;)</code></td>
<td>수 시간 캐싱</td>
<td>뉴스 피드</td>
</tr>
<tr>
<td><code>cacheLife(&#39;days&#39;)</code></td>
<td>며칠간 캐싱</td>
<td>제품 목록</td>
</tr>
<tr>
<td><code>cacheLife({ revalidate: 60 })</code></td>
<td>60초마다 갱신</td>
<td>실시간 데이터</td>
</tr>
</tbody></table>
<pre><code class="language-typescript">import { cacheLife } from &#39;next/cache&#39;;

async function getWeatherData(city: string) {
  &#39;use cache&#39;;
  cacheLife(&#39;hours&#39;); // 1시간 캐싱

  const response = await fetch(`https://weather-api.com/${city}`);
  return response.json();
}</code></pre>
<h3 id="7-4-캐시-무효화-api">7-4. 캐시 무효화 API</h3>
<pre><code class="language-typescript">import { revalidateTag, updateTag } from &#39;next/cache&#39;;

// Server Action에서 캐시 갱신
export async function updateProduct(productId: string, data: ProductData) {
  &#39;use server&#39;;

  await db.product.update({
    where: { id: productId },
    data,
  });

  // 태그 기반 캐시 무효화 (프로필 지정 필수)
  revalidateTag(`product-${productId}`, &#39;default&#39;);

  // 🆕 Read-your-writes 시맨틱: 변경 즉시 반영
  updateTag(`product-${productId}`);
}</code></pre>
<hr>
<h2 id="8-개발-생산성-최적화-dx">8. 개발 생산성 최적화 (DX)</h2>
<h3 id="8-1-server-components-hmr-cache">8-1. Server Components HMR Cache</h3>
<p>로컬 개발 중 HMR 발생 시, 서버 컴포넌트 내의 <code>fetch</code> 응답을 캐싱합니다.</p>
<pre><code class="language-typescript">// 개발 모드에서 자동 적용
async function DataComponent() {
  // 코드 수정으로 HMR이 발생해도
  // 이 fetch는 캐시된 결과를 반환 (재호출 X)
  const data = await fetch(&#39;https://api.example.com/expensive-data&#39;);
  return &lt;div&gt;{data}&lt;/div&gt;;
}</code></pre>
<p><strong>이점:</strong></p>
<ul>
<li>불필요한 API 재호출 방지</li>
<li>유료 API 비용 절감</li>
<li>빠른 UI 확인 가능</li>
</ul>
<h3 id="8-2-turbopack-file-system-caching">8-2. Turbopack File System Caching</h3>
<pre><code class="language-typescript">// next.config.ts
const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
};</code></pre>
<p><strong>효과:</strong></p>
<ul>
<li>개발 서버 재시작 속도 ~50% 개선</li>
<li>컴파일 결과물이 디스크에 캐싱됨</li>
</ul>
<hr>
<h2 id="9-강력한-seo-및-메타데이터-관리">9. 강력한 SEO 및 메타데이터 관리</h2>
<h3 id="9-1-서버-사이드-렌더링의-seo-이점">9-1. 서버 사이드 렌더링의 SEO 이점</h3>
<blockquote>
</blockquote>
<pre><code>[클라이언트 사이드 렌더링 (CSR)]
크롤러 요청 → 빈 HTML → JS 로드 → 렌더링 → 콘텐츠 색인
(크롤러가 JS를 실행하지 않으면 색인 실패)
&gt;
[서버 사이드 렌더링 (SSR) - Next.js]
크롤러 요청 → 완성된 HTML → 즉시 콘텐츠 색인 ✅
(JS 실행 필요 없음)</code></pre><blockquote>
</blockquote>
<h3 id="9-2-메타데이터-관리">9-2. 메타데이터 관리</h3>
<p><strong>설정 기반 (Config-based):</strong></p>
<pre><code class="language-typescript">// 정적 메타데이터
export const metadata = {
  title: &#39;My Blog&#39;,
  description: &#39;A blog about web development&#39;,
  openGraph: {
    title: &#39;My Blog&#39;,
    description: &#39;A blog about web development&#39;,
    images: [&#39;/og-image.png&#39;],
  },
};

// 동적 메타데이터 (⚠️ params 비동기 처리 필수)
export async function generateMetadata({
  params,
}: {
  params: Promise&lt;{ slug: string }&gt;;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}</code></pre>
<p><strong>파일 기반 (File-based):</strong></p>
<pre><code>app/
├── favicon.ico         → &lt;link rel=&quot;icon&quot;&gt;
├── icon.png            → &lt;link rel=&quot;icon&quot;&gt;
├── apple-icon.png      → &lt;link rel=&quot;apple-touch-icon&quot;&gt;
├── opengraph-image.png → &lt;meta property=&quot;og:image&quot;&gt;
├── twitter-image.png   → &lt;meta name=&quot;twitter:image&quot;&gt;
└── sitemap.ts          → /sitemap.xml</code></pre><h3 id="9-3-seo-성능-최적화-수치">9-3. SEO 성능 최적화 수치</h3>
<table>
<thead>
<tr>
<th>지표</th>
<th>설명</th>
<th>Next.js 16 목표</th>
</tr>
</thead>
<tbody><tr>
<td>FCP (First Contentful Paint)</td>
<td>첫 콘텐츠 렌더링</td>
<td>~300ms</td>
</tr>
<tr>
<td>LCP (Largest Contentful Paint)</td>
<td>최대 요소 렌더링</td>
<td>~1.2s</td>
</tr>
<tr>
<td>TTFB (Time to First Byte)</td>
<td>첫 바이트 수신</td>
<td>~100ms</td>
</tr>
</tbody></table>
<hr>
<h2 id="10-partial-pre-rendering-ppr의-완성">10. Partial Pre-rendering (PPR)의 완성</h2>
<p>PPR은 <strong>정적 구조와 동적 데이터를 한 페이지 내에서 결합</strong>하는 혁신적인 렌더링 전략입니다.</p>
<h3 id="10-1-작동-원리">10-1. 작동 원리</h3>
<pre><code class="language-typescript">export default async function ProductPage({ params }: Props) {
  const { id } = await params;

  return (
    &lt;main&gt;
      {/* 정적 영역: 빌드 시 프리렌더링 */}
      &lt;Header /&gt;
      &lt;ProductImages productId={id} /&gt;

      {/* 동적 영역: Suspense로 스트리밍 */}
      &lt;Suspense fallback={&lt;PriceSkeleton /&gt;}&gt;
        &lt;DynamicPrice productId={id} /&gt;
      &lt;/Suspense&gt;

      &lt;Suspense fallback={&lt;ReviewsSkeleton /&gt;}&gt;
        &lt;LiveReviews productId={id} /&gt;
      &lt;/Suspense&gt;

      {/* 캐시된 영역 */}
      &lt;CachedRecommendations category={product.category} /&gt;

      &lt;Footer /&gt;
    &lt;/main&gt;
  );
}</code></pre>
<h3 id="10-2-사용자-경험-플로우">10-2. 사용자 경험 플로우</h3>
<blockquote>
</blockquote>
<pre><code>1. 즉시 (0ms)     → 정적 Shell 표시 (Header, Footer)
2. 50ms           → 캐시된 ProductImages 로드
3. 100-200ms      → DynamicPrice 스트리밍 완료
4. 200-500ms      → LiveReviews 스트리밍 완료</code></pre><blockquote>
</blockquote>
<hr>
<h2 id="📝-결론-및-마이그레이션-가이드">📝 결론 및 마이그레이션 가이드</h2>
<p>Next.js 16은 <strong>&quot;명시성(Explicitness)&quot;</strong>을 핵심 가치로 삼아, 개발자가 애플리케이션의 동작을 더 정확하게 예측하고 제어할 수 있도록 설계되었습니다.</p>
<h3 id="핵심-체크리스트">핵심 체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> <strong>params/searchParams 비동기화</strong>: 모든 동적 라우트에서 <code>await</code> 적용</li>
<li><input disabled="" type="checkbox"> <strong>캐싱 전략 재정의</strong>: <code>&quot;use cache&quot;</code> + <code>cacheLife</code> 조합으로 명시적 설정</li>
<li><input disabled="" type="checkbox"> <strong>특수 파일 활용</strong>: <code>forbidden.tsx</code>, <code>unauthorized.tsx</code> 도입</li>
<li><input disabled="" type="checkbox"> <strong>API 라우트 업데이트</strong>: Route Handler의 params도 비동기 처리</li>
<li><input disabled="" type="checkbox"> <strong>PPR 적용</strong>: Suspense와 <code>&quot;use cache&quot;</code>를 활용한 하이브리드 렌더링</li>
</ul>
<h3 id="버전별-변경사항-요약">버전별 변경사항 요약</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>Next.js 14</th>
<th>Next.js 15</th>
<th>Next.js 16</th>
</tr>
</thead>
<tbody><tr>
<td>params 접근</td>
<td>동기</td>
<td>비동기 (도입)</td>
<td>비동기 (필수)</td>
</tr>
<tr>
<td>기본 캐싱</td>
<td>자동</td>
<td>선택적</td>
<td>명시적</td>
</tr>
<tr>
<td>PPR</td>
<td>실험적</td>
<td>안정화 진행</td>
<td>완전 통합</td>
</tr>
<tr>
<td>특수 파일</td>
<td>기본</td>
<td>기본</td>
<td>401/403 추가</td>
</tr>
<tr>
<td>Layout 최적화</td>
<td>기본</td>
<td>개선</td>
<td>Deduplication</td>
</tr>
</tbody></table>
<blockquote>
<p>👍업데이트 된 Next.js 16 의 핵심 내용을 전반적으로 살펴 보았습니다. 
Vecel 등에서 공유한 React, Nextjs 관련 Skills 을 엮어 <strong>Spec</strong> 을 정의하고 agent와 함께 더욱 예측 가능하고 유지보수 하기 쉬운 프로덕트를 개발하기 용이할 것 같습니다!!</p>
</blockquote>
<p><strong>Reference</strong></p>
<ul>
<li><a href="https://nextjs.org/docs/app/guides">https://nextjs.org/docs/app/guides (전체적인 next.js16 내용 with app routing)</a></li>
<li><a href="https://nextjs.org/blog/next-16">https://nextjs.org/blog/next-16 (변경내용 한번에 보기)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📝 LLM 시스템 구축을 위한 5가지 워크플로우 디자인 패턴 정리]]></title>
            <link>https://velog.io/@twonezero_98/LLM-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%EC%9D%84-%EC%9C%84%ED%95%9C-5%EA%B0%80%EC%A7%80-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@twonezero_98/LLM-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%EC%9D%84-%EC%9C%84%ED%95%9C-5%EA%B0%80%EC%A7%80-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 23 Jan 2026 06:27:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/b818181b-20a6-40c6-9c20-6b6e4e91a66d/image.png" alt=""></p>
<p>LLM 기반 애플리케이션이 단순한 프롬프팅을 넘어 복잡한 시스템으로 진화함에 따라, 신뢰성과 성능을 확보하기 위한 <strong>워크플로우 설계</strong>가 중요해짐. Anthropic에서 정의한 5가지 핵심 디자인 패턴을 분석하고 정리함.</p>
<hr>
<h2 id="🏗️-1-prompt-chaining">🏗️ 1. Prompt Chaining</h2>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/c4a1a1b2-4f3b-4818-9790-1692ace90d61/image.png" alt=""></p>
<h4 id="작업을-고정된-일련의-하위-작업-단계로-분해하여-순차적으로-실행하는-패턴임">작업을 고정된 일련의 하위 작업 단계로 분해하여 순차적으로 실행하는 패턴임.</h4>
<ul>
<li><strong>특징:</strong> 하나의 LLM 출력이 다음 LLM의 입력이 되는 구조. 중간에 코드(Gate)를 넣어 흐름을 제어할 수 있음.</li>
<li><strong>장점:</strong> 각 단계를 정밀하게 설계할 수 있어 가드레일 적용이 용이하고 성능 최적화에 유리함.</li>
<li><strong>예시:</strong> <code>섹터 선정</code> -&gt; <code>페인 포인트 식별</code> -&gt; <code>솔루션 제안</code> 순으로 이어지는 비즈니스 로직.</li>
</ul>
<blockquote>
<p>💡 <strong>Note:</strong> Anthropic은 이를 워크플로우로 정의하지만, 첫 번째 단계의 결과에 따라 이후 작업의 맥락이 결정된다는 점에서 에이전트적 특성(자율성)과 경계가 모호한 지점이 있음.</p>
</blockquote>
<hr>
<h2 id="🚦-2-routing">🚦 2. Routing</h2>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/822b5fb8-2d8f-40bc-99cd-5922c715cf77/image.png" alt=""></p>
<h4 id="입력을-분류하고-해당-작업에-가장-적합한-전문-모델이나-프롬프트로-전달하는-패턴임">입력을 분류하고, 해당 작업에 가장 적합한 전문 모델이나 프롬프트로 전달하는 패턴임.</h4>
<ul>
<li><strong>특징:</strong> &#39;Router&#39; 역할을 하는 LLM이 입력값의 의도를 파악하여 분기 처리를 수행함.</li>
<li><strong>장점:</strong> 각 분야에 특화된 전문가(Specialist) 모델을 활용할 수 있어 관심사 분리(Separation of Concerns)가 가능함.</li>
<li><strong>적용:</strong> 고객 문의를 기술 지원, 결제 문의, 일반 상담 등으로 분류하여 처리할 때 매우 유용함.</li>
</ul>
<hr>
<h2 id="⚡-3-parallelization">⚡ 3. Parallelization</h2>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/f30388ee-0e35-4004-be17-2efa9dff602e/image.png" alt=""></p>
<h4 id="하나의-작업을-여러-개로-쪼개어-동시에-처리한-후-결과를-합치는-패턴임">하나의 작업을 여러 개로 쪼개어 동시에 처리한 후 결과를 합치는 패턴임.</h4>
<ul>
<li><strong>특징:</strong> 코드(Python 등) 수준에서 제어하며, 여러 LLM 호출을 동시(Concurrent)에 실행함.</li>
<li><strong>장점:</strong> 처리 속도(Latency)를 단축하고 결과의 일관성을 높일 수 있음.</li>
<li><strong>유형:</strong><ul>
<li><strong>Sectioning:</strong> 큰 작업을 독립적인 하위 작업으로 나누어 병렬 처리.</li>
<li><strong>Voting/Averaging:</strong> 동일한 작업을 여러 번 수행하여 결과의 평균을 내거나 최적의 답변을 선택.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="🎭-4-orchestrator-worker">🎭 4. Orchestrator-Worker</h2>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/8d43e9e9-7206-4eaf-b803-afbc9e33ea9e/image.png" alt=""></p>
<h4 id="복잡한-작업을-llm이-동적으로-분해하고-하위-작업을-할당하는-패턴임">복잡한 작업을 LLM이 동적으로 분해하고 하위 작업을 할당하는 패턴임.</h4>
<ul>
<li><strong>특징:</strong> 3번(병렬화)과 유사해 보이지만, 제어 주체가 코드가 아닌 <strong>LLM(Orchestrator)</strong>이라는 점이 핵심임.</li>
<li><strong>동작:</strong> 오케스트레이터가 작업을 분석하여 필요한 워커(Worker)의 수와 종류를 결정하고, 최종 결과를 합성(Synthesizer)함.</li>
</ul>
<blockquote>
<p>🚀 <strong>Insight</strong>: 워커에게 할당하는 방식에 재량권이 부여되므로 워크플로우와 에이전트의 중간 단계라고 볼 수 있음.</p>
</blockquote>
<hr>
<h2 id="🔄-5-evaluator-optimizer">🔄 5. Evaluator-Optimizer</h2>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/73b4a4a9-2aec-4140-9c64-92f5b8596cc4/image.png" alt=""></p>
<h4 id="생성된-결과를-검증하고-피드백을-통해-지속적으로-개선하는-루프-패턴임">생성된 결과를 검증하고 피드백을 통해 지속적으로 개선하는 루프 패턴임.</h4>
<ul>
<li><strong>특징:</strong> 생성자(Generator)와 검증자(Evaluator)가 협력함. 검증자가 거절(Reject)하면 피드백과 함께 생성자에게 되돌려 보냄.</li>
<li><strong>장점:</strong> 정확성, 예측 가능성, 견고성(Robustness)을 극대화할 수 있음.</li>
<li><strong>활용:</strong> 코드 생성, 기술 문서 작성 등 품질 보증이 필수적인 도메인에서 가장 강력한 효과를 발휘함.</li>
</ul>
<blockquote>
<p>🚀 <strong>Insight</strong>: 프로덕션 환경에서는 100% 보장되는 LLM은 없음. 따라서 이 패턴과 같은 검증 에이전트(Validation Agent)를 배치하는 것이 품질 상한선을 높이는 가장 현실적인 방법임.</p>
</blockquote>
<hr>
<h2 id="📌-요약-및-결론">📌 요약 및 결론</h2>
<ul>
<li>Made by NotebookLLM
<img src="https://velog.velcdn.com/images/twonezero_98/post/e2b4e47d-9a11-4df7-be13-5556a3a85764/image.png" alt=""></li>
</ul>
<table>
<thead>
<tr>
<th>패턴</th>
<th>핵심 메커니즘</th>
<th>주요 용도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chaining</strong></td>
<td>고정된 순서</td>
<td>단계별 가드레일이 필요한 복합 작업</td>
</tr>
<tr>
<td><strong>Routing</strong></td>
<td>분류 및 분기</td>
<td>전문가 모델 활용 및 비용 최적화</td>
</tr>
<tr>
<td><strong>Parallelization</strong></td>
<td>동시 실행</td>
<td>대량 작업 처리 및 속도 개선</td>
</tr>
<tr>
<td><strong>Orchestrator</strong></td>
<td>동적 할당</td>
<td>구조화되지 않은 복잡한 문제 해결</td>
</tr>
<tr>
<td><strong>Evaluator</strong></td>
<td>반복 개선</td>
<td>높은 정확도와 품질 보증</td>
</tr>
</tbody></table>
<p>단순한 챗봇을 넘어 시스템을 설계할 때, 위 패턴들을 적재적소에 조합(Composition)하는 능력이 차세대 AI 개발자의 핵심 역량이 될 것임.</p>
<hr>
<p><strong>참고</strong>: <a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic&#39;s Design Patterns for LLM Workflows</a></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Agent 구현 정리: 정적 워크플로우, 자율 에이전트 손수 구현해보기]]></title>
            <link>https://velog.io/@twonezero_98/Agent-%EA%B5%AC%ED%98%84-%EC%A0%95%EB%A6%AC-%EC%A0%95%EC%A0%81-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%90%EC%9C%A8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%86%90%EC%88%98-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/Agent-%EA%B5%AC%ED%98%84-%EC%A0%95%EB%A6%AC-%EC%A0%95%EC%A0%81-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%90%EC%9C%A8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%86%90%EC%88%98-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 22 Jan 2026 11:15:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/twonezero_98/post/d8974cf7-b640-4fdc-84c0-5e55ec293acf/image.png" alt=""></p>
<h2 id="📌-개요">📌 개요</h2>
<p>Multi-Agent 시스템에서 여러 에이전트를 조율하고 실행하는 Planning Agent를 구현했음. 이번 구현에서는 두 가지 접근 방식을 비교함:</p>
<ol>
<li><strong>PlanningAgent</strong>: 사전 정의된 순차적 워크플로우를 따르는 정적 에이전트</li>
<li><strong>AutonomousPlanningAgent</strong>: OpenAI Tool Calling을 활용하여 동적으로 도구를 선택하고 실행하는 자율 에이전트</li>
</ol>
<blockquote>
<p><strong>Key Concept: Autonomous Agent (자율 에이전트)</strong></p>
<p>사전에 정의된 규칙 대신 LLM이 스스로 계획을 수립하고 적절한 도구(Tool)를 호출하여 목표를 달성하는 시스템임. 이는 기존의 정적 워크플로우와 달리 상황에 따라 유연하게 대응할 수 있는 장점이 있음.</p>
</blockquote>
<hr>
<h2 id="🔧-구현-내용">🔧 구현 내용</h2>
<h3 id="1️⃣-planningagent-정적-워크플로우-구현">1️⃣ PlanningAgent: 정적 워크플로우 구현</h3>
<p>PlanningAgent는 세 가지 하위 에이전트를 순차적으로 실행하는 전통적인 접근 방식임:</p>
<pre><code class="language-python">class PlanningAgent(Agent):
    name = &quot;Planning Agent&quot;
    color = Agent.GREEN
    DEAL_THRESHOLD = 50

    def __init__(self, collection):
        &quot;&quot;&quot;
        Create instances of the 3 Agents that this planner coordinates across
        &quot;&quot;&quot;
        self.log(&quot;Planning Agent is initializing&quot;)
        self.scanner = ScannerAgent()
        self.ensemble = EnsembleAgent(collection)
        self.messenger = MessagingAgent()
        self.log(&quot;Planning Agent is ready&quot;)</code></pre>
<h4 id="워크플로우-실행-로직">워크플로우 실행 로직</h4>
<p><code>plan()</code> 메서드는 다음과 같은 순서로 작업을 수행함:</p>
<pre><code class="language-python">def plan(self, memory: List[str] = []) -&gt; Optional[Opportunity]:
    &quot;&quot;&quot;
    Run the full workflow:
    1. Use the ScannerAgent to find deals from RSS feeds
    2. Use the EnsembleAgent to estimate them
    3. Use the MessagingAgent to send a notification of deals
    &quot;&quot;&quot;
    self.log(&quot;Planning Agent is kicking off a run&quot;)
    selection = self.scanner.scan(memory=memory)
    if selection:
        opportunities = [self.run(deal) for deal in selection.deals[:5]]
        opportunities.sort(key=lambda opp: opp.discount, reverse=True)
        best = opportunities[0]
        self.log(
            f&quot;Planning Agent has identified the best deal has discount ${best.discount:.2f}&quot;
        )
        if best.discount &gt; self.DEAL_THRESHOLD:
            self.messenger.alert(best)
        self.log(&quot;Planning Agent has completed a run&quot;)
        return best if best.discount &gt; self.DEAL_THRESHOLD else None
    return None</code></pre>
<p><strong>워크플로우 단계:</strong></p>
<ol>
<li><strong>스캔</strong>: ScannerAgent를 통해 RSS 피드에서 딜 정보 수집</li>
<li><strong>가격 평가</strong>: 각 딜에 대해 EnsembleAgent로 실제 가치 추정</li>
<li><strong>정렬 및 선택</strong>: 할인율 기준으로 정렬하여 최고의 딜 선택</li>
<li><strong>알림</strong>: 할인율이 임계값($50)을 초과하면 MessagingAgent로 사용자에게 알림</li>
</ol>
<hr>
<h3 id="2️⃣-autonomousplanningagent-tool-calling-기반-자율-에이전트">2️⃣ AutonomousPlanningAgent: Tool Calling 기반 자율 에이전트</h3>
<p>AutonomousPlanningAgent는 OpenAI의 Tool Calling 기능을 활용하여 LLM이 스스로 도구를 선택하고 실행하도록 구현했음.</p>
<h4 id="tool-정의">Tool 정의</h4>
<p>세 가지 도구를 JSON Schema 형식으로 정의함:</p>
<pre><code class="language-python">scan_function = {
    &quot;name&quot;: &quot;scan_the_internet_for_bargains&quot;,
    &quot;description&quot;: &quot;Returns top bargains scraped from the internet along with the price each item is being offered for&quot;,
    &quot;parameters&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {},
        &quot;required&quot;: [],
        &quot;additionalProperties&quot;: False,
    },
}

estimate_function = {
    &quot;name&quot;: &quot;estimate_true_value&quot;,
    &quot;description&quot;: &quot;Given the description of an item, estimate how much it is actually worth&quot;,
    &quot;parameters&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
            &quot;description&quot;: {
                &quot;type&quot;: &quot;string&quot;,
                &quot;description&quot;: &quot;The description of the item to be estimated&quot;,
            },
        },
        &quot;required&quot;: [&quot;description&quot;],
        &quot;additionalProperties&quot;: False,
    },
}

notify_function = {
    &quot;name&quot;: &quot;notify_user_of_deal&quot;,
    &quot;description&quot;: &quot;Send the user a push notification about the single most compelling deal; only call this one time&quot;,
    &quot;parameters&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
            &quot;description&quot;: {
                &quot;type&quot;: &quot;string&quot;,
                &quot;description&quot;: &quot;The description of the item itself scraped from the internet&quot;,
            },
            &quot;deal_price&quot;: {
                &quot;type&quot;: &quot;number&quot;,
                &quot;description&quot;: &quot;The price offered by this deal scraped from the internet&quot;,
            },
            &quot;estimated_true_value&quot;: {
                &quot;type&quot;: &quot;number&quot;,
                &quot;description&quot;: &quot;The estimated actual value that this is worth&quot;,
            },
            &quot;url&quot;: {
                &quot;type&quot;: &quot;string&quot;,
                &quot;description&quot;: &quot;The URL of this deal as scraped from the internet&quot;,
            },
        },
        &quot;required&quot;: [&quot;description&quot;, &quot;deal_price&quot;, &quot;estimated_true_value&quot;, &quot;url&quot;],
        &quot;additionalProperties&quot;: False,
    },
}</code></pre>
<h4 id="tool-call-처리-메커니즘">Tool Call 처리 메커니즘</h4>
<p>LLM이 요청한 도구 호출을 실제로 실행하는 핸들러:</p>
<pre><code class="language-python">def handle_tool_call(self, message):
    &quot;&quot;&quot;
    Actually call the tools associated with this message
    &quot;&quot;&quot;
    mapping = {
        &quot;scan_the_internet_for_bargains&quot;: self.scan_the_internet_for_bargains,
        &quot;estimate_true_value&quot;: self.estimate_true_value,
        &quot;notify_user_of_deal&quot;: self.notify_user_of_deal,
    }
    results = []
    for tool_call in message.tool_calls:
        tool_name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments)
        tool = mapping.get(tool_name)
        result = tool(**arguments) if tool else &quot;&quot;
        results.append(
            {&quot;role&quot;: &quot;tool&quot;, &quot;content&quot;: result, &quot;tool_call_id&quot;: tool_call.id}
        )
    return results</code></pre>
<h4 id="자율-실행-루프">자율 실행 루프</h4>
<p>LLM이 작업을 완료할 때까지 도구 호출을 반복하는 루프:</p>
<pre><code class="language-python">def plan(self, memory: List[str] = []) -&gt; Optional[Opportunity]:
    &quot;&quot;&quot;
    Run the full workflow, providing the LLM with tools to surface scraped deals to the user
    &quot;&quot;&quot;
    self.log(&quot;Autonomous Planning Agent is kicking off a run&quot;)
    self.memory = memory
    self.opportunity = None
    messages = self.messages[:]
    done = False
    while not done:
        response = self.openai.chat.completions.create(
            model=self.MODEL, messages=messages, tools=self.get_tools()
        )
        if response.choices[0].finish_reason == &quot;tool_calls&quot;:
            message = response.choices[0].message
            results = self.handle_tool_call(message)
            messages.append(message)
            messages.extend(results)
        else:
            done = True
    reply = response.choices[0].message.content
    self.log(f&quot;Autonomous Planning Agent completed with: {reply}&quot;)
    return self.opportunity</code></pre>
<p><strong>실행 흐름:</strong></p>
<ol>
<li>LLM에게 시스템 메시지와 사용자 메시지 전달</li>
<li>LLM이 도구 호출을 요청하면 <code>handle_tool_call()</code>로 실행</li>
<li>도구 실행 결과를 메시지 히스토리에 추가</li>
<li>LLM이 &quot;OK&quot;를 반환할 때까지 반복</li>
</ol>
<hr>
<h2 id="🎯-두-접근-방식의-비교">🎯 두 접근 방식의 비교</h2>
<table>
<thead>
<tr>
<th>특성</th>
<th>PlanningAgent</th>
<th>AutonomousPlanningAgent</th>
</tr>
</thead>
<tbody><tr>
<td><strong>워크플로우</strong></td>
<td>고정된 순차 실행</td>
<td>LLM이 동적으로 결정</td>
</tr>
<tr>
<td><strong>유연성</strong></td>
<td>낮음 (코드 수정 필요)</td>
<td>높음 (프롬프트 수정으로 조정 가능)</td>
</tr>
<tr>
<td><strong>예측 가능성</strong></td>
<td>높음</td>
<td>중간 (LLM 판단에 의존)</td>
</tr>
<tr>
<td><strong>구현 복잡도</strong></td>
<td>낮음</td>
<td>중간 (Tool Calling 메커니즘 필요)</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>낮음</td>
<td>높음 (LLM 호출 횟수 증가)</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>제한적</td>
<td>우수 (새 도구 추가 용이)</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>정적 vs 동적 워크플로우 선택 기준</strong></p>
<ul>
<li><strong>정적 워크플로우</strong>: 작업 순서가 명확하고 변하지 않는 경우, 비용 효율성이 중요한 경우</li>
<li><strong>동적 워크플로우</strong>: 상황에 따라 유연한 대응이 필요한 경우, 복잡한 의사결정이 필요한 경우</li>
</ul>
</blockquote>
<hr>
<h2 id="💡-핵심-학습-내용">💡 핵심 학습 내용</h2>
<h3 id="openai-tool-calling의-동작-원리">OpenAI Tool Calling의 동작 원리</h3>
<ol>
<li><strong>도구 정의</strong>: JSON Schema 형식으로 함수 시그니처와 설명 제공</li>
<li><strong>LLM 요청</strong>: 도구 목록과 함께 LLM에 요청 전송</li>
<li><strong>도구 선택</strong>: LLM이 상황에 맞는 도구와 인자 결정</li>
<li><strong>실행 및 피드백</strong>: 도구 실행 결과를 LLM에 다시 전달</li>
<li><strong>반복</strong>: LLM이 작업 완료를 선언할 때까지 반복</li>
</ol>
<h3 id="자율-에이전트-설계-시-고려사항">자율 에이전트 설계 시 고려사항</h3>
<pre><code class="language-python">system_message = &quot;You find great deals on bargain products using your tools, and notify the user of the best bargain.&quot;
user_message = &quot;&quot;&quot;
First, use your tool to scan the internet for bargain deals. Then for each deal, use your tool to estimate its true value.
Then pick the single most compelling deal where the price is much lower than the estimated true value, and use your tool to notify the user.
Then just reply OK to indicate success.
&quot;&quot;&quot;</code></pre>
<blockquote>
<p><strong>효과적인 프롬프트 작성</strong></p>
<ul>
<li>명확한 작업 순서 제시</li>
<li>종료 조건 명시 (&quot;reply OK to indicate success&quot;)</li>
<li>도구 사용 목적과 제약사항 설명 (&quot;only call this one time&quot;)</li>
</ul>
</blockquote>
<hr>
<h2 id="🧪-실험-및-검증">🧪 실험 및 검증</h2>
<p>Tool Calling 메커니즘을 단계별로 실험했음:</p>
<ol>
<li><strong>가짜 함수로 프로토타입 구현</strong>: 실제 에이전트 없이 Tool Calling 흐름 검증</li>
<li><strong>도구 스키마 정의 및 테스트</strong>: JSON Schema 형식의 도구 정의 검증</li>
<li><strong>실제 에이전트 통합</strong>: ScannerAgent, EnsembleAgent, MessagingAgent와 통합</li>
</ol>
<p>실행 결과:</p>
<pre><code>Fake function to scan the internet - this returns a hardcoded set of deals
Fake function to estimating true value of The Hisense R6 Serie... - this always returns $300
Fake function to estimating true value of The Poly Studio P21 ... - this always returns $300
Fake function to estimating true value of The Lenovo IdeaPad S... - this always returns $300
Fake function to estimating true value of The Dell G15 gaming ... - this always returns $300
Fake function to notify user of The Poly Studio P21 ... which costs 30 and estimate is 300</code></pre><p><strong>LLM이 자율적으로</strong>:</p>
<ol>
<li>스캔 도구 호출</li>
<li>각 deal에 대해 가치 평가 도구 호출</li>
<li>최고의 deal 선택 및 알림 도구 호출</li>
<li>&quot;OK&quot; 응답으로 작업 완료</li>
</ol>
<hr>
<h2 id="📝-결론">📝 결론</h2>
<p>이번 구현을 통해 정적 워크플로우와 자율 에이전트의 장단점을 명확히 이해했음. </p>
<p><strong>PlanningAgent</strong>는 예측 가능하고 비용 효율적이지만 유연성이 제한적임. 반면 <strong>AutonomousPlanningAgent</strong>는 OpenAI Tool Calling을 활용하여 상황에 맞게 동적으로 대응할 수 있지만, LLM 호출 비용과 예측 불가능성이라는 트레이드오프가 존재함.</p>
<p>실제 프로덕션 환경에서는 두 접근 방식을 하이브리드로 결합하는 것이 효과적일 수 있음:</p>
<ul>
<li>핵심 워크플로우는 정적으로 구현</li>
<li>복잡한 의사결정이 필요한 부분만 자율 에이전트 활용</li>
</ul>
<blockquote>
<p><strong>자율 에이전트 구현 시 주의사항</strong></p>
<ul>
<li>무한 루프 방지를 위한 최대 반복 횟수 설정 필요</li>
<li>도구 호출 비용 모니터링 필수</li>
<li>중요한 작업은 사람의 승인 단계 추가 권장</li>
</ul>
</blockquote>
<hr>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<ul>
<li><a href="https://platform.openai.com/docs/guides/function-calling">OpenAI Function Calling Documentation</a></li>
<li><a href="https://developers.googleblog.com/developers-guide-to-multi-agent-patterns-in-adk/">Multi-Agent System Design Patterns</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[파인튜닝 학습 정리 ]]></title>
            <link>https://velog.io/@twonezero_98/%ED%8C%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D-%ED%95%99%EC%8A%B5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@twonezero_98/%ED%8C%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D-%ED%95%99%EC%8A%B5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 17 Jan 2026 12:36:50 GMT</pubDate>
            <description><![CDATA[<h2 id="1-qlora-효율적인-모델-학습의-핵심-🧠">1. QLoRA: 효율적인 모델 학습의 핵심 🧠</h2>
<p>거대 모델을 전체 튜닝하기에는 자원 소모가 너무 큼. 이를 해결하기 위해 <strong>QLoRA(Quantized Low-Rank Adaptation)</strong>를 도입함.</p>
<h3 id="💡-핵심-메커니즘">💡 핵심 메커니즘</h3>
<ul>
<li><p><strong>Freeze:</strong> 기존 모델의 가중치($W_0$)를 4비트 양자화 상태로 고정하여 메모리 점유율을 최소화함.</p>
</li>
<li><p><strong>Adapter:</strong> 별도의 작은 행렬($lora_A$, $lora_B$)만 추가하여 이 부분만 학습함.</p>
</li>
<li><p>수학적 원리:</p>
<p>  $$y = W_0x + \Delta Wx = W_0x + (B \cdot A)x$$</p>
<p>  여기서 $A$와 $B$는 저차원(Rank) 행렬로, 학습 파라미터를 수백만 개에서 수십만 개 수준으로 줄여줌.</p>
</li>
</ul>
<h3 id="🛠️-레이어-구조-q_proj-예시">🛠️ 레이어 구조 (q_proj 예시)</h3>
<pre><code>  (q_proj): lora.Linear4bit(
  (base_layer): Linear4bit(in_features=3072, out_features=3072, bias=False)
  (lora_A): Linear(in_features=3072, out_features=32, bias=False)
  (lora_B): Linear(in_features=32, out_features=3072, bias=False)
)</code></pre><ul>
<li><strong>Rank(r=32):</strong> 원래 $3072 \times 3072$ 크기의 변화를 학습하는 대신, 중간에 32차원을 둔 두 개의 행렬로 분해함. 약 940만 개의 파라미터를 19만 개로 압축한 효과임.</li>
</ul>
<hr>
<h2 id="2-모델-구조-분석-attention-vs-mlp-🔍">2. 모델 구조 분석: Attention vs MLP 🔍</h2>
<p>LLM의 내부는 크게 &#39;소통&#39;을 담당하는 Attention과 &#39;지식&#39;을 담당하는 MLP로 나뉨.</p>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>역할</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Attention</strong></td>
<td>단어 간의 관계(Context) 파악</td>
</tr>
<tr>
<td><strong>MLP</strong></td>
<td>개별 토큰의 의미 분석 및 지식 추출</td>
</tr>
</tbody></table>
<h3 id="llama-3의-swiglu-구조">Llama 3의 SwiGLU 구조</h3>
<p>Llama 3 모델은 성능 향상을 위해 개선된 MLP 구조를 사용함.</p>
<ul>
<li><strong>gate_proj / up_proj:</strong> 입력을 8192차원의 고차원으로 확장하여 복잡한 특징을 추출함.</li>
<li><strong>SiLU 활성화 함수:</strong> 비선형성을 도입하여 복잡한 논리 구조를 학습함.</li>
<li><strong>down_proj:</strong> 처리된 정보를 다시 3072차원으로 압축하여 다음 레이어로 전달함.</li>
</ul>
<hr>
<h2 id="3-학습-프로세스와-하이퍼파라미터-⚙️">3. 학습 프로세스와 하이퍼파라미터 ⚙️</h2>
<p>모델 학습은 <strong>순전파 → 손실 계산 → 역전파 → 최적화</strong>의 4단계 사이클을 반복함.</p>
<h3 id="🔄-training-4-steps">🔄 Training 4-Steps</h3>
<ol>
<li><strong>Forward Pass:</strong> 입력 데이터를 통해 다음 토큰을 예측함.</li>
<li><strong>Loss Calculation:</strong> 예측값과 정답 간의 차이를 계산함.<ul>
<li>$Loss = -\log(P_{target})$ (정답 확률에 음의 로그를 취함)</li>
</ul>
</li>
<li><strong>Backward Pass:</strong> 오차를 역으로 전달하며 각 파라미터의 기여도(Gradient)를 계산함.</li>
<li><strong>Optimization:</strong> <strong>Paged AdamW</strong> 옵티마이저를 사용해 가중치를 미세하게 조정함.</li>
</ol>
<h3 id="📊-주요-설정값">📊 주요 설정값</h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>T4 GPU(Lite version)</strong></th>
<th><strong>A100 GPU(Full version)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Epochs</strong></td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td><strong>Batch Size</strong></td>
<td>32</td>
<td>256</td>
</tr>
<tr>
<td><strong>LoRA Rank (r)</strong></td>
<td>32</td>
<td>256</td>
</tr>
<tr>
<td><strong>Learning Rate</strong></td>
<td>1e-4 (Cosine)</td>
<td>1e-4 (Cosine)</td>
</tr>
</tbody></table>
<blockquote>
<p>T4 환경과 A100 환경에서 데이터셋의 크기가 다름.</p>
</blockquote>
<hr>
<h2 id="4-훈련-모니터링">4. 훈련 모니터링</h2>
<p>학습 도중 특정 시점에서 성능이 급격히 저하되는 현상을 포착함.
<a href="https://wandb.ai/home">Weights and Biases</a> 를 통해 모니터링.</p>
<blockquote>
<p>⚠️ 과적합(Overfitting) 탐지</p>
<p>아래 그래프를 보면 <strong>6,200 Step(약 2-Epoch)</strong> 지점에서 <code>train/loss</code>는 급격히 떨어지는 반면, <code>eval/loss</code>는 급격히 튀어 오르는 것을 확인할 수 있음. 이는 모델이 훈련 데이터의 노이즈까지 암기하기 시작했다는 강력한 신호임.<img src="https://velog.velcdn.com/images/twonezero_98/post/5c0eb1f8-58b4-4793-b452-6371e66e0bd0/image.png" alt=""></p>
</blockquote>
<h3 id="✅-해결-방안-및-개선-아이디어">✅ 해결 방안 및 개선 아이디어</h3>
<ol>
<li><strong>Weight Decay 상향:</strong> 가중치의 제곱 합을 손실 함수에 더해 모델의 복잡도를 강제로 제한함. 가중치를 작게 유지하여 &#39;부드러운&#39; 함수를 만듦.</li>
<li><strong>Learning Rate 하향:</strong> 보폭을 줄여 손실 함수의 최저점에 더 세밀하게 접근함.</li>
<li><strong>Cosine Scheduler 최적화:</strong> 후반부 학습률을 더 낮게 설정하여 안착을 도움.</li>
<li><strong>Dropout 활용:</strong> 학습 시 뉴런을 무작위로 비활성화하여 특정 경로에 의존하지 않도록 함 (현재 0.1 설정 유지).</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring-AI로 RAG 챗봇 만들어보기]]></title>
            <link>https://velog.io/@twonezero_98/Spring-AI%EB%A1%9C-RAG-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/Spring-AI%EB%A1%9C-RAG-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 09 Jun 2025 09:54:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>SpringAI 를 활용하여 문서를 기반으로 답변하는 챗봇을 만드는 과정 정리.</p>
</blockquote>
<hr>
<h2 id="🧠-rag-구조-간단-정리">🧠 RAG 구조 간단 정리</h2>
<p>RAG는 크게 두 단계로 나뉘어져 있다.</p>
<ol>
<li><strong>Retrieval (검색)</strong>: 질문과 관련된 문서를 벡터 검색을 통해 가져옴</li>
<li><strong>Generation (생성)</strong>: 검색된 문서를 기반으로 답변 생성</li>
</ol>
<p>Spring-AI는 이걸 아주 간단하게 구성할 수 있도록 <code>DocumentRetriever</code>, <code>EmbeddingModel</code>, <code>VectorStore</code> 같은 컴포넌트 및 상위 인터페이스를 제공한다.</p>
<ul>
<li>아래 그림은 기본적은 RAG 의 흐름을 볼 수 있는 예시이다. 그림에서는 llamaindex, langchain 을 이용했지만, 그 구조는 SpringAI 와 같다.
<img src="https://velog.velcdn.com/images/twonezero_98/post/116b7ab2-c596-437e-b178-88c008ec8b67/image.png" alt=""></li>
</ul>
<hr>
<h2 id="🔧-준비-사항">🔧 준비 사항</h2>
<h3 id="0-pgvector-설치">0. PgVector 설치</h3>
<ul>
<li>Local 환경에 Pgvector 를 설치하기 위해서는 Postgresql 과 pgvector와 관련된 확장들도 설치해야 하므로 복잡하다. 따라서 <code>Docker</code> 를 통해 설치하면 편리하다.</li>
<li>참고: <a href="https://docs.spring.io/spring-ai/reference/api/vectordbs/pgvector.html#_prerequisites">Pgvector for SpringAI</a></li>
</ul>
<p><strong>docker-compose.yml</strong></p>
<pre><code class="language-yml">version: &#39;3.8&#39;
services:
  pgvector-db:
    image: pgvector/pgvector:pg16 # PostgreSQL with pgvector support
    container_name: pgvector-db
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - ./postgres/data:/var/lib/postgresql/data</code></pre>
<h3 id="1-의존성-추가">1. 의존성 추가</h3>
<pre><code class="language-groovy">repositories {
  mavenCentral()
}

ext {
  set(&#39;springAiVersion&#39;, &quot;1.0.0&quot;)
}

dependencies {
  implementation &#39;org.springframework.ai:spring-ai-advisors-vector-store&#39;
  implementation &#39;org.springframework.ai:spring-ai-starter-model-ollama&#39;
  implementation &#39;org.springframework.ai:spring-ai-starter-vector-store-pgvector&#39;
}

dependencyManagement {
  imports {
    mavenBom &quot;org.springframework.ai:spring-ai-bom:${springAiVersion}&quot;
  }
}</code></pre>
<h3 id="2-applicationyml-설정">2. application.yml 설정</h3>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  ai:
      ollama:
      base-url: http://localhost:11434  # Ollama 서버 URL
      chat:
        options:
          model: gemma3:4b # 사용할 chat 모델
      embedding:
        options:
          model:  # RAG 위한 임베딩 모델
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        # dimensions: 1536 &lt;- 없으면 각 임베딩 모델에 맞는 적절한 dimensions 로 적용됨
        max-document-batch-size: 10000 # Optional: 각 배치 마다 처리될 수 있는 최대 document 수</code></pre>
<hr>
<h2 id="📄-문서-임베딩--저장">📄 문서 임베딩 &amp; 저장</h2>
<p>SpringAI 에서는 검색 증강 생성 패턴을 구현하기 위한, ETL(Extract, Transform, and Load) 데이터 엔지니어링과 관련된 컴포넌트 및 인터페이스를 제공한다.</p>
<blockquote>
<ul>
<li>문서 데이터를 로드하여 가공 후 Vector DB 에 저장하는 흐름
<img src="https://velog.velcdn.com/images/twonezero_98/post/72265ba0-105f-4837-8668-a8fbd399b8cd/image.png" alt=""></li>
</ul>
</blockquote>
<p>위 그림과 같이 ETL 을 위한 인터페이스는 크게 <code>DocumentReader</code>, <code>DocumentTransformer</code>, <code>DocumentWriter</code> 가 존재하고, 아래의 클래스 다이어그램은 해당 인터페이스의 구현이 나와 있다.
<img src="https://velog.velcdn.com/images/twonezero_98/post/f33357f6-e829-4f46-99f7-7d73345dde91/image.png" alt=""></p>
<p>각 구현체를 통해, 문서의 특성 및 상황을 고려하여 ETL 파이프라인을 구성할 수 있다. 또한, 각 문서 특성에 맞는 의존성을 설치하여야 한다. 예를 들어, pdf 문서를 로드하기 위해서는 아래의 의존성을 추가하면 된다. </p>
<pre><code>dependencies {
    implementation &#39;org.springframework.ai:spring-ai-pdf-document-reader&#39;
}</code></pre><ul>
<li>pdf를 로드하고 Vector DB 에 저장하는 예시</li>
</ul>
<pre><code class="language-java">@Component
public class MyPagePdfDocumentReader {
    private final VectorStore vectorStore;

    void getDocsFromPdf() {

        PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(&quot;classpath:/sample1.pdf&quot;,
                PdfDocumentReaderConfig.builder()
                    .withPageTopMargin(0)
                    .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                        .withNumberOfTopTextLinesToDelete(0)
                        .build())
                    .withPagesPerDocument(1)
                    .build());

        List&lt;Document&gt; docs = pdfReader.get();

        vectorStore.add(docs);
    }

}</code></pre>
<hr>
<h2 id="💬-advisor-api-for-rag">💬 Advisor API for RAG</h2>
<p>SpringAI 는 RAG 패턴을 쉽게 적용하기 위해 Advisor API를 사용한다.
RAG 패턴은 <strong>사용자 프롬프트와 문서의 유사성을 평가하고 비슷한 문서 chunk 를 조합하여 사용자 프롬프트에 추가하는 방식</strong>이다. 이를 위해 <code>SearchRequest</code> 를 사용하여 문서 필터 및 유사도 옵션을 설정 가능하다.</p>
<h3 id="questionansweradvisor">QuestionAnswerAdvisor</h3>
<ul>
<li><p>모든 문서에 대한 유사성 검사</p>
<pre><code class="language-java">ChatResponse response = ChatClient.builder(chatModel)
      .build().prompt()
      .advisors(new QuestionAnswerAdvisor(vectorStore))
      .user(userText)
      .call()
      .chatResponse();</code></pre>
</li>
<li><p><code>SearchRequest</code> 를 사용</p>
</li>
</ul>
<blockquote>
<p>유사도 임계값을 <em>0.8</em> 으로 설정하고 검색된 상위 6개 결과를 반환</p>
</blockquote>
<pre><code class="language-java">var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build())
        .build();</code></pre>
<h3 id="chatclient-에-advisor-추가">ChatClient 에 Advisor 추가</h3>
<p>ChatClient 의 기본 템플릿을 구성하여 Bean 을 설정할 수도 있다.
예를 들어, <code>QuestionAnswerAdvisor</code> 를 기본 Advisor 로 사용하려면 아래와 같이 작성할 수 있다. </p>
<pre><code class="language-java">    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem(persona, StandardCharsets.UTF_8) //llm 의 페르소나, 기본 지시사항을 설정
                .defaultAdvisors(QuestionAnswerAdvisor
                    .builder(vectorStore)
                      .searchRequest(SearchRequest.builder().build())
                    .build())
                .build();
    }</code></pre>
<blockquote>
<p>위와 같이 ChatClient 에 Advisor 를 추가하여 쉽게 QA 챗봇을 구현 가능하다.</p>
</blockquote>
<hr>
<h2 id="📌-마무리">📌 마무리</h2>
<p>지금까지 문서를 기반으로 답변하는 챗봇, 즉 RAG 시스템을 간단하게 구성해봤다. 실제 서비스에 적용하려면 문서 전처리, 인덱싱 전략, 프롬프트 튜닝 등이 더 필요하지만, 이 정도만 해도 기본 기능은 충분히 구현 가능하다.</p>
<p>다음 글에서는 각 <strong>문서 특성에 맞게 문서 처리 파이프라인을 고도화</strong>하고, <strong>VectorStore 에 문서 chunk 를 저장할 때 Metadata 추가</strong>하는 내용을 정리할 것이다.</p>
<hr>
<h2 id="⏭️-todo">⏭️ TODO</h2>
<ul>
<li>📂 문서 파이프라인 고도화: 분할, 요약, 필터링 전략</li>
<li>🧩 Modular-RAG 구조 설계: RAG 와 관련된 처리를 모듈화하여 쉽게 파이프라인 구성하기</li>
<li>🧠 LLM 응답 향상을 위한 페르소나 및 시스템 프롬프트 작성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring-AI로 LLM 사용해보기]]></title>
            <link>https://velog.io/@twonezero_98/Spring-AI%EB%A1%9C-LLM-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/Spring-AI%EB%A1%9C-LLM-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 07 Jun 2025 08:42:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>최근 LLM을 개발하고 활용하는 추세가 빠르게 확산되고 있다. Spring AI는 LangChain과 같이 이러한 LLM을 보다 쉽게 사용하도록 돕는 라이브러리. 이러한 흐름과 함께, Spring Boot 환경에서 Spring AI를 활용하면 LLM 기능을 간편하게 통합하여 개발할 수 있을 것 같아 정리 사용 과정을 정리해보려고 한다.</p>
</blockquote>
<h2 id="🔍-목표">🔍 목표</h2>
<ul>
<li>Ollama 로컬 모델을 활용하여 Spring-AI 기반의 간단한 챗봇 구현</li>
<li>Spring Boot 프로젝트에서 LLM을 연동하는 기초 흐름 이해</li>
</ul>
<hr>
<h2 id="🧠-ollama란">🧠 Ollama란?</h2>
<p><a href="https://ollama.com">Ollama</a>는 로컬에서 LLM 모델(Mistral, LLaMA, Gemma 등)을 쉽게 실행할 수 있도록 도와주는 플랫폼이다. Docker를 통해 설치가 가능하며 RESTful API를 통해 다양한 언어에서 접근할 수 있는 것이 장점. 개발 환경에서 빠르게 테스트하고 배포 없이 모델을 활용할 수 있다는 점에서 RAG 시스템이나 LLM 서비스 개발에 유용하다.</p>
<hr>
<h2 id="⚙️-환경-설정">⚙️ 환경 설정</h2>
<h3 id="1-ollama-설치">1. Ollama 설치</h3>
<ul>
<li>Ollama 설치는 공식 홈페이지에서 간편하게 설치가 가능하고, Mac 유저또는 Windows는 git bash를 통해 아래와 같이 터미널에서 설치 후 사용해 볼 수 있다. 자세한 건 Ollama 공식홈페이지 참고</li>
</ul>
<pre><code class="language-bash">curl -fsSL https://ollama.com/install.sh | sh
ollama run mistral</code></pre>
<h3 id="2-spring-boot-프로젝트-설정">2. Spring Boot 프로젝트 설정</h3>
<ul>
<li>Spring-AI 1.0.0 이 release 되었으므로 사용하는 것이 좋다. Spring initailizer 에서 간편하게 의존성 추가할 수 있다. 다른 필요한 의존성들 또한 Spring initializer 에서 추가 가능하다.</li>
<li><strong>주의할 점</strong>: SpringAI 1.0.0 이전의 버전들은 라이브러리의 모듈 이름 등의 변화가 있으니 잘 체크해보고 넣어야 함.</li>
</ul>
<p><strong>build.gradle</strong> 또는 <strong>pom.xml</strong>에 Spring-AI 의존성 추가:</p>
<pre><code class="language-groovy">
ext {
  set(&#39;springAiVersion&#39;, &quot;1.0.0&quot;)
}

dependencies {
  implementation &#39;org.springframework.ai:spring-ai-starter-model-ollama&#39;
}

dependencyManagement {
  imports {
    mavenBom &quot;org.springframework.ai:spring-ai-bom:${springAiVersion}&quot;
  }
}</code></pre>
<h4 id="ollama-를-사용하기-위한-프로젝트-설정">Ollama 를 사용하기 위한 프로젝트 설정</h4>
<ul>
<li>공식 홈페이지를 참고하면 각 LLM 서빙 프레임워크 및 API 제공자에 맞게 설정 및 사용 방법이 나와 있다.
참고: <a href="https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html">Ollama Chat</a></li>
</ul>
<p><strong>application.yml 설정</strong>:</p>
<pre><code class="language-yaml">spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: gemma3:4b
          temperature: 0.7
          # 외 다른 옵션도 설정 가능 없으면 기본 값</code></pre>
<p>모델은 ollama에서 공식으로 제공하는 gemma3:<code>4b</code> 를 사용하였다.</p>
<blockquote>
</blockquote>
<p><code>4b</code> 는 LLM 의 파라미터 수를 의미함.
<a href="https://ollama.com/library/gemma3/tags">ollama gemma3</a> 페이지를 참고하여 여러 파라미터로 된 모델 및 양자화된 버전까지 선택하여 사용할 수 있다.</p>
<blockquote>
</blockquote>
<hr>
<h2 id="💬-간단한-chat-api-구현">💬 간단한 Chat API 구현</h2>
<p> Spring AI 는 auto-configuration 이 제공되므로 의존성을 추가하고 간단한 application.yml 설정을 하면 바로 사용할 수 있다.</p>
<h3 id="1-chatcontroller-에서-바로-테스트">1. ChatController 에서 바로 테스트</h3>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/chat&quot;)
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @PostMapping
    public ResponseEntity&lt;String&gt; chat(@RequestBody String message) {
        ChatResponse chatResponse = chatClient.prompt()
                .user(&quot;Tell me a joke&quot;)
                .call()
                .chatResponse();

        String res = chatResposne
                .getResult().getOutput().getText();

        return ResponseEntity.ok(res);

    }
}</code></pre>
<h3 id="2-chatclient-인터페이스-활용">2. ChatClient 인터페이스 활용</h3>
<p>Spring-AI에서는 <code>ChatClient</code>라는 추상화를 제공하므로 이를 이용하면 용이하다. 위의 Controller에서 처럼 제공하는 API 를 통해 LLM 서버와 통신이 가능하다. 아래는 공식문서 내용을 발췌한 것이다.</p>
<blockquote>
</blockquote>
<h3 id="call-return-values">call() return values</h3>
<p>After specifying the call() method on ChatClient, there are a few different options for the response type.</p>
<blockquote>
</blockquote>
<ul>
<li><strong>String content()</strong>: returns the String content of the response<blockquote>
</blockquote>
</li>
<li><strong>ChatResponse chatResponse()</strong>: returns the ChatResponse object that contains multiple generations and also metadata about the response, for example how many token were used to create the response.<blockquote>
</blockquote>
</li>
<li><strong>ChatClientResponse chatClientResponse()</strong>: returns a ChatClientResponse object that contains the ChatResponse object and the ChatClient execution context, giving you access to additional data used during the execution of advisors (e.g. the relevant documents retrieved in a RAG flow).<blockquote>
</blockquote>
</li>
<li><strong>entity()</strong> to return a Java type<blockquote>
</blockquote>
</li>
<li><strong>entity(ParameterizedTypeReference<T> type)</strong>: used to return a Collection of entity types.<blockquote>
</blockquote>
</li>
<li><strong>entity(Class<T> type)</strong>: used to return a specific entity type.<blockquote>
</blockquote>
</li>
<li><strong>entity(StructuredOutputConverter<T> structuredOutputConverter)</strong>: used to specify an instance of a StructuredOutputConverter to convert a String to an entity type.<blockquote>
</blockquote>
You can also invoke the stream() method instead of call().<blockquote>
</blockquote>
</li>
</ul>
<hr>
<h2 id="✅-테스트">✅ 테스트</h2>
<pre><code class="language-bash">curl -X POST http://localhost:8080/chat -H &quot;Content-Type: text/plain&quot; -d &quot;안녕!&quot;</code></pre>
<h3 id="응답-예시">응답 예시</h3>
<pre><code class="language-json">&quot;안녕하세요! 무엇을 도와드릴까요?&quot;</code></pre>
<hr>
<h2 id="📌-마무리">📌 마무리</h2>
<p>이번 글에서는 Spring-AI와 Ollama를 이용해 가장 기본적인 챗봇 기능이 되는 지만 체크해 보았다. 다음 글에서는 Postgresql 에서 확장적으로 제공하는 PgVector DB 를 활용하여 데이터를 임베딩하고 RAG 를 구현하는 방법을 정리할 것이다.</p>
<hr>
<h2 id="🗓️-다음-글-예고">🗓️ 다음 글 예고</h2>
<ul>
<li>📦 PgVector와 벡터 DB 연동하기</li>
<li>🔍 문서 기반 QnA 시스템: RAG 기본 개념 및 구현</li>
<li>📂 Document ETL Pipeline 설계</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringSecurity - Method 단위 인가]]></title>
            <link>https://velog.io/@twonezero_98/SpringSecurity-Method-%EB%8B%A8%EC%9C%84-%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@twonezero_98/SpringSecurity-Method-%EB%8B%A8%EC%9C%84-%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 20 Nov 2024 09:39:57 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-서비스-코드에서의-유저-검증">기존 서비스 코드에서의 유저 검증</h2>
<p>수정이나 삭제 등의 비즈니스 로직을 구현할 때, 엔티티의 소유자를 확인하고자 하는 로직을 작성해야할 때가 있습니다.
이런 경우에 서비스 코드마다 유저가 소유자인지 확인하는 코드를 써야 합니다.
아래에 보이는 코드는 짧을 지 모르지만, 서비스 코드마다 중복된 검증 로직이 들어가는 것은 비효율적일 수 있습니다.</p>
<pre><code class="language-java">// diary fetched ...
if (!diary.getUserAccount().getId().equals(userId)) {
            throw new CustomBaseException(DIARY_NOT_OWNER);
}
// 이후 비즈니스 로직</code></pre>
<h2 id="enablemethodsecurity-활용">@EnableMethodSecurity 활용</h2>
<p>Spring Security 에서는 AOP 개념을 활용한 Method 단위 Security 전략을 구현할 수 있습니다.</p>
<blockquote>
<p>MethodSecurity 를 이용하기 위해서는 SecurityConfig 와 <code>@EnableMethodSecurity</code> 어노테이션을 선언한 Configuration 과 <code>MethodSecurityExpressionHandler</code> 이 필요합니다.</p>
</blockquote>
<h3 id="기본적인-securityconfig">기본적인 SecurityConfig</h3>
<pre><code class="language-java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .cors(cors -&gt; cors
                        .configurationSource(corsConfigurationSource())
                )
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(
                        auth -&gt; auth.anyRequest().permitAll()
                )
                .build();
    }


    //따로 정의한 AuthConfig 클래스 내용 일부
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(authProvider);
    }
}</code></pre>
<blockquote>
<p>Spring Security 5.7 이후 부터는 WebSecurityConfigurerAdapter 를 상속받아 구현하지 않고 SecurityFilterChain 이라는 메소드 체인 형식의 코드를 구현하여 빈으로 주입하는 방식으로 이루어 집니다.</p>
<p>AuthenticationManager 는 useDetailService와 passwordEncoder를 주입받고, PrioviderManager 를 반환함으로써 login 시나 다른 인증관련 로직 시 Security Context 에 접근해 user 정보를 저장하거나 관리 할 수 있습니다.</p>
</blockquote>
<h3 id="methodsecurityconfig">MethodSecurityConfig</h3>
<pre><code class="language-java">import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@RequiredArgsConstructor
@EnableMethodSecurity
@Configuration
public class MethodSecurityConfig {
    private final CustomPermissionEvaluator customPermissionEvaluator;

    @Bean
    public MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(customPermissionEvaluator);
        return expressionHandler;
    }
}</code></pre>
<p>위 클래스에서 중요한 것은 <code>@EnableMethodSecurity</code> 와 <code>MethodSecurityExpressionHandler</code> 를 구현하고 등록하는 것입니다.</p>
<h3 id="custompermissionevaluator">CustomPermissionEvaluator</h3>
<pre><code class="language-java">import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

import static com.bodytok.healthdiary.exepction.CustomError.*;

@Slf4j
@RequiredArgsConstructor
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    private final DiaryRepository diaryRepository;

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // Security Context 에 존재하는 유저 정보 가져오기
        CustomUserDetails userPrincipal = (CustomUserDetails) authentication.getPrincipal();
        TargetType type = TargetType.fromString(targetType);
        // targetId 가 중요하다.
        return switch (type) {
            case DIARY -&gt; {
                Diary diary = diaryRepository.findById((Long) targetId)
                        .orElseThrow(() -&gt; new CustomBaseException(DIARY_NOT_FOUND));
                yield diary.getUserAccount().getId().equals(userPrincipal.getId());
            }
            case COMMENT -&gt; {
                Comment comment = commentRepository.findById((Long) targetId)
                        .orElseThrow(() -&gt; new CustomBaseException(COMMENT_NOT_FOUND));
                yield comment.getUserAccount().getId().equals(userPrincipal.getId());
            }
            default -&gt; {
                log.error(&quot;[인가 실패] 사용자 소유가 아닙니다.&quot;);
                yield false;
            }
        };
    }

    public enum TargetType {
        DIARY,
        COMMENT;

        public static TargetType fromString(String targetType) {
            try {
                return TargetType.valueOf(targetType.toUpperCase());
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(&quot;Invalid target type: &quot; + targetType);
            }
        }
    }
}</code></pre>
<blockquote>
<p><code>PermissionEvaluator</code> 을 구현하는 <code>CustomPermissionEvaluator</code> 를 작성합니다.</p>
<p>구현해야하는 함수인 시그니처가 다른 두 개의 hasPermission 을 이용해 boolean 값을 리턴함으로서 인가를 수행할 수 있습니다.
위 함수에서 중요한 인자는 <strong>targetId, targetType</strong> 입니다.</p>
</blockquote>
<ul>
<li>CustomUserDetails 는 UserDetails 를 구현하여 작성한 인증 클래스입니다.</li>
<li>작성된 로직은 Security Context 에 존재하는 유저의 정보를 가져와 현재 조회하는 엔티티의 유저 아이디와 비교해서 소유자인지 검증하는 것입니다.</li>
</ul>
<p>아래는 <code>@PreAuthorize()</code> 를 활용한, 컨트롤러에서의 메소드 단위 보안 예시 코드입니다.</p>
<pre><code class="language-java">    @PreAuthorize(&quot;hasPermission(#diaryId, &#39;DIARY&#39;, &#39;UPDATE&#39;)&quot;)
    @PutMapping(&quot;/{diaryId}&quot;) 
    @Operation(summary = &quot;다이어리 수정 - 이미지 먼저 저장 or 삭제 후 진행&quot;) // Swagger-ui 를 위한 어노테이션임
    public ResponseEntity&lt;Void&gt; updateDiary(
            @PathVariable(name = &quot;diaryId&quot;) Long diaryId,
            @RequestBody DiaryUpdate request,
            @AuthenticationPrincipal CustomUserDetails userDetails
    ) {
        ... request 를 dto 로 변환 후 서비스로 넘기는 로직
    }</code></pre>
<ul>
<li>hasPermission( #diaryId, &#39;DIARY&#39;, &#39;UPDATE&#39; ) 의 인자들은 순서대로, 위에서 봤던 targetId, targetType, permission 입니다.</li>
</ul>
<blockquote>
<p><code>@PreAuthorization</code> 안의 사용된 문법은 SpEL 이라고 하여 Spring 에서 사용하는 표현식입니다.
<a href="https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html">참고 링크</a></p>
</blockquote>
<h3 id="debug">Debug</h3>
<p>디버깅 시 아래와 같이 targetId 와 targetType 이 잘 들어간 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/twonezero_98/post/8de138d6-2a05-44f2-9f67-9c6c774e4bab/image.png" alt=""></p>
<h2 id="결론">결론</h2>
<p>SpringSecurity 를 활용하여 메소드 단위의 인가를 AOP 기반으로 하는 코드를 구현해 보았습니다. 이를 더 잘 사용하기 위해서는 AOP 에 대한 이해가 더욱 필요할 것 같습니다.
<a href="https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#method-security-architecture">Method Security - SpringSecurity</a></p>
<h3 id="참고">참고</h3>
<p><strong>IntelliJ 로 개발 시 주의</strong></p>
<p>위와 같이 설정하면 잘 돼야 할텐데 계속해서 targetId 가 들어가지 않는 경우가 있습니다.
그런 상황이라면,
<strong>settings -&gt; Build Tools -&gt; Gradle</strong> 에서 <code>Intellij</code> 로 빌드하지 말고 <code>Gradle</code> 로 빌드를 하면 해결할 수 있습니다.</p>
<blockquote>
<p>Spring Boot 3.2 이상인가에서 <code>@PathVariable</code> 에 들어온 타입을 Serializable 로 읽지 않는 것 같습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[멀티모듈에서의 Dockerfile 최적화하기]]></title>
            <link>https://velog.io/@twonezero_98/%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88%EC%97%90%EC%84%9C%EC%9D%98-Dockerfile-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88%EC%97%90%EC%84%9C%EC%9D%98-Dockerfile-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 16 Nov 2024 06:33:11 GMT</pubDate>
            <description><![CDATA[<p>기존 프로젝트에서 작성했던 Dockerfile을 통해 이미지를 빌드하면 속도 저하 및 많은 용량에 대한 문제가 있어, 그에 대한 해결을 공유하고자 합니다.</p>
<h1 id="프로젝트-구성">프로젝트 구성</h1>
<p>프로젝트에 대한 구성은 아래의 글에서 확인할 수 있습니다.</p>
<ul>
<li><a href="https://velog.io/@twonezero_98/Github-Actions-Docker-EC2-%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">[Github Actions + Docker + EC2] 로 배포 자동화 구성하기</a></li>
</ul>
<h1 id="기존-dockerfile">기존 Dockerfile</h1>
<p>아래는 기존의 Dockerfile 입니다. 루트 컨텍스트에서 Dockerfile 의 빌드가 실행되게 하여, 각 서비스 모듈이 의존하는 다른 모듈들을 참조하기 위해 모든 파일을 복사하여 빌드를 수행합니다.</p>
<pre><code class="language-dockerfile"># Stage 1: Build the application
FROM gradle:8.10.1-jdk17 AS build

WORKDIR /app

# ARG로 전달된 파일 디렉터리 설정
ARG FILE_DIRECTORY

# 파일 복사
COPY $FILE_DIRECTORY /app

# Reservation 모듈만 빌드
RUN ./gradlew :reservation:clean :reservation:build -x test --no-daemon

FROM openjdk:17-jdk-slim

COPY --from=build /app/reservation/build/libs/*SNAPSHOT.jar /app.jar

# JAR 파일 실행
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</code></pre>
<h2 id="문제점">문제점</h2>
<p>현재 Dockerfile 의 문제점은 아래와 같습니다.</p>
<p><strong>1. 모든 파일을 복사</strong></p>
<ul>
<li><code>FILE_DIRECTORY</code> 는 루트 컨텍스트의 <strong>docker-compose.yml</strong> 에서 넘겨준 인자로, 루트 위치를 넘겨줍니다.</li>
<li><code>COPY $FILE_DIRECTORY /app</code> 를 통해 루트 내의 하위 파일들을 모두 복사하므로 매우 비효율적입니다.</li>
</ul>
<p><strong>2. JDK 베이스 이미지 사용</strong></p>
<ul>
<li>jar를 실행하기 위한 런타임 환경만 있으면 되는데, 불필요하게 Jdk 를 사용하고 있습니다. 이는 이미지를 무겁게 만듭니다.</li>
</ul>
<h2 id="개선된-dockerfile">개선된 Dockerfile</h2>
<p>아래는 <code>reservation</code> 모듈의 Dockerfile 예시입니다.</p>
<pre><code class="language-dockerfile"># Stage 1: Build the application
FROM gradle:8.10.1-jdk17 AS build

WORKDIR /app

# ARG로 전달된 파일 디렉터리 설정
ARG FILE_DIRECTORY

# 필요한 모듈만 복사
COPY $FILE_DIRECTORY/settings.gradle /app/settings.gradle
COPY $FILE_DIRECTORY/gradle /app/gradle
COPY $FILE_DIRECTORY/gradlew /app/gradlew
COPY $FILE_DIRECTORY/reservation /app/reservation
COPY $FILE_DIRECTORY/build.gradle /app/build.gradle

COPY $FILE_DIRECTORY/GlowGrow-common/build.gradle /app/GlowGrow-common/build.gradle
COPY $FILE_DIRECTORY/GlowGrow-common/src /app/GlowGrow-common/src

COPY $FILE_DIRECTORY/GlowGrow-security/build.gradle /app/GlowGrow-security/build.gradle
COPY $FILE_DIRECTORY/GlowGrow-security/src /app/GlowGrow-security/src

COPY $FILE_DIRECTORY/GlowGrow-kafka/build.gradle /app/GlowGrow-kafka/build.gradle
COPY $FILE_DIRECTORY/GlowGrow-kafka/src /app/GlowGrow-kafka/src

# Gradle 빌드 캐시 사용을 위한 외부 의존성 다운로드
RUN ./gradlew :reservation:dependencies --no-daemon

# Reservation 모듈만 빌드
RUN ./gradlew :reservation:clean :reservation:build -x test --no-daemon

# Stage 2: Run the application
FROM eclipse-temurin:17-jre-alpine

# 빌드된 JAR 파일 복사
COPY --from=build /app/reservation/build/libs/*SNAPSHOT.jar /app.jar

# JAR 파일 실행
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
</code></pre>
<p><strong>1. 필요한 모듈만 복사하기</strong></p>
<ul>
<li>필요한 모듈들만 복사하도록 하여 빌드 시간을 단축시키고 이미지 크기를 줄일 수 있습니다.</li>
</ul>
<p><strong>2. JDK 대신 JRE 사용</strong></p>
<ul>
<li>JAR 파일만 실행할 거라면 JRE 이미지를 사용하는 것이 더 적합합니다. </li>
<li>JRE 이미지는 JDK보다 훨씬 가벼우므로 Docker 이미지 크기를 줄일 수 있습니다.</li>
</ul>
<p><strong>3. 멀티스테이지 빌드 최적화</strong></p>
<ul>
<li>Gradle 빌드에서 종속성 모듈을 처리할 때 전체 프로젝트를 복사하기보다 필요한 부분만 복사하여 빌드를 진행할 수 있습니다. </li>
<li>이 부분은 Gradle의 캐시를 활용하는 방식으로 최적화할 수 있습니다.</li>
</ul>
<h2 id="비교">비교</h2>
<p><strong>개선 전 크기</strong>
<img src="https://velog.velcdn.com/images/twonezero_98/post/18455321-afda-4cdb-b065-5fecaf5a98a0/image.JPG" alt=""></p>
<p><strong>개선 후 크기</strong>
<img src="https://velog.velcdn.com/images/twonezero_98/post/06730570-7721-4ca3-bb48-8857c5fda4b2/image.JPG" alt=""></p>
<blockquote>
<p>개선 전과 후가 거의 1.7 배의 차이를 보이므로 유의미한 수정이라고 할 수 있겠습니다.</p>
</blockquote>
<p>사실 속도에 있어서는, 기존 빌드 속도가 느린 편은 아니며 현재 GithubActions 를 통해 Docker 빌드 스테이징을 수행하므로, 로컬 캐싱을 사용할 수 없고, GithubActions에서 캐싱을 진행해야 합니다. 이를 위해 Docker에서 제공하는 플러그인을 사용할 수 있습니다. 아래의 글을 참고하여 각자 구현해 보면 좋을 것 같습니다.</p>
<ul>
<li>참고하기<ul>
<li><a href="https://github.com/docker/buildx">Docker buildx 공식 깃헙</a></li>
<li><a href="https://torbjorn.tistory.com/688">GithubActions + buildx 캐싱 블로그</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Github Actions + Docker + EC2] 로 배포 자동화 구성하기]]></title>
            <link>https://velog.io/@twonezero_98/Github-Actions-Docker-EC2-%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/Github-Actions-Docker-EC2-%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 16 Nov 2024 05:16:18 GMT</pubDate>
            <description><![CDATA[<p>현재의 개발에서는 <strong>지속적 배포(Continuous Deployment, CD)</strong>를 통해 빠르고 안정적인 서비스 제공이 필수가 되었습니다. 특히, <strong>MSA(Microservices Architecture)</strong>를 채택한 프로젝트에서는 각 서비스의 독립성과 배포 효율성을 극대화하는 파이프라인 설계가 중요합니다.</p>
<p>이번 글에서는 멀티모듈 MSA 프로젝트를 기반으로, Github Actions와 Docker, 그리고 EC2를 활용해 배포 자동화를 구성한 경험과 주요 고려 사항을 공유하려 합니다.</p>
<blockquote>
<p>이 글을 통해 다음 내용을 얻을 수 있습니다</p>
</blockquote>
<ul>
<li>멀티모듈 프로젝트에서 CI/CD의 필요성과 구성 전략</li>
<li>Github Actions의 workflow를 활용한 단계별 자동화</li>
<li>Docker 이미지를 빌드하고, EC2로 배포하는 실무적인 접근법</li>
<li>실사용 중 발생했던 이슈와 해결 방안</li>
</ul>
<hr>
<h1 id="1-프로젝트-구성">1. 프로젝트 구성</h1>
<p>프로젝트는 <strong>멀티모듈 기반의 MSA(Microservices Architecture)</strong>로 설계되었으며, 각 기능별로 독립적인 모듈로 분리되어 있습니다. 이러한 구조를 통해 서비스 간 의존성을 최소화하고, 독립적인 배포 및 확장이 가능하도록 구성했습니다.</p>
<h2 id="멀티모듈-구성-방식">멀티모듈 구성 방식</h2>
<p>아래는 주요 기능별로 모듈이 구성된 방식입니다</p>
<blockquote>
<p><strong>공통 모듈 (GlowGrow)</strong></p>
</blockquote>
<ul>
<li>GlowGrow-common: 공통 유틸리티, DTO, 예외 처리 등 모든 서비스에서 사용하는 공통 코드</li>
<li>GlowGrow-security: JWT 기반 인증/인가 및 보안 관련 설정</li>
<li>GlowGrow-kafka: Kafka 관련 설정</li>
<li>GlowGrow-redis: Redis 관련 설정<blockquote>
</blockquote>
</li>
</ul>
<blockquote>
<p><strong>도메인 서비스 모듈</strong></p>
</blockquote>
<ul>
<li>Auth: JWT를 활용한 로그인/회원 인증 기능</li>
<li>Post: 게시글 및 프로필 관리, S3 이미지 업로드, 검색 및 인기 정렬 기능</li>
<li>Reservation: 디자이너 예약 타임테이블 관리, 리뷰/신고 생성 및 상태 관리</li>
<li>Grade: 예약/리뷰/신고 데이터를 기반으로 회원 등급 계산 및 관리</li>
<li>Payment: Toss API를 활용한 결제 및 정산 관리</li>
<li>Promotion: 프로모션/쿠폰 관리 및 Redis 동시성 처리</li>
<li>Notification: Kafka 이벤트 기반 알림 처리</li>
<li>Chat: WebSocket 기반 실시간 채팅, Kafka 브로커를 통한 메시지 전달</li>
<li>Multimedia: 파일 업로드/다운로드 및 관리</li>
</ul>
<blockquote>
<p><strong>인프라/운영 관련 모듈</strong></p>
</blockquote>
<ul>
<li>Gateway: API Gateway를 통한 클라이언트 요청 라우팅 및 인증 처리</li>
<li>Eureka: 서비스 디스커버리 및 레지스트리</li>
<li>Logging/Monitoring: Loki, Prometheus, Grafana를 활용한 로그 및 성능 모니터링</li>
</ul>
<hr>
<h2 id="github-actions--docker">Github Actions + Docker</h2>
<p>배포 자동화와 관련된 도구는 많지만 Github Actions + Docker 조합은 구성과 사용이 비교적 간단하기에 선택했습니다. 또한 아래와 같은 이유들로 배포 구성 기술로 채택했습니다.</p>
<p><strong>완벽한 자동화</strong></p>
<ul>
<li>GitHub Actions는 GitHub 리포지토리와 긴밀히 통합됩니다. 코드가 푸시되거나 PR(Pull Request)이 생성되면 자동으로 빌드, 테스트, 배포 과정을 실행합니다. 이를 통해 개발자는 코어 로직 개발에 더 집중할 수 있습니다.</li>
</ul>
<p><strong>환경 간 일관성 보장</strong></p>
<ul>
<li><p>Docker는 “한 번 빌드하면 어디서나 실행 가능”한 컨테이너 환경을 제공합니다.</p>
</li>
<li><p>로컬 개발 환경과 프로덕션 환경의 차이를 없애고, 모든 서비스가 동일한 상태로 배포되도록 보장합니다.</p>
</li>
<li><p>이를 통해 <em>“내 로컬에서는 잘 되는데”</em> 라는 문제를 미연에 방지할 수 있습니다.</p>
</li>
</ul>
<p><strong>멀티모듈 MSA를 위한 최적화</strong></p>
<ul>
<li>GitHub Actions와 Docker의 조합은 특히 <strong>마이크로서비스 아키텍처(MSA)</strong>에서 그 진가를 발휘합니다. 각 서비스는 독립적인 Docker 이미지를 생성하므로 개별적으로 빌드 및 배포가 가능합니다. </li>
<li>이를 통해 하나의 서비스 업데이트가 다른 서비스에 영향을 미치지 않도록 합니다.</li>
</ul>
<hr>
<h1 id="2-cd-파이프라인-설계">2. CD 파이프라인 설계</h1>
<h2 id="배포-자동화의-목표-및-주요-단계">배포 자동화의 목표 및 주요 단계</h2>
<p>CD 파이프라인은 소스 코드 변경 사항이 자동으로 빌드되고 테스트되며, 최종적으로 배포되는 단계를 자동화합니다. 주요 단계는 아래와 같습니다:</p>
<p><strong>1. 개발 브랜치 및 PR 관리</strong></p>
<ul>
<li>각 서비스는 독립적으로 기능 개발 후, <code>dev</code> 브랜치로 PR을 통해 병합됩니다.</li>
<li><code>dev</code> 브랜치에서는 테스트를 통해 기능의 정상 동작 여부를 확인합니다.</li>
</ul>
<p><strong>2.운영 브랜치로 병합 및 배포</strong></p>
<ul>
<li><code>main</code> 브랜치로 병합되면, Github Actions 워크플로우가 실행되어 자동으로 EC2에 배포됩니다.</li>
<li>병합 즉시 빌드 및 테스트가 수행됩니다.</li>
<li>배포 과정은 이미지 생성, Docker Hub 업로드, EC2로 전송 및 컨테이너 실행을 포함합니다.</li>
</ul>
<h2 id="각-모듈의-독립성과-통합-배포-전략">각 모듈의 독립성과 통합 배포 전략</h2>
<p><strong>모듈별 Dockerfile 관리</strong></p>
<ul>
<li>각 모듈은 자체 Dockerfile을 보유하며, <code>docker-compose.yml</code>에서 통합 관리됩니다.
예: eureka, gateway, reservation 등 서비스가 개별적으로 빌드 가능.</li>
</ul>
<p><strong>통합된 docker-compose.yml</strong></p>
<ul>
<li>루트 컨텍스트에서 docker-compose.yml을 사용해 모든 서비스를 정의하고, 환경 설정(.env)을 기반으로 관리합니다.</li>
</ul>
<p><strong>depends_on 설정으로 서비스 간 의존성을 제어합니다.</strong></p>
<ul>
<li>동일한 네트워크에서 여러 컨테이너가 통신하도록 설계</li>
</ul>
<hr>
<h1 id="3-github-actions-workflow">3. Github Actions Workflow</h1>
<h2 id="워크플로우의-단계별-구성-소개">워크플로우의 단계별 구성 소개</h2>
<p>Github Actions는 push 이벤트를 감지하여 자동으로 워크플로우를 실행합니다.
파이프라인의 주요 단계: <code>코드 푸시 → 빌드 → 이미지 생성 및 푸시 → 배포</code></p>
<ul>
<li><strong>CD.yml</strong></li>
</ul>
<blockquote>
</blockquote>
<pre><code class="language-yaml">name: CD with Gradle
&gt;
on:
  push:
    branches: [ &quot;main&quot; ]
&gt;
&gt;
# 실제 실행될 내용들을 정의합니다.
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    # 각 서비스를 모두 빌드할 수 있도록 변수로 지정합니다.
    # https://docs.github.com/ko/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow
    strategy:
      matrix:
        service: [eureka, auth, gateway, GlowGrow-users, notification, payment, post, promotion, reservation]
&gt;
    steps:
      - name: Checkout
        uses: actions/checkout@v4
&gt;
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;
&gt;
      - name: Build with Gradle
        run: ./gradlew :${{matrix.service}}:clean :${{matrix.service}}:build -x test --no-daemon
&gt;
  Docker:
    name: Build docker image and Push to registry
    needs: build
    runs-on: ubuntu-latest
&gt;
    steps:
      - name: Checkout
        uses: actions/checkout@v4
&gt;
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
&gt;
      - name: web docker build and push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
      # docker compose 를 이용해서 여러 이미지를 모두 빌드하고, 별도의 script를 사용해서 이미지를 push 합니다.
&gt;
      - name: Give execution permission
        run: chmod +x ./dockerTagAndPush.sh
&gt;
      - name: Build, Tag and Push docker image to Hub
        run: |
          docker compose build
          ./dockerTagAndPush.sh
        env:
          DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }}
&gt;
  Deploy:
    name: Deploy
    needs: Docker
    runs-on: ubuntu-latest
&gt;
    steps:
      - uses: actions/checkout@v4
&gt;
      # docker compose로 container를 실행하기 위해 docker-compose.yml 을 EC2로 복사합니다.
      - name: Copy Docker compose file to EC2
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_KEY }}
          source: &quot;docker-compose.yml&quot;
          target: &quot;/home/ubuntu&quot; # target 은 디렉토리임. target directory 아래에 같은 이름의 파일로 옮겨진다.
&gt;
      # ssh를 통해 EC2에 접속하고 docker container를 재시작합니다.
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }}
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_KEY }}
          port: 22
          envs: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, DOCKER_HUB_NAMESPACE
          script: |
            sudo docker-compose down
 &gt; 
            # 이미지 업데이트
            sudo docker-compose pull
 &gt; 
            # 컨테이너 실행
            SPRING_PROFILES_ACTIVE=prod sudo docker-compose --env-file /home/ubuntu/.env up -d</code></pre>
<blockquote>
</blockquote>
<h4 id="1-코드-푸시-및-브랜치-감지">1. 코드 푸시 및 브랜치 감지</h4>
<ul>
<li><code>main</code> 브랜치를 대상으로 워크플로우가 실행됩니다.</li>
<li><code>main</code> 브랜치 병합 시 실제 배포가 시작됩니다.</li>
</ul>
<h4 id="2-빌드-및-테스트">2. 빌드 및 테스트</h4>
<ul>
<li>Gradle을 사용하여 각 서비스 모듈을 빌드합니다.</li>
<li><code>./gradlew :${{matrix.service}}:build</code> 명령으로 각 서비스 독립적으로 빌드.</li>
<li>테스트는 <code>-x test</code>로 배포 시 제외 가능합니다.</li>
</ul>
<h4 id="3-도커-이미지-생성-및-푸시">3. 도커 이미지 생성 및 푸시</h4>
<ul>
<li>docker compose와 <code>dockerTagAndPush.sh</code> 스크립트를 통해 이미지 생성.</li>
<li>생성된 이미지를 Docker Hub에 태그별로 푸시합니다 (<em>latest, commit_hash</em>).</li>
</ul>
<h4 id="4-ec2로-배포">4. EC2로 배포</h4>
<ul>
<li>EC2에 docker-compose.yml 파일 전송.</li>
<li>기존 컨테이너를 종료(docker-compose down) 후 이미지를 업데이트(docker-compose pull).</li>
<li>프로파일별 환경 설정 적용: <code>SPRING_PROFILES_ACTIVE=prod.</code></li>
</ul>
<hr>
<h3 id="docker-이미지를-멀티모듈로-빌드">Docker 이미지를 멀티모듈로 빌드</h3>
<p>워크플로우에서 Docker 부분에서 <code>dockerTagAndPush.sh</code> 를 실행하는 모습을 볼 수 있습니다.
해당 쉘 스크립트는 각 서비스 모듈의 Docker 이미지를 빌드하고, Docker hub 와 같은 이미지 레지스트리로 push 합니다.</p>
<ul>
<li><strong>dockerTagAndPush.sh</strong></li>
</ul>
<blockquote>
</blockquote>
<pre><code class="language-shell"># 모든 서비스 도커 이미지를 빌드합니다.
services=(
  &quot;glowgrow-eureka&quot; &quot;glowgrow-gateway&quot; &quot;glowgrow-auth&quot; &quot;glowgrow-user&quot;
  &quot;glowgrow-payment&quot; &quot;glowgrow-notification&quot; &quot;glowgrow-post&quot; &quot;glowgrow-promotion&quot; &quot;glowgrow-reservation&quot; &quot;glowgrow-multimedia&quot;
)
&gt;
# 도커 이미지에 commit hash를 기반으로한 이미지 태그를 설정합니다.
commit_hash=$(git rev-parse --short HEAD)
&gt;
for service in &quot;${services[@]}&quot;
do
  imageName=&quot;$DOCKER_HUB_NAMESPACE/$service&quot; # 워크플로우에서 env 로 넣어준 네임스페이스
&gt;
  # 도커 이미지 빌드 (해당 service 디렉토리에 Dockerfile이 있어야 합니다.)
  docker build -t &quot;$imageName:latest&quot; &quot;./$service&quot;
&gt;
  # 이미지를 구분하기 위해서 latest 이외의 태그를 추가합니다.
  docker tag &quot;$imageName:latest&quot; &quot;$imageName:$commit_hash&quot;
&gt;
  # Docker Hub에 push
  docker push &quot;$imageName:latest&quot;
  docker push &quot;$imageName:$commit_hash&quot;
&gt;
  echo &quot;$service 이미지가 빌드되어 Docker hub에 푸쉬되었습니다.&quot;
done
&gt;
echo &quot;모든 서비스의 이미지 빌드 및 푸쉬가 완료되었습니다.&quot;
&gt;</code></pre>
<blockquote>
</blockquote>
<h1 id="5-주요-이슈와-해결-방법">5. 주요 이슈와 해결 방법</h1>
<p><strong>1. 멀티모듈 간 의존성 문제</strong></p>
<ul>
<li>문제: 각 모듈이 공통 라이브러리를 의존할 경우 Docker 빌드가 실패하거나 누락이 됩니다.</li>
<li>해결: Dockerfile의 컨텍스트를 루트에서 시작하여 의존하는 모듈을 참조해야 합니다.
<a href="https://velog.io/@twonezero_98/SpringBoot-%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88-MSA-%EC%97%90%EC%84%9C%EC%9D%98-Docker-%EB%B9%8C%EB%93%9C-%EB%AC%B8%EC%A0%9C">멀티모듈에서의 Docker 빌드 문제</a></li>
</ul>
<p><strong>2. Docker 이미지 빌드 최적화</strong></p>
<ul>
<li>문제: 만약 서비스 모듈이나 공통 모듈 추가로 인한 빌드 속도 및 크기 문제</li>
<li>해결<ul>
<li>빌드 후 Docker 로 실행할 것이기에 jdk 이미지 대신에 빌드만을 위한 jre 관련 이미지 사용</li>
<li>Gradle의 빌드 캐시와 병렬 빌드 옵션 활용.</li>
<li>의존하는 모듈만 copy</li>
<li>Docker 이미지는 변경된 서비스만 재빌드하여 효율성 증대.
<a href="https://velog.io/@twonezero_98/%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88%EC%97%90%EC%84%9C%EC%9D%98-Dockerfile-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0">멀티모듈에서의 Dockerfile 최적화하기</a></li>
</ul>
</li>
</ul>
<h1 id="6-결론-및-배운-점">6. 결론 및 배운 점</h1>
<blockquote>
<p>멀티모듈 MSA에서 GithubActions 와 Docker 조합으로 CD 를 구성하는 방법을 학습하고, 그 장점에 대해서 알 수 있었습니다.
또한 Docker 빌드 최적화에 대해서도 학습하고 적용해 보았습니다. </p>
</blockquote>
<h3 id="향후-개선-방안">향후 개선 방안</h3>
<ul>
<li>Blue-Green Deployment 도입<ul>
<li>배포 중 서비스 중단 없는 업데이트 가능.</li>
</ul>
</li>
<li>Helm 및 Kubernetes<ul>
<li>컨테이너 오케스트레이션 도입으로 확장성과 유연성 개선.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프(스프링 심화 1기) 솔직 후기!]]></title>
            <link>https://velog.io/@twonezero_98/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%AC%ED%99%94-1%EA%B8%B0-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@twonezero_98/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%AC%ED%99%94-1%EA%B8%B0-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 01 Nov 2024 06:33:42 GMT</pubDate>
            <description><![CDATA[<p>3개월 동안 진행한 내일배움캠프 스프링 심화 1기를 수료하고 그에 대한 저의 개인적인 후기를 공유하고자 합니다~!</p>
<h2 id="내일배움캠프-이전의-삶">내일배움캠프 이전의 삶</h2>
<blockquote>
<p>저는 컴퓨터과학과를 전공한 후, 졸업하고 1년 동안 혼자서 방향을 잡지 못하고 방황했습니다. 개발 공부를 하긴 했지만, 체계적인 학습이 부족하여 실질적인 기술을 쌓는 데 어려움을 겪었습니다. 이 시기에 다양한 기술을 배우고 싶었지만, 혼자서는 한계가 있었습니다.</p>
</blockquote>
<hr>
<h2 id="내일배움캠프를-선택한-계기">내일배움캠프를 선택한 계기</h2>
<blockquote>
<p>내일배움캠프를 선택한 이유는 체계적인 교육과 실무 중심의 프로젝트 경험을 통해 제 실력을 발전시키고 싶었기 때문입니다. SpringBoot와 MSA(마이크로서비스 아키텍처)에 대한 지식을 배우고, 팀프로젝트를 경험할 수 있다는 것이 저의 목표와 잘 맞았습니다.
또한, 스프링 심화 1기는 기존 프로젝트 경험을 가진 인원을 선발했기에 어느 정도 실력이 있는 팀원들(경력자도 다수 있었음)과 개발을 할 수 있어 큰 도움이 되었습니다.</p>
</blockquote>
<hr>
<h2 id="내일배움캠프의-장점">내일배움캠프의 장점</h2>
<h3 id="1-사전캠프강의-및-msa-강의-제공">1. 사전캠프강의 및 MSA 강의 제공</h3>
<blockquote>
<p>개강 전부터 <strong>기본기를 기를 수 있는 사전캠프</strong>가 있어, 수업에 들어가기 전에 필요한 기초 지식을 쌓을 수 있었습니다. 사전캠프라고 해서 기본적인 Spring 강의만 있는 것이 아닌 Docker, Jpa 등에 대한 더욱 자세한 내용을 포함한 강의도 포함되어 있어, 개강 전 팀프로젝트에서 필요한 지식들을 한 번 다시 되새겨보거나, 더욱 업그레이드 할 수 있었습니다.
그리고, 실제 개강 후 제공되는 강의에서는 MSA 에 대한 기본적인 내용들과 실습도 포함되어 있어, 프로젝트 시 필요한 지식들을 가질 수 있게 됩니다.</p>
</blockquote>
<h3 id="2-현직-튜터의-멘토링">2. 현직 튜터의 멘토링</h3>
<blockquote>
<p><strong>캠프 정규 시간 동안 상주하시는 다섯 분의 튜터님들의 멘토링</strong>을 통해 개발적인 지식이나 프로젝트에 대한 내용을 논의할 수 있고, 실제 현직자 분들의 꿀팁을 배울 수 있어 좋습니다.</p>
</blockquote>
<h3 id="3-실무-중심의-프로젝트">3. 실무 중심의 프로젝트</h3>
<blockquote>
<p>강의를 듣는 2~3 주 정도의 기간 이후, 총 <strong>세 번의 프로젝트</strong>를 진행하게 되는데 아래와 같이 마지막을 제외한 두 번의 프로젝트는 내용 발제에서 주제 및 개발 가이드라인을 잡아주게 되고, 마지막 최종 프로젝트에서는 실제로 제대로 배포 레벨까지 개발할 수 있도록, 대규모 트래픽을 가정한 성능 테스트의 가이드 라인 및 배포 비용 지원(<strong>팀 당 20만원ㄷㄷ</strong>)까지 해줍니다.</p>
</blockquote>
<h4 id="1-ai-검증-비즈니스-프로젝트-프로젝트">1. AI 검증 비즈니스 프로젝트 프로젝트</h4>
<p>주문 관리 플랫폼을 모놀리식 아키텍쳐로 개발해보면서, Springboot 서버 개발 실력을 기르고, 다음 프로젝트를 진행하기 앞서, MSA(마이크로서비스 아키텍쳐) 도입의 필요성에 대한 생각을 해볼 수 있습니다. 아래는 <strong>프로젝트 개발 가이드 예시</strong> 입니다.
<img src="https://velog.velcdn.com/images/twonezero_98/post/67bf78c2-4d16-41f0-9bb8-cc54fc05f252/image.png" alt=""></p>
<h4 id="2-대규모-ai-시스템-설계-프로젝트">2. 대규모 AI 시스템 설계 프로젝트</h4>
<p>앞의 주문 관리 플랫폼 개발 프로젝트에 이어서, 물류 관리 및 배송 시스템을 <strong>MSA</strong>로 개발해 볼 수 있습니다. 특히 <strong>API 연동, 데이터 무결성 유지, 그리고 서비스 간 통신의 신뢰성을 확보하는 방법</strong>에 대해 집중적으로 고민하고, Spring Cloud와 Spring Boot를 활용하여 이러한 과제들을 해결할 방안들을 모색하는 과정에서, MSA 개발에 대한 실력을 빠르게 높일 수 있었습니다. </p>
<p>이 또한 개발 프로세스 가이드가 제공이 되고, <strong>필수 기능과 도전 기능</strong> 가이드로 나뉘어 프로젝트 진행 상황에 따라 프로젝트의 완성도를 높일 수 있습니다.
<img src="https://velog.velcdn.com/images/twonezero_98/post/eb832793-f580-46ff-bec4-113361dd46d8/image.png" alt=""></p>
<h4 id="3-최종-프로젝트-자유-주제-msa-프로젝트">3. 최종 프로젝트 (자유 주제 MSA 프로젝트)</h4>
<p>두 번의 팀 프로젝트를 통해 충분히 개발 프로세스에 대해 이해했다면, 마지막으로 자유 주제를 가지고 <strong>실제 배포 레벨까지 이루어지는 MSA 프로젝트를 개발</strong>할 수 있습니다. 
자유 주제이지만, 너무 걱정하지 않아도 됩니다. 초기 아이디어를 가지고 <strong>튜터님들과의 미팅</strong>을 통해 개발적인 고려 사항이나 추가해야할 사항들을 충분히 논의하고 프로젝트를 고도화 할 수 있습니다.</p>
<p>또한, 최종 프로젝트를 진행하면서 의무적으로 <strong>WIL(Weekly I Learned)</strong> 를 작성해야 하기 때문에 해당 내용을 통해 개발 중 발생했던 <strong>트러블슈팅에 대해서 집중적으로 고민하고, 정리해 볼 수 있습니다.</strong> 이는 노션에서 공개적으로 작성하기 때문에 <strong>다른 팀이나 팀원들의 트러블 슈팅을 간접적으로 경험</strong>할 수 있어 개발적인 식견을 높일 수 있어 좋았습니다.</p>
<p>개발을 완료했다면, 최종 발표를 준비하기 위해 <strong>브로슈어, 깃헙 리드미 작성이나 이미지 제작</strong>을 하는 시간을 가지기 때문에 포트폴리오 어필에도 도움이 될 것 같습니다.</p>
<blockquote>
<p>추가적으로 최종프로젝트에 대한 <strong>Bug Bounty</strong> 이벤트 경연을 진행해서, 각 팀끼리 서로 버그를 발견해주고, 부족한 부분에 대해 리팩토링을 진행할 수 있습니다. 또한 점수를 매겨 상품 또한 받을 수 있습니다.( <em>저희 팀은 1등을 해서 키보드를 받았습니다<del>!</del>!</em> )</p>
</blockquote>
<ul>
<li><a href="https://github.com/final-T/GlowGrow">최종 프로젝트 Github Link</a>
<img src="https://velog.velcdn.com/images/twonezero_98/post/e9974f85-9478-4135-ba81-d71b750f99d1/image.png" alt=""></li>
</ul>
<hr>
<h3 id="4-무제한-취업-지원">4. 무제한 취업 지원</h3>
<blockquote>
<p>3개월 동안 집중적으로 개발 실력을 기르고 포트폴리오를 완성했다면, 그것에 대해 정리하는 시간을 가지고, 취업을 위한 어필을 할 수 있어야겠죠. 내일배움캠프에서는 취업할 때까지 아래와 같이 무제한 취업 지원을 해줍니다. 저는 이 글을 작성하는 시점에서는 이력서, 포트폴리오 완성 기간에 참여해서 매우 기대가 됩니다!</p>
</blockquote>
<ul>
<li>이력서, 포트폴리오 완성 및 피드백 (약 2주)</li>
<li>면접 피드백 ( 서류 합격 시 면접 대비 )</li>
<li>한달 인턴 ( 수료 후 6개월 이내 )</li>
</ul>
<hr>
<h2 id="내일배움캠프-이전과-이후-무엇이-가장-달라졌나요">내일배움캠프 이전과 이후, 무엇이 가장 달라졌나요?</h2>
<blockquote>
<p>캠프를 통해 기술적인 지식뿐만 아니라, 문제 해결 능력과 팀워크의 중요성을 깨달았습니다. 이전에는 혼자서 고민하던 문제들을 이제는 팀원들과 함께 해결할 수 있는 능력을 갖추게 되었습니다. 
또한 <strong>SpringBoot와 Docker, 배포, 모니터링 지식 및 마이크로서비스 아키텍쳐에 대한 지식을  쌓아, 실제로 대규모 트래픽을 처리할 수 있는 MSA 백엔드 서버 개발에 대한 자신감</strong>을 얻었습니다.</p>
</blockquote>
<hr>
<h2 id="내일배움캠프-생활-중-가장-기억에-남았던-순간은">내일배움캠프 생활 중 가장 기억에 남았던 순간은?</h2>
<blockquote>
<p>가장 기억에 남는 순간은 팀 프로젝트 발표 날이었습니다. 각 팀이 준비한 내용을 발표하고, 서로의 아이디어를 공유하는 과정에서 많은 것을 배웠습니다. 특히, 다른 팀의 프로젝트를 보며 새로운 영감을 얻고, 제 프로젝트에 대한 피드백을 받는 것이 매우 유익했습니다.</p>
</blockquote>
<hr>
<h2 id="내일배움캠프를-고민하시는-분들에게-한-마디">내일배움캠프를 고민하시는 분들에게 한 마디</h2>
<blockquote>
<p>내일배움캠프는 단순한 교육이 아니라, 실무 경험을 쌓고 네트워크를 형성할 수 있는 좋은 기회인 것 같습니다. 고민하고 계신다면, 주저하지 말고 도전해보세요. 여러분의 경력에 큰 도움이 될 것이라 생각합니다.</p>
</blockquote>
<p>이 후기를 통해 내일배움캠프의 경험과 그로 인해 얻은 변화에 대해 잘 전달될 수 있기를 바랍니다. 여러분도 이 캠프를 통해 많은 것을 배우고 성장하시길 바랍니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot 멀티모듈 MSA 에서의 Docker 빌드 문제]]></title>
            <link>https://velog.io/@twonezero_98/SpringBoot-%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88-MSA-%EC%97%90%EC%84%9C%EC%9D%98-Docker-%EB%B9%8C%EB%93%9C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@twonezero_98/SpringBoot-%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88-MSA-%EC%97%90%EC%84%9C%EC%9D%98-Docker-%EB%B9%8C%EB%93%9C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 14 Oct 2024 14:26:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>진행 중인 MSA 프로젝트에서 각 서비스 모듈에 대해 Docker를 활용한 CI/CD 파이프라인을 구축하던 중 Docker 환경 구축에 관련된 몇 가지 문제를 겪었습니다. 이를 해결하는 과정을 공유하고자 합니다.</p>
</blockquote>
<h1 id="겪었던-문제">겪었던 문제</h1>
<h2 id="1-docker-빌드-컨텍스트에서-모듈-참조-문제">1. Docker 빌드 컨텍스트에서 모듈 참조 문제</h2>
<p>프로젝트가 멀티모듈 구조로 되어 있어서, 서비스 모듈별로 Docker 이미지를 빌드할 때 각 서비스가 공통 모듈(common, security)을 의존하고 있었습니다. Docker 빌드 중 해당 모듈들을 컨텍스트에서 가져오지 못해 빌드가 실패하는 문제가 발생했습니다. 구체적으로 Gradle 빌드 단계에서 공통 모듈이 복사되지 않았기 때문에 Could not get unknown property 에러가 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/2831ad4b-67d5-4de9-884f-0ddcacb0b3a8/image.png" alt=""></p>
<ul>
<li>문제 발생 시의 Dockefile</li>
</ul>
<pre><code class="language-docker"># Stage 1: Build the application
FROM gradle:8.10.1-jdk17 AS build

WORKDIR /app

COPY . /app

RUN ./gradlew clean build -x test

FROM openjdk:17-jdk-slim

COPY --from=build /app/auth/build/libs/*SNAPSHOT.jar /app.jar

# JAR 파일 실행
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</code></pre>
<h3 id="해결">해결</h3>
<blockquote>
<p>해당 문제를 해결하기 위해 Docker 이미지 빌드 시의 컨텍스트가 루트 모듈에서 시작하도록 지정해야 합니다.</p>
</blockquote>
<ol>
<li>단일 이미지 빌드 시 루트 모듈의 위치에서 터미널 명령어 실행</li>
</ol>
<pre><code class="language-bash"> docker build --build-arg FILE_DIRECTORY=. -f ./&lt;모듈 이름&gt;/Dockerfile -t my-app .</code></pre>
<ol start="2">
<li>루트 모듈 위치에서 Docker-compose.yml 작성</li>
</ol>
<pre><code class="language-yml">version: &#39;3.8&#39;

services:
//... 중략
  auth:
    image: ${DOCKER_HUB_NAMESPACE}/glowgrow-auth
    container_name: auth
    ports:
      - &quot;19099:19099&quot;
    build:
      context: .
      dockerfile: ./auth/Dockerfile
      args:
        - FILE_DIRECTORY=.
    environment:
      - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE}
    depends_on:
      - eureka

//... 생략</code></pre>
<ul>
<li>컨테이너에 대한 설명을 살펴 보면 <strong>build context 가 루트 모듈의 현재 위치에서 시작</strong>되고, args 를 통해 현재의 위치를 변수로 넘겨주고 있는 것을 볼 수 있습니다.</li>
</ul>
<p>해당 변경에 따라 아래와 같이 <strong>Dockerfile</strong> 도 수정해야합니다. </p>
<pre><code class="language-docker"># Stage 1: Build the application
FROM gradle:8.10.1-jdk17 AS build

WORKDIR /app

# ARG로 전달된 파일 디렉터리 설정
ARG FILE_DIRECTORY

# 파일 복사
COPY $FILE_DIRECTORY /app

# auth 모듈만 빌드
RUN ./gradlew :auth:clean :auth:build -x test --no-daemon

FROM openjdk:17-jdk-slim

COPY --from=build /app/auth/build/libs/*SNAPSHOT.jar /app.jar

# JAR 파일 실행
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</code></pre>
<blockquote>
<p>루트 모듈에 존재하는 다른 모듈들에 대한 의존성과 gradle 을 참조하여, 해당 모듈을 Build 해줄 수 있었습니다.</p>
</blockquote>
<h2 id="2-dockerfile-에서-gradlew-못-찾는-문제">2. Dockerfile 에서 gradlew 못 찾는 문제</h2>
<p>위 문제가 있기 전 사소하지만, 시간을 많이 잡아먹은 문제가 하나 있었습니다.
해당 문제를 해결하기 전,  아까 위의 Dockerfile 을 실행해보면 아래와 같이 에러를 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/16d01df9-2212-4bb2-8981-e4e2cbda49db/image.png" alt=""></p>
<p>내용만 보면 gradlew 에 대한 권한문제가 있거나, 애초에 gradlew 파일이 없다고 생각할 수 있습니다. 하지만, 검색 후 단순히 <strong>개행 문제</strong>였던 것을 알 수 있습니다.</p>
<blockquote>
<ul>
<li>참고 글
<a href="https://stackoverflow.com/questions/70844518/cant-run-gradle-wrapper-with-docker-build/71962964#71962964">스택오버플로우</a></li>
</ul>
</blockquote>
<p>저는 윈도우 노트북을 사용하고 있어, 인텔리제이의 개행 형식도 CRLF 로 되어 있었습니다.
그런데 같은 프로젝트를 진행 중인 팀원들은 모두 Mac OS 를 사용하고 있어 저한테만 에러가 발생했던 것입니다.</p>
<h3 id="해결-1">해결</h3>
<p>단순히 인텔리제이에서 gradlew 을 열어 CRLF 를 LF 로 변경만 하면 끝입니다. (매우 허무함)</p>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/56e76fe8-e2db-41d0-a5c4-238515bf3e1f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 실행 시 port 사용 불가 에러]]></title>
            <link>https://velog.io/@twonezero_98/Docker-%EC%8B%A4%ED%96%89-%EC%8B%9C-port-%EC%82%AC%EC%9A%A9-%EB%B6%88%EA%B0%80-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@twonezero_98/Docker-%EC%8B%A4%ED%96%89-%EC%8B%9C-port-%EC%82%AC%EC%9A%A9-%EB%B6%88%EA%B0%80-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Wed, 09 Oct 2024 13:52:26 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>프로젝트를 진행하면서, 여러 팀원과 환경을 맞추기 위해 편리하게 docker 를 설정 후 실행하고 있었다.</p>
<p>그런데, 어느 날 잘 실행되던 docker 가 안되는 것이다...!
아래 사진은 에러 내용이다.</p>
<pre><code class="language-bash">Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:2181 -&gt; 0.0.0.0:0: listen tcp 0.0.0.0:2181: bind: An attempt was made to access a socket in a way forbidden by its access permissions.</code></pre>
<p>내용을 살펴보면, docker를 2181 port 로 binding 하려고 하면 접근 권한이 없다고 하는 것 같다.</p>
<h2 id="원인-파악">원인 파악</h2>
<p>팀원들 중 나만 윈도우 운영체제를 사용하고 있어서 나에게만 발생한 에러이기에 <code>window</code>, <code>docker</code> <code>port bind error</code> 등의 단어 조합으로 검색했더니 좋은 글을 발견했다.</p>
<blockquote>
<p><a href="https://blog.retrotv.dev/docker-bind-error/">https://blog.retrotv.dev/docker-bind-error/</a></p>
</blockquote>
<p>원인은 아래와 같을 것이라고 해당 글에서는 말하고 있다.</p>
<blockquote>
<p>일반적으로 포트를 점유한 프로세스도 찾을 수 없는데도 해당 포트를 사용 중인 상황을 겪고 있다면, 보통은 <strong>운영체제나 프로세스에 의해 미리 동적 포트가 할당되어서</strong> 그럴 가능성이 높다. 운영체제나 프로세스는 처음 시작 시, 시스템에서 사용할 포트를 동적으로 할당 시킨다. 당연한 소리지만, 이 때 할당된 포트는 사용자가 사용할 수 없다. 컴퓨터를 재부팅 시, 다시 포트를 사용 할 수 있게 되는 것은 매번 이 동적으로 할당된 포트가 바뀌게 되기 때문이다.</p>
</blockquote>
<p>잘 되던 docker 가 실행이 안 되었던 이유가 운영체제의 동적 포트 점유 때문이라는 것을 알았다! 우선, 점유된 port 가 어떤 것인지 확인하려면 Powershell 을 열어 아래 명령어를 입력하자.</p>
<pre><code class="language-bash">netsh interface ipv4 show excludedportrange protocol=tcp</code></pre>
<p><img src="https://velog.velcdn.com/images/twonezero_98/post/10f73743-7880-4c72-9109-6d5c65e95700/image.png" alt=""></p>
<p>위 사진과 같이, 여러 범위로 포트들이 운영체제에 의해 미리 할당돼있는 것을 볼 수 있다. (<em>문제 발생 시, port 사진이 없어서 퍼온 사진입니다.</em>)</p>
<p>그래서 해당 글에서 제시한 해결책을 따르면, Powershell 에서 <code>netsh</code>(<em>Network Shell 의 약자로 Windows 운영 체제에서 사용되는 명령줄 도구로, 네트워크 설정 및 관리를 위한 명령어</em>)를 사용하여 내가 사용하고자 하는 port 를 미리 점유하는 설정을 하면 된다.</p>
<h2 id="해결">해결</h2>
<ol>
<li>일단 port가 다른 프로세스에 의해 사용 중일 가능성이 있으므로, window NAT(<a href="https://learn.microsoft.com/ko-kr/virtualization/hyper-v-on-windows/user-guide/setup-nat-network">윈도우 NAT이란?</a>) 네트워크를 멈추게 한다.</li>
</ol>
<pre><code class="language-bash">net stop winnat</code></pre>
<ol start="2">
<li>그 다음, 내가 사용하고자 하는 port를 시스템 또는 다른 내부 프로세스가 사용하지 않도록 미리 점유해놓는다.</li>
</ol>
<pre><code class="language-bash">netsh int ipv4 add excludedportrange protocol=tcp startport=2181 numberofports=1 store=persistent</code></pre>
<ul>
<li>중요한 것은 <code>startport</code> 를 통해 내가 사용하는 port 를 지정하는 것이다.</li>
<li><code>numberofports</code> 는 startport 부터 어디 범위까지 지정할 지 설정하는 것이다.</li>
</ul>
<ol start="3">
<li>위 명령어를 입력했다면, 다시 winnat 을 실행하고 확인해보자.</li>
</ol>
<pre><code>net start winnat

netsh interface ipv4 show excludedportrange protocol=tcp</code></pre><p><img src="https://velog.velcdn.com/images/twonezero_98/post/2094fbd3-670b-4679-bbf3-7d5d3f09d9a2/image.png" alt=""></p>
<p>나는 zookeeper 를 사용하기 위해  <strong>2181</strong> 포트를 미리 내가 사용할 수 있게 점유해놓았다. 숫자 맨 오른쪽에 &#39;*&#39; 표시가 시스템이나 내부 프로세스가 사용하지 않는다는 뜻이다.</p>
<h3 id="정리">정리</h3>
<blockquote>
<p>해당 문제 덕분에(?) window 가 관리하는 가상 네트워크에 대해 조금이라도 살펴보고 powershell 에 대한 사용도 조금 더 나아진 것 같다. 이제 더 이상 docker port 문제로 시간을 버리지는 않을 것 같다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebMvcTest 진행 중 ComponentScan 에러]]></title>
            <link>https://velog.io/@twonezero_98/WebMvcTest-%EC%A7%84%ED%96%89-%EC%A4%91-ComponentScan-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@twonezero_98/WebMvcTest-%EC%A7%84%ED%96%89-%EC%A4%91-ComponentScan-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Sun, 06 Oct 2024 10:22:53 GMT</pubDate>
            <description><![CDATA[<h3 id="문제">문제</h3>
<p>프로젝트 진행 중, 컨트롤러에 대한 단위테스트를 하기 위해 Mockito 와 WebMvcTest 를 이용해 테스트를 작성하고 있었다.</p>
<p>현재 프로젝트는 여러 멀티모듈로 이어져 있고, application 에서 다른 모듈의 component 도 scan 하고 있어서 SpringBootTest 를 통해 모든 context 를 load 하는 것은 비효율적이라고 판단했다. 그래서 WebMvcTest 로 진행~!</p>
<pre><code class="language-java">@DisplayName(&quot;[Reservaition] - TimeSlot&quot;)
@AutoConfigureMockMvc
@WebMvcTest(TimeSlotController.class)
class TimeSlotControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private TimeSlotService timeSlotService;

    @Autowired
    private ObjectMapper objectMapper;

    @DisplayName(&quot;[POST] 예약 타임 슬롯 생성 - 정상 호출&quot;)
    @Test
    @WithMockUser(username = &quot;user@example.com&quot;, roles = {&quot;PROVIDER&quot;})
    void 예약타임슬롯_생성_성공() throws Exception {
        // 테스트용 데이터 준비
        LocalDate localDate = LocalDate.parse(&quot;2024-10-02&quot;);
        CreateTimeSlotRequest request = new CreateTimeSlotRequest(1L, localDate, 20);
        TimeSlotDto timeSlotDto = createTimeSlotDto(localDate);
        given(timeSlotService.createTimeSlot(any(CreateTimeSlotRequestDto.class),any())).willReturn(timeSlotDto);

        // POST 요청 수행 및 결과 검증
        mvc.perform(post(&quot;/api/time-slots&quot;)
                        .contentType(&quot;application/json&quot;)
                        .content(objectMapper.writeValueAsString(request))
                        .with(csrf())
                        .with(user(&quot;user@example.com&quot;).roles(&quot;PROVIDER&quot;))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.code&quot;).value(200))
                .andExpect(jsonPath(&quot;$.message&quot;).value(TIMESLOT_CREATE_SUCCESS.getMessage()))
                .andExpect(jsonPath(&quot;$.data.id&quot;).value(timeSlotDto.id().toString()))
                .andExpect(jsonPath(&quot;$.data.serviceProviderId&quot;).value(1L))
                .andExpect(jsonPath(&quot;$.data.availableDate&quot;).value(&quot;2024-10-02&quot;))
                .andExpect(jsonPath(&quot;$.data.availableTime&quot;).value(20))
                .andExpect(jsonPath(&quot;$.data.isReserved&quot;).value(false))
                .andDo(print());

        then(timeSlotService).should().createTimeSlot(any(CreateTimeSlotRequestDto.class), any());
    }
//Other codes...
}</code></pre>
<blockquote>
<p>해당 테스트는 예약가능시간정보 CRUD 에 대한 단위테스트를 목적으로 작성되었다. 하지만 WebMvcTest + Application의 ComponentScan의 영향으로 TimeSlotController와 전혀 상관 없는, 다른 Controller들을 빈으로 등록하다가 에러가 발생한다.</p>
</blockquote>
<p>Application 에는 아래와 같이 다른 멀티모듈( common, security...) 등의 필요한 component 들을 scan 하도록 설정해놓았었다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableFeignClients
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
@ComponentScan(basePackages = {&quot;com.tk.gg.common&quot;,&quot;com.tk.gg.security&quot;})
public class ReservationApplication { //Reservation 모듈에는 reservation, review, timeSlot 서비스가 존재한다.

    public static void main(String[] args) {
        SpringApplication.run(ReservationApplication.class, args);
    }
}</code></pre>
<p>common 과 security 등은 다른 멀티모듈에 존재하므로 어떻게 수정해야 하는지 고민이었다.</p>
<p>그래서, 관련 Spring document 를 찾아보았더니 WebMvcTest 의 bean 스캔에 대한 글을 발견했다.
<a href="https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html#testing.spring-boot-applications.spring-mvc-tests">Auto-configured Spring MVC Tests</a></p>
<p>공식문서에 나와 있는 어노테이션으로 만들어진 Component 외에는 따로 <code>@Import</code> 를 통해 추가적으로 scan 하도록 명시해야한다고 이해했다.</p>
<h3 id="해결">해결</h3>
<p>그러면 WebMvcTest는 Configuration 파일을 스캔하지 않기 때문에, ComponentScan 을 Application 에 설정하지 않고 Configuration 파일을 따로 작성해 설정하면 될 것 같았다.</p>
<pre><code class="language-java">package com.tk.gg.reservation.infrastructure.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {&quot;com.tk.gg.common&quot;,&quot;com.tk.gg.security&quot;})
public class ComponentScanConfig {
}
</code></pre>
<p>지금 보니까, infrastructure 디렉토리에 해당 config 를 생성한 게 조금 어색한 것 같긴 하지만... 일단 놔두고 테스트를 실행해보자.</p>
<p>별 것 아닌 성공테스트들 이지만...초록색이 뜨면 항상 기분은 좋다...
<img src="https://velog.velcdn.com/images/twonezero_98/post/444f8135-7dd0-496f-9a3b-34ab1825d325/image.png" alt=""></p>
<h4 id="다른-문제점">다른 문제점</h4>
<p>사실, 위의 문제가 발생하기 전에 또 다른 문제가 있었다.
현재 프로젝트는 MSA 로 이루어져 있고, Security 모듈이 존재했다. 그리고 요청 프로세스는 아래와 같다.</p>
<blockquote>
<p>사용자가 <code>gateway</code> 를 통해 <code>auth-service</code> 에 회원가입 및 로그인 요청 -&gt; <code>security</code> 모듈에서 인증 정보 저장 및 다음 요청 시 인증 로직 수행, request 별 인가 -&gt; 유저 정보를 methodArgumentResolver 에 담기 </p>
</blockquote>
<p>프로세스에서, 요청 인자 resolver 에 담기 위해 어노테이션과 <code>HandlerMethodArgumentResolver</code> 를 구현하였고, 아래와 같이 컨트롤러에서 유저 정보를 얻을 수 있다.</p>
<pre><code class="language-java">
@PostMapping
public GlobalResponse&lt;TimeSlotResponse&gt; create(
            @RequestBody @Valid CreateTimeSlotRequest request,
            @AuthUser AuthUserInfo userInfo //유저 정보
            ) {
        return ApiUtils.success(
                TIMESLOT_CREATE_SUCCESS.getMessage(),
                TimeSlotResponse.from(timeSlotService.createTimeSlot(request.toDto(), userInfo))
        );
}</code></pre>
<p>문제는 MockMvc 기반의 단위 컨트롤러 테스트를 진행할 때, <code>AuthUserInfo</code> 의 구현체를 mocking 해서 argumentResolver 에 등록하려고 했기에 발생했다.</p>
<blockquote>
<p>여기서<code>AuthUserInfo</code> 자체는 인터페이스이고, impl 의 생성자는 모두 PRIVATE 으로 설정 및 <code>builder</code> 를 통해 생성되게 작성되었기 때문에 에러가 발생한다.</p>
</blockquote>
<pre><code class="language-java">@BeforeEach
    public void setup() {
        // SecurityMockMvc 설정
        mvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(SecurityMockMvcConfigurers.springSecurity())  // Spring Security 설정 적용
                .build();

        given(authUserArgumentResolver.supportsParameter(any())).willReturn(true);

        given(authUserArgumentResolver.resolveArgument(any(), any(), any(), any()))
                .willReturn(AuthUserInfoImpl.builder()
                        .id(1L)
                        .email(&quot;user@example.com&quot;)
                        .username(&quot;TestUser&quot;)
                        .userRole(UserRole.PROVIDER)
                        .token(&quot;sample-token&quot;)
                        .build());
    }</code></pre>
<p><code>AuthUserInfo</code> 라는 것은 사실 Security 모듈에 존재하고, WebMvcTest 에서는 관심을 가지지 않아도 되는 부분이었다. 그런데, 계속 저 부분에 집착하느라 시간을 많이 날린 것 같다...</p>
<p>어떻게 해도 나의 기존 지식과 검색으로는 해결이 되지 않아 그냥 setup 부분을 지우고, 컨트롤러의 요청이 잘 수행되고 데이터가 원하는 형식으로 반환되는지만 확인하였다. ( <em>이게 맞는 것 같음</em> )</p>
<blockquote>
<p>이제, Test 부분에 <code>@WithMockUser</code> 와 <code>user()</code> 를 통해 인증된 가짜 유저를 넣어 테스트를 수행하면 간단한 일이었다.</p>
</blockquote>
<h3 id="최종-테스트-코드">최종 테스트 코드</h3>
<pre><code class="language-java">@DisplayName(&quot;[Reservaition] - TimeSlot&quot;)
@AutoConfigureMockMvc
@WebMvcTest(TimeSlotController.class)
class TimeSlotControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private TimeSlotService timeSlotService;

    @Autowired
    private ObjectMapper objectMapper;

    @DisplayName(&quot;[POST] 예약 타임 슬롯 생성 - 정상 호출&quot;)
    @Test
    @WithMockUser(username = &quot;user@example.com&quot;, roles = {&quot;PROVIDER&quot;})
    void 예약타임슬롯_생성_성공() throws Exception {
        // 테스트용 데이터 준비
        LocalDate localDate = LocalDate.parse(&quot;2024-10-02&quot;);
        CreateTimeSlotRequest request = new CreateTimeSlotRequest(1L, localDate, 20);
        TimeSlotDto timeSlotDto = createTimeSlotDto(localDate);
        given(timeSlotService.createTimeSlot(any(CreateTimeSlotRequestDto.class),any())).willReturn(timeSlotDto);

        // POST 요청 수행 및 결과 검증
        mvc.perform(post(&quot;/api/time-slots&quot;)
                        .contentType(&quot;application/json&quot;)
                        .content(objectMapper.writeValueAsString(request))
                        .with(csrf())
                        .with(user(&quot;user@example.com&quot;).roles(&quot;PROVIDER&quot;))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.code&quot;).value(200))
                .andExpect(jsonPath(&quot;$.message&quot;).value(TIMESLOT_CREATE_SUCCESS.getMessage()))
                .andExpect(jsonPath(&quot;$.data.id&quot;).value(timeSlotDto.id().toString()))
                .andExpect(jsonPath(&quot;$.data.serviceProviderId&quot;).value(1L))
                .andExpect(jsonPath(&quot;$.data.availableDate&quot;).value(&quot;2024-10-02&quot;))
                .andExpect(jsonPath(&quot;$.data.availableTime&quot;).value(20))
                .andExpect(jsonPath(&quot;$.data.isReserved&quot;).value(false))
                .andDo(print());

        then(timeSlotService).should().createTimeSlot(any(CreateTimeSlotRequestDto.class), any());
    }


    @DisplayName(&quot;[GET] 예약타임슬롯 페이징, 정렬 조회 - 정상 호출&quot;)
    @Test
    @WithMockUser(username = &quot;user@example.com&quot;, roles = {&quot;PROVIDER&quot;})
    void 예약타임슬롯_전체조회_페이징및정렬_성공() throws Exception {
        Sort sort = Sort.by(Sort.Order.desc(&quot;availableDate&quot;));
        Pageable pageable = PageRequest.of(0, 5, sort);
        // 저장 메서드 모킹
        given(timeSlotService.getAllTimeSlot(eq(null),eq(null), eq(pageable)))
                .willReturn(new PageImpl&lt;&gt;(List.of(), pageable, 0));

        mvc.perform(get(&quot;/api/time-slots&quot;)
                    .queryParam(&quot;page&quot;, &quot;0&quot;)
                    .queryParam(&quot;size&quot;, &quot;5&quot;)
                    .queryParam(&quot;sort&quot;, &quot;availableDate,desc&quot;)
                    .contentType(MediaType.APPLICATION_JSON)
                    .with(csrf())
                    .with(user(&quot;user@example.com&quot;).roles(&quot;PROVIDER&quot;))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.code&quot;).value(200))
                .andExpect(jsonPath(&quot;$.data.page.size&quot;).value(5))
                .andExpect(jsonPath(&quot;$.message&quot;).value(TIMESLOT_RETRIEVE_SUCCESS.getMessage()))
                .andDo(print());

        then(timeSlotService).should().getAllTimeSlot(eq(null),eq(null), eq(pageable));
    }

    @DisplayName(&quot;[GET] 예약타임슬롯 단건 조회 - 정상 호출&quot;)
    @Test
    @WithMockUser(username = &quot;user@example.com&quot;, roles = {&quot;PROVIDER&quot;})
    void 예약타임슬롯_단건조회_성공() throws Exception {
        // 저장 메서드 모킹
        LocalDate localDate = LocalDate.parse(&quot;2024-10-02&quot;);
        TimeSlotDto timeSlotDto = createTimeSlotDto(localDate);
        given(timeSlotService.getTimeSlotDetails(timeSlotDto.id())).willReturn(timeSlotDto);

        mvc.perform(get(&quot;/api/time-slots/{timeSlotId}&quot;, timeSlotDto.id())
                        .contentType(MediaType.APPLICATION_JSON)
                        .with(csrf())
                        .with(user(&quot;user@example.com&quot;).roles(&quot;PROVIDER&quot;))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.code&quot;).value(200))
                .andExpect(jsonPath(&quot;$.message&quot;).value(TIMESLOT_RETRIEVE_SUCCESS.getMessage()))
                .andDo(print());

        then(timeSlotService).should().getTimeSlotDetails(timeSlotDto.id());
    }

    @DisplayName(&quot;[POST] 예약 타임 슬롯 수정 - 정상 호출&quot;)
    @Test
    @WithMockUser(username = &quot;user@example.com&quot;, roles = {&quot;PROVIDER&quot;})
    void 예약타임슬롯_수정_성공() throws Exception {
        // 테스트용 데이터 준비
        LocalDate localDate = LocalDate.parse(&quot;2024-10-02&quot;);
        UpdateTimeSlotRequest request = new UpdateTimeSlotRequest(1L,localDate,10,false);
        willDoNothing().given(timeSlotService)
                .updateTimeSlot(any(UUID.class),any(UpdateTimeSlotRequestDto.class),any());

        // POST 요청 수행 및 결과 검증
        mvc.perform(put(&quot;/api/time-slots/{timeSlotId}&quot;, UUID.randomUUID())
                        .contentType(&quot;application/json&quot;)
                        .content(objectMapper.writeValueAsString(request))
                        .with(csrf())
                        .with(user(&quot;user@example.com&quot;).roles(&quot;PROVIDER&quot;))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath(&quot;$.code&quot;).value(200))
                .andExpect(jsonPath(&quot;$.message&quot;).value(TIMESLOT_UPDATE_SUCCESS.getMessage()))
                .andDo(print());

        then(timeSlotService).should().updateTimeSlot(any(UUID.class),any(UpdateTimeSlotRequestDto.class), any());
    }


    private TimeSlotDto createTimeSlotDto(LocalDate localDate){
        return TimeSlotDto.builder()
                .id(UUID.randomUUID())
                .availableDate(localDate)
                .serviceProviderId(1L)
                .isReserved(false)
                .availableTime(20).build();
    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>