<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>namerong 님의 블로그</title>
    <link>https://namerong.tistory.com/</link>
    <description>namerong 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 8 Aug 2026 22:05:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>namerong</managingEditor>
    <item>
      <title>냉파마스터 2차 프로젝트: AI 장보기 추천 Agent 연동과 LLM 사용량 관리 구현</title>
      <link>https://namerong.tistory.com/217</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이번 작업은 냉파마스터 2차 프로젝트의 리팩토링 및 확장 작업이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 프로젝트에서는 냉장고 재료 관리, 장보기 목록 관리, 사전 재료 검색, 관리자 사전 재료 관리 기능을 중심으로 기본 CRUD 흐름을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 프로젝트에서는 여기서 한 단계 확장해, 사용자의 냉장고와 장보기 상태를 기반으로 AI가 필요한 재료를 추천하고, 추천 결과를 사용자가 직접 승인해 장보기 목록에 추가할 수 있는 구조를 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업 범위는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;#238 AI 장보기 추천 MVP 구현&lt;/li&gt;
&lt;li&gt;#239 AI 대화 세션 및 메시지 기록 구현&lt;/li&gt;
&lt;li&gt;#240 LLM 사용량 로그 저장 구현&lt;/li&gt;
&lt;li&gt;#316 프론트 AI 장보기 추천 화면 연동&lt;/li&gt;
&lt;li&gt;#317 관리자 LLM 사용량 전체 조회 API 구현&lt;/li&gt;
&lt;li&gt;#318 관리자 LLM 사용량 로그 화면 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 작업 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 장보기 기능은 사용자가 직접 재료를 검색하고 장보기 목록에 추가하는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 냉장고에 이미 있는 재료, 장보기 목록에 이미 담긴 재료, 사용자가 못 먹는 재료 등을 매번 직접 고려해서 장보기 목록을 구성하는 것은 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 작업에서는 AI Agent를 활용해 다음과 같은 흐름을 만들고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 냉장고 상태 조회&lt;br /&gt;&amp;rarr; 장보기 목록 조회&lt;br /&gt;&amp;rarr; 못 먹는 재료와 선호 음식 정보 반영&lt;br /&gt;&amp;rarr; 추천 후보 재료 생성&lt;br /&gt;&amp;rarr; AI 추천 결과 반환&lt;br /&gt;&amp;rarr; 사용자가 선택한 항목만 장보기 목록에 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 AI가 추천 결과를 바로 DB에 저장하지 않도록 한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 추천은 어디까지나 제안이고, 실제 장보기 목록에 반영할지는 사용자가 직접 선택해야 한다고 판단했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 전체 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 AI 추천 기능은 Backend와 FastAPI Agent 서버를 분리해서 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend는 기존 회원 인증, 냉장고, 장보기, 사전 재료 데이터를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI Agent는 Backend에서 전달받은 사용자 상태와 후보 재료를 기반으로 LLM 추천 결과를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 프론트에서 AI 추천 버튼 클릭&lt;/li&gt;
&lt;li&gt;프론트가 Backend의 AI 추천 API 호출&lt;/li&gt;
&lt;li&gt;Backend가 로그인한 사용자의 냉장고/장보기/못 먹는 재료/선호 음식 정보 조회&lt;/li&gt;
&lt;li&gt;Backend가 추천 제외 상품 ID 목록 생성&lt;/li&gt;
&lt;li&gt;Backend가 추천 후보 상품 목록을 Agent 서버에 전달&lt;/li&gt;
&lt;li&gt;Agent가 LLM 또는 fallback 로직으로 추천 결과 생성&lt;/li&gt;
&lt;li&gt;Backend가 추천 결과를 사용자에게 반환&lt;/li&gt;
&lt;li&gt;Backend가 AI 대화 기록과 LLM 사용량 로그 저장&lt;/li&gt;
&lt;li&gt;사용자가 원하는 추천 항목만 장보기 목록에 추가&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. #238 AI 장보기 추천 MVP 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 AI 장보기 추천 API를 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 API는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /api/v1/agent/shopping-recommendations&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API는 사용자의 냉장고와 장보기 상태를 기반으로 추천 재료 목록을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천에서 제외해야 하는 데이터는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 냉장고에 있는 재료&lt;/li&gt;
&lt;li&gt;아직 구매하지 않은 장보기 항목&lt;/li&gt;
&lt;li&gt;사용자가 못 먹는 재료로 등록한 재료&lt;/li&gt;
&lt;li&gt;재추천 시 이전 추천 화면에 이미 노출된 재료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 냉장고와 장보기 목록만 제외했지만, 이후 사용자가 &amp;ldquo;재추천을 눌러도 같은 추천이 다시 나온다&amp;rdquo;는 문제를 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프론트에서 현재 화면에 떠 있는 추천 재료의 productId를 excludeProductIds로 Backend에 전달하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend는 이 값을 기존 제외 목록에 합쳐 추천 후보에서 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 최종 제외 목록은 다음 기준으로 만들어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;냉장고 보유 재료 productId&lt;/li&gt;
&lt;li&gt;미구매 장보기 항목 productId&lt;/li&gt;
&lt;li&gt;못 먹는 재료 productId&lt;/li&gt;
&lt;li&gt;화면에 이미 노출된 추천 productId&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 재추천 시 같은 항목이 반복 노출되는 문제를 줄였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. #239 AI 대화 세션 및 메시지 기록 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 추천은 단순히 결과만 보여주고 끝나는 기능이 아니라, 사용자가 나중에 이전 추천 내용을 다시 확인할 수 있어야 한다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AI 대화 세션과 메시지 기록을 저장하는 구조를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현한 주요 테이블은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;conversation_sessions&lt;/li&gt;
&lt;li&gt;conversation_messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;conversation_sessions는 AI 추천 요청 단위의 세션 정보를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;conversation_messages는 해당 세션 안에서 사용자 요청 메시지와 AI 응답 메시지를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 API는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /api/v1/ai/conversation-sessions&lt;/li&gt;
&lt;li&gt;GET /api/v1/ai/conversation-sessions/{sessionId}/messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API들은 로그인한 사용자의 데이터만 조회할 수 있도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 세션이 아닌 경우에는 조회되지 않도록 소유자 검증도 함께 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 통해 AI 추천 결과를 단발성 응답이 아니라, 사용자별 기록 데이터로 관리할 수 있게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. #240 LLM 사용량 로그 저장 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 기능은 호출량, 실패 여부, 토큰 사용량, 예상 비용을 추적하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 추후 구독 정책이나 사용량 제한, 비용 분석을 고려한다면 LLM 사용 로그는 반드시 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 LLM 사용량 로그 저장 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하는 주요 정보는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memberId&lt;/li&gt;
&lt;li&gt;modelName&lt;/li&gt;
&lt;li&gt;promptTokens&lt;/li&gt;
&lt;li&gt;completionTokens&lt;/li&gt;
&lt;li&gt;totalTokens&lt;/li&gt;
&lt;li&gt;estimatedCost&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;failureMessage&lt;/li&gt;
&lt;li&gt;createdAt&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 FastAPI Agent에서 usage 값을 내려주고, Backend가 해당 값을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 OpenAI 연동 전 fallback 또는 rule-based 추천이 동작하는 경우에는 modelName을 rule-based-mvp로 저장하고, 토큰 수와 비용은 0으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 gpt-4.1-mini 사용 시에는 입력 토큰과 출력 토큰 단가를 기준으로 예상 비용을 계산하도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prompt는 입력 토큰, Completion은 출력 토큰, Total은 두 값을 합친 전체 토큰 수다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. #316 프론트 AI 장보기 추천 화면 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend와 Agent API가 준비된 뒤, 기존 장보기 화면에 AI 추천 기능을 연결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 화면의 디자인과 사용자 흐름을 해치지 않는 것이 중요했기 때문에, 장보기 목록 화면 안에 AI 추천 버튼과 추천 결과 카드만 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 구현한 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 AI 추천 버튼 클릭&lt;/li&gt;
&lt;li&gt;추천 API 호출&lt;/li&gt;
&lt;li&gt;추천 결과를 카드 형태로 표시&lt;/li&gt;
&lt;li&gt;추천 결과는 자동 저장하지 않음&lt;/li&gt;
&lt;li&gt;사용자가 담기 버튼을 누른 항목만 장보기 목록에 추가&lt;/li&gt;
&lt;li&gt;추가된 항목은 추천 목록에서 제거&lt;/li&gt;
&lt;li&gt;기존 장보기 추가/수정/삭제/체크 흐름은 그대로 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 AI 추천 결과를 바로 DB에 저장하지 않고 화면 상태로만 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해야 사용자가 원하지 않는 재료가 자동으로 장보기 목록에 들어가는 문제를 막을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. #317 관리자 LLM 사용량 전체 조회 API 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 사용량은 일반 사용자 본인 조회뿐 아니라, 관리자 관점에서도 확인할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 관리자 전용 전체 LLM 사용량 조회 API를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 API는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /api/v1/admin/ai/usage-logs&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 응답에는 다음 정보를 포함했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memberId&lt;/li&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;nickname&lt;/li&gt;
&lt;li&gt;modelName&lt;/li&gt;
&lt;li&gt;promptTokens&lt;/li&gt;
&lt;li&gt;completionTokens&lt;/li&gt;
&lt;li&gt;totalTokens&lt;/li&gt;
&lt;li&gt;estimatedCost&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;failureMessage&lt;/li&gt;
&lt;li&gt;createdAt&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 사용자는 접근할 수 없고, 관리자 권한을 가진 사용자만 전체 로그를 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 최신순으로 정렬해 운영자가 최근 AI 호출 상태를 빠르게 확인할 수 있도록 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. #318 관리자 LLM 사용량 로그 화면 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 API를 만든 뒤에는 프론트 관리자 화면에서도 LLM 사용량 로그를 확인할 수 있도록 화면을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 화면에서는 다음 정보를 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 이메일&lt;/li&gt;
&lt;li&gt;닉네임&lt;/li&gt;
&lt;li&gt;모델명&lt;/li&gt;
&lt;li&gt;Prompt 토큰&lt;/li&gt;
&lt;li&gt;Completion 토큰&lt;/li&gt;
&lt;li&gt;Total 토큰&lt;/li&gt;
&lt;li&gt;예상 비용&lt;/li&gt;
&lt;li&gt;성공/실패 상태&lt;/li&gt;
&lt;li&gt;실패 메시지&lt;/li&gt;
&lt;li&gt;호출 일시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 비용은 달러 기준으로 저장되지만, 화면에서는 원화로도 볼 수 있도록 환율 API를 이용해 KRW 변환 표시를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제 운영 환경에서는 환율 API 장애나 변동성을 고려해 fallback 환율도 함께 두었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 트러블슈팅: Flyway Migration이 적용되지 않던 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업 중 가장 크게 막혔던 부분은 Flyway migration이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 대화 세션과 LLM 사용량 로그를 저장하기 위해 새로운 테이블을 추가했고, 이를 Flyway migration으로 관리하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 V4, V5 migration 파일을 추가했는데도 flyway_schema_history 테이블에 버전이 등록되지 않는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 SQL 파일명 문제라고 생각했지만, 실제로는 DB 연결 정보와 migration 적용 대상 DB를 함께 확인해야 하는 문제였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발생 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend를 실행했는데 신규 migration이 적용되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flyway_schema_history를 조회해도 기대했던 V4, V5가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 dev 브랜치를 병합한 뒤에는 Backend 실행 자체가 실패하는 상황도 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인한 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 Flyway가 어떤 migration을 적용했는지 확인했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT installed_rank, version, description, script, success
FROM flyway_schema_history
ORDER BY installed_rank;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리로 Flyway가 실제로 어떤 파일까지 실행했는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 현재 내가 접속한 DB가 Backend가 바라보는 DB와 같은지 확인했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT current_database();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 schema도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT current_schema();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 migration으로 생성되어야 하는 테이블이 실제로 존재하는지도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
  AND table_name IN (
    'conversation_sessions',
    'conversation_messages',
    'llm_usage_logs'
  );&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 크게 두 가지로 나눠 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 application-secret.env의 DB 접속 정보가 현재 실행하려는 DB와 맞지 않았던 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 Flyway가 바라보는 DB와 내가 직접 쿼리로 확인하던 DB가 일치하지 않을 가능성이 있었다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, migration 파일은 존재하지만 Backend 실행 시점에 다른 DB를 보고 있으면 flyway_schema_history에는 기대한 버전이 보이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 dev 브랜치를 병합하면서 환경 설정이나 migration 파일 순서가 바뀌면, 기존에 정상 실행되던 Backend도 다시 실패할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Backend 실행 로그에서 실제 DB URL을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 application-secret.env의 DB_URL, DB_USERNAME, DB_PASSWORD 값을 점검했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 현재 접속 DB와 schema를 SQL로 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 flyway_schema_history와 information_schema.tables를 조회해 migration 적용 여부와 실제 테이블 생성 여부를 함께 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 확인 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Backend 실행 로그에서 실제 DB URL 확인&lt;/li&gt;
&lt;li&gt;application-secret.env의 DB 접속 정보 확인&lt;/li&gt;
&lt;li&gt;current_database()로 현재 접속 DB 확인&lt;/li&gt;
&lt;li&gt;current_schema()로 현재 schema 확인&lt;/li&gt;
&lt;li&gt;flyway_schema_history에서 V4, V5 적용 여부 확인&lt;/li&gt;
&lt;li&gt;information_schema.tables로 실제 테이블 생성 여부 확인&lt;/li&gt;
&lt;li&gt;Backend 재실행 후 migration 정상 반영 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 DB 연결 정보를 정리하고 Backend를 재실행하자 migration이 정상 반영되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway 문제를 볼 때는 migration 파일만 보면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일명이 맞는지, SQL 문법이 맞는지도 중요하지만, 그보다 먼저 &amp;ldquo;내가 지금 어느 DB를 보고 있는지&amp;rdquo;를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 Flyway migration을 점검할 때는 다음 기준이 중요하다는 것을 알게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Backend가 실제로 연결한 DB 확인&lt;/li&gt;
&lt;li&gt;내가 직접 쿼리로 확인하는 DB 확인&lt;/li&gt;
&lt;li&gt;flyway_schema_history 확인&lt;/li&gt;
&lt;li&gt;실제 테이블 생성 여부 확인&lt;/li&gt;
&lt;li&gt;dev 병합 후 설정 변경 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Flyway 문제는 단순히 SQL 파일 문제가 아니라, DB 연결 정보와 migration 이력, 실제 schema 상태를 함께 확인해야 해결할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 테스트 및 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서 확인한 내용은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 추천 API 200 응답 확인&lt;/li&gt;
&lt;li&gt;냉장고 보유 재료 추천 제외 확인&lt;/li&gt;
&lt;li&gt;미구매 장보기 항목 추천 제외 확인&lt;/li&gt;
&lt;li&gt;못 먹는 재료 추천 제외 확인&lt;/li&gt;
&lt;li&gt;excludeProductIds 기반 재추천 제외 확인&lt;/li&gt;
&lt;li&gt;AI 대화 세션 저장 확인&lt;/li&gt;
&lt;li&gt;AI 메시지 기록 저장 확인&lt;/li&gt;
&lt;li&gt;LLM 사용량 로그 저장 확인&lt;/li&gt;
&lt;li&gt;관리자 전체 LLM 로그 조회 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frontend&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장보기 화면 AI 추천 버튼 표시 확인&lt;/li&gt;
&lt;li&gt;추천 결과 카드 표시 확인&lt;/li&gt;
&lt;li&gt;추천 항목 장보기 담기 확인&lt;/li&gt;
&lt;li&gt;기존 장보기 CRUD 영향 없음 확인&lt;/li&gt;
&lt;li&gt;관리자 LLM 사용량 화면 표시 확인&lt;/li&gt;
&lt;li&gt;빌드 성공 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FastAPI Agent 서버 추천 응답 확인&lt;/li&gt;
&lt;li&gt;OpenAI API Key 없는 경우 fallback 동작 확인&lt;/li&gt;
&lt;li&gt;사용량 응답 구조 확인&lt;/li&gt;
&lt;li&gt;선호 음식 기반 prompt 전달 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 이번 작업에서 신경 쓴 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서 가장 신경 쓴 부분은 AI가 직접 DB를 수정하지 않도록 한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 추천 결과를 바로 장보기 목록에 저장하면 편해 보일 수 있지만, 사용자가 원하지 않는 재료가 자동으로 추가될 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 추천과 저장을 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 API는 추천 결과만 반환하고, 실제 장보기 추가는 사용자가 선택한 항목만 별도 API로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기존 장보기 중복 등록 방지 정책을 그대로 재사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 일반 장보기 추가와 AI 추천 장보기 추가가 서로 다른 정책으로 동작하지 않고, 동일한 서비스 로직을 기준으로 관리된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 남은 개선 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 AI 장보기 추천 기능은 MVP 수준으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 추천 품질과 속도 측면에서는 추가 개선 여지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 개선할 수 있는 부분은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추천 후보 재료 수 최적화&lt;/li&gt;
&lt;li&gt;LLM prompt 길이 축소&lt;/li&gt;
&lt;li&gt;추천 이유 문장 길이 제한&lt;/li&gt;
&lt;li&gt;같은 조건의 추천 캐싱&lt;/li&gt;
&lt;li&gt;재추천 시 더 다양한 결과 제공&lt;/li&gt;
&lt;li&gt;사용자 선호 음식과 식단 패턴 반영 강화&lt;/li&gt;
&lt;li&gt;FREE/PREMIUM 권한 정책 적용&lt;/li&gt;
&lt;li&gt;AI 대화 기록 화면 고도화&lt;/li&gt;
&lt;li&gt;LLM 비용 통계 대시보드 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 추천 속도는 Agent에 넘기는 후보 재료 수와 prompt 길이에 영향을 많이 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 단계에서는 후보군을 DB에서 먼저 줄이고, Agent에는 꼭 필요한 정보만 전달하는 방향으로 개선할 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 냉파마스터의 장보기 기능을 단순 CRUD에서 AI 기반 추천 흐름으로 확장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 냉장고, 장보기, 못 먹는 재료, 선호 음식 데이터를 활용해 추천 후보를 만들고, FastAPI Agent와 LLM을 연결할 수 있는 기반을 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 추천 결과를 저장하지 않고 사용자 승인 후 장보기 목록에 추가하는 방식으로 안전한 사용자 흐름을 유지했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 단순히 AI API를 붙이는 것이 아니라, 기존 서비스 정책과 AI 추천 흐름을 어떻게 연결할지 고민한 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 추천 품질, 응답 속도, 구독 정책, 비용 관리까지 확장해 더 실사용에 가까운 AI 장보기 Agent로 발전시킬 수 있을 것 같다.&lt;/p&gt;</description>
      <category>개발 프로젝트/[팀 프로젝트] 냉파마스터</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/217</guid>
      <comments>https://namerong.tistory.com/217#entry217comment</comments>
      <pubDate>Thu, 6 Aug 2026 18:58:03 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]CI/CD, GitHub Actions, Argo CD, Jenkins, Kubernetes, AWS 배포</title>
      <link>https://namerong.tistory.com/216</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션을 개발한 뒤 서버에 배포하는 흐름과, 이를 자동화하는 CI/CD 개념을 학습했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 AWS 환경에서 직접 배포를 진행하며 EC2와 Elastic Beanstalk 같은 배포 방식의 차이를 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD 개념&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Argo CD&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EC2 배포&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC 기본 개념&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-cicd란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;20&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. CI/CD란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD는 개발한 코드를 자동으로 검사하고 배포하기 위한 개발 자동화 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;코드 작성
