<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog on scalalang2</title><link>https://scalalang2.com/blog/</link><description>Recent content in Blog on scalalang2</description><generator>Hugo</generator><language>ko-KR</language><copyright>Copyright © scalalang2.</copyright><lastBuildDate>Wed, 31 Dec 2025 04:04:42 +0000</lastBuildDate><atom:link href="https://scalalang2.com/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>대규모 단일 통합 MMORPG 서버 아키텍처</title><link>https://scalalang2.com/p/%EB%8B%A8%EC%9D%BC-%ED%86%B5%ED%95%A9-mmorpg-%EC%84%9C%EB%B2%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1260106b4c1d/</link><pubDate>Wed, 31 Dec 2025 04:04:42 +0000</pubDate><guid>https://scalalang2.com/p/%EB%8B%A8%EC%9D%BC-%ED%86%B5%ED%95%A9-mmorpg-%EC%84%9C%EB%B2%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-1260106b4c1d/</guid><description>&lt;h3 id="single-unified-mmorpg-server-architecture"&gt;Single Unified MMORPG Server Architecture&lt;/h3&gt;
&lt;p&gt;2009년, 한국인터넷진흥원은 자바/스프링 프레임워크를 기반으로 한 전자정부 표준프레임워크를 발표합니다. 이후 모든 공공기관의 소프트웨어는 이 표준프레임워크를 따라 개발되면서, 국내 IT 업계에서 백엔드 개발은 자연스럽게 자바/스프링 경험으로 모아지는 경향이 생겼습니다. 최근 마이크로서비스 아키텍처의 발전으로 기술 선택의 폭이 넓어지긴 했지만, 여전히 자바/스프링 프레임워크는 백엔드 개발의 대세로 자리 잡고 있습니다.&lt;/p&gt;
&lt;p&gt;반면, 게임 업계의 상황은 조금 다릅니다. 90년대 초 ‘바람의나라’가 등장한 이래 30년 가까운 세월이 흘렀지만, MMORPG 서버 기술 스택은 IT 산업만큼 활발하게 공유되거나 발전하지 못했습니다. 저는 가장 큰 이유를 다음 요소로 뽑고 있습니다. 게임은 프로그래밍은 물론 디자인, 아트, 사운드, 기획 등 많은 분야의 전문가들이 적게는 수십명 많게는 수백명이 함께 협업하기 때문에 하나의 게임을 만들기 위해 막대한 자본이 투자됩니다. 이로 인해 새로운 대작 게임이 출시될 기회 자체가 적었고, 자연스럽게 혁신적인 서버 구조를 시도하고 기술을 성숙시킬 자양분도 부족했던 것으로 보입니다.&lt;/p&gt;
&lt;p&gt;글을 작성하는 시점에서 최근에 출시한 대형 자본이 투입된 게임들 역시 유저를 여러 서버로 분산시키는 전통적인 방식을 따르고 있습니다. 유저 격차 해소와 랭킹 같은 희소한 자원의 분배 등 여러 기획적인 이유로 서버를 나누기도 하지만, 이 글에서는 순수하게 기술적인 관점에서 단일 통합 MMORPG 서버를 구현하기 위한 아키텍처를 제안해 보고자 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;제가 이 주제에 대해서 고민해보고 자료를 찾아봤는데요. 2019년에 동일한 주제로 발표해주신 박재완님의 &lt;a href="https://marsettler.com/huge-single-mmorpg-server/"&gt;‘거대한 단일 MMORPG 서버[1]’&lt;/a&gt;를 참고했습니다. 해당 발표는 대략적으로 내용을 소개했다면 저는 조금 더 디테일을 넣어서 아키텍처까지 다루어 봤습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;참고로 저는 MMORPG 게임 서버에 참여해 본 적이 없습니다. 지금부터 하는 이야기는 순수하게 제 뇌피셜이며, 개인적 기술 선호에 따라 선택한 스택이기 때문에 정답이 아닙니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-64f45c58"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-64f45c58"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0-OSQQD0H_UGDGZQlwPDVw-c45b6f4ddb67.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-64f45c58"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0-OSQQD0H_UGDGZQlwPDVw-c45b6f4ddb67.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;사진 출처 : &lt;a href="https://m.dongascience.com/ko/news/44830"&gt;동아 사이언스&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;로켓 연구의 선구자인 로버트 고다드 박사는 인류 최초의 액체연료 로켓을 발사하고, 로켓의 기본 원리에 대한 연구를 진행했지만, 그가 살아있을 때 로켓이 실제로 우주까지 도달하는 것을 직접 보지는 못했습니다. 제가 경험도 없는데 이런 글을 쓰는게 어떻게 보일까 부담스러웠지만, 문제 해결을 위해 사고하고는 활동이 꼭 경험을 필요로 하는 건 아니라고 생각해서 용기내어 글을 적어봅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;전통적인 MMORPG 게임 서버 구조&lt;/li&gt;
&lt;li&gt;단일 통합 MMORPG 서버&lt;/li&gt;
&lt;li&gt;프로토콜과 로드밸런서&lt;/li&gt;
&lt;li&gt;Microsoft Orleans : 로직 구현을 위한 프레임워크&lt;/li&gt;
&lt;li&gt;NoSQL 데이터베이스&lt;/li&gt;
&lt;li&gt;NATS : 서버간 통신 Pub/Sub 모델&lt;/li&gt;
&lt;li&gt;데모 소개 및 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="전통적인-mmorpg-게임-서버-구조"&gt;전통적인 MMORPG 게임 서버 구조&lt;/h2&gt;
&lt;p&gt;게임 서버가 다른 서버와 구분되는 가장 큰 차이는 상태를 다루는 방식입니다. 흔히 HTTP 프로토콜로 만들어진 서버가 요청을 독립적으로 처리하는 &lt;strong&gt;비상태(Stateless)&lt;/strong&gt; 환경으로 운영되는 것과 달리, 게임 서버는 유저의 연결을 유지하고 모든 정보를 실시간으로 처리하는 &lt;strong&gt;상태 기반(Stateful)&lt;/strong&gt; 서버라는 점입니다.&lt;/p&gt;
&lt;p&gt;게임 서버는 단위 시간 T 동안 N명의 유저로부터 M개의 액션을 입력받습니다. 서버의 방송 주기를 60Hz로 설정했다면, 약 16.67ms 주기로 로직 스레드에서 유저들의 액션을 처리하고, 그 결과 상태를 다시 클라이언트들에게 전송하게 됩니다. 이 방송 주기(헤르츠)는 게임의 장르나 반응성 요구 수준에 따라 다르게 설정할 수 있습니다. 게임 서버의 동기화 방식과 설계에 대한 더 자세한 내용은 아래 정리된 글을 참고하시면 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B2%8C%EC%9E%84-%EC%84%9C%EB%B2%84-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%9A%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A0%88%EC%9D%B4%ED%84%B4%EC%8B%9C-%EB%B3%B4%EC%83%81-%ED%85%8C%ED%81%AC%EB%8B%89-253640eb57d6" title="https://medium.com/rate-labs/%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B2%8C%EC%9E%84-%EC%84%9C%EB%B2%84-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%9A%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A0%88%EC%9D%B4%ED%84%B4%EC%8B%9C-%EB%B3%B4%EC%83%81-%ED%85%8C%ED%81%AC%EB%8B%89-253640eb57d6"&gt;&lt;strong&gt;멀티-플레이어 게임 서버와 레이턴시 보상 테크닉&lt;/strong&gt;&lt;br&gt;
medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B2%8C%EC%9E%84-%EC%84%9C%EB%B2%84-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%9A%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A0%88%EC%9D%B4%ED%84%B4%EC%8B%9C-%EB%B3%B4%EC%83%81-%ED%85%8C%ED%81%AC%EB%8B%89-253640eb57d6"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-메인-로직-스레드"&gt;① 메인 로직 스레드&lt;/h3&gt;
&lt;p&gt;게임 공간에서 한 유저가 수행하는 행위는 다른 유저에게도 관측 가능해야 합니다. 상호작용과 반응성을 보장하기 위해 같은 공간에 존재하는 유저들은 보통 동일한 서버에 접속하고 해당 서버가 주요 로직을 모두 처리한 뒤 처리 결과를 다시 클라이언트에게 전송합니다.&lt;/p&gt;
&lt;p&gt;메이플스토리나 로스트아크처럼 보스 레이드가 핵심 콘텐츠인 게임 혹은 지역 이동 시 로딩이 존재하는 비심리스(non-seamless) 형태의 월드 구조를 가진 게임에서는 여러 전용 서버를 분리해 각 공간의 로직 처리를 분담하기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-532331e5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-532331e5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ajj9bUemoWqiGzdqqKiLvA-e37fdb848aae.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-532331e5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ajj9bUemoWqiGzdqqKiLvA-e37fdb848aae.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;단순하고 자주 쓰이는 형태의 게임 서버 구조&lt;/p&gt;
&lt;p&gt;서버의 유지보수와 생산성을 높이기 위해서 메인 로직은 싱글 스레드에서 다루는 경우가 많습니다. 반면 단일 서버의 성능을 높이기 위해서 로직 처리를 멀티-스레드 형태로 다루는 서버들도 있습니다. 하지만 개인적으로는 멀티-스레드를 통해서 성능을 높인다는 생각은 좋은 접근 방법이 아니라고 생각합니다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❶ &lt;a href="https://en.wikipedia.org/wiki/Amdahl%27s_law"&gt;암달의 법칙&lt;/a&gt;[2]에 의해 아무리 병렬 처리 효율을 높인다고 해도 코어 개수에 따른 성능 증가율은 선형적으로 증가하지 않습니다.&lt;/li&gt;
&lt;li&gt;❷ 현대 대부분의 서버는 &lt;strong&gt;NUMA&lt;/strong&gt; 아키텍처를 사용하기 때문에 멀티-스레드를 사용해서 확장성과 성능을 높이고자 하는 방식은 너무 단순화된 모델입니다. 게임 플레이를 처리할 때 공유 메모리에 대한 접근이 빈번하기 때문에 멀티-스레드 전략이 단순한 싱글-스레드 전략보다 빠르다고 할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;분산 시스템 분야에서 &lt;a href="https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-mcsherry.pdf"&gt;COST[3]&lt;/a&gt; (Configuration that Outperforms a Single Thread) 라는 개념이 있습니다. COST는 시스템의 확장성을 평가할 때 사용하는 단위로, 단일-코어로만 이루어진 시스템보다 Outperform 하려면 몇 개의 코어가 필요한지를 나타냅니다. 논문에서는 대부분의 병렬 시스템들이 단일 코어 성능을 넘기 위해서 100개 이상의 CPU가 필요했다고 주장합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-source-of-the-truth"&gt;② Source of the Truth&lt;/h3&gt;
&lt;p&gt;온라인 게임 비즈니스에서 데이터베이스는 주로 백업의 역할을 하고 데이터의 진실 공급원은 서버의 메모리 상태가 됩니다. 게임 안에서 공격을 통해 다수의 몬스터의 체력을 변경시키고, 보상을 얻어서 인벤토리에 넣는 등 모든 상태 변화가 굉장히 빈번하게 일어납니다.&lt;/p&gt;
&lt;p&gt;이런 모든 상태 변화마다 데이터베이스에 저장하고 싶어도 IOPS가 감당이 안 되기 때문에 메모리에서 일정 주기마다 캐시 레이어와 데이터베이스 레이어로 데이터를 보내는파이프라인을 구성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5af1d4bc"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5af1d4bc"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rdRAnQBGDjZuHk1V9W173w-72c599f7f0d7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5af1d4bc"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rdRAnQBGDjZuHk1V9W173w-72c599f7f0d7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;어떤 분은 서버 장애가 발생했을 때는 어떻게 하나요? 라고 걱정하실 분도 있을 텐데요. 장애가 발생하면 서버 로그를 기준으로 상태를 복원하거나, 복잡한 복원 프로세스를 만들지 않고 고객센터 대응으로 처리하기도 합니다.&lt;/p&gt;
&lt;p&gt;많은 RPG 게임들은 특정 &lt;strong&gt;게임 서버와 전용 데이터베이스가 일정 수의 유저를 전담해서 처리하는 방식을 선호해왔습니다&lt;/strong&gt;. 예를 들어, 서버 한 대가 최대 1만 명의 동시 접속자를 수용할 수 있는 상황에서, 출시 이후 예상되는 동시 접속자가 10만 명이라면, 서버 선택 화면에서 보이는 게임 서버와 그에 대응하는 데이터베이스 세트를 10개 세팅해서 물리적으로 격리하는 방식으로 서버를 관리합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-be2925bc"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-be2925bc"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GBME5A28Zi5nyNp2cs1XvA-24a450037fde.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-be2925bc"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GBME5A28Zi5nyNp2cs1XvA-24a450037fde.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;마비노기 모바일과 아이온2의 서버 선택 화면&lt;/p&gt;
&lt;p&gt;왜 이런 구조로 운영이 가능할까요? 저는 두 가지 이유가 있다고 생각합니다. ❶ &lt;strong&gt;게임이 생각보다 마이너한 취미입니다.&lt;/strong&gt; 한국갤럽조사연구소가 2024년에 조사한 ‘&lt;a href="https://www.gallup.co.kr/gallupdb/reportContent.asp?seqNo=1481"&gt;한국인이 좋아하는 50가지 [문화편]&lt;/a&gt;[4]’ 자료에 따르면 게임을 가장 많이하는 10대 남성은 44%이고, 이 비율은 나이가 들수록 줄어들며 30대에서는 22%, 40대에서는 14%로 감소했습니다. 여성의 경우에는 게임을 그나마 가장 많이 하는 10대에서는 9% 비율을 차지 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6ac41a39"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6ac41a39"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rSpAgM5kX3fGvKiiN9Z3Ng-f3ef3057d914.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6ac41a39"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rSpAgM5kX3fGvKiiN9Z3Ng-f3ef3057d914.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.gallup.co.kr/gallupdb/reportContent.asp?seqNo=1481"&gt;한국인이 좋아하는 50가지 문화편&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;❷ &lt;strong&gt;게임 산업은 근본적으로 ‘승자 독식’ 구조가 아닙니다.&lt;/strong&gt; 온라인 영상 공유 플랫폼 시장 1위 플랫폼인 유튜브는 월간 활성 사용자(MAU)가 약 25억 명에 이릅니다. 플랫폼 산업은 승자만 살아남아 시장의 대부분을 독점하는 경향이 강합니다. 반면, 게임 시장에서 각 유저들은 취향에 따라 선호하는 게임을 선택합니다. 게임 안에서도 AOS, FPS, 배틀로얄, 익스트랙션, 서브컬처 등 다양한 장르가 공존하며, 각 장르 내에서는 현재 탑급게임도 몇 년만 지나면 순위권에서 쫓겨날 정도로 유행이 빠르게 변하기도 하고, 시장 전체를 독식하는 타이틀도 없습니다.&lt;/p&gt;
&lt;p&gt;따라서 게임 산업의 패러다임이 바뀌지 않는 한, MMORPG 서버가 수용해야 할 이용자 수의 규모는 예측 가능한 범위 안에 머물게 됩니다. 이로 인해 서버 개수를 수동적으로 늘리거나 통합하는 방식으로도 충분히 운영이 가능합니다. 만약 전 세계 모든 인구가 MMORPG 게임을 즐긴다면 이런 서버 운영 방식은 현실적으로나 비용적으로 바람직 하지 않을겁니다.&lt;/p&gt;
&lt;h2 id="단일-통합-mmorpg-서버"&gt;단일 통합 MMORPG 서버&lt;/h2&gt;
&lt;p&gt;레이턴시 때문에 글로벌 원서버는 힘들더라도 동일 국가 안에서는 단일 서버를 운영하면 서버 간 장벽으로 인해 생기는 시장 및 커뮤니티 단절을 줄일 수 있고, 서버가 달라서 같이 즐길 수 없거나, 인구가 적은 시골 서버라서 부족한 유동성 때문에 원하는 세팅을 할 수 없고 나아가 도전 컨텐츠를 즐길 수 없는 등 여러 불편을 해결할 수 있습니다. 저는 지금부터 이 단일 통합 서버를 만들기 위한 아키텍처를 제안해보고자 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;지금부터 하는 이야기는 정답이 아니라 &lt;br&gt;
저의 기술 선호도가 많이 들어가 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;최대 동시접속자 수(CCU)&lt;/strong&gt; : 현재 스팀에서 가장 많이 플레이하는 게임인 카운터 스트라이크의 최대 동접자 수는 대략 150만명입니다. 계산 편의를 위해 게임이 감당해야 하는 최대 동접자 수를 100만명으로 계산하겠습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리량 (QPS) &lt;/strong&gt;: &lt;a href="https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking"&gt;유저는 초당 20회의 이동 및 전투 패킷을 전송한다고 가정했습니다.&lt;/a&gt; 링크된 자료는 데디케이트 서버 게임 기준의 값이고 MMORPG는 그보다는 적다고 가정해도 괜찮을 것 같습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대역폭 수준 (Bandwidth)&lt;/strong&gt; : 유저 한명의 대역폭은 upstream은 10~15 kbps, downstream은 50 ~ 100 kbps로 가정했습니다[5].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;100만 CCU기준을 잡으면 서버는 초당 2000만 건의 요청(Request)을 처리할 수 있어야 하고 upstream 기준에서 필요한 대역폭은 최대 15Gbps, downstream의 경우에는 100Gbps로 생각해봅시다. 이는 단일서버로는 물리적으로 처리가 어렵기 때문에 수평 확장할 수 있는 구조가 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-56e27e65"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-56e27e65"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xQD6iFXmCwiYXWI2DFuB1g-2b0f697cbf17.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-56e27e65"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xQD6iFXmCwiYXWI2DFuB1g-2b0f697cbf17.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 계산은 서울대학교에서 발표한 논문[5]인 “2D와 3D에 기반한 MMORPG 트래픽간의 특성 비교”에서 나온 값을 참고했습니다. 위 그림은 엔씨소프트의 유명한 MMORPG인 리니지 1과 리니지2의 upstream 패킷 크기과 downstream 패킷 크기를 실측한 데이터를 CDF로 나타낸 그래프입니다. (패킷 헤더는 제거한 값입니다)&lt;/p&gt;
&lt;p&gt;아마 이 값으로 실제 계산해보시면, 위 계산보다는 작게 나오실텐데요. 저는 L4 레벨에서 네트워크 프로토콜을 처음부터 만드는 것 보다는 L7 레벨의 표준 프로토콜(e.g. gRPC)을 사용하는 것을 선호하기 때문에 패킷 크기를 높여서 계산했습니다.&lt;/p&gt;
&lt;h3 id="가장-이상적인-모델"&gt;가장 이상적인 모델&lt;/h3&gt;
&lt;p&gt;이상적인 세계에 존재하는 최고의 게임 서버는 단순히 데이터베이스와 게임 서버 1대씩만 운영하는데, 장애가 절대 발생하지 않으면서 동시접속자 100만명의 요청을 처리할 수 있는 CPU와 메모리, 네트워크 대역폭을 모두 갖춘 상태입니다. 코드의 복잡도가 매우 단순해져서 개발자는 온전히 비즈니스 로직에만 집중하면 됩니다. 하지만, 현실에서는 다양한 이유로 이런 이상적인 시스템은 존재하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-70829063"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-70829063"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UKPv7j3HQCBs1mCzG3rtAQ-e4e9334785a6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-70829063"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UKPv7j3HQCBs1mCzG3rtAQ-e4e9334785a6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;가장 이상적인 형태의 서버 아키테처&lt;/p&gt;
&lt;h3 id="게이트웨이-서버와-로직-서버-분리"&gt;게이트웨이 서버와 로직 서버 분리&lt;/h3&gt;
&lt;p&gt;라이온하트 스튜디오는 GamesOnAWS 2022에서 브로드캐스팅을 전담하는 게이트웨이 서버와 로직을 처리하는 게임 서버를 분리하는 방법을 소개했습니다[6]. 많은 수의 유저 커넥션과 네트워크 I/O 비용이 비싸기 때문에 주요 로직을 처리하는 서버와 분리해서 효율성을 높인 부분이 좋아보였습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/JNH5fB8B_ec?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;MMORPG의 성능 최적화 사례 공유 ‘카카오게임즈 — 오딘: 발할라 라이징’&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7f29d921"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7f29d921"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ID0EOwHBkRI_XAcnz-ceng-6246ffe8b3d8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7f29d921"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ID0EOwHBkRI_XAcnz-ceng-6246ffe8b3d8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;네트워크 I/O를 전담하는 게이트웨이 서버&lt;/p&gt;
&lt;p&gt;이 전략을 사용해서 많은 유저와 커넥션을 연결하고 네트워크 I/O를 담당하는 게이트웨이 서버와 메인 로직을 담당하는 게임 서버를 분리합니다. 이제 N대의 게이트웨이 서버와 M개의 게임 서버가 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f08e23da"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f08e23da"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-V3dDrp-ZLZEvoBVnsjPggA-506e32747111.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f08e23da"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-V3dDrp-ZLZEvoBVnsjPggA-506e32747111.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이렇게 네트워크 홉이 한 단계 늘어나는 것에 대해서 유저 반응성을 걱정하는 분들도 있을텐데요. 데이터센터내 노드간 왕복 레이턴시는 대략 500 마이크로초 정도로 알려져 있습니다[7].&lt;/p&gt;
&lt;h3 id="채널링"&gt;채널링&lt;/h3&gt;
&lt;p&gt;특정 지역에 유저가 과도하게 밀집하면 서버의 연산 부하와 클라이언트의 렌더링 부하가 급증하여 원활한 플레이가 어려워집니다. 이를 해결하기 위해 일정 단위로 유저를 그룹화하여 분산 처리하는 과정이 필요한데, 많은 게임들이 하나의 논리적 서버 아래 여러 개의 채널 서버를 두어 로직을 분산하는 방식을 채택하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cc2b18f0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cc2b18f0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UAyo-EsxXAQwEnuYB5iq2A-01ff64ba8a85.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cc2b18f0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UAyo-EsxXAQwEnuYB5iq2A-01ff64ba8a85.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;대표 MMORPG 게임 메이플스토리의 채널링&lt;/p&gt;
&lt;h3 id="쿠버네티스"&gt;쿠버네티스&lt;/h3&gt;
&lt;p&gt;쿠버네티스를 사용하면 확장성과 회복 탄력성, 비용 효율성을 모두 갖춘 서버 아키텍처를 편리하게 설계할 수 있습니다. 또한, 소규모 인원으로도 고도화된 모니터링 시스템과 CI/CD 파이프라인을 쉽게 구축할 수 있고 Agones, Karpenter, Istio 등 풍부한 생태계를 활용해 운영 효율을 높일 수 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 장시간 운영하다 보면 서버 비용 절감이나 여러가지 이유로 클라우드 제공사를 변경하거나 국내 IDC로 이전하게 될 수도 있는데요. 쿠버네티스와 그에 잘맞는 스택을 채용하면 벤더에 종속적이지 않고 이런 이벤트에도 유연하게 대처할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-479b0c02"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-479b0c02"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-L0xxUyYCdSvYcIGr81HWeg-3949b8a198f1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-479b0c02"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-L0xxUyYCdSvYcIGr81HWeg-3949b8a198f1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아마 온-프레미스 환경에서 베어메탈 서버로 MMORPG 서버를 개발한 경험이 많으신 분들이 쿠버네티스를 보면 생소하다고 느끼시는 분들도 있을 것 같고 한편으로는 걱정하시는 부분들도 있을텐데요. 가상화 계층이 늘어나는게 부담일 수 있지만, 높은 수준의 반응성을 요구하는 세션 기반 게임서버들도 쿠버네티스와 Agones를 사용해서 운영하는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;대표적으로 전세계적인 인기를 누리고 있는 PUBG의 배틀그라운드와 최근 TGA에서 멀티플레이어 부분을 수상했고 높은 동접자 수를 유지하고 있는 아크레이더스가 쿠버네티스로 게임 서버를 운영하고 유저에게 인게임 플레이 경험을 제공하고 있습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/GhYUjTxSOkE?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/6MEndHKa8q4?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id="프로토콜과-로드밸런서"&gt;프로토콜과 로드밸런서&lt;/h2&gt;
&lt;p&gt;유저는 게임 서버에 들어오면 하나의 게이트웨이 서버를 할당받아서 패킷을 주고 받아야 합니다. 여기서는 프로토콜 이야기에서 출발해서 로드밸런서 사용에 대해서 다룹니다.&lt;/p&gt;
&lt;h2 id="-프로토콜"&gt;① 프로토콜&lt;/h2&gt;
&lt;p&gt;유저가 서버와 패킷을 주고 받으려면 프로토콜을 결정해야 하는데요. TCP와 UDP를 같이 쓰는 경우도 있고 TCP만 사용하는 경우도 있습니다.&lt;/p&gt;
&lt;p&gt;TCP는 순서 보장을 위해 패킷 유실이 발생하면 재전송하게 됩니다. 이 때, 패킷 유실이 너무 심하면 HOL(Head of Line) blocking 문제로 인해 패킷 처리가 계속 늦어질 수 있습니다. 게임 캐릭터의 현재 위치를 보고하는 이동 패킷의 경우는 과거 정보보다 현재 정보가 더 중요하고 전송 주기가 빠르기 때문에 TCP의 순서제어 같은 매커니즘이 필요하진 않습니다. 실시간성이 강조되는 FPS, 레이싱, 액션 게임들은 대부분 UDP 프로토콜과 UDP에 TCP/IP의 기능을 조금 더한 RUDP를 같이 사용하곤 합니다.&lt;/p&gt;
&lt;p&gt;반면 RPG 게임은 상대적으로 호흡이 길다고 생각합니다. 이 부분은 사람마다 주관적인 평가가 가능하기 때문에 다른 생각을 가지신 분들도 계실 것 같습니다. FPS에서는 0.1초의 차이가 생사를 가르기 때문에 즉각적인 반응이 필수적이지만 RPG는 스킬 쿨타임이 존재하고 카메라의 위치가 캐릭터로부터 거리가 좀 있기 때문에 약간의 오차와 레이턴시는 감내할 수 있다고 생각합니다. 대표적으로 엔씨소프트의 ｢리니지2｣, 블리자드의 ｢월드 오브 워 크래프트｣가 TCP/IP를 사용합니다[8].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b3b588f3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b3b588f3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zitPGZxrOa0fCIVZ3tKfVQ-ef944a70aafa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b3b588f3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zitPGZxrOa0fCIVZ3tKfVQ-ef944a70aafa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;월드 오브 워 크래프트의 한 장면&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-61d86174"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-61d86174"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wisbZ2Ee27xY3Ix0IPmB3Q-b0ea90bf89d4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-61d86174"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wisbZ2Ee27xY3Ix0IPmB3Q-b0ea90bf89d4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;20년 정도 전인 2006년도 논문[8]은 MMORPG에서 TCP/IP 프로토콜이 안좋은 이유를 설명하는 논문입니다. 제 주관적인 생각은 TCP/IP로 운영하는 MMORPG 게임이 사례가 없는 것도 아니고, 네트워크 환경도 2026년 기준에서는 더 높은 대역폭과 전송량을 처리할 수 있기 때문에 gPRC (HTTP/2)도 좋다고 생각합니다. 근거는 업고 괜찮지 않을까 정도로 나이브하게 생각했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;번외로 QUIC은 UDP이긴 하지만 본질적으로는 TCP/IP에서 제공하는 매커니즘을 대부분 구현한 프로토콜이라서 별개의 이야기로 생각하고 있습니다. QUIC Datagram을 사용하면 비신뢰 환경으로도 사용할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/quic-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B5%AC%EA%B8%80-%EB%98%90-%EB%84%88%EC%95%BC-932befde91a1" title="https://medium.com/rate-labs/quic-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B5%AC%EA%B8%80-%EB%98%90-%EB%84%88%EC%95%BC-932befde91a1"&gt;&lt;strong&gt;QUIC 프로토콜 | 구글 또 너야?&lt;/strong&gt;&lt;br&gt;
*QUIC 프로토콜은 2012년 구글이 처음 발표한 범용 전송 계층 네트워크 프로토콜이다. TCP/IP와 TLS레이어가 나누어져 있어서 불필요한 라운드 트립 딜레이(RTT)가 발생했는데, QUIC은 이 과정을 UDP…*medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/quic-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B5%AC%EA%B8%80-%EB%98%90-%EB%84%88%EC%95%BC-932befde91a1"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="-로드밸런서"&gt;② 로드밸런서&lt;/h2&gt;
&lt;p&gt;유저는 게임에 접속하면 게이트웨이 서버 하나를 할당받아 커넥션을 긴 시간동안 유지하게 됩니다. 이 때, 어떤 게이트웨이 서버를 할당받을지 결정해야 하는데요. 로드밸런서를 게이트웨이 앞단에 붙여서 트래픽을 분배할 수 있습니다.&lt;/p&gt;
&lt;p&gt;로드밸런서는 크게 L4과 L7로 구분되는데요. AWS에서는 NLB와 ALB라는 이름으로 나누어져 있습니다. L7는 패킷 암호화, 캐싱, 압축 및 라우팅등 다양한 기능을 지원하지만 그만큼 CPU를 더 많이 사용하기 때문에 레이턴시가 소폭 증가합니다.&lt;/p&gt;
&lt;p&gt;L4 로드밸런서는 패킷 헤더만 보고 목적지로 보내주기 때문에 처리속도가 더 빠르고 TCP와 UDP 프로토콜 모두 이용할 수 있습니다. 하지만, 패킷 암호화가 필요하면 직접 구현해야 하는데요. 인증서 관리가 번거롭고 코드 복잡성이 높아집니다. 그리고 어차피 모든 패킷을 암호화 하겠다고 생각한다면 반응성은 떨어질 수 밖에 없습니다. 여기서는 L7 로드밸런서는 ALB로 결정하되 나중에 상황에 따라 바꿀 수 있다고 생각해둡시다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-25512cad"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-25512cad"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YmGkLilxwfH15G6NzcYcxQ-089407a50d61.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-25512cad"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YmGkLilxwfH15G6NzcYcxQ-089407a50d61.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NLB에선 stickiness을 활성화 하고, ALB에서는 gRPC의 streaming RPC을 사용하면 한 번 유저가 커넥션을 맺고 나면 모두 동일한 서버와 통신을 하게 됩니다.&lt;/p&gt;
&lt;h2 id="microsoft-orleans--로직-구현을-위한-프레임워크"&gt;Microsoft Orleans : 로직 구현을 위한 프레임워크&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/dotnet/orleans"&gt;Orleans&lt;/a&gt;는 2014년 마이크로소프트 리서치에서 &lt;a href="https://www.microsoft.com/en-us/research/publication/orleans-distributed-virtual-actors-for-programmability-and-scalability/"&gt;논문&lt;/a&gt;[9]으로 처음 발표한 분산 액터 모델 프레임워크입니다. 액터 모델은 Stateful한 연산을 자주 다루는 게임이란 도메인은 잘 맞는 부분이 있습니다. 프로그래머는 추상화된 기반 위에서 로직을 작성하기만 하면 되죠. .NET 기반으로 작성되어 있기 때문에 C#에 익숙한 개발자라면 쉽게 이용 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;앞서 COST라는 개념을 언급한 것 처럼 분산 액터 모델은 최고 성능을 내기 위한 최고의 방법은 아니지만, 높은 개발 생산성, 가용성, 확장성을 갖추고 있습니다. 서버 중 일부 장애가 나더라도 빠르게 복원 되며, 높은 트래픽을 요구사항에 대응하려면 단순히 서버의 개수를 늘리는 것으로 해결 됩니다.&lt;/p&gt;
&lt;p&gt;앞으로 Orleans 프레임워크의 내부 동작을 이해하는데 필요한 2개의 논문을 다루는 추가 글을 작성할 예정입니다. 여기서는 Orleans에 대한 간단한 소개와 함께 왜 게임이란 도메인과 잘 어울리는지 예시와 함께 설명하겠습니다.&lt;/p&gt;
&lt;h3 id="-분산-액터-모델"&gt;① 분산 액터 모델&lt;/h3&gt;
&lt;p&gt;액터 모델은 분산 환경에서 사용할 수 있는 &lt;strong&gt;객체 지향 프로그래밍(OOP)&lt;/strong&gt;으로 받아들이는 게 이해하기 쉽습니다. 일반적으로 객체를 하나 생성하면 런타임이 메모리 어딘가에 객체를 저장하죠. 이 객체는 메모리 주소에 의해 메모리 공간이 특정되고 필요시 접근해서 값을 읽을 수 있습니다.&lt;/p&gt;
&lt;p&gt;단일 노드만으로는 메모리와 한계로 100만명의 동시접속자를 표현하는 객체를 생성할 수 없습니다. N대의 컴퓨팅 노드를 Orleans 클러스터로 구성하면 객체를 생성할 때 클러스터 내 임의의 노드 중 하나에 객체가 생성됩니다. 개발자는 객체가 어디에 생성되었는지 알 필요없이 추상화된 레벨에서만 코드를 작성하면 됩니다. 이 객체를 &lt;strong&gt;‘액터(Actor)’&lt;/strong&gt;라고 부릅니다.&lt;/p&gt;
&lt;p&gt;아래 그림은 클러스터 안에 3대의 노드와 다수의 Actor가 생성된 모습을 볼 수 있습니다. Orleans는 클러스터를 구성하는 하나의 노드를 Silo라고 부르고 Actor를 Grain이라고 부릅니다. 이제부터 용어 혼동을 방지하기 위해 저도 Grain으로 용어를 통일하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7832a67b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7832a67b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E4OSjURJPS-0RwHz75XXLA-93fe09a36915.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7832a67b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E4OSjURJPS-0RwHz75XXLA-93fe09a36915.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/orleans/overview"&gt;3대의 Silo로 구성된 Orleans Cluster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Grain은 객체의 ID와 도메인에 해당하는 behavior를 필수적으로 가져야 하고, 선택적으로 상태값을 가집니다. behavior는 Grain을 정의하는 클래스 이름으로 구분되는데요. 이 값이 동일하더라도 ID값이 다르면 서로 다른 객체로 존재합니다. 예를 들어 동일한 PlayerGrain으로 선언된 객체에 대해서 &lt;code&gt;PlayerGrain(&amp;quot;Alice&amp;quot;)&lt;/code&gt;와 &lt;code&gt;PlayerGrain(“Bob”)&lt;/code&gt;은 서로 다른 값입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b5e7ab3a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b5e7ab3a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-agMr1oIOXFiki0PZFkj0sQ-a5d16ea09f13.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b5e7ab3a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-agMr1oIOXFiki0PZFkj0sQ-a5d16ea09f13.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Actor Model하면 또 떠오르는 프레임워크 중에 Akka가 있습니다. &lt;a href="https://www.youtube.com/watch?v=FUEZibcZEkg"&gt;데브시스터즈가 개발한 쿠키런 킹덤이 Akka&lt;/a&gt;[10]로 개발되었습니다. 만약 Java/Kotlin/Scala 에 익숙하시다면 Akka도 좋은 선택지라고 생각합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-아이템-11-교환-예시"&gt;② 아이템 1:1 교환 예시&lt;/h3&gt;
&lt;p&gt;Orleans로 코딩하는 게 어떤 느낌인지 체감하기 위해 MMORPG의 핵심 기능중 하나인 1:1 아이템 교환 시스템을 설계하고 만들어보겠습니다. 거래는 두 명의 유저가 서로의 아이템을 올리고, 양쪽 모두 ‘확인(Confirm)’을 눌렀을 때만 성사되어야 합니다. 이 과정에서 아이템 복사나 증발 같은 치명적인 버그가 발생하면 안 되므로 데이터 무결성이 중요합니다.&lt;/p&gt;
&lt;p&gt;Grain을 만들기 위해 먼저 두 개의 인터페이스부터 선언해봅니다. Grain은 목적지를 찾기 위한 ID가 존재합니다. &lt;code&gt;IGrainWithStringKey&lt;/code&gt; 인터페이스를 상속받음으로써 ID를 String 값으로 사용하겠다고 선언합니다. 거래 세션은 고유 ID(e.g. UUID)를 가질 것입니다.&lt;/p&gt;
&lt;p&gt;액터 모델끼리의 상호작용은 네트워크를 타고 이루어지기 때문에 전부 비동기 연산입니다. 그래서 리턴값은 반드시 &lt;code&gt;Task&lt;/code&gt;여야 하고요. 값을 반환하고 싶다면 &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt;를 선언하시면 됩니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 플레이어 Grain 인터페이스&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPlayerGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGrainWithStringKey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 아이템 추가&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 아이템 삭제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 거래 세션에 참여한다&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;JoinTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tradeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;partnerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 거래를 그냥 종료한다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;LeaveTrade&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 거래 세션 Grain 인터페이스&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITradeGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGrainWithStringKey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 거래 세션 참여&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 거래 세션 이탈&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 거래할 아이템 올리기&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;OfferItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 양쪽 다 확정 시 거래 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0fb4b578"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0fb4b578"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-w1Ft4oVPlvTR0ipOr-fOvg-8847e1683672.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0fb4b578"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-w1Ft4oVPlvTR0ipOr-fOvg-8847e1683672.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아래는 구현 코드를 간소화해서 표현했습니다. Grain은 반드시 &lt;code&gt;Grain&lt;/code&gt; 클래스를 상속받아야 합니다. 그리고 Grain 객체를 획득하려면 멤버 변수 &lt;code&gt;GrainFactory&lt;/code&gt;의 &lt;code&gt;GetGrain&amp;lt;TInterface&amp;gt;(ID)&lt;/code&gt; 함수를 사용하면 됩니다. OOP로 생각하면 &lt;code&gt;new&lt;/code&gt; 키워드로 객체를 생성하는 행위와 같습니다.&lt;/p&gt;
&lt;p&gt;Orleans 런타임은 객체의 활성화 및 비활성화를 직접 관리하는데요. 만약 생성된 객체가 없다면 객체를 생성하고 &lt;code&gt;OnActivateAsync&lt;/code&gt; 함수를 호출합니다. 거래가 끝나고 일정 시간 동안 요청이 없으면 런타임이 메모리를 정리합니다. 즉, 개발자는 ‘객체를 언제 메모리에서 해제해야 하지?’를 고민할 필요가 없습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 플레이어를 나타내는 Actor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IPlayerGrain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;currentTradeSession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_inventory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;JoinTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tradeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;partnerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// 이미 거래 중인 건이 있다면 그냥 리턴 한다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;currentTradeSession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tradeId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tradeGrain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GrainFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITradeGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;tradeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tradeGrain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPrimaryKeyString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;LeaveTrade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ... 생략&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 거래 로직을 담당하는 Actor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITradeGrain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 각 플레이어가 내놓은 아이템 목록 (PlayerId -&amp;gt; {ItemId, ItemName})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 확정 버튼 누른 플레이어 목록&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;OfferItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;확정 이후 아이템 추가 불가능&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 아이템 변경 시 확정 취소&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 둘 다 확정했다면 거래 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_confirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ExecuteTrade&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;ExecuteTrade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerA_Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerB_Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerA&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GrainFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPlayerGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;playerA_Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerB&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GrainFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPlayerGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;playerB_Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;itemsFromA&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerA_Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;itemsFromB&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_offeredItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerB_Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A의 아이템 처리 (A에서 삭제 -&amp;gt; B에 추가)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itemsFromA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// B의 아이템 처리 (B에서 삭제 -&amp;gt; A에 추가)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itemsFromB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 과정에서 우리는 복잡한 DB 트랜잭션 격리 수준이나 락을 고민하지 않고, 마치 평범한 객체 지향 프로그래밍 하듯이 분산 서비스를 만들고 있습니다, 또 하나 주목해야 할 점은 &lt;code&gt;TradeGrain&lt;/code&gt;은 Thread-safe하지 않은 &lt;code&gt;Dictionary&lt;/code&gt;와 &lt;code&gt;List&lt;/code&gt; 자료구조를 사용한다는 것입니다.&lt;/p&gt;
&lt;p&gt;Grain의 모든 호출은 동일한 스레드에서 순차적으로 호출되는 게 보장되기 때문에 동시성 문제가 발생하지 않습니다[11]. 즉, &lt;code&gt;AddItem&lt;/code&gt;과 &lt;code&gt;Confirm&lt;/code&gt; 요청이 동시에 들어와도 Orleans는 이를 메시지 큐에 저장해서 줄 세운 뒤 하나씩 처리해 줍니다.&lt;/p&gt;
&lt;p&gt;Silo가 하나인 단독 클러스터인 경우 모든 객체가 같은 호스트에 생성되기 때문에 함수 호출은 네트워크 스택을 타지 않아 직렬화 비용 없이 직접 실행됩니다. 만약 Silo가 많아지면 많아질수록 이 세 개의 Grain(플레이어 A, 플레이어 B, 거래 세션)은 서로 다른 호스트에 생성될 확률이 높아지면서 네트워크 호출을 발생시키는데요. 데이터센터 내 통신이기 때문에 레이턴시 손실이 크진 않지만, 여러 서버가 부하를 분담하고 있으며 노드의 대수를 추가함으로써 전체 처리량은 계속 증가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e151bc5b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e151bc5b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VFGzT6h3-TqdFg06sK3UkQ-3a63d037aa4d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e151bc5b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VFGzT6h3-TqdFg06sK3UkQ-3a63d037aa4d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-예외-처리"&gt;③ 예외 처리&lt;/h3&gt;
&lt;p&gt;네트워크는 다양한 이유로 실패할 수 있습니다. 목적지 시스템의 장애가 있거나, 네트워크가 혼잡할 수 있고, 상어가 해저 케이블을 물어뜯는 중일 수도 있죠. 그래서 분산 환경에서 네트워크 패킷이 어떤 형태로 도착할 것 이라는 가정은 &lt;strong&gt;최대 한 번(At most once)&lt;/strong&gt; 또는 &lt;strong&gt;적어도 한 번 (At least once)&lt;/strong&gt;밖에 없습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;최대 한 번 전송 (At most once delivery)&lt;/strong&gt; : 수신자 입장에서 메시지가 도착하거나 도착하지 않을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;적어도 한 번 전송 (At least once delivery) &lt;/strong&gt;: 메시지를 최소 한 번 받습니다. 다만 2번 이상 받을 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cc217b58"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cc217b58"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QK1D3qTahc1IRxiwz8sjjQ-d4340bb732e2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cc217b58"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QK1D3qTahc1IRxiwz8sjjQ-d4340bb732e2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;상어가 해저 케이블을 물어뜯는 모습 : 출처 — &lt;a href="https://www.youtube.com/watch?v=1ex7uTQf4bQ"&gt;유튜브&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Orleans를 비롯한 액터 모델은 &lt;strong&gt;최대 한 번(At most once)&lt;/strong&gt;을 기본 전송 시맨틱으로 가지고 있습니다. 거래를 하는 중간에 하나의 노드에 장애가 발생하면 해당 호출은 Timeout Exception을 발생시키면서 최초로 전송한 쪽으로 예외를 전파시킵니다. Actor의 상태가 지금은 단순한 메모리이기 때문에 거래 확정 당시 네트워크 장애가 발생하면 데이터 무결성을 해치게 됩니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;ExecuteTrade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A의 아이템 처리 (A에서 삭제 -&amp;gt; B에 추가)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itemsFromA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 여기서 장애가 발생하면 A의 아이템만 사라진 셈이다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;playerB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d1120412"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d1120412"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sftVGeQ-1rCw_bDplA1KWg-b0d6a836c3dd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d1120412"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sftVGeQ-1rCw_bDplA1KWg-b0d6a836c3dd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;거래 중 통신 오류로 인해 데이터 무결성이 깨진 상황&lt;/p&gt;
&lt;p&gt;분산환경에서도 데이터의 무결성을 지키는 몇 가지 방법이 알려져 있습니다. 대표적으로 &lt;strong&gt;2단계 커밋(2 Phase Commit), TCC(Try-confirm/Cancel), &lt;/strong&gt;Saga Pattern&lt;strong&gt;이 있는데요. 무엇하나 직접 구현하려고 하면 굉장히 피곤해집니다. Orleans는 액터의 상태를 데이터베이스에 저장하는 저장소와 &lt;/strong&gt;2단계 커밋으로 구현된 분산 트랜잭션을 내장된 기능으로 제공합니다.&lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/10/EldeebBernstein-TransactionalActors-MSR-TR-1.pdf"&gt;[12]&lt;/a&gt;&lt;a href="https://www.vldb.org/pvldb/vol17/p3720-eldeeb.pdf"&gt;[13]&lt;/a&gt;&lt;a href="http://sites.computer.org/debull/A19june/p3.pdf"&gt;[14]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이를 사용하면 일부 장애 상황이 나타나면 트랜잭션에 연관된 모든 액터의 상태를 장애 이전으로 상황으로 안전하게 롤백시켜줍니다.&lt;/p&gt;
&lt;h3 id="-영속성-레이어와-분산-트랜잭션-사용"&gt;④ 영속성 레이어와 분산 트랜잭션 사용&lt;/h3&gt;
&lt;p&gt;Orleans에서 트랜잭션 기능을 사용하려면 먼저 저장소 부터 설정해야 합니다. RDBMS 계열은 유명한 것들은 제공하는데요. 여기서는 수평 확장이 가능한 키-밸류 기반 데이터베이스가 잘 어울린다고 생각해서 DynamoDB를 사용합니다. 데이터베이스 선택 이유는 뒤에서 한 번 더 설명하겠습니다. &lt;a href="https://learn.microsoft.com/en-us/dotnet/orleans/grains/grain-persistence/?pivots=orleans-7-0"&gt;저장소는 클러스터를 세팅할 때 설정에 추가해주면 됩니다.&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;siloBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDynamoDBGrainStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Default&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;configureOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* dynamodb 세팅 */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;siloBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이렇게 저장소를 선택했다면 어떤 값을 저장할지 클래스를 선언해야 합니다. 위에서 TradeGrain은 상태를 그냥 메모리에 적어두었는데요. 이제 이 값들을 상태 클래스로 바꿔 봅시다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[GenerateSerializer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeState&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Id(0)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OfferedItems&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Id(1)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ConfirmedPlayers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[Reentrant]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITradeGrain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ITransactionalState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TradeState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 생성자 주입을 통해 TransactionalState를 받습니다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// &amp;#34;trade_data&amp;#34;는 저장소에 저장될 때 사용되는 식별자 이름입니다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TradeGrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [TransactionalState(&amp;#34;trade_data&amp;#34;)]&lt;/span&gt; &lt;span class="n"&gt;ITransactionalState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TradeState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;상태 클래스는 일반 Plain Object로 선언해서 다루면 됩니다. 클래스 위에는 &lt;code&gt;GenerateSerializer&lt;/code&gt; 속성을 반드시 선언해야 합니다. 객체 데이터를 데이터베이스에 바이너리 형태로 직렬화해서 저장하기 때문에 Orleans에서 직렬화기를 자동으로 만들어주는 기능입니다.&lt;/p&gt;
&lt;p&gt;그리고 Orleans는 .NET 프레임워크 기반이기 때문에 의존성 주입(DI) 기능을 기본적으로 사용하는데요. 의존성 주입을 통해 &lt;code&gt;ITransactionalState&amp;lt;T&amp;gt;&lt;/code&gt; 객체를 받아서 멤버 변수에 저장해둡니다. TradeGrain위에 &lt;code&gt;Reentrant&lt;/code&gt; 속성이 추가된 걸 알 수 있는데요. 트랜잭션 컨텍스트를 Grain 안쪽으로 전달하기 위해서 반드시 사용해야 합니다. 왜 이 속성이 필요한지는 다른 글에서 자세히 다루겠습니다.&lt;/p&gt;
&lt;p&gt;다음은 상태를 읽거나 변경하는 모든 메서드에 &lt;code&gt;Transaction&lt;/code&gt;속성을 달아서 이함수를 호출하면 분산 트랜잭션을 생성하거나 참여한다는 것을 선언합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Orleans&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPlayerGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGrainWithStringKey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;RemoveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;JoinTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tradeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITradeGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGrainWithStringKey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;OfferItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [Transaction(TransactionOption.CreateOrJoin)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이제 마지막입니다. 데이터를 읽거나 쓸때 &lt;code&gt;PerformRead&lt;/code&gt; 또는 &lt;code&gt;PerformUpdate&lt;/code&gt; 함수를 사용합니다. 해당 함수는 트랜잭션 처리에 필요한 컨텍스트 정보를 갱신함과 동시에 값을 변경하거나 읽습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isTradeReady&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PerformUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 참여자 2명이 모두 확정했는지 확인&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfferedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfirmedPlayers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;isTradeReady&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 조건 충족시 거래 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 이 호출은 모두 같은 트랜잭션 컨텍스트에 포함되어 실행됩니다.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isTradeReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ExecuteTrade&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션의 의미에 맞게 이 연산은 원자성을 보장합니다. 장애로 인한 실패나 혹은 논리적 오류로 인해 개발자가 인위적으로 Exception을 발생시키면 모든 연산을 이전 상태로 복원하는 것을 보장합니다. 또한 현재 트랜잭션에 참여중인 다 수의 Grain에 대한 상태 변경 연산을 안전하게 커밋함으로써 원자성과 무결성을 보장해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c37e28c2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c37e28c2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j6IWXCh-TwBcygQRXzCmug-5f32feba4dc5.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c37e28c2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j6IWXCh-TwBcygQRXzCmug-5f32feba4dc5.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;트랜잭션이 취소되는 과정을 보여준다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ba7ca88e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ba7ca88e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Usr4SITRffuDrlwbxoBn2Q-20583b826bc1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ba7ca88e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Usr4SITRffuDrlwbxoBn2Q-20583b826bc1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;게임 로직 서버는 분산 액터 모델인 Orleans 클러스터로 구성한다&lt;/p&gt;
&lt;h2 id="nosql-데이터베이스"&gt;NoSQL 데이터베이스&lt;/h2&gt;
&lt;p&gt;게임 로직 서버를 Orleans로 사용하면서 확장성을 챙겼습니다. 이제는 플레이어의 진행도를 안전하게 보관할 데이터베이스를 고민해야 합니다. 이 데이터베이스 분야를 크게 양분하면 &lt;strong&gt;관계형 데이터베이스 (RDBMS)&lt;/strong&gt;와 &lt;strong&gt;비관계형 데이터베이스 (NoSQL)&lt;/strong&gt;로 나눌 수 있습니다.&lt;/p&gt;
&lt;p&gt;데이터베이스를 선택할 때는 각각의 장단점이 있고 어떤 것을 선택해도 구현할 수 있는 방법은 있어 보입니다. 디스코드&lt;a href="http://How%20Discord%20Stores%20Trillions%20of%20Messages"&gt;[15]&lt;/a&gt;는 ScyllaDB라는 분산 키-밸류 데이터베이스를 사용하는 반면, 슬랙&lt;a href="https://slack.engineering/scaling-datastores-at-slack-with-vitess/"&gt;[16]&lt;/a&gt;은 MySQL의 분산 확장 버전인 Vitess를 채택했습니다. 두 회사 모두 성공적으로 채팅 애플리케이션을 구현했지만, 그 기술적 선택은 서로 달랐습니다.&lt;/p&gt;
&lt;p&gt;다시 말해 NoSQL과 RDBMS 어떤 것을 선택해도 주어진 문제를 해결할 수 있는 방법은 있다고 생각하기 때문에 결국에는 취향 차이라고 정도라고 생각합니다만, 저는 여기서는 다음 두 가지 이유로 키-밸류 기반의 분산 데이터베이스를 추천합니다. 대표적인 데이터베이스로는 AWS DynamoDB, Azure Table, ScyllaDB가 있습니다.&lt;/p&gt;
&lt;h3 id="-관계형-vs-비관계형"&gt;① 관계형 vs 비관계형&lt;/h3&gt;
&lt;p&gt;RDBMS와 NoSQL의 가장 큰 차이점 중 하나는 데이터 모델의 표현 방식입니다. 관계형 데이터 모델에서는 데이터를 &lt;strong&gt;‘테이블 단위의 스키마’&lt;/strong&gt;로 표현하고 정규화를 거듭할 수록 &lt;strong&gt;‘테이블과 테이블 사이의 관계성’&lt;/strong&gt;이 생깁니다.&lt;/p&gt;
&lt;p&gt;하지만 서버는 객체지향 프로그래밍 철학으로 개발하기 때문에 우리가 코드에서 주로 다루게 되는 데이터는 &lt;strong&gt;‘객체’&lt;/strong&gt; 입니다. 따라서 테이블과 객체 사이에는 표현의 차이가 존재하기 때문에 RDBMS를 사용할 때 테이블과 객체 사이의 복잡한 전환 계층이 필요합니다.&lt;/p&gt;
&lt;p&gt;이렇게 관계형 데이터베이스와 객체지향 프로그래밍의 개념적 차이로 인해 전환 비용이 발생하는 문제를 &lt;strong&gt;임피던스 불일치(impedence mismatch)&lt;/strong&gt;라고 부르며 이게 생각보다 일할 때 체감되는 비용이 큽니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1f626f65"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1f626f65"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rr2I3cBHDDS7i_7U9ZV5yA-1ca9803a67b0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1f626f65"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rr2I3cBHDDS7i_7U9ZV5yA-1ca9803a67b0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;임피던스 불일치의 예시&lt;/p&gt;
&lt;p&gt;반면 스키마를 제약하지 않는 &lt;strong&gt;비관계형(NoSQL)데이터베이스&lt;/strong&gt;는 유연한 스키마를 제공하여 이러한 불일치를 줄일 수 있습니다. 객체를 JSON 혹은 다른 방식으로 직렬화 해서 데이터를 저장함으로써 더 자연스럽게 표현할 수 있고 코드의 간결성과 유지보수성을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;객체 데이터는 지역성(locality)이 높아서 상대적인 성능 이점이 있습니다. 데이터가 여러 테이블로 나눠져 있는 관계형 DB에서는 전체 문서를 조회할 때 다수의 인덱스과 디스크 탐색이 필요하기 때문에 레이턴시가 증가하고 처리량이 상대적으로 낮아집니다.&lt;/p&gt;
&lt;p&gt;게임의 경우 플레이어 본인의 정보를 조회하고, 본인의 정보만 편집하는 경우가 많고 요청이 매우 빈번하게 이루어지기 때문에 객체를 그대로 직렬화해서 저장할 수 있는 NoSQL이 더 높은 성능과 낮은 레이턴시를 가질 수 있습니다. 아래는 제가 즐겨하는 게임 중 하나의 인벤토리입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RDBMS에서 인벤토리라는 테이블에 808개의 레코드를 기록할 건지, 아니면 인벤토리 객체 전체를 직렬화해서 NoSQL에 저장하는 차이를 상상하면 이해가 쉬울 것 같습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-304cd258"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-304cd258"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-ZsIniThML23nAqB4-71662eaf5c1e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-304cd258"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-ZsIniThML23nAqB4-71662eaf5c1e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;본인의 비틱 디스크&lt;/p&gt;
&lt;h3 id="-확장성"&gt;② 확장성&lt;/h3&gt;
&lt;p&gt;오랜 시간 개발되고 많이 사용하는 MySQL, PostgreSQL, Oracle 등은 본래 단일 서버 환경 기준으로 개발된 제품입니다. 다중-서버를 운영하는 게임에서는 괜찮아도 단일 서버를 운영하는 게임에서는 높은 트래픽에 대응 할 수단이 많지 않습니다.&lt;/p&gt;
&lt;p&gt;전통적인 RDBMS는 리더/팔로워 구조로 쓰기 노드는 단일 인스턴스로 제한되어 있고 읽기 성능에 대해서만 비동기 환경에서 확장성을 가집니다. 데이터를 생산하는 사람과 이를 소비하는 사람의 비율이 극명하게 차이나는 플랫폼 서비스는 RDBMS가 좋은 경우가 많습니다. (e.g. 뉴스라는 도메인을 생각하면 글을 생산하는 사람보다 읽는 사람이 훨씬 많습니다.)&lt;/p&gt;
&lt;p&gt;반면, 게임이란 도메인은 쓰기 비율이 굉장히 높은 분야입니다. 사용자의 모든 행동이 플레이어 상태 변경을 필요로 하기 때문에 처리해야 하는 요청이 많은 상황이라면 단일 쓰기 인스턴스에 의존하는 전통적인 RDBMS는 아쉬울 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f9a62459"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f9a62459"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-HtQM7k3xk852UVQdl2Wikw-87f172020fe9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f9a62459"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-HtQM7k3xk852UVQdl2Wikw-87f172020fe9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Primary/Secondary 복제 모델&lt;/p&gt;
&lt;p&gt;앞서 슬랙이 MySQL의 분산 확장 버전인 Vitess로 구현되어 있다고 소개한 바 있는데요. 슬랙이 Vitess를 선택한 이유는 도메인을 생각해 볼 필요가 있습니다. 워크스페이스와 멤버, 채널과 채팅은 1:N의 관계성을 가집니다. 만약 N:M의 관계를 주로 다루게 되면 거의 모든 노드에 걸쳐서 원자성을 보장하는 분산 트랜잭션이 필요하기 때문에 단일 데이터베이스보다 성능이 낮을 수 있습니다.&lt;/p&gt;
&lt;p&gt;AWS DynamoDB는 키-밸류 기반의 완전 관리형 NoSQL 데이터베이스이며 예측 가능한 레이턴시와 테이블의 무제한 확장성을 제공합니다. 하나의 데이터는 아이템이라고 불리고, 아이템은 테이블에 저장됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5d7e62d1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5d7e62d1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-t4oe4BFY7dFBbggmNWTYAg-b26fbfe3700a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5d7e62d1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-t4oe4BFY7dFBbggmNWTYAg-b26fbfe3700a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Dynamo DB는 데이터를 파티셔닝 해서 저장하고 있습니다. 시스템은 자동으로 특정 파티션에 트래픽이 많이 몰려서 처리량이 특정 임계값을 넘어가면 이 파티션을 분할돼서 서로 다른 인스턴스에 할당됩니다. 내가 사용하는 데이터에 따라 작게는 2, 3대의 서버로부터 많게는 수천대의 서버로 데이터를 분산 저장합니다. 이런 특징 때문에 AWS 데이터센터가 감당할 수 있는 만큼 확장이 가능하며 일관된 레이턴시를 제공합니다.&lt;/p&gt;
&lt;p&gt;RDBMS는 성능에 악영향을 미치는 원인이 많고 내부 시스템이 복잡하기 때문에 개발자가 자기가 작성하고 있는 코드가 운영 환경 에서도 잘 동작할 거라는 확신을 가지기 어려운데요. DynamoDB는 모든 요청에 대해 예측 가능한 수준의 레이턴시를 제공하기 때문에 개발자가 성능을 최적화 할 때, API 호출 횟수와 데이터 크기만 신경 쓰면 될 정도로 단순해집니다.&lt;/p&gt;
&lt;p&gt;위에서 아이템 1:1 교환 예시에서 언급했듯이 Orleans 프레임워크는 Grain(=Actor)의 상태를 저장하는 동작을 추상화 시켰기 때문에 개발자는 데이터 저장소만 추가하면 시스템이 자동으로 필요한 순간에 DynamoDB API를 호출해서 데이터를 저장하고 불러옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5347f04b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5347f04b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-hmILXImq4ksu-IiEVAZRxA-e6009c0c99f1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5347f04b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-hmILXImq4ksu-IiEVAZRxA-e6009c0c99f1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;게임서버에서 Actor의 상태를 직렬화 해서 DynamoDB에 저장한다.&lt;/p&gt;
&lt;p&gt;DynamoDB와 Vitess를 포함한 분산 데이터베이스들은 1:N (One-To-Many) 관계의 데이터를 다룰 때는 높은 지역성을 가지기 때문에 효율적이지만, N:M (Many-To-Many) 관계의 쿼리가 필요할 때는 한 가지 문제점이 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 특정 플레이어가 소유한 모든 아이템을 조회해줘 같은 연산은 파티션 키가 유저 ID인 경우 한 대의 인스턴스에만 질의하면 됩니다. 반면, 특정 아이템를 소유한 모든 플레이어를 조회해줘 같은 연산은 거의 모든 인스턴스에 질의를 하기 때문에 오히려 성능에 안좋은 영향을 끼칩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f3666b8b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f3666b8b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UPOXV5FSOO_0aPGLlh5mwg-6081a72e7dd2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f3666b8b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UPOXV5FSOO_0aPGLlh5mwg-6081a72e7dd2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Many-To-Many 관계를 다루는 경우가 빈번한 경우&lt;/p&gt;
&lt;p&gt;따라서, 운영 목적으로 인해 OLAP 쿼리가 필요한 경우에는 이를 질의하기 편한 저장소가 필요한데요. 요즘에는 오브젝트 스토리지를 활용해서 비용을 절감하는 Clickhouse가 눈에 좀 들어왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-68e7b39d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-68e7b39d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dvh2jOzrBAAQtlpI3V4yig-98f9b6459a5d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-68e7b39d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dvh2jOzrBAAQtlpI3V4yig-98f9b6459a5d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DynamoDB는 특정 기간 내 발생한 변경 사항만을 S3로 추출하는 증분 백업 기능을 제공합니다. 백업 작업은 운영 테이블의 성능에 영향을 미치지 않으며 RCU 또한 소모하지 않으므로, 약 15분 주기로 ClickHouse에 데이터를 지속적으로 동기화하여 실시간성 제약이 비교적 완화된 분석 쿼리를 수행합니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 Orleans는 Grain의 상태를 직렬화하여 바이너리 타입으로 저장하기에, 해당 데이터를 역직렬화하여 JSON 포맷으로 변환할 필요가 있는데요. Clickhouse의 UDF 기능을 활용하면 구현이 가능해 보였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;추가로 DynamoDB에 대해 더 알고 싶으시다면 아래 글을 참고해주세요. AWS라는 특정 벤더에 종속되는 것이 부담스럽다면, DynamoDB API와 호환되는 ScyllaDB를 고려해보시는 것도 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625" title="https://medium.com/rate-labs/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625"&gt;&lt;strong&gt;DynamoDB의 시스템 디자인과 분산 트랜잭션 구현 원리&lt;/strong&gt;&lt;br&gt;
medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="nats--서버간-통신-pubsub-모델"&gt;NATS : 서버간 통신 Pub/Sub 모델&lt;/h2&gt;
&lt;p&gt;로직 처리는 Orleans 클러스터로 구성된 게임 서버에 의해 이루어집니다. 게임 서버는 로직 처리 결과를 주기적으로 게이트웨이 서버로 전달하고 게이트웨이 서버가 다시 이 정보를 유저에게 전달합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-45e4141b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-45e4141b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cYzBWuOPo4hJDmRShVu3Dg-9b5c31f11ef2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-45e4141b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cYzBWuOPo4hJDmRShVu3Dg-9b5c31f11ef2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 때, 게임 서버에서 지금 발생시킨 정보를 어떤 게이트웨이 서버에게 전달해야하는지 라우팅 정보를 직접 관리하면 코드가 너무 복잡해지고, 그렇다고 모든 게이트웨이 서버에게 전달해버리면 게이트웨이 서버에 더해지는 부담이 너무 커집니다.&lt;/p&gt;
&lt;p&gt;따라서 NATS 라는 경량 메시지 브로커 계층을 추가해서, 서로 원하는 토픽에만 메시지를 발행/구독 하는방식으로 복잡도를 낮추고자 했습니다. 예를 들어 심리스 오픈 월드를 구현한다고 합시다. 전체 지역을 쿼드 트리로 나눈 다음 자신의 AOI(Area Of Interests) 레벨에 따라 주변 필드 ID를 구한 다음 관심있는 필드의 변경 사항을 업데이트 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6caa8f0a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6caa8f0a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ILQ8vXiqig3gvTabmI0-hw-5f9fde8bcfbe.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6caa8f0a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ILQ8vXiqig3gvTabmI0-hw-5f9fde8bcfbe.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;심리스 오픈 월드 구현 예시&lt;/p&gt;
&lt;p&gt;메시지 발행/구독을 지원하는 브로커 시스템하면 Redis Pub/Sub은 많이 들어보셨어도 NATS는 생소하신 분들이 많을 것 같습니다. Redis는 다양한 자료구조를 지원하는 범용 시스템이라면 NATS는 메시징을 위해서 최적화된 시스템 입니다. 한 때 65만 동시접속자를 처리했던 에픽게임즈의 &lt;strong&gt;폴 가이즈&lt;/strong&gt;가 NATS를 서버 간 통신에 서비스 디스커버리 용도로 사용하고 있고[17], 국내에서는 토스 증권이 클라이언트 알림 용도로 NATS를 사용합니다[18].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SLASH’24의 발표에 따르면 NATS가 Redis Pub/Sub에 비해 높은 처리량을 가진다고 했는데요. Redis는 7버전부터 Sharded Pub/Sub을 지원하기 때문에 아마 벤치마크 자체는 다시 평가해봐야 할 것 같습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-841bb96e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-841bb96e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KqCg6AfcbtF1_HurpeFEkQ-49b3de0c758c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-841bb96e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KqCg6AfcbtF1_HurpeFEkQ-49b3de0c758c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0b053801"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0b053801"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--Md_Ie1eo1VLY8hacVH7-g-ae1f571965c7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0b053801"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--Md_Ie1eo1VLY8hacVH7-g-ae1f571965c7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;게이트웨이 서버와 게임 서버 사이의 NATS 레이어가 추가됨&lt;/p&gt;
&lt;h2 id="데모-소개-및-마무리"&gt;데모 소개 및 마무리&lt;/h2&gt;
&lt;p&gt;앞서 논의된 아키텍처를 바탕으로 간단한 &lt;a href="https://github.com/scalalang2/seamless-world-server-with-orleans"&gt;데모 프로젝트(Github)&lt;/a&gt;[19]를 구현했습니다. Helm Chart에 NATS와 DynamoDB Local 버전을 포함해 필요한 인프라 구성 요소들을 선언했습니다. 게임 로직은 쿼드트리 기반의 공간 분할을 적용한 심리스 오픈 월드를 구성했습니다. 각 구역을 담당하는 &lt;code&gt;IWorldGrain&lt;/code&gt; 은 지역 내 유저 위치 정보를 관리하며 100ms 주기로 최신 상태를 NATS 토픽에 발행(Publish) 합니다.&lt;/p&gt;
&lt;p&gt;더미 클라이언트는 무작위 좌표에 스폰되어 한 방향으로 뛰어가도록 구현했고 100ms 마다 게이트웨이 서버 자신의 위치 패킷을 전송합니다. 첨부된 영상은 Minikube로 로컬 쿠버네티스 클러스터를 구동한 다음 게임 서버와 게이트웨이 서버는 1대씩 실행하고, 100개의 더미 클라이언트를 생성한 모습입니다. AOI관리에 따라 인접한 유저 정보는 동기화되고, 범위를 벗어난 유저 객체는 메모리에서 해제되는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://player.vimeo.com/video/1150551992?app_id=122963" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;데모 게임 — Godot Engine을 사용했고 캐릭터 이동을 구현할 때 이 &lt;a href="https://www.youtube.com/watch?v=JlgZtOFMdfc"&gt;영상&lt;/a&gt;을 참고했습니다.&lt;/p&gt;
&lt;p&gt;제가 경험한 것과 생각한 것을 바탕으로 내용을 전개했는데요. 숙련된 엔지니어분들의 다양한 관점이나 최적화 제안은 언제나 환영하며, 댓글로 의견 주시면 살펴보도록 하겠습니다. 긴 글 함께 해주셔서 감사드리며, 2026년 새해 좋은 일만 있으시길 바라겠습니다. ☺️&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://marsettler.com/huge-single-mmorpg-server/"&gt;거대한 단일 MMORPG 서버&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://en.wikipedia.org/wiki/Amdahl%27s_law"&gt;Amdahl’s law — Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-mcsherry.pdf"&gt;Scalability! But at what COST? @ USENIX HOT’OS 2015&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://www.gallup.co.kr/gallupdb/reportContent.asp?seqNo=1492"&gt;한국인이 좋아하는 50가지 [그 밖의 것들]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://mmlab.snu.ac.kr/wp-content/uploads/2025/05/ITService_MMORPG.pdf"&gt;2D와 3D에 기반한 MMORPG 트래픽간의 특성 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://www.youtube.com/watch?v=JNH5fB8B_ec"&gt;MMORPG의 성능 최적화 사례 공유 ‘카카오게임즈 — 오딘: 발할라 라이징’&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://gist.github.com/jboner/2841832"&gt;Latency Numbers Every Programmer Should Know&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://dl.acm.org/doi/10.1145/1178823.1178830"&gt;An empirical evaluation of TCP performance in online games&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://www.microsoft.com/en-us/research/publication/orleans-distributed-virtual-actors-for-programmability-and-scalability/"&gt;Orleans: Distributed Virtual Actors for Programmability and Scalability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://www.youtube.com/watch?v=FUEZibcZEkg"&gt;NDC21 쿠키런: 킹덤 | 서버 아키텍처 뜯어먹기!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[11] &lt;a href="https://www.ledjonbehluli.com/posts/orleans_grain_message_processing/"&gt;Just how does a grain process a message? - Ledjon Behluli&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[12] &lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/10/EldeebBernstein-TransactionalActors-MSR-TR-1.pdf"&gt;Transactions for Distributed Actors in the Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[13] &lt;a href="https://www.vldb.org/pvldb/vol17/p3720-eldeeb.pdf"&gt;Cloud Actor-Oriented Database Transactions in Orleans VLDB’24&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[14] &lt;a href="http://sites.computer.org/debull/A19june/p3.pdf"&gt;Resurrecting Middle-Tier Distributed Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[15] &lt;a href="https://discord.com/blog/how-discord-stores-trillions-of-messages"&gt;How Discord Stores Trillions of Messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[16] &lt;a href="https://slack.engineering/scaling-datastores-at-slack-with-vitess/"&gt;Scaling Datastores at Slack with Vitess&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[17] &lt;a href="https://gdcvault.com/play/1034308/Terminal-Velocity-Lessons-Learned-from"&gt;Lessons Learned from Server Scaling During the Launch of ‘Fall Guys’&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[18] &lt;a href="https://static.toss.im/slash24/QR/slash24-11.pdf"&gt;SLASH’24 | SSE 이벤트 푸쉬로 불필요한 Polling 제거하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[19] &lt;a href="https://github.com/scalalang2/seamless-world-server-with-orleans"&gt;Github | seamless-world-server-with-orleans&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Azure Cosmos DB의 DiskANN 고성능 벡터 인덱스</title><link>https://scalalang2.com/p/azure-cosmos-db%EC%9D%98-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EB%B2%A1%ED%84%B0-%EC%9D%B8%EB%8D%B1%EC%8A%A4-52f874e4bd7b/</link><pubDate>Sun, 14 Sep 2025 02:03:17 +0000</pubDate><guid>https://scalalang2.com/p/azure-cosmos-db%EC%9D%98-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EB%B2%A1%ED%84%B0-%EC%9D%B8%EB%8D%B1%EC%8A%A4-52f874e4bd7b/</guid><description>&lt;p&gt;OpenAI는 대조학습을 활용한 CLIP 모델에서 멀티-모달 데이터를 하나의 공유된 임베딩 공간으로 투영할 수 있음을 보여주었고, 문장 내에서 단어간 관계성을 계산하는 어텐션 매커니즘을 활용한 트랜스포머 아키텍처를 활용한 언어 모델의 발전은 텍스트를 의미 있는 벡터 공간으로 표현하는 임베딩 기술을 크게 발전시켰습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/iv-5mZ_9CPY?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;임베딩 기술이 발전하면서 벡터 데이터베이스와 이를 활용한 RAG, 멀티-모달 검색 등이 업계에서 많은 주목을 받고 있습니다. 이번 포스팅에서는 VLDB’25에서 마이크로소프트가 발표한 &lt;strong&gt;Azure Cosmos DB를 활용한 비용 효율적인 저지연 벡터 검색 (Cost-Effective, Low Latency Vector Search with Azure Cosmos DB)[1]&lt;/strong&gt;에 대해 다룹니다.&lt;/p&gt;
&lt;p&gt;사실 벡터 데이터베이스는 효율적인 벡터 검색을 위한 &lt;strong&gt;인덱스 자료구조&lt;/strong&gt;가 핵심 역할을 하는데요. Azure Cosmos DB에서 활용하는 벡터 인덱스 구조인 &lt;strong&gt;DiskANN (NIPS’19)[2]&lt;/strong&gt;를 이해하면 해당 논문에서 말하는 바는 쉽게 이해할 수 있습니다.&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;벡터 인덱스&lt;/li&gt;
&lt;li&gt;파티션 기반과 그래프 기반 인덱싱&lt;/li&gt;
&lt;li&gt;Disk ANN&lt;/li&gt;
&lt;li&gt;Azure Cosmos DB&lt;/li&gt;
&lt;li&gt;평가 및 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="벡터-인덱스"&gt;벡터 인덱스&lt;/h2&gt;
&lt;p&gt;먼저 용어부터 구분하면서 시작하겠습니다. &lt;strong&gt;벡터&lt;/strong&gt;란 N차원 공간에서 크기와 방향을 갖는 숫자 배열로 표현되는 수학적 대상을 의미하고 &lt;strong&gt;임베딩&lt;/strong&gt;이란 텍스트, 이미지, 음성 등 비정형 데이터를 딥러닝 모델이 학습한 공간으로 사상시켜서 얻은 벡터 표현을 의미합니다.&lt;/p&gt;
&lt;p&gt;역색인 방식을 사용하는 전통적인 텍스트 기반 검색엔진은 텍스트가 문서에 포함되어 있는가를 기준으로 문서를 찾았다면, 벡터 인덱스를 사용하면 의미 단위의 시맨틱 검색이 가능해집니다. 예를 들어, 쇼핑몰에서 검색어로 &lt;strong&gt;노트북&lt;/strong&gt; 이라는 키워드 대신 &lt;strong&gt;가벼운데 가성비 있는 노트북&lt;/strong&gt; 등 의미가 추가된 키워드를 줘도 시스템이 연관된 상품을 더 잘 찾을 수 있습니다.&lt;/p&gt;
&lt;p&gt;벡터 인덱스는 수 천, 수 억개의 벡터 정보를 데이터베이스에 저장했을 때 쿼리로 주어진 정보와 가장 의미적으로 가까운 벡터를 효율 적으로 찾기 위한 자료구조입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;벡터 유사도 검색을 구현하는 가장 간단한 방법은, 저장된 모든 벡터를 선형 탐색(&lt;strong&gt;O(N)&lt;/strong&gt;)하면서 거리를 계산하는 것입니다. 하지만 이 방식은 데이터 규모가 커질수록 비효율적이므로 더 나은 방법을 찾아야 합니다. 여기서는 논문 내용을 이해하기 위한 기초적인 내용을 다루고 있습니다. 벡터 검색에 대한 더 깊은 내용은 &lt;a href="https://medium.com/u/89ed5885f0af"&gt;Pangyoalto&lt;/a&gt;님의 다음 두 글을 추천드립니다.\&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pangyoalto.com/faiss-1-hnsw/"&gt;벡터 검색 알고리즘 살펴보기(1): Similarity Search와 HNSW&lt;br&gt;
&lt;/a&gt;- &lt;a href="https://pangyoalto.com/hnsw-spann/"&gt;벡터 검색 알고리즘 살펴보기(2): HNSW, SPANN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id="-근사-최근접-이웃-탐색-anns-approximate-nearest-neighbor-search"&gt;① 근사 최근접 이웃 탐색 (ANNS: approximate nearest neighbor search)&lt;/h3&gt;
&lt;p&gt;머신러닝이 발전하면서 피쳐 공간의 차원이 점차 증가하고 있으나, 학습에 쓰이는 데이터 표본 수는 상대 적으로 적기 때문에 고차원 공간에서 너무 희소하게 분포되는 현상이 나타나며 이를 &lt;strong&gt;차원의 저주&lt;/strong&gt;라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/geekculture/curse-of-dimensionality-e97ba916cb8f" title="https://medium.com/geekculture/curse-of-dimensionality-e97ba916cb8f"&gt;&lt;strong&gt;Curse of dimensionality&lt;/strong&gt;&lt;br&gt;
medium.com&lt;/a&gt;&lt;a href="https://medium.com/geekculture/curse-of-dimensionality-e97ba916cb8f"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6118b07c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6118b07c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wnNOrIlevLCpg2ViSqh5PQ-338c3cf746d4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6118b07c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wnNOrIlevLCpg2ViSqh5PQ-338c3cf746d4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;출처 : &lt;a href="https://medium.com/geekculture/curse-of-dimensionality-e97ba916cb8f"&gt;https://medium.com/geekculture/curse-of-dimensionality-e97ba916cb8f&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;차원이 증가할 수록 희소한 공간이 많아지기 때문에 상대적인 거리라는 값이 의미를 잃어버리게 됩니다. 저는 이를 직관적으로 이해하기 위해서 흔히 다음과 같은 예시를 드는데요. 철수와 민수를 구분하는 데이터를 &lt;strong&gt;성별&lt;/strong&gt; 하나만 사용해서 벡터를 만들면 이 둘 사이의 거리는 0입니다.&lt;/p&gt;
&lt;p&gt;여기서 MBTI, 직업, 거주지 등 다양한 변수를 추가할 때 마다 서로가 구별되기 때문에 벡터 거리상으로는 계속 멀어지게 되는데요. 유클리드 거리 수식을 보면 차원의 수가 증가할 수록 값은 벡터의 거리가 1로 수렴하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-602f0d7b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-602f0d7b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-As9ZXy_DVIBAIGNm8mrzOw-6f8d1e092a5e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-602f0d7b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-As9ZXy_DVIBAIGNm8mrzOw-6f8d1e092a5e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;유클리드 거리 계산&lt;/p&gt;
&lt;p&gt;따라서 가장 가까운 벡터를 찾는 건 사실상 불가능에 가깝고, 가장 인접할 가능성이 높은 k 개의 이웃을 찾는 것을 목표로 합니다. 이를 좀 더 formal하게 정의하면, 쿼리와 알고리즘의 결과값 X가 k개의 최근접 이웃 후보를 가지고 있고 ground-truth를 G라고 할 때, k- @ recall k는 다음과 같이 정의됩니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e5f74786"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e5f74786"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rJecniEeNzW8bYJo0ab5PA-70d6d95cd25e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e5f74786"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rJecniEeNzW8bYJo0ab5PA-70d6d95cd25e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-파티션-기반-인덱싱"&gt;② 파티션 기반 인덱싱&lt;/h3&gt;
&lt;p&gt;파티션 기반 인덱스란 벡터 데이터들을 특정 파티션 단위로 나누어서 쿼리로 찾아야 할 대상을 줄이는 인덱싱 기법입니다. SPANN, ScaNN 등 여러 기법들이 있는데 대부분 IVF(Inverted File Index)를 기반에 두고 있습니다.&lt;/p&gt;
&lt;p&gt;IVF를 수행하려면 먼저 파티션 개수인 파라미터와 탐색할 파티션의 개수인 nlist와 nprobe를 결정한 다음 K-평균 클러스터링, LSH 등을 활용해서 군집화를 수행해서 nlist로 결정한 개수만큼 중심점을 추출합니다. RDBMS에서 인덱스를 생성할 때는 데이터가 없어도 인덱스부터 만들 수 있었는데요, 벡터 인덱스에서는 클러스터링관련 알고리즘을 활용하기 때문에 데이터 부터 모은다음 인덱스를 생성해야합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-03fd453d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-03fd453d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k1ZiwL1oR2oi2lhnSuI1sQ-fb8b4e64b269.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-03fd453d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k1ZiwL1oR2oi2lhnSuI1sQ-fb8b4e64b269.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Voronoi_diagram"&gt;Voronoi diagram — Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이렇게 얻어진 중심점들을 기준으로 공간이 분할되면, 각 벡터는 가장 가까운 중심점에 대응되는 파티션에 할당됩니다. 한번 파티션이 나워진 뒤 검색을 수행할 때는 주어진 쿼리 벡터와 가장 가까운 중심점(또는 여러 개의 중심점, nprobe에 따라 다름)을 찾은 후 해당 파티션들 안에서 후보 벡터와의 거리를 비교하여 가장 가까운 벡터를 찾습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-984052f3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-984052f3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CEIA8y4go7woDyZCCWoelw-44edb67d06df.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-984052f3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CEIA8y4go7woDyZCCWoelw-44edb67d06df.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.nvidia.com/blog/accelerated-vector-search-approximating-with-nvidia-cuvs-ivf-flat/"&gt;NVIDIA : Accelerated Vector Search: Approximating with NVIDIA cuVS Inverted Index&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;파티션의 개수를 k개로 하고 전체 데이터 수를 N이라 할 때, IVF 인덱스의 탐색 시간 복잡도는 O(k + N/k)가 됩니다. 탐색하는 파티션이 많을수록 검색 속도는 느려지지만 정확도(recall)는 증가합니다.&lt;/p&gt;
&lt;h2 id="-diskann"&gt;③ DiskANN&lt;/h2&gt;
&lt;p&gt;지금부터는 &lt;strong&gt;그래프 기반 벡터 인덱스&lt;/strong&gt;를 소개하려고 합니다. 이 분야는 &lt;strong&gt;NSW, &lt;/strong&gt;HNSW라는 잘 알려진 알고리즘이 있지만, 이번 포스팅의 주인공인 Cosmos DB가 DiskANN를 사용하고 있기 때문에 이 글에서는 이것만 소개합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;제가 다른 알고리즘은 얕게만 알고 있어서 설명할 능력이 부족하기도 하고 DiskANN의 기본 원리는 다른 그래프 기반 알고리즘과 많은 부분을 공유하므로, DiskANN을 이해하면 다른 관련 알고리즘을 학습하는 데에 도움이 되실 것으로 기대합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="diskann"&gt;DiskANN&lt;/h2&gt;
&lt;p&gt;DiskANN은 다음 특징을 가지는 ANN 알고리즘입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DiskANN은 64GB RAM을 가진 서버에서 100차원의 벡터 데이터 1억개를 인덱싱하고 서빙할 수 있습니다. 95% + 1-recall@1의 성능을 가지며 레이턴시는 5ms 미만입니다.&lt;/li&gt;
&lt;li&gt;Vamana 알고리즘을 개발했으며 NSG, HNSW보다 더 차원이 적은 그래프 인덱스를 생성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Vamana는 인-메모리 모드에서도 동작하며 검색 속도가 NSG, HNSW와 비슷하거나 더 우수합니다.&lt;/li&gt;
&lt;li&gt;Vamana는 벡터 압축 스킴(e.g. Product Quantization)과 쉽게 통합할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vamana로 바로 들어가기 전에 선행 지식으로 필요한 GreedySearch에 대해 이야기 하면서 시작해봅시다.&lt;/p&gt;
&lt;h3 id="-greedysearch-알고리즘"&gt;① GreedySearch 알고리즘&lt;/h3&gt;
&lt;p&gt;DiskANN을 포함한 NSW, HNSW는 벡터들을 그래프로 구축하고 GreedySearch라는 탐색 알고리즘을 활용합니다. 그래프에서 정점은 하나의 벡터를 의미하고 간선은 벡터와 벡터 사이의 거리를 말합니다. 거리는 코사인 유사도와 유클리드 거리 등을 활용할 수 있습니다. 일단 논문에서는 유클리드 거리를 사용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dd2c1883"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dd2c1883"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SzcIHkCKhC9Hfk7TO7tfvQ-207b7918d64e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dd2c1883"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SzcIHkCKhC9Hfk7TO7tfvQ-207b7918d64e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DiskANN / GreedySearch 알고리즘&lt;/p&gt;
&lt;p&gt;GreedySearch의 함수에 4개의 매개변수가 존재하는데요. s는 시작 정점을 의미하며 아무거나 선택해도 상관없습니다. x_q는 우리가 찾고자하는 쿼리벡터를 의미합니다. 예를 들어, 검색어로 “&lt;strong&gt;가벼운데 가성비 있는 노트북&lt;/strong&gt;”을 입력한 경우 이를 임베딩 해서 변환한 벡터가 쿼리벡터가 되고 이것과 가장 가까운 정점들 k개가 우리가 찾고자 하는 결과값입니다.&lt;/p&gt;
&lt;p&gt;마지막 매개변수인 L은 탐색에 어느정도 시간을 쓸 수 있는가를 결정합니다. 이 값을 늘릴수록 더 많은 정점을 탐색하기 때문에 정확도가 증가하지만, 반대로 레이턴시도 같이 증가하기 때문에 반응성이 떨어집니다.&lt;/p&gt;
&lt;p&gt;알고리즘은 아직 방문하지 않은 정점 집합 ($L \setminus V$) 중에서 쿼리 벡터와 가장 가까운 벡터 ($p^* = \operatorname{argmin}(\lVert p - q \rVert)$) 를 찾고 $p^*$ 벡터의 이웃을 다시 탐색 공간에 추가하면서 더 이상 가까운 이웃이 나타나지 않을때까지 반복합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a67c6ced"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a67c6ced"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SlVMQCnhYcm59lQ6DHN2NQ-8c2e4d7c06e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a67c6ced"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SlVMQCnhYcm59lQ6DHN2NQ-8c2e4d7c06e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Azure Cosmos DB 논문에서 인용한 L과 Recall간의 관계&lt;/p&gt;
&lt;p&gt;위 그림은 천만개의 벡터를 저장했을 때 L을 변경해가면서 정확도(Recall)와 레이턴시 그리고 컴퓨팅 사용량(Request Units)의 변화량을 보여줍니다. L=50세팅에서는 p99에서 20ms에 가까운 레이턴시로 서비스를 제공하지만 L을 높이면 레이턴시와 정확도가 같이 증가합니다.&lt;/p&gt;
&lt;h3 id="-robustprune"&gt;② RobustPrune&lt;/h3&gt;
&lt;p&gt;Greedy Search 알고리즘이 잘 수렴하기 위한 충분 조건으로 &lt;strong&gt;SNG(sparse neighborhood graph)&lt;/strong&gt;라는 속성을 만족해야 하는데요. 이를 만족하는 그래프를 구축하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;집합 S를 P \ {p}로 초기화합니다.&lt;/li&gt;
&lt;li&gt;집합 S에서 p와 가장 가까운 정점 $p^*$을 찾아 간선을 추가합니다.&lt;/li&gt;
&lt;li&gt;모든 정점 $p&amp;rsquo;$에 대해, 만약 $d(p,p&amp;rsquo;) &amp;gt; d(p,p^*)$라면 $p&amp;rsquo;$를 후보에서 제거합니다.&lt;/li&gt;
&lt;li&gt;집합 S가 공집합이 아닐 때까지 2~3 과정을 반복합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-28085b1f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-28085b1f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-U3lWhhZs_ualsvHZnKd6jQ-0d24c460f21f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-28085b1f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-U3lWhhZs_ualsvHZnKd6jQ-0d24c460f21f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그림에서 빨간색 정점은 (3)번 과정에서 제거된 노드를 의미하고 파란색 정점은 아직 제거되지 않아서 다음 Iteration에서 평가될 노드를 의미합니다. 이 알고리즘은 O(N²)의 시간복잡도를 가집니다.&lt;/p&gt;
&lt;p&gt;하지만 이러한 그래프 구축 방식은 특정 상황에서 탐색 성능이 크게 저하되는 엣지 케이스가 발생할 수 있는데요. 가령, 모든 벡터 정점이 하나의 차원 위에 완벽히 정렬되는 경우가 이에 해당합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-fca09ad0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-fca09ad0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5DYALOm6KLRDVjHfX9_MAg-c97309216bcf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-fca09ad0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5DYALOm6KLRDVjHfX9_MAg-c97309216bcf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이를 보완하기 위해 DiskANN에서는 간선 제거에 대한 임계값 α를 사용한 RobustPrune 알고리즘을 사용합니다. 매개변수는 총 4개가 등장합니다. p는 간선을 추가할 정점을 의미하고 V는 후보군을 의미합니다. α은 임계값을 의미하며 R은 간선의 최대 갯수입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9227202d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9227202d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vu0F42igUX1MKSpzC2Jc8Q-f8ac6fe315b9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9227202d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vu0F42igUX1MKSpzC2Jc8Q-f8ac6fe315b9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;RobustPrune 알고리즘&lt;/p&gt;
&lt;h3 id="-vamana"&gt;③ Vamana&lt;/h3&gt;
&lt;p&gt;마지막으로 Vamana 알고리즘은 그래프 구축 알고리즘입니다. 나이브한 방법을 사용하면 그래프 구축의 시간 복잡도가 O(N²)이기 때문에 Vamana 알고리즘에서는 RobustPrune을 정점 일부를 샘플링 해서 간선을 구축합니다. 아래 노테이션 중에 medoid는 그래프 내에서 다른 모든 정점과의 거리 합이 가장 작은 정점을 의미하는데요. 꼭 정확한 값을 구할 필요는 없어 보이고 적당히 휴리스틱하게 구해서 써도 될 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0612ad41"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0612ad41"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2ffbW1yi7rh_ZcDjx2MN8A-3144002f7e49.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0612ad41"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2ffbW1yi7rh_ZcDjx2MN8A-3144002f7e49.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Vamana Indexing Algorithm&lt;/p&gt;
&lt;p&gt;일단 내가 가지고 있는 데이터 셋에서 랜덤그래프 G를 만듭니다. 즉, 앞서 말했듯이 데이터 셋을 충분히 가지고 있는 상황에서 수행해야 합니다. 그리고 랜덤한 정점 i에 대해서 GreedySearch를 수행한 뒤 방문한 노드에 대해 RobustPrune을 수행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-298ae2f2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-298ae2f2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4Pv99F54zVfRLGYzJ-wQqQ-d44bce83fc3e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-298ae2f2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4Pv99F54zVfRLGYzJ-wQqQ-d44bce83fc3e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그림은 Vamana Indexing 알고리즘에서 각 Iteration 마다 그래프 모양이 어떻게 변하는지 보여줍니다.&lt;/p&gt;
&lt;h3 id="-product-quantization"&gt;④ Product Quantization&lt;/h3&gt;
&lt;p&gt;여기서 두 가지 문제가 더 남아있습니다. DiskANN은 데이터를 1억개 이상 서빙하는게 목표였죠? (1) 1억개 이상의 정점을 가진 그래프를 메모리에 올려두고 인덱스 빌딩을 하면 메모리를 초과하게 되구요. (2) 고차원 벡터를 사용하면 주어진 탐색 시간 내에 빠른 탐색이 어려울 수 있습니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-92145cf9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-92145cf9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BtkOiPGjKTehtCJWb25CdA-5c6781431549.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-92145cf9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BtkOiPGjKTehtCJWb25CdA-5c6781431549.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;(1)번 문제는 그래프를 여러 개의 파티션으로 분리한 다음에 개별적으로 인덱스를 빌딩한 다음에 하나로 병합해서 SSD에 저장합니다. 너무 딱 잘라서 나눠버리면 쿼리 성능이 떨어지기 때문에 적당히 정점들이 일부 겹치게끔 파티션을 나눠서 진행하는 방법으로 해결합니다.&lt;/p&gt;
&lt;p&gt;이제 (2)번 문제가 중요한데요. 유명한 벡터 압축 기법인 Product Quantization을 사용합니다. Product Quantization[3]은 벡터를 여러 개의 서브 벡터로 나눈 다음에 서브 벡터 끼리 군집화를 진행한 다음 centroid id를 부여해서 부호화합니다. 이렇게 인코딩 된 값을 PQ code라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cf4f37a5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cf4f37a5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LHah0Tcqf8G9x8TjBV1Lzw-bbb557fdf6d3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cf4f37a5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LHah0Tcqf8G9x8TjBV1Lzw-bbb557fdf6d3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;예를 들어, 32비트 부동 소수점 값으로 이루어진 크기가 1024인 벡터를 상상해봅시다. 원본 벡터를 저장하기 위해 필요한 공간은 1024 × 32 = 4096 byte 인데요. 이를 8개의 서브벡터로 나누어서 8 bit = log(256)의 ID를 부여하면 8 byte로 무려 512배 압축할 수 있습니다. 이렇게 PQ로 압축해도 벡터와 벡터간의 거리 성질은 어느 정도 보존 되는 것으로 알려져 있습니다.&lt;/p&gt;
&lt;h2 id="azure-cosmos-db"&gt;Azure Cosmos DB&lt;/h2&gt;
&lt;p&gt;Azure Cosmos DB에서 벡터 인덱싱은 기존에 존재하던 B+ Tree에 DiskANN을 결합시켜서 벡터 인덱스를 지원하기 때문에 앞서 설명했던 DiskANN을 이해했다면 이 논문의 거의 대부분은 이해할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Azure Cosmos DB[4]는 완전 관리형, 글로벌로 분산된, 멀티 모델 데이터베이스 서비스입니다. 여기서 멀티&lt;strong&gt;-&lt;/strong&gt;모델 이란 의미는 키-밸류, 문서, 그래프, 관계형 등 다양한 형태의 정보를 단일 인터페이스로 다룰 수 있게 통합했다는 의미인데요. 실제로는 DB마다 사용법은 조금씩 다르지만 서버리스, 글로벌 복제, 확장성, 파티션 분할 등의 특징은 공통적으로 가집니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9afca9e5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9afca9e5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Nu08tTv-6eOKXWi-mYn5eg-cef9e6acc27b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9afca9e5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Nu08tTv-6eOKXWi-mYn5eg-cef9e6acc27b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://azure.microsoft.com/en-us/blog/azure-cosmos-db-database-for-intelligent-cloud-intelligent-edge-era/"&gt;https://azure.microsoft.com/en-us/blog/azure-cosmos-db-database-for-intelligent-cloud-intelligent-edge-era/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Azure Cosmos DB는 서버리스 제품이기 때문에 사용자가 독립적인 머신을 빌리는 것이 아니라 멀티 테넌트 형태로 다른 이용자와 함께 서비스를 공유하게 됩니다.&lt;/p&gt;
&lt;p&gt;Cosmos DB는 다양한 데이터베이스 작업의 비용을 Request Unit(RU)라는 단위로 표준화하며, 처리량은 초당 Request Unit(RU/s) 기준으로 측정됩니다. Request Unit은 CPU, 메모리, IOPS 등 시스템 자원을 추상화한 일종의 통화 단위인데요. 따라서 사용자는 실제 머신 임대 비용이 아닌 자신이 사용한 RU 단위에 따라 비용을 지불하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-87ca49d4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-87ca49d4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7gD2NQhyq1K8Mvlve_0qNg-16b7ce2dd0c7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-87ca49d4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7gD2NQhyq1K8Mvlve_0qNg-16b7ce2dd0c7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/request-units"&gt;Request Units in Azure Cosmos DB&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-schema-agnostic-indexing"&gt;① Schema Agnostic Indexing&lt;/h3&gt;
&lt;p&gt;Azure Cosmos DB의 모든 데이터베이스가 벡터 인덱싱을 지원하는 건 아니고, Azure Cosmos DB for NoSQL에만 적용되어 있습니다. 이 DB는 JSON데이터만 사용해서 데이터를 저장, 색인, 쿼리 할 수 있습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;prd_20250914_001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Wireless Noise-Cancelling Headphones&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;category&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Electronics/Headphones&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;brand&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Acme Audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;KRW&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;199000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;attributes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;color&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Black&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;connection&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bluetooth 5.3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;batteryLifeHours&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;weightGrams&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;media&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;thumbnail&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://cdn.example.com/images/prd_20250914_001_thumb.jpg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;images&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://cdn.example.com/images/prd_20250914_001_1.jpg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://cdn.example.com/images/prd_20250914_001_2.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;embedding&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;values&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;0.0182&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0427&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0911&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0035&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0279&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;0.0554&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0071&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0448&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0362&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;0.0209&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0098&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0331&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;0.0115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0042&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0297&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;-0.0168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0220&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;dim&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text-embedding-3-small&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;source&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;title+category+attributes&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;상품 데이터를 예시로 보면,상품명과 카테고리, 상품 속성을 임베딩해서 벡터를 구하고 이를 Azure Cosmos DB에 저장하면 &lt;code&gt;embedding.values&lt;/code&gt; 에 대해서 벡터 검색을 수행할 수 있습니다. 임베딩 하는 방법은 LLM기업에서 제공하는 API를 사용하거나 HuggingFace에서 적당한 모델을 다운로드 받으셔서 사용하면 됩니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# pip install openai&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;임베딩은 텍스트를 벡터로 표현합니다.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;벡터는 의미적 유사도를 계산하는 데 사용됩니다.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text-embedding-3-small&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;NoSQL 제품군은 재밌는 특징을 하나 가지고 있는데요. JSON 데이터를 저장하면 내가 스키마를 지정하지 않아도 모든 필드에 대해 인덱스를 자동으로 생성해줍니다. 이를 Schema Agnostic Indexing 이라고 부르는데요. 2015년 VLDB에서 소개된 논문인 &lt;a href="https://www.vldb.org/pvldb/vol8/p1668-shukla.pdf"&gt;Schema-Agnostic Indexing with Azure DocumentDB[5]&lt;/a&gt; 에서 관련 내용을 다루고 있습니다.&lt;/p&gt;
&lt;p&gt;여기서는 단순하게만 알아보겠습니다. NoSQL 제품에서는 JSON 문서를 저장할 때 이를 트리구조로 변환한 다음 인덱스에 전부 저장합니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-28cff48f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-28cff48f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QV2IAfH9lBOMjZBjuiafTA-3bbd370abd73.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-28cff48f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QV2IAfH9lBOMjZBjuiafTA-3bbd370abd73.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;조금 더 상세하게 설명하면 아래 JSON 문서를 저장하면, 오른쪽 처럼 정보를 분해할 수 있습니다. 이렇게 분해된 경로를 키로 가지고 문서 ID를 값으로 가지게끔 Bw-Tree에 저장합니다. (Bw-Tree는 또 뭘까요 🤔)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b3046722"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b3046722"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-o97pf3GTUYRbaY_kmR8vuw-08611e41b345.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b3046722"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-o97pf3GTUYRbaY_kmR8vuw-08611e41b345.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Bw-Tree (2013)[6][7]는 B+ Tree의 특징은 그대로 유지하면서 lock-free 형태로 만든 버전입니다. 기본적인 아이디어는 트리를 직접 변경하는 대신 변경 사항인 D elta Update를 Leaf노드에 추가하고 모든 연산을 CAS(Compare-And-Swap)으로만 진행합니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b44fd71c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b44fd71c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ajDJwCeglHP-GnbXPsvohg-6ffefc227aad.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b44fd71c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ajDJwCeglHP-GnbXPsvohg-6ffefc227aad.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Cosmos DB에서는 Bw-Tree가 Foward Index 및 Inverted Index 모두 지원합니다. Inverted Index에서는 경로를 키로 가지고 리프 노드에서 문서 번호를 리스트로 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3a92e86a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3a92e86a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_-9mOvezkI1Txxbxh5xkFw-990a39671466.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3a92e86a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_-9mOvezkI1Txxbxh5xkFw-990a39671466.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Bw-Tree는 B+Tree의 Lock-free 버전입니다. 익숙하지 않은 용어가 등장하면 글을 해석할 때 어려울 수 있기 때문에 B+Tree를 떠올리면서 읽으시면 이해가 더 수월할 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="-diskann을-bw-tree와-결합"&gt;② DiskANN을 Bw-Tree와 결합&lt;/h2&gt;
&lt;p&gt;이번 포스팅의 주인공 논문은 바로 현재 이 상태에서, 벡터 인덱스를 추가로 지원을 어떻게 했는가를 다루고 있습니다. 이를 활용해서 아래 쿼리를 지원할 수 있습니다. 사용방법이 꽤 간단합니다. &lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/vectordistance"&gt;VectorDistance&lt;/a&gt; 함수를 사용해서 가장 가까운 벡터를 찾겠다는 선언만 해주면 됩니다.&lt;/p&gt;
&lt;p&gt;SELECT TO&lt;br&gt;
P 10&lt;br&gt;
c.id,&lt;br&gt;
c.title,&lt;br&gt;
c.product,&lt;br&gt;
VectorDistance(c.contentVector, @query_vector) AS SimilarityScore&lt;br&gt;
FROM c&lt;br&gt;
WHERE c.category = &amp;ldquo;game&amp;rdquo;&lt;br&gt;
ORDER BY VectorDistance(c.contentVector, @query_vector);&lt;/p&gt;
&lt;p&gt;벡터 인덱스를 Bw-Tree에서 지원하는 가장 쉬운 방법은 테이블 ID와 경로를 키로 가지면서 리프노드에는 테이블 내에 있는 모든 문 서의 벡터들을 리스트 형태로 저장해두는 겁니다. 벡터 관련 쿼리가 들어오면 O(N²)으로 모든 벡터를 전수조사 하면서 계산해보면 됩니다. 당연히 이 방법은 현실적이지 않죠&lt;/p&gt;
&lt;p&gt;동일한 접근법으로 Product Quantization을 사용하면 데이터 압축 효과가 있기 때문에 성능을 다수 높일 수 있습니다. 논문에서는 &lt;strong&gt;OpenAI Ada v3 embedding&lt;/strong&gt; 모델을 96x나 압축 저장할 수 있다고 했습니다. 당연히 이 방법도 벡터 개수가 천만개 단위를 넘어가면 비현실적인 방법입니다.&lt;/p&gt;
&lt;p&gt;지금까지 설명한 배경을 바탕으로, Cosmos DB의 벡터 인덱스 구현 핵심은 아래 그림으로 요약할 수 있습니다. 양자화된 벡터 값을 키로 가지면서 리프 노드에는 문서 ID를 적어 두는 것과 문서 ID를 키로 가지면서 그래프 사이의 인접리스트를 형성하도록 인덱스에기록해둡니다. 그리고 그래프 구축 및 탐색에는 DiskANN의 알고리즘을 이용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b4dcbe41"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b4dcbe41"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RKaB9SqX4g-meEQDjnsIDQ-7bb5507eaa6e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b4dcbe41"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RKaB9SqX4g-meEQDjnsIDQ-7bb5507eaa6e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 인덱스 레이아웃이 실제로는 B+ 트리에서 TermKey를 경로로 가지며 리프노드에 TermValue값을 저장한 상황을 상상하시면 됩니다. 첫 번째 Inverted Term인 (TermPrefix + DocumentId + Quantized Vector Value)의 경우는 단순히 양자화된 벡터값을 빠르게 찾기 위해서 사용하며 Value에는 의미없는 Dummy값을 저장합니다. B+ 트리에서 문서 ID로 양자화된 벡터값을 찾을 때 “Prefix Seek” API를 사용해서 찾을 수 있다고 합니다.&lt;/p&gt;
&lt;p&gt;두 번째 Forward Term에 해당하는 (TermPrefix + Document ID)를 키로 가지는 값은 해당 문서의 이웃 정점인 인접 리스트를 저장해서 그래프 정보를 가지고 있습니다. 바로 이 정보를 활용해서 DiskANN의 GreedySearch를 수행합니다.&lt;/p&gt;
&lt;h3 id="-minibatchinsert"&gt;③ MiniBatchInsert&lt;/h3&gt;
&lt;p&gt;디테일한 부분에서는 DiskANN과 조금씩 다릅니다. 새 정점을 그래프에 삽입할 때 Insert 요청을 모아 배치로 처리하고, RobustPrune까지 병렬로 수행한 뒤 결과를 일괄 업데이트합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c4629546"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c4629546"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ltit1-4VI3PAwO1c2FIPmw-5d101755ea3c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c4629546"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ltit1-4VI3PAwO1c2FIPmw-5d101755ea3c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-re-rank"&gt;④ Re-rank&lt;/h3&gt;
&lt;p&gt;쿼리를 수행할 때 quantizedVectorListMultiplier를 기입할 수 있습니다. 양자화된 벡터는 압축 표현이기 때문에 양자화된 벡터로 Greedy Search를 수행하면 정확도가 떨어집니다. 그래서 실제로는 일부러 원하는 것 이상으로 많이 후보군을 선정한 다음 &lt;strong&gt;원본 벡터(full precision vector)로 다시 계산해서 순위를 재-정렬합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c0db6194"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c0db6194"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dk2jJeR9d-_tfPVb0_b3tA-2b09e5e96dd2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c0db6194"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dk2jJeR9d-_tfPVb0_b3tA-2b09e5e96dd2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-36d6a88b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-36d6a88b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EhtTedzrR9ty3SGDDED6tg-990a7d5245b7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-36d6a88b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EhtTedzrR9ty3SGDDED6tg-990a7d5245b7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-filter-aware-search"&gt;⑤ Filter-Aware Search&lt;/h3&gt;
&lt;p&gt;애플리케이션에서는 다음 처럼 predicate를 포함하는 검색을 수행하는 경우가 흔합니다. 이런 경우에는 Filter-Aware 탐색을 제안하는데요. 비트맵 인덱스를 사용해서 서로 비교하는 전략을 사용합니다. 아래 알고리즘에서는 레이블 비트맵이 서로 동일한 경우 $0 &amp;lt; \beta \le 1$ 의 값을 곱해서 해당 정점에 어드밴티지를 더합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a44379c3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a44379c3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-XfPxCVGAgV99GgYZjBjZGg-8623b6177939.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a44379c3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-XfPxCVGAgV99GgYZjBjZGg-8623b6177939.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="평가-및-마무리"&gt;평가 및 마무리&lt;/h2&gt;
&lt;p&gt;앞서 GreedySearch에 대해 설명할 때 아래 평가자료를 사용했었는데요. 해당 그림은 Cosmos DB 논문[1]에서 인용한 그림이었습니다. 1000만개의 벡터를 저장하고 L값을 증가시키면서 레이턴시와 컴퓨팅 사용량의 변화를 관측했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-59af6aa8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-59af6aa8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-q-qZVn6jodn3zLm1apuADw-d803a33b4034.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-59af6aa8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-q-qZVn6jodn3zLm1apuADw-d803a33b4034.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;다음 두 그래프는 서로 다른 데이터셋을 활용해서 데이터를 100K, 1M, 10M 증가시켰을 때의 성능 변화 추이를 보여줍니다. L은 100으로 동일합니다. 데이터가 증가하면 레이턴시가 증가하긴 하지만 100K와 10M을 비교했을 때, 데이터는 100배 증가해도 레이턴시는 2배까지는 증가하지 않았다고 평가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-095907bb"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-095907bb"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YAnMbcGnmG7Gvyujq7bWBQ-07526b17f279.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-095907bb"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YAnMbcGnmG7Gvyujq7bWBQ-07526b17f279.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Zilliz, Pinecone 등 다른 서비스와 비용에 대해서 평가한 자료도 있습니다. CosmosDB가 쿼리 비용이 훨씬 저렴한 것으로 나오는데요. 알고리즘의 차이가 있다기 보다는 서버리스 제품이라서 그런걸까? 하는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5ed3e2b4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5ed3e2b4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-I1Dt0W5G7valbtu8XjGTAg-65ece59eda39.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5ed3e2b4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-I1Dt0W5G7valbtu8XjGTAg-65ece59eda39.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Milvus, Zilliz와 같은 전용 벡터 데이터베이스는 뛰어난 성능을 제공 하긴 하지만 운영 DB는 그대로 두고 전용 벡터 데이터베이스를 구축해야 하므로, 시스템 아키텍처가 복잡해지고 비용이 증가합니다.&lt;/p&gt;
&lt;p&gt;반면 Cosmos DB는 스케일 아웃 및 글로벌 복제, Schema Agnostic Indexing 등을 제공하면서 벡터 검색을 지원하기 때문에 서비스 운영에 필요한 데이터와 같이 통합해서 사용할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;Azure를 사용하지 않는 개발 환경이라면, PostgreSQL의 벡터 검색 성능을 높이기 위해 &lt;a href="https://github.com/timescale/pgvectorscale"&gt;timescale/pgvectorscale (Github 2.2k)&lt;/a&gt;확장 플러그인을 활용하는 것을 고려해볼 수 있습니다. 이 플러그인은 논문과 마찬가지로 DiskANN을 사용하여 PostgreSQL 데이터베이스에 고성능 벡터 인덱스를 지원합니다&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://arxiv.org/abs/2505.05885"&gt;Cost-Effective, Low Latency Vector Search with Azure Cosmos DB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://dl.acm.org/doi/abs/10.5555/3454287.3455520"&gt;DiskANN: fast accurate billion-point nearest neighbor search on a single node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.pinecone.io/learn/series/faiss/product-quantization/"&gt;Pinecone — Product Quantization: Compressing high-dimensional vectors by 97%&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://azure.microsoft.com/en-us/blog/azure-cosmos-db-database-for-intelligent-cloud-intelligent-edge-era/"&gt;Azure Cosmos DB — database for Intelligent Cloud — Intelligent Edge era&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://www.vldb.org/pvldb/vol8/p1668-shukla.pdf"&gt;Schema-Agnostic Indexing with Azure DocumentDB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://www.microsoft.com/en-us/research/publication/the-bw-tree-a-b-tree-for-new-hardware/"&gt;The Bw-Tree: A B-tree for New Hardware Platforms&lt;/a&gt; — Microsoft&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://dl.acm.org/doi/10.1145/3183713.3196895"&gt;Building a Bw-Tree Takes More Than Just Buzz Words — SIGMOD’18&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>비전 언어 모델은 보지 않는다?</title><link>https://scalalang2.com/p/%EB%B9%84%EC%A0%84-%EC%96%B8%EC%96%B4-%EB%AA%A8%EB%8D%B8%EC%9D%80-%EB%B3%B4%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-ca598163a09a/</link><pubDate>Tue, 10 Jun 2025 11:36:46 +0000</pubDate><guid>https://scalalang2.com/p/%EB%B9%84%EC%A0%84-%EC%96%B8%EC%96%B4-%EB%AA%A8%EB%8D%B8%EC%9D%80-%EB%B3%B4%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-ca598163a09a/</guid><description>&lt;p&gt;KAIST와 윌리엄 &amp;amp; 메리 대학 공동 연구진이 “비전 언어 모델(VLM)은 이미지를 ‘보는’ 것이 아니라, 편견에 기반해 암기된 지식을 활용할 뿐이다”이라는 과감한 제목을 가진 논문이 최근 발표했습니다[1][2].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;VLMs don’t actually “see” — they rely on memorized knowledge instead of visual analysis due to bias&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이는 2024년 ‘VLMs are blind(VLM은 시각장애를 가지고 있다)[3]’라는 연구를 통해 이미 VLM의 한계를 지적했던 윌리엄 &amp;amp; 메리 연구진의 후속 연구이기도 합니다. 이들의 주장은 VLM의 성능과 신뢰성에 대한 근본적인 질문을 던집니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 해당 연구의 핵심 내용과, 만약 이 주장이 사실일 경우 우리에게 시사하는 바가 무엇인지 생각해 보는 시간을 가져봅니다. 비전공자 시선에서 공부한 내용이기 때문에 혹시나 틀린 내용이 있다면 댓글로 피드백 부탁드립니다.&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;비전 언어 모델 (VLM : Vision Language Model)&lt;/li&gt;
&lt;li&gt;CLIP : 문장과 이미지의 연결&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;h2 id="비전-언어-모델-vlm--vision-language-model"&gt;비전 언어 모델 (VLM : Vision Language Model)&lt;/h2&gt;
&lt;p&gt;비전 언어 모델은 &lt;strong&gt;대규모 언어 모델(LLM)&lt;/strong&gt;과 &lt;strong&gt;시각적 인코더(Visual Encoder)&lt;/strong&gt;를 결합해서 사용하는 멀티-모달 AI 시스템입니다. 대표적으로 Stable Diffusion 같은 텍스트-이미지 생성 모델이 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;멀티-모달 AI&lt;/strong&gt;
사람은 사과라는 객체를 시각, 미각, 촉각, 언어적 정보를 통합해서 인식합니다. 이처럼 언어 뿐 아니라 음성, 영상, 이미지 등 여러 감각정보를 활용하도록 설계된 AI를 멀티-모달 AI라고 부릅니다[4].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI가 시각 정보를 활용할 수 있게 되면 다양한 분야에 활용할 수있습니다. 예를 들어, 이미지 기반 질문 답변(VQA), DALLE와 Stable Diffusion 같은 이미지 생성 모델, 구글의 VEO 2와 같은 영상 생성 모델이 대표적입니다. 국내 사례로는 &lt;a href="https://tv.naver.com/v/33860378"&gt;네이버에서 패션 상품 검색에 AI를 활용한 사례&lt;/a&gt;를 소개한 바 있습니다.&lt;/p&gt;
&lt;p&gt;아래 그림은 멀티-모달 모델의 능력을 평가하는데 사용되는 MMMU Benchmark[5]의 예시 그림입니다. 과학, 의료, 엔지니어링, 예술 분야에서 전문가 수준의 질문과 연관된 그림을 주었을 때 이를 얼마나 잘 수행하는지 평가합니다. 아직까지는 인간 전문가의 능력이 가장 높지만 멀티-모달 모델이 발전하면서 곧 추월할 것으로 예상됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-305e7f02"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-305e7f02"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3w6p44OsHmsSjU0xJYF5VA-d867b6c39947.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-305e7f02"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3w6p44OsHmsSjU0xJYF5VA-d867b6c39947.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mmmu-benchmark.github.io/"&gt;MMMU Benchmark&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;하나 재밌는 사례를 더 소개하자면 한시간 분량이 넘는 영상 파일을 업로드하면 10여개의 쇼츠를 자동으로 만들어주는 &lt;a href="https://www.opus.pro/"&gt;OpusClip&lt;/a&gt;이 500만명의 유저를 모으며 올 해 소프트뱅크로부터 290억원 투자를 받기도 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ccbea809"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ccbea809"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3fRjsUYP8BsXQYdLgg3K_A-aba5022aa331.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ccbea809"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3fRjsUYP8BsXQYdLgg3K_A-aba5022aa331.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.opus.pro/"&gt;OpusClip 소개&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="clip--문장과-이미지의-연결"&gt;CLIP : 문장과 이미지의 연결&lt;/h2&gt;
&lt;p&gt;VLM을 이야기 할 때 빼놓을 수 없는 대표적인 연구인 &lt;a href="https://arxiv.org/abs/2103.00020"&gt;CLIP&lt;/a&gt;은 2021년 OpenAI에서 발표된 논문으로, 시각적 정보와 언어적 정보를 효과적으로 연결하여 학습하는 핵심 컴포넌트입니다. CLIP 자체는 이미지에서 문장을 추출하는 모델이지만 여기서 학습된 인코더가 이미지 생성형 AI 혹은 LVLM에 자주 사용됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-18577a7d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-18577a7d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-V1liC_u9ukra6qrQOSpKEQ-5f41d2aa69e2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-18577a7d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-V1liC_u9ukra6qrQOSpKEQ-5f41d2aa69e2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cdn.openai.com/papers/dall-e-2.pdf"&gt;OpenAI : Hierarchical Text-Conditional Image Generation with CLIP Latents&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;CLIP은 이번 포스팅의 주제와 직접적으로 깊은 관련은 없지만, VLM을 설명할 때 자주 언급되는 개념이기 때문에 함께 정리했습니다. CLIP은 논문을 직접 읽지는 않았고 조회수 15만에 달하는 어느 유튜브 영상을 참고했습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/T9XSU0pKX2E?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;CLIP에서는 두 가지 사실만 기억하고 계시면 됩니다. 이게 ‘왜 동작하나요?’라는 질문은 뒤로 한채 어떻게 동작하는지만 이해하려고 한다면 꽤 단순합니다.&lt;/p&gt;
&lt;h3 id="-natural-language-supervision"&gt;① Natural Language Supervision&lt;/h3&gt;
&lt;p&gt;CLIP의 가장 큰 특징 중 하나는 인터넷에 존재하는 이미지와 문장 쌍을 대규모로 수집해 학습했다는 점입니다. 최근에는 레딧이 앤트로픽을 고소하는 등, AI 기업들이 저작권 분쟁에 자주 휘말리는 걸 보면 인터넷에서 데이터를 확보하는 것이 당연해 보이지만, CLIP이 발표될 당시만 해도 주로 ImageNet과 같은 공개 데이터셋을 활용해 모델 자체를 개선하는 연구가 많았습니다.&lt;/p&gt;
&lt;p&gt;마치 Transformer가 구글에서 처음 연구되었지만, 대규모 모델 확장과 상업적 성공은 OpenAI의 ChatGPT가 이룬 것처럼, 이미지 분야에서도 인터넷에 존재하는 방대한 데이터를 활용해 대규모 이미지 학습을 시도하는 움직임이 나타났습니다. 이를 Natural Language Supervision이라고 부르며, 과거에는 데이터 라벨러를 고용해 이미지를 일일이 수작업으로 라벨링했지만, CLIP에서는 인터넷에서 이미지와 함께 제공되는 각주를 라벨로 활용했습니다.&lt;/p&gt;
&lt;h3 id="-clip-contrastive-language-image-pre-training"&gt;② CLIP (Contrastive Language-Image Pre-training)&lt;/h3&gt;
&lt;p&gt;CLIP은 인터넷에서 수집한 4억 쌍의 (이미지, 텍스트) 데이터를 활용하여 이미지와 텍스트 인코더를 각각 학습합니다. 학습 과정에서 각 배치의 모든 이미지-텍스트 임베딩 쌍에 대해 코사인 유사도를 계산하고, 정답에 해당하는 대각 성분의 유사도는 최대화, 대각 성분을 제외한 나머지 오답 쌍의 유사도는 최소화하는 방향으로 최적화합니다.&lt;/p&gt;
&lt;p&gt;이렇게 학습된 모델은 이미지와 텍스트를 동일한 임베딩 공간에 위치시키며, 새로운 이미지에 대해 여러 라벨(텍스트)과의 유사도를 비교해 가장 적합한 라벨을 자동으로 할당할 수 있습니다. 이로써 CLIP은 별도의 추가 학습 없이도 다양한 이미지-텍스트 연관 태스크에 zero-shot으로 활용될 수 있습니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-98be1615"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-98be1615"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-6rsf9jFQXz5FwsVsALh25Q-81fe6c90607a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-98be1615"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-6rsf9jFQXz5FwsVsALh25Q-81fe6c90607a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;CLIP 학습 전략과 추론 단계에서의 활용&lt;/p&gt;
&lt;h2 id="비전-언어-모델은-보지-않는다"&gt;비전 언어 모델은 보지 않는다&lt;/h2&gt;
&lt;p&gt;앤트로픽이 2024년 6월에 공개한 Claude 제품 설명서[6]의 [Section 5.3] 에서 시각적 능력에 대해 언급하고 있습니다. 아래 그림은 다양한 시각 능력 평가 벤치마크 결과와, 통계 데이터가 이미지로 제공된 상황에서 G7 국가들의 인터넷 사용 연령층 차이를 추출하는 예시를 보여줍니다. 이를 통해 Claude를 비롯한 여러 AI가 이미지를 높은 수준으로 분석할 수 있음을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-314a314b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-314a314b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RthnAessyeZ5mALMWCsDPA-67afb4c72e1d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-314a314b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RthnAessyeZ5mALMWCsDPA-67afb4c72e1d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;시각 능력을 보여주는 예시&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-680e9bbd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-680e9bbd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xV7e2RREj0zfUZd6AwFeUw-1628902813cf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-680e9bbd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xV7e2RREj0zfUZd6AwFeUw-1628902813cf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;다양한 시각 능력 평가 벤치마크 결과&lt;/p&gt;
&lt;p&gt;하지만, 연구자들은 VLM들이 이미지를 높은 수준으로 분석할 수 있는 능력을 갖추고 있음에도 불구하고, 실제로는 매우 단순하고 명확해 보이는 태스크에서 종종 오류를 보이는 것을 발견했습니다.&lt;/p&gt;
&lt;p&gt;여기 다소 기괴한 사진이 주어져 있습니다. 이 글을 읽고 있는 여러분은 아래 사진 속 동물이 몇 개의 다리를 가지고 있다고 보이시나? 아마 다들 어렵지 않게 대상이 3개의 다리를 가진 것을 발견하셨을 겁니다. 하지만 이를 여러 AI에게 질의하면 아마 높은 확률로 3개를 정확히 짚어내지 못합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c0ab8942"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c0ab8942"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8ywwPzbWWk1qTAg2PTD8JQ-82b5eb90422b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c0ab8942"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8ywwPzbWWk1qTAg2PTD8JQ-82b5eb90422b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3개의 다리를 가진 닭이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6560e8ae"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6560e8ae"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_t7i41P-jqnuGuXbzFhVIA-3ec6b2c1f9b2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6560e8ae"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_t7i41P-jqnuGuXbzFhVIA-3ec6b2c1f9b2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Gemini 2.5 Pro를 통해서 질의해본 결과이다. 모델을 바꿔도 정확한 답변을 하지 못한다.&lt;/p&gt;
&lt;h3 id="-vlms-are-blind"&gt;① VLMs are blind&lt;/h3&gt;
&lt;p&gt;VLMs are blind는 지식에 거의 의존하지 않는 시각적 능력을 평가하기 위한 테스트를, VLMs are biased는 시각 정보 내에서 특정 지식을 발견할 때 편향된 답변을 하게 된다는 것을 보여주기 위한 벤치마크 데이터를 각각 제시하여 두 연구 모두 VLM의 한계를 규명하려는 시도를 보여줍니다.&lt;/p&gt;
&lt;p&gt;두 논문 모두 VLM의 성능을 평가하기 위한 벤치마크를 만드는데 중점을 두고 있기 때문에 왜 이런 현상이 발생했는지는 자세히 규명하지 않았습니다. 우선 여기서는 두 논문에서 제시하는 벤치마크 예시들을 살펴보겠습니다. 먼저 VLMs are blind 에서는 사전 지식을 요구하지 않고 오직 시각적 능력만 요구하는 7가지 태스크를 분류했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d18f4de3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d18f4de3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0KzVnGjGdvJTTjxvE9vx9g-6000916b384a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d18f4de3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0KzVnGjGdvJTTjxvE9vx9g-6000916b384a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;examples of VLMs are blind&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1. Counting line intersections :&lt;/strong&gt; 많은 VLM들이 AI2D, ChartQA 벤치마크 자료와 같이 다이어그램이나 차트 데이터를 분석할 수 있는데요. 그렇다면 임의의 선분이 교차하는 지를 잘 판별할 수 있다고 해석하는게 무리는 아닐 겁니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2. Two circles&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3. The circled letter&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4. Counting overlapping shapes :&lt;/strong&gt; 마이크로소프트에서 GPT-4V의 능력을 광범위하게 평가한 논문을 보면 사물의 개수를 잘 세는 것을 볼 수 있습니다. VLMs are blind 논문에서는 만약 VLM이 난시를 가지고 있다면 올림픽 심볼처럼 원을 가까이 겹쳐두면 제대로 카운팅 할 수 없다고 가정했는데요. 이 사실은 추후 VLMs are biased 논문에서 난시가 문제가 아니라 편향된 답변임이 밝혀집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5. Counting the nested squares&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;6. Counting the rows and columns of a grid&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7. Following single-colored paths&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;논문에서 제시하는 태스크는 모형을 사용한 추상적인 지적 테스트에 국한된 것 처럼 보이지만 이를 확장하면 실생활 문제와 연관될 수 있습니다. 아래는 논문 Appendix에 실린 예시 중 하나입니다. (공교롭게도 한국 지하철 노선도가 그 예시로 뽑혔네요)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e5e0067a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e5e0067a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YwHXoV4pFaPD7kDVAAtT_g-32ef74835d5f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e5e0067a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YwHXoV4pFaPD7kDVAAtT_g-32ef74835d5f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아마 연구자들은 모델이 난시(blurry vision)를 가지고 있다고 가정해서 난시가 해결하기 어려울 것 같은 태스크들을 준비한 것 같지만 “VLMs are blind” 라는 제목은 다소 과장된 표현이라고 느껴졌습니다. 실제로 VLM은 글자를 인식하는 &lt;a href="https://www.docvqa.org/docvqa"&gt;OCR 능력&lt;/a&gt;은 뛰어나기 때문에, 난시라고 보기에는 무리가 있다고 생각하며 연구자들이 제목을 고치거나 혹은 이 가설을 입증하기 위한 노력을 했어야 한다고 봅니다.&lt;/p&gt;
&lt;p&gt;오히려 이미지를 인코딩하여 벡터로 변환하는 과정 자체가 도형이나 구조적 정보를 처리하는 데 적합하지 않은 것이 아닌가 하는 의문이 들었는데요.&lt;/p&gt;
&lt;h3 id="-vlms-are-biased"&gt;② VLMs are biased&lt;/h3&gt;
&lt;p&gt;이런 의문들을 뒤로 한채 이번에는 VLMs are biased 논문에서 주장한 내용을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ae8fa1d8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ae8fa1d8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wGsyw3QRlXC526-BH66ofQ-a6715e1f4b11.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ae8fa1d8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wGsyw3QRlXC526-BH66ofQ-a6715e1f4b11.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;VLMs are biased&lt;/p&gt;
&lt;p&gt;평가 대상인 LVLM중에서 OpenAI의 o3모델은 추론 모델인데 이미지를 확대해서 보거나, 잘라서(Crop), 회전해서도 보는 등 부가 능력을 갖춘 모델입니다. 그럼에도 불구하고 논문에서 제시하는 다음 4개의 태스크에서는 상당히 많이 실패하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Counting animal legs when an extra leg is added&lt;/li&gt;
&lt;li&gt;Counting visual elements in modified familiar patterns (logos, flags, ches pieces and game boards)&lt;/li&gt;
&lt;li&gt;Visual testing on original and modified optical illusion patterns&lt;/li&gt;
&lt;li&gt;Counting the circles or lines in an anomaly cell among a patterned grid&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;논문에서 주장하는 바에 따르면 익숙한 동물인 닭에 다리를 하나 더 붙이거나, 유럽기에서 별을 하나 빼거나, 아디다스 3선 신발에 선을 하나 더 추가하는 등 우리가 잘 아는 사물에 약간의 변형을 가하면, &lt;strong&gt;모델은 대상을 정확히 관찰하지 않고 암기된 답변을 내놓는 경향이 있습니다.&lt;/strong&gt; 이로 인해 대부분의 모델에서 성능이 크게 저하되는 현상이 나타납니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1a8fa21d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1a8fa21d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aQs728NeAHOJ4ks-tu3GPg-a1405865ab69.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1a8fa21d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aQs728NeAHOJ4ks-tu3GPg-a1405865ab69.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;VLMs are biased&lt;/p&gt;
&lt;p&gt;아래 실험 결과는 모델이 &lt;strong&gt;암기된&lt;/strong&gt; 편향된 답변을 많이 한다는 주장에 힘을 실어줍니다. 대부분의 모델이 사실과 다른 이미지에 대해서 편향과 정렬된 답변을 했습니다. 즉, 강아지의 다리는 4개라는게 자명한 사실이기 때문에 오답으로 6개라고 답변하지는 않는다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-15c14f15"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-15c14f15"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-06y9_HIqde73l0Icz264TA-da246434d3f1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-15c14f15"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-06y9_HIqde73l0Icz264TA-da246434d3f1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;연구자들은 CoT(Chain of Thought)처럼 프롬프트를 보강해 정답률을 높일 수 있는지 확인했습니다. 이를 위해 Double-Check와 Debiased-Prompt와 같은 기법을 적용했으며, 예를 들어 “두 번 확인해라”, “사전 지식을 활용하지 말아라”와 같은 지시어를 프롬프트에 추가했습니다. 전체 데이터셋에서는 약 3~4%의 성능 향상이 있었으나, Baseline 성능 자체가 매우 낮아 이 정도의 상승을 유의미하다고 보기는 어렵습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ffe11e46"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ffe11e46"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JAE-2ArFHzsfMYQ-7xw_vQ-25a09276c566.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ffe11e46"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JAE-2ArFHzsfMYQ-7xw_vQ-25a09276c566.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9b92d865"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9b92d865"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_RTETM0A1cviwvJMtmXD2g-1782e7e6e47b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9b92d865"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_RTETM0A1cviwvJMtmXD2g-1782e7e6e47b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="논쟁들"&gt;논쟁들&lt;/h2&gt;
&lt;p&gt;아카이브(arXiv)에 게재된 지 일주일 정도 된 이 논문은 해커뉴스(Hacker News)에서 100여 개의 댓글이 달리며 활발한 토론이 이루어졌습니다. 댓글을 천천히 읽던 중 알아두면 도움이 될 만한 의견들이 몇 가지 보여서 몇 가지 소개드립니다.&lt;/p&gt;
&lt;h3 id="-점화-효과-priming-effect"&gt;① 점화 효과 (priming effect)&lt;/h3&gt;
&lt;p&gt;일부 사람들은 이런 현상이 나타나는 원인을 프라이밍 효과와 연관지어 설명하기도 합니다. 프라이밍 효과는 먼저 처리한 정보에 의해 떠오른 개념이 뒤에 이어지는 정보의 해석에 영향을 미치는 현상입니다. 점화 효과는 하나의 개념이 떠오르면 이어진 정보에 대한 해석이 빠른 경우를 의미하고 이에 대한 반대 효과로 스트루프 효과가 있습니다. 스트루프 효과는 두 가지 정보가 서로 상충되는 경우 정보 해석이 늦어지는 현상을 말합니다.&lt;/p&gt;
&lt;p&gt;아래 그림을 통해 직접 체험해볼 수 있습니다. 왼쪽 그림에서는 순서대로 보이는 색상을 말하기 편한 반면 오른쪽 그림에서는 단어 정보가 먼저 뇌에 입력되기 때문에 정보 해석이 상대적으로 늦어집니다. 과거 2차 세계대전에서는 이 방식을 통해 스파이를 색출했다는 루머가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-43f9fe08"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-43f9fe08"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QGrRUieZHZu91HvEyr4JCQ-6a40956960e4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-43f9fe08"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QGrRUieZHZu91HvEyr4JCQ-6a40956960e4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Stroop Effect&lt;/p&gt;
&lt;p&gt;사람이 겪는 이런 현상처럼, VLM 또한 사진 속에서 특정 동물을 특정지은 다음에는 해당 사물이 가지는 일반적인 특성을 말한다는 점에서 유사한 면이 있습니다. 아니면 누군가 말한 것 처럼 LLM이 그저 확률적 앵무새[8]라서 닭은 다리가 2개라는 학습된 단어를 반복해서 말하는 걸까요&lt;/p&gt;
&lt;h3 id="-편향은-자연스러운-것이다"&gt;② 편향은 자연스러운 것이다&lt;/h3&gt;
&lt;p&gt;thomastjeffery는 모델은 논리적으로 추론하는 것이 아니라, 주어진 데이터 셋 내에서 프롬프트와 가장 유사한 통계적 관계를 보여주는 편향의 집합체일 뿐이라고 정의했습니다. 따라서 모델이 사물을 실제로 보고 이해하는 게 아니라는 논문의 핵심 주장에 힘을 실었습니다.&lt;/p&gt;
&lt;h3 id="-함정-질문"&gt;③ 함정 질문&lt;/h3&gt;
&lt;p&gt;jbay808은 VLM이 지능 수준을 묻는 일반적인 태스크에서 높은 정답률을 보인다는 사실만으로, 실제로 이미지를 ‘보지 않는다’고 주장하는 것은 논리적 비약이라고 지적합니다. 또한, “이 사진 속 강아지의 다리가 몇 개일까요?”라는 질문을 받으면 사람은 함정이 있을 수 있음을 인지하고 실제로 다리를 세어보지만, 버스 광고에 다리가 5개 달린 강아지가 그려져 있어도 대부분의 사람은 그 미세한 변화를 쉽게 알아차리지 못할 것이라고 말합니다. 그러나 사람이 그 변화를 알아차리지 못했다고 해서, 그 이미지를 ‘보지 않았다’고 단정할 수는 없습니다.&lt;/p&gt;
&lt;p&gt;이 댓글을 보고 혹시나 해서 프롬프트에 &lt;a href="https://www.perplexity.ai/search/how-many-legs-does-this-animal-lu3ipBXWQIerfn9HovbAfw#1"&gt;“이게 함정 질문일 수 있고 이 동물은 평소보다 추가적인 다리를 가지고 있을 수도 있어”&lt;/a&gt;라는 문장을 추가해서 Gemini 2.5 Pro에 넣어봤지만 여전히 정답은 2개라고 말했습니다. 또한 VLM이 사람과 비슷한 양상을 보인다고 해서 그게 좋다는 건 아닙니다. 우리는 사람과 비슷한 사고 수준을 가진 AI를 원하는게 아니라 인간보다 더 뛰어난 존재가 필요하니까요.&lt;/p&gt;
&lt;h3 id="-vlm의-근본적인-한계점이-아니다"&gt;④ VLM의 근본적인 한계점이 아니다&lt;/h3&gt;
&lt;p&gt;CLIP을 설명하면서 이야기 한 것 처럼 인터넷 세상에서는 반사실적(counter-factual) 학습 데이터가 충분하지 않기 때문에 발생한 문제이지, 학습 데이터에 반사실적 예시를 포함시키면 된다는 주장이 있습니다. 즉, VLM 자체의 근본적인 한계점이 아니라 학습 데이터가 충분하지 않다는 주장입니다.&lt;/p&gt;
&lt;p&gt;하지만 이런 주장에 대해 반론을 제시할 수 있는 논문이 있습니다. NIPS 2024에 발표된 연구[9]에서는 VLM의 실패가 인지 과학의 ‘&lt;strong&gt;결합 문제(binding problem)&lt;/strong&gt;’와 관련이 있다고 설명합니다. 결합 문제란 색상, 모양, 위치, 방향, 질감 등 서로 다른 자극들이 왜 하나의 단일한 상으로 구상되는지에 대한 문제입니다. 이런 요소들은 서로 떨어진 뉴런을 통해 지각한 후에 이것이 우리 뇌에서 통합되어서 하나의 완전한 상으로 귀결되는데요. 이유는 잘 모르지만 사람은 이를 잘 수행해내는 한편 VLM은 이를 잘 결합하지 못하기 때문에 사람은 쉽게 할 수 있는 태스크에서 실패하곤 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-384485ac"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-384485ac"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PvcVRbVsw3P6KBhYtl0bHA-4a944adee9ca.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-384485ac"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PvcVRbVsw3P6KBhYtl0bHA-4a944adee9ca.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;VLM의 한계점을 제시하고 이를 해결하려고 시도하는 연구들이 종종 있습니다. 마이크로소프트는 GPT-4V에서 사물에 숫자만 부여해도 성능이 크게 증가한다는 연구[9]를 2023년에 제시했습니다. 하지만 이후 발표된 최근 연구들은 대부분 2~3% 수준의 점진적인 성능 개선을 보이는 데 그치고 있으며, VLM의 근본적인 패러다임을 바꿀 만한 성과는 찾지는 못했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3aaf047e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3aaf047e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--CAkLxqAz1KkwKptWONx3g-a74eb9f00f67.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3aaf047e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--CAkLxqAz1KkwKptWONx3g-a74eb9f00f67.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;오늘은 현재 VLM이 가진 근본적인 한계점을 설명하는 몇 개의 연구를 다뤄봤는데요. 실없게도 가장 먼저 든 생각은 CAPTCHA 시스템을 만들 때 AI를 방어하기 위해 이런 한계점을 이용해볼 수 있겠다는 생각이 들었습니다. 그래도 언젠가는 인류가 정복하지 않을까 해서, 앞으로 어떤 연구가 이런 문제점을 해결해 줄지 지켜보는 재미가 있을 것 같습니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://arxiv.org/abs/2505.23941"&gt;Vision Language Models are Biased&lt;/a&gt; (arxiv)&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://vlmsarebiased.github.io/"&gt;Vision Language Models are Biased (homepage)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://vlmsareblind.github.io/"&gt;Vision language models are blind (homepage)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://www.samsungsds.com/kr/insights/multi-modal-ai.html#:~:text=%EC%A7%80%EA%B8%88%EC%9D%98%20%27%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%8B%AC%27%EC%9D%80%20%EC%8B%9C%EA%B0%81%2C%20%EC%B2%AD%EA%B0%81%EC%9D%84%20%EB%B9%84%EB%A1%AF%ED%95%9C%20%EC%97%AC%EB%9F%AC%20%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC,%EB%B0%9B%EC%95%84%EB%93%A4%EC%97%AC%EC%84%9C%20%ED%95%99%EC%8A%B5%ED%95%98%EA%B3%A0%20%EC%82%AC%EA%B3%A0%ED%95%98%EB%8A%94%20AI%EB%A5%BC%20%27%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%8B%AC%20AI%27%EB%9D%BC%EA%B3%A0%20%ED%95%A9%EB%8B%88%EB%8B%A4."&gt;인간처럼 사고하는 멀티모달(Multi Modal) AI란?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://mmmu-benchmark.github.io/"&gt;A Massive Multi-discipline Multimodal Understanding and Reasoning Benchmark for Expert AGI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://assets.anthropic.com/m/61e7d27f8c8f5919/original/Claude-3-Model-Card.pdf"&gt;The Claude 3 Model Family: Opus, Sonnet, Haiku — Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://news.ycombinator.com/item?id=44169413"&gt;Hackernews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://s10251.pcdn.co/pdf/2021-bender-parrots.pdf"&gt;On the Dangers of Stochastic Parrots: Can Language Models Be Too Big?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://arxiv.org/abs/2411.00238"&gt;Understanding the Limits of Vision Language Models Through the Lens of the Binding Problem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>멀티-에이전트 LLM 코인 투자 팀 만들기</title><link>https://scalalang2.com/p/%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-llm-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%ED%88%AC%EC%9E%90-%ED%8C%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-1763549bcdf1/</link><pubDate>Sun, 18 May 2025 13:48:44 +0000</pubDate><guid>https://scalalang2.com/p/%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-llm-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%ED%88%AC%EC%9E%90-%ED%8C%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-1763549bcdf1/</guid><description>&lt;p&gt;세계 각지의 연구자 및 공학자들이 LLM을 활용해 다양한 시도를 하고 있습니다. 우리는, 인류는 이 기술로 무엇을 어디까지 할 수 있을까요? 이제는 ChatGPT를 통해 궁금증을 풀고, 창작의 영역까지 맡기는 일이 일상이 되었습니다. 그리고 최근, LLM에게 도구를 쥐어주고 직접 업무를 지시하는 에이전트 분야가 새로운 가능성을 제시하면서 많은 사람들의 시선이 모이고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/u/89ed5885f0af"&gt;Pangyoalto&lt;/a&gt;님의 글[1]을 통해 LLM 활용 분야 중 하나인 멀티-에이전트 LLM 패러다임을 접하게 되었습니다. 몇몇 논문과 정보를 살펴보면서 이를 직접 활용해보고자 &lt;a href="https://github.com/scalalang2/crypto-ai-hedge-fund"&gt;암호화폐 투자 팀(crypto-ai-hedge-fund / Github)&lt;/a&gt;을 만드는 개인 프로젝트를 시작했는데요. 이 글에서는 프로젝트를 진행하며 참고했던 논문(Trading Agent AAAI’25)[2]와 실제 개발한 내용에 대해 간단히 공유하고자 합니다.&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;멀티-에이전트 LLM의 등장&lt;/li&gt;
&lt;li&gt;트레이딩 팀 만들기&lt;/li&gt;
&lt;li&gt;구현하기&lt;/li&gt;
&lt;li&gt;논문과의 차이점&lt;/li&gt;
&lt;li&gt;평가&lt;/li&gt;
&lt;li&gt;마무리 및 추가 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="멀티-에이전트-llm의-등장"&gt;멀티-에이전트 LLM의 등장&lt;/h2&gt;
&lt;p&gt;지난 몇 년간 대규모 언어 모델(LLM)은 추론, 생성, 질의응답 등 다양한 태스크를 훌륭하게 수행했습니다. 하지만 컨텍스트가 긴 태스크에 대해서는 대표적으로 두 가지 한계점이 있습니다.&lt;/p&gt;
&lt;h3 id="-context-window"&gt;① Context-Window&lt;/h3&gt;
&lt;p&gt;Context-Window란 LLM의 입력 토큰의 길이 제한 입니다. GPT-4o 모델은 최대 125,000개의 토큰을 처리할 수 있고, API로만 접근 가능한 GPT-4.1 모델과 Gemini 2.5 Pro는 8x 높은 100만 자의 토큰을 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;문장 생성 과정 중에 컨텍스트 길이를 초과하면 에러를 반환하거나 윈도우 내에서 앞의 문장을 지워야 하는데요. 보통 유저가 사용하는 애플리케이션은 후자를 채택해서 윈도우 앞단의 토큰을 지웁니다. 하지만, 이 방식은 LLM이 과거 정보를 유실하기 때문에 정확한 답변을 하지 못하게 되는 문제가 있습니다.&lt;/p&gt;
&lt;h3 id="-lost-in-the-middle"&gt;② Lost-in-the-middle&lt;/h3&gt;
&lt;p&gt;스탠포드 및 UC 버클리 공동 연구진은 긴 컨텍스트(longer-context) 환경에서 LLM의 성능을 평가하는 실험을 진행했습니다. 연구팀은 GPT 모델에 질문을 제시하고 정답을 맞히는지 측정하는데요. 이때, 답변 도출에 필요한 문서와 관련 없는 문서를 함께 배열한 뒤, 정답 문서의 위치를 바꿔가며 GPT의 성능 변화를 관찰했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-19141819"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-19141819"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1QXUwcrwAlk9biwVWJ4G0w-4a246ab3eecb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-19141819"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1QXUwcrwAlk9biwVWJ4G0w-4a246ab3eecb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;실험 결과, 정답 문서가 입력 정보의 처음이나 마지막 부분에 위치할 때 정확도가 높게 나타났습니다. 반면, 정답 문서가 중간 부분에 있을 경우에는 정확한 답변을 하지 못하는 경향을 보였습니다. 이러한 현상을 Lost-in-the-middle[3] 문제 라고 합니다.&lt;/p&gt;
&lt;p&gt;GPT의 컨텍스트 윈도우가 무제한이여도 세상 모든 정보를 전달하고 답변을 요청하면 제대로 수행하지 못하겠죠. 이러한 한계 때문에 최근에는 사용자 입력이 들어왔을 때, 관련 문서를 검색 엔진에서 찾아 컨텍스트에 추가하여 처리하는 &lt;strong&gt;검색 증강 생성(RAG, Retrieval Augmented Generation)&lt;/strong&gt; 방식이 많이 활용되고 있습니다&lt;/p&gt;
&lt;h3 id="-masked-attention-구조의-특성"&gt;③ Masked Attention 구조의 특성&lt;/h3&gt;
&lt;p&gt;이 문제는 ② 번의 현상과도 연관성이 있습니다. Transformer(e.g. GPT, Gemini) 기반 LLM은 문장을 순차적으로 생성하는 자기회귀구조를 사용합니다. 이때 Masked Attention은 특성상 앞서 등장한 토큰들에만 어텐션을 할 수 있고 미래의 정보를 참조하지 못하게 제한해서 문장의 일관성과 흐름은 유지했지만, 앞서 등장한 정보와 뒤에 등장한 정보 간의 연결성은 제한되는 구조적 한계를 있습니다.&lt;/p&gt;
&lt;p&gt;최근에는 &lt;a href="https://arxiv.org/abs/2502.09992"&gt;확산 모델 LLM&lt;/a&gt; 이나 혹은 어텐션 매커니즘 자체를 보완하는 &lt;a href="https://ai.meta.com/research/publications/multi-token-attention/"&gt;Multi-Token Attention (Meta)&lt;/a&gt; 연구들이 있긴 하지만 당장 현시점에서는 아직 연구단계로 보여집니다.&lt;/p&gt;
&lt;h2 id="멀티-에이전트-llm의-등장-1"&gt;멀티-에이전트 LLM의 등장&lt;/h2&gt;
&lt;p&gt;이런 문제를 해결하기 위해 멀티-에이전트 LLM을 활용할 수 있습니다. 구글의 한 연구진은 Chain-of-Agent [4]를 통해 매니저가 여러 작업자(Worker)들에게 문서의 해석을 요청하고 본인이 내용을 취합해서 답변하면 앞서 서술한 문제점을 많이 완화할 수 있다는 것을 밝혀냈습니다.&lt;/p&gt;
&lt;p&gt;Large-Context 문제가 아니더라도, 여러 명의 LLM 에이전트가 토론, 정보 공유, 반성, 협상을 통해 문제를 해결하도록 하는 접근법은 그 자체로 매우 흥미로운 아이디어입니다. 스탠포드 연구진은 Smallvile이라는 가상의 마을에 에이전트를 배치해서 각각 일상생활을 영위하고, 서로 대화하며, 새로운 관계를 형성하는 가상의 인공사회를 만들었습니다[5].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0d26d904"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0d26d904"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BzfPRYQO-cQ3U6IQeBU5Bg-05c4c1a55402.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0d26d904"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BzfPRYQO-cQ3U6IQeBU5Bg-05c4c1a55402.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Generative Agents: Interactive Simulacra of Human Behavior&lt;/p&gt;
&lt;p&gt;MetaGPT[6]는 실제 소프트웨어 개발 집단의 구성원들(e.g. PM, 아키텍트, 엔지니어, QA) 구현해서 이를 상호작용 시킴으로써 다양한 소프트웨어를 만들 수 있음을 보여줬습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-24dc8e6b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-24dc8e6b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zvBjZlf92IhqKaypMx40IQ-cc49e8111c20.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-24dc8e6b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zvBjZlf92IhqKaypMx40IQ-cc49e8111c20.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;에이전트 간의 협력적 추론 과정은 단일 LLM이 가질 수 있는 편향을 줄이고,견고한 해결책을 도출하는 데 기여합니다. 여러 에이전트가 각자의 관점에서 정보를 분석하고 의견을 교환함으로써 마치 현실 세계의 전문가 팀처럼 집단 지성을 발휘하여 더 나은 의사결정을 내릴 수 있을지도 모릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;아직 멀티-에이전트가 단일 LLM보다 더 좋다고 최종 결론을 내릴 수 있을만한 사항은 아닌 것 같아서 마지막 문장을 애매하게 적었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="트레이딩-팀-만들기"&gt;트레이딩 팀 만들기&lt;/h2&gt;
&lt;p&gt;이처럼 LLM의 가능성과 한계를 살펴본 후, 실제로 이를 활용해보고 싶다는 생각이 들었습니다. 현업은 평범한 서버 개발자이기 때문에 개인적으로 기본 지식을 공부하면서 멀티-에이전트 LLM &lt;a href="https://github.com/scalalang2/crypto-ai-hedge-fund"&gt;암호화폐 투자 팀(crypto-ai-hedge-fund)&lt;/a&gt; 개발을 시작했습니다.&lt;/p&gt;
&lt;p&gt;처음에는 &lt;a href="https://github.com/virattt/ai-hedge-fund"&gt;ai-hedge-fund&lt;/a&gt; 프로젝트의 구조를 참고해서 각 전문가 에이전트의 의견을 모아서 최종 결정을 하는 방식으로 구현을 시작했고, 실제 구현한 뒤 2–3주 정도 100만원 정도를 운용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2f916615"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2f916615"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-mngr5Em3B4PxphvY-55acde45521d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2f916615"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-mngr5Em3B4PxphvY-55acde45521d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/virattt/ai-hedge-fund"&gt;https://github.com/virattt/ai-hedge-fund&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8503fca9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8503fca9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ZhfHtzkf1PuaFpnYM4CIvg-4f592ca17b34.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8503fca9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ZhfHtzkf1PuaFpnYM4CIvg-4f592ca17b34.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;트레이딩 봇 동작 결과&lt;/p&gt;
&lt;p&gt;때마침 시장 상황이 좋아 수익을 올릴 수 있었지만, 암호화폐의 경우 뉴스나 기술적 분석 외에는 가치를 평가할 수 있는 데이터가 부족하다고 느껴 전문가 에이전트의 확장에 한계를 경험했습니다. 게다가 저 역시 암호화폐 도메인에 대한 지식이 부족한 점도 영향을 미쳤습니다.&lt;/p&gt;
&lt;p&gt;이에 대한 대안으로 참고할 만한 자료를 찾던 중 TradingAgents(AAAI’25) 논문을 발견했고, 이를 바탕으로 구조를 다음과 같이 변경하게 되었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TradingAgents는 UCLA와 MIT 소속 연구자들이 공동으로 진행한 연구입니다. 실제로 학술적 가치가 높다고 보기는 어렵지만, 참고할 만한 자료가 많지 않았고 정량 평가 결과도 함께 제공되어 있어서 구조를 그대로 활용했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ff47185b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ff47185b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7p3JLQ7cwm0lcBuDm-L29w-7353e5a201a0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ff47185b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7p3JLQ7cwm0lcBuDm-L29w-7353e5a201a0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;암호화폐 투자 팀 구성&lt;/p&gt;
&lt;p&gt;위 그림에서 묘사된 팀 구성은 논문에서 제시한 구조와 동일합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Analyst Team :&lt;/strong&gt; 기술적 분석, 뉴스 분석, 감정 분석에 특화된 전문가 에이전트 3명이 본인에게 주어진 데이터를 해석하고 취합해서 이를 리서치 팀으로 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Research Team &lt;/strong&gt;: 각각 상승(Bullish) 및 하락(Bearish)에 더 무게를 두고 있는 두명의 전문가 에이전트가 주어진 정보를 바탕으로 N 라운드 동안 의견을 주고 받습니다. 토론 이후에 각각 매수 및 매도 의견을 낸 뒤, 이를 트레이더에게 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trader&lt;/strong&gt; : 분석팀과 리서치팀의 관점을 토대로 트레이딩 의사결정을 수행합니다. 정보와 자산을 평하고 정량적, 정성적 평가를 통해 최적화된 트레이딩 행위를 결정합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk Manager :&lt;/strong&gt; Trader의 결정에 대해 단일 거래의 포지션 크기를 제한하거나 전체 포트폴리오의 비중이 균등하도록 재조정 하는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;백본 모델은 처음에는 GPT-4o를 사용했었는데요. 환각증세가 너무 심해서 프롬프트를 이리저리 바꿔보다가 잘 해결이 안되더라구요. 그러다, 한 달 전에 출시된 &lt;a href="https://openai.com/index/gpt-4-1/"&gt;GPT-4.1&lt;/a&gt; 모델로 변경했더니 환각 증세의 많은 부분이 저절로 해결되었습니다.&lt;/p&gt;
&lt;h2 id="구현하기"&gt;구현하기&lt;/h2&gt;
&lt;h3 id="-한-달-예산-책정"&gt;① 한 달 예산 책정&lt;/h3&gt;
&lt;p&gt;저는 개인 프로젝트의 운영 비용을 반드시 월 10만 원 이하로 유지하는 것을 목표로 삼았습니다. 평범한 회사원으로서, 이 프로젝트는 취미이자 리서치 목적으로 시작한 것이기 때문에 수익에는 큰 기대를 두지 않고 있습니다.&lt;/p&gt;
&lt;p&gt;서버는 구글 클라우드(GCP)의 us-central 리전에서 e2-micro 인스턴스를 무료로 사용할 수 있기 때문에, IP 대여 및 네트워크 비용만 부담하면 운영비를 크게 절약할 수 있습니다. 어차피 API 호출만 주로 이루어지기 때문에 고성능 인스턴스가 필요하지도 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3568d419"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3568d419"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9oAlNT8mE6jkrIDqGRIKTA-797036326808.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3568d419"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9oAlNT8mE6jkrIDqGRIKTA-797036326808.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;문제는 API 비용인데요. 현재 GPT-4.1 모델의 사용 요금은 100만 토큰당 $2.00로 책정되어 있습니다. 100만 토큰이 책 5~6권 분량이라고 해서 처음에는 비용이 크게 들지 않을 것이라 생각했지만, 매일 운영하다 보니 금세 월 10만 원 예산을 초과하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0688f7d9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0688f7d9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gVjTdwwc_rBuTw_50BD9IQ-a41bef1a9b45.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0688f7d9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gVjTdwwc_rBuTw_50BD9IQ-a41bef1a9b45.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://openai.com/api/pricing/"&gt;https://openai.com/api/pricing/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그래서 실제 에이전트의 대화는 마지막 거래 이후 가격 변동이 2% 이상 발생했을 때만 실행되도록 규칙을 추가해, 불필요한 API 호출을 줄였습니다. 또한 JSON 데이터를 표 형태로 바꿔 입력 토큰 수를 줄이려 노력했습니다. 입력 토큰을 줄이는 것은 단순히 비용 절감뿐 아니라, 답변 품질에도 긍정적인 영향을 미칩니다.&lt;/p&gt;
&lt;h3 id="-사용한-api"&gt;② 사용한 API&lt;/h3&gt;
&lt;p&gt;뉴스 정보는 텔레그램 뉴스 채널들에서 수집합니다. 감정 분석에는 X(트위터)도 고려했는데요. X에는 프로젝트에 지나치게 유리하게 올라오는 글이 많아 서 실제 시장 감정과는 차이가 있다고 판단해 일단 보류했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-222048eb"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-222048eb"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Nlh0bDVWXR-YUOG7ae4ovw-d635de0725c2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-222048eb"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Nlh0bDVWXR-YUOG7ae4ovw-d635de0725c2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;텔레그램 채널들&lt;/p&gt;
&lt;p&gt;거래는 업비트 API를 이용했습니다. 마지막으로, OpenAI 모델을 사용하긴 했지만, AutoGen은 OpenAI뿐만 아니라 Gemini, Ollama 등 다양한 모델도 지원하므로 필요에 따라 쉽게 변경할 수 있습니다.&lt;/p&gt;
&lt;h3 id="-구현-언어"&gt;③ 구현 언어&lt;/h3&gt;
&lt;p&gt;AutoGen이 .NET 버전을 지원하기 때문에, 익숙한 C#을 사용해 구현했습니다. 처음에는 Python으로도 시도했는데 제가 Python의 타입 시스템을 제대로 활용하지 못해서 코드를 C#으로 다시 작성했습니다.&lt;/p&gt;
&lt;h2 id="논문과의-차이점"&gt;논문과의 차이점&lt;/h2&gt;
&lt;p&gt;제 구현이 논문과 다른 중요한 차이점은 ReAct[7] 프롬프트를 사용하지 않았다는 점입니다. ReAct는 LLM이 문제를 해결할 때 추론과 행동을 번갈아 수행하도록 유도하는 프롬프트 기법입니다.&lt;/p&gt;
&lt;p&gt;논문에서는 모든 에이전트가 ReAct 프롬프트를 사용한다고 명시되어 있습니다. 하지만 실제로 구현해보니, 에이전트에게 &lt;strong&gt;“데이터를 조회”&lt;/strong&gt;하는 행동을 부여하면 항상 사전에 데이터를 조회하는 행동을 반복했습니다. 아마도 정보가 없는 것보다 많은 것이 무조건 유리하다는 판단 때문이겠죠. 그래서 데이터를 미리 조회해서 입력 프롬프트로 넣어주는 방식과 실질적인 차이가 없다고 느껴, ReAct를 사용하는 것이 의미가 없다고 판단했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ReAct&lt;/strong&gt;
ReAct 논문에서는 요리 레시피를 수행하는 에이전트를 예시로 들었습니다. 예를 들어, 가상 요리 에이전트에게 레시피에 필요한 재료 중 하나가 없다는 상황을 주면, 에이전트는 스스로 선반을 &lt;strong&gt;“열어&lt;/strong&gt;” 대체 재료를 찾는 등의 행동을 선택합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;이처럼 ReAct 방식은 단순히 데이터를 조회하는 것과 달리, 에이전트가 상황에 따라 다양한 행동을 자율적으로 선택하고, 그 과정에서 창의적인 문제 해결을 시도한다는 점에서 차별성이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이것 외에는 논문에서 주장한 내용을 수용했는데요. 예를 들어 LLM들이 서로 대화할 때는 순수 자연어 대신 구조화된 포맷(e.g. Markdown, JSON)을 사용해야 효율이 좋다고 합니다. 이건 MetaGPT 에서도 동일하게 주장합니다.&lt;/p&gt;
&lt;h2 id="평가"&gt;평가&lt;/h2&gt;
&lt;p&gt;에이전트의 수행 능력을 측정하는 메트릭은 다음 4가지를 사용했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;누적 수익율 (CR, Cumulative Return)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연간 수익율 (AR, Annual Returns)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ko.wikipedia.org/wiki/%EC%83%A4%ED%94%84_%EB%B9%84%EC%9C%A8"&gt;샤프 비율&lt;/a&gt; &lt;strong&gt;(SR, Sharpe Ratio)&lt;/strong&gt; : (자산 수익률 — 무위험 수익률)의 기댓값을 자산 수익율의 표준편차로 나눈 값입니다. 샤프비율이 높다는 것은 투자에서 &lt;strong&gt;“위험 대비 수익 효율이 좋다”&lt;/strong&gt;는 의미를 가집니다. 즉, 동일한 변동성을 감수할 때 더 높은 초과수익을 얻고 있다는 뜻이며, Risk Manager가 포트폴리오를 배분할 때 이용할 수 있습니다. 무위험 수익율에는 일반적으로 국고채금리나 예금금리를 이용할 수 있는데요. 저는 암호화폐를 거래 대상으로 삼았기 때문에 비트코인의 수익율을 이용하기로 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;최대 낙폭 지수 (MDD, Maximum Draw Down)&lt;/strong&gt; : 내가 가진 포지션에서 전고점 대비 하락율을 나타내는 지표입니다. MDD가 적을수록 안정적인 투자를 하고 있다고 볼 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;저는 운용 기간이 짧아 자체 데이터로서의 가치가 없기 때문에, 논문에서 제시한 평가 자료를 소개합니다. 논문에서는 기술적 분석이나 Buy&amp;amp;Hold 전략보다 모든 성과 지표에서 좋은 결과를 얻었다고 주장하지만&lt;/p&gt;
&lt;p&gt;사실상 엔비디아를 대상으로 평가했다면 Buy&amp;amp;Hold 전략이 세상 모든 퀀트 투자 및 AI 트레이딩 전략을 가볍게 넘었을 거라서, 아래 데이터가 실제 투자 성과를 객관적으로 보여주는 자료는 아니라고 생각합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1012b3a1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1012b3a1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SUrCjKAYl7AcfysTBoEY3A-9450fb35bc36.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1012b3a1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SUrCjKAYl7AcfysTBoEY3A-9450fb35bc36.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/pdf/2412.20138"&gt;https://arxiv.org/pdf/2412.20138&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;제가 구현하면서 가장 어려웠던 부분은 프롬프트를 작성하는 일이었습니다. 특히 해당 분야에 대한 도메인 지식이 부족하면 프롬프트를 제대로 만드는 것 자체가 쉽지 않다고 느낍니다. GPT의 도움을 받아 어떻게든 동작하도록 만들긴 했지만, 이 프롬프트가 정말 좋은지, 혹시 잘못된 부분은 부분은 없는지 판단하려면 트레이딩에 이해가 필요합니다. 그래서 투자 자체에 대해서 조금 공부할 필요성을 느끼고 있습니다.&lt;/p&gt;
&lt;p&gt;이후에는 실제 운용을 해보면서 조금씩 고쳐보려고 하는데요. 최근에 구글의 Lee Boonstra가 발간한 66페이지의 짧은 프롬프트 엔지니어링 책이 화제였습니다. 해당 글을 참고하면서 다듬을 부분은 없는지 찾아보고 발전시켜 볼 생각입니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://pangyoalto.com/llm-agent-guide-for-newbie/"&gt;평범한 개발자들을 위한 LLM 에이전트 프레임워크 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://arxiv.org/abs/2412.20138"&gt;TradingAgents: Multi-Agents LLM Financial Trading Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://arxiv.org/abs/2307.03172"&gt;Lost in the Middle: How Language Models Use Long Contexts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://research.google/blog/chain-of-agents-large-language-models-collaborating-on-long-context-tasks/"&gt;Chain of Agents: Large language models collaborating on long-context tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://arxiv.org/abs/2304.03442"&gt;Generative Agents: Interactive Simulacra of Human Behavior&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://arxiv.org/pdf/2308.00352"&gt;MetaGPT: Meta Programming for A Multi-Agent Collaborative Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://arxiv.org/pdf/2210.03629"&gt;ReAct: Synergizing Reasoning and Acting in Language Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>CacheBlend | RAG기반 LLM 추론 속도 개선하기</title><link>https://scalalang2.com/p/cacheblend-rag%EA%B8%B0%EB%B0%98-llm-%EC%B6%94%EB%A1%A0-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-bdf8fa7c8e40/</link><pubDate>Sat, 19 Apr 2025 23:43:17 +0000</pubDate><guid>https://scalalang2.com/p/cacheblend-rag%EA%B8%B0%EB%B0%98-llm-%EC%B6%94%EB%A1%A0-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-bdf8fa7c8e40/</guid><description>&lt;p&gt;&lt;a href="https://arxiv.org/pdf/2405.16444"&gt;CacheBlend&lt;/a&gt;[1]는 EuroSys’2025에서 최우수 논문(Best Paper)로 선정된 두 개의 논문 중 하나입니다. CacheBlend은 기존 접근법과 비교하여 RAG, QA 태스크에서 &lt;strong&gt;TTFT(time-to-first-token)&lt;/strong&gt;를 2.2–3.3배, 그리고 추론 처리량은 2.8–5배 개선했다고 주장합니다.&lt;/p&gt;
&lt;p&gt;해당 논문의 저자는 &lt;a href="https://lmcache.ai/"&gt;LMCache&lt;/a&gt;[2]라는 이름의 오픈소스를 개발했고, LMCache는 vLLM 위에서 동작하며 RAG 기반의 프롬프트를 처리할 때 추론 속도를 높이는데 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4ba313a9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4ba313a9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-yAfO-ZvgWNl_-7YD6UmR8A-ce3a0e256510.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4ba313a9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-yAfO-ZvgWNl_-7YD6UmR8A-ce3a0e256510.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;vLLM을 LMCache와 함께 사용할 때 TTFT가 큰 폭으로 개선된다고 소개하고 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;본업이 아닌 취미로 논문을 읽는 사람의 시선에서 정리한 내용입니다. 혹시 알고 계신 것과 다른 내용이 있거나 궁금한 점이 있으시면 피드백 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/u/89ed5885f0af"&gt;Pangyoalto&lt;/a&gt;님이 작년에 vLLM에 대해서 소개해주셨는데요.&lt;br&gt;
본 글과 함께 보시면 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/efficient-memory-management-for-large-language-model-serving-with-pagedattention-01aacffe3b78" title="https://medium.com/rate-labs/efficient-memory-management-for-large-language-model-serving-with-pagedattention-01aacffe3b78"&gt;&lt;strong&gt;Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/strong&gt;&lt;br&gt;
*해당 글은 필자의 블로그에 이미 발간된 글입니다.&lt;br&gt;
본 글은 Efficient Memory Management for Large Language Model Serving with PagedAttention(2023)…*medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/efficient-memory-management-for-large-language-model-serving-with-pagedattention-01aacffe3b78"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="table-of-contents"&gt;Table Of Contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;트랜스포머(Transformer)&lt;/li&gt;
&lt;li&gt;KV 캐시&lt;/li&gt;
&lt;li&gt;논문의 배경&lt;/li&gt;
&lt;li&gt;CacheBlend&lt;/li&gt;
&lt;li&gt;성능 평가&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="트랜스포머transformer"&gt;트랜스포머(Transformer)&lt;/h2&gt;
&lt;p&gt;2017년 발표된 &lt;a href="https://research.google/pubs/attention-is-all-you-need/"&gt;Attention Is All You Need&lt;/a&gt;[3] 논문으로 잘 알려진 트랜스포머는 대규모 언어 모델(LLM)의 핵심 컴포넌트입니다. 트랜스포머는 입력 시퀀스 내 모든 토큰쌍 간의 관계를 어텐션 매커니즘을 통해 계산합니다.&lt;/p&gt;
&lt;p&gt;각 레이어에서는 들어온 입력에 대해 &lt;strong&gt;쿼리(Q), &lt;/strong&gt;키(K), &lt;strong&gt;밸류(V)&lt;/strong&gt; 벡터를 생성하고 이들 간 내적 연산을 통해 각 토큰이 얼마나 다른 토큰과 강하게 의미가 연결되는지 계산합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-67ee723a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-67ee723a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CyCNFP2Nlv35eCndoUrbcA-255f30f77eaa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-67ee723a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CyCNFP2Nlv35eCndoUrbcA-255f30f77eaa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;트랜스포머 구조&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-09a14eb6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-09a14eb6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UqfoFl9G-EWguxGxwn_hNw-ce22331b8b31.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-09a14eb6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UqfoFl9G-EWguxGxwn_hNw-ce22331b8b31.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;어텐션 계산 수식&lt;/p&gt;
&lt;h2 id="kv-캐시"&gt;KV 캐시&lt;/h2&gt;
&lt;p&gt;KV캐시&lt;a href="https://huggingface.co/blog/not-lain/kv-caching"&gt;[4]&lt;/a&gt;&lt;a href="https://medium.com/@plienhar/llm-inference-series-4-kv-caching-a-deeper-look-4ba9a77746c8"&gt;[5]&lt;/a&gt;&lt;a href="https://developer.nvidia.com/blog/mastering-llm-techniques-inference-optimization/"&gt;[6]&lt;/a&gt;는 LLM에서 토큰을 생성하는 &lt;strong&gt;디코더&lt;/strong&gt;에서 추론 속도를 높이는 데 쓰입니다. 자기 회귀 언어 모델(Autoregressive LLM)은 문장의 다음 토큰을 생성하는데 모든 이전 토큰의 K, V벡터를 사용하는데요. 이 과정에서 일부 반복적인 어텐션 계산이 필요하며, 이것이 디코딩 성능에 큰 영향을 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ae96d7a2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ae96d7a2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-mSugAChiU_eZ_ogIhzu22w-7a182e726b0b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ae96d7a2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-mSugAChiU_eZ_ogIhzu22w-7a182e726b0b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;인코더 과정에서의 Self-Attention과 달리, 디코더에서 t 번째 토큰의 어텐션 계산에 필요한 값은 이전 토큰들의 Key 벡터와 Value 벡터 뿐입니다. 이를 캐싱해서 불필요한 어텐션 계산 과정을 줄이면 &lt;strong&gt;시간복잡도가 2차함수 O(n²)에서 선형시간 O(n) 으로 줄어듭니다.&lt;/strong&gt; (이때 n은 입력 시퀀스 길이)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d88c652a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d88c652a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TP4ZF0A3tbTz2IsjRJQ7Ug-0bc15afe52d7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d88c652a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TP4ZF0A3tbTz2IsjRJQ7Ug-0bc15afe52d7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;KV 캐시를 활용한 계산 방법 / 행렬 차원은 고려하지 않고 대충 그린 그림입니다. &lt;a href="https://medium.com/@plienhar/llm-inference-series-4-kv-caching-a-deeper-look-4ba9a77746c8"&gt;(그림 출처)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위 그림은 어텐션 레이어 하나에 대해서 표현한 것이지만, 실제로는 모든 트랜스포머 레이어에서 KV 캐시를 저장해야 합니다. 임의의 층 L에서의 입력값은 L-1 층에서의 결과값인 W_O 의 어텐션 결과값이기 때문에 이전 토큰의 K/V 벡터를 캐싱한다는 것은이전 토큰의 어텐션 결과값을 캐싱한다는 의미 입니다. &lt;a href="https://arxiv.org/pdf/2312.04333"&gt;(LLaMA 2–7B 모델은 32개의 트랜스포머 레이어를 가지고 있습니다)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;KV캐시의 의미 자체는 디코딩 과정에서 나오는 K/V 벡터를 캐싱하는 것이지만, 이를 더 활용할 수 있는 방법이 있습니다. 예를 들어 LLM 에이전트에게 시스템 프롬프트를 사용하면 토큰을 생성할 때 항상 같은 K/V 벡터를 사용하게 되겠죠.&lt;/p&gt;
&lt;h2 id="논문의-배경"&gt;논문의 배경&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;RAG(Retrieval-Augmented Generation)&lt;/strong&gt;란 LLM을 더 잘 사용하기 위한 방법론 중 하나입니다. RAG는 흔히 사용자의 질문에 더 잘 답변하기 위해 연관된 텍스트 청크를 &lt;a href="https://medium.com/rate-labs/milvus-%EB%B2%A1%ED%84%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-b26065c51c16"&gt;Milvus&lt;/a&gt; 혹은 &lt;a href="https://cloud.google.com/vertex-ai?hl=ko"&gt;Google Vertex&lt;/a&gt;와 같은 검색엔진에서 찾은 다음 사용자 입력 앞단에 같이 덧붙여서 LLM에 입력으로 주는 방식으로 동작합니다.&lt;/p&gt;
&lt;p&gt;LLM은 학습한 시점 이후부터는 새로 추가된 지식이 없기 때문에, 이미 과거가 되어버린 정보를 줄 수 있다는 단점이 있습니다. 주기적으로 다시 학습시키면 되겠지만 비용 부담이 크기 때문에 최근에는 RAG를 통해 이를 극복하고자 하는 시도가 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b2802181"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b2802181"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-26YnsfeB_3dxB1Ur-8612c42e796f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b2802181"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-26YnsfeB_3dxB1Ur-8612c42e796f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/abs/2312.10997"&gt;Retrieval-Augmented Generation for Large Language Models: A Survey&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/rag%EC%9D%98-%EC%A7%A7%EC%9D%80-%EC%97%AD%EC%82%AC-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-%EC%B2%AB-%EB%85%BC%EB%AC%B8%EB%B6%80%ED%84%B0-%EC%B5%9C%EA%B7%BC-%EB%8F%99%ED%96%A5%EA%B9%8C%EC%A7%80-53c07b9b3bee" title="https://medium.com/rate-labs/rag%EC%9D%98-%EC%A7%A7%EC%9D%80-%EC%97%AD%EC%82%AC-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-%EC%B2%AB-%EB%85%BC%EB%AC%B8%EB%B6%80%ED%84%B0-%EC%B5%9C%EA%B7%BC-%EB%8F%99%ED%96%A5%EA%B9%8C%EC%A7%80-53c07b9b3bee"&gt;&lt;strong&gt;RAG의 짧은 역사 훑어보기(첫 논문부터 최근 동향까지)&lt;/strong&gt;&lt;br&gt;
*해당 글은 필자의 블로그에 이미 발간된 글입니다.*medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/rag%EC%9D%98-%EC%A7%A7%EC%9D%80-%EC%97%AD%EC%82%AC-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0-%EC%B2%AB-%EB%85%BC%EB%AC%B8%EB%B6%80%ED%84%B0-%EC%B5%9C%EA%B7%BC-%EB%8F%99%ED%96%A5%EA%B9%8C%EC%A7%80-53c07b9b3bee"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;번외&lt;/strong&gt;
일반적으로 PostgreSQL의 성능 저하 이슈를 LLM에 물어보면, 쿼리를 최적화하세요. 설정을 점검하세요. 리소스를 확인하세요 등 잘 알려진 몇개의 답변밖에 하지 못합니다. 하지만, 내가 실제 사용하고 있는 쿼리, 실제 현재 컴퓨팅 자원 메트릭, 데이터베이스 로그 등을 함께 입력으로 제공하면 더 정확한 답변을 들을 수 있겠죠. &lt;a href="https://www.amazon.science/publications/panda-performance-debugging-for-databases-using-llm-agents"&gt;AWS 연구팀은 지금 말한 내용을 실제 구현하고 평가했습니다[6]&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;본 포스팅의 주인공인 CacheBlend 논문에서는 RAG 환경에서 K/V 캐시를 재활용하는 방법을 제시합니다. 논문에서 제시한 해결책을 다루기 앞서 현재 K/V 캐시를 활용하는 두 가지 패턴의 문제점을 먼저 짚고 넘어갑니다.&lt;/p&gt;
&lt;h3 id="-prefix-caching"&gt;① Prefix Caching&lt;/h3&gt;
&lt;p&gt;Prefix Caching이란 LLM 에이전트의 시스템 프롬프트처럼 무조건 반복적으로 사용되는 경우 사용합니다. Prefix Caching은 원래 LLM 추론 과정과 같은 연산을 하기 때문에 출력 텍스트 품질 손실이 없습니다. &lt;a href="https://docs.vllm.ai/en/latest/"&gt;vLLM&lt;/a&gt;, &lt;a href="https://github.com/sgl-project/sglang"&gt;SGLang&lt;/a&gt;, &lt;a href="https://arxiv.org/abs/2404.12457"&gt;RAGCache&lt;/a&gt;[7] 시스템들이 이런 기법을 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;하지만, RAG를 사용하게 되면 여러 텍스트 청크를 검색엔진에서 가져와서 붙이기 때문에 Prefix Caching을 사용할 수 없습니다. 예를 들어 &lt;strong&gt;&lt;code&gt;C1, C2&lt;/code&gt;&lt;/strong&gt; 두개의 문장을 가져왔다고 합시다. 문장 &lt;strong&gt;&lt;code&gt;C1 + C2&lt;/code&gt;&lt;/strong&gt;를 이어 붙여서 사용하면 2가지 문제가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;첫 문장인 &lt;strong&gt;C1&lt;/strong&gt;은 트랜스포머 구조에서 Positional Embedding이 보존되는 반면, 그 다음에 등장하는 문장인 C2는 위치가 달라졌기 때문에 다시 계산해야 합니다. 따라서 K/V 캐시 전체를 다시 계산해야 하죠&lt;/li&gt;
&lt;li&gt;이를 무시하고 그냥 사용한다고 하면, C2의 캐시에는 C1의 문장을 포함한 어텐션 값이 없기 때문에 잘못된 답을 생산합니다. &lt;strong&gt;이 두가지 이유로 RAG에서는 Prefix Caching을 그대로 사용하기 어렵습니다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아래 그림은 메시와 호날두의 피파 월드컵 통산 골 수를 비교하는 입력을 넣었을 때 두 선수의 기록을 각각 찾아서 이어붙인 사례를 보여줍니다. 두 문장의 K/V 캐시를 그냥 이어붙이면 답변을 제대로 못하는 것을 볼 수 있습니다. (메호대전을 언급하다니..)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-de0efc19"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-de0efc19"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qfsgQzt0JlWW4wyU78yCNg-957d7775e73d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-de0efc19"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qfsgQzt0JlWW4wyU78yCNg-957d7775e73d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그림 출처 : CacheBlend 논문&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;번외&lt;/strong&gt;&lt;br&gt;
RAGCache는 논문을 대충 훑어보기만 했는데요. 방법론 자체가 Prefix에 자주 등장하는 텍스트의 K/V 캐시를 더 메모리에 오래 상주시키자는 아이디어에서 출발합니다. CacheBlend와는 접근법이 많이 다르기 때문에 이정도로만 짧게 소개하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-full-kv-reuse"&gt;② Full KV reuse&lt;/h3&gt;
&lt;p&gt;논문에서는 &lt;strong&gt;PromptCache(MLSys’2024)&lt;/strong&gt;[8] 논문의 접근법을 Full KV reuse라고 부르고 있습니다. PromptCache는 입력의 접두가(prefix)가 아니더라도 앞에 buffer라고 불리는 의미없는 더미 텍스트를 넣어서 텍스트 청크의 위치 정보는 보존하는 방법입니다.&lt;/p&gt;
&lt;p&gt;하지만, 이 접근법은 문장과 문장사이의 어텐션은 무시하기 때문에 RAG에서는 사용하기 어렵습니다. CacheBlend 논문에서는 RAG 태스크를 수행할 때 이어 붙인 텍스트 청크가 증가할 수록 PromptCache 방법론의 F1-Score가 감소하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2c7acde9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2c7acde9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ep9fbh28knYHSjgJvg6hQA-010e3df20bc8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2c7acde9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ep9fbh28knYHSjgJvg6hQA-010e3df20bc8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;PromptCache(= Full KV reuse)의 답변 품질이 감소하는 이유는, 앞서 말했듯 &lt;strong&gt;문장과 문장 사이의 어텐션(cross chunk attention)&lt;/strong&gt;을 무시하기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7a674f97"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7a674f97"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-g8O9LoVoLRTFZj87fk4qBw-484291ed8396.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7a674f97"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-g8O9LoVoLRTFZj87fk4qBw-484291ed8396.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;(a) KV 값을 처음부터 계산한 경우, (b) Full KV reuse를 사용한 경우&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;번외 1)&lt;/strong&gt;
RAG에서 텍스트 청크 개수가 많아지면 처음엔 성능이 증가하다가 이후 어느 순간 감소하는 것을 볼 수 있습니다. 이는 LLM이 초반부와 마지막에 있는 문장은 집중해서 보고, 문장 가운데에 있는 정보는 잃어버리는 경향이 있다는 스탠포드 및 UC Berkeley 연구진이 보고한 &lt;a href="https://arxiv.org/pdf/2307.03172"&gt;Lost in the Middle[9]&lt;/a&gt; 문제와 관련이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;번외 2)&lt;/strong&gt;
CacheBlend는 PromptCache의 논문을 인용했지만, 사실 PromptCache 에서 해결하고자 하는 문제는 RAG가 아닌 템플릿화된 프롬프트에서 성능을 높이는 방법입니다. 이 둘을 직접 비교하긴 어려우나 제 개인적인 생각으로는 CacheBlend가 상위호환처럼 보이기는 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2042a547"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2042a547"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uhMFyZZnnlmyeSpMPS5jaw-456cd20acdfd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2042a547"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uhMFyZZnnlmyeSpMPS5jaw-456cd20acdfd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/pdf/2311.04934"&gt;출처 : PromptCache&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="cacheblend"&gt;CacheBlend&lt;/h2&gt;
&lt;p&gt;CacheBlend의 목표는 RAG 환경에서 성능과 레이턴시를 모두 챙길 수 있는 방법을 찾는 것입니다. 먼저 중요한 용어부터 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4ddd5be8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4ddd5be8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-R9rkMBDDgtJg2L7HyIf-eg-59710fda2ce0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4ddd5be8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-R9rkMBDDgtJg2L7HyIf-eg-59710fda2ce0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;표에는 많은 기호가 적혀있지만 이 글을 보시는 분은 마지막 두 개의 표현에만 주목하시면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KV deviation(= $\Delta_{kv}$)&lt;/strong&gt; 는 전체를 다시 계산했을 경우와 K/V 캐시간의 절댓값 오차를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attention deviation(= $\Delta_{attn}$)&lt;/strong&gt; 는 KV deviation과 마찬가지로 캐시 사용 없이 전체를 계산했을 때의 어텐션 값과 캐시를 이용한 경우의 어텐션 값 차이를 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CacheBlend의 목적은 캐시 중 일부를 업데이트 하면서(KV_new), 실제로 계산했을 경우의 어텐션 값 차이를 최소화 하는 것입니다. 이를 통해 RAG 환경에서 여러 청크들의 KV 캐시를 재활용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-61af9092"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-61af9092"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-r0RHgnAqbM9VA-PhG2xbhQ-4927f55c8fbc.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-61af9092"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-r0RHgnAqbM9VA-PhG2xbhQ-4927f55c8fbc.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 말을 아래 그림에서 잘 표현해주고 있습니다. CacheBlend는 K/V 캐시 중에서 일부 벡터만 다시 계산하고 나머지 부분은 캐시를 그대로 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-020cb268"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-020cb268"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UUj6sWILE0_QdRX2wxvOKQ-c40403c4d3c0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-020cb268"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UUj6sWILE0_QdRX2wxvOKQ-c40403c4d3c0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그럼 이제 중요한 질문이 떠오릅니다. &lt;strong&gt;어떤 토큰을 재계산할 지 어떻게 결정할까요?,&lt;/strong&gt; 가장 좋은 건 $\Delta_{kv}$값을 가장 크게 만드는 &lt;strong&gt;범인&lt;/strong&gt;을 찾아서 해당 벡터만 다시 계산하는 겁니다. 이 말을 달리하면, 다시 계산했을 때 $\Delta_{kv}$이 작아지는 토큰을 찾아서 업데이트해야 합니다. 제가 여기서는 범인이라고 표현했지만 논문에서는 이를 &lt;strong&gt;HKVD(High-KV-Deviation)&lt;/strong&gt; 토큰 이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;HKVD를 구하려면 이를 비교할 수 있는 &lt;strong&gt;KV_full(전체 계산)&lt;/strong&gt;값이 필요하기 때문에 우리는 휴리스틱한 접근법이 필요한데요. 논문 저자들은 트랜스포머에서 다음 두 가지 현상을 발견합니다.&lt;/p&gt;
&lt;h3 id="-한-레이어에서-hkvd-토큰을-다시-계산하면-delta_attn이-큰폭으로-감소한다"&gt;① 한 레이어에서 HKVD 토큰을 다시 계산하면 $\Delta_{attn}$이 큰폭으로 감소한다.&lt;/h3&gt;
&lt;p&gt;아래 그림은 논문 저자들이 3개의 모델에 대해서 K/V 벡터를 재계산 할 비율 R%를 늘려가면서 $\Delta_{attn}$의 변화량을 관찰한 결과입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-403401cd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-403401cd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xJM2nBUMl3MI5EuLbsnQzw-bfa70a59b925.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-403401cd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xJM2nBUMl3MI5EuLbsnQzw-bfa70a59b925.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;어림잡아 10-20% 정도만 재계산해도 $\Delta_{attn}$ 값이 큰 폭으로 감소하는 것을 볼 수 있습니다. 저자들은 이 현상의 근거를 &lt;a href="https://openai.com/index/sparse-transformer/"&gt;&lt;strong&gt;어텐션 희소성(attention sparsity)[10]&lt;/strong&gt;&lt;/a&gt;에서 찾았습니다 (높은 어텐션값은 일부 소수의 토큰에 집중된 경향이 있습니다)&lt;/p&gt;
&lt;h3 id="-레이어-간-hkvd토큰은-높은-상관관계를-가진다"&gt;② 레이어 간 HKVD토큰은 높은 상관관계를 가진다.&lt;/h3&gt;
&lt;p&gt;어떤 한 레이어에서 발견된 HKVD 토큰은 다음 레이어에서도 HKVD토큰인 경향이 매우 크다고 합니다. 논문 저자들은 이에 대한 근거로 인접한 레이어에서 각 토큰들의 $\Delta_{kv}$ 값의 &lt;a href="https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient"&gt;스피어만 상관계수&lt;/a&gt;를 제시합니다.&lt;/p&gt;
&lt;p&gt;쉽게 비유하자면, 중학교 시험에서 순위권 성적을 받은 학생이 고등학교에서도 순위권 성적을 받을 확률이 높은 것으로 이해하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-40c2cc92"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-40c2cc92"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-shLTApQB2uPxPZ16GCu1iw-58f2188ddda2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-40c2cc92"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-shLTApQB2uPxPZ16GCu1iw-58f2188ddda2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;뒤 두 사실을 근거로 논문 저자들은 첫 번째 레이어에서는 캐시를 사용하지 않고 정석대로 계산한 다음에 r%의 높은 성적($\Delta_{kv}$)을 받은 토큰들을 선택해서 모든 레이어에서 이 친구들만 재계산합니다.&lt;/p&gt;
&lt;p&gt;하지만 수능이라는 단 한 번의 시험이 학업수행능력의 평가의 척도로 쓰인다면 조금 아쉬울 수 있겠죠. GPT 3.5의 LLM 레이어는 96개인 것으로 알려져 있습니다. 논문 저자들은 첫 번째 레이어에서만 상위권 학생을 고정시켜 버리면 통계적으로 조금 불안정한 값이 될 수 있다고 합니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;점진적 필터링 스킴(gradual filtering scheme)&lt;/strong&gt;을 추가로 사용하는데요. 첫 번째 레이어에서 모집하고자 하는 r%의 학생 보다 더 많은 수를 선발해서 이를 다음 레이어에서 점진적으로 줄여나갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-55b3a6ba"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-55b3a6ba"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-tzpGdYEec73bGfPb3hFgZQ-59069bc7cea1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-55b3a6ba"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-tzpGdYEec73bGfPb3hFgZQ-59069bc7cea1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;논문의 핵심 아이디어가 이해하고 보면 생각보다 단순하죠? 논문에서는 &lt;strong&gt;｢5장. 시스템 디자인｣, ｢6장. 구현｣&lt;/strong&gt; 에서 vLLM에 3000줄의 코드를 추가하면서 어떻게 이를 구현했는지 설명하는 내용이 있습니다. 본 글에서는 이 내용을 생략하였으니 해당 내용이 궁금하신 분들은 논문을 참고하시기 바랍니다.&lt;/p&gt;
&lt;h2 id="성능-평가"&gt;성능 평가&lt;/h2&gt;
&lt;p&gt;CacheBlend의 평가 결과를 요약하면 3가지로 말할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TTFT 개선&lt;/strong&gt; : 여러 모델과 태스크에 대해서 캐시를 전혀 사용하지 않는 것 대비 2.2–3.3x의 속도 개선&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;높은 퀄리티 유지 :&lt;/strong&gt; PromptCache 대비 CacheBlend는 0.15 ~ 0.35 높은F1-Score 및 Rouge-L을 기록하고 있다. 캐시를 쓰지 않고 재계산 했을때의 성능과 비교하면 0.01–0.03의 품질 저하가 있지만 이는 수용할 수 있는 범위 이내이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;높은 처리량 :&lt;/strong&gt; CacheBlend는 3.3x ~ 5x 높은 처리량을 가진다. (RAG 환경에서의 성능 비교이기 때문에 어쩌면 당연한 결과입니다. 다른 방법론들은 RAG에서 가져오는 청크 크기가 클수록 TTFT가 선형적으로 느려질 것입니다)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-752167fa"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-752167fa"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7pcwKkGh8tKi6ACH2wNYNA-59f793e822d0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-752167fa"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7pcwKkGh8tKi6ACH2wNYNA-59f793e822d0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;퀄리티는 유지하면서 TTFT는 개선했다 (청크 개수 = 6)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-da5b4984"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-da5b4984"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGZtDkknSgR_b9ydz3vjYw-29078eb64683.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-da5b4984"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGZtDkknSgR_b9ydz3vjYw-29078eb64683.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;요청 수가 증가해도 타 모델 대비 안정적인 TTFT를 보여준다&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;이번 글에서는 KV 캐시를 사용하는 &lt;strong&gt;자기회귀 언어 모델(Autoregressive LLM)&lt;/strong&gt;에서 성능을 개선한 CacheBlend 논문을 다루었습니다. 사실 제가 LLM에 크게 관심이 있기 보단 상 받은 논문이니까 읽어나 보자는 마음으로 접근했는데요. 생각보다 많은 내용을 배웠고 또한 재밌게 읽었습니다.&lt;/p&gt;
&lt;p&gt;본 스터디에는 N사에서 LLM 업무를 하시는 분이 계신데요. 이 분이 몇 달전에 &lt;a href="https://arxiv.org/pdf/2502.09992"&gt;확산 모델 기반 LLM[11]&lt;/a&gt; 논문을 소개해 주신적이 있습니다. 실제로 확산 모델(Diffusion Model)연구의 권위자 분들이 창업한 &lt;a href="https://www.inceptionlabs.ai/"&gt;인셉션 랩스의 머큐리&lt;/a&gt;를 이용해보시면 문장 생성 속도가 엄청 빠르다는 것을 체감할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d83841a5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d83841a5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pJAt6vgozQ7kP2MnZnGxJw-87326323ded8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d83841a5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pJAt6vgozQ7kP2MnZnGxJw-87326323ded8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;자동차의 가속능력을 평하기 위해서 &lt;strong&gt;제로백(정지 상태에서 시속 100km 가속에 걸리는 시간)&lt;/strong&gt; 이라는 평가 지표를 많이 사용했었습니다. 전기차가 등장하기 이전에는 이것이 제조사의 능력을 평가하는 척도로 사용되었지만, 전기차라는 패러다임이 등장하고 나서는 왠만한 전기차는 내연기관의 제로백을 가뿐히 넘습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/pdf/2502.09992"&gt;확산 모델 기반 LLM[11]&lt;/a&gt;로 패러다임이 옮겨가면, 이미 나온 제품들에는 위기가 오고, 이제 막 연구를 시작한 사람들에게는 기회가 올 수도 있습니다. 확산 모델 기반 LLM도 트랜스포머 구조를 이용하기 때문에 오늘 배운 CacheBlend가 적용될 수 있긴 하지만 자기회귀 모델에 적용했을 때와 비교해서 놀랄 만한 성능일지는 한 번 지켜봐야 할 것 같습니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://arxiv.org/pdf/2405.16444"&gt;CacheBlend: Fast Large Language Model Serving for RAG with Cached Knowledge Fusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://lmcache.ai/"&gt;LMCache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://research.google/pubs/attention-is-all-you-need/"&gt;Attention Is All You Need&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://huggingface.co/blog/not-lain/kv-caching"&gt;KV Caching Explained: Optimizing Transformer Inference Efficiency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://medium.com/u/4e8bdd342794"&gt;Pierre Lienhart&lt;/a&gt; | &lt;a href="https://medium.com/@plienhar/llm-inference-series-4-kv-caching-a-deeper-look-4ba9a77746c8"&gt;LLM Inference Series: 4. KV caching, a deeper look&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://www.amazon.science/publications/panda-performance-debugging-for-databases-using-llm-agents"&gt;Panda: Performance debugging for databases using LLM agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://arxiv.org/abs/2404.12457"&gt;RAGCache: Efficient Knowledge Caching for Retrieval-Augmented Generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://arxiv.org/pdf/2311.04934"&gt;Prompt Cache: Modular Attention Reuse for Low-Latency Inference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://cs.stanford.edu/~nfliu/papers/lost-in-the-middle.arxiv2023.pdf"&gt;Lost in the Middle: How Language Models Use Long Context&lt;/a&gt;s&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://openai.com/index/sparse-transformer/"&gt;Generative modeling with sparse transformers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[11] &lt;a href="https://arxiv.org/pdf/2502.09992"&gt;Large Language Diffusion Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Dynamo DB냐 RDBMS냐 그것이 문제로다</title><link>https://scalalang2.com/p/dynamo-db%EB%83%90-rdbms%EB%83%90-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4-45fdf48f6a9d/</link><pubDate>Wed, 29 Jan 2025 08:11:27 +0000</pubDate><guid>https://scalalang2.com/p/dynamo-db%EB%83%90-rdbms%EB%83%90-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4-45fdf48f6a9d/</guid><description>&lt;p&gt;회사에 새롭게 입사한 신입 직원이 이런 질문을 했습니다. “왜 여기는 RDBMS를 안쓰고 DynamoDB를 쓰나요?” 저는 RDBMS는 서버 운영 중에 샤드 개수를 늘리고 축소하는 것이 쉽지 않고 &lt;strong&gt;DynamoDB는 수평 확장이 쉽다고 답변했습니다&lt;/strong&gt;. 하지만 답변하면서도 머릿속에 몇몇 반례들이 떠올랐고 답변이 추상적이라고 느껴서 여러모로 스스로 아쉬운 답변이었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CockroachDB나 Vitess처럼 비교적 최근에 만들어진 RDBMS들은 기본적으로 MySQL 혹은 PostgreSQL의 프로토콜과 호환되게 만들면서도 수평 확장을 지원합니다[1].&lt;/li&gt;
&lt;li&gt;심지어 CockroachDB는 PostgreSQL을 제공하지만, 내부 구조는 LSM 트리 기반의 키-밸류 저장소를 이용합니다[2]. 다시 말해 DynamoDB와 유사한 아키텍처 위에 데이터 표현방식만 관계형 데이터로 다룰 수 있도록 제공한다는 뜻입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;몇 가지 반례를 떠오르고 나니, 자연스럽게 다음 질문이 떠올랐습니다. RDBMS가 수평확장이 쉽게 된다면 DynamoDB를 선택해야 할 이유가 있을까? 이제는 RDBMS가 DynamoDB의 완벽한 상위호환일까?&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;미완성의 저주&lt;/li&gt;
&lt;li&gt;과거와 현재, 그리고 Aurora Limitless Database&lt;/li&gt;
&lt;li&gt;관계형 vs 비관계형&lt;/li&gt;
&lt;li&gt;복제 (Replication)&lt;/li&gt;
&lt;li&gt;파티셔닝 (Partitioning)&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="미완성의-저주"&gt;미완성의 저주&lt;/h2&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7be7997c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7be7997c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-O_e-qAAloTyIly4OitxGWA-2ee222dc4486.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7be7997c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-O_e-qAAloTyIly4OitxGWA-2ee222dc4486.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Reddit 에서 &lt;strong&gt;DynamoDB/RDBMS 논쟁[3]&lt;/strong&gt;을 보던중에 누군가 엔지니어에게 허락된 정답은 &lt;strong&gt;“it depends”&lt;/strong&gt; 뿐이라고 남겼습니다. 소프트웨어 아키텍처에서 많은 부분이 트레이드-오프가 존재한다지만, 의사결정에 있어 모든 반론을 받아들인다면 제대로 된 결정을 할 수 없고 미완성의 바다에 장기간 표류하는 저주에 걸릴 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f689eedc"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f689eedc"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VrBb2hAOqbuhC5xDhGp0Nw-c7a486e3e7ed.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f689eedc"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VrBb2hAOqbuhC5xDhGp0Nw-c7a486e3e7ed.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://page.kakao.com/content/60800785"&gt;지. 지구의 운동에 대하여&lt;/a&gt;, &lt;a href="https://www.netflix.com/title/81765022"&gt;넷플릭스 상영중&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;프로그래밍 언어, 디자인 패턴 등은 중요하지만, 절대적인 비교우위를 따지기는 어렵습니다. 객체지향 또는 함수형 패러다임의 언어 중 어느 것을 선택하더라도 그 자체로 잘못된 결정이라고 할 수 없습니다. 이러한 논쟁은 종종 개인의 선호도나 경험에 기반한 주관적인 감정 표현으로 귀결되곤 합니다.&lt;/p&gt;
&lt;p&gt;데이터베이스 선택에 있어서도 RDBMS와 키-밸류 저장소는 각각의 장단점이 있습니다. 디스코드[4]는 ScyllaDB라는 분산 키-밸류 데이터베이스를 사용하는 반면, 슬랙[5]은 MySQL의 분산 확장 버전인 Vitess를 채택했습니다. 두 회사 모두 성공적으로 채팅 애플리케이션을 구현했지만, 그 기술적 선택은 서로 달랐습니다.&lt;/p&gt;
&lt;p&gt;서버 개발 과정에서 RDBMS와 키-밸류 데이터베이스 중 어느 것을 선택할지에 대해 의견이 대립할 때가 있습니다. 어느 한 쪽의 선택이 틀린 것은 없습니다. 디스코드와 슬랙의 사례를 보면 RDBMS로는 죽어도 못만들어 같은 상황은 없다는 거죠. 반대의 경우도 마찬가지죠. 여기서 만약 양측이 자신의 입장을 고수한다면 영원히 합의점을 찾기 어려울 겁니다.&lt;/p&gt;
&lt;p&gt;저는 이 글을 통해 우리 팀이 어떤 가치에 더 우선순위를 두느냐에 따라 &lt;strong&gt;RDBMS&lt;/strong&gt;와 &lt;strong&gt;키-밸류 데이터베이스&lt;/strong&gt; 중 하나를 선택할 수 있는 기준을 찾아보고자 했습니다. 미래에 비슷한 상황이 있을 때 이 글을 다시 보면서 제가 올바른 의사결정을 할 수 있길 바랍니다.&lt;/p&gt;
&lt;h2 id="과거와-현재-그리고-aurora-limitless-database"&gt;과거와 현재, 그리고 Aurora Limitless Database&lt;/h2&gt;
&lt;p&gt;ChatGPT에게 RDBMS와 키-밸류 데이터베이스의 차이에 대해 질문하면 성능및 확장성 관점에서 &lt;strong&gt;RDBMS는 수직 확장이 일반적이며 키-밸류 DB는 수평적 확장이 쉽고 빠른 읽기/쓰기에 적합하다고 합니다&lt;/strong&gt;. 정보의 바다에서 인류 대다수의 지식을 종합해서 이야기하는 ChatGPT의 특성상 이것이 일반적인 견해라고 해석할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1153d55f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1153d55f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-I0QyRzt0P4WbhQTdqIi-eQ-43dee0dd7ac8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1153d55f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-I0QyRzt0P4WbhQTdqIi-eQ-43dee0dd7ac8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;ChatGPT : RDBMS와 키-밸류 데이터베이스의 차이점이 뭐야?&lt;/p&gt;
&lt;p&gt;AWS를 이용하고 있다면 ChatGPT의 이 답변이 최근까지는 사실에 가까웠을지도 모릅니다. AWS RDS는 기본적으로 샤딩을 지원하지 않기 때문에 &lt;strong&gt;애플리케이션 레벨&lt;/strong&gt;에서 샤딩을 직접 구현하는 경우가 많았습니다[6][7][8]. (AWS Aurora Serverless도 수직 확장만 지원합니다)&lt;/p&gt;
&lt;p&gt;지금은 Citus[9], Vitess, Cockroach DB[10]처럼 수평 확장을 지원하는 RDBMS가 있습니다. 저는 &lt;strong&gt;클라우드 관리형 DB&lt;/strong&gt;가 아니라면 사용할 생각이 없었지만 최근 불과 2개월 전인 2024년 11월 &lt;a href="https://aws.amazon.com/ko/blogs/korea/amazon-aurora-postgresql-limitless-database-is-now-generally-available/"&gt;Amazon Aurora Limitless Database[11]&lt;/a&gt;가 출시되었습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/pUqVCK7Ggh0?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h3 id="-aws-aurora"&gt;① AWS Aurora&lt;/h3&gt;
&lt;p&gt;AWS Aurora는 데이터 스토리지 레이어와 연산 처리 레이어를 분리한 구조를 가지고 있습니다. 이 시스템에서 연산 능력은 ACU(Aurora Capacity Unit)라는 단위로 측정되는데, 각 ACU는 약 2GB의 메모리와 그에 맞는 CPU 성능, 네트워크 대역폭을 제공합니다.&lt;/p&gt;
&lt;p&gt;Aurora의 장점은 필요에 따라 ACU를 유연하게 조절할 수 있다는 점입니다. 사용한 ACU와 저장 공간에 따라 비용이 청구되므로, 트래픽 변동이 심한 서비스에 특히 효과적입니다. 예를 들어, 온라인 게임에서 피크 시간과 한가한 시간의 동시 접속자 수가 5–6배 차이 날 때 Aurora를 사용하면 이런 변동에 효율적으로 대응할 수 있습니다.&lt;/p&gt;
&lt;p&gt;기존 AWS RDS / Aurora Serverless 제품군에서는 Multi-master cluster를 생성한 뒤 샤딩을 서버 코드에서 직접 구현했다면 Aurora Limitless Database는 처음부터 샤딩을 통한 수평 확장이 가능한 데이터베이스를 목표로 개발되었습니다.&lt;/p&gt;
&lt;h2 id="-aurora-limitless-database"&gt;② Aurora Limitless Database&lt;/h2&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a5a1be2a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a5a1be2a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-K-wYkRq7rT5NUjmHFlBIlg-cf53ea75483a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a5a1be2a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-K-wYkRq7rT5NUjmHFlBIlg-cf53ea75483a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : &lt;a href="https://aws.amazon.com/blogs/aws/amazon-aurora-postgresql-limitless-database-is-now-generally-available/"&gt;Amazon Aurora PostgreSQL Limitless Database is now generally available&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Aurora Limitless Database는 처음부터 샤딩을 통한 수평 확장이 가능한 데이터베이스를 목표로 개발되었습니다. Limitless Database를 생성하면 단일 엔드포인트가 노출되어 사용자가 편리하게 데이터베이스를 이용할 수 있습니다. Limitless Database는 하나의 샤드 그룹으로 구성되어 있고 샤드 그룹은 2개의 계층으로 이루어져 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;라우터 계층 : 샤드 키 분할 정책, 리샤딩 전략, 쿼리 라우팅 등을 관리&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;샤드 계층 : 데이터의 부분집합이 저장되는 PostgreSQL 노드&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;클러스터 생성 시 최소 ACU와 최대 ACU를 설정해야 하며, 이 값에 따라 초기 라우터와 샤드의 개수가 결정되는데요. 최소 ACU는 16, 최대 ACU는 6144까지 설정 가능합니다&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-cluster.html"&gt;[15]&lt;/a&gt;. 샤드의 데이터 용량 혹은 트래픽이 커지면 Shard Split을 통해 샤드를 분할할 수 있으며, 이는 수동으로도 가능하고 자동으로도 가능합니다.&lt;/p&gt;
&lt;p&gt;하지만 한 번 분할된 샤드는 다시 병합되지 않고, 개별 샤드나 라우터를 삭제하는 건 불가능하기 때문에 최소 ACU가 증가하게 됩니다[16]. 다시 말하면 수평 확장은 노드의 추가를 통해 이루어지고 비피크 타임을 위한 스케일 다운은 수직적으로 ACU를 늘리고 줄이면서 동작합니다. (e.g. 도쿄 리전에 라우터 8개, 샤드 16개를 구성한다면 최소 ACU는 96개로 대략 하루에 60만원 정도 청구됩니다)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DynamoDB와 Limitless Database는 모두 수평 확장을 지원하여 쓰기 비율이 높은 게임 서버의 요구사항을 충족할 수 있습니다.&lt;/strong&gt; 그러나 이 두 데이터베이스 간의 선택은 단순히 관계형 모델과 문서 모델의 차이만은 아닙니다. 더 깊이 있는 비교를 위해 다음 세 가지 측면에서 분석해보겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;관계형 vs 비관계형&lt;/li&gt;
&lt;li&gt;복제 메커니즘&lt;/li&gt;
&lt;li&gt;파티셔닝 전략&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이를 통해 두 데이터베이스의 차이점을 명확히 하고, 최종적으로 전문가의 의견을 종합해 선택 기준을 잡아보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;평소에 관심있게 기술 서적을 보시는 분들은 &lt;a href="https://www.yes24.com/product/goods/59566585"&gt;DDIA&lt;/a&gt; 라고 불리는 책에서 위 세 주제를 깊이 있게 다루는 것을 아실 겁니다. 아래 내용에 정리한 내용도 많은 부분 DDIA 책을 참고했고 일부 문구를 인용했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="관계형-vs-비관계형"&gt;관계형 vs 비관계형&lt;/h2&gt;
&lt;p&gt;RDBMS와 Dynamo DB의 가장 큰 차이점 중 하나는 데이터 모델의 표현 방식입니다. 관계형 데이터 모델에서는 데이터를 테이블 단위의 스키마로 표현합니다. 서버를 대부분 객체지향 프로그래밍 언어로 개발하기 때문에 관계형 데이터(테이블, 로우, 칼럼)와 객체 사이의 복잡한 전환 계층이 필요합니다. 이렇게 관계형 데이터베이스와 객체지향 프로그래밍의 개념적 차이로 인해 발생하는 문제를 &lt;strong&gt;임피던스 불일치(impedence mismatch)라고&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2d3fc894"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2d3fc894"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nEdGAls-6CXPjfPvlq5zLQ-283ce80b37b6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2d3fc894"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nEdGAls-6CXPjfPvlq5zLQ-283ce80b37b6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;반면 스키마를 제약하지 않는 &lt;strong&gt;비관계형(NoSQL)데이터베이스&lt;/strong&gt;는 유연한 스키마를 제공하여 이러한 불일치를 줄일 수 있습니다. JSON 형식으로 데이터를 저장함으로써 객체를 더 자연스럽게 표현할 수 있으며, 이는 코드의 간결성과 유지보수성을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;JSON 문서는 지역성(locality)이 높아서 상대적인 성능 이점이 있습니다. 데이터가 여러 테이블로 나눠져 있는 관계형 DB에서는 전체 문서를 조회할 때 다수의 인덱스과 디스크 탐색이 필요하기 때문에 레이턴시는 증가하고 처리량이 상대적으로 낮아집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;게임의 경우 플레이어 본인의 정보를 조회하고, 본인의 정보만을 수정하는 요청이 빈번한 경우에는 JSON 형식의 저장방식이 코드의 간결함을 유지하는데 도움이 될 수 있습니다. 아래는 제가 즐겨하는 게임 중 하나인 젠레스 존 제로의 인벤토리 모습입니다. 800개가 넘는 아이템을 한 번에 조회해야 하기 때문에 이 경우에는 문서 자체를 저장하는게 좋을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-14a1a95c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-14a1a95c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-eKTRhR5leBkYLlKuZv1UlQ-00ed480f996c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-14a1a95c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-eKTRhR5leBkYLlKuZv1UlQ-00ed480f996c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;본인의 비틱 디스크 후후&lt;/p&gt;
&lt;p&gt;Vitess, CockroachDB 그리고 Limitless Database가 수평 확장을 지원하긴 하지만 일각에서는 이 사실에 비판적인 시각이 있습니다. Apache Cassandra의 핵심 개발자 중 한명인 Jonathan Ellis는 2009년에 다음 발표[17]에서 관계형 데이터베이스는 수평 확장이 불가능 하다고 주장합니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6eab378e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6eab378e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D5e4sc8tgvjPD0_4O32sQQ-d5c18f166f6f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6eab378e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D5e4sc8tgvjPD0_4O32sQQ-d5c18f166f6f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;관계형 DB에서 쓰기를 수평확장 하는 건 사실상 불가능하다. &lt;br&gt;
&lt;strong&gt;만약 수평 확장을 한다면 그건 더 이상 관계형이 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위의 주장이 어떤 뜻으로 쓰인 건지는 정확히 알 수 없지만, 제가 알고 있는 지식에 기대어 생각해보면 다음과 같이 상상해볼 수 있습니다. 샤딩으로 수평 확장된 데이터베이스에서 데이터가 여러 샤드로 나뉘어져 있으면 트랜잭션은 여러 샤드에 걸쳐 &lt;strong&gt;2단계 커밋 프로토콜(2PC)&lt;/strong&gt;을 수행해야 ACID 속성을 만족시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e63ebf37"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e63ebf37"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iYUNQW8F-uAKn2nHxQBxcg-35c86b88cf31.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e63ebf37"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iYUNQW8F-uAKn2nHxQBxcg-35c86b88cf31.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ebrary.net/64872/computer_science/introduction_phase_commit#google_vignette"&gt;출처 : 데이터 중심 애플리케이션 설계&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2단계 커밋 프로토콜은 그 특성상 많이 사용할수록 성능과 처리량이 저하될 수밖에 없습니다. MySQL 분산 데이터베이스인 Vitess는 초기 모델에서 분산 트랜잭션을 지원하지 않았습니다&lt;a href="https://vitess.io/blog/2016-06-07-distributed-transactions-in-vitess/"&gt;[18]&lt;/a&gt;. 그래서 중간에 한 노드에서 중단이 발생하면 데이터 불일치가 발생합니다. 현재 Vitess는 2단계 커밋 프로토콜을 실험 버전으로 제공하고 있지만, 프로덕션 환경에서는 주의해서 사용하라는 경고 문구가 있으며 가능한 한 사용을 자제하도록 권고하고 있습니다[19].&lt;/p&gt;
&lt;p&gt;수평 확장을 위해 2단계 커밋 프로토콜이 발생하지 않도록 데이터를 설계할 수도 있습니다. 최신 RDBMS는 JSON 컬럼을 지원하기 때문에 문서 전체를 단일 행에 저장하면 트랜잭션이 단일 샤드에 대해 원자적으로 동작할 수 있습니다. 하지만 그렇게 하면 Jonathan Ellis의 주장처럼 데이터는 더 이상 관계형으로 표현되지 않게 됩니다.&lt;/p&gt;
&lt;h2 id="복제-replication"&gt;복제 (Replication)&lt;/h2&gt;
&lt;p&gt;복제란 데이터를 네트워크에 연결된 여러 노드에 분산해서 복사본을 유지하는 매커니즘입니다. 데이터베이스에서는 고가용성을 높이거나 읽기 연산의 처리량을 높이거나 지리적으로 사용자에 가깝게 복제해서 지연시간을 줄이는데 이용합니다. 데이터를 복제하는데에는 크게 동기식 복제, 비동기 복제, 정족수 복제와 합의를 이용한 상태복제머신이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-df6fa402"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-df6fa402"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--NfBPOS-y493q9s1Cpa47A-966781b448bd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-df6fa402"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--NfBPOS-y493q9s1Cpa47A-966781b448bd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;(좌) 비동기 복제 / (우) 동기 복제&lt;/p&gt;
&lt;p&gt;가장 기본적인 RDBMS를 사용한다면 보통 쓰기 인스턴스 한 대와 여러개의 복제 인스턴스(read replica)를 구성하게 됩니다. 쓰기 인스턴스(리더)에 발생한 요청은 연결된 읽기 인스턴스(팔로워)로 로그를 복제해서 복사본을 구성하는데요. 이를 리더- 팔로워 모델이라고 합니다.&lt;/p&gt;
&lt;p&gt;리더-팔로워 모델은 요청을 수행하는 리더에 장애가 발생하면, 팔로워 노드 중 하나가 다시 리더로 선출됩니다. 비동기 복제를 사용한다면 리더가 요청을 복제하지 못하고 장애가 발생하면 클라이언트는 본인의 요청이 유실되었다고 느끼게 됩니다. 동기식 복제는 모든 팔로워가 복제를 완료한 시점에 리더가 응답을 주기 때문에 일관성은 보장되나 팔로워 노드가 장애 상황이면 리더는 팔로워가 복구 될 때 까지 대기해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;번외편 / &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/replication/"&gt;Redis의 복제 기본 설정&lt;/a&gt;은 비동기로 동작하며, 1초마다 리더에게 ACK를 전달합니다. 이는 장애 시점에서 최대 1초간의 요청이 유실될 가능성이 있다는 의미입니다. 그래서 WAIT 커맨드로 동기 복제를 수행할 수 있는 기능이 있지만 성능을 다소 희생해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="정족수-복제quorum-replication"&gt;정족수 복제(Quorum Replication)&lt;/h3&gt;
&lt;p&gt;Aurora DB에서는 스토리지 레이어와 연산 레이어가 분리되어 있습니다. 그리고 스토리지 레이어는 최대 6대 중에 4대에 복제가 완료되어야 트랜잭션이 커밋됩니다. 정족수 복제는 이론적으로는 W+R ≥ N을 만족한다고 하더라도 일부 엣지케이스가 있다고 알려져 있습니다만, Aurora의 논문에 따르면 Aurora 스토리지 노드의 독특한 구조 때문에 합의까지는 필요 없다고 언급되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-20b14b44"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-20b14b44"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TRsggfT-94_J4th9_rFJhA-af022f0d3f94.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-20b14b44"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TRsggfT-94_J4th9_rFJhA-af022f0d3f94.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pages.cs.wisc.edu/~yxy/cs764-f20/papers/aurora-sigmod-17.pdf"&gt;Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Database&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="합의-알고리즘-consensus-algorithm"&gt;합의 알고리즘 (Consensus Algorithm)&lt;/h3&gt;
&lt;p&gt;합의 알고리즘은 높은 수준의 일관성과 내결함성을 제공하는 알고리즘입니다. 이러한 특성 때문에 쿠버네티스의 etcd, 아파치 카프카의 주키퍼 등 많은 분산 시스템의 컨트롤 플레인에서 활용되고 있습니다. 과거엔 Paxos 알고리즘이 주로 사용되었지만, 최근에는 Raft 알고리즘이 더 널리 채택되는 추세입니다.&lt;/p&gt;
&lt;p&gt;DynamoDB는 데이터 저장 시 Paxos 알고리즘을 사용하여 서로 다른 가용 영역(AZ)에 2-out-of-3 방식으로 복제합니다. 정족수 복제와 합의 알고리즘을 통한 높은 가용성 보장은 복제 노드 수가 적을수록 효율적입니다. 결론적으로, Aurora와 DynamoDB는 모두 고수준의 복제 기술을 사용하므로 가용성 측면에서 큰 차이가 없다고 볼 수 있습니다.&lt;/p&gt;
&lt;h2 id="파티셔닝-partitioning"&gt;파티셔닝 (Partitioning)&lt;/h2&gt;
&lt;p&gt;데이터가 매우 크거나 요청량이 많다면 복제로는 부족하고 데이터를 &lt;strong&gt;파티션&lt;/strong&gt; 단위로 쪼갤 필요가 있는데, 이 작업을 &lt;strong&gt;샤딩&lt;/strong&gt;이라고 합니다. (우선은 파티셔닝이 가장많이 쓰이는 용어이므로 파티션으로 통일했습니다)&lt;/p&gt;
&lt;p&gt;파티셔닝이 적용되는 데이터베이스는 맨 처음으로 파티션 키를 무엇으로 쓸지 결정해야 합니다. 이 파티션키가 동일한 데이터는 같은 파티션에 배치되는데요. 예를 들어 유저와 관련된 정보를 조회하는 일이 많다면 user_id를 파티션 키로 고려할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Aurora Limitless Database 에서는 테이블을 생성하기 전에 테이블 모드와 샤드키를 설정해야 합니다. 테이블에는 &lt;strong&gt;샤드 테이블(sharded)&lt;/strong&gt;과 &lt;strong&gt;참조 테이블(reference table)&lt;/strong&gt; 두 개가 있는데, 참조 테이블은 모든 노드에 복사본을 유지해서 조인 성능을 높이는데 사용합니다. 이런 개념은 거의 대부분 샤딩을 지원하는 데이터베이스에서 공유하고 있습니다.&lt;/p&gt;
&lt;p&gt;SET rds_aurora.limitless_create_table_mode=&amp;lsquo;sharded&amp;rsquo;;&lt;br&gt;
SET rds_aurora.limitless_create_table_shard_key=&amp;rsquo;{&amp;ldquo;item_id&amp;rdquo;, &amp;ldquo;item_cat&amp;rdquo;}&amp;rsquo;;&lt;br&gt;
CREATE TABLE items(item_id int, item_cat varchar, val int, item text);&lt;/p&gt;
&lt;p&gt;키를 파티션으로 나누는 방법에는 크게 &lt;strong&gt;범위 기반&lt;/strong&gt;과 &lt;strong&gt;해시 기반&lt;/strong&gt;이 있습니다. DynamoDB와 Aurora Limitless는 모두 해시 기반으로 파티션을 다룹니다만 단순히 &lt;strong&gt;모듈러 연산&lt;/strong&gt;을 취하기 보단 더 동적으로 이를 제어합니다. Aurora Limitless 에서는 엔지니어가 직접 SQL 명령어 단일 샤드 하나를 쪼갤 수 있는데요. 특정 샤드가 핫 파티션이 된 경우에 사용하면 적절히 부하를 분산시킬 수 있습니다[21].&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rds_aurora&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limitless_split_shard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;subcluster_id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rds_aurora&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limitless_list_shard_scale_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1691300000000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;br&gt;
job_id | action | job_details | status | submission_time | message&lt;br&gt;
&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;+&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;-+&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;ndash;+&amp;mdash;&amp;mdash;&amp;mdash;+&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;+&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;&amp;mdash;-&lt;br&gt;
1691300000000 | SPLIT_SHARD | Split Shard 3 by User | SUCCESS | 2023-08-06 05:33:20+00 | Scaling job succeeded. +&lt;br&gt;
| | | | | New shard instance with ID 7 was created.&lt;br&gt;
(1 row)&lt;/p&gt;
&lt;p&gt;DynamoDB에서 파티셔닝은 테이블의 처리량(throughput)에 의해 결정됩니다. 클라우드 장비의 스펙이나 네트워크 대역폭을 고려해서 하나의 파티션이 최대 1000 WCUs 만큼만 처리 가능하다는 결론이 나왔다고 합시다. 만약 테이블이 3200 WCUs로 설정했으면 DynamoDB는 800WCUs씩 할당된 4개의 파티션을 생성합니다. 시간이 지나서, 고객이 6000 WCUs로 증가시키면 기존에 존재하는 파티션을 분할합니다. 다시 말하면, 1000 WCUs를 가진 6개의 파티션이 아닌, 750WCUs를 가진 8개의 파티션을 생성하게 됩니다.&lt;/p&gt;
&lt;p&gt;이 과정을 반복하다보면 처음에는 파티션 하나에서 출발해서 트래픽이 증가할 수록 AWS가 관리하는 수천대의 스토리지 노드로 파티션이 분할됩니다. 이런 방식으로 DynamoDB는 일관된 레이턴시를 보장합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AWS RDS처럼 인스턴스를 임대하는 방식이 아닌 아마존이 거대한 키-밸류 DB를 운영하고 있고 멀티-테넌트 방식으로 우리에게 서비스를 제공하는 것이다 보니까 가능한거겠죠?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dc28a4d3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dc28a4d3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-83pOJA_GnaFr89hH6xXujw-0c291c5fdb12.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dc28a4d3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-83pOJA_GnaFr89hH6xXujw-0c291c5fdb12.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그림 출처 : &lt;a href="https://medium.com/rate-labs/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625"&gt;DynamoDB의 시스템 디자인과 분산 트랜잭션 구현 원리&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-파티션-리밸런싱"&gt;① 파티션 리밸런싱&lt;/h3&gt;
&lt;p&gt;데이터베이스를 운영하면 하루에도 몇 번씩 상황이 변합니다. 특정 파티션에 쿼리 요청량이 증가하면 이 부하를 다른 노드로 분배할 필요가 있고, 장애가 발생하면 해당 노드가 담당하던 역할을 다른 노드가 이어 받아야 합니다. 이 경우 클러스터에서 한 노드에서 담당하던 부하를 다른 곳으로 옮기는 과정을 &lt;strong&gt;리밸런싱(rebalancing), &lt;/strong&gt;리샤딩(resharding)이라고 합니다.&lt;/p&gt;
&lt;p&gt;만약 인스턴스를 임대하는 방식이라면 파티션 리밸런싱 전략을 알아둘 필요가 있는데요. Aurora Limitless와 DynamoDB는 모두 파티션을 분할해서 새로운 인스턴스를 할당할 뿐 클러스터 내 노드 끼리 파티션 데이터를 서로 교환하진 않습니다. Aurora Limitless는 위에서 다루었던 샤드 분할 쿼리가 리밸런싱 대안으로 제공됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;주로 온-디맨드 인스턴스로 운영하는 Apache Kafka, Elasticsearch 등은 파티션 리밸런싱할 때 클러스터 내의 노드끼리 파티션을 교환합니다. 이 때, 교환되는 파티션 개수를 작게 하기 위해서 일부러 노드 수 보다 많은 양의 파티션을 사전에 미리 생성하곤 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-자동-리밸런싱과-수동-리밸런싱"&gt;② 자동 리밸런싱과 수동 리밸런싱&lt;/h3&gt;
&lt;p&gt;파티션 리밸런싱은 자동으로 실행될까요? 아니면 수동으로 실행될까요? 이는 데이터베이스마다 조금씩 다릅니다. Elasticsearch는 자동으로 리밸런싱 하고 카우치베이스는 파티션 할당은 자동으로 제안하지만 반영되려면 관리자가 확정해야 합니다.&lt;/p&gt;
&lt;p&gt;DynamoDB는 완전 관리형 키-밸류 데이터베이스로 개발자가 신경써야 할 내용이 크게 없지만, Aurora Limitless Database는 자동으로 샤드를 분할할 수 있는 기능이 제공되지만 &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-shard-split.html#limitless-shard-split.finalize"&gt;확정 시점에는 downtime이 존재합니다&lt;/a&gt;. 왠만하면 운영 중에는 지양해야겠죠.&lt;/p&gt;
&lt;p&gt;리밸런싱은 요청 경로를 재설정해야 하고 대량의 데이터가 노드 사이를 이동해야 하기 때문에 비용이 매우 큰 연산입니다. 운영중에 처리하면 네트워크나 노드에 부하가 발생하게 되고 다른 유저의 요청 성능이 저하 될 수 있습니다. 카카오페이 에서는 샤딩이 얼마나 어려운 작업인지 회고하기도 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6b655050"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6b655050"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KwvD9JFo7NcdSaqpBvlD7Q-959cecb4a640.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6b655050"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KwvD9JFo7NcdSaqpBvlD7Q-959cecb4a640.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://tech.kakaopay.com/post/2023-aws-reinvent-1/"&gt;카카오페이 : AWS re:Invent 2023, 관심 세션을 중심으로 (1편): Aurora DB, Amplify&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;리밸런싱과 자동 장애 감지가 조합되면 예상치못한 사이드-이펙트가 있을 수 있습니다. 예를 들어 리밸런싱 과정에서 특정 노드에 부하가 생겼다면 장애 감지 시스템이 새로운 노드로 교체하려고 할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="aurora-limitless-database-도입전-고려사항"&gt;Aurora Limitless Database 도입전 고려사항&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;여기서는 제가 만약 Aurora Limitless Database를 실제로 도입한다면, 이런것들이 고려되어야 하겠다고 생각한 내용입니다. 많은 부분들이 미흡할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-분산-트랜잭션"&gt;❶ 분산 트랜잭션&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-query.distributed.html"&gt;Aurora Limitless Database는 내부적으로 샤드간 트랜잭션을 2PC로 지원합니다&lt;/a&gt;. &lt;a href="https://medium.com/rate-labs/%EA%B5%AC%EA%B8%80-spanner%EC%9D%98-paxos-%EB%B0%8F-truetime-%ED%99%9C%EC%9A%A9%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-46cb5c165de8"&gt;2PC를 수행하면서 구글 Spanner의 TrueTime과 비슷한 접근법으로 성능 향상을 꾀한듯 보입니다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;만약에 성능 때문에 분산 트랜잭션을 피하고 싶어도 회피할 수 없는 상황이 하나 있는데요. 샤드키의 경우에는 UPDATE 쿼리가 동작하지 않기 때문에 레코드 삭제와 삽입을 트랜잭션으로 묶어서 처리해야 합니다.&lt;/p&gt;
&lt;p&gt;postgres_limitless=&amp;gt; UPDATE items SET user_id = 11 WHERE user_id = 1;&lt;br&gt;
ERROR: Shard key column update is not supported&lt;br&gt;
&lt;br&gt;
postgres_limitless=&amp;gt; UPDATE items SET user_id = 11 WHERE username = &amp;lsquo;scalalang2&amp;rsquo;;&lt;br&gt;
ERROR: Shard key column update is not supported&lt;/p&gt;
&lt;h3 id="-다대다-관계-데이터"&gt;❷ 다대다 관계 데이터&lt;/h3&gt;
&lt;p&gt;다대다(Many-To-Many) 관계의 데이터는 샤딩과 키-밸류 모두 어울리지 않을 수 있습니다. 예를 들면, 유튜브에서 &lt;strong&gt;영상 A의 댓글 목록 조회(A)&lt;/strong&gt; 기능과 &lt;strong&gt;내가 작성한 댓글 목록 조회(B)&lt;/strong&gt;하는 기능이 있습니다. 이 기능은 서로 배타적인 성격을 가집니다.&lt;/p&gt;
&lt;p&gt;쿼리 A를 빠르게 처리하려면 댓글에서 샤드 키를 영상 ID로 잡아야 하고, 쿼리 B를 빠르게 처리하려면 댓글의 샤드키를 유저 ID로 잡아야하는 배타적인 상황이 연출되는데요. 어느 한 쪽의 편의를 주면 다른 기능이 거의 모든 샤드를 스캔해야 하기 때문에 매우 비효율적입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 논리대로라면 유튜브는 자신이 쓴 댓글 리스트를 조회하는 기능이 없겠다라고 추론할 수 있습니다. 실제로 유튜브 내에서는 내 댓글 조회 기능이 없고 Google 활동이라는 별도 페이지가 존재했습니다. 유튜브 내에서 댓글을 작성하면 약 10~20초 뒤에 Google 활동 페이지에 노출됩니다. 즉, 이벤트를 수신바당서 2차 DB를 구성한 것으로 보입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-485ac512"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-485ac512"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-U6WzJLnl0TKtdwe_oFAPEg-0fc49d2b3a24.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-485ac512"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-U6WzJLnl0TKtdwe_oFAPEg-0fc49d2b3a24.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Google 내 활동 페이지에 있는 나의 댓글 목록 조회 기능&lt;/p&gt;
&lt;h3 id="-read-replica-미지원"&gt;❸ Read Replica 미지원&lt;/h3&gt;
&lt;p&gt;Aurora Limitless Database는 라우터 계층이 쿼리를 받아서 처리하는데요. 그래서 Read Replica란 개념이 없습니다. 당장 공개된 정보가 부족해서 읽기 전용 복제본을 사용할 수 없어서 읽기 전용 워크로드를 처리할 수 없는 건지는 확신하긴 어렵지만 맥락을 보면 당장은 쓰기 전용 워크로드가 높은 애플리케이션에만 적합해 보입니다.&lt;/p&gt;
&lt;h3 id="-성능의-불확실성"&gt;❹ 성능의 불확실성&lt;/h3&gt;
&lt;p&gt;AWS의 유명한 엔지니어인 Marc Brooker는 &lt;a href="https://brooker.co.za/blog/2022/01/19/predictability.html"&gt;DynamoDB’s Best Feature: Predictability[22]&lt;/a&gt; 에서 DynamoDB의 최대 장점은 &lt;strong&gt;예측 가능성&lt;/strong&gt;이라고 말합니다.&lt;/p&gt;
&lt;p&gt;SQL 데이터베이스는 복잡한 요인들이 상호작용하여 성능을 정확히 예측하기 어렵습니다. 간단한 SELECT나 JOIN 쿼리조차도 다음 요소들의 영향을 받습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;인덱스 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="2"&gt;
&lt;li&gt;캐시 상태&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="3"&gt;
&lt;li&gt;키 분포&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="4"&gt;
&lt;li&gt;쿼리 옵티마이저의 실행 계획&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="5"&gt;
&lt;li&gt;동시에 실행 중인 다른 쿼리&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="6"&gt;
&lt;li&gt;DB가 가비지 컬렉션 중인지 등 (e.g. VACCUM, Purge Thread)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol start="7"&gt;
&lt;li&gt;기타 등&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 요인들 때문에 개발자가 작성한 코드가 실제 운영 환경에서 어떻게 동작할지, 그리고 제품이 성장하고 조건이 변화함에 따라 어떤 예상치 못한 결과가 발생할지 예측하기 어렵습니다.&lt;/p&gt;
&lt;p&gt;반면, DynamoDB는 &lt;strong&gt;예측 가능한 성능&lt;/strong&gt;을 제공하기 때문에 안정적이고 확장 가능한 시스템을 구축하는 데 큰 도움을 제공합니다. 관계형 데이터베이스도 확장 가능한 서비스 아키텍처를 구현할 수 있지만, 예기치 않은 부하 상황에서 시스템의 안정성을 유지하려면 상당한 추가 노력과 전문 지식이 필요합니다.&lt;/p&gt;
&lt;p&gt;DynamoDB는 처음부터 안정성과 부하 관리를 고려하도록 설계되어 있어 있어서 개발자들이 시스템의 성능과 확장성을 쉽게 예측하고 관리할 수 있게 해주며 서버 코드의 영향을 더 직관적으로 이해하고 최적화할 수 있게 해줍니다. 또한, 자동 수평확장 기능은 개발자가 시스템을 이해하는데 더 많은 에너지를 투입하는 것 보다 실제 비즈니스 가치에 집중할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;지금까지 DynamoDB와 AWS에서 새롭게 발표한 RDBMS인 Aurora Limitless를 비교 분석해보았습니다. 신뢰성 있는 시스템 구축을 위해 필요한 &lt;strong&gt;파티셔닝과 복제&lt;/strong&gt;는 두 시스템 모두 비슷한 철학을 가지고 설계했습니다.&lt;/p&gt;
&lt;p&gt;이제는 RDBMS는 수직 확장이 용이하고 DynamoDB는 수평 확장이 용이하기 때문에 성능을 원한다면 키-밸류 데이터베이스를 써야 한다는 이야기가 옛말처럼 느껴지기도 합니다. 유명한 두 채팅 시스템인 디스코드와 슬랙은 서로 다른 컨셉의 데이터베이스를 사용하기 때문에 어느 한 쪽의 선택이 무조건적으로 나쁘다고 할 순 없다고 봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-df66b814"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-df66b814"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GjYc3tPmxHblS_qlPcnXIQ-24d6d3a97323.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-df66b814"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GjYc3tPmxHblS_qlPcnXIQ-24d6d3a97323.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;저는 위에서 나열된 고려사항 중 &lt;strong&gt;❹ 성능의 불확실성&lt;/strong&gt; 부분에서 Marc Brooker님의 의견에 공감하고 있습니다. 회사에서 저의 역할은 기술적 자아실현이 아닌 게임 서버 개발이며, 게임 서버의 가장 중요한 역할은 플레이 경험을 해치지 않는 것 입니다.&lt;/p&gt;
&lt;p&gt;게임 서버의 특성상 플레이어가 본인의 정보를 조회하고 수정하는게 빈번하고 플레이어간 연결성 수준이 다소 작기 때문에 키-밸류 기반의 데이터베이스가 좋은 선택지가 될 수 있다고 느꼈습니다. Dynamo DB의 예측 가능성과 자동 확장 기능은 팀이 시스템 리서치나 최적화에 많은 공을 들이는 대신 컨텐츠 개발에 집중할 수 있도록 도와줄 것으로 기대합니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] Citus | Postgres 분산 데이터베이스 A-Z 소개&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://www.cockroachlabs.com/blog/distributed-sql-key-value-store/"&gt;The architecture of a distributed SQL database, part 1: Converting SQL to a KV store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.reddit.com/r/aws/comments/11bpfen/when_would_you_not_choose_dynamodb_where_youd/"&gt;When would you NOT choose DynamoDB where you’d typically use RDBMS?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://discord.com/blog/how-discord-stores-billions-of-messages"&gt;How Discord Stores Billions of Messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://slack.engineering/scaling-datastores-at-slack-with-vitess/"&gt;Scaling Datastores at Slack with Vitess&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://tech.kakao.com/posts/325"&gt;ADT 활용 예제1: MySQL Shard 데이터 재분배&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://engineering.linecorp.com/ko/blog/line-manga-server-side"&gt;LINE Manga 데이터베이스 샤딩 — 서버 엔지니어 편&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://techblog.woowahan.com/2687/"&gt;DB분산처리를 위한 sharding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://medium.com/p/8f2fe3dd3428"&gt;Citus | Postgres 분산 데이터베이스 A-Z 소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://tech.devsisters.com/posts/cockroachdb-in-production/"&gt;데브시스터즈 — CockroachDB in Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[11] &lt;a href="https://aws.amazon.com/ko/blogs/korea/amazon-aurora-postgresql-limitless-database-is-now-generally-available/"&gt;Amazon Aurora PostgreSQL Limitless Database 정식 출시&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[12] &lt;a href="https://steamdb.info/app/578080/charts/"&gt;Steam DB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[13] &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-reqs-limits.html"&gt;Aurora PostgreSQL Limitless Database requirements and considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[14] &lt;a href="https://aws.amazon.com/blogs/aws/amazon-aurora-postgresql-limitless-database-is-now-generally-available/"&gt;Amazon Aurora PostgreSQL Limitless Database is now generally available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[15] &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-cluster.html"&gt;Creating a DB cluster that uses Aurora PostgreSQL Limitless Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[16] &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-reqs-limits.html"&gt;Aurora PostgreSQL Limitless Database requirements and considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[17] &lt;a href="https://www.slideshare.net/slideshow/cassandra-open-source-bigtable-dynamo/1786870#3"&gt;Cassandra: Open Source Bigtable + Dynamo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[18] &lt;a href="https://vitess.io/blog/2016-06-07-distributed-transactions-in-vitess/"&gt;Distributed Transactions in Vitess&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[19] &lt;a href="https://vitess.io/docs/20.0/reference/features/two-phase-commit/"&gt;Vitess — Two-Phase Commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[20] &lt;a href="https://www.usenix.org/system/files/atc22-elhemali.pdf"&gt;Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[21] &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless-shard-split.html"&gt;Splitting a shard in a DB shard group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[22] &lt;a href="https://brooker.co.za/blog/2022/01/19/predictability.html"&gt;DynamoDB’s Best Feature: Predictability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>3D Gaussian Splatting 대충이라도 이해해보기</title><link>https://scalalang2.com/p/3d-gaussian-splatting%EC%9D%B4-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C%EC%9D%98-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84%EC%9D%84-%EB%B0%94%EA%BF%80-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-bf7bc66d7ae4/</link><pubDate>Sat, 04 Jan 2025 10:07:55 +0000</pubDate><guid>https://scalalang2.com/p/3d-gaussian-splatting%EC%9D%B4-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C%EC%9D%98-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84%EC%9D%84-%EB%B0%94%EA%BF%80-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-bf7bc66d7ae4/</guid><description>&lt;h3 id="3d-gs가-게임-개발의-새로운-패러다임이-될-수-있을까"&gt;3D GS가 게임 개발의 새로운 패러다임이 될 수 있을까&lt;/h3&gt;
&lt;p&gt;2023년 8월, 컴퓨터 그래픽스 최대 학회인 SIGGRAPH에서 발표된 하나의 논문이 학계에서 굉장히 많은 관심을 받고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1483232b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1483232b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KZ7qEQjJErplS2aAy5QITw-8c694eeb47cc.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1483232b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KZ7qEQjJErplS2aAy5QITw-8c694eeb47cc.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arxiv.org/abs/2308.04079"&gt;3D Gaussian Splatting for Real-Time Radiance Field Rendering&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이 논문에서 풀고자 하는 문제는 이미지 데이터를 3D 물체 혹은 공간으로 재구성 하는 겁니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/oeJbalGBVzw?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Gaussian Splatting 데모 영상&lt;/p&gt;
&lt;p&gt;이미지로부터 3D를 복원하는 문제는 오래전부터 연구되어온 분야입니다. 대표적으로 다양한 각도에서 촬영된 이미지로 Point Cloud를 복원하는 &lt;strong&gt;SfM(Structure from Moiton)&lt;/strong&gt;은 이스라엘 바이츠만 과학 연구소의 &lt;a href="https://en.wikipedia.org/wiki/Shimon_Ullman"&gt;Shimon Ullman&lt;/a&gt;교수님이 이미 1979년도에 발표했습니다.&lt;/p&gt;
&lt;p&gt;2010년도 부터 딥러닝 기반의 AI가 급속도로 발전하기 시작했고, 2020년에는 UC 버클리에서 3D공간을 딥러닝 모델로 해석하는 &lt;a href="https://www.matthewtancik.com/nerf"&gt;NeRF(Neural Radiance Field)&lt;/a&gt;가 등장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-af168463"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-af168463"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-svRJgJ2vLfi662mVMmntIw-39fc6c8e0c17.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-af168463"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-svRJgJ2vLfi662mVMmntIw-39fc6c8e0c17.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.matthewtancik.com/nerf"&gt;NeRF의 구조도&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NeRF는 지금까지 Google Research를 비롯한 많은 곳에서 적극적으로 연구하기 시작하면서 발전해왔는데요. 그러던 중 2023년 8월에 Gaussian Splatting이 SIGGRAPH에 발표되면서 그동안 NeRF가 주도해온 연구 패러다임을 한 번 깨고 이제는 거의 양분하는 것 처럼 보입니다.&lt;/p&gt;
&lt;p&gt;Gaussian Splatting을 특히 주목할 만한 건 &lt;strong&gt;① 이 친구는 딥러닝 모델이 아니라는 점&lt;/strong&gt;과 &lt;strong&gt;② NeRF에 비해 연산 효율이 좋아서 고해상도(1080p)로 높은 FPS로 실시간 렌더링이 가능하다는 점&lt;/strong&gt;입니다. 이번 글에서는 Gaussian Splatting에 대해 알아보고 이것이 게임 개발의 패러다임을 어떻게 바꾸게 될지 상상해보는 시간을 가져보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e899d466"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e899d466"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KrqP65f7yxWo0eHI4B9VyA-0e22f1f2d587.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e899d466"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KrqP65f7yxWo0eHI4B9VyA-0e22f1f2d587.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/"&gt;Evaluation of Gaussian Splatting&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;
우선 저는 그래픽스 전문가가 아니기 때문에 설명이 부족한 부분이 많습니다. 이 글은 저와 같이 그래픽스 관련 개념이 생소하신 분들이 3D GS를 입문할 때 보시기에 적당할 거라고 기대하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;NeRF (Neural Radiance Field)&lt;/li&gt;
&lt;li&gt;3D Gaussian Splatting&lt;/li&gt;
&lt;li&gt;Rasterization&lt;/li&gt;
&lt;li&gt;Optimization&lt;/li&gt;
&lt;li&gt;Evaluation&lt;/li&gt;
&lt;li&gt;Mesh로 변환해서 이용하기&lt;/li&gt;
&lt;li&gt;레퍼런스&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="nerf-neural-radiance-field"&gt;NeRF (Neural Radiance Field)&lt;/h2&gt;
&lt;p&gt;NeRF는 다양한 각도에서 촬영한 이미지를 학습해서 3D 공간 정보를 가지고 있는 신경망을 말합니다. 네트워크 자체는 간단한 Fully-connected Network로 구성되어 있으며 &lt;strong&gt;3D 공간의 위치 좌표(x, y, z)&lt;/strong&gt;와 &lt;strong&gt;카메라가 사물을 바라보는 각도&lt;/strong&gt;가 입력으로 주어지고 출력으로는 해당 점의 색상을 반환합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2a9d485c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2a9d485c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E9GnILb77xHaAilz5IchvQ-4602e636c051.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2a9d485c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E9GnILb77xHaAilz5IchvQ-4602e636c051.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NeRF 학습 구조도 [1]&lt;/p&gt;
&lt;p&gt;위 그림은 논문에 삽입된 그림으로 NeRF의 동작을 잘 보여주고 있습니다. 카메라에서 Ray를 보내 3D의 위치에 대한 색상을 얻어서 Volume Rendering 기법으로 최종적으로 2D 화면에 투영될 색상을 결정합니다.&lt;/p&gt;
&lt;p&gt;잘 생각해보면 &lt;strong&gt;1920 x 1080 크기&lt;/strong&gt;의 이미지는 대략 200만개의 픽셀 정보를 가지고 있습니다. 게다가 Ray가 지나가면서 공간 좌표에 대해 추론(Inference)해야 하기 때문에 렌더링 과정이 비효율적이라는 단점이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/rate-labs/%ED%8F%89%EB%B2%94%ED%95%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3d-%EC%83%9D%EC%84%B1-%EB%AA%A8%EB%8D%B8-%EC%97%B0%EA%B5%AC-%EB%A7%9B%EB%B3%B4%EA%B8%B0-dream-fusion-by-google-11c3370920ae" title="https://medium.com/rate-labs/%ED%8F%89%EB%B2%94%ED%95%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3d-%EC%83%9D%EC%84%B1-%EB%AA%A8%EB%8D%B8-%EC%97%B0%EA%B5%AC-%EB%A7%9B%EB%B3%B4%EA%B8%B0-dream-fusion-by-google-11c3370920ae"&gt;&lt;strong&gt;AI와 게임 개발 / 3D 생성 모델 연구 맛보기&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;NeRF에서 DreamFusion 까지&lt;/em&gt;medium.com&lt;/a&gt;&lt;a href="https://medium.com/rate-labs/%ED%8F%89%EB%B2%94%ED%95%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3d-%EC%83%9D%EC%84%B1-%EB%AA%A8%EB%8D%B8-%EC%97%B0%EA%B5%AC-%EB%A7%9B%EB%B3%B4%EA%B8%B0-dream-fusion-by-google-11c3370920ae"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="3d-gaussian-splatting"&gt;3D Gaussian Splatting&lt;/h2&gt;
&lt;p&gt;게임을 좋아하시는 분들은 3D 그래픽을 폴리곤으로 이루어진 Mesh로 표현한다는 사실을 아실겁니다. 3D 데이터를 표현하는 방법 중에 3차원 공간 전체를 점들의 집합으로 표현하는 &lt;strong&gt;포인트 클라우드(Point Cloud)&lt;/strong&gt;가 있습니다. 이는 자율주행차의 눈 역할을 하는 라이다(LiDAR) 센서가 세상을 인식하는 방식이기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-306f7960"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-306f7960"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DhyvTuotg-GoK1gDWmPMDg-b01cbd8fff36.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-306f7960"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DhyvTuotg-GoK1gDWmPMDg-b01cbd8fff36.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;출처 : &lt;a href="https://ouster.com/downloads/sample-lidar-data"&gt;https://ouster.com/downloads/sample-lidar-data&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이미지로부터 포인트 클라우드와 실제 카메라의 위치를 계산하는 방법은 오래전부터 연구되어 왔습니다. OpenMVG[2] 오픈소스를 이용하면 이 과정을 쉽게 구현할 수 있습니다. 하지만 포인트 클라우드는 공간 표현이 불연속적이며, 엘리어싱 및 holes를 만드는 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3D GS(Gaussian Splatting)&lt;/strong&gt;은 포인트 클라우드를 확장한 개념으로 각 좌표에 존재하는 포인트는 점이 아닌 3차원 가우시안 분포를 가집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-67e3320e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-67e3320e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-mkRwZp3r5wNQKxl1wD6krA-97ab61cb7c62.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-67e3320e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-mkRwZp3r5wNQKxl1wD6krA-97ab61cb7c62.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source: &lt;a href="https://en.wikipedia.org/wiki/Gaussian_function"&gt;(1)&lt;/a&gt;, &lt;a href="https://math.stackexchange.com/questions/2580887/is-there-any-graphical-explanation-of-multivariate-gaussian"&gt;(2)&lt;/a&gt;, &lt;a href="https://www.researchgate.net/figure/sualization-of-a-3D-Gaussian-model-a-Uncertainty-ellipsoid-for_fig5_231212225"&gt;(3)&lt;/a&gt;, 아이디어 출처 : &lt;a href="https://xoft.tistory.com/49"&gt;xoft 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그리고 포인트 클라우드 처럼 여러 좌표에 값을 가지고 있기 때문에 아래 그림과 닮은 데이터를 얻게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b95031fd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b95031fd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-URxFwtUHiz8YCK_NZXypRA-ba6733cc2d77.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b95031fd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-URxFwtUHiz8YCK_NZXypRA-ba6733cc2d77.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3차원 공간에 3개의 3D Gaussian이 존재하고 있다.&lt;/p&gt;
&lt;p&gt;3D GS 내용을 본격적으로 탐구하기 전에 이렇게 표현된 결과물이 렌더링된 결과를 보겠습니다. 아래 그림은 &lt;strong&gt;2024년 게임 개발자 컨퍼런스(GDC)&lt;/strong&gt;에서 사진으로 사물을 스캔하는 앱인 &lt;a href="https://apps.apple.com/us/app/kiri-engine-3d-scanner-lidar/id1577127142"&gt;KIRI Engine&lt;/a&gt; 개발사 발표의 한 장면입니다[3].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-881ac717"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-881ac717"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MMm2vZBCJUijvzUsjkGWVw-545708db04f2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-881ac717"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MMm2vZBCJUijvzUsjkGWVw-545708db04f2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 그림은 Mesh와 3D GS로 표현된 객체를 확대했을 때의 차이를 보여줍니다. Mesh는 폴리곤 면이 드러나는 반면, 3D GS는 타원체의 집합으로 나타납니다. 본 논문은 사진을 입력 데이터로 사용해 이러한 3차원 객체를 효과적으로 표현하는 타원체 집합을 찾는 것을 목표로 합니다.&lt;/p&gt;
&lt;p&gt;처음에 3D GS는 딥러닝 방식이 아니라고 언급했습니다. 대신, &lt;strong&gt;경사 하강법(Gradient Descent)&lt;/strong&gt;을 통해 파라미터를 최적화하여 사물을 잘 표현하는 가우시안을 얻습니다. 경사 하강법은 딥러닝 이전부터 존재했던 최적화 이론의 기법이므로 논문에서는 이 과정을 &lt;strong&gt;‘학습’&lt;/strong&gt;이 아닌 &lt;strong&gt;‘최적화’&lt;/strong&gt; 단계로 표현합니다&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a4802364"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a4802364"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-btuSw3GuCn7Y1wgdvUG_OQ-053d73c01103.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a4802364"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-btuSw3GuCn7Y1wgdvUG_OQ-053d73c01103.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3D GS 전체 구조도 [4]&lt;/p&gt;
&lt;p&gt;손실 함수를 정의하고, 오차를 미분하여 파라미터를 조정하는 방식은 딥러닝의 기본 원리와 유사하기 때문에 완전히 낯설지는 않으실 거라고 생각합니다.&lt;/p&gt;
&lt;p&gt;카메라가 방향으로 보일 법한 이미지를 3D GS 과정을 거쳐서 만들고 이를 원본 이미지와 비교하는 손실 함수를 정의한 뒤 경사하강법으로 Loss가 최적화 되는 3D Gaussian을 찾습니다.&lt;/p&gt;
&lt;h2 id="rasterization"&gt;Rasterization&lt;/h2&gt;
&lt;h3 id="-가우시안-도형을-2차원으로-투영시키기"&gt;① 가우시안 도형을 2차원으로 투영시키기&lt;/h3&gt;
&lt;p&gt;3차원 가우시안 정보는 (0, 0)을 원점으로 가지는 &lt;strong&gt;공분산 행렬 Σ&lt;/strong&gt;로 표현합니다. 정규분포 &lt;em&gt;N&lt;/em&gt;(&lt;em&gt;μ&lt;/em&gt;, &lt;em&gt;σ²&lt;/em&gt;)의 파라미터가 평균과 분산을 가지는데, 차원이 2개 이상인 가우시안 분포에서는 공분산으로 표현합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;plt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;from scipy.stats import multivariate_normal&lt;br&gt;
\&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;[0.5, 2, 0.2],&lt;br&gt;
[0.3, 0.2, 1]])&lt;br&gt;
&lt;br&gt;
num_points = 1000&lt;br&gt;
points = np.random.multivariate_normal(mean, cov, num_points)&lt;br&gt;
pdf_values = multivariate_normal.pdf(points, mean=mean, cov=cov)&lt;br&gt;
&lt;br&gt;
&amp;hellip;&lt;br&gt;
&lt;br&gt;
plt.show()&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0ea8e85d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0ea8e85d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bLpt2sDCqmiSBEQ0rN6ATQ-05dd46dc670a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0ea8e85d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bLpt2sDCqmiSBEQ0rN6ATQ-05dd46dc670a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="공분산-행렬-σ-로-시각화한-3차원-가우시안-데이터"&gt;공분산 행렬 Σ 로 시각화한 3차원 가우시안 데이터&lt;/h3&gt;
&lt;p&gt;우리는 이제 이 가우시안 정보로 2차원으로 투영시켜서 이미지로 만든다음 원본과 비교해서 손실 값을 계산해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4e54cd3b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4e54cd3b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ntFNnxJM_mSGGecXxypK_w-54de7a953279.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4e54cd3b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ntFNnxJM_mSGGecXxypK_w-54de7a953279.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;수식은 2001년에 발표된 &lt;strong&gt;EWA Volume Splatting[5]&lt;/strong&gt; 논문에서 소개된 내용입니다. 3D GS 논문은 이를 사용했다고만 간단히 언급했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3d673650"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3d673650"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7S27cZJZIiTklH1rtYNkPg-68242ee1fcf7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3d673650"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7S27cZJZIiTklH1rtYNkPg-68242ee1fcf7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : EWA Volume Splatting [5]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;W :&lt;/strong&gt; (0,0)을 원점으로 가진 가우시안 분포를 카메라의 위치를 원점으로 하는 곳으로 선형변환 하는 행렬입니다.&lt;/li&gt;
&lt;li&gt;J : &lt;strong&gt;자코비안(Jacobian)&lt;/strong&gt;행렬로 비선형 변환된 값을 국소적으로 선형 변환으로 근사하는 역할을 합니다. 자세한 내용은 공돌이의 &lt;a href="https://angeloyeo.github.io/2020/07/24/Jacobian.html#google_vignette"&gt;&lt;strong&gt;수학 정리 노트[6]&lt;/strong&gt;&lt;/a&gt;에 소개되어 있습니다.&lt;/li&gt;
&lt;li&gt;마지막 W^T 및 J^T는 공분산 행렬의 Key Property중 하나로 결과값의 대칭성을 유지하는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a5698f2b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a5698f2b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RkXfHpDj-AqClzkal6j48w-fae0dfa54b05.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a5698f2b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RkXfHpDj-AqClzkal6j48w-fae0dfa54b05.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Jacobian 행렬의 기하학적 의미&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Jacobian term이 붙은게정확히 어떤 의미를 가지는지는 제 역량이 부족해서 제대로 이해하지 못했습니다. 수식의 유도 과정은 &lt;a href="https://qiita.com/scomup/items/f8632151712828e9625d"&gt;다음 글[7]&lt;/a&gt;에서 더 자세히 설명되어 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이렇게 &lt;strong&gt;변환된 행렬 Σ’&lt;/strong&gt; 에 대해 3번째 행과 열을 버린 &lt;strong&gt;2 x 2 행렬&lt;/strong&gt;을 공분산으로 사용해서 2차원 가우시안으로 이용합니다.&lt;/p&gt;
&lt;h3 id="-공분산-행렬을-타원체로-다루기"&gt;② 공분산 행렬을 타원체로 다루기&lt;/h3&gt;
&lt;p&gt;공분산 행렬은 항상 &lt;strong&gt;정부호 행렬(고윳값이 항상 양수)&lt;/strong&gt;이어야 합니다. 아래 코드를 보면 음수의 고윳값을 가지는 행렬은 애초에 함수를 통과하지 못하는 걸 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;import numpy as np&lt;br&gt;
\&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;eigenvalues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eigvals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cov&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;print(egeinvalues) # [3, -1]&lt;br&gt;
&lt;br&gt;
np.random.multivariate_normal(mean=[0, 0], cov=cov, size=1000)&lt;br&gt;
# Error : covariance is not symmetric positive-semidefinite.&lt;/p&gt;
&lt;p&gt;3D GS는공분산 행렬의 각 원소들을 &lt;strong&gt;Graident Descent&lt;/strong&gt;로 학습하기 때문에 자칫 &lt;strong&gt;대칭성&lt;/strong&gt;이나 &lt;strong&gt;정부호 행렬의 성질&lt;/strong&gt;을 잃어버릴 수 있습니다. 3D GS는 이 문제를 해결하기 위해 직관적인 방법을 사용하는데 가우시안을 타원체의 도형으로 취급하는 겁니다. 이 타원체는 &lt;strong&gt;Rotation Matrix(R)&lt;/strong&gt;와 &lt;strong&gt;Scaling Matrix(S)&lt;/strong&gt;로 표현합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-846db801"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-846db801"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BsJK23EzlEJF8TKUyoyq1w-c217d093109c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-846db801"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BsJK23EzlEJF8TKUyoyq1w-c217d093109c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;회전을 표현하는 쿼터니언과 Scaling Matrix로 타원체를 표현하고 있다.&lt;/p&gt;
&lt;p&gt;R과 S두 행렬을 가지고 &lt;strong&gt;행렬 Σ&lt;/strong&gt;는 아래 수식으로 표현합니다. 위에서 Σ를 어떻게 2차원에 투영시키는지 다루었었는데요. 그 때 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-39b8b8d8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-39b8b8d8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VKtUeWXx8eBWfoXDQISCvw-9044301d2638.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-39b8b8d8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VKtUeWXx8eBWfoXDQISCvw-9044301d2638.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;수식을 잘 보면 &lt;strong&gt;주성분 분석(PCA)&lt;/strong&gt;에서 공분산 행렬의 &lt;strong&gt;고윳값 분해&lt;/strong&gt;와 의미가 같다는 걸 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-376b1231"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-376b1231"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iXMjeQyA6nT6W-Bg90qKxA-e9e7721c4c4b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-376b1231"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iXMjeQyA6nT6W-Bg90qKxA-e9e7721c4c4b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;(2)는 고유값 분해의 정의입니다. 고유벡터를 모아둔 행렬 V는 A가 대칭행렬인 경우에는 그 역행렬이 &lt;strong&gt;V^T&lt;/strong&gt;입니다. 위 수식에서 S는 대각 행렬이기 때문에 SS^T는 각 원소에 제곱한 값입니다.&lt;/p&gt;
&lt;p&gt;다시 말하면 위 수식은 타원체의 모양을 잘 나타내는 &lt;strong&gt;주축 방향(= 고유 벡터)&lt;/strong&gt;와 주축 방향으로의 &lt;strong&gt;타원체의 크기(=고윳값)&lt;/strong&gt;를 결정한다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2f48536f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2f48536f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m9PFq5UkHsssB9IXc7ekNQ-d4e9b6d9e8b4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2f48536f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m9PFq5UkHsssB9IXc7ekNQ-d4e9b6d9e8b4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://angeloyeo.github.io/2019/07/27/PCA.html#google_vignette"&gt;source : 주성분 분석(PCA) — 공돌이의 수학 노트&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;쿼터니언&lt;/strong&gt;&lt;br&gt;
물체의 회전이라는 건 각 Pitch, Roll, Yaw 각 축에 대해 회전한 정도로 표현할 수 있는데요. 이를 직접 사용하는 오일러 각은 연산도 비효율적이며 짐벌 락(Gimbal Lock)현상이 발생하기 때문에 게임 개발할 때는 흔히 회전을 &lt;strong&gt;쿼터니언&lt;/strong&gt;으로 변환시켜서 사용하곤 합니다. 위 수식에서 R은 쿼터니언에서 사용하는 행렬로 i, j, k, r 4개의 변수를 가지고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;유명한 &lt;a href="https://www.youtube.com/watch?v=d4EgbgTm0Bg"&gt;&lt;em&gt;수학 채널 3Blue1Borwn&lt;/em&gt;&lt;/a&gt; 에서 쿼터니언에 대해 다룬 적이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c9540c01"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c9540c01"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LWzqte3Cf50nnnOLD66IWw-89c9d5ba1941.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c9540c01"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LWzqte3Cf50nnnOLD66IWw-89c9d5ba1941.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[&lt;a href="https://www.researchgate.net/figure/a-Pitch-yaw-and-roll-angles-of-an-aircraft-with-body-orientation-O-u-v-original_fig7_348803228"&gt;source&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;③ Spherical Harmonics | 색상 표현하기 &lt;br&gt;
사물은 바라보는 방향에 따라 보이는 색상이 다릅니다.&lt;strong&gt; 이를 (2)에서 정의한 타원체에 표현시키기 위해서 &lt;/strong&gt;구면 조화 함수(Spherical Harmonics)를 사용하는데요. 이는 언리얼 엔진에서 광원 효과를 주는 볼류메트릭 라이트맵[8]의 원리이기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6da666f8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6da666f8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JCxufrbXs9iYMpuDJg1Fag-7a8e6850d600.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6da666f8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JCxufrbXs9iYMpuDJg1Fag-7a8e6850d600.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;흠.. 대학원 양자역학이요..?&lt;/p&gt;
&lt;p&gt;먼저 X축, Y축으로 이루어진 평면 좌표계 대신 구면좌표계라는 개념이 있습니다. 이 좌표계는 두개의 각도 &lt;strong&gt;θ, Ψ&lt;/strong&gt;로 구체에서 특정 방향의 값을 표현하는데요. 이 때 반지름은 무시합니다. &lt;strong&gt;구면 조화 함수(Spherical Harmonics)&lt;/strong&gt;는 이 두개의 각도를 입력받아서 특정 값을 반환하는 함수입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e391bc2e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e391bc2e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3Bw1JzsW50nROyHY6o-0ng-fae86f855abf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e391bc2e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3Bw1JzsW50nROyHY6o-0ng-fae86f855abf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : &lt;a href="https://mathworld.wolfram.com/SphericalCoordinates.html"&gt;Spherical Coordinates&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-95aaeb08"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-95aaeb08"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DkZNXw8xxshdlEhH7-thaA-4e456ca81c31.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-95aaeb08"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DkZNXw8xxshdlEhH7-thaA-4e456ca81c31.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Spherical_harmonics"&gt;https://en.wikipedia.org/wiki/Spherical_harmonics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위 함수가 구면 조화 함수입니다. 파라미터 중에 l과 m은 각운동량 양자수니 자기 양자수니 하는 알 수 없는 말을 합니다. 허허.. &lt;strong&gt;우리는 저 수식은 머릿속에서 지워버리고 결과만 살펴보겠습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-24252e2f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-24252e2f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dkbctHf-9V8eI1H1Sj4bVA-c734b9594237.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-24252e2f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dkbctHf-9V8eI1H1Sj4bVA-c734b9594237.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : &lt;a href="https://www.researchgate.net/figure/Real-part-of-a-set-of-spherical-harmonics-mapped-to-the-surface-of-a-sphere-The-colour_fig1_345372557"&gt;Efficient HRTF Representation Using Compact Mode HRTFs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위 구면체들의 집합은 구면조화 함수의 l과 m에 따른 변화된 모습입니다. 우측 그림을 보면 제가 파랑색에 화살표로 표시했는데요. 이는 이 함수의 결과값이 파랑색이라는게 아니라 비슷한 값을 반환한다는 의미입니다.&lt;/p&gt;
&lt;p&gt;최종적으로 색상을 결정할 때는 이 구면조화 함수들의 결과값에다가 가중치를 곱해서 합산한 값이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1d2417b7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1d2417b7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3SsXUEnpSZzFV8qs4xH3mA-24a0de700efa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1d2417b7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3SsXUEnpSZzFV8qs4xH3mA-24a0de700efa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;완전히 같진 않지만 의사 코드를 작성해보면 아래와 비슷합니다. 실제 논문 저자들이 &lt;a href="https://github.com/graphdeco-inria/gaussian-splatting/blob/54c035f7834b564019656c3e3fcc3646292f727d/utils/sh_utils.py#L57-L112"&gt;구현한 코드&lt;/a&gt;를 보면 L=3 까지만 사용하고 있습니다. 위 변수에서 C1, C2, …, Cn을 &lt;strong&gt;SH Coefficient&lt;/strong&gt; 라고 부릅니다.&lt;/p&gt;
&lt;p&gt;import numpy as np&lt;br&gt;
\&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spherical_harmonics_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coefficients&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Y00&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Y1m1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Y10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Y11&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;br&gt;
color = (&lt;br&gt;
coefficients[0] * Y00 +&lt;br&gt;
coefficients[1] * Y1m1 +&lt;br&gt;
coefficients[2] * Y10 +&lt;br&gt;
coefficients[3] * Y11&lt;br&gt;
)&lt;br&gt;
&lt;br&gt;
return color&lt;/p&gt;
&lt;h3 id="그런데-이걸-왜-이렇게-하는-걸까요"&gt;그런데 이걸 왜 이렇게 하는 걸까요?&lt;/h3&gt;
&lt;p&gt;푸리에 변환을 들어보신 분들은 임의의 신호를 주기함수들의 합으로 분해할 수 있다는 걸 아실겁니다. 푸리에 변환은 서로 직교하는 기저 함수들의 선형 결합으로 표현합니다. 어떤 벡터A 를 기저 벡터의 선형 결합으로 표현하는 것과 같은 느낌입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-514ff2b8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-514ff2b8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Od2klXam3JO29J-NB_2-3g-95f60b54bf05.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-514ff2b8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Od2klXam3JO29J-NB_2-3g-95f60b54bf05.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;벡터를 기저들의 선형 결합으로 표현하기&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-397696f5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-397696f5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-NYZUDEHsc6oe7OV0-dc8f896f0601.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-397696f5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-NYZUDEHsc6oe7OV0-dc8f896f0601.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.nti-audio.com/ko/%EC%A7%80-%EC%9B%90/%EC%B8%A1%EC%A0%95-%EB%85%B8%ED%95%98%EC%9A%B0/%EB%B9%A0%EB%A5%B8-fourier-%EB%B3%80%ED%99%98-fft"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위키피디아의 설명을 보면 구면조화함수는 3차원 공간에서 구면 표면 위의 함수를 표현하는 데 사용되는 &lt;strong&gt;직교 기저 함수 집합&lt;/strong&gt;이라고 설명하고 있습니다. 이는 푸리에 급수와 유사한 방식으로 작동하여, 구면 위의 복잡한 함수를 더 단순한 기저 함수들의 가중 합으로 분해할 수 있게 해준다고 합니다[9].&lt;/p&gt;
&lt;p&gt;3차원 물체가 색상을 결정하는 많은 독립적인 변수들이 있을거에요. 추상적으로 해석하면 &lt;strong&gt;광원의 위치, 그림자, 빛을 반사하는 정도, 표면 특성&lt;/strong&gt; 등이 있을텐데 이것이 색상을 결정하는 정도를 가중치로 매겨서 합산한다고 이해하면 좋을 것 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since the spherical harmonics form &lt;strong&gt;a complete set of orthogonal functions and thus an orthonormal basis, &lt;/strong&gt;each function defined on the surface of a sphere can be written as a sum of these spherical harmonics. This is similar to periodic functions defined on a circle that can be expressed as a sum of circular functions (sines and cosines) via Fourier series.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="-tile-based-rasterization"&gt;④ Tile-based Rasterization&lt;/h2&gt;
&lt;p&gt;Rasterization은 그래픽스에서 벡터 정보를 픽셀로 변환하는 과정을 말합니다. 위에서 우리는 &lt;strong&gt;(1) 타원체를 2차원으로 투영시키는 방법, &lt;/strong&gt;(2) 구면체 도형을 표현하는 방법&lt;strong&gt;과 &lt;/strong&gt;(3) 색상을 표현하는 방법을 개별적으로 다루었다면 이제 이 정보들을 모아서 최종적으로 2D 픽셀 정보를 결정하는 방법을 다룹니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-065fc873"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-065fc873"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sjiEzXs3hhYJ1qVfHbXYcg-c86e42645799.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-065fc873"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sjiEzXs3hhYJ1qVfHbXYcg-c86e42645799.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CullGaussian&lt;/strong&gt; 함수는 카메라 시야에 들어오는 정보 이외의 가우시안 정보들을 제거합니다. view frustum 보면 너무 가깝거나 완전히 먼 가우시안 정보도 함께 제거합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ScreenspaceGaussians(M, S, V)&lt;/strong&gt;는 위에서 (1)에서 다루었던 3차원 가우시안을 2차원으로 투영시킨 시킨 결과값 M’, S’을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CreateTiles(w,h)&lt;/strong&gt;는 이미지를 16x16의 타일로 나누는데요. 이는 GPU에서 병렬처리하는 단위로 사용하기 위함입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DuplicateWithKeys, SortByKeys&lt;/strong&gt;는 가우시안 정보를 깊이(depth)를 기준으로 정렬해서 가까이 있는 사물에 더 많은 색상을 반영하도록 합니다.&lt;/li&gt;
&lt;li&gt;마지막으로 &lt;strong&gt;BlendInOrders&lt;/strong&gt; 에서 가우시안 집합을 블렌딩 해서 픽셀의 색상을 결정하는데요. 이는 NeRF에서 볼류메트릭 렌더링의 원리와 유사합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-10de9da0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-10de9da0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CvVCRRSnVAdcIzIK5iYEHQ-1455ff08c096.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-10de9da0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CvVCRRSnVAdcIzIK5iYEHQ-1455ff08c096.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : Stanford Seminar — &lt;a href="https://www.youtube.com/watch?v=eHr_jA8HnkA"&gt;Perception-Rich Robot Autonomy with Neural Environment Models&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="optimization"&gt;Optimization&lt;/h2&gt;
&lt;p&gt;이번에는 가우시안 정보의 2D 이미지 변환에서 &lt;strong&gt;학습 최적화 단계&lt;/strong&gt;를 다룹니다. 딥러닝 기본 개념을 아신다면 &lt;strong&gt;Gradient Descent 원리&lt;/strong&gt;에 익숙하실 것이므로, 이 부분을 더 쉽게 이해하실 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b8ad60a4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b8ad60a4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-l9oagxtY-w8PbLQJDP1P7A-2c1884253a30.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b8ad60a4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-l9oagxtY-w8PbLQJDP1P7A-2c1884253a30.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SfM(Structure from Motion)&lt;/strong&gt;은 전통적으로 사용하는 이미지에서 포인트 클라우드를 얻어내는 기법입니다. 여기서 얻어낸 포인트 정보를 초기 좌표 정보로 활용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;InitAttributes()&lt;/strong&gt; 에서 &lt;strong&gt;가우시안 정보 Σ&lt;/strong&gt; (회전 행렬 R, 크기 변환 행렬 S)와 색상을 결정할 &lt;strong&gt;SH Coefficient 계수, &lt;/strong&gt;그리고 &lt;strong&gt;불투명도 A&lt;/strong&gt;를 임의를 초기화 합니다. 이 정보는 아래의 &lt;strong&gt;Adam Optimizer&lt;/strong&gt;에 의해 전부 학습될 겁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금부터 굉장히 집중해서 봐야할 내용이 있는데요. &lt;strong&gt;IsRefinementIteration&lt;/strong&gt; 조건문으로 특정 주기마다 &lt;strong&gt;① Pruning&lt;/strong&gt;과 &lt;strong&gt;② Densification&lt;/strong&gt; 과정을 진행합니다. 논문에서는 이 주기를 단순히 100번째 iteration마다 실행합니다.&lt;/p&gt;
&lt;h3 id="-pruning"&gt;① Pruning&lt;/h3&gt;
&lt;p&gt;지나치게 투명해서(a &amp;lt; ε) 정보로써 의미가 없거나, 지나치게 큰 경우에는 가우시안을 삭제합니다. 논문에서는 ε값을 1/255로 설정했습니다.&lt;/p&gt;
&lt;h3 id="-densification"&gt;② Densification&lt;/h3&gt;
&lt;p&gt;Densification은 말 그대로 밀집도 있게 만드는 과정입니다. 아래는 기하학적 특징을 잘 담지 못하는 &lt;strong&gt;Under Reconstruction&lt;/strong&gt; 과 가우시안의 영역이 너무 큰 &lt;strong&gt;Over Reconstruction&lt;/strong&gt;의 상황을 보여주는데요. 연구자들은 이 두 상황을 모두 가우시안의 위치에 대한 Loss값의 Gradient인 &lt;strong&gt;▽p&lt;/strong&gt; 로 찾을 수 있다고 보았습니다.&lt;/p&gt;
&lt;p&gt;이 값이 정해진 임계값 $τ_p(0.0002)$ 보다 크면 가우시안들이 최적화되지못하고 위치를 계속 변경하려고 한다고 봤습니다. 그래서, 크기에 대한 임계값 $τ_s$보다 크면 가우시안을 1.6x로 줄인 다음 분할하고 작다면 하나 더 복제합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8319db26"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8319db26"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QY7zW7eG7uIRz5YILUGMFQ-9254c23c8ef9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8319db26"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QY7zW7eG7uIRz5YILUGMFQ-9254c23c8ef9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : [4]&lt;/p&gt;
&lt;h2 id="evaluation"&gt;Evaluation&lt;/h2&gt;
&lt;p&gt;3D GS는 이미지의 유사도를 평가 지표에 있어서는 3개의 데이터셋에 대해 대부분 SOTA의 성능을 보여주었습니다. 여기서 더욱 눈여겨 볼 사실은 Mip-NeRF360은 학습 시간이 거의 2일에 걸쳐서 하는 반면, 3D GS는 7K iteration에서는 6분, 30K에서는 30&lt;del&gt;40분 정도만 소요했습니다. 다만, 가우시안이 보통 1M&lt;/del&gt;5M개 정도 형성된다고 하고 GPU를 병렬로 쓰기 때문에 메모리를 상대적으로 많이 사용합니다. 아마 이런 부분은 후속 연구가 진행되면서 빠르게 개선되지 않을까 싶습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f640aacd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f640aacd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1RzsjWP_e-v6p4DbseumTw-c3e172ba8af0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f640aacd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1RzsjWP_e-v6p4DbseumTw-c3e172ba8af0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a7f28ece"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a7f28ece"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-q0vbfO2hMqn_y56C-SRUWw-df23c44f198d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a7f28ece"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-q0vbfO2hMqn_y56C-SRUWw-df23c44f198d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;source : [4]&lt;/p&gt;
&lt;p&gt;다음으로는 논문에서 저자들이 도입한 기법들이 꽤 많은데요. 각각의 기법을 비활성화 한 상태로 실험을 진행했을 때 본인들이 내린 결정들이 좋은 결정들이었는지는 평가합니다. 평가에 사용된 메트릭은 PSNR 점수 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-550e163e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-550e163e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j3tI_uDo1odmwhz_K78qQw-78ab10d5c3e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-550e163e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j3tI_uDo1odmwhz_K78qQw-78ab10d5c3e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Random Init은 &lt;strong&gt;SfM(Structure from Motion)&lt;/strong&gt;에서 얻은 포인트 클라우드 정보를 사용하지 않고 무작위로 생성한 결과입니다.&lt;/li&gt;
&lt;li&gt;No-Split, No-Clone은 Densification 전략을 의미합니다.&lt;/li&gt;
&lt;li&gt;No-SH는 Sphericial Harmonics를 사용하지 않는 경우입니다.&lt;/li&gt;
&lt;li&gt;Isotropic은 공분산에서 모든 분산의 값이 동일한 경우를 말하는데요. 공분산 행렬에서 모든 파라미터를 최적화 대상으로 결정하는게 옳은 판단이었는지 확인했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-be9a5657"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-be9a5657"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VDMotVo2jyX66zIPUMWkBg-9464f0e001b2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-be9a5657"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VDMotVo2jyX66zIPUMWkBg-9464f0e001b2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;논문에서는 이 내용을 더욱 정성적으로 설명해줍니다. 예를 들어 배경이 뭐가 어떻고, 사물이 어떻게 보이고 등을 사진과 함께 설명하고 있는데요. 여기서는 결론만 말하면 모든 결정사항을 다 적용한 Full 버전이 가장 성능이 좋았습니다.&lt;/p&gt;
&lt;h3 id="mesh로-변환해서-사용하기"&gt;Mesh로 변환해서 사용하기&lt;/h3&gt;
&lt;p&gt;실제로 몇몇 영상&lt;a href="https://www.youtube.com/watch?v=xdDzChfFY_A"&gt;(1)&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=4xTEyz9bx5E"&gt;(2)&lt;/a&gt;과 &lt;a href="https://github.com/xverse-engine/XV3DGS-UEPlugin"&gt;구현체&lt;/a&gt;를 보면 &lt;strong&gt;언리얼 엔진&lt;/strong&gt; 혹은 &lt;strong&gt;유니티&lt;/strong&gt;에서 렌더링 할 수 있도록 플러그인들은 개발이 된 것 같습니다. 그래도 사실 게임 개발에서는 Mesh가 필요합니다.&lt;/p&gt;
&lt;p&gt;AI들이 이동할 때 최적 경로 계산에 활용되는 &lt;strong&gt;(1) Navigation Mesh&lt;/strong&gt;이 없고 (2) 맵이 넓은 경우에는 멀리있는 사물은 대충 표현하고 카메라와 가까이 있는 건 디테일 하게 표현하는 LOD 기법들이 사용되곤 하는데요. 3D GS로는 사물의 형태를 유지하면서 미세하게 조정하기에는 어려워 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4dc48532"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4dc48532"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jgZ_JVuN1F-kefRN7bN9IA-0ddf4fcfa886.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4dc48532"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jgZ_JVuN1F-kefRN7bN9IA-0ddf4fcfa886.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://3dstudio.co/3d-lod-level-of-detail/"&gt;[source]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(3) 물리엔진에 의한 충돌이나 레이 캐스팅, 그리고 (4) 물리 기반 렌더링 기법등을 사용할 수 없기 때문에 3D GS 자체로는 그대로 사용하기 어렵고 Mesh로 한 번 변환시켜 줘야 하는데요.&lt;/p&gt;
&lt;p&gt;이 글을 작성하는 시점에서 가장 최근 연구를 찾아보니 &lt;strong&gt;CVPR 2024&lt;/strong&gt;에서 3D GS를 Mesh로 SuGaR[1]가 있습니다. 이걸 이용하면 게임 엔진에서 사용해볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-910026a4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-910026a4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xUq1dMOlvU7uUHj88qMgwg-d2dc2057fefc.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-910026a4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xUq1dMOlvU7uUHj88qMgwg-d2dc2057fefc.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="마무리"&gt;마무리&lt;/h3&gt;
&lt;p&gt;이 글에서는 Gaussian Splatting이라는 혁신적인 기술을 소개했습니다. 비록 학계에서는 이미 상당한 주목을 받은 주제라서 이미 발행된 좋은 아티클들이 많았습니다. 조금 늦었지만, 그래도 이 글이 저와 같이 그래픽스 분야가 생소하신 분들에게 새로운 시각으로 내용을 이해하는데 도움을 줄 수 있길 기대합니다.&lt;/p&gt;
&lt;p&gt;제 역량이 된다면 다음 글에서는 실제로 이를 사용해서 게임 엔진에 올려보는 튜토리얼을 준비해보겠습니다.&lt;/p&gt;
&lt;h2 id="레퍼런스"&gt;레퍼런스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://www.matthewtancik.com/nerf"&gt;Representing Scenes as Neural Radiance Fields for View Synthesis — ECCV 2020&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://github.com/openMVG/openMVG"&gt;open Multiple View Geometry library. Basis for 3D computer vision and Structure from Motion.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.youtube.com/watch?v=zTwHmxfKvOs"&gt;Use 3D Gaussian Splatting In Game Development, What The Internet Doesn’t Tell You | GDC Talk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/"&gt;3D Gaussian Splatting for Real-Time Radiance Field Rendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://www.cs.umd.edu/~zwicker/publications/EWAVolumeSplatting-VIS01.pdf"&gt;EWA Volume Splatting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://angeloyeo.github.io/2020/07/24/Jacobian.html#google_vignette"&gt;자코비안 행렬의 기하학적 의미 — 공돌이의 수학 정리 노트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://qiita.com/scomup/items/f8632151712828e9625d"&gt;Reconstruct 3D from images! Implementing bundle adjustment in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/volumetric-lightmaps-in-unreal-engine"&gt;Unreal Engine — Volumetric Lightmap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://en.wikipedia.org/wiki/Spherical_harmonics"&gt;Spherical Harmonics — Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://anttwo.github.io/sugar/"&gt;SuGaR: Surface-Aligned Gaussian Splatting for Efficient 3D Mesh Reconstruction and High-Quality Mesh Rendering : CVPR 2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>스팟 인스턴스를 활용한 클라우드 비용 절감 전략</title><link>https://scalalang2.com/p/%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EC%A0%84%EB%9E%B5-f2e8bff1c932/</link><pubDate>Tue, 19 Nov 2024 03:05:34 +0000</pubDate><guid>https://scalalang2.com/p/%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EC%A0%84%EB%9E%B5-f2e8bff1c932/</guid><description>&lt;p&gt;AWS로부터 시작된 클라우드 컴퓨팅의 개념이 등장한 이후부터 기업은 꾸준히 리소스를 온-프레미스에서 클라우드로 마이그레이션 하고 있습니다. 이에 따라, 클라우드 비용 절감은 기업에서 중요하게 생각하는 토픽입니다. 이 비용에 가장 크게 기여하는 항목 중 하나는 컴퓨팅 리소스인데요. AWS, Azure, GCP 등 대부분의 클라우드 사업자들은 두 가지 인스턴스 타입을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-50862b93"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-50862b93"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-eminFo7La1Kx--6GpCUuCg-d44bd72d714e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-50862b93"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-eminFo7La1Kx--6GpCUuCg-d44bd72d714e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;온-디맨드 인스턴스&lt;/strong&gt;는 가용성이 높은 타입으로, 엔드-유저의 요청을 직접 서빙하는 서버에서 많이 이용합니다. 반면, &lt;strong&gt;스팟 인스턴스&lt;/strong&gt;는 데이터센터에서 놀고있는 컴퓨팅 자원을 임대해주는 개념으로 온-디맨드 서비스에 비해 3배에서 11배 저렴한 가격으로 이용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다만, 비선점 자원이기 때문에 다른 곳에서 온-디맨드 서비스를 요구하면 내가 이용하던 스팟 인스턴스를 회수해갑니다. 즉, 가용성이 떨어지며 불확실성을 가지고 있습니다. 이런 특성으로 인해 AI 학습, 대용량 데이터 분석, CI/CD 분야에서 주로 스팟 인스턴스를 이용하며 AI 및 데이터 분석 프레임워크들은 주기적으로 중간 연산 결과를 &lt;strong&gt;체크포인트&lt;/strong&gt;로 남겨서 스팟 인스턴스의 불확실성에 대응하고 있습니다.&lt;/p&gt;
&lt;p&gt;대부분, 이 정도만 해도 충분하지만 실제 세상에서는 &lt;strong&gt;마감-기한(Deadline-sensitive)&lt;/strong&gt;이 정해진 작업들이 있습니다. 예를 들어, 추천 서비스의 경우 광고를 등록하면 한시간 내에 바로 엔드-유저에게 노출되어야 한다는 정책이 있으면 단순 전략만으론 요구사항을 충족할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-272bc32e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-272bc32e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jTjiMCV1f0dzWrkEvPhtxQ-c143e09a5970.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-272bc32e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jTjiMCV1f0dzWrkEvPhtxQ-c143e09a5970.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=2ECq6gGmeAk"&gt;USENIX — NSDI ’24 — Can’t Be Late: Optimizing Spot Instance Savings under Deadlines&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;오늘은 NSDI’24 에서 best paper award를 받은 논문이자, Apache Spark의 개념을 세우는데 기여한 두 명의 교수님 Ion Stoica와 Scott Shenker이 교신저자로 참여한 논문인 &lt;a href="https://www.usenix.org/conference/nsdi24/presentation/wu-zhanghao"&gt;Can’t Be Late: Optimizing Spot Instance Savings under Deadlines&lt;/a&gt;[1] 을 소개합니다.&lt;/p&gt;
&lt;p&gt;이 논문은 마감-기한을 지켜야하는 애플리케이션이 스팟-인스턴스를 어떻게 레버리지 해야하는 지 정책을 세우는데 의의가 있습니다. 연구진들은 이미 여러 클라우드 서비스에 걸쳐서 스팟-인스턴스로 AI학습 비용을 낮추는 &lt;a href="https://skypilot.readthedocs.io/"&gt;SkyPilot 프레임워크&lt;/a&gt;[2]를 한 차례 발표한 바 있으며 3개월 동안의 실제 실험을 통해 자신들의 전략이 비용을 절감하는데 효과적이라고 입증했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-538ec8b5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-538ec8b5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m5-1bnggLWnvo1kogL8K3A-0870790864fa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-538ec8b5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m5-1bnggLWnvo1kogL8K3A-0870790864fa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://skypilot.readthedocs.io/en/latest/docs/index.html"&gt;&lt;strong&gt;SkyPilot : Run AI on Any Infra&lt;/strong&gt; — Unified, Faster, Cheaper&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스팟 인스턴스의 특징&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문제 정의&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Greedy Policy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비용 분석&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uniform Progress Policy&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="스팟-인스턴스의-특징"&gt;스팟 인스턴스의 특징&lt;/h2&gt;
&lt;p&gt;스팟 인스턴스의 특징을 이해하는 데서 출발합니다. 스팟 인스턴스는 비선점 자원이기 때문에 현재 내가 이용중이었더라도 자원이 언제라도 회수될 수 있습니다. 그리고 스팟 인스턴스를 사용하고 싶다고 해서 언제나 이용할 수 있는 것도 아닙니다. 저자들은 최근 AI학습으로 많이 요구되고 있는 엔비디아 V100과 K80의 인스턴스의 가용성과 선점(Preemption)현상을 3개월 동안 관찰했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ac7c743d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ac7c743d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lsyx3_-lbMpvYbgHIvX2eg-7a4cc7f9da59.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ac7c743d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lsyx3_-lbMpvYbgHIvX2eg-7a4cc7f9da59.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그림에서 왼쪽 그래프는 스팟 인스턴스의 가용성이 가용 영역(AZ)마다 다른 모습을 보여주며, 예측하기 어렵다는 것을 보여줍니다. 그래프에서 가로 실선은 스팟-인스턴스가 가용하다는 것을 나타내며 &lt;strong&gt;회색 선&lt;/strong&gt;은 스팟-인스턴스를 임대 받을 수 없었다는 것을 보여줍니다. 인스턴스가 회수될 때 마다 세로선으로 표기했습니다. 오른쪽 그래프는 인스턴스가 얼마나 가용 가능한 상태였는지를 6 시간 윈도우로 나눈 비율을 보여줍니다.&lt;/p&gt;
&lt;p&gt;어떤 AZ에서는 인스턴스의 가용성이 높았던 반면, 다른 AZ에서는 인스턴스를 거의 이용할 수 없는 상태였음을 알 수 있습니다. 이는 AZ 사이의 상관관계는 거의 없으며 논문의 목표에 부합하기 위해서는 불확실성에 견고한 정책을 세워야 함을 의미합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a0579efd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a0579efd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-OrybZpzemNneGko7G3yLKw-29f26533cb26.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a0579efd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-OrybZpzemNneGko7G3yLKw-29f26533cb26.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;스팟 인스턴스 가용성의 변동성이 매우 큰 반면에 가격 자체는 안정적인 편에 속합니다. 이는 AWS가 &lt;a href="https://aws.amazon.com/blogs/compute/new-amazon-ec2-spot-pricing/"&gt;스팟-인스턴스의 가격 정책&lt;/a&gt;에서 경매 방식을 폐기한 것과 관련 있고, &lt;a href="https://cloud.google.com/spot-vms/pricing"&gt;GCP는 30일 동안은 고정된 가격으로 판매&lt;/a&gt;하고 있기 때문에 스팟-인스턴스는 비교적 안정적인 가격으로 제공되고 있습니다.&lt;/p&gt;
&lt;h2 id="문제-정의--deadline-sensitive-problem"&gt;문제 정의 / Deadline-sensitive Problem&lt;/h2&gt;
&lt;p&gt;지금까지 스팟 인스턴스 특성을 알아봤다면, 이제는 우리가 해를 구해야 하는 &lt;strong&gt;문제가 무엇인지 정의&lt;/strong&gt;해야 합니다. 여기서는 Deadline-senitive한 Task를 안정적으로 운영하는 것을 목표로 하는데요. 그래서 세 가지 변수를 정의합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C(t) = 작업을 마치기 위해 필요한 컴퓨팅 시간&lt;/li&gt;
&lt;li&gt;R(t) = 작업을 마쳐야 하는 마감 기한&lt;/li&gt;
&lt;li&gt;changeover delay (d) = 상태 전환 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기선 &lt;strong&gt;C(0)&lt;/strong&gt;와 &lt;strong&gt;R(0)&lt;/strong&gt;는 처음 시작부터 알 수 있다고 가정합니다. R(0)를 안다는 건 당연하죠. 1시간 뒤에 추천 시스템에 의해 노출되어야 한다면 R(0) = 1시간 입니다.&lt;/p&gt;
&lt;p&gt;그리고 세 가지 상태를 정의합니다. ‘Idle’은 작업이 실행되지 않는 상태, ‘VM’은 온-디맨드 인스턴스로 작업을 수행하는 상태, 그리고 ‘Spot’은 스팟 인스턴스로 작업을 실행하는 상태를 나타냅니다. ‘changeover delay(d)’는 상태 전환 시 발생하는 추가 비용을 의미하며, 이는 새 인스턴스 실행 시간, 애플리케이션 구동 시간, 마지막 체크포인트로부터의 복구 시간 등을 포함합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0610a3d6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0610a3d6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-r63r3JFO-mASmuUE0KKyUg-9e788c24e969.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0610a3d6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-r63r3JFO-mASmuUE0KKyUg-9e788c24e969.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Deadline-sensitive Problem의 목표는 $C(R(0)) \le 0$ 조건을 만족하면서 총 비용을 최소화하는 것입니다. 즉, 주어진 기한 내에 작업을 완료하면서 스팟 인스턴스와 온-디맨드 인스턴스를 최적으로 활용하여 비용 효율성을 극대화하는 것입니다.&lt;/p&gt;
&lt;h2 id="greedy-policy"&gt;Greedy Policy&lt;/h2&gt;
&lt;p&gt;Greedy 정책은 탐욕법을 적용한 가장 기본적이면서 간단한 정책입니다. 이 정책은 다음과 같은 간단한 규칙을 따릅니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;스팟 인스턴스 우선 사용:&lt;/strong&gt; 가능한 한 스팟 인스턴스를 최대한 활용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exploitation Rule:&lt;/strong&gt; 스팟 인스턴스를 사용 중이면 계속 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety Net Rule:&lt;/strong&gt; 만약 R(t) &amp;lt; C(t) + 2d 조건이 충족되면, 온-디맨드 인스턴스로 전환하여 작업을 완료합니다. 이는 작업 완료를 보장하기 위한 안전장치 역할을 합니다. 만약, R(t) &amp;lt; C(t) + 2d 인데 스팟 인스턴스를 사용하다가 Preemption이 발생하면 마감기한을 지킬 수 없게 되겠죠&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thrifty Rule &lt;/strong&gt;: C(t) = 0 라면 Idle 상태가 되야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;여기서 Safety Net Rule, Thrifty Rule, Exploitation Rule 용어는 이 후 반복해서 등장하기에 머리속에 넣어두시면 가독성을 높일 수 있습니다. 그리고, 앞서서 우리는 스팟 인스턴스 가격이 비교적 안정적이라는 사실을 언급했는데요. 미래에 이 가격이 불안정해진다면 Greedy 정책은 동작하지 않겠죠.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-758b6a31"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-758b6a31"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iS2euqRjJ-Dj5rK7h7PsuA-b9fdb1b1aa81.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-758b6a31"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iS2euqRjJ-Dj5rK7h7PsuA-b9fdb1b1aa81.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Greedy 정책을 적용한 경우의 인스턴스 비용&lt;/p&gt;
&lt;p&gt;위 그림은 Greedy 정책을 사용한 경우의 상태 전환 및 비용을 보여줍니다.&lt;/p&gt;
&lt;h2 id="비용-분석"&gt;비용 분석&lt;/h2&gt;
&lt;p&gt;앞서 정의한 Deadline-sensitive Problem은 &lt;a href="https://gazelle-and-cs.tistory.com/36"&gt;&lt;strong&gt;온라인 문제[3]&lt;/strong&gt;&lt;/a&gt;로 해석할 수 있습니다. 온라인 문제 중에 대표 문제인 &lt;strong&gt;스키 대여 문제[4]&lt;/strong&gt;를 다루고 다시 논문 이야기로 돌아오겠습니다.&lt;/p&gt;
&lt;h3 id="-스키-대여-문제"&gt;① 스키 대여 문제&lt;/h3&gt;
&lt;p&gt;겨울에 스키장에 놀러갔다고 합시다. 스키를 타기 위해 우리에게는 두 가지 선택지가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;스키 장비를 50만원을 주고 구입합니다.&lt;/li&gt;
&lt;li&gt;5만원을 내고 장비를 대여합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;어떤 선택을 해야 경제적일까요? 현재로서는 답을 구할 수 없습니다. 그 답은 이번 겨울에 얼마나 자주 스키를 타러 갈 것인지, 스키장이 이번 겨울에는 언제까지 영업할 수 있을지에 따라 달려있죠.&lt;/p&gt;
&lt;p&gt;미래의 모든 상황을 알고 있다면 이 결정은 굉장히 간단해집니다. 만약 스키장에 11번 넘게 갈 거라면 분명히 장비를 구입하는게 낫고, 9번 이하로 간다면 대여 하는게 좋습니다. 하지만 온라인 문제에서는 일단 미래의 일은 알 수 없다고 가정합니다.&lt;/p&gt;
&lt;p&gt;우리가 일반적으로 다루는 알고리즘은 입력이 모두 주어집니다. 반면 이처럼 모든 입력이 한 번에 모두 주어지지 않고, 일부만 주어지는 문제를 &lt;strong&gt;온라인 문제(online problem)&lt;/strong&gt;라고 하며, 새로운 입력이 주어지면 곧바로 결정을 내리는 알고리즘을 &lt;strong&gt;온라인 알고리즘&lt;/strong&gt; 이라고 합니다.&lt;/p&gt;
&lt;h3 id="-경쟁비"&gt;② 경쟁비&lt;/h3&gt;
&lt;p&gt;경쟁비란 온라인 알고리즘 출력의 &lt;strong&gt;최악의 경우(worst-case) 발생하는 비용과 최적해(optimal solution) 비용의 비율&lt;/strong&gt;로 온라인 알고리즘을 평가하는 척도입니다.&lt;/p&gt;
&lt;p&gt;스키장에 방문한 회수를 t라고 한다면 t = 1 일 때 장비 구입의 경쟁비는 10 입니다. 왜냐면, 최악의 경우의 비용은 50만원으로 장비를 구입한 다음, 두 번 다시 스키장에 방문하지 않는 것이며 이 때의 최적해는 대여 비용인 5만원 이기 때문에 경쟁비는 10입니다.&lt;/p&gt;
&lt;p&gt;만약 t = 2 일때의 경쟁비는 어떻게 될까요? t = 1 에서 대여를 한 번 했고, t = 2 에서 구입을 했으니 비용은 55만원입니다. 그리고 최적해는 두 번 모두 대여해서 10만원을 내고 두 번 다시 스키장에 가지 않는 것입니다. 재밌는 사실은 t = 1 일 때의 경쟁비는 10이었죠. t = 2 에서 장비를 구입하는 것의 경쟁비는 5.5로 대략 1.8배 줄어든 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 식으로 표현하면 t ≤ 10인 경우의 경쟁비는 &lt;strong&gt;(500 + 50(t-1))/50t&lt;/strong&gt; 이고, t &amp;gt; 10 인 경우에는 처음부터 장비를 구입하는게 최적해가 되기 때문에 식은 (500+50(t-1))/500가 됩니다. 이 해석에 따르면 우리는 10번 스키장에 간 다음에 장비를 사는게 낫습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;경쟁비는 논문의 내용을 이해하는데에는 필요한 용어입니다만, 중요한 비중을 가진 단어는 아닙니다. 온라인 문제 자체를 깊이있게 공부하고 싶으신 분은 &lt;a href="https://gazelle-and-cs.tistory.com/36"&gt;가젤님 블로그 글&lt;/a&gt;을 추천드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="-greedy-정책의-worst-case-비용-분석"&gt;③ Greedy 정책의 worst case 비용 분석&lt;/h2&gt;
&lt;p&gt;Greedy 정책의 worst case 비용은 어떻게 될까요? 설정한 Greedy 정책에 정확히 반대 포지션을 가진 어떤 정책이 있다고 가정해봅시다. 이 정책은 적대적(Adversarial)이지도 않고 나의 결정을 미리 알 수 있는 오라클도 아니지만 우연히 내가 스팟-인스턴스를 필요로 하는 순간, R(t) ≥ C(t) + 2d, 에 모든 스팟인스턴스를 가져가고 있습니다. 그러면 이 경우의 worst case 비용은 모든 연산을 온-디맨드 인스턴스로 수행하는 경우이고 이 비용을 K라고 정의합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;스팟-인스턴스의 가용성이 충분하지 않은 상황에서 Greedy 정책을 사용하는 작업 여러 개를 동시에 돌린다고 가정해도 worst case 비용이 발생할 가능성이 크다고 직관적으로 이해할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 경우를 피하기 위해서 전체 시간을 사전에 정해진 파라미터 N으로 나눈 다음에 나누어진 구간 별로 Greedy 정책을 적용합니다. 물론 이 경우에도 최악의 비용이 K이기 때문에 &lt;strong&gt;n-sliced greedy&lt;/strong&gt;와 &lt;strong&gt;shifted (n-1)-sliced greedy&lt;/strong&gt; 정책을 시간 t에 대해서 균등한 확률로 선택해서 적용하는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1ba7c042"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1ba7c042"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9Rz943IrIwKfeH4l2PzdYw-c7c14b602ecf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1ba7c042"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9Rz943IrIwKfeH4l2PzdYw-c7c14b602ecf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이를 randomizaed shifted greedy (RSF) 라고 부르고, 남은 시간 R(0)가 C(0)보다 2배 이상 크다면 경쟁비가 아래 수식을 따르게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e392cf0f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e392cf0f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2XO28YkKkaO3t2nkMHWu8A-e07630a2734e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e392cf0f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2XO28YkKkaO3t2nkMHWu8A-e07630a2734e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;수식에서 (1)은 온-디맨드 인스턴스 비용을 의미하고, (2)는 스팟 인스턴스 비용을 의미합니다. Safety Net Rule에 의해 마지막 1/n 은 어차피 온-디맨드 인스턴스를 돌려야 하고 남는 시간인 (1–1/n)에 대해서 온-디맨드 인스턴스와 스팟 인스턴스가 각각 절반씩 나눠 가집니다.&lt;/p&gt;
&lt;h2 id="-rsf-정책의-평균-비용-분석"&gt;④ RSF 정책의 평균 비용 분석&lt;/h2&gt;
&lt;p&gt;위에서 RSF 정책의 경쟁비가 worst case보다 작음을 보여주었습니다. 이제는 &lt;strong&gt;평균 비용(Average case)&lt;/strong&gt;을 알아봅시다. 먼저 용어부터 정의할 건데요. 우리는 스팟 인스턴스의 가용성 수준을 모르기 때문에 스팟 인스턴스를 사용할 수 있는 비율(r)이 일정하다고 가정합니다. 예를 들어보면 총 5시간 중에 4시간은 스팟 인스턴스를 사용할 수 있었고, 1시간은 대기해야만 했다면 r = 0.8 입니다.&lt;/p&gt;
&lt;p&gt;그리고, 스팟 인스턴스의 비용은 1로 고정이구요. 온-디맨드 인스턴스의 가격 k는스팟 인스턴스 비용보다는 크기 때문에 k &amp;gt; 1 이라고 가정합니다. 단순 &lt;strong&gt;Greedy 전략&lt;/strong&gt;은 &lt;strong&gt;R(t’) &amp;lt; C(t’) + O(d)&lt;/strong&gt;이 참이라면 스팟-인스턴스를 사용하고 반대의 경우 온-디맨드 인스턴스로 전환하는 전략입니다. 시간 t’ 을 작업이 전환되기 까지의 시간으로 정의하면 &lt;strong&gt;평균 비용(p)&lt;/strong&gt;은 아래 수식으로 정의할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-718bf83c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-718bf83c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zKbIt2DuWdWDTomwzcUdOg-6348c14d738f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-718bf83c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zKbIt2DuWdWDTomwzcUdOg-6348c14d738f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;r — k가 항상 음수이기 때문에 스팟 인스턴스 사용 시간(t’)이 길수록 비용이 줄어듭니다. 이 간단한 공식의 실효성을 보여주기 위해, 48시간 동안 실제 전략을 적용한 비용 변화를 그래프로 제시합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4adcfac8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4adcfac8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4v4_zWPoG5jmlbk_fN-IGA-3dd054b3f5c2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4adcfac8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4v4_zWPoG5jmlbk_fN-IGA-3dd054b3f5c2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;논문에서 저자들이 주장하고 싶은 바는 RSF전략이 평균적으로 Greedy 보다 더 적은 비용을 사용한다는 것이었죠. RSF 전략은 스팟 인스턴스의 가용성 수준인 R이 평균 r과 분산 v를 가진 확률 변수를 가진다고 가정하고 수식을 전개합니다. (자세한 내용은 논문의 Appendix A.2.2 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-52b0d95c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-52b0d95c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-reeAGJicoqpM4-aeSk4DCA-99ef13968133.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-52b0d95c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-reeAGJicoqpM4-aeSk4DCA-99ef13968133.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;v_hat은 R(0)를 n등분한 구역별로 분산을 구한 값이고 v는 전체 R(0) 시간 스케일에 대한 분산 값을 의미합니다. 저자들은 항상 v_hat &amp;gt; v이 참이라고 예상하고 있어서 RSF와 greedy 전략의 비용 차이인 Δ값이 양수라고 예상하고 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;대충 설명하는 것처럼 느끼실 수 있는데요.&lt;br&gt;
제가 제대로 이해하지 못해서 논문의 내용을 옮겨 왔습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="uniform-progress-policy"&gt;Uniform Progress Policy&lt;/h2&gt;
&lt;p&gt;이제 이 포스팅의 마지막 단원입니다. 위에서는 이론적인 배경을 먼저 보여주었다면 지금부터는 실제로 이용할 수 있는 정책에 대해 다룹니다. 먼저 RSF의 수정 버전인 &lt;strong&gt;Time Sliced 정책&lt;/strong&gt;을 설명한 다음, 파라미터가 없이 동작하는 &lt;strong&gt;Uniform Progress 정책&lt;/strong&gt;을 다룹니다.&lt;/p&gt;
&lt;h3 id="-time-sliced"&gt;① Time Sliced&lt;/h3&gt;
&lt;p&gt;Time Sliced는 n-sliced greedy policy를 수정한 버전으로 두 가지 개선점이 있습니다. (1) 스팟을 사용하고 있었다면 C_i(t) ≤ 0이 되어도 계속 이용합니다. (2) 구간 i가 더 많은 일을 한다면 그 진행도를 다음 구간 i+1가 이어 받아서 계산합니다. 그리고 클라우드는 비적대적(non-adversarial) 환경이기 때문에 RSF에서 랜덤하게 선택했던 것을 실제로는 적용하진 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e74053df"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e74053df"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8zei2vecY3_T13514Pb8jg-ae2e54fc1dc3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e74053df"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8zei2vecY3_T13514Pb8jg-ae2e54fc1dc3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Time Sliced와 Greedy 정책의 차이를 보여준다.&lt;/p&gt;
&lt;p&gt;저자들은 AWS에서 600개의 랜덤한 p3.2xlarge 인스턴스 타입에 대해 실험을 진행했습니다. Time Sliced 정책에서 파라미터 N을 잘 고르면 33% ~ 62% 까지 비용절감이 가능했다고 하는데요. 문제는 파라미터 N을 결정하는 번거로움에 있습니다. 그래서 여기서 한 단계 더 나아가 완전한 parameter-free 정책인 Uniform Progress를 개발합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-19170a12"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-19170a12"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3k6yRgrY9CorhscTryzhmw-664556f6189d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-19170a12"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3k6yRgrY9CorhscTryzhmw-664556f6189d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="-uniform-progress"&gt;② Uniform Progress&lt;/h2&gt;
&lt;p&gt;Uniform Progress는 파라미터 N까지 제거된 정책입니다. 소위 말하는 딸깍만 하면 바로 적용되는 알고리즘입니다. 먼저 용어부터 알아보겠습니다. cp(t_i)는 시간 t_i에 대해 현재까지 진행된 작업 진척도 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b555192a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b555192a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iThxMIzz__4Sgqzgdo9Rvw-680ca89da294.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b555192a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-iThxMIzz__4Sgqzgdo9Rvw-680ca89da294.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그리고 ep(t_i)는 최소한 이 정도까지는 작업했어야 함을 의미하는 예상값인데요. R(0)와 C(0)의 비율을 시간만큼 곱한값입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e975eb7e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e975eb7e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aXWrJ_n654sKiJaTHylxuA-5f59e91fa8f8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e975eb7e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aXWrJ_n654sKiJaTHylxuA-5f59e91fa8f8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Uniform Progress는 총 4가지 규칙을 따릅니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;현재 상태가 Idle 이며 cp(t) &amp;lt; ep(t) 라면 온-디맨드 인스턴스로 전환해서 예상치를 따라갑니다.&lt;/li&gt;
&lt;li&gt;Spot 인스턴스를 사용할 수 있으면 on-demand에서 spot으로 전환합니다. 이는 cp(t) &amp;lt; ep(t)여도 마찬가지구요. 한 번 스팟을 사용하면 선점 당할때까지 이용합니다.&lt;/li&gt;
&lt;li&gt;최악의 경우에도 deadline을 지키기 위해서 Safety Net Rule이 최상위 정책으로 있습니다.&lt;/li&gt;
&lt;li&gt;Spot Instance가 빈번하게 떨어지는 경우에는 역효과가 있어서, 온-디맨드 상태일 때 cp(t) ≥ ep(t+2d)가 될 때 까지는 온-디맨드를 유지하는 정책을 추가해서 최종 Uniform Progress를 완성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다음은 Uniform Progress와 Time Sliced의 상태 전이 과정을 State Machine으로 표현한 그림입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-daf0a52f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-daf0a52f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-p8PNkqBDByB68t2nDyLQVg-5689fbfd1c2e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-daf0a52f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-p8PNkqBDByB68t2nDyLQVg-5689fbfd1c2e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="평가"&gt;평가&lt;/h3&gt;
&lt;p&gt;논문에서 저자들은 Uniform Progress 정책을 실제로 AWS와 GCP에서 적용해본 결과를 공유했습니다. 스팟 인스턴스의 가용율은 70% ~ 90% 정도 되었다고 하구요. ML 학습과 데이터 분석을 수행한 결과를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7eacff38"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7eacff38"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ywCsdMWHe4te-UyzrFC2Tg-2632b874afdd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7eacff38"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ywCsdMWHe4te-UyzrFC2Tg-2632b874afdd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 표는 온-디맨드만 사용한 것 대비 얼마나 비용을 아낄 수 있었는지를 보여줍니다. 아래 표에서는 Greedy 정책과 비교하는데요. Greedy 정책에 비해 큰 폭으로 비용이 감소함을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-533fe91a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-533fe91a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-joa1jFpb4hdOcd8RXhXwaw-3b34ac866ec2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-533fe91a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-joa1jFpb4hdOcd8RXhXwaw-3b34ac866ec2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;마지막으로 마감 기한이 &lt;strong&gt;짧은 경우(Tight DDL)&lt;/strong&gt;와 &lt;strong&gt;긴 경우 (Loose DDL)&lt;/strong&gt;에도 유의미하게 작동하는지도 평가합니다. job fraction은 R(0)와 C(0)의 비율을 의미합니다. 이 값이 클 수록 컴퓨팅 파워를 더 타이트하게 운용해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4d1395cd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4d1395cd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MisZEeUm7WLlgsw8PYZWoQ-2a2e223dbe00.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4d1395cd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MisZEeUm7WLlgsw8PYZWoQ-2a2e223dbe00.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;평가표중에 표기된 &lt;strong&gt;Omniscient&lt;/strong&gt;는 클라우드 사업자들이 Spot Availability에 대한 오라클(API)를 제공한다는 가정하에 동작하는 정책입니다. 논문은 꽤 많은 지면을 할애해서 이를 설명하는데요. 이 포스팅에서는 모든 내용을 생략했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="마무리"&gt;마무리&lt;/h3&gt;
&lt;p&gt;개발을 잘하기 위해 가장 중요한 능력은 무엇일까요? 많은 중요한 요소들이 있을텐데 제가 가장 부러워 하는 건 &lt;strong&gt;문제를 인지하는 능력&lt;/strong&gt;입니다. 모든 문제해결은 문제를 내가 문제라고 인식하고 있어야 가능합니다.&lt;/p&gt;
&lt;p&gt;이 논문의 1저자인 Zhanghao Wu는 스팟인스턴스를 활용하는데 마감기한을 지키기 위해서는 온-디맨드 인스턴스의 사용비율이 높다는 문제를 인식했고 이를 해결하기 위한 사고과정을 거쳐 이 논문을 발표했으며, 이를 적용한 SkyPilot 프레임워크의 개발자이기도 합니다.&lt;/p&gt;
&lt;p&gt;만약, 이 것을 문제라고 인식하지 않고 &lt;strong&gt;이정도 돈은 낼만 하다고&lt;/strong&gt; 만족하고 있었다면 이 논문은 나오지 않았을 겁니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://www.usenix.org/conference/nsdi24/presentation/wu-zhanghao"&gt;Can’t Be Late: Optimizing Spot Instance Savings under Deadlines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://skypilot.readthedocs.io/en/latest/docs/index.html"&gt;SkyPilot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://gazelle-and-cs.tistory.com/36"&gt;온라인 알고리즘 (Online Algorithm)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://theory.stanford.edu/~trevisan/cs261/lecture17.pdf"&gt;Stanford University — CS261: Optimization — Lecture 17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://zhanghaowu.me/"&gt;Zhanghao Wu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>24' 현대모비스 알고리즘 대회 후기</title><link>https://scalalang2.com/p/24-%ED%98%84%EB%8C%80%EB%AA%A8%EB%B9%84%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%8C%80%ED%9A%8C-%ED%9B%84%EA%B8%B0-c2ce076311ec/</link><pubDate>Thu, 11 Jul 2024 02:02:34 +0000</pubDate><guid>https://scalalang2.com/p/24-%ED%98%84%EB%8C%80%EB%AA%A8%EB%B9%84%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%8C%80%ED%9A%8C-%ED%9B%84%EA%B8%B0-c2ce076311ec/</guid><description>&lt;p&gt;2024년 현대모비스 알고리즘 경진대회에 다녀왔습니다. &lt;br&gt;
간단하게 후기를 남겨봅니다.&lt;/p&gt;
&lt;h3 id="취미"&gt;취미&lt;/h3&gt;
&lt;p&gt;거창한 목표가 있는 건 아니고 매주 열리는 온라인 경쟁 프로그래밍 대회인 AtCoder/LeetCode Weekly에 참여해서 도파민을 채우고 있습니다. 그냥 매주 하다보니까 취미인 것 같기도 한데, 어쩔 땐 진짜 하기 싫고 약간 알고리즘과 애증의 관계를 맺고 있습니다.&lt;/p&gt;
&lt;h3 id="현대모비스-알고리즘-경진대회"&gt;현대모비스 알고리즘 경진대회&lt;/h3&gt;
&lt;p&gt;해외에서 개최하는 온라인 대회만 하다가, &lt;a href="https://career.programmers.co.kr/competitions/3980"&gt;현대모비스에서 매년 알고리즘 경진대회&lt;/a&gt;를 개최한다는 소식을 알게 되었고 그래도 한 번쯤은 오프라인 대회 향기를 맡아보고 싶어서 지원했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b849a080"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b849a080"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-L0BVejgnApeawCymLiTPzA-c77d988614f5.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b849a080"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-L0BVejgnApeawCymLiTPzA-c77d988614f5.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;상을 준다고는 하지만, 애초에 내 실력으로는 20등 안에 들거라고는 꿈도 꾸지 않았다.&lt;/p&gt;
&lt;p&gt;대회자체는 온라인 예선전을 치르고, 예선전에서 성적순으로 본선 진출자 100명을 뽑아서 오프라인 대회를 치르는 구조입니다. 다행인 점은 어릴 때 부터 IOI에 출전한 이세계 천재들이 대학에 진학해 ICPC/UCPC를 준비하고 있는 학생부와 PS판에서 은퇴한 사람들, 의문의 애니 프사 은둔 고수들, 평범한 직장인을 위한 일반부가 나뉘어져 있어서 별도로 평가한다는 점입니다.&lt;/p&gt;
&lt;p&gt;저는 당연히 일반부로 참여했고&lt;br&gt;
그냥 본선 진출만 노리자는 가벼운 마음으로 참가했습니다.&lt;/p&gt;
&lt;h3 id="온라인-예선전"&gt;온라인 예선전&lt;/h3&gt;
&lt;p&gt;알고리즘 경진대회라는 이름 답게 코딩테스트에서나 나올 법한 가벼운 문제들은 아니었습니다. 1번 부터 SCC + Topological Sort가 필요한 문제였고 한 시간 반 동안 비비다가 겨우 AC를 받았습니다.&lt;/p&gt;
&lt;p&gt;2번은 구현이 까다로운 백트래킹 비슷한 문제였는데 코드를 완성하는게 시간이 좀 걸렸지만 한 번에 풀려서 기분이 좋았습니다. 3번도 굉장히 복잡한 구현능력을 요구하는 문제였는데 시간이 부족해서 포기했습니다. 시간이 충분히 주어졌어도 올바르게 구현할 수 있을지는 모르겠네요.&lt;/p&gt;
&lt;h3 id="오프라인-본선-진출"&gt;오프라인 본선 진출&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8f7af2ea"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8f7af2ea"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-48-RdsH-0X7S2rdRAyMDfg-d82234ed416e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8f7af2ea"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-48-RdsH-0X7S2rdRAyMDfg-d82234ed416e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;사실 가채점 할 때 본선 진출 할 수 있을 거라고 예상하긴 했습니다만, 저보다 더 실력이 뛰어나신 분들이 있음에도 제가 진출하게 되어서 뭔가 묘한 감정이 교차했습니다.&lt;/p&gt;
&lt;p&gt;제가 평소에 해외 온라인 대회에서 항상 상위권 랭크를 달성하시는 분들을 뒤로하고 본선에 갈 수 있었던 이유를 보자면, 1번 문제인 SCC 알고리즘이 하필 최근 앳코더 대회에서 만났던 친구라 바로 떠올리고 활용하고, 올바르게 구현할 수 있었구요. 2번 문제는 밀도 진한 구현 문제였는데 이런 유형을 별로 안좋아시는 분들이 많이 거르신 것 같았습니다.&lt;/p&gt;
&lt;p&gt;애초에 3번 문제를 못 풀 실력이라는 걸 확신하고 2번에 내 모든걸 건다는 심정으로 한 시간 동안 구현한게 도움이 되지 않았나 싶습니다. 그리고 나중에 들리는 소문에 의하면 학생부는 최소 3문제 + 시간 컷이 있었고 일반부는 2문제가 커트라인이라는 말을 들었습니다.&lt;/p&gt;
&lt;h3 id="본선-후기"&gt;본선 후기&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-36eab3d6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-36eab3d6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Q87lzGp-2DVWsL-e5YrG1w-0e4a4c71b55b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-36eab3d6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Q87lzGp-2DVWsL-e5YrG1w-0e4a4c71b55b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-82afcec3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-82afcec3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3bMmZ7IB2E5pQ_bDxwFxvQ-b086063e9a99.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-82afcec3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3bMmZ7IB2E5pQ_bDxwFxvQ-b086063e9a99.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;본선 후기를 짧게 하려고 합니다. AC를 받진 못했고 테스트 케이스만 몇개 긁고 왔습니다. 총 4문제가 출제되었구요. 확실히 문제가 많이 어려웠는지 2번 문제를 풀이한 사람이 7명 밖에 되지 않았고 3번, 4번 문제는 아무도 풀이하지 못했습니다.&lt;/p&gt;
&lt;p&gt;대회 중간에 새로운 1등이 갱신되거나 나를 제친 사람들이 생기면 알림으로 멘탈 공격을 가합니다. 반대로 제가 제출하면 내가 제친 사람 수를 알려줬는데 그런 부분이 재밌긴 했습니다. 처음부터 기대조차 하지 않았으니 당연히 실망도 없었습니다.&lt;/p&gt;
&lt;p&gt;대회가 끝나고 현대모비스 관계자 분들이 회사의 비전과 문화에 대해서 설명해주셨는데요. 신입 초봉이 8000만원으로 굉장히 높았다는 점과 아직까지도 재택/출근 하이브리드 제도를 유지하고 있는 몇 안되는 기업이라는 점이 인상적이었습니다. 본선만 진출해도 입사 지원시 서류 및 코딩테스트가 면제되는 걸 보고 현대모비스가 얼마나 소프트웨어 역량 확보에 진심인지 알 수 있었습니다. 이 혜택은 매년 조금씩은 달라질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hyundai.co.kr/news/CONT0000000000156410" title="https://www.hyundai.co.kr/news/CONT0000000000156410"&gt;&lt;strong&gt;현대모비스, &amp;lsquo;2024 알고리즘 경진대회&amp;rsquo; 성료&lt;/strong&gt;&lt;br&gt;
*현대모비스 알고리즘 경진대회에서 서울대 학생 강태규와 졸업생 이상엽이 우승하여 아이오닉5 전기차를 부상으로 받았습니다. 대회는 SW 인재 발굴을 목적으로 진행되었으며, 총 상금은 1억7,000만 원입니다.*www.hyundai.co.kr&lt;/a&gt;&lt;a href="https://www.hyundai.co.kr/news/CONT0000000000156410"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.econovill.com/news/articleView.html?idxno=659862" title="https://www.econovill.com/news/articleView.html?idxno=659862"&gt;&lt;strong&gt;현대모비스, 알고리즘 경진대회 성료&amp;hellip;&amp;ldquo;서울대·카이스트 모였다&amp;rdquo;&lt;/strong&gt;&lt;br&gt;
*현대모비스는 대한민국 소프트웨어(SW) 고수 100인의 치열한 한판 승부인 &amp;lsquo;2024 현대모비스 알고리즘 경진대회&amp;rsquo; 본선 결과 학생부 강태규(서울대 컴퓨터공학과)씨와 일반부 이상엽(서울대 통계학과 졸업)씨가 최종…*www.econovill.com&lt;/a&gt;&lt;a href="https://www.econovill.com/news/articleView.html?idxno=659862"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;나중에 기사를 통해서 결과를 보니 학생부 1등과 일반부 1등 모두 서울대 분들이셨습니다. 별로 의미는 없었지만 이런 분들과 순위 경쟁 한적이 있었다는 것 만큼은 제 미래의 자손들에게 꼭 전하겠습니다.&lt;/p&gt;
&lt;p&gt;마지막으로 리더보드에 이름이 없는 걸 보고 빈손으로 돌아가나 싶었는데.. 그래도 본선 진출자들을 위해 &lt;strong&gt;뱅앤올룹슨 아웃도어 스피커&lt;/strong&gt; 제품과 맛있는 과일까지 챙겨주셔서 감동받았습니다. 과일들은 집에 있는 제 예비신부와 맛있게 잘 먹었습니다.&lt;/p&gt;
&lt;p&gt;사실 &lt;strong&gt;알고리즘 문제 해결(PS)&lt;/strong&gt;이 마이너한 취미이고 컴퓨터공학을 전공하는 학생분들도 취업에 도움이 되는 자바/스프링이나 AI/Python 공부를 선호하지 알고리즘을 공부하시는 분들은 진짜들이 아니고서는 잘 보진 못했습니다. 현대모비스 알고리즘 대회가 국내에서 개최되는 몇 안되는 PS 공개 대회인 만큼 그 역사가 오랫동안 유지되었으면 하는 마음이 있습니다.&lt;/p&gt;
&lt;p&gt;이런 귀중한 대회를 매년 개최해주시는 현대모비스 관계자 분들께 감사의 말씀 올리면서 글을 마무리 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2d51e571"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2d51e571"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xN8scrDjj83R851TZxODAA-b075bf070b86.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2d51e571"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xN8scrDjj83R851TZxODAA-b075bf070b86.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-28d77162"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-28d77162"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qBRXBY0Wsrn215JeoYmZcQ-941ff1f4500a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-28d77162"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qBRXBY0Wsrn215JeoYmZcQ-941ff1f4500a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1b56fc2b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1b56fc2b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LwYEXIYAllD7TrYy-tMTuw-af3fd9e7a1fb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1b56fc2b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LwYEXIYAllD7TrYy-tMTuw-af3fd9e7a1fb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description></item><item><title>세션 게임의 매치메이킹 알고리즘</title><link>https://scalalang2.com/p/%EC%84%B8%EC%85%98-%EA%B2%8C%EC%9E%84%EC%9D%98-%EB%A7%A4%EC%B9%98%EB%A9%94%EC%9D%B4%ED%82%B9-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-c29989ddadcc/</link><pubDate>Sun, 16 Jun 2024 14:25:07 +0000</pubDate><guid>https://scalalang2.com/p/%EC%84%B8%EC%85%98-%EA%B2%8C%EC%9E%84%EC%9D%98-%EB%A7%A4%EC%B9%98%EB%A9%94%EC%9D%B4%ED%82%B9-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-c29989ddadcc/</guid><description>&lt;h2 id="elo-glicko-trueskill-eomm-그리고-enmatch"&gt;Elo, Glicko, TrueSkill, EOMM 그리고 EnMatch&lt;/h2&gt;
&lt;p&gt;헝가리 출생의 마케트 대학교 물리학 교수이자 체스 마스터 였던 엘로(Arpad Elo)님 께서는 체스 선수의 능력을 평가하고자 통계 기반의 Elo 알고리즘을 개발했습니다. 1960년에는 &lt;strong&gt;미국 체스 연맹(USCF)&lt;/strong&gt;이, &lt;strong&gt; 1970년에는 &lt;/strong&gt;세계 체스 연맹(FIDE)에서 Elo 모델을 채택합니다.&lt;/p&gt;
&lt;p&gt;Elo 시스템은 각 선수들에게 능력치를 나타내는 점수를 부여합니다. 이 점수는 경기 결과에 따라 변화하는데요. 승리하면 증가하고 패배하면 감소합니다. 많은 온라인 게임에서 MMR이라는 이름으로 이 개념을 매치메이킹 단계에서 이용하고 있습니다.&lt;/p&gt;
&lt;p&gt;오늘은 온라인 게임에서 유저들의 능력치를 평가하는 방법 중 유명한 Elo, Glicko, TrueSkill를 소개하고 EA에서 개발한 EOMM, 최근 AAAI’24 에서 발표된 EnMatch 논문 내용을 다룹니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이번 편은 저도 완벽하게 모든 내용을 이해한 게 아니여서 내용에 대한 자신감이 많이 없습니다. 이 글은 재미로만 읽어주시고, 정확한 내용은 레퍼런스 자료들을 참고하시기 바랍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;매치메이킹 시스템&lt;/li&gt;
&lt;li&gt;매치메이킹 요구사항의 다양성&lt;/li&gt;
&lt;li&gt;Elo&lt;/li&gt;
&lt;li&gt;Glicko&lt;/li&gt;
&lt;li&gt;TrueSkill™&lt;/li&gt;
&lt;li&gt;EOMM&lt;/li&gt;
&lt;li&gt;EnMatch&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="매치메이킹-시스템"&gt;매치메이킹 시스템&lt;/h2&gt;
&lt;p&gt;매치메이킹은 게임을 시작하기 위해 대기중인 플레이어를 하나의 게임으로 배치하는 것을 말합니다. 이 때 진행되는 게임의 단위를 흔히 &lt;strong&gt;세션(Session)&lt;/strong&gt;이라고 부릅니다. 일반적인 세션 기반 게임의 서버 구성은 백엔드 서버와 매치메이커 그리고 데디케이트 서버 Fleet으로 구분됩니다.&lt;/p&gt;
&lt;p&gt;백엔드 서버가 회원, 인증, 상점, 플레이 정보, 소셜, 매칭 등 게임 플레이 이외에 필요한 기능을 수행합니다. 유저가 세션에 진입하기 위해 매칭을 돌리면 백엔드 서버가 플레이어들을 하나의 세션으로 할당하는데, 매치메이커가 플레이어를 어떤 세션에 할당할 것인지 결정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c33f9c65"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c33f9c65"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e9C_Ro4XYe5HjNBo6HNLsw-818199e494f0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c33f9c65"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e9C_Ro4XYe5HjNBo6HNLsw-818199e494f0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그림 그리기 귀찮았는데.. 구글 형님들 감사합니다. &lt;a href="https://open-match.dev/site/docs/guides/matchmaker/"&gt;[출처 : OpenMatch]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-98279b27"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-98279b27"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JUrtT3_sRQaKyLXSYGkY9w-3edc345d13f9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-98279b27"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JUrtT3_sRQaKyLXSYGkY9w-3edc345d13f9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;e.g. PUBG:BATTLEGROUNDS : 세션 기반 게임에서 플레이어는 로비에서 매칭된 후에 데디케이트 게임서버로 이동한다.&lt;/p&gt;
&lt;h2 id="매치메이킹-요구사항의-다양성"&gt;매치메이킹 요구사항의 다양성&lt;/h2&gt;
&lt;p&gt;게임을 제작하는 팀 마다 매치메이커에 다양한목표를 설정하곤 합니다. 게임 디자이너들은 일차적으로 높은 품질의 게임 플레이 경험을 주고 싶어 합니다. 이를 위해 플레이어간의 레이턴시나 매칭에 걸리는 시간을 최소화 하면서도 게임의 내용을 재밌게 하기 위해 스킬 기반의 매칭(SBMM)을 이용하거나, 플랫폼 유저를 분리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;일부 회사들은 비즈니스 목적으로 매치메이킹 시스템을 이용하기도 합니다. GDC’2016에서 액티비전[1]은 한 발표에서 일반적으로는 &lt;strong&gt;게임 디자이너&lt;/strong&gt;들은 게임이 가장 재밌는 방향 혹은 가장 공정한 방향으로 매치하길 원하지만 비즈니스 목표로 장기적인 monetary value를 높이는 방향을 설정할 수 있다고 설명합니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/-pglxege-gU?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;GDC’2016 | 스킬, 매치메이킹, 레이팅 시스템 디자인&lt;/p&gt;
&lt;p&gt;여기서 말하는 비즈니스 팀의 목표인 Monetary value의 의미를 액티비전이 2015년 등록한 특허[2]에서 추측해볼 수 있습니다. 물론 이건 개인적인 뇌피셜이고 실제로 발표자 분께서 의도하신 건 아닐 수 있습니다.&lt;/p&gt;
&lt;p&gt;어쨋든, 이름 마저 &lt;strong&gt;멀티플레이 게임에서 마이크로 결제를 이끄는 시스템&lt;/strong&gt;이라는 이 특허는 결제를 유도하기 위해 어떻게 매치메이킹 로직을 이용할 수 있는지 수 많은 사례를 소개합니다. 특허문서에서 하나의 예시만 가져와 보자면, 유저가 새로운 무기를 결제하면 다음 게임은 일부러 그 무기가 가장 유용하게 사용되게끔 유도해서 결제한 것을 후회하지 않도록 연출합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;if the player purchased a particular weapon, the microtransaction engine may match the player in a gameplay session in which the particular weapon is highly effective, giving the player an impression that the particular weapon was a good purchase.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;올해 초 콜 오브 듀티에서 공개한 매치메이킹 문서[3]에서는 플레이어가 지속적으로 패배할 경우, 진행중인 게임을 종료할 가능성이 높아져 플레이어 풀의 영향을 끼치는 구조가 나선 효과를 만든다고 설명했습니다.&lt;/p&gt;
&lt;p&gt;플레이어의 능력을 통계 모델을 이용해서 객관적으로 평가하는 데서 출발한 스포츠의 스킬 레이팅 시스템은 온라인 게임에서 동일한 능력을 가진 플레이어를 세션에 모아 균형 잡힌 경기를 만드려고 하는 것을 스킬 기반 매치메이킹(SBMM) 이라고 합니다.&lt;/p&gt;
&lt;p&gt;최근에는 게임을 스포츠가 아닌 서비스로 바라보는 시각이 생기면서 매칭을 공정한 게임의 규칙을 구축하는 것 뿐 아니라 다양한 용도로 사용하는 사례가 공유되고 있습니다. 대표적으로 UCLA와 EA가 공동으로 연구한 EOMM이 있으며 AAAI’24 에서도 중국의 대형 게임사 넷이즈에서도 발표한 EnMatch가 있습니다.&lt;/p&gt;
&lt;p&gt;지금부터는 SBMM에서 쓸 수 있는 대표적인 스킬 평가 시스템인 ELO, TrueSkill, Glicko에 대해서 먼저 다루고, 그 다음에 EOMM과 EnMatch 논문을 소개합니다.&lt;/p&gt;
&lt;h2 id="elo-rating-system"&gt;Elo Rating System&lt;/h2&gt;
&lt;p&gt;Elo[4]는 물리학자 교수이자 체스 마스터였던 엘로(Arpad Elo)가 만든 선수 평가 시스템입니다. Elo는 &lt;strong&gt;플레이어들의 능력 수준은 정규 분포를 따른다&lt;/strong&gt;고 가정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-53c62e64"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-53c62e64"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-xcdM52y3OOgAcfV1-3adb4284ab17.jpg"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-53c62e64"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-xcdM52y3OOgAcfV1-3adb4284ab17.jpg"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Apard Elo (1903 ~ 1992)&lt;/p&gt;
&lt;p&gt;많은 온라인 게임들이 채택하고 있고, 본인이 자주 참여하는 온라인 경쟁적 프로그래밍 대회인 AtCoder, LeetCode 에서도 Elo 기반의 점수 시스템을 사용하고 있습니다[5].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dd374897"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dd374897"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-N6ZjRuUqWx9RPp1bQ5QVcw-af68f8d3eb34.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dd374897"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-N6ZjRuUqWx9RPp1bQ5QVcw-af68f8d3eb34.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;본인의 부끄러운 LeetCode Contest 점수&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9de48789"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9de48789"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-spau-60rrigGz8MqGoc0dA-c42a292fa5d3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9de48789"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-spau-60rrigGz8MqGoc0dA-c42a292fa5d3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;본인의 부끄러운 AtCoder Contest 점수&lt;/p&gt;
&lt;p&gt;위키피디아에서 기재된 가장 기본적인 Elo Scheme은 다음과 같이 정의합니다. 경기 기록이 없는 선수들은 초기값을 부여해서 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-50131c97"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-50131c97"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nmWUOr-QPzxt-2WCPbW7hQ-40791daaa38d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-50131c97"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nmWUOr-QPzxt-2WCPbW7hQ-40791daaa38d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Elo에서 상대 전적을 계산하는 공식&lt;/p&gt;
&lt;p&gt;E_{A,B}는 A가 B를 이길 확률을 의미합니다. 만약 두 선수의 점수 (R_B, R_A)가 서로 같다면 두 선수의 승률은 값은 1/2로 동일합니다. R_B &amp;gt; R_A인 상황에서는 승률이 &amp;lt; 50%로 낮아진다. 수식을 잘 보면 400 이라는 상수가 있는데, 이는 점수가 400점 정도 차이나면 승률이 90:10 정도 차이나도록 설정한 값입니다.&lt;/p&gt;
&lt;p&gt;이 값은 마음대로 설정해도 되는데, 선수들의 점수 범위를 넓게 가져가고 싶으면 큰 값을 사용하면 되고 좁은 범위로 설정하고 싶다면 작은 값을 사용하면 됩니다. 이렇게 승률을 계산했다면 실제 경기 결과를 가지고 나의 점수에 반영합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-12cce291"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-12cce291"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gbn5T4XAkUx6h831HO-bhw-dda535f6c546.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-12cce291"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gbn5T4XAkUx6h831HO-bhw-dda535f6c546.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;점수 반영 계산식&lt;/p&gt;
&lt;p&gt;경기 결과인 Score는 게임에서 이긴 경우 1.0, 비긴 경우 0.5, 패배한 경우 0을 가집니다. K factor는 경기 결과가 점수의 증감에 얼마나 영향을 미치는지 나타내는 파라미터입니다. 만약 나의 &lt;strong&gt;예상 승률(Expected)&lt;/strong&gt;이 낮은 상황에서 예상치 못한 승리를 거두면 점수가 큰 폭으로 증가합니다. 반대로 상대방은 예상 승률 대비해서 패배를 경험했기에 큰 폭으로 점수가 감소하게 됩니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/VnOVLBbYlU0?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;순위 시스템 : ELO, TrueSkill 그리고 나만의 시스템&lt;/p&gt;
&lt;p&gt;Elo는 간단하고 객관적인 점수 시스템이며 많은 연구에서 신뢰성이 높다고 평가 받았고 지금도 많은 온라인 게임에서 사용하는 점수 시스템이지만, 다음 같은 문제가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;시간이 지나면 점수의 인플레이션이 발생한다.&lt;/li&gt;
&lt;li&gt;점수가 너무 점진적으로 증가한다. 내가 굉장히 재능있는 플레이어라면 진짜 점수로 수렴하기 까지 많은 경기 회수가 필요하다.&lt;/li&gt;
&lt;li&gt;팀 경기에서 적용한다면 내가 얼마나 활약했는지와 무관하게 오로지 경기 결과로만 평가된다.&lt;/li&gt;
&lt;li&gt;시간에 따른 숙련도 감소를 표현하지 않는다. 1년 동안 게임을 접었다가 복귀했는데 게임의 변화된 시스템이나 경기 규칙과는 무관하게 나의 점수는 그대로 유지되어 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="glicko"&gt;Glicko&lt;/h2&gt;
&lt;p&gt;Glicko[6]는 1995년 Mark Glickman이 개발한 Elo 시스템의 발전된 버전인 평가(Rating) 시스템입니다. Elo에 영감을 받은 많은 평가 시스템들은 유저의 능력치가 정규 분포를 따른다고 가정하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-eab1cf7d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-eab1cf7d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lcn-MsvIuotXxUGw7hhvGg-a360666234d0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-eab1cf7d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lcn-MsvIuotXxUGw7hhvGg-a360666234d0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=VnOVLBbYlU0&amp;amp;t=964s"&gt;Ranking Systems: Elo, TrueSkill and Your Own&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Elo 시스템에선 플레이어의 능력치를 나타내는 정규분포 &lt;em&gt;N&lt;/em&gt;(&lt;em&gt;μ&lt;/em&gt;, &lt;em&gt;σ&lt;/em&gt;²)에서 평균이 점수가 되고요, 표준편차 &lt;em&gt;σ는 고정된 값이라고 가정하고 있습니다. Glicko 시스템은 단순합니다. 이 표준편차 σ가 플레이어마다 다르다고 가정한겁니다.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Glicko system therefore extends the Elo system by computing not only a rating, which can be thought of as a “best guess” of one’s playing strength, but also a “ratings deviation” (RD) or, in statistical terminology, a standard deviation, which measures the uncertainty in a rating&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Glicko 시스템은 플레이어의 능력치를 나타내는 점수와 더불어 점수 편차(Ratings Deviation : RD)값을 같이 계산합니다. 이 값은점수에 대한 확실함을 가지는 정도를 표현합니다. 정규분포에서는 95%의 값들이 평균(μ)에서 ±2σ 에 존재합니다. Glicko 시스템도 플레이어의 점수가 1700점이고 RD가 200점이라면 플레이어 능력이 구간 [1500 ~ 1900]에 95%의 확신으로 존재한다고 생각합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b03da971"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b03da971"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s4tliBDJhYvE-IjAQtNLTQ-818f206237a3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b03da971"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s4tliBDJhYvE-IjAQtNLTQ-818f206237a3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;경기를 시작하기 전에 RD를 새로운 값으로 교체합니다. 유럽 프리미어 축구, 리그 오브 레전드 대회는 시즌제로 운영되고 선수마다 상이한 공백기가 존재합니다. RD는 이런 공식경기에서 모습을 비춘 기간에 따른 실력 변화를 가정하고 있고 공백기가 클수록 RD값이 더 커지는 성격을 가지고 있습니다. RD의 최소값 350은 기록이 없는 선수에게 할당되는 초기값입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e97bcd25"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e97bcd25"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-hrmvl8V3pcQQPEs8SlWOgg-b5660829e54c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e97bcd25"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-hrmvl8V3pcQQPEs8SlWOgg-b5660829e54c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;갑자기 수학식이 등장하니까 머리가 멍해지고, 가슴이 답답해지는게 느껴지는데요. 심호흡 한 번 하시고 용기를 가지고 눈을 크게 뜨고 잘 보면 &lt;strong&gt;사실 Glicko는 2개의 숫자만 다루는 간단한 시스템입니다.&lt;/strong&gt; 여기서는 저 수학식이 어떻게 유도되었는지 설명하지 않겠습니다. 논문[6] 에서도 자세한 설명이 없었고 제가 수학적 배경이 부족해서 하나 하나가 어떤 의미를 가지는지 이해할 수는 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-262ae01e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-262ae01e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--MGDbZ5B9b2E8zzdn98oVA-4d4c581e0624.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-262ae01e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--MGDbZ5B9b2E8zzdn98oVA-4d4c581e0624.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 수식부터 출발하겠습니다. 경기 결과 이후에 기존에 가지고 있던 점수(r)과 점수편차(RD)는 각각 새로운 값인 r’과 RD’으로 갱신됩니다. 수식을 잘 보시면 (1) q는 ln10/400인 상수입니다. Glicko 시스템은 2명 이상이 참여하는 경기에 대해서 계산할 수 있습니다. 자신 이외의 상대방의 수를 의미하는 m이 존재합니다. 시그마 수식 안에 있는 &lt;strong&gt;g(RD)&lt;/strong&gt;도 다른 변수 없이 바로 계산할 수 있는 값이죠&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a9cb481c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a9cb481c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lqgrv_dUbVrCg3SulWyifg-980ac6aef502.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a9cb481c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lqgrv_dUbVrCg3SulWyifg-980ac6aef502.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;나와 상대방 점수를 가지고 계산하는 조건부 확률 기대 승률 E의 수식은 Elo에서 쓰던 것과 유사한데요 (r-r_j)에 g(RD)를 추가로 곱해준 모습입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5fa812f9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5fa812f9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LrUDra2VJPmrPb-CtIzgtQ-a14834b98153.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5fa812f9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LrUDra2VJPmrPb-CtIzgtQ-a14834b98153.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정리하자면 매 경기마다 우리가 갱신하고자 하는 값은 점수(r)과 점수편차(RD)이지만, 계산에 이용되는 q, g(RD), d², E(s|r,r_j,RD_j)는 눈을 크게 뜨고 잘보면 쉽게 계산할 수 있는 값들입니다. 이렇게 보면 구현이 엄청 복잡한 알고리즘은 아니죠? 눈에 보이는대로 계산하면 됩니다.&lt;/p&gt;
&lt;p&gt;Elo에서는 내 점수가 50점이 오르면 상대방은 50점이 내려가는 반면, Glicko는 그렇게 동작하진 않습니다. Glicko의 점수는 양측 선수들의 r과 RD값에 의존하여 변동됩니다. Glicko는 현재 전세계 최대 게임 플랫폼 Steam을 가진 Valve에서 유통하는 게임인 카운터 스트라이크와 팀 포트리스[7], 그리고 Dota2[8] 등에 적용되어 있습니다.&lt;/p&gt;
&lt;h2 id="trueskill"&gt;TrueSkill™&lt;/h2&gt;
&lt;p&gt;TrueSkill™[9]은 마이크로소프트가 Xbox Network에서 매치메이킹에 이용하기 위해 만든 평가 시스템입니다. TrueSkill은 베이즈 통계학에 기반한 머신러닝 기법을 사용하고 있습니다. 2018년에 &lt;strong&gt;TrueSkill 2&lt;/strong&gt;[10]가 추가로 발표되었는데 크게 달라진게 아니여서 근간이 되는 TrueSkill를 먼저 보시고, 나중에 TrueSkill 2를 보셔도 무방합니다.&lt;/p&gt;
&lt;p&gt;Elo와 Glicko가 체스 경기를 염두해두고 설계되었다면, TrueSkill은 처음부터 온라인 게임을 위해서 설계된 평가 시스템입니다. 그래서 마이크로소프트는 본인들이 만들게 될 평가 시스템의 요구사항을 다음과 같이 정리했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;팀 게임을 지원할 것 : 팀 사이즈에 상관없이 일반적으로 적용할 수 있는 알고리즘이어야 한다.&lt;/li&gt;
&lt;li&gt;Skill Rating을 바꿀 수 있을 것, 과거에 얼마나 많은 게임을 했던 상관없이 Skill Rating 변경이 가능해야 한다.&lt;/li&gt;
&lt;li&gt;현재 작동하고 있는 매치메이커와 호환성을 가질것 (Elo, Glicko 등을 사용하고 있었어도 TrueSkill로 큰 변경없이 전환이 가능해야 한다)&lt;/li&gt;
&lt;li&gt;게임의 본질과 일치하는 인센티브를 만들어야 한다. 예를 들어, 힐을 많이 주는 사람한테 점수를 주는 시스템은 플레이어가 스스로 자해하고 힐을 하는 걸로 악용할 수 있다.&lt;/li&gt;
&lt;li&gt;학습에 필요한 데이터는 최소화 해야 한다. 게임을 처음 런칭한 직후에 바로 매치메이킹이 동작해야 함&lt;/li&gt;
&lt;li&gt;연산 코스트가 작을 것&lt;/li&gt;
&lt;li&gt;게임의 시스템이 변경되어도 스킬 시스템 자체의 변경이 크지 않을 것&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="여기서-trueskill-2는-아래-6가지-사항을-추가로-최적화-했습니다"&gt;여기서 TrueSkill 2는 아래 6가지 사항을 추가로 최적화 했습니다.&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;플레이어의 잠재 스킬은 개별적인 통계 수치에 기반한다. e.g. 개인적인 KDA와 팀 성적 결과 (승리,패배)&lt;/li&gt;
&lt;li&gt;게임을 하다가 탈주하면, 포기한 것으로 간주하고 게임에서 졌다고 가정하고 값을 갱신한다.&lt;/li&gt;
&lt;li&gt;어떤 게임 모드에서 플레이어의 능력치는 다른 게임 모드의 능력과 관계 있다고 가정한다. e.g. 리그 오브 레전드에서 협곡과 칼바람&lt;/li&gt;
&lt;li&gt;첫 게임의 결과에 대해 큰 편향치(bias)를 가진다.&lt;/li&gt;
&lt;li&gt;스쿼드를 꾸리는 경우 평소보다 더 퍼포먼스가 높다고 가정한다. 배틀그라운드에서 솔플보다 스쿼드가 더 유리하니까 1000점을 가진 솔플러와 300점대로 구성된 4명의 스쿼드를 동등하게 평가할 수 있다.&lt;/li&gt;
&lt;li&gt;시간에 따른 실려의 불확실성 증가를 반영했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;논문에서는 TrueSkill을 구현하기 그래프 모델에서 베이즈 추론을 하는 프레임워크인 Infer.NET[12]을 사용했다고 했습니다. 깃허브에 방문하시고 코드를 구경하다보면 TrueSkill을 구현한 &lt;a href="https://github.com/dotnet/infer/blob/885a5e561e0d4adda2d7a629409d72aac46d8b96/test/Tests/InferTests.cs#L5983-L6016"&gt;테스트 코드&lt;/a&gt;를 발견하실 수 있습니다. 혹시 적용할 계획이 있으시면 해당 코드를 참고하면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;지금부터는 제 나름대로 이해한 TrueSkill의 원리에 대해 설명해보려고 합니다. 아무래도 통계 기반 지식이 부족해서 충분할 설명이 안될 것 같습니다. TrueSkill에 대한 자세한 내용은 Moserware 블로그[11]에서 더 쉽고 자세히 설명되어 있으니까요. 제 설명이 부족하시다면 해당 블로그 글을 참고해주시기 바랍니다. TrueSkill을 설명하기 전에 베이즈 통계학에 대한 이야기 부터 시작해야합니다.&lt;/p&gt;
&lt;p&gt;우리가 교과과정에서 가장 먼저 만나는 통계는 빈도주의 통계학입니다. 동전을 던져서 앞면이 나올 확률은 50%입니다. 전체 집합의 개수와 앞면이 나올 빈도수를 세어보고 연역적 사고로 확률을 계산합니다. 동전을 5번 던져보고 결과가 &lt;strong&gt;HTHHT&lt;/strong&gt;가 나온 것을 보고 앞면이 나올 확률은 3/5 이구나 하지 않죠&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-434aff11"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-434aff11"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E4k0EApVUdXKJXSA5e-ALg-7e080e9637f4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-434aff11"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E4k0EApVUdXKJXSA5e-ALg-7e080e9637f4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;베이즈 통계학은 빈도주의와 달리 데이터에 따라 설정한 파라미터에 대한 신념을 평가합니다. 베이즈 정리는 가설(Hypothesis)과 새로운 정보 (Evidence)가 존재하면 새로운 정보에 대해 가설을 업데이트 하는 방법 P(H|E)를 표현합니다. 연역적 사고로 통계를 표현하는 빈도주의와 달리 베이지안 주의는 경험을 통해 확률을 갱신하는 귀납적인 접근 방법이라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;TrueSkill은 플레이어들의 능력치 s와 세션 내의 팀 할당 정보인 A를 가지고 있을 때 경기 결과(r)의 확률 분포를 $P(r\mid s,A)$로 정의합니다. 이 식 자체는 나의 현재 능력치를 가지고 경기 결과를 얻는 것이지만, &lt;strong&gt;우리가 계산하고 싶은 건 경기 결과를 보고 능력치를 어떻게 갱신할 것인가이죠&lt;/strong&gt;. 이는 베이즈 정리에 의해 다음과 같이 유도됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d8974b4e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d8974b4e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Q-Lls83342yOTJOX9JupLw-971091b9426a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d8974b4e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Q-Lls83342yOTJOX9JupLw-971091b9426a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;플레이어들의 능력치의 분포를 나타내는 사전 확률 분포 p(s)는 플레이어들의 능력치의 결합 확률 분포라고 가정하고 인수분해(Factorization) 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-bc84dad9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-bc84dad9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gZHbSeC-pDg5eDI8aK6ZNw-23a8e2e56e41.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-bc84dad9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gZHbSeC-pDg5eDI8aK6ZNw-23a8e2e56e41.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;TrueSkill도 플레이어의 능력치를 정규분포로 표현하고 있습니다. 그리고 경기에서 보여준 실제 퍼포먼스는 고정된 분산값 $\beta^2$와 플레이어의 개별 능력치 s_i를 평균으로 가진 정규분포를 따른다고 가정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-72c0da05"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-72c0da05"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D_PXbfbLHixkguzqYhtEoA-2ccff1795d3e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-72c0da05"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D_PXbfbLHixkguzqYhtEoA-2ccff1795d3e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그리고 게임의 &lt;strong&gt;팀 퍼포먼스&lt;/strong&gt;는 개별 플레이어들이 보여주는 퍼포먼스의 총합입니다. 팀 퍼포먼스 까지는 이미 결정된 값입니다. 플레이어의 스킬을 나타내는 &lt;strong&gt;평균(μ)&lt;/strong&gt;과 &lt;strong&gt;분산(σ)&lt;/strong&gt;은 경기 시작 전에 이미 주어진 값이고, 퍼포먼스 pi 는 고정된 분산값을 가진 정규 분포일 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1e83583e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1e83583e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0OsS0pu8EFNZAcnkN7h48A-7b29afe77a8e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1e83583e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0OsS0pu8EFNZAcnkN7h48A-7b29afe77a8e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;팀 퍼포먼스 까지 계산했는데요. 팀 퍼포먼스가 더 큰 팀 순서대로 게임에서 순위를 차지할 것으로 가정해서 확률 분포를 정의합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dc0b4e59"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dc0b4e59"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BC8QHWGsZClL4k_G2sn_ng-25a27afcf06b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dc0b4e59"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-BC8QHWGsZClL4k_G2sn_ng-25a27afcf06b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 조건부 확률은 단순히 팀 성적대로 순위가 매겨질 것이라고 가정한 값이고 &lt;strong&gt;실제로는 관찰된 순위로 팀 성적을 정렬한 다음에 성적을 맵핑하니다.&lt;/strong&gt; 게임에서 보여준 두 팀의 성적차(d)를 계산하는데요. 이 성적차이의 절댓값이 미리 설정한 상수인 &lt;strong&gt;엡실론(ε : draw margin)&lt;/strong&gt; 이내에 있으면 비등한 경기였다고 해석합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a04eb872"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a04eb872"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ksp6FtvO5WJZmqi0QPbntg-2dc73e813b83.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a04eb872"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ksp6FtvO5WJZmqi0QPbntg-2dc73e813b83.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="trueskill--factor-graph"&gt;TrueSkill : Factor Graph&lt;/h3&gt;
&lt;p&gt;Factor Graph[13]는 확률론에서 확률 분포 함수를 인수분해 해서 계산할 때 표현하는 그래프를 말합니다. 위에서 소개했던 사전 확률 p(s)와 가능도 함수 p(r|s)를 Factor Graph로 표현하면 다음과 같이 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;지금부터는 제가 이 분야를 제대로 아는게 아니라서 &lt;br&gt;
그냥 앵무새 처럼 문서에 나와있는 대로 읊었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-94d38183"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-94d38183"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-d-ukUaDNsMEulSTLVXMUAQ-7e304e709516.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-94d38183"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-d-ukUaDNsMEulSTLVXMUAQ-7e304e709516.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;어우 무섭게 생긴 그래프인데요. 눈을 크게 뜨고 잘보면 전부 위에서 했던 이야기 입니다. 이 이분 그래프(Bipartite Graph | 인접한 노드끼리 색상을 다르게 색칠할 수 있는 그래프)에서 원으로 생긴 그래프에 표현된 s, p, t, d에 대한 정의는 위에서 모두 소개했습니다.&lt;/p&gt;
&lt;p&gt;게임이 모두 끝나면 경기 결과 d_i에 대한 &lt;strong&gt;주변 확률(Marginal Distribution)&lt;/strong&gt;을 계산 해야 합니다. 논문에서는 Message Passing 알고리즘과 KL Divergence를 사용한다는 언급이 있었는데요. 배경 지식이 부족해서 자세히 이해하진 못했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6b44296a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6b44296a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JKYp3WQDDqheFS6klI6vuA-7b5f375c5c40.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6b44296a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JKYp3WQDDqheFS6klI6vuA-7b5f375c5c40.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그럼 이걸 어떻게 계산하느냐? 위 그림에서 각 그래프 생긴 모양대로 계산 방법을 친절하게 알려주고 있는데요. 기반이 되는 이론은 베이즈 통계학 기반의 머신러닝 학문에서 다루는 &lt;strong&gt;Belief Propagation&lt;/strong&gt;을 토대로 2001년에 논문으로 발표된 &lt;a href="https://en.wikipedia.org/wiki/Expectation_propagation"&gt;Expectation Propagation&lt;/a&gt;을 사용합니다.&lt;/p&gt;
&lt;p&gt;수식의 의미를 깊이 이해하려면 굉장히 먼 길을 돌아가야 할 것 처럼 보입니다. 😔 다행히도 우리는 마이크로소프트 형님들이 미리 만들어주신 Infer.NET을 사용하면 쉽게 계산할 수 있습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;[]&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;[&lt;/span&gt;&lt;span class="n"&gt;playerMeans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;InferenceEngine&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InferenceEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ExpectationPropagation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;marg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;marg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Infer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Skills[&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;]=&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;marg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;TrueSkill의 수학적 배경에 대한 내용은 The Math behind TrueSkill[14]에서 좀 더 자세히 다루고 있으니 참고바랍니다. 복잡한 수학적 배경을 다 빼고 간단하게만 다루면 이길 것으로 예상한 팀이 이기면 점수가 조금 반영되고, 예상 외로 질 것 같은 팀이 이기면 점수가 크게 반영됩니다. TrueSkill은 얼마나 크게 이겼는지는 따지지 않고 경기 결과만 성적에 반영합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-67fa7275"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-67fa7275"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8BJAMHBms16_6SA9bqtyZw-bddd8684df51.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-67fa7275"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8BJAMHBms16_6SA9bqtyZw-bddd8684df51.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;팀 성적 차이(= t)&lt;/strong&gt;가 음수라는 말은 이길 것으로 생각되었던 팀이 졌다는 걸 의미합니다.&lt;/p&gt;
&lt;h3 id="trueskill--experiment"&gt;TrueSkill : Experiment&lt;/h3&gt;
&lt;p&gt;지금부터는 논문에서 TrueSkill을 평가하는데 이용한 3가지 지표를 소개하려고 합니다. 순서대로 (1) Match Quality, (2) Win Probability, (3) Convergence Properties 입니다. 실험에서 사용한 데이터는 Xbox의 대표 타이틀인 헤일로 시리즈의 베타 테스트에서 수집한 자료입니다.&lt;/p&gt;
&lt;h3 id="1-match-quality"&gt;(1) Match Quality&lt;/h3&gt;
&lt;p&gt;위에서 두 팀 t1, t2의 성적 차이를 d = t1-t2로 계산했고 |d| ≤ $\epsilon$ 이면 비등한 경기였다고 평가한다고 했습니다. 이 때 &lt;strong&gt;엡실론(ε)&lt;/strong&gt; 값을 draw margin이라고 합니다. 이 draw margin은 경험적으로 타이트한 게임이 되는 확률 분포에서 가져와서 사용했다고 합니다. Halo 5 에서는 1/10³으로 설정했다고 합니다.&lt;/p&gt;
&lt;p&gt;TrueSkill이 원하는 건 경기 결과가 공정하게, 다르게 말하면 비등 비등한 경기가 되도록 매칭하는 겁니다. 아래 그래프는 Elo와 TrueSkill간의 비등한 경기 비율을 보여줍니다. TrueSkill이 Free for All(8명 끼리 서로 대전하는 모드)와 Head To Head(1 vs 1) 모드에서 높은 매칭 퀄리티를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f84f3447"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f84f3447"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CWKE870maKjhaflXKOVIZg-a90e15ea9545.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f84f3447"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CWKE870maKjhaflXKOVIZg-a90e15ea9545.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Elo vs TrueSkill 매칭 퀄리티 비교&lt;/p&gt;
&lt;h3 id="2-win-probability"&gt;(2) Win Probability&lt;/h3&gt;
&lt;p&gt;다음 그래프는 승률 분포를 보여줍니다. 승률이 크게 나온다면 잘하는 사람이 상대적으로 실력이 부족한 사람과 많이 매칭되었다는 것을 나타냅니다. TrueSkill은 게임 수가 적어도 승률이 35% ~ 65% 사이에 나타나도록 공정한 게임을 매치했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-91764fee"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-91764fee"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CdqUeQhvvt9wnIoellwlQw-da5b6defec53.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-91764fee"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CdqUeQhvvt9wnIoellwlQw-da5b6defec53.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Elo vs TrueSkill 승률 편차 비교&lt;/p&gt;
&lt;h3 id="3-convergence-properties"&gt;(3) Convergence Properties&lt;/h3&gt;
&lt;p&gt;Elo는 점수가 너무 느리게 수렴한다는 단점을 가지고 있습니다. 다음 그래프는 TrueSkill은 적은 게임 수에도 빠르게 점수가 수렴한다는 장점을 가지고 있습니다. 실력이 챌린저급인 선수는 빠르게 점수를 올려줘야 실력차이에 따른 불쾌한 경험(양학)을 당하는 게임 수가 줄어들 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8248a75d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8248a75d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j3NSIzeqqV7KlOAFWsgf9w-282970c952e7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8248a75d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j3NSIzeqqV7KlOAFWsgf9w-282970c952e7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="trueskill--마무리"&gt;TrueSkill : 마무리&lt;/h2&gt;
&lt;p&gt;TrueSkill 2는 게임 중간에 탈주한 사람이 있는지, 게임이 비록 패배했지만 나는 잘했는지(내 탓 아님 = K/D/A 성적을 반영)를 추가로 반영했는데요. Factor Graph에서 노드를 추가한 방식이기 때문에 Expectation Propagation을 이용하는 기본 원리는 동일합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1cf54212"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1cf54212"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-N4rcj9iAlfl1dEOJAp10NA-1ce08d2fc5e1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1cf54212"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-N4rcj9iAlfl1dEOJAp10NA-1ce08d2fc5e1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;지금까지 대표적인 실력 평가 방법인 Elo, Glicko, TrueSkill을 다루었습니다. 이런 스킬 평가 시스템은 대전 게임을 스포츠라는 시각을 가지면서 공정한 혹은 비등한 게임이 되도록 매칭하는 것을 목표로 하고 있습니다.&lt;/p&gt;
&lt;p&gt;이제부터는 게임을 스포츠가 아닌 서비스로 바라보는 시각에서 출발한 2개의 매치메이킹 알고리즘을 소개해드리려고 합니다. TrueSkill이 지면이 꽤 많았는데요. 지금부터는 빠르게 소개드리고 마무리 합니다.&lt;/p&gt;
&lt;h2 id="eomm"&gt;EOMM&lt;/h2&gt;
&lt;p&gt;EOMM[15]은 2017년, EA와 UCLA가 공동으로 연구하로 발표한 매치메이킹 프레임워크입니다. EOMM은 게임을 스포츠가 아닌 서비스로 바라보면서 이탈율(churn rate)을 최소화 하는 것을 목표로 합니다.&lt;/p&gt;
&lt;p&gt;그래서 EOMM이 활용하는 정보가 TrueSkill 같은 정교한 평가 시스템에만 국한되지는 않고, 게임 설치 날짜, 얼마나 잦은 주기로 플레이 하는지 등 데이터를 사용합니다. 어차피 로지스틱 회귀를 돌릴 거라서 게임 결과에 영향을 줄 것 같은 요소를 포함시키면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4bfec939"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4bfec939"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k65RdSoRm1xzyy_9KPebQQ-d201f9ab78c7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4bfec939"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k65RdSoRm1xzyy_9KPebQQ-d201f9ab78c7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;EOMM의 매치메이킹은 플레이어 풀에서 플레이어 간의 관계를 완전 그래프로 표현합니다. 그래프에서 정점은 플레이어 상태를 나타내며 플레이어 프로필 정보들을 가지고 있습니다. 그리고 간선으로 이어진 두 플레이어의 관계를 &lt;strong&gt;확률분포 P(c|s_i, s_j)&lt;/strong&gt;로 표현하는데요. 이는 si 플레이어가 sj와 게임하고 난 후 게임을 이탈하는 비율을 말합니다.&lt;/p&gt;
&lt;p&gt;정리하면 EOMM은 두 플레이어의 예상 이탈율을 최소화 하도록 매치메이킹을 해야 하는데요. 이 때 아직 발견되지 않은 이탈 확률 분포는 로지스틱 회귀를 돌려서 계산합니다. c(si,sj)는 플레이어 i가 이탈할 확률을, c(sj,si)는 플레이어 j의 이탈율이기 때문에 이 총합이 최대한 작아야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-bd784fa3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-bd784fa3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uqav79FRxO29W7K-pCV8fQ-9966fa219f6d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-bd784fa3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uqav79FRxO29W7K-pCV8fQ-9966fa219f6d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="eomm-이론적-배경"&gt;EOMM: 이론적 배경&lt;/h2&gt;
&lt;p&gt;여기서 이탈율(churn)을 나타내는 함수 c는 로지스틱 회귀 모델로 구하는데요. 회귀 모델이 분류하고자 하는 값은 플레이어가 게임 이후에 (1) 다음 게임을 진행하는지 혹은 (2) 게임을 종료하고 8시간 동안 나타나는 않는지 입니다.&lt;/p&gt;
&lt;p&gt;만약 모든 매칭의 이탈율에 대해서 &lt;strong&gt;c(승리) + c(패배) &amp;gt; 2 &lt;/strong&gt; c(비등함)의 결과를 보여준다면, 위에서 다루었듯 TrueSkil이 항상 비등한 경기를 추구하기 때문에 그냥 SBMM을 사용하면 될 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f0be8b2b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f0be8b2b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-HHOZ8CTBkUKsMRoabhDKNw-a00cf7f88535.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f0be8b2b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-HHOZ8CTBkUKsMRoabhDKNw-a00cf7f88535.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그런데 만약 &lt;strong&gt;c(승리) + c(패배) &amp;lt; 2 &lt;/strong&gt; (비등함)&lt;strong&gt; 이라면 어떨까요? 매번 비등한 경기를 추구하는 SBMM이 플레이어가 게임에 몰입하는데 방해요소로 작용합니다. 물론 이런 가정이 &lt;/strong&gt;공평한 게임이 좋은 게임이다라는 직관과는 모순되긴 해도 실제 게임에서는 이 현상이 충분히 발생할 수 있습니다.&lt;/p&gt;
&lt;h2 id="eomm--예측-모델-및-그래프-매칭"&gt;EOMM : 예측 모델 및 그래프 매칭&lt;/h2&gt;
&lt;p&gt;로지스틱 회귀 모델은 고객이탈예측 모델을 개발하는데 많이 사용되어 왔습니다. 아래 (4)번 수식은 위에서 봐왔던 대로 두 플레이어를 매칭했을 때 계산되는 이탈율을 의미합니다. 이를 (5)번 수식과 동일한 것으로 보는데요. o(i,j)는 플레이어 i의 경기 결과를 의미합니다. o(i,j)가 승리라면 반대로 o(j,i)는 패배가 됩니다. 다시 말하면, &lt;strong&gt;승리,패배,무승부의 경우에 계산되는 이탈율의 합으로 나타냅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(6)에서 경기 결과에 대한 확률 분포를 구할 때, 플레이어의 능력치 s가 μ로 바뀌는데 이 값은 Elo, Glicko와 같은 평가 시스템을 도입했을 때 사용하는 값입니다. 마지막으로, 이탈 함수 c에서 상대방 플레이어 정보가 제거되었는데요. 논문에서는 경기 결과에 대한 확률 분포를 구했다면 이탈율은 상대방 정보와 독립적일 수 있다고 하는데요. 저는 랜덤 매칭보다 EOMM이 더 낫다는 가정하에 설정된 수식이라고 이해했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-16df3660"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-16df3660"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-961cmr5AIiteC0QBeqGPeA-baa0c93ac28f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-16df3660"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-961cmr5AIiteC0QBeqGPeA-baa0c93ac28f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;EOMM의 예측모델은 이 이탈율을 계산하기 위해 로지스틱 회귀를 사용합니다. 회귀 모델의 입력값으로는 플레이어의 상태값과 플레이어의 최근 10경기, 지난 경기 대비 흐른 시간 등을 참고해서 플레이어가 경기 이후에 8시간 이내로 게임을 즐길 것인지 아니면 게임을 이탈해버릴 것인지를 분류합니다.&lt;/p&gt;
&lt;p&gt;예측 모델 까지 준비가 되었으니, 이제 플레이어 풀에서 실제 매칭을 해야하는데요. 완전 그래프에서 각 정점을 &lt;strong&gt;중복 없이&lt;/strong&gt; 1:1로 매칭하는 문제를 &lt;strong&gt;완전 매칭(perfect matching)&lt;/strong&gt;문제라 부릅니다. EOMM은 가중치의 합을 최소화 하는 것이 목적이기에 최소 가중치 완전 매칭(WMPM)이라고 부릅니다. 대표적으로 &lt;strong&gt;헝가리안 알고리즘&lt;/strong&gt;[16][17]으로 계산할 수 있습니다 .알고리즘 자체는 &lt;strong&gt;시간 복잡도가 O(N³)&lt;/strong&gt;으로 느린편이긴 하지만, 매칭 풀에 플레이어 수가 평균 500명 정도라면 평범한 스펙을 가진 서버에서 1~2초 정도에 전부 매칭시킬 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;온라인 경쟁 프로그래밍 대회에서 수행 시간 1초를 사용하는데 필요한 시간복잡도가 흔히 10⁸인 것으로 알려져 있습니다. 이건 어떤 스펙의 서버를 사용하느냐에 따라 다를 수 있습니다. &lt;strong&gt;추가로 EOMM을 사용한다면 완전 그래프를 형성해야 하는 만큼 매칭 서버가 수평 확장하는 것을 기대하기 어렵겠다는 생각을 햇습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4bfec939"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4bfec939"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k65RdSoRm1xzyy_9KPebQQ-d201f9ab78c7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4bfec939"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k65RdSoRm1xzyy_9KPebQQ-d201f9ab78c7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="eomm-평가"&gt;EOMM 평가&lt;/h2&gt;
&lt;p&gt;EOMM의 평가에는 알아두면 좋을 내용들이 몇 가지 있습니다. 아래 표는 EA가 운영하는 게임에서 데이터를 가져 왔구요. 플레이어들이 마지막 3경기를 어떤 양상으로 보냈는지에 따른 이탈율을 보여줍니다.&lt;/p&gt;
&lt;p&gt;예를 들어 연속된 3번의 승리(WWW)를 경험한 플레이어의 이탈율은 3.7% 였고, 두번의 패배와 한 번의 승리(LLW)를 경험한 사람은 2.6%, 3번 연속 패배한 사람은 5.1%로 가장 높은 이탈율을 보여주었습니다. 계속 이기기만 하는 게임의 이탈율이 제법 높았다는 게 재밌었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-237959b7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-237959b7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-chIIA_EG8p65HwtfIgRdgQ-75027ebccd2e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-237959b7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-chIIA_EG8p65HwtfIgRdgQ-75027ebccd2e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;EOMM의 성능 평가 부분에서는 스킬 기반의 SBMM과 랜덤 매칭을 비교합니다. 아래 표는 한 번의 매칭 이후에 플레이어 잔존율을 보여주는데요. SBMM과 비교해서 최대 1.1% 정도의 성능 향상을 보여줍니다. 당장은 크기가 작아보이지만 같은 플레이어가 EOMM 매칭을 20번 정도 경험하면 SBMM대비 15% 높은 잔존율을 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d61c6424"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d61c6424"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cC9IEcwn05AiDum5NY63Zg-06bf95a2c0ad.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d61c6424"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cC9IEcwn05AiDum5NY63Zg-06bf95a2c0ad.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="enmatch"&gt;EnMatch&lt;/h2&gt;
&lt;p&gt;EnMatch는 올해 개최된 AAAI’24 컨퍼런스에서 중국의 대형 게임사인 넷이즈(NetEase)산하의 Fuxi AI Lab에서 발표한 매치메이킹 알고리즘입니다.&lt;/p&gt;
&lt;p&gt;EOMM이 전통적인 머신러닝 기법인 로지스틱 회귀를 이용했다면 EnMatch는 구글이 조합 최적화 문제(e.g. &lt;a href="https://www.acmicpc.net/problem/1708"&gt;Convex Hull&lt;/a&gt;, &lt;a href="https://www.acmicpc.net/problem/2098"&gt;외판원 순회&lt;/a&gt;)를 인공신경망으로 계산할 수 있도록 개발한 포인터 네트워크[19]를 활용한게 특징입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;포인터 네트워크에 대한 자세한 설명은 별도 포스팅에서 따로 다루려고 합니다. 이 포스팅에서는 EnMatch에 대한 원리보다는 실험 결과 위주로 소개하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="enmatch-평가"&gt;EnMatch 평가&lt;/h2&gt;
&lt;p&gt;넷이즈의 Fuxi AI Lab에서도 본인들이 운영하는 게임의 라이브 데이터를 가지고 실험을 진행했습니다. EOMM에서 확장해서 경기 도중에 주고받은 채팅 수와 좋아요(Upvote), 싫어요(Downvote) 버튼을 누른 회수를 수집했습니다.&lt;/p&gt;
&lt;p&gt;그리고 실력이 비등한 사람끼리 매칭한 게임 결과(fair)와 고수준의 플레이어와 실력이 부족한 플레이어 끼리 팀을 이루게 한 경우(diversity)의 사회적인 인터랙션을 분석합니다. 다양성을 갖춘 팀 일수록 뚜렷하게 높은 사회적인 상호작용을 보여주는데요. 이를 논문에서는 &lt;strong&gt;Collaboration Engagement&lt;/strong&gt; 라고 부릅니다. 그리고 다음과 같이 결론을 냅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 결과는 공평한 게임만으로는 좋은 유저 경험을 유지할 수 없습니다. cross-tier 매치메이킹은 유저의 몰입을 최적화 하기 위해 필연적으로 사용되어야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;the results also show that ensuring fair games alone is not suffcient to maintain a good user experience, and cross-tier matchmaking is necessary to optimize player engagement&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8327e564"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8327e564"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SGB6PDKbHFSwWe8xCTwdPQ-951e045347e9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8327e564"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SGB6PDKbHFSwWe8xCTwdPQ-951e045347e9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;EOMM이 마지막 3경기 결과를 토대로 이탈율을 분석했다면, EnMatch는 플레이어가 선택한 직업군에 따른 이탈율을 분석했습니다. 논문에는 어떤 게임의 데이터인지는 안나와있지만 넷이즈가 오버워치를 퍼블리싱하고 있으니까 대입해서 상상해 봅시다.&lt;/p&gt;
&lt;p&gt;공격군 캐릭터(Main)로 3번 이긴 유저의 이탈율보다, 지원형 캐릭터(Support)로만 3번 이긴 유저의 이탈율이 더 높았습니다. 반대로 패배한 경우에는 공격군으로 연속해서 진 사람의 이탈율이 8.4%로 가장 높았는데요. 지원형 캐릭터를 고른 유저보다 패배에 따른 좌절감을 더 크게 느끼나 봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1c79864b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1c79864b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ep4ezv4l58m2kia17TXtig-c994ca1ede56.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1c79864b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ep4ezv4l58m2kia17TXtig-c994ca1ede56.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이제 모든 걸 다 건너 뛰어서 실험 결과에 대한 내용입니다. 학습에 사용된 데이터셋은 SPG, RPGPVP라는 이름으로 등장한 2개의 게임 데이터를 사용했습니다. 모델이 알고싶은 건 플레이어가 게임이 끝난 이후에 30분 동안 남아있었는지, 화나서 게임을 꺼버렸는지 여부입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b55c58b4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b55c58b4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--dYHKQElSbRVrmuLbtfSQw-2c70456d032d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b55c58b4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--dYHKQElSbRVrmuLbtfSQw-2c70456d032d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;다음으로 등장하는 건 실험 결과인데요. 분류 모델을 평가할 때 사용하는 메트릭인 정확도(ACC)와 AUC를 비교했고 가장 높은 성능을 보여줍니다. 참고로 평가 표에 등장하는 OptMatch도 같은 연구실에서 발표한 논문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9f531fb5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9f531fb5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-C1PeJsedHNY0Db0dz1DBcA-9c233c79be32.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9f531fb5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-C1PeJsedHNY0Db0dz1DBcA-9c233c79be32.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="enmatch-실제-적용-후기"&gt;EnMatch 실제 적용 후기&lt;/h2&gt;
&lt;p&gt;이 부분이 논문에서 가장 재밌는 파트입니다. 넷이즈는 EnMatch를 실제로 본인들이 운영중인 게임에 적용해서 성능을 평가했습니다. OptMatch(KDD’20)와 GloMatch (KDD’20)를 비교했는데요. 둘 다 같은 연구실에서 예전에 발표한 매치메이킹 논문입니다.&lt;/p&gt;
&lt;p&gt;게임이 끝난 이후에 30분 안에 다시 게임을 진행한 플레이어를 기준으로 평가했으며 EnMatch가 가장 높은 성능을 보여주었다고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9a64855c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9a64855c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5vsE-MJd5_lgwoxpCEecDQ-181f4d75f123.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9a64855c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5vsE-MJd5_lgwoxpCEecDQ-181f4d75f123.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 공정한 게임을 만들기 위해 플레이어의 능력을 객관적으로 평가하고자 하는 Elo, Glicko, TrueSkill을 소개했습니다. 다음으로는 게임을 스포츠가 아닌 서비스로 바라보면서 고객 이탈율을 줄이기 위한 목적으로 설계된 EOMM과 EnMatch 논문을 살펴봤습니다.&lt;/p&gt;
&lt;p&gt;만약 이런 매치메이킹 알고리즘을 진짜로 사용한다고 하면, 실제로는 고려해야 할 변수가 더 많습니다. 한 예시를 들자면 논문에서는 매칭 대기시간은 평가 대상으로 고려하지 않는데요. 하지만, 아마존이 A/B 테스트에서 100ms의 레이턴시가 증가하면 매출이 1% 감소하는 것을 발견하고, 구글이 0.5초의 딜레이가 발생하면 &lt;a href="https://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html"&gt;20% 트래픽 감소&lt;/a&gt;를 경험한 것 처럼 길어진 매칭 대기시간이 주는 부정적인 영향도 함께 고려해야 합니다.&lt;/p&gt;
&lt;p&gt;마지막으로 EOMM으로 부터 시작한 이탈율 개선을 위한 매치메이킹 시스템이 이탈 위험 고객을 위해 게임에 장기적으로 충성하는 유저들의 경험을 해친다는 주장이 있습니다. 이에 대한 자세한 내용은 아래 유튜브에 소개되어 있으니, 흥미 있으신 분들은 같이 보시면 재밌으실 것 같습니다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/s1GqWEyBaXs?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://www.youtube.com/watch?v=-pglxege-gU&amp;amp;t=1307s"&gt;Skill, Matchmaking, and Ranking Systems Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://patents.google.com/patent/US20160005270A1/en"&gt;System and method for driving microtransactions in multiplayer video games&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.callofduty.com/ko/blog/2024/01/call-of-duty-update-an-Inside-look-at-matchmaking"&gt;콜 오브 듀티 업데이트: 매치메이킹 세부 내용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://en.wikipedia.org/wiki/Elo_rating_system"&gt;ELO Rating System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://leetcode.com/discuss/general-discussion/468851/New-Contest-Rating-Algorithm-%28Coming-Soon%29"&gt;New Contest Rating Algorithm — LeetCode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="http://www.glicko.net/glicko/glicko.pdf"&gt;The Glicko system — Dr. Mark E. Glickman at Harvard University&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://store.steampowered.com/oldnews/38643"&gt;TEAM FORTRESS 2 UPDATE RELEASED&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://www.dota2.com/newfrontiers?v=2"&gt;Dota2 New Frontiers Update&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2007/01/NIPS2006_0688.pdf"&gt;TrueSkill™ : A Bayesian Skill Rating System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://www.microsoft.com/en-us/research/uploads/prod/2018/03/trueskill2.pdf"&gt;TrueSkill 2: An improved Bayesian skill rating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[11] &lt;a href="https://www.moserware.com/2010/03/computing-your-skill.html"&gt;Moserware : Computing Your Skill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[12] &lt;a href="https://github.com/dotnet/infer"&gt;Infer.NET on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[13] &lt;a href="https://en.wikipedia.org/wiki/Factor_graph"&gt;Factor graph | Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[14] &lt;a href="https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf"&gt;The Math Behind TrueSkill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[15] &lt;a href="https://arxiv.org/pdf/1702.06820"&gt;EOMM: An Engagement Optimized Matchmaking Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[16] &lt;a href="https://gazelle-and-cs.tistory.com/29"&gt;할당 문제 &amp;amp; 헝가리안 알고리즘&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[17] &lt;a href="https://www.acmicpc.net/problem/14216"&gt;BOJ | 할 일 정하기 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[18] &lt;a href="https://ojs.aaai.org/index.php/AAAI/article/view/28760"&gt;EnMatch: Matchmaking for Better Player Engagement via Neural Combinatorial Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[19] &lt;a href="https://research.google/pubs/pointer-networks/"&gt;Pointer Networks at NIPS’2015&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>AI와 게임 개발 / 3D 생성 모델 연구 맛보기</title><link>https://scalalang2.com/p/%ED%8F%89%EB%B2%94%ED%95%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3d-%EC%83%9D%EC%84%B1-%EB%AA%A8%EB%8D%B8-%EC%97%B0%EA%B5%AC-%EB%A7%9B%EB%B3%B4%EA%B8%B0-dream-fusion-by-google-11c3370920ae/</link><pubDate>Tue, 07 May 2024 04:33:35 +0000</pubDate><guid>https://scalalang2.com/p/%ED%8F%89%EB%B2%94%ED%95%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3d-%EC%83%9D%EC%84%B1-%EB%AA%A8%EB%8D%B8-%EC%97%B0%EA%B5%AC-%EB%A7%9B%EB%B3%B4%EA%B8%B0-dream-fusion-by-google-11c3370920ae/</guid><description>&lt;h3 id="nerf에서-dreamfusion-까지"&gt;NeRF에서 DreamFusion 까지&lt;/h3&gt;
&lt;p&gt;1994년 폴란드에 설립된 회사인 &lt;strong&gt;CD PROJECT&lt;/strong&gt;는 본래 게임의 CD를 복제 판매하는 회사였다. 나중에는 직접 게임 개발에 뛰어들었고 위쳐3로 초대박을 치면서 기업 신화의 주인공이 되었다. 이를 발판으로 사이버펑크 장르의 AAA급 오픈월드 게임 &lt;strong&gt;‘사이버펑크 2077’&lt;/strong&gt;을 준비한다. CDPR은 2018년, E3에서 사이버펑크 2077의 게임 플레이 트레일러를 공개했다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/o4wHlc6m68g?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;출시일을 3번이나 연기하는 등, 우여 곡절 끝에 출시된 게임은 영상에서 보여줬던 대부분 기능을 찾아볼 수 없었고 콘솔에서 최적화가 되지 않아 정상적인 플레이가 가능한 수준이 아니었다. &lt;strong&gt;사이버펑크 2077이 공개한 게임플레이 영상은 실제로는 대부분 구현조차 되지 않은 데모 영상일 뿐이었다.&lt;/strong&gt; 논란 끝에 소니는 플레이스테이션에서 게임을 내려 버렸고, 마이크로소프트는 사이버펑크를 설치하려는 유저에게 경고 팝업을 띄우는 조치를 했다. (지금은 애니메이션의 성공과 DLC 출시로 어느 정도 회복되었다)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-59a7a16b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-59a7a16b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-c1MOCqRlLnHp8zPiafdf_A-f378e7dfa1be.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-59a7a16b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-c1MOCqRlLnHp8zPiafdf_A-f378e7dfa1be.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;최근에도 The Day Before라는 게임이 가짜 플레이 영상으로 사용자를 속였고, 얼리 엑세스로 선보인 게임에서 각종 버그는 물론이고, 스팀에서 환불이 가능한 기준인 120분 플레이 시간 정책을 방해하려고 백그라운드에서 게임 프로세스를 계속 돌려두고 있다는 의혹이 있었다. 많은 논란 끝에 개발사는 폐업하였고, 스팀은 조건없이 게임을 전액 환불했다.&lt;/p&gt;
&lt;p&gt;필자는 국내의 한 게임회사에서 평범하게 게임 서버를 개발하고 있다. 그러다 보니 AI와 관련해서 여러 내용을 심심치 않게 접하고 있는데, AI 시장도 Hype가 심하다 보니 개발팀의 실력과 현재의 기술 수준을 부풀려서 VC로 부터 투자를 받아 법인 차량부터 바꾸거나, 정부 과제의 눈 먼돈을 받기 위해 코딩보다 제안서를 더 많이 쓰는 회사도 있다.&lt;/p&gt;
&lt;p&gt;게임 분야에서는 3D 모델을 AI로 렌더링을 제공한다는 소프트웨어들이 데모 영상만 공개하고 고가의 선주문을 받고있으며, 일부 유튜버와 수익을 공유하며 바이럴 마케팅에만 힘쓰는 모습이 심심치 않게 보인다.&lt;/p&gt;
&lt;p&gt;그러다 보니 AI 기술이 어디까지가 진실이고 어디까지가 Hype인지 스스로 경계를 찾고 싶었다. 최신 연구 동향을 파악하기 위해 논문 한편을 읽어봤다. 오늘은 구글이 2023년에 ICLR에서 발표한 &lt;a href="https://iclr.cc/virtual/2023/oral/12607"&gt;Dream Fusion&lt;/a&gt;을 소개한다. Dream Fusion은 Diffusion Model을 이용해서 텍스트를 3D 모델로 변환하는 방법을 다룬다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;본인은 컴퓨터 그래픽스나 AI 전문가가 아니라, 평범한 개발자의 시선에서 능력이 되는데 까지만 이해한 것이니 재미로만 봐주세요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-12882603"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-12882603"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8YWNlDxukPHutRu68Um7dw-6e9197affc4f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-12882603"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8YWNlDxukPHutRu68Um7dw-6e9197affc4f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DreamFusion 으로 생성한 3D 오브젝트&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Background&lt;/li&gt;
&lt;li&gt;Text-to-3D Model&lt;/li&gt;
&lt;li&gt;NeRF(Neural Radiance Fields)&lt;/li&gt;
&lt;li&gt;Diffusion Model &amp;amp; DDPM&lt;/li&gt;
&lt;li&gt;Dream Fusion : Score Distillation Sampling&lt;/li&gt;
&lt;li&gt;Dream Fusion : Algorithm&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;Ending&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;필자는 현재 신작 게임을 준비하고 있다. 그러다 보니 하나의 게임을 만들기 위해 들어가는 비용과 시간이 얼마나 큰지 잘 알고 있다. AI 기술이 개발팀의 생산성을 크게 높여준다면 게임의 컨텐츠 분량과 퀄리티를 한 단계 더 높일 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;요즘 많은 중국 게임사들이 높은 퀄리티의 컨텐츠를 5~6주 간격으로 DLC 수준의 업데이트를 제공하는 미친 개발속도를 자랑하다보니, 인건비가 비싼 일본과 한국, 유럽 등에서 제작되는 게임들은 중국 게임사의 생산성을 따라가려면 한 명 한 명의 생산성을 높이는 것이 필요한 상황이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5c544a12"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5c544a12"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rWNjQufw_ezzraiTVpjw2Q-75393504f4d8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5c544a12"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rWNjQufw_ezzraiTVpjw2Q-75393504f4d8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://sensortower.com/ko/blog/Genshin-Impact-has-achieved-revenue-of-more-than-4B-since-its-launch"&gt;2022년 센서타워 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;게임 뿐만 아니라 증강현실, 영화, 애니메이션 등 컴퓨터 그래픽을 활용하는 모든 분야에서 AI 도구가 생산성을 높여줄 수 있다면 제작자가 만든 컨텐츠의 표현력 향상과 컨텐츠 분량 확대를 통해 사람들이 충분히 긴 시간동안 즐길 수 있도록 도움을 줄 것이다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/Lb2wwEx6DVw?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;뜬금 없지만 최근 State of Unreal 2024 에서 소개된 게임이 인상적이어서 인용해보았다.&lt;/p&gt;
&lt;h2 id="text-to-3d-model"&gt;Text-to-3D Model&lt;/h2&gt;
&lt;p&gt;최근 DALLE, Stable Diffusion, Midjourney 등의 인공지능이 텍스트를 기반으로 고해상도의 이미지를 만드는 능력을 선보이고 있다. 최근 생성 모델 트렌드는 Diffusion Model [1]연구를 기반으로 대규모의 이미지-텍스트 학습 데이터를 이용했다.&lt;/p&gt;
&lt;p&gt;Text-to-3D Model을 만든다고 하면 3D 데이터를 Diffusion Model 학습에 이용하면 되지만, 아쉽게도 3D 에셋은 제작도 어렵고 절대적인 개수도 이미지에 비하면 한참 부족하다. 그래서 최신 연구들은 2D 이미지 모델로 3D 생성 모델을 만드는 방법을 고민하고 있다. 오늘 다루는 Dream Fusion도 이러한 방법으로 3D 모델을 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b5cf40a6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b5cf40a6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wkQfKTJHxYn50dnM1ac8wA-af6a79b025c6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b5cf40a6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wkQfKTJHxYn50dnM1ac8wA-af6a79b025c6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그림 출처 : &lt;a href="https://iclr.cc/virtual/2023/oral/12607"&gt;ICLR 2023 — DreamFusion: Text-to-3D using 2D Diffusion&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;논문 내용을 다루기 전에 &lt;strong&gt;NeRF(Neural Radiance Fields)&lt;/strong&gt;[2]와 &lt;strong&gt;Diffusion Model&lt;/strong&gt;[1]에 대한 사전 지식이 필요해서 이 개념을 먼저 다루면서 시작한다.&lt;/p&gt;
&lt;h2 id="nerfneural-radiance-fields"&gt;NeRF(Neural Radiance Fields)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;NeRF(Neural Radiance Fields)&lt;/strong&gt;는 다양한 각도에서 촬영한 이미지를 기반으로 해서 3D 공간을 구현한 신경망 자체를 의미한다. 가장 기본적인 분류모델, 언어모델은 대규모의 학습 데이터로 일반화된 지식을 잘 학습하는 방식이라면 NeRF는 신경망 하나가 하나의 공간(Scene)만을 표현하고 학습 데이터도 동일한 사물을 여러 각도에서 촬영한 사진을 이용한다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/JuH79E8rdKc?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;NeRF는 네트워크 자체는 컨볼루션 레이어 없이 Fully-connected Network 만을 이용한다. 입력 데이터로는 &lt;strong&gt;카메라의 위치 좌표 (x,y,z)&lt;/strong&gt; 와 &lt;strong&gt;카메라의 각도&lt;/strong&gt; (&lt;strong&gt;θ, ϕ&lt;/strong&gt;) 를 사용한다. 네트워크의 출력은 이미지이다.&lt;/p&gt;
&lt;p&gt;아래 그림을 잘 보면 ray가 지나가는 각 위치(x,y,z)와 각도(θ, ϕ)를 샘플링 해서 F(&lt;strong&gt;θ)&lt;/strong&gt;에 색상을 질의하는데 이 과정을 논문에서 query 라고 표현했다. 이 과정을 여러번 거쳐서 각 색상을 얻어온 다음에 Volume Rendering 기법으로 최종적으로 2D 이미지로 보여줄 색상을 결정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5934e3a2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5934e3a2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9-m8uLK6UmZSVpVVa29aFw-ed02c5fcb6c5.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5934e3a2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9-m8uLK6UmZSVpVVa29aFw-ed02c5fcb6c5.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NeRF 학습 구조도 출처 : [2]&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c1519d66"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c1519d66"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GhRa8L9LF0DZS5hlGpEKQg-289e0f774d4b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c1519d66"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GhRa8L9LF0DZS5hlGpEKQg-289e0f774d4b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NeRF는 공간에 대한 이미지를 사용해서 하나의 딥러닝 모델이 공간 자체를 학습하게 한다.&lt;/p&gt;
&lt;p&gt;정리하자면 입력값은 카메라의 위치, 출력값은 해당 카메라에서 바라본 이미지를 2차원으로 투영한 값이다. 네트워크는 컨볼루션 레이어 없이 Fully Connected Layer를 사용한다. NeRF는 여기서 학습의 성능을 높이기 위해 3가지 정도를 더 준비했다.&lt;/p&gt;
&lt;h3 id="-volume-rendering"&gt;① Volume Rendering&lt;/h3&gt;
&lt;p&gt;게임을 취미로 가진 사람들이라면 컴퓨터 게임은 주로 &lt;strong&gt;폴리곤(Polygon)&lt;/strong&gt;으로 3D 모델링을 표현한다는 것을 알고 있을 것이다. 반대로 3D 오브젝트를 표현하는 방식중에 복셀(Voxel)이 있다. Volume + Pixel을 나타내는 말이며 2차원 이미지를 누적한 것이라고 생각하면 된다. 일상생활에서는 뇌 컴퓨터 단층촬영(Brain CT)를 하면 나오는 이미지가 복셀로 표현된다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-55e10b57"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-55e10b57"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-ODge4Vj5Js-RZGN4-2d1edffb2e46.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-55e10b57"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-ODge4Vj5Js-RZGN4-2d1edffb2e46.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;완전 어렸을 때 시험 문제로 정사각형은 총 몇개냐 라는 문제가 나온적이 있었지…&lt;/p&gt;
&lt;p&gt;Volume Rendering은 예전부터 사용된 3D Voxel 데이터를 2차원으로 투영하는 방식이다. 물체를 바라보는 카메라 위치로부터 Ray Casting 하면서 물체와 만나는 모든 색상값을 누적해 2차원 픽셀에 표현될 색상을 계산한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-010dcfd0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-010dcfd0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0lse8_gHxO5SCSlgMkvsmw-600231c340e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-010dcfd0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-0lse8_gHxO5SCSlgMkvsmw-600231c340e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.researchgate.net/figure/Volume-rendering-Diagram-shows-how-a-ray-interacts-with-a-3D-volume-image-The-voxel_fig7_12601209"&gt;출처 : &lt;strong&gt;Clinical Utility of Three-dimensional US1&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ray는 카메라의 위치에 O와 이동 거리 * 시간 Dt을 더해서 O + Dt로 표현한다. 논문에서는 Ray의 결과값인 C(r)를 구하는 Volume Rendering 수식을 아래와 같이 표현했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-123c33e6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-123c33e6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3_xGeA4Qha_W7KF7Xrek8w-d111ee1e94ea.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-123c33e6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3_xGeA4Qha_W7KF7Xrek8w-d111ee1e94ea.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여기서 T(t)는 광선이 시간에 따라 누적된 &lt;strong&gt;투과율(빛이 물체를 통과할 수 있는 정도)&lt;/strong&gt;을 나타낸다. 이 값은 시간에 따라 값이 작아질 것이다. 다시 말하면 빛이 통과하는 곳의 밀도가 큰 부분은 값을 크게 가지고 달의 뒷면 처럼 빛이 닿을 수 없는 공간의 색상은 적게 합성되는 원리이다.&lt;/p&gt;
&lt;p&gt;그리고 밀도를 나타내는 volume density &lt;strong&gt;σ(x)&lt;/strong&gt; 와 색상값 c를 곱해서 ray 하나의 색상값을 계산한다. 여기서 &lt;strong&gt;σ(x)&lt;/strong&gt; 또한 학습된 확률 분포이다. 이제 구적법을 사용해서 적당히 값을 샘플링해서 근삿값을 계산한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f040efe7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f040efe7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nRwqDuxbIILGs30R8t_o7A-b9b5813d3777.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f040efe7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nRwqDuxbIILGs30R8t_o7A-b9b5813d3777.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;수식 자체는 1995년에 발표된 논문, Optical models for direct volume rendering 에 기반해서 완성되었다.&lt;/p&gt;
&lt;p&gt;이렇게 사용한 Volume Rendering 수식은 미분이 가능하기 때문에 오차역전파로 학습할 수 있다.&lt;/p&gt;
&lt;h3 id="-positional-encoding"&gt;② Positional Encoding&lt;/h3&gt;
&lt;p&gt;입력값이 &lt;strong&gt;(x,y,z,θ,ϕ&lt;/strong&gt;) 로 굉장히 차원이 작았기 때문에&lt;br&gt;
Positional Encoding으로 차원을 늘리는 작업을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8946d1ab"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8946d1ab"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1S-R0sHCMTwGASBEqtVJvg-0569f5e033c2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8946d1ab"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1S-R0sHCMTwGASBEqtVJvg-0569f5e033c2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-계층적-밀도-샘플링-hierarchical-volume-sampling"&gt;③ 계층적 밀도 샘플링 (Hierarchical Volume Sampling)&lt;/h3&gt;
&lt;p&gt;아래 그림을 잘 들여다 보면 비어있는 공간의 데이터는 샘플링해도 Loss에 기여하는게 없기 때문에 계산을 최적화 하기 위해서는 좋은 데이터를 샘플링 해야 한다. 논문에서는 &lt;strong&gt;coarse&lt;/strong&gt; 네트워크와 fine를 학습해서 신경망으로부터 데이터를 샘플링 하는 방식을 제안한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b94fa901"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b94fa901"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gsAqjRq88Ie0EUtQZx1zhQ-b254f1d717e3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b94fa901"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-gsAqjRq88Ie0EUtQZx1zhQ-b254f1d717e3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;먼저, 균일(Uniform)하게 데이터 Nc 개를 샘플링한다. 그리고 이를 &lt;strong&gt;coarse&lt;/strong&gt; 네트워크에 색상을 얻어온 뒤에 이 결과값을 다시 fine 네트워크에 넣어서 평가한다. 이러면 fine 네트워크는 볼륨과 관련된 정보로 점점 편향된다. 볼륨 렌더링 수식에서 가중치 w 를 아래와 같이 정의하고, 색상에 기여하는 가중치에 대해서 전체를 나누면 확률 밀도 함수(PDF)가 하나 나오는데 이 확률 분포에서 Inverse Transform Sampling으로 값을 다시 샘플링해서 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b70467fb"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b70467fb"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9UjdsSFb7t4jOobPtGEiSw-5fc5d66da9e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b70467fb"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9UjdsSFb7t4jOobPtGEiSw-5fc5d66da9e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-497a4e9a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-497a4e9a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SMRt2OnGmy_3rMSr_MhXHA-822f119457cd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-497a4e9a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SMRt2OnGmy_3rMSr_MhXHA-822f119457cd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;색상을 표현하는데 더 많이 기여하는 가중치일수록 확률이 높게 나온다.&lt;/p&gt;
&lt;p&gt;마지막으로 &lt;strong&gt;coarse&lt;/strong&gt; 네트워크에서 얻어온 값과 fine 네트워크에서 얻어온 값으로 Loss를 사용해서 신경망을 학습한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ca172af0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ca172af0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cVIwBfFia-Y8-Uy5GsLlGQ-3691b37656b7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ca172af0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cVIwBfFia-Y8-Uy5GsLlGQ-3691b37656b7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="-nerf의-단점"&gt;④ NeRF의 단점&lt;/h3&gt;
&lt;p&gt;우선 데이터 구하기가 까다롭다. 실외 촬영으로 데이터를 구한다고 치면 해가 계속 이동하기 때문에 정적인 순간의 3D 공간 사진을 구하기가 어렵다. 또한, NeRF Model 하나가 하나의 공간만을 다루기 때문에 다른 용도로 사용하기 어렵다.&lt;/p&gt;
&lt;p&gt;지금까지가 NeRF에 대해서 대략적으로 소개하였고 원본 논문과 Keras 홈페이지[3]에 더 자세한 내용이 나와있다.&lt;/p&gt;
&lt;h2 id="diffusion-model--ddpm"&gt;Diffusion Model &amp;amp; DDPM&lt;/h2&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/W-O7AZNzbzQ?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Diffusion Model에 대해서는 따로 논문을 보진 않았고 &lt;a href="https://shine-chanter-521.notion.site/Image-Generative-8832125b52ef4a8d9cc3c05233198e9e"&gt;본 스터디에서 한 번 진행했었던 자료&lt;/a&gt;와 위 영상을 토대로 정리했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Diffusion Model이란 원본 데이터에 노이즈를 점진적으로 추가해서 가우시안 분포를 따르는 노이즈를 만들고, 노이즈로부터 점차 노이즈를 걷어내서 원본 이미지로 복원하는 프로세스를 가진 생성 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5c571b2a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5c571b2a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zYdyGqjFHeicwuU3upG6aA-197b03ecaa1b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5c571b2a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zYdyGqjFHeicwuU3upG6aA-197b03ecaa1b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Diffusion Models: A Comprehensive Survey of Methods and Applications[4]&lt;/p&gt;
&lt;p&gt;원본 이미지에서 노이즈로 향하는 과정을 Forward Process, 노이즈에서 이미지로 향하는 과정을 Reverse Process라고 부르는데 이 두 과정은 현재 상태를 바로 이전 상태에만 의존한다고 가정하는 마르코프-체인으로 정의한다.&lt;/p&gt;
&lt;p&gt;노이즈를 추가하는 과정을 표현하는 함수 q는 step 마다 가우시안 분포에서 노이즈를 조금씩 추가한다. 아래 수식에서 &lt;strong&gt;β값&lt;/strong&gt;은 노이즈가 추가되는 량을 결정하며 가우시안 분포의 평균과 분산이 저렇게 생긴 이유는 노이즈를 계속 추가하기만 하면 값이 커지니까 값을 조금 scale down 시키는데 이런 상황까지 표현한 것이다[5].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f26c4442"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f26c4442"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pK8seVGUBozpeiiUc6IPOg-6761f311a8b6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f26c4442"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pK8seVGUBozpeiiUc6IPOg-6761f311a8b6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Forward process를 이렇게 정의했는데, 우리가 원하는 것은 노이즈에서 이미지로 향하는 p 함수를 구하는 것이다. 이 함수는 학습을 통해 q(x_t|x_t-1)에서 노이즈를 제거하는 확률 분포의 평균과 분산을 배운다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-13317bd9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-13317bd9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-96PjBPr3nzhYDMEmLlEwrA-823c74ca7dd3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-13317bd9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-96PjBPr3nzhYDMEmLlEwrA-823c74ca7dd3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8bce6989"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8bce6989"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SZ1T1p9_lo-3OKmt6lg5vA-7ce38a547aca.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8bce6989"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SZ1T1p9_lo-3OKmt6lg5vA-7ce38a547aca.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Forward Process의 q의 확률 분포는 이미 주어졌으니, 네트워크를 최적화 하는 Loss는 두 확률 분포를 추정하는 KL Divergence Loss를 이용할 수 있다. 원래대로라면 Denoising Process를 매 step마다 진행해서 매우 느릴 것이지만, DDPM의 저자들(Ho et al)[1]께서는 노이즈를 생성하는 q 함수를 특정 x0에서 임의의 단계 t로 바로 건너 뛸 수 있는 수식을 세우셨다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1c11a500"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1c11a500"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GMOyF3wytDBBu4QOmNhDxw-92f652df91e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1c11a500"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GMOyF3wytDBBu4QOmNhDxw-92f652df91e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그리고 이제 이 수식으로 다시 denoising 하는 p 함수를 학습하기 위해 Loss세워야 한다. 식 자체를 이리저리 잘 유도하면 아래와 같은 최종 수식이 완성된다고 하시는데 본인은 수학에 자신이 없는지라 처음부터 이해할 생각도 하지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c8c1eb23"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c8c1eb23"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KjJHLF58V5iYbDsUJtPR-g-2930216eb26d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c8c1eb23"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KjJHLF58V5iYbDsUJtPR-g-2930216eb26d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;정말 다행히도 앞에 있는 상수는 쿨하게 제거해버리고 실제 노이즈인 ε과 신경망으로 학습하는 εθ의 차이만으로 Loss를 사용해도 충분히 잘 작동한다고 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-fd1bedd8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-fd1bedd8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zwhA880SMjN5Io6oweGPGQ-2075f9f6f80e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-fd1bedd8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zwhA880SMjN5Io6oweGPGQ-2075f9f6f80e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ho et al. (2020) found that predicting worked best, especially when combined with a reweighted loss function&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="score-distillation-sampling"&gt;Score Distillation Sampling&lt;/h2&gt;
&lt;p&gt;지금부터는 이 글의 주제인 Text로부터 3D 모델을 만드는 Dream Fusion[7] 내용을 다룬다. Dream Fusion은 기존에 이미 텍스트로 2D 이미지를 생성하는 Diffusion Model에 사용된 개념을 사용하고 3D로 렌더링 하는데에는 NeRF를 이용한다.&lt;/p&gt;
&lt;p&gt;하지만, Diffusion Model을 그대로 사용하지는 않는데 Diffusion Model은 샘플링된 노이즈로부터 이미지를 생성하는 반면, 우리가 원하는 건 카메라의 위치와 각도로부터 3D 모델을 생성하는 것이기 때문이다. 이를 &lt;strong&gt;differentiable image parameterization(DIP)&lt;/strong&gt;[8] 라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6d25d8d2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6d25d8d2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aP6LXjVmIUhABfqsAkkO8Q-29631e5f6320.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6d25d8d2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-aP6LXjVmIUhABfqsAkkO8Q-29631e5f6320.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DIP의 핵심 개념을 보여주는 그림이다.&lt;/p&gt;
&lt;p&gt;생성함수 g 가 파라미터 θ를 입력으로 받아서 이미지 x = g(θ)를 생성한다. 생성함수 g는 미분 가능(differentiable)하기 때문에 오차 역전파 알고리즘(Backpropagation)으로 학습할 수 있다. 3D 세계에서는 생성함수 g는 volume tric renderer이고 파라미터 θ는 3D Volume 정보를 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-bc1fa6fe"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-bc1fa6fe"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VHnyLquRudhnZcuRyvD6Fw-1e3a6ca86125.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-bc1fa6fe"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VHnyLquRudhnZcuRyvD6Fw-1e3a6ca86125.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Dream Fusion의 구조도&lt;/p&gt;
&lt;p&gt;위 그림은 DreamFusion의 구조도이다. U-Net과 Transformer에 자물쇠 아이콘이 있는 걸 볼 수 있는데, 이 둘은 학습 대상이 아니고 pre-trained 된 모델을 이용한다. 마지막으로 Diffusion Model과 NeRF에서 만들어진 이미지를 비교해서 만들어진 Loss를 다시 NeRF로 학습하는 구조를 가지고 있다. Diffusion Model은 구글의 &lt;a href="https://imagen.research.google/"&gt;Imagen&lt;/a&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;DreamFusion에서 정의한 Loss를 &lt;strong&gt;Score Distillation Sampling (SDS)&lt;/strong&gt; 라고 부르는데 위의 내용을 잘 따라왔다면 크게 어렵진 않다. 여기서 Distillation은 Hinton 교수님이 예전에 제안하신 지식 증류[9]를 의미하고, Score Function은 확률 분포를 파라미터에 편미분 한 값이다. 다시 말해서 그냥 Diffusion Model의 지식을 빌려오겠다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-61012248"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-61012248"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4J2wr1unwVWYOnfTojPSGg-e87170965ea3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-61012248"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4J2wr1unwVWYOnfTojPSGg-e87170965ea3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;SDS Loss의 내용&lt;/p&gt;
&lt;p&gt;눈을 크게 뜨고 잘 보면, 위에서 봤던 Diffusion Model의 L_simple을 따르고 있다. 이미지 x는 NeRF로 생성된 이미지 g(θ)를 입력값으로 주었고 Diffusion Model인 ε_hat 의 파라미터에서 y가 추가된 걸 볼 수 있는데 이건 텍스트를 임베팅 한 값이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-43a84f07"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-43a84f07"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9qj6bYEPY5EwT7aAvfSlTQ-a8d534a9a428.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-43a84f07"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9qj6bYEPY5EwT7aAvfSlTQ-a8d534a9a428.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;z_t를 논문에서는 &lt;strong&gt;잠재변수(latent-variable)&lt;/strong&gt;라고 말하고 있는데 이는 초기 이미지 x에 대해서 step t에 대해 노이즈를 씌운 값이다. 마지막으로 우측의 미분한 값을 살펴보면 파라미터 θ에 대해 편미분 한 값이고 w(t)는 다시 잠재변수 zt를 x에 대해 편미분한 값이 NeRF 모델을 업데이트 할 Gradient가 된다. (이런 수식을 chain rule 이라고 불렀던가..?)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2350dec7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2350dec7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zfE0br98-L82eOwnMRYz3w-f4e85469fe5f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2350dec7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zfE0br98-L82eOwnMRYz3w-f4e85469fe5f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c6a24ad3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c6a24ad3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MI_DTmuQOmlaAa9uB9m5dA-d05a3a48ff71.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c6a24ad3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-MI_DTmuQOmlaAa9uB9m5dA-d05a3a48ff71.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Appendix 에 포함된 의사코드&lt;/p&gt;
&lt;p&gt;마지막으로 Appendix에 포함된 의사코드를 같이 첨부했다.&lt;/p&gt;
&lt;h2 id="dream-fusion--algorithm"&gt;Dream Fusion : Algorithm&lt;/h2&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-bc1fa6fe"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-bc1fa6fe"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VHnyLquRudhnZcuRyvD6Fw-1e3a6ca86125.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-bc1fa6fe"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VHnyLquRudhnZcuRyvD6Fw-1e3a6ca86125.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Dream Fusion의 구조도&lt;/p&gt;
&lt;h3 id="neural-rendering"&gt;Neural Rendering&lt;/h3&gt;
&lt;p&gt;Dream Fusion은 NeRF에서 향상된 버전인 mip-NERF 360(CVPR 2022)[11]을 사용한다. 초기에는 랜덤한 가중치로 NeRF를 만들고 랜덤한 카메라 각도로 이미지를 계속 생성하면서 가중치를 업데이트 한다. 즉, 우리가 3D 모델 하나를 원할 때 마다 NeRF Scene을 하나씩 만들어야 한다.&lt;/p&gt;
&lt;h3 id="shading"&gt;Shading&lt;/h3&gt;
&lt;p&gt;컴퓨터 그래픽스에서 쉐이딩이란 광원의 위치나 각도에 따라 색을 변형시켜서 입체감을 주는 작업이다. Dream Fusion은 NeRF에는 없는 셰이딩 개념을 추가했다. 구조도에서 L은 빛의 위치를 의미한다. 그리고 &lt;strong&gt;MLP(Multi-layer Perceptron의 약자이니 네트워크를 의미)&lt;/strong&gt;는 이제 볼륨을 의미하는 τ와 함께 albedo값 ρ를 출력값으로 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a8535e69"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a8535e69"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ez_etn0ip8i4NbC3RiElMA-e2c7dac49967.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a8535e69"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ez_etn0ip8i4NbC3RiElMA-e2c7dac49967.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;albedo[12][13]&lt;/strong&gt;는 물체가 빛을 받았을 때 반사하는 정도를 나타내는 값이다. 실제 셰게에서 물체마다 이 값이 다른 것에 착안해서 컴퓨터 그래픽스에서도 이용하고 있다. 논문에서는 (τ,ρ)로 색상 c를 계산하는 수식까지 설명하고 있는데 여기서는 느낌으로만 이해하자면 반사율이 높을 수록 밝아지고 낮을 수록 어두운 색채를 가지게 된다. (흰눈의 반사율이 90% 정도이다)&lt;/p&gt;
&lt;p&gt;왜 셰이딩을 추가했느냐? 하면, albedo값을 랜덤하게 아주 밝은(1,1,1)값으로 바꾸면서 학습하면 Texture가 없는 결과를 만든다. “다람쥐”를 만들도록 명령했을 때 단지 다람쥐가 포함된 이상한 사진이 덜 나온다고 한다.&lt;/p&gt;
&lt;h3 id="카메라-및-광원-샘플링"&gt;카메라 및 광원 샘플링&lt;/h3&gt;
&lt;p&gt;학습 단계에서 카메라는 완전 랜덤으로 만들진 않고 elevation 각도는 [-10,90] 사이, Azimuth(방위각)는 [0,360]에서 샘플링한다. 광원의 위치를 샘플링 하는 기준 또한 카메라와 동일하다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-51a99692"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-51a99692"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-smxzEz1M2X5l9wGd-1f8f7bb27a04.jpg"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-51a99692"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-smxzEz1M2X5l9wGd-1f8f7bb27a04.jpg"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그림 출처 : &lt;a href="https://www.celestis.com/resources/faq/what-are-the-azimuth-and-elevation-of-a-satellite/"&gt;https://www.celestis.com/resources/faq/what-are-the-azimuth-and-elevation-of-a-satellite/&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="diffusion-loss-with-view-dependent-conditioning"&gt;Diffusion loss with view-dependent conditioning&lt;/h3&gt;
&lt;p&gt;샘플링된 카메라의 위치에 따라 텍스트 임베딩 할 때, 프롬프트에 사족을 조금 붙인다. 예를 들어 elevation이 60도를 넘으면 “overhead view” 라고 붙이며 물체를 바라보는 방향에 따라 “front view”, “back view” 등 프롬프트에 조미료를 첨가한다.&lt;/p&gt;
&lt;p&gt;이것 말고도 논문에서는 학습에서 사용하는 테크닉들이 몇 개 더 있는데, 여기서는 생략한다.&lt;/p&gt;
&lt;h3 id="학습"&gt;학습&lt;/h3&gt;
&lt;p&gt;하나의 모델을 만들 때마다 4개의 칩이 탑재된 TPUv4 머신으로 15k학습을 했고 대략 1.5시간 정도 소요되었다.&lt;/p&gt;
&lt;h3 id="nerf로-3d-mesh-만들기"&gt;NeRF로 3D Mesh 만들기&lt;/h3&gt;
&lt;p&gt;실제로 게임을 만들 때는 3D Mesh로 변경해서 사용해야 하는데, 이를 위해서 3차원 상의 점과 밀도값으로 Mesh를 생성하는 Marching Cube[14]을 이용한다. (1987년 SIGGRAPH 에서 발표된 논문)&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;한계점은 논문의 Discusion 항목과 OpenReview[15] 내용을 참고했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dream Fusion의 핵심 아이디어가 Dream Field(CVPR 2022)[16]와 유사하다고 한다. Dream Field에서는 CLIP을 사용했다면 Dream Fusion은 Diffusion Model을 증류해서 사용한다.&lt;/li&gt;
&lt;li&gt;연구에서는 모델의 해상도를 64 x 64로 잡아서 디테일이 많이 부족하다. 그렇다고 모델을 키워버리면 학습이 매우 느려서 실용적이지 않다. 후속 연구인 &lt;strong&gt;Prolific Dreamer(NeurIPS 2023)&lt;/strong&gt;[17]는 512 x 512의 해상도로 3D 모델을 생성보았다. 그리고 Stable Diffusion에서도 Stable Zero 라는 3D 생성 모델을 공개했는데 논문으로 공개된 건 없지만 SDS Loss을 사용한다고 하니까 핵심 개념은 Dream Fusion과 교집합이 있는 것으로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Prolific Dreamer 논문을 잠깐 봤는데, 17GB GPU Memory가 필요했고 &lt;strong&gt;NVIDIA A100 GPU 하나(약 1300만원)&lt;/strong&gt;에서 몇 시간 정도 학습했다고 한다. 모델 크기만 키운 건 아니고 텍스트 프롬프트를 확률 변수로 취급하고 LoRA로 언어 모델까지 파인 튜닝 하는 구조를 가지고 있다. 논문을 키워드 중심으로 봐서 더 자세히는 모른다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-239466ff"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-239466ff"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3jk4MJfpkzbc4YyeIMKSyw-e899b0b925c2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-239466ff"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3jk4MJfpkzbc4YyeIMKSyw-e899b0b925c2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;ProlificDreamer 구조도&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-57b20a24"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-57b20a24"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pzTUCK7cz-kdcvwcjNymXw-90f8e99c01d9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-57b20a24"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pzTUCK7cz-kdcvwcjNymXw-90f8e99c01d9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Prolific Dreamer의 생성 결과&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SDS 가 이미지 샘플링에 있어서 ancestral sampling과 비교하면 완벽한 Loss가 아니라고 하는데 필자는 해당 연구는 몰라서 잘 몰라서 여기서는 생략한다.&lt;/li&gt;
&lt;li&gt;2D 모델에 비해서 Mode Collapse가 더 부각된다는 리뷰가 있다. 손실 함수 특성상 텍스트 프롬프트에 대한 다양성이 줄어든다고 하는데 연구자들은 프롬프트에 “a DSLR photo of”를 붙이거나 high guidance weights로 신경망을 빨리 포화시키면 어느 정도 개선할 수 있다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0d42bf34"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0d42bf34"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e9vPiDFWgYhdmPmTxhH0Yg-0a313fb6830e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0d42bf34"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e9vPiDFWgYhdmPmTxhH0Yg-0a313fb6830e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="ending"&gt;Ending&lt;/h2&gt;
&lt;p&gt;이번 글에서는 NeRF에서 출발해서 Diffusion Model, Dream Fusion까지의 내용을 다루었다. 텍스트를 3D로 합성하는 분야의 주요 연구들이 &lt;a href="https://github.com/threestudio-project/threestudio"&gt;threestudio 레포지토리&lt;/a&gt;에서 관리되고 개발되고 있는 것으로 보인다. 여기서 한걸음 더 나아가실 분들은 위 링크 내용을 따라가보면 도움이 될 것 같다.&lt;/p&gt;
&lt;p&gt;지금까지의 연구 성과들을 당장 게임에 활용할 수 있냐고 묻는다면 아직은 부족하다. 3D Mesh의 Polygon개수가 렌더링이나 네트워크 성능에 영향을 미치기 때문에 아티스트는 퀄리티와 성능 사이의 적정한 밸런스를 조절해야 하고 Collision Mesh나 애니메이션을 위한 리깅은 자동으로 만들어지지 않는다. 그렇다고 현재의 결과물이 2D 생성 모델처럼 해상도가 높다거나 참고할 수 있을 만한 수준도 아니다.&lt;/p&gt;
&lt;p&gt;당장 사용할 만한 건 게임 내 대사에 음성 AI를 활용할 수 있고, 단순 일러스트만 필요한 경우에는 AI로 생성한 후 리터칭 해서 사용하는 사례도 있다. 이건 원래도 대기업에서는 외주로 주던거라서 외주 비용이 감소하는 것이지 개발팀의 생산성이 늘었다고 체감하지는 못했지만, 이런 AI 기술을 잘 활용하는 소규모 개발팀들의 게임 퀄리티가 갈수록 높아지고 있다고 느끼고 있다.&lt;/p&gt;
&lt;h3 id="references"&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://proceedings.neurips.cc/paper/2020/file/4c5bcfec8584af0d967f1ab10179ca4b-Paper.pdf"&gt;Denoising Diffusion Probabilistic Models (NeurlIPS 2020)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://www.ecva.net/papers/eccv_2020/papers_ECCV/papers/123460392.pdf"&gt;NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis&lt;/a&gt; (ECCV 2020)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://keras.io/examples/vision/nerf/"&gt;[3] Keras | 3D volumetric rendering with NeRF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] Diffusion Models: A Comprehensive Survey of Methods and Applications&lt;/li&gt;
&lt;li&gt;[5] Purpose of scaling mean by 1−𝛽𝑡 in forward diffusion&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://arxiv.org/abs/1505.04597"&gt;U-Net: Convolutional Networks for Biomedical Image Segmentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://dreamfusion3d.github.io/"&gt;DreamFusion: Text-to-3D using 2D Diffusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="https://distill.pub/2018/differentiable-parameterizations/"&gt;Differentiable Image Parameterizations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://medium.com/curg/dataset-distillation-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A6%9D%EB%A5%98%ED%95%B4%EC%84%9C-%EC%95%95%EC%B6%95%ED%95%98%EA%B8%B0-db76599c8d02"&gt;Dataset Distillation- 데이터를 증류해서 압축하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://mathworld.wolfram.com/ScoreFunction.html"&gt;Score Function — Wolfram MathWorld&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[11] &lt;a href="https://jonbarron.info/mipnerf360/"&gt;Mip-NeRF 360: Unbounded Anti-Aliased Neural Radiance Fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[12] &lt;a href="https://www.scratchapixel.com/lessons/3d-basic-rendering/rendering-3d-scene-overview/introduction-shading.html"&gt;Rendering an Image of a 3D Scene&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[13] &lt;a href="https://ko.wikipedia.org/wiki/%EB%B0%98%EC%82%AC%EC%9C%A8%5C"&gt;반사율 | 위키피디아&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[14] &lt;a href="https://xoft.tistory.com/47"&gt;xoft | Marching Cube : 3D Point to Mesh생성 알고리즘&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[15] &lt;a href="https://openreview.net/forum?id=FjNys5c7VyY"&gt;OpenReview | Dream Fusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[16] &lt;a href="https://ajayj.com/dreamfields"&gt;Zero-Shot Text-Guided Object Generation with Dream Fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[17] &lt;a href="https://ml.cs.tsinghua.edu.cn/prolificdreamer/"&gt;ProlificDreamer: High-Fidelity and Diverse Text-to-3D Generation with Variational Score Distillation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>프리퀄 | 마이크로 서비스 시대의 부하 분산 정책</title><link>https://scalalang2.com/p/%ED%94%84%EB%A6%AC%ED%80%84-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%8B%9C%EB%8C%80%EC%9D%98-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B0-%EC%A0%95%EC%B1%85-7776d03e77d6/</link><pubDate>Mon, 04 Mar 2024 09:30:31 +0000</pubDate><guid>https://scalalang2.com/p/%ED%94%84%EB%A6%AC%ED%80%84-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%8B%9C%EB%8C%80%EC%9D%98-%EB%B6%80%ED%95%98-%EB%B6%84%EC%82%B0-%EC%A0%95%EC%B1%85-7776d03e77d6/</guid><description>&lt;p&gt;당신의 눈 앞에 n 개의 공이 있다. 우리는 이를 n 개의 상자에 담으려고 한다. 만약 우리가 공을 하나씩 넣을 때 마다 상자를 균등한 확률 (uniformly at random)로 고르고 그 상자에 공을 넣는다고 하자. 그러면 높은 확률로 모든 상자에 들어간 공의 개수는 $\frac{3 \ln n}{\ln \ln n}$ 을 넘지 않는다.&lt;/p&gt;
&lt;p&gt;그러면 이번에는 상자를 두 개씩 골라보자. 두 개의 상자를 비교한뒤 공의 개수가 더 적은 쪽으로 공을 넣는다. 무언가 절차가 추가되었지만 오버헤드가 그렇게 큰 것 같진 않다. 이러면 결과가 얼마나 좋아질까? 놀랍게도 높은 확률로 모든 상자는 공의 개수가 $\frac{\ln \ln n}{\ln 2} + O(1)$ 을 넘지 않는다&lt;a href="https://gazelle-and-cs.tistory.com/41?category=893068"&gt;[1]&lt;/a&gt;. &lt;a href="https://medium.com/the-intuition-project/load-balancing-the-intuition-behind-the-power-of-two-random-choices-6de2e139ac2f"&gt;이 글&lt;/a&gt;[2]은 이를 직접 시뮬레이션한 분포를 보여준다.&lt;/p&gt;
&lt;p&gt;100만개의 공을 1000개의 상자로 분배하려고 한다. 하나는 랜덤하게 분배하는 경우의 그래프를, 다른 하나는 상자를 두 개씩 고르고 더 작은 상자를 고른 그래프를 보여준다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;plt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;balls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bincount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minlength&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;unique_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_counts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;unique_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;green&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a04835ed"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a04835ed"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k4xxJR_HQY6UyRCf4LKNlw-46937349a2cd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a04835ed"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-k4xxJR_HQY6UyRCf4LKNlw-46937349a2cd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;new_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;box1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;box1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;new_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;box2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;box1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;box2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;unique_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_counts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;unique_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;green&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6da9664d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6da9664d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jIckIC1xPpxxtIbQPnypfA-a1706a3f3ccf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6da9664d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jIckIC1xPpxxtIbQPnypfA-a1706a3f3ccf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 문제는 컴퓨터 과학에서 해시 테이블, 부하 분산 등에서 중요하게 다룬다. 이를 &lt;strong&gt;두 개씩 고르는 것의 능력(power of two choice)&lt;/strong&gt;이라고 부르고 d개의 상자를 고르는 경우에는 &lt;strong&gt;PodC(power of d choice)&lt;/strong&gt; 라고 부른다. PodC는 Nginx, Envoy Proxy 등 현업에서 많이 쓰이는 프록시 서비스들이 기본적으로 제공하는 부하 분산 전략이다&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers"&gt;[3]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;이제 2개씩 고르면 부하 부산의 결과가 더 좋아진다는 것은 알았다. 이제 2가지 질문이 남는다. ① 서버 중에서 무엇을 기준으로 로드가 더 적다고 평가할 것인가? ② 샘플링은 어떻게 할 것인가? 오늘은 구글 리서치가 NSDI’24 에서 발표한 &lt;a href="https://arxiv.org/pdf/2312.10172.pdf"&gt;&lt;strong&gt;프리퀄(Prequal)[4]&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;정책&lt;/strong&gt;에 대해 소개한다.&lt;/p&gt;
&lt;p&gt;지금까지는 CPU 사용량 기준으로 요청을 분배했다면, 이 논문에서는 요청 수(requests-in-flight)와 예상 지연 시간(estimated latecny)을 기준으로 부하를 분산해야 한다고 주장한다.&lt;/p&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU 기준 부하 분산 정책의 이면&lt;/li&gt;
&lt;li&gt;꼬리 지연(Tail Latency)&lt;/li&gt;
&lt;li&gt;YouTube에 프리퀄을 적용한 결과&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;h2 id="cpu-기준-부하-분산-정책의-이면"&gt;CPU 기준 부하 분산 정책의 이면&lt;/h2&gt;
&lt;p&gt;먼저 논문에서 표현하는 용어부터 정리하고 시작하자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-tenant :&lt;/strong&gt; 멀티 테넌트 서비스는 aws/gcp/azure 처럼 물리적 머신을 다른 사용자와 공유하는 환경이다. 쿠버네티스 환경도 서로 다른 성격의 Pod가 호스트 머신을 공유한다는 점에서 멀티 테넌트 환경이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Allocation :&lt;/strong&gt; 모던 대규모 애플리케이션은 수 많은 서버가 RPC 통신을 통해 서로 상호작용하며 비즈니스 요구사항을 수행한다. 각 서비스를 담당하는 서버들의 규모는 워크로드에 따라 수십대에서 수백대 까지 존재한다. 각 서버는 가상 머신(VM)에서 실행되고 호스트 머신으로 부터 일정 부분의 CPU cycle을 할당받는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Utilization&lt;/strong&gt; : 특정 시간 동안 각 서버가 사용한 CPU cycle과 할당받은 할당량의 관계를 CPU Utilization 이라고 한다. 이 값은 100%를 넘을수도 있고 넘지 않을 수도 있다. CPU Allocation은 최소로 보장해주는 값이다. 실제로는 호스트의 자원이 넉넉하면 할당량 이상의 자원을 쓸 수 있게 해준다. 하나의 서비스는 수 많은 서버를 복제해서 생성한다. 이렇게 복제된 서버의 CPU Utilization을 평균낸 값을 job utilization이라고 부른다. (여기서 복제란 표현은 쿠버네티스에서 replica 라고 부르는 것과 같다)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolation &lt;/strong&gt;: 서버는 다른 VM들과 호스트 자원을 공유한다. 서버 입장에서 호스트에 입주해있는 다른 사용자를 논문에서는 &lt;strong&gt;적대자(antagonist)&lt;/strong&gt; 라고 부른다. 이 표현이 좀 어색해서, 이 글에서는 &lt;strong&gt;이웃&lt;/strong&gt;이라고 부르겠다. 호스트는 이웃이 나를 방해하지 않도록 할 의무가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-181a4d2b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-181a4d2b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KEL5FPiL7_ANvDChSwnnYQ-cac84afd2b81.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-181a4d2b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KEL5FPiL7_ANvDChSwnnYQ-cac84afd2b81.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;유튜브에 서비스의 1분/1초 기준 CPU 사용량을의 히트맵 분포를 보여준다.&lt;/p&gt;
&lt;p&gt;서버(replica)들의 CPU 사용율을 동일하게 하는 가진 부하 분산 정책은 얼핏 합리적이게 보이기도 하다. 위 그림은 하루를 기준으로 서버의 CPU 사용율을 샘플링한 결과로 위에 있는 그래프는 1분, 아래 그림은 1초로 샘플링한 결과를 보여준다. Y축에서 1.0에 그어진 빨간 선은 CPU limit을 의미한다. 짙은 빨간색에 가까울수록 서버가 많이 밀집해 있다는 의미이다.&lt;/p&gt;
&lt;p&gt;1분을 기준으로 보면 거의 할당량 이하로 계산되는 것으로 보이지만, 1초를 기준으로 샘플링 해보면 피크 타임에는 항상 CPU할당량 이상을 요구한다. 호스트 머신은 높은 부하를 주는 VM의 행위를 제한해서 층간소음을 내지 않을 의무가 있다. 다시 말하면 호스트 머신이 여러 서비스들이 바쁘게 돌아가고 있다면 내 서비스가 임시로 빌릴 CPU 자원이 없다.&lt;/p&gt;
&lt;p&gt;예를 들어 보자, 우리는 100대의 서버를 운영하고 있고 각 서버는 호스트의 40% 정도의 CPU를 할당받았고, 1번과 2번 노드에서 이웃의 CPU 이용율이 60%까지 올라온 상황이다. job utilization이 할당량의 1.1x 만큼 필요하다면 1번, 2번 노드를 제외한 나머지 98개의 머신은 4%의 자원을 추가로 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-108d4fe7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-108d4fe7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wKzpUjyYtkHLBhUsxU9b_Q-9f9b3a79426d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-108d4fe7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wKzpUjyYtkHLBhUsxU9b_Q-9f9b3a79426d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;시끄러운 이웃과 시끄럽지 않은 이웃이 공존한 상황에서 CPU 할당량 이상을 요청한 경우&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 예시는 환경에 따라 조금씩 다르게 적용될 수 있다. AWS에서 T타입 인스턴스는 크레딧을 기반으로 필요에 따라 CPU를 더 사용할 수 있는 Burstable-type인 반면, M타입 인스턴스는 Non-burstable type이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Non-burstable 인스턴스를 사용하는 멀티 테넌트 환경에서도 프리퀄 정책이 유효한지는 논문만 보고는 결론을 낼 수 없었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;정리하자면 높은 꼬리 지연을 줄이기 위해서는 높은 부하를 받고 있는 서버를 실시간으로 식별해야 한다. 하지만 아래와 같은 문제가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시끄러운 이웃으로 부터 영향을 받는 상황이 있다.&lt;/li&gt;
&lt;li&gt;CPU 이용율은 가까운 과거에서 집계된 정보일 뿐, 실시간 부하를 반영하지 못한다.&lt;/li&gt;
&lt;li&gt;CPU 이용율만 고려하여 락 컨텐션, 메모리, 네트워크 대역폭 등 다른 지연 요소는 고려하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="꼬리-지연tail-latency"&gt;꼬리 지연(Tail Latency)&lt;/h2&gt;
&lt;p&gt;꼬리 지연(tail latency)이란 high-percentil latency라고도 부르며 드물게 높은 지연이 발생하는 현상을 말한다. p99에서 레이턴시가 10ms이면 100명중 99명은 10ms이하의 요청을 경험한다. p99.9에서 레이턴시가 100ms이면 1000명중 한 명은 100ms를 경험한다는 표현이다. 어떻게 보면 큰 문제가 아닌 것 처럼 보이기도 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9bf8a419"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9bf8a419"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EQQjgslKtSPzQI48A8htJQ-3de6ce58320f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9bf8a419"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EQQjgslKtSPzQI48A8htJQ-3de6ce58320f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;마이크로 서비스 아키텍처에서 서비스 호출 관계를 표현&lt;/p&gt;
&lt;p&gt;마이크로 서비스 시대에서 하나의 요청은 많은 서비스와 상호작용하면서 비즈니스 요구사항을 완수한다. Parallel Wait은 동시에 다수의 서비스에 요청을 보내고 모든 응답이 돌아올 때 까지 대기하는 패턴이다. Serial Chain은 한 서비스가 또 다른 연관 서비스에 의존하는 경우이며 Blocking Call 이기 때문에 의존하는 서비스의 길이가 길수록 지연이 커진다.&lt;/p&gt;
&lt;p&gt;직관적으로 받아들이면 꼬리지연이 굉장히 흔하게 발생할 수 있고, 서비스 요구사항에 큰 영향을 준다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ceceec5e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ceceec5e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--uwd7BEHQPp-QBxXxmaiUQ-cbe92f1b233b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ceceec5e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1--uwd7BEHQPp-QBxXxmaiUQ-cbe92f1b233b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Parallel calls 에서 꼬리 지연의 영향을 보여주는 시뮤레이션&lt;/p&gt;
&lt;p&gt;AWS 엔지니어인 &lt;a href="https://twitter.com/MarcJBrooker"&gt;Marc Brooker&lt;/a&gt;는 자신의 &lt;a href="https://brooker.co.za/blog/2021/04/19/latency.html"&gt;블로그[5]&lt;/a&gt;에서 꼬리 지연의 영향을 시각적으로 잘 표현했다. 위 그림은 Parallel Wait 패턴에서 요청을 보내는 서비스 수에 따라 경험하는 레이턴시의 분포를 보여준다. 블로그에 방문하면 더 자세한 내용이 나와있다.&lt;/p&gt;
&lt;h2 id="youtube에-프리퀄을-적용한-결과"&gt;YouTube에 프리퀄을 적용한 결과&lt;/h2&gt;
&lt;p&gt;논문은 프리퀄(Prequal)에 대한 설명에 들어가기 앞서, YouTube 웹사이트의 부하 분산 정책을 &lt;strong&gt;가중 라운드 로빈(WRR:Weighted Round Robin)&lt;/strong&gt;에서 프리퀄로 변경한 결과부터 보여주면서 쇼앤프루브 한다. 유튜브 서비스는 마이크로서비스 아키텍처로 구성되어 있기에 모든 연관 서비스와의 상호작용에서 부하 분산 정책이 일관되지 않을 수 있다. 논문에서는 홈페이지 서비스에만 적용된 결과를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2fb562db"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2fb562db"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pJLwVahvdsWV3FAQRjIvaA-958bb0a4e469.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2fb562db"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pJLwVahvdsWV3FAQRjIvaA-958bb0a4e469.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;메모리, CPU, RIF(requests-in-flight)를 보여주는 그래프 (1초 단위로 샘플링한 데이터이다)&lt;/p&gt;
&lt;p&gt;08:00시 이전에는 라운드 로빈 정책이 적용되어 있다가 08:00부터는 프리퀄로 전환한 결과를 보여준다. 프리퀄은 모든 분야에서 높은 개선을 보여준다. 꼬리 지연은 2x나 감소했고, RIF는 5x-10x, CPU의 tail utilization은 2x, 메모리의 tail usage는 10~20% 감소했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a5bd5cf3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a5bd5cf3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pHZtjA-Zlv-W8FZXxY_0FQ-9d1c68b60a1b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a5bd5cf3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pHZtjA-Zlv-W8FZXxY_0FQ-9d1c68b60a1b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Normalized YouTube Homepage server request error rate and latency p99.9 (red), p99 (green), and p50 (blue)&lt;/p&gt;
&lt;p&gt;위 그림은 p99.9(빨간색), p99(초록색), p50(파랑색)에서 지연에 에러가 발생한 비율(e.g. timeout)과 레이턴시를 보여준다. 그 이전까지는 분포가 산만하게 오두방정 떨다가 08:00시에 프리퀄이 적용되자 tail latency를 멱살잡고 평균 근처까지 끌어 내린 결과를 볼 수 있다. 그리고 error ratio는 거의 0에 가깝도록 감소했다.&lt;/p&gt;
&lt;p&gt;구글 연구진들이 쇼앤프루브를 한 결과, 프리퀄은 현재 유튜브를 구성하는 다른 메이저 서비스는 물론, 구글의 다른 많은 서비스에 적용되어 있다고 한다.&lt;/p&gt;
&lt;h2 id="프리퀄-시스템-디자인"&gt;프리퀄 시스템 디자인&lt;/h2&gt;
&lt;p&gt;클라이언트는 주어진 정보를 바탕으로 어떤 서버로 요청을 전송할 지 선택한다. 프리퀄은 전용 로드밸런서로 만들 수도 있고 gRPC 처럼 클라이언트 사이드에서 로드 밸런싱을 구현할수도 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;클라이언트 사이드 로드 밸런싱 (client side load balancing)&lt;/strong&gt;
dedicated load balancer를 이용하지 않고, 클라이언트가 직접 어느 서버로 요청을 전송할지 결정하는 매커니즘이다. gRPC에는 dns lookup으로 도메인 뒤에 있는 서버 IP를 조회한 뒤 정책에 따라 부하 분산하는 매커니즘을 가지고 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;프리퀄도 클라이언트-사이드 로드 밸런싱 매커니즘이다. 이 매커니즘을 프록시 서비스안에서 구현할 수 있기에 정책이 적용되는 부분이 어디에 있는가에 따라서 구분한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-af663aa5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-af663aa5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-InLWwLGJ3bYC2Tidn9wVSw-538d323ab3e0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-af663aa5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-InLWwLGJ3bYC2Tidn9wVSw-538d323ab3e0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;프리퀄은 클라이언트 및 전용 로드 밸런서에 적용될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-196f409c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-196f409c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IyC_zdxIb6LC4xxrECE5Mw-62d7b5ca7408.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-196f409c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IyC_zdxIb6LC4xxrECE5Mw-62d7b5ca7408.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Prequal 구조도&lt;/p&gt;
&lt;p&gt;논문에서는 그림이 없고 말로만 때우고 있어서 독자의 상상력을 요구한다. 논문 내용을 바탕으로 위와같이 Prequal의 구조도를 그려보았다.&lt;/p&gt;
&lt;h3 id="-부하-신호-load-signal"&gt;① 부하 신호 (Load signal)&lt;/h3&gt;
&lt;p&gt;Prequal은 &lt;strong&gt;서버-사이드 모듈&lt;/strong&gt;의 도움을 받는다. 서버는 현재 처리중인 요청 수(requests-in-flight)와 예상 레이턴시(estimated latency)를 계산한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RIF(requests-in-flight)&lt;/strong&gt; : 요청 수는 RPC 프레임워크로부터 애플리케이션 로직에 요청이 진입했으나 아직 응답을 주지 않은 개수를 의미한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예상 레이턴시 (estimated latency) &lt;/strong&gt;: 서버는 요청에 대해 응답을 주고나서 레이턴시 정보를 현재 RIF수와 함께 저장한다. 클라이언트가 서버에게 probe를 요청하면 서버는 현재 RIF수에 해당하는 latency의 평균값을 반환한다. 해당 값이 없다면 가장 가까운 RIF값을 기준으로 계산한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;서버가 수행하는 쿼리마다 조금의 오버헤드가 추가되었다. 프리퀄은 &lt;strong&gt;무시해도 될 정도(negligible)&lt;/strong&gt;의 오버헤드만 추가하는 것을 목표로 한다. 위 RIF 및 예상 레이턴시를 계산하기 위한 자료구조를 충분히 작게 유지해야 한다. 논문에서는 초당 요청 수가 충분히 많다면 마지막 $O(1-10)\,\mathrm{ms}$ 에 발생한 정보만을 저장한다고 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-73875064"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-73875064"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xfHkgpYPw2wnukGTnwzsQQ-4c882f6f2b87.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-73875064"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xfHkgpYPw2wnukGTnwzsQQ-4c882f6f2b87.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;RIF와 예상 레이턴시를 계산하는 방법&lt;/p&gt;
&lt;h3 id="-probing-rate"&gt;② Probing Rate&lt;/h3&gt;
&lt;p&gt;프로브(probe)는 클라이언트가 서버에 정기적으로 RIF와 예상 지연 정보를 요청하는 프로세스이다. probing rate는 단위 시간 동안 얼마나 자주 이 프로세스를 수행할지 결정하는 값으로, 클라이언트가 수행해야 하는 쿼리 수에 비례해서 증가한다. 값이 증가할수록 더 자주 서버에게 정보를 요구한다.&lt;/p&gt;
&lt;p&gt;클라이언트는 단위 쿼리당 요구할 probe의 수인 r_probe 값을 결정한다. probing rate는 &lt;strong&gt;단위 시간당 처리하는 쿼리 수 &lt;/strong&gt; r_probe 값으로 결정된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;클라이언트가 프로브를 수행할 때 접속할 수 있는 서버 중에서 하나를 랜덤으로 선정한다. 이는 &lt;strong&gt;thundering herd&lt;/strong&gt; 현상을 방지하기 위함이다. 만약 서버와 통신하는 RPC Client가 2개 이상인데 모든 서버에 프로브를 수행한다면 다수의 클라이언트가 특정 하나의 서버를 최선의 선택이라고 판단해서 요청을 분배할 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-replica-selection--server-selection"&gt;③ Replica Selection (= Server Selection)&lt;/h3&gt;
&lt;p&gt;클라이언트는 프로브 프로세스를 통해 서버마다 RIF와 예상 지연 시간 정보를 알고있다. 나이브한 방법으로 예상 지연 시간이 가장 짧은 서버로 요청을 분배할 수 있다. 하지만, 실제로는 RIF와 예상 지연 시간 사이에 적절한 밸런스가 중요하다.&lt;/p&gt;
&lt;p&gt;유튜브에서는 처리하고 있는 쿼리마다 일정량의 메모리를 요구한다. 아마 대부분의 서버가 비슷할 것 같은데 RIF값이 증가할수록 현재 점유하고 있는 메모리가 증가할 것이다. 그리고 RIF는 미래의 부하를 나타내는 선행지표이며 예상 레이턴시는 가장 최근에 수행된 쿼리를 기준으로 계산된 값이다. 둘 다 CPU utilization 보다는 최신성을 나타내는 지표이다.&lt;/p&gt;
&lt;p&gt;프리퀄은 레이턴시와 RIF를 줄이기 위해 HCL(a hot cold lexicographic) 서버 선택 규칙을 사용한다. 클라이언트는 Q_RIF(quantile-RIF) 값을 사전에 결정하는데 이 값에 따라 서버의 상태가 hot인지 cold인지 결정된다. 만약 Q_RIF가 0.5이면 RIF의 중위값보다 큰 서버는 hot으로 결정하고 반대는 cold 상태로 결정한다. HCL 서버 선택 규칙은 cold 인 서버 중에서 예상 레이턴시가 가장 작은쪽으로 요청을 분배한다.&lt;/p&gt;
&lt;h3 id="-probe-pools"&gt;④ Probe pools&lt;/h3&gt;
&lt;p&gt;Probe pools는 클라이언트가 서버 프로브 정보를 저장하는 공간이다. pool의 엘리먼트(element)는 하나의 서버와 프로브를 응답한 시간 쌍으로 구성된다.&lt;/p&gt;
&lt;p&gt;Probe pools은 staleness, depletion, degradation 이 세 가지 문제를 풀기 위한 추가적인 매커니즘을 가지고 있다. 논문은 긴 지면을 할애해서 설명하고 있는데 여기서는 내용을 생략해서 각 문제가 무엇인지, 그리고 어떻게 해결했는지만 서술한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;staleness : 풀에 저장된 load signal이 너무 오래되서 정확하지 않음&lt;/strong&gt;
이 문제는 풀에 저장된 프로브에 타임아웃을 설정해서 해결한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;depletion : 풀이 비어있는 상황&lt;/strong&gt;
클라이언트가 서버 선택을 위해 프로브 정보를 이용할 때 마다 재사용 예산(reuse budge)이 1씩 증가하는데 이 값이 최댓값에 도달하면 해당 프로브는 풀에서 제거된다. 풀이 완전히 비어있는 상황을 방지하기 위해 재사용 예산을 설정하는 파라미터가 존재한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;degradation : 특정 replica가 high-load 라는 편향을 가진 경우&lt;/strong&gt;
풀에서 특정 서버가 도저히 선택될 수 없는 load signal을 가지고 있는 경우에 &lt;strong&gt;많은 수의 서버 선택 라운드&lt;/strong&gt;를 거쳐도 풀에 지속적으로 남아있다. 이런 편향을 방지하기 위해 정기적으로 high load probe를 지운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="-sinkholing"&gt;⑤ Sinkholing&lt;/h3&gt;
&lt;p&gt;서버가 애플리케이션 로직을 타기 전에 에러 코드를 반환하는 경우 레이턴시에 대한 평가가 왜곡된다. 이런 현상을 싱크홀이라고 한다. 프리퀄은 휴리스틱한 방법으로 이를 해결했다고 하는데 자세한 내용은 없다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prequal includes some heuristics to avoid sinkholing, but since they are not central to our contribution, we have chosen to simplify our exposition by omitting details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;⑥ 동기 모드&lt;br&gt;
비동기식 프로브&lt;strong&gt;는 정보를 저장하는 &lt;/strong&gt;프로브 풀&lt;strong&gt;을 사용한다. 서버 선택 시, 프로브 풀에서 정보를 가져와 평가하여 서버를 선택한다. &lt;/strong&gt;동기식 프로브&lt;strong&gt;는 프로브 풀을 사용하지 않는다. 쿼리가 들어올 때마다 &lt;/strong&gt;d개의 서버를 무작위로 선택하여 프로브 정보를 조회하고, 이 정보를 기반으로 서버를 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;유튜브&lt;/strong&gt;에서는 실제 프리퀄 사용 시 &lt;strong&gt;동기식 프로브&lt;/strong&gt;를 사용했다. 반면, 테스트베드 실험에서는 &lt;strong&gt;비동기식 프로브&lt;/strong&gt;를 사용했다.&lt;/p&gt;
&lt;p&gt;지금까지 프리퀄의 설계에 대해서 설명해 왔는데, 사실 필자도 아직까지 햇갈리는 부분도 있고 논문에서 명확하게 설명을 안해준 부분도 있다. 예를 들면 probing rate값이 100이면 10ms당 하나씩 요청을 보내는건가? 라는 추측만 할 뿐 논문에서는 디테일한 설명이 부족해 이해하는데 어려움이 있었다.&lt;/p&gt;
&lt;p&gt;하지만 프리퀄의 내용은 잊더라도, 단 하나만 기억하면 된다. RIF와 Latency를 기준으로 부하 분산을 하는 목적을 가지고 있고 그것을 구현하는 방법이 프리퀄이다. 업계에 경험이 많아지면 분명 더 단순한 설계를 가진 방법론이 나올 것으로 기대한다.&lt;/p&gt;
&lt;h2 id="성능-평가"&gt;성능 평가&lt;/h2&gt;
&lt;p&gt;성능 평가는 테스트-베드 환경에서 이루어진다. 테스트-베드 환경은 100개의 클라이언트와 100개의 서버로 구성되어 있으며 각 서버는 해시 연산을 하는 단순한 작업만 수행한다. 하나의 요청이 몇 번의 해시 연산을 하는지는 정규 분포를 따라 결정된다. 모든 서버와 클라이언트는 구글 데이터센터에 상주해있고 데이터 센터에 적용된 기본 Isolation 정책이 적용되어 있다.&lt;/p&gt;
&lt;p&gt;각 서버는 10%의 CPU를 할당했으며, 프로브 풀의 최대값은 16, Q_RIF는 0.84로 선택했다. 그리고 probing rate를 결정하는 r_probe값은 3으로 결정한다. 필자가 제대로 이해한 게 맞다면 클라이언트는 쿼리를 수행한 뒤 비동기로 3개의 서버에 프로브를 요청한다.&lt;/p&gt;
&lt;h3 id="-시끄러운-이웃에-대한-저항성"&gt;① 시끄러운 이웃에 대한 저항성&lt;/h3&gt;
&lt;p&gt;각 서버의 CPU load는 할당량의 75%에서 시작해서 1.74x 까지 점진적으로 증가한다. 서버에 부하를 주기 위해 초당 5.6k의 쿼리부터 시작해서 초당 13k의 쿼리가 생성되도록 증가시켰다. 현재 많이 사용되는 부하 분산 알고리즘인 Weighted Round Robin(WRR) 알고리즘과 프리퀄을 비교했으며, 각 시간마다 처음 30분은 WRR을, 나머지 30분은 프리퀄을 적용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b828eee6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b828eee6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WAZJp_m0Z5gwq9VBlQooig-0c540c7f0352.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b828eee6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WAZJp_m0Z5gwq9VBlQooig-0c540c7f0352.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;CPU 부하 증가에 따른&lt;/p&gt;
&lt;p&gt;우선 CPU load가 증가할 수록 WRR은 에러율과 꼬리지연이 크게 증가한다. 반면 프리퀄은 지연이 조금 증가하긴 하지만 WRR에 비해서는 귀여운 수준이며 에러율은 0에 근사하다. 여기서 에러율이란 클라이언트의 요청이 5초를 넘겨서 타임아웃이 발생하는 비율이다. 그래서 그래프(a)를 보면 Y축의 최댓값이 5초인 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;여기서 가장 흥미로운 부분은 (c) CPU Utilization 분포이다.&lt;/strong&gt; WRR 정책은 본인의 책무를 충실히 이행해서 서버의 CPU load가 균일하도록 잘 분포했다. 반면 프리퀄 정책은 애초에 CPU load와는 무관하기 때문에 분포가 고르게 퍼져있다. 이 결과는 연구자들이 논문 제목에서부터 주장하는 것 처럼 Load is not what you should balance 라는 것이며 가상머신의 시끄러운 이웃의 부하(antagonist load)가 내 서비스에 끼치는 영향이 크다는 것을 말해준다.&lt;/p&gt;
&lt;h3 id="-replica-selection-rule"&gt;② Replica selection rule&lt;/h3&gt;
&lt;p&gt;데이터센터는 다양한 하드웨어 세대와 프로세서 아키텍처의 물리적 장비로 구성되어 있고, 우리가 프로비전 하는 서버는 다양한 하드웨어로 배포된다. 서비스마다 최적의 성능을 내는 하드웨어가 다를 수 있다. 예를 들면 유튜브는 A라는 장비에서 더 좋은 성능을 내고, 구글맵은 B라는 장비에서 더 좋은 성능을 낼 수 있다.&lt;/p&gt;
&lt;p&gt;서버를 선택하는 규칙에서 이러한 점까지 고려하는 다양한 알고리즘들이 있다. 위에서 당신이 분산해야 하는 건 cpu load가 아니다 라고 주장했다면, 여기서는 프리퀄의 서버 선택 규칙인 HCL이 최선이었는가를 평가한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d431b538"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d431b538"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-kKGyE7cagcWNMhR_HCijUQ-be40bdaa655e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d431b538"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-kKGyE7cagcWNMhR_HCijUQ-be40bdaa655e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;서버 선택 규칙에 따른 레이턴시 성능 평가&lt;/p&gt;
&lt;p&gt;논문은 더 많은 지면을 할애해서 이를 설명하는데&lt;br&gt;
이 포스팅에서는 생략하려고 한다.&lt;/p&gt;
&lt;p&gt;한가지 눈여겨 볼만한 건 C3 알고리즘이 프리퀄과 비교할만한 성능을 보여주었는데 이것도 RIF를 기준으로 로드 밸런싱 하는 알고리즘이다. 다만 프리퀄보다는 조금 더 복잡하다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/czZO_MaNLXs?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;프리퀄은 CPU load로 부하 분산 하는 건 VM에서 배포되는 모던한 마이크로 서비스 아키텍처에서는 좋은 선택이 아니며 RIF와 latency를 기준으로 요청을 분할해야 최적의 성능을 낸다고 주장한다.&lt;/p&gt;
&lt;p&gt;유튜브에서는 요청 한 번에 상자 5개를 선택한다. 다시 말해 유저 요청 하나당 서버에 6번의 요청이 간다. 이게 직관적으로는 성능에 문제가 없나 라는 의문을 가지게 되는데, 구글 데이터 센터 내에서 주고 받는 probe 통신 지연이 1ms 이하라고 한다. RIF와 latency만 주고받으면 되기 때문에 기껏 해봐야 4 byte integer 2개 전송하는 거라서 납득은 된다. 그리고 논문에서는 tail latency를 줄여서 얻는 이익이 더 크다고 강조했다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://gazelle-and-cs.tistory.com/41"&gt;상자에 공 넣기, 상자를 두 개씩 고르면? (Power of Two Choices)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://medium.com/the-intuition-project/load-balancing-the-intuition-behind-the-power-of-two-random-choices-6de2e139ac2f"&gt;Load Balancing: The Intuition Behind the Power of Two Random Choices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers"&gt;Envoy Proxy : Supported load balancers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://arxiv.org/pdf/2312.10172.pdf"&gt;Load is not what you should balance: Introducing Prequal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://brooker.co.za/blog/2021/04/19/latency.html"&gt;Tail Latency Might Matter More Than You Think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>QUIC 프로토콜 | 구글 또 너야?</title><link>https://scalalang2.com/p/quic-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B5%AC%EA%B8%80-%EB%98%90-%EB%84%88%EC%95%BC-932befde91a1/</link><pubDate>Fri, 26 Jan 2024 02:51:53 +0000</pubDate><guid>https://scalalang2.com/p/quic-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EA%B5%AC%EA%B8%80-%EB%98%90-%EB%84%88%EC%95%BC-932befde91a1/</guid><description>&lt;p&gt;QUIC 프로토콜은 2012년 구글이 발표한 전송 계층 프로토콜이다. HTTPS 프로토콜은 TCP/IP와 TLS레이어가 나누어져 있어서 불필요한 라운드 트립 딜레이(RTT)가 발생했는데, QUIC은 이 과정을 UDP 위에서 한 번의 라운드-트립으로 구현해서 효율성을 높였다.&lt;/p&gt;
&lt;p&gt;현재 QUIC 프로토콜은 구글 크롬과 유튜브에 적용되어 있고, 구글 사내 통신의 많은 부분도 QUIC 프로토콜로 통신하고 있다. 구글이 추산하기로는 전세계 통신의 7%를 QUIC 프로토콜이 차지하고 있다고 한다. &lt;em&gt;(구글만 사용해도 상당한 점유율을 달성할 것 같긴 하지만..)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;2022년, IETF에서 QUIC 프로토콜을 기반으로 동작하는 HTTP/3 표준화 문서인 &lt;a href="https://datatracker.ietf.org/doc/html/rfc9114"&gt;RFC 9114&lt;/a&gt;를 제정했다. 이런 흐름에 맞춰 &lt;a href="https://zdnet.co.kr/view/?no=20221115093304"&gt;네이버가 처음으로 국내에서 HTTP/3를 도입&lt;/a&gt;한다. 이 글은 구글이 2017년 SIGCOMM에서 발표한 논문과 HPBN 저서를 인용해서 QUIC의 탄생 배경과 구현 및 특징을 소개한다. 하나의 프로토콜을 온전히 이해하려면 알아야 할 내용이 많기 때문에, 필자는 QUIC의 모든 것을 이해할 생각은 없다. 이 글에서는 평범한 K-엔지니어가 알아두면 나중에 소개팅 대화 주제로 좋을 내용만 정리했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3098822.3098842"&gt;The QUIC Transport Protocol: Design and Internet-Scale Deployment | ACM SIGCOMM 2017&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hpbn.co/"&gt;High Performance Browser Networking (HPBN) | DDIA 다음으로 인상깊게 본 책이다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;논문 한줄평 — 구글, 또 너야..? 대단하다 진짜&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;IN A NUTSHELL&lt;/li&gt;
&lt;li&gt;Motivation&lt;/li&gt;
&lt;li&gt;설계 및 구현&lt;/li&gt;
&lt;li&gt;인터넷 스케일로 배포하기&lt;/li&gt;
&lt;li&gt;성능 맛보기와 한계점&lt;/li&gt;
&lt;li&gt;QUIC 프로토콜 개발 경험 공유&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="in-a-nutshell"&gt;IN A NUTSHELL&lt;/h2&gt;
&lt;p&gt;바빠서 이 글을 다 읽을 시간이 없다면 이 부분만 읽어도 된다. 우리가 웹 브라우저에서 이 블로그를 보기 위해 HTTPS 통신을 하면 가장 먼저 TCP/IP 프로토콜의 3 way handshake 과정을 통해 ISN번호를 교환한 다음, TLS 레이어에서 공개키 암호화 알고리즘을 이용해 서로가 사용할 대칭키를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ffd71f36"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ffd71f36"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NeORnQXlynMR82pInGtOLA-4a497c5472e1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ffd71f36"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NeORnQXlynMR82pInGtOLA-4a497c5472e1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hpbn.co/transport-layer-security-tls/"&gt;HPBN | TLS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;네트워크 대역폭은 키울 수 있지만, 빛의 속도는 상수이기 때문에 네트워크 왕복 시간(RTT)은 더 빨라질 수 없다. 빛이 1초에 지구 7바퀴 반 정도를 돈다고 했던가? 이를 단순하게 계산해봐도 지구 반대편까지 가는 통신은 적어도 66ms이며 왕복시간으로 133ms이상 소요된다. 다시 말해서 우리는 불필요한 라운드-트립을 줄일 필요가 있다. HTTP/2의 전신인 SPDY를 만든 Mike Belshe는 &lt;a href="https://docs.google.com/a/chromium.org/viewer?a=v&amp;amp;pid=sites&amp;amp;srcid=Y2hyb21pdW0ub3JnfGRldnxneDoxMzcyOWI1N2I4YzI3NzE2"&gt;More bandwidth doesn’t matter (much)&lt;/a&gt; 에서 다음과 같이 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a6398898"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a6398898"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TROKtb8NjImE0d8WK8lbvw-ab9fe78e3049.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a6398898"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TROKtb8NjImE0d8WK8lbvw-ab9fe78e3049.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;HPBN | 대역폭이 증가하는 경우 / RTT가 감소하는 경우의 페이지 로드 시간 차이&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RTT를 150ms 에서 100ms로 줄이는 것이 유저의 대역폭을 3.9Mbps에서 10Mbps로 심지어 1Gbps로 높이는 것 보다 웹 페이지 로드 시간을 줄이는데 더 큰 영향을 준다. 페이지 로드 시간을 줄이는 다른 방법은 RTT의 회수 자체를 줄이는 것이다. 오늘날 유저의 요청이 서버에 도달하기 까지 DNS resolution, TCP handshake, TLS negotiation 등 많은 라운드-트립 과정을 거친다. 만약 더 적은 라운드-트립으로 데이터가 도달할 수 있는 프로토콜을 만든다면, 페이지 로드 시간을 개선할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;여기 까지 듣고나면, 우리는 TCP/TLS프로토콜에서 발생하는 2번의 라운드-트립을 한번으로 줄이고 싶다는 강한 욕망이 든다.&lt;/strong&gt; QUIC은 UDP 프로토콜 위에서 구현되었는데 사실 말이 UDP지 그 위에다 TCP 프로토콜의 흐름 제어, 오류 제어, 혼잡 제어와 SPDY의 스트림 멀티플렉싱 기능이 모두 구현되어 있다. TCP 프로토콜은 OS 커널에 들어있기 때문에 이를 직접 변경하면 모든 네트워크 장비들이 OS 업데이트를 해야 한다. 구글 스스로도 안드로이드 파편화 문제를 통해 이게 말이 안된다는 것은 잘 알고 있 때문에 UDP 위에서 새로운 프로토콜을 구현했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-eea23498"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-eea23498"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-21oT1D59hHxGdZnK1eYnUg-23910570c1bb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-eea23498"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-21oT1D59hHxGdZnK1eYnUg-23910570c1bb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Ref. &lt;a href="https://web.eecs.umich.edu/~xumiao/docs/imc23-quic-poster.pdf"&gt;QUIC is not Quick Enough over Fast Internet&lt;/a&gt; | ACM IMC’23&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;본 글에서 설명하는 QUIC은 2017년 ACM SIGCOMM에서 소개된 논문을 기반으로 설명한다. 따라서, IETF 기구에서 표준화 중인 HTTP/3과 QUICv2의 디테일한 부분에서 차이가 있을 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CDN 업체인 Cloudflare가 조사한 바에 따르면, 2023년 기준 HTTP/3의 점유율이 20%가 훌쩍 넘는다. 필자의 짧은 생각일 뿐이지만, 아마존 &lt;a href="https://aws.amazon.com/ko/blogs/aws/new-http-3-support-for-amazon-cloudfront/"&gt;CloudFront가 HTTP/3을 지원하는 등&lt;/a&gt; CDN 서비스들이 이를 지원하면서 반영된 것이 아닌가 싶다. (정확한 출처는 못찾았는데, 어디서 본 자료에서 CDN 서비스가 전세계 트래픽의 많은 부분을 차지하고 있다고 한다)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d7095b4f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d7095b4f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IVtf1cncYE7E2WjdVaqQJw-27e0dd9fcf86.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d7095b4f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IVtf1cncYE7E2WjdVaqQJw-27e0dd9fcf86.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.cloudflare.com/http3-usage-one-year-on"&gt;Cloudflare | Examining HTTP/3 usage one year on&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;
&lt;p&gt;뻔한 이야기지만, 모바일의 보급과 웹 서비스 및 플랫폼들이 성장하면서 방대한 양의 트래픽이 발생하고 있다. 꼬리 지연(tail latency)는 여전히 웹 플랫폼을 확장시키는데 있어 어려움으로 작용하고 있고 2012년부터 암호화된 HTTPS 프로토콜이 보급되면서 레이턴시를 증가시키고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-92b9fd8b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-92b9fd8b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-baEy53lOgmk2cEEkrmEfmA-58da2b407579.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-92b9fd8b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-baEy53lOgmk2cEEkrmEfmA-58da2b407579.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;구글 프론트-엔드 서버에서 암호화 통신이 차지하는 비중&lt;/p&gt;
&lt;p&gt;네트워크 대역폭은 증가할 수 있지만 빛의 속도는 상수이기 때문에 하드웨어적으로 통신 품질을 개선하는데에는 한계가 있다. 논문은 QUIC 이라는 새로운 프로토콜을 디자인 하게된 이유를 3가지로 말한다.&lt;/p&gt;
&lt;h3 id="-protocol-entrenchment"&gt;① Protocol Entrenchment&lt;/h3&gt;
&lt;p&gt;Entrenchment(참호)란 참호전으로 비유해서 무언가 고착화 되었다는 의미이다. 방화벽들은 보안을 이유로 친숙하지 않은 프로토콜의 패킷을 이유없이 막거나 라우터들은 특정 프로토콜의 초당 패킷 전송량이나 크기를 제한할 수 있고, 몇몇 NAT는 패킷 헤더를 변경하거나 WAN은 데이터를 압축해서 전송하기도 한다. 네트워크의 중간에 있는 개체들을 Middlebox라고 부른다.&lt;/p&gt;
&lt;p&gt;Middlebox들이 네트워크 전반에 거쳐 커스터마이징을 하다보니 프로토콜을 변경하는 일이 어렵다. TCP 프로토콜에 일부 변경사항을 추가해도 이것이 실제 인터넷 전반에 적용되기 까지는 수십년이 소요된다. 일단 TCP 자체가 OS 커널에 구현되어 있기 때문에 이를 변경하는게 어려울 뿐더러 만약, 변경한다해도 모든 장비가 OS 업그레이드를 해야한다. 프로토콜의 변경 속도가 OS에 종속된다.&lt;/p&gt;
&lt;p&gt;이런 이유로 구글은 커널이 아닌 유저 스페이스에서 UDP위에서 QUIC을 구현한다. 또한, &lt;strong&gt;UDP의 전송 헤더를 암호화 해버려서 Middlebox들이 패킷을 마음대로 변조할 수 없게 만들었다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;QUIC encrypts transport headers and builds transport functions atop UDP, avoiding dependence on vendors and network operators and moving control of transport deployment to the applications that directly benefit from them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-hanshake-delay"&gt;② Hanshake Delay&lt;/h3&gt;
&lt;p&gt;TCP Handshake 과정은 최소 한 번의 라운드-트립 딜레이를 요구하고 커넥션이 맺어지기 전까지는 애플리케이션 데이터를 보낼 수 없다. 시간이 흘러, 암호화 통신에 대한 요구가 증가하면서 TLS 프로토콜 레이어가 추가된다. TLS는 디피-헬만 키 교환 과정을 거쳐 서로 암호화에 사용할 대칭키를 으로 만드는데 여기서 2번의 라운드-트립이 더 발생한다. 대역폭은 증가해도 빛의 속도는 상수이기 때문에 불필요한 라운드-트립을 한 번으로 줄일 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ffd71f36"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ffd71f36"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NeORnQXlynMR82pInGtOLA-4a497c5472e1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ffd71f36"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NeORnQXlynMR82pInGtOLA-4a497c5472e1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hpbn.co/transport-layer-security-tls/#tls-handshake"&gt;HPBN | TLS Handshake&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-hol-blokcing-delay"&gt;③ HOL Blokcing Delay&lt;/h3&gt;
&lt;p&gt;Head-of-line(HOL) blocking 이란 TCP 전송 계층에서 패킷 손실이 발생하면서 뒤에 있는 요청들이 밀리는 현상을 말한다. 수신자는 데이터 순서를 보장하기 위해서 TCP Buffer에 패킷을 채우고 재조립한다. 만약 앞에서 패킷 일부가 누락되면 순서를 보장하기 위해 뒤에오는 패킷들 전체가 버퍼에서 대기하고 있어야 한다. 이는 커널에서 이루어지는 일이기 때문에 애플리케이션에서는 마치 통신이 지연된 것 처럼 보이는데 이런 현상을 Head-of-line blocking이라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e80187a1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e80187a1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-a6Lj752PpRGDidkr-L30Ww-d6b68565577d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e80187a1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-a6Lj752PpRGDidkr-L30Ww-d6b68565577d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hpbn.co/building-blocks-of-tcp/#head-of-line-blocking"&gt;HPBN | TCP Head-of-line blocking&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;QUIC에서는 하나의 커넥션에서 패킷의 성격을 &lt;strong&gt;스트림&lt;/strong&gt;이라는 논리적인 단위로 분리해서 패킷을 전송하는 것으로 이 문제를 완화한다. 동일하게 패킷 순서를 보장하지만, 패킷 손실에 의한 HOL Delay는 한 스트림으로 제한된다.&lt;/p&gt;
&lt;h2 id="설계-및-구현"&gt;설계 및 구현&lt;/h2&gt;
&lt;p&gt;QUIC은 쉬운 배포, 보안, Handshake와 Head-of-line blocking delay의 감소 등 많은 목표를 달성하기 위해 설계 및 구현되었다. 여기서는 2017년 논문에서 나온 QUIC의 모습을 설명한다. 현재 IETF에서 HTTP/3 명세를 제정하고 Draft로 나온 QUICv2와는 디테일한 부분에서 조금씩 다른점이 있다.&lt;/p&gt;
&lt;h3 id="-connection-establishment"&gt;① Connection Establishment&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f850de4b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f850de4b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wB4phLmQmvWATLh57Ffewg-3053fab1474f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f850de4b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wB4phLmQmvWATLh57Ffewg-3053fab1474f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;QUIC의 커넥션 생성과정&lt;/p&gt;
&lt;p&gt;QUIC은 TCP/IP와 TLS 프로토콜이 서로 2번의 핸드셰이크를 하던 비효율을 제거하고 이를 하나의 과정으로 만들었다. Initial 1-RTT는 서버를 처음 방문한 사람이 겪게되는 과정으로, 처음 CHLO(Client Hello) 메시지를 보내면 서버가 REJECT를 응답하면서 (1) server config(공개 키를 포함), (2) 인증서 체인 정보, (3) server config를 개인키로 서명한 정보, 그리고 (4) &lt;strong&gt;source address token 이&lt;/strong&gt;라고 해서 서버가 바라본 클라이언트의 IP주소를 시간과 함께 전달한다.&lt;/p&gt;
&lt;p&gt;클라이언트가 Complete CHLO를 전송하고 Server Hello(SHLO)를 보내는 과정에서 디피-헬만(Diffie-Hellman) 키 교환 알고리즘을 이용해서 앞으로 암호화에 이용할 forward-secure key를 생성한다.&lt;/p&gt;
&lt;p&gt;0-RTT는 단순히 1-RTT에서 생성했던 암호키를 저장했다가 나중에 재사용해서 핸드셰이크 과정을 거치지 않는 것을 말한다. (4) source address token이 서버가 발급한 것이 맞다는 것을 검증한 다음, 클라이언트의 IP가 변경되지 않았다면 해당 요청을 유효한 것으로 인정한다.&lt;/p&gt;
&lt;p&gt;마지막으로, 버전 협상 (Version Negotiation)이라고 해서 커넥션 생성 과정에서 클라이언트가 원하는 프로토콜 버전을 입력해서 전송한다. 만약 서버가 지원하지 않는 프로토콜이라면 버전 협상 패킷을 클라이언트에게 전달한다. QUIC의 목표중 하나는 프로토콜의 쉬운 배포이다. QUIC의 마이너 패치를 할 때마다 클라이언트 및 모든 서버에서 패치할 수는 없기 때문에 하위 호환성을 보장하기 위해 이런 과정이 필요하다. 부가적인 Round-trip은 발생하지만 서버가 지원하는 QUIC 버전의 범위가 넓을수록 덜 발생한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;디피-헬만 키 교환 알고리즘&lt;/strong&gt;
동일한 키로 암호화하고 복호화 하는 것을 대칭키 알고리즘이라고 부르며 보통 AES-256 등의 알고리즘이 있다. 반면, 비대칭키는 암호화와 복호화 혹은 전자서명과 검증에 쓰이는 키가 공개키와 개인키로 서로 다른 값을 사용할 수 있는 알고리즘을 말한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;디피-헬만 키 교환의 아름다움은 대칭키로 써야 할 forward-secure key를 만들 때 이를 네트워크에 노출시키지 않고도 Client와 Server가 각각 동일한 값을 만들 수 있다는 점이다. 따라서, 비대칭키 알고리즘은 연산 복잡도가 크기 때문에 키 교환에만 사용하고 실제 암호화 통신에서는 대칭키 알고리즘을 이용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1bed14f9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1bed14f9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sCOkV0uU6JXmBBsjjMUN_Q-bf62f8a03bfa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1bed14f9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sCOkV0uU6JXmBBsjjMUN_Q-bf62f8a03bfa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mdpi.com/2079-9292/7/12/370"&gt;Ref. Certificate Based Authentication Mechanism for PMU Communication Networks Based on IEC&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-stream-multiplexing"&gt;② Stream Multiplexing&lt;/h3&gt;
&lt;p&gt;Head-of-line blocking은 패킷의 도착 순서를 보장하다 보니 생기는 현상이다. QUIC에서는 하나의 커넥션을 맺어두고 요청의 성격을 &lt;strong&gt;‘Stream’&lt;/strong&gt;이라는 단위로 분리해서 문제를 완화한다. QUIC Stream은 양방향 바이트 스트림을 제공하는 경량화 및 추상화된 개념이다. Stream은 stream ID로 구분되는데 홀수 번호의 ID는 클라이언트가 사용하고 짝수 번호의 ID는 서버가 이용하도록 규칙을 정했다. 프레임 마지막에 FIN bit를 넣으면 해당 스트림은 종료한다.&lt;/p&gt;
&lt;p&gt;대역폭은 한정적이기 때문에 스트림을 무한정 만들수는 없다. 하나의 커넥션 은 다수의 스트림 안에서 대역폭을 어떻게 할당할 지 결정해야 한다. 논문에서는 HTTP/2 프로토콜의 스케쥴링 방법을 따른다고 했지만, 2022년 공개된 HTTP/3 명세에서는 이 기능이 제외되어 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HTTP/3 does not provide a means of signaling priority. Note that, while there is no explicit signaling for priority, this does not mean that prioritization is not important for achieving good performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;스트림에 우선순위를 부여하는 기능 자체가 &lt;a href="https://datatracker.ietf.org/doc/rfc9218/"&gt;RFC 9218&lt;/a&gt;로, 별도 규격으로 제정되었다. 필요에 따라 선택적으로 사용할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cf824ad5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cf824ad5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-J7BatK9MqKnDJQHzmp8QTQ-242decc731d0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cf824ad5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-J7BatK9MqKnDJQHzmp8QTQ-242decc731d0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;QUIC 패킷 기본 구조&lt;/p&gt;
&lt;p&gt;위 그림은 QUIC 패킷의 기본 구조를 나타낸 것으로 커넥션 ID, 패킷 번호를 포함한 공통 헤더와 하나 이상의 프레임으로 구성되어 있다. 한 프레임은 Stream ID로 본인의 패킷이 어떤 스트림에 속하는지 구분한다. 스트림과 프레임의 관계를 HPBN에서 아래 그림과 같이 잘 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5c456a88"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5c456a88"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s03PSlD_XBhhfBUjB3Ykdw-f3f2ce671363.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5c456a88"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s03PSlD_XBhhfBUjB3Ykdw-f3f2ce671363.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hpbn.co/http2/"&gt;HPBN / Streams, Messages, and Frames&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="-authentication-and-encryption"&gt;③ Authentication and Encryption&lt;/h3&gt;
&lt;p&gt;패킷 헤더와 Handshake과정에서 공개되는 일부 정보를 제외하고는 모든 QUIC패킷은 암호화된 채로 이루어진다. 필자는 Authentication이란 말을 사실 잘 이해하지 못했고 중요하게도 생각하지 않아서 더 찾아보진 않았다. 내 생각에는 아마도 인증서 발급기관이 인증서를 발급받은 회사 및 개인의 신원을 보증해주는 Chain of Trust를 이야기하는 것으로 이해했다.&lt;/p&gt;
&lt;h3 id="-loss-recovery"&gt;④ Loss Recovery&lt;/h3&gt;
&lt;p&gt;신뢰성 연결을 보장하기 위해서 수신자가 패킷을 제대로 전송받지 않았을 경우 송신자는 동일한 패킷을 재전송해서 손실을 복구해야 한다. 송신자의 입장에서 패킷이 &lt;strong&gt;‘전달되지 않았다’&lt;/strong&gt; 라는 판단은 Timeout으로 결정한다. 클라이언트마다 서버로 통신하는 라운드-트립 시간 모두 다르기 때문에 Timeout시간도 다르게 결정해야 한다. TCP 프로토콜은 SampleRTT라는 왕복시간을 예측해서 이 값을 Timeout의 기준점으로 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-69bb2acd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-69bb2acd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m02nSLMIUhMtwf-0ywVVYw-969e00f32a29.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-69bb2acd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m02nSLMIUhMtwf-0ywVVYw-969e00f32a29.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://tcpcc.systemsapproach.org/algorithm.html"&gt;TCP Congestion Control: A Systems Approach&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;TCP/IP는 패킷을 재전송할 때 시퀀스 번호를 동일한 값을 주기 때문에, 서버가 보내준 ACK가 어떤 요청에 대해서 보내준 건지 판단할 수 없는 문제가 발생한다. 이를 retransmission ambiguity 라고 부른다. 그림 (a)는 SampleRTT가 실제보다 길게 왜곡된 상황을 보여주며, 그림 (b)는 SampleRTT가 더 짧게 왜곡된 상황을 보여준다.&lt;/p&gt;
&lt;p&gt;QUIC은 재전송패킷도 새로운 번호를 부여하도록해서 이 모호성 문제를 해결한다. 또한 클라이언트의 정교한 RTT계산을 돕기 위해, 서버에서 요청을 수행하는데 걸린 시간을 첨부해서 응답한다. TCP/IP보다 더욱 정확한 RTT계산방식으로 인해 불필요한 패킷 재전송을 방지했으며, 서버간 딜레이를 이용하는 BBR, PCC 혼잡제어 컨트롤러에 도움을 준다.&lt;/p&gt;
&lt;h3 id="-flow-control--congestion-control"&gt;⑤ Flow Control &amp;amp; Congestion Control&lt;/h3&gt;
&lt;p&gt;흐름 제어란 송신자 측의 처리 속도를 감안해서 전송량을 조절하는 것을 말하고, 혼잡 제어란 네트워크 대역폭을 감안해서 전송량을 조절하는 것을 말한다. QUIC은 HTTP/2와 비슷하게 &lt;strong&gt;Credit-based flow control&lt;/strong&gt;을 커넥션 레벨과 스트림 레벨에 각각 적용한다.&lt;/p&gt;
&lt;p&gt;QUIC은 특별한 혼잡제어 알고리즘을 채택하진 않았고, 사용자가 직접 개발할 수 있도록 설계되었다. 다만, 논문 저자들은 TCP프로토콜과 동일한 CUBIC 알고리즘을 사용했다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Credit-based flow control&lt;/strong&gt;
대역폭이 큰 네트워크 대비, 처리 속도가 늦어서 버퍼 오버플로우가 발생하는 것을 막는 흐름 제어 방식이다. 정확한 정의는 사람마다, 문헌마다 다르게 내릴 수 있다. Credit-based flow control 기본적인 아이디어는 가상의 Capacity가 스트림에 할당되고 송신자가 DATA 패킷을 보내면, 패킷 크기만큼 용량이 채워진다. Capacity가 가득 차면 송신자는 DATA패킷을 보내지 못하고 수신자 측에서 WINDOW_UPDATE 패킷을 보내서 다시 크레딧을 채워줘야 송신자가 데이터를 전송할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e78213c8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e78213c8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jZEV_mcCbSdtupnGnB7gZA-3687957c9b7b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e78213c8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jZEV_mcCbSdtupnGnB7gZA-3687957c9b7b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Credit-based Flow Control의 예시 by scalalang2 (직접 그렸다 ㅎㅎ..)&lt;/p&gt;
&lt;h3 id="-nat-rebinding-and-connection-migration"&gt;⑥ NAT Rebinding and Connection Migration&lt;/h3&gt;
&lt;p&gt;NAT 장비가 클라이언트 IP랑 포트를 변경해도, 64-bit의 커넥션 ID로 클라이언트를 식별할 수 있도록 하는 기능을 커넥션 마이그레이션이라고 정의한다. 이 기능은 아직 구현되지 않았고, 작업중이라고 한다.&lt;/p&gt;
&lt;h3 id="-quic-discovery-for-https"&gt;⑦ QUIC Discovery for HTTPS&lt;/h3&gt;
&lt;p&gt;클라이언트는 내가 통신하는 서버가 QUIC을 지원하는지 모른다. 처음에는 TCP/IP기반의 HTTP로 통신하는데, 만약 서버가 QUIC을 지원한다면 HTTP 응답 헤더 Alt-Svc 필드에 “우리는 QUIC을 지원하니까 한 번 맛만 보실래요?” 라고 표현해야한다.&lt;/p&gt;
&lt;h2 id="인터넷-스케일로-배포하기"&gt;인터넷 스케일로 배포하기&lt;/h2&gt;
&lt;p&gt;구글은 인터넷 브라우저 구글 크롬에 QUIC을 도입했고 리포팅을 동의한 사용자에 한해서 A/B 테스트를 진행하면서 QUIC 프로토콜을 발전시켜왔다. 논문에서 보여주는 실험 결과 데이터들이 제한된 실험 환경에서 이루어진 게 아니라 실제 인터넷 세상에 배포되고 평가된 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2013년, June — Google Chrome에 QUIC이 배포되었다. 이 때는 특정 옵션을 부여하고 실행해야 활성화 되어서 QUIC팀 전용 목적으로 사용했다.&lt;/li&gt;
&lt;li&gt;2014년, — 0.025% 미만의 유저에 대해서 A/B 테스트를 진행한다. 시간이 지나 점점 안전하다고 평가받는다.&lt;/li&gt;
&lt;li&gt;2017년, 무려 3년이 지나, 모든 구글 크롬 유저와 안드로이드 유튜브 앱 유저들의 통신 프로토콜을 QUIC으로 전환한다. 2016년에 유튜브 앱 통신을 QUIC으로 전환했더니 구글 egress 에서 QUIC 프로토콜의 비중이 15%에서 30%로 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-87e7fae6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-87e7fae6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-48iMerb-vZ-GmyyFjrEFkA-5a82d89c81eb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-87e7fae6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-48iMerb-vZ-GmyyFjrEFkA-5a82d89c81eb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3번의 회귀 테스트를 진행하면서, 검색 레이턴시를 측정한 결과&lt;/p&gt;
&lt;p&gt;위 그림은 QUIC을 구글 검색에 사용하면서 3번의 회귀 테스트를 진행한 결과이다. Y축이 검색 레이턴시를 얼마나 감소시켰는지를 보여주며 값이 클 수록 효과가 컸다는 의미이다. 중간에 아예 트래픽이 차단된 경우가 있는데 이 때는 0-RTT 통신을 할 때 암호화되지 않은 패킷이 처리되는 버그가 있어서 QUIC 통신을 전환했다고 한다. 새로운 프로토콜에 대한 도입을 준비하면서 이렇게 긴급대응할 수 있는 환경까지 준비했다.&lt;/p&gt;
&lt;p&gt;3번째 회귀 테스트에서 UDP-Proxy를 지원했다. 구글의 REL은 상업용 혹은 특정 중요한 서비스를 제외하고는 TLS-Termination Proxy 처럼 작동했다. QUIC은 기본적으로 패킷을 암호화 하기 때문에 전송계층과 암호계층을 분리하는 TLS Termination Proxy같은 개념이 없기에 구글 REL을 온전히 사용하지 못하고 있었다. 3번째 회귀 테스트에서 QUIC패킷을 포워드 하는 UDP Proxy를 도입해서 REL을 활성화했고 그 결과 검색 레이턴시가 감소했다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 REL : restricted edge locations&lt;br&gt;
구글의 많은 서버들이 ISP에 배포되어 있는데 이런 환경을 본인들은 REL이라고 부른다고 한다. 즉, UDP-Proxy가 없던 시절의 QUIC 프로토콜을 쓰는 유저들은 ISP를 거쳐서 구글의 un-restricted front-end server를 접근할 수 없었기 때문에 거대한 네트워크 망을 이용하지 못하는 일부 손실이 있었다. 라고 필자는 이해했다. (내가 구글 직원이었다면 확실한 말을 했을텐데 아쉽다..)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="성능-맛보기와-한계점"&gt;성능 맛보기와 한계점&lt;/h2&gt;
&lt;p&gt;성능 그룹은 크게 QUIC을 쓰는 집단과 TCP/TLS를 쓰는 집단 두 개로 나눠서 진행했다. QUIC 그룹 안에서도 1-RTT+를 통째로 진행한 프로토콜은 따로 분류했고, TCP/TLS 그룹에서는 검색 결과는 HTTP/2를 이용했고, 유튜브 재생은 HTTP/1.1로 진행되었다. 두 그룹 모두 혼잡 제어 컨트롤러는 CUBIC 알고리즘을 사용했다. 검색 결과 데이터는 2016년 12월 12일 부터 일주일 간 수집된 데이터를 이용했고, 유튜브 데이터는 2017년 1월 19일부터 일주일 간 수집된 데이터를 이용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-361a8b59"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-361a8b59"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cESrm_WaRM2JTmYFGb1i2w-260222ca86fb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-361a8b59"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cESrm_WaRM2JTmYFGb1i2w-260222ca86fb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Handshake Latency 비교&lt;/p&gt;
&lt;p&gt;0-RTT 통신은 처음부터 커넥션이 맺어져 있다고 간주하고 handshake 비용을 0원으로 처리했다. 그것을 감안하더라도 1-RTT+ 통신에 대해서는 확실히 눈에 띄는 개선이 존재했다. TCP/TLS 대비 불필요한 RTT가 1회 감소했기 때문에 당연한 결과이다.&lt;/p&gt;
&lt;p&gt;이 논문이 발표되고 나서, TLS 1.3 버전에서도 0-RTT를 지원했기 때문에 지금 다시 측정하면 조금 결과는 다를 수 있다, 그럼에도 TCP레벨에서 handshake 비용이 있기 때문에 QUIC이 더 빠를 것이다. 논문 저자들은 위 그래프는 QUIC의 더욱 개선된 loss recovery로 인한 가치가 일부 포함되어 있을 것이라고 말했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이 연구가 얼마나 가치가 있는가?&lt;/strong&gt; Handshake latency는 실제 서버가 처리하는 연산을 포함해서 전체 레이턴시의 20% 정도만을 차지하지만, 그 영향력은 상당하다. 아마존은 레이턴시가 100ms씩 증가할 때마다 수익이 1% 감소하는 현상을 발견했으며, 구글 검색 서비스는 100ms의 지연이 있으면 DAU가 크게 감소하는 현상을 발견했다.&lt;/p&gt;
&lt;p&gt;이제부터 검색 도메인과 유튜브 재생 및 Rebuffer Rate, 총 3개의 실험 결과를보여준다. 검색과 영상, 이 두 도메인은 아래와 같은 차이가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;검색 도메인 : low load latency-sensitive application&lt;/strong&gt;&lt;br&gt;
로드는 적지만 레이턴시에 민감함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스트리밍 : heavy load bandwidth-sensitive application&lt;/strong&gt;&lt;br&gt;
로드가 크고 대역폭에 민감함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="-검색-레이턴시"&gt;① 검색 레이턴시&lt;/h3&gt;
&lt;p&gt;검색을 구생하고 결과를 받기까지의 레이턴시를 의미하며, 이미지 및 임베딩된 컨텐츠 전체를 포함한다. 전체 페이로드는 평균적으로 구글 PC 검색 결과는 100KB 정도 되고, 모바일 컨텐츠는 40 KB 정도가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7420c8c5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7420c8c5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KMcywtwSCjzgVHEt9KW3rA-64e2a6975624.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7420c8c5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-KMcywtwSCjzgVHEt9KW3rA-64e2a6975624.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;구글에서 집계한 모든 커넥션의 평균 RTT를 조사한 결과&lt;/p&gt;
&lt;p&gt;구글에서 집계한 모든 커넥션의 평균 RTT를 조사한 그림으로, Y축인 CDF는 누적 분포 함수(Cumulative Distribution Function)을 의미한다. 20% 커넥션의 최소 RTT는 150ms보다 크고, 10% 커넥션의 RTT는 300ms보다 크다. 따라서, 한 번의 RTT를 줄이는게 의미가 크다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-790fed71"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-790fed71"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DDQ1JY03MHhQurNyaROEbQ-73195ddf3e20.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-790fed71"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DDQ1JY03MHhQurNyaROEbQ-73195ddf3e20.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;TCP/TLS대비 QUIC을 도입했을 때의 효과&lt;/p&gt;
&lt;p&gt;모바일의 경우 1% ~ 5%의 낮은 레이턴시의 경우 오히려 약간의 비용이 발생했는데, 이 현상은 아래 한계편에서 좀 더 다룬다. p99에서 최대 16.7%의 레이턴시 감속 있었다. 모바일의 경우에는 PC 데스크탑 보다 평균 지연 감소율이 낮게 측정되었는데, 이는 모바일에 한해서는 0-RTT 통신 비중이 PC보다 낮기 때문이다.&lt;/p&gt;
&lt;p&gt;이 현상에는 2가지 원인이 있는데, 첫번째로는 모바일 유저의 네트워크 변경시 IP주소가 달라져서 source address token 정보가 무효화되기 때문이고, 두 번째로는 네트워크 변경시 접근하는 데이터 센터 지역이 달라질 수 있기 때문이다. 즉, 기존에 연결을 맺었던 서버가 아니라 새로운 곳으로 접근하기 때문에 1-RTT를 다시 수행해야 한다. 논문 저자들이 직접 서버 로그를 분석한 결과, 모바일 유저의 65%정도만 0-RTT를 수행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-39f156b4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-39f156b4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qR9OLIyw6IiW1nhwVWIYuQ-5f1836d1ce87.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-39f156b4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qR9OLIyw6IiW1nhwVWIYuQ-5f1836d1ce87.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;검색 결과 레이턴시&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Handshake Latency 그래프랑 구분하자, &lt;br&gt;
이 그림은 검색 결과를 만드는 연산까지 포함한 레이턴시이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-video-latency"&gt;② Video Latency&lt;/h3&gt;
&lt;p&gt;유튜브에서는 부드러운 재생을 위해 실제로 영상을 보여주기 이전에 몇초의 프레임 데이터를 받아서 메모리에 올려두고 있어야 한다. 다시 말해, 유저가 재생 버튼을 클릭한 순간부터 실제로 재생되기 까지의 딜레이를 Video Latency라고 말한다.&lt;/p&gt;
&lt;p&gt;이 때 저장해야 할 데이터의 총량은 유저의 Bitrate에 따라 달라진다. 이 부분은 대역폭이 크고 작을 때 Handshake 비용이 전체 레이턴시에서 차지하는 비중만 달라질 뿐이지 확실히 이득은 있었고 QUIC이 동작하는 성격이 검색 레이턴시와 비슷하다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ab877077"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ab877077"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qfuo3dOhnNzsITEvLRBvkA-349ed30184dc.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ab877077"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-qfuo3dOhnNzsITEvLRBvkA-349ed30184dc.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Mean Video Latecny&lt;/p&gt;
&lt;h3 id="-video-rebuffer-rate"&gt;③ Video Rebuffer Rate&lt;/h3&gt;
&lt;p&gt;앞서, 영상을 재생할 때 메모리(버퍼)에 일부 프레임 정보를 저장해야 한다고 했다. 만약, 영상을 재생하다가 버퍼가 채워지기도 전에 모두 소진하면 영상은 잠깐 멈추고, 플레이어는 다시 버퍼를 채워야 한다. 소위 ‘버퍼링이 심하다’라고 말하는 현상으로, Video Rebuffer Ratio는 영상 시청 시간 중 영상이 멈춘 시간의 비율을 말하며 아래 식으로 계산한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(Rebuffer Time) / (Rebuffer Time + Video Play Time)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3a9d61b2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3a9d61b2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TVjHXG8BkNFsIIEHz16KMw-5f8476af15de.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3a9d61b2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TVjHXG8BkNFsIIEHz16KMw-5f8476af15de.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Rebuffer Ratio Reduction by percentile&lt;/p&gt;
&lt;p&gt;결과를 보면 p99에서 18.5%으로 매우 높은 효과를 보여준다. &lt;strong&gt;버퍼를 다시 채우는 행위는, 이미 연결된 커넥션을 통해 하는 거라서 Handshke RTT는 크게 영향이 없다.&lt;/strong&gt; 즉 논문 저자들은 이 결과는 Loss Recovery Latency와 혼잡 지연 상황에서 TCP보다 높은 효율을 보여주기 때문이라고 설명한다.&lt;/p&gt;
&lt;h3 id="-loss-recovery-latency"&gt;④ Loss Recovery Latency&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d69c1466"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d69c1466"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-a4r1xGmOk218K87vExj7qw-440f9ee54678.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d69c1466"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-a4r1xGmOk218K87vExj7qw-440f9ee54678.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;TCP/IP에서 재전송되는 비율&lt;/p&gt;
&lt;p&gt;먼저, TCP/IP의 재전송 비율이 얼마나 되는지 보여주면서 Loss Recovery의 효율을 개선하면 어떻게 영향을 미치는지 보여주고자 한다. 700ms RTT가 넘는 구간에서는 평균적으로 재전송율이 2%인데, 이는 평균치라서 실제 재전송 비율은 p90에서 8%, p95에서 18%로 대단히 높다.&lt;/p&gt;
&lt;p&gt;다시 Rebuffer Rate 이야기로 돌아와보자, 논문에서는 어떤 이유인지는 설명이 없었지만 영상 플레이는 2개의 TCP 커넥션을 맺어서 재생 정보를 전달 받는다. 2개의 개별 커넥션을 쓰면 한 쪽에서 받은 패킷 ACK 정보를 다른쪽에서 공유하지 않기 때문에, 정확한 SampleRTT를 계산할 수 없어서 손실 감지(loss detection)에서 손해를 본다.&lt;/p&gt;
&lt;p&gt;이는 꼭 유튜브 재생 뿐만 아니라 웹 브라우저에서도 다수의 TCP 세션을 열어서 동시에 컨텐츠를 다운로드 받고, 서버에서 데이터베이스로 커넥션을 맺을 때에도 커넥션 풀(Connection Pool)을 이용하는 것과 비슷하다. QUIC은 하나의 컨게션에서 여러 스트림을 논리적으로 나눠서 통신하는 구조이기 때문에 loss recovery에서 이득이 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;HTTP 프로토콜을 쓰면 MySQL 쿼리 응답속도가 더 빨라질까?&lt;/strong&gt;&lt;br&gt;
백엔드 개발자들은 서버를 만들 때, 다수의 데이터베이스 커넥션을 만들어두고 돌려 쓰는게 기본적인 패턴이었다. 그렇다면, QUIC의 Multiplexing과 0-RTT 그리고, Loss Recovery의 혜택을 보면 쿼리 응답속도도 더 빨라질까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;찾아보니 Vitess를 만들고 현재 메인테이너인 사람들이 창업한 PlanetScale에서 유사한 실험을 진행한 아티클이 있었다. 왜 세상에는 우리가 무언가를 떠올리면 이미 해볼거 다 해본 사람들이 있는걸까? 대단하다.&lt;br&gt;
&lt;a href="https://planetscale.com/blog/faster-mysql-with-http3#running-the-tests"&gt;https://planetscale.com/blog/faster-mysql-with-http3#running-the-tests&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론은 네트워크 혼잡도와, 손실율, RTT가 높으면 QUIC을 이용하는 효율이 높아진다. 따라서, 대부분의 상황에서 QUIC이 도움을 줄 것으로 생각한다.&lt;/p&gt;
&lt;h3 id="-limitation"&gt;⑤ Limitation&lt;/h3&gt;
&lt;p&gt;거의 모든 기술은 Trade-off가 존재한다. QUIC은 아래의 한계점 혹은 고려해야할 사항이 존재한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;QUIC은 TCP보다 CPU를 3.5% 더 사용한다. 주로, 암호학 연산이 큰데 이를 최적화 하기 위해 모바일에 최적화된 ChaCha20를 도입했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-warmed connections:&lt;/strong&gt;
브라우저는 방문하는 웹 사이트가 많아서 커넥션을 재사용하기 어려울 수 있는데, 모바일 앱은 이미 잘 알려진 목적지 주소를 이용하기 때문에 커넥션을 맺어두고 두고 두고 재사용할 수 있다. 이 경우는 0-RTT의 효과를 크게 보기 어렵다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High bandwidth, low-delay, low-loss ntwork:&lt;/strong&gt;
네트워크가 너무 맛있게 풍부한 경우, QUIC은 아주 작은 효과만 있었고 때로는 성능에 안좋은 영향을 주기도 했다. 논문 저자들은 이것이 QUIC의 문제라기 보다는 커널 레벨에서 동작하는 TCP 혹은 UDP가 아닌, QUIC은 유저 스페이스에서 동작하기 때문에 스케쥴러의 비효율성 때문이라고 생각했다. (커널-레벨에 추가되면 좀 괜찮아 질까? 기대해볼 수 있다)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile devices:&lt;/strong&gt;&lt;br&gt;
많은 서버들이 모바일 컨텐츠는 데이터 전송량을 줄이기 위해 최적화를 진행하기도 한다. 모바일은 로컬 기기의 CPU가 병목이 되는 경우도 있어서 QUIC의 개선으로 인한 효용이 적을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quic-프로토콜-개발-경험-공유"&gt;QUIC 프로토콜 개발 경험 공유&lt;/h2&gt;
&lt;p&gt;논문 마지막에는, 구글이 QUIC을 글로벌 스케일로 배포하면서 겪은 경험들을 소개하는 것으로 마친다.&lt;/p&gt;
&lt;h3 id="-패킷-크기"&gt;① 패킷 크기&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4517a5b2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4517a5b2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F5s0x6TOT8EmB8VBiOF9KA-1941ac5e9145.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4517a5b2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F5s0x6TOT8EmB8VBiOF9KA-1941ac5e9145.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;1200부터 5byte 간격으로 늘리면서 도달율을 조사한 그래프&lt;/p&gt;
&lt;p&gt;1200부터 1500까지 5byte 간격으로 세팅하고, 24,000개의 크롬에서 에코 서버로 전송하면서 패킷 도달율을 조사했다. 일반적으로 이더넷 MTU의 최대 패킷이 1500 byte 정도라서 UDP 패킷 크기가 커질수록 도달율이 낮아졌다. 사실 필자는 그래프를 어떻게 해석해야 할지는 잘 몰라서 제대로 해석할 수 없었다. 어쨋든 이 데이터를 기반으로 QUIC의 기본 패킷 크기를 1350 byte로 결정했다고 한다.&lt;/p&gt;
&lt;h3 id="udp-블럭킹과-쓰로틀"&gt;UDP 블럭킹과 쓰로틀&lt;/h3&gt;
&lt;p&gt;2016년, 데이터를 보니까 95.3%의 유저는 QUIC을 사용할 수 있었고, 4.4%의 유저는 QUIC을 전혀 이용할 수 없었다. 수동적으로 조사해본 결과 대다수의 유저가 기업 네트워크를 이용해서 방화벽이 막혀있었고, 아직까지 ISP들이 QUIC패킷을 차단한 경우는 없었다.&lt;/p&gt;
&lt;p&gt;나머지 0.3%의 유저들은 특정 ISP 사업자들이 UDP 통신에 Rate Limit을 걸어둔 경우였는데, 구글이 직접 연락해서 Limit을 해제하거나 높여달라고 말했다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We manually disable QUIC at our servers for entire Autonomous Systems (AS) where such throttling is detected and reach out to the operators running the network, asking them to either remove or at least raise their limits. ㅋㅋㅋ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-fec-forward-error-correction"&gt;② FEC (Forward Error Correction)&lt;/h3&gt;
&lt;p&gt;FEC는 오류를 클라이언트 레벨에서 고칠 수 있도록 부가적인 정보를 포함해서 보내는 것이다. 체크섬 처럼 손실된 정보를 발견하는 용도가 아니라 직접 오류가 난 부분까지 고칠 수 있는 알고리즘이다. 전공수업을 열심히 들었다면 해밍 코드라는 용어를 기억할 것이다.&lt;/p&gt;
&lt;p&gt;FEC를 썻을때 재전송율은 크게 줄었으나 결국 레이턴시가 증가했다. 이 패킷까지 포함시키면 대역폭 부담이 늘어나는 트레이드-오프가 존재해서 현재는 제거된 상태라고한다. 적절히 잘 쓰기만 한다면 엄청난 기능인 건 확실해서 후속 연구들이 꾸준히 이루어지고 있다.&lt;/p&gt;
&lt;h3 id="-user-space-development"&gt;③ User-space development&lt;/h3&gt;
&lt;p&gt;커널이 아닌, 유저 스페이스에서 개발하는 것이 어떤 경험을 주었는지 설명한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;방대한 단위 테스트와 E2E 테스트 코드를 작성하면서 견고한 프로토콜을 만들어 나갔다.&lt;/li&gt;
&lt;li&gt;QUIC 전용 시뮬레이터도 개발했다.&lt;/li&gt;
&lt;li&gt;커널 API에 의해 메모리가 제한되지도 않았고 다른 인프라 도구들과 결합하기도 쉬워서, QUIC 프로토콜에 대한 방대한 디버깅 로그를 남겼고 대규모 분석을 수행했다.&lt;/li&gt;
&lt;li&gt;수십년 전에 개발된, CUBIC알고리즘을 다시 구현하면서 치명적인 버그를 발견해서 이를 수정했고, 이 하나의 버그를 고치니까 QUBIC의 재전송율이 30% 감소했고, CPU 효율성은 17% 증가했다. 그리고 TCP 재전송율이 20% 증가했다. &lt;a href="https://www.ietf.org/proceedings/94/slides/slides-94-tcpm-8.pdf"&gt;이 내용은 해당 링크의 발표자료에 자세히 나와있다.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;그리고 논문 저자들은 많은 실험들을 빠르게 진행할했으며, 실험 결과 유용하지 않다고 여겨지는 것들은 빠르게 폐기했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7b875b70"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7b875b70"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j4M9wL9kC1_jpjV64CAmXg-51766e0c0b1a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7b875b70"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j4M9wL9kC1_jpjV64CAmXg-51766e0c0b1a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;1년동안 배포된 QUIC 프로토콜 버전&lt;/p&gt;
&lt;p&gt;논문저자들은 2015년부터 2016년까지 약 2년 가까이 되는 시간동안 7개의 버전을 배포해서 빠르게 실험하고 결과를 정리했다&lt;/p&gt;
&lt;h3 id="-experiences-with-middleboxes"&gt;④ Experiences with Middleboxes&lt;/h3&gt;
&lt;p&gt;마지막으로는 인터넷 중간사업자들과의 경험을 공유하면서 역시 암호화 하길 잘했다는 것으로 결론낸다.&lt;/p&gt;
&lt;p&gt;QUIC이 모든 패킷이 암호화 된게 아니기 때문에 결국 공개된 정보들이 존재하는데, Middlebox 벤더들이 이를 식별하고 별도의 무언가 처리를 하고 있었다. 프로토콜 일부를 변경하는 과정에서 UDP 통신 자체의 문제가 발생한 것이다. 구글이 벤더사에 연락해서 이를 해결하긴 했지만 ㅋㅋ&lt;/p&gt;
&lt;p&gt;이 경험을 통해 “아 역시 중간 사업자들의 영향없이 패킷이 변조없이 온전하게 유저에게 도달되었다는 걸 증명할 유일한 방법”은 암호화 뿐이구나 라고 생각했다고 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This experience reinforces the premise on which QUIC was designed: when deploying end-to-end changes, encryption is the only means available to ensure that bits that ought not be used by a middlebox are in fact not used by one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘은 구글이 &lt;a href="https://dl.acm.org/doi/10.1145/3098822.3098842"&gt;ACM SIGCOMM’17 에서 발표한 논문&lt;/a&gt; 을 정리하면서 QUIC에 대해 설계 철학과 성능 및 개발 과정을 소개했다. 쓰다보니까 그냥 논문 한편을 통째로 정리한 꼴이 되었는데, 매번 이런 글만 쓰게 되는것 같아서.. 다음에는 나의 인사이트가 담긴 글을 써보려고 한다.&lt;/p&gt;
&lt;p&gt;또 하나 재밌는 점은 커널이 아닌 유저-스페이스에서 개발되는 프로토콜이다 보니까 프로토콜이 다양한 언어로 개발되고 있다는 점이다. TCP/UDP는 코드를 보려면 커널 코드를 알아야하는데 QUIC은 &lt;a href="https://github.com/microsoft/msquic"&gt;c&lt;/a&gt;, &lt;a href="https://github.com/quic-go/quic-go"&gt;go&lt;/a&gt;, &lt;a href="https://github.com/aws/s2n-quic"&gt;rust&lt;/a&gt; 등 다양한 언어로 구현되어 있다.&lt;/p&gt;
&lt;p&gt;많은 기업들이 현재 gRPC를 적극적으로 도입하면서 HTTP/2에 대해서 어느 정도 익숙해졌다. 2022년에 IETF에서 QUIC 기반의 HTTP/3를 발표하면서 새로운 프로토콜에 대한 평가가 업계 전반에 적극적으로 이루어지고 있다. 기업단위로 HTTP/3를 도입해서 네트워크 비용 절감을 기대해 볼 수도 있고, 여러모로 기대가 되는 기술이다.&lt;/p&gt;
&lt;p&gt;TCP/IP는 커널 레벨에 들어가 있어서, TCP 세션이 갑자기 끊겨도 원인이 무엇인지 개발자 입장에서 분석할 수 있는 방법이 많이 없었는데 User-space 레벨에서 프로토콜이 구현되어 있다보니까 앞으로 eBPF가 아니라면 분석하기 까다로웠던 네트워크 프로토콜 레벨의 대규모 분석도 용이할 것 으로 보인다.&lt;/p&gt;</description></item><item><title>SIEVE로 보는 최신 캐시 연구 동향 | LRU중심 연구에서 FIFO의 발전</title><link>https://scalalang2.com/p/%EC%96%B4%EB%96%A4-%EC%86%8C%EA%B0%80-%EC%9D%BC%EC%9D%84-%EB%8D%94-%EC%9E%98%ED%95%A9%EB%8B%88%EA%B9%8C-fifo%EA%B0%80-lru%EB%B3%B4%EB%8B%A4-%EB%82%AB%EC%8A%B5%EB%8B%88%EB%8B%A4%EC%9A%94-1a49b9060ce4/</link><pubDate>Sun, 03 Dec 2023 07:02:04 +0000</pubDate><guid>https://scalalang2.com/p/%EC%96%B4%EB%96%A4-%EC%86%8C%EA%B0%80-%EC%9D%BC%EC%9D%84-%EB%8D%94-%EC%9E%98%ED%95%A9%EB%8B%88%EA%B9%8C-fifo%EA%B0%80-lru%EB%B3%B4%EB%8B%A4-%EB%82%AB%EC%8A%B5%EB%8B%88%EB%8B%A4%EC%9A%94-1a49b9060ce4/</guid><description>&lt;h3 id="s3fifo--sieve로-보는-최신-캐시-연구-동향"&gt;S3FIFO / SIEVE로 보는 최신 캐시 연구 동향&lt;/h3&gt;
&lt;p&gt;캐시는 모던 컴퓨팅 인프라의 핵심 컴포넌트이며 성능은 높이고 비용은 줄이기 위해 사용된다. 웹, 앱, CDN, 백엔드 서버, 데이터베이스 그리고 가상메모리에 쓰이는 페이지 교체까지 소프트웨어 스택 전반에 많이 채용되고 있는 우수사원이다.&lt;/p&gt;
&lt;p&gt;오늘은 최신 캐시 연구 동향을 알아보기 위해 &lt;strong&gt;SOSP’23, &lt;/strong&gt;NSDI’24에 등재된 두 개의 논문을 다룬다. 두 논문은 카네기 멜런 대학교 &lt;a href="https://www.pdl.cmu.edu/Publications/index.shtml"&gt;병렬 데이터 연구실(Parallel Data Lab)&lt;/a&gt; 에서 발표된 논문으로, 모두 본인이 각자의 분야에서 가장 뛰어난(SOTA) 캐시 알고리즘이라고 주장한다. S3-FIFO 캐시는 14개의 데이터셋에서 10개 분야에서 최고의 효율성을 기록했으며 Optimized-LRU와 비교하여 6x 성능을 향상시켰고, SIEVE는 코드 단 20줄을 고쳐서 ARC 알고리즘의 캐시 미스율을 최대 63.2% 줄였다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3600006.3613147"&gt;FIFO queues are all you need for cache eviction | SOSP 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://junchengyang.com/publication/nsdi24-SIEVE.pdf"&gt;SIEVE is Simpler than LRU: an Efficient Turn-Key Eviction Algorithm for Web Caches&lt;/a&gt; | NSDI 2024&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;논문 근본론&lt;/li&gt;
&lt;li&gt;소프트웨어 캐시(Cache)&lt;/li&gt;
&lt;li&gt;캐시를 평가하는 메트릭들&lt;/li&gt;
&lt;li&gt;LRU (Least Recently Used) 캐시는 아니되옵니다&lt;/li&gt;
&lt;li&gt;원-히트-원더 (One-hit-wonder)&lt;/li&gt;
&lt;li&gt;S3-FIFO 설계 및 구현&lt;/li&gt;
&lt;li&gt;S3-FIFO 성능 평가 및 분석&lt;/li&gt;
&lt;li&gt;SIEVE 설계 및 구현&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="논문-근본론"&gt;논문 근본론&lt;/h2&gt;
&lt;p&gt;본격적인 이야기를 시작함에 앞서, 당신이 이 글을 끝까지 완주할 수 있도록 설득하기 위해 논문의 근본에 대해 말할 수 있어야 한다. 기본적으로 논문이란 다음 3가지를 만족해야 근본이 되기 위한 기본 조건은 갖추게 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이 문제가 왜 반드시 해결되어야 하는 ‘&lt;strong&gt;심각한 문제’&lt;/strong&gt;인지 설명해야 한다.&lt;/li&gt;
&lt;li&gt;그동안 알려진 방법으로는 왜 (완벽히) 해결이 안되는지 서술해야 한다.&lt;/li&gt;
&lt;li&gt;본인이 제시하는 해결 방법과 연구 결과가 어떤 영향을 주는지 설명해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;세 항목이 잘 나타나 있고 좋은 학회에서 피어리뷰 까지 거쳤다면 비로소 근본있는 논문이라 할 수 있겠다. 필자는 여기에 더해 논문을 읽어보기 전에 저자의 프로필까지 보는 편이다.&lt;/p&gt;
&lt;p&gt;두 논문의 주요 저자인 &lt;a href="https://jasony.me/"&gt;Juncheng Yang&lt;/a&gt; 은 카네기 멜런 대학교의 박사과정을 지내고 있다. 본격적인 연구 생활 이전에는 트위터에서 인턴 및 연구원 생활을 지냈는데 저자의 주요 연구인 Segcache는 NSDI’21 에서 best-paper award를 수상했으며 이 연구성과가 반영된 소프트웨어가 트위터에 배포되어 실제로 쓰이고 있다고 한다. 여기까지 탐색해보고 속으로 생각했다. ‘아, 이 논문은 근본의 자세가 되었구나’ 이제 본격적으로 내용을 알아보자.&lt;/p&gt;
&lt;h2 id="소프트웨어-캐시-cache"&gt;소프트웨어 캐시 (Cache)&lt;/h2&gt;
&lt;p&gt;캐시는 모던 컴퓨팅 인프라의 핵심 구성요소로 키/밸류 데이터베이스, 웹 캐시, CDN, 백엔드 서버 등 여러 분야에서 요청 레이턴시를 낮추고, 네트워크 대역폭을 줄이고, 반복적인 계산을 미리 해두는 등의 역할을 한다.&lt;/p&gt;
&lt;p&gt;넷플릭스는 오직 캐시를 위해서만 &lt;a href="https://netflixtechblog.medium.com/cache-warming-leveraging-ebs-for-moving-petabytes-of-data-adcf7a4a78c3"&gt;18,000여대의 서버를 운영하고&lt;/a&gt; 있고 트위터는 10만 CPU와 100 TB이상의 DRAM을 캐시를 위해서 대규모의 클러스터를 운영한다. 사실 우리가 다니고 있는 이 직장에서도 각 조직별로 사용하고 있는 캐시의 역할을 생각해보면 캐시의 성능을 높일 수 있다는 것이 이 산업분야에서 어떤 의미를 지니는지 쉽게 상상할 수 있다.&lt;/p&gt;
&lt;p&gt;캐시는 내가 다루는 모든 &lt;strong&gt;데이터&lt;/strong&gt;보다 저장 용량이 작기 때문에 저장 공간이 부족하면 일부 데이터를 지우고 새로운 내용을 채워 넣어야 한다. 이 때 어떤 데이터를 캐시에서 추방할 건 지 결정하는 &lt;strong&gt;축출 알고리즘(Eviction Algorithm)&lt;/strong&gt;이 캐시의 핵심이다. e.g. LRU(Least Recently Used)캐시는 가장 오랫동안 참조되지 않은 데이터를 큐에서 밀어낸다.&lt;/p&gt;
&lt;h2 id="캐시를-평가하는-메트릭들"&gt;캐시를 평가하는 메트릭들&lt;/h2&gt;
&lt;p&gt;어떤 캐시가 좋은 캐시인지 평가하는 항목에는 5가지가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;효율성(Efficiency) :&lt;/strong&gt; 효율성은 캐시에 요청이 들어왔을 때 필요한 정보가 얼마나 자주 캐시에 남아있는가?를 의미한다. 보통 &lt;strong&gt;캐시 미스 비율(miss ratio)&lt;/strong&gt;로 평가한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리율(Throughput) :&lt;/strong&gt; 캐시가 초당 얼마나 많은 요청을 소화할 수 있는지 나타내며 QPS(Queries Per Seconds)라는 단위로 표현한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;확장성(Scalability) :&lt;/strong&gt; 캐시에 동시 접근 할 때 캐시의 성능인 QPS가 얼마나 증가하는지를 가지고 상대적인 확장성의 높낮이를 비교할 수 있다. 모던 컴퓨터들은 점점더 많은 CPU를 탑재하고 있으며 AMD EPYC 9654P 장비는 192개의 CPU core를 가지고 있다.&lt;/li&gt;
&lt;li&gt;⭐ &lt;strong&gt;플래시 친화성(Flash Friendly) &lt;/strong&gt;: 캐시가 얼마나 플래시에 친화적인지 설명한다. 백엔드 개발자인 우리는 인-메모리나 Redis를 사용해서 캐시를 구현하면 DRAM을 많이 사용해서 구현할텐데 많은 데이터를 리전별로 공급해야 하는 CDN 사업자들은 SSD 같은 낸드 플래시 기반의 저장장치로 캐시로 구현한다고 한다. 하지만, 플래시의 경우 쓰기 횟수가 제한되어 있는데다 랜덤 액세스가 쓰기 증폭을 유발하는 등 자칫 잘못하면 플래시의 수명을 빨리 갉아먹는다. 완전히 마모되어서 쓸 수 없는 상태를 wear out되었다고 표현한다. 플래시 친화성은 캐시가 얼마나 flash 디바이스에 친화적으로 디자인 되었는지를 평가하는 척도이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;심플함(Simplicity) :&lt;/strong&gt; 캐시가 얼마나 쉽게 구현되어 있는지를 나타낸다. 구현이 간단할수록 디버깅이 쉽고 유지보수가 간단하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lru-least-recently-used-캐시는-아니되옵니다"&gt;LRU (Least Recently Used) 캐시는 아니되옵니다&lt;/h2&gt;
&lt;p&gt;논문 저자의 말을 빌리면, 지난 수 세기동안 캐시 연구의 주인공은 LRU 캐시였다. 일반적으로 &lt;strong&gt;요청 워크로드(request workloads)&lt;/strong&gt;가 최근 데이터가 더 자주 접근된다는 temporal locality 성질을 가지고 있어서 LRU가 FIFO캐시보다 더 효율적이다 라는 것이 일반적인 통념이라고 알려져 있다. ARU, SLRU, LIRS 등 최근 캐시 연구들은 모두 LRU를 기반으로 설계되어 왔다.&lt;/p&gt;
&lt;p&gt;그렇다면 왜 그동안 LRU가 캐시 연구분야에서 강세였는가? 이런 통념을 뒤집는다는 게 학술적으로 어떤 의미를 지니고 있는지 좀 더 피부로 느끼려면 &lt;a href="https://en.wikipedia.org/wiki/B%C3%A9l%C3%A1dy%27s_anomaly"&gt;Belady’s Anomaly&lt;/a&gt;에 대해 이해하면 좋다. 다음 그림과 같은 요청이 시간 순서대로 들어온다고 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e9c10e73"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e9c10e73"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JX8fJObUtUjlJBjuiri4iQ-0812724cc162.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e9c10e73"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-JX8fJObUtUjlJBjuiri4iQ-0812724cc162.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;캐시에 위 그림에 나타낸 요청이 순서대로 입력으로 주어진다&lt;/p&gt;
&lt;p&gt;그리고 크기가 3인 FIFO 큐를 사용해서 들어오는 요청을 캐싱한다. 아래 그림에서 &lt;strong&gt;PF는 Page Fault&lt;/strong&gt;를 나타내며 캐시 미스를 의미하고 X는 페이지 교체가 발생하지 않은 캐시 히트를 의미한다. 아래 그림을 보면 크기가 3인 FIFO 큐를 사용할 경우 캐시 히트가 3회 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-30bce031"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-30bce031"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cBSElk1gYFCeriQ8xvpRGA-751fea93a4a8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-30bce031"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cBSElk1gYFCeriQ8xvpRGA-751fea93a4a8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.geeksforgeeks.org/beladys-anomaly-in-page-replacement-algorithms/"&gt;Ref. Geeks for Geeks — Belady’s Anomaly in Page Replacement Algorithms&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;아 캐시 크기가 너무 작으니까 캐시 크기를 늘리면 캐시 히트가 증가하고 캐시 미스가 감소해야겠지? 라는 생각이 직관적이지만 캐시 크기를 3에서 4로 늘릴 경우 오히려 캐시 미스가 증가하는 현상이 나타나는데 이를 &lt;strong&gt;Belady‘s Anomaly&lt;/strong&gt; 이라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9234fe03"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9234fe03"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vHzGQxj6CmjL51cce-BT_Q-bf25bc0ae63a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9234fe03"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vHzGQxj6CmjL51cce-BT_Q-bf25bc0ae63a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.geeksforgeeks.org/beladys-anomaly-in-page-replacement-algorithms/"&gt;Ref. Geeks for Geeks — Belady’s Anomaly in Page Replacement Algorithms&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Belady’s Anomaly&lt;/strong&gt;은 이미 1980s 대에 정립된 이론이며 캐시에서 축출할 데이터를 선택할 때 우선순위를 고려하지 않아서 발생하는 것으로 알려진다. 그리고 이런 이상 현상은 LRU를 사용할 경우 확실히 발생하지 않아 캐시 미스가 감소한다. 이것을 그동안의 캐시 연구가 LRU를 최적화하는데 집중된 이론적 배경으로 설명할 수 있다.&lt;/p&gt;
&lt;p&gt;1990년대에는 2개 이상의 정적인 LRU를 사용하는 연구가 있었고, 2000년대에는 LRU의 크기를 동적으로 변형시키거나 복잡한 메트릭을 평가 지표로 활용하곤 했다. 2010/2020년대에 들어서는 머신러닝을 이용해서 캐시에서 어떤 오브젝트를 밀어낼 것인지 선택하는 알고리즘들이 개발되면서 LRU를 이용한 캐시 구현이 점점 더 복잡해져 가고 있다. (e.g. LRU-k, TwoQ, SLRU, ARC, LIRS, GLCache, HALP)&lt;/p&gt;
&lt;p&gt;논문의 저자는 LRU 캐시에 3가지 문제점이 있다고 지적한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LRU캐시를 구현하는 방법은 다양하지만 &lt;a href="https://github.com/hashicorp/golang-lru/blob/main/internal/list.go#L10-L32"&gt;흔히 양방향 링크드 리스트(Doubly Linked List)로 구현되어 객체당 2개의 포인터를 요구한다&lt;/a&gt;. 캐시 객체의 개수가 많고 크기가 작을수록 오버헤드를 유발한다. (e.g. twitter, facebook 같은 워크로드)&lt;/li&gt;
&lt;li&gt;LRU는 캐시 히트가 발생하면 오브젝트에 락을 걸고 해당 객체를 리스트의 head로 프로모팅 해야 하는데 이 과정에서 최소 6번의 랜덤 액세스가 발생한다. Meta에서 개발된 &lt;a href="https://rocksdb.org/blog/2014/05/14/lock.html"&gt;RocksDB는 LRU 캐시의 락을 확장성 병목으로 지목하기도 했다.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;마지막으로 LRU 캐시는 플래시 친화적이지 않다. 오브젝트 축출 순서가 정렬되어 있지 않기 때문에 랜덤 엑세스를 유발한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="원-히트-원더-one-hit-wonder"&gt;원-히트-원더 (One-hit-wonder)&lt;/h2&gt;
&lt;p&gt;캐시 축출 알고리즘인 LRU는 temporal locality성질에 의해 존재의 타당성을 얻었다면 저자는 FIFO 도입의 타당성을 입증하기 위해 원-히트-원더 (One-hit-wonder)라는 데이터의 새로운 성질을 사용했다. &lt;strong&gt;원-히트-원더&lt;/strong&gt;란 요청 시퀀스에서 한 번만 등장하는 오브젝트의 비율을 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b6b4806a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b6b4806a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ibtEJVH3Oc0EjcN5NqdZnw-693147fb85e7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b6b4806a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ibtEJVH3Oc0EjcN5NqdZnw-693147fb85e7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;S3-FIFO Queue | One-hit-wonder ratio&lt;/p&gt;
&lt;p&gt;위 그림을 보면 A부터 E까지 5개의 요청이 17초 동안 입력으로 주어진다. 시퀀스의 길이가 17인 경우 원-히트-원더는 ‘E’ 하나로 전체 객체 중 비율이 20%이지만 시퀀스 길이를 줄이수록 원-히트-원더 비율이 증가함을 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ab5ccc74"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ab5ccc74"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UIqHLqeHLpVnLvmHrzd83A-aef304a56060.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ab5ccc74"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UIqHLqeHLpVnLvmHrzd83A-aef304a56060.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Zipf’s law , &lt;a href="https://github.com/twitter/cache-trace"&gt;Twitter&lt;/a&gt; , &lt;a href="http://iotta.snia.org/traces/block-io/388"&gt;Microsoft Research&lt;/a&gt; 의 데이터를 보여주는 라인그래프&lt;/p&gt;
&lt;p&gt;웹 캐시 워크로드는 일반적으로 &lt;strong&gt;멱급수 법칙&lt;/strong&gt;(Power-law | generalized Zipfian)을 따른다고 알려져 있다. 위 그림에서 왼쪽 두 개의 그래프는 Zipf 분포를 나타내고 오른쪽 두개는 트위터와 마이크로소프트 리서치에서 공개한 데이터를 토대로 원-히트-원더의 비율을 계산한 것이다.&lt;/p&gt;
&lt;p&gt;그래프의 X축은 시퀀스가 포함하는 오브젝트의 비율을 나타낸 것이다. 그림 (d)를 보면 전체 오브젝트 중 10%를 포함하는 시퀀스에서 원-히트-원더 비율은 트위터의 경우 25%, MSR 데이터는 75%인 걸 알 수 있다. 트위터의 경우 Zipf 그래프를 추종한다기엔 너무 낮은 걸 알 수 있는데 소셜 네트워크 데이터가 일반적으로 좀 더 세게 왜곡(skew)되어 있다고 한다. &lt;strong&gt;이 데이터는 우리에게 캐시 크기를 전체의 10%로 책정 한다면 72%~78% 정도의 객체는 두 번 이상 쓰이지 않기 때문에 공간만 차지한다는 것을 알려준다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e200b140"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e200b140"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-do0zyEBKvIvWfP4LAMLCUA-b70738beab0e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e200b140"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-do0zyEBKvIvWfP4LAMLCUA-b70738beab0e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;LRU 캐시 정책을 적용해서 객체를 축출할 당시의 freq 값이 몇인지 보여준다&lt;/p&gt;
&lt;p&gt;저자들은 실제 데이터에 LRU 캐시 정책을 적용해보면서 오브젝트가 캐시에서 제거될 때 freq 카운트 값을 기록했다. 위에서 앞서 보여준 라인 그래프와 유사하게 트위터의 경우 대략 26%, MSR은 78%정도의 객체가 freq 값이 1인걸 보여준다. LRU캐시에 기록되긴 했지만 2번 이상 발생할 요청이 아니다. 세상에서 가장 완벽한 캐시를 만들려면 미래에 발생할 정보를 완벽히 알고있어야 하지만 그것은 불가능하다.&lt;/p&gt;
&lt;h3 id="quick-demotion--빠르게-강등시키기"&gt;Quick Demotion | 빠르게 강등시키기&lt;/h3&gt;
&lt;p&gt;위 관찰결과에 따라 자리만 차지하고 쓸모 없는 객체들은 빠르게 캐시에서 밀어낼 필요가 있다. 논문에서는 이를 빠른 강등(Quick Demotion) 이라고 표현했다. 기존에는 CDN 서버들이 Bloom Filter를 사용해서 이 역할을 하고 있지만 Bloom Filter는 너무 빠르게 정보를 제거해서 정확도가 떨어진다.&lt;/p&gt;
&lt;p&gt;ARC, LIRS 등 캐시 알고리즘들이 캐시 오염(cache pollution)을 막기 위해 여러 가지 방법을 도입해서 사용하고 있지만 &lt;strong&gt;원-히트-원더 오브젝트가 얼마나 오랫동안 캐시에서 생존할 수 있는지&lt;/strong&gt;는 표현하지 못해서 너무 빨리 제거하거나 너무 느리게 제거하곤 했다. 논문 저자들은 3개의 FIFO 큐만 이용해서원-히트-원더 비율을 낮추는 방법을 고안했다. 논문에 따르면 SOTA보다 높은 효율성을 가지면서 FIFO만 사용해서 설계된 첫번째 알고리즘이라고 한다.&lt;/p&gt;
&lt;h2 id="s3-fifo-설계-및-구현"&gt;S3-FIFO 설계 및 구현&lt;/h2&gt;
&lt;p&gt;위에서 많은 이야기를 했지만 S3-FIFO의 설계 및 구현 난이도는 굉장히 쉬운 편이다. 숙련된 코더라면 당장 에디터 창을 열어서 본인이 직접 코딩할 수 있을 정도이다. S3-FIFO는 전체 캐시 크기의 10%의 공간을 차지하는 small queue &lt;strong&gt;S&lt;/strong&gt;와 main queue &lt;strong&gt;M&lt;/strong&gt; 그리고 나중에 M 보내기 위한 ghost queue &lt;strong&gt;G&lt;/strong&gt; 이 세가지 FIFO queue로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-baff4bef"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-baff4bef"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IITUp_SuQ0jQXJ8sXUwtjQ-4007187c32b8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-baff4bef"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IITUp_SuQ0jQXJ8sXUwtjQ-4007187c32b8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;S3-FIFO의 구조&lt;/p&gt;
&lt;h3 id="1-읽기-연산-read"&gt;1️⃣ 읽기 연산 (Read)&lt;/h3&gt;
&lt;p&gt;큐에 저장될 각 오브젝트는 2 비트 공간을 가진 빈도수 정보를 기록한다. 읽기 연산은 S 혹은 M에 이미 객체가 있다면 freq 변수를 min(freq+1,3) 으로 업데이트하고, 객체가 없다면 캐시에 정보를 저장하는 삽입 연산을 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-683d442e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-683d442e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ha5lIuowYCFeqidsNMhDbw-122257d30f38.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-683d442e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Ha5lIuowYCFeqidsNMhDbw-122257d30f38.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="2-삽입-연산-insert"&gt;2️⃣ 삽입 연산 (Insert)&lt;/h3&gt;
&lt;p&gt;캐시 공간이 가득 차 있다면 캐시 안에 있는 데이터를 제거해서 공간을 확보해야 한다. 이 연산이 &lt;strong&gt;evict()&lt;/strong&gt; 이다. 공간을 확보 한 뒤에 ghost queue G에 데이터가 있다면 main queue M에 객체를 삽입하고 아니라면 S에 삽입한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5ef24a0e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5ef24a0e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WAqFsdrN1ZjHSTTEQZ8PGw-e9d0d6d361fe.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5ef24a0e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WAqFsdrN1ZjHSTTEQZ8PGw-e9d0d6d361fe.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="3-캐시-축출-연산-eviction"&gt;3️⃣ 캐시 축출 연산 (Eviction)&lt;/h3&gt;
&lt;p&gt;small queue S의 크기가 전체 캐시 크기의 10%를 넘는다면 S의 데이터를 제거하고 아니라면 M의 데이터를 제거한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4f9c9d7d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4f9c9d7d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vaC5L_2MdgWbEaq_onxw4Q-7e77c9683e35.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4f9c9d7d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-vaC5L_2MdgWbEaq_onxw4Q-7e77c9683e35.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;small queue S 에서 데이터를 제거할 때, 읽기 연산으로 기록한 빈도수가 1을 넘으면 main queue M으로 데이터를 이동시키고 아니라면 ghost queue G로 넘긴다. 주의 할 점은 M으로 데이터를 이동시키는 연산은 실제 지우는 것으로 취급하지 않고 G로 이동시킬 때만 evicted 변수를 true로 대입한다. 이를 비유하자면 &lt;strong&gt;단기기억(small queue)&lt;/strong&gt;에서 &lt;strong&gt;장기기억(main queue)&lt;/strong&gt;으로 보내는 걸로 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-73cd42b1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-73cd42b1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2Xc_rGioXnevrldmpR__Hw-d8ccc3cf3c46.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-73cd42b1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-2Xc_rGioXnevrldmpR__Hw-d8ccc3cf3c46.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;main queue M 에서 데이터를 제거할 때는 freq 변수가 0인 데이터만 제거한다. 이 값이 1 이상이라면 -1을 하고 큐의 맨 앞에 다시 삽입한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f3a936f6"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f3a936f6"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sfhXwis70HlSDR7snGNtDQ-0200a29f6dcd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f3a936f6"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sfhXwis70HlSDR7snGNtDQ-0200a29f6dcd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;의사코드이긴 하지만 전체 라인 수가 40줄 밖에 안되고 단순한 FIFO queue만을 사용하고 있기 때문에 알고리즘과 구현 난이도가 굉장히 쉽고 간편한게 특징이다.&lt;/p&gt;
&lt;h3 id="4-연산-비용-분석"&gt;4️⃣ 연산 비용 분석&lt;/h3&gt;
&lt;p&gt;캐시 미스가 발생하면 S or M 에서 캐시 축출이 발생하고 이 연산은 큐의 tail object를 M 또는 G로 이동시키는 연산을 포함한다. 특별히 M에서 발생하는 캐시 축출은 freq 변수를 보고 tail object를 큐의 head로 프로모팅 하는 과정을 포함한다. 프로모팅 하는 과정이 while문으로 되어 있는데 논문 저자들은 이게 실제로 발생하는 경우가 캐시 히트보다 현저히 적기 때문에 이 정도 오버헤드는 무시해도 된다고 말하고 있다.&lt;/p&gt;
&lt;p&gt;ghost queue G는 장기기억으로 저장할지 말지만 결정하기 때문에 객체의 고유 아이디(ID)만을 저장한다. 만약 객체의 평균 크기가 4KB이고 ID가 4 byte integer라면 전체 캐시 크기의 0.09%만을 차지한다. G의 경우 꼭 큐일 필요는 없이 버킷 해시 테이블 같은 인덱스 자료구조를 사용해도 무방하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;마지막으로 이 부분이 가장 중요해 보이는데&lt;/strong&gt; 이 모든 연산이 lock-free 자료구조를 이용하면 락을 잡지 않고도 쓰레드-안전하게 구현이 가능하다는 것이다. 이것이 락을 잡는 LRU보다 성능이 더 높다고 주장하는 이유이다. 아래 구현체를 &lt;a href="https://github.com/golang-design/lockfree/blob/master/queue.go"&gt;lockfree queue를 go언어&lt;/a&gt;로 구현한 일부를 가져왔다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;directItem&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// allocate new item&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lastnext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;directItem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;loaditem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lastnext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;loaditem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;loaditem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// are tail and next consistent?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lastnext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// was tail pointing to the last node?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;casitem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lastnext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// try to link item at the end of linked list&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;casitem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// enqueue is done. try swing tail to the inserted node&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// tail was not pointing to the last node&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;casitem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lastnext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// try swing tail to the next node&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;사실 코드를 보면 쓰레드를 명시적으로 블럭킹 하지 않았을 뿐이지 CAS (Compare-And-Set) 연산과 loop를 이용해서 조건을 만족할 때 까지 계속 시도한다. 스레드의 개수가 CPU 코어 개수 보다 작다면 컨텍스트 스위칭 비용이 발생하지 않기에 성능이 높다라곤 하는데 lock-free 자료구조가 정말 더 빠른지에 대해서는 논란의 여지가 있다. 이 이야기는 본 포스팅의 지면을 할애해서 따로 설명하진 않는다.&lt;/p&gt;
&lt;h2 id="s3-fifo-성능-평가-및-분석"&gt;S3-FIFO 성능 평가 및 분석&lt;/h2&gt;
&lt;p&gt;성능 평가 항목에서 아래 세 질문의 답을 내릴 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;S3-FIFO의 효율성이 정말 SOTA 인가?&lt;/li&gt;
&lt;li&gt;S3-FIFO가 정말로 확장성이 높은가?&lt;/li&gt;
&lt;li&gt;S3-FIFO가 진짜로 flash cache design에 도움이 되는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="데이터-셋dataset"&gt;데이터 셋(Dataset)&lt;/h3&gt;
&lt;p&gt;평가를 위해 총 14개의 데이터 셋을 이용했으며 이 중 11개는 공개된 데이터이고 3개는 기업에서 제공하는 비공개 데이터 셋이다. 웹 워크로드 뿐 아니라 블록 I/O, CDN, 키-밸류 데이터를 포함해서 총 854조개의 요청과 61조개의 오브젝트와 21,088 TB 규모의 트래픽, 그리고 총 3,753 TB 규모의 데이터를 가지고 실험을 진행했다. 아래 표는 이번 실험에 이용된 데이터 셋이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8876ea4e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8876ea4e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AErmh7Hsx_8_GBd2VODqLw-34ac7d6542ef.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8876ea4e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AErmh7Hsx_8_GBd2VODqLw-34ac7d6542ef.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;S3-FIFO 큐의 성능 평가를 위해 사용된 데이터 셋&lt;/p&gt;
&lt;p&gt;실험 자체는 &lt;a href="https://github.com/1a1a11a/libCacheSim"&gt;libCacheSim&lt;/a&gt; 이라는 시뮬레이터를 이용했다. 재밌는 건 이게 논문 저자가 직접 개발한 시뮬레이터라는 것이다. META에서 만든 &lt;a href="https://github.com/facebook/CacheLib"&gt;libCache&lt;/a&gt;을 시뮬레이션 용도로 변형한 거라서 실험 결과의 신뢰성에 대해서는 의심하지 않아도 된다. 필자가 재밌다고 한 건 코딩보다는 연구에만 강한 박사과정 학생분들도 꽤 많은데 이 분은 코딩 좀 치는 사람이라는 것이 재밌었다.&lt;/p&gt;
&lt;h3 id="효율성-평가-efficiency"&gt;효율성 평가 (Efficiency)&lt;/h3&gt;
&lt;p&gt;효율성은 얼마나 캐시 미스가 작은지를 나타내는 지표이다. 아래 그림이 S3-FIFO 큐와 다른 13개의 캐시를 비교한 그림이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7bd84291"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7bd84291"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-oHRh6WuCxneawoB2C-DCGQ-aa309d46dc73.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7bd84291"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-oHRh6WuCxneawoB2C-DCGQ-aa309d46dc73.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그래프의 Y축은 정말 기본적인 FIFO 큐에 비해서 얼마나 캐시 미스를 줄였는지 비율을 나타낸다. 값이 클수록 좋은 캐시 알고리즘이다. 그림을 보면 S3-FIFO큐가 모든 알고리즘 중에서 가장 효율이 좋음을 알 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;논문에서는 각 알고리즘 별로 얘는 이래서 안좋고, 쟤는 저래서 안좋고 캐시들이 왜 성능이 안좋은지 조목 조목 까는 공간이 있는데 이 포스팅에서는 다루지 않는다. 궁금하신 분은 논문의 8–9 페이지를 참고하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그림의 왼쪽 그래프는 전체 객체의 10%를 캐시 공간으로 잡는 경우이고 오른쪽 그래프는 캐시 공간을 0.1%로 설정한 경우의 실험 결과이다. 캐시 공간을 줄이면 캐시 미스율도 같이 떨어지는 경향이 나타난다. 그리고 일부 데이터 셋에 대해선 S3-FIFO의 성능이 다른 것 보다 낮은 경우가 있었다. 이는 대 부분의 객체가 단 2번만 히트되는 워크로드인 경우 small queue S의 캐시 미스가 빈번히 발생했다고 한다. 하지만 이는 TinyLFU, LIRS, 2Q 처럼 캐시 공간을 여러개로 분리한 알고리즘 대부분에 적용되는 항목이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5b639e7c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5b639e7c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cCzTv5xtA7ElvoXkHzWsHg-574785d681e6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5b639e7c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-cCzTv5xtA7ElvoXkHzWsHg-574785d681e6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;데이터 셋 별로 비교한 그림 / 거의 뭐 그림 그리기 장인이다.&lt;/p&gt;
&lt;p&gt;위 표는 각 데이터 셋 별로 결과를 세부적으로 나타낸 것이다. 캐시 알고리즘이 다양한 데이터 셋에 대해서 잘 적용되어야 견고한 알고리즘이라고 말할 수 있을 것이다. 캐시 크기가 큰 경우 S3-FIFO 큐가 14개 데이터셋 중에 13개 데이터 셋에 대해서 가장 우위를 점하고 있으며 캐시 크기가 작은 경우 에는 쓸만한 정도로 좋았다 라고 볼 수 있음을 알 수 있다.&lt;/p&gt;
&lt;h3 id="s3-fifo는-왜-효과적인가"&gt;S3-FIFO는 왜 효과적인가?&lt;/h3&gt;
&lt;p&gt;S3-FIFO의 핵심은 원-히트-원더 객체를 빠르게 필터링 해서 가치가 없는 데이터를 캐시 공간에서 빠르게 제거하고 가치가 높은 객체를 오랫동안 보존하는 것이라고 설명했다. 이 원리가 잘 작동하는지 평가하기 위해 아래 두 가지 메트릭을 평가했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Normalized quick demotion speed&lt;/strong&gt;&lt;br&gt;
객체가 얼마나 빨리 small queue S에서 제거되었는지 속도를 나타낸 것이다. LRU에서 데이터가 제거되는 속도를 기준 점으로 잡고 ARC, TinyLFU와 비교했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick demotion precision&lt;/strong&gt; : 제거된 오브젝트가 다시 캐시에 요청으로 들어오기 까지 걸리는 시간이 &lt;strong&gt;캐시 크기/캐시 미스율&lt;/strong&gt; 보다 크다면 “아 역시 빨리 제거하길 잘했다&amp;quot; 라고 판단하고 이 비율을 계산했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-207d4bf0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-207d4bf0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7oGYhnYDfsiwLN4vDukZIQ-c851c1e8f154.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-207d4bf0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7oGYhnYDfsiwLN4vDukZIQ-c851c1e8f154.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;캐시 축출의 정확도와 속도를 나타낸 표&lt;/p&gt;
&lt;p&gt;위 그래프에서 밝은 색일 수록 전체 캐시 크기에서 small queue S에 할당한 비율이 더 크다는 걸 의미한다. S3-FIFO와 TinyLFU가 비교해 볼만하다. 우측 하단의 그림 (d)를 보면 캐시 크기가 작은 경우 정확도 차이가 0.01% 정도로 거의 비슷하다고 하고, 그림 (b)를 보면 TinyLFU의 정확도가 더 높다고 나온다.&lt;/p&gt;
&lt;p&gt;하지만 S3-FIFO에서는 small queue S가 클 수록 더 느리게 제거하고 정확도가 증가한다는 뚜렷한 경향성을 보여주지만 Tiny LFU는 불안정한 데이터를 보여준다. 즉 이 알고리즘을 실제 현업에 도입한기엔 캐시 알고리즘으로써의 견고함이 떨어진다.&lt;/p&gt;
&lt;p&gt;S3-FIFO가 꼭 FIFO queue를 써야 했는가? LRU 3개로 구현하면 왜 안되는지 따져볼 수 있다. 논문에서는 그것도 구현해봤는데 비교할 가치를 느끼지 못할 정도로 제대로 동작하지 않았다고 한다.&lt;/p&gt;
&lt;h3 id="플래시-친화적인가"&gt;플래시 친화적인가?&lt;/h3&gt;
&lt;p&gt;SSD를 오랫동안 사용할 수 있는지 알아보기 위해 CDN 데이터를 가지고 실험을 해본다. 플래시는 쓰기 횟수의 제한이 있기 때문에 최대한 DRAM을 많이 사용해야 한다. S3-FIFO를 생각해보면 small queue S와 ghost queue G는 메모리에 배치하고 장기기억에 해당하는 main queue M은 SSD에 분할해서 할당해서 플래시 친화성을 높인다.&lt;/p&gt;
&lt;p&gt;Wikimedia &amp;amp; Tencent Photo CDN 데이터를 이용해서 실험을 진행했으며 3개의 비교군을 설정했다. Probabilistic 따로 안찾아봤고 &lt;a href="https://www.usenix.org/conference/nsdi19/presentation/eisenman"&gt;Flashield&lt;/a&gt;&lt;strong&gt;(NSDI’19)&lt;/strong&gt; 는 스탠포드 대학 연구진들이 발표한 연구로 머신 러닝을 이용한다.&lt;/p&gt;
&lt;p&gt;CDN에서는 캐시를 평가할 때 캐시 미스율보다 네트워크 대역폭까지 함께 고려한다. 아래 그림의 Y축은 얼마나 많은 데이터 쓰기가 발생했는지 보여주고 X축은 캐시 미스율을 나타낸다. 그래프의 원점에 가까울수록 좋은 캐시라는 의미이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-15ac979e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-15ac979e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-tjUPPZm2LKYCdo5XAD64WA-03b1b5eb1409.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-15ac979e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-tjUPPZm2LKYCdo5XAD64WA-03b1b5eb1409.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;S3-FIFO가 얼마나 플래시 친화적인지 보여주는 그래프&lt;/p&gt;
&lt;h2 id="성능-평가"&gt;성능 평가&lt;/h2&gt;
&lt;p&gt;마지막으로 캐시의 QPS를 평가해보자 스레드 개수를 높여가면서 처리율이 증가하는지 따져보았다. 킹론상 S3-FIFO는 lock-free queue를 사용하기에 확장성이 높고 다른 LRU기반 캐시들은 락을 잡아서 구현해야 하기에 스레드 개수에 따른 성능 확장은 기대하기 어렵다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;아래 표에서는 뜬금없이 Segcache라는 애가 등장하는데 이것도 논문 1저자가 연구한 캐시의 한 종류이다. 논문 낸 김에 끼워팔아서 이것도 드셔보실래요? 권장하듯이 등장한 거라 별로 큰 의미는 없는 것 같다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c12c5f63"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c12c5f63"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E2zkBMpXcfIrPGrlBRBZ9g-0e141b60450c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c12c5f63"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E2zkBMpXcfIrPGrlBRBZ9g-0e141b60450c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;S3-FIFO 큐의 성능 평가&lt;/p&gt;
&lt;h2 id="sieve-설계-및-구현"&gt;SIEVE 설계 및 구현&lt;/h2&gt;
&lt;p&gt;S3-FIFO가 큐를 세 개나 쓰면서 구현했다면 SIEVE는 한술 더 떠서 FIFO 큐 하나만을 사용해서 구현한다. 9개의 SOTA알고리즘과 비교해서 캐시 미스율을 크게 낮췄고 여기서도 락을 잡지 않아서 16-thread의 LRU캐시보다 2배 이상의 성능을 가진다고 한다.&lt;/p&gt;
&lt;p&gt;SIEVE는 최고의 알고리즘은 아니지만 심플하면서도 강력한 캐시 축출 알고리즘이라고 시장에서 포지셔닝 하고 있다. 아래 표는 캐시 히트, 축출, 삽입연산을 구현하기위해 필요한 코드 라인 수를 비교해서 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6fd64e2a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6fd64e2a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7JWMd5WqiD0PwaYtJtOZxg-c8994b01fb29.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6fd64e2a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7JWMd5WqiD0PwaYtJtOZxg-c8994b01fb29.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;각 알고리즘 별 필요한 코드 구현 라인 수&lt;/p&gt;
&lt;p&gt;뿐만 아니라 실제 오픈소스를 수정해서 SIEVE를 구현하는데 필요한 라인수를 언어별로 보여준다. 테스트 코드를 수정하기 위한 라인 수는 표현하지 않았다. 새로운 캐시들이 계속 연구 결과로 발표되고 있지만 현업에서 실제로 적용되는 경우는 많지 않은데 그 이유를 구현의 복잡해지면 무언가 잘못되었을 때 디버깅 하기가 어렵고 버그가 발생할 가능성이 크기 때문이라고 설명한다. 이 논문에선 알고리즘의 심플함을 많이 강조한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d8a81c0e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d8a81c0e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-etgDYcbG0nbBKX393mtYww-fdd6f633392a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d8a81c0e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-etgDYcbG0nbBKX393mtYww-fdd6f633392a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;오픈소스에 SIEVE를 도입하기 위해 필요한 코드 구현 라인 수&lt;/p&gt;
&lt;h3 id="lazy-promotion--quick-demotion"&gt;Lazy Promotion &amp;amp; Quick Demotion&lt;/h3&gt;
&lt;p&gt;Lazy Promotion은 객체를 캐시에 &lt;strong&gt;‘유지’&lt;/strong&gt;하겠다 라는 판단을 데이터를 제거해야하는 시점에 판단하는 것을 말한다. LRU 캐시는 객체의 접근 빈도수를 업데이트 하면서 객체를 즉시 head로 프로모팅 하는 반면에 이를 최대한 지연 시켜서 프로모션에 필요한 비용을 줄이겠다는 전략이다. Quick Demotion은 위에서 한 번 설명한 내용이라 생략한다. 이 둘이 SIEVE의 주요 전략이다.&lt;/p&gt;
&lt;h3 id="clock"&gt;CLOCK&lt;/h3&gt;
&lt;p&gt;SIEVE는 다른 캐시보다 CLOCK 알고리즘과의 비교가 매우 중요해보인다. 둘의 구현이 정말 종이 한장 차이로 비슷하기 때문이다. CLOCK 알고리즘은 객체에게 두 번째 기회를 줘서 다시 큐의 앞 부분으로 이동시키는 캐시 알고리즘으로 이미 &lt;a href="https://www.multicians.org/paging-experiment.pdf"&gt;1969년에 논문&lt;/a&gt;으로 출간되었다.&lt;/p&gt;
&lt;h3 id="구현-implementation"&gt;구현 (Implementation)&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b4eaa3c3"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b4eaa3c3"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Iwm6cQm7EtNshQjcLbDezA-a844143a76fa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b4eaa3c3"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Iwm6cQm7EtNshQjcLbDezA-a844143a76fa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;SIEVE와 CLOCK 구현체의 비교&lt;/p&gt;
&lt;p&gt;위 그림은 SIEVE 알고리즘의 구현 원리와 함께 비교를 위해 FIFO-Reinsertion도 같이 보여준다. 사실 FIFO-Reinsertion과 CLOCK이 같은 알고리즘이기 때문에 CLOCK과 비교한다고 받아들이는게 좋다.&lt;/p&gt;
&lt;p&gt;SIEVE는 하나의 큐와 hand 라는 특수한 포인터 변수를 함께 사용한다. 캐시 히트가 발생하는 경우 객체의 visited 변수를 1로 변경하는 역할만 하고 아무것도 하지 않는다. 캐시 미스의 경우 hand포인터가 head로 향하는데 방문하는 모든 객체의 visited 변수를 0으로 바꾸고, 만약 이미 0이라면 해당 객체를 캐시에서 제거한다. 아래는 SIEVE의 의사코드를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b53ae76f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b53ae76f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rJDjhFBpsKlwBIg4DmtLXA-b4393daa0d5f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b53ae76f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rJDjhFBpsKlwBIg4DmtLXA-b4393daa0d5f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;SIEVE 알고리즘의 의사코드&lt;/p&gt;
&lt;h2 id="평가"&gt;평가&lt;/h2&gt;
&lt;p&gt;데이터 셋은 트위터와 메타의 데이터 셋을 포함해 총 7개의 데이터 소스를 이용했으며 총 1559개의 트레이스 데이터를 가지고 평가를 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-86b35483"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-86b35483"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WF2DGAx5DL6JBL7zWC0Ilw-2eee07e24522.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-86b35483"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WF2DGAx5DL6JBL7zWC0Ilw-2eee07e24522.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;1559개의 트레이스를 이용한 평과 결과&lt;/p&gt;
&lt;p&gt;캐시 크기가 큰 경우 SIEVE는 거의 모든 분야에서 개선된 모습을 보여준다. SIEVE는 10%의 해당하는 트레이스에 대해 최대 42% 캐시 미스 감소율을 보였고 CDN1에 대해서 평균적으로 21%의 캐시 미스 감소율을 보여준다. 다른 알고리즘과 비교해서는 ARC보다 1.5% 감소를 보여주는데 논문에서는 특별히 1.5%를 감소시킨다는 건 굉장히 큰 결과라고 한 번 강조했다.&lt;/p&gt;
&lt;p&gt;반면, 캐시 크기가 작을 경우 다른 알고리즘과 비교해 높은 성능 개선은 보여주지 못했는데 그 이유는 이 알고리즘이 &lt;strong&gt;탐색-저항(scan-resistant)&lt;/strong&gt;하지 않기 때문이다. 이는 아래에서 좀 더 설명한다. 위 그래프에서 특히 눈 여겨볼 점은 알고리즘의 복잡성을 나타내는 코드 라인 수 표와 비교했을 때 적은 노력으로 큰 효과를 낼 수 있다는 점과, 비슷한 알고리즘인 CLOCK과 비교했을 때 뚜렷한 성능개선을 보여준다는 걸 주의 깊게 보면 좋을 것 같다.&lt;/p&gt;
&lt;h3 id="왜-clock과-비교해서-더-효율성이-좋은가"&gt;왜 CLOCK과 비교해서 더 효율성이 좋은가?&lt;/h3&gt;
&lt;p&gt;이 부분은 논문의 5장. Visualizing the shifting process 에 그림과 함께 자세히 설명하고 있다. 필자가 이해한 것을 바탕으로 후려쳐서 설명해보자면 앞서 캐시 정책의 두 핵심이 Lazy Promotion과 Quick Demotion이라고 설명했다.&lt;/p&gt;
&lt;p&gt;CLOCK의 경우 tail 포인터에서 축출한 대상에게 한 번 더 기회를 주는 느낌으로 head로 옮기는 반면 SIEVE는 hand 포인터가 “너 다음에 두고보자” 라고 언지를 두면서 head로 포인터가 움직인다. 즉 다시 한 번 기회를 받은 객체들은 평균적으로 큐의 뒷 부분에 위치하게 되고 새로운 객체들은 head 쪽에 위치하게 된다. hand 포인터가 점점 head 쪽으로 가까워지기 때문에 캐시 미스가 발생할 경우 축출 대상을 탐색할 때 &lt;strong&gt;새로운 객체&lt;/strong&gt;를 위주로 탐색하기 때문에 Quick Demotion이 제대로 작동하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-df6714ab"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-df6714ab"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s-CEtILbnlJ1Koa12STn0w-129015709b2b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-df6714ab"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-s-CEtILbnlJ1Koa12STn0w-129015709b2b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;SIEVE의 sifting 과정을 묘사한 그림&lt;/p&gt;
&lt;h3 id="cache-primitives-로서의-가능성"&gt;Cache Primitives 로서의 가능성&lt;/h3&gt;
&lt;p&gt;여기서 또 하나 강조하는 점이 SIEVE가 좀 더 복잡하고 고급진 캐시 정책을 설계할 때 Cache Primitive로서 역할을 할 수 있다는 것이다. 대부분의 고도화된 캐시 정책들이 FIFO, LRU, LFU를 핵심 재료로 사용하면서 성능을 높이고 있는데 SIEVE도 이런 핵심 재료로 쓰일 수 있다고 주장한다. 예를 들어 ARC 알고리즘에서 LRU 컴포넌트를 SIEVE로 교체하면 최대 62.5%, 평균 3.7%의 개선이 있다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ead7a9da"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ead7a9da"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Xmojh3M2mPhOnRulAOWOzA-4939aa64df45.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ead7a9da"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Xmojh3M2mPhOnRulAOWOzA-4939aa64df45.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;SIEVE / Cache primitives 로서의 가능성을 보여주는 그래프&lt;/p&gt;
&lt;h3 id="scan-resistant"&gt;Scan-Resistant&lt;/h3&gt;
&lt;p&gt;SIEVE는 일부러 메타와 트위터에서 공개한 &lt;strong&gt;웹 캐시 워크로드&lt;/strong&gt; 데이터셋을 중심으로 실험을 진행했다. 그 이유는 SIEVE가 scan-resistant 하지 않기 때문에 범위 쿼리를 많이 포함하는 Block I/O 데이터 셋에 취약하기 때문이다.&lt;/p&gt;
&lt;p&gt;scan-resistant란 범위로 탐색하는 쿼리가 주어질 때 캐시가 여전히 좋은가를 말한다. 범위 탐색을 하는 경우 대량의 객체들이 캐시로 후다닥 들어올텐데 이 과정에서 SIEVE의 캐시 크기가 너무 작으면 그 특징을 제대로 써먹지도 못하고 객체가 빠르게 축출된다. 이런 이유로 LRU또한 범위 쿼리가 들어오면 그 만큼 정보를 업데이트 해야 하기 때문에 scan-resistant 하지 않다고 본다.&lt;/p&gt;
&lt;p&gt;반면 S3-FIFO는 원-히트-원더를 제거하겠다는 목표가 scan-resistant의 목표와 비슷한데다 대 부분이 단기기억에 해당하는 small queue S에서 제거되고 장기기억인 main queue M의 영향성이 적기 때문에 scan-resistant한 캐시 정책이라고 평가하고 있다.&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;오늘은 2023년 SOSP에서 소개된 S3-FIFO와, 2024년 NSDI에서 발표될 SIEVE 이 두 개의 논문을 다루었다. 원래는 S3-FIFO만을 다루려고 했는데 관련 자료를 계속 탐색하다 보니 글이 길어졌다. 분량은 다소 길었지만 내용이 어렵지 않아서 소화하는데 문제는 없을 것이다.&lt;/p&gt;
&lt;p&gt;그리고 필자는 캐시 연구 분야를 이 두 논문을 통해서 처음 접했기 때문에 논문 저자들이 선택한 데이터가 체리픽 되어 있는지 혹은 다른 곳에서 발표된 더 좋은 성과를 낸 연구가 있는데 관련 연구에 포함이 안되어 있는건지 등을 걸러낼 시야가 부족하다. 그래도 논문의 저자가 &lt;a href="https://x.com/1a1a11a/status/1683925976248451072?s=20"&gt;구글, VMWare를 포함한 여러 기업들에서 가능성을 평가하고 있다고 하니&lt;/a&gt; 한 번 지켜 볼만 하다.&lt;/p&gt;
&lt;p&gt;마지막으로 필자도 이 연구들을 직접 평가해보고자 go언어로 캐시 라이브러리를 작성 및 평가를 진행해봤다. 바로 &lt;a href="https://github.com/scalalang2/golang-fifo"&gt;여기&lt;/a&gt;에서 구현체를 확인할 수 있다. 실제 데이터가 아니라서 그런지 몰라도 S3-FIFO가 TinyLFU에 비해 미세하게 더 효율성이 떨어졌다. 오히려 마지막에 설명한 SIEVE가 이 둘을 미세하게 능가했으며 멀티-쓰레드 환경에서 더 높은 성능을 보여주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4a426d3b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4a426d3b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3X3z7RuxLFaUi3UcVce1Cg-4e26f7f7723e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4a426d3b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-3X3z7RuxLFaUi3UcVce1Cg-4e26f7f7723e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://x.com/richardartoul/status/1748897092221751582?s=20"&gt;https://x.com/richardartoul/status/1748897092221751582?s=20&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;한 번 만들어두니까 많은 사람들이 평가해주는 모습이 너무 신기하다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3600006.3613147"&gt;FIFO queues are all you need for cache eviction | SOSP 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://junchengyang.com/publication/nsdi24-SIEVE.pdf"&gt;SIEVE is Simpler than LRU: an Efficient Turn-Key Eviction Algorithm for Web Caches&lt;/a&gt; | NSDI 2024&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>멀티-플레이어 게임 서버와 레이턴시 보상 테크닉</title><link>https://scalalang2.com/p/%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B2%8C%EC%9E%84-%EC%84%9C%EB%B2%84-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%9A%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A0%88%EC%9D%B4%ED%84%B4%EC%8B%9C-%EB%B3%B4%EC%83%81-%ED%85%8C%ED%81%AC%EB%8B%89-253640eb57d6/</link><pubDate>Mon, 16 Oct 2023 03:12:16 +0000</pubDate><guid>https://scalalang2.com/p/%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B2%8C%EC%9E%84-%EC%84%9C%EB%B2%84-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%9A%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A0%88%EC%9D%B4%ED%84%B4%EC%8B%9C-%EB%B3%B4%EC%83%81-%ED%85%8C%ED%81%AC%EB%8B%89-253640eb57d6/</guid><description>&lt;p&gt;멀티-플레이어 게임 서버는 높은 레이턴시 환경에서 일관성과 반응성이 떨어지기 마련인데 이러한 부정적 경험을 숨기기 위한 여러 보상 테크닉들이 존재한다. 오늘은 레이턴시 보상 연관 논문 80여개를 정리하고 분류 체계를 만든 서베이 논문 한 편을 소개한다. 이 논문은 레이턴시 보상 분야의 기존 연구들의 성과와 발전 방향을 정리했다. 이런 잘 쓰여진 서베이 논문 한 편 딱 읽어두면, 나중에 다른 논문을 읽을 때 도움이 된다.&lt;/p&gt;
&lt;p&gt;이 글이 게임업계의 서버 프로그래머 분들과 게임업계를 꿈꾸는 학생분들 그리고 게임에 관심있는 다른 분야의 서버 프로그래머들에게 흥미로운 내용이 되길 기대한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3519023"&gt;&lt;em&gt;A Survey and Taxonomy of Latency Compensation Techniques for Network Computer Games&lt;/em&gt;&lt;/a&gt;* — ACM Survey 2022에 기고된 논문으로 저자인* &lt;a href="https://www.linkedin.com/in/shengmei-liu-b81734117/"&gt;&lt;em&gt;SHENGMEI LIU&lt;/em&gt;&lt;/a&gt;&lt;em&gt;는 우스터 폴리테크닉 대학에서 박사과정을 마치고 현재 구글의 소프트웨어 엔지니어로 재직중이다.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(옮긴이) 국내 어느 한 게임회사의 월급루팡으로 일하고 있다.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;게임 서버 네트워크와 동기화&lt;/li&gt;
&lt;li&gt;레이턴시 보상 (Latency Compensation)&lt;/li&gt;
&lt;li&gt;피드백 (Feedback)&lt;/li&gt;
&lt;li&gt;예측 (Prediction)&lt;/li&gt;
&lt;li&gt;시간 조작 (Time Manipulation)&lt;/li&gt;
&lt;li&gt;월드 조정 (World Adjustment)&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="게임-서버-네트워크와-동기화"&gt;게임 서버 네트워크와 동기화&lt;/h2&gt;
&lt;p&gt;게임 네트워크의 토폴로지는 크게 4가지로 구분할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Peer-to-Peer&lt;/strong&gt; : 서버 없이 플레이어들이 직접 통신하는 구조이다. 게임 이외에 별도 서버가 필요없어서 수업 시간에 선생님이 인터넷을 끊어도 스타크래프트를 할 수 있다는 장점이 있다. 하지만 게임의 전체 상태를 동기화 하고 입력값을 검증하는 서버가 없어서 치팅(e.g. 핵)에 취약하고 성능이 낮은 플에이어에게 전체 플레이 성능이 바운드된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dedicated Game Server (DGS)&lt;/strong&gt; : 데디케이트 서버란 게임 로직 전체 혹은 일부가 원격지 전담 서버로 호스팅 되고 클라이언트는 데디케이트 서버에 붙어서 게임 한 판을 진행한다. 클라이언트-서버 구조로 통신하게 된다. 배틀그라운드에서는 매칭이 완료되면 수십명의 사람이 외딴 섬에 낙하산을 타고 내려와 생존 게임이 시작되는데 &lt;a href="https://bbs.pubg.game.daum.net/gaia/do/pubg/gm/read?articleId=56&amp;amp;bbsId=PN002&amp;amp;objCate1=253&amp;amp;pageIndex=1"&gt;이 때 데디케이트 서버가 이번 판의 게임 상태 동기화를 책임진다&lt;/a&gt;. 매 판마다 플레이를 책임지는 서버가 분리되어 있어서 한 서버의 문제가 다른 게임을 하고 있는 플레이어에게 전파되지 않는다. 쾌적한 게임 플레이를 위해서는 메모리, CPU, 네트워크 대역폭 이 세가지 요소를 모두 풍부하게 제공해야한다. 그래서 보통 데디케이트 서버를 제공해야하는 온라인 게임 서비스들은 서버 비용이 억 소리나게 나간다. &lt;a href="https://www.fortnite.com/news/postmortem-of-service-outage-at-3-4m-ccu"&gt;에픽게임즈의 포트나이트는 3400만명 동접을 수용하기 위해 수천대의 AWS c4.8xlarge 인스턴스를 운영하고 있다고 밝힌 바 있다.&lt;/a&gt; 대략 2500대를 운영하고 있다고 가정하면 데디케이트 서버 혼자서만 한달에 30억씩 비용이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c93f5a3a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c93f5a3a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YnKNqDJk4s9UiVygyfuy8A-9d0ed268ef08.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c93f5a3a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-YnKNqDJk4s9UiVygyfuy8A-9d0ed268ef08.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 1. 게임:배틀그라운드 — 게임 시작과 함께 플레이어들이 낙하산을 타고 포인트로 하강하는 모습]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client Server Model&lt;/strong&gt; : 클라이언트-서버 모델로 서버 하나가 동기화를 책임지는 모델을 말한다. 데디케이트 서버랑 구분을 둔 이유는 MMORPG처럼 판수 개념이 없는 실시간 온라인 게임에서 필요한 서버 형태랑은 개념이 미묘하게 다르다고 생각했기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Listen Server (Client-Hosted Model)&lt;/strong&gt; : 클라이언트-서버 모델이지만 게임 플레이어 중 하나를 호스트로 선정해서 해당 유저가 서버 역할까지 책임지면서 플레이 하는 방식을 말한다. 개발자 입장에서는 서버 유지비를 아낄 수 있다. 경쟁이 중요한 요소가 아니라면 이 모델을 고려해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;보통은 네 가지 모델 중에서 한 가지 정도를 채택하지만, 만드는 게임의 요구사항에 따라 적절히 혼합해서 사용할 수 있다. 넥슨게임즈는 언리얼 엔진에서 Dedicated Server와 Listen Server를 혼합해서 사용한 SPICA를 NDC에서 두 번의 발표를 통해 소개헀다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/DCA-b-9j08o" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/aulqNFU3gjo?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h3 id="게임-서버-동기화-synchronization"&gt;게임 서버 동기화 (Synchronization)&lt;/h3&gt;
&lt;p&gt;멀티-플레이어 게임에서 나와 내 친구 혹은 떨어져 있는 누군가와 같은 게임을 플레이 하고 있다는 걸 보여주려면 매 순간 게임 상태가 동기화 되어야 한다. FPS 게임에서 나는 엄폐물로 이동해서 숨어있는데 동기화 시간 차이로 적한테는 신체 부위가 노출된 상태라면 원활한 게임 자체가 불가능하다. 게임 업계에서는 이런 상태 동기화 이슈를 흔히 &lt;strong&gt;NETCODE&lt;/strong&gt; 라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-055c1d55"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-055c1d55"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-opbT42DCjuqtvQIGwZTRgw-279e311585ad.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-055c1d55"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-opbT42DCjuqtvQIGwZTRgw-279e311585ad.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 2. &lt;a href="https://technology.riotgames.com/news/peeking-valorants-netcode"&gt;PEEKING INTO VALORANT’S NETCODE | 두 플레이어의 시야가 서로 다른 상황&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;게임 서버는 NETCODE 이슈를 줄이기 위해 적절한 동기화 전략을 채택해야 한다. &lt;a href="https://www.youtube.com/watch?v=j3eQNm-Wk04"&gt;넥슨 개발자 컨퍼런스의 발표&lt;/a&gt;에서 발췌한 내용을 토대로 게임 서버의 동기화란 무엇인지 어떤 방식으로 동작하는지 가장 기본적인 원형을 소개한다. &lt;strong&gt;다음 장에서부터 본격적으로 논문 내용을 다루면서 레이턴시로 인한 동기화 이슈를 해결하는 방법에 대해 다루려고한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="-lock-step-protocol"&gt;① Lock-Step Protocol&lt;/h3&gt;
&lt;p&gt;Lock-Step 방식은 가장 단순한 게임 동기화 모델이면서 동시에 안티 치팅 프로토콜이다. P2P 네트워크에서는 상대방이 이번 프레임에 어떤 행동을 했는지 먼저 보고 나의 행동을 결정하면서 치팅할 수 있는데 이를 lock-ahead cheating 이라고 부른다. 주식 시장에서 선행 매매(Front-Running)와 비슷한 원리라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;Lock-step Protocol에서는 각 행위에 대한 커미트먼트(e.g. hash)를 먼저 전파하고 이를 동의하면 모든 플레이어 행위가 공개되고 동기화를 진행한다. lock-ahead cheating 문제의 부분적인 해법을 제공하지만 가장 레이턴시가 느린 플레이어에게 게임의 품질이 바운드 되며 게임에 참여할 수 있는 인원이 제한된다. 턴제 게임에서는 체감하기 어렵지만 높은 반응성이 요구되는 FPS 게임에서 Lock-step으로 구현하면 플레이 경험이 감소할 것이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Lock-step protocol은 설명하는 자료마다 정의하는 방식이 조금 다르다. &lt;a href="https://m.blog.naver.com/linegamedev/221061964789"&gt;안티치팅 기능만 버리고 각 라운드별로 동기식으로 동기화 한다는 의미로 Lock-step synchronization 이라는 용어를 따로 분리해서 설명하기도 한다(출처: 라인 기술 블로그)&lt;/a&gt;. 일단 이 글에서는 위키백과에 등재된 단어를 더 근본으로 쳐서 소개했다. 동일한 입력값에 대해 상태 전이가 결정적으로 동작하기 때문에 Deterministic Lock-step 이라고 부르기도 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-370e1fc2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-370e1fc2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1MgQFE71M0zoZvV5AGLI8A-3f4b9d1d0e0b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-370e1fc2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1MgQFE71M0zoZvV5AGLI8A-3f4b9d1d0e0b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 3. Lock-Step Protocol의 동기화 모델]&lt;/p&gt;
&lt;p&gt;대표적인 예시로 국민 민속놀이 스타크래프트가 있는데 4:4 빠른 무한의 후반부가 되면 1000개 이상의 오브젝트 상태를 전송할 수 있는 대역폭이 필요하다. 스타크래프트가 탄생한 시점과 그 당시 컴퓨터의 사양을 생각해보면 아마 클라이언트 상태 커미트머트만 매 라운드마다 합의하는 방식을 이용했을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-43b850b7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-43b850b7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CZNsCTWM86t60s1pNBwRyA-f709460c5b9c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-43b850b7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CZNsCTWM86t60s1pNBwRyA-f709460c5b9c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 4. Lock-step 방식의 동기화 모델에서 플레이어의 지연이 전체 플레이어의 블럭킹으로 이어진다.]&lt;/p&gt;
&lt;p&gt;Lock-step 방식으로 동기화 하는 모델에서는 플레이어 한명의 지연이 모든 플레이어에게 영향을 준다. 스타크래프트에선 한 명의 네트워크 이슈가 발생하면 45초 동안 기다렸다가 플레이어의 추방 투표가 진행됟다.&lt;/p&gt;
&lt;h3 id="-서버-허가형-모델server-authoritative"&gt;② 서버 허가형 모델(Server Authoritative)&lt;/h3&gt;
&lt;p&gt;Lock-step 방식은 클라이언트가 직접 이벤트를 받아서 직접 렌더링 하는 방식인 반면, 서버 허가형 모델은 서버가 게임 렌더링에 필요한 월드 상태 로직을 직접 가지고 있으면서 클라이언트에게 사물의 위치나 상태 등을 방송하는 방식을 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c002968c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c002968c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m0RcosVQnTTd5V5XCNjFHg-b22c4395266d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c002968c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m0RcosVQnTTd5V5XCNjFHg-b22c4395266d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 5. 서버 허가형 모델(Server Authoritative)]&lt;/p&gt;
&lt;p&gt;서버가 먼저 게임의 상태를 전이한 뒤, 클라이언트가 서버의 상태를 뒤따라가면서 렌더링 하는 방식이다. 클라이언트는 항상 서버의 과거 상태를 뒤늦게 반영하게 된다. 서버는 클라이언트의 입력을 받을 때 입력값에 대한 검증 및 상태를 업데이트 한다. 만약 클라이언트가 갑자기 순간이동 한다거나 게임 내에서 일어날 수 없는 행위를 하면 서버에서 해당 이벤트를 거부할 수 있다. 아마 이런 특징 때문에 서버 허가형 모델이라고 불리는 듯 하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tick Rate&lt;/strong&gt;는 게임 서버의 방송 주기이다. 만약 16.66ms 주기로 클라이언트로 방송한다면 게임 서버는 1초에 60프레임을 계산하는 상황이고 이때 Tick rate는 헤르츠(Hz) 단위로 표현해서 60Hz 라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-52cc392c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-52cc392c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-A_yZmFNmFK9adGFuQXtxwA-4ad70eff3f3f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-52cc392c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-A_yZmFNmFK9adGFuQXtxwA-4ad70eff3f3f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 6. &lt;a href="https://docs-multiplayer.unity3d.com/netcode/1.0.0/reference/glossary/ticks-and-update-rates/"&gt;Unity | Ticks and update rates&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;Tick Rate는 게임의 종류마다 다르게 선택할 수 있다. FPS 장르를 생각해보자. Tick Rate 10Hz 게임은 1분에 약 600 프레임을 계산한다. 만약 1분에 750발을 발사할 수 있는 총기가 있다고 했을 때, 어떤 특정 한 프레임에서 플레이어는 상태가 반영되는 시점에 이상하게 다른 프레임보다 데미지가 더 높게 발생하는 현상을 겪게 된다. 플레이어의 행위가 턴마다 제한되는 턴제 게임에서는 Tick Rate를 여유롭게 선택할 수 있다.&lt;/p&gt;
&lt;p&gt;넥슨의 FPS 신작 베일드 엑스퍼트(VX)는 경쟁전에 한해 100Hz라는 높은 틱레이트를 제공하고 이미 성공해 버려서 돈을 많이 버는 라이엇 게임즈의 발로란트는 128Hz를 제공한다. 컴퓨터의 그래픽 카드나 모니터의 주사율이 낮으면 이런 성능 좋은 서버의 혜택을 제대로 못받을 수 있다. 친구들과 만남의 장인 PC방을 가면 높은 주사율의 모니터를 제공하는 FPS전용 좌석이 따로 마련되어 있는 걸 볼 수 있다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 게임 서버의 네트워크 종류와 동기화의 가장 기본적인 두 가지 모델에 대해서 다루었다. 다음 장 부터 본격적으로 레이턴시가 게임에 미치는 영향과 함께 이를 숨길 수 있는 레이턴시 보상 기술들에 대해 알아보자.&lt;/p&gt;
&lt;h2 id="레이턴시-보상-latency-compensation"&gt;레이턴시 보상 (Latency Compensation)&lt;/h2&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-8fcbb185"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-8fcbb185"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-6iZN1biWAaxkj8MxA6Hnpg-364681290eaa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-8fcbb185"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-6iZN1biWAaxkj8MxA6Hnpg-364681290eaa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 7. 출처 : 논문에서 발췌한 반응성과 일관성에 대한 그림]&lt;/p&gt;
&lt;p&gt;레이턴시는 게임 플레이의 &lt;strong&gt;반응성(Responsiveness)&lt;/strong&gt;과 &lt;strong&gt;일관성(Consistency)&lt;/strong&gt;에 영향을 미친다. 그림의 (a) 예시에서는 플레이어가 입력을 전송하고 서버가 상태를 업데이트 하고 전달하는데 나의 행위와 상태 전이 사이에 시간이 존재한다. 이 시간이 클 수록 반응성은 떨어지고 프레임 드랍이 발생한다. (b예시는 일관성의 문제를 보여준다. 클라이언트 A는 이미 월드 상태를 업데이트 했는데 클라이언트 B로 가는 통신이 지연이 발생하면 서로 바라보고 있는 대상의 위치가 달라지게 된다. 플레이어의 위치가 중요한 경쟁 게임에서는 이런 작은 불일치가 플레이 경험에 크게 영향을 미친다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e0aa8f6e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e0aa8f6e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-z3uvtTySVSGoAJKtleW1BA-2bd4949c2f79.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e0aa8f6e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-z3uvtTySVSGoAJKtleW1BA-2bd4949c2f79.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 8. 반응성과 일관성의 트레이드-오프 관계]&lt;/p&gt;
&lt;p&gt;일관성과 반응성은 일종의 트레이드-오프 관계가 형성된다. 그림에서 좌표 (0,0)인 원점이 가장 좋은 상태인데 네트워크 지연이 존재하는 한 사실상 도달할 수 없는 경지이다. 여기서 일관성을 지키려고 하면 반응성을 훼손하게 되고 반응성을 지키고자 하면 일관성이 훼손된다.&lt;/p&gt;
&lt;p&gt;두 특징 사이의 의사결정은 게임 플레이의 종류에 따라 다르다. 광활한 맵에서 캐릭터를 다른 지역으로 이동하는 행위는 일관성은 조금 부족해도 되지만 트랙위를 달리는 레이싱 게임에서는 높은 일관성이 필요하다. FPS게임에서도 넓은 범위를 타격하는 무기와 좁은 범위를 정밀 타격하는 무기 사이에서 필요한 일관성 수준이 다를 수 있다. (e.g. 오버워치의 윈스턴과 위도우메이커)&lt;/p&gt;
&lt;p&gt;레이턴시 보상 (Latency Compensation)은 레이턴시에 따른 부정적 경험을 줄이기 위해 사용자의 입력이나 게임의 상태값을 조정하는 테크닉을 표현하는 단어이다. 이 논문은 총 80여개의 관련 연구를 조사하고 4가지 그룹의 11개의 테크닉으로 레이턴시 보상을 분류화/체계화 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b287273a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b287273a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TbqNQ-BuQs-I24wBDDmTsg-f8e2c30595b0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b287273a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-TbqNQ-BuQs-I24wBDDmTsg-f8e2c30595b0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 9. Latency Compensation Techniques]&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-11989222"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-11989222"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ZkPWoJV4lrh5UsL-vUZqzA-a3a450c7a650.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-11989222"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ZkPWoJV4lrh5UsL-vUZqzA-a3a450c7a650.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 10. 논문에서 각 분류화된 용어마다 참고한 논문들을 표로 나타낸 것이다.]&lt;/p&gt;
&lt;p&gt;[Fig 9] 에서 초록색으로 칠해진 영역은 클라이언트 코드에서 적용할 수 있는 테크닉을 의미하고 파랑색은 서버에서 사용하는 테크닉을 나머지는 클라이언트/서버 모두에 적용할 수 있는 기법들을 표현한다. 이 포스팅의 나머지 단락에서는 이 분류의 큰 그룹인 피드백, 예측, 시간 조작, 월드 조정에 대해 다루고자 한다.&lt;/p&gt;
&lt;p&gt;논문에서는 각 기법마다 플레이어가 얼마나 태스크를 잘 수행하는지, 게임 플레이의 만족도는 어떻게 변화하였는지 등 영향을 평가한 관련 연구를 포함하고 있다. 이 포스팅에서는 각 테크닉이 무엇인지에 대해서만 소개한다.&lt;/p&gt;
&lt;h2 id="피드백-feedback"&gt;피드백 (Feedback)&lt;/h2&gt;
&lt;p&gt;피드백이란 실제 게임 상태를 변경하지 않고 시각적, 음향적 정보를 이용하면서 레이턴시를 느끼지 않도록 하는 방법이다.&lt;/p&gt;
&lt;h3 id="레이턴시-은폐latency-concealment"&gt;레이턴시 은폐(Latency Concealment)&lt;/h3&gt;
&lt;p&gt;플레이어의 입력에 변경되지 않는 게임 상태를 은폐하는 방법이다. 예를 들면, 레이싱 게임에서 부스터를 쓰면 차량이 빠르게 앞으로 이동해야 하는데 서버가 이동 처리를 하기 전에 먼저 부스터 이펙트나 소리부터 먼저 보여줌으로써 레이턴시에 대한 인식을 숨긴다. FPS에서는 총알이 발사되는 것을 서버가 패킷을 받아보기 이전에 클라이언트에게 먼저 반동을 보여줄 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7a0560b4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7a0560b4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-f5dKK17gFymnadAd4hs4BQ-b418eeb295cf.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7a0560b4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-f5dKK17gFymnadAd4hs4BQ-b418eeb295cf.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 11. 레이턴시 은폐 — Latency Concealment]&lt;/p&gt;
&lt;p&gt;위 그림은 이를 잘 표현해주는데 플레이어 입력 N시점에 시각적 효과를 먼저 보여주고 t2 시간에서 실제 게임 상태가 반영된다. 그리고 &lt;strong&gt;이미지 워핑(image warping)&lt;/strong&gt; 또한 레이턴시 은폐 카테고리에 속한다. 이미지 워핑은 이미지 프로세싱의 한 분야로 원본 이미지를 왜곡시키는 기술이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-31aab122"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-31aab122"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-brp67hSzEYDRoLNJii3t1Q-bbeca4dd0dca.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-31aab122"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-brp67hSzEYDRoLNJii3t1Q-bbeca4dd0dca.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 12. 좌-원본이미지, 우-워핑된 이미지]&lt;/p&gt;
&lt;p&gt;&lt;a href="https://research.nvidia.com/sites/default/files/pubs/2020-07_Post-Render-Warp-with/HPG_2020_Latewarp_AuthorVersion_0.pdf"&gt;NVIDA의 2020년 논문&lt;/a&gt;에서는 이미지 워핑을 통해 FPS게임에서 에임 시점을 자연스럽게 변형하는 방법을 소개한다. 렌더링이 아니라 굳이 이미지 워핑인 이유는 클라우드 게임을 대상으로 작성된 논문이기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b5e47b44"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b5e47b44"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_G3HFNfkRFz0wF_D1IN-BA-0a799fab76f6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b5e47b44"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-_G3HFNfkRFz0wF_D1IN-BA-0a799fab76f6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://research.nvidia.com/sites/default/files/pubs/2020-07_Post-Render-Warp-with/HPG_2020_Latewarp_AuthorVersion_0.pdf"&gt;[Fig 13. Post-Render Warp with Late Input Sampling Improves Aiming Under High Latency Conditions]&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="레이턴시-노출-latency-exposure"&gt;레이턴시 노출 (Latency Exposure)&lt;/h3&gt;
&lt;p&gt;레이턴시 노출은 사용자에게 클라이언트-서버간 레이턴시를 시각정 정보로 제공하는 것을 말한다. 게임의 상하단 모서리를 잘 보면 라운드-트립 시간을 작게 표기해주는 경우가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6ef1817b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6ef1817b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sRujCBqbziTi3nX9Ajg_4w-ec922332b610.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6ef1817b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-sRujCBqbziTi3nX9Ajg_4w-ec922332b610.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 14. 레이턴시 노출]&lt;/p&gt;
&lt;p&gt;이게 뭔가 싶겠지만 놀랍게도 몇몇 연구는 레이턴시를 노출시켜주는게 하나의 좋은 전략이 될 수 있다고 소개한다. 대표적으로 &lt;a href="https://citeseerx.ist.psu.edu/document?repid=rep1&amp;amp;type=pdf&amp;amp;doi=b151dfd6ff2f06f80d0e6547bd1efc019858bd22"&gt;이 연구&lt;/a&gt;에서는 테니스 게임을 할 때 플레이어에게 지연 시간을 노출 시키면 유저가 스스로 볼의 궤적을 예측하면서 대처하기 때문에 더 나은 플레이가 가능하다고도 보고한다.&lt;/p&gt;
&lt;h2 id="예측-prediction"&gt;예측 (Prediction)&lt;/h2&gt;
&lt;p&gt;서버 허가형 모델에서는 서버가 플레이어의 행위를 인가해주고 렌더링된 결과를 클라이언트에게 보고한다. 예측은 서버에게 결과를 받기 전에 클라이언트가 미리 예측해서 먼저 렌더링 하고 나중에 서버의 결과를 받아서 보정하는 방식이다. 예측은 실제 게임 업계에서 정말 자주 쓰이는 기법이고 넥슨 개발자 컨퍼런스(NDC)에서도 자주 소개되었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=HSRo7TAV4T4&amp;amp;t=1156s&amp;amp;ab_channel=NDC"&gt;[NDC 22] 실시간 MMORPG의 플레이 감각을 날카롭게 벼려보자! — 프라시아 전기의 예측과 보정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=a80r9WqnECk&amp;amp;ab_channel=NDC"&gt;[NDC 22] ‘카트라이더: 드리프트’ 머신러닝을 활용한 위치 예측 개선&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예측은 반응성을 높여서 플레이 경험에 긍정적인 영향을 주기도 하지만 유저간 일관성을 깨뜨려 문제가 되기도 한다. 만약 플레이어가 이동을 했는데 서버가 입력을 거부하거나 또는 레이턴시가 너무 큰 상황에서 클라이언트의 세계와 서버의 세계가 너무 다르다면 플레이어의 위치가 강제로 이동되거나 나도 모르게 피격 판정을 받는 등 불쾌한 경험을 주게 된다.&lt;/p&gt;
&lt;h3 id="셀프-예측self-prediction"&gt;셀프 예측(Self-prediction)&lt;/h3&gt;
&lt;p&gt;게임은 그 자체로 게임 엔진 위에서 동작하기 때문에 클라이언트에서 예측기법을 적용하기 굉장히 용이한다. 플레이어의 행위가 입력되면 클라이언트가 먼저 진행하고 서버에게 허가를 구한다. 왠만하면 서버가 허락해주겠지 라는 가정이 있다면 아주 좋은 방법이다. 서버가 너무 자주 플레이어의 입력을 거부하면 셀프-예측은 무용지물이 될 것이다. 이런 특징 때문에 몇몇은 &lt;strong&gt;낙관적 동시성(Optimistic Concurrency)&lt;/strong&gt;에 비유해서 설명하기도 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-908af159"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-908af159"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D8SNzIKokgKN8aoqlwczTQ-b6995ecfdcbb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-908af159"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-D8SNzIKokgKN8aoqlwczTQ-b6995ecfdcbb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 15. Self-prediction]&lt;/p&gt;
&lt;p&gt;[Fig 15]를 보자, t0에서 플레이어의 입력을 받아서 클라이언트에서 먼저 오브젝트를 이동시킨다. 이후 서버로 전송해서 반영된 월드 상태를 받았을 때는 나의 입력이 허가를 받거나, 허가를 거부하거나 둘 중 하나의 상태로 결정된다. 서버의 상태와 나의 상태가 동일하면 새로운 위치가 컨펌되지만 입력이 거부되었다면 서버의 상태로 클라이언트의 상태가 롤백 처리된다.&lt;/p&gt;
&lt;p&gt;입력이 거부된 상태가 아니어도 클라이언트와 서버 사이의 지연이 크면 &lt;strong&gt;러버밴딩(rubber-banding)&lt;/strong&gt;문제가 발생할 수 있는데, 클라이언트의 화면이 항상 게임 서버 상태보다 미래 시점이기 때문에 서버의 상태를 너무 늦게 전달받으면 서버의 과거 상태로 보정되면서 [Fig 16] 처럼 보여진다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-87ae2340"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-87ae2340"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-rbWFAcRD0wT2--lb-084acd546133.gif"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-87ae2340"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-rbWFAcRD0wT2--lb-084acd546133.gif"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 16. &lt;a href="https://www.neogaf.com/threads/since-when-has-lag-been-described-as-rubber-banding.815200/page-2"&gt;Rubber banding&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;완벽한 해결방법은 아니지만, 그래도 여러 연구결과에 따르면 100ms-150ms 사이의 레이턴시 환경에서 유저가 게임 내 태스크를 수행하는데 있어, 셀프-예측 기법을 적용하면 15~20%의 퍼포먼스 향상이 있다고 한다.&lt;/p&gt;
&lt;h3 id="보간법interpolation"&gt;보간법(Interpolation)&lt;/h3&gt;
&lt;p&gt;정보가 적은 상황에서 렌더링 할때 좀 더 자연스럽게 보정하는 방법으로 게임 서버로부터 받은 상태 정보와 바로 이전까지 가지고 있던 상태 정보의 중간 상태를 예측해서 렌더링 하는데 끼워넣는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ac0d4efd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ac0d4efd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5xKgfHicRtbfiTam6tY0nw-05e426332cb8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ac0d4efd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-5xKgfHicRtbfiTam6tY0nw-05e426332cb8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 17. Interpolation]&lt;/p&gt;
&lt;p&gt;서버의 방송주기(Hz)보다 클라이언트의 초당 프레임이 더 많을 때 자연스럽게 표현하기 위해 사용할 수 있다. 예를 들어 서버의 방송주기가 30Hz, 클라이언트의 렌더링 주기가 60Hz인 상황이라면 부족한 정보를 채워서 렌더링 해야한다. 이 때 쓰이는 예측을 보간법(Interpolation)이라고 부른다. 아래 영상을 보면 그 효과를 볼 수 있다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/swI8fb4V2c4?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;[Fig 18. Frame Rate Interpolation]&lt;/p&gt;
&lt;h3 id="추론-extrapolation"&gt;추론 (Extrapolation)&lt;/h3&gt;
&lt;p&gt;추론은 신호가 오지 않는 동안의 행동을 플레이어가 추측해서 상태를 갱신하는 알고리즘이다. [Fig 19] 에서 t0에서 t1로 오브젝트의 상태가 업데이트 되었다면 t2에서 서버로부터 응답을 받지 못했을 때, 클라이언트가 새로운 포지션을 추론해서 렌더링 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-72857097"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-72857097"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nEpDga-iHVBAUdI0N_ya2g-c5b56c4af87c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-72857097"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nEpDga-iHVBAUdI0N_ya2g-c5b56c4af87c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 19. Extrapolation]&lt;/p&gt;
&lt;p&gt;추론은 레이턴시 보상 테크닉 중에서도 오랜시간 동안 많이 연구된 분야여서 게임 업계에서 정말 자주 쓰이는 방식이다. 업계에서는 &lt;strong&gt;데드 레커닝(dead-reckoning)&lt;/strong&gt; 이라는 이름으로 널리 알려져 있다. 데드 레커닝은 오브젝트의 위치, 속도, 중력 가속도 등의 물리값으로 다음 상태를 예측하는 방법이다.&lt;/p&gt;
&lt;p&gt;만약 속도(velocity)값을 알고 있다면 렌더링 시간 차이를 곱해서 현재 위치를 계산할 수 있다. 다른 예로 중력가속도 처럼 상수값의 가속도가 주어지는 환경에서는 가속도 값을 이용해서 현재 위치를 계산할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-45f8fea9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-45f8fea9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j1ABb7Ht-lgNIp8ErycS6Q-b4d8ba5704b8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-45f8fea9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-j1ABb7Ht-lgNIp8ErycS6Q-b4d8ba5704b8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 20. 속도를 이용한 위치 추론]&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-53f92c3e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-53f92c3e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8aepTempQPP9aM-4NlWmyw-042063d9f4dd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-53f92c3e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-8aepTempQPP9aM-4NlWmyw-042063d9f4dd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 21. 가속도가 주어진 환경에서 위치 추론]&lt;/p&gt;
&lt;p&gt;일반적으로 자유낙하 처럼 관성이 많이 적용되는 환경의 물체가 예측이 정확하고, 관성이 적고 플레이어의 개입이 많은 물체일수록 예측이 어려워 진다. 데드레커닝의 정보 처리에 쓰이는 건 게임과 상황마다 난이도가 달라진다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7409fb5e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7409fb5e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e4Bx1x7zrMDC8Cd_8VT6Pg-13f7345bb730.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7409fb5e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-e4Bx1x7zrMDC8Cd_8VT6Pg-13f7345bb730.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 22. 추론 정보의 단순함/복잡성 정도]&lt;/p&gt;
&lt;p&gt;물리법칙이 적용되서 간단하게 연산할 수있는건 정확도가 매우 높고 알고리즘이 단순하다. 만약, 오브젝트가 목적지 까지 이동하는 경로에 장애물이나 존재하거나 다른 환경적 요소가 존재하면 더 많은 프로세싱이 필요하고 AI가 컨트롤 하는 NPC의 위치를 잡는다거나 머신러닝이 들어가면 추론에 쓰이는 연산이 복잡해지는 특징을 가지고 있다. 어떤 연구에서는 주변 환경과 플레이어가 관심을 가질만한 주변 사물을 가지고 행위를 예측하는 방법을 제안하기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0d374084"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0d374084"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-VxaBwMe9qE_2CjvQ-e24d0325ee41.jpg"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0d374084"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-VxaBwMe9qE_2CjvQ-e24d0325ee41.jpg"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 23. &lt;a href="https://v.daum.net/v/20171208092002343"&gt;배틀그라운드의 보급상자, 죽어도 봐야지&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;일반적으로 데드레커닝은 (a) &lt;strong&gt;반응성&lt;/strong&gt;을 높이고, (b) 클라이언트의 추론만 정확하다면 &lt;strong&gt;네트워크 통신량&lt;/strong&gt;을 줄이는데 쓰이고 있다.&lt;/p&gt;
&lt;h3 id="투기적-실행speculative-execution"&gt;투기적 실행(Speculative Execution)&lt;/h3&gt;
&lt;p&gt;특정 상황에서 플레이어가 취할 수 있는 행위가 제한적이라면 해당 행위가 발현되었을 때의 상태를 미리 계산해두고 플레이어의 행위에 따라 서버를 기다리지 않고 렌더링을 즉시 해줄수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b0b57e3d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b0b57e3d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ifnbdnVI-OpOReYdThgziQ-f8763f640073.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b0b57e3d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ifnbdnVI-OpOReYdThgziQ-f8763f640073.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 24. Speculative Execution]&lt;/p&gt;
&lt;p&gt;[Fig 24]를 보면 초록색 공은 플레이어의 입력에 따라 이동할 수 있는 공간이 두 가지 밖에 없다. 미리 계산된 두 가지 상태 중 플레이어의 입력에 따라 값을 반영하고 채택되지 않은 상태 값은 버린다. 둠과 퀘이크 시리즈로 유명한 이드 소프트웨어는 게임의 상태값 대신 입력에 대한 오브젝트의 모션 벡터를 클라이언트에게 전송하는 방법으로 레이턴시를 줄이는 &lt;a href="http://Systems%20and%20methods%20for%20player%20input%20motion%20compensation%20by%20anticipating%20motion%20vectors%20and/or%20caching%20repetitive%20motion%20vectors"&gt;특허&lt;/a&gt;를 보유하고 있다. (e.g. 한 프레임에 다수의 입력을 동시에 누르는 경우 벡터 연산으로 다음 프레임에 렌더링할 값을 만들 수 있다)&lt;/p&gt;
&lt;h2 id="시간-조작-time-manipulation"&gt;시간 조작 (Time Manipulation)&lt;/h2&gt;
&lt;p&gt;게임 월드의 시간을 되돌려서 평가하는 방법이다. 이전 장에서 다룬 예측 기술은 반응성을 높이는 반면 &lt;strong&gt;시간 조작은 반응성을 조금 양보하고 일관성을 높이는 기술이다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="time-warp"&gt;Time Warp&lt;/h3&gt;
&lt;p&gt;서버의 입장에서 플레이어의 이벤트가 도달했을 때 월드의 시간을 과거로 잠깐 되감기 해서 플레이어의 행위를 검증하는 방법이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-478fe7b2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-478fe7b2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-J9WrJI-ZSq_szEaN-3f41201e5070.jpg"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-478fe7b2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-J9WrJI-ZSq_szEaN-3f41201e5070.jpg"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 25. &lt;a href="https://developer.valvesoftware.com/wiki/Lag_Compensation"&gt;Valve Developer Community | Lag Compensation&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;논문에서는 타임 워프라는 용어를 썻지만 업계마다, 그리고 회사마다 부르는 용어가 조금씩은 다르다. [Fig 25]는 밸브 개발자 포럼의 위키에 등재된 렉 보정에 실린 그림이다. FPS 게임에서 플레이어가 사격을 했는데 서버의 입장에서는 표적과 거리가 조금 있는 상황이다.&lt;/p&gt;
&lt;p&gt;서버와 클라이언트 간의 레이턴시가 존재하기 때문에 클라이언트가 인식한 세상과 서버가 인식하는 세상이 다를 수 밖에 없기 때문에 서버가 클라이언트의 입력이 발생한 시간까지 되돌려보고 사격이 발생한 시점과 물체의 위치가 일치하면 피격 판정을 내린다. 하지만 이 방식은 &lt;strong&gt;shot around the corner&lt;/strong&gt; 딜레마를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-567be4d8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-567be4d8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-o-BRFATmnNjbjf-XKIWVHA-5e0be45aabda.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-567be4d8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-o-BRFATmnNjbjf-XKIWVHA-5e0be45aabda.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 26. Time warp]&lt;/p&gt;
&lt;p&gt;[Fig 26]의 오른쪽 그림을 보면 파랑색 플레이어가 t1 시간에 초록색 플레이어를 사격한 상황이다. t2 시간에서 서버가 입력값을 받아봤을 때는 이미 초록색 플레이어가 안전 구역으로 이동한 상황이다. 이 때 타임워프를 적용하면 초록색 플레이어는 피격되는데 마치 난 이미 숨었는데 피격당한 것 같은 연출이 일어난다. 즉 공격자에게 어드밴티지를 줄 것인지 반대로 피해자에게 어드밴티지를 줄 것인지의 문제인데 보통은 조작감을 위해서 공격자에게 어드밴티지를 준다.&lt;/p&gt;
&lt;h3 id="outgoing-time-delay"&gt;Outgoing Time Delay&lt;/h3&gt;
&lt;p&gt;서버가 의도적으로 클라이언트 마다 적용되는 방송 주기에 버퍼를 줘서 레이턴시를 균일하게 만들어 게임의 공정성과 일관성을 높이는 방식을 사용할 수 있다. 예를 들어 L_i where (1 ≤ i ≤ N) 를 i번째 클라이언트의 레이턴시라고 하고 D=max(L_i)라고 했을때 각 클라이언트 마다 D-L_i 만큼 버퍼를 두면 모든 플레이어가 서버와의 통신 주기가 균일하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-c301c117"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-c301c117"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NDWVTQkZp0U5qA18JngW1Q-48a50a63d3b2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-c301c117"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NDWVTQkZp0U5qA18JngW1Q-48a50a63d3b2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 28. Outgoing Time Delay]&lt;/p&gt;
&lt;p&gt;NDC 2022년 프라시아 전기 팀의 발표를 보면 Outgoing Time Delay로 정지 명령이 발생한 물체의 위치의 정확성을 높이는 사례가 나온다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/HSRo7TAV4T4?start=1184&amp;feature=oembed&amp;start=1184" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h3 id="incoming-time-delay"&gt;Incoming Time Delay&lt;/h3&gt;
&lt;p&gt;Outgoing Time Delay와 목적은 비슷하다. Outgoing 방식은 서버가 메시지를 보낼 때 버퍼를 두는 반면, Incoming Time Delay는 클라이언트가 스스로 발생한 행위를 지연시켜서 모든 클라이언트에게 입력이 도달한 후에 동시에 상태를 갱신하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7131208f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7131208f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QAUJJZ5QO_E26hMZcN91Kw-d6b7c40d26c4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7131208f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-QAUJJZ5QO_E26hMZcN91Kw-d6b7c40d26c4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 29. Incomign Delay]&lt;/p&gt;
&lt;p&gt;Time Delay는 모두 반응성을 조금 양보하는 대신 일관성을 높여서 게임의 공정성을 더해준다. 500ms 레이턴시 까지 발생하는 게임 환경에서 상태 불일치 문제를 해결할 수 있다고 한다. 비록 반응성과 일관성 사이의 트레이드 오프가 있긴 했지만 QoE(Quality of Experience)를 조금만 희생해도 250ms 의 네트워크 환경에서 30% 정도의 성능 향상이 있다고 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://dl.acm.org/doi/10.1145/1460563.1460635"&gt;관련 연구&lt;/a&gt; — 게임과 동시편집은 멀티-플레이어 환경에서 일정 태스크를 수행한다는 점에서 일부 교집합이 존재한다. 따라서 논문에서 소개된 결과와 관련 연구 중에서 그룹웨어에 적용되는 내용이 일부 보인다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="롤백-넷코드-rollback-netcode"&gt;롤백 넷코드 (Rollback Netcode)&lt;/h3&gt;
&lt;p&gt;이건 논문에서 소개된 내용은 아니지만 시간 조작과 관련해서 1:1 격투게임에서 쓰이는 유명한 기술 중에 롤백 넷코드(Rollback Netcode)가 있다. 롤백 넷코드란 기본적으로 상대방 플레이어의 마지막 입력으로 인한 관성을 추론해서 오브젝트를 렌더링 하되, 플레이어의 입력이 시간 지연으로 너무 늦게 도착하더라도 실제 입력이 발생한 시간을 기준으로 롤백 시키는 방식이다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/0NLe4IpdS1w?start=98&amp;feature=oembed&amp;start=98" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Rollback Netcode 에 대해 설명하는 유튜브 영상&lt;/p&gt;
&lt;p&gt;Time Warp는 서버 입장에서 가상으로 시간을 되돌려서 플레이어의 입력에 대한 정당성을 검증한다면 롤백 넷코드는 실제로 시간을 되돌려서 동기화를 맞춘다는 입장이다. 격투 게임은 빠른 반응성 그리고 정확한 시간에 타격해서 콤보를 완성시켜야 하는 미션이 있는데 Incoming Time Delay 기법으로 동기화를 맞추면 이런 미션을 제대로 수행하기 어려워 진다.&lt;/p&gt;
&lt;p&gt;정상 범주 내에서의 딜레이라면 롤백 넷코드를 사용해도 플레이어들이 레이턴시를 느낄 수 없기 때문에 좋은 방법이다. 한 가지 재밌는 점은 이런 예측 및 롤백을 통한 동기화 방식을 사용하면 아래와 같은 디자인을 사용하면 플레이어가 “이게 뭐지&amp;quot; 하는 경험을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1de11828"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1de11828"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RKCUT0JK96R71KV3tveAzg-402120508195.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1de11828"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RKCUT0JK96R71KV3tveAzg-402120508195.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;스트리트 파이터 3 — K.O. 화면&lt;/p&gt;
&lt;p&gt;상대방이 딸피인 산황에서 내가 타격할 때 상대방은 방어하거나 반응하지 못하거나 이 두 가지 갈림길이 존재한다. 만약 상대방의 방어 입력이 지연된다면 내 화면에서는 K.O.로 우선 렌더링하고 롤백 넷코드로 과거로 복구되는 경험을 하게 된다. 이런 경험을 방지하기 위해서 K.O. 팝업을 늦게 띄운다거나 슬로우 모션으로 대체할 수도 있다.&lt;/p&gt;
&lt;p&gt;관련 오픈소스중에 &lt;a href="https://github.com/pond3r/ggpo"&gt;GGPO SDK&lt;/a&gt;가 있는데, 이 오픈소스를 개발한 Tom Cannon은 현재 라이엇 게임즈 대전 격투 게임인 프로젝트 L의 프로듀서이자 테크 리드를 맡고 있다.&lt;/p&gt;
&lt;div class="video-embed"&gt;&lt;iframe src="https://www.youtube.com/embed/cuympmzTzro?feature=oembed" title="Embedded media" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id="월드-조정world-adjustment"&gt;월드 조정(World Adjustment)&lt;/h2&gt;
&lt;p&gt;일반적으로 레이턴시(ping)가 낮으면 게임의 난이도가 상승해서 부정적인 플레이 경험을 줄 수 있다. 이를 일부 보정하고 플레이 경험을 높이기 위해 가상 세계를 조정할 수 있다.&lt;/p&gt;
&lt;h3 id="조작감-보정-control-assistance"&gt;조작감 보정 (Control Assistance)&lt;/h3&gt;
&lt;p&gt;레이턴시로 인한 플레이어 행위의 부정확성을 약간 도와주는 것이다. [Fig 30]을 보면 파랑색 플레이어가 발사한 총알은 원래 빗나가지만 가까운 오브젝트로 궤적이 변경되도록 해서 피격 판정을 해주고 있다. 또 다른 예시로는 웹 사이트에서도 물체를 드래그 할때 커서와 가까운 오브젝트를 집어주듯이 게임에서도 비슷하게 조작감을 보조해줄 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-97d9d1e8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-97d9d1e8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7AR9Byh2I1tYrjLJiB7faw-e7c05931b618.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-97d9d1e8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7AR9Byh2I1tYrjLJiB7faw-e7c05931b618.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 30. Control Assistance]&lt;/p&gt;
&lt;h3 id="속성-스케일링attribute-scaling"&gt;속성 스케일링(Attribute Scaling)&lt;/h3&gt;
&lt;p&gt;객체에 속한 수치나 게임 세계의 파라미터를 조정는 기법이다. 한 가지 예를 들자면 &lt;a href="https://wah.cse.cuhk.edu.hk/wah/Wah/papers/C177/C177.pdf"&gt;이 논문&lt;/a&gt; 에서는 1:1 대전 게임에서 플레이어 A가 플레이어 B의 지연을 알아채기 힘들도록 피격하는 연출 시간을 길게 가져가는 방법을 사용한다. 예를 들어 활을 이용해 활시위를 당기면 나에게서 멀어질 때는 빠르게 보여주고 플레이어 B에게 닿을 때, 아직 B에게서 응답이 오지 않았다면 약간의 슬로우 모션을 연출해서 보여주는 식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dadccffd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dadccffd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-W04_XLURPljzzTI3_ZQ_NQ-eeb24cd230c7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dadccffd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-W04_XLURPljzzTI3_ZQ_NQ-eeb24cd230c7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;[Fig 31. Attribute Scaling]&lt;/p&gt;
&lt;p&gt;KAIST에서 발표한 HCI 연구 중 한 &lt;a href="https://dl.acm.org/doi/abs/10.1145/3290605.3300790"&gt;논문&lt;/a&gt;에서는 Flappy Bird 게임에서 레이턴시가 존재하면 게임의 난이도를 소폭 하향해서 Error Rate를 정상 상태로 맞추는 방안을 제안하기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-45671d23"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-45671d23"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SFD_EcC03slw0rQMQWYmXQ-a6c1af0c7a87.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-45671d23"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SFD_EcC03slw0rQMQWYmXQ-a6c1af0c7a87.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="마무리"&gt;마무리&lt;/h2&gt;
&lt;p&gt;멀티-플레이어 게임 환경에서 게임 서버는 플레이어 간의 행위를 교환하고 빠르게 그리고 자주 게임 상태를 업데이트 해서 부드러운 플레이 경험을 유저들에게 제공해야 한다.&lt;/p&gt;
&lt;p&gt;공교롭게도 과거에 비해 인터넷은 더 빨라졌지만 글로벌 원빌드 게임이 트렌드로 자리 잡으면서 국경을 넘어 더 먼거리의 플레이어와 교류해야 하고 모바일 게임 시장이 커지면서 플레이어들은 랙에 더 노출되고 있다. 이를 보정하기 위해 여러 연구가 존재한다. 이번 포스팅에서는 여러개의 논문을 분석하고 분류화해서 소개한 Survey 논문 하나를 정리했다.&lt;/p&gt;
&lt;p&gt;이 글이 게임 업계의 개발자 분들과 게임 업계를 꿈꾸는 학생분들, 게임에 적용되는 기술에 관심이 많은 타 업계의 개발자 분들에게 도움이 되었길 바랍니다.&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3519023"&gt;A Survey and Taxonomy of Latency Compensation Techniques for Network Computer Games&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bbs.pubg.game.daum.net/gaia/do/pubg/gm/read?articleId=56&amp;amp;bbsId=PN002&amp;amp;objCate1=253&amp;amp;pageIndex=1"&gt;배틀그라운드 : [개발일지] 서버 성능 개선&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=j3eQNm-Wk04&amp;amp;ab_channel=NDC"&gt;[NDC] CAP 이론을 통한 네트워크 동기화 기법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs-multiplayer.unity3d.com/netcode/1.0.0/reference/glossary/ticks-and-update-rates/"&gt;Unity | Ticks and update rates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=hadNizGE-Yc&amp;amp;ab_channel=YonseiEsportsLab"&gt;(Paper Review) Post-Render Warp with Late Input Sampling Improves Aiming Under High Latency Condit. — YESLAB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://citeseerx.ist.psu.edu/document?repid=rep1&amp;amp;type=pdf&amp;amp;doi=b151dfd6ff2f06f80d0e6547bd1efc019858bd22"&gt;Revealing Delay in Collaborative Environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://patents.google.com/patent/US10341678B2/en"&gt;Systems and methods for player input motion compensation by anticipating motion vectors and/or caching repetitive motion vectors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.valvesoftware.com/wiki/Lag_Compensation"&gt;Valve Developer Community | Lag Compensation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wah.cse.cuhk.edu.hk/wah/Wah/papers/C177/C177.pdf"&gt;CONCEALING NETWORK DELAYS IN DELAY-SENSITIVE ONLINE INTERACTIVE GAMES BASED ON JUST-NOTICEABLE DIFFERENCES&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>DynamoDB의 시스템 디자인과 분산 트랜잭션 구현 원리</title><link>https://scalalang2.com/p/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625/</link><pubDate>Mon, 04 Sep 2023 14:29:38 +0000</pubDate><guid>https://scalalang2.com/p/%EC%95%84-%ED%95%B4%EB%B4%90-dynamodb-%EB%93%A4%EC%96%B4%EA%B0%84%EB%8B%A4-f8da282bc625/</guid><description>&lt;p&gt;DynamoDB는 AWS 관리형 분산 키-밸류 저장소입니다. 만약 본인이 AWS 클라우드를 이용하고 있고 대규모 트래픽을 견뎌야 하는 키-밸류 NoSQL 데이터베이스가 필요하다면 DynamoDB를 검토하게 되실 겁니다.&lt;/p&gt;
&lt;p&gt;AWS는 USENIX ATC학회에 2022년, 2023년에 각각 2개의 논문을 발표했습니다. 하나는 시스템 디자인에 대해, 또 하나는 분산 트랜잭션 구현 원리에 대해 소개합니다. 이번 글은 이 두개의 논문을 요약 정리했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.usenix.org/conference/atc22/presentation/elhemali"&gt;Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service, USENIX ATC 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.usenix.org/conference/atc23/presentation/idziorek"&gt;Distributed Transactions at Scale in Amazon DynamoDB, USENIX ATC 2023&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1장. 키-밸류 저장소를 글로벌 스케일로 확장하기&lt;/li&gt;
&lt;li&gt;2장. DynamoDB 개요와 시스템 아키텍처&lt;/li&gt;
&lt;li&gt;3장. 분산 트랜잭션 구현의 어려움&lt;/li&gt;
&lt;li&gt;4장. DynamoDB에서 분산 트랜잭션을 구현한 방법&lt;/li&gt;
&lt;li&gt;5장. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="1장-키-밸류-저장소를-글로벌-스케일로-확장하기"&gt;1장. 키-밸류 저장소를 글로벌 스케일로 확장하기&lt;/h3&gt;
&lt;p&gt;키-밸류 저장소는 단순하게 &lt;code&gt;PutItem(key, value)&lt;/code&gt;, &lt;code&gt;GetItem(key)&lt;/code&gt;이 2개의 연산을 지원하는 데이터베이스라고 말할 수 있습니다. AWS엔지니어로 빙의해서 글로벌 트래픽을 견딜 수 있는 키-밸류 NoSQL 데이터베이스를 만들어야 한다면 어떻게 할 수 있을까요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AWS DynamoDB는 피크 타임 기준 1초에 1000억건의 요청을 처리하고 있습니다. (논문에서 발췌), 이 정도는 되야 글로벌 스케일이라고 할 수 있군요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7c6066ed"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7c6066ed"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-K_U9FbXY6VU6uuE_mhKynw-8e894acaf1d1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7c6066ed"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-K_U9FbXY6VU6uuE_mhKynw-8e894acaf1d1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;디렉토리 기반 아키텍처&lt;/p&gt;
&lt;p&gt;위 그림은 디렉토리-기반 아키텍처(Directory-Driven Architecture)라고 불리는 분산 키-밸류 시스템을 묘사합니다. 루트 노드는 키에 대응되는 &lt;strong&gt;스토리지 노드&lt;/strong&gt; 정보를 맵핑해서 가지고 있고, 클라이언트가 루트 노드로 부터 정보를 받아 스토리지 노드에 직접 접근하면 트래픽과 디스크 I/O를 분산시키는 효과가 있습니다. 서버의 개수만 충분히 많이 준비한다면 글로벌 트래픽을 받기에 부족함이 없습니다.&lt;/p&gt;
&lt;p&gt;벗(But), 이 시스템은 세 가지 문제가 있습니다. &lt;strong&gt;①&lt;/strong&gt; 서버의 개수가 고정되어 있어서 트래픽이 많지 않은 상황에서는 불필요한 리소스 낭비가 예상된다는 것, &lt;strong&gt;②&lt;/strong&gt; 노드에 장애가 발생하면 자칫 데이터가 유실될 위험이 있습니다. &lt;strong&gt;③&lt;/strong&gt; 특정 키가 핫스팟인 경우 특정 서버에 과부하가 발생할 수 있습니다. 우리는 이런 문제를 해결하기 위해 &lt;strong&gt;일관성 해시(Consistent Hash)&lt;/strong&gt;를 이용할 수 있습니다. 한 번, 저장된 데이터의 각 키들을 선형적인 선으로 길게 늘어놓고 양쪽 끝을 잡아 구부려서 원을 만든다고 상상해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-910ba46b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-910ba46b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rxgWq9VsDIz__AlV-d-XBQ-6ac62966a0ab.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-910ba46b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-rxgWq9VsDIz__AlV-d-XBQ-6ac62966a0ab.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;일관성 해시링&lt;/p&gt;
&lt;p&gt;일관성 해시란 위와 같이 노드와 키를 가상의 링(Ring)으로 표현한 모습입니다. 이제 링(Ring)위에 키와 서버를 해시값을 이용해서 적절한 위치에 둡니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7653a377"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7653a377"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dldR_lWgGBMkx89_-WKzMQ-bb8a0bc959a5.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7653a377"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dldR_lWgGBMkx89_-WKzMQ-bb8a0bc959a5.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;일관성 해시링에서 키가 배분되는 모습&lt;/p&gt;
&lt;p&gt;키가 저장될 서버는 원 위에서 각 개별 키들이 오른쪽 시계방향으로 흘러서 만나게 되는 첫번째 서버로 결정합니다. 그림 (A)를 보시면, {K1, K4}는 S1서버에 할당되고 {K2}는 S2에 할당되는 모습을 볼 수 있습니다. 만약 지금 {S1, S2, S3}로 구성된 클러스터에 S4 서버가 하나 더 추가된다면 다시 키를 어디에 저장할지 결정합니다. 간단하죠? 하지만 단순히 서버를 랜덤한 위치에 두는 것은 그림 (B)처럼 애매한 위치로 형성되면 파티션이 균등하게 형성되지 못해서 특정 서버에 부하가 몰리게 됩니다. 이 문제를 해결하기 위해 가상 노드라는 개념을 이용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9d15e0c2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9d15e0c2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jXWjliO7VMdVKwl7ygxAMA-6feb43103f48.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9d15e0c2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jXWjliO7VMdVKwl7ygxAMA-6feb43103f48.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;가상 노드를 이용한 서버군 배치&lt;/p&gt;
&lt;p&gt;가상노드란 동일한 서버를 랜덤하게 여러개 생성해서 링 위에 배치하는 방법입니다. 실제로는 서로 다른 해시 함수 N개를 준비하고 이 해시함수의 결과로 위치를 선정합니다. 위와 같은 방식을 따를 경우 서버가 추가되고 삭제되면서 재배치 되어야 하는 키의 총 개수는 k/N이 됩니다.&lt;/p&gt;
&lt;p&gt;②번 문제는 데이터를 복제 저장해서 사용합니다. 원래의 일관성 해시 링은 키가 시계방향으로 가면서 만나는 첫 번째 서버에 저장하는 거라면, Dynamo에서는 시계방향으로 만나는 K개의 서버에 데이터를 중복 저장하는 방식으로 복제합니다. 이 방식을 Cassandra에서는 &lt;strong&gt;&lt;code&gt;SimpleStrategy&lt;/code&gt;&lt;/strong&gt;라고 부르구요. 그리고 Cassandra는 데이터 센터 화재 같은 사고도 고려해서 여러 데이터 센터에 걸쳐 복제할 수 있는 네트워크 토폴로지 전략도 지원하고 있씁니다.&lt;/p&gt;
&lt;h3 id="이런-분산-키-밸류-시스템은-어디에-이용할까요"&gt;이런 분산 키-밸류 시스템은 어디에 이용할까요?&lt;/h3&gt;
&lt;p&gt;소수의 생산자가 80%의 컨텐츠를 생산해내는 파레토의 법칙이 작용하는 서비스는 전통적인 관계형 DB(RDBMS)가 좋은 선택지가 될겁니다. RDBMS는 쓰기 노드는 한 대로 제한되며 다수의 복제 노드가 존재합니다. 데이터를 쓸 때는 쓰기 노드에 요청을 보내고 데이터를 읽을때는 읽기 노드에서 불러들이는게 일반적인 패턴입니다. 유저 트래픽이 증가해도 읽기 요청만 급격하게 늘어날 뿐 쓰기 요청은 안정적이기 때문에 트래픽의 증감에 대해 유연하게 대처할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-7741215a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-7741215a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PbpmViRbpLL1d2l9GTSlAg-3f1367d706ff.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-7741215a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PbpmViRbpLL1d2l9GTSlAg-3f1367d706ff.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;전통적인 RDBMS 복제 클러스터 구성&lt;/p&gt;
&lt;p&gt;애플리케이션 성격에 따라 쓰기 비율이 높은 경우가 있습니다. 대표적인 예시로 채팅 서비스는 사용자 모두가 쓰기 요청을 발생시키는 생산자가 되고 준실시간으로 반응해야 합니다. 전통적인 데이터베이스는 읽기 성능을 위해 인덱스를 사용하는데요. 대규모 데이터가 쓰여지고 있는 상황에선 인덱스의 크기가 매우 크기 때문에 쓰기 속도가 시간을 따라 점차 느려집니다. 그래서 많은 채팅 서비스들은 분산 키-밸류 데이터베이스를 활용하고 있습니다. 대표적으로 &lt;a href="https://discord.com/blog/how-discord-stores-trillions-of-messages"&gt;Discord 에서 Cassandra를 사용하고 있는 것&lt;/a&gt;으로 잘 알려져 있습니다.&lt;/p&gt;
&lt;h3 id="2장-dynamodb-개요와-시스템-아키텍처"&gt;2장. DynamoDB 개요와 시스템 아키텍처&lt;/h3&gt;
&lt;h3 id="-dynamodb의-역사"&gt;① DynamoDB의 역사&lt;/h3&gt;
&lt;p&gt;DynamoDB의 이름은 아마존이 2007년에 발표한 논문인 &lt;a href="https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf%5C"&gt;Dynamo: Amazon’s Highly Available Key-value Store&lt;/a&gt; 에서 유래되었습니다. 오늘날 인용수가 6000이 넘은 seminal-paper입니다. Dynamo는 일관성 해시를 채용한 분산 키-밸류 저장소인데요. 이름이 같아서 DynamoDB도 일관성 해시를 이용해서 개발되었다고 오해하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;USENIX ATC 22에서 발표된 &lt;a href="https://www.usenix.org/conference/atc22/presentation/elhemali"&gt;Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service&lt;/a&gt; 논문에는 이 오해를 푸는 내용이 서술되어 있습니다. Dynamo는 원래 아마존에서 쇼핑 장바구니 데이터를 저장하기 위해 개발되었고 수평 확장성, 가용성, 영속성을 보장하는 데이터베이스입니다. 이 당시에는 클라우드 관리형 서비스는 아니었고 스스로 관리해야 하는 설치형 데이터베이스였는데요. 당시에는 글로벌 스케일에서 사용할 수 있는 유일한 데이터베이스였기 때문에 아마존 내에 많이 전파되었습니다.&lt;/p&gt;
&lt;p&gt;하지만, 아마존 엔지니어들이 Dynamo를 직접 운영해야 했고 데이터베이스 전문가 수준의 지식을 요구했기 때문에 부담이 있었죠. 무엇보다 애플리케이션 서버가 데이터베이스 시스템에 직접 접근하는 방식이 커넥션 관리, 동시 처리, 스키마 업그레이드 등의 이유로 확장성있게 사용하는데에 병목이 있었습니다.&lt;/p&gt;
&lt;p&gt;그리고 시간이 지나 &lt;code&gt;Amazon S3&lt;/code&gt;와 &lt;code&gt;Amazon SimpleDB&lt;/code&gt;가 런칭되었습니다. 이 시스템은 엔지니어가 스토리지 관리 부담없이 API만을 이용하는 시스템이었는데요. 아마존의 엔지니어들은 Dynamo가 더 좋은 성능과 기능을 갖추었음에도 S3와 SimpleDB를 적극 활용하기 시작합니다. 하지만 SimpleDB는 테이블당 용량과 초당 처리량(throughput)이 제한되어 있었는데요. 아마존 엔지니어들은 그래서 동일한 스키마를 갖춘 데이터를 여러 테이블로 쪼개는 편법으로 대응해왔습니다. 그리고 바로 이 페인포인트를 해결하고자 Amazon DynamoDB가 Dynamo와 SimpleDB의 특징들만 모아 2012년 AWS에 출시되었습니다.&lt;/p&gt;
&lt;h3 id="-dynamodb의-특징"&gt;② DynamoDB의 특징&lt;/h3&gt;
&lt;p&gt;DynamoDB의 최대 장점을 하나 뽑자면 &lt;strong&gt;일관된 레이턴시&lt;/strong&gt;를 보장한다는 겁니다. 데이터베이스를 직접 운영한다면 트래픽이 급격히 늘어날 때 노드를 제때 증설하지 못해서 유저에게 안좋은 경험만 남길 수도 있고요. 데이터베이스에 데이터가 꾸준히 증가하면 어느 순간 쓰기/읽기 성능이 떨어지는 걸 경험할 수 있습니다. 각 상황마다 대응할 수 있는 방법은 존재하지만 DynamoDB를 사용한다면 이런 상황에도 항상 일관된 경험을 제공하기 때문에 개발자가 온전히 서비스 개발에 집중할 수 있습니다. 이 외에도 DynamoDB는 아래 6개의 특징을 가집니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;완전 관리형 클라우드 서비스&lt;/strong&gt;&lt;br&gt;
DynamoDB는 개발자를 대신해 소프트웨어 업데이트, 하드웨어 관리, 클러스터 설정, 장애 복구, 데이터 암호화, 백업 등을 처리해주는 완전 관리형 서비스입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 테넌트 아키텍처&lt;/strong&gt;
DynamoDB의 스토리지 노드는 서로 다른 고객의 데이터를 동일한 물리적 장비에 저장합니다. 이는 고객의 비용을 절감할 수 있게 해주고 아마존의 입장에서는 리소스를 필요한 곳에 적절히 사용할 수 있게 해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;테이블의 무제한 스케일&lt;/strong&gt;&lt;br&gt;
DynamoDB에서 하나의 데이터는 아이템이라고 불리고, 아이템은 테이블에 저장됩니다. DynamoDB의 테이블은 데이터를 무제한 저장할 수 있습니다. 시스템은 자동으로 테이블의 크기에 따라 작게는 2,3대의 서버로부터 많게는 수천대의 서버로 데이터를 분산해서 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예측가능한 성능 제공&lt;/strong&gt;
&lt;code&gt;GetItem&lt;/code&gt;, &lt;code&gt;PutItem API&lt;/code&gt;는 항상 일관된 레이턴시를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고가용성&lt;/strong&gt;&lt;br&gt;
데이터를 여러개의 데이터 센터(AZ)에 분산 복제하며, 노드에 장애가 발생하면 안전하게 새로운 노드에 데이터를 복제해서 영속성과 가용성을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;탄력적인 스키마&lt;/strong&gt;
스키마를 강제하지 않습니다. 단순히 키/밸류 DB로 이용할 수도 있고 문서 형식의 데이터를 이용할 수도 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DynamoDB의 데이터 스키마는 아래 그림과 같습니다. 데이터의 집합은 테이블 단위로 관리하며 한 건의 데이터를 &lt;strong&gt;아이템(Item)&lt;/strong&gt;이라고 부릅니다. 아이템은 파티션 키, 정렬 키, 그리고 속성(Attribute)로 구성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-9256ee63"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-9256ee63"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z7RO4TIEATOcg1Pm_N1kZA-20acca0798bb.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-9256ee63"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z7RO4TIEATOcg1Pm_N1kZA-20acca0798bb.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DynamoDB의 테이블 레이아웃&lt;/p&gt;
&lt;h3 id="-시스템-아키텍처"&gt;③ 시스템 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1d9a0bcf"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1d9a0bcf"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m_VobWINmEt4Ww_ne-_SzA-92c6c3b4ddf9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1d9a0bcf"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-m_VobWINmEt4Ww_ne-_SzA-92c6c3b4ddf9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DynamoDB의 시스템 구성도 : &lt;a href="https://www.usenix.org/system/files/atc22-elhemali.pdf"&gt;USENIX ATC’ 22&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DynamoDB는 데이터를 저장할 스토리지 노드를 선정할 때 해시함수를 이용하긴 하지만 네트워크 토폴로지나 시스템 구성이 일관성 해시와는 많이 다릅니다. 오히려 마이크로서비스의 집합이라고 보는게 좋을 것 같습니다. 위 그림은 DynamoDB의 시스템 구성도를 보여줍니다. 요청 라우터(Request Router)는 유저 요청의 인증/인가, 개별 요청을 알맞은 스토리지 서버로 라우팅 하는 책임을 가집니다. 요청 라우터는 모든 읽기/업데이트 요청을 받아 처리하고, 개별 요청의 목적지를 메타데이터 서비스로부터 조회해서 알맞은 서버로 할당합니다.&lt;/p&gt;
&lt;p&gt;그림에는 나와있지 않지만, 다양한 마이크로서비스들이 시스템을 보조하고 있습니다. 대표적으로 AutoAdmin 서비스는 스토리지 장비들을 모니터링 하고 있다가 장애가 발생하면 새로운 장비로 교체하고 데이터를 복제하는 명령을 내리는 역할을 합니다. 이 외에도 point-in-time 복구, 온디맨드 백업, 업데이트 스트림, 글로벌 테이블, 글로벌 세컨더리 인덱스, 분산 트랜잭션 등을 수행하는 마이크로 서비스들이 존재합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;초기 Consistent Hash를 이용한 Dynamo는 Self-managed데이터베이스였습니다. 현재 AWS의 DynamoDB는 마이크로 서비스들의 집합이며 키-밸류 저장 기능을 제공하는 서비스의 모습을 하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="-스토리지-노드"&gt;④ 스토리지 노드&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f56c2ca1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f56c2ca1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-77tKOVb0vGCRoncoh1RMNQ-097a13802dec.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f56c2ca1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-77tKOVb0vGCRoncoh1RMNQ-097a13802dec.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;DynamoDB에 데이터가 쓰이는 과정&lt;/p&gt;
&lt;p&gt;스토리지 노드는 DynamoDB에서 실질적으로 데이터를 저장하는 인스턴스이며 여러 가용영역(AZ)에 걸쳐 &lt;strong&gt;3대의 노드로 클러스터를 이룬 상태복제머신&lt;/strong&gt;입니다. Paxos 합의알고리즘을 이용하고 있으며 3대의 노드 중 하나가 리더로 선정됩니다. 오직 리더만 쓰기 요청을 수행할 수 있고 읽기 일관성을 제공합니다. 쓰기는 항상 정족수 이상의 노드에 WAL(write-ahead logs)에 기록되어야 합니다. 이 때, 복제노드(Follower)는 최종일관성(Eventual Consistency)를 제공하고 리더는 강한 일관성(Strong Consistency)을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4a332cb4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4a332cb4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jbL7J7FZtwBkSmavh-wQqg-022cb9bc2d77.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4a332cb4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-jbL7J7FZtwBkSmavh-wQqg-022cb9bc2d77.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;스토리지 노드의 내부 구조&lt;/p&gt;
&lt;p&gt;스토리지 노드는 키-밸류 데이터를 저장하는 역할을 하고 있는데요. DynamoDB는 LSI(Local Secondary Index)라는 파티션 내의 인덱싱을 지원하고 있어서 B-Tree 자료구조를 가지고 있습니다. 인덱스를 가진 스토리지 복제 노드 이외에 &lt;strong&gt;WAL 로그&lt;/strong&gt;만 가지고 있는 로그 복제 노드(Log replica)도 있습니다. 이 노드의 역할은 정족수를 채울 수 없을 정도로 스토리지 노드에 장애가 발생하면 임시적으로 멤버로 참여할 수 있도록 대기하고 있습니다.&lt;/p&gt;
&lt;h3 id="-bursting-adaptive-capacity-gac"&gt;⑤ Bursting, Adaptive Capacity, GAC&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;여기서는 Bursting, Adaptive Capacity, GAC에 대해 설명합니다 :) &lt;br&gt;
이번 장을 읽고 이 세 가지 단어를 설명할 수 있는지 유의하며 읽어주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DynamoDB는 초당 처리량을 나타내는 &lt;strong&gt;Write Capacity Units(WCUs), &lt;/strong&gt;Read Capacity Units(RCUs) 수치에 따라 돈($)을 지불해야 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WCUs : 1KB 용량에 한해 1초에 1번의 쓰기 요청을 보낼 수 있음&lt;/li&gt;
&lt;li&gt;RCUs: 4KB 용량에 한해 1초에 1번의 일관성 읽기를 제공함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현재 서울 리전을 사용하면 1000 WCU=$0.7049, 1000 RCU=$0.14098로 가격이 형성되어 있습니다. 1초에 1000건의 요청을 처리할 수 있는 키-밸류 저장소를 가진 셈입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a5f4b570"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a5f4b570"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1uw01-QkBLFWTlYQgXTucA-91f9b2da1861.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a5f4b570"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-1uw01-QkBLFWTlYQgXTucA-91f9b2da1861.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;데이터는 파티션 키를 기준으로 분할해서 여러 스토리지에 저장된다&lt;/p&gt;
&lt;p&gt;자, 이제 AWS 앤지니어에 빙의해서 생각해보자구요. 내가 3000 WCUs를 세팅했으면 반드시 1초에 3000건의 요청을 서비스가 받아줘야 합니다. 이건 돈과 신뢰의 문제입니다. 완벽할 순 없더라도 최대한의 노력으로(best-effort) 초당 3000건의 요청을 데이터 접근을 제공해야 합니다. 고객의 데이터량이 많지 않더라도 하나의 물리적 노드에 전부 집어넣으면 네트워크 대역폭, I/O 등으로 인해 요청을 전부 처리할 수 없습니다. 따라서, 파티션 키를 기준으로 데이터를 적절히 분할해서 각 &lt;strong&gt;파티션&lt;/strong&gt;에 저장합니다.&lt;/p&gt;
&lt;p&gt;초기 버전의 DynamoDB에서 파티셔닝은 테이블의 처리량(throughput)에 의해 결정됩니다. 클라우드 장비의 스펙이나 네트워크 대역폭을 고려해서 하나의 파티션이 최대 1000 WCUs 만큼만 처리 가능하다는 결론이 나왔다고 합시다. &lt;strong&gt;만약 테이블이 3200 WCUs로 설정했으면 DynamoDB는 800WCUs씩 할당된 4개의 파티션을 생성합니다&lt;/strong&gt;. 시간이 지나서, 고객이 6000 WCUs로 증가시키면 &lt;strong&gt;기존에 존재하는 파티션을 분할합니다.&lt;/strong&gt; 다시 말하면, 1000 WCUs를 가진 6개의 파티션이 아닌, 750WCUs를 가진 8개의 파티션을 생성하게 됩니다. 여기엔 총 3가지 문제가 있습니다.&lt;/p&gt;
&lt;h3 id="처리량-희석throughput-dilution"&gt;처리량 희석(throughput dilution)&lt;/h3&gt;
&lt;p&gt;위 예시에서 고객이 WCUs를 증가시켰지만 파티션 별로 할당된 WCUs는 800에서 750으로 감소했습니다.&lt;/p&gt;
&lt;h3 id="핫-파티션hot-patition"&gt;핫 파티션(hot patition)&lt;/h3&gt;
&lt;p&gt;특정 아이템에 대한 트래픽이 몰리는 경우가 있습니다. 쇼핑몰에서 특정 제품이 할인율이 높아서 인기가 많다거나, 화제가 되는 인물의 인스타그램 팔로우 요청 등 많은 예시를 상상해 볼 수 있습니다. 이 경우, 파티셔닝을 하더라도 특정 파티션에 트래픽이 몰리게 되어서 파티션에 할당된 WCUs를 모두 소진하고 쓰로틀링(throttling)됩니다. &lt;em&gt;(throttling을 표현하는 적절한 한국어를 모르겠네요&lt;/em&gt; 🤔&lt;em&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="균등하지-못한-접근-패턴-non-uniform-access"&gt;균등하지 못한 접근 패턴 (non-uniform access)&lt;/h3&gt;
&lt;p&gt;서비스가 항상 균일한 트래픽을 발생시키는 건 아닙니다. 모바일 게임의 경우 출/퇴근 시간이나 새로운 이벤트 및 컨텐츠 출시가 있으면 트래픽이 증가합니다. &lt;a href="https://engineering.linecorp.com/ko/blog/how-line-messaging-servers-prepare-for-new-year-traffic"&gt;라인 메신저에선 새해에 트래픽이 급증하는 걸로 알려져 있습니다&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b6e512d8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b6e512d8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z-qZ6QnqFcweb-qJd4blaA-c45c21abc36c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b6e512d8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z-qZ6QnqFcweb-qJd4blaA-c45c21abc36c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;쓰로틀링/돈낭비가 발생하는 경우&lt;/p&gt;
&lt;p&gt;Provisioned WCUs를 작게 잡으면 요청 쓰로틀링이 발생하고, 높게 하자니 돈을 낭비하게 됩니다. 이 문제를 해결하기 위해, 현재의 DynamoDB는 Bursting 이라는 개념을 사용합니다. &lt;strong&gt;기본적인 아이디어는 “과거 사용하지 않았던 WCU를 누적하고 있다가 필요할 때 소진한다 입니다”이며,&lt;/strong&gt; 파티션에 할당된 WCU보다 더 많은 요청을 처리하도록 합니다.&lt;/p&gt;
&lt;p&gt;DynamoDB는 대표적인 처리율 제한(Rate Limit) 알고리즘인 토큰 버킷을 사용합니다. 여기선, 1초 단위의 토큰 버킷이 아닌 5분 동안 토큰을 계속 쌓아두다가 필요할 때 사용하게 됩니다. 이 값은 &lt;strong&gt;전역 승인 제어(GAC : Global Admission Control)&lt;/strong&gt; 서버가 관리하고 있다가 Request Router가 로컬의 토큰을 모두 소진하고 나면 GAC서버로 부터 요청하여 토큰을 얻어냅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-dcdd03a5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-dcdd03a5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AGViPGbRWixp8r8RYfqRUg-a83235aaf29b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-dcdd03a5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AGViPGbRWixp8r8RYfqRUg-a83235aaf29b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;GAC : Global Admission Control&lt;/p&gt;
&lt;p&gt;이것으로 &lt;strong&gt;처리량 희석&lt;/strong&gt;과 short-lived spike문제는 어느 정도 대응할 수 있게 되었습니다. DynamoDB는 긴 시간에 걸쳐 반복적으로 발생하는 long lived spike에도 대응할 수 있게 사용자에게 &lt;strong&gt;Adaptive Capacity&lt;/strong&gt; 기능을 제공합니다. AWS에선 테이블 단위로 토큰 소모량과 트래픽 양을 모니터링 하고 있다가 테이블에 쓰로틀링이 발생하면 용량을 늘리고, 남는 토큰이 있으면 용량을 줄입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-472d86f9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-472d86f9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7lInhD-j1_mxuVYgH_TN3g-2aebf94f38a1.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-472d86f9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7lInhD-j1_mxuVYgH_TN3g-2aebf94f38a1.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/ko/blogs/database/how-amazon-dynamodb-adaptive-capacity-accommodates-uneven-data-access-patterns-or-why-what-you-know-about-dynamodb-might-be-outdated/"&gt;[출처 : AWS 블로그] 파티션 4에 Adaptive Capacity가 적용된 모습&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;논문에선 bursting과 마찬가지로 best-effort 접근법이고 단순하지만 99.99%의 throttling을 막는 효과가 있다고 표현했습니다. 현재 DynamoDB에서 기본값으로 테이블을 생성하면 5 WCUs/5 RCUs와 Adaptive Capacity가 적용된 걸 사용하게 됩니다. 물론, 아마도 우리가 내야 할 비용또한 고무줄 처럼 늘었다 줄었다 할겁니다. 하지만, 어차피 고객 경험을 위해 감내해야 할 트래픽이고 고정값을 사용하는 것 보다는 비용 절감 효과가 있을 겁니다.&lt;/p&gt;
&lt;h3 id="3장-분산-트랜잭션-구현의-어려움"&gt;3장. 분산 트랜잭션 구현의 어려움&lt;/h3&gt;
&lt;p&gt;애플리케이션 개발자들은 수십년 동안 장애 및 동시성 처리를 위해 데이터베이스가 제공하는 트랜잭션 기능에 의존해왔습니다. 트랜잭션 개념이 없는 어딘가의 평행우주에선 프로그래머들이 아래 문제점들을 해결하느라 매일 밤낮으로 일하고 있을 겁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소프트웨어나 하드웨어는 언제나 실패할 수 있다&lt;/li&gt;
&lt;li&gt;애플리케이션은 연산을 실행하다가 언제나 죽을 수 있다&lt;/li&gt;
&lt;li&gt;네트워크는 어떤 이유에서라도 언제나 단절될 수 있다. &lt;a href="https://www.youtube.com/watch?v=4WlOnlRncK0&amp;amp;t=16s"&gt;구글의 해저 케이블은 상어의 공격을 받아서 단절된 적이 있다.&lt;/a&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&gt;트랜잭션은 여러개의 읽기와 쓰기 연산을 하나의 논리적 단위로 묶어서 이런 문제들을 단순화 시킨 매커니즘입니다. 트랜잭션의 &lt;strong&gt;원자성(Atomicity)&lt;/strong&gt;은 일련의 순차적인 연산들이 장애 상황에서 부분 결과를 만들지 않을 것을 보장하고, 고립성(Isolation)은 여러 트랜잭션이 경쟁적으로 실행되고 있는 상황에서 서로간의 연산이 간섭을 일으키지 않을 것을 보장합니다.&lt;/p&gt;
&lt;p&gt;트랜잭션이 엄청난 편의 기능을 제공함에도 불구하고 NoSQL 생태계에서는 트랜잭션을 제공 하는 사례가 적습니다. 분산 키-밸류 데이터베이스는 무제한 수평 확장성, 자동 파티셔닝, 복제를 이용한 내결함성 등을 갖추기 위해 트랜잭션 기능을 희생하는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;분산 데이터베이스에서 데이터는 여러 노드로 파티셔닝 되어 있는데, &lt;strong&gt;신뢰할 수 없는 네트워크 위에서 여러 노드에 걸쳐 원자성을 보장해야 하는 분산 트랜잭션은 구현 및 실제 운영하기에 여러 어려움들이 있습니다.&lt;/strong&gt; 이번장에서는 분산 트랜잭션에 대해 제가 아는 만큼만 소개해드리고자 합니다.&lt;/p&gt;
&lt;h3 id="1-클라이언트에서-여러-노드로-트랜잭션-전송하기"&gt;(1) 클라이언트에서 여러 노드로 트랜잭션 전송하기&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0399d286"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0399d286"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7mO7vJlxJeRJi13NOl3GMQ-4822166874b3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0399d286"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-7mO7vJlxJeRJi13NOl3GMQ-4822166874b3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;분산 트랜잭션의 잘못된 예시&lt;/p&gt;
&lt;p&gt;클라이언트가 여러 데이터베이스로 트랜잭션을 전송하고 개별적으로 커밋/어보트를 결정하는 건 바람직하지 않습니다. 어떤 노드에선 제약 조건 위반으로 인해 실패할 수 있고, 네트워크 문제로 요청이 누락되었거나, 장애가 발생했을 수도 있습니다. 커밋시킨 후에 나중에 어보트로 소급적용하는 건 트랜잭션에선 허용되지 않습니다. 일단 커밋된 연산은 다른 트랜잭션이 읽을 수 있는 상태가 되고 어보트를 소급적용하려면 트랜잭션에 의존한 모든 연산을 어보트 해야 하기 때문입니다.&lt;/p&gt;
&lt;h3 id="2-two-generals-problems"&gt;(2) Two Generals’ Problems&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6dd0faae"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6dd0faae"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IAZDUz5LDZIA6ZqKIORQdw-577d4aee93ee.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6dd0faae"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-IAZDUz5LDZIA6ZqKIORQdw-577d4aee93ee.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.networkpages.nl/distributed-consensus-what-do-money-transactions-and-attacking-armies-have-in-common/"&gt;Ref. Distributed consensus: What do money transactions and attacking armies have in common?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두개의 군대가 계곡 아래의 성을 양쪽에서 포위하고 공격할 준비가 되어 있습니다. 군대의 각 장군은 전령을 보내 공격 시간을 합의해야 하는데, 전령이 이동하면서 붙잡힐 가능성이 있습니다. 만약, 하나의 군대가 단독으로 공격을 시도하면 대패할 것이기 때문에 &lt;strong&gt;반드시 공격 시간을 동의&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;p&gt;결론부터 말씀드리자면, &lt;strong&gt;컴퓨터 과학자들은 이 문제는 절대 풀 수 없음을 증명했습니다.&lt;/strong&gt; 우리는 복잡한 수학 계산 없이도 이를 직관적으로 이해할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1b1dff37"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1b1dff37"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pjWIQvnJ1t0R3tzdkOnXOA-b7bd7f591031.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1b1dff37"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-pjWIQvnJ1t0R3tzdkOnXOA-b7bd7f591031.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;순환 오류에 빠진 합의 문제&lt;/p&gt;
&lt;p&gt;장군 A가 먼저, 공격 시간을 합의하기 위해 메신저를 보냅니다. 여기서 메신저가 붙잡혀서 메시지가 도착하지 않을 수 있습니다. 그래서 장군 A는 장군 B로부터 확인(Acknowledgement) 메시지를 받아야 합니다. 반대로 장군 B도 똑같은 상황입니다. 본인이 전송한 확인 메시지가 유실 될 수 있기 때문에 장군 A로부터 ACK 메시지를 받아야 합니다. 이런 서로간의 모순이 영원히 발생하기 때문에 두 명의 장군은 시간을 합의할 수 없습니다. 이 문제는 2대의 노드가 서로 통신하여 트랜잭션을 커밋하도록 결정하는게 왜 불가능한지를 설명해줍니다,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;원래 이 문제는 분산 합의에 대해 설명하는 문제입니다. 현대 분산 시스템들은 네트워크의 유실이 자주 발생하지 않는다고 가정하고, 3대 이상의 노드에서 다수결만큼은 합의할 수 있도록 하는 알고리즘을 연구하고 개발되어 왔습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="3-two-phase-commit-protocol"&gt;(3) Two-Phase Commit Protocol&lt;/h3&gt;
&lt;p&gt;현실적인 대안으로 많은 서비스들은 &lt;strong&gt;2단계 커밋 프로토콜&lt;/strong&gt;을 이용해서 분산 트랜잭션을 구현합니다. 구현 원리는 단순해요. 코디네이터(Coordinator)라고 불리는 제 3자가 커밋과 어보트 메시지를 결정해서 각 데이터베이스에 전달합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2374f191"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2374f191"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LC2TV3jD2jwMRfRmLWf43Q-0ef5081c04ab.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2374f191"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LC2TV3jD2jwMRfRmLWf43Q-0ef5081c04ab.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Two-Phase Commit Protocl&lt;/p&gt;
&lt;p&gt;2PC 트랜잭션은 평상시처럼 데이터를 읽고, 쓰면서 시작합니다. 여기서 개별 데이터베이스 혹은 노드들은 참여자(participant) 라고 부릅니다. 참여자가 데이터를 모두 임시적으로 연산을 하고나면 코디네이터가 준비 단계에서 트랜잭션을 실제로 커밋할 수 있는지 물어봅니다. 만약, 참여자가 YES라고 답변한다면 이 트랜잭션은 무조건 커밋되어야 합니다. 나중에 디스크 공간이 모조라네, 제약조건을 제대로 체크하지 않았네 하는 변명으로 어보트 할 수 없습니다. 마지막으로 커밋 명령을 내려서 트랜잭션을 모든 노드에 커밋합니다.&lt;/p&gt;
&lt;p&gt;이 프로토콜은 단순하지만 확실하게 원자성을 보장하는 것으로 알려져 있습니다. 그런데, 소프트웨어 엔지니어인 우리가 보기에 직관적으로 몇 가지 고려해야할 사항들이 떠오릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) 먼저, 코디네이터가 단일 장애 지점(SPoF)입니다. 코디네이터에서 장애가 나면 코디네이터가 복구될때까지 하염없이 기다릴 수 밖에 없습니다. 즉, &lt;strong&gt;가용성&lt;/strong&gt;이 다소 감소됩니다. Paxos/Raft 같은 분산 합의 알고리즘을 쓰면 이 문제를 다소 완화 할 수 있긴합니다.&lt;/li&gt;
&lt;li&gt;(2) 데이터베이스가 장애가 발생하면 복구될때 까지 코디네이터는 무한히 재시도 해야 합니다. 데이터베이스가 복구 될때까지 참여자는 대기할 수 밖에 없습니다. 이런 상태에 있는 참여자의 트랜잭션을 불확실하다(uncertain)고 합니다.&lt;/li&gt;
&lt;li&gt;(3) 원자성은 보장하지만 강한 일관성은 보장하지 않습니다. 2PC 프로토콜은 기본적으로 최종 일관성(Eventual Consistency)만을 보장합니다.&lt;/li&gt;
&lt;li&gt;(4) 참여자들이 서로의 자원을 요구하는 경우, 교착상태(deadlock)에 빠집니다.&lt;/li&gt;
&lt;li&gt;(5) 여러번의 동기식 네트워크 통신을 요구하기 때문에 &lt;strong&gt;성능이 매우 감소합니다.&lt;/strong&gt; &lt;a href="https://docplayer.net/47484464-Distributed-transactions-in-mysql.html"&gt;MySQL의 분산 트랜잭션은 10배 이상 느리다고 보고&lt;/a&gt;되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 이유로 2단계 커밋에 대한 평가가 사람마다 엇갈립니다. 구글의 &lt;a href="https://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf"&gt;스패너: 구글의 전역 데이터베이스&lt;/a&gt; 논문에서는 트랜잭션 없이 코딩하는 것 보다 트랜잭션을 과하게 쓰고 있다가 성능 문제가 발생하면, 그 때 대응하는 것이 낫다 라고 주장하는 한편, 어떤 클라우드 서비스는 성능 문제가 너무 커서 분산 트랜잭션을 지원하지 않기도 합니다.&lt;/p&gt;
&lt;h3 id="4장-dynamodb의-분산-트랜잭션"&gt;4장. DynamoDB의 분산 트랜잭션&lt;/h3&gt;
&lt;p&gt;트랜잭션은 애플리케이션 개발자들이 장애 및 동시성에 대한 고민을 덜어주고 생산성을 높여줍니다. 만약 클라우드 서비스가 트랜잭션 기능을 지원해주면 개발자들의 업무 효율을 많이 높여줄 수 있겠죠? 이런 이유로 DynamoDB는 트랜잭션 기능을 지원하기로 합니다.&lt;/p&gt;
&lt;p&gt;Amazon 엔지니어들은 ACID 특징을 제공하면서도 성능, 가용성, 확장성을 희생하지 않으려 했습니다. 자 이제, 트랜잭션을 지원하기로 했으니 기획부터 해야겠죠? &lt;a href="https://www.usenix.org/system/files/atc23-idziorek.pdf"&gt;USENIX ATC 2023에 발표된 논문&lt;/a&gt;에서 Amazon 엔지니어들은 트랜잭션이 다음의 특징을 가지도록 설계했다고 밝혔습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Transactions are submitted as single request&lt;/strong&gt;&lt;br&gt;
전통적인 RDBMS는 커넥션을 미리 맺어두고 BEGIN, COMMIT, ROLLBACK 메시지를 여러번 교환해서 트랜잭션을 완성합니다. 이런 방식의 추상화는 트랜잭션의 생명주기가 너무 길어지면 성능에 악영향을 미치고요. DynamoDB는 멀티 테넌트 서비스이기 때문에 이런 긴 주기를 가진 세션을 살려두기가 어려웠습니다. DynamoDB의 트랜잭션은 일련의 동작을 하나의 API로 전부 전송하도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transactions rely on a transaction coordinator while non-transaction operations bypass the two-phase coordination&lt;/strong&gt;
DynamoDB의 트랜잭션 요청은 트랜잭션 코디네이터 서비스를 거쳐서 2단계 커밋(2PC) 프로토콜을 이용해 원자성을 보장합니다. &lt;em&gt;(이제는 익숙한 내용이시죠?)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transactions update items in place.&lt;/strong&gt;
MySQL InnoDB, PostgreSQL은 트랜잭션들이 마지막으로 커밋된 버전을 접근해서 읽어가는 방식인 MVCC를 이용합니다. 하지만, 이런 스냅샷 격리는 추가적인 저장 공간을 요구하는데요. DynamoDB에서 이를 구현하려면 매우 많은 변경이 필요했고, 버전 리텐션 정책이나 기타 스토리지 비용이 증가하기 때문에 단일 버전의 데이터만을 다룹니다. 즉, 데이터를 변경하고자 하면 그 즉시 변경이 반영됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transactions do not acquire locks.&lt;/strong&gt;
병렬로 실행되는 트랜잭션들이 동일한 데이터에 읽기/쓰기를 수행할 때 충돌로 인한 문제를 해소하기 위해 전통적으로 2단계 잠금(&lt;em&gt;Two-Phase Locking)을 쓰기도 합니다. 하지만,&lt;/em&gt; 트랜잭션 실패시 락을 해제(releasing) 하는 것과 데드락 상황을 방지하는 등 여러가지 노력이 필요합니다. DynamoDB는 설계를 단순화 하기 위해 낙관적 동시성을 이용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;⭐ Transactions are serially ordered using timestamps.&lt;/strong&gt;
스토리지 노드에 동시에 들어오는 트랜잭션 요청들에 대해서 직렬성을 보장하기 위해 Timestamp Ordering을 이용합니다. &lt;strong&gt;이건 MVCC도 사용할 수 없고 2PL도 사옹할 수 없었기 때문에 그나마 최선의 대안으로 채택한 느낌이 있습니다. (필자 피셜)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="1-dynamodb의-분산-트랜잭션"&gt;(1) DynamoDB의 분산 트랜잭션&lt;/h3&gt;
&lt;p&gt;DynamoDB에서 트랜잭션은 TransactGetItems, TransactWriteItems 이 두 가지 API에 의해 제공됩니다. 이 트랜잭션은 Blocking없이 동작을 수행하며 성공/실패에 대한 응답을 줍니다. 만약, 동시 트랜잭션에 의해 충돌이 일어나면 DynamoDB는 트랜잭션을 거부(Reject)합니다. 예를 들어, TransactGetItems를 이용할 때, 동시에 동일한 데이터를 수정하고 있는 트랜잭션이 있다면 해당 요청은 거부됩니다.&lt;/p&gt;
&lt;p&gt;//Check if customer exists&lt;br&gt;
Check checkItem = new Check().&lt;br&gt;
withTableName(&amp;ldquo;Customers&amp;rdquo;).&lt;br&gt;
withKey(&amp;ldquo;CustomerUniqueId&amp;rdquo;).&lt;br&gt;
withConditionExpression (&amp;ldquo;attribute_exists(CustomerId)&amp;rdquo;);&lt;br&gt;
&lt;br&gt;
//Update status of the item in Products&lt;br&gt;
Update updateItem = new Update().&lt;br&gt;
withTableName (&amp;ldquo;Products&amp;rdquo;).&lt;br&gt;
withKey (&amp;quot; BookUniqueId &amp;ldquo;).&lt;br&gt;
withConditionExpression (&amp;rdquo; expected_status &amp;quot; = &amp;ldquo;IN_STOCK&amp;rdquo;).&lt;br&gt;
withUpdateExpression (&amp;ldquo;SET ProductStatus = SOLD&amp;rdquo;);&lt;br&gt;
&lt;br&gt;
//Insert the order item in the orders table&lt;br&gt;
Put putItem = new Put ().&lt;br&gt;
withTableName (&amp;ldquo;Orders&amp;rdquo;).&lt;br&gt;
withItem (&amp;quot;{&amp;ldquo;OrderId&amp;rdquo;: &amp;quot; OrderUniqueId &amp;ldquo;, &amp;ldquo;ProductId&amp;rdquo; :&amp;rdquo; BookUniqueId &amp;ldquo;, &amp;ldquo;CustomerId&amp;rdquo;&lt;br&gt;
&amp;quot; CustomerUniqueId &amp;ldquo;, &amp;quot; OrderStatus&amp;rdquo;:&amp;ldquo;CONFIRMED&amp;rdquo;,&amp;ldquo;OrderCost&amp;rdquo;: 100}&amp;rdquo;).&lt;br&gt;
withConditionExpression(&amp;quot; attribute_not_exists (OrderId)&amp;quot;);&lt;br&gt;
&lt;br&gt;
TransactWriteItemsRequest twiReq = new TransactWriteItemsRequest ().&lt;br&gt;
withTransactItems ([ checkItem , putItem , updateItem ]) ;&lt;br&gt;
&lt;br&gt;
DynamoDBclient.transactWriteItems(twiReq);&lt;/p&gt;
&lt;p&gt;위 코드는 DynamoDB에서 트랜잭션을 발생하는 예시 코드 입니다. &lt;code&gt;checkItem&lt;/code&gt;, &lt;code&gt;updateItem&lt;/code&gt;, &lt;code&gt;putItem&lt;/code&gt; 이 세 가지 연산을 생성한 다음에 트랜잭션 객체를 생성할 때 배열로 연속적인 연산을 지정하고 네트워크를 통해 DynamoDB로 전송됩니다. 코드를 잘보면 &lt;code&gt;withConditionExpression&lt;/code&gt; 을 이용해 제약 조건을 검사하는 코드가 있습니다. 이런 일련의 연산을 정의함으로써 우리는 &lt;strong&gt;원자성&lt;/strong&gt;과 &lt;strong&gt;무결성&lt;/strong&gt;을 챙길 수 있습니다.&lt;/p&gt;
&lt;h3 id="2-dynamodb-트랜잭션-코디네이터"&gt;(2) DynamoDB 트랜잭션 코디네이터&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2ae7971b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2ae7971b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-puZpVaL5IDe4TObYxBqKAg-4ec92417941c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2ae7971b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-puZpVaL5IDe4TObYxBqKAg-4ec92417941c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.usenix.org/system/files/atc23-idziorek.pdf"&gt;Ref. DynamoDB 트랜잭션 하이-레벨 아키텍처 | USENIX ATC 2023 | AWS 논문&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;트랜잭션 요청은 요청 라우터를 거쳐서 트랜잭션 코디네이터로 전달됩니다. 만약, 트랜잭션이 아닌 일반 Get/Put 요청의 경우에는 요청 라우터 단에서 처리됩니다. 코디네이터라.. 이름이 익숙하죠? DynamoDB는 여러 스토리지 노드에 걸쳐 원자성을 보장하기 위해 2단계 커밋(2PC) 프로토콜을 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cd4b7425"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cd4b7425"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGhM8NmhqhnwiRHHa_8hlw-cc720eef8984.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cd4b7425"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGhM8NmhqhnwiRHHa_8hlw-cc720eef8984.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.usenix.org/system/files/atc23-idziorek.pdf"&gt;Ref. DynamoDB / Transaction Coordinator, two phase commit protocol&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;트랜잭션 코디네이터는 여러 스토리지 노드로 1. 준비 단계에서 데이터를 쓸 수 있는지 물어봅니다. 만약 데이터를 커밋할 수 있다면 트랜잭션 코디네이터는 커밋 메시지를 보내고 참여자 노드는 본인의 로컬 데이터를 수정하면서 &lt;strong&gt;마지막으로 수정한 날짜(last write timestamp)&lt;/strong&gt;를 기록합니다. 이 모든 단계가 끝나야 트랜잭션 코디네이터는 그제서야 성공/실패 유무를 요청 라우터한테 전달하고, 요청 라우터는 클라이언트로 응답을 포워딩 합니다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;last write timestamp&lt;/code&gt; 가 이후에 설명할 &lt;strong&gt;Timestamp Ordering&lt;/strong&gt; 에서 트랜잭션에 직렬성을 부여할 때 사용되는데요. 만약, 데이터를 삭제한 상황이라면 이 값이 존재할 수 없겠죠. DynamoDB는 툼스톤(Tombstones)을 남기는 대신에 파티션 별로 가장 높은 수정 날짜를 기록하는 것으로 삭제된 데이터의 timestamp까지 기록합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 툼스톤(Tombstone) 이란?&lt;/strong&gt;
분산 복제 환경에서 데이터를 바로 삭제하는게 애매한 케이스가 있습니다. 예를 들면, &lt;strong&gt;장바구니가 최대 개수만큼 차서 가장 오래된 상품을 제거&lt;/strong&gt;한다고 해봅시다. 만약 복제 노드가 딱 이 데이터를 삭제하다가 장애가 발생하고 다시 복구되면 오래된 상품을 제거하는 연산을 2번 수행할 수 있습니다. 그래서 카산드라 같은 분산 DB는 데이터를 바로 삭제하지 않고 툼스톤 레코드를 남기고 아주 나중에 삭제하는 전략을 사용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="3-timestamp-ordering-protocol"&gt;(3) Timestamp Ordering Protocol&lt;/h3&gt;
&lt;p&gt;DynamoDB는 분산 환경에서 원자성을 보장하는데는 2PC를 이용하고 스토리지 노드 내에서의 원자성과 무결성을 보장하는데는 Timestamp Ordering 프로토콜을 키-밸류 시맨틱에 맞게 변형해서 사용하고 있습니다. 이 내용을 더 잘 이해할 수 있게 먼저, 가장 기본적인 모습의 Timestamp Ordering(TO) 부터 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f45f7845"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f45f7845"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zGv3dJh5M8iIfqippMC6aA-b4159105091a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f45f7845"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zGv3dJh5M8iIfqippMC6aA-b4159105091a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;기본적인 Timestamp Ordering의 동작 원리를 보여준다&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;각 트랜잭션에는 타임스탬프가 부여됩니다. 임의의 두 트랜잭션 A, B의 타임스탬프가 각각 TS(A), TS(B) 이고 TS(A) &amp;lt; TS(B)의 관계를 가진다면 TO는 A가 먼저실행되고 B가 이후에 실행됨을 보장됩니다.&lt;/li&gt;
&lt;li&gt;모든 데이터는 &lt;strong&gt;읽은 시간(R-TS)&lt;/strong&gt;과 &lt;strong&gt;쓴 시간(W-TS)&lt;/strong&gt;를 기록하고있습니다. 만약, 트랜잭션 A가 데이터 X를 수정하려고 하는데 R-TS(X) &amp;gt; TS(A) 혹은 W-TS(X) &amp;gt; TS(A)의 관계를 가진다면 트랜잭션을 어보트 합니다. &lt;strong&gt;다시 말하면,현재 트랜잭션의 타임스탬프 시간 상 미래의 데이터를 읽으려고 하면 트랜잭션을 어보트 합니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;만약, 트랜잭션 A가 데이터 X를 읽으려고 할때, W-TS(X) &amp;gt; TS(A) 라면 트랜잭션을 어보트 하고요. 아니라면 데이터를 읽은 뒤 데이터 X에대한 읽은 시간을 max(R-TS(X), TS(A))로 교체합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;규칙이 매우 간단하죠? 이 알고리즘은 실행된 트랜잭션을 일렬로 세웠을 때 임의의 두 트랜잭션 i, j such that (T_i &amp;lt; T_j)를 선정했을 때 타임스탬프가 TS(T_i) &amp;lt; TS(T_j)의 관계가 성립함을 보장합니다. 즉, DynamoDB의 트랜잭션 코디네이터가 트랜잭션에 &lt;strong&gt;단조 증가 시계&lt;/strong&gt;로 타임스탬프를 제대로 부여하고 네트워크 상에서 스토리지 노드에게 차례대로 전송되기만 한다면 나름 괜찮은 동시성 제어 알고리즘이라고 할 수 있습니다. &lt;a href="https://faculty.cc.gatech.edu/~jarulraj/courses/8803-s21/slides/14-timestamp-ordering.pdf"&gt;TO에 대한 더 자세한 내용은 조지아 공과대학의 분산 시스템 강의자료에서 확인하실 수 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;단순하고 강력한 매커니즘이지만 단점도 있습니다. 불필요하게 트랜잭션을 어보트 할수도 있고 무엇보다 실행 시간이 긴 트랜잭션(long running tx)의 경우 너무 쉽게 어보트 된다는 겁니다. 그럼에도 DynamoDB가 TO를 채택한 이유를 고객들과 같이 커뮤니케이션 하다보니 long running tx 까지는 지원할 필요가 없었다고 합니다. &lt;em&gt;(논문의 결론을 읽으시면 됩니다)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="4-dynamodb의-timestamp-ordering-protocol"&gt;(4) DynamoDB의 Timestamp Ordering Protocol&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cd4b7425"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cd4b7425"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGhM8NmhqhnwiRHHa_8hlw-cc720eef8984.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cd4b7425"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-uGhM8NmhqhnwiRHHa_8hlw-cc720eef8984.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.usenix.org/system/files/atc23-idziorek.pdf"&gt;Ref. DynamoDB / Transaction Coordinator, two phase commit protocol&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위에서 아키텍처를 설명하면서 DynamoDB에서 트랜잭션은 &lt;strong&gt;트랜잭션 코디네이터&lt;/strong&gt;를 통해 실행된다고 했었죠. 이 코디네이터가 트랜잭션에 타임스탬프를 부여합니다. 그리고 물론, 대규모 트래픽을 받기 위해서 코디네이터 또한 복수개의 인스터스로 구성되어 있습니다. 그리고, 그림을 잘 보시면 &lt;code&gt;Ledger&lt;/code&gt; 라는 별개의 저장공간이 부여되어 있는데요. 이는 코디네이터 장애 발생시 다른 코디네이터가 이어서 처리 할 수 있도록 중간 상태를 기록하는 테이블이고 이 정보도 DynamoDB에 저장되어 있습니다.&lt;/p&gt;
&lt;p&gt;DynamoDB는 Timestamp Ordering을 키-밸류 저장 시맨틱에 맞춰서 몇 가지 규칙을 수정해서 이용하고 있습니다. 아래는 논문의 &lt;code&gt;4장. Adapting timestamp ordering for keyvalue operations&lt;/code&gt; 을 정리한 내용입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reads to individual items can always be performed successfully even if there is a prepared transaction that is attempting to write that item.&lt;/strong&gt;&lt;br&gt;
단순 데이터 한 개를 읽는 연산은 다른 트랜잭션이 준비 단계에 있어도 바로 실행해서 결과값을 주어도 Timestamp Ordering의 제약 조건에 위배되지 않습니다. 생각해보면 직관적으로 이해할 수 있습니다. 이 트랜잭션의 타임스탬프는 이미 데이터의 마지막 쓴 시간과 스토리지 노드에서 대기중인 트랜잭션보다 과거 시간을 받게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Writes to individual items can be performed immediately and serialized before any prepared transactions in many cases&lt;/strong&gt;&lt;br&gt;
DynamoDB에서 트랜잭션이 아닌 단순 요청은 바로 스토리지 노드로 라우팅 됩니다. 스토리지 노드는 이 요청에 대해 자신이 가지고 있는 모든 트랜잭션보다 더 과거의 타임스탬프를 부여합니다. 이렇게 하면 단순 쓰기 요청도 바로 실행할 수 있고 전체 요청의 직렬성을 보장하게 됩니다. 즉, 유저의 요청이 대기 상태로 있지 않고 바로 연산을 실행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Writes to individual items can be performed immediately or delayed and serialized after any prepared transactions in other cases&lt;/strong&gt;&lt;br&gt;
(2)를 바로 적용하기 힘든 경우도 있습니다. 예를 들면, 내 잔액이 $100 이상인가? 와 같은 조건을 검사하는 트랜잭션이 준비 단계에 있다면 내 요청이 이 조건을 위배되게 만듭니다. 이 경우 요청을 거부(Reject)해도 되지만, DynamoDB에서는 요청을 큐에 넣어뒀다가 트랜잭션이 처리되면 그 때 타임스탬프를 새로 부여하고 연산을 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write transactions can be accepted even with an old timestamp.&lt;/strong&gt;&lt;br&gt;
우리가 DynamoDB의 분산 트랜잭션에 대해 이야기 하고 있다는 컨텍스트를 잊으시면 안됩니다. 겨우 하나의 트랜잭션이 여러 대의 스토리지 노드에 요청을 전달할 수 있습니다. 만약, 너무 잦은 어보트가 발생하면 트랜잭션 기능이 있어도 사용성이 매우 떨어지겠죠. 그래서 왠만하면 통과되는게 좋습니다. 그래서 어떤 일을 하냐면 &lt;strong&gt;스토리지 노드에 도착한 쓰기 연산이 트랜잭션의 일부이고 너무 과거의 타임스탬프를 가지고 있다고 하더라도 트랜잭션을 취소시키지 않는다는 겁니다.&lt;/strong&gt; 어차피 미래의 타임스탬프에 의해 덮어쓰여질 값이기 때문에 그냥 무시합니다. (기본적인 Timestamp Ordering에선 이 트랜잭션은 취소될 겁니다)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read transactions can be executed in a single round rather than using a two-phase protocol&lt;/strong&gt;
데이터를 읽기만 하는 트랜잭션이라면 단일 페이즈로 끝날수도 있습니다. 트랜잭션 코디네이터는 개별 스토리지 노드로 요청을 보낼 때 읽기 시간을 동봉해서 전송합니다. 스토리지 노드는 읽기 시간 보다 데이터의 시간이 더 현재와 가깝다면 어보트 하고 아니라면 결과값을 반환합니다. 코디네이터는 모든 결과를 종합했을 때 모두 정상 응답을 주었을 때 클라이언트에게 결과값을 반환합니다. 이게 단일 페이즈가 가능한 이유는 아이템의 읽기 시간은 데이터로 기록하지 않기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transactions that write multiple items in a single partition can be executed in a single round rather than using a two-phase protocol.&lt;/strong&gt;
쓰기 트랜잭션도 단일 페이즈로 가능한 경우가 있습니다. 트랜잭션이 참조하는 모든 데이터가 하나의 스토리지 노드에 존재하는 경우죠. 따로 준비/커밋 단계를 할 필요없이 바로 커밋해버리면 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;논문에서는 트랜잭션과 트랜잭션이 아닌 것, 트랜잭션의 read-set(=참조하는 데이터 집합)크기가 증가함에 따라 레이턴시가 얼마나 증가하는 지 등 다양한 측면에서 &lt;strong&gt;성능 평가 결과&lt;/strong&gt;를 소개 자세히 소개하고 있습니다.&lt;/p&gt;
&lt;p&gt;저는 여기선 성능 평가 내용은 생략하려고 합니다. 다시 원점으로 돌아와서 이 논문의 취지를 다시 생각해보면 DynamoDB의 예측 가능한 성능을 제공한다는 것, 무한 수평 확장 가능하다는 특색을 지키면서 쓸만한 수준의 트랜잭션 기능을 제공하는 것이었잖아요? 아마도 성능 평가 항목에는 이 주장을 뒷받침 할 수 있는 근거들이 있을겁니다.&lt;/p&gt;
&lt;h3 id="5장-마무리"&gt;5장. 마무리&lt;/h3&gt;
&lt;p&gt;오늘 우리는 AWS 엔지니어들이 USENIX ATC 학회에 보고한 두개의 논문을 리뷰했습니다. 하나는 시스템 디자인에 대해, 다른 하나는 분산 트랜잭션에 대해 다루었습니다.&lt;/p&gt;
&lt;p&gt;오늘도 내용이 많이 길었네요,&lt;br&gt;
긴 글 읽어주셔서 감사드리며 다음에 다시 뵙겠습니다.&lt;/p&gt;</description></item><item><title>소프트웨어 엔지니어가 알아야 할 로그에 대한 모든 것</title><link>https://scalalang2.com/p/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EB%A1%9C%EA%B7%B8%EC%97%90-%EB%8C%80%ED%95%9C-%EB%AA%A8%EB%93%A0-%EA%B2%83-11513af8b998/</link><pubDate>Mon, 14 Aug 2023 09:03:12 +0000</pubDate><guid>https://scalalang2.com/p/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EB%A1%9C%EA%B7%B8%EC%97%90-%EB%8C%80%ED%95%9C-%EB%AA%A8%EB%93%A0-%EA%B2%83-11513af8b998/</guid><description>&lt;h3 id="apache-kafka의-탄생배경을-알아보자"&gt;Apache Kafka의 탄생배경을 알아보자&lt;/h3&gt;
&lt;p&gt;많은 시스템이 Apache Kafka를 메시지 큐와 실시간 데이터 처리에 이용하고 있습니다. Linkedin 에서 Apache Kafka를 개발한 &lt;a href="https://twitter.com/jaykreps"&gt;Jay Kreps&lt;/a&gt;는 &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying"&gt;이 글&lt;/a&gt;에서 &lt;strong&gt;Kafka를 개발하게 된 이유&lt;/strong&gt;와 철학을 로그라는 하나의 주제에서 출발하여 멋지게 설명하고 있습니다. 오늘은 제가 거인의 어깨 위에 올라타 그가 설명한 로그에 대한 이야기를 해볼까 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 글은 Jay Kreps가 &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying"&gt;링크드인에 기재한 내용&lt;/a&gt;을 토대로 재구성하고 원문 내용을 요약했습니다. 10년 전 글이기 때문에, 시대 배경을 상상하면서 읽으시면 더 맛있게 드실 수 있을것으로 기대하고요. 필요한 경우 제가 추가적인 내용을 기입하여 이해를 돕고자 했습니다. &lt;strong&gt;더 상세한 내용은 원본글을 참고하시기 바랍니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;원문에 나온 내용은 평범한 텍스트로 작성하였고, 이렇게 bar(|)와 같이 쓰인 문장은 제가 추가적인 내용을 덧붙인 것입니다. 그리고 저자가 표현한 문구임을 명확하게 하기 위해서 저자의 이름인 Jay Kreps를 줄여서 Jay라고 반복적으로 불렀습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="index"&gt;INDEX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Part 1. 로그&lt;/li&gt;
&lt;li&gt;Part 1. 데이터베이스의 로그&lt;/li&gt;
&lt;li&gt;Part 1. 분산 시스템의 로그&lt;/li&gt;
&lt;li&gt;Part 1. 데이터베이스와 상태복제머신&lt;/li&gt;
&lt;li&gt;Part 1. 테이블과 이벤트&lt;/li&gt;
&lt;li&gt;Part 2. 로그 기반 데이터 플로우&lt;/li&gt;
&lt;li&gt;Part 2. Extract-Transform-Load&lt;/li&gt;
&lt;li&gt;Part 2. 로그와 이벤트&lt;/li&gt;
&lt;li&gt;Part 2. 확장성있는 로그 만들기&lt;/li&gt;
&lt;li&gt;Part 3. 로그와 실시간 스트림 프로세싱&lt;/li&gt;
&lt;li&gt;Part 3. Dataflow Graph&lt;/li&gt;
&lt;li&gt;Part 3. Stateful 실시간 프로세싱&lt;/li&gt;
&lt;li&gt;Part 3. 로그 컴팩션&lt;/li&gt;
&lt;li&gt;Part 3. 로그 중심의 인프라 스택&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="part-1-로그"&gt;Part 1. 로그&lt;/h2&gt;
&lt;p&gt;로그(Log)라는 단어를 들으실 때 소프트웨어 엔지니어 분들이라면 애플리케이션에서 발생하여 에러가 포함되어 있고 시스템 상태를 표현하는 트레이스(Trace) 정보가 담겨져 있는 걸 상상하실지도 모를텐데요. Jay는 이런 정보는 애플리케이션 로그(Application Logs)라고 부르며 로그와는 철저하게 분리합니다.&lt;/p&gt;
&lt;p&gt;이 글 전체에서 반복적으로 등장하는 로그란 &lt;strong&gt;“가장 단순하게 추상화된 저장소이며, Append-only만 가능하고, 전체 이벤트가 시간 순서대로 정렬된 자료구조”&lt;/strong&gt; 를 의미합니다. 로그는 이벤트 혹은 정보의 집합이며 로그에 담긴 하나의 정보는 레코드(Record)라고 부릅니다. 아래 그림은 여기서 설명하는 로그의 모습을 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-19e010b7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-19e010b7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-w_Kx7Z9UxMt9wgl5OkvbxA-57fa7946a2ca.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-19e010b7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-w_Kx7Z9UxMt9wgl5OkvbxA-57fa7946a2ca.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Logs : Simple Abstraction of Storage, Append-only, Totally-ordered&lt;/p&gt;
&lt;p&gt;로그의 레코드에는 각각 고유의 번호가 부여집니다. 로그에서 데이터는 시간 순서대로 정렬되며 로그의 왼쪽에 존재하는 이벤트는 오른쪽에 존재하는 이벤트보다 과거에 발생되었음을 의미합니다. 로그 구조는 데이터베이스의 테이블과 크게 다르지 않습니다. 로그에서의 각 정보는 레코드라고 부르고 테이블에서는 행(row)이라고 부를 뿐입니다.&lt;/p&gt;
&lt;p&gt;로그의 구조는 단순하지만 로그는 이벤트가 “언제” 발생했는지, “무엇이” 발생했는지에 대한 중요한 단서를 가지고 있기 때문에 데이터베이스와 분산시스템에서 매우 중요한 역할을 합니다.&lt;/p&gt;
&lt;h2 id="part-1-데이터베이스의-로그"&gt;Part 1. 데이터베이스의 로그&lt;/h2&gt;
&lt;p&gt;로그는 데이터베이스가 탄생한 시점부터 매우 중요하게 쓰인 구조입니다. Jay는 여기서 로그를 누가 개발했는지는 몰라도 이진 탐색(Binary Search)처럼 너무 쉽고, 너무 당연해서 초기 발명자는 이를 기록할만한 가치를 못찾은 것으로 생각했습니다.&lt;/p&gt;
&lt;p&gt;데이터베이스는 인덱스와 테이블 내의 데이터가 장애 상황에서 안전하게 복구할 수 있도록 데이터를 변경하기 전에 “무엇을 변경해야 하는지”를 로그에 기록하는데요. 대부분의 데이터베이스에서 이를 WAL(write-ahead-logs) 라고 부릅니다.&lt;/p&gt;
&lt;p&gt;이후 시간이 지나서 데이터베이스 분야에서는 ACID원칙이라는 개념이 태동하였고, 여러 인스턴스로 데이터를 복제하도록 발전했습니다. 로그라는 존재가 “무엇을”, “언제”라는 정보를 순서대로 저장했기 때문에 원격 복제를 구현할 때 매우 핵심이 됩니다. Mysql, PostgreSQL은 모두 자체적인 로그 전송 프로토콜(log shipping protocol)을 지니고 있고, 복제 노드(Replica)에 이 로그를 전송해서 데이터 복제를 수행합니다.&lt;/p&gt;
&lt;p&gt;복제 노드(Replica)가 메인 노드(Primary)노드에게 데이터를 구독한다는 개념은 Primary/Secondary모델 혹은 Leader/Follower모델 이라고 부르는데요. 거의 모든 데이터베이스가 이런 복제 모델을 택하고 있습니다. 이런 추상화는 데이터베이스에만 한정되지 않고 데이터 플로우와 실시간 처리 및 거의 모든 메시징 시스템의 매우 이상적인 모습입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-99be86a4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-99be86a4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NHImfhzJA5EgMu2TXN3nSw-110b380b9fc0.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-99be86a4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NHImfhzJA5EgMu2TXN3nSw-110b380b9fc0.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Primary/Secondary 복제 모델&lt;/p&gt;
&lt;h2 id="part-1-분산-시스템의-로그"&gt;Part 1. 분산 시스템의 로그&lt;/h2&gt;
&lt;p&gt;데이터를 순서대로 저장한다는 것, 데이터를 분산시켜서 복제한다는 로그의 개념은 분산시스템에서 더욱 중요합니다. 분산 시스템의 핵심은 어떤 변경사항을 어떤 순서로 진행해야 하는지 합의하는 과정입니다. 분산 시스템이 로그를 중심으로 설계된 데에는 &lt;strong&gt;상태 복제 머신 원칙(State Replication Machine Principles)&lt;/strong&gt;이 작용합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;상태 복제 머신 원칙&lt;/strong&gt;
두 개의 프로세스가 동일한 상태에서 출발해서, 동일한 입력을 같은 순서로 입력 받는다면 최종적으로 같은 상태를 가지게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;결정성(Determinitic)&lt;/strong&gt;
결정성은 컴퓨터 과학분야에서 시간에 의존하지 않는 프로세싱을 의미합니다. 예를 들어 특정 순간에 A 주식의 가격을 가져오는 함수는 언제 실행하냐에 따라 결과값이 달라지기 때문에 비결정성입니다. 상태 복제 머신은 서로 다른 노드가 하나의 값에 합의하기 위해서 반드시 프로세싱은 결정적으로 동작해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;현대 많은 분산 시스템들은 내부에 합의 알고리즘을 이용한 상태 복제 머신을 가지고 있습니다. 쿠버네티스의 etcd, Apache Kafka는 주키퍼를 쓰고 있고, Kafka최근 빌드는 Raft 알고리즘을 채용했습니다. 비트코인으로 유명세를 탄 블록체인은 블록 단위의 로그를 노드들끼리 복제하고 비잔틴 환경에서 합의하는 시스템입니다. 멀티-플레이어 게임 서버또한, 유저가 바라보는 세계를 동일하게 합의하는 시스템으로 상태 복제 머신으로 볼 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Raft는 2014년에 스탠포드 대학의 연구원들이 발표한 합의알고리즘으로 많은분산시스템이 채용하는 상태복제머신 입니다. 아래 그림은 Raft논문에서 표현된 상태 복제 머신의 구조도를 보여주는데요. &lt;a href="https://ebrary.net/64883/computer_science/consensus_algorithms_total_order_broadcast"&gt;근본적으로 분산 시스템에서 여러 노드가 동일한 값을 ‘합의(Consensus)&amp;lsquo;하는 것은 전체 순서가 동일한 로그를 복제하는 것과 같습니다.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5cac088d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5cac088d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PYNHLNzTlR_MEJFXcjtWiQ-c2cc06848ca5.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5cac088d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-PYNHLNzTlR_MEJFXcjtWiQ-c2cc06848ca5.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이런 로그 복제 시스템의 아름다운 점은 &lt;strong&gt;로그의 인덱스 혹은 타임스탬프가 복제 노드들의 상태를 표현하는 역할을 한다는 겁니다.&lt;/strong&gt; 분산 시스템에 연결된 수많은 노드들의 현재 상태를 표현하는 데에는 로그의 몇 번 째 레코드를 프로세싱 했는가? 라는 단순한 질문으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h2 id="part-1-데이터베이스와-상태복제머신"&gt;Part 1. 데이터베이스와 상태복제머신&lt;/h2&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-92d4ef50"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-92d4ef50"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9VCcuEGeJUq3NfNdbWcouQ-33de8823c989.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-92d4ef50"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-9VCcuEGeJUq3NfNdbWcouQ-33de8823c989.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;리더-팔로워 복제와 상태복제머신의 차이&lt;/p&gt;
&lt;p&gt;데이터베이스 연구자들은 &lt;strong&gt;물리적 로그&lt;/strong&gt;와 &lt;strong&gt;논리적 로그&lt;/strong&gt;를 분리해서 생각합니다. 물리적 로그는 변경된 내용 그 자체를 의미하고, 논리적 로그는 변경을 이끌어내는 명령문 같은 것을 말합니다. 예를 들면 SQL구문이 있습니다. 분산 시스템은 위에서 바라본 두 개의 복제 형태를 분리하는데요. 리더-팔로워 모델에서의 팔로워는 물리적 로그 형태를 그대로 적용하는 수동적인 복제 모습을 띠고, 상태 복제 머신에서는 각 참여자가 능동적으로 논리적 로그를 복제 받아서 같은 명령을 수행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a4af864f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a4af864f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bDLIEkb_RlxQnMkw55vj-w-2c632d42ccf4.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a4af864f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bDLIEkb_RlxQnMkw55vj-w-2c632d42ccf4.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;명령을 복제하는 머신과 최종 결과만을 복제하는 머신&lt;/p&gt;
&lt;p&gt;간단하게 하나의 값만을 가지고 있는 시스템이 있다고 상상해보면, 상태 복제 머신은 상태를 변경할 수 있는 연산을 명령어 형태로 입력받아서 능동적으로 처리하고, 리더-팔로워 모델에서 팔로워들은 수동적인 입장에서 완성된 데이터를 받습니다. 위 그림은 두 모델의 차이를 보여줍니다. 이 예제는 또한 &lt;strong&gt;왜 로그의 순서가 중요한지 보여줍니다.&lt;/strong&gt; 순서가 다르게 입력이 되면 완전히 다른 결론이 도출되고 일관성이 깨집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;리더-팔로워 모델은 RDBMS에서 많이 사용하기 때문에 친숙하실지도 모릅니다. 상태복제는 분산 시스템에서 많이 이용됩니다. 대표적으로 쿠버네티스 내부에서 메타데이터 및 상태 정보를 저장하는데 쓰이는 etcd는 Raft로 구현된 상태복제머신입니다. RDBMS의 복제 모델과 상태 복제 머신은 무엇이 다른걸까요? 왜 쿠버네티스는 다른 DB를 데이터 저장소로 이용하지 않을까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ac9dc42d"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ac9dc42d"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wmQpwcqBLTHE9DiU3xxKJQ-0904e39df2b3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ac9dc42d"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wmQpwcqBLTHE9DiU3xxKJQ-0904e39df2b3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Primary-Secondary Model vs State Machine Replication&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;리더-팔로워 모델은 요청을 수행하는 리더에 장애가 발생하면, 팔로워 노드 중 하나가 다시 리더로 선출됩니다. 이 과정에서 리더가 요청을 복제하지 못하고 장애가 발생하면 클라이언트는 본인의 요청이 유실되었다고 느끼게 됩니다. 왜냐면 선대 리더가 ACK 메세지를 주었거든요. 즉 내결함성을 갖추지 못한겁니다. 일부 데이터베이스에서는 &lt;strong&gt;동기식 복제&lt;/strong&gt;라고 해서 팔로워가 트랜잭션을 커밋한 시점에 리더가 응답을 주도록 할 수 있긴 합니다, 하지만 이 경우 일관성은 보장되나 팔로워 노드가 장애 상황이면 리더는 팔로워가 복구 될 때 까지 대기해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;반면, 상태 복제 머신에 쓰이는 합의 알고리즘은 Non-Byzantine 환경에서 일부 장애를 허용하는 내결함성을 갖추었습니다. 클러스터 내 전체 노드에서 정족수(quorum)만큼 복제가 완료된 경우 해당 시점의 메시지를 커밋하고, 완벽하게 커밋된 상태의 정보만 클라이언트에게 응답을 줍니다. 따라서, RDBMS보다 &lt;strong&gt;높은 가용성(High availability)&lt;/strong&gt;과 &lt;strong&gt;강한 일관성(Strong consistency)&lt;/strong&gt;을 제공해야 하고 읽기-중심의 애플리케이션인 경우 상태복제머신을 이용할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="part-1-테이블과-이벤트"&gt;Part 1. 테이블과 이벤트&lt;/h2&gt;
&lt;p&gt;은행 시스템에서 로그는 입출금 기록이며, 테이블은 현재 내 계좌의 잔액과 동일합니다. 재밌는 점은 로그에 쓰인 이벤트(Event)와 테이블은 동일성을 띈다는 겁니다. 입출금 기록으로 현재 계좌의 상태를 알 수 있고 반대로 계좌의 변경내역을 다시 로그에 이벤트를 기록할 수 있습니다. 서로 기록하는 방식은 다르지만 동일성을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;물론 로그가 더 중요한 역할을 합니다. 로그는 현재 상태의 최종본을 구할 수 있을 뿐 아니라, 특정 시점의 중간 상태를 만들 수도 있으며 테이블의 모든 이전 상태를 백업할 수 있습니다. 우리에게 친숙한 버전 관리 시스템인 Git도 이런 로그를 복제하는 시스템으로, 소스코드에서 첫번째 커밋이 작성된 시점부터 현재까지 모든 순간의 중간 상태를 복원할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;로그와 테이블의 동일성이라는 특징을 이용하기 위해 테이블의 변경 내역을 자동으로 이벤트로 만들어주는 &lt;strong&gt;변경 데이터 캡쳐(CDC)&lt;/strong&gt;라 불리는 분야가 있습니다. 대표적인 도구로 Kafka Connect가 있는데요. MySQL, ElasticSearch 등 다양한 데이터 소스에서 발생된 변경 내역을 Kafka에 이벤트로 전송해주고, 이벤트를 다시 테이블로 만들어주는 기능을 가지고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cac43924"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cac43924"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-kC5whZdLtM8XID6K-032058c52c7d.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cac43924"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-kC5whZdLtM8XID6K-032058c52c7d.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developers.redhat.com/blog/2020/04/14/capture-database-changes-with-debezium-apache-kafka-connectors"&gt;Ref. Red Hat developer | Capture database changes with Debezium Apache Kafka connectors&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="part-2-로그-기반-데이터-플로우"&gt;Part 2. 로그 기반 데이터 플로우&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Data_integration"&gt;데이터 통합(Data Ingeration)&lt;/a&gt;은 여러 데이터 소스들의 하나의 통합된 데이터 저장소로 모으는 것을 말합니다. 데이터 소스를 한 데 모아서 ETL 프로세싱을 거쳐 데이터 웨어하우스로 모으는 과정을 지칭하는 말인데요.&lt;/p&gt;
&lt;p&gt;Jay는 원문 글에서 &lt;strong&gt;신뢰할 수 있는 데이터 전달 방식&lt;/strong&gt;이 없다면 하둡 클러스터나 빅데이터 시스템들은 그냥 비싸고 사용하기 어려운 저장소일 뿐이라고 표현했습니다. 당시 많은 기업들은 빅 데이터(Big Data)라는 유행어에만 매료되어 있을 뿐 신뢰할 수 있는 데이터 플로우를 구축하는 일에는 관심이 적었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1e68813e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1e68813e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xfmqJZ7Dz8htPQP9p-gGxw-04f672641749.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1e68813e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xfmqJZ7Dz8htPQP9p-gGxw-04f672641749.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;기존의 데이터 통합 과정&lt;/p&gt;
&lt;p&gt;당시에는 데이터 통합을 위해 데이터 소스와 도착지가 End-to-End로 연결되는 일이 많았습니다. 서버에서 로그를 파일로 내리면 파일째로 가져가는 방식이 많이 사용되었죠. 이 과정에서 일부는 유실되기도 하고 강하게 커플링 되어 있기 때문에 한 곳의 장애가 다른 곳으로 쉽게 전파되기도 합니다. Jay는 많은 기업이 신뢰할 수 있는 완성형 데이터 플로우에 대한 고민은 적게 하면서 바로 빅 데이터와 같은 고급 데이터 모델링 테크닉을 적용하고 싶어하는 것 같다고 말했습니다.&lt;/p&gt;
&lt;p&gt;데이터 통합 과정은 &lt;strong&gt;이벤트 데이터&lt;/strong&gt;와 &lt;strong&gt;특수 목적 데이터베이스&lt;/strong&gt;의 발전으로 더욱 어려워 졌습니다. 우선, 이벤트 데이터란 웹/모바일 사용자의 행위 로그, 데이터 센터 장비의 모니터링 로그 등을 말하며 전통적인 데이터베이스에서는 감당하기 어려운 대규모의 데이터가 생산됩니다. 그리고, 요즘에는 서비스가 RDBMS만 사용하는 경우가 많이 없죠. OLAP, 검색 엔진, 배치 프로세싱, 오브젝트 스토리지, 캐시, 그래프 데이터, 벡터 데이터 등 특수한 목적으로 활용하는 데이터베이스가 많아졌는데요. 이들 사이의 인터페이스가 모두 달라서 데이터를 통합하는데 어려움을 줍니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;그런데 잠깐, 위에서 우리는 로그와 테이블은 동일성을 가지며, 모든 데이터는 결정적이며 순서가 보장된 형태의 선형 자료구조로 바꿀 수 있다고 했습니다. 그리고 분산 시스템과 데이터베이스에서는 요청을 로그에 저장/복제하고 이 이벤트를 읽어서 내부 데이터를 변경하는 시스템입니다. 그렇다면, 데이터 시스템들의 메모리 혹은 디스크로만 상주해있던 로그를 네트워크의 중간 미들웨어로 추상화 시킨다면 어떨까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-acc3e340"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-acc3e340"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F9HUILSOgl7l9QK8m85COw-a8479f96cc28.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-acc3e340"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F9HUILSOgl7l9QK8m85COw-a8479f96cc28.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;데이터베이스가 가지고 있는 로그를 미들웨어로 추상화한 모습&lt;/p&gt;
&lt;p&gt;논리적인 데이터 소스들은 로그로 모델링 할 수 있습니다. 그리고 데이터 소스는 이벤트 로그를 만드는 애플리케이션이며 데이터베이스의 테이블은 해당 이벤트로 인한 변경 사항을 반영하는 서버입니다. 따라서, 어떠한 시스템이라도 위의 그림처럼 로그를 구독해서 각자의 테이블을 만들 수 있습니다. 이때 구독자(Subscriber)는 캐시가 될 수도 있고 하둡이 될 수도 있습니다. 데이터 시스템의 종류는 상관 없습니다. 로그가 곧 데이터이니까요.&lt;/p&gt;
&lt;p&gt;이런 구조는 데이터를 발행하는 프로듀서와 데이터를 읽는 컨슈머 사이의 결합성을 느슨하게 합니다. 프로듀서와 컨슈머 사이의 데이터를 쓰고/읽는 속도가 달라서 발생하는 여러 문제들을 해결해주는 중간 버퍼의 역할도 해줍니다. &lt;strong&gt;이벤트를 푸시하는 기존의 방식은 데이터베이스가 메인터넌스로 잠시 중단되는 상황에서 문제가 되는데요. 이벤트를 구독하는 모델에서는 각 컨슈머가 스스로 본인의 페이스를 조절합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기서 로그 미들웨어는 로그와 동일한 속성을 가집니다. Append-only이며 리플레이가 가능하고, 순서가 보장되어있습니다. Jay는 &lt;strong&gt;발행/구독(Pub/Sub)&lt;/strong&gt;과 로그는 다른 걸로 간주합니다. 발행/구독 모델은 간접적인 메시징의 전달방식일 뿐 무엇을 보장해주는 지는 표현하지 않죠. 반면, 로그라는 용어는 영속적이며 강한 순서 보장의 시맨틱을 내포합니다.&lt;/p&gt;
&lt;h2 id="part-2-extract-transform-load"&gt;Part 2. Extract-Transform-Load&lt;/h2&gt;
&lt;p&gt;여기서는 전통적인 ETL 파이프라인을 비판합니다. ETL 프로세스에서는 데이터를 가져오고(Extract) 목적 시스템이 읽기(Load) 편한 형태로 변환(Transform) 합니다. 문제는 이 파이프라인이 다른 시스템에 데이터를 공급하기에는 유용하지 않을 가능성이 크다는 겁니다. 모든 데이터 소스를 모아서 하나의 데이터 웨어하우스를 ETL로 구축하는 건 실현하기 매우 어렵습니다.&lt;/p&gt;
&lt;p&gt;만약 통합된 로그 시스템과 데이터를 잘 정의해서 수집한다면 새로운 데이터 시스템이 할 일은 그저 로그 파이프라인에 연결하는 단일 프로세스만 구축하는 일이 될 겁니다.&lt;/p&gt;
&lt;h2 id="part-2-로그와-이벤트"&gt;Part 2. 로그와 이벤트&lt;/h2&gt;
&lt;p&gt;로그 파이프라인의 부수효과로는 &lt;strong&gt;디커플링&lt;/strong&gt;과 &lt;strong&gt;이벤트 기반 아키텍처&lt;/strong&gt;입니다. 요즘에는 이 개념이 매우 중요하죠. 예전에는 서버에서 이벤트를 파일로 남기고 이 파일을 각 데이터베이스가 스크랩해가는 방식을 채용했습니다. 이는 데이터의 흐름이 데이터베이스의 기능이나 배치 프로세싱 스케쥴에 강하게 결합되어 있습니다. Kafka는 이 방식을 &lt;strong&gt;이벤트 기반(Event-driven)&lt;/strong&gt;으로 바꾸는데 획기적입니다. 수백 혹은 수천개의 이벤트 타입을 정의하고 각 데이터 프로듀서는 이 형식에 맞춰 이벤트를 전송할 뿐이죠.&lt;/p&gt;
&lt;p&gt;이해를 돕기위해 링크드인의 채용 공고(Job Posting)기능을 예시로 듭니다. 채용 공고는 사용자에게는 정보만 보여주면 되지만, 뒷단에서는 아래의 요구사항을 충족하기 위해 이벤트를 기록해야 합니다.&lt;/p&gt;
&lt;p&gt;오프라인 프로세싱을 위해 하둡과 데이터 웨어하우스로 전송한다.&lt;/p&gt;
&lt;p&gt;특정 사용자가 컨텐츠 스크래핑을 하지 않는지 검사하기 위해 페이지 뷰를 카운팅 해야 한다. (e.g. 채용 공고를 복사해서 다른 채용 사이트로 가져가는지)&lt;/p&gt;
&lt;p&gt;채용 공고를 낸 직원이 보는 분석 페이지에서 통계를 보여주기 위해 데이터를 집계한다.&lt;/p&gt;
&lt;p&gt;특정 사용자에게 과도하게 중복 추천을 하지 않기 위해 어떤 채용 공고를 봤는지 이벤트를 기록해야 한다.&lt;/p&gt;
&lt;p&gt;어떤 채용 공고가 있는지 추적해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cbd6cd6e"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cbd6cd6e"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Oo3Ewg857AffXXzZ8LMAhQ-a755886bfd80.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cbd6cd6e"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Oo3Ewg857AffXXzZ8LMAhQ-a755886bfd80.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;End-to-End 이벤트 전송 vs 로그 파이프라인을 이용하는 방식&lt;/p&gt;
&lt;p&gt;이 요구사항을 구현하기 위해 데이터를 직접 푸시하면 시스템의 복잡성이 증가하고 각 데이터베이스 시스템들과 강하게 결합하게 됩니다. 시스템 중 하나라도 장애가 발생하면 선택할 수 있는 건 기록을 포기하거나 장애가 복구될때 까지 기다리는 것 뿐입니다. 이벤트-기반 접근법은 이런 문제상황을 단순화 시켜버립니다. 채용 공고를 개발하는 담당자는 &lt;strong&gt;‘사실(fact)’&lt;/strong&gt; 정보의 이벤트만 로그로 기록하고 컨슈머들은 잘 정의된 로그를 활용하기만 하면 됩니다. 컨슈머들은 이벤트를 보고 실시간 추천을 할 수도 있고, 보안 관제, 분석 시스템, 데이터 웨어하우스 등 다양한 용도로 사용합니다.&lt;/p&gt;
&lt;h2 id="part-2-확장성있는-로그-만들기"&gt;Part 2. 확장성있는 로그 만들기&lt;/h2&gt;
&lt;p&gt;지금까지는 로그라는 자료구조를 추상화해서 미들웨어로 만들면 어떤 장점이 있는지 다루었습니다. 그렇다면 이러한 로그 시스템은 어떻게 만들 수 있을까요? 확장성 있고(Scalable), 빠르고, 저렴한 로그 시스템을 만들 수 없다면 이러한 주장은 그저 상상에서 그치겠죠.&lt;/p&gt;
&lt;p&gt;Apache Kafka는 확장성 있는 로그를 만들기 위해 3가지 트릭을 이용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로그를 파티셔닝한다&lt;/li&gt;
&lt;li&gt;배치 읽기/쓰기로 최적화 한다&lt;/li&gt;
&lt;li&gt;불필요한 데이터 카피를 방지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="파티셔닝"&gt;파티셔닝&lt;/h3&gt;
&lt;p&gt;수평 확장을 위해 로그를 파티션 단위로 분리합니다. 로그는 원래 전체 순서가 보장된 형태를 일컫는 말이지만 Apache Kafka에서는 파티션 단위의 순서만 보장합니다. 로그를 기록할 때 데이터의 파티션 키를 기록해야 하는데요. 유저 키(UserID)를 파티션 키로 잡으면 해당 유저가 발생한 이벤트에 대해서는 순서가 부분적으로 보장됩니다. 이 파티션은 브로커들에게 데이터가 복제되어서 내결함성을 갖추고, 특정 순간에는 하나의 브로커가 리더 역할을 하며 리더에 장애가 발생하면 남아있는 노드 중 하나가 리더로 선출됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ab5b58ec"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ab5b58ec"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F2uNHyIsbZkWR1yO6GZkpg-e2b41e7b7044.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ab5b58ec"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F2uNHyIsbZkWR1yO6GZkpg-e2b41e7b7044.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.confluent.io/kafka/design/replication.html"&gt;Ref. Confluent : Kafka Replication and Committed Messages&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="배치-읽기쓰기"&gt;배치 읽기/쓰기&lt;/h3&gt;
&lt;p&gt;파일 시스템을 다룰 때 데이터를 선형적으로 읽고/쓰는 패턴이 많아서 배치를 사용한 최적화가 쉽습니다. 로그 또한 연속된 데이터를 한꺼번에 읽고 쓰는 패턴이기 때문에 Kafka는 이 개념을 적극적으로 사용합니다. 클라이언트가 서버로 로그를 기록할 때, 반대로 클라이언트가 서버로부터 로그를 읽을 때, 서버간 데이터를 복제할 때 등 다양한 데이터 교환이 배치로 동작합니다.&lt;/p&gt;
&lt;h3 id="불필요한-데이터-복사-방지"&gt;불필요한 데이터 복사 방지&lt;/h3&gt;
&lt;p&gt;Kafka는 메모리, 디스크, 데이터 전송에 이용되는 데이터 표현을 하나의 단일 바이너리 포맷으로 통합해서 이용합니다. 이는 zero copy data transfer 등 다양한 최적화를 이용하기 쉽게 합니다.&lt;/p&gt;
&lt;h2 id="part-3-로그와-실시간-스트림-프로세싱"&gt;Part 3. 로그와 실시간 스트림 프로세싱&lt;/h2&gt;
&lt;p&gt;스트림 프로세싱은 이벤트가 연속적이며 경계가 무한히 확장된 상황에서 데이터를 처리하는 방법을 다루는 데이터 처리 패러다임입니다. RDBMS에서 SQL로 데이터를 핸들링 하거나 배치 처리 도구를 이용하는 건 특정 시간에 고정된 데이터 셋에 대해 연산을 수행하는 반면, 스트림 프로세싱은 실시간으로 발생하는 데이터를 처리합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;많은 연구자들은 스트림 프로세싱이 배치 프로세싱을 대체할 수 있을 것으로 기대하고 있습니다.&lt;/strong&gt; Jay는 그동안 스트림 프로세싱보다 배치 프로세싱이 더 많이 연구된 데에는 데이터를 수집하는 방식의 차이 때문이라고 봅니다. 국가에서 인구조사를 하는 방식을 생각해보죠.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;과거 인구조사는 각 지방관료가 지역마다 집을 하나씩 방문해가면서 데이터를 모아 중앙 행정기관으로 전송하는 방식을 따릅니다. 시스템으로 말하자면 애플리케이션이 로그를 주기적으로 덤프하고 Logstash같은 도구들이 한 번에 가져가는 것과 같죠. 이런 데이터 수집 방식을 사용하면 자연스럽게 기업의 데이터 처리 방식은 배치 모델이 될 수 밖에 없습니다.&lt;/li&gt;
&lt;li&gt;만약, 인구조사 방식이 처음부터 실시간으로 데이터를 전송하는 방식을 사용했다면 자연스럽게 계산 모델도 연속적인 데이터를 어떻게 처리할 것인지 고민했을 겁니다. 현재 링크드인은 거의 모든 시스템이 배치 모델로 데이터를 수집하지 않습니다. 데이터 수집 방식이 변경됨에 따라 스트림 프로세싱에 대한 관심이 증가했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;배치 프로세싱 분야는 계산 모델을 Map/Reduce로 통합시켰고, RDBMS 분야에서는 벤더와 상관없이 SQL을 질의문으로 사용합니다. Jay는 이런 통합이 스트림 프로세싱 세계에서도 가능하다고 봅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;최근에는 SQL문을 스트림 처리 도메인에 어울리는 표준을 만들려고 &lt;a href="https://www-conf.slac.stanford.edu/xldb2016/talks/published/Weds_9_Hyde_calcite-streaming-sql-xldb-2016.pdf"&gt;Streaming SQL&lt;/a&gt;이라는 분야가 연구되고 있고, 대표적으로는 Apache Calcite가 있습니다. 구글이 개발한 Apache Beam에서는 PCollection이라는 객체로 Flink, Spark등 다양한 스트림 처리기를 추상화 시킵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.yes24.com/Product/Goods/102360104"&gt;스트리밍 시스템(Streaming Systems)&lt;/a&gt;를 집필한 저자들은 &lt;strong&gt;배치는 스트림 프로세싱의 하위 분야이며 잘 만들어진 스트림 프로세싱은 배치 처리를 대체할 수 있다고 주장합니다.&lt;/strong&gt; Jay 또한 비슷한 생각을 가지고 있는데요. 하루 단위로 데이터를 배치 처리하는 모델은 스트림 시스템에서는 현재 시간에서 과거 24시간 까지의 윈도우를 처리하는 것과 같습니다. 아직까지 완전히 배치 도구를 대체하지 못한 데에는 이러한 대규모 데이터를 실시간으로 수집할 수 있는 환경이 지금까지는 갖추어지지 않아서 학계의 연구 대상이 되지 못했기 때문이라고 진단합니다. 현재 아직까지 대세로 자리잡은 도구는 없지만 &lt;a href="https://beam.apache.org/"&gt;Apache Beam&lt;/a&gt; 등이 통합된 인터페이스를 제공하려는 시도를 하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="part-3-dataflow-graph"&gt;Part 3. Dataflow Graph&lt;/h2&gt;
&lt;p&gt;[Part 2]에서는 프로듀서가 발행한 이벤트르 저장할 통합된 자료구조로써의 Kafka의 역할을 설명했는데요. 스트림 프로세싱 세계에서는 로그를 처리하는 프로세서의 출력값이 다른 프로세서의 입력으로 주어질 수 있습니다. 데이터가 전체 시스템에서 어떻게 처리되는지 모델링 하는 방법을 데이터 플로우 모델이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f1318e05"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f1318e05"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EE4DE7wGVGR9RiebRG7FIw-bc85d1a40e98.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f1318e05"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-EE4DE7wGVGR9RiebRG7FIw-bc85d1a40e98.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Dataflow Model&lt;/p&gt;
&lt;p&gt;데이터 플로우의 각 프로세서를 End-to-End로 연결하면 확장성 있고 신뢰성있는 데이터 플로우를 만들 수 없습니다. 프로세서 A와 프로세서 B가 처리하는 속도가 서로 다르다고 해봅시다. A는 1초에 30개의이벤트를 생산하는데 B는 1초에 10개의 이벤트만 소화할 수 있습니다. 이 때, 프로세서가 할 수 있는 전략은 1. 데이터를 드랍시키거나 2. 블럭킹을 하거나 3. 버퍼링을 하거나 이 세가지 뿐입니다. 데이터를 드랍시키는 건 말도 안되고 그렇다고 블럭킹시키면 그래프에서 한 정점에서 발생한 대기가 그래프 전체로 전파될 위험이 있습니다. 가장 좋은 전략은 버퍼링을 시키는 건데요. 스트림 프로세서들의 입력과 출력 데이터를 저장하는 중간 버퍼로 Kafka를 이용하면 좋습니다. Kafka는 또한 데이터의 부분 정렬 기능을 매우 중요하게 생각하고 있죠.&lt;/p&gt;
&lt;h2 id="part-3-stateful-실시간-프로세싱"&gt;Part 3. Stateful 실시간 프로세싱&lt;/h2&gt;
&lt;p&gt;실시간 프로세싱이 Stateless 하다면 얼마나 좋았겠냐만은, 실제 세계에서는 Stateful한 연산이 필요한 경우가 많습니다. 쇼핑몰에서 오픈 이래 현재까지 &lt;strong&gt;유저 한명당 평균 판매금액&lt;/strong&gt;을 실시간으로 계산하고 싶다고 한다면 전체 데이터에 대해 매 순간 집계할 수 없기 때문에 과거에서 계산한 이력, 즉 컨텍스트를 가지고 다녀야 합니다.&lt;/p&gt;
&lt;p&gt;클릭 이벤트를 수신받고 유저 정보를 데이터베이스에서 가져와서 조인하는 경우 컨텍스트를 저장해야 합니다. 왜 컨텍스트 정보가 필요할까요? 스트림 프로세서에 장애가 발생해서 디스크가 날아간 경우 로그를 순차적으로 읽으면서 재처리를 해야 하는데요. 만약 유저가 미래에 탈퇴한 경우 과거에 수행한 결과와 현재 처리한 결과가 달라집니다. 즉, 결정성을 지킬 수 없게 되죠.&lt;/p&gt;
&lt;p&gt;두 가지 대안이 있습니다. (1) 메모리에 저장한다, 이 경우 스트림 프로세서가 중간에 부러진 경우 정보가 휘발됩니다. (2) S3같은 원격 저장소에 저장한다, 프로세서의 데이터 Locality가 낮아서 많은 네트워크 round-trip을 요구합니다. 테이블과 로그가 사실은 동일한 것이기 때문에 스트림 프로세서가 바라보는 테이블을 로그로부터 구성하는 아이디어가 있습니다. (e.g. &lt;a href="http://samza.incubator.apache.org/learn/documentation/0.7.0/container/state-management.html"&gt;Apache Samza&lt;/a&gt;) 유저 데이터베이스가 CDC를 이용해 변경 내역이 Kafka에 로그로 기록되어 있다면 스트림 프로세서는 데이터베이스를 직접 호출하는 것이 아니라 CDC의 로그 기록을 읽어서 테이블을 내부적으로 만들고 이를 조인하자는 내용입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apache Flink와 구글 클라우드의 Dataflow에서는 각 스트림 프로세서가 처리한 출력 결과를 체크포인트라는 이름으로 저장하고 있다가 장애가 발생하면, 마지막으로 처리한 체크포인트를 읽어와서 재처리를 수행합니다. 스트리밍 시스템에서는 Exactly Once Semantic이 매우 중요합니다. &lt;a href="https://medium.com/rate-labs/%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1%EC%9D%98-%EA%B8%B4-%EC%97%AC%EC%A0%95%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EC%A0%95%ED%91%9C-with-flink-8e3953f97986"&gt;스트림 프로세싱에 대한 자세한 내용은 제 지난 포스팅이 도움을 줄 수 있을 것 같아요 :)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5c443cd4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5c443cd4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bGagNU5xZB2kljveoGI2ZQ-4753851ccbac.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5c443cd4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-bGagNU5xZB2kljveoGI2ZQ-4753851ccbac.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://flink.apache.org/2020/10/15/from-aligned-to-unaligned-checkpoints-part-1-checkpoints-alignment-and-backpressure/"&gt;Ref. Apache Flink : From Aligned to Unaligned Checkpoints — Part 1: Checkpoints, Alignment, and Backpressure&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="part-3-로그-컴팩션"&gt;Part 3. 로그 컴팩션&lt;/h2&gt;
&lt;p&gt;당연하게도 디스크 공간이 제한되어 있으므로 로그를 영구히 저장할 수는 없습니다. 그래서 로그를 적절히 제거해야 하는데요. 이를 위한 2가지 전략이 있습니다. 보관 주기를 설정하는 것과 로그 컴팩션입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;(보관 주기 설정)&lt;/strong&gt; 유저의 클릭 이벤트 같은 경우 과거의 데이터가 꼭 필요하지 않다면 리텐션 기간을 정해서 로그의 부관주기를 결정하고 특정 임계 시간을 넘긴 오래된 레코드는 삭제합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;(중요!)&lt;/strong&gt; 로그와 테이블의 동일성을 설명하면서 로그가 현재 상태를 구성하거나 과거 시점의 데이터를 복원되는데 활용할 수 있다는 점을 강조했었죠. 그런데, 시간을 기준으로 데이터를 지워버리면 데이터를 완전히 복구하거나 과거시점으로 돌아갈 수 없습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-89a90955"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-89a90955"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-v4l9CTMvDQfNB0yzByqdfQ-837c42ce3772.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-89a90955"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-v4l9CTMvDQfNB0yzByqdfQ-837c42ce3772.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;UserID를 파티션키로 잡은 Kafka 토픽의 모습&lt;/p&gt;
&lt;p&gt;로그의 보관 주기를 이용해서 로그를 지우는 경우 특정 유저의 이벤트 전체가 제거될 수 있습니다. 그러면 (2)번에서 설명한 테이블과 로그의 동일성 특징을 이용할 수 없겠죠. 그래서 카프카는 같은 파티션 키를 가진 이벤트 중에서 오래된 것을 제거하는 옵션을 지원합니다. 다시 말하자면 같은 파티션 키마다 최근의 이벤트는 유지되도록 해주는데요. 아래 상황에서 유용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데이터 변경 구독 :&lt;/strong&gt; 캐시, 검색엔진, 하둡 등 다양한 데이터 시스템에서 최근의 데이터만을 다루는 경우에는 과거 데이터를 삭제해도 되지만 장애 상황에서 온전한 데이터셋을 복구해야 하는 경우&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 소싱 :&lt;/strong&gt; 컴팩션을 이용하면 각 키에 대해 최신 상태는 보관된다는 것을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고가용성을 위한 저널링 :&lt;/strong&gt; 로컬 연산을 수행하는 프로세스의 변경된 상태를 로그로 저장해서 내결함성을 챙길 수 있습니다. 프로세스에서 장애가 발생하면 다른 프로세스가 해당 상태를 이어받아서 이어서 처리하는 방식입니다. 대표적으로 카운팅, 집계 등 “group-by” 스타일의 프로세싱 스타일이 있는데요. Kafka Stream이 이를 위해 로그 컴팩션을 이용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-aee962ef"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-aee962ef"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-2WQ8GLS8IkLKP4KB-5e10bf85e832.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-aee962ef"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-2WQ8GLS8IkLKP4KB-5e10bf85e832.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.confluent.io/kafka/design/log_compaction.html"&gt;Ref. Kafka : Log Compaction&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="part-3-로그-중심의-인프라-스택"&gt;Part 3. 로그 중심의 인프라 스택&lt;/h2&gt;
&lt;p&gt;우리가 분산 시스템을 스스로 개발한다고 생각해봅시다. 분산 시스템은 사실 로그를 복제하는 시스템이기 때문에 내부에 로그를 구현하려면 아래의 특징을 만족하는 로그를 직접 구현해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터의 일관성 보장&lt;/li&gt;
&lt;li&gt;노드 간 데이터 복제&lt;/li&gt;
&lt;li&gt;데이터의 커밋시맨틱 제공 (복제가 온전히 수행되고 나서 writer에게 ACK를 주는 경우)&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&gt;이 항목은 실제로 분산 데이터 시스템이 해야 하는 많은 일들을 표현한 겁니다. 이 문제는 Kafka가 해결해주었으니, 이제 분산 시스템을 구현하기 위해 남은 일은 인덱싱 전략과 Query API를 만드는 일입니다.&lt;/p&gt;
&lt;p&gt;이건 로그와 Serving Layer 두 가지로 나뉩니다. 로그는 변경 내역을 순차적으로 저장하고 서빙 노드(serving node)는 인덱스를 구성하고 사용자에게 Query API를 제공하는 일을 합니다. 예를 들어 검색 엔진은 Inverted Index가 필요할 것이고 키-밸류 저장소는 이진 트리나 SSTABLE이 필요할 겁니다. 이를 추상화 시키면 아래와 같은 모습이 될 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-99b99281"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-99b99281"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Stq5_qlsYoR_mJcQKPsa7Q-25ae1f1660f2.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-99b99281"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Stq5_qlsYoR_mJcQKPsa7Q-25ae1f1660f2.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;로그와 서빙 레이어&lt;/p&gt;
&lt;p&gt;서빙 노드는 단일 프로세스로 개발해도 괜찮습니다. 로그가 단일 진실 공급원(Single Source of Truth) 이기 때문이죠. 분산 시스템을 직접 개발할 때 처럼 로그 복제, 장애 복구, 파티션 리밸런싱 같은 일은 우리가 신경 쓸 필요가없을 겁니다. 이 모든 복잡성은 로그 시스템에게 위임하고 다양한 시스템들은 동일한 로그를 공유하면서 서로 다른 인덱스 타입을 지원하게 될겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-2df23f60"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-2df23f60"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F0084rCUF6gzNTdWdKjxWg-d369ea8c6f0e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-2df23f60"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F0084rCUF6gzNTdWdKjxWg-d369ea8c6f0e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying"&gt;Log-centric Infrastructure Stack&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="엔딩마무리"&gt;엔딩 — 마무리&lt;/h3&gt;
&lt;p&gt;겸사 겸사 글을 마무리 해볼까 해요, 지금까지 복잡하게 얽힌 데이터 시스템 사이의 관계를 ‘로그’ 라는 하나의 자료구조로 추상화 시켜서 단순화시켰습니다. 그리고 Apache Kafka를 만들면서 로그라는 시스템을 현대 시스템 설계의 핵심으로 이끄는 내용이 인상깊었습니다. &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying"&gt;Jay의 글은 제게 깊은 인상을 주었는데요, 글을 읽고 나서 2가지 생각이 들었습니다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(1) 기존의 문제에서 ‘조금’ 개선하는 건 재능만 있으면 누구나 할 수 있지만 기존 관행에서 문제점을 발견하는 것과 이를 본질적으로 관통하는 새로운 패러다임을 제시하는 건 &lt;strong&gt;‘창의력’&lt;/strong&gt;이 필요하다고 느꼈습니다. 저는 대학원에서 블록체인을 연구했었는데요. 이 분야에서는 Majority is not enough라는 논문과 Flash Boys 2.0이 그랬고, AI쪽으로는 데이터 생성 기법의 근본이라고 할 수 있는 GAN 논문이 비슷한 인상을 주었습니다.&lt;/p&gt;
&lt;p&gt;(2) &lt;strong&gt;복잡한 관계를 단순한 디자인으로 추상화 시키는 능력이 매우 중요하다고 생각합니다.&lt;/strong&gt; 이 글은 추상화의 아름다움을 다시 한 번 느끼게 해주고 중요성을 깨닫게 해주었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;제가 너무 이 글을 과장한걸지도 몰라요, 평소에 분산 시스템을 잘 아시는 분이라면 뭐야 뻔한 내용밖에 없는데? 라고 생각하실지도 모르겠습니다. 오늘은 글이 조금 길었는데 누군가에게 도움이 되었길 바랍니다 : )&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>벡터 데이터베이스 소개 | 음악 검색 기능은 어떻게 만드는 걸까?</title><link>https://scalalang2.com/p/milvus-%EB%B2%A1%ED%84%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-b26065c51c16/</link><pubDate>Mon, 26 Jun 2023 11:32:58 +0000</pubDate><guid>https://scalalang2.com/p/milvus-%EB%B2%A1%ED%84%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-b26065c51c16/</guid><description>&lt;p&gt;벡터 임베딩(Vector Embedding)은 머신 러닝의 핵심 요소로 이미지, 음성, 단백질 분자 구조 등의 &lt;strong&gt;비정형 데이터&lt;/strong&gt;를 벡터로 표현하는 방법이다. 비정형 데이터를 일단, 벡터로 표현하고 나면 이를 수로 취급할 수 있어서 벡터 간의 유사도를 계산할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e1135a4f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e1135a4f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lwE8ggdIbieiryFebcGbMg-38225368543a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e1135a4f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-lwE8ggdIbieiryFebcGbMg-38225368543a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;딥러닝을 사용해서 비정형 데이터를 벡터로 임베딩 한다.&lt;/p&gt;
&lt;p&gt;딥러닝 모델에게 입력으로 데이터를 주면, 출력으로 나온 벡터에는 해당 데이터의 특징들이 수치화 되서 표현된다. 아래 그림은 &lt;strong&gt;벡터 임베딩&lt;/strong&gt; 결과를 후려쳐서 표기하였는데, 아마 딥러닝 괴물들이 보면 이게 무슨 말이냐며 오열할지도 모르겠지만 일단 내가 이해한 수준은 이정도이다. 이미지에서 특징을 추출하는 일은 VGG나 ResNet 오픈소스를 이용하면 쉽게 개발할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b58f0057"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b58f0057"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-b9_SMGi6-i2UUq508no4UA-224d48c9eab7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b58f0057"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-b9_SMGi6-i2UUq508no4UA-224d48c9eab7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;고양이와 사자 이미지의 벡터 임베딩 결과&lt;/p&gt;
&lt;p&gt;우리는 정형 데이터를 관리할 때 주로 관계형 데이터베이스(RDBMS)에 저장하고 질의문(SQL)을 이용해 원하는 결과를 찾는데 이용한다. 벡터 또한 (1) 저장하고 (2) 벡터를 찾는 질의를 할 수 있는 데이터베이스가 있으면 어떨까? &lt;strong&gt;‘고양이 이미지를 모두 찾아줘’&lt;/strong&gt; 라는 질의를 전송하면 고양이와 유사한 벡터를 찾아주거나 혹은 네이버의 음악검색 서비스를 직접 구현할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6fddc1be"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6fddc1be"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-blqUYbPmoBXvtS1C-0e5c4a7deafd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6fddc1be"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-blqUYbPmoBXvtS1C-0e5c4a7deafd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;출처 : &lt;a href="https://m.blog.naver.com/naver_search/221167578156"&gt;https://m.blog.naver.com/naver_search/221167578156&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;오늘은 벡터의, 벡터에 의한, 벡터만을 위한 데이터베이스 Milvus 논문에 대해 다룬다[1]. Milvus는 2021년 SIGMOD 에서 발표된 논문으로 현재 오픈소스[2]로 공개되어 있으며 Zilliz 라는 회사에서 클라우드 서비스로 운영하고 있다. 글의 내용은 벡터 데이터베이스에 대한 전반적인 소개가 아닌, Milvus가 1. 어떤 목적을 가지고 개발되었는지 2. 어떻게 개발되었는지 내부 구조를 자세히 뜯어보고 3. 응용 사례가 무엇인지 중심으로 다루고자 한다.&lt;/p&gt;
&lt;h3 id="목차"&gt;목차&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;벡터 데이터베이스&lt;/li&gt;
&lt;li&gt;Milvus 탄생 배경&lt;/li&gt;
&lt;li&gt;시스템 디자인&lt;/li&gt;
&lt;li&gt;CPU/GPU를 이용한 고성능 쿼리 구현&lt;/li&gt;
&lt;li&gt;성능 최적화 평가&lt;/li&gt;
&lt;li&gt;고급 쿼리 프로세싱&lt;/li&gt;
&lt;li&gt;응용 사례&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="벡터-데이터베이스"&gt;벡터 데이터베이스&lt;/h3&gt;
&lt;p&gt;벡터 데이터베이스는 고차원(high-dimensional) 벡터를 관리하는 시스템이다. 여기서 관리란 저장, 갱신, 삭제, 검색 등을 포함하여 기능을 제공한다는 의미이고 우리가 사용하는 DBMS의 기능이 벡터에 대해 적용된다고 생각하면 된다. 우리는 데이터의 형태와 목적에 따라 다양한 종류의 데이터베이스를 이용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-5e5dfec1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-5e5dfec1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z7e9VRoZHL3B6HUgiyaf-Q-eb570d1f3611.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-5e5dfec1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Z7e9VRoZHL3B6HUgiyaf-Q-eb570d1f3611.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;데이터베이스 종류에 따른 분류&lt;/p&gt;
&lt;p&gt;텍스트 검색을 위해 검색 엔진 시스템에 데이터를 적재하는 것처럼 유사한 벡터를 찾기 위해 벡터 데이터베이스를 이용할 수 있다. 아래는 대표적인 응용방법이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입력으로 주어진 이미지와 비슷한 이미지를 찾는다&lt;/li&gt;
&lt;li&gt;입력으로 주어진 문서의 토픽과 감정이 비슷한 문서를 찾는다&lt;/li&gt;
&lt;li&gt;이 상품과 평점과 특징이 비슷한 상품을 검색한다&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 실제 상용 레벨에서 사용할 수 있으려면 (1) 대규모 벡터 데이터에 대해 빠른 쿼리가 가능해야 하며, (2) 벡터 데이터를 삽입/삭제/변경이 용이해야 한다. 유튜브는 1분에 500시간 분량의 영상이 업로드 되고 이를 실시간으로 다른 유저에게 추천해줄 수 있어야 하기 때문에 대규모 데이터 셋에 대한 응답속도가 합리적이어야 하고 (3) 단순 유사도 검색만으로는 모든 기능을 구현하기엔 부족할 수 있다. 속성 필터링, 멀티 벡터 쿼리 등의 고급 쿼리를 지원하면 유연하게 기능을 구현하는데 도움이 된다. 지금부터 소개할 Milvus는 이 세가지 미덕을 모두 갖추고 있다.&lt;/p&gt;
&lt;h3 id="milvus-탄생-배경"&gt;Milvus 탄생 배경&lt;/h3&gt;
&lt;p&gt;Milvus 이전에 벡터 데이터베이스가 없던 것은 아니었다. 기존에 있던 제품들은 크게 세 가지 분류 나눌 수 있는데 모두 사용하기에 아쉬운 점이 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;① Library :&lt;/strong&gt; 대표적으로 &lt;a href="https://github.com/facebookresearch/faiss"&gt;Facebook Faiss&lt;/a&gt;, &lt;a href="https://github.com/microsoft/SPTAG"&gt;Microsoft SPTAG&lt;/a&gt; 가 벡터 유사도 검색을 지원하지만, 1. DBMS보다는 라이브러리 구현체에 가까우며, 모든 데이터가 메모리에 상주해 있을 것을 가정하기 때문에 대규모 데이터 셋을 다루기 어려웠으며 2.일반적으로 정적인 데이터를 다루기 때문에 실시간으로 데이터가 삽입/변경 되는 애플리케이션에서 빠른 쿼리를 지원할 수 없었으며, 속성 필터링과 같은 고급 쿼리를 지원하지 않았다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;② RDBMS 확장 :&lt;/strong&gt; 알리바바의 PASE나 &lt;a href="https://github.com/pgvector/pgvector"&gt;pgvector&lt;/a&gt;와 같이 기존 데이터베이스에 &lt;strong&gt;“vector column”&lt;/strong&gt;을 추가하고 질의기를 추가해서 RDBMS에서 벡터 유사도 검색을 수행할 수 있게 하는 접근도 있다. 하지만 기존 RDBMS는 벡터를 일급 시민(first-class citizen)으로 다루지 않으며, 질의 최적화기(Query Optimizer)를 변경하여 CPU나 GPU를 최대한 사용하여 성능을 끌어올리기 어려웠다. 또한, 고급쿼리를 지원하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;③벡터 데이터베이스&lt;/strong&gt; : Vearch가 벡터를 위한 검색엔진으로 개발되었지만, 우리 Milvus와 성능을 비교하면 한참 떨어진다. Milvus는 최소 6.4x 에서 최대 47.0x 정도 더 빠른 성능을 보여주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-199aa1fe"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-199aa1fe"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-FlvQALQuLvgHJFs6JQ9gng-6ef6b1a08f01.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-199aa1fe"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-FlvQALQuLvgHJFs6JQ9gng-6ef6b1a08f01.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Milvus와 기존 시스템의 성능 비교&lt;/p&gt;
&lt;p&gt;Milvus는 벡터를 일급 시민으로 취급하는 특수 목적 데이터베이스이며 데이터 과학 및 AI 애플리케이션을 위한 대규모 벡터 검색을 수행할 수 있다. 언급한 많은 아쉬운점을 해결했다&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;개발자 친화적 :&lt;/strong&gt; Python/Java/Go/C++로 구현된 SDK과 RESTful API 지원으로 개발자 친화적으로 접근했다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고급 쿼리 지원 :&lt;/strong&gt; 속성 필터링 및 멀티 벡터 쿼리를 지원한다. 속성 필터링은 Vector 유사도 검색과 더불어서 일부 속성에 대한 검색을 지원하는 기능이다. (e.g. 유사한 상품을 찾지만 가격이 $1,000 미만인 제품만 보겠다), 멀티 벡터 쿼리는 다수의 벡터를 입력값으로 주는 방식이다. (1) 색상에 대한 벡터, (2) 사물의 위치에 대한 벡터 를 동시에 주면 이 두가지를 만족하는 데이터를 찾는 식이다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인덱스 :&lt;/strong&gt; 벡터 검색을 빠르게 수행하기 위해 여타 DBMS와 마찬가지로 인덱싱이 필요하다. 현재, 벡터를 인덱싱 하는 방법은 다양한데, Milvus는 유명한 인덱스는 기본 지원하고 새로운 인덱스를 추가할 수 있도록 확장 가능하게 설계되었다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동적 데이터 :&lt;/strong&gt; LSM 기반의 구조를 채용해 벡터를 동적으로 관리한다. 즉, 삽입 삭제가 가능하도록 했으며 스냅샷 격리(e.g. MVCC)를 지원해 일관성 있는 실시간 검색을 가능하게 했다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Milvus는 페이스북의 Faiss를 메인으로 하여 개발되었지만, Faiss를 상당히 의미있는 수치만큼 성능을 높였으며 DBMS의 면모를 갖추기 위해 기능을 개발하였다. 현재 Milvus는 전 세계에서 이미지 프로세싱, 자연어 처리, 음성 인식, 추천 시스템 등 다양한 영역에서 활용되고 있다&lt;/p&gt;
&lt;h3 id="시스템-디자인"&gt;시스템 디자인&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-92402de9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-92402de9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UHOKbie4kmJyT7iPNyT6PQ-ff36eab6670b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-92402de9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UHOKbie4kmJyT7iPNyT6PQ-ff36eab6670b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Milvus 데이터베이스의 시스템 디자인&lt;/p&gt;
&lt;p&gt;Milvus의 시스템은 쿼리 엔진, GPU 엔진, 저장소 이렇게 크게 3가지로 구분되며 세부적으로는 그림 내의 점선으로 표기된 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;쿼리 프로세싱 :&lt;/strong&gt; 벡터 쿼리, 속성 필터링, 멀티 벡터 쿼리 연산을 처리한다. Milvus는 데이터를 Entity라는 개념으로 표현하는데 vector data &amp;amp; non-vector data를 포함하는 용어로 데이터 하나를 의미한다. 아래 코드는 Milvus의 데이터를 Node.js에서 나타낸다. 보면 알다시피, 벡터만 저장하는 건 아니고 속성(Attribute)를 함께 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;const data = Array.from({ length: 2000 }, (v,k) =&amp;gt; ({&lt;br&gt;
&amp;ldquo;book_id&amp;rdquo;: k,&lt;br&gt;
&amp;ldquo;word_count&amp;rdquo;: k+10000,\&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;book_intro&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인덱싱 :&lt;/strong&gt; 인덱스를 이용하는 건 효율적인 데이터 조회를 위해서 필수적인 요소이지만, 벡터분야에서는 아직까지도 연구가 이루어져 있고 하나의 인덱스 자료구조가 모든 상황에서 적절한 one-size-fit-all가 없다. 최근까지 연구된 벡터 인덱스들은 성능, 정확도, 공간 복잡도에 대해 트레이드-오프가 존재했다. Milvus에서는 IVF 계열의 인덱스와 HNSW와 같은 그래프 인덱스를 지원하고 있으며 앞으로 새로운 벡터 인덱스가 연구되더라도 쉽게 추가할 수 있도록 코드 구조를 설계했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IVF 인덱스 :&lt;/strong&gt; 논문에서 설명된 건 아니지만, 벡터 인덱스에 대한 이해를 돕기 위해 예시를 하나 가져왔다. 대표적인 인덱스인 IVF는 보로노이 다이어그램을 연상하게 하는 각 벡터 들의 중심점을 선정하고 그 점들간의 구분선을 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-fe562174"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-fe562174"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F4mJSgtRQkpeJhccX309sw-fb29afd0cbe6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-fe562174"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-F4mJSgtRQkpeJhccX309sw-fb29afd0cbe6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.pinecone.io/learn/vector-indexes/"&gt;출처 : Pinecome : Nearest Neighbor Indexes for Similarity Search&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;동적 데이터 관리 &lt;/strong&gt;: 엔티티 데이터는 LSM-Tree를 이용하여 저장/삭제 연산을 수행한다. LSM Tree는 실시간으로 대량의 데이터를 저장하는데 용이한 반면, 데이터 조회에는 몇 가지 제약사항이 있는 자료구조이다. B-Tree는 &lt;strong&gt;읽기 중심(read-intensive)&lt;/strong&gt;의 연산을 처리하는데 이점이 있고 LSM Tree는 &lt;strong&gt;쓰기 중심(write-intensive)&lt;/strong&gt;의 연산을 처리하는데 이점이 있다고 보면 좋다. 그 용도가 명확해서 많은 스토리지 내부에는 LSM Tree를 이용하는 경우가 많다. (조금 더 자세한 내용은 필자의 포스팅[3] 을 참고)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스토리지 관리&lt;/strong&gt; : 하나의 엔티티는 1개 이상의 벡터 정보와 속성 정보를 가지고 있다. 벡터와 같은 경우에는 물리적으로 연속적으로 저장하는 반면, 속성 정보는 데이터 압축 및 쿼리의 효율성을 위해 &lt;strong&gt;컬럼-기반(Columnar) 저장소&lt;/strong&gt;를 사용한다. 예를 들어 &lt;strong&gt;{ A, B, C }&lt;/strong&gt; 세 개의 엔티티가 있고 각각 벡터 v1, v2 가 엔티티에 속한다면 스토리지에는 &lt;strong&gt;{ A_v1, B_v1, C_v1, A_v2, B_v2, C_v2 }&lt;/strong&gt; 의 모습으로 저장된다. 컬럼-기반 저장 포맷은 &lt;strong&gt;아파치 파케이(Apache Parquet)&lt;/strong&gt;와 유사한 방식으로 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;필자 주 — 아파치 파케이&lt;/strong&gt;&lt;br&gt;
컬럼 기반 저장 포맷으로 대량의 데이터를 압축하여 저장하는게 용이하고, 특정 그룹마다 최솟값, 최댓값과 같은 정보를 저장하고 있어서 데이터의 풀 스캔을 피하면서 point-query를 가능하게 하는 포맷이다. 재밌는 한 예로, Grafana Labs의 Tempo가 대규모 트레이싱 정보를 파케이 포맷으로 저장하고 오브젝트 스토리지(e.g. Amazon S3)를 메인 저장소로 사용하고 있어서 Elasticsearch, Cassandra를 주요 스토리지로 사용하는 Jaeger보다 운영 비용이 매우 저렴하다. 대신 쿼리는 좀 느린편&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;버퍼 풀 (Buffer Pool) :&lt;/strong&gt; Milvus는 쿼리에 필요한 데이터가 대부분 메모리에 상주해 있을 것이라고 가정한다. 대개 이런 가정은 최신 데이터에 대한 조회율이 높을거야 등의 생각에 기인한다. 만약 이런 가정이 틀렸다면 &lt;strong&gt;LRU 캐시 버퍼 관리자(Buffer Pool Manager)&lt;/strong&gt;에 의존하여 데이터를 메모리로 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 스토리지 (Multi Storage) :&lt;/strong&gt; 유연성 및 신뢰성을 위해 Local File System, Amazon S3, HDFS를 포함한 다수의 스토리지 포맷을 지원한다. 요즘 이런거 해주는 오픈소스들 너무 좋다. MinIO를 사용하면 로컬에서도 쉽게 개발 환경을 구축해서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;분산 시스템 (Distributed System) &lt;/strong&gt;: 분산 시스템으로 운영할 수 있으며 최신 분산 시스템의 철학인 스토리지/컴퓨팅 분리, 공유 스토리지, 읽기/쓰기 분리, 단일 쓰기 다중 읽기의 개념을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cpugpu를-이용한-고성능-쿼리-구현"&gt;CPU/GPU를 이용한 고성능 쿼리 구현&lt;/h3&gt;
&lt;p&gt;위에서는 Milvus의 특징에 대해 소개했다면, 여기서는 CPU/GPU를 이용하여 Milvus가 어떻게 쿼리 성능을 높였는지에 대해 소개한다. CPU/GPU/FPGA 등 서로 다른 종류의 프로세서가 동시에 탑재된 것을 ‘&lt;strong&gt;이종 컴퓨팅(Heterogeneous Computing)&lt;/strong&gt;’이라고 부른다.&lt;/p&gt;
&lt;p&gt;이 부분을 이해하려면 먼저, 벡터 데이터를 어떻게 인덱싱을 수행하여 저장하는지 그리고 쿼리는 어떻게 동작하는지에 대해 알고 있어야 한다. Milvus는 페이스북의 Faiss[4] 오픈소스를 사용하여 개발되었기 때문에 해당 오픈소스에 구현된 인덱스를 그대로 이용했다. 지금 부터 가장 사용하기 쉽고 합리적인 성능을 갖추었다고 알려진 IVF인덱스에 대해 간략히 소개하고자 하며 자료 [5], [6]을 참고했다.&lt;/p&gt;
&lt;h3 id="-ivf-인덱스"&gt;① IVF 인덱스&lt;/h3&gt;
&lt;p&gt;IVF(Inverted File Index)는 유사한 벡터 데이터를 찾기 위한 인덱스 자료구조이다. IVF은 &lt;strong&gt;양자화(Quantization)&lt;/strong&gt; 알고리즘 이라고도 불리는데, 여기서 양자화란 컴퓨터에서 처리하기 어려운 거대한 데이터를 압축 표현하여 성능을 높이는 방법을 말한다. 예를 들어 float32 데이터 타입을 int8 로 압축할 수 있다면, 크기가 4배로 줄어드는 효과가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-38156c00"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-38156c00"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zNVBSyySP14oxEAZX36YQA-674995902e0c.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-38156c00"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-zNVBSyySP14oxEAZX36YQA-674995902e0c.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Inverted File Index (IVF) Flat 의 대략적인 동작 과정&lt;/p&gt;
&lt;p&gt;위 그림은 IVF_FLAT 인덱스의 대략적인 동작 과정을 보여준다. 먼저, 벡터를 일정한 개수(M)로 나누고 K-means clustering을 수행하여 검색의 범위를 축소시킨다. 결과로 나온 인코딩된 벡터는 모든 벡터에 대해서 동일한 크기를 가지게 된다. 이 때 어떤 방식으로 인코딩 할지에 따라서, 다시 말하자면 어떻게 양자화 할지에 따라서 IVF_FLAT, IVF_SQ8, IVF_PQ 등 이 종류가 나뉘게 된다.&lt;/p&gt;
&lt;p&gt;이러한 컨셉은 &lt;strong&gt;보로노이 다이어그램(Voronoi Diagram)&lt;/strong&gt;의 컨셉으로 설명할 수 있는데 아래 그림이 보로노이 다이어그램이다. 검은색 점을 K-means clustering으로 군집화된 그룹들의 중앙점(centroid)라고 본다면 임의의 정점 사이에 동일한 거리를 가지도록 선을 그으면 아래 그림이 완성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-4bf69b47"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-4bf69b47"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DxaKOdhge5gKj8iEfZF7Gg-6386362d4357.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-4bf69b47"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DxaKOdhge5gKj8iEfZF7Gg-6386362d4357.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;출처 : 보로노이 다이어그램 | Wikipedia&lt;/p&gt;
&lt;p&gt;이제 우리가 입력값으로 주어진 벡터(Query Vector)가 어느 그룹에 속하는지만 효율적으로 찾을 수 있다면 인덱스가 완성된다. 단, 다이어그램에 애매하게 걸친 벡터가 존재할 수 있는데 이를 Edge Problem 이라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-aee745b5"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-aee745b5"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GNoU7zGCD4NqfTV7F1yR_w-a6d027608585.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-aee745b5"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-GNoU7zGCD4NqfTV7F1yR_w-a6d027608585.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;IVF 인덱스에서 Edge Problem을 나타내는 그림&lt;/p&gt;
&lt;p&gt;위 그림과 같이 A,B,C,D 로 군집화된 벡터들 사이에 쿼리 벡터를 입력으로 주어지면 이 친구는 동시에 A에 속할수도 있고, D에 속할수도 있다. 그림이 2차원 평면으로 그려져있기 때문에 별 일 아니라고 느껴지긴 하지만 실제 세상에서 우리가 군집화 하는 차원은 훨씬 더 고차원이기 때문에 위와 같은 상황이 발생할 가능성이 크다. 그래서 IVF Index에서는 최대 몇개의 그룹을 탐색할건지 결정하는 nprobe 값을 파라미터로 결정해야 한다. nprobe값을 결정하는데에 성능(Performance)과 정확도(Accuracy) 사이의 트레이드 오프가 존재한다. 높은 값을 쓸 수록 정확도는 올라가지만 성능을 희생해야 한다.&lt;/p&gt;
&lt;h3 id="-cache-aware-optimization"&gt;② Cache-aware Optimization&lt;/h3&gt;
&lt;p&gt;이런 IVF 계열의 인덱스를 사용하여 쿼리를 수행할 때 주어진 M개의 쿼리를 어떻게 효율적으로 처리할 것인지가 문제가 된다. 실제 애플리케이션에서는 유저가 한개의 쿼리만 수행하는 것이 아니라 동시에 m개의 배치 쿼리를 던질 수 있기 때문에 아주 빠르게 처리가 되어야 한다. &lt;strong&gt;Milvus논문에서는 페이스북 Faiss의 쿼리 성능 문제를 대량의 캐시 미스가 발생하여 성능 비효율이 보인다고 진단했다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Faiss는 멀티 쓰레드로 각각의 쿼리를 병렬로 처리하는 디자인은 사용한다. 각 스레드는 하나의 쿼리를 담당하여 처리하며, 쿼리에 대한 수행이 끝나면 스레드 자은 반환된다. 이거 마치 톰캣에서 Servlet이 하는 역할과 비슷하지 않은가? 이게 뭐가 문제지 라고 하면서 논문을 읽으면 이런 이야기가 나온다. (1) 각 스레드마다 쿼리를 수행하기 위한 대량의 데이터가 CPU 캐시로 스트림되며 &lt;strong&gt;다음 쿼리에서 재사용되기 어렵다.&lt;/strong&gt; t를 필요한 전체 스레드의 개수, m을 쿼리의 개수라고 볼 때 m/t 시간 만큼 데이터 접근이 필요하다. (2) 쿼리의 개수 m이 매우 작을 때 가용 가능한 모든 스레드를 사용하지 않는다.&lt;/p&gt;
&lt;p&gt;(1)의 문제를 자세히 이해하기 위해서 이 글[7]을 참고했다. CPU가 고성능이 되는 것과 별개로 메모리의 성능은 크게 향상을 하지 못했다. 즉, CPU에서 메인 메모리(L4)로 접근하는 비용이 비싸기 때문에 CPU에 가까이 있는 캐시 메모리(SRAM)을 이용하는데 메인 메모리 보다는 용량이 적기 때문에 IVF 인덱스를 수행하는데 필요한 정보를 저장하기 어렵기 때문에 캐시 미스가 자주 발생하는 것으로 해석된다.&lt;/p&gt;
&lt;p&gt;Milvus 에서 각 스레드는 일정한 크기 b=n/t 만큼의 데이터 벡터를 할당 받고 각각의 쿼리는 L3 CPU Cach에 알맞도록 s 그룹으로 파티셔닝하여 병렬로 처리한다. 아래 그림은 Milvus 구조의 모습을 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-45adb1de"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-45adb1de"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AGzRbjvXJtRwVNsKFQuU8w-223c4e2f2a41.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-45adb1de"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-AGzRbjvXJtRwVNsKFQuU8w-223c4e2f2a41.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Cache-aware design in Milvus&lt;/p&gt;
&lt;p&gt;위 구조에서 top-k 유사도 탐색은 각 쿼리 블록 마다 병렬로 이루어지며, 결과를 동기화 하기 위한 오버헤드를 줄이기 위해 결과를 힙(Heap)에 저장한다. 힙은 스레드 마다 할당되며 각 쿼리의 결과가 스레드의 각 힙에 전파되며 나중에 병합하여 top-k 유사도 결과를 도출한다. 이런 구조를 사용하면 m/(s*t) 만큼만 전체 데이터 접근에 필요하므로 1.5x 에서 2.7x까지 성능을 향상할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cc3d3c82"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cc3d3c82"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4io4yYCI8nsa5bmiurPkDw-3a3305cde895.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cc3d3c82"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4io4yYCI8nsa5bmiurPkDw-3a3305cde895.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;L3 캐시가 증가함에 따라 성능 향상이 있는지 확인한 실험 결과&lt;/p&gt;
&lt;h3 id="-simd-aware-optimization"&gt;③ SIMD-aware Optimization&lt;/h3&gt;
&lt;p&gt;SIMD는 CPU 내에서 하나의 명령어로 다수의 데이터를 처리하는 것을 말한다. 특히 이게 벡터 연산을 할 때 매우 효율적인데 쉽게 생각하면 두 벡터의 합은 벡터 내의 모든 원소를 더하는 것이다. 원소의 개수가 N개라면 N만큼의 합 명령어가 필요하지만, SIMD를 사용하면 하나의 명령어로 벡터의 합을 병렬로 처리하도록 CPU 자체적으로 지원한다.&lt;/p&gt;
&lt;p&gt;Faiss 에서도 SIMD를 이용하여 벡터 유사도 탐색의 속도를 높이고 있었는데, Milvus에서는 AVX512 지원을 추가로 구현하였고 자동 SIMD 명령어 선택 알고리즘을 사용하여 성능으 한쳬라 더 높였다. AVX는 고급 벡터 확장(Advanced Vector eXtensions) 이란 의미로 벡터 연산에 최적화된 명령어 셋이며, 현재 장비에서 지원되는 SIMD명령어가 상이한경우 자동으로 사용할 수 있는 SIMD명령어를 선택하여 처리하는 것이 구현되었다.&lt;/p&gt;
&lt;h3 id="-gpu-oriented-optimization"&gt;④ GPU-oriented Optimization&lt;/h3&gt;
&lt;p&gt;Faiss는 공유 메모리 크기의 한계로 유사도 탐색의 결과가 1024개로 제한되어 있었는데, 영상 탐색이나 추천 시스템에서는 데이터의 검증이나 랭킹을 다시 매기는 요구 등에 의하여 1024개 이상의 데이터를 처리할 필요가 있었다. Milvus에서는 16384개 이상의 결과를 낼 수 있도록 수정되었다. 메모리 크기가 늘어난 건 아니어서 같은 쿼리를 다수의 라운드에 걸쳐서 누적하여 처리하는 방법으로 알고리즘이 수정되었다.&lt;/p&gt;
&lt;p&gt;일반적으로 GPU 메모리는 대량의 데이터를 저장하기에 용량이 충분하지 않아서 Faiss에서는 벡터 데이터를 IVF_SQ8 포맷으로 한 번 더 압축하여 CPU 메모리에서 GPU 메모리로 전송(via PICe bus)하여 사용한다. 논문의 저자들은 이러한 방식이 PICe 버스의 대역폭이 낮아서 오히려 오버헤드가 커서 GPU를 이용한다고 쿼리 성능이 항상 빠르지 않다는 걸 관측했다.&lt;/p&gt;
&lt;p&gt;쿼리의 배치 사이즈가 커야 대량의 데이터를 전송하는 오버헤드를 감수할 만 하다는 실험결과를 토대로, 특정 임계값 이상의 쿼리가 들어올 때는 쿼리 전체를 GPU에 넘겨서 처리한다. IVF 계열의 인덱스에서는 1. nprobe 만큼의 데이터 버킷을 찾는 것과 2. (1)의 결과로 주어진 주어진 버킷을 탐색하는 것 이 두가지 연산을 거치는데 1.은 GPU에서 처리하고 2.는 CPU에서 처리하는 것으로 구현되었다.&lt;/p&gt;
&lt;h3 id="성능-최적화-평가evaluation"&gt;성능 최적화 평가(Evaluation)&lt;/h3&gt;
&lt;p&gt;Milvus의 이런 접근법이 실제 성능향상이 있었는지 평가하기 위해 징동(중국의 쿠팡이며 LCS 리그의 그 징동이 맞다)에서 개발한 Vearch와 마이크로소프트 SPTAG와 상업 문제로 익명 처리를 한 상용 서비스 A, B, C를 비교하였다. 평가 방식은 1000만개의 벡터 데이터를 저장하고, 1만개의 랜덤 쿼리를 발생시켜서 recall이 1에 근사할 때 까지 수행하여 쿼리 성능을 관측했다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;recall(재현율)&lt;/strong&gt;&lt;br&gt;
벡터의 유사도 탐색이라 함은, 말 그대로 유사한 벡터를 찾는 것이다. 실제 데이터의 정답은 ground-truth라고 이 데이터의 집합을 S, 쿼리의 집합 S’라고 할때 S와 S’의 교집합이 몇개냐에 따라 recall을 계산한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-3a0ac603"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-3a0ac603"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Fv7zurRXRAitjCx6FhU1NA-c2dedebe07be.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-3a0ac603"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Fv7zurRXRAitjCx6FhU1NA-c2dedebe07be.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Milvus와 경쟁 제품간의 성능 비교&lt;/p&gt;
&lt;p&gt;위 그림은 IVF 계열 인덱스에 대해서 성능 결과를 비교한 것으로, 확실히 타사 제품에 비해 높은 성능을 보여주었다. 아래는 각 제품들 간의 정확한 수치와 몇 가지 추가 정보를 나열했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vearch : 6.4x ~ 27.0x 높은 성능을 보여주었다.&lt;/li&gt;
&lt;li&gt;System B : System B는 4대의 노드 세팅을 요구함에도 153.7x 정도 성능이 높았다.&lt;/li&gt;
&lt;li&gt;System C : System C는 2대의 노드에서 동작함에도 Milvus가 4.7x ~ 11.5x 정도 성능이 높았다.&lt;/li&gt;
&lt;li&gt;SPTAG : SPTAG에 비해서는 1.3x ~ 2.1x 정도 향상에 그쳤으나, Milvus보다 14x 정도 많은 메모리를 요구했으며 높은 재현율(e.g. 0.99)을 요구하는 쿼리를 수행할 수 없었다. 그리고 GPU라는 치트키를 쓴다면 Milvus가 훨씬 높은 성능을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;본 포스팅에서는 단순히 IVF 인덱스 계열에 대한 평가 결과만 소개하였지만, 논문에서는 그래프 계열의 인덱스인 HNSW에 대해서도 평가한 내용이 있으며, 속성 필터링 및 멀티 벡터 쿼리의 성능 또한 비교한 자료가 있다. 아무래도 Milvus가 최신 논문이며 본인들이 원하는 환경에서 실험을 진행했기 때문에 높은 성능을 보일 수 밖에 없긴 하다. 더 자세한 내용을 원한다면 논문 [1]을 참고 하면 된다.&lt;/p&gt;
&lt;h3 id="고급-쿼리-프로세싱"&gt;고급 쿼리 프로세싱&lt;/h3&gt;
&lt;p&gt;위에서는 IVF 인덱스를 이용해서 벡터의 유사도 탐색을 어떻게 수행하였고, 어떤 방식으로 성능을 올렸는지에 대해 설명했다. &lt;strong&gt;고급 쿼리 프로세싱&lt;/strong&gt; 장에서는 &lt;strong&gt;속성 필터링(Attribute Filtering)&lt;/strong&gt;과 &lt;strong&gt;멀티 벡터 쿼리(Multi-vector query)&lt;/strong&gt;가 Milvus에서 어떻게 구현되었는지 설명한다.&lt;/p&gt;
&lt;h3 id="-속성-필터링-attribute-filtering"&gt;① 속성 필터링 (Attribute Filtering)&lt;/h3&gt;
&lt;p&gt;Milvus 데이터베이스의 데이터 단위인 엔티티는 벡터 데이터와 키-밸류로 구성된 속성 정보의 조합으로 이루어져있다. 속성 필터링은 벡터 유사도 탐색을 수행하면서 동시에 일부 속성을 만족하는 데이터를 탐색하는 것을 말한다. 예를 들어 T셔츠와 유사한 옷을 찾되, 가격은 $10 이상 &amp;amp; $100 이하의 조건을 찾고 싶을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-91da101f"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-91da101f"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Mu2vpOW1lRpR7-VGyD6dSw-5f05f7e4cb83.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-91da101f"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Mu2vpOW1lRpR7-VGyD6dSw-5f05f7e4cb83.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;속성 필터링 쿼리 구현 전략&lt;/p&gt;
&lt;p&gt;위 그림에서 전략이 A~D까지는 기존에 연구된 사례이며, 전략 E가 Milvus에서 새롭게 구현된 내용이다. [A] 속성을 검색하고 벡터는 풀스캔을 치는 방식이며 정확도가 가장 높다. [B] 속성을 만족하는 벡터를 찾은뒤 인덱스를 타서 유사도 검색을 하는 방식이다. [C] 벡터를 먼저 찾고 속성을 필터링 하는 방식이다. 여기서 A, B, C는 쿼리의 개수, 탐색해야하는 데이터 크기 등에 따라 성능이 제각각 다르다. [D] 는 상황에 따라 A, B, C를 적절히 선택하겠다는 전략을 의미한다.&lt;/p&gt;
&lt;p&gt;마지막으로 Milvus에서 구현된 [E]는 자주 사용되는 쿼리 속성에 대해 데이터를 파티셔닝 한 뒤, 특정 파티션에 대해서 전략 [D]를 취한다. 예를 들어 &lt;strong&gt;가격&lt;/strong&gt;에 대해서 쿼리가 빈번한 경우 모든 데이터를 P1=[0&lt;del&gt;100], P2=[100&lt;/del&gt;200], P3[200~300],… 등으로 파티셔닝 한다. 그리고 쿼리 p의 범위가 p ≥ $50 &amp;amp; p ≤ $150 이라면 파티션 그룹 P1, P2 에 대해서만 수행하여 결과를 반환한다.&lt;/p&gt;
&lt;h3 id="-멀티-벡터-쿼리-multi-vector-query"&gt;② 멀티 벡터 쿼리 (Multi-vector Query)&lt;/h3&gt;
&lt;p&gt;많은 애플리케이션에서 하나의 데이터는 다수의 벡터로 표현될 수 있다. 레시피를 저장할 때는 이미지 정보와 텍스트 정보를 함께 저장할 수 있고, 영상 정보를 벡터로 저장할 때는 front-face vector, side-face vector 그리고 카메라에 담긴 포즈에 대한 값을 함께 저장할 수 있다.&lt;/p&gt;
&lt;p&gt;멀티 벡터 쿼리란 하나의 엔티티가 다수의 벡터 { v0, v1, … , vn } 개를 저장하고 있을 때에도 유사도 검색을 지원하는 것을 말한다. 본 포스팅의 앞부분에서는 하나의 벡터에 대해서만 유사도 검색을 수행하는 과정을 소개했었다. 이를 다수의 벡터로 확장한다는 개념이다. 이를 가장 쉽게 구현하는 방식은 각각의 벡터에 대해서 합산하지 않고, 각 벡터마다 top-k개의 결과를 가지고 추려내는 것이다. 구현은 단순하지만 결과값의 정확도가 떨어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ff160e5b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ff160e5b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-nJe6YlRsvOEornDV-f5fc7ee53afe.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ff160e5b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-nJe6YlRsvOEornDV-f5fc7ee53afe.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;score 계산 방식&lt;/p&gt;
&lt;p&gt;그래서 두 벡터의 유사도를 평가하는 함수 f에 대해 모두 집계하는 함수 g를 계산하여 가장 점수가 높은 벡터를 찾는 것으로 문제를 정의할 수 있다. 이제는 점수를 매기는 함수 g 결과값에 퀄리티가 의존한다 .엔티티 X, Y가 있을 때 각각의 점수는 위와 같이 계산된다. 이 때, g는 일반적으로 weighted-sum, average/median과 같은 모노토닉 연산을 사용하여 합산한다.&lt;/p&gt;
&lt;p&gt;Milvus에서는 멀티 벡터 쿼리를 처리하기 위해 두 가지 접근법을 상황에 따라 적용한다. (1) Vector fusion 은 단순하게 엔티티 내의 벡터 들을 모두 이어붙여서(concatenation) 하나의 벡터로 저장하고 함수 g를 적용하는 방식이다. 함수 g가 벡터의 내적(inner product)처럼 분해할 수 있는 형태면 이런 방식을 취해도 결과가 동일함이 증명된다. (2) 만약 유클리드 거리처럼 분해할 수 없는(not decomposable) 형태의 함수를 스코어 함수 g로 이용한다면 &lt;strong&gt;NRA (no random accesss)&lt;/strong&gt; 알고리즘을 조금 변형한 형태를 이용해서 top-k 연산을 처리한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NRA(no random access)&lt;/strong&gt; 알고리즘은 N개 이상의 정렬된 집합을 입력으로 받고 스코어 함수 g를 &lt;strong&gt;순차적(sequential)&lt;/strong&gt;으로 처리하는 알고리즘이다. 이름에서 알 수 있듯이 random access을 줄여서 성능을 높이는 기법이라는 걸 알 수 있는데 자세한 동작 과정은 해당 논문 [8]의 섹션 3.2를 참고하면 된다. 아무래도 2001년에 나온 기법이다 보니까 현대 시대에는 맞지 않는게 있을 수 있다. 우선, 랜덤 엑세스는 더 이상 심한 오버헤드를 발생시키지 않는다는 것이 있다. NVMe SSD가 흔한 시대로 넘어와서는 데이터를 CPU에서 정렬시키는 비용보다 그냥 SSD에서 랜덤 엑세스로 접근하는게 빠르다. 이건 필자의 개인적인 의견을 덧붙여 보았고, Milvus에서는 NRA의 입력값이 전체 데이터의 정렬된 셋이라는 문제점 때문에 집합 선정을 점차 늘려나가면서 휴리스틱하게 K개를 찾는 방식을 사용하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e3ebcf71"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e3ebcf71"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WoaU6nhnpBy7IX2VE5-G5g-55c7b4943df3.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e3ebcf71"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-WoaU6nhnpBy7IX2VE5-G5g-55c7b4943df3.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Iterative merging : Milvus의 멀티 쿼리 구현 방식&lt;/p&gt;
&lt;p&gt;알고리즘을 보면 k개를 선정할 수있을 때 까지 NRA의 입력으로 주어지는 집합 R을 키워나가는 것을 볼 수 있다.&lt;/p&gt;
&lt;h3 id="응용-사례"&gt;응용 사례&lt;/h3&gt;
&lt;p&gt;논문에서는 저자들이 이미지 검색, 영상 검색, 분자 구조 분석, COVID-19 데이터셋 탐색, 개인화 추천, 생물학적 다중 인증, 질문 답변, 이미지-텍스트 탐색, 보행자 탐색, 음식 레시피 검색 등 이렇게 10가지 분야에 대해 Milvus에 적용했다고 말했고, &lt;a href="https://github.com/milvus-io/bootcamp"&gt;실제로 이 곳 깃허브에 들어가보면 코드&lt;/a&gt;도 같이 소개되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-1d50067c"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-1d50067c"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nt6X5CYB03vcUIA5WFc9nA-0a8ea72bfd45.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-1d50067c"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-nt6X5CYB03vcUIA5WFc9nA-0a8ea72bfd45.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Milvus를 이용한 이미지 탐색&lt;/p&gt;
&lt;p&gt;여기서는 데모가 아닌, 실전에서 Milvus가 어떻게 쓰이고 있는지 소개해보려고 한다. 먼저, 이미지 탐색은 VGG와 ResNet 딥러닝 모델을 쓰면 이미지를 벡터로 변환할 수 있다는게 잘 알려져 있는 응용 사례였고 Milvus를 이용하면 간단하게 이미지 검색 기능을 구현할 수 있다. 중국에서는 Qichacha와 Beike Zhafang 회사가 Milvus를 이용해 이미지 검색을 지원하고 있다. 각각 중국 회사의 신용정보을 검색할 수 있는 서비스와 직방과 같은 온라인 부동산 중계 서비스이며 모두 중국 내에서 유니콘에 해당하는 스타트업이다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.bigo.tv/ko/"&gt;비고라이브(BIGO)&lt;/a&gt;로 알려진 BIGO Technology 에서는 자사의 숏폼 플랫폼 Likee에서 중복 동영상을 제거하기 위해 Milvus를 이용하고 있다[9]. 그리고 중국 제약회사인 &lt;a href="https://www.wuxiapptec.com"&gt;Apptech&lt;/a&gt; 에서는 신약 개발에 도움을 얻기 위해서, 분자 구조를 탐색용도로 Milvus를 채택하였다. 아무래도 Milvus 가 중국에서 먼저 유즈케이스를 만들면서 성장하다보니까 중국 및 아시아 위주로 유즈 케이스가 소개되었다.&lt;/p&gt;
&lt;h3 id="마무리"&gt;마무리&lt;/h3&gt;
&lt;p&gt;벡터 데이터베이스의 분야에서 이용되는 Milvus에 대해 소개하였다. 마지막으로 유즈케이스 설명에서 아무래도 중국에 기반한 회사다 보니까 중국 위주의 사례가 많이 보고되었다. 미국에서는 Head of AWS AI Labs 였던 Edo Liberty가 창립한 &lt;a href="https://www.pinecone.io"&gt;Pinecone&lt;/a&gt;이 주로 채용되고 있는 것으로 보인다. 대표적인 유즈 케이스로는 Shopify나 Zapier등이 있다. Milvus 논문에서는 pinecome에 대한 언급은 없었는데, 두 회사의 창립일이 비슷한 것으로 보아 아마 서로의 존재를 모르고 있던게 아닐까 싶다.&lt;/p&gt;
&lt;p&gt;Pinecone이 미국 빅테크 출신이 개발했다는 타이틀은 있지만 공개된 코드가 없다. 이 회사가 Datadog 수준으로 이 분야에서 독보적인 서비스를 제공한다면 경쟁력이 있을 수도 있지만, 요즘 오픈소스는 쿠버네티스에 배포부터 쉽게 하라고 Helm chart까지 작성되어 나오는게 기본 소양인지라 운영 부담은 많이 하락한 상황이다. Milvus가 비슷한 수준의 퍼포먼스만 낸다면 이 또한 좋은 선택지가 아닐까 싶다.&lt;/p&gt;
&lt;p&gt;마지막으로, 중국 IT 기술이 나날이 발전하고 있는게 피부로 느껴진다. 논문을 탐색해보면 중국인 유학생 또는 중국 대학에서 작성한게 많이 보이고, 무엇보다 나의 인생 갓겜 원신을 만든 호요버스의 기술력은 한국에서는 흉내내기도 어렵다. 저잣거리에 나도는 소문에 따르면 유니티 개발자들조차 호요버스 개발자를 초청해 유니티에 대해서 배움을 얻을 정도라고 하니 이제는 중국한테서 기술을 배워야 하지 않을까&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://www.cs.purdue.edu/homes/csjgwang/pubs/SIGMOD21_Milvus.pdf"&gt;Milvus: A Purpose-Built Vector Data Management System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://github.com/milvus-io/milvus"&gt;Milvus : A cloud-native vector database, storage for next generation AI applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://medium.com/curg/nvme-ssd%EC%9D%98-%EB%93%B1%EC%9E%A5%EC%9D%B4-%ED%82%A4-%EB%B0%B8%EB%A5%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%84%B1%EB%8A%A5%EC%97%90-%EB%AF%B8%EC%B9%A0-%EC%98%81%ED%96%A5%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-f94f2e952d27"&gt;NVMe SSD의 등장이 키-밸류 데이터베이스 성능에 미칠 영향에 관하여 by @scalalang2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://github.com/facebookresearch/faiss"&gt;Github | Facebookresearch/faiss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="https://www.youtube.com/watch?v=PNVJvZEkuXo&amp;amp;ab_channel=mildlyoverfitted"&gt;Product quantization in Faiss and from scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://www.pinecone.io/learn/vector-indexes/"&gt;Nearest Neighbor Indexes for Similarity Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://blog.naver.com/PostView.nhn?blogId=techref&amp;amp;logNo=222251292946"&gt;캐시(Cache) 메모리란? 지역성의 원리 L1, L2, L3 캐시 동작과 캐시 미스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="http://www.cs.umd.edu/~samir/498/topk.pdf"&gt;A Survey of Top-k Query Processing Techniques in Relational Database Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://medium.com/vector-database/how-short-video-platform-likee-removes-duplicate-videos-with-milvus-3ac838ca0715"&gt;How Short-video Platform Likee Removes Duplicate Videos with Milvus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>스트림 프로세싱의 긴 여정을 위한 이정표 (w. Apache Flink)</title><link>https://scalalang2.com/p/%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1%EC%9D%98-%EA%B8%B4-%EC%97%AC%EC%A0%95%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EC%A0%95%ED%91%9C-with-flink-8e3953f97986/</link><pubDate>Sun, 14 May 2023 14:12:47 +0000</pubDate><guid>https://scalalang2.com/p/%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1%EC%9D%98-%EA%B8%B4-%EC%97%AC%EC%A0%95%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EC%A0%95%ED%91%9C-with-flink-8e3953f97986/</guid><description>&lt;p&gt;이 글은 스트림 프로세싱이란 개념이 생소하고 Flink를 현업에서 도입하고자 하는 분들이 어디서 부터 시작해야 하는지, 앞으로 무엇을 공부해야하는 지 알 수 있는 이정표 역할을 할 수 있길 기대하면서 작성했습니다.&lt;/p&gt;
&lt;p&gt;저도 당분간 업무 영역이 이쪽으로 바꾸어서, 급하게 공부한거라 제가 맞는 말을 하고 있는건지 확신이 없습니다, 잘 아시는 분이 계시다면 공격적인 코멘트 부탁드립니다.&lt;/p&gt;
&lt;h3 id="목차"&gt;목차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;스트림 프로세싱 (Stream Processing)&lt;/li&gt;
&lt;li&gt;윈도우 (Window)&lt;/li&gt;
&lt;li&gt;워터마크 (Watermark)&lt;/li&gt;
&lt;li&gt;정확히 한 번 처리 (Excatly Once Semantic)&lt;/li&gt;
&lt;li&gt;한계 (Limitation)&lt;/li&gt;
&lt;li&gt;Flink 설치 및 플레이&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="스트림-프로세싱"&gt;스트림 프로세싱&lt;/h3&gt;
&lt;p&gt;2000년대 초반부터 많은 곳에서 도입한 대규모 데이터를 다루는 방법은, MapReduce 기반의 대규모 배치 시스템입니다. 대표적으로는 Hadoop 생태계의 제품군들이 있습니다. 배치 시스템은 기본적으로 대규모 데이터에 복잡한 연산을 수행하기 때문에 지연시간이 상대적으로 크구요. 스트림 처리 패러다임에서는 단순한 연산을 짧은 지연시간 내에 처리하는 것을 목표로 합니다.&lt;/p&gt;
&lt;p&gt;스트림과 배치를 구분하는 많은 특징들이 있지만, &lt;strong&gt;학자들이 대표적으로 구분하는 방식은 데이터의 범위가 고정되어 있는지 (bounded), 고정되어 있지 않은지 (unbounded) 를 기준으로 삼고 있습니다.&lt;/strong&gt; 배치 시스템은 대규모의 데이터를 고정된 시간 범위 안에 있는 ‘유한’ 데이터에 사용하는 시스템으로 보고, 스트림 프로세싱은 시간 범위가 고정되어 있지 않은 ‘무한’ 대의 데이터를 처리하는 것을 목표로 합니다.&lt;/p&gt;
&lt;p&gt;요즘, 어떤 사람들은 스트리밍 시스템과 배치 시스템을 통합하고자 하는 시도를 하는데요. 아직 까지는 여러 한계점이 존재합니다. 이는 &lt;strong&gt;&lt;code&gt;[#5. 한계]&lt;/code&gt;&lt;/strong&gt;&lt;code&gt; &lt;/code&gt;에서 좀 더 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-90ddb8dd"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-90ddb8dd"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ax3UshsQV4E8nnTMYpfOpw-b55670cc2572.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-90ddb8dd"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-ax3UshsQV4E8nnTMYpfOpw-b55670cc2572.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;배치 프로세싱과 스트림 프로세싱의 비교표&lt;/p&gt;
&lt;p&gt;이런, 스트림 프로세싱을 어디에 쓸 수 있을까요? 먼저 스트림 프로세싱이 관심을 가지는 데이터는 어떤 모양일까 상상해봅시다.&lt;/p&gt;
&lt;p&gt;전체 회원 목록 같은 데이터는 스트림 프로세싱에서 관심을 가지는 데이터가 아닙니다. 이런 데이터는 특정 시간에 데이터베이스에서 질의 했을 때 반환되는 전체 목록이지 스트림으로 표현할 수 없습니다. 스트림이란 &lt;strong&gt;“새로운 회원이 생성됨”&lt;/strong&gt;과 같은 이벤트 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e7499e13"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e7499e13"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-7sa-cntKAegkxCVL-c2467c9191a6.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e7499e13"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-7sa-cntKAegkxCVL-c2467c9191a6.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;배치 프로세싱과 스트림 프로세싱의 차이 [2]&lt;/p&gt;
&lt;h3 id="스트림-데이터의-예시"&gt;스트림 데이터의 예시&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;쇼핑몰 고객의 구매 요청&lt;/li&gt;
&lt;li&gt;항공사 예약 발생&lt;/li&gt;
&lt;li&gt;보험금 청구&lt;/li&gt;
&lt;li&gt;은행 트랜잭션 발생&lt;/li&gt;
&lt;li&gt;클릭 이벤트&lt;/li&gt;
&lt;li&gt;서버 로그&lt;/li&gt;
&lt;li&gt;현재 IoT 장비의 위치&lt;/li&gt;
&lt;li&gt;기타 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="스트림-데이터를-활용하는-대표적인-예시는-다음이-있습니다"&gt;스트림 데이터를 활용하는 대표적인 예시는 다음이 있습니다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;은행에서 이상 거래를 탐지&lt;/li&gt;
&lt;li&gt;쇼핑몰에서 구매자의 수요에 맞춰 동적 가격 계산&lt;/li&gt;
&lt;li&gt;온라인 사용자의 행동 패턴 분석&lt;/li&gt;
&lt;li&gt;실시간 추천 시스템&lt;/li&gt;
&lt;li&gt;기타 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;요즘에 마이크로소프트 아키텍처(MSA) 때문에 이벤트 소싱과 CQRS에 대한 이야기를 많이하죠. 이런 패턴들도 이벤트 데이터를 입력으로 받기 때문에, Flink와 같은 스트리밍 시스템을 단순히 분석용으로 쓰지 않고 비즈니스에 접목시키려는 움직임도 있습니다[7][10].&lt;/p&gt;
&lt;p&gt;위에서 회원 목록을 가진 데이터베이스는 스트림이 아니다라고 했는데, 관계형 데이터베이스의 변경 내역을 이벤트로 수신해서 스트림 프로세싱을 수행할 수 있는 카프카 커넥트(Kafka Connect)같은 오픈소스들도 있어서 사용할 수 있는 영역이 꽤 넓습니다.&lt;/p&gt;
&lt;h3 id="좋은-스트림-시스템의-조건"&gt;좋은 스트림 시스템의 조건&lt;/h3&gt;
&lt;p&gt;좋은 스트림 시스템은 어떤 걸까요? 이 질문에 대해서 답을 할 수 있다면 아마 스트림 시스템이 무엇을 해줄 수 있고 무엇을 못하는지도 이해할 수 있을 겁니다.&lt;/p&gt;
&lt;h3 id="-입력-데이터가-어느-빈도로-발생하는지-예측할-수-없습니다-즉-발생하는-부하를-정확히-예측하기-힘듭니다"&gt;① 입력 데이터가 어느 빈도로 발생하는지 예측할 수 없습니다. 즉, 발생하는 부하를 정확히 예측하기 힘듭니다&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;부하에 유동적으로 대응할 수 있어야 하며, 데이터 유입이 급증해도 성능이 저하되지 않아야 합니다&lt;/li&gt;
&lt;li&gt;급증한 데이터에 대해 스트림 프로세싱을 수행하지 못한다면, 데이터는 계속 지연되기 때문에 실시간이라고 부르기 힘들겠죠&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;② 데이터는 잘못된 순서로 들어오거나, 데이터가 발생한 시간과 시스템이 데이터를 인식한 시간이 다를 수 있습니다. 전자를 이벤트 시간(Event Time) 후자를 인입 시간(Processing Time) 이라고 부릅니다&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터가 잘못된 순서로 들어오거나 지연되어서 들어온 경우에도 스트림 프로세싱을 수행할 수 있어야 하거나, 적어도 얼마나 지연되면 데이터를 버릴 것인지 기준은 제시할 수 있어야 합니다. 이를 스트림 프로세싱 생태계에서는 이런 기준을 워터마크라고 부릅니다&lt;/li&gt;
&lt;li&gt;워터마크에 대한 내용은 따로 세션을 따서 설명합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="-시스템은-내결함성을-가져야-합니다"&gt;③ 시스템은 내결함성을 가져야 합니다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;현대 시스템이라면 당연히 갖춰야 할 덕목입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="-정확히-한-번-수행"&gt;④ 정확히 한 번 수행&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;스트림 시스템을 보다 보면 정확히 한 번 처리(Exactly Once)를 보장한다는 이야기가 자주 나옵니다. 이는 스트림 데이터 특징 때문에 그런데요.&lt;/li&gt;
&lt;li&gt;스트림 데이터를 처리할 때, 외부 리소스에 의존하는 코드가 있다면 (현재 외부 온도, 주식 가격 등) 데이터를 처리 할 때마다 값이 변하게 됩니다. 이를 비결정적(non-determinism) 이라고 부릅니다.&lt;/li&gt;
&lt;li&gt;이 이야기는 &lt;strong&gt;&lt;code&gt;[정확히 한 번 처리]&lt;/code&gt;&lt;/strong&gt; 에서 더 자세하게 다뤄보겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="데이터-플로우-data-flow"&gt;데이터 플로우 (Data Flow)&lt;/h3&gt;
&lt;p&gt;데이터 플로우는 구글 클라우드 플랫폼(GCP)에서 스트림 분석 시스템을 담당하는 제품입니다. 이 제품명은 구글에서 2015년 발표한 &lt;strong&gt;The Dataflow Model[3]&lt;/strong&gt; 이라는 논문에서 유래가 되었는데 현대 스트림 프로세싱의 핵심 이론들이 잘 설명되어 있습니다.&lt;/p&gt;
&lt;p&gt;개발자 커뮤니티에서는, 웹 개발은 개념만 알고 있으면 언어나 프레임워크가 달라져도 진입장벽이 낮다는 말이 있는데요. 스트림 프로세싱의 세계도 마찬가지입니다. Apache Flink, Beam 등 다양한 제품군들은 사실 공통된 개념을 차용해서 만들어졌기 때문에 코딩 스타일, 용어, 시스템의 가정사항 등이 유사합니다.&lt;/p&gt;
&lt;p&gt;그래서 본 글에서 설명하는 모든 개념들은 DataFlow Model에서 말하는 용어와 동일하구요. 지금부터는 현대 스트림 프로세싱에서 통용되는 일반적인 개념들을 한데 묶어서 &lt;strong&gt;DataFlow Model&lt;/strong&gt; 이라고 칭하겠습니다. 실제 역사적으로는 각 용어의 등장 시기가 조금 상이합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-ae84fad1"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-ae84fad1"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-rMO4XJoQBc45Tt9v-5cfbf979ca5f.jpg"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-ae84fad1"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-rMO4XJoQBc45Tt9v-5cfbf979ca5f.jpg"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Google Cloud DataFlow&lt;/p&gt;
&lt;h3 id="윈도우-라이프사이클"&gt;윈도우 라이프사이클&lt;/h3&gt;
&lt;p&gt;DataFlow Model의 여러 개념중에서 첫 번째로 윈도우에 대해서 설명하려고 하는데요. 사실 스트리밍 시스템과 배치 시스템은 큰 차이가 없습니다. 배치 시스템은 유한(bounded) 데이터를, 스트리밍 시스템은 무한(unbounded) 데이터를 다룬다고 했습니다.&lt;/p&gt;
&lt;p&gt;어떻게 무한대의 데이터를 처리할까요? 스트리밍 시스템은 무한 데이터 속에서 유한개의 데이터를 선정해서 하나의 시점을 만드는데요. 그것이 윈도우 입니다. 윈도우를 사용한다는 건 무한 데이터를 유한개의 데이터로 바꿔서 처리한다는 아이디어로 출발합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-f9c22459"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-f9c22459"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E2vjgD73QcsEhGK4rw0S7g-bcd13101735f.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-f9c22459"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-E2vjgD73QcsEhGK4rw0S7g-bcd13101735f.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;윈도우(Window) 라이프 사이클을 도식화 한 그림&lt;/p&gt;
&lt;p&gt;Apache Flink에서 윈도우는 위 그림과 같은 라이프 사이클을 가집니다. 하나의 윈도우는 입력 스트림(Input Stream)을 소스로 받고 출력 스트림(Output Stream)을 생성하는 큰 구조로 이루어져 있고, 이렇게 생성된 출력 스트림은 또 다른 곳에서 입력 스트림이 됩니다.&lt;/p&gt;
&lt;h3 id="윈도우-할당자-window-assigner"&gt;윈도우 할당자 (Window Assigner)&lt;/h3&gt;
&lt;p&gt;들어온 데이터를 하나 이상의 윈도우에 할당하는 역할을 합니다. Flink에는 기본적으로 텀블링 윈도우, 슬라이딩 윈도우, 세션 윈도우 글로벌 윈도우 4개의 기본 윈도우 할당자를 제공합니다.&lt;/p&gt;
&lt;p&gt;윈도우 할당자는 입력 데이터를 보고 현재 조건에 만족하는 윈도우가 없으면 새로운 윈도우를 생성하고 데이터를 삽입합니다. 하나의 윈도우는 데이터가 모두 담겨있다는 확신이 들면 윈도우 함수를 먹여서 데이터에 연산을 수행하여 데이터를 변환하고 윈도우를 삭제합니다.&lt;/p&gt;
&lt;h3 id="트리거-trigger"&gt;트리거 (Trigger)&lt;/h3&gt;
&lt;p&gt;윈도우는 하나의 &lt;strong&gt;&lt;code&gt;트리거&lt;/code&gt;&lt;/strong&gt;와 &lt;strong&gt;&lt;code&gt;윈도우 함수(Window Function)&lt;/code&gt;&lt;/strong&gt; 를 가지고 있습니다. 함수에는 윈도우에 들어온 데이터에 대해 어떤 연산을 적용할 것인지 정의하고, 트리거는 언제 윈도우에 있는 데이터로 연산을 수행할 지 결정하는 역할을 합니다. 예를 들면, 윈도우 안에 있는 데이터가 4개 이상이라면 혹은 워터마크가 윈도우 종점을 통과했다면 이라는 조건을 달 수 있습니다.&lt;/p&gt;
&lt;h3 id="소멸자-evictor"&gt;소멸자 (Evictor)&lt;/h3&gt;
&lt;p&gt;트리거가 발생해서 윈도우를 구체화 하기 전에, 데이터 일부를 연산에서 제외시키도록 결정하는 역할을 하고 있습니다. 일종의 필터라고 볼 수 있겠네요.&lt;/p&gt;
&lt;h3 id="keyed-or-non-keyed-window"&gt;Keyed or Non-Keyed Window&lt;/h3&gt;
&lt;p&gt;Flink에서는 &lt;strong&gt;&lt;code&gt;keyBy&lt;/code&gt;&lt;/strong&gt; 함수로 데이터의 특정 값을 키로 잡을 수 있습니다. 만약 키를 잡지 않으면 하나의 세션에서 처리하구요. 키를 잡으면 병렬로 처리됩니다. 카프카에서 PartitionKey를 생각하시면 되겠습니다.&lt;/p&gt;
&lt;h3 id="4가지-기본-윈도우"&gt;4가지 기본 윈도우&lt;/h3&gt;
&lt;p&gt;지금부터 대표적인 윈도우 4가지를 소개하겠습니다. 이번 장에 첨부된 내용과 그림은 Flink 공식문서[4]만을 인용했습니다.&lt;/p&gt;
&lt;h3 id="텀블링-윈도우-tumbling-window"&gt;텀블링 윈도우 (Tumbling Window)&lt;/h3&gt;
&lt;p&gt;고정 크기 윈도우라고도 부르며 일정한 크기로 윈도우를 할당합니다. 데이터의 개수가 될수도 있고 시간이 될수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-12c89495"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-12c89495"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-B8b_1CqHk952aCEszUPg5Q-ecc04a35ad4b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-12c89495"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-B8b_1CqHk952aCEszUPg5Q-ecc04a35ad4b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;텀블링 윈도우&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# type: DataStream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# tumbling event-time windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TumblingEventTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# tumbling processing-time windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TumblingProcessingTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# daily tumbling event-time windows offset by -8 hours.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TumblingEventTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;윈도우의 기준을 이벤트 시간으로 나눌지 프로세싱 시간으로 나눌지도 결정할 수 있고 시간도 밀리초 단위까지 세분화 해서 윈도우를 자를 수 있습니다. 위 코드에서 마지막 예시는 Offset을 &lt;code&gt;TumblingEventTimeWindows.of&lt;/code&gt; 함수의 두번째 인자로 주었는데요. 시간을 나누는 기준을 의미합니다. 예를 들어 &lt;code&gt;Time.days(1)&lt;/code&gt; 을 사용하면 기본값은 00시 부터 23:59:59 인데, 이를 아침 8시부터 다음날 아침 8시로 지정하고 싶다면? 두 번째 파라미터로 +8 시간을 넘겨주면 됩니다. (위 함수의 두번 째 파라미터의 이름은 Offset 입니다)&lt;/p&gt;
&lt;h3 id="슬라이딩-윈도우sliding-window"&gt;슬라이딩 윈도우(Sliding Window)&lt;/h3&gt;
&lt;p&gt;슬라이딩 윈도우 또한 고정 크기의 윈도우를 가지는데, &lt;code&gt;window slide parameter&lt;/code&gt; 를 지정해서 윈도우가 생성되는 빈도를 결정합니다. 즉, 크기와 생성 빈도수를 결정하기 때문에 빈도수를 크기보다 작은 값으로 주면 데이터가 여러 윈도우에 겹치게 되겠죠?&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-33885aca"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-33885aca"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DYBTe_g4DspkDeuCX-d4jQ-a8f58fb8cf8a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-33885aca"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-DYBTe_g4DspkDeuCX-d4jQ-a8f58fb8cf8a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;슬라이딩 윈도우&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# type: DataStream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# sliding event-time windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SlidingEventTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# sliding processing-time windows&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SlidingProcessingTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# sliding processing-time windows offset by -8 hours&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SlidingProcessingTimeWindows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;윈도우를 생성하는 방법 자체는 비슷하고, &lt;code&gt;window&lt;/code&gt; 함수로 윈도우 할당자의 성격을 지정하는게 인상적이죠. &lt;strong&gt;&lt;code&gt;변수 input&lt;/code&gt;&lt;/strong&gt;은 데이터 스트림 객체라고만 알고 있으시면 됩니다.&lt;/p&gt;
&lt;h3 id="세션-윈도우-session-window"&gt;세션 윈도우 (Session Window)&lt;/h3&gt;
&lt;p&gt;세션 윈도우는 특이하게 특정 시간 동안 데이터가 나타나지 않으면 구체화 하는 윈도우입니다. 예를 들면, 유저 단위로 프로세싱하려고 할 때 사용할 수 있습니다. (e.g. 특정 유저가 10분간 활동이 없으면, 윈도우를 구체화 한다)&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-21937c89"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-21937c89"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SVsv4hsJ5lZCBqj1mZ_Meg-2a3c701e3798.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-21937c89"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-SVsv4hsJ5lZCBqj1mZ_Meg-2a3c701e3798.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;세션 윈도우&lt;/p&gt;
&lt;h3 id="글로벌-윈도우global-window"&gt;글로벌 윈도우(Global Window)&lt;/h3&gt;
&lt;p&gt;글로벌 윈도우는 같은 키를 가진 모든 데이터를 하나의 전역 윈도우에 담습니다. &lt;strong&gt;글로벌 윈도우는 반드시 커스텀 트리거를 정의해서 사용해야 하구요, 그러지 않으면 아무런 역할도 하지 않습니다. 왜냐면 윈도우에 끝 지점이라는게 존재하지 않아서 기본 트리거는 아무런 Firing 도 안하기 때문입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-cfcbf9b4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-cfcbf9b4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xjBX0P9EBQZyTM2w0dt4WA-5a91d2ad2e60.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-cfcbf9b4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-xjBX0P9EBQZyTM2w0dt4WA-5a91d2ad2e60.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;글로벌 윈도우&lt;/p&gt;
&lt;h3 id="allowed-lateness와-side-output"&gt;Allowed Lateness와 Side Output&lt;/h3&gt;
&lt;p&gt;스트리밍 시스템에서는 크게 두가지 시간 도메인이 중요합니다. 이벤트가 실제로 발생된 시점인 이벤트 시간과 시스템이 데이터를 인지한 인입 시간이죠. 이벤트 시간과 인입 시간이 완전히 동일하면 참 좋겠지만, 실제로는 괴리가 있습니다. 스트리밍 시스템이란 책[5] 에서는 다소 극단 적인 예시를 듭니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;비행기 안에서 스마트폰 앱에서 터치를 하고 곧바로 비행기 모드로 돌린 다음, 2시간 뒤 비행기에서 내리고 비행기 모드를 풀면 서버는 2시간 이전의 데이터를 받게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;책에서 든 예시는 극단적이지만 서버-to-서버 통신에서도 크고 작은 지연은 항상 발생됩니다. 여기서 중요한 건 스트리밍 시스템의 트리거는 어쨋든 윈도우를 언제 닫을지를 결정해야 한다는 겁니다. 언제 들어올지 모르는 데이터를 위해 윈도우를 구체화 하지 않으면 성능이 저하됩니다.&lt;/p&gt;
&lt;p&gt;Flink에서는 Allowed Lateness 라고 불리는 수치를 결정해서 데이터가 늦으면 언제까지 늦는지를 지정합니다. 기본값은 0이구요. 기본값을 이용하면 윈도우를 지나서 도착한 데이터는 누락(Drop)됩니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# type: DataStream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;assigner&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_lateness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;실제로는 데이터가 누락되는게 용인 될 수 없는 요구사항이 많을 겁니다. 그 때는 SideOutput으로 선언해서 늦게 들어온 데이터를 별도의 스트림으로 다룰 수 있습니다. 이를테면 해당 스트림을 이용해서 카프카의 dead letter 토픽으로 전송해서 누락된 데이터를 수신받을 수 있습니다&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;late_output_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OutputTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;late-data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# type: DataStream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;assigner&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_lateness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_output_late_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;late_output_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;windowed&lt;/span&gt; &lt;span class="n"&gt;transformation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;late_stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_side_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;late_output_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="워터마크-watermark"&gt;워터마크 (Watermark)&lt;/h3&gt;
&lt;p&gt;위에서 이벤트 시간과 인입 시간에 대해 지속적으로 언급했었는데요. 워터마크에 대해 이야기 할 때 중요한 용어이기 때문에 다시 한 번 정리 할 필요가 있습니다. Flink 공식 문서에 아주 좋은 그림이 있어서 인용해봅니다[6].&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a2022aee"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a2022aee"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-38O0TZJIn3Lof8XwZ-6-NA-f7ce1ed3801a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a2022aee"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-38O0TZJIn3Lof8XwZ-6-NA-f7ce1ed3801a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Apache Flink의 시간 도메인&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;이벤트 시간 (Event Time)&lt;/code&gt;&lt;/strong&gt;은 데이터를 발생시킨 주체의 시간을 말하고, &lt;strong&gt;&lt;code&gt;인입 시간 (Processing Time)&lt;/code&gt;&lt;/strong&gt;은 윈도우가 데이터를 받은 시간을 말합니다. 실시간 스트리밍에서 이런 인식이 중요한 이유는 이 두 시간 사이에는 반드시 왜곡이 발생하기 때문입니다.&lt;/p&gt;
&lt;p&gt;윈도우라는 개념 자체가 ‘무한’ 데이터를 ‘유한’개의 집합으로 나눠서 다루는 개념이라고 했었죠. 10분 단위의 텀블링 윈도우를 사용한다면, &lt;code&gt;10:00 ~ 10:10&lt;/code&gt; 사이에 존재하는 모든 데이터가 도착하고 윈도우 함수를 먹여야 제대로 된 값이 계산될 겁니다. 실제 세상에서는 10:09 의 데이터가 10:11 에 도착할 수 있기 때문에 인입 시간을 기준으로 윈도우를 구체화 하는 건 데이터가 누락 될 위험이 굉장히 커집니다.&lt;/p&gt;
&lt;p&gt;여기서 워터마크가 사용됩니다. 워터마크는 $&lt;code&gt;F(P) -&amp;gt; E&lt;/code&gt;$ 의 형태로 인입 시간을 받으면 이벤트 시간을 리턴하는 함수로 표현할 수 있습니다. 반환된 E는 지금 이 순간 부터 E 이전에 존재하는 모든 데이터를 받았다는 의미로, 다르게 말하면 앞으로는 E 이전의 시간을 가진 데이터가 입력으로 주어지는 일이 없다는 말이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-85caf1c4"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-85caf1c4"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-23nFycOs3-pT3ql66VbeKA-82e07f314944.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-85caf1c4"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-23nFycOs3-pT3ql66VbeKA-82e07f314944.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The DataFlow Model — Watermark&lt;/p&gt;
&lt;p&gt;인입 시간과 이벤트 시간의 왜곡이 존재할 때 아주 완벽한 워터마크 함수는 위 그래프에서 굵은 실선을 그리는 함수가 될 겁니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 음.. 근데 뭔가 이상합니다. 이러한 워터마크를 정확히 계산할 수 있나요? &lt;strong&gt;스트리밍 시스템은 입력 소스에 대한 어떤 정보도 없기 때문에 완벽한 워터마크를 만드는 건 애초에 불가능합니다.&lt;/strong&gt; 그래서 &lt;strong&gt;워터마크 함수는 휴리스틱할 수 밖에 없습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;완벽한 워터마크를 만드는 게 불가능 한 건 아닙니다. 제가 참고한 책 스트리밍 시스템[5] 에서는 완벽한 워터마크를 만들기 위한 두 가지 조건을 아래처럼 설명했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apache Kafka 처럼 부분 순서를 보장하는 시스템이 존재하고 이벤트를 완벽히 시간 순서대로 순차적으로 쌓은 경우&lt;/li&gt;
&lt;li&gt;그냥 프로세싱 시간을 기준으로 윈도우를 구체화 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;[조건 1]&lt;/strong&gt;을 보면 명확하죠, 애초에 순서대로 데이터가 도착한다는 보장이 있으면 완벽한 워터마크를 쉽게 만들 수 있습니다. 10:00~10:10 의 텀블링 윈도우를 가지고 있었는데 이벤트 시간이 10:11인 데이터를 받았다면 윈도우를 구체화 해도 된다는 명확한 신호입니다. &lt;strong&gt;[조건 2]&lt;/strong&gt;는 그냥 이벤트 시간을 무시하는 것이기 때문에 완벽한 워터마크를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;즉, 위 두가지 경우가 아니라면 워터마크는 항상 휴리스틱 할 수 밖에 없고 완벽한 계산을 요구하는 비즈니스라면 늦게 들어온 데이터(lateness)를 어떻게 처리해야 하는가를 고민해야 합니다. 실제로 Flink를 이용해서 비즈니스 로직을 구현하신 다면 관련 유관부서와 이런 이야기를 하게 될겁니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;데이터가 발생한 시점부터 최대 30분 이내로는 카프카로 메시지가 전달받을 것을 가정했다. 그 이상의 괴리가 발생하면 데이터가 누락될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이것이 워터마크에 대한 기본적인 개념이었다면, 실제로 시스템을 구축할 때, 워터마크가 어떻게 생성되고 스트림이 병렬로 생성되었을 때, 각 스트림으로 전파되는지 알 필요가 있습니다. 이 내용은 이 논문[8]에 잘 나와있습니다. 구글 직원들이 작성한 논문이구요. 자사 제품인 Google Cloud DataFlow와 Flink를 비교하는 내용을 담고 있습니다.&lt;/p&gt;
&lt;h3 id="정확히-한-번-처리-exactly-once-semantic"&gt;정확히 한 번 처리 (Exactly Once Semantic)&lt;/h3&gt;
&lt;p&gt;스트리밍 시스템들을 보면 &lt;strong&gt;정확히 한 번 처리(Exactly Once)&lt;/strong&gt;하는 것을 아주 중요하게 강조하고 있습니다.&lt;/p&gt;
&lt;p&gt;데이터 처리에 있어서 누락이 없어야 하는 건 굉장히 중요하죠. 윈도우에서 연산을 먹이는 게 누락되면 안되고요. 정당한 시점에 데이터를 받았는데 윈도우 할당자가 적절한 윈도우에 포함시키지 않고 누락시켰다면 그것도 문제입니다. 정확히 한 번 처리에 있어서 두 가지 관점을 볼 수 있습니다. &lt;strong&gt;① 윈도우가 정확히 한 번 구체화 되었는가?, &lt;/strong&gt;② 데이터가 중복으로 들어가진 않았는가?&lt;/p&gt;
&lt;p&gt;만약, 스트리밍 시스템이 이걸 제대로 하지 못한다면 우리가 지금까지 공부한 모든 것들이 쓰레기가 됩니다. 그냥 배치 시스템의 결과를 기다리면 되는거죠. 스트리밍 시스템을 쓸 이유가 없습니다. 다행히도 구글 데이터 플로우는 이에 대한 방법을 제공하고 있습니다.&lt;/p&gt;
&lt;h3 id="-윈도우가-정확히-한-번-구체화-되었는가"&gt;① 윈도우가 정확히 한 번 구체화 되었는가?&lt;/h3&gt;
&lt;p&gt;애초에 윈도우가 정확히 한 번만 구체화 되야 하는게 왜 중요할까요? 왜냐면 윈도우 함수 자체는 멱등(idempotent)하지 않을 수 있기 때문입니다. 예를 들어, 은행에서 이상 거래 감지 프로세스를 돌리고 있는데 윈도우 함수에 외부 정보를 인용하면 어떨까요? 예를 들면, 해당 고객의 현재 위치나 거래 기록은 API를 호출하는 시점에 따라 달라지는 값입니다.&lt;/p&gt;
&lt;p&gt;윈도우가 2번 이상 구체화 되면 윈도우가 생성한 출력 스트림의 데이터가 완전히 달라질 수 있습니다. 이게 어떤 문제를 일으킬지 모르는 거죠. Google Cloud DataFlow 에서는 이런 비결정적인 처리를 &lt;strong&gt;체크포인트(Checkpoint)&lt;/strong&gt;를 사용해서 결정적인 처리로 치환해서 문제를 해결합니다. 각 변환에서 나오는 출력 스트림을 고유 ID를 붙여서 체크포인트에 저장한 다음에 이후에 입력 스트림은 이 체크포인트를 재사용 해서 정확히 한 번 처리를 보장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-55224461"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-55224461"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wR2T4gOk_hzAPPXtvBh3Zw-c44612e89901.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-55224461"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-wR2T4gOk_hzAPPXtvBh3Zw-c44612e89901.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://flink.apache.org/2020/10/15/from-aligned-to-unaligned-checkpoints-part-1-checkpoints-alignment-and-backpressure/"&gt;Apache Flink Checkpoint&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그래서 Apache Flink에서는 이 체크포인트 정보를 관리하는게 굉장히 중요합니다. 그래서 Amazon S3에 체크포인트를 저장하고 관리하는게 마음이 아주 편안해지겠죠. 아마존 제품군을 쓰신다면 Amazon Kinesis Data Analytics (Amazon KDA)도 좋은 선택입니다. 이런 고민을 할 필요 없이 행복하게 코딩만 하면 됩니다.&lt;/p&gt;
&lt;h3 id="-데이터가-중복으로-들어가진-않았는가"&gt;② 데이터가 중복으로 들어가진 않았는가?&lt;/h3&gt;
&lt;p&gt;이는 간단하게 데이터에 고유 ID를 부여해서 윈도우에 중복된 데이터가 들어가 있는지 아닌지만 검증해서 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;스트리밍 시스템[5] 에선, 위 두가지 사항을 구현하기 위해 어떻게 성능을 최적화를 했는지에 대한 깊은 이야기도 공유하고 있습니다. 교환법칙이 성립하는 연산에 대해서는 스트림을 나누지 않고 하나로 합쳐서 체크포인트가 생성되는 지점을 줄이기도 하구요. 블룸 필터를 사용해서 데이터의 중복 체크를 효율적으로 하기도 합니다. 더 많은 내용이 궁금하시다면 서적 [5]를 추천합니다.&lt;/p&gt;
&lt;h3 id="한계-limitation"&gt;한계 (Limitation)&lt;/h3&gt;
&lt;p&gt;스트리밍 시스템 [5] 책의 서문에서는 잘 만들어진 스트리밍 시스템이 있다면, 더 이상 배치 시스템은 필요하지 않다라는 꽤 대범한 멘트로 독자를 유혹하고 있습니다. 과연 그럴까요? 이 세션에서는 제가 현업에 스트리밍 시스템을 적용하면서 아직까지 해결하지 못한 고민들을 한계점이라고 적었습니다. 거대한 지구 어디선가 누군가는 이 문제를 해결했을 수도 있고, 문제라고 인식하지 않을 수도 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;재처리 (Re-processing)&lt;/strong&gt;
윈도우 함수의 연산이 변경되었다면 어떻게 할까요? 이미 지나간 입력 스트림에 대해서 처음부터 연산을 수행할 수 있을까요? 일단, 너무 느리겠죠. Map/Reduce기반의 배치 시스템에서는 단 번에 대규모 클러스터를 운용해 수초내에 처리할 수 있는게 스트림 프로세싱에서는 조금 버거울 수 있습니다. 또한, 모든 데이터가 카프카에 저장되어 있다면 아주 좋겠지만.. 카프카에서 제거된 데이터가 있으면 아주 까다로워 집니다. &lt;strong&gt;시스템을 구축하고 시간이 몇 개월 지나서 스키마에 새로운 필드를 추가한다거나 집계 하려는 필드를 변경하려는 요구사항이 들어오면 어떨까요?&lt;/strong&gt; 상상만 해도 벌써 퇴근하고 싶어집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;누락 데이터&lt;/strong&gt;
추천 시스템, 이상 탐지 등의 요구사항에서는 일부 데이터가 누락되어도 경향성을 계산하는데는 큰 문제가 없을 수 있습니다. 표본 집단에 대해서 연산을 실행한거라고 보면 되니까요. 반면, 집계 처리와 같이 데이터 누락이 발생되면 곤란한 상황도 있을 겁니다. 스트리밍 시스템이 워터마크를 이용해서 어느정도 휴리스틱하게 윈도우를 확정짓긴 하지만 여전히 데이터가 누락될 수 있다는 가정이 존재하기 때문에 이를 슬기롭게 대처할 수 있는 방안도 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모리 문제&lt;/strong&gt;
스트림 프로세싱이 무한 데이터를 다루는 컨셉인데, 메모리는 무한하지 않기 때문에 생기는 문제들이 있습니다. 윈도우를 무작정 크게 1시간 단위로 잡는다면 메모리나 디스크에 1시간 단위의 데이터를 모두 가지고 있어야 합니다. 사실 1시간 단위의 윈도우를 쓴다면 그냥 배치 시스템을 쓰는게 더 좋은 대안일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocking Operation&lt;/strong&gt;
스트리밍 코드 어디선가 외부 리소스에서 정보를 받아오는 HTTP 호출을 하면, 출력 스트림이 나오기 까지 지연이 계속 추가될 수 있습니다. 먼저, HTTP 호출을 예시로 들었지만 2개 이상의 스트림을 조인해서 연산을 하는 경우에도 처리율이 낮으면 지연시간이 또 느려집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다른 분야에서는 여러 고수들과 업계 선배분들이 이미 매를 먼저 맞아놔서, 후발대로 개발을 시작한 우리가 편하게 일할 수 있는 환경이 갖추어져 있는데요. 대표적으로 도커와 쿠버네티스가 있겠죠.. 저는 이제 쿠버네티스 없는 환경에서는 일 못할 것 같습니다.&lt;/p&gt;
&lt;p&gt;그런데 스트리밍 시스템은 아직 도입한 사례도 찾기 힘들고, 업계의 경험이 미성숙한 상태라서 매를 맞는 대상이 내가 될 수 있습니다. 스트리밍 시스템 도입을 위해 이 글까지 찾아오신 분들이 계시면 같이 화이팅 하자는 말씀 드립니다.&lt;/p&gt;
&lt;h3 id="flink-설치-및-플레이"&gt;Flink 설치 및 플레이&lt;/h3&gt;
&lt;p&gt;마지막으로, 제가 스트리밍 프로세싱을 직접 코딩하기 위해 만들었던 인프라 환경을 공유하고 마치겠습니다. Apache Flink 환경을 구축해보고 파이썬으로 테스트 해볼 수 있습니다. 소스 코드는 &lt;a href="https://github.com/scalalang2/pyflink-playground"&gt;이 링크&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;p&gt;환경 구축은 쿠버네티스를 사용했고 쿠버네티스에서 다양한 서비스를 설치해서 다양한 환경의 인프라를 세팅해보고 코딩해보실 수 있습니다. 아래는 기본으로 설치하는 서비스들입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apache Flink&lt;/strong&gt;
&lt;a href="https://github.com/apache/flink-kubernetes-operator"&gt;Apache Flink Kubernetes Operator&lt;/a&gt;를 이용해서 설치합니다. Flink는 태스크를 관리하는 JobManager와 실제 태스크를 수행하는 TaskManager 두 개의 인스턴스로 구성되는데요. 이 오퍼레이터를 이용하면 새로운 태스크가 등록될 때 동적으로 TaskManager를 생성하고 할당해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache Kafka&lt;/strong&gt;
대부분의 스트리밍 시스템들은 입력 데이터 소스로 아파치 카프카를 이용합니다. 데이터의 영속성을 보장하면서 메시지 큐 역할을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/provectus/kafka-ui"&gt;Apache Kafka UI&lt;/a&gt;
카프카의 메시지를 웹 인터페이스로 확인하기 위해 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MinIO&lt;/strong&gt;
Amazon S3의 API와 완벽히 호환되는 쿠버네티스 네이티브 오브젝트 스토리지입니다. 로컬에서 MinIO로 테스트 해보고 이를 S3 레벨까지 올려서 프로덕션 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 외에도 필요에 따라 MongoDB, MySQL등을 추가로 설치해서 사용하시면 됩니다. 코드에서는 Helm Chart를 이용해서 서비스들을 관리하는데요. Helm Chart를 이용하면 복잡한 인프라도 선언적으로 쓸 수 있고 관리가 편합니다.&lt;/p&gt;
&lt;h3 id="로컬-쿠버네티스-설치"&gt;로컬 쿠버네티스 설치&lt;/h3&gt;
&lt;p&gt;쿠버네티스를 로컬에 설치하기 위해 &lt;a href="https://k3d.io/v5.4.9/#installation"&gt;k3d&lt;/a&gt;를 이용했습니다.&lt;/p&gt;
&lt;p&gt;$ k3d cluster create local-cluster \\
&amp;ndash;api-port 6550 \\
-p &amp;ldquo;30000-30010:30000-30010@server:0&amp;rdquo; \\
&amp;ndash;agents 2&lt;/p&gt;
&lt;p&gt;위 명령어를 도커가 실행중인 상태에서 실행하셔야 됩니다. 도커 위에 가상의 쿠버네티스 환경을 설치하는 명령어 입니다. &lt;code&gt;-p 30000–30010:30000–30010&lt;/code&gt; 옵션으로 쿠버네티스의 NodePort와 호스트의 포트를 바인딩 했습니다. 이걸 설정함으로써 쿠버네티스의 올라간 서비스를 로컬에서 접근할 수 있습니다.&lt;/p&gt;
&lt;h3 id="flink-cluster-생성"&gt;Flink Cluster 생성&lt;/h3&gt;
&lt;p&gt;Flink Kubernetes Operator로 Flink를 설치합니다.&lt;/p&gt;
&lt;p&gt;# Insall Flink Kubernetes Operator&lt;br&gt;
helm repo add flink-operator-repo \\
&lt;a href="https://downloads.apache.org/flink/flink-kubernetes-operator-1.4.0/"&gt;https://downloads.apache.org/flink/flink-kubernetes-operator-1.4.0/&lt;/a&gt;&lt;br&gt;
helm install -f flink/values.yml \\
flink-kubernetes-operator \\
flink-operator-repo/flink-kubernetes-operator&lt;br&gt;
&lt;br&gt;
# Deploy Flink Server&lt;br&gt;
kubectl apply -f flink/deployment.yml&lt;/p&gt;
&lt;p&gt;helm 으로 시작하는 두 명령어는 오퍼레이터를 설치하는 명령어이구요. 실제 Flink Seerver는 kubectl 명령어로 설치했습니다.&lt;/p&gt;
&lt;h3 id="컴포넌트-설치"&gt;컴포넌트 설치&lt;/h3&gt;
&lt;p&gt;Kafka, MinIO 등의 필요 컴포넌트를 설치합니다. 이는 헬름 차트를 이용했습니다. 차트 이름이 반드시 play 여야 한다는 점에 유의해주세요.&lt;/p&gt;
&lt;p&gt;$ helm install play ./helm-charts&lt;/p&gt;
&lt;p&gt;모두 설치하고 나서 &lt;code&gt;kubectl get pods&lt;/code&gt; 명령어로 확인해보면 아래 처럼 서비스가 설치된 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;$ kubectl get pods&lt;br&gt;
NAME READY STATUS RESTARTS AGE&lt;br&gt;
flink-kubernetes-operator-545c768689-m8kpv 1/1 Running 0 11m&lt;br&gt;
play-minio-78fd77d9cd-zw7qh 0/1 Pending 0 2m21s&lt;br&gt;
play-minio-post-job-crwz7 1/1 Running 0 2m21s&lt;br&gt;
play-kafka-faker-68bc96b949-9xrth 1/1 Running 0 2m21s&lt;br&gt;
play-kafka-0 1/1 Running 0 2m21s&lt;br&gt;
play-kafka-ui-6d464f9fd-4mg5w 1/1 Running 0 2m21s&lt;/p&gt;
&lt;p&gt;여기 까지 설치하고 나면 &lt;code&gt;localhsot:30000&lt;/code&gt; 으로 접속해서 Flink Dashboard가 나오는 걸 보실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d477b0c7"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d477b0c7"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-JquzfxZuDZpGVaUt-617aa25857ff.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d477b0c7"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/0-JquzfxZuDZpGVaUt-617aa25857ff.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Apache Flink Dashboard&lt;/p&gt;
&lt;p&gt;마지막으로 Job을 등록하려면 flink 명령어를 설치해야되는데요. 하나의 바이너리로 만들어진게 없어서 Flink에서 압축 파일을 직접 다운로드 받아서 해제하고 .bashrc 나 .bash_profile 에 명령어를 PATH 환경변수로 등록해야 합니다. &lt;a href="https://nightlies.apache.org/flink/flink-docs-stable/docs/try-flink/local_installation/"&gt;자세한 설치 가이드는 이곳에 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;버전에 민감한 친구라서 아래 버전을 반드시 맞춰주셔야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.9&lt;/li&gt;
&lt;li&gt;Java 11&lt;/li&gt;
&lt;li&gt;Flink 1.17.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 글을 작성하는 시점에 1.17.0이 최신버전이라서 이렇게 맞추었지만, 소스 코드에서 버전을 적절히 변경하시면 이전 버전에서도 동작합니다. Apache Flink Kubernetes Operator에서 지원하기만 하면 잘 동작했습니다.&lt;/p&gt;
&lt;p&gt;$ flink &amp;ndash;version&lt;br&gt;
Version: 1.17.0&lt;/p&gt;
&lt;p&gt;여기 까지 인내심을 가지고 잘 따라오셨으면 축하드립니다! 이제 드디어 Flink를 이용해서 스트리밍 시스템을 코딩해볼 수 있는 단계까지 오셨습니다. 다음으로는, 아주 간단한 예시와 무엇부터 공부하시면 좋을지 소개하고 글을 마무리하겠습니다.&lt;/p&gt;
&lt;h3 id="table-api-vs-datastream-api"&gt;Table API vs DataStream API&lt;/h3&gt;
&lt;p&gt;Flink는 크게 Table API와 DataStream API를 사용해서 코딩할 수 있는데요. DataStream이 조금 더 저수준이고 TableAPI가 고수준의 인터페이스를 제공합니다. 즉, 처음 시작하시는 분들은 TableAPI만 쓰고도 왠만한 요구사항은 전부 커버할 수 있구요. 조금 더 딥하게 봐야겠다 하면 그 때 DataStream API를 공부해도 무방합니다.&lt;/p&gt;
&lt;p&gt;TableAPI는 스트리밍 SQL이라고 하는 특수한 SQL문법을 사용하는데, &lt;a href="https://calcite.apache.org/docs/reference.html"&gt;Apache Calcite&lt;/a&gt; 기반이라고 합니다. 아래 코드는 가짜 데이터를 생성하는 출력 스트림을 만들고 그걸 카프카에 저장하는 예제입니다.&lt;/p&gt;
&lt;p&gt;env = StreamExecutionEnvironment.get_execution_environment()&lt;br&gt;
t_env = StreamTableEnvironment.create(stream_execution_environment=env)&lt;br&gt;
&lt;br&gt;
t_env.get_config().set(&amp;lsquo;pipeline.jars&amp;rsquo;, known_args.dep)&lt;br&gt;
t_env.get_config().set(&amp;lsquo;pipeline.classpaths&amp;rsquo;, known_args.dep)&lt;br&gt;
&lt;br&gt;
# define source table with datagen&lt;br&gt;
t_env.execute_sql(&amp;quot;&amp;quot;&amp;quot;&lt;br&gt;
CREATE TABLE bank_transfers(&lt;br&gt;
transaction_id BIGINT,&lt;br&gt;
from_account_id BIGINT,&lt;br&gt;
to_account_id BIGINT,&lt;br&gt;
amount DECIMAL(32, 2)&lt;br&gt;
) WITH (&lt;br&gt;
&amp;lsquo;connector&amp;rsquo; = &amp;lsquo;datagen&amp;rsquo;&lt;br&gt;
)&lt;br&gt;
&amp;ldquo;&amp;rdquo;&amp;quot;)&lt;br&gt;
&lt;br&gt;
# define sink table with kafka connector&lt;br&gt;
t_env.execute_sql(&amp;quot;&amp;quot;&amp;quot;&lt;br&gt;
CREATE TABLE bank_transfers_sink(&lt;br&gt;
transaction_id BIGINT,&lt;br&gt;
from_account_id BIGINT,&lt;br&gt;
to_account_id BIGINT,&lt;br&gt;
amount DECIMAL(32, 2)&lt;br&gt;
) WITH (&lt;br&gt;
&amp;lsquo;connector&amp;rsquo; = &amp;lsquo;kafka&amp;rsquo;,&lt;br&gt;
&amp;rsquo;topic&amp;rsquo; = &amp;lsquo;bank_transfers&amp;rsquo;,&lt;br&gt;
&amp;lsquo;properties.bootstrap.servers&amp;rsquo; = &amp;lsquo;play-kafka-headless:9092&amp;rsquo;,&lt;br&gt;
&amp;lsquo;format&amp;rsquo; = &amp;lsquo;json&amp;rsquo;&lt;br&gt;
)&lt;br&gt;
&amp;ldquo;&amp;rdquo;&amp;quot;)&lt;br&gt;
&lt;br&gt;
t_env.sql_query(&amp;ldquo;SELECT * FROM bank_transfers&amp;rdquo;) \\
.execute_insert(&amp;ldquo;bank_transfers_sink&amp;rdquo;).wait()&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bank_transfers&lt;/code&gt; 테이블은 datagen이라는 커넥터를 사용해서 내가 이런 데이터를 만들 거라는 스키마를 선언합니다. 그리고 &lt;code&gt;INSERT INTO A SELECT * FROM B&lt;/code&gt; 문법으로 B의 테이블에서 A로 저장한다는 쿼리를 실행합니다.&lt;/p&gt;
&lt;p&gt;여기서 커넥터(Connector)는 입출력 데이터 소스를 지정하는 방식을 말하는데요. Flink에서는 Kafka, ElasticSearch, MongoDB, FileSystem, JDBC 등의 커넥터를 기본적으로 지원합니다. 여기서 지원하지 않는 커넥터를 입출력 소스로 쓸려고 하면 매우 슬픈 길로 들어가야 합니다.&lt;/p&gt;
&lt;p&gt;마지막으로 이렇게 작성한 코드를 Flink 서버로 업로드 하면 자동으로 태스크가 실행되는 걸 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;flink run -py ../jobs/helloworld/main.py -m localhost:30000&lt;/p&gt;
&lt;h3 id="레퍼런스"&gt;레퍼런스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[1] &lt;a href="https://arxiv.org/pdf/2008.00842.pdf#page=29&amp;amp;zoom=100,396,978"&gt;A Survey on the Evolution of Stream Processing Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;a href="https://flink.apache.org/use-cases/"&gt;Use Cases | Apache Flink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;a href="https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43864.pdf"&gt;The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/dev/datastream/operators/windows/#window-functions"&gt;Windows | Apache Flink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href="http://www.yes24.com/Product/Goods/102360104"&gt;스트리밍 시스템 | Yes24&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[6] &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/concepts/time/"&gt;Timely Stream Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[7] &lt;a href="https://dl.acm.org/doi/10.1145/3317287.3321612"&gt;Online Event Processing | Achieving consistency where distributed transactions have failed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[8] &lt;a href="http://www.vldb.org/pvldb/vol14/p3135-begoli.pdf"&gt;Watermarks in Stream Processing Systems: Semantics and Comparative Analysis of Apache Flink and Google Cloud Dataflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[9] &lt;a href="https://github.com/scalalang2/pyflink-playground"&gt;PyFlink Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[10] &lt;a href="https://netflixtechblog.com/towards-a-reliable-device-management-platform-4f86230ca623"&gt;Towards a Reliable Device Management Platform&lt;/a&gt; | Netflix&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>우리는 애플리케이션이 필요하다</title><link>https://scalalang2.com/p/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%98%EB%8B%A4-3e40f48458f-2023-01-24_-----------------3e40f48458f/</link><pubDate>Tue, 24 Jan 2023 14:15:15 +0000</pubDate><guid>https://scalalang2.com/p/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%98%EB%8B%A4-3e40f48458f-2023-01-24_-----------------3e40f48458f/</guid><description>&lt;p&gt;이 글은 2023년에 쓰는 처음이자 마지막 블록체인 글이며 블록체인에 대한 나의 다양한 회의적인 시각과 개인적인 희망을 담아 원하는 발전 방향을 제시한다.&lt;/p&gt;
&lt;h3 id="우리는-실패하고-있다"&gt;우리는 실패하고 있다&lt;/h3&gt;
&lt;p&gt;이 글을 쓰기 위해 펜을 잡은 2023년 1월 24일에는 수 많은 블록체인들이 있으며, 매일 같이 트위터에서 존재감을 드러내고 있다. 대표적으로 이더리움, 폴리곤, 아발란체, 옵티미즘, 아비트럼, 코스모스, 솔라나, 앱토스, 스이 등이 있다. &lt;strong&gt;이더리움&lt;/strong&gt;을 제외한 많은 블록체인들은 &lt;strong&gt;이더리움의 부족한 블록 공간&lt;/strong&gt;을 채우겠다고 선언하면서 성장했다.&lt;/p&gt;
&lt;p&gt;그리고 2022년 테라와 FTX 붕괴와 미 연준의 인플레이션 억제를 위한 유동성 회수로 암호화폐 시장 전체적으로 가격이 급락하면서, 블록체인은 수요를 잃고 오히려 공급이 넘치는 상황이 연출되게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-0bd8c61a"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-0bd8c61a"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RqhgfOyQ5204QW4e20Mdvg-a14f8e909ff9.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-0bd8c61a"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-RqhgfOyQ5204QW4e20Mdvg-a14f8e909ff9.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;폴리곤, 옵티미즘, 아비트럼의 일간 트랜잭션 발생 수 [&lt;a href="https://dune.com/defimochi/eth-l2-user-overview"&gt;https://dune.com/defimochi/eth-l2-user-overview&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-86d9ed44"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-86d9ed44"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Qvf1-PIg5jRRNSLkzxg0Nw-544a26aef090.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-86d9ed44"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Qvf1-PIg5jRRNSLkzxg0Nw-544a26aef090.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;앱토스에서 생성중인 블록 [&lt;a href="https://apscan.io/blocks"&gt;https://apscan.io/blocks&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;앱토스는 블록에 반드시 투표 트랜잭션을 포함하기 때문에 최소 2개의 트랜잭션은 블록에 포함된다. 즉, 위 그림에서 실질적인 유저 트랜잭션은 1개에 불과하다. 멀티체인 내러티브를 가진 코스모스 생태계 지표를 보면, 얼마나 많은 블록들이 비어 있는 채로 생성되고 있는지 직관적으로 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6bbf1386"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6bbf1386"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VKf_Y3SXuTDtSUL1p8_tOA-47148e2aff41.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6bbf1386"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-VKf_Y3SXuTDtSUL1p8_tOA-47148e2aff41.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Stride 체인에서 생성중인 비어있는 블록 [&lt;a href="https://www.mintscan.io/stride/blocks"&gt;https://www.mintscan.io/stride/blocks&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;이 시장에서 대장격인 이더리움 또한, 일간 활성 계정 수가 평균 10만~20만개로, 2년전인 2020년 수준으로 회귀하였다. 그 동안, 이더리움은 여러번의 업데이트를 통해 블록의 Gas Limit을 3000만 수준으로 확장하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-be580939"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-be580939"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-kYgbXo9xmjtQwisHkLUNHQ-a651546e7bd7.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-be580939"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-kYgbXo9xmjtQwisHkLUNHQ-a651546e7bd7.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이더리움의 활성 계정 수 [&lt;a href="https://etherscan.io/chart/active-address"&gt;https://etherscan.io/chart/active-address&lt;/a&gt;]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;활성 계정 수를 다루는 차트는 보낸 이와 받은 이 주소를 모두 포함해서 더블 카운팅 하고 있다. 또한 화이트리스트를 딴다거나 에어드랍을 받기위해 다 계정을 쓰는 블록체인의 전통 문화를 감안하면 실제 유저는 절반에도 못 미칠 것이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위에서 나열한 현상들은 우리에게 블록체인 수요는 감소하고 공급은 늘어나고 있다는 것을 시사한다. 넘치는 공급에도 불구하고 지금 또 다른 L1/L2 블록체인들이 출시를 앞두고 있다. 왜 우리는 애플리케이션 개발에는 외면한 채 인프라 확장에만 집중하고 있을까? 불필요한 인프라를 개발하기 위해 많은 금액과 시간이 소모되고 있지만, 블록체인 개발 커뮤니티가 이 문제에 대해 침묵하고 있는 이유가 무엇일까?&lt;/p&gt;
&lt;p&gt;모두가 공감할 수 있는 정답은 아니겠지만, 필자의 생각으로는 블록 공간에 대한 수요는 없어도 메인넷 코인에 대한 수요는 있기 때문일 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-6da86b80"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-6da86b80"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NhJztHbUNi7RtMBRw0nj0g-d58fde49635a.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-6da86b80"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-NhJztHbUNi7RtMBRw0nj0g-d58fde49635a.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;업비트의 앱토스 가격 차트 — 블록 공간에 대한 수요는 없어도 메인넷 코인수요는 존재한다.&lt;/p&gt;
&lt;p&gt;소프트웨어를 만드는 사람으로써 우리의 목표는 사용자의 필요를 충족시킬 수 있는 제품을 만드는 것이다. 아무리 잘 만든 소프트웨어를 개발해도 사용자들이 필요로 하지 않는다면 외면받을 수 밖에 없다. 실리콘밸리의 전설이자 Y 콤비네이터의 창업자 폴 그레이엄은 성공적인 스타트업의 조건으로 “사용자들이 정말로 원하는 것을 만들어라” 라고 조언한다. 인프라에 대한 수요는 없고 메인넷 코인에 대한 수요만 존재하는 지금의 현실이 소프트웨어 개발자인 우리들에게 이야기 해주는 비극적인 주제는 단 한가지이다. 우리는 철저히 실패하고 있다.&lt;/p&gt;
&lt;h3 id="기술-발전의-장벽"&gt;기술 발전의 장벽&lt;/h3&gt;
&lt;p&gt;1903년 12월 17일 라이트 형제가 동력비행에 성공하면서 비행기의 시대가 열렸고, 1864년 맥스웰이 전파의 존재를 주장한 이래 1901년 최초로 대서양을 횡단하는 무선통신을 성공하면서 인터넷 발달의 기반을 다졌다. 메르세데스-벤츠의 공동 창립자 카를 벤츠(Karl Benz)는 1886년 최초의 가솔린 자동차를 발명하였다. 이렇게 나열하고 보면 현대 사회에서 누리는 많은 기술들은 약 120~150여년 전에 시작되었다. 그 당시를 살고 있던 사람들은 산업혁명시기와 겹쳐서 급격하게 바뀌는 삶을 보면서 미래가 급진적으로 바뀔 것이라고 상상했을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-416850e8"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-416850e8"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Wmx1wvXO7iKXvixdkaxWNg-a320de146a80.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-416850e8"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-Wmx1wvXO7iKXvixdkaxWNg-a320de146a80.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.boredpanda.com/french-artists-imagine-future-19th-century-postcards/?utm_source=google&amp;amp;utm_medium=organic&amp;amp;utm_campaign=organic"&gt;과거 사람들이 상상한 미래&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;사실 실제 세상은 과거 사람들이 생각했던 것 보다 훨씬 정적이다. 내가 역사 발전의 중간에 서 있어서 무딘 걸지는 몰라도 세상과 나 자신에 대한 의식을 자각하기 시작한 시절부터 지금까지 큰 변화는 느껴지지 않는다. 기술의 발전은 초기에 급격하게 일어나지만 무언가 장벽이 있어서 성장 곡선이 사실은 지수 함수가 아니라 로그 함수 였던 건 아닐까? ( 레이 커즈와일은 《특이점이 온다》에서 기술의 발전은 가속도가 붙는다고 주장했다. )&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;우리는 빛 보다 빠른 물체가 없음을 안다. 그래서 빛보다 빨리 한국에서 미국으로 정보를 전송할 수는 없기 때문에 레이턴시는 필연적으로 일정 시간 이상 존재한다. 트랜지스터의 크기는 더 이상 작아지기 힘들기 때문에 코어 수를 늘리는 멀티 코어 컴퓨팅이 현재 대세로 자리잡았다. &lt;a href="https://news.einfomax.co.kr/news/articleView.html?idxno=4236096"&gt;(삼전이 1나노 반도체를 양산하겠다고 선언하면서 이 한계를 다시 한 번 넘으려고 한다. 이 전자과 괴물들..)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;사르트르의 소설책 《구토》의 주인공 로캉탱은 누군가 물수제비를 하는 모습을 보고 본인도 물수제비를 하고자 조약돌을 들고선 문득 구토를 할 것 같은 매스꺼움을 느꼈다. 존재에 대해 필연성을 부여하던 시대에 살던 주인공은 어떤 인과적 이유없이 우연히 존재하는 조약돌을 쥐고는 세상의 과잉에 대해 자각하며 매스꺼움을 느낀다. 이 글을 쓰고 있는 나도 요즘 몇몇 블록체인 기술들의 존재가 필연적이 아니라는 생각을 하기 시작하면서 매스꺼움을 느끼고 있다. 블록체인 인프라도 과잉이다.&lt;/p&gt;
&lt;p&gt;토마스 쿤은 과학자들이 세상을 바라보는 어떤 틀에 갇혀서 연구하고 있다고 생각했으며 이를 《과학혁명의 구조》 에서 &lt;strong&gt;패러다임&lt;/strong&gt;이라고 불렀다. 토마스 쿤은 과학의 패러다임이 변화되는 과정을 정상과학 – 위기 – 혁명의 세 단계로 나누었다. 필자가 생각하기에는 web 3.0 을 위해 블록체인을 이용한다는 패러다임은 위기 단계에 있다. 패러다임 전환을 통해 큰 혁명이 있어야 한다고 생각한다. 아마 새로운 패러다임은 지금의 &lt;strong&gt;“블록체인”&lt;/strong&gt;과 같은 구조는 아닐 것이다. 필자가 생각하는 근거를 한 번 알아보자.&lt;/p&gt;
&lt;h3 id="다중-리더-복제-구조"&gt;다중 리더 복제 구조&lt;/h3&gt;
&lt;p&gt;블록체인은 데이터베이스와 동일하게 해석하면 다중 리더의 복제 데이터베이스 구조라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-a89f16e0"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-a89f16e0"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UL4lkrHvIcaOog5HEif6-Q-2b415c6a7b35.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-a89f16e0"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-UL4lkrHvIcaOog5HEif6-Q-2b415c6a7b35.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;단일 리더 복제와 다중 리더 복제의 모습&lt;/p&gt;
&lt;p&gt;흔히 Web 2.0라고 불리는 플랫폼에서는 대규모 스케일의 데이터를 처리하기 위해서 단일 리더 구조의 데이터베이스를 많이 사용하며 사용자에게는 복제 지연에 따른 데이터 불일치를 숨기는 방향으로 개발한다. 예를 들어, 미국에서 트위터를 하고 있는 Alice의 트윗이 실제로 한국에 살고 있는 나에게 까지 도달하는데는 필연적인 레이턴시가 존재한다. 플랫폼은 이 사실을 숨기기 위해 실제 트윗을 작성한 당사자인 “Alice” 에게는 본인이 쓴 내용은 항상 보여주고 한국에 있는 “나”는 잠시 후에 복제되어서 온 데이터를 받아보며 Alice와 나를 동기화 한다&lt;/p&gt;
&lt;p&gt;블록체인과 비슷한 다중 리더 복제 구조는 데이터가 안정화 되기 까지의 시간 등의 문제로 인해 중요한 데이터에 대해서는 사용하기 어렵다. 예를 들어, 동일한 데이터에 대한 쓰기가 여러 리더에 요청으로 동시에 도착할 때 최종적인 데이터가 서로 다르게 수렴하지 않도록 이 충돌을 어떤 방식으로든 해소해야 한다. 보통은 쓰기 요청에 대해 단조 시계를 붙여서 더 높은 버전의 데이터로 덮어쓰는 최종 쓰기 승리 (Last Write Win)이라는 전략을 사용한다.&lt;/p&gt;
&lt;p&gt;블록체인에서는 이 쓰기 충돌을 해소하기 위해 분기 선택 규칙을 사용한다. 비트코인은 작업증명으로 탄생한 블록들 중에서 가장 긴 블록을 선택하고 초기 이더리움은 블록 트리 중에서 가장 무거운 서브 트리를 선택하는 방식으로 쓰기 충돌을 해소하며, 전체 트랜잭션에 대한 줄을 세운다.&lt;/p&gt;
&lt;h3 id="충돌-회피"&gt;충돌 회피&lt;/h3&gt;
&lt;p&gt;실제로는 다중 리더 복제 DB에서는 데이터에 충돌 외에도 서로 인과 관계가 일치하지 않는다거나, 시간이 갑자기 이동한다거나 하는 등의 문제가 발생할 수 있다. 그래서 글로벌 스케일의 시스템이 아니라면, 대부분 이 충돌 자체를 문제 삼지 않게 하기 위해 단일 리더의 복제 시스템을 이용한다. 블록체인에서도 마찬가지로 한 시점에 여러 리더가 존재할 수 있는 구조라면 이 충돌을 해소하기 까지 많은 시간이 걸리기 때문에 사용자 경험에 매우 좋지 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-44a0f644"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-44a0f644"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-i4MO0VBioThrEFzuNk2Lgw-14bb50b30ae8.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-44a0f644"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-i4MO0VBioThrEFzuNk2Lgw-14bb50b30ae8.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;현재까지 알려진 블록이 최신인줄 알았는데 실제로는 더 무거운 서브 트리가 나타나면 모든 노드는 그 데이터를 최종적으로 따라야 한다. &lt;strong&gt;이렇게 현재까지의 진실로 알려진 블록이 갑자기 지워지고 새로운 블록을 채택하는 현상을 Block Reorg 라고 부른다.&lt;/strong&gt; 폴리곤 메인넷은 하루에도 크고 작은 Reorg가 발생하고 있다. 그래서 거래소들은 데이터가 충분히 모든 노드에 수렴할 때 까지 기다리면서 사용자의 트랜잭션을 컨펌한다.&lt;/p&gt;
&lt;p&gt;PBFT 에서는 이런 데이터가 즉각 컨펌되게 하기 위해 블록을 생성할 때마다 1/3 + 1이상의 노드가 투표에 참여해야 한다. 많은 네트워크 트래픽을 요구하기 때문에 성능이 매우 느리고 리더의 숫자를 무한정 늘릴 수 없다는 제약이 있다. 몇몇 블록체인들은 BFT계열의 레이턴시 증가 문제를 해결하기 위해 한 순간에 존재할 수 있는 리더의 개수를 소수로 제한하는 알고리즘을 사용한다. 알고랜드는 VRF를 통해 리더를 단 한명으로 선출하고 아발란체의 &lt;a href="https://www.youtube.com/watch?v=-nC42_sOqhQ&amp;amp;ab_channel=Avalanche"&gt;Snowman++&lt;/a&gt; 에서는 전체 검증자 중에서 일부를 사전에 선택한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-02524c45"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-02524c45"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-h7Zx20yNwmM5W3ys6awRMw-c3b480785981.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-02524c45"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-h7Zx20yNwmM5W3ys6awRMw-c3b480785981.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="http://www.yes24.com/Product/Goods/59566585?pid=123487&amp;amp;cosemkid=go16406746660905354&amp;amp;gclid=CjwKCAiAoL6eBhA3EiwAXDom5nn_qbO-DHHqf_pVJpiAPxXRZuI5ALyRmc_6ZJSBL8F452HpGuxePRoC_mIQAvD_BwE"&gt;도서 : 데이터 중심 애플리케이션 설계&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;인과성을 해치는 요청의 경우 파티션 키를 잡아서 동일한 파티션에 밀어 넣는 것으로 문제를 해결할 수 있지만, 중요한 비즈니스를 개발할 때 하나의 키로만 순서를 맞추기에는 인과성 문제를 모두 해결하기 어려운 케이스가 있다. &lt;a href="https://www.youtube.com/watch?v=v9rcKpUZw4o"&gt;그래서 은행처럼 중요한 트랜잭션을 다루는 시스템은 단일 데이터베이스로 해결하는 경우가 흔하다.&lt;/a&gt; — 토스 | Slash 2022&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="낙관적-동시성"&gt;낙관적 동시성&lt;/h3&gt;
&lt;p&gt;대규모 트랜잭션 처리에 쓰이는 낙관적 동시성의 기본적인 아이디어는 다수의 트랜잭션이 서로 충돌하지 않을 것이라고 가정하고 병렬로 처리한 다음, 만약 충돌이 생기면 그 때 해소하겠다는 전략을 취한다. 이는 실제로 트랜잭션이 충돌하지 않으면 순서대로 줄을 세워서 처리하는 것 보다 더 높은 처리량을 기록할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-d12d8912"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-d12d8912"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CchdxNLdIahWiV0NVpokqA-a17f92558315.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-d12d8912"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-CchdxNLdIahWiV0NVpokqA-a17f92558315.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;미국에서 발생한 송금과 한국에서 발생한 송금은 병렬로 처리해도 충돌하지 않을 것이다.&lt;/p&gt;
&lt;p&gt;실제로는 충돌이 자주 발생할수록 처리량이 낮아진다. 그래서 전통적인 데이터베이스에서는 고립 수준(Isolation Level)을 통해 일련의 트랜잭션 집합 안에서 충돌이 발생하는 정도와 순차적으로 처리되는 트랜잭션의 비율을 결정한다.&lt;/p&gt;
&lt;p&gt;일부 블록체인들은 이런 낙관적 동시성 아이디어를 도입해서 처리량을 높이고자 한다. 앱토스는 BlockSTM을 통해 낙관적 동시성을 구현했다. Solana는 스마트 컨트랙트를 작성할 때 어카운트 라는 이름으로 모든 종속된 데이터를 트랜잭션에 포함해서 넘기고 Sealevel VM에서 트랜잭션 사이의 종속 관계를 사전에 파악하고 병렬 처리할 건 병렬처리 하고, 순차처리 해야하는 건 순차처리하는 재밌는 방식을 사용한다.&lt;/p&gt;
&lt;p&gt;하지만 서술했듯이 실제로 데이터 충돌이 자주 발생하면 결국 전체 순서를 맞춰서 순차 처리하는 이더리움과 비교해서 더 좋을게 없다. 아니, 더 나쁘다. 실제 인터넷 세상에서는 구글,네이버,카카오,아마존,넷플릭스 처럼 하나의 플랫폼이 모든 트래픽을 점유하는 승자독식구조이다. 블록체인 세상이라고 사는 사람이 달라진게 아니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;모든 사람이 유니스왑을 통해 거래한다면 트랜잭션은 병렬처리 될 수 없다. 낙관적 동시성을 도입했다는 블록체인은 실제로는 알고리즘 보다는 참여가능한 검증자의 수준을 높여서 성능 혜택을 봤다.&lt;/strong&gt; 솔라나는 검증자가 되기위해 128GB의 메모리와 12개의 CPU코어 이상을 권장한다. 그래서 앱토스, 솔라나, 스이 등이 아무리 TPS 자랑을 해도 현실적으로는 3000~4000 TPS 정도로 수렴하지 않을까 생각한다.&lt;/p&gt;
&lt;h3 id="stateless-blockchain"&gt;Stateless Blockchain&lt;/h3&gt;
&lt;p&gt;모든 리더에 데이터가 복제되어야 한다는 블록체인 패러다임의 문제를 회피하고자, 각 노드들은 전체 데이터를 저장하는 대신 글로벌 상태에 대해 작은 증명(Commitment)만 업로드 하는 무상태 블록체인(Stateless Blockchain)을 제안하기도 했다.&lt;/p&gt;
&lt;p&gt;하지만, 지금까지 알려진 모든 stateless 블록체인들은 유저가 제출한 증명이 너무 자주 무효화 되기 때문에 유저가 지속적으로 블록체인을 관찰해야 한다는 문제점이 있다. 이더리움, 솔라나를 쓰는 우리는 오프라인 상태에서 외부 활동을 하더라도 우리의 자산 데이터가 그대로 저장되어 있지만 이런 암호학적 stateless 블록체인은 항상 인터넷에 연결하면서 주기적으로 데이터를 가져오고 증명을 업데이트 해야한다.&lt;/p&gt;
&lt;h3 id="제안"&gt;제안&lt;/h3&gt;
&lt;p&gt;지금까지 두 가지 이야기를 했다. 소프트웨어 개발자로써 우리들은 사용자의 요구를 충족하는데 실패하고 있다는 주장과 블록체인 패러다임의 한계로 인해 성장할 수 있는 확장성은 제약이 존재한다고 주장했다. &lt;strong&gt;그래서 이곳에서 필자가 제안하는 블록체인의 활용처는 금융 및 자산화 플랫폼으로만 사용할 것을 제안한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;먼저, 인프라는 지금도 계속 수평확장되고 있지만 궁극적으로는 UI/UX 문제를 해결하지 못하고 있다. 다수의 블록체인이 공존하고 있는 멀티체인 패러다임이 주류로 잡는다고 하더라도 사용자들은 비동기로 자산이 이동하는 경험을 하기 때문에 복잡한 UI/UX와 어려운 접근성이 허들로 작용할 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-e77aebf2"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-e77aebf2"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dUOE1KWn0EeBxERDpy9Jww-1820e70cb7aa.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-e77aebf2"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-dUOE1KWn0EeBxERDpy9Jww-1820e70cb7aa.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;크롬에 지갑만 4개가 설치되어있다. 즐겨찾기 해둔 브릿지만 해도 몇 개인가?&lt;/p&gt;
&lt;p&gt;이를 해결하려면 결국 단일 블록체인을 사용하며 고도화 하는 방향으로 개발해야 할 것이다. 하지만 성장할 수 있는 확장성에는 제한이 있기 때문에 궁극적으로 블록체인을 ‘덜’ 사용하는 방향으로 개발하는 것이 바람직하다. 탈중앙화된 소셜 미디어를 구현할 때 모든 로직을 스마트 컨트랙트로 구현하는 것은 낭비다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-b7eb8d26"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-b7eb8d26"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4goUGsg0tEaKOXRPsXhVQg-1b6bad3738dd.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-b7eb8d26"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-4goUGsg0tEaKOXRPsXhVQg-1b6bad3738dd.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://brunch.co.kr/@jeffpaik/104"&gt;https://brunch.co.kr/@jeffpaik/104&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Web3.0은 자본의 소유권과 자본의 이동을 인터넷 세상에서 가능케 했다는 것에서 의의가 크다. 필자는 이 자본에 관한 용도 이외에 블록체인을 써야하는 정당성을 아직까지 납득하지 못했다. Steem 블록체인 처럼 실제 포스팅된 글을 블록에 올려서 저장하면서 블록 공간을 낭비할 필요가 없다고 생각한다.&lt;/p&gt;
&lt;p&gt;필자는 신뢰가 필요없는 시스템 보다는 사회에서 보증하는 신용을 더 신뢰한다. 이미 블록체인은 신뢰가 필요없는 시스템에서 해킹 당하고, 러그 당하면서 사람들의 신뢰를 잃었다. 오히려 법적으로 잘 통제된 기존 전통 금융이 더 믿음이 간다. 이를 부정한다면 계좌에 매달 들어오는 급여를 메타마스크로 옮겨야 할 것이다&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;자본이 접근하지 않았던 무형자산을 자본이 접근하기 쉽게 바꾸고, 서로를 모르는 두 당사자가 서로의 자산을 거래하고, 국경을 넘어서 송금하는 등 금융 플랫폼으로써의 블록체인이 활용될 때야 말로 그 존재 의미가 빛이 난다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 이야기에 대해 DSRV의 개발자이시자 Boom labs 코어 컨트리뷰터이신 Sigrid Jin님께서는 블록체인을 탈 중앙화 관점이 아니라, 비기술 섹터가 가지고 있던 영역을 소프트웨어로 가져오는 트로이목마로써 가능하다는 조언을 해주셨습니다. 감사합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-27e169e9"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-27e169e9"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LX0B9cMLwlqT08l8yC7RAQ-edf62c59a88b.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-27e169e9"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-LX0B9cMLwlqT08l8yC7RAQ-edf62c59a88b.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이런거 블록체인으로 만들지 마라, 국가적 돈낭비가 아닐 수 없다&lt;/p&gt;
&lt;h3 id="꼭-블록체인이-아니어도-된다"&gt;꼭 블록체인이 아니어도 된다&lt;/h3&gt;
&lt;p&gt;최근 미디엄이 오픈한 &lt;a href="https://me.dm/"&gt;me.dm&lt;/a&gt;을 보면서 이것이 web3.0이 나아가야 할 방향이구나 느꼈다. me.dm은 실제로는 &lt;a href="https://joinmastodon.org/"&gt;mastodon&lt;/a&gt; 이라는 오픈소스를 기반으로 동작한다. 다수의 서버들이 소셜 네트워크 서비스를 구성하는데 서로 다른 주체에 의해 운영되는 서버들이 ActivityPub 프로토콜 아래에서 정보를 교환하고 발견한다.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption image-zoom-container"
&gt;
 
 &lt;input
 type="checkbox"
 id="img-39383e1b"
 class="image-zoom-toggle"
 aria-label="放大图片："
 /&gt;
 &lt;label
 for="img-39383e1b"
 class="image-zoom-label"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-n_ZTK6Ysk587OPaL2iXgaA-e5b6246a677e.png"
 alt=""
 loading="lazy"
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;label
 for="img-39383e1b"
 class="image-zoom-overlay"
 &gt;
 &lt;img
 src="https://scalalang2.com/img/medium/1-n_ZTK6Ysk587OPaL2iXgaA-e5b6246a677e.png"
 alt=""
 class="zoomable-image"
 /&gt;
 &lt;/label&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://joinmastodon.org/"&gt;https://joinmastodon.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;실제로 me.dm에서 쓴 글은 &lt;a href="http://mastodon.social/"&gt;mastodon.social&lt;/a&gt; 에서 발견되고 그 역도 가능하다. 여기서는 “Social networking that’s not for sale. Stay in control of your own timeline” 라고 말한다 ! 그렇다. 탈중앙화는 꼭 합의에 형태로 구현되지 않아도 되며 꼭 완벽한 탈중앙화를 이룰 필요는 없다. 중요한 건 사용자들이 정말로 원하는 것을 제공한하는 거라고 생각한다. 사람들은 블록체인 버전의 트위터 보다 검열없는 타임라인과 광고없는 소셜미디어를 원하는 것 처럼 말이다.&lt;/p&gt;
&lt;p&gt;사람들은 진심으로 탈중앙화를 원하는게 아니라 자신의 자산이 안전하게 보관되는 것을 필요로 한다. 그 방법이 탈중앙화된 블록체인이 아니라 다른 중앙화된 방식으로 보관되어도 괜찮다고 생각한다. 은행에서 발행한 디지털 포인트를 자유롭게 현금화 가능하게 법이 허락하고 API를 통해 누구나 확장 프로그램을 만들 수 있게 했어도 지금과 같은 규모로 디지털 자산 시장은 성장했을 것이다.&lt;/p&gt;
&lt;p&gt;우리는 또 다른 블록체인의 출시와 메인넷 코인들의 펌핑과 덤핑보다 애플리케이션이 필요하다. 맨날 똑같은 기능의 이름만 다른 Dex, 대출, 스테이블 코인, nft 복사 붙여넣기 말고 실제 애플리케이션이 개발되길 기대한다. 그런 의미에서 필자는 그 어떤 서비스보다 STEPN과 Reddit이 블록체인에 기여한 점이 크다고 생각한다.&lt;/p&gt;</description></item></channel></rss>