<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>computer club</title>
    <link>https://coco-log.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 4 Aug 2026 14:06:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>voider</managingEditor>
    <image>
      <title>computer club</title>
      <url>https://tistory1.daumcdn.net/tistory/4088857/attach/317b8912aaf940b0be974fa96108ee56</url>
      <link>https://coco-log.tistory.com</link>
    </image>
    <item>
      <title>k-NN과 ANN</title>
      <link>https://coco-log.tistory.com/214</link>
      <description>&lt;p&gt;추천 시스템, 콘텐츠 기반 정보 검색, 시맨틱 분석 등 다양한 분야에서 &amp;#39;유사성(similarity)&amp;#39;에 기반한 데이터 탐색이 중요해지고 있다. 유사성 기반 탐색의 기본적인 접근 방식으로 k-최근접 이웃(k-Nearest Neighbors, k-NN)과 근사 이웃 탐색(Approximate Nearest Neighbor, ANN)이 있다. 본 글에서는 k-NN의 원리 및 한계를 이해하고, 이의 대안으로 ANN의 개념과 기술적 특징을 설명한다.&lt;/p&gt;
&lt;h2&gt;1. k-Nearest Neighbor(k-NN)&lt;/h2&gt;
&lt;h3&gt;1-1 k-NN의 정의 및 원리&lt;/h3&gt;
&lt;p&gt;k-NN은 데이터 포인트 간 거리를 기반으로 동작하는 가장 기본적인 알고리즘 중 하나다. 지도 학습(supervised learning)에서 특정 데이터 포인트와 거리가 가장 가까운 &amp;#39;k&amp;#39;개 이웃 데이터, 즉 최근접 이웃(nearest neighbor)을 식별하고, 이들의 레이블 데이터를 이용하여 해당 포인트의 클래스를 분류하거나 값을 예측한다.&lt;/p&gt;
&lt;p&gt;k-NN의 핵심 연산인 &lt;strong&gt;가장 가까운 k개 이웃 찾기&lt;/strong&gt;는 분류나 예측 뿐아니라 그 자체로 유사성 검색 문제에 직접 적용된다. 이것은 전체 데이터셋 내에서 가장 유사한, 즉 가장 거리가 가까운 상위 k개 데이터를 탐색하는 작업을 의미한다. 두 데이터 포인트 간 거리가 가까울수록 높은 유사도를 갖는다고 간주한다.&lt;/p&gt;
&lt;h3&gt;1-2 유사성 측정: 거리 계산&lt;/h3&gt;
&lt;p&gt;k-NN은 데이터 포인트 간 거리 또는 유사도를 정량적으로 측정하여 이웃 관계를 정의한다. 데이터 특성에 따라 적합한 측정 지표를 선택할 수 있다. 예를 들어 색상 정보는 RGB(Red, Green, Blue) 값으로 표현 가능하며, 이는 3차원 벡터 $(R, G, B)$로 간주하고 거리를 계산해서 색상의 유사도를 판단할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[예시] 유클리드 거리를 이용한 색상 벡터 거리 계산&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Red: $(255,0,0)$&lt;/li&gt;
&lt;li&gt;Orange: $(255,127,0)$&lt;/li&gt;
&lt;li&gt;Violet: $(128,0,128)$&lt;br&gt;유클리드 거리는 $d$차원 공간상 두 점 $q, p$에 대해 다음과 같이 계산한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$&lt;br&gt;\text{Distance}(p, q) = \sqrt{\sum_{i=1}^{d} (p_i - q_i)^2}&lt;br&gt;$$&lt;br&gt;&lt;strong&gt;Distance(Red, Orange)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\sqrt{(255-255)^2 + (0-127)^2 + (0-0)^2} = 127&lt;br&gt;$$&lt;br&gt;&lt;strong&gt;Distance(Red, Violet)&lt;/strong&gt;&lt;br&gt;$$&lt;br&gt;\sqrt{(255-128)^2 + (0-0)^2 + (0-128)^2} \approx 180.3&lt;br&gt;$$&lt;br&gt;Red와 Orange간 거리는 127, Red와 Violet간 거리는 약 180.3이다. 거리가 짧을수록 높은 유사도를 의미하므로 이 예시에서는 Orange가 Violet보다 Red에 더 유사한 색이라고 판단한다.&lt;/p&gt;
&lt;h3&gt;1-3 주요 거리/유사도 계산법&lt;/h3&gt;
&lt;p&gt;k-NN 탐색 시 데이터 특성에 맞게 다음과 같은 다양한 거리 또는 유사도 계산을 사용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Euclidean Distance: $L_2$ &lt;/li&gt;
&lt;li&gt;Manhattan Distance: $L_1$&lt;/li&gt;
&lt;li&gt;Cosine Similarity: 벡터의 각도를 이용한 유사도 (크기보다 방향이 중요할 때)&lt;/li&gt;
&lt;li&gt;Hamming Distance&lt;/li&gt;
&lt;li&gt;Jaccard Similarity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1-4 k-NN의 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;원리가 단순하고 직관적이어서 이해 및 구현 용이하다.&lt;/li&gt;
&lt;li&gt;별도 모델 학습 과정이 필요없는 비모수적(non-parametric) 방법이다. (Lazy Learning)&lt;/li&gt;
&lt;li&gt;데이터셋의 크기가 작을 경우 효과적으로 작동할 수 있다.&lt;/li&gt;
&lt;li&gt;다양한 거리/유사도 척도 문제를 특성에 맞게 선택하여 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1-5 k-NN의 한계&lt;/h3&gt;
&lt;p&gt;단순함에도 불구하고 k-NN의 데이터 규모와 복잡성이 증가함에 따라 다음과 같은 명확한 한계를 갖는다.&lt;/p&gt;
&lt;h4&gt;1. 계산 복잡도 및 검색 속도&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;새로운 쿼리에 대해 최근접 이웃을 찾으려면 저장된 모든 데이터 포인트와 거리를 계산해야 한다.(무차별 대입 방식, brute force search)&lt;/li&gt;
&lt;li&gt;데이터셋 크기가 $n$이고 차원이 $d$일 때, 단일 쿼리에 대한 검색 시간 복잡도는 $O(nd)$이다.&lt;/li&gt;
&lt;li&gt;$n$ 또는 $d$가 증가함에 따라 검색 시간이 선형적으로 증가하므로 대규모 또는 고차원 데이터셋에서는 검색 효율성이 현저히 저하된다.&lt;h4&gt;2. 차원의 저주(Curse of Dimensionality)&lt;/h4&gt;
&lt;/li&gt;
&lt;li&gt;데이터 차원 $d$가 높아지면(고차원 벡터 공간), 데이터 포인트들이 서로 멀리 떨어져 희소하게 분포하는 경향이 나타난다.&lt;/li&gt;
&lt;li&gt;이로 인해 최근접 이웃과 그렇지 않은 이웃 간 거리 차이가 상대적으로 줄어들어 거리 측정 변별력이 약화되고 성능이 저하될 수 있다.&lt;h4&gt;3. 데이터 스케일 의존성&lt;/h4&gt;
&lt;/li&gt;
&lt;li&gt;k-NN 거리 계산은 각 피처(차원)가 사용하는 값의 범위에 따라 결과가 달라진다. 값의 범위가 큰 피처는 작은 피처에 비해 수치적인 차이가 더 크게 나타날 수 있다. 예를 들어 유클리드 거리 계산 시 각 피처 값이 차이를 제곱하여 더하는데, 스케일이 큰 피처에서 발생한 값의 차이가 전체 거리 값에 더 지배적인 영향을 미치게 된다. 결과적으로 스케일이 큰 특정 피처의 중요도가 다른 피처들에 비해 과대평가될 수 있다.&lt;/li&gt;
&lt;li&gt;따라서 k-NN 적용 전 데이터 정규화 또는 표준화와 같은 스케일링 전처리 과정이 요구된다.&lt;h4&gt;4. 이상치 및 k 값 민감성&lt;/h4&gt;
&lt;/li&gt;
&lt;li&gt;이웃 후보군에 이상치가 포함되어 있을 경우 결과 정확성에 부정적인 영향을 미칠 수 있다.&lt;/li&gt;
&lt;li&gt;k값의 선택은 이상치 영향과 지역적 패턴 보존 사이의 트레이드오프를 갖는다. k가 크면 이상치에 덜 민감해지지만 지역 구조 정보가 희석될 수 있고, k가 작으면 이상치 및 노이즈에 민감해진다.&lt;/li&gt;
&lt;li&gt;여기서 지역 패턴이란 전체 데이터 중 가까운 이웃끼리 만들어내는 특성을 말한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 근사 이웃 탐색(Approximate Nearest Neighbor, ANN)&lt;/h2&gt;
&lt;h3&gt;2-1. ANN의 필요성 및 정의&lt;/h3&gt;
&lt;p&gt;k-NN의 주된 한계는 대규모, 고차원 데이터에서 $O(nd)$ 검색 시간 복잡도다. 실시간 환경에서는 이런 계산 비용이 실용성(?)을 저해하는 주요 요인이다.&lt;br&gt;ANN은 k-NN의 확장성 및 속도 문제를 해결하기 위해 제안된 방법을 통칭한다. ANN은 정확성을 일정 수준 허용하는 대신 검색 속도를 향상시키는 것을 목표로 한다. 즉, 100% 정확한 최근접 이웃을 보장하는 대신 실제 최근접 이웃과 매우 유사할 확률이 높은 근사 이웃을 빠른 시간에 찾는 데 집중한다.&lt;/p&gt;
&lt;h3&gt;2-2. ANN의 의미&lt;/h3&gt;
&lt;p&gt;ANN은 다음과 같은 이점을 제공한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;속도 향상: $O(nd)$ 복잡도를 극복하고 다수 ANN 알고리즘은 $O(log n)$ 수준의 준선형 검색 시간을 달성하여 검색 속도를 개선한다.&lt;/li&gt;
&lt;li&gt;확장성: 대규모 데이터셋 및 고차원 벡터 환경에서도 효율적인 유사성 검색이 가능하다.&lt;/li&gt;
&lt;li&gt;실용성: 대규모 벡터 데이터 기반의 시맨틱 검색, 추천 시스템 등을 현실적으로 구현 가능하게 하는 핵심 기반 기술이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2-3. ANN의 작동 원리: 인덱싱&lt;/h3&gt;
&lt;p&gt;ANN 알고리즘 핵심은 인덱싱 구조를 사전에 구축하여 검색 시 전체 데이터와 비교 연산을 최소화하는 데 있다. 인덱스는 고차원 벡터 공간을 효과적으로 탐색하기 위해 설계된 자료 구조다. 주요 인덱싱 전략은 다음과 같이 분류할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;공간 분할 기반:&lt;/strong&gt; 트리를 이용하거나(k-d 트리, Annoy 등) 해싱 기법(Locality-Sensitive Hashing, LSH)을 이용하여 전체 벡터 공간을 여러 하위 영역으로 분할하고, 쿼리와 관련성이 높은 영역 중심으로 탐색한다.&lt;ul&gt;
&lt;li&gt;비교적 직관적이며 트리 기반 접근법으로 관리가 쉽지만 고차원에서 성능 저하 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그래프 기반:&lt;/strong&gt; 데이터 포인트를 노드(node)로, 인접 이웃 관계를 엣지(edge)로 표현하는 근접성 그래프(proximity graph)를 구축한다(HNSW, NSG 등). 검색은 그래프 탐색(graph traversal)을 통해 효율적으로 수행된다. 최근 우수한 성능으로 널리 활용된다.&lt;ul&gt;
&lt;li&gt;높은 정확도와 효율성 제공하지만 인덱스 구축 시간이 비교적 길고 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;압축/양자화 기반:&lt;/strong&gt; 벡터 데이터를 저차원 표현으로 압축하거나 이산적인 코드로 변환(Product Quantization, PQ 등)하여 메모리 사용량을 줄이고 거리 계산 속도를 가속화한다. 이 과정에서 일부 정보 손실이 발생할 수 있다.&lt;ul&gt;
&lt;li&gt;저장공간 및 계산 속도 면에서 유리하나 정보 손실로 인한 정확도 저하 위험이 있다.&lt;h3&gt;2-4. ANN의 주요 트레이드오프&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ANN 기법을 활용할 때는 다음과 같은 요소들 간의 트레이드 오프를 고려해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정확도(리콜) vs. 속도:&lt;/strong&gt; 일반적으로 더 높은 검색 정확도(리콜: 실제 k개의 최근접 이웃 중 찾은 비율)를 요구할수록 검색 속도가 저하되는 경향이 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인덱스 구축 시간 vs. 검색 시간:&lt;/strong&gt; 고성능 검색을 위한 복잡한 인덱스는 구축하는 데 더 많은 시간을 소요할 수 있다. 데이터 갱신 빈도와 검색 속도 요구사항을 고려하여 균형점을 찾아야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모리 사용량:&lt;/strong&gt; 인덱스 구조는 원본 데이터 외 추가적인 메모리 공간을 필요로 한다. 인덱스의 종류와 파라미터 설정에 따라 메모리 요구량이 달라진다.&lt;h3&gt;2-5. ANN 응용&lt;/h3&gt;
ANN 기술은 다양한 데이터 집약적 응용 분야에서 활용된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검색:&lt;/strong&gt; 시맨틱 검색, 문서 유사도 분석 등.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;추천 시스템:&lt;/strong&gt; 사용자 또는 아이템 간 유사성 기반 추천.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;컴퓨터 비전:&lt;/strong&gt; 이미지 검색(CBIR), 객체 인식, 특징점 매칭.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자연어 처리:&lt;/strong&gt; 단어/문장 임베딩 유사성 분석, 기계 번역.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 마이닝:&lt;/strong&gt; 중복 데이터 탐지, 이상 탐지, 클러스터링 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 결론&lt;/h2&gt;
&lt;p&gt;k-NN은 유사성 검색의 기본 원리를 제공하는 직관적인 알고리즘이나, 대규모 및 고차원 데이터 환경에서는 $O(nd)$ 시간 복잡도로 인해 실용성에 한계가 있다. ANN은 이러한 한계를 극복하기 위해 정확성을 일부 절충하여 속도와 확장성을 극대화하는 접근법이다. 효율적인 인덱싱 기법을 통해 준선형 검색 시간 복잡도를 달성함으로써, ANN은 현대의 다양한 데이터 중심 응용 시스템에서 필수적인 기술 요소로 자리 잡았다. 따라서 응용 문제의 특성(데이터 규모, 차원, 요구 정확도 및 속도)을 고려하여 k-NN 또는 적합한 ANN 기법을 선택하고 관련 트레이드오프를 최적화하는 것이 중요하다.&lt;/p&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Amazon Web Service Korea Youtbue. &lt;a href=&quot;https://youtu.be/quwnA6P48Jo?feature=shared&quot;&gt;&amp;quot;Amazon OpenSearch Service KNN 기능을 사용한 유사 이미지 검색 구현하기&amp;quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Elastic Blog. &lt;a href=&quot;https://www.elastic.co/blog/understanding-ann&quot;&gt;&amp;quot;Understanding the approximate nearest neighbor (ANN) algorithm&amp;quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/214</guid>
      <comments>https://coco-log.tistory.com/214#entry214comment</comments>
      <pubDate>Thu, 8 May 2025 07:38:22 +0900</pubDate>
    </item>
    <item>
      <title>VARCHAR(1)은 1 바이트가 아니다</title>
      <link>https://coco-log.tistory.com/213</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; VARCHAR는 &amp;ldquo;Variable Character&amp;rdquo;의 약자로, 가변적인 길이의 텍스트(Character)를 저장할 수 있는 데이터 타입이다.&lt;br /&gt;&amp;bull; VARCHAR(10)은 0~10자의 문자열을 저장할 수 있으며, VARCHAR는 저장된 문자열 길이에 따라 공간을 효율적으로 사용한다.&lt;br /&gt;&amp;bull; CHAR(n)은 항상 n 바이트를 사용하지만, VARCHAR(n)은 실제 텍스트 길이 + 추가 길이 저장용 1~2 바이트만 사용한다.&lt;br /&gt;&amp;bull; 예를 들어, &quot;hi&quot;를 VARCHAR(10)에 저장하면 3바이트가 사용된다. (&quot;hi&quot; = 2바이트 + 길이 저장 1바이트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. VARCHAR의 추가 바이트(Extra Bytes)&lt;br /&gt;&amp;bull; VARCHAR는 길이를 저장하기 위해 추가 바이트를 사용한다.&lt;br /&gt;&amp;bull; n &amp;le; 255 &amp;rarr; 1바이트 오버헤드&lt;br /&gt;&amp;bull; n &amp;gt; 255 &amp;rarr; 2바이트 오버헤드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. CHAR vs VARCHAR 비교&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;데이터 타입&lt;/th&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;CHAR(n)&lt;/td&gt;
