<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 기록 블로그</title>
    <link>https://mildwpaper.tistory.com/</link>
    <description>언젠간 당신처럼 되고 싶어요.</description>
    <language>ko</language>
    <pubDate>Mon, 15 Jun 2026 00:44:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>마술사의 수습생</managingEditor>
    <image>
      <title>개발 기록 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3483941/attach/49397696253342ec85076ff1431d16bd</url>
      <link>https://mildwpaper.tistory.com</link>
    </image>
    <item>
      <title>카프카 리벨런싱 조건</title>
      <link>https://mildwpaper.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;리벨런싱이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카에는 파티션이라는 개념이 존재한다.&lt;br /&gt;프로듀서가 토픽으로 메시지를 발행하면 카프카 클러스터에 저장되고, 이후 컨슈머들이 해당 토픽의 메시지를 폴링하여 읽고 처리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 로그처럼 메시지를 쌓는 구조를 가지며, 1 대 1 대응으로 처리가된다면 문제가 없겠지만, 1 대 N으로 접근한다면 어떻게될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐에 쌓인 메시지를 처리하다 실패했을때 처리가 불가능 할 것이다. 예를들어 1번 컨슈머가 처리하다 오류가나서 에러처리하고 종료됐을때, 다른 2번 컨슈머가 다른 메시지들을 처리하여 1번이 처리하던 메시지는 넘어가야하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 카프카에서는 병렬적으로 처리하기 위해서 하나의 토픽에 대해 파티션으로 나눠서 저장하는 방법을 사용한다. 1번 인스턴스 - 1번 파티션, 2번 인스턴스 - 2번 파티션이 사용할 수 있도록 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 카프카는 컨슈머 그룹을 통해 하나의 토픽을 여러 파티션으로 나누어 각 컨슈머 인스턴스에 할당하여 병렬로 처리할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹에서 한 인스턴스가 문제가 생기면, 다른 인스턴스가 대신 처리할 수 있도록 파티션을 재할당 한다. 이 과정을 리밸런싱이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;리밸런싱이란?
카프카에서 리밸런싱(Rebalancing)이란 컨슈머 그룹 내에서 파티션 할당을 다시 계산하고 재분배하는 과정을 말한다.
컨슈머 그룹은 토픽의 파티션을 나누어 각 컨슈머 인스턴스가 병렬로 처리할 수 있게 설계되어 있다. 그러나 컨슈머가 새로 추가되거나, 장애가 발생해 세션이 끊기거나, 구독 중인 토픽의 파티션 수가 변경되면 기존 할당 정보가 무효화된다.
이때 그룹 코디네이터는 리밸런싱을 통해 파티션을 남은 컨슈머에게 재할당하거나 새로 참여한 컨슈머에게 분배한다. 이를 통해 장애가 발생해도 메시지 처리를 중단하지 않고 다른 인스턴스가 이어서 처리할 수 있게 보장한다.
하지만 리밸런싱 과정에서는 잠시 모든 컨슈머가 할당을 반납하고 새로운 할당을 받는 단계가 필요하다. 이로 인해 메시지 소비가 일시적으로 멈추거나 지연될 수 있으며, 너무 자주 리밸런싱이 일어나면 서비스 지연이 심해질 수 있다. 이를 완화하기 위해 Kafka에서는 incremental rebalance와 같은 최적화 기법을 지원한다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리벨런싱 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리벨런싱의 조건은 여러가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 인스턴스가 새로 추가될 때&lt;/li&gt;
&lt;li&gt;컨슈머 인스턴스가 종료되거나 장애로 빠질 때 (세션 타임아웃/하트비트 실패)&lt;/li&gt;
&lt;li&gt;컨슈머 그룹이 구독 중인 토픽의 파티션 수가 변경될 때&lt;/li&gt;
&lt;li&gt;그룹 코디네이터 브로커가 변경되거나 장애가 발생할 때&lt;/li&gt;
&lt;li&gt;컨슈머가 구독하는 토픽 목록을 변경할 때 (subscribe 변경)&lt;/li&gt;
&lt;li&gt;컨슈머 그룹 설정(파티셔너, 할당 전략 등)이 변경될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 주목할 점은,&lt;br /&gt;카프카 메시지로 비동기 처리를 하다보니 로직을 오랫동안 처리할 수 있다고 착각할 수 있는데, max.poll.interval.ms(기본값 5분)에 따라 최대로 동작할 수 있는 시간을 지정할 수 있다. 물론 최대 제한시간이 존재한다.(약 24일 정도라고 한다)&lt;br /&gt;24일동안 동작할 수 있게 선택지를 줘도, 이정도까지 사용하는것은 안좋은 선택일 수 있다. 너무 크게 잡으면 장애감지를 24일 뒤에 할 수 있다라고 볼 수 있기 때문이다. 너무 작게 잡으면 리벨런싱이 자주 일어날 것 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 max.poll.interval.ms을 넘어서면 왜 리벨런싱이 일어날까? 서버의 성능은 동일해서 다시해도 똑같이 오래걸릴텐데, 그 이유는 해당 인스턴스가 죽었다고 판단하고 다른 인스턴스에게 재할당 해주려는 목표이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리벨런싱 유의할점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋다. 이제 리벨런싱이 일어나는 이유와 하면 좋다는것까지 알았다. 그렇다면 단점은 뭘까?&lt;br /&gt;kafka는 오프셋 커밋(offset commit) 을 기준으로 &quot;어디까지 처리했는지&quot;를 기억한다. 리밸런싱은 모든 컨슈머가 파티션 할당을 반납하고 새로 할당 받는 과정인데, 이때 아직 커밋되지 않은 메시지가 있을 수 있다. 그렇다면 처리를 하고 커밋만 안돼서 메시지를 다시 처리할 수 있는 가능성이 생긴다. 이를 위해 중복 처리를 방지해야한다. 예를 들어 처리한 내용에 대한 내용이 db에 있다면 update를 한다던지 무시하는 처리를 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 리밸런싱이 일어나는 동안에는 모든 동작이 멈춰 병목이 생길 수 있다. 이는 파티션 갯수에 따라 어느정도 걸리는지 달라지니, 성능을 위해서 파티션을 무분별하게 늘리는것은 좋지 못하다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/70</guid>
      <comments>https://mildwpaper.tistory.com/70#entry70comment</comments>
      <pubDate>Wed, 16 Jul 2025 09:53:15 +0900</pubDate>
    </item>
    <item>
      <title>Rag를 사용해서 자신의 전문 비서를 만들어 보자 - Lang Chain</title>
      <link>https://mildwpaper.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;직전 글인 &quot;Rag를 사용해서 자신의 전문 비서를 만들어 보자&quot;에서는 Lang Chain을 사용하지 않았다. Lang Chain은 LLM을 활용한 애플리케이션을 쉽게 만들 수 있도록 도와주는 프레임워크이다. 때문에 해당 프레임워크를 사용하면 더욱 깔끔한 코드로 이전에 작성한 코드를 개선할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 페이지에서는 그저 이전 코드를 Lnag chain을 통해 바꾸면 어떤식으로 구현되는지만 파악하고 넘어가 볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lang Chain이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 외부 데이터(문서, DB, 웹, API 등)와 LLM을 연결하거나, 복잡한 파이프라인(Chain), 에이전트(Agent), 툴을 구축할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 특징은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Document Loader: PDF, txt, 웹, 데이터베이스 등 다양한 소스로부터 데이터를 쉽게 불러올 수 있음.&lt;/li&gt;
&lt;li&gt;Text Splitter: 긴 텍스트를 LLM에 맞게 청크(Chunk) 단위로 분할.&lt;/li&gt;
&lt;li&gt;Embedding &amp;amp; Vector Store: 텍스트를 임베딩(벡터)으로 변환하고, 벡터DB(Qdrant, FAISS 등)에 저장/검색.&lt;/li&gt;
&lt;li&gt;Chain: LLM과 다양한 구성 요소를 연결하여 파이프라인(예: 질문 &amp;rarr; 문서 검색 &amp;rarr; LLM에 컨텍스트 넣기 &amp;rarr; 답변 생성) 구축.&lt;/li&gt;
&lt;li&gt;Agent: 여러 개의 도구(예: 계산, 웹검색, DB질의 등)를 사용해서 LLM이 상황에 따라 적절히 툴을 선택해서 답변하도록 함.&lt;/li&gt;
&lt;li&gt;Prompt Template: 프롬프트를 유연하게 조합해서 관리할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 Lang Chain을 통해서 개선해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하기 위해서는 아래와 같은 패키지들이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;langchain&lt;/li&gt;
&lt;li&gt;langchain_community&lt;/li&gt;
&lt;li&gt;qdrant-client&lt;/li&gt;
&lt;li&gt;sentence-transformers&lt;/li&gt;
&lt;li&gt;pdfplumber&lt;/li&gt;
&lt;li&gt;langchain-google-genai&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보면 from langchain_community.document_loaders와 같이 langchain_community와 langchain만 있으면 될 것 같은데 나머지는 왜 설치해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것은 LangChain이 &quot;Wrapper&quot; 역할만 하고 실제 기능(임베딩 모델, 벡터DB, PDF 로더 등)은 외부 패키지를 그대로 사용하기 때문이다. langchain 자체는 &quot;공통 인터페이스&quot;만 갖고 있고, 실제 데이터 로딩, 임베딩, 벡터스토어, LLM 호출 등은 별도 community 패키지 또는 외부 패키지에서 처리되는 구조이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import os
from langchain_community.document_loaders import PDFPlumberLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Qdrant
from langchain_google_genai import ChatGoogleGenerativeAI

# 1. 환경설정
os.environ[&quot;GOOGLE_API_KEY&quot;] = &quot;키 입력&quot;
qdrant_url = &quot;http://localhost:6333&quot;
collection_name = &quot;pdf_chunks&quot;

# 2. PDF에서 텍스트 추출 및 문서 객체 생성
loader = PDFPlumberLoader(&quot;example.pdf&quot;)
documents = loader.load()

# 3. 텍스트 chunk 단위 분할
splitter = RecursiveCharacterTextSplitter(
    chunk_size=300, chunk_overlap=50, separators=[&quot;\n&quot;, &quot; &quot;]
)
docs = splitter.split_documents(documents)
print(f&quot;총 {len(docs)}개 chunk 추출됨.&quot;)

# 4. 임베딩 모델 로드 (한국어 SBERT)
embedding = HuggingFaceEmbeddings(
    model_name=&quot;snunlp/KR-SBERT-V40K-klueNLI-augSTS&quot;
)

# 5. Qdrant 벡터스토어에 저장
vectorstore = Qdrant.from_documents(
    docs,
    embedding,
    url=qdrant_url,
    collection_name=collection_name,
    force_recreate=True  # 기존 컬렉션 삭제 후 생성
)

# 6. Retriever 생성
retriever = vectorstore.as_retriever(search_type=&quot;similarity&quot;, search_kwargs={&quot;k&quot;: 3})

# 7. Gemini LLM 래퍼 (langchain-google-genai)
llm = ChatGoogleGenerativeAI(model=&quot;gemini-2.0-flash&quot;, temperature=0)

# 8. RAG 파이프라인 함수
def rag_ask_gemini(query):
    # 1. Vectorstore에서 유사 chunk 검색
    relevant_docs = retriever.get_relevant_documents(query)
    context = &quot;\n&quot;.join([doc.page_content for doc in relevant_docs])

    # 2. Gemini LLM에 프롬프트 전달
    prompt = f&quot;&quot;&quot;아래 내용을 참고하여 질문에 답변해줘.

[참고 내용]
{context}

[질문]
{query}
&quot;&quot;&quot;
    response = llm.invoke(prompt)
    print(f&quot;\n[질문] {query}\n&quot;)
    print(f&quot;[AI 답변] {response.content.strip()}\n&quot;)
    print(&quot;-&quot; * 40)