-&amp;gt; Git push
-&amp;gt; 빌드
-&amp;gt; 테스트
-&amp;gt; 이미지 생성
-&amp;gt; 서버 배포
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI와 CD는 역할이 조금 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;35&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;37&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Continuous Integration&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드 변경 사항을 자주 통합하고 빌드/테스트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;38&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CD&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Continuous Delivery 또는 Continuous Deployment&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검증된 코드를 배포 가능한 상태로 만들거나 실제 배포&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI는 &amp;ldquo;코드가 정상적으로 합쳐질 수 있는지 확인하는 단계&amp;rdquo;에 가깝다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CD는 &amp;ldquo;검증된 결과물을 서버에 반영하는 단계&amp;rdquo;에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;43&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-ci가-필요한-이유&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;45&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. CI가 필요한 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 명이 동시에 개발하면 코드가 자주 바뀐다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 사람이 매번 직접 빌드하고 테스트하면 실수하기 쉽고 시간이 많이 든다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI를 사용하면 Git에 코드가 올라갈 때마다 자동으로 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;push 또는 pull request
-&amp;gt; 의존성 설치
-&amp;gt; build
-&amp;gt; test
-&amp;gt; 결과 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;60&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI의 목적은 문제가 있는 코드가 main 또는 dev 브랜치에 섞이는 것을 줄이는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;62&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-cd가-필요한-이유&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;64&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. CD가 필요한 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;66&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CD는 배포 과정을 자동화한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;68&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수동 배포는 보통 다음 작업을 사람이 직접 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;서버 접속
-&amp;gt; 최신 코드 받기
-&amp;gt; 빌드
-&amp;gt; 실행 중인 서버 중지
-&amp;gt; 새 버전 실행
-&amp;gt; 로그 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;79&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CD를 사용하면 이 흐름을 자동화할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;배포 브랜치에 merge
-&amp;gt; 빌드
-&amp;gt; Docker 이미지 생성
-&amp;gt; 서버 또는 Kubernetes에 반영
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;88&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 자동화의 장점은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;90&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;90&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반복 작업을 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;91&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사람의 실수를 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;92&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 속도가 빨라진다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;93&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 이력을 추적하기 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;95&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-github-actions&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;97&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. GitHub Actions&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;99&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions는 GitHub에서 제공하는 CI/CD 자동화 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;101&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub 저장소 안에 workflow 파일을 작성하면 특정 이벤트에 맞춰 작업이 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;push
pull_request
workflow_dispatch
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;109&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 다음 흐름을 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;GitHub에 push
-&amp;gt; GitHub Actions 실행
-&amp;gt; Gradle build
-&amp;gt; 테스트
-&amp;gt; Docker 이미지 빌드
-&amp;gt; 배포 서버로 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;120&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions는 GitHub 저장소와 바로 연결되어 있기 때문에, 개인 프로젝트나 팀 프로젝트에서 CI를 시작하기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;122&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-jenkins&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;124&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. Jenkins&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;126&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins는 오래전부터 많이 사용된 CI/CD 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;128&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions가 GitHub 중심의 자동화라면, Jenkins는 별도의 서버에 설치해서 사용하는 자동화 서버에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Jenkins 서버
-&amp;gt; Git 저장소 감지
-&amp;gt; 빌드
-&amp;gt; 테스트
-&amp;gt; 배포 스크립트 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;138&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins의 특징은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;140&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;140&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;플러그인이 많다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;141&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오래된 회사 시스템에서도 많이 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;142&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자유도가 높다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;143&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 서버를 운영해야 하므로 관리 부담이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;145&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;147&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도구&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;149&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub 저장소와 연동이 쉽고 설정이 간단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;150&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자유도가 높고 기존 기업 환경에서 많이 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;152&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-argo-cd&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;154&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. Argo CD&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Argo CD는 Kubernetes 환경에서 많이 사용하는 GitOps 기반 배포 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitOps는 Git 저장소를 배포 상태의 기준으로 삼는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Git에 배포 설정 변경
-&amp;gt; Argo CD가 감지
-&amp;gt; Kubernetes 상태와 비교
-&amp;gt; Git 상태와 다르면 자동 동기화
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Argo CD는 &amp;ldquo;Git에 적힌 상태가 실제 서버 상태가 되도록 맞춰주는 도구&amp;rdquo;라고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 Git에 다음 상태가 있다면:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;backend image: v2
replicas: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;176&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Argo CD는 Kubernetes 클러스터가 실제로 이 상태가 되도록 반영한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;178&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-kubernetes&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;180&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. Kubernetes&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;182&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes는 컨테이너를 여러 서버에서 안정적으로 실행하고 관리하는 플랫폼이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;184&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker가 컨테이너 하나를 실행하는 데 초점이 있다면, Kubernetes는 여러 컨테이너를 운영 환경에서 관리하는 데 초점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;186&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes가 해주는 일은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;188&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;188&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 실행&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;189&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 서버에 컨테이너 배치&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;190&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장애가 난 컨테이너 재시작&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;191&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;트래픽 분산&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;192&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 버전 관리&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;193&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케일 아웃&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;194&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정과 비밀값 관리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Docker = 컨테이너 실행 도구
Kubernetes = 컨테이너 운영/관리 플랫폼
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;203&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-cicd-도구들의-관계&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;205&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. CI/CD 도구들의 관계&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;207&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 도구는 역할이 조금씩 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;209&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도구&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주 역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;211&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub 이벤트 기반 빌드/테스트/배포 자동화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;212&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;독립적인 CI/CD 자동화 서버&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;213&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Argo CD&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes 배포 상태를 Git 기준으로 동기화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;214&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 실행과 운영 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예시 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;GitHub Actions
-&amp;gt; 코드 빌드
-&amp;gt; 테스트
-&amp;gt; Docker 이미지 생성
-&amp;gt; 이미지 저장소에 push

Argo CD
-&amp;gt; Git의 Kubernetes 배포 설정 감지
-&amp;gt; Kubernetes 클러스터에 반영
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;230&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-aws-ec2-배포&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;232&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. AWS EC2 배포&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;234&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2는 AWS에서 제공하는 가상 서버이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 Ubuntu 서버를 만들고, 그 안에 Java, Docker, Nginx 같은 필요한 도구를 설치해서 애플리케이션을 실행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 배포 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;EC2 인스턴스 생성
-&amp;gt; SSH 접속
-&amp;gt; 필요한 런타임 설치
-&amp;gt; 프로젝트 파일 또는 Docker 이미지 준비
-&amp;gt; 서버 실행
-&amp;gt; 보안 그룹에서 포트 오픈
-&amp;gt; 외부 접속 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2만 사용해도 프로젝트 배포는 가능하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다만 로드 밸런싱, 자동 스케일링, 모니터링 같은 기능은 직접 추가로 구성해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;252&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-elastic-beanstalk&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;254&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. Elastic Beanstalk&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk는 AWS에서 애플리케이션 배포를 쉽게 해주는 서비스이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;258&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 EC2를 하나하나 설정하는 대신, 애플리케이션을 올리면 필요한 인프라 구성을 AWS가 대신 처리해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk가 도와주는 기능은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;262&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;262&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로비저닝&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;263&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런싱&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;264&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자동 스케일링&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;265&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모니터링&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;266&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 배포 관리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;268&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 프로비저닝은 애플리케이션 실행에 필요한 서버, 네트워크, 환경을 준비하는 과정이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;서버 생성
-&amp;gt; 런타임 준비
-&amp;gt; 네트워크 설정
-&amp;gt; 배포 환경 구성
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;277&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-ec2와-elastic-beanstalk-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;279&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. EC2와 Elastic Beanstalk 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2는 서버를 직접 빌려서 직접 관리하는 방식이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk는 애플리케이션 중심으로 배포하면 AWS가 필요한 EC2와 관련 리소스를 어느 정도 자동으로 관리해주는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;284&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;286&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;관리 단위&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;287&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 자유도&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;높음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상대적으로 제한 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;288&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영 편의성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 관리 필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자동 구성 기능 제공&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;289&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적합한 경우&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 설정을 직접 제어하고 싶을 때&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빠르게 배포 환경을 만들고 싶을 때&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;291&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 프로젝트를 단순히 올려보는 단계라면 EC2만으로도 충분하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 로드 밸런싱, 자동 스케일링, 모니터링까지 편하게 붙이고 싶다면 Elastic Beanstalk를 고려할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;294&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-vpc&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;296&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. VPC&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC는 AWS 안에서 사용하는 사용자 전용 가상 네트워크이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;AWS 전체 공간 안에
내 서비스만 사용하는 네트워크 영역을 따로 만드는 것
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;305&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC 안에서 다음을 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;307&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;307&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IP 대역&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;308&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Subnet&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;309&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Routing Table&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;310&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Internet Gateway&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;311&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Security Group&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;312&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Network ACL&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;314&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2도 결국 특정 VPC와 Subnet 안에 생성된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 접속하려면 인터넷 연결 경로와 보안 그룹 설정이 맞아야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;317&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-실제-aws-배포-흐름&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;319&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. 실제 AWS 배포 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 배포를 진행할 때는 코드 실행만 보면 안 되고, 서버와 네트워크까지 함께 확인해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;1. 애플리케이션 빌드
2. 서버 또는 배포 서비스 준비
3. 실행 환경 설정
4. 애플리케이션 업로드 또는 이미지 배포
5. 서버 실행
6. 포트 확인
7. AWS 보안 그룹 확인
8. 외부 접속 테스트
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로컬에서 잘 실행되더라도 EC2에서 접속이 안 되면 다음을 확인해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;338&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;338&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션이 실제로 실행 중인가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;339&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 내부 포트가 열려 있는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;340&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 보안 그룹에서 해당 포트를 허용했는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;341&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;퍼블릭 IP 또는 도메인이 올바른가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;342&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방화벽 또는 네트워크 설정이 막고 있지 않은가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;344&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;15-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;346&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;15. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;348&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;348&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD는 빌드, 테스트, 배포 흐름을 자동화하는 방식이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;349&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI는 코드 변경을 자동으로 빌드하고 테스트하는 단계이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;350&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CD는 검증된 결과물을 배포 가능한 상태로 만들거나 실제 서버에 배포하는 단계이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;351&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions는 GitHub 이벤트 기반으로 CI/CD를 구성하는 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;352&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jenkins는 별도 서버에서 운영하는 범용 CI/CD 자동화 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;353&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Argo CD는 Kubernetes 환경에서 Git 상태를 기준으로 배포를 동기화하는 GitOps 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;354&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes는 컨테이너를 여러 서버에서 운영하고 관리하는 플랫폼이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;355&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2는 AWS에서 제공하는 가상 서버이며 직접 서버 설정과 배포를 관리해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;356&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Elastic Beanstalk는 배포에 필요한 서버, 로드 밸런싱, 자동 스케일링, 모니터링을 대신 구성해주는 서비스이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;357&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC는 AWS 안에서 사용하는 사용자 전용 가상 네트워크이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;358&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2만으로도 배포는 가능하지만 운영 편의 기능은 직접 구성해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;359&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 배포에서는 애플리케이션 실행뿐 아니라 포트, 보안 그룹, 네트워크 설정까지 함께 확인해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/216</guid>
      <comments>https://namerong.tistory.com/216#entry216comment</comments>
      <pubDate>Wed, 5 Aug 2026 16:16:05 +0900</pubDate>
    </item>
    <item>
      <title>냉파마스터 2차 프로젝트: 장보기 리팩토링과 AI Agent 확장 기반 만들기</title>
      <link>https://namerong.tistory.com/215</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1차 프로젝트에서는 냉장고, 사전 재료, 장보기 기능의 기본 CRUD와 프론트 연동을 중심으로 구현했다.&lt;br /&gt;&lt;/span&gt;2차 프로젝트에서는 단순 기능 추가보다는 기존 기능을 더 안정적으로 다듬고, &lt;br /&gt;이후 AI Agent 기능으로 확장할 수 있는 구조를 만드는 것을 목표로 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업 범위는 크게 세 가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 장보기 항목 중복 등록 방지&lt;br /&gt;둘째, AI Agent가 사용할 Tool API 구조 정리&lt;br /&gt;셋째, 냉장고와 장보기 상태를 기반으로 한 장보기 추천 MVP 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 이슈는 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;이슈&lt;/td&gt;
