<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>バックエンド</title>
    <link>https://gilbert9172.tistory.com/</link>
    <description>gilbert9172 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 17 Apr 2026 01:51:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gilbert9172</managingEditor>
    <image>
      <title>バックエンド</title>
      <url>https://tistory1.daumcdn.net/tistory/7136172/attach/6c1d3ff3285d4083acc571797f61a59c</url>
      <link>https://gilbert9172.tistory.com</link>
    </image>
    <item>
      <title>12. 데이터 시스템의 미래</title>
      <link>https://gilbert9172.tistory.com/233</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❒ 개요&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❒ 1. 데이터 통합&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;소프트웨어 제품과 그 제품고가 잘 어울리는 환경 사이의 대응 관계를 파악하는 것&lt;/li&gt;
&lt;li&gt;복잡한 환경에서는 데이터를 여러 가지 다른 방법으로 사용하기 때문에, 소프트웨어가 모든 상황에 대처할 가능성이 낮음.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  1-1. 파생 데이터에 특화된 도구의 결합&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개요&lt;/blockquote&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;193&quot; data-start=&quot;106&quot;&gt;OLTP용 데이터베이스(트랜잭션 처리 DB)만으로는 &lt;b&gt;임의 키워드 검색(full-text search)&lt;/b&gt; 같은 걸 잘 처리하기 어려움.&lt;/li&gt;
&lt;li data-end=&quot;304&quot; data-start=&quot;194&quot;&gt;PostgreSQL 같은 DB도 풀텍스트 기능을 제공하지만
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;304&quot; data-start=&quot;194&quot;&gt;&lt;b&gt;복잡하고 정교한 검색&lt;/b&gt;을 하려면 Elasticsearch, Solr 같은 &lt;b&gt;전문 검색 도구&lt;/b&gt;가 더 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;419&quot; data-start=&quot;305&quot;&gt;반대로, &lt;b&gt;검색 인덱스는 system of record&lt;/b&gt;로 쓰기엔 내구성/일관성 측면에서 좋지 않음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;419&quot; data-start=&quot;305&quot;&gt;따라서, 중요한 데이터는 여전히 &lt;b&gt;RDB에 저장&lt;/b&gt;해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;420&quot;&gt;그래서 실무에서는 아래의 도구를 조합해서 요구사항을 만족 시킴&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;420&quot;&gt;&amp;ldquo;쓰기/정합성&amp;rdquo;은 DB&lt;/li&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;420&quot;&gt;&amp;ldquo;검색/질의&amp;rdquo;는 검색엔진&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;420&quot;&gt;같은 비즈니스 데이터가 여러 시스템에 복제&amp;middot;변환돼 존재하기 때문에
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;420&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터플로에 대한 추론 (Reasoning about dataflows)&lt;/blockquote&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;&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;파생 데이터 vs 분산 트랜잭션&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추상적인 수준에서 보면 파생 데이터와 분산 트랜잭션은 다른 방식으로 유사한 목표를 달성함.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;분산 트랜잭션
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로그를 사용해서 순서를 결정&lt;/li&gt;
&lt;li&gt;결정적 재시도와 멱등성을 기반으로 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;두 시스템의 가장 큰 차이점
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 시스템
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;선형성을 지원함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파생 데이터 시스템
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;비동기로 갱신되기 때문에 기본적으로 동시간 갱신 보장을 하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 순서화의 제약 (The limits of total ordering)&lt;/blockquote&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;하지만&amp;nbsp; 파티셔닝, 다중 데이터센터, 마이크로서비스, 오프라인 클라이언트 등 현실적인 요구가 늘어나면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 전역 total order를 유지하는 것이 근본적으로 어렵고, 현재 합의 알고리즘으로도 한계가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;인과성 획득을 위한 이벤트 순서화&lt;br /&gt;&lt;i&gt;➔ 모든 이벤트를 전역 순서로 정하지 못하더라도, 중요한 &amp;lsquo;인과관계(causality)&amp;rsquo;만큼은 어떻게 보존할 것인가?&lt;/i&gt;&lt;/blockquote&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;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;이 경우, 뒷담 깐거를 들길 수 있음.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 문제를 해결할 수 있는 3가지 방법 제시&amp;nbsp;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;논리적 타임스탬프(Logical timestamps)&lt;/li&gt;
&lt;li&gt;&amp;ldquo;사용자가 본 상태&amp;rdquo;를 이벤트로 기록하기&lt;/li&gt;
&lt;li&gt;충돌 해소(Conflict resolution) 알고리즘&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt; 1-2. 일괄 처리와 스트림 처리&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;필자는...&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;입력 ➔ 형 변환 ➔ 필터링 ➔ 집계 ➔ 모델 학습 ➔ 평가 ➔ 출력&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;파생 상태 유지&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력과 출력을 잘 정의한 결정적 함수의 원리는
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내결함성에 도움이 됨.&lt;/li&gt;
&lt;li&gt;조직 내의 데이터플로 추론을 단순화 함.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이론상으로 파생 데이터 시스템은
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;관계형 DB가 색인할 테이블에 기록하는 트랜잭션 내에서 보조 색인을 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 동기식으로 갱신하는 것처럼 동기식으로 운영할 수 있음.&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;애플리케이션 발전을 위한 데이터 재처리&lt;/blockquote&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;/li&gt;
&lt;li&gt;새로운 타입의 레코드를 추가하는 것과 같은 간단한 것으로 제한됨.&lt;/li&gt;
&lt;/ul&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;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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;람다 아키텍쳐&lt;/blockquote&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;람다&lt;span&gt; &lt;/span&gt;아키텍처를&lt;span&gt; &lt;/span&gt;쉽게&lt;span&gt; &lt;/span&gt;설명하면&lt;span&gt;, &lt;/span&gt;들어오는&lt;span&gt; &lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;삭제하거나&lt;span&gt; &lt;/span&gt;수정하지&lt;span&gt; &lt;/span&gt;않고&lt;span&gt; &lt;/span&gt;일단&lt;span&gt; &lt;/span&gt;저장해두고&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 필요할&lt;span&gt; &lt;/span&gt;때마다&lt;span&gt; &lt;/span&gt;저장된&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;다양하게&lt;span&gt; &lt;/span&gt;분석하거나&lt;span&gt; &lt;/span&gt;복구하는&lt;span&gt; &lt;/span&gt;구조라고&lt;span&gt; &lt;/span&gt;이해하면 됨.&lt;/li&gt;
&lt;li&gt;즉&lt;span&gt;, &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;바꾸거나&lt;span&gt; &lt;/span&gt;지우지&lt;span&gt; &lt;/span&gt;않고&lt;span&gt; &lt;/span&gt;계속&lt;span&gt; &lt;/span&gt;추가만&lt;span&gt; &lt;/span&gt;해서&lt;span&gt; &quot;&lt;/span&gt;모든&lt;span&gt; &lt;/span&gt;기록을&lt;span&gt; &lt;/span&gt;남겨두는&lt;span&gt; &lt;/span&gt;방식&lt;span&gt;&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실제로&lt;span&gt; &lt;/span&gt;잘못된&lt;span&gt; &lt;/span&gt;결과가&lt;span&gt; &lt;/span&gt;나오거나&lt;span&gt; &lt;/span&gt;새로운&lt;span&gt; &lt;/span&gt;분석&lt;span&gt; &lt;/span&gt;방법이&lt;span&gt; &lt;/span&gt;생겼을&lt;span&gt; &lt;/span&gt;때도&lt;span&gt; &lt;/span&gt;원본&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;바탕으로&lt;span&gt; &lt;/span&gt;다시&lt;span&gt; &lt;/span&gt;계산하거나&lt;span&gt;, &lt;br /&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 원하는&lt;span&gt; &lt;/span&gt;정보로&lt;span&gt; &lt;/span&gt;바꿀&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있음.&lt;span&gt;​ &lt;/span&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;이후에 일괄 처리자가 같은 이벤트 집합을 소비해 정확한 버전의 파생 뷰에 반영함.&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;반면에 스트림 처리자는 신뢰성이 떨어지고 내결함성을 확보하기 어렵다는 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;근데 필자는 람다 아키텍쳐에 문제가 있다고 생각함
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;배치 코드 + 스트림 코드를 &lt;b&gt;두 벌&lt;/b&gt; 유지해야 함.&lt;/li&gt;
&lt;li&gt;전체 재처리는 비싸서, 결국 &lt;b&gt;&amp;ldquo;증분 배치&amp;rdquo;&lt;/b&gt;가 필요해짐. (특정 시간의 데이터만 배치 처리하는 등..)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일괄 처리와 스트림 처리의 통합&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최근에는 같은 시스템에서 일괄 처리 연산과 스트림 연산을 모두 구현함으로써&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 람다 아키텍처의 단점을 빼고 장점만 취할 수 있게 하는 작업이 진행되고 있음.&lt;/li&gt;
&lt;li&gt;배치와 스트림을 한 시스템으로 통합하려면 &lt;b&gt;다음 세 가지 기능&lt;/b&gt;이 필요함.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;최근 이벤트 스트림을 다루는 처리 엔진에서 과거 이벤트를 재생하는 능력&lt;/li&gt;
&lt;li&gt;스트림 처리에서도 &lt;b&gt;Exactly-once semantics&lt;/b&gt; 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 시간(event time)&lt;/b&gt; 기준 윈도우링 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위 세 가지 기능을 가지고 있다면&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;람다 아키텍처처의 단점은 없앨 수 있고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 스트리밍/데이터플로우 엔진&lt;/b&gt;이
&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;을 만들 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;6bbcfe52-8364-4434-85da-9cb0cf9211a3&quot; data-message-author-role=&quot;assistant&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;&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;❐ 2. 데이터베이스 언번들링 (데이터베이스의 분리)&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자는 &lt;b&gt;운영체제(Unix)&lt;/b&gt; 와 &lt;b&gt;데이터베이스(DB)&lt;/b&gt; 를 비교하면서&amp;nbsp; &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 두 시스템이 &lt;b&gt;같은 목적을 다른 철학으로 해결한 역사적 흐름&lt;/b&gt;을 설명하는 파트&lt;/li&gt;
&lt;li&gt;Unix는 단순하지만 직접 해야 할 게 많음&lt;/li&gt;
&lt;li&gt;DB는 강력하지만 내부가 감춰져 있음.&lt;/li&gt;
&lt;li&gt;현대 시스템은 이 둘의 장점을 결합해 &amp;ldquo;유연하면서도 강력한 데이터 시스템&amp;rdquo;을 지향함.
&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;의 출발점&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-1. 데이터 저장소 기술 구성하기&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터베이스가 제공하는 다양한 기능에 대한 요약 설명&lt;/blockquote&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;Full-text 검색 색인은 텍스트에서 키워드 검색을 가능하게 하는 기능이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;색인 생성하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필자는 인덱스 생성이 단순한 DB 내부 작업이 아니라,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 재처리(reprocessing)&lt;/b&gt; 와 &lt;b&gt;파생 데이터(derived data)&lt;/b&gt; 의 일종임을 강조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;CREATE INDEX 실행 시 일어나는 일&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;일관된 스냅샷(consisent snapshot) 확보&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;732&quot;&gt;인덱스를 생성하려면, 특정 시점의 테이블 상태를 완전하게 읽어야 함.&lt;/li&gt;
&lt;li data-end=&quot;833&quot; data-start=&quot;776&quot;&gt;DB는 트랜잭션 격리 수준을 활용해 &lt;b&gt;일관된 시점의 데이터 복사본(snapshot)&lt;/b&gt; 을 확보.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;833&quot; data-start=&quot;776&quot;&gt;&lt;b&gt;인덱싱할 필드 추출 및 정렬&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;960&quot; data-start=&quot;861&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;960&quot; data-start=&quot;861&quot;&gt;예: CREATE INDEX ON users(email)&lt;/li&gt;
&lt;li data-end=&quot;960&quot; data-start=&quot;861&quot;&gt;모든 users.email 값을 읽어서 정렬 후, B-Tree 혹은 다른 인덱스 구조로 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;960&quot; data-start=&quot;861&quot;&gt;&lt;b&gt;인덱스 파일 쓰기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1018&quot; data-start=&quot;982&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1018&quot; data-start=&quot;982&quot;&gt;정렬된 데이터를 기반으로 새로운 인덱스 파일을 디스크에 기록.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1018&quot; data-start=&quot;982&quot;&gt;&lt;b&gt;스냅샷 이후에 발생한 쓰기(write) 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1085&quot; data-start=&quot;1055&quot;&gt;인덱스 생성 중에도 테이블은 계속 쓰기될 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;1137&quot; data-start=&quot;1089&quot;&gt;DB는 &amp;ldquo;스냅샷 이후 발생한 변경분(backlog)&amp;rdquo;을 추적하여 새 인덱스에 반영.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1137&quot; data-start=&quot;1089&quot;&gt;&lt;b&gt;지속적인 동기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1206&quot; data-start=&quot;1158&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1206&quot; data-start=&quot;1158&quot;&gt;인덱스 생성이 끝나면, 이후 트랜잭션이 테이블에 쓰기할 때마다 인덱스도 함께 갱신..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1206&quot; data-start=&quot;1158&quot;&gt;즉, 새 인덱스를 만든다는 것은
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1206&quot; data-start=&quot;1158&quot;&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;blockquote data-ke-style=&quot;style2&quot;&gt;모든 것의 메타데이터베이스 (The meta-database of everything)&lt;/blockquote&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;span&gt;하나의&lt;/span&gt; DB &lt;span&gt;엔진&lt;/span&gt; &lt;span&gt;안에&lt;/span&gt; &lt;span&gt;모든&lt;/span&gt; &lt;span&gt;인덱스&lt;/span&gt; &lt;span&gt;기능&lt;/span&gt; &lt;span&gt;포함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;B-tree, Hash, Spatial &lt;span&gt;등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 기능을 독립된 전문 시스템으로 분리(Unbundling)&lt;/b&gt; 하는 방향으로 진화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;텍스트&lt;/span&gt; &lt;span&gt;검색&lt;/span&gt; : Elasticsearch, OpenSearch&lt;/li&gt;
&lt;li&gt;이벤트&lt;span&gt; &lt;/span&gt;스트림&lt;span&gt; &lt;/span&gt;저장 : Kafka&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필자는 서로다른 저장소와 처리 도구를 사용하지만 하나의 응집된 시스템으로 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 구성할 수 있는 2가지 방법이 있다고 생각함.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;연합 데이터 베이스 : 읽기를 통합
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;611&quot; data-start=&quot;550&quot;&gt;여러 종류의 &lt;b&gt;저장 엔진과 처리 방식&lt;/b&gt;을 대상으로 &lt;b&gt;하나의 통합 쿼리 인터페이스&lt;/b&gt;를 제공하는 접근.&lt;/li&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;612&quot;&gt;즉, 다양한 데이터 소스를 &lt;b&gt;하나의 SQL처럼 읽을 수 있게 하는 방식&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;612&quot;&gt;쓰기(write) 일관성 유지나 트랜잭션 처리에는 약함&lt;/li&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;612&quot;&gt;PostgreSQL Foreign Data Wrapper (FDW)\
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;612&quot;&gt;외부의 다른 데이터 소스(MySQL, CSV, REST API 등)를 PostgreSQL 테이블처럼 읽을 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;언번들링 데이터베이스 : 쓰기를 통합
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1259&quot; data-start=&quot;1195&quot;&gt;여러 저장소 간에 &lt;b&gt;쓰기(write)&lt;/b&gt; 를 &lt;b&gt;일관되게 동기화(synchronize)&lt;/b&gt; 하는 문제를 다룸.&lt;/li&gt;
&lt;li data-end=&quot;1302&quot; data-start=&quot;1260&quot;&gt;즉, &lt;b&gt;&amp;ldquo;데이터 변경을 여러 시스템에 안정적으로 반영&amp;rdquo;&lt;/b&gt; 하는 방식.&lt;/li&gt;
&lt;li data-end=&quot;1395&quot; data-start=&quot;1314&quot;&gt;&lt;b&gt;Change Data Capture (CDC)&lt;/b&gt;, &lt;b&gt;Event Log&lt;/b&gt;, &lt;b&gt;Outbox Pattern&lt;/b&gt; 등을 통한 데이터 동기화.&lt;/li&gt;
&lt;li data-end=&quot;1445&quot; data-start=&quot;1396&quot;&gt;예: 트랜잭션 로그를 Kafka로 보내고, 이를 검색 인덱스나 캐시 시스템이 구독함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;언번들링이 동작하게 만들기&lt;/blockquote&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;702&quot; data-start=&quot;656&quot;&gt;Federation vs Unbundling &amp;mdash; &amp;ldquo;같은 동전의 양면&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;761&quot; data-start=&quot;703&quot;&gt;&lt;b&gt;Federation&lt;/b&gt; : 여러 데이터 소스를 하나의 인터페이스로 &lt;b&gt;읽기(read)&lt;/b&gt; 통합&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;&lt;b&gt;Unbundling&lt;/b&gt; : 여러 데이터 시스템 간 &lt;b&gt;쓰기(write)&lt;/b&gt; 를 일관되게 &lt;b&gt;동기화(synchronize)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 둘 다 &lt;b&gt;이질적인 시스템을 결합(composition)&lt;/b&gt; 한다는 점에서 동일한 목표를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;쓰기 동기화(Write Synchronization)는 훨씬 어려움
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;읽기 통합은 단지 &amp;ldquo;데이터 모델 간 매핑(mapping)&amp;rdquo; 문제라서 상대적으로 단순함.&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;하지만 &lt;b&gt;쓰기 통합&lt;/b&gt;은 여러 시스템에 걸쳐 데이터를 &lt;b&gt;정합성 있게 동시에 반영&lt;/b&gt;해야 함.&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;즉, &amp;ldquo;한쪽에 쓴 내용이 다른 쪽에도 반영되어야 한다&amp;rdquo;는 문제는 훨씬 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;전통적 접근: 분산 트랜잭션(Distributed Transactions)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;과거에는 여러 저장소에 걸친 쓰기를 &lt;b&gt;2PC &lt;/b&gt;등의 분산 트랜잭션으로 처리하려고 했다.&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;762&quot;&gt;하지만 이 방식은 다음과 같은 문제가 있음:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1383&quot; data-start=&quot;1271&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1304&quot; data-start=&quot;1271&quot;&gt;이기종 시스템 간에는 표준화된 트랜잭션 프로토콜이 없음.&lt;/li&gt;
&lt;li data-end=&quot;1349&quot; data-start=&quot;1307&quot;&gt;네트워크 지연, 장애 시 전체 시스템이 멈출 위험(결합도가 너무 높음).&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;따라서 &lt;b&gt;확장성과 견고성이 떨어지는 구조&lt;/b&gt;가 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;추천 접근: 이벤트 로그(Event Log) 기반의 비동기 통합
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;저자는 대신 다음 방식을 제안 ➔&amp;nbsp; &lt;b&gt;&amp;ldquo;비동기 이벤트 로그 + 멱등(idempotent) 쓰기&amp;rdquo;&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;즉, 데이터를 쓸 때 즉시 여러 시스템에 쓰는 대신,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;&lt;b&gt;하나의 이벤트 로그&lt;/b&gt;에 변경을 기록하고&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;다른 시스템들은 이 로그를 구독(consume)하며 상태를 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;이 방식은 &lt;b&gt;Derived Data System&lt;/b&gt;, &lt;b&gt;Change Data Capture(CDC)&lt;/b&gt; 와 같은 형태로 구현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;왜 이벤트 로그 방식이 더 나은가
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;시스템 수준의 이점 - 느슨한 결합
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1383&quot; data-start=&quot;1352&quot;&gt;각 구성 요소가 &lt;b&gt;비동기적으로 통신&lt;/b&gt;하므로, 일부 시스템이 느리거나 장애가 나도 전체가 멈추지 않음.&lt;/li&gt;
&lt;li data-end=&quot;1898&quot; data-start=&quot;1863&quot;&gt;이벤트 로그가 &lt;b&gt;버퍼 역할&lt;/b&gt;을 하며 데이터 손실을 방지.&lt;/li&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1901&quot;&gt;장애가 복구되면 &lt;b&gt;소비자(consumer)&lt;/b&gt; 는 이벤트를 재처리(catch up) 가능.&lt;/li&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1901&quot;&gt;반대로 분산 트랜잭션은 동기적으로 묶여 있어서 &lt;b&gt;부분 장애가 전체 장애로 확산(escalation)&lt;/b&gt; 되기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1901&quot;&gt;조직 수준의 이점 &amp;mdash; 독립 개발과 유지보수
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1901&quot;&gt;각 팀이 &lt;b&gt;자신의 시스템에만 집중&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2195&quot; data-start=&quot;2173&quot;&gt;팀 간 인터페이스(계약)가 명확해짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;내구성 (Durability)&lt;/li&gt;
&lt;li&gt;순서 보장 (Ordering)&lt;/li&gt;
&lt;li&gt;멱등성 (Idempotence)&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;b&gt;Unbundling(분리된 데이터 시스템의 통합)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 을 가능하게 만드는 핵심은&lt;br /&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 분산 트랜잭션이 아니라 &lt;b&gt;비동기 이벤트 로그(asynchronous log)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 기반의 통합이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;시스템적으로는 느슨한 결합과 복원력(resilience)을 확보하고,&lt;/li&gt;
&lt;li&gt;조직적으로는 독립된 개발&amp;middot;운영을 가능하게 한다.&lt;/li&gt;
&lt;li&gt;결국 &amp;ldquo;로그(Log)&amp;rdquo;는 &lt;b&gt;데이터 통합의 중심 축&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이자,&lt;br /&gt;&lt;/span&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 현대 분산 아키텍처에서 데이터 일관성을 유지하는 가장 현실적인 수단&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&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;blockquote data-ke-style=&quot;style2&quot;&gt;언번들링 vs 통합 시스템&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자는 &amp;ldquo;Unbundling이 미래의 방향&amp;rdquo;이라고 말하면서도, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 통합형 데이터베이스의 역할이 여전히 필수적임을 강조&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 즉, &amp;ldquo;모든 걸 분리하라&amp;rdquo;가 아니라 &amp;ldquo;필요할 때만 분리하라&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Unbundling이 대체가 아니라 &amp;ldquo;보완&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;Unbundling(분리형)&amp;rdquo; 접근이 &lt;b&gt;데이터베이스를 완전히 대체하지는 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, 스트림/배치 시스템의 결과를 저장하고 서빙(Serving) 하는 역할은 여전히 DB가 맡는다.&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;뭐가 빠졌지?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리는 Kafka, DB, Search, Stream Processor 등 훌륭한 부품을 이미 갖고 있음&lt;/li&gt;
&lt;li&gt;하지만 이들을 Unix 파이프처럼 단순하게 조합할 &lt;b&gt;&amp;ldquo;언어(shell)&amp;rdquo;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-2. 데이터플로 주변 애플리케이션 설계&lt;/p&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&gt;데이터 파생(derivation)은 모든 시스템의 핵심 동작이다.&lt;/li&gt;
&lt;li&gt;하지만 단순 인덱스 생성 외의 복잡한 파생 로직은 애플리케이션 코드로 직접 구현해야 한다.&lt;/li&gt;
&lt;li&gt;대부분의 데이터베이스는 이러한 &amp;ldquo;파생 로직(derivation function)&amp;rdquo;을 내장적으로 다루지 못한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 이는 Unbundled Architecture 의 핵심 과제 중 하나다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;파생 함수로서의 애플리케이션 코드&amp;nbsp;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 파생 데이터는 원본 데이터를 변환 함수(transformation function) 를 통해 얻어짐.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;자동화된 파생 함수 vs 커스텀 파생 함수&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1905&quot; data-start=&quot;1810&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;자동화된 파생 함수&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1905&quot; data-start=&quot;1810&quot;&gt;보조 인덱스(secondary index) 생성은 너무 자주 쓰이기 때문에&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; DB가 내부적으로 &lt;b&gt;자동 처리 기능&lt;/b&gt;으로 내장 (CREATE INDEX).&lt;/li&gt;
&lt;li data-end=&quot;1951&quot; data-start=&quot;1906&quot;&gt;즉, &lt;b&gt;derivation function이 DB 엔진에 내장&lt;/b&gt;되어 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2121&quot; data-start=&quot;2054&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;커스텀 파생 함수&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2121&quot; data-start=&quot;2054&quot;&gt;전문 검색, 머신러닝, 캐시 구축 등은 &lt;b&gt;도메인 특화 로직&lt;/b&gt;이 필요함.&lt;/li&gt;
&lt;li data-end=&quot;2159&quot; data-start=&quot;2122&quot;&gt;따라서 표준화된 기능이 아니라 &lt;b&gt;직접 코드&lt;/b&gt;로 구현해야 함.&lt;/li&gt;
&lt;li data-end=&quot;2222&quot; data-start=&quot;2160&quot;&gt;이 때 애플리케이션 코드가 derivation function 역할을 수행.&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;애플리케이션 코드와 상태의 분리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;DB는 데이터 저장에, 애플리케이션은 코드 실행에&amp;rdquo; &amp;rarr; 역할 분리가 합리적&lt;/li&gt;
&lt;li&gt;&amp;ldquo;상태(state)는 DB에, 로직(logic)은 코드에&amp;rdquo;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 이것이 오늘날의 기본적인 시스템 구조&lt;/li&gt;
&lt;li&gt;DB는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&amp;ldquo;공유 가능한 가변 변수(shared mutable variable)&amp;rdquo;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;처럼 작동
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;하지만 문제는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB 변경을 실시간으로 감지(subscribe)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하기 어려움&lt;/li&gt;
&lt;li&gt;&lt;span&gt;대부분의 DB는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;수동적(polling)&lt;/b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;ldquo;값이 바뀌었는지&amp;rdquo;를 주기적으로 쿼리해야 함.&lt;/li&gt;
&lt;li&gt;최근의 CDC&amp;middot;Change Stream 기술은 이 간극을 메워,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB와 애플리케이션 간의 진정한 분리가 가능함&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;데이터플로 : 상태 변경과 애플리케이션 코드 간 상호작용&lt;br /&gt;➔ &lt;i&gt;Unbundled Database = Dataflow 시스템으로 재조립된 DB&lt;/i&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;926&quot; data-start=&quot;895&quot;&gt;기존의 관계 &amp;mdash; &amp;ldquo;코드가 상태를 조작한다&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1116&quot; data-start=&quot;1025&quot;&gt;전통적으로 우리는 DB를 &lt;b&gt;단순한 상태 저장소(state holder)&lt;/b&gt; 로 봄&lt;/li&gt;
&lt;li data-end=&quot;1116&quot; data-start=&quot;1025&quot;&gt;애플리케이션이 DB를 읽고 &amp;rarr; 로직 처리 후 &amp;rarr; DB에 다시 쓴다.&lt;/li&gt;
&lt;li data-end=&quot;1197&quot; data-start=&quot;1117&quot;&gt;즉, 애플리케이션은 &lt;b&gt;명령어 중심(command-driven)&lt;/b&gt; 으로 동작하고,&lt;/li&gt;
&lt;li data-end=&quot;1197&quot; data-start=&quot;1117&quot;&gt;DB는 &lt;b&gt;수동적(passive)&lt;/b&gt; 역할에 머문다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1197&quot; data-start=&quot;1117&quot;&gt;새로운 관점 &amp;mdash; &amp;ldquo;상태 변화와 코드의 상호작용(interplay)&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1357&quot;&gt;이제는 DB의 상태 변화 자체를 &lt;b&gt;이벤트(event)&lt;/b&gt; 로 보고,&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1357&quot;&gt;애플리케이션 코드가 그 변화를 &lt;b&gt;구독(subscribe)&lt;/b&gt; 하여 반응하는 구조로 변화.&lt;/li&gt;
&lt;li data-end=&quot;1552&quot; data-start=&quot;1451&quot;&gt;예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1498&quot; data-start=&quot;1460&quot;&gt;사용자가 주문 생성 &amp;rarr; &amp;ldquo;OrderCreated&amp;rdquo; 이벤트 발생&lt;/li&gt;
&lt;li data-end=&quot;1552&quot; data-start=&quot;1501&quot;&gt;코드가 이 이벤트를 받아 &amp;rarr; 결제 요청, 재고 차감 등의 후속 상태 변화(trigger)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1602&quot; data-start=&quot;1553&quot;&gt;즉, &lt;b&gt;하나의 상태 변화가 또 다른 상태 변화를 유도하는 체계적인 데이터 흐름&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1602&quot; data-start=&quot;1553&quot;&gt;역사적&amp;middot;구조적 맥락
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1831&quot; data-start=&quot;1688&quot;&gt;이 개념은 &lt;b&gt;&amp;ldquo;데이터베이스와 스트림은 동등하다&amp;rdquo;&lt;/b&gt; 는 책의 핵심 논의에서 이어진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1787&quot; data-start=&quot;1741&quot;&gt;DB의 트랜잭션 로그(log)는 곧 이벤트 스트림(event stream)이다.&lt;/li&gt;
&lt;li data-end=&quot;1831&quot; data-start=&quot;1790&quot;&gt;따라서 로그를 구독하면 DB의 상태 변화를 실시간으로 감지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1988&quot; data-start=&quot;1832&quot;&gt;비슷한 아이디어가 이미 오래전부터 존재했음:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1888&quot; data-start=&quot;1861&quot;&gt;Actor 모델 (메시지 기반 동시성)&lt;/li&gt;
&lt;li data-end=&quot;1933&quot; data-start=&quot;1891&quot;&gt;Tuple space 모델 (프로세스가 공유 상태의 변화에 반응)&lt;/li&gt;
&lt;li data-end=&quot;1988&quot; data-start=&quot;1936&quot;&gt;Triggers &amp;amp; Secondary Indexes (DB 내부의 자동 반응 로직)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1988&quot; data-start=&quot;1936&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Unbundling 관점에서 본 Dataflow&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2265&quot; data-start=&quot;2201&quot;&gt;DB 내부에서만 일어나던 &amp;ldquo;상태 변화에 대한 반응(trigger)&amp;rdquo;을 외부 시스템으로 확장하는 것.&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2266&quot;&gt;예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2404&quot; data-start=&quot;2273&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2300&quot; data-start=&quot;2273&quot;&gt;DB &amp;rarr; Kafka (Change Event)&lt;/li&gt;
&lt;li data-end=&quot;2340&quot; data-start=&quot;2303&quot;&gt;Kafka &amp;rarr; Elasticsearch (검색 인덱스 업데이트)&lt;/li&gt;
&lt;li data-end=&quot;2374&quot; data-start=&quot;2343&quot;&gt;Kafka &amp;rarr; ML Pipeline (모델 업데이트)&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;Kafka &amp;rarr; Cache (UI 캐시 리빌드)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;이 모든 과정이 &lt;b&gt;데이터플로우(dataflow)&lt;/b&gt; 로 연결된다.&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;&lt;b&gt;즉, &amp;ldquo;상태 변화 &amp;rarr; 코드 반응 &amp;rarr; 새로운 상태 생성&amp;rdquo;의 연쇄.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;데이터플로우와 일반 메시징 시스템의 차이점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;파생 데이터를 유지하기 위해선 아래 두 가지 조건이 매우 중요함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;순서 보장&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;내구성 &amp;amp; 내결함성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;Stream Processor = 현대의 파생 함수 엔진
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2404&quot; data-start=&quot;2377&quot;&gt;각 스트림 연산자(operator)는 &amp;ldquo;데이터 변화&amp;rdquo;를 입력으로 받아 새로운 상태를 산출하는 &amp;ldquo;함수&amp;rdquo; 로 동작.&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;blockquote data-ke-style=&quot;style2&quot;&gt;스트림 처리자와 서비스&lt;/blockquote&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;새로운 패러다임: Stream-based Dataflow Systems
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1654&quot; data-start=&quot;1577&quot;&gt;스트림 처리 시스템도 &amp;ldquo;작은 단위의 연산(operators)&amp;rdquo;을 연결하여 큰 시스템을 구성한다는 점에서 마이크로서비스와 유사함.&lt;/li&gt;
&lt;li data-end=&quot;1771&quot; data-start=&quot;1655&quot;&gt;그러나 &lt;b&gt;핵심 차이는 통신 방식&lt;/b&gt;에 있음:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1771&quot; data-start=&quot;1685&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1730&quot; data-start=&quot;1685&quot;&gt;Microservice &amp;rarr; &lt;b&gt;Request/Response (동기식)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1771&quot; data-start=&quot;1733&quot;&gt;Dataflow &amp;rarr; &lt;b&gt;Message Stream (비동기식)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1771&quot; data-start=&quot;1733&quot;&gt;즉, &amp;ldquo;함수 호출&amp;rdquo;이 아닌 &amp;ldquo;데이터 흐름(event flow)&amp;rdquo;으로 상호작용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1771&quot; data-start=&quot;1733&quot;&gt;스트림 기반 접근의 본질: &lt;b&gt;Stream Join&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2611&quot; data-start=&quot;2563&quot;&gt;이 접근은 &amp;ldquo;RPC 호출&amp;rdquo;을 &amp;ldquo;스트림 조인(stream join)&amp;rdquo;으로 대체한 것.&lt;/li&gt;
&lt;li data-end=&quot;2694&quot; data-start=&quot;2612&quot;&gt;두 이벤트 스트림:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2694&quot; data-start=&quot;2627&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2656&quot; data-start=&quot;2627&quot;&gt;purchase events (구매 이벤트)&lt;/li&gt;
&lt;li data-end=&quot;2694&quot; data-start=&quot;2659&quot;&gt;exchange rate updates (환율 이벤트)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2739&quot; data-start=&quot;2695&quot;&gt;두 스트림을 시간 기준으로 조인하여 &amp;rarr; &amp;ldquo;구매 시점의 환율&amp;rdquo;을 결합.&lt;/li&gt;
&lt;li data-end=&quot;2739&quot; data-start=&quot;2695&quot;&gt;&lt;b&gt;시간 의존성(time-dependence)&lt;/b&gt; 주의:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2854&quot; data-start=&quot;2776&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2803&quot; data-start=&quot;2776&quot;&gt;나중에 재처리할 경우 환율이 달라질 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;2854&quot; data-start=&quot;2804&quot;&gt;따라서 &amp;ldquo;구매 시점의 환율&amp;rdquo;을 복원하려면 과거 환율 이벤트를 함께 보관해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2854&quot; data-start=&quot;2804&quot;&gt;데이터플로우 접근은 &lt;b&gt;&amp;ldquo;요청 기반(request-driven)&amp;rdquo; &amp;rarr; &amp;ldquo;이벤트 기반(event-driven)&amp;rdquo;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-3. 파생 상태 관찰하기&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FttTa/dJMb99SfUPO/BoHMICJsrMgTVOCE0SiM7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FttTa/dJMb99SfUPO/BoHMICJsrMgTVOCE0SiM7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FttTa/dJMb99SfUPO/BoHMICJsrMgTVOCE0SiM7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFttTa%2FdJMb99SfUPO%2FBoHMICJsrMgTVOCE0SiM7k%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;1284&quot; height=&quot;456&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;456&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 data-end=&quot;2273&quot; data-start=&quot;2237&quot;&gt;&lt;b&gt;Write Path&lt;/b&gt;는 &lt;b&gt;즉시 처리(eager), &lt;/b&gt;&lt;b&gt;Read Path&lt;/b&gt;는 &lt;b&gt;요청 시 처리(lazy) &lt;/b&gt;라는 관점에서 서로 보완 관계를 이룬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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 style=&quot;text-align: center;&quot;&gt;설계 방향&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Write-heavy (사전 계산 중심)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;- 쓰기 시점에 많은 계산 수행- 읽기 시 매우 빠름&lt;/td&gt;
&lt;td&gt;검색 인덱스, Materialized View&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Read-heavy (요청 시 계산 중심)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;- 쓰기는 가볍지만- 읽기 시 많은 계산 필요&lt;/td&gt;
&lt;td&gt;OLTP 쿼리, 실시간 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;균형형&lt;/b&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파생 데이터(derived dataset)는 &amp;ldquo;미리 계산할지, 나중에 계산할지&amp;rdquo; 의 균형점이다.&lt;/li&gt;
&lt;li&gt;시스템 설계는 &lt;b&gt;&amp;ldquo;계산 시점&amp;rdquo;을 어디에 둘 것인가&lt;/b&gt;의 문제다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구체화 뷰와 캐싱&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스는 &amp;ldquo;쓰기 시점에 미리 정리해두는 구조&quot;이므로 읽기 속도를 빠르게 만들지만, 쓰기 오버헤드&amp;nbsp;존재&lt;/li&gt;
&lt;li&gt;현실적 절충: &lt;b&gt;Common Queries Cache&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자주 등장하는 쿼리만 &lt;b&gt;미리 계산(cache or materialized view)&lt;/b&gt; 해둠.&lt;/li&gt;
&lt;li&gt;나머지 쿼리는 기존 인덱스를 이용해 실시간 검색.&lt;/li&gt;
&lt;li&gt;이 구조를 &lt;b&gt;&amp;ldquo;common query cache&amp;rdquo;&lt;/b&gt; 혹은 &lt;b&gt;&amp;ldquo;materialized view&amp;rdquo;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;오프라인 대응 가능한 상태 저장 클라이언트&lt;br /&gt;&lt;i&gt;➔ &amp;ldquo;만약 클라이언트가 자체적으로 상태(state)를 갖는다면, 서버-클라이언트 구조는 어떻게 달라질까?&amp;rdquo;&lt;/i&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지난 20년간 웹 애플리케이션은 &lt;b&gt;&amp;ldquo;서버 중심, 클라이언트 무상태&quot;&lt;/b&gt; 모델을 기본으로 함.&lt;/li&gt;
&lt;li&gt;PA(Single Page Application)와 모바일 앱의 등장으로 패러다임이 바뀜&lt;/li&gt;
&lt;li&gt;클라이언트가 저장하는 데이터는 &lt;b&gt;서버의 상태(state)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 를 &lt;/span&gt;&lt;b&gt;부분 복제(partial replica)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 한 것.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2497&quot; data-start=&quot;2377&quot;&gt;서버 &amp;rarr; 진리의 원본(Source of Truth)&lt;/li&gt;
&lt;li data-end=&quot;2497&quot; data-start=&quot;2425&quot;&gt;클라이언트 &amp;rarr; 구체화 뷰 혹은 캐시&lt;/li&gt;
&lt;li data-end=&quot;2555&quot; data-start=&quot;2498&quot;&gt;서버의 상태 변경이 클라이언트에 반영될 때까지 &lt;b&gt;동기화 지연(sync delay)&lt;/b&gt;&amp;nbsp;존재함&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;blockquote data-ke-style=&quot;style2&quot;&gt;상태 변경을 클라이언트에게 푸시하기&lt;br /&gt;➔ &lt;i&gt;로컬 상태를 어떻게 서버 상태와 동기화할 것인가&lt;/i&gt;&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Polling 기반, Stale Cache&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변화 (Push 기반 프로토콜의 등장)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;Server-Sent Events (SSE)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 같은 기술이 등장하면서&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1423&quot; data-start=&quot;1323&quot;&gt;서버 &amp;rarr; 클라이언트 방향의 &lt;b&gt;푸시(push)&lt;/b&gt; 통신이 가능해짐.&lt;/li&gt;
&lt;li data-end=&quot;1423&quot; data-start=&quot;1323&quot;&gt;클라의 로컬 상태는 더 이상 &lt;b&gt;정적 캐시&lt;/b&gt;가 아니라, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 서버 이벤트 스트림과 &lt;b&gt;실시간으로 동기화되는 복제 상태&lt;/b&gt;가 된다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1873&quot; data-start=&quot;1780&quot;&gt;기존에는 Write Path가 &lt;b&gt;서버 내부의 파생 데이터(인덱스, 뷰 등)&lt;/b&gt; 까지만 도달
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1873&quot; data-start=&quot;1780&quot;&gt;하지만 Write Path가 &lt;b&gt;클라이언트까지 확장&lt;/b&gt;됨.&lt;/li&gt;
&lt;li data-end=&quot;1921&quot; data-start=&quot;1874&quot;&gt;즉, 서버의 변경 이벤트가 클라이언트로 &lt;b&gt;스트림 &lt;/b&gt;형태로 전송.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1921&quot; data-start=&quot;1874&quot;&gt;오프라인 상태를 고려한 동기화 전략
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2770&quot; data-start=&quot;2713&quot;&gt;클라이언트는 종종 오프라인 상태가 되므로, 서버의 이벤트를 받을 수 없는 시간 구간이 생김.&lt;/li&gt;
&lt;li data-end=&quot;2838&quot; data-start=&quot;2771&quot;&gt;로그 기반 메시징 시스템(Kafka 등)의 &lt;b&gt;offset 재연결 패턴&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;종단 간 이벤트 스트림&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;현대 데이터 시스템의 미래는 &amp;ldquo;&lt;/span&gt;&lt;b&gt;end-to-end event streams&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rdquo;이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;즉, 사용자의 입력부터 다른 사용자 화면의 변화까지 하나의 이벤트 스트림으로 연결되는 구조.&lt;/li&gt;
&lt;li&gt;이를 위해서는 &amp;ldquo;stateless request/response&amp;rdquo; 패러다임을 넘어&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;ldquo;stateful, publish/subscribe dataflow&amp;rdquo;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 로 전환해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터를 &lt;/b&gt;&lt;b&gt;질의&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하는 대신 &lt;/span&gt;&lt;b&gt;변화를 구독(subscribe)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;해야 한다.\&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;읽기도 이벤트다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;748&quot; data-start=&quot;721&quot;&gt;기존 구조: &amp;ldquo;읽기와 쓰기&amp;rdquo;의 분리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;748&quot; data-start=&quot;721&quot;&gt;지금까지의 모델에서는 다음처럼 역할이 나뉨:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;898&quot; data-start=&quot;857&quot;&gt;&lt;b&gt;Write Path:&lt;/b&gt; 이벤트 로그를 기반으로 파생 데이터 생성.&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;&lt;b&gt;Read Path:&lt;/b&gt; 저장소(DB, Cache, Index)를 쿼리하여 결과 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;즉, 읽기(Read)는 &lt;b&gt;한 번의 네트워크 요청&lt;/b&gt;, 쓰기(Write)는 &lt;b&gt;이벤트 스트림의 일부&lt;/b&gt;로만 다뤄짐.&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;그러나 저자는 읽기도 이벤트로 볼 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;새로운 관점: &amp;ldquo;읽기 요청도 이벤트다&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;즉, 읽기 요청(read query)을 하나의 &lt;b&gt;이벤트(read event)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 로 표현하고,&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;이를 &lt;b&gt;스트림 프로세서(stream processor)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 가 처리할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;새로운 데이터플로우 구조:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;[Read Request Event &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Stream&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;] &amp;rarr; [&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Stream&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; Processor] &amp;rarr; [Read Result Event &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Stream&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;]&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;사용자는 &amp;ldquo;요청&amp;rdquo; 이벤트를 발행(publish)&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;스트림 프로세서가 &amp;ldquo;결과&amp;rdquo; 이벤트를 발행(subscribe &amp;amp; process)&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;이 구조는 &lt;b&gt;요청/응답(request/response)&lt;/b&gt; 모델을 &lt;b&gt;pub/sub&lt;/b&gt;&amp;nbsp;패러다임으로 대체함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;Stream-Table Join으로서의 &amp;ldquo;읽기&amp;rdquo;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;쓰기와 읽기를 모두 이벤트로 표현하면,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;ldquo;읽기 요청 스트림&amp;rdquo;과 &amp;ldquo;데이터 스트림(또는 테이블)&amp;rdquo;을 &lt;b&gt;조인(join)&lt;/b&gt; 하는 형태로 해석할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;One-off Read vs Subscription Read
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;901&quot;&gt;&lt;b&gt;일회성 조회(one-off read):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2095&quot; data-start=&quot;2063&quot;&gt;단일 요청을 조인에 통과시켜 결과를 반환하고 종료.&lt;/li&gt;
&lt;li data-end=&quot;2119&quot; data-start=&quot;2098&quot;&gt;일반적인 &amp;ldquo;쿼리 1회 실행&amp;rdquo; 형태.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2119&quot; data-start=&quot;2098&quot;&gt;&lt;b&gt;구독형 조회(subscribe request):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2243&quot; data-start=&quot;2157&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2177&quot; data-start=&quot;2157&quot;&gt;지속적으로 조인 상태를 유지.&lt;/li&gt;
&lt;li data-end=&quot;2202&quot; data-start=&quot;2180&quot;&gt;데이터가 바뀌면 새 결과를 푸시.&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;즉, &amp;ldquo;Reactive Query / Live Query&amp;rdquo; 모델.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;읽기 이벤트를 기록(Log)할 때의 부가 가치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;읽기 이벤트를 로그에 남기면,&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 데이터 혈통(data provenance)&lt;/b&gt; 과 &lt;b&gt;인과관계(causality)&lt;/b&gt; 추적이 가능함&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예시:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;사용자 A가 상품을 봄 &amp;rarr; 재고 &amp;ldquo;있음&amp;rdquo; 상태 확인 &amp;rarr; 구매 결정&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;그 후 재고가 소진됨&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;나중에 &amp;ldquo;왜 구매 버튼을 눌렀는가?&amp;rdquo;를 분석하려면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;사용자가 &amp;ldquo;당시 어떤 정보를 봤는지&amp;rdquo; 기록이 필요함 &amp;rarr; &lt;b&gt;읽기 로그&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;읽기 이벤트를 남기면, 시스템 전체의 &lt;b&gt;원인(Why)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 을 재구성할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2205&quot;&gt;쓰기와 읽기를 모두 로그로 통합하면&amp;hellip;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2969&quot; data-start=&quot;2879&quot;&gt;읽기 이벤트도 로그에 기록하면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2969&quot; data-start=&quot;2879&quot;&gt;&lt;b&gt;장점:&lt;/b&gt; 완전한 인과 추적 가능 (cause &amp;amp; effect)&lt;/li&gt;
&lt;li data-end=&quot;2969&quot; data-start=&quot;2879&quot;&gt;&lt;b&gt;단점:&lt;/b&gt; 저장 공간, I/O 부하 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2969&quot; data-start=&quot;2879&quot;&gt;하지만 이미 쓰기 로그를 운영 중이라면, &lt;b&gt;읽기 로그도 함께 남기는 것은 자연스러운 확장&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;다중 파티션 데이터 처리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트위터의 분산 RPC
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다중&lt;span&gt; &lt;/span&gt;파티션에&lt;span&gt; &lt;/span&gt;분산된&lt;span&gt; &lt;/span&gt;데이터를&lt;span&gt; &lt;/span&gt;스트림&lt;span&gt; &lt;/span&gt;조합으로&lt;span&gt; &lt;/span&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 data-end=&quot;2392&quot; data-start=&quot;2264&quot;&gt;MPP (Massively Parallel Processing) 데이터베이스도 유사한 일을 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2392&quot; data-start=&quot;2322&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2359&quot; data-start=&quot;2322&quot;&gt;쿼리를 DAG(Directed Acyclic Graph)로 분리&lt;/li&gt;
&lt;li data-end=&quot;2376&quot; data-start=&quot;2362&quot;&gt;각 노드가 병렬로 실행&lt;/li&gt;
&lt;li data-end=&quot;2392&quot; data-start=&quot;2379&quot;&gt;마지막에 결과를 결합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2479&quot; data-start=&quot;2393&quot;&gt;스트림 프로세서는 이 구조를 이미 &lt;b&gt;기본적으로 내장&lt;/b&gt;하고 있음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2479&quot; data-start=&quot;2435&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2479&quot; data-start=&quot;2435&quot;&gt;따라서 동일한 처리를 &lt;b&gt;실시간으로(streaming)&lt;/b&gt; 수행할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2479&quot; data-start=&quot;2435&quot;&gt;실용적 제안
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2849&quot; data-start=&quot;2806&quot;&gt;만약 단순히 일회성 쿼리라면, MPP DB를 쓰는 게 더 간단할 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;2908&quot; data-start=&quot;2850&quot;&gt;하지만 &lt;b&gt;지속적 스트림 기반 처리&lt;/b&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;&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;&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;❐ 3. 정확성을 목표로&lt;/h3&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;size18&quot;&gt;  3-1.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;250&quot; data-start=&quot;216&quot; data-ke-style=&quot;style2&quot;&gt;데이터 시스템만 믿어서는 &amp;ldquo;완전하게 안전&amp;rdquo;하지 않다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;251&quot;&gt;트랜잭션 격리 수준이 높고, 직렬화 가능한 DB를 사용한다고 해도 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 애플리케이션 레벨의 버그로 인해 데이터 손실/손상은 발생할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;251&quot;&gt;예
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;251&quot;&gt;애플리케이션 버그로 잘못된 값을 UPDATE&lt;/li&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;251&quot;&gt;삭제하면 안 되는 데이터를 DELETE&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; DB의 직렬화 트랜잭션이 이런 문제를 해결해주지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;251&quot;&gt;&lt;b&gt;데이터 안전은 DB가 아니라 애플리케이션도 함께 책임져야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;502&quot; data-start=&quot;476&quot; data-ke-style=&quot;style2&quot;&gt;Exactly-once 처리의 어려움&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;534&quot; data-start=&quot;503&quot;&gt;메시지 처리 중 오류가 생기면 보통 2가지 선택이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;534&quot; data-start=&quot;503&quot;&gt;&lt;b&gt;포기한다&lt;/b&gt; &amp;rarr; 데이터 유실&lt;/li&gt;
&lt;li data-end=&quot;534&quot; data-start=&quot;503&quot;&gt;&lt;b&gt;재시도한다&lt;/b&gt; &amp;rarr; 실제로는 성공했는데 응답이 끊겨서 재시도하면 &lt;b&gt;중복 처리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;534&quot; data-start=&quot;503&quot;&gt;이 중 &amp;ldquo;재시도했을 때도 결과가 한 번만 처리되는 것&amp;rdquo;이 바로 &lt;b&gt;Exactly-once semantics&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;534&quot; data-start=&quot;503&quot;&gt;하지만 현실에서는 정말 구현하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;735&quot; data-start=&quot;703&quot; data-ke-style=&quot;style2&quot;&gt;Idempotence(멱등성)으로 해결하는 방법&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;736&quot;&gt;가장 효과적인 방식은 작업 자체를 멱등(idempotent) 하게 만드는 것.&lt;/li&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;736&quot;&gt;멱등성을 지원하려면 다음 같은 추가 메타데이터가 필요할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;736&quot;&gt;요청 ID&lt;/li&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;736&quot;&gt;실행된 작업 ID 로그&lt;/li&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;736&quot;&gt;fencing token(노드 장애 시 중복 실행 방지) 등&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;blockquote data-end=&quot;1068&quot; data-start=&quot;1026&quot; data-ke-style=&quot;style2&quot;&gt;Duplicate suppression(중복 억제)가 필요한 이유&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1126&quot; data-start=&quot;1069&quot;&gt;TCP 같은 네트워크 계층도 중복을 억제해주지만, 이는 단일 TCP 연결 안에서만 의미가 있다.&lt;/li&gt;
&lt;li data-end=&quot;1126&quot; data-start=&quot;1069&quot;&gt;문제는:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1126&quot; data-start=&quot;1069&quot;&gt;요청이 DB에 전달되고 COMMIT 했는데&lt;/li&gt;
&lt;li data-end=&quot;1126&quot; data-start=&quot;1069&quot;&gt;클라이언트가 응답을 못 받고 타임아웃 &amp;rarr; 다시 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1126&quot; data-start=&quot;1069&quot;&gt;이 경우 DB 트랜잭션 레벨에서 duplicate suppression이 되지 않으면 &lt;b&gt;중복 처리 위험&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1590&quot; data-start=&quot;1561&quot; data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;고급 트랜잭션 프로토콜&amp;rdquo;도 완벽하지 않다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1671&quot; data-start=&quot;1591&quot;&gt;2PC 같은 프로토콜은 TCP 연결과 트랜잭션을 분리해주지만, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 여전히 애플리케이션이 중복 요청을 보낼 때까지 막지는 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;애플리케이션 레벨에서 Operation ID 추가하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1714&quot;&gt;중복 요청을 확실히 막으려면 애플리케이션에서 end-to-end로 중복을 억제해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1714&quot;&gt;핵심 아이디어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1714&quot;&gt;클라이언트가 &lt;b&gt;request_id(UUID)&lt;/b&gt; 를 생성&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1714&quot;&gt;서버로 POST할 때 함께 보냄&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1714&quot;&gt;DB에서 request_id를 PK/UNIQUE로 사용하여 이미 처리된 요청이면 INSERT가 실패 &amp;rarr; 중복 방지&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;blockquote data-ke-style=&quot;style2&quot;&gt;End-to-End Argument란 무엇인가?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;Saltzer, Reed, Clark이 정의한 개념&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;어떤 기능이 정말로 필요하다면, &lt;b&gt;시스템의 끝단(end-to-end)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 에서 보장해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;낮은 레벨(TCP, 네트워크, 등)의 보장만으로는 부족하다.&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;Duplicate suppression
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;TCP는 패킷 중복을 해결해줘도 &lt;b&gt;클라이언트 &amp;rarr; 서버 &amp;rarr; DB&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 전체에 걸친 중복은 막지 못함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;그러므로 duplicate suppression은 &lt;b&gt;DB까지 end-to-end로 관통해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;데이터 무결성 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;TCP/Ethernet 체크섬은 전송 중 오류만 잡아낸다&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;하지만 서버 버그, 디스크 손상은 감지 못함&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 결국 end-to-end 체크가 필요 (예: 애플리케이션 레벨 체크섬)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;암호화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;WiFi 암호화는 집 안 공격자만 막음&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2376&quot;&gt;서버 공격자는 못 막음 &amp;rarr; TLS처럼 end-to-end 암호화가 필요&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;결론 &amp;mdash; 데이터 안전은 결국 애플리케이션 책임도 크다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3087&quot; data-start=&quot;2910&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2978&quot; data-start=&quot;2910&quot;&gt;DB가 강력한 트랜잭션을 제공해도 &lt;b&gt;중복 억제, 멱등성, end-to-end 검증&lt;/b&gt; 없이는 데이터 손상 가능&lt;/li&gt;
&lt;li data-end=&quot;3038&quot; data-start=&quot;2979&quot;&gt;트랜잭션은 많은 문제를 &amp;ldquo;commit or abort&amp;rdquo;로 추상화하지만 현실의 장애는 더 복잡하다&lt;/li&gt;
&lt;li data-end=&quot;3087&quot; data-start=&quot;3039&quot;&gt;대규모 분산 환경에서는 &lt;b&gt;애플리케이션 수준의 end-to-end 안전성&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-2. 제약 조건 강제하기&lt;/p&gt;
&lt;blockquote data-end=&quot;479&quot; data-start=&quot;440&quot; data-ke-style=&quot;style2&quot;&gt;유니크 제약(uniqueness constraint)의 어려움&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;481&quot;&gt;유저네임, 이메일, 계좌 ID처럼 &amp;ldquo;하나만 있어야 하는 값&amp;rdquo;을 보장하는 것은 쉬워 보이지만,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 분산된 환경에서는 매우 어렵다.&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;481&quot;&gt;예를 들어:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;481&quot;&gt;두 개의 노드가 동시에 같은 이메일로 회원가입&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;481&quot;&gt;두 데이터센터에서 같은 좌석을 동시에 예약&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 모두 &amp;ldquo;유일해야 하는 값&amp;rdquo;이기 때문에 충돌을 해결해야 한다.&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;blockquote data-end=&quot;709&quot; data-start=&quot;660&quot; data-ke-style=&quot;style2&quot;&gt;왜 어려울까? &amp;rarr; 유니크 보장은 합의(consensus)를 필요로 하기 때문&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;여러 노드가 동시에 같은 값을 삽입하려고 할 때,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 시스템은 어느 요청이 승자이며, 나머지는 거절해야 하는지 결정해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;이는 결국 &lt;b&gt;합의(consensus)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 문제다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;가장 간단한 방식:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;&lt;b&gt;리더(leader)를 하나 두고&lt;/b&gt;, 모든 유니크 판단을 리더가 하게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;문제점:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;리더 노드 장애 &amp;rarr; 다시 합의 필요&lt;/li&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;리더 한 개가 처리량 병목이 됨&lt;/li&gt;
&lt;li data-end=&quot;786&quot; data-start=&quot;710&quot;&gt;클라이언트가 지구 반대편 &amp;rarr; 높은 지연(latency)&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;blockquote data-end=&quot;986&quot; data-start=&quot;967&quot; data-ke-style=&quot;style2&quot;&gt;파티셔닝 기반 유니크 보장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;유니크 조건을 값 기반으로 파티션하면 확장성이 높아진다.\&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;예
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;요청 ID를 key로 파티션 &amp;rarr; 같은 request_id는 항상 같은 파티션으로 감&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;username을 hash(username) 기준으로 파티션 &amp;rarr; 동일 username 충돌은 같은 파티션에서만 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;즉, &lt;b&gt;동일 값을 유니크하게 만들려면 같은 파티션에서 처리되도록 라우팅&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하면 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;이 방식의 장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;파티션 별로 독립적 처리 가능 &amp;rarr; 확장성 높음&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;각 파티션에서 순서가 보장되면(total order), 충돌 해결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;하지만 단점도 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;&lt;b&gt;비동기 멀티마스터 복제&lt;/b&gt;에서는 불가능(서로 다른 노드가 서로 모르게 같은 값을 허용할 수 있기 때문)\&lt;/li&gt;
&lt;li data-end=&quot;1023&quot; data-start=&quot;988&quot;&gt;유니크를 강하게 보장하려면 &lt;b&gt;동기적 합의 또는 파티션 기반의 순차 처리&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 필요함.&lt;/span&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;blockquote data-end=&quot;1447&quot; data-start=&quot;1422&quot; data-ke-style=&quot;style2&quot;&gt;로그 기반 메시징에서 유니크 보장하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1449&quot;&gt;Kafka처럼 &lt;b&gt;로그 기반 메시징 시스템&lt;/b&gt;을 사용하면 유니크 제약을 비교적 쉽게 강하게 보장할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1449&quot;&gt;핵심 이유:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1449&quot;&gt;로그는 &lt;b&gt;모든 메시지를 순서대로(total order)&lt;/b&gt; 기록&lt;/li&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1449&quot;&gt;파티션 단위로는 한 스레드가 순서대로 처리&lt;/li&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1449&quot;&gt;따라서 충돌 상황에서도 &amp;ldquo;누가 먼저 왔는지&amp;rdquo; 명확히 판단 가능&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;blockquote data-end=&quot;2051&quot; data-start=&quot;1996&quot; data-ke-style=&quot;style2&quot;&gt;Multi-partition Request Processing (여러 파티션이 관여하면?)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2101&quot; data-start=&quot;2053&quot;&gt;문제는 여기서부터다. 금전 이체 같은 작업은 여러 파티션을 동시에 건드린다.&lt;/li&gt;
&lt;li data-end=&quot;2101&quot; data-start=&quot;2053&quot;&gt;전통적인 DB 방식:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2101&quot; data-start=&quot;2053&quot;&gt;3개 파티션이 모두 참여 &amp;rarr; &lt;b&gt;분산 트랜잭션(2PC)&lt;/b&gt; 필요&lt;/li&gt;
&lt;li data-end=&quot;2101&quot; data-start=&quot;2053&quot;&gt;처리량 저하 + 높은 지연 &amp;rarr; 문제가 많음&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;blockquote data-end=&quot;2280&quot; data-start=&quot;2246&quot; data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;분산 트랜잭션 없이도 &amp;ldquo;동일한 정확성&amp;rdquo;을 달성하는 법&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;2단계 파이프라인 분리 + 로그 기반 처리 + 멱등성 조합이다.&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;단계&lt;span&gt; 1 &amp;mdash; &lt;/span&gt;요청&lt;span&gt; ID&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;기반으로&lt;span&gt; &lt;/span&gt;단일&lt;span&gt; &lt;/span&gt;메시지로&lt;span&gt; &lt;/span&gt;로깅
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;클라이언트가 송금 요청을 하나의 메시지로 보내고&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;로그에 append (request_id 파티션)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;단계 2 &amp;mdash; stream processor가 이 메시지를 읽고, 두 개의 별도 명령을 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;A 계좌 파티션: &amp;ldquo;A 계좌에서 10 빼세요&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;B 계좌 파티션: &amp;ldquo;B 계좌에 10 더하세요&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;단계 3 &amp;mdash; 두 계좌 파티션 소비자가 각각 명령 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;request_id를 기준으로 &lt;b&gt;중복 제거&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;명령이 여러 번 와도 멱등 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;결과:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;A에서 빠지고 B에서 더하는 작업 모두 &lt;b&gt;exactly once&lt;/b&gt; 적용됨&lt;/li&gt;
&lt;li data-end=&quot;2342&quot; data-start=&quot;2282&quot;&gt;단 &lt;b&gt;분산 트랜잭션 없이도 동일한 정확성 확보&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;장애 상황에서도 안전한 이유&lt;br /&gt;&lt;i&gt;➔ 만약 단계 2의 프로세서가 크래시 한다면?&lt;/i&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3000&quot; data-start=&quot;2907&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2922&quot; data-start=&quot;2907&quot;&gt;체크포인트에서 재시작&lt;/li&gt;
&lt;li data-end=&quot;2960&quot; data-start=&quot;2923&quot;&gt;로그에서 다시 읽고 같은 debit/credit 메시지 생성&lt;/li&gt;
&lt;li data-end=&quot;3000&quot; data-start=&quot;2961&quot;&gt;하지만 단계 3에서 request_id로 dedupe &amp;rarr; 중복 방지&lt;/li&gt;
&lt;li data-end=&quot;3000&quot; data-start=&quot;2961&quot;&gt;즉,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3000&quot; data-start=&quot;2961&quot;&gt;로그는 append-only&lt;/li&gt;
&lt;li data-end=&quot;3000&quot; data-start=&quot;2961&quot;&gt;처리 과정은 deterministic&lt;/li&gt;
&lt;li data-end=&quot;3000&quot; data-start=&quot;2961&quot;&gt;결과는 멱등 &amp;rarr; &amp;ldquo;한 번만 처리된 것처럼&amp;rdquo; 보장 가능&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;  3-3. 적시성과 무결성&lt;/p&gt;
&lt;blockquote data-end=&quot;667&quot; data-start=&quot;617&quot; data-ke-style=&quot;style2&quot;&gt;Transactions는 원래 &amp;ldquo;Timely&amp;rdquo;하다 (즉, Linearizable)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;688&quot; data-start=&quot;668&quot;&gt;ACID 트랜잭션은 다음을 보장한다.&lt;/li&gt;
&lt;li data-end=&quot;727&quot; data-start=&quot;690&quot;&gt;&lt;b&gt;Commit이 끝난 직후, 모든 읽기는 그 결과를 본다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;727&quot; data-start=&quot;690&quot;&gt;즉, 쓰기(Write) &amp;rarr; Commit &amp;rarr; Read 가 즉시 반영된다.&lt;/li&gt;
&lt;li data-end=&quot;727&quot; data-start=&quot;690&quot;&gt;이게 바로 선형성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;836&quot; data-start=&quot;806&quot; data-ke-style=&quot;style2&quot;&gt;하지만 Stream Processing에서는 이게 깨진다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;867&quot; data-start=&quot;837&quot;&gt;Kafka처럼 로그 기반 비동기 파이프라인에서:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;891&quot; data-start=&quot;869&quot;&gt;클라이언트는 메시지를 전송만 하고&lt;/li&gt;
&lt;li data-end=&quot;923&quot; data-start=&quot;892&quot;&gt;실제 처리는 비동기 파이프라인에서 나중에 일어난다&lt;/li&gt;
&lt;li data-end=&quot;959&quot; data-start=&quot;924&quot;&gt;그래서 &amp;ldquo;commit하자마자 읽으면 반영된다&amp;rdquo;는 보장이 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;959&quot; data-start=&quot;924&quot;&gt;즉, &lt;b&gt;타임라인 상 지연이 생기는 게 기본&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1044&quot; data-start=&quot;997&quot; data-ke-style=&quot;style2&quot;&gt;Timeliness = &amp;ldquo;업데이트된 최신 상태를 얼마나 빨리 볼 수 있나?&amp;rdquo;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1123&quot; data-start=&quot;1050&quot;&gt;복제지연(replication lag) 때문에&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 내가 조금 전 업데이트한 데이터를 다른 노드에서는 잠시동안 못볼 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;1124&quot;&gt;하지만 몇 초 안에 eventually 반영됨.&lt;/li&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;1124&quot;&gt;Timeliness는 결국 &amp;ldquo;언젠가는 최신 상태가 되게 하는 속성&amp;rdquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1244&quot; data-start=&quot;1194&quot; data-ke-style=&quot;style2&quot;&gt;CAP의 &amp;ldquo;Consistency&amp;rdquo;도 사실 Timeliness 의미에 가깝다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1245&quot;&gt;Linearizability를 제공하는 시스템 = &amp;ldquo;강한 최신성 보장 시스템&amp;rdquo;.&lt;/li&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1245&quot;&gt;그리고 더 약한 형태도 있다:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1245&quot;&gt;&lt;b&gt;Read-after-write consistency&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1245&quot;&gt;&lt;b&gt;Monotonic reads&lt;/b&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;blockquote data-end=&quot;1408&quot; data-start=&quot;1375&quot; data-ke-style=&quot;style2&quot;&gt;Integrity = &amp;ldquo;데이터 자체가 틀리지 않음&amp;rdquo;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&gt;Integrity는 timeliness보다 훨씬 더 중요하며 핵심적이다.&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&gt;Integrity는 이런 것을 의미한다:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&gt;데이터가 손상되지 않았다 (no corruption)&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&gt;데이터가 사라지지 않았다 (no data loss)&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&gt;모순된 상태가 없다 (no contradictory state)&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1410&quot;&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;blockquote data-end=&quot;1708&quot; data-start=&quot;1679&quot; data-ke-style=&quot;style2&quot;&gt;Integrity가 깨지면 &amp;ldquo;복구가 불가능하다&amp;rdquo;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1738&quot; data-start=&quot;1709&quot;&gt;Timeliness 문제는 시간이 지나면 해결되지만,&lt;/li&gt;
&lt;li data-end=&quot;1738&quot; data-start=&quot;1709&quot;&gt;Integrity 문제는:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1738&quot; data-start=&quot;1709&quot;&gt;기다린다고 해결되지 않는다&lt;/li&gt;
&lt;li data-end=&quot;1738&quot; data-start=&quot;1709&quot;&gt;복구 작업이 필요하다 (manual repair)&lt;/li&gt;
&lt;li data-end=&quot;1738&quot; data-start=&quot;1709&quot;&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;blockquote data-end=&quot;1969&quot; data-start=&quot;1917&quot; data-ke-style=&quot;style2&quot;&gt;4. Event-driven 시스템은 Timeliness와 Integrity를 &amp;ldquo;분리&amp;rdquo;한다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;ACID 세계에서는 동시에 제공되지만 이벤트 기반 스트림 시스템에서는 재밌는 특징이 있다:&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;&lt;b&gt;Integrity를 보장하면서, Timeliness를 포기할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;즉,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;파이프라인은 비동기일 수 있고&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;최신 데이터가 바로 반영되지 않아도&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;Integrity만 보장되면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;Integrity를 지키는 핵심 기법들
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;Exactly-once / Effectively-once 처리&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;멱등(idempotent) 연산&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;중복 제거(deduplication)&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;요청 ID(request_id)를 end-to-end로 전달&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;불변 로그(immutable log)에 기반한 재처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1971&quot;&gt;이 조합 덕분에 스트림 시스템은 오류가 나도 정합성을 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;2426&quot; data-start=&quot;2393&quot; data-ke-style=&quot;style2&quot;&gt;5. 스트림 시스템에서 Integrity를 보장하는 패턴&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2516&quot; data-start=&quot;2474&quot;&gt;모든 write를 단일 메시지로 표현 &amp;rarr; 원자적으로 기록&lt;/li&gt;
&lt;li data-end=&quot;2516&quot; data-start=&quot;2474&quot;&gt;모든 파생 상태는 deterministic function으로 유도&lt;/li&gt;
&lt;li data-end=&quot;2516&quot; data-start=&quot;2474&quot;&gt;request_id를 end-to-end로 전달 &amp;rarr; 중복 억제&lt;/li&gt;
&lt;li data-end=&quot;2516&quot; data-start=&quot;2474&quot;&gt;모든 메시지를 immutable하게 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;2855&quot; data-start=&quot;2807&quot; data-ke-style=&quot;style2&quot;&gt;6. Loosely interpreted constraints (느슨한 제약 조건)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;현실에서는 &amp;ldquo;유니크 제약&amp;rdquo;이 흔히 느슨하게 해도 된다&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;엄격하게 리니어라이저블하게 처리할 필요가 없는 상황 예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;동시에 두 사람이 같은 좌석을 예약 &amp;rarr; 한쪽에게 사과하고 다른 좌석 제안&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;재고 5개인데 6개 주문됨 &amp;rarr; 사과하고 환불/지연 안내&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;호텔 오버부킹(일반적 패턴) &amp;rarr; 보상 제공&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;계정 overdraft &amp;rarr; 나중에 수수료 부과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;즉,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;완벽한 정확성은 비싸다&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;실제 비즈니스는 사과/보상 프로세스로 수습 가능하다&lt;/li&gt;
&lt;li data-end=&quot;2919&quot; data-start=&quot;2886&quot;&gt;Integrity는 유지하되 Timeliness는 굳이 엄격할 필요가 없다&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;blockquote data-end=&quot;3243&quot; data-start=&quot;3190&quot; data-ke-style=&quot;style2&quot;&gt;7. Coordination-avoiding Data Systems (조율을 피하는 시스템)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;두 가지 관찰:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;스트림 시스템은 &lt;b&gt;분산 트랜잭션 없이도 integrity 보장&lt;/b&gt; 가능&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;많은 제약 조건은 &lt;b&gt;임시 위반 가능&lt;/b&gt;(나중에 사과해서 해결할 수 있다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;즉, &lt;b&gt;동기적 조율(synchronous coordination)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 없이도 &lt;/span&gt;정확성을 확보할 수 있는 시스템을 만들 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;협업이 필요 없는 대신:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;로그 기반 비동기 처리&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;request_id&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;멱등 처리&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;재생 가능한 파이프라인&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;느슨한 timeliness&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;이런 시스템의 장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;더 높은 가용성&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;더 높은 성능&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;지연이 적음&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;장애 복구에 강함&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;조율 비용이 거의 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3279&quot; data-start=&quot;3267&quot;&gt;AWS Dynamo, Cassandra, Kafka 기반 시스템 등이 이 철학을 따른다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-3. 믿어라 하지만 확인하라.&lt;/p&gt;
&lt;blockquote data-end=&quot;3058&quot; data-start=&quot;3033&quot; data-ke-style=&quot;style2&quot;&gt;시스템은 항상 &amp;ldquo;잘못될 수 있다&amp;rdquo;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3171&quot; data-start=&quot;3059&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;이런 일들은 드물지만 &amp;ldquo;언젠가는 반드시&amp;rdquo; 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;하드웨어 비트 플립&lt;/li&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;디스크 silent corruption&lt;/li&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;네트워크 데이터 손상&lt;/li&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;DB 버그&lt;/li&gt;
&lt;li data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;애플리케이션 버그&lt;br /&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3171&quot; data-start=&quot;3059&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot; data-end=&quot;3073&quot; data-start=&quot;3059&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;3209&quot; data-start=&quot;3178&quot; data-ke-style=&quot;style2&quot;&gt;가장 위험한 태도 = 기술을 맹신하는 것&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3282&quot; data-start=&quot;3210&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3258&quot; data-start=&quot;3232&quot;&gt;&amp;ldquo;트랜잭션이니까 데이터는 틀리지 않겠지&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;3258&quot; data-start=&quot;3232&quot;&gt;&amp;ldquo;DB가 알아서 무결성 지켜주겠지&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;3356&quot; data-start=&quot;3318&quot; data-ke-style=&quot;style2&quot;&gt;해결책 = Auditing &amp;amp; Verification&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3486&quot; data-start=&quot;3357&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3380&quot; data-start=&quot;3357&quot;&gt;데이터가 손상됐는지 정기적으로 확인&lt;/li&gt;
&lt;li data-end=&quot;3394&quot; data-start=&quot;3381&quot;&gt;백업 복원 테스트&lt;/li&gt;
&lt;li data-end=&quot;3411&quot; data-start=&quot;3395&quot;&gt;해시 기반 데이터 검증&lt;/li&gt;
&lt;li data-end=&quot;3429&quot; data-start=&quot;3412&quot;&gt;이벤트 로그 기반 재처리&lt;/li&gt;
&lt;li data-end=&quot;3447&quot; data-start=&quot;3430&quot;&gt;end-to-end 검증&lt;/li&gt;
&lt;li data-end=&quot;3486&quot; data-start=&quot;3448&quot;&gt;self-validating/self-auditing 시스템 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;3544&quot; data-start=&quot;3493&quot; data-ke-style=&quot;style2&quot;&gt;Event sourcing과 로그 기반 시스템은 Auditing에 매우 적합&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3645&quot; data-start=&quot;3545&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3558&quot; data-start=&quot;3545&quot;&gt;event는 불변&lt;/li&gt;
&lt;li data-end=&quot;3595&quot; data-start=&quot;3559&quot;&gt;deterministic derivation &amp;rarr; 재현 가능&lt;/li&gt;
&lt;li data-end=&quot;3625&quot; data-start=&quot;3596&quot;&gt;provenance(데이터의 기원) 추적 용이&lt;/li&gt;
&lt;li data-end=&quot;3645&quot; data-start=&quot;3626&quot;&gt;hash 기반 무결성 검증 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;3702&quot; data-start=&quot;3652&quot; data-ke-style=&quot;style2&quot;&gt;앞으로의 데이터 시스템은 &amp;ldquo;Trust, but verify&amp;rdquo; 철학을 따른다&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3780&quot; data-start=&quot;3703&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3733&quot; data-start=&quot;3703&quot;&gt;암호학적 무결성 검증(Merkle Tree 등)&lt;/li&gt;
&lt;li data-end=&quot;3758&quot; data-start=&quot;3734&quot;&gt;자체 감사 기능(self-audit)&lt;/li&gt;
&lt;li data-end=&quot;3780&quot; data-start=&quot;3759&quot;&gt;지속적 end-to-end 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/233</guid>
      <comments>https://gilbert9172.tistory.com/233#entry233comment</comments>
      <pubDate>Mon, 10 Nov 2025 19:46:35 +0900</pubDate>
    </item>
    <item>
      <title>11. 스트림 처리</title>
      <link>https://gilbert9172.tistory.com/231</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 0. 개요&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;현실 세계에서 데이터는 무한하고(unbounded) 시간이 지나면서 계속(gradually) 유입된다.&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;/li&gt;&lt;li&gt;일반적으로 &quot;스트림&quot;은 시간에 흐름에 따라 점진적으로 생산된 데이터를 일컫는다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. 이벤트 스트림 전송&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Polling 방식의 한계&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;생산자는 자신이 생성한 모든 이벤트를 저장소에 기록&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;소비자는 주기적으로 저장소를 polling하여 마지막 실행 이후 새로 생긴 이벤트를 확인&lt;/li&gt; 
   &lt;li&gt;이런 방식은 하루치 데이터를 하루가 끝날 때 처리하는 &lt;b&gt;배치 처리&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;와 유사&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 polling을 자주 할수록&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;새 이벤트를 실제로 얻는 요청 비율은 낮아짐.&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 시스템 오버헤드가 커지게 됨. (불필요한 요청을 많이하기 때문)&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 &lt;b&gt;새 이벤트가 발생했을 때 소비자에게 알림이 전달되는(push) 방식&lt;/b&gt;이 더 효율적&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;데이터베이스에도 trigger 기능이 있긴 함.&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;근데 트리거는 기능이 제한적이고 데이터베이스를 설계한 이후에 도입된 개념&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  1-1. 메시징 시스템&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메시징 시스템&amp;nbsp;&lt;/blockquote&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;을 사용하는 방식&amp;nbsp;&lt;/li&gt;&lt;li&gt;메시징 시스템에서는 다수의 생산자 노드가 동일한 토픽으로 메시지를 전송할 수 있고,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;소비자 노드가 토픽 하나에서 메시지를 받아 갈 수 있음.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시스템을 구별하는데 도움이 되는 2가지 질문&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;소비자가 메시지를 처리하는 속도보다, 생산자가 메시지를 전송하는 속도가 더 빠르면? 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;메시지 버리기 / 버퍼링 / 배압&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;노드가 죽거나 일시적으로 오프라인이 된다면 손실되는 메시지가 있을까?&amp;nbsp;&lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;생산자에서 소비자로 메시지를 직접 전달하기 - Direct messaging&amp;nbsp;&lt;/blockquote&gt;&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;한다. 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;UDP 멀티-캐스트&lt;/li&gt; 
   &lt;li&gt;ZeroMQ&lt;/li&gt; 
   &lt;li&gt;...&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;본래의 설계대로 동작하면 잘 동작함 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;b&gt;생산자와 소비자가 항상 온라인 상태라고 가정함.&lt;/b&gt;&lt;/li&gt; 
   &lt;li&gt;TCP, UDP, WebSocket 같은 시스템은 이런 상황에서 문제 없이 동작&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;하지만 혀용 가능한 범위가 상당히 제한적이다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;“네트워크 일시 장애” 정도는 커버하지만&lt;/li&gt; 
   &lt;li&gt;생산자 또는 소비자가&lt;b&gt; 오프라인이 되는 경우&lt;/b&gt;는 처리하지 못한다&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;네트워크 상에서 재전송을 지원하더라고, 애플리케이션 레벨에서는 모를 수 있음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;메시지 브로커 (메시지 큐)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;Direct messaging의 대안으로 널리 사용되는 방법&lt;/li&gt; 
 &lt;li&gt;근본적으로 메시지 스트림을 처리하는데 최적화된 데이터베이스의 일종&lt;/li&gt; 
 &lt;li&gt;메시지 브로커는 서버로 구동되고 생산자와 소비자는 서버의 클라이언트로 접속함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;생산자 ➔ 브로커 ➔ 소비자&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;이 방식에서는 브로커에 데이터가 모이기 때문에 소비자 또는 생산자 노드가 오프라인이여도 쉽게 대처 가능 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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; 
 &lt;li&gt;대기 큐를 사용하면 소비자는 일반적으로 비동기로 동작함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메시지 브로커와 데이터베이스 비교&lt;/blockquote&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 166px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 16.6279%; height: 18px; text-align: center;&quot;&gt;구분&lt;/td&gt;&lt;td style=&quot;width: 32.7907%; height: 18px; text-align: center;&quot;&gt;데이터베이스&lt;/td&gt;&lt;td style=&quot;width: 29.4186%; height: 18px; text-align: center;&quot;&gt;메시지 브로커&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 37px;&quot;&gt;&lt;td style=&quot;width: 16.6279%; height: 37px;&quot;&gt;&lt;b&gt;삭제&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 32.7907%; height: 37px;&quot;&gt;데이터를 &lt;b&gt;명시적 삭제&lt;/b&gt;해야 함&lt;/td&gt;&lt;td style=&quot;width: 29.4186%; height: 37px;&quot;&gt;메시지는 &lt;b&gt;소비자에게 전달되면 자동 삭제&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 37px;&quot;&gt;&lt;td style=&quot;width: 16.6279%; height: 37px;&quot;&gt;&lt;b&gt;큐 크기&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 32.7907%; height: 37px;&quot;&gt;저장 공간이 커도 문제 없음&lt;/td&gt;&lt;td style=&quot;width: 29.4186%; height: 37px;&quot;&gt;작업 집합이 작다고 가정 (작은 큐 크기)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 37px;&quot;&gt;&lt;td style=&quot;width: 16.6279%; height: 37px;&quot;&gt;&lt;b&gt;데이터 선택/조회 방식&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 32.7907%; height: 37px;&quot;&gt;SQL, 인덱스, 조건 검색 등 쿼리 중심&lt;/td&gt;&lt;td style=&quot;width: 29.4186%; height: 37px;&quot;&gt;Topic 기반 구독, 패턴 매칭 제공&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 37px;&quot;&gt;&lt;td style=&quot;width: 16.6279%; height: 37px;&quot;&gt;&lt;b&gt;질의와 변경 감지&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 32.7907%; height: 37px;&quot;&gt;- 쿼리 결과는 &lt;b&gt;특정 시점(snapshot)&lt;/b&gt; 기준&lt;br&gt;- 이후 변경은 자동 반영되지 않음 (polling 필요)&lt;br&gt;- 정적 질의 위주&lt;/td&gt;&lt;td style=&quot;width: 29.4186%; height: 37px;&quot;&gt;-&lt;b&gt; 데이터 변경 시 자동 알림(push)&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;- 새 메시지가 생기면 구독자에게 전달&lt;br&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;blockquote data-ke-style=&quot;style2&quot;&gt;복수 소비자&lt;br&gt;&lt;i&gt;➔ 같은 토피에서 메시지를 읽을 때 사용하는 주요 패턴&lt;/i&gt;&lt;/blockquote&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dakZfS/dJMcaaQ7dOS/QIR1W1zkvDlFWh6tF0d4u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dakZfS/dJMcaaQ7dOS/QIR1W1zkvDlFWh6tF0d4u1/img.png&quot; data-alt=&quot;로드 벨런싱&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dakZfS/dJMcaaQ7dOS/QIR1W1zkvDlFWh6tF0d4u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdakZfS%2FdJMcaaQ7dOS%2FQIR1W1zkvDlFWh6tF0d4u1%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;307&quot; height=&quot;264&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로드 벨런싱&lt;/figcaption&gt;
&lt;/figure&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&gt;각 메시지는 소비자 중 하나로 전달.&lt;/li&gt;&lt;li&gt;따라서 소비자들은 해당 메시지를 처리하는 작업을 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 공유한다.&lt;/li&gt;&lt;li&gt;브로커가 메시지를 전달할 소비자를 임의로 지정한다.&lt;/li&gt;&lt;li&gt;메시지 처리 비용이 비싸서, 처리를 병렬화 하기 위해 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 소비자를 추가 하고싶을 때 유용함.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J6wZC/dJMcacBoCOG/pbmhsHKU9vr5aIvjZCoiyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J6wZC/dJMcacBoCOG/pbmhsHKU9vr5aIvjZCoiyK/img.png&quot; data-alt=&quot;팬 아웃&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J6wZC/dJMcacBoCOG/pbmhsHKU9vr5aIvjZCoiyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ6wZC%2FdJMcacBoCOG%2FpbmhsHKU9vr5aIvjZCoiyK%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;306&quot; height=&quot;270&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팬 아웃&lt;/figcaption&gt;
&lt;/figure&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&gt;각 메시지는 모든 소비자에게 전달된다.&lt;/li&gt;&lt;li&gt;여러 독립적인 소비자가 브로드캐스팅된 동일한 메시지를 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 간섭 없이 청취(tune-in)할 수 있다.&lt;/li&gt;&lt;li&gt;이것은 같은 입력 파일을 읽어 여러 다른 일괄 처리 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 작업에서 사용하는 것과 동일&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&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&gt;위 두가지 패턴은 함께 사용이 가능함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;소비가 그룹 A,B가 TopicA를 구독&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;확인 응답과 재전송&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;메시지 브로커는 메시지를 잃어버리지 않기 위해서 &lt;b&gt;확인 응답(acknowledgments)&lt;/b&gt;을 사용한다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;/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;b&gt;원자적 커밋 프로토콜&lt;/b&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로드밸런싱과 결합하면 생기는 문제&lt;/blockquote&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sy9yj/dJMcacBoCZ1/bvHisACQrzZRWRAppgRiWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sy9yj/dJMcacBoCZ1/bvHisACQrzZRWRAppgRiWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sy9yj/dJMcacBoCZ1/bvHisACQrzZRWRAppgRiWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsy9yj%2FdJMcacBoCZ1%2FbvHisACQrzZRWRAppgRiWK%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;1080&quot; height=&quot;614&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;에 영향을 미친다.&lt;/li&gt;&lt;li&gt;이건 필연적으로 발생하는 문제 (로드벨런싱을 사용하지 않으면 문제를 피할 수 있음.)&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt; 1-2. 파티셔닝된 로그&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개요&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;AMQP/JMS 형식의 메시징 처리는 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&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;데이터베이스의 지속성 있는 저장 방법과 메시징 시스템의 지연시간이 짧은 알림 기능을 조합할 수는 없을까?&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 로그 기반 메시지 브로커(log-based message broker)&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로그를 사용한 메시지 저장소&lt;/blockquote&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsai0y/dJMcadNP9CK/pSlOPGnFx9kxiqvrnKTsP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsai0y/dJMcadNP9CK/pSlOPGnFx9kxiqvrnKTsP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsai0y/dJMcadNP9CK/pSlOPGnFx9kxiqvrnKTsP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsai0y%2FdJMcadNP9CK%2FpSlOPGnFx9kxiqvrnKTsP1%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;1418&quot; height=&quot;660&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;소비자가 로그 끝에 도달하면 새 메시지가 추가됐다는 알림을 기다림.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;디스크 하나를 쓸 때보다 처리량을 높이기 위햇 확장하는 방법으로 로그를 파티셔닝 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이렇게 하면 각 파티션은 다른 파티션과 &lt;b&gt;독립적으로 읽고 쓰기가 가능한 로그&lt;/b&gt;가 됨.&lt;/li&gt; 
   &lt;li&gt;&lt;b&gt;토픽&lt;/b&gt;은 같은 형식의 메시지를 전달하는 파티션들의 그룹으로 정의한다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;각 메시지에는 &lt;b&gt;오프셋(단조 증가하는 순번)&lt;/b&gt;이 붙음&amp;nbsp;&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;파티션 안에서는 순서가 보장되지만, &lt;b&gt;파티션 간에는 순서 보장 없음.&lt;/b&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;대표 예시 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;로그 방식과 전통적인 메시징 방식의 비교&lt;/blockquote&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;b&gt;한 스레드(single-thread)&lt;/b&gt;로 순차적으로 처리&amp;nbsp;&lt;/li&gt; 
 &lt;li&gt;이런 거친 방식의 로드벨런싱(Coarse-grained Load Balancing) 방법은 몇 가지 불리한 면이 있다. 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;파티션 수 한계 : 소비자 수는 파티션 수보다 많을 수 없음.&lt;/li&gt; 
   &lt;li&gt;Head-of-line-blocking : 앞에서 지연되면 뒤 파티션들도 모두 지연&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;언제 뭘 쓰면 되냐 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;JMS/AMQP 방식의 메시지 브로커 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;메시지 순서는 중요하지 않은데, 처리 비용이 비싸고 병렬화 처리하고 싶은 경우&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;로그 기반 접근법 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;메시지 순서가 중요 + 메시지 처리 속도 빠름 + 처리량 많음&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소비자 오프셋&lt;/blockquote&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;추적 오버헤드가 감소&lt;/li&gt; 
   &lt;li&gt;일괄 처리와 파이프라이닝을 수행할 수 있는 기회를 제공 ➔ 로그 기반 시스템의 처리량 향상&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;데이터베이스 복제에 사용되는 &lt;b&gt;로그 순차 번호(log sequence number)&lt;/b&gt;와 상당히 유사함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;디스크 공간 사용&lt;/blockquote&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;Kafka는 로그를 무한히 저장하지 않고, 시간이 지나면 &lt;b&gt;오래된 로그(segment)&lt;/b&gt;는 자동으로 삭제 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소비자가 생산자를 따라갈 수 없을 때&lt;/blockquote&gt;&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;&lt;/li&gt; 
 &lt;li&gt;로그 기반 접근법은 고정 크기의 버퍼를 사용하는 &lt;b&gt;버퍼링&lt;/b&gt;&amp;nbsp;형태.&lt;/li&gt; 
 &lt;li&gt;소비자가 뒤쳐지면 필요한 메시지를 읽지 못 할 수 있음.&lt;/li&gt; 
 &lt;li&gt;버퍼는 충분히 크다면, 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;운영자가 느린 소비자를 수정해서 &lt;b&gt;메시지 손실이 발생하기 전까지 따라잡도록&lt;/b&gt; 할 수 있음.&lt;/li&gt; 
   &lt;li&gt;kafka는 버퍼는 메모리 기반 큐보다 훨씬 큼 (수GB ~ 수 TB)&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;blockquote data-ke-style=&quot;style2&quot;&gt;오래된 메시지 생성&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;로그 기반 접근 법도 오래된 메시지를 읽을 수 있음.&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;메시지를 파일에 append하고, 소비자가 읽을 때 &lt;b&gt;그 메시지를 삭제하지 않음.&lt;/b&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이런 특성 때문에 &lt;b&gt;로그 기반 메시징은 이전 장의 배치 처리와 비슷&lt;/b&gt;&lt;/li&gt; 
   &lt;li&gt;즉, &lt;b&gt;입력 데이터(로그)&lt;/b&gt;를 그대로 두고,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 소비자는 그걸 읽어 &lt;b&gt;결과를 별도로 만들어내는 구조.(파생된 결과 데이터)&lt;/b&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;이런 구조 덕분에, 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;실험(코드 변경)을 자유롭게 할 수 있음.&lt;/li&gt; 
   &lt;li&gt;에러나 버그 발생시 복구도 쉬움&lt;/li&gt; 
   &lt;li&gt;조직 내 여러 데이터 흐름(dataflow)을 통합하는 데 매우 유용&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;blockquote data-ke-style=&quot;style2&quot;&gt;근데 시간 지나면 삭제한다고 했는데?&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;Kafka는 &lt;b&gt;보존(retention) 정책&lt;/b&gt;에 따라 오래된 로그(segment)를 자동으로 삭제&lt;/li&gt;&lt;li&gt;Kafka 자체는 “단기 버퍼”로 쓰고, 장기 보존은 &lt;b&gt;데이터 레이크(HDFS/S3)&lt;/b&gt;에 저장하는 게 일반적&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 2. 데이터베이스와 스트림&amp;nbsp;&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  2-1. 시스템 동기화 유지하기&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터베이스와 스트림&lt;/blockquote&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;스트림은 “시간의 흐름에 따라 일어난 사건(event)”을 저장&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이중쓰기 문제&lt;/blockquote&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;두 시스템의 데이터 불일치(inconsistency)&lt;/b&gt; 가 발생&lt;/li&gt;&lt;li&gt;이런 문제를 완벽히 해결하려면 &lt;b&gt;원자적 커밋(atomic commit)&lt;/b&gt; 이나 &lt;b&gt;2PC &lt;/b&gt;같은 고비용 트랜잭션이 필요&lt;/li&gt;&lt;li&gt;단일 리더 복제 구조에서는 리더가 쓰기 순서를 정해 덜 복잡하지만,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 리더가 여러 개이거나 없는 구조에서는 충돌이 자주 발생&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  2-2. 변경 데이터 캡처&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;변경 데이터 캡처(change data capture, CDC)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;CDC는 데이터베이스에 기록하는 모든 데이터의 변화를 관찰해 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다른 시스템으로 복제할 수 있는 형태로 추출하는 과정&lt;/li&gt;&lt;li&gt;데이터가 기록되자마자 변경 내용을 스트림으로 제공할 수 있으면 특히 유용함.&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G1d0n/dJMcabvJiWn/XH7yvYghz32CH6TfBd2t7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G1d0n/dJMcabvJiWn/XH7yvYghz32CH6TfBd2t7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G1d0n/dJMcabvJiWn/XH7yvYghz32CH6TfBd2t7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG1d0n%2FdJMcabvJiWn%2FXH7yvYghz32CH6TfBd2t7k%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;1230&quot; height=&quot;566&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;위의 예시에서 검색 색인 뿐만 아니라 데이터 웨어하우스도 &quot;변경 스트림의 소비자&quot;임.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;변경 데이터 캡처의 구현&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;“검색 인덱스나 &quot;데이터 웨어하우스&quot; 같은 시스템은&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 원본 데이터베이스의 로그를 소비하여 만들어지는 &lt;b&gt;파생&lt;b&gt;(derived)&lt;/b&gt; 데이터 시스템&lt;/b&gt;&lt;/li&gt; 
 &lt;li&gt;CDC는 레코드 시스템의 정확한 데이터 복제본을 가지게 하기 위해 레코드 시스템에 발생하는&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 모든 변경 사항을 파생 데이터 시스템에 반영하는 것을 보장하는 메커니즘&lt;/li&gt; 
 &lt;li&gt;CDC는 본질적으로 변경 사항을 캡처할 DB 하나를 리더로 하고 나머지를 팔로워로 한다.&lt;/li&gt; 
 &lt;li&gt;CDC를 구현하는데 DB trigger를 사용하기도 함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;하지만 이 방식은 전반적으로 취약하고, 성능 오버헤드가 상당함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;메시지 브로커와 동일하게 비동기 방식으로 동작 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;초기 스냅숏&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;모든 변경 사항을 영구적으로 보관하는 일은 디스크 공간이 많이 필요하고, &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 로그를 재생하는 작업도 너무 오래 걸림. 그래서 로그를 적당히 잘라야 함.&lt;/li&gt;&lt;li&gt;일부 CDC 도구는 스냅숏 기능을 내장하고 있으나, 수작업으로 해야하는 CDC 도구도 있음.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로그 컴팩션&lt;/blockquote&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;로그에 데이터베이스에 있는 모든 키의 최신 값이 존재하는 것이 보장됨.&lt;/li&gt; 
   &lt;li&gt;따라서 검색 색인과 같은 파생 데이터 시스템을 재구축할 때마다 새 소비자는&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 컴팩션된 로그 토픽의 오프셋 0부터 시작해서 모든 키를 스캔하면 됨.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;아파치 카프카는 로그 컴팩션 기능을 제공함.&lt;/li&gt; 
 &lt;li&gt;메시지 브로커는 일시적 메시징뿐만 아니라 지속성 있는 저장소로도 사용 가능함 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;변경 스트림용 API 지원&lt;/blockquote&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;예) 리싱크DB는 질의 결과에 변경이 있을 때 알림을 받을 수 있게 구독이 가능한 질의를 지원하는 등...&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  2-3. 이벤트 소싱&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이벤트 소싱&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;DDD 커뮤니티에서 개발한 기법&lt;/li&gt;&lt;li&gt;CDC와 유사하게 애플리케이션 상태 변화를 모두 변경 이벤트 로그로 저장한다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;CDC vs 이벤트 소싱&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;CDC : DB 내부의 변경 로그를 읽어서 &lt;b&gt;데이터 변화를 외부로 전달&lt;/b&gt;하는 방식 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;&lt;b&gt;데이터베이스를 자유롭게 수정 가능한(mutable)&lt;/b&gt; 방식으로 사용&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔&amp;nbsp;INSERT / UPDATE / DELETE 를 마음대로 수행할 수 있다.&lt;/li&gt; 
   &lt;li&gt;데이터베이스의 &lt;b&gt;변경 로그(replication log)&lt;/b&gt;를 읽어서 &lt;b&gt;변경 내용을 추출&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ DB에 실제로 기록된 순서를 그대로 반영할 수 있어서 &lt;b&gt;경쟁 조건&lt;/b&gt;이 생기지 않는다.&lt;/li&gt; 
   &lt;li&gt;애플리케이션은 &lt;b&gt;CDC가 동작 중이라는 사실을 몰라도 된다.&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔&amp;nbsp;즉, DB는 평소처럼 사용하고, CDC는 백그라운드에서 로그를 감시&lt;/li&gt; 
  &lt;/ol&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 data-end=&quot;728&quot; data-start=&quot;608&quot;&gt;애플리케이션 로직 자체가 이벤트 기반으로 설계&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 이벤트들을 불변(immutable) 형태로 이벤트 로그에 기록한다.&lt;/li&gt; 
   &lt;li data-end=&quot;830&quot; data-start=&quot;729&quot;&gt;이벤트 저장소는 “append-only” 구조이며, 수정/삭제 금지.&lt;/li&gt; 
   &lt;li data-end=&quot;980&quot; data-start=&quot;831&quot;&gt;이벤트는 &lt;b&gt;낮은 수준의 데이터 변경&lt;/b&gt;이 아니라, &lt;b&gt;비즈니스 수준에서 실제 일어난 일&lt;/b&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;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이벤트 소싱의 특징&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1099&quot; data-start=&quot;739&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li data-end=&quot;826&quot; data-start=&quot;739&quot;&gt;모든 상태 변화는 이벤트 로그로 남는다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;826&quot; data-start=&quot;739&quot;&gt;&quot;회원가입됨”, “주문됨”, “결제됨”, “배송완료됨” 같은 이벤트들이 시간순으로 기록됨.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;940&quot; data-start=&quot;828&quot;&gt;데이터는 절대 수정하거나 삭제하지 않는다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;940&quot; data-start=&quot;828&quot;&gt;변경이 생기면 &lt;b&gt;“이전 상태를 취소하는 새 이벤트”&lt;/b&gt;를 추가함.&lt;/li&gt; 
   &lt;li data-end=&quot;940&quot; data-start=&quot;828&quot;&gt;즉, 로그를 Append 방식으로만 저장&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;1034&quot; data-start=&quot;942&quot;&gt;현재 상태는 이벤트를 전부 재생(Replay)하여 도출함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;1034&quot; data-start=&quot;942&quot;&gt;시스템 장애나 버그가 생겨도 이벤트를 다시 읽어 현재 상태를 재구성하기 용이함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;1099&quot; data-start=&quot;1036&quot;&gt;디버깅 및 변경 추적이 용이하다.&lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이벤트 로그에서 현재 상태 파생하기&lt;/blockquote&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;b&gt;이벤트 로그&lt;/b&gt;를 가져와 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 사용자에게 보여주기에 적당한 애플리케이션 상태로 변환해야 함.&lt;/li&gt; 
   &lt;li&gt;이 변환 과정은 결정적(deterministic) 과정이어야 함. (다시 수행해도 똑같은 상태여야 하기 때문)&lt;/li&gt; 
   &lt;li&gt;일반적으로 이벤트 로그에서 파생된 현재 상태의 스냅숏을 저장하는 메커니즘이 있음. 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;따라서, 매번 전체 로그를 반복해서 재처리할 필요는 없음.&lt;/li&gt; 
    &lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;CDC와 이벤트 소싱의 로그 관리 방식 차이&lt;/blockquote&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;260&quot; data-start=&quot;219&quot;&gt;CDC 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;260&quot; data-start=&quot;219&quot;&gt;데이터베이스의 상태 변경을 그대로 반영&lt;/li&gt; 
   &lt;li data-end=&quot;260&quot; data-start=&quot;219&quot;&gt;즉, &lt;b&gt;“기본키 기준으로 가장 최신 상태”&lt;/b&gt; 만 유지하면 된다.&lt;/li&gt; 
   &lt;li data-end=&quot;260&quot; data-start=&quot;219&quot;&gt;이전 상태(이전 이벤트)는 &lt;b&gt;로그 컴팩션&lt;/b&gt;을 통해 지워진다.&lt;/li&gt; 
   &lt;li data-end=&quot;260&quot; data-start=&quot;219&quot;&gt;불필요한 옛 버전은 버려도 현재 상태 복원이 가능함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;260&quot; data-start=&quot;219&quot;&gt;이벤트 소싱 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;618&quot; data-start=&quot;535&quot;&gt;이벤트 소싱은 &lt;b&gt;사용자의 의도나 행동 자체&lt;/b&gt;를 이벤트로 기록&lt;/li&gt; 
   &lt;li data-end=&quot;695&quot; data-start=&quot;619&quot;&gt;각 이벤트는 &lt;b&gt;과거 기록을 덮어쓰지 않으며&lt;/b&gt;, 시스템 상태를 복원하려면 &lt;b&gt;모든 이벤트의 전체 히스토리&lt;/b&gt;가 필요&lt;/li&gt; 
   &lt;li data-end=&quot;753&quot; data-start=&quot;696&quot;&gt;따라서 &lt;b&gt;로그 컴팩션은 불가능하다. &lt;/b&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;명령과 이벤트&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;이벤트 소싱의 철학은 이벤트와 명령(command)를 구분하는데 있다.&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;명령에 대한 무결성이 검증되고 승인되면, 명령은 지속성 있는 불변 이벤트가 됨.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;이벤트는 생성 시점에 사실(fact)가 된다.&lt;/li&gt; 
 &lt;li&gt;이벤트 스트림의 소비자는 이벤트를 거절하지 못한다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;소비자가 이벤트를 받는 시점에는 이벤트는 이미 불변 로그의 일부&lt;/li&gt; 
   &lt;li&gt;따라서 &lt;b&gt;유효성 검증&lt;/b&gt;은 이벤트로 바뀌기 &lt;b&gt;이전 단계(명령 단계)&lt;/b&gt;에서 &lt;b&gt;동기적&lt;/b&gt;으로 수행해야 함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;비동기 처리로 유효성을 검사하기&amp;nbsp;&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;좌석 예약처럼 &lt;b&gt;여러 사용자가 동시에 같은 자원을 요청하는 경우&lt;/b&gt;를 예로 들었음.&lt;/li&gt; 
   &lt;li data-end=&quot;809&quot; data-start=&quot;782&quot;&gt;먼저 “가예약”을 이벤트 발행&lt;/li&gt; 
   &lt;li data-end=&quot;809&quot; data-start=&quot;782&quot;&gt;이후에 &lt;b&gt;유효성 검증 후 &lt;/b&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  2-4. 상태와 스트림 그리고 불변성&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;불변성 원리 덕분에...&lt;/blockquote&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;이 원리가 이벤트 소싱과 CDC를 매우 강력하게 만들어줌.&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;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;변경된 상태는 시간의 흐름에 따라 변한 이벤트의 마지막 결과&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;불변 이벤트의 장점&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;회계 원장(ledger)처럼 신뢰 가능한 기록 유지 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;즉, &lt;b&gt;“언제, 어떤 변화가 있었는가”&lt;/b&gt; 를 신뢰성 있게 남길 수 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;오류 복구 및 감사(audit)에 강함&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;잘못된 데이터가 생겨도 기존 기록을 수정하지 않고 “오류를 보정하는 새로운 이벤트”를 추가&lt;/li&gt; 
   &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;코드 실수나 데이터 오염이 발생했을 때, 불변 로그를 통해 과거 상태를 재현하기가 훨씬 쉬워짐&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;데이터 손실 및 복구 위험 감소&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;잘못된 데이터가 저장되어도 &lt;b&gt;이전 이벤트가 보존되어 있어 복구 가능&lt;/b&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;분석 및 추적에 유용한 풍부한 히스토리&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;190&quot; data-start=&quot;149&quot;&gt;불변 이벤트는 단순히 “현재 상태”뿐 아니라 &lt;b&gt;사용자의 행동 패턴, 취소된 행동의 흔적&lt;/b&gt;까지 포함&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;동일한 이벤트 로그로 여러 가지 뷰 만들기&lt;/blockquote&gt;&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; 동일한 이벤트 로그로 다른 여러 읽기 전용 뷰를 만들 수 있다. 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;Druid는 카프카로부터 직접 데이터를 읽어 처리&lt;/li&gt; 
   &lt;li&gt;Pistachio는 분산 키-값 저장소로 카프카를 커밋 로그처럼 사용&lt;/li&gt; 
   &lt;li&gt;카프카 커넥트 싱크는 카프카에서 여러 데이터베이스와 색인에 데이터를 내보낼 수 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이벤트 로그를 기반으로 하면 시스템을 바꾸거나 확장하기 훨씬 쉽다.&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;새로운 기능이 필요하면? &lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기존 DB는 변경하지 않고, 이벤트 로그를 읽어서 적절한 &quot;읽기 전용 뷰&quot; 만들면 됨&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이벤트 로그는 &lt;b&gt;원본 데이터의 근거(ground truth)&lt;/b&gt; 이기 때문에,&lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;새로운 시스템은 기존 시스템의 로그를 그대로 읽어 &lt;b&gt;독립적으로 실행&lt;/b&gt;할 수 있음.&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 신/구 버전이 같이 공존할 수 있고, 점진적으로 구 버전을 없앨 수 있음.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;대표적인 응용의 예로 CQRS가 있음&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;431&quot; data-start=&quot;368&quot;&gt;DB 스키마, 색인(index), 저장 방식이 “쓰기 성능”과 “읽기 편의성”을 동시에 만족시키기 어려움&lt;/li&gt; 
   &lt;li data-end=&quot;499&quot; data-start=&quot;432&quot;&gt;따라서, 데이터를 &lt;b&gt;읽기와 쓰기&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;동시성 제어&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;이벤트 소싱과 CDC의 가장 큰 단점은 이벤트 로그의 소비가 대게 비동기로 이뤄진다는 것.&lt;/li&gt; 
 &lt;li&gt;해결책 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;읽기 뷰의 갱신과 로그에 이벤트를 추가하는 작업을 동기식으로 수행하는 방법&lt;/li&gt; 
   &lt;li&gt;이벤트 로그로 현재 상태를 만드는 방법&lt;/li&gt; 
   &lt;li&gt;이벤트 로그와 애플리케이션 상태를 같은 파티션 단위로 설계하는 방법&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;불변성의 한계&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;438&quot; data-start=&quot;294&quot;&gt;많은 시스템은 불변 구조를 사용한다. 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;438&quot; data-start=&quot;294&quot;&gt;Git, Mercurial, Fossil 같은 버전 관리 시스템&lt;/li&gt; 
   &lt;li data-end=&quot;438&quot; data-start=&quot;294&quot;&gt;이들은 &lt;b&gt;과거의 모든 변경 이력(history)&lt;/b&gt; 을 보존하기 위해 불변 데이터를 사용한다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;불변성은 데이터의 &lt;b&gt;일관성, 감사 가능성, 복구 용이성&lt;/b&gt; 측면에서 매우 유용하다.&lt;/li&gt; 
 &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;하지만 “모든 데이터를 영구적으로 불변”으로 두는 것은 현실적으로 어려움&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;저장소 용량과 성능 문제 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-end=&quot;652&quot; data-start=&quot;579&quot;&gt;데이터가 계속 쌓이기만 하고 삭제되지 않으면, &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 저장소가 점점 커져서 &lt;b&gt;관리 비용과 성능 문제가 발생&lt;/b&gt;할 수 있다.&lt;/li&gt; 
     &lt;li data-end=&quot;735&quot; data-start=&quot;653&quot;&gt;특히 &lt;b&gt;자주 갱신되는 데이터셋&lt;/b&gt;의 경우,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 불변 이력을 모두 유지하면 &lt;b&gt;공간 낭비&lt;/b&gt;와 &lt;b&gt;검색 성능 저하&lt;/b&gt;로 이어질 수 있다.&lt;/li&gt; 
     &lt;li data-end=&quot;788&quot; data-start=&quot;736&quot;&gt;압축이나 가비지 컬렉션을 해야 하지만, 이 또한 복잡하고 비용이 큼&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;법적 / 관리적 이유로 삭제가 필요한 경우 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;이런 경우에는 “삭제 이벤트를 추가하는 것”으로 해결되지 않는다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 왜냐하면 &lt;b&gt;기존 데이터가 여전히 로그 안에 존재하기 때문&lt;/b&gt;.&lt;/li&gt; 
     &lt;li data-end=&quot;488&quot; data-start=&quot;439&quot;&gt;첨부터 기록하지 않았던 것 처럼 해야함.&lt;/li&gt; 
    &lt;/ul&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;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 3. 스트림 처리&amp;nbsp;&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스트림을 처리하는 3가지 선택지&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;이벤트에서 데이터를 꺼내서 저장소 시스템에 기록하고, 데이터를 질의&lt;/li&gt;&lt;li&gt;이벤트를 사용자에게 직접 전달&lt;/li&gt;&lt;li&gt;하나 이상의 입력 스트림을 처리해 하나 이상의 출력 스트림을 생산 (이번장에서 살펴볼 부분)&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-1. 스트림 처리의 사용 (Uses of Stream Processing)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모니터링&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;복잡한 이벤트 처리 (complex event processing, CEP)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;1990년대에 이벤트 스트림 분석용으로 개발된 방법&lt;/li&gt;&lt;li&gt;특정 이벤트 패턴을 검색해야 하는 애플리케이션에 특히 적합&lt;/li&gt;&lt;li&gt;&lt;b&gt;실시간&lt;/b&gt;으로 발생하는 여러 &lt;b&gt;이벤트 스트림을 분석&lt;/b&gt;하여, &lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 특정한 패턴이나 조건이 충족될 때&lt;/b&gt; 새로운 &quot;복합 이벤트&quot;를 생성하는 시스템&lt;/li&gt;&lt;li&gt;감지한 이벤트 패턴을 묘사하기 위해 종종 SQL과 같은 고수준 선언형 질의 언어를 사용하기도 함.&lt;/li&gt;&lt;li&gt;이 시스템에서는 질의는 오랜 기간 저장되고, 입력 스트림으로 부터 들어오느 이벤트는 지속적으로&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 질의를 지나 흘러가면서 이벤트 패턴에 매칭되는 질의를 찾는다.&lt;/li&gt;&lt;li&gt;에스퍼, 아파마, SQL스크립트 등이 있음.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스트림 분석&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;ex. 특정 유형의 이벤트 빈도 측정, 특정 기간에 걸치 값의 이동 평균 계산...&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;일반적으로 이런 통계는 고정된 시간 간격 기준으로 계산한다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;집계 시간 간격 = 윈도우(window)&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;최적화를 위해 &quot;확률적 알고리즘&quot;을 사용하기도 함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;구체화 뷰 유지하기&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이때 애플리케이션의 상태로 &lt;b&gt;&quot;구체화 뷰&quot;&lt;/b&gt;라고 할 수 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;구체화 뷰를 만들기 위해서는 잠재적으로 임의의 시간 범위에 발생한 모든 이벤트가 필요함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;따라서 모든 이벤트가 필요함. ➔ 시작 시점까지 늘려진 윈도우가 필요함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;스트림 처리 시스템이라면 이론적으로는 모두 구체화 뷰를 유지하는 데 쓸 수 있다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;하지만, materialized view 유지에는 &lt;b&gt;이전 모든 이벤트의 상태를 계속 기억하고 갱신하는 능력&lt;/b&gt;이 필요하다.&lt;/li&gt; 
   &lt;li&gt;반면 많은 분석 중심 스트림 처리 시스템은 “윈도우 기반(한정된 기간)” 데이터만 처리하도록 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 설계되어 있기 때문에, 이런 영구 상태 유지 요구사항과는 &lt;b&gt;설계 철학이 충돌&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;스트림 상에서 검색하기&lt;/blockquote&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;전통적인 검색 엔진은 먼저 문서를 indexing하고 index를 통해 질의를 실행&lt;/li&gt;&lt;li&gt;반대로 스트림 검색은 처리 순서가 반대임.&amp;nbsp;&lt;/li&gt;&lt;li&gt;질의를 먼저 저장함. 그리고 질의를 지나가면서 실행됨.&lt;/li&gt;&lt;li&gt;즉, “아직 저장되지 않은, 실시간으로 흘러들어오는 데이터”를 &lt;b&gt;계속 감시&lt;/b&gt;하면서 조건이 충족되면 즉시 결과를 생성&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메시지 전달과 RPC&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;앞에서 메시지 전달 시스템을 RPC 대안은로 사용할 수 있다고 했음. (139쪽)'&lt;/li&gt; 
 &lt;li&gt;메시지 전달 시스템이 RPC처럼 사용될 수 있지만, 스트림 처리와는 다른 특성을 가짐.&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;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; RPC는 &lt;b&gt;요청-응답(request-response)&lt;/b&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;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 추가적인 재시도 로직을 구현하지 않으면, 처리에 내결함성을 보장하지 못함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-2. 시간에 관한 추론 (Reasoning About Time)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ (특히 분석 목적으로 사용할 때) 스트림 처리자는 종종 시간을 다뤄야할 때가 있다.&lt;/i&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이벤트 시간 대 처리 시간&lt;/blockquote&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;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Koyzr/dJMb99SdMHc/z0NN8FjlpOkr88ITsLKV7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Koyzr/dJMb99SdMHc/z0NN8FjlpOkr88ITsLKV7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Koyzr/dJMb99SdMHc/z0NN8FjlpOkr88ITsLKV7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKoyzr%2FdJMb99SdMHc%2Fz0NN8FjlpOkr88ITsLKV7K%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;1021&quot; height=&quot;548&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;준비 여부 인식&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;이벤트를 시간 단위로 윈도우(Window)로 묶어 처리할 때 생기는 문제 
  &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;윈도우를 이미 종료한 후에 도착한 낙오자(straggler) 이벤트를 처리할 방법이 필요함 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;낙오자 이벤트를 무시하기 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li data-end=&quot;479&quot; data-start=&quot;438&quot;&gt;정상적인 상황에서는 늦게 도착하는 이벤트의 비율이 매우 적기 때문.&lt;/li&gt; 
     &lt;li data-end=&quot;519&quot; data-start=&quot;483&quot;&gt;하지만 만약 낙오가 많아지면 경고를 보내거나 추적할 수 있다.&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;수정 값을 발행하기 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;늦게 도착한 이벤트를 포함해 다시 윈도우를 계산(갱신)&lt;/li&gt; 
     &lt;li&gt;그에 따라 이전 출력 결과를 취소하거나 보정&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;681&quot; data-start=&quot;640&quot;&gt;일부 이벤트는 네트워크 지연이나 장비 문제로 늦게 도착할 수 있음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;791&quot; data-start=&quot;682&quot;&gt;따라서 시스템은 “언제 윈도우가 완전히 끝났다고 선언할지”를 판단하기 위해&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 타임아웃&lt;/b&gt;이나 &lt;b&gt;특수 메시지(‘더 이상 이전 타임스탬프는 없다’는 신호)&lt;/b&gt; 를 사용할 수도 있음.&lt;/li&gt; 
   &lt;li data-end=&quot;791&quot; data-start=&quot;682&quot;&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;blockquote data-ke-style=&quot;style2&quot;&gt;어쨋든 어떤 시계를 사용할 것인가.&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;이벤트의 타임스탬프는 모바일 장치 로컬 시계를 따르는, 실제 사용자와 상호작용이&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 발생했던 실제 시각이어야 함.&lt;/li&gt; 
 &lt;li&gt;하지만 우연히 잘못된 시간이 설정됐을 가능성이 있기 때문에 사용자가 제어하는 장비의&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 시계를 항상 신뢰하기는 어려움.&lt;/li&gt; 
 &lt;li&gt;잘못된 장치 시계를 조정하는 한 가지 방법은 세 가지 타임스탬프를 로그로 남기는 것 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;이벤트가 발생한 시간 (장치 시계를 따른다)&lt;/li&gt; 
   &lt;li&gt;이벤트를 서버로 보낸 시간 (장치 시계를 따른다)&lt;/li&gt; 
   &lt;li&gt;서버에서 이벤트를 받은 시간 (서버 시계를 따른다)&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;2번과 3번의 타임스탬프 차이를 구하면 장치 시계와 서버 시계 간의 오프셋을 추정할 수 있음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이때 필요한 타임스탬프 정확도에 비해 네크워크 지연은 무시할 만하고,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 이벤트가 발생한 시간과 이벤트를 서버로 보낸 시간 사이에는 장치 시계 오프셋이&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 변하지 않았다고 가정&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;윈도우 유형&lt;br&gt;&lt;i&gt;➔ 윈도우 기간을 어떻게 정의할지 결정해야 함&lt;/i&gt;&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;텀블링 윈도우 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;고정된 크기의 윈도우&lt;/li&gt; 
   &lt;li&gt;모든 이벤트는 정확히 한 윈도우에 속함.&lt;/li&gt; 
   &lt;li&gt;ex. {10시03분00초, 10시03분59}&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;홉핑 윈도우 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;고정된 크기의 윈도우&lt;/li&gt; 
   &lt;li&gt;결과를 매끄럽게 만들기 위해 윈도우를 중첩할 수 있음.&lt;/li&gt; 
   &lt;li&gt;1분 크기의 홉을 이용하는 5분 윈도우 예시 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;A1윈도우 : &quot;10시03분00초~10시07분59초&quot;&lt;/li&gt; 
     &lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;A2윈도우 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&quot;10시04분00초~10시08분59초&quot;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;슬라이딩 윈도우 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;각 시간 간격 사이에서 발생한 모든 이벤트를 포함&lt;/li&gt; 
   &lt;li&gt;시간 기준으로 정렬한 이벤트를 버퍼에 유지하고 이벤트가 만료되면 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 윈도우에서 제거하는 방식으로 구현할 수 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;세션 윈도우 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-3. 스트림 조인&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;3가지 조인 유형&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;스트림 상에서 수행하는 조인은 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;일괄 처리 작업에서 수행하는 조인보다 훨씬 어려움.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;3가지 유형 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;스트림-스트림 조인&lt;/li&gt; 
   &lt;li&gt;스트림 테이블 조인&lt;/li&gt; 
   &lt;li&gt;테이블-테이블 조인&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스트림-스트림 조인 (윈도우 조인)&lt;/blockquote&gt;&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;와 그 결과를 클릭한 &lt;b&gt;클릭 이벤트&lt;/b&gt;를 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 연결(조인)하여 사용자의 검색 ➔ 클릭 행동을 분석하는 방식.&lt;/li&gt; 
 &lt;li&gt;적절한 윈도우 선택이 필요한 이유&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;176&quot; data-start=&quot;75&quot;&gt;클릭이 항상 발생하지 않음.&lt;/li&gt; 
   &lt;li data-end=&quot;176&quot; data-start=&quot;75&quot;&gt;검색 이벤트와 클릭 이벤트는 시간 차이가 일정하지 않음.&lt;/li&gt; 
   &lt;li data-end=&quot;286&quot; data-start=&quot;178&quot;&gt;네트워크 지연으로 이벤트 순서가 바뀔 수도 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;286&quot; data-start=&quot;178&quot;&gt;이런 유형의 조인을 구현하려면 스트림 처리가 상태(state)를 유지해야 함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;286&quot; data-start=&quot;178&quot;&gt;예를 들면, 모든 이벤트를 세션 ID로 색인&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;blockquote data-ke-style=&quot;style2&quot;&gt;스트림 테이블 조인 (스트림 강화)&lt;/blockquote&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;“강화(enriching)”&lt;/b&gt; 하는 형태&lt;/li&gt; 
 &lt;li&gt;수행 방법 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li data-end=&quot;313&quot; data-start=&quot;277&quot;&gt;스트림 처리기는 들어오는 활동 이벤트(스트림)를 받는다.&lt;/li&gt; 
   &lt;li data-end=&quot;356&quot; data-start=&quot;314&quot;&gt;각 이벤트의 &lt;b&gt;사용자 ID&lt;/b&gt;를 기준으로 데이터베이스를 조회한다.&lt;/li&gt; 
   &lt;li data-end=&quot;393&quot; data-start=&quot;357&quot;&gt;데이터베이스에서 프로필 정보를 가져와 이벤트에 추가한다.&lt;/li&gt; 
   &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;이렇게 강화된 이벤트를 출력한다.&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;또 다른 방법은 네트워크 왕복 없이 질의하도록 스트림 처리자 내부에 DB 사본을 적재하는 것&amp;nbsp;&lt;/li&gt; 
 &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;스트림은 시간이 흘러가면서 DB 내용이 변할 가능성이 높음. 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;따라서 DB 로컬 복사본을 최신 상태로 유지해야 함.&lt;/li&gt; 
   &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;이 문제는 CDC를 사용하면 해결 가능함.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;스트림-스트림 조인과 매우 비슷함.&lt;/li&gt; 
 &lt;li data-end=&quot;415&quot; data-start=&quot;394&quot;&gt;가장 큰 차이점은 테이블 조인을 할 때, 테이블 변경 로그 스트림 쪽은 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;시작 시간&quot;까지 이어지는 윈도우를 사용하며 레코드의 새 버전으로 오래된 것을 덮어씌운다.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;테이블 테이블 조인 (구체화 뷰 유지)&lt;/blockquote&gt;&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;해야 하는 스트림 기반 “구체화 뷰 유지”와 같다.&lt;/li&gt;&lt;li&gt;단순 조회가 아니라 변화가 발생할 때마다 조인 결과(타임라인 캐시)를 &lt;b&gt;실시간으로 갱신&lt;/b&gt;하는 과정&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;조인의 시간 의존성&lt;/blockquote&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;시간에 따라 변하는 상태를 조인해야 한다면 어느 시점을 조인에 사용해야 할까?&amp;nbsp;&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;예시) 세율&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;복수 개의 스트림에 걸친 이벤트 순서가 결정되지 않으면 조인도 비결정적 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이 문제를 데이터 웨어하우승는 천천히 변하는 차원(slowly changing dimension, SCS)라 함.&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;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-4. 내결함성&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;내결함성&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;380&quot; data-start=&quot;125&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;180&quot; data-start=&quot;125&quot;&gt;&lt;b&gt;스트림 처리의 핵심 과제:&lt;/b&gt; 장애가 발생해도 결과가 중복되거나 누락되지 않게 하는 것.&lt;/li&gt; 
 &lt;li data-end=&quot;270&quot; data-start=&quot;181&quot;&gt;일괄 처리(예: MapReduce)는 태스크 단위로 &lt;b&gt;명확한 재처리 경계&lt;/b&gt;가 존재하기 때문에&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;실패 후 재시작해도 동일한 결과를 얻을 수 있다.&lt;/li&gt; 
 &lt;li data-end=&quot;380&quot; data-start=&quot;271&quot;&gt;이러한 특성을 &lt;b&gt;정확히 한 번 시맨틱스(Exactly-once semantics)&lt;/b&gt; 라고 하며,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 실제 의미는 “&lt;b&gt;결과적으로 한 번만 반영(effectively-once)&lt;/b&gt;”이다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;380&quot; data-start=&quot;271&quot;&gt;&amp;nbsp;즉, 태스크가 여러 번 실행되더라도 출력 결과는 정확히 한 번만 나타나야 한다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;527&quot; data-start=&quot;432&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;527&quot; data-start=&quot;432&quot;&gt;스트림 처리에서도 동일한 문제가 있지만, 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;527&quot; data-start=&quot;432&quot;&gt;실시간성 요구로 인해 태스크가 끝날 때까지 기다릴 수 없기 때문에 &lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다른 방식의 내결함성 설계&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;마이크로 일괄 처리와 체크포인트&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;674&quot; data-start=&quot;595&quot;&gt;스트림을 짧은 구간(예: 1초 단위)으로 잘라 일괄 처리하듯 다루는 방법. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;674&quot; data-start=&quot;595&quot;&gt;마이크로 일괄 처리(microbatching)&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;801&quot; data-start=&quot;676&quot;&gt;특징 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;801&quot; data-start=&quot;687&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;729&quot; data-start=&quot;687&quot;&gt;일반 배치보다 빠르지만, 완전 실시간은 아님 (지연 시간 약 1초).&lt;/li&gt; 
   &lt;li data-end=&quot;770&quot; data-start=&quot;732&quot;&gt;배치 단위로 상태를 저장하고, 장애 시 체크포인트에서 재시작.&lt;/li&gt; 
   &lt;li data-end=&quot;801&quot; data-start=&quot;773&quot;&gt;대표적으로 Spark Streaming이 사용.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;930&quot; data-start=&quot;803&quot;&gt;단점 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;930&quot; data-start=&quot;814&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;886&quot; data-start=&quot;814&quot;&gt;실시간성이 떨어지고, 체크포인트 접근만으로는 외부 시스템(DB, 브로커 등)에 중복된 출력이 발생할 수 있음.&lt;/li&gt; 
   &lt;li data-end=&quot;930&quot; data-start=&quot;889&quot;&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;blockquote data-ke-style=&quot;style2&quot;&gt;원자적 커밋 재검토&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;1076&quot; data-start=&quot;988&quot;&gt;장애 시에도 “모든 출력을 한 번만 발생”시키려면 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;1076&quot; data-start=&quot;988&quot;&gt;스트림 처리기와 외부 시스템 간의 &lt;b&gt;원자적 커밋(atomic commit)&lt;/b&gt; 이 필요&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;1157&quot; data-start=&quot;1077&quot;&gt;하지만 스트림 프레임워크는 여러 노드와 메시지 시스템 간 동기화가 어려워 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;1157&quot; data-start=&quot;1077&quot;&gt;2PC는&amp;nbsp;현실적으로 비효율적&lt;/li&gt; 
   &lt;li data-end=&quot;1204&quot; data-start=&quot;1158&quot;&gt;대신, 내부 트랜잭션처럼 동작하는 &lt;b&gt;상태 관리 기반 커밋 방식&lt;/b&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;멱등성&lt;/blockquote&gt;&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;를 달성하기 위한 실용적 접근법.&lt;/li&gt;&lt;li&gt;같은 연산이 여러 번 수행되더라도 &lt;b&gt;결과가 동일&lt;/b&gt;해야 한다.&lt;/li&gt;&lt;li&gt;외부 시스템(예: Kafka, DB)에 쓰기 시 메시지 오프셋이나 마지막 처리 상태를 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 함께 기록하여 중복 실행 시 동일 결과를 유지하도록 만든다.&lt;/li&gt;&lt;li&gt;Trident (Storm 기반 프레임워크) 등은 이러한 멱등성을 전제로 작업 순서를 보장하며&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;“정확히 한 번 처리”&lt;/b&gt;를 실질적으로 구현한다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실패 후 상태 재구축&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;1806&quot; data-start=&quot;1734&quot;&gt;스트림 처리에서는 조인, 윈도우, 집계 등 상태를 가진 연산이 많기 때문에 장애 시 &lt;b&gt;상태 복구&lt;/b&gt;가 매우 중요&lt;/li&gt; 
 &lt;li data-end=&quot;2025&quot; data-start=&quot;1808&quot;&gt;복구 방식 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2025&quot; data-start=&quot;1822&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li data-end=&quot;1901&quot; data-start=&quot;1822&quot;&gt;원본 데이터 재질의 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;1901&quot; data-start=&quot;1847&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li data-end=&quot;1878&quot; data-start=&quot;1847&quot;&gt;모든 입력 이벤트를 재처리하여 동일한 결과 재생성&lt;/li&gt; 
     &lt;li data-end=&quot;1901&quot; data-start=&quot;1884&quot;&gt;느리지만 단순하고 확실함&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li data-end=&quot;2025&quot; data-start=&quot;1904&quot;&gt;&lt;b&gt;상태 스냅샷 복원 (Checkpoint Restore)&lt;/b&gt; 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;2025&quot; data-start=&quot;1949&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li data-end=&quot;1985&quot; data-start=&quot;1949&quot;&gt;주기적으로 상태를 저장소(HDFS, Kafka 등)에 백업&lt;/li&gt; 
     &lt;li data-end=&quot;2025&quot; data-start=&quot;1991&quot;&gt;장애 발생 시 해당 시점의 스냅샷을 불러와 빠르게 복구&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;2079&quot; data-start=&quot;2027&quot;&gt;일부 DB(예: VoltDB)는 노드 간 &lt;b&gt;상태 복제&lt;/b&gt;를 통해 장애 시 즉시 복구한다.&lt;/li&gt; 
 &lt;li data-end=&quot;2150&quot; data-start=&quot;2081&quot;&gt;상태 복제가 불필요한 경우도 있음 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li data-end=&quot;2150&quot; data-start=&quot;2081&quot;&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;트레이드 오프&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li data-end=&quot;2278&quot; data-start=&quot;2191&quot;&gt;복구 방식 선택은 &lt;b&gt;인프라 성능 특성&lt;/b&gt;에 달려 있다. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;2278&quot; data-start=&quot;2229&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li data-end=&quot;2257&quot; data-start=&quot;2229&quot;&gt;디스크 접근이 느리고 네트워크가 빠른 시스템&lt;/li&gt; 
   &lt;li data-end=&quot;2278&quot; data-start=&quot;2260&quot;&gt;혹은 그 반대의 시스템 등&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;2364&quot; data-start=&quot;2279&quot;&gt;따라서 “로컬 상태 vs 원격 상태” 중 어떤 쪽을 더 자주 백업할지,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 혹은 실시간 복제를 유지할지 등의 &lt;b&gt;트레이드오프 설계&lt;/b&gt;가 필요함.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/231</guid>
      <comments>https://gilbert9172.tistory.com/231#entry231comment</comments>
      <pubDate>Sat, 1 Nov 2025 17:33:36 +0900</pubDate>
    </item>
    <item>
      <title>10장. 일괄 처리(Batch Processing)</title>
      <link>https://gilbert9172.tistory.com/230</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 0. 개요&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시스템 구축 방법의 세 가지 유형&lt;br /&gt;&lt;i&gt;➔ 온라인 시스템이 유일한 시스템 구축 방법은 아니다.&lt;/i&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스 (온라인 시스템)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;응답 시간은 서비스 성능 측정의 중요한 지표&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일괄 처리 시스템 (오프라인 시스템)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;온라인 서비스와 일관 처리 시스템 사이의 어딘가..&lt;/li&gt;
