<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Life Engineering</title>
    <link>https://catscats.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 20 Aug 2026 13:48:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>흑개1</managingEditor>
    <image>
      <title>Life Engineering</title>
      <url>https://tistory1.daumcdn.net/tistory/2716675/attach/dc89eea63fda4083a5831ee0772406ad</url>
      <link>https://catscats.tistory.com</link>
    </image>
    <item>
      <title>실험 플랫폼 설계 (Experimentation Platform)</title>
      <link>https://catscats.tistory.com/273</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실험 플랫폼&lt;/b&gt; 이란?: 실험은&amp;nbsp;&lt;b&gt;대조군(A)과 실험군(B)을 나누어 유입되는 사용자들의 반응을 통해 어떤 방법이 가장 효과적인지를 검증&lt;/b&gt;하는 과정, 실험 플랫폼을 이 과정을 자동화하는 플랫폼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 실험 metadata를 설정하는 admin, 집계 및 적재 component, group 배정 component 로 나뉨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 파트 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o5orH/btsPfuE6pMA/w0FvBkAMmJKdeH9tE1iTM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o5orH/btsPfuE6pMA/w0FvBkAMmJKdeH9tE1iTM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o5orH/btsPfuE6pMA/w0FvBkAMmJKdeH9tE1iTM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo5orH%2FbtsPfuE6pMA%2Fw0FvBkAMmJKdeH9tE1iTM0%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;1024&quot; height=&quot;544&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 플랫폼 어드민&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스별로 실험이나 기능 플래그를 생성하고 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공하는 기능은 아래와 같음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 정보와 기본 정보 관리
&lt;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;종료: 설정된 위너를 반환 (A, B 중 효과적이라고 판별된 그룹)&lt;/li&gt;
&lt;li&gt;보관: 보관 상태의 실험은 수정 불가, 그룹 분배 요청 시 기본 그룹 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;QA를 위한 실험 강제 할당&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; 사용 - 슬롯 할당에는 사용자의 식별자 값(회원번호, 디바이스 아이디)을 사용하여 해시값을 추출한 뒤 modular 연산을 적용한 뒤 결과값에 1을 더해 슬롯값 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실험 대상 범위와 실험 간 충돌: 동시에 여러 실험을 진행하는 경우 &lt;b&gt;실험 대상 범위 slot을 다르게 설정&lt;/b&gt;하면 실험 간 충돌로 인해 결과가 오염되는 것을 방지 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A사 사례: 실험을 sequential 하게 진행한다 ..?&lt;/li&gt;
&lt;li&gt;오염 방지를 위해 어떤 방법론이 있는지?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실험 이벤트 조회: 실험에서 발생한 모든 이벤트 유형을 조회하거나 등록, 수집된 이벤트 데이터를 통해 사용자의 클릭률, 전환율을 계산. 다음에 소개될 데이터 수집 및 집계 pipeline에서 일정 시간마다 실험 내에서 발생한 이벤트 유형을 추출하고 중복을 제거해 실험 플랫폼 DB에 저장함&lt;/li&gt;
&lt;li&gt;실험 목표 설정: 분자를 측정 지표, 분모를 기준 지표로 하여 목표를 설정할 수 있음. 분자 이벤트: 주문 이벤트(OrderEvent), 분모 이벤트: 장바구니 진입 이벤트(CartAccessEvent) 로 설정하면 장바구니 진입 이후 주문 이벤트 발생 비율을 구할 수 있음&lt;/li&gt;
&lt;li&gt;실험 결과: 그룹별 전환율과 A 그룹 대비 증감률 등을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그룹 분배 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 분배 서버는 사용자 정보(식별자 및 태그 정보 등)와 실험 키를 넘겨주면 사용자가 어떤 그룹에 분배되었는지 결과를 내려주는 역할을 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqvFNG/btsPgBb0JBU/MgDy3xCsrcKFLBJ8NF9bf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqvFNG/btsPgBb0JBU/MgDy3xCsrcKFLBJ8NF9bf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqvFNG/btsPgBb0JBU/MgDy3xCsrcKFLBJ8NF9bf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqvFNG%2FbtsPgBb0JBU%2FMgDy3xCsrcKFLBJ8NF9bf1%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;532&quot; height=&quot;600&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 마름모꼴 요소는 각 정책을 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 분배 결과를 얻기 위해서는 실험이나 기능 플래그 데이터(그룹 비율, 강제 할당 정보) 가 필요한데 이 정보는 Redis 에 저장되어 있음. 매번 Redis 에 요청을 보내는 대신 Local caching 을 적용하여 성능 향상 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 적재 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 수집하고 있지 않은 서비스도 API 를 통해 이벤트 데이터를 적재할 수 있도록 서비스 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 적재 데이터를 받아 로그로 남기고 airflow의 dag를 통해 실험 플랫폼 이벤트 테이블로 추출됨&lt;/p&gt;
&lt;h1&gt;데이터 추출/집계 아키텍쳐&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다양한 서비스들의 로그 데이터를 실험 플랫폼 이벤트 테이블(Glue Data Catalog Table)로 추출하거나 필요한 데이터를 실험 플랫폼 데이터베이스(RDS)로 전달하는 작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;airflow dag 의 task로 다음과 같은 추출/집계 작업을 진행하게 됨&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;Hive 테이블이란?
&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;PARTITION, Parquet, Metastore 등으로 성능 최적화 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hive 테이블&lt;/b&gt;은 HDFS에 저장된 대용량 로그 데이터를 SQL처럼 쉽게 다루기 위한 추상 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 플랫폼 이벤트 데이터 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스와 이벤트 적재 API를 통해 쌓인 로그 정보 기반으로 실험에 필요한 이벤트 데이터를 추출하고 이벤트 테이블에 적재(Hive Table)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 플랫폼 이벤트 테이블을 통해 실험에서 발생한 이벤트 유형을 추출하거나 결과를 집계할 수 있음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 이벤트 유형 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 플랫폼 이벤트 테이블에 적재된 데이터를 기반으로 중복이 제거된 실험 별 이벤트 유형(장바구니 진입, 주문 전환 등)을 실험 플랫폼 데이터베이스(실험 플랫폼 RDB)에 적재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 이벤트 테이블 &amp;rarr; 중복 제거 &amp;rarr; RDB 저장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB에 적재하여 어드민에서 실험에서 발생한 이벤트 유형 목록 확인 가능&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 결과 데이터 집계와 적재&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실험 플랫폼 RDB에 저장된 실험 데이터를 Hive 테이블로 적재&lt;/li&gt;
&lt;li&gt;실험 플랫폼 RDB에 저장된 실험 목표 데이터를 Hive 테이블로 적재&lt;/li&gt;
&lt;li&gt;실험 플랫폼 이벤트테이블에 실험 테이블 + 실험 목표 테이블을 결과 집계시 활용하여 RDB에 결과 데이터 적재&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;넷플릭스 예시:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DepFb/btsPhy6IMtH/so4KrE3K6oNqaFGBe2HUm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DepFb/btsPhy6IMtH/so4KrE3K6oNqaFGBe2HUm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DepFb/btsPhy6IMtH/so4KrE3K6oNqaFGBe2HUm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDepFb%2FbtsPhy6IMtH%2Fso4KrE3K6oNqaFGBe2HUm0%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;1100&quot; height=&quot;558&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;558&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;AB Server&lt;/b&gt;: 클라이언트(App)가 사용자 할당 요청을 보내면 사용자에게 어떤 실험 그룹을 할당할지 응답&lt;/li&gt;
&lt;li&gt;저장소: Cassandra + EVCache
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cassandar: 실험 메타데이터 및 유저 할당 정보 저장&lt;/li&gt;
&lt;li&gt;EVCache: 캐시 기능, Cassandra 직접 쿼리 막아서 성능 향상&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;Kafka &amp;rarr; Hive: 데이터 적재 및 분석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spark Streaming&lt;/b&gt;은 Kafka로부터 데이터를 실시간 ingest&lt;/li&gt;
&lt;li&gt;변환 후 Elasticsearch에 저장 &amp;rarr; &lt;b&gt;실시간 시각화 플랫폼 ABlaze&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;[참고]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CausalInferenceLab/Experimentation-platform-materials?tab=readme-ov-file&quot;&gt;https://github.com/CausalInferenceLab/Experimentation-platform-materials?tab=readme-ov-file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/9935/&quot;&gt;https://techblog.woowahan.com/9935/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/273</guid>
      <comments>https://catscats.tistory.com/273#entry273comment</comments>
      <pubDate>Sun, 13 Jul 2025 14:58:35 +0900</pubDate>
    </item>
    <item>
      <title>[System Design Interview] Chapter.13 검색어 자동 완성 시스템</title>
      <link>https://catscats.tistory.com/272</link>
      <description>&lt;h1&gt;요구사항&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능적 요구사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 입력하는 단어는 검색어의 첫 부분으로 한정&lt;/li&gt;
&lt;li&gt;5개의 자동완성 검색어가 표시&lt;/li&gt;
&lt;li&gt;질의는 영어로만 지원&lt;/li&gt;
&lt;li&gt;Spelling 체크 X&lt;/li&gt;
&lt;li&gt;시스템의 계산 결과는 순위 모델에 의해 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비기능적 요구사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 응답속도는 100밀리초 이내&lt;/li&gt;
&lt;li&gt;규모 확장성: 초당 24000 건의 질의(QPS) 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;시스템 구조&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자료 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나이브한 아키텍처는 아래와 같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;query frequency&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ramen&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;radiation&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 질의문을 저장하는 query 필드와 빈도를 저장하는 frequency 필드를 저장하는 DB 테이블을 만들어서 SELECT * from frequency_table WHERE query LIKE 'ra%' ORDER BY frequeny DESC LIMIT 5 이렇게 질의할 수도 있겠지만 데이터가 많아지면 데이터베이스가 병목이 될 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 정보를 저장하는 자료구조는 트라이(TRIE) 를 이용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AXNbr/btsOttk3JDX/XhLgYt91tgY7C4idYcGvlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AXNbr/btsOttk3JDX/XhLgYt91tgY7C4idYcGvlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AXNbr/btsOttk3JDX/XhLgYt91tgY7C4idYcGvlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAXNbr%2FbtsOttk3JDX%2FXhLgYt91tgY7C4idYcGvlK%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;683&quot; height=&quot;377&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;각 노드는 글자 하나를 저장하며, 해당 글자 다음에 등장할 수 있는 모든 글자의 개수인 26개의 자식 노드를 가질 수 있음&lt;/li&gt;
&lt;li&gt;각 트리 노드는 하나의 단어, 또는 prefix string 을 나타냄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p: prefix의 길이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n : 트라이 안에 있는 노드 갯수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c : 주어진 노드의 자식 노드 갯수&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;시간복잡도는 1. 해당 prefix 를 표현하는 노드를 찾음(O(p)) + 2. 해당 노드에서 시작하는 하위 트리 탐색하여 모든 유효 노드를 찾음(O(c) )+ 3.유효 노드를 정렬하여 가장 인기있는 검색어 k 개를 찾음(O(clogc) )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, &lt;b&gt;접두어를 최대 길이를 제한&lt;/b&gt;(O(p) &amp;rarr; O(작은 상수값) ) 하거나 &lt;b&gt;노드에 인기 검색어를 캐시&lt;/b&gt;하게 되면 시간 복잡도가 O(1) 가 바뀔 수 있게 됨. 즉 각 노드는 자식 노드 외에 해당 접두어에 대한 Top-K 정보를 저장하여 전체 트리를 탐색하지 않고도 빠르게 결과를 반환할 수 있도록 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색어 자동 완성 시스템의 서비스는 크게 2개로 나뉨&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;&lt;b&gt;데이터 수집 서비스&lt;/b&gt;: 사용자가 입력한 질의를 준실시간으로 수집하고 Trie 에 반영하는 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Request Flow&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYRvgM/btsOr3nARYS/Ks2ZZA1t2vOx40DKz27tr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYRvgM/btsOr3nARYS/Ks2ZZA1t2vOx40DKz27tr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYRvgM/btsOr3nARYS/Ks2ZZA1t2vOx40DKz27tr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYRvgM%2FbtsOr3nARYS%2FKs2ZZA1t2vOx40DKz27tr0%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;611&quot; height=&quot;421&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trie 를 단일 서버에 저장하면 한계가 있으므로 여러 노드에 분산 저장함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Zookeeper&lt;/b&gt; 는 Trie 샤드의 메타데이터 관리 및 조정의 역할을 수행함(=shard map manager). 접두어 범위를 나누고 어떤 노드가 어떤 범위를 담당하는지 기록. 각 노드는 prefix 기반으로 어떤 샤드에 요청해야 하는 지를 판단할 수 있음 (=service discovery 역할)&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;분산 캐시를 이용하여 각 결과를 캐시해 두거나 브라우저단에서 일정 시간동안 캐싱을 해서(e.g. cache-control 헤더) 후속 질의의 결과는 캐시에서 가져올 수 있도록 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 플로우는 다음과 같음&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 접두어 입력 (e.g. &quot;ba&quot;)&lt;/li&gt;
&lt;li&gt;Load Balancer가 임의의 서버(N1)에 요청 전달&lt;/li&gt;
&lt;li&gt;N1은 캐시 확인 &amp;rarr; 없으면 Zookeeper에서 어떤 노드(T2 등)가 담당하는지 확인&lt;/li&gt;
&lt;li&gt;T2에 질의 &amp;rarr; 자동완성 후보 받아서 캐시에 저장 + 사용자에게 응답&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Data Collection Flow&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1607&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FOGId/btsOrzUTeBv/JMdv6YekkHWkNRIqkrdAXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FOGId/btsOrzUTeBv/JMdv6YekkHWkNRIqkrdAXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FOGId/btsOrzUTeBv/JMdv6YekkHWkNRIqkrdAXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFOGId%2FbtsOrzUTeBv%2FJMdv6YekkHWkNRIqkrdAXK%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;1607&quot; height=&quot;202&quot; data-origin-width=&quot;1607&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터소스(사용자 검색 기록, 클릭한 링크 제목 .. etc)들에서 Phrase 와 Weight 쌍 수집(최근 검색 횟수, 클릭률, 검색 후 체류시간 .. etc)&lt;/li&gt;
&lt;li&gt;(Phrase, Weight) 쌍들은 Log Streaming System (e.g. Kafka) 에서 실시간으로 스트리밍됨&lt;/li&gt;
&lt;li&gt;실시간으로 스트리밍 되는 데이터를 기반으로 Aggregator 가 데이터를 수신하여 같은 phrase 는 계속 합쳐지게 함. 일정 주기마다 해당 phrase 에 대한 weight 를 누적해서 DB 에 저장하게 됨&lt;/li&gt;
&lt;li&gt;집계한 데이터를 시간 단위로 NoSQL DB (e.g. cassnadra) 에 저장함.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;[phrase]     [timestamp]     [sum_weight]
 &quot;bat&quot;       2025-06-08 10     35  
 &quot;bat&quot;       2025-06-08 11     18  
 &quot;bag&quot;       2025-06-08 11     12  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저장하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 검색어에 더 높은 가중치를 둘 수 있음&lt;/li&gt;