&lt;td&gt;고정 길이&lt;/td&gt;
&lt;td&gt;항상 n 바이트 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR(n)&lt;/td&gt;
&lt;td&gt;가변 길이&lt;/td&gt;
&lt;td&gt;실제 텍스트 길이 + 1~2바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. VARCHAR(1)은 1바이트가 아니다&lt;br /&gt;&amp;bull; VARCHAR(1)의 크기는 사용하는 문자셋(Character Set)에 따라 달라진다.&lt;br /&gt;&amp;bull; 예를 들어, utf8mb4를 사용하면 저장하는 문자에 따라 1~4바이트가 필요하다.&lt;br /&gt;&amp;bull; &quot;A&quot; (ASCII 문자) &amp;rarr; 1바이트&lt;br /&gt;&amp;bull; &quot;한&quot; (한글) &amp;rarr; 3바이트&lt;br /&gt;&amp;bull; &quot; &quot; (이모지) &amp;rarr; 4바이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. VARCHAR(n)과 문자셋에 따른 최대 저장 크기&lt;br /&gt;&amp;bull; utf8mb4는 최대 4바이트를 사용하므로, VARCHAR(n)이 실제로 차지하는 공간은:&lt;br /&gt;&amp;bull; VARCHAR(100) &amp;rarr; (100 &amp;times; 4) + 1바이트 (최대 401바이트)&lt;br /&gt;&amp;bull; VARCHAR(300) &amp;rarr; (300 &amp;times; 4) + 2바이트 (최대 1,202바이트)&lt;/p&gt;</description>
      <category>메모</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/213</guid>
      <comments>https://coco-log.tistory.com/213#entry213comment</comments>
      <pubDate>Tue, 25 Feb 2025 19:30:24 +0900</pubDate>
    </item>
    <item>
      <title>카프카 기본 개념</title>
      <link>https://coco-log.tistory.com/212</link>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kafka&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apache Kafka 공식 페이지에서 &amp;quot;높은 성능의 데이터 파이프라인, 분석 스트리밍, 테이터 통합을 위해 사용하는 분산 이벤트 스트리밍 플랫폼이&amp;quot;라고 소개한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;분산 이벤트 스트리밍 플랫폼&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;분산(Distributed)&lt;ul&gt;
&lt;li&gt;여러 서버에 걸쳐 분산된 아키텍처로 구성된 시스템을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이벤트(Event)&lt;ul&gt;
&lt;li&gt;애플리케이션에서 발생하는 어떤 사건이나 상태 변화를 의미한다. 게시물 조회, 등록, 수정 같은 것들이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스트리밍(Streaming)&lt;ul&gt;
&lt;li&gt;데이터가 지속적으로 생성되고, 이를 실시간으로 처리하고 소비할 수 있도록 설계된 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이벤트 스트리밍&lt;ul&gt;
&lt;li&gt;이벤트 스트리밍은 이벤트(또는 데이터)를 지속적으로 처리, 저장, 전달하는 기술이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kafka를 구성하는 요소(들)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kafka Cluster&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;카프카 클러스터는 주키퍼, 브로커, 토픽, 파티션, 리플리케이션으로 구성된 논리적 개념이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;         . +----------------+
           | Kafka Cluster  |
    .      +----------------+
        .     /     |     \
             /      |      \
            /       |       \