&lt;td&gt;작업 내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#234&lt;/td&gt;
&lt;td&gt;장보기 항목 중복 등록 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#235&lt;/td&gt;
&lt;td&gt;AI 장보기 자동화 Agent API 명세 작성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#236&lt;/td&gt;
&lt;td&gt;Agent 냉장고/장보기 조회 Tool API 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#237&lt;/td&gt;
&lt;td&gt;Agent 장보기 추가 Tool API 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#238&lt;/td&gt;
&lt;td&gt;AI 장보기 추천 MVP 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 장보기 항목 중복 등록 방지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 장보기 기능은 같은 재료를 여러 번 추가할 수 있었다.&lt;br /&gt;사용자가 직접 추가할 때는 물론이고, 이후 레시피 기반 추가나 AI 추천 추가가 붙으면 같은 재료가 반복해서 쌓일 가능성이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 동일 사용자 기준으로 다음 조건을 만족하는 장보기 항목은 중복 등록으로 판단했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 사용자&lt;/li&gt;
&lt;li&gt;같은 productId&lt;/li&gt;
&lt;li&gt;삭제되지 않은 항목&lt;/li&gt;
&lt;li&gt;아직 구매 완료되지 않은 항목&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 사용자가 이미 &amp;ldquo;감자 1개&amp;rdquo;를 장보기 목록에 담아둔 상태에서 다시 감자를 추가하면 409 Conflict를 반환하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 경우는 재등록을 허용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 기존 항목을 삭제한 경우&lt;/li&gt;
&lt;li&gt;사용자가 기존 항목을 구매 완료 처리한 경우&lt;/li&gt;
&lt;li&gt;다른 사용자가 같은 재료를 담은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 처리한 이유는 장보기 목록이 &amp;ldquo;현재 구매 예정인 항목&amp;rdquo;을 관리하는 기능이기 때문이다.&lt;br /&gt;이미 삭제했거나 구매 완료한 항목은 현재 구매 예정 목록이 아니므로 다시 추가할 수 있어야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Agent API 명세 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 프로젝트의 핵심 확장 방향은 AI Agent였다.&lt;br /&gt;다만 AI가 바로 DB를 수정하도록 만들면 위험하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Agent API는 다음 원칙으로 설계했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI는 필요한 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;AI는 추천 결과를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;추천 결과는 DB에 바로 저장하지 않는다.&lt;/li&gt;
&lt;li&gt;사용자가 승인한 항목만 장보기 목록에 저장한다.&lt;/li&gt;
&lt;li&gt;저장 시에는 기존 ShoppingItemService 정책을 그대로 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 위해 API 번호는 기존 냉장고, 장보기 API와 충돌하지 않도록 700번대로 분리했다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API ID&lt;/td&gt;
&lt;td&gt;기능&lt;/td&gt;
&lt;td&gt;Method&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-700&lt;/td&gt;
&lt;td&gt;Agent 냉장고 조회 Tool&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;/api/v1/agent/tools/fridge-items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-701&lt;/td&gt;
&lt;td&gt;Agent 장보기 조회 Tool&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;/api/v1/agent/tools/shopping-items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-702&lt;/td&gt;
&lt;td&gt;AI 장보기 추천 생성&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;/api/v1/agent/shopping-recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-703&lt;/td&gt;
&lt;td&gt;Agent 승인 항목 장보기 추가&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;/api/v1/agent/tools/shopping-items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 추천 생성과 저장을 분리한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 API는 &amp;ldquo;이 재료를 사면 좋겠다&amp;rdquo;는 후보만 반환하고,&lt;br /&gt;장보기 추가 API는 사용자가 선택한 항목만 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 AI 추천이 잘못되더라도 사용자 데이터가 자동으로 변경되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Agent 냉장고/장보기 조회 Tool API 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 장보기 추천을 하려면 먼저 사용자의 현재 상태를 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Agent Tool API로 냉장고와 장보기 목록 조회 API를 만들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /api/v1/agent/tools/fridge-items&lt;/li&gt;
&lt;li&gt;GET /api/v1/agent/tools/shopping-items&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API는 새 조회 로직을 따로 만들기보다는 기존 냉장고, 장보기 Service를 재사용했다.&lt;br /&gt;이미 기존 Service에는 로그인 사용자 기준 조회와 소유자 검증 흐름이 들어가 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent API라고 해서 별도 정책을 다시 만들면 같은 기능이 중복되고,&lt;br /&gt;나중에 정책이 바뀔 때 한쪽만 수정되는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Agent Tool API는 컨트롤러만 분리하고,&lt;br /&gt;실제 데이터 조회는 기존 Service를 사용하도록 구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Agent 승인 항목 장보기 추가 Tool API 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 추천한 항목을 사용자가 승인하면 장보기 목록에 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때도 핵심은 기존 장보기 등록 정책을 재사용하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ShoppingItemService에는 다음 정책이 들어가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재하지 않는 productId 차단&lt;/li&gt;
&lt;li&gt;비활성 사전 재료 차단&lt;/li&gt;
&lt;li&gt;중복 장보기 항목 차단&lt;/li&gt;
&lt;li&gt;로그인 사용자 기준 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent 승인 항목 추가 API가 별도 저장 로직을 가지면 이 정책들이 중복되거나 누락될 수 있다.&lt;br /&gt;그래서 Agent API는 요청을 받은 뒤 내부적으로 기존 장보기 등록 Service를 호출하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Agent를 통해 추가하든 사용자가 직접 추가하든 같은 정책을 타게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 #234에서 구현한 중복 등록 방지 정책도 Agent 추가 API에 그대로 적용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. AI 장보기 추천 MVP 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 AI 장보기 추천은 LLM을 직접 호출하는 형태는 아니다.&lt;br /&gt;아직 FastAPI Agent 서버나 LLM 연동 정책이 확정되지 않았기 때문에, 먼저 Spring Boot 내부에서 규칙 기반 MVP로 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 추천 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인한 사용자를 조회한다.&lt;/li&gt;
&lt;li&gt;사용자의 냉장고 재료 목록을 조회한다.&lt;/li&gt;
&lt;li&gt;사용자의 장보기 목록을 조회한다.&lt;/li&gt;
&lt;li&gt;냉장고에 이미 있는 productId를 제외 목록에 추가한다.&lt;/li&gt;
&lt;li&gt;장보기 목록 중 미구매 상태인 productId를 제외 목록에 추가한다.&lt;/li&gt;
&lt;li&gt;활성 상태의 사전 재료 중 제외 목록에 없는 재료를 추천한다.&lt;/li&gt;
&lt;li&gt;추천 결과는 DB에 저장하지 않고 응답으로만 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자의 냉장고에 두부가 있고, 장보기 목록에 감자가 있다면 추천 후보에서는 두부와 감자를 제외한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 재료 전체: 두부, 감자, 양파, 대파, 계란&lt;br /&gt;냉장고 보유: 두부&lt;br /&gt;장보기 예정: 감자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 후보: 양파, 대파, 계란&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 아직 완전한 AI 추천은 아니지만, 이후 LLM을 붙이기 위한 기반 구조로는 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 연동 이후에는 현재 구조를 다음처럼 확장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;냉장고 재료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장보기 예정 재료&lt;/li&gt;
&lt;li&gt;못 먹는 재료&lt;/li&gt;
&lt;li&gt;선호 음식&lt;/li&gt;
&lt;li&gt;식단 목적&lt;/li&gt;
&lt;li&gt;제외 productId&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; FastAPI Agent 서버 전달&lt;br /&gt;&amp;rarr; LLM 추천 생성&lt;br /&gt;&amp;rarr; Spring Boot에서 productId 검증&lt;br /&gt;&amp;rarr; 사용자에게 추천 목록 반환&lt;br /&gt;&amp;rarr; 사용자가 승인한 항목만 장보기 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 추천 이유도 고정 문구로 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;냉장고와 장보기 목록에 없는 재료입니다.&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 LLM이 붙으면 추천 이유를 더 자연스럽게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;현재 냉장고에 양파와 두부가 있어 된장찌개 조리에 필요한 애호박을 추천합니다.&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 사용자 상태 기반 추천 이유까지 제공할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 추천 결과를 바로 저장하지 않은 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 추천 기능에서 가장 조심해야 할 부분은 자동 저장이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 추천한 결과를 바로 장보기 목록에 저장하면 사용자가 원하지 않는 재료가 목록에 들어갈 수 있다.&lt;br /&gt;특히 장보기 목록은 실제 구매 행동으로 이어지는 기능이기 때문에, 사용자의 승인 단계가 필요하다고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 구조는 다음처럼 나눴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 생성 API&lt;br /&gt;&amp;rarr; 추천 목록만 반환&lt;br /&gt;&amp;rarr; DB 저장 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;승인 항목 추가 API&lt;br /&gt;&amp;rarr; 사용자가 선택한 항목만 저장&lt;br /&gt;&amp;rarr; 기존 장보기 등록 정책 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 AI 기능을 붙이더라도 사용자 데이터 변경은 항상 사용자의 선택을 거치게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서는 AI 장보기 추천 Service 테스트를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트한 내용은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;냉장고 보유 재료는 추천에서 제외된다.&lt;/li&gt;
&lt;li&gt;미구매 장보기 항목은 추천에서 제외된다.&lt;/li&gt;
&lt;li&gt;추천 결과는 요청 limit 이하로 반환된다.&lt;/li&gt;
&lt;li&gt;limit 값이 없으면 기본값이 적용된다.&lt;/li&gt;
&lt;li&gt;존재하지 않는 회원이면 추천에 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 중 한 번 실패한 부분도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 테스트에서 새로 만든 추천 가능 재료가 반드시 결과에 포함될 것이라고 기대했다.&lt;br /&gt;하지만 로컬 DB에는 이미 활성 사전 재료가 많이 존재하고, 추천 결과는 limit 개수만큼 잘려 반환된다.&lt;br /&gt;따라서 새로 만든 재료가 추천 후보에는 포함될 수 있어도, limit 안에 반드시 들어온다고 보장할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 테스트 기준을 다음처럼 수정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제외되어야 하는 재료가 실제로 제외되는지 확인&lt;/li&gt;
&lt;li&gt;추천 결과 개수가 limit을 넘지 않는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수정하니 테스트가 실제 추천 로직의 목적과 더 잘 맞았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 현재 한계와 다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 #238은 LLM 연동 전 규칙 기반 MVP다.&lt;br /&gt;따라서 다음 내용은 아직 남아 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FastAPI Agent 서버 연동&lt;/li&gt;
&lt;li&gt;LLM 호출 구조 구현&lt;/li&gt;
&lt;li&gt;LLM 실패 시 예외 처리&lt;/li&gt;
&lt;li&gt;추천 이유 생성 고도화&lt;/li&gt;
&lt;li&gt;PREMIUM/FREE 권한 정책 연결&lt;/li&gt;
&lt;li&gt;추천 결과 정렬 기준 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에는 아직 PREMIUM/FREE 구독 권한 구조가 없기 때문에, 무료/유료 사용자 분기는 바로 구현하지 않았다.&lt;br /&gt;이 부분은 결제 또는 구독 정책이 확정된 뒤 연결하는 것이 맞다고 판단했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 단순히 AI 추천 API 하나를 만든 작업이라기보다는,&lt;br /&gt;기존 장보기 기능을 AI Agent가 안전하게 사용할 수 있도록 기반을 만든 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 세 가지를 중요하게 봤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 Service 정책 재사용&lt;/li&gt;
&lt;li&gt;추천과 저장 흐름 분리&lt;/li&gt;
&lt;li&gt;사용자 데이터 자동 변경 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 LLM이 붙은 완성형 AI 기능은 아니지만,&lt;br /&gt;냉장고와 장보기 상태를 기반으로 추천 후보를 만들고,&lt;br /&gt;사용자가 승인한 항목만 장보기 목록에 저장하는 구조를 만들었다는 점에서 2차 프로젝트의 출발점으로 의미가 있었다.&lt;/p&gt;</description>
      <category>개발 프로젝트/[팀 프로젝트] 냉파마스터</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/215</guid>
      <comments>https://namerong.tistory.com/215#entry215comment</comments>
      <pubDate>Tue, 4 Aug 2026 17:42:11 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]Dockerfile, Docker Compose, AWS EC2 기초</title>
      <link>https://namerong.tistory.com/214</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile로 애플리케이션 이미지를 만들고, Docker Compose로 여러 컨테이너를 함께 실행하는 흐름을 학습했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가로 AWS EC2 같은 클라우드 서버에 파일을 올리고, 서버에서 Docker 이미지로 실행하는 기본 배포 흐름도 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 이미지와 컨테이너&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JRE와 JDK 차이&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Boot JAR Docker 이미지 생성&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;scp로 EC2 서버에 파일 전송&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 방화벽 설정&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Compose&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Network&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;17&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Volume&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cloud, IaaS, PaaS, SaaS&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;19&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 주요 서비스&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;21&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-dockerfile이란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;23&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Dockerfile이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile은 Docker 이미지를 만들기 위한 설명서이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지를 만들 때 필요한 실행 환경, 파일, 명령어, 실행 순서를 적어둔다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;FROM eclipse-temurin:21-jre

ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 줄의 의미는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;40&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;명령어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;42&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FROM&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 기본 이미지에서 시작할지 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;43&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ARG&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지 빌드 시 사용할 변수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;44&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;COPY&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로컬 파일을 이미지 안으로 복사&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;45&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ENTRYPOINT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너가 실행될 때 실행할 명령어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 위 Dockerfile은 다음 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;JRE 21 환경 준비
-&amp;gt; build/libs/*.jar 파일을 app.jar로 복사
-&amp;gt; java -jar /app.jar 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;55&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-jre와-jdk-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;57&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. JRE와 JDK 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java 애플리케이션을 실행할 때 JRE와 JDK를 구분해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;61&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;63&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JRE&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java 애플리케이션 실행 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;64&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JDK&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JRE + 개발 도구&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;66&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JRE는 이미 만들어진&amp;nbsp;.jar&amp;nbsp;파일을 실행하는 데 충분하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JDK는 Java 코드를 컴파일하고 개발할 때 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 Dockerfile은 이미 빌드된 Spring Boot JAR를 실행하는 목적이므로 JDK가 아니라 JRE 이미지를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;FROM eclipse-temurin:21-jre
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;개발/빌드 필요 -&amp;gt; JDK
실행만 필요 -&amp;gt; JRE
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;82&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-spring-boot-앱-docker-이미지-만들기&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;84&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Spring Boot 앱 Docker 이미지 만들기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;86&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Boot 프로젝트는 먼저 JAR 파일로 빌드한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;./gradlew build
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 다음 Dockerfile이 있는 위치에서 이미지를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker build -t namhyerhin/dockertest .
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서&amp;nbsp;-t는 이미지 이름과 태그를 붙이는 옵션이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker build -t 이미지이름 .
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;104&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막의&amp;nbsp;.은 현재 디렉터리의 Dockerfile과 파일들을 build context로 사용한다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;106&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-이미지와-컨테이너&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;108&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 이미지와 컨테이너&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;110&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker에서 이미지는 실행을 위한 템플릿이고, 컨테이너는 이미지를 실제로 실행한 상태이다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;112&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;114&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 환경, 코드, 라이브러리를 묶은 것&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;115&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지를 실행한 프로세스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;117&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비유하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;fix&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;이미지 = 설계도
컨테이너 = 설계도로 실행된 실제 프로그램
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하나의 이미지로 여러 컨테이너를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;126&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-ec2-서버로-파일-전송&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;128&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. EC2 서버로 파일 전송&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로컬에서 빌드한 JAR 파일이나 Dockerfile을 EC2 서버로 올릴 때&amp;nbsp;scp를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JAR 파일 전송:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;scp -i &quot;/System/Volumes/Data/Users/hyerhin/ubuntu-server.pem&quot; -r /Users/hyerhin/Desktop/hyerhin/Study/lecture/13_AWS/dockerpro/build/libs/dockerpro-0.0.1-SNAPSHOT.jar ubuntu@13.125.208.6:~
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;138&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile 전송:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;scp -i &quot;/System/Volumes/Data/Users/hyerhin/ubuntu-server.pem&quot; -r /Users/hyerhin/Desktop/hyerhin/Study/lecture/13_AWS/dockerpro/Dockerfile ubuntu@13.125.208.6:~
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;144&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;로컬에서 JAR 빌드
-&amp;gt; scp로 EC2에 JAR와 Dockerfile 업로드
-&amp;gt; EC2에서 docker build
-&amp;gt; 컨테이너 실행
-&amp;gt; EC2 보안 그룹에서 포트 오픈
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에서 애플리케이션을 띄웠더라도 EC2 방화벽, 즉 보안 그룹에서 해당 포트를 열지 않으면 외부에서 접속할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;156&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-포트-확인과-프로세스-종료&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 포트 확인과 프로세스 종료&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;160&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특정 포트를 어떤 프로세스가 사용 중인지 확인할 때&amp;nbsp;lsof를 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;lsof -i :8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로세스를 종료할 때는 PID를 확인한 뒤 종료한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;apache&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;kill [PID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;172&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 8080 포트를 이미 다른 프로세스가 사용 중이면 Spring Boot나 Docker 컨테이너가 같은 포트로 실행되지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;174&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-docker-compose란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;176&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. Docker Compose란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;178&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Compose는 여러 컨테이너를 한 번에 정의하고 실행하는 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;180&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 예제에서는 Node.js 앱과 Redis를 함께 실행했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;services:
  app:
    build: .
    ports:
      - &quot;3000:3000&quot;
    depends_on:
      - redis

  redis:
    image: redis:alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;195&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구조는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;app 컨테이너
-&amp;gt; Node.js Express 서버

redis 컨테이너
-&amp;gt; Redis 저장소
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;depends_on은&amp;nbsp;app&amp;nbsp;컨테이너가&amp;nbsp;redis&amp;nbsp;컨테이너에 의존한다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;207&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-compose에서-컨테이너끼리-통신&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;209&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. Compose에서 컨테이너끼리 통신&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node.js 코드에서는 Redis에 다음 주소로 접속한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;const client = redis.createClient({ url: 'redis://redis:6379' });
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서&amp;nbsp;redis는 localhost가 아니라 Compose 서비스 이름이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;redis:
  image: redis:alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;224&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Compose 안에서는 같은 네트워크에 있는 서비스끼리 서비스 이름으로 접근할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;app 컨테이너
-&amp;gt; redis:6379
-&amp;gt; redis 컨테이너
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;232&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-docker-compose-명령어&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;234&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. Docker Compose 명령어&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;236&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자주 사용하는 명령어는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;242&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스들을 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker-compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;248&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스를 중지하고 생성된 컨테이너와 네트워크를 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker-compose logs -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;254&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 중인 로그를 계속 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker-compose config
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Compose 설정 파일이 어떻게 해석되는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker-compose run [service]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특정 서비스 컨테이너를 일회성으로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;268&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-docker-network&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;270&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. Docker Network&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Network는 컨테이너들이 서로 통신할 수 있게 해주는 가상 네트워크이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 목록은 다음 명령어로 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker network ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;280&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적인 네트워크 드라이버는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;282&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;드라이버&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;284&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;bridge&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 네트워크. 같은 호스트 안의 컨테이너를 격리된 가상 LAN에 연결&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;285&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;host&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너가 호스트 네트워크를 그대로 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;286&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;overlay&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 Docker 호스트 간 컨테이너 연결&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;287&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;macvlan&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너에 별도 MAC 주소를 부여해 물리 네트워크에 직접 연결&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;288&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;none&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 없이 컨테이너 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 단일 서버에서 Compose를 사용할 때는 기본적으로&amp;nbsp;bridge&amp;nbsp;계열 네트워크가 만들어진다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;292&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-docker-volume&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;294&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. Docker Volume&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Volume은 컨테이너가 삭제되어도 데이터를 유지하거나, 호스트와 컨테이너 사이에 데이터를 공유하기 위해 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 내부 파일은 컨테이너를 삭제하면 함께 사라질 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 Volume을 사용하면 데이터가 컨테이너 생명주기와 분리된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;컨테이너 삭제
-&amp;gt; 컨테이너 내부 파일은 사라질 수 있음
-&amp;gt; Volume 데이터는 유지 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적인 방식은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;309&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방식&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;311&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Volume&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker가 관리하는 저장 공간&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;312&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Bind Mount&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호스트의 특정 경로를 컨테이너에 직접 연결&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;314&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발 중에는 로컬 폴더를 바로 연결하는 Bind Mount가 편할 수 있고, 운영에서는 Docker Volume을 사용하는 경우가 많다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;316&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-클라우드-서버란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;318&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. 클라우드 서버란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;320&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라우드 서버는 물리 서버를 직접 구매하지 않고, 클라우드 제공자가 제공하는 가상 서버를 빌려 사용하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;322&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;324&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;324&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;초기 비용을 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;325&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 만큼 서버를 늘리거나 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;326&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장애 대응과 인프라 관리 기능을 활용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;327&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지역, 네트워크, 저장소 같은 자원을 쉽게 선택할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;329&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-hypervisor&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;331&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. Hypervisor&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hypervisor는 가상 머신을 생성하고 실행해주는 기술이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VirtualBox도 Hypervisor의 한 종류로 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;337&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;타입&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;339&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Type 1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하드웨어 위에서 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;340&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Type 2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 운영체제 위에서 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;342&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VirtualBox는 일반적으로 내 OS 위에서 실행되므로 Type 2 방식에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;344&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;15-iaas-paas-saas&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;346&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;15. IaaS, PaaS, SaaS&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;348&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라우드 서비스 모델은 보통 IaaS, PaaS, SaaS로 구분한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-line=&quot;350&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예시 느낌&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;352&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IaaS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버, 네트워크, 스토리지 같은 인프라 제공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;353&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PaaS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 실행 플랫폼 제공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앱 배포 플랫폼&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;354&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SaaS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완성된 소프트웨어 제공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네이버 클라우드, Gmail 같은 서비스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;356&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;IaaS = 가상 서버를 빌림
PaaS = 실행 플랫폼을 빌림
SaaS = 완성된 서비스를 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;364&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;16-aws-주요-서비스&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;366&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;16. AWS 주요 서비스&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;368&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS에는 다양한 인프라 서비스가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;370&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;372&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가상 서버 제공&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;373&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지, 문서, 동영상 같은 객체 스토리지&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;374&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 내부 네트워크 구성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;375&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자와 권한 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;376&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Route 53&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DNS 서비스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;377&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 컨테이너 실행 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;378&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EKS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kubernetes 기반 컨테이너 실행 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;379&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메시지 큐 서비스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;380&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Lambda&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 없이 함수 단위 코드 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;382&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 흐름에서는 EC2가 가장 직접적으로 연결된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2에 Ubuntu 서버를 만들고, 그 위에서 Docker를 설치해 애플리케이션 컨테이너를 실행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;385&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;17-iam-계정&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;387&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;17. IAM 계정&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM은 AWS 리소스에 대한 접근 권한을 관리하는 서비스이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;누가
어떤 리소스에
어떤 작업을 할 수 있는지
정하는 보안 시스템
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 어떤 사용자는 EC2 조회만 가능하고, 어떤 사용자는 S3 업로드까지 가능하게 권한을 나눌 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;400&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실무에서는 루트 계정을 직접 쓰기보다 IAM 사용자나 역할을 만들어 권한을 제한해서 사용하는 것이 안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;402&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;18-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;404&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;18. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;406&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;406&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile은 Docker 이미지를 만들기 위한 실행 환경과 순서를 적는 파일이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;407&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker build -t 이미지명 .으로 현재 디렉터리의 Dockerfile을 기준으로 이미지를 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;408&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JRE는 Java 실행 환경이고, JDK는 JRE에 개발 도구까지 포함한 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;409&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미 빌드된 Spring Boot JAR 실행만 필요하면 JRE 이미지로 충분하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;410&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지는 실행 템플릿이고, 컨테이너는 이미지를 실행한 상태이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;411&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;scp를 사용하면 로컬 파일을 EC2 서버로 전송할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;412&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2에서 서버를 띄워도 보안 그룹에서 포트를 열어야 외부 접속이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;413&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;lsof -i :8080으로 특정 포트를 사용하는 프로세스를 확인할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;414&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Compose는 여러 컨테이너를 한 번에 정의하고 실행하는 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;415&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Compose 안에서는 서비스 이름으로 컨테이너끼리 통신할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;416&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Network는 컨테이너 간 통신을 위한 가상 네트워크이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;417&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Volume은 컨테이너 삭제와 무관하게 데이터를 유지하거나 공유하기 위해 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;418&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cloud는 서버와 인프라 자원을 직접 소유하지 않고 빌려 쓰는 방식이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;419&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IaaS는 인프라, PaaS는 플랫폼, SaaS는 완성된 소프트웨어를 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;420&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EC2는 가상 서버, S3는 객체 저장소, IAM은 접근 권한 관리 서비스이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/214</guid>
      <comments>https://namerong.tistory.com/214#entry214comment</comments>
      <pubDate>Tue, 4 Aug 2026 16:18:34 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]Linux, Ubuntu, Vim, Docker 기초</title>
      <link>https://namerong.tistory.com/213</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux와 Ubuntu의 기본 개념, 터미널 명령어, 에디터 사용법, Docker의 이미지와 컨테이너 개념을 학습했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 환경에서는 GUI보다 터미널을 많이 사용하기 때문에 기본 명령어와 파일 편집 도구에 익숙해지는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux와 Ubuntu 개념&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell과 터미널&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nano, vi, vim 차이&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;절대경로와 상대경로&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 Linux 명령어&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리디렉션과&amp;nbsp;grep&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 이미지와 컨테이너&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 컨테이너 실행, 접속, 중지, 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-linux란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;20&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Linux란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux는 운영체제의 핵심인 Kernel을 기반으로 만들어진 운영체제 계열이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버, 클라우드, Docker, 안드로이드 등 다양한 환경에서 많이 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux 자체는 하나의 완성된 제품이라기보다, 여러 배포판의 기반이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적인 Linux 배포판은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;29&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포판&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;31&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입문자가 사용하기 좋고 자료가 많음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;32&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Debian&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;안정성을 중요하게 여김&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;33&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CentOS / Rocky Linux&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 환경에서 많이 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;34&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fedora&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최신 기술 반영이 빠름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;35&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Alpine&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가볍고 Docker 이미지에서 자주 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수업에서는 Ubuntu를 중심으로 Linux 환경을 다뤘다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;39&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-ubuntu와-virtualbox&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;41&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Ubuntu와 VirtualBox&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu는 Linux 배포판 중 하나이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데스크톱이나 서버 환경에서 많이 사용되고, 패키지 설치 자료가 많아 학습용으로도 적합하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VirtualBox는 내 컴퓨터 안에 가상 컴퓨터를 만들어주는 프로그램이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;내 컴퓨터
-&amp;gt; VirtualBox
-&amp;gt; Ubuntu 가상 머신
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 실제 컴퓨터 운영체제를 바꾸지 않고도 Ubuntu 환경을 실습할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;56&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-shell이란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;58&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Shell이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;60&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell은 사용자가 입력한 명령어를 운영체제에 전달하는 프로그램이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;사용자 명령어
-&amp;gt; Shell
-&amp;gt; OS Kernel
-&amp;gt; 실행 결과
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 터미널에 다음 명령어를 입력하면:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell이 이 명령을 해석해서 현재 디렉터리의 파일 목록을 출력한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;77&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자주 사용하는 Shell은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;79&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;81&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;sh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적인 Shell&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;82&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;bash&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux에서 많이 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;83&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zsh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;macOS 기본 Shell로 많이 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;85&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-nano-vi-vim-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;87&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. Nano, vi, vim 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;89&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;터미널 환경에서는 파일을 수정할 때 터미널용 에디터를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;91&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에디터&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특징&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;93&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nano&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용법이 단순하고 입문자에게 쉬움&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;94&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vi&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대부분의 Unix/Linux 서버에 기본 포함된 에디터&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;95&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vim&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vi의 확장판으로 기능이 더 많음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;97&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;nano는 화면 아래에 단축키가 보여서 처음 쓰기 쉽다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vi와&amp;nbsp;vim은 처음에는 어렵지만, 서버 환경에서 자주 만난다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vim은 플러그인을 붙이면 IDE처럼 사용할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;자동완성
문법 강조
파일 탐색
검색
Git 연동
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;110&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에서 설정 파일을 빠르게 고쳐야 할 때&amp;nbsp;vi&amp;nbsp;또는&amp;nbsp;vim을 사용할 일이 많다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;112&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-apt&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;114&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. Apt&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;116&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;apt는 Ubuntu에서 패키지를 설치하고 관리하는 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;123&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;125&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;명령어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;127&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;apt update&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치 가능한 패키지 목록 갱신&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;128&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;apt install&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패키지 설치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;129&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;apt remove&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패키지 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;131&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu에서 프로그램을 설치할 때 자주 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;133&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-절대경로와-상대경로&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;135&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 절대경로와 상대경로&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;137&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;경로는 파일이나 디렉터리의 위치를 나타낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;절대경로는 루트 디렉터리&amp;nbsp;/부터 시작하는 전체 경로이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;/home/user/project/file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;145&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상대경로는 현재 위치를 기준으로 한 경로이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;./file.txt
../project/file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자주 쓰는 기호는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;154&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기호&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;156&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;루트 디렉터리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;157&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 디렉터리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;158&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;..&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상위 디렉터리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;159&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;~&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 사용자의 홈 디렉터리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;161&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-기본-linux-명령어&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;163&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 기본 Linux 명령어&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;165&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자주 사용하는 명령어는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;167&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;명령어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;169&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pwd&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 위치 출력&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;170&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ls&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 목록 출력&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;171&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;cd&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디렉터리 이동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;172&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mkdir&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디렉터리 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;173&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;touch&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빈 파일 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;174&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;rm&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 또는 디렉터리 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;175&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;cat&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 내용 출력&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;176&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;cp&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 복사&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;177&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mv&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 이동 또는 이름 변경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;178&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;find&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 검색&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;179&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;which&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;명령어 실행 위치 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;181&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예시:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;mkdir test
cd test
touch memo.txt
cat memo.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;190&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-rm-사용-주의&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;192&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. rm 사용 주의&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;194&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;rm은 삭제 명령어이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;휴지통으로 보내는 것이 아니라 바로 삭제되기 때문에 조심해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;rm file.txt
rm -r folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;202&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 다음 명령은 매우 위험하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;rm -rf /
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;208&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;삭제 명령어는 실행 전 경로를 꼭 확인해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;210&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-리디렉션&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;212&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. 리디렉션&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리디렉션은 명령어의 출력 결과를 파일로 보내는 기능이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;echo &quot;hello&quot; &amp;gt; memo.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;220&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;gt;는 기존 내용을 덮어쓴다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;echo &quot;world&quot; &amp;gt;&amp;gt; memo.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;226&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;gt;&amp;gt;는 기존 내용 뒤에 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;230&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기호&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;232&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 파일에 덮어쓰기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;233&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 파일에 이어쓰기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;234&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 내용을 입력으로 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;236&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-grep과-정규-표현식&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;238&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. grep과 정규 표현식&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;240&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;grep은 파일 내용에서 특정 문자열을 찾는 명령어이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;grep &quot;error&quot; app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;246&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정규 표현식을 함께 사용하면 더 복잡한 패턴도 찾을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;grep &quot;^Error&quot; app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;252&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;254&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표현식&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;256&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;^&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;줄의 시작&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;257&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;$&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;줄의 끝&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;258&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;임의의 한 글자&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;259&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;*&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞 패턴이 0번 이상 반복&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;261&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그 파일에서 에러를 찾거나 설정 파일에서 특정 값을 찾을 때 많이 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;263&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-docker란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;265&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. Docker란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행하고 관리하는 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;269&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단히 말하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;프로그램 실행에 필요한 것들
+ 라이브러리
+ 설정
+ 실행 환경
= 이미지로 묶음

이미지를 실행한 상태
= 컨테이너
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;282&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker를 사용하면 내 컴퓨터, 서버, AWS 등 어디서 실행하든 비슷한 환경을 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;284&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-이미지와-컨테이너&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;286&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. 이미지와 컨테이너&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;288&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker에는 이미지와 컨테이너가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;290&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;292&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행에 필요한 파일, 라이브러리, 설정을 묶은 템플릿&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;293&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지를 실제로 실행한 상태&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;295&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비유하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;fix&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;이미지 = 실행 설계도
컨테이너 = 설계도로 실제 실행된 프로그램
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하나의 이미지로 여러 컨테이너를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;304&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-docker를-사용하는-이유&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;306&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. Docker를 사용하는 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;308&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker를 사용하면 실행 환경을 맞추기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;310&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 어떤 프로젝트가 특정 버전의 Ubuntu, Java, Python, 라이브러리를 필요로 한다면 이 환경을 이미지로 묶어둘 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;314&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;314&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 차이로 인한 오류를 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;315&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 쉬워진다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;316&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 단위로 실행과 삭제가 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;317&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에 여러 서비스를 격리해서 실행할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;318&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker Hub, GitHub, AWS 등과 연결해 배포하기 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;320&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;15-scale-up과-scale-out&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;322&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;15. Scale Up과 Scale Out&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 성능을 늘리는 방식에는 Scale Up과 Scale Out이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;326&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;328&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scale Up&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 한 대의 성능을 높이는 것&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;329&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scale Out&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버나 컨테이너 수를 늘리는 것&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;331&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 CPU와 RAM을 더 좋은 것으로 바꾸는 것은 Scale Up이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;같은 서버를 여러 대로 늘리는 것은 Scale Out이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;334&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker는 컨테이너 단위로 서비스를 여러 개 띄우기 좋아서 Scale Out 구조와 잘 맞는다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;336&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;16-docker-기본-명령어&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;338&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;16. Docker 기본 명령어&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;340&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 중인 컨테이너 목록을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;346&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 컨테이너 목록을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 명령어 도움말을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker --help
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;358&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너를 시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker start ubuntu_test2
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;364&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너에 접속한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker attach ubuntu_test2
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;370&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너를 중지한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker container stop ubuntu_test2
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;376&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너를 강제로 삭제한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;axapta&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker container rm -f ubuntu_test2
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;382&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중지된 컨테이너를 한 번에 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;axapta&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;docker container prune
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;388&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;17-컨테이너에서-빠져나오기&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;390&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;17. 컨테이너에서 빠져나오기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;392&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너에 접속한 상태에서 빠져나오는 방법은 두 가지가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Ctrl + P, Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 방식은 컨테이너를 계속 실행한 채로 빠져나온다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Ctrl + D
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;405&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 방식은 Shell을 종료하므로 컨테이너도 종료될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;407&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;409&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방법&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;411&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ctrl + P, Q&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 실행 유지 후 빠져나오기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;412&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ctrl + D&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell 종료&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;413&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;exit&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell 종료&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;415&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;18-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;417&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;18. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;419&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;419&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linux는 서버와 클라우드 환경에서 많이 사용하는 운영체제 계열이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;420&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu는 Linux 배포판 중 하나로 학습과 서버 환경에서 자주 사용된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;421&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VirtualBox를 사용하면 내 컴퓨터 안에서 Ubuntu 가상 머신을 실행할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;422&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Shell은 사용자의 명령어를 운영체제에 전달하는 프로그램이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;423&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nano는 쉽고, vi는 기본 제공 가능성이 높고, vim은 확장성이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;424&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;apt는 Ubuntu의 패키지 설치 및 관리 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;425&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;절대경로는&amp;nbsp;/부터 시작하고, 상대경로는 현재 위치를 기준으로 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;426&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ls,&amp;nbsp;mkdir,&amp;nbsp;touch,&amp;nbsp;rm,&amp;nbsp;cat,&amp;nbsp;cp,&amp;nbsp;mv,&amp;nbsp;find,&amp;nbsp;which는 기본 명령어로 익숙해져야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;427&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리디렉션은 명령어 출력 결과를 파일로 저장하거나 이어붙일 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;428&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;grep은 파일 내용에서 특정 문자열이나 패턴을 찾을 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;429&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker는 애플리케이션을 컨테이너로 실행하고 관리하는 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;430&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지는 실행 환경 템플릿이고, 컨테이너는 이미지를 실행한 상태이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;431&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker를 사용하면 환경 차이를 줄이고 배포를 쉽게 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;432&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker ps는 실행 중인 컨테이너,&amp;nbsp;docker ps -a는 전체 컨테이너를 보여준다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;433&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ctrl + P, Q는 컨테이너를 유지한 채 빠져나오는 방법이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;434&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker container prune은 중지된 컨테이너를 정리할 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/213</guid>
      <comments>https://namerong.tistory.com/213#entry213comment</comments>
      <pubDate>Mon, 3 Aug 2026 15:51:46 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]LangSmith로 Workflow 추적과 평가하기</title>
      <link>https://namerong.tistory.com/212</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith를 사용해 AI Workflow 실행 과정을 추적하고, 실패 원인을 확인하며, 수정 전후 결과를 Dataset과 Experiment로 비교하는 흐름을 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;8&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith 환경 설정 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@traceable을 사용한 실행 추적&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow 실행 Trace 기록&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패 Workflow 추적&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;baseline 버전과 fixed 버전 비교&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dataset 생성&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Evaluator 작성&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Experiment 실행과 비교&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;17&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-langsmith란&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;19&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. LangSmith란?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith는 LangChain, LangGraph 기반 애플리케이션의 실행 과정을 관찰하고 평가할 수 있게 도와주는 도구이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AI Workflow는 내부에서 여러 단계가 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Planner
-&amp;gt; Executor
-&amp;gt; Task 실행
-&amp;gt; Validator
-&amp;gt; 결과 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 흐름이 실패했을 때 단순히 &amp;ldquo;답이 이상하다&amp;rdquo;로는 원인을 찾기 어렵다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith를 사용하면 각 단계가 어떤 입력을 받고, 어떤 출력을 만들었는지 Trace로 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;36&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-환경-변수-설정&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;38&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 환경 변수 설정&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith에 Trace를 업로드하려면 환경 변수가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;LANGSMITH_API_KEY
LANGSMITH_TRACING
LANGSMITH_PROJECT
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;48&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 확인 함수에서는 누락된 값과 잘못된 값을 검사했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;LANGSMITH_REQUIRED = (
    &quot;LANGSMITH_API_KEY&quot;,
    &quot;LANGSMITH_TRACING&quot;,
    &quot;LANGSMITH_PROJECT&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;58&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LANGSMITH_TRACING은&amp;nbsp;true&amp;nbsp;계열 값이어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if tracing and tracing not in {&quot;true&quot;, &quot;1&quot;, &quot;yes&quot;, &quot;on&quot;}:
    errors.append(&quot;LANGSMITH_TRACING은 true여야 합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;65&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API Key는 그대로 출력하지 않고 일부만 마스킹해서 확인했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Key를 로그에 그대로 노출하지 않는 습관도 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;68&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-trace&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;70&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Trace&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Trace는 실행 기록이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 함수가 어떤 입력으로 실행되었고, 어떤 결과나 에러를 냈는지 남긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 Workflow에서는 각 Task 함수를&amp;nbsp;@traceable로 감쌌다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@traceable(name=&quot;task-destination&quot;, run_type=&quot;tool&quot;)
def traced_destination(request, context):
    execute_destination(request, context)
    return {
        &quot;destinations&quot;: [
            place.model_dump() for place in context[&quot;destinations&quot;]
        ]
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;88&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 LangSmith에서&amp;nbsp;task-destination,&amp;nbsp;task-budget,&amp;nbsp;task-compose&amp;nbsp;같은 단계를 각각 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;90&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-workflow-trace-구조&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;92&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. Workflow Trace 구조&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;94&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow 전체도&amp;nbsp;@traceable로 감싸서 하나의 Chain 실행으로 기록했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@traceable(name=&quot;d-travel-workflow&quot;, run_type=&quot;chain&quot;)
def run_workflow(inputs: dict, version: PlanVersion = &quot;fixed&quot;) -&amp;gt; dict:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;102&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;run_workflow
-&amp;gt; build_workflow
-&amp;gt; planner
-&amp;gt; executor
-&amp;gt; task-requirements
-&amp;gt; task-weather
-&amp;gt; task-destination
-&amp;gt; task-budget
-&amp;gt; task-compose
-&amp;gt; validator
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;117&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Trace를 보면 전체 흐름뿐 아니라 내부 Task별 실행 결과까지 볼 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 실패했을 때 어느 Task에서 문제가 생겼는지 확인하기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;120&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-baseline과-fixed&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;122&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. baseline과 fixed&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는 Workflow 버전을 나누어 실행했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;PlanVersion = Literal[&quot;baseline&quot;, &quot;fixed&quot;, &quot;llm&quot;, &quot;runtime_error&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 버전의 의미는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%; height: 93px;&quot; border=&quot;1&quot; data-line=&quot;132&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;버전&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;134&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;baseline&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 전 Plan&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;135&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fixed&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 후 Plan&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;136&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;llm&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 LLM이 생성한 Plan&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-line=&quot;137&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;runtime_error&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오류 재현용 Plan&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;baseline은&amp;nbsp;destination&amp;nbsp;Task가 빠진 Plan이었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;requirements
-&amp;gt; weather
-&amp;gt; budget
-&amp;gt; compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장소 후보를 검색하지 않고 예산 계산과 일정 작성을 진행하므로 검증 실패가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fixed는 장소 검색 Task까지 포함한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;requirements
-&amp;gt; weather
-&amp;gt; destination
-&amp;gt; budget
-&amp;gt; compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;160&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 수정 전후의 차이를 LangSmith에 기록하고 비교할 수 있게 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;162&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-실패-trace-확인&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;164&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 실패 Trace 확인&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패를 일부러 재현하는 코드도 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;version=&quot;runtime_error&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;172&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;runtime_error&amp;nbsp;버전은&amp;nbsp;compose&amp;nbsp;Task가 빠져 있어 최종&amp;nbsp;draft가 생성되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if draft is None:
    raise ValueError(&quot;실행 결과에 Draft가 없습니다. compose Task를 확인하세요.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 실패를 LangSmith Trace로 남기면 어느 단계까지 실행됐고, 어떤 Task가 빠져서 예외가 발생했는지 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;181&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패 디버깅에서 중요한 점은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;실패 결과만 보지 말고
실행 경로와 중간 결과를 같이 확인한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;188&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-validator-보완&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;190&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. Validator 보완&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;192&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Validator는 Workflow 결과가 요구사항을 만족하는지 판단한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;194&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 예산 조건 검증도 추가되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if budget_options and not any(
    bool(option[&quot;within_budget&quot;]) for option in budget_options
):
    cheapest = min(int(option[&quot;total&quot;]) for option in budget_options)
    report = ValidationReport(
        passed=False,
        failed_conditions=[
            f&quot;최저 예상 비용 {cheapest:,}원이 예산 {state['request'].budget:,}원을 초과합니다.&quot;
        ],
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 Task가 실행됐는지만 보는 것이 아니라, 실제 결과가 사용자의 예산 조건을 만족하는지도 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;211&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-dataset&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;213&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. Dataset&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dataset은 평가에 사용할 입력과 기대 결과 모음이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;EVALUATION_CASES = [
    {
        &quot;inputs&quot;: {
            &quot;goal&quot;: &quot;비 오는 날 아이와 갈 송파 하루 일정을 만들어줘.&quot;,
            &quot;weather&quot;: &quot;rain&quot;,
            &quot;people&quot;: 3,
            &quot;budget&quot;: 200_000,
            &quot;children&quot;: True,
        },
        &quot;outputs&quot;: {&quot;expected_pass&quot;: True},
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dataset을 만들면 같은 테스트 케이스로 여러 버전의 Workflow를 반복 평가할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;workflow-v1 Dataset
-&amp;gt; baseline Experiment
-&amp;gt; fixed Experiment
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;240&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-evaluator&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;242&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. Evaluator&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;244&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Evaluator는 실제 실행 결과와 기대 결과를 비교하는 함수이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def workflow_evaluator(outputs: dict, reference_outputs: dict) -&amp;gt; dict:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;251&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;평가 기준은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;253&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;253&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 PASS/FAIL이 기대 결과와 같은가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;254&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PASS 케이스에서는 장소가 선택되었는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;255&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FAIL 케이스에서는 기대한 실패 이유가 포함되었는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;256&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Task 누락 때문에 우연히 실패한 것은 아닌가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;258&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과는 점수와 코멘트로 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;return {
    &quot;key&quot;: &quot;workflow_requirement_match&quot;,
    &quot;score&quot;: int(passed),
    &quot;comment&quot;: &quot;...&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;268&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Evaluator를 사용하면 단순히 실행만 해보는 것이 아니라, 결과 품질을 기준에 따라 판단할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;270&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-experiment&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;272&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. Experiment&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Experiment는 특정 버전의 Workflow를 Dataset에 대해 실행한 평가 기록이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;client.evaluate(
    baseline_target,
    data=DATASET_NAME,
    evaluators=[workflow_evaluator],
    experiment_prefix=&quot;workflow-baseline&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 후 버전도 같은 Dataset으로 평가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;client.evaluate(
    fixed_target,
    data=DATASET_NAME,
    evaluators=[workflow_evaluator],
    experiment_prefix=&quot;workflow-fixed&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 같은 입력에 대해 baseline과 fixed 결과를 비교할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;같은 Dataset
-&amp;gt; baseline 실행 결과
-&amp;gt; fixed 실행 결과
-&amp;gt; 점수 비교
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;305&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-langsmith를-쓰는-이유&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;307&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. LangSmith를 쓰는 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;309&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AI Workflow는 일반 코드보다 디버깅이 어렵다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LLM, Tool, Graph, Validator가 함께 동작하기 때문에 어디서 문제가 생겼는지 확인하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith를 사용하면 다음을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;314&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;314&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 입력이 들어왔는지&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;315&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 Plan이 만들어졌는지&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;316&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 Task가 실행되었는지&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;317&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 Task의 결과가 무엇인지&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;318&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어느 단계에서 예외가 발생했는지&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;319&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 전후 품질이 좋아졌는지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, LangSmith는 AI 기능의 로그, 디버깅, 평가를 위한 관찰 도구라고 이해할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;323&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;325&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;327&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;327&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangSmith는 LangChain, LangGraph 실행 흐름을 추적하고 평가하는 도구이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;328&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Trace는 함수나 Workflow의 입력, 출력, 에러를 기록한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;329&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@traceable을 사용하면 특정 함수를 LangSmith 추적 대상으로 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;330&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;run_type=&quot;tool&quot;은 개별 Task 실행 추적에 사용했다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;331&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;run_type=&quot;chain&quot;은 전체 Workflow 실행 추적에 사용했다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;332&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 변수로&amp;nbsp;LANGSMITH_API_KEY,&amp;nbsp;LANGSMITH_TRACING,&amp;nbsp;LANGSMITH_PROJECT가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;333&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API Key는 로그에 그대로 출력하지 않고 마스킹해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;334&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;baseline과 fixed 버전을 나누면 수정 전후 결과를 비교할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;335&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패 Trace를 보면 어떤 Task가 누락되었거나 어느 단계에서 예외가 발생했는지 확인할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;336&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dataset은 평가에 사용할 입력과 기대 결과 모음이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;337&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Evaluator는 실제 결과가 기대 조건을 만족하는지 점수화한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;338&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Experiment는 Dataset을 대상으로 특정 버전을 실행한 평가 기록이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;339&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;같은 Dataset으로 baseline과 fixed를 평가하면 개선 여부를 비교할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;340&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AI Workflow는 실행 결과뿐 아니라 중간 과정까지 관찰해야 디버깅할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/212</guid>
      <comments>https://namerong.tistory.com/212#entry212comment</comments>
      <pubDate>Fri, 31 Jul 2026 14:29:43 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]Agent Safety와 Plan-Execute-Replan Workflow</title>
      <link>https://namerong.tistory.com/211</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent가 Tool을 사용할 때 필요한 안전 제어와, 복잡한 작업을&amp;nbsp;계획 -&amp;gt; 실행 -&amp;gt; 검증 -&amp;gt; 재계획&amp;nbsp;흐름으로 처리하는 Workflow 구조를 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;8&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory 보완&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;thread_id&amp;nbsp;기반 대화 기억 분리&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Safety&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재시도, Fallback, Handoff 처리&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Graph의 종료 조건과&amp;nbsp;recursion_limit&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Planner&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Executor&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Validator&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Replanner&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;17&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Pydantic 기반 실행 계약 정의&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;19&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-agent-memory-보완&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;21&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Agent Memory 보완&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory는 이전 대화를 기억해 다음 요청에서 활용하는 구조이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는&amp;nbsp;Checkpointer를 사용해 같은&amp;nbsp;thread_id의 대화 메시지를 이어가도록 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;checkpointer=InMemorySaver()
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대화는&amp;nbsp;thread_id를 기준으로 분리된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;config = {
    &quot;configurable&quot;: {
        &quot;thread_id&quot;: &quot;student-1&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;같은 thread에서는 이전에 말한 이름, 동행인, 예산을 기억해 다음 Tool 호출 인자로 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;1. 내 이름은 수진이고 아이와 여행해. 전체 예산은 20만원이야.
2. 내 조건에 맞는 장소를 추천해줘.
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 번째 질문에는 이름, 동행인, 예산이 직접 들어있지 않지만, 같은 thread에 이전 대화가 저장되어 있다면 Agent가 그 정보를 찾아 Tool 인자를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;49&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-thread_id가-중요한-이유&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;51&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. thread_id가 중요한 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory에서 중요한 점은 사용자별 기억이 섞이면 안 된다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;student-1
-&amp;gt; 수진, 아이 동반, 예산 정보 있음

student-2
-&amp;gt; 이전 정보 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;63&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;student-1에서 장소 추천을 요청하면 이전 정보를 활용할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만&amp;nbsp;student-2에서 같은 질문을 하면 정보가 없으므로 Tool을 호출하지 않고 추가 질문을 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;66&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉,&amp;nbsp;thread_id는 &amp;ldquo;이 대화가 누구의 흐름인지&amp;rdquo; 구분하는 기준이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;68&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-agent-safety&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;70&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Agent Safety&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent는 Tool을 사용할 수 있지만, 모든 판단을 모델에게만 맡기면 위험할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 실패 결과에 따라 재시도할지, 중단할지, 담당자에게 넘길지 결정하는 안전 제어가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는 Tool 결과를 다음 상태로 나누었다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;77&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ResultCode&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;79&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FOUND&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색 성공&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성공 종료&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;80&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NOT_FOUND&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과 없음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제한 횟수 안에서 재시도&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;81&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TIMEOUT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일시적 실패&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제한 횟수 안에서 재시도&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;82&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AUTH_ERROR&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;권한 문제&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재시도 없이 Handoff&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;83&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NOT_RUN&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아직 실행 안 함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;초기 상태&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;85&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-retry-fallback-handoff&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;87&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. Retry, Fallback, Handoff&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;89&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패라고 해서 모두 같은 방식으로 처리하면 안 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;NOT_FOUND / TIMEOUT
-&amp;gt; 다시 시도 가능

AUTH_ERROR
-&amp;gt; 다시 시도해도 해결되지 않음
-&amp;gt; 담당자 확인 필요
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;검색 실행
-&amp;gt; 결과 확인
-&amp;gt; FOUND면 성공 종료
-&amp;gt; NOT_FOUND 또는 TIMEOUT이면 재시도
-&amp;gt; 최대 횟수 초과 시 Fallback
-&amp;gt; AUTH_ERROR면 Handoff
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fallback은 자동 처리를 더 이상 하지 않고 사용자에게 조건 변경을 요청하는 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;조건에 맞는 장소를 찾지 못했습니다.
실내, 무료, 산책 중 하나로 조건을 변경해 주세요.
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Handoff는 자동 처리로 해결할 수 없는 상황을 담당자 확인으로 넘기는 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;인증 문제로 자동 검색을 계속할 수 없습니다.
담당자 확인이 필요합니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;125&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-안전-규칙은-모델이-아니라-graph가-강제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;127&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 안전 규칙은 모델이 아니라 Graph가 강제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;129&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한 점은 재시도 여부와 최대 횟수를 모델의 판단에만 맡기지 않는다는 것이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모델은 답변을 만들고 Tool을 고를 수 있지만, 반복 제어와 종료 조건은 Graph State와 Edge에서 강제한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def route_after_guard(state):
    if result == &quot;AUTH_ERROR&quot;:
        return &quot;finalize_handoff&quot;
    if result == &quot;FOUND&quot;:
        return &quot;agent&quot;
    if result in RETRYABLE_RESULTS and state[&quot;attempts&quot;] &amp;lt; state[&quot;max_attempts&quot;]:
        return &quot;agent&quot;
    return &quot;finalize_fallback&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Agent가 계속 Tool을 호출하려 해도 Graph의 안전 규칙이 막을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;145&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-recursion_limit&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;147&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. recursion_limit&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Graph에서 종료 조건을 잘못 만들면 무한 반복이 생길 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;search_destination
-&amp;gt; search_destination
-&amp;gt; search_destination
-&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 경우&amp;nbsp;recursion_limit가 마지막 안전장치 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;graph.invoke(state, config={&quot;recursion_limit&quot;: 5})
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만&amp;nbsp;recursion_limit는 최후의 방어선일 뿐이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정상적인 Graph라면 업무 조건으로 먼저&amp;nbsp;END에 도달해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;167&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-plan-execute-replan-workflow&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;169&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. Plan-Execute-Replan Workflow&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;171&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복잡한 작업은 한 번에 처리하기보다 단계별로 나누는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;173&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는 여행 일정 생성을 다음 흐름으로 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Planner
-&amp;gt; Executor
-&amp;gt; Validator
-&amp;gt; Replanner
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;182&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 단계의 역할은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;184&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단계&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;186&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Planner&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;목표를 실행 가능한 Task 목록으로 분해&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;187&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Executor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Plan에 적힌 Task를 순서대로 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;188&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Validator&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 결과가 완료 기준을 만족하는지 검증&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;189&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Replanner&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패 원인을 보고 재시도할지 중단할지 판단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;191&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-plan&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;193&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. Plan&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;195&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Plan은 Planner와 Executor가 공유하는 실행 계약이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class Plan(BaseModel):
    goal: str
    tasks: list[TaskName]
    success_criteria: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Task는 정해진 값만 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;TaskName = Literal[
    &quot;requirements&quot;,
    &quot;weather&quot;,
    &quot;destination&quot;,
    &quot;budget&quot;,
    &quot;compose&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 Task의 의미는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;218&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Task&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;220&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;requirements&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인원, 날씨, 아이 동반, 예산 조건 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;221&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;weather&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;날씨를 실내외 장소 선택 정책으로 변환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;222&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;destination&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조건에 맞는 장소 후보 검색&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;223&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;budget&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;후보별 예상 비용 계산&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;224&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;compose&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조사와 계산 결과로 일정 초안 작성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;226&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Plan에는 같은 Task가 중복되면 안 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if len(tasks) != len(set(tasks)):
    raise ValueError(&quot;Plan에는 같은 Task를 중복할 수 없습니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;233&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 불필요한 반복 실행을 막을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;235&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-planner&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;237&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. Planner&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Planner는 사용자의 목표를 보고 필요한 Task를 선택한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;241&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 목표가 다음과 같다면:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;아이와 비 오는 날 20만원 이내의 송파 하루 일정을 만들어줘.
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;247&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 Task는 다음처럼 구성될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;requirements
-&amp;gt; weather
-&amp;gt; destination
-&amp;gt; budget
-&amp;gt; compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;257&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면 &amp;ldquo;예상 비용만 계산해줘&amp;rdquo; 같은 요청이라면 모든 Task가 필요하지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Planner는 목표를 작은 실행 단계로 분해하는 역할이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;261&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-executor&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;263&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. Executor&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;265&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Executor는 Plan에 들어 있는 Task를 순서대로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;for task in plan.tasks:
    TASK_HANDLERS[task](request, context)
    path.append(task)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;273&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TASK_HANDLERS는 Task 이름과 실제 실행 함수를 연결한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;TASK_HANDLERS = {
    &quot;requirements&quot;: execute_requirements,
    &quot;weather&quot;: execute_weather,
    &quot;destination&quot;: execute_destination,
    &quot;budget&quot;: execute_budget,
    &quot;compose&quot;: execute_compose,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Executor는 실행 중간 결과를&amp;nbsp;context에 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;requirements 결과
weather 결과
destinations 결과
budget_options 결과
draft 결과
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;295&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-context&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;297&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. Context&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;299&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;context는 Workflow 안에서 Task들이 공유하는 중간 결과 저장소이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;301&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어&amp;nbsp;destination&amp;nbsp;Task는 장소 후보를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;context[&quot;destinations&quot;] = search_destinations(request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;budget&amp;nbsp;Task는 앞에서 저장된 장소 후보를 읽어 비용을 계산한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;places = context.get(&quot;destinations&quot;, [])
context[&quot;budget_options&quot;] = calculate_budget_options(request, places)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;314&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Task들은 독립적으로 실행되는 것이 아니라 이전 Task가 만든 context를 읽고 다음 결과를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;316&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-validator&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;318&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. Validator&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;320&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Validator는 실행 결과가 목표 조건을 만족하는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;322&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검증 기준은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;324&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;324&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 Task 결과가 context에 존재하는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;325&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비 오는 날이면 실내 장소인가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;326&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아이 동반이면 아이 친화 장소인가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;327&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예상 비용이 예산을 넘지 않는가&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;328&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일정 초안이 실제로 선택되었는가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if request.weather == &quot;rain&quot; and not draft.indoor:
    failures.append(&quot;비 오는 날에는 실내 장소가 필요합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검증 결과는&amp;nbsp;ValidationReport로 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class ValidationReport(BaseModel):
    passed: bool
    failed_conditions: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;343&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성공하면 Workflow는 종료하고, 실패하면 Replanner로 넘어간다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;345&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-replanner&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;347&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. Replanner&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;349&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Replanner는 검증 실패 원인을 보고 다시 실행할지 중단할지 판단한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class ReplanDecision(BaseModel):
    action: Literal[&quot;retry&quot;, &quot;stop&quot;]
    reason: str
    plan: Plan | None = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;358&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한 규칙은&amp;nbsp;retry를 선택했다면 수정된 Plan이 반드시 있어야 한다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if self.action == &quot;retry&quot; and self.plan is None:
    raise ValueError(&quot;retry에는 수정 Plan이 필요합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패가 누락된 Task 때문에 발생했다면 Plan을 수정해 다시 실행할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 예산이 너무 낮아서 어떤 장소도 조건을 만족하지 못한다면 Plan을 바꿔도 해결되지 않을 수 있다. 이 경우&amp;nbsp;stop이 더 적절하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;368&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;15-전체-workflow-흐름&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;370&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;15. 전체 Workflow 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;372&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 Graph 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;haml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; planner
-&amp;gt; executor
-&amp;gt; validator
-&amp;gt; 검증 성공이면 END
-&amp;gt; 검증 실패이고 재시도 가능하면 replanner
-&amp;gt; retry면 executor
-&amp;gt; stop이면 END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;385&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 구조의 장점은 문제 발생 지점을 분리해서 확인할 수 있다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Plan이 이상한가?
Task 실행 결과가 빠졌는가?
Draft가 조건을 만족하지 못했는가?
재계획으로 해결 가능한 문제인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;394&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;16-agent-safety와-workflow의-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;396&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;16. Agent Safety와 Workflow의 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Safety는 Tool 실행 중 실패를 안전하게 제어하는 데 집중한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Tool 실패
-&amp;gt; 재시도 가능 여부 판단
-&amp;gt; Fallback 또는 Handoff
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Plan-Execute-Replan Workflow는 복잡한 목표를 단계별 작업으로 나누고, 결과를 검증한 뒤 필요하면 계획을 수정하는 데 집중한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;목표
-&amp;gt; 계획
-&amp;gt; 실행
-&amp;gt; 검증
-&amp;gt; 재계획
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;418&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;목적&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;420&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Safety&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 실행 실패를 안전하게 제어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;421&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복잡한 작업을 계획, 실행, 검증, 재계획으로 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;423&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;17-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;425&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;17. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;427&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;427&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory는 같은&amp;nbsp;thread_id의 대화 메시지를 이어준다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;428&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른&amp;nbsp;thread_id끼리는 기억이 섞이면 안 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;429&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent가 이전 대화를 바탕으로 Tool 인자를 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;430&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 정보를 수정하면 가장 최근 값을 기준으로 판단해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;431&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Safety는 실패 종류에 따라 재시도, Fallback, Handoff를 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;432&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NOT_FOUND,&amp;nbsp;TIMEOUT은 재시도 가능한 실패로 볼 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;433&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AUTH_ERROR는 재시도하지 않고 Handoff하는 것이 적절하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;434&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재시도 횟수는&amp;nbsp;max_attempts로 제한해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;435&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Graph의 반복은 반드시 종료 조건을 가져야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;436&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;recursion_limit는 무한 반복을 막는 최후의 안전장치이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;437&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Planner는 목표를 실행 가능한 Task 목록으로 분해한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;438&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Executor는 Plan의 Task를 순서대로 실행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;439&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Context는 Task 사이의 중간 결과를 저장한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;440&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Validator는 실행 결과가 조건을 만족하는지 검증한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;441&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Replanner는 실패 원인을 보고 retry 또는 stop을 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;442&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Pydantic 모델을 사용하면 Planner, Executor, Validator 사이의 데이터 계약을 명확히 만들 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;443&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복잡한 Agentic 작업은 &amp;ldquo;모델의 판단&amp;rdquo;과 &amp;ldquo;코드가 강제하는 규칙&amp;rdquo;을 분리해야 더 안전하게 운영할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/211</guid>
      <comments>https://namerong.tistory.com/211#entry211comment</comments>
      <pubDate>Thu, 30 Jul 2026 15:02:55 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]Agentic AI, Tool Calling, ReAct, Agent Memory</title>
      <link>https://namerong.tistory.com/210</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agentic AI의 기본 구조를 학습했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이전까지는 RAG, VectorStore, LangGraph처럼 정해진 흐름을 구성하는 데 집중했다면, 이번에는 모델이 상황에 따라 Tool을 선택하고 실행 결과를 보고 다음 행동을 결정하는 구조를 다뤘다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow와 Agent 차이&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool Contract와 Schema&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangChain&amp;nbsp;create_agent&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool Calling 메시지 흐름&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ReAct 구조&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Action, Observation, Final Answer&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Checkpointer 기반 Agent Memory&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;thread_id를 통한 대화 기억 분리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-workflow와-agent-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;20&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Workflow와 Agent 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow는 개발자가 실행 순서를 미리 정해두는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;질문 분석
-&amp;gt; 장소 검색이 필요하면 search_destination 실행
-&amp;gt; 예산 계산이 필요하면 estimate_day_budget 실행
-&amp;gt; 답변 작성
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 어떤 Tool을 언제 실행할지 개발자가&amp;nbsp;if문으로 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if needs_destination:
    search_destination.invoke(...)

if needs_budget:
    estimate_day_budget.invoke(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면 Agent는 모델이 질문과 Tool 설명을 보고 실행 중에 어떤 Tool을 사용할지 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;사용자 질문
-&amp;gt; 모델이 Tool Schema 확인
-&amp;gt; 필요한 Tool 선택
-&amp;gt; Tool 실행
-&amp;gt; 실행 결과를 보고 최종 답변
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;51&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분결정 주체특징&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;53&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-line=&quot;55&quot;&gt;
&lt;tr data-line=&quot;55&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발자&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름이 명확하고 예측 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;56&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모델&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;질문에 따라 실행 중 Tool 선택&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;58&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순하고 고정된 흐름이면 Workflow가 더 안전하고 충분하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 질문이 다양하고 어떤 Tool이 필요할지 매번 달라진다면 Agent 구조가 유용하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;61&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-tool-contract&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;63&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Tool Contract&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;65&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent가 Tool을 사용하려면 단순 Python 함수만 있어서는 부족하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모델은 함수 내부 코드를 직접 이해하는 것이 아니라, Tool의 이름, 설명, 입력 Schema를 보고 사용 방법을 판단한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@tool(args_schema=BudgetInput)
def estimate_day_budget(
    travelers: int,
    ticket_price: int,
    meal_budget: int
) -&amp;gt; int:
    return travelers * (ticket_price + meal_budget)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;78&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool Contract의 핵심은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요소의미&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;80&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-line=&quot;82&quot;&gt;
&lt;tr data-line=&quot;82&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 이름&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모델이 호출할 기능 이름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;83&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 설명&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;언제 이 도구를 써야 하는지 알려주는 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;84&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입력 Schema&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 인자를 어떤 타입으로 받아야 하는지 정의&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;86&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 예산 계산 Tool은 다음 입력을 요구한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class BudgetInput(BaseModel):
    travelers: int
    ticket_price: int
    meal_budget: int
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;95&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 Schema가 있으면 모델이 Tool을 호출할 때 필요한 인자 구조를 맞출 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;97&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-schema-검증과-업무-규칙-검증&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;99&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Schema 검증과 업무 규칙 검증&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;101&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool에는 두 종류의 검증이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 번째는 Schema 검증이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필수 필드가 빠졌거나 타입이 맞지 않으면 함수 실행 전에 막는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;travelers 필수
ticket_price 필수
meal_budget 필수
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;112&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 번째는 업무 규칙 검증이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;타입은 맞지만 값이 업무적으로 말이 안 되는 경우 함수 내부에서 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;if travelers &amp;lt; 1:
    raise ValueError(&quot;여행 인원은 1명 이상이어야 합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;120&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분처리 위치예시&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;122&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-line=&quot;124&quot;&gt;
&lt;tr data-line=&quot;124&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Schema 검증&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수 실행 전&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필수값 누락, 타입 오류&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;125&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;업무 규칙 검증&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수 내부&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인원 0명, 음수 금액&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;127&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색 결과 없음은 입력 오류가 아니라 정상 실행 결과로 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;검색 결과 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;133&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 경우는 예외라기보다 &amp;ldquo;검색은 했지만 조건에 맞는 데이터가 없음&amp;rdquo;으로 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;135&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-create_agent&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;137&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. create_agent&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;create_agent는 모델과 Tool을 연결해 Agent를 만드는 함수이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;agent = create_agent(
    model=model,
    tools=[estimate_day_budget, search_destination],
    system_prompt=&quot;너는 여행 도우미다. 필요한 경우에만 Tool을 사용한다.&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent는 사용자 질문을 보고 다음을 판단한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Tool이 필요한가?
어떤 Tool이 필요한가?
Tool 인자는 무엇인가?
Tool 결과를 어떻게 최종 답변에 반영할 것인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 질문이 다음과 같다면:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;3명이 입장료 35000원, 식비 20000원씩 쓰면 얼마야?
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent는&amp;nbsp;estimate_day_budget&amp;nbsp;Tool을 선택하고 다음 인자를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;travelers&quot;: 3,
  &quot;ticket_price&quot;: 35000,
  &quot;meal_budget&quot;: 20000
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;174&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-tool-calling-메시지-흐름&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;176&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. Tool Calling 메시지 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;178&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent 실행 결과는 단순 문자열 하나가 아니라 여러 메시지 흐름으로 구성된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;HumanMessage
-&amp;gt; AIMessage with tool_calls
-&amp;gt; ToolMessage
-&amp;gt; AIMessage final answer
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;187&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 메시지의 의미는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메시지의미&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;189&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-line=&quot;191&quot;&gt;
&lt;tr data-line=&quot;191&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HumanMessage&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 질문&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;192&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AIMessage&amp;nbsp;with&amp;nbsp;tool_calls&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모델이 Tool 실행을 요청한 메시지&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;193&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ToolMessage&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 실행 결과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;194&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AIMessage&amp;nbsp;final&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 결과를 반영한 최종 답변&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Agent는 바로 답변하지 않고 중간에 Tool을 호출할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;질문
-&amp;gt; Action: Tool 호출
-&amp;gt; Observation: Tool 결과
-&amp;gt; Final Answer: 최종 답변
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;205&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-react-구조&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;207&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. ReAct 구조&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ReAct는&amp;nbsp;Reasoning + Acting의 흐름을 가진 Agent 패턴이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Action 1
-&amp;gt; Observation 1
-&amp;gt; Action 2
-&amp;gt; Observation 2
-&amp;gt; Final Answer
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 장소 검색에서 처음에는 strict 모드로 정확히 검색한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Action 1: strict 검색
Observation 1: NOT_FOUND
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색 결과가 없으면 Tool 결과의&amp;nbsp;retry_hint를 보고 broad 모드로 다시 검색한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Action 2: broad 검색
Observation 2: FOUND
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 성공한 결과를 근거로 최종 답변한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;Final Answer
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;241&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ReAct의 핵심은 Tool 결과를 보고 다음 행동을 바꿀 수 있다는 점이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 한 번 Tool을 호출하고 끝나는 구조보다 더 유연하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;244&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-toolnode와-tools_condition&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;246&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. ToolNode와 tools_condition&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;248&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangGraph에서는 Agent와 Tool 실행을 Graph로 구성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;builder.add_node(&quot;agent&quot;, call_model)
builder.add_node(&quot;tools&quot;, ToolNode(tools))
builder.add_conditional_edges(&quot;agent&quot;, tools_condition)
builder.add_edge(&quot;tools&quot;, &quot;agent&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;257&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; agent
-&amp;gt; tool_calls가 있으면 tools
-&amp;gt; Tool 실행 후 다시 agent
-&amp;gt; tool_calls가 없으면 END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;tools_condition은 마지막 AIMessage에 Tool 호출이 있는지 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;269&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;269&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 호출이 있으면&amp;nbsp;tools&amp;nbsp;노드로 이동&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;270&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 호출이 없으면 종료&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Agent가 더 이상 Tool이 필요 없다고 판단할 때까지&amp;nbsp;agent -&amp;gt; tools -&amp;gt; agent&amp;nbsp;흐름을 반복할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;274&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-정보-부족-시-tool-호출하지-않기&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;276&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. 정보 부족 시 Tool 호출하지 않기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;278&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent가 항상 Tool을 호출하면 안 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예산 계산에는 최소한 인원, 입장료, 식비가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;3명 여행 예산을 계산해줘
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 질문에는 인원은 있지만 입장료와 식비가 없다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 모델이 값을 추측해서 Tool을 호출하면 잘못된 결과가 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;288&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;올바른 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;정보 부족 확인
-&amp;gt; Tool 호출하지 않음
-&amp;gt; 사용자에게 필요한 값 질문
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent를 만들 때 system prompt에 이런 규칙을 명확히 넣어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;인원, 입장료, 식비 중 하나라도 없으면 값을 추측하지 말고 사용자에게 질문한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;302&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-agent-memory&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;304&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. Agent Memory&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;306&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory는 이전 대화를 기억해 다음 Tool 호출에 활용하는 구조이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;308&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는&amp;nbsp;InMemorySaver를 checkpointer로 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;create_agent(
    model=ChatOpenAI(...),
    tools=[recommend_destination],
    checkpointer=InMemorySaver(),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고&amp;nbsp;thread_id로 대화 세션을 구분했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;config = {
    &quot;configurable&quot;: {
        &quot;thread_id&quot;: &quot;student-1&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;328&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;같은&amp;nbsp;thread_id로 대화를 이어가면 이전 메시지가 checkpoint에 저장되어 다음 요청에서 다시 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;1. 내 이름은 수진이고 아이와 여행해. 전체 예산은 20만원이야.
2. 내 조건에 맞는 장소를 추천해줘.
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 번째 질문에는 이름, 동행인, 예산이 직접 들어있지 않다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 같은 thread에 이전 대화가 저장되어 있으면 Agent가 그 정보를 찾아 Tool 인자로 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;338&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-thread_id로-기억-분리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;340&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. thread_id로 기억 분리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;342&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Memory에서 중요한 점은 사용자별 또는 세션별 기억이 섞이면 안 된다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;student-1 thread
-&amp;gt; 수진, 아이, 예산 20만원

student-2 thread
-&amp;gt; 이전 정보 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;student-1에서 장소 추천을 요청하면 이전 정보를 사용할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만&amp;nbsp;student-2에서 같은 질문을 하면 이전 정보가 없으므로 Tool을 호출하지 않고 추가 질문을 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉,&amp;nbsp;thread_id는 대화 기억을 분리하는 기준이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;357&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-수정된-정보는-최신-값-사용&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;359&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. 수정된 정보는 최신 값 사용&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;361&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 이전 정보를 바꿀 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;전체 예산을 15만원으로 변경해줘
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;367&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 추천을 요청하면 Agent는 예전 예산 20만원이 아니라 최신 예산 15만원을 사용해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;이전 값: 20만원
수정 값: 15만원
Tool 인자: max_budget = 150000
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;375&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory는 단순히 과거를 저장하는 것뿐만 아니라, 대화 안에서 가장 최근 정보를 기준으로 판단해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;377&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;379&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;381&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;381&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Workflow는 개발자가 실행 순서를 정하는 구조이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;382&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent는 모델이 질문과 Tool Schema를 보고 필요한 Tool을 선택하는 구조이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;383&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool Contract는 Tool 이름, 설명, 입력 Schema로 구성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;384&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반 함수와 달리 LangChain Tool은 모델에게 사용 방법을 설명할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;385&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Schema 검증은 함수 실행 전 입력 구조를 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;386&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;업무 규칙 검증은 함수 내부에서 값의 의미를 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;387&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;create_agent는 모델과 Tool을 연결해 Agent를 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;388&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool Calling은&amp;nbsp;AIMessage -&amp;gt; ToolMessage -&amp;gt; Final Answer&amp;nbsp;흐름으로 진행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;389&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ReAct는 Action과 Observation을 반복하며 다음 행동을 조정하는 구조이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;390&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tool 결과가&amp;nbsp;NOT_FOUND이면 Agent는 다른 방식으로 재검색할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;391&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정보가 부족하면 Tool을 호출하지 않고 사용자에게 추가 질문해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;392&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangGraph의&amp;nbsp;ToolNode는 실제 Tool 실행을 담당한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;393&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;tools_condition은 Tool 호출 여부에 따라 다음 노드를 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;394&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Checkpointer는 같은&amp;nbsp;thread_id의 대화 메시지를 이어준다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;395&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Agent Memory는 이전 대화를 바탕으로 Tool 인자를 생성할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;396&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른&amp;nbsp;thread_id끼리는 기억이 섞이면 안 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;397&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 정보를 수정하면 Agent는 가장 최근 값을 기준으로 판단해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/210</guid>
      <comments>https://namerong.tistory.com/210#entry210comment</comments>
      <pubDate>Wed, 29 Jul 2026 17:01:16 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]FastAPI 파일 브릿지와 LangGraph 기초</title>
      <link>https://namerong.tistory.com/209</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI와 Spring을 HTTP로 연결해 파일 업로드를 처리하는 구조와, LangGraph를 사용해 AI 작업 흐름을 Graph 형태로 구성하는 방법을 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;8&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI 파일 업로드 API 구현&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서&amp;nbsp;MultipartFile을 받아 FastAPI로 전달&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RestClient를 사용한 서버 간 HTTP 통신&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangChain Chain과 LangGraph Graph 차이&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangGraph의&amp;nbsp;State,&amp;nbsp;Node,&amp;nbsp;Edge&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reducer를 사용한 State 병합&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조건 분기 라우팅&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RAG 흐름을 Graph로 구성&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;질문 의도에 따라 RAG 또는 일반 답변으로 분기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-fastapi-파일-업로드-api&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;20&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. FastAPI 파일 업로드 API&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI에서는 파일과 일반 값을 함께 받기 위해&amp;nbsp;multipart/form-data&amp;nbsp;형식을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@app.post(&quot;/api/v1/files&quot;, response_model=FileInfo, status_code=201)
async def upload_file(
    file: Annotated[UploadFile, File()],
    description: Annotated[str, Form(min_length=1, max_length=100)]
):
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 역할은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;34&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;36&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UploadFile&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;업로드된 파일 객체&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;37&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;File()&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;multipart 요청의 파일 part&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;38&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Form()&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;multipart 요청의 일반 입력값&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;39&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;description&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 하나의 요청 안에 파일과 설명을 함께 담아 보낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;43&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-파일-검증-흐름&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;45&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 파일 검증 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 업로드에서는 아무 파일이나 저장하면 안 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 다음 조건을 검증했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;50&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;50&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;허용된 Content-Type인지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;51&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;확장자와 Content-Type이 일치하는지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;52&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 크기가 1MB 이하인지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;53&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원본 파일명에서 경로 정보 제거&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;54&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장 파일명은 UUID로 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;ALLOWED_TYPES = {
    &quot;text/plain&quot;: &quot;.txt&quot;,
    &quot;application/pdf&quot;: &quot;.pdf&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;content = await file.read(MAX_FILE_SIZE + 1)

if len(content) &amp;gt; MAX_FILE_SIZE:
    raise HTTPException(status_code=413, detail=&quot;파일은 1MB 이하여야 합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;70&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일명은 사용자가 보낸 이름을 그대로 쓰지 않고 UUID를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;file_id = str(uuid4())
stored_name = f&quot;{file_id}{suffix}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;77&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 파일명 충돌을 줄이고, 사용자가 보낸 파일명에 의존하지 않는 저장 구조를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;79&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-파일-metadata-저장&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;81&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 파일 Metadata 저장&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 자체는&amp;nbsp;.txt&amp;nbsp;또는&amp;nbsp;.pdf로 저장하고, 파일 정보는 별도의&amp;nbsp;.json&amp;nbsp;파일로 저장했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;metadata_path(file_id).write_text(
    json.dumps(info.model_dump(), ensure_ascii=False, indent=2),
    encoding=&quot;utf-8&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장되는 정보는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;94&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;96&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;id&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 고유 ID&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;97&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;original_name&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 업로드한 원본 파일명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;98&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;content_type&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 MIME 타입&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;99&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;size&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 크기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;100&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;description&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;101&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;stored_name&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에 저장된 파일명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일과 metadata를 분리하면 나중에 목록 조회, 다운로드, 삭제 기능을 만들 때 관리하기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;105&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-spring과-fastapi-파일-브릿지&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;107&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. Spring과 FastAPI 파일 브릿지&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;109&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 서버는 클라이언트로부터 파일을 받고, 그 파일을 다시 FastAPI 서버로 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;브라우저
-&amp;gt; Spring Controller
-&amp;gt; Spring RestClient
-&amp;gt; FastAPI 파일 업로드 API
-&amp;gt; FastAPI 저장
-&amp;gt; Spring 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;122&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한 점은 Spring이 Python 함수를 직접 실행하는 것이 아니라, FastAPI 서버에 HTTP 요청을 보낸다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Controller에서는 파일을 다음처럼 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity&amp;lt;?&amp;gt; upload(
        @RequestPart(&quot;file&quot;) MultipartFile file,
        @RequestParam(&quot;description&quot;) String description
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;134&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고&amp;nbsp;FastApiFileClient에서 FastAPI로 다시 multipart 요청을 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;processing&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;MultiValueMap&amp;lt;String, Object&amp;gt; body = new LinkedMultiValueMap&amp;lt;&amp;gt;();
body.add(&quot;file&quot;, new HttpEntity&amp;lt;&amp;gt;(resource, fileHeaders));
body.add(&quot;description&quot;, description);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;142&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-restclient와-multipart-전달&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;144&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. RestClient와 Multipart 전달&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서 외부 API를 호출할 때&amp;nbsp;RestClient를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;return restClient.post()
        .uri(&quot;/api/v1/files&quot;)
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body(body)
        .retrieve()
        .body(FileInfo.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;157&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MultipartFile은 그대로 외부 API에 넣을 수 없기 때문에&amp;nbsp;ByteArrayResource로 감싸서 전달했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
    @Override
    public String getFilename() {
        return file.getOriginalFilename();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;168&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서&amp;nbsp;getFilename()을 오버라이드하는 이유는 FastAPI 쪽에서 원본 파일명을 인식할 수 있게 하기 위해서다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;170&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-외부-api-예외-처리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;172&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 외부 API 예외 처리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI에서 파일 형식이 잘못되었거나 서버 연결이 실패할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서는 이 경우를 나누어 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;catch(RestClientResponseException e) {
    return ResponseEntity.status(e.getStatusCode())
            .body(Map.of(
                    &quot;code&quot;, &quot;FASTAPI_REJECTED&quot;,
                    &quot;message&quot;, e.getResponseBodyAsString()
            ));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;d&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;catch(IOException | RestClientException e) {
    return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
            .body(Map.of(
                    &quot;code&quot;, &quot;UPLOAD_FAILED&quot;,
                    &quot;message&quot;, &quot;파일 전달에 실패했습니다.&quot;
            ));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;197&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;199&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상황&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;응답&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;201&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI가 파일을 거절&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI의 상태 코드 전달&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;202&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 읽기 실패 또는 통신 실패&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;502 Bad Gateway&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;203&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI 서버 연결 불가&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;503 Service Unavailable&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부 서버와 연동할 때는 성공 흐름뿐 아니라 실패 흐름도 반드시 고려해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;207&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-langchain-chain과-langgraph-graph-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;209&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. LangChain Chain과 LangGraph Graph 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangChain Chain은 작업 순서가 고정된 파이프라인에 적합하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;verilog&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;retrieve -&amp;gt; generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 항상 검색 후 답변 생성만 한다면 Chain으로 충분하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;219&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 질문에 따라 실행 경로가 달라지거나, 중간 상태를 여러 노드가 공유하거나, 분기와 반복이 필요하면 LangGraph가 더 적합하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;haml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; classify
-&amp;gt; 조건에 따라 budget / destination / general
-&amp;gt; END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;230&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적합한 상황&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;232&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Chain&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;순서가 고정된 단순 흐름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;233&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Graph&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분기, 반복, 조건 처리, 상태 공유가 필요한 흐름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;235&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-langgraph의-state-node-edge&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;237&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. LangGraph의 State, Node, Edge&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangGraph는 작업 흐름을&amp;nbsp;State,&amp;nbsp;Node,&amp;nbsp;Edge로 구성한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;241&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;243&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;State&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 Node가 공유하는 데이터&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;244&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 작업을 수행하는 함수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;245&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Edge&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node 사이의 실행 순서&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;246&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;START&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래프 시작 지점&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;247&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;END&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래프 종료 지점&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예시는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class TripState(TypedDict):
    place: str
    travelers: int
    ticket_price: int
    total_price: int
    message: str
    path: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;261&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node는 State 전체를 반환하지 않아도 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자신이 변경할 값만&amp;nbsp;dict로 반환하면 LangGraph가 기존 State에 합쳐준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def calculate_total(state: TripState) -&amp;gt; dict:
    total = state[&quot;travelers&quot;] * state[&quot;ticket_price&quot;]
    return {&quot;total_price&quot;: total}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;270&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-reducer&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;272&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. Reducer&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 같은 State 필드를 여러 번 업데이트하면 새 값으로 덮어써질 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 리스트처럼 계속 누적해야 하는 값은 덮어쓰기보다 합치기가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;277&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 사용하는 것이 Reducer이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class ResearchState(TypedDict):
    topic: str
    notes: Annotated[list[str], operator.add]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;operator.add를 사용하면 여러 Node가 반환한&amp;nbsp;notes가 덮어써지지 않고 이어 붙는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;destination note
+ budget note
+ meal note
+ transport note
= 최종 notes
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;295&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reducer는 여러 Node가 같은 State 필드를 업데이트할 때 &amp;ldquo;어떻게 합칠지&amp;rdquo; 정하는 규칙이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;297&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-conditional-routing&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;299&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. Conditional Routing&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;301&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 질문에 같은 작업을 실행할 필요는 없다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예산 질문이면 예산 노드로, 장소 질문이면 장소 노드로, 일반 질문이면 일반 노드로 보내는 것이 더 효율적이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;304&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 질문을 분류하는 Router Node를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def classify(state: RouteState) -&amp;gt; dict:
    if &quot;입장료&quot; in state[&quot;question&quot;]:
        route = &quot;budget&quot;
    elif &quot;장소&quot; in state[&quot;question&quot;]:
        route = &quot;destination&quot;
    else:
        route = &quot;general&quot;
    return {&quot;route&quot;: route}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;317&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고&amp;nbsp;add_conditional_edges로 다음 노드를 선택한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;builder.add_conditional_edges(&quot;classify&quot;, choose_route)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;haml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; classify
-&amp;gt; budget 또는 destination 또는 general
-&amp;gt; END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조건 분기를 사용하면 질문 의도에 맞는 작업만 실행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;334&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-rag-graph&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;336&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. RAG Graph&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RAG도 LangGraph로 구성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;verilog&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; retrieve
-&amp;gt; generate
-&amp;gt; END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;347&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 Node의 역할은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;349&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;351&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;retrieve&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VectorStore에서 질문과 관련 있는 문서 검색&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;352&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;generate&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색된 context를 근거로 LLM 답변 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;pf&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def retrieve(state: RagState) -&amp;gt; dict:
    documents = store.similarity_search(state[&quot;question&quot;], k=2)
    return {
        &quot;context&quot;: [document.page_content for document in documents],
        &quot;path&quot;: [&quot;retrieve&quot;]
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def generate(state: RagState) -&amp;gt; dict:
    prompt = (
        &quot;아래 문서만 근거로 답하세요.\n&quot;
        f&quot;[문서]\n{context}\n[질문]\n{state['question']}&quot;
    )
    answer = model.invoke(prompt).content
    return {&quot;answer&quot;: answer}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;373&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 구조의 장점은 RAG가 틀렸을 때 어느 단계가 문제인지 확인하기 쉽다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;xl&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;검색된 context가 이상함 -&amp;gt; Retrieval 문제
context는 맞는데 답변이 이상함 -&amp;gt; Generation 문제
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;380&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-question-router&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;382&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. Question Router&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;384&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;질문에 따라 RAG를 사용할지 일반 답변을 사용할지 나누는 구조도 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;START
-&amp;gt; classify
-&amp;gt; RAG 질문이면 retrieve -&amp;gt; rag_answer
-&amp;gt; 일반 질문이면 general
-&amp;gt; END
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;394&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 다음 질문은 RAG가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;아쿠아리움 입장료는 얼마야?
무료 산책 장소는 어디야?
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;401&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 질문들은 수업용 문서에 있는 정보를 근거로 답해야 하므로&amp;nbsp;retrieve를 거친다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;403&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면 다음 질문은 일반 답변으로 처리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;여행 가방을 가볍게 싸는 방법은?
아이 여행 준비물은 무엇이야?
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;410&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 질문은 특정 문서 검색이 꼭 필요하지 않으므로&amp;nbsp;general&amp;nbsp;노드로 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 불필요한 Embedding 검색 비용을 줄이고, 질문 성격에 맞는 처리 흐름을 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;414&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;416&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;418&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;418&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI는&amp;nbsp;UploadFile,&amp;nbsp;File,&amp;nbsp;Form으로 파일과 일반 값을 함께 받을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;419&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 업로드 요청은&amp;nbsp;multipart/form-data&amp;nbsp;형식을 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;420&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 저장 전 Content-Type, 확장자, 크기 검증이 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;421&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장 파일명은 UUID를 사용하면 충돌을 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;422&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring은&amp;nbsp;RestClient로 FastAPI API를 호출할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;423&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring이 Python 함수를 직접 실행하는 것이 아니라 HTTP 요청으로 FastAPI와 통신한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;424&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MultipartFile을 외부 API로 전달할 때는&amp;nbsp;ByteArrayResource와&amp;nbsp;MultiValueMap을 사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;425&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부 API 연동에서는&amp;nbsp;RestClientResponseException,&amp;nbsp;RestClientException&amp;nbsp;등 실패 상황을 나누어 처리해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;426&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangChain Chain은 고정된 순서의 작업에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;427&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LangGraph는 분기, 반복, 상태 공유가 필요한 작업 흐름에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;428&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;State는 그래프 전체에서 공유되는 데이터이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;429&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Node는 State를 읽고 일부 값을 업데이트하는 작업 단위이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;430&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Edge는 Node 사이의 실행 순서를 정의한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;431&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reducer는 같은 State 필드를 여러 Node가 업데이트할 때 병합 방식을 정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;432&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Conditional Routing은 질문 의도에 따라 다음 실행 노드를 선택하는 방식이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;433&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RAG Graph는 검색 단계와 답변 생성 단계를 분리해 디버깅하기 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;434&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Question Router를 사용하면 RAG가 필요한 질문과 일반 질문을 나누어 처리할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/209</guid>
      <comments>https://namerong.tistory.com/209#entry209comment</comments>
      <pubDate>Tue, 28 Jul 2026 15:47:50 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]FastAPI REST API, Router 분리, File Upload, Spring 연동</title>
      <link>https://namerong.tistory.com/208</link>
      <description>&lt;h2 id=&quot;1-학습-주제&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 학습 주제&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI로 REST API를 구성하고, 기능별 Router 분리, 파일 입출력, 파일 업로드, Spring 서버와 FastAPI 서버를 HTTP로 연결하는 흐름을 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;8&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REST CRUD API 구현&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;9&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GET,&amp;nbsp;PUT,&amp;nbsp;PATCH&amp;nbsp;차이 이해&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;10&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;APIRouter로 기능별 API 분리&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;11&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Pydantic Schema 재사용&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;12&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Python&amp;nbsp;Path를 활용한 파일 입출력&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;13&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;multipart/form-data&amp;nbsp;기반 파일 업로드&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 확장자, Content-Type, 용량 검증&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;15&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&amp;nbsp;RestClient로 FastAPI 호출&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 서버가 FastAPI 서버를 중간에서 연결하는 구조&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-line=&quot;18&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;2-rest-crud-api&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;20&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. REST CRUD API&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REST API는 URL과 HTTP Method를 조합해서 자원을 다루는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 실습에서는&amp;nbsp;Task&amp;nbsp;자원을 기준으로 목록 조회, 단건 조회, 전체 수정, 부분 수정을 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;GET /api/v1/tasks
GET /api/v1/tasks/{task_id}
PUT /api/v1/tasks/{task_id}
PATCH /api/v1/tasks/{task_id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 Method의 역할은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;35&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Method&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;37&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GET&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 조회&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;38&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;POST&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새 데이터 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;39&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PUT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 데이터를 전체 교체&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;40&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PATCH&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 데이터 일부 수정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;41&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DELETE&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;43&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;3-get-목록-조회와-필터링&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;45&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. GET 목록 조회와 필터링&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;목록 조회에서는 Query Parameter를 사용해 조건 검색을 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;def list_tasks(
    completed: bool | None = None,
    priority: Literal[&quot;low&quot;, &quot;normal&quot;, &quot;high&quot;] | None = None,
    keyword: Annotated[str | None, Query(min_length=2)] = None
):
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;57&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 예시는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;GET /api/v1/tasks?completed=true
GET /api/v1/tasks?priority=high
GET /api/v1/tasks?keyword=FastAPI
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;65&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Query Parameter는 특정 자원 하나를 찾기보다는 목록에서 조건을 걸 때 자주 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;67&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;4-put과-patch-차이&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;69&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. PUT과 PATCH 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;71&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PUT은 자원 전체를 교체하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;replaced = TaskResponse(id=task_id, **request.model_dump())
tasks[task_id] = replaced
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;78&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 클라이언트가&amp;nbsp;title,&amp;nbsp;description,&amp;nbsp;priority,&amp;nbsp;completed를 모두 보내야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 값 중 일부만 유지하고 싶어도 전체 데이터를 다시 보내는 방식에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면&amp;nbsp;PATCH는 일부 필드만 수정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;changed_fields = request.model_dump(exclude_unset=True)
updated = task.model_copy(update=changed_fields)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;88&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 핵심은&amp;nbsp;exclude_unset=True이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트가 실제로 보낸 필드만 추려서 수정하기 때문에, 보내지 않은 필드는 기존 값이 유지된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;PUT = 전체 교체
PATCH = 일부 수정
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;96&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;5-apirouter로-api-분리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;98&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. APIRouter로 API 분리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API가 많아지면 하나의&amp;nbsp;main.py에 모든 코드를 넣기 어렵다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 FastAPI에서는&amp;nbsp;APIRouter를 사용해 기능별로 API를 나눌 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;router = APIRouter(prefix=&quot;/students&quot;, tags=[&quot;students&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 메인 앱에서 router를 조립한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;app.include_router(courses.router, prefix=&quot;/api/v1&quot;)
app.include_router(students.router, prefix=&quot;/api/v1&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;114&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 구조를 사용하면 다음처럼 역할을 나눌 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;main.py
-&amp;gt; FastAPI 앱 생성
-&amp;gt; router 등록
-&amp;gt; 공통 설정 관리

students.py
-&amp;gt; 학생 API 담당

courses.py
-&amp;gt; 강좌 API 담당

schemas.py
-&amp;gt; 공통 요청/응답 모델 관리
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서 Controller를 기능별로 나누는 것과 비슷한 느낌으로 이해할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;134&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;6-schema-분리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;136&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. Schema 분리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;138&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;schemas.py에는 여러 router에서 함께 사용하는 Pydantic 모델을 분리했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class StudentCreate(BaseModel):
    name: str = Field(min_length=2, max_length=30)
    level: Level = &quot;beginner&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;class CourseResponse(BaseModel):
    id: int
    title: str
    level: Level
    hours: int
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Schema를 분리하면 API 파일에서는 요청 처리 흐름에 집중할 수 있고, 데이터 구조는 한 곳에서 관리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;156&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;7-python-file-io&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;158&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. Python File I/O&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;160&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 입출력에서는&amp;nbsp;pathlib.Path를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / &quot;data&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 읽기와 쓰기는 다음처럼 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;path.read_text(encoding=&quot;utf-8&quot;)
path.write_text(content, encoding=&quot;utf-8&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;바이너리 파일은&amp;nbsp;read_bytes(),&amp;nbsp;write_bytes()를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;target.write_bytes(source.read_bytes())
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;180&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;텍스트 파일과 이미지, PDF 같은 바이너리 파일은 처리 방식이 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #cccccc; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;182&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용 메서드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;184&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;텍스트&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;read_text,&amp;nbsp;write_text&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;185&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;바이너리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;read_bytes,&amp;nbsp;write_bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-line=&quot;187&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;8-안전한-파일-경로-처리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;189&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 안전한 파일 경로 처리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;191&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일명을 그대로 조합하면 사용자가&amp;nbsp;../&amp;nbsp;같은 경로를 넣어 data 폴더 밖 파일에 접근할 위험이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서&amp;nbsp;resolve()를 사용해 실제 경로를 계산하고, 기준 폴더 안에 있는지 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;candidate = (DATA_DIR / filename).resolve()

if DATA_DIR.resolve() not in candidate.parents:
    raise ValueError(&quot;data폴더 밖의 경로는 사용할 수 없습니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;202&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 API에서는 단순히 저장하고 읽는 것뿐 아니라, 허용된 폴더 밖으로 나가지 못하게 막는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;204&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;9-form과-file-upload&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;206&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. Form과 File Upload&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;208&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 업로드는 일반 JSON 요청과 다르게&amp;nbsp;multipart/form-data&amp;nbsp;형식을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;210&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI에서는 파일은&amp;nbsp;File, 일반 폼 값은&amp;nbsp;Form으로 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;async def upload_file(
    file: Annotated[UploadFile, File()],
    description: Annotated[str, Form(min_length=1, max_length=100)]
):
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;219&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청에는 다음 값이 함께 들어간다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;file = 업로드 파일
description = 파일 설명
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;226&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 파일과 일반 문자열 값을 하나의 요청으로 함께 보낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;228&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;10-uploadfile-검증&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;230&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. UploadFile 검증&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 업로드에서는 다음 검증을 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-line=&quot;234&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-line=&quot;234&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;허용된 Content-Type인지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;235&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 확장자와 Content-Type이 일치하는지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;236&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 크기가 1MB 이하인지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;237&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원본 파일명에서 경로 정보를 제거&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;238&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UUID로 저장 파일명 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;content = await file.read(MAX_FILE_SIZE + 1)

if len(content) &amp;gt; MAX_FILE_SIZE:
    raise HTTPException(status_code=413, detail=&quot;파일은 1MB 이하여야 합니다.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;247&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일명은 사용자가 보낸 값을 그대로 저장하지 않고 UUID를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;file_id = str(uuid4())
stored_name = f&quot;{file_id}{suffix}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;254&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 같은 이름의 파일이 업로드되어도 충돌을 줄일 수 있고, 사용자가 보낸 파일명에 의존하지 않아도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;256&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;11-파일-metadata-저장&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;258&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. 파일 Metadata 저장&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 본문은&amp;nbsp;.txt&amp;nbsp;또는&amp;nbsp;.pdf로 저장하고, 파일 정보는&amp;nbsp;.json으로 따로 저장했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;metadata_path(file_id).write_text(
    json.dumps(info.model_dump(), ensure_ascii=False, indent=2),
    encoding=&quot;utf-8&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;269&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장되는 정보는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;id
original_name
content_type
size
description
stored_name
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;280&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 자체와 파일 설명 정보를 분리해서 저장하면, 나중에 목록 조회나 다운로드 기능을 만들 때 활용하기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;282&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;12-spring과-fastapi-연동&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;284&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;12. Spring과 FastAPI 연동&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;286&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 서버에서 FastAPI 서버를 직접 호출하는 구조도 학습했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;288&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI 쪽에는 상태 확인 API가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;@app.get(&quot;/health&quot;)
def health():
    return {&quot;status&quot;: &quot;ok&quot;, &quot;service&quot;: &quot;fastapi-file-service&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서는&amp;nbsp;RestClient를 사용해 FastAPI 서버로 HTTP 요청을 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;return restClient.get()
        .uri(&quot;/health&quot;)
        .retrieve()
        .body(HealthResponse.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;305&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;클라이언트
-&amp;gt; Spring Controller
-&amp;gt; Spring Service 또는 Client
-&amp;gt; FastAPI 서버 HTTP 호출
-&amp;gt; FastAPI 응답
-&amp;gt; Spring 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한 점은 Spring이 Python 함수를 직접 실행하는 것이 아니라, HTTP API로 FastAPI 서버를 호출한다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-line=&quot;318&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;13-spring에서-외부-api-장애-처리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;320&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;13. Spring에서 외부 API 장애 처리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;322&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI 서버가 꺼져 있거나 연결할 수 없는 경우를 대비해 예외 처리를 추가했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;try {
    return ResponseEntity.ok(fastApiFileClient.checkHealth());
} catch (RestClientException e) {
    return unavailable(&quot;FastAPI서버에 연결할 수 없습니다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부 서버 호출은 실패할 수 있기 때문에, 예외를 그대로 터뜨리지 않고 명확한 응답으로 바꿔주는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #2b2b2b; color: #cccccc; text-align: start;&quot;&gt;&lt;code&gt;503 Service Unavailable
{
  &quot;code&quot;: &quot;FASTAPI_UNAVAILABLE&quot;,
  &quot;message&quot;: &quot;FastAPI서버에 연결할 수 없습니다.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-line=&quot;342&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;14-핵심-정리&quot; style=&quot;color: #cccccc; text-align: start;&quot; data-line=&quot;344&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14. 핵심 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #cccccc; text-align: start;&quot; data-line=&quot;346&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-line=&quot;346&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REST API는 HTTP Method와 URL을 조합해 자원을 다룬다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;347&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GET은 조회,&amp;nbsp;PUT은 전체 교체,&amp;nbsp;PATCH는 일부 수정에 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;348&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PATCH에서는&amp;nbsp;exclude_unset=True를 사용하면 클라이언트가 보낸 필드만 수정할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;349&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API가 많아지면&amp;nbsp;APIRouter로 기능별 파일을 분리하는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;350&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공통 요청/응답 모델은&amp;nbsp;schemas.py처럼 따로 분리하면 재사용하기 쉽다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;351&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 입출력에서는&amp;nbsp;Path를 사용하면 경로를 안전하고 명확하게 다룰 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;352&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 API에서는 data 폴더 밖 접근을 막는 경로 검증이 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;353&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 업로드는&amp;nbsp;multipart/form-data&amp;nbsp;형식으로 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;354&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI에서는&amp;nbsp;UploadFile,&amp;nbsp;File,&amp;nbsp;Form을 사용해 파일과 일반 값을 함께 받을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;355&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;업로드 파일은 Content-Type, 확장자, 크기를 검증해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;356&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장 파일명은 UUID를 사용하면 파일명 충돌을 줄일 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;357&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring과 FastAPI는 직접 함수 호출이 아니라 HTTP 요청으로 연동한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;358&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&amp;nbsp;RestClient는 외부 API 호출을 담당할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-line=&quot;359&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부 API 호출은 실패 가능성이 있으므로 예외 처리와&amp;nbsp;503&amp;nbsp;응답 같은 장애 처리가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL/[TIL]</category>
      <author>namerong</author>
      <guid isPermaLink="true">https://namerong.tistory.com/208</guid>
      <comments>https://namerong.tistory.com/208#entry208comment</comments>
      <pubDate>Mon, 27 Jul 2026 16:09:52 +0900</pubDate>
    </item>
  </channel>
</rss>