&lt;li&gt;오래된 phrase 는 weight 를 낮게 계산하거나 삭제하는 등의 처리를 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cassandra 를 사용한 이유?:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cassandra 는 쓰기 성능 중심 아키텍처임. LSM 트리 기반이라 디스크에 순차적으로 빠르게 기록 가능, Aggregator 가 수많은 phrase + timestamp + weight 데이터를 빠르게 기록 가능&lt;/li&gt;
&lt;li&gt;복합 키 (Parititon Key + Clustering Columns) 조합으로 정렬된 데이터를 저장할 수 있음: phrase + timestamp 조합으로 primary key 생성하면 phrase 별로 시간 순으로 weight 를 저장하고 효율적으로 조회가 가능함&lt;/li&gt;
&lt;li&gt;수평 확장성: 노드 추가만으로 자동 샤딩 + 리밸런싱 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 특정 시간마다 Applier 가 DB 에서 데이터를 꺼내 Trie 를 새로 구성하고 각 Trie 노드에 top-K 단어를 삽입하고, Trie 를 여러 복제 노드에 반영함&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 추가적으로 분산 캐시에 주기적으로 트라이 데이터베이스의 스냅샷을 떠서 읽기 연산 성능을 높일 수 있다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최적화 전략&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDN 사용: 자주 요청되는 prefix 결과를 사용자 근처의 CDN 에 캐싱해두기&lt;/li&gt;
&lt;li&gt;Prefetch 전략: ba 입력 시, bat, ball, bag 결과까지 미리 보내서 빠르게 대응한다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/272</guid>
      <comments>https://catscats.tistory.com/272#entry272comment</comments>
      <pubDate>Sun, 8 Jun 2025 02:45:18 +0900</pubDate>
    </item>
    <item>
      <title>Domain Driven Design 개요</title>
      <link>https://catscats.tistory.com/271</link>
      <description>&lt;h1&gt;DDD&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD는 &lt;b&gt;비즈니스 도메인을 중심으로 소프트웨어를 모델링&lt;/b&gt;하는 설계 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티, 밸류 객체, 애그리거트, 도메인 서비스 등으로 구조화하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 복잡성을 효과적으로 다루고 유지보수를 쉽게&lt;/b&gt; 만듦&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;도메인(Domain)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스의 문제 영역 (ex: 금융, 쇼핑몰, 물류 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;서브도메인(Subdomain)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인을 구성하는 하위 영역 (ex: 결제, 주문, 배송 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;유비쿼터스 언어(Ubiquitous Language)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 전문가와 개발자가 &lt;b&gt;같은 단어를 같은 의미로 사용하는 공통 언어&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: &quot;주문 승인&quot;, &quot;상품 할인&quot;, &quot;잔액 부족&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;엔티티(Entity)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고유 ID로 식별되는 객체 (ex: 사용자, 주문, 상품 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. &lt;b&gt;밸류 오브젝트(Value Object)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고유 ID 없이 값 자체로 의미가 있는 객체 (ex: 주소, 기간 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. &lt;b&gt;애그리거트(Aggregate)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나 이상의 엔티티/VO를 묶어 &lt;b&gt;일관성을 관리하는 단위&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하나의 루트 엔티티(Aggregate Root)만 외부와 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. &lt;b&gt;도메인 서비스(Domain Service)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 엔티티/밸류 객체 간 로직이 필요한 경우, 그걸 담당하는 &lt;b&gt;도메인 로직 보관소&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. &lt;b&gt;도메인 이벤트(Domain Event)&lt;/b&gt;&lt;/h3&gt;
&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zIHxH/btsOsBqvbjQ/U7w08ns522LGWhudMIpaGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zIHxH/btsOsBqvbjQ/U7w08ns522LGWhudMIpaGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zIHxH/btsOsBqvbjQ/U7w08ns522LGWhudMIpaGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzIHxH%2FbtsOsBqvbjQ%2FU7w08ns522LGWhudMIpaGK%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;581&quot; height=&quot;409&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애그리거트(Aggregate)란?&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;하나의 트랜잭션 경계이자 일관성을 유지하는 도메인 객체들의 집합&lt;/b&gt;&lt;/h2&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;&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;b&gt;하나의 루트 엔티티(Aggregate Root)&lt;/b&gt; 가 존재하고,&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749295917560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Order {
    private final OrderId id;
    private final List&amp;lt;OrderLine&amp;gt; orderLines = new ArrayList&amp;lt;&amp;gt;();
    private OrderStatus status;

    public void addOrderLine(Product product, int quantity) {
        orderLines.add(new OrderLine(product.getId(), quantity));
    }

    public void complete() {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException(&quot;Order already completed&quot;);
        }
        this.status = OrderStatus.COMPLETED;
        // 도메인 이벤트 발행 가능
    }
}&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;OrderLine 은 외부에서 직접 추가/삭제 못하고 반드시 Order 메서드를 통해서만 가능!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuRuYs/btsOrDQdWok/KvVCEHK6KNbC068Qn4K5r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuRuYs/btsOrDQdWok/KvVCEHK6KNbC068Qn4K5r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuRuYs/btsOrDQdWok/KvVCEHK6KNbC068Qn4K5r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuRuYs%2FbtsOrDQdWok%2FKvVCEHK6KNbC068Qn4K5r0%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;680&quot; height=&quot;614&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;614&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;Entity: 같은 ID 를 가지면 같은 Entity&lt;/li&gt;
&lt;li&gt;Value Object : (Value Object Instance 1 == Value Object Instance 2) 이면 같은 value object (보통 Java에서는 hashCode(), equals() 로 오버라이딩 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R6cQ8/btsOrGlSQoy/AFvNnl4xiqKfTGIxnemIkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R6cQ8/btsOrGlSQoy/AFvNnl4xiqKfTGIxnemIkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R6cQ8/btsOrGlSQoy/AFvNnl4xiqKfTGIxnemIkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR6cQ8%2FbtsOrGlSQoy%2FAFvNnl4xiqKfTGIxnemIkk%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;1191&quot; height=&quot;610&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;610&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;Aggregate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련 있는 entity와 value object를 하나의 그룹으로 묶은 것으로 항상 Valid 한 상태를 가져야 함. &lt;b&gt;Transactional boundary&lt;/b&gt; 이기도 함, &lt;b&gt;즉 Aggregate 내부에서 일어나는 모든 변경은 한 트랜잭션 내에서 일어나야 하며 성공하면 커밋되고 실패하면 롤백&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;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;Hexagonal Architecture&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjuz72/btsOsS6C5W9/P4vpmSxMRFBYy5dmznqK21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjuz72/btsOsS6C5W9/P4vpmSxMRFBYy5dmznqK21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjuz72/btsOsS6C5W9/P4vpmSxMRFBYy5dmznqK21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjuz72%2FbtsOsS6C5W9%2FP4vpmSxMRFBYy5dmznqK21%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;905&quot; height=&quot;618&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Hexagonal Architecture&quot; (헥사고날 아키텍처)는 &lt;span data-token-index=&quot;1&quot;&gt;애플리케이션의 핵심 도메인 로직을 외부 환경(웹, DB, 메시지 큐 등)으로부터 완전히 분리&lt;/span&gt;하기 위한 소프트웨어 아키텍처 패턴.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 로직을 중심에 두고, 외부 시스템과의 연결을 포트(Ports)와 어댑터(Adapters)로 추상화해서 연결하는 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749296054468&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;         [ Adapter: REST API ]
                 |
      [ Port: Inbound (Input) ]
                 |
         [   Application   ]  &amp;larr; 핵심 비즈니스 로직 (도메인)
                 |
      [ Port: Outbound (Output) ]
                 |
     [ Adapter: DB / Kafka / 외부 API ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boWq3Q/btsOs282xAb/ATsZPQkkniVxfq3MxKwpuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boWq3Q/btsOs282xAb/ATsZPQkkniVxfq3MxKwpuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boWq3Q/btsOs282xAb/ATsZPQkkniVxfq3MxKwpuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboWq3Q%2FbtsOs282xAb%2FATsZPQkkniVxfq3MxKwpuK%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;1130&quot; height=&quot;616&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;616&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;도메인: 비즈니스 규칙이 존재하는 핵심 영역. Aggregate, Entities, Value Object 가 포함됨.&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;Inbound Port: 애플리케이션에 &lt;b&gt;명령을 전달&lt;/b&gt; (Controller &amp;rarr; UseCase)&lt;/li&gt;
&lt;li&gt;Outbound Port: 애플리케이션이 외부에 &lt;b&gt;요청을 보냄&lt;/b&gt; (Repository, 외부 API 호출 등)&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: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Inbound Adapter: REST API Controller, CLI, Kafka Consumer 등&lt;/li&gt;
&lt;li&gt;Outbound Adapter: JPA Repository, Kafka Producer, 외부 REST Client 등&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cC5PC6/btsOsEgt0Co/JALhYwqjxYK1FLtz4mjLL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cC5PC6/btsOsEgt0Co/JALhYwqjxYK1FLtz4mjLL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cC5PC6/btsOsEgt0Co/JALhYwqjxYK1FLtz4mjLL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcC5PC6%2FbtsOsEgt0Co%2FJALhYwqjxYK1FLtz4mjLL1%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;1100&quot; height=&quot;619&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Driving Side 의 Adapter 는 Port 를 use 하고, Driven Side의 Adapter 는 Port를 implement 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;포트&lt;/b&gt;란 인터페이스이고, &lt;b&gt;어댑터&lt;/b&gt; 는 클라이언트에게 제공해야 할 인터페이스를 따르면서도 내부 구현은 서버의 인터페이스로 위임하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749296099319&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TotalRentalServiceImpl implements TotalRentalService {
 
    private final CustomerRepository customerRepository;
    private final RentalRepository rentalRepository;
    private final InventoryService inventoryService;
    private final RentalHistoryRepository rentalHistoryRepository;
 
    public TotalRentalServiceImpl(CustomerRepository customerRepository,
                                  RentalRepository rentalRepository,
                                  InventoryService inventoryService,
                                  RentalHistoryRepository rentalHistoryRepository) {
        this.customerRepository = customerRepository;
        this.rentalRepository = rentalRepository;
        this.inventoryService = inventoryService;
        this.rentalHistoryRepository = rentalHistoryRepository;
    }
 
    @Override
    public RentalHistory rent(RentalTarget target) {
        Customer borrower = customerRepository.find(target.customerId())
                                              .orElseThrow(() -&amp;gt; new NotFoundException(target.customerId()));
        Rental rental = rentalRepository.find(target.rentalId())
                                        .orElseThrow(() -&amp;gt; new NotFoundException(target.rentalId()));
        Item rentedItem = inventoryService.rent(rental, borrower)
                                          .orElseThrow(AlreadyRentedException::new);
        RentalHistory history = RentalHistory.of(UUID.randomUUID().toString(),
                                                 RentalSpec.of(borrower, rental),
                                                 rentedItem);
        rentalHistoryRepository.save(history);
        return history;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 주 포트 / 주 어댑터 &lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;867&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpfcu/btsOtqBOeLY/Oe1FwsTRGRzFC6a3XVKaxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpfcu/btsOtqBOeLY/Oe1FwsTRGRzFC6a3XVKaxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpfcu/btsOtqBOeLY/Oe1FwsTRGRzFC6a3XVKaxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqpfcu%2FbtsOtqBOeLY%2FOe1FwsTRGRzFC6a3XVKaxk%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;1928&quot; height=&quot;867&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;867&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러는 TotalRentalService 의 rent() 를 사용하고 HTTP 를 통한 인터페이스를 클라이언트에게 제공하여 클라이언트가 TotalRentalService 를 이용할 수 있게 하고 있는 역할을 하는 어댑터임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TotalRentalService 는 인터페이스를 제공하므로 포트임 외부에서 요청해야 동작하는 포트와 어댑터를 주요소(primary) 라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;부 포트 / 부 어댑터&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMrOJl/btsOqFU7UnH/7zHkdKtqIKHwvvjhBKshV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMrOJl/btsOqFU7UnH/7zHkdKtqIKHwvvjhBKshV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMrOJl/btsOqFU7UnH/7zHkdKtqIKHwvvjhBKshV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMrOJl%2FbtsOqFU7UnH%2F7zHkdKtqIKHwvvjhBKshV0%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;1822&quot; height=&quot;729&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;729&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository는 TotalRentalService 가 사용할 인터페이스를 제공하고 있기 때문에 포트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisRepository 는 Repository 의 인터페이스를 따르면서 내부적으로 Redis 프로토콜과 연결되는 어댑터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 호출하면 동작하는 포트와 어댑터를 부요소(secondary) 라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AFO9g/btsOsxVY8H2/F81gmtRACJ9qJmz6S9X6lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AFO9g/btsOsxVY8H2/F81gmtRACJ9qJmz6S9X6lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AFO9g/btsOsxVY8H2/F81gmtRACJ9qJmz6S9X6lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAFO9g%2FbtsOsxVY8H2%2FF81gmtRACJ9qJmz6S9X6lK%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;743&quot; height=&quot;444&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary의 경우, 만약 HTTP 외에 RPC 를 제공한다고 해도 RPC 어댑터만 만들고, 포트는 하나의 포트로 사용하면 됨..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secondary의 경우도, MySQL 의 어댑터를 포트의 인터페이스에 준하는 Redis 어댑터로 교체해서 사용할 수 있다 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어댑터를 애플리케이션에 맞출 것. (어댑터 DTO를 애플리케이션 내부로 넘기지 말것)&lt;/h4&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;Primary&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑터에서 도메인 모델로 변환한다. 어댑터 DTO 를 애플리케이션 도메인 모델로 전환하여, 어댑터가 전환되더라도 애플리케이션 내부 코드는 변하지 않도록 함&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class RentalController {
     
    private final TotalRentalService totalRentalService;
 
    // ...
    public Response&amp;lt;RentalHistoryView&amp;gt; rent(@RequestBody RentParam param) {
        // ...
        totalRentalService.rent(param); // 애플리케이션이 어댑터를 알게 되는 상황
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 어댑터 DTO 가 내부 애플리케이션 로직에 스며들게 되면 안됨&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class RentalController {
     
    private final TotalRentalService totalRentalService;
    // ...
 
    public Response&amp;lt;RentalHistoryView&amp;gt; rent(@RequestBody RentParam param) {
        // ...
        totalRentalService.rent(param.toRentTarget());
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 도메인 모델로 변환하여 어댑터 변경에도 애플리케이션은 영향을 받지 않도록 함&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Secondary&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpInventoryService 가 연동한 서비스의 DTO 를 그대로 반환한다면, API Spec 이 변경될 경우 새로 생성한 어댑터에서는 StoreItem 을 사용할 수 없다&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class HttpInventoryService implements InventoryService {
    // ...
 
    @Override
    public Optional&amp;lt;StoredItem&amp;gt; rent(Rental rental, Customer borrower) {
        // ... HTTP 통신
        // ... JSON 역직렬화
        return Optional.of(storedItem);    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 부어댑터가 부포트를 준수하도록 한다&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class HttpInventoryService implements InventoryService {
    // ...
 
    @Override
    public Optional&amp;lt;Item&amp;gt; rent(Rental rental, Customer borrower) {
        // ... HTTP 통신
        // ... JSON 역직렬화
        return Optional.of(storedItem.toItem());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Item 이라는 객체를 리턴하는 부포트를 준수하도록 하여 변경에 대응할 수 있게 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 인터페이스 자체를 특정 Adapter 에 치우치게 설계하지 말고 도메인 관점에서 도메인이 필요로 하는 인터페이스를 설계하자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=8Z5IAkWcnIw&quot;&gt;https://www.youtube.com/watch?v=8Z5IAkWcnIw&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bDWApqAUjEI&quot;&gt;https://www.youtube.com/watch?v=bDWApqAUjEI&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture&quot;&gt;https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture&lt;/a&gt;&lt;/p&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/271</guid>
      <comments>https://catscats.tistory.com/271#entry271comment</comments>
      <pubDate>Sat, 7 Jun 2025 20:36:51 +0900</pubDate>
    </item>
    <item>
      <title>Design a Notification Service / Usecase of Netflix's  Push Server System</title>
      <link>https://catscats.tistory.com/270</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 시스템 설계하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;요구 조건&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알림 지원 형태: 푸시 알림, SMS 메시지, 이메일&lt;/li&gt;
&lt;li&gt;soft real-time 시스템; 가능한 빨리 전달되어야 하나 약간의 지연은 OK&lt;/li&gt;
&lt;li&gt;iOS 단말, 안드로이드 단말, 랩톱/데스크탑 지원&lt;/li&gt;
&lt;li&gt;사용자가 알람을 받지 않도록 설정 가능&lt;/li&gt;
&lt;li&gt;성능: 약 천만 건의 모바일 푸시 알림 보낼 수 있어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;시스템 구성&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림 메커니즘 동작 방식&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XxUUR/btsObXf0B9f/qBsPLwJPdikBQTUfB3USmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XxUUR/btsObXf0B9f/qBsPLwJPdikBQTUfB3USmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XxUUR/btsObXf0B9f/qBsPLwJPdikBQTUfB3USmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXxUUR%2FbtsObXf0B9f%2FqBsPLwJPdikBQTUfB3USmK%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;492&quot; height=&quot;135&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;135&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;: 알림 요청을 만들어 사용자 단말에 알림을 실제로 보내는 제3자 알림 서비스에 전송함. 제3자 알림 서비스에는 APNs(iOS), FCM(Android), Twilio(SMS) 등이 있음. 알림 요청을 만들려면 다음과 같은 데이터가 필요.
&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;payload: 알림 내용을 담은 JSON 딕셔너리&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;시스템 구조도&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE0Net/btsOcmT9Ye7/i7cvWaeWvhHPYOgF1fHjN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE0Net/btsOcmT9Ye7/i7cvWaeWvhHPYOgF1fHjN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE0Net/btsOcmT9Ye7/i7cvWaeWvhHPYOgF1fHjN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE0Net%2FbtsOcmT9Ye7%2Fi7cvWaeWvhHPYOgF1fHjN0%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;713&quot; height=&quot;424&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;424&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;: 마이크로서비스 or 크론잡 등의 서비스로 알림 시스템 서버의 API 를 통해 알림을 보낼 서버&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;알림 전송 API: 각 마이크로서비스에게 알림 전송을 위한 API 제공&lt;/li&gt;
&lt;li&gt;Validation: 이메일 주소, 전화번호에 대한 기본적 검증 수행&lt;/li&gt;
&lt;li&gt;데이터베이스 및 캐시 질의: 알림에 포함시킬 데이터를 가져옴 (e.g. 알림 템플릿, 사용자 정보, 단말 정보, 알림 설정 등)&lt;/li&gt;
&lt;li&gt;알림 전송: 알림 데이터를 메시지 큐에 넣음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 큐&lt;/b&gt;: 버퍼 역할, 알림의 종류 별로 메시지 큐 구성, 컴포넌트 간 decoupling&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업 서버&lt;/b&gt;: 메시지 큐에서 전송할 알림을 꺼내 제3자 서비스로 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제 3자 서비스&lt;/b&gt;: 실제 사용자의 단말로 알림을 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안정성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 손실 방지&lt;/b&gt;: 작업 서버에서 third-party service 로 알림 전송 실패 시, 알림 데이터를 데이터베이스에 저장하고 재시도 매커니즘을 구현, 알림 로그를 데이터베이스에 유지하고 이를 재시도&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WxM2N/btsOclVfIRN/yczdAuNYnCCKAZg5putOyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WxM2N/btsOclVfIRN/yczdAuNYnCCKAZg5putOyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WxM2N/btsOclVfIRN/yczdAuNYnCCKAZg5putOyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWxM2N%2FbtsOclVfIRN%2FyczdAuNYnCCKAZg5putOyk%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;503&quot; height=&quot;292&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;292&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;b&gt;이벤트 ID&lt;/b&gt;를 검사하여 이전에 전송한 내역이 있는 중복된 이벤트면 버리고, 그렇지 않으면 알림을 발송&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타 고려사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;알림 템플릿&lt;/b&gt;: 알림 메시지의 일부 변수만 바꾸면 지정한 형식에 맞춰 알림을 만들어 낼 수 있게 함. 알림 서버에 해당 내용을 DB나 캐시에 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;알림 설정&lt;/b&gt;: 사용자의 알림 설정 내역 저장, 테이블에는 user_id, channel (알림 전송 채널), opt_in(알림 수신 여부 저장) 저장&lt;/li&gt;
&lt;li&gt;전송률 제한, 재시도, 큐 모니터링&lt;/li&gt;
&lt;li&gt;이벤트 추적: 데이터 분석 서비스를 통해 알림 이벤트를 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;Wrap-up&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3xljI/btsObVCvgWA/PNOC7PyDXOkivfqcvd6GQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3xljI/btsObVCvgWA/PNOC7PyDXOkivfqcvd6GQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3xljI/btsObVCvgWA/PNOC7PyDXOkivfqcvd6GQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3xljI%2FbtsObVCvgWA%2FPNOC7PyDXOkivfqcvd6GQK%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;1280&quot; height=&quot;1664&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1664&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;각 서비스가 알림 게이트웨이로 알림 전송
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;게이트웨이는 알림을 하나씩 수신하거나 배치 단위로 수신할 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;알림 게이트웨이는 알림 서비스로 알림을 전달, 알림 서비스에서는 유효성 검사, 형식 지정, 스케줄링 등이 실행. 템플릿을 이용해 메시지 형식을 정의 가능&lt;/li&gt;
&lt;li&gt;알림 서비스는 라우터(메시지 큐)로 메시지를 전달&lt;/li&gt;
&lt;li&gt;제3자 Channel 로 연결되어 알림을 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;Use Case: Scaling Push Messaging for Millions of Devices @Netflix&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Overview of the system&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPUnAS/btsOaEB8kfG/rxY53bOJ1bIVA7X8dTlSE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPUnAS/btsOaEB8kfG/rxY53bOJ1bIVA7X8dTlSE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPUnAS/btsOaEB8kfG/rxY53bOJ1bIVA7X8dTlSE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPUnAS%2FbtsOaEB8kfG%2FrxY53bOJ1bIVA7X8dTlSE1%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;722&quot; height=&quot;412&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Push registry&lt;/b&gt; that keep metadata about user-to-server mappings.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Push Library&lt;/b&gt; is a interface that provide interface for sending notification that hides all the details sendMessage() call puts the mssage into a push message queue, by using queues the senders and receivers can be decoupled.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Message Processor&lt;/b&gt; is the component that reads from the queue and perform the actual delivery; It looks up the push registry to identify which user is connected to which server, and then deliver to Zuul Push Servers.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Zuul Push Servers&lt;/b&gt; register users to push registry with the mapping information between particular user and serveer, and then send the push message using Websockets / SSE which provides persistent and open connections with multiple devices.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Component&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zuul push server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zuul push servers must keep 10000+ persistent connection. How is it possible?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tukgZ/btsObJPRX9T/izxzE66itgJjA9oun3RpI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tukgZ/btsObJPRX9T/izxzE66itgJjA9oun3RpI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tukgZ/btsObJPRX9T/izxzE66itgJjA9oun3RpI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtukgZ%2FbtsObJPRX9T%2FizxzE66itgJjA9oun3RpI1%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;692&quot; height=&quot;388&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1748101142318&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected void addPushHandlers(ChannelPipeline pl) {
 pl.addLast(new HttpServerCodec());
 pl.addLast(new HttpObjectAggregator());
 pl.addLast(getPushAuthHandler());  //custom auth handler 
 pl.addLast(new WebSocketServerCompressionHandler());
 pl.addLast(new WebSocketServerProtocolHandler());
 pl.addLast(getPushRegistrationHandler());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;use this async i/o programming model (all open connections to single thread) and netty &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Push Registry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;also, PushRegistrationHanlder(=handler for push registry) looks like this&lt;/p&gt;
&lt;pre id=&quot;code_1748101162873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyRegistration extends PushRegistrationHandler {
 @Override
 protected void registerClient(
 ChannelHandlerContext ctx,
 PushUserAuth auth,
 PushConnection conn,
 PushConnectionRegistry registry) {
 super.registerClient(ctx, authEvent, conn, registry);
 ctx.executor().submit(() -&amp;gt; storeInRedis(auth));
 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requirements for data store for push registry:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;low read latency (every push event is sent, the service would look up the push registry)&lt;/li&gt;
&lt;li&gt;record expiry (in case of the phantom connection to clients, it does clean such phantom registration records after certain timeout)&lt;/li&gt;
&lt;li&gt;sharding&lt;/li&gt;
&lt;li&gt;replications&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netflix use Dynomite (their own internal tools) which wraps around Redis and adds some features like auto-sharding, read/write quorum, cross-region replications&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Message Processor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;component to queue, route and deliver&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;queue&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netflix use kakfa for message queue&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To ensure the success delivery of message, you can subscribe to push delivery status queue or read it DB table in a batch mode where all of push messages are logged&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;route&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Also sender might have no idea which region that client might be connected, so message processing component take care of routing that message to the correct region on behalf of the senders By utilizing kafka, Netflix replicate messages in all the three regions so that they can actullay deliver them across the regions&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;processing the priority&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To implement priority between messages, they use different queues for different prioritoies so priority inversion which means that message of higher priority is made to wait behind bunch of meessages of lower priority could never happen&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;how they handle large throughput?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;They run multiple message processor instance in parallel to scale their message throughput;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container management system(Mezzos) makes it easy for them to quickly start spin up a bunch of message processor instances when the message processing is falling behind&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Critically, it automatically adjust the number of message processor according to number of messages which waits in the push message queue without any additional settings&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Operating Notification Service&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thundering herd problems&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The connection with client is stateful by using SSE or WebSocket; But when the quick deploy/rollback happens, all connections that connected to existing cluster are closed and they tried to reconnect to new cluster in a same time .. this makes &lt;b&gt;thundering herd problems&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;it give rise to a big spike to the server and increase the traffic at that time so they implement some solutions to disperse these traffics generated by reconnection&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;To limit the lifetime of single connection; so it auto-close after the certain amount of time, then client would try to reconnect to other server according to LB, so it could prevent stickiness to one server&lt;/li&gt;
&lt;li&gt;randomize the lifetime; so it can make the reconnection time can be dispersed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgw3N8/btsOa9Ihzby/JF2XGcC2mrg1O7OmQzT8LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgw3N8/btsOa9Ihzby/JF2XGcC2mrg1O7OmQzT8LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgw3N8/btsOa9Ihzby/JF2XGcC2mrg1O7OmQzT8LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgw3N8%2FbtsOa9Ihzby%2FJF2XGcC2mrg1O7OmQzT8LK%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;706&quot; height=&quot;358&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Currently, the server no longer closes connections directly. Instead, it asks the client to terminate the connection.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This design is based on how TCP handles connection teardown: the side that initiates the close enters TIME_WAIT state, which on Linux can hold file descriptor for up to 2 minutes. Since server-side file descriptor are more valuable (due to handling lage amount of concurrent connections), they shift this burden to client.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To handle unresponsive client, the server also includes timer; if the client doesn&amp;rsquo;t reply the request of closing within a certain timeout, the server forcefully terminates it&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Optimize push server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;one large &amp;amp; good server &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; more number of cheap &amp;amp; small servers&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;At the beginning, Netflix saw that most of connections are idle so usage of CPU and memory is low; so they chose one server which contains a tons of connections as long as the server can handle But that would make SOF, and again, thundering herd problem happened when the failure of the server was arosed. &lt;br /&gt;&lt;br /&gt;So they used &lt;b&gt;Goldilocks&lt;/b&gt; strategy which is using moderate spec of multiple servers. This allowed the system to remain resilient even if some servers failed.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;how to auto-scale the servers?&lt;/b&gt;:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;they use the number of &lt;b&gt;open connections&lt;/b&gt; for metrics (most of connections are idle, so CPU doesn&amp;rsquo;t work. RPS(request per second) doesn&amp;rsquo;t work as well, because the conection is long-lived which means that there are no continuous requests) so they export that metrics to CloudWatch, AWS can make configuration about auto-scaling based on that custom metrics.&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;출처:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&quot;&gt;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748101354082&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;system-design-101/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md at main &amp;middot; ByteByteGoHq/system-de&quot; data-og-description=&quot;Explain complex systems using visuals and simple terms. Help you prepare for system design interviews. - ByteByteGoHq/system-design-101&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&quot; data-og-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GFwD4/hyYWRpTfBc/EzEuSW596H1nkS6h6iz851/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/UIsu4/hyYYHG6YUD/M0cjtCxJkGW7SGSKKj2X21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GFwD4/hyYWRpTfBc/EzEuSW596H1nkS6h6iz851/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/UIsu4/hyYYHG6YUD/M0cjtCxJkGW7SGSKKj2X21/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;system-design-101/data/guides/how-does-netflix-scale-push-messaging-for-millions-of-devices.md at main &amp;middot; ByteByteGoHq/system-de&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explain complex systems using visuals and simple terms. Help you prepare for system design interviews. - ByteByteGoHq/system-design-101&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748101382401&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;system-design-101/data/guides/how-does-a-typical-push-notification-system-work.md at main &amp;middot; ByteByteGoHq/system-design-101&quot; data-og-description=&quot;Explain complex systems using visuals and simple terms. Help you prepare for system design interviews. - ByteByteGoHq/system-design-101&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&quot; data-og-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fH5kx/hyYYzI3pKr/FfBnQ9zNJk1EzedvskbMok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/behNdV/hyY0pyuiJw/oZFc2kQY6VgqswusTkJ0Q1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ByteByteGoHq/system-design-101/blob/main/data/guides/how-does-a-typical-push-notification-system-work.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fH5kx/hyYYzI3pKr/FfBnQ9zNJk1EzedvskbMok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/behNdV/hyY0pyuiJw/oZFc2kQY6VgqswusTkJ0Q1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;system-design-101/data/guides/how-does-a-typical-push-notification-system-work.md at main &amp;middot; ByteByteGoHq/system-design-101&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explain complex systems using visuals and simple terms. Help you prepare for system design interviews. - ByteByteGoHq/system-design-101&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.infoq.com/presentations/neflix-push-messaging-scale/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.infoq.com/presentations/neflix-push-messaging-scale/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748101389357&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Scaling Push Messaging for Millions of Devices @Netflix&quot; data-og-description=&quot;Susheel Aroskar talks about Zuul Push, a scalable push notification service that handles millions of &amp;quot;always-on&amp;quot; persistent connections from all the Netflix apps running. He covers the design of the Zuul Push server and reviews the design details of the ba&quot; data-og-host=&quot;www.infoq.com&quot; data-og-source-url=&quot;https://www.infoq.com/presentations/neflix-push-messaging-scale/&quot; data-og-url=&quot;https://www.infoq.com/presentations/neflix-push-messaging-scale/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btYdYW/hyYYA82r2J/01k3WA4Ku69WF5QYvQCKk0/img.jpg?width=320&amp;amp;height=320&amp;amp;face=101_53_218_180,https://scrap.kakaocdn.net/dn/jfYz0/hyYYxYM91c/jHuoVifPR29dJn1v3S90T0/img.jpg?width=320&amp;amp;height=320&amp;amp;face=101_53_218_180&quot;&gt;&lt;a href=&quot;https://www.infoq.com/presentations/neflix-push-messaging-scale/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.infoq.com/presentations/neflix-push-messaging-scale/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btYdYW/hyYYA82r2J/01k3WA4Ku69WF5QYvQCKk0/img.jpg?width=320&amp;amp;height=320&amp;amp;face=101_53_218_180,https://scrap.kakaocdn.net/dn/jfYz0/hyYYxYM91c/jHuoVifPR29dJn1v3S90T0/img.jpg?width=320&amp;amp;height=320&amp;amp;face=101_53_218_180');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Scaling Push Messaging for Millions of Devices @Netflix&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Susheel Aroskar talks about Zuul Push, a scalable push notification service that handles millions of &quot;always-on&quot; persistent connections from all the Netflix apps running. He covers the design of the Zuul Push server and reviews the design details of the ba&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.infoq.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이 위 링크의 영상은 꼭 보길 추천한다..&amp;nbsp;&lt;/p&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/270</guid>
      <comments>https://catscats.tistory.com/270#entry270comment</comments>
      <pubDate>Sun, 25 May 2025 00:43:38 +0900</pubDate>
    </item>
    <item>
      <title>Java의 비동기 프로그래밍 CompletableFuture</title>
      <link>https://catscats.tistory.com/269</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현대 웹 서비스: 다양한 소스의 콘텐츠를 조합, 여러 웹 서비스에 접근해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 즉 외부 서비스나 데이터베이스 결과를 기다리는 스레드를 블록함으로 연산 자원을 낭비하는 일은 피해야함&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성을 구현하는 자바 지원의 진화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runnable, thread &amp;rarr; ExecutorService (스레드 실행과 태스크 submit을 분리) &amp;rarr; Callable, Future (Runnable, Thread의 변형을 반환) &amp;rarr; RecursiveTask (분할/정복 알고리즘의 포크/조인 구현을 지원) &amp;rarr; 스트림/람다 지원에 기반한 병렬 프로세싱 &amp;rarr; CompletableFuture 지원(&lt;b&gt;분산 비동기 프로그래밍&lt;/b&gt; 지원) &amp;rarr; Flow 인터페이스 (발행-구독 프로토콜)&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;h3 data-ke-size=&quot;size23&quot;&gt;Executor와 스레드 풀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutorService 는 태스크를 제출하고 나중에 결과를 수집할 수 있는 인터페이스 제공&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExecutorService newFixedThreadPool(int nThreads)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 스레드라 불리는 nThreads를 포함하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀에서 사용하지 않은 스레드로 제출된 태스크를 먼저 온 순서대로 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀의 장점&lt;/p&gt;
&lt;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;동시에 수천 개의 테스크를 스레드 풀에 아무 오버헤드 없이 submit 가능&lt;/li&gt;
&lt;li&gt;큐의 크기 조정, 거부 정책, 태스크 종류에 따른 우선순위 등 설정 가능&lt;/li&gt;
&lt;/ol&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;불필요하게 sleep 되거나 I/O 기다리거나, 네트워크 연결 기다리는 태스크가 있다면 나머지 작업 스레드가 남은 태스크를 모두 처리해야 하므로 작업 효율성이 떨어짐&lt;/li&gt;
&lt;li&gt;프로그램 종료 전에 모든 스레드 풀을 종료해야 함(Thread.setDeaemon 메서드 제공)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄격한 포크 조인 / 여유로운 포크 조인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄격한 포크 조인:&lt;/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;스레드 생성 + join() 이 메서드 호출 내에 추가되는 형태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여유로운 포크 조인:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 호출에 의해 스레드가 생성되고 메서드가 반환된 이후에도 계속 실행&lt;/li&gt;
&lt;li&gt;메서드 반환된 이후에도 만들어진 태스크 실행이 계속되는 메서드를 &lt;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;실행 중인 스레드가 종료되지 않은 상황에서 main 메서드가 반환될 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기 API와 비동기 API&lt;/h2&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;int y = f(x);
int z = g(x);
System.out.println(y+z);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f, g가 서로 독립된 함수라면 &amp;rarr; f,g를 별도의 CPU 코어로 실행해서 f,g 중 오래 걸리는 작업의 시간으로 줄일 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 별도의 스레드로 f와 g를 실행하자&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedPool(2);
Future&amp;lt;Integer&amp;gt; y = executorService.submit(() -&amp;gt; f(x));
Future&amp;lt;Integer&amp;gt; z = executorService.submit(() -&amp;gt; g(x));
System.out.println(y.get() + z.get());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현할 수 있지만, 더럽다! &amp;rarr; CompletableFuture 이용하자, Flow 이용하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안은 2가지가 있다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Future 형식 API&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Future&amp;lt;Integer&amp;gt; y = f(x);
Future&amp;lt;Integer&amp;gt; z = g(x);
System.out.println(y.get() + z.get()); //두 Future가 완료되어 결과가 합쳐지기를 기다림
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액티브 형식 API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 형식의 프로그래밍을 이용&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void f(int x, IntConsumer deaelWithResult);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return 문으로 결과를 반환하는 게 아니라 결과가 준비되면 이를 람다로 호출하는 태스크를 만듬&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;int x = 5;
Result result = new Result();

f(x, (int y) -&amp;gt; {
	result.left = y;
	System.out.println((result.left + result.right)):
};

g(x, (int y) -&amp;gt; {
	result.right = y;
	System.out.println((result.left + result.right)):
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 쓰면 사실 값을 2번 출력할 수도 있고 .. 호출 합계가 정확하게 출력이 되지 않을 수있음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액티브 API는 보통 한 결과가 아니라 일련의 이벤트에 반응하도록 설계되었으므로, Future 을 이용하는 것이 더 적절하다, 나중에 스트림으로 연결&lt;/li&gt;
&lt;li&gt;Future API는 일회성의 값을 처리하는데 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로킹 동작은 해로운 것으로 간주&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀에서 잠을 자는 태스크나 블로킹 되는 태스크는 다른 태스크가 시작되지 못하게 막으므로 자원을 소비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법 &amp;rarr;&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;태스크를 블록하는 것보다는 다른 작업을 태스크로 제출하고 현재 태스크는 종료한다 , I/O 작업의 경우에도 블록하지 않는 &amp;lsquo;읽기 시작&amp;rsquo; 메서드를 호출하고 읽기 작업이 끝나면 이를 처리할 다음 태스크를 런타임 라이브러리에 스케줄하도록 요청하고 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 API 에서 예외 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CompletableFuture: exceptionally()&lt;/li&gt;
&lt;li&gt;Reactive: 예외 발생 시 콜백 생성, onError()&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CompletableFuture와 콤비네이터를 이용한 동시성&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExecutorService executorService = Executors.newFixedThreaadPool(10);
int x = 1337;

CompletableFuture&amp;lt;Integer&amp;gt; a = new CompletableFuture&amp;lt;&amp;gt;();
CompletableFuture&amp;lt;Integer&amp;gt; b = new CompletableFuture&amp;lt;&amp;gt;();
//c작업은 a,b 작업이 끝날 때까지 thread에서 실행되지 않는다
CompletableFuture&amp;lt;Integer&amp;gt; c = a.thenCombine(b, (y,z)-&amp;gt;(y+z)); 
executorService.submit(()-&amp;gt;a.complete(f(x)));
executorService.submit(()-&amp;gt;b.complete(g(x)));

System.out.println(c.get());
executorService.shutdown();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenCombine 사용 시 &amp;rarr; f , g가 끝난 다음에야 덧셈 계산이 실행, 병렬 실행의 효율성 높힐 수 있다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액티브 프로그래밍&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future는 한 번만 실행해 결과를 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 프로그래밍은 여러 Future 같은 객체를 통해 여러 결과를 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자가 구독할 수 있는 발행자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 연결을 구독이라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 연결을 이용해 메시지를 전송&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;interface Publisher&amp;lt;T&amp;gt; {
	void subscribe(Subscriber&amp;lt;? super T&amp;gt; subscriber);
}

interface Subscriber&amp;lt;T&amp;gt; {
	void onNext(T t);
	void onSubscribe(Subscription subscription); //Publisher &amp;lt;-&amp;gt; Subscriber 사이에 채널이 연결되면 첫 이벤트로 이 메서드가 호출됨
}

interface Subscription{  //이 객체 통해 요청
  void cancel();
  void request(long n);
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액티브 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 환경이 변화에 대응하도록 전체 아키텍처가 설계된 프로그램 가리킴&lt;/p&gt;
&lt;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;rarr; 리액티브 프로그래밍&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액티브 프로그래밍 인터페이ㅣ스 설계는 메시지 주도 속성을 반영,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리할 입력을 기다리고 결과를 다른 컴포넌트로 보내면서 시스템이 반응&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 API 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture가 기존 Future 에 반해 가질 수 있는 이점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Future 집합이 실행하는 모든 태스크의 완료를 기다림&lt;/li&gt;
&lt;li&gt;Future 집합에서 가장 빨리 완료되는 태스크를 기다렸다가 결과를 얻는다(&amp;rArr; 여러 태스크가 다양한 방식으로 같은 결과를 구하는 상황)&lt;/li&gt;
&lt;li&gt;프로그램적으로 Future을 완료시킴 (비동기 동작에 수동으로 결과 제공)&lt;/li&gt;
&lt;li&gt;Future의 완료 동작에 반응 (결과를 기다리면서 블록되지 않고 결과가 준비되었다는 알림을 받은 다음에 Future의 결과로 원하는 추가 동작 실행 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public Future&amp;lt;Double&amp;gt; getPriceAsync(String product){
 CompletableFuture&amp;lt;Double&amp;gt; futurePrice = new CompletableFuture&amp;lt;&amp;gt;();
 new Thread( () -&amp;gt; { 
		 try {
			 double price = calculatePrice(product); //calCulatePrice :: 처리 시간이 오래 걸리는 외부 api
			 futurePrice.complete(price);
		 } catch (Exception e) {
			 futurePrice.completeExceptionally(ex); //error 발생 시 발생 후 future 종료
		 }
}).start();
return futurePrice;
}
&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;실제 가격을 계산하는 다른 스레드를 만든 다음에 계산 결과를 기다리지 않고 결과를 포함할 Future 인스턴스를 바로 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Future&amp;lt;Double&amp;gt; futurePrice = getPriceAsync(&quot;product&quot;);
//다른 스레드가 제품 가격을 계산 하는 동안 .. 이 스레드는 다른 작업 수행
doSomethingElse();
try {
	  double price = futurePrice.get();//가격 정보가 있으면 Future에서 가격 정보를 읽고  
    //가격 정보가 없으면 가격 정보를 받을 때까지 block
    System.out.println(price);
 } catch (Exception e) {
  throw new RuntimeException(e);
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getPriceAsync 는 즉시 Future 반환, Future의 get 메서드 호출 .. &amp;rarr; &lt;b&gt;결국 결과값을 받을 때까지 block 된다..&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Future 작업이 끝났을 때만 이를 통지받으면서 람다 표현식이나 메서드 참조로 정의된 콜백 메서드를 실행하는 방법이 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 supplyAsync 로 CompleableFuture 만들 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 Executor 를 선택적으로 전달 가능&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
Returns a new CompletableFuture that is asynchronously completed
     * by a task running in the given executor with the value obtained
     * by calling the given Supplier.
**/
public Future&amp;lt;Double&amp;gt; getPriceAsync(String product){
  return CompletableFuture.supplyAsync(() -&amp;gt; calculatePrice(product));
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비블록 코드 만들기&lt;/h2&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public List&amp;lt;String&amp;gt; findPrices(String product){
	 List&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt; priceFutures =
		 shops.stream() //CompletableFuture 로 각각의 계산을 비동기적으로 계산
		 .map(shop -&amp;gt; CompletableFuture.supplyAsync(
					 () -&amp;gt; shop.getPrice(product)))
			.collect(Collectors.toList());
		
		return priceFutures.stream()
				.map(CompletableFuture::join)  //모든 비동기 동작이 끝나길 기다림
				.collect(toList());
		}
	
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 스트림 파이프라인으로 처리한 이유:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림 연산은 게으른 특성이 있으므로 하나의 파이프라인으로 연산을 처리했다면 모든 가격 정보 요청 동작이 동기적, 순차적으로 이루어지게 된다, 기존 요청 작업이 완료되어야 join이 결과를 반환하면서 다음 상점으로 정보를 요청할 수있기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 스트림 파이프라인으로 처리하면서 CompletableFuture를 리스트로 모은 다음 다른 작업과는 독립적으로 각자의 작업을 수행함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;but.. 더 확장성이 좋은 방법은 없을까&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 Executor 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀 조정 공식&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Nthrads = NCPU * UCPU * (1+W/C)

- NCPU 는 코어 수
- UCPU 는 0과 1 사이 값 갖는 CPU 사용 비율
- W/C는 대기 시간과 계산 시간의 비율
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀은 데몬 스레드 로 만듦 (t.setDaemon(true);)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 일반 스레드가 실행 중이면 프로그램은 종료되지 않음,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 어떤 이벤트를 한없이 기다리면서 종료되지 않는 일반 스레드가 있다면 결국 자원이 고갈되서 문제가 생길 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데몬 스레드는 자바 프로그램이 종료될 때 강제로 실행이 종료될 수 있음&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 작업 파이프라인 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 작업과 동기 작업 조합&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;public List&amp;lt;String&amp;gt; findPrices(String product){
	List&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt; priceFutures =
		shop.stream()
		.map(shop -&amp;gt; CompletableFuture.supplyAsync(()-&amp;gt;shop.getPrice(product), executor))
		//thenApply:: CompletableFuture 가 끝날때까지 block하지 않음, CompletableFuture
		//가 끝날 때 람다 표현식을 적용할 수 있음
		.map(future -&amp;gt; future.thenApply(Quote::parse))
		//첫번째 연산의 결과를 인수로 받고 CompletableFuture 로 반환
		.map(future -&amp;gt; future.thenCompose(quote -&amp;gt; 
						CompletableFuture.supplyAsync( () -&amp;gt; Discount.applyDiscount(quote, executor)))
		.collect(toList());
		
		
	return priceFutures.stream().map(CompletableFuture::join).collect(toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Async 로 끝나지 않는 메서드 : 이전 작업을 수행한 스레드와 같은 스레드에서 작업 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Async 로 끝나는 메서드: 다음 작업이 &lt;b&gt;다른 스레드&lt;/b&gt;에서 실행되도록 스레드 풀에 작업 제출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 위 코드는 두 번째 CompleableFuture의 결과가 첫 번째 CompletableFuture의 결과에 의존하므로 최종 실행시간에는 영향 미치지 X, 스레드 전환 오버헤드가 적게 발생하면서 효율성이 좋은 thenCompose 활용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;thenApply vs thenCompose&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;public &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; thenApply(
        Function&amp;lt;? super T,? extends U&amp;gt; fn) {
        return uniApplyStage(null, fn);
    }

public &amp;lt;U&amp;gt; CompletableFuture&amp;lt;U&amp;gt; thenCompose(
        Function&amp;lt;? super T, ? extends CompletionStage&amp;lt;U&amp;gt;&amp;gt; fn) {
        return uniComposeStage(null, fn);
    }
    
 // synchronous mapping function.
 
 CompletableFuture&amp;lt;Integer&amp;gt; future = 
    CompletableFuture.supplyAsync(() -&amp;gt; 1)
                     .thenApply(x -&amp;gt; x+1);
 
 
 // thenCompose is used if you have an asynchronous mapping function 
 // (i.e. one that returns a CompletableFuture). 
 //It will then return a future with the result directly, 
 // rather than a nested future.
 CompletableFuture&amp;lt;Integer&amp;gt; future = 
    CompletableFuture.supplyAsync(() -&amp;gt; 1)
                     .thenCompose(x -&amp;gt; CompletableFuture.supplyAsync(() -&amp;gt; x+1));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenApply: 동기 연산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenCompose: 비동기 연산&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;독립적인 두 개의 CompletableFuture 합치기&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Future&amp;lt;Double&amp;gt; futurePriceInUSD = 
//1st task 요청
	 CompletableFuture.supplyAsync(()-&amp;gt; shop.getPrice(product))
	 
	 .thenCombine(
		 CompletableFuture.supplyAsync(
		 //환율 요청하는 2nd task 요청
				 () -&amp;gt; exchangeService.getRate(Money.EUR, Money.USD))
					 //exchangeService가 1초 안에 결과 제공하지 않으면 default value 사용
				 .completeOnTimeout(DEFAULT_RATE, 1, TimeUnit.SECONDS),
				 //thenCombineAsync 일 경우 별도의 스레드에서 실행 ..
				 (price, rate) -&amp;gt; price * rate 
			));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getPrice : Executor 스레드 1에서 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`getRate:`` Executor 스레드 2에서 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 합쳐서 join &amp;hellip;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CompletableFuture의 종료에 대응하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각상점에서 가격 정보를 제공할 때마다 즉시 보여주는 방법..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 findPrices 코드는 전체 상점이 가격을 다 리턴해야 보여줌 ..&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;//각 pipeline 이 one executor thread 에서 실행 ..

public Stream&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt; findPricesStream(String product){
	return shops.stream()
		.map(shop -&amp;gt; CompletableFuture.supplyAsync( ()-&amp;gt; shop.getPrice(product), executor))
		.map(future -&amp;gt; future.thenApply(Quote::parse))
		.map(future -&amp;gt; future.thenCompose(quote -&amp;gt; 
					CompletableFuture.supplyAsync( ()-&amp;gt; Discount.applyDiscount(quote), executor)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenAccept : Returns a new CompletionStage that, when this stage completes normally, is executed with &lt;b&gt;this stage's result as the argument to the supplied action.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public CompletionStage&amp;lt;Void&amp;gt; thenAccept(Consumer&amp;lt;? super T&amp;gt; action);

CompletableFuture[] futures = findPricesStream(&quot;myphone&quot;)
																.map(f -&amp;gt; f.thenAccept(System.out::println))
																.toArray(size -&amp;gt; new CompletableFuture[size]);
																
//모든 completablefuture 가 완료되어야 CompletableFuture&amp;lt;Void&amp;gt; 반환
CompletableFuture.allOf(futures).join();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;allOf 모든 CompletableFuture가 완료되어야 CompletableFuture&amp;lt;Void&amp;gt; 가 완료됨&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Wrap-up&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 값 &amp;rarr; CompletableFuture&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일련의 sequential 한 값 &amp;rarr; Flow API&lt;/p&gt;</description>
      <category>공부/Java</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/269</guid>
      <comments>https://catscats.tistory.com/269#entry269comment</comments>
      <pubDate>Tue, 22 Apr 2025 23:16:39 +0900</pubDate>
    </item>
    <item>
      <title>Netty 기초</title>
      <link>https://catscats.tistory.com/268</link>
      <description>&lt;h1&gt;Netty - async and event-driven&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 blocking model 은 아래와 같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SEGrG/btsNuE3YVIz/cIZUHN6CK3yJs7D2Sjbty0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SEGrG/btsNuE3YVIz/cIZUHN6CK3yJs7D2Sjbty0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SEGrG/btsNuE3YVIz/cIZUHN6CK3yJs7D2Sjbty0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSEGrG%2FbtsNuE3YVIz%2FcIZUHN6CK3yJs7D2Sjbty0%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;871&quot; height=&quot;260&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 Thread 마다 socket connection 을 가지게 하고, blocking I/O 모델일 경우 다음과 같은 drawback 이 발생할 수 있다&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;많은 thread 가 I/O 이벤트 결과가 나타날 때까지 기다려야 함&lt;/li&gt;
&lt;li&gt;각 연결마다 thread 가 배정되는 형태이기 때문에, connection 이 늘어난다면 thread 도 늘어나게 될 것이고 그에 따른 context-switching 비용도 늘어날 것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Java NIO 가 도입되었음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setsockopt() : socket의 read/write call 이 즉시 리턴됨&lt;/li&gt;
&lt;li&gt;non-blocking socket 을 등록할 수 있음: event notification API (e.g. select(), poll() ) 를 이용해서 데이터가 읽기/쓰기 준비가 되었는지 확인할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PM1fh/btsNvilzUdv/f8IbUOSfJGkpRcT3OMyGL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PM1fh/btsNvilzUdv/f8IbUOSfJGkpRcT3OMyGL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PM1fh/btsNvilzUdv/f8IbUOSfJGkpRcT3OMyGL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPM1fh%2FbtsNvilzUdv%2Ff8IbUOSfJGkpRcT3OMyGL0%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;690&quot; height=&quot;296&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.nio.channels.Selector : event notification API 를 이용해 어떤 socket이 I/O 준비가 되었는지 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Figure 1.2 모델을 사용하면서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적은 thread 로부터 많은 connection을 핸들링 할 수 있음, memory 관리나 context-switching 으로부터 오버헤드가 줄어듬&lt;/li&gt;
&lt;li&gt;스레드는 핸들할 I/O 가 없을 경우 다른 task 의 작업을 할 수 있게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Netty는 Asynchrounous 하고 event-driven 이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;operation 이 끝날 때까지 블록되지 않게 함. async method 는 즉시 리턴되며 완료 시 user에게 알림한다&lt;/li&gt;
&lt;li&gt;Selector는 적은 수의 thread 로부터 이벤트에 대한 많은 연결을 할 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty의 Core Component 로는 다음과 같은 것들이 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channels:파일이나 네트워크 소켓과 같은 것들의 연결이라고 볼 수 있음, I/O operation 들을 수행함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;an open connection to an entity&lt;/b&gt; such as a hardware device, a file, a network socket, or a program component &lt;b&gt;that is capable of performing one or more distinct I/O operations, for example reading or writing&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Callbacks: Netty는 콜백을 이벤트를 핸들링할때 사용한다 &lt;b&gt;interface ChannelHandler&lt;/b&gt; 구현하면서 이벤트가 핸들링되며, 콜백이 트리거 되게 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class ConnectHandler extends ChannelInboundHandlerAdapter {
@Override
	public void channelActive(ChannelHandlerContext ctx) //새로운 connection 이 만들어 질때 트리거됨
	throws Exception {
	System.out.println(
	&quot;Client &quot; + ctx.channel().remoteAddress() + &quot; connected&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;Futures: async operation 의 결과에 대한 placeholder 로 작동, 미래의 어떤 순간에 완료될 것이며 결과에 대한 access 를 제공한다 Netty에서는 &lt;b&gt;ChannelFuture&lt;/b&gt; 제공, &lt;b&gt;ChannelFutureListner&lt;/b&gt; 사용해서 operation 이 완료되었는지 에러로 종료되었는지 여부를 알 수 있다 Nettty의 outbound I/O operation 은 ChannelFuture 를 반환하게 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Channel channel = ...;
// Does not block
ChannelFuture future = channel.connect(
new InetSocketAddress(&quot;192.168.0.1&quot;, 25));
	future.addListener(new ChannelFutureListener() {//operation 이 완료되면 알림받는 리스너 등록
			@Override
			public void operationComplete(ChannelFuture future) { //완료될 시에 호출됨
			if (future.isSuccess()){
			ByteBuf buffer = Unpooled.copiedBuffer(
			&quot;Hello&quot;,Charset.defaultCharset());
			ChannelFuture wf = future.channel()
			.writeAndFlush(buffer);
			....
			} else {
			Throwable cause = future.cause();
			cause.printStackTrace();
			}
			}
});
&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;Event &amp;amp; Handlers:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBW62/btsNwdXD9qA/wd18JqQ1T1ugmJV7KP6300/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBW62/btsNwdXD9qA/wd18JqQ1T1ugmJV7KP6300/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBW62/btsNwdXD9qA/wd18JqQ1T1ugmJV7KP6300/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBW62%2FbtsNwdXD9qA%2Fwd18JqQ1T1ugmJV7KP6300%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;747&quot; height=&quot;267&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 event 는 user가 구현한 handlers 에 의해 처리됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty의 &lt;b&gt;ChannelHandler&lt;/b&gt; 는 hanlder 에 대한 abstraction 을 제공&lt;/p&gt;
&lt;h1&gt;Netty application&lt;/h1&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;//다수 Channel 에서도 이 Handler 가 안전하게 사용될 수 있음
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println(
                &quot;Server received: &quot; + in.toString(CharsetUtil.UTF_8));
        ctx.write(in);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChannelHandler 는 다양한 유형의 이벤트에 대해 호출됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 이벤트 수명 주기에 연결 및 커스텀 로직을 작성하기 위해 ChannelHandler 를 구현할 수 있음&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class EchoSever {
    private final int port;

    public EchoSever(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception{
        if (args.length != 1){
            System.err.println(&quot;Usage: &quot;+EchoSever.class.getSimpleName() +&quot; &amp;lt;port&quot;);
        }
        int port = Integer.parseInt(args[0]);
        new EchoSever(port).start();
    }

    public void start() throws Exception{
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup(); //create the event loop group
        try{
            ServerBootstrap b = new ServerBootstrap(); //create the server bootstrap
            b.group(group)
                    .channel(NioServerSocketChannel.class) //specify the use of NIO transport Channel
                    .localAddress(new InetSocketAddress(port))  //sets the socket address using port
                    .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() { //add EchoServerHanlder to the channel's channelPipleline
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(serverHandler);
                        }
                    });
            ChannelFuture f = b.bind().sync(); //bind the server asychronously
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Channelnitializer&lt;/b&gt;: 새로운 connection 이 허용되면, 자식 Channel 이 만들어지고 Channel Initializer 가 ChannelPipleline 에 EchoServerHandler 를 추가함&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class EchoClient {
    private final String host;
    private final int port;
    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture f = b.connect().sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println(
                    &quot;Usage: &quot; + EchoClient.class.getSimpleName() +
                            &quot; &amp;lt;host&amp;gt; &amp;lt;port&amp;gt;&quot;);
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Sharable
public class EchoClientHandler extends
        SimpleChannelInboundHandler&amp;lt;ByteBuf&amp;gt; {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer(&quot;Netty rocks!&quot;,
                CharsetUtil.UTF_8));
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        System.out.println(
                &quot;Client received: &quot; + in.toString(CharsetUtil.UTF_8));
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Netty components and design&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Channel&lt;/h2&gt;
&lt;h1&gt;Transports&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selector: Channel 의 상태에 변화가 있을 때 알림받을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 상태의 변화는 다음과 같음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;New Channel 이 accept 되고 준비되었을 때&lt;/li&gt;
&lt;li&gt;Channel 연결이 완료되었을 때&lt;/li&gt;
&lt;li&gt;Channel 이 데이터가 존재하고 읽기가 준비되었을 때&lt;/li&gt;
&lt;li&gt;Channel 이 데이터 쓰기가 가능할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 패턴은 java.nio.channels.SelectionKey 에 정의된 bit pattern 임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태에 변화가 있을 때 이 패턴들을 결합해서 사용함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ky49w/btsNuCSD6xY/yBYIPidv7kPV2AAaBec0p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ky49w/btsNuCSD6xY/yBYIPidv7kPV2AAaBec0p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ky49w/btsNuCSD6xY/yBYIPidv7kPV2AAaBec0p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fky49w%2FbtsNuCSD6xY%2FyBYIPidv7kPV2AAaBec0p0%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;949&quot; height=&quot;578&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select() 대신 epoll() 사용&lt;/p&gt;</description>
      <category>공부/Java</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/268</guid>
      <comments>https://catscats.tistory.com/268#entry268comment</comments>
      <pubDate>Tue, 22 Apr 2025 23:12:52 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션과 격리성 레벨</title>
      <link>https://catscats.tistory.com/267</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&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;트랜잭션의 ACID 속성에 대해 설명할 수 있다.&lt;/li&gt;
&lt;li&gt;트랜잭션의 Commit/Abort 기능이 애플리케이션에 주는 이점을 설명할 수 있다.&lt;/li&gt;
&lt;li&gt;동시성 문제의 종류를 이해하고 이를 방지할 수 있는 격리 수준의 종류에 대해 설명할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;Transaction&lt;/h1&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;트랜잭션을 사용함으로써 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있음(all or nothing, 부분적 결과를 허용하지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도 할 수 있다는 점.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ACID?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 흔하게 들어보는 개념이지만. .DB 마다 ACID 구현이 제각각이다 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원자성(Atomicity)&lt;/b&gt;: &lt;b&gt;어보트 능력(abortability)&lt;/b&gt; 에 더 가깝다, 여러 읽기/쓰기 작업이 하나의 원자적인 트랜잭션으로 묶이게 되고, 작업이 완료(커밋)되지 못하면 어보트됨 &amp;rarr; 애플리케이션 입장에서는 트랜잭션이 어보트 되었다면 이 트랜잭션이 어떤 것도 변경하지 않았음을 알 수 있음. 애플리케이션이 부분 실패를 고려할 필요가 없게 해줌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일관성(Consistency)&lt;/b&gt;: 항상 진실이어야 하는 데이터에 관한 정의(불변식)이 있다는 것(e.g. 회계 시스템에서 모든 계좌의 대변과 차변은 항상 맞아떨어져야 함) &amp;rarr; BUT 이것은 애플리케이션의 속성에 가까움 DB는 불변식을 위반하는 데이터를 막을 수는 있지만 (e.g. 외래키 재약 조건 ..), 일관성을 지키기 위해 DB만으로 되는 것이 아님&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;격리성(Isolation)&lt;/b&gt;: 동시에 실행되는 트랜잭션은 서로 격리됨. 한 트랜잭션은 다른 트랜잭션의 내용을 전부 볼 수 있던지 아니면 아무것도 볼 수 없든지 둘 중 하나여만 한다. &lt;b&gt;직렬성&lt;/b&gt; 과 유사한 의미로, 여러 트랜잭션이 동시에 실행되더라도 트랜잭션이 순차적으로 실행된 결과와 동일함을 보장함. 추후에 다룰 격리수준에서 더 자세히 설명 예정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지속성(Durability)&lt;/b&gt;: 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생했거나 데이터베이스가 죽더라도 기록한 모든 데이터는 손실되지 않는다는 보장. 물론 완벽한 지속성은 존재하지 않는다 (하드웨어와 백업이 동시에 파괴되어 버릴 경우..)&lt;/p&gt;
&lt;h1&gt;Isolation Level&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 격리 수준은 트랜잭션 간 방지해야 하는 현상에 따라 정의됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Preventable Read Phenomena by Isolation Level&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolation Level Dirty Read Nonrepeatable Read(fuzzy read) Phantom Read&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read uncommitted&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read committed&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable read&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Not possible&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;Dirty Read: 다른 트랜잭션에 의해 변경된 커밋되지 않은 데이터를 읽음&lt;/li&gt;
&lt;li&gt;Non-Repeatable Read: 같은 쿼리를 동일 트랜잭션 내에서 두 번 실행했을 때 결과가 달라지는 현상 (다른 트랜잭션에 의해 데이터가 수정되고 커밋되었을 경우)&lt;/li&gt;
&lt;li&gt;Phantom Read: 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 현상&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Read committed&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 수준의 트랜잭션 격리로, &lt;b&gt;두 가지&lt;/b&gt;를 보장함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;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;오라클 11g 기본 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 1이 쿼리 도중 트랜잭션 2가 데이터를 변경하거나 추가해서 커밋하게 되면, 쿼리 결과가 달라지는 fuzzy read나, 없던 row가 튀어나오는 phantom read 가 생길 수 있음&lt;/p&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;Row lock(TX lock) 이용,&lt;/b&gt; INSERT,&amp;nbsp;UPDATE,&amp;nbsp;DELETE,&amp;nbsp;MERGE, or&amp;nbsp;SELECT&amp;nbsp;... FOR&amp;nbsp;UPDATE&amp;nbsp; 수행 시 해당 row 에 대한 잠금을 획득하여 트랜잭션이 커밋되거나 어보트 될 때까지 잠금을 보유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 한 트랜잭션만 잠금을 보유할 수 있고 다른 트랜잭션이 쓰기를 원한다면 잠금을 보유한 트랜잭션이 커밋되거나 어보트 된 후에 잠금을 얻을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 기준 다른 트랜잭션이 변경 중이면 그 변경 전 버전을 &lt;b&gt;Undo Log&lt;/b&gt; 를 이용해 읽음&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Repeatable read&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷 격리라고도 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 트랜잭션은 데이터베이스의 &lt;b&gt;일관된 스냅샷&lt;/b&gt; 으로부터 읽음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 봄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷 격리의 핵심 원리는 &lt;b&gt;읽는 쪽에서 쓰는 쪽을 차단하지도 않고, 쓰는 쪽에서 읽는 쪽을 차단하지도 않는다는 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷 격리는 &lt;b&gt;MVCC(multi-version concurrency control)&lt;/b&gt; 로 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mySQL InnoDB 에서는 트랜잭션 ID + undo log 를 사용해서 구현되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 row 마다 3개의 field 가 저장된다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB_TRX_ID: insert나 update 를 수행한 가장 마지막 트랜잭션 id&lt;/li&gt;
&lt;li&gt;DB_ROLL_PTR: undo log 의 레코드를 가리킴, update 전의 내용을 다시 가져오는데 필요한 정보가 포함&lt;/li&gt;
&lt;li&gt;DB_ROW_ID: 새 행이 삽입될 때 증가되는 행 id&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mo4Mj/btsNbPbHlbf/tSqoqeHqHgGxwtOm9ZGwRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mo4Mj/btsNbPbHlbf/tSqoqeHqHgGxwtOm9ZGwRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mo4Mj/btsNbPbHlbf/tSqoqeHqHgGxwtOm9ZGwRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMo4Mj%2FbtsNbPbHlbf%2FtSqoqeHqHgGxwtOm9ZGwRk%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;624&quot; height=&quot;452&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 update나 delete 를 하면 변경 전 데이터는 undo log 에 저장되고, 현재 데이터는 덮어씀. 다른 트랜잭션이 자기보다 나중에 커밋된 데이터를 보게 되면 undo log 를 참조해서 트랜잭션 시작 시점에 맞게 버전을 재구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;undo log는 더이상 이전 버전의 row를 빌드하는데 필요한 스냅샷이 할당된 트랜잭션이 없는 경우에 삭제됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅숏 격리는 각 데이터베이스에서 다른 이름을 사용, 오라클에서는 직렬성, postgreSQL 과 mySQL 에서는 반복 읽기(repeatable read) 하고 함&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;갱신 손실 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 트랜잭션이 동시에 쓰기를 실행한다면? 두 트랜잭션이 데이터베이스에서 값을 읽고 변경한 후, 변경한 값을 다시 쓴다면 (e.g. 카운터를 증가시킴), 동시에 트랜잭션 작업이 이루어질 경우 두번째 변경이 첫번째 변경을 포함하지 않으므로 변경 중 하나가 손실될 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;명시적 잠금&lt;/b&gt;: 데이터 수정 전에 SELECT .. FOR UPDATE 를 이용하여 질의에 의해 반환된 모든 로우에 대해 &lt;b&gt;Exclusive Lock&lt;/b&gt; (배타 락) 을 건다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오라클 Lock 모드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Shared lock mode&lt;/b&gt;: 여러 트랜잭션이 동시에 공유 잠금을 획득할 수 있음, 공유 잠금이 걸린 데이터에 대해 쓰기 작업은 불가능(shared lock 이 걸릴 경우 exclusive lock은 획득 불가). 보통은 일관된 읽기를 제공하는 MVCC 로 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Exclusive lock mode&lt;/b&gt;: 리소스를 배타적으로 잠그는 첫번째 트랜잭션은 배타적 잠금이 해제될 때까지 데이터를 변경할 수 있는 유일한 트랜잭션임, 다른 트랜잭션이 변경하려고 하면 blocking 됨. 단 MVCC 때문에 이전 데이터는 읽을 수 (SELECT) 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;갱신 손실 자동 감지&lt;/b&gt;: 스냅숏 격리와 결합해 Oracle 의 경우 갱신 손실이 발생하면 감지하여 문제가 되는 트랜잭션을 어보트 시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5J06X/btsNacGlGrm/kOf4vzA3hkfqxPYXCTo5r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5J06X/btsNacGlGrm/kOf4vzA3hkfqxPYXCTo5r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5J06X/btsNacGlGrm/kOf4vzA3hkfqxPYXCTo5r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5J06X%2FbtsNacGlGrm%2FkOf4vzA3hkfqxPYXCTo5r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;470&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼: Session1, 오: Session2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session2의 트랜잭션 시작 이후 Session1이 같은 데이터에 대해 수정해서 커밋했기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session2가 같은 데이터에 대해 UPDATE 시 &lt;b&gt;ORA-08177: can't serialize access for this transaction&lt;/b&gt; 에러가 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 특별한 데이터베이스 기능을 사용하지 않아도 갱신 손실이 자동 감지되기 때문에 오류가 덜 발생하게 해줌&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle Database permits a serializable transaction to modify a row only if changes to the row made by other transactions were&amp;nbsp;already&amp;nbsp;committed when the serializable transaction began. &lt;b&gt;The database generates an error when a serializable transaction tries to update or delete data changed by a different transaction that committed&amp;nbsp;after&amp;nbsp;the serializable transaction began&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ORA-08177: Cannot serialize access for this transaction
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 스큐와 팬텀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 스큐: 두 트랜잭션이 동시에 조건을 확인하고 충돌 없이 각자 업데이트했지만, 전체적으로 무결성이 깨지는 상황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팬텀:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 새 행 삽입이 아니라 아래 조건 역시 팬텀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화 유형 예&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INSERT&lt;/td&gt;
&lt;td&gt;새로 조건을 만족하는 행 삽입됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE&lt;/td&gt;
&lt;td&gt;기존 행이 조건을 만족하도록 바뀜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;조건 범위의 행이 사라져서 COUNT 등 조건이 달라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시와 같이 동시에 같은 조건의 행을 읽고, 각각 조건을 만족한다고 판단 후 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 트랜잭션 도중 다른 트랜잭션에 의해 조건의 결과가 깨지게 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXsOTn/btsNbepvTGf/5r70vIrHK15SrbPkEtmIzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXsOTn/btsNbepvTGf/5r70vIrHK15SrbPkEtmIzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXsOTn/btsNbepvTGf/5r70vIrHK15SrbPkEtmIzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXsOTn%2FbtsNbepvTGf%2F5r70vIrHK15SrbPkEtmIzK%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;640&quot; height=&quot;474&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 스큐는 두 트랜잭션이 같은 객체를 읽어서 그 중 일부를 갱신할 때 나타날 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 스큐 방지 방법?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Next Key Lock&lt;/b&gt;: MySQL InnoDB 엔진에서 REPEATBLE READ 격리 수준에 사용되는 방식으로 인덱스의 레코드 (record lock) + 레코드의 바로 인접한 사이의 간격(gap lock) 을 잠궈 팬텀을 방지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;색인 범위 잠금이란? 색인(인덱스)을 기준으로 특정 범위에 대해 공유 잠금 또는 배타 잠금을 거는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1743940910710&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//users 테이블에 age 로 index 가 설정되었다고 가정
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
// InnoDB는 age 인덱스를 따라 20 ~ 30 사이 각 row + 그 사이의 gap에 대해
// next key lock을 걸게됨
// 다른 트랜잭션은 해당 범위 안 데이터에 대해 INSERT, UPDATE, DELETE 를 할 수 없음&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;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Serializable&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬성 트랜잭션을 구현하는 3가지 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 순서대로 실행하기: 각 트랜잭션 실행 시간이 짧고 트랜잭션 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 처리량 낮아야 함&lt;/li&gt;
&lt;li&gt;2단계 잠금(Two-Phased Locking): 직렬성을 구현하는 가장 표준적인 방법. 비관적(pessimistic) 동시성 제어로 다음과 같이 동작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 쓰기를 원한다면 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다&lt;/li&gt;
&lt;li&gt;트랜잭션 A가 객체를 쓰고 트랜잭션 B가 객체를 읽기 원한다면 마찬가지로 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다&lt;/li&gt;
&lt;/ul&gt;
읽는 쪽, 쓰는 쪽을 둘다 막는 방식은 각 객체에 잠금을 이용해 구현동시성이 줄어드는 단점, 객체에 쓴다면 배타 락, 객체를 읽는다면 공유 락을 사용 (동시에 여러 트랜잭션이 공유 락을 획득하는 것은 허용)&lt;/li&gt;
&lt;li&gt;직렬성 스냅숏 격리(SSI): &lt;b&gt;낙관적 동시성 제어 과거의 읽기에 영향을 미치는 쓰기 감지 &amp;amp; 오래된 MVCC 버전을 읽었는지 감지&lt;/b&gt;, 트랜잭션이 커밋을 원할 때 실행이 직렬적이지 않으면 어보트 시킴기본적으로 스냅샷 격리를 실행하지만 트랜잭션 간의 읽기-쓰기 충돌을 모니터링하여(트랜잭션 간 의존성 그래프를 이용) 트랜잭션 그래프에서 동시에 실행되는 트랜잭션의 anomaly 를 식별하고 anomaly가 발생하지 않도록 트랜잭션을 롤백&lt;br /&gt;PostgreSQL 의 &lt;b&gt;Serializable&lt;/b&gt; 에서 구현됨&lt;br /&gt;읽기는 쓰기를 차단하지 않고, 쓰기도 읽기를 차단하지 않음 &amp;rarr; 2PL 보다 성능 우수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;//session 1
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 1;

INSERT INTO mytab VALUES (2, 30);

//session 2
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 2;
//session 1에서 읽은 데이터를 session 2에서 변경
INSERT INTO mytab VALUES (1, 300);
//session2 에서 정상적으로 커밋
COMMIT;

//session 1
//session 1에서 커밋 시, ERROR 발생
//트랜잭션을 커밋하려고 할 때 데이터베이스는 무시된 쓰기 중 커밋된게 있는지 확인 후
//커밋된 것이 있으면 어보트 시킴
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;ERROR:  could not serialize access
        due to read/write dependencies
        among transactions
DETAIL:  Cancelled on identification
         as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.
&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;p data-ke-size=&quot;size16&quot;&gt;출처:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E28271_01/server.1111/e25789/consist.htm#BABEIHGJ&quot;&gt;https://docs.oracle.com/cd/E28271_01/server.1111/e25789/consist.htm#BABEIHGJ&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/&quot;&gt;https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-next-key-locking.html#:~:text=To&quot;&gt;https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-next-key-locking.html#:~:text=To&lt;/a&gt; prevent phantoms%2C InnoDB uses,the index records it encounters.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wiki.postgresql.org/wiki/SSI&quot;&gt;https://wiki.postgresql.org/wiki/SSI&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wiki.postgresql.org/wiki/Serializable#Cahill2009&quot;&gt;https://wiki.postgresql.org/wiki/Serializable#Cahill2009&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 중심 애플리케이션 설계&amp;nbsp; 7장&lt;/p&gt;</description>
      <category>공부/Database</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/267</guid>
      <comments>https://catscats.tistory.com/267#entry267comment</comments>
      <pubDate>Sun, 6 Apr 2025 20:54:25 +0900</pubDate>
    </item>
    <item>
      <title>[System Design Interview] Chapter.06 키-값 저장소 설계</title>
      <link>https://catscats.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;분산 키-값 저장소&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템 설계 시 고려해야 할 점들&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CAP 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 일관성 (&lt;b&gt;Consistency&lt;/b&gt;) : 분산 시스템에 접속하는 모든 클라이언트는 어떤 노드에 접속했느냐에 상관없이 언제나 같은 데이터를 봐야 함&lt;/li&gt;
&lt;li&gt;가용성 (&lt;b&gt;Availability&lt;/b&gt;) : 분산 시스템에 접속하는 클라이언트는 일부 노드에 장애가 발생하더라도 항상 응답을 받을 수 있어야 함&lt;/li&gt;
&lt;li&gt;파티션 감내 (&lt;b&gt;Partition tolerance&lt;/b&gt;): 파티션은 두 노드 사이에 장애가 발생하였음을 의미, 파티션 감내는 네트워크에 파티션이 생기더라도 시스템은 계속 동작함을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3가지 요소를 동시에 만족시키는 분산 시스템을 설계하는 것은 불가능하다. 네트워크 장애는 피할 수 없는 것으로 여겨지므로, 파티션 감내는 반드시 감내할 수 있도록 설계되어야 함. 보통 &lt;b&gt;CP&lt;/b&gt; 시스템 (일관성 + 파티션 감내), &lt;b&gt;AP&lt;/b&gt; 시스템 (가용성 + 파티션 감내) 중에 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 n1, n2, n3 노드가 있고 n3 이 네트워크 장애로 통신이 단절된다면&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;CP&lt;/b&gt; 시스템: 일관성을 선택하였으므로, 데이터 불일치 문제를 피하기 위해 n1, n2에 쓰기 연산을 중단시켜야 함 (가용성 깨짐), 네트워크 장애가 해결될 때까지 응답 오류를 반환해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AP&lt;/b&gt; 시스템: 가용성을 선택하였으므로, n1,n2 에 쓰기 연산을 허용, 낡은 데이터를 반한할 위험이 있더라도 읽기 연산 허용(일관성 깨짐)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 파티션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(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;노드가 추가되거나 삭제될 때 데이터의 이동을 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DynamoDB에서는 안정 해싱(&lt;b&gt;consistent hashing&lt;/b&gt;) 을 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 컨셉은 아래와 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시 공간이 ring 과 같은 공간에 할당, 각 노드는 hash function을 거쳐 ring의 특정 position을 할당. key 도 마찬가지로 hash function을 거쳐 ring의 시계방향에 가장 가까운 쪽의 Node 로 할당됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 다음과 같은 문제가 발생할 수 있음.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드가 링의 공간(해시 공간)에 랜덤하게 배치될 수 있으며, 이는 불균형을 초래할 수 있음&lt;/li&gt;
&lt;li&gt;노드 성능을 반영하지 못한다(&lt;b&gt;oblivious to the heterogeneity in the performance of nodes.&lt;/b&gt;) 저성능 노드와 고성능 노드가 같은 양의 데이터를 핸들링한다면, 저성능 노드가 병목이 발생할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 가상 노드(&lt;b&gt;virtual node&lt;/b&gt;) 개념이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드는 한 개 이상의 가상 노드를 가지게 됨.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물리적인 노드(서버)에 여러 개의 가상 노드를 매핑하여, 노드의 성능에 따라 더 많은 가상 노드를 배정: 예를 들어, 성능이 좋은 노드는 해시 링에서 더 많은 가상 노드를 가지게 되므로 더 많은 데이터를 저장하고 처리&lt;/li&gt;
&lt;li&gt;노드 간 부하가 균등하게 분배될 수 있음&lt;/li&gt;
&lt;li&gt;규모 확장 및 자동화가 가능하다 - 시스템 부하에 따라 서버가 자동으로 추가되거나 삭제되게 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 다중화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb9jeC/btsMykL5HYI/WWZSKN8lFkgJvATbxXXUBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb9jeC/btsMykL5HYI/WWZSKN8lFkgJvATbxXXUBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb9jeC/btsMykL5HYI/WWZSKN8lFkgJvATbxXXUBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb9jeC%2FbtsMykL5HYI%2FWWZSKN8lFkgJvATbxXXUBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;334&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 N개 서버에 비동기적으로 다중화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamo의 경우, 특정 key가 1개의 코디네이터에 배정되서 코디네이터가 데이터 복제를 책임지는 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 Key K의 경우, 코디네이터의 시계방향에 있는 N-1 개의 노드에 복사되게 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 키는 preference list 를 가지는데, 이 키를 저장할 노드의 목록을 의미한다. 단 이 목록에 virtual node 가 포함되어 실제 N개보다 적은 물리 서버에 저장될 수 있다 &amp;rarr; 이 목록에 오직 Physical node 만 포함하도록 설계할 수 있음&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 일관성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 노드에 분산된 데이터는 적절히 동기화가 되어야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정족수 합의(Quorom Consensus)&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;N = 사본 갯수&lt;/li&gt;
&lt;li&gt;W = 쓰기 연산에 대한 정족수, 쓰기 연산이 성공한 것으로 간주되려면 W개의 서버로부터 쓰기 연산이 성공했다는 응답을 받아야 함&lt;/li&gt;
&lt;li&gt;R = 읽기 연산에 대한 정족 수, 읽기 연산이 성공한 것으로 간주되려면 R개의 서버로부터 응답을 받아야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 W개의 서버에 데이터가 동기화된다는 것이 아니라, 코디네이터(중재자)가 최소 W개의 서버로부터 성공 응답을 받는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W, R 값이 커질 수록 시스템의 일관성의 수준은 향상되지만, 중재자는 항상 제일 느린 서버로부터 응답을 받아야 하므로 latency는 감소할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 W+ R &amp;gt; N의 경우 강한 일관성이 보장됨. 일관성을 보증할 데이터가 최소 1개는 겹치기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W + R &amp;le; N 은 강한 일관성이 보장되지 않으나, latency는 증가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일관성 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일관성 모델은 데이터 일관성 모델의 수준을 결정한다.&lt;/p&gt;
&lt;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;eventual consistency&lt;/b&gt;): 약한 일관성의 형태로, 갱신 결과가 결국에는 모든 노드에 동기화 되는 모델임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 일관성의 경우, 쓰기 연산이 병렬적으로 발생하면 시스템에 저장된 값의 일관성이 깨질 수 있음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일관성 불일치 해소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamo의 경우, &lt;b&gt;데이터 비저닝&lt;/b&gt; 을 통해 이 문제를 해소함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버저닝은 데이터를 변경할 때마다 새로운 버전을 만들고, 각 버전은 변경 불가능(Immutable) 함.&lt;/li&gt;
&lt;li&gt;동시성 문제 발생 시, 특정 객체(데이터)에 각각 다른 버전이 생길 수 있으며, 충돌이 발생했으니 이건 나중에 reconcile 해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌 감지 및 reconcile 을 위해 &lt;b&gt;벡터 시계(vector clocks)&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;(node, counter) 의 쌍으로 이루어짐&lt;/li&gt;
&lt;li&gt;각 쌍은 모든 object의 version 마다 존재&lt;/li&gt;
&lt;li&gt;각 쌍이 서로 충돌되는 데이터인지 (parallel branch), 선후 관계를 가지는 데이터인지 결정할 수 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 쌍의 counter 가 두 번째 쌍의 모든 노드의 counter 보다 작으면 첫 번째 쌍은 두 번째 쌍의 이전 버전이다&lt;/li&gt;
&lt;li&gt;그렇지 않으면 두 쌍은 충돌되었고, reconcile 이 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QvQQ6/btsMx6f5l3W/CCv01nXVp4zLTXbWUNlE8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QvQQ6/btsMx6f5l3W/CCv01nXVp4zLTXbWUNlE8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QvQQ6/btsMx6f5l3W/CCv01nXVp4zLTXbWUNlE8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQvQQ6%2FbtsMx6f5l3W%2FCCv01nXVp4zLTXbWUNlE8k%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;853&quot; height=&quot;600&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 D를 서버 Si에 기록하고자 한다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[Si, vi] 가 있으면 vi 를 증가&lt;/li&gt;
&lt;li&gt;그렇지 않으면 새 항목[Si, 1] 를 만듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽을 때 충돌이 발생할 경우 충돌된 모든 version을 반환한다 &amp;rarr; 클라이언트가 직접 해결하도록 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌된 버전이 있을 경우 클라이언트는 &lt;b&gt;이 버전들을 보고 reconcile 을 수행하고 충돌된 버전들을 단일 버전으로 병합&lt;/b&gt;하게 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 시계 방식의 경우 2가지 단점이 있다&lt;/p&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;LWW (Last write wins): 타임 스탬프를 비교해서 가장 최신으로 병합&lt;/li&gt;
&lt;li&gt;application-level logic merged: 애플리케이션 로직 따라 병합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;벡터 시계의 벡터 사이즈가 빨리 늘어날 수 있다 - vector clock의 size limit 을 설정하여 해결 가능. (threshold 설정)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 감지&lt;/h3&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 노드는 멤버십 목록을 유지&lt;/li&gt;
&lt;li&gt;노노드는 주기적으로 박동 카운터 증가시키고, 무작위로 선정된 노드들에게 자기 박동 카운터 목록을 보냄&lt;/li&gt;
&lt;li&gt;박동 카운터 목록을 받은 노드는 멤버십 값을 최신 값으로 갱신&lt;/li&gt;
&lt;li&gt;박동 카운터 값이 일정 시간동안 갱신되지 않으면 해당 멤버는 장애 상태인 것으로 간주&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가십 프로토콜을 이용해 다음 과정을 거치게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자신이 알고 있는 정보를 상대 노드에게 전파&lt;/li&gt;
&lt;li&gt;상대 노드도 받은 정보를 기반으로 다른 노드들에게 전파&lt;/li&gt;
&lt;li&gt;이 과정을 반복하면서 전체 네트워크에 정보가 전파됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가십 프로토콜을 이용해 최종적 일관성을 달성할 수 있으며, 랜덤하게 다른 노드들과 정보 교환된 것이 전체 네트워크로 확산된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일시적 장애 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느슨한 정족수(&lt;b&gt;sloppy quorom&lt;/b&gt;) 를 이용해, 해시 링에서 가장 가까운 N개를 쿼럼으로 삼기보다 &lt;b&gt;건강한 N개의 서버를 쿼럼&lt;/b&gt;으로 지정한다. (장애 서버는 무시한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임시 위탁 기법(hinded handoff)&lt;/b&gt;: 장애 상태인 서버로 가는 요청을 다른 서버가 처리하고, 그동안 발생한 변경사항은 주기적으로 스캔되는 local database 에 남겨두고, 장애 서버가 복구되었을 때 이 hint를 그 서버에 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 네트워크 장애가 있어도 읽기 / 쓰기 연산이 가능하도록 한다 (=high availibility)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영구적 장애 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;머클 트리(Merkle tree)&lt;/b&gt; : 사본 간의 일관성이 망가진 상태를 탐지하고 전송 데이터 양을 줄이기 위해 사용된다. 리프 노드일 경우, 데이터 블록의 해시 값이 저장되며 리프 노드가 아닐 경우 각 자식 노드 레이블로부터 계산된 해시 값이 저장된다. (아래 그림 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBVFeu/btsMALH1VJu/aubSS17h2SAdM0ek3U9k71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBVFeu/btsMALH1VJu/aubSS17h2SAdM0ek3U9k71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBVFeu/btsMALH1VJu/aubSS17h2SAdM0ek3U9k71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBVFeu%2FbtsMALH1VJu%2FaubSS17h2SAdM0ek3U9k71%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;751&quot; height=&quot;465&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 머클 트리의 비교는 루트 노드의 해시 값을 비교하는 것으로 시작, 점차 아래쪽으로 탐색해 가면서 해시 값이 다른 노드를 찾아서 다른 데이터를 갖는 버킷을 찾아서 그 버킷들만 동기화함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷 트리의 이점?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레플리카 간 일관성이 깨졌을 때, 동기화해야 하는 데이터의 양을 줄일 수 있다 (=해시 값이 다른 데이터들만 동기화한다)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;읽기, 쓰기 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 키-값 저장소에서 쓰기를 처리하는 방식은 DDIA 03장 - 저장소와 설계에 더 자세하게 나와있다 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 정리하면 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인메모리 memtable 에 쓰기를 진행 (이 memtable 은 주로 red-black tree 자료구조이며, red-black tree는 임의의 순서로 write 해도 읽을 때 정렬된 순서로 읽힌다) , 추후 크래시를 대비해 커밋 로그에 쓰기 요청이 기록된다&lt;/li&gt;
&lt;li&gt;memtable (=메모리 캐시) 이 가득차면 데이터는 디스크에 있는 SSTable 에 기록된다
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SSTable 은 키-값 쌍을 정렬된 리스트 형태로 관리하는 테이블&lt;/li&gt;
&lt;li&gt;memtable 은 정렬된 형태이기 때문에 쓰기를 효율적으로 수행할 수 있다. 이 파일은 최신 세그먼트 파일이 되고, 쓰기는 계속 새로운 멤테이블 인스턴스에 기록함&lt;/li&gt;
&lt;li&gt;백그라운드 스레드에서 SSTable의 병합 및 컴팩션 과정을 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;읽기 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 요청 시 먼저 멤테이블에서 키를 찾고 디스크 상의 가장 최신 세그먼트 &amp;rarr; 그 다음 최신 세그먼트 .. 이렇게 찾게 됨&lt;/li&gt;
&lt;li&gt;다만 없는 key를 찾을 경우엔 모든 세그먼트(=모든 SSTable) 을 뒤져야 하므로, 이럴 땐 성능이 떨어질 수 있으므로 &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;블룸 필터: 블룸 필터(Bloom Filter)는 &lt;b&gt;공간 효율적인 확률적 데이터 구조&lt;/b&gt;로, 특정 요소가 집합(Set)에 존재하는지 여부를 빠르게 판별, 해당 키가 없을 가능성이 높은 SSTable을 건너뛰도록 도와줌 (SSTable에 키가 저장되어 있는지 &lt;b&gt;사전에 확인하는 캐시 역할)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;scylladb의 경우 멤테이블에 쿼리한 키가 있는지 블룸 필터로 체크하고, 있으면 반환한다. 키가 없는 경우 Multi-level 로 세그먼트를 찾는다 ..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnRvNt/btsMAY8iFPd/3KSxheoYvUkkhKxtok9nYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnRvNt/btsMAY8iFPd/3KSxheoYvUkkhKxtok9nYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnRvNt/btsMAY8iFPd/3KSxheoYvUkkhKxtok9nYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnRvNt%2FbtsMAY8iFPd%2F3KSxheoYvUkkhKxtok9nYK%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;671&quot; height=&quot;406&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDePQq/btsMzOrQqEQ/soyldD7PGOnOWlTvRcdwoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDePQq/btsMzOrQqEQ/soyldD7PGOnOWlTvRcdwoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDePQq/btsMzOrQqEQ/soyldD7PGOnOWlTvRcdwoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDePQq%2FbtsMzOrQqEQ%2FsoyldD7PGOnOWlTvRcdwoK%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;711&quot; height=&quot;406&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라언트는 키-값 저장소가 제공하는 API (get, put) 와 통신&lt;/li&gt;
&lt;li&gt;중계자는 클라이언트에게 키-값 저장소에 대한 proxy 역할을 하는 노드 &amp;rarr; 중계자에게 읽기, 쓰기 요청 보내면 적절한 노드에 매핑시켜 줌&lt;/li&gt;
&lt;li&gt;노드는 안정 해시의 링 위에 분포한다&lt;/li&gt;
&lt;li&gt;노드는 자동으로 추가, 삭제 될 수 있도록 완전히 분산&lt;/li&gt;
&lt;li&gt;데이터는 여러 노드에 다중화된다 (replication)&lt;/li&gt;
&lt;li&gt;모든 노드는 같은 책임을 지므로 single point of failure 는 발생하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 6장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&lt;/a&gt;&lt;/p&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/266</guid>
      <comments>https://catscats.tistory.com/266#entry266comment</comments>
      <pubDate>Sat, 1 Mar 2025 21:57:06 +0900</pubDate>
    </item>
    <item>
      <title>[System Design Interview] Chapter05. 안정 해시 설계</title>
      <link>https://catscats.tistory.com/265</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장에서는 어떻게 &lt;b&gt;데이터를 고르게 분산할 수 있는지&lt;/b&gt;에 대해 알아본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로, 데이터를 N 개의 서버에 나누어서 저장하게 될 때 해시 함수를 이용해서 데이터를 분배해서 저장하는 방법을 생각하게 된다. (해시 테이블과 유사)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이 방법은 다음과 같은 취약점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 서버에 장애가 발생할 경우, &lt;b&gt;장애가 발생한 서버에 저장된 데이터가 모두 재배치&lt;/b&gt; 되어야 한다&lt;/li&gt;
&lt;li&gt;캐시 서버일 경우, 장애가 발생한 서버에 저장된 키였을 경우 그 키에 대한 데이터가 없는 다른 서버에 접속하게 되어 대규모 cache miss 가 날 수 있다&lt;/li&gt;
&lt;li&gt;Hotspot key 문제가 발생할 수 있다 &amp;rarr; 데이터를 균등하게 배치하지 못해 특정한 샤드에 대한 접근이 많을 경우이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시(&lt;b&gt;consistent hash&lt;/b&gt;) 는 해시 테이블 크기가 조정될 때, 평균적으로 k/n 개의 키만 재배치하는 해시 기술이다. (k=키의 갯수, n=슬롯(서버)의 개수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시 기술의 특징은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;샤드에 걸쳐 키를 균등하게 배포&lt;/li&gt;
&lt;li&gt;서버 추가 및 삭제 시 재배치되는 키의 갯수를 최소화함, 각 서버에 균등하게 키가 배포됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시는 다음과 같이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 공간의 범위(SHA-1 기준, 보통 0 부터 2^160 - 1 까지) 가 있고, 이 범위의 공간을 갖는 링이 존재한다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버와 키의 해시 값을 해시 함수를 이용해 구한 뒤, 링의 특정 지점에 배치한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 키가 저장되는 서버는 해당 키의 위치로부터 시계 방향으로 링을 탐색해 가면서 만나는 첫번째 서버이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uaxgN/btsMy3iHhfA/iZ9MGPSokSVYJ9J6ftFcKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uaxgN/btsMy3iHhfA/iZ9MGPSokSVYJ9J6ftFcKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uaxgN/btsMy3iHhfA/iZ9MGPSokSVYJ9J6ftFcKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuaxgN%2FbtsMy3iHhfA%2FiZ9MGPSokSVYJ9J6ftFcKk%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;342&quot; height=&quot;239&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A, B, C 가 서버이고 1,2,3,4 가 키 일때, 1은 A, 2는 B, 3과 4는 D 로 배치되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 기본 절차는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버와 키를 균등 분포 해시 함수를 이용해 해시 링에 배치한다&lt;/li&gt;
&lt;li&gt;키의 위치를 링의 시계 방향으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 다음과 같은 drawback 이 발생할 수 있다&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;따라서 이 문제를 해결하기 위해 &lt;b&gt;가상 노드&lt;/b&gt; 가 존재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 노드는 실제 노드 또는 서버를 가리키는 노드로서, 하나의 서버는 링 위에 여러 가상 노드를 가질 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 노드의 갯수를 늘리면 키의 분포도 균등해 지게 된다 (아래의 표와 같이)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OnG8D/btsMA7D4vON/KDOqJGeleaPcZ3TK5SknwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OnG8D/btsMA7D4vON/KDOqJGeleaPcZ3TK5SknwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OnG8D/btsMA7D4vON/KDOqJGeleaPcZ3TK5SknwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOnG8D%2FbtsMA7D4vON%2FKDOqJGeleaPcZ3TK5SknwK%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;501&quot; height=&quot;364&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 노드는 데이터를 균등하게 분배하여 부하 균형을 유지하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 면접 사례로 배우는 대규모 설계 기초 5장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tom-e-white.com/2007/11/consistent-hashing.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tom-e-white.com/2007/11/consistent-hashing.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>공부/System Architecture</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/265</guid>
      <comments>https://catscats.tistory.com/265#entry265comment</comments>
      <pubDate>Sat, 1 Mar 2025 21:53:25 +0900</pubDate>
    </item>
    <item>
      <title>[Desinging Data-Intensive Applications] 02장. 데이터 모델과 질의 언어</title>
      <link>https://catscats.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에 적합한 데이터 모델을 선택하는 방법 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 그 모델에 따른 질의 언어에 대해 알아보자 ..&lt;/p&gt;
&lt;h1&gt;관계형 모델과 문서 모델&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관계형 모델과 문서 모델은 어떻게 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 모델은 보통 우리가 아닌 RDB로, &lt;b&gt;관계&lt;/b&gt; 로 구성되고 각 관계는 &lt;b&gt;튜플&lt;/b&gt; 의 모음임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BUT RDB에는 다음과 같은 drawback 이 있음&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;대규모 데이터셋 or 높은 쓰기량 달성을 해야 할 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 RDB 에서, 데이터를 테이블에 저장하려면 app 코드 &amp;harr; DB 모델 객체 간 전환 계층이 필요 (&lt;b&gt;임피던스 불일치&lt;/b&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;linkedin 예제를 보면, 같은 table 데이터를 JSON 형식으로 저장할 수 있음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-16 오전 11.22.41.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y4g4E/btsMj9QELGk/Fm7ZJfjrf0plelhArG56N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y4g4E/btsMj9QELGk/Fm7ZJfjrf0plelhArG56N0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y4g4E/btsMj9QELGk/Fm7ZJfjrf0plelhArG56N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY4g4E%2FbtsMj9QELGk%2FFm7ZJfjrf0plelhArG56N0%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;680&quot; height=&quot;619&quot; data-filename=&quot;스크린샷 2025-02-16 오전 11.22.41.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-16 오전 11.22.49.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beQzva/btsMkvF0cta/sM4Jk1LCPrT6lCIUOKo9kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beQzva/btsMkvF0cta/sM4Jk1LCPrT6lCIUOKo9kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beQzva/btsMkvF0cta/sM4Jk1LCPrT6lCIUOKo9kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeQzva%2FbtsMkvF0cta%2FsM4Jk1LCPrT6lCIUOKo9kk%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;650&quot; height=&quot;484&quot; data-filename=&quot;스크린샷 2025-02-16 오전 11.22.49.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SON 형식으로 데이터를 저장 (= 문서 형식으로 데이터를 저장) 하면 ,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지역성&lt;/b&gt; 을 가짐. 질의하려는 데이터가 한 곳에 있어 질의 하나로 충분함. RDB의 경우 다중 join 필요&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 Linkedin 사례처럼 중복된 데이터를 정규화해 id로 표현할 수 있지만, 문서 데이터베이스에는 조인에 대한 지원이 약함. app 코드에서 조인을 수행해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서형 데이터베이스에서 &lt;b&gt;다대다&lt;/b&gt; 관계를 표현하기 위해서는 1) 데이터를 중복하거나 (비정규화 하거나) 2) app에서 조인을 흉내내어 수동으로 해결해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서형 모델을 더 선호되는 케이스는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스키마 유연성&lt;/li&gt;
&lt;li&gt;지역성에 기인한 더 나은 성능 (1번의 질의로 app에서 사용하는 데이터 구조와 유사한 데이터를 얻을 수 있음)&lt;/li&gt;
&lt;li&gt;어떤 시점에 발생한 이벤트를 기록하는 경우 (다대다 관계가 필요하지 않음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BUT 문서형 모델을 선택하려면, 다대다 관계에서는 이점이 없을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비정규화로 중복 줄이기를 할 수 있지만, app 코드에서 데이터 일관성을 위해 추가 작업을 해야 하고 DB단에서 수행되는 조인보다 성능이 안나옴&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;RDB도 무조건 답이 아닌 것이 문서와 비슷한 구조를 여러 테이블로 나누어 &lt;b&gt;shredding&lt;/b&gt; 하는 기법은 다루기 힘든 스키마와 불필요한 app 코드를 발생시킬 수 있다 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때그때 적절한 데이터 모델을 선택할 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB는 &lt;b&gt;쓰기 스키마&lt;/b&gt; (스키마는 명시적이고 쓰여진 모든 데이터가 스키마를 따르고 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Document DB는 &lt;b&gt;읽기 스키마&lt;/b&gt; (구조는 암묵적이고 읽을 때만 해석)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 스키마 접근 방식은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션 안의 어떤 항목이 모두 동일한 구조가 아닐 때 (다른 유형으로 구성돼 있을 때) 유용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 유형의 오브젝트가 있고 각 오브젝트 별로 테이블에 넣는 방법은 실용적이지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 제어할 수 없고 언제나 변경 가능한 외부 시스템에 의해 데이터 구조가 결정되는 경우 유용함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질의를 위해, 문서형 DB를 &lt;b&gt;저장소 지역성&lt;/b&gt; 을 사용하면 이점이 있음 (질의하려는 데이터가 인접한 곳에 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB의 경우 색인이 있다고 하더라도 디스크 탐색을 해야 하기 때문에 더 많은 시간이 소요됨&lt;/p&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;/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;클러스터 테이블이란?: &lt;b&gt;특정 key 값을 공유하는 다른 table의 row가 같은 data block에 저장되는 것 (지역성)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 join 되어 query되는 경우가 많은 table 인 경우 Disk I/O 를 줄일 수 있음, 클러스터 key 가 각 row 에 따로 저장되는 것이 아니기 때문에 적은 storage 를 쓸 수 있음&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;BUT 테이블이 자주 업데이트 되거나(데이터 블록을 재배치해야 하는 문제가 있음), full table scan을 하는 경우 유용하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류에는 인덱스 클러스터 (클러스터 키 기반으로 인덱스 생성), 해쉬 클러스터 (인덱스 키가 해쉬 함수, hask key value가 row가 저장된 disk 주소를 가리킴, 인덱스 클러스터와 비교해서 disk I/O 한번이 줄어듬)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빅 테이블 데이터 모델 (카산드라, HBase ..): 칼럼 패밀리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;An HBase table contains&amp;nbsp;&lt;b&gt;column families, which are the logical and physical grouping of columns&lt;/b&gt;. There are&amp;nbsp;column qualifiers&amp;nbsp;inside of a column family, which are the columns.&amp;nbsp;(&lt;a href=&quot;https://www.ibm.com/docs/en/db2-big-sql/7.1?topic=performance-hbase-basics&quot;&gt;https://www.ibm.com/docs/en/db2-big-sql/7.1?topic=performance-hbase-basics&lt;/a&gt;)&lt;/p&gt;
&lt;h1&gt;다양한 질의 언어&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령형 질의 언어 (IMS, 코다실) - 어떻게 질의 해야 하는지 표현, 명령어를 특정 순서대로 수행하게 질의하기 때문에 다중 코어에 적절하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선언형 질의 언어&lt;/b&gt; (sql) - 데이터베이스 상세 구현이 숨겨져 있어 질의 변경하지 않고도 구현을 변경하여 최적화 가능, 병렬 실행 가능&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;맵 리듀스 함수 (Map-reduce)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 데이터를 처리하기 위한 프로그래밍 모델,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 전용 질의를 수행할 때 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map(&amp;rArr; collect와 유사), reduce 함수를 기반&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map, reduce 함수는 순수 함수여야 함. 입력으로 전달된 데이터만 사용하고 추가적으로 DB 질의를 수행할 수 없어야 하고, side-effect 가 없어야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제약사항이 있어야 장애가 발생해도 재실행 할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고 DB는 집계 pipeline 함수가 있음&lt;/p&gt;
&lt;h1&gt;그래프 기반 데이터 모델&lt;/h1&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;RDB에서 &lt;b&gt;다대다 관계가 매우 일반적이&lt;/b&gt;라면 ? &amp;rarr; 그래프 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 모델은 &lt;b&gt;정점&lt;/b&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;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;속성 그래프 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;식별자&lt;/li&gt;
&lt;li&gt;속성 컬렉션 (키-값 쌍)&lt;/li&gt;
&lt;li&gt;유입 간선 set&lt;/li&gt;
&lt;li&gt;유출 간선 set&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;식별자&lt;/li&gt;
&lt;li&gt;간선 시작 정점&lt;/li&gt;
&lt;li&gt;간선 끝 정점&lt;/li&gt;
&lt;li&gt;정점 간 관계 유형 레이블&lt;/li&gt;
&lt;li&gt;속성 컬렉션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 유형의 관계에 대해 서로 다른 레이블을 사용하면 &lt;b&gt;단일 그래프에 다른 유형의 정보를 저장하면서도 데이터 모델 유지 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 구조를 변경하더라도 단일 그래프에 손쉽게 통합할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이퍼 질의&lt;/b&gt; 를 이용할 수 있음&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;MATCH (:Person {name: 'Keanu Reeves'})-[:ACTED_IN]-&amp;gt;(:Movie)&amp;lt;-[:ACTED_IN]-(coActor:Person),
  (coActor)-[:ACTED_IN]-&amp;gt;(:Movie)&amp;lt;-[:ACTED_IN]-(:Person {name:'Tom Hanks'})
RETURN DISTINCT coActor.name AS coActor
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keanu Reeves&amp;nbsp;and&amp;nbsp;Tom Hanks 와 함께 연기한 배우를 retrieve 하는 query&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 data 간 연결이 복잡하지만 각 data의 유형이 다른 경우 graph 를 사용하면 될 듯 함 (&amp;rArr; social media 같은 경우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sql에서 위와 같은 그래프를 질의할 경우, 질의에 필요한 조인을 미리 알고 있어야 함 .. &lt;b&gt;재귀 공통 테이블식&lt;/b&gt; 을 이용할 수 있지만 그래프 질의에 비해 복잡할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 질의에서는 가변적으로 여러 간선을 순회해야 함, 미리 수를 고정할 수 없음&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트리플 저장소 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리플 저장소에는 모든 정보를 주어, 서술어, 목적어 처럼 3가지 형식으로 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어는 그래프의 정점임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적어는 두 가지 타입 중 하나&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서술어와 목적어는 주어 정점에서 속성의 키-값 쌍과 유사&lt;/li&gt;
&lt;li&gt;서술어는 그래프의 간선이고 주어나 목적어가 정점이 될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDF는 자원 기술 프레임워크로 서로 다른 웹 사이트가 일관된 형식으로 데이터를 기술할 수 있도록 하는 방법, 스파클(SPARQL)은 RDF 데이터 모델 사용한 트리플 저장소 질의 언어&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;PREFIX  dc: &amp;lt;http://purl.org/dc/elements/1.1/&amp;gt;
PREFIX  : &amp;lt;http://example.org/book/&amp;gt;

SELECT  $title
WHERE   { :book1  dc:title  $title }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 데이터베이스&lt;/b&gt; 는 app이 요구하는 데이터 형식이 문서 자체에 포함되어 있고 문서 간 관계가 없을 때 주로 사용&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/tables-and-table-clusters.html#GUID-22771373-D8B8-4A76-9F08-1567489269BB&quot;&gt;https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/tables-and-table-clusters.html#GUID-22771373-D8B8-4A76-9F08-1567489269BB&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://neo4j.com/docs/cypher-manual/5/queries/basic/&quot;&gt;https://neo4j.com/docs/cypher-manual/5/queries/basic/&lt;/a&gt;&lt;/p&gt;</description>
      <category>공부/Database</category>
      <author>흑개1</author>
      <guid isPermaLink="true">https://catscats.tistory.com/264</guid>
      <comments>https://catscats.tistory.com/264#entry264comment</comments>
      <pubDate>Sun, 16 Feb 2025 11:24:06 +0900</pubDate>
    </item>
  </channel>
</rss>