+-----------+ +-----------+ +-----------+
|  Broker 1 | |  Broker 2 | |  Broker 3 |
+-----------+ +-----------+ +-----------+&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zookeeper&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메타데이터 관리&lt;ul&gt;
&lt;li&gt;카프카 클러스터의 메타데이터(브로커, 토픽, 파티션 정보)를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리더 선출&lt;ul&gt;
&lt;li&gt;각 파티션의 리더(leader) 브로커를 선출한다. 리더 브로커는 파티션의 모든 읽기/쓰기 요청을 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;브로커 상태 관리&lt;ul&gt;
&lt;li&gt;브로커 상태를 모니터링 하고 브로커가 추가되거나 제거될 때 다른 브로커들에게 전파한다. 이것을 통해 &lt;strong&gt;클러스터의 동기화&lt;/strong&gt;를 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카프카 최신 버전에서 주키퍼 의존성을 제거했음&lt;ul&gt;
&lt;li&gt;주키퍼를 유지 보수하는 것은 추가 비용이 든다. (서버, 운영) 카프카가 자체적으로 메타데이터를 관리할 수 있는 기능을 추가해서 이 비용을 없애려는 목적.&lt;/li&gt;
&lt;li&gt;카프카가 자체적으로 메타데이터를 관리하면 더 빨리 메타데이터를 조회하고 업데이트 할 수 있으므로 성능을 높일 수 있다.&lt;/li&gt;
&lt;li&gt;주키퍼는 클러스터 내 단일 장애 지점(Single Point of Failure)이다. 주키퍼 의존성을 제거해서 SPOF를 없앨 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;KRaft(Kafka Raft)&lt;ul&gt;
&lt;li&gt;카프카는 주키퍼를 대체하기 위해 KRaft를 도입.&lt;/li&gt;
&lt;li&gt;KRaft는 카프카 브로커들이 직접 메타데이터를 관리하고 &lt;a href=&quot;https://seongjin.me/raft-consensus-algorithm/&quot;&gt;Raft consensus 알고리즘&lt;/a&gt;을 사용하여 메타데이터의 일관성을 유지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Broker&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;브로커는 메시지를 저장하고 관리하며 프로듀서와 컨슈머의 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;여러 브로커가 모여 카프카 클러스터를 구성한다.&lt;/li&gt;
&lt;li&gt;데이터를 분산 저장하고 복제하여 높은 가용성과 내구성 보장한다.&lt;ul&gt;
&lt;li&gt;높은 가용성(HA, High availability)&lt;ul&gt;
&lt;li&gt;시스템이 최대한 오랫동안 중단없이 지속적으로 운영될 수 있는 능력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내구성(Durability)&lt;ul&gt;
&lt;li&gt;데이터가 시스템에 한번 기록되면 시스템 장애나 오류가 발생해도 데이터가 손실되지 않고 안전하게 보관된다는 것을 보장하는 것을 의미.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;주키퍼와 상호 작용. KRaft 방식을 사용한다면 브로커가 직접 메타데이터를 관리.&lt;/li&gt;
&lt;li&gt;클라이언트(컨슈머, 프로듀서) 요청 처리&lt;/li&gt;
&lt;li&gt;Replication 및 leader - follower 기능&lt;/li&gt;
&lt;li&gt;데이터 압축 및 삭제(로그 세그먼트 압축, 삭제)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Producer&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터를 Kafka 클러스터의 특정 토픽으로 전송하는 클라이언트 애플리케이션. 데이터가 안정적으로 전송되었다는 것을 보장하기 위해 ACK를 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consumer&lt;/p&gt;
&lt;ul&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;li&gt;오프셋은 &lt;code&gt;__consumer_offsets&lt;/code&gt;라는 내부 토픽에 컨슈머 그룹 단위로 기록됨.&lt;/li&gt;
&lt;li&gt;컨슈머는 메시지를 읽고 나서 어디까지 읽었는지 오프셋을 커밋한다.&lt;/li&gt;
&lt;li&gt;브로커는 내부 토픽에 오프셋을 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consumer Group&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컨슈머 그룹은 컨슈머 인스턴스가 협력하여 하나의 토픽을 병렬로 소비할 수 있도록 하는 개념이다. 동일한 그룹 ID를 공유하는 컨슈머들로 구성된다.&lt;/li&gt;
&lt;li&gt;컨슈머 그룹 내 컨슈머는 할당된 파티션에서 데이터를 소비한다. 파티션 할당은 브로커가 한다.&lt;/li&gt;
&lt;li&gt;파티션 수보다 컨슈머 수가 많은 것은 의미없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;       +-------------------+
       |   Consumer Group  |
       |     (my-group)    |
       +-------------------+
          /         |         \
         /          |          \
        /           |           \
+------------+ +------------+ +------------+
| Consumer 1 | | Consumer 2 | | Consumer 3 |
+------------+ +------------+ +------------+
     |               |               |
+---------+ +---------+ +---------+ +---------+ +---------+ 
| Part 0  | | Part 1  | | Part 2  | | Part 3  | | Part 4  |
+---------+ +---------+ +---------+ +---------+ +---------+ 
    |            |            |          |           |
  Kafka Cluster with Topic (my-topic) and 6 Partitions&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Topic&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;토픽은 메시지를 구분하는 논리적 단위다. 토픽은 카프카 클러스터에서 데이터가 저장되고 프로듀서, 컨슈머가 데이터를 주고받는 기본 단위다. 토픽은 여러 파티션으로 나뉘어져 있고, 각 파티션은 여러 브로커에 분산 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Partition&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;토픽은 여러 파티션으로 나뉘어진다. 파티션은 메시지를 순차 저장하고, 저장된 메시지는 고유한 오프셋을 가진다.&lt;/li&gt;
&lt;li&gt;파티션 내에서는 메시지의 순서를 보장하지만 여러 파티션에 걸쳐 메시지가 분산 저장되기 때문에 전체 메시지 순서는 보장할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replication&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파티션 데이터의 내구성(durability)을 보장하기 위해 파티션은 여러 브로커에 복제된다. 각 파티션은 하나의 리더와 여러 팔로워 복제본을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Topic: my-topic
+----------------+----------------+----------------+
| Partition 0    | Partition 1    | Partition 2    |
|                |                |                |
| Msg 0, Msg 1,  | Msg 0, Msg 1,  | Msg 0, Msg 1,  |
| Msg 2, Msg 3   | Msg 2, Msg 3   | Msg 2, Msg 3   |
+----------------+----------------+----------------+&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study/kafka</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/212</guid>
      <comments>https://coco-log.tistory.com/212#entry212comment</comments>
      <pubDate>Sun, 23 Jun 2024 23:51:48 +0900</pubDate>
    </item>
    <item>
      <title>elasticsearch join type field</title>
      <link>https://coco-log.tistory.com/211</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #343541;&quot;&gt;엘라스틱서치 조인 가이드 문서 링크&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #343541;&quot;&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html#parent-join&quot;&gt;https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html#parent-join&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #343541;&quot;&gt;엘라스틱서치는 Parent-join이라는 것을 제공함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #343541;&quot;&gt;The join data type is a special field that creates parent/child relation within documents of the same index. The relations section defines a set of possible relations within the documents, each relation being a parent name and a child name. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`join` 타입은 동일한 인덱스 내에서 parent-child 관계를 만드는 특별한 필드다. `relations` 섹션은 문서 내에서 가능한 관계 집합을 정의한다. 각 릴레이션의 관계는 parent 이름과 child 이름으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; Elasticsearch에서 Join은 같은 문서 내에서만 가능함. 7.x 버전부터 하나의 인덱스에 다양한 문서를 저장할 수 없으므로, 같은 문서 내에서 부모-자식 관계를 만들어서 조인을 할 수 있다는 것으로 보임.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;We don&amp;rsquo;t recommend using multiple levels of relations to replicate a relational model. Each level of relation adds an overhead at query time in terms of memory and computation. For better search performance, denormalize your data instead.&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;b&gt;&amp;rarr; 그나마도 권장하지는 않음. 반드시 필요한 경우가 아니라면 비정규화 하라고 말하고 있음.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ftfa5/btsp8boKKxv/GeqYpjoHkHUY0PW3T7qiD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ftfa5/btsp8boKKxv/GeqYpjoHkHUY0PW3T7qiD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ftfa5/btsp8boKKxv/GeqYpjoHkHUY0PW3T7qiD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFtfa5%2Fbtsp8boKKxv%2FGeqYpjoHkHUY0PW3T7qiD0%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;856&quot; height=&quot;632&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인을 사용하려면 문서를 인덱싱할 때 릴레이션의 이름과 도큐먼트의 optional parent `source`에 제공해야 함. 예를 들어, 두 개의 `question` 도큐먼트를 만드려면 위와 같은 쿼리를 해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 같은 문서 내에서 조인을 하려면 색인할 때 릴레이션의 이름을 지정해야 하는 것 같음. `my_join_field`에 이것이 어떤 역할인지(parent or child), 또한 (선택적으로) 부모가 무엇인지도 `source`에 전달해야 하는 것 같음?&lt;/b&gt;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;Parent-Join and performance&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join 필드는 RDB처럼 쓰면 안 된다. Elasticsearch 성능의 핵심은 데이터를 비정규화 하는 것. 각 조인 필드 `has_child` 또는 `has_parent` 쿼리는 성능에 TAX를 부과함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`join` 필드가 의미있는 경우는 데이터에 한 엔티티가 다른 엔티티보다 훨씬 많은&amp;nbsp;&lt;span style=&quot;color: #212529;&quot;&gt;one-to-many relationship이&lt;/span&gt;&amp;nbsp;포함된 경우.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, products와 offers. 이 경우 offers가 products보다 훨씬 많은 경우에는 product를 상위 문서로, offer를 하위 문서로 모델링하는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; Product와 Offer는 다른 도큐먼트인 것 같은데 어떻게 조인한다는 것인지?&lt;/b&gt;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;Parent-Join restrictions&lt;/b&gt;&lt;/h1&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;parent와 child 도큐먼트는 반드시 같은 샤드 내에 인덱싱 되어야 함. 왜냐하면 하위 문서를 getting, deleting, updating 할 때 같은 `routing` 값을 지정해야 함.&lt;/li&gt;
&lt;li&gt;엘리먼트는 여러 자식을 가질 수 있지만 부모는 오직 하나여야 함.&lt;/li&gt;
&lt;li&gt;기존 `join` 필드에 새 릴레이션을 추가하는 게 가능함&lt;/li&gt;
&lt;li&gt;또한 기존 요소에 자식을 추가하는 것도 가능함. 하지만 해당 요소가 이미 부모인 경우에만 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Post Document와 Comment Document를 Parent-Child 관계를 맺어서 조인하여 사용할 수 있는지 검토하며 공부한 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Elasticsearch는 Join type이라는 특별한 타입을 정의한다. 이 필드를 이용해서 parent-child라는 relations를 지정해서 조인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;has_child, has_parent 쿼리
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;has_child: 지정한 child document를 포함하는 parent document 조회&lt;/li&gt;
&lt;li&gt;has_parent: 지정한 parent를 부모로 가지는 child document 조회&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주의할 점은 엘라스틱서치는 서로 다른 인덱스 간 조인을 지원하지 않는다. 주로 계층형 데이터이면서 1:N 관계일 때 사용되며, RDB의 self-join과 비슷하다. 웬만하면 비정규화를 사용하도록 권장하고 있다. 문서에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;has_child,&lt;span&gt;&amp;nbsp;&lt;/span&gt;has_parent&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리는 성능에 tax를 부과하는 것이며, 정말 필요한 경우가 아니라면 비정규화 하라고 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;parent - child relations를 만들 때 몇 가지 제약사항도 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 당 하나의 조인 필드 매핑만 허용함&lt;/li&gt;
&lt;li&gt;parent와 child 도큐먼트는 반드시 같은 샤드 내에 인덱싱 되어야 함. 왜냐하면 하위 문서를 getting, deleting, updating 할 때 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;routing&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 지정해야 함.&lt;/li&gt;
&lt;li&gt;엘리먼트는 여러 자식을 가질 수 있지만 부모는 오직 하나여야 함.&lt;/li&gt;
&lt;li&gt;기존&lt;span&gt;&amp;nbsp;&lt;/span&gt;join&lt;span&gt;&amp;nbsp;&lt;/span&gt;필드에 새 릴레이션을 추가하는 게 가능함&lt;/li&gt;
&lt;li&gt;또한 기존 요소에 자식을 추가하는 것도 가능함. 하지만 해당 요소가 이미 부모인 경우에만 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용을 검토하며 테스트 해본 내용&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;code&gt;# Parent Join(has-child) Example