# 9. 질의 예시
rag_ask_gemini(&quot;이 문서의 핵심 주제를 요약해줘.&quot;)
rag_ask_gemini(&quot;문서에서 등장하는 주요 인물은 누구야?&quot;)
rag_ask_gemini(&quot;문서에서 다루는 기술적 내용이 뭐야?&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retriever(리트리버) : 어떤 질문(쿼리)이 들어왔을 때, 내가 가진 문서(혹은 텍스트 조각, chunk)들 중 가장 관련성이 높은 것들만 골라서 뽑아주는 역할을 하는 컴포넌트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 실행을 통해 이전과 비슷한 응답을 받을 수 있는것을 확인했다. 기존 코드보다 위의 코드가 더 깔끔해진 것을 느낄 수 있었다. 이는 벡터DB에 데이터를 저장하고 조회하는 부분에서 느꼈고, 파이썬과 LLM을 사용해서 무언가 한다면 Lang Chain은 사용하지 않을 수 없겠다고 생각했다. 이후에는 에이전트 기능에대해 알아 볼 예정이다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/69</guid>
      <comments>https://mildwpaper.tistory.com/69#entry69comment</comments>
      <pubDate>Tue, 8 Jul 2025 18:53:03 +0900</pubDate>
    </item>
    <item>
      <title>Rag를 사용해서 자신의 전문 비서를 만들어 보자</title>
      <link>https://mildwpaper.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 나는 GPT를 사용해서 나에대한 정보를 요약 정리할 일이 많아졌는데, 요청을 하다보면 나에대한 정보를 잊어버리는 것 같다. 이를 해결하기 위해 Rag 기법을 사용해서 나에대한 정보 요청을 할때마다 나의 기반 정보를 첨부하여 보내도록 해보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대형 언어 모델(LLM)은 학습 시점 이후에 나온 정보나 조직 내부의 비공개 자료를 알지 못한다는 한계가 있다. RAG는 &amp;ldquo;검색(Retrieval) &amp;rarr; 생성(Generation)&amp;rdquo; 두 단계를 결합해 이 문제를 해결한다. 질의가 들어오면 벡터 DB 등에서 관련 문서를 찾아 LLM 프롬프트에 주입하고, 모델은 이를 근거로 답변을 생성한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;벡터 DB란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB는 &amp;ldquo;문서 조각(chunk)&amp;rdquo;을 임베딩 벡터로 변환해 저장하고, 질의 임베딩과 유사도가 높은 벡터를 빠르게 검색해 주는 역할을 한다. 일반적인 RDS는 정확한 값을 기반으로 조회한다면 벡터 DB는 유사도를 기반으로 정보를 조회한다. Elasticsearch도 vector field를 지원해 벡터 검색이 가능하나, 본격적인 ANN 최적화(대규모&amp;middot;저지연)는 전용 벡터 DB 대비 떨어진다고 한다. 나는 이번 예제에서 벡터DB인 Qdrant를 사용해볼것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 실습을 위해 python을 사용할 예정이다. 예제 코드는 아래와 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Qdrant 실행&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;#    - Docker에서 Qdrant 실행: 
#      docker run -p 6333:6333 qdrant/qdrant&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 코드를 작성하기 전에 Qdrant를 Docker를 통해 띄워놓고 진행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 코드 실행&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;벡터 DB 사용 예제&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 필요한 패키지 설치
# pip install qdrant-client sentence-transformers

from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, Distance, VectorParams
from sentence_transformers import SentenceTransformer

# 1. Qdrant 클라이언트 연결 (Docker로 로컬 실행 전제)
client = QdrantClient(url=&quot;http://localhost:6333&quot;)  
collection_name = &quot;kim_minsu_profile&quot;

# 2. 인물 정보 데이터 (홍길동)
minsu_docs = [
    (&quot;1&quot;, &quot;홍길동은 1990년생으로, 서울대학교 컴퓨터공학과를 졸업했다.&quot;),
    (&quot;2&quot;, &quot;주요 경력은 카카오에서 백엔드 개발자로 5년, 네이버에서 데이터 엔지니어로 3년 근무했다.&quot;),
    (&quot;3&quot;, &quot;파이썬, 자바, 스칼라에 능숙하며, 분산 시스템과 머신러닝에 관심이 많다.&quot;),
    (&quot;4&quot;, &quot;2022년부터는 AI 스타트업에서 CTO로 재직 중이다.&quot;),
    (&quot;5&quot;, &quot;취미는 등산과 독서, 최근에는 마라톤 대회에도 참가했다.&quot;),
    (&quot;6&quot;, &quot;홍길동의 이메일 주소는 rag.test@email.com 이다.&quot;),
    (&quot;7&quot;, &quot;네이버 근무 당시 데이터 파이프라인 자동화 프로젝트를 리드했다.&quot;),
    (&quot;8&quot;, &quot;AI 스타트업에서는 챗봇 개발과 RAG 아키텍처 구축을 담당했다.&quot;),
    (&quot;9&quot;, &quot;서울특별시 강남구에 거주한다.&quot;),
    (&quot;10&quot;, &quot;최근 발표 논문은 'Scalable RAG Pipeline for Korean Language'이다.&quot;)
]

# 3. 컬렉션 생성 (이미 존재할 경우 자동 삭제 후 생성)
client.recreate_collection(
    collection_name=collection_name,
    vectors_config=VectorParams(size=384, distance=Distance.COSINE)
)

# 4. 임베딩 모델 준비
embedder = SentenceTransformer('all-MiniLM-L6-v2')

ids, texts = zip(*minsu_docs)
embeddings = embedder.encode(list(texts), convert_to_numpy=True)

# 5. Qdrant에 인물 정보 삽입
points = [
    PointStruct(id=int(_id), vector=vec.tolist(), payload={&quot;text&quot;: txt})
    for _id, vec, txt in zip(ids, embeddings, texts)
]
client.upsert(collection_name=collection_name, points=points)

# 6. 자연어 질의 기반 RAG 검색 함수 정의
def rag_search(query, k=3):
    q_vec = embedder.encode([query], convert_to_numpy=True)[0].tolist()
    hits = client.search(
        collection_name=collection_name,
        query_vector=q_vec,
        limit=k
    )
    print(f&quot;[Query] {query}&quot;)
    print(&quot;-&quot; * 40)
    for hit in hits:
        print(f&quot;&amp;bull; {hit.payload['text']} (Score: {hit.score:.4f})&quot;)
    print()

# 7. 예시 질의
rag_search(&quot;홍길동의 전공과 학교는?&quot;)
rag_search(&quot;홍길동은 어떤 회사에서 일했어?&quot;)
rag_search(&quot;홍길동의 최근 주요 프로젝트나 연구는?&quot;)
rag_search(&quot;이메일 알려줘&quot;)
rag_search(&quot;사는 곳은 어디야?&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1KHXt/btsO8MeHlj7/6UXWZGNkKR9KIKi1jTAJB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1KHXt/btsO8MeHlj7/6UXWZGNkKR9KIKi1jTAJB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1KHXt/btsO8MeHlj7/6UXWZGNkKR9KIKi1jTAJB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1KHXt%2FbtsO8MeHlj7%2F6UXWZGNkKR9KIKi1jTAJB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;320&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 실행시켜본다면 위와 같은 결과를 볼 수 있다. 하지만 생각보다 적중률이 많이 낮은 것 같다. 그렇게 생각한 이유는 &quot;이메일 알려줘&quot;라는 쿼리에 대해서는 엉뚱한 답변이 돌아왔기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;1428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BUGEu/btsO8mmZu4c/mdWPcKQLeMEqEiLHak2Ty1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BUGEu/btsO8mmZu4c/mdWPcKQLeMEqEiLHak2Ty1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BUGEu/btsO8mmZu4c/mdWPcKQLeMEqEiLHak2Ty1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBUGEu%2FbtsO8mmZu4c%2FmdWPcKQLeMEqEiLHak2Ty1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;547&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;1428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 벡터 DB의 임베딩 모델을 바꿔주면 어느정도 해소가 된다. 기본 모델인 'all-MiniLM-L6-v2' 보다는 한글의 특화된 모델인 'snunlp/KR-SBERT-V40K-klueNLI-augSTS'를 사용하니 이메일에 대한 질의를 잘 응답했다. 하지만 사는곳에대해 물어봤을때 docs에 거주하는곳으로 적혀있어서 그런지 유사도가 낮게 나왔다. 이는 질의와 docs의 데이터 양쪽의 데이터가 너무 짧아서 발생하는 일이다. 다른 한글 특화 모델인 'jhgan/ko-sroberta-multitask'를 써도 결과가 만족스럽지 못했다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;alt_queries = [
    &quot;사는 곳&quot;,
    &quot;거주하는 곳&quot;,
    &quot;거주지&quot;,
    &quot;주소&quot;,
    &quot;현재 거주 중인 지역&quot;
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우에는 LLM을 활용해서 한가지 질의에 대해 다양한 질의로 확장하는 방법으로 해소 할 수 있다. 비슷한 방법으로 docs데이터에 사는곳과 거주지 정보를 중복으로 저장하는 방법도 있다. 이밖에도 청크 방식을 바꿔보거나, ES를 같이 사용하는 하이브리드 기법, 're-ranking'을 사용하는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;re-ranking : 필요한 n개의 정보보다 더 많은 정보를 빠르게 가져와 cross-encoder를 사용하여 다시 순위를 매겨 상위 n개의 정보를 뽑아 사용하는 기법&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Rag 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선을 위한 다양한 기법들은 추후 다시 학습하도록 하고, 먼저 벡터DB와 Gemini와 연계해보도록 하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, Distance, VectorParams
from sentence_transformers import SentenceTransformer
import os
import google.generativeai as genai

# 1. Qdrant 클라이언트 연결 (Docker로 로컬 실행 전제)
client = QdrantClient(url=&quot;http://localhost:6333&quot;)  
collection_name = &quot;kim_minsu_profile&quot;

os.environ[&quot;GOOGLE_API_KEY&quot;] = &quot;키값 입력&quot;

# 이전 코드와 동일

def rag_ask_gemini(query, k=3):
    # Qdrant에서 관련 문서 검색
    q_vec = embedder.encode([query], convert_to_numpy=True)[0].tolist()
    hits = client.search(
        collection_name=collection_name,
        query_vector=q_vec,
        limit=k
    )
    retrieved = [hit.payload['text'] for hit in hits]

    # 프롬프트 작성
    context = &quot;\n&quot;.join(retrieved)
    prompt = f&quot;&quot;&quot;아래 내용을 참고하여 질문에 답변해줘.

[참고 내용]
{context}

[질문]
{query}
&quot;&quot;&quot;

    # Gemini 호출
    genai.configure(api_key=os.environ[&quot;GOOGLE_API_KEY&quot;])
    model = genai.GenerativeModel('gemini-2.0-flash')
    response = model.generate_content(prompt)
    print(f&quot;\n[질문] {query}\n&quot;)
    print(f&quot;[AI 답변] {response.text.strip()}\n&quot;)
    print(&quot;[참고문서]&quot;)
    for doc in retrieved:
        print(&quot; -&quot;, doc)

# 질의
rag_ask_gemini(&quot;홍길동의 전공과 학교는?&quot;)
rag_ask_gemini(&quot;홍길동은 어떤 회사에서 일했어?&quot;)
rag_ask_gemini(&quot;홍길동의 최근 주요 프로젝트나 연구는?&quot;)
rag_ask_gemini(&quot;이메일 알려줘&quot;)
rag_ask_gemini(&quot;사는 곳은 어디야?&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 완성된 코드는 위와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drgqGq/btsO9128gLQ/fNsq5xmyqukwikeWJ4DlZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drgqGq/btsO9128gLQ/fNsq5xmyqukwikeWJ4DlZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drgqGq/btsO9128gLQ/fNsq5xmyqukwikeWJ4DlZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrgqGq%2FbtsO9128gLQ%2FfNsq5xmyqukwikeWJ4DlZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;497&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질의를 하게되면 참고 문서 기반을 통해 Gemini가 잘 응답해 주는 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PDF 정보 입력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 정보를 저렇게 배열로 만들어서 넣지 않고 PDF를 통째로 기입하여 사용하고자 한다. 해당 방법은 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import os
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, Distance, VectorParams
from sentence_transformers import SentenceTransformer
import google.generativeai as genai
import pdfplumber

# 1. 환경설정: Qdrant, Gemini API 키
client = QdrantClient(url=&quot;http://localhost:6333&quot;)
collection_name = &quot;pdf_chunks&quot;
os.environ[&quot;GOOGLE_API_KEY&quot;] = &quot;키 입력&quot;

# 2. PDF에서 텍스트 추출
pdf_path = &quot;example.pdf&quot;  # PDF 파일명 입력
with pdfplumber.open(pdf_path) as pdf:
    text = &quot;\n&quot;.join([page.extract_text() for page in pdf.pages if page.extract_text()])

# 3. 텍스트를 chunk 단위로 분할
def chunk_text(text, chunk_size=300, overlap=50):
    return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size - overlap)]

chunks = chunk_text(text)
print(f&quot;총 {len(chunks)}개 chunk 추출됨.&quot;)

# 4. 임베딩 모델 로드 (한국어 문서일 때 추천)
embedder = SentenceTransformer('snunlp/KR-SBERT-V40K-klueNLI-augSTS')
vector_dim = embedder.get_sentence_embedding_dimension()

# 5. Qdrant 컬렉션(벡터 DB) 초기화
if client.collection_exists(collection_name=collection_name):
    client.delete_collection(collection_name=collection_name)
client.create_collection(
    collection_name=collection_name,
    vectors_config=VectorParams(size=vector_dim, distance=Distance.COSINE)
)

# 6. chunk별 임베딩 및 Qdrant 등록
embeddings = embedder.encode(chunks, convert_to_numpy=True)
points = [
    PointStruct(id=i, vector=vec.tolist(), payload={&quot;text&quot;: chunk})
    for i, (vec, chunk) in enumerate(zip(embeddings, chunks))
]
client.upsert(collection_name=collection_name, points=points)

# 7. 검색 + Gemini로 답변 생성 함수
def rag_ask_gemini(query, k=3):
    q_vec = embedder.encode([query], convert_to_numpy=True)[0].tolist()
    hits = client.search(
        collection_name=collection_name,
        query_vector=q_vec,
        limit=k
    )
    retrieved = [hit.payload['text'] for hit in hits]
    context = &quot;\n&quot;.join(retrieved)
    prompt = f&quot;&quot;&quot;아래 내용을 참고하여 질문에 답변해줘.

[참고 내용]
{context}

[질문]
{query}
&quot;&quot;&quot;
    genai.configure(api_key=os.environ[&quot;GOOGLE_API_KEY&quot;])
    # 최신 지원 모델명 자동 확인
    model_name = &quot;gemini-2.0-flash&quot;
    model = genai.GenerativeModel(model_name)
    response = model.generate_content(prompt)
    print(f&quot;\n[질문] {query}\n&quot;)
    print(f&quot;[AI 답변] {response.text.strip()}\n&quot;)
    print(&quot;-&quot; * 40)

# 8. 질의 예시
rag_ask_gemini(&quot;이 문서의 핵심 주제를 요약해줘.&quot;)
rag_ask_gemini(&quot;문서에서 등장하는 주요 인물은 누구야?&quot;)
rag_ask_gemini(&quot;문서에서 다루는 기술적 내용이 뭐야?&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것을 위한 코드는 위와 같다. 그 원리는 PDF로부터 텍스트를 추출하고 이를 chunk로 나눠 벡터DB에 저장하여 사용하는것이다. 아래 결과를 보면 알 수 있다. 아무래도 해당 기술의 핵심은 위에서 문제가 됐던 벡터DB의 검색 결과이다. 이를 위해 chunk_size와 overlap의 값 설정이 중요할텐데, 해당 값들은 다음과 같은 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;chunk_size :&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번에 벡터 임베딩해서 DB에 저장할 &amp;ldquo;텍스트 조각&amp;rdquo;의 최대 길이(자/토큰 수)이다.&lt;/li&gt;
&lt;li&gt;너무 작으면 문맥 정보가 부족해 질문-문서 매칭률이 낮아짐. &amp;ldquo;짧은 질의에만 반응하는&amp;rdquo; 쓸모없는 벡터가 많아져 검색 효율 저하&lt;/li&gt;
&lt;li&gt;너무 크면 한 chunk 안에 너무 많은 정보가 들어가 &amp;ldquo;질문-정답 구간&amp;rdquo;이 멀어지고, 임베딩이 희석되어 관련성 점수도 낮아질 수 있음. LLM 입력 컨텍스트 용량 초과(메모리, 속도 저하 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;overlap :&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk를 만들 때 앞 chunk의 끝부분 일부를 다음 chunk의 앞부분에도 포함시킨다.&lt;/li&gt;
&lt;li&gt;너무 작으면정답이 두 chunk에 걸쳐 끊기면, 일부 질문에 답을 못 찾게 됨&lt;/li&gt;
&lt;li&gt;너무 크면같은 내용이 중복으로 저장되어, DB 용량 증가, 중복 검색, 리턴 문서 중복률 상승&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 값들은 직접 적용해보면서 최적의 값을 찾아 나가야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUl7Z9/btsO8X1u9uY/o5Z6IVtTOAzbHMSjKBklT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUl7Z9/btsO8X1u9uY/o5Z6IVtTOAzbHMSjKBklT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUl7Z9/btsO8X1u9uY/o5Z6IVtTOAzbHMSjKBklT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUl7Z9%2FbtsO8X1u9uY%2Fo5Z6IVtTOAzbHMSjKBklT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;397&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행시키면 질의에 따라 나에대한 정보를 출력시킬 수 있다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/68</guid>
      <comments>https://mildwpaper.tistory.com/68#entry68comment</comments>
      <pubDate>Tue, 8 Jul 2025 17:16:29 +0900</pubDate>
    </item>
    <item>
      <title>MSA에 꼭 필요한 Terraform 사용해보기</title>
      <link>https://mildwpaper.tistory.com/67</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;테라폼(Terraform)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라를 코드로 관리하는 도구이다. 클라우드 인프라를 코드로 정의하고 자동으로 생성/변경/삭제할 수 있게 해준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜필요할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 인프라를 직접 클릭하거나 명령어로 하나하나 설정했다. 이는 반복적이고 실수가 발생하기 쉽다. 테라폼은 이를 코드로 자동화 해준다. 코드로 관리하기 때문에 버전관리(git), 리뷰, 재사용, 일관성 유지가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 근무했던 직장에서는 MSA의 구조가 아니기 때문에 한개의 프로젝트에서 많아봐야 5개의 서버 구성이 필요했다. 때문에 이렇게 적은 수의 경우는 오버헤드만 늘어날 뿐 큰 이득을 보지 못할 수 있다. 하지만 MSA처럼 다수의 서버로 구성해야하는 경우 테라폼을 사용하면 서버를 붕어빵 찍어내듯 만들 수 있고 관리가 용이하니 많은 기업에서 사용할 것 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 개념&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Provider&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;AWS, GCP, Azure, Kubernetes 등 인프라를 제공하는 플랫폼과 연결해주는 플러그인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Resource&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인프라의 구성 요소 (예: EC2 인스턴스, S3 버킷 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Module&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;반복적으로 사용할 수 있는 재사용 가능한 코드 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;State&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;현재 인프라의 상태를 기록한 파일. Terraform은 이 파일을 기준으로 변경 사항을 추적한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Plan&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 리소스가 변경되는지 미리 보여주는 단계.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Apply&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제로 인프라에 변경을 적용하는 단계.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드 작성 (.tf 파일)&lt;/li&gt;
&lt;li&gt;terraform init &amp;ndash; 필요한 플러그인 다운로드 (Provider 설치)&lt;/li&gt;
&lt;li&gt;terraform plan &amp;ndash; 어떤 리소스가 어떻게 변경되는지 미리 확인&lt;/li&gt;
&lt;li&gt;terraform apply &amp;ndash; 실제 인프라 적용&lt;/li&gt;
&lt;li&gt;terraform destroy &amp;ndash; 인프라 제거 (선택적)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 AWS와 같은 클라우드 환경이 아니더라도 docker와 k8s에 적용할 수 있다. AWS는 비용이 청구될 수 있으니 k8s를 통해 실행해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사전 환경&lt;/h4&gt;
&lt;h5&gt;HashiCorp GPG 키 및 저장소 추가&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 Ubuntu 공식 저장소에 포함되어 있지 않기 때문에 Terraform 제작사(HashiCorp)의 자체 저장소를 APT에 등록해야 한다.&lt;br /&gt;그리고 이 저장소가 진짜 HashiCorp에서 온 것인지 검증하기 위해 GPG 키를 등록하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y gnupg software-properties-common curl

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

echo &quot;deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main&quot; \
| sudo tee /etc/apt/sources.list.d/hashicorp.list&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Terraform 설치&lt;/h5&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install terraform&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;설치 확인&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform -v&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파일 생성&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;terraform {
  required_providers {
    kubernetes = {
      source  = &quot;hashicorp/kubernetes&quot;
      version = &quot;~&amp;gt; 2.27.0&quot;
    }
  }
}

provider &quot;kubernetes&quot; {
  # minikube start 를 하면 생성됨.
  config_path = &quot;~/.kube/config&quot;
}

# 1. Namespace 생성
resource &quot;kubernetes_namespace&quot; &quot;demo&quot; {
  metadata {
    name = &quot;terraform-demo&quot;
  }
}

# 2. Nginx Pod 생성
resource &quot;kubernetes_pod&quot; &quot;nginx&quot; {
  metadata {
    name      = &quot;nginx&quot;
    namespace = kubernetes_namespace.demo.metadata[0].name
    labels = {
      app = &quot;nginx&quot;
    }
  }

  spec {
    container {
      name  = &quot;nginx&quot;
      image = &quot;nginx:latest&quot;

      port {
        container_port = 80
      }
    }
  }
}

# 3. ClusterIP Service 생성
resource &quot;kubernetes_service&quot; &quot;nginx&quot; {
  metadata {
    name      = &quot;nginx-service&quot;
    namespace = kubernetes_namespace.demo.metadata[0].name
  }

  spec {
    selector = {
      app = &quot;nginx&quot;
    }

    port {
      port        = 80
      target_port = 80
    }

    type = &quot;NodePort&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 간단하게 nginx를 k8s를 통해 실행시키는 코드이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행&lt;/h4&gt;
&lt;h5&gt;terraform init&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어 실행시 현재 디렉토리에 있는 모든 &lt;code&gt;.tf&lt;/code&gt; 파일을 실행시킨다. 이때 &lt;code&gt;.terraform/&lt;/code&gt; 디렉토리가 생성되며, 파일에 설정한 플러그인 또한 설치된다.&lt;/p&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;terraform plan&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어는 실제 적용 전에 무엇이 변경될지 미리 보여준다.&lt;br /&gt;어떤 리소스가 생성/변경/삭제될지 출력해주며, 실제로 아무 것도 적용되진 않는다.&lt;/p&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;terraform apply&lt;/h5&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 .tf 파일의 정의대로 인프라를 구성한다. &lt;code&gt;&quot;Do you want to perform these actions?&quot;&lt;/code&gt;라는 메시지가 나왔을때 yes라고 입력하면 실행된다. 실행 후에는 &lt;code&gt;terraform.tfstat&lt;/code&gt;파일이 생성되는데, 이는 현재 인프라 상태를 추적한다. 임의 수정은 하지 않는것을 권장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btPAjG/btsO6hkE0jW/FINqe4JKmUDl5Gtjxz5RK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btPAjG/btsO6hkE0jW/FINqe4JKmUDl5Gtjxz5RK0/img.png&quot; data-alt=&quot;실행이 잘 된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btPAjG/btsO6hkE0jW/FINqe4JKmUDl5Gtjxz5RK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtPAjG%2FbtsO6hkE0jW%2FFINqe4JKmUDl5Gtjxz5RK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;300&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행이 잘 된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;terraform destroy&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.tf&lt;/code&gt; 파일로 만든 인프라를 모두 삭제한다. &lt;code&gt;.tfstat&lt;/code&gt;파일 기준으로 만든 리소스를 삭제한다. 도커라면 이미지까지 삭제해준다. 다만 사용하고 있는 이미지라면 삭제하지 못한다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/67</guid>
      <comments>https://mildwpaper.tistory.com/67#entry67comment</comments>
      <pubDate>Sat, 5 Jul 2025 09:47:29 +0900</pubDate>
    </item>
    <item>
      <title>ElasticSearch 인덱스 갱신을 위한 Debezium 사용해보기</title>
      <link>https://mildwpaper.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ElasticSearch를 이용한 검색 기능 만들어보기에서 백엔드에서 RDB에 데이터를 저장후에 ES인덱싱까지 해주는 과정을 거쳤었다. 이는 RDB에 저장후 ES로직에서 에러가 발생할경우 정합성이 안맞을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium은 DB의 실제 변경사항을 토대로 Kafka메시지를 발행한다. 이를 통해 정합성을 어느정도 맞출 수 있다. 다음 과정을 통해 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 필요한 환경은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- mariadb&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- elasticsearch&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- zookeeper&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- kafka&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- debezium&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서버들은 docker를 통해 간단히 세팅해둘 수 있다. 아래 docker-compose.yml을 참고하자&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1750562543946&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.7&quot;

services:
  maria01:
    image: mariadb:latest
	command:
      --server-id=1
      --log-bin=mysql-bin
      --binlog-format=ROW
    container_name: maria01
    restart: always
    ports:
      - &quot;3306:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: 1111
      MYSQL_DATABASE: es
      MYSQL_USER: user
      MYSQL_PASSWORD: userpw
    volumes:
      - ~/data/mariadb:/data

  es01:
  
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    deploy:
      resources:
        limits:
          memory: 4g
    container_name: es01
    environment:
      - discovery.type=single-node
      - ELASTIC_PASSWORD=1111
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
      - TZ=Asia/Seoul
    ports:
      - &quot;9200:9200&quot;
    volumes:
      - ~/data/es:/usr/share/elasticsearch/data

  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.2
    container_name: zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - &quot;2181:2181&quot;
    volumes:
      - ~/data/zookeeper:/var/lib/zookeeper/data

  kafka:
    image: confluentinc/cp-kafka:7.5.2
    container_name: kafka
    depends_on:
      - zookeeper
    ports:
      - &quot;9092:9092&quot;
      - &quot;29092:29092&quot;
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT2://localhost:29092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT2://0.0.0.0:29092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT2:PLAINTEXT
    volumes:
      - ~/data/kafka:/var/lib/kafka/data

  connect:
    image: debezium/connect:2.6
    container_name: connect
    depends_on:
      - kafka
      - maria01
      - es01
    ports:
      - &quot;8083:8083&quot;
    environment:
      - BOOTSTRAP_SERVERS=kafka:9092
      - GROUP_ID=1
      - CONFIG_STORAGE_TOPIC=my_connect_configs
      - OFFSET_STORAGE_TOPIC=my_connect_offsets
      - STATUS_STORAGE_TOPIC=my_connect_statuses
      - CONNECT_KEY_CONVERTER=org.apache.kafka.connect.json.JsonConverter
      - CONNECT_VALUE_CONVERTER=org.apache.kafka.connect.json.JsonConverter
      - CONNECT_REST_ADVERTISED_HOST_NAME=connect
      - CONNECT_PLUGIN_PATH=/kafka/connect,/usr/share/java,/etc/kafka-connect/jars
      - CONNECT_DEBEZIUM_SNAPSHOT_MODE=initial
    restart: always&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium이 DB에 접근하여 변경사항을 확인 후 Kafka메시지를 발행하기 위해서는 몇몇 권한이 필요하다. 때문에 아래와 같은 명령어를 통해 설정해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1750562645665&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker container 접근 명령어
docker exec -it maria01 mariadb -u root -p

# 권한 부여 명령어
GRANT REPLICATION SLAVE, REPLICATION CLIENT, BINLOG MONITOR ON *.* TO 'user'@'%';
GRANT RELOAD ON *.* TO 'user'@'%';
FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;94&quot; data-start=&quot;30&quot;&gt;&lt;b&gt;REPLICATION SLAVE:&lt;/b&gt; 바이너리 로그를 읽어 복제하거나 CDC를 수행할 수 있게 해준다.&lt;/li&gt;
&lt;li data-end=&quot;158&quot; data-start=&quot;95&quot;&gt;&lt;b&gt;REPLICATION CLIENT:&lt;/b&gt; 복제 및 바이너리 로그의 상태 정보를 조회할 수 있게 해준다.&lt;/li&gt;
&lt;li data-end=&quot;230&quot; data-start=&quot;159&quot;&gt;&lt;b&gt;BINLOG MONITOR:&lt;/b&gt; 바이너리 로그 이벤트를 읽을 수 있는 권한이다.&lt;/li&gt;
&lt;li data-is-last-node=&quot;&quot; data-end=&quot;284&quot; data-start=&quot;231&quot;&gt;&lt;b&gt;RELOAD:&lt;/b&gt; 테이블, 로그, 권한 정보를 강제로 갱신할 수 있게 해준다.&lt;/li&gt;
&lt;li data-is-last-node=&quot;&quot; data-end=&quot;284&quot; data-start=&quot;231&quot;&gt;&lt;b&gt;RELOAD: &lt;/b&gt;RELOAD은 Kafka Sink Connector를 통해 발행한 Kafka메시지를 컨슘해서 테이블을 수정할 예정이기 때문에 미리 넣어두자. 테이블, 로그, 권한 정보를 강제로 갱신(플러시)할 수 있게 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 Debezium 커넥터를 Kafka Connect에 등록해서MariaDB의 job_posting 테이블에서 발생하는 변경사항(Insert/Update/Delete)을 Kafka 토픽으로 스트리밍하게 만드는 설정을 해줘야한다. 아래 명령어를 통해 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750563063801&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X POST http://localhost:8083/connectors -H &quot;Content-Type: application/json&quot; -d '{
  &quot;name&quot;: &quot;mariadb-connector&quot;,
  &quot;config&quot;: {
    &quot;connector.class&quot;: &quot;io.debezium.connector.mysql.MySqlConnector&quot;,
    &quot;database.hostname&quot;: &quot;maria01&quot;,
    &quot;database.port&quot;: &quot;3306&quot;,
    &quot;database.user&quot;: &quot;user&quot;,
    &quot;database.password&quot;: &quot;userpw&quot;,
    &quot;database.server.id&quot;: &quot;184054&quot;,
    &quot;database.include.list&quot;: &quot;es&quot;,
    &quot;table.include.list&quot;: &quot;es.job_posting&quot;,
    &quot;schema.history.internal.kafka.bootstrap.servers&quot;: &quot;kafka:9092&quot;,
    &quot;schema.history.internal.kafka.topic&quot;: &quot;dbhistory.fullfillment&quot;,
    &quot;include.schema.changes&quot;: &quot;false&quot;,
    &quot;topic.prefix&quot;: &quot;dbserver1&quot;
  }
}'&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;다른 옵션들은 어느정도 이해가 간다. 'schema.history.internal.kafka.topic'는 스키마의 변경 이력을 기록하는 토픽이다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하고 DB에 데이터를 추가, 수정, 삭제를 한다면 카프카 메시지가 발행된 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1750563175178&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 도커 컨테이너 접근
docker exec -it kafka bash
# 토픽 확인
kafka-topics --bootstrap-server localhost:9092 --list
# 메시지 확인
kafka-console-consumer --bootstrap-server localhost:9092 --topic dbserver1.es.job_posting --from-beginning&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.16.19.png&quot; data-origin-width=&quot;2842&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bP99mM/btsOL4e3f1b/vOqwB3u0cfKxxr1T6Kv9uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bP99mM/btsOL4e3f1b/vOqwB3u0cfKxxr1T6Kv9uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bP99mM/btsOL4e3f1b/vOqwB3u0cfKxxr1T6Kv9uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbP99mM%2FbtsOL4e3f1b%2FvOqwB3u0cfKxxr1T6Kv9uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2842&quot; height=&quot;502&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.16.19.png&quot; data-origin-width=&quot;2842&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1750646876684&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;schema&quot;: {
    &quot;type&quot;: &quot;struct&quot;,
    &quot;fields&quot;: [
      { &quot;type&quot;: &quot;int64&quot;, &quot;optional&quot;: false, &quot;field&quot;: &quot;id&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;company&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;location&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;category&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;title&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;description&quot; },
      { &quot;type&quot;: &quot;string&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;requirements&quot; },
      { &quot;type&quot;: &quot;boolean&quot;, &quot;optional&quot;: true, &quot;field&quot;: &quot;is_deleted&quot; }
    ],
    &quot;optional&quot;: false,
    &quot;name&quot;: &quot;dbserver1.es.job_posting.Value&quot;
  },
  &quot;payload&quot;: {
    &quot;before&quot;: null,
    &quot;after&quot;: {
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;백엔드 개발자&quot;,
      &quot;company&quot;: &quot;테크 스타트업&quot;,
      &quot;location&quot;: &quot;서울&quot;,
      &quot;category&quot;: &quot;개발&quot;,
      &quot;description&quot;: &quot;Spring Boot 기반 서버 개발 및 API 설계&quot;,
      &quot;requirements&quot;: &quot;Java 3년 이상 경력 , Spring 경험자&quot;,
      &quot;is_deleted&quot;: false
    },
    &quot;source&quot;: {
      &quot;version&quot;: &quot;2.6.2.Final&quot;,
      &quot;connector&quot;: &quot;mysql&quot;,
      &quot;name&quot;: &quot;dbserver1&quot;,
      &quot;ts_ms&quot;: 1750562152000,
      &quot;snapshot&quot;: &quot;false&quot;,
      &quot;db&quot;: &quot;es&quot;,
      &quot;sequence&quot;: null,
      &quot;table&quot;: &quot;job_posting&quot;,
      &quot;server_id&quot;: 1,
      &quot;gtid&quot;: null,
      &quot;file&quot;: &quot;mysql-bin.000002&quot;,
      &quot;pos&quot;: 4240,
      &quot;row&quot;: 0,
      &quot;thread&quot;: null,
      &quot;query&quot;: null
    },
    &quot;op&quot;: &quot;c&quot;,
    &quot;ts_ms&quot;: 1750562152114,
    &quot;ts_us&quot;: 1750562152114173695,
    &quot;transaction&quot;: null
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;887&quot; data-start=&quot;756&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;793&quot; data-start=&quot;756&quot;&gt;before: 변경 전 레코드, Insert의 경우 null&lt;/li&gt;
&lt;li data-end=&quot;836&quot; data-start=&quot;794&quot;&gt;after: 변경 후 레코드&lt;/li&gt;
&lt;li data-end=&quot;887&quot; data-start=&quot;837&quot;&gt;op: 동작 타입 c = create, u = update, d =delete&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;998&quot; data-start=&quot;889&quot; data-ke-size=&quot;size16&quot;&gt;실제 ES에 넣을 때는 이 중에서 after만 추출해서 넣는 경우가 많다. 이럴 때 사용하는 게 바로 Kafka Connect의 Transform 옵션 중 unwrap이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 해당 메시지를 컨슘해서 DB를 갱신시키는 Kafka Sink Connector를 설정해야한다. 기존에 띄워놓은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;debezium에 플러그인을 설치하여 진행해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 &lt;a href=&quot;https://www.confluent.io/hub/confluentinc/kafka-connect-elasticsearch&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.confluent.io/hub/confluentinc/kafka-connect-elasticsearch&lt;/a&gt; 사이트에서 zip파일을 다운로드 후 파일을 컨테이너 안의 kafka/connect 안으로 옮겨준 뒤 restart해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750563377979&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker cp ./confluentinc-kafka-connect-elasticsearch-15.0.0 connect:/kafka/connect/
docker restart connect
# 플러그인 확인 명령어
curl http://localhost:8083/connector-plugins&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 확인 명령어를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;io.confluent.connect.elasticsearch.ElasticsearchSinkConnector가 있는지 잘 확인해주자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 아까와 비슷하게 Kafka 토픽(dbserver1.es.job_posting)에 쌓인 MariaDB 변경 데이터를 Elasticsearch로 자동 연동(동기화)하도록 Kafka Connect Sink 커넥터를 등록하는 과정을 거쳐야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1750563501297&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X POST http://localhost:8083/connectors -H &quot;Content-Type: application/json&quot; -d '{
  &quot;name&quot;: &quot;es-sink-connector&quot;,
  &quot;config&quot;: {
    &quot;connector.class&quot;: &quot;io.confluent.connect.elasticsearch.ElasticsearchSinkConnector&quot;,
    &quot;tasks.max&quot;: &quot;1&quot;,
    &quot;topics&quot;: &quot;dbserver1.es.job_posting&quot;,
    &quot;connection.url&quot;: &quot;http://es01:9200&quot;,
    &quot;type.name&quot;: &quot;_doc&quot;,
    &quot;key.ignore&quot;: &quot;true&quot;,
    &quot;schema.ignore&quot;: &quot;true&quot;,
    &quot;transforms&quot;: &quot;unwrap&quot;,
    &quot;transforms.unwrap.type&quot;: &quot;io.debezium.transforms.ExtractNewRecordState&quot;
  }
}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tasks.max옵션을 통해 실행될 최대 태스크 개수를 조절하여 병렬 처리도 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key.ignore는 Kafka 메시지의 key 값을 무시할지 여부를 결정한다. (true는 무시) 보통 Debezium의 메시지는 key가 따로 필요하지 않으므로 true로 설정한다. 왜냐하면, 대부분의 ES 연동 실무에서는 ES의 document ID는 자동 생성하거나 Debezium value에서 따로 ID 필드를 지정해서 사용하기 때문이다. 즉, Debezium 메시지의 Kafka key를 굳이 ES의 document ID로 직접 매핑하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;schema.ignore는 Kafka 메시지의 스키마 정보를 무시할지 여부를 결정한다. true로 하면 스키마 정보 없이 값만 사용. Debezium은 기본적으로 schema 포함, ES에는 주로 값만 넣기 때문에 true를 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transforms에는 여러가지 옵션이 있으니 상황에 따라 맞춰 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;transforms&quot;: &quot;unwrap,createKey,addField&quot;, 이런식으로 여러개도 적용 가능하다는 점 참고하자.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;unwrap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;unwrap&quot;&lt;/td&gt;
&lt;td&gt;Debezium 메시지에서 after만 추출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;createKey&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;createKey&quot;&lt;/td&gt;
&lt;td&gt;value의 특정 필드를 key로 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;addField&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;addField&quot;&lt;/td&gt;
&lt;td&gt;value에 새 필드 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;replace&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;replace&quot;&lt;/td&gt;
&lt;td&gt;불필요/민감 필드 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mask&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;mask&quot;&lt;/td&gt;
&lt;td&gt;value의 특정 필드 마스킹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;tsConvert&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;tsConvert&quot;&lt;/td&gt;
&lt;td&gt;타임스탬프 포맷 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;hoist&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;hoist&quot;&lt;/td&gt;
&lt;td&gt;value 전체를 특정 필드로 래핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;route&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;transforms&quot;: &quot;route&quot;&lt;/td&gt;
&lt;td&gt;토픽명 패턴 변경(라우팅)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램3.drawio.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ymOei/btsOKIDXc3O/GnHIIEhQ8zpscFkaEVg74k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ymOei/btsOKIDXc3O/GnHIIEhQ8zpscFkaEVg74k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ymOei/btsOKIDXc3O/GnHIIEhQ8zpscFkaEVg74k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FymOei%2FbtsOKIDXc3O%2FGnHIIEhQ8zpscFkaEVg74k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;243&quot; data-filename=&quot;제목 없는 다이어그램3.drawio.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 까지 했다면 모든 세팅이 완료되어 위 와 같은 구조가 된것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.17.42.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT3TSz/btsONayCbhi/IfTxaHKwRx5ZYILel1jrAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT3TSz/btsONayCbhi/IfTxaHKwRx5ZYILel1jrAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT3TSz/btsONayCbhi/IfTxaHKwRx5ZYILel1jrAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT3TSz%2FbtsONayCbhi%2FIfTxaHKwRx5ZYILel1jrAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1560&quot; height=&quot;1538&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.17.42.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 ES에 요청하여 검색결과를 볼 수 있었으며,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.16.42.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVGuh/btsONcJX0lY/DRbUXMVxh8qP84ATzIt4C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVGuh/btsONcJX0lY/DRbUXMVxh8qP84ATzIt4C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVGuh/btsONcJX0lY/DRbUXMVxh8qP84ATzIt4C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVGuh%2FbtsONcJX0lY%2FDRbUXMVxh8qP84ATzIt4C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3104&quot; height=&quot;2024&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.16.42.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지도 잘 뜨는것을 확인 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우려되는점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium을 사용하면서 우려해야할 몇몇 점들이 있다. 특히 DB 컬럼 변경 후 ES 인덱스 매핑을 갱신하지 않는다면 에러가 발생하고, Sink Connector가 지속적으로 적재 실패할것이다. 또한 ES 서버의 순간적 다운타임으로 인한 적재 실패 및 Connector 장애가 발생할 수 있다.&lt;br /&gt;&lt;br /&gt;이런 이슈를 예방/조치하기 위해, Connector의 `Dead Letter Queue` 기능을 활용하여 실패 건을 별도 토픽에 저장하고,&amp;nbsp; Kafka, ES, Connector의 장애/에러를 Slack/Email 등으로 실시간 알림과 ES, Kafka, DB 스키마/매핑을 사전에 주기적으로 검증이 필요하겠다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/66</guid>
      <comments>https://mildwpaper.tistory.com/66#entry66comment</comments>
      <pubDate>Sun, 22 Jun 2025 12:40:49 +0900</pubDate>
    </item>
    <item>
      <title>ElasticSearch를 이용한 검색 기능 만들어보기 - 1</title>
      <link>https://mildwpaper.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘의 목표는 ES를 활용하여 JobBoard 사이트를 개발하는것이다. 간단한 조회기능부터, SwaggerUI를 통한 create, update, delete기능까지 만들것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단한 사이트부터 만들어보자. 요즘 AI는 간단한 프론트 페이지 정도는 가볍게 구현해주니. AI를 활용해서 HTML, CSS, JS를 통해 페이지를 하나 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-18 오후 3.19.45.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;1974&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cV6iiF/btsOLcqSZBr/1kzJWJWfvOqZzvAyiKFTR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cV6iiF/btsOLcqSZBr/1kzJWJWfvOqZzvAyiKFTR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV6iiF/btsOLcqSZBr/1kzJWJWfvOqZzvAyiKFTR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcV6iiF%2FbtsOLcqSZBr%2F1kzJWJWfvOqZzvAyiKFTR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3104&quot; height=&quot;1974&quot; data-filename=&quot;스크린샷 2025-06-18 오후 3.19.45.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;1974&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 데이터 모델을 설계하고 해당 모델을 토대로 더미를 넣어 어떤식으로 출력된는지 확인한다. 위의 사진 정도면 어느정도 몰입해서 할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 백엔드 개발을 진행해야한다. 백엔드 프로젝트의 구조는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1750414818004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├── application
│&amp;nbsp;&amp;nbsp; └── service
├── common
│&amp;nbsp;&amp;nbsp; └── config
├── domain
│&amp;nbsp;&amp;nbsp; ├── model
│&amp;nbsp;&amp;nbsp; ├── repository
│&amp;nbsp;&amp;nbsp; └── service
├── infrastructure
│&amp;nbsp;&amp;nbsp; ├── config
│&amp;nbsp;&amp;nbsp; ├── entity
│&amp;nbsp;&amp;nbsp; └── repository
└── presentation
    └── controller&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처 구조를 통해 개발하려고했다. 클린 아키텍처는 소프트웨어를 여러 계층으로 분리하여, 핵심 비즈니스 로직(엔티티)이 외부 요소(UI, DB 등)에 영향을 받지 않도록 설계하는 아키텍처이다. 모든 의존성은 바깥에서 안쪽으로만 향하며, 이를 통해 관심사의 분리, 낮은 결합도, 높은 테스트 용이성, 유지보수와 확장성을 달성할 수 있다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;layer간의 객체 매핑은 mapstruct를 통해 깔끔하게 진행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 조회 로직을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1750415159413&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public SearchResponse&amp;lt;JobPostingEsDoc&amp;gt; searchIds(String keyword, int page, int size) {
        try {
            return esClient.search(s -&amp;gt; s
                            .index(&quot;jobposting&quot;)
                            .size(size)
                            .from(page * size)
                            .query(q -&amp;gt; q.match(m -&amp;gt; m.field(&quot;title&quot;).query(keyword))),
                    JobPostingEsDoc.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 로직은 JobPostingEsRepository.class중 일부이다. 페이징 처리를 포함한 Es의 간단한 조회로직을 통해 Es로부터 값을 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750415226721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public JobPostingSearchRs findBySearch(PostingSearchRq rq) {
        SearchResponse&amp;lt;JobPostingEsDoc&amp;gt; response = jobPostingEsRepository.searchIds(rq.getQ(), rq.getPage()-1, rq.getSize());

        List&amp;lt;Long&amp;gt; ids = response.hits().hits().stream()
                .map(hit -&amp;gt; hit.source().id())
                .toList();

        List&amp;lt;JobPostingDto&amp;gt; jobPosting = jobPostingDomainService.findByIds(ids).stream().map(jobPostingDtoMapper::toDto).toList();

        return new JobPostingSearchRs(jobPosting, rq.getPage(), rq.getSize(), response.hits().total().value());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 JobPostingService.class에서 JobPostingEsDoc Response를 받아 id List를 통해 RDB로 요청하여 JobPosting데이터를 받아온다. JobPostingEsDoc에도 동일한 값이 있겠지만, 이렇게 한 이유는 실무에서는 RDB로부터 값을 가져와 사용해야 할 (댓글, 좋아요 등)정보 등 이 있을 것이기 때문에 이렇게 코드를 구현해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 트랜잭션이 분리되어, RDB에 먼저 저장하고 이후에 Es에 저장하는 방식이기 때문에, 특정 구간에서 RDB와 Es의 정합성이 안맞을 수 있으므로 유의해야한다. 이 때문에 실시간성이 필요한 데이터는 RDB로부터 데이터를 가져오고 Es는 필터 역할만 하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750415466896&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public void save(JobPosting jobPosting) {
        JobPostingEsDoc doc = jobPostingMapper.toEsDoc(jobPosting);

        try {
            IndexResponse response = esClient.index(i -&amp;gt; i
                    .index(&quot;jobposting&quot;)
                    .id(String.valueOf(doc.id()))
                    .document(doc)
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium을 사용하기 전 단계이기 때문에, 테스트용으로 코드상에서 데이터 저장시 Es의 인덱스를 갱신해 주도록 코드를 구현해 놨다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 4.11.39.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RPFOc/btsOLFM15KZ/MPZIs5E3fBVezOk05Z4xH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RPFOc/btsOLFM15KZ/MPZIs5E3fBVezOk05Z4xH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RPFOc/btsOLFM15KZ/MPZIs5E3fBVezOk05Z4xH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRPFOc%2FbtsOLFM15KZ%2FMPZIs5E3fBVezOk05Z4xH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3104&quot; height=&quot;2024&quot; data-filename=&quot;스크린샷 2025-06-20 오후 4.11.39.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 저장하면 위 사진과 같이 Es에 저장된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 과정을 통해&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.07.49.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBcDzc/btsOLeI3wxI/XW08pKjFCmQB55juampNk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBcDzc/btsOLeI3wxI/XW08pKjFCmQB55juampNk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBcDzc/btsOLeI3wxI/XW08pKjFCmQB55juampNk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBcDzc%2FbtsOLeI3wxI%2FXW08pKjFCmQB55juampNk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3104&quot; height=&quot;2024&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.07.49.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Es를 활용하여 간단한 JobBoard의 검색기능을 만들 수 있었다. 실무에서는 더 복잡한 필터링과 비즈니스 로직이 있겠지만, Debezium을 사용하여 데이터가 갱신되는것을 먼저 보고싶으니 나중에 개선해 볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium은 코드 오류, 네트워크 장애 등으로 인해 RDB에는 저장됐지만 ES에는 누락/지연되는 동기화 문제를 해결할 수 있다 또한 DB의 로그에서 직접 변경 이벤트를 추출하기 때문에 실제로 커밋된 변경사항만 감지하므로, &amp;ldquo;DB와 100% 일치&amp;rdquo;를 목표로 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램2.drawio.png&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7GfO1/btsOKtfCWNz/AkMHKx4TS61kUqO0W6f8xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7GfO1/btsOKtfCWNz/AkMHKx4TS61kUqO0W6f8xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7GfO1/btsOKtfCWNz/AkMHKx4TS61kUqO0W6f8xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7GfO1%2FbtsOKtfCWNz%2FAkMHKx4TS61kUqO0W6f8xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;156&quot; data-filename=&quot;제목 없는 다이어그램2.drawio.png&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 위와같은 비슷한 구조가 (API Gateway는 없지만)완성됐다. 다음으로는 Debezium을 사용해서 데이터를 갱신시키고 갱신된 데이터를 조회하는것 까지 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;진행하면서 발생한 문제&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Es를 쓰다보니 한글에 대한 검색시 형태소 분석기에 문제가 있었다. 해당 문제는 &quot;개발&quot;이라고 검색하면, &quot;개발자&quot;도 포함되어 검색이 되어야하는데 그렇지 않았던 이슈이다. 이를 해결하기위해 nori라는 형태소 분석기 플러그인을 설치하고 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750415770766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it &amp;lt;es-container-id&amp;gt; bin/elasticsearch-plugin install analysis-nori
docker restart &amp;lt;es-container-id&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인은 도커를 사용하고 있으므로 컨테이너에 접근해서 직섭 install 해주는 과정을 거친 후에 컨테이너를 restart 하여 설치를 완료했다. 유의할점은 해당 플러그인을 적용하기 위해서는 인덱스를 지우고 다시 PUT해야 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 5.18.09.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uv6J9/btsOL4yZVWL/Cuqd9PAi2ZuFkp1C9shQFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uv6J9/btsOL4yZVWL/Cuqd9PAi2ZuFkp1C9shQFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uv6J9/btsOL4yZVWL/Cuqd9PAi2ZuFkp1C9shQFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuv6J9%2FbtsOL4yZVWL%2FCuqd9PAi2ZuFkp1C9shQFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3104&quot; height=&quot;2024&quot; data-filename=&quot;스크린샷 2025-06-20 오후 5.18.09.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하여 위와같은 명령어 처리를 거친 후에야 &quot;개발&quot;을 입력해서 개발자와 연관된 데이터들을 리턴받을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/65</guid>
      <comments>https://mildwpaper.tistory.com/65#entry65comment</comments>
      <pubDate>Fri, 20 Jun 2025 19:39:27 +0900</pubDate>
    </item>
    <item>
      <title>ElasticSearch를 이용한 검색 기능 만들어보기 - 0</title>
      <link>https://mildwpaper.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 기업에서 ElasticSearch와 Debezium 그리고 테라폼 경험을 요구하고있다. 테라폼은 ElasticSearch를 먼저 경험하고 난 뒤에 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단한 설계를 해보자. 모두 구현할것은 아니지만, 만약 사용한다면 이러한 그림이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자는 클라이언트를 통해 검색 요청을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클라이언트는 API Gateway를 통해 Backend 서버로 요청 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 서버는 비즈니스 로직을 통해 검색어를 전처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 서버는 ES를 통해 문서 검색을한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 서버는 ES를 통해 받아온 문서 id를 통해 DB에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. DB에 조회 요청을 할때는 좋아요 수, 댓글 수 등 Es에 없는 추가 정보를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 클라이언트로 데이터를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F762G/btsOKjK3mqc/25fJcrORIUzx6wk53dh3c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F762G/btsOKjK3mqc/25fJcrORIUzx6wk53dh3c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F762G/btsOKjK3mqc/25fJcrORIUzx6wk53dh3c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF762G%2FbtsOKjK3mqc%2F25fJcrORIUzx6wk53dh3c0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;156&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 쓰기 또는 수정, 삭제 작업의 경우 아래와 같이 진행될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자는 쓰기,수정,삭제 중 한 작업을 클라이언트로 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클라이언트는 API Gateway를 거쳐 Backend 서버로 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Backend는 DB정보에 데이터를 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Backend는 ES 색인을 갱신하기 위해 비동기메시지(Kafka)를 통해 메시지를 퍼블리싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 갱신서버에서는 메시지를 컨슘 후 ES의 색인을 갱신한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램.drawio.png&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGOgzx/btsOFBLu8V3/tQbdbkdN9Xz9umEKG2Loc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGOgzx/btsOFBLu8V3/tQbdbkdN9Xz9umEKG2Loc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGOgzx/btsOFBLu8V3/tQbdbkdN9Xz9umEKG2Loc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGOgzx%2FbtsOFBLu8V3%2FtQbdbkdN9Xz9umEKG2Loc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;234&quot; data-filename=&quot;제목 없는 다이어그램.drawio.png&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때의 과정에서 Debezium가 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Change Data Capture(CDC)를 구현하는 오픈소스 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC란 데이터베이스에서 발생하는 변경 사항(INSERT, UPDATE, DELETE)을 실시간으로 캡쳐하여 이벤트 형태로 다른 시스템으로 전송하는 기술이다. Debezium은 이러한 변경 이벤트를 Apache Kafka와 같은 스트리밍 시스템으로 전달하여 다른 저장소로 실시간 데이터베이스 변경 사항을 처리할 수 있도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램3.drawio.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lWBhR/btsOGqboBTd/vzMDDMdns4qxN5uQmzZfxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lWBhR/btsOGqboBTd/vzMDDMdns4qxN5uQmzZfxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lWBhR/btsOGqboBTd/vzMDDMdns4qxN5uQmzZfxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlWBhR%2FbtsOGqboBTd%2FvzMDDMdns4qxN5uQmzZfxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;243&quot; data-filename=&quot;제목 없는 다이어그램3.drawio.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium을 사용하기 된다면 위와 같은 구조가 될 것이다. 이와 같이 방향성을 잡고 진행해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/64</guid>
      <comments>https://mildwpaper.tistory.com/64#entry64comment</comments>
      <pubDate>Wed, 18 Jun 2025 15:08:13 +0900</pubDate>
    </item>
    <item>
      <title>[book] 스프링으로 시작하는 리액티브 프로그래밍 - 0</title>
      <link>https://mildwpaper.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 프로그래밍이란?&lt;br /&gt;리액티브 시스템은 비동기 메시지 통신을 기반으로 한다. 비동기 메시지 통신은 Blocking I/O 방식이 아닌 Non-Blocking I/O 방식의 통신&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선언형 프로그래밍
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선언형 프로그래밍 방식은 실행할 동작을 구체적으로 명시하지 않고 이러이러한 동작을 하겠다는 목표만 선언한다.&lt;/li&gt;
&lt;li&gt;C언어나 Java는 명령형 프로그래밍 방식이다. 실행할 동작을 구체적으로 명시하는 프로그래밍 코드 형태.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 스트림
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 스트림이란 데이터가 지속적으로 발생한다는 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;public class Examplel_1 {

    public static void main (String[] args) {

        List&amp;lt;Integer&amp;gt; numbers = Arrays asList(1, 3, 21, 10, 8, 11);

        int sum = 0 ;

        for(int number : numbers){

            if number &amp;gt; 6 &amp;amp;&amp;amp; (number % 2 ! = 0)) {
                sum += number;
            }
        }

        System.out.println(7|: &quot; + sum);

    }
}

public class Examplel_2 {

    public static void main (String[] args) {

        List&amp;lt;Integer&amp;gt; numbers = Arrays asList (1, 3, 21, 10, 8, 11);

        int sum = numbers.stream()
            .filter (number -&amp;gt; number &amp;gt; 6 &amp;amp;&amp;amp; (number % 2 ! = 0))
            .mapToInt (number -&amp;gt; number)
            .sum();

        System.out. println(&quot;sum: &quot; + sum) ;

    ｝

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1은 명령형, 예제 2는 선언형프로그래밍 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 2에서는 마치 'for문을 돌면서 numbers List에 포함된 숫자들에 하나씩 접근하겠어'라고 내가 할 동작을 직접 설명하기보다는 'numbers List에 포함된 숫자들에 접근을 좀 해줘'라고 내가 아닌 다른 누군가에게 부탁하는 것과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 코드 1-1에서는 if문을 사용한 반면에, 코드 1-2에서는 if문 대신에 filter 메서드를 사용해서 조건에 맞는 데이터를 필터링하며(5번 라인), Sum 메서드를 사용하여 filter 메서드에서 필터링된 숫자들의 합계를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 1-1에서는 조건에 맞는 숫자들을 sum 변수에 하나씩 더하는 동작을 직접 했지만, 코드 1-2에서는 Sum이라는 메서드를 선언만 했지 구체적인 숫자를 더 하는 동작은 스트림 내부에서 대신 처리해 주는 모습을 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 프로그래밍 코드 구성&lt;br /&gt;Publisher : 데이터를 제공하는 역할&lt;br /&gt;Subscriber : Publisher가 제공한 데이터를 전달받아서 사용하는 주체&lt;br /&gt;Data Source : Publisher의 입력으로 들어오는 데이터를 대표하는 용어&lt;br /&gt;Operator : Publisher Subscriber 사이에서 가공 처리를 담당&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;리액티브 스트림즈&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 리액티브한 코드를 작성하기 위해서는 코드 구성을 용이하게 해주는 라이브러리가 있어야한다. 라이브러리를 어떻게 구현할지 정의해 놓은 별도의 표준 사양이 리액티브 스트림즈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 스트림즈를 구현한 구현체로 RxJava, Reactor, Akka Streams, Java 9 Flow API 등이 있다.&lt;br /&gt;Spring framework는 Reactor와 궁합이 가장 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 스트림즈 구성 요소&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Publisher&lt;/td&gt;
&lt;td&gt;데이터를 생성하고 통지(발행, 게시, 방출)하는 역할을 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscriber&lt;/td&gt;
&lt;td&gt;구독한 Publisher로부터 통지(발행, 게시, 방출)된 데이터를 전달받아서 처리하는 역 할을 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Publisher에 요청할 데이터의 개수를 지정하고, 데이터의 구독을 취소하는 역할을 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processor&lt;/td&gt;
&lt;td&gt;Publisher와 Subscriber의 기능을 모두 가지고 있다. 즉, Subscriber로서 다른 Publisher를 구독할 수 있고, Publisher로서 다른 Subscriber가 구독할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher와 Subscriber간의 데이터 전달 과정은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 Subscriber는 전달받을 데이터를 구독한다(subscribe).&lt;/li&gt;
&lt;li&gt;다음으로 Publisher는 데이터를 발행할 준비가 되었음을 Subscriber에 알린다(onSubscribe).&lt;/li&gt;
&lt;li&gt;Publisher가 데이터를 통지할 준비가 되었다는 알림을 받은 Subscriber 는 전달받기를 원하는 데이터의 개수를 Publisher에게 요청합니다.(Subscription.request)&lt;/li&gt;
&lt;li&gt;다음으로 Publisher는 Subscriber로부터 요청받은 만큼의 데이터를 발행한다(onNext)&lt;/li&gt;
&lt;li&gt;이렇게 Publisher와 Subscriber 간에 데이터 통지, 데이터 수신, 데이 터 요청의 과정을 반복하다가 Publisher가 모든 데이터를 통지하게 되 면 마지막으로 데이터 전송이 완료되었음을 Subscriber에게 알린다 (onComplete). 만약에 Publisher가 데이터를 처리하는 과정에서 에러가 발생하면 에러가 발생했음을 Subscriber에게 알린다(onError).&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface Publisher&amp;lt;T&amp;gt; {
    public void subscribe(Subscriber&amp;lt;? super T&amp;gt; s) ;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka의 경우 Publisher와 Subscriber 사이에 브로커가 존재하여 토픽을 바라보면 되지만, 리액티브 스트림즈에서는 브로커가 없기때문에 Publisher가 Subscriber를 등록하는 형태로 구독이 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface Subscriber&amp;lt;T&amp;gt; {
    public void onSubscribe (Subscription s);
    public void onNext(T t);
    public void onError (Throwable t) ;
    public void onComplete();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onSubscribe 메서드는 구독 시작 시점에 어떤 처리를 하는 역할을 한다. 여기서의 처리는 Publisher에게 요청할 데이터의 개수를 지정하거나 구독을 해지하는 것을 의미하는데, 이것은 onSubscribe 메서드의 파라미터로 전달 되는 Subscription 객체를 통해서 이루어진다.&lt;/li&gt;
&lt;li&gt;onNext 메서드는 Publisher가 통지한 데이터를 처리하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;onError 메서드는 Publisher가 데이터 통지를 위한 처리 과정에서 에러가 발생했을 때 해당 에러를 처리하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;onComplete 메서드는 Publisher가 데이터 통지를 완료했음을 알릴 때 호출 되는 메서드이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscription&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public interface Subscription {
    public void request(long n);
    public void cancel();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscription가 구독한 데이터의 개수를 요청하거나, 구독을 해지하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용어 설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signal : Publisher와 Subscriber가 주고받는 상호작용&lt;br /&gt;demand : Subscriber가 Publisher에게 요청하는 데이터를 의미&lt;br /&gt;emit : Publisher의 데이터 발행&lt;br /&gt;Upstream/Downstream&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class Example2_5 {

    public static void main (Stringll args) {
        Flux
            .just (1, 2, 3, 4, 5, 6)
            .filter (n -&amp;gt; n % 2 == 0)
            .map (n &amp;rarr;&amp;gt; n * 2)
            .subscribe(System.out::println) ;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.just().filter().map.subcribe() 와같이 하나로 연결된 것 처럼 보이는것을 메서드 체인이라고 한다.&lt;br /&gt;map을 기준으로 예를 들면 map 위에 있는 filter, just는 upstream이고 subscribe는 downstream이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sequence : publisher가 emit하는 데이터의 연속적인 흐름을 정의, flux를 통해 데이터를 생성, emit하고 filter메서드를 통해 필터링 후 map메서드를 통해 변환하는 과정 자체를 sequence라 부름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;source : '최초의' 라는 의미로 사용. original이라는 용어로도 사용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;publisher 구현을 위한 주요 기본 규칙&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;규칙&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Publisher가 Subscriber에게 보내는 onNext signal의 총 개수는 항상 해당 Subscriber의 구독을 통해 요청된 데이터의 총 개수보다 더 작거나 같아야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Publisher는 요청된 것보다 적은 수의 onNext signal을 보내고 onComplete 또는 onError를 호출하여 구독을 종료할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Publisher의 데이터 처리가 실패하면 onError signal을 보내야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Publisher의 데이터 처리가 성공적으로 종료되면 onComplete signal을 보내야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Publisher가 Subscriber에게 onError 또는 onComplete signal을 보내는 경우 해당&lt;br /&gt;&lt;br /&gt;Subscriber의 구독은 취소된 것으로 간주되어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;일단 종료 상태 signal을 받으면(onError, onComplete) 더 이상 signal이 발생되지 않아야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;구독이 취소되면 Subscriber는 결국 signal을 받는 것을 중지해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber 구현을 위한 기본 규칙&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;규칙&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Subscriber는 Publisher로부터 onNext signal을 수신하기 위해 Subscription.request(n) 를 통해 Demand signa을 Publisher에게 보내야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Subscriber.onComplete() 및 Subscriber.onError(Throwable t)는 Subscription 또는 Publisher의 메서드를 호출해서는 안 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Subscriber.onComplete() 및 Subscriber.onError(Throwable t)는 signal을 수신한 후 구 독이 취소된 것으로 간주해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;구독이 더 이상 필요하지 않은 경우 Subscriber는 Subscription.cancel()을 호출해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Subscriber.onSubscribe( )는 지정된 Subscriber에 대해 최대 한 번만 호출되어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscription 구현을 위한 주요 기본 규칙&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;규칙&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;구독은 Subscriber가 onNext 또는 onSubscribe 내에서 동기적으로 Subscription request 를 호출하도록 허용해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;구독이 취소된 후 추가적으로 호출되는 Subscription.request(ong n)는 효력이 없어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;구독이 취소된 후 추가적으로 호출되는 Subscription.cancel()은 효력이 없어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;구독이 취소되지 않은 동안 Subscription.requestong n)의 매개변수가 0보다 작거나 같 으면 javalang.legalArgumentException과 함께 onError signal을 보내야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;구독이 취소되지 않은 동안 Subscription.cancel()은 Publisher가 Subscriber에게 보내는 signal을 결국 중지하도록 요청해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;구독이 취소되지 않은 동안 Subscription.cancel()은 Publisher에게 해당 구독자에 대한 참조를 결국 삭제하도록 요청해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Subscription.cancel(), Subscription.request() 호출에 대한 응답으로 예외를 던지는 것 을 허용하지 않는다..&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;구독은 무제한 수의 request 호출을 지원해야 하고 최대 2^63-1개의 Demand를 지원해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Reactor&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 Srping Framework 팀의 주도하에 개발된 리액티브 스트림즈의 구현체이다. Spring WebFlux 프레임워크에 라이브러리로 포함되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor의 Publisher타입은 크게 2가지이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flux[N] : n은 n개, 즉 무한대의 데이터를 emit할 수 있다.&lt;/li&gt;
&lt;li&gt;Mono[0|1] : 0개 또는 1개 의 데이터만 emit하는 단발성 데이터에 특화된 퍼블리셔이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 Backpressure-Ready network라는 publicsher로부터 전달받은 데이터를 처리하는 데 있어 과부하가 걸리지 않도록 제어하는 기능을 제공&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Example5_1 {

    public static void main (Stringll args) {

    Flux&amp;lt;String&amp;gt; sequence = Flux.just(&quot;Hello&quot;, &quot;Reactor&quot;);
    sequence.map(data -&amp;gt; data. toLowerCase ( ))
        .subscribe(data -&amp;gt; System.out.println(data));

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;Hello&quot;, &quot;Reactor&quot;&lt;/code&gt; -&amp;gt; 데이터 소스&lt;br /&gt;Flux -&amp;gt; Publisher&lt;br /&gt;&lt;code&gt;data -&amp;gt; System.out.println(data)&lt;/code&gt; -&amp;gt; subscriber&lt;br /&gt;&lt;code&gt;.just(&quot;Hello&quot;, &quot;Reactor&quot;), data -&amp;gt; data. toLowerCase ( )&lt;/code&gt; -&amp;gt; Operator&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/62</guid>
      <comments>https://mildwpaper.tistory.com/62#entry62comment</comments>
      <pubDate>Wed, 11 Jun 2025 18:04:46 +0900</pubDate>
    </item>
    <item>
      <title>헥사고날 아키텍처에 관한 생각</title>
      <link>https://mildwpaper.tistory.com/61</link>
      <description>&lt;h1&gt;헥사고날 아키텍처란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 어떤 회사를 봐도 MSA에 대한 요구사항들이 있다. MSA 구조로 개발을 하다보면 자주 접하는 헥사고날 아키텍처에 대해 내 생각을 정리해보려고 한다. 헥사고날 아키텍처(Hexagonal Architecture, 또는 포트와 어댑터 아키텍처)는 비즈니스 로직을 외부 환경으로부터 분리하여 유연하고 테스트하기 쉬운 구조를 만드는 데 목적이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클린아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처(Clean Architecture)는 소프트웨어 설계에서 의존성 방향을 명확히 하여 유연하고 테스트 가능하며 유지보수가 쉬운 시스템을 만들기 위한 아키텍처 스타일이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;헥사고날 아키텍처와 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 의존성 방향은 같고, 도메인 보호, 테스트 용이성을 우선시 한다는 공통점이 있다.&lt;br /&gt;그리고 아래와 같은 차이점이 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;클린 아키텍처&lt;/th&gt;
&lt;th&gt;헥사고날 아키텍처 (Ports &amp;amp; Adapters)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;주 목적&lt;/td&gt;
&lt;td&gt;계층적 의존성 통제&lt;/td&gt;
&lt;td&gt;외부 의존성과의 유연한 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중심 요소&lt;/td&gt;
&lt;td&gt;Entity &amp;amp; Use Case&lt;/td&gt;
&lt;td&gt;Domain &amp;amp; Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의존성 방향&lt;/td&gt;
&lt;td&gt;바깥 &amp;rarr; 안쪽&lt;/td&gt;
&lt;td&gt;바깥 &amp;rarr; 안쪽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 인터페이스 명칭&lt;/td&gt;
&lt;td&gt;Interface Adapter&lt;/td&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내부 인터페이스 명칭&lt;/td&gt;
&lt;td&gt;Use Case &amp;amp; Entity&lt;/td&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;계층적, 유스케이스 중심&lt;/td&gt;
&lt;td&gt;IO와 독립된 도메인 강조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 적용 예시&lt;/td&gt;
&lt;td&gt;복잡한 비즈니스 로직 중심 앱&lt;/td&gt;
&lt;td&gt;시스템 간 통신 많은 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 클린는 복잡한 유스케이스 강조 앱, 헥사고날은 다양한 외부와의 연동 많은 시스템(API, 메시징 등)에 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 아키텍처는 다음과 같은 디렉토리 구조로 설계하는 것이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;com.example.hexagonal
├── domain
│   ├── model
│   │   └── Member.java
│   ├── port
│   │   ├── in
│   │   │   └── RegisterMemberUseCase.java
│   │   └── out
│   │       └── SaveMemberPort.java
│   └── service
│       └── MemberDomainService.java
├── application
│   └── service
│       └── RegisterMemberService.java
├── adapter
│   ├── in
│   │   └── web
│   │       └── MemberController.java
│   └── out
│       └── persistence
│           ├── MemberJpaEntity.java
│           ├── MemberRepository.java
│           └── MemberPersistenceAdapter.java
└── config
    └── BeanConfig.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 디렉토리를 보면 model 객체가 있고, port의 하위로 in/out이 존재한다. 여기에는 interface로 구성된 클래스들이 존재하는데, 이는 외부 로직으로부터 격리되기 위해서 포트를 통해 접근하도록 설계된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain.service의 MemberDomainService는 model 객체가 혼자 처리할 수 없는 비즈니스 로직이나 공용 로직을 처리한다. RegisterMemberService는 UseCase에 대한 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;adapter는 외부 소통을 담당한다. in 패키지에는 외부로부터 호출을 받아 처리하는 Controller가 있고, out에는 비즈니스 로직을 처리할때 필요한 데이터 등을 요청하기 위한 JpaEnitity이 있다. 이외에도 WebClient 등이 사용될것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 인상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조는 도메인 비즈니스 로직을 외부와 잘 격리해 놓은 듯 하다. 하지만 기능 개발시 격리를 위해 작성해야 할 오버헤드 또한 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Client &amp;rarr; Client Adapter &amp;rarr; DTO &amp;rarr; Mapping &amp;rarr; DTO &amp;rarr; Client Port &amp;rarr; Service &amp;rarr; Service Port &amp;rarr; Service Adapter &amp;rarr; Domain (Model) &amp;rarr; Mapping &amp;rarr; DTO &amp;rarr; Controller&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 각 레이어에 전달할때도 오버헤드가 존재한다. 예를들어 adapter.MemberJpaEntity, domain.model.Member가 있다. 실무 환경에서는 외부로의 데이터 통신을 위해 MemberDto 또한 존재할것이다. 이 말은 데이터의 조회, 저장, 수정시에 &lt;code&gt;MemberDto -&amp;gt; Member -&amp;gt; MemberJpaEntity&lt;/code&gt;의 변환을 거쳐야한다는소리이다. 또한 MemberJpaEntity에서 Member객체로 변환시 지연 로딩의 기능을 사용하지 못하게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 과정을 거치면서 까지 데이터를 격리하는데는 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 격리(Isolation)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;순수 도메인 로직을 유지&lt;/b&gt;하면서, 인프라 최적화나 레거시 스키마 대응을 독립적으로 관리할 수 있다. 예를들어 DB에 존재하는 shardKey, create_at, update_at soft_deleted 추가 삭제 등이 비즈니스 로직에 영향을 끼치지 않게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 단순화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 변경이 Model 테스트를 깨뜨리지 않고, Entity 매핑 코드만 단위 테스트 대상으로 분리 가능하다. 테스트 진행시 DB 연결과 CRUD에 대한 번거로움이 있는데 이를 제외할 수 있다는건 큰 장점으로 다가온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;지연로딩 제거
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA사용시 즉시로딩 지연로딩으로 인해 원하지 않는 쿼리를 발생시킬 수 있는데 이를 신경쓰지 않고 비즈니스 로직에 집중할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지연로딩 제거
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지연 로딩은 필요한 시점에만 데이터를 로딩하므로, 메모리 사용량을 줄일 수 있는 장점이 있다. 이를 사용할 수 없게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변환 과정의 오버헤드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MemberDto -&amp;gt; Member -&amp;gt; MemberJpaEntity&lt;/code&gt;와 같이 객체를 변환하는 과정이 복잡해진다. 이를 위한 MapStruct매핑 라이브러리가 있긴하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;많은 절차와 그에 따른 관리해야할 코드들
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 든 예시와 같이 &lt;code&gt;Client &amp;rarr; Client Adapter &amp;rarr; DTO &amp;rarr; Mapping &amp;rarr; DTO &amp;rarr; Client Port &amp;rarr; Service &amp;rarr; Service Port &amp;rarr; Service Adapter &amp;rarr; Domain (Model) &amp;rarr; Mapping &amp;rarr; DTO &amp;rarr; Controller&lt;/code&gt; 절차로 로직이 수행된다. 이는 코드를 관리하는 개발자가 만들어야할 파일과 코드가 늘어난다는소리이다. 이에 따라 신규 개발에서 새롭게 개발되는 api를 위해 많은 파일들을 작성해야 한다는 것 이고, 이에따라 개발 속도는 감소될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처는 비즈니스 로직을 외부 환경으로부터 분리하여 유연하고 테스트하기 쉬운 구조를 만드는 데 목적이 있다. 내가 느낀점은 외부와의 소통을 강조한 디렉토리 구조를 제외하곤 클린 아키텍처와 매우 비슷하다고 느꼈다. 또한 열심히 개발할 당시에는 도메인 중심의 개발을 하려고 했었는데, JpaEntity와 model객체의 분리는 생각을 못 했었다. 그 이유는 새롭게 추가되는 기능이 많다보니 하나의 단계가 추가되는것이 부담으로 다가왔기 때문이다. 하지만 클린 아키텍처가 지향하는 것에대해 깊이 공감하기 때문에 이러한 구조를 따라 개발하도록 노력해야 하겠다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/61</guid>
      <comments>https://mildwpaper.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 9 Jun 2025 10:52:42 +0900</pubDate>
    </item>
    <item>
      <title>유튜브 설계 (가상 면접 사례로 배우는 대규모 시스템 설계)</title>
      <link>https://mildwpaper.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브 시스템을 설계하는 것은 넷플릭스와 같은 비디오 플랫폼을 설계하는 문제에도 적용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 2020년에 조사된 결과 데이터이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월간 능동 사용자 수: 2십 억(2billion)&lt;/li&gt;
&lt;li&gt;매일 재생되는 비디오 수: 5십 억(5billion)&lt;/li&gt;
&lt;li&gt;미국 성인 가운데 73%가 유튜브 이용&lt;/li&gt;
&lt;li&gt;5천만(50million) 명의 창작자&lt;/li&gt;
&lt;li&gt;유튜브의 광고 수입은 2019년 기준으로 150억(15.1bilion) 달러이며 이는 2018년도 대비 36%가 증가한 수치&lt;/li&gt;
&lt;li&gt;모바일 인터넷 트래픽 가운데 37%를 유튜브가 점유&lt;/li&gt;
&lt;li&gt;80개 언어로 이용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브는 단순 비디오를 보는 것 말고도 댓글을 남길 수 있고, 비디오를 공유하거나 좋아요 버튼을 누를 수도 있고, 자기 재생목록에 저장하기, 구독 등 다양한 기능이 있다. 때문에 이번 설계에서는 어느정도 설계 범위를 좁혀 진행해보도록 하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 비디오 업로드&lt;/li&gt;
&lt;li&gt;원활한 비디오 재생&lt;/li&gt;
&lt;li&gt;재생 품질 선택 기능&lt;/li&gt;
&lt;li&gt;낮은 인프라 비용(infrastructure cost)&lt;/li&gt;
&lt;li&gt;높은 가용성과 규모 확장성, 그리고 안정성&lt;/li&gt;
&lt;li&gt;지원 클라이언트: 모바일 앱, 웹브라우저, 그리고 스마트 TV&lt;/li&gt;
&lt;li&gt;일간 능동 사용자(DAU: Daily Active User) 수는 5백만(5million)&lt;/li&gt;
&lt;li&gt;한 사용자는 하루에 평균 5개의 비디오를 시청&lt;/li&gt;
&lt;li&gt;10%의 사용자가 하루에 1비디오 업로드&lt;/li&gt;
&lt;li&gt;비디오 평균 크기는 300MB&lt;/li&gt;
&lt;li&gt;비디오 저장을 위해 매일 새로 요구되는 저장 용량=5백만 X 10% x 300MB = 150TB&lt;/li&gt;
&lt;li&gt;CDN 비용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 CDN을 통해 비디오를 서비스할 경우 CDN에서 나가는 데이터 의 양에 따라 과금한다.&lt;/li&gt;
&lt;li&gt;아마존의 클라우드프론트(CloudFront)를 CDN 솔루션으로 사용할 경 우, 100% 트래픽이 미국에서 발생한다고 가정하면 1GB당 S0.02의 요금 이 발생한다(그림 14-2). 문제를 단순화하기 위해 비디오 스트리밍 비용 만 따지도록 하겠다.&lt;/li&gt;
&lt;li&gt;따라서 매일 발생하는 요금은 5백만&amp;times;5비디오 x0.3GB X S0.02=$150,000 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개략적 설계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비디오 업로드 절차&lt;/li&gt;
&lt;li&gt;비디오 스트리밍 절차&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비디오 업로드 절차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 업로드 절차에는 다음과 같은 컴포넌트들로 구성되어 있다.&lt;br /&gt;사용자 : 컴퓨터나 모바일, 스마트tv를 통해 유튜브를 시청한다&lt;br /&gt;로드밸런서 : API서버 각각으로 요청을 분산한다&lt;br /&gt;API 서버 : 비디오 스트리밍을 제외한 다른 모든 요청을 처리한다.&lt;br /&gt;메타데이터DB : 비디오의 메타데이터를 보관한다. 샤딩과 다중화를 적용하여 성능 및 가용성 요구사항을 충족&lt;br /&gt;메타데이터 캐시 : 성능을 높이기 위해 비디오 메타데이터와 사용자 객체는 캐시한다.&lt;br /&gt;원본 저장소 : 원본 비디오를 보관할 대형 저장소&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[원본 저장소]
       ▲
       │
       │
[사용자 단말 (TV / 셋탑 / 모바일 / PC)]
       │
       ▼
  [로드밸런서]
       │
       ▼
   [API 서버]
     ├────────────► [메타데이터 캐시]
     │
     └────────────► [메타데이터 데이터베이스]
                            ▲
                            │
             [트랜스코딩 완료 핸들러]
                            ▲
                            │
               [트랜스코딩 완료 큐] 
                            ▲
                            │
                    [트랜스코딩 서버] ◄────[원본 저장소(위 저장소와 같은 것)]
                            │
                            ▼
                [트랜스코딩 비디오 저장소]
                            │
                            ▼
                            [CDN]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 트랜스코딩은 비디오 인코딩이라 부르기도 하는 절차로, 비디오의 포맷을 변환하는 절차다. 단말이나 대역폭 요구사항에 맞는 최적의 비디오 스트림을 제공하기 위해 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비디오 스트리밍 절차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 스트리밍이 이루어지는 절차를 논하기전에 스트리밍 프로토콜에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍 프로토콜은 비디오 스트리밍을 위해 데이터를 전송할 때 쓰이는 표준화된 통신방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MPEG-DASH. MPEG은 &amp;ldquo;Moving Picture Experts Group&quot;의 약어이며, DASH는 &quot;Dynamic Adaptive Streaming over HTTP&quot;의 약어다.&lt;/li&gt;
&lt;li&gt;애플(Apple) HLS. HIS는 &amp;ldquo;HTTP Live Streaming&amp;rdquo;의 약어다.&lt;/li&gt;
&lt;li&gt;마이크로소프트 스무드 스트리밍(Microsoft Smooth Streaming).&lt;/li&gt;
&lt;li&gt;어도비 HTTP 동적 스트리밍(Adobe HTTP Dynamic Streaming, HDs).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억해야할것은 프로토콜마다 지원하는 비디오 인코딩이 다르고 플레이어도 다르다는것이다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[사용자 단말 (TV / 셋탑 / 모바일 / PC)]
       │
       ▼
  [CDN]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상세설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오가 다른 단말에서도 순조롭게 재생되려면 다른 단말과 호환되는 비트레이트와 포맷으로 저장되어야 한다. 비트레이트는 비디오를 구성하는 비트가 얼마나 빨리 처리되어야하는지를 나타내는 단위이다. 비트레이트가 높은 비디오는 일반적으로 고화질 비디오다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 트랜스코딩은 다음과 같은 이유로 중요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가공되지 않은 원본 비디오는 저장공간을 많이 차지한다.&lt;/li&gt;
&lt;li&gt;상당수의 단말과 브라우저는 특정 종류의 비디오 포맷만 지원한다. 따라서 호환성을 위해 미리 여러 포맷으로 인코딩해 두는것이 바람직하다.&lt;/li&gt;
&lt;li&gt;사용자에게 끊김 없는 고화질 비디오 재생을 보장하려면, 네트워크 대역폭이 충분하지 않은 사용자에게는 저화질 비디오를 보내는것이 바람직하다.&lt;/li&gt;
&lt;li&gt;모바일 단말의 경우 상황이 수시로 달라질 수 있다. 비디오 화질을 자동으로 변경하거나 수동으로 변경할 수 있도록 하는 것이 바람직하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코딩 포멧은 대부분 두 부분으로 구성되어있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 : 컨테이너 포멧은 비디오파일, 오디오, 메타데이터를 담는 바구니같은것. (.avi, mov, mp4)&lt;/li&gt;
&lt;li&gt;코덱: 비디오 화질은 보존하면서 파일 크기를 줄일 목적으로 고안된 압축 및 압축 해제 알고리즘. (h.264, vp9, hevc 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 원본은 비디오, 오디오, 메타데이터의 세부분으로 나누어 처리된다. 검사는 좋은 품질의 비디오인지, 손상이 없는지 확인하는 절차이고, 비디오 인코딩은 비디오를 다양한 해상도, 코텍, 비트레이트 조합으로 인코딩하는 작업이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[전처리기]
     │
     ├──────────────►
     ▼               │
[DAG 스케줄러]         │
     │               ▼
     ▼        [임시 저장소]
[자원 관리자]           ▲
     │               │
     ▼               │
[작업 실행 서버] ───────┘
     │
     ▼
[인코딩된 비디오]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전처리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 분할 : 비디오 스트림을 GOP(group of pictures)라고 불리는 단위로 쪼갠다.&lt;br /&gt;DAG 생성 : 클라이언트 프로그래머가 작성한 설정 파일에 따라 DAG를 만든다.&lt;br /&gt;데이터 캐시 : 전처리기는 분할된 비디오의 캐시이기도 하다(임시 저장소 활용). 비디오 인코딩이 실패하면 시스템은 보관된 데이터를 활용해 인코딩을 재개한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAG스케줄러&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[원본 비디오]
   ├──► [비디오] ──► [작업]
   │                     ├─ 검사
   │                     ├─ 인코딩
   │                     ├─ 섬네일 추출
   │                     ├─ ...
   │                     └─ 워터마크
   │
   ├──► [오디오] ──► [오디오 인코딩] 
   │
   └──► [메타데이터]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자원 관리자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자원 배분을 효과적으로 수행하는 역할을 담당한다.&lt;br /&gt;작업 스케쥴러를통해 작업 큐, 작업 서버 큐, 실행 큐로부터 메시지를 받아 작업을 실행한다. 작업 서버 큐는 작업 서버의 가용 상태 정보가 보관되어있는 우선순위 큐다. 실행 큐는 현재 실행중인 작업 및 작업 서버 정보가 보관되어있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 관리자는 작업 큐에서 가장 높은 우선순위의 작업을 꺼낸다.&lt;/li&gt;
&lt;li&gt;작업 관리자는 해당 작업을 실행하기 적합한 작업 서버를 고른다.&lt;/li&gt;
&lt;li&gt;작업 스케줄러는 해당 작업 서버에게 작업 실행을 지시한다.&lt;/li&gt;
&lt;li&gt;작업 스케줄러는 해당 작업이 어떤 서버에게 할당되었는지에 관한 정보를 실행 큐에 넣는다.&lt;/li&gt;
&lt;li&gt;작업 스케줄러는 작업이 완료되면 해당 작업을 실행 큐에서 제거한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작업 서버&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[원본 비디오]
   ├──► [비디오] ──► [작업]
   │                     ├─ 검사
   │                     ├─ 인코딩
   │                     ├─ 섬네일 추출
   │                     ├─ ...
   │                     └─ 워터마크
   │                              │
   │                              ▼
   │                          [병합]
   │                             ▲
   ├──► [오디오] ──► [오디오 인코딩] ─┘
   │
   └──► [메타데이터]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 완료되면 병합을 진행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임시 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 저장소에 보관한 데이터는 비디오 프로세싱이 완료되면 삭제한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 전부를 한번에 업로드로 올리는 것은 비효율적이다. 하나의 비디오는 작은 GOP들로 분할할 수 있다. 분할한 GOP병렬적으로 업로드하면 일부가 실패해도 빠르게 업로드를 재개할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속도 최적화는 업로드 센터를 근거리에 지정하여 개선할 수 있다. 이를 위해 CDN을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낮은 응답지연을 달성하는 일은 어려운 일이다. 이를 위해 느슨한 결합을 만들고 병렬성을 높이는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정성으 최적화 하기위해 미리 사인된 업로드 URL을 사용할 수 있다. 허가받은 사용자만이 올바른 저장소에 비디오를 업로드할 수 있도록 presigned URL을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 많은 콘텐츠 제작자가 비디오를 인터넷에 업로드하기를 꺼려하는 이유는 원본을 도난당할까봐서 이다. 이를 위해 DRM, AES암호화, 워터마크를 도입하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오류 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드 오류: 몇 회 재시도한다.&lt;/li&gt;
&lt;li&gt;비디오 분할 오류: 낡은 버전의 클라이언트가 GOP 경계에 따라 비디오를 분할하지 못하는 경우라면 전체 비디오를 서버로 전송하고 서버가 해당 비 디오 분할을 처리하도록 한다.&lt;/li&gt;
&lt;li&gt;트랜스코딩 오류: 재시도한다.&lt;/li&gt;
&lt;li&gt;전처리 오류: DAG 그래프를 재생성한다.&lt;/li&gt;
&lt;li&gt;DAG 스케줄러 오류: 작업을 다시 스케줄링한다.&lt;/li&gt;
&lt;li&gt;자원 관리자 큐에 장애 발생: 사본(replica)을 이용한다.&lt;/li&gt;
&lt;li&gt;작업 서버 장애: 다른 서버에서 해당 작업을 재시도한다.&lt;/li&gt;
&lt;li&gt;API 서버 장애: API 서버는 무상태 서버이므로 신규 요청은 다른 API 서버로 우회될 것이다.&lt;/li&gt;
&lt;li&gt;메타데이터 캐시 서버 장애: 데이터는 다중화되어 있으므로 다른 노드에서 데이터를 여전히 가져올 수 있을 것이다. 장애가 난 캐시 서버는 새로운 것 으로 교체한다.&lt;/li&gt;
&lt;li&gt;메타데이터 데이터베이스 서버 장애:&lt;/li&gt;
&lt;li&gt;주 서버가 죽었다면 부 서버 가운데 하나를 주 서버로 교체한다.&lt;/li&gt;
&lt;li&gt;부 서버가 죽었다면 다른 부 서버를 통해 읽기 연산을 처리하고 죽은 서 버는 새것으로 교체한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web</category>
      <author>마술사의 수습생</author>
      <guid isPermaLink="true">https://mildwpaper.tistory.com/60</guid>
      <comments>https://mildwpaper.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 4 Jun 2025 12:39:37 +0900</pubDate>
    </item>
  </channel>
</rss>