&lt;li&gt;입력 이벤트가 발생한 직후 바로 작동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. 유닉스 도구로 일괄 처리하기&lt;/h3&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;size18&quot;&gt;  1-1. 단순 로그분석&lt;/p&gt;
&lt;pre id=&quot;code_1761463566074&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 웹 사이트에서 가장 인기 높은 체이지 5개
cat /var/log/nginx/access.log |
  awk '{print$7}' |
  sort            |
  uniq -c         |
  sort -r -n      |
  head -n 5&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;unix 기반 방식은 상당히 강력함(incredibly powerful).&lt;/li&gt;
&lt;li&gt;수 기가 바이트의 로그 파일을 수 초 내로 처리할 수 있고, 필요에 따라 분석방법을 수정하기도 쉬움.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;연쇄 명령 vs 맞춤형 프로그램(custom program)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1761463889490&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;counts = Hash.new(0)

File.open('...') do |file|
    file.each do |line|
        url = line.split[6]
        counts[url] += 1
    end
end

top5 = counts.map(|url, count| [count, url] }.sort.reverse[0...5]
top5.each(|count, url| puts &quot;#{count} #{url}&quot; }&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;unix 연쇄 명령 대신 위와 같이 작성할 수도 있음.&lt;/li&gt;
&lt;li&gt;하지만 두 가지 방법은 실행 흐름이 크게 다름. (특히 대용량 파일을 분석해 보면 차이가 확 드러남)&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;정렬 vs 인메모리 집계&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;작업 세트가 충분히 작다면 임메모리 해시 테이블도 잘 동작함&lt;/li&gt;
&lt;li&gt;여기서 작업 세트는 단순히 고유 URL 수로 결정 (ex. [url: ~~, count: 10])&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정렬
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;허용 메모리 &amp;lt; 작업 세트&quot;의&lt;/b&gt; 경우 좋음&lt;/li&gt;
&lt;li&gt;접근법은 SS 테이블과 LSM 트리에서 설명한 원리와 다르지 않음.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  1-2. Unix 철학&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;아이디어&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;다른 방법으로 데이터 처리가 필요할 때 정원 호스와 같이 &lt;b&gt;여러 다른 프로그램을 연결하는 방법이 필요&lt;/b&gt;하다&quot;&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;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;철학&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SRP&lt;/li&gt;
&lt;li&gt;작은 프로그램들을 조합해서 더 큰 일을 하게 만들라&lt;/li&gt;
&lt;li&gt;소프트웨어를 빠르게 써볼 수 있게 설계 &amp;amp; 구축&lt;/li&gt;
&lt;li&gt;프로그래밍 작업을 줄이려면 도구를 써라&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;동일(uniform) 인터페이스&lt;/blockquote&gt;
&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;해야 함.&lt;/li&gt;
&lt;li&gt;unix에서 인터페이스는 &lt;b&gt;파일(파일 디스크립터)&lt;/b&gt;
&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;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;unix 입장에서 읽고 쓸 수 있으면 다 파일임.(소켓, 드라이버 등등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 파일 디스크립터 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➔ 커널이 관리하는 파일/소켓/파이프 등에 대한 &lt;b&gt;식별자 번호&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 88px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 20.1465%;&quot;&gt;번호(fd)&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 26.7399%;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 37.4847%;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 15.6288%;&quot;&gt;방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 20.1465%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 26.7399%;&quot;&gt;stdin&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 37.4847%;&quot;&gt;표준 입력&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 15.6288%;&quot;&gt;입력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 20.1465%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 26.7399%;&quot;&gt;stdout&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 37.4847%;&quot;&gt;표준 출력&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 15.6288%;&quot;&gt;출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 20.1465%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 26.7399%;&quot;&gt;stderr&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 37.4847%;&quot;&gt;표준 에러 출력&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 15.6288%;&quot;&gt;출력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1761465402438&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;hello&quot; &amp;gt; out.txt   # stdout(1)을 out.txt로 연결
cat &amp;lt; input.txt          # stdin(0)을 input.txt로 연결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;&amp;nbsp;➔ 리다이렉션(&amp;gt;, &amp;lt;)이 바로 &lt;b&gt;파일 디스크립터 번호를 재연결&lt;/b&gt;하는 행위&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터의 발칸화(Balkanization)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터넷이 고립된 여러 개의 섬처럼 나뉘어 있는 현상이나, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 프로그램 언어나 데이터 파일 포맷 등이 분화발전하는 것을 의미하기도 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로직과 연결의 분리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그니깐 unix는 stdin, stdout을 통해서 파일을 읽고 쓰는 역할에만 집중
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(Default) stdin은 키보드로부터, stout은 화면으로 출력&lt;/li&gt;
&lt;li&gt;`cat &amp;lt; input.txt &amp;gt; output.txt` 이렇게 하면 파일에서 입력 가져와서, 파일로 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pipe의 역할
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;한 프로세스의 stdout을 다른 프로세스의 stdin과 연결함.&lt;/li&gt;
&lt;li&gt;`grep &quot;ERROR&quot; &amp;lt; input.txt | sort |uniq -c &amp;gt; output.txt`&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파일 내용을 stdin으로 리다이렉트함&lt;/li&gt;
&lt;li&gt;stdin으로 받은 데이터에서 필터링 수행&lt;/li&gt;
&lt;li&gt;sort, uniq 작업하고 output에 쓰기(리다이렉트) 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이때 데이터는 디스크에 쓰지 않고, 인메모리 버퍼에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;sort
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;오로지 입력 스트림을 받아서 정렬하는 역할만 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;stdin과 stdout을 사용할 때의 제약
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;투명성과 실험 (유닉스 도구가 성공적인 이유)&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;유닉스 명령에 들어가는 입력 파일은 일반적으로 불변으로 처리한다.&lt;/li&gt;
&lt;li&gt;원하는 형태의 출력이 나오는지 언제든 확인 가능하다.&lt;/li&gt;
&lt;li&gt;특정 파이프라인 단계의 출력을 파일에 쓰고, 다음에 입력으로 사용할 수 있음.&lt;/li&gt;
&lt;/ol&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 2. 맵리듀스와 분산 파일 시스템&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵리듀스&lt;/blockquote&gt;
&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;의 파일을 입력과 출력으로 사용하고,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 대용량 데이터셋을 처리하는 코드를 작성하는 &lt;b&gt;프로그래밍 프레임워크&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;병렬로 수행하는 코드를 직접 작성하지 않고도 여러 장비에서 병렬로 처리가 가능함.&lt;/li&gt;
&lt;li&gt;매우 불친절하고 brute-force 방식이지만, 엄청 효율적인 도구임&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;blockquote data-ke-style=&quot;style2&quot;&gt;HDFS(Hadoop Distributed File System)&lt;/blockquote&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;비공유 원칙을 기반으로 함(NAS, SAN과 반대)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일반적인 데이터센터 네트워크에 연결된 네트워크면 충분함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TODO&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-1. 맵리듀스 작업 실행하기&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터 처리 패턴&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력형식 파서를 사용해서 입력 파일 읽은 후, 레코드로 쪼갠다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;[사용자가 작성한 코드]&lt;/span&gt; 각 입력 레코드마다 매퍼 함수를 호출해 키와 값을 추출한다.&lt;/li&gt;
&lt;li&gt;키를 기준으로 key-value 쌍을 모두 정렬한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;[사용자가 작성한 코드]&lt;/span&gt; 정렬된 key-value 쌍을 대상으로 reduce 함수를 호출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;콜백함수(mapper, reducer)&lt;br /&gt;&lt;i&gt;➔ 맵리듀스 작업을 생성하려면 구현해야 함.&lt;/i&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;b&gt;키와 값을 추출&lt;/b&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;맵리듀스 프레임워크는, 같은 키를 가진 레코드를 모으고 해당 값의 집합을 반복해 리듀서 함수를 호출함.&lt;/li&gt;
&lt;li&gt;리듀서는 &lt;b&gt;출력 레코드를 생성&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵리듀스의 분산 실행&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스 프레임워크가 장비 간 데이터 이동하는 부분을 처리하기 때문에&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; mapper와 reducer는 한 번에 하나의 레코드만 처리하는 것에 집중하면 됨.&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;blockquote data-ke-style=&quot;style2&quot;&gt;하둡 맵리듀스 작업에서의 데이터 플로우&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRHgUd/dJMb9gRtbKR/aUX4VyWFjrEvzXG2XNtzKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRHgUd/dJMb9gRtbKR/aUX4VyWFjrEvzXG2XNtzKk/img.png&quot; data-alt=&quot;그림10-1. 매퍼3개와 리듀서 3개로 구성된 맵리듀스 작업&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRHgUd/dJMb9gRtbKR/aUX4VyWFjrEvzXG2XNtzKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRHgUd%2FdJMb9gRtbKR%2FaUX4VyWFjrEvzXG2XNtzKk%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;1112&quot; height=&quot;730&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림10-1. 매퍼3개와 리듀서 3개로 구성된 맵리듀스 작업&lt;/figcaption&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;작업 입력으로 HDFS 상의 &lt;b&gt;디렉터리&lt;/b&gt;를 사용하는 것이 일반적임.&lt;/li&gt;
&lt;li&gt;&quot;입력 디렉터리 내 각 파일 or 파일 블록&quot;을 &lt;b&gt;독립된 `맵 태스크`에서 처리할 독립 파티션으로 간주&lt;/b&gt;
&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;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 입력 파일은 보통 그 크기가 엄청 큼&lt;/li&gt;
&lt;li&gt;맵리듀서의 스케줄러는 '복제본을 가지고 있는 노드'에 메모리와 CPU 자원의 여유가 있다면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 데이터를 네트워크로 옮기기보다, 데이터가 &lt;b&gt;이미 저장된 서버에서 바로 연산을 실행&lt;/b&gt;하려고 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 데이터 가까이에서 연산하기 원리 적용&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;1. 매퍼&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 파일을 읽어서 (key, value) 쌍으로 변환&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;2. 파티셔닝&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;791&quot; data-start=&quot;685&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;685&quot;&gt;매퍼가 생성한 (key, value) 쌍은
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;685&quot;&gt;여러 리듀서 중 어느 리듀서로 갈지를 &lt;b&gt;키의 해시값(hash)&lt;/b&gt; 으로 결정&lt;/li&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;685&quot;&gt;예: hash(key) % 리듀서 수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;685&quot;&gt;즉, 같은 키를 가진 모든 데이터는 반드시 &lt;b&gt;같은 리듀서&lt;/b&gt;&lt;span style=&quot;caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로 이동하게 됨.&lt;/span&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;3. 셔플 &amp;amp; 정렬&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1015&quot; data-start=&quot;929&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;976&quot; data-start=&quot;929&quot;&gt;같은 키 끼리 모으는 과정
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;976&quot; data-start=&quot;929&quot;&gt;&lt;b&gt;각 매퍼의 출력(key-value 쌍)&lt;/b&gt; 을 키 기준으로 정렬 (Sort)&lt;/li&gt;
&lt;li data-end=&quot;1000&quot; data-start=&quot;977&quot;&gt;&lt;b&gt;같은 키끼리 묶음(Group)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1015&quot; data-start=&quot;1001&quot;&gt;&lt;b&gt;리듀서로 전송&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1015&quot; data-start=&quot;1001&quot;&gt;즉, m1, m2, m3 ...의 결과를 &lt;b&gt;리듀서별로 분배&lt;/b&gt;&lt;span style=&quot;caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하는 과정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 리듀서&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;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터 가까이에서 연산하기 (putting the computation near the data)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 원리를 적용하면
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;네트워크를 통해 입력 파일을 복사하는 부담 감소&lt;/li&gt;
&lt;li&gt;네트워크 부하 감소&lt;/li&gt;
&lt;li&gt;지역성 증가&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵 리듀스 워크플로&lt;/blockquote&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;일반적으로 n개의 맵리듀스 작업을 연결해 workflow로 구성해서 사용함.&lt;/li&gt;
&lt;li&gt;하둡 맵리듀스 프레임워크의 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;워크플로를 제공해주지 않기 때문에 작업은 &lt;b&gt;디렉터리 이름&lt;/b&gt;을 통해 암묵적으로 연결됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;연결된 맵리듀스 작업은 유닉스의 명령 파이프라인과 유사하진 않음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스케쥴러&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일괄 처리 작업의 출력은 작업이 성공적으로 끝났을 때만 유효함.&lt;/li&gt;
&lt;li&gt;따라서 워크플로 상에서 선행 작업의 입력 디렉터리를 생성하는 작업이 끝나야 다음 작업을 할 수 있음.&lt;/li&gt;
&lt;li&gt;하둡 맵리듀스 작업 간 수행 의존성을 관리하기 위해 다양한 스케쥴러가 개발됨.&lt;/li&gt;
&lt;li&gt;스케쥴러가 있어서 유지보수할 때 유용함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-2. 리듀스 사이드 조인과 그룹화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 맵리듀스에는 일반적으로 이야기하는 색인 개념이 없다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사용자 활동 이벤트 분석 예제&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1090&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v7FWK/dJMb9NPmtzZ/tdlKRhMBXEQ29vlWwXKCI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v7FWK/dJMb9NPmtzZ/tdlKRhMBXEQ29vlWwXKCI1/img.png&quot; data-alt=&quot;그림 10-2. 사용자 활동 이벤트 로그와 사용자 데이터베이스 간 조인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v7FWK/dJMb9NPmtzZ/tdlKRhMBXEQ29vlWwXKCI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7FWK%2FdJMb9NPmtzZ%2FtdlKRhMBXEQ29vlWwXKCI1%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;1090&quot; height=&quot;468&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1090&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 10-2. 사용자 활동 이벤트 로그와 사용자 데이터베이스 간 조인&lt;/figcaption&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;위에서 학습했든, 일괄 처리에서 처리량을 높이기 위해서는 같은 장비에서 연산을 해야 한다.&lt;/li&gt;
&lt;li&gt;그래서 이 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;'사용자 데이터베이스'의 사본을 가져와 '사용자 활동 이벤트 로그'가 저장된 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 분산 파일 시스템에 넣는 방법을 고려해 볼 수 있음.&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;blockquote data-ke-style=&quot;style2&quot;&gt;정렬 병합 조인&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLfAyV/dJMb9Qyyqmp/OlZn9zGJetKOKDKn9OK291/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLfAyV/dJMb9Qyyqmp/OlZn9zGJetKOKDKn9OK291/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLfAyV/dJMb9Qyyqmp/OlZn9zGJetKOKDKn9OK291/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLfAyV%2FdJMb9Qyyqmp%2FOlZn9zGJetKOKDKn9OK291%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;1410&quot; height=&quot;592&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한 매퍼는 활동 이벤트를 훑어 사용자 ID를 키로, 활동 이벤트를 값으로 추출
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;[사용자 ID : 활동 이벤트]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다른 매퍼는 사용자 데이터베이스를 훑어 사용자 ID를 키도 사용자 생일을 값으로 추출
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;[사용자 ID : 생일]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그리고 key로 매퍼의 출력을 파티셔닝해 k-v 쌍으로 정렬하면
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;같은 사용자의 활동 이벤트와 사용자 레코드는 리듀서의 입력으로 서로 인접해서 들어감.&lt;/li&gt;
&lt;li&gt;매퍼가 생성한 &lt;b&gt;키&lt;/b&gt;는 값을 &lt;b&gt;보낼 목적지의 주소 역할&lt;/b&gt;을 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보조 정렬
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;맵리듀스에서 작업 레코드를 재배열하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보조 정렬 이후 실제 조인 수행
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;보조 정렬했기 때문에, 첫 번째 값은 항상 생년월일 레코드&lt;/li&gt;
&lt;li&gt;리듀서는 지역 변수에 생년월일 저장하고,&amp;nbsp;&lt;b&gt;{url : 연령}&lt;/b&gt; 쌍을 출력함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이렇게 하면
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;리듀서는 특정 사용자 ID의 모든 레코드를 한 번에 처리할 수 있음.&lt;/li&gt;
&lt;li&gt;결과적으로 데이터를 주고받는 네트워크 송수신 과정이 없어도 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;같은 곳으로 연관된 데이터 가져오기&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;아키텍처 이해하기&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&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: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;그룹화&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스의 간단한 그룹화 방법은, 매퍼가 k-v 쌍을 생성할 때 &lt;b&gt;그룹화할 대상을 키로 지정&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;쏠림 다루기 (Handling Skew)&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;같은 키를 가지는 모든 레코드를 같은 장소로 모으는 패턴&lt;/b&gt;은 제대로 동작하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 매퍼와 리듀서가 완전히 끝나야지만 맵리듀스 작업이 끝나기 때문에&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 느린 리듀서가 완료할 때까지 후속 작업 들은 기다려야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;조인 최적화 방법&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Pig: Skewed Join
&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조인의 다른 입력(상대 테이블)은 핫 키 관련 레코드를 해당 리듀서들에 복제해 전달.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;효과: 핫 키 작업을 병렬화.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;비용: 복제 오버헤드&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Crunch: Sharded Join
&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;아이디어는 Pig와 유사하게 랜덤 분산으로 핫 스팟 완화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Hive: Skewed Join 최적화&lt;/span&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;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;집계 최적화&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2단계 그룹핑
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1단계: 핫 키 레코드를 랜덤 리듀서로 보내 각 리듀서가 부분 집계(키별 압축 값) 수행.&lt;/li&gt;
&lt;li&gt;2단계: 모든 부분 집계를 한 번 더 합쳐 키당 최종 값 생성.&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;&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;size18&quot;&gt;  2-3. 맵 사이드 조인&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지금 까지는 리듀스 사이드 조인&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵 사이드 조인&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;브로드캐스트 해시 조인&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;203&quot; data-start=&quot;99&quot;&gt;큰 데이터셋과 작은 데이터셋을 조인할 때 사용하는 &lt;b&gt;가장 단순하고 효율적인 map-side join 방식&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;282&quot; data-start=&quot;204&quot;&gt;작은 데이터셋을 모든 매퍼에 브로드캐스트(복제)하여, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 각 매퍼가 메모리에 로드한 후 &lt;b&gt;해시 테이블(작은 데이터 셋) 기반으로 조인&lt;/b&gt;을 수행&lt;/li&gt;
&lt;li data-end=&quot;282&quot; data-start=&quot;204&quot;&gt;인메모리 해시 테이블로 적재하는 대신 로컬 디스크에 읽기 전용 색인으로 저장하기도 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;파티션 해시 조인&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양쪽 입력 데이터(예: 사용자 DB, 활동 로그)가 같은 기준(key)과 같은 해시 함수로 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 같은 개수의 파티션으로 나뉘어 있을 경우, 각 파티션별로 독립적으로 해시 조인(Hash Join)을 수행할 수 있다.&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 data-end=&quot;402&quot; data-start=&quot;349&quot;&gt;사용자 ID가 3으로 끝나는 &lt;b&gt;사용자 데이터&lt;/b&gt;를 메모리에 로드(해시 테이블 생성)&lt;/li&gt;
&lt;li data-end=&quot;440&quot; data-start=&quot;405&quot;&gt;사용자ID가 3으로 끝나는 &lt;b&gt;활동 로그&lt;/b&gt;를 스캔&lt;/li&gt;
&lt;li data-end=&quot;440&quot; data-start=&quot;405&quot;&gt;제대로 파티션 됐다면, 조인할 레코드가 모두 같은 번호의 파티션에 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;440&quot; data-start=&quot;405&quot;&gt;이 방법은 각 매퍼의 해시 테이블에 적재해야 할 데이터의 양을 줄일 수 있다는 장점이 있음.&lt;/li&gt;
&lt;li data-end=&quot;440&quot; data-start=&quot;405&quot;&gt;하이브에서는 '버킷 맵 조인'이라고 부름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵 사이드 병합 조인&lt;/blockquote&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;br /&gt;&amp;nbsp; &amp;nbsp; 입력 크기가 메모리에 적재 가능한지 고려할 필요가 없음.&lt;/li&gt;
&lt;li&gt;이 방식이 가능하다면 선행 맵리듀스 작업이 이미 파티셔닝 &amp;amp; 정렬을 해놨다는 뜻.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;맵 사이드 조인을 사용하는 맵리듀스 워크플로&lt;/blockquote&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;작은 데이터셋은 브로드캐스트(join)하거나, 동일한 파티션으로 분할되어야 한다.&lt;/li&gt;
&lt;li&gt;하둡 생태계에서는 데이터셋 파티셔닝 관련 메타데이터를 관리하는 H카탈로그나 하이브 메타스토어를 사용하기도 함.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-4. 일괄 처리 워크플로의 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 일괄 처리는 입력 데이터셋 부분을 스캔하는 것이 일반적이라 분석에 더 가까움.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;검색 색인 구축&lt;/blockquote&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;정해진 문서 집합을 대상으로 full-text 검색이 필요하다면 일괄 처리가 효율적임
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;매퍼는 필요에 따라 문서 집합을 파티셔닝 하고 각 리듀서가 해당 파티션에 대한 색인을 구축한다.&lt;/li&gt;
&lt;li&gt;그리고 색인은 분산 파일 시스템에 저장됨.&lt;/li&gt;
&lt;/ol&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;일괄 처리의 출력으로 키-값 저장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배치 프로세스의 출력을 웹 애플리케이션이 질의하는 DB로 보내는 방법이 있을까?&lt;/li&gt;
&lt;li&gt;가장 심플한 방법 : 직접 매퍼/리듀서 내에서 선호하는 DB로 요청 보내는 것&lt;/li&gt;
&lt;li&gt;근데 좋은 방법은 아님. 왜?
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일단 일괄 처리 태스크는 데이터양이 많아서 레코드마다 네트워크 요청을 하면 성능 떨어짐&lt;/li&gt;
&lt;li&gt;외부에 출력을 생성하게 되면, 맵리듀스 작업의 실패&amp;amp;재시도 과정이 노출됨.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;그럼 더 좋은 방법은?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일괄 처리 작업 &lt;b&gt;내부에 완전히 새로운 DB를 구축&lt;/b&gt;해 분산 파일 시스템의 작업 출력 디렉터리에 저장&lt;/li&gt;
&lt;li&gt;이때 데이터 파일은 한 번 기록되면 불변이고 서버에 bulk로&amp;nbsp; 적재해 읽기 전용 질의를 처리할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터베이스 파일을 생성하는 작업은 굉장히 좋은 맵리듀스 활용법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;일괄 처리 출력에 관한 철학&lt;br /&gt;&lt;i&gt;➔ 일괄 처리의 철학은 &amp;ldquo;입력은 불변, 출력은 완전히 새로 생성&amp;rdquo;이라는 유닉스 철학에서 비롯됨.&lt;/i&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;845&quot; data-start=&quot;414&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;526&quot; data-start=&quot;414&quot;&gt;재실행이 용이함
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;526&quot; data-start=&quot;433&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;472&quot; data-start=&quot;433&quot;&gt;코드 오류나 실패 시 입력 데이터만 유지하면 재실행으로 복구 가능.&lt;/li&gt;
&lt;li data-end=&quot;526&quot; data-start=&quot;476&quot;&gt;잘못된 데이터가 DB에 기록되지 않아 &amp;ldquo;사람의 실수(human fault)&amp;rdquo;에도 안전.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;640&quot; data-start=&quot;527&quot;&gt;되돌릴 수 있는 구조
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;640&quot; data-start=&quot;549&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;614&quot; data-start=&quot;549&quot;&gt;결과를 쉽게 되돌릴 수 있게 설계하여 &amp;ldquo;비가역성 최소화(minimizing irreversibility)&amp;rdquo; 실현.&lt;/li&gt;
&lt;li data-end=&quot;640&quot; data-start=&quot;618&quot;&gt;애자일 개발 및 실험적 개발에 적합.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;641&quot;&gt;맵리듀스의 내결함성(fault tolerance)
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;749&quot; data-start=&quot;679&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;705&quot; data-start=&quot;679&quot;&gt;실패한 태스크는 동일 입력으로 자동 재시도.&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;709&quot;&gt;여러 번 실패 시 프레임워크가 해당 출력 폐기 후 작업 실패로 처리.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;845&quot; data-start=&quot;750&quot;&gt;입력-출력의 명확한 분리
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;845&quot; data-start=&quot;774&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;817&quot; data-start=&quot;774&quot;&gt;코드 수정, 모니터링, 디버깅 시 외부 부수 효과가 없으므로 단순함 유지.&lt;/li&gt;
&lt;li data-end=&quot;845&quot; data-start=&quot;821&quot;&gt;출력의 품질은 입력과 코드만으로 결정됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;size18&quot;&gt;  2-5. 하둡과 분산 데이터베이스의 비교&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;HDFS는 파일 시스템이고 맵리듀스는 특별한 방식으로 구현된 유닉스 프로세스다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;저장소의 다양성&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;270&quot; data-start=&quot;208&quot;&gt;전통적인 DB는 특정 모델(관계형, 문서형 등)에 맞게 데이터를 구조화해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;371&quot; data-start=&quot;271&quot;&gt;반면 HDFS는 훨씬 유연해서, 어떤 형태의 데이터든 그대로 저장 가능함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;371&quot; data-start=&quot;271&quot;&gt;텍스트, 이미지, 비디오, 센서 데이터, 시퀀스 등 다양한 형태를 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;495&quot; data-start=&quot;373&quot;&gt;하둡은 &lt;b&gt;데이터를 먼저 저장하고 나중에 해석하는(dump first, process later)&lt;/b&gt;&amp;nbsp;방식&lt;/li&gt;
&lt;li data-end=&quot;495&quot; data-start=&quot;373&quot;&gt;MPP DB는 데이터를 저장하기 전에 이미 &lt;b&gt;정해진 스키마에 맞게 가공&lt;/b&gt;해야 함.&lt;/li&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1141&quot;&gt;하둡은 데이터를 저장할 때 스키마를 강제하지 않음. 대신 데이터를 읽을 때 스키마를 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1141&quot;&gt;schema-on-read&lt;/li&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1141&quot;&gt;데이터 해석의 책임은 소비자에게 있다.&lt;/li&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1141&quot;&gt;이는 &lt;b&gt;초밥 원리(sushi principle)&lt;/b&gt;로 부름 - 데이터는 원시(raw) 상태가 더 좋다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1529&quot; data-start=&quot;1362&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1362&quot;&gt;하둡은 ETL의 중간 저장소 역할로 자주 사용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1475&quot; data-start=&quot;1401&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1436&quot; data-start=&quot;1401&quot;&gt;트랜잭션 처리 시스템 &amp;rarr; 원시 데이터 덤프 (하둡 저장)&lt;/li&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1439&quot;&gt;이후 맵리듀스 작업은 관계형 형태로 데이터를 정제하고, 분석을 위해 MPP 데이터웨어하우스로 이동&lt;/li&gt;
&lt;li data-end=&quot;1529&quot; data-start=&quot;1476&quot;&gt;즉, &lt;b&gt;수집 단계(하둡)&lt;/b&gt; 와 &lt;b&gt;분석 단계(MPP DB)&lt;/b&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;처리 모델의 다양성&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MPP(Massively Parallel Processing) 데이터베이스에 대해서
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;353&quot; data-start=&quot;236&quot;&gt;MPP DB는 &lt;b&gt;일체형&lt;/b&gt; 구조로&amp;nbsp;디스크 저장소, 쿼리 최적화, 스케줄링, 실행 엔진이 긴밀하게 통합되어 있음.&lt;/li&gt;
&lt;li data-end=&quot;416&quot; data-start=&quot;354&quot;&gt;데이터베이스의 특성과 쿼리 유형에 맞게 &lt;b&gt;튜닝이 최적화&lt;/b&gt;되어 있어 매우 좋은 성능을 낼 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;508&quot; data-start=&quot;417&quot;&gt;SQL 언어를 통해 복잡한 처리를 코드 작성 없이 수행할 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;508&quot; data-start=&quot;417&quot;&gt;Tableau 같은 BI(비즈니스 인텔리전스) 도구로 시각화하기 쉬움.&lt;/li&gt;
&lt;li data-end=&quot;549&quot; data-start=&quot;509&quot;&gt;즉, &lt;b&gt;분석용 쿼리&lt;/b&gt;와 &lt;b&gt;정형 데이터 처리&lt;/b&gt;에는 매우 효율적.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;549&quot; data-start=&quot;509&quot;&gt;SQL 기반 처리의 한계
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;580&quot;&gt;SQL은 &lt;b&gt;모든 종류의 처리에는 적합하지 않다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;614&quot;&gt;머신러닝, 추천 시스템, 자연어 처리 등은 '통계적/비정형 처리'가 필요하여 SQL로 표현하기 어려움.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;이러한 비정형 처리에는 &lt;b&gt;전용 애플리케이션 모델&lt;/b&gt;이나 &lt;b&gt;코드 기반 접근&lt;/b&gt;이 필요함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;맵리듀스를 이용하면...
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;엔지니어가 직접 작성한 코드를 대규모 데이터셋에 대해 실행할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하이브처럼 &lt;b&gt;HDFS 위에 SQL 실행 엔진을 추가&lt;/b&gt;할 수도 있음.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;즉, SQL과 코드 기반 처리를 &lt;b&gt;혼합 활용&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;시간이 지나면서...
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하나의 처리 모델로는 모든 작업을 해결하기 어렵다는 점이 드러났다.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하둡은 이를 해결하기 위해 여러 형태의 처리 모델을 동시에 지원할 수 있도록 발전
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;맵리듀스, SQL, 스트리밍 등이 있다고 함. (맵 리듀스를 너머에서 자세히 다룰 예정)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하둡의 &lt;b&gt;플랫폼 개방성&lt;/b&gt; 덕분에 기존 MPP 데이터베이스보다 훨씬 다양한 처리 방식을 수용 가능하게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하둡 생태계의 확장
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;하둡 생태계에는 두 가지 대표적 데이터베이스가 존재
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;&lt;b&gt;HBase&lt;/b&gt; &amp;rarr; OLTP&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;&lt;b&gt;Impala&lt;/b&gt; &amp;rarr; MPP 스타일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;두 시스템 모두 맵리듀스를 사용하진 않지만, HDFS를 저장소로 사용한다.&lt;/li&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;둘이 통합해서 쓸 수도 있음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;688&quot;&gt;HBase는 빠른 쓰기/조회 중심, Impala는 복잡한 쿼리 중심으로 쓴다고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;빈번하게 발생하는 결함을 줄이는 설계&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스와 MPP 비교 시 가장 두드러지는 차이점
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;결함을 다루는 방식&lt;/li&gt;
&lt;li&gt;메모리 및 디스크를 사용하는 방식&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1114&quot; data-start=&quot;937&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;맵리듀스는...
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;&lt;b&gt;대용량&lt;/b&gt; 데이터 처리에 적합&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;오랜 시간 수행되는 작업에서도 일부 실패 시 &lt;b&gt;재시작이 용이&lt;/b&gt;하다.&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;작업은 &lt;b&gt;태스크 단위&lt;/b&gt;로 나뉘어 &lt;b&gt;병렬 실행&lt;/b&gt;되며, 태스크 하나가 실패해도 전체 작업을 다시 실행하지 않아도 된다.&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;대신 모든 &lt;b&gt;중간 결과를 디스크에 저장&lt;/b&gt;하기 때문에 &lt;b&gt;복구는 빠르지만, 메모리 효율성은 낮음.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;태스크 종료가 예상치 못하게 자주 발생하더라도 견딜 수 있게 설계되어 있음.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&gt;근데 현실 세계에서는 이런 장애가 자주 일어나진 않음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;659&quot;&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;&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;&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;❐ 3. 맵리듀스를 넘어&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;...&lt;/blockquote&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;반면에 맵리듀스 원시 API를 사용해서 복잡한 연산을 구현하는 일은 실제로 매우 어렵고 수고스러움.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-1. 중간상태 구체화 (Materialization of Intermediate State)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;중간 상태 &amp;amp; 구체화&lt;/blockquote&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;보통 한 작업은 같은 팀 내의&amp;nbsp; 다른 특정 작업의 입력으로만 사용됨.&lt;/li&gt;
&lt;li&gt;이 경우에 분산 파일 시스템 상에 있는 파일들을 &lt;b&gt;중간 상태(Intermediate state)&lt;/b&gt;라고 함.&lt;/li&gt;
&lt;li&gt;그리고 중간 상태를 파일로 기록하는 작업을 &lt;b&gt;구체화(materialization)&lt;/b&gt;라고 함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;unix의 파이프는 중간 상태를 구체화하진 않고, 인메모리 버퍼에 &lt;b&gt;스트리밍(streaming)&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;중간 상태를 구체화하는 맵리듀스 접근 방식의 단점&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스는 선행작업이 완료되어야만 후행작업을 할 수 있음. &amp;rarr; 워크플로 전체 수행 시간이 느려짐&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;blockquote data-ke-style=&quot;style2&quot;&gt;데이터플로 엔진&lt;/blockquote&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;b&gt;하나로 다룸.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그리고 이 엔진들은 여러 처리 단계를 통해 데이터 흐름을 명시적으로 모델링하기 때문에&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 데이터플로 엔진이라고 부름&lt;/li&gt;
&lt;li&gt;데이터플로 엔진은
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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;i&gt;연산자 : 맵과 리듀스를 번갈아 수행하는 규칙을 지키지 않아도 되는 함수&lt;/i&gt;&lt;/li&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;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;내결함성&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템에서 중간 상태를 모두 구체화할 때 챙길 수 있는 이점 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;b&gt;내구성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;아무래도 모든 중간상태를 다 가지고 있으니깐 쉽게 내결함성을 보장함.&lt;/li&gt;
&lt;li&gt;중간 상태를 사용하지 않는 애들 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 스파크, 플링크, 테즈
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그럼 어떻게 내결함성을 보장할까?&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 유효한 데이터로부터 계산을 다시 해서 복구함 &lt;i&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(항상 정답은 아님!)&lt;/span&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;근데 이렇게 하려면 데이터가 어떻게 연산됐는지 추적을 해야 함.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;스파크의 경우에는 RDD(Resilient distributed dataset) 추상화를 사용함.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;플링크는 연산자 상태를 체크포인트로 남김.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;데이터를 재연산할 때 중요한 점은? &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;b&gt;연산의 결정적(&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;deter&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ministic&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;) 여부 == 멱등성&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;물론 비결정적 연산도 있기 때문에, 원인을 제거해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-2. 그래프와 반복 처리&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;페이지랭크&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;알고리즘 : 이형적 폐쇄(transitive closure)&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh9RVK/dJMcacuB8WY/bk7uxdwyB5bOZ1F5QbVbKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh9RVK/dJMcacuB8WY/bk7uxdwyB5bOZ1F5QbVbKk/img.png&quot; data-alt=&quot;그림2.6 - 데이터베이스에 포함된 북미 지역의 모든 위치 목록을 만드는 예제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh9RVK/dJMcacuB8WY/bk7uxdwyB5bOZ1F5QbVbKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh9RVK%2FdJMcacuB8WY%2Fbk7uxdwyB5bOZ1F5QbVbKk%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;669&quot; height=&quot;261&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림2.6 - 데이터베이스에 포함된 북미 지역의 모든 위치 목록을 만드는 예제&lt;/figcaption&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;반복적인 스타일로 구현하는 이유&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스에는 &quot;완료할 때 까지 반복&quot;이라는 개념이 없음.&lt;/li&gt;
&lt;li&gt;왜냐면 맵리듀스는 데이터를 일회성으로만 처리하기 때문임.&lt;/li&gt;
&lt;li&gt;접근법
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;외부 스케줄러가 이 알고리즘의 한 단계를 연산하기 위해 일괄 처리를 수행함.&lt;/li&gt;
&lt;li&gt;해당 일괄 처리가 완료되면, 스케줄러는 종료 조건을 기반으로 완료됐는지 확인함.&lt;/li&gt;
&lt;li&gt;아직 끝나지 않았다면 스케줄러는 1단계로 돌아가서 다음 일괄 처리를 수행한다.&lt;/li&gt;
&lt;/ol&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;blockquote data-ke-style=&quot;style2&quot;&gt;프리글 처리 모델&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벌크 동기식 병렬(Bulk synchronous parallel, BSP) 연산 모델
&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;구현체 : 아파치 지라프, 스파크 그래프 X API, 플링크 젤리 등.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;내결함성&lt;/blockquote&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;병렬 실행&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;219&quot; data-start=&quot;51&quot;&gt;정점의 실행 위치와 메시지 전달
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;219&quot; data-start=&quot;79&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;146&quot; data-start=&quot;79&quot;&gt;정점은 특정 장비에서 실행될 필요가 없으므로, 메시지를 보낼 때 &lt;b&gt;정점 ID&lt;/b&gt;를 사용해 다른 정점으로 전달함.&lt;/li&gt;
&lt;li data-end=&quot;219&quot; data-start=&quot;150&quot;&gt;그래프를 어떤 장비에 분할(파티셔닝)하고, 메시지를 어떻게 라우팅 할지를 &lt;b&gt;프리글(Pre-gel) 프레임워크&lt;/b&gt;가 담당.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;384&quot; data-start=&quot;221&quot;&gt;그래프 파티셔닝 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;384&quot; data-start=&quot;243&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;295&quot; data-start=&quot;243&quot;&gt;일반적으로 &lt;b&gt;통신이 자주 발생하는 정점끼리&lt;/b&gt; 같은 장비에 위치시키는 것이 이상적이지만,&lt;/li&gt;
&lt;li data-end=&quot;350&quot; data-start=&quot;299&quot;&gt;실제로는 복잡하므로 단순히 &lt;b&gt;임의로 부여된 정점 ID 기준&lt;/b&gt;으로 나누는 경우가 많다.&lt;/li&gt;
&lt;li data-end=&quot;384&quot; data-start=&quot;354&quot;&gt;즉, 관련성이 높은 정점끼리 묶이지 않을 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;555&quot; data-start=&quot;386&quot;&gt;통신 오버헤드 문제
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;555&quot; data-start=&quot;407&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;442&quot; data-start=&quot;407&quot;&gt;분산 환경에서는 장비 간 통신(메시지 전달) 오버헤드가 큼.&lt;/li&gt;
&lt;li data-end=&quot;499&quot; data-start=&quot;446&quot;&gt;특히 &lt;b&gt;중간 상태의 메시지&lt;/b&gt;가 많아지면 원본 그래프보다 통신 비용이 훨씬 커질 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;555&quot; data-start=&quot;503&quot;&gt;네트워크 메시지 전송으로 인해 &lt;b&gt;분산 그래프 알고리즘의 성능 저하&lt;/b&gt;가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;756&quot; data-start=&quot;557&quot;&gt;단일 장비 처리의 이점
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;756&quot; data-start=&quot;580&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;642&quot; data-start=&quot;580&quot;&gt;그래프가 &lt;b&gt;단일 컴퓨터 메모리에 들어갈 만큼 작다면&lt;/b&gt;, 분산보다 단일 장비에서 처리하는 게 훨씬 효율적.&lt;/li&gt;
&lt;li data-end=&quot;681&quot; data-start=&quot;646&quot;&gt;심지어 단일 스레드로 실행하더라도 성능이 더 좋을 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;756&quot; data-start=&quot;685&quot;&gt;단일 장비에서 처리할 수 없는 큰 그래프의 경우에는 &lt;b&gt;GraphChi&lt;/b&gt; 같은 단일 장비용 그래프 프레임워크 사용도 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;891&quot; data-start=&quot;758&quot;&gt;결론
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;891&quot; data-start=&quot;771&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;842&quot; data-start=&quot;771&quot;&gt;그래프가 너무 커서 단일 장비 메모리에 담을 수 없다면, &lt;b&gt;프리글(Pre-gel)&lt;/b&gt; 같은 &lt;b&gt;분산 접근법&lt;/b&gt;을 써야 함.&lt;/li&gt;
&lt;li data-end=&quot;891&quot; data-start=&quot;846&quot;&gt;병렬 그래프 알고리즘의 효율적인 실행은 여전히 &lt;b&gt;연구가 진행 중인 분야&lt;/b&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;&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;size18&quot;&gt;  3-3. 고수준 API와 언어&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;배경&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;231&quot; data-start=&quot;76&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;157&quot; data-start=&quot;76&quot;&gt;&lt;b&gt;MapReduce&lt;/b&gt;가 대규모 데이터를 처리할 수 있게 되면서 분산 일괄 처리 기술이 성숙함.&lt;/li&gt;
&lt;li data-end=&quot;231&quot; data-start=&quot;158&quot;&gt;물리적인 인프라 문제는 해결되었지만, &lt;b&gt;프로그래밍 모델&lt;/b&gt;은 여전히 복잡했음
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;231&quot; data-start=&quot;158&quot;&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;blockquote data-end=&quot;257&quot; data-start=&quot;238&quot; data-ke-style=&quot;style2&quot;&gt;고수준 API의 등장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;537&quot; data-start=&quot;258&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;333&quot; data-start=&quot;258&quot;&gt;하이브, 피그, 캐스캐이딩, 크런치 등의 &lt;b&gt;고수준 언어&amp;middot;API&lt;/b&gt;가 등장함.&lt;/li&gt;
&lt;li data-end=&quot;406&quot; data-start=&quot;334&quot;&gt;이들은 개발자가 직접 맵리듀스 코드를 작성하지 않고도 &lt;b&gt;데이터플로우 방식&lt;/b&gt;으로 일괄 처리 수행할 수 있게 함.&lt;/li&gt;
&lt;li data-end=&quot;463&quot; data-start=&quot;407&quot;&gt;스파크, 플링크도 이 계보에 속하며 고수준 데이터플로우 API를 제공함.&lt;/li&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;464&quot;&gt;이 API들은 관계형 스타일의 빌딩 블록을 통해 데이터 조인, 필터링, 그룹화 등의 작업을 표현할 수 있게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;563&quot; data-start=&quot;544&quot; data-ke-style=&quot;style2&quot;&gt;선언형 언어로의 전환&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;812&quot; data-start=&quot;564&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;662&quot; data-start=&quot;564&quot;&gt;기존에는 코드를 명시적으로 작성해 조인을 수행했지만, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 선언형 접근법(declarative approach)은 시스템이 어떤 조인 방식을 쓸지 자동으로 결정함.&lt;/li&gt;
&lt;li data-end=&quot;755&quot; data-start=&quot;663&quot;&gt;하이브, 스파크, 플링크는 &lt;b&gt;비용 기반 최적화(Cost-based optimization)를&lt;/b&gt; 수행해&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 조인 순서나 실행 계획을 스스로 변경하기도 함.&lt;/li&gt;
&lt;li data-end=&quot;812&quot; data-start=&quot;756&quot;&gt;개발자는 모든 알고리즘을 직접 이해할 필요 없이 &quot;무엇을 할지(what)&amp;rdquo;만 정의하면 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;840&quot; data-start=&quot;819&quot; data-ke-style=&quot;style2&quot;&gt;함수형 모델과 코드 실행&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1032&quot; data-start=&quot;841&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;841&quot;&gt;MapReduce와 그 후속 프레임워크는 SQL과 달리 함수형 프로그래밍 모델을 따름.
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;983&quot; data-start=&quot;896&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;938&quot; data-start=&quot;896&quot;&gt;함수 호출(map, reduce)을 이용해 코드를 작성하고 병렬 실행함.&lt;/li&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;941&quot;&gt;파싱, 자연어 처리, 이미지 분석 등에서도 이러한 함수형 패턴이 활용됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1032&quot; data-start=&quot;984&quot;&gt;이미 다양한 &lt;b&gt;통계/수치 계산용 라이브러리&lt;/b&gt;가 존재하며 이를 그대로 활용 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1067&quot; data-start=&quot;1039&quot; data-ke-style=&quot;style2&quot;&gt;임의 코드 실행과 MPP DB의 비교&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1292&quot; data-start=&quot;1068&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1190&quot; data-start=&quot;1068&quot;&gt;MPP(Massively Parallel Processing) 데이터베이스는 일반적으로 &lt;b&gt;SQL 기반&lt;/b&gt;이고,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 일괄 처리 시스템(MapReduce, Spark 등)은 &lt;b&gt;임의 코드 실행 가능성&lt;/b&gt;을 강조함.&lt;/li&gt;
&lt;li data-end=&quot;1292&quot; data-start=&quot;1191&quot;&gt;하지만 최근에는 경계가 점점 모호해져서, &lt;b&gt;MPP DB + 일괄 처리 프레임워크&lt;/b&gt;가 기능적으로 수렴하는 추세.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1318&quot; data-start=&quot;1299&quot; data-ke-style=&quot;style2&quot;&gt;고수준 API의 장점&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1437&quot; data-start=&quot;1319&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1337&quot; data-start=&quot;1319&quot;&gt;생산성과 유지보수성이 높아짐.&lt;/li&gt;
&lt;li data-end=&quot;1360&quot; data-start=&quot;1338&quot;&gt;장비 수준에서도 효율적인 실행 가능.&lt;/li&gt;
&lt;li data-end=&quot;1437&quot; data-start=&quot;1361&quot;&gt;조인 최적화, 코드 벡터화(Vectorization), 내부 루프 최적화 등으로 CPU 캐시 낭비를 줄이고 실행 속도를 향상.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1463&quot; data-start=&quot;1444&quot; data-ke-style=&quot;style2&quot;&gt;다양한 분야로의 확장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1678&quot; data-start=&quot;1464&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1576&quot; data-start=&quot;1464&quot;&gt;일괄 처리 프레임워크는 점차 다양한 분야로 확장됨:
&lt;ul style=&quot;list-style-type: circle;&quot; data-end=&quot;1576&quot; data-start=&quot;1497&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1513&quot; data-start=&quot;1497&quot;&gt;&lt;b&gt;통계&amp;middot;수치 알고리즘&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1547&quot; data-start=&quot;1516&quot;&gt;&lt;b&gt;추천 시스템 / 머신러닝 (예: Mahout)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1576&quot; data-start=&quot;1550&quot;&gt;&lt;b&gt;공간 검색(k-최근접 이웃, KNN)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1635&quot; data-start=&quot;1577&quot;&gt;Mahout은 MapReduce, Spark, Flink 위에서 실행되는 머신러닝 알고리즘 모음.&lt;/li&gt;
&lt;li data-end=&quot;1678&quot; data-start=&quot;1636&quot;&gt;MADlib은 관계형 MPP DB 내부에서 실행되는 유사한 라이브러리임.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/230</guid>
      <comments>https://gilbert9172.tistory.com/230#entry230comment</comments>
      <pubDate>Sun, 26 Oct 2025 17:18:06 +0900</pubDate>
    </item>
    <item>
      <title>Part3. 파생 데이터 (Derived Data)</title>
      <link>https://gilbert9172.tistory.com/229</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 정리&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1부와 2부에서는
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;분산 데이터베이스로 가기 위해 고려해야 할 모든 주요 사항을 밑바닥 부터 다뤘음.&lt;/li&gt;
&lt;li&gt;하지만 1,2부에서는 애플리케이션이 단일 데이터베이스를 사용한다고 가정했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3부에서는
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다양한 특징을 가지는 여러 데이터 시스템을 일관성 있는 &lt;b&gt;하나의 애플리케이션 아키텍처&lt;/b&gt;로 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;통합하는 문제&lt;/b&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;&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;&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;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;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 데이터를 저장하고 처리하는 시스템은 크게 두 분류로 나눌수 있음.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;레코드 시스템&lt;/blockquote&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;진실의 근원(source of truth)&lt;/b&gt;라고도 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;파생 데이터 시스템&lt;/blockquote&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;b&gt;캐시&lt;/b&gt;를 들 수 있음.&lt;/li&gt;
&lt;li&gt;엄밀히 말하자면, 파생 데이터는 &lt;b&gt;중복(rerdundant)&lt;/b&gt;라고 할 수 있음.&lt;/li&gt;
&lt;li&gt;읽기 질의 성능을 높이는 데 종종 필수적 요소임.&lt;/li&gt;
&lt;li&gt;대개 &lt;b&gt;비정규화&lt;/b&gt; 과정을 통해 생성됨 (스테이킹 개발할 때 grossRewardAmount 같은거)&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;&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;&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;❐ 3부 개요&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10장에서는
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(mapReduce와 같은)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;일괄 처리 방식&lt;b&gt;(batch-oriented)&lt;/b&gt; 데이터플로 시스템&lt;/b&gt;을 살펴볼 예정&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대규모 데이터 시스템을 구축하기 위한 &lt;b&gt;원리&lt;/b&gt;가 무엇인지 알아볼 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;11장에서는
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10장과 동일한 아이디어를 데이터 스트림에 적용해볼 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;12장에서는
&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;b&gt;확장 가능&lt;/b&gt;하면 &lt;b&gt;유지보수하기 쉬운&lt;/b&gt; 애플리케이션을 구축하기 위해서&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 앞서 언급한 도구들을 &lt;b&gt;어떻게 사용해야 하는지에 대한 아이디어를 모색&lt;/b&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;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/229</guid>
      <comments>https://gilbert9172.tistory.com/229#entry229comment</comments>
      <pubDate>Sun, 26 Oct 2025 16:03:46 +0900</pubDate>
    </item>
    <item>
      <title>9장. 일관성과 합의 (Consistency and Consensus)</title>
      <link>https://gilbert9172.tistory.com/227</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 0. Description&lt;/h3&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이번장 에서는..&lt;/blockquote&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;그리고 8장에서 설명한 모든 문제가 발생할 수 있다고 가정한다.&lt;/li&gt;
&lt;li&gt;시간은 최선을 다하더라도 근사치 밖에 쓸 수 없다.&lt;/li&gt;
&lt;li&gt;노드는 멈출 수 있고, 언제라도 죽을 수 있다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;내결함성을 지닌 시스템을 구축하는 가장 좋은 방법은?&lt;/blockquote&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;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 대신 신뢰성 있는 공통 추상화 계층을 만들어서, 그 위에 애플리케이션을 올리면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 각 앱은 그 보장(트랜잭션, exactly-once)에 의존해 더 안전하게 동작할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;합의 (Consensus)&lt;/blockquote&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;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. 일관성 보장 (Consistency Guarantees)&lt;/h3&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;복제 데이터베이스&lt;/blockquote&gt;
&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;을 제공&lt;/li&gt;
&lt;li&gt;쓰기를 멈추고 불특정 시간 동안 기다리면 결국 모든 읽기 요청이 같은 값을 반환(&lt;b&gt;수렴&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;하지만 이것은 매우 약한 보장(weak guarantee)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;왜냐면 언제 복제본이 수렴될지에 대한 정보가 하나도 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이러한 최종적 일관성의 엣지 케이스는 &lt;b&gt;시스템에 결함&lt;/b&gt;이 있거나 &lt;b&gt;동시성이 높을 때&lt;/b&gt;만 드러남.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;강한 일관성 모델&amp;nbsp;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞으로 데이터 시스템이 제공할 수 있는 더 강한 일관성 모델에 대해서 살펴 볼 예정
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;선형성(linearlizability)&lt;/li&gt;
&lt;li&gt;이벤트 순서화 문제 (인과성과 전체 순서화와 관련된 문제 검토)&lt;/li&gt;
&lt;li&gt;분산 트랜잭션을 원자적으로 커밋하는 방법&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;강한 보장을 제공하는 시스템은
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;성능이 나쁘거나&lt;/li&gt;
&lt;li&gt;약한 보장이 제공하는 시스템보다 내결함성이 약할 수도&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 2. 선형성 (Linearizability)&lt;/h3&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;아이디어&lt;/blockquote&gt;
&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;만 있는 것처럼 보여주자!&lt;/li&gt;
&lt;li&gt;읽힌 값이 최근에 갱신된 값임을 보장해줌 ➔ 최신성 보장(recency guarantee)&lt;/li&gt;
&lt;li&gt;단점은 모든 복제본에 값이 write 될 때 까지 기다여야 하기 때문에 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성 위반 예시&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;924&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMZnWf/dJMb9j1HJvR/bVIxVv8aW2X4gSuayzBih0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMZnWf/dJMb9j1HJvR/bVIxVv8aW2X4gSuayzBih0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMZnWf/dJMb9j1HJvR/bVIxVv8aW2X4gSuayzBih0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMZnWf%2FdJMb9j1HJvR%2FbVIxVv8aW2X4gSuayzBih0%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;1308&quot; height=&quot;924&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;924&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;/li&gt;
&lt;li&gt;밥의 질의가 오래된 결과를 반환했다는 사실이 선형성 위반&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-1. 시스템에 선형성을 부여하는 것은 무엇인가?(What Makes a System Linearizable?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ &quot;복사본이 하나 뿐인 것처럼 보이게 하자&quot;를 이해하기 위해 예제를 살펴보자.&lt;/i&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;동시에 같은 키(x)를 읽고 쓰는 예시&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qvVvv/dJMb86BieSF/zRXvNQpOQgGIPBHDKKG8f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qvVvv/dJMb86BieSF/zRXvNQpOQgGIPBHDKKG8f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qvVvv/dJMb86BieSF/zRXvNQpOQgGIPBHDKKG8f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqvVvv%2FdJMb86BieSF%2FzRXvNQpOQgGIPBHDKKG8f1%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;1310&quot; height=&quot;430&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;430&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;/li&gt;
&lt;li&gt;결과적으로 A,B는 서로 다른 값을 받을 수도 있다.&lt;/li&gt;
&lt;li&gt;즉, &quot;데이터의 단일 복사본&quot;을 모방하는 시스템에 기대하는 바가 아님!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pT47v/dJMb9NPkbln/xNkbQ5NoGgSCM21EFm0290/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pT47v/dJMb9NPkbln/xNkbQ5NoGgSCM21EFm0290/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pT47v/dJMb9NPkbln/xNkbQ5NoGgSCM21EFm0290/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpT47v%2FdJMb9NPkbln%2FxNkbQ5NoGgSCM21EFm0290%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;1536&quot; height=&quot;502&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;502&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;b&gt;원자적으로 바뀌는 시점&lt;/b&gt;이 있어야 한다고 가정함.&lt;/li&gt;
&lt;li&gt;예시의 경우에는, A가 x를 1로 읽었으면 B도 1로 읽어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecjqYo/dJMb9NV5JjU/xqGA62hBuQZdBYm8A6YluK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecjqYo/dJMb9NV5JjU/xqGA62hBuQZdBYm8A6YluK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecjqYo/dJMb9NV5JjU/xqGA62hBuQZdBYm8A6YluK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecjqYo%2FdJMb9NV5JjU%2FxqGA62hBuQZdBYm8A6YluK%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;1352&quot; height=&quot;658&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;658&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;/li&gt;
&lt;li&gt;선형성의 요구사항은 연산 표시를 모은 선들이 항상 시간순으로 진행돼야 함.&lt;/li&gt;
&lt;li&gt;참고로 위 예시에서 클라이언트B의 마지막 읽기는 선형적이지 않음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 연산은 x를 2에서 4로 갱신하는 C의 cas쓰기와 동시적&lt;/li&gt;
&lt;li&gt;다른 요청이 없으면 2가 맞는데 A가 읽기를 했고, 그 결과가 4&lt;/li&gt;
&lt;li&gt;그렇다면 B도 4를 읽어야 함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;선형성 vs 직렬성&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;b&gt;어떤 순서에 따라 실행&lt;/b&gt;되는 것 처럼 동작하도록 &lt;b&gt;보장&lt;/b&gt;해줌.&lt;/li&gt;
&lt;li&gt;이때 순서가 트랜잭션이 실제로 실행되는 순서와 달라도 상관이 없음.&lt;/li&gt;
&lt;li&gt;ex. 직렬성 스냅숏 격리 ➔ 미리 떠놓은 스냅샷을 읽기 때문에 비선형적&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;b&gt;최신성 보장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;선형성 연산은 트랜잭션으로 묶지 않음 ➔ &quot;write skew&quot; 같은 문제를 막지 못함.&lt;/li&gt;
&lt;li&gt;ex. 2PL, 실제적인 직렬 실행을 기반으로 한 직렬성 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성 + 직렬성&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성과 직렬성의 조합을 &lt;i&gt;&lt;b&gt;'엄격한 직렬성'&lt;/b&gt;&lt;/i&gt; 또는 &lt;i&gt;&lt;b&gt;'강한 단일 복사본 직렬성'&lt;/b&gt;&lt;/i&gt; 이라고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt; 2-2. 선형에 기대기 (Relying on Linearizability)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 어떤 환경에서 선형성이 유용한지 알아보자.&lt;/i&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;잠금과 리더 선출&lt;/blockquote&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;b&gt;무조건 선형적&lt;/b&gt;이여야 함.&lt;/li&gt;
&lt;li&gt;분산 잠금과 리더 선출을 구현하기 위해 코디네이션 서비스가 사용되는데&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 이들은 &lt;b&gt;합의 알고리즘을 사용&lt;/b&gt;해 &lt;b&gt;선형성 연산을 내결함성이 있는 방식으로 구현&lt;/b&gt;함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;제약 조건과 유일성 보장&lt;/blockquote&gt;
&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;하고 싶다면 &lt;b&gt;선형성이 필요&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;채널 간 타이밍 의존성&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rd7vz/dJMb9NV5JMN/ty8WUSX8oKcawktOmiGWpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rd7vz/dJMb9NV5JMN/ty8WUSX8oKcawktOmiGWpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rd7vz/dJMb9NV5JMN/ty8WUSX8oKcawktOmiGWpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frd7vz%2FdJMb9NV5JMN%2Fty8WUSX8oKcawktOmiGWpk%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;1588&quot; height=&quot;450&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;450&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;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;현재 '이미지 크기 변경 모듈'은 두개의 채널과 통신함. ➔ 메시지 큐 &amp;amp; 파일 저장소&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;b&gt;경쟁 조건&lt;/b&gt;이 발생할 수 있음.&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예상 시나리오&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 &quot;ABC.png&quot; 업로드. 그리고 큐에 메시지 &quot;ABC-1&quot; 전달&lt;/li&gt;
&lt;li&gt;모종의 이유로 큐에서 딜레이 발생&lt;/li&gt;
&lt;li&gt;그 사이에 사용자가 이미지로 교체함(이때 이미지 명 같음). 그리고 큐에 메시지 &quot;ABC-2&quot; 전달&lt;/li&gt;
&lt;li&gt;큐 내부 사정이나 네트워크 지연 때문에 &quot;ABC-2&quot;를 먼저 모듈에 전달.&lt;/li&gt;
&lt;li&gt;모듈에서 &quot;ABC-2&quot;의 정보를 바탕으로 리사이즈&lt;/li&gt;
&lt;li&gt;그리고 큐에서 &quot;ABC-1&quot;를 모듈에 전달.&lt;/li&gt;
&lt;li&gt;과거의 정보를 바탕으로 리사이즈된 이미지가 최종적으로 저장됨.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;/div&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이벤트 순서 제어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메세지에 타임스탬프 또는 버전 정보를 추가해서 가장 최신 이벤트만 처리하도록 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;payload 정보 변경 + 재시도 메커니즘(retry, outbox 등등)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;payload에 메시지의 UUID를 넘기고, UUID를 기반으로 리사이즈하기&lt;/li&gt;
&lt;li&gt;만약 DB에 저장되기 전에 메시지가 소비 될 경우를 대비해서 재시도 메커니즘 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-3. 선형성 시스템 구현하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 정말 심플하게 진짜 복사본을 하나만 사용하기. 그러나 이 방법으로 결함을 견뎌낼 수 없다.&lt;/i&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;5장에서 봤던 내용을 선형적으로 만들 수 있을까?&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단일 리더 복제 (선형적이 될 가능성이 있음)&lt;/li&gt;
&lt;li&gt;합의 알고리즘 (선형적)&lt;/li&gt;
&lt;li&gt;다중 리더 복제 (비선형적)&lt;/li&gt;
&lt;li&gt;리더 없는 복제 (아마도 비선형적)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;'일 기준 시계'를 기반으로 LWW 충돌 해서 방법은 거의 확실히 비선형적&lt;/li&gt;
&lt;li&gt;시계 타임스탬ㅁ프는 clock skew 때문에 이벤트의 실제 순서와 일치함을 보장할 수 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성과 정족수&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L4ed3/dJMb9OtVPGJ/j2g1eVkNBhMKxtzwWsXhI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L4ed3/dJMb9OtVPGJ/j2g1eVkNBhMKxtzwWsXhI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L4ed3/dJMb9OtVPGJ/j2g1eVkNBhMKxtzwWsXhI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL4ed3%2FdJMb9OtVPGJ%2Fj2g1eVkNBhMKxtzwWsXhI1%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;1312&quot; height=&quot;752&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 36.2787%; height: 51px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.8605%; height: 17px;&quot;&gt;n&lt;/td&gt;
&lt;td style=&quot;width: 15.7366%; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.8605%; height: 17px;&quot;&gt;w&lt;/td&gt;
&lt;td style=&quot;width: 15.7366%; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 11.8605%; height: 17px;&quot;&gt;r&lt;/td&gt;
&lt;td style=&quot;width: 15.7366%; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.8605%;&quot;&gt;w + r &amp;gt; n&lt;/td&gt;
&lt;td style=&quot;width: 15.7366%;&quot;&gt;5 &amp;gt; 3 (true)&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;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;A의 요청이 완료된 후, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;B가 요청함.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;선형적이라면 B는 1을 읽어야 하는데, 0을 읽음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;결과적으로 비선형적&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다이나모 스타일 정족수를 선형적으로 만드는게 가능함(성능은 당연히 저하됨)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;읽기 복구를 동기식으로 처리해서 가능하게 함.&lt;/span&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;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-4. 선형성의 비용&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단일 리더 설정은 비선형적&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 리더 설정에서 데이터센터 사이의 네트워크가 끊기면 팔로워 데이터센터로 접속한&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 클라이언트들은 리더로 연결할 수 없으므로 데이터베이스에 아무것도 쓸 수 없고,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 선형성 읽기도 전혀 할 수 없음.&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;blockquote data-ke-style=&quot;style2&quot;&gt;CAP(Consistency / Availability /&amp;nbsp; Partition tolerance) 정리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관성, 가용성, 분할 허용성 사이의 trade-off를 설명하는 정리&lt;/li&gt;
&lt;li&gt;원래는 데이터베이스에서 trade-off에 대한 논의를 시작하려는 목적&lt;/li&gt;
&lt;li&gt;정확한 정의 없이 경험 법칙으로 제안됐음.&lt;/li&gt;
&lt;li&gt;공식적으로 정의된 CAP 정리는 매우 범위가 좁음.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 오직 하나의 일관성 모델과 한 종류의 결함만 고려함.&lt;/li&gt;
&lt;li&gt;결론 : 역사적 영향력은 있는데 &lt;b&gt;시스템을 설계할 때는 실용적 가치 없음.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;도움이 안되는 CAP 정리&lt;/b&gt;&lt;br /&gt;- CAP은 때때로 '일관성', '가용성', '분할 허용성' 중 2개를 고르라는 것으로 표현됨.&lt;br /&gt;- 근데 이런 식으로 생각하면 오해의 소지가 있음.&lt;br /&gt;- 왜? 네트워크가 올바르게 동작할 때는 시스템이 일관성(선형성)과&amp;nbsp; 가용성 모두를 제공하기 때문임.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;➔ CAP의 올바른 정의 : &lt;b&gt;&quot;네트워크 분단(Partition)이 생겼을 때&quot;&lt;/b&gt; 일관성과 가용성 중 하나를 선택하라&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성과 네트워크 지연&lt;/blockquote&gt;
&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; 느리다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;따라서 선형성을 보장하면, 성능이 떨어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 3. 순서화 보장 (Ordering Guarantees)&lt;/h3&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-1. 순서화와 인과성&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;순서화는 인과성을 보존하는데 도움을 준다.&lt;/blockquote&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시스템이 인과성에 부과된 순서를 지키면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 그 시스템은 &lt;b&gt;인과적으로 일관적(causally consistent)&lt;/b&gt;이라고 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;수학적 집합은 부분적으로 순서가 정해짐(partially ordererd)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;{a, b, c} / {a, b} / {b, c} ➔ 이런 경우라면 포함관계를 설명할 수 있기 때문에 비교할 수 있음.&lt;/li&gt;
&lt;li&gt;{a, b} / {b, c} ➔ 이 경우에 각 요소는 어떤 집합과의 포함관계를 설명할 수 없기 때문에 비교할 수 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;인과적 순서가 전체 순서는 아니다. (선형성 &amp;ne; 인과성)&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;선형성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;연산의 &lt;b&gt;전체 순서&lt;/b&gt;를 정할 수 있다.&lt;/li&gt;
&lt;li&gt;복사본이 하나만 있는 것처럼 동작하고 모든 연산이 원자적이면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 어떤 두 연산에 대해 항상 둘 중 하나가 먼저 실행됐다고 할 수 있다는 뜻&lt;/li&gt;
&lt;li&gt;선형성 데이터스토어에는 동시적 연산이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 타임라인&lt;/b&gt;이 있고, 모든 연산은 그 타임라인을 따라서 전체 순서가 정해져야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인과성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;동시에 실행되면 비교할 수가 없다.&lt;/li&gt;
&lt;li&gt;인과성이 전체 순서가 아니라 &lt;b&gt;부분 순서&lt;/b&gt;를 정의한다는 뜻&lt;/li&gt;
&lt;li&gt;동시성은 타임라인이 갈라졌다가 다시 합쳐지는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;이 경우 다른 branch에 있는 연산은 비교 불가(즉, 동시적)하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성은 인과적 일관성보다 강하다.&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&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;
&lt;li&gt;선형성이 필요해 보이는 경우, &lt;b&gt;진짜 필요한건 일관적 일관성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;인과적 의존성 담기 (Capturing causal dependencies)&lt;/blockquote&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;b&gt;인과적 의존성을 추적&lt;/b&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;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-2. 일련번호 순서화(Sequence Number Ordering)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일련번호나 타임스탬프를 써서 이벤트 순서를 정하기&lt;/blockquote&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;i&gt;'일련번호' &lt;/i&gt;&lt;/b&gt;나 &lt;b&gt;&lt;i&gt;'타임스탬프' &lt;/i&gt;&lt;/b&gt;를 써서 이벤트 순서를 정하는 것
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;앞에서 학습했듯, 타임스탬프는 논리적 시계에서 얻어도 됨.&lt;/li&gt;
&lt;li&gt;논리적 시계는 연산을 식별하는 시퀀스를 생성하는 알고리즘임.&lt;/li&gt;
&lt;li&gt;보통 모든 연산마다 증가하는 카운터를 사용함.&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;일련번호 or 타임스탬프&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;크기가 작고, 전체 순서를 제공함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;즉, 모든 연산은 고유 시퀀스를 갖고 항상 두 개의 시퀀스를 비교하면 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인과성에 일관적인 전체 순서대로 일련번호를 생성할 수 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;연산 A가 B보다 인과적으로 먼저 실행됐다면, A는 전체 순서에서도 B보다 먼저임.&lt;/li&gt;
&lt;li&gt;근데 연산 C, D가 동시에 일어났다면?
&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;A &amp;gt; B &amp;gt;&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;C &amp;gt;&amp;nbsp;D&lt;/span&gt;&amp;nbsp; or&amp;nbsp; A &amp;gt; B&lt;span&gt; &amp;gt;&lt;/span&gt;&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;D &amp;gt;&amp;nbsp;C&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;순서화를 더 부과한다? ➔ &lt;b&gt;인과성이 없는 연산도 순서를 매겨 버림.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;결과적으로 이 방식은 인과 관계를 잘 반영해서, 전체 순서를 잘 만들어줌. (A,B)&lt;/li&gt;
&lt;li&gt;근데 동시적에 발생한 연산의 경우에도 강제로 순서를 매겨버림. (C, D)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;UUID로 만든 전체 순서의 문제점&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UUID로 만든 전체 순서의 문제점.&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;랜덤 UUID는 서로 비교할 수 있기 때문에, 전체 순서를 만들 수 있음.&lt;/li&gt;
&lt;li&gt;실제로 A가 먼저 발생하고 B가 발생한 상황에서 UUID를 비교했는데, &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; B가 먼저 발생한 상황이라고 결정하는 문제가 있을 수 있음.&lt;/li&gt;
&lt;li&gt;결과적으로 UUID를 사용한 전체 순서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;인과성이 깨지게 됨&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단일 리더 환경이 아닌 경우 일련번호를 생성하는 방법&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;/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;A노드는 1 ~ 1000, B노드는 1001 ~ 2000&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➔ 위 세가지는 확장성이 좋지만, 생성한 &lt;b&gt;일련번호가 인과성에 일관적이지 않게 된다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 세가지 방법의 문제점&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 노드는 초당 연산수가 다를 수 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bGyxe8/dJMb9Pl44zd/AAAAAAAAAAAAAAAAAAAAAJ_ZjUJToq9iuYkikOD3wKtnUzOkjpZW5nC_yiG_-QL2/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1761922799&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=vqxRrzSCY3flnRABb378NgGsMy8%3D&quot; data-image-src=&quot;https://blog.kakaocdn.net/dna/bGyxe8/dJMb9Pl44zd/AAAAAAAAAAAAAAAAAAAAAJ_ZjUJToq9iuYkikOD3wKtnUzOkjpZW5nC_yiG_-QL2/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1761922799&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=vqxRrzSCY3flnRABb378NgGsMy8%3D&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;630&quot; data-filename=&quot;blob&quot; /&gt;&lt;/li&gt;
&lt;li&gt;인과적으로 나중에 실행된 연산(B)이 실제로 더 낮은 타임스탬프를 배정 받음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;1003 번이 199번보다 먼저 실행되는 상황이 발생할 수 있음.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;램포트 타임 스탬프&amp;nbsp;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK5nmA/dJMb83EzubZ/ScWA4oInEIekEJpjTkQXW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK5nmA/dJMb83EzubZ/ScWA4oInEIekEJpjTkQXW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK5nmA/dJMb83EzubZ/ScWA4oInEIekEJpjTkQXW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK5nmA%2FdJMb83EzubZ%2FScWA4oInEIekEJpjTkQXW1%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;1212&quot; height=&quot;530&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;530&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인과 관계를 만족시키는 논리적 시계(Logical Clock)&lt;/li&gt;
&lt;li&gt;실제 물리적 시간이 아니라, &lt;b&gt;&amp;ldquo;어떤 이벤트가 다른 이벤트보다 앞섰다&amp;rdquo;는 관계만을 보장&lt;/b&gt;하는 시계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메커니즘&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 노드는 고유 식별자를 갖고, 각 노드는 처리한 연산 개수를 카운터로 유지&lt;/li&gt;
&lt;li&gt;램포트 타임스탬프는 그냥 &lt;b&gt;[카운터, 노드ID]&lt;/b&gt; 의 쌍&lt;/li&gt;
&lt;li&gt;때로 같은 카운터 값이 같을 수 있지만, 타임스탬프에 노드 ID를 포함시켜서 유일성을 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;핵심 아이디어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 노드와 모든 클라가 지금까지 본 카운터 값 중 &lt;b&gt;최대값&lt;/b&gt;을 추적하고 모든 요청에 &lt;b&gt;그 값을 포함&lt;/b&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;버전 벡터와의 차이점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;버전 벡터는 두 연산이 동시적인지 인과적인지 구분할 수 있음. (&lt;b&gt;부분 순서만 표현&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;램포트 타임스탬프는 &lt;b&gt;항상 전체 순서화를 강제&lt;/b&gt;함.&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;타임스탬프 순서화로는 충분하지 않다.&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 유일한 사용자 계정을 생성하는 시나리오&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드A가 `username = gilbert`요청을 받음.&lt;/li&gt;
&lt;li&gt;이 요청을 지금 바로 처리해도 되는지 여부를 판단해야 함.&lt;/li&gt;
&lt;li&gt;이 시점에 노드A는 다른 노드들이 무슨 일을 하고 있는지 모름.&lt;/li&gt;
&lt;li&gt;결국 &amp;ldquo;지금 시점&amp;rdquo;에서는 안전하게 결정할 근거가 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, 모든 &lt;b&gt;노드의 정보(연산)를 받아야&lt;/b&gt;만 &lt;b&gt;전체 순서를 알 수 있는 거&lt;/b&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;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-3. 전체 브로드 캐스트&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 순서 브로드 캐스트(= 원자적 브로드캐스트)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 리더 복제는 한 노드를 리더로 선택하고 리더의 단일 CPU 코어에서 모든 연사을&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 차례대로 배열함으로써 연산의 전체 순서를 정함&lt;/li&gt;
&lt;li&gt;여기서 어려운 문제는 &lt;b&gt;처리량이 단일 리더가 처리할 수 있는 수준을 넘었을 때&lt;/b&gt;임.&lt;/li&gt;
&lt;li&gt;분산 시스템 분야에서는 위 문제를 이 방법으로 해결하는 것으로 알려져 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;전체 순서 브로드캐스트(total order broadcast) = 원자적 브로드캐스트(atomic broadcast)&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;전체 순서 브로드캐스트란?&lt;/blockquote&gt;
&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;하는 통신 방식&lt;/li&gt;
&lt;li&gt;두 가지 안전성 속성을 항상 만족해야 한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;신뢰성 있는 전달 (Reliable delivery)&lt;/li&gt;
&lt;li&gt;전체 순서가 정해진 전달 (Totally ordered delivery)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;물론 네트워크가 끊긴 경우에는 메시지 전달을 못하지만, 복구되면 원래대로 동작해야 한다.&lt;/li&gt;
&lt;li&gt;Zookeeper나 etcd 같은 합의 서비스는 전체 순서 브로드캐스트를 실제로 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 순서 브로드캐스트 사용하기&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;상태 기계 복제(state machine replication)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 메시지가 데이터베이스에 쓰기를 나타내고 모든 복제 서버가 같은 쓰기 연산을 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 같은 순서로 처리하면 복제 서버들은 서로 일관성 있는 상태를 유지하는 원리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;직렬성 트랜잭션 구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 복제 서버가 쓰기 연산을 같은 순서로 처리하면, 서로 일관성 있는 상태를 유지&lt;/li&gt;
&lt;li&gt;상태 기계 복제(state machine replication)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로그를 만드는 방법 중 하나
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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: circle;&quot; data-ke-list-type=&quot;circle&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;TOB 특징&lt;/blockquote&gt;
&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;에 그 &lt;b&gt;순서가 고정&lt;/b&gt;됨.&lt;/li&gt;
&lt;li&gt;중간에 소급적용이 불가능 (즉, 끼워 넣기 X)&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;blockquote data-ke-style=&quot;style2&quot;&gt;TOB를 사용해 선형성 저장소 구현하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성과 TOB가 같다고 할 순 없지만, 어느정도 링크되어 있음.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;전체 순서 브로드캐스트
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;비동기&lt;/li&gt;
&lt;li&gt;고정된 순서는 보장하지만, &lt;b&gt;언제 전달될지&lt;/b&gt;는 보장되지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;최신성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;`TOB(추가 전용 로그로 사용) + CAS 연산`
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트 A(나), B가 동시에 &quot;gilbert&quot; 생성 시도.&lt;/li&gt;
&lt;li&gt;로그 순서는 TOB가 [Create(A), Create(B)]로 확정. ➔ 모든 노드가 동일하게 가지고 있게됨.&lt;/li&gt;
&lt;li&gt;&quot;gilbert&quot;라는 이름에 대해 &lt;b&gt;처음 등장한 메시지가&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;나의 메시지라면? 내가 닉 먹는거임&lt;/li&gt;
&lt;li&gt;내 메시지가 아니라면? 나의 요청은 abort&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 동일한 메시지가 처리된다면?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;멱등성 보장을 통해 동일한 결과를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;근데 이 방법은 &lt;span style=&quot;color: #ee2323;&quot;&gt;선형성 읽기는 보장하지 않음.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로그로부터 비동기로 갱신되는 저장소를 읽으면 오래된 값이 읽힐 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;읽기를 선형적으로 만들기 위한 몇 가지 선택지가 있음.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그 순서에 따라 읽기&lt;/li&gt;
&lt;li&gt;로그의 최신 위치를 동기화한 후 읽기&lt;/li&gt;
&lt;li&gt;최신 복제본에서만 읽기&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;선형성 저장소를 사용해 TOB 구현하기&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 선형성이 보장되면 TOB 구현할 수 있다.&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현 아이디어
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;전역 카운터
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;737&quot; data-start=&quot;708&quot;&gt;선형성을 보장하는 저장소에 정수를 저장한다.&lt;/li&gt;
&lt;li data-end=&quot;779&quot; data-start=&quot;741&quot;&gt;이 정수는 전역적으로 하나뿐인 &amp;ldquo;메시지 번호&amp;rdquo; 역할을 한다.&lt;/li&gt;
&lt;li data-end=&quot;827&quot; data-start=&quot;783&quot;&gt;원자적 `increment-and-get` 연산을 지원한다고 &lt;b&gt;가정&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;827&quot; data-start=&quot;783&quot;&gt;메시지를 보낼 때
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;827&quot; data-start=&quot;783&quot;&gt;브로드캐스트하고 싶은 메시지를 만들 때, 먼저 increment-and-get을 실행해서&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 그 결과값을 메시지의 &amp;ldquo;일련번호(sequence number)&amp;rdquo; 로 붙인다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;&amp;raquo; increment-and-get() 연산이 전역에서 단 하나의 순서로 실행됨&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;827&quot; data-start=&quot;783&quot;&gt;모든 노드로 전송
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1127&quot; data-start=&quot;1091&quot;&gt;메시지와 함께 부여된 일련번호를 다른 노드들에게 보낸다.&lt;/li&gt;
&lt;li data-end=&quot;1127&quot; data-start=&quot;1091&quot;&gt;모든 노드가 동일한 순서로 메시지를 전달받게 된다.&lt;/li&gt;
&lt;li data-end=&quot;1175&quot; data-start=&quot;1131&quot;&gt;메시지가 유실되면 재전송 가능.(일련번호가 있으므로 중복 판별 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1282&quot; data-start=&quot;1246&quot;&gt;램포트 타임스탬프와의 차이점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;1282&quot; data-start=&quot;1246&quot;&gt;선형성 레지스터를 증가시켜 순열을 형성함.&lt;/li&gt;
&lt;li data-end=&quot;1282&quot; data-start=&quot;1246&quot;&gt;즉, 순열(sequence)이 실제 순서와 100프로 동일함.&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;선형성, TOB == 합의&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드가 죽거나, 네트워크가 끊기는 경우엔?&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉, &quot;전역적으로 레지스터&amp;rdquo;를 만들려면 노드 간 &lt;b&gt;의견이 완전히 일치해야&lt;/b&gt; 함.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 이건 바로 &lt;b&gt;합의(Consensus)&lt;/b&gt; 문제의 본질&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성과 TOB가 합의 문제로 귀결되는 이유
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;결국엔 &lt;b&gt;모든 노드가 다음에 어떤 값이 되어야 하는지 동의&lt;/b&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;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 4. 분산 트랜잭션 합의&lt;/h3&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;합의&lt;/blockquote&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;(Informally)&lt;/span&gt;, 합의의 목적은 단지 &lt;b&gt;여러 노드들이 뭔가에 동의하게 만드는 것&lt;/b&gt;임&amp;nbsp;&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드가 동의하는 것이 중요한 케이스
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;리더 선출&lt;/li&gt;
&lt;li&gt;원자적 커밋&lt;/li&gt;
&lt;/ol&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;blockquote data-ke-style=&quot;style2&quot;&gt;합의 불가능성&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FLP 정리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;어떤 노드가 죽을 위험이 있다면 항상 합의에 이를 수 있는 알고리즘은 없다&lt;/b&gt;를 증명한 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아주 제한적인 조건&lt;/b&gt;(이상적인 가정) 아래에서만 &amp;ldquo;불가능&amp;rdquo;을 증명함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분산 시스템에서는 노드가 죽을 수 있다고 가정하기에, 신뢰성 있는 합의는 불가능함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  4-1. 원자적 커밋관 2단계 커밋&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단일 노드에서 분산 원자적 커밋으로 (From single-node to distributed atomic commit)&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터가 디스크에 지속성 있게 쓰여지는 &lt;b&gt;순서&lt;/b&gt;에 결정적으로 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 트랜잭션에 여러 노드가 관련한다면? (ex. 다중 객체 트랜잭션, 용어 파티셔닝된 보조 색인)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 경우, 원자성을 보장할 수 없기 때문에 각 노드에서 독립적으로 트랜잭션을 커밋하는 것은 충분하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋을 되돌릴 수 없음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;물론 보상 트랜잭션 (compensating transaction)으로 비슷해 보이게 할 순 있는데&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 데이터베이스의 관점에서 이는 엄연히 분리된 트랜잭션.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 트랜잭션 사이에 걸친 정확성 요구사항은 application의 몫&lt;/i&gt;&lt;/span&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;blockquote data-ke-style=&quot;style2&quot;&gt;2단계 커밋 소개 (Introduction to two-phase commit)&lt;/blockquote&gt;
&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;하는 알고리즘&lt;/li&gt;
&lt;li&gt;단일 노드 트랜잭션에서는 보통 존재하지 않는, &lt;b&gt;코디네이터(coordinator)&lt;/b&gt;를 사용함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2단계 커밋 메커니즘&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;➔ 2PC의 commit/abort 과정은 두 단계로 나뉨 (그래서 이름이 이럼)&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boUqMh/dJMb9aX0kDf/TuYcrL4zcZeK7pembpLPeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boUqMh/dJMb9aX0kDf/TuYcrL4zcZeK7pembpLPeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boUqMh/dJMb9aX0kDf/TuYcrL4zcZeK7pembpLPeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboUqMh%2FdJMb9aX0kDf%2FTuYcrL4zcZeK7pembpLPeK%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;1226&quot; height=&quot;502&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;평소처럼 애플리케이션이 여러 DB 노드에서 데이터를 읽고 쓰면서 시작
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여러 DB노드를 &lt;b&gt;참여자(participant)&lt;/b&gt;라고 부름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션이 커밋할 준비가 되면 코디네이터가 &lt;b&gt;1단계를 시작&lt;/b&gt;함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 노드에 준비 요청을 보내서 커밋할 수 있는지 물어봄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그 후 코디네이터는 참여자들의 응답을 추적함.&lt;/li&gt;
&lt;li&gt;참여자 중,&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;모두 &quot;Yes&quot;&lt;/b&gt;로 응답하면 코디네이터는 2단계에서 &lt;b&gt;commit&lt;/b&gt; 요청 &amp;amp; commit됨.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나라도 &quot;No&quot;&lt;/b&gt;로 응답하면 &lt;b&gt;abort&lt;/b&gt; 요청을 보냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;약속에 관한 시스템 (A system of promises)&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션 분산 트랜잭션 희망
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션은 코디네잍어에게 &lt;span style=&quot;color: #006dd7;&quot;&gt;트랜잭션 ID&lt;/span&gt;를 요청한다.&lt;/li&gt;
&lt;li&gt;ID는 전역으로 유일함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션은 각 참여자(DB 노드)에서 단일 노드 트랜잭션 시작
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단일 노드 트랜잭션에 전역적으로 유일한 &lt;span style=&quot;color: #006dd7;&quot;&gt;트랜잭션 ID&lt;/span&gt;를 붙임&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;i&gt;➔ 이렇게 하면 문제가 생겨도 코디네이터나 참여자중 누군가가 abort 시킬 수 있음.&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션 커밋 준비 상태
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;코디네이터는 모든 참여자에게 전역 &lt;span style=&quot;color: #006dd7;&quot;&gt;트랜잭션 ID&lt;/span&gt;로 태깅된 &lt;b&gt;준비 요청을 보냄.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;보낸 요청 중 하나라도 실패 or 타임아웃 터지면 그 &lt;span style=&quot;color: #006dd7;&quot;&gt;트랜잭션 ID&lt;/span&gt;로 abort 요청 보냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;참여자가 준비 요청 받으면 모든 상황에서 트랜잭션을 커밋할 수 있는지 확인
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이때 참여자는 &lt;b&gt;제약 조건 위반&lt;/b&gt;이나 &lt;b&gt;충돌이 없는지&lt;/b&gt;도 미리 검사해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코디네이터의 최종 결정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 준비 요청에 대해 응답을 받았을 때 커밋할 것인지 abort할 것인지 결정&lt;/li&gt;
&lt;li&gt;코디네이터는 추후 죽는 경우에 어떻게 결정했는지 알 수 있도록 그 결정을 디스크에 있는&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 트랜잭션 로그에 기록해야 함. 이를 &lt;b&gt;커밋 포인트&lt;/b&gt;라고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 참여자에게 결과(commit or abort) 요청 전송
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;만약 요청 전송에 문제가 생기면, 성공할 때 까지 무한 재시도&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2PC가 원자성을 보장하는 근거&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;참여자가 &quot;Yes&quot;에 투표할 때, 나중에 &lt;b&gt;무조건 커밋할 수 있음을 약속&lt;/b&gt;함.&lt;/li&gt;
&lt;li&gt;코디네이터도 한 번 결정하면 그 &lt;b&gt;결정을 변경할 수 없음&lt;/b&gt;(위에서 본 무한 재시도)&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코디네이터 장애&lt;/blockquote&gt;
&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;에는 트랜잭션을 abort 할 수 있음.&lt;/li&gt;
&lt;li&gt;참여자가 준비 요청을 받고 &quot;Yes&quot;에 투표했다면 트랜잭션을 &lt;b&gt;일방적으로&lt;/b&gt; abort 할 수 없음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일단 코디네이터로부터 커밋/어보트 여부를 회신 받을 때까지 대기 해야함.&lt;/li&gt;
&lt;li&gt;이런 상태에서 참여자의 트랜잭션을 &lt;b&gt;의심스럽다(in doubt), 불확실하다(uncertain)&lt;/b&gt;고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코디네이터에 장애가 생기면 복구되기를 기다려야 함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;코디네이터는 2PC를 완료할 수 있는 유일한 방법이기 때문.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;코디네이터가 복구되면 &lt;/span&gt;&lt;b&gt;트랜잭션 로그&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 읽어서 의심스러운&amp;nbsp;&lt;/span&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;blockquote data-ke-style=&quot;style2&quot;&gt;3단계 커밋&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2PC는 코디네이터가 복구하시를 기다리는 경우가 있어, &lt;b&gt;블로킹 원자적 커밋 프로토콜&lt;/b&gt;이라고 불림.&lt;/li&gt;
&lt;li&gt;이론상으로 &lt;b&gt;논블로킹하게&lt;/b&gt; 만들 수 있긴 함. 근데 간단하진 않음.&lt;/li&gt;
&lt;li&gt;3PC
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;3PC는 지연에 제한이 있는 네트워크와 응답 시간에 제한이 있는 노드를 가정함.&lt;/li&gt;
&lt;li&gt;기약 없는 네트워크 지연 or 프로세스 중단이 있는 경우, 3PC는 원자성 보장 못함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일반적으로 &lt;b&gt;논블로킹 원자적 커밋&lt;/b&gt;은
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;완벽한 장애 감지기 (perfect failure detector) 메커니즘이 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;암튼, 이런 이유로 2PC가 계속 쓰임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  4-2. 현실의 분산 트랜잭션&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;엇갈린 평판&lt;/blockquote&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex. MySQL의 분산 트랜잭션은 단일 노드 트랜잭션 보다 10배 이상 느림 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 디스크 강제 쓰기 &amp;amp;&amp;nbsp; 네트워크 왕복 시간&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;blockquote data-ke-style=&quot;style2&quot;&gt;두 가지 종류의 분산 트랜잭션&amp;nbsp;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터베이스 내부 분산 트랜잭션
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;260&quot; data-start=&quot;216&quot;&gt;&lt;b&gt;하나의 분산 데이터베이스 시스템 내부&lt;/b&gt;에서 발생하는 트랜잭션&lt;/li&gt;
&lt;li data-end=&quot;325&quot; data-start=&quot;261&quot;&gt;같은 DB 소프트웨어 안에서 여러 노드가 하나의 트랜잭션에 참여하는 형태&lt;/li&gt;
&lt;li data-end=&quot;325&quot; data-start=&quot;261&quot;&gt;ex. MySQL NDB Cluster, VoltDB, CockroachDB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;325&quot; data-start=&quot;261&quot;&gt;이종 분산 트랜잭션 (Heterogeneous Distributed Transaction)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;832&quot; data-start=&quot;803&quot;&gt;&lt;b&gt;서로 다른 기술&lt;/b&gt;이 섞인 트랜잭션이에요.&lt;/li&gt;
&lt;li data-end=&quot;909&quot; data-start=&quot;833&quot;&gt;트랜잭션에 참여하는 시스템이 DB만 있는 게 아니라 메시지 브로커, 다른 벤더의 DB일 수도 있음.&lt;/li&gt;
&lt;li data-end=&quot;957&quot; data-start=&quot;919&quot;&gt;ex. Oracle DB + Kafka 메시지 큐 + MySQL DB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;정확히 한 번 메시지 처리 (Exactly-once message processing)&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐에서 나온 메시지는&lt;b&gt; 그 메시지를 처리하는 DB 트랜잭션이 커밋에 성공했을 때&lt;/b&gt;만 처리된 것 간주할 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(메시지 확인 +&lt;/b&gt;&amp;nbsp;&lt;b&gt;데이터베이스 쓰기)&lt;/b&gt;를 단일 트랜잭션에서 원자적 단위로 묶으면,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 중간에 장애가 나도 재시도를 통해 &lt;b&gt;effectively-once&lt;/b&gt; &lt;b&gt;processing&lt;/b&gt; 보장할 수 있음.&lt;/li&gt;
&lt;li&gt;그러나 이런 분산 트랜잭션은,
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션에 영향을 받는 모든 시스템이 &lt;b&gt;동일한 원자적 커밋 프로토콜&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;XA(eXtended Architecture) 트랜잭션&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X/Open XA는 이종 기술에 걸친 2PC을 구현하는 표준이다.&lt;/li&gt;
&lt;li&gt;XA는 postgresql, mysql, db2, sql서버, 오라클을 포함한 여러 관계정 데이터베이스와&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 엑티브MQ, 호닛MQ를 포함한 메시지 브로커에서 지원됨.&lt;/li&gt;
&lt;li&gt;XA는 네트워크 프로토콜이 아니고 트랜잭션 코디네이터와 연결되는 인터페이스틑 제공하는 API&lt;/li&gt;
&lt;li&gt;XA는 애플리케이션이 네트워크 드라이버나 클라이언트 라이브러리를 사용해&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 참여자 DB나 메시징 서비스와 통신한다고 가정함.&lt;/li&gt;
&lt;li&gt;코디네이터가 죽었다 깨어나면 XA 콜백을 사용해서 디스크에 저장한 상태를 공유함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;의심스러운(in doubt) 상태에 있는 동안 잠금을 유지하는 문제&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션은 커밋/어보트 될 때까지 잠금을 가지고 있어야 하는데,&lt;br /&gt;&amp;nbsp; &amp;nbsp; 2PC를 사용하면 의심스러운 트랜잭션은 상태가 변경되기 전까지 계속 잠금을 잡아야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코디네이터 장애에서 복구하기&lt;/blockquote&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;고아+의심 트랜잭션(orphaned in-doubt transactions)&lt;/b&gt;이 생길 수 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 로그 손실, 소프트웨어 오염 등의 이유로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이런 트랜잭션은 영원히 잠금을 유지한테로 남아있게 됨.&lt;/li&gt;
&lt;li&gt;DB 서버 재부팅하면? 그래도 안됨.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;2PC의 메커니즘은 재시작하더라도 in-doubt 트랜잭션의 잠금을 유지해야 함.&lt;/li&gt;
&lt;li&gt;그렇지 않으면 &lt;b&gt;원자성을 위반할 위험&lt;/b&gt;이 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이걸 해결하려면, 관리자가 개입해야 함(수동 작업 필요)&lt;/li&gt;
&lt;li&gt;여러 XA 구현체는 참여자가 독단적으로 의심서르오누 트랜잭션을 커밋/어보트할지 결정할 수 있는&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;경험적 결정(heuristic decision)&lt;/b&gt;이라고 부르는 대책이 있긴함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;근데 경험적은 2PC의 약속 체계를 위반하는 거임.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;분산 트랜잭션의 제약&lt;br /&gt;&lt;i&gt;➔ 11장, 12장에서 대안적인 방법 학습할 예정&lt;/i&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;단일 장애점(Single Point of Failure) 문제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;트랜잭션 코디네이터가 단일 노드에서 실행되면, 장애가 발생 시 전체 시스템이 멈출 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;코디네이터 장애 시 트랜잭션이 잠금 상태로 남거나 다른 애플리케이션 서버까지 영향을 받음.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;대부분의 코디네이터 구현체는 &lt;b&gt;고가용성(HA)&lt;/b&gt; 을 지원하지 않거나 제한적인 복제만 제공함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;애플리케이션 서버 비정상 종료 문제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;트랜잭션 참여자 중 하나라도 비정상 종료되면, 코디네이터가 그 상태를 알 수 없음.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;재시작 시 트랜잭션의 &lt;b&gt;일관성 복구&lt;/b&gt;를 위해 데이터베이스 로그나 별도의 저장소가 필요하지만,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 대부분의 애플리케이션 서버는 이에 적합하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;공통 분모(Shared Protocol) 부재
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;서로 다른 데이터베이스/시스템이 함께 XA 트랜잭션을 수행하려면 최소한의 공통 프로토콜이 필요.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;하지만 각 DB나 시스템이 표준화된 방식(예: SSI)을 따르지 않으면&amp;nbsp;일관성을 유지하기 어려움.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;XA가 아닌 시스템 간의 제한
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;XA를 지원하지 않는 데이터베이스나 내부 DB에서는 분산 트랜잭션이 불가능함.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;일부 2PC(성공 사례도 있지만, 시스템 일부가 응답하지 않으면 실패함.&lt;/li&gt;
&lt;li data-end=&quot;147&quot; data-start=&quot;101&quot;&gt;따라서 &lt;b&gt;장애에 취약하고 복구가 어려움&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;size18&quot;&gt;  4-3. 내결함성을 지닌 합의&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;합의 문제&amp;nbsp;&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;하나&lt;span&gt; &lt;/span&gt;이상의&lt;span&gt; &lt;/span&gt;노드가&lt;span&gt; &lt;/span&gt;값을&lt;span&gt; &lt;/span&gt;제안&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;합의&lt;span&gt; &lt;/span&gt;알고리즘이&lt;span&gt; &lt;/span&gt;그&lt;span&gt; &lt;/span&gt;&lt;b&gt;제안들&lt;span&gt; &lt;/span&gt;중&lt;span&gt; &lt;/span&gt;하나를&lt;span&gt; &lt;/span&gt;최종&lt;span&gt; &lt;/span&gt;값으로&lt;span&gt; &lt;/span&gt;결정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위 정의에 따르면, 합의는 아래의 속성을 만족해야 함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;안전성 속성 (내결함성 상관없으면 이 세개 속성 만족시키는 건 쉬움)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;균일한 동의 : 어떤 두 노드도 다르게 결정하지 않는다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;무결성 : 어떤 노드도 두 번 결정하지 않는다.&lt;/li&gt;
&lt;li&gt;유효성 : 한 노드가 값v를 결정하면, 어떤 노드에서 제안된 것이다.&lt;/li&gt;
&lt;/ol&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;/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;blockquote data-ke-style=&quot;style2&quot;&gt;'종료 속성' 자세히 알아보기&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&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: circle;&quot; data-ke-list-type=&quot;circle&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;대부분의 합의 알고리즘은 비잔틴 결함이 없다고 가정&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;'합의 알고리즘'과 '전체 순서 브로드캐스트(TOB)'&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;뷰스탬프 복제 (Viewstamped Replication)&lt;/li&gt;
&lt;li&gt;팍소스(Paxos) ➔ Leaderless&lt;/li&gt;
&lt;li&gt;멀티 팍소스(Multi-Paxos) &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Leader-based&lt;/li&gt;
&lt;li&gt;라프트(Raft) ➔ Leader-based&lt;/li&gt;
&lt;li&gt;잽 (Zab)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위 알고리즘에서 '형식적 모델'을 직접 사용하진 않고, &lt;b&gt;값의 순차열을 결정해서 TOB 알고리즘을 만듬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;TOB는 &amp;ldquo;모든 노드가 같은 순서로 메시지를 받게 하기 위해&amp;rdquo; 각 메시지 순서마다 &lt;b&gt;합의를 반복해서 실행&lt;/b&gt;하는 과정&lt;/li&gt;
&lt;li&gt;뷰스탬프 복제, 라프트, 잽은 전체 순서 브로드캐스트를 직접 구현함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;단일 리더 복제와 합의&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;에포크 번호 붙이기와 정족수&lt;br /&gt;&lt;i&gt;➔ 지금까지 설명한 합의 프로토콜은 단일 리더를 보장하지 않음.&lt;/i&gt;&lt;/blockquote&gt;
&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;하고 각 에포크 내에서는 &lt;b&gt;리더가 유일하다고 보장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;팍소스 - 투표 번호(ballot number)&lt;/li&gt;
&lt;li&gt;뷰스탬프 복제 - 뷰 번호(view number)&lt;/li&gt;
&lt;li&gt;라포트 - 텀 번호(term number)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;에포크 번호는 전체 순서가 있고 단조 증가함. 따라서 &lt;b&gt;번호가 큰 놈이 리더&lt;/b&gt;가 됨.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;물론, 정족수로부터 투표를 받음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;노드들은 총 두 번의 투표를 함. (이 때 투표를 하는 정족수는 겹쳐야 함.)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;리더 선출하기 위해서&lt;/li&gt;
&lt;li&gt;리더의 제안에 투표하기 위해서&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이 투표는 2PC와 비슷해 보이긴 함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;합의의 제약&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이는 클러스터에 노드를 그냥 추가하거나 제거할 수 없다는 뜻&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;합의 알고리즘의 동적 멤버십(dynamic membership) 확장은 클러스터에 있는 노드 집합이&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 시간이 지남에 따라 바뀌는 것을 허용하지만, 이들은 정적 멤버십 알고리즘보다 이해하기 어려움.&lt;/li&gt;
&lt;li&gt;합의 시스템은 장애 노드를 감지하기 위해 일반적으로 타임아웃에 의존함.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;멤버십과 코디네이션 서비스&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Zookeeper나 etcd 같은 프로젝트는
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;분산 키-값 저장소&quot;나 &quot;코디네이션 설정 서비스&quot;라고 설명됨.&lt;/li&gt;
&lt;li&gt;작은 양의 데이터를 보관하도록 설계됐고, 이 소량의 데이터는 내결함성을 지닌 TOB 알고리즘을 사용해 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 모든 노드에 걸쳐 복제됨.&lt;/li&gt;
&lt;li&gt;데이터베이스 처럼 보이지만 목적이 다름. 주 목적은 &lt;b&gt;분산 시스템 간의 일관성&lt;/b&gt;과 &lt;b&gt;합의를 보장&lt;/b&gt;하는 것&lt;/li&gt;
&lt;li&gt;이렇기 때문에 HBase, Hadoop YARN, OpenStack Nova, Kafka는 모두 Zookeeper에 의존함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ZooKeeper가 재공하는 흥미로운 기능들&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;선형성 원자적 연산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;연산의 전체 순서화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;장애 감지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;변경 알림&lt;/span&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;blockquote data-ke-style=&quot;style2&quot;&gt;작업을 노드에 할당하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Zookeeper는 아래의 경우에 유용함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;리더 선출 필요한 경우&amp;nbsp;&lt;/li&gt;
&lt;li&gt;파티셔닝된 자원이 있고, 어떤 파티션을 어떤 노드에 할당할지를 결정해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(원자적 연산 + 단명 노드 + 알림)을 잘 사용하면&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 사람의 개입 없이 애플리케이션이 결함으로부터 자동으로 복구될 수 있다.&lt;/li&gt;
&lt;li&gt;주키퍼는(보통 3~5개) 고정된 수의 노드에서 합의를 수행하고, 수천 대의 클라이언트가 이를 이용&lt;/li&gt;
&lt;li&gt;그래서 zookeeper 왜 씀? ➔ 합의 알고리즘 구현하기 엄청 빡셈(성공한 기록이 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;서비스 찾기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 서비스에 연결하려면 어떤 IP 주소로 접속해야 하는지 알아내는 용도로도 자주 사용됨.&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;이런 목적으로 어떤 합의 시스템은 읽기 전용 캐시 복제 서버를 지원함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;멤버십 서비스&lt;/blockquote&gt;
&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;&lt;/li&gt;
&lt;li&gt;장애 감지를 합의와 연결하면 노드들은 어떤 노드가 살아 있는 것으로 여겨져야 하는지&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 혹은 죽은 것으로 여겨져야 하는지에 동의할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/227</guid>
      <comments>https://gilbert9172.tistory.com/227#entry227comment</comments>
      <pubDate>Fri, 17 Oct 2025 20:12:58 +0900</pubDate>
    </item>
    <item>
      <title>8장. 분산 시스템의 골칫거리(The Trouble with Distributed Systems)</title>
      <link>https://gilbert9172.tistory.com/226</link>
      <description>&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&gt;분산 시스템을 다루는 것은 단일 컴퓨터에서 소프트웨어를 작성하는 것과 근본적으로 다르다.&lt;/li&gt;&lt;li&gt;결국 엔지니어로서 우리의 과제는 모든 것이 잘못되는 와중에도 &lt;b&gt;시스템이 자신의 일을 하도록 만드는 것&lt;/b&gt;&lt;/li&gt;&lt;li&gt;9장에서 우리는 분산 시스템에서 그러한 보장을 제공할 수 있는 알고리즘의 예를 살펴보겠습니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. 결함과 부분 장애(Faults and Partial Failures)&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;부분장애란?&lt;/blockquote&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;부분장애는 비결정적(nondeterministic) ➔ 같은 조건에서도 결과가 매번 달라질 수 있음.&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;size18&quot;&gt;  1-1. Cloud Computing and Supercomputing&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;대규모 컴퓨팅 구축 방법(철학)&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;고성능 컴퓨터&lt;/li&gt;&lt;li&gt;클라우드 컴퓨팅&lt;/li&gt;&lt;li&gt;위 두 철학의 중간&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;철학에 따라 결함 처리 방법이 다름&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;노드 하나에 장애가 나면, 모든 작업을 중단한다.&lt;/li&gt; 
   &lt;li&gt;장애가 복구되면 중단 시점부터 다시 계산을 한다.&lt;/li&gt; 
   &lt;li&gt;결론 : 부분 장애를 &lt;b&gt;전체 장애로 확대&lt;/b&gt;해서 해결&amp;nbsp;&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;blockquote data-ke-style=&quot;style2&quot;&gt;분산 시스템이 동작하게 하려면...&amp;nbsp;&lt;/blockquote&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템을 구축하기&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;개별 구성 요소가 가끔 오류를 내더라도, 상위 계층에서 보정·재전송·검증 같은 메커니즘을 &lt;br&gt;추가하면 전체 시스템의 신뢰성을 끌어올릴 수 있다.&lt;br&gt;&lt;br&gt;예를 들어, 무선 통신처럼 잡음으로 오류가 생길 수 있어도, 오류정정코드가 있으면 잘못된 비트를 &lt;br&gt;감지·수정해 올바른 데이터로 복구할 수 있다. 또 IP는 패킷을 유실·지연·중복·순서 뒤바꿈할 수 있는 &lt;br&gt;“신뢰할 수 없는” 프로토콜이지만, 그 위의 TCP가 유실된 패킷을 재전송시키고, 순서를 맞춰 재조립해 &lt;br&gt;“더 신뢰성 있는” 연결처럼 보이게 한다.&lt;br&gt;&lt;br&gt;하지만 한계는 있다. 오류정정코드가 처리할 수 있는 오류량에는 상한이 있고, TCP가 손실·중복은 해결해도&lt;br&gt;네트워크 지연 자체를 없애진 못한다. 그럼에도 이런 상위 계층의 설계 덕분에 낮은 수준의 잡다한 장애를 단순화해&lt;br&gt;다루기 쉬워지고, 남은 문제에 집중할 수 있게 된다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 2. 신뢰성 없는 네트워크 (Unreliable Networks)&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&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;&lt;/li&gt; 
 &lt;li&gt;이런 종류의 네트워크에서 노드끼리 메시지(패킷)을 보낼 수 있음.&lt;/li&gt; 
 &lt;li&gt;하지만 네트워크는 메시지가 언제 도착할지 혹은 메시지 도착 여부를 보장하진 않음.&lt;/li&gt; 
 &lt;li&gt;요청을 보내고 응답을 기다리는 동안 여러가지 문제가 발생할 수 있음. 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;요청 손실&lt;/li&gt; 
   &lt;li&gt;요청이 큐에서 늦게 전송&lt;/li&gt; 
   &lt;li&gt;원격 노드에 장애 발생&lt;/li&gt; 
   &lt;li&gt;원격 노드의 응답이 늦음&lt;/li&gt; 
   &lt;li&gt;네트워크가 손실&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;다른 노드로 요청을 보내서 응답을 받지 못했다면 그 이유를 아는 것은 &lt;b&gt;불가능&lt;/b&gt;함. 
  &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;&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;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt; 2-1. 현실의 네트워크 결함 (Network Faults in Practice)&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;span&gt;, &lt;/span&gt;하드웨어&lt;span&gt; &lt;/span&gt;중복만으로는&lt;span&gt; &lt;/span&gt;운영&lt;span&gt;·&lt;/span&gt;설정&lt;span&gt; &lt;/span&gt;실수를&lt;span&gt; &lt;/span&gt;예방할&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;없어서&lt;span&gt; &lt;/span&gt;전체&lt;span&gt; &lt;/span&gt;장애율&lt;span&gt; &lt;/span&gt;개선에&lt;span&gt; &lt;/span&gt;한계가&lt;span&gt; &lt;/span&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;상어 이슈 등등&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;결국 네트워크 결함은 일어날 수 있음! 피할 방법이 없다.&lt;/li&gt; 
 &lt;li&gt;그렇다고 반드시 네트워크 결함을 견더내도록(tolerating) 처리할 필요는 없음. 
  &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;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt; 2-2. 결함 감지(Detecting Faults)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시스템은 결함 있는 노드를 자동으로 감지할 수 있어야 함.&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;예를 들면&amp;nbsp; 
  &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;단일 리더 복제를 사용하는 분산DB에서 리더에 장애가 나면 팔로워 중 하나가 리더로 승격돼야 한다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;근데 &lt;b&gt;네트워크의 불확실성&lt;/b&gt; 때문에 &lt;b&gt;노드가 동작 중인지 아닌지 구별하기 어려움&lt;/b&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;/li&gt; 
 &lt;li&gt;결국 몇번이고 다시 요청보내서 타임아웃 내에 응답받지 못하면, 마침내 노드가 죽었다고 선언할 수 있음.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt; 2-3. 타임아웃과 기약 없는 지연 (Timeouts and Unbounded Delays)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;타임 아웃은 얼마나 길어야 될까?&amp;nbsp;&lt;/blockquote&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;노드가 일시적으로 느려졌을 뿐인데, 죽었다고 잘못 선언할 위험이 있음.&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;타임 아웃이 낮으면 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;rtt(round-trip time)가 순간적으로 급증하기만 해도 시스템 균형이 깨짐&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;blockquote data-ke-style=&quot;style2&quot;&gt;패킷의 최대 지연 시간이 보장된 네트워크를 사용하는 경우&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;모든 패킷은 전송 시간이 d 보다 더 걸리지 않음.&lt;/li&gt;&lt;li&gt;장애가 나지 않은 노드는 항상 요청을 r 시간 내에 처리한다고 가정&lt;/li&gt;&lt;li&gt;그렇다면 2d+r을 타임아웃으로 사용하는게 합리적&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;유감스럽게도 대부분의 시스템은 위와 같은 상황을 보장하지 않음!&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;비동기 네트워크는 기약 없는 지연(unbounded delay)가 있고, 요청을 특정 시간 내에 처리한다고 보장 못함.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;네트워크 혼잡과 큐대기&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;네트워크에서 패킷 지연의 변동성은 큐 대기 때문인 경우가 많음.&lt;br&gt; 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;네트워크 혼잡 (network congestion)&lt;/li&gt; 
   &lt;li&gt;CPU 코어가 바쁜 경우 큐에서 댇기&lt;/li&gt; 
   &lt;li&gt;가상 환경에서 실행되는 운영체제&lt;/li&gt; 
   &lt;li&gt;TCP의 흐름 제어(flow control)&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li&gt;TCP는 타임아웃 내에 확인 응답을 받지 못하면 &lt;b&gt;패킷이 손실됐다고 간주&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;타임아웃 대안책 : 파이 증가 장애 감지기 (Phi Accrual failure detector)&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;시스템이 지속적으로 &lt;b&gt;응답 시간&lt;/b&gt;과 &lt;b&gt;변동성(jitter)&lt;/b&gt;을 측정하고&lt;/li&gt; 
   &lt;li&gt;관찰된 응답 시간 분포에 따라 &lt;b&gt;타임아웃을 자동으로 조절&lt;/b&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;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt; 2-4. 동기 vs 비동기 네트워크 (Synchronous Versus Asynchronous Networks)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;하드웨어에서는 왜 네트워크를 신뢰성 있게 만들 수 없을까?&lt;/blockquote&gt;&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;와 &lt;b&gt;데이터센터 네트워크&lt;/b&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;회선(circuit)이 만들어짐&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;➔ 통화가 끝날때 까지 유지&lt;/span&gt;&amp;nbsp;&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;li&gt;이를 &lt;b&gt;제한 있는 지연(bounded delay)&lt;/b&gt;라고 함.&lt;/li&gt; 
    &lt;/ul&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;blockquote data-ke-style=&quot;style2&quot;&gt;그냥 네트워크 지연을 예측 가능하게 만들 수는 없을까?&lt;/blockquote&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 87px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 36.1241%; height: 18px; text-align: center;&quot;&gt;전화 네트워크&lt;/td&gt;&lt;td style=&quot;width: 45.7751%; height: 18px; text-align: center;&quot;&gt;데이터센터 네트워크&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 36.1241%; height: 18px;&quot;&gt;그 누구도 사용할 수 없는 회선을 사용함&lt;/td&gt;&lt;td style=&quot;width: 45.7751%; height: 18px;&quot;&gt;가용 네트워크 대역폭을 기회주의 적으로 사용&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 36.1241%; height: 17px;&quot;&gt;회선이 구성됐을 때 왕복 시간의 최대치를 보장&lt;/td&gt;&lt;td style=&quot;width: 45.7751%; height: 17px;&quot;&gt;큐 대기의 영향을 받는 패킷 교화 프로토콜(기약 없는 지연)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 36.1241%;&quot;&gt;초당 비트 개수가 상당히 고정되어 있음.&lt;/td&gt;&lt;td style=&quot;width: 45.7751%;&quot;&gt;순간적으로 몰리는 트래픽(bursty traffic)에 최적화&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;blockquote data-ke-style=&quot;style2&quot;&gt;지연 시간과 자원 사용률&lt;/blockquote&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;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 3. 신뢰성 없는 시계 (Unreliable Clocks)&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;두 가지 관점 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;지속 시간 (Durations)&lt;/li&gt; 
   &lt;li&gt;시점 (Points in time)&lt;/li&gt; 
  &lt;/ol&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;size18&quot;&gt;  3-1. 단조 시계 vs 일 기준 시계 (Monotonic Versus Time-of-Day Clocks)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일 기준 시계 (Time-of-Day Clocks)&lt;/blockquote&gt;&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;&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ex.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;2025-10-13 21:34:12.123 KST&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;벽시계 시간(wall-clock time)이라고도 함.&lt;/li&gt; 
 &lt;li&gt;NTP(Network time protocol)로 동기화 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;한 장비의 timestamp는 다른 장비의 timestamp와 동일한 의미를 지닌다는 뜻&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;blockquote data-ke-style=&quot;style2&quot;&gt;'일 기준 시계'는 이상한 점&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;시계 점프 (clock jump) 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;NTP 보정 시 시계가 과거로 되돌아가거나 순간적으로 뛰는 현상 발생 → 경과 시간 측정에 부적합&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;윤초(leap second) 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;지구 자전의 불규칙성으로 인해 발생하는 시간 차이를 보정하기 위해 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 협정 세계시(UTC)에 1초를 더하거나 빼서 시간을 조정하는 것&lt;/li&gt; 
   &lt;li&gt;윤초 반영이 불완전해 시계가 실제 시간과 미묘하게 어긋날 수 있음&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;낮은 해상도(coarse-grained) 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;과거 시스템은 10ms 단위로만 시간 갱신 → 세밀한 타이밍 측정 불가&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단조 시계 (Monotonic)&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;ex. 32500523123ns, 523.411s&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;해상도가 상당히 좋음 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;시간 구간을 마이크로초나 그 이하 단위로 측정할 수 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;분산 시스템에서 단조 시계는 나름 good 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;다른 노드의 시계 사이에 동기화가 돼야 한다는 가정이 없고,&lt;/li&gt; 
   &lt;li&gt;측정이 약간 부정확해도 민감하지 않기 때문&lt;/li&gt; 
  &lt;/ol&gt; &lt;/li&gt; 
 &lt;li data-end=&quot;431&quot; data-start=&quot;346&quot;&gt;한 시점에서 단조 시계 값을 기록하고, 일정 일을 한 후 나중에 다시 값을 확인하면 &lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 두 값의 차이로 경과 시간&lt;/b&gt;을 구할 수 있음.&lt;/li&gt; 
 &lt;li data-end=&quot;560&quot; data-start=&quot;432&quot;&gt;단조 시계의 &lt;b&gt;절대적인 값 자체는 의미 없음. &lt;/b&gt;단지 두 시점 사이의 &lt;b&gt;차이&lt;/b&gt;가 의미를 가짐.&amp;nbsp;&lt;/li&gt; 
 &lt;li data-end=&quot;604&quot; data-start=&quot;561&quot;&gt;따라서 “이 값이 실제 시간 몇 시인가?” 같은 질문에는 사용할 수 없다.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;다중 CPU 환경에서 단조 시계&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;여러 CPU 소켓이 있는 서버의 각 CPU는 독립된 타이머를 가질 수 있음.&lt;/li&gt;&lt;li&gt;운영체제는 여러 CPU에서 실행되는 스레드 간의 시간 차이를 줄이기 위해 단조적으로 보이게 하려고 노력&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;blockquote data-ke-style=&quot;style2&quot;&gt;NTP와 단조 시계&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;NTP는 단조 시계가 진행하는 진도수를 조정할 수 있음 - 시계를 돌린다(slewing)라고 함 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;컴퓨터의 로컬 시계가 NTP 서버 보다 빠르거나 느리다는 것을 발견했을 때&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;NTP가 조절할 수 있는 범위는 0.05% 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;단, 단조 시계가 앞이나 뒤로 뛰게 할 순 없음. (시간 점프 X)&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-2. 시계 동기화와 정확도 (Clock Synchronization and Accuracy)&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시계 동기화&lt;/blockquote&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;일 기준 시계는 NTP 서버나 다른 외부 시간에 맞춰 설정돼야 유용함.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시계가 정확한 시간을 알려주게 하는 방법은 신뢰성이 높지 않음.&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;컴퓨터의 quartz(석영) 시계는 아주 정확하지 않음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;드리프트(drift) 현상이 생김 : 더 빠르거나 느리게 실행되는 현상&lt;/li&gt; 
   &lt;li&gt;드리프트는 장비의 온도에 영향을 받음.&amp;nbsp;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;컴퓨터 시계가 NTP 서버와 차이가 많이 나면, 동기화 거부 또는 로컬 시계 강제 리셋 가능성 있음.&lt;/li&gt; 
 &lt;li&gt;윤초가 발생하는 경우&lt;/li&gt; 
 &lt;li&gt;가상화 된 하드웨어&lt;/li&gt; 
 &lt;li&gt;완전히 제어할 수 없는 장치에서 소프트웨서 실행하는 경우&lt;/li&gt; 
 &lt;li&gt;등등...&lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  3-3. 동기화된 시계에 의존하기 (Relying on Synchronized Clocks)&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시계도 완벽하지 않다!&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;이벤트 순서화용 타임스탬프 (Timestamps for ordering events)&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;LWW(Last Write Win)라고 배웠음.&lt;/li&gt; 
   &lt;li&gt;이때 &quot;최근&quot;의 정의는 &lt;b&gt;'로컬-일 기준 시계'에 의존하면 틀릴 수도 있음!!&lt;/b&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;그래서 순서가 보장되도록 NTP 동기화를 정확히 할 수 있나? &lt;b&gt;불가능&lt;/b&gt;&lt;/li&gt; 
 &lt;li&gt;하지만 대안책은 있음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;카운터를 기반으로하는 &lt;b&gt;논리적 시계(Logical clock)&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시계 읽기는 신뢰 구간이 있다. (Clock readings have a confidence interval)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;불확실성 경계는 시간 출처(time source)를 기반으로 계산할 수 있음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;i&gt;불확실성 = 동기화 후 드리프트 오차 + NTP 서버의 오차 + RTT&lt;/i&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;아쉽게도 대부분 시스템은 불확실성을 노출하지 않음. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;`clock_gettime()`은 해당 타임스탬프의 예상 오차 범위를 말해주지 않음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;구글의 &lt;b&gt;트루타임(TrueTime) API&lt;/b&gt;는 로컬 시계의 신뢰 구간을 명시적으로 보고함.&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;반환 값 : [earliest, latest]&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;blockquote data-ke-style=&quot;style2&quot;&gt;전역 스냅숏용 동기화된 시계 (Synchronized clocks for global snapshots)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;스패너(Spanner)&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;트루타임 API가 보고한 시계 신뢰 구간을 사용하여 스냅숏 구현한다.&lt;/li&gt; 
   &lt;li&gt;두 개의 신뢰 구간이 있는데, 두 구간이 겹치지 않는다면 B는 분명히 A보다 나중에 실행 된 거임&lt;/li&gt; 
   &lt;li&gt;읽기 쓰기 트랜잭션을 커밋하기 전에 의도적으로 신뢰 구간 길이만큼 기다린다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 트랜잭션 타임스탬프가 인과성을 반영하는 것을 보장하기 위함임.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 이렇게 하면, &quot;데이터를 읽을 지도 모를 트랜잭션&quot;이 충분히 나중에 실행되는게 보장됨&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ 대기 시간을 가능하면 짧게 유지하기 위해, 스패너는 시계 불확실성을 가능하면 작게 유지해야 함.&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;size18&quot;&gt;&lt;br&gt;  3-4. 프로세스 중단 (Process Pauses)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;임차권 (lease)&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;다른 노드들에게 리더가 죽었다고 알 수 있게 해주는 방법 중 하나&lt;/li&gt;&lt;li&gt;타임아웃이 있는 잠금과 비슷함.&lt;/li&gt;&lt;li&gt;임차권을 획득한 노드는 임차권이 만료될 때까지 본인이 리더임을 알 수 있음.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 아래 코드의 문제점은?
while (true) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val request = getIncomingRequest()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 항상 임차권이 적어도 10초는 남아 있게 보장한다
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (lease.expiryTimeMillis - System.currentTimeMillis() &amp;lt; 10_000) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;lease = lease.renew()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (lease.isValid()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;process(request)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;동기화된 시계에 의존 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;임차권 만료 시간(lease.expiryTimeMillis)가 다른 장비에서 설정됐는데 로컬 시계랑 비교함.&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;`lease.isValid()` 줄 근처에서 15초 동안 멈춘다면?&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;&quot;stop-the-world&quot; GC가 동작하는 경우&lt;/li&gt; 
     &lt;li&gt;가상 장비가 &quot;suspend&quot; 됐다가 다시 &quot;resume&quot; 되는 경우&lt;/li&gt; 
     &lt;li&gt;운영체제가 다른 스레드로 컨텍스트 스위치하는 경우&lt;/li&gt; 
     &lt;li&gt;하이퍼바이저가 다른 가상 장비로 스위치되는 경우&lt;/li&gt; 
     &lt;li&gt;느린 디스크 I/O 작업이 있는 경우 (ex. 페이지 폴트, 스와핑 등등)&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;응답 시간 보장&lt;/blockquote&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;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;이를 이른바 엄격한 실시간 시스템(hard real-time systems)이라고 함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;실시간 시스템&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;서버가 클라이언트에게 데이터를 푸시하고 엄격한 응답 시간 제약 없이&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 스트림 처리하는 것을 나타냄&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;실시간을 보장하려면? 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&quot;실시간 운영체제(RTOS)&quot;가 필요함&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;대부분 서버측 데이터 처리 시스템에서 실시간 보장은 경제적이지도 적절하지도 않음.&lt;br&gt; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;blockquote data-ke-style=&quot;style2&quot;&gt;가비지 컬렉션의 영향을 제한하기 (Limiting the impact of garbage collection)&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;GC로 인한 중단을 노드가 중단되는 것으로 간주하고,&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 노드가 GC를 하는 동안, 다른 노드가 클라의 요청을 처리 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이 방법은 GC로 인한 중단을 클라한테 감추고 응답 시간을 줄여줌.&lt;/li&gt; 
   &lt;li&gt;지연 시간에 민감한 금융 거래 시스템에서 이 방법을 쓰는 곳도 있음.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;(방법1의 변형)&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;수명이 짧은 객체만 가비지 컬렉터를 사용하고, 수명이 긴 객체는 GC가 돌기전에 프로세스 재시작&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;➔ 이 방법들이 GC로 인한 중단을 완전히 막을 순 없지만, 애플리케이션에 유의미한 영향을 미칠 수 있음.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 4. 지식, 진실 그리고 거짓말 (Knowledge, Truth, and Lies)&lt;/h3&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;  4-1. 진실은 다수결로 결정된다.&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;대신 여러 분산 알고리즘은 정족수(quorum), 즉 노드들 사이의 투표에 의존한다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;리더와 잠금&amp;nbsp;&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&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;분산 시스템에서 이런 상황을 구현하려면 중의해야 함. 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;A 노드가 스스로를 유일한 노드라고 믿어도, 네트워크 끊겼거나 GC 중단의 이유로&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다른 노드들이 동의하지 않을 수 있음.&lt;/li&gt; 
   &lt;li&gt;즉, 다른 노드들이 A가 죽었다고 생각해서, 그들끼리 리더를 선출했을 수 있음.&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;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jjsmt/btsRaXEKy7H/KY9nAXHaEJthoXDfjcUJkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jjsmt/btsRaXEKy7H/KY9nAXHaEJthoXDfjcUJkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jjsmt/btsRaXEKy7H/KY9nAXHaEJthoXDfjcUJkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJjsmt%2FbtsRaXEKy7H%2FKY9nAXHaEJthoXDfjcUJkk%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;1448&quot; height=&quot;522&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;HBase 사례: 잠금을 잘못 구현해서 생긴 데이터 오염 버그 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;원인 : 잠금을 잘못 구현해서, GC 중단으로 인해서 클라1이 임차권이 유효하다고 판단&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;팬싱 토큰&lt;/blockquote&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcDCVK/btsQ9ZJPqn1/bdfxX6AsuUfT2uyklgyyZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcDCVK/btsQ9ZJPqn1/bdfxX6AsuUfT2uyklgyyZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcDCVK/btsQ9ZJPqn1/bdfxX6AsuUfT2uyklgyyZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcDCVK%2FbtsQ9ZJPqn1%2FbdfxX6AsuUfT2uyklgyyZK%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;1210&quot; height=&quot;540&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;HBase 같은 사례의 해결 방법 : 펜싱(fencing)&lt;/li&gt; 
 &lt;li&gt;&amp;nbsp;잠금 서버가 잠금이나 임차권을 승인할 때마다 &lt;b&gt;펜싱 토큰&lt;/b&gt;도 반환한다고 가정 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;펜싱토큰 : 잠금이 승인될 때마다 증가하는 숫자&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;메커니즘 : 자원 자체가 이미 처리된 것보다 오래된 토큰을 사용해서 쓰는 것을 거부함&lt;/li&gt; 
 &lt;li&gt;잠금 서비스로 주키퍼를 사용하면 트랜잭션id나 노드 버전을 펜싱 토큰으로 사용할 수 있음.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;  4-2. 비잔틴 결함(Byzantine fault)&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;비잔틴 결함 &amp;amp; 비잔틴 장군 문제&lt;/blockquote&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;➔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;신뢰할 수 없는 환경에서 합의해 도달하는 문제 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;&lt;b&gt;두 장군 문제&lt;/b&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;비잔틴 내결함성을 지난다.&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;웹 애플리케이션에서는 클라이언트의 행동이 임의적이고 악의적이라고 예상해야 함.&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;SQL injection, cross site scripting 막아야 함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;약한 형태의 거짓말&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&quot;거짓말&quot;로 부터 보호해주는 메커니즘을 소프트웨어에 추가하는게 가치가 있을 수 있음.&lt;/li&gt; 
 &lt;li&gt;근데 이런 보호 메커니즘이 비잔틴 내결함성을 지니진 않음. 그래도 할 가치는 있음. 
  &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
   &lt;li&gt;네트워크 오염 문제 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;애플리케이션 레벨에서 별도의 체크섬 검증을 추가&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;애플리케이션 입력 검증 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;입력한 값 검증하기&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;NTP 
    &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
     &lt;li&gt;여러 서버를 비교하여 이상치 제거&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ol&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;size18&quot;&gt;&lt;br&gt;  4-3. 시스템 모델과 현실&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;알고리즘 작성 방법&lt;/blockquote&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;이렇게 하기 위해서는 시스템엣어 발생할 것으로 예상되는 결함의 종류를 정형화 해야 함.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;타이밍 가정에 대해서 흔히 사용되는 시스템 모델&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;동기식 모델 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;부분 동기식 보델 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;비동기식 모델 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;가장 널리 쓰이는 세 가지 노드용 시스템 모델&lt;/blockquote&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;죽으면 중단(crash-stop) 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;노드가 한 번 죽으면 다시는 복구되지 않음&lt;/li&gt; 
   &lt;li&gt;단순 시뮬레이션 환경, 임베디드 시스템&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;죽으면 복구(crash-recovery) 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;노드가 죽었다가 나중에 다시 살아남 (디스크 등 비휘발성 저장소는 유지)&lt;/li&gt; 
   &lt;li&gt;대부분의 현실 시스템&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;비잔틴(임의적) 장애(Byzantine Fault) 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;노드가 거짓 정보, 오류 메시지, 잘못된 결과를 반환할 수 있음&lt;/li&gt; 
   &lt;li&gt;악의적 공격, 하드웨어 결함, 버그 등&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;알고리즘의 정확성&lt;/blockquote&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;알고리즘이 정확하다(correct)는 게 어떤 의미인지 정의하기 위해 알고리즘의 속성을 기술할 수 있다.&lt;/li&gt; 
 &lt;li&gt;예를 들어 펜싱 토큰을 생성한다면... 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;그런데 노드가 다 죽거나, 무한 네트워크 지연이면 알고리즘도 아무것도 못함.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;안전성과 활동성&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;➔ 안전성과 활동성 속성을 구별하면 어려운 시스템 모델을 다루는데 도움이 됨.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;&amp;nbsp;안전성 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&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;/li&gt; 
 &lt;li&gt;활동성 
  &lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;가용성&lt;/li&gt; 
   &lt;li&gt;그 정의에 &quot;결국에(eventually)&quot; 이라는 단어를 포함하는 것&amp;nbsp;&lt;/li&gt; 
   &lt;li&gt;좋은 일은 결국 일어난다&lt;/li&gt; 
   &lt;li&gt;안전성과 반대로 동작(특정 시점 못찾지만, 미래에는 만족시킬 수 있음)&amp;nbsp;&lt;/li&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;분산 알고리즘은 시스템 모델의 모든 상황에서&lt;span&gt;&amp;nbsp;활동성 속성에 대해서는 경고를 하는게 허용됨.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시스템 모델을 현실 세계에 대응시키기&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt; 
   &lt;li&gt;이론적 모델만으로는 불완전하며, 예상치 못한 상황을 처리하는&lt;b&gt; “실제 코드”&lt;/b&gt;가 필수.&lt;/li&gt; 
   &lt;li&gt;즉, “불가능한 일이 실제로 일어나면 어떻게 대응할지까지 설계해야 한다” 는 의미.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;알고리즘이 올바르다고 증명됐더라도 반드시 현실 시스템에서의 구현도 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 언제나 올바르게 동작한다고 단정 지을 수 없음.&lt;/li&gt; 
&lt;/ul&gt;&lt;table style=&quot;border-collapse: collapse; width: 100.814%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.8147%;&quot;&gt;구분&lt;/td&gt;&lt;td style=&quot;width: 28.5466%;&quot;&gt;이론적 가정&lt;/td&gt;&lt;td style=&quot;width: 43.3364%;&quot;&gt;현실에서의 문제&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.8147%;&quot;&gt;&lt;b&gt;Crash-Recovery 모델&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 28.5466%;&quot;&gt;노드가 죽더라도 디스크의 데이터는 &lt;br&gt;안전하게 남아 있음&lt;/td&gt;&lt;td style=&quot;width: 43.3364%;&quot;&gt;디스크 오염, 펌웨어 버그, 하드웨어 인식 실패 등으로 &lt;br&gt;&lt;b&gt;데이터 손실 가능&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.8147%;&quot;&gt;&lt;b&gt;정족수(Quorum) 알고리즘&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 28.5466%;&quot;&gt;“기억하고 있다”는 노드의 선언을 신뢰&lt;/td&gt;&lt;td style=&quot;width: 43.3364%;&quot;&gt;노드가 이전 데이터 잊어버릴 수 있음 → &lt;b&gt;정확성 깨짐&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.8147%;&quot;&gt;&lt;b&gt;비잔틴이 아닌(non-Byzantine) 가정&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 28.5466%;&quot;&gt;노드가 악의적 행동은 하지 않음&lt;/td&gt;&lt;td style=&quot;width: 43.3364%;&quot;&gt;실제로는 버그, 하드웨어 결함 등으로 &lt;b&gt;예상치 못한 행동&lt;/b&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;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/데이터 중심 애플리케이션 설계</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/226</guid>
      <comments>https://gilbert9172.tistory.com/226#entry226comment</comments>
      <pubDate>Mon, 13 Oct 2025 21:49:30 +0900</pubDate>
    </item>
    <item>
      <title>Common Batch Patterns</title>
      <link>https://gilbert9172.tistory.com/224</link>
      <description>&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;일부 배치 작업은 Spring Batch에서 제공하는 기성(ready-made) 컴포넌트만으로도 구성할 수 있다.&lt;br /&gt;예를 들어, ItemReader와 ItemWriter 구현체들은 매우 다양한 시나리오를 커버할 수 있다.&lt;br /&gt;하지만 실무에서는 완벽히 일치하지 않는 요구사항이 많기 때문에, 일부 구간(특히 쓰기/처리)은 &lt;br /&gt;직접 구현해야 할 수도 있다.&lt;br /&gt;&lt;br /&gt;이 장에서는 커스텀 비즈니스 로직에서 자주 사용되는 몇 가지 공통 패턴의 예시를 제공한다.&lt;br /&gt;이러한 예시들은 주로 Listener 인터페이스를 활용한 것들이다. &lt;br /&gt;또한, 필요하다면 ItemReader나 ItemWriter도 Listener 인터페이스를 직접 구현해야 할 수도 있다.&lt;/blockquote&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;style3&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. Logging Item Processing and Failures&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #191e1e; text-align: start;&quot;&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Step 내에서 &lt;b&gt;아이템별로(error item-by-item) &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;(StepFactoryBean으로 생성되는) chunk-oriented Step은 이러한 요구를 간단히 처리할 수 있도록&lt;/li&gt;
&lt;li&gt;ItemReadListener(읽기 에러 처리용)와 ItemWriteListener(쓰기 에러 처리용)를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760096489527&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun job(): Job {
    return JobBuilder(&quot;sampleStep&quot;, jobRepository)
        ...
        .listener(ItemFailureLoggerListener())
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1760096215781&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ItemFailureLoggerListener : ItemListenerSupport&amp;lt;Any, Any&amp;gt;() {

    companion object {
        private val logger: Log = LogFactory.getLog(&quot;item.error&quot;)
    }

    override fun onReadError(ex: Exception) {
        logger.error(&quot;Encountered error on read&quot;, ex)
    }

    override fun onWriteError(ex: Exception, items: MutableList&amp;lt;out Any&amp;gt;?) {
        logger.error(&quot;Encountered error on write&quot;, ex)
    }
}&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;blockquote data-ke-style=&quot;style2&quot;&gt;에러 로깅/보정 처리는 메인 트랜잭션과 분리해서 커밋하자!&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 기반 Step에서는 처리/쓰기 단계에서 예외가 나면 &lt;b&gt;현재 트랜잭션이 롤백&lt;/b&gt;됨.&lt;/li&gt;
&lt;li&gt;이때 리스너(onWriteError, onProcessError 등)에서&lt;b&gt; DB에 에러 로그를 남기면?&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;➔ &lt;/b&gt;그 로그도 &lt;b&gt;같이 롤백되어 사라질&lt;/b&gt; 수 있음!&lt;/li&gt;
&lt;li&gt;따라서, 트랜잭션 전파 수준을 REQUIRES_NEW로 지정하는 방법을 추천!&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;&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;&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;❐ 2. Stopping a Job Manually for Business Reasons&lt;/h3&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;size18&quot;&gt;  2-1. Exception 던지기&lt;/p&gt;
&lt;pre id=&quot;code_1760098967934&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PoisonPillItemProcessor&amp;lt;T&amp;gt; : ItemProcessor&amp;lt;T, T&amp;gt; {

    override fun process(item: T): T? {
        if (isPoisonPill(item)) {
            throw PoisonPillException(&quot;Poison pill detected: $item&quot;)
        }
        return item
    }

    private fun isPoisonPill(item: T): Boolean {
        // 예: &quot;STOP&quot; 문자열을 만나면 종료
        return item == &quot;STOP&quot;
    }
}

class PoisonPillException(message: String) : RuntimeException(message)&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;exception을 던저서 배치 종료&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-2. NULL 반환하기&lt;/p&gt;
&lt;pre id=&quot;code_1760099003588&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class EarlyCompletionItemReader&amp;lt;T&amp;gt;(
    private val delegate: ItemReader&amp;lt;T&amp;gt;
) : ItemReader&amp;lt;T&amp;gt; {

    override fun read(): T? {
        val item = delegate.read() ?: return null
        return if (isEndItem(item)) null else item
    }

    private fun isEndItem(item: T): Boolean {
        // 예: 특정 값이 나오면 종료
        return item == &quot;STOP_SIGNAL&quot;
    }
}&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;ItemReader가 null을 반환하면 batch 완료로 간주&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-3. Custom CompletionPolicy 구현하기&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CompletionPolicy&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760099732996&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StepBuilder extends StepBuilderHelper&amp;lt;StepBuilder&amp;gt; {

    public &amp;lt;I, O&amp;gt; SimpleStepBuilder&amp;lt;I, O&amp;gt; chunk(
        int chunkSize, 
        PlatformTransactionManager transactionManager
    ) {
        return new SimpleStepBuilder&amp;lt;I, O&amp;gt;(this).transactionManager(transactionManager).chunk(chunkSize);
    }
    
    public &amp;lt;I, O&amp;gt; SimpleStepBuilder&amp;lt;I, O&amp;gt; chunk(
        CompletionPolicy completionPolicy,
        PlatformTransactionManager transactionManager
    ) {
        return new SimpleStepBuilder&amp;lt;I, O&amp;gt;(this).transactionManager(transactionManager).chunk(completionPolicy);
    }
    
    //...
}&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;CompletionPolicy는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;청크(chunk)가 언제 끝날지를 판단하는 전략&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;위 소스는 StepBuilder의 chunk 메소드&lt;/li&gt;
&lt;li&gt;이 중 두 번째 메소드를 보면 알겠지만, completionPolicy를 파라미터로 입력 받음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;예제 코드&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760100579030&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun stepWithTimeLimitPolicy(): Step {
    return StepBuilder(&quot;timeLimitedStep&quot;, jobRepository)
        .chunk&amp;lt;String, String&amp;gt;(SpecialCompletionPolicy(limit = 5, maxDurationSeconds = 10), platformTransactionManager)
        .reader(reader())
        .writer(writer())
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;SpecialCompletionPolicy.kt&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_1760100205193&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * SpecialCompletionPolicy
 * - 5개 아이템을 처리하거나
 * - 특정 아이템(&quot;STOP&quot;)을 만나거나
 * - 10초가 경과하면 청크를 종료
 */
class SpecialCompletionPolicy(
    private val limit: Int = 5,
    private val maxDurationSeconds: Long = 10
) : CompletionPolicy {

    private var counter = 0
    private var complete = false
    private var startTime: Instant? = null

    override fun start(context: RepeatContext): RepeatContext {
        counter = 0
        complete = false
        startTime = Instant.now()
        return context
    }

    override fun update(context: RepeatContext) {
        counter++
    }

    override fun isComplete(context: RepeatContext): Boolean {
        val elapsedSeconds = Duration.between(startTime, Instant.now()).seconds
        return counter &amp;gt;= limit || complete || elapsedSeconds &amp;gt;= maxDurationSeconds
    }

    override fun isComplete(
        context: RepeatContext,
        result: RepeatStatus
    ): Boolean {
        return isComplete(context)
    }

    fun markComplete() {
        complete = true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2-4. StepExecution에 flag 설정하기&lt;/p&gt;
&lt;pre id=&quot;code_1760101190914&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class TerminateOnPoisonReadListener : ItemReadListener&amp;lt;Any&amp;gt;, StepExecutionListener {

    private lateinit var stepExecution: StepExecution

    override fun beforeStep(stepExecution: StepExecution) {
        this.stepExecution = stepExecution
    }

    override fun afterRead(item: Any) {
        if (isPoisonPill(item)) {
            // Step 중단 플래그 설정 &amp;rarr; 이후 프레임워크가 JobInterruptedException 발생
            stepExecution.setTerminateOnly()
        }
    }

    override fun afterStep(stepExecution: StepExecution) = stepExecution.exitStatus

    private fun isPoisonPill(item: Any): Boolean =
        item == &quot;STOP&quot; // 예: 포이즌 신호
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1760101222862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun stepWithTerminateFlag(): Step = StepBuilder(&quot;terminateFlagStep&quot;, jobRepository)
    .chunk&amp;lt;String, String&amp;gt;(10, transactionManager)
    .reader(reader())
    .writer(writer())
    .listener(TerminateOnPoisonReadListener())
    .build()&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;특정 신호(포이즌)나 외부 조건을 만나면 &lt;b&gt;즉시 중단&lt;/b&gt;해야 할 때 사용 (결과는 &lt;span style=&quot;color: #ee2323;&quot;&gt;비정상 종료&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;정상적으로 끝내고 싶으면 CompletionPolicy를 구현&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;&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;&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;❐ 3. Adding a Footer Record&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Footer 달기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 쓰기 작업 후에 Footer(하단에 고정으로 나와야 하는 그런거)가 필요한 경우&lt;/li&gt;
&lt;li&gt;Spring Batch에서는 FlatFileFooterCallback 인터페이스를 제공해줌.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760106807493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun itemWriter(outputResource: Resource): FlatFileItemWriter&amp;lt;String&amp;gt; =
    FlatFileItemWriterBuilder&amp;lt;String&amp;gt;()
        .name(&quot;itemWriter&quot;)
        .resource(outputResource)
        .lineAggregator(lineAggregator())   // LineAggregator&amp;lt;String&amp;gt;
        .headerCallback(headerCallback())   // FlatFileHeaderCallback
        .footerCallback(footerCallback())   // FlatFileFooterCallback
        .build()&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;blockquote data-ke-style=&quot;style2&quot;&gt;인터페이스 정의&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760101780136&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface FlatFileFooterCallback {
    void writeFooter(Writer writer) throws IOException;
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3-1. Writing a Summary Footer&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출력 과정에서 정보를 집계(aggregate)해 파일 끝에 붙이는 Footer&lt;b&gt; 레코드&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 필요한 경우가 흔함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 footer는 보통 파일의 요약 정보를 제공하거나 체크섬(checksum)을 제공하는 용도로 사용됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760107717471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public TradeItemWriter tradeItemWriter() {
	TradeItemWriter itemWriter = new TradeItemWriter();

	itemWriter.setDelegate(flatFileItemWriter(null));

	return itemWriter;
}

@Bean
public FlatFileItemWriter&amp;lt;String&amp;gt; flatFileItemWriter(Resource outputResource) {
	return new FlatFileItemWriterBuilder&amp;lt;String&amp;gt;()
			.name(&quot;itemWriter&quot;)
			.resource(outputResource)
			.lineAggregator(lineAggregator())
			.footerCallback(tradeItemWriter())
			.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;TradeItemWriter.kt&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_1760107929238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TradeItemWriter(
    private val delegate: ItemWriter&amp;lt;Trade&amp;gt;
) : ItemWriter&amp;lt;Trade&amp;gt;, FlatFileFooterCallback {

    private var totalAmount: BigDecimal = BigDecimal.ZERO

    override fun write(items: Chunk&amp;lt;out Trade&amp;gt;) {
        // 1) 이번 청크의 합계를 먼저 산출
        var chunkTotal = BigDecimal.ZERO
        for (trade in items) {
            chunkTotal = chunkTotal.add(trade.amount)
        }

        // 2) 실제 쓰기 시도 (여기서 예외나 skip이 나면 totalAmount는 아직 증가하지 않음)
        delegate.write(items)

        // 3) 예외 없이 성공한 경우에만 누적 합계 갱신
        totalAmount = totalAmount.add(chunkTotal)
    }

    override fun writeFooter(writer: Writer) {
        writer.write(System.lineSeparator())
        writer.write(&quot;Total Amount Processed: $totalAmount&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;재시작 가능하게 ItemStream 으로 상태 저장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TradeItemWriter는 totalAmount를 내부 상태로 들고 있으므로 Step이 재시작 불가&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;ItemStream을 구현&lt;/li&gt;
&lt;li&gt;open에서 과거 값을 복원&lt;/li&gt;
&lt;li&gt;update에서 최신 값을 ExecutionContext에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이렇게 하면 재시작 시 이전 지점부터 이어서 합계를 계속 누적할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760108334102&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun tradeWriter(fileWriter: FlatFileItemWriter&amp;lt;Trade&amp;gt;): StreamSafeTradeItemWriter {
    val writer = StreamSafeTradeItemWriter(fileWriter)
    fileWriter.setFooterCallback(writer) // footer 연결
    return writer
}

// reader,writer가 ItemStream이면 Step이 자동으로 open/update/close 호출
@Bean
fun step(): Step = StepBuilder(&quot;tradeExport&quot;, jobRepository)
    .chunk&amp;lt;Trade, Trade&amp;gt;(100, transactionManager)
    .reader(reader())
    .writer(writer())
    .build()&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;StreamSafeTradeItemWriter.kt&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_1760107929238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class StreamSafeTradeItemWriter(
    private val delegate: ItemWriter&amp;lt;Trade&amp;gt;
) : ItemWriter&amp;lt;Trade&amp;gt;, FlatFileFooterCallback, ItemStream {

    companion object {
        private const val KEY_TOTAL = &quot;total.amount&quot;
    }

    private var totalAmount: BigDecimal = BigDecimal.ZERO

    override fun write(items: Chunk&amp;lt;out Trade&amp;gt;) {
        // 이번 청크 합계를 먼저 계산
        var chunkTotal = BigDecimal.ZERO
        for (t in items) chunkTotal = chunkTotal.add(t.amount)

        // 실제 쓰기(예외 발생 시 totalAmount는 아직 안 늘림)
        delegate.write(items)

        // 성공 시에만 누적 합계 반영
        totalAmount = totalAmount.add(chunkTotal)
    }

    // ===== FlatFileFooterCallback =====
    override fun writeFooter(writer: Writer) {
        writer.write(System.lineSeparator())
        writer.write(&quot;Total Amount Processed: $totalAmount&quot;)
    }

    // ===== ItemStream (재시작 상태 저장/복원) =====
    override fun open(executionContext: ExecutionContext) {
        if (executionContext.containsKey(KEY_TOTAL)) {
            totalAmount = executionContext[KEY_TOTAL] as BigDecimal
        }
    }

    override fun update(executionContext: ExecutionContext) {
        executionContext.put(KEY_TOTAL, totalAmount)
    }

    override fun close() { /* no-op */ }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 4. Driving Query Based ItemReaders&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왜 Driving Query 접근법을 사용할까?&lt;/blockquote&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;대규모 데이터셋의 cursor 문제&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➔ 이러한 문제로 인해 키&lt;b&gt;(주로 ID)&lt;/b&gt;를 기준으로 페이지 단위로 잘라서 반복 조회하는 Driving Query 방법 사용&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;blockquote data-ke-style=&quot;style2&quot;&gt;Driving Query Job example&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NQs1L/btsQ4PUPiZd/c4DuuwkYOOUVA6vWtgKzm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NQs1L/btsQ4PUPiZd/c4DuuwkYOOUVA6vWtgKzm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NQs1L/btsQ4PUPiZd/c4DuuwkYOOUVA6vWtgKzm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNQs1L%2FbtsQ4PUPiZd%2Fc4DuuwkYOOUVA6vWtgKzm0%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;1350&quot; height=&quot;892&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;892&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;전체 행(row)을 가져오는 대신, ID 컬럼만 선택&lt;/li&gt;
&lt;li&gt;이후 단계(Processor나 Writer 등)에서 이 ID를 이용해 필요한 데이터를 다시 조회하거나 처리함.&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;&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;&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;❐ 5. Multi-Line Records&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;docs 읽어보기&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;&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;&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;❐ 6. Executing System Commands&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 배치 작업은 배치 내부에서 외부 명령(command)을 실행해야 하는 경우가 있음.&lt;/li&gt;
&lt;li&gt;보통 이런 작업은 스케쥴러(cron 등)에서 따로 실행할 수 있지만...
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이렇게 하면 Spring Batch의 실행 메타데이터(JobExecution, StepExecution)와 분리됨.&lt;/li&gt;
&lt;li&gt;따라서, 일관된 관리가 어려워지는 단점이 발생함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그래서 Spring Batch는 이를 쉽게 관리하기 위해 아래의 구현체를 제공해줌.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Tasklet 구현체 중 하나인 &lt;b&gt;&lt;i&gt;SystemCommandTasklet&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760174012215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SystemCommandTasklet tasklet() {
    SystemCommandTasklet tasklet = new SystemCommandTasklet();

    // 실행할 명령어 (OS 명령)
    tasklet.setCommand(&quot;echo hello&quot;);

    // 명령어 실행 제한 시간 (밀리초 단위)
    tasklet.setTimeout(5000);

    return tasklet;
}&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 7. Handling Step Completion When No Input is Found&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SpringBatch의 기본 동작&lt;/blockquote&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&quot;할 일이 없다&quot;로 판단하고, 정상 종료(COMPLETED) 처리함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기본 제공되는 모든 ItemReader 구현체도 이 방식을 따름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;문제 상황&lt;/blockquote&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;이런 경우에도 Step이 &quot;정상 종료&quot;로 기록됨.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해결 방안: NoWorkFoundStepExecutionListener&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBatch는 이런 경우를 대비해, 입력이 없을 때 Step을 실패로 처리하도록 도와주는 구현체 제공&lt;/li&gt;
&lt;li&gt;그것이 바로 &lt;b&gt;&lt;i&gt;NoWorkFoundStepExecutionListener&lt;/i&gt;&lt;/b&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;&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;&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;❐ 8. Passing Data to Future Steps&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ExecutionContext의 역할&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step 간에 데이터를 전달해야 할 때는 ExecutionContext를 사용&lt;/li&gt;
&lt;li&gt;ExecutionContext는 두 가지가 있으며,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StepExecutionContext : Step 실행 중에만 유지되고, chunk 커밋마다 갱신&lt;/li&gt;
&lt;li&gt;JobExecutionContext : Job 전체 실행 동안 유지되며, 각 Step이 끝날 때 한 번씩 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;따라서, Step이 실행 중일 때는 모든 데이터를 &lt;b&gt;StepExecutionContext&lt;/b&gt;에 저장해야 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇지 않고 &lt;b&gt;JobExecutionContext&lt;/b&gt;에 저장하면? &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ➔ &lt;span style=&quot;color: #ee2323;&quot;&gt;Step이 실패&lt;/span&gt;할 경우 &lt;span style=&quot;color: #ee2323;&quot;&gt;해당 데이터&lt;/span&gt;는 영구 저장되지 않아 &lt;span style=&quot;color: #ee2323;&quot;&gt;손실될 수 있음.&lt;/span&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;blockquote data-ke-style=&quot;style2&quot;&gt;그래서 다른 Step으로 데이터 전달은 어떻게 할 수 있을까?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step이 종료 후, StepExecutionContext에 있던 데이터를 JobExecutionContext로 &lt;b&gt;승격(promote)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SpringBatch는 이걸 할 수 있게끔, &lt;b&gt;&lt;i&gt;ExecutionContextPromotionListener&lt;/i&gt;&lt;/b&gt;를 제공함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ExecutionContextPromotionListener 구현하기&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ExecutionContext에 있는 데이터중에서 승격해야 하는 항목들의 key를 리스너에 설정해야 함.&lt;/li&gt;
&lt;li&gt;또한 &lt;b&gt;프로모션이 발생해야 하는 종료 코드(Exit Code) 패턴 목록&lt;/b&gt;을 선택적으로 설정할 수 있음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로는 Step이 COMPLETED 되었을 때만 데이터가 승격&lt;/li&gt;
&lt;li&gt;필요하다면, &lt;b&gt;추가적인 종료 코드 패턴&lt;/b&gt;(NO_WORK_FOUND, WARNING 등)을 설정해 둘 수도 있음.&lt;/li&gt;
&lt;li&gt;기본값은 COMPLETED이며, 다른 모든 리스너와 마찬가지로 해당 Step에 등록해야 함.&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;blockquote data-ke-style=&quot;style2&quot;&gt;Sample Code&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760183489864&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class PromoteContextJobConfig(
    private val jobRepository: JobRepository,
    private val platformTransactionManager: PlatformTransactionManager,
    private val savingItemWriter: SavingItemWriter,
    private val retrievingItemWriter: RetrievingItemWriter,
    private val promotionListener: PromotionListener
) {

    // ====== Job ======
    @Bean
    fun job1(): Job =
        JobBuilder(&quot;job1&quot;, jobRepository)
            .start(step1())
            .next(step2())
            .build()

    // ====== Step1: 값 저장 + 승격 ======
    @Bean
    fun step1(): Step =
        StepBuilder(&quot;step1&quot;, jobRepository)
            .chunk&amp;lt;String, String&amp;gt;(3, platformTransactionManager)
            .reader(ListItemReader(listOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;)))
            .writer(savingItemWriter)          // 처리 개수를 StepContext에 저장
            .listener(promotionListener.get()) // someKey 승격(JobContext로)
            .build()

    // ====== Step2: 승격된 값 사용 ======
    @Bean
    fun step2(): Step =
        StepBuilder(&quot;step2&quot;, jobRepository)
            .chunk&amp;lt;String, String&amp;gt;(1, platformTransactionManager)
            .reader(ListItemReader(listOf(&quot;F&quot;)))
            .writer(retrievingItemWriter)
            .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;SavingItemWriter.kt&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_1760183587196&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class SavingItemWriter : ItemWriter&amp;lt;String&amp;gt; {

    private lateinit var stepExecution: StepExecution

    @BeforeStep
    fun captureStepExecution(stepExecution: StepExecution) {
        this.stepExecution = stepExecution
    }

    override fun write(items: Chunk&amp;lt;out String&amp;gt;) {
        val stepCtx: ExecutionContext = stepExecution.executionContext
        var processed = stepCtx.getInt(&quot;someKey&quot;, 0)
        processed += items.size()                 // 이번 chunk에서 처리한 개수 누적
        stepCtx.putInt(&quot;someKey&quot;, processed)      // StepContext에 저장(&amp;rarr; Step 끝나면 승격)
        println(&quot;[step1] processed so far = $processed&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;RetrievingItemWriter.kt&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_1760183587196&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RetrievingItemWriter : ItemWriter&amp;lt;Any&amp;gt; {

    private var someObject: Any? = null

    @BeforeStep
    fun retrieveInterstepData(stepExecution: StepExecution) {
        val jobExecution: JobExecution = stepExecution.jobExecution
        val jobCtx: ExecutionContext = jobExecution.executionContext
        someObject = jobCtx.get(&quot;someKey&quot;)
        println(&quot;[step2] promoted value = $someObject&quot;)
    }

    override fun write(items: Chunk&amp;lt;out Any&amp;gt;) {
        // 실제 비즈니스 로직에서 someObject를 활용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/Spring Batch docs</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/224</guid>
      <comments>https://gilbert9172.tistory.com/224#entry224comment</comments>
      <pubDate>Fri, 10 Oct 2025 18:51:41 +0900</pubDate>
    </item>
    <item>
      <title>Retry</title>
      <link>https://gilbert9172.tistory.com/223</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ Retry&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Retry&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Batch 처리에서 일시적인 오류(transient error) 때문에 전체 작업이 실패하는 걸 방지하기 위해,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 자동 재시도(retry) 메커니즘을 제공한다.&lt;/li&gt;
&lt;li&gt;예를 들어 이런 경우에 유용하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;네트워크 일시 장애로 API 호출 실패&lt;/li&gt;
&lt;li&gt;DB Deadlock 발생 (DeadlockLoserDataAccessException)&lt;/li&gt;
&lt;li&gt;외부 시스템 응답 지연&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;491&quot; data-start=&quot;399&quot;&gt;실패 시 바로 중단하지 않고 일정 횟수까지 재시도 하는 방식으로 안정성을 높인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Retry 기능의 분리&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Batch 2.2.0 이후, Retry 기능은 별도의 라이브러리로 분리됐다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예전엔 RepeatTemplate 내부에서 Retry 로직을 직접 포함했지만&lt;/li&gt;
&lt;li&gt;지금은 &lt;b&gt;Spring Retry&lt;/b&gt;가 RetryTemplate, BackOff, Policy 등을 담당하고,&lt;/li&gt;
&lt;li&gt;Spring Batch는 단지 &lt;b&gt;이를 통합해서 사용하는 구조&lt;/b&gt;예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, Spring Batch의 내부 Retry 동작은 실제로 &lt;b&gt;Spring Retry의 API&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 기반으로 동작한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소스코드&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760089623933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RetryOperations.java
public interface RetryOperations {...}
  
// BatchRetryTemplate.java
public class BatchRetryTemplate implements RetryOperations {...}

// FaultTolerantStepBuilder.java
public class FaultTolerantStepBuilder&amp;lt;I, O&amp;gt; extends SimpleStepBuilder&amp;lt;I, O&amp;gt; {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/Spring Batch docs</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/223</guid>
      <comments>https://gilbert9172.tistory.com/223#entry223comment</comments>
      <pubDate>Fri, 10 Oct 2025 18:50:22 +0900</pubDate>
    </item>
    <item>
      <title>Scaling and Parallel Processing</title>
      <link>https://gilbert9172.tistory.com/222</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. Multi-thread Step&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 2. Parallel Steps&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 3. Remote Chunk&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Remote Chunk란?&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cijWiW/btsQ6OUxTi8/ngLQghYA0uGWmNr7KgDLK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cijWiW/btsQ6OUxTi8/ngLQghYA0uGWmNr7KgDLK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cijWiW/btsQ6OUxTi8/ngLQghYA0uGWmNr7KgDLK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcijWiW%2FbtsQ6OUxTi8%2FngLQghYA0uGWmNr7KgDLK1%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;974&quot; height=&quot;674&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;674&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;Reader/Processor와 Writer를 서로 다른 프로세스(노드)로 분리해서 실행하는 구조
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마스터 노드: 데이터를 읽고 나누어(Chunk 단위로) 전송&lt;/li&gt;
&lt;li&gt;워커 노드: 전달받은 Chunk를 실제로 Write (DB 저장 등)&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;blockquote data-ke-style=&quot;style2&quot;&gt;언제 효과적일까?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manager 컴포넌트는 하나의 프로세스로 동작하고, worker는 여러 개의 원격 프로세스로 동작함&lt;/li&gt;
&lt;li&gt;이 패턴은 Manager가 병목(bottleneck)이 되지 않을 때 가장 효율적&lt;/li&gt;
&lt;li&gt;따라서, &lt;b&gt;아이템을 읽는 작업보다 처리 작업이 더 비싼 경우&lt;/b&gt;에 특히 효과적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Manager&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manager는 Spring Batch의 Step 구현체&lt;/li&gt;
&lt;li&gt;청크(chunk) 단위의 아이템을 메시지 형태로 미들웨어(MQ 등)에 전송하는 일반화된 ItemWriter를 사용&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;➔ 즉, Manager는 일반적인 Step과 동일하게 동작하지만,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;714&quot; data-start=&quot;607&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;633&quot; data-start=&quot;607&quot;&gt;ItemReader &amp;rarr; 데이터를 읽고&lt;/li&gt;
&lt;li data-end=&quot;666&quot; data-start=&quot;634&quot;&gt;ItemProcessor &amp;rarr; 데이터를 가공한 뒤&lt;/li&gt;
&lt;li data-end=&quot;714&quot; data-start=&quot;667&quot;&gt;ItemWriter &amp;rarr; &lt;b&gt;직접 DB에 쓰지 않고 메시지로 전송&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760084770603&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun masterStep(): Step {
    return StepBuilder(&quot;masterStep&quot;, jobRepository)
        .chunk&amp;lt;String, String&amp;gt;(10, txManager)
        .reader(itemReader)
        .processor(itemProcessor)
        .writer(chunkMessageWriter) // DB X &amp;rarr; MQ 전송
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Worker&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Worker는 사용 중인 미들웨어(Message Broker) 에 맞는 표준 리스너(listener) 로 구현
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 JMS를 사용한다면, Worker는 MessageListener 구현체가 된다.&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;ChunkProcessor 인터페이스를 통해 표준 ItemProcessor 또는 ItemWriter를 사용하여 데이터를 처리&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;➔ 즉, Worker는 MQ로부터 메시지를 수신하고 그 내용을 &amp;ldquo;청크 단위로&amp;rdquo; 처리&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2088&quot; data-start=&quot;1944&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1971&quot; data-start=&quot;1944&quot;&gt;Manager &amp;rarr; MQ로 Chunk 전송&lt;/li&gt;
&lt;li data-end=&quot;2013&quot; data-start=&quot;1972&quot;&gt;Worker &amp;rarr; 메시지 수신 (MessageListener 역할)&lt;/li&gt;
&lt;li data-end=&quot;2088&quot; data-start=&quot;2014&quot;&gt;Worker 내부에서는 &amp;rarr; ItemProcessor, ItemWriter를 사용해 데이터를 처리 (예: DB 저장)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1760084887635&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class WorkerMessageListener(
    itemWriter: ItemWriter&amp;lt;String&amp;gt;
) : MessageListener {

    private val chunkProcessor: ChunkProcessor&amp;lt;String&amp;gt; =
        SimpleChunkProcessor(null, itemWriter)

    override fun onMessage(message: Message) {
        val items: List&amp;lt;String&amp;gt; = extractItems(message)
        chunkProcessor.process(items)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;언제 효과적일까?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manager 컴포넌트는 하나의 프로세스로 동작하고, worker는 여러 개의 원격 프로세스로 동작함&lt;/li&gt;
&lt;li&gt;이 패턴은 Manager가 병목(bottleneck)이 되지 않을 때 가장 효율적&lt;/li&gt;
&lt;li&gt;따라서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;아이템을 읽는 작업보다 처리 작업이 더 비싼 경우&lt;/b&gt;에 특히 효과적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;부하 분산&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이템들은 &lt;b&gt;동적으로 분할(dynamically divided)&lt;/b&gt;되어 미들웨어(Message Broker) 를 통해 분산&lt;/li&gt;
&lt;li&gt;따라서 모든 리스너(Worker)가 적극적으로 메시지를 소비하는(eager consumer) 방식이라면,
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자동으로 부하 분산(load balancing)이 이루어짐&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;&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;&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;❐ 4. Partitioning&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Partitioning&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Batch는 &lt;b&gt;Step execution을 분할&lt;/b&gt;해서&lt;b&gt; 원격으로 실행&lt;/b&gt;할 수 있는 SPI도 제공
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쉽게 말하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Step 자체를 여러 개로 쪼개서 각각 다른 프로세스(또는 서버)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 실행&lt;/li&gt;
&lt;li&gt;SPI : Service Provider Interface&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Worker들은 별도의 Step 인스턴스로 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Manager가 Worker에게 보내는 메시지는 &lt;b&gt;내구성(durability),&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전달 보장(guaranteed delivery)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 필요하지 않음.&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 MQ에 반드시 영구 저장할 필요가 없다는 뜻&lt;/li&gt;
&lt;li&gt;왜냐하면 Worker 실행은 &lt;b&gt;Spring Batch의 JobRepository 메타데이터&lt;/b&gt;에 의해 관리되기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이해하기 쉬운 예시&lt;br /&gt;➔ 회원 테이블을 ID 기준으로 4개 구간으로 나누고, 각 구간을 다른 서버가 동시에 처리하고 싶다면?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3274&quot; data-start=&quot;3264&quot;&gt;Master
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3338&quot; data-start=&quot;3275&quot;&gt;StepPartitioner가 4개의 Partition(1~2500, 2501~5000, &amp;hellip;) 생성&lt;/li&gt;
&lt;li data-end=&quot;3387&quot; data-start=&quot;3339&quot;&gt;PartitionHandler가 각 Partition을 원격 Worker로 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3400&quot; data-start=&quot;3389&quot;&gt;Workers
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3468&quot; data-start=&quot;3401&quot;&gt;각 서버에서 동일한 Step 실행 (SELECT * FROM members WHERE id BETWEEN ...)&lt;/li&gt;
&lt;li data-end=&quot;3479&quot; data-start=&quot;3469&quot;&gt;각각 결과 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Partitioning의 핵심 SPI 구조를 구성하는 3요소 이해하기&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8Q1b/btsQ4WfrwVn/2TdJM5TQBwgLxMiqwg1lvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8Q1b/btsQ4WfrwVn/2TdJM5TQBwgLxMiqwg1lvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8Q1b/btsQ4WfrwVn/2TdJM5TQBwgLxMiqwg1lvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8Q1b%2FbtsQ4WfrwVn%2F2TdJM5TQBwgLxMiqwg1lvk%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;1124&quot; height=&quot;722&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;722&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;Spring Batch의 SPI의 구성
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;PartitionStep&lt;/b&gt;이라는 특별한 Step 구현체&amp;nbsp;&lt;/li&gt;
&lt;li&gt;환경에 맞게 구현해야 하는&lt;b&gt; 두 개의 전략 인터페이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;StepExecutionSplitter : Step을 여러 Partition으로 나누는 전략&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;PartitionHandler :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;각 Partition을 분배하고 Worker에서 실행되게 하는 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;&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;size18&quot;&gt;  4-1. PartitionHandler&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manager가 만든 각 StepExecution(파티션 단위 작업)을 실제로 Worker에게 전달하는 역할
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;멀티 스레드 방식으로 Step을 병렬 실행해주는 기본 PartitionHandler&lt;/b&gt;를 제공함&lt;/li&gt;
&lt;li&gt;데이터를 &lt;b&gt;어떻게 분할(Split)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 할지, &lt;/span&gt;또는 여러 Step 실행 결과를 &lt;b&gt;어떻게 합칠(Aggregate)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 지 알 필요 없음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일반적으로 PartitionHandler는 &lt;b&gt;복구(resilience)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 나 &lt;/span&gt;&lt;b&gt;장애조치(failover)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 를 다룰 필요도 없음.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;왜냐하면 이런 기능들은 대부분 사용 중인 &lt;b&gt;메시징 시스템&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이나 &lt;/span&gt;&lt;b&gt;그리드 플랫폼&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 담당하기 때문&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;메시징이나 Worker 실패와 상관없이,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;부분 재실행 가능&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;JobRepository에 저장된 메타데이터를 기준으로 &lt;b&gt;&amp;ldquo;어떤 파티션이 완료됐는지&amp;rdquo;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 를 알고 있으므로 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PartitionHandler 인터페이스는 다양한 환경에 맞게 구현할 수 있음.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  4-2. Partitioner&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Partitioner란?&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 StepExecution을 실행하기 위한 &lt;b&gt;ExecutionContext&lt;/b&gt;를 생성하는 것
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, &lt;b&gt;&quot;작업을 어떻게 나눌지&amp;rdquo;&lt;/b&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;blockquote data-ke-style=&quot;style2&quot;&gt;인터페이스 정의&lt;/blockquote&gt;
&lt;pre id=&quot;code_1760087321943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Partitioner {
    Map&amp;lt;String, ExecutionContext&amp;gt; partition(int gridSize);
}

// &quot;partition1&quot; -&amp;gt; { startId=1, endId=1000 }
// &quot;partition2&quot; -&amp;gt; { startId=1001, endId=2000 }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;983&quot; data-start=&quot;830&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;895&quot; data-start=&quot;830&quot;&gt;&lt;b&gt;gridSize&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;895&quot; data-start=&quot;830&quot;&gt;나눌 파티션 개수 (예: 4개라면 4개의 StepExecution 생성)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;896&quot;&gt;&lt;b&gt;return Map&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;896&quot;&gt;key: 파티션 이름&lt;/li&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;896&quot;&gt;value: ExecutionContext (해당 파티션에서 사용할 변수들)&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;&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;size18&quot;&gt;  4-3. Binding Input Data to Steps&lt;/p&gt;
&lt;blockquote data-end=&quot;270&quot; data-start=&quot;215&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;Partitioner가 생성한 ExecutionContext 데이터를 Step에 바인딩하는 방법&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;352&quot; data-start=&quot;272&quot;&gt;Spring Batch에서 Partitioning을 사용할 때, 각 파티션은 서로 다른 데이터를 처리해야 함.&lt;/li&gt;
&lt;li data-end=&quot;352&quot; data-start=&quot;272&quot;&gt;예를 들어,
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;373&quot; data-start=&quot;353&quot;&gt;파티션마다 다른 파일을 읽거나&lt;/li&gt;
&lt;li data-end=&quot;396&quot; data-start=&quot;374&quot;&gt;서로 다른 ID 구간을 처리하거나&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;397&quot;&gt;날짜별로 분리된 데이터를 다루거나&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;397&quot;&gt;이때 핵심은 바로
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;397&quot;&gt;Partitioner가 생성한 &lt;b&gt;ExecutionContext&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 &lt;/span&gt;실제 Step의 Reader나 Writer에 &lt;b&gt;동적으로 바인딩&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하는 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Partitioner가 만든 ExecutionContext 데이터를 &lt;b&gt;@StepScope Bean&lt;/b&gt;과 &lt;b&gt;SpEL&lt;/b&gt;로 런타임에 바인딩하면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 하나의 Step으로 여러 파티션 데이터를 유연하게 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Book/Spring Batch docs</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/222</guid>
      <comments>https://gilbert9172.tistory.com/222#entry222comment</comments>
      <pubDate>Fri, 10 Oct 2025 18:16:41 +0900</pubDate>
    </item>
    <item>
      <title>Item processing</title>
      <link>https://gilbert9172.tistory.com/221</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;❐ 1. Chaining itemProcessor&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemProcessor로 부가적인 로직을 짤 수 있음.&lt;/li&gt;
&lt;li&gt;근데 ItemProcessor를 체이닝을 걸어서 쓰고 싶다면?&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1759938472189&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class BatchConfig {

    @Bean
    fun ioSampleJob(jobRepository: JobRepository, step1: Step): Job =
        JobBuilder(&quot;ioSampleJob&quot;, jobRepository)
            .start(step1)
            .build()

    @Bean
    fun step1(
        jobRepository: JobRepository,
        transactionManager: PlatformTransactionManager
    ): Step =
        StepBuilder(&quot;step1&quot;, jobRepository)
            .chunk&amp;lt;Any, Any&amp;gt;(2, transactionManager)
            .reader(fooReader())
            .processor(compositeProcessor())
            .writer(foobarWriter())
            .build()

    @Bean
    fun compositeProcessor(): CompositeItemProcessor&amp;lt;Any, Any&amp;gt; =
        CompositeItemProcessor&amp;lt;Any, Any&amp;gt;().apply {
            setDelegates(listOf(FooProcessor(), BarProcessor()))
        }

    // 아래 메서드/클래스들은 실제 구현에 맞게 정의되어 있어야 합니다.
    fun fooReader() = /* ItemReader&amp;lt;Any&amp;gt; */ TODO()
    fun foobarWriter() = /* ItemWriter&amp;lt;Any&amp;gt; */ TODO()

    class FooProcessor : ItemProcessor&amp;lt;Any, Any&amp;gt; {
        override fun process(item: Any): Any? = /* transform */ item
    }

    class BarProcessor : ItemProcessor&amp;lt;Any, Any&amp;gt; {
        override fun process(item: Any): Any? = /* transform */ item
    }
}&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;나머지는 읽어보면 될듯.&lt;/p&gt;</description>
      <category>Book/Spring Batch docs</category>
      <author>gilbert9172</author>
      <guid isPermaLink="true">https://gilbert9172.tistory.com/221</guid>
      <comments>https://gilbert9172.tistory.com/221#entry221comment</comments>
      <pubDate>Thu, 9 Oct 2025 00:48:20 +0900</pubDate>
    </item>
  </channel>
</rss>