# create index
PUT /companies
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;employees&quot;: {
        &quot;type&quot;: &quot;join&quot;,
        &quot;relations&quot;: {
          &quot;company&quot;: &quot;employee&quot;
        }
      }
    }
  }
}

# index parent document
POST /companies/_doc/1
{
  &quot;name&quot;: &quot;Acme Corp&quot;,
  &quot;employees&quot;: &quot;company&quot;
}

POST /companies/_doc/2
{
  &quot;name&quot;: &quot;XYZ Corp&quot;,
  &quot;employees&quot;: &quot;company&quot;
}

# index child document
POST /companies/_doc/3?routing=1
{
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;employees&quot;: {
    &quot;name&quot;: &quot;employee&quot;,
    &quot;parent&quot;: 1
  }
}

POST /companies/_doc/4?routing=1
{
  &quot;name&quot;: &quot;Jane Smith&quot;,
  &quot;employees&quot;: {
    &quot;name&quot;: &quot;employee&quot;,
    &quot;parent&quot;: 1
  }
}

POST /companies/_doc/5?routing=2
{
  &quot;name&quot;: &quot;Bob Johnson&quot;,
  &quot;employees&quot;: {
    &quot;name&quot;: &quot;employee&quot;,
    &quot;parent&quot;: 2
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 테스트 데이터를 만들어 둔 뒤, 조인 쿼리를 날려봄&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;code&gt;# join query: Acme Corp에 다니는 employees를 조회
GET /companies/_search
{
  &quot;query&quot;: {
    &quot;has_parent&quot;: {
      &quot;parent_type&quot;: &quot;company&quot;,
      &quot;query&quot;: {
        &quot;match&quot;: {
          &quot;name&quot;: &quot;Acme Corp&quot;
        }
      }
    }
  }
}

# 결과
    &quot;hits&quot;: [
      {
        &quot;_index&quot;: &quot;companies&quot;,
        &quot;_id&quot;: &quot;3&quot;,
        &quot;_score&quot;: 1,
        &quot;_routing&quot;: &quot;1&quot;,
        &quot;_source&quot;: {
          &quot;name&quot;: &quot;John Doe&quot;,
          &quot;employees&quot;: {
            &quot;name&quot;: &quot;employee&quot;,
            &quot;parent&quot;: 1
          }
        }
      },
      {
        &quot;_index&quot;: &quot;companies&quot;,
        &quot;_id&quot;: &quot;4&quot;,
        &quot;_score&quot;: 1,
        &quot;_routing&quot;: &quot;1&quot;,
        &quot;_source&quot;: {
          &quot;name&quot;: &quot;Jane Smith&quot;,
          &quot;employees&quot;: {
            &quot;name&quot;: &quot;employee&quot;,
            &quot;parent&quot;: 1
          }
        }
      }
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 인덱스 내에서 성격이 다른 두 가지 문서를 운용하는 것이 부적합하다고 판단했기 때문에 사용하지 않기로 결정함. 한 document를 comment로도 사용하고 post로도 사용할 때 문제 생길 여지 많음.&lt;/p&gt;
&lt;h1 id=&quot;jointype-NestedQuery&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Nested Query&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대안으로 nested 쿼리를 사용할 수도 있다. nested는 post의 프로퍼티로 nested 타입을 지정한 뒤, 그 안에 comments 프로퍼티를 저장하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;code&gt;# parent join(nested) example

# create index
PUT /blog_posts
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;title&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;content&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;comments&quot;: {
        &quot;type&quot;: &quot;nested&quot;
      }
    }
  }
}

# index parent document
POST /blog_posts/_doc/1
{
  &quot;title&quot;: &quot;엘라스틱서치를 소개합니다&quot;,
  &quot;content&quot;: &quot;엘라스틱서치는 파워풀한 분산 검색 엔진입니다&quot;,
  &quot;comments&quot;: []
}

POST /blog_posts/_doc/2
{
  &quot;title&quot;: &quot;키바나 시작하기&quot;,
  &quot;content&quot;: &quot;키바나는 데이터 비주얼라이제이션과 탐색 툴입니다.&quot;,
  &quot;comments&quot;: []
}

# index child document
PUT /blog_posts/_doc/1
{
  &quot;title&quot;: &quot;엘라스틱서치를 소개합니다&quot;,
  &quot;content&quot;: &quot;엘라스틱서치는 파워풀한 분산 검색 엔진입니다&quot;,
  &quot;comments&quot;: [
    {
      &quot;text&quot;: &quot;Great introduction to Elasticsearch!&quot;
    },
    {
      &quot;text&quot;: &quot;Looking forward to learning more about Elasticsearch.&quot;
    }
  ]
}


PUT /blog_posts/_doc/2
{
  &quot;title&quot;: &quot;키바나 시작하기&quot;,
  &quot;content&quot;: &quot;키바나는 데이터 비주얼라이제이션과 탐색 툴입니다.&quot;,
  &quot;comments&quot;: [
    {
      &quot;text&quot;: &quot;Kibana's visualization features are impressive!&quot;
    }
  ]
}

# nested query
GET /blog_posts/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;nested&quot;: {
            &quot;path&quot;: &quot;comments&quot;,
            &quot;query&quot;: {
              &quot;match&quot;: {
                &quot;comments.text&quot;: &quot;Elasticsearch&quot;
              }
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Study/elasticsearch</category>
      <category>elasticsearch join</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/211</guid>
      <comments>https://coco-log.tistory.com/211#entry211comment</comments>
      <pubDate>Sat, 5 Aug 2023 18:23:24 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch: 애널라이저, 토크나이저, 노멀라이저</title>
      <link>https://coco-log.tistory.com/210</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;애널라이저는 9개 이상의 캐릭터 필터, 1개의 토크나이저, 0개 이상의 토큰 필터로 구성된다. 동작 역시 캐릭터 필터 -&amp;gt; 토크나이저 -&amp;gt; 토큰 필터 순서로 수행된다. 애널라이저는 입력한 텍스트에 캐릭터 필터를 적용하여 문자열을 변형시킨 뒤 토크나이저를 적용하여 여러 토큰으로 쪼갠다. 쪼개진 토큰의 스트림에 토큰 필터를 적용해서 토큰에 특정한 변형을 가한 결과가 최종적으로 분석 완료된 텀이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘라스틱서치는 애널라이저의 동작을 테스트할 수 있는 API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;example&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;analyzer&quot;: &quot;standard&quot;,
  &quot;text&quot;: [&quot;Hello, HELLO, World!&quot;]
}

//result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;hello&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 5,
      &quot;type&quot;: &quot;&amp;lt;ALPHANUM&amp;gt;&quot;,
      &quot;position&quot;: 0
    },
    {
      &quot;token&quot;: &quot;hello&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 12,
      &quot;type&quot;: &quot;&amp;lt;ALPHANUM&amp;gt;&quot;,
      &quot;position&quot;: 1
    },
    {
      &quot;token&quot;: &quot;world&quot;,
      &quot;start_offset&quot;: 14,
      &quot;end_offset&quot;: 19,
      &quot;type&quot;: &quot;&amp;lt;ALPHANUM&amp;gt;&quot;,
      &quot;position&quot;: 2
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;character filter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터 필터는 텍스트를 캐릭터 스트림으로 받아서 특정한 문자를 추가, 변경, 삭제한다. 애널라이저에는 0개 이상의 캐릭터 필터를 지정할 수 있다. 여러 캐릭터 필터가 지정됐다면 순서대로 수행된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;tokenizer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토크나이저는 캐릭터 스트림을 받아서 여러 토큰으로 쪼개어 토큰 스트림을 만든다. 애널라이저에는 한 개의 토크나이저만 지정할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;standard tokenizer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트를 단어 단위로 나누는 가장 기본적인 토크나이저다. 대부분의 문장 부호가 사라진다. 필드 매핑엩 ㅡㄱ정 애널라이저를 지정하지 않으면 기본값으로 standard 애널라이저가 적용된다. 스탠다드 애널라이저가 사용하는 토크나이저가 스탠다드 토크나이저다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;keyword tokenizer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어온 텍스트를 쪼개지 않고 그대로 내보낸다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;tokenizer&quot;: &quot;keyword&quot;,
  &quot;text&quot;: [&quot;Hello, HELLO, World!&quot;]
}

