<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hak_0.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 11 Sep 2025 06:48:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hak_0.log</title>
            <url>https://velog.velcdn.com/images/hak_0/profile/5e71d736-1376-4186-8141-df1ef6643027/image.gif</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hak_0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hak_0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[RAG]]></title>
            <link>https://velog.io/@hak_0/RAG</link>
            <guid>https://velog.io/@hak_0/RAG</guid>
            <pubDate>Thu, 11 Sep 2025 06:48:51 GMT</pubDate>
            <description><![CDATA[<h1 id="rag란">RAG란?</h1>
<blockquote>
<p>RAG(Retrieval-Augmented Generation)는 LLM의 단점 중 ‘<strong>사실 관계 오류 가능성’과 ‘맥락 이해의 한계’</strong> 를 개선하는데 초점을 맞춘 방법</p>
</blockquote>
<p><strong>RAG는 LLM에 외부 지식 베이시를 연결하여 모델의 생성 능력과 사실 관계 파악 능력을 향상시키는 기술</strong></p>
<blockquote>
</blockquote>
<h3 id="사용-방식">사용 방식</h3>
<ol>
<li>외부 지식 활용<ol>
<li>대규모의 구조화된 지식 베이스(예: DB, 위키피디아)를 모델에 연결</li>
<li>주어진 질의에 대한 관련 정보를 지식 베이스에 검색 및 추출</li>
</ol>
</li>
<li>증거 기반 생성<ol>
<li>검색된 지식 정보를 증거로 활용하여 보다 사실에 기반한 답변 생성</li>
<li>생성된 답변의 출처를 명시함으로써 신뢰성 향상</li>
</ol>
</li>
<li>맥락 이해력 향상<ol>
<li>외부 지식을 통해 질의에 대한 배경 지식과 맥락 정보를 파악</li>
<li>단순한 패턴 매칭이 아닌 추론 능력을 바탕으로 한 답변 생성</li>
</ol>
</li>
</ol>
<p>즉, RAG는 기존 LLM의 생성 능력과 외부 지식 베이스의 정보를 결합함으로써, 보다 정확하고 사실에 기반한 답변을 제공할 수 있음. 또한 모델의 출력결과에 대한 증거를 제시할 수 있어 설명 가능성과 신뢰성을 높일 수 있음</p>
<h3 id="구성-요소">구성 요소</h3>
<ol>
<li>질의 인코더(Query Encoder): 사용자가 질문을 이해가 위한 언어 모델. 주어진 질문을 벡터 형태로 인코더</li>
<li>지식 검색기(Knowledge Retriever): 인코딩된 질문을 바탕으로 외부 지식 베이스에서 관련 정보를 검색</li>
<li>지식 증강 생성기 (Knowledge-Augmented Generator): 검색된 지식을 활용하여 질문에 대한 답변을 생성하는 언어 모델. 기존의 LLM과 유사하지만, 검색된 지식을 추가 입력으로 받아 보다 정확하고 풍부한 답변 생성</li>
</ol>
<h3 id="rag의-동작-과정-요약">RAG의 동작 과정 요약</h3>
<ol>
<li>사용자의 질문이 주어지면 질의 인코더가 이를 이해하기 쉬운 형태로 변환</li>
<li>지식 검색기가 인코딩된 질문을 바탕으로 외부 지식 베이스에서 정보를 검색</li>
<li>검색된 지식은 지식 증강 생성기의 입력으로 전달</li>
<li>지식 증강 생성기는 지식을 활용하여 사용자 질문에 대한 답변 생성</li>
</ol>
<h3 id="rag의-등장-배경과-필요성"><strong>RAG의 등장 배경과 필요성</strong></h3>
<p>RAG는 자연어 처리와 인공지능 기술의 발전, 그리고 증가하는 사용자의 요구에 따라 등장하게 되었습니다. RAG의 등장 배경과 필요성을 다음과 같이 정리할 수 있습니다.</p>
<ol>
<li>지식 기반 질의응답 시스템의 한계<ul>
<li>초기의 질의응답 시스템은 주로 제한된 도메인의 구조화된 데이터를 기반으로 동작했습니다. 이는 시스템이 다룰 수 있는 주제와 질문의 유형이 한정적이라는 문제가 있었습니다.</li>
<li>사용자의 다양한 정보 요구를 충족시키기 위해서는 보다 광범위한 지식을 활용할 수 있는 시스템이 필요하게 되었습니다.</li>
</ul>
</li>
<li>비정형 텍스트 데이터의 폭발적 증가<ul>
<li>인터넷의 발달과 디지털 기기의 보급으로 웹페이지, 뉴스 기사, 소셜 미디어 게시물 등 비정형 텍스트 데이터가 기하급수적으로 증가하고 있습니다.</li>
<li>이러한 대규모 텍스트 데이터는 방대한 지식을 포함하고 있어, 질의응답 시스템의 지식 베이스로 활용할 수 있는 잠재력이 높습니다.</li>
<li>그러나 비정형 데이터를 효과적으로 처리하고 활용하기 위해서는 기존과는 다른 접근 방식이 필요했습니다.</li>
</ul>
</li>
<li>사전 학습된 언어 모델의 발전<ul>
<li>BERT, GPT 등 사전 학습된 대규모 언어 모델의 등장은 자연어 처리 분야에 큰 변화를 가져왔습니다.</li>
<li>이러한 언어 모델은 방대한 텍스트 데이터로부터 언어의 구조와 의미를 학습하여, 다양한 언어 이해 및 생성 태스크에서 뛰어난 성능을 보여주었습니다.</li>
<li>사전 학습된 언어 모델을 질의응답 시스템에 활용함으로써, 보다 자연스럽고 문맥을 고려한 답변 생성이 가능해졌습니다.</li>
</ul>
</li>
<li>실시간 정보 제공에 대한 사용자의 요구 증대<ul>
<li>인터넷과 모바일 기기의 발달로 사용자들은 언제 어디서나 필요한 정보를 즉시 얻고자 하는 요구가 커지고 있습니다.</li>
<li>단순히 정보를 검색하는 것을 넘어, 대화형 인터페이스를 통해 원하는 정보를 직관적으로 얻고자 하는 사용자가 늘어났습니다.</li>
<li>이에 따라 사용자의 질문을 이해하고 적절한 답변을 실시간으로 제공할 수 있는 지능형 질의응답 시스템의 필요성이 대두되었습니다.</li>
</ul>
</li>
<li>지식 검색과 답변 생성의 통합 필요성<ul>
<li>기존의 질의응답 시스템은 지식 검색과 답변 생성을 별도의 단계로 처리하는 경우가 많았습니다. 이로 인해 검색된 정보와 생성된 답변 사이의 정합성이 떨어지는 문제가 발생했습니다.</li>
<li>지식 검색과 답변 생성을 통합적으로 수행할 수 있는 프레임워크의 필요성이 제기되었고, 이는 RAG 아키텍처의 등장으로 이어졌습니다.</li>
</ul>
</li>
</ol>
<h2 id="rag-기술-접목-사례">RAG 기술 접목 사례</h2>
<ol>
<li>Anthropic&#39;s Constitutional AI (CAI)<ul>
<li>Anthropic사는 RAG 기술을 활용한 대화형 AI 모델인 CAI를 개발했습니다.</li>
<li>CAI는 대화 과정에서 외부 지식을 활용하여 사용자의 질문에 답변을 생성합니다.</li>
<li>생성된 응답의 근거가 되는 출처를 명시하여 신뢰성을 높였습니다.</li>
</ul>
</li>
<li>Perplexity AI<ul>
<li>Perplexity AI는 RAG 기반의 질의응답 서비스를 제공하는 스타트업입니다.</li>
<li>사용자의 질문에 대해 웹 검색을 통해 관련 정보를 수집하고, 이를 바탕으로 응답을 생성합니다.</li>
<li>제공된 응답의 출처와 검색 과정을 사용자에게 투명하게 공개합니다.</li>
</ul>
</li>
</ol>
<hr>
<h1 id="rag-적용-과정">RAG 적용 과정</h1>
<h3 id="1-데이터-준비-및-chunking">1. 데이터 준비 및 Chunking</h3>
<ul>
<li>EDM 예시 보고서, 증빙 문서 수집: 내부 보고서, 정책, 실적 자료 등.</li>
<li>Chunking(쪼개기):<ul>
<li>각 문서를 “논리적 단위(섹션, 헤딩, 표 항목)”로 나누고, 메타데이터(카테고리, 연도,보고서 종류 등)를 붙임</li>
</ul>
</li>
</ul>
<h3 id="2-임베딩-및-벡터화">2. 임베딩 및 벡터화</h3>
<ul>
<li>임베딩 모델 선정:<ul>
<li>OpenAI, Cohere, HuggingFace(국문이면 KoSimCSE/BGE-me)</li>
</ul>
</li>
<li>벡터 추출:<ul>
<li>각 chunk(문단/항목)를 임베딩 모델로 벡터화</li>
</ul>
</li>
</ul>
<h3 id="3-벡터-db-저장">3. 벡터 DB 저장</h3>
<ul>
<li>ChromaDB, Pinecone, mindsDB, Weaviate 등 오픈소스 또는 클라우드 벡터 DB 사용</li>
<li>저장 정보:<ul>
<li>벡터, chunk의 원문, 메타데이터(특히 heading, 구분, 문서ID)</li>
</ul>
</li>
</ul>
<h3 id="4-rag-유형-파이프라인-연결">4. RAG 유형 파이프라인 연결</h3>
<ul>
<li>질의(Report 생성 요청) → 벡터화</li>
<li>벡터DB에서 의미적으로 가장 유사한 chunk Top-N Retrieval</li>
<li>Retrieval chunk를 LLM 프롬프트로 증강(augementation)</li>
<li>LLM(예: GPT-4, Gemini 등)에 전달하여 실제 리포트 생성</li>
</ul>
<h3 id="샘플-코드-llamaindex--chromadb">샘플 코드 (LlamaIndex + ChromaDB)</h3>
<p>아래 코드는 “EDM 예시 보고서”를 RAG 시스템에 저장하고, AI 기반 자동 보고서 생성을 수행하는 과정의 예입니다.</p>
<pre><code class="language-python">from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext, ServiceContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from chromadb.config import Settings
from llama_index.llms.openai import OpenAI

# 1. 문서 로딩 및 파싱
docs = SimpleDirectoryReader(&quot;edm_sample_docs/&quot;).load_data()

# 2. 벡터DB 연동 및 인덱스 생성
db = chromadb.PersistentClient(path=&quot;edm_vectordb&quot;)
chroma_store = ChromaVectorStore(chroma_client=db, collection_name=&quot;edm_reports&quot;)
storage_context = StorageContext.from_defaults(vector_store=chroma_store)

# 3. LlamaIndex로 임베딩 및 저장
index = VectorStoreIndex.from_documents(docs, storage_context=storage_context, service_context=ServiceContext.from_defaults(llm=OpenAI(&quot;gpt-3.5-turbo&quot;, temperature=0)))

# 4. RAG 질의/리포트 생성
query_engine = index.as_query_engine()
answer = query_engine.query(&quot;2024년 우리 회사 환경 실적 보고서를 써줘.&quot;)
print(answer)
</code></pre>
<h3 id="파이프라인-요약">파이프라인 요약</h3>
<ol>
<li>EDM 문서 수집 → 논리적 chunk로 분해</li>
<li>각 chunk를 임베딩(벡터화) 및 벡터DB 저장</li>
<li>리포트 요청(질의) → 벡터화 → 유사 chunk top-N 검색</li>
<li>검색 chunk + 질문 조합하여 LLM 프롬프트 구성</li>
<li>LLM에게 결과 생성 요청 → 리포트 생성 및 사용자 제공</li>
</ol>
<hr>
<h2 id="rag-프레임워크">RAG 프레임워크</h2>
<hr>
<h2 id="llamaindex"><strong>LlamaIndex</strong></h2>
<h3 id="특징">특징</h3>
<ul>
<li><strong>RAG(검색증강생성) 특화:</strong> LLM과 외부 데이터를 효율적으로 연결해, 인덱싱·검색·프롬프트 증강을 자동화합니다.</li>
<li><strong>여러 데이터 소스 통합:</strong> API, DB, 파일(PDF, 워드 등), S3, 구글드라이브, SharePoint 등 다양한 커넥터를 지원.</li>
<li><strong>고급 인덱스 설계:</strong> 벡터·트리·리스트·키워드 인덱스 등 데이터 특성에 맞게 다양한 구조 설계.</li>
<li><strong>빠르고 직관적:</strong> 몇 줄의 파이썬 코드만으로 PoC~프로덕션까지 구현 가능, ramp-up(도입 장벽)이 낮음.</li>
<li><strong>풍부한 확장성:</strong> Prompt, LLM, Embedding 모델을 쉽게 교체·조합, LangChain 등 다른 프레임워크와 연동 가능.</li>
</ul>
<h3 id="주요-기능--사용-흐름"><strong>주요 기능 &amp; 사용 흐름</strong></h3>
<ol>
<li><strong>데이터 수집/파싱(Ingestion)</strong><ul>
<li>내부 문서, DB, 웹 등에서 데이터 수집 및 자동 파싱.</li>
</ul>
</li>
<li><strong>청킹(Chunking) &amp; 인덱싱</strong><ul>
<li>문서를 논리 단위 단락(chunk)별로 분할, 메타데이터와 함께 인덱스 형성.</li>
</ul>
</li>
<li><strong>임베딩(Embedding)</strong><ul>
<li>각 chunk를 임베딩 모델로 벡터화 후 벡터DB에 저장.</li>
</ul>
</li>
<li><strong>검색 &amp; 프롬프트 증강</strong><ul>
<li>사용자 질의를 임베딩해 유사 chunk를 검색 후, 프롬프트에 증거(문맥)로 자동 조합.</li>
</ul>
</li>
<li><strong>LLM 응답</strong><ul>
<li>프롬프트를 LLM에 전달, 증빙 기반의 정확한 생성 결과 제공.</li>
</ul>
</li>
</ol>
<h3 id="비용-구조"><strong>비용 구조</strong></h3>
<ul>
<li><strong>LlamaIndex 자체:</strong> 오픈소스 버전은 무료, 관리형 SaaS 플랫폼은 크레딧 기반(Free~Enterprise, 2025년 기준).<ul>
<li>Free(10,000크레딧/월), Starter, Pro, Enterprise 등 플랜 선택 가능.</li>
<li>추가 사용분은 $1/1,000 크레딧 기준 별도 과금.</li>
</ul>
</li>
<li><strong>LLM/Embedding 호출 비용:</strong> 실제 비용은 사용 LLM(OpenAI, Gemini 등)·Embedding API 비용이 별도 발생.<ul>
<li>예) GPT-3.5-turbo는 1,000토큰 당 $0.002</li>
<li>인덱스 구축 시에도 일부 인덱스(TreeIndex 등)에 LLM 호출 필요.</li>
</ul>
</li>
<li><strong>비용 예측:</strong> 개발 단계에서 토큰 예측기/MockLLM으로 예상 비용 시뮬레이션 가능</li>
</ul>
<h3 id="난이도"><strong>난이도</strong></h3>
<ul>
<li><strong>도입/학습 곡선:</strong><ul>
<li>RAG 관점에서는 매우 쉽고, 튜토리얼·샘플코드가 많음.</li>
<li>커스텀 인덱스 설계, 대규모 데이터/멀티소스 통합, 성능 튜닝 등은 중급 이상의 숙련 필요.</li>
</ul>
</li>
<li><strong>운영 난이도:</strong><ul>
<li>수백만 단위 대규모 데이터에도 안정적으로 확장(스케일), 파이프라인 관리 용이.</li>
</ul>
</li>
</ul>
<h3 id="강점"><strong>강점</strong></h3>
<ul>
<li><strong>특화된 RAG 성능:</strong> 대규모 데이터(수천~수억 문서)에서도 빠르고 정확한 검색·응답.</li>
<li><strong>높은 확장성/유연성:</strong> 거의 모든 LLM, 벡터DB, 프레임워크와 연동(Plug&amp;Play).</li>
<li><strong>실패/재현 방지:</strong> 프롬프트 증강·메타데이터 활용 등 고급기능 내장.</li>
<li><strong>커뮤니티 성장:</strong> 빠른 기능 개선, 다양한 실전 예제와 문서 지원.</li>
</ul>
<h3 id="단점-및-한계"><strong>단점 및 한계</strong></h3>
<ul>
<li><strong>범용 에이전트 프레임워크(X):</strong> LangChain만큼의 &#39;체인/에이전트/워크플로우&#39; 복잡 응용은 다소 제한적.</li>
<li><strong>복잡한 커스텀화 시 곡선↑:</strong> 고급 인덱스 튜닝, 멀티쿼리, 구조화 데이터 지원 등은 내부 구조 숙지가 필요할 수 있음.</li>
<li><strong>소규모 프로젝트에 과할 수 있음:</strong> 소규모(몇 페이지)라면 인덱스 구축이 오버헤드.</li>
<li><strong>에코시스템 상대 열세:</strong> 최대 커뮤니티 규모는 LangChain/LangGraph 등이 더 큼.</li>
<li><strong>엔터프라이즈 운영:</strong> 대용량+보안+고가용성 요구 시 지속적인 관리·업데이트 필요.</li>
</ul>
<h3 id="실제-적용-시-tip"><strong>실제 적용 시 TIP</strong></h3>
<ul>
<li><strong>ESG/EDM에 최적:</strong> PDF, 정책문서, 실적보고 등 구조화+비정형 혼합데이터의 증거기반 보고서 자동화에 강력.</li>
<li><strong>PoC~프로덕션 단계별 사용:</strong> 무료 플랜→유료 크레딧 방식으로 확장 무난.</li>
<li><strong>데이터 커넥터(LlamaHub) 적극 활용:</strong> 내부 ERP/DB~클라우드 스토리지와 손쉽게 연동 가능.</li>
</ul>
<h3 id="결론"><strong>결론:</strong></h3>
<p>LlamaIndex는 대용량, 복잡 문서 기반의 AI 검색/생성/RAG 솔루션에 크게 특화된 데이터-LLM 연동 프레임워크입니다.</p>
<p>학습 난이도 대비 강력한 기능, 저렴하거나 예측 가능한 비용, 뛰어난 유연성을 바탕으로 ESG 데이터관리·보고서 자동화 등 EDM 업무에 매우 적합합니다.</p>
<p>PoC~대규모 서비스까지, 실제 데이터를 중심으로 한 실전 최적화가 가능합니다.</p>
<hr>
<h2 id="langchain"><strong>LangChain</strong></h2>
<h3 id="특징-1">특징</h3>
<ul>
<li><p><strong>목적:</strong></p>
<p>  ChatGPT, GPT-4 등 LLM을 바탕으로 <strong>복잡한 프로세스(데이터 수집→임베딩→검색→생성...)</strong>를 쉽고 유연하게 연결하는 프레임워크</p>
</li>
<li><p><strong>체인(Chain) 구조:</strong></p>
<p>  “단계별 작업(문서 쪼개기→임베딩→DB검색→생성)” 등을 한데 묶어 순차적으로 실행.</p>
<p>  단계 간 데이터 전달 및 후처리 정의가 명확.</p>
</li>
<li><p><strong>에이전트(Agent):</strong></p>
<p>  LLM이 상황에 맞춰 <strong>다양한 도구(검색API, 계산기, DB, 코드실행…)</strong>를 동적으로 호출</p>
<p>  예: “웹에서 자료 찾고, 요약한 뒤, 엑셀 파일로 출력”</p>
</li>
<li><p><strong>방대한 연결성:</strong></p>
<ul>
<li>30여개 이상 LLM(OpenAI, Anthropic, Google 등) 지원</li>
<li>각종 벡터DB, 일반DB, 클라우드, PDF/웹/Slack/GoogleSheet 등 플러그인 형태로 손쉬운 연동</li>
<li>데이터 파이프라인, 챗봇, RAG 시스템, 자동화 워크플로우 등 테스트→서비스 스케일 확장</li>
</ul>
</li>
<li><p><strong>커뮤니티·예제·확장성:</strong></p>
<p>  가장 많은 사용자와 빠른 업데이트, 사용사례 및 튜토리얼 풍부</p>
<p>  오픈소스+커머셜(사업자용) 모두 지원</p>
</li>
</ul>
<h3 id="비용-구조-1"><strong>비용 구조</strong></h3>
<ul>
<li><p><strong>자체 사용비:</strong></p>
<p>  오픈소스 프레임워크 자체는 무료</p>
<p>  단, 실제 LLM/임베딩 모델/서드파티 툴(벡터DB 등)의 API 사용에 따라 별도 비용 발생</p>
</li>
<li><p><strong>운영비용:</strong></p>
<p>  코드 구조에 따라 API 호출 반복, 외부 시스템(검색/DB 트래픽 등)에 따른 직접비가 늘 수 있음</p>
<p>  대량 서비스/멀티에이전트 설계 시 토큰 소모 예측 필요</p>
</li>
</ul>
<h3 id="난이도-및-적용성"><strong>난이도 및 적용성</strong></h3>
<ul>
<li><p><strong>학습 난이도:</strong></p>
<p>  구조 이해(체인/에이전트/메모리)를 한 번 익히면 매우 유연</p>
<p>  복잡한 업무(질문→검색→로그 저장→후처리 등)를 한 곳에서 통합 관리 가능</p>
<p>  입문 튜토리얼/실무 예제가 매우 많음</p>
</li>
<li><p><strong>확장성:</strong></p>
<p>  신규 API, DB, LLM 등 추가 연결/교체가 쉬움</p>
<p>  수십 개의 체인, 자동화 플로우도 일관되게 설계 가능</p>
</li>
</ul>
<h3 id="강점-1"><strong>강점</strong></h3>
<ul>
<li><p><strong>모듈/워크플로우 설계 유연성:</strong></p>
<p>  다양한 데이터/툴/LLM/후처리를 단일 레시피로 손쉽게 조합</p>
</li>
<li><p><strong>Agent(에이전트) 방식을 통한 동적 작업:</strong></p>
<p>  사용자의 질문과 상황에 따라 자동으로 “웹검색→QR코드 생성→메일 발송”도 현업에 적용 가능</p>
</li>
<li><p><strong>방대한 벤더·도구·API 지원:</strong></p>
<p>  OpenAI, Google, Slack, Pinecone, Chroma, Salesforce 등 수십종 쉽게 통합</p>
</li>
<li><p><strong>대형 프로젝트, 복합 서비스에 적합:</strong></p>
<p>  (QA+챗봇+자동 문서화+다국어 번역+코드실행 등 동시에 요구시)</p>
</li>
</ul>
<h3 id="단점-및-한계-1"><strong>단점 및 한계</strong></h3>
<ul>
<li><p><strong>복잡한 구조의 오버헤드:</strong></p>
<p>  단일 기능만 쓴다면 구조가 과할 수 있음(학습/코드 길이↑)</p>
</li>
<li><p><strong>디버깅/모니터링:</strong></p>
<p>  체인이 길어지거나 Agent가 동적으로 움직일 때, 중간과정 추적이 불편한 편</p>
</li>
<li><p><strong>실행 환경/의존성:</strong></p>
<p>  꼭 필요한 최소 모듈만 불러오는 것이 중요.</p>
<p>  (기본 설치 시 의존성 패키지가 많아질 수 있음)</p>
</li>
<li><p><strong>성능:</strong></p>
<p>  워크플로우가 복잡할수록 실제 응답 속도는 간단한 파이프라인 대비 느려질 수 있음</p>
</li>
</ul>
<h3 id="실제-적용-팁"><strong>실제 적용 팁</strong></h3>
<ul>
<li><p><strong>단순 RAG만 할 경우 LlamaIndex가 더 쉽고 빠르지만,</strong></p>
<p>  여러 데이터, LLM, 워크플로우·자동화 까지 필요하다면 LangChain이 필수</p>
</li>
<li><p><strong>여러 LLM/벡터DB/Agent를 자유롭게 조합해 데모~서비스까지 일관 설계 가능</strong></p>
</li>
<li><p><strong>ESG/EDM 분야:</strong></p>
<ul>
<li>외부 벤치마킹 연동, 업무별 자동화(예: ESG 공시 대응 챗봇, 다중 평가보고서 자동화 등)</li>
<li>보고서 생성→파일화→이메일 발송까지 전 프로세스 일관 설계</li>
</ul>
</li>
</ul>
<h2 id="비교-요약"><strong>비교 요약</strong></h2>
<p><strong>정리:</strong></p>
<ul>
<li><strong>정교한 문서 검색·증거 반영에 집중:</strong> → LlamaIndex</li>
<li><strong>여러 도구·워크플로우 체인, Agent 동작 등 복합 자동화:</strong> → LangChain</li>
</ul>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>LlamaIndex</strong></th>
<th><strong>LangChain</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>주요 목적</strong></td>
<td>RAG(검색증강생성)에 특화된 데이터 인덱싱 및 검색</td>
<td>LLM 오케스트레이션(체인, 에이전트, 통합) 범용 프레임워크</td>
</tr>
<tr>
<td><strong>핵심 강점</strong></td>
<td>- 문서 데이터 기반 정밀 인덱싱 및 검색에 최적화- 고급 인덱스 구조 지원(트리, 리스트 등)- 쉽고 빠른 PoC 및 간단한 코드 구현 가능</td>
<td>- 다양한 LLM, 도구, API 연동 폭넓음- 복잡한 워크플로우(체인, 에이전트) 구성 가능- 동적 도구 호출 및 멀티태스크 지원</td>
</tr>
<tr>
<td><strong>사용성/학습곡선</strong></td>
<td>초보자도 빠르게 도입 가능, 검색 중심으로 상대적 간단함</td>
<td>강력하지만 구조 복잡, 체인과 에이전트 개념 학습 필요</td>
</tr>
<tr>
<td><strong>데이터 소스 지원</strong></td>
<td>API, DB, PDF, 문서 등 데이터 인덱싱 중심</td>
<td>다양한 데이터 소스(웹, PDF, API, DB 등) 및 멀티모달 지원</td>
</tr>
<tr>
<td><strong>프롬프트 처리</strong></td>
<td>검색 결과 기반 자동 프롬프트 증강에 초점</td>
<td>프롬프트 관리, 체인 내 데이터 흐름 제어에 유연성 있음</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>데이터 인덱싱 및 검색에 최적화, LangChain과 연동 가능</td>
<td>API, LLM, DB 등 다양한 시스템과 광범위한 통합 환경에 적합</td>
</tr>
<tr>
<td><strong>복잡한 응용</strong></td>
<td>주로 검색증강생성 중심 응용에 적합</td>
<td>챗봇, 에이전트, 자동화 등 복합 AI 워크플로우 구현에 강함</td>
</tr>
<tr>
<td><strong>성능/확장성</strong></td>
<td>대용량 데이터 빠른 검색 및 응답에 강점</td>
<td>대규모/복잡한 작업 처리에 적합, 실제 속도는 파이프라인 복잡도에 영향</td>
</tr>
<tr>
<td><strong>커뮤니티/생태계</strong></td>
<td>빠르게 성장 중, 문서 및 예제가 풍부</td>
<td>가장 큰 커뮤니티와 생태계, 다양한 예제와 플러그인 존재</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>오픈소스 기본 무료, LLM 호출 API 비용 별도</td>
<td>오픈소스 기본 무료, LLM 호출 및 외부 API 별도 비용 발생</td>
</tr>
<tr>
<td><strong>적합 사례</strong></td>
<td>- ESG 문서, 법률, 보고서 등 비정형+구조문서 검색- 증거 기반 리포트 자동화</td>
<td>- 복잡한 멀티모달 데이터 처리- 동적 에이전트, 복합 워크플로우 자동화</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 별 특징 정리]]></title>
            <link>https://velog.io/@hak_0/AI-%EB%B3%84-%ED%8A%B9%EC%A7%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@hak_0/AI-%EB%B3%84-%ED%8A%B9%EC%A7%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 29 Aug 2025 02:43:52 GMT</pubDate>
            <description><![CDATA[<h2 id="🤖-openai---gpt-시리즈-gpt">🤖 OpenAI - GPT 시리즈 (GPT)</h2>
<p>GPT는 현재 가장 널리 알려진 고성능 범용 모델입니다.</p>
<p>특징:</p>
<p>뛰어난 범용성: 추론, 코딩, 글쓰기, 창작 등 거의 모든 분야에서 매우 높은 성능을 보입니다.</p>
<p>강력한 멀티모달: 텍스트뿐만 아니라 이미지, 음성, 비디오를 실시간으로 이해하고 상호작용하는 능력이 뛰어납니다.</p>
<p>방대한 생태계: 가장 많은 사용자와 API를 보유하고 있어 관련 자료나 서드파티 도구를 찾기 쉽습니다.</p>
<p>컨텍스트 윈도우: 128,000 토큰</p>
<p>추천 용도: 복잡한 문제 해결, 전문적인 글쓰기, 코딩 지원 등 최고 수준의 성능이 필요한 대부분의 작업에 적합합니다.</p>
<p>주요 AI 모델별 특징과 컨텍스트 윈도우 크기를 간략하게 정리해 드릴게요.</p>
<h2 id="🤖-openai---gpt-시리즈-gpt-4o">🤖 OpenAI - GPT 시리즈 (GPT-4o)</h2>
<p>GPT-4o는 현재 가장 널리 알려진 고성능 범용 모델입니다.</p>
<p>특징:</p>
<p>뛰어난 범용성: 추론, 코딩, 글쓰기, 창작 등 거의 모든 분야에서 매우 높은 성능을 보입니다.</p>
<p>강력한 멀티모달: 텍스트뿐만 아니라 이미지, 음성, 비디오를 실시간으로 이해하고 상호작용하는 능력이 뛰어납니다.</p>
<p>방대한 생태계: 가장 많은 사용자와 API를 보유하고 있어 관련 자료나 서드파티 도구를 찾기 쉽습니다.</p>
<p>컨텍스트 윈도우: 128,000 토큰</p>
<p>추천 용도: 복잡한 문제 해결, 전문적인 글쓰기, 코딩 지원 등 최고 수준의 성능이 필요한 대부분의 작업에 적합합니다.</p>
<h2 id="🧠-google---gemini-시리즈">🧠 Google - Gemini 시리즈</h2>
<p>구글의 서비스와 강력하게 통합된, 특히 대용량 컨텍스트 처리에 특화된 모델입니다.</p>
<p>특징:</p>
<p>초대형 컨텍스트 윈도우: 한 번에 매우 많은 양의 정보를 처리할 수 있어 긴 문서, 코드베이스, 비디오 전체를 분석하고 질문에 답하는 데 독보적입니다.</p>
<p>네이티브 멀티모달: 설계부터 텍스트, 이미지, 음성, 비디오를 통합적으로 이해하도록 만들어져 여러 형태의 데이터를 넘나드는 작업에 강합니다.</p>
<p>구글 생태계 연동: 구글 검색, 워크스페이스(Docs, Sheets 등)와 긴밀하게 연동되어 최신 정보 검색 및 요약 능력이 우수합니다.</p>
<p>컨텍스트 윈도우: 1,000,000 토큰 (기본)</p>
<p>추천 용도: 수백 페이지 분량의 논문·법률 문서 요약, 장시간 비디오 내용 분석, 방대한 코드베이스 이해 등 대용량 정보 처리 작업에 가장 강력합니다.</p>
<h2 id="📚-anthropic---claude시리즈">📚 Anthropic - Claude시리즈</h2>
<p>안정성과 긴 컨텍스트 처리 능력의 균형을 맞춘 모델입니다.</p>
<p>특징:</p>
<p>안정성과 윤리성: 유해하거나 위험한 답변을 생성하지 않도록 설계된 &#39;헌법 AI(Constitutional AI)&#39;를 기반으로 하여 기업 환경에서 사용하기에 안정적입니다.</p>
<p>긴 글 요약 및 작성: 긴 컨텍스트 윈도우를 바탕으로 장문의 문서를 정확하게 요약하거나 창의적인 글을 자연스럽게 작성하는 능력이 뛰어납니다.</p>
<p>빠른 응답 속도: 모델 라인업(Haiku, Sonnet, Opus)이 다양하여, 비용과 속도에 맞춰 최적의 모델을 선택할 수 있습니다.</p>
<p>컨텍스트 윈도우: 200,000 토큰</p>
<p>추천 용도: 긴 보고서나 책을 요약하고 분석하는 작업, 안전성이 중요한 고객 응대 챗봇, 창의적인 글쓰기 등에 적합합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LangSmith]]></title>
            <link>https://velog.io/@hak_0/LangSmith</link>
            <guid>https://velog.io/@hak_0/LangSmith</guid>
            <pubDate>Wed, 18 Jun 2025 02:23:55 GMT</pubDate>
            <description><![CDATA[<h3 id="🚀-1-소개">🚀 1. 소개</h3>
<p>LangSmith는 대규모 언어 모델(LLM) 기반 애플리케이션의 개발 주기를 가속화하고, 특히 성능을 체계적으로 평가하고 개선하기 위해 설계된 강력한 프레임워크입니다.</p>
<p>이 문서는 ESG 설문 검수 AI 시스템에 LangSmith를 도입한 이유, 주요 기능, 실제 활용 방안, 도입 성과를 설명합니다.</p>
<p>⸻</p>
<h3 id="💡-2-langsmith-시스템-개요">💡 2. LangSmith 시스템 개요</h3>
<p>LangSmith는 LLM 기반 애플리케이션의 개발, 디버깅, 테스트, 평가 과정을 지원하는 포괄적인 플랫폼입니다.</p>
<p><strong>2.1. 핵심 기능</strong>
    •    실시간 추적 (Real-time Tracing): LLM 호출, 체인 실행, 에이전트 동작 등을 추적하여 병목 진단 가능
    •    LLM-as-Judge: LLM이 AI 응답을 평가하는 자동화된 지능형 평가 시스템
    •    데이터 관리 (Data Management): 데이터셋과 평가 결과를 저장, 관리, 버전 관리
    •    대시보드 &amp; 시각화: 평가 결과를 실시간 시각화
    •    협업 기능: 팀원 간 결과 공유 및 피드백 용이</p>
<p><strong>2.2. 주요 평가 방식</strong>
    •    LLM Judge: GPT-4o 등 강력한 모델을 활용해 다면적 기준으로 평가
    •    구조화된 피드백: JSON 기반의 상세한 평가 및 개선 제안 제공
    •    다중 평가자: 형식, 내용, 논리성 등 다각도로 평가
    •    자동 데이터 추적: 모든 실행/결과 기록 저장</p>
<p>⸻</p>
<h3 id="✨-3-주요-장점-llm-as-judge를-통한-지능형-평가">✨ 3. 주요 장점: LLM-as-Judge를 통한 지능형 평가</h3>
<p><strong>3.1. 지능형 평가 시스템 구현</strong>
    •    의미 기반 평가: 키워드가 아닌 문맥/의미 이해 중심
    •    도메인 전문성 반영: ESG 전문가처럼 평가 프롬프트 설계
    •    구체적 피드백 제공: JSON 형태의 개선 제안 포함
    •    일관성 있는 반복 평가: 동일 프롬프트 기준으로 성능 비교 용이</p>
<p><strong>3.2. 기존 방식과의 비교</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존 방식</th>
<th>LangSmith (LLM-as-Judge)</th>
</tr>
</thead>
<tbody><tr>
<td>평가 기준</td>
<td>키워드, 규칙 기반</td>
<td>의미 기반, 구조/논리 분석</td>
</tr>
<tr>
<td>평가 깊이</td>
<td>정오답, 피상적</td>
<td>정성적, 심층적</td>
</tr>
<tr>
<td>피드백</td>
<td>단순 결과</td>
<td>구체적 제안 포함 JSON</td>
</tr>
<tr>
<td>확장성</td>
<td>수작업 부담 큼</td>
<td>자동 대규모 평가 가능</td>
</tr>
<tr>
<td>전문성</td>
<td>반영 어려움</td>
<td>프롬프트로 반영 가능</td>
</tr>
</tbody></table>
<h3 id="✅-llm-as-judge-평가-프롬프트-예시">✅ LLM-as-Judge 평가 프롬프트 예시</h3>
<pre><code class="language-python">judge_prompt = &quot;&quot;&quot;
[Role]: 당신은 ESG 보고서 전문가이자 엄격한 평가관입니다.
[Task]: 제공된 ESG 섹션 내용을 평가하고 점수 및 개선안을 JSON으로 작성하세요.
[Evaluation Criteria]:
1. 섹션 구조의 논리성 (40점)
2. 표 형식 완성도 (35점)
3. 가독성과 표현 명확성 (25점)

[Output Format]:
{
  &quot;total_score&quot;: float,
  &quot;feedback_summary&quot;: &quot;전반 요약&quot;,
  &quot;criteria_scores&quot;: {
    &quot;section_structure_logic&quot;: float,
    &quot;table_format_completeness&quot;: float,
    &quot;overall_readability&quot;: float
  },
  &quot;improvement_suggestions&quot;: {
    &quot;section_structure&quot;: [...],
    &quot;table_format&quot;: [...],
    &quot;readability&quot;: [...]
  }
}
&quot;&quot;&quot;</code></pre>
<h3 id="🗃️-4-데이터-관리-기능">🗃️ 4. 데이터 관리 기능</h3>
<p><strong>4.1. 체계적인 데이터셋 관리의 이점</strong>
    •    자동 데이터셋 생성: PoC 로그에서 입력/출력 추출
    •    버전 관리: 데이터셋 이력 추적
    •    결과 연결: 입력, 출력, 평가 결과를 LangSmith에 자동 저장
    •    재사용성: 다양한 테스트에 반복 활용 가능</p>
<p><strong>4.2. Python 예시</strong></p>
<pre><code class="language-python">from langsmith import Client

class LangSmithDatasetManager:
    def __init__(self, api_key: str):
        self.client = Client(api_key=api_key)
        self.poc_logs = [...]  # 실제 로그 로딩 생략

    def create_langsmith_dataset(self, dataset_name: str, description: str = None):
        examples = []
        for log in self.poc_logs:
            example = {
                &quot;inputs&quot;: {&quot;question&quot;: log[&quot;question_number&quot;]},
                &quot;outputs&quot;: {&quot;ground_truth&quot;: log[&quot;raw_model_answer&quot;]}
            }
            examples.append(example)
        return self.client.create_dataset(
            dataset_name=dataset_name,
            description=description,
            examples=examples
        )</code></pre>
<h3 id="📊-5-대시보드-및-시각화">📊 5. 대시보드 및 시각화</h3>
<p><strong>5.1. 주요 기능</strong>
    •    표 형태 결과 시각화
    •    실시간 업데이트
    •    드릴다운 분석 가능
    •    비교 분석 기능
    •    필터링 및 검색 기능</p>
<p><strong>5.2. 예시 (가상)</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>점수</th>
<th>상세 피드백</th>
</tr>
</thead>
<tbody><tr>
<td>형식 준수도</td>
<td>93.2%</td>
<td>✅ 테이블 헤더 일관성 필요</td>
</tr>
<tr>
<td>내용 정확성</td>
<td>84.4%</td>
<td>⚠️ 외부 문서 참조 오류 있음</td>
</tr>
<tr>
<td>데이터 완성도</td>
<td>87.8%</td>
<td>✅ 일부 선택 항목 누락 확인</td>
</tr>
<tr>
<td>종합 점수</td>
<td>88.5%</td>
<td>✅ 전반적으로 만족, 미세 조정 필요</td>
</tr>
</tbody></table>
<h3 id="🛠️-6-esg-시스템에서의-langsmith-활용">🛠️ 6. ESG 시스템에서의 LangSmith 활용</h3>
<p><strong>6.1. 평가 방법론</strong></p>
<p>a. 형식 평가 (llm_format_judge)
    •    평가 기준: 섹션 구성, 표 정렬, 구조 일관성 등</p>
<p>b. 내용 평가 (llm_content_judge)
    •    평가 기준: 정책 정보의 정확성, 검수 논리성 등</p>
<p>c. 데이터 완성도 평가 (llm_completeness_judge)
    •    평가 기준: 필수 데이터 포함 여부, 표 내용 정확성</p>
<p><strong>6.2. 실제 구현 예시</strong></p>
<pre><code class="language-python">from langsmith import evaluate

class ESGEvaluationSystem:
    def run_evaluation(self, dataset_name):
        results = evaluate(
            self.run_esg_model,
            data=dataset_name,
            evaluators=[
                self.llm_format_judge.evaluate_fn,
                self.llm_content_judge.evaluate_fn,
                self.llm_completeness_judge.evaluate_fn
            ],
            experiment_prefix=&quot;esg_evaluation&quot;
        )
        return results</code></pre>
<h3 id="✅-7-구현-결과-및-성과">✅ 7. 구현 결과 및 성과</h3>
<p><strong>7.1. 테스트 결과</strong></p>
<table>
<thead>
<tr>
<th>지표</th>
<th>목표</th>
<th>실제</th>
<th>달성 여부</th>
</tr>
</thead>
<tbody><tr>
<td>형식 준수도</td>
<td>90%</td>
<td>93.2%</td>
<td>✅</td>
</tr>
<tr>
<td>내용 정확성</td>
<td>80%</td>
<td>84.4%</td>
<td>✅</td>
</tr>
<tr>
<td>데이터 완성도</td>
<td>85%</td>
<td>87.8%</td>
<td>✅</td>
</tr>
<tr>
<td>종합 점수</td>
<td>85%</td>
<td>88.5%</td>
<td>✅</td>
</tr>
</tbody></table>
<p><strong>7.2. 도입 효과</strong>
    •    지능형 평가: 의미 기반 피드백 제공
    •    데이터 재현성 확보: 자동화된 버전 관리
    •    피드백 루프 단축: 실시간 결과 확인
    •    협업 용이: 대시보드를 통한 결과 공유</p>
<p>⸻</p>
<h3 id="🎯-8-결론-esg-ai-품질-향상의-핵심-도구">🎯 8. 결론: ESG AI 품질 향상의 핵심 도구</h3>
<p>LangSmith는 다음을 가능하게 합니다:
    •    LLM-as-Judge를 통한 의미 기반 평가 자동화
    •    데이터셋 및 결과의 체계적 관리
    •    직관적이고 빠른 대시보드 피드백
    •    개발 주기 단축과 협업 증진</p>
<p>👉 ESG AI 시스템에 있어 LangSmith는 필수적 품질 평가 프레임워크입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[싱글톤 패턴]]></title>
            <link>https://velog.io/@hak_0/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@hak_0/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 18 Jun 2025 00:49:22 GMT</pubDate>
            <description><![CDATA[<h2 id="싱글톤-패턴singleton-pattern-단-하나의-객체만-허용합니다">싱글톤 패턴(Singleton Pattern): 단 하나의 객체만 허용합니다</h2>
<p>소프트웨어 디자인 패턴 중 하나인 <strong>싱글톤 패턴(Singleton Pattern)</strong>은 특정 클래스의 인스턴스, 즉 객체가 애플리케이션 내에서 단 하나만 생성되는 것을 보장하는 설계 방식입니다. 생성자가 여러 번 호출되더라도 실제로 생성되는 객체는 최초에 생성된 하나이며, 이후의 모든 호출에서는 최초 생성된 객체를 반환합니다.</p>
<p>이는 마치 우리 반에 반장은 단 한 명만 존재하는 것과 같습니다. 누구든 &quot;반장 나와!&quot;라고 외치면 항상 같은 그 한 명의 반장이 나오는 것과 같은 원리입니다.</p>
<p>왜 싱글톤 패턴을 사용할까요?
싱글톤 패턴은 주로 다음과 같은 목적을 위해 사용됩니다.</p>
<p>메모리 낭비 방지: 객체를 생성할 때마다 메모리를 할당받게 됩니다. 만약 여러 곳에서 동일한 역할을 하는 객체가 필요할 때마다 새로운 객체를 생성한다면 불필요한 메모리 소모가 발생합니다. 싱글톤 패턴을 사용하면 최초 한 번만 객체를 생성하고 이후에는 계속해서 재사용하므로 메모리를 절약할 수 있습니다.
데이터 공유 용이: 단 하나뿐인 객체이므로, 애플리케이션의 여러 부분에서 해당 객체의 데이터를 공유하고 상태를 일관성 있게 관리하기 용이합니다. 예를 들어, 애플리케이션 전체의 환경 설정을 관리하는 객체나, 데이터베이스 연결을 관리하는 커넥션 풀(Connection Pool) 같은 경우에 유용하게 사용될 수 있습니다.
전역적인 접근성: 싱글톤 객체는 전역 변수처럼 애플리케이션의 어느 곳에서나 쉽게 접근하여 사용할 수 있습니다. 이를 통해 다른 객체들과의 상호작용이 편리해집니다.</p>
<h3 id="싱글톤-패턴의-핵심-구현-원리">싱글톤 패턴의 핵심 구현 원리</h3>
<p>싱글톤 패턴을 구현하기 위해서는 다음의 두 가지 핵심 요소를 지켜야 합니다.</p>
<ol>
<li>private 생성자: 외부에서 new 키워드를 사용하여 무분별하게 새로운 객체를 생성하는 것을 막기 위해 생성자의 접근 제어자를 private으로 선언합니다.</li>
<li>static 메서드와 static 변수: 유일한 인스턴스를 저장하기 위한 static 변수를 클래스 내부에 선언하고, 이 인스턴스에 접근할 수 있도록 public static 메서드(일반적으로 getInstance()라는 이름을 사용)를 제공합니다. 이 메서드는 인스턴스가 아직 생성되지 않았다면 새로 생성하여 반환하고, 이미 생성되어 있다면 기존의 인스턴스를 반환합니다.</li>
</ol>
<h3 id="파이썬-구현-예시">파이썬 구현 예시</h3>
<pre><code class="language-python">class Singleton:
    _instance = None  # 유일한 인스턴스를 저장할 클래스 변수

    def __new__(cls):
        # 인스턴스가 아직 없으면 새로 생성
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            print(&quot;싱글톤 객체를 새로 생성했습니다.&quot;)
        else:
            print(&quot;기존 싱글톤 객체를 반환합니다.&quot;)
        return cls._instance

    def __init__(self):
        # __init__은 __new__ 호출 후 매번 호출되므로, 
        # 실제 초기화는 한 번만 수행되도록 주의해야 합니다.
        if not hasattr(self, &#39;_initialized&#39;): # 초기화 플래그를 사용하여 한 번만 초기화되도록
            self.value = &quot;기본값&quot;
            self._initialized = True
            print(&quot;싱글톤 객체를 초기화했습니다.&quot;)

    def do_something(self):
        print(f&quot;싱글톤 객체에서 작업 수행 중입니다. 현재 값: {self.value}&quot;)

# 사용 예시
print(&quot;--- 첫 번째 객체 생성 시도 ---&quot;)
s1 = Singleton()
s1.value = &quot;첫 번째 설정값&quot;
s1.do_something()

print(&quot;\n--- 두 번째 객체 생성 시도 ---&quot;)
s2 = Singleton()
s2.do_something()

print(&quot;\n--- 세 번째 객체 생성 시도 ---&quot;)
s3 = Singleton()
s3.value = &quot;세 번째 설정값 (s1, s2와 동일한 객체)&quot;
s3.do_something()

print(&quot;\n--- 객체 동일성 확인 ---&quot;)
print(f&quot;s1과 s2는 같은 객체인가요? {s1 is s2}&quot;)
print(f&quot;s2와 s3는 같은 객체인가요? {s2 is s3}&quot;)
print(f&quot;s1.value: {s1.value}, s2.value: {s2.value}, s3.value: {s3.value}&quot;)</code></pre>
<p>코드 설명:</p>
<ul>
<li><code>_instance</code> = None: 클래스 변수로, 싱글톤 인스턴스를 저장합니다. 초기에는 None으로 설정합니다.</li>
<li><code>__new__(cls)</code>:<ul>
<li><code>if cls._instance is None</code>:: 만약 <code>_instance</code>가 아직 생성되지 않았다면 (즉, 첫 번째 객체 생성 시도라면)</li>
<li><code>cls._instance</code> = <code>super().__new__(cls)</code>: 부모 클래스의 <code>__new__</code> 메서드를 호출하여 실제 객체를 생성하고 <code>_instance</code>에 저장합니다.</li>
<li><code>else</code>: 이미 _instance가 존재하면 기존의 인스턴스를 반환합니다.</li>
</ul>
</li>
<li><code>__init__(self)</code>: <code>__new__</code>는 객체를 생성하고 반환하지만, <code>__init__</code>은 객체가 반환된 후 매번 호출됩니다. 따라서 <code>_initialized</code>와 같은 플래그를 사용하여 실제 초기화 로직은 한 번만 실행되도록 하는 것이 중요합니다.</li>
</ul>
<h3 id="싱글톤-패턴의-장단점">싱글톤 패턴의 장단점</h3>
<p><strong>장점</strong>    </p>
<ul>
<li>메모리 효율성: 단 하나의 인스턴스만 사용하므로 메모리 낭비를 줄일 수 있습니다.    </li>
<li>데이터 일관성: 전역적으로 하나의 상태를 유지하므로 데이터의 일관성을 보장하기 쉽습니다.</li>
<li>전역 접근: 어디서든 쉽게 접근하여 사용할 수 있어 편리합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>의존성 증가: 싱글톤 객체를 사용하는 다른 클래스들과의 결합도(Coupling)가 높아져 유연성이 떨어질 수 있습니다.</li>
<li>테스트의 어려움: 전역 상태를 가지므로 단위 테스트(Unit Test)를 수행하기가 까다롭습니다. 테스트마다 독립적인 환경을 구축하기 어렵기 때문입니다.</li>
<li>단일 책임 원칙 위배 가능성: 하나의 객체가 너무 많은 역할과 데이터를 가지게 될 수 있습니다.</li>
<li>멀티스레드 환경에서의 동시성 문제: 여러 스레드가 동시에 getInstance() 메서드를 호출할 경우, 여러 개의 인스턴스가 생성될 수 있는 문제가 발생할 수 있습니다. 이를 방지하기 위한 동기화 처리가 필요합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mock ]]></title>
            <link>https://velog.io/@hak_0/Mock</link>
            <guid>https://velog.io/@hak_0/Mock</guid>
            <pubDate>Wed, 23 Apr 2025 08:53:15 GMT</pubDate>
            <description><![CDATA[<h1 id="mock">Mock</h1>
<h3 id="patchtarget-new_callable--asyncmock">patch(target, new_callable = AsyncMock)</h3>
<ul>
<li>여러개 사용 가능</li>
<li>순서 중요 마지막 patch 가 첫번째 인자임 아래에서 위로</li>
</ul>
<pre><code class="language-python">@patch(&quot;app.repo.get_user&quot;, new_callable=AsyncMock)
@patch(&quot;app.repo.get_post&quot;, new_callable=AsyncMock)
async def test_service(mock_get_post, mock_get_user):  # 마지막 patch가 첫 번째 인자
    ...</code></pre>
<h3 id="return_value">.return_value</h3>
<ul>
<li>mock 함수가 await 되었을 때 리턴해줄 값</li>
<li>즉 대체한 함수가 결과값을 어떤 것을 줄지 내가 결정하는 것이다</li>
</ul>
<pre><code class="language-python">@patch(&quot;app.repo.get_user&quot;, new_callable=AsyncMock)
async def test_service(mock_get_user):
    mock_get_user.return_value = User(id=1, name=&quot;테스트&quot;)
    result = await get_user()  # 이 get_user는 실제 DB 안 타고 mock됨</code></pre>
<h3 id="side_effect">.side_effect</h3>
<ul>
<li>특정 값을 돌려주고싶다 -&gt; 가능 (리스트로도 가능)</li>
<li>호출 시 에러 던지고 싶다 -&gt; 가능 (return_value 는 객체를 주는거지 실제 raise 처럼 동작을 안함)</li>
<li>여러번 호출시 다른값 -&gt; 리스트 형태 가능</li>
</ul>
<pre><code class="language-python"># 1. 예외 발생 (주로 try/except 처리 테스트 시)
mock_get_user.side_effect = Exception(&quot;DB 에러&quot;)

# 2. 여러 값 순차 리턴
mock_get_user.side_effect = [User(id=1), User(id=2), User(id=3)]

# 3. 일반 return_value 처럼 사용할 떄
mock_get_user.side_effect = lambda: User(id=1)</code></pre>
<ul>
<li>side_effect는 다음 중 하나여야 함:<ol>
<li>예외 객체 → side_effect = ValueError(&quot;에러&quot;) → raise ValueError(...)처럼 작동</li>
<li>함수 or lambda → side_effect = lambda: User(id=1) → 직접 실행 후 결과 리턴</li>
<li>iterable (list 등) → side_effect = [User(id=1), User(id=2)] → 순서대로 리턴</li>
</ol>
</li>
</ul>
<h3 id="assert_called_once">assert_called_once()</h3>
<p>이 mock 함수가 정확히 한 번 호출됐냐? 검사<br>로직 안에서 조건 분기에 따라 특정 함수가 안 불리거나, 정확히 한 번만 불리는 걸 보장해야 할 때.</p>
<h3 id="assert_called_with">assert_called_with(...)</h3>
<p>필요한 경우<br>로직에서 특정 파라미터로 정확하게 함수 호출했는지 확인하고 싶을 때<br>특히 “값에 따라 다르게 동작”하는 로직에서는 매우 유용  </p>
<h3 id="예시">예시</h3>
<pre><code class="language-python">@pytest.mark.asyncio
@patch(&quot;app.domain.free_board.services.create_free_board_by_id&quot;, new_callable=AsyncMock)
async def test_create_free_board_by_id_service(mock_create):
    # given
    dummy_user = object()
    dummy_data = AsyncMock()
    dummy_result = FreeBoardResponseDTO(
        id=1,
        user=UserSchema(
            id=1
        ),
        title=&quot;test title&quot;,
        content=&quot;test content&quot;,
        image_url=None,
        view_count=1,
        created_at=&quot;2025-04-23T12:00:00&quot;,
        updated_at=&quot;2025-04-23T12:00:00&quot;,
    )
    mock_create.return_value = dummy_result # 여기서 create_free_board_by_id 이 함수 호출시 값을 지정해줌

    # when
    result = await create_free_board_by_id_service(dummy_data, dummy_user)

    # then
    mock_create.assert_called_once_with(dummy_data, dummy_user) # 그래서 서비스 로직을 잘 작성 하였으면 return 값이 안에서 호출한 저값에대한 mock 데이터 값이 나옴
    assert result == dummy_result</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Locust with django]]></title>
            <link>https://velog.io/@hak_0/Locust-with-django</link>
            <guid>https://velog.io/@hak_0/Locust-with-django</guid>
            <pubDate>Sun, 30 Mar 2025 10:03:57 GMT</pubDate>
            <description><![CDATA[<h2 id="locust">Locust</h2>
<blockquote>
<p>실제 사용자가 대용량으로 접속했을 때, 서버가 잘 버티는지 테스트하기 위해</p>
</blockquote>
<p>현재 내 프로젝트는 EC2 micro 서버에서 작동 중이기에 테스트 하기에 적합하지 않고 로컬에서만 실험해 보려한다.  </p>
<h4 id="locust의-역할--부하-테스트load-testing">Locust의 역할 = 부하 테스트(Load Testing)</h4>
<blockquote>
<ul>
<li>수천 명의 사용자가 동시에 요청을 보낼때</li>
<li>서버의 응답속도, 실패율, 에러발생 여부, 병목 지점을 추측할 수 있다.</li>
</ul>
</blockquote>
<p>장고에 적용시키기<br><code>pip install locust</code></p>
<ul>
<li>locustfile.py 생성<pre><code class="language-python"># locustfile.py
from locust import HttpUser, between, task

</code></pre>
</li>
</ul>
<p>class UserBehavior(HttpUser):
    host = &quot;<a href="http://127.0.0.1:8000&quot;">http://127.0.0.1:8000&quot;</a>
    wait_time = between(1, 3)  # 요청 간 간격 (초)</p>
<pre><code>def on_start(self):
    response = self.client.post(
        &quot;/api/user/login/&quot;,
        json={&quot;email&quot;: &quot;test@example.com&quot;, &quot;password&quot;: &quot;!!test1234&quot;},
    )

    data = response.json()
    self.access_token = data.get(&quot;access_token&quot;)
    self.headers = {&quot;Authorization&quot;: f&quot;Bearer {self.access_token}&quot;}

@task
def get_profile(self):
    self.client.get(&quot;/api/user/profile/&quot;, headers=self.headers)

@task
def update_profile(self):
    self.client.patch(
        &quot;/api/user/profile/&quot;,
        headers=self.headers,
        json={&quot;nickname&quot;: &quot;updated_nick&quot;},
    )

@task
def logout(self):
    self.client.post(&quot;/api/user/logout/&quot;, headers=self.headers)</code></pre><pre><code>
* 실행
`locust -f locustfile.py`

## Locust로 테스트하면 좋은 API 유형

| 유형 | 설명 | 예시 |
|------|------|------|
| **트래픽이 몰리는 대표 API** | 사용자 요청이 집중되는 핵심 기능 | 로그인, 메인 페이지, 피드 조회 등 |
| **자원 소모가 큰 API** | 처리 시간/메모리/CPU 부하가 큰 작업 | AI 응답 생성, 파일 업로드/변환 등 |
| **DB에 쓰기 많은 API** | 동시에 많은 사용자가 데이터를 생성/수정 | 댓글 작성, 게시글 등록 등 |
| **외부 연동이 포함된 API** | 외부 API 호출로 지연이나 실패 가능 | 결제, AI API 호출, 이메일 전송 등 |
| **비동기 처리 확인용 API** | 비동기 작업 완료 시간이나 큐 처리 확인 | Celery 작업, AI 응답 대기 등 |


### Django 테스트 코드 vs Locust 테스트 비교표

| 항목              | Django 테스트 코드 (`TestCase`) | Locust 테스트               |
|-------------------|----------------------------------|-----------------------------|
| **목적**           | 기능 정확성 테스트 (유닛/통합)       | 성능, 부하 테스트              |
| **언제 실행?**      | CI/CD, PR 머지 시 자동 실행         | 서버 성능 검증 시 수동/자동 실행 |
| **중점**           | 기능이 정상적으로 동작하는가?        | 얼마나 많은 요청을 견디는가?     |
| **요청 수**        | 적음 (1~수십 건)                   | 많음 (수백~수천 건 이상)        |
| **실패 시 의미**     | 코드 로직 오류                     | 시스템 병목, 성능 한계 노출       |
| **도구/프레임워크** | `unittest`, `pytest`, `APIClient` | `locust`, `gevent`           |

#### 출력 예시

![](https://velog.velcdn.com/images/hak_0/post/779cefe8-f59e-47bc-b9c7-1034f8912eb2/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[brute_force with redis,django]]></title>
            <link>https://velog.io/@hak_0/bruteforce-with-redisdjango</link>
            <guid>https://velog.io/@hak_0/bruteforce-with-redisdjango</guid>
            <pubDate>Sun, 30 Mar 2025 08:07:41 GMT</pubDate>
            <description><![CDATA[<h2 id="brute-force">Brute Force</h2>
<blockquote>
<p><strong>브루트포스(Brute Force) 공격을 방어</strong> 하기위해
로그인 실패 횟수를 기억하고, 일정 횟수이상 실패하면 <strong>일시 차단</strong>하는 기능이 필요</p>
</blockquote>
<h3 id="django-axes">django-axes</h3>
<blockquote>
<p>Django에서 로그인 시도 추적&amp;차단을 해주는 오픈소스 패키지</p>
</blockquote>
<p>기본 특징은 </p>
<table>
<thead>
<tr>
<th align="left">항목</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">자동 로그인 시도 기록</td>
<td align="left">DB에 로그인 실패 내역 저장</td>
</tr>
<tr>
<td align="left">차단 정책 제공</td>
<td align="left">특정 IP, 계정 등 차단 기능</td>
</tr>
<tr>
<td align="left">관리자 화면 지원</td>
<td align="left">차단 해제, 기록 조회 가능</td>
</tr>
<tr>
<td align="left">DB 기반</td>
<td align="left">시도 기록은 DB에 저장됨(보통 RDB)</td>
</tr>
</tbody></table>
<h3 id="redis--커트서마이징-방식이-필요한-이유">Redis + 커트서마이징 방식이 필요한 이유</h3>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>성능</td>
<td>Redis는 메모리 기반이라 DB보다 발고, 요청 많은 시스템에서 유리함</td>
</tr>
<tr>
<td>확장성</td>
<td>마이크로서비스, 서버리스 환경에서 공유 캐시로 활용 가능</td>
</tr>
<tr>
<td>가볍게 쓸 수 있음</td>
<td>django-axes보다 의존성 적고, 내가 원하는 정책만 넣을 수 있음</td>
</tr>
<tr>
<td>유연한 정책 구현 가능</td>
<td>IP + 이메일 기준, 로그인 성공 시 초기화, 제한 시간 자유 설정 등 완전 제어 가능</td>
</tr>
<tr>
<td>로그인 외에도 확장 가능</td>
<td>이메일 인증, OTP 시도 제한 등 여러 곳에 활용 가능</td>
</tr>
</tbody></table>
<h2 id="django-로그인-시도-제한-방식-비교-django-axes-vs-redis--커스터마이징">Django 로그인 시도 제한 방식 비교: <code>django-axes</code> vs <code>Redis + 커스터마이징</code></h2>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>django-axes</code></th>
<th><code>Redis + 커스터마이징</code></th>
</tr>
</thead>
<tbody><tr>
<td>저장소</td>
<td>DB</td>
<td>Redis (메모리 기반)</td>
</tr>
<tr>
<td>성능</td>
<td>낮음 (DB 쓰기 부하)</td>
<td>높음 (빠른 응답, 경량 처리)</td>
</tr>
<tr>
<td>설정 난이도</td>
<td>쉬움 (설치 + 세팅만 하면 됨)</td>
<td>중간 (직접 코드 구현 필요)</td>
</tr>
<tr>
<td>커스터마이징</td>
<td>제한적 (정해진 정책만 설정 가능)</td>
<td>자유롭게 구현 가능 (IP, 이메일, TTL 등)</td>
</tr>
<tr>
<td>적용 유연성</td>
<td>로그인 시도 제한에 국한됨</td>
<td>로그인, OTP, 이메일 인증 등 다양하게 확장 가능</td>
</tr>
<tr>
<td>관리자 UI</td>
<td>Django Admin에 내장</td>
<td>없음 (원하면 직접 구현 가능)</td>
</tr>
<tr>
<td>보안 정책 제어</td>
<td>일부 가능 (설정으로 조절)</td>
<td>완전한 제어 가능 (개발자 마음대로)</td>
</tr>
<tr>
<td>추가 기능</td>
<td>자동 차단 해제, 알림 등 제공</td>
<td>직접 구현해야 함</td>
</tr>
</tbody></table>
<h3 id="💻-커스터-마이징-redis--django-구현-예">💻 커스터 마이징 redis + django 구현 예</h3>
<pre><code class="language-python">import os
import redis
from rest_framework.exceptions import PermissionDenied

if os.getenv(&quot;DOCKER_ENV&quot;, &quot;false&quot;).lower() ==&quot;true&quot;:
    REDIS_HOST = os.getenv(&quot;REDIS_HOST&quot;, &quot;redis&quot;)
else:
    REDIS_HOST = &quot;localhost&quot;

r = redis.Redis(host=REDIS_HOST, port=6379, db=0, decode_responses=True)

def get_login_attempt_key(key):
    return f&quot;login_attempt_{key}&quot;

def check_login_attempt_key(key, limit=5, block_time = 300):
    redis_key = get_login_attempt_key(key)
    attempts = r.get(redis_key)

    if attempts and int(attempts) &gt;= limit:
        raise PermissionDenied(
            detail=&quot;로그인 시도 횟수를 초과했습니다. 잠시후 다시 시도하세요&quot;,
            code=&quot;Too_much_attempts&quot;
        )
    pipe = r.pipeline()
    pipe.incr(redis_key)
    pipe.expire(redis_key, block_time)
    pipe.execute()

def reset_login_attempt(key):
    redis_key = get_login_attempt_key(key)
    r.delete(redis_key)</code></pre>
<p><strong>Redis pipline() 메서드 정리</strong></p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>set(key, value)</code></td>
<td>키에 값 저장</td>
</tr>
<tr>
<td><code>get(key)</code></td>
<td>키의 값 가져오기</td>
</tr>
<tr>
<td><code>incr(key)</code></td>
<td>키의 값을 1 증가</td>
</tr>
<tr>
<td><code>decr(key)</code></td>
<td>키의 값을 1 감소</td>
</tr>
<tr>
<td><code>expire(key, seconds)</code></td>
<td>키의 만료 시간 설정 (초 단위)</td>
</tr>
<tr>
<td><code>delete(key)</code></td>
<td>키 삭제</td>
</tr>
<tr>
<td><code>exists(key)</code></td>
<td>키 존재 여부 확인 (1 또는 0 반환)</td>
</tr>
<tr>
<td><code>ttl(key)</code></td>
<td>키의 TTL(남은 시간) 확인 (초 단위)</td>
</tr>
<tr>
<td><code>hset(name, key, value)</code></td>
<td>해시(hash)에 값 저장</td>
</tr>
<tr>
<td><code>hget(name, key)</code></td>
<td>해시에서 값 가져오기</td>
</tr>
<tr>
<td><code>sadd(key, value)</code></td>
<td>Set 자료형에 값 추가</td>
</tr>
<tr>
<td><code>smembers(key)</code></td>
<td>Set 전체 값 조회</td>
</tr>
</tbody></table>
<ul>
<li>redis.Redis(host=&quot;host&quot;, port=6379, db = 0, decode=response=True) decode =&gt; 기본적으로 UTF-8 로 디코딩해서 사용하기 위해</li>
</ul>
<p><strong>Login API 적용</strong></p>
<pre><code class="language-python">class UserLoginView(APIView):
    @swagger_auto_schema(
        security=[{&quot;Bearer&quot;: []}],
        request_body=UserLoginSerializer,
        responses={
            200: UserLoginSerializer,
            400: openapi.Response(
                description=(
                    &quot;잘못된 요청 시 응답\n&quot;
                    &quot;- `code`:`missmatch` , 이메일 또는 비밀번호 불일치.\n&quot;
                ),
            ),
            403: openapi.Response(
                description=(
                    &quot;- `code`:`not_verified`, 인증되지 않은 이메일\n&quot;
                    &quot;- `code`:`Too_much_attempts`, 로그인 시도횟수 5회 초과 실패 5분 간 불가&quot;
                )
            ),
        },
    )
    def post(self, request):
        email = request.data.get(&quot;email&quot;)
        password = request.data.get(&quot;password&quot;)

        check_login_attempt_key(email)

        user = authenticate(email=email, password=password)
        if not user:
            return Response(
                {
                    &quot;error&quot;: &quot;이메일 또는 비밀번호가 올바르지 않습니다.&quot;,
                    &quot;code&quot;: &quot;missmatch&quot;,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        if not user.email_verified:
            return Response(
                {
                    &quot;error&quot;: &quot;이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.&quot;,
                    &quot;code&quot;: &quot;not_verified&quot;,
                },
                status=status.HTTP_403_FORBIDDEN,
            )

        refresh = RefreshToken.for_user(user)
        access_token = str(refresh.access_token)

        store_access_token(user.id, access_token, 3600)
        store_refresh_token(user.id, str(refresh), 86400)

        # activity log 추가 = 로그인
        ActivityLog.objects.create(
            user_id=user,
            action=&quot;LOGIN&quot;,
            ip_address=get_client_ip(request),
        )

        reset_login_attempt(email)

        return Response(
            {
                &quot;access_token&quot;: access_token,
                &quot;refresh_token&quot;: str(refresh),
                &quot;token_type&quot;: &quot;Bearer&quot;,
                &quot;expires_in&quot;: 3600,
            },</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Throttle with DRF]]></title>
            <link>https://velog.io/@hak_0/Throttle-with-DRF</link>
            <guid>https://velog.io/@hak_0/Throttle-with-DRF</guid>
            <pubDate>Sat, 29 Mar 2025 14:19:34 GMT</pubDate>
            <description><![CDATA[<h1 id="throttling">Throttling</h1>
<blockquote>
<p>왜 쓰는가 ?</p>
<ul>
<li>API 남용 방지(과도한 요청)</li>
<li>비즈니스 정책 반영(무료/유료 티어)</li>
<li>서버 자원 보호</li>
<li>보안 목적으로는 사용 X(IPsppofing 쉽게 가능함)</li>
</ul>
</blockquote>
<h3 id="why-use">Why Use?</h3>
<ul>
<li><strong>비인증 사용자에게 낮은 제한</strong>, 인증 사용자에게 더 높은 제한 주고 싶을 때</li>
<li>특정 API가 리소스를 많이 써서, 해당 API만 따로 제한 걸고 싶을 때</li>
<li><strong>짧은 시간(버스트)</strong> 과 <strong>긴 시간(지속적)</strong> 요청 제한을 함께 쓰고 싶을 때</li>
</ul>
<h2 id="설정-방법">설정 방법</h2>
<ol>
<li>settings.py 전역 설정</li>
</ol>
<pre><code class="language-python">REST_FRAMEWORK = {
    # 전역설정
    &#39;DEFAULT_THROTTLE_CLASSES&#39; : [
        &#39;rest_framework.throttling.AnonRateThrottle&#39;, # 비인증
        &#39;rest_framework.throttling.UserRateThrottle&#39;,
    ]
    # 설정 값
    &#39;DEFAULT_THROTTLE_RATES&#39; : {
        &#39;anon&#39;: &#39;100/day&#39;,
        &#39;user&#39;: &#39;1000/day&#39;, 
    }
}</code></pre>
<ul>
<li>DEFAULT_THROTTLE_CLASSES 를 설정해놓으면 전역에 설정된다.</li>
<li>DEFAULT_THROTTLE_RATES 를 설정하고 개인 API에 적용시키면 덮어써 진다</li>
<li>전역 설정을 하지 않고 API마다 따로 설정하고 싶으면 아래의 RATES 부분만 settings에 작성한 후
개인 API에 <code>throttle_class</code> 를 지정해 주면 된다.</li>
</ul>
<ol start="2">
<li>개별 View에서 설정<pre><code class="language-python">from rest_framework.throttling import UserRateThrottle
</code></pre>
</li>
</ol>
<p>class MyView(APIView):
    throttle_classes = [UserRateThrottle]</p>
<pre><code>
3. @api_view와 데코레이터
```python
@api_view([&#39;GET&#39;])
@throttle_classes([UserRateThrottle])
def my_view(request):
    ...</code></pre><h2 id="주요-throttle-클래스들">주요 Throttle 클래스들</h2>
<table>
<thead>
<tr>
<th>클래스</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>AnonRateThrottle</strong></td>
<td>인증 안 된 사용자에게 IP 기준으로 제한</td>
</tr>
<tr>
<td><strong>UserRateThrottle</strong></td>
<td>인증된 사용자 ID 기준 제한(없으면 IP로 fallback)</td>
</tr>
<tr>
<td><strong>ScopedRateThrottle</strong></td>
<td>View 별로 throttle scope 설정해 제한 가능</td>
</tr>
<tr>
<td><strong>CustomThrottle</strong></td>
<td>직접 throttle 로직 상성 가능(BaseThrottle 상속)</td>
</tr>
</tbody></table>
<h2 id="사용자-구분-기준">사용자 구분 기준</h2>
<ul>
<li>X-Forwarded-For 헤더 -&gt; 프록시를 거친 경우에 사용됨<ul>
<li>X-Forwarded-For 헤더는 클라이언트의 원래 IP주소를 알기 위해 사용하는 HTTP 헤더이다. 이 헤더는 프록시나 로드밸런서 같은 중간 서버를 거쳐서 들어온 요청에 추가된다.</li>
<li>ex) <code>X-Forwarded-For</code>: 10.0.0.1, 123.123.123.1<ul>
<li>123.123.123.1 은 실제 클라이언트 IP</li>
<li>나머지는 프록시나 로드밸런스 IP 이다 장고는 이걸 기준으로 클라이언트 IP를 뽑으려 한다.</li>
</ul>
</li>
</ul>
</li>
<li>없으면 Remote_ADDR 사용<ul>
<li>X-Forwarded-For가 없으면 장고는 기본적으로 WSGI 환경 변수인 REMOTE_ADDR을 사용한다
이건 실제 서버에 도달한 요청의 IP인데, <strong>프록시를 거쳤다면 프록시의 IP</strong> 일 수 있어서 정확하지 않을 수 있음</li>
</ul>
</li>
<li>NUM_PROXIES 설정으로 실제 클라이언트 IP 추출 가능<ul>
<li>X-Forwarded-For에서 실제 클라이언트 IP를 뽑을 ㄸ&#39;ㅐ, 프록시를 몇개 거치는지 알려주는 설정<pre><code class="language-python">REST_FRAMEWORK = {
&#39;NUM_PROXIES&#39;: 2
</code></pre>
</li>
</ul>
</li>
</ul>
<h1 id="x-forwarded-for-1231231231-10001-10002">X-Forwarded-For: 123.123.123.1, 10.0.0.1, 10.0.0.2</h1>
<h1 id="앞에-1번째--실제-클라이언트--나머지-2개-num_proxies-proxy-ip-로-보는-것이다">앞에 1번째 = 실제 클라이언트 , 나머지 2개 NUM_PROXIES proxy ip 로 보는 것이다.</h1>
<p>}</p>
<pre><code>

### 💻 예시: Burst + Sustained 이중 제한

* Burst, Sustained
```python
class BurstRateThrottle(UserRateThrottle):
    scope = &#39;burst&#39;
class SustainedRateThrottle(UserRateThrottle):
    scope = &#39;sustained&#39;

REST_FRAMEWORK = {
    &#39;DEFAULT_THROTTLE_CLASSES&#39;: [
        &#39;my_app.throttles.BurstRateThrottle&#39;,
        &#39;my_app.throttles.SustainedRateThrottle&#39;,
    ],
    &#39;DEFAULT_THROTTLE_RATES&#39;: {
        &#39;burst: &#39;60/min&#39;,
        &#39;sustained&#39; : &#39;1000/day&#39;,
    }
}

# 이중 제한 적용
class myapp_view(APIView):
    throttle_classes = [BurstRateThrottline, SustainedRateThrottle]</code></pre><h4 id="burst버스트">burst(버스트)</h4>
<ul>
<li>짧은 시간 동안 허용되는 최대 요청 수</li>
<li>Ex) 60/min -&gt; 1분에 최대 60번 요청 허용</li>
</ul>
<p><strong>목적</strong><br>&quot;잠깐동안 급하게 많은 요청을 보내는 건 허용하되,<br>너무 오래 그렇게 보내지는 못 하게 하자&quot;</p>
<p><strong>비유</strong><br>손님이 순간적으로 몰려드는건 괜챃지만,<br>계속 몰리면 주방이 터지는 거니까 그런 걸 막는 장치</p>
<h4 id="sustained지속">sustained(지속)</h4>
<ul>
<li>긴 시간 동안 허용되는 총 요청 수를 의미</li>
<li>Ex) 1000/day -&gt; 하루에 최대 1000번 요청 가능</li>
</ul>
<p><strong>목적</strong><br>&quot;서버 자원을 하루 단위로 적절히 분배해서 남용 방지&quot;  </p>
<p><strong>비유</strong><br>하루에 너무 많이 오는 단골손님을 쫓아낼 수는 없지만<br>일정 이상은 못 사게 제한하는 것</p>
<p><strong>burst + sustained</strong></p>
<ul>
<li>사용자가 1분 안에 60번 넘게 요청 -&gt; burst 초과로 차단</li>
<li>사용자가 하루 종일 천천히 요청해서 1000번 도달 -&gt; sustained 초과로 차단</li>
</ul>
<h2 id="결론">결론</h2>
<p><code>Throttle</code>를 사용하는데 하나의 제한이 아닌 burst + sustained로 나눠서 두개의 제한을 둘수 있다.<br>또한 인증된 User와 그렇지 않은 유저의 제한을 나눌수 있다.<br><code>UserRateThrottle</code> -&gt; 인증된 유저<br><code>AnonRateThrottle</code> -&gt; 인증되지 않은 유저</p>
<p><strong>각각을 상속</strong>받아 스코프를 <strong>나누어 제한</strong>을 나눌수도 있다.</p>
<p>API 프로젝트를 하다 Throttle에 대한 개념을 정리해 보았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 개념]]></title>
            <link>https://velog.io/@hak_0/Django-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@hak_0/Django-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Thu, 27 Feb 2025 09:34:41 GMT</pubDate>
            <description><![CDATA[<h3 id="장고-shell">장고 shell</h3>
<h3 id="sqlmigrate"><code>sqlmigrate</code></h3>
<blockquote>
<ul>
<li>makemigartions 를 하면 DB에 실행 되기전의 파일이 생성된다.</li>
</ul>
</blockquote>
<ul>
<li>sqlmigrate는 마이그레이션이 실제로 어떤 SQL을 실행할지 미리 확인하는 명령어 이다.</li>
<li>데이터베이스에 변경을 적용하지 않고, SQL만 출력한다.</li>
<li><code>python manage.py sqlmigrate &lt;앱이름&gt; &lt;마이그레이션 번호&gt;</code></li>
</ul>
<h3 id="model">Model</h3>
<ul>
<li>auto_now: 세이브 할때마다 수정</li>
<li>auto_now_add: 처음 넣을때 생성</li>
</ul>
<h3 id="django-settings">Django Settings</h3>
<ul>
<li>시간설정은 일단 우선 한국기준으로 세팅하자<pre><code class="language-python"># shettings.py
LANGUAGE_CODE = &quot;en-us&quot;
</code></pre>
</li>
</ul>
<p>TIME_ZONE = &quot;Asia/Seoul&quot;</p>
<p>USE_I18N = True</p>
<p>USE_TZ = False</p>
<pre><code>
### 단위 테스트로 잡을수 없는 동시성 문제

#### test
&gt; * `poetry add django-ninja` 
&gt;   - DRF 보다 가볍고 FastAPI 스타일을 지원하는 장고 라이브러리 스웨거 문서지원

```python
from ninja import NinjaAPI
api = NinjaAPI()


@api.get(&quot;like&quot;)
def api_do_like(request: HttpRequest, user_id: int, article_id: int) -&gt; None:
    do_like(user_id, article_id)
    return None</code></pre><ul>
<li><p>swagger-ui 를 사용해 데이터를 넣어 본다 그리고 http url를 찾는다</p>
</li>
<li><p><code>ctx.captured_queries</code> : Django의 테스트 환경에서 실행된 데이터베이스 쿼리를 캡처할 때 사용하는 속성</p>
<pre><code class="language-python">from django.test.utils import CaptureQueriesContext
from django.db import connection
from myapp.models import User
</code></pre>
</li>
</ul>
<p>with CaptureQueriesContext(connection) as ctx:
    User.objects.filter(is_active=True).count()  # 실행할 쿼리</p>
<h1 id="실행된-쿼리-확인">실행된 쿼리 확인</h1>
<p>for query in ctx.captured_queries:
    print(query[&quot;sql&quot;])  # 실행된 SQL 출력</p>
<pre><code>
#### httpie 를 사용해 다발성으로 넣어본다
&gt; * curl 대신 http를 사용할수 있게 해주는 패키지
&gt; * `brew install httpie`

```HTML
http &quot;:8000/api/like?user_id=2&amp;article_id=2&quot; &amp;
http &quot;:8000/api/like?user_id=2&amp;article_id=2&quot; &amp;
http &quot;:8000/api/like?user_id=2&amp;article_id=2&quot; &amp;
http &quot;:8000/api/like?user_id=2&amp;article_id=2&quot; &amp;
http &quot;:8000/api/like?user_id=2&amp;article_id=2&quot; &amp;</code></pre><p>이 코드를 터미널에 실행시키면 DB에서 한개가 아닌 여러개의 데이터가 들어간 것을 확인할 수 있다.</p>
<ul>
<li>View코드 에서 같은 코드가 들어가는걸 막아놨다
문제는 거의 동시에 같은 입력값을 넣으면 DB에서 데이터가 없다고 판단하고 넣어버리는 것이다.</li>
</ul>
<h4 id="이-문제를-해결하기-위해선-model의-내용을-수정해서-그전에-차단해-주어야한다">이 문제를 해결하기 위해선 Model의 내용을 수정해서 그전에 차단해 주어야한다.</h4>
<p><strong><code>UniqueConstarnt</code></strong></p>
<pre><code class="language-python">class Like(BaseModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    class Meta:
        constraints = [
            models.UniqueConstraint(fields=[&quot;user&quot;,&quot;article&quot;], name=&quot;UIX_user_id_article_id&quot;)
        ]</code></pre>
<p>이후 migrate 를 진행한 후 다시한번 넣어보면 하나의 컬럼만 형성된 것을 확인할 수 있다.</p>
<h2 id="debug">Debug</h2>
<p>테스트 코드에서 중단점을 찍어 디버그를 할수 있다. 디버그시 변수명, 타입명 등을 바로 찾을수 있고 어떤 에러가 발생했는지 확인할 수도 있다. Exception 으로 처리해놓은 에러 확인가능.</p>
<h3 id="중단점을-찍어야-하는-주요-위치">중단점을 찍어야 하는 주요 위치</h3>
<p>✅ 함수의 시작 부분 → 입력 값이 올바른지 확인
✅ 조건문 내부 (If-Else) → 어떤 분기로 가는지 확인
✅ 반복문 내부 (For / While) → 루프가 정상적으로 도는지 확인
✅ 예외가 발생할 가능성이 있는 부분 → 오류 원인 분석
✅ 변수 값이 변경되는 부분 → 데이터 흐름 추적</p>
<h3 id="evalutae">Evalutae</h3>
<blockquote>
<p>PyCharm의 Evaluate Expression (표현식 평가) 기능은 디버깅 중 특정 변수의 값을 확인하거나 표현식을 실행할 때 사용하는 도구입니다.</p>
</blockquote>
<p>📌 주요 기능
✅ 디버깅 중 특정 변수 값 확인
✅ 특정 코드나 표현식을 실행하여 결과 확인
✅ 새로운 값으로 변수 변경 가능</p>
<h3 id="wasweb-apllication-server">WAS(Web Apllication Server)</h3>
<blockquote>
<p>웹 서버와(HTTP요청 처리)와 애플리케이션 실행 환경을 제공하는 서버</p>
</blockquote>
<p>사용자의 요청을 처리하고 DB와 연동하며 동적인 응답을 생성</p>
<p>웹서버는 정적 콘텐츠 제공, WAS는 동적인 로직 처리를 담당.</p>
<p>WAS는 웹 애플리케이션이 실행되는 핵심 서버로, 클라이언트의 요청을 받아서 비즈니스 로직을 실행하고 응답을 생성하는 역할을 합니다. 웹 서버와 함께 사용되며, 다양한 기술과 연계하여 확장성과 보안성을 높일 수 있음</p>
<h4 id="settingspy">settings.py</h4>
<p>Author.objects.using(&quot;default&quot;) # default 로 정의해 놓은 DB를 사용
Author.objects.using(&quot;read&quot;) # 읽기 전용으로 지정해 놓은  DB사용</p>
<h2 id="coverage">coverage</h2>
<blockquote>
<ul>
<li>테스트가 코드의 몇 퍼센트를 실행했는지 측정하는 지표</li>
</ul>
</blockquote>
<ul>
<li><p><code>poetry run coverage run manage.py test</code> 테스트 실행 및 커버리지 측정</p>
</li>
<li><p><code>poetry run coverage report -m</code>  커버리지 리포트 출력</p>
</li>
<li><p>miss -&gt; 1줄: 한번도 실행되지 않은 줄 체크</p>
</li>
<li><p>-m =&gt; show missing : 어느 라인에서 버그가 발생했는지 확인</p>
</li>
</ul>
<p>coverage 는 전체코드 분의 제품 테스트 코드 이다. 그래서 모든 함수에 대해 테스트코드를 작성해야한다.</p>
<pre><code class="language-python">[tool.coverage.report]
fail_under = 80 # 몇프로를 기준으로 coverage가 됬는지 하는 것

[tool.coverage.run]
omit = [ # coverage 측정을 할 필요가 없는 것들
  &quot;.mypy_cache/**/*&quot;, # mypy가 내부적으로 하는 커버리지
  &quot;*/migrations/*&quot;, # 장고에서 만든건데 할 필요가 없음
  &quot;*/*test*.py&quot;, # 테스트 코드 할필요 없음
  # 앱의 실제 코드가 테스트에 의해 얼마나 실행되었는지 측정하는 것인데 
  # 테스트를 수행하는 코드이기에 측정할 필요가 없음
]</code></pre>
<p><strong>ci 파일에 이걸 넣는다</strong></p>
<pre><code class="language-yml">      - name: Test python project
        run: |
          poetry run coverage run manage.py test
          poetry run coverage report -m</code></pre>
<ul>
<li>리눅스에서 성공 여부를 체크할 수 있다. <code>echo $?</code> 바로 직전의 코드를 출력하는 명령어인데 <code>eixt code</code> 가 출력된다 리눅스에서 exit 코드는 0을 제외한 다른 숫자는 전부 오류가 발생했단 것이다</li>
</ul>
<h2 id="용어">용어</h2>
<ul>
<li>LazyLoad : 정말로 필요하기 전까지 가져오지 않는것을 LazyLoad라 한다.</li>
<li>eagerload &lt;=&gt; prefetch : 미리 가져오는것</li>
</ul>
<p>select related : join
prefetch related : 개별 쿼리가 생성</p>
<h2 id="rdbms">RDBMS</h2>
<p>가장큰 특징 : 트랜잭션</p>
<blockquote>
<p>서버가 다운되면 앞에 내용이 처리되는  것을 방지하기위해 전부 실패되거나 전부 성공을 처리한다 이것이
Atomicity 이다.</p>
</blockquote>
<p>기본 구조는 모든것을 실행 후 commit or Rollback 을 하는 것이다.</p>
<p>기본 구조는 원래 BEGIN; 을 시작한후 모든 처리동작을 한후 COMMIT 을 하는것이다 or Rollback</p>
<p>쉽게말해 다른 브랜치로 이동하여 작업하는 것이다 그래서 다른곳에서의 일이 영향을 받지 않는다.</p>
<h3 id="acid">ACID</h3>
<ul>
<li><p><strong>A, Atomicity</strong>: 일련의 데이터베이스 연산이 모두 성공하거나 모두 실패하는 특성. (all or nothing)</p>
</li>
<li><p><strong>C, Consistency</strong>: 트랜젝션이 끝나도 integrity 가 유지되는 특성. (foreign key 가 없는 id 를 가리키거나 하지 않습니다.)</p>
</li>
<li><p><strong>I, Isolation</strong>: 한 트랜젝션에서 일어난 변경사항이 다른 트랜젝션에서 조회되는 정도를 의미합니다. mysql 에서는 다음과 같은 isolation level 이 존재합니다.(innodb 기준) <a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html">https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html</a></p>
<ul>
<li>SERIALIZABLE</li>
<li>REPEATABLE READ (디폴트) (다른 트랜젝션에서의 변경사항은 commit 되기 전까지는 안 보인다)</li>
<li>READ COMMITTED</li>
<li>READ UNCOMMITTED</li>
</ul>
</li>
<li><p><strong>D, Durability</strong>: 트랜젝션이 COMMIT 되고나면 영구적으로 저장되는 특성을 의미합니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 기본 세팅]]></title>
            <link>https://velog.io/@hak_0/%EA%B0%9C%EB%B0%9C-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@hak_0/%EA%B0%9C%EB%B0%9C-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Wed, 26 Feb 2025 08:18:21 GMT</pubDate>
            <description><![CDATA[<h2 id="tip-파이참-단축키">Tip, 파이참 단축키</h2>
<ul>
<li>sifht 2번 파이참 내부의 모든걸 찾는 단축키</li>
<li>cmd + option + k : 커밋과 푸쉬를 한번에 하는 단축키</li>
<li>cmd 엔터 : 커밋</li>
<li>iterm, poetry 가상환경 사용 : poetry env activate 해서 나온 가상환경 실행 동작 복붙</li>
<li>shift 2번 모든걸 찾을수 있는 단축키</li>
<li>cmd + option + k : 커밋과 푸쉬 한번에 하는 단축키</li>
<li>cmd + shift +  n : 아무 파일이나 아무경로에 빠르게 만드는 단축키</li>
<li>cmd + option + l : 코드 정렬 단축키 -&gt; sql</li>
<li>cmd + 7 : 구조 확인 데이터베이스</li>
<li>control + shift + R : 해당 함수만 테스트 실행</li>
<li>control + shift + D : 해당 함수만 디버깅</li>
<li>option + enter : 객체 추가 클래스에서</li>
<li>control + r: 터미널 창에서 명령창 그전에 했떤거 검색가능</li>
<li>option + 방향키 : 단어 단위로 옮겨감</li>
<li>fn + 방향키 : 끝과 끝</li>
<li>파이참 기능 커맨드+R : 찾기 + 수정</li>
<li>내가설정해 놓은것 컨트롤 + a,z : 위아래로 해당영역 설정</li>
<li>거기서 움직이려면 움직이면 다같이 움직임 선택하려면 쉬프트 누르고 오른쪽</li>
</ul>
<ul>
<li>HTTP Request Header:<ul>
<li>Host : 누구에게 메세지를 보낼 것이냐, 받는 사람이름의 역할</li>
<li>User-Agent : 보내는 내가 누구냐</li>
<li>Content-type : 타입</li>
</ul>
</li>
</ul>
<h4 id="정적-동적">정적 동적</h4>
<p>static : 정적, 실행 전에 결정
dynamic : 동적, 실행 중에 결정</p>
<h3 id="git--파이참">Git + 파이참</h3>
<ul>
<li>git init : 왼쪽 commit 관리하는 확장파일 생성됨 </li>
<li>커밋전 무엇을 커밋하는지 항상 확인하고 실행하는 습관</li>
<li>I/O 동작이 일어나는 것은 필요한지 안필요한지 항상 체크 시스템에 큰 부하를 일으킴 ex) print</li>
</ul>
<h4 id="db-연결">DB 연결</h4>
<p>파이참 오른쪽 db 에서 연결 시키기
안에서 sql문 실행 시켜서 확인</p>
<p><code>poetry add pymysql cryptography</code></p>
<p><code>poetry add -D **types-PyMySQL**</code></p>
<pre><code class="language-python">pymysql.install_as_MySQLdb()
DATABASES = {
    &quot;default&quot;: {
        &quot;ENGINE&quot;: &quot;django.db.backends.mysql&quot;,
        &quot;NAME&quot;: &quot;oz_django&quot;,
        &quot;USER&quot;: &quot;root&quot;,
        &quot;PASSWORD&quot;: &quot;1234&quot;,
        &quot;HOST&quot;: &quot;localhost&quot;,
        &quot;PORT&quot;: &quot;3306&quot;,
    }
}</code></pre>
<h2 id="개발시-테스트-환경-구축-및-통일">개발시 테스트 환경 구축 및 통일</h2>
<ul>
<li><p>poetry add django-stubs : 장고의 타입힌팅 지원 패키지가 설치</p>
<ul>
<li>정적 타입 분석을 위한 패키지</li>
<li>mypy와 함께 Django 코드의 타입 검사를 수행</li>
<li>Python의 기본적인 타입 힌팅 뿐만 아니라, Django ORM 관련 타입도 제공</li>
</ul>
</li>
<li><p>dev dependency &lt;--&gt; dependency (poetry add -D 패키지, poetry add 패키지)</p>
<ul>
<li>dev : 개발환경에서만 사용하는 패키지</li>
<li>기본: 실제 배포환경에서의 패키지</li>
<li><strong>개발하다가 이건 개발환경에서만 쓰겟다 싶은건 -d로해서 추가로 깔고 기존에 있떤건 삭제시킨다</strong></li>
</ul>
</li>
<li><p>프로젝트 생성</p>
</li>
<li><p>ignore 생성, <code>.gitignore</code></p>
<pre><code class="language-sh">.idea/
venv/
__pycache__/</code></pre>
</li>
</ul>
<h3 id="ipython">ipython</h3>
<p>`poetry add ipython </p>
<h3 id="black">black</h3>
<blockquote>
<ul>
<li>codeformatter: 여러 개발자의 코드 스타일을 통일시켜주는 패키지 <ul>
<li>ex) &#39; &#39; -&gt; &quot; &quot; , 공백...  맞춰진것 commit 목록에서 확인 가능</li>
</ul>
</li>
<li>poetry add -D black</li>
</ul>
</blockquote>
<ul>
<li><p><code>poetry run black .</code> </p>
</li>
<li><p>poetry.toml 추가</p>
<pre><code class="language-toml"># 자동으로 넘겨지는것 방지 기존이 80
[tool.black]
line-length = 120
</code></pre>
</li>
</ul>
<h1 id="파이참-수정">파이참 수정</h1>
<h1 id="preference---editor---code-style---hard-wrap-at--설정한-블랙-값으로-수정">preference -&gt; editor -&gt; code style -&gt; hard wrap at : 설정한 블랙 값으로 수정</h1>
<pre><code>### isort
&gt; * `poetry run isort .`
&gt; * 알파벳 순서를 맞추는 정도의 동작. 

* poetry.toml 추가
```toml
[tool.isort]
profile = &quot;black&quot;</code></pre><h3 id="mypy">Mypy</h3>
<blockquote>
<ul>
<li>코드를 읽고 잠재적인 에러를 찾는다</li>
<li><code>poetry run mypy .</code></li>
</ul>
</blockquote>
<ul>
<li>poetry.toml 추가<pre><code class="language-python">[tool.mypy]
plugins = [&quot;mypy_django_plugin.main&quot;]
python_version = &quot;3.13&quot;
strict = true # mypy 진가 타입을 엄격히 써야함
</code></pre>
</li>
</ul>
<p>[[tool.mypy.overrides]]  # 특정한 모듈에 한해서 덮어쓰겠다.
module = &quot;<em>.migrations.</em>&quot; 
ignore_errors = true # 위의 폴더에 관해서는 mypy를 무시하겠다. 어차피 장고에서 만들어주니</p>
<p>[[tool.mypy.overrides]]
module = &quot;manage&quot;
ignore_errors = true # 위의 폴더에 관해서는 mypy를 무시하겠다. 어차피 장고에서 만들어주니</p>
<p>[tool.django-stubs]
django_settings_module = &quot;폴더명.settings&quot; # settings의 경로 명시</p>
<pre><code>
* mypy 에서는 튜플, 리스트, 딕 이런건 의미 없기에 None 빈 형태는 의미가 없다 그래서 내부의 요소에도 type을 정해줘야 한다. =&gt; mypy strict True 옵션
* `reveal_type(어떤 타입이 들어가 있는지 확인할 타입명)` : from typing import reveal_type
* mypy는 정적이다, 그리고 코드를 실행해 보지 않는다. 

### Github Action
&gt; * Continuous integration(CI)
&gt; * github에 push 할 때마다 테스트가 자동으로 실행되도록 만들기
&gt; * ci.yml 안에있는 내용을 github에서 docker처럼 새로 만들고 테스트 해본후 없앤다.
&gt; * 독립된 환경에서 독립적인 자기만의 데이터를 가지고 실행

- Continuous IntegrationCI 가 필요한 이유 : 수십 명이 하나의 코드베이스에서 작업을 할 때 
다음과 같은 과정을 반복하게 됩니다.

    - 테스트가 끝난 코드(master)에서 새로운 branch 를 생성합니다.
    - 새로운 branch 에서 작업합니다. (기능 추가, 최적화 등등)
    - 로컬에서 테스트 합니다.
    - PR 을 올립니다. 코드리뷰, 테스트
    - 개발서버에 배포 합니다. (staging), 개발 서버에서 기계를 통해 기존의 기능까지 전부 잘되나 검증
    - develop 서버에서 테스트 합니다.
    - master 에 merge, 배포합니다.


 이렇게 작업할 때 마다 반복적인 작업이 있습니다. PR 을 올리기 전에 테스트를 다 통과해야 한다는 점, merge 할 때 마다 테스트 해야 하는 점 등이죠.

 이를 자동화 함으로써

- human error 를 줄입니다.
- 반복적인 작업에 들어가는 시간을 아낌으로써 더 효율적으로 일할 수 있습니다.

* 아래의 내용을 커밋해서 push 합니다.
* 깃허브 액션 스크립트, `.github/workflows/ci.yml` 폴더 및 파일 생성
```yaml
name: Django CI

# 트리거 언제 실행한건 지
on:
  push: # 푸쉬 할때마다
#  workflow_dispatch: # 그 버튼을 누르면 실행

jobs:
  ci:
    runs-on: ubuntu-22.04 # github action 이 제공하는 ubuntu는 mysql 이 기본적으로 설치되어 있음
    env:
      # 환경 변수
      DB_HOST: 127.0.0.1
      DB_PORT: 3306
      DB_USER: root
      DB_PASSWORD: 1234 # 깃허브에 넣는거라 비밀번호 의미없음
      DB_DATABASE: oz_django

    steps: # 단계
      - name: Check out the codes
        uses: actions/checkout@v2

      - name: Setup python environment
        id: setup-python
        uses: actions/setup-python@v2
        with:
           python-version: &#39;3.13&#39;

      - name: Set timezone to KST
        run: |
          sudo rm /etc/localtime
          sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

      # Start Mysql
      # https://ovirium.com/blog/how-to-make-mysql-work-in-your-github-actions/
      - name: Start Mysql
        # 이미 설치된 mysql을 단순히 킴
        run: |
          sudo systemctl start mysql
          mysql -e &quot;use mysql; FLUSH PRIVILEGES; ALTER USER &#39;${{ env.DB_USER }}&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;${{ env.DB_PASSWORD }}&#39;;&quot; -uroot -proot
          mysql -e &#39;CREATE DATABASE ${{ env.DB_DATABASE }};&#39; -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
      - name: Install Poetry
        run: |
          curl -sSL curl -sSL https://install.python-poetry.org | POETRY_VERSION=2.0.1 python3 -
          echo &quot;${HOME}/.local/bin&quot; &gt;&gt; $GITHUB_PATH

      - name: Install dependencies
        run: |
          poetry install --no-root

      - name: Run black
        # check 만 하고 실제론 바꾸지 않는다 우리 프로젝트에서 바꿔야 하니까
        run: |
          poetry run black . --check          

      - name: Run isort
        # 마찬가지
        run: |
          poetry run isort . --check --diff

      - name: Run Mypy
        run: |
          poetry run mypy .

#      - name: Test python project
#        run: |
#          poetry run python manage.py test</code></pre><ul>
<li><code>on: push</code> → 액션이 언제 실행되는지를 정의합니다.  <code>on: push</code> 라면, push 할 때마다 실행합니다. <a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#on">https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#on</a></li>
<li><code>jobs</code>→ 하나의 workflow 는 여러개의 job 으로 구성됩니다. 여기서는 ci 라는 이름(id)의 job 하나만 정의하여서 사용하였습니다. <a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobs">https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobs</a></li>
<li><code>runs-on</code> → job 이 실행되는 machine 을 의미합니다. github 가 제공하는 ubuntu-latest (현재 버전 20) 을 사용합니다. <a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idruns-on">https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idruns-on</a></li>
<li><code>steps</code> → 하나의 job 은 여러개의 step 으로 구성됩니다. step 은 명령을 실행하거나 다른 action 을 실행합니다.</li>
<li><code>uses</code> → 실행할 action 을 가리킵니다.</li>
<li><code>with</code> → action 에 전달할 parameter 변수입니다.</li>
<li><code>run</code> → 실행할 명령어입니다.</li>
<li><code>run: |</code> → yaml 문법입니다. <code>|</code> (파이프라인)을 사용해 value 가 여러 줄(multiline) 이라는 것을 의미합니다.</li>
</ul>
<p>TL; DR:</p>
<p>push 될 때 마다 → 우분투에서 → 파이썬을 설치하고 → poetry, django, 등등등 종속성 설치하고 → test 를 실행한다.</p>
<ul>
<li><p>push</p>
<p>  <code>git remote add origin https://github.com/deokbaae/oz_django.git</code></p>
<ul>
<li><p>pycharm 에서 shift 2번 연타 후 push</p>
<p>github login</p>
</li>
</ul>
</li>
<li><p>매번 테스트 코드를 사용할 수 없으니 test.sh 제작
<code>poetry add coverage</code> 패키지 설치를 한다.</p>
</li>
<li><p><code>chmod +x ./test.xh</code> 를 통해 관리자 권한으로 허용해준다</p>
</li>
<li><p><code>./test.sh</code></p>
<pre><code class="language-sh">set -eo pipefail
</code></pre>
</li>
</ul>
<p>COLOR_GREEN=<code>tput setaf 2;</code>
COLOR_NC=<code>tput sgr0;</code> # No Color</p>
<p>echo &quot;Starting black&quot;
poetry run black .
echo &quot;OK&quot;</p>
<p>echo &quot;Starting isort&quot;
poetry run isort .
echo &quot;OK&quot;</p>
<p>echo &quot;Starting mypy&quot;
poetry run mypy .
echo &quot;OK&quot;</p>
<p>echo &quot;Starting test with coverage&quot;
poetry run coverage run manage.py test
poetry run coverage report -m</p>
<p>echo &quot;${COLOR_GREEN}All tests passed successfully!${COLOR_NC}&quot;</p>
<p>```</p>
<h3 id="agile-애자일-개념-정리">Agile (애자일) 개념 정리</h3>
<h4 id="📝-애자일agile-개요">📝 애자일(Agile) 개요</h4>
<p><strong>Agile(애자일)</strong> 은 소프트웨어 개발 및 프로젝트 관리에서 <strong>유연하고, 빠르고, 협업 중심</strong>으로 작업하는 방법론입니다.<br><strong>기본 철학</strong>: <strong>작은 단위로 빠르게 개발하고, 지속적으로 피드백을 반영하여 개선</strong>하는 방식</p>
<hr>
<h4 id="애자일의-핵심-원칙">애자일의 핵심 원칙</h4>
<p>애자일의 기본 원칙은 <strong>&quot;애자일 선언(Agile Manifesto)&quot;</strong>에서 정의되었습니다.</p>
<ol>
<li><strong>프로세스보다 개인과 팀워크를 중시</strong>  </li>
<li><strong>방대한 문서보다 실행 가능한 소프트웨어를 중시</strong>  </li>
<li><strong>계약 협상보다 고객과의 협력을 중시</strong>  </li>
<li><strong>초기 계획보다 변화에 대한 유연성을 중시</strong>  </li>
</ol>
<p>즉, <strong>빠른 피드백, 반복적인 개선, 고객과의 협업</strong>이 애자일의 핵심입니다. 🚀</p>
<hr>
<h4 id="애자일의-대표적인-방법론">애자일의 대표적인 방법론</h4>
<table>
<thead>
<tr>
<th>방법론</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Scrum (스크럼)</strong></td>
<td>짧은 개발 주기(<code>스프린트</code>), 역할 분담 (Product Owner, Scrum Master, Dev Team)</td>
</tr>
<tr>
<td><strong>Kanban (칸반)</strong></td>
<td><code>To Do - In Progress - Done</code> 형태의 <strong>시각적 관리 시스템</strong></td>
</tr>
<tr>
<td><strong>XP (익스트림 프로그래밍)</strong></td>
<td>TDD(테스트 주도 개발), 페어 프로그래밍, 지속적 배포</td>
</tr>
<tr>
<td><strong>Lean (린 개발)</strong></td>
<td>낭비 최소화, 최적의 효율성 추구</td>
</tr>
</tbody></table>
<hr>
<h4 id="애자일-프로세스-scrum-예시">애자일 프로세스 (Scrum 예시)</h4>
<ol>
<li><strong>백로그(Backlog) 작성</strong> - 해야 할 작업 목록 정의  </li>
<li><strong>스프린트 계획(Sprint Planning)</strong> - 1~4주 동안 개발할 목표 설정  </li>
<li><strong>데일리 스탠드업(Daily Stand-up)</strong> - 매일 15분 회의로 진행 상황 공유  </li>
<li><strong>스프린트 개발(Sprint Execution)</strong> - 개발 진행 (코딩, 테스트, 배포)  </li>
<li><strong>스프린트 리뷰(Sprint Review)</strong> - 고객과 함께 기능 시연 및 피드백  </li>
<li><strong>회고(Retrospective)</strong> - 개선할 점 논의 후 다음 스프린트 반영  </li>
</ol>
<p>📌 <strong>이 과정이 반복되면서 지속적인 개선이 이루어짐!</strong></p>
<hr>
<h4 id="애자일-vs-전통적-개발-워터폴">애자일 vs 전통적 개발 (워터폴)</h4>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>애자일 (Agile)</th>
<th>워터폴 (Waterfall)</th>
</tr>
</thead>
<tbody><tr>
<td>개발 방식</td>
<td>반복적(Iterative)</td>
<td>순차적(Sequential)</td>
</tr>
<tr>
<td>요구사항</td>
<td>계속 변경 가능</td>
<td>초기에 확정</td>
</tr>
<tr>
<td>피드백 반영</td>
<td>빠르게 반영 가능</td>
<td>다음 개발 단계까지 반영 불가</td>
</tr>
<tr>
<td>고객 참여</td>
<td>지속적인 협업</td>
<td>초기 및 최종 단계에만 참여</td>
</tr>
<tr>
<td>문서화</td>
<td>최소한의 문서</td>
<td>상세한 문서 작성 필요</td>
</tr>
</tbody></table>
<p>🔥 <strong>애자일은 유연하게 변화에 대응 가능</strong>,  
⚠️ <strong>워터폴은 계획이 확실할 때 적합</strong>  </p>
<hr>
<h4 id="애자일-개발의-장점">애자일 개발의 장점</h4>
<p>✅ <strong>빠른 피드백 반영</strong> → 고객 요구사항을 신속하게 적용 가능<br>✅ <strong>변화에 유연하게 대응</strong> → 요구사항 변경 시 큰 문제 없이 진행 가능<br>✅ <strong>지속적 개선 &amp; 품질 향상</strong> → 짧은 주기로 반복 개발하며 품질 향상  </p>
<hr>
<h3 id="최종-요약">최종 요약</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>애자일 (Agile)</strong></td>
<td>빠르고 유연한 개발 방법론</td>
</tr>
<tr>
<td><strong>애자일 핵심 원칙</strong></td>
<td>팀워크, 실행 가능 소프트웨어, 고객 협업, 유연성</td>
</tr>
<tr>
<td><strong>애자일 방법론</strong></td>
<td>Scrum, Kanban, XP, Lean</td>
</tr>
<tr>
<td><strong>애자일 장점</strong></td>
<td>빠른 피드백, 변화 대응, 지속적 개선</td>
</tr>
</tbody></table>
<h2 id="dto-data-transfer-object란">DTO (Data Transfer Object)란?</h2>
<p>DTO(Data Transfer Object) 는 데이터를 전달하기 위한 객체로,
클라이언트와 서버 또는 애플리케이션 내부에서 데이터를 효율적으로 주고받기 위해 사용됩니다.</p>
<h3 id="dto의-주요-역할">DTO의 주요 역할</h3>
<pre><code>1.    데이터 전달을 위한 객체 (Request / Response)
•    API 요청/응답 시 데이터를 정리하여 전달
2.    불필요한 데이터 노출 방지
•    민감한 정보를 숨기고 필요한 데이터만 포함 가능
3.    데이터 변환 (예: 모델 → DTO, DTO → JSON)
•    ORM 모델을 API 응답 형식으로 변환할 때 사용</code></pre><p>📌 쉽게 말하면?
➡️ “딕셔너리(dict)보다 구조화된 데이터 전송을 위해 사용하는 클래스!”</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제]]></title>
            <link>https://velog.io/@hak_0/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C</link>
            <guid>https://velog.io/@hak_0/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C</guid>
            <pubDate>Tue, 25 Feb 2025 07:38:46 GMT</pubDate>
            <description><![CDATA[<h1 id="운영체제">운영체제</h1>
<blockquote>
<ul>
<li>Operating System</li>
<li>ex) windows, android, linux</li>
<li>운영체제는 메모리를 이용해서 새로운 동작을 하고 그안에서 어떤 데이터를 다룰수 있게 인터페이스를 제공해주는 역할을 함</li>
</ul>
</blockquote>
<ul>
<li>운영체제는 플랫폼<ul>
<li>운영체제란, 컴퓨터에 포함된 CPU나 메모리, 입출력 기기 등이 사용자에 기대에 맞게 역할을 수행할 수 있도록 도와주는 창구 역할을 하는 소프트웨어</li>
<li>이러한 역할을 <code>플랫폼 소프트웨어</code>라고 부르기도 함</li>
<li>사용기기나 목적에 따라 필요한 운영체제의 유형이 다르기 때문에 운영체제의 종류가 다양함</li>
</ul>
</li>
</ul>
<h2 id="📝-운영체제의-대표적인-역할들">📝 운영체제의 대표적인 역할들</h2>
<blockquote>
<ul>
<li>프로세스 관리</li>
<li>메모리 관리 : 데이터 들을 필요한 공간에 맞게 사용</li>
<li>파일 시스템 관리</li>
</ul>
</blockquote>
<h2 id="📝-운영체제의-구조">📝 운영체제의 구조</h2>
<blockquote>
<ul>
<li>커널</li>
</ul>
</blockquote>
<ul>
<li>커널: 사용자가 직접 접근 X, 아주 핵심적이고 중요한 파트이기에<ul>
<li>사용자가 커널에 요청을 CLI(Command Line Interface)를 통해 할 수 있음</li>
<li>또는 GUI(Grapic User Interface) -&gt; 데스크톱 컴퓨터를 키면 <strong>바탕화면 같은것이 GUI</strong>임</li>
</ul>
</li>
<li>시스템 콜<ul>
<li>어플리케이션이 컴퓨터의 자원에 접근할 떄 위험성 없이 적절한 방법으로 작업을 수행 할 수 있게 해주는 어플리케이션과 커널간의 인터페이스 이다.</li>
<li>쉽게 말해서 커널에서 정해준 함수만 사용할 수 있음</li>
</ul>
</li>
<li>드라이버<ul>
<li>그래픽카드, USB, 메로리 등 다양한 외부기기와 컴퓨터를 연결했을때 정상적으로 사용되려면 운영체제가 지원을 해줘야 사용을 가능</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hak_0/post/369f59df-5409-472c-88b5-10f1e96114f5/image.png" alt=""></p>
<hr>
<h2 id="📝-컴퓨터-하드웨어-구조">📝 컴퓨터 하드웨어 구조</h2>
<p><img src="https://velog.velcdn.com/images/hak_0/post/7aff9765-b471-4a52-90fb-bb45a25413ee/image.png" alt=""></p>
<h3 id="cpucentral-processing-unit-중앙-처리-장치">CPU(Central Processing Unit): 중앙 처리 장치</h3>
<ul>
<li><strong>컴퓨터 내부에서 주요 연산을 처리하느 두뇌 역할</strong></li>
<li>ALU(Arithmetic Logic Unit, 산술 논리 연산 장치): 연산 담당 파트</li>
<li>CU(Control Unit): 연산 명령을 해석</li>
<li>Register Set: 데이터를 처리하기 위해 임시 저장 장치</li>
</ul>
<h3 id="main-memoryradom-access-memory--ram">Main Memory(Radom Access Memory : RAM)</h3>
<ul>
<li>프로그램의 실행 파일이 올라가서 실행되는 영역</li>
<li>프로세스 관리를 위해 존재하는 메모리</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>메인 메모리 (주기억장치, RAM)</th>
<th>보조 기억c 장치 (저장장치, HDD/SSD)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>역할</strong></td>
<td>실행 중인 프로그램과 데이터 저장</td>
<td>데이터 및 파일을 영구적으로 저장</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>빠름 (나노초 단위)</td>
<td>느림 (밀리초 단위)</td>
</tr>
<tr>
<td><strong>휘발성</strong></td>
<td><strong>휘발성</strong> (전원 OFF 시 데이터 삭제)</td>
<td><strong>비휘발성</strong> (전원 OFF 후에도 데이터 유지)</td>
</tr>
<tr>
<td><strong>용량</strong></td>
<td>보통 수 GB~수십 GB</td>
<td>보통 수백 GB~수 TB</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td>RAM (DDR4, DDR5 등)</td>
<td>HDD, SSD, USB, 외장 하드</td>
</tr>
</tbody></table>
<h3 id="버스-시스템">버스 시스템</h3>
<blockquote>
<p>컴퓨터를 구성하는 요소들 사이에서 데이터를 주고받기 위해 사용되는 경로</p>
</blockquote>
<ol>
<li><p>데이터 버스(Data Bus)</p>
<ul>
<li>역할: CPU와 메모리, 입출력 장치 간 데이터를 전달</li>
<li>특징: 양방향 전송 가능</li>
<li>예시: CPU가 RAM에서 데이터를 읽거나 저장할 때 사용</li>
</ul>
</li>
<li><p>주소 버스(Address Bus)</p>
<ul>
<li>역할: CPU가 메모리나 입출력 장치의 특정 주소를 지정</li>
<li>특징: 단방향 전송 (CPU → 메모리/입출력 장치)</li>
<li>예시: CPU가 특정 메모리 주소에 접근할 때 사용</li>
</ul>
</li>
<li><p>제어 버스(Control Bus)</p>
<ul>
<li>역할: CPU가 시스템의 다양한 장치들을 제어하는 신호를 전달</li>
<li>특징: 양방향 또는 단방향 전송</li>
<li>예시: 읽기(Read), 쓰기(Write), 인터럽트(Interrupt) 신호 등</li>
</ul>
</li>
</ol>
<h4 id="📌-한마디로-정리">📌 한마디로 정리!</h4>
<blockquote>
<ul>
<li>데이터 버스: 데이터 이동</li>
<li>주소 버스: 위치 지정</li>
<li>제어 버스: 명령 전달</li>
</ul>
</blockquote>
<h2 id="📝-cpu-이해">📝 CPU 이해</h2>
<ul>
<li>ALU(Arithmetic and Logic Unit, 산술 논리 연산 장치): 연산 담당 파트<ul>
<li>산술 연산</li>
<li>논리 연산</li>
<li>바이너리 형태를 이해하는 능력이 없음 그래서 CPU에 등록된 명령어를 해석하기 위해 CU가 필요함</li>
</ul>
</li>
<li>CU(Control Unit): 연산 명령을 해석, C나 JAVA같은 compile 언어로 작성한 코드를 compile하면 실행 파일이 만들어짐, 실행 파일엔 CPU에 일을 시키기 위한 명령어가 저장되어 있음</li>
<li>Register Set: 데이터를 처리하기 위해 임시 저장 장치<ul>
<li>프로그램 카운터(Program Counter): 메모리에서 가져다 실행할 다음 명령을 일시적으로 저장하는 레지스터</li>
<li>인스터럭션 레지스터(Instruction Register): 이제 해석해야할 명령어를 저장하는 레지스터</li>
<li>어드레스 레지스터(Address Register): 메모리의 주소를 저장하는 레지스터, CPU가 읽어들이고자 하는 주소값을 <strong>어드레스 버스(Address Bus)</strong> 로 보낼때 거치는 레지스트</li>
<li>버퍼 레지스터(Buffer Register): 메모리의 읽거나 쓰려는 데이터나 명령을 일시적으로 저장하는 레지스터, 데이터를 주고받는 데이터가 바로 쏴지지않고 잠시 대기하는 상태</li>
<li>플래그 레지스터(Flag Register): 연산 결과, CPU상태에 대한 부가적인 정보를 저장하는 레지스터</li>
<li>스택 포인터(Stack Pointer): 스택의 꼭대기를 가리키는 레지스터, 스택이란 지역변수나 매개변수를 저장하기위해 사용하는 메모리 영역의 이름, 다음꺼내 써야할 데이터를 알려주기위해 사용하는 레지스터</li>
<li>플래그</li>
</ul>
</li>
</ul>
<ol>
<li>CPU 명령전달</li>
<li>CU 명령 해석</li>
<li>ALU 명령 실행</li>
<li>일을 하는 와중 중 명령이 들어오면 Register에서 대기</li>
</ol>
<ul>
<li><p>클럭 펄스(Clock Pulse)</p>
<ul>
<li>시스템 동기화를 위해 존재하는 클럭 펄스</li>
<li>초당 처리할 수 있는 명령의 갯수, CPU는 그 클럭에 맞춰서 일을 진행함</li>
<li>동기 명령을 가지런히 정렬해서 처리하기 위해 클럭에 맞춰서 하는것</li>
</ul>
</li>
<li><p>즉 이러한 시스템을 매끄럽게 진행되도록 도와주는 것이 운영체제 이다.</p>
</li>
</ul>
<h2 id="📝-프로그램-실행-과정">📝 프로그램 실행 과정</h2>
<h4 id="폰-노이만-구조">폰 노이만 구조</h4>
<blockquote>
<p>CPU &lt;--&gt; 메모리 &lt;- 프로그램</p>
</blockquote>
<ul>
<li><code>프로그램 코드(인간친화적) -&gt; 어셈블리 코드(컴퓨터 친화적) -&gt; 바이너리 코드(1과 0으로 구성된 코드)</code></li>
</ul>
<ol>
<li>실행파일이 만들어짐 -&gt; 하드디스크 같은 보조메모리에 저장</li>
<li>메인 메모리에 등록</li>
<li>CPU에 의해 차레대로 실행(순차적, 스택으로 쌓여있음)<ol>
<li>Fetch: 메모리 상에 등록된 명렁어를 CPU로 가져옴(레지스트)</li>
<li>Decode: 가져다 놓은 명령어를 CPU가 해석(CU)</li>
<li>Execution: 수행(ALU)</li>
</ol>
</li>
</ol>
<h3 id="버스-시스템-1">버스 시스템</h3>
<ul>
<li>데이터 버스: 데이터 이동을 위해 필요한 버스</li>
<li>컨트롤 버스: CPU가 원하는 바를 메모리에 전달하기 위한 버스</li>
<li>어드레스 버스: 주소값을 이동하기 위해 필요한 버스</li>
</ul>
<h2 id="📝-프로그램-실행-중-돌발상황-인터럽트">📝 프로그램 실행 중 돌발상황, 인터럽트</h2>
<blockquote>
<ul>
<li>CPU가 어떤 작업을 수행하고 있을 때</li>
<li>CPU의 작업을 방해하는 신호를 가리켜 인터럽트</li>
<li>-&gt; CPU가 지금 하고있는 작업을 잠시 멈추고 우선적으로 처리해야할 작업이 생겼다라고 신호를 보내는 것</li>
</ul>
</blockquote>
<ul>
<li>정상적으로 수행할 수 없는 명령어가 입력되면, CPU는 인터럽트를 발생시킨다 -&gt; <code>예외</code> Ex) ValueError, ZeroDivisionError...</li>
<li>입출력 장치(하드웨어)로부터 발생하는 인터럽트는 <code>비동기 인터럽트</code>라고 표현 하기도 함 CPU가 발생시키는 인터럽트가 아니기에 Ex) KeyBoardInterrupt(ctrl+c) ...</li>
</ul>
<h3 id="인터럽트-핸들링">인터럽트 핸들링</h3>
<blockquote>
<ul>
<li>인터럽트가 발생하면, 인터럽트에 대응하는 무언가가 동작해야 한다. 이를 <code>인터럽트 서비스 루틴</code> 이라한다.</li>
<li>인터럽트 서비스 루틴은 이터럽트를 처리하기 위해 특정 인터럽트 신호에 대해 미리 정의되어 있는 프로그 또는 함수 이다.</li>
</ul>
</blockquote>
<pre><code class="language-python">import time
import signal

def handler(signum, frame):
    print(&quot;키보드 인터럽트 감지&quot;)
    print(&quot;신호 번호: &quot;, signum )
    print(&quot;스텍 프레임:&quot;, frame)
    exit()

signal.signal(signal.SIGINT, hanlder)

while True:
    print(&quot;5초 간격으로 출력중...&quot;)
    time.sleep(5)</code></pre>
<h2 id="📝-프로세스">📝 프로세스</h2>
<blockquote>
<ul>
<li>프로세스란 <code>실행중인 프로그램</code>을 뜻함. 운영체제는 필요한 자원을 할당하고 관리, 그렇게 시작한 프로그램은 프로세스 가 됨</li>
</ul>
</blockquote>
<h3 id="프로세스-구조">프로세스 구조</h3>
<ul>
<li>명령어가 담긴 코드 영역<ul>
<li>프로세스가 실행하는 코드 즉 명령어 집합이 저장되어 있는 영역</li>
</ul>
</li>
<li>전역변수 등이 담긴 데이터 영역</li>
<li>지역변수 등이 담긴 스택 영역<ul>
<li>휘발성 변수가 저장되어 있는 곳</li>
</ul>
</li>
<li>동적 메모리 할당을 위한 힙 영역</li>
</ul>
<h4 id="레지스터">레지스터</h4>
<blockquote>
<ul>
<li>프로그램의 실행을 위해서는 절대적으로 레지스터가 필요<ul>
<li>현재 실행중인 프로세스를 위한 데이터들로 채워짐</li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-python"># 파이썬 프로그램도 프로세스가 될수 있음
import os

# getpid: 운영체제 관리를 위한 프로세스 ID
# 프로세스에는 ID가 할당됨
print(&quot;파이썬 코드 실행 중! 실행 중인 프로세스의 아이디는 :&quot;, os.getpid())

# 내 컴퓨터 운영체제 안에서 돌아가고 있는 프로세스를 조회하는 코드
# pip install psutil

import psutil

for proc in psutil.process.iter():
    ps_name = proc.name()
    print(ps_name)
    if &quot;Chrome&quot; in ps_name :
        print(ps_name,  proc.pid)
</code></pre>
<h2 id="📝-프로세스-상태">📝 프로세스 상태</h2>
<p><img src="https://velog.velcdn.com/images/hak_0/post/8a369759-336e-4488-8cc8-3e695c1b5d76/image.png" alt=""></p>
<ul>
<li><p>CPU는 한 번에 하나의 연산밖에 수행할 수 있다.</p>
</li>
<li><p>CPU는 여러 개의 프로세스를 동시에 실행하지 않고, <strong>빠르게 번갈아 가며 실행</strong>한다.</p>
</li>
<li><p>운영체제는 빠르게 번갈아 수행되는 프로세스의 순서를 관리하고 자원을 분배해야함</p>
<ul>
<li>PCB(Process Control Block): 프로세스와 관련된 자료구조를 저장하는 구조</li>
<li>프로세스를 식별하기 위해 필요한 정보들이 저장됨</li>
<li>프로세스 ID, 레지스터 데이터, 스케줄링 정보, 그리고 상태 ...</li>
</ul>
</li>
<li><p>운영체제는 CPU가 여러개의 프로세스를 번갈아 수행할수 있게 해야함, 그래서 각 프로세스는 각 상황에 따라 상태정보를 가짐</p>
</li>
<li><p>생성 -&gt; 준비 &lt;--&gt; 실행 -&gt; 대기 -&gt; 종료, 또는 다시 준비 -&gt; 실행 -&gt; 대기 -&gt; ...종료(소멸)</p>
<ul>
<li>대기상태는 대기상태가 된 이유가 사라지고 다시 준비상태가 되어야 다시 실행될 수 있음</li>
</ul>
</li>
</ul>
<h3 id="프로세스-계층">프로세스 계층</h3>
<ul>
<li>부모 프로세스 -&gt; 자식 프로세스 (부모 프로세스에 의해 생성된 프로세스): 독릭적이며 자신만의 ID를 가지고 있음</li>
</ul>
<pre><code class="language-python"># 계층을 확인하는 코드
import psutil

for proc in psutil.process_iter():
    ps_name = proc.name()

    if &quot;Chrome&quot; in ps_name :
        child = proc.children()
        print(ps_name, proc.status(), proc.parent(), child)

        if child :
            print(f&quot;{ps_name}의 자식 프로세스&quot;, child)</code></pre>
<h2 id="📝-컨텍스트-스위칭">📝 컨텍스트 스위칭</h2>
<p><img src="https://velog.velcdn.com/images/hak_0/post/af6bd7e3-2aa4-4e37-9315-5be39259229f/image.png" alt=""></p>
<h3 id="멀티-프로세스-운영체제">멀티 프로세스 운영체제</h3>
<blockquote>
<ul>
<li>여러개의 프로세스를 관리해가며, 마치 동시에 실행되는 것처럼 운영할 수 있는 운영체제</li>
<li>운영체제가 실행 중일때 다른 프로세스를 여러개를 실행한다는 의미임</li>
<li>운영체제에 매우 부담되는 작업</li>
</ul>
</blockquote>
<h3 id="컨텍스트-스위칭">컨텍스트 스위칭</h3>
<blockquote>
<ul>
<li>프로세스를 실행하는 중에 다른 프로세스를 실행 하기 위해 실행중인 프로세스의 상태를 저장하고 다른 프로세스의 데이터 상태로 교체하는 것을 컨텍스트 스위칭 이라함</li>
<li><code>문맥 교환</code></li>
</ul>
</blockquote>
<ul>
<li>현재 실행중인 상태의 레지스터를 다 저장해서 프로세스로 넣어 준비상태의 프로세스를 만들고 백업 해둠</li>
<li>그리고 실행중인 상태의 프로세스와 번갈아 가면서 사용하는 것임</li>
</ul>
<h2 id="프로세스-생성">프로세스 생성</h2>
<blockquote>
<ul>
<li>프로그램 실행 시, 운영체제는 코드 영역과 데이터 영역을 메인 메모리에 올리고 빈 스택과 빈 힙을 만들어 공간을 확보한다. 이는 시스템에게 상당한 부담을 주는 일<ul>
<li>새로운 프로세슽를 생성하는 것보다, 기존 프로세스를 복사하는 것이 빠름, 따라서 모든 프로세스는 최초의 프로세스로부터 복사된다 -&gt; 부모 프로세스 로부터 자식 프로세스가 생성</li>
<li>부모 프로세스를 복사하는 시스템 함수를 <code>fork()</code> 함수라 함</li>
<li><code>fork()</code>를 통해 만들어진 함수를 <code>exec()</code> 시스템 함수를 이용해 코드 영역과 데이터 영역을 원하는 내용으로 덮어쓰기 위한 작업을 해줘야함</li>
<li>그러면 부모 프로세스와 다른 프로세스가 만들어지고 동작하게 됨</li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-python"># 멀티 프로세싱을 위한 작업

from multiprocessing import Process
import os

def func():
    print(&quot;안녕, 함수야!&quot;)
    print(&quot;나의 프로세스 아이디: &quot;, os.getpid())
    pirnt(&quot;나의 부모 프로세스 아이디:&quot;, os.getppid()) # 부모 프로세스

if __name__ == &quot;__main__&quot; :
    print(&quot;앱 프로세스 아이디 : &quot;, os.getpid())
    child1 = Process(target=func).start()
    child1 = Process(target=func).start()
    child1 = Process(target=func).start()
# 부모 프로세스는 전부 같은 값이 나올것이고 자식은 다르레 나옴
# 만약 함수를 여러개 만들어서 다른것을 출력하더라도 아이디 값은 다르게 나오겟지만
# 한 모듈에서 파생되어서 나오는 것이니 부모 프로세스는 동일하게 나온다</code></pre>
<h2 id="📝-스레드의-이해">📝 스레드의 이해</h2>
<blockquote>
<ul>
<li>컨텍스트 스위칭으로 인한 부하를 줄이기 위한 답은 프로세스를 줄이는 것이다.</li>
<li>프로세스는 슬하에 스레드를 두어 작업을 처리할 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>모든영역을 공유하지만 스택 영역많은 공유하지 않는다.(지역변수, 매개변수를 저장하는 영역)<ul>
<li>작업마다 따로 가지지않으면 제대로 작업이 처리되지 않음 -&gt; 개별적 쓰레드 할당</li>
<li>스택은 함수 호출시 전달되는 인자, 되돌아갈 주소값, 및 지역변수등을 저장하는 메모리 함수이다. 그 구조조차 스택의 구조이다, 따라서 메모리 공간이 독립적이지 않으면 추가적인 실행 흐름을 만들수 없음</li>
</ul>
</li>
<li>처리할 작업이 많아져도 프로세스를 늘리는 것이 아닌 스레드를 늘려 효율적으로 처리 가능</li>
<li>스레드도 개별적으로 구분이 되어야 하기에 개별 ID가 존재</li>
</ul>
<h3 id="메모리-구조-관점에서-본-스레드-특징">메모리 구조 관점에서 본 스레드 특징</h3>
<ul>
<li>스레드는 스레드가 하나 생성될 때마다 스레드를 위한 스택영역이 추가로 생성될 뿐, 그 이외의 영역은 프로세스 영역을 공유</li>
<li>스레드는 프로세스가 처리해야 할 작업을 수행하기 위해 존재하는 것이므로, 코드 영역을 공유해 명령어에 접근할 수 있어야 한다.</li>
<li>명령어 실행 시 전역변수, 정적변수, 지역변수 등의 데이터에 접근해야 하므로 데이터 영역과 힙 영역과 공유해야 한다.</li>
</ul>
<pre><code class="language-python"># 하위 쓰레드 만들기
import threading
import os

def func():
    print(&quot;안녕, 함수야!&quot;)
    print(&quot;나의 프로세스 아이디: &quot;, os.getpid())
    print(&quot;스레드 아이디:&quot;, threading.get_native_id())

if __name__==&quot;__main__&quot; :
    print(&quot;기존 프로세스 아이디 :&quot;, os.getpid())
    thread1 = threading.Thread(target=func) # target에 맡길 작업 투척
    thread1.start()

---
import threading
import os
import time

def something(word):
    while True:
        print(word)
        time.sleep(3)

if __name__==&quot;__main__&quot; :
    print(&quot;기존 프로세스 아이디 :&quot;, os.getpid())
    t = threading.Thread(target=something, args=(&quot;happy&quot;,))
    t.daemon = True # 메인 기능이 끝나면 같이 끝나게 만드는 기능(자기의 메인)
    t.start()
    print(&quot;메인 스레드에서 반복문 시작&quot;)
    while True:
        try:
            print(&quot;daily...&#39;)
            time.sleep(1)
        except KeyBoardInterrupt:
            print(&quot;goodbye&quot;)
            break

# daily의 메인 쓰레드 안에서 something의 하위스레드가 실행되는것(3초) 메인은 1초</code></pre>
<h2 id="cpu-스케줄링">CPU 스케줄링</h2>
<blockquote>
<p>CPU는 여러개의 프로세스를 빠르게 번갈아 실행하기 위해, 각프로세스를 위해 일하는 시간을 조금씩 나누어 배분한다</p>
</blockquote>
<ul>
<li><p>프로세스 스케줄링: 프로세스의 CPU 점유 순서 및 방법을 결정짓는 일을 가르킴, 알고리즘을 통해 분류</p>
</li>
<li><p>프로세스 우선순위</p>
<ul>
<li>프로세스 별로 요구하는 자원이 상이하기 때문에, 운영체제(스케줄러)가 모든 프로세스에 동일한 빈도로 CPU를 할당한느 것은 비합리적임</li>
<li>운영체제는 프로세스마다 우선순위를 부여하고 이를 기준으로 프로세스를 실행한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-python"># ps -el | grep python: mac에서 가능한 현재 돌아가고 있는 프로세스 를 볼수있음(파이썬 이름을 가진)
</code></pre>
<h2 id="📝-스케줄링-알고리즘">📝 스케줄링 알고리즘</h2>
<h3 id="queue">Queue</h3>
<blockquote>
<p>운영체제는 준비 상태의 프로세스와 대기 상태의 프로세스를 관리하기 위해 큐(Queue) 자료구조를 사용</p>
</blockquote>
<ul>
<li><p>준비 Q: CPU를 이용하고 싶은 프로세스들이 줄을 서기 위해 존재하는 Q</p>
</li>
<li><p>대기 Q: 입출력 장치를 이용하기 위해 대기상태에 있는 프로세스들이 줄을서기 위해 존재</p>
</li>
<li><p>Q: 프로세스의 정보를 가지고 있는 프로세스 컨트롤 블록들이 줄을서고 운영체제는 줄을 서 있는 컨트롤 블록을 참고해서 어떤 프로세스를 실행할지 결정</p>
</li>
</ul>
<h3 id="스케줄링-알고리즘시-고려사항">스케줄링 알고리즘시 고려사항</h3>
<blockquote>
<ul>
<li>부하가 최소화되어야 함</li>
<li>컴퓨팅 자원을 효율적으로 사용해야 함</li>
<li>균형잡힌 스케줄링을 해야 함</li>
<li>대기 및 응답 시간이 너무 길어서는 안됨</li>
</ul>
</blockquote>
<ul>
<li><strong>선입선출(First In First Out)</strong><ul>
<li>단순하고 직관적이지만 실행시간이 짧은 프로세스가 실행 시간이 긴 프로세스 뒤에서 한참 기다리는 상황이 벌어질 수 있음,</li>
<li>(우선순위가 같다면)순서대로 작업하는 것</li>
</ul>
</li>
<li><strong>최단 작업 우선(Shortest Job First)</strong><ul>
<li>프로세스의 실행 시간을 정확히 예측할수 없다는 문제가 있어 현실적이지 않음</li>
<li>(우선순위가 같다면)실행시간이 짧은 것부터 실행</li>
</ul>
</li>
<li><strong>라운드 로빈(Round Robin)</strong><ul>
<li>(우선순위가 같다면)정해진 시간 만큼만 CPU를 점유하고, 시간이 다지나면 컨텍스트 스위칭하는 방식</li>
</ul>
</li>
<li><strong>우선순위 스케줄링(Priority Scheduling)</strong><ul>
<li>앞선 알고리즘의 기반이 되는 아이디어, 우선순위만 고려할 경우 우선순위가 낮은 프로세스가 배제되어 버리는 &#39;기아&#39; 상태에 빠질 수 있음</li>
<li>각 프로세스마다 우선순위를 부여해서 우선순위를 먼저 처리하는 과정</li>
</ul>
</li>
</ul>
<h2 id="📝-프로세스-간-통신">📝 프로세스 간 통신</h2>
<blockquote>
<ul>
<li>프로세스는 독립적으로 실행되지만, 필요시 다른 프로세스와 데이터를 주고받으며 통신하는 경우도 있음</li>
<li>이를 가르켜 <strong>IPC(Inter-Process Coummunication)</strong>라 한다</li>
</ul>
</blockquote>
<h4 id="통신을-하려면-통신의-각주체가-만날-수-있는-매개체가-필요">통신을 하려면, 통신의 각주체가 만날 수 있는 매개체가 필요</h4>
<ul>
<li>기본적으로 프로세스는 독립적이기에 서로 통신을 할 수가 없음</li>
</ul>
<ul>
<li>메일슬롯 방식(IPC)(프로세스 간에 관계가 없는 관계에서)<ul>
<li>데이터를 받기 위해서 프로세스가 우체통 역할을 하는 객체를 마련하고 이를 통해 데이터를 주고 받는 방식, 단방향이기에 각각 만들어야 함</li>
</ul>
</li>
<li>파이프 방식(IPC)(관계가 있는 파일)<ul>
<li>익명파이프 또는 네임드 파이프를 통해 데이터를 주고 받는다</li>
<li>익명파이프는 서로 관계가 있는 프로세스 간에 통신을 할떄 사용하는 단방향 파이프</li>
<li>네임드 파이프는 프로세스 간에 양방향 통신을 할떄 사용하는 파이프</li>
</ul>
</li>
</ul>
<pre><code class="language-python"># 파이프 기반 통신
from multiprocessing import Process, Pipe
import os

def send(conn):
    print(f&quot;{os.getpid()}가 {os.getppid()}에게 데이터를 보낸다&quot;)
    conn.send(&quot;Hello Parent!&quot;) # 부모 프로세스에게 메시지 전송
    conn.close() # 파이프 닫기

if __name__==&quot;__main__&quot;:
    parent, child = Pipe() # 양방향 통신이 가능한 파이프 생성
    p = Process(target=send, args=(child,))
    p.start()
    print(&quot;기존 프로세스 아이디:&quot;, os.getpid())# 부모 프로세스 ID 출력
    print(parent.recv()) # 자식 프로세스로부터 데이터 받기

    p.join() # 프로세스가 작업을 종료할떄까지 기다림

# 기존프로세스 아이디:
# 자식이 부모에게 보낸다
# hello parent # recv 에서 받은 것

# 실행 흐름
1. 부모 프로세스(main)가 실행되며 Pipe()를 생성
2. 새로운 프로세스(send 함수)가 실행되면서 자식 프로세스가 생성됨
3. 자식 프로세스가 부모에게 데이터 전송 (conn.send())
4. 부모 프로세스가 데이터를 수신 (parent.recv())
5. 자식 프로세스 종료 후 p.join()을 통해 대기 후 종료

--- 추가예제
from multiprocessing import Process
import os 
import time

def writer() :
    print(f&quot;{os.getpid()}가 파일에 쓴다&quot;)
    with open(&quot;13.txt&quot;, &quot;w&quot;) as f: # 파이썬의 w는 생성도 포함 없으면 알아서 파일만듬
        f.write(&quot;hello&quot;)

def reader() :
    print(f&quot;{os.getpid()}가 파일을 읽는다&quot;)
    with open(&quot;13.txt&quot;, &quot;r&quot;) as f: 
        print(f.read())

if __name__ == &quot;__main__&quot;:
    p1 = Process(target=writer) # 읽을게 있어야 읽으니까 writer먼저
    p1.start()

    time.sleep(2) # 딜레이를 줌으로써 쓰는중에 접근할 수 없게

    p2 = Process(target=reader)
    p2.start()

    p1.join()
    p2.join()

# 7756가 파일에 쓴다
# 7759가 파일을 읽는다
# HELLO</code></pre>
<h2 id="📝-동기화">📝 동기화</h2>
<blockquote>
<ul>
<li>프로세스들은 서로 독립적이지만, 프로세스 간 통신을 하거나 같은 대상에 대한 작업을 함으로써 협력할 수 있다. 그런데 이때, 동시다발적으로 작업을 처리하면 문제가 발생한다.</li>
<li>한 파일을 두고 같이 작업을 시행하려 할때 문제 발생</li>
<li>프로세스 + 스레드 둘다 적용</li>
</ul>
</blockquote>
<ul>
<li>공유 자원: 프로세스 간 통신에서 공동으로 이용하는 변수가 파일, 입출력 기기등이 존재<ul>
<li>각 프로세스의 접근 순서에 따라 결과가 달라질 수 있는데, 프로세스가 동시에 실행할 경우 문제가 발생할 수 있는 영역을 <code>임계구역</code>이라 한다.</li>
</ul>
</li>
</ul>
<h3 id="상호배제">상호배제</h3>
<blockquote>
<ul>
<li>동기화 기법은 임계구역에서 발생할 수 있는 문제를 해결하기 위한 기법</li>
<li>동기화 기법 구현 시에는 <code>상호배제</code>라는 조건을 만족시켜야 한다.</li>
</ul>
</blockquote>
<ul>
<li>상호배제란 하나의 프로세스가 임계구역에 들어갔다면 다른 프로세스는 임계구역에 들어갈 수 없다는 조건</li>
</ul>
<h3 id="뮤텍스-락">뮤텍스 락</h3>
<blockquote>
<ul>
<li>동시에 접근해서는 안되는 자원에 동시접근 못하도록 막는 동기화 도구</li>
<li>임계구역에 접근하는 프로세스는 뮤텍스 락을 통해 다른 프로세스에서 접근하지 못하도록 막을수 있음</li>
<li><strong>공유 자원이 하나인 경우</strong></li>
<li><code>acquire()</code>: 진입 시 문을 잠그는 함수</li>
<li><code>release()</code>: 다쓰고 나올떄 문잠금을 해제하는 함수</li>
</ul>
</blockquote>
<pre><code class="language-python">from multiprocessing import Process, Value # 프로세스간의 값을 공유하게 만들고 싶을때 사용하는 파이썬 생성자 함수

def counter1(share_num, cnt):
    for i in range(cnt):
        share_num.value += 1

def counter2(share_num, cnt):
    for i in range(cnt):
        snum.value -= 1

if __name__==&quot;__main__&quot;:
    shared_number = Value(&#39;i&#39;, 0)
    p1 = Process(target=counter1, args=(shared_number,5000))
    p1.start()

    p2 = Process(target=counter2, args=(shared_number,5000))
    p2.start()

    p1.join()
    p2.join()

    print(&quot;final num is:&quot;, shared_num.value)

# 0이 안됨 공유자원이여서 값이 꼬여버림

# 뮤텍스 락을 설정한후 실행
from multiprocessing import Process, Value, Lock # 뮤텍스 락 역할

def counter1(share_num, cnt, lock):
    lock.acquire() # 문잠금
    try :
        for i in range(cnt):
            share_num.value += 1
    finally :
        lock.release()

def counter2(share_num, cnt, lock):
    lock.acquire()
    try:
        for i in range(cnt):
            snum.value -= 1
    finally:
        lock.release()

if __name__==&quot;__main__&quot;:

    lock = Lock()1
    shared_number = Value(&#39;i&#39;, 0)
    p1 = Process(target=counter1, args=(shared_number,5000), lock)
    p1.start()

    p2 = Process(target=counter2, args=(shared_number,5000), lock )
    p2.start()

    p1.join()
    p2.join()

    print(&quot;finall num is:&quot;, shared_num.value)

# 0으로 잘 나온다.
</code></pre>
<h3 id="세마포어key라-생각">세마포어(key라 생각)</h3>
<blockquote>
<ul>
<li><strong>공유자원이 여러개</strong></li>
<li>공유자원에 대하여 세마포어 즉 키를 전달해서 공유자원을 못쓰도록 막는것</li>
<li><code>wait()</code> : 프로세스가 임계구역에 들어갈 수 있는지 기다려야 하는지 알려주는 용도</li>
<li><code>signal()</code> : 임계구역 함수에서 기다리고 있는 함수에서 이제 들어가도 된다고 신호를 보내는 함수</li>
</ul>
</blockquote>
<h4 id="쓰레드-동기화-예시">쓰레드 동기화 예시</h4>
<pre><code class="language-python">import threading
from multiprocessing import Value, Lock # 뮤텍스 락 역할

def counter1(share_num, cnt, lock):
    lock.acquire() # 문잠금
    try :
        for i in range(cnt):
            share_num.value += 1
    finally :
        lock.release()

def counter2(share_num, cnt, lock):
    lock.acquire()
    try:
        for i in range(cnt):
            snum.value -= 1
    finally:
        lock.release()

if __name__==&quot;__main__&quot;:

    lock = Lock()1
    shared_number = Value(&#39;i&#39;, 0)
    t1 = threading.Thread(target=counter1, args=(shared_number,5000), lock)
    t1.start()

    t2 = threading.Thread(target=counter2, args=(shared_number,5000), lock )
    t2.start()

    t1.join()
    t2.join()

    print(&quot;finall num is:&quot;, shared_num.value)

# 0으로 잘 나온다.
</code></pre>
<h2 id="📝-데드락deadlock">📝 데드락(Deadlock)</h2>
<blockquote>
<ul>
<li>기다리는 대상이 꼬리에 꼬리를 물고 이어져서 아무도 작업을 진행하지 못하는 상황을 데드락이라 한다.</li>
<li>공유자원으로 인해 발생</li>
</ul>
</blockquote>
<ul>
<li>데드락 발생조건 4가지 중 모두 만족해야 발생 가능성이 생김<ol>
<li>상호배제(Mutual exclusion) : 공유자원 같이 사용할 수 없어야 한다.</li>
<li>비선점(No preemption) : 공유자원을 가지고 있을때 빼았아서 사용할 수 없어야 한다</li>
<li>점유 및 대기(Hold and wait) : 쓰고있을 경우 사용하기 위해 기다리고 있어야 한다.</li>
<li><strong>원형 대기(Circular waite)</strong> : 꼬리에 꼬리를 물어 둥글게 형성되어 있어야 한다.</li>
</ol>
</li>
</ul>
<h3 id="식사하는-철학자-문제">식사하는 철학자 문제</h3>
<h4 id="📌-문제-개요">📌 문제 개요</h4>
<ul>
<li><strong>N명의 철학자</strong>가 원형 테이블에 앉아 있음.</li>
<li>각 철학자는 <strong>생각(Thinking)하거나 식사(Eating)</strong> 함.</li>
<li><strong>N개의 포크(Fork)</strong>가 놓여 있으며, 각 철학자는 왼쪽과 오른쪽의 포크를 사용해야 식사할 수 있음.</li>
<li><strong>문제:</strong> 모든 철학자가 <strong>교착 상태(Deadlock)나 기아 상태(Starvation)</strong> 없이 원활하게 식사할 수 있도록 동기화해야 함.</li>
</ul>
<h4 id="📌-문제-발생-가능성">📌 문제 발생 가능성</h4>
<ul>
<li><p><strong>경쟁 조건(Race Condition)</strong>  </p>
<ul>
<li>여러 철학자가 동시에 포크를 집으려고 하면 <strong>충돌</strong>이 발생할 수 있음.  </li>
</ul>
</li>
<li><p><strong>교착 상태(Deadlock)</strong>  </p>
<ul>
<li>모든 철학자가 <strong>왼쪽 포크만 집고 오른쪽 포크를 기다리는 상황</strong>이 발생하면 <strong>영원히 대기</strong>하게 됨.  </li>
</ul>
</li>
<li><p><strong>기아 상태(Starvation)</strong>  </p>
<ul>
<li>일부 철학자가 계속해서 포크를 획득하지 못하면 <strong>식사를 못하고 계속 굶는 상태</strong>가 됨.</li>
</ul>
</li>
</ul>
<h4 id="📌-해결-방법">📌 해결 방법</h4>
<h5 id="1-순서-기반-해결법-홀수-짝수-철학자"><strong>1. 순서 기반 해결법 (홀수-짝수 철학자)</strong></h5>
<ul>
<li><strong>짝수 번호 철학자</strong>: <strong>왼쪽 → 오른쪽 포크 순서로 집음</strong>  </li>
<li><strong>홀수 번호 철학자</strong>: <strong>오른쪽 → 왼쪽 포크 순서로 집음</strong><br>📌 <strong>장점</strong>: 교착 상태(Deadlock) 방지 가능</li>
</ul>
<h5 id="2-웨이터butler-알고리즘"><strong>2. 웨이터(Butler) 알고리즘</strong></h5>
<ul>
<li><strong>중앙 관리자(웨이터, Mutex)</strong>가 철학자들에게 <strong>최대 (N-1)개까지만 포크를 허용</strong>  </li>
<li>철학자가 식사하려면 웨이터에게 <strong>허가 요청</strong>을 해야 함<br>📌 <strong>장점</strong>: Deadlock 예방 및 자원 사용 관리 가능</li>
</ul>
<h5 id="3-개별-철학자-스레드--세마포어"><strong>3. 개별 철학자 스레드 + 세마포어</strong></h5>
<ul>
<li>각 철학자 스레드가 <strong>세마포어(Semaphore)</strong>를 사용하여 포크를 집도록 제한  </li>
<li><strong>두 개의 세마포어</strong>를 확보한 철학자만 식사 가능</li>
</ul>
<h4 id="📌-파이썬-코드-예제-semaphore-사용">📌 파이썬 코드 예제 (Semaphore 사용)</h4>
<pre><code class="language-python">import threading
import time

N = 5  # 철학자 수
forks = [threading.Semaphore(1) for _ in range(N)]  # 포크 세마포어 배열

def philosopher(i):
    left = i
    right = (i + 1) % N

    while True:
        print(f&quot;철학자 {i} 생각 중...&quot;)
        time.sleep(1)

        # 포크 집기
        forks[left].acquire()  # 왼쪽 포크 획득
        forks[right].acquire()  # 오른쪽 포크 획득

        print(f&quot;🍽️ 철학자 {i} 식사 중...&quot;)
        time.sleep(2)

        # 포크 내려놓기
        forks[left].release()
        forks[right].release()

        print(f&quot;철학자 {i} 식사 완료. 다시 생각 시작...&quot;)

# 철학자 스레드 생성 및 실행
threads = []
for i in range(N):
    t = threading.Thread(target=philosopher, args=(i,))
    threads.append(t)
    t.start()

# 모든 스레드가 종료될 때까지 대기
for t in threads:
    t.join()</code></pre>
<h4 id="📌-코드-설명">📌 코드 설명</h4>
<ol>
<li><strong><code>threading.Semaphore(1)</code></strong>: 각 포크를 <strong>이진 세마포어</strong>로 설정 (한 번에 한 철학자만 사용 가능)  </li>
<li><strong>각 철학자는 왼쪽과 오른쪽 포크를 집고 식사</strong>  </li>
<li><strong>식사가 끝나면 포크를 놓고 다시 생각 상태로 전환</strong>  </li>
<li><strong>스레드 기반 실행</strong>을 통해 병렬적으로 실행</li>
</ol>
<h4 id="📌-해결된-문제점">📌 해결된 문제점</h4>
<ul>
<li><p><strong>Deadlock 방지</strong>  </p>
<ul>
<li>두 개의 포크를 동시에 획득한 경우만 식사가 가능하여 <strong>교착 상태 예방</strong>  </li>
</ul>
</li>
<li><p><strong>Starvation 해결 가능</strong>  </p>
<ul>
<li>웨이터 알고리즘 등 추가 적용 가능  </li>
</ul>
</li>
<li><p><strong>경쟁 조건 방지</strong>  </p>
<ul>
<li><code>Semaphore.acquire()</code>를 사용하여 한 번에 하나의 철학자만 포크를 획득</li>
</ul>
</li>
</ul>
<h4 id="📌-결론">📌 결론</h4>
<ul>
<li><strong>식사하는 철학자 문제는 동기화(Synchronization)의 대표적인 문제</strong>  </li>
<li><strong>세마포어, 웨이터 알고리즘 등을 활용하여 Deadlock과 Starvation을 방지 가능</strong>  </li>
<li><strong>멀티스레딩 환경에서 공유 자원(포크) 관리 방법을 학습하는 데 중요한 개념</strong> </li>
</ul>
<h2 id="📝-메모리">📝 메모리</h2>
<ul>
<li>레지스터<ul>
<li>가장 빠른 대신 용량이 적음</li>
<li>휘발성</li>
</ul>
</li>
<li>캐시(L1/L2)<ul>
<li>CPU와 RAM사이에서 중간 저장소</li>
<li>CPU에 가장 근접해 있는 메모리</li>
<li>레지스터와 RAM사이에 데이터 이동이 있을때 속도를 보완하는 역할을 하는 메모리</li>
<li>CPU 입장에선 바로바로 데이터를 꺼내어 쓸쑤 있는 저장소 같은 느낌</li>
<li>휘발성</li>
</ul>
</li>
<li>메인 메모리(RAM)<ul>
<li>운영체제와 프로세서 들이 올라가는 공간</li>
<li>실행중인 프로그램을 올리기 위해 주로 사용</li>
<li>휘발성</li>
</ul>
</li>
<li>보조기억장치(HDD/SSD)<ul>
<li>크고작은 파일들을 저장하기 위한 용도</li>
<li>속도는 느리지마 용량은 큼</li>
<li>비휘발성</li>
</ul>
</li>
</ul>
<h3 id="메모리-관리-방식">메모리 관리 방식</h3>
<h4 id="가변-분할-방식">가변 분할 방식</h4>
<blockquote>
<ul>
<li>프로세스의 크기에 따라 가변적으로 메모리 할당</li>
<li>연속메모리 할당</li>
<li>segmentation</li>
</ul>
</blockquote>
<ul>
<li><strong>스와핑</strong><ul>
<li>실행 상태의 프로세스는 메인 메모리에 올리고, 실행 상태가 아닌 프로세스들은 보조 장치에 따로 마련된 스왑 영역에 올린 다음 프로세스의 상태 변화에 따라 두공간 사이에서 프로세스가 이동하는것을 가리켜 스와핑 이라한다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hak_0/post/f0314128-1455-4502-94ca-717ab8ce49ec/image.png" alt=""></p>
<ul>
<li><strong>외부 단편화</strong><ul>
<li>프로세스를 메모리에 연속 배치하는 작업을 하다보면 메모리가 낭비되는 &#39;외부 단편화&#39; 상황이 발생하기도 함</li>
<li>기존의 메모리 들의 작업이 끝나고 분할되어 있는 공간은 충분하지만 쪼개어져 있어 외부의 큰 메모리를 요하는 작업을 실행할 수가 없을때 외부 단편화라 한다.</li>
<li><strong>조각 모음</strong> : 현재 실행중인 작업을 중단하고 빈공간을 모아서 다시 실행 하는 것을 의미함, 시스템에 큰 부하를 주어 빈번하게 사용하면 안됨</li>
</ul>
</li>
</ul>
<h4 id="garbage-collection-예제">Garbage Collection 예제</h4>
<pre><code class="language-python"># 문자열 객체를 변수 my_name이 참조했다
# 레퍼런스 카운트가 1인 상태
my_name = &quot;Gookhee&quot;

# 레퍼런스 카운트가 2인 상태 : 국희라는 이름을 참조할수 있는 변수가 2개란 뜻
your_name = my_name

my_name = 1 # 레퍼런스 카운트 1
your_name = 2 # 레퍼런스 카운트 0

# 레퍼런스 카운트가 0이된 것을 제거 대상이라 생각한다
# 이 것을 Garbage Collection 이라 생각한다 : 소멸 대상으로 등록</code></pre>
<pre><code class="language-js">let players ={
    boys :{
        Bregkamp: &quot;Striker&quot;
    }
}

let persons = players

players = [&quot;Son&quot;, &quot;Park&quot;]

let human = persons.boys

persons = &quot;persons&quot;

human = null // 비로소 레퍼런스 카운트 0 -&gt; 소멸대상 -&gt; 시스템의 여유가 생길때 삭제</code></pre>
<h4 id="고정-분할-방식">고정 분할 방식</h4>
<blockquote>
<ul>
<li>메모리를 고정된 크기로 나누어 놓고 프로세스마다 필요한 칸만큼 할당</li>
<li>비연속 메모리 할당</li>
<li>paging</li>
</ul>
</blockquote>
<ul>
<li><p>외부 단편화</p>
<blockquote>
<p>프로세스를 메모리에 연속 배치하는 작업을 하다보면 메모리가 낭비되는 &#39;외부 단편화&#39; 상황이 발생하기도 함</p>
</blockquote>
</li>
<li><p>프로세스가 공간을 차지고 있다 작업이 종료되지만 공간이 쪼개</p>
</li>
</ul>
<h2 id="📝-가상-메모리-관리">📝 가상 메모리 관리</h2>
<blockquote>
<p>작은 메인 메모리(RAM)이 프로세스를 실행한다?, 그렇다면 실행할 프로세스가 메인 메모리 보다 덩치가 큰경우엔 스와핑을 활용해 사용</p>
</blockquote>
<h3 id="페이징">페이징</h3>
<blockquote>
<ul>
<li>고정 분할 방식</li>
<li>페이징 기법에서는 메모리 공간을 일정한 크기의 페이지로 나누어 다룬다.</li>
<li>페이지 안에서 빈공간이 남는 <strong>내부 단편화</strong>가 발생하는 문제</li>
</ul>
</blockquote>
<ul>
<li><p>논리 주소 공간</p>
<ul>
<li>사용자와 프로세스가 참조하는 공간</li>
<li>실제 메모리보다 더 큰 공간</li>
<li>보조 기억장치의 크기를 지원받아서 크기를 키운것</li>
</ul>
</li>
<li><p>물리 주소 공간</p>
<ul>
<li>실제 메모리의 공간</li>
</ul>
</li>
<li><p>CPU가 논리주소를 통해 메모리에 접근하고 이를 물리주소로 변환하여 실제메모리에 올리는 것으로 프로세스를 실행 함</p>
</li>
<li><p>페이지 테이블</p>
<ul>
<li>페이지 번호와 프레임 번호를 짝지어 주는 테이블</li>
<li>CPU내에는 각프로세스 테이블이 적재된 주소를 가리키는 페이지 테이블 베이스 레지스터가 있음</li>
</ul>
</li>
<li><p>페이지 테이블 엔트리</p>
<ul>
<li>접근비트: 페이지가 메모리에 올라온 후 데이터에 접근이 있었는지, 1은 접근이 있었다 반대는 0</li>
<li>변경비트: 페이지가 메모리에 올라온 후 데이터의 변경이 있었는지, 1은 접근이 있었다 반대는 0</li>
<li>유효비트: 페이지가 어디에 있는지, 0이라면 스왑영역, 1이라면 물리영역에 있다. -&gt; 메모리에 올라가 있지 않은 영역 즉 스왑영역에 접근하려 한다면 페이지폴트 라는 예외가 발생한다 -&gt; 다른 처리가 필요</li>
<li>보호비트: 페이지에 대한 읽기, 쓰기, 실행 권한이 어떻게 되는지</li>
</ul>
</li>
<li><p>페이지드 세그멘테이션</p>
<ul>
<li>가변 분할 방식과 고정 분할 방식을 혼합한 가상 메모리 관리 방식</li>
<li>두 방식의 단점을 보완 - 세그먼트를 다시 페이지로 나누어 관리</li>
</ul>
</li>
</ul>
<pre><code class="language-python">foods =[&quot;햄버거&quot;, &quot;샐러드&quot;, &quot;비스킷&quot;]

print(id(foods)) # 논리 메모리 주소 확인

mv = memoryview(b&quot;happy day&quot;) # b: 바이트 형식으로 저장하겠다.

print(mv)

print(mv[0])
print(mv[1])
print(mv[2]) # 같은 p니까 같은 값 도출
print(mv[3])

# 메모리 사용 현황

import psutil
import os

print(&quot;메모리 사용량 조회하기&quot;)

memory_dict = dict(psutil.virtual_memory()._asdict()) # 시스템 사용량에 대해 튜플형식으로 반환해줌
print(memory_dict)

total = memory_dict[&quot;total&quot;] # 메모리 총량
availabe = memory_dict[&quot;available&quot;] # 메모리 즉시 제공 가능량
percent = memory_dict[&quot;percent&quot;] # 메모리 사용률

pid = os.getpid()
current_process = psutil.Process(pid)

kb = current_process.memory_info()[0] / 2 ** 20 # 메모리 사용량

---

import tracemalloc # 메모리가 어떻게 할당되는지 추적하는 것, python 3.4 ~~

tracemalloc.start()

# 메모리 할당이 진행되는 작업 아무거나 
data = [b&#39;%d&#39; % num for num in range(1, 10001)]
joined_data = b&#39; &#39;.join(data)

current, peak = tracemalloc.get_traced_memory()
print(f&quot;현재 메모리 사용량 : {current / 10 ** 6} MB&quot;)
print(f&quot;최대 메모리 사용량 : {peak / 10 ** 6} MB&quot;)

tracemalloc.stop()

tracemalloc.get_tracemalloc_memory() # 트레이스 말록 사용하느라 사용한 메모리 확인
print(traced / 10 ** 6)</code></pre>
<h2 id="📝-페이지-교체">📝 페이지 교체</h2>
<h3 id="요구-페이징">요구 페이징</h3>
<blockquote>
<ul>
<li>CPU가 특정 페이지에 접근하는 명령어를 실행 햇을때, 해당 페이지가 스왑 영역에 있어서 당장 실행시킬 수 없는 상태일 경우에는 <code>&#39;페이지 폴트&#39;</code> 예외가 발생한다.</li>
<li>페이지 폴트 예외가 발생하면 스와핑 작업이 먼저 진행된 후에 프로세스가 실행된다.</li>
<li>실행할 모든 프로세스를 메모리에 올려두는 것은 시스템에 부담이 될 수 있는 만큼 당장 필요한 페이지만을 메모리에 우선 적재하는 방법을 가리켜 <code>&#39;요구페이징&#39;</code> 이라 한다</li>
</ul>
</blockquote>
<ul>
<li>페이지 폴트시 -&gt; 페이지 교체 정책 대표적인 3가지<ul>
<li>선입선출: 가장 오래된 페이지 교체</li>
<li>최적 페이지 교체: 자주 사용될 예정인 페이지를 내버려두고 사용이 거의 안될 예정인 페이지를 교체, 실제로 구현하기 어려움 예상이 어려워서</li>
<li>LRU(Least Recently Used): 최근 사용빈도가 가장 적은것을 교체하는 것, 구현하기 쉽고 성능도 좋음</li>
</ul>
</li>
</ul>
<pre><code class="language-python"># 선입선출 하나 만들어보기 운영체제라 생각하고
class PageReplacementFIFO:
    def __init__(self, capacity):
        self.capaciry = capacity
        self.pages = []

    def access_page(self, page) : # 4개째부터 교체가 이루어지게 하자
        if page not in self.pages : # 기존의 페이지에 없으면 집어넣자
            if len(self.pages) &gt;= self.capacity:
                self.pages.pop(0) # 제일 앞에 있는 페이지 제거
            self.pages.append(page)
    def status(self):
        print(&quot;현재 페이지 상태 :&quot;, self.pages)

page_replacement = PageReplacementFIFO(capacity = 3)
page_replacement.status() # 빈상태 []
page_replacement.access_page(3)
page_replacement.status() # [3]
page_replacement.access_page(2)
page_replacement.status() # [3,2]
page_replacement.access_page(1)
page_replacement.status() # [3,2,1]

page_replacement.access_page(4)
page_replacement.status() # [2,1,4]
</code></pre>
<h3 id="스래싱thrashing">스래싱(thrashing)</h3>
<blockquote>
<ul>
<li>모든것의 근본적인 이유는 메모리 공간의 부족, 즉 프레임 부족 때문</li>
<li>프레임이 부족하면 페이지 볼트가 자주 발생</li>
<li>페이지 폴트가 발생하면 잦은 스와핑 작업으로 인해 CPU 사용률이 떨어지게 된다.</li>
<li>CPU 사용률이 떨어지면 운영체제는 더 많은 프로세스를 메모리에 올리려 하고, 이는 더 잦은 페이지 폴트로 이어져 악순환에 빠지게 된다.</li>
<li>이러한 문제를 <code>스레싱</code>이라고 한다.</li>
</ul>
</blockquote>
<h2 id="📝-스래싱thrashing-개요">📝 스래싱(Thrashing) 개요</h2>
<ul>
<li><strong>스래싱(Thrashing)</strong> 은 가상 메모리 시스템에서 <strong>페이지 부재(Page Fault)</strong>가 너무 자주 발생하여 <strong>CPU가 메모리 스왑 처리에만 집중하고 실제 작업을 거의 수행하지 못하는 상태</strong>를 의미함.</li>
<li>스래싱이 발생하면 <strong>성능이 급격히 저하</strong>되며, 시스템이 비효율적으로 동작함.</li>
</ul>
<hr>
<h3 id="📌-스래싱-해결책">📌 스래싱 해결책</h3>
<table>
<thead>
<tr>
<th><strong>해결 방법</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>프레임 할당량 조정(Frame Allocation Adjustment)</strong></td>
<td>프로세스가 사용하는 <strong>물리 메모리 프레임 개수를 조절</strong>하여 스래싱 방지</td>
</tr>
<tr>
<td><strong>워킹셋(Working Set) 기법</strong></td>
<td>프로세스가 일정 시간 동안 <strong>자주 참조하는 페이지 집합(Working Set)을 유지</strong>하여 성능 향상</td>
</tr>
<tr>
<td><strong>페이지 부하 제어(Page Load Control)</strong></td>
<td>한 번에 메모리에 올리는 페이지 수를 제한하여 <strong>과부하 방지</strong></td>
</tr>
<tr>
<td><strong>PFF(Page Fault Frequency) 기법</strong></td>
<td>페이지 부재(Page Fault) 발생 빈도를 기준으로 <strong>프레임 수를 동적으로 조정</strong></td>
</tr>
<tr>
<td><strong>로컬 프레임 할당(Local Allocation) vs. 글로벌 할당(Global Allocation)</strong></td>
<td><strong>로컬 할당:</strong> 프로세스마다 고정된 프레임 수 할당 <br> <strong>글로벌 할당:</strong> 모든 프로세스가 공유하는 프레임을 동적으로 할당</td>
</tr>
<tr>
<td><strong>페이지 교체 알고리즘 최적화</strong></td>
<td>LRU(Least Recently Used), LFU(Least Frequently Used) 등의 효율적인 페이지 교체 알고리즘을 적용</td>
</tr>
</tbody></table>
<hr>
<h3 id="📌-워킹셋working-set-기법">📌 워킹셋(Working Set) 기법</h3>
<h4 id="1-개념"><strong>1. 개념</strong></h4>
<ul>
<li><strong>워킹셋(Working Set)</strong> 이란 <strong>프로세스가 일정 시간 동안 자주 참조하는 페이지들의 집합</strong>을 의미함.</li>
<li>가상 메모리에서 프로세스가 일정 기간 내 참조한 페이지를 추적하여 <strong>자주 사용하는 페이지를 메모리에 유지</strong>하고, <strong>사용하지 않는 페이지를 제거</strong>함으로써 스래싱을 방지.</li>
</ul>
<h4 id="2-워킹셋의-정의"><strong>2. 워킹셋의 정의</strong></h4>
<ul>
<li><strong>W(Δ)</strong>: 특정 시간 구간 <strong>Δ(델타)</strong> 동안 <strong>프로세스가 참조한 페이지 집합</strong></li>
<li><strong>Δ 값 선택이 중요</strong>  <ul>
<li>Δ 값이 너무 작으면 → 너무 많은 페이지가 제거됨 (과도한 페이지 부재)  </li>
<li>Δ 값이 너무 크면 → 불필요한 페이지까지 유지됨 (메모리 낭비)  </li>
</ul>
</li>
</ul>
<h4 id="3-워킹셋을-활용한-스래싱-방지"><strong>3. 워킹셋을 활용한 스래싱 방지</strong></h4>
<ul>
<li>워킹셋을 유지하며 <strong>최소한의 프레임을 확보</strong>하여 스래싱을 방지.</li>
<li>일정 시간 동안 참조되지 않은 페이지는 <strong>스왑 아웃(Swap Out)</strong>하여 메모리 활용도를 높임.</li>
</ul>
<h4 id="4-워킹셋-기법의-장점"><strong>4. 워킹셋 기법의 장점</strong></h4>
<p>✔ <strong>스래싱 방지</strong>: 자주 사용하는 페이지만 유지하여 CPU가 불필요한 스왑을 하지 않도록 함<br>✔ <strong>메모리 효율 증가</strong>: 실제 필요한 페이지만 유지하므로 불필요한 페이지 사용 감소<br>✔ <strong>동적 프레임 조정 가능</strong>: 프로세스의 실행 패턴에 따라 필요한 페이지를 동적으로 조절 가능  </p>
<hr>
<h2 id="📝-파일">📝 파일</h2>
<blockquote>
<p>의미있는 정보들을 한덩어리로 모아 둔 논리적 단위, 보저기억 장치에 저장, 실행시 메인메모리 사용</p>
<ul>
<li>파일이름, 파일 실행에 필요한 정보, 메타데이터</li>
</ul>
</blockquote>
<ul>
<li>확장자 : 운영체제에게 있어 중요한 정보</li>
<li>응용 프로그램이 파일에 접근하려면 시스템 호출을 제공받아야 한다.</li>
</ul>
<pre><code class="language-python">with open(&quot;number_one.txt&quot;, &quot;w&quot;) as f:
    f.write(&quot;one&quot;)
with open(&quot;number_two.txt&quot;, &quot;w&quot;) as f:
    f.write(&quot;two&quot;)
with open(&quot;number_three.txt&quot;, &quot;w&quot;) as f:
    f.write(&quot;three&quot;)
with open(&quot;number_four.txt&quot;, &quot;w&quot;) as f:
    f.write(&quot;four&quot;)

import glob # 파일네임의 패턴을 이용해 한꺼번에 접근

for filename in glob.glob(&quot;*.txt&quot;, recursive= True): 
    print(filename)

import fileinput # 내용들도 한번에 처리하기

with fileinput.input(glob.glob(&quot;*.txt&quot;)) as fi:
    for line in fi :
        print(line) # 모든 파일 내용 읽기

import fnmatch # filenamematch
import os

for filename in os.listdir(&#39;.&#39;): # listdir: 현재 디렉토리에 있는것을 찾음
    if fnmatch.fnmatch(filename, &quot;??????_one.txt&quot;) :
        print(filename) # number_one.txt

---

# 파일 관련 예외는 운영체제와 관계가 있음
try : 
    f = open(&quot;none.txt&quot;, &quot;r&quot;) # 존재하지 않는것에 r을 하면 예러
    print(f.read())
    f.close()
except FileNotFoundError as e:
    print(e)
    issubclass(FileNotFoundError, OSError) # 첫번쨰 인자가 두번쨰 인자의 하위인자가 맞는지
</code></pre>
<pre><code class="language-js">const fs = require(&quot;fs&quot;)
fs.readFIle(&quot;number_one.txt&quot;, &quot;utf8&quot;, (err, data) =&gt; {
    if(err){
        console.log(&quot;파일을 읽는 도중 오류가 발생.&quot;, err)
        return;
    } 
    console.log(&quot;파일 내용:&quot;, data)
})

let content = &quot;four four four&quot;
fs.writeFile(&quot;number_four.txt&quot;, content, (err) =&gt; {
    if(err) {
        console.log(&quot;파일을 쓰는 도중 오류가 발생.&quot;, err)
        return;
    }
    console.log(&quot;파일쓰기 완료&quot;)

})

let content = &quot;four four four&quot;
fs.appendFile(&quot;number_four.txt&quot;, content, (err) =&gt; {
    if(err) {
        console.log(&quot;파일을 쓰는 도중 오류가 발생.&quot;, err)
        return;
    }
    console.log(&quot;파일쓰기 완료&quot;)

})</code></pre>
<h2 id="📝-디렉터리">📝 디렉터리</h2>
<blockquote>
<ul>
<li>운영체제는 파일을 정돈할 수 있도록 디렉터리를 지원</li>
<li>디렉터리도 파일의 일종</li>
<li>디렉터리에 저장된 파일 정보는 테이블 형태로 관리</li>
<li>테이블 행 하나하나를 가리켜 디렉터리 엔트리 라고 부른다</li>
</ul>
</blockquote>
<pre><code class="language-python"># os 파일 시스템 관련 함수
# pwd를 통해 현재 파일위치를 찾는다
pwd = &quot;경로 복붙&quot;

filenames = os.listdir(pwd) # 경로상의 파일들 반환 

# 디렉터리인지 아닌지 여부
print(os.path.isdir(filenams[0])) # False
print(os.path.isdir(pwd + &quot;/폴더&quot;) # True

# 파일인지 아닌지
print(os.path.isfile(filenams[0])) # True
print(os.path.isfile(pwd + &quot;/폴더&quot;) # False

# 파일이름과 확장자 분리
filepath = pwd + &quot;/&quot; + filenames[0]
print(filepath)
os.path.splitext(filepath)
name, ext = os.path.splitext(filepath)
print(ext) # .txt

---
# 경로와 확장자 이요해 파일 찾고, 내용 읽기
import os
def searchFile(dirname, extension):
    filenames = os.listdir(dirname)
    for filename in filenames:
        filepath = os.path.join(dirname, filename)
        if os.path.isdir(filepath):
            searchFile(filepath, extension)
        elif os.path.isfile(filepath) :
            name, ext = os.path.splitext(filepath) :
            if ext == extension :
                with open(filepathm, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
                    print(f.read())

searchFile(&quot;pwd경로&quot;, &quot;.js&quot;)  # 자바스크립트 코드 읽음

---
# 파일 복사 또는 이동
import os
import shutil

pwd = &quot;/&quot;
filenames = os.listdir(pwd)

for filename in filenames:
    if &quot;tokyo&quot; in filename :
        origin = os.path.join(pwd, filename)
        print(origin)

        shutil.copy(origin, os.path.join(pwd, &quot;copy.txt&quot;)) # 원본 , 어디다가, 
        shutil.move(origin, os.path.join(pwd, &quot;anony&quot;)) # 파일 이동

---
# 파일 경로를 문자열이 아닌 객체로 다루기
import os
import pathlib

for p in pathlib.Path.cwd().glob(&quot;*.txt&quot;): # 현재 워킹디렉토리 cwd current working directory
    print(p.parent) # 해당 텍스트들의 부모 경로가 나옴
    new_p = os.path.join(p.parent, p.name)
    print(new_p) # 내 파일의 총 경로

</code></pre>
<h2 id="📝-파일과-메모리">📝 파일과 메모리</h2>
<ul>
<li>하나의 파일은 여러개의 블록으로 이루어져 있다.</li>
<li>블록을 메미로에 할당할 때는 연속방식 또는 불연속방식을 사용할 수 있다.</li>
</ul>
<h2 id="🔗-연결-리스트-linked-list-자료구조">🔗 연결 리스트 (Linked List) 자료구조</h2>
<h3 id="📌-개요">📌 개요</h3>
<p><strong>연결 리스트(Linked List)</strong>는 메모리의 <strong>불연속 할당</strong>을 기반으로 데이터를 저장하는 선형 자료구조입니다.<br>연결 리스트는 데이터를 노드(Node) 단위로 관리하며, 각 노드는 데이터와 다음 노드를 가리키는 포인터(Pointer)를 가지고 있습니다.</p>
<hr>
<h3 id="📌-연결-리스트의-구조">📌 연결 리스트의 구조</h3>
<ul>
<li>연결 리스트는 <strong>노드(Node)</strong>라는 단위로 구성됨</li>
<li>각 노드는 <strong>데이터 영역</strong>과 <strong>다음 노드를 가리키는 포인터</strong>로 구성됨</li>
<li>메모리에서 연속적이지 않은 영역을 사용 (불연속 메모리 할당)</li>
</ul>
<h4 id="🔹-기본적인-연결-리스트의-형태">🔹 기본적인 연결 리스트의 형태:</h4>
<pre><code>Head
 |
 ▼
[Data | Next] -&gt; [Data | Next] -&gt; [Data | Next] -&gt; NULL</code></pre><hr>
<h3 id="📌-연결-리스트의-특징">📌 연결 리스트의 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>불연속 메모리 할당</strong></td>
<td>노드가 메모리에 흩어져 저장되며 포인터로 연결</td>
</tr>
<tr>
<td><strong>가변 크기</strong></td>
<td>데이터를 동적으로 추가하거나 삭제 가능</td>
</tr>
<tr>
<td><strong>동적 메모리 할당</strong></td>
<td>실행 시간에 노드를 동적으로 생성 및 삭제</td>
</tr>
<tr>
<td><strong>탐색 속도</strong></td>
<td>순차 접근만 가능하여 탐색 속도는 느림</td>
</tr>
<tr>
<td><strong>삽입·삭제 효율적</strong></td>
<td>중간에 데이터를 삽입하거나 삭제할 때 매우 효율적</td>
</tr>
</tbody></table>
<hr>
<h3 id="📌-연결-리스트의-종류">📌 연결 리스트의 종류</h3>
<h4 id="1-단일-연결-리스트-singly-linked-list"><strong>1. 단일 연결 리스트 (Singly Linked List)</strong></h4>
<ul>
<li><strong>한 방향</strong>으로만 다음 노드를 가리키는 포인터를 가짐</li>
<li><strong>Head 노드</strong>부터 순차적으로 접근 가능</li>
</ul>
<h4 id="2-이중-연결-리스트-doubly-linked-list"><strong>2. 이중 연결 리스트 (Doubly Linked List)</strong></h4>
<ul>
<li><strong>양방향(앞, 뒤)</strong>으로 접근 가능한 포인터를 가짐</li>
<li>노드 탐색과 삭제가 더 쉬움</li>
</ul>
<h4 id="3-원형-연결-리스트-circular-linked-list"><strong>3. 원형 연결 리스트 (Circular Linked List)</strong></h4>
<ul>
<li><strong>마지막 노드가 첫 번째 노드를 가리키도록 구성</strong></li>
<li>순환적인 데이터 구조를 나타낼 때 사용</li>
</ul>
<hr>
<h3 id="📌-연결-리스트의-장점과-단점">📌 연결 리스트의 장점과 단점</h3>
<h4 id="장점"><strong>장점</strong></h4>
<ul>
<li>삽입과 삭제가 빠름 (O(1) 가능)</li>
<li>메모리 동적 할당으로 유연한 메모리 관리 가능</li>
<li>메모리의 낭비가 적음 (필요한 만큼만 사용)</li>
</ul>
<h4 id="단점"><strong>단점</strong></h4>
<ul>
<li><strong>임의 접근(Random Access)이 불가능</strong> (순차 접근만 가능)</li>
<li>탐색(Search)에 시간이 오래 걸림 (O(n))</li>
</ul>
<hr>
<h3 id="📌-연결-리스트와-배열의-비교">📌 연결 리스트와 배열의 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>연결 리스트 (Linked List)</th>
<th>배열 (Array)</th>
</tr>
</thead>
<tbody><tr>
<td>메모리 할당</td>
<td><strong>불연속적 할당</strong> (포인터로 연결)</td>
<td>연속적 할당</td>
</tr>
<tr>
<td>데이터 접근</td>
<td>순차 접근만 가능</td>
<td>임의 접근 가능 (빠름)</td>
</tr>
<tr>
<td>삽입 및 삭제</td>
<td>빠름 (O(1))</td>
<td>느림 (O(n))</td>
</tr>
<tr>
<td>탐색 속도</td>
<td>느림 (O(n))</td>
<td>빠름 (O(1))</td>
</tr>
<tr>
<td>크기 조절</td>
<td>쉬움 (동적 조정)</td>
<td>어렵거나 비용이 큼</td>
</tr>
</tbody></table>
<hr>
<h3 id="📌-결론-1">📌 결론</h3>
<ul>
<li>연결 리스트는 <strong>불연속 할당 방식</strong>을 사용하는 선형 자료구조로, 포인터를 이용하여 데이터 간 연결을 유지합니다.</li>
<li>삽입과 삭제가 효율적이며 동적 메모리 관리가 중요한 경우에 특히 유용합니다.</li>
<li>탐색 속도는 배열보다 느리기 때문에 상황에 따라 적합하게 선택하여 사용해야 합니다. </li>
</ul>
<hr>
<h3 id="📌-전체-코드-c언어">📌 전체 코드 (C언어)</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

// 노드 구조체 정의
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 연결 리스트 함수들
void insertFront(Node** head, int value);
void deleteNode(Node** head, int key);
void printList(Node* head);

// 메인 함수
int main() {
    Node* head = NULL;  // 초기 연결 리스트는 비어 있음

    // 노드 추가
    insertFront(&amp;head, 10);
    insertFront(&amp;head, 20);
    insertFront(&amp;head, 30);

    printf(&quot;연결 리스트 출력: &quot;);
    printList(head);

    // 노드 삭제
    deleteNode(&amp;head, 20);
    printf(&quot;노드 삭제 후 출력: &quot;);
    printList(head);

    return 0;
}

// 연결 리스트의 맨 앞에 노드 삽입
void insertFront(Node** head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode-&gt;data = value;
    newNode-&gt;next = *head;
    *head = newNode;
}

// 특정 값 가진 노드 삭제
void deleteNode(Node** head, int key) {
    Node* temp = *head, *prev = NULL;

    if (temp != NULL &amp;&amp; temp-&gt;data == key) {
        *head = temp-&gt;next;
        free(temp);
        return;
    }

    while (temp != NULL &amp;&amp; temp-&gt;data != key) {
        prev = temp;
        temp = temp-&gt;next;
    }

    if (temp == NULL) return;

    prev-&gt;next = temp-&gt;next;
    free(temp);
}

// 연결 리스트 출력
void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf(&quot;%d -&gt; &quot;, temp-&gt;data);
        temp = temp-&gt;next;
    }
    printf(&quot;NULL\n&quot;);
}</code></pre>
<hr>
<h3 id="📌-실행-결과-예시">📌 실행 결과 예시</h3>
<pre><code>연결 리스트 출력: 30 -&gt; 20 -&gt; 10 -&gt; NULL
노드 삭제 후 출력: 30 -&gt; 10 -&gt; NULL</code></pre><ul>
<li><code>30 -&gt; 20 -&gt; 10 -&gt; NULL</code> 순서로 추가됨</li>
<li><code>20</code>을 삭제한 후 <code>30 -&gt; 10 -&gt; NULL</code>이 출력됨</li>
</ul>
<hr>
<h2 id="📝-데이터-표현">📝 데이터 표현</h2>
<blockquote>
<ul>
<li>컴퓨터는 모든 정보를 0과 1로 표현한다.</li>
<li>컴퓨터의 저장장치가 비트 단위로 구성되어 있기 때문이다<ul>
<li>비트란, On과 Off상태만 표현할 수 있는 정보 단위이다.</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li><p>부동 소수점: 컴퓨터에서 실수(Real Number)를 표현하는 방식</p>
<ul>
<li>부호(Sign): 양수(0) 또는 음수(1)를 표현하는 부분</li>
<li>지수부(Exponent): 소수점 위치를 나타내는 부분</li>
<li>가수부(Mantissa or Fraction): 숫자의 정밀도를 나타내는 부분</li>
</ul>
</li>
<li><p>컴퓨터가 읽고, 표현할 수 있는 문자의 모음인 <code>문자 셋 charcter set</code> 이 있다</p>
<ul>
<li>문자 인코딩</li>
<li>ex) UTF-8, 아스키코드, ...</li>
</ul>
</li>
</ul>
<h2 id="📝-스레드-풀링">📝 스레드 풀링</h2>
<ul>
<li>스레드는 프로세스의 작업을 나누어 관리하는 유용한 도구지만 스레드가 너무 많아지는 것은 바람직하지 않다.</li>
<li>스레드의 생성과 소멸은 시스템에 많은 부담을 준다.</li>
<li><strong>스레드 풀은</strong>, 특정 개수의 스레드가 여러 개의 일을 차례대로 수행하는 기법이다.</li>
</ul>
<pre><code class="language-python"># 개념은 같은 프로세스 풀 예제
import concurrent.futures
import time

def task(name) :
    time.sleep(0.5)
    return f&quot;{name}의 작업 완료&quot;

if __name__ == &quot;__main__&quot; :

    with concurrent.futures.ProcessPoolExecutor(max_workers=3) as executor :
        future_name = {executor.submit(task, f&quot;Task-{i}&quot;) : f&quot;Task-{i}&quot; for i in range(5)}
        print(future_name)
        # 작업 완료된 순서대로 결과 출력, 작업이 들어간 순서대로 처리하지 않을 수도 있음
        for future in concurrent.futures.as_complted(future_name):
            name = future_name[future]
            try :
                result = future.result()
                print(f&quot;{name}의 결과 : {result}&quot;)
            except Exception as e:
                print(e)</code></pre>
<h2 id="📝-라운드로빈-모델">📝 라운드로빈 모델</h2>
<blockquote>
<p>선입 선출이지만 정해진 시간 만큼만 일하는 것</p>
</blockquote>
<pre><code class="language-python">def roundrobin(processes, busrt_time, time_quantum) :
    n = len(processes)
    remaining_time = list(burst_time)
    turnaround_time = [0] * n
    waiting_time = [0] * n

    time = 0
    queue = []

    while True :
        all_completed = True # 모든 프로세스 종료 시 반복문 종료를 위한 플래그

        for i in range(n) :
           if remaining_time[i] &gt; 0:
               all_completed = False

               if remaining_time[i] &gt; time_quantum :
                   time += time_quantum
                   remaining_time[i] -= time_quantum
                   queue.append(i)
                else :
                    time += remaining_time[i]
                    turnaround_time[i] = time
                    remaining_time[i] = 0
                    waiting_time[i] = turnaround_time[i] - burst_time[i]
        if all_completed:
            break

    print(&quot;Process\tTurnaround Time\tWaiting Time&quot;)
    for i in range(n):
        print(f&quot;P{i+1}\t\t{turnaround_time[i]}\t\t{waiting_time[i]}&quot;)

# 함수 호출
processes = [1, 2 ,3]
burst_time = [10, 5, 8]
time_slice = 2

roundrobin(processes, burst_time, time_slice)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[배포 with Vercel, Cloudtype]]></title>
            <link>https://velog.io/@hak_0/%EB%B0%B0%ED%8F%AC-with-FastAPI</link>
            <guid>https://velog.io/@hak_0/%EB%B0%B0%ED%8F%AC-with-FastAPI</guid>
            <pubDate>Wed, 19 Feb 2025 13:31:09 GMT</pubDate>
            <description><![CDATA[<h2 id="배포-개념과-중요성">배포 개념과 중요성</h2>
<h3 id="배포-deployment">배포 Deployment</h3>
<ul>
<li><p>배포란?</p>
<ul>
<li>개발된 코드를 사용자들이 접근할 수 있도록 서버에 배치하는 과정</li>
<li>로컬 환경에서 실행되던 코드를 프로덕션(운영) 환경으로 이동</li>
<li>배포 후에도 유지보수와 모니터링 필요</li>
</ul>
</li>
<li><p>배포 환경의 유형</p>
<ul>
<li>로컬 환경 (Local Environment): 개발자 로컬 컴퓨터에서 실행</li>
<li>스테이징 환경 (Staging Environment): 실제 배포 전에 테스트하는 환경</li>
<li>프로덕션 환경 (Production Environment): 최종 사용자들이 사용하는 실제 환경</li>
</ul>
</li>
</ul>
<h4 id="배포의-주요-단계">배포의 주요 단계</h4>
<ol>
<li>코드 준비: 로컬에서 실행 가능한 상태로 코드 작성 및 검증</li>
<li>빌드/패키징: 코드와 의존성을 묶어 실행 가능한 형태로 준비</li>
<li>환경 구성: 서버에서 실행에 필요한 설정 파일 작성 (환경 변수, 포트 설정)<ul>
<li>환경 파일을 만들어 감춰나야 예를들어 access토큰 같은 탈취를 안당함</li>
</ul>
</li>
<li>배포 및 검증: 프로덕션 서버에서 실행 및 테스트</li>
</ol>
<h4 id="배포-방식-비교-전통적인-방식-vs-최신-방식">배포 방식 비교: 전통적인 방식 vs 최신 방식</h4>
<ul>
<li><p>전통적인 배포 방식: 서버 직접 설정 (On-Premise)</p>
<ul>
<li>특징<ul>
<li>직접 설정 가능: 개발자가 환경을 자유롭게 구성 가능</li>
<li>추가 비용 없음: 직접 서버를 관리하여 추가 서비스 비용이 적음</li>
</ul>
</li>
<li>단점<ul>
<li>환경 불일치 문제: 개발 환경과 운영 환경이 다르면 오류 발생</li>
<li>수동 배포 비효율성: 변경 사항이 있을 때마다 직접 서버에 접속해서 배포해야 함</li>
<li>확장성 부족: 트래픽이 많아질 경우 서버 성능을 조정하는 것이 어려움</li>
</ul>
</li>
</ul>
</li>
<li><p>최신 배포 방식: Docker, CI/CD, 클라우드 플랫폼을 활용한 자동화 배포 Cloud-based, Serverless</p>
<ul>
<li>특징<ul>
<li>컨테이너(ex. Docker) 사용: 개발 환경과 운영 환경을 동일하게 유지</li>
<li>CI/CD 자동화</li>
<li>클라우드 서비스 (AWS, GCP, Azure, NCP 등) 이용: 확장성 보장</li>
</ul>
</li>
<li>배포 자동화 예시<ul>
<li>Github -&gt; Cloudtype으로 자동 배포</li>
<li>코드 푸시(git push)만 하면 자동으로 배포</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="클라우드-서비스-장단점">클라우드 서비스 장단점</h3>
<table>
<thead>
<tr>
<th>클라우드 서비스</th>
<th>장점</th>
<th>단점</th>
<th>추천 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>AWS Free Tier</strong></td>
<td>● 다양한 서비스 제공 (EC2, Lambda 등)  <br> ● 1년간 무료 사용  <br> ● 강력한 확장성 및 유연성</td>
<td>● 복잡한 설정 과정  <br> ● 1년 후 요금 발생</td>
<td>● 대규모 프로젝트  <br> ● 복잡한 마이크로서비스 및 확장 가능한 애플리케이션</td>
</tr>
<tr>
<td><strong>Google Cloud Platform (GCP)</strong></td>
<td>● 강력한 데이터 분석 도구  <br> ● Kubernetes와의 통합 지원  <br> ● 무료 크레딧 제공 (최초 $300)</td>
<td>● 설정 및 관리를 위한 학습 필요  <br> ● 무료 크레딧 소진 후 유료</td>
<td>● 데이터 중심 프로젝트  <br> ● AI/ML, 데이터 처리 및 분석</td>
</tr>
<tr>
<td><strong>Microsoft Azure</strong></td>
<td>● Windows 기반 시스템에 강력한 지원  <br> ● 다양한 지역에 데이터센터 제공</td>
<td>● 설정이 복잡하고 비용이 다소 높음  <br> ● 무료 티어 제한적</td>
<td>● Windows 기반 프로젝트  <br> ● 기업용 애플리케이션</td>
</tr>
</tbody></table>
<hr>
<table>
<thead>
<tr>
<th>클라우드 서비스</th>
<th>장점</th>
<th>단점</th>
<th>추천 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Heroku</strong></td>
<td>● 간편한 배포 및 사용  <br> ● 개발자 친화적  <br> ● 비교적 낮은 단가의 기본 플랜</td>
<td>● 기본 플랜에서 슬립 모드 (대기 상태) 발생  <br> ● 유료 플랜 비용이 다소 높음</td>
<td>● 스타트업 및 개인 프로젝트  <br> ● 간단한 웹 애플리케이션 및 API 서버</td>
</tr>
<tr>
<td><strong>Vercel</strong></td>
<td>● 정적 사이트 및 서버리스 함수에 최적화  <br> ● 무료 플랜 제공  <br> ● 자동화된 배포</td>
<td>● 동적 애플리케이션에는 제약  <br> ● 데이터베이스 연결 시 추가 설정 필요</td>
<td>● 정적 사이트  <br> ● JAMstack 애플리케이션 (Next.js 등)</td>
</tr>
<tr>
<td><strong>Netlify</strong></td>
<td>● 정적 사이트 배포에 최적화  <br> ● 무료 플랜 제공  <br> ● CI/CD 파이프라인 내장</td>
<td>● 서버리스 기능 제한  <br> ● 동적 애플리케이션에는 적합하지 않음</td>
<td>● 프론트엔드 중심 프로젝트  <br> ● 간단한 마케팅 페이지 및 블로그</td>
</tr>
</tbody></table>
<hr>
<table>
<thead>
<tr>
<th>클라우드 서비스</th>
<th>장점</th>
<th>단점</th>
<th>추천 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Fly.io</strong></td>
<td>● Edge 서버를 통한 지리적 분산 배포  <br> ● 무료 크레딧 제공  <br> ● Docker 이미지 지원: 컨테이너 기반 배포 가능</td>
<td>● 복잡한 설정 필요: 초보자에게는 설정이 어려울 수 있음  <br> ● 대규모 애플리케이션에 제한적: 높은 트래픽 처리에는 한계</td>
<td>● 글로벌 사용자 대상 애플리케이션  <br> ● 소규모 API</td>
</tr>
<tr>
<td><strong>Render</strong></td>
<td>● 자동화된 CI/CD 파이프라인  <br> ● 무료 티어 제공  <br> ● 쉬운 설정</td>
<td>● 높은 사용량 시 유료 전환 필요  <br> ● 제한된 무료 리소스</td>
<td>● 개인 프로젝트  <br> ● API 서버, 웹 애플리케이션</td>
</tr>
<tr>
<td><strong>Railway</strong></td>
<td>● 직관적인 UI 및 빠른 배포  <br> ● 사용량 기반 청구  <br> ● 여러 언어 및 데이터베이스 지원</td>
<td>● 무료 티어 사용량 제한  <br> ● 높은 트래픽 시 요금 발생</td>
<td>● 개인 및 소규모 팀 프로젝트  <br> ● 데이터베이스가 필요한 애플리케이션</td>
</tr>
</tbody></table>
<h3 id="클라우드-서비스-선택-시-고려사항">클라우드 서비스 선택 시 고려사항</h3>
<ul>
<li><p>프로젝트 규모 및 복잡도</p>
<ul>
<li>소규모 프로젝트: Render, Railway</li>
<li>대규모 확장 가능한 프로젝트: AWS, GCP, Azure</li>
</ul>
</li>
<li><p>예산</p>
<ul>
<li>무료 티어 제공 여부 확인: Fly.io, Render, Railway 등</li>
<li>높은 트래픽이나 데이터베이스 사용이 예상되면 비용 분석 필수</li>
</ul>
</li>
<li><p>설정의 간편성</p>
<ul>
<li>빠른 설정: Heroku, Render 추천</li>
<li>복잡한 설정과 유연성을 원한다면 AWS, GCP 추천</li>
</ul>
</li>
<li><p>기술 스택 호환성</p>
<ul>
<li>정적 사이트 중심: Vercel, Netlify</li>
<li>서버리스 애플리케이션: AWS Lambda, GCP CloudFunction</li>
<li>데이터베이스 중심: Railway, AWS RDS</li>
</ul>
</li>
<li><p>서비스 안정성 및 지원</p>
<ul>
<li>글로벌 서비스, 데이터 센터: AWS, GCP, Azure 추천</li>
<li>지역 기반 서비스: 데이터 센터 위치를 확인할 것</li>
</ul>
</li>
<li><p>성능 및 확장성</p>
<ul>
<li>예측 가능한 트래픽: Render, Vercel</li>
<li>높은 확장성 요구: AWS, GCP, Azure</li>
</ul>
</li>
<li><p>CI / CD 및 개발자 도구</p>
<ul>
<li>자동화 배포: Render, Vercel, Netilfy 추천</li>
<li>데이터 분석, AI 통합이 필요하다면 GCP 추천</li>
</ul>
</li>
</ul>
<h2 id="devops와-배포-자동화">DevOps와 배포 자동화</h2>
<ul>
<li><p>DevOps: Development + Operations</p>
<ul>
<li>소프트웨어 개발과 운영을 하나로 통합하여 빠르고 안정적인 배포를 가능하게 하는 문화 및 기술</li>
<li><strong>자동화</strong> -&gt; 안정성 증가 및 빠른 업데이트 가능</li>
<li>CI(Continuous Integration) 지속적 통합: 코드 변경 사항을 자동으로 빌드 및 테스트</li>
<li>CD(Continuous Deployment) 지속적 배포: 코드 변경 사항을 운영 환경에서 자동 배포</li>
</ul>
</li>
<li><p>CI/CD 파이프라인 예시</p>
<ul>
<li>개발자가 코드 변경 후 github에 푸시</li>
<li>CI 시스템 (Github Actions)이 자동으로 빌드 및 테스트 실행</li>
<li>테스트 통과 시, CD 시스템 (Cloudtype)이 배포 자동화 진행</li>
<li>배포 완료 후, 운영 서버에서 서비스 활성화</li>
</ul>
</li>
</ul>
<h3 id="배포의-확장성과-유지보수">배포의 확장성과 유지보수</h3>
<ul>
<li><p>배포 시스템이 확장성을 가지려면?</p>
<ul>
<li>MSA(Micro Service Architecture): 여러 작은 서비스로 분리</li>
<li>Container Orchestration (Kubernetes) 사용: 자동 확장 및 관리 지원</li>
<li>무중단 배포 (Blue-Green Deployment): 운영 중에도 장애 없이 배포 (프론트면 실제 파일에 덮어 씌워 버리는 것)</li>
</ul>
</li>
<li><p>배포 후 지속적인 모니터링</p>
<ul>
<li>서비스 상태 체크: Health Check API</li>
<li>로깅 및 오류 분석: Sentry, Prometheus</li>
<li>성능 모니터링: Grafana</li>
</ul>
</li>
</ul>
<h3 id="docker란">Docker란</h3>
<ul>
<li>애플리케이션을 컨테이너라는 가상 환경에서 실행하는 기술<ul>
<li>컨테이너를 사용하면 OS에 영향을 받지 않고 어디서든 동일한 환경에서 실행 가능<ul>
<li>A는 Windows에서, B는 CentOS에서 프로그래밍을 할 때 불일치가 발생할 수 있으며, 이 때 컨테이너 기술을 사용하면 둘이 동일한 환경에서 서버 구동 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="컨테이너-vs-가상머신-vm">컨테이너 vs 가상머신 (VM)</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>가상머신 (VM)</th>
<th>Docker 컨테이너</th>
</tr>
</thead>
<tbody><tr>
<td><strong>실행 속도</strong></td>
<td>느림</td>
<td>빠름</td>
</tr>
<tr>
<td><strong>운영체제</strong></td>
<td>각 VM마다 필요</td>
<td>호스트 OS 공유</td>
</tr>
<tr>
<td><strong>리소스 사용</strong></td>
<td>많음 (RAM, CPU 차지)</td>
<td>적음</td>
</tr>
<tr>
<td><strong>실행 환경</strong></td>
<td>무겁고 복잡</td>
<td>가볍고 단순</td>
</tr>
</tbody></table>
<h3 id="가상머신이란-">가상머신이란 ?</h3>
<blockquote>
<p>하드웨어 자원을 가상화하여 독립적인 운영체제를 실행할 수 있도록 만든 소프트웨어 환경을 의미해. 즉, 하나의 물리적인 컴퓨터 위에서 여러 개의 가상 컴퓨터를 실행</p>
</blockquote>
<ul>
<li><p>특징</p>
<ol>
<li>독립적인 운영체제 실행<ul>
<li>VM마다 각각의 운영체제(Windows, Linux 등)를 설치할 수 있음.</li>
<li>호스트 OS(기본 운영체제)와 다른 OS도 설치 가능.</li>
</ul>
</li>
<li>하드웨어 리소스 분할<ul>
<li>CPU, RAM, 저장소 등의 자원을 가상화하여 할당.</li>
<li>하나의 물리 서버에서 여러 VM을 실행할 수 있음.</li>
</ul>
</li>
<li>보안 및 격리성<ul>
<li>각 VM은 서로 완전히 독립적이라, 하나의 VM이 문제가 생겨도 다른 VM에 영향을 주지 않음.</li>
</ul>
</li>
<li>유연한 배포와 관리<ul>
<li>클라우드 서비스(AWS, Azure, GCP)에서 가상머신을 쉽게 생성 및 관리 가능.</li>
</ul>
</li>
</ol>
</li>
<li><p>VM의 대표적인 종류</p>
<ul>
<li><strong>VirtualBox</strong> → 로컬 환경에서 VM 실행 (무료)</li>
<li><strong>VMware</strong> → 기업에서 많이 사용 (유료)</li>
<li><strong>Hyper-V</strong> → Windows 전용 가상화 소프트웨어</li>
<li><strong>AWS EC2, GCP Compute Engine</strong> → 클라우드 기반 가상머신</li>
</ul>
</li>
<li><p>가상머신(VM)은 언제 사용하면 좋을까?</p>
<ul>
<li>서로 다른 OS 환경을 실행해야 할 때 (예: Windows에서 Linux 실행)  </li>
<li>보안이 중요한 애플리케이션을 실행할 때 (완전한 격리가 필요할 때)  </li>
<li>물리적 서버를 여러 개로 나눠서 사용하고 싶을 때</li>
</ul>
</li>
</ul>
<h3 id="도커">도커</h3>
<ul>
<li><p>핵심</p>
<ul>
<li>이미지(image): 실행 가능한 애플리케이션 패키지</li>
<li>컨테이너(Container): 실행 중인 이미지 (실제 애플리케이션이 실행됨)</li>
<li>Dockerfile: Docker 이미지를 생성하는 설정 파일</li>
</ul>
</li>
<li><p>주요 명령어</p>
<ul>
<li>실행 중인 컨테이너 목록 조회: <code>docker ps</code></li>
<li>모든 컨테이너 목록 (중지된 컨테이너 포함): <code>docker ps -a</code></li>
<li>실행 중인 컨테이너 중지: <code>docker stop &lt;container_id&gt;</code></li>
<li>실행 중인 컨테이너 강제종료: <code>docker kill &lt;container_id&gt;</code></li>
<li>컨테이너 삭제: <code>docker rm &lt;container_id&gt;</code></li>
<li>컨테이너 로그 확인: <code>docker logs &lt;container_id&gt;</code></li>
</ul>
</li>
</ul>
<h4 id="이미지-빌드--실행">이미지 빌드 &amp; 실행</h4>
<ul>
<li>도커 이미지 빌드<ul>
<li><code>docker build -t &lt;이미지명&gt;&lt;태그&gt;</code>, (<code>docker build -t app .</code>)</li>
</ul>
</li>
<li>도커 컨테이너 실행 (포트 매핑 포함)<ul>
<li><code>docker run -d -p &lt;호스트포트&gt;:&lt;컨테이너포트&gt; &lt;이미지명&gt;</code></li>
</ul>
</li>
</ul>
<h4 id="이미지-및-볼륨-관리">이미지 및 볼륨 관리</h4>
<ul>
<li>모든 도커 이미지 목록 확인<ul>
<li><code>docker images</code></li>
</ul>
</li>
<li>특정 도커 이미지 삭제<ul>
<li><code>docker rmi &lt;image_id&gt;</code></li>
</ul>
</li>
<li>사용하지 않는 이미지 및 볼륨 정리<ul>
<li><code>docker system prune -a</code></li>
</ul>
</li>
</ul>
<h3 id="docker-컨테이너-최적화">Docker 컨테이너 최적화</h3>
<ul>
<li><p>실행 환경을 최대한 가볍게 만들어야 함</p>
<ul>
<li>작은 베이스 이미지 사용<ul>
<li><code>FROM python:3.10 (922MB)</code></li>
<li><code>FROM python:3.10-alpine (25MB)</code></li>
</ul>
</li>
</ul>
</li>
<li><p>불필요한 캐시 저장 방지 &amp; 빌드 속도 최적화</p>
<pre><code class="language-python"># 1. Python 3.10 기반 이미지 사용
FROM python:3.10-alpine (alpine사용 경량화)
</code></pre>
</li>
</ul>
<h1 id="2-작업-디렉토리-생성">2. 작업 디렉토리 생성</h1>
<p>WORKDIR /app</p>
<h1 id="3-의존성-설치">3. 의존성 설치</h1>
<p>COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt</p>
<h1 id="4-fastapi-애플리케이션-복사">4. FastAPI 애플리케이션 복사</h1>
<p>COPY . .</p>
<h1 id="5-컨테이너-실행-명령어-설정">5. 컨테이너 실행 명령어 설정</h1>
<p>CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</p>
<pre><code>* 다중 스테이지 빌드 적용
```python
# 1. 빌드 스테이지 (의존성 설치만 수행)
FROM python:3.10-alpine (alpine사용 경량화)
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 2. 실행 스테이지 (필요한 파일만 복사)
FROM python:3.10-alpine (alpine사용 경량화)
WORKDIR /app
COPY --from==builder /app /app
COPY . .
CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</code></pre><ul>
<li><p>.dockerignore 활용</p>
<ul>
<li><strong>pycache</strong>/</li>
<li>.env</li>
<li>*.log</li>
</ul>
</li>
<li><p>FastAPI Docker 이미지 빌드 예시</p>
<pre><code class="language-python"># main.py
from fastapi import FastAPI
</code></pre>
</li>
</ul>
<p>app = FastAPI()</p>
<p>@app.get(&quot;/&quot;)
async def read_root():
    return {&quot;message&quot;: &quot;FastAPI on Docker!&quot;}</p>
<h1 id="requirementstxt">requirements.txt</h1>
<p>fastapi
uvicorn</p>
<h1 id="dockerfile">Dockerfile</h1>
<h1 id="1-python-310-기반-이미지-사용">1. Python 3.10 기반 이미지 사용</h1>
<p>FROM python:3.10-alpine</p>
<h1 id="2-작업-디렉토리-생성-1">2. 작업 디렉토리 생성</h1>
<p>WORKDIR /app</p>
<h1 id="3-의존성-설치-1">3. 의존성 설치</h1>
<p>COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt</p>
<h1 id="4-fastapi-애플리케이션-복사-1">4. FastAPI 애플리케이션 복사</h1>
<p>COPY . .</p>
<h1 id="5-컨테이너-실행-명령어-설정-1">5. 컨테이너 실행 명령어 설정</h1>
<p>CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</p>
<pre><code>### Docker Compose로 FastAPI + DB 연동

* Docker Compose: 여러 개의 Docker 컨테이너를 한 번에 실행하는
도구
* 주로 서버 + DB 등을 한 번에 돌릴 때 사용
  - ex) FastAPI + PostgreSQL + Redis
* `docker-compose up -d`
```yaml
# compose.yaml
version: &quot;3.9&quot; # Docker Compose 버전

services:
  db:
    image: postgres: 13 # PostgreSQL 13 버전 사용
    container_name: fastapi_postgres
    restart: always # 컨테이너가 종료되면 자동 재시작
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: fastapi_password
      POSTGRES_DB: fastapi_db
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - postgres_data:/var/lib/postgresql/data # DB 데이터 저장

  fastapi:
    build: . # 현재 디렉토리의 Dockerfile를 사용해 이미지 빌드
    container_name: fastapi_app
    depends_on:
      - db # db 서비스가 실행된 후 fastapi 서비스 실행
    ports:
      - &quot;8000:8000&quot;
    environment:
      DATABASE_URL: &quot;postgresql://fastapi_user:fastapi_password@db:5432/fastapi_db&quot;

volumes:
  postgres_data: # PostgreSQL 데이터를 저장할 볼륨(컨테이너 재시작 시 데이터 유지)</code></pre><h4 id="도커-실습">도커 실습</h4>
<pre><code class="language-sh"># (1) 도커 이미지 빌드 (현재 디렉토리의 Dockerfile 사용)
docker build -t app .

# (2) 빌드된 이미지 확인
docker images

# (3) 컨테이너 실행 (-d: 백그라운드 실행, -p: 포트 매핑)
docker run -d -p 8000:8000 app


# (1) 실행 중인 컨테이너 확인
docker ps   

# (2) 컨테이너 중지 (b = 컨테이너 ID or 이름)
docker stop b   

# (3) 실행 중인 컨테이너가 있는지 다시 확인
docker ps -a  



# (1) 특정 컨테이너 삭제 (중지된 상태여야 함)
docker rm b    

# (2) 모든 컨테이너 삭제 (실행 중인 것도 포함, 강제)
docker rm $(docker ps -aq) -f  


# (1) 실행 중인 컨테이너 확인
docker ps  

# (2) 실행 중인 컨테이너 중지 (필수)
docker stop &lt;container_id&gt;   

# (3) 실행 중지 후 이미지 삭제 (-f: 실행 중인 것도 삭제)
docker rmi 이미지명 -f    

# (4) 사용하지 않는 이미지 전부 삭제 (dangling 이미지)
docker image prune -a</code></pre>
<h2 id="배포-방법-개요">배포 방법 개요</h2>
<blockquote>
<ul>
<li>컨테이너를 클라우드에 올린다고 바로 실행되지 않음</li>
</ul>
</blockquote>
<ul>
<li>로컬에서는 Docker 컨테이너 실행만으로 API에 접근 가능</li>
<li>클라우드에서는<ul>
<li>컨테이너가 실행되더라도 네트워크 접근이 차단되어 있음</li>
<li>로드 밸런서, 방화벽, 인바운드 규칙 설정이 필요할 수 있음</li>
<li>게다가 여러 컨테이너를 운영하려면 오케스트레이션 필요<ul>
<li>보통 Kubernetes(쿠버네티스) 사용</li>
<li>여러개를 관리하려면 필요한 개념</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="쿠버네티스">쿠버네티스</h3>
<h4 id="kubernetes는-꼭-사용해야-할까">Kubernetes는 꼭 사용해야 할까?</h4>
<ul>
<li>단일 컨테이너라면 K8S 없이 실행 가능</li>
<li>하지만 트래픽 증가, 자동 스케일링, 무중단 배포, 2개 이상 컨테이너를 사용 시 쿠버네티스를 사용해야 함</li>
<li>K8S 사용 시 배포 방법<ul>
<li>컨테이너를 Registry에 업로드</li>
<li>VPC, Subnet, NAT GW, Storage 설정</li>
<li>Kubernetes Service에 Registry 연결 (템플릿 배포)</li>
</ul>
</li>
</ul>
<h2 id="cloudtype-배포">Cloudtype 배포</h2>
<ol>
<li>Cloudtype 가입 (카드 등록 필수)</li>
<li>컨테이너 기반 환경: SQLite 그대로 사용할 예정<ul>
<li>단 컨테이너가 재시작될 시 파일이 삭제될 수 있음 = 저장 경로를 지정함으로써 문제 해결<pre><code class="language-python">database.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
import os
</code></pre>
</li>
</ul>
</li>
</ol>
<p>DB_DIR = &quot;./data&quot;
if not os.path.exists(DB_DIR):
    os.makedirs(DB_DIR)</p>
<p>DATABASE_URL = &quot;sqlite:///./data/test.db&quot;</p>
<p>engine = create_engine(DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()</p>
<p>class Ticket(Base):
    <strong>tablename</strong> = &quot;tickets&quot;
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, index=True)
    age = Column(Integer)
    price = Column(Integer)</p>
<p>Base.metadata.create_all(bind=engine)</p>
<p>def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()</p>
<pre><code>3. cloudtype.yaml 추가
```yaml
# cloudtype.yaml
app: fastapi-tickets
service: web
env: python
start: uvicorn main:app --host 0.0.0.0 --port 8000
</code></pre><ol start="4">
<li>Github Repository 생성</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/b7113030-e87e-4377-80ac-8f1afb610543/image.png" alt=""></p>
<ol start="5">
<li>Cloudtype 접속</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/d46c7b77-d256-405d-b0ef-c7964183b227/image.png" alt=""></p>
<ol start="6">
<li>Cloudtype 배포</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/154c3751-2caf-4e6a-beda-177356a1116d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/1907677c-3bd6-4612-8a90-58b44ce38cd1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/2d27ab4d-ce9b-4b69-b2e0-b212418976b9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/b2997d22-0089-4337-b60d-2a6790ddc937/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/22083056-d2e7-4a48-8c96-a009471584f2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/fef5f928-3d25-40bc-8914-0ae0f389587f/image.png" alt=""></p>
<h2 id="vercel-배포">Vercel 배포</h2>
<ul>
<li>Serverless 환경<ul>
<li>SQLite는 로컬 기반이며 Vercel에서 사용 불가</li>
<li>따라서 외부 DB를 생성하여 연결하는 방향으로 진행 예정</li>
</ul>
</li>
<li>배포 시 확인할 것<ul>
<li>vercel.json</li>
<li>vercel CLI 설치 (npm 사전 설치 필요)</li>
<li>NeonDB, Vercel 회원가입</li>
<li>requirements.txt 더블체크 (특히 psycopg2-binary)</li>
</ul>
</li>
</ul>
<ol>
<li>pip install psycopg2-binary + NeonDB로 접속 로그인 후 connect로 내 DB주소 확인, 아래 postgresql 부분 복사</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/c3525d54-cb87-4038-871b-d79311b4dd7d/image.png" alt=""></p>
<ol start="2">
<li>database.py<pre><code class="language-python"># database.py
import os
</code></pre>
</li>
</ol>
<p>DATABASE_URL = os.getenv(&quot;DATABASE_URL&quot;, &quot;postgresql://user:password@dbhost:5432/dbname&quot;)</p>
<p>engine = create_engine(DATABASE_URL)</p>
<pre><code>3.vercel.json
```js
{
  &quot;builds&quot;: [
    { &quot;src&quot;: &quot;main.py&quot;, &quot;use&quot;: &quot;@vercel/python&quot; }
  ],
  &quot;routes&quot;: [
    { &quot;src&quot;: &quot;/(.*)&quot;, &quot;dest&quot;: &quot;main.py&quot; }
  ]
}</code></pre><ol start="4">
<li>vercel 설치 및 프로젝트 초기화<ol>
<li>npm install -g vercel</li>
<li>vercel login (vercel 웹사이트 회원가입)<ul>
<li>Continue with GitHub(원하는 것에 맞춰)</li>
<li>vercel link </li>
<li>Scope 선택</li>
<li>N</li>
<li>제작한 프로젝트 이름</li>
<li>./</li>
</ul>
</li>
</ol>
</li>
<li>환경변수 설정<ul>
<li>vercel env add DATABASE_URL(postgres url 복붙) </li>
<li>Production</li>
</ul>
</li>
<li>배포 시작 : vercel .</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/fdf9c10a-443a-402c-839a-2fb273d8e938/image.png" alt=""></p>
<ol start="7">
<li>오류 발생시 </li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/2e2fb855-8020-4317-a207-cd1d943939f6/image.png" alt=""></p>
<ol start="8">
<li>환경 오류 발생시 </li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/46d3dfb4-f7ce-4a30-8eac-84687ef8f288/image.png" alt=""></p>
<ol start="9">
<li>완료</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hak_0/post/0f842eed-9acb-4cc4-9952-66a0fca9ccad/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI WebSocket]]></title>
            <link>https://velog.io/@hak_0/FastAPI-WebSocket</link>
            <guid>https://velog.io/@hak_0/FastAPI-WebSocket</guid>
            <pubDate>Tue, 18 Feb 2025 08:52:04 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-소켓web-socket">웹 소켓(Web Socket)</h2>
<blockquote>
<ul>
<li>클라이언트와 서버 간 양방향 통신을 지원하는 프로토콜</li>
<li>HTTP 요청과 다르게 한 번 연결되면 지속적으로 데이터 송수신 가능<ul>
<li>클라이언트가 요청하지 않아도 서버가 데이터를 보낼 수 있음 (Push
방식 지원)</li>
</ul>
</li>
<li>ws:// 또는 wss://(보안) 방식 활용(web socket security)</li>
<li>실시간 통신</li>
</ul>
</blockquote>
<h3 id="필요-이유">필요 이유</h3>
<p>= 기존 HTTP 방식의 문제점</p>
<ul>
<li><strong>실시간성 부족</strong><ul>
<li>HTTP 요청-응답 방식은 클라이언트가 요청해야만 서버가 응답 가능</li>
<li>실시간 알림, 채팅 등에는 비효율적</li>
</ul>
</li>
<li><strong>풀링(Pulling)</strong>과 긴 <strong>연결(Long Polling)</strong>의 한계<ul>
<li>풀링: 클라이언트가 일정 시간마다 서버에 요청 (과부하 문제)</li>
<li>긴 연결: 클라이언트가 연결을 유지하며 서버 응답을 기다림 (비효율적)</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>특징</th>
<th>HTTP</th>
<th>WebSocket</th>
</tr>
</thead>
<tbody><tr>
<td><strong>연결 방식</strong></td>
<td>요청-응답 방식</td>
<td>지속적인 연결 유지</td>
</tr>
<tr>
<td><strong>통신 방식</strong></td>
<td>클라이언트가 요청하면 서버가 응답</td>
<td>서버와 클라이언트가 자유롭게 송수신 가능</td>
</tr>
<tr>
<td><strong>활용 사례</strong></td>
<td>REST API, 정적 페이지 요청</td>
<td>실시간 채팅, 알림 시스템, 주식 데이터 스트리밍</td>
</tr>
</tbody></table>
<h3 id="websocket-프로토콜-구조">WebSocket 프로토콜 구조</h3>
<blockquote>
<p><code>pip3 install &#39;uvicorn[standard]&#39;</code></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hak_0/post/d7300447-74a9-483b-b000-d847440212a1/image.png" alt=""></p>
<ul>
<li>Handshake<ul>
<li>HTTP 요청을 통해 WebSocket 연결 수립 (이 때 Upgrade: websocket헤더 사용)</li>
<li>기존 HTTP 요청을 WebSOcket통신으로 전환하는 과정</li>
<li>클라이언트와 서버간에 WebSocket 연결을 수립하기 위해 수행하는 초기 단계를 의미</li>
</ul>
<ol>
<li>클라이언트가 서버에 WebSocket 요청을 보냄 (Upgrade 요청</li>
<li>서버가 WebSocket 연결을 허용하면 응답 (101 Switching Protocols)</li>
<li>WebSocket 연결이 수립되고, 지속적인 데이터 통신 가능</li>
</ol>
</li>
<li>메시지 송수신 (Message Exchange)<ul>
<li>텍스트 또는 바이너리 메시지 교환</li>
</ul>
</li>
<li>연결 종료(Connection Close)<ul>
<li>클라이언트 또는 서버가 연결 종료</li>
</ul>
</li>
</ul>
<h4 id="🧑💻-서버-구현">🧑‍💻 서버 구현</h4>
<pre><code class="language-python">from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket(&quot;/ws&quot;) # 보편적으로 /ws로 url을 지정
async def websocket_endpoint(websocket: WebSocket): # 실시간으로 여러 요청을 처리하기에 비동기가 적합
    await websocket.accept() # WebSocket 연결 수락
    while True: 서버를 게속 유지하기위해 (돌리기 위해)
        data = await websocket.receive_text() # 클라이언트 메세지 수신
        await websocket.send_text(f&quot;서버 응답: {data}&quot;) # 클라이언트에게 응답</code></pre>
<ul>
<li>postman 설정
<img src="https://velog.velcdn.com/images/hak_0/post/704a959e-97eb-4197-b682-3a43be6c2866/image.png" alt="">
<img src="https://velog.velcdn.com/images/hak_0/post/876fcbfd-db52-4089-bd98-8c6c8e3677e8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hak_0/post/021e2ffa-139e-40b1-99d3-38045fe89bf1/image.png" alt="">
<img src="https://velog.velcdn.com/images/hak_0/post/dfbe8355-663f-497b-a72e-7ace994ea6ac/image.png" alt=""></li>
</ul>
<h3 id="websocket-서버-구현---disconnect-1000">WebSocket 서버 구현 - Disconnect 1000</h3>
<ul>
<li>Starlette.websockets.WebSocketDisconnect: (1000, ‘’)<ul>
<li>에러코드 1000: WebSocket이 정상적으로 종료됨</li>
<li>클라이언트(예: Postman, 브라우저)가 WebSocket 연결을 닫으면
서버에서 발생하는 오류</li>
</ul>
</li>
<li>해결 방법: try-except을 추가하여 예외 처리<ul>
<li>fastapi에서 WebSocketDisconnect을 import 후, except 처리</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hak_0/post/38a1a598-806d-4ec0-81ad-645cb413ee83/image.png" alt=""></p>
<pre><code class="language-python">from fastapi import FastAPI, WebSocket, WebSocketDisconnect


app = FastAPI()
@app.websocket(&quot;/ws&quot;)
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_text()
            await websocket.send_text(f&quot;서버 응답 : {data}&quot;)
    except WebSocketDisconnect:
        print(&quot;클라이언트 연결 종료&quot;)</code></pre>
<h3 id="websocket을-활용한-실시간-기능">WebSocket을 활용한 실시간 기능</h3>
<blockquote>
<ul>
<li>대표적인 서비스 사례<ul>
<li>실시간 채팅: 카카오톡, 슬랙, 디스코드 등</li>
<li>실시간 알림 시스템: SNS 알림, 주문 처리 상태 알림 등</li>
<li>실시간 주식 데이터 스트리밍: 증권 거래소 API</li>
<li>실시간 협업 기능: Google Docs같은 실시간 편집</li>
</ul>
</li>
</ul>
</blockquote>
<h4 id="실시간-알림-시스템-구현">실시간 알림 시스템 구현</h4>
<ul>
<li><p>기본 개념</p>
<ul>
<li>클라이언트(Web, Mobile 등)가 서버와 WebSocket 연결을 맺음</li>
<li>서버가 특정 이벤트(예: 주문 완료, 메시지 도착, 좋아요 등) 감지</li>
<li>서버가 연결된 모든 클라이언트에게 즉시 알림 전송</li>
</ul>
</li>
<li><p>시스템 구성도
<img src="https://velog.velcdn.com/images/hak_0/post/f8e39376-3554-46a8-9189-2b4519f13755/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p>여러 클라이언트가 WebSocket을 통해 서버에 연결될 수 있도록 구성</p>
<ul>
<li><p>ConnectionManager 클래스를 만들어 연결 관리</p>
</li>
<li><p>사용자가 연결을 끊으면 자동으로 제거됨</p>
<pre><code class="language-python">class ConnectionManager:
&quot;&quot;&quot;WebSocket 연결관리&quot;&quot;&quot;
def __init__(self): # 연결된 사용자 리스트
    self.active_connections: List[WebSocket] = []

async def connect(self, websocket: WebSocket):
    &quot;&quot;&quot; 클라이언트가 websocket 연결을 요청하면 리스트에 추가 &quot;&quot;&quot;
    await websocket.accept()
    self.active_connections.append(websocket)

def disconnect(self, websocket: WebSocket):
    self.active_connections.remove(websocket)

async def broadcast(self, message: str):
    &quot;&quot;&quot; 모든 연결된 클라이언트에게 메세지 전송 &quot;&quot;&quot;
    for connection in self.active_connections:
        await connection.send_text(message)
</code></pre>
</li>
</ul>
</li>
</ul>
<p>manager = ConnectionManager()</p>
<h1 id="특정-요청을-감지하면-서버가-사용자에게-전달">특정 요청을 감지하면 서버가 사용자에게 전달</h1>
<pre><code>* 특정 사용자에게만 1:1 알림 보내기
```python

from typing import Dict, List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect


app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[str, WebSocket] = {}

    async def connect(self, websocket: WebSocket, username: str):
        &quot;&quot;&quot; 특정 사용자의 WebSocket 연결 관리 &quot;&quot;&quot;
        await websocket.accept()
        self.active_connections[username] = websocket

    def disconnect(self, username: str):
        &quot;&quot;&quot; 사용자가 연결을 끊으면 제거 &quot;&quot;&quot;
        if username in self.active_connections:
            del self.active_connections[username]

    async def send_private_message(self, username: str, message: str):
        &quot;&quot;&quot; 특정 사용자에게 메세지 전송 &quot;&quot;&quot;
        if username in self.active_connections:
            await self.active_connections[username].send_text(message)

manager = ConnectionManager()

@app.websocket(&quot;/ws/{username}&quot;)
async def websocket_endpoint(username: str,websocket: WebSocket):
    &quot;&quot;&quot; 클라이언트가 /ws 경로로 WebSocket 연결 요청 &quot;&quot;&quot;
    await manager.connect(websocket, username)

    try:
        while True:
            data = await websocket.receive_text()
            await manager.send_private_message(username, f&quot;📢 개인 메시지: {data}&quot;)
    except WebSocketDisconnect:
        manager.disconnect(username)

@app.post(&quot;/send-message/{username}&quot;)
async def send_message(username: str, message: str):
    &quot;&quot;&quot; 특정 사용자에게 메시지를 전송하는 API (Postman 테스트용) &quot;&quot;&quot;
    await manager.send_private_message(username, message)
    return {&quot;message&quot;: f&quot;📢 {username}에게 메시지 전송 완료!&quot;}</code></pre><ul>
<li>postman
<img src="https://velog.velcdn.com/images/hak_0/post/6c86c1b2-50e6-47ca-aad5-8b58a9da9281/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/hak_0/post/181d1fe1-51f8-4fc5-92ad-995859639e59/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hak_0/post/d37a1303-431b-4d66-9fe3-1079fa678bf8/image.png" alt=""></p>
<h3 id="websocket-보안-강화">WebSocket 보안 강화</h3>
<ul>
<li>보안 프로토콜 적용 (wss://)</li>
<li>DDos 공격 방어<ul>
<li>디도스: 고의로 접속량을 폭주시켜 서버를 마비시키는 것</li>
<li>IP 기반 연결 제한</li>
<li>Rate Limiting: 일정 시간 내 특정 요청 횟수 초과 시 차단</li>
<li>Cloudflare, AWS WAF 같은 방화벽 솔루션 활용</li>
</ul>
</li>
</ul>
<h3 id="websocket-성능-최적화">WebSocket 성능 최적화</h3>
<ul>
<li><p>WebSocket Keep-Alive 설정</p>
<ul>
<li>기본적으로 장시간 연결을 유지하므로 서버 부하 발생 가능</li>
<li>Ping/Pong 메시지를 사용하여 비활성 연결 종료</li>
<li>특정 시간마다 클라이언트와 서버 간 메시지 송수신<pre><code class="language-python">while Ture:
await websocket.send_text(&quot;ping&quot;)
await asyncio.sleep(30) # 30초마다 ping 전송</code></pre>
</li>
</ul>
</li>
<li><p>로드 밸런싱 Load Balancing</p>
<ul>
<li>하나의 서버가 많은 요청을 처리하면 서버 과부하 발생</li>
<li>여러 개의 서버를 사용하여 트래픽을 분산</li>
<li>클라이언트 요청을 여러 서버 중 하나로 자동으로 분배</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI Backgroud Tasks]]></title>
            <link>https://velog.io/@hak_0/FastAPI-Backgroud-Tasks</link>
            <guid>https://velog.io/@hak_0/FastAPI-Backgroud-Tasks</guid>
            <pubDate>Tue, 18 Feb 2025 07:39:07 GMT</pubDate>
            <description><![CDATA[<h2 id="background-tasks란">Background Tasks란</h2>
<blockquote>
<ul>
<li>사용자와 직접적인 상호작용 없이 실행되는 작업</li>
<li>API 요청을 처리한 후 별도 스레드에서 작업 실행</li>
<li><code>BackgroundTasks.add_tasks()</code>를 사용하여 작업 추가</li>
</ul>
</blockquote>
<h4 id="동기-vs-비동기-vs-백그라운드-작업-차이">동기 vs 비동기 vs 백그라운드 작업 차이</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>개념</th>
<th>실행 방식</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>동기 (Synchronous)</strong></td>
<td>요청이 끝날 때까지 다음 코드 실행 안됨</td>
<td>순차적으로 코드 실행</td>
<td>코드 흐름이 단순함</td>
<td>성능이 느림</td>
</tr>
<tr>
<td><strong>비동기 (Asynchronous)</strong></td>
<td><code>async/await</code>를 사용하여 다른 작업 실행 가능</td>
<td>비동기 I/O, 이벤트 루프 활용</td>
<td>동시성 처리 가능</td>
<td>코드가 복잡해질 수 있음</td>
</tr>
<tr>
<td><strong>백그라운드 작업 (BackgroundTasks)</strong></td>
<td>요청 응답 후에도 실행되는 별도 작업</td>
<td>FastAPI의 <code>BackgroundTasks</code> 사용</td>
<td>응답 속도가 빠름</td>
<td>실행이 보장되지 않음</td>
</tr>
</tbody></table>
<ul>
<li>백그라운드 작업 방식: 즉시 응답을 반환후, 실제 작업은 백그라운드에서 실행<ul>
<li>실행 예시 : 콘솔에선 5초후에 작업이 실행<pre><code class="language-python">from fastapi import FastAPI, BackgroundTasks
import time
</code></pre>
</li>
</ul>
</li>
</ul>
<p>app = FastAPI()</p>
<p>def background_task():
    time.sleep(5)
    print(&quot;백그라운드 작업 완료&quot;)</p>
<p>@app.get(&quot;/&quot;)
async def stsart_background_task(background_tasks: BacgroundTasks):
    background_tasks.add_task(background_task)
    return {&quot;msg&quot;: &quot;백그라운드 작업 실행 중...&quot;}</p>
<pre><code>### Background Tasks가 필요한 이유
&gt; * 이메일 전송: 사용자가 요청하면 즉시 응답을 주고, 실제 이메일 전송은 백그라운드에서 진행
&gt; * 파일 업로드: 파일을 업로드한 후, 처리는 백그라운드에서 수행
&gt; * 로그 기록: API 호출 로그를 백그라운드에서 저장
&gt; * 장기 실행 작업: 데이터 분석, PDF 생성, 크롤링 등

* 이점
  - **빠른 응답 시간**: 사용자는 즉시 응답을 받고 작업은 따로 실행
  - **서버 부하 감소**: 여러 요청이 들어와도 메인 스레드가 차단되지 않음
  - **유저 경험 개선**: 이메일 전송이나 데이터 저장 작업이 끝날 때까지 기다릴 필요 없음

### FastAPI의 Background Tasks 작동 방식

1. API 요청을 받음
2. add_task()에 해당 요청 처리
3. FastAPI는 일단 즉시 응답을 반환
4. add_task()에 대한 내용이 백그라운드에서 실행됨
5. 그 외 서버가 다른 요청을 처리하는 동안 작업이 진행됨

### Background Tasks 주의점
* 백그라운드 작업이 **실패**해도 클라이언트는 **모름**
  - 작업이 실패해도 응답은 이미 전송됨
  - 해결책: 작업 결과를 데이터베이스에 저장하고 후속 확인 API 제공

* 서버 **재시작** 시 작업이 **사라짐**
  - 서버가 재시작되면 미완료된 백그라운드 작업이 유실됨
  - 해결책: Celery같은 작업 큐 사용 고려

* 대량의 백그라운드 작업은 성능 저하 가능성
  - 한 번에 너무 많은 작업이 들어오면 서버 성능 저하
  - 해결책: 워커 프로세스를 활용하여 작업 분산

### 🧑‍💻 예시

* 이메일 전송 : 즉시 응답 주고, 이메일 전송은 백그라운드에서 진행
```python
from fastapi import FastAPI, BackgroundTasks 
import time
app = FastAPI()

def send email(email: str, message: str) :
    time.sleep(5)
    pirnt(f&quot;이메일 전송 완료: {email}, 내용: {message}&quot;)
@app.post(&quot;/send-email/&quot;)
async def send_email_endpoint(email: str, message: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email, email, message)
    return {&quot;message&quot;: &quot;이메일 전송 중...&quot;}</code></pre><ul>
<li>로그 기록 : 요청이 끝난 후 로그가 저장 됨<pre><code class="language-python">def write_log(log_message: str):
  with open(&quot;log.txt&quot;, &quot;a&quot;) as log_file: # append 추가모드
  log_file.write(log_message + &quot;\n&quot;)</code></pre>
</li>
<li>사용자 활동 기록: DB에 사용자 활동을 백그라운드에서 기록<pre><code class="language-python"># database.py
from sqlalchemy import Column, Integer, String, create_engine 
from sqlalchemy.orm import sessionmaker, declarative_base
</code></pre>
</li>
</ul>
<p>DATABASE_URL = &quot;sqlite:///./test.db&quot;
engine = create_engine(DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False}) # sqlite 전용 설정
SessionLocal = sessionmaker(autocommit=False, autoflush=False,bind=engine)
Base = declarative_base()</p>
<p>class UserActivity(Base):
    <em>tablename</em> = &quot;user_activities&quot;
    id = Column (Integer, primary_key=True, index=True)
    username = Column (String, index=True)
    action = Column (String)</p>
<p>Base.metadata.create_all (bind=engine)</p>
<p>def get_db() :
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()</p>
<h1 id="mainpy">main.py</h1>
<p>from fastapi import FastAPI, BackgroundTasks, Depends
from sqlalchemy.orm import Session 
from database import get_db, UserActivity
app = FastAPI()</p>
<p>def save_activity(username: str, action: str, db: Session):
    new_activity = UserActivity(username=username, action = action)
    db.add(new_activity)
    db.commit()</p>
<p>@app.post(&quot;/track-activity/&quot;)
async def track_activity(username: str, action: str, background_tasks: BackgroundTasks, db: Session=Depends(get_db)):
    background_tasks.add_task(save_activity, username, action, db)</p>
<pre><code>
* backtracking 예시(파일 업로드)
```python
# database.py
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import Column, Integer, String, create_engine


DATABASE_URL = &quot;sqlite:///./test.db&quot;

engine = create_engine(
    DATABASE_URL,
    connect_args={&quot;check_same_thread&quot;: False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

class FileRecord(Base):
    __tablename__ = &quot;items&quot;
    id = Column(Integer, primary_key=True, index=True)
    filename = Column(String, index=True)
    status = Column(String)

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# main.py

from fastapi import BackgroundTasks, Depends, FastAPI, File, UploadFile
from pydantic import BaseModel
from sqlalchemy.orm import Session
import os

from database import get_db, FileRecord


app = FastAPI()

UPLOAD_DIR = &quot;uploads&quot;
# 지정된 경로의 폴더를 생성하는 함수
# 해당폴더가 이미 존재해도 에러발생 x
os.makedirs(UPLOAD_DIR, exist_ok=True)

# os.path.join(UPLOAD_DIR, filename)
# UPLOAD_DIR 디렉토리 안에 filename 파일을 저장할 경로를 만듭니다
# os.path.join()을 사용하면 OS에 따라 적절한 경로 구분자를 자동으로 적용합니다.

# &quot;wb&quot; 모드는 바이너리 쓰기 모드
# 텍스트 파일이 아닌 이미지, 동영상, PDF 같은 바이너리 파일을 저장할 때 사용
def save_file(filename: str, content: bytes):
    with open(os.path.join(UPLOAD_DIR, filename), &quot;wb&quot;) as f:
        f.write(content)
    print(f&quot;파일 저장 완료: {filename}&quot;)

# ValueError: I/O operation on closed file. 발생
# 로컬에서 파일으 업로드하기에 경로를 명시적으로 지정을 안하면 인식을 못함, 아래 비추
# def save_fiel(file: UploadFile):
#     with open(f&quot;uploads/{file.filename}&quot;, &quot;wb&quot;) as f:
#         f.write(file.file.read())
#     pritn(f&quot;파일 저장 완료: {file.filename}&quot;)

def log_file_upload(filename: str, db: Session):
    new_record = FileRecord(filename=filename, status=&quot;Saved&quot;)
    db.add(new_record)
    db.commit()
    print(f&quot;로그 저장 완료: {filename}&quot;)

@app.post(&quot;/upload-file/&quot;)
async def upload_file(file: UploadFile = File(...),
                background_tasks: BacgroundTasks = BackgroundTasks(),
                db: Session = Depends(get_db)):
    file_contet = await file.read() # 파일을 미리 읽어서 백그라운드 작업으로 전달
    background_tasks.add_task(save_file,file.filename, file_content)
    bacgroound_tasks.add_task(log_file_upload, file.filename, db)

    return {&quot;msg&quot; : &quot; 파일 업로드 중 ...&quot;}</code></pre><ul>
<li>DB에 넣으려면, 테이블에 추가 (<code>LargeBinary</code>)<pre><code class="language-python">from fastapi import FastAPI, UploadFile, File, HTTPException
from sqlalchemy import Column, Integer, LargeBinary, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
</code></pre>
</li>
</ul>
<p>app = FastAPI()</p>
<p>Base = declarative_base()
DATABASE_URL = &quot;sqlite:///./test.db&quot;
engine = create_engine(DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False})
SessionLocal = sessionmaker(bind=engine)</p>
<p>class FileData(Base):
    <strong>tablename</strong> = &quot;files&quot;
    id = Column(Integer, primary_key=True, autoincrement=True)
    data = Column(LargeBinary)</p>
<p>Base.metadata.create_all(bind=engine)</p>
<p>@app.post(&quot;/upload/&quot;)
async def upload_file(file: UploadFile = File(...)):
    db: Session = SessionLocal()
    file_content = await file.read()</p>
<pre><code>new_file = FileData(data=file_content)
db.add(new_file)
db.commit()
db.refresh(new_file)
db.close()

return {&quot;file_id&quot;: new_file.id}</code></pre><p>@app.get(&quot;/download/{file_id}&quot;)
async def download_file(file_id: int):
    db: Session = SessionLocal()
    file_data = db.query(FileData).filter(FileData.id == file_id).first()
    db.close()</p>
<pre><code>if not file_data:
    raise HTTPException(status_code=404, detail=&quot;File not found&quot;)

return {&quot;file_content&quot;: file_data.data}  # 실제 서비스에서는 파일을 반환하는 방식 사용</code></pre><pre><code>* api 코드는 동일함 save 할때 그냥 contet를 추가적으로 넣으면됨
```python
new_record = FileRecord(filename=filename, status=&quot;Saved&quot;, file = content)</code></pre><ul>
<li><p>DB에서 이미지 불러오기, 똑같이 가지고 오면되지만 return값에, <code>media_type =&quot;png/image&quot;</code>를 넣어준다</p>
<pre><code class="language-python">@app.get(&quot;/image/{image_id}&quot;)
async def get_image(image_id: int):
  db: Session = SessionLocal()
  image_data = db.query(ImageData).filter(ImageData.id == image_id).first()
  db.close()

  if not image_data:
      raise HTTPException(status_code=404, detail=&quot;Image not found&quot;)

  return Response(content=image_data.data, media_type=&quot;image/png&quot;)</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI TestCode]]></title>
            <link>https://velog.io/@hak_0/FastAPI-TestCode</link>
            <guid>https://velog.io/@hak_0/FastAPI-TestCode</guid>
            <pubDate>Mon, 17 Feb 2025 09:44:24 GMT</pubDate>
            <description><![CDATA[<h2 id="테스트-코드">테스트 코드</h2>
<blockquote>
<p>테스트가 필요한 이유</p>
<ul>
<li>코드 변경 시 기존 기능이 정상적으로 작동하는지 보장하기 위해</li>
<li>자동화된 테스트를 통해 버그를 빠르게 발견하고 수정 가능</li>
<li>특히 API 개발에서는 요청/응답을 검증하는 것이 중요</li>
</ul>
</blockquote>
<ol>
<li>유닛테스트</li>
<li>모델 테스트</li>
<li>라우터 테스트</li>
<li>통합 테스트</li>
</ol>
<h4 id="pytest">pytest</h4>
<blockquote>
<p>Python의 대표적인 테스트 프레임워크</p>
<ul>
<li><code>pip install pytest</code></li>
</ul>
</blockquote>
<h4 id="httpx">httpx</h4>
<blockquote>
<p>FastAPI에서 requests 대신 사용하는 비동기 HTTP 클라이언트</p>
</blockquote>
<ul>
<li><code>pip install httpx</code></li>
</ul>
<h4 id="testclient">TestClient</h4>
<blockquote>
<p>FastAPI 애플리케이션에 직접 요청을 보내고 응답 검증</p>
</blockquote>
<h3 id="종류">종류</h3>
<ul>
<li>단위 테스트(Unit Test) : 개발 함수, 모듈 단위로 동작을 검증</li>
<li>통합 테스트(Integration Test) : 여러 모듈이 함꼐 동작할 때의 결과를 검증</li>
<li>엔드투엔드 테스트(E2E Test) : 실제 사용자 시나리오를 기반으로 전체 시스템을 테스트</li>
</ul>
<h3 id="환경구성">환경구성</h3>
<blockquote>
<ul>
<li>pytest 및 httpx 패키지 설치 : <code>pip install pytest httpx</code></li>
<li>FastAPI 설정: 테스트 하고싶은 엔드포인트 작성 : 파일명 main.py</li>
<li>테스트 코드 실행, <code>pytest test_main.py</code></li>
</ul>
</blockquote>
<ul>
<li>기본적인 pytest 테스트 코드 작성<ul>
<li>test_main.py<pre><code class="language-python">from fastapi.testclient import TestClient
from main import app
</code></pre>
</li>
</ul>
</li>
</ul>
<p>client = TestClient(app)</p>
<p>def test_ping():
    response = client.get(&quot;/ping&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;msg&quot;: &quot;pong&quot;}</p>
<h1 id="pytest-test_mainpy">pytest test_main.py</h1>
<pre><code>
### 테스트 시나리오

#### 존재하지 않는 엔드포인트 테스트
```python
def test_not_found():
    response = client.get(&quot;/invalid-endpoint&quot;)
    assert response.staus_code == 404</code></pre><h4 id="필수-파라미터가-없을때-예외-처리">필수 파라미터가 없을때 예외 처리</h4>
<pre><code class="language-python">@app.get(&quot;/greet/&quot;)
async def greet(name: str):
    return {&quot;msg&quot; : f&quot;Hello, {name}!&quot;}

def test_greet():
    response = client.get(&quot;/greet/&quot;, params = {&quot;name&quot; : &quot;Alice&quot;})
    assert response.status_code == 200
    assert response.json() == {&quot;msg&quot; : &quot;Hello, Alice!&quot;}

def test_greet_missing_param():
    response = client.get(&quot;/greet/&quot;)
    assert response.status_code == 422</code></pre>
<h3 id="단위-테스트-unit-test">단위 테스트 (Unit Test)</h3>
<ul>
<li><p>단위 테스트란?</p>
<ul>
<li>코드의 개별 기능(함수, 클래스, 모듈) 단위로 동작을 검증하는 테스트</li>
<li>데이터베이스, 외부 API 호출 등 의존성이 없는 상태에서 실행됨</li>
</ul>
</li>
<li><p>단위 테스트의 필요성</p>
<ul>
<li>코드 변경 후에도 기능이 정상적으로 유지되는지 보장</li>
<li>버그를 조기에 발견하여 수정 비용 절감</li>
<li>빠른 실행 속도로 자주 실행 가능</li>
</ul>
</li>
<li><p><strong>최대한 많은 케이스를 찾아서 테스트</strong></p>
</li>
</ul>
<table>
<thead>
<tr>
<th>검증 항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>정상적인 API 동작</td>
<td>요청이 정상적으로 처리되는지 확인</td>
</tr>
<tr>
<td>잘못된 입력값 테스트</td>
<td>잘못된 데이터 입력 시 FastAPI가 예외를 올바르게 처리하는지 검증</td>
</tr>
<tr>
<td>엣지 케이스 (경계값)</td>
<td>0, 큰 숫자, 빈 값 등 예상치 못한 입력 처리</td>
</tr>
<tr>
<td>FastAPI의 의존성 주입 테스트</td>
<td>API 엔드포인트에서 의존성 주입(Depends)을 활용한 테스트</td>
</tr>
</tbody></table>
<pre><code class="language-python">from fastapi import FastAPI
    app = FastAPI ()
def subtract(a: int, b: int):
    # 빼기 연산 함수
    return {&quot; result&quot;: a - b}
@app.get(&quot;/math/subtract&quot;)
def subtract_route(a: int, b: int):
# FastAPI 엔드포인트 (subtract API)
    return subtract(a, b)</code></pre>
<pre><code>● 정상적인 뺄셈
● 음수 포함 뺄셈
● 0을 포함한 뺄셈
● 잘못된 입력값
● 경계값 (최소, 최대값)</code></pre><h4 id="depends를-활용한-단위-테스트">Depends를 활용한 단위 테스트</h4>
<pre><code class="language-python">app = FastAPI()

def get_default_number():
    # 의존성 함수
    return 10

def subtract(a: int, b:int, default= int = Depends(get_default_number)):
    return {&quot;result&quot; : a-b-default}

@app.get(&quot;/math/subtract&quot;)
async def subtract_endpoint(a: int, b:int, default= int = Depends(get_default_number)):
    return subtract(a, b, default)</code></pre>
<ul>
<li>의존성 주입 함수에 dependency_overrides로 테스트 데이터를 덮어씌움<h2 id="통합-테스트-integration-test">통합 테스트 (Integration Test)</h2>
<blockquote>
<ul>
<li>통합 테스트의 필요성<ul>
<li>단위 테스트만으로는 모듈 간 연결이 올바르게 동작하는지 확인할 수 없음</li>
<li>API와 데이터베이스 간의 데이터 흐름이 정상적으로 이루어지는지 검증
필요</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li>다양한 API 요청 조합을 테스트하여 전체 시스템이 예상대로 동작하는지
확인</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>단위 테스트 (Unit Test)</th>
<th>통합 테스트 (Integration Test)</th>
</tr>
</thead>
<tbody><tr>
<td>테스트 대상</td>
<td>개별 함수, 모듈</td>
<td>여러 모듈, DB, API 간 연동</td>
</tr>
<tr>
<td>실행 속도</td>
<td>빠름</td>
<td>상대적으로 느림</td>
</tr>
<tr>
<td>의존성</td>
<td>없음 (Mock 사용)</td>
<td>실제 DB, API 필요</td>
</tr>
<tr>
<td>목적</td>
<td>코드 로직 검증</td>
<td>전체 흐름 테스트</td>
</tr>
</tbody></table>
<pre><code class="language-python"># main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from database import get_db, Product

app = FastAPI()

# 요청 데이터 모델 정의
class ProductCreate(BaseModel):
    name: str
    price: int

# 제품 추가 API
@app.post(&quot;/products/&quot;)
async def create_product(product: ProductCreate, db: Session = Depends(get_db)):
    product = Product(name=product.name, price=product.price)
    db.add(product)
    db.commit()
    db.refresh(product)
    return product

# 제품 조회 API
@app.get(&quot;/products/{product_id}&quot;)
async def read_product(product_id: int, db: Session = Depends(get_db)):
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404, detail=&quot;Product not found&quot;)
    return product

# database.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = &quot;sqlite:///./test.db&quot;

engine = create_engine(DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class Product(Base):
    __tablename__ = &quot;products&quot;
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    price = Column(Integer)

Base.metadata.create_all(bind=engine)

# DB 세션 의존성
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# test_main.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from database import Base, get_db, Product
from main import app

# 테스트용 데이터베이스 설정
SQLALCHEMY_DATABASE_URL = &quot;sqlite:///./test.db&quot;
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={&quot;check_same_thread&quot;: False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

client = TestClient(app)

# 한 번만 실행하여 전체 테스트에서 DB를 공유
@pytest.fixture(scope=&quot;module&quot;)
def setup_test_db():
    Base.metadata.create_all(bind=engine)  # 새로 생성
    db = TestingSessionLocal()

    # 초기 데이터 삽입
    test_product = Product(id=1, name=&quot;Laptop&quot;, price=1500)
    db.add(test_product)
    db.commit()

    yield db  # 모든 테스트에서 공유됨

    db.close()
    Base.metadata.drop_all(bind=engine)  # 모든 테스트 종료 후 DB 삭제

# 제품 추가 API 테스트
def test_create_product(setup_test_db):
    response = client.post(&quot;/products/&quot;, json={&quot;name&quot;: &quot;Phone&quot;, &quot;price&quot;: 800})
    assert response.status_code == 200
    assert response.json()[&quot;name&quot;] == &quot;Phone&quot;
    assert response.json()[&quot;price&quot;] == 800

# 존재하는 제품 조회 테스트 (Laptop &amp; Phone 조회 가능)
def test_read_product(setup_test_db):
    response = client.get(&quot;/products/1&quot;)  # ID=1 Laptop 조회
    assert response.status_code == 200
    assert response.json()[&quot;name&quot;] == &quot;Laptop&quot;
    assert response.json()[&quot;price&quot;] == 1500

    response = client.get(&quot;/products/2&quot;)  # ID=2 Phone 조회
    assert response.status_code == 200
    assert response.json()[&quot;name&quot;] == &quot;Phone&quot;
    assert response.json()[&quot;price&quot;] == 800

# 존재하지 않는 제품 조회 테스트 (404 반환)
def test_read_product_not_found(setup_test_db):
    response = client.get(&quot;/products/999&quot;)  # 존재하지 않는 ID 요청
    assert response.status_code == 404
    assert response.json()[&quot;detail&quot;] == &quot;Product not found&quot;</code></pre>
<h2 id="테스트-코드-최적화-개념">테스트 코드 최적화 개념</h2>
<ul>
<li>테스트 코드가 많아질수록 발생하는 문제<ul>
<li>실행 속도가 느려짐</li>
<li>중복 코드가 많아짐</li>
<li>테스트 케이스가 늘어남</li>
</ul>
</li>
</ul>
<h3 id="테스트-최적화를-위한-핵심-기법">테스트 최적화를 위한 핵심 기법</h3>
<table>
<thead>
<tr>
<th>기법</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Mocking</strong></td>
<td>DB나 외부 API 호출 없이 가짜 데이터를 반환하도록 처리</td>
</tr>
<tr>
<td><strong>Fixture</strong></td>
<td>반복된 설정을 줄여 코드 재사용성 향상</td>
</tr>
<tr>
<td><strong>Parametrize</strong></td>
<td>같은 테스트를 여러 입력값으로 실행</td>
</tr>
<tr>
<td><strong>성능 측정</strong></td>
<td>테스트 실행 속도를 체크하여 병목 구간 식별</td>
</tr>
</tbody></table>
<h3 id="mocking">Mocking</h3>
<blockquote>
<ul>
<li>Mocking: DB나 외부 API 호출 없이 가짜 데이터를 반환하도록 처리<ul>
<li>실제 데이터베이스와의 연결 없이 API 로직만 검증 가능</li>
<li>테스트 시 불필요한 DB 접근을 막고, 실행 속도를 개선</li>
</ul>
</li>
<li>컴퓨터 입장에서 실제 DB를 연결하지 않고 뒤에 값이 저걸 쓰려나 보다 라고 생각을함</li>
<li>그래서 부하가 안걸림, 기존의 사용에 <code>.return_value</code> 만 붙여주면 됨</li>
</ul>
</blockquote>
<table>
<thead>
<tr>
<th>Mocking 방식</th>
<th>사용 예제</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>MagicMock()</strong></td>
<td><code>mock_db = MagicMock()</code></td>
<td>객체를 가짜로 생성하여 동작을 정의 가능</td>
</tr>
<tr>
<td><strong>patch()</strong></td>
<td><code>@patch(&quot;main.get_db&quot;)</code></td>
<td>특정 함수를 가짜로 대체</td>
</tr>
<tr>
<td><strong>AsyncMock()</strong></td>
<td><code>mock_async = AsyncMock()</code></td>
<td>비동기 함수(Mock)를 지원</td>
</tr>
</tbody></table>
<ul>
<li><p>patch 에서 앞의 main에 불러와진 모듈은 전부 사용가능 그래서 get_db가 import 되어있어서 사용 가능</p>
<ul>
<li>@patch(&quot;모듈.함수&quot;) 에서 &quot;함수&quot;부분이 mock_함수명 형식으로 매칭</li>
<li>여러개의 @patch를 사용할 경우, 가장 마지막 @patch가 첫번쨰 매개변수로 등록</li>
<li>Mocking된 객체는 MagicMokc() 타입이므로, .return_value를 설정해서 동작을 정의할 수 있음</li>
<li>정의된 순서대로 아래부터 바로 적용할 함수에 순차적으로 적어야 함<pre><code class="language-python">@patch(&quot;auth.verify_password&quot;)
@patch(&quot;auth.create_token&quot;)
def test_login(mock_create_token, mock_verify_password, mock_db, username, password, expected_status):</code></pre>
</li>
</ul>
</li>
<li><p>기존방식</p>
<pre><code class="language-python">@app.get(&quot;/users/(user_id}&quot;)
async def get_user(user_id: int, db: Session = Depends (get_db)):
  user = db.query(User).filter(User.id = user_id).first()
  if not user:
      raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
  return user
</code></pre>
</li>
</ul>
<p>def test_get_user():
    response = client.get(&quot;/users/1&quot;)
    assert response.status_code ==200</p>
<pre><code>
* **Mocking 활용**
```python
@app.get(&quot;/users/(user_id}&quot;)
async def get_user(user_id: int, db: Session = Depends (get_db)):
    user = db.query(User).filter(User.id = user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
    return user

# mocking
from unittest.comk import MacigMock

def test_get_user_with_mock():
    mock_db = MagicMock()
    mock_db.query.return_value.filter.return_value.first.return_value= User(id=1, name=&quot;Mock User&quot;, email = &quot;mock@example.com&quot;)

    response = client.get(&quot;/users/1&quot;, dependencies={&quot;db&quot;: mock_db})
    assert response.status_code == 200
    assert response.json()[&quot;name&quot;] == &quot;Mock User&quot;

# patch
@patch(&quot;main.get_db&quot;) # 데이터 베이스가 있는거 같다 그냥 생각을 하게 만듬 컴퓨터에(실제 DB가 없어도 되는거임)
def test_get_user_with_mock(mock_get_db):
    mock_db = MagicMock()
    mock_db.query.return_value.filter.return_value.first.return_value =User(id=1, name=&quot;Mock User&quot;, email =&quot;mock@example.com&quot;)
    # Patch를 적용하면서 FastAPI의 의존성 오버라이드 설정
    mock_get_db.return_value = mock_db
    app.dependency_overrides[get_db] = lambda: mock_db

    response = client.get(&quot;/users/1&quot;)

    assert response.status_code == 200
    assert response.json()[&quot;name&quot;] == &quot;Mock User&quot;

    # 테스트 완료 후 의존성 초기화
    app.dependency_overrides.clear()</code></pre><ul>
<li><strong>AsyncMock</strong><blockquote>
<ul>
<li>API 최적화를 위해 사용 할 경우도 있음 대부분 MagicMock 으로 처리</li>
</ul>
</blockquote>
</li>
</ul>
<pre><code class="language-python">from unittest.mock import AsyncMock

mock_db = AsyncMock()  
mock_db.query.return_value.filter.return_value.first.return_value = User(id=1, name=&quot;Async Mock User&quot;, email=&quot;asycn@example.com&quot;)</code></pre>
<h3 id="fixture">Fixture</h3>
<blockquote>
<ul>
<li>Fixture: 테스트 실행 전 공통 환경을 설정하는 기능<ul>
<li>여러 테스트에서 중복된 코드 없이 동일한 설정 공유</li>
<li>DB 초기화, API 클라이언트 생성, 기본 데이터 삽입 등에 사용</li>
</ul>
</li>
<li>공통 환경을 설정하는 기능</li>
</ul>
</blockquote>
<ul>
<li>Fixture의 scope 설정</li>
</ul>
<table>
<thead>
<tr>
<th>Scope 옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>function</strong></td>
<td>각 테스트 함수 실행 전후로 초기화 (기본값)</td>
</tr>
<tr>
<td>class</td>
<td>클래스 단위로 실행되며, 클래스 내부의 모든 테스트에서 공유</td>
</tr>
<tr>
<td><strong>module</strong></td>
<td>파일(.py 파일) 단위로 실행되며, 한 번만 초기화 후 공유</td>
</tr>
<tr>
<td>session</td>
<td>pytest 실행 시 한 번만 실행 (가장 긴 수명)</td>
</tr>
</tbody></table>
<ul>
<li><p>function : 각각의 함수에 각각의 데이터베이스를 세팅</p>
<ul>
<li>한 함수가 끝나면 그 데이터베이스가 초기화 됨</li>
</ul>
</li>
<li><p>module : 중복된 데이터를 줄이면서 테스트 하기위해, 한 파일안에서 중복된 데이터 사용</p>
</li>
<li><p>예시</p>
<pre><code class="language-python">def test_create_user():
  db = TestingSessionLocal()
  Base.metadata.create_all(bind=engine)
  ...
  Base.metadata.drop_all(bind_engine)
  db.close()
</code></pre>
</li>
</ul>
<h1 id="변경">변경</h1>
<p>import pytest</p>
<p>@pytest.fixture(scope=&quot;module&quot;)
def test_db():
    &quot;&quot;&quot; 한 번만 실행하여 전체 테스트에서 공유하는 데이터베이스 설정 &quot;&quot;&quot;
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    yield db # 모든 테스트에서 공유됨
    db.close()
    Base.metadata.drop_all(bind=engine) # 모든 테스트 종료 후 DB 삭제</p>
<pre><code>
### Parametrize
&gt; * Parametrize: 동일한 테스트를 다양한 입력값으로 실행할 때 사용
&gt;   - @pytest.mark.parametrize를 활용하여 자동 반복 실행
&gt;   - 코드 중복 없이 테스트를 간결하게 유지 가능

* 데코레이터 밑의 인덱스로 처리된 값들을 데코레이터 안의 파라미터에 맞춰 데이터들을 순차적으로 전부 테스트함

* 기존방식
```python
@app.get(&quot;/math/add&quot;)
async def add_numbers(a: int, b: int):
    return {&quot;result&quot; : a + b}

def test_add_numbers_1():
    response = client.get(&quot;/math/add?a=3&amp;b=5&quot;)
    assert response.status_code == 200
    assert response.json()[&quot;result&quot;] == 8
def test_add_numbers_2():
    response = client.get(&quot;/math/add?a=-2&amp;b=7&quot;)
    assert response.status_code == 200
    assert response.json()[&quot;result&quot;] == 5</code></pre><ul>
<li>Parametrize 적용<pre><code class="language-python">import pytest
@pytest.mark.parametrize(&quot;a, b, expected&quot;, [
  (3, 5, 8),
  (-2, 7, 5),
  (10, -10, 0),
  (0, 0, 0)
])
def test_add_numbers(a, b, expected):
  response = client.get(f&quot;/math/add?a={a}&amp;b={b}&quot;)
  assert response.status_code == 200
  assert response.json()[&quot;result&quot;] == expected
</code></pre>
</li>
</ul>
<p>@app.post(&quot;/users/&quot;)
async def create_user(user: dict):
    return {&quot;id&quot;: 1, &quot;name&quot;: user[&quot;name&quot;], &quot;email&quot;: user[&quot;email&quot;]}</p>
<p>@pytest.mark.parametrize(&quot;user_data, expected_status&quot;, [
    ({&quot;name&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;<a href="mailto:alice@example.com">alice@example.com</a>&quot;}, 200),
    ({&quot;name&quot;: &quot;Bob&quot;}, 422), # 이메일 누락
    ({&quot;email&quot;: &quot;invalid.com&quot;}, 422), # 이름 누락 및 이메일 형식 오류
])
def test_create_user(user_daa, expected_status):
    response = client.post(&quot;/users/&quot;, josn=user_data)
    assert response.status_code == expected_status</p>
<pre><code>
### 성능 측정
&gt; * 테스트 실행 시간이 길어질 경우, 특정 API의 성능을 분석해야 함
&gt;   - time.time()을 사용하여 API 응답 시간을 측정
&gt;   - 특정 API가 성능 저하를 일으키는지 파악 가능\

```python
import time

def test_api_performance():
    start_time = time.time()
    response = client.get(&quot;/math/add?a=10000&amp;b=20000&quot;)
    end_time = time.time()

    assert response.status_code == 200
    assert response.json()[&quot;result&quot;] == 30000
    print(f&quot;API 실행 시간: {end_time -start_time:.5f}초&quot;}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI Pydantic 고급]]></title>
            <link>https://velog.io/@hak_0/FastAPI-Pydantic-%EA%B3%A0%EA%B8%89</link>
            <guid>https://velog.io/@hak_0/FastAPI-Pydantic-%EA%B3%A0%EA%B8%89</guid>
            <pubDate>Sun, 16 Feb 2025 15:50:57 GMT</pubDate>
            <description><![CDATA[<h2 id="pydantic-고급">pydantic 고급</h2>
<h3 id="tip">Tip</h3>
<ul>
<li>email : EmailStr | None = None<ul>
<li>EmailStr 이메일 형식 검증, pydnatic 모듈에 있음</li>
<li>Or none 선택적이라고 명시하는것 Optional 과 같음 3.10 버전 이후부터 존재</li>
<li>EamilStr = None 이랑 같음</li>
</ul>
</li>
</ul>
<h4 id="✅-1-pydantic-vs-django-serializer-비교">✅ 1. Pydantic vs Django Serializer 비교</h4>
<table>
<thead>
<tr>
<th>기능</th>
<th>FastAPI (<code>Pydantic</code>)</th>
<th>Django (<code>Serializer</code>)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>데이터 검증</strong></td>
<td>✅ 가능 (<code>BaseModel</code> 사용)</td>
<td>✅ 가능 (<code>serializers.Serializer</code> 사용)</td>
</tr>
<tr>
<td><strong>JSON 직렬화</strong></td>
<td>✅ 가능 (Python 객체 → JSON 변환)</td>
<td>✅ 가능 (<code>.data</code> 사용)</td>
</tr>
<tr>
<td><strong>DB 모델 연동</strong></td>
<td>❌ X (<code>Pydantic</code>은 DB 모델과 분리됨)</td>
<td>✅ 가능 (<code>serializers.ModelSerializer</code> 사용 가능)</td>
</tr>
<tr>
<td><strong>데이터 역직렬화</strong></td>
<td>✅ 가능 (JSON → Pydantic 객체)</td>
<td>✅ 가능 (<code>.is_valid()</code> 사용)</td>
</tr>
<tr>
<td><strong>요청 데이터 처리</strong></td>
<td>✅ <code>request.body</code> 자동 변환</td>
<td>✅ <code>request.data</code> (Django REST Framework 필요)</td>
</tr>
</tbody></table>
<p>🔹 <strong>결론:</strong>  </p>
<ul>
<li><code>Pydantic</code>은 <strong>DB 모델과 완전히 독립적</strong>이며, 요청 데이터를 검증하는 데 사용됨.  </li>
<li>Django <code>Serializer</code>는 <strong>Django 모델과 연동 가능</strong> (<code>ModelSerializer</code> 지원)  </li>
</ul>
<h3 id="pydnatic의-validator-개념-및-사용법">pydnatic의 validator 개념 및 사용법</h3>
<ul>
<li>구조<ul>
<li><code>@field_validator(&quot;&lt;필드 이름&gt;&quot;), @classmethod</code> 데코레이터를 사용하여 특정 필드의 값을 검증</li>
<li>검증 함수는 항상 클래스 메서드 형태로 정의되며, 첫 번째 인자로 필드값을 받음</li>
<li>검증 실패 시 예외를 발생시켜야 함 : ValueError, TypeError...</li>
</ul>
</li>
<li>예시<pre><code class="language-python">from pydantic import BaseModel, field_validator
</code></pre>
</li>
</ul>
<p>class ExampleModel(BaseModel):
    number: int</p>
<pre><code>@field_validator(&quot;number&quot;)
@classmethod
def validator_number(cls, value):
    if value &lt; 1 :
        raise ValueError(&quot;Number must be at least 1&quot;)
    if value &gt; 100 :
        raise ValueError(&quot;Number must not exceed 100&quot;)
    return Value</code></pre><pre><code>* 동작
  - 모델 초기화 시, 해당 필드의 값이 검증 로직을 통과해야 함
  - 통과하지 못하면 예외 메세지가 반환
```python
example = ExampleModel(number = 50)
print(example) # Output : number 50</code></pre><ul>
<li>데이터 검증이 필요한 이유<ul>
<li>사용자가 입력하는 데이터가 항상 올바르지는 않음</li>
<li><strong>보안문제</strong> : 잘못 입력된 SQL injection, XSS등 보안 취약점을 유발할 수 있음</li>
<li><strong>비즈니스 로직 보장</strong> : 이메일 형식, 전화번호 국가별 형식, 나이 제한 등</li>
</ul>
</li>
</ul>
<h2 id="고급-pydantic-검증">고급 pydantic 검증</h2>
<blockquote>
<ul>
<li>단일 필드 검증을 넘어 필드 간 관계를 고려한 검증이 필요</li>
<li>실제 서비스에서 발생할 수 있는 논리적 오류를 방지</li>
<li>보안, 데이터 정합성을 보장하기 위해 더 정교한 검증이 필요</li>
</ul>
</blockquote>
<h3 id="field_validator"><code>@field_validator</code></h3>
<ul>
<li>비밀번호 검증<pre><code class="language-python">from pydantic import BaseModel, field_validator, ValidationError
</code></pre>
</li>
</ul>
<p>special_word = &quot;!@#$%^&amp;*()-_+=&quot;</p>
<p>class UserRegister(BaseModel):
    username : str
    password : str</p>
<pre><code>@field_validator(&quot;password&quot;)
@classmethod
def validate_password(cls, value):
    if len(value) &lt; 8:
        raise ValueError(&quot;password must be at least 8 characters&quot;)
    flag = False
    for char in value:
        if char.isupper():
            flag = True
            break
    if count == 0 :
        raise ValueError(&quot;password must contain at least one uppercase letter&quot;)

    if not any(char in special_word for char in value):
        raise ValueError(&quot;password must contain at least one special character&quot;)

    return value</code></pre><pre><code>
* re 정규식 사용
```python
class UserRegister(BaseModel):
    username: str
    password: str

    @field_validator(&quot;password&quot;)
    @classmethod
    def validate_password(cls, value):
        if len(value) &lt; 8:
            raise ValueError(&quot;Password must be at least 8 characters long&quot;)
        if not re.search(r&quot;[A-Z]&quot;, value):
            raise ValueError(&quot;Password must contain at least one uppercase letter&quot;)
        if not re.search(r&quot;\d&quot;, value):
            raise ValueError(&quot;Password must contain at least one number&quot;)
        if not re.search(r&quot;[!@#$%^&amp;*(),.?\&quot;:{}|&lt;&gt;]&quot;, value):
            raise ValueError(&quot;Password must contain at least one special character&quot;)
        return value</code></pre><h3 id="mode-model_validator">mode, <code>@model_validator</code></h3>
<blockquote>
<p>poetry add email-validator</p>
</blockquote>
<ul>
<li><p>before</p>
<ul>
<li>시점: 데이터 유효성 검증 전에 실행(데이터 받기전에 변환시키고)</li>
<li>데이터 접근 방식: dict 형태로 원시 데이터 접근</li>
<li>예제: 입력데이터 변환(소문자변환, 공백 제거, ...), 타입변환(str -&gt; int)</li>
</ul>
</li>
<li><p>after</p>
<ul>
<li>시점: 데이터 유효성 검증 후 실행(데이터 받고 검사)</li>
<li>데이터 접근 방식: self 객체를 사용해 필드값 접근</li>
<li>예제: 여러 필드간 검증, 값의 논리적 검증, 비즈니스 로직 반영(role 기반 접근 제한)</li>
</ul>
</li>
</ul>
<pre><code class="language-python">class UserRegister(BaseModel):
    username : str
    password : str
    confirm_password : str

    @model_validator(mode=&quot;after&quot;)
    def check_password_match(self):
        if self.password != self.confirm_password:
            raise ValueError(&quot;Passwords do not match&quot;)
        return self

from pydantic import BaseModel, model_validator, Field, EmailStr

class ContactInfo(BaseModel):
    email : EmailStr | None = None
    phone_number : str | None = None # Optional , 선택적이란 의미

    @model_validator(mode=&quot;after&quot;)
    def exist_email_phone(self):
        if self.email is None and self.phone_number is None:
            raise ValueError(&quot;Must have email or phone_number&quot;)
        return self


# no_email = ContactInfo(phone_number=&quot;phone_number&quot;)
no_phone_number = ContactInfo(email=&quot;email@email.com&quot;)
# no = ContactInfo()
print(no_phone_number.email)

    @model_validator(mode=&quot;before&quot;)
    @classmethod
    def preprocess_username(cls, data):
        #  객체가 특정 클래스(또는 데이터 타입)의 인스턴스인지 확인하는 함수
        if isinstance(data, dict) and &quot;username&quot; in data:
            data[&quot;username&quot;] = data[&quot;username&quot;].lower()
        return data</code></pre>
<h3 id="computed_field"><code>@computed_field</code></h3>
<blockquote>
<ul>
<li>자동계산 필드<ul>
<li>입렵값 기반으로 자동 계산되는 필드<ul>
<li>나이대신 생년원일을 입력받고 자동으로 계산</li>
<li>정가와 할인율을 입력하면 할인가를 자동으로 계산</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-python">from pydantic import BaseModel, computed_field
from datetime import datetime

class User(BaseModel):
    name : str
    birth_year : int

    @computed_field
    @property
    def age(self) -&gt; int:
    return datetime.now().yaer - self.birth_year

user = User(name=&quot;Alice&quot;, brith_year = 2000)
print(user.age) # output : 25</code></pre>
<h3 id="초기값-동적-설정-default_factory">초기값 동적 설정, <code>default_factory</code></h3>
<blockquote>
<ul>
<li>초기값을 동적으로 설정할 떄 사용<ul>
<li>자동증가 ID</li>
<li>created_at을 현재시간으로 설정</li>
<li>verification code를 위해 6개의 랜덤숫자 설정</li>
</ul>
</li>
<li>default_factory는 해당 필드에 값이 제공되지 않았을 때, 기본적으로 어떤 값을 넣을지를 지정하는 기능</li>
<li>기본값을 제공하지 않으면 실행되는 기능</li>
</ul>
</blockquote>
<pre><code class="language-python">from pydantic import BaseModel, Field
from datetime import datetime

class LogEntry(BaseModel):
    message: str
    created_at: datetime = Field(default_factory=datetime.utcnow)

log1 = LogEntry(message=&quot;System started&quot;)
print(log1.created_at) # 자동생성된 UTC 시간

# 
import uuid
from datetime import datetime
from pydantic import BaseModel, Field

class User(BaseModel):
    # uuid.uuid4() : 랜덤한 UUID 생성
    user_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    name: str
    role: str = Field(default=&quot;user&quot;)
    created_at : str = Field(default_factory=lambda: datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;))

user1 = User(name=&quot;alice&quot;)
print(user1)

#
import random
from datetime import datetime, timedelta
from pydantic import BaseModel, Field, field_serializer

class Otp(BaseModel):
    phone_number : str
    otp: int=Field(default_factory=lambda: random.randint(100000,999999))
    otp_expiry: str=Field(default_factory=lambda : (datetime.now()+ timedelta(minutes=5)).isoformat())

    # @field_serializer(&quot;otp_expiry&quot;)
    # def serialize_datetime(self, value: datetime) -&gt; str:
    #     return value.isoformat()


otp = Otp(phone_number=&quot;010&quot;)

print(otp)</code></pre>
<h3 id="여러타입-지원">여러타입 지원</h3>
<blockquote>
<ul>
<li>특정 필드에 여러 타입 지원<ul>
<li>product_id가 int또는 str일 경우</li>
<li>score가 int또는 float일 경우</li>
</ul>
</li>
<li><code>Union</code> 사용</li>
</ul>
</blockquote>
<pre><code class="language-python">from pydantic import
from typing import Union

class Product(BaseModel):
    product_id: Union[int, str]
    name: str

p1 = Product(product_id=123, name=&quot;Laptop&quot;)
p2 = Product(product_id=&quot;XYZ-456&quot;, name=&quot;Phone&quot;)

print(p1.product_id) # 123
print(p2.product_id) # &quot;XYZ-456&quot;</code></pre>
<h3 id="직렬화-json_encoders-field_serializer">직렬화, <code>json_encoders</code>, <code>@field_serializer</code></h3>
<blockquote>
<ul>
<li>직렬화 Serialization: 다양한 종류의 데이터를 기계가 쓰고 읽기 편리하게 나타낸 방식<ul>
<li>기본적으로 datetime, Decimal 등은 JSON으로 직렬화할 수 없음</li>
<li>API응답에서 날짜 형식을 통일하거나 소수점 자리수 제한할 때 필요</li>
</ul>
</li>
<li>json_encoders는 Pydantic에서 특정 데이터 타입을 JSON으로 변환할 때 사용하는 커스텀 인코더</li>
<li>datetime을 직접 str 형식으로 변환해서 JSON으로 출력하고 싶을 때 사용</li>
<li>기본 FastAPI 서버내에서는 기본값 유지</li>
</ul>
</blockquote>
<pre><code class="language-python">from pydantic import BaseModel
from datetime import datetime

class Order(BaseModel):
    order_id: int
    total_price: float
    created_at: datetime

    # class Config:
    #     json_encoders = {
    #         datetime: lambda v: v.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)
    #     }

    @field_serializer(&quot;created_at&quot;)
    def serialize_datetime(self, value: datetime) -&gt; str:
        return value.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)

order = Order(order_id=1, total_price=100.5, created_at = datetime.now())
print(order.model_dump_json())</code></pre>
<h3 id="응답-필터링-response_model_exclude">응답 필터링, <code>response_model_exclude</code></h3>
<blockquote>
<ul>
<li>응답에서 민감한정보(비밀번호, API Key 등)을 숨기고 싶을 떄</li>
<li>유저 권한에 따라 보여줄 데이터 조절<ul>
<li>admin은 모든 필드</li>
<li>일반 유저는 일부 피륻</li>
</ul>
</li>
</ul>
</blockquote>
<pre><code class="language-python">from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    username: str
    email: str
    password: str

@app.get(&quot;/users/me&quot;, response_model=User, response_model_exclude={&quot;password&quot;})
async def get_user():
    user_data = {&quot;username&quot;:&quot;testuser&quot;,&quot;email&quot;:&quot;test@test.com&quot;,&quot;password&quot;:&quot;mypassword&quot;}
    return User(**user_data)

# username, email 만 출력됨</code></pre>
<h3 id="json--xml-응답-포맷-변환-response-xmletreeelementtree">JSON &amp; XML 응답 포맷 변환, <code>Response</code>, <code>xml.etree.ElementTree</code></h3>
<blockquote>
<ul>
<li>API가 JSON 뿐만 아니라 XML도 지원해야할 때</li>
<li>사용자가 원하는 응답 포맷을 동적으로 변경하도록 설정</li>
</ul>
</blockquote>
<pre><code class="language-python">from fastapi import FastAPI
from fastapi.responses import JSONResponse, Response
import xml.etree.ElementTree as ET

app = FastAPI()

@app.get(&quot;/data/&quot;)
async def get_data(format: str=&quot;json&quot;):
    data = {&quot;message&quot;: &quot;Hello, FastAPI&quot;}

    if format == &quot;xml&quot;:
        root = ET.Element(&quot;response&quot;)
        message = ET.SubElement(root, &quot;message&quot;)
        message.text = data[&quot;message&quot;]
        xml_str = ET.tostring(root, encoding=&quot;utf-8&quot;, method=&quot;xml&quot;)
        return Response(content=xml_str, media_type=&quot;application/xml&quot;)

    return JSONResponse(content=data)

# GET /data/?fromat=xml
# &lt;response&gt;
#     &lt;message&gt;Hello, FastAPI&lt;/message&gt;
# &lt;/response&gt;</code></pre>
<h3 id="다국어-응답처리accept-language">다국어 응답처리(Accept-Language)</h3>
<blockquote>
<ul>
<li>글로벌 API에서는 사용자의 언어에 따라 다른 메세지 반환 필요</li>
<li>브라우저 요청 헤더 <code>Accept-Lanugage</code>를 사용하여 자동 감지</li>
<li>지원하는 언어가 없으면 기본값을 설정해야 함</li>
</ul>
</blockquote>
<pre><code class="language-python">from fastapi import FastAPI, Header

app = FastAPI()

    responses = {
        &quot;en&quot;: {&quot;message&quot;: &quot;Hello, welcome!&quot;},
        &quot;ko&quot;: {&quot;message&quot;: &quot;안녕하세요, 환영합니다!&quot;},
        &quot;fr&quot;: {&quot;message&quot;: &quot;Bonjour, bienvenue!&quot;}
    }
@app.get(&quot;/greet/&quot;)
async def greet(accept_language: str = Header(&quot;en&quot;)):
    return responses.get(accept_language, responses[&quot;en&quot;]) # 기본값 en</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 인증(JWT)]]></title>
            <link>https://velog.io/@hak_0/FastAPI-%EC%9D%B8%EC%A6%9DJWT</link>
            <guid>https://velog.io/@hak_0/FastAPI-%EC%9D%B8%EC%A6%9DJWT</guid>
            <pubDate>Thu, 13 Feb 2025 14:08:21 GMT</pubDate>
            <description><![CDATA[<h2 id="인증의-중요성-및-jwt">인증의 중요성 및 JWT</h2>
<ul>
<li>인증이 중요한 이유?<ul>
<li>보안 강화: 사용자 데이터 및 시스템 자원을 보호</li>
<li>권한 관리: 인증을 통해 사용자의 권한을 검증</li>
<li>사용자 경험: 안전한 환경에서 맞춤형 서비스 제공</li>
</ul>
</li>
</ul>
<h3 id="인증-vs-인가">인증 vs 인가</h3>
<ul>
<li>인증 : 너가 누구냐</li>
<li>인가 : 인증이 됬으면 너가 뭘 할수있냐</li>
</ul>
<h2 id="jwtjson-web-token">JWT(Json Web Token)</h2>
<ul>
<li><p>인증 정보를 JSON형식으로 저장하고, 서명을 통해 위변조 방지</p>
</li>
<li><p>주로 클라이언트와 서버간의 인증을 위해 사용</p>
</li>
<li><p>HEADER/PAYLOAD/SIGNATURE</p>
</li>
<li><p>장점</p>
<ul>
<li>무상태 인증: 서버가 세션을 유지할 필요 없음</li>
<li>확장성: 다양한 서비스에서 쉽게 통합 가능</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>크기가 크므로 반복적으로 보내는 경우 성능 이슈 발생</li>
<li>만료되지 않은 토큰이 탈취되면 보안 문제가 발생</li>
</ul>
</li>
</ul>
<h3 id="인증-디펜던시-depends-개념">인증 디펜던시 (Depends 개념)</h3>
<ul>
<li>인증 디펜던시란?<ul>
<li>FastAPI의 Depends를 사용하면 특정 기능 (예: DB 연결, 인증 등)을 경로
함수에서 반복적으로 재사용할 수 있음</li>
</ul>
</li>
<li>인증 디펜던시가 필요한 이유<ul>
<li>인증 및 사용자 검증 로직을 코드 중복 없이 모듈화할 수 있음</li>
<li>인증이 필요한 API 엔드포인트에서 간편하게 적용 가능</li>
<li>JWT 토큰을 자동으로 검증하고 사용자 정보를 API 경로 함수에 전달할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="인증-디펜던시의-동작-방식">인증 디펜던시의 동작 방식</h3>
<ul>
<li><p>기본적인 인증 흐름</p>
<ol>
<li>클라이언트가 로그인하여 JWT 토큰을 발급받음</li>
<li>클라이언트는 모든 요청의 Authorization 헤더에 JWT 토큰을 포함하여 보냄</li>
<li>FastAPI는 Depends(get_current_user)를 통해 인증 디펜던시를 실행</li>
<li>JWT 토큰 검증 과정
a. 토큰이 유효한지 확인: jwt.decode
b. 만료 여부 확인
c. 사용자가 존재하는지 데이터베이스에서 확인</li>
<li>인증 성공 시, 해당 사용자 정보를 경로 함수에 전달</li>
</ol>
</li>
<li><p>JWT 설정: Oauth2PasswordBearer(tokenUrl=“login”)</p>
<ul>
<li>클라이언트가 Authorization: Bearer <code>&lt;jWT_TOKEN&gt;</code> 형태로 요청을 보낼 때
추출하는 역할</li>
<li>tokenUrl=“login”은 로그인 API 엔드포인트를 지정하는 용도</li>
</ul>
</li>
</ul>
<h3 id="인증-디펜던시-구현-방법">인증 디펜던시 구현 방법</h3>
<ul>
<li>인증이 필요한 엔드포인트 적용<ul>
<li>ex) 프로필 조회 API<pre><code class="language-python">from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from passlib.context import CryptContext
</code></pre>
</li>
</ul>
</li>
</ul>
<p>class Token(BaseModel):
    access_token : str
    token_type : str</p>
<p>pwd_context = CryptContext(schemes=[&quot;bcrypt&quot;], deprecated=&quot;auto&quot;)</p>
<h1 id="passlib-라이브러리를-사용해-비밀번호를-안전하게-해싱하고-검증하기위한-설정">Passlib 라이브러리를 사용해 비밀번호를 안전하게 해싱하고 검증하기위한 설정</h1>
<h1 id="bcrypt-알고리즘을-사용하여-비밀번호-암호화">bcrypt 알고리즘을 사용하여 비밀번호 암호화</h1>
<h1 id="deprecated-사용해-이전-해싱-방식이-자동으로-업데이트-되도록-설정">deprecated 사용해 이전 해싱 방식이 자동으로 업데이트 되도록 설정</h1>
<p>oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;login&quot;)</p>
<h1 id="oauth2-인증-방식-중-password-flow-방식을-사용하여-액세스-토큰을-발급하고-인증을-수행">OAuth2 인증 방식 중 “Password Flow” 방식을 사용하여 액세스 토큰을 발급하고 인증을 수행</h1>
<h1 id="oauth2passwordbearertokenurllogin-→-사용자가-로그인할-때-jwt-토큰을-발급하는-url을-지정">OAuth2PasswordBearer(tokenUrl=&quot;login&quot;) → 사용자가 로그인할 때 JWT 토큰을 발급하는 URL을 지정</h1>
<h1 id="fastapi의-dependsoauth2_scheme를-통해-자동으로-요청-헤더에서-토큰을-추출-가능">FastAPI의 Depends(oauth2_scheme)를 통해 자동으로 요청 헤더에서 토큰을 추출 가능</h1>
<p>def hash_password(password: str):
    return pwd_context.hash(password)</p>
<p>def verify_password(plain_password: str, hashed_password: str):
    return pwd_context.verify(plain_password, hashed_password)</p>
<p>SECRET_KEY = &quot;secret-key&quot;
ALGORITHM = &quot;HS256&quot;
ACCESS_TOKEN_EXPIRE_MINUTES = 30</p>
<p>def create_access_token(data: dict): # JWT 엑세스 토큰을 생성하는 함수 
    to_encode = data.copy() # data 딕셔너리를 복사하여 변경해도 원본 데이터가 영향을 받지 않도록 보호
    expire = datetime.now() + timedelta(minutes =  ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({&quot;exp&quot;: expire}) 
    # to_encode 딕셔너리에 &quot;exp&quot;(만료 시간) 키 추가
    # JWT를 생성할 때 만료 시간이 포함되어야 자동으로 검증 가능
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) # JWT를 생성하는 핵심 코드
    return encoded_jwt</p>
<p>def get_current_user(token: str = Depends(oauth2_scheme)):
    &quot;&quot;&quot;
    1. 전달된 JWT 토큰을 검증하여 사용자 정보를 가져오는 함수입니다.
    2. JWT 토큰이 유효한지 확인하고, 만료 여부를 체크해야 합니다.
    3. 토큰에서 사용자 정보를 추출한 후, 데이터베이스(또는 가짜 DB)에서 해당 사용자가 존재하는지 확인해야 합니다.
    4. 사용자가 없거나 토큰이 유효하지 않다면, 401 Unauthorized 오류를 반환해야 합니다.
    &quot;&quot;&quot;
    try:
        # 1. JWT 토큰 디코딩
        # SECRET_KEY와 알고리즘을 사용하여 토큰을 디코딩합니다.
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])</p>
<pre><code>    # 2. 사용자 이름 추출
    # 토큰 페이로드에서 &quot;sub&quot; (subject), &quot;exp&quot; 필드를 가져옵니다.
    username = payload.get(&quot;sub&quot;)
    exp = payload.get(&quot;exp&quot;)

    # 3. 토큰이 유효하지 않거나 sub 필드가 없으면 오류 반환
    if username is None:
        raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)

    # 4. 사용자 검증
    # 데이터베이스(또는 가짜 DB)에서 사용자가 존재하는지 확인합니다.
    if username not in fake_users_db:
        raise HTTPException(status_code=401, detail=&quot;User not found&quot;)

    # 5. 토큰 만료 시간 검증
    # exp시간을 확인합니다.
    if exp is None or datetime.utcnow() &gt; datetime.utcfromtimestamp(exp):
        raise HTTPException(status_code=401, detail=&quot;Token has expired&quot;)

    # 6. 검증된 사용자 정보 반환
    return fake_users_db[username]

except JWTError:
    # 7. 토큰 검증 실패 시 오류 반환
    raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)</code></pre><h1 id="login-api">Login API</h1>
<p>@app.post(&quot;/login&quot;)
async def login_user(user: UserLoginRegister, db: AsyncSession= Depends(get_db)):
    result = await db.execute(select(User).where(User.username == user.username))
    existing_user = result.scalars().first()
    if not existing_user or not verify_password(user.password, existing_user.password):
        raise HTTPException(
            status_code=404,
            detail=&quot;User not found&quot;
        )
    access_token = create_access_token(data={&quot;sub&quot;: existing_user.username})
    return {&quot;access_token&quot;: access_token, &quot;token_type&quot;: &quot;bearer&quot;}</p>
<h1 id="profile-api">Profile API</h1>
<p>@app.get(&quot;/profile&quot;)
def get_profile(current_user: dict = Depends(get_current_user)):
    return {&quot;username&quot;: current_user[&quot;username&quot;]}</p>
<pre><code>
#### 인증 디펜던시 장점
* 코드 재사용성 증가
  - Depends(get_current_user)를 활용하면 API마다 중복된 인증 코드 작성이
불필요
  - 여러 API에서 일관된 방식으로 인증 적용 가능
* 보안성 강화
  - JWT 토큰을 중앙에서 검증: 일괄적인 보안 적용 가능
  - 모든 요청마다 get_current_user를 호출하여 인증을 검증
* 유지보수 용이
  - 인증 방식이 변경되더라도, get_current_user만 수정하면 전체 API에 반영됨
  - 관리가 쉽고, 확장성이 높음

### 🧑‍💻 DB에 연결한 모든 코드

```python
from datetime import datetime, timedelta, timezone

from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError

from pydantic import BaseModel
from passlib.context import CryptContext
from sqlalchemy import Column, String, Integer, select
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base



DATABASE_URL = &quot;sqlite+aiosqlite:///./test.db&quot;

async_engine = create_async_engine(
    DATABASE_URL,
    echo=True, # 쿼리 로그 출력
)

AsyncSessionLocal = sessionmaker(
    bind=async_engine, # 어떤 DB에 연결할지 설정
    class_=AsyncSession, 
    expire_on_commit=False # 세션이 커밋될 때 객체가 사라지지 않도록 설정
    # 📌 기본적으로 SQLAlchemy는 세션이 commit()되면 객체를 메모리에서 제거
    # 📌 expire_on_commit=False를 설정하면 세션이 종료되더라도 객체를 계속 유지 가능
)

Base = declarative_base()
# Base: 모든 데이터베이스의 부모 역할
# SQLAlchemy ORM(Object Relational Mapper) 모델을 정의하는 기본 클래스를 생성
# 📌 SQLAlchemy에서 ORM을 사용할 때, 모든 모델 클래스는 Base를 상속받아야 함.
# 📌 declarative_base()는 테이블을 정의할 때 필요한 “메타데이터”를 포함하는 베이스 클래스를 제공.


class User(Base):
    __tablename__ = &quot;users&quot;
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String)
    password = Column(String)

app = FastAPI()

# DB 초기화
async def init_db():
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
        # 데이터베이스가 아직 존재하지 않는다면 테이블을 생성
        # 이미 존재하는 경우, 기존 테이블을 변경하지 않고 그대로 유지
        # 단순히 테이블을 생성하는 역할이며, 기존 데이터에는 영향을 주지 않음

async def get_db():
    async with AsyncSessionLocal() as session: # async with 블록을 사용하여 세션이 자동으로 닫히도록 보장
        yield session
        # 비동기 SQLAlchemy 세션을 생성하고 반환하는 역할
        # FastAPI의 Depends()를 통해 엔드포인트에서 데이터베이스 세션을 사용 가능

        # 📌 **세션(Session)**은 데이터베이스와의 연결을 관리하는 객체로,
        #  쿼리 실행, 데이터 추가, 수정, 삭제 등의 작업을 할 때 사용됨.

class UserRegister(BaseModel):
    username : str
    password : str

class UserLogin(BaseModel):
    username : str
    password : str

class Token(BaseModel):
    access_token : str
    token_type : str

pwd_context = CryptContext(schemes=[&quot;bcrypt&quot;], deprecated=&quot;auto&quot;)
# Passlib 라이브러리를 사용해 비밀번호를 안전하게 해싱하고 검증하기위한 설정
# bcrypt 알고리즘을 사용하여 비밀번호 암호화
# deprecated 사용해 이전 해싱 방식이 자동으로 업데이트 되도록 설정

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;login&quot;)
# OAuth2 인증 방식 중 “Password Flow” 방식을 사용하여 액세스 토큰을 발급하고 인증을 수행
# OAuth2PasswordBearer(tokenUrl=&quot;login&quot;) → 사용자가 로그인할 때 JWT 토큰을 발급하는 URL을 지정
# FastAPI의 Depends(oauth2_scheme)를 통해 자동으로 요청 헤더에서 토큰을 추출 가능

def hash_password(password: str):
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str):
    return pwd_context.verify(plain_password, hashed_password)

SECRET_KEY = &quot;secret-key&quot;
ALGORITHM = &quot;HS256&quot;
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict): # JWT 엑세스 토큰을 생성하는 함수 
    to_encode = data.copy() # data 딕셔너리를 복사하여 변경해도 원본 데이터가 영향을 받지 않도록 보호
    expire = datetime.now() + timedelta(minutes =  ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({&quot;exp&quot;: expire}) 
    # to_encode 딕셔너리에 &quot;exp&quot;(만료 시간) 키 추가
    # JWT를 생성할 때 만료 시간이 포함되어야 자동으로 검증 가능
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) # JWT를 생성하는 핵심 코드
    return encoded_jwt

@app.post(&quot;/register&quot;)
async def register(user: UserRegister, db : AsyncSession = Depends(get_db)):
    new_user = User(username=user.username, password= hash_password(user.password))
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user)

    return new_user

@app.post(&quot;/login&quot;)
async def login_user(user: UserLoginRegister, db: AsyncSession= Depends(get_db)):
    result = await db.execute(select(User).where(User.username == user.username))
    existing_user = result.scalars().first()
    if not existing_user or not verify_password(user.password, existing_user.password):
        raise HTTPException(
            status_code=404,
            detail=&quot;User not found&quot;
        )
    access_token = create_access_token(data={&quot;sub&quot;: existing_user.username})
    return {&quot;access_token&quot;: access_token, &quot;token_type&quot;: &quot;bearer&quot;}

async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)):
    try:
        # 1. JWT 토큰 디코딩
        # SECRET_KEY와 알고리즘을 사용하여 토큰을 디코딩합니다.
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

        # 2. 사용자 이름 추출
        # 토큰 페이로드에서 &quot;sub&quot; (subject), &quot;exp&quot; 필드를 가져옵니다.
        username = payload.get(&quot;sub&quot;)
        exp = payload.get(&quot;exp&quot;)

        result = await db.execute(select(User).filter(User.username == username))
        user = result.scalars().first()

        # 3. 토큰이 유효하지 않거나 sub 필드가 없으면 오류 반환
        if username is None:
            raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)

        # 4. 사용자 검증
        # 데이터베이스(또는 가짜 DB)에서 사용자가 존재하는지 확인합니다.
        if not user:
            raise HTTPException(status_code=401, detail=&quot;User not found&quot;)

        # 5. 토큰 만료 시간 검증
        # exp시간을 확인합니다.
        if exp is None or datetime.utcnow() &gt; datetime.utcfromtimestamp(exp):
            raise HTTPException(status_code=401, detail=&quot;Token has expired&quot;)

        # 6. 검증된 사용자 정보 반환
        return user

    except JWTError:
        # 7. 토큰 검증 실패 시 오류 반환
        raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)

@app.get(&quot;/profile&quot;)
async def get_profile(current_user: dict = Depends(get_current_user)):
    return current_user
# 현재 로그인한 사용자의 토큰 만료 시간을 가져오는 GET /token-expiry API
@app.get(&quot;/token_expiry&quot;)
async def token_expiry(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        exp = payload.get(&quot;exp&quot;)
        if exp is None:
            raise HTTPException(status_code=401, detail=&quot;Token missing expiration time&quot;)

        if datetime.utcnow() &gt; datetime.utcfromtimestamp(exp):
            raise HTTPException(status_code=404, detail=&quot;Expired token&quot;)

        return {&quot;expire_time&quot;: datetime.utcfromtimestamp(exp)}
    except ExpiredSignatureError:
        raise HTTPException(status_code=401, detail=&quot;Expired token&quot;)
    except JWTError:
        raise HTTPException(status_code=404, detail=&quot;Invalid token&quot;)

# 관리자만 접근 가능한 GET /admin API
@app.get(&quot;/admin/&quot;)
async def admin(current_user: dict = Depends(get_current_user)):
    if current_user.role == &quot;admin&quot;:
        return {&quot;msg&quot;:&quot;welcome admin&quot;}
    raise HTTPException(status_code=403, detail=&quot;Forbidden&quot;)

# 사용자의 프로필을 수정하는 POST /profile API
@app.put(&quot;/profile/&quot;)
async def put_profile(
        update_user: UserLogin,
        current_user: dict = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    result = await db.execute(select(User).filter(User.username == current_user.username))
    existing_user = result.scalars().first()

    if not existing_user:
        raise HTTPException(status_code=404, detail=&quot;Its not your profile.&quot;)
    existing_user.username = update_user.username
    await db.commit()
    await db.refresh(existing_user)

    return {&quot;message&quot;: &quot;Profile updated successfully&quot;, &quot;username&quot;: existing_user.username}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI DB(Async)]]></title>
            <link>https://velog.io/@hak_0/FastAPI-DBAsync</link>
            <guid>https://velog.io/@hak_0/FastAPI-DBAsync</guid>
            <pubDate>Thu, 13 Feb 2025 13:52:40 GMT</pubDate>
            <description><![CDATA[<h2 id="비동기-asynchronous">비동기 Asynchronous</h2>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>비동기 (Asynchronous)</th>
<th>동기 (Synchronous)</th>
</tr>
</thead>
<tbody><tr>
<td>실행 방식</td>
<td>여러 작업을 동시에 실행 가능</td>
<td>한 번에 하나의 작업만 실행</td>
</tr>
<tr>
<td>코드 구조</td>
<td><code>async</code>/<code>await</code> 사용</td>
<td>일반적인 함수 호출</td>
</tr>
<tr>
<td>실행 순서</td>
<td><code>await</code>이 걸린 작업을 기다리는 동안 다른 작업 실행 가능</td>
<td>하나의 작업이 끝나야 다음 작업 실행</td>
</tr>
<tr>
<td>성능</td>
<td>CPU 및 I/O 작업을 병렬로 실행하여 속도 향상</td>
<td>하나씩 순차적으로 실행되어 속도가 느릴 수 있음</td>
</tr>
<tr>
<td>예제</td>
<td><code>async def fetch_data(): await asyncio.sleep(3)</code></td>
<td><code>def fetch_data(): time.sleep(3)</code></td>
</tr>
<tr>
<td>사용 사례</td>
<td>API 호출, 데이터베이스 쿼리, 웹 크롤링 등</td>
<td>간단한 계산, 파일 처리 등</td>
</tr>
<tr>
<td>멀티태스킹</td>
<td>가능 (<code>asyncio.gather()</code> 활용)</td>
<td>불가능 (작업이 끝날 때까지 대기)</td>
</tr>
<tr>
<td>리소스 활용</td>
<td>효율적 (작업 대기 중에도 다른 작업 수행)</td>
<td>비효율적 (작업이 끝날 때까지 대기)</td>
</tr>
<tr>
<td>에러 처리</td>
<td><code>try/except</code>와 <code>await</code> 조합 필요</td>
<td>일반적인 <code>try/except</code> 처리 가능</td>
</tr>
</tbody></table>
<h3 id="비동기-db-작업">비동기 DB 작업</h3>
<ul>
<li>데이터베이스 요청(ex. 데이터 삽입, 조회, 수정 등)을 처리할 때 I/O 작업(ex. 네트워크
또는 파일 시스템 접근)이 발생</li>
<li>이 I/O 작업 동안 애플리케이션이 멈추지 않고 다른 작업을 처리할 수 있도록 비차단
방식을 사용</li>
</ul>
<p>=&gt; 성능 향상, 동시성 지원 ,자원 절약</p>
<h3 id="비동기-await">비동기 await</h3>
<ul>
<li>비동기 함수 내에서 순차적으로 실행하지만</li>
<li>같은 비동기 함수가 gather로 묵여서 실행되었을때 순차적으로 진행되는걸 지정</li>
<li>await 함수가 실행중이면 그 밑의 작업은 진행하지 않음, 같이 실행된 다른 비동기 함수는 동작중</li>
</ul>
<h2 id="비동기-db-작업과-sqlalchemy">비동기 DB 작업과 SQLAlchemy</h2>
<ul>
<li>비동기 DB 설정<ul>
<li>create_async_engine</li>
<li>AsyncSession<h4 id="기본설정">기본설정</h4>
</li>
</ul>
</li>
</ul>
<pre><code class="language-python">from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker 
from sqlalchemy import Column, Integer, String
# 비동기 SoLite 데이터베이스 URL 설정
DATABASE_URL = &quot;sqlite+aiosqlite:///./test.db&quot;
# 비동기 SQLAtchemy 엔진 생성
async_engine = create_async_engine (
    DATABASE_URL,
    echo=True, # SOL 쿼리 로그 출력
)
# 비동기 세션 생성
AsyncSessionLocal = sessionmaker (
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False
)
# Base 클래스 생성
Base = declarative_base()</code></pre>
<ul>
<li>데이터베이스 초기화<ul>
<li>테이블 생성: 동일하게 설정</li>
<li>엔진 초기화: async with 사용<pre><code class="language-python">async def init_db():
async with async_engine.begin() as conn:
    await conn. run_sync (Base.metadata.create_ all)
</code></pre>
</li>
</ul>
</li>
</ul>
<h1 id="데이터베이스-초기화-실행">데이터베이스 초기화 실행</h1>
<p>if <em>name</em> == &quot;<em>main</em>&quot;:
    import asyncio
    asyncio. run (init_db())</p>
<pre><code>
### 🧑‍💻CRUD 구현
```python
from sqlalchemy import Column, Integer, String, text, select
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base

DATABASE_URL = &quot;sqlite+aiosqlite:///./test.db&quot;

async_engine = create_async_engine(
    DATABASE_URL,
    echo=True, # 쿼리 로그 출력
)

# 비동기 세션 생성
AsyncSessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    expire_on_commit=False,
    class_=AsyncSession,
)

# Base클래스 생성
Base = declarative_base()

# 데이터베이스 모델 정의
class User(Base):
    __tablename__ = &quot;users&quot;

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    email = Column(String, unique=True, index=True)

# 데이터베이스 초기화 함수
async def init_db(): # 테이블 생성 함수
    async with async_engine.connect() as conn:
    # async with : 연결을 자동으로 열고 닫는 역할(자원 누수를 방지)
    # async with 블록이 끝나면 자동으로 conn.close()가 호출됨.
    # 즉, async with는
    # try-finally처럼 동작하여 자원을 자동으로 정리함
    # 데이터베이스와 연결하는 코드

        await conn.run_sync(Base.metadata.create_all)
        # 모델을 생성하는 함수
        # await conn : 비동기 SQLAlchemey에서 동기 코드를 실행할 떄 사용하는 방식

# 비동기 세션 생성 함수
async def get_db():
    async with AsyncSessionLocal() as session:
        yield session
        # ✔ await은 비동기 작업이 끝날 때까지 기다리는 역할을 함.
        # ✔ yield는 함수의 실행을 멈추고, 나중에 다시 실행을 이어갈 수 있도록 하는 역할을 함.

# CRUD

# CREATE
async def create_user(name: str, email : str):
    async with AsyncSessionLocal() as session:
        new_user = User(name=name, email=email)
        session.add(new_user)
        await session.commit()
        await session.refresh(new_user)
        return new_user
# READ
async def read_user():
    async with AsyncSessionLocal() as session:
        result = await session.execute(text(&quot;SELECT * FROM users&quot;))
        result2 = await session.execute(select(User))
        users = result.fetchall()
        users2 = result2.scalars().all()
        return users2
    # users = await session.query(User).all() # 비동기에서 사용 불가

async def read_user_by_email(email: str):
    async with AsyncSessionLocal() as session:
        result = await session.execute(
            text(&quot;SELECT * FROM users WHERE email = :email&quot;), {&quot;email&quot;: email}  # ✅ 바인딩 적용
        )
        result2 = await session.execute(select(User).filter(User.email == email))
        user = result.fetchone()
        user2 = result2.scalars().first()
        return user2

# PUT
async def update_user(user_id: int, name: str, email: str):
    async with AsyncSessionLocal() as session:
        result = await session.execute(
            text(&quot;SELECT * FROM users WHERE id = :user_id&quot;),{&quot;user_id&quot;: user_id} 
        )
        result2 = await session.execute(select(User).filter(User.id == user_id))
        user3 = await session.get(User, user_id)

        user = result.fetchone()
        user2 = result2.scalars().first()

        user2.name = name
        user2.email = email
        if not user:
            return None

        await session.commit()
        await session.refresh(user2)

        return user2

# DELETE
async def delete_user(user_id :int):
    async with AsyncSessionLocal() as session:
        # result = await session.execute(
        #     text(&quot;DELETE FROM users WHERE id = :user_id&quot;),{&quot;user_id&quot;: user_id} 
        # )

        user = await session.get(User, user_id)
        if not user:
            return None
        await session.delete(user)
        await session.commit()</code></pre><h2 id="await의-정확한-동작">await의 정확한 동작</h2>
<ul>
<li>await 함수 전체가 아니라 특정 비동기 작업을 기다린다!!!<ul>
<li>await를 붙이면 해당 비동기 작업이 끝날때까지 실행을 멈추고 기다린다</li>
<li>같은 비동기 함수 안의 다른코드들은 병렬적으로 실행될 수 있음</li>
</ul>
</li>
</ul>
<h3 id="🧑💻-fastapi-기반-crud-구현">🧑‍💻 FastAPI 기반 CRUD 구현</h3>
<pre><code class="language-python">from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

DATABASE_URL = &quot;sqlite+aiosqlite:///./test.db&quot;

async_engine = create_async_engine(
    DATABASE_URL,
    echo = True
)

AsyncSessionLocal = sessionmaker(
    bind = async_engine,
    expire_on_commit = False,
    class_=AsyncSession
)

Base = declarative_base()

app = FastAPI()

class Animal(Base):
    __tablename__ = &quot;animals&quot;

    id = Column(Integer, primary_key=True, index=True)
    species = Column(String, index=True)
    age = Column(Integer, index=True)
    name = Column(String, index=True)

async def init_db():
        async with async_engine.connect() as conn:
            await conn.run_sync(Base.metadata.create_all)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

# 등록된 모든 동물 목록을 반환
@app.get(&quot;/animals/&quot;)
async def get_all_animals(db: AsyncSession = Depends(get_db)):
    results = await db.execute(select(Animal))
    animals = results.scalars().all()
    return animals

# 특정 동물 종의 세부 정보 조회
@app.get(&quot;/animals/by-species&quot;)
async def get_animals_by_species(species: str=None, db: AsyncSession = Depends(get_db)):
    if species:
        result = await db.execute(select(Animal).filter(Animal.species==species))
        animals = result.scalars().all()
        if not animals :
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
        return animals
    return []


# 특정 나이 이상의 동물 조회
@app.get(&quot;/animals/by-min-age&quot;)
async def get_animals_by_min_age(min_age:int=None, db: AsyncSession = Depends(get_db)):
    if min_age:
        results = await db.execute(select(Animal).filter(Animal.age &gt;= min_age))
        animals = results.scalars().all()
        if not animals:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
        return animals
    return []

# 특정 문자열의 이름을 가진 동물 검색
@app.get(&quot;/animals/search-by-name&quot;)
async def get_animals_by_name(name_contains: str=None, db: AsyncSession= Depends(get_db)):
    if name_contains:
        results = await db.execute(select(Animal).filter(Animal.name.like(f&quot;%{name_contains}%&quot;)))
        animals = results.scalars().all()
        if not animals:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
        return animals
    return []

# 동물 목록을 나이순(오름차순)으로 정렬
@app.get(&quot;/animals/age_asc&quot;)
async def get_animals_by_asc(db : AsyncSession = Depends(get_db)):
    results = await db.execute(select(Animal).order_by(Animal.age))
    animals = results.scalars().all()
    return animals

# 특정 id의 동물 정보 조회
@app.get(&quot;/animals/{animal_id}&quot;)
async def get_animal_by_id(animal_id: int, db: AsyncSession = Depends(get_db)):
    # results = await db.execute(select(Animal).filter(Animal.id == animal_id))
    # animal = results.scalars().first()
    animal = await db.get(Animal, animal_id)
    if not animal:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    return animal
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI DB(SQLAlchemy)]]></title>
            <link>https://velog.io/@hak_0/FastAPI-DBSQLAlchemy</link>
            <guid>https://velog.io/@hak_0/FastAPI-DBSQLAlchemy</guid>
            <pubDate>Thu, 13 Feb 2025 11:59:38 GMT</pubDate>
            <description><![CDATA[<h2 id="sqlalchemy">SQLAlchemy</h2>
<blockquote>
<ul>
<li>poetry add sqlalchemy</li>
</ul>
</blockquote>
<ul>
<li>Engine<ul>
<li>데이터베이스와의 연결을 관리하는 핵심 요소</li>
<li>데이터베이스 URL을 통해 연결 설정<pre><code class="language-python">from sqlalchemy import create_engine
engine = create_engine(&quot;sqlite:///./test.db&quot;)
</code></pre>
</li>
</ul>
</li>
</ul>
<h1 id="보통">보통</h1>
<p>DATABASE_URL = &quot;sqlite:///./test.db&quot;
engine = create_engine(
    DATABASE_URL,
    echo=True
)</p>
<pre><code>* Session
  - 데이터베이스와의 작업(삽입, 조회, 수정, 삭제)을 관리하며,트랜잭션을 제어
  - 여러 작업을 하나의 트랜잭션으로 묶어 처리 가능

* Base
  - 테이블 정의를 위한 클래스 기반
  - 모든 테이블 클래스는 Base를 상속받아 정의
```python
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()</code></pre><ul>
<li><p>SQLAlchemy 기본 문법</p>
<ul>
<li>데이터베이스 생성<pre><code class="language-python">Base.metadata.create_all(bind=engine)</code></pre>
</li>
</ul>
</li>
<li><p>함수선언시 실행방법</p>
<pre><code class="language-python">@app.on_event(&quot;startup&quot;)
async def startup():
  await init_db()</code></pre>
<h3 id="foreginkey-역참조">ForeginKey, 역참조</h3>
</li>
<li><p><code>relationship</code> : 역참조할 명 서로 지정해줘야함</p>
<ul>
<li><p>첫번쨰 인자 :</p>
<ul>
<li>SQL에선 없는 타입이라 SQLAlchemy 내에서 찾아야함</li>
<li>해당 모델 클래스명 입력</li>
</ul>
</li>
<li><p>두번째인자 <code>back_populates=</code></p>
<ul>
<li>반대편 필드의 ForeginKey로 지정 된 것을 지정</li>
<li>어떤 필드와 연결될건지 지정<h4 id="1n-관계">1:N 관계</h4>
<pre><code class="language-python">class User(Base):
__tablename__ = &#39;users&#39;
</code></pre>
</li>
</ul>
<p>user_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
username = Column(String, nullable=False)
email = Column(String, nullable=False)
password = Column(String, nullable=False)
role = Column(String, default = &quot;user&quot;)
is_active = Column(Boolean)</p>
<h1 id="첫번째인자-연결할-모델-클래스명-유저에서-찾는거니까">첫번째인자: 연결할 모델 클래스명, 유저에서 찾는거니까,</h1>
<h1 id="생성시에-역참조할거는-아직-db에-없으니까-함수내에서-찾아야함-그래서-클래스명-자체를-쓰는거임-order">생성시에 역참조할거는 아직 DB에 없으니까 함수내에서 찾아야함 그래서 클래스명 자체를 쓰는거임 Order</h1>
<h1 id="sql-에선-실재로-이런관계가-없으니-sqlalchemy-자체에서-찾아야함-그래서-클래스명으로-사용">sql 에선 실재로 이런관계가 없으니 sqlalchemy 자체에서 찾아야함 그래서 클래스명으로 사용</h1>
<h1 id="두번째인자-order테이블의-user를-참조한것">두번째인자: Order테이블의 user를 참조한것</h1>
<h1 id="relationship은-foreignkey와-다르게-sqlalchemy-orm-내부에서-참조할-필드명을-사용해야-하기-때문에-user를-써야-함">relationship()은 ForeignKey()와 다르게 SQLAlchemy ORM 내부에서 참조할 필드명을 사용해야 하기 때문에 user를 써야 함</h1>
<p>orders = relationship(&quot;Order&quot;, back_populates=&quot;user&quot;, cascade=&quot;all, delete&quot;)</p>
</li>
</ul>
</li>
</ul>
<p>class Order(Base):
    <strong>tablename</strong> = &#39;orders&#39;</p>
<pre><code>order_id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String, nullable=False)
total_price = Column(DECIMAL(65, 2))
is_paid = Column(Boolean)
created_at = Column(DATETIME, default=datetime.utcnow)

# 실제 users의 DB테이블 이름을 가르킴, 이건 SQL내에서 실행되야 하는거니까

user_id = Column(ForeignKey(&#39;users.user_id&#39;),nullable=False)
user = relationship(&quot;User&quot;, back_populates=&quot;orders&quot;)</code></pre><pre><code>#### N:N 관계 : secondary 에 두 테이블을 연결할 모델이 필요함
```python
order_products = Table(
    &quot;order_products&quot;,
    Base.metadata,
    Column(&quot;order_id&quot;, ForeignKey(&quot;orders.order_id&quot;), primary_key=True),
    Column(&quot;product_id&quot;, ForeignKey(&quot;products.product_id&quot;), primary_key=True)
)

class Order(Base):
    __tablename__ = &#39;orders&#39;

    order_id = Column(Integer, primary_key=True, autoincrement=True)
    username = Column(String, nullable=False)
    total_price = Column(DECIMAL(65, 2))
    is_paid = Column(Boolean)
    created_at = Column(DATETIME, default=datetime.utcnow)

    # 실제 users의 DB테이블 이름을 가르킴, 이건 SQL내에서 실행되야 하는거니까

    user_id = Column(ForeignKey(&#39;users.user_id&#39;),nullable=False)
    user = relationship(&quot;User&quot;, back_populates=&quot;orders&quot;)

    # Product 과 참조
    products = relationship(&quot;Product&quot;, secondary=order_products, back_populates=&quot;orders&quot;)


class Product(Base):
    __tablename__ = &#39;products&#39;

    product_id = Column(String, primary_key=True)
    name = Column(String, nullable=False)
    price = Column(Float, nullable=False)
    discount = Column(Float)
    final_price = Column(Float)

    # Order와 참조
    orders = relationship(&quot;Order&quot;, secondary=order_products, back_populates=&quot;products&quot;)</code></pre><ul>
<li><p>FastAPI : joinedload() = Django : select_related(), JOIN</p>
<ul>
<li>JOIN을 사용하여 한 번의 쿼리로 데이터를 가져옴 (즉시 로드)</li>
<li>1:N 관계에서 N이 많으면 중복 데이터가 많아지고 비효율적</li>
<li>User 1개에 대한 모든 주문 내역을 한 번에 가져올 때 적합</li>
<li>관리자가 특정 사용자 주문 내역을 보고 싶을 때<pre><code class="language-python"># Django
order = Order.objects.select_related(&quot;users&quot;).get(order_id=1)
# FastAPI
result = await db.execute(
    select(Order).options(joinedload(Order.users)).where(Order.Order_id == Order_id)
)</code></pre>
</li>
</ul>
</li>
<li><p>FastAPI : selectinload() = Django : prefetch_related(), 전부 가져옴 한번에</p>
<ul>
<li>IN 서브쿼리를 사용하여 여러 개의 데이터를 한 번에 로드 (즉시 로드)</li>
<li>1:N 관계에서 N이 많을 경우 JOIN보다 효율적</li>
<li>User 목록을 가져오면서 각 사용자의 주문도 같이 로드할 때 적합</li>
<li>여러 개의 User에 대해 Order 데이터를 한꺼번에 가져올 때 사용<pre><code class="language-python"># Django
user = User.objects.prefetch_related(&quot;orders&quot;).get(user_id=1)
# FastAPI
result = await db.execute(
    select(User).options(selectinload(User.orders)).where(User.user_id == user_id)
)</code></pre>
</li>
</ul>
</li>
<li><p>API</p>
<pre><code class="language-python">@orders_router.post(&quot;/&quot;)
async def create_order(
      order: OrderSchema,
      db: AsyncSession=Depends(get_db),
      user : dict = Depends(get_current_user)
  ):
  products = await db.execute(select(Product).where(Product.product_id.in_(order.product_id)))
  products = products.scalars().all()

  if not products:
      raise HTTPException(status_code=400, detail=&quot;No valid products found&quot;)

  new_order = Order(
      username = user.username,
      total_price = order.total_price,
      is_paid = order.is_paid
  )

  db.add(new_order)
  await db.commit()
  await db.refresh(new_order)
  # 주문이 없는 관계에서 바로 추가하면 주문이 없는 상태이기에 에러가 발생 그래서 따로 생성해야함
  # Many-to-Many 관계(relationship(secondary=order_products))에서 .extend()를 호출하면,
  # SQLAlchemy ORM이 내부적으로 변경을 감지하고 트랜잭션에 포함함
  # 이 트랜잭션을 DB에 반영하려면 반드시 db.commit()을 호출해야 함
  new_order.products.extend(products)
  await db.commit()
  await db.refresh(new_order)
  return new_order

</code></pre>
</li>
</ul>
<p>@orders_router.get(
    &quot;/&quot;,
    response_model=List[OrderSchema],
    response_model_exclude={
        &quot;order_id&quot;,
        &quot;is_paid&quot;,
        &quot;created_at&quot;
    }
)
async def current_user_orders(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
    orders = await db.execute(
        select(Order)
        .options(selectinload(Order.products))
        .where(Order.username == user.username)
    )
    orders = orders.scalars().all()</p>
<pre><code>return orders</code></pre><pre><code>#### 🧑‍💻 총 코드
```python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine(&quot;sqlite:///./test.db&quot;)
SessionLocal = sessionmaker (bind=engine)
session = SessionLocal()

Base = declarative_base()
Base.metadata.create_all(bind=engine)</code></pre><h4 id="🧑💻-crud">🧑‍💻 CRUD</h4>
<pre><code class="language-python"># 데이터 삽입
session = SessionLocal()
new_user = User(name=&quot;John&quot;)
session.add(new_user)
session.commit()

# 조회
users = session.query(User).all()
for user in users:
    print(user.name)
# 조건조회
user = session.query(User).filter(User.name ==&#39;John&#39;).first()
print(user.id, user.name, user.email)

# 수정
user = session.query(User).filter(User.name ==&#39;John&#39;).first()
if user:
    user.email = &quot;new@new.com&quot;
    session.commit()
# 삭제
if user:
    session.delete(user)
    session.commit()</code></pre>
<h3 id="🧑💻-실습">🧑‍💻 실습</h3>
<pre><code class="language-python">from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

DATABASE_URL = &quot;sqlite:///./test.db&quot;

engine = create_engine(
    DATABASE_URL,
    connect_args={&quot;check_same_thread&quot;: False}, # 멀티 쓰레드 활성화
)

SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
)

Base = declarative_base()

app = FastAPI()

class User(Base):
    __tablename__ = &quot;users&quot;

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    email = Column(String, unique=True,index=True)

Base.metadata.create_all(bind=engine)

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

def get_db(): # 데이터베이스 연결
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get(&quot;/users/name&quot;)
def read_username(db:Session = Depends(get_db)):
    users = db.query(User.name).order_by(User.name, User.id.desc()).all()
    return [user[0] for user in users]

@app.get(&quot;/users/count&quot;)
def read_username(db:Session = Depends(get_db)):
    users = db.query(User.name).order_by(User.name, User.id.desc()).all()
    return len(users)
@app.get(&quot;/users/&quot;)
def read_users(skip: int = 0, limit: int = 10,db: Session = Depends(get_db)):
    users = db.query(User).order_by(User.name, User.id.desc()).offset(skip).limit(limit).all()
    return users
# offset(skip): skip 개수만큼 건너뛰고 데이터를 가져옴.
# limit(limit): 최대 limit 개수만큼 데이터만 가져옴.
@app.post(&quot;/users/add/&quot;)
def add_user(user : UserResponse, db : Session = Depends(get_db)):
    new_user = User(id=user.id, name=user.name, email=user.email)
    db.add(new_user)
    db.commit()
    return {
        &quot;msg&quot; : &quot;success create&quot;
    }

@app.get(&quot;/users/search&quot;)
def read_search_user(email : str= None, name: str =None, db: Session = Depends(get_db)):
    if email:
        user = db.query(User).filter(User.email == email).first()
        if user:
            return user
    if name:
        user = db.query(User).filter(User.name.like(f&quot;%{name}%&quot;)).first()
        if user:
            return user
    raise HTTPException(status_code=404, detail=&quot;User not found&quot;)



@app.get(&quot;/users/{user_id}&quot;)
def read_user(user_id: int, db : Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
    return user

@app.get(&quot;/users/ge/{user_id}&quot;)
def read_ge_user(user_id : int, db : Session = Depends(get_db)):
    user = db.query(User).filter(User.id &gt;= user_id).all()
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User not found&quot;)
    return user
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI Depends]]></title>
            <link>https://velog.io/@hak_0/FastAPI-Depends</link>
            <guid>https://velog.io/@hak_0/FastAPI-Depends</guid>
            <pubDate>Thu, 13 Feb 2025 11:54:33 GMT</pubDate>
            <description><![CDATA[<h3 id="fastapi에서는-dependecny-injection-을-통해-연결">FastAPI에서는 <strong>Dependecny Injection</strong> 을 통해 연결</h3>
<blockquote>
<ul>
<li>Dependency Injection 개념<ul>
<li>특정 동작에 필요한 의존성을 함수나 클래스 외부에서 주입하는 패턴</li>
<li>FastAPI에서는 Depends를 사용하여 필요한 의존성을 정의하고 관리할 수 있음</li>
<li>주로 데이터베이스 세션, 인증, 설정 등의 관리에서 사용됨</li>
<li>특정 함수를 엔드포인트에 자동으로 주입</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li><p>연결 가능 목록</p>
<ul>
<li>데이터베이스 세션(DB connection)<pre><code class="language-python">@app.get(&quot;/items/&quot;)
async def read_items(db: AsyncSession = Depends(get_db)):  # ✅ DB 세션 자동 주입
result = await db.execute(&quot;SELECT * FROM items&quot;)
items = result.fetchall()
return items</code></pre>
</li>
<li>JWT토큰을 통한 사용자 인증<pre><code class="language-python">SECRET_KEY = &quot;secret-key&quot;
ALGORITHM = &quot;HS256&quot;
</code></pre>
</li>
</ul>
<p>oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;login&quot;)</p>
<p>async def get_current_user(token: str = Depends(oauth2_scheme)):  # ✅ 토큰 자동 주입</p>
<pre><code>try:
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username = payload.get(&quot;sub&quot;)
    if not username:
        raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)
except Exception:
    raise HTTPException(status_code=401, detail=&quot;Invalid token&quot;)

return {&quot;username&quot;: username}</code></pre><p>@app.get(&quot;/profile/&quot;)
async def read_profile(current_user: dict = Depends(get_current_user)):  # ✅ 현재 사용자 정보 자동 주입</p>
<pre><code>return {&quot;message&quot;: f&quot;Hello, {current_user[&#39;username&#39;]}!&quot;}</code></pre><pre><code>- 환경 변수, 설정 값
```python

class AppSettings:
  def __init__(self):
      self.debug = True
      self.api_key = &quot;my-secret-api-key&quot;

async def get_settings():
  return AppSettings()

@app.get(&quot;/settings/&quot;)
async def read_settings(settings: AppSettings = Depends(get_settings)):  # ✅ 설정 값 자동 주입
    return {&quot;debug&quot;: settings.debug, &quot;api_key&quot;: settings.api_key}</code></pre><ul>
<li>공통적으로 실행할 미들웨어 기능(예: 로깅, 보안 검사)<pre><code class="language-python">from fastapi import Request, Depends
</code></pre>
</li>
</ul>
<p>async def log_request(request: Request):</p>
<pre><code>print(f&quot;📌 요청 수신: {request.method} {request.url}&quot;)
return request</code></pre><p>@app.get(&quot;/secure-data/&quot;)
async def secure_data(request: Request = Depends(log_request)):  # ✅ 요청 로깅 자동 적용</p>
<pre><code>return {&quot;message&quot;: &quot;This is secure data!&quot;}</code></pre><pre><code>- OAuth2 인증 시스템(OAuth2PasswordBearer)
```python
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;token&quot;)

@app.get(&quot;/users/me/&quot;)
async def read_users_me(token: str = Depends(oauth2_scheme)):  # ✅ 토큰 자동 주입
    return {&quot;token&quot;: token}</code></pre></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>