//result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;Hello, HELLO, World!&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 20,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 0
    }
  ]
}&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;ngram tokenizer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트를 min_ngram 값 이상 max_ngram 값 이하 단위로 쪼갠다. min_gram: 2, max_gram=3으로 지정한 뒤, hello라는 텍스트를 토크나이징 하면 &quot;he&quot;, &quot;hel&quot;, &quot;el&quot;, &quot;hell&quot;, &quot;ll&quot;, &quot;llo&quot; 총 5개의 토큰이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ngram example&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;tokenizer&quot;: {
    &quot;type&quot;: &quot;ngram&quot;,
    &quot;min_gram&quot;: 3,
    &quot;max_gram&quot;: 4
  },
  &quot;text&quot;: [&quot;Hello, World!&quot;]
}
//result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;Hel&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 3,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 0
    },
    {
      &quot;token&quot;: &quot;Hell&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 4,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 1
    },
    {
      &quot;token&quot;: &quot;ell&quot;,
      &quot;start_offset&quot;: 1,
      &quot;end_offset&quot;: 4,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 2
    },
    {
      &quot;token&quot;: &quot;ello&quot;,
      &quot;start_offset&quot;: 1,
      &quot;end_offset&quot;: 5,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 3
    },
    {
      &quot;token&quot;: &quot;llo&quot;,
      &quot;start_offset&quot;: 2,
      &quot;end_offset&quot;: 5,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 4
    },
    {
      &quot;token&quot;: &quot;llo,&quot;,
      &quot;start_offset&quot;: 2,
      &quot;end_offset&quot;: 6,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 5
    },
    {
      &quot;token&quot;: &quot;lo,&quot;,
      &quot;start_offset&quot;: 3,
      &quot;end_offset&quot;: 6,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 6
    },
    {
      &quot;token&quot;: &quot;lo, &quot;,
      &quot;start_offset&quot;: 3,
      &quot;end_offset&quot;: 7,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 7
    },
    {
      &quot;token&quot;: &quot;o, &quot;,
      &quot;start_offset&quot;: 4,
      &quot;end_offset&quot;: 7,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 8
    },
    {
      &quot;token&quot;: &quot;o, W&quot;,
      &quot;start_offset&quot;: 4,
      &quot;end_offset&quot;: 8,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 9
    },
    {
      &quot;token&quot;: &quot;, W&quot;,
      &quot;start_offset&quot;: 5,
      &quot;end_offset&quot;: 8,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 10
    },
    {
      &quot;token&quot;: &quot;, Wo&quot;,
      &quot;start_offset&quot;: 5,
      &quot;end_offset&quot;: 9,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 11
    },
    {
      &quot;token&quot;: &quot; Wo&quot;,
      &quot;start_offset&quot;: 6,
      &quot;end_offset&quot;: 9,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 12
    },
    {
      &quot;token&quot;: &quot; Wor&quot;,
      &quot;start_offset&quot;: 6,
      &quot;end_offset&quot;: 10,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 13
    },
    {
      &quot;token&quot;: &quot;Wor&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 10,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 14
    },
    {
      &quot;token&quot;: &quot;Worl&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 11,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 15
    },
    {
      &quot;token&quot;: &quot;orl&quot;,
      &quot;start_offset&quot;: 8,
      &quot;end_offset&quot;: 11,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 16
    },
    {
      &quot;token&quot;: &quot;orld&quot;,
      &quot;start_offset&quot;: 8,
      &quot;end_offset&quot;: 12,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 17
    },
    {
      &quot;token&quot;: &quot;rld&quot;,
      &quot;start_offset&quot;: 9,
      &quot;end_offset&quot;: 12,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 18
    },
    {
      &quot;token&quot;: &quot;rld!&quot;,
      &quot;start_offset&quot;: 9,
      &quot;end_offset&quot;: 13,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 19
    },
    {
      &quot;token&quot;: &quot;ld!&quot;,
      &quot;start_offset&quot;: 10,
      &quot;end_offset&quot;: 13,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 20
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min_gram 3, max_gram 4로 지정하고 &quot;Hello, World!&quot;라는 문자열을 토크나이징 하면 총 21개의 토큰이 나온다. 여기에는 사실상 무의미한 공백이나 문장 부호 같은 토큰도 포함됐다. 이런 문제를 피하기 위해 ngram 토크나이저에는 token_chars라는 속성을 통해 토큰에 포함시킬 타입의 문자를 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;token_chars&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;Letter: 언어의 글자로 분류되는 문자&lt;/li&gt;
&lt;li&gt;Digit: 숫자로 분류되는 문자&lt;/li&gt;
&lt;li&gt;whitespace: 띄어쓰기 또는 줄바꿈 문자 등 공백으로 인식되는 문자&lt;/li&gt;
&lt;li&gt;punctutation: 문장 부호&lt;/li&gt;
&lt;li&gt;symbol: 기호&lt;/li&gt;
&lt;li&gt;custom: custom_token_chars 설정을 통해 따로 지정한 커스텀 문자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;tokenizer&quot;: {
    &quot;type&quot;: &quot;ngram&quot;,
    &quot;min_gram&quot;: 3,
    &quot;max_gram&quot;: 4,
    &quot;token_chars&quot;: [&quot;Letter&quot;]
  },
  &quot;text&quot;: [&quot;Hello, World!&quot;]
}

// result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;Hel&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 3,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 0
    },
    {
      &quot;token&quot;: &quot;Hell&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 4,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 1
    },
    {
      &quot;token&quot;: &quot;ell&quot;,
      &quot;start_offset&quot;: 1,
      &quot;end_offset&quot;: 4,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 2
    },
    {
      &quot;token&quot;: &quot;ello&quot;,
      &quot;start_offset&quot;: 1,
      &quot;end_offset&quot;: 5,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 3
    },
    {
      &quot;token&quot;: &quot;llo&quot;,
      &quot;start_offset&quot;: 2,
      &quot;end_offset&quot;: 5,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 4
    },
    {
      &quot;token&quot;: &quot;Wor&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 10,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 5
    },
    {
      &quot;token&quot;: &quot;Worl&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 11,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 6
    },
    {
      &quot;token&quot;: &quot;orl&quot;,
      &quot;start_offset&quot;: 8,
      &quot;end_offset&quot;: 11,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 7
    },
    {
      &quot;token&quot;: &quot;orld&quot;,
      &quot;start_offset&quot;: 8,
      &quot;end_offset&quot;: 12,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 8
    },
    {
      &quot;token&quot;: &quot;rld&quot;,
      &quot;start_offset&quot;: 9,
      &quot;end_offset&quot;: 12,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 9
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;token_chars 설정에 letter를 지정하면 문장 부호와 공백을 제거한 문자열만 토크나이징 한다.&lt;br /&gt;여기서 공백 문자를 전후해 위치한 문자인 oWo 같은 토큰은 포함되지 않는다. token_chars 속성에 letter만을 포함하게 지정했으므로 공백 문자를 무시하고 &quot;oWo&quot; 토큰을 생성해야 할 것 같지만 결과는 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ngram 토크나이저는 먼저 token_chars에 지정되지 않은 문자를 기준으로 텍스트를 쪼갠다. 이렇게 하면 token_chars에 지정되지 않은 문자는 자연스럽게 최종 결과에 포함되지 않는다. 그 다음 단어를 min_gram 이상 max_gram 이하의 문자 길이를 가진 토큰으로 쪼갠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ngram 토크나이저는 엘라스틱서치에서 RDB로 따지자면 LIKE 검색과 유사한 검색을 구현하고 싶을 때, 자동 완성 관련 서비스를 구현하고 싶을 때 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min_gram과 max_gram의 값 차이가 2 이상으로 벌어지면 분석 시도가 실패한다. 이 제한은 index.max_ngram_diff 설정에서 지정할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;edge_ngram tokenizer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;edge_ngram 토크나이저는 ngram 토크나이저와 유사하다. 입력된 텍스트를 token_chars에 지정되지 않은 문자 기준으로 삼아 단어 단위로 쪼갠다. 그 다음 각 단어를 min_gram 이상 max_gram 이하의 문자 길이를 가진 토큰으로 쪼갠다. 하지만 ngram 토크나이저와 다르게 생성된 모든 토큰의 시작 글자를 단어의 시작 글자로 고정시켜서 생성한다. edge_ngram 토크나이저에서 토큰의 시작은 단어의 시작 글자여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;example&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;tokenizer&quot;: {
    &quot;type&quot;: &quot;edge_ngram&quot;,
    &quot;min_gram&quot;: 3,
    &quot;max_gram&quot;: 4,
    &quot;token_chars&quot;: [&quot;letter&quot;]
  },
  &quot;text&quot;: [&quot;Hello, World!&quot;]
}

// result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;Hel&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 3,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 0
    },
    {
      &quot;token&quot;: &quot;Hell&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 4,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 1
    },
    {
      &quot;token&quot;: &quot;Wor&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 10,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 2
    },
    {
      &quot;token&quot;: &quot;Worl&quot;,
      &quot;start_offset&quot;: 7,
      &quot;end_offset&quot;: 11,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 3
    }
  ]
}&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;토큰 필터는 토큰 스트림을 받아서 토큰을 추가, 변경, 삭제한다. 하나의 애널라이저에 토큰 필터를 0개 이상 지정할 수 있다. 토큰 필터가 여러 개 지정된 경우에는 순차적으로 적용된다. 엘라스틱서치는 몇 가지 빌트인 토큰 필터를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lowercase / uppercase&lt;/li&gt;
&lt;li&gt;stop: 불용어 지정하여 제거&lt;/li&gt;
&lt;li&gt;synonym: 유의어 사전을 지정하여 지정된 유의어를 치환한다&lt;/li&gt;
&lt;li&gt;pattern_replace: 정규식을 사용하여 토큰의 내용을 치환한다&lt;/li&gt;
&lt;li&gt;stemmer: 지원되는 몇몇 언어의 어간 추출을 수행. 한국어 지원X&lt;/li&gt;
&lt;li&gt;trim: 토큰 전후에 위치한 공백 제거&lt;/li&gt;
&lt;li&gt;truncate: 지정한 길이로 토큰을 자른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;analyze API에서 토큰 필터도 테스트 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;GET _analyze
{
  &quot;filter&quot;: [&quot;lowercase&quot;],
  &quot;text&quot;: [&quot;Hello, World!&quot;]
}

//result
{
  &quot;tokens&quot;: [
    {
      &quot;token&quot;: &quot;hello, world!&quot;,
      &quot;start_offset&quot;: 0,
      &quot;end_offset&quot;: 13,
      &quot;type&quot;: &quot;word&quot;,
      &quot;position&quot;: 0
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;built in analyzer&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;standard analyzer: standard tokenizer + lowercase token filter로 구성된다. 애널라이저를 지정하지 않으면 적용되는 기본 애널라이저다.&lt;/li&gt;
&lt;li&gt;simple analyzer: letter가 아닌 문자 단위로 토큰을 쪼갠 뒤 lowercase 토큰 필터를 적용한다.&lt;/li&gt;
&lt;li&gt;whitespace: analyzer: whitespace tokenizer로 구성된다. 공백 문자 단위로 토큰을 쪼갠다.&lt;/li&gt;
&lt;li&gt;stop analyzer: standard 애널라이저와 같지만 뒤에 stop 토큰 필터를 적용해서 불용어 제거함&lt;/li&gt;
&lt;li&gt;pattern analyzer: keyword 토크나이저로 구성됨. 분석하지 않고 큰 토큰을 그대로 반환&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;haskell&quot;&gt;&lt;code&gt;PUT analyzer_test
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;analyzer&quot;: {
        &quot;default&quot;: {
          &quot;type&quot;: &quot;keyword&quot; // default analyzer 지정
        }
      }
    }
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;defaultText&quot;: {
        &quot;type&quot;: &quot;text&quot;
      },
      &quot;standardText&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;standard&quot; // default analyzer와 다른 애널라이저 지정
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;노멀라이저&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노멀라이저는 애널라이저와 비슷한 역할을 하나 적용 대상이 text 타입이 아닌 keyword 타입 필드라는 차이가 있다. 또한 애널라이저와 다르게 단일 토큰을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노멀라이저는 토크나이저 없이 캐릭터 필터, 토큰 필터로 구성된다. 또한 캐릭터 필터와 토큰 필터를 모두 조합할 수 있는 것은 아니다. 최종적으로 단일 토큰을 생성해야 하기 때문에 글자 단위로 작업을 수행하는 필터만 사용할 수 있다. 엘라스틱서치에서 제공하는 빌트인 노멀라이저는 lowercase밖에 없다. 다른 방법으로 keyword 타입 필드를 처리하려면 커스텀 노멀라이즈를 사용해야 한다.&lt;/p&gt;</description>
      <category>Study/elasticsearch</category>
      <category>analyzer</category>
      <category>character filter</category>
      <category>ElasticSearch</category>
      <category>normalizer</category>
      <category>token filter</category>
      <category>tokenizer</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/210</guid>
      <comments>https://coco-log.tistory.com/210#entry210comment</comments>
      <pubDate>Sat, 5 Aug 2023 18:18:13 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch object와 nested 타입 비교</title>
      <link>https://coco-log.tistory.com/209</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;object와 nested 모두 필드 하위에 다른 필드가 들어가는 계층 구조의 데이터를 담는 타입이다. 이 둘은 유사하지만 배열을 처리할 때 동작하는 방식이 다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;object vs. nested&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;object
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;용도: 일반적인 계층 구조에 사용&lt;/li&gt;
&lt;li&gt;성능: 상대적으로 가벼움&lt;/li&gt;
&lt;li&gt;검색: 일반적인 쿼리를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nested
&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;검색: 전용 nested 쿼리로 감싸서 사용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;object type&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 문서는 필드의 하위에 다른 필드를 여럿 포함하는 객체 데이터를 담는다. object가 객체 데이터의 기본 타입이다. object와 nested의 차이는 객체 배열에 대해 쿼리할 때 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 테스트 데이터를 색인한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;PUT object_test/_doc/1
{
  &quot;price&quot;: 2770.75,
  &quot;spec&quot;: {
    &quot;cores&quot;: 12,
    &quot;memory&quot;: &quot;128&quot;,
    &quot;storage&quot;: 8000
  }
}

PUT object_test/_doc/2
{
  &quot;spec&quot;: [
    {
      &quot;cores&quot;: 12,
      &quot;memory&quot;: 128,
      &quot;storage&quot;: 8000
    },
    {
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 64,
      &quot;storage&quot;: 8000
    },
    {
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 32,
      &quot;storage&quot;: 4000
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인덱스에서 spec.cores가 6개이며, spec.memory는 128인 문서를 검색하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;GET object_test/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;term&quot;: {
            &quot;spec.cores&quot;: &quot;6&quot;
          }
        },
        {
          &quot;term&quot;: {
            &quot;spec.memory&quot;: &quot;128&quot;
          }
        }
      ]
    }
  }

# 결과
  {
	&quot;_index&quot;: &quot;object_test&quot;,
	&quot;_id&quot;: &quot;2&quot;,
	&quot;_score&quot;: 1.1513612,
	&quot;_source&quot;: {
	  &quot;spec&quot;: [
		{
		  &quot;cores&quot;: 12,
		  &quot;memory&quot;: 128,
		  &quot;storage&quot;: 8000
		},
		{
		  &quot;cores&quot;: 6,
		  &quot;memory&quot;: 64,
		  &quot;storage&quot;: 8000
		},
		{
		  &quot;cores&quot;: 6,
		  &quot;memory&quot;: 32,
		  &quot;storage&quot;: 4000
		}
	  ]
	}
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 보면 cores가 6이면서, memory가 128인 문서는 존재하지 않는데 검색이 됐다. 이 검색 결과로 object 타입 데이터가 어떻게 평탄화되는지 알 수 있다. 위 문서는 내부적으로 다음과 같이 평탄화 된다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
	&quot;spec.cores&quot;: [12, 6, 6],
	&quot;spec.memory&quot;: [128, 64, 32],
	&quot;spec.storage&quot;: [8000, 8000, 4000]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spec.cores의 역색인에서 6을 찾을 수 있고, spec.memory에서 128을 찾을 수 있으니 해당 object가 검색 결과로 나타난다. 이처럼 object 타입 배열은 배열을 구성하는 객체 데이터를 서로 독립적인 데이터로 취급하지 않는다. 어떤 경우에도 독립적이어야 한다면 nested 타입을 사용해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nested type&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 타입은 object 타입과 다르게 배열 내 각 객체를 독립적으로 취급한다. 다음과 같이 type에 nested를 명시해서 새 인덱스를 생성한 후 동일한 내용의 문서를 색인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// create index
PUT nested_test
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;spec&quot;: {
        &quot;type&quot;: &quot;nested&quot;,
        &quot;properties&quot;: {
          &quot;cores&quot;: {
            &quot;type&quot;: &quot;long&quot;
          },
          &quot;memory&quot;: {
            &quot;type&quot;: &quot;long&quot;
          },
          &quot;storage&quot;: {
            &quot;type&quot;: &quot;long&quot;
          }
        }
      }
    }
  }
}

// index document
PUT nested_test/_doc/1
{
  &quot;spec&quot;: [
    {
      &quot;cores&quot;: 12,
      &quot;memory&quot;: 128,
      &quot;storage&quot;: 8000
    },
    {
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 64,
      &quot;storage&quot;: 8000
    },
    {
      &quot;cores&quot;: 6,
      &quot;memory&quot;: 32,
      &quot;storage&quot;: 4000
    }
  ]
}

// search
GET nested_test/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;term&quot;: {
            &quot;spec.cores&quot;: {
              &quot;value&quot;: &quot;6&quot;
            }
          }
        },
        {
          &quot;term&quot;: {
            &quot;spec.memory&quot;: {
              &quot;value&quot;: &quot;128&quot;
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 순서로 진행하면 object 타입과 달리 일치하는 문서가 없다는 결과를 얻는다. 여기서 이상한 점은 쿼리를 조금 수정해서 존재하는 문서를 조회해도 결과가 없다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET nested_test/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;term&quot;: {
            &quot;spec.cores&quot;: {
              &quot;value&quot;: &quot;12&quot;
            }
          }
        },
        {
          &quot;term&quot;: {
            &quot;spec.memory&quot;: {
              &quot;value&quot;: &quot;128&quot;
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코어 개수가 12개이면서, 메모리가 128인 문서는 실제로 존재하는데도 결과는 null이다. nested 타입은 객체 배열의 각 객체를 내부적으로 별도의 루씬 문서로 분리해 저장한다. 배열 원소가 100개라면 부모 문서까지 101개의 문서가 내부적으로 생성된다. nested의 이런 동작 방식은 엘라스틱서치 내에서 굉장히 특수하다. 때문에 nested 쿼리라는 전용 쿼리를 이용해서 검색해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 쿼리를 지정하고, path 부분에 검색 대상이 될 nested 타입 필드 이름을 지정한다. 그 다음 query 절을 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nested query&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET nested_test/_search
{
  &quot;query&quot;: {
    &quot;nested&quot;: {
      &quot;path&quot;: &quot;spec&quot;,
      &quot;query&quot;: {
        &quot;bool&quot;: {
          &quot;must&quot;: [
            {
              &quot;term&quot;: {
                &quot;spec.cores&quot;: {
                  &quot;value&quot;: &quot;12&quot;
                }
              }
            },
            {
              &quot;term&quot;: {
                &quot;spec.memory&quot;: {
                  &quot;value&quot;: &quot;128&quot;
                }
              }
            }
          ]
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested 타입은 내부적으로 각 객체를 별도의 문서로 분리해서 저장한다. 따라서 성능 문제가 있을 수 있다.&lt;/p&gt;</description>
      <category>Study/elasticsearch</category>
      <category>object nested 타입 비교</category>
      <category>엘라스틱서치</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/209</guid>
      <comments>https://coco-log.tistory.com/209#entry209comment</comments>
      <pubDate>Sat, 5 Aug 2023 15:50:12 +0900</pubDate>
    </item>
    <item>
      <title>카프카 프로듀서, 컨슈머 기본 동작</title>
      <link>https://coco-log.tistory.com/208</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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJK3Zc/btrZ700eD5E/skDGrT0FIoMoqBfI0ItUR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJK3Zc/btrZ700eD5E/skDGrT0FIoMoqBfI0ItUR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJK3Zc/btrZ700eD5E/skDGrT0FIoMoqBfI0ItUR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJK3Zc%2FbtrZ700eD5E%2FskDGrT0FIoMoqBfI0ItUR1%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;813&quot; height=&quot;900&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&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;code&gt;ProducerRecord&lt;/code&gt;는 카프카로 전송하기 위한 실제 데이터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;topic, partition, key, value로 구성&lt;/li&gt;
&lt;li&gt;카프카는 특정 토픽으로 값(메시지)을 전달하기 때문에 topic/value는 필수값.&lt;/li&gt;
&lt;li&gt;특정 파티션을 지정하기 위한 레코드의 파티션과 정렬을 위한 키는 선택사항&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;레코드는 프로듀서의 &lt;code&gt;send()&lt;/code&gt; 메서드를 통해 &lt;code&gt;serializer&lt;/code&gt; -&amp;gt; &lt;code&gt;partitioner&lt;/code&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 기본적으로 라운드 로빈(RR) 방식을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send()&lt;/code&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;메시지를 보내고 확인하지 않기&lt;/li&gt;
&lt;li&gt;동기 전송&lt;/li&gt;
&lt;li&gt;비동기 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;컨슈머 기본 동작&lt;/h1&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;li&gt;파티션 수와 컨슈머 수는 1:1로 매핑되는 것이 일반적.
&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;/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;오토커밋
&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;/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;오토 커밋과 달리 poll로 가져온 후 현재 오프셋을 커밋함.&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;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;/li&gt;
&lt;li&gt;이건 당연한데, 1번부터 100번 오프셋을 비동기로 가져온다고 생각해보자.&lt;/li&gt;
&lt;li&gt;(극단적인 예시) 1번부터 100번까지의 오프셋 중 2번 빼고 모두 성공했고 100번 오프셋을 커밋함.&lt;/li&gt;
&lt;li&gt;2번 오프셋을 재시도하다가 성공해서 2번 오프셋을 커밋함.&lt;/li&gt;
&lt;li&gt;그럼 컨슈머는 마지막 커밋이 2번 오프셋이니 3번부터 오프셋을 받아오려고 시도함.&lt;/li&gt;
&lt;li&gt;중복 발생.&lt;/li&gt;
&lt;li&gt;이런 이유로 비동기 방식은 재시도하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&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;컨슈머는 컨슈머 그룹 안에 속한 것이 일반적인 구조.&lt;/li&gt;
&lt;li&gt;하나의 그룹 안에 여러 컨슈머가 구성될 수 있음.&lt;/li&gt;
&lt;li&gt;토픽의 파티션과 일대일로 매핑되어 메시지를 가져옴.&lt;/li&gt;
&lt;li&gt;그룹 내 컨슈머들은 서로의 정보를 공유함. 컨슈머01이 문제가 생겨 종료되면 컨슈머02 또는 컨슈머03은 컨슈머01이 컨슘하던 파티션을 대신 컨슘함.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study/kafka</category>
      <category>consumer</category>
      <category>Producer</category>
      <category>카프카</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/208</guid>
      <comments>https://coco-log.tistory.com/208#entry208comment</comments>
      <pubDate>Mon, 20 Feb 2023 19:36:40 +0900</pubDate>
    </item>
    <item>
      <title>카프카 용어</title>
      <link>https://coco-log.tistory.com/207</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;kafka&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;broker&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;producer&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;message&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;topic&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition&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;2 또는 4 정도로 생성한 후, 메시지 처리량이나 컨슈머의 LAG을 모니터링하면서 조금씩 늘려가는 방법이 좋음.&lt;/li&gt;
&lt;li&gt;LAG은 '프로듀서가 보낸 메시지 수(카프카에 남은 메시지 수) - 컨슈머가 가져간 메시지 수를 나타냄.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 5개의 메시지를 카프카로 전송&lt;/li&gt;
&lt;li&gt;컨슈머가 4개의 메시지를 가져감.&lt;/li&gt;
&lt;li&gt;이때 LAG은 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ng3Ug/btrZUCd0k6r/kBN6AcsvMlF4EevrVkZ91K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ng3Ug/btrZUCd0k6r/kBN6AcsvMlF4EevrVkZ91K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ng3Ug/btrZUCd0k6r/kBN6AcsvMlF4EevrVkZ91K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fng3Ug%2FbtrZUCd0k6r%2FkBN6AcsvMlF4EevrVkZ91K%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;606&quot; height=&quot;405&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;segment&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;&lt;code&gt;kafka-logs&lt;/code&gt; 디렉터리 내에 파티션 디렉터리에서 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zmMHo/btrZUBTG3YK/qko7r7CHigMTkTZkZfAtqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zmMHo/btrZUBTG3YK/qko7r7CHigMTkTZkZfAtqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zmMHo/btrZUBTG3YK/qko7r7CHigMTkTZkZfAtqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzmMHo%2FbtrZUBTG3YK%2Fqko7r7CHigMTkTZkZfAtqk%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;481&quot; height=&quot;371&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;replication&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;b&gt;토픽의 '파티션'이 리플리케이션&lt;/b&gt; 되는 것.&lt;/li&gt;
&lt;li&gt;원본과 리플리케이션을 구분하기 위해 &lt;b&gt;리더(원본)&lt;/b&gt;와 &lt;b&gt;팔로워(replica)&lt;/b&gt;라는 용어를 사용한다.&lt;/li&gt;
&lt;li&gt;`replication-factor` 옵션으로 카프카 내 몇 개의 리플리케이션을 만들 것인지 설정할 수 있다.&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;li&gt;카프카에서 권장하는 리플리케이션 팩터는 3이다. 여기서 오해하지 말아야 할 것은 &quot;리플리케이션 팩터가 3&quot;이라는 의미는 리플리케이션이 3개라는 의미가 아니다. 아래 테이블 참고.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;리플리케이션 팩터 수&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;리더 수&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;팔로워 수&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ZooKeeper&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카프카의 메타데이터 관리 및 브로커의 health check을 담당한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study/kafka</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/207</guid>
      <comments>https://coco-log.tistory.com/207#entry207comment</comments>
      <pubDate>Sun, 19 Feb 2023 20:08:36 +0900</pubDate>
    </item>
    <item>
      <title>카프카 기본</title>
      <link>https://coco-log.tistory.com/206</link>
      <description>&lt;h1&gt;&lt;b&gt;기본 &lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&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;분산 시스템은&amp;nbsp; 네트워크상에서 연결된 컴퓨터들의 그룹.&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 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;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;/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;다른 메시징 시스템은 메시지를 전송하고 나면 메시지를 삭제함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아직도 그런지 모르겠음. 2018에 나온&lt;span style=&quot;color: #555555;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/59789254&quot;&gt;카프카, 데이터 플랫폼의 최강자&lt;/a&gt;는 그렇게 설명했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카프카는 디스크에 세그먼트 형식으로 메시지를 저장함.&lt;/li&gt;
&lt;li&gt;디스크에 저장하기 때문에 어떤 이유로 메시지를 전송하지 못한 경우에도 다시 전송할 수 있음.&lt;/li&gt;
&lt;li&gt;하지만 디스크 I/O가 발생함.&lt;/li&gt;
&lt;li&gt;카프카는 이 문제를 OS의 페이지 캐시로 해결함.&lt;/li&gt;
&lt;li&gt;페이지 캐시란 직접 디스크에 I/O 하지 않고, 물리 메모리 중 애플리케이션이 사용하지 않는 일부 잔여 메모리를 활용함.&lt;/li&gt;
&lt;li&gt;카프카는 페이지 캐시를 통해 데이터를 읽고 쓰게 설계됨.&lt;/li&gt;
&lt;li&gt;메모리에 I/O 하기 때문에 디스크 I/O에 비해 훨씬 빠름&lt;/li&gt;
&lt;/ul&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;/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;프로듀서가 메시지를 전송할 때 10개의 메시지를 10번 전송하는 것은 네트워크 오버헤드가 발생할 수 있음.&lt;/li&gt;
&lt;li&gt;TPS 10000이라면 1초에 10000번의 네트워크 I/O가 발생하는 것임.&lt;/li&gt;
&lt;li&gt;카프카는 이 문제를 배치 전송으로 해결함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프로듀서는 이벤트가 발생할 때마다 메시지를 전송하는 것이 아닌 메시지 여러 개를 묶어서 한 번에 전송함.&lt;/li&gt;
&lt;li&gt;이렇게 하면 네트워크 I/O가 확연하게 줄어들기 때문에 오버헤드가 발생할 확률이 낮아짐.&lt;/li&gt;
&lt;li&gt;카프카에서는 배치 전송을 권장하고 있음.&lt;/li&gt;
&lt;/ul&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;/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;카프카에서는 gzip, snappy, lz4, zstd 등 압축 타입을 지원함.&lt;/li&gt;
&lt;li&gt;압축만으로도 네트워크 대역폭이나 회선 비용을 줄일 수 있는데 배치 전송과 결합하면 더욱 높은 효과를 얻을 수 있음.&lt;/li&gt;
&lt;li&gt;높은 압축률이 필요한 경우 gzip, zstd를 권장&lt;/li&gt;
&lt;li&gt;빠른 응답속도가 중요하다면 lz4, snappy 권장&lt;/li&gt;
&lt;li&gt;하지만 메시지 형식이나 크기에 따라 달라질 수 있으니 직접 테스트 해보고 결정하는 것이 좋음.&lt;/li&gt;
&lt;/ul&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;/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;카프카는 '토픽'에 데이터를 저장함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 이는 논리적 개념이고 실제로 데이터가 저장되는 곳은&lt;span&gt;&amp;nbsp;&lt;/span&gt;파티션임.&lt;/li&gt;
&lt;li&gt;토픽 내에서 파티션으로 나뉘는 이유는 병렬 처리 때문.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파티션의 메시지가 저장되는 위치를 오프셋이라고 부름.&lt;/li&gt;
&lt;li&gt;오프셋은 순차적으로 증가하는 64비트 정수임.&lt;/li&gt;
&lt;li&gt;각 파티션에서 오프셋은 고유한 숫자임.&lt;/li&gt;
&lt;li&gt;카프카는 오프셋을 통해 메시지의 순서를 보장함.&lt;/li&gt;
&lt;li&gt;컨슈머에서는 오프셋을 이용해 마지막까지 읽은 메시지 위치를 알 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&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;카프카는 고가용성 보장을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;리플리케이션&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 제공.&lt;/li&gt;
&lt;li&gt;리더와 팔로워라는 용어로 구분.&lt;/li&gt;
&lt;li&gt;고가용성을 위해 무작정 늘리면 안 됨. 그만큼 브로커의 디스크 공간이 소비됨.&lt;/li&gt;
&lt;li&gt;팩터 수를 3으로 권장함.&lt;/li&gt;
&lt;/ul&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;/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;주키퍼는 하둡이나 HBase 등에서도 사용되는 아파치 탑 레벨 프로젝트임.&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;3대로 구성할 경우 2대가 죽으면 나머지 1대도 사용할 수 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;znode를 이용해 카프카의 메타데이터를 기록함.&lt;/li&gt;
&lt;li&gt;주키퍼는 지노드를 이용해 브로커의 노드 관리, 토픽 관리, 컨트롤러 관리 등 매우 중요한 역할임.&lt;/li&gt;
&lt;li&gt;하지만 카프카가 발전하고 주키퍼 성능의 한계가 드러나기 시작함.&lt;/li&gt;
&lt;li&gt;카프카 팀은 주키퍼 의존성을 걷어내는 작업을 진행 중임.&lt;/li&gt;
&lt;li&gt;이미 주키퍼 의존성이 제거된 버전이 릴리스 되었지만 아직 운영에서 쓰기엔 무리.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/kafka</category>
      <category>apache kafka</category>
      <category>카프카 기본 개념</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/206</guid>
      <comments>https://coco-log.tistory.com/206#entry206comment</comments>
      <pubDate>Sun, 19 Feb 2023 20:02:27 +0900</pubDate>
    </item>
    <item>
      <title>ports and adapter 패턴 또는 헥사고날 아키텍처</title>
      <link>https://coco-log.tistory.com/205</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 글은 제 주관이 많이 들어있고 팩트와 다를 수 있습니다. 지적해주시면 고치겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 신규 프로젝트에 포트 앤 어댑터 패턴을 도입하기로 했다. 헥사고날 아키텍처보다 포트 앤 어댑터 패턴이라는 말이 더 명료하게 이 패턴을 설명하는 것 같다. 어쨌든 공부할 시간이 많지는 않았고 짧게 공부한 기록을 남겨본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 앤 어댑터 패턴에 대해 이야기하기 전에 전통적(?)인 레이어드 아키텍처를 먼저 짚고 넘어가야 할 것 같다.&lt;br /&gt;레이어드 아키텍처는 프레젠테이션, 도메인, 영속 세 개의 계층으로 이루어진 아키텍처다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;121&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7mNJF/btrW56KrIb1/tgwctkiEXSNKC6FZwsjGH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7mNJF/btrW56KrIb1/tgwctkiEXSNKC6FZwsjGH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7mNJF/btrW56KrIb1/tgwctkiEXSNKC6FZwsjGH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7mNJF%2FbtrW56KrIb1%2FtgwctkiEXSNKC6FZwsjGH1%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;121&quot; height=&quot;261&quot; data-origin-width=&quot;121&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;프레젠테이션 계층은 HTTP 요청을 처리한다. 도메인 계층은 비즈니스 로직을 담당하는 가장 핵심 레이어다. 마지막으로 영속 계층은 데이터 영속화를 담당한다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 규칙이긴 하지만 강제되지는 않는다. 흔히 영속 계층에 있는 JPA 개체(entity)를 프레젠테이션 영역까지 반환하는 코드를 곳곳에서 볼 수 있다. 소규모 프로젝트에서야 괜찮을지 모르지만 시간이 지나면 프로젝트는 커지고 점점 어디가 어디인지 알 수 없게 된다. 함께 개발하는 사람이 많아질수록 코드는 꼬일 것이다. 어떤 사람은 도메인 레이어에서 비즈니스 로직을 작성하겠지만, 어떤 사람은 도메인과 도메인의 의존성을 끊겠다고 프레젠테이션 레이어에 A서비스를 호출한 결과를 B서비스에 넘기는 중요한 비즈니스 로직 순서를 관장하기도 한다. A를 호출한 다음 B를 호출하는 게 뭐가 어떻느냐고 할 수 있지만 이것은 중요한 비즈니스 로직의 일부다. 비즈니스의 순서를 프레젠테이션 레이어가 알고 있다는 것은 레이어의 책임을 위빈하는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처의 가장 큰 문제점은 데이터베이스 드리븐 개발을 유도한다는 점이다. 보통 Java/Spring으로 개발하면 JPA를 통해 영속 계층을 구현한다. 이럴 경우 도메인 계층과 영속 계층이 강결합된다. 왜? 이런 예시를 들어볼 수도 있을 것 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// domain layer
class PostService(
    private val postRepository: PostRepository
) {
    fun postUpdate(PostUpdateReq req) {
        val post = postRepository.findByIdOrNull(req.id) 
            ?: throw PostNotFoundException(req.postId)

        //영속 계층의 JPA 엔티티를 불러와서 content 수정 메소드를 호출
        post.updateContent(req.content)
    }
}

// persistence layer
@Entity
class Post {
    var content: String = &quot;&quot;

    // 도메인 레이어로부터 content를 받아서 수정
    fun updateContent(content: String) {
        this.content = content
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 계층에 있는 게시물 본문을 수정하는 로직이다. 영속 계층과 매우 강결합되어 있고 비즈니스 로직이 레이어에 걸쳐 분산되어 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 계층은 영속 계층에서 Post를 조회해온다.&lt;/li&gt;
&lt;li&gt;영속 계층의 content를 새로운 content로 바꾸라는 커맨드 메소드를 호출한다.&lt;/li&gt;
&lt;li&gt;영속 계층의 Post 객체는 content를 인자로 받아서 본문 내용을 갱신한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속 계층은 비즈니스 로직을 처리하는 영역이 아니다. 이 코드는 도메인 레이어의 응집도가 낮고, 책임이 여러 레이어로 분산됐다. 레이어드 아키텍처 원칙을 지키고 있다고 할 수도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 코드를 작성하면 가장 중요하고 보호받아야 할 도메인 레이어에 대한 테스트도 어려워진다. 또한 개발 시작부터 JPA 관련된 설정에 신경써야 하기 때문에 계속 JPA와 엮어서 도메인 영역을 생각할 수밖에 없다. 그러다보면 &lt;code&gt;도메인 == JPA 엔티티&lt;/code&gt;라는 착각에 빠진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 도메인 계층과 영속 계층을 확실하게 분리해보면 어떨까?&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// domain layer
class PostService(
    private val postRepository: PostRepository
) {
    fun postUpdate(PostUpdateReq req) {
        val postEntity = postRepository.findByIdOrNull(req.id) 
            ?: throw PostNotFoundException(req.postId)
        val post = Post.of(postEntity)

        //영속 계층의 JPA 엔티티를 불러와서 content 수정 메소드를 호출
        post.updateContent(req.content)

        // 비즈니스 로직은 도메인 레이어의 Post에서 처리하고 
        // 영속 계층은 update 쿼리만 날리도록 한다.
        postRepository.update(post.toJpaEntity())

    }
}

// 도메인 계층의 Post 클래스
class Post {}

// 영속 계층의 PostJpaEntity클래스
@Entity
@Table(name=&quot;post&quot;)
class PostJpaEntity {}

@Repository
class PostRepository(
    private val postJpaRepository: PostRepository
) {

    fun findByIdOrNull(id: Long) {
        // find post...
    }

    fun update(post: PostJpaEntity) {
        // update...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접적으로 JPARepository를 상속받는 &lt;code&gt;PostRepository&lt;/code&gt;를 의존할 때보다는 덜 의존적일 수도 있으나, 여전히 의존적이기는 마찬가지다. 영속 계층의 변경에 도메인 로직이 영향을 받야아 할까? 그럴 이유가 없다. 파일 시스템에 저장하든, 데이터베이스에 저장하든, 캐시에 저장하든 도메인 로직은 달라지지 않는다. 이것이 레이어드 아키텍처의 한계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처가 무작정 &amp;lt;나쁘다&amp;gt;는 의미가 아니다. 대규모 프로젝트에서도 여전히 레이어드 아키텍처는 유효하다. 어쩌면 그것만으로 충분한지도 모르겠다. 굳이 먼길을 가지 않더라도 약간의 우회만으로 한계를 보완해서 쓰는 팀도 분명히 있을 것이다. 하지만 어쩄든 Ports and adapter 또는 헥사고날이라고 하는 아키텍처가 이 레이어드 아키텍처의 한계를 극복할 수 있다고 주장하고 있다. 그리고 따지고 보면 이 길이 그다지 먼길도 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ports and adapter 패턴 또는 헥사고날 아키텍처는 도메인 로직을 가장 안쪽에 두고 보호한다. 도메인 로직은 외부로 향하는 의존성을 가지지 않는다. 이렇게 하는 방법은 의외로 간단하다. 의존성 역전이다. 헥사고날, 육각형이라는 의미는 다방면으로 확장해서 사용할 수 있다는 의미로 이름지어졌다는데, 개인적으로는 ports and adapter 패턴이라는 말이 더 직관적이고 이해하기 쉬운 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ports and adapter 컨셉은 간단하다.&lt;br /&gt;(도메인 바깥에서)&lt;b&gt;어댑터를 만들어서 &lt;/b&gt;(도메인 내부의)&lt;b&gt; 포트에 꽂아라.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kX7pW/btrXa1HPIKY/ib3v3aU2noNPnPkgsZpcO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kX7pW/btrXa1HPIKY/ib3v3aU2noNPnPkgsZpcO1/img.png&quot; data-alt=&quot;https://reflectoring.io/spring-hexagonal/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kX7pW/btrXa1HPIKY/ib3v3aU2noNPnPkgsZpcO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkX7pW%2FbtrXa1HPIKY%2Fib3v3aU2noNPnPkgsZpcO1%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;956&quot; height=&quot;470&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://reflectoring.io/spring-hexagonal/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ports and adapter를 검색하면 이런 다이어그램을 찾아볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Port는 in port와 out port로 나눈다. 나누는 기준도 명료하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in port
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 domain을 사용하는 adapter가 구현해야 하는 포트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;out port
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;domain이 영속 계층 같은 외부 모듈을 사용해야 할 때 만드는 포트.&lt;/li&gt;
&lt;li&gt;외부 영속 모듈은 out port를 구현하지만, 도메인 모듈은 Port에만 의존하기 때문에 어디서 어떤 구현체가 들어오는지 알 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 UseCase.&lt;br /&gt;UseCase는 말 그대로 유즈케이스를 담은 인터페이스다. 이를 테면 게시물을 수정한다고 하면 게시물을 수정하는 유즈케이스 메소드가 하나 있는 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface PostUpdateUseCase {
    fun update(PostUpdateCommand command)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 비즈니스 로직은 이 유즈케이스를 구현해서 만든다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// domain
class PostUpdateService(
    private val postFindPort: PostFindPort,
    private val postUpdatePort: PostUpdatePort,
): PostUpdateUseCase {

    override fun update(PostUpdateCommand command) {
        // post는 JPA 엔티티가 아니라 도메인 모듈의 POJO 엔티티임.
        val post = postFindPort.find(command.id)
        // 업데이트
        post.updateContent(command.content)

        //port의 update()를 호출.
        //UpdatePort의 구현은 런타임에 결정되고, 도메인은 알 수 없음.
        postUpdatePort.update(post)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 도메인 로직을 호출하는 in adapter를 간단하게 만들어보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// web 계층
class PostUpdateController(
    private val postUpdateUseCase: PostUpdateUseCase
) {

    @PutMapping(&quot;/post&quot;)
    fun updatePost(String content) {
        // 도메인 레이어의 UseCase를 호출한다. 구현체는 알 수 없음.
        // 그냥 UseCase 인터페이스에 해야 할 일을 위임.
        postUpdateUseCase.update(PostUpdateCommand.of(content))
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;out port를 구현하는 영속 계층을 간단하게 만들면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 도메인 계층의 Port Interface
public interface PostUpdatePort {
    fun updateContent(post: Post)
}

// 영속 계층. 도메인 계층은 해당 계층이 있는지도 모름.
class PostUpdateAdapter(
    private val postRepository: PostJpaRepository
): PostUpdatePort { //도메인 계층의 PostUpdatePort를 구현함
    override fun updateContent(post: Post) {
        val postJpaEntity = PostJpaEntity.of(post)
        postRepository.save(postJpaEntity)    
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 Ports and adapter 패턴을 사용해서 외부로부터 도메인 계층을 보호하는 프로젝트를 구성할 수 있다. 만약 JPA가 아니라 다른 ORM 또는 SQL Mapper로 변경한다던가, 데이터베이스 자체를 바꾼다던가 혹은 저장소를 캐시로 바꾼다고 하더라도 out port만 구현한다면 도메인 로직은 그대로다. 마찬가지로 Web 요청이 아니라 배치 시스템이나 그외 다른 시스템에서도 마찬가지로 in Port만 구현한다면 도메인 로직을 그대로 사용할 수 있다. 책임이 잘게 나눠지기 때문에 유지보수도 쉬워진다. 거대한 &lt;code&gt;PostService&lt;/code&gt;에서 작업해야 할 곳을 찾는 것보다 비즈니스 로직을 구현한 클래스가 들어있는 service 패키지의 &lt;code&gt;UpdatePostService&lt;/code&gt;에서 작업해야 할 곳을 찾는 것이 훨씬 빠르고 수월할 것이다. 이것이 클린 아키텍처, 헥사고날 아키텍처, 포트 앤 어댑터 패턴을 이야기하는 사람들이 말하는 이 아키텍처의 장점이다.&lt;br /&gt;그런 단점은? 소스코드가 늘어나고 패키지, 클래스의 개수는 더 늘어난다(고 한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 진짜 이 패턴의 단점이 무엇인지 모르겠다. 아직은 이론만 공부하고 간단한 샘플 코드만 만들어봐서 간단하고 명료한 규칙으로 꽤 괜찮은 아키텍처를 구성할 수 있는 실버불릿처럼 느껴진다. 앞으로 실제 프로젝트에 적용해보면서 단점이나 애매한 점을 기록하고 몇 달 뒤에 다시 한 번 여기에 대해 쓰겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;만들면서 배우는 클린 아키텍처(만배클아)/위키북스 톰 홈버그 저, 박소은 옮김&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wikibook/clean-architecture&quot;&gt;https://github.com/wikibook/clean-architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=g6Tg6_qpIVc&quot;&gt;[NHN FORWARD 22] 클린 아키텍처 애매한 부분 정해 드립니다.&lt;/a&gt;&lt;/li&gt;
&lt;li id=&quot;title&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=MKfSLrwLex8%20&quot;&gt;클린아키텍처는 죽었다! 헥사고날 아키텍처 10분만에 대충 이해시켜 드림 | Hexagonal architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study</category>
      <category>ports and adapter</category>
      <category>헥사고날 아키텍처</category>
      <author>voider</author>
      <guid isPermaLink="true">https://coco-log.tistory.com/205</guid>
      <comments>https://coco-log.tistory.com/205#entry205comment</comments>
      <pubDate>Wed, 25 Jan 2023 22:04:44 +0900</pubDate>
    </item>
  </channel>